@vishu1301/script-writing 0.4.2 → 0.4.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +384 -42
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +6 -2
- package/dist/index.d.ts +6 -2
- package/dist/index.js +365 -43
- package/dist/index.js.map +1 -1
- package/package.json +3 -1
package/dist/index.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { useState, useRef, useCallback, useMemo, useEffect } from 'react';
|
|
2
|
-
import { ArrowRightLeft, MessageCircle, Brackets, UserRound, Sparkles, Clapperboard, ArrowRight, User, ChevronRight, Save, FileDown, Cog } from 'lucide-react';
|
|
2
|
+
import { ArrowRightLeft, MessageCircle, Brackets, UserRound, Sparkles, Clapperboard, ArrowRight, User, ChevronRight, Upload, Save, FileDown, Cog } from 'lucide-react';
|
|
3
3
|
import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
|
|
4
|
+
import * as pdfjs from 'pdfjs-dist';
|
|
4
5
|
|
|
5
6
|
var __defProp = Object.defineProperty;
|
|
6
7
|
var __defProps = Object.defineProperties;
|
|
@@ -124,6 +125,99 @@ var blockStyles = {
|
|
|
124
125
|
}
|
|
125
126
|
}
|
|
126
127
|
};
|
|
128
|
+
pdfjs.GlobalWorkerOptions.workerSrc = `//unpkg.com/pdfjs-dist@${pdfjs.version}/build/pdf.worker.min.mjs`;
|
|
129
|
+
function PdfImporter({ onScriptImported, children }) {
|
|
130
|
+
const [isProcessing, setIsProcessing] = useState(false);
|
|
131
|
+
const [error, setError] = useState(null);
|
|
132
|
+
const fileInputRef = useRef(null);
|
|
133
|
+
const handleFileChange = async (event) => {
|
|
134
|
+
var _a;
|
|
135
|
+
const file = (_a = event.target.files) == null ? void 0 : _a[0];
|
|
136
|
+
if (!file) {
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
setIsProcessing(true);
|
|
140
|
+
setError(null);
|
|
141
|
+
try {
|
|
142
|
+
const arrayBuffer = await file.arrayBuffer();
|
|
143
|
+
const pdf = await pdfjs.getDocument(arrayBuffer).promise;
|
|
144
|
+
const processPage = async (pageNumber) => {
|
|
145
|
+
const page = await pdf.getPage(pageNumber);
|
|
146
|
+
const content = await page.getTextContent();
|
|
147
|
+
const items = content.items.filter((item) => "str" in item && item.str.trim().length > 0);
|
|
148
|
+
if (items.length === 0) return "";
|
|
149
|
+
const lines = [];
|
|
150
|
+
for (const item of items) {
|
|
151
|
+
let found = false;
|
|
152
|
+
for (const line of lines) {
|
|
153
|
+
if (Math.abs(line.y - item.transform[5]) < 5) {
|
|
154
|
+
line.items.push({ x: item.transform[4], text: item.str });
|
|
155
|
+
found = true;
|
|
156
|
+
break;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
if (!found) {
|
|
160
|
+
lines.push({ y: item.transform[5], items: [{ x: item.transform[4], text: item.str }] });
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
lines.sort((a, b) => b.y - a.y);
|
|
164
|
+
return lines.map((line) => {
|
|
165
|
+
line.items.sort((a, b) => a.x - b.x);
|
|
166
|
+
return line.items.map((item) => item.text).join(" ");
|
|
167
|
+
}).join("\n");
|
|
168
|
+
};
|
|
169
|
+
let title = "";
|
|
170
|
+
if (pdf.numPages > 0) {
|
|
171
|
+
title = await processPage(1);
|
|
172
|
+
}
|
|
173
|
+
let scriptContent = "";
|
|
174
|
+
for (let i = 2; i <= pdf.numPages; i++) {
|
|
175
|
+
scriptContent += await processPage(i) + "\n\n";
|
|
176
|
+
}
|
|
177
|
+
onScriptImported(title.trim(), scriptContent);
|
|
178
|
+
} catch (err) {
|
|
179
|
+
console.error("Error processing PDF:", err);
|
|
180
|
+
setError(err instanceof Error ? `Error processing PDF: ${err.message}` : "An unknown error occurred.");
|
|
181
|
+
} finally {
|
|
182
|
+
setIsProcessing(false);
|
|
183
|
+
if (event.target) {
|
|
184
|
+
event.target.value = "";
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
};
|
|
188
|
+
const handleClick = () => {
|
|
189
|
+
var _a;
|
|
190
|
+
(_a = fileInputRef.current) == null ? void 0 : _a.click();
|
|
191
|
+
};
|
|
192
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
193
|
+
/* @__PURE__ */ jsx(
|
|
194
|
+
"input",
|
|
195
|
+
{
|
|
196
|
+
ref: fileInputRef,
|
|
197
|
+
type: "file",
|
|
198
|
+
accept: "application/pdf",
|
|
199
|
+
onChange: handleFileChange,
|
|
200
|
+
disabled: isProcessing,
|
|
201
|
+
className: "hidden",
|
|
202
|
+
id: "pdf-importer-input"
|
|
203
|
+
}
|
|
204
|
+
),
|
|
205
|
+
/* @__PURE__ */ jsx(
|
|
206
|
+
"button",
|
|
207
|
+
{
|
|
208
|
+
onClick: handleClick,
|
|
209
|
+
disabled: isProcessing,
|
|
210
|
+
className: "flex items-center justify-center gap-2 w-auto px-4 h-12 rounded-full bg-zinc-950 text-white shadow-xl shadow-zinc-900/20 border border-white/10 hover:bg-zinc-800 hover:scale-105 active:scale-95 transition-all duration-300",
|
|
211
|
+
"aria-label": "Import Script from PDF",
|
|
212
|
+
children: isProcessing ? /* @__PURE__ */ jsx("span", { className: "text-sm font-semibold", children: "Processing..." }) : children
|
|
213
|
+
}
|
|
214
|
+
),
|
|
215
|
+
error && /* @__PURE__ */ jsxs("p", { className: "sr-only", children: [
|
|
216
|
+
"Error: ",
|
|
217
|
+
error
|
|
218
|
+
] })
|
|
219
|
+
] });
|
|
220
|
+
}
|
|
127
221
|
function ScreenplayEditorView({
|
|
128
222
|
blocks,
|
|
129
223
|
pages,
|
|
@@ -145,9 +239,11 @@ function ScreenplayEditorView({
|
|
|
145
239
|
handleKeyDown,
|
|
146
240
|
handleFocus,
|
|
147
241
|
handleBlur,
|
|
242
|
+
handleScriptImport,
|
|
148
243
|
onSave,
|
|
149
244
|
onSaveAsPdf,
|
|
150
|
-
onSyncWithCloud
|
|
245
|
+
onSyncWithCloud,
|
|
246
|
+
handleSceneNumberChange
|
|
151
247
|
}) {
|
|
152
248
|
const [isRulesOpen, setIsRulesOpen] = useState(false);
|
|
153
249
|
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
@@ -206,7 +302,26 @@ function ScreenplayEditorView({
|
|
|
206
302
|
className: `relative rounded-sm transition-all duration-200 outline-none ${focusedBlockId === block.id ? "bg-zinc-100/50" : "bg-transparent"}`,
|
|
207
303
|
children: block.type === "SCENE_HEADING" ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
208
304
|
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 px-4 py-1 bg-transparent", children: [
|
|
209
|
-
/* @__PURE__ */ jsx(
|
|
305
|
+
/* @__PURE__ */ jsx(
|
|
306
|
+
"input",
|
|
307
|
+
{
|
|
308
|
+
className: "absolute -left-16 top-2 w-12 text-right text-zinc-400 font-semibold select-none bg-transparent outline-none focus:ring-1 focus:ring-blue-400 rounded-sm",
|
|
309
|
+
spellCheck: false,
|
|
310
|
+
value: block.sceneNumber || "",
|
|
311
|
+
onChange: (e) => handleSceneNumberChange(
|
|
312
|
+
block.id,
|
|
313
|
+
e.target.value.toUpperCase()
|
|
314
|
+
),
|
|
315
|
+
onFocus: () => handleFocus(block.id),
|
|
316
|
+
onBlur: () => handleBlur(block.id),
|
|
317
|
+
onKeyDown: (e) => {
|
|
318
|
+
if (e.key === "Enter" || e.key === "Backspace") {
|
|
319
|
+
e.stopPropagation();
|
|
320
|
+
}
|
|
321
|
+
},
|
|
322
|
+
"aria-label": "Scene Number"
|
|
323
|
+
}
|
|
324
|
+
),
|
|
210
325
|
/* @__PURE__ */ jsxs(
|
|
211
326
|
"select",
|
|
212
327
|
{
|
|
@@ -399,6 +514,10 @@ function ScreenplayEditorView({
|
|
|
399
514
|
pageIndex
|
|
400
515
|
)) }),
|
|
401
516
|
/* @__PURE__ */ jsxs("div", { className: "fixed bottom-6 right-6 flex flex-col items-end gap-4 z-50", children: [
|
|
517
|
+
/* @__PURE__ */ jsx(PdfImporter, { onScriptImported: handleScriptImport, children: /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
518
|
+
/* @__PURE__ */ jsx(Upload, { className: "w-5 h-5" }),
|
|
519
|
+
/* @__PURE__ */ jsx("span", { className: "text-sm font-semibold", children: "Import" })
|
|
520
|
+
] }) }),
|
|
402
521
|
onSave && /* @__PURE__ */ jsxs(
|
|
403
522
|
"button",
|
|
404
523
|
{
|
|
@@ -501,6 +620,7 @@ function createNewBlock(type) {
|
|
|
501
620
|
if (type === "SCENE_HEADING") {
|
|
502
621
|
newBlock.sceneType = "INT.";
|
|
503
622
|
newBlock.timeOfDay = "DAY";
|
|
623
|
+
newBlock.sceneNumber = "";
|
|
504
624
|
} else if (type === "PARENTHETICAL") {
|
|
505
625
|
newBlock.text = "()";
|
|
506
626
|
}
|
|
@@ -508,12 +628,12 @@ function createNewBlock(type) {
|
|
|
508
628
|
}
|
|
509
629
|
function addBlockAfter(blocks, currentBlockId) {
|
|
510
630
|
const currentIndex = blocks.findIndex((b) => b.id === currentBlockId);
|
|
511
|
-
if (currentIndex === -1) {
|
|
512
|
-
|
|
513
|
-
}
|
|
514
|
-
const currentBlock = blocks[currentIndex];
|
|
515
|
-
const nextType = getNextBlockType(currentBlock.type);
|
|
631
|
+
if (currentIndex === -1) return { newBlocks: blocks, newBlockId: "" };
|
|
632
|
+
const nextType = getNextBlockType(blocks[currentIndex].type);
|
|
516
633
|
const newBlock = createNewBlock(nextType);
|
|
634
|
+
if (nextType === "SCENE_HEADING") {
|
|
635
|
+
newBlock.sceneNumber = generateNextSceneNumber(blocks, currentIndex);
|
|
636
|
+
}
|
|
517
637
|
const newBlocks = [
|
|
518
638
|
...blocks.slice(0, currentIndex + 1),
|
|
519
639
|
newBlock,
|
|
@@ -538,10 +658,150 @@ function deleteBlock(blocks, blockIdToDelete) {
|
|
|
538
658
|
function updateBlock(blocks, id, key, value) {
|
|
539
659
|
return blocks.map((b) => b.id === id ? __spreadProps(__spreadValues({}, b), { [key]: value }) : b);
|
|
540
660
|
}
|
|
661
|
+
var generateNextSceneNumber = (blocks, currentIndex) => {
|
|
662
|
+
const prevScenes = blocks.slice(0, currentIndex + 1).filter((b) => b.type === "SCENE_HEADING");
|
|
663
|
+
if (prevScenes.length === 0) return "1";
|
|
664
|
+
const lastScene = prevScenes[prevScenes.length - 1];
|
|
665
|
+
const lastNum = lastScene.sceneNumber || "1";
|
|
666
|
+
const match = lastNum.match(/^(\d+)([A-Z]*)$/);
|
|
667
|
+
if (!match) return "1";
|
|
668
|
+
const baseNumber = match[1];
|
|
669
|
+
const currentSuffix = match[2];
|
|
670
|
+
const scenesAfter = blocks.slice(currentIndex + 1).filter((b) => b.type === "SCENE_HEADING");
|
|
671
|
+
if (scenesAfter.length > 0) {
|
|
672
|
+
if (currentSuffix === "") {
|
|
673
|
+
return `${baseNumber}A`;
|
|
674
|
+
} else {
|
|
675
|
+
return `${baseNumber}${incrementChar(currentSuffix)}`;
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
return String(parseInt(baseNumber) + 1);
|
|
679
|
+
};
|
|
680
|
+
function incrementChar(text) {
|
|
681
|
+
if (text === "") return "A";
|
|
682
|
+
const lastChar = text.slice(-1);
|
|
683
|
+
const remaining = text.slice(0, -1);
|
|
684
|
+
if (lastChar === "Z") {
|
|
685
|
+
return incrementChar(remaining) + "A";
|
|
686
|
+
}
|
|
687
|
+
return remaining + String.fromCharCode(lastChar.charCodeAt(0) + 1);
|
|
688
|
+
}
|
|
541
689
|
function changeBlockType(blocks, id, newType) {
|
|
690
|
+
const currentIndex = blocks.findIndex((b) => b.id === id);
|
|
691
|
+
if (currentIndex === -1) return blocks;
|
|
692
|
+
const currentBlock = blocks[currentIndex];
|
|
542
693
|
const newBlock = createNewBlock(newType);
|
|
694
|
+
if (newType === "PARENTHETICAL") {
|
|
695
|
+
const cleanText = currentBlock.text.replace(/[()]/g, "");
|
|
696
|
+
newBlock.text = `(${cleanText})`;
|
|
697
|
+
} else {
|
|
698
|
+
newBlock.text = currentBlock.text;
|
|
699
|
+
}
|
|
700
|
+
if (newType === "SCENE_HEADING") {
|
|
701
|
+
newBlock.sceneNumber = generateNextSceneNumber(blocks, currentIndex);
|
|
702
|
+
newBlock.text = newBlock.text.toUpperCase();
|
|
703
|
+
}
|
|
543
704
|
return blocks.map((b) => b.id === id ? __spreadProps(__spreadValues({}, newBlock), { id: b.id }) : b);
|
|
544
705
|
}
|
|
706
|
+
function parseScreenplayText(content) {
|
|
707
|
+
const lines = content.split("\n");
|
|
708
|
+
const blocks = [];
|
|
709
|
+
let lastBlock = null;
|
|
710
|
+
let activeSpeaker = null;
|
|
711
|
+
for (let i = 0; i < lines.length; i++) {
|
|
712
|
+
let originalLine = lines[i];
|
|
713
|
+
let trimmedLine = lines[i].trim().replace(/\s+/g, " ");
|
|
714
|
+
const leadingSpaces = originalLine.search(/\S/);
|
|
715
|
+
if (lines[i].trim() === "") {
|
|
716
|
+
activeSpeaker = null;
|
|
717
|
+
continue;
|
|
718
|
+
}
|
|
719
|
+
const isPageNumber = /^\d+$/.test(trimmedLine) || /^(PAGE|pg\.?)\s?\d+$/i.test(trimmedLine) || /^[\d]+\.$/.test(trimmedLine);
|
|
720
|
+
if (trimmedLine.length === 0 || isPageNumber) continue;
|
|
721
|
+
let currentBlockType = null;
|
|
722
|
+
const isAllUpperCase = trimmedLine === trimmedLine.toUpperCase() && /[A-Z]/.test(trimmedLine);
|
|
723
|
+
const sceneHeadingStartRegex = /^(?:\d+[A-Z]?\.?\s*)?(INT\.?\/EXT\.?|I\/E|INT|EXT|EST\.)\b/i;
|
|
724
|
+
const isTransition = isAllUpperCase && (trimmedLine.endsWith(" TO:") || ["FADE IN:", "FADE OUT.", "CUT TO BLACK."].includes(trimmedLine));
|
|
725
|
+
if (sceneHeadingStartRegex.test(trimmedLine)) {
|
|
726
|
+
currentBlockType = "SCENE_HEADING";
|
|
727
|
+
activeSpeaker = null;
|
|
728
|
+
} else if (isTransition) {
|
|
729
|
+
currentBlockType = "TRANSITION";
|
|
730
|
+
activeSpeaker = null;
|
|
731
|
+
} else if (isAllUpperCase && !trimmedLine.startsWith("(")) {
|
|
732
|
+
let nextLine = "";
|
|
733
|
+
for (let j = i + 1; j < lines.length; j++) {
|
|
734
|
+
const nextTrimmed = lines[j].trim();
|
|
735
|
+
const nextIsPage = /^\d+$/.test(nextTrimmed) || /^(PAGE|pg\.?)\s?\d+$/i.test(nextTrimmed);
|
|
736
|
+
if (nextTrimmed.length > 0 && !nextIsPage) {
|
|
737
|
+
nextLine = nextTrimmed;
|
|
738
|
+
break;
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
if (nextLine && (nextLine.startsWith("(") || nextLine !== nextLine.toUpperCase()) || /\s\(.*\)$/.test(trimmedLine)) {
|
|
742
|
+
currentBlockType = "CHARACTER";
|
|
743
|
+
activeSpeaker = trimmedLine;
|
|
744
|
+
} else {
|
|
745
|
+
currentBlockType = "ACTION";
|
|
746
|
+
activeSpeaker = null;
|
|
747
|
+
}
|
|
748
|
+
} else if (trimmedLine.startsWith("(") && trimmedLine.endsWith(")")) {
|
|
749
|
+
currentBlockType = "PARENTHETICAL";
|
|
750
|
+
} else if (activeSpeaker && ((lastBlock == null ? void 0 : lastBlock.type) === "CHARACTER" || (lastBlock == null ? void 0 : lastBlock.type) === "PARENTHETICAL" || (lastBlock == null ? void 0 : lastBlock.type) === "DIALOGUE")) {
|
|
751
|
+
const isLastLineComplete = /[.!?]"?$/.test(lastBlock.text.trim());
|
|
752
|
+
if (leadingSpaces < 15 && (lastBlock == null ? void 0 : lastBlock.type) !== "CHARACTER" && isLastLineComplete) {
|
|
753
|
+
currentBlockType = "ACTION";
|
|
754
|
+
activeSpeaker = null;
|
|
755
|
+
} else {
|
|
756
|
+
currentBlockType = "DIALOGUE";
|
|
757
|
+
}
|
|
758
|
+
} else {
|
|
759
|
+
currentBlockType = "ACTION";
|
|
760
|
+
activeSpeaker = null;
|
|
761
|
+
}
|
|
762
|
+
if (lastBlock && lastBlock.type === currentBlockType && (currentBlockType === "ACTION" || currentBlockType === "DIALOGUE") && !(currentBlockType === "DIALOGUE" && !activeSpeaker)) {
|
|
763
|
+
lastBlock.text += " " + trimmedLine;
|
|
764
|
+
} else {
|
|
765
|
+
const newBlock = createNewBlock(currentBlockType);
|
|
766
|
+
if (currentBlockType === "SCENE_HEADING") {
|
|
767
|
+
let workingLine = trimmedLine;
|
|
768
|
+
const sceneNumMatch = workingLine.match(/^(\d+[A-Z]*)\.?\s+/i);
|
|
769
|
+
if (sceneNumMatch) {
|
|
770
|
+
newBlock.sceneNumber = String(sceneNumMatch[1]).toUpperCase();
|
|
771
|
+
workingLine = workingLine.replace(/^(\d+[A-Z]*)\.?\s+/i, "").trim();
|
|
772
|
+
} else {
|
|
773
|
+
newBlock.sceneNumber = "";
|
|
774
|
+
}
|
|
775
|
+
const typeMatch = workingLine.match(
|
|
776
|
+
/^(INT\.?\/EXT\.?|I\/E|INT|EXT|EST\.)/i
|
|
777
|
+
);
|
|
778
|
+
if (typeMatch) {
|
|
779
|
+
let sType = typeMatch[0].toUpperCase().replace(/[^A-Z/.]/g, "");
|
|
780
|
+
if (!sType.endsWith(".")) sType += ".";
|
|
781
|
+
newBlock.sceneType = sType;
|
|
782
|
+
workingLine = workingLine.substring(typeMatch[0].length).trim();
|
|
783
|
+
}
|
|
784
|
+
const suffixRegex = /[.\-\s]+(DAY|NIGHT|CONTINUOUS|LATER|MORNING|EVENING|DUSK|DAWN|MORN|AFT|SUNSET|SUNRISE)(?:\s+.*)*$/i;
|
|
785
|
+
const suffixMatch = workingLine.match(suffixRegex);
|
|
786
|
+
if (suffixMatch) {
|
|
787
|
+
const rawTime = suffixMatch[1].toUpperCase();
|
|
788
|
+
const nightKeys = ["NIGHT", "EVENING", "DUSK", "SUNSET"];
|
|
789
|
+
newBlock.timeOfDay = nightKeys.includes(rawTime) ? "NIGHT" : "DAY";
|
|
790
|
+
workingLine = workingLine.substring(0, suffixMatch.index).trim();
|
|
791
|
+
} else {
|
|
792
|
+
workingLine = workingLine.replace(/\s+\d+(\s+\d+)*$/, "").trim();
|
|
793
|
+
newBlock.timeOfDay = "DAY";
|
|
794
|
+
}
|
|
795
|
+
newBlock.text = workingLine.replace(/^[-.\s]+/, "").replace(/[-.\s]+$/, "").toUpperCase();
|
|
796
|
+
} else {
|
|
797
|
+
newBlock.text = trimmedLine;
|
|
798
|
+
}
|
|
799
|
+
blocks.push(newBlock);
|
|
800
|
+
lastBlock = newBlock;
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
return blocks.length > 0 ? blocks : [createNewBlock("SCENE_HEADING")];
|
|
804
|
+
}
|
|
545
805
|
|
|
546
806
|
// app/hook/use-screenplay-editor.ts
|
|
547
807
|
var initialBlocks = [
|
|
@@ -549,6 +809,7 @@ var initialBlocks = [
|
|
|
549
809
|
id: uuid(),
|
|
550
810
|
type: "SCENE_HEADING",
|
|
551
811
|
text: "",
|
|
812
|
+
sceneNumber: "1",
|
|
552
813
|
sceneType: "INT.",
|
|
553
814
|
timeOfDay: "DAY"
|
|
554
815
|
}
|
|
@@ -627,6 +888,16 @@ function useScreenplayEditor() {
|
|
|
627
888
|
() => ["(V.O.)", "(O.S.)", "(O.C.)", "(SUBTITLE)", "(CONT'D)"],
|
|
628
889
|
[]
|
|
629
890
|
);
|
|
891
|
+
const handleSceneNumberChange = useCallback(
|
|
892
|
+
(id, newNumber) => {
|
|
893
|
+
setBlocks(
|
|
894
|
+
(prevBlocks) => prevBlocks.map(
|
|
895
|
+
(block) => block.id === id ? __spreadProps(__spreadValues({}, block), { sceneNumber: newNumber.toUpperCase() }) : block
|
|
896
|
+
)
|
|
897
|
+
);
|
|
898
|
+
},
|
|
899
|
+
[]
|
|
900
|
+
);
|
|
630
901
|
const locations = useMemo(() => {
|
|
631
902
|
const locs = blocks.filter((b) => b.type === "SCENE_HEADING" && b.text.trim() !== "").map((b) => b.text.trim().toUpperCase());
|
|
632
903
|
return [...new Set(locs)];
|
|
@@ -644,15 +915,32 @@ function useScreenplayEditor() {
|
|
|
644
915
|
}, [blocks]);
|
|
645
916
|
const sceneNumbers = useMemo(() => {
|
|
646
917
|
const map = {};
|
|
647
|
-
let
|
|
918
|
+
let fallbackCount = 0;
|
|
648
919
|
blocks.forEach((block) => {
|
|
649
920
|
if (block.type === "SCENE_HEADING") {
|
|
650
|
-
|
|
651
|
-
|
|
921
|
+
if (block.sceneNumber) {
|
|
922
|
+
map[block.id] = block.sceneNumber;
|
|
923
|
+
const base = parseInt(block.sceneNumber);
|
|
924
|
+
if (!isNaN(base)) fallbackCount = Math.max(fallbackCount, base);
|
|
925
|
+
} else {
|
|
926
|
+
fallbackCount++;
|
|
927
|
+
map[block.id] = String(fallbackCount);
|
|
928
|
+
}
|
|
652
929
|
}
|
|
653
930
|
});
|
|
654
931
|
return map;
|
|
655
932
|
}, [blocks]);
|
|
933
|
+
useCallback(() => {
|
|
934
|
+
let count = 1;
|
|
935
|
+
setBlocks(
|
|
936
|
+
(prev) => prev.map((b) => {
|
|
937
|
+
if (b.type === "SCENE_HEADING") {
|
|
938
|
+
return __spreadProps(__spreadValues({}, b), { sceneNumber: String(count++) });
|
|
939
|
+
}
|
|
940
|
+
return b;
|
|
941
|
+
})
|
|
942
|
+
);
|
|
943
|
+
}, []);
|
|
656
944
|
useEffect(() => {
|
|
657
945
|
if (newBlockId && refs.current[newBlockId]) {
|
|
658
946
|
const block = blocks.find((b) => b.id === newBlockId);
|
|
@@ -673,11 +961,13 @@ function useScreenplayEditor() {
|
|
|
673
961
|
useEffect(() => {
|
|
674
962
|
blocks.forEach((block) => {
|
|
675
963
|
const element = refs.current[block.id];
|
|
676
|
-
if (element
|
|
677
|
-
element.innerText
|
|
964
|
+
if (element) {
|
|
965
|
+
if (element.innerText !== block.text && document.activeElement !== element) {
|
|
966
|
+
element.innerText = block.text;
|
|
967
|
+
}
|
|
678
968
|
}
|
|
679
969
|
});
|
|
680
|
-
}, [blocks
|
|
970
|
+
}, [blocks]);
|
|
681
971
|
useEffect(() => {
|
|
682
972
|
const handleClickOutside = (e) => {
|
|
683
973
|
const target = e.target;
|
|
@@ -857,17 +1147,15 @@ function useScreenplayEditor() {
|
|
|
857
1147
|
const el = refs.current[focusedBlockId];
|
|
858
1148
|
if (el) {
|
|
859
1149
|
el.focus();
|
|
860
|
-
const
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
setCaretPosition(el,
|
|
864
|
-
} else {
|
|
865
|
-
setCaretPosition(el, newBlock.text.length);
|
|
1150
|
+
const currentBlock = blocks.find((b) => b.id === focusedBlockId);
|
|
1151
|
+
if (currentBlock) {
|
|
1152
|
+
const pos = newType === "PARENTHETICAL" ? el.innerText.length - 1 : el.innerText.length;
|
|
1153
|
+
setCaretPosition(el, Math.max(0, pos));
|
|
866
1154
|
}
|
|
867
1155
|
}
|
|
868
|
-
},
|
|
1156
|
+
}, 10);
|
|
869
1157
|
},
|
|
870
|
-
[focusedBlockId]
|
|
1158
|
+
[focusedBlockId, blocks]
|
|
871
1159
|
);
|
|
872
1160
|
const handleSelectCharacterExtension = useCallback(
|
|
873
1161
|
(extension) => {
|
|
@@ -923,8 +1211,6 @@ function useScreenplayEditor() {
|
|
|
923
1211
|
const el = refs.current[id];
|
|
924
1212
|
if (el) {
|
|
925
1213
|
el.focus();
|
|
926
|
-
const newBlock = createNewBlock(newType);
|
|
927
|
-
el.innerText = newBlock.text;
|
|
928
1214
|
if (newType === "PARENTHETICAL") {
|
|
929
1215
|
setCaretPosition(el, 1);
|
|
930
1216
|
} else {
|
|
@@ -1055,28 +1341,62 @@ function useScreenplayEditor() {
|
|
|
1055
1341
|
},
|
|
1056
1342
|
[blocks, handleBlockTextChange]
|
|
1057
1343
|
);
|
|
1058
|
-
const
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1344
|
+
const handleScriptImport = useCallback(
|
|
1345
|
+
(title, content) => {
|
|
1346
|
+
const parsedBlocks = parseScreenplayText(content);
|
|
1347
|
+
if (parsedBlocks.length > 0) {
|
|
1348
|
+
let fallbackCount = 1;
|
|
1349
|
+
const finalizedBlocks = parsedBlocks.map((block) => {
|
|
1350
|
+
if (block.type === "SCENE_HEADING") {
|
|
1351
|
+
if (block.sceneNumber && block.sceneNumber.trim().length > 0) {
|
|
1352
|
+
const isPureNumber = /^\d+$/.test(block.sceneNumber);
|
|
1353
|
+
if (isPureNumber) {
|
|
1354
|
+
fallbackCount = parseInt(block.sceneNumber) + 1;
|
|
1355
|
+
}
|
|
1356
|
+
return block;
|
|
1357
|
+
}
|
|
1358
|
+
return __spreadProps(__spreadValues({}, block), { sceneNumber: String(fallbackCount++) });
|
|
1359
|
+
}
|
|
1360
|
+
return block;
|
|
1361
|
+
});
|
|
1362
|
+
setBlocks(finalizedBlocks);
|
|
1363
|
+
setTimeout(() => {
|
|
1364
|
+
var _a;
|
|
1365
|
+
const firstId = parsedBlocks[0].id;
|
|
1366
|
+
if (firstId && refs.current[firstId]) {
|
|
1367
|
+
setFocusedBlockId(firstId);
|
|
1368
|
+
(_a = refs.current[firstId]) == null ? void 0 : _a.focus();
|
|
1369
|
+
}
|
|
1370
|
+
}, 100);
|
|
1371
|
+
}
|
|
1372
|
+
},
|
|
1373
|
+
[refs]
|
|
1374
|
+
);
|
|
1375
|
+
const handleFocus = useCallback(
|
|
1376
|
+
(id) => {
|
|
1377
|
+
if (blurTimeout.current) {
|
|
1378
|
+
clearTimeout(blurTimeout.current);
|
|
1379
|
+
}
|
|
1380
|
+
setFocusedBlockId(id);
|
|
1381
|
+
const block = blocks.find((b) => b.id === id);
|
|
1382
|
+
if ((block == null ? void 0 : block.type) === "CHARACTER") {
|
|
1383
|
+
const trimmedText = block.text.trim();
|
|
1384
|
+
const openParenIndex = trimmedText.lastIndexOf("(");
|
|
1385
|
+
const closeParenIndex = trimmedText.lastIndexOf(")");
|
|
1386
|
+
if (openParenIndex !== -1 && openParenIndex > closeParenIndex) {
|
|
1387
|
+
setShowExtensionSuggestions(true);
|
|
1388
|
+
setShowSuggestions(false);
|
|
1389
|
+
} else {
|
|
1390
|
+
setShowExtensionSuggestions(false);
|
|
1391
|
+
setShowSuggestions(openParenIndex === -1);
|
|
1392
|
+
}
|
|
1071
1393
|
} else {
|
|
1394
|
+
setShowSuggestions(true);
|
|
1072
1395
|
setShowExtensionSuggestions(false);
|
|
1073
|
-
setShowSuggestions(openParenIndex === -1);
|
|
1074
1396
|
}
|
|
1075
|
-
}
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
}
|
|
1079
|
-
}, [blocks]);
|
|
1397
|
+
},
|
|
1398
|
+
[blocks]
|
|
1399
|
+
);
|
|
1080
1400
|
const handleBlur = useCallback((id) => {
|
|
1081
1401
|
if (document.activeElement === refs.current[id]) return;
|
|
1082
1402
|
blurTimeout.current = setTimeout(() => {
|
|
@@ -1103,6 +1423,8 @@ function useScreenplayEditor() {
|
|
|
1103
1423
|
handleBlockTypeChange,
|
|
1104
1424
|
handleSelectCharacterExtension,
|
|
1105
1425
|
handleKeyDown,
|
|
1426
|
+
handleScriptImport,
|
|
1427
|
+
handleSceneNumberChange,
|
|
1106
1428
|
handleFocus,
|
|
1107
1429
|
handleBlur
|
|
1108
1430
|
};
|