@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.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;
|
|
@@ -73,7 +74,6 @@ var blockStyles = {
|
|
|
73
74
|
textAlign: "left",
|
|
74
75
|
marginLeft: "2.0in",
|
|
75
76
|
maxWidth: "4.0in",
|
|
76
|
-
fontWeight: 700,
|
|
77
77
|
letterSpacing: "0.1em",
|
|
78
78
|
outline: "none",
|
|
79
79
|
whiteSpace: "pre-wrap",
|
|
@@ -125,6 +125,99 @@ var blockStyles = {
|
|
|
125
125
|
}
|
|
126
126
|
}
|
|
127
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
|
+
}
|
|
128
221
|
function ScreenplayEditorView({
|
|
129
222
|
blocks,
|
|
130
223
|
pages,
|
|
@@ -146,9 +239,11 @@ function ScreenplayEditorView({
|
|
|
146
239
|
handleKeyDown,
|
|
147
240
|
handleFocus,
|
|
148
241
|
handleBlur,
|
|
242
|
+
handleScriptImport,
|
|
149
243
|
onSave,
|
|
150
244
|
onSaveAsPdf,
|
|
151
|
-
onSyncWithCloud
|
|
245
|
+
onSyncWithCloud,
|
|
246
|
+
handleSceneNumberChange
|
|
152
247
|
}) {
|
|
153
248
|
const [isRulesOpen, setIsRulesOpen] = useState(false);
|
|
154
249
|
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
@@ -207,7 +302,26 @@ function ScreenplayEditorView({
|
|
|
207
302
|
className: `relative rounded-sm transition-all duration-200 outline-none ${focusedBlockId === block.id ? "bg-zinc-100/50" : "bg-transparent"}`,
|
|
208
303
|
children: block.type === "SCENE_HEADING" ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
209
304
|
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 px-4 py-1 bg-transparent", children: [
|
|
210
|
-
/* @__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
|
+
),
|
|
211
325
|
/* @__PURE__ */ jsxs(
|
|
212
326
|
"select",
|
|
213
327
|
{
|
|
@@ -400,6 +514,10 @@ function ScreenplayEditorView({
|
|
|
400
514
|
pageIndex
|
|
401
515
|
)) }),
|
|
402
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
|
+
] }) }),
|
|
403
521
|
onSave && /* @__PURE__ */ jsxs(
|
|
404
522
|
"button",
|
|
405
523
|
{
|
|
@@ -502,6 +620,7 @@ function createNewBlock(type) {
|
|
|
502
620
|
if (type === "SCENE_HEADING") {
|
|
503
621
|
newBlock.sceneType = "INT.";
|
|
504
622
|
newBlock.timeOfDay = "DAY";
|
|
623
|
+
newBlock.sceneNumber = "";
|
|
505
624
|
} else if (type === "PARENTHETICAL") {
|
|
506
625
|
newBlock.text = "()";
|
|
507
626
|
}
|
|
@@ -509,12 +628,12 @@ function createNewBlock(type) {
|
|
|
509
628
|
}
|
|
510
629
|
function addBlockAfter(blocks, currentBlockId) {
|
|
511
630
|
const currentIndex = blocks.findIndex((b) => b.id === currentBlockId);
|
|
512
|
-
if (currentIndex === -1) {
|
|
513
|
-
|
|
514
|
-
}
|
|
515
|
-
const currentBlock = blocks[currentIndex];
|
|
516
|
-
const nextType = getNextBlockType(currentBlock.type);
|
|
631
|
+
if (currentIndex === -1) return { newBlocks: blocks, newBlockId: "" };
|
|
632
|
+
const nextType = getNextBlockType(blocks[currentIndex].type);
|
|
517
633
|
const newBlock = createNewBlock(nextType);
|
|
634
|
+
if (nextType === "SCENE_HEADING") {
|
|
635
|
+
newBlock.sceneNumber = generateNextSceneNumber(blocks, currentIndex);
|
|
636
|
+
}
|
|
518
637
|
const newBlocks = [
|
|
519
638
|
...blocks.slice(0, currentIndex + 1),
|
|
520
639
|
newBlock,
|
|
@@ -539,10 +658,142 @@ function deleteBlock(blocks, blockIdToDelete) {
|
|
|
539
658
|
function updateBlock(blocks, id, key, value) {
|
|
540
659
|
return blocks.map((b) => b.id === id ? __spreadProps(__spreadValues({}, b), { [key]: value }) : b);
|
|
541
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
|
+
}
|
|
542
689
|
function changeBlockType(blocks, id, newType) {
|
|
690
|
+
const currentIndex = blocks.findIndex((b) => b.id === id);
|
|
691
|
+
if (currentIndex === -1) return blocks;
|
|
543
692
|
const newBlock = createNewBlock(newType);
|
|
693
|
+
if (newType === "SCENE_HEADING") {
|
|
694
|
+
newBlock.sceneNumber = generateNextSceneNumber(blocks, currentIndex);
|
|
695
|
+
}
|
|
544
696
|
return blocks.map((b) => b.id === id ? __spreadProps(__spreadValues({}, newBlock), { id: b.id }) : b);
|
|
545
697
|
}
|
|
698
|
+
function parseScreenplayText(content) {
|
|
699
|
+
const lines = content.split("\n");
|
|
700
|
+
const blocks = [];
|
|
701
|
+
let lastBlock = null;
|
|
702
|
+
let activeSpeaker = null;
|
|
703
|
+
for (let i = 0; i < lines.length; i++) {
|
|
704
|
+
let originalLine = lines[i];
|
|
705
|
+
let trimmedLine = lines[i].trim().replace(/\s+/g, " ");
|
|
706
|
+
const leadingSpaces = originalLine.search(/\S/);
|
|
707
|
+
if (lines[i].trim() === "") {
|
|
708
|
+
activeSpeaker = null;
|
|
709
|
+
continue;
|
|
710
|
+
}
|
|
711
|
+
const isPageNumber = /^\d+$/.test(trimmedLine) || /^(PAGE|pg\.?)\s?\d+$/i.test(trimmedLine) || /^[\d]+\.$/.test(trimmedLine);
|
|
712
|
+
if (trimmedLine.length === 0 || isPageNumber) continue;
|
|
713
|
+
let currentBlockType = null;
|
|
714
|
+
const isAllUpperCase = trimmedLine === trimmedLine.toUpperCase() && /[A-Z]/.test(trimmedLine);
|
|
715
|
+
const sceneHeadingStartRegex = /^(?:\d+[A-Z]?\.?\s*)?(INT\.?\/EXT\.?|I\/E|INT|EXT|EST\.)\b/i;
|
|
716
|
+
const isTransition = isAllUpperCase && (trimmedLine.endsWith(" TO:") || ["FADE IN:", "FADE OUT.", "CUT TO BLACK."].includes(trimmedLine));
|
|
717
|
+
if (sceneHeadingStartRegex.test(trimmedLine)) {
|
|
718
|
+
currentBlockType = "SCENE_HEADING";
|
|
719
|
+
activeSpeaker = null;
|
|
720
|
+
} else if (isTransition) {
|
|
721
|
+
currentBlockType = "TRANSITION";
|
|
722
|
+
activeSpeaker = null;
|
|
723
|
+
} else if (isAllUpperCase && !trimmedLine.startsWith("(")) {
|
|
724
|
+
let nextLine = "";
|
|
725
|
+
for (let j = i + 1; j < lines.length; j++) {
|
|
726
|
+
const nextTrimmed = lines[j].trim();
|
|
727
|
+
const nextIsPage = /^\d+$/.test(nextTrimmed) || /^(PAGE|pg\.?)\s?\d+$/i.test(nextTrimmed);
|
|
728
|
+
if (nextTrimmed.length > 0 && !nextIsPage) {
|
|
729
|
+
nextLine = nextTrimmed;
|
|
730
|
+
break;
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
if (nextLine && (nextLine.startsWith("(") || nextLine !== nextLine.toUpperCase()) || /\s\(.*\)$/.test(trimmedLine)) {
|
|
734
|
+
currentBlockType = "CHARACTER";
|
|
735
|
+
activeSpeaker = trimmedLine;
|
|
736
|
+
} else {
|
|
737
|
+
currentBlockType = "ACTION";
|
|
738
|
+
activeSpeaker = null;
|
|
739
|
+
}
|
|
740
|
+
} else if (trimmedLine.startsWith("(") && trimmedLine.endsWith(")")) {
|
|
741
|
+
currentBlockType = "PARENTHETICAL";
|
|
742
|
+
} else if (activeSpeaker && ((lastBlock == null ? void 0 : lastBlock.type) === "CHARACTER" || (lastBlock == null ? void 0 : lastBlock.type) === "PARENTHETICAL" || (lastBlock == null ? void 0 : lastBlock.type) === "DIALOGUE")) {
|
|
743
|
+
const isLastLineComplete = /[.!?]"?$/.test(lastBlock.text.trim());
|
|
744
|
+
if (leadingSpaces < 15 && (lastBlock == null ? void 0 : lastBlock.type) !== "CHARACTER" && isLastLineComplete) {
|
|
745
|
+
currentBlockType = "ACTION";
|
|
746
|
+
activeSpeaker = null;
|
|
747
|
+
} else {
|
|
748
|
+
currentBlockType = "DIALOGUE";
|
|
749
|
+
}
|
|
750
|
+
} else {
|
|
751
|
+
currentBlockType = "ACTION";
|
|
752
|
+
activeSpeaker = null;
|
|
753
|
+
}
|
|
754
|
+
if (lastBlock && lastBlock.type === currentBlockType && (currentBlockType === "ACTION" || currentBlockType === "DIALOGUE") && !(currentBlockType === "DIALOGUE" && !activeSpeaker)) {
|
|
755
|
+
lastBlock.text += " " + trimmedLine;
|
|
756
|
+
} else {
|
|
757
|
+
const newBlock = createNewBlock(currentBlockType);
|
|
758
|
+
if (currentBlockType === "SCENE_HEADING") {
|
|
759
|
+
let workingLine = trimmedLine;
|
|
760
|
+
const sceneNumMatch = workingLine.match(/^(\d+[A-Z]*)\.?\s+/i);
|
|
761
|
+
if (sceneNumMatch) {
|
|
762
|
+
newBlock.sceneNumber = String(sceneNumMatch[1]).toUpperCase();
|
|
763
|
+
workingLine = workingLine.replace(/^(\d+[A-Z]*)\.?\s+/i, "").trim();
|
|
764
|
+
} else {
|
|
765
|
+
newBlock.sceneNumber = "";
|
|
766
|
+
}
|
|
767
|
+
const typeMatch = workingLine.match(
|
|
768
|
+
/^(INT\.?\/EXT\.?|I\/E|INT|EXT|EST\.)/i
|
|
769
|
+
);
|
|
770
|
+
if (typeMatch) {
|
|
771
|
+
let sType = typeMatch[0].toUpperCase().replace(/[^A-Z/.]/g, "");
|
|
772
|
+
if (!sType.endsWith(".")) sType += ".";
|
|
773
|
+
newBlock.sceneType = sType;
|
|
774
|
+
workingLine = workingLine.substring(typeMatch[0].length).trim();
|
|
775
|
+
}
|
|
776
|
+
const suffixRegex = /[.\-\s]+(DAY|NIGHT|CONTINUOUS|LATER|MORNING|EVENING|DUSK|DAWN|MORN|AFT|SUNSET|SUNRISE)(?:\s+.*)*$/i;
|
|
777
|
+
const suffixMatch = workingLine.match(suffixRegex);
|
|
778
|
+
if (suffixMatch) {
|
|
779
|
+
const rawTime = suffixMatch[1].toUpperCase();
|
|
780
|
+
const nightKeys = ["NIGHT", "EVENING", "DUSK", "SUNSET"];
|
|
781
|
+
newBlock.timeOfDay = nightKeys.includes(rawTime) ? "NIGHT" : "DAY";
|
|
782
|
+
workingLine = workingLine.substring(0, suffixMatch.index).trim();
|
|
783
|
+
} else {
|
|
784
|
+
workingLine = workingLine.replace(/\s+\d+(\s+\d+)*$/, "").trim();
|
|
785
|
+
newBlock.timeOfDay = "DAY";
|
|
786
|
+
}
|
|
787
|
+
newBlock.text = workingLine.replace(/^[-.\s]+/, "").replace(/[-.\s]+$/, "").toUpperCase();
|
|
788
|
+
} else {
|
|
789
|
+
newBlock.text = trimmedLine;
|
|
790
|
+
}
|
|
791
|
+
blocks.push(newBlock);
|
|
792
|
+
lastBlock = newBlock;
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
return blocks.length > 0 ? blocks : [createNewBlock("SCENE_HEADING")];
|
|
796
|
+
}
|
|
546
797
|
|
|
547
798
|
// app/hook/use-screenplay-editor.ts
|
|
548
799
|
var initialBlocks = [
|
|
@@ -550,6 +801,7 @@ var initialBlocks = [
|
|
|
550
801
|
id: uuid(),
|
|
551
802
|
type: "SCENE_HEADING",
|
|
552
803
|
text: "",
|
|
804
|
+
sceneNumber: "1",
|
|
553
805
|
sceneType: "INT.",
|
|
554
806
|
timeOfDay: "DAY"
|
|
555
807
|
}
|
|
@@ -628,6 +880,16 @@ function useScreenplayEditor() {
|
|
|
628
880
|
() => ["(V.O.)", "(O.S.)", "(O.C.)", "(SUBTITLE)", "(CONT'D)"],
|
|
629
881
|
[]
|
|
630
882
|
);
|
|
883
|
+
const handleSceneNumberChange = useCallback(
|
|
884
|
+
(id, newNumber) => {
|
|
885
|
+
setBlocks(
|
|
886
|
+
(prevBlocks) => prevBlocks.map(
|
|
887
|
+
(block) => block.id === id ? __spreadProps(__spreadValues({}, block), { sceneNumber: newNumber.toUpperCase() }) : block
|
|
888
|
+
)
|
|
889
|
+
);
|
|
890
|
+
},
|
|
891
|
+
[]
|
|
892
|
+
);
|
|
631
893
|
const locations = useMemo(() => {
|
|
632
894
|
const locs = blocks.filter((b) => b.type === "SCENE_HEADING" && b.text.trim() !== "").map((b) => b.text.trim().toUpperCase());
|
|
633
895
|
return [...new Set(locs)];
|
|
@@ -645,15 +907,32 @@ function useScreenplayEditor() {
|
|
|
645
907
|
}, [blocks]);
|
|
646
908
|
const sceneNumbers = useMemo(() => {
|
|
647
909
|
const map = {};
|
|
648
|
-
let
|
|
910
|
+
let fallbackCount = 0;
|
|
649
911
|
blocks.forEach((block) => {
|
|
650
912
|
if (block.type === "SCENE_HEADING") {
|
|
651
|
-
|
|
652
|
-
|
|
913
|
+
if (block.sceneNumber) {
|
|
914
|
+
map[block.id] = block.sceneNumber;
|
|
915
|
+
const base = parseInt(block.sceneNumber);
|
|
916
|
+
if (!isNaN(base)) fallbackCount = Math.max(fallbackCount, base);
|
|
917
|
+
} else {
|
|
918
|
+
fallbackCount++;
|
|
919
|
+
map[block.id] = String(fallbackCount);
|
|
920
|
+
}
|
|
653
921
|
}
|
|
654
922
|
});
|
|
655
923
|
return map;
|
|
656
924
|
}, [blocks]);
|
|
925
|
+
useCallback(() => {
|
|
926
|
+
let count = 1;
|
|
927
|
+
setBlocks(
|
|
928
|
+
(prev) => prev.map((b) => {
|
|
929
|
+
if (b.type === "SCENE_HEADING") {
|
|
930
|
+
return __spreadProps(__spreadValues({}, b), { sceneNumber: String(count++) });
|
|
931
|
+
}
|
|
932
|
+
return b;
|
|
933
|
+
})
|
|
934
|
+
);
|
|
935
|
+
}, []);
|
|
657
936
|
useEffect(() => {
|
|
658
937
|
if (newBlockId && refs.current[newBlockId]) {
|
|
659
938
|
const block = blocks.find((b) => b.id === newBlockId);
|
|
@@ -1056,28 +1335,62 @@ function useScreenplayEditor() {
|
|
|
1056
1335
|
},
|
|
1057
1336
|
[blocks, handleBlockTextChange]
|
|
1058
1337
|
);
|
|
1059
|
-
const
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1338
|
+
const handleScriptImport = useCallback(
|
|
1339
|
+
(title, content) => {
|
|
1340
|
+
const parsedBlocks = parseScreenplayText(content);
|
|
1341
|
+
if (parsedBlocks.length > 0) {
|
|
1342
|
+
let fallbackCount = 1;
|
|
1343
|
+
const finalizedBlocks = parsedBlocks.map((block) => {
|
|
1344
|
+
if (block.type === "SCENE_HEADING") {
|
|
1345
|
+
if (block.sceneNumber && block.sceneNumber.trim().length > 0) {
|
|
1346
|
+
const isPureNumber = /^\d+$/.test(block.sceneNumber);
|
|
1347
|
+
if (isPureNumber) {
|
|
1348
|
+
fallbackCount = parseInt(block.sceneNumber) + 1;
|
|
1349
|
+
}
|
|
1350
|
+
return block;
|
|
1351
|
+
}
|
|
1352
|
+
return __spreadProps(__spreadValues({}, block), { sceneNumber: String(fallbackCount++) });
|
|
1353
|
+
}
|
|
1354
|
+
return block;
|
|
1355
|
+
});
|
|
1356
|
+
setBlocks(finalizedBlocks);
|
|
1357
|
+
setTimeout(() => {
|
|
1358
|
+
var _a;
|
|
1359
|
+
const firstId = parsedBlocks[0].id;
|
|
1360
|
+
if (firstId && refs.current[firstId]) {
|
|
1361
|
+
setFocusedBlockId(firstId);
|
|
1362
|
+
(_a = refs.current[firstId]) == null ? void 0 : _a.focus();
|
|
1363
|
+
}
|
|
1364
|
+
}, 100);
|
|
1365
|
+
}
|
|
1366
|
+
},
|
|
1367
|
+
[refs]
|
|
1368
|
+
);
|
|
1369
|
+
const handleFocus = useCallback(
|
|
1370
|
+
(id) => {
|
|
1371
|
+
if (blurTimeout.current) {
|
|
1372
|
+
clearTimeout(blurTimeout.current);
|
|
1373
|
+
}
|
|
1374
|
+
setFocusedBlockId(id);
|
|
1375
|
+
const block = blocks.find((b) => b.id === id);
|
|
1376
|
+
if ((block == null ? void 0 : block.type) === "CHARACTER") {
|
|
1377
|
+
const trimmedText = block.text.trim();
|
|
1378
|
+
const openParenIndex = trimmedText.lastIndexOf("(");
|
|
1379
|
+
const closeParenIndex = trimmedText.lastIndexOf(")");
|
|
1380
|
+
if (openParenIndex !== -1 && openParenIndex > closeParenIndex) {
|
|
1381
|
+
setShowExtensionSuggestions(true);
|
|
1382
|
+
setShowSuggestions(false);
|
|
1383
|
+
} else {
|
|
1384
|
+
setShowExtensionSuggestions(false);
|
|
1385
|
+
setShowSuggestions(openParenIndex === -1);
|
|
1386
|
+
}
|
|
1072
1387
|
} else {
|
|
1388
|
+
setShowSuggestions(true);
|
|
1073
1389
|
setShowExtensionSuggestions(false);
|
|
1074
|
-
setShowSuggestions(openParenIndex === -1);
|
|
1075
1390
|
}
|
|
1076
|
-
}
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
}
|
|
1080
|
-
}, [blocks]);
|
|
1391
|
+
},
|
|
1392
|
+
[blocks]
|
|
1393
|
+
);
|
|
1081
1394
|
const handleBlur = useCallback((id) => {
|
|
1082
1395
|
if (document.activeElement === refs.current[id]) return;
|
|
1083
1396
|
blurTimeout.current = setTimeout(() => {
|
|
@@ -1104,6 +1417,8 @@ function useScreenplayEditor() {
|
|
|
1104
1417
|
handleBlockTypeChange,
|
|
1105
1418
|
handleSelectCharacterExtension,
|
|
1106
1419
|
handleKeyDown,
|
|
1420
|
+
handleScriptImport,
|
|
1421
|
+
handleSceneNumberChange,
|
|
1107
1422
|
handleFocus,
|
|
1108
1423
|
handleBlur
|
|
1109
1424
|
};
|