From 0df435d427a05b0c30d42874c47dbbcfef6060a4 Mon Sep 17 00:00:00 2001 From: tjcouch-sil Date: Fri, 14 Oct 2022 09:01:11 -0500 Subject: [PATCH 1/7] Added writing Scripture - by book not implemented --- react-electron-poc/src/main/main.ts | 88 ++++++++++++ react-electron-poc/src/main/preload.ts | 24 ++++ .../TextPanels/ScriptureTextPanelSlate.tsx | 126 ++++++++++++++---- react-electron-poc/src/renderer/preload.d.ts | 11 ++ .../src/renderer/services/ScriptureService.ts | 39 ++++++ .../src/renderer/util/ScriptureUtil.ts | 49 +++++++ react-electron-poc/src/renderer/util/Util.ts | 20 +++ 7 files changed, 328 insertions(+), 29 deletions(-) diff --git a/react-electron-poc/src/main/main.ts b/react-electron-poc/src/main/main.ts index c3f585d..ff4699d 100644 --- a/react-electron-poc/src/main/main.ts +++ b/react-electron-poc/src/main/main.ts @@ -164,6 +164,41 @@ async function getFilesText(filePaths: string[], delay = 0): Promise { ); } +/** + * Writes the string to a file asynchronously. Delays 1ms or more if desired + * @param filePath Path to file from assets + * @param fileContents string to write into the file + * @param delay delay before resolving promise in ms + * @returns promise that resolves after delay ms and then writing the fil + */ +async function writeFileText( + filePath: string, + fileContents: string, + delay = 0, +): Promise { + return delayPromise((resolve, reject) => { + const start = performance.now(); + fs.writeFile(getAssetPath(filePath), fileContents, (err) => { + if (err) reject(err.message); + else resolve(); + console.log( + `Writing ${filePath} took ${performance.now() - start} ms`, + ); + }); + }, delay); +} + +async function writeFilesText( + files: { filePath: string; fileContents: string }[], + delay = 0, +): Promise { + return Promise.all( + files.map(({ filePath, fileContents }) => + writeFileText(filePath, fileContents, delay), + ), + ); +} + /** Simulating how long it may take Paratext to load and serve the Scriptures */ const getScriptureDelay = 75; /** Simulating how long it may take Paratext to serve the resource info */ @@ -283,6 +318,38 @@ async function handleGetScriptureChapter( } } +async function handleWriteScriptureBook( + _event: IpcMainInvokeEvent, + fileExtension: string, + shortName: string, + bookNum: number, + contents: ScriptureChapter[], +): Promise { + throw new Error( + 'writeScriptureBook was deemed not necessary for this POC and was not implemented', + ); +} + +async function handleWriteScriptureChapter( + _event: IpcMainInvokeEvent, + fileExtension: string, + shortName: string, + bookNum: number, + chapter: number, + contents: ScriptureChapter, +): Promise { + try { + return await writeFileText( + `testScripture/${shortName}/${bookNum}-${chapter}.${fileExtension}`, + contents.contents as string, + getScriptureDelay, + ); + } catch (e) { + console.log(e); + throw new Error(`Failed to write ${shortName} ${bookNum} ${chapter}`); + } +} + /** These test files are from breakpointing at UsfmSinglePaneControl.cs at the line that gets Css in LoadUsfm. */ async function handleGetScriptureStyle( _event: IpcMainInvokeEvent, @@ -359,6 +426,27 @@ const ipcHandlers: { bookNum: number, chapter: number, ) => handleGetScriptureChapter(event, 'json', shortName, bookNum, chapter), + 'ipc-scripture:writeScriptureBook': ( + event, + shortName: string, + bookNum: number, + contents: ScriptureChapter[], + ) => handleWriteScriptureBook(event, 'json', shortName, bookNum, contents), + 'ipc-scripture:writeScriptureChapter': ( + event, + shortName: string, + bookNum: number, + chapter: number, + contents: ScriptureChapter, + ) => + handleWriteScriptureChapter( + event, + 'json', + shortName, + bookNum, + chapter, + contents, + ), 'ipc-scripture:getScriptureBookRaw': ( event, shortName: string, diff --git a/react-electron-poc/src/main/preload.ts b/react-electron-poc/src/main/preload.ts index e4eb112..f804565 100644 --- a/react-electron-poc/src/main/preload.ts +++ b/react-electron-poc/src/main/preload.ts @@ -38,6 +38,30 @@ contextBridge.exposeInMainWorld('electronAPI', { bookNum, chapter, ), + writeScriptureBook: ( + shortName: string, + bookNum: number, + contents: ScriptureChapterContent[], + ): Promise => + ipcRenderer.invoke( + 'ipc-scripture:writeScriptureBook', + shortName, + bookNum, + contents, + ), + writeScriptureChapter: ( + shortName: string, + bookNum: number, + chapter: number, + contents: ScriptureChapterContent, + ): Promise => + ipcRenderer.invoke( + 'ipc-scripture:writeScriptureChapter', + shortName, + bookNum, + chapter, + contents, + ), getScriptureBookRaw: ( shortName: string, bookNum: number, diff --git a/react-electron-poc/src/renderer/components/panels/TextPanels/ScriptureTextPanelSlate.tsx b/react-electron-poc/src/renderer/components/panels/TextPanels/ScriptureTextPanelSlate.tsx index 60e89ad..2848744 100644 --- a/react-electron-poc/src/renderer/components/panels/TextPanels/ScriptureTextPanelSlate.tsx +++ b/react-electron-poc/src/renderer/components/panels/TextPanels/ScriptureTextPanelSlate.tsx @@ -1,7 +1,8 @@ -import { getScripture } from '@services/ScriptureService'; +import { getScripture, writeScripture } from '@services/ScriptureService'; import { ChapterElementProps, CharElementProps, + CustomDescendant, CustomElement, CustomElementProps, CustomSlateEditor, @@ -16,7 +17,7 @@ import { ScriptureContentChunk, VerseElementProps, } from '@shared/data/ScriptureTypes'; -import { debounce, isString, isValidValue, newGuid } from '@util/Util'; +import { debounce, groupBy, isString, isValidValue, newGuid } from '@util/Util'; import React, { createElement, CSSProperties, @@ -51,10 +52,11 @@ import { useSlate, } from 'slate-react'; import { - getLastVerseInScriptureContents, + chunkScriptureChapter, getTextFromScrRef, parseChapter, parseVerse, + unchunkScriptureContent, } from '@util/ScriptureUtil'; import { withHistory } from 'slate-history'; import isHotkey from 'is-hotkey'; @@ -589,17 +591,21 @@ const getScriptureChunkEditorSlateId = ( interface ScriptureChunkEditorSlateProps extends Omit { - virtualizedIndex: number; + chunkIndex: number; virtualizedStyle: CSSProperties; editorGuid: string; scrChapterChunk: ScriptureContentChunk; searchString: string | null; notifyUpdatedScrRef: () => void; + updateScrChapterChunk: ( + chunkIndex: number, + updatedScrChapterChunk: ScriptureContentChunk, + ) => void; } const ScriptureChunkEditorSlate = memo( ({ - virtualizedIndex, + chunkIndex, virtualizedStyle, editorGuid, editable, @@ -611,11 +617,25 @@ const ScriptureChunkEditorSlate = memo( scrChapterChunk, searchString, notifyUpdatedScrRef, + updateScrChapterChunk, }: ScriptureChunkEditorSlateProps) => { // Slate editor // TODO: Put in a useEffect listening for scrChapters and create editors for the number of chapters const [editor] = useState(createSlateEditor); + /** When the contents are changed, update the chapter chunk */ + const onChange = useCallback( + (value: CustomDescendant[]) => { + // Filter out changes that are just selection changes - thanks to the Slate tutorial https://docs.slatejs.org/walkthroughs/06-saving-to-a-database + if (editor.operations.some((op) => op.type !== 'set_selection')) + updateScrChapterChunk(chunkIndex, { + ...scrChapterChunk, + contents: value, + }); + }, + [editor, updateScrChapterChunk, chunkIndex, scrChapterChunk], + ); + // Focus the editor when we close the search bar // TODO: Could do this directly on the hotkey useEffect(() => { @@ -843,7 +863,7 @@ const ScriptureChunkEditorSlate = memo( id={getScriptureChunkEditorSlateId( editorGuid, chapter, - virtualizedIndex, + chunkIndex, )} > {/* ------------------- @@ -852,7 +872,11 @@ const ScriptureChunkEditorSlate = memo( }/${virtualizedIndex}: ${JSON.stringify( scrChapterChunk.contents, ).substring(0, 20)}`} */} - + { const { scrChapters, onFocus, ...scrChunkEditorSlateProps } = props; - const { book, chapter, verse, useVirtualization } = - scrChunkEditorSlateProps; + const { + browseBook, + shortName, + book, + chapter, + verse, + useVirtualization, + } = scrChunkEditorSlateProps; // Search string for search highlighting. When null, don't show the search box. When '' or other, show the search box const [searchString, setSearchString] = useState(null); @@ -959,29 +989,15 @@ export const ScriptureTextPanelSlate = ScriptureTextPanelHOC( ] : scrChapter.contents; + // If not virtualizing, create one chunk per chapter const chunkSize = useVirtualization ? CHUNK_SIZE : scrChapterContents.length; - const chapterChunks: ScriptureContentChunk[] = []; - for ( - let i = 0; - i < Math.ceil(scrChapterContents.length / chunkSize); - i++ - ) { - const chunkContents = scrChapterContents.slice( - i * chunkSize, - i * chunkSize + chunkSize, - ); - chapterChunks.push({ - chapter: scrChapter.chapter, - chunkNum: i, - finalVerse: - getLastVerseInScriptureContents(chunkContents), - contents: chunkContents, - }); - } - return chapterChunks; + return chunkScriptureChapter( + { ...scrChapter, contents: scrChapterContents }, + chunkSize, + ); }); } return []; @@ -1145,6 +1161,57 @@ export const ScriptureTextPanelSlate = ScriptureTextPanelHOC( didIUpdateScrRef.current = true; }, []); + /** + * Updates the Scripture chunk at the provided index with updated contents. + * DOES NOT update the chunk's reference in order to avoid React re-rendering. Could potentially pose some issues somewhere or another + * @param chunkIndex which chunk index had a change + * @param scrChapterChunk the updated Scripture chunk + */ + const updateScrChapterChunk = useCallback( + ( + chunkIndex: number, + updatedScrChapterChunk: ScriptureContentChunk, + ) => { + const scrChapterChunk = scrChaptersChunked[chunkIndex]; + scrChapterChunk.contents = updatedScrChapterChunk.contents; + + const start = performance.now(); + // Group the chunks by chapter + const scrChapterChunkMap = groupBy( + scrChaptersChunked, + (chapterChunk) => chapterChunk.chapter, + ); + + // Reassemble the chunks into scrChapters and write + const scrChaptersUnchunked: ScriptureChapterContent[] = []; + // eslint-disable-next-line no-restricted-syntax + for (const [ + chunkChapter, + chapterChunks, + ] of scrChapterChunkMap.entries()) { + scrChaptersUnchunked.push( + unchunkScriptureContent(chapterChunks, chunkChapter), + ); + } + + console.log( + `Unchunking Scripture Chapters took ${ + performance.now() - start + } ms`, + ); + + writeScripture( + shortName, + book, + // Save whole book if we're browsing by book because why not + // TODO: save only the relevant chapter for performance + browseBook ? -1 : chapter, + scrChaptersUnchunked, + ); + }, + [scrChaptersChunked, shortName, browseBook, book, chapter], + ); + // When the scrRef changes, tell the virtualized list to scroll to the appropriate chunk // TODO: When you figure out how not to recreate the editors every time the scrRef changes, allow the chunks to scroll to the scrRefs for themselves (as in non-virtualized) useEffect(() => { @@ -1231,7 +1298,7 @@ export const ScriptureTextPanelSlate = ScriptureTextPanelHOC( attributes={{} as never} > ); diff --git a/react-electron-poc/src/renderer/preload.d.ts b/react-electron-poc/src/renderer/preload.d.ts index 09ffe21..ff6738c 100644 --- a/react-electron-poc/src/renderer/preload.d.ts +++ b/react-electron-poc/src/renderer/preload.d.ts @@ -18,6 +18,17 @@ declare global { bookNum: number, chapter: number, ): Promise; + writeScriptureBook( + shortName: string, + bookNum: number, + contents: ScriptureChapterContent[], + ): Promise; + writeScriptureChapter( + shortName: string, + bookNum: number, + chapter: number, + contents: ScriptureChapterContent, + ): Promise; getScriptureBookRaw( shortName: string, bookNum: number, diff --git a/react-electron-poc/src/renderer/services/ScriptureService.ts b/react-electron-poc/src/renderer/services/ScriptureService.ts index 59c0145..8bb9aa6 100644 --- a/react-electron-poc/src/renderer/services/ScriptureService.ts +++ b/react-electron-poc/src/renderer/services/ScriptureService.ts @@ -60,6 +60,45 @@ export const getScripture = async ( } }; +/** + * Writes the specified Scripture chapter in the specified book from the specified project in Slate JSON + * @param shortName the short name of the project + * @param bookNum number of book to write + * @param chapter number of chapter to write. Defaults to -1 meaning the whole book + * @returns Promise that resolves true when writing is finished or false if there was an exception + */ +export const writeScripture = async ( + shortName: string, + bookNum: number, + chapter = -1, + contents: ScriptureChapterContent[], +): Promise => { + try { + const contentsJSON = contents.map((content) => ({ + ...content, + contents: JSON.stringify(content.contents, null, 4), + })) as unknown as ScriptureChapterContent[]; + if (chapter >= 0) + await window.electronAPI.scripture.writeScriptureChapter( + shortName, + bookNum, + chapter, + contentsJSON[0], + ); + else + await window.electronAPI.scripture.writeScriptureBook( + shortName, + bookNum, + contentsJSON, + ); + return true; + } catch (e) { + console.log(e); + return false; + } + // Make sure to stringify the contents before sending them over +}; + /** * Gets the specified Scripture chapter in the specified book from the specified project in USX * @param shortName the short name of the project diff --git a/react-electron-poc/src/renderer/util/ScriptureUtil.ts b/react-electron-poc/src/renderer/util/ScriptureUtil.ts index 96e5d17..0be831f 100644 --- a/react-electron-poc/src/renderer/util/ScriptureUtil.ts +++ b/react-electron-poc/src/renderer/util/ScriptureUtil.ts @@ -1,7 +1,9 @@ import { CustomElement, FormattedText, + ScriptureChapterContent, ScriptureContent, + ScriptureContentChunk, ScriptureReference, } from '@shared/data/ScriptureTypes'; import { isString, isValidValue } from './Util'; @@ -343,3 +345,50 @@ export const getLastVerseInScriptureContents = ( } return defaultVerse; }; + +// Scripture Chunking functions + +/** + * Splits the contents of the Scripture chapter supplied into chunks of size chunkSize + * @param scrChapter the chapter contents to split into chunks + * @param chunkSize max number of Scripture contents to put in each chunk + * @returns array of Scripture content chunks which together make up the chapter + */ +export const chunkScriptureChapter = ( + scrChapter: ScriptureChapterContent, + chunkSize: number, +): ScriptureContentChunk[] => { + const chapterChunks: ScriptureContentChunk[] = []; + for ( + let i = 0; + i < Math.ceil(scrChapter.contents.length / chunkSize); + i++ + ) { + const chunkContents = scrChapter.contents.slice( + i * chunkSize, + i * chunkSize + chunkSize, + ); + chapterChunks.push({ + chapter: scrChapter.chapter, + chunkNum: i, + finalVerse: getLastVerseInScriptureContents(chunkContents), + contents: chunkContents, + }); + } + return chapterChunks; +}; + +/** + * Combines chunks of Scripture content into the content of a scripture Chapter. + * Note: this does not currently respect the chunks' chunkNum order. It just assembles in the array order. + * @param scrChapterChunks array of Scripture content chunks which together make up a chapter + * @param chapter the chapter number to assemble + * @returns Assembled Scripture chapter whose content is the combined chunks + */ +export const unchunkScriptureContent = ( + scrChapterChunks: ScriptureContentChunk[], + chapter: number, +): ScriptureChapterContent => ({ + chapter, + contents: scrChapterChunks.flatMap((chapterChunk) => chapterChunk.contents), +}); diff --git a/react-electron-poc/src/renderer/util/Util.ts b/react-electron-poc/src/renderer/util/Util.ts index 70c96fd..60e7291 100644 --- a/react-electron-poc/src/renderer/util/Util.ts +++ b/react-electron-poc/src/renderer/util/Util.ts @@ -45,6 +45,26 @@ export function debounce void>( }) as T; } +/** + * Groups each item in the array of items into a map according to the keySelector + * @param items array of items to group by + * @param keySelector function to run on each item to get the key for the group to which it belongs + * @returns map of keys to groups of items + */ +export function groupBy( + items: V[], + keySelector: (item: V) => K, +): Map> { + const map = new Map(); + items.forEach((item) => { + const key = keySelector(item); + const group = map.get(key); + if (group) group.push(item); + else map.set(key, [item]); + }); + return map; +} + /** string[] of element tags that cannot have contents */ export const voidElements: string[] = [ 'area', From ecd9b2464e96f2e0abc9e68174eff4fac56efe58 Mon Sep 17 00:00:00 2001 From: tjcouch-sil Date: Fri, 14 Oct 2022 10:27:37 -0500 Subject: [PATCH 2/7] Finished implementing writeScripture API --- .../TextPanels/ScriptureTextPanelSlate.tsx | 43 +++++++++---------- 1 file changed, 20 insertions(+), 23 deletions(-) diff --git a/react-electron-poc/src/renderer/components/panels/TextPanels/ScriptureTextPanelSlate.tsx b/react-electron-poc/src/renderer/components/panels/TextPanels/ScriptureTextPanelSlate.tsx index 2848744..ab83a92 100644 --- a/react-electron-poc/src/renderer/components/panels/TextPanels/ScriptureTextPanelSlate.tsx +++ b/react-electron-poc/src/renderer/components/panels/TextPanels/ScriptureTextPanelSlate.tsx @@ -1162,7 +1162,7 @@ export const ScriptureTextPanelSlate = ScriptureTextPanelHOC( }, []); /** - * Updates the Scripture chunk at the provided index with updated contents. + * Updates the Scripture chunk at the provided index with updated contents and saves the edited chapter. * DOES NOT update the chunk's reference in order to avoid React re-rendering. Could potentially pose some issues somewhere or another * @param chunkIndex which chunk index had a change * @param scrChapterChunk the updated Scripture chunk @@ -1172,44 +1172,41 @@ export const ScriptureTextPanelSlate = ScriptureTextPanelHOC( chunkIndex: number, updatedScrChapterChunk: ScriptureContentChunk, ) => { - const scrChapterChunk = scrChaptersChunked[chunkIndex]; - scrChapterChunk.contents = updatedScrChapterChunk.contents; + const editedScrChapterChunk = scrChaptersChunked[chunkIndex]; + const editedChapter = editedScrChapterChunk.chapter; + editedScrChapterChunk.contents = + updatedScrChapterChunk.contents; const start = performance.now(); - // Group the chunks by chapter - const scrChapterChunkMap = groupBy( - scrChaptersChunked, - (chapterChunk) => chapterChunk.chapter, + // Reassemble the edited chapter and send it to the backend to save (no need to save the whole book) + // Get the chunks for the chapter that was edited + const editedScrChapterChunks = scrChaptersChunked.filter( + (chapterChunk) => chapterChunk.chapter === editedChapter, ); // Reassemble the chunks into scrChapters and write - const scrChaptersUnchunked: ScriptureChapterContent[] = []; - // eslint-disable-next-line no-restricted-syntax - for (const [ - chunkChapter, - chapterChunks, - ] of scrChapterChunkMap.entries()) { - scrChaptersUnchunked.push( - unchunkScriptureContent(chapterChunks, chunkChapter), - ); - } + const editedScrChaptersUnchunked: ScriptureChapterContent[] = [ + unchunkScriptureContent( + editedScrChapterChunks, + editedChapter, + ), + ]; console.log( - `Unchunking Scripture Chapters took ${ + `Performance: Unchunking Scripture Chapter ${editedChapter} for saving took ${ performance.now() - start } ms`, ); + // Send the chapter to the backend for saving writeScripture( shortName, book, - // Save whole book if we're browsing by book because why not - // TODO: save only the relevant chapter for performance - browseBook ? -1 : chapter, - scrChaptersUnchunked, + editedChapter, + editedScrChaptersUnchunked, ); }, - [scrChaptersChunked, shortName, browseBook, book, chapter], + [scrChaptersChunked, shortName, book], ); // When the scrRef changes, tell the virtualized list to scroll to the appropriate chunk From 304c059e4a6d10aaf450d2ffe215c7e6c9ad0102 Mon Sep 17 00:00:00 2001 From: tjcouch-sil Date: Fri, 14 Oct 2022 10:53:27 -0500 Subject: [PATCH 3/7] Added lots more marker styles --- .../TextPanels/ScriptureTextPanelSlate.tsx | 61 ++++++++++++++++++- 1 file changed, 60 insertions(+), 1 deletion(-) diff --git a/react-electron-poc/src/renderer/components/panels/TextPanels/ScriptureTextPanelSlate.tsx b/react-electron-poc/src/renderer/components/panels/TextPanels/ScriptureTextPanelSlate.tsx index ab83a92..afb7b3d 100644 --- a/react-electron-poc/src/renderer/components/panels/TextPanels/ScriptureTextPanelSlate.tsx +++ b/react-electron-poc/src/renderer/components/panels/TextPanels/ScriptureTextPanelSlate.tsx @@ -198,14 +198,73 @@ const EditorElements: { [type: string]: ElementInfo } = { validStyles: [ { style: 'p' }, { style: 'q' }, + { style: 'q1' }, { style: 'q2' }, + { style: 'q3' }, + { style: 'q4' }, + { style: 'd' }, + { style: 'sp' }, { style: 'b' }, + { style: 'pb' }, + { style: 'h' }, + { style: 'rem' }, + { style: 'toc' }, + { style: 'toc1' }, + { style: 'toc2' }, + { style: 'toc3' }, + { style: 'imt' }, + { style: 'imt1' }, + { style: 'imt2' }, + { style: 'imt3' }, + { style: 'imt4' }, + { style: 'imte' }, + { style: 'imte1' }, + { style: 'imte2' }, + { style: 'mt' }, + { style: 'mt1' }, + { style: 'mt2' }, + { style: 'mt3' }, + { style: 'mt4' }, + { style: 'mte' }, + { style: 'mte1' }, + { style: 'mte2' }, + { style: 'ms' }, + { style: 'ms1' }, + { style: 'is' }, + { style: 'is1' }, + { style: 'is2' }, + { style: 's' }, + { style: 's1' }, + { style: 's2' }, + { style: 'iot' }, + { style: 'io' }, + { style: 'io1' }, + { style: 'io2' }, + { style: 'io3' }, + { style: 'io4' }, + { style: 'lit' }, + { style: 'id' }, ], }, char: { component: CharElement, inline: true, - validStyles: [{ style: 'nd', canClose: true }], + validStyles: [ + { style: 'nd', canClose: true }, + { style: 'bk', canClose: true }, + { style: 'pn', canClose: true }, + { style: 'wj', canClose: true }, + { style: 'k', canClose: true }, + { style: 'ord', canClose: true }, + { style: 'add', canClose: true }, + { style: 'no', canClose: true }, + { style: 'it', canClose: true }, + { style: 'bd', canClose: true }, + { style: 'bdit', canClose: true }, + { style: 'em', canClose: true }, + { style: 'sc', canClose: true }, + { style: 'sup', canClose: true }, + ], }, chapter: { component: ChapterElement, From 9f390ee3882416e723d70c32fe7fbdc609a91817 Mon Sep 17 00:00:00 2001 From: tjcouch-sil Date: Fri, 14 Oct 2022 10:58:09 -0500 Subject: [PATCH 4/7] Added a few more useful marker styles --- .../components/panels/TextPanels/ScriptureTextPanelSlate.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/react-electron-poc/src/renderer/components/panels/TextPanels/ScriptureTextPanelSlate.tsx b/react-electron-poc/src/renderer/components/panels/TextPanels/ScriptureTextPanelSlate.tsx index afb7b3d..81d88ba 100644 --- a/react-electron-poc/src/renderer/components/panels/TextPanels/ScriptureTextPanelSlate.tsx +++ b/react-electron-poc/src/renderer/components/panels/TextPanels/ScriptureTextPanelSlate.tsx @@ -202,6 +202,7 @@ const EditorElements: { [type: string]: ElementInfo } = { { style: 'q2' }, { style: 'q3' }, { style: 'q4' }, + { style: 'qr' }, { style: 'd' }, { style: 'sp' }, { style: 'b' }, @@ -230,6 +231,8 @@ const EditorElements: { [type: string]: ElementInfo } = { { style: 'mte2' }, { style: 'ms' }, { style: 'ms1' }, + { style: 'mr' }, + { style: 'r' }, { style: 'is' }, { style: 'is1' }, { style: 'is2' }, From 3aa4f46e362ab5dc26746b8062f8d4d4be1cbc1d Mon Sep 17 00:00:00 2001 From: tjcouch-sil Date: Fri, 14 Oct 2022 11:17:59 -0500 Subject: [PATCH 5/7] Updated chunk height when changing text, misc fixes --- react-electron-poc/src/main/main.ts | 12 +- .../TextPanels/ScriptureTextPanelHOC.tsx | 1 - .../TextPanels/ScriptureTextPanelSlate.tsx | 129 +++++++++--------- 3 files changed, 70 insertions(+), 72 deletions(-) diff --git a/react-electron-poc/src/main/main.ts b/react-electron-poc/src/main/main.ts index ff4699d..50eabb5 100644 --- a/react-electron-poc/src/main/main.ts +++ b/react-electron-poc/src/main/main.ts @@ -188,7 +188,7 @@ async function writeFileText( }, delay); } -async function writeFilesText( +/* async function writeFilesText( files: { filePath: string; fileContents: string }[], delay = 0, ): Promise { @@ -197,7 +197,7 @@ async function writeFilesText( writeFileText(filePath, fileContents, delay), ), ); -} +} */ /** Simulating how long it may take Paratext to load and serve the Scriptures */ const getScriptureDelay = 75; @@ -320,10 +320,10 @@ async function handleGetScriptureChapter( async function handleWriteScriptureBook( _event: IpcMainInvokeEvent, - fileExtension: string, - shortName: string, - bookNum: number, - contents: ScriptureChapter[], + _fileExtension: string, + _shortName: string, + _bookNum: number, + _contents: ScriptureChapter[], ): Promise { throw new Error( 'writeScriptureBook was deemed not necessary for this POC and was not implemented', diff --git a/react-electron-poc/src/renderer/components/panels/TextPanels/ScriptureTextPanelHOC.tsx b/react-electron-poc/src/renderer/components/panels/TextPanels/ScriptureTextPanelHOC.tsx index 50bb2eb..b98aa7f 100644 --- a/react-electron-poc/src/renderer/components/panels/TextPanels/ScriptureTextPanelHOC.tsx +++ b/react-electron-poc/src/renderer/components/panels/TextPanels/ScriptureTextPanelHOC.tsx @@ -14,7 +14,6 @@ import { memo, PropsWithChildren, useCallback, - useEffect, useState, } from 'react'; import usePromise from 'renderer/hooks/usePromise'; diff --git a/react-electron-poc/src/renderer/components/panels/TextPanels/ScriptureTextPanelSlate.tsx b/react-electron-poc/src/renderer/components/panels/TextPanels/ScriptureTextPanelSlate.tsx index 81d88ba..931170d 100644 --- a/react-electron-poc/src/renderer/components/panels/TextPanels/ScriptureTextPanelSlate.tsx +++ b/react-electron-poc/src/renderer/components/panels/TextPanels/ScriptureTextPanelSlate.tsx @@ -17,7 +17,7 @@ import { ScriptureContentChunk, VerseElementProps, } from '@shared/data/ScriptureTypes'; -import { debounce, groupBy, isString, isValidValue, newGuid } from '@util/Util'; +import { debounce, isString, isValidValue, newGuid } from '@util/Util'; import React, { createElement, CSSProperties, @@ -639,17 +639,15 @@ const EST_CHUNK_HEIGHT = 710; const SLATE_VIRTUALIZED_LOAD_TIME = 200; /** - * Get the id for the ScriptureContnetChunkInfo div holding the contents of the editor chunk + * Get the id for the ScriptureContetChunkInfo div holding the contents of the editor chunk * @param editorGuid Unique ID for this editor - * @param chapter Chapter number - * @param chunkNum Chunk number within this chapter + * @param chunkIndex Index of chunk in the virtualized list * @returns Id string for div holding the editor chunk contents */ const getScriptureChunkEditorSlateId = ( editorGuid: string, - chapter: number, - chunkNum: number, -) => `scrChunkEd-${editorGuid}-${chapter}-${chunkNum}`; + chunkIndex: number, +) => `scrChunkEd-${editorGuid}-${chunkIndex}`; interface ScriptureChunkEditorSlateProps extends Omit { @@ -922,11 +920,7 @@ const ScriptureChunkEditorSlate = memo( return (
{/* ------------------- {`${ @@ -997,14 +991,8 @@ export const ScriptureTextPanelSlate = ScriptureTextPanelHOC( (props: ScriptureTextPanelSlateProps) => { const { scrChapters, onFocus, ...scrChunkEditorSlateProps } = props; - const { - browseBook, - shortName, - book, - chapter, - verse, - useVirtualization, - } = scrChunkEditorSlateProps; + const { shortName, book, chapter, verse, useVirtualization } = + scrChunkEditorSlateProps; // Search string for search highlighting. When null, don't show the search box. When '' or other, show the search box const [searchString, setSearchString] = useState(null); @@ -1073,22 +1061,26 @@ export const ScriptureTextPanelSlate = ScriptureTextPanelHOC( const editorGuid = useRef(newGuid().substring(0, 8)); /** Invalidate virtualized-list-cached chunk heights and get again from our cache or recalculate from DOM */ - const onItemsRendered = ({ - overscanStartIndex, - }: ListOnItemsRenderedProps) => { - if (virtualizedList.current) - virtualizedList.current.resetAfterIndex(overscanStartIndex); - }; + const onItemsRendered = useCallback( + ({ overscanStartIndex }: ListOnItemsRenderedProps) => { + if (virtualizedList.current) + virtualizedList.current.resetAfterIndex(overscanStartIndex); + }, + [], + ); /** * Invalidate virtualized-list-cached chunk heights and get again from our cache or recalculate from DOM * @param chunkIndex the chunk index at and after which to invalidate chunk heights. Defaults to 0 (all chunks) */ - const invalidateVirtualizedListCachedHeights = (chunkIndex = 0) => { - onItemsRendered({ - overscanStartIndex: chunkIndex >= 0 ? chunkIndex : 0, - } as ListOnItemsRenderedProps); - }; + const invalidateVirtualizedListCachedHeights = useCallback( + (chunkIndex = 0) => { + onItemsRendered({ + overscanStartIndex: chunkIndex >= 0 ? chunkIndex : 0, + } as ListOnItemsRenderedProps); + }, + [onItemsRendered], + ); /** At startup, calculate editor chunk heights in the DOM */ useEffect(() => { @@ -1096,7 +1088,7 @@ export const ScriptureTextPanelSlate = ScriptureTextPanelHOC( invalidateVirtualizedListCachedHeights, SLATE_VIRTUALIZED_LOAD_TIME, ); - }, []); + }, [invalidateVirtualizedListCachedHeights]); /** Our cache of height of each editor chunk if measured in the DOM. Does not include estimates unlike the virtualized-list-cached heights */ const editorChunkHeights = useRef<(ChunkHeight | undefined)[]>([]); @@ -1106,29 +1098,34 @@ export const ScriptureTextPanelSlate = ScriptureTextPanelHOC( * @param chunkIndex index of the chunk to clear * @param hard if true, completely deletes the cached height. If false (default), marks it stale for recalculating next time the chunk is in the DOM */ - const invalidateCachedChunkHeight = ( - chunkIndex: number, - hard = false, - ) => { - // Invalidate all chunk heights - if (chunkIndex < 0) { - if (!hard) { - editorChunkHeights.current.forEach((editorChunkHeight) => { - if (editorChunkHeight && !editorChunkHeight.stale) - editorChunkHeight.stale = true; - }); - } else editorChunkHeights.current = []; - } - // Invalidate a particular chunk height - else if (!hard) { - const cachedChunkHeight = - editorChunkHeights.current[chunkIndex]; - if (cachedChunkHeight) cachedChunkHeight.stale = true; - } else { - editorChunkHeights.current[chunkIndex] = undefined; - } - invalidateVirtualizedListCachedHeights(chunkIndex); - }; + const invalidateCachedChunkHeight = useCallback( + (chunkIndex: number, hard = false) => { + // Invalidate all chunk heights + if (chunkIndex < 0) { + if (!hard) { + editorChunkHeights.current.forEach( + (editorChunkHeight) => { + if ( + editorChunkHeight && + !editorChunkHeight.stale + ) + editorChunkHeight.stale = true; + }, + ); + } else editorChunkHeights.current = []; + } + // Invalidate a particular chunk height + else if (!hard) { + const cachedChunkHeight = + editorChunkHeights.current[chunkIndex]; + if (cachedChunkHeight) cachedChunkHeight.stale = true; + } else { + editorChunkHeights.current[chunkIndex] = undefined; + } + invalidateVirtualizedListCachedHeights(chunkIndex); + }, + [invalidateVirtualizedListCachedHeights], + ); /** * Current scrollOffset aka viewport position for the virtualized view. @@ -1137,26 +1134,22 @@ export const ScriptureTextPanelSlate = ScriptureTextPanelHOC( const virtualizedScrollOffset = useRef(0); /** Keep track of current offset */ - const onScroll = ({ scrollOffset }: ListOnScrollProps) => { + const onScroll = useCallback(({ scrollOffset }: ListOnScrollProps) => { virtualizedScrollOffset.current = scrollOffset; - }; + }, []); /** * Get the size of the editor chunk for virtualization. Gets size from the DOM or estimates size * @param chunkIndex chunk index for the editor chunk * @returns height of editor chunk */ - const getChunkHeight = (chunkIndex: number) => { + const getChunkHeight = useCallback((chunkIndex: number) => { const cachedChunkHeight = editorChunkHeights.current[chunkIndex]; if (cachedChunkHeight && !cachedChunkHeight.stale) return cachedChunkHeight.height; const editorChunk = document.getElementById( - getScriptureChunkEditorSlateId( - editorGuid.current, - chapter, - chunkIndex, - ), + getScriptureChunkEditorSlateId(editorGuid.current, chunkIndex), ); if (editorChunk) { const editorChunkHeight = editorChunk.scrollHeight; @@ -1195,7 +1188,7 @@ export const ScriptureTextPanelSlate = ScriptureTextPanelHOC( } } return cachedChunkHeight?.height || EST_CHUNK_HEIGHT; - }; + }, []); /** Delay invalidating cached chunk heights partially to reduce lag * and partially because there is a problem where the elements may be @@ -1267,8 +1260,14 @@ export const ScriptureTextPanelSlate = ScriptureTextPanelHOC( editedChapter, editedScrChaptersUnchunked, ); + + // Invalidate the updated chunk's cached height so we recalculate in case we added a line or something + // TODO: setTimeout required because we need to wait for Slate to update the editor chunk to the new height. Clean this up by listening for the end of slate changes somehow? + setTimeout(() => { + invalidateCachedChunkHeight(chunkIndex); + }, 1); }, - [scrChaptersChunked, shortName, book], + [scrChaptersChunked, shortName, book, invalidateCachedChunkHeight], ); // When the scrRef changes, tell the virtualized list to scroll to the appropriate chunk From 0489bdaa2175d71123cd9862703cfbcf789a3f06 Mon Sep 17 00:00:00 2001 From: tjcouch-sil Date: Fri, 14 Oct 2022 11:19:05 -0500 Subject: [PATCH 6/7] Moved @types/is-hotkey to dev dependency --- react-electron-poc/package-lock.json | 2 +- react-electron-poc/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/react-electron-poc/package-lock.json b/react-electron-poc/package-lock.json index 7321d33..d615da3 100644 --- a/react-electron-poc/package-lock.json +++ b/react-electron-poc/package-lock.json @@ -7,7 +7,6 @@ "hasInstallScript": true, "license": "MIT", "dependencies": { - "@types/is-hotkey": "^0.1.7", "dockview": "^1.5.1", "electron-debug": "^3.2.0", "electron-log": "^4.4.8", @@ -28,6 +27,7 @@ "@teamsupercell/typings-for-css-modules-loader": "^2.5.1", "@testing-library/jest-dom": "^5.16.5", "@testing-library/react": "^13.3.0", + "@types/is-hotkey": "^0.1.7", "@types/jest": "^28.1.7", "@types/node": "18.7.6", "@types/react": "^18.0.17", diff --git a/react-electron-poc/package.json b/react-electron-poc/package.json index 8280375..25afa1d 100644 --- a/react-electron-poc/package.json +++ b/react-electron-poc/package.json @@ -110,7 +110,6 @@ } }, "dependencies": { - "@types/is-hotkey": "^0.1.7", "dockview": "^1.5.1", "electron-debug": "^3.2.0", "electron-log": "^4.4.8", @@ -131,6 +130,7 @@ "@teamsupercell/typings-for-css-modules-loader": "^2.5.1", "@testing-library/jest-dom": "^5.16.5", "@testing-library/react": "^13.3.0", + "@types/is-hotkey": "^0.1.7", "@types/jest": "^28.1.7", "@types/node": "18.7.6", "@types/react": "^18.0.17", From 43338f22618dee53b8693db4fc40061395fb4748 Mon Sep 17 00:00:00 2001 From: tjcouch-sil Date: Fri, 14 Oct 2022 13:28:16 -0500 Subject: [PATCH 7/7] Prevent ScrRefSelector from going below lowest values --- .../src/renderer/components/Components.css | 5 + .../renderer/components/ScrRefSelector.tsx | 137 ++++++++++-------- .../TextPanels/ScriptureTextPanelSlate.tsx | 9 +- .../src/renderer/services/ScriptureService.ts | 11 +- .../src/renderer/util/ScriptureUtil.ts | 29 ++-- 5 files changed, 121 insertions(+), 70 deletions(-) diff --git a/react-electron-poc/src/renderer/components/Components.css b/react-electron-poc/src/renderer/components/Components.css index 7d9dc5a..9ce2f6c 100644 --- a/react-electron-poc/src/renderer/components/Components.css +++ b/react-electron-poc/src/renderer/components/Components.css @@ -33,6 +33,11 @@ width: 4ch } +.scr-toolbar .selector-area .change-btns { + width: auto; + margin: 0px; +} + .scr-toolbar .selector-area .change-btn { padding: 2px 1px; border-right: 1px solid #505070; diff --git a/react-electron-poc/src/renderer/components/ScrRefSelector.tsx b/react-electron-poc/src/renderer/components/ScrRefSelector.tsx index fefdd6a..489cb10 100644 --- a/react-electron-poc/src/renderer/components/ScrRefSelector.tsx +++ b/react-electron-poc/src/renderer/components/ScrRefSelector.tsx @@ -7,6 +7,9 @@ import { offsetBook, offsetChapter, offsetVerse, + FIRST_SCR_BOOK_NUM, + FIRST_SCR_CHAPTER_NUM, + FIRST_SCR_VERSE_NUM, } from '@util/ScriptureUtil'; import React, { useCallback, useEffect, useState } from 'react'; import './Components.css'; @@ -49,65 +52,85 @@ export default ({ scrRef, handleSubmit }: ScrRefSelectorProps) => { {getBookLongNameFromNum(scrRef.book)} - - - + + + + + {scrRef.chapter}: - - - + + + + + {scrRef.verse} - - - + + + + + (() => { if (scrChapters && scrChapters.length > 0) { - return scrChapters.flatMap((scrChapter) => { + const start = performance.now(); + const scrChapterChunks = scrChapters.flatMap((scrChapter) => { // TODO: When loading, the contents come as a string. Consider how to improve the loading value in ScriptureTextPanelHOC const scrChapterContents = isString(scrChapter.contents) ? [ @@ -1049,6 +1050,12 @@ export const ScriptureTextPanelSlate = ScriptureTextPanelHOC( chunkSize, ); }); + console.log( + `Performance: chunking scrChapters took ${ + performance.now() - start + } ms`, + ); + return scrChapterChunks; } return []; }, [scrChapters, useVirtualization]); diff --git a/react-electron-poc/src/renderer/services/ScriptureService.ts b/react-electron-poc/src/renderer/services/ScriptureService.ts index 8bb9aa6..8eb2409 100644 --- a/react-electron-poc/src/renderer/services/ScriptureService.ts +++ b/react-electron-poc/src/renderer/services/ScriptureService.ts @@ -36,10 +36,9 @@ export const getScripture = async ( ), }), ); - const end = performance.now(); console.log( `Performance: Parsing JSON for getScripture(${shortName}, ${bookNum}, ${chapter}) took ${ - end - start + performance.now() - start } ms`, ); return scrChapterContentsParsed; @@ -74,10 +73,16 @@ export const writeScripture = async ( contents: ScriptureChapterContent[], ): Promise => { try { + const start = performance.now(); const contentsJSON = contents.map((content) => ({ ...content, - contents: JSON.stringify(content.contents, null, 4), + contents: JSON.stringify(content.contents), })) as unknown as ScriptureChapterContent[]; + console.log( + `Performance: Stringifying ${shortName} ${bookNum}:${chapter} took ${ + performance.now() - start + } ms`, + ); if (chapter >= 0) await window.electronAPI.scripture.writeScriptureChapter( shortName, diff --git a/react-electron-poc/src/renderer/util/ScriptureUtil.ts b/react-electron-poc/src/renderer/util/ScriptureUtil.ts index 0be831f..ce47d85 100644 --- a/react-electron-poc/src/renderer/util/ScriptureUtil.ts +++ b/react-electron-poc/src/renderer/util/ScriptureUtil.ts @@ -77,8 +77,10 @@ const scrBookNames: string[][] = [ ['JUD', 'Jude'], ['REV', 'Revelation'], ]; -const firstScrBookNum = 1; -const lastScrBookNum = scrBookNames.length - 1; +export const FIRST_SCR_BOOK_NUM = 1; +export const LAST_SCR_BOOK_NUM = scrBookNames.length - 1; +export const FIRST_SCR_CHAPTER_NUM = 1; +export const FIRST_SCR_VERSE_NUM = 0; export const getBookNumFromName = (bookName: string): number => { return scrBookNames.findIndex((bookNames) => bookNames.includes(bookName)); @@ -87,20 +89,26 @@ export const getBookNumFromName = (bookName: string): number => { export const getAllBookNamesFromNum = (bookNum: number): string[] => { return [ ...scrBookNames[ - bookNum < firstScrBookNum || bookNum > lastScrBookNum ? 0 : bookNum + bookNum < FIRST_SCR_BOOK_NUM || bookNum > LAST_SCR_BOOK_NUM + ? 0 + : bookNum ], ]; }; export const getBookShortNameFromNum = (bookNum: number): string => { return scrBookNames[ - bookNum < firstScrBookNum || bookNum > lastScrBookNum ? 0 : bookNum + bookNum < FIRST_SCR_BOOK_NUM || bookNum > LAST_SCR_BOOK_NUM + ? 0 + : bookNum ][0]; }; export const getBookLongNameFromNum = (bookNum: number): string => { return scrBookNames[ - bookNum < firstScrBookNum || bookNum > lastScrBookNum ? 0 : bookNum + bookNum < FIRST_SCR_BOOK_NUM || bookNum > LAST_SCR_BOOK_NUM + ? 0 + : bookNum ][1]; }; @@ -109,8 +117,8 @@ export const offsetBook = ( offset: number, ): ScriptureReference => ({ book: Math.max( - firstScrBookNum, - Math.min(scrRef.book + offset, lastScrBookNum), + FIRST_SCR_BOOK_NUM, + Math.min(scrRef.book + offset, LAST_SCR_BOOK_NUM), ), chapter: 1, verse: 1, @@ -121,14 +129,17 @@ export const offsetChapter = ( offset: number, ): ScriptureReference => ({ ...scrRef, - chapter: scrRef.chapter + offset, + chapter: Math.max(FIRST_SCR_CHAPTER_NUM, scrRef.chapter + offset), verse: 1, }); export const offsetVerse = ( scrRef: ScriptureReference, offset: number, -): ScriptureReference => ({ ...scrRef, verse: scrRef.verse + offset }); +): ScriptureReference => ({ + ...scrRef, + verse: Math.max(FIRST_SCR_VERSE_NUM, scrRef.verse + offset), +}); /** Parse a verse number from a string */ export const parseVerse = (verseText: string): number | undefined => {