Spaces:
Running
on
CPU Upgrade
Running
on
CPU Upgrade
Commit
β’
316ef9c
1
Parent(s):
a477577
add text highlight
Browse files
src/components/editor/ScriptEditor/index.tsx
CHANGED
@@ -10,11 +10,12 @@ import { useTheme } from "@/controllers/ui/useTheme"
|
|
10 |
import { themes } from "@/controllers/ui/theme"
|
11 |
|
12 |
import "./styles.css"
|
|
|
13 |
|
14 |
export function ScriptEditor() {
|
15 |
|
16 |
-
const
|
17 |
-
const
|
18 |
const draft = useEditor(s => s.draft)
|
19 |
const setDraft = useEditor(s => s.setDraft)
|
20 |
const loadDraftFromClap = useEditor(s => s.loadDraftFromClap)
|
@@ -38,14 +39,14 @@ export function ScriptEditor() {
|
|
38 |
)
|
39 |
|
40 |
useEffect(() => {
|
41 |
-
if (!
|
42 |
// let's do something basic for now: we disable the
|
43 |
// timeline-to-editor scroll sync when the user is
|
44 |
// hovering the editor
|
45 |
if (useEditor.getState().mouseIsInside) { return }
|
46 |
|
47 |
-
if (horizontalTimelineRatio !==
|
48 |
-
|
49 |
}
|
50 |
// various things we can do here!
|
51 |
// move the scroll:
|
@@ -63,27 +64,29 @@ export function ScriptEditor() {
|
|
63 |
// => I think we should restore the "follow cursor during playback"
|
64 |
// feature, because this is doable.
|
65 |
|
66 |
-
}, [
|
67 |
|
68 |
-
const onMount = (
|
69 |
-
const
|
70 |
-
if (!
|
71 |
|
72 |
-
|
|
|
|
|
73 |
|
74 |
-
|
75 |
-
jumpCursorOnLineClick(
|
76 |
})
|
77 |
|
78 |
-
|
79 |
onDidScrollChange({ scrollTop, scrollLeft, scrollWidth, scrollHeight })
|
80 |
})
|
81 |
|
82 |
// as an optimization we can use this later, for surgical edits,
|
83 |
// to perform real time updates of the timeline
|
84 |
|
85 |
-
|
86 |
-
|
87 |
for (const change of modelContentChangedEvent.changes) {
|
88 |
// console.log(" - change:", change)
|
89 |
}
|
@@ -95,6 +98,7 @@ export function ScriptEditor() {
|
|
95 |
}
|
96 |
|
97 |
const setMonaco = useEditor(s => s.setMonaco)
|
|
|
98 |
const setMouseIsInside = useEditor(s => s.setMouseIsInside)
|
99 |
const themeName = useUI(s => s.themeName)
|
100 |
const editorFontSize = useUI(s => s.editorFontSize)
|
@@ -128,19 +132,23 @@ export function ScriptEditor() {
|
|
128 |
}
|
129 |
|
130 |
// Apply the custom theme immediately after defining it
|
131 |
-
monaco.editor.setTheme(themes.backstage.id)
|
|
|
|
|
|
|
|
|
|
|
|
|
132 |
}
|
133 |
|
134 |
return (
|
135 |
<div
|
136 |
className="h-full"
|
137 |
onMouseEnter={() => setMouseIsInside(true)}
|
138 |
-
|
139 |
>
|
140 |
<Editor
|
141 |
height="100%"
|
142 |
-
defaultLanguage="plaintext"
|
143 |
-
defaultValue={draft}
|
144 |
beforeMount={beforeMount}
|
145 |
theme={themeName}
|
146 |
onMount={onMount}
|
|
|
10 |
import { themes } from "@/controllers/ui/theme"
|
11 |
|
12 |
import "./styles.css"
|
13 |
+
import { ClapSegmentCategory } from "@aitube/clap"
|
14 |
|
15 |
export function ScriptEditor() {
|
16 |
|
17 |
+
const standaloneCodeEditor = useEditor(s => s.standaloneCodeEditor)
|
18 |
+
const setStandaloneCodeEditor = useEditor(s => s.setStandaloneCodeEditor)
|
19 |
const draft = useEditor(s => s.draft)
|
20 |
const setDraft = useEditor(s => s.setDraft)
|
21 |
const loadDraftFromClap = useEditor(s => s.loadDraftFromClap)
|
|
|
39 |
)
|
40 |
|
41 |
useEffect(() => {
|
42 |
+
if (!standaloneCodeEditor) { return }
|
43 |
// let's do something basic for now: we disable the
|
44 |
// timeline-to-editor scroll sync when the user is
|
45 |
// hovering the editor
|
46 |
if (useEditor.getState().mouseIsInside) { return }
|
47 |
|
48 |
+
if (horizontalTimelineRatio !== standaloneCodeEditor.getScrollTop()) {
|
49 |
+
standaloneCodeEditor.setScrollPosition({ scrollTop: horizontalTimelineRatio })
|
50 |
}
|
51 |
// various things we can do here!
|
52 |
// move the scroll:
|
|
|
64 |
// => I think we should restore the "follow cursor during playback"
|
65 |
// feature, because this is doable.
|
66 |
|
67 |
+
}, [standaloneCodeEditor, horizontalTimelineRatio])
|
68 |
|
69 |
+
const onMount = (codeEditor: MonacoEditor.editor.IStandaloneCodeEditor) => {
|
70 |
+
const { textModel } = useEditor.getState()
|
71 |
+
if (!textModel) { return }
|
72 |
|
73 |
+
codeEditor.setModel(textModel)
|
74 |
+
|
75 |
+
setStandaloneCodeEditor(codeEditor)
|
76 |
|
77 |
+
codeEditor.onMouseDown((e) => {
|
78 |
+
jumpCursorOnLineClick(codeEditor.getPosition()?.lineNumber)
|
79 |
})
|
80 |
|
81 |
+
codeEditor.onDidScrollChange(({ scrollTop, scrollLeft, scrollWidth, scrollHeight }: MonacoEditor.IScrollEvent) => {
|
82 |
onDidScrollChange({ scrollTop, scrollLeft, scrollWidth, scrollHeight })
|
83 |
})
|
84 |
|
85 |
// as an optimization we can use this later, for surgical edits,
|
86 |
// to perform real time updates of the timeline
|
87 |
|
88 |
+
textModel.onDidChangeContent((modelContentChangedEvent: MonacoEditor.editor.IModelContentChangedEvent) => {
|
89 |
+
console.log("onDidChangeContent:")
|
90 |
for (const change of modelContentChangedEvent.changes) {
|
91 |
// console.log(" - change:", change)
|
92 |
}
|
|
|
98 |
}
|
99 |
|
100 |
const setMonaco = useEditor(s => s.setMonaco)
|
101 |
+
const setTextModel = useEditor(s => s.setTextModel)
|
102 |
const setMouseIsInside = useEditor(s => s.setMouseIsInside)
|
103 |
const themeName = useUI(s => s.themeName)
|
104 |
const editorFontSize = useUI(s => s.editorFontSize)
|
|
|
132 |
}
|
133 |
|
134 |
// Apply the custom theme immediately after defining it
|
135 |
+
monaco.editor.setTheme(themes.backstage.id)
|
136 |
+
|
137 |
+
const textModel: MonacoEditor.editor.ITextModel = monaco.editor.createModel(
|
138 |
+
draft,
|
139 |
+
"plaintext"
|
140 |
+
)
|
141 |
+
setTextModel(textModel)
|
142 |
}
|
143 |
|
144 |
return (
|
145 |
<div
|
146 |
className="h-full"
|
147 |
onMouseEnter={() => setMouseIsInside(true)}
|
148 |
+
onMouseLeave={() => setMouseIsInside(false)}
|
149 |
>
|
150 |
<Editor
|
151 |
height="100%"
|
|
|
|
|
152 |
beforeMount={beforeMount}
|
153 |
theme={themeName}
|
154 |
onMount={onMount}
|
src/components/editor/ScriptEditor/styles.css
CHANGED
@@ -1,3 +1,23 @@
|
|
1 |
.monaco-editor-background {
|
2 |
@apply transition-colors;
|
3 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
.monaco-editor-background {
|
2 |
@apply transition-colors;
|
3 |
+
}
|
4 |
+
|
5 |
+
.entity {
|
6 |
+
|
7 |
+
/*
|
8 |
+
padding and rounded corners only make sense if we use a background:
|
9 |
+
@apply rounded-xl px-1.5 py-1 font-semibold;
|
10 |
+
*/
|
11 |
+
}
|
12 |
+
|
13 |
+
.entity.entity-location {
|
14 |
+
@apply text-green-300/90 !important;
|
15 |
+
}
|
16 |
+
|
17 |
+
.entity.entity-character {
|
18 |
+
@apply text-orange-300/90 !important;
|
19 |
+
}
|
20 |
+
|
21 |
+
.entity.entity-highlight {
|
22 |
+
@apply font-semibold;
|
23 |
+
}
|
src/controllers/editor/getDefaultEditorState.ts
CHANGED
@@ -3,7 +3,8 @@ import { EditorState } from "./types"
|
|
3 |
export function getDefaultEditorState(): EditorState {
|
4 |
const state: EditorState = {
|
5 |
monaco: undefined,
|
6 |
-
|
|
|
7 |
mouseIsInside: false,
|
8 |
draft: "",
|
9 |
lineNumberToMentionedSegments: {},
|
|
|
3 |
export function getDefaultEditorState(): EditorState {
|
4 |
const state: EditorState = {
|
5 |
monaco: undefined,
|
6 |
+
textModel: undefined,
|
7 |
+
standaloneCodeEditor: undefined,
|
8 |
mouseIsInside: false,
|
9 |
draft: "",
|
10 |
lineNumberToMentionedSegments: {},
|
src/controllers/editor/types.ts
CHANGED
@@ -12,8 +12,10 @@ export type ScrollData = {
|
|
12 |
export type EditorState = {
|
13 |
monaco?: Monaco
|
14 |
|
|
|
|
|
15 |
// reference to the React component
|
16 |
-
|
17 |
|
18 |
// used to know if the user is actually inside the editor or not
|
19 |
mouseIsInside: boolean
|
@@ -36,13 +38,16 @@ export type EditorState = {
|
|
36 |
|
37 |
export type EditorControls = {
|
38 |
setMonaco: (monaco?: Monaco) => void
|
39 |
-
|
|
|
40 |
setMouseIsInside: (mouseIsInside: boolean) => void
|
41 |
loadDraftFromClap: (clap: ClapProject) => void
|
42 |
setDraft: (draft: string) => void
|
43 |
publishDraftToTimeline: () => Promise<void>
|
44 |
onDidScrollChange: (scrollData: ScrollData) => void
|
45 |
jumpCursorOnLineClick: (line?: number) => void
|
|
|
|
|
46 |
}
|
47 |
|
48 |
export type EditorStore = EditorState & EditorControls
|
|
|
12 |
export type EditorState = {
|
13 |
monaco?: Monaco
|
14 |
|
15 |
+
textModel?: MonacoEditor.editor.ITextModel
|
16 |
+
|
17 |
// reference to the React component
|
18 |
+
standaloneCodeEditor?: MonacoEditor.editor.IStandaloneCodeEditor
|
19 |
|
20 |
// used to know if the user is actually inside the editor or not
|
21 |
mouseIsInside: boolean
|
|
|
38 |
|
39 |
export type EditorControls = {
|
40 |
setMonaco: (monaco?: Monaco) => void
|
41 |
+
setTextModel: (textModel?: MonacoEditor.editor.ITextModel) => void
|
42 |
+
setStandaloneCodeEditor: (standaloneCodeEditor?: MonacoEditor.editor.IStandaloneCodeEditor) => void
|
43 |
setMouseIsInside: (mouseIsInside: boolean) => void
|
44 |
loadDraftFromClap: (clap: ClapProject) => void
|
45 |
setDraft: (draft: string) => void
|
46 |
publishDraftToTimeline: () => Promise<void>
|
47 |
onDidScrollChange: (scrollData: ScrollData) => void
|
48 |
jumpCursorOnLineClick: (line?: number) => void
|
49 |
+
highlightElements: () => void
|
50 |
+
applyClassNameToKeywords: (className?: string, keywords?: string[], caseSensitive?: boolean) => void
|
51 |
}
|
52 |
|
53 |
export type EditorStore = EditorState & EditorControls
|
src/controllers/editor/useEditor.ts
CHANGED
@@ -11,8 +11,9 @@ import { Monaco } from "@monaco-editor/react"
|
|
11 |
|
12 |
export const useEditor = create<EditorStore>((set, get) => ({
|
13 |
...getDefaultEditorState(),
|
14 |
-
setMonaco: (monaco?: Monaco) => { set({ monaco}) },
|
15 |
-
|
|
|
16 |
setMouseIsInside: (mouseIsInside: boolean) => { set({ mouseIsInside }) },
|
17 |
loadDraftFromClap: (clap: ClapProject) => {
|
18 |
const { setDraft } = get()
|
@@ -20,13 +21,17 @@ export const useEditor = create<EditorStore>((set, get) => ({
|
|
20 |
setDraft(clap.meta.screenplay)
|
21 |
},
|
22 |
setDraft: (draft: string) => {
|
23 |
-
const { draft: previousDraft } = get()
|
24 |
if (draft === previousDraft) { return }
|
25 |
set({ draft })
|
26 |
|
27 |
-
|
28 |
-
if (!
|
29 |
-
|
|
|
|
|
|
|
|
|
30 |
},
|
31 |
publishDraftToTimeline: async (): Promise<void> => {
|
32 |
const { draft } = get()
|
@@ -72,7 +77,7 @@ export const useEditor = create<EditorStore>((set, get) => ({
|
|
72 |
const timeline: TimelineStore = useTimeline.getState()
|
73 |
if (!timeline.timelineCamera || !timeline.timelineControls) { return }
|
74 |
|
75 |
-
const {
|
76 |
|
77 |
const scrollRatio = scrollTop / scrollHeight
|
78 |
const scrollX = Math.round(leftBarTrackScaleWidth + scrollRatio * timeline.contentWidth)
|
@@ -112,6 +117,92 @@ export const useEditor = create<EditorStore>((set, get) => ({
|
|
112 |
timeline.setCursorTimestampAtInMs(startTimeInMs)
|
113 |
},
|
114 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
115 |
}))
|
116 |
|
117 |
|
|
|
11 |
|
12 |
export const useEditor = create<EditorStore>((set, get) => ({
|
13 |
...getDefaultEditorState(),
|
14 |
+
setMonaco: (monaco?: Monaco) => { set({ monaco }) },
|
15 |
+
setTextModel: (textModel?: MonacoEditor.editor.ITextModel) => { set({ textModel }) },
|
16 |
+
setStandaloneCodeEditor: (standaloneCodeEditor?: MonacoEditor.editor.IStandaloneCodeEditor) => { set({ standaloneCodeEditor }) },
|
17 |
setMouseIsInside: (mouseIsInside: boolean) => { set({ mouseIsInside }) },
|
18 |
loadDraftFromClap: (clap: ClapProject) => {
|
19 |
const { setDraft } = get()
|
|
|
21 |
setDraft(clap.meta.screenplay)
|
22 |
},
|
23 |
setDraft: (draft: string) => {
|
24 |
+
const { draft: previousDraft, highlightElements, textModel } = get()
|
25 |
if (draft === previousDraft) { return }
|
26 |
set({ draft })
|
27 |
|
28 |
+
|
29 |
+
if (!textModel) { return }
|
30 |
+
// we need to update the model
|
31 |
+
textModel?.setValue(draft)
|
32 |
+
|
33 |
+
// and highlight the text again
|
34 |
+
highlightElements()
|
35 |
},
|
36 |
publishDraftToTimeline: async (): Promise<void> => {
|
37 |
const { draft } = get()
|
|
|
77 |
const timeline: TimelineStore = useTimeline.getState()
|
78 |
if (!timeline.timelineCamera || !timeline.timelineControls) { return }
|
79 |
|
80 |
+
const { standaloneCodeEditor } = get()
|
81 |
|
82 |
const scrollRatio = scrollTop / scrollHeight
|
83 |
const scrollX = Math.round(leftBarTrackScaleWidth + scrollRatio * timeline.contentWidth)
|
|
|
117 |
timeline.setCursorTimestampAtInMs(startTimeInMs)
|
118 |
},
|
119 |
|
120 |
+
highlightElements: () => {
|
121 |
+
const timeline: TimelineStore = useTimeline.getState()
|
122 |
+
const { clap } = timeline
|
123 |
+
|
124 |
+
const { textModel, standaloneCodeEditor, applyClassNameToKeywords } = get()
|
125 |
+
if (!textModel || !standaloneCodeEditor || !clap) { return }
|
126 |
+
|
127 |
+
const characters = clap.entities.filter(entity => entity.category === ClapSegmentCategory.CHARACTER).map(entity => entity.triggerName)
|
128 |
+
|
129 |
+
// any character
|
130 |
+
applyClassNameToKeywords(
|
131 |
+
"entity entity-character",
|
132 |
+
characters
|
133 |
+
)
|
134 |
+
|
135 |
+
// UPPERCASE CHARACTER
|
136 |
+
applyClassNameToKeywords(
|
137 |
+
"entity entity-character entity-highlight",
|
138 |
+
characters,
|
139 |
+
true
|
140 |
+
)
|
141 |
+
|
142 |
+
const locations = clap.entities.filter(entity => entity.category === ClapSegmentCategory.LOCATION).map(entity => entity.triggerName)
|
143 |
+
// any location
|
144 |
+
applyClassNameToKeywords(
|
145 |
+
"entity entity-location",
|
146 |
+
locations
|
147 |
+
)
|
148 |
+
|
149 |
+
// UPPERCASE LOCATION
|
150 |
+
applyClassNameToKeywords(
|
151 |
+
"entity entity-location entity-highlight",
|
152 |
+
locations,
|
153 |
+
true
|
154 |
+
)
|
155 |
+
},
|
156 |
+
applyClassNameToKeywords: (className: string = "", keywords: string[] = [], caseSensitive = false) => {
|
157 |
+
const timeline: TimelineStore = useTimeline.getState()
|
158 |
+
const { clap } = timeline
|
159 |
+
|
160 |
+
const { textModel, standaloneCodeEditor } = get()
|
161 |
+
if (!textModel || !standaloneCodeEditor || !clap) { return }
|
162 |
+
|
163 |
+
keywords.forEach((entityTriggerName: string): void => {
|
164 |
+
const matches: MonacoEditor.editor.FindMatch[] = textModel.findMatches(
|
165 |
+
// searchString β The string used to search. If it is a regular expression, set isRegex to true.
|
166 |
+
// searchString: string,
|
167 |
+
entityTriggerName,
|
168 |
+
|
169 |
+
// @param searchOnlyEditableRange β Limit the searching to only search inside the editable range of the model.
|
170 |
+
// searchOnlyEditableRange: boolean,
|
171 |
+
false,
|
172 |
+
|
173 |
+
// / @param isRegex β Used to indicate that searchString is a regular expression.
|
174 |
+
// isRegex: boolean,
|
175 |
+
false,
|
176 |
+
|
177 |
+
// @param matchCase β Force the matching to match lower/upper case exactly.
|
178 |
+
// matchCase: boolean,
|
179 |
+
caseSensitive,
|
180 |
+
|
181 |
+
// @param wordSeparators β Force the matching to match entire words only. Pass null otherwise.
|
182 |
+
// wordSeparators: string | null,
|
183 |
+
null,
|
184 |
+
|
185 |
+
// @param captureMatches β The result will contain the captured groups.
|
186 |
+
// captureMatches: boolean,
|
187 |
+
false,
|
188 |
+
|
189 |
+
// limitResultCount β Limit the number of results
|
190 |
+
// limitResultCount?: number
|
191 |
+
)
|
192 |
+
|
193 |
+
matches.forEach((match: MonacoEditor.editor.FindMatch): void => {
|
194 |
+
standaloneCodeEditor.createDecorationsCollection([
|
195 |
+
{
|
196 |
+
range: match.range,
|
197 |
+
options: {
|
198 |
+
isWholeLine: false,
|
199 |
+
inlineClassName: className
|
200 |
+
}
|
201 |
+
},
|
202 |
+
])
|
203 |
+
})
|
204 |
+
})
|
205 |
+
}
|
206 |
}))
|
207 |
|
208 |
|
src/controllers/ui/theme.ts
CHANGED
@@ -150,12 +150,12 @@ export const lore: UITheme = {
|
|
150 |
author: "Clapper",
|
151 |
description: "",
|
152 |
defaultBgColor: "#151520",
|
153 |
-
defaultTextColor: "#
|
154 |
defaultPrimaryColor: "#DE4A80",
|
155 |
logoColor: "#DE4A80",
|
156 |
editorBgColor: "#151520",
|
157 |
editorCursorColor: '#DE4A80',
|
158 |
-
editorTextColor: "#
|
159 |
monitorBgColor: "#151520",
|
160 |
monitorSecondaryTextColor: "#D6D3D1",
|
161 |
monitorPrimaryTextColor: "#DE4A80",
|
|
|
150 |
author: "Clapper",
|
151 |
description: "",
|
152 |
defaultBgColor: "#151520",
|
153 |
+
defaultTextColor: "#f6d6d8",
|
154 |
defaultPrimaryColor: "#DE4A80",
|
155 |
logoColor: "#DE4A80",
|
156 |
editorBgColor: "#151520",
|
157 |
editorCursorColor: '#DE4A80',
|
158 |
+
editorTextColor: "#f6d6d8",
|
159 |
monitorBgColor: "#151520",
|
160 |
monitorSecondaryTextColor: "#D6D3D1",
|
161 |
monitorPrimaryTextColor: "#DE4A80",
|