@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.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;
|
|
@@ -126,6 +147,99 @@ var blockStyles = {
|
|
|
126
147
|
}
|
|
127
148
|
}
|
|
128
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
|
+
}
|
|
129
243
|
function ScreenplayEditorView({
|
|
130
244
|
blocks,
|
|
131
245
|
pages,
|
|
@@ -147,9 +261,11 @@ function ScreenplayEditorView({
|
|
|
147
261
|
handleKeyDown,
|
|
148
262
|
handleFocus,
|
|
149
263
|
handleBlur,
|
|
264
|
+
handleScriptImport,
|
|
150
265
|
onSave,
|
|
151
266
|
onSaveAsPdf,
|
|
152
|
-
onSyncWithCloud
|
|
267
|
+
onSyncWithCloud,
|
|
268
|
+
handleSceneNumberChange
|
|
153
269
|
}) {
|
|
154
270
|
const [isRulesOpen, setIsRulesOpen] = react.useState(false);
|
|
155
271
|
return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
@@ -208,7 +324,26 @@ function ScreenplayEditorView({
|
|
|
208
324
|
className: `relative rounded-sm transition-all duration-200 outline-none ${focusedBlockId === block.id ? "bg-zinc-100/50" : "bg-transparent"}`,
|
|
209
325
|
children: block.type === "SCENE_HEADING" ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
210
326
|
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-2 px-4 py-1 bg-transparent", children: [
|
|
211
|
-
/* @__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
|
+
),
|
|
212
347
|
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
213
348
|
"select",
|
|
214
349
|
{
|
|
@@ -401,6 +536,10 @@ function ScreenplayEditorView({
|
|
|
401
536
|
pageIndex
|
|
402
537
|
)) }),
|
|
403
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
|
+
] }) }),
|
|
404
543
|
onSave && /* @__PURE__ */ jsxRuntime.jsxs(
|
|
405
544
|
"button",
|
|
406
545
|
{
|
|
@@ -503,6 +642,7 @@ function createNewBlock(type) {
|
|
|
503
642
|
if (type === "SCENE_HEADING") {
|
|
504
643
|
newBlock.sceneType = "INT.";
|
|
505
644
|
newBlock.timeOfDay = "DAY";
|
|
645
|
+
newBlock.sceneNumber = "";
|
|
506
646
|
} else if (type === "PARENTHETICAL") {
|
|
507
647
|
newBlock.text = "()";
|
|
508
648
|
}
|
|
@@ -510,12 +650,12 @@ function createNewBlock(type) {
|
|
|
510
650
|
}
|
|
511
651
|
function addBlockAfter(blocks, currentBlockId) {
|
|
512
652
|
const currentIndex = blocks.findIndex((b) => b.id === currentBlockId);
|
|
513
|
-
if (currentIndex === -1) {
|
|
514
|
-
|
|
515
|
-
}
|
|
516
|
-
const currentBlock = blocks[currentIndex];
|
|
517
|
-
const nextType = getNextBlockType(currentBlock.type);
|
|
653
|
+
if (currentIndex === -1) return { newBlocks: blocks, newBlockId: "" };
|
|
654
|
+
const nextType = getNextBlockType(blocks[currentIndex].type);
|
|
518
655
|
const newBlock = createNewBlock(nextType);
|
|
656
|
+
if (nextType === "SCENE_HEADING") {
|
|
657
|
+
newBlock.sceneNumber = generateNextSceneNumber(blocks, currentIndex);
|
|
658
|
+
}
|
|
519
659
|
const newBlocks = [
|
|
520
660
|
...blocks.slice(0, currentIndex + 1),
|
|
521
661
|
newBlock,
|
|
@@ -540,10 +680,150 @@ function deleteBlock(blocks, blockIdToDelete) {
|
|
|
540
680
|
function updateBlock(blocks, id, key, value) {
|
|
541
681
|
return blocks.map((b) => b.id === id ? __spreadProps(__spreadValues({}, b), { [key]: value }) : b);
|
|
542
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
|
+
}
|
|
543
711
|
function changeBlockType(blocks, id, newType) {
|
|
712
|
+
const currentIndex = blocks.findIndex((b) => b.id === id);
|
|
713
|
+
if (currentIndex === -1) return blocks;
|
|
714
|
+
const currentBlock = blocks[currentIndex];
|
|
544
715
|
const newBlock = createNewBlock(newType);
|
|
716
|
+
if (newType === "PARENTHETICAL") {
|
|
717
|
+
const cleanText = currentBlock.text.replace(/[()]/g, "");
|
|
718
|
+
newBlock.text = `(${cleanText})`;
|
|
719
|
+
} else {
|
|
720
|
+
newBlock.text = currentBlock.text;
|
|
721
|
+
}
|
|
722
|
+
if (newType === "SCENE_HEADING") {
|
|
723
|
+
newBlock.sceneNumber = generateNextSceneNumber(blocks, currentIndex);
|
|
724
|
+
newBlock.text = newBlock.text.toUpperCase();
|
|
725
|
+
}
|
|
545
726
|
return blocks.map((b) => b.id === id ? __spreadProps(__spreadValues({}, newBlock), { id: b.id }) : b);
|
|
546
727
|
}
|
|
728
|
+
function parseScreenplayText(content) {
|
|
729
|
+
const lines = content.split("\n");
|
|
730
|
+
const blocks = [];
|
|
731
|
+
let lastBlock = null;
|
|
732
|
+
let activeSpeaker = null;
|
|
733
|
+
for (let i = 0; i < lines.length; i++) {
|
|
734
|
+
let originalLine = lines[i];
|
|
735
|
+
let trimmedLine = lines[i].trim().replace(/\s+/g, " ");
|
|
736
|
+
const leadingSpaces = originalLine.search(/\S/);
|
|
737
|
+
if (lines[i].trim() === "") {
|
|
738
|
+
activeSpeaker = null;
|
|
739
|
+
continue;
|
|
740
|
+
}
|
|
741
|
+
const isPageNumber = /^\d+$/.test(trimmedLine) || /^(PAGE|pg\.?)\s?\d+$/i.test(trimmedLine) || /^[\d]+\.$/.test(trimmedLine);
|
|
742
|
+
if (trimmedLine.length === 0 || isPageNumber) continue;
|
|
743
|
+
let currentBlockType = null;
|
|
744
|
+
const isAllUpperCase = trimmedLine === trimmedLine.toUpperCase() && /[A-Z]/.test(trimmedLine);
|
|
745
|
+
const sceneHeadingStartRegex = /^(?:\d+[A-Z]?\.?\s*)?(INT\.?\/EXT\.?|I\/E|INT|EXT|EST\.)\b/i;
|
|
746
|
+
const isTransition = isAllUpperCase && (trimmedLine.endsWith(" TO:") || ["FADE IN:", "FADE OUT.", "CUT TO BLACK."].includes(trimmedLine));
|
|
747
|
+
if (sceneHeadingStartRegex.test(trimmedLine)) {
|
|
748
|
+
currentBlockType = "SCENE_HEADING";
|
|
749
|
+
activeSpeaker = null;
|
|
750
|
+
} else if (isTransition) {
|
|
751
|
+
currentBlockType = "TRANSITION";
|
|
752
|
+
activeSpeaker = null;
|
|
753
|
+
} else if (isAllUpperCase && !trimmedLine.startsWith("(")) {
|
|
754
|
+
let nextLine = "";
|
|
755
|
+
for (let j = i + 1; j < lines.length; j++) {
|
|
756
|
+
const nextTrimmed = lines[j].trim();
|
|
757
|
+
const nextIsPage = /^\d+$/.test(nextTrimmed) || /^(PAGE|pg\.?)\s?\d+$/i.test(nextTrimmed);
|
|
758
|
+
if (nextTrimmed.length > 0 && !nextIsPage) {
|
|
759
|
+
nextLine = nextTrimmed;
|
|
760
|
+
break;
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
if (nextLine && (nextLine.startsWith("(") || nextLine !== nextLine.toUpperCase()) || /\s\(.*\)$/.test(trimmedLine)) {
|
|
764
|
+
currentBlockType = "CHARACTER";
|
|
765
|
+
activeSpeaker = trimmedLine;
|
|
766
|
+
} else {
|
|
767
|
+
currentBlockType = "ACTION";
|
|
768
|
+
activeSpeaker = null;
|
|
769
|
+
}
|
|
770
|
+
} else if (trimmedLine.startsWith("(") && trimmedLine.endsWith(")")) {
|
|
771
|
+
currentBlockType = "PARENTHETICAL";
|
|
772
|
+
} else if (activeSpeaker && ((lastBlock == null ? void 0 : lastBlock.type) === "CHARACTER" || (lastBlock == null ? void 0 : lastBlock.type) === "PARENTHETICAL" || (lastBlock == null ? void 0 : lastBlock.type) === "DIALOGUE")) {
|
|
773
|
+
const isLastLineComplete = /[.!?]"?$/.test(lastBlock.text.trim());
|
|
774
|
+
if (leadingSpaces < 15 && (lastBlock == null ? void 0 : lastBlock.type) !== "CHARACTER" && isLastLineComplete) {
|
|
775
|
+
currentBlockType = "ACTION";
|
|
776
|
+
activeSpeaker = null;
|
|
777
|
+
} else {
|
|
778
|
+
currentBlockType = "DIALOGUE";
|
|
779
|
+
}
|
|
780
|
+
} else {
|
|
781
|
+
currentBlockType = "ACTION";
|
|
782
|
+
activeSpeaker = null;
|
|
783
|
+
}
|
|
784
|
+
if (lastBlock && lastBlock.type === currentBlockType && (currentBlockType === "ACTION" || currentBlockType === "DIALOGUE") && !(currentBlockType === "DIALOGUE" && !activeSpeaker)) {
|
|
785
|
+
lastBlock.text += " " + trimmedLine;
|
|
786
|
+
} else {
|
|
787
|
+
const newBlock = createNewBlock(currentBlockType);
|
|
788
|
+
if (currentBlockType === "SCENE_HEADING") {
|
|
789
|
+
let workingLine = trimmedLine;
|
|
790
|
+
const sceneNumMatch = workingLine.match(/^(\d+[A-Z]*)\.?\s+/i);
|
|
791
|
+
if (sceneNumMatch) {
|
|
792
|
+
newBlock.sceneNumber = String(sceneNumMatch[1]).toUpperCase();
|
|
793
|
+
workingLine = workingLine.replace(/^(\d+[A-Z]*)\.?\s+/i, "").trim();
|
|
794
|
+
} else {
|
|
795
|
+
newBlock.sceneNumber = "";
|
|
796
|
+
}
|
|
797
|
+
const typeMatch = workingLine.match(
|
|
798
|
+
/^(INT\.?\/EXT\.?|I\/E|INT|EXT|EST\.)/i
|
|
799
|
+
);
|
|
800
|
+
if (typeMatch) {
|
|
801
|
+
let sType = typeMatch[0].toUpperCase().replace(/[^A-Z/.]/g, "");
|
|
802
|
+
if (!sType.endsWith(".")) sType += ".";
|
|
803
|
+
newBlock.sceneType = sType;
|
|
804
|
+
workingLine = workingLine.substring(typeMatch[0].length).trim();
|
|
805
|
+
}
|
|
806
|
+
const suffixRegex = /[.\-\s]+(DAY|NIGHT|CONTINUOUS|LATER|MORNING|EVENING|DUSK|DAWN|MORN|AFT|SUNSET|SUNRISE)(?:\s+.*)*$/i;
|
|
807
|
+
const suffixMatch = workingLine.match(suffixRegex);
|
|
808
|
+
if (suffixMatch) {
|
|
809
|
+
const rawTime = suffixMatch[1].toUpperCase();
|
|
810
|
+
const nightKeys = ["NIGHT", "EVENING", "DUSK", "SUNSET"];
|
|
811
|
+
newBlock.timeOfDay = nightKeys.includes(rawTime) ? "NIGHT" : "DAY";
|
|
812
|
+
workingLine = workingLine.substring(0, suffixMatch.index).trim();
|
|
813
|
+
} else {
|
|
814
|
+
workingLine = workingLine.replace(/\s+\d+(\s+\d+)*$/, "").trim();
|
|
815
|
+
newBlock.timeOfDay = "DAY";
|
|
816
|
+
}
|
|
817
|
+
newBlock.text = workingLine.replace(/^[-.\s]+/, "").replace(/[-.\s]+$/, "").toUpperCase();
|
|
818
|
+
} else {
|
|
819
|
+
newBlock.text = trimmedLine;
|
|
820
|
+
}
|
|
821
|
+
blocks.push(newBlock);
|
|
822
|
+
lastBlock = newBlock;
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
return blocks.length > 0 ? blocks : [createNewBlock("SCENE_HEADING")];
|
|
826
|
+
}
|
|
547
827
|
|
|
548
828
|
// app/hook/use-screenplay-editor.ts
|
|
549
829
|
var initialBlocks = [
|
|
@@ -551,6 +831,7 @@ var initialBlocks = [
|
|
|
551
831
|
id: uuid(),
|
|
552
832
|
type: "SCENE_HEADING",
|
|
553
833
|
text: "",
|
|
834
|
+
sceneNumber: "1",
|
|
554
835
|
sceneType: "INT.",
|
|
555
836
|
timeOfDay: "DAY"
|
|
556
837
|
}
|
|
@@ -629,6 +910,16 @@ function useScreenplayEditor() {
|
|
|
629
910
|
() => ["(V.O.)", "(O.S.)", "(O.C.)", "(SUBTITLE)", "(CONT'D)"],
|
|
630
911
|
[]
|
|
631
912
|
);
|
|
913
|
+
const handleSceneNumberChange = react.useCallback(
|
|
914
|
+
(id, newNumber) => {
|
|
915
|
+
setBlocks(
|
|
916
|
+
(prevBlocks) => prevBlocks.map(
|
|
917
|
+
(block) => block.id === id ? __spreadProps(__spreadValues({}, block), { sceneNumber: newNumber.toUpperCase() }) : block
|
|
918
|
+
)
|
|
919
|
+
);
|
|
920
|
+
},
|
|
921
|
+
[]
|
|
922
|
+
);
|
|
632
923
|
const locations = react.useMemo(() => {
|
|
633
924
|
const locs = blocks.filter((b) => b.type === "SCENE_HEADING" && b.text.trim() !== "").map((b) => b.text.trim().toUpperCase());
|
|
634
925
|
return [...new Set(locs)];
|
|
@@ -646,15 +937,32 @@ function useScreenplayEditor() {
|
|
|
646
937
|
}, [blocks]);
|
|
647
938
|
const sceneNumbers = react.useMemo(() => {
|
|
648
939
|
const map = {};
|
|
649
|
-
let
|
|
940
|
+
let fallbackCount = 0;
|
|
650
941
|
blocks.forEach((block) => {
|
|
651
942
|
if (block.type === "SCENE_HEADING") {
|
|
652
|
-
|
|
653
|
-
|
|
943
|
+
if (block.sceneNumber) {
|
|
944
|
+
map[block.id] = block.sceneNumber;
|
|
945
|
+
const base = parseInt(block.sceneNumber);
|
|
946
|
+
if (!isNaN(base)) fallbackCount = Math.max(fallbackCount, base);
|
|
947
|
+
} else {
|
|
948
|
+
fallbackCount++;
|
|
949
|
+
map[block.id] = String(fallbackCount);
|
|
950
|
+
}
|
|
654
951
|
}
|
|
655
952
|
});
|
|
656
953
|
return map;
|
|
657
954
|
}, [blocks]);
|
|
955
|
+
react.useCallback(() => {
|
|
956
|
+
let count = 1;
|
|
957
|
+
setBlocks(
|
|
958
|
+
(prev) => prev.map((b) => {
|
|
959
|
+
if (b.type === "SCENE_HEADING") {
|
|
960
|
+
return __spreadProps(__spreadValues({}, b), { sceneNumber: String(count++) });
|
|
961
|
+
}
|
|
962
|
+
return b;
|
|
963
|
+
})
|
|
964
|
+
);
|
|
965
|
+
}, []);
|
|
658
966
|
react.useEffect(() => {
|
|
659
967
|
if (newBlockId && refs.current[newBlockId]) {
|
|
660
968
|
const block = blocks.find((b) => b.id === newBlockId);
|
|
@@ -675,11 +983,13 @@ function useScreenplayEditor() {
|
|
|
675
983
|
react.useEffect(() => {
|
|
676
984
|
blocks.forEach((block) => {
|
|
677
985
|
const element = refs.current[block.id];
|
|
678
|
-
if (element
|
|
679
|
-
element.innerText
|
|
986
|
+
if (element) {
|
|
987
|
+
if (element.innerText !== block.text && document.activeElement !== element) {
|
|
988
|
+
element.innerText = block.text;
|
|
989
|
+
}
|
|
680
990
|
}
|
|
681
991
|
});
|
|
682
|
-
}, [blocks
|
|
992
|
+
}, [blocks]);
|
|
683
993
|
react.useEffect(() => {
|
|
684
994
|
const handleClickOutside = (e) => {
|
|
685
995
|
const target = e.target;
|
|
@@ -859,17 +1169,15 @@ function useScreenplayEditor() {
|
|
|
859
1169
|
const el = refs.current[focusedBlockId];
|
|
860
1170
|
if (el) {
|
|
861
1171
|
el.focus();
|
|
862
|
-
const
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
setCaretPosition(el,
|
|
866
|
-
} else {
|
|
867
|
-
setCaretPosition(el, newBlock.text.length);
|
|
1172
|
+
const currentBlock = blocks.find((b) => b.id === focusedBlockId);
|
|
1173
|
+
if (currentBlock) {
|
|
1174
|
+
const pos = newType === "PARENTHETICAL" ? el.innerText.length - 1 : el.innerText.length;
|
|
1175
|
+
setCaretPosition(el, Math.max(0, pos));
|
|
868
1176
|
}
|
|
869
1177
|
}
|
|
870
|
-
},
|
|
1178
|
+
}, 10);
|
|
871
1179
|
},
|
|
872
|
-
[focusedBlockId]
|
|
1180
|
+
[focusedBlockId, blocks]
|
|
873
1181
|
);
|
|
874
1182
|
const handleSelectCharacterExtension = react.useCallback(
|
|
875
1183
|
(extension) => {
|
|
@@ -925,8 +1233,6 @@ function useScreenplayEditor() {
|
|
|
925
1233
|
const el = refs.current[id];
|
|
926
1234
|
if (el) {
|
|
927
1235
|
el.focus();
|
|
928
|
-
const newBlock = createNewBlock(newType);
|
|
929
|
-
el.innerText = newBlock.text;
|
|
930
1236
|
if (newType === "PARENTHETICAL") {
|
|
931
1237
|
setCaretPosition(el, 1);
|
|
932
1238
|
} else {
|
|
@@ -1057,28 +1363,62 @@ function useScreenplayEditor() {
|
|
|
1057
1363
|
},
|
|
1058
1364
|
[blocks, handleBlockTextChange]
|
|
1059
1365
|
);
|
|
1060
|
-
const
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1366
|
+
const handleScriptImport = react.useCallback(
|
|
1367
|
+
(title, content) => {
|
|
1368
|
+
const parsedBlocks = parseScreenplayText(content);
|
|
1369
|
+
if (parsedBlocks.length > 0) {
|
|
1370
|
+
let fallbackCount = 1;
|
|
1371
|
+
const finalizedBlocks = parsedBlocks.map((block) => {
|
|
1372
|
+
if (block.type === "SCENE_HEADING") {
|
|
1373
|
+
if (block.sceneNumber && block.sceneNumber.trim().length > 0) {
|
|
1374
|
+
const isPureNumber = /^\d+$/.test(block.sceneNumber);
|
|
1375
|
+
if (isPureNumber) {
|
|
1376
|
+
fallbackCount = parseInt(block.sceneNumber) + 1;
|
|
1377
|
+
}
|
|
1378
|
+
return block;
|
|
1379
|
+
}
|
|
1380
|
+
return __spreadProps(__spreadValues({}, block), { sceneNumber: String(fallbackCount++) });
|
|
1381
|
+
}
|
|
1382
|
+
return block;
|
|
1383
|
+
});
|
|
1384
|
+
setBlocks(finalizedBlocks);
|
|
1385
|
+
setTimeout(() => {
|
|
1386
|
+
var _a;
|
|
1387
|
+
const firstId = parsedBlocks[0].id;
|
|
1388
|
+
if (firstId && refs.current[firstId]) {
|
|
1389
|
+
setFocusedBlockId(firstId);
|
|
1390
|
+
(_a = refs.current[firstId]) == null ? void 0 : _a.focus();
|
|
1391
|
+
}
|
|
1392
|
+
}, 100);
|
|
1393
|
+
}
|
|
1394
|
+
},
|
|
1395
|
+
[refs]
|
|
1396
|
+
);
|
|
1397
|
+
const handleFocus = react.useCallback(
|
|
1398
|
+
(id) => {
|
|
1399
|
+
if (blurTimeout.current) {
|
|
1400
|
+
clearTimeout(blurTimeout.current);
|
|
1401
|
+
}
|
|
1402
|
+
setFocusedBlockId(id);
|
|
1403
|
+
const block = blocks.find((b) => b.id === id);
|
|
1404
|
+
if ((block == null ? void 0 : block.type) === "CHARACTER") {
|
|
1405
|
+
const trimmedText = block.text.trim();
|
|
1406
|
+
const openParenIndex = trimmedText.lastIndexOf("(");
|
|
1407
|
+
const closeParenIndex = trimmedText.lastIndexOf(")");
|
|
1408
|
+
if (openParenIndex !== -1 && openParenIndex > closeParenIndex) {
|
|
1409
|
+
setShowExtensionSuggestions(true);
|
|
1410
|
+
setShowSuggestions(false);
|
|
1411
|
+
} else {
|
|
1412
|
+
setShowExtensionSuggestions(false);
|
|
1413
|
+
setShowSuggestions(openParenIndex === -1);
|
|
1414
|
+
}
|
|
1073
1415
|
} else {
|
|
1416
|
+
setShowSuggestions(true);
|
|
1074
1417
|
setShowExtensionSuggestions(false);
|
|
1075
|
-
setShowSuggestions(openParenIndex === -1);
|
|
1076
1418
|
}
|
|
1077
|
-
}
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
}
|
|
1081
|
-
}, [blocks]);
|
|
1419
|
+
},
|
|
1420
|
+
[blocks]
|
|
1421
|
+
);
|
|
1082
1422
|
const handleBlur = react.useCallback((id) => {
|
|
1083
1423
|
if (document.activeElement === refs.current[id]) return;
|
|
1084
1424
|
blurTimeout.current = setTimeout(() => {
|
|
@@ -1105,6 +1445,8 @@ function useScreenplayEditor() {
|
|
|
1105
1445
|
handleBlockTypeChange,
|
|
1106
1446
|
handleSelectCharacterExtension,
|
|
1107
1447
|
handleKeyDown,
|
|
1448
|
+
handleScriptImport,
|
|
1449
|
+
handleSceneNumberChange,
|
|
1108
1450
|
handleFocus,
|
|
1109
1451
|
handleBlur
|
|
1110
1452
|
};
|