-
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathInterlinearizerLoader.tsx
More file actions
201 lines (184 loc) · 7.76 KB
/
InterlinearizerLoader.tsx
File metadata and controls
201 lines (184 loc) · 7.76 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
import type { UseWebViewScrollGroupScrRefHook, UseWebViewStateHook } from '@papi/core';
import papi, { logger } from '@papi/frontend';
import { useData, useLocalizedStrings, useSetting } from '@papi/frontend/react';
import { TabToolbar } from 'platform-bible-react';
import type { SelectMenuItemHandler } from 'platform-bible-react';
import { isPlatformError } from 'platform-bible-utils';
import { useCallback, useMemo, useState } from 'react';
import useInterlinearizerBookData from '../hooks/useInterlinearizerBookData';
import useOptimisticBooleanSetting from '../hooks/useOptimisticBooleanSetting';
import type { InterlinearProjectSummary } from '../types/interlinear-project-summary';
import ContinuousScrollToggle from './ContinuousScrollToggle';
import Interlinearizer from './Interlinearizer';
import ProjectModals, { type ModalState } from './ProjectModals';
import ScriptureNavControls from './ScriptureNavControls';
const STRING_KEYS: `%${string}%`[] = ['%interlinearizer_continuousScrollToggle%'];
/**
* Root component for the Interlinearizer WebView. Loads book data and settings, manages modal state
* for project creation/selection/metadata, then renders error and loading states or delegates to
* {@link Interlinearizer} when data is ready.
*
* @param props - Component props
* @param props.projectId - PAPI project ID passed from the host
* @param props.useWebViewScrollGroupScrRef - Hook that exposes the shared scroll-group scripture
* reference and its setter
* @param props.useWebViewState - Hook for reading and writing values persisted in the WebView's
* saved state (survives tab restores)
* @returns The interlinearizer layout: tab toolbar, loading/error states or main view, and any
* currently open project modal.
*/
export default function InterlinearizerLoader({
projectId,
useWebViewScrollGroupScrRef,
useWebViewState,
}: Readonly<{
projectId: string;
useWebViewScrollGroupScrRef: UseWebViewScrollGroupScrRefHook;
useWebViewState: UseWebViewStateHook;
}>) {
const [scrRef, setScrRef, scrollGroupId, setScrollGroupId] = useWebViewScrollGroupScrRef();
const [interfaceMode] = useSetting('platform.interfaceMode', 'simple');
const {
isLoading: isSettingLoading,
onChange: handleContinuousScrollChange,
value: continuousScroll,
} = useOptimisticBooleanSetting(projectId, 'interlinearizer.continuousScroll', true);
const { book, chapterSegments, isLoading, bookError, tokenizeError } = useInterlinearizerBookData(
{ projectId, scrRef },
);
const hasError = !!bookError || !!tokenizeError;
const showLoading = isLoading || isSettingLoading;
const [localizedStrings] = useLocalizedStrings(STRING_KEYS);
const [modal, setModal] = useState<ModalState>('none');
/**
* Persisted snapshot of the active interlinear project — kept in WebView state so it survives tab
* restores. The setter lives in {@link ProjectModals}, which writes to the same `'activeProject'`
* key; this component reads the value to decide which menu items to show.
*/
const [activeProject] = useWebViewState<InterlinearProjectSummary | undefined>(
'activeProject',
undefined,
);
/**
* Routes top-menu commands to the appropriate modal. `openSelectProjectModal` opens the select
* modal; `openNewProjectModal` opens the create modal directly; `openProjectInfoModal` opens the
* metadata modal for the currently active project.
*
* @param item - The menu item that was activated.
*/
const menuCommandHandler = useCallback<SelectMenuItemHandler>(
(item) => {
if (item.command === 'interlinearizer.openSelectProjectModal') {
setModal('select');
} else if (item.command === 'interlinearizer.openNewProjectModal') {
setModal('create');
} else if (item.command === 'interlinearizer.openProjectInfoModal') {
if (activeProject) {
setModal('metadata');
}
}
},
[activeProject],
);
/**
* Fetches the top-menu data for this WebView from the platform's menu data provider, hiding "View
* Project Info" when no interlinear project is currently active.
*/
const [webViewMenuPossiblyError] = useData(papi.menuData.dataProviderName).WebViewMenu(
'interlinearizer.mainWebView',
{ topMenu: undefined, includeDefaults: true, contextMenu: undefined },
);
/**
* Top-menu descriptor passed to {@link TabToolbar}. Identical to
* `webViewMenuPossiblyError.topMenu` except that the `interlinearizer.openProjectInfoModal` item
* is filtered out when no project is active, since that command requires an active project to act
* on.
*/
const projectMenuData = useMemo(() => {
/* v8 ignore next 3 -- PlatformError from useData is not reachable through the mock */
const menu =
webViewMenuPossiblyError && !isPlatformError(webViewMenuPossiblyError)
? webViewMenuPossiblyError
: { topMenu: undefined, includeDefaults: true, contextMenu: undefined };
if (!menu.topMenu || activeProject) return menu.topMenu;
const { items } = menu.topMenu;
/* v8 ignore next */ if (!Array.isArray(items)) return menu.topMenu;
return {
...menu.topMenu,
items: items.filter(
(item) => !('command' in item) || item.command !== 'interlinearizer.openProjectInfoModal',
),
};
}, [webViewMenuPossiblyError, activeProject]);
return (
<div className="tw:flex tw:flex-col tw:h-full">
<TabToolbar
className="tw:z-10"
projectMenuData={projectMenuData}
startAreaChildren={
interfaceMode === 'power' ? (
<ScriptureNavControls
scrRef={scrRef}
handleSubmit={setScrRef}
scrollGroupId={scrollGroupId}
onChangeScrollGroupId={setScrollGroupId}
/>
) : undefined
}
endAreaChildren={
<ContinuousScrollToggle
checked={continuousScroll}
disabled={isSettingLoading}
label={localizedStrings['%interlinearizer_continuousScrollToggle%']}
onCheckedChange={handleContinuousScrollChange}
/>
}
onSelectProjectMenuItem={menuCommandHandler}
/* v8 ignore next 3 -- stub required by TabToolbar API, no behaviour to test */
onSelectViewInfoMenuItem={() => {
logger.warn('Interlinearizer: unexpected onSelectViewInfoMenuItem call');
}}
/>
{hasError || showLoading || !book ? (
<div className="tw:flex tw:flex-col tw:gap-4 tw:p-4">
{bookError && (
<div className="tw:flex tw:flex-col tw:gap-2">
<h2 className="tw:text-lg tw:font-medium tw:text-destructive">Error loading book</h2>
<pre className="tw:overflow-auto tw:rounded-md tw:bg-muted tw:text-foreground tw:p-4 tw:text-sm">
{bookError}
</pre>
</div>
)}
{tokenizeError && (
<div className="tw:flex tw:flex-col tw:gap-2">
<h2 className="tw:text-lg tw:font-medium tw:text-destructive">
Error processing book
</h2>
<pre className="tw:overflow-auto tw:rounded-md tw:bg-muted tw:text-foreground tw:p-4 tw:text-sm">
{tokenizeError.message}
</pre>
</div>
)}
{!hasError && showLoading && (
<p className="tw:text-sm tw:text-muted-foreground">Loading…</p>
)}
</div>
) : (
<Interlinearizer
book={book}
bookSegments={chapterSegments}
continuousScroll={continuousScroll}
scrRef={scrRef}
setScrRef={setScrRef}
/>
)}
<ProjectModals
activeProject={activeProject}
modal={modal}
projectId={projectId}
setModal={setModal}
useWebViewState={useWebViewState}
/>
</div>
);
}