forked from akai/readest
Support book notes synchronization
This commit is contained in:
@@ -13,10 +13,12 @@ import { Overlayer } from 'foliate-js/overlayer.js';
|
||||
import { useEnv } from '@/context/EnvContext';
|
||||
import { BookNote, HighlightColor, HighlightStyle } from '@/types/book';
|
||||
import { uniqueId } from '@/utils/misc';
|
||||
import { useBookDataStore } from '@/store/bookDataStore';
|
||||
import { useSettingsStore } from '@/store/settingsStore';
|
||||
import { useReaderStore } from '@/store/readerStore';
|
||||
import { useNotebookStore } from '@/store/notebookStore';
|
||||
import { useFoliateEvents } from '../../hooks/useFoliateEvents';
|
||||
import { useNotesSync } from '../../hooks/useNotesSync';
|
||||
import { getPopupPosition, getPosition, Position, TextSelection } from '@/utils/sel';
|
||||
import { eventDispatcher } from '@/utils/event';
|
||||
import Toast from '@/components/Toast';
|
||||
@@ -24,7 +26,6 @@ import AnnotationPopup from './AnnotationPopup';
|
||||
import WiktionaryPopup from './WiktionaryPopup';
|
||||
import WikipediaPopup from './WikipediaPopup';
|
||||
import DeepLPopup from './DeepLPopup';
|
||||
import { useBookDataStore } from '@/store/bookDataStore';
|
||||
|
||||
const Annotator: React.FC<{ bookKey: string }> = ({ bookKey }) => {
|
||||
const { envConfig } = useEnv();
|
||||
@@ -33,6 +34,9 @@ const Annotator: React.FC<{ bookKey: string }> = ({ bookKey }) => {
|
||||
const { getProgress, getView, getViewsById, getViewSettings } = useReaderStore();
|
||||
const { isNotebookPinned, isNotebookVisible } = useNotebookStore();
|
||||
const { setNotebookVisible, setNotebookNewAnnotation } = useNotebookStore();
|
||||
|
||||
useNotesSync(bookKey);
|
||||
|
||||
const config = getConfig(bookKey)!;
|
||||
const progress = getProgress(bookKey)!;
|
||||
const bookData = getBookData(bookKey)!;
|
||||
@@ -198,6 +202,7 @@ const Annotator: React.FC<{ bookKey: string }> = ({ bookKey }) => {
|
||||
const { booknotes = [] } = config;
|
||||
const annotations = booknotes.filter(
|
||||
(item) =>
|
||||
!item.deletedAt &&
|
||||
item.type === 'annotation' &&
|
||||
item.style &&
|
||||
CFI.compare(item.cfi, start) >= 0 &&
|
||||
@@ -274,7 +279,7 @@ const Annotator: React.FC<{ bookKey: string }> = ({ bookKey }) => {
|
||||
annotations[existingIndex] = annotation;
|
||||
views.forEach((view) => view?.addAnnotation(annotation));
|
||||
} else {
|
||||
annotations.splice(existingIndex, 1);
|
||||
annotations[existingIndex]!.deletedAt = Date.now();
|
||||
setShowAnnotPopup(false);
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { useAuth } from '@/context/AuthContext';
|
||||
import { useSync } from '@/hooks/useSync';
|
||||
import { useBookDataStore } from '@/store/bookDataStore';
|
||||
import { SYNC_NOTES_INTERVAL_SEC } from '@/services/constants';
|
||||
|
||||
export const useNotesSync = (bookKey: string) => {
|
||||
const { user } = useAuth();
|
||||
const { syncedNotes, syncNotes, lastSyncedAtNotes } = useSync(bookKey);
|
||||
const { getConfig, setConfig } = useBookDataStore();
|
||||
|
||||
const config = getConfig(bookKey);
|
||||
const bookHash = bookKey.split('-')[0]!;
|
||||
|
||||
useEffect(() => {
|
||||
if (!config || !user) return;
|
||||
syncNotes([], bookHash, 'pull');
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
const lastSyncTime = useRef<number>(0);
|
||||
const syncTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
useEffect(() => {
|
||||
if (!config || !user) return;
|
||||
const bookNotes = config.booknotes ?? [];
|
||||
const newNotes = bookNotes.filter(
|
||||
(note) => lastSyncedAtNotes < note.updatedAt || lastSyncedAtNotes < (note.deletedAt ?? 0),
|
||||
);
|
||||
newNotes.forEach((note) => {
|
||||
note.bookHash = bookHash;
|
||||
});
|
||||
const now = Date.now();
|
||||
const timeSinceLastSync = now - lastSyncTime.current;
|
||||
if (timeSinceLastSync > SYNC_NOTES_INTERVAL_SEC * 1000) {
|
||||
lastSyncTime.current = now;
|
||||
syncNotes(newNotes, bookHash, 'both');
|
||||
} else {
|
||||
if (syncTimeoutRef.current) clearTimeout(syncTimeoutRef.current);
|
||||
syncTimeoutRef.current = setTimeout(
|
||||
() => {
|
||||
lastSyncTime.current = Date.now();
|
||||
syncNotes(newNotes, bookHash, 'both');
|
||||
syncTimeoutRef.current = null;
|
||||
},
|
||||
SYNC_NOTES_INTERVAL_SEC * 1000 - timeSinceLastSync,
|
||||
);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [config]);
|
||||
|
||||
useEffect(() => {
|
||||
if (syncedNotes?.length && config) {
|
||||
const newNotes = syncedNotes.filter((note) => note.bookHash === bookHash);
|
||||
if (!newNotes.length) return;
|
||||
const oldNotes = config.booknotes ?? [];
|
||||
const mergedNotes = [
|
||||
...oldNotes.filter((oldNote) => !newNotes.some((newNote) => newNote.id === oldNote.id)),
|
||||
...newNotes,
|
||||
];
|
||||
setConfig(bookKey, { ...config, booknotes: mergedNotes });
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [syncedNotes]);
|
||||
};
|
||||
@@ -2,11 +2,11 @@ import { useEffect, useRef } from 'react';
|
||||
import { useAuth } from '@/context/AuthContext';
|
||||
import { useSync } from '@/hooks/useSync';
|
||||
import { BookConfig } from '@/types/book';
|
||||
import { deserializeConfig, serializeConfig } from '@/utils/serializer';
|
||||
import { useBookDataStore } from '@/store/bookDataStore';
|
||||
import { DEFAULT_BOOK_SEARCH_CONFIG, SYNC_PROGRESS_INTERVAL_SEC } from '@/services/constants';
|
||||
import { useReaderStore } from '@/store/readerStore';
|
||||
import { useSettingsStore } from '@/store/settingsStore';
|
||||
import { deserializeConfig, serializeConfig } from '@/utils/serializer';
|
||||
import { DEFAULT_BOOK_SEARCH_CONFIG, SYNC_PROGRESS_INTERVAL_SEC } from '@/services/constants';
|
||||
|
||||
export const useProgressSync = (
|
||||
bookKey: string,
|
||||
|
||||
@@ -76,6 +76,8 @@ export function useSync(bookKey?: string) {
|
||||
|
||||
try {
|
||||
const result = await syncClient.pullChanges(since, type, bookId);
|
||||
const records = result[type];
|
||||
if (!records.length) return;
|
||||
const maxTime = computeMaxTimestamp(result[type]);
|
||||
setLastSyncedAt(maxTime);
|
||||
setSyncResult(result);
|
||||
@@ -187,6 +189,9 @@ export function useSync(bookKey?: string) {
|
||||
syncedBooks,
|
||||
syncedConfigs,
|
||||
syncedNotes,
|
||||
lastSyncedAtBooks,
|
||||
lastSyncedAtNotes,
|
||||
lastSyncedAtConfigs,
|
||||
pullChanges,
|
||||
pushChanges,
|
||||
syncBooks,
|
||||
|
||||
@@ -151,6 +151,7 @@ export async function POST(req: NextRequest) {
|
||||
.insert(dbRec)
|
||||
.select()
|
||||
.single();
|
||||
console.log('Inserted record:', inserted);
|
||||
if (insertError) return { error: insertError.message };
|
||||
authoritativeRecords.push(inserted);
|
||||
} else {
|
||||
@@ -172,7 +173,7 @@ export async function POST(req: NextRequest) {
|
||||
.match(matchConditions)
|
||||
.select()
|
||||
.single();
|
||||
console.log('Updating record:', updated);
|
||||
console.log('Updated record:', updated);
|
||||
if (updateError) return { error: updateError.message };
|
||||
authoritativeRecords.push(updated);
|
||||
} else {
|
||||
|
||||
@@ -88,3 +88,4 @@ export const DOWNLOAD_READEST_URL = 'https://readest.com?utm_source=readest_web'
|
||||
export const READEST_WEB_BASE_URL = 'https://web.readest.com';
|
||||
|
||||
export const SYNC_PROGRESS_INTERVAL_SEC = 60;
|
||||
export const SYNC_NOTES_INTERVAL_SEC = 60;
|
||||
|
||||
Reference in New Issue
Block a user