@khorsheed/dsh-reader 0.3.0 → 0.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +29 -0
- package/README.en.md +26 -121
- package/README.i18n.yaml +3 -3
- package/README.md +26 -121
- package/cordis.patch.yml +7 -0
- package/lib/client.js +29105 -6692
- package/lib/index.js +3500 -321
- package/lib/typert.host.js +2145 -259
- package/lib/typert.remote-client.d.ts +97 -3
- package/lib/typert.remote-client.js +1590 -250
- package/lib/types/captions.d.ts +99 -0
- package/lib/types/captions.js +236 -0
- package/lib/types/client/MediaPlayer.d.ts +55 -0
- package/lib/types/client/MediaPlayer.js +106 -0
- package/lib/types/client/NotesView.d.ts +57 -0
- package/lib/types/client/NotesView.js +124 -0
- package/lib/types/client/ReaderPane.d.ts +20 -3
- package/lib/types/client/ReaderPane.js +860 -164
- package/lib/types/client/SpeakerLegend.d.ts +18 -0
- package/lib/types/client/SpeakerLegend.js +26 -0
- package/lib/types/client/canvas-sync.d.ts +84 -0
- package/lib/types/client/canvas-sync.js +59 -0
- package/lib/types/client/contract.d.ts +130 -3
- package/lib/types/client/extract-article.d.ts +38 -1
- package/lib/types/client/extract-article.js +561 -33
- package/lib/types/client/icons.d.ts +1 -1
- package/lib/types/client/icons.js +1 -1
- package/lib/types/client/index.js +81 -2
- package/lib/types/client/locales.d.ts +279 -11
- package/lib/types/client/locales.js +278 -10
- package/lib/types/client/notes.d.ts +142 -0
- package/lib/types/client/notes.js +334 -0
- package/lib/types/client/parse-rss.js +40 -0
- package/lib/types/client/preset-visibility.d.ts +93 -0
- package/lib/types/client/preset-visibility.js +115 -0
- package/lib/types/client/selectors.d.ts +5 -0
- package/lib/types/client/selectors.js +5 -0
- package/lib/types/client/session.d.ts +21 -1
- package/lib/types/client/session.js +12 -0
- package/lib/types/client/store.js +4 -1
- package/lib/types/client/translate.d.ts +26 -1
- package/lib/types/client/translate.js +29 -12
- package/lib/types/client/useLocalTranscript.d.ts +43 -0
- package/lib/types/client/useLocalTranscript.js +129 -0
- package/lib/types/client/useMedia.d.ts +68 -0
- package/lib/types/client/useMedia.js +299 -0
- package/lib/types/client/useMediaStates.d.ts +9 -0
- package/lib/types/client/useMediaStates.js +43 -0
- package/lib/types/client/useNotes.d.ts +110 -0
- package/lib/types/client/useNotes.js +423 -0
- package/lib/types/client/usePolish.d.ts +47 -0
- package/lib/types/client/usePolish.js +162 -0
- package/lib/types/document-fetch.d.ts +106 -0
- package/lib/types/document-fetch.js +100 -0
- package/lib/types/index.d.ts +15 -0
- package/lib/types/index.js +3 -0
- package/lib/types/local-transcribe.d.ts +171 -0
- package/lib/types/local-transcribe.js +473 -0
- package/lib/types/media-host.d.ts +91 -0
- package/lib/types/media-host.js +230 -0
- package/lib/types/media.d.ts +100 -0
- package/lib/types/media.js +207 -0
- package/lib/types/notes-types.d.ts +158 -0
- package/lib/types/notes-types.js +8 -0
- package/lib/types/notes.d.ts +213 -0
- package/lib/types/notes.js +718 -0
- package/lib/types/polish.d.ts +41 -0
- package/lib/types/polish.js +136 -0
- package/lib/types/remote.d.ts +118 -0
- package/lib/types/remote.js +151 -1
- package/lib/types/service.d.ts +81 -6
- package/lib/types/service.js +286 -65
- package/lib/types/skill.d.ts +20 -0
- package/lib/types/skill.js +78 -0
- package/lib/types/store.js +30 -8
- package/lib/types/tool.d.ts +23 -0
- package/lib/types/tool.js +40 -0
- package/lib/types/tools.d.ts +69 -0
- package/lib/types/tools.js +400 -0
- package/lib/types/types.d.ts +90 -2
- package/lib/types/types.js +66 -0
- package/lib/types/xiaoyuzhou.d.ts +43 -0
- package/lib/types/xiaoyuzhou.js +134 -0
- package/lib/types/ytdlp.d.ts +111 -0
- package/lib/types/ytdlp.js +269 -0
- package/package.json +33 -20
- package/lib/invariant.js +0 -38
- package/lib/types/invariant.d.ts +0 -29
- package/lib/types/invariant.js +0 -38
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The detail view's media half, as one hook: the transcript's state, the
|
|
3
|
+
* 「字幕」 switch, the remembered position, the current paragraph and
|
|
4
|
+
* following it, seeking from a paragraph's time, and the player's size (full,
|
|
5
|
+
* or the mini bar while the reader is down in the transcript).
|
|
6
|
+
*
|
|
7
|
+
* Following: the playing paragraph scrolls into view until the reader
|
|
8
|
+
* scrolls by hand; then it stops — no timer pulls the page back — and
|
|
9
|
+
* 「回到正在播放」 resumes it (2026-10-20).
|
|
10
|
+
*
|
|
11
|
+
* The transcript is the article body (`<p data-t>` blocks, captions.ts). This
|
|
12
|
+
* hook only decorates it — a time chip per paragraph, drawn from a
|
|
13
|
+
* `data-label` through CSS so a selection never picks the time up — and marks
|
|
14
|
+
* the current paragraph with `data-now`. It never rewrites text nodes, which
|
|
15
|
+
* belong to `translate.ts`.
|
|
16
|
+
*
|
|
17
|
+
* @module @khorsheed/dsh-reader/client/useMedia
|
|
18
|
+
*/
|
|
19
|
+
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
20
|
+
import { formatClock } from "../captions.js";
|
|
21
|
+
import { blockElements } from "./notes.js";
|
|
22
|
+
/** Scrolled this far into the transcript, a video shrinks to the mini bar (px)… */
|
|
23
|
+
const MINI_AFTER_PX = 160;
|
|
24
|
+
/** …and back near the top it opens again. */
|
|
25
|
+
const FULL_BEFORE_PX = 24;
|
|
26
|
+
/** Positions kept per entry, at most. */
|
|
27
|
+
const MAX_POSITIONS = 200;
|
|
28
|
+
const POSITIONS_KEY = 'dsh-reader.mediaPositions';
|
|
29
|
+
const HIDDEN_KEY = 'dsh-reader.captionsHidden';
|
|
30
|
+
const HINT_KEY = 'dsh-reader.timeHintSeen';
|
|
31
|
+
/** A small per-browser record (a convenience: an empty store is a fine answer). */
|
|
32
|
+
function readRecord(key) {
|
|
33
|
+
try {
|
|
34
|
+
return JSON.parse(globalThis.localStorage?.getItem(key) ?? '{}');
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return {};
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
function writeRecord(key, value) {
|
|
41
|
+
try {
|
|
42
|
+
globalThis.localStorage?.setItem(key, JSON.stringify(value));
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
// Private window / blocked storage: the position simply is not remembered.
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
/** The start time a block carries (`data-t`), when it is a transcript paragraph. */
|
|
49
|
+
export function blockTime(element) {
|
|
50
|
+
const value = element?.getAttribute('data-t');
|
|
51
|
+
if (value === null || value === undefined)
|
|
52
|
+
return undefined;
|
|
53
|
+
const seconds = Number(value);
|
|
54
|
+
return Number.isFinite(seconds) ? seconds : undefined;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* The open entry's media state.
|
|
58
|
+
*
|
|
59
|
+
* @param input - what the pane already holds.
|
|
60
|
+
*/
|
|
61
|
+
export function useMedia(input) {
|
|
62
|
+
const { entryId, media, articleHtml, articleRef, detailRef, chipTitle, speakerLabel } = input;
|
|
63
|
+
const [transcriptOf, setTranscriptOf] = useState(undefined);
|
|
64
|
+
const transcript = transcriptOf !== undefined && transcriptOf.entryId === entryId ? transcriptOf.state : undefined;
|
|
65
|
+
const setTranscript = useCallback((id, state) => { setTranscriptOf({ entryId: id, state }); }, []);
|
|
66
|
+
const [hiddenIds, setHiddenIds] = useState(() => readRecord(HIDDEN_KEY));
|
|
67
|
+
const [followPaused, setFollowPaused] = useState(false);
|
|
68
|
+
const [mini, setMiniState] = useState(false);
|
|
69
|
+
const [hintOwed, setHintOwed] = useState(() => {
|
|
70
|
+
try {
|
|
71
|
+
return globalThis.localStorage?.getItem(HINT_KEY) !== '1';
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
const playerRef = useRef(null);
|
|
78
|
+
const nowRef = useRef(null);
|
|
79
|
+
const paused = useRef(false);
|
|
80
|
+
/** A mini bar the reader asked for stays until they open it again. */
|
|
81
|
+
const pinnedMini = useRef(false);
|
|
82
|
+
const ownScroll = useRef(false);
|
|
83
|
+
/** When the playback time last moved (a paused video has nothing to follow). */
|
|
84
|
+
const advancedAt = useRef(0);
|
|
85
|
+
const lastSeconds = useRef(undefined);
|
|
86
|
+
const lastSaved = useRef(0);
|
|
87
|
+
const startAt = entryId === undefined ? 0 : readRecord(POSITIONS_KEY)[entryId] ?? 0;
|
|
88
|
+
const hidden = entryId !== undefined && hiddenIds[entryId] === true;
|
|
89
|
+
// A different entry: no paragraph is playing yet, the player opens full.
|
|
90
|
+
useEffect(() => {
|
|
91
|
+
nowRef.current = null;
|
|
92
|
+
paused.current = false;
|
|
93
|
+
pinnedMini.current = false;
|
|
94
|
+
setFollowPaused(false);
|
|
95
|
+
setMiniState(false);
|
|
96
|
+
}, [entryId]);
|
|
97
|
+
const setMini = useCallback((value) => {
|
|
98
|
+
pinnedMini.current = value;
|
|
99
|
+
setMiniState(value);
|
|
100
|
+
}, []);
|
|
101
|
+
const dismissHint = useCallback(() => {
|
|
102
|
+
setHintOwed(false);
|
|
103
|
+
try {
|
|
104
|
+
globalThis.localStorage?.setItem(HINT_KEY, '1');
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
// The hint shows again next time; harmless.
|
|
108
|
+
}
|
|
109
|
+
}, []);
|
|
110
|
+
const setHidden = useCallback((value) => {
|
|
111
|
+
if (entryId === undefined)
|
|
112
|
+
return;
|
|
113
|
+
setHiddenIds(current => {
|
|
114
|
+
const next = { ...current };
|
|
115
|
+
if (value)
|
|
116
|
+
next[entryId] = true;
|
|
117
|
+
else
|
|
118
|
+
delete next[entryId];
|
|
119
|
+
writeRecord(HIDDEN_KEY, next);
|
|
120
|
+
return next;
|
|
121
|
+
});
|
|
122
|
+
}, [entryId]);
|
|
123
|
+
// Decorate the transcript's paragraphs with their time chip.
|
|
124
|
+
useEffect(() => {
|
|
125
|
+
const article = articleRef.current;
|
|
126
|
+
if (article === null || media === undefined)
|
|
127
|
+
return;
|
|
128
|
+
for (const block of blockElements(article)) {
|
|
129
|
+
const seconds = blockTime(block);
|
|
130
|
+
if (seconds === undefined || block.querySelector(':scope > [data-reader-ts]') !== null)
|
|
131
|
+
continue;
|
|
132
|
+
const chip = article.ownerDocument.createElement('button');
|
|
133
|
+
chip.type = 'button';
|
|
134
|
+
chip.setAttribute('data-reader-ts', String(seconds));
|
|
135
|
+
chip.setAttribute('data-label', formatClock(seconds));
|
|
136
|
+
chip.setAttribute('data-reader-no-translate', '1');
|
|
137
|
+
chip.setAttribute('aria-label', chipTitle(formatClock(seconds)));
|
|
138
|
+
chip.title = chipTitle(formatClock(seconds));
|
|
139
|
+
block.prepend(chip);
|
|
140
|
+
}
|
|
141
|
+
}, [articleHtml, media, articleRef, chipTitle]);
|
|
142
|
+
// Who speaks: a name at the start of the paragraph where the voice changes
|
|
143
|
+
// (a transcript's habit — not repeated on every paragraph), drawn from CSS
|
|
144
|
+
// content like the time, so selections and translation never take it; kept
|
|
145
|
+
// current as the reader renames a speaker.
|
|
146
|
+
useEffect(() => {
|
|
147
|
+
const article = articleRef.current;
|
|
148
|
+
if (article === null || media === undefined)
|
|
149
|
+
return;
|
|
150
|
+
let previous = null;
|
|
151
|
+
for (const block of blockElements(article)) {
|
|
152
|
+
const spk = block.getAttribute('data-spk');
|
|
153
|
+
if (spk === null)
|
|
154
|
+
continue;
|
|
155
|
+
const changes = spk !== previous;
|
|
156
|
+
previous = spk;
|
|
157
|
+
let label = block.querySelector(':scope > [data-reader-spk]');
|
|
158
|
+
if (!changes) {
|
|
159
|
+
label?.remove();
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
if (label === null) {
|
|
163
|
+
label = article.ownerDocument.createElement('span');
|
|
164
|
+
label.setAttribute('data-reader-spk', spk);
|
|
165
|
+
label.setAttribute('data-reader-no-translate', '1');
|
|
166
|
+
const chip = block.querySelector(':scope > [data-reader-ts]');
|
|
167
|
+
if (chip !== null)
|
|
168
|
+
chip.after(label);
|
|
169
|
+
else
|
|
170
|
+
block.prepend(label);
|
|
171
|
+
}
|
|
172
|
+
label.setAttribute('data-label', speakerLabel(Number(spk)));
|
|
173
|
+
}
|
|
174
|
+
}, [articleHtml, media, articleRef, speakerLabel]);
|
|
175
|
+
const seek = useCallback((seconds) => {
|
|
176
|
+
paused.current = false;
|
|
177
|
+
setFollowPaused(false);
|
|
178
|
+
playerRef.current?.seek(seconds);
|
|
179
|
+
}, []);
|
|
180
|
+
// A chip click seeks (capture phase: it must not reach the translation click).
|
|
181
|
+
useEffect(() => {
|
|
182
|
+
const article = articleRef.current;
|
|
183
|
+
if (article === null || media === undefined)
|
|
184
|
+
return;
|
|
185
|
+
const onClick = (event) => {
|
|
186
|
+
const chip = event.target?.closest?.('[data-reader-ts]');
|
|
187
|
+
if (chip === null || chip === undefined)
|
|
188
|
+
return;
|
|
189
|
+
event.preventDefault();
|
|
190
|
+
event.stopPropagation();
|
|
191
|
+
seek(Number(chip.getAttribute('data-reader-ts')));
|
|
192
|
+
};
|
|
193
|
+
article.addEventListener('click', onClick, true);
|
|
194
|
+
return () => { article.removeEventListener('click', onClick, true); };
|
|
195
|
+
}, [articleHtml, media, articleRef, seek]);
|
|
196
|
+
// A hand scroll stops following (our own scroll does not), and the scroll
|
|
197
|
+
// depth sizes a video: down in the transcript it is the mini bar.
|
|
198
|
+
useEffect(() => {
|
|
199
|
+
const scroller = detailRef.current;
|
|
200
|
+
if (scroller === null || media === undefined)
|
|
201
|
+
return;
|
|
202
|
+
const onScroll = () => {
|
|
203
|
+
if (media.kind === 'video' && !pinnedMini.current) {
|
|
204
|
+
if (scroller.scrollTop > MINI_AFTER_PX)
|
|
205
|
+
setMiniState(true);
|
|
206
|
+
else if (scroller.scrollTop < FULL_BEFORE_PX)
|
|
207
|
+
setMiniState(false);
|
|
208
|
+
}
|
|
209
|
+
};
|
|
210
|
+
// Only the reader's own gestures stop following — a layout change (the
|
|
211
|
+
// player shrinking) scrolls too, and must not count.
|
|
212
|
+
const onHand = () => {
|
|
213
|
+
if (ownScroll.current || paused.current || nowRef.current === null)
|
|
214
|
+
return;
|
|
215
|
+
if (Date.now() - advancedAt.current > 3000)
|
|
216
|
+
return;
|
|
217
|
+
paused.current = true;
|
|
218
|
+
setFollowPaused(true);
|
|
219
|
+
};
|
|
220
|
+
const onKey = (event) => {
|
|
221
|
+
if (['ArrowUp', 'ArrowDown', 'PageUp', 'PageDown', 'Home', 'End', ' '].includes(event.key))
|
|
222
|
+
onHand();
|
|
223
|
+
};
|
|
224
|
+
scroller.addEventListener('scroll', onScroll, { passive: true });
|
|
225
|
+
scroller.addEventListener('wheel', onHand, { passive: true });
|
|
226
|
+
scroller.addEventListener('touchmove', onHand, { passive: true });
|
|
227
|
+
scroller.addEventListener('keydown', onKey);
|
|
228
|
+
return () => {
|
|
229
|
+
scroller.removeEventListener('scroll', onScroll);
|
|
230
|
+
scroller.removeEventListener('wheel', onHand);
|
|
231
|
+
scroller.removeEventListener('touchmove', onHand);
|
|
232
|
+
scroller.removeEventListener('keydown', onKey);
|
|
233
|
+
};
|
|
234
|
+
}, [media, detailRef, entryId]);
|
|
235
|
+
const onTime = useCallback((seconds) => {
|
|
236
|
+
if (entryId === undefined)
|
|
237
|
+
return;
|
|
238
|
+
if (lastSeconds.current !== undefined && Math.abs(seconds - lastSeconds.current) > 0.05)
|
|
239
|
+
advancedAt.current = Date.now();
|
|
240
|
+
lastSeconds.current = seconds;
|
|
241
|
+
// The position, every few seconds of play (not on every tick).
|
|
242
|
+
if (Math.abs(seconds - lastSaved.current) >= 5) {
|
|
243
|
+
lastSaved.current = seconds;
|
|
244
|
+
const positions = readRecord(POSITIONS_KEY);
|
|
245
|
+
delete positions[entryId];
|
|
246
|
+
const next = { [entryId]: Math.floor(seconds), ...positions };
|
|
247
|
+
writeRecord(POSITIONS_KEY, Object.fromEntries(Object.entries(next).slice(0, MAX_POSITIONS)));
|
|
248
|
+
}
|
|
249
|
+
const article = articleRef.current;
|
|
250
|
+
if (article === null)
|
|
251
|
+
return;
|
|
252
|
+
let current;
|
|
253
|
+
for (const block of blockElements(article)) {
|
|
254
|
+
const t = blockTime(block);
|
|
255
|
+
if (t === undefined)
|
|
256
|
+
continue;
|
|
257
|
+
if (t > seconds + 0.25)
|
|
258
|
+
break;
|
|
259
|
+
current = block;
|
|
260
|
+
}
|
|
261
|
+
if (current === undefined || current === nowRef.current)
|
|
262
|
+
return;
|
|
263
|
+
nowRef.current?.removeAttribute('data-now');
|
|
264
|
+
current.setAttribute('data-now', '1');
|
|
265
|
+
nowRef.current = current;
|
|
266
|
+
if (!paused.current) {
|
|
267
|
+
ownScroll.current = true;
|
|
268
|
+
current.scrollIntoView({ block: 'center', behavior: 'smooth' });
|
|
269
|
+
setTimeout(() => { ownScroll.current = false; }, 1200);
|
|
270
|
+
}
|
|
271
|
+
}, [entryId, articleRef]);
|
|
272
|
+
const resumeFollow = useCallback(() => {
|
|
273
|
+
paused.current = false;
|
|
274
|
+
setFollowPaused(false);
|
|
275
|
+
const current = nowRef.current;
|
|
276
|
+
if (current === null)
|
|
277
|
+
return;
|
|
278
|
+
ownScroll.current = true;
|
|
279
|
+
current.scrollIntoView({ block: 'center', behavior: 'smooth' });
|
|
280
|
+
setTimeout(() => { ownScroll.current = false; }, 1200);
|
|
281
|
+
}, []);
|
|
282
|
+
return {
|
|
283
|
+
media,
|
|
284
|
+
transcript,
|
|
285
|
+
setTranscript,
|
|
286
|
+
hidden,
|
|
287
|
+
setHidden,
|
|
288
|
+
startAt,
|
|
289
|
+
playerRef,
|
|
290
|
+
onTime,
|
|
291
|
+
followPaused,
|
|
292
|
+
resumeFollow,
|
|
293
|
+
mini,
|
|
294
|
+
setMini,
|
|
295
|
+
hintOwed,
|
|
296
|
+
dismissHint,
|
|
297
|
+
seek,
|
|
298
|
+
};
|
|
299
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { MediaCardStates } from '../types.ts';
|
|
2
|
+
import type { ReaderPaneProps } from './contract.ts';
|
|
3
|
+
/**
|
|
4
|
+
* @param entryIds - the media cards on the wall.
|
|
5
|
+
* @param active - whether the wall is on screen.
|
|
6
|
+
* @param remote - the Remote verb.
|
|
7
|
+
*/
|
|
8
|
+
export declare function useMediaStates(entryIds: readonly string[], active: boolean, remote: Pick<ReaderPaneProps, 'mediaStates'>): MediaCardStates | undefined;
|
|
9
|
+
//# sourceMappingURL=useMediaStates.d.ts.map
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The wall's video / audio cards: what each one's transcript is (platform
|
|
3
|
+
* captions, a local transcript, transcribing, none), read from the host for
|
|
4
|
+
* the media cards on screen and re-read every few seconds while any of them
|
|
5
|
+
* is transcribing — the card's pill is how a reader sees a job they left run.
|
|
6
|
+
*
|
|
7
|
+
* @module @khorsheed/dsh-reader/client/useMediaStates
|
|
8
|
+
*/
|
|
9
|
+
import { useEffect, useRef, useState } from 'react';
|
|
10
|
+
/** How often a wall with a transcribing card re-reads (ms). */
|
|
11
|
+
const POLL_MS = 3000;
|
|
12
|
+
const LIVE = new Set(['fetching', 'queued', 'transcribing']);
|
|
13
|
+
/**
|
|
14
|
+
* @param entryIds - the media cards on the wall.
|
|
15
|
+
* @param active - whether the wall is on screen.
|
|
16
|
+
* @param remote - the Remote verb.
|
|
17
|
+
*/
|
|
18
|
+
export function useMediaStates(entryIds, active, remote) {
|
|
19
|
+
const [states, setStates] = useState(undefined);
|
|
20
|
+
const remoteRef = useRef(remote);
|
|
21
|
+
remoteRef.current = remote;
|
|
22
|
+
const key = entryIds.join(',');
|
|
23
|
+
const live = states !== undefined && Object.values(states.states).some(status => LIVE.has(status.state));
|
|
24
|
+
useEffect(() => {
|
|
25
|
+
if (!active || key === '')
|
|
26
|
+
return undefined;
|
|
27
|
+
let cancelled = false;
|
|
28
|
+
const read = () => {
|
|
29
|
+
void remoteRef.current.mediaStates({ entryIds: key.split(',') }).then(answer => {
|
|
30
|
+
if (!cancelled && answer.ok)
|
|
31
|
+
setStates(answer.value);
|
|
32
|
+
});
|
|
33
|
+
};
|
|
34
|
+
read();
|
|
35
|
+
const timer = live ? setInterval(read, POLL_MS) : undefined;
|
|
36
|
+
return () => {
|
|
37
|
+
cancelled = true;
|
|
38
|
+
if (timer !== undefined)
|
|
39
|
+
clearInterval(timer);
|
|
40
|
+
};
|
|
41
|
+
}, [key, active, live]);
|
|
42
|
+
return states;
|
|
43
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The detail view's notes, as one hook: loading an article's notes, handing
|
|
3
|
+
* the host its text, painting the highlights, the note sheet, and the
|
|
4
|
+
* gestures on notes.
|
|
5
|
+
*
|
|
6
|
+
* Kept out of `ReaderPane.tsx` on purpose: the pane already owns the reading
|
|
7
|
+
* position, the translation and the fetch lifecycle, and the notes only READ
|
|
8
|
+
* those (the article element, the segmentation, the view) — they never drive
|
|
9
|
+
* them.
|
|
10
|
+
*
|
|
11
|
+
* @module @khorsheed/dsh-reader/client/useNotes
|
|
12
|
+
*/
|
|
13
|
+
import { type MutableRefObject } from 'react';
|
|
14
|
+
import type { ReaderDiscussion, ReaderEntryNotes, ReaderNote } from '../notes-types.ts';
|
|
15
|
+
import type { ReaderPaneProps } from './contract.ts';
|
|
16
|
+
import type { ReaderEntry } from './parse-rss.ts';
|
|
17
|
+
import type { BuiltArticle, TranslationView } from './translate.ts';
|
|
18
|
+
import { type SyncCanvas, type SyncKind } from './canvas-sync.ts';
|
|
19
|
+
import { type NoteSelection } from './notes.ts';
|
|
20
|
+
/** Which face of the detail view is showing. */
|
|
21
|
+
export type DetailTab = 'article' | 'notes';
|
|
22
|
+
/** What is being synced to a canvas: some notes, or one discussion note. */
|
|
23
|
+
export type SyncTarget = {
|
|
24
|
+
readonly kind: 'notes';
|
|
25
|
+
readonly ids: readonly string[];
|
|
26
|
+
} | {
|
|
27
|
+
readonly kind: 'discussion';
|
|
28
|
+
readonly id: string;
|
|
29
|
+
};
|
|
30
|
+
/** The 「同步到画布」 sheet's state and gestures. */
|
|
31
|
+
export interface SyncFace {
|
|
32
|
+
/** Whether the canvas inbox is there at all (the gesture hides without it). */
|
|
33
|
+
readonly available: boolean;
|
|
34
|
+
readonly target: SyncTarget | null;
|
|
35
|
+
/** The canvases to pick from; null while loading. */
|
|
36
|
+
readonly canvases: readonly SyncCanvas[] | null;
|
|
37
|
+
readonly pick: string | null;
|
|
38
|
+
readonly last: string | null;
|
|
39
|
+
readonly withReplies: boolean;
|
|
40
|
+
/** Set when the picked canvas retired 灵感: the categories it takes. */
|
|
41
|
+
readonly kinds: readonly SyncKind[] | null;
|
|
42
|
+
readonly kind: string | null;
|
|
43
|
+
readonly busy: boolean;
|
|
44
|
+
readonly error: string | null;
|
|
45
|
+
/** The last sync's landing place, shown for a moment with an 打开. */
|
|
46
|
+
readonly done: {
|
|
47
|
+
readonly canvasId: string;
|
|
48
|
+
readonly cardId: string;
|
|
49
|
+
readonly title: string;
|
|
50
|
+
} | null;
|
|
51
|
+
readonly open: (target: SyncTarget) => void;
|
|
52
|
+
readonly setPick: (canvasId: string) => void;
|
|
53
|
+
readonly setWithReplies: (on: boolean) => void;
|
|
54
|
+
readonly setKind: (kind: string) => void;
|
|
55
|
+
readonly run: () => void;
|
|
56
|
+
readonly cancel: () => void;
|
|
57
|
+
readonly openDone: () => void;
|
|
58
|
+
}
|
|
59
|
+
/** What the hook reads from the pane. */
|
|
60
|
+
export interface UseNotesInput {
|
|
61
|
+
readonly props: ReaderPaneProps;
|
|
62
|
+
readonly entry: ReaderEntry | undefined;
|
|
63
|
+
readonly sourceLabel: string;
|
|
64
|
+
readonly articleHtml: string | null;
|
|
65
|
+
readonly articleRef: MutableRefObject<HTMLDivElement | null>;
|
|
66
|
+
readonly builtRef: MutableRefObject<BuiltArticle | null>;
|
|
67
|
+
readonly detailRef: MutableRefObject<HTMLDivElement | null>;
|
|
68
|
+
/** Anything whose change means the article's text nodes changed under us. */
|
|
69
|
+
readonly paintKey: unknown;
|
|
70
|
+
readonly translateView: TranslationView;
|
|
71
|
+
/** A transcript speaker's display name (with the names that make it, so a rename re-snapshots). */
|
|
72
|
+
readonly speakerLabel?: (speaker: number) => string;
|
|
73
|
+
}
|
|
74
|
+
/** What the pane renders from. */
|
|
75
|
+
export interface NotesFace {
|
|
76
|
+
readonly tab: DetailTab;
|
|
77
|
+
readonly setTab: (tab: DetailTab) => void;
|
|
78
|
+
readonly entry: ReaderEntryNotes | undefined;
|
|
79
|
+
readonly count: number;
|
|
80
|
+
readonly missing: ReadonlySet<string>;
|
|
81
|
+
readonly sheet: NoteSelection | null;
|
|
82
|
+
readonly saving: boolean;
|
|
83
|
+
readonly failed: boolean;
|
|
84
|
+
readonly hint: boolean;
|
|
85
|
+
readonly startNote: () => void;
|
|
86
|
+
readonly saveNote: (comment: string) => void;
|
|
87
|
+
readonly cancelSheet: () => void;
|
|
88
|
+
readonly jump: (note: ReaderNote) => void;
|
|
89
|
+
readonly edit: (note: ReaderNote, comment: string) => Promise<boolean>;
|
|
90
|
+
readonly remove: (note: ReaderNote) => void;
|
|
91
|
+
readonly discuss: (noteIds: readonly string[]) => void;
|
|
92
|
+
readonly canOpenSession: boolean;
|
|
93
|
+
readonly openSession: (sessionId: string) => void;
|
|
94
|
+
readonly sessionTitle: (sessionId: string) => string | undefined;
|
|
95
|
+
readonly addReply: (note: ReaderNote, text: string) => Promise<boolean>;
|
|
96
|
+
readonly editReply: (note: ReaderNote, index: number, text: string) => Promise<boolean>;
|
|
97
|
+
readonly deleteDiscussion: (discussion: ReaderDiscussion) => void;
|
|
98
|
+
readonly jumpPara: (para: number) => void;
|
|
99
|
+
readonly check: () => void;
|
|
100
|
+
/** Put a 润色 (re-translate) request into the draft: the whole article, or continuing at ¶`from`. */
|
|
101
|
+
readonly polish: (total: number, from?: number) => void;
|
|
102
|
+
readonly sync: SyncFace;
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* The notes of the open article, and every gesture on them.
|
|
106
|
+
*
|
|
107
|
+
* @param input - what the pane already holds.
|
|
108
|
+
*/
|
|
109
|
+
export declare function useNotes(input: UseNotesInput): NotesFace;
|
|
110
|
+
//# sourceMappingURL=useNotes.d.ts.map
|