From 9e3886e9be4af59a20207b67f69ae921843067c6 Mon Sep 17 00:00:00 2001 From: tjcouch-sil Date: Fri, 16 Sep 2022 09:29:32 -0500 Subject: [PATCH 1/6] Implemented new API ScriptureChapter design, Started using ScriptureTextPanel --- react-electron-poc/src/main/main.ts | 25 +- react-electron-poc/src/main/preload.ts | 14 +- .../src/renderer/components/layout/Layout.tsx | 218 +++++++++--------- .../panels/TextPanels/ScriptureTextPanel.tsx | 54 +++-- .../panels/TextPanels/TextPanel.css | 2 +- react-electron-poc/src/renderer/preload.d.ts | 18 +- .../src/renderer/services/ScriptureService.ts | 12 +- .../src/shared/data/ScriptureTypes.ts | 16 ++ 8 files changed, 210 insertions(+), 149 deletions(-) diff --git a/react-electron-poc/src/main/main.ts b/react-electron-poc/src/main/main.ts index e8c7903..5896169 100644 --- a/react-electron-poc/src/main/main.ts +++ b/react-electron-poc/src/main/main.ts @@ -13,7 +13,12 @@ import fs from 'fs'; import { app, BrowserWindow, shell, IpcMainInvokeEvent } from 'electron'; import { autoUpdater } from 'electron-updater'; import log from 'electron-log'; -import { ResourceInfo, ScriptureContent } from '@shared/data/ScriptureTypes'; +import { + ResourceInfo, + ScriptureChapter, + ScriptureChapterString, + ScriptureContent, +} from '@shared/data/ScriptureTypes'; import MenuBuilder from './menu'; import { resolveHtmlPath } from './util'; import { ipcMain } from './electron-extensions'; @@ -186,10 +191,16 @@ async function handleGetScriptureBook( fileExtension: string, shortName: string, bookNum: number, -): Promise<(ScriptureContent | string)[]> { +): Promise { + // TODO: If we want to implement this, parse file and split out into actual chapters return getFilesText( [`testScripture/${shortName}/${bookNum}.${fileExtension}`], getScriptureDelay, + ).then((filesContents) => + filesContents.map((fileContents, ind) => ({ + chapter: ind, + contents: fileContents, + })), ); } @@ -212,11 +223,14 @@ async function handleGetScriptureChapter( shortName: string, bookNum: number, chapter: number, -): Promise { +): Promise { return getFileText( `testScripture/${shortName}/${bookNum}-${chapter}.${fileExtension}`, getScriptureDelay, - ); + ).then((fileContents) => ({ + chapter, + contents: fileContents, + })); } /** These test files are from breakpointing at UsfmSinglePaneControl.cs at the line that gets Css in LoadUsfm. */ @@ -239,8 +253,7 @@ async function handleGetResourceInfo( }, getResourceInfoDelay); } -async function handleGetAllResourceInfo( -): Promise { +async function handleGetAllResourceInfo(): Promise { return delayPromise((resolve, reject) => { const start = performance.now(); fs.readdir( diff --git a/react-electron-poc/src/main/preload.ts b/react-electron-poc/src/main/preload.ts index 5dd84f5..1d5a2e6 100644 --- a/react-electron-poc/src/main/preload.ts +++ b/react-electron-poc/src/main/preload.ts @@ -1,5 +1,5 @@ import { contextBridge, ipcRenderer, IpcRendererEvent } from 'electron'; -import { ResourceInfo, ScriptureContent } from '@shared/data/ScriptureTypes'; +import { ResourceInfo, ScriptureChapter, ScriptureChapterContent, ScriptureChapterString } from '@shared/data/ScriptureTypes'; /** * Whitelisted channel names through which the main and renderer processes can communicate. @@ -17,7 +17,7 @@ contextBridge.exposeInMainWorld('electronAPI', { getScriptureBook: ( shortName: string, bookNum: number, - ): Promise => + ): Promise => ipcRenderer.invoke( 'ipc-scripture:getScriptureBook', shortName, @@ -27,7 +27,7 @@ contextBridge.exposeInMainWorld('electronAPI', { shortName: string, bookNum: number, chapter: number, - ): Promise => + ): Promise => ipcRenderer.invoke( 'ipc-scripture:getScriptureChapter', shortName, @@ -37,7 +37,7 @@ contextBridge.exposeInMainWorld('electronAPI', { getScriptureBookRaw: ( shortName: string, bookNum: number, - ): Promise => + ): Promise => ipcRenderer.invoke( 'ipc-scripture:getScriptureBookRaw', shortName, @@ -47,7 +47,7 @@ contextBridge.exposeInMainWorld('electronAPI', { shortName: string, bookNum: number, chapter: number, - ): Promise => + ): Promise => ipcRenderer.invoke( 'ipc-scripture:getScriptureChapterRaw', shortName, @@ -57,7 +57,7 @@ contextBridge.exposeInMainWorld('electronAPI', { getScriptureBookHtml: ( shortName: string, bookNum: number, - ): Promise => + ): Promise => ipcRenderer.invoke( 'ipc-scripture:getScriptureBookHtml', shortName, @@ -67,7 +67,7 @@ contextBridge.exposeInMainWorld('electronAPI', { shortName: string, bookNum: number, chapter: number, - ): Promise => + ): Promise => ipcRenderer.invoke( 'ipc-scripture:getScriptureChapterHtml', shortName, diff --git a/react-electron-poc/src/renderer/components/layout/Layout.tsx b/react-electron-poc/src/renderer/components/layout/Layout.tsx index 231eaba..75aa4c3 100644 --- a/react-electron-poc/src/renderer/components/layout/Layout.tsx +++ b/react-electron-poc/src/renderer/components/layout/Layout.tsx @@ -2,46 +2,53 @@ import './Layout.css'; import { DockviewReact, DockviewReadyEvent } from 'dockview'; import '@node_modules/dockview/dist/styles/dockview.css'; import { DockViewPanels, PanelFactory } from '@components/panels/Panels'; -import { useCallback } from 'react'; -import { TextPanelProps } from '@components/panels/TextPanels/TextPanel'; +import { useCallback, useState } from 'react'; import { getAllResourceInfo, getResourceInfo, - getScriptureHtml, } from '@services/ScriptureService'; +import { ScriptureTextPanelProps } from '@components/panels/TextPanels/ScriptureTextPanel'; +import { ScriptureReference } from '@shared/data/ScriptureTypes'; const Layout = () => { - const onReady = useCallback((event: DockviewReadyEvent) => { - // Test resource info api - getAllResourceInfo() - .then((allResourceInfo) => - console.log( - `All Resource Info:\n${allResourceInfo - .map( - (resourceInfo) => - `\tResource: ${resourceInfo.shortName}${ - resourceInfo.editable ? ' editable' : '' - }`, - ) - .join('\n')}`, - ), - ) - .catch((r) => console.log(r)); - getResourceInfo('zzz6') - .then((resourceInfo) => - console.log( - `Resource: ${resourceInfo.shortName}${ - resourceInfo.editable ? ' editable' : '' - }`, - ), - ) - .catch((r) => console.log(r)); + const [scrRef, setScrRef] = useState({ + book: 19, + chapter: 119, + verse: 1, + }); - const panelFactory = new PanelFactory(event); - /* const erb = panelFactory.addPanel('Erb', undefined, { + const onReady = useCallback( + (event: DockviewReadyEvent) => { + // Test resource info api + getAllResourceInfo() + .then((allResourceInfo) => + console.log( + `All Resource Info:\n${allResourceInfo + .map( + (resourceInfo) => + `\tResource: ${resourceInfo.shortName}${ + resourceInfo.editable ? ' editable' : '' + }`, + ) + .join('\n')}`, + ), + ) + .catch((r) => console.log(r)); + getResourceInfo('zzz6') + .then((resourceInfo) => + console.log( + `Resource: ${resourceInfo.shortName}${ + resourceInfo.editable ? ' editable' : '' + }`, + ), + ) + .catch((r) => console.log(r)); + + const panelFactory = new PanelFactory(event); + /* const erb = panelFactory.addPanel('Erb', undefined, { title: 'ERB', }); */ - /* const textPanel = panelFactory.addPanel( + /* const textPanel = panelFactory.addPanel( 'TextPanel', { placeholderText: 'Loading zzz6 Psalm 119 USX', @@ -51,87 +58,82 @@ const Layout = () => { title: 'zzz6: Psalm 119 USX', }, ); */ - const htmlTextPanel = panelFactory.addPanel( - 'HtmlTextPanel', - { - placeholderText: 'Loading CSB Psalm 119 HTML', - textPromise: getScriptureHtml('CSB', 19, 119).then( - (result) => result[0], - ), - } as TextPanelProps, - { - title: 'CSB: Psalm 119 HTML', - }, - ); - const htmlTextPanelOHEB = panelFactory.addPanel( - 'HtmlTextPanel', - { - placeholderText: 'Loading OHEB Psalm 119 HTML', - textPromise: getScriptureHtml('OHEB', 19, 119).then( - (result) => result[0], - ), - } as TextPanelProps, - { - title: 'OHEB: Psalm 119 HTML', - position: { - direction: 'right', - referencePanel: htmlTextPanel.id, + const csbPanel = panelFactory.addPanel( + 'ScriptureTextPanel', + { + shortName: 'CSB', + editable: false, + ...scrRef, + } as ScriptureTextPanelProps, + { + title: 'CSB: Psalm 119 HTML', }, - }, - ); - const editableHtmlTextPanel = panelFactory.addPanel( - 'EditableHtmlTextPanel', - { - placeholderText: 'Loading zzz6 Psalm 119 Editable HTML', - textPromise: getScriptureHtml('zzz6', 19, 119).then( - (result) => result[0], - ), - } as TextPanelProps, - { - title: 'zzz6: Psalm 119 Editable HTML', - position: { - direction: 'below', - referencePanel: htmlTextPanel.id, + ); + const ohebPanel = panelFactory.addPanel( + 'ScriptureTextPanel', + { + shortName: 'OHEB', + editable: false, + ...scrRef, + } as ScriptureTextPanelProps, + { + title: 'OHEB: Psalm 119 HTML', + position: { + direction: 'right', + referencePanel: csbPanel.id, + }, }, - }, - ); - panelFactory.addPanel( - 'HtmlTextPanel', - { - placeholderText: 'Loading NIV84 Psalm 119 HTML', - textPromise: getScriptureHtml('NIV84', 19, 119).then( - (result) => result[0], - ), - } as TextPanelProps, - { - title: 'NIV84: Psalm 119 HTML', - position: { - direction: 'below', - referencePanel: htmlTextPanelOHEB.id, + ); + panelFactory.addPanel( + 'ScriptureTextPanel', + { + shortName: 'zzz6', + editable: true, + ...scrRef, + } as ScriptureTextPanelProps, + { + title: 'zzz6: Psalm 119 Editable HTML', + position: { + direction: 'below', + referencePanel: csbPanel.id, + }, }, - }, - ); - panelFactory.addPanel( - 'EditableHtmlTextPanel', - { - placeholderText: 'Loading zzz1 Psalm 119 Editable HTML', - textPromise: getScriptureHtml('zzz1', 19, 119).then( - (result) => result[0], - ), - } as TextPanelProps, - { - title: 'zzz1: Psalm 119 Editable HTML', - position: { - direction: 'below', - referencePanel: htmlTextPanelOHEB.id, + ); + panelFactory.addPanel( + 'ScriptureTextPanel', + { + shortName: 'NIV84', + editable: false, + ...scrRef, + } as ScriptureTextPanelProps, + { + title: 'NIV84: Psalm 119 HTML', + position: { + direction: 'below', + referencePanel: ohebPanel.id, + }, }, - }, - ); + ); + panelFactory.addPanel( + 'ScriptureTextPanel', + { + shortName: 'zzz1', + editable: true, + ...scrRef, + } as ScriptureTextPanelProps, + { + title: 'zzz1: Psalm 119 Editable HTML', + position: { + direction: 'below', + referencePanel: ohebPanel.id, + }, + }, + ); - event.api.getPanel(htmlTextPanel.id)?.focus(); + event.api.getPanel(csbPanel.id)?.focus(); - // TODO: Figure out how to resize panels or do anything with them really - /* setTimeout( + // TODO: Figure out how to resize panels or do anything with them really + /* setTimeout( () => textPanel.api.setSize({ width: textPanel.api.width, @@ -139,7 +141,9 @@ const Layout = () => { }), 1000, ); */ - }, []); + }, + [scrRef], + ); return (
diff --git a/react-electron-poc/src/renderer/components/panels/TextPanels/ScriptureTextPanel.tsx b/react-electron-poc/src/renderer/components/panels/TextPanels/ScriptureTextPanel.tsx index 66bea9b..4179623 100644 --- a/react-electron-poc/src/renderer/components/panels/TextPanels/ScriptureTextPanel.tsx +++ b/react-electron-poc/src/renderer/components/panels/TextPanels/ScriptureTextPanel.tsx @@ -2,29 +2,40 @@ import { getScriptureStyle, getScriptureHtml, } from '@services/ScriptureService'; +import { + ResourceInfo, + ScriptureChapter, + ScriptureReference, +} from '@shared/data/ScriptureTypes'; import { useEffect, useState } from 'react'; import useStyle from 'renderer/hooks/useStyle'; import './TextPanel.css'; -export interface ScriptureTextPanelProps { - shortName: string; - book: number; - chapter: number; - verse: number; -} +export interface ScriptureTextPanelProps + extends ScriptureReference, + ResourceInfo {} export const ScriptureTextPanel = ({ shortName, + editable, book, chapter, verse, }: ScriptureTextPanelProps) => { - const [scrHtml, setScrHtml] = useState(undefined); + const [scrChapters, setScrChapters] = useState< + ScriptureChapter[] | undefined + >(undefined); const [scrStyle, setScrStyle] = useState(''); useEffect(() => { - // eslint-disable-next-line promise/catch-or-return - getScriptureStyle(shortName).then((s) => setScrStyle(s)); + // TODO: Fix RTL scripture style sheets + getScriptureStyle(shortName) + .then((s) => { + if (shortName !== 'OHEB' && shortName !== 'zzz1') + setScrStyle(s); + return undefined; + }) + .catch((r) => console.log(r)); }, [shortName]); useStyle(scrStyle); @@ -39,23 +50,32 @@ export const ScriptureTextPanel = ({ book, chapter, ); - if (scriptureRefIsCurrent) setScrHtml(scriptureHtml); + if (scriptureRefIsCurrent) setScrChapters(scriptureHtml); })(); } return () => { scriptureRefIsCurrent = false; - setScrHtml(undefined); + setScrChapters(undefined); }; }, [shortName, book, chapter]); - const display = scrHtml || `Loading${shortName ? ` ${shortName}` : ''}...`; + const display = scrChapters || [ + { chapter: -1, contents: `Loading ${shortName}...` }, + ]; return ( -
+
+ {display.map((scrChapter) => ( +
+ ))} +
); }; diff --git a/react-electron-poc/src/renderer/components/panels/TextPanels/TextPanel.css b/react-electron-poc/src/renderer/components/panels/TextPanels/TextPanel.css index ab48e92..50c7295 100644 --- a/react-electron-poc/src/renderer/components/panels/TextPanels/TextPanel.css +++ b/react-electron-poc/src/renderer/components/panels/TextPanels/TextPanel.css @@ -2,5 +2,5 @@ width: 100%; height: 100%; background-color: white; - overflow-x: scroll; + overflow-y: scroll; } diff --git a/react-electron-poc/src/renderer/preload.d.ts b/react-electron-poc/src/renderer/preload.d.ts index bf88edc..44539c4 100644 --- a/react-electron-poc/src/renderer/preload.d.ts +++ b/react-electron-poc/src/renderer/preload.d.ts @@ -1,5 +1,9 @@ import { IpcChannel } from 'main/preload'; -import { ResourceInfo, ScriptureContent } from '@shared/data/ScriptureTypes'; +import { + ResourceInfo, + ScriptureChapterContent, + ScriptureChapterString, +} from '@shared/data/ScriptureTypes'; declare global { interface Window { @@ -8,30 +12,30 @@ declare global { getScriptureBook( shortName: string, bookNum: number, - ): Promise; + ): Promise; getScriptureChapter( shortName: string, bookNum: number, chapter: number, - ): Promise; + ): Promise; getScriptureBookRaw( shortName: string, bookNum: number, - ): Promise; + ): Promise; getScriptureChapterRaw( shortName: string, bookNum: number, chapter: number, - ): Promise; + ): Promise; getScriptureBookHtml( shortName: string, bookNum: number, - ): Promise; + ): Promise; getScriptureChapterHtml( shortName: string, bookNum: number, chapter: number, - ): Promise; + ): Promise; getScriptureStyle(shortName: string): Promise; getResourceInfo(shortName: string): Promise; getAllResourceInfo(): Promise; diff --git a/react-electron-poc/src/renderer/services/ScriptureService.ts b/react-electron-poc/src/renderer/services/ScriptureService.ts index c17628a..e1f871a 100644 --- a/react-electron-poc/src/renderer/services/ScriptureService.ts +++ b/react-electron-poc/src/renderer/services/ScriptureService.ts @@ -1,4 +1,8 @@ -import { ResourceInfo, ScriptureContent } from '@shared/data/ScriptureTypes'; +import { + ResourceInfo, + ScriptureChapterContent, + ScriptureChapterString, +} from '@shared/data/ScriptureTypes'; /** * Gets the specified Scripture chapter in the specified book from the specified project in Slate JSON @@ -11,7 +15,7 @@ export const getScripture = async ( shortName: string, bookNum: number, chapter = -1, -): Promise => { +): Promise => { return chapter >= 0 ? window.electronAPI.scripture .getScriptureChapter(shortName, bookNum, chapter) @@ -30,7 +34,7 @@ export const getScriptureRaw = async ( shortName: string, bookNum: number, chapter = -1, -): Promise => { +): Promise => { return chapter >= 0 ? window.electronAPI.scripture .getScriptureChapterRaw(shortName, bookNum, chapter) @@ -49,7 +53,7 @@ export const getScriptureHtml = async ( shortName: string, bookNum: number, chapter = -1, -): Promise => { +): Promise => { return chapter >= 0 ? window.electronAPI.scripture .getScriptureChapterHtml(shortName, bookNum, chapter) diff --git a/react-electron-poc/src/shared/data/ScriptureTypes.ts b/react-electron-poc/src/shared/data/ScriptureTypes.ts index 4d5504f..8b49e6b 100644 --- a/react-electron-poc/src/shared/data/ScriptureTypes.ts +++ b/react-electron-poc/src/shared/data/ScriptureTypes.ts @@ -13,3 +13,19 @@ export interface ResourceInfo { export interface ScriptureContent { text: string; } + +/** Scipture chapter contents along with which chapter it is */ +export interface ScriptureChapter { + chapter: number; + contents: unknown; +} + +/** Scipture chapter contents Slate object along with which chapter it is */ +export interface ScriptureChapterContent extends ScriptureChapter { + contents: ScriptureContent; +} + +/** Scripture chapter string (usx, usfm, html, etc) along with which chapter it is */ +export interface ScriptureChapterString extends ScriptureChapter { + contents: string; +} From 4f34c32f5e03ef240905957ffca751f59736e8db Mon Sep 17 00:00:00 2001 From: tjcouch-sil Date: Fri, 16 Sep 2022 15:44:02 -0500 Subject: [PATCH 2/6] Added usePromise hook, reworked useStyle hook --- react-electron-poc/src/main/main.ts | 8 +-- react-electron-poc/src/main/preload.ts | 6 +- .../panels/TextPanels/ScriptureTextPanel.tsx | 66 +++++++------------ .../src/renderer/hooks/usePromise.ts | 38 +++++++++++ .../src/renderer/hooks/useStyle.ts | 51 ++++++++++++++ .../src/renderer/hooks/useStyle.tsx | 28 -------- 6 files changed, 120 insertions(+), 77 deletions(-) create mode 100644 react-electron-poc/src/renderer/hooks/usePromise.ts create mode 100644 react-electron-poc/src/renderer/hooks/useStyle.ts delete mode 100644 react-electron-poc/src/renderer/hooks/useStyle.tsx diff --git a/react-electron-poc/src/main/main.ts b/react-electron-poc/src/main/main.ts index 5896169..39bc426 100644 --- a/react-electron-poc/src/main/main.ts +++ b/react-electron-poc/src/main/main.ts @@ -13,12 +13,7 @@ import fs from 'fs'; import { app, BrowserWindow, shell, IpcMainInvokeEvent } from 'electron'; import { autoUpdater } from 'electron-updater'; import log from 'electron-log'; -import { - ResourceInfo, - ScriptureChapter, - ScriptureChapterString, - ScriptureContent, -} from '@shared/data/ScriptureTypes'; +import { ResourceInfo, ScriptureChapter } from '@shared/data/ScriptureTypes'; import MenuBuilder from './menu'; import { resolveHtmlPath } from './util'; import { ipcMain } from './electron-extensions'; @@ -43,6 +38,7 @@ const isDebug = if (isDebug) { require('electron-debug')(); + console.log('Debug start!'); } const installExtensions = async () => { diff --git a/react-electron-poc/src/main/preload.ts b/react-electron-poc/src/main/preload.ts index 1d5a2e6..a4f541f 100644 --- a/react-electron-poc/src/main/preload.ts +++ b/react-electron-poc/src/main/preload.ts @@ -1,5 +1,9 @@ import { contextBridge, ipcRenderer, IpcRendererEvent } from 'electron'; -import { ResourceInfo, ScriptureChapter, ScriptureChapterContent, ScriptureChapterString } from '@shared/data/ScriptureTypes'; +import { + ResourceInfo, + ScriptureChapterContent, + ScriptureChapterString, +} from '@shared/data/ScriptureTypes'; /** * Whitelisted channel names through which the main and renderer processes can communicate. diff --git a/react-electron-poc/src/renderer/components/panels/TextPanels/ScriptureTextPanel.tsx b/react-electron-poc/src/renderer/components/panels/TextPanels/ScriptureTextPanel.tsx index 4179623..e99ff5f 100644 --- a/react-electron-poc/src/renderer/components/panels/TextPanels/ScriptureTextPanel.tsx +++ b/react-electron-poc/src/renderer/components/panels/TextPanels/ScriptureTextPanel.tsx @@ -7,7 +7,9 @@ import { ScriptureChapter, ScriptureReference, } from '@shared/data/ScriptureTypes'; -import { useEffect, useState } from 'react'; +import { isValidValue } from '@util/Util'; +import { useCallback, useEffect, useRef, useState } from 'react'; +import usePromise from 'renderer/hooks/usePromise'; import useStyle from 'renderer/hooks/useStyle'; import './TextPanel.css'; @@ -22,51 +24,31 @@ export const ScriptureTextPanel = ({ chapter, verse, }: ScriptureTextPanelProps) => { - const [scrChapters, setScrChapters] = useState< - ScriptureChapter[] | undefined - >(undefined); - - const [scrStyle, setScrStyle] = useState(''); - useEffect(() => { - // TODO: Fix RTL scripture style sheets - getScriptureStyle(shortName) - .then((s) => { - if (shortName !== 'OHEB' && shortName !== 'zzz1') - setScrStyle(s); - return undefined; - }) - .catch((r) => console.log(r)); - }, [shortName]); - - useStyle(scrStyle); - - useEffect(() => { - let scriptureRefIsCurrent = false; - if (shortName && book) { - scriptureRefIsCurrent = true; - (async () => { - const scriptureHtml = await getScriptureHtml( - shortName, - book, - chapter, - ); - if (scriptureRefIsCurrent) setScrChapters(scriptureHtml); - })(); - } - - return () => { - scriptureRefIsCurrent = false; - setScrChapters(undefined); - }; - }, [shortName, book, chapter]); + useStyle( + useCallback(async () => { + // TODO: Fix RTL scripture style sheets + if (!shortName) return undefined; + const style = await getScriptureStyle(shortName); + return shortName !== 'OHEB' && shortName !== 'zzz1' + ? style + : undefined; + }, [shortName]), + ); - const display = scrChapters || [ - { chapter: -1, contents: `Loading ${shortName}...` }, - ]; + const [scrChapters] = usePromise( + useCallback(async () => { + if (!shortName || !isValidValue(book) || !isValidValue(chapter)) + return null; + return getScriptureHtml(shortName, book, chapter); + }, [shortName, book, chapter]), + useState([ + { chapter: -1, contents: `Loading ${shortName}...` }, + ])[0], + ); return (
- {display.map((scrChapter) => ( + {scrChapters.map((scrChapter) => (
( + promiseFactoryCallback: () => Promise, + defaultValue: T, + preserveValue = true, +): [value: T, isLoading: boolean] => { + const [value, setValue] = useState(defaultValue); + const [loading, setLoading] = useState(true); + useEffect(() => { + let promiseIsCurrent = true; + setLoading(true); + (async () => { + const result = await promiseFactoryCallback(); + if (promiseIsCurrent) { + // If the promise returned null, it purposely did this to do nothing. Maybe its dependencies are not set up + if (result != null) setValue(result); + setLoading(false); + } + })(); + + return () => { + // Mark this promise as old and not to be used + promiseIsCurrent = false; + if (!preserveValue) setValue(defaultValue); + }; + }, [promiseFactoryCallback, defaultValue, preserveValue]); + + return [value, loading]; +}; diff --git a/react-electron-poc/src/renderer/hooks/useStyle.ts b/react-electron-poc/src/renderer/hooks/useStyle.ts new file mode 100644 index 0000000..0f0df98 --- /dev/null +++ b/react-electron-poc/src/renderer/hooks/useStyle.ts @@ -0,0 +1,51 @@ +import { useEffect, useRef } from 'react'; + +/** Awaits a promise to get styles and applies them when resolved. + * @param promiseFactoryCallback a function that returns the promise to await. If the promise resolves to null, the value will not change. + * WARNING: MUST BE WRAPPED IN A useCallback. The reference must not be updated every render + */ +export default (promiseFactoryCallback: () => Promise) => { + const stylePrev = useRef(undefined); + useEffect(() => { + let promiseIsCurrent = true; + let styleElement: HTMLStyleElement; + (async () => { + const style = await promiseFactoryCallback(); + if (promiseIsCurrent) { + // If the styles haven't changed, we don't need to do anything + if (stylePrev.current === style) return; + stylePrev.current = style; + + // If there are no styles, we don't need to do anything + if (!style) return; + + const start = performance.now(); + + // Add styles to document + styleElement = document.createElement('style'); + styleElement.appendChild(document.createTextNode(style)); + document.head.appendChild(styleElement); + + console.log( + `Style apply time: ${performance.now() - start} ms`, + ); + } + })(); + + return () => { + // If the style was added, remove it + if (styleElement) { + const start = performance.now(); + + // Remove styles + document.head.removeChild(styleElement); + + console.log( + `Style remove time: ${performance.now() - start} ms`, + ); + } + // Mark this promise as old and not to be used + promiseIsCurrent = false; + }; + }, [promiseFactoryCallback]); +}; diff --git a/react-electron-poc/src/renderer/hooks/useStyle.tsx b/react-electron-poc/src/renderer/hooks/useStyle.tsx deleted file mode 100644 index b9c1623..0000000 --- a/react-electron-poc/src/renderer/hooks/useStyle.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import { useEffect } from 'react'; - -export default (styles: string) => { - useEffect(() => { - // TODO: Could potentially improve by checking style vs previous style, though useEffect may already be doing this. Worth considering - // If there are no styles, we don't need to do anything - if (!styles) return; - - let start = performance.now(); - - // Add styles to document - const styleElement = document.createElement('style'); - styleElement.appendChild(document.createTextNode(styles)); - document.head.appendChild(styleElement); - - console.log(`Style apply time: ${performance.now() - start} ms`); - - // eslint-disable-next-line consistent-return - return () => { - start = performance.now(); - - // Remove styles - document.head.removeChild(styleElement); - - console.log(`Style remove time: ${performance.now() - start} ms`); - }; - }, [styles]); -}; From 4986292dcb99465018ef3970bfc6721b9b31c2a0 Mon Sep 17 00:00:00 2001 From: tjcouch-sil Date: Mon, 19 Sep 2022 09:47:04 -0500 Subject: [PATCH 3/6] Added Scripture Reference Selector --- .../src/renderer/components/Components.css | 14 ++++ .../renderer/components/ScrRefSelector.tsx | 58 ++++++++++++++ .../src/renderer/components/layout/Layout.tsx | 77 +++++++++++-------- .../src/renderer/hooks/useStyle.ts | 1 + 4 files changed, 117 insertions(+), 33 deletions(-) create mode 100644 react-electron-poc/src/renderer/components/Components.css create mode 100644 react-electron-poc/src/renderer/components/ScrRefSelector.tsx diff --git a/react-electron-poc/src/renderer/components/Components.css b/react-electron-poc/src/renderer/components/Components.css new file mode 100644 index 0000000..7b8c242 --- /dev/null +++ b/react-electron-poc/src/renderer/components/Components.css @@ -0,0 +1,14 @@ +.scrref-form { + padding: 5px; + background-color: #1c1c2a; +} + +.scrref-input { + width: 120px; +} + +.scrref-button { + padding: 5px 10px; + margin: 2px 5px; + font-size: 11pt; +} diff --git a/react-electron-poc/src/renderer/components/ScrRefSelector.tsx b/react-electron-poc/src/renderer/components/ScrRefSelector.tsx new file mode 100644 index 0000000..544b75c --- /dev/null +++ b/react-electron-poc/src/renderer/components/ScrRefSelector.tsx @@ -0,0 +1,58 @@ +import { ScriptureReference } from '@shared/data/ScriptureTypes'; +import React, { useCallback, useEffect, useState } from 'react'; +import './Components.css'; + +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface ScrRefSelectorProps { + scrRef: ScriptureReference; + handleSubmit: (scrRef: ScriptureReference) => void; +} + +// TODO: Move to util +const regexpScrRef = /([^ ]+) ([^:]+):(.+)/; +const getRefFromText = (refText: string): ScriptureReference => { + if (!refText) return { book: -1, chapter: -1, verse: -1 }; + const scrRefMatch = refText.match(regexpScrRef); + if (!scrRefMatch || scrRefMatch.length < 4) + return { book: -1, chapter: -1, verse: -1 }; + return { + book: parseInt(scrRefMatch[1], 10), + chapter: parseInt(scrRefMatch[2], 10), + verse: parseInt(scrRefMatch[3], 10), + }; +}; + +const getTextFromRef = (scrRef: ScriptureReference): string => + `${scrRef.book} ${scrRef.chapter}:${scrRef.verse}`; + +export default ({ scrRef, handleSubmit }: ScrRefSelectorProps) => { + const [currentRefText, setCurrentRefText] = useState( + getTextFromRef(scrRef), + ); + + const handleChange = useCallback( + (e: React.ChangeEvent) => { + setCurrentRefText(e.currentTarget.value); + }, + [], + ); + + useEffect(() => { + setCurrentRefText(getTextFromRef(scrRef)); + }, [scrRef]); + + return ( +
{ + e.preventDefault(); + handleSubmit(getRefFromText(currentRefText)); + }} + > + + +
+ ); +}; diff --git a/react-electron-poc/src/renderer/components/layout/Layout.tsx b/react-electron-poc/src/renderer/components/layout/Layout.tsx index 75aa4c3..24c2d28 100644 --- a/react-electron-poc/src/renderer/components/layout/Layout.tsx +++ b/react-electron-poc/src/renderer/components/layout/Layout.tsx @@ -1,22 +1,40 @@ import './Layout.css'; -import { DockviewReact, DockviewReadyEvent } from 'dockview'; +import { DockviewApi, DockviewReact, DockviewReadyEvent } from 'dockview'; import '@node_modules/dockview/dist/styles/dockview.css'; import { DockViewPanels, PanelFactory } from '@components/panels/Panels'; -import { useCallback, useState } from 'react'; +import { useCallback, useRef, useState } from 'react'; import { getAllResourceInfo, getResourceInfo, } from '@services/ScriptureService'; import { ScriptureTextPanelProps } from '@components/panels/TextPanels/ScriptureTextPanel'; import { ScriptureReference } from '@shared/data/ScriptureTypes'; +import ScrRefSelector from '@components/ScrRefSelector'; const Layout = () => { + const dockviewApi = useRef(undefined); + const [scrRef, setScrRef] = useState({ book: 19, chapter: 119, verse: 1, }); + const updateScrRef = useCallback((newScrRef: ScriptureReference) => { + setScrRef(newScrRef); + + dockviewApi.current?.panels.forEach((panel) => + panel.update({ + params: { + params: { + ...panel.params, + ...newScrRef, + }, + }, + }), + ); + }, []); + const onReady = useCallback( (event: DockviewReadyEvent) => { // Test resource info api @@ -44,20 +62,22 @@ const Layout = () => { ) .catch((r) => console.log(r)); + dockviewApi.current = event.api; + const panelFactory = new PanelFactory(event); /* const erb = panelFactory.addPanel('Erb', undefined, { - title: 'ERB', - }); */ + title: 'ERB', + }); */ /* const textPanel = panelFactory.addPanel( - 'TextPanel', - { - placeholderText: 'Loading zzz6 Psalm 119 USX', - textPromise: getScripture('zzz6', 19, 119), - } as TextPanelProps, - { - title: 'zzz6: Psalm 119 USX', - }, - ); */ + 'TextPanel', + { + placeholderText: 'Loading zzz6 Psalm 119 USX', + textPromise: getScripture('zzz6', 19, 119), + } as TextPanelProps, + { + title: 'zzz6: Psalm 119 USX', + }, + ); */ const csbPanel = panelFactory.addPanel( 'ScriptureTextPanel', { @@ -114,7 +134,7 @@ const Layout = () => { }, }, ); - panelFactory.addPanel( + const zzz1Panel = panelFactory.addPanel( 'ScriptureTextPanel', { shortName: 'zzz1', @@ -129,30 +149,21 @@ const Layout = () => { }, }, ); - - event.api.getPanel(csbPanel.id)?.focus(); - - // TODO: Figure out how to resize panels or do anything with them really - /* setTimeout( - () => - textPanel.api.setSize({ - width: textPanel.api.width, - height: 100, - }), - 1000, - ); */ }, [scrRef], ); return ( -
- -
+ <> + +
+ +
+ ); }; export default Layout; diff --git a/react-electron-poc/src/renderer/hooks/useStyle.ts b/react-electron-poc/src/renderer/hooks/useStyle.ts index 0f0df98..d9e403f 100644 --- a/react-electron-poc/src/renderer/hooks/useStyle.ts +++ b/react-electron-poc/src/renderer/hooks/useStyle.ts @@ -5,6 +5,7 @@ import { useEffect, useRef } from 'react'; * WARNING: MUST BE WRAPPED IN A useCallback. The reference must not be updated every render */ export default (promiseFactoryCallback: () => Promise) => { + // TODO: Consider doing the useCallback in here and enabling react-hooks/exhaustive-deps on this hook https://github.com/facebook/react/tree/main/packages/eslint-plugin-react-hooks#advanced-configuration const stylePrev = useRef(undefined); useEffect(() => { let promiseIsCurrent = true; From 90ff9d1fb9a77571cf134749aefec8da9a9d9586 Mon Sep 17 00:00:00 2001 From: tjcouch-sil Date: Mon, 19 Sep 2022 11:38:49 -0500 Subject: [PATCH 4/6] Changed PanelFactory to PanelManager, manages panel titles and such, updated ScrRefSelector to use book names --- .../renderer/components/ScrRefSelector.tsx | 31 ++--- .../src/renderer/components/layout/Layout.tsx | 39 ++---- .../components/panels/PanelManager.ts | 105 ++++++++++++++++ .../src/renderer/components/panels/Panels.ts | 43 +------ .../src/renderer/util/ScriptureUtil.ts | 115 ++++++++++++++++++ 5 files changed, 242 insertions(+), 91 deletions(-) create mode 100644 react-electron-poc/src/renderer/components/panels/PanelManager.ts create mode 100644 react-electron-poc/src/renderer/util/ScriptureUtil.ts diff --git a/react-electron-poc/src/renderer/components/ScrRefSelector.tsx b/react-electron-poc/src/renderer/components/ScrRefSelector.tsx index 544b75c..dc16724 100644 --- a/react-electron-poc/src/renderer/components/ScrRefSelector.tsx +++ b/react-electron-poc/src/renderer/components/ScrRefSelector.tsx @@ -1,4 +1,5 @@ import { ScriptureReference } from '@shared/data/ScriptureTypes'; +import { getTextFromScrRef, getScrRefFromText } from '@util/ScriptureUtil'; import React, { useCallback, useEffect, useState } from 'react'; import './Components.css'; @@ -8,26 +9,9 @@ export interface ScrRefSelectorProps { handleSubmit: (scrRef: ScriptureReference) => void; } -// TODO: Move to util -const regexpScrRef = /([^ ]+) ([^:]+):(.+)/; -const getRefFromText = (refText: string): ScriptureReference => { - if (!refText) return { book: -1, chapter: -1, verse: -1 }; - const scrRefMatch = refText.match(regexpScrRef); - if (!scrRefMatch || scrRefMatch.length < 4) - return { book: -1, chapter: -1, verse: -1 }; - return { - book: parseInt(scrRefMatch[1], 10), - chapter: parseInt(scrRefMatch[2], 10), - verse: parseInt(scrRefMatch[3], 10), - }; -}; - -const getTextFromRef = (scrRef: ScriptureReference): string => - `${scrRef.book} ${scrRef.chapter}:${scrRef.verse}`; - export default ({ scrRef, handleSubmit }: ScrRefSelectorProps) => { const [currentRefText, setCurrentRefText] = useState( - getTextFromRef(scrRef), + getTextFromScrRef(scrRef), ); const handleChange = useCallback( @@ -38,7 +22,7 @@ export default ({ scrRef, handleSubmit }: ScrRefSelectorProps) => { ); useEffect(() => { - setCurrentRefText(getTextFromRef(scrRef)); + setCurrentRefText(getTextFromScrRef(scrRef)); }, [scrRef]); return ( @@ -46,10 +30,15 @@ export default ({ scrRef, handleSubmit }: ScrRefSelectorProps) => { className="scrref-form" onSubmit={(e: React.FormEvent) => { e.preventDefault(); - handleSubmit(getRefFromText(currentRefText)); + handleSubmit(getScrRefFromText(currentRefText)); }} > - + diff --git a/react-electron-poc/src/renderer/components/layout/Layout.tsx b/react-electron-poc/src/renderer/components/layout/Layout.tsx index 24c2d28..3d49ff4 100644 --- a/react-electron-poc/src/renderer/components/layout/Layout.tsx +++ b/react-electron-poc/src/renderer/components/layout/Layout.tsx @@ -1,7 +1,7 @@ import './Layout.css'; -import { DockviewApi, DockviewReact, DockviewReadyEvent } from 'dockview'; +import { DockviewReact, DockviewReadyEvent } from 'dockview'; import '@node_modules/dockview/dist/styles/dockview.css'; -import { DockViewPanels, PanelFactory } from '@components/panels/Panels'; +import { DockViewPanels } from '@components/panels/Panels'; import { useCallback, useRef, useState } from 'react'; import { getAllResourceInfo, @@ -10,9 +10,10 @@ import { import { ScriptureTextPanelProps } from '@components/panels/TextPanels/ScriptureTextPanel'; import { ScriptureReference } from '@shared/data/ScriptureTypes'; import ScrRefSelector from '@components/ScrRefSelector'; +import { PanelManager } from '@components/panels/PanelManager'; const Layout = () => { - const dockviewApi = useRef(undefined); + const panelManager = useRef(undefined); const [scrRef, setScrRef] = useState({ book: 19, @@ -23,16 +24,7 @@ const Layout = () => { const updateScrRef = useCallback((newScrRef: ScriptureReference) => { setScrRef(newScrRef); - dockviewApi.current?.panels.forEach((panel) => - panel.update({ - params: { - params: { - ...panel.params, - ...newScrRef, - }, - }, - }), - ); + panelManager.current?.updateScrRef(newScrRef); }, []); const onReady = useCallback( @@ -62,9 +54,7 @@ const Layout = () => { ) .catch((r) => console.log(r)); - dockviewApi.current = event.api; - - const panelFactory = new PanelFactory(event); + panelManager.current = new PanelManager(event); /* const erb = panelFactory.addPanel('Erb', undefined, { title: 'ERB', }); */ @@ -78,18 +68,15 @@ const Layout = () => { title: 'zzz6: Psalm 119 USX', }, ); */ - const csbPanel = panelFactory.addPanel( + const csbPanel = panelManager.current.addPanel( 'ScriptureTextPanel', { shortName: 'CSB', editable: false, ...scrRef, } as ScriptureTextPanelProps, - { - title: 'CSB: Psalm 119 HTML', - }, ); - const ohebPanel = panelFactory.addPanel( + const ohebPanel = panelManager.current.addPanel( 'ScriptureTextPanel', { shortName: 'OHEB', @@ -97,14 +84,13 @@ const Layout = () => { ...scrRef, } as ScriptureTextPanelProps, { - title: 'OHEB: Psalm 119 HTML', position: { direction: 'right', referencePanel: csbPanel.id, }, }, ); - panelFactory.addPanel( + panelManager.current.addPanel( 'ScriptureTextPanel', { shortName: 'zzz6', @@ -112,14 +98,13 @@ const Layout = () => { ...scrRef, } as ScriptureTextPanelProps, { - title: 'zzz6: Psalm 119 Editable HTML', position: { direction: 'below', referencePanel: csbPanel.id, }, }, ); - panelFactory.addPanel( + panelManager.current.addPanel( 'ScriptureTextPanel', { shortName: 'NIV84', @@ -127,14 +112,13 @@ const Layout = () => { ...scrRef, } as ScriptureTextPanelProps, { - title: 'NIV84: Psalm 119 HTML', position: { direction: 'below', referencePanel: ohebPanel.id, }, }, ); - const zzz1Panel = panelFactory.addPanel( + panelManager.current.addPanel( 'ScriptureTextPanel', { shortName: 'zzz1', @@ -142,7 +126,6 @@ const Layout = () => { ...scrRef, } as ScriptureTextPanelProps, { - title: 'zzz1: Psalm 119 Editable HTML', position: { direction: 'below', referencePanel: ohebPanel.id, diff --git a/react-electron-poc/src/renderer/components/panels/PanelManager.ts b/react-electron-poc/src/renderer/components/panels/PanelManager.ts new file mode 100644 index 0000000..4485221 --- /dev/null +++ b/react-electron-poc/src/renderer/components/panels/PanelManager.ts @@ -0,0 +1,105 @@ +import { ScriptureReference } from '@shared/data/ScriptureTypes'; +import { getTextFromScrRef } from '@util/ScriptureUtil'; +import { newGuid } from '@util/Util'; +import { DockviewReadyEvent, AddPanelOptions } from 'dockview'; +import { PanelType } from './Panels'; +import { ScriptureTextPanelProps } from './TextPanels/ScriptureTextPanel'; + +export interface PanelInfo { + id: string; + type: PanelType; + /** The title for the panel. Set if provided, undefined if generated */ + title?: string; +} + +/** Dockview Panel builder for our panels */ +// eslint-disable-next-line import/prefer-default-export +export class PanelManager { + /** Map of panel id to information about that panel (particularly useful for generating title) */ + panelsInfo: Map; + + constructor(readonly dockview: DockviewReadyEvent) { + this.panelsInfo = new Map(); + } + + static generatePanelTitle( + panelInfo: PanelInfo | undefined, + panelProps: object = {}, + ): string { + if (!panelInfo) + throw new Error('generatePanelTitle: panelInfo undefined!'); + if (panelInfo.title || panelInfo.title === '') return panelInfo.title; + + if (panelInfo.type === 'ScriptureTextPanel') { + const scrPanelProps = panelProps as ScriptureTextPanelProps; + return `${scrPanelProps.shortName}: ${getTextFromScrRef({ + book: scrPanelProps.book, + chapter: scrPanelProps.chapter, + verse: -1, + })}${scrPanelProps.editable ? ' Editable' : ''}`; + } + return panelInfo.type; + } + + /** Returns AddPanelOptions for the specified input */ + static buildPanel( + panelType: PanelType, + panelProps: object = {}, + addPanelOptions: Omit< + AddPanelOptions, + 'id' | 'component' | 'params' + > = {}, + ): [PanelInfo, AddPanelOptions] { + const panelInfo = { + id: newGuid(), + type: panelType, + title: addPanelOptions.title, + }; + return [ + panelInfo, + { + ...addPanelOptions, + id: panelInfo.id, + component: panelType, + params: { ...panelProps }, + title: PanelManager.generatePanelTitle(panelInfo, panelProps), + }, + ]; + } + + /** Creates a new panel and adds it to the view */ + addPanel( + panelType: PanelType, + panelProps: object = {}, + addPanelOptions: Omit< + AddPanelOptions, + 'id' | 'component' | 'params' + > = {}, + ) { + const [panelInfo, panelOptions] = PanelManager.buildPanel( + panelType, + panelProps, + addPanelOptions, + ); + this.panelsInfo.set(panelInfo.id, panelInfo); + return this.dockview.api.addPanel(panelOptions); + } + + updateScrRef(newScrRef: ScriptureReference): void { + this.dockview.api.panels.forEach((panel) => { + const panelPropsUpdated = { + ...panel.params, + ...newScrRef, + }; + panel.update({ + params: { + params: panelPropsUpdated, + title: PanelManager.generatePanelTitle( + this.panelsInfo.get(panel.id), + panelPropsUpdated, + ), + }, + }); + }); + } +} diff --git a/react-electron-poc/src/renderer/components/panels/Panels.ts b/react-electron-poc/src/renderer/components/panels/Panels.ts index da03627..d1d2868 100644 --- a/react-electron-poc/src/renderer/components/panels/Panels.ts +++ b/react-electron-poc/src/renderer/components/panels/Panels.ts @@ -1,11 +1,5 @@ -import { - PanelCollection, - IDockviewPanelProps, - AddPanelOptions, - DockviewReadyEvent, -} from 'dockview'; +import { PanelCollection, IDockviewPanelProps } from 'dockview'; import { createElement, FunctionComponent } from 'react'; -import { newGuid } from '@util/Util'; import { Erb } from './Erb/Erb'; import { TextPanel } from './TextPanels/TextPanel'; import { HtmlTextPanel } from './TextPanels/HtmlTextPanel'; @@ -45,38 +39,3 @@ export const DockViewPanels: PanelCollection = return ; }, }; */ - -/** Dockview Panel builder for our panels */ -export class PanelFactory { - constructor(readonly event: DockviewReadyEvent) {} - - /** Returns AddPanelOptions for the specified input */ - static buildAddPanel( - panelType: PanelType, - panelProps: object = {}, - addPanelOptions: Omit< - AddPanelOptions, - 'id' | 'component' | 'params' - > = {}, - ): AddPanelOptions { - return { - ...addPanelOptions, - id: newGuid(), - component: panelType, - params: { ...panelProps }, - }; - } - - addPanel( - PanelType: PanelType, - panelProps: object = {}, - addPanelOptions: Omit< - AddPanelOptions, - 'id' | 'component' | 'params' - > = {}, - ) { - return this.event.api.addPanel( - PanelFactory.buildAddPanel(PanelType, panelProps, addPanelOptions), - ); - } -} diff --git a/react-electron-poc/src/renderer/util/ScriptureUtil.ts b/react-electron-poc/src/renderer/util/ScriptureUtil.ts new file mode 100644 index 0000000..030e7cf --- /dev/null +++ b/react-electron-poc/src/renderer/util/ScriptureUtil.ts @@ -0,0 +1,115 @@ +import { ScriptureReference } from '@shared/data/ScriptureTypes'; + +const scrBookNames: string[][] = [ + ['ERROR'], + ['GEN', 'Genesis'], + ['EXO', 'Exodus'], + ['LEV', 'Leviticus'], + ['NUM', 'Numbers'], + ['DEU', 'Deuteronomy'], + ['JOS', 'Joshua'], + ['JDG', 'Judges'], + ['RUT', 'Ruth'], + ['1SA', '1 Samuel'], + ['2SA', '2 Samuel'], + ['1KI', '1 Kings'], + ['2KI', '2 Kings'], + ['1CH', '1 Chronicles'], + ['2CH', '2 Chronicles'], + ['EZR', 'Ezra'], + ['NEH', 'Nehemiah'], + ['EST', 'Esther'], + ['JOB', 'Job'], + ['PSA', 'Psalm', 'Psalms'], + ['PRO', 'Proverbs'], + ['ECC', 'Ecclesiastes'], + ['SNG', 'Song of Songs', 'Song of Solomon'], + ['ISA', 'Isaiah'], + ['JER', 'Jeremiah'], + ['LAM', 'Lamentations'], + ['EZK', 'Ezekiel'], + ['DAN', 'Daniel'], + ['HOS', 'Hosea'], + ['JOL', 'Joel'], + ['AMO', 'Amos'], + ['OBA', 'Obadiah'], + ['JON', 'Jonah'], + ['MIC', 'Micah'], + ['NAM', 'Nahum'], + ['HAB', 'Habakkuk'], + ['ZEP', 'Zephaniah'], + ['HAG', 'Haggai'], + ['ZEC', 'Zechariah'], + ['MAL', 'Malachi'], + ['MAT', 'Matthew'], + ['MRK', 'Mark'], + ['LUK', 'Luke'], + ['JHN', 'John'], + ['ACT', 'Acts'], + ['ROM', 'Romans'], + ['1CO', '1 Corinthians'], + ['2CO', '2 Corinthians'], + ['GAL', 'Galatians'], + ['EPH', 'Ephesians'], + ['PHP', 'Philippians'], + ['COL', 'Colossians'], + ['1TH', '1 Thessalonians'], + ['2TH', '2 Thessalonians'], + ['1TI', '1 Timothy'], + ['2TI', '2 Timothy'], + ['TIT', 'Titus'], + ['PHM', 'Philemon'], + ['HEB', 'Hebrews'], + ['JAS', 'James'], + ['1PE', '1 Peter'], + ['2PE', '2 Peter'], + ['1JN', '1 John'], + ['2JN', '2 John'], + ['3JN', '3 John'], + ['JUD', 'Jude'], + ['REV', 'Revelation'], +]; +const firstScrBookNum = 1; +const lastScrBookNum = scrBookNames.length - 1; + +export const getBookNumFromName = (bookName: string): number => { + return scrBookNames.findIndex((bookNames) => bookNames.includes(bookName)); +}; + +export const getAllBookNamesFromNum = (bookNum: number): string[] => { + return [ + ...scrBookNames[ + bookNum < firstScrBookNum || bookNum > lastScrBookNum ? 0 : bookNum + ], + ]; +}; + +export const getBookShortNameFromNum = (bookNum: number): string => { + return scrBookNames[ + bookNum < firstScrBookNum || bookNum > lastScrBookNum ? 0 : bookNum + ][0]; +}; + +export const getBookLongNameFromNum = (bookNum: number): string => { + return scrBookNames[ + bookNum < firstScrBookNum || bookNum > lastScrBookNum ? 0 : bookNum + ][1]; +}; + +const regexpScrRef = /([^ ]+) ([^:]+):(.+)/; +export const getScrRefFromText = (refText: string): ScriptureReference => { + if (!refText) return { book: -1, chapter: -1, verse: -1 }; + const scrRefMatch = refText.match(regexpScrRef); + if (!scrRefMatch || scrRefMatch.length < 4) + return { book: -1, chapter: -1, verse: -1 }; + return { + book: getBookNumFromName(scrRefMatch[1]), + chapter: parseInt(scrRefMatch[2], 10), + verse: parseInt(scrRefMatch[3], 10), + }; +}; + +export const getTextFromScrRef = (scrRef: ScriptureReference): string => + `${getBookLongNameFromNum(scrRef.book)} ${scrRef.chapter}${ + scrRef.verse >= 0 ? `:${scrRef.verse}` : '' + }`; From f7b4ae2f9d5dcf9fb7da589f36c322d24b72f7f5 Mon Sep 17 00:00:00 2001 From: tjcouch-sil Date: Mon, 19 Sep 2022 14:24:05 -0500 Subject: [PATCH 5/6] Added ScrRef buttons, caught errors fetching resource text --- react-electron-poc/src/main/main.ts | 42 ++++++---- .../src/renderer/components/Components.css | 80 ++++++++++++++++-- .../renderer/components/ScrRefSelector.tsx | 84 ++++++++++++++++++- .../renderer/components/panels/Erb/Erb.css | 7 +- .../src/renderer/services/ScriptureService.ts | 78 +++++++++++++---- .../src/renderer/util/ScriptureUtil.ts | 48 ++++++++++- 6 files changed, 297 insertions(+), 42 deletions(-) diff --git a/react-electron-poc/src/main/main.ts b/react-electron-poc/src/main/main.ts index 39bc426..d6bd7cb 100644 --- a/react-electron-poc/src/main/main.ts +++ b/react-electron-poc/src/main/main.ts @@ -189,15 +189,20 @@ async function handleGetScriptureBook( bookNum: number, ): Promise { // TODO: If we want to implement this, parse file and split out into actual chapters - return getFilesText( - [`testScripture/${shortName}/${bookNum}.${fileExtension}`], - getScriptureDelay, - ).then((filesContents) => - filesContents.map((fileContents, ind) => ({ - chapter: ind, - contents: fileContents, - })), - ); + try { + return await getFilesText( + [`testScripture/${shortName}/${bookNum}.${fileExtension}`], + getScriptureDelay, + ).then((filesContents) => + filesContents.map((fileContents, ind) => ({ + chapter: ind, + contents: fileContents, + })), + ); + } catch (e) { + console.log(e); + throw new Error(`No data for ${shortName} ${bookNum}`); + } } /** @@ -220,13 +225,18 @@ async function handleGetScriptureChapter( bookNum: number, chapter: number, ): Promise { - return getFileText( - `testScripture/${shortName}/${bookNum}-${chapter}.${fileExtension}`, - getScriptureDelay, - ).then((fileContents) => ({ - chapter, - contents: fileContents, - })); + try { + return await getFileText( + `testScripture/${shortName}/${bookNum}-${chapter}.${fileExtension}`, + getScriptureDelay, + ).then((fileContents) => ({ + chapter, + contents: fileContents, + })); + } catch (e) { + console.log(e); + throw new Error(`No data for ${shortName} ${bookNum} ${chapter}`); + } } /** These test files are from breakpointing at UsfmSinglePaneControl.cs at the line that gets Css in LoadUsfm. */ diff --git a/react-electron-poc/src/renderer/components/Components.css b/react-electron-poc/src/renderer/components/Components.css index 7b8c242..95085d8 100644 --- a/react-electron-poc/src/renderer/components/Components.css +++ b/react-electron-poc/src/renderer/components/Components.css @@ -1,14 +1,84 @@ .scrref-form { - padding: 5px; + padding: 7px; background-color: #1c1c2a; } -.scrref-input { +.scrref-form button { + font-size: 11pt; +} + +.scrref-form .selector-area { + color: white; +} + +.scrref-form .selector-area span { + margin: 0px 5px; +} + +.scrref-form .selector-area .change-btn { + padding: 2px 1px; + border-right: 1px solid #505070; + border-top: 1px solid #505070; + border-bottom: 1px solid #505070; + border-radius: 0px 5px 5px 0px; + background-color: #505070; + color: #1c1c2a; + box-sizing: border-box; +} + +.scrref-form .selector-area .change-btn.left { + border-radius: 5px 0px 0px 5px; + border-left: 1px solid #505070; + border-right: 0px; +} + +.scrref-form .selector-area .splitter { + margin: 0px; + width: 1px; + display: inline-block; + box-sizing: border-box; +} + +.scrref-form .selector-area .splitter.changed { + border-right: 1px solid #505070; + display: inline; +} + +.scrref-form input { width: 120px; + margin: 4px; + padding: 4px; + border-radius: 5px; + transition: all ease-in 0.1s; +} + +.scrref-form input:focus, +.scrref-form input:hover, +.scrref-form input.changed { + background-color: #505070; + color: white; } -.scrref-button { +.scrref-form .enter-button { padding: 5px 10px; - margin: 2px 5px; - font-size: 11pt; + background-color: #505070; + color: white; +} + +.scrref-form input, +.scrref-form .enter-button:disabled, +.scrref-form .selector-area .change-btn:disabled { + background-color: #1c1c2a; + color: #505070; +} + +.scrref-form input, +.scrref-form .enter-button:disabled { + border: 1px solid #505070; +} + +.scrref-form .selector-area .change-btn:hover:enabled, +.scrref-form .enter-button:hover:enabled { + background-color: #8a8aac; + color: white; } diff --git a/react-electron-poc/src/renderer/components/ScrRefSelector.tsx b/react-electron-poc/src/renderer/components/ScrRefSelector.tsx index dc16724..dd39949 100644 --- a/react-electron-poc/src/renderer/components/ScrRefSelector.tsx +++ b/react-electron-poc/src/renderer/components/ScrRefSelector.tsx @@ -1,5 +1,13 @@ import { ScriptureReference } from '@shared/data/ScriptureTypes'; -import { getTextFromScrRef, getScrRefFromText } from '@util/ScriptureUtil'; +import { + getTextFromScrRef, + getScrRefFromText, + getBookLongNameFromNum, + areScrRefsEqual, + offsetBook, + offsetChapter, + offsetVerse, +} from '@util/ScriptureUtil'; import React, { useCallback, useEffect, useState } from 'react'; import './Components.css'; @@ -25,6 +33,8 @@ export default ({ scrRef, handleSubmit }: ScrRefSelectorProps) => { setCurrentRefText(getTextFromScrRef(scrRef)); }, [scrRef]); + const isScrRefChanged = !areScrRefsEqual(currentRefText, scrRef); + return (
{ handleSubmit(getScrRefFromText(currentRefText)); }} > + + {getBookLongNameFromNum(scrRef.book)} + + + + {scrRef.chapter}: + + + + {scrRef.verse} + + + + - diff --git a/react-electron-poc/src/renderer/components/panels/Erb/Erb.css b/react-electron-poc/src/renderer/components/panels/Erb/Erb.css index dbdc665..bcfe65f 100644 --- a/react-electron-poc/src/renderer/components/panels/Erb/Erb.css +++ b/react-electron-poc/src/renderer/components/panels/Erb/Erb.css @@ -29,11 +29,14 @@ button { box-shadow: 0px 8px 28px -6px rgba(24, 39, 75, 0.12), 0px 18px 88px -4px rgba(24, 39, 75, 0.14); transition: all ease-in 0.1s; - cursor: pointer; opacity: 0.9; } -button:hover { +button:enabled { + cursor: pointer; +} + +button:hover:enabled { transform: scale(1.05); opacity: 1; } diff --git a/react-electron-poc/src/renderer/services/ScriptureService.ts b/react-electron-poc/src/renderer/services/ScriptureService.ts index e1f871a..e739420 100644 --- a/react-electron-poc/src/renderer/services/ScriptureService.ts +++ b/react-electron-poc/src/renderer/services/ScriptureService.ts @@ -3,6 +3,7 @@ import { ScriptureChapterContent, ScriptureChapterString, } from '@shared/data/ScriptureTypes'; +import { getTextFromScrRef } from '@util/ScriptureUtil'; /** * Gets the specified Scripture chapter in the specified book from the specified project in Slate JSON @@ -16,11 +17,28 @@ export const getScripture = async ( bookNum: number, chapter = -1, ): Promise => { - return chapter >= 0 - ? window.electronAPI.scripture - .getScriptureChapter(shortName, bookNum, chapter) - .then((result) => [result]) - : window.electronAPI.scripture.getScriptureBook(shortName, bookNum); + try { + return chapter >= 0 + ? await window.electronAPI.scripture + .getScriptureChapter(shortName, bookNum, chapter) + .then((result) => [result]) + : await window.electronAPI.scripture.getScriptureBook( + shortName, + bookNum, + ); + } catch (e) { + console.log(e); + return [ + { + chapter, + contents: { + text: `Could not get contents of ${shortName} ${getTextFromScrRef( + { book: bookNum, chapter, verse: -1 }, + )}`, + }, + }, + ]; + } }; /** @@ -35,11 +53,26 @@ export const getScriptureRaw = async ( bookNum: number, chapter = -1, ): Promise => { - return chapter >= 0 - ? window.electronAPI.scripture - .getScriptureChapterRaw(shortName, bookNum, chapter) - .then((result) => [result]) - : window.electronAPI.scripture.getScriptureBookRaw(shortName, bookNum); + try { + return chapter >= 0 + ? await window.electronAPI.scripture + .getScriptureChapterRaw(shortName, bookNum, chapter) + .then((result) => [result]) + : await window.electronAPI.scripture.getScriptureBookRaw( + shortName, + bookNum, + ); + } catch (e) { + console.log(e); + return [ + { + chapter, + contents: `Could not get contents of ${shortName} ${getTextFromScrRef( + { book: bookNum, chapter, verse: -1 }, + )}`, + }, + ]; + } }; /** @@ -54,11 +87,26 @@ export const getScriptureHtml = async ( bookNum: number, chapter = -1, ): Promise => { - return chapter >= 0 - ? window.electronAPI.scripture - .getScriptureChapterHtml(shortName, bookNum, chapter) - .then((result) => [result]) - : window.electronAPI.scripture.getScriptureBookHtml(shortName, bookNum); + try { + return chapter >= 0 + ? await window.electronAPI.scripture + .getScriptureChapterHtml(shortName, bookNum, chapter) + .then((result) => [result]) + : await window.electronAPI.scripture.getScriptureBookHtml( + shortName, + bookNum, + ); + } catch (e) { + console.log(e); + return [ + { + chapter, + contents: `Could not get contents of ${shortName} ${getTextFromScrRef( + { book: bookNum, chapter, verse: -1 }, + )}`, + }, + ]; + } }; /** diff --git a/react-electron-poc/src/renderer/util/ScriptureUtil.ts b/react-electron-poc/src/renderer/util/ScriptureUtil.ts index 030e7cf..bed7904 100644 --- a/react-electron-poc/src/renderer/util/ScriptureUtil.ts +++ b/react-electron-poc/src/renderer/util/ScriptureUtil.ts @@ -1,7 +1,8 @@ import { ScriptureReference } from '@shared/data/ScriptureTypes'; +import { isString } from './Util'; const scrBookNames: string[][] = [ - ['ERROR'], + ['ERR', 'ERROR'], ['GEN', 'Genesis'], ['EXO', 'Exodus'], ['LEV', 'Leviticus'], @@ -96,6 +97,32 @@ export const getBookLongNameFromNum = (bookNum: number): string => { ][1]; }; +export const offsetBook = ( + scrRef: ScriptureReference, + offset: number, +): ScriptureReference => ({ + book: Math.max( + firstScrBookNum, + Math.min(scrRef.book + offset, lastScrBookNum), + ), + chapter: 1, + verse: 1, +}); + +export const offsetChapter = ( + scrRef: ScriptureReference, + offset: number, +): ScriptureReference => ({ + ...scrRef, + chapter: scrRef.chapter + offset, + verse: 1, +}); + +export const offsetVerse = ( + scrRef: ScriptureReference, + offset: number, +): ScriptureReference => ({ ...scrRef, verse: scrRef.verse + offset }); + const regexpScrRef = /([^ ]+) ([^:]+):(.+)/; export const getScrRefFromText = (refText: string): ScriptureReference => { if (!refText) return { book: -1, chapter: -1, verse: -1 }; @@ -113,3 +140,22 @@ export const getTextFromScrRef = (scrRef: ScriptureReference): string => `${getBookLongNameFromNum(scrRef.book)} ${scrRef.chapter}${ scrRef.verse >= 0 ? `:${scrRef.verse}` : '' }`; + +export const areScrRefsEqual = ( + scrRef1: ScriptureReference | string, + scrRef2: ScriptureReference | string, +): boolean => { + if (scrRef1 === scrRef2) return true; + + const scrRef1Final: ScriptureReference = isString(scrRef1) + ? getScrRefFromText(scrRef1 as string) + : (scrRef1 as ScriptureReference); + const scrRef2Final: ScriptureReference = isString(scrRef2) + ? getScrRefFromText(scrRef2 as string) + : (scrRef2 as ScriptureReference); + return ( + scrRef1Final.book === scrRef2Final.book && + scrRef1Final.chapter === scrRef2Final.chapter && + scrRef1Final.verse === scrRef2Final.verse + ); +}; From 7973504f30aa06a9f84a06af211984541906dde1 Mon Sep 17 00:00:00 2001 From: tjcouch-sil Date: Mon, 19 Sep 2022 14:49:51 -0500 Subject: [PATCH 6/6] Added reactive content-editable for editable chapters --- react-electron-poc/.eslintrc.js | 1 - .../panels/TextPanels/ScriptureTextPanel.tsx | 69 ++++++++++++++++--- .../src/renderer/services/ScriptureService.ts | 6 +- 3 files changed, 61 insertions(+), 15 deletions(-) diff --git a/react-electron-poc/.eslintrc.js b/react-electron-poc/.eslintrc.js index ff33f89..2eed088 100644 --- a/react-electron-poc/.eslintrc.js +++ b/react-electron-poc/.eslintrc.js @@ -9,7 +9,6 @@ module.exports = { // TJ's rules indent: 'off', - 'react/jsx-indent': ['warn', 4], 'react/jsx-indent-props': ['warn', 4], 'comma-dangle': ['error', 'always-multiline'], 'prettier/prettier': ['warn', { tabWidth: 4, trailingComma: 'all' }], diff --git a/react-electron-poc/src/renderer/components/panels/TextPanels/ScriptureTextPanel.tsx b/react-electron-poc/src/renderer/components/panels/TextPanels/ScriptureTextPanel.tsx index e99ff5f..8e26d51 100644 --- a/react-electron-poc/src/renderer/components/panels/TextPanels/ScriptureTextPanel.tsx +++ b/react-electron-poc/src/renderer/components/panels/TextPanels/ScriptureTextPanel.tsx @@ -7,8 +7,10 @@ import { ScriptureChapter, ScriptureReference, } from '@shared/data/ScriptureTypes'; +import { getTextFromScrRef } from '@util/ScriptureUtil'; import { isValidValue } from '@util/Util'; import { useCallback, useEffect, useRef, useState } from 'react'; +import ContentEditable, { ContentEditableEvent } from 'react-contenteditable'; import usePromise from 'renderer/hooks/usePromise'; import useStyle from 'renderer/hooks/useStyle'; import './TextPanel.css'; @@ -42,22 +44,67 @@ export const ScriptureTextPanel = ({ return getScriptureHtml(shortName, book, chapter); }, [shortName, book, chapter]), useState([ - { chapter: -1, contents: `Loading ${shortName}...` }, + { + chapter: -1, + contents: `Loading ${shortName} ${getTextFromScrRef({ + book, + chapter, + verse: -1, + })}...`, + }, ])[0], ); + const editableScrChapters = useRef([ + { + chapter: -1, + contents: `Loading ${shortName} ${getTextFromScrRef({ + book, + chapter, + verse: -1, + })}...`, + }, + ]); + const [, setForceRefresh] = useState(0); + const forceRefresh = useCallback( + () => setForceRefresh((value) => value + 1), + [setForceRefresh], + ); + + useEffect(() => { + editableScrChapters.current = scrChapters; + forceRefresh(); + }, [scrChapters, forceRefresh]); + + const handleChange = (evt: ContentEditableEvent, editedChapter: number) => { + const editedChapterInd = editableScrChapters.current.findIndex( + (scrChapter) => scrChapter.chapter === editedChapter, + ); + editableScrChapters.current[editedChapterInd] = { + ...editableScrChapters.current[editedChapterInd], + contents: evt.target.value, + }; + }; + return (
- {scrChapters.map((scrChapter) => ( -
- ))} + {editable + ? editableScrChapters.current.map((scrChapter) => ( + handleChange(e, scrChapter.chapter)} + /> + )) + : scrChapters.map((scrChapter) => ( +
+ ))}
); }; diff --git a/react-electron-poc/src/renderer/services/ScriptureService.ts b/react-electron-poc/src/renderer/services/ScriptureService.ts index e739420..95e0a4c 100644 --- a/react-electron-poc/src/renderer/services/ScriptureService.ts +++ b/react-electron-poc/src/renderer/services/ScriptureService.ts @@ -34,7 +34,7 @@ export const getScripture = async ( contents: { text: `Could not get contents of ${shortName} ${getTextFromScrRef( { book: bookNum, chapter, verse: -1 }, - )}`, + )}.`, }, }, ]; @@ -69,7 +69,7 @@ export const getScriptureRaw = async ( chapter, contents: `Could not get contents of ${shortName} ${getTextFromScrRef( { book: bookNum, chapter, verse: -1 }, - )}`, + )}.`, }, ]; } @@ -103,7 +103,7 @@ export const getScriptureHtml = async ( chapter, contents: `Could not get contents of ${shortName} ${getTextFromScrRef( { book: bookNum, chapter, verse: -1 }, - )}`, + )}.`, }, ]; }