@vishu1301/script-writing 0.4.1 → 0.4.3
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 +365 -30
- 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 +346 -31
- package/dist/index.js.map +1 -1
- package/package.json +3 -1
package/dist/index.cjs
CHANGED
|
@@ -3,6 +3,27 @@
|
|
|
3
3
|
var react = require('react');
|
|
4
4
|
var lucideReact = require('lucide-react');
|
|
5
5
|
var jsxRuntime = require('react/jsx-runtime');
|
|
6
|
+
var pdfjs = require('pdfjs-dist');
|
|
7
|
+
|
|
8
|
+
function _interopNamespace(e) {
|
|
9
|
+
if (e && e.__esModule) return e;
|
|
10
|
+
var n = Object.create(null);
|
|
11
|
+
if (e) {
|
|
12
|
+
Object.keys(e).forEach(function (k) {
|
|
13
|
+
if (k !== 'default') {
|
|
14
|
+
var d = Object.getOwnPropertyDescriptor(e, k);
|
|
15
|
+
Object.defineProperty(n, k, d.get ? d : {
|
|
16
|
+
enumerable: true,
|
|
17
|
+
get: function () { return e[k]; }
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
n.default = e;
|
|
23
|
+
return Object.freeze(n);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
var pdfjs__namespace = /*#__PURE__*/_interopNamespace(pdfjs);
|
|
6
27
|
|
|
7
28
|
var __defProp = Object.defineProperty;
|
|
8
29
|
var __defProps = Object.defineProperties;
|
|
@@ -75,7 +96,6 @@ var blockStyles = {
|
|
|
75
96
|
textAlign: "left",
|
|
76
97
|
marginLeft: "2.0in",
|
|
77
98
|
maxWidth: "4.0in",
|
|
78
|
-
fontWeight: 700,
|
|
79
99
|
letterSpacing: "0.1em",
|
|
80
100
|
outline: "none",
|
|
81
101
|
whiteSpace: "pre-wrap",
|
|
@@ -127,6 +147,99 @@ var blockStyles = {
|
|
|
127
147
|
}
|
|
128
148
|
}
|
|
129
149
|
};
|
|
150
|
+
pdfjs__namespace.GlobalWorkerOptions.workerSrc = `//unpkg.com/pdfjs-dist@${pdfjs__namespace.version}/build/pdf.worker.min.mjs`;
|
|
151
|
+
function PdfImporter({ onScriptImported, children }) {
|
|
152
|
+
const [isProcessing, setIsProcessing] = react.useState(false);
|
|
153
|
+
const [error, setError] = react.useState(null);
|
|
154
|
+
const fileInputRef = react.useRef(null);
|
|
155
|
+
const handleFileChange = async (event) => {
|
|
156
|
+
var _a;
|
|
157
|
+
const file = (_a = event.target.files) == null ? void 0 : _a[0];
|
|
158
|
+
if (!file) {
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
setIsProcessing(true);
|
|
162
|
+
setError(null);
|
|
163
|
+
try {
|
|
164
|
+
const arrayBuffer = await file.arrayBuffer();
|
|
165
|
+
const pdf = await pdfjs__namespace.getDocument(arrayBuffer).promise;
|
|
166
|
+
const processPage = async (pageNumber) => {
|
|
167
|
+
const page = await pdf.getPage(pageNumber);
|
|
168
|
+
const content = await page.getTextContent();
|
|
169
|
+
const items = content.items.filter((item) => "str" in item && item.str.trim().length > 0);
|
|
170
|
+
if (items.length === 0) return "";
|
|
171
|
+
const lines = [];
|
|
172
|
+
for (const item of items) {
|
|
173
|
+
let found = false;
|
|
174
|
+
for (const line of lines) {
|
|
175
|
+
if (Math.abs(line.y - item.transform[5]) < 5) {
|
|
176
|
+
line.items.push({ x: item.transform[4], text: item.str });
|
|
177
|
+
found = true;
|
|
178
|
+
break;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
if (!found) {
|
|
182
|
+
lines.push({ y: item.transform[5], items: [{ x: item.transform[4], text: item.str }] });
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
lines.sort((a, b) => b.y - a.y);
|
|
186
|
+
return lines.map((line) => {
|
|
187
|
+
line.items.sort((a, b) => a.x - b.x);
|
|
188
|
+
return line.items.map((item) => item.text).join(" ");
|
|
189
|
+
}).join("\n");
|
|
190
|
+
};
|
|
191
|
+
let title = "";
|
|
192
|
+
if (pdf.numPages > 0) {
|
|
193
|
+
title = await processPage(1);
|
|
194
|
+
}
|
|
195
|
+
let scriptContent = "";
|
|
196
|
+
for (let i = 2; i <= pdf.numPages; i++) {
|
|
197
|
+
scriptContent += await processPage(i) + "\n\n";
|
|
198
|
+
}
|
|
199
|
+
onScriptImported(title.trim(), scriptContent);
|
|
200
|
+
} catch (err) {
|
|
201
|
+
console.error("Error processing PDF:", err);
|
|
202
|
+
setError(err instanceof Error ? `Error processing PDF: ${err.message}` : "An unknown error occurred.");
|
|
203
|
+
} finally {
|
|
204
|
+
setIsProcessing(false);
|
|
205
|
+
if (event.target) {
|
|
206
|
+
event.target.value = "";
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
};
|
|
210
|
+
const handleClick = () => {
|
|
211
|
+
var _a;
|
|
212
|
+
(_a = fileInputRef.current) == null ? void 0 : _a.click();
|
|
213
|
+
};
|
|
214
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
215
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
216
|
+
"input",
|
|
217
|
+
{
|
|
218
|
+
ref: fileInputRef,
|
|
219
|
+
type: "file",
|
|
220
|
+
accept: "application/pdf",
|
|
221
|
+
onChange: handleFileChange,
|
|
222
|
+
disabled: isProcessing,
|
|
223
|
+
className: "hidden",
|
|
224
|
+
id: "pdf-importer-input"
|
|
225
|
+
}
|
|
226
|
+
),
|
|
227
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
228
|
+
"button",
|
|
229
|
+
{
|
|
230
|
+
onClick: handleClick,
|
|
231
|
+
disabled: isProcessing,
|
|
232
|
+
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",
|
|
233
|
+
"aria-label": "Import Script from PDF",
|
|
234
|
+
children: isProcessing ? /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-sm font-semibold", children: "Processing..." }) : children
|
|
235
|
+
}
|
|
236
|
+
),
|
|
237
|
+
error && /* @__PURE__ */ jsxRuntime.jsxs("p", { className: "sr-only", children: [
|
|
238
|
+
"Error: ",
|
|
239
|
+
error
|
|
240
|
+
] })
|
|
241
|
+
] });
|
|
242
|
+
}
|
|
130
243
|
function ScreenplayEditorView({
|
|
131
244
|
blocks,
|
|
132
245
|
pages,
|
|
@@ -148,9 +261,11 @@ function ScreenplayEditorView({
|
|
|
148
261
|
handleKeyDown,
|
|
149
262
|
handleFocus,
|
|
150
263
|
handleBlur,
|
|
264
|
+
handleScriptImport,
|
|
151
265
|
onSave,
|
|
152
266
|
onSaveAsPdf,
|
|
153
|
-
onSyncWithCloud
|
|
267
|
+
onSyncWithCloud,
|
|
268
|
+
handleSceneNumberChange
|
|
154
269
|
}) {
|
|
155
270
|
const [isRulesOpen, setIsRulesOpen] = react.useState(false);
|
|
156
271
|
return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
@@ -209,7 +324,26 @@ function ScreenplayEditorView({
|
|
|
209
324
|
className: `relative rounded-sm transition-all duration-200 outline-none ${focusedBlockId === block.id ? "bg-zinc-100/50" : "bg-transparent"}`,
|
|
210
325
|
children: block.type === "SCENE_HEADING" ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
211
326
|
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2 px-4 py-1 bg-transparent", children: [
|
|
212
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
327
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
328
|
+
"input",
|
|
329
|
+
{
|
|
330
|
+
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",
|
|
331
|
+
spellCheck: false,
|
|
332
|
+
value: block.sceneNumber || "",
|
|
333
|
+
onChange: (e) => handleSceneNumberChange(
|
|
334
|
+
block.id,
|
|
335
|
+
e.target.value.toUpperCase()
|
|
336
|
+
),
|
|
337
|
+
onFocus: () => handleFocus(block.id),
|
|
338
|
+
onBlur: () => handleBlur(block.id),
|
|
339
|
+
onKeyDown: (e) => {
|
|
340
|
+
if (e.key === "Enter" || e.key === "Backspace") {
|
|
341
|
+
e.stopPropagation();
|
|
342
|
+
}
|
|
343
|
+
},
|
|
344
|
+
"aria-label": "Scene Number"
|
|
345
|
+
}
|
|
346
|
+
),
|
|
213
347
|
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
214
348
|
"select",
|
|
215
349
|
{
|
|
@@ -402,6 +536,10 @@ function ScreenplayEditorView({
|
|
|
402
536
|
pageIndex
|
|
403
537
|
)) }),
|
|
404
538
|
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "fixed bottom-6 right-6 flex flex-col items-end gap-4 z-50", children: [
|
|
539
|
+
/* @__PURE__ */ jsxRuntime.jsx(PdfImporter, { onScriptImported: handleScriptImport, children: /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
540
|
+
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.Upload, { className: "w-5 h-5" }),
|
|
541
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-sm font-semibold", children: "Import" })
|
|
542
|
+
] }) }),
|
|
405
543
|
onSave && /* @__PURE__ */ jsxRuntime.jsxs(
|
|
406
544
|
"button",
|
|
407
545
|
{
|
|
@@ -504,6 +642,7 @@ function createNewBlock(type) {
|
|
|
504
642
|
if (type === "SCENE_HEADING") {
|
|
505
643
|
newBlock.sceneType = "INT.";
|
|
506
644
|
newBlock.timeOfDay = "DAY";
|
|
645
|
+
newBlock.sceneNumber = "";
|
|
507
646
|
} else if (type === "PARENTHETICAL") {
|
|
508
647
|
newBlock.text = "()";
|
|
509
648
|
}
|
|
@@ -511,12 +650,12 @@ function createNewBlock(type) {
|
|
|
511
650
|
}
|
|
512
651
|
function addBlockAfter(blocks, currentBlockId) {
|
|
513
652
|
const currentIndex = blocks.findIndex((b) => b.id === currentBlockId);
|
|
514
|
-
if (currentIndex === -1) {
|
|
515
|
-
|
|
516
|
-
}
|
|
517
|
-
const currentBlock = blocks[currentIndex];
|
|
518
|
-
const nextType = getNextBlockType(currentBlock.type);
|
|
653
|
+
if (currentIndex === -1) return { newBlocks: blocks, newBlockId: "" };
|
|
654
|
+
const nextType = getNextBlockType(blocks[currentIndex].type);
|
|
519
655
|
const newBlock = createNewBlock(nextType);
|
|
656
|
+
if (nextType === "SCENE_HEADING") {
|
|
657
|
+
newBlock.sceneNumber = generateNextSceneNumber(blocks, currentIndex);
|
|
658
|
+
}
|
|
520
659
|
const newBlocks = [
|
|
521
660
|
...blocks.slice(0, currentIndex + 1),
|
|
522
661
|
newBlock,
|
|
@@ -541,10 +680,142 @@ function deleteBlock(blocks, blockIdToDelete) {
|
|
|
541
680
|
function updateBlock(blocks, id, key, value) {
|
|
542
681
|
return blocks.map((b) => b.id === id ? __spreadProps(__spreadValues({}, b), { [key]: value }) : b);
|
|
543
682
|
}
|
|
683
|
+
var generateNextSceneNumber = (blocks, currentIndex) => {
|
|
684
|
+
const prevScenes = blocks.slice(0, currentIndex + 1).filter((b) => b.type === "SCENE_HEADING");
|
|
685
|
+
if (prevScenes.length === 0) return "1";
|
|
686
|
+
const lastScene = prevScenes[prevScenes.length - 1];
|
|
687
|
+
const lastNum = lastScene.sceneNumber || "1";
|
|
688
|
+
const match = lastNum.match(/^(\d+)([A-Z]*)$/);
|
|
689
|
+
if (!match) return "1";
|
|
690
|
+
const baseNumber = match[1];
|
|
691
|
+
const currentSuffix = match[2];
|
|
692
|
+
const scenesAfter = blocks.slice(currentIndex + 1).filter((b) => b.type === "SCENE_HEADING");
|
|
693
|
+
if (scenesAfter.length > 0) {
|
|
694
|
+
if (currentSuffix === "") {
|
|
695
|
+
return `${baseNumber}A`;
|
|
696
|
+
} else {
|
|
697
|
+
return `${baseNumber}${incrementChar(currentSuffix)}`;
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
return String(parseInt(baseNumber) + 1);
|
|
701
|
+
};
|
|
702
|
+
function incrementChar(text) {
|
|
703
|
+
if (text === "") return "A";
|
|
704
|
+
const lastChar = text.slice(-1);
|
|
705
|
+
const remaining = text.slice(0, -1);
|
|
706
|
+
if (lastChar === "Z") {
|
|
707
|
+
return incrementChar(remaining) + "A";
|
|
708
|
+
}
|
|
709
|
+
return remaining + String.fromCharCode(lastChar.charCodeAt(0) + 1);
|
|
710
|
+
}
|
|
544
711
|
function changeBlockType(blocks, id, newType) {
|
|
712
|
+
const currentIndex = blocks.findIndex((b) => b.id === id);
|
|
713
|
+
if (currentIndex === -1) return blocks;
|
|
545
714
|
const newBlock = createNewBlock(newType);
|
|
715
|
+
if (newType === "SCENE_HEADING") {
|
|
716
|
+
newBlock.sceneNumber = generateNextSceneNumber(blocks, currentIndex);
|
|
717
|
+
}
|
|
546
718
|
return blocks.map((b) => b.id === id ? __spreadProps(__spreadValues({}, newBlock), { id: b.id }) : b);
|
|
547
719
|
}
|
|
720
|
+
function parseScreenplayText(content) {
|
|
721
|
+
const lines = content.split("\n");
|
|
722
|
+
const blocks = [];
|
|
723
|
+
let lastBlock = null;
|
|
724
|
+
let activeSpeaker = null;
|
|
725
|
+
for (let i = 0; i < lines.length; i++) {
|
|
726
|
+
let originalLine = lines[i];
|
|
727
|
+
let trimmedLine = lines[i].trim().replace(/\s+/g, " ");
|
|
728
|
+
const leadingSpaces = originalLine.search(/\S/);
|
|
729
|
+
if (lines[i].trim() === "") {
|
|
730
|
+
activeSpeaker = null;
|
|
731
|
+
continue;
|
|
732
|
+
}
|
|
733
|
+
const isPageNumber = /^\d+$/.test(trimmedLine) || /^(PAGE|pg\.?)\s?\d+$/i.test(trimmedLine) || /^[\d]+\.$/.test(trimmedLine);
|
|
734
|
+
if (trimmedLine.length === 0 || isPageNumber) continue;
|
|
735
|
+
let currentBlockType = null;
|
|
736
|
+
const isAllUpperCase = trimmedLine === trimmedLine.toUpperCase() && /[A-Z]/.test(trimmedLine);
|
|
737
|
+
const sceneHeadingStartRegex = /^(?:\d+[A-Z]?\.?\s*)?(INT\.?\/EXT\.?|I\/E|INT|EXT|EST\.)\b/i;
|
|
738
|
+
const isTransition = isAllUpperCase && (trimmedLine.endsWith(" TO:") || ["FADE IN:", "FADE OUT.", "CUT TO BLACK."].includes(trimmedLine));
|
|
739
|
+
if (sceneHeadingStartRegex.test(trimmedLine)) {
|
|
740
|
+
currentBlockType = "SCENE_HEADING";
|
|
741
|
+
activeSpeaker = null;
|
|
742
|
+
} else if (isTransition) {
|
|
743
|
+
currentBlockType = "TRANSITION";
|
|
744
|
+
activeSpeaker = null;
|
|
745
|
+
} else if (isAllUpperCase && !trimmedLine.startsWith("(")) {
|
|
746
|
+
let nextLine = "";
|
|
747
|
+
for (let j = i + 1; j < lines.length; j++) {
|
|
748
|
+
const nextTrimmed = lines[j].trim();
|
|
749
|
+
const nextIsPage = /^\d+$/.test(nextTrimmed) || /^(PAGE|pg\.?)\s?\d+$/i.test(nextTrimmed);
|
|
750
|
+
if (nextTrimmed.length > 0 && !nextIsPage) {
|
|
751
|
+
nextLine = nextTrimmed;
|
|
752
|
+
break;
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
if (nextLine && (nextLine.startsWith("(") || nextLine !== nextLine.toUpperCase()) || /\s\(.*\)$/.test(trimmedLine)) {
|
|
756
|
+
currentBlockType = "CHARACTER";
|
|
757
|
+
activeSpeaker = trimmedLine;
|
|
758
|
+
} else {
|
|
759
|
+
currentBlockType = "ACTION";
|
|
760
|
+
activeSpeaker = null;
|
|
761
|
+
}
|
|
762
|
+
} else if (trimmedLine.startsWith("(") && trimmedLine.endsWith(")")) {
|
|
763
|
+
currentBlockType = "PARENTHETICAL";
|
|
764
|
+
} else if (activeSpeaker && ((lastBlock == null ? void 0 : lastBlock.type) === "CHARACTER" || (lastBlock == null ? void 0 : lastBlock.type) === "PARENTHETICAL" || (lastBlock == null ? void 0 : lastBlock.type) === "DIALOGUE")) {
|
|
765
|
+
const isLastLineComplete = /[.!?]"?$/.test(lastBlock.text.trim());
|
|
766
|
+
if (leadingSpaces < 15 && (lastBlock == null ? void 0 : lastBlock.type) !== "CHARACTER" && isLastLineComplete) {
|
|
767
|
+
currentBlockType = "ACTION";
|
|
768
|
+
activeSpeaker = null;
|
|
769
|
+
} else {
|
|
770
|
+
currentBlockType = "DIALOGUE";
|
|
771
|
+
}
|
|
772
|
+
} else {
|
|
773
|
+
currentBlockType = "ACTION";
|
|
774
|
+
activeSpeaker = null;
|
|
775
|
+
}
|
|
776
|
+
if (lastBlock && lastBlock.type === currentBlockType && (currentBlockType === "ACTION" || currentBlockType === "DIALOGUE") && !(currentBlockType === "DIALOGUE" && !activeSpeaker)) {
|
|
777
|
+
lastBlock.text += " " + trimmedLine;
|
|
778
|
+
} else {
|
|
779
|
+
const newBlock = createNewBlock(currentBlockType);
|
|
780
|
+
if (currentBlockType === "SCENE_HEADING") {
|
|
781
|
+
let workingLine = trimmedLine;
|
|
782
|
+
const sceneNumMatch = workingLine.match(/^(\d+[A-Z]*)\.?\s+/i);
|
|
783
|
+
if (sceneNumMatch) {
|
|
784
|
+
newBlock.sceneNumber = String(sceneNumMatch[1]).toUpperCase();
|
|
785
|
+
workingLine = workingLine.replace(/^(\d+[A-Z]*)\.?\s+/i, "").trim();
|
|
786
|
+
} else {
|
|
787
|
+
newBlock.sceneNumber = "";
|
|
788
|
+
}
|
|
789
|
+
const typeMatch = workingLine.match(
|
|
790
|
+
/^(INT\.?\/EXT\.?|I\/E|INT|EXT|EST\.)/i
|
|
791
|
+
);
|
|
792
|
+
if (typeMatch) {
|
|
793
|
+
let sType = typeMatch[0].toUpperCase().replace(/[^A-Z/.]/g, "");
|
|
794
|
+
if (!sType.endsWith(".")) sType += ".";
|
|
795
|
+
newBlock.sceneType = sType;
|
|
796
|
+
workingLine = workingLine.substring(typeMatch[0].length).trim();
|
|
797
|
+
}
|
|
798
|
+
const suffixRegex = /[.\-\s]+(DAY|NIGHT|CONTINUOUS|LATER|MORNING|EVENING|DUSK|DAWN|MORN|AFT|SUNSET|SUNRISE)(?:\s+.*)*$/i;
|
|
799
|
+
const suffixMatch = workingLine.match(suffixRegex);
|
|
800
|
+
if (suffixMatch) {
|
|
801
|
+
const rawTime = suffixMatch[1].toUpperCase();
|
|
802
|
+
const nightKeys = ["NIGHT", "EVENING", "DUSK", "SUNSET"];
|
|
803
|
+
newBlock.timeOfDay = nightKeys.includes(rawTime) ? "NIGHT" : "DAY";
|
|
804
|
+
workingLine = workingLine.substring(0, suffixMatch.index).trim();
|
|
805
|
+
} else {
|
|
806
|
+
workingLine = workingLine.replace(/\s+\d+(\s+\d+)*$/, "").trim();
|
|
807
|
+
newBlock.timeOfDay = "DAY";
|
|
808
|
+
}
|
|
809
|
+
newBlock.text = workingLine.replace(/^[-.\s]+/, "").replace(/[-.\s]+$/, "").toUpperCase();
|
|
810
|
+
} else {
|
|
811
|
+
newBlock.text = trimmedLine;
|
|
812
|
+
}
|
|
813
|
+
blocks.push(newBlock);
|
|
814
|
+
lastBlock = newBlock;
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
return blocks.length > 0 ? blocks : [createNewBlock("SCENE_HEADING")];
|
|
818
|
+
}
|
|
548
819
|
|
|
549
820
|
// app/hook/use-screenplay-editor.ts
|
|
550
821
|
var initialBlocks = [
|
|
@@ -552,6 +823,7 @@ var initialBlocks = [
|
|
|
552
823
|
id: uuid(),
|
|
553
824
|
type: "SCENE_HEADING",
|
|
554
825
|
text: "",
|
|
826
|
+
sceneNumber: "1",
|
|
555
827
|
sceneType: "INT.",
|
|
556
828
|
timeOfDay: "DAY"
|
|
557
829
|
}
|
|
@@ -630,6 +902,16 @@ function useScreenplayEditor() {
|
|
|
630
902
|
() => ["(V.O.)", "(O.S.)", "(O.C.)", "(SUBTITLE)", "(CONT'D)"],
|
|
631
903
|
[]
|
|
632
904
|
);
|
|
905
|
+
const handleSceneNumberChange = react.useCallback(
|
|
906
|
+
(id, newNumber) => {
|
|
907
|
+
setBlocks(
|
|
908
|
+
(prevBlocks) => prevBlocks.map(
|
|
909
|
+
(block) => block.id === id ? __spreadProps(__spreadValues({}, block), { sceneNumber: newNumber.toUpperCase() }) : block
|
|
910
|
+
)
|
|
911
|
+
);
|
|
912
|
+
},
|
|
913
|
+
[]
|
|
914
|
+
);
|
|
633
915
|
const locations = react.useMemo(() => {
|
|
634
916
|
const locs = blocks.filter((b) => b.type === "SCENE_HEADING" && b.text.trim() !== "").map((b) => b.text.trim().toUpperCase());
|
|
635
917
|
return [...new Set(locs)];
|
|
@@ -647,15 +929,32 @@ function useScreenplayEditor() {
|
|
|
647
929
|
}, [blocks]);
|
|
648
930
|
const sceneNumbers = react.useMemo(() => {
|
|
649
931
|
const map = {};
|
|
650
|
-
let
|
|
932
|
+
let fallbackCount = 0;
|
|
651
933
|
blocks.forEach((block) => {
|
|
652
934
|
if (block.type === "SCENE_HEADING") {
|
|
653
|
-
|
|
654
|
-
|
|
935
|
+
if (block.sceneNumber) {
|
|
936
|
+
map[block.id] = block.sceneNumber;
|
|
937
|
+
const base = parseInt(block.sceneNumber);
|
|
938
|
+
if (!isNaN(base)) fallbackCount = Math.max(fallbackCount, base);
|
|
939
|
+
} else {
|
|
940
|
+
fallbackCount++;
|
|
941
|
+
map[block.id] = String(fallbackCount);
|
|
942
|
+
}
|
|
655
943
|
}
|
|
656
944
|
});
|
|
657
945
|
return map;
|
|
658
946
|
}, [blocks]);
|
|
947
|
+
react.useCallback(() => {
|
|
948
|
+
let count = 1;
|
|
949
|
+
setBlocks(
|
|
950
|
+
(prev) => prev.map((b) => {
|
|
951
|
+
if (b.type === "SCENE_HEADING") {
|
|
952
|
+
return __spreadProps(__spreadValues({}, b), { sceneNumber: String(count++) });
|
|
953
|
+
}
|
|
954
|
+
return b;
|
|
955
|
+
})
|
|
956
|
+
);
|
|
957
|
+
}, []);
|
|
659
958
|
react.useEffect(() => {
|
|
660
959
|
if (newBlockId && refs.current[newBlockId]) {
|
|
661
960
|
const block = blocks.find((b) => b.id === newBlockId);
|
|
@@ -1058,28 +1357,62 @@ function useScreenplayEditor() {
|
|
|
1058
1357
|
},
|
|
1059
1358
|
[blocks, handleBlockTextChange]
|
|
1060
1359
|
);
|
|
1061
|
-
const
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1360
|
+
const handleScriptImport = react.useCallback(
|
|
1361
|
+
(title, content) => {
|
|
1362
|
+
const parsedBlocks = parseScreenplayText(content);
|
|
1363
|
+
if (parsedBlocks.length > 0) {
|
|
1364
|
+
let fallbackCount = 1;
|
|
1365
|
+
const finalizedBlocks = parsedBlocks.map((block) => {
|
|
1366
|
+
if (block.type === "SCENE_HEADING") {
|
|
1367
|
+
if (block.sceneNumber && block.sceneNumber.trim().length > 0) {
|
|
1368
|
+
const isPureNumber = /^\d+$/.test(block.sceneNumber);
|
|
1369
|
+
if (isPureNumber) {
|
|
1370
|
+
fallbackCount = parseInt(block.sceneNumber) + 1;
|
|
1371
|
+
}
|
|
1372
|
+
return block;
|
|
1373
|
+
}
|
|
1374
|
+
return __spreadProps(__spreadValues({}, block), { sceneNumber: String(fallbackCount++) });
|
|
1375
|
+
}
|
|
1376
|
+
return block;
|
|
1377
|
+
});
|
|
1378
|
+
setBlocks(finalizedBlocks);
|
|
1379
|
+
setTimeout(() => {
|
|
1380
|
+
var _a;
|
|
1381
|
+
const firstId = parsedBlocks[0].id;
|
|
1382
|
+
if (firstId && refs.current[firstId]) {
|
|
1383
|
+
setFocusedBlockId(firstId);
|
|
1384
|
+
(_a = refs.current[firstId]) == null ? void 0 : _a.focus();
|
|
1385
|
+
}
|
|
1386
|
+
}, 100);
|
|
1387
|
+
}
|
|
1388
|
+
},
|
|
1389
|
+
[refs]
|
|
1390
|
+
);
|
|
1391
|
+
const handleFocus = react.useCallback(
|
|
1392
|
+
(id) => {
|
|
1393
|
+
if (blurTimeout.current) {
|
|
1394
|
+
clearTimeout(blurTimeout.current);
|
|
1395
|
+
}
|
|
1396
|
+
setFocusedBlockId(id);
|
|
1397
|
+
const block = blocks.find((b) => b.id === id);
|
|
1398
|
+
if ((block == null ? void 0 : block.type) === "CHARACTER") {
|
|
1399
|
+
const trimmedText = block.text.trim();
|
|
1400
|
+
const openParenIndex = trimmedText.lastIndexOf("(");
|
|
1401
|
+
const closeParenIndex = trimmedText.lastIndexOf(")");
|
|
1402
|
+
if (openParenIndex !== -1 && openParenIndex > closeParenIndex) {
|
|
1403
|
+
setShowExtensionSuggestions(true);
|
|
1404
|
+
setShowSuggestions(false);
|
|
1405
|
+
} else {
|
|
1406
|
+
setShowExtensionSuggestions(false);
|
|
1407
|
+
setShowSuggestions(openParenIndex === -1);
|
|
1408
|
+
}
|
|
1074
1409
|
} else {
|
|
1410
|
+
setShowSuggestions(true);
|
|
1075
1411
|
setShowExtensionSuggestions(false);
|
|
1076
|
-
setShowSuggestions(openParenIndex === -1);
|
|
1077
1412
|
}
|
|
1078
|
-
}
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
}
|
|
1082
|
-
}, [blocks]);
|
|
1413
|
+
},
|
|
1414
|
+
[blocks]
|
|
1415
|
+
);
|
|
1083
1416
|
const handleBlur = react.useCallback((id) => {
|
|
1084
1417
|
if (document.activeElement === refs.current[id]) return;
|
|
1085
1418
|
blurTimeout.current = setTimeout(() => {
|
|
@@ -1106,6 +1439,8 @@ function useScreenplayEditor() {
|
|
|
1106
1439
|
handleBlockTypeChange,
|
|
1107
1440
|
handleSelectCharacterExtension,
|
|
1108
1441
|
handleKeyDown,
|
|
1442
|
+
handleScriptImport,
|
|
1443
|
+
handleSceneNumberChange,
|
|
1109
1444
|
handleFocus,
|
|
1110
1445
|
handleBlur
|
|
1111
1446
|
};
|