diff --git a/apps/readest-app/src-tauri/tauri.conf.json b/apps/readest-app/src-tauri/tauri.conf.json index 0ac3dd7f..3a959bc8 100644 --- a/apps/readest-app/src-tauri/tauri.conf.json +++ b/apps/readest-app/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "../node_modules/@tauri-apps/cli/config.schema.json", "productName": "Readest", - "version": "0.1.0", + "version": "0.2.0", "identifier": "com.bilingify.digest", "build": { "frontendDist": "../out", diff --git a/apps/readest-app/src/app/globals.css b/apps/readest-app/src/app/globals.css index 0c1d04e2..1341221b 100644 --- a/apps/readest-app/src/app/globals.css +++ b/apps/readest-app/src/app/globals.css @@ -32,12 +32,3 @@ foliate-view { height: 100vh; border: none; } - -/* Hide scrollbar for Chrome, Safari and Opera */ -::-webkit-scrollbar { - display: none; -} -/* Hide scrollbar for IE and Edge */ -*{ - -ms-overflow-style: none; -} \ No newline at end of file diff --git a/apps/readest-app/src/app/reader/components/BookCard.tsx b/apps/readest-app/src/app/reader/components/BookCard.tsx index ef36b8fa..6e78a37e 100644 --- a/apps/readest-app/src/app/reader/components/BookCard.tsx +++ b/apps/readest-app/src/app/reader/components/BookCard.tsx @@ -10,10 +10,14 @@ interface BookCardProps { const BookCard: React.FC = ({ cover, title, author }) => { return ( -
- Book cover +
+ Book cover
-

{title}

+

{title}

{formatAuthors(author)}

-
+
-
+
+ {activeTab === 'toc' && ( + + )} + {activeTab === 'bookmarks' &&
Bookmarks
} + {activeTab === 'annotations' &&
Annotations
} +
+
-
- {activeTab === 'toc' &&
Table of Contents
} - {activeTab === 'bookmarks' &&
Bookmarks
} - {activeTab === 'annotations' &&
Annotations
} -
{ + for (const item of toc) { + if (item.href === href) { + return [item]; + } + if (item.subitems) { + const path = findParentPath(item.subitems, href); + if (path.length) { + return [item, ...path]; + } + } + } + return []; +}; + +const createExpanderIcon = (isExpanded: boolean) => { + return ( + + + + ); +}; + +const TOCItemView: React.FC<{ + bookId: string; + item: TOCItem; + depth: number; + setCurrentHref: (href: string) => void; + currentHref: string | null; + expandedItems: string[]; +}> = ({ bookId, item, depth, setCurrentHref, currentHref, expandedItems }) => { + const [isExpanded, setIsExpanded] = useState(expandedItems.includes(item.href || '')); + const { foliateView } = useReaderStore(); + + const handleToggleExpand = (event: React.MouseEvent) => { + event.preventDefault(); + event.stopPropagation(); + setIsExpanded((prev) => !prev); + }; + + const handleClickItem = (event: React.MouseEvent) => { + event.preventDefault(); + if (item.href) { + foliateView?.goTo(item.href); + setCurrentHref(item.href); + } + }; + + const isActive = currentHref === item.href; + + useEffect(() => { + setIsExpanded(expandedItems.includes(item.href || '')); + }, [expandedItems, item.href]); + + return ( +
  • + + {item.subitems && ( + + {createExpanderIcon(isExpanded)} + + )} + + {item.label} + + + {item.subitems && isExpanded && ( +
      + {item.subitems.map((subitem) => ( + + ))} +
    + )} +
  • + ); +}; + +const TOCView: React.FC<{ + bookId: string; + toc: TOCItem[]; + currentHref: string | null; +}> = ({ bookId, toc, currentHref: href }) => { + const [currentHref, setCurrentHref] = useState(href); + const [expandedItems, setExpandedItems] = useState([]); + const { foliateView } = useReaderStore(); + const tocRef = useRef(null); + + const tocRelocateHandler = (event: Event) => { + const detail = (event as CustomEvent).detail; + const { tocItem } = detail; + if (tocItem?.href) { + setCurrentHref(tocItem.href); + } + }; + + useFoliateEvents(foliateView, bookId, { onRelocate: tocRelocateHandler }); + + const expandParents = (toc: TOCItem[], href: string) => { + const parentPath = findParentPath(toc, href).map((item) => item.href); + setExpandedItems(parentPath.filter(Boolean) as string[]); + }; + + useEffect(() => { + const currentItem = tocRef.current?.querySelector(`[data-href="${currentHref}"]`); + if (currentItem) { + const rect = currentItem.getBoundingClientRect(); + const isVisible = rect.top >= 0 && rect.bottom <= window.innerHeight; + if (!isVisible) { + (currentItem as HTMLElement).scrollIntoView({ behavior: 'smooth', block: 'center' }); + } + (currentItem as HTMLElement).setAttribute('aria-current', 'page'); + } + if (currentHref) { + expandParents(toc, currentHref); + } + }, [toc, currentHref]); + + return ( +
    +
    +
      + {toc && + toc.map((item) => ( + + ))} +
    +
    +
    + ); +}; + +export default TOCView; diff --git a/apps/readest-app/src/app/reader/hooks/useFoliateEvents.ts b/apps/readest-app/src/app/reader/hooks/useFoliateEvents.ts index fef0330c..1ce3a251 100644 --- a/apps/readest-app/src/app/reader/hooks/useFoliateEvents.ts +++ b/apps/readest-app/src/app/reader/hooks/useFoliateEvents.ts @@ -15,13 +15,14 @@ export const useFoliateEvents = ( const setProgress = useReaderStore((state) => state.setProgress); const defaultLoadHandler = (event: Event) => { - console.log('load event:', event); + const detail = (event as CustomEvent).detail; + console.log('load event:', detail); }; const defaultRelocateHandler = (event: Event) => { const detail = (event as CustomEvent).detail; // console.log('relocate:', detail); - setProgress(bookId, detail.fraction, detail.cfi, detail.location); + setProgress(bookId, detail.fraction, detail.cfi, detail.tocItem?.href, detail.location); }; const onLoad = handlers?.onLoad || defaultLoadHandler; diff --git a/apps/readest-app/src/app/reader/page.tsx b/apps/readest-app/src/app/reader/page.tsx index e1307cd7..3376e460 100644 --- a/apps/readest-app/src/app/reader/page.tsx +++ b/apps/readest-app/src/app/reader/page.tsx @@ -25,7 +25,7 @@ const ReaderPage = () => { }; initLibrary(); - }, [envConfig, setLibrary]); + }, [envConfig, setSettings, setLibrary]); return ( settings.globalReadSettings && ( diff --git a/apps/readest-app/src/libs/document.ts b/apps/readest-app/src/libs/document.ts index 38ef515b..00e4d94f 100644 --- a/apps/readest-app/src/libs/document.ts +++ b/apps/readest-app/src/libs/document.ts @@ -32,6 +32,12 @@ Map.groupBy ??= (iterable, callbackfn) => { export type DocumentFile = File; +export interface TOCItem { + label: string; + href?: string; + subitems?: TOCItem[]; +} + export interface BookDoc { metadata: { title: string; @@ -39,6 +45,7 @@ export interface BookDoc { editor?: string; publisher?: string; }; + toc: Array; getCover(): Promise; } diff --git a/apps/readest-app/src/services/appService.ts b/apps/readest-app/src/services/appService.ts index 2656439e..8e79b1d8 100644 --- a/apps/readest-app/src/services/appService.ts +++ b/apps/readest-app/src/services/appService.ts @@ -142,7 +142,7 @@ export abstract class BaseAppService implements AppService { async loadBookContent(book: Book): Promise { const fp = getFilename(book); - let file = await new RemoteFile(this.fs.getURL(`${this.localBooksDir}/${fp}`), fp).open(); + const file = await new RemoteFile(this.fs.getURL(`${this.localBooksDir}/${fp}`), fp).open(); return { book, file, config: await this.loadBookConfig(book) }; } diff --git a/apps/readest-app/src/store/readerStore.ts b/apps/readest-app/src/store/readerStore.ts index a9aa08ea..6c10e143 100644 --- a/apps/readest-app/src/store/readerStore.ts +++ b/apps/readest-app/src/store/readerStore.ts @@ -3,6 +3,7 @@ import { create } from 'zustand'; import { BookNote, BookContent, Book, BookConfig, PageInfo } from '@/types/book'; import { EnvConfigType } from '@/services/environment'; import { SystemSettings } from '@/types/settings'; +import { FoliateView } from '@/app/reader/components/FoliateViewer'; interface BookState { loading?: boolean; @@ -10,28 +11,55 @@ interface BookState { book?: Book | null; file?: File | null; config?: BookConfig | null; - fraction?: number; } interface ReaderStore { library: Book[]; books: Record; settings: SystemSettings; + foliateView: FoliateView | null; setLibrary: (books: Book[]) => void; setSettings: (settings: SystemSettings) => void; + setProgress: ( + id: string, + progress: number, + location: string, + href: string, + pageinfo: PageInfo, + ) => void; + setFoliateView: (view: FoliateView | null) => void; + + saveConfig: (envConfig: EnvConfigType, book: Book, config: BookConfig) => void; + saveSettings: (envConfig: EnvConfigType, settings: SystemSettings) => void; + fetchBook: (envConfig: EnvConfigType, id: string) => Promise; - setProgress: (id: string, progress: number, location: string, pageinfo: PageInfo) => void; addBookmark: (id: string, bookmark: BookNote) => void; } -export const useReaderStore = create((set) => ({ +export const useReaderStore = create((set, get) => ({ library: [], books: {}, settings: {} as SystemSettings, + foliateView: null, + setFoliateView: (view) => set({ foliateView: view }), setLibrary: (books: Book[]) => set({ library: books }), setSettings: (settings: SystemSettings) => set({ settings }), + + saveConfig: async (envConfig: EnvConfigType, book: Book, config: BookConfig) => { + const appService = await envConfig.getAppService(); + const { library } = get(); + const bookIndex = library.findIndex((b) => b.hash === book.hash); + if (bookIndex !== -1) { + book.lastUpdated = Date.now(); + library[bookIndex] = book; + } + set({ library }); + config.lastUpdated = Date.now(); + appService.saveBookConfig(book, config); + appService.saveLibraryBooks(library); + }, saveSettings: async (envConfig: EnvConfigType, settings: SystemSettings) => { const appService = await envConfig.getAppService(); await appService.saveSettings(settings); @@ -74,7 +102,7 @@ export const useReaderStore = create((set) => ({ } }, - setProgress: (id: string, progress: number, location: string, pageinfo: PageInfo) => + setProgress: (id: string, progress: number, location: string, href: string, pageinfo: PageInfo) => set((state) => { const book = state.books[id]; if (!book) return state; @@ -86,6 +114,7 @@ export const useReaderStore = create((set) => ({ config: { ...book.config, lastUpdated: Date.now(), + href, progress, location, pageinfo, diff --git a/apps/readest-app/src/types/book.ts b/apps/readest-app/src/types/book.ts index c84a1a83..d46e84bd 100644 --- a/apps/readest-app/src/types/book.ts +++ b/apps/readest-app/src/types/book.ts @@ -33,6 +33,7 @@ export interface BookConfig { lastUpdated: number; progress?: number; location?: string; + href?: string; pageinfo?: PageInfo; bookmarks?: BookNote[]; annotations?: BookNote[]; diff --git a/apps/readest-app/src/utils/book.ts b/apps/readest-app/src/utils/book.ts index c2b57121..9d340454 100644 --- a/apps/readest-app/src/utils/book.ts +++ b/apps/readest-app/src/utils/book.ts @@ -32,6 +32,10 @@ interface LanguageMap { [key: string]: string; } +interface Contributor { + name: LanguageMap; +} + const formatLanguageMap = (x: string | LanguageMap): string => { if (!x) return ''; if (typeof x === 'string') return x; @@ -41,7 +45,7 @@ const formatLanguageMap = (x: string | LanguageMap): string => { const listFormat = new Intl.ListFormat('en', { style: 'long', type: 'conjunction' }); -const formatContributors = (contributors: any) => +export const formatAuthors = (contributors: string | Contributor | [string | Contributor]) => Array.isArray(contributors) ? listFormat.format( contributors.map((contributor) => @@ -51,5 +55,3 @@ const formatContributors = (contributors: any) => : typeof contributors === 'string' ? contributors : formatLanguageMap(contributors?.name); - -export const formatAuthors = (authors: any) => formatContributors(authors);