Spaces:
Running
on
CPU Upgrade
Running
on
CPU Upgrade
'use client' | |
import { create } from 'zustand' | |
import { Monaco } from '@monaco-editor/react' | |
import MonacoEditor from 'monaco-editor' | |
import { ClapProject, ClapSegmentCategory } from '@aitube/clap' | |
import { | |
TimelineStore, | |
useTimeline, | |
leftBarTrackScaleWidth, | |
} from '@aitube/timeline' | |
import { | |
ScriptEditorStore, | |
EditorView, | |
ScrollData, | |
} from '@aitube/clapper-services' | |
import { getDefaultScriptEditorState } from './getDefaultScriptEditorState' | |
export const useScriptEditor = create<ScriptEditorStore>((set, get) => ({ | |
...getDefaultScriptEditorState(), | |
setMonaco: (monaco?: Monaco) => { | |
set({ monaco }) | |
}, | |
setTextModel: (textModel?: MonacoEditor.editor.ITextModel) => { | |
set({ textModel }) | |
}, | |
setStandaloneCodeEditor: ( | |
standaloneCodeEditor?: MonacoEditor.editor.IStandaloneCodeEditor | |
) => { | |
set({ standaloneCodeEditor }) | |
}, | |
setMouseIsInside: (mouseIsInside: boolean) => { | |
set({ mouseIsInside }) | |
}, | |
loadDraftFromClap: (clap: ClapProject) => { | |
const { setDraft } = get() | |
setDraft(clap.meta.screenplay) | |
}, | |
setDraft: (draft: string) => { | |
const { draft: previousDraft, highlightElements, textModel } = get() | |
if (draft === previousDraft) { | |
return | |
} | |
set({ draft }) | |
if (!textModel) { | |
return | |
} | |
// we need to update the model | |
textModel?.setValue(draft) | |
// and highlight the text again | |
highlightElements() | |
}, | |
publishDraftToTimeline: async (): Promise<void> => { | |
const { draft } = get() | |
console.log(`user asked to update the whole scene! this is expensive..`) | |
// we can do something smart, which is to only reconstruct the impacted segments | |
// and shift the rest along the time axis, without modifying it | |
}, | |
onDidScrollChange: ({ | |
scrollHeight, | |
scrollLeft, | |
scrollTop, | |
scrollWidth, | |
}: ScrollData) => { | |
const { | |
scrollHeight: previousScrollHeight, | |
scrollLeft: previousScrollLeft, | |
scrollTop: previousScrollTop, | |
scrollWidth: previousScrollWidth, | |
mouseIsInside, | |
} = get() | |
// skip if nothing changed | |
if ( | |
scrollHeight === previousScrollHeight && | |
scrollLeft === previousScrollLeft && | |
scrollTop === previousScrollTop && | |
scrollWidth === previousScrollWidth | |
) { | |
return | |
} | |
set({ | |
scrollHeight, | |
scrollLeft, | |
scrollTop, | |
scrollWidth, | |
}) | |
// if the scroll event happened while we where inside the editor, | |
// then we need to dispatch the it | |
if (mouseIsInside) { | |
const timeline: TimelineStore = useTimeline.getState() | |
if (!timeline.timelineCamera || !timeline.timelineControls) { | |
return | |
} | |
const { standaloneCodeEditor } = get() | |
const scrollRatio = scrollTop / scrollHeight | |
const scrollX = Math.round( | |
leftBarTrackScaleWidth + scrollRatio * timeline.contentWidth | |
) | |
/*console.log({ | |
scrollHeight, | |
scrollLeft, | |
scrollTop, | |
scrollWidth, | |
scrollRatio, | |
scrollX | |
}) | |
*/ | |
if (useTimeline.getState().scrolX !== scrollX) { | |
useTimeline.setState({ scrollX }) | |
timeline.timelineCamera.position.setX(scrollX) | |
timeline.timelineControls.target.setX(scrollX) | |
} | |
} | |
}, | |
jumpCursorOnLineClick: (line?: number) => { | |
if (typeof line !== 'number') { | |
return | |
} | |
const timeline: TimelineStore = useTimeline.getState() | |
const { lineNumberToMentionedSegments } = timeline | |
const mentionedSegments = lineNumberToMentionedSegments[line] || [] | |
const firstMentionedSegment = mentionedSegments.at(0) | |
if (typeof firstMentionedSegment?.startTimeInMs !== 'number') { | |
return | |
} | |
const { startTimeInMs } = firstMentionedSegment | |
timeline.setCursorTimestampAtInMs(startTimeInMs) | |
}, | |
highlightElements: () => { | |
const timeline: TimelineStore = useTimeline.getState() | |
const { clap } = timeline | |
const { textModel, standaloneCodeEditor, applyClassNameToKeywords } = get() | |
if (!textModel || !standaloneCodeEditor || !clap) { | |
return | |
} | |
const characters = clap.entities | |
.filter((entity) => entity.category === ClapSegmentCategory.CHARACTER) | |
.map((entity) => entity.triggerName) | |
// any character | |
applyClassNameToKeywords('entity entity-character', characters) | |
// UPPERCASE CHARACTER | |
applyClassNameToKeywords( | |
'entity entity-character entity-highlight', | |
characters, | |
true | |
) | |
const locations = clap.entities | |
.filter((entity) => entity.category === ClapSegmentCategory.LOCATION) | |
.map((entity) => entity.triggerName) | |
// any location | |
applyClassNameToKeywords('entity entity-location', locations) | |
// UPPERCASE LOCATION | |
applyClassNameToKeywords( | |
'entity entity-location entity-highlight', | |
locations, | |
true | |
) | |
}, | |
applyClassNameToKeywords: ( | |
className: string = '', | |
keywords: string[] = [], | |
caseSensitive = false | |
) => { | |
const timeline: TimelineStore = useTimeline.getState() | |
const { clap } = timeline | |
const { textModel, standaloneCodeEditor } = get() | |
if (!textModel || !standaloneCodeEditor || !clap) { | |
return | |
} | |
keywords.forEach((entityTriggerName: string): void => { | |
const matches: MonacoEditor.editor.FindMatch[] = textModel.findMatches( | |
// searchString β The string used to search. If it is a regular expression, set isRegex to true. | |
// searchString: string, | |
entityTriggerName, | |
// @param searchOnlyEditableRange β Limit the searching to only search inside the editable range of the model. | |
// searchOnlyEditableRange: boolean, | |
false, | |
// / @param isRegex β Used to indicate that searchString is a regular expression. | |
// isRegex: boolean, | |
false, | |
// @param matchCase β Force the matching to match lower/upper case exactly. | |
// matchCase: boolean, | |
caseSensitive, | |
// @param wordSeparators β Force the matching to match entire words only. Pass null otherwise. | |
// wordSeparators: string | null, | |
null, | |
// @param captureMatches β The result will contain the captured groups. | |
// captureMatches: boolean, | |
false | |
// limitResultCount β Limit the number of results | |
// limitResultCount?: number | |
) | |
matches.forEach((match: MonacoEditor.editor.FindMatch): void => { | |
standaloneCodeEditor.createDecorationsCollection([ | |
{ | |
range: match.range, | |
options: { | |
isWholeLine: false, | |
inlineClassName: className, | |
}, | |
}, | |
]) | |
}) | |
}) | |
}, | |
setCurrent: (current?: string) => { | |
set({ current }) | |
}, | |
undo: () => {}, | |
redo: () => {}, | |
})) | |