From 8cef74bb98c1850746cfc77da67b1bce93a57cea Mon Sep 17 00:00:00 2001 From: Youwes09 Date: Fri, 22 May 2026 04:04:59 -0500 Subject: [PATCH] Chore: Restructure Repository for SvelteKit --- .gitignore | 27 +- .npmrc | 1 + README.md | 183 +---- {src => _old}/App.svelte | 0 {src => _old}/api/client.ts | 0 {src => _old}/api/index.ts | 0 {src => _old}/api/mutations/chapters.ts | 0 {src => _old}/api/mutations/downloads.ts | 0 {src => _old}/api/mutations/extensions.ts | 0 {src => _old}/api/mutations/index.ts | 0 {src => _old}/api/mutations/manga.ts | 0 {src => _old}/api/mutations/mutations.md | 0 {src => _old}/api/mutations/tracking.ts | 0 {src => _old}/api/queries/chapters.ts | 0 {src => _old}/api/queries/downloads.ts | 0 {src => _old}/api/queries/extensions.ts | 0 {src => _old}/api/queries/index.ts | 0 {src => _old}/api/queries/manga.ts | 0 {src => _old}/api/queries/meta.ts | 0 {src => _old}/api/queries/queries.md | 0 {src => _old}/api/queries/tracking.ts | 0 {src => _old}/api/queries/updater.ts | 0 {src => _old}/core/actions/index.ts | 0 {src => _old}/core/actions/selectPortal.ts | 0 {src => _old}/core/algorithms/filter.ts | 0 {src => _old}/core/algorithms/index.ts | 0 {src => _old}/core/algorithms/paginate.ts | 0 {src => _old}/core/algorithms/queue.ts | 0 {src => _old}/core/algorithms/search.ts | 0 {src => _old}/core/algorithms/sort.ts | 0 {src => _old}/core/async/batchRequests.ts | 0 .../core/async/createPaginatedQuery.ts | 0 {src => _old}/core/async/fetchWithRetry.ts | 0 {src => _old}/core/async/index.ts | 0 {src => _old}/core/auth.ts | 0 {src => _old}/core/backup.ts | 0 {src => _old}/core/cache/imageCache.ts | 0 {src => _old}/core/cache/index.ts | 0 {src => _old}/core/cache/memoryCache.ts | 0 {src => _old}/core/cache/pageCache.ts | 0 {src => _old}/core/cache/queryCache.ts | 0 {src => _old}/core/cover/autoLink.ts | 0 {src => _old}/core/cover/autoLinkWorker.ts | 0 {src => _old}/core/cover/coverHash.ts | 0 {src => _old}/core/cover/coverResolver.ts | 0 {src => _old}/core/cover/index.ts | 0 {src => _old}/core/keybinds/defaultBinds.ts | 0 {src => _old}/core/keybinds/index.ts | 0 {src => _old}/core/keybinds/keybindEngine.ts | 0 .../core/persistence/credentialVault.ts | 0 {src => _old}/core/persistence/index.ts | 0 {src => _old}/core/persistence/persist.ts | 0 {src => _old}/core/theme.ts | 0 {src => _old}/core/ui/idle.ts | 0 {src => _old}/core/ui/index.ts | 0 {src => _old}/core/ui/touchscreen.ts | 0 {src => _old}/core/ui/zoom.ts | 0 {src => _old}/core/updater.ts | 0 {src => _old}/core/util.ts | 0 {src => _old}/design/base/animations.css | 0 {src => _old}/design/base/index.css | 0 {src => _old}/design/base/reset.css | 0 {src => _old}/design/base/scrollbars.css | 0 {src => _old}/design/base/typography.css | 0 {src => _old}/design/themes/dark.css | 0 {src => _old}/design/themes/index.css | 0 {src => _old}/design/themes/light.css | 0 {src => _old}/design/themes/midnight.css | 0 {src => _old}/design/themes/original.css | 0 {src => _old}/design/themes/warm.css | 0 {src => _old}/design/tokens/colors.css | 0 {src => _old}/design/tokens/index.css | 0 {src => _old}/design/tokens/motion.css | 0 {src => _old}/design/tokens/radius.css | 0 {src => _old}/design/tokens/shadows.css | 0 {src => _old}/design/tokens/spacing.css | 0 {src => _old}/design/tokens/typography.css | 0 {src => _old}/design/tokens/zindex.css | 0 {src => _old}/design/utilities/layout.css | 0 {src => _old}/design/utilities/text.css | 0 {src => _old}/design/utilities/visibility.css | 0 .../discover/components/GenreDrillPage.svelte | 0 .../discover/components/KeywordTab.svelte | 0 .../discover/components/Search.svelte | 0 .../discover/components/SourceTab.svelte | 0 .../discover/components/TagTab.svelte | 0 {src => _old}/features/discover/index.ts | 0 .../features/discover/lib/searchFilter.ts | 0 .../downloads/components/DownloadItem.svelte | 0 .../downloads/components/DownloadQueue.svelte | 0 .../downloads/components/Downloads.svelte | 0 {src => _old}/features/downloads/index.ts | 0 .../features/downloads/lib/autoRetry.ts | 0 .../features/downloads/lib/downloadQueue.ts | 0 .../downloads/store/downloadState.svelte.ts | 0 .../components/ExtensionCard.svelte | 0 .../components/ExtensionFilters.svelte | 0 .../components/ExtensionLibrary.svelte | 0 .../extensions/components/Extensions.svelte | 0 {src => _old}/features/extensions/index.ts | 0 .../extensions/lib/extensionHelpers.ts | 0 .../extensions/lib/extensionLibrary.ts | 0 .../panels/ExtensionSettingsPanel.svelte | 0 .../panels/SourceMigrateModal.svelte | 0 .../home/components/ActivityFeed.svelte | 0 .../home/components/ActivityHeatmap.svelte | 0 .../home/components/HeroSlotPicker.svelte | 0 .../features/home/components/HeroStage.svelte | 0 .../features/home/components/Home.svelte | 0 .../features/home/components/RecsRow.svelte | 0 .../features/home/components/StatsGrid.svelte | 0 .../settings => _old/features/home}/index.ts | 0 .../features/home/lib/homeHelpers.ts | 0 .../features/home/lib/recommendations.ts | 0 .../library/components/Library.svelte | 0 .../library/components/LibraryFilters.svelte | 0 .../library/components/LibraryGrid.svelte | 0 .../library/components/LibraryToolbar.svelte | 0 {src => _old}/features/library/index.ts | 0 .../features/library/lib/librarySort.ts | 0 .../features/library/lib/libraryUpdater.ts | 0 .../library/panels/BulkAutomationPanel.svelte | 0 .../library/store/libraryState.svelte.ts | 0 .../reader/components/PageView.svelte | 0 .../features/reader/components/Reader.svelte | 0 .../reader/components/ReaderControls.svelte | 0 .../reader/components/ReaderOverlay.svelte | 0 .../components/ReaderPresetPanel.svelte | 0 .../components/ReaderProgressBar.svelte | 0 {src => _old}/features/reader/index.ts | 0 .../features/reader/lib/chapterActions.ts | 0 .../features/reader/lib/chapterLoader.ts | 0 {src => _old}/features/reader/lib/index.ts | 0 .../features/reader/lib/navigation.ts | 0 .../features/reader/lib/pageLoader.ts | 0 .../features/reader/lib/pinchZoom.ts | 0 .../features/reader/lib/readerKeybinds.ts | 0 .../features/reader/lib/scrollHandler.ts | 0 .../features/reader/lib/zoomHelpers.ts | 0 .../reader/store/readerState.svelte.ts | 0 .../recent/components/HistoryPanel.svelte | 0 .../features/recent/components/Recent.svelte | 0 .../recent/components/UpdatesPanel.svelte | 0 .../series/components/ChapterList.svelte | 0 .../series/components/SeriesActions.svelte | 0 .../series/components/SeriesDetail.svelte | 0 .../series/components/SeriesHeader.svelte | 0 {src => _old}/features/series/index.ts | 0 .../features/series/lib/chapterList.ts | 0 .../features/series/lib/mangaPrefs.ts | 0 .../series/panels/AutomationPanel.svelte | 0 .../series/panels/CoverPickerPanel.svelte | 0 .../series/panels/MarkersPanel.svelte | 0 .../series/panels/MigrateModal.svelte | 0 .../series/panels/SeriesLinkPanel.svelte | 0 .../series/panels/TrackingPanel.svelte | 0 .../features/settings/components/Settings.css | 0 .../settings/components/Settings.svelte | 0 .../settings/components/ThemeEditor.svelte | 0 .../home => _old/features/settings}/index.ts | 0 .../settings/sections/AboutSettings.svelte | 0 .../sections/AppearanceSettings.svelte | 0 .../sections/AutomationSettings.svelte | 0 .../settings/sections/ContentSettings.svelte | 0 .../settings/sections/DevtoolsSettings.svelte | 0 .../settings/sections/FoldersSettings.svelte | 0 .../settings/sections/GeneralSettings.svelte | 0 .../settings/sections/KeybindsSettings.svelte | 0 .../settings/sections/LibrarySettings.svelte | 0 .../sections/PerformanceSettings.svelte | 0 .../settings/sections/ReaderSettings.svelte | 0 .../settings/sections/SecuritySettings.svelte | 0 .../settings/sections/StorageSettings.svelte | 0 .../settings/sections/TrackingSettings.svelte | 0 .../tracking/components/Tracking.svelte | 0 .../tracking/components/TrackingCard.svelte | 0 .../components/TrackingPreview.svelte | 0 .../components/TrackingToolbar.svelte | 0 {src => _old}/features/tracking/index.ts | 0 .../features/tracking/lib/trackingSync.ts | 0 .../tracking/store/trackingState.svelte.ts | 0 {src => _old}/main.ts | 0 {src => _old}/shared/chrome/AuthGate.svelte | 0 {src => _old}/shared/chrome/Layout.svelte | 0 {src => _old}/shared/chrome/Sidebar.svelte | 0 .../shared/chrome/SplashScreen.svelte | 0 {src => _old}/shared/chrome/TitleBar.svelte | 0 {src => _old}/shared/chrome/Toaster.svelte | 0 .../shared/manga/MangaPreview.svelte | 0 .../shared/manga/SourceBrowse.svelte | 0 {src => _old}/shared/manga/ThreeDCard.svelte | 0 {src => _old}/shared/manga/Thumbnail.svelte | 0 {src => _old}/shared/ui/ContextMenu.svelte | 0 {src => _old}/shared/ui/SourceList.svelte | 0 {src => _old}/store/app.svelte.ts | 0 {src => _old}/store/boot.svelte.ts | 0 {src => _old}/store/discord.ts | 0 {src => _old}/store/index.ts | 0 {src => _old}/store/notifications.svelte.ts | 0 {src => _old}/store/state.svelte.ts | 0 {src => _old}/types/api.ts | 0 {src => _old}/types/chapter.ts | 0 {src => _old}/types/extension.ts | 0 {src => _old}/types/history.ts | 0 {src => _old}/types/index.ts | 0 {src => _old}/types/manga.ts | 0 {src => _old}/types/settings.ts | 0 {src => _old}/types/tracking.ts | 0 package.json | 48 +- pnpm-lock.yaml | 742 ++++++++++++++---- pnpm-workspace.yaml | 2 + src/app.css | 340 ++++++++ src/app.d.ts | 4 + src/app.html | 12 + src/hooks.client.ts | 50 ++ src/lib/assets/favicon.svg | 22 + src/lib/core/algorithms/filter.ts | 5 + src/lib/core/algorithms/paginate.ts | 29 + src/lib/core/algorithms/queue.ts | 29 + src/lib/core/algorithms/search.ts | 33 + src/lib/core/algorithms/sort.ts | 32 + src/lib/core/cache/memoryCache.ts | 44 ++ src/lib/core/cache/queryCache.ts | 241 ++++++ src/lib/core/cover/autoLinkWorker.ts | 29 + src/lib/core/cover/coverHash.ts | 54 ++ src/lib/core/keybinds/defaultBinds.ts | 50 ++ src/lib/core/keybinds/keybindEngine.ts | 14 + src/lib/core/persistence/credentialVault.ts | 88 +++ src/lib/core/ui/touchscreen.ts | 234 ++++++ src/lib/core/ui/zoom.ts | 53 ++ src/lib/core/util.ts | 223 ++++++ src/lib/request-manager/chapters.ts | 40 + src/lib/request-manager/downloads.ts | 25 + src/lib/request-manager/extensions.ts | 51 ++ src/lib/request-manager/manga.ts | 58 ++ src/lib/request-manager/tracking.ts | 28 + src/lib/server-adapters/suwayomi/chapters.ts | 86 ++ src/lib/server-adapters/suwayomi/downloads.ts | 105 +++ .../server-adapters/suwayomi/extensions.ts | 191 +++++ src/lib/server-adapters/suwayomi/index.ts | 517 ++++++++++++ src/lib/server-adapters/suwayomi/manga.ts | 196 +++++ src/lib/server-adapters/suwayomi/types.ts | 67 ++ src/lib/server-adapters/types.ts | 94 +++ src/lib/state/app.svelte.ts | 10 + src/lib/state/downloads.svelte.ts | 16 + src/lib/state/extensions.svelte.ts | 36 + src/lib/state/library.svelte.ts | 53 ++ src/lib/state/notifications.svelte.ts | 25 + src/lib/state/reader.svelte.ts | 41 + src/lib/state/series.svelte.ts | 36 + src/lib/state/tracking.svelte.ts | 8 + src/lib/types/api.ts | 62 ++ src/lib/types/chapter.ts | 19 + src/lib/types/extension.ts | 24 + src/lib/types/history.ts | 72 ++ src/lib/types/index.ts | 539 +++++++++++++ src/lib/types/manga.ts | 62 ++ src/lib/types/settings.ts | 312 ++++++++ src/lib/types/tracking.ts | 37 + src/routes/+layout.svelte | 7 + src/routes/+layout.ts | 2 + src/routes/+page.svelte | 2 + static/robots.txt | 3 + svelte.config.js | 23 + tsconfig.json | 26 +- vite.config.ts | 27 +- 266 files changed, 5093 insertions(+), 396 deletions(-) create mode 100644 .npmrc rename {src => _old}/App.svelte (100%) rename {src => _old}/api/client.ts (100%) rename {src => _old}/api/index.ts (100%) rename {src => _old}/api/mutations/chapters.ts (100%) rename {src => _old}/api/mutations/downloads.ts (100%) rename {src => _old}/api/mutations/extensions.ts (100%) rename {src => _old}/api/mutations/index.ts (100%) rename {src => _old}/api/mutations/manga.ts (100%) rename {src => _old}/api/mutations/mutations.md (100%) rename {src => _old}/api/mutations/tracking.ts (100%) rename {src => _old}/api/queries/chapters.ts (100%) rename {src => _old}/api/queries/downloads.ts (100%) rename {src => _old}/api/queries/extensions.ts (100%) rename {src => _old}/api/queries/index.ts (100%) rename {src => _old}/api/queries/manga.ts (100%) rename {src => _old}/api/queries/meta.ts (100%) rename {src => _old}/api/queries/queries.md (100%) rename {src => _old}/api/queries/tracking.ts (100%) rename {src => _old}/api/queries/updater.ts (100%) rename {src => _old}/core/actions/index.ts (100%) rename {src => _old}/core/actions/selectPortal.ts (100%) rename {src => _old}/core/algorithms/filter.ts (100%) rename {src => _old}/core/algorithms/index.ts (100%) rename {src => _old}/core/algorithms/paginate.ts (100%) rename {src => _old}/core/algorithms/queue.ts (100%) rename {src => _old}/core/algorithms/search.ts (100%) rename {src => _old}/core/algorithms/sort.ts (100%) rename {src => _old}/core/async/batchRequests.ts (100%) rename {src => _old}/core/async/createPaginatedQuery.ts (100%) rename {src => _old}/core/async/fetchWithRetry.ts (100%) rename {src => _old}/core/async/index.ts (100%) rename {src => _old}/core/auth.ts (100%) rename {src => _old}/core/backup.ts (100%) rename {src => _old}/core/cache/imageCache.ts (100%) rename {src => _old}/core/cache/index.ts (100%) rename {src => _old}/core/cache/memoryCache.ts (100%) rename {src => _old}/core/cache/pageCache.ts (100%) rename {src => _old}/core/cache/queryCache.ts (100%) rename {src => _old}/core/cover/autoLink.ts (100%) rename {src => _old}/core/cover/autoLinkWorker.ts (100%) rename {src => _old}/core/cover/coverHash.ts (100%) rename {src => _old}/core/cover/coverResolver.ts (100%) rename {src => _old}/core/cover/index.ts (100%) rename {src => _old}/core/keybinds/defaultBinds.ts (100%) rename {src => _old}/core/keybinds/index.ts (100%) rename {src => _old}/core/keybinds/keybindEngine.ts (100%) rename {src => _old}/core/persistence/credentialVault.ts (100%) rename {src => _old}/core/persistence/index.ts (100%) rename {src => _old}/core/persistence/persist.ts (100%) rename {src => _old}/core/theme.ts (100%) rename {src => _old}/core/ui/idle.ts (100%) rename {src => _old}/core/ui/index.ts (100%) rename {src => _old}/core/ui/touchscreen.ts (100%) rename {src => _old}/core/ui/zoom.ts (100%) rename {src => _old}/core/updater.ts (100%) rename {src => _old}/core/util.ts (100%) rename {src => _old}/design/base/animations.css (100%) rename {src => _old}/design/base/index.css (100%) rename {src => _old}/design/base/reset.css (100%) rename {src => _old}/design/base/scrollbars.css (100%) rename {src => _old}/design/base/typography.css (100%) rename {src => _old}/design/themes/dark.css (100%) rename {src => _old}/design/themes/index.css (100%) rename {src => _old}/design/themes/light.css (100%) rename {src => _old}/design/themes/midnight.css (100%) rename {src => _old}/design/themes/original.css (100%) rename {src => _old}/design/themes/warm.css (100%) rename {src => _old}/design/tokens/colors.css (100%) rename {src => _old}/design/tokens/index.css (100%) rename {src => _old}/design/tokens/motion.css (100%) rename {src => _old}/design/tokens/radius.css (100%) rename {src => _old}/design/tokens/shadows.css (100%) rename {src => _old}/design/tokens/spacing.css (100%) rename {src => _old}/design/tokens/typography.css (100%) rename {src => _old}/design/tokens/zindex.css (100%) rename {src => _old}/design/utilities/layout.css (100%) rename {src => _old}/design/utilities/text.css (100%) rename {src => _old}/design/utilities/visibility.css (100%) rename {src => _old}/features/discover/components/GenreDrillPage.svelte (100%) rename {src => _old}/features/discover/components/KeywordTab.svelte (100%) rename {src => _old}/features/discover/components/Search.svelte (100%) rename {src => _old}/features/discover/components/SourceTab.svelte (100%) rename {src => _old}/features/discover/components/TagTab.svelte (100%) rename {src => _old}/features/discover/index.ts (100%) rename {src => _old}/features/discover/lib/searchFilter.ts (100%) rename {src => _old}/features/downloads/components/DownloadItem.svelte (100%) rename {src => _old}/features/downloads/components/DownloadQueue.svelte (100%) rename {src => _old}/features/downloads/components/Downloads.svelte (100%) rename {src => _old}/features/downloads/index.ts (100%) rename {src => _old}/features/downloads/lib/autoRetry.ts (100%) rename {src => _old}/features/downloads/lib/downloadQueue.ts (100%) rename {src => _old}/features/downloads/store/downloadState.svelte.ts (100%) rename {src => _old}/features/extensions/components/ExtensionCard.svelte (100%) rename {src => _old}/features/extensions/components/ExtensionFilters.svelte (100%) rename {src => _old}/features/extensions/components/ExtensionLibrary.svelte (100%) rename {src => _old}/features/extensions/components/Extensions.svelte (100%) rename {src => _old}/features/extensions/index.ts (100%) rename {src => _old}/features/extensions/lib/extensionHelpers.ts (100%) rename {src => _old}/features/extensions/lib/extensionLibrary.ts (100%) rename {src => _old}/features/extensions/panels/ExtensionSettingsPanel.svelte (100%) rename {src => _old}/features/extensions/panels/SourceMigrateModal.svelte (100%) rename {src => _old}/features/home/components/ActivityFeed.svelte (100%) rename {src => _old}/features/home/components/ActivityHeatmap.svelte (100%) rename {src => _old}/features/home/components/HeroSlotPicker.svelte (100%) rename {src => _old}/features/home/components/HeroStage.svelte (100%) rename {src => _old}/features/home/components/Home.svelte (100%) rename {src => _old}/features/home/components/RecsRow.svelte (100%) rename {src => _old}/features/home/components/StatsGrid.svelte (100%) rename {src/features/settings => _old/features/home}/index.ts (100%) rename {src => _old}/features/home/lib/homeHelpers.ts (100%) rename {src => _old}/features/home/lib/recommendations.ts (100%) rename {src => _old}/features/library/components/Library.svelte (100%) rename {src => _old}/features/library/components/LibraryFilters.svelte (100%) rename {src => _old}/features/library/components/LibraryGrid.svelte (100%) rename {src => _old}/features/library/components/LibraryToolbar.svelte (100%) rename {src => _old}/features/library/index.ts (100%) rename {src => _old}/features/library/lib/librarySort.ts (100%) rename {src => _old}/features/library/lib/libraryUpdater.ts (100%) rename {src => _old}/features/library/panels/BulkAutomationPanel.svelte (100%) rename {src => _old}/features/library/store/libraryState.svelte.ts (100%) rename {src => _old}/features/reader/components/PageView.svelte (100%) rename {src => _old}/features/reader/components/Reader.svelte (100%) rename {src => _old}/features/reader/components/ReaderControls.svelte (100%) rename {src => _old}/features/reader/components/ReaderOverlay.svelte (100%) rename {src => _old}/features/reader/components/ReaderPresetPanel.svelte (100%) rename {src => _old}/features/reader/components/ReaderProgressBar.svelte (100%) rename {src => _old}/features/reader/index.ts (100%) rename {src => _old}/features/reader/lib/chapterActions.ts (100%) rename {src => _old}/features/reader/lib/chapterLoader.ts (100%) rename {src => _old}/features/reader/lib/index.ts (100%) rename {src => _old}/features/reader/lib/navigation.ts (100%) rename {src => _old}/features/reader/lib/pageLoader.ts (100%) rename {src => _old}/features/reader/lib/pinchZoom.ts (100%) rename {src => _old}/features/reader/lib/readerKeybinds.ts (100%) rename {src => _old}/features/reader/lib/scrollHandler.ts (100%) rename {src => _old}/features/reader/lib/zoomHelpers.ts (100%) rename {src => _old}/features/reader/store/readerState.svelte.ts (100%) rename {src => _old}/features/recent/components/HistoryPanel.svelte (100%) rename {src => _old}/features/recent/components/Recent.svelte (100%) rename {src => _old}/features/recent/components/UpdatesPanel.svelte (100%) rename {src => _old}/features/series/components/ChapterList.svelte (100%) rename {src => _old}/features/series/components/SeriesActions.svelte (100%) rename {src => _old}/features/series/components/SeriesDetail.svelte (100%) rename {src => _old}/features/series/components/SeriesHeader.svelte (100%) rename {src => _old}/features/series/index.ts (100%) rename {src => _old}/features/series/lib/chapterList.ts (100%) rename {src => _old}/features/series/lib/mangaPrefs.ts (100%) rename {src => _old}/features/series/panels/AutomationPanel.svelte (100%) rename {src => _old}/features/series/panels/CoverPickerPanel.svelte (100%) rename {src => _old}/features/series/panels/MarkersPanel.svelte (100%) rename {src => _old}/features/series/panels/MigrateModal.svelte (100%) rename {src => _old}/features/series/panels/SeriesLinkPanel.svelte (100%) rename {src => _old}/features/series/panels/TrackingPanel.svelte (100%) rename {src => _old}/features/settings/components/Settings.css (100%) rename {src => _old}/features/settings/components/Settings.svelte (100%) rename {src => _old}/features/settings/components/ThemeEditor.svelte (100%) rename {src/features/home => _old/features/settings}/index.ts (100%) rename {src => _old}/features/settings/sections/AboutSettings.svelte (100%) rename {src => _old}/features/settings/sections/AppearanceSettings.svelte (100%) rename {src => _old}/features/settings/sections/AutomationSettings.svelte (100%) rename {src => _old}/features/settings/sections/ContentSettings.svelte (100%) rename {src => _old}/features/settings/sections/DevtoolsSettings.svelte (100%) rename {src => _old}/features/settings/sections/FoldersSettings.svelte (100%) rename {src => _old}/features/settings/sections/GeneralSettings.svelte (100%) rename {src => _old}/features/settings/sections/KeybindsSettings.svelte (100%) rename {src => _old}/features/settings/sections/LibrarySettings.svelte (100%) rename {src => _old}/features/settings/sections/PerformanceSettings.svelte (100%) rename {src => _old}/features/settings/sections/ReaderSettings.svelte (100%) rename {src => _old}/features/settings/sections/SecuritySettings.svelte (100%) rename {src => _old}/features/settings/sections/StorageSettings.svelte (100%) rename {src => _old}/features/settings/sections/TrackingSettings.svelte (100%) rename {src => _old}/features/tracking/components/Tracking.svelte (100%) rename {src => _old}/features/tracking/components/TrackingCard.svelte (100%) rename {src => _old}/features/tracking/components/TrackingPreview.svelte (100%) rename {src => _old}/features/tracking/components/TrackingToolbar.svelte (100%) rename {src => _old}/features/tracking/index.ts (100%) rename {src => _old}/features/tracking/lib/trackingSync.ts (100%) rename {src => _old}/features/tracking/store/trackingState.svelte.ts (100%) rename {src => _old}/main.ts (100%) rename {src => _old}/shared/chrome/AuthGate.svelte (100%) rename {src => _old}/shared/chrome/Layout.svelte (100%) rename {src => _old}/shared/chrome/Sidebar.svelte (100%) rename {src => _old}/shared/chrome/SplashScreen.svelte (100%) rename {src => _old}/shared/chrome/TitleBar.svelte (100%) rename {src => _old}/shared/chrome/Toaster.svelte (100%) rename {src => _old}/shared/manga/MangaPreview.svelte (100%) rename {src => _old}/shared/manga/SourceBrowse.svelte (100%) rename {src => _old}/shared/manga/ThreeDCard.svelte (100%) rename {src => _old}/shared/manga/Thumbnail.svelte (100%) rename {src => _old}/shared/ui/ContextMenu.svelte (100%) rename {src => _old}/shared/ui/SourceList.svelte (100%) rename {src => _old}/store/app.svelte.ts (100%) rename {src => _old}/store/boot.svelte.ts (100%) rename {src => _old}/store/discord.ts (100%) rename {src => _old}/store/index.ts (100%) rename {src => _old}/store/notifications.svelte.ts (100%) rename {src => _old}/store/state.svelte.ts (100%) rename {src => _old}/types/api.ts (100%) rename {src => _old}/types/chapter.ts (100%) rename {src => _old}/types/extension.ts (100%) rename {src => _old}/types/history.ts (100%) rename {src => _old}/types/index.ts (100%) rename {src => _old}/types/manga.ts (100%) rename {src => _old}/types/settings.ts (100%) rename {src => _old}/types/tracking.ts (100%) create mode 100644 pnpm-workspace.yaml create mode 100644 src/app.css create mode 100644 src/app.d.ts create mode 100644 src/app.html create mode 100644 src/hooks.client.ts create mode 100644 src/lib/assets/favicon.svg create mode 100644 src/lib/core/algorithms/filter.ts create mode 100644 src/lib/core/algorithms/paginate.ts create mode 100644 src/lib/core/algorithms/queue.ts create mode 100644 src/lib/core/algorithms/search.ts create mode 100644 src/lib/core/algorithms/sort.ts create mode 100644 src/lib/core/cache/memoryCache.ts create mode 100644 src/lib/core/cache/queryCache.ts create mode 100644 src/lib/core/cover/autoLinkWorker.ts create mode 100644 src/lib/core/cover/coverHash.ts create mode 100644 src/lib/core/keybinds/defaultBinds.ts create mode 100644 src/lib/core/keybinds/keybindEngine.ts create mode 100644 src/lib/core/persistence/credentialVault.ts create mode 100644 src/lib/core/ui/touchscreen.ts create mode 100644 src/lib/core/ui/zoom.ts create mode 100644 src/lib/core/util.ts create mode 100644 src/lib/request-manager/chapters.ts create mode 100644 src/lib/request-manager/downloads.ts create mode 100644 src/lib/request-manager/extensions.ts create mode 100644 src/lib/request-manager/manga.ts create mode 100644 src/lib/request-manager/tracking.ts create mode 100644 src/lib/server-adapters/suwayomi/chapters.ts create mode 100644 src/lib/server-adapters/suwayomi/downloads.ts create mode 100644 src/lib/server-adapters/suwayomi/extensions.ts create mode 100644 src/lib/server-adapters/suwayomi/index.ts create mode 100644 src/lib/server-adapters/suwayomi/manga.ts create mode 100644 src/lib/server-adapters/suwayomi/types.ts create mode 100644 src/lib/server-adapters/types.ts create mode 100644 src/lib/state/app.svelte.ts create mode 100644 src/lib/state/downloads.svelte.ts create mode 100644 src/lib/state/extensions.svelte.ts create mode 100644 src/lib/state/library.svelte.ts create mode 100644 src/lib/state/notifications.svelte.ts create mode 100644 src/lib/state/reader.svelte.ts create mode 100644 src/lib/state/series.svelte.ts create mode 100644 src/lib/state/tracking.svelte.ts create mode 100644 src/lib/types/api.ts create mode 100644 src/lib/types/chapter.ts create mode 100644 src/lib/types/extension.ts create mode 100644 src/lib/types/history.ts create mode 100644 src/lib/types/index.ts create mode 100644 src/lib/types/manga.ts create mode 100644 src/lib/types/settings.ts create mode 100644 src/lib/types/tracking.ts create mode 100644 src/routes/+layout.svelte create mode 100644 src/routes/+layout.ts create mode 100644 src/routes/+page.svelte create mode 100644 static/robots.txt create mode 100644 svelte.config.js diff --git a/.gitignore b/.gitignore index ca5398b..234f3cb 100644 --- a/.gitignore +++ b/.gitignore @@ -1,30 +1,33 @@ -# --- Build Artifacts --- node_modules/ suwayomi-raw/ suwayomi-windows.zip -suwayomi.zip dist/ dist-tauri/ target/ bin/ out/ - -# --- Nix --- .direnv/ result result-* -# --- Logs --- *.log npm-debug.log* yarn-debug.log* -yarn-error.log* .env +.env.* +!.env.example +!.env.test .env.local .env.*.local -# --- IDEs & OS --- +.output +.vercel +.netlify +.wrangler +/.svelte-kit +/build + .vscode/ .idea/ .DS_Store @@ -34,15 +37,19 @@ yarn-error.log* *.sln *.swp -# --- Tauri specific --- src-tauri/target/ src-tauri/binaries/ src-tauri/gen/ -# --- Flatpak build artifacts --- +.DS_Store +Thumbs.db + +vite.config.js.timestamp-* +vite.config.ts.timestamp-* + build-dir/ repo/ dist/ packaging/frontend-dist.tar.gz *.flatpak -.flatpak-builder/\n# --- Staged sidecar binaries (platform-specific, never commit) ---\nsrc-tauri/binaries/ +./flatpak-builder diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000..b6f27f1 --- /dev/null +++ b/.npmrc @@ -0,0 +1 @@ +engine-strict=true diff --git a/README.md b/README.md index a7a30f4..6be6c3f 100644 --- a/README.md +++ b/README.md @@ -1,173 +1,42 @@ -
- Moku -
+# sv -
+Everything you need to build a Svelte project, powered by [`sv`](https://github.com/sveltejs/cli). -[![Release](https://www.shieldcn.dev/github/release/moku-project/Moku.svg?variant=outline&size=default)](https://github.com/moku-project/Moku/releases/latest) -![GitHub Downloads](https://www.shieldcn.dev/github/downloads/moku-project/Moku.svg?variant=outline&size=default) -[![Stars](https://www.shieldcn.dev/github/stars/moku-project/Moku.svg?variant=outline&size=default)](https://github.com/moku-project/Moku) -[![Discord](https://www.shieldcn.dev/discord/members/x97hj8zR72.svg?variant=outline&size=default)](https://discord.gg/x97hj8zR72) +## Creating a project -
+If you're seeing this, you've probably already done this step. Congrats! -
- -Moku is a fast, minimal manga reader frontend for [Suwayomi-Server](https://github.com/Suwayomi/Suwayomi-Server). It wraps Suwayomi's GraphQL API in a lightweight Tauri app — no Electron overhead. - ---- - -## Screenshots - -
- Home -
- -
- Search - Tag Search - Settings - Preview - Downloads - Reader Settings -
- -
- View all screenshots → -
- ---- - -## Features - -- **Library management** — organize manga into folders, track unread counts, filter by genre -- **Per-folder sorting & filtering** — each folder has its own independent sort (unread, A–Z, recently read, latest chapter, and more) and publication status filter (Ongoing, Completed, Hiatus, etc.) -- **Built-in reader** — single page, long strip, configurable fit modes, customizable keybinds -- **Markers** — pin color-coded notes to any page while reading; markers appear as dots on the progress bar and are browseable under Series Detail → Manage → Markers -- **Extension support** — install and manage Suwayomi extensions directly from the app -- **Download management** — queue and monitor chapter downloads with progress toasts -- **Automation** — pre-download titles automatically and optionally delete chapters after they're marked as read (accessible from Series Detail) -- **Discord Rich Presence** — shows the manga title, current chapter, and an elapsed timer in your Discord status; configurable in Settings → General -- **Auto-start server** — optionally launch Suwayomi in the background on startup -- **Multiple themes** — Dark, Light, Midnight, Warm, High Contrast, and more -- **Auto-updates** — in-app update checker with silent background notifications -- **Improved NSFW filtering** — expanded tag parser gives the Hide NSFW setting better coverage across sources - ---- - -## Installation - -
- -![Runs on Windows](https://www.shieldcn.dev/badge/Runs%20on-Windows-0078D4.svg?logo=windows&logoColor=fff) -![Runs on Linux](https://www.shieldcn.dev/badge/Runs%20on-Linux-FCC624.svg?logo=linux&logoColor=000) -![Runs on MacOS](https://www.shieldcn.dev/badge/Runs%20on-MacOS-000000.svg?mode=light&logo=apple&logoColor=fff) - -
- -### Windows - -**winget:** - -```powershell -winget install Moku.Moku +```sh +# create a new project +npx sv create my-app ``` -> Thanks to [@frozenKelp](https://github.com/frozenKelp) for setting up and maintaining the winget package through v0.9.0. +To recreate this project with the same configuration: -Or download the `.exe` installer from the [releases page](https://github.com/moku-project/Moku/releases/latest). Suwayomi-Server and a JRE are bundled. - -### Linux (Flatpak, recommended) - -Suwayomi-Server and a bundled JRE are included — no separate install needed. - -```bash -flatpak install io.github.moku_app.Moku +```sh +# recreate this project +pnpm dlx sv@0.15.3 create --template minimal --types ts --install pnpm . ``` -Or download the latest `moku.flatpak` from the [releases page](https://github.com/moku-project/Moku/releases/latest) and install manually: +## Developing -```bash -flatpak install moku.flatpak +Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server: + +```sh +npm run dev + +# or start the server and open the app in a new browser tab +npm run dev -- --open ``` -### Nix +## Building -```bash -nix run github:moku-project/Moku +To create a production version of your app: + +```sh +npm run build ``` -Add to your flake: +You can preview the production build with `npm run preview`. -```nix -inputs.moku.url = "github:moku-project/Moku"; -``` - -### macOS - -Download the `.dmg` from the [releases page](https://github.com/moku-project/Moku/releases/latest). - -> **Note:** Builds are ad-hoc signed. On first launch you may need to run: -> ```bash -> xattr -rd com.apple.quarantine /Applications/Moku.app -> ``` - ---- - -## Requirements - -If you're not using the bundled Flatpak or Windows installer, [Suwayomi-Server](https://github.com/Suwayomi/Suwayomi-Server) must be running separately. By default Moku connects to `http://127.0.0.1:4567`. - -You can point Moku at any Suwayomi instance — local or remote — via **Settings → General → Server URL**. - ---- - -## Development - -**Prerequisites:** [Rust](https://rustup.rs), [Node.js](https://nodejs.org), [pnpm](https://pnpm.io), and [Tauri v2 prerequisites](https://tauri.app/start/prerequisites/). - -```bash -git clone https://github.com/moku-project/Moku -cd Moku -pnpm install -pnpm tauri:dev -``` - -Or with Nix: - -```bash -nix develop -pnpm install -pnpm tauri:dev -``` - ---- - -## Stack - -| | | -|---|---| -| [Tauri v2](https://tauri.app) | Native app shell | -| [Svelte 5](https://svelte.dev) + [TypeScript](https://www.typescriptlang.org) | UI | -| [Vite](https://vitejs.dev) | Frontend bundler | -| [Crane](https://github.com/ipetkov/crane) | Nix Rust builds | - ---- - -## Community - -Questions, feedback, or just want to hang out — join the Discord. - -[![Discord](https://www.shieldcn.dev/discord/members/x97hj8zR72.svg?variant=secondary&size=large)](https://discord.gg/x97hj8zR72) - ---- - -## License - -Distributed under the [Apache 2.0 License](./LICENSE). - ---- - -## Disclaimer - -Moku does not host or distribute any content. The developers have no affiliation with any content providers accessible through connected sources. +> To deploy your app, you may need to install an [adapter](https://svelte.dev/docs/kit/adapters) for your target environment. diff --git a/src/App.svelte b/_old/App.svelte similarity index 100% rename from src/App.svelte rename to _old/App.svelte diff --git a/src/api/client.ts b/_old/api/client.ts similarity index 100% rename from src/api/client.ts rename to _old/api/client.ts diff --git a/src/api/index.ts b/_old/api/index.ts similarity index 100% rename from src/api/index.ts rename to _old/api/index.ts diff --git a/src/api/mutations/chapters.ts b/_old/api/mutations/chapters.ts similarity index 100% rename from src/api/mutations/chapters.ts rename to _old/api/mutations/chapters.ts diff --git a/src/api/mutations/downloads.ts b/_old/api/mutations/downloads.ts similarity index 100% rename from src/api/mutations/downloads.ts rename to _old/api/mutations/downloads.ts diff --git a/src/api/mutations/extensions.ts b/_old/api/mutations/extensions.ts similarity index 100% rename from src/api/mutations/extensions.ts rename to _old/api/mutations/extensions.ts diff --git a/src/api/mutations/index.ts b/_old/api/mutations/index.ts similarity index 100% rename from src/api/mutations/index.ts rename to _old/api/mutations/index.ts diff --git a/src/api/mutations/manga.ts b/_old/api/mutations/manga.ts similarity index 100% rename from src/api/mutations/manga.ts rename to _old/api/mutations/manga.ts diff --git a/src/api/mutations/mutations.md b/_old/api/mutations/mutations.md similarity index 100% rename from src/api/mutations/mutations.md rename to _old/api/mutations/mutations.md diff --git a/src/api/mutations/tracking.ts b/_old/api/mutations/tracking.ts similarity index 100% rename from src/api/mutations/tracking.ts rename to _old/api/mutations/tracking.ts diff --git a/src/api/queries/chapters.ts b/_old/api/queries/chapters.ts similarity index 100% rename from src/api/queries/chapters.ts rename to _old/api/queries/chapters.ts diff --git a/src/api/queries/downloads.ts b/_old/api/queries/downloads.ts similarity index 100% rename from src/api/queries/downloads.ts rename to _old/api/queries/downloads.ts diff --git a/src/api/queries/extensions.ts b/_old/api/queries/extensions.ts similarity index 100% rename from src/api/queries/extensions.ts rename to _old/api/queries/extensions.ts diff --git a/src/api/queries/index.ts b/_old/api/queries/index.ts similarity index 100% rename from src/api/queries/index.ts rename to _old/api/queries/index.ts diff --git a/src/api/queries/manga.ts b/_old/api/queries/manga.ts similarity index 100% rename from src/api/queries/manga.ts rename to _old/api/queries/manga.ts diff --git a/src/api/queries/meta.ts b/_old/api/queries/meta.ts similarity index 100% rename from src/api/queries/meta.ts rename to _old/api/queries/meta.ts diff --git a/src/api/queries/queries.md b/_old/api/queries/queries.md similarity index 100% rename from src/api/queries/queries.md rename to _old/api/queries/queries.md diff --git a/src/api/queries/tracking.ts b/_old/api/queries/tracking.ts similarity index 100% rename from src/api/queries/tracking.ts rename to _old/api/queries/tracking.ts diff --git a/src/api/queries/updater.ts b/_old/api/queries/updater.ts similarity index 100% rename from src/api/queries/updater.ts rename to _old/api/queries/updater.ts diff --git a/src/core/actions/index.ts b/_old/core/actions/index.ts similarity index 100% rename from src/core/actions/index.ts rename to _old/core/actions/index.ts diff --git a/src/core/actions/selectPortal.ts b/_old/core/actions/selectPortal.ts similarity index 100% rename from src/core/actions/selectPortal.ts rename to _old/core/actions/selectPortal.ts diff --git a/src/core/algorithms/filter.ts b/_old/core/algorithms/filter.ts similarity index 100% rename from src/core/algorithms/filter.ts rename to _old/core/algorithms/filter.ts diff --git a/src/core/algorithms/index.ts b/_old/core/algorithms/index.ts similarity index 100% rename from src/core/algorithms/index.ts rename to _old/core/algorithms/index.ts diff --git a/src/core/algorithms/paginate.ts b/_old/core/algorithms/paginate.ts similarity index 100% rename from src/core/algorithms/paginate.ts rename to _old/core/algorithms/paginate.ts diff --git a/src/core/algorithms/queue.ts b/_old/core/algorithms/queue.ts similarity index 100% rename from src/core/algorithms/queue.ts rename to _old/core/algorithms/queue.ts diff --git a/src/core/algorithms/search.ts b/_old/core/algorithms/search.ts similarity index 100% rename from src/core/algorithms/search.ts rename to _old/core/algorithms/search.ts diff --git a/src/core/algorithms/sort.ts b/_old/core/algorithms/sort.ts similarity index 100% rename from src/core/algorithms/sort.ts rename to _old/core/algorithms/sort.ts diff --git a/src/core/async/batchRequests.ts b/_old/core/async/batchRequests.ts similarity index 100% rename from src/core/async/batchRequests.ts rename to _old/core/async/batchRequests.ts diff --git a/src/core/async/createPaginatedQuery.ts b/_old/core/async/createPaginatedQuery.ts similarity index 100% rename from src/core/async/createPaginatedQuery.ts rename to _old/core/async/createPaginatedQuery.ts diff --git a/src/core/async/fetchWithRetry.ts b/_old/core/async/fetchWithRetry.ts similarity index 100% rename from src/core/async/fetchWithRetry.ts rename to _old/core/async/fetchWithRetry.ts diff --git a/src/core/async/index.ts b/_old/core/async/index.ts similarity index 100% rename from src/core/async/index.ts rename to _old/core/async/index.ts diff --git a/src/core/auth.ts b/_old/core/auth.ts similarity index 100% rename from src/core/auth.ts rename to _old/core/auth.ts diff --git a/src/core/backup.ts b/_old/core/backup.ts similarity index 100% rename from src/core/backup.ts rename to _old/core/backup.ts diff --git a/src/core/cache/imageCache.ts b/_old/core/cache/imageCache.ts similarity index 100% rename from src/core/cache/imageCache.ts rename to _old/core/cache/imageCache.ts diff --git a/src/core/cache/index.ts b/_old/core/cache/index.ts similarity index 100% rename from src/core/cache/index.ts rename to _old/core/cache/index.ts diff --git a/src/core/cache/memoryCache.ts b/_old/core/cache/memoryCache.ts similarity index 100% rename from src/core/cache/memoryCache.ts rename to _old/core/cache/memoryCache.ts diff --git a/src/core/cache/pageCache.ts b/_old/core/cache/pageCache.ts similarity index 100% rename from src/core/cache/pageCache.ts rename to _old/core/cache/pageCache.ts diff --git a/src/core/cache/queryCache.ts b/_old/core/cache/queryCache.ts similarity index 100% rename from src/core/cache/queryCache.ts rename to _old/core/cache/queryCache.ts diff --git a/src/core/cover/autoLink.ts b/_old/core/cover/autoLink.ts similarity index 100% rename from src/core/cover/autoLink.ts rename to _old/core/cover/autoLink.ts diff --git a/src/core/cover/autoLinkWorker.ts b/_old/core/cover/autoLinkWorker.ts similarity index 100% rename from src/core/cover/autoLinkWorker.ts rename to _old/core/cover/autoLinkWorker.ts diff --git a/src/core/cover/coverHash.ts b/_old/core/cover/coverHash.ts similarity index 100% rename from src/core/cover/coverHash.ts rename to _old/core/cover/coverHash.ts diff --git a/src/core/cover/coverResolver.ts b/_old/core/cover/coverResolver.ts similarity index 100% rename from src/core/cover/coverResolver.ts rename to _old/core/cover/coverResolver.ts diff --git a/src/core/cover/index.ts b/_old/core/cover/index.ts similarity index 100% rename from src/core/cover/index.ts rename to _old/core/cover/index.ts diff --git a/src/core/keybinds/defaultBinds.ts b/_old/core/keybinds/defaultBinds.ts similarity index 100% rename from src/core/keybinds/defaultBinds.ts rename to _old/core/keybinds/defaultBinds.ts diff --git a/src/core/keybinds/index.ts b/_old/core/keybinds/index.ts similarity index 100% rename from src/core/keybinds/index.ts rename to _old/core/keybinds/index.ts diff --git a/src/core/keybinds/keybindEngine.ts b/_old/core/keybinds/keybindEngine.ts similarity index 100% rename from src/core/keybinds/keybindEngine.ts rename to _old/core/keybinds/keybindEngine.ts diff --git a/src/core/persistence/credentialVault.ts b/_old/core/persistence/credentialVault.ts similarity index 100% rename from src/core/persistence/credentialVault.ts rename to _old/core/persistence/credentialVault.ts diff --git a/src/core/persistence/index.ts b/_old/core/persistence/index.ts similarity index 100% rename from src/core/persistence/index.ts rename to _old/core/persistence/index.ts diff --git a/src/core/persistence/persist.ts b/_old/core/persistence/persist.ts similarity index 100% rename from src/core/persistence/persist.ts rename to _old/core/persistence/persist.ts diff --git a/src/core/theme.ts b/_old/core/theme.ts similarity index 100% rename from src/core/theme.ts rename to _old/core/theme.ts diff --git a/src/core/ui/idle.ts b/_old/core/ui/idle.ts similarity index 100% rename from src/core/ui/idle.ts rename to _old/core/ui/idle.ts diff --git a/src/core/ui/index.ts b/_old/core/ui/index.ts similarity index 100% rename from src/core/ui/index.ts rename to _old/core/ui/index.ts diff --git a/src/core/ui/touchscreen.ts b/_old/core/ui/touchscreen.ts similarity index 100% rename from src/core/ui/touchscreen.ts rename to _old/core/ui/touchscreen.ts diff --git a/src/core/ui/zoom.ts b/_old/core/ui/zoom.ts similarity index 100% rename from src/core/ui/zoom.ts rename to _old/core/ui/zoom.ts diff --git a/src/core/updater.ts b/_old/core/updater.ts similarity index 100% rename from src/core/updater.ts rename to _old/core/updater.ts diff --git a/src/core/util.ts b/_old/core/util.ts similarity index 100% rename from src/core/util.ts rename to _old/core/util.ts diff --git a/src/design/base/animations.css b/_old/design/base/animations.css similarity index 100% rename from src/design/base/animations.css rename to _old/design/base/animations.css diff --git a/src/design/base/index.css b/_old/design/base/index.css similarity index 100% rename from src/design/base/index.css rename to _old/design/base/index.css diff --git a/src/design/base/reset.css b/_old/design/base/reset.css similarity index 100% rename from src/design/base/reset.css rename to _old/design/base/reset.css diff --git a/src/design/base/scrollbars.css b/_old/design/base/scrollbars.css similarity index 100% rename from src/design/base/scrollbars.css rename to _old/design/base/scrollbars.css diff --git a/src/design/base/typography.css b/_old/design/base/typography.css similarity index 100% rename from src/design/base/typography.css rename to _old/design/base/typography.css diff --git a/src/design/themes/dark.css b/_old/design/themes/dark.css similarity index 100% rename from src/design/themes/dark.css rename to _old/design/themes/dark.css diff --git a/src/design/themes/index.css b/_old/design/themes/index.css similarity index 100% rename from src/design/themes/index.css rename to _old/design/themes/index.css diff --git a/src/design/themes/light.css b/_old/design/themes/light.css similarity index 100% rename from src/design/themes/light.css rename to _old/design/themes/light.css diff --git a/src/design/themes/midnight.css b/_old/design/themes/midnight.css similarity index 100% rename from src/design/themes/midnight.css rename to _old/design/themes/midnight.css diff --git a/src/design/themes/original.css b/_old/design/themes/original.css similarity index 100% rename from src/design/themes/original.css rename to _old/design/themes/original.css diff --git a/src/design/themes/warm.css b/_old/design/themes/warm.css similarity index 100% rename from src/design/themes/warm.css rename to _old/design/themes/warm.css diff --git a/src/design/tokens/colors.css b/_old/design/tokens/colors.css similarity index 100% rename from src/design/tokens/colors.css rename to _old/design/tokens/colors.css diff --git a/src/design/tokens/index.css b/_old/design/tokens/index.css similarity index 100% rename from src/design/tokens/index.css rename to _old/design/tokens/index.css diff --git a/src/design/tokens/motion.css b/_old/design/tokens/motion.css similarity index 100% rename from src/design/tokens/motion.css rename to _old/design/tokens/motion.css diff --git a/src/design/tokens/radius.css b/_old/design/tokens/radius.css similarity index 100% rename from src/design/tokens/radius.css rename to _old/design/tokens/radius.css diff --git a/src/design/tokens/shadows.css b/_old/design/tokens/shadows.css similarity index 100% rename from src/design/tokens/shadows.css rename to _old/design/tokens/shadows.css diff --git a/src/design/tokens/spacing.css b/_old/design/tokens/spacing.css similarity index 100% rename from src/design/tokens/spacing.css rename to _old/design/tokens/spacing.css diff --git a/src/design/tokens/typography.css b/_old/design/tokens/typography.css similarity index 100% rename from src/design/tokens/typography.css rename to _old/design/tokens/typography.css diff --git a/src/design/tokens/zindex.css b/_old/design/tokens/zindex.css similarity index 100% rename from src/design/tokens/zindex.css rename to _old/design/tokens/zindex.css diff --git a/src/design/utilities/layout.css b/_old/design/utilities/layout.css similarity index 100% rename from src/design/utilities/layout.css rename to _old/design/utilities/layout.css diff --git a/src/design/utilities/text.css b/_old/design/utilities/text.css similarity index 100% rename from src/design/utilities/text.css rename to _old/design/utilities/text.css diff --git a/src/design/utilities/visibility.css b/_old/design/utilities/visibility.css similarity index 100% rename from src/design/utilities/visibility.css rename to _old/design/utilities/visibility.css diff --git a/src/features/discover/components/GenreDrillPage.svelte b/_old/features/discover/components/GenreDrillPage.svelte similarity index 100% rename from src/features/discover/components/GenreDrillPage.svelte rename to _old/features/discover/components/GenreDrillPage.svelte diff --git a/src/features/discover/components/KeywordTab.svelte b/_old/features/discover/components/KeywordTab.svelte similarity index 100% rename from src/features/discover/components/KeywordTab.svelte rename to _old/features/discover/components/KeywordTab.svelte diff --git a/src/features/discover/components/Search.svelte b/_old/features/discover/components/Search.svelte similarity index 100% rename from src/features/discover/components/Search.svelte rename to _old/features/discover/components/Search.svelte diff --git a/src/features/discover/components/SourceTab.svelte b/_old/features/discover/components/SourceTab.svelte similarity index 100% rename from src/features/discover/components/SourceTab.svelte rename to _old/features/discover/components/SourceTab.svelte diff --git a/src/features/discover/components/TagTab.svelte b/_old/features/discover/components/TagTab.svelte similarity index 100% rename from src/features/discover/components/TagTab.svelte rename to _old/features/discover/components/TagTab.svelte diff --git a/src/features/discover/index.ts b/_old/features/discover/index.ts similarity index 100% rename from src/features/discover/index.ts rename to _old/features/discover/index.ts diff --git a/src/features/discover/lib/searchFilter.ts b/_old/features/discover/lib/searchFilter.ts similarity index 100% rename from src/features/discover/lib/searchFilter.ts rename to _old/features/discover/lib/searchFilter.ts diff --git a/src/features/downloads/components/DownloadItem.svelte b/_old/features/downloads/components/DownloadItem.svelte similarity index 100% rename from src/features/downloads/components/DownloadItem.svelte rename to _old/features/downloads/components/DownloadItem.svelte diff --git a/src/features/downloads/components/DownloadQueue.svelte b/_old/features/downloads/components/DownloadQueue.svelte similarity index 100% rename from src/features/downloads/components/DownloadQueue.svelte rename to _old/features/downloads/components/DownloadQueue.svelte diff --git a/src/features/downloads/components/Downloads.svelte b/_old/features/downloads/components/Downloads.svelte similarity index 100% rename from src/features/downloads/components/Downloads.svelte rename to _old/features/downloads/components/Downloads.svelte diff --git a/src/features/downloads/index.ts b/_old/features/downloads/index.ts similarity index 100% rename from src/features/downloads/index.ts rename to _old/features/downloads/index.ts diff --git a/src/features/downloads/lib/autoRetry.ts b/_old/features/downloads/lib/autoRetry.ts similarity index 100% rename from src/features/downloads/lib/autoRetry.ts rename to _old/features/downloads/lib/autoRetry.ts diff --git a/src/features/downloads/lib/downloadQueue.ts b/_old/features/downloads/lib/downloadQueue.ts similarity index 100% rename from src/features/downloads/lib/downloadQueue.ts rename to _old/features/downloads/lib/downloadQueue.ts diff --git a/src/features/downloads/store/downloadState.svelte.ts b/_old/features/downloads/store/downloadState.svelte.ts similarity index 100% rename from src/features/downloads/store/downloadState.svelte.ts rename to _old/features/downloads/store/downloadState.svelte.ts diff --git a/src/features/extensions/components/ExtensionCard.svelte b/_old/features/extensions/components/ExtensionCard.svelte similarity index 100% rename from src/features/extensions/components/ExtensionCard.svelte rename to _old/features/extensions/components/ExtensionCard.svelte diff --git a/src/features/extensions/components/ExtensionFilters.svelte b/_old/features/extensions/components/ExtensionFilters.svelte similarity index 100% rename from src/features/extensions/components/ExtensionFilters.svelte rename to _old/features/extensions/components/ExtensionFilters.svelte diff --git a/src/features/extensions/components/ExtensionLibrary.svelte b/_old/features/extensions/components/ExtensionLibrary.svelte similarity index 100% rename from src/features/extensions/components/ExtensionLibrary.svelte rename to _old/features/extensions/components/ExtensionLibrary.svelte diff --git a/src/features/extensions/components/Extensions.svelte b/_old/features/extensions/components/Extensions.svelte similarity index 100% rename from src/features/extensions/components/Extensions.svelte rename to _old/features/extensions/components/Extensions.svelte diff --git a/src/features/extensions/index.ts b/_old/features/extensions/index.ts similarity index 100% rename from src/features/extensions/index.ts rename to _old/features/extensions/index.ts diff --git a/src/features/extensions/lib/extensionHelpers.ts b/_old/features/extensions/lib/extensionHelpers.ts similarity index 100% rename from src/features/extensions/lib/extensionHelpers.ts rename to _old/features/extensions/lib/extensionHelpers.ts diff --git a/src/features/extensions/lib/extensionLibrary.ts b/_old/features/extensions/lib/extensionLibrary.ts similarity index 100% rename from src/features/extensions/lib/extensionLibrary.ts rename to _old/features/extensions/lib/extensionLibrary.ts diff --git a/src/features/extensions/panels/ExtensionSettingsPanel.svelte b/_old/features/extensions/panels/ExtensionSettingsPanel.svelte similarity index 100% rename from src/features/extensions/panels/ExtensionSettingsPanel.svelte rename to _old/features/extensions/panels/ExtensionSettingsPanel.svelte diff --git a/src/features/extensions/panels/SourceMigrateModal.svelte b/_old/features/extensions/panels/SourceMigrateModal.svelte similarity index 100% rename from src/features/extensions/panels/SourceMigrateModal.svelte rename to _old/features/extensions/panels/SourceMigrateModal.svelte diff --git a/src/features/home/components/ActivityFeed.svelte b/_old/features/home/components/ActivityFeed.svelte similarity index 100% rename from src/features/home/components/ActivityFeed.svelte rename to _old/features/home/components/ActivityFeed.svelte diff --git a/src/features/home/components/ActivityHeatmap.svelte b/_old/features/home/components/ActivityHeatmap.svelte similarity index 100% rename from src/features/home/components/ActivityHeatmap.svelte rename to _old/features/home/components/ActivityHeatmap.svelte diff --git a/src/features/home/components/HeroSlotPicker.svelte b/_old/features/home/components/HeroSlotPicker.svelte similarity index 100% rename from src/features/home/components/HeroSlotPicker.svelte rename to _old/features/home/components/HeroSlotPicker.svelte diff --git a/src/features/home/components/HeroStage.svelte b/_old/features/home/components/HeroStage.svelte similarity index 100% rename from src/features/home/components/HeroStage.svelte rename to _old/features/home/components/HeroStage.svelte diff --git a/src/features/home/components/Home.svelte b/_old/features/home/components/Home.svelte similarity index 100% rename from src/features/home/components/Home.svelte rename to _old/features/home/components/Home.svelte diff --git a/src/features/home/components/RecsRow.svelte b/_old/features/home/components/RecsRow.svelte similarity index 100% rename from src/features/home/components/RecsRow.svelte rename to _old/features/home/components/RecsRow.svelte diff --git a/src/features/home/components/StatsGrid.svelte b/_old/features/home/components/StatsGrid.svelte similarity index 100% rename from src/features/home/components/StatsGrid.svelte rename to _old/features/home/components/StatsGrid.svelte diff --git a/src/features/settings/index.ts b/_old/features/home/index.ts similarity index 100% rename from src/features/settings/index.ts rename to _old/features/home/index.ts diff --git a/src/features/home/lib/homeHelpers.ts b/_old/features/home/lib/homeHelpers.ts similarity index 100% rename from src/features/home/lib/homeHelpers.ts rename to _old/features/home/lib/homeHelpers.ts diff --git a/src/features/home/lib/recommendations.ts b/_old/features/home/lib/recommendations.ts similarity index 100% rename from src/features/home/lib/recommendations.ts rename to _old/features/home/lib/recommendations.ts diff --git a/src/features/library/components/Library.svelte b/_old/features/library/components/Library.svelte similarity index 100% rename from src/features/library/components/Library.svelte rename to _old/features/library/components/Library.svelte diff --git a/src/features/library/components/LibraryFilters.svelte b/_old/features/library/components/LibraryFilters.svelte similarity index 100% rename from src/features/library/components/LibraryFilters.svelte rename to _old/features/library/components/LibraryFilters.svelte diff --git a/src/features/library/components/LibraryGrid.svelte b/_old/features/library/components/LibraryGrid.svelte similarity index 100% rename from src/features/library/components/LibraryGrid.svelte rename to _old/features/library/components/LibraryGrid.svelte diff --git a/src/features/library/components/LibraryToolbar.svelte b/_old/features/library/components/LibraryToolbar.svelte similarity index 100% rename from src/features/library/components/LibraryToolbar.svelte rename to _old/features/library/components/LibraryToolbar.svelte diff --git a/src/features/library/index.ts b/_old/features/library/index.ts similarity index 100% rename from src/features/library/index.ts rename to _old/features/library/index.ts diff --git a/src/features/library/lib/librarySort.ts b/_old/features/library/lib/librarySort.ts similarity index 100% rename from src/features/library/lib/librarySort.ts rename to _old/features/library/lib/librarySort.ts diff --git a/src/features/library/lib/libraryUpdater.ts b/_old/features/library/lib/libraryUpdater.ts similarity index 100% rename from src/features/library/lib/libraryUpdater.ts rename to _old/features/library/lib/libraryUpdater.ts diff --git a/src/features/library/panels/BulkAutomationPanel.svelte b/_old/features/library/panels/BulkAutomationPanel.svelte similarity index 100% rename from src/features/library/panels/BulkAutomationPanel.svelte rename to _old/features/library/panels/BulkAutomationPanel.svelte diff --git a/src/features/library/store/libraryState.svelte.ts b/_old/features/library/store/libraryState.svelte.ts similarity index 100% rename from src/features/library/store/libraryState.svelte.ts rename to _old/features/library/store/libraryState.svelte.ts diff --git a/src/features/reader/components/PageView.svelte b/_old/features/reader/components/PageView.svelte similarity index 100% rename from src/features/reader/components/PageView.svelte rename to _old/features/reader/components/PageView.svelte diff --git a/src/features/reader/components/Reader.svelte b/_old/features/reader/components/Reader.svelte similarity index 100% rename from src/features/reader/components/Reader.svelte rename to _old/features/reader/components/Reader.svelte diff --git a/src/features/reader/components/ReaderControls.svelte b/_old/features/reader/components/ReaderControls.svelte similarity index 100% rename from src/features/reader/components/ReaderControls.svelte rename to _old/features/reader/components/ReaderControls.svelte diff --git a/src/features/reader/components/ReaderOverlay.svelte b/_old/features/reader/components/ReaderOverlay.svelte similarity index 100% rename from src/features/reader/components/ReaderOverlay.svelte rename to _old/features/reader/components/ReaderOverlay.svelte diff --git a/src/features/reader/components/ReaderPresetPanel.svelte b/_old/features/reader/components/ReaderPresetPanel.svelte similarity index 100% rename from src/features/reader/components/ReaderPresetPanel.svelte rename to _old/features/reader/components/ReaderPresetPanel.svelte diff --git a/src/features/reader/components/ReaderProgressBar.svelte b/_old/features/reader/components/ReaderProgressBar.svelte similarity index 100% rename from src/features/reader/components/ReaderProgressBar.svelte rename to _old/features/reader/components/ReaderProgressBar.svelte diff --git a/src/features/reader/index.ts b/_old/features/reader/index.ts similarity index 100% rename from src/features/reader/index.ts rename to _old/features/reader/index.ts diff --git a/src/features/reader/lib/chapterActions.ts b/_old/features/reader/lib/chapterActions.ts similarity index 100% rename from src/features/reader/lib/chapterActions.ts rename to _old/features/reader/lib/chapterActions.ts diff --git a/src/features/reader/lib/chapterLoader.ts b/_old/features/reader/lib/chapterLoader.ts similarity index 100% rename from src/features/reader/lib/chapterLoader.ts rename to _old/features/reader/lib/chapterLoader.ts diff --git a/src/features/reader/lib/index.ts b/_old/features/reader/lib/index.ts similarity index 100% rename from src/features/reader/lib/index.ts rename to _old/features/reader/lib/index.ts diff --git a/src/features/reader/lib/navigation.ts b/_old/features/reader/lib/navigation.ts similarity index 100% rename from src/features/reader/lib/navigation.ts rename to _old/features/reader/lib/navigation.ts diff --git a/src/features/reader/lib/pageLoader.ts b/_old/features/reader/lib/pageLoader.ts similarity index 100% rename from src/features/reader/lib/pageLoader.ts rename to _old/features/reader/lib/pageLoader.ts diff --git a/src/features/reader/lib/pinchZoom.ts b/_old/features/reader/lib/pinchZoom.ts similarity index 100% rename from src/features/reader/lib/pinchZoom.ts rename to _old/features/reader/lib/pinchZoom.ts diff --git a/src/features/reader/lib/readerKeybinds.ts b/_old/features/reader/lib/readerKeybinds.ts similarity index 100% rename from src/features/reader/lib/readerKeybinds.ts rename to _old/features/reader/lib/readerKeybinds.ts diff --git a/src/features/reader/lib/scrollHandler.ts b/_old/features/reader/lib/scrollHandler.ts similarity index 100% rename from src/features/reader/lib/scrollHandler.ts rename to _old/features/reader/lib/scrollHandler.ts diff --git a/src/features/reader/lib/zoomHelpers.ts b/_old/features/reader/lib/zoomHelpers.ts similarity index 100% rename from src/features/reader/lib/zoomHelpers.ts rename to _old/features/reader/lib/zoomHelpers.ts diff --git a/src/features/reader/store/readerState.svelte.ts b/_old/features/reader/store/readerState.svelte.ts similarity index 100% rename from src/features/reader/store/readerState.svelte.ts rename to _old/features/reader/store/readerState.svelte.ts diff --git a/src/features/recent/components/HistoryPanel.svelte b/_old/features/recent/components/HistoryPanel.svelte similarity index 100% rename from src/features/recent/components/HistoryPanel.svelte rename to _old/features/recent/components/HistoryPanel.svelte diff --git a/src/features/recent/components/Recent.svelte b/_old/features/recent/components/Recent.svelte similarity index 100% rename from src/features/recent/components/Recent.svelte rename to _old/features/recent/components/Recent.svelte diff --git a/src/features/recent/components/UpdatesPanel.svelte b/_old/features/recent/components/UpdatesPanel.svelte similarity index 100% rename from src/features/recent/components/UpdatesPanel.svelte rename to _old/features/recent/components/UpdatesPanel.svelte diff --git a/src/features/series/components/ChapterList.svelte b/_old/features/series/components/ChapterList.svelte similarity index 100% rename from src/features/series/components/ChapterList.svelte rename to _old/features/series/components/ChapterList.svelte diff --git a/src/features/series/components/SeriesActions.svelte b/_old/features/series/components/SeriesActions.svelte similarity index 100% rename from src/features/series/components/SeriesActions.svelte rename to _old/features/series/components/SeriesActions.svelte diff --git a/src/features/series/components/SeriesDetail.svelte b/_old/features/series/components/SeriesDetail.svelte similarity index 100% rename from src/features/series/components/SeriesDetail.svelte rename to _old/features/series/components/SeriesDetail.svelte diff --git a/src/features/series/components/SeriesHeader.svelte b/_old/features/series/components/SeriesHeader.svelte similarity index 100% rename from src/features/series/components/SeriesHeader.svelte rename to _old/features/series/components/SeriesHeader.svelte diff --git a/src/features/series/index.ts b/_old/features/series/index.ts similarity index 100% rename from src/features/series/index.ts rename to _old/features/series/index.ts diff --git a/src/features/series/lib/chapterList.ts b/_old/features/series/lib/chapterList.ts similarity index 100% rename from src/features/series/lib/chapterList.ts rename to _old/features/series/lib/chapterList.ts diff --git a/src/features/series/lib/mangaPrefs.ts b/_old/features/series/lib/mangaPrefs.ts similarity index 100% rename from src/features/series/lib/mangaPrefs.ts rename to _old/features/series/lib/mangaPrefs.ts diff --git a/src/features/series/panels/AutomationPanel.svelte b/_old/features/series/panels/AutomationPanel.svelte similarity index 100% rename from src/features/series/panels/AutomationPanel.svelte rename to _old/features/series/panels/AutomationPanel.svelte diff --git a/src/features/series/panels/CoverPickerPanel.svelte b/_old/features/series/panels/CoverPickerPanel.svelte similarity index 100% rename from src/features/series/panels/CoverPickerPanel.svelte rename to _old/features/series/panels/CoverPickerPanel.svelte diff --git a/src/features/series/panels/MarkersPanel.svelte b/_old/features/series/panels/MarkersPanel.svelte similarity index 100% rename from src/features/series/panels/MarkersPanel.svelte rename to _old/features/series/panels/MarkersPanel.svelte diff --git a/src/features/series/panels/MigrateModal.svelte b/_old/features/series/panels/MigrateModal.svelte similarity index 100% rename from src/features/series/panels/MigrateModal.svelte rename to _old/features/series/panels/MigrateModal.svelte diff --git a/src/features/series/panels/SeriesLinkPanel.svelte b/_old/features/series/panels/SeriesLinkPanel.svelte similarity index 100% rename from src/features/series/panels/SeriesLinkPanel.svelte rename to _old/features/series/panels/SeriesLinkPanel.svelte diff --git a/src/features/series/panels/TrackingPanel.svelte b/_old/features/series/panels/TrackingPanel.svelte similarity index 100% rename from src/features/series/panels/TrackingPanel.svelte rename to _old/features/series/panels/TrackingPanel.svelte diff --git a/src/features/settings/components/Settings.css b/_old/features/settings/components/Settings.css similarity index 100% rename from src/features/settings/components/Settings.css rename to _old/features/settings/components/Settings.css diff --git a/src/features/settings/components/Settings.svelte b/_old/features/settings/components/Settings.svelte similarity index 100% rename from src/features/settings/components/Settings.svelte rename to _old/features/settings/components/Settings.svelte diff --git a/src/features/settings/components/ThemeEditor.svelte b/_old/features/settings/components/ThemeEditor.svelte similarity index 100% rename from src/features/settings/components/ThemeEditor.svelte rename to _old/features/settings/components/ThemeEditor.svelte diff --git a/src/features/home/index.ts b/_old/features/settings/index.ts similarity index 100% rename from src/features/home/index.ts rename to _old/features/settings/index.ts diff --git a/src/features/settings/sections/AboutSettings.svelte b/_old/features/settings/sections/AboutSettings.svelte similarity index 100% rename from src/features/settings/sections/AboutSettings.svelte rename to _old/features/settings/sections/AboutSettings.svelte diff --git a/src/features/settings/sections/AppearanceSettings.svelte b/_old/features/settings/sections/AppearanceSettings.svelte similarity index 100% rename from src/features/settings/sections/AppearanceSettings.svelte rename to _old/features/settings/sections/AppearanceSettings.svelte diff --git a/src/features/settings/sections/AutomationSettings.svelte b/_old/features/settings/sections/AutomationSettings.svelte similarity index 100% rename from src/features/settings/sections/AutomationSettings.svelte rename to _old/features/settings/sections/AutomationSettings.svelte diff --git a/src/features/settings/sections/ContentSettings.svelte b/_old/features/settings/sections/ContentSettings.svelte similarity index 100% rename from src/features/settings/sections/ContentSettings.svelte rename to _old/features/settings/sections/ContentSettings.svelte diff --git a/src/features/settings/sections/DevtoolsSettings.svelte b/_old/features/settings/sections/DevtoolsSettings.svelte similarity index 100% rename from src/features/settings/sections/DevtoolsSettings.svelte rename to _old/features/settings/sections/DevtoolsSettings.svelte diff --git a/src/features/settings/sections/FoldersSettings.svelte b/_old/features/settings/sections/FoldersSettings.svelte similarity index 100% rename from src/features/settings/sections/FoldersSettings.svelte rename to _old/features/settings/sections/FoldersSettings.svelte diff --git a/src/features/settings/sections/GeneralSettings.svelte b/_old/features/settings/sections/GeneralSettings.svelte similarity index 100% rename from src/features/settings/sections/GeneralSettings.svelte rename to _old/features/settings/sections/GeneralSettings.svelte diff --git a/src/features/settings/sections/KeybindsSettings.svelte b/_old/features/settings/sections/KeybindsSettings.svelte similarity index 100% rename from src/features/settings/sections/KeybindsSettings.svelte rename to _old/features/settings/sections/KeybindsSettings.svelte diff --git a/src/features/settings/sections/LibrarySettings.svelte b/_old/features/settings/sections/LibrarySettings.svelte similarity index 100% rename from src/features/settings/sections/LibrarySettings.svelte rename to _old/features/settings/sections/LibrarySettings.svelte diff --git a/src/features/settings/sections/PerformanceSettings.svelte b/_old/features/settings/sections/PerformanceSettings.svelte similarity index 100% rename from src/features/settings/sections/PerformanceSettings.svelte rename to _old/features/settings/sections/PerformanceSettings.svelte diff --git a/src/features/settings/sections/ReaderSettings.svelte b/_old/features/settings/sections/ReaderSettings.svelte similarity index 100% rename from src/features/settings/sections/ReaderSettings.svelte rename to _old/features/settings/sections/ReaderSettings.svelte diff --git a/src/features/settings/sections/SecuritySettings.svelte b/_old/features/settings/sections/SecuritySettings.svelte similarity index 100% rename from src/features/settings/sections/SecuritySettings.svelte rename to _old/features/settings/sections/SecuritySettings.svelte diff --git a/src/features/settings/sections/StorageSettings.svelte b/_old/features/settings/sections/StorageSettings.svelte similarity index 100% rename from src/features/settings/sections/StorageSettings.svelte rename to _old/features/settings/sections/StorageSettings.svelte diff --git a/src/features/settings/sections/TrackingSettings.svelte b/_old/features/settings/sections/TrackingSettings.svelte similarity index 100% rename from src/features/settings/sections/TrackingSettings.svelte rename to _old/features/settings/sections/TrackingSettings.svelte diff --git a/src/features/tracking/components/Tracking.svelte b/_old/features/tracking/components/Tracking.svelte similarity index 100% rename from src/features/tracking/components/Tracking.svelte rename to _old/features/tracking/components/Tracking.svelte diff --git a/src/features/tracking/components/TrackingCard.svelte b/_old/features/tracking/components/TrackingCard.svelte similarity index 100% rename from src/features/tracking/components/TrackingCard.svelte rename to _old/features/tracking/components/TrackingCard.svelte diff --git a/src/features/tracking/components/TrackingPreview.svelte b/_old/features/tracking/components/TrackingPreview.svelte similarity index 100% rename from src/features/tracking/components/TrackingPreview.svelte rename to _old/features/tracking/components/TrackingPreview.svelte diff --git a/src/features/tracking/components/TrackingToolbar.svelte b/_old/features/tracking/components/TrackingToolbar.svelte similarity index 100% rename from src/features/tracking/components/TrackingToolbar.svelte rename to _old/features/tracking/components/TrackingToolbar.svelte diff --git a/src/features/tracking/index.ts b/_old/features/tracking/index.ts similarity index 100% rename from src/features/tracking/index.ts rename to _old/features/tracking/index.ts diff --git a/src/features/tracking/lib/trackingSync.ts b/_old/features/tracking/lib/trackingSync.ts similarity index 100% rename from src/features/tracking/lib/trackingSync.ts rename to _old/features/tracking/lib/trackingSync.ts diff --git a/src/features/tracking/store/trackingState.svelte.ts b/_old/features/tracking/store/trackingState.svelte.ts similarity index 100% rename from src/features/tracking/store/trackingState.svelte.ts rename to _old/features/tracking/store/trackingState.svelte.ts diff --git a/src/main.ts b/_old/main.ts similarity index 100% rename from src/main.ts rename to _old/main.ts diff --git a/src/shared/chrome/AuthGate.svelte b/_old/shared/chrome/AuthGate.svelte similarity index 100% rename from src/shared/chrome/AuthGate.svelte rename to _old/shared/chrome/AuthGate.svelte diff --git a/src/shared/chrome/Layout.svelte b/_old/shared/chrome/Layout.svelte similarity index 100% rename from src/shared/chrome/Layout.svelte rename to _old/shared/chrome/Layout.svelte diff --git a/src/shared/chrome/Sidebar.svelte b/_old/shared/chrome/Sidebar.svelte similarity index 100% rename from src/shared/chrome/Sidebar.svelte rename to _old/shared/chrome/Sidebar.svelte diff --git a/src/shared/chrome/SplashScreen.svelte b/_old/shared/chrome/SplashScreen.svelte similarity index 100% rename from src/shared/chrome/SplashScreen.svelte rename to _old/shared/chrome/SplashScreen.svelte diff --git a/src/shared/chrome/TitleBar.svelte b/_old/shared/chrome/TitleBar.svelte similarity index 100% rename from src/shared/chrome/TitleBar.svelte rename to _old/shared/chrome/TitleBar.svelte diff --git a/src/shared/chrome/Toaster.svelte b/_old/shared/chrome/Toaster.svelte similarity index 100% rename from src/shared/chrome/Toaster.svelte rename to _old/shared/chrome/Toaster.svelte diff --git a/src/shared/manga/MangaPreview.svelte b/_old/shared/manga/MangaPreview.svelte similarity index 100% rename from src/shared/manga/MangaPreview.svelte rename to _old/shared/manga/MangaPreview.svelte diff --git a/src/shared/manga/SourceBrowse.svelte b/_old/shared/manga/SourceBrowse.svelte similarity index 100% rename from src/shared/manga/SourceBrowse.svelte rename to _old/shared/manga/SourceBrowse.svelte diff --git a/src/shared/manga/ThreeDCard.svelte b/_old/shared/manga/ThreeDCard.svelte similarity index 100% rename from src/shared/manga/ThreeDCard.svelte rename to _old/shared/manga/ThreeDCard.svelte diff --git a/src/shared/manga/Thumbnail.svelte b/_old/shared/manga/Thumbnail.svelte similarity index 100% rename from src/shared/manga/Thumbnail.svelte rename to _old/shared/manga/Thumbnail.svelte diff --git a/src/shared/ui/ContextMenu.svelte b/_old/shared/ui/ContextMenu.svelte similarity index 100% rename from src/shared/ui/ContextMenu.svelte rename to _old/shared/ui/ContextMenu.svelte diff --git a/src/shared/ui/SourceList.svelte b/_old/shared/ui/SourceList.svelte similarity index 100% rename from src/shared/ui/SourceList.svelte rename to _old/shared/ui/SourceList.svelte diff --git a/src/store/app.svelte.ts b/_old/store/app.svelte.ts similarity index 100% rename from src/store/app.svelte.ts rename to _old/store/app.svelte.ts diff --git a/src/store/boot.svelte.ts b/_old/store/boot.svelte.ts similarity index 100% rename from src/store/boot.svelte.ts rename to _old/store/boot.svelte.ts diff --git a/src/store/discord.ts b/_old/store/discord.ts similarity index 100% rename from src/store/discord.ts rename to _old/store/discord.ts diff --git a/src/store/index.ts b/_old/store/index.ts similarity index 100% rename from src/store/index.ts rename to _old/store/index.ts diff --git a/src/store/notifications.svelte.ts b/_old/store/notifications.svelte.ts similarity index 100% rename from src/store/notifications.svelte.ts rename to _old/store/notifications.svelte.ts diff --git a/src/store/state.svelte.ts b/_old/store/state.svelte.ts similarity index 100% rename from src/store/state.svelte.ts rename to _old/store/state.svelte.ts diff --git a/src/types/api.ts b/_old/types/api.ts similarity index 100% rename from src/types/api.ts rename to _old/types/api.ts diff --git a/src/types/chapter.ts b/_old/types/chapter.ts similarity index 100% rename from src/types/chapter.ts rename to _old/types/chapter.ts diff --git a/src/types/extension.ts b/_old/types/extension.ts similarity index 100% rename from src/types/extension.ts rename to _old/types/extension.ts diff --git a/src/types/history.ts b/_old/types/history.ts similarity index 100% rename from src/types/history.ts rename to _old/types/history.ts diff --git a/src/types/index.ts b/_old/types/index.ts similarity index 100% rename from src/types/index.ts rename to _old/types/index.ts diff --git a/src/types/manga.ts b/_old/types/manga.ts similarity index 100% rename from src/types/manga.ts rename to _old/types/manga.ts diff --git a/src/types/settings.ts b/_old/types/settings.ts similarity index 100% rename from src/types/settings.ts rename to _old/types/settings.ts diff --git a/src/types/tracking.ts b/_old/types/tracking.ts similarity index 100% rename from src/types/tracking.ts rename to _old/types/tracking.ts diff --git a/package.json b/package.json index d41ed24..1b80845 100644 --- a/package.json +++ b/package.json @@ -1,33 +1,33 @@ { "name": "moku", - "version": "0.5.0", + "private": true, + "version": "0.9.4", "type": "module", "scripts": { - "dev": "vite", - "build": "vite build", + "dev": "vite dev", "preview": "vite preview", - "tauri": "tauri", - "tauri:dev": "tauri dev --config src-tauri/tauri.dev.conf.json" - }, - "dependencies": { - "@tauri-apps/api": "^2.11.0", - "@tauri-apps/plugin-http": "^2.5.8", - "@tauri-apps/plugin-os": "^2.3.2", - "@tauri-apps/plugin-process": "^2.3.1", - "@tauri-apps/plugin-shell": "^2.3.5", - "@tauri-apps/plugin-store": "~2.4.2", - "clsx": "^2.1.1", - "phosphor-svelte": "^3.1.0", - "svelte-spa-router": "^5.1.0", - "tauri-plugin-discord-rpc-api": "github:Youwes09/tauri-plugin-discord-rpc", - "tauri-plugin-drpc": "^1.0.3" + "prepare": "svelte-kit sync || echo ''", + "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", + "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", + "build:static": "MOKU_TARGET=static vite build", + "build:node": "MOKU_TARGET=node vite build", + "build:tauri": "MOKU_TARGET=static vite build", + "build:android": "MOKU_TARGET=static vite build", + "tauri:dev": "tauri dev", + "tauri:build": "tauri build" }, "devDependencies": { + "@sveltejs/adapter-node": "^5.5.4", + "@sveltejs/adapter-static": "^3.0.10", + "@sveltejs/kit": "^2.57.0", "@sveltejs/vite-plugin-svelte": "^7.0.0", - "@tauri-apps/cli": "^2.11.0", - "svelte": "^5.55.5", - "svelte-check": "^4.4.7", - "typescript": "^6.0.3", - "vite": "^8.0.10" + "@tauri-apps/cli": "^2.0.0", + "svelte": "^5.55.2", + "svelte-check": "^4.4.6", + "typescript": "^6.0.2", + "vite": "^8.0.7" + }, + "dependencies": { + "@tauri-apps/api": "^2.0.0" } -} +} \ No newline at end of file diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a6084c0..8842c45 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -9,56 +9,35 @@ importers: .: dependencies: '@tauri-apps/api': - specifier: ^2.11.0 + specifier: ^2.0.0 version: 2.11.0 - '@tauri-apps/plugin-http': - specifier: ^2.5.8 - version: 2.5.8 - '@tauri-apps/plugin-os': - specifier: ^2.3.2 - version: 2.3.2 - '@tauri-apps/plugin-process': - specifier: ^2.3.1 - version: 2.3.1 - '@tauri-apps/plugin-shell': - specifier: ^2.3.5 - version: 2.3.5 - '@tauri-apps/plugin-store': - specifier: ~2.4.2 - version: 2.4.2 - clsx: - specifier: ^2.1.1 - version: 2.1.1 - phosphor-svelte: - specifier: ^3.1.0 - version: 3.1.0(svelte@5.55.5(@typescript-eslint/types@8.57.1))(vite@8.0.10) - svelte-spa-router: - specifier: ^5.1.0 - version: 5.1.0(svelte@5.55.5(@typescript-eslint/types@8.57.1)) - tauri-plugin-discord-rpc-api: - specifier: github:Youwes09/tauri-plugin-discord-rpc - version: https://codeload.github.com/Youwes09/tauri-plugin-discord-rpc/tar.gz/d2fd312945d0573153e0e7e2d2dfb131acecc52c - tauri-plugin-drpc: - specifier: ^1.0.3 - version: 1.0.3(typescript@6.0.3) devDependencies: + '@sveltejs/adapter-node': + specifier: ^5.5.4 + version: 5.5.4(@sveltejs/kit@2.60.1(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.5(@typescript-eslint/types@8.57.1))(vite@8.0.10))(svelte@5.55.5(@typescript-eslint/types@8.57.1))(typescript@6.0.3)(vite@8.0.10)) + '@sveltejs/adapter-static': + specifier: ^3.0.10 + version: 3.0.10(@sveltejs/kit@2.60.1(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.5(@typescript-eslint/types@8.57.1))(vite@8.0.10))(svelte@5.55.5(@typescript-eslint/types@8.57.1))(typescript@6.0.3)(vite@8.0.10)) + '@sveltejs/kit': + specifier: ^2.57.0 + version: 2.60.1(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.5(@typescript-eslint/types@8.57.1))(vite@8.0.10))(svelte@5.55.5(@typescript-eslint/types@8.57.1))(typescript@6.0.3)(vite@8.0.10) '@sveltejs/vite-plugin-svelte': specifier: ^7.0.0 version: 7.0.0(svelte@5.55.5(@typescript-eslint/types@8.57.1))(vite@8.0.10) '@tauri-apps/cli': - specifier: ^2.11.0 - version: 2.11.0 + specifier: ^2.0.0 + version: 2.11.2 svelte: - specifier: ^5.55.5 + specifier: ^5.55.2 version: 5.55.5(@typescript-eslint/types@8.57.1) svelte-check: - specifier: ^4.4.7 + specifier: ^4.4.6 version: 4.4.7(picomatch@4.0.4)(svelte@5.55.5(@typescript-eslint/types@8.57.1))(typescript@6.0.3) typescript: - specifier: ^6.0.3 + specifier: ^6.0.2 version: 6.0.3 vite: - specifier: ^8.0.10 + specifier: ^8.0.7 version: 8.0.10 packages: @@ -97,6 +76,9 @@ packages: '@oxc-project/types@0.127.0': resolution: {integrity: sha512-aIYXQBo4lCbO4z0R3FHeucQHpF46l2LbMdxRvqvuRuW2OxdnSkcng5B8+K12spgLDj93rtN3+J2Vac/TIO+ciQ==} + '@polka/url@1.0.0-next.29': + resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==} + '@rolldown/binding-android-arm64@1.0.0-rc.17': resolution: {integrity: sha512-s70pVGhw4zqGeFnXWvAzJDlvxhlRollagdCCKRgOsgUOH3N1l0LIxf83AtGzmb5SiVM4Hjl5HyarMRfdfj3DaQ==} engines: {node: ^20.19.0 || >=22.12.0} @@ -195,11 +177,214 @@ packages: '@rolldown/pluginutils@1.0.0-rc.17': resolution: {integrity: sha512-n8iosDOt6Ig1UhJ2AYqoIhHWh/isz0xpicHTzpKBeotdVsTEcxsSA/i3EVM7gQAj0rU27OLAxCjzlj15IWY7bg==} + '@rollup/plugin-commonjs@29.0.2': + resolution: {integrity: sha512-S/ggWH1LU7jTyi9DxZOKyxpVd4hF/OZ0JrEbeLjXk/DFXwRny0tjD2c992zOUYQobLrVkRVMDdmHP16HKP7GRg==} + engines: {node: '>=16.0.0 || 14 >= 14.17'} + peerDependencies: + rollup: ^2.68.0||^3.0.0||^4.0.0 + peerDependenciesMeta: + rollup: + optional: true + + '@rollup/plugin-json@6.1.0': + resolution: {integrity: sha512-EGI2te5ENk1coGeADSIwZ7G2Q8CJS2sF120T7jLw4xFw9n7wIOXHo+kIYRAoVpJAN+kmqZSoO3Fp4JtoNF4ReA==} + engines: {node: '>=14.0.0'} + peerDependencies: + rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 + peerDependenciesMeta: + rollup: + optional: true + + '@rollup/plugin-node-resolve@16.0.3': + resolution: {integrity: sha512-lUYM3UBGuM93CnMPG1YocWu7X802BrNF3jW2zny5gQyLQgRFJhV1Sq0Zi74+dh/6NBx1DxFC4b4GXg9wUCG5Qg==} + engines: {node: '>=14.0.0'} + peerDependencies: + rollup: ^2.78.0||^3.0.0||^4.0.0 + peerDependenciesMeta: + rollup: + optional: true + + '@rollup/pluginutils@5.3.0': + resolution: {integrity: sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q==} + engines: {node: '>=14.0.0'} + peerDependencies: + rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 + peerDependenciesMeta: + rollup: + optional: true + + '@rollup/rollup-android-arm-eabi@4.60.4': + resolution: {integrity: sha512-F5QXMSiFebS9hKZj02XhWLLnRpJ3B3AROP0tWbFBSj+6kCbg5m9j5JoHKd4mmSVy5mS/IMQloYgYxCuJC0fxEQ==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm64@4.60.4': + resolution: {integrity: sha512-GxxTKApUpzRhof7poWvCJHRF51C67u1R7D6DiluBE8wKU1u5GWE8t+v81JvJYtbawoBFX1hLv5Ei4eVjkWokaw==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-darwin-arm64@4.60.4': + resolution: {integrity: sha512-tua0TaJxMOB1R0V0RS1jFZ/RpURFDJIOR2A6jWwQeawuFyS4gBW+rntLRaQd0EQ4bd6Vp44Z2rXW+YYDBsj6IA==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.60.4': + resolution: {integrity: sha512-CSKq7MsP+5PFIcydhAiR1K0UhEI1A2jWXVKHPCBZ151yOutENwvnPocgVHkivu2kviURtCEB6zUQw0vs8RrhMg==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-freebsd-arm64@4.60.4': + resolution: {integrity: sha512-+O8OkVdyvXMtJEciu2wS/pzm1IxntEEQx3z5TAVy4l32G0etZn+RsA48ARRrFm6Ri8fvqPQfgrvNxSjKAbnd3g==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.60.4': + resolution: {integrity: sha512-Iw3oMskH3AfNuhU0MSN7vNbdi4me/NiYo2azqPz/Le16zHSa+3RRmliCMWWQmh4lcndccU40xcJuTYJZxNo/lw==} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-linux-arm-gnueabihf@4.60.4': + resolution: {integrity: sha512-EIPRXTVQpHyF8WOo219AD2yEltPehLTcTMz2fn6JsatLYSzQf00hj3rulF+yauOlF9/FtM2WpkT/hJh/KJFGhA==} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-arm-musleabihf@4.60.4': + resolution: {integrity: sha512-J3Yh9PzzF1Ovah2At+lHiGQdsYgArxBbXv/zHfSyaiFQEqvNv7DcW98pCrmdjCZBrqBiKrKKe2V+aaSGWuBe/w==} + cpu: [arm] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-arm64-gnu@4.60.4': + resolution: {integrity: sha512-BFDEZMYfUvLn37ONE1yMBojPxnMlTFsdyNoqncT0qFq1mAfllL+ATMMJd8TeuVMiX84s1KbcxcZbXInmcO2mRg==} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-arm64-musl@4.60.4': + resolution: {integrity: sha512-pc9EYOSlOgdQ2uPl1o9PF6/kLSgaUosia7gOuS8mB69IxJvlclko1MECXysjs5ryez1/5zjYqx3+xYU0TU6R1A==} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-loong64-gnu@4.60.4': + resolution: {integrity: sha512-NxnomyxYerDh5n4iLrNa+sH+Z+U4BMEE46V2PgQ/hoB909i8gV1M5wPojWg9fk1jWpO3IQnOs20K4wyZuFLEFQ==} + cpu: [loong64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-loong64-musl@4.60.4': + resolution: {integrity: sha512-nbJnQ8a3z1mtmrwImCYhc6BGpThAyYVRQxw9uKSKG4wR6aAYno9sVjJ0zaZcW9BPJX1GbrDPf+SvdWjgTuDmnw==} + cpu: [loong64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-ppc64-gnu@4.60.4': + resolution: {integrity: sha512-2EU6acNrQLd8tYvo/LXW535wupT3m6fo7HKo6lr7ktQoItxTyOL1ZCR/GfGCuXl2vR+zmfI6eRXkSemafv+iVg==} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-ppc64-musl@4.60.4': + resolution: {integrity: sha512-WeBtoMuaMxiiIrO2IYP3xs6GMWkJP2C0EoT8beTLkUPmzV1i/UcOSVw1d5r9KBODtHKilG5yFxsGRnBbK3wJ4A==} + cpu: [ppc64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-riscv64-gnu@4.60.4': + resolution: {integrity: sha512-FJHFfqpKUI3A10WrWKiFbBZ7yVbGT4q4B5o1qKFFojqpaYoh9LrQgqWCmmcxQzVSXYtyB5bzkXrYzlHTs21MYA==} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-riscv64-musl@4.60.4': + resolution: {integrity: sha512-mcEl6CUT5IAUmQf1m9FYSmVqCJlpQ8r8eyftFUHG8i9OhY7BkBXSUdnLH5DOf0wCOjcP9v/QO93zpmF1SptCCw==} + cpu: [riscv64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-s390x-gnu@4.60.4': + resolution: {integrity: sha512-ynt3JxVd2w2buzoKDWIyiV1pJW93xlQic1THVLXilz429oijRpSHivZAgp65KBu+cMcgf1eVVjdnTLvPxgCuoQ==} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-x64-gnu@4.60.4': + resolution: {integrity: sha512-Boiz5+MsaROEWDf+GGEwF8VMHGhlUoQMtIPjOgA5fv4osupqTVnJteQNKJwUcnUog2G55jYXH7KZFFiJe0TEzQ==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-x64-musl@4.60.4': + resolution: {integrity: sha512-+qfSY27qIrFfI/Hom04KYFw3GKZSGU4lXus51wsb5EuySfFlWRwjkKWoE9emgRw/ukoT4Udsj4W/+xxG8VbPKg==} + cpu: [x64] + os: [linux] + libc: [musl] + + '@rollup/rollup-openbsd-x64@4.60.4': + resolution: {integrity: sha512-VpTfOPHgVXEBeeR8hZ2O0F3aSso+JDWqTWmTmzcQKted54IAdUVbxE+j/MVxUsKa8L20HJhv3vUezVPoquqWjA==} + cpu: [x64] + os: [openbsd] + + '@rollup/rollup-openharmony-arm64@4.60.4': + resolution: {integrity: sha512-IPOsh5aRYuLv/nkU51X10Bf75Bsf6+gZdx1X+QP5QM6lIJFHHqbHLG0uJn/hWthzo13UAc2umiUorqZy3axoZg==} + cpu: [arm64] + os: [openharmony] + + '@rollup/rollup-win32-arm64-msvc@4.60.4': + resolution: {integrity: sha512-4QzE9E81OohJ/HKzHhsqU+zcYYojVOXlFMs1DdyMT6qXl/niOH7AVElmmEdUNHHS/oRkc++d5k6Vy85zFs0DEw==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.60.4': + resolution: {integrity: sha512-zTPgT1YuHHcd+Tmx7h8aml0FWFVelV5N54oHow9SLj+GfoDy/huQ+UV396N/C7KpMDMiPspRktzM1/0r1usYEA==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-x64-gnu@4.60.4': + resolution: {integrity: sha512-DRS4G7mi9lJxqEDezIkKCaUIKCrLUUDCUaCsTPCi/rtqaC6D/jjwslMQyiDU50Ka0JKpeXeRBFBAXwArY52vBw==} + cpu: [x64] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.60.4': + resolution: {integrity: sha512-QVTUovf40zgTqlFVrKA1uXMVvU2QWEFWfAH8Wdc48IxLvrJMQVMBRjuQyUpzZCDkakImib9eVazbWlC6ksWtJw==} + cpu: [x64] + os: [win32] + + '@standard-schema/spec@1.1.0': + resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} + '@sveltejs/acorn-typescript@1.0.9': resolution: {integrity: sha512-lVJX6qEgs/4DOcRTpo56tmKzVPtoWAaVbL4hfO7t7NVwl9AAXzQR6cihesW1BmNMPl+bK6dreu2sOKBP2Q9CIA==} peerDependencies: acorn: ^8.9.0 + '@sveltejs/adapter-node@5.5.4': + resolution: {integrity: sha512-45X92CXW+2J8ZUzPv3eLlKWEzINKiiGeFWTjyER4ZN4sGgNoaoeSkCY/QYNxHpPXy71QPsctwccBo9jJs0ySPQ==} + peerDependencies: + '@sveltejs/kit': ^2.4.0 + + '@sveltejs/adapter-static@3.0.10': + resolution: {integrity: sha512-7D9lYFWJmB7zxZyTE/qxjksvMqzMuYrrsyh1f4AlZqeZeACPRySjbC3aFiY55wb1tWUaKOQG9PVbm74JcN2Iew==} + peerDependencies: + '@sveltejs/kit': ^2.0.0 + + '@sveltejs/kit@2.60.1': + resolution: {integrity: sha512-mQjlkNo+rJvpln7V2IGY2j99BqhcFbS4UN0AQNKNYfhBAFZTuCDAdW3a1sgf330mvtNvsBXn3HpAhcmvdJTcIQ==} + engines: {node: '>=18.13'} + hasBin: true + peerDependencies: + '@opentelemetry/api': ^1.0.0 + '@sveltejs/vite-plugin-svelte': ^3.0.0 || ^4.0.0-next.1 || ^5.0.0 || ^6.0.0-next.0 || ^7.0.0 + svelte: ^4.0.0 || ^5.0.0-next.0 + typescript: ^5.3.3 || ^6.0.0 + vite: ^5.0.3 || ^6.0.0 || ^7.0.0-beta.0 || ^8.0.0 + peerDependenciesMeta: + '@opentelemetry/api': + optional: true + typescript: + optional: true + '@sveltejs/vite-plugin-svelte@7.0.0': resolution: {integrity: sha512-ILXmxC7HAsnkK2eslgPetrqqW1BKSL7LktsFgqzNj83MaivMGZzluWq32m25j2mDOjmSKX7GGWahePhuEs7P/g==} engines: {node: ^20.19 || ^22.12 || >=24} @@ -210,103 +395,94 @@ packages: '@tauri-apps/api@2.11.0': resolution: {integrity: sha512-7CinYODhky9lmO23xHnUFv0Xt43fbtWMyxZcLcRBlFkcgXKuEirBvHpmtJ89YMhyeGcq20Wuc47Fa4XjyniywA==} - '@tauri-apps/cli-darwin-arm64@2.11.0': - resolution: {integrity: sha512-UfMeDNlgIP252rm/KSTuu8yHatPua5TjtUEUf+jyIzVwBNcIl7Ywkdpfj+e5jVVg3EfCTp+4gwuL1dNpgF8clg==} + '@tauri-apps/cli-darwin-arm64@2.11.2': + resolution: {integrity: sha512-+4UZzLt+eOAEQCwgd+TqKgyUJMrvx+BgdXLLaqJYmPqzP+nE6YZr/hY6CWLYGQb8jFn99jEkmC6uA3tNvamA1w==} engines: {node: '>= 10'} cpu: [arm64] os: [darwin] - '@tauri-apps/cli-darwin-x64@2.11.0': - resolution: {integrity: sha512-lY1+aPlgyMN7vgjtCdQ3+WODfZkebAcxnrCrO0HjqDpKSXieDkrJbimqeaoM4RwhTSrCLRHfVYiYrfE5E131tg==} + '@tauri-apps/cli-darwin-x64@2.11.2': + resolution: {integrity: sha512-VjYYtZUPqDMLutSfJEyxFE3Bz+DPi7c8wC3imckgvciLDZLq4qwKJxBicg0BXGhXjJsl8vKWgWRFNMPELQ+Xyg==} engines: {node: '>= 10'} cpu: [x64] os: [darwin] - '@tauri-apps/cli-linux-arm-gnueabihf@2.11.0': - resolution: {integrity: sha512-5uCP0AusgN3NrKC8EpkuJwjek1k8pEffBdugJSpXPey/QGbPEb8vZ542n/giJ2mZPjMSllDkdhG2QIDpBY4PpQ==} + '@tauri-apps/cli-linux-arm-gnueabihf@2.11.2': + resolution: {integrity: sha512-yMemD6f4i95AQriS8EazyOFzbE34yjnP16i3IOzpHGQvBoy2DjypFMFBq0NtPuITURv/cOGguRtHR5d79/9CSA==} engines: {node: '>= 10'} cpu: [arm] os: [linux] - '@tauri-apps/cli-linux-arm64-gnu@2.11.0': - resolution: {integrity: sha512-loDPqtRHMSbIcrH2VBd4GgHoQlF7jJnrZj7MxA2lj1cixS/jEgMAPFqj83U6Wvjete4HfYplbE/gCpSFifA9jw==} + '@tauri-apps/cli-linux-arm64-gnu@2.11.2': + resolution: {integrity: sha512-cgI91D2wL8GSgoWwZXDqt+DwnuZCP2/bz03QAE4TrhgAKIsrB4hX26W/H1EONPUUNkqrsgeCD0wU6pcNjV/5kw==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] libc: [glibc] - '@tauri-apps/cli-linux-arm64-musl@2.11.0': - resolution: {integrity: sha512-DtSE8ZBlB9H+L+eHkfZ3myt00EVEyAB3e41juEHoE2qT88fgVlJvyrwa9SZYc/xTwCS9TnmK+R84tpg+ZsAg7Q==} + '@tauri-apps/cli-linux-arm64-musl@2.11.2': + resolution: {integrity: sha512-X1rm0BERqAAggtYTESSgXrS3sz4Sb/OiPiz54UqISlXW+GkR3vNIGnsy/lejNmoXGVqri3Q53BCfQiclOIyRPw==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] libc: [musl] - '@tauri-apps/cli-linux-riscv64-gnu@2.11.0': - resolution: {integrity: sha512-5QdgS4LD+kntClI1aj2JmwjW38LosNXxwCe8viIHEwqYIWuMPdNEIau6/cLogI38Yzx9DnfCPRfEWLyI+5li8Q==} + '@tauri-apps/cli-linux-riscv64-gnu@2.11.2': + resolution: {integrity: sha512-usbMLJbT3KtkOrBMDVeGYNM35aTHXx38SJSzTMSqqjeUIOQ+iVPjb2yAGNAE+KqmBbAx4FOFIyMeKXx2M/JKGQ==} engines: {node: '>= 10'} cpu: [riscv64] os: [linux] libc: [glibc] - '@tauri-apps/cli-linux-x64-gnu@2.11.0': - resolution: {integrity: sha512-5UynPXo3Zq9khjVdAbD+YogeLltdVUeOah2ioSIM3tu6H7wY9vMy6rgGJhv9r5R8ZXmk9GttMippdqYJWrnLnA==} + '@tauri-apps/cli-linux-x64-gnu@2.11.2': + resolution: {integrity: sha512-Ru4gwJKPG0ctVGchRGpRup4Y4lW2SSfFnrbQcyHhCliKy4g8Qz97TrUgCur4CbWyAgKxvGh3SjrkA0LDYzDGiw==} engines: {node: '>= 10'} cpu: [x64] os: [linux] libc: [glibc] - '@tauri-apps/cli-linux-x64-musl@2.11.0': - resolution: {integrity: sha512-CNz7fHbApz1Zyhhq73jtGn9JqgNEV/lIWnTnUo6h6ujw+mHsTmkLszvJSM8W6JBaDjNpTTFr/RSNoVL5FMwcTg==} + '@tauri-apps/cli-linux-x64-musl@2.11.2': + resolution: {integrity: sha512-eUm7T6clN1MMmNSRQ9gaWsQdyehQx2Gmn5hht/QUlqZQI/qcP2OJK5dnaxqwFzCr2HdsEo9ydxaqcS1oJzMvUw==} engines: {node: '>= 10'} cpu: [x64] os: [linux] libc: [musl] - '@tauri-apps/cli-win32-arm64-msvc@2.11.0': - resolution: {integrity: sha512-K+br+VXZ+Xx0n/9FdWohpW5Ugq+2FQUpJScqcPl1hTxXfh3fgjYgt4qA2NgrjlJo+zZPNrmUMl+NLvm0ufEqBQ==} + '@tauri-apps/cli-win32-arm64-msvc@2.11.2': + resolution: {integrity: sha512-HeeZW80jU+gVTOEX4X/hC6NVSAdDVXajwP5fxIZ/3z9WvUC7qrudX2GMTilYq6Dg0e0sk0XgsAJD1hZ5wPBXUA==} engines: {node: '>= 10'} cpu: [arm64] os: [win32] - '@tauri-apps/cli-win32-ia32-msvc@2.11.0': - resolution: {integrity: sha512-OFV+s3MLZnd75zl0ZAFU5riMpGK4waUEA8ZDuijDsnkU0btz/gHhqh5jVlOn8thyvgdtT3Xyoxqo099MMifH3g==} + '@tauri-apps/cli-win32-ia32-msvc@2.11.2': + resolution: {integrity: sha512-YhjQNZcXfbkCLyazSv1nPnJ9iRFE1wm6kc51FDbU10/Dk09io+6PAGMLjkxnX2GdM0qMnDmTjstY8mTDVvtKeA==} engines: {node: '>= 10'} cpu: [ia32] os: [win32] - '@tauri-apps/cli-win32-x64-msvc@2.11.0': - resolution: {integrity: sha512-AeDTWBd2cOZ6TX133BWsoo+LutG9o0JRcgjMsIfLE13ZugpgCMv/2dJbUiBGeRvbPOGin5A3aYmsArPVV6ZSHQ==} + '@tauri-apps/cli-win32-x64-msvc@2.11.2': + resolution: {integrity: sha512-d2JchlFIpZevZVReyqhQOekJmb1UH3rhZ5VX6sH3ty9ETE0TKQavpihvoScUXfKKpW6HZC0MrFGRU0ZtD+w3gA==} engines: {node: '>= 10'} cpu: [x64] os: [win32] - '@tauri-apps/cli@2.11.0': - resolution: {integrity: sha512-W5Wbuqsb2pHFPTj4TaRNKTj5rwXhDShPiLSY9T18y4ouSR/NNCptAEFxFsBtyNRgL6Vs1a/q9LzfqqYzEwC+Jw==} + '@tauri-apps/cli@2.11.2': + resolution: {integrity: sha512-bk3HemqvGRoy+5D/dVMUQHKMYLglD0jVnMm/0iGMH6ufZ+p8r14m6BpIixwij3PBvZdvORUp1YifTD8QxVZ1Nw==} engines: {node: '>= 10'} hasBin: true - '@tauri-apps/plugin-http@2.5.8': - resolution: {integrity: sha512-oxd7oypzQeu8kAfFCrw534Kq7Cw+NzozcnCY21O4rz3A+veJiIiuSCMIprgGcZOcLAXFP9GmDhKUbhuKWcunRw==} - - '@tauri-apps/plugin-os@2.3.2': - resolution: {integrity: sha512-n+nXWeuSeF9wcEsSPmRnBEGrRgOy6jjkSU+UVCOV8YUGKb2erhDOxis7IqRXiRVHhY8XMKks00BJ0OAdkpf6+A==} - - '@tauri-apps/plugin-process@2.3.1': - resolution: {integrity: sha512-nCa4fGVaDL/B9ai03VyPOjfAHRHSBz5v6F/ObsB73r/dA3MHHhZtldaDMIc0V/pnUw9ehzr2iEG+XkSEyC0JJA==} - - '@tauri-apps/plugin-shell@2.3.5': - resolution: {integrity: sha512-jewtULhiQ7lI7+owCKAjc8tYLJr92U16bPOeAa472LHJdgaibLP83NcfAF2e+wkEcA53FxKQAZ7byDzs2eeizg==} - - '@tauri-apps/plugin-store@2.4.2': - resolution: {integrity: sha512-0ClHS50Oq9HEvLPhNzTNFxbWVOqoAp3dRvtewQBeqfIQ0z5m3JRnOISIn2ZVPCrQC0MyGyhTS9DWhHjpigQE7A==} - '@tybys/wasm-util@0.10.1': resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==} + '@types/cookie@0.6.0': + resolution: {integrity: sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==} + '@types/estree@1.0.8': resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + '@types/resolve@1.20.2': + resolution: {integrity: sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==} + '@types/trusted-types@2.0.7': resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==} @@ -335,6 +511,13 @@ packages: resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} engines: {node: '>=6'} + commondir@1.0.1: + resolution: {integrity: sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==} + + cookie@0.6.0: + resolution: {integrity: sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==} + engines: {node: '>= 0.6'} + deepmerge@4.3.1: resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} engines: {node: '>=0.10.0'} @@ -346,6 +529,13 @@ packages: devalue@5.8.0: resolution: {integrity: sha512-2zA9pFEsnp7vWBZbXF5JAgAq0fsUIt/1XPbRiAmRV3lp/2C3upzH+sADiyy66aFCihoLEsrQHxNM5w1gIDfsBg==} + devalue@5.8.1: + resolution: {integrity: sha512-4CXDYRBGqN+57wVJkuXBYmpAVUSg3L6JAQa/DFqm238G73E1wuyc/JhGQJzN7vUf/CMphYau2zXbfWzDR5aTEw==} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + esm-env@1.2.2: resolution: {integrity: sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==} @@ -357,8 +547,8 @@ packages: '@typescript-eslint/types': optional: true - estree-walker@3.0.3: - resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + estree-walker@2.0.2: + resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} fdir@6.5.0: resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} @@ -374,9 +564,30 @@ packages: engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} os: [darwin] + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + hasown@2.0.3: + resolution: {integrity: sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==} + engines: {node: '>= 0.4'} + + is-core-module@2.16.2: + resolution: {integrity: sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==} + engines: {node: '>= 0.4'} + + is-module@1.0.0: + resolution: {integrity: sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==} + + is-reference@1.2.1: + resolution: {integrity: sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ==} + is-reference@3.0.3: resolution: {integrity: sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==} + kleur@4.1.5: + resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==} + engines: {node: '>=6'} + lightningcss-android-arm64@1.32.0: resolution: {integrity: sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==} engines: {node: '>= 12.0.0'} @@ -461,6 +672,10 @@ packages: resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==} engines: {node: '>=4'} + mrmime@2.0.1: + resolution: {integrity: sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==} + engines: {node: '>=10'} + nanoid@3.3.12: resolution: {integrity: sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} @@ -469,14 +684,8 @@ packages: obug@2.1.1: resolution: {integrity: sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==} - phosphor-svelte@3.1.0: - resolution: {integrity: sha512-nldtxx+XCgNREvrb7O5xgDsefytXpSkPTx8Rnu3f2qQCUZLDV1rLxYSd2Jcwckuo9lZB1qKMqGR17P4UDC0PrA==} - peerDependencies: - svelte: ^5.0.0 || ^5.0.0-next.96 - vite: '>=5' - peerDependenciesMeta: - vite: - optional: true + path-parse@1.0.7: + resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} @@ -493,23 +702,40 @@ packages: resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} engines: {node: '>= 14.18.0'} - regexparam@2.0.2: - resolution: {integrity: sha512-A1PeDEYMrkLrfyOwv2jwihXbo9qxdGD3atBYQA9JJgreAx8/7rC6IUkWOw2NQlOxLp2wL0ifQbh1HuidDfYA6w==} - engines: {node: '>=8'} + resolve@1.22.12: + resolution: {integrity: sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==} + engines: {node: '>= 0.4'} + hasBin: true rolldown@1.0.0-rc.17: resolution: {integrity: sha512-ZrT53oAKrtA4+YtBWPQbtPOxIbVDbxT0orcYERKd63VJTF13zPcgXTvD4843L8pcsI7M6MErt8QtON6lrB9tyA==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true + rollup@4.60.4: + resolution: {integrity: sha512-WHeFSbZYsPu3+bLoNRUuAO+wavNlocOPf3wSHTP7hcFKVnJeWsYlCDbr3mTS14FCizf9ccIxXA8sGL8zKeQN3g==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + sade@1.8.1: resolution: {integrity: sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==} engines: {node: '>=6'} + set-cookie-parser@3.1.0: + resolution: {integrity: sha512-kjnC1DXBHcxaOaOXBHBeRtltsDG2nUiUni+jP92M9gYdW12rsmx92UsfpH7o5tDRs7I1ZZPSQJQGv3UaRfCiuw==} + + sirv@3.0.2: + resolution: {integrity: sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g==} + engines: {node: '>=18'} + source-map-js@1.2.1: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} + supports-preserve-symlinks-flag@1.0.0: + resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} + engines: {node: '>= 0.4'} + svelte-check@4.4.7: resolution: {integrity: sha512-JRafFTRmaPUOqmri4u1WuIKgBLiHi6wIaB57i99pmHq5BAc3ioIpzdUN/RX32ij9GhI6ALMHKvnVxu68sFZlag==} engines: {node: '>= 18.0.0'} @@ -518,28 +744,18 @@ packages: svelte: ^4.0.0 || ^5.0.0-next.0 typescript: '>=5.0.0' - svelte-spa-router@5.1.0: - resolution: {integrity: sha512-YGtXOF+advJnQBvS4mvRnHVUF0qpSI2pUrSxidz4s7X9x80IKEgYzupx6j46gZcIcN3BYjmuLUbsgQv1S5bZzQ==} - peerDependencies: - svelte: ^5.0.0 - svelte@5.55.5: resolution: {integrity: sha512-2uCs/LZ9us+AktdzYJM8OcxQ8qnPS1kpaO7syGT/MgO+6Qr1Ybl+TqPq+97u7PHqmmMlye5ZkoyXONy5mjjAbw==} engines: {node: '>=18'} - tauri-plugin-discord-rpc-api@https://codeload.github.com/Youwes09/tauri-plugin-discord-rpc/tar.gz/d2fd312945d0573153e0e7e2d2dfb131acecc52c: - resolution: {tarball: https://codeload.github.com/Youwes09/tauri-plugin-discord-rpc/tar.gz/d2fd312945d0573153e0e7e2d2dfb131acecc52c} - version: 0.1.0 - - tauri-plugin-drpc@1.0.3: - resolution: {integrity: sha512-vl5dXhjKbl7+Nf9veW12usdmIUtZXwEf91SzxQPZlbRRJ/sjizbbQlnkUTtx6baJuGzz0KXXgP9xUhF39BdiXQ==} - peerDependencies: - typescript: ^5.0.0 - tinyglobby@0.2.16: resolution: {integrity: sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==} engines: {node: '>=12.0.0'} + totalist@3.0.1: + resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==} + engines: {node: '>=6'} + tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} @@ -648,6 +864,8 @@ snapshots: '@oxc-project/types@0.127.0': {} + '@polka/url@1.0.0-next.29': {} + '@rolldown/binding-android-arm64@1.0.0-rc.17': optional: true @@ -699,10 +917,155 @@ snapshots: '@rolldown/pluginutils@1.0.0-rc.17': {} + '@rollup/plugin-commonjs@29.0.2(rollup@4.60.4)': + dependencies: + '@rollup/pluginutils': 5.3.0(rollup@4.60.4) + commondir: 1.0.1 + estree-walker: 2.0.2 + fdir: 6.5.0(picomatch@4.0.4) + is-reference: 1.2.1 + magic-string: 0.30.21 + picomatch: 4.0.4 + optionalDependencies: + rollup: 4.60.4 + + '@rollup/plugin-json@6.1.0(rollup@4.60.4)': + dependencies: + '@rollup/pluginutils': 5.3.0(rollup@4.60.4) + optionalDependencies: + rollup: 4.60.4 + + '@rollup/plugin-node-resolve@16.0.3(rollup@4.60.4)': + dependencies: + '@rollup/pluginutils': 5.3.0(rollup@4.60.4) + '@types/resolve': 1.20.2 + deepmerge: 4.3.1 + is-module: 1.0.0 + resolve: 1.22.12 + optionalDependencies: + rollup: 4.60.4 + + '@rollup/pluginutils@5.3.0(rollup@4.60.4)': + dependencies: + '@types/estree': 1.0.8 + estree-walker: 2.0.2 + picomatch: 4.0.4 + optionalDependencies: + rollup: 4.60.4 + + '@rollup/rollup-android-arm-eabi@4.60.4': + optional: true + + '@rollup/rollup-android-arm64@4.60.4': + optional: true + + '@rollup/rollup-darwin-arm64@4.60.4': + optional: true + + '@rollup/rollup-darwin-x64@4.60.4': + optional: true + + '@rollup/rollup-freebsd-arm64@4.60.4': + optional: true + + '@rollup/rollup-freebsd-x64@4.60.4': + optional: true + + '@rollup/rollup-linux-arm-gnueabihf@4.60.4': + optional: true + + '@rollup/rollup-linux-arm-musleabihf@4.60.4': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.60.4': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.60.4': + optional: true + + '@rollup/rollup-linux-loong64-gnu@4.60.4': + optional: true + + '@rollup/rollup-linux-loong64-musl@4.60.4': + optional: true + + '@rollup/rollup-linux-ppc64-gnu@4.60.4': + optional: true + + '@rollup/rollup-linux-ppc64-musl@4.60.4': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.60.4': + optional: true + + '@rollup/rollup-linux-riscv64-musl@4.60.4': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.60.4': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.60.4': + optional: true + + '@rollup/rollup-linux-x64-musl@4.60.4': + optional: true + + '@rollup/rollup-openbsd-x64@4.60.4': + optional: true + + '@rollup/rollup-openharmony-arm64@4.60.4': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.60.4': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.60.4': + optional: true + + '@rollup/rollup-win32-x64-gnu@4.60.4': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.60.4': + optional: true + + '@standard-schema/spec@1.1.0': {} + '@sveltejs/acorn-typescript@1.0.9(acorn@8.16.0)': dependencies: acorn: 8.16.0 + '@sveltejs/adapter-node@5.5.4(@sveltejs/kit@2.60.1(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.5(@typescript-eslint/types@8.57.1))(vite@8.0.10))(svelte@5.55.5(@typescript-eslint/types@8.57.1))(typescript@6.0.3)(vite@8.0.10))': + dependencies: + '@rollup/plugin-commonjs': 29.0.2(rollup@4.60.4) + '@rollup/plugin-json': 6.1.0(rollup@4.60.4) + '@rollup/plugin-node-resolve': 16.0.3(rollup@4.60.4) + '@sveltejs/kit': 2.60.1(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.5(@typescript-eslint/types@8.57.1))(vite@8.0.10))(svelte@5.55.5(@typescript-eslint/types@8.57.1))(typescript@6.0.3)(vite@8.0.10) + rollup: 4.60.4 + + '@sveltejs/adapter-static@3.0.10(@sveltejs/kit@2.60.1(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.5(@typescript-eslint/types@8.57.1))(vite@8.0.10))(svelte@5.55.5(@typescript-eslint/types@8.57.1))(typescript@6.0.3)(vite@8.0.10))': + dependencies: + '@sveltejs/kit': 2.60.1(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.5(@typescript-eslint/types@8.57.1))(vite@8.0.10))(svelte@5.55.5(@typescript-eslint/types@8.57.1))(typescript@6.0.3)(vite@8.0.10) + + '@sveltejs/kit@2.60.1(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.5(@typescript-eslint/types@8.57.1))(vite@8.0.10))(svelte@5.55.5(@typescript-eslint/types@8.57.1))(typescript@6.0.3)(vite@8.0.10)': + dependencies: + '@standard-schema/spec': 1.1.0 + '@sveltejs/acorn-typescript': 1.0.9(acorn@8.16.0) + '@sveltejs/vite-plugin-svelte': 7.0.0(svelte@5.55.5(@typescript-eslint/types@8.57.1))(vite@8.0.10) + '@types/cookie': 0.6.0 + acorn: 8.16.0 + cookie: 0.6.0 + devalue: 5.8.1 + esm-env: 1.2.2 + kleur: 4.1.5 + magic-string: 0.30.21 + mrmime: 2.0.1 + set-cookie-parser: 3.1.0 + sirv: 3.0.2 + svelte: 5.55.5(@typescript-eslint/types@8.57.1) + vite: 8.0.10 + optionalDependencies: + typescript: 6.0.3 + '@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.5(@typescript-eslint/types@8.57.1))(vite@8.0.10)': dependencies: deepmerge: 4.3.1 @@ -714,80 +1077,64 @@ snapshots: '@tauri-apps/api@2.11.0': {} - '@tauri-apps/cli-darwin-arm64@2.11.0': + '@tauri-apps/cli-darwin-arm64@2.11.2': optional: true - '@tauri-apps/cli-darwin-x64@2.11.0': + '@tauri-apps/cli-darwin-x64@2.11.2': optional: true - '@tauri-apps/cli-linux-arm-gnueabihf@2.11.0': + '@tauri-apps/cli-linux-arm-gnueabihf@2.11.2': optional: true - '@tauri-apps/cli-linux-arm64-gnu@2.11.0': + '@tauri-apps/cli-linux-arm64-gnu@2.11.2': optional: true - '@tauri-apps/cli-linux-arm64-musl@2.11.0': + '@tauri-apps/cli-linux-arm64-musl@2.11.2': optional: true - '@tauri-apps/cli-linux-riscv64-gnu@2.11.0': + '@tauri-apps/cli-linux-riscv64-gnu@2.11.2': optional: true - '@tauri-apps/cli-linux-x64-gnu@2.11.0': + '@tauri-apps/cli-linux-x64-gnu@2.11.2': optional: true - '@tauri-apps/cli-linux-x64-musl@2.11.0': + '@tauri-apps/cli-linux-x64-musl@2.11.2': optional: true - '@tauri-apps/cli-win32-arm64-msvc@2.11.0': + '@tauri-apps/cli-win32-arm64-msvc@2.11.2': optional: true - '@tauri-apps/cli-win32-ia32-msvc@2.11.0': + '@tauri-apps/cli-win32-ia32-msvc@2.11.2': optional: true - '@tauri-apps/cli-win32-x64-msvc@2.11.0': + '@tauri-apps/cli-win32-x64-msvc@2.11.2': optional: true - '@tauri-apps/cli@2.11.0': + '@tauri-apps/cli@2.11.2': optionalDependencies: - '@tauri-apps/cli-darwin-arm64': 2.11.0 - '@tauri-apps/cli-darwin-x64': 2.11.0 - '@tauri-apps/cli-linux-arm-gnueabihf': 2.11.0 - '@tauri-apps/cli-linux-arm64-gnu': 2.11.0 - '@tauri-apps/cli-linux-arm64-musl': 2.11.0 - '@tauri-apps/cli-linux-riscv64-gnu': 2.11.0 - '@tauri-apps/cli-linux-x64-gnu': 2.11.0 - '@tauri-apps/cli-linux-x64-musl': 2.11.0 - '@tauri-apps/cli-win32-arm64-msvc': 2.11.0 - '@tauri-apps/cli-win32-ia32-msvc': 2.11.0 - '@tauri-apps/cli-win32-x64-msvc': 2.11.0 - - '@tauri-apps/plugin-http@2.5.8': - dependencies: - '@tauri-apps/api': 2.11.0 - - '@tauri-apps/plugin-os@2.3.2': - dependencies: - '@tauri-apps/api': 2.11.0 - - '@tauri-apps/plugin-process@2.3.1': - dependencies: - '@tauri-apps/api': 2.11.0 - - '@tauri-apps/plugin-shell@2.3.5': - dependencies: - '@tauri-apps/api': 2.11.0 - - '@tauri-apps/plugin-store@2.4.2': - dependencies: - '@tauri-apps/api': 2.11.0 + '@tauri-apps/cli-darwin-arm64': 2.11.2 + '@tauri-apps/cli-darwin-x64': 2.11.2 + '@tauri-apps/cli-linux-arm-gnueabihf': 2.11.2 + '@tauri-apps/cli-linux-arm64-gnu': 2.11.2 + '@tauri-apps/cli-linux-arm64-musl': 2.11.2 + '@tauri-apps/cli-linux-riscv64-gnu': 2.11.2 + '@tauri-apps/cli-linux-x64-gnu': 2.11.2 + '@tauri-apps/cli-linux-x64-musl': 2.11.2 + '@tauri-apps/cli-win32-arm64-msvc': 2.11.2 + '@tauri-apps/cli-win32-ia32-msvc': 2.11.2 + '@tauri-apps/cli-win32-x64-msvc': 2.11.2 '@tybys/wasm-util@0.10.1': dependencies: tslib: 2.8.1 optional: true + '@types/cookie@0.6.0': {} + '@types/estree@1.0.8': {} + '@types/resolve@1.20.2': {} + '@types/trusted-types@2.0.7': {} '@typescript-eslint/types@8.57.1': @@ -805,12 +1152,20 @@ snapshots: clsx@2.1.1: {} + commondir@1.0.1: {} + + cookie@0.6.0: {} + deepmerge@4.3.1: {} detect-libc@2.1.2: {} devalue@5.8.0: {} + devalue@5.8.1: {} + + es-errors@1.3.0: {} + esm-env@1.2.2: {} esrap@2.2.5(@typescript-eslint/types@8.57.1): @@ -819,9 +1174,7 @@ snapshots: optionalDependencies: '@typescript-eslint/types': 8.57.1 - estree-walker@3.0.3: - dependencies: - '@types/estree': 1.0.8 + estree-walker@2.0.2: {} fdir@6.5.0(picomatch@4.0.4): optionalDependencies: @@ -830,10 +1183,28 @@ snapshots: fsevents@2.3.3: optional: true + function-bind@1.1.2: {} + + hasown@2.0.3: + dependencies: + function-bind: 1.1.2 + + is-core-module@2.16.2: + dependencies: + hasown: 2.0.3 + + is-module@1.0.0: {} + + is-reference@1.2.1: + dependencies: + '@types/estree': 1.0.8 + is-reference@3.0.3: dependencies: '@types/estree': 1.0.8 + kleur@4.1.5: {} + lightningcss-android-arm64@1.32.0: optional: true @@ -891,17 +1262,13 @@ snapshots: mri@1.2.0: {} + mrmime@2.0.1: {} + nanoid@3.3.12: {} obug@2.1.1: {} - phosphor-svelte@3.1.0(svelte@5.55.5(@typescript-eslint/types@8.57.1))(vite@8.0.10): - dependencies: - estree-walker: 3.0.3 - magic-string: 0.30.21 - svelte: 5.55.5(@typescript-eslint/types@8.57.1) - optionalDependencies: - vite: 8.0.10 + path-parse@1.0.7: {} picocolors@1.1.1: {} @@ -915,7 +1282,12 @@ snapshots: readdirp@4.1.2: {} - regexparam@2.0.2: {} + resolve@1.22.12: + dependencies: + es-errors: 1.3.0 + is-core-module: 2.16.2 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 rolldown@1.0.0-rc.17: dependencies: @@ -938,12 +1310,53 @@ snapshots: '@rolldown/binding-win32-arm64-msvc': 1.0.0-rc.17 '@rolldown/binding-win32-x64-msvc': 1.0.0-rc.17 + rollup@4.60.4: + dependencies: + '@types/estree': 1.0.8 + optionalDependencies: + '@rollup/rollup-android-arm-eabi': 4.60.4 + '@rollup/rollup-android-arm64': 4.60.4 + '@rollup/rollup-darwin-arm64': 4.60.4 + '@rollup/rollup-darwin-x64': 4.60.4 + '@rollup/rollup-freebsd-arm64': 4.60.4 + '@rollup/rollup-freebsd-x64': 4.60.4 + '@rollup/rollup-linux-arm-gnueabihf': 4.60.4 + '@rollup/rollup-linux-arm-musleabihf': 4.60.4 + '@rollup/rollup-linux-arm64-gnu': 4.60.4 + '@rollup/rollup-linux-arm64-musl': 4.60.4 + '@rollup/rollup-linux-loong64-gnu': 4.60.4 + '@rollup/rollup-linux-loong64-musl': 4.60.4 + '@rollup/rollup-linux-ppc64-gnu': 4.60.4 + '@rollup/rollup-linux-ppc64-musl': 4.60.4 + '@rollup/rollup-linux-riscv64-gnu': 4.60.4 + '@rollup/rollup-linux-riscv64-musl': 4.60.4 + '@rollup/rollup-linux-s390x-gnu': 4.60.4 + '@rollup/rollup-linux-x64-gnu': 4.60.4 + '@rollup/rollup-linux-x64-musl': 4.60.4 + '@rollup/rollup-openbsd-x64': 4.60.4 + '@rollup/rollup-openharmony-arm64': 4.60.4 + '@rollup/rollup-win32-arm64-msvc': 4.60.4 + '@rollup/rollup-win32-ia32-msvc': 4.60.4 + '@rollup/rollup-win32-x64-gnu': 4.60.4 + '@rollup/rollup-win32-x64-msvc': 4.60.4 + fsevents: 2.3.3 + sade@1.8.1: dependencies: mri: 1.2.0 + set-cookie-parser@3.1.0: {} + + sirv@3.0.2: + dependencies: + '@polka/url': 1.0.0-next.29 + mrmime: 2.0.1 + totalist: 3.0.1 + source-map-js@1.2.1: {} + supports-preserve-symlinks-flag@1.0.0: {} + svelte-check@4.4.7(picomatch@4.0.4)(svelte@5.55.5(@typescript-eslint/types@8.57.1))(typescript@6.0.3): dependencies: '@jridgewell/trace-mapping': 0.3.31 @@ -956,11 +1369,6 @@ snapshots: transitivePeerDependencies: - picomatch - svelte-spa-router@5.1.0(svelte@5.55.5(@typescript-eslint/types@8.57.1)): - dependencies: - regexparam: 2.0.2 - svelte: 5.55.5(@typescript-eslint/types@8.57.1) - svelte@5.55.5(@typescript-eslint/types@8.57.1): dependencies: '@jridgewell/remapping': 2.3.5 @@ -982,19 +1390,13 @@ snapshots: transitivePeerDependencies: - '@typescript-eslint/types' - tauri-plugin-discord-rpc-api@https://codeload.github.com/Youwes09/tauri-plugin-discord-rpc/tar.gz/d2fd312945d0573153e0e7e2d2dfb131acecc52c: - dependencies: - '@tauri-apps/api': 2.11.0 - - tauri-plugin-drpc@1.0.3(typescript@6.0.3): - dependencies: - typescript: 6.0.3 - tinyglobby@0.2.16: dependencies: fdir: 6.5.0(picomatch@4.0.4) picomatch: 4.0.4 + totalist@3.0.1: {} + tslib@2.8.1: optional: true diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml new file mode 100644 index 0000000..efc037a --- /dev/null +++ b/pnpm-workspace.yaml @@ -0,0 +1,2 @@ +onlyBuiltDependencies: + - esbuild diff --git a/src/app.css b/src/app.css new file mode 100644 index 0000000..027fdb2 --- /dev/null +++ b/src/app.css @@ -0,0 +1,340 @@ +:root { + --bg-void: #080808; + --bg-base: #0c0c0c; + --bg-surface: #101010; + --bg-raised: #151515; + --bg-overlay: #1a1a1a; + --bg-subtle: #202020; + + --border-dim: #1c1c1c; + --border-base: #242424; + --border-strong: #2e2e2e; + --border-focus: #4a5c4a; + + --text-primary: #f0efec; + --text-secondary: #c8c6c0; + --text-muted: #8a8880; + --text-faint: #4e4d4a; + --text-disabled: #2a2a28; + + --accent: #6b8f6b; + --accent-dim: #2a3d2a; + --accent-muted: #1a251a; + --accent-fg: #a8c4a8; + --accent-bright: #8fb88f; + + --color-error: #c47a7a; + --color-error-bg: #1f1212; + --color-success: #7aab7a; + --color-info: #7a9ec4; + --color-info-bg: #121a1f; + --color-read: #2e2e2c; + + --dot-active: var(--accent); + --dot-inactive: var(--text-faint); + + --t-fast: 0.08s ease; + --t-base: 0.14s ease; + --t-slow: 0.22s ease; + + --radius-sm: 3px; + --radius-md: 5px; + --radius-lg: 7px; + --radius-xl: 10px; + --radius-2xl: 14px; + --radius-full: 9999px; + + --sp-1: 4px; + --sp-2: 8px; + --sp-3: 12px; + --sp-4: 16px; + --sp-5: 20px; + --sp-6: 24px; + --sp-8: 32px; + --sp-10: 40px; + + --sidebar-width: 52px; + --titlebar-height: 36px; + + --font-ui: "DM Mono", "Fira Mono", ui-monospace, monospace; + --font-sans: "DM Sans", ui-sans-serif, system-ui, sans-serif; + + --text-2xs: 10px; + --text-xs: 11px; + --text-sm: 12px; + --text-base: 13px; + --text-md: 14px; + --text-lg: 15px; + --text-xl: 17px; + --text-2xl: 20px; + --text-3xl: 24px; + + --weight-normal: 400; + --weight-medium: 500; + --weight-semi: 600; + + --leading-none: 1; + --leading-tight: 1.3; + --leading-snug: 1.45; + --leading-base: 1.6; + + --tracking-tight: -0.02em; + --tracking-normal: 0; + --tracking-wide: 0.06em; + --tracking-wider: 0.1em; + + --z-reader: 50; + --z-modal: 100; + --z-settings: 150; +} + +[data-theme="dark"] { + --bg-void: #000000; + --bg-base: #080808; + --bg-surface: #0d0d0d; + --bg-raised: #111111; + --bg-overlay: #171717; + --bg-subtle: #1e1e1e; + + --border-dim: #252525; + --border-base: #303030; + --border-strong: #3e3e3e; + --border-focus: #5a7a5a; + + --text-primary: #ffffff; + --text-secondary: #e8e6e0; + --text-muted: #b0aea8; + --text-faint: #6e6c68; + --text-disabled: #303030; + + --accent: #7aaa7a; + --accent-dim: #2e4a2e; + --accent-muted: #1e2e1e; + --accent-fg: #bcd8bc; + --accent-bright: #9fcf9f; +} + +[data-theme="light"] { + --bg-void: #d8d4ce; + --bg-base: #e2deda; + --bg-surface: #ece8e2; + --bg-raised: #f5f2ec; + --bg-overlay: #ffffff; + --bg-subtle: #e4e0d8; + + --border-dim: #c4c0b8; + --border-base: #b0aca4; + --border-strong: #989490; + --border-focus: #3a5a3a; + + --text-primary: #080806; + --text-secondary: #181612; + --text-muted: #38342e; + --text-faint: #706c64; + --text-disabled: #b0aca4; + + --accent: #2a5a2a; + --accent-dim: #b0ccb0; + --accent-muted: #c8dcc8; + --accent-fg: #183818; + --accent-bright: #1e4e1e; + + --color-error: #8a1a1a; + --color-error-bg: #f8e0e0; + --color-read: #e0dcd4; +} + +[data-theme="midnight"] { + --bg-void: #050810; + --bg-base: #080c18; + --bg-surface: #0c1020; + --bg-raised: #101428; + --bg-overlay: #151a30; + --bg-subtle: #1a2038; + + --border-dim: #1a2035; + --border-base: #222840; + --border-strong: #2c3450; + --border-focus: #4a5c8a; + + --text-primary: #eeeef8; + --text-secondary: #c0c4d8; + --text-muted: #808498; + --text-faint: #404860; + --text-disabled: #202840; + + --accent: #6a7ab8; + --accent-dim: #252d50; + --accent-muted: #181e38; + --accent-fg: #a8b4e8; + --accent-bright: #8896d0; +} + +[data-theme="original"] { + --bg-void: #080808; + --bg-base: #0c0c0c; + --bg-surface: #101010; + --bg-raised: #151515; + --bg-overlay: #1a1a1a; + --bg-subtle: #202020; + + --border-dim: #1c1c1c; + --border-base: #242424; + --border-strong: #2e2e2e; + --border-focus: #4a5c4a; + + --text-primary: #f0efec; + --text-secondary: #c8c6c0; + --text-muted: #8a8880; + --text-faint: #4e4d4a; + --text-disabled: #2a2a28; + + --accent: #6b8f6b; + --accent-dim: #2a3d2a; + --accent-muted: #1a251a; + --accent-fg: #a8c4a8; + --accent-bright: #8fb88f; + + --color-error: #c47a7a; + --color-error-bg: #1f1212; + --color-success: #7aab7a; + --color-info: #7a9ec4; + --color-info-bg: #121a1f; +} + +[data-theme="warm"] { + --bg-void: #0c0a06; + --bg-base: #100e08; + --bg-surface: #16130c; + --bg-raised: #1c1810; + --bg-overlay: #221e14; + --bg-subtle: #28241a; + + --border-dim: #201c10; + --border-base: #2c2818; + --border-strong: #3a3420; + --border-focus: #6a5a30; + + --text-primary: #f5f0e0; + --text-secondary: #d8d0b0; + --text-muted: #988c60; + --text-faint: #584e30; + --text-disabled: #302a18; + + --accent: #c0902a; + --accent-dim: #3a2c10; + --accent-muted: #261e0c; + --accent-fg: #e0b860; + --accent-bright: #d0a040; +} + +*, *::before, *::after { + box-sizing: border-box; + margin: 0; + padding: 0; +} + +html, body { + height: 100%; + overflow: hidden; + background: var(--bg-void); + color: var(--text-primary); +} + +#svelte { + height: 100%; +} + +button { + cursor: pointer; + font: inherit; + color: inherit; + background: none; + border: none; + padding: 0; +} + +input, textarea, select { + font: inherit; + color: inherit; +} + +a { + color: inherit; + text-decoration: none; +} + +ul, ol { list-style: none; } + +img, svg { display: block; max-width: 100%; } + +p { margin: 0; } + +body { + font-family: var(--font-sans); + font-size: var(--text-base); + font-weight: var(--weight-normal); + line-height: var(--leading-base); + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + text-rendering: optimizeLegibility; +} + +* { + scrollbar-width: thin; + scrollbar-color: transparent transparent; +} + +*::-webkit-scrollbar { width: 4px; height: 4px; } +*::-webkit-scrollbar-track { background: transparent; } +*::-webkit-scrollbar-thumb { background: transparent; border-radius: 99px; } +*::-webkit-scrollbar-thumb:hover { background: transparent; } + +@keyframes fadeIn { + from { opacity: 0; } + to { opacity: 1; } +} + +@keyframes fadeUp { + from { opacity: 0; transform: translateY(5px); } + to { opacity: 1; transform: translateY(0); } +} + +@keyframes fadeDown { + from { opacity: 0; transform: translateY(-5px); } + to { opacity: 1; transform: translateY(0); } +} + +@keyframes scaleIn { + from { opacity: 0; transform: scale(0.97); } + to { opacity: 1; transform: scale(1); } +} + +@keyframes spin { + from { transform: rotate(0deg); } + to { transform: rotate(360deg); } +} + +@keyframes pulse { + 0%, 100% { opacity: 1; } + 50% { opacity: 0.35; } +} + +@keyframes shimmer { + from { background-position: -200% 0; } + to { background-position: 200% 0; } +} + +.anim-fade-in { animation: fadeIn 0.14s ease both; } +.anim-fade-up { animation: fadeUp 0.18s ease both; } +.anim-fade-down { animation: fadeDown 0.18s ease both; } +.anim-scale-in { animation: scaleIn 0.14s ease both; } +.anim-pulse { animation: pulse 1.6s ease infinite; } +.anim-spin { animation: spin 0.7s linear infinite; } + +.skeleton { + background: linear-gradient(90deg, var(--bg-raised) 25%, var(--bg-overlay) 50%, var(--bg-raised) 75%); + background-size: 200% 100%; + animation: shimmer 1.4s ease infinite; + border-radius: var(--radius-sm); +} diff --git a/src/app.d.ts b/src/app.d.ts new file mode 100644 index 0000000..1a3a8f8 --- /dev/null +++ b/src/app.d.ts @@ -0,0 +1,4 @@ +declare global { + namespace App {} +} +export {}; \ No newline at end of file diff --git a/src/app.html b/src/app.html new file mode 100644 index 0000000..08d1b2d --- /dev/null +++ b/src/app.html @@ -0,0 +1,12 @@ + + + + + + + %sveltekit.head% + + +
%sveltekit.body%
+ + \ No newline at end of file diff --git a/src/hooks.client.ts b/src/hooks.client.ts new file mode 100644 index 0000000..73d5fb9 --- /dev/null +++ b/src/hooks.client.ts @@ -0,0 +1,50 @@ +import { initRequestManager } from '$lib/request-manager' +import { initPlatformService } from '$lib/platform-service' +import { appState } from '$lib/state/app.svelte' + +function isTauri(): boolean { + return '__TAURI_INTERNALS__' in window +} + +function isCapacitor(): boolean { + return 'Capacitor' in window +} + +async function resolvePlatformAdapter() { + if (isTauri()) { + const { TauriAdapter } = await import('$lib/platform-adapters/tauri') + return new TauriAdapter() + } + if (isCapacitor()) { + const { CapacitorAdapter } = await import('$lib/platform-adapters/capacitor') + return new CapacitorAdapter() + } + const { WebAdapter } = await import('$lib/platform-adapters/web') + return new WebAdapter() +} + +async function resolveServerAdapter() { + const { SuwayomiAdapter } = await import('$lib/server-adapters/suwayomi') + return new SuwayomiAdapter() +} + +async function boot() { + try { + const [serverAdapter, platformAdapter] = await Promise.all([ + resolveServerAdapter(), + resolvePlatformAdapter(), + ]) + + initRequestManager(serverAdapter) + initPlatformService(platformAdapter) + + appState.platform = isTauri() ? 'tauri' : isCapacitor() ? 'capacitor' : 'web' + appState.version = await platformAdapter.getVersion() + appState.status = 'ready' + } catch (e) { + appState.error = String(e) + appState.status = 'error' + } +} + +boot() diff --git a/src/lib/assets/favicon.svg b/src/lib/assets/favicon.svg new file mode 100644 index 0000000..f522609 --- /dev/null +++ b/src/lib/assets/favicon.svg @@ -0,0 +1,22 @@ + + + + + + diff --git a/src/lib/core/algorithms/filter.ts b/src/lib/core/algorithms/filter.ts new file mode 100644 index 0000000..f5ecd05 --- /dev/null +++ b/src/lib/core/algorithms/filter.ts @@ -0,0 +1,5 @@ +export { shouldHideNsfw, shouldHideSource, dedupeSourcesByLang } from "@core/util"; + +export function buildFilter(...predicates: ((item: T) => boolean)[]): (item: T) => boolean { + return (item) => predicates.every((p) => p(item)); +} \ No newline at end of file diff --git a/src/lib/core/algorithms/paginate.ts b/src/lib/core/algorithms/paginate.ts new file mode 100644 index 0000000..979dab8 --- /dev/null +++ b/src/lib/core/algorithms/paginate.ts @@ -0,0 +1,29 @@ +export interface PaginationState { + visible: number; +} + +export interface PaginationResult { + items: T[]; + hasMore: boolean; + remaining: number; +} + +export function createPaginator(pageSize: number) { + return { + slice(all: T[], visible: number): PaginationResult { + return { + items: all.slice(0, visible), + hasMore: all.length > visible, + remaining: Math.max(0, all.length - visible), + }; + }, + + nextVisible(current: number): number { + return current + pageSize; + }, + + reset(): number { + return pageSize; + }, + }; +} diff --git a/src/lib/core/algorithms/queue.ts b/src/lib/core/algorithms/queue.ts new file mode 100644 index 0000000..81a6a8c --- /dev/null +++ b/src/lib/core/algorithms/queue.ts @@ -0,0 +1,29 @@ +export interface AsyncQueue { + enqueue(item: T): void; + drain(): void; + clear(): void; + size(): number; +} + +export function createAsyncQueue( + worker: (item: T) => Promise, + concurrency = 1, +): AsyncQueue { + const queue: T[] = []; + let active = 0; + + function next() { + while (active < concurrency && queue.length > 0) { + const item = queue.shift()!; + active++; + worker(item).finally(() => { active--; next(); }); + } + } + + return { + enqueue(item) { queue.push(item); next(); }, + drain() { next(); }, + clear() { queue.length = 0; }, + size() { return queue.length; }, + }; +} diff --git a/src/lib/core/algorithms/search.ts b/src/lib/core/algorithms/search.ts new file mode 100644 index 0000000..0c92805 --- /dev/null +++ b/src/lib/core/algorithms/search.ts @@ -0,0 +1,33 @@ +export interface SearchResult { + item: T; + score: number; +} + +export function searchItems( + items: T[], + query: string, + getField: (item: T) => string, +): T[] { + const q = query.trim().toLowerCase(); + if (!q) return items; + return items.filter(item => getField(item).toLowerCase().includes(q)); +} + +export function searchWithScore( + items: T[], + query: string, + getField: (item: T) => string, +): SearchResult[] { + const q = query.trim().toLowerCase(); + if (!q) return items.map(item => ({ item, score: 0 })); + + return items + .map(item => { + const field = getField(item).toLowerCase(); + if (!field.includes(q)) return null; + const score = field === q ? 2 : field.startsWith(q) ? 1 : 0; + return { item, score }; + }) + .filter((r): r is SearchResult => r !== null) + .sort((a, b) => b.score - a.score); +} diff --git a/src/lib/core/algorithms/sort.ts b/src/lib/core/algorithms/sort.ts new file mode 100644 index 0000000..d148045 --- /dev/null +++ b/src/lib/core/algorithms/sort.ts @@ -0,0 +1,32 @@ +export type SortDir = "asc" | "desc"; + +export interface SortField { + key: string; + comparator: (a: T, b: T, context?: Record) => number; +} + +export interface SortConfig { + fields: SortField[]; + defaultField: string; + defaultDir: SortDir; +} + +export interface Sorter { + sort(items: T[], field: string, dir: SortDir, context?: Record): T[]; +} + +export function createSorter(config: SortConfig): Sorter { + const fieldMap = new Map(config.fields.map(f => [f.key, f])); + + return { + sort(items, field, dir, context) { + const f = fieldMap.get(field) ?? fieldMap.get(config.defaultField); + if (!f) return [...items]; + const d = dir ?? config.defaultDir; + return [...items].sort((a, b) => { + const cmp = f.comparator(a, b, context); + return d === "asc" ? cmp : -cmp; + }); + }, + }; +} diff --git a/src/lib/core/cache/memoryCache.ts b/src/lib/core/cache/memoryCache.ts new file mode 100644 index 0000000..1cc6431 --- /dev/null +++ b/src/lib/core/cache/memoryCache.ts @@ -0,0 +1,44 @@ +interface MemEntry { + value: T; + expiresAt: number; + key: string; +} + +export class MemoryCache { + readonly #cap: number; + readonly #ttl: number; + readonly #map = new Map>(); + + constructor(capacity: number, ttlMs: number) { + this.#cap = capacity; + this.#ttl = ttlMs; + } + + get(key: string): T | undefined { + const entry = this.#map.get(key); + if (!entry) return undefined; + if (Date.now() > entry.expiresAt) { this.#map.delete(key); return undefined; } + this.#map.delete(key); + this.#map.set(key, entry); + return entry.value; + } + + set(key: string, value: T): void { + if (this.#map.has(key)) this.#map.delete(key); + else if (this.#map.size >= this.#cap) this.#map.delete(this.#map.keys().next().value!); + this.#map.set(key, { value, expiresAt: Date.now() + this.#ttl, key }); + } + + has(key: string): boolean { + const entry = this.#map.get(key); + if (!entry) return false; + if (Date.now() > entry.expiresAt) { this.#map.delete(key); return false; } + return true; + } + + delete(key: string): void { this.#map.delete(key); } + + clear(): void { this.#map.clear(); } + + get size(): number { return this.#map.size; } +} \ No newline at end of file diff --git a/src/lib/core/cache/queryCache.ts b/src/lib/core/cache/queryCache.ts new file mode 100644 index 0000000..1baae89 --- /dev/null +++ b/src/lib/core/cache/queryCache.ts @@ -0,0 +1,241 @@ +interface Entry { + promise: Promise; + fetchedAt: number; + fetcher?: () => Promise; + ttl?: number; +} + +const store = new Map>(); +const subs = new Map void>>(); +const keyToGroups = new Map>(); +const groups = new Map>(); + +export const DEFAULT_TTL_MS = 5 * 60 * 1_000; + +function notify(key: string) { subs.get(key)?.forEach(cb => cb()); } + +function registerGroups(key: string, group?: string | string[]) { + if (!group) return; + for (const tag of Array.isArray(group) ? group : [group]) { + if (!groups.has(tag)) groups.set(tag, new Set()); + groups.get(tag)!.add(key); + if (!keyToGroups.has(key)) keyToGroups.set(key, new Set()); + keyToGroups.get(key)!.add(tag); + } +} + +function unregisterKey(key: string) { + const tags = keyToGroups.get(key); + if (tags) { + for (const tag of tags) groups.get(tag)?.delete(key); + keyToGroups.delete(key); + } +} + +export const cache = { + get(key: string, fetcher: () => Promise, ttl = DEFAULT_TTL_MS, group?: string | string[]): Promise { + const existing = store.get(key) as Entry | undefined; + if (existing && Date.now() - existing.fetchedAt < ttl) return existing.promise; + const promise = fetcher().catch(err => { + if (err?.name !== "AbortError") store.delete(key); + return Promise.reject(err); + }) as Promise; + store.set(key, { promise, fetchedAt: Date.now(), fetcher: fetcher as () => Promise, ttl }); + registerGroups(key, group); + promise.then(() => notify(key)).catch(() => {}); + return promise; + }, + + set(key: string, value: T, group?: string | string[]) { + const existing = store.get(key) as Entry | undefined; + store.set(key, { + promise: Promise.resolve(value), + fetchedAt: Date.now(), + fetcher: existing?.fetcher, + ttl: existing?.ttl, + }); + registerGroups(key, group); + notify(key); + }, + + update(key: string, fn: (prev: T) => T) { + const existing = store.get(key) as Entry | undefined; + if (!existing) return; + const next = existing.promise.then(fn); + store.set(key, { ...existing, promise: next, fetchedAt: Date.now() }); + next.then(() => notify(key)).catch(() => {}); + }, + + refresh(key: string): Promise | undefined { + const existing = store.get(key) as Entry | undefined; + if (!existing?.fetcher) return undefined; + const promise = (existing.fetcher as () => Promise)().catch(err => { + if (err?.name !== "AbortError") store.delete(key); + return Promise.reject(err); + }); + store.set(key, { ...existing, promise: promise as Promise, fetchedAt: Date.now() }); + promise.then(() => notify(key)).catch(() => {}); + return promise; + }, + + refreshGroup(tag: string): void { + const keys = groups.get(tag); + if (!keys) return; + for (const key of [...keys]) { + const existing = store.get(key); + if (existing?.fetcher) { + const promise = existing.fetcher().catch(err => { + if (err?.name !== "AbortError") store.delete(key); + return Promise.reject(err); + }); + store.set(key, { ...existing, promise, fetchedAt: Date.now() }); + promise.then(() => notify(key)).catch(() => {}); + } + } + }, + + has(key: string): boolean { return store.has(key); }, + + ageOf(key: string): number | undefined { + const e = store.get(key); + return e ? Date.now() - e.fetchedAt : undefined; + }, + + isStale(key: string): boolean { + const e = store.get(key); + if (!e) return true; + return Date.now() - e.fetchedAt >= (e.ttl ?? DEFAULT_TTL_MS); + }, + + clear(key: string) { + unregisterKey(key); + store.delete(key); + notify(key); + }, + + clearGroup(tag: string) { + const keys = groups.get(tag); + if (!keys) return; + for (const key of [...keys]) { + keyToGroups.get(key)?.delete(tag); + if (keyToGroups.get(key)?.size === 0) keyToGroups.delete(key); + store.delete(key); + notify(key); + } + groups.delete(tag); + }, + + clearAll() { + const allKeys = [...store.keys()]; + store.clear(); + groups.clear(); + keyToGroups.clear(); + allKeys.forEach(notify); + }, + + subscribe(key: string, cb: () => void): () => void { + if (!subs.has(key)) subs.set(key, new Set()); + subs.get(key)!.add(cb); + return () => subs.get(key)?.delete(cb); + }, +}; + +export const CACHE_GROUPS = { + LIBRARY: "g:library", + SOURCES: "g:sources", +} as const; + +export const CACHE_KEYS = { + LIBRARY: "library", + RECENT_UPDATES: "recent_updates", + ALL_MANGA: "all_manga_unfiltered", + CATEGORIES: "categories", + SEARCH: "search_all_manga", + SOURCES: "sources", + POPULAR: "popular", + GENRE: (genre: string) => `genre:${genre}`, + MANGA: (id: number) => `manga:${id}`, + CHAPTERS: (id: number) => `chapters:${id}`, + + sourceMangaPages(sourceId: string, type: "POPULAR" | "LATEST" | "SEARCH", query?: string | string[]): string { + const q = Array.isArray(query) ? [...query].sort().join("+") : (query ?? ""); + return `pages:${sourceId}:${type}:${q}`; + }, + + sourceMangaPage(sourceId: string, type: "POPULAR" | "LATEST" | "SEARCH", page: number, query?: string | string[]): string { + const q = Array.isArray(query) ? [...query].sort().join("+") : (query ?? ""); + return `page:${sourceId}:${type}:${page}:${q}`; + }, +} as const; + +const inflight = new Map>(); + +export function deduped(key: string, fetcher: () => Promise): Promise { + if (inflight.has(key)) return inflight.get(key) as Promise; + const p = fetcher().finally(() => inflight.delete(key)); + inflight.set(key, p); + return p; +} + +const _pageSets = new Map>(); + +export interface PageSet { + add(page: number): void; + pages(): Set; + next(): number; + clear(): void; +} + +export function getPageSet(sourceId: string, type: "POPULAR" | "LATEST" | "SEARCH", query?: string | string[]): PageSet { + const key = CACHE_KEYS.sourceMangaPages(sourceId, type, query); + return { + add(page) { if (!_pageSets.has(key)) _pageSets.set(key, new Set()); _pageSets.get(key)!.add(page); }, + pages() { return new Set(_pageSets.get(key) ?? []); }, + next() { const s = _pageSets.get(key); return s?.size ? Math.max(...s) + 1 : 1; }, + clear() { _pageSets.delete(key); }, + }; +} + +const FRECENCY_KEY = "moku-source-frecency"; +const MAX_FRECENCY_SOURCES = 4; +type FrecencyMap = Record; + +function loadFrecency(): FrecencyMap { + try { const r = localStorage.getItem(FRECENCY_KEY); return r ? JSON.parse(r) : {}; } + catch { return {}; } +} + +function saveFrecency(map: FrecencyMap) { + try { localStorage.setItem(FRECENCY_KEY, JSON.stringify(map)); } catch {} +} + +export function recordSourceAccess(sourceId: string) { + if (!sourceId || sourceId === "0") return; + const map = loadFrecency(); + map[sourceId] = (map[sourceId] ?? 0) + 1; + saveFrecency(map); +} + +export function getTopSources(sources: T[]): T[] { + const map = loadFrecency(); + const withScore = sources.map(s => ({ s, score: map[s.id] ?? 0 })); + if (withScore.some(x => x.score > 0)) { + return withScore.sort((a, b) => b.score - a.score).slice(0, MAX_FRECENCY_SOURCES).map(x => x.s); + } + return sources.slice(0, MAX_FRECENCY_SOURCES); +} + +export async function refreshMangaCache(mangaId: number, thumbnailUrl?: string): Promise { + const didRefresh = cache.refresh(CACHE_KEYS.MANGA(mangaId)); + if (!didRefresh) cache.clear(CACHE_KEYS.MANGA(mangaId)); + + cache.clear(CACHE_KEYS.CHAPTERS(mangaId)); + cache.clear(CACHE_KEYS.LIBRARY); + cache.clear(CACHE_KEYS.ALL_MANGA); + + if (thumbnailUrl) { + const { revokeBlobUrl, getBlobUrl } = await import("@core/cache/imageCache"); + revokeBlobUrl(thumbnailUrl); + getBlobUrl(thumbnailUrl, 999).catch(() => {}); + } +} \ No newline at end of file diff --git a/src/lib/core/cover/autoLinkWorker.ts b/src/lib/core/cover/autoLinkWorker.ts new file mode 100644 index 0000000..b5bae69 --- /dev/null +++ b/src/lib/core/cover/autoLinkWorker.ts @@ -0,0 +1,29 @@ +interface WorkerMsg { + focalTitle: string; + focalId: number; + allManga: { id: number; title: string }[]; + linkedIds: number[]; +} + +function titleSimilarity(a: string, b: string): number { + const norm = (s: string) => + s.toLowerCase().replace(/[^a-z0-9\s]/g, "").split(/\s+/).filter(Boolean); + const wa = new Set(norm(a)); + const wb = new Set(norm(b)); + if (!wa.size || !wb.size) return 0; + const intersection = [...wa].filter(w => wb.has(w)).length; + return intersection / new Set([...wa, ...wb]).size; +} + +self.onmessage = (e: MessageEvent) => { + const { focalTitle, focalId, allManga, linkedIds } = e.data; + const matches: number[] = []; + + for (const m of allManga) { + if (m.id === focalId) continue; + if (linkedIds.includes(m.id)) continue; + if (titleSimilarity(focalTitle, m.title) >= 0.65) matches.push(m.id); + } + + self.postMessage(matches); +}; \ No newline at end of file diff --git a/src/lib/core/cover/coverHash.ts b/src/lib/core/cover/coverHash.ts new file mode 100644 index 0000000..5dbf7c2 --- /dev/null +++ b/src/lib/core/cover/coverHash.ts @@ -0,0 +1,54 @@ +const THUMB_SIZE = 16; +const DUPE_THRESH = 0.12; + +const hashCache = new Map(); + +function toGray(data: Uint8ClampedArray, pixels: number): Uint8ClampedArray { + const gray = new Uint8ClampedArray(pixels); + for (let i = 0; i < pixels; i++) { + const o = i * 4; + gray[i] = (data[o] * 299 + data[o + 1] * 587 + data[o + 2] * 114) / 1000; + } + return gray; +} + +function loadThumb(url: string): Promise { + return new Promise((resolve, reject) => { + const img = new Image(); + img.crossOrigin = "anonymous"; + img.onload = () => { + const canvas = document.createElement("canvas"); + canvas.width = canvas.height = THUMB_SIZE; + const ctx = canvas.getContext("2d")!; + ctx.drawImage(img, 0, 0, THUMB_SIZE, THUMB_SIZE); + resolve(toGray(ctx.getImageData(0, 0, THUMB_SIZE, THUMB_SIZE).data, THUMB_SIZE * THUMB_SIZE)); + }; + img.onerror = reject; + img.src = url; + }); +} + +function similarity(a: Uint8ClampedArray, b: Uint8ClampedArray): number { + let diff = 0; + for (let i = 0; i < a.length; i++) diff += Math.abs(a[i] - b[i]); + return diff / (a.length * 255); +} + +export async function getHash(url: string): Promise { + if (hashCache.has(url)) return hashCache.get(url)!; + try { + const thumb = await loadThumb(url); + hashCache.set(url, thumb); + return thumb; + } catch { + return null; + } +} + +export function areDuplicates(a: Uint8ClampedArray, b: Uint8ClampedArray): boolean { + return similarity(a, b) <= DUPE_THRESH; +} + +export function clearHashCache(): void { + hashCache.clear(); +} \ No newline at end of file diff --git a/src/lib/core/keybinds/defaultBinds.ts b/src/lib/core/keybinds/defaultBinds.ts new file mode 100644 index 0000000..83cac86 --- /dev/null +++ b/src/lib/core/keybinds/defaultBinds.ts @@ -0,0 +1,50 @@ +export interface Keybinds { + turnPageRight: string; + turnPageLeft: string; + firstPage: string; + lastPage: string; + turnChapterRight: string; + turnChapterLeft: string; + exitReader: string; + toggleReadingDirection: string; + togglePageStyle: string; + toggleFullscreen: string; + openSettings: string; + toggleBookmark: string; + toggleMarker: string; + toggleAutoScroll: string; +} + +export const DEFAULT_KEYBINDS: Keybinds = { + turnPageRight: "ArrowRight", + turnPageLeft: "ArrowLeft", + firstPage: "ctrl+ArrowLeft", + lastPage: "ctrl+ArrowRight", + turnChapterRight: "]", + turnChapterLeft: "[", + exitReader: "Backspace", + toggleReadingDirection: "d", + togglePageStyle: "q", + toggleFullscreen: "f", + openSettings: "o", + toggleBookmark: "m", + toggleMarker: "n", + toggleAutoScroll: "s", +}; + +export const KEYBIND_LABELS: Record = { + turnPageRight: "Turn page right (→)", + turnPageLeft: "Turn page left (←)", + firstPage: "Jump to first page", + lastPage: "Jump to last page", + turnChapterRight: "Turn chapter right (→)", + turnChapterLeft: "Turn chapter left (←)", + exitReader: "Exit reader", + toggleReadingDirection: "Toggle reading direction", + togglePageStyle: "Toggle page style", + toggleFullscreen: "Toggle fullscreen", + openSettings: "Open settings", + toggleBookmark: "Toggle bookmark", + toggleMarker: "Toggle marker", + toggleAutoScroll: "Toggle auto scroll", +}; \ No newline at end of file diff --git a/src/lib/core/keybinds/keybindEngine.ts b/src/lib/core/keybinds/keybindEngine.ts new file mode 100644 index 0000000..5c1487e --- /dev/null +++ b/src/lib/core/keybinds/keybindEngine.ts @@ -0,0 +1,14 @@ +export function eventToKeybind(e: KeyboardEvent): string { + if (["Control", "Alt", "Shift", "Meta"].includes(e.key)) return ""; + const parts: string[] = []; + if (e.ctrlKey) parts.push("ctrl"); + if (e.altKey) parts.push("alt"); + if (e.shiftKey) parts.push("shift"); + if (e.metaKey) parts.push("meta"); + parts.push(e.key); + return parts.join("+"); +} + +export function matchesKeybind(e: KeyboardEvent, bind: string): boolean { + return eventToKeybind(e) === bind; +} diff --git a/src/lib/core/persistence/credentialVault.ts b/src/lib/core/persistence/credentialVault.ts new file mode 100644 index 0000000..3289193 --- /dev/null +++ b/src/lib/core/persistence/credentialVault.ts @@ -0,0 +1,88 @@ +const VAULT_KEY = "moku-credential-vault"; +const SALT_ITERATIONS = 200_000; +const KEY_USAGE: KeyUsage[] = ["encrypt", "decrypt"]; + +export interface VaultPayload { + refreshToken?: string; + basicUser?: string; + basicPass?: string; + authMode: "UI_LOGIN" | "BASIC_AUTH" | "NONE"; +} + +interface StoredVault { + salt: string; + iv: string; + data: string; +} + +function toB64(buf: ArrayBuffer): string { + return btoa(String.fromCharCode(...new Uint8Array(buf))); +} + +function fromB64(s: string): Uint8Array { + return Uint8Array.from(atob(s), (c) => c.charCodeAt(0)); +} + +async function deriveKey(pin: string, salt: Uint8Array): Promise { + const enc = new TextEncoder(); + const keyMat = await crypto.subtle.importKey("raw", enc.encode(pin), "PBKDF2", false, ["deriveKey"]); + return crypto.subtle.deriveKey( + { name: "PBKDF2", salt, iterations: SALT_ITERATIONS, hash: "SHA-256" }, + keyMat, + { name: "AES-GCM", length: 256 }, + false, + KEY_USAGE, + ); +} + +export function vaultExists(): boolean { + return !!localStorage.getItem(VAULT_KEY); +} + +export async function lockVault(pin: string, payload: VaultPayload): Promise { + const salt = crypto.getRandomValues(new Uint8Array(16)); + const iv = crypto.getRandomValues(new Uint8Array(12)); + const key = await deriveKey(pin, salt); + + const enc = new TextEncoder(); + const cipher = await crypto.subtle.encrypt( + { name: "AES-GCM", iv }, + key, + enc.encode(JSON.stringify(payload)), + ); + + localStorage.setItem(VAULT_KEY, JSON.stringify({ + salt: toB64(salt), + iv: toB64(iv), + data: toB64(cipher), + } satisfies StoredVault)); +} + +export async function unlockVault(pin: string): Promise { + const raw = localStorage.getItem(VAULT_KEY); + if (!raw) return null; + + try { + const stored = JSON.parse(raw) as StoredVault; + const key = await deriveKey(pin, fromB64(stored.salt)); + const plain = await crypto.subtle.decrypt( + { name: "AES-GCM", iv: fromB64(stored.iv) }, + key, + fromB64(stored.data), + ); + return JSON.parse(new TextDecoder().decode(plain)) as VaultPayload; + } catch { + return null; + } +} + +export function clearVault(): void { + localStorage.removeItem(VAULT_KEY); +} + +export async function rekeyVault(oldPin: string, newPin: string): Promise { + const payload = await unlockVault(oldPin); + if (!payload) return false; + await lockVault(newPin, payload); + return true; +} \ No newline at end of file diff --git a/src/lib/core/ui/touchscreen.ts b/src/lib/core/ui/touchscreen.ts new file mode 100644 index 0000000..72731fd --- /dev/null +++ b/src/lib/core/ui/touchscreen.ts @@ -0,0 +1,234 @@ +export interface LongPressOptions { + onLongPress: (e: PointerEvent) => void; + duration?: number; + moveThreshold?: number; +} + +export function longPress(node: HTMLElement, opts: LongPressOptions) { + const { onLongPress, duration = 500, moveThreshold = 8 } = opts; + let timer: ReturnType | null = null; + let startX = 0, startY = 0; + let fired = false; + + function start(e: PointerEvent) { + if (e.button !== 0 && e.pointerType === "mouse") return; + startX = e.clientX; startY = e.clientY; fired = false; + timer = setTimeout(() => { timer = null; fired = true; onLongPress(e); }, duration); + } + function move(e: PointerEvent) { + if (!timer) return; + const dx = e.clientX - startX, dy = e.clientY - startY; + if (Math.sqrt(dx * dx + dy * dy) > moveThreshold) cancel(); + } + function cancel() { if (timer) { clearTimeout(timer); timer = null; } } + + node.addEventListener("pointerdown", start); + node.addEventListener("pointermove", move); + node.addEventListener("pointerup", cancel); + node.addEventListener("pointerleave", cancel); + node.addEventListener("pointercancel",cancel); + + return { + get fired() { return fired; }, + destroy() { + cancel(); + node.removeEventListener("pointerdown", start); + node.removeEventListener("pointermove", move); + node.removeEventListener("pointerup", cancel); + node.removeEventListener("pointerleave", cancel); + node.removeEventListener("pointercancel",cancel); + }, + }; +} + +export interface TapOptions { + onTap: (e: PointerEvent) => void; + onDoubleTap?: (e: PointerEvent) => void; + doubleTapGap?: number; +} + +export function tap(node: HTMLElement, opts: TapOptions) { + const { onTap, onDoubleTap, doubleTapGap = 300 } = opts; + let lastTap = 0; + let pending: ReturnType | null = null; + let startX = 0, startY = 0; + const SLOP = 8; + + function down(e: PointerEvent) { startX = e.clientX; startY = e.clientY; } + function up(e: PointerEvent) { + const dx = e.clientX - startX, dy = e.clientY - startY; + if (Math.sqrt(dx * dx + dy * dy) > SLOP) return; + const now = Date.now(); + if (onDoubleTap && now - lastTap < doubleTapGap) { + if (pending) { clearTimeout(pending); pending = null; } + onDoubleTap(e); + lastTap = 0; + } else { + lastTap = now; + if (onDoubleTap) { + pending = setTimeout(() => { pending = null; onTap(e); }, doubleTapGap); + } else { + onTap(e); + } + } + } + + node.addEventListener("pointerdown", down); + node.addEventListener("pointerup", up); + return { destroy() { + node.removeEventListener("pointerdown", down); + node.removeEventListener("pointerup", up); + }}; +} + +export interface SwipeOptions { + onSwipeLeft?: (e: PointerEvent) => void; + onSwipeRight?: (e: PointerEvent) => void; + onSwipeUp?: (e: PointerEvent) => void; + onSwipeDown?: (e: PointerEvent) => void; + threshold?: number; + lockAxis?: boolean; +} + +export function swipe(node: HTMLElement, opts: SwipeOptions) { + const { onSwipeLeft, onSwipeRight, onSwipeUp, onSwipeDown, threshold = 40, lockAxis = true } = opts; + let startX = 0, startY = 0, active = false; + + function down(e: PointerEvent) { + if (e.pointerType === "mouse") return; + startX = e.clientX; startY = e.clientY; active = true; + node.setPointerCapture(e.pointerId); + } + function up(e: PointerEvent) { + if (!active) return; active = false; + const dx = e.clientX - startX, dy = e.clientY - startY; + const ax = Math.abs(dx), ay = Math.abs(dy); + if (Math.max(ax, ay) < threshold) return; + if (lockAxis && ax > ay) { + if (dx < 0) onSwipeLeft?.(e); else onSwipeRight?.(e); + } else if (lockAxis && ay >= ax) { + if (dy < 0) onSwipeUp?.(e); else onSwipeDown?.(e); + } else { + if (ax >= ay) { if (dx < 0) onSwipeLeft?.(e); else onSwipeRight?.(e); } + else { if (dy < 0) onSwipeUp?.(e); else onSwipeDown?.(e); } + } + } + function cancel() { active = false; } + + node.addEventListener("pointerdown", down); + node.addEventListener("pointerup", up); + node.addEventListener("pointercancel", cancel); + return { destroy() { + node.removeEventListener("pointerdown", down); + node.removeEventListener("pointerup", up); + node.removeEventListener("pointercancel", cancel); + }}; +} + +export interface PinchOptions { + onPinch: (scale: number, origin: { x: number; y: number }) => void; + onPinchEnd?: (scale: number) => void; +} + +export interface PinchGestureOptions { + onPinch: (scale: number, origin: { x: number; y: number }) => void; + onPinchEnd?: (scale: number) => void; +} + +export interface PinchGesture { + onPointerDown: (e: PointerEvent) => void; + onPointerMove: (e: PointerEvent) => void; + onPointerUp: (e: PointerEvent) => void; + isPinching: () => boolean; +} + +export function createPinchGesture(opts: PinchGestureOptions): PinchGesture { + const { onPinch, onPinchEnd } = opts; + const pointers = new Map(); + let initDist = 0; + + function pdist(a: PointerEvent, b: PointerEvent) { + const dx = a.clientX - b.clientX, dy = a.clientY - b.clientY; + return Math.sqrt(dx * dx + dy * dy); + } + function pmid(a: PointerEvent, b: PointerEvent) { + return { x: (a.clientX + b.clientX) / 2, y: (a.clientY + b.clientY) / 2 }; + } + + function onPointerDown(e: PointerEvent) { + pointers.set(e.pointerId, e); + if (pointers.size === 2) { + const [a, b] = [...pointers.values()]; + initDist = pdist(a, b); + } + } + function onPointerMove(e: PointerEvent) { + if (!pointers.has(e.pointerId)) return; + pointers.set(e.pointerId, e); + if (pointers.size !== 2 || initDist === 0) return; + const [a, b] = [...pointers.values()]; + onPinch(pdist(a, b) / initDist, pmid(a, b)); + } + function onPointerUp(e: PointerEvent) { + if (pointers.size === 2 && onPinchEnd) { + const [a, b] = [...pointers.values()]; + onPinchEnd(pdist(a, b) / initDist); + } + pointers.delete(e.pointerId); + initDist = 0; + } + + return { onPointerDown, onPointerMove, onPointerUp, isPinching: () => pointers.size >= 2 }; +} + +export function pinch(node: HTMLElement, opts: PinchOptions) { + const gesture = createPinchGesture(opts); + function down(e: PointerEvent) { node.setPointerCapture(e.pointerId); gesture.onPointerDown(e); } + node.addEventListener("pointerdown", down); + node.addEventListener("pointermove", gesture.onPointerMove); + node.addEventListener("pointerup", gesture.onPointerUp); + node.addEventListener("pointercancel", gesture.onPointerUp); + return { destroy() { + node.removeEventListener("pointerdown", down); + node.removeEventListener("pointermove", gesture.onPointerMove); + node.removeEventListener("pointerup", gesture.onPointerUp); + node.removeEventListener("pointercancel", gesture.onPointerUp); + }}; +} + +export interface DragScrollOptions { + direction?: "x" | "y" | "both"; + onDragStart?: () => void; + onDragEnd?: () => void; +} + +export function dragScroll(node: HTMLElement, opts: DragScrollOptions = {}) { + const { direction = "both", onDragStart, onDragEnd } = opts; + let active = false, startX = 0, startY = 0, scrollX = 0, scrollY = 0; + + function down(e: PointerEvent) { + if (e.pointerType === "mouse") return; + active = true; + startX = e.clientX; startY = e.clientY; + scrollX = node.scrollLeft; scrollY = node.scrollTop; + node.setPointerCapture(e.pointerId); + onDragStart?.(); + } + function move(e: PointerEvent) { + if (!active) return; + if (direction !== "x") node.scrollTop = scrollY - (e.clientY - startY); + if (direction !== "y") node.scrollLeft = scrollX - (e.clientX - startX); + } + function up() { if (active) { active = false; onDragEnd?.(); } } + + node.addEventListener("pointerdown", down); + node.addEventListener("pointermove", move); + node.addEventListener("pointerup", up); + node.addEventListener("pointercancel", up); + return { destroy() { + node.removeEventListener("pointerdown", down); + node.removeEventListener("pointermove", move); + node.removeEventListener("pointerup", up); + node.removeEventListener("pointercancel", up); + }}; +} \ No newline at end of file diff --git a/src/lib/core/ui/zoom.ts b/src/lib/core/ui/zoom.ts new file mode 100644 index 0000000..09f520b --- /dev/null +++ b/src/lib/core/ui/zoom.ts @@ -0,0 +1,53 @@ +let _appliedZoom: number = -1; +let _vhRafId: number | null = null; + +export function applyZoom(uiZoom: number) { + if (uiZoom === _appliedZoom) return; + _appliedZoom = uiZoom; + document.documentElement.style.setProperty("--ui-zoom", String(uiZoom)); + document.documentElement.style.setProperty("--ui-scale", String(uiZoom)); + document.documentElement.style.zoom = `${uiZoom * 100}%`; + if (_vhRafId !== null) cancelAnimationFrame(_vhRafId); + _vhRafId = requestAnimationFrame(() => { + _vhRafId = null; + document.documentElement.style.setProperty("--visual-vh", `${window.innerHeight / uiZoom}px`); + }); +} + +export function zoomDelta(e: KeyboardEvent, current: number): number | null { + if (!e.ctrlKey) return null; + if (e.key === "=" || e.key === "+") { e.preventDefault(); return Math.min(2.0, Math.round((current + 0.1) * 10) / 10); } + if (e.key === "-") { e.preventDefault(); return Math.max(0.5, Math.round((current - 0.1) * 10) / 10); } + if (e.key === "0") { e.preventDefault(); return 1.0; } + return null; +} + +export function clampZoom(z: number, min: number, max: number): number { + return Math.round(Math.min(max, Math.max(min, z)) * 1000) / 1000; +} + +export function captureZoomAnchor( + containerEl: HTMLElement | null, + style: string, + out: { el: HTMLElement | null; offset: number }, +) { + if (!containerEl || style !== "longstrip") return; + const containerTop = containerEl.getBoundingClientRect().top; + for (const img of containerEl.querySelectorAll("img[data-local-page]")) { + const rect = img.getBoundingClientRect(); + if (rect.bottom > containerTop) { out.el = img; out.offset = rect.top - containerTop; return; } + } +} + +export function restoreZoomAnchor( + containerEl: HTMLElement | null, + out: { el: HTMLElement | null; offset: number }, +) { + if (!out.el || !containerEl) return; + const el = out.el; + out.el = null; + requestAnimationFrame(() => { + const containerTop = containerEl!.getBoundingClientRect().top; + containerEl!.scrollTop += (el.getBoundingClientRect().top - containerTop) - out.offset; + }); +} diff --git a/src/lib/core/util.ts b/src/lib/core/util.ts new file mode 100644 index 0000000..35b9ab2 --- /dev/null +++ b/src/lib/core/util.ts @@ -0,0 +1,223 @@ +import type { Manga, Source } from "$lib/types"; +import type { Settings } from "$lib/types"; + +export { clsx as cn } from "clsx"; + +export function timeAgo(ts: number): string { + const diff = Date.now() - ts, m = Math.floor(diff / 60000); + if (m < 1) return "Just now"; + if (m < 60) return `${m}m ago`; + const h = Math.floor(m / 60); + if (h < 24) return `${h}h ago`; + const d = Math.floor(h / 24); + if (d < 7) return `${d}d ago`; + return new Date(ts).toLocaleDateString("en-US", { month: "short", day: "numeric" }); +} + +export function dayLabel(ts: number): string { + const d = new Date(ts), now = new Date(); + if (d.toDateString() === now.toDateString()) return "Today"; + const yest = new Date(now); yest.setDate(now.getDate() - 1); + if (d.toDateString() === yest.toDateString()) return "Yesterday"; + return d.toLocaleDateString("en-US", { weekday: "long", month: "long", day: "numeric" }); +} + +export function formatReadTime(m: number): string { + if (m < 1) return "< 1 min"; + if (m < 60) return `${m} min`; + const h = Math.floor(m / 60), r = m % 60; + return r === 0 ? `${h}h` : `${h}h ${r}m`; +} + +const STRICT_TAGS: string[] = [ + "adult", "mature", "hentai", "ecchi", "erotic", "pornograph", + "18+", "smut", "explicit", "sexual violence", + "gore", "guro", "graphic violence", "torture", "body horror", +]; + +const MODERATE_TAGS: string[] = [ + "adult", "mature", "hentai", "ecchi", "erotic", "pornograph", + "18+", "smut", "explicit", "sexual violence", +]; + +type ContentFilterSettings = Pick< + Settings, + "contentLevel" | "sourceOverridesEnabled" | "nsfwAllowedSourceIds" | "nsfwBlockedSourceIds" +>; + +function blockedTagsForSettings(settings: ContentFilterSettings): string[] { + if (settings.contentLevel === "strict") return STRICT_TAGS; + if (settings.contentLevel === "moderate") return MODERATE_TAGS; + return []; +} + +function genreMatchesBlocklist(genre: string[], blockedTags: string[]): boolean { + if (!blockedTags.length) return false; + return genre.some(g => { + const norm = g.toLowerCase().trim(); + return blockedTags.some(tag => { + const idx = norm.indexOf(tag); + if (idx === -1) return false; + const before = idx === 0 || /\W/.test(norm[idx - 1]); + const after = idx + tag.length === norm.length || /\W/.test(norm[idx + tag.length]); + return before && after; + }); + }); +} + +export function shouldHideNsfw( + manga: Pick, + settings: ContentFilterSettings, +): boolean { + if (settings.contentLevel === "unrestricted") return false; + + const srcId = manga.source?.id; + const blocked = settings.sourceOverridesEnabled ? (settings.nsfwBlockedSourceIds ?? []) : []; + const allowed = settings.sourceOverridesEnabled ? (settings.nsfwAllowedSourceIds ?? []) : []; + + if (srcId && blocked.includes(srcId)) return true; + + const sourceAllowed = !!(srcId && allowed.includes(srcId)); + + if (!sourceAllowed && manga.source?.isNsfw) return true; + + return genreMatchesBlocklist(manga.genre ?? [], blockedTagsForSettings(settings)); +} + +export function shouldHideSource( + source: Pick, + settings: ContentFilterSettings, +): boolean { + if (settings.contentLevel === "unrestricted") return false; + + if (settings.sourceOverridesEnabled) { + if ((settings.nsfwBlockedSourceIds ?? []).includes(source.id)) return true; + if ((settings.nsfwAllowedSourceIds ?? []).includes(source.id)) return false; + } + + return source.isNsfw; +} + +export function dedupeSourcesByLang( + sources: Source[], + preferredLang: string, + settings: ContentFilterSettings, + applyHide = false, +): Source[] { + const map = new Map(); + for (const s of sources) { + if (s.id === "0") continue; + if (applyHide && shouldHideSource(s, settings)) continue; + const existing = map.get(s.name); + if (!existing) { map.set(s.name, s); continue; } + const existingPref = existing.lang === preferredLang; + const newPref = s.lang === preferredLang; + if (newPref && !existingPref) map.set(s.name, s); + else if (!existingPref && !newPref && s.lang < existing.lang) map.set(s.name, s); + } + return Array.from(map.values()); +} + +export function dedupeSources(sources: Source[], preferredLang: string): Source[] { + const byName = new Map(); + for (const src of sources) { + if (src.id === "0") continue; + if (!byName.has(src.name)) byName.set(src.name, []); + byName.get(src.name)!.push(src); + } + const picked: Source[] = []; + for (const group of byName.values()) { + const preferred = group.find(s => s.lang === preferredLang); + picked.push(preferred ?? group.sort((a, b) => a.lang.localeCompare(b.lang))[0]); + } + return picked; +} + +export function normalizeTitle(title: string): string { + return title + .toLowerCase() + .replace(/\(official\)|\(web comic\)|\(webtoon\)|\(manhwa\)|\(manhua\)/gi, "") + .replace(/[^a-z0-9\s]/g, " ") + .replace(/^(the|a|an)\s+/, "") + .replace(/\s+/g, " ") + .trim(); +} + +function norm(s: string): string { + return s.toLowerCase().replace(/[^a-z0-9]/g, " ").replace(/\s+/g, " ").trim(); +} + +function descFingerprint(desc: string | null | undefined): string | null { + if (!desc) return null; + const n = norm(desc); + return n.length >= 60 ? n.slice(0, 200) : null; +} + +function authorFingerprint(author?: string | null, artist?: string | null): string | null { + const parts = [author, artist].filter(Boolean).map(s => norm(s!)); + return parts.length ? parts.sort().join("|") : null; +} + +export function dedupeMangaByTitle(items: T[], links: Record = {}): T[] { + const byTitle = new Map(); + const byDesc = new Map(); + const byAuthorDesc = new Map(); + const byId = new Map(); + const out: T[] = []; + + for (const m of items) { + const tk = normalizeTitle(m.title); + const dk = descFingerprint(m.description); + const ak = (dk && m.author) ? `${authorFingerprint(m.author, m.artist)}||${dk}` : null; + + const linkedIds = links[m.id] ?? []; + const linkedIdx = linkedIds.map(lid => byId.get(lid)).find(i => i !== undefined); + const existingIdx = + linkedIdx ?? + byTitle.get(tk) ?? + (dk ? byDesc.get(dk) : undefined) ?? + (ak ? byAuthorDesc.get(ak) : undefined); + + if (existingIdx !== undefined) { + const existing = out[existingIdx]; + const mBetter = + (m.inLibrary && !existing.inLibrary) || + (!existing.inLibrary && (m.downloadCount ?? 0) > (existing.downloadCount ?? 0)); + + if (mBetter) { + out[existingIdx] = m; + byTitle.set(tk, existingIdx); + byId.set(m.id, existingIdx); + if (dk) byDesc.set(dk, existingIdx); + if (ak) byAuthorDesc.set(ak, existingIdx); + } + continue; + } + + const idx = out.length; + out.push(m); + byTitle.set(tk, idx); + byId.set(m.id, idx); + if (dk) byDesc.set(dk, idx); + if (ak) byAuthorDesc.set(ak, idx); + } + + return out; +} + +export function dedupeMangaById(items: T[]): T[] { + const seen = new Set(); + const out: T[] = []; + for (const m of items) { + if (!seen.has(m.id)) { seen.add(m.id); out.push(m); } + } + return out; +} \ No newline at end of file diff --git a/src/lib/request-manager/chapters.ts b/src/lib/request-manager/chapters.ts new file mode 100644 index 0000000..bd25178 --- /dev/null +++ b/src/lib/request-manager/chapters.ts @@ -0,0 +1,40 @@ +import { getAdapter } from '$lib/request-manager' +import { seriesState } from '$lib/state/series.svelte' +import { readerState } from '$lib/state/reader.svelte' + +export async function loadChapters(mangaId: string) { + seriesState.chaptersLoading = true + seriesState.chaptersError = null + try { + seriesState.chapters = await getAdapter().getChapters(mangaId) + } catch (e) { + seriesState.chaptersError = String(e) + } finally { + seriesState.chaptersLoading = false + } +} + +export async function loadChapterPages(chapterId: string) { + readerState.pagesLoading = true + readerState.pagesError = null + try { + readerState.pages = await getAdapter().getChapterPages(chapterId) + } catch (e) { + readerState.pagesError = String(e) + } finally { + readerState.pagesLoading = false + } +} + +export async function markRead(id: string, read: boolean) { + await getAdapter().markChapterRead(id, read) + const chapter = seriesState.chapters.find(c => c.id === id) + if (chapter) chapter.read = read +} + +export async function markManyRead(ids: string[], read: boolean) { + await getAdapter().markChaptersRead(ids, read) + for (const c of seriesState.chapters) { + if (ids.includes(c.id)) c.read = read + } +} diff --git a/src/lib/request-manager/downloads.ts b/src/lib/request-manager/downloads.ts new file mode 100644 index 0000000..4519a40 --- /dev/null +++ b/src/lib/request-manager/downloads.ts @@ -0,0 +1,25 @@ +import { getAdapter } from '$lib/request-manager' +import { downloadsState } from '$lib/state/downloads.svelte' + +export async function loadDownloads() { + try { + downloadsState.items = await getAdapter().getDownloads() + } catch (e) { + downloadsState.error = String(e) + } +} + +export async function enqueueDownload(chapterId: string) { + await getAdapter().enqueueDownload(chapterId) + await loadDownloads() +} + +export async function dequeueDownload(chapterId: string) { + await getAdapter().dequeueDownload(chapterId) + downloadsState.items = downloadsState.items.filter(d => d.chapterId !== chapterId) +} + +export async function clearDownloads() { + await getAdapter().clearDownloads() + downloadsState.items = [] +} diff --git a/src/lib/request-manager/extensions.ts b/src/lib/request-manager/extensions.ts new file mode 100644 index 0000000..4e02688 --- /dev/null +++ b/src/lib/request-manager/extensions.ts @@ -0,0 +1,51 @@ +import { getAdapter } from '$lib/request-manager' +import { extensionsState } from '$lib/state/extensions.svelte' + +export async function loadExtensions() { + extensionsState.loading = true + extensionsState.error = null + try { + extensionsState.items = await getAdapter().getExtensions() + } catch (e) { + extensionsState.error = String(e) + } finally { + extensionsState.loading = false + } +} + +export async function loadSources() { + try { + extensionsState.sources = await getAdapter().getSources() + } catch (e) { + extensionsState.error = String(e) + } +} + +export async function installExtension(id: string) { + await getAdapter().installExtension(id) + await loadExtensions() +} + +export async function uninstallExtension(id: string) { + await getAdapter().uninstallExtension(id) + extensionsState.items = extensionsState.items.filter(e => e.id !== id) +} + +export async function updateExtension(id: string) { + await getAdapter().updateExtension(id) + await loadExtensions() +} + +export async function browseSource(sourceId: string, page: number) { + extensionsState.browseLoading = true + extensionsState.browseError = null + try { + const result = await getAdapter().browseSource(sourceId, page) + extensionsState.browseResults = result.items + extensionsState.browseHasMore = result.hasNextPage + } catch (e) { + extensionsState.browseError = String(e) + } finally { + extensionsState.browseLoading = false + } +} diff --git a/src/lib/request-manager/manga.ts b/src/lib/request-manager/manga.ts new file mode 100644 index 0000000..dcbcec2 --- /dev/null +++ b/src/lib/request-manager/manga.ts @@ -0,0 +1,58 @@ +import { getAdapter } from '$lib/request-manager' +import { libraryState } from '$lib/state/library.svelte' +import { seriesState } from '$lib/state/series.svelte' +import type { MangaFilters, MangaMeta } from '$lib/server-adapters/types' + +export async function loadLibrary(filters: MangaFilters = { inLibrary: true }) { + libraryState.loading = true + libraryState.error = null + try { + const result = await getAdapter().getMangaList(filters) + libraryState.items = result.items + } catch (e) { + libraryState.error = String(e) + } finally { + libraryState.loading = false + } +} + +export async function loadManga(id: string) { + seriesState.loading = true + seriesState.error = null + try { + seriesState.current = await getAdapter().getManga(id) + } catch (e) { + seriesState.error = String(e) + } finally { + seriesState.loading = false + } +} + +export async function searchManga(query: string, sourceId?: string) { + libraryState.loading = true + libraryState.error = null + try { + libraryState.searchResults = await getAdapter().searchManga(query, sourceId) + } catch (e) { + libraryState.error = String(e) + } finally { + libraryState.loading = false + } +} + +export async function addToLibrary(mangaId: string) { + await getAdapter().addToLibrary(mangaId) + await loadLibrary() +} + +export async function removeFromLibrary(mangaId: string) { + await getAdapter().removeFromLibrary(mangaId) + libraryState.items = libraryState.items.filter(m => m.id !== mangaId) +} + +export async function updateMangaMeta(id: string, meta: Partial) { + await getAdapter().updateMangaMeta(id, meta) + if (seriesState.current?.id === id) { + await loadManga(id) + } +} diff --git a/src/lib/request-manager/tracking.ts b/src/lib/request-manager/tracking.ts new file mode 100644 index 0000000..94eedb3 --- /dev/null +++ b/src/lib/request-manager/tracking.ts @@ -0,0 +1,28 @@ +import { getAdapter } from '$lib/request-manager' +import { trackingState } from '$lib/state/tracking.svelte' + +export async function loadTrackers() { + trackingState.loading = true + trackingState.error = null + try { + trackingState.trackers = await getAdapter().getTrackers() + } catch (e) { + trackingState.error = String(e) + } finally { + trackingState.loading = false + } +} + +export async function linkTracker(mangaId: string, trackerId: string, remoteId: string) { + await getAdapter().linkTracker(mangaId, trackerId, remoteId) + await loadTrackers() +} + +export async function syncTracking(mangaId: string) { + trackingState.syncing = true + try { + await getAdapter().syncTracking(mangaId) + } finally { + trackingState.syncing = false + } +} diff --git a/src/lib/server-adapters/suwayomi/chapters.ts b/src/lib/server-adapters/suwayomi/chapters.ts new file mode 100644 index 0000000..b509f1e --- /dev/null +++ b/src/lib/server-adapters/suwayomi/chapters.ts @@ -0,0 +1,86 @@ +export const GET_CHAPTERS = ` + query GetChapters($mangaId: Int!) { + chapters(condition: { mangaId: $mangaId }) { + nodes { + id name chapterNumber sourceOrder isRead isDownloaded isBookmarked + pageCount mangaId uploadDate realUrl lastPageRead lastReadAt scanlator + } + } + } +` + +export const GET_RECENTLY_UPDATED = ` + query GetRecentlyUpdated { + chapters(orderBy: FETCHED_AT, orderByType: DESC, first: 300) { + nodes { + id name chapterNumber sourceOrder isRead lastPageRead mangaId fetchedAt + manga { id title thumbnailUrl inLibrary } + } + } + } +` + +export const FETCH_CHAPTERS = ` + mutation FetchChapters($mangaId: Int!) { + fetchChapters(input: { mangaId: $mangaId }) { + chapters { + id name chapterNumber sourceOrder isRead isDownloaded isBookmarked + pageCount mangaId uploadDate realUrl lastPageRead lastReadAt scanlator + } + } + } +` + +export const FETCH_CHAPTER_PAGES = ` + mutation FetchChapterPages($chapterId: Int!) { + fetchChapterPages(input: { chapterId: $chapterId }) { pages } + } +` + +export const MARK_CHAPTER_READ = ` + mutation MarkChapterRead($id: Int!, $isRead: Boolean!) { + updateChapter(input: { id: $id, patch: { isRead: $isRead } }) { + chapter { id isRead } + } + } +` + +export const MARK_CHAPTERS_READ = ` + mutation MarkChaptersRead($ids: [Int!]!, $isRead: Boolean!) { + updateChapters(input: { ids: $ids, patch: { isRead: $isRead } }) { + chapters { id isRead } + } + } +` + +export const UPDATE_CHAPTERS_PROGRESS = ` + mutation UpdateChaptersProgress($ids: [Int!]!, $isRead: Boolean, $isBookmarked: Boolean, $lastPageRead: Int) { + updateChapters(input: { ids: $ids, patch: { isRead: $isRead, isBookmarked: $isBookmarked, lastPageRead: $lastPageRead } }) { + chapters { id isRead isBookmarked lastPageRead } + } + } +` + +export const DELETE_DOWNLOADED_CHAPTERS = ` + mutation DeleteDownloadedChapters($ids: [Int!]!) { + deleteDownloadedChapters(input: { ids: $ids }) { + chapters { id isDownloaded } + } + } +` + +export const SET_CHAPTER_META = ` + mutation SetChapterMeta($chapterId: Int!, $key: String!, $value: String!) { + setChapterMeta(input: { meta: { chapterId: $chapterId, key: $key, value: $value } }) { + meta { key value } + } + } +` + +export const DELETE_CHAPTER_META = ` + mutation DeleteChapterMeta($chapterId: Int!, $key: String!) { + deleteChapterMeta(input: { chapterId: $chapterId, key: $key }) { + meta { key value } + } + } +` \ No newline at end of file diff --git a/src/lib/server-adapters/suwayomi/downloads.ts b/src/lib/server-adapters/suwayomi/downloads.ts new file mode 100644 index 0000000..5954078 --- /dev/null +++ b/src/lib/server-adapters/suwayomi/downloads.ts @@ -0,0 +1,105 @@ +const QUEUE_FRAGMENT = ` + state + queue { + progress state tries + chapter { + id name pageCount mangaId + manga { id title thumbnailUrl } + } + } +` + +export const GET_DOWNLOAD_STATUS = ` + query GetDownloadStatus { + downloadStatus { ${QUEUE_FRAGMENT} } + } +` + +export const ENQUEUE_DOWNLOAD = ` + mutation EnqueueDownload($chapterId: Int!) { + enqueueChapterDownload(input: { id: $chapterId }) { + downloadStatus { ${QUEUE_FRAGMENT} } + } + } +` + +export const ENQUEUE_CHAPTERS_DOWNLOAD = ` + mutation EnqueueChaptersDownload($chapterIds: [Int!]!) { + enqueueChapterDownloads(input: { ids: $chapterIds }) { + downloadStatus { state } + } + } +` + +export const DEQUEUE_DOWNLOAD = ` + mutation DequeueDownload($chapterId: Int!) { + dequeueChapterDownload(input: { id: $chapterId }) { + downloadStatus { state } + } + } +` + +export const DEQUEUE_CHAPTERS_DOWNLOAD = ` + mutation DequeueChaptersDownload($chapterIds: [Int!]!) { + dequeueChapterDownloads(input: { ids: $chapterIds }) { + downloadStatus { ${QUEUE_FRAGMENT} } + } + } +` + +export const REORDER_DOWNLOAD = ` + mutation ReorderDownload($chapterId: Int!, $to: Int!) { + reorderChapterDownload(input: { chapterId: $chapterId, to: $to }) { + downloadStatus { ${QUEUE_FRAGMENT} } + } + } +` + +export const START_DOWNLOADER = ` + mutation StartDownloader { + startDownloader(input: {}) { + downloadStatus { ${QUEUE_FRAGMENT} } + } + } +` + +export const STOP_DOWNLOADER = ` + mutation StopDownloader { + stopDownloader(input: {}) { + downloadStatus { ${QUEUE_FRAGMENT} } + } + } +` + +export const CLEAR_DOWNLOADER = ` + mutation ClearDownloader { + clearDownloader(input: {}) { + downloadStatus { ${QUEUE_FRAGMENT} } + } + } +` + +export const FETCH_SOURCE_MANGA = ` + mutation FetchSourceManga($source: LongString!, $type: FetchSourceMangaType!, $page: Int!, $query: String, $filters: [FilterChangeInput!]) { + fetchSourceManga(input: { source: $source, type: $type, page: $page, query: $query, filters: $filters }) { + mangas { id title thumbnailUrl inLibrary } + hasNextPage + } + } +` + +export const SET_DOWNLOADS_PATH = ` + mutation SetDownloadsPath($path: String!) { + setSettings(input: { settings: { downloadsPath: $path } }) { + settings { downloadsPath } + } + } +` + +export const SET_LOCAL_SOURCE_PATH = ` + mutation SetLocalSourcePath($path: String!) { + setSettings(input: { settings: { localSourcePath: $path } }) { + settings { localSourcePath } + } + } +` \ No newline at end of file diff --git a/src/lib/server-adapters/suwayomi/extensions.ts b/src/lib/server-adapters/suwayomi/extensions.ts new file mode 100644 index 0000000..db3e663 --- /dev/null +++ b/src/lib/server-adapters/suwayomi/extensions.ts @@ -0,0 +1,191 @@ +export const GET_EXTENSIONS = ` + query GetExtensions { + extensions { + nodes { + apkName pkgName name lang versionName + isInstalled isObsolete hasUpdate iconUrl + } + } + } +` + +export const GET_SOURCES = ` + query GetSources { + sources { + nodes { + id name lang displayName iconUrl isNsfw + isConfigurable supportsLatest + extension { pkgName } + } + } + } +` + +export const GET_SOURCE_SETTINGS = ` + query GetSourceSettings($id: LongString!) { + source(id: $id) { + id + displayName + preferences { + ... on CheckBoxPreference { + type: __typename + CheckBoxTitle: title + CheckBoxSummary: summary + CheckBoxDefault: default + CheckBoxCurrentValue: currentValue + key + } + ... on SwitchPreference { + type: __typename + SwitchPreferenceTitle: title + SwitchPreferenceSummary: summary + SwitchPreferenceDefault: default + SwitchPreferenceCurrentValue: currentValue + key + } + ... on ListPreference { + type: __typename + ListPreferenceTitle: title + ListPreferenceSummary: summary + ListPreferenceDefault: default + ListPreferenceCurrentValue: currentValue + entries + entryValues + key + } + ... on EditTextPreference { + type: __typename + EditTextPreferenceTitle: title + EditTextPreferenceSummary: summary + EditTextPreferenceDefault: default + EditTextPreferenceCurrentValue: currentValue + dialogTitle + dialogMessage + key + } + ... on MultiSelectListPreference { + type: __typename + MultiSelectListPreferenceTitle: title + MultiSelectListPreferenceSummary: summary + MultiSelectListPreferenceDefault: default + MultiSelectListPreferenceCurrentValue: currentValue + entries + entryValues + key + } + } + } + } +` + +export const GET_SETTINGS = ` + query GetSettings { + settings { extensionRepos } + } +` + +export const GET_SERVER_SECURITY = ` + query GetServerSecurity { + settings { + authMode authUsername + socksProxyEnabled socksProxyHost socksProxyPort socksProxyVersion socksProxyUsername + flareSolverrEnabled flareSolverrUrl flareSolverrTimeout + flareSolverrSessionName flareSolverrSessionTtl flareSolverrAsResponseFallback + } + } +` + +export const FETCH_EXTENSIONS = ` + mutation FetchExtensions { + fetchExtensions(input: {}) { + extensions { + apkName pkgName name lang versionName + isInstalled isObsolete hasUpdate iconUrl + } + } + } +` + +export const UPDATE_EXTENSION = ` + mutation UpdateExtension($id: String!, $install: Boolean, $uninstall: Boolean, $update: Boolean) { + updateExtension(input: { id: $id, patch: { install: $install, uninstall: $uninstall, update: $update } }) { + extension { apkName pkgName name isInstalled hasUpdate } + } + } +` + +export const UPDATE_EXTENSIONS = ` + mutation UpdateExtensions($ids: [String!]!, $install: Boolean, $uninstall: Boolean, $update: Boolean) { + updateExtensions(input: { ids: $ids, patch: { install: $install, uninstall: $uninstall, update: $update } }) { + extensions { apkName pkgName name isInstalled hasUpdate } + } + } +` + +export const INSTALL_EXTERNAL_EXTENSION = ` + mutation InstallExternalExtension($url: String!) { + installExternalExtension(input: { extensionUrl: $url }) { + extension { apkName pkgName name isInstalled } + } + } +` + +export const UPDATE_SOURCE_PREFERENCE = ` + mutation UpdateSourcePreference($source: LongString!, $change: SourcePreferenceChangeInput!) { + updateSourcePreference(input: { source: $source, change: $change }) { + source { id displayName } + } + } +` + +export const SET_SOURCE_META = ` + mutation SetSourceMeta($sourceId: LongString!, $key: String!, $value: String!) { + setSourceMeta(input: { meta: { sourceId: $sourceId, key: $key, value: $value } }) { + meta { key value } + } + } +` + +export const DELETE_SOURCE_META = ` + mutation DeleteSourceMeta($sourceId: LongString!, $key: String!) { + deleteSourceMeta(input: { sourceId: $sourceId, key: $key }) { + meta { key value } + } + } +` + +export const SET_EXTENSION_REPOS = ` + mutation SetExtensionRepos($repos: [String!]!) { + setSettings(input: { settings: { extensionRepos: $repos } }) { + settings { extensionRepos } + } + } +` + +export const SET_SERVER_AUTH = ` + mutation SetServerAuth($authMode: AuthMode!, $authUsername: String!, $authPassword: String!) { + setSettings(input: { settings: { authMode: $authMode, authUsername: $authUsername, authPassword: $authPassword } }) { + settings { authMode authUsername } + } + } +` + +export const CLEAR_CACHED_IMAGES = ` + mutation ClearCachedImages($cachedPages: Boolean, $cachedThumbnails: Boolean, $downloadedThumbnails: Boolean) { + clearCachedImages(input: { + cachedPages: $cachedPages + cachedThumbnails: $cachedThumbnails + downloadedThumbnails: $downloadedThumbnails + }) { + cachedPages cachedThumbnails downloadedThumbnails + } + } +` + +export const RESET_SETTINGS = ` + mutation ResetSettings { + resetSettings(input: {}) { + settings { extensionRepos } + } + } +` \ No newline at end of file diff --git a/src/lib/server-adapters/suwayomi/index.ts b/src/lib/server-adapters/suwayomi/index.ts new file mode 100644 index 0000000..f4bc504 --- /dev/null +++ b/src/lib/server-adapters/suwayomi/index.ts @@ -0,0 +1,517 @@ +import type { + ServerAdapter, + ServerConfig, + ServerStatus, + MangaFilters, + MangaMeta, + PaginatedResult, + Page, + DownloadItem, + UpdateResult, +} from '$lib/server-adapters/types' +import type { Manga, Chapter, Extension, Source, Tracker } from '$lib/types' + +interface GQLResponse { + data: T + errors?: { message: string }[] +} + +const GET_LIBRARY = ` + query GetLibrary { + mangas(condition: { inLibrary: true }) { + nodes { + id title thumbnailUrl inLibrary downloadCount unreadCount bookmarkCount + description status author artist genre inLibraryAt lastFetchedAt + source { id name displayName } + chapters { totalCount } + lastReadChapter { id chapterNumber } + firstUnreadChapter { id chapterNumber } + } + } + } +` + +const GET_MANGA = ` + query GetManga($id: Int!) { + manga(id: $id) { + id title description thumbnailUrl status author artist genre inLibrary realUrl + inLibraryAt lastFetchedAt updateStrategy + source { id name displayName } + lastReadChapter { id chapterNumber lastPageRead } + firstUnreadChapter { id chapterNumber } + highestNumberedChapter { id chapterNumber } + } + } +` + +const GET_CHAPTERS = ` + query GetChapters($mangaId: Int!) { + chapters(condition: { mangaId: $mangaId }) { + nodes { + id name chapterNumber sourceOrder isRead isDownloaded isBookmarked + pageCount mangaId uploadDate realUrl lastPageRead lastReadAt scanlator + } + } + } +` + +const GET_DOWNLOAD_STATUS = ` + query GetDownloadStatus { + downloadStatus { + state + queue { + progress state tries + chapter { + id name pageCount mangaId + manga { id title thumbnailUrl } + } + } + } + } +` + +const GET_EXTENSIONS = ` + query GetExtensions { + extensions { + nodes { + apkName pkgName name lang versionName + isInstalled isObsolete hasUpdate iconUrl + } + } + } +` + +const GET_SOURCES = ` + query GetSources { + sources { + nodes { + id name lang displayName iconUrl isNsfw + isConfigurable supportsLatest + } + } + } +` + +const GET_TRACKERS = ` + query GetTrackers { + trackers { + nodes { + id name icon isLoggedIn isTokenExpired authUrl + supportsPrivateTracking supportsReadingDates supportsTrackDeletion + scores + statuses { value name } + } + } + } +` + +const FETCH_MANGA = ` + mutation FetchManga($id: Int!) { + fetchManga(input: { id: $id }) { + manga { + id title description thumbnailUrl status author artist genre inLibrary realUrl + source { id name displayName } + } + } + } +` + +const FETCH_SOURCE_MANGA = ` + mutation FetchSourceManga($source: LongString!, $type: FetchSourceMangaType!, $page: Int!, $query: String) { + fetchSourceManga(input: { source: $source, type: $type, page: $page, query: $query }) { + mangas { id title thumbnailUrl inLibrary } + hasNextPage + } + } +` + +const UPDATE_MANGA = ` + mutation UpdateManga($id: Int!, $inLibrary: Boolean) { + updateManga(input: { id: $id, patch: { inLibrary: $inLibrary } }) { + manga { id inLibrary } + } + } +` + +const SET_MANGA_META = ` + mutation SetMangaMeta($mangaId: Int!, $key: String!, $value: String!) { + setMangaMeta(input: { meta: { mangaId: $mangaId, key: $key, value: $value } }) { + meta { key value } + } + } +` + +const FETCH_CHAPTERS = ` + mutation FetchChapters($mangaId: Int!) { + fetchChapters(input: { mangaId: $mangaId }) { + chapters { + id name chapterNumber sourceOrder isRead isDownloaded isBookmarked + pageCount mangaId uploadDate realUrl lastPageRead lastReadAt scanlator + } + } + } +` + +const FETCH_CHAPTER_PAGES = ` + mutation FetchChapterPages($chapterId: Int!) { + fetchChapterPages(input: { chapterId: $chapterId }) { pages } + } +` + +const MARK_CHAPTER_READ = ` + mutation MarkChapterRead($id: Int!, $isRead: Boolean!) { + updateChapter(input: { id: $id, patch: { isRead: $isRead } }) { + chapter { id isRead } + } + } +` + +const MARK_CHAPTERS_READ = ` + mutation MarkChaptersRead($ids: [Int!]!, $isRead: Boolean!) { + updateChapters(input: { ids: $ids, patch: { isRead: $isRead } }) { + chapters { id isRead } + } + } +` + +const ENQUEUE_DOWNLOAD = ` + mutation EnqueueDownload($chapterId: Int!) { + enqueueChapterDownload(input: { id: $chapterId }) { + downloadStatus { state } + } + } +` + +const DEQUEUE_DOWNLOAD = ` + mutation DequeueDownload($chapterId: Int!) { + dequeueChapterDownload(input: { id: $chapterId }) { + downloadStatus { state } + } + } +` + +const CLEAR_DOWNLOADER = ` + mutation ClearDownloader { + clearDownloader(input: {}) { + downloadStatus { state } + } + } +` + +const FETCH_EXTENSIONS = ` + mutation FetchExtensions { + fetchExtensions(input: {}) { + extensions { + apkName pkgName name lang versionName + isInstalled isObsolete hasUpdate iconUrl + } + } + } +` + +const UPDATE_EXTENSION = ` + mutation UpdateExtension($id: String!, $install: Boolean, $uninstall: Boolean, $update: Boolean) { + updateExtension(input: { id: $id, patch: { install: $install, uninstall: $uninstall, update: $update } }) { + extension { apkName pkgName name isInstalled hasUpdate } + } + } +` + +const BIND_TRACK = ` + mutation BindTrack($mangaId: Int!, $trackerId: Int!, $remoteId: LongString!) { + bindTrack(input: { mangaId: $mangaId, trackerId: $trackerId, remoteId: $remoteId }) { + trackRecord { id trackerId remoteId } + } + } +` + +const TRACK_PROGRESS = ` + mutation TrackProgress($mangaId: Int!) { + trackProgress(input: { mangaId: $mangaId }) { + trackRecords { id trackerId lastChapterRead status } + } + } +` + +const UPDATE_LIBRARY = ` + mutation UpdateLibrary { + updateLibrary(input: {}) { + updateStatus { jobsInfo { isRunning finishedJobs totalJobs } } + } + } +` + +function mapChapter(raw: Record): Chapter { + return { + id: raw.id as number, + name: raw.name as string, + chapterNumber: raw.chapterNumber as number, + sourceOrder: raw.sourceOrder as number, + read: (raw.isRead as boolean) ?? false, + downloaded: (raw.isDownloaded as boolean) ?? false, + bookmarked: (raw.isBookmarked as boolean) ?? false, + pageCount: (raw.pageCount as number) ?? 0, + mangaId: raw.mangaId as number, + fetchedAt: raw.fetchedAt as string | undefined, + uploadDate: raw.uploadDate as string | null | undefined, + realUrl: raw.realUrl as string | null | undefined, + lastPageRead: raw.lastPageRead as number | undefined, + lastReadAt: raw.lastReadAt as string | undefined, + scanlator: raw.scanlator as string | null | undefined, + manga: raw.manga as Chapter['manga'], + } +} + +function mapManga(raw: Record): Manga { + const inLibraryAt = raw.inLibraryAt as string | null | undefined + return { + ...(raw as unknown as Manga), + tags: raw.genre as string[] | undefined, + addedAt: inLibraryAt ? new Date(inLibraryAt).getTime() : undefined, + lastReadAt: raw.lastReadChapter + ? Date.now() + : undefined, + } +} + +function mapExtension(raw: Record): Extension { + return { + ...(raw as unknown as Extension), + id: raw.pkgName as string, + } +} + +function mapDownloadItem(raw: Record): DownloadItem { + const chapter = raw.chapter as Record + const manga = chapter?.manga as Record + return { + chapterId: String(chapter?.id), + mangaId: String(chapter?.mangaId ?? manga?.id), + chapterName: chapter?.name as string, + mangaTitle: manga?.title as string, + progress: (raw.progress as number) ?? 0, + state: mapDownloadState(raw.state as string), + } +} + +function mapDownloadState(state: string): DownloadItem['state'] { + switch (state) { + case 'DOWNLOADING': return 'downloading' + case 'FINISHED': return 'finished' + case 'ERROR': return 'error' + default: return 'queued' + } +} + +export class SuwayomiAdapter implements ServerAdapter { + private baseUrl = 'http://127.0.0.1:4567' + private authHeader: string | null = null + + async connect(config: ServerConfig) { + this.baseUrl = config.baseUrl.replace(/\/$/, '') + if (config.credentials) { + const { username, password } = config.credentials + this.authHeader = 'Basic ' + btoa(`${username}:${password}`) + } + } + + async getStatus(): Promise { + try { + const res = await fetch(`${this.baseUrl}/api/graphql`, { + method: 'POST', + headers: this.headers(), + body: JSON.stringify({ query: '{ aboutServer { name } }' }), + }) + return res.ok ? 'connected' : 'error' + } catch { + return 'disconnected' + } + } + + private headers(): Record { + const h: Record = { 'Content-Type': 'application/json' } + if (this.authHeader) h['Authorization'] = this.authHeader + return h + } + + private async gql(query: string, variables?: Record): Promise { + const res = await fetch(`${this.baseUrl}/api/graphql`, { + method: 'POST', + headers: this.headers(), + body: JSON.stringify({ query, variables }), + }) + if (!res.ok) throw new Error(`Suwayomi HTTP ${res.status}`) + const json: GQLResponse = await res.json() + if (json.errors?.length) throw new Error(json.errors[0].message) + return json.data + } + + async getManga(id: string): Promise { + const data = await this.gql<{ manga: Record }>( + GET_MANGA, { id: Number(id) } + ) + return mapManga(data.manga) + } + + async getMangaList(filters: MangaFilters): Promise> { + if (filters.inLibrary) { + const data = await this.gql<{ mangas: { nodes: Record[] } }>(GET_LIBRARY) + return { items: data.mangas.nodes.map(mapManga), hasNextPage: false } + } + const data = await this.gql<{ mangas: { nodes: Record[] } }>(GET_LIBRARY) + return { items: data.mangas.nodes.map(mapManga), hasNextPage: false } + } + + async searchManga(query: string, sourceId?: string): Promise { + if (!sourceId) return [] + const data = await this.gql<{ + fetchSourceManga: { mangas: Record[] } + }>(FETCH_SOURCE_MANGA, { + source: sourceId, + type: 'SEARCH', + page: 1, + query, + }) + return data.fetchSourceManga.mangas.map(mapManga) + } + + async addToLibrary(mangaId: string) { + await this.gql(UPDATE_MANGA, { id: Number(mangaId), inLibrary: true }) + } + + async removeFromLibrary(mangaId: string) { + await this.gql(UPDATE_MANGA, { id: Number(mangaId), inLibrary: false }) + } + + async updateMangaMeta(id: string, meta: Partial) { + for (const [key, value] of Object.entries(meta)) { + if (value === undefined) continue + await this.gql(SET_MANGA_META, { + mangaId: Number(id), + key, + value: String(value), + }) + } + } + + async getChapters(mangaId: string): Promise { + const data = await this.gql<{ chapters: { nodes: Record[] } }>( + GET_CHAPTERS, { mangaId: Number(mangaId) } + ) + return data.chapters.nodes.map(mapChapter) + } + + async getChapter(id: string): Promise { + const chapters = await this.gql<{ chapters: { nodes: Record[] } }>( + GET_CHAPTERS, { mangaId: 0 } + ) + const found = chapters.chapters.nodes.find(c => String(c.id) === id) + if (!found) throw new Error(`Chapter ${id} not found`) + return mapChapter(found) + } + + async getChapterPages(id: string): Promise { + const data = await this.gql<{ fetchChapterPages: { pages: string[] } }>( + FETCH_CHAPTER_PAGES, { chapterId: Number(id) } + ) + return data.fetchChapterPages.pages.map((url, index) => ({ index, url })) + } + + async markChapterRead(id: string, read: boolean) { + await this.gql(MARK_CHAPTER_READ, { id: Number(id), isRead: read }) + } + + async markChaptersRead(ids: string[], read: boolean) { + await this.gql(MARK_CHAPTERS_READ, { ids: ids.map(Number), isRead: read }) + } + + async getDownloads(): Promise { + const data = await this.gql<{ + downloadStatus: { queue: Record[] } + }>(GET_DOWNLOAD_STATUS) + return data.downloadStatus.queue.map(mapDownloadItem) + } + + async enqueueDownload(chapterId: string) { + await this.gql(ENQUEUE_DOWNLOAD, { chapterId: Number(chapterId) }) + } + + async dequeueDownload(chapterId: string) { + await this.gql(DEQUEUE_DOWNLOAD, { chapterId: Number(chapterId) }) + } + + async clearDownloads() { + await this.gql(CLEAR_DOWNLOADER) + } + + async getExtensions(): Promise { + await this.gql(FETCH_EXTENSIONS) + const data = await this.gql<{ extensions: { nodes: Record[] } }>( + GET_EXTENSIONS + ) + return data.extensions.nodes.map(mapExtension) + } + + async installExtension(id: string) { + await this.gql(UPDATE_EXTENSION, { id, install: true }) + } + + async uninstallExtension(id: string) { + await this.gql(UPDATE_EXTENSION, { id, uninstall: true }) + } + + async updateExtension(id: string) { + await this.gql(UPDATE_EXTENSION, { id, update: true }) + } + + async getSources(): Promise { + const data = await this.gql<{ sources: { nodes: Source[] } }>(GET_SOURCES) + return data.sources.nodes + } + + async browseSource(sourceId: string, page: number): Promise> { + const data = await this.gql<{ + fetchSourceManga: { mangas: Record[]; hasNextPage: boolean } + }>(FETCH_SOURCE_MANGA, { + source: sourceId, + type: 'LATEST', + page, + }) + return { + items: data.fetchSourceManga.mangas.map(mapManga), + hasNextPage: data.fetchSourceManga.hasNextPage, + } + } + + async getTrackers(): Promise { + const data = await this.gql<{ trackers: { nodes: Tracker[] } }>(GET_TRACKERS) + return data.trackers.nodes + } + + async linkTracker(mangaId: string, trackerId: string, remoteId: string) { + await this.gql(BIND_TRACK, { + mangaId: Number(mangaId), + trackerId: Number(trackerId), + remoteId, + }) + } + + async syncTracking(mangaId: string) { + await this.gql(TRACK_PROGRESS, { mangaId: Number(mangaId) }) + } + + async checkForUpdates(mangaIds?: string[]): Promise { + if (mangaIds?.length) { + const results: UpdateResult[] = [] + for (const id of mangaIds) { + const before = await this.getChapters(id) + await this.gql(FETCH_CHAPTERS, { mangaId: Number(id) }) + const after = await this.getChapters(id) + results.push({ mangaId: id, newChapters: after.length - before.length }) + } + return results + } + await this.gql(UPDATE_LIBRARY) + return [] + } +} \ No newline at end of file diff --git a/src/lib/server-adapters/suwayomi/manga.ts b/src/lib/server-adapters/suwayomi/manga.ts new file mode 100644 index 0000000..12717f7 --- /dev/null +++ b/src/lib/server-adapters/suwayomi/manga.ts @@ -0,0 +1,196 @@ +export const GET_LIBRARY = ` + query GetLibrary { + mangas(condition: { inLibrary: true }) { + nodes { + id title thumbnailUrl inLibrary downloadCount unreadCount bookmarkCount + description status author artist genre inLibraryAt lastFetchedAt + source { id name displayName } + chapters { totalCount } + lastReadChapter { id chapterNumber } + firstUnreadChapter { id chapterNumber } + } + } + } +` + +export const GET_MANGA = ` + query GetManga($id: Int!) { + manga(id: $id) { + id title description thumbnailUrl status author artist genre inLibrary realUrl + inLibraryAt lastFetchedAt updateStrategy + source { id name displayName } + lastReadChapter { id chapterNumber lastPageRead } + firstUnreadChapter { id chapterNumber } + highestNumberedChapter { id chapterNumber } + } + } +` + +export const GET_CATEGORIES = ` + query GetCategories { + categories { + nodes { + id name order default includeInUpdate includeInDownload + mangas { + nodes { id title thumbnailUrl inLibrary downloadCount unreadCount } + } + } + } + } +` + +export const MANGAS_BY_GENRE = ` + query MangasByGenre($filter: MangaFilterInput, $first: Int, $offset: Int) { + mangas(filter: $filter, first: $first, offset: $offset, orderBy: IN_LIBRARY_AT, orderByType: DESC) { + nodes { + id title thumbnailUrl inLibrary genre status + source { id displayName } + } + pageInfo { hasNextPage } + totalCount + } + } +` + +export const LIBRARY_UPDATE_STATUS = ` + query LibraryUpdateStatus { + libraryUpdateStatus { + jobsInfo { + isRunning finishedJobs totalJobs skippedMangasCount skippedCategoriesCount + } + mangaUpdates { + status + manga { id title thumbnailUrl unreadCount } + } + } + lastUpdateTimestamp { timestamp } + } +` + +export const FETCH_MANGA = ` + mutation FetchManga($id: Int!) { + fetchManga(input: { id: $id }) { + manga { + id title description thumbnailUrl status author artist genre inLibrary realUrl + source { id name displayName } + } + } + } +` + +export const UPDATE_MANGA = ` + mutation UpdateManga($id: Int!, $inLibrary: Boolean) { + updateManga(input: { id: $id, patch: { inLibrary: $inLibrary } }) { + manga { id inLibrary } + } + } +` + +export const UPDATE_MANGAS = ` + mutation UpdateMangas($ids: [Int!]!, $inLibrary: Boolean) { + updateMangas(input: { ids: $ids, patch: { inLibrary: $inLibrary } }) { + mangas { id inLibrary } + } + } +` + +export const UPDATE_MANGA_CATEGORIES = ` + mutation UpdateMangaCategories($mangaId: Int!, $addTo: [Int!]!, $removeFrom: [Int!]!) { + updateMangaCategories(input: { id: $mangaId, patch: { addToCategories: $addTo, removeFromCategories: $removeFrom } }) { + manga { id } + } + } +` + +export const UPDATE_MANGAS_CATEGORIES = ` + mutation UpdateMangasCategories($ids: [Int!]!, $addTo: [Int!]!, $removeFrom: [Int!]!) { + updateMangasCategories(input: { ids: $ids, patch: { addToCategories: $addTo, removeFromCategories: $removeFrom } }) { + mangas { id } + } + } +` + +export const CREATE_CATEGORY = ` + mutation CreateCategory($name: String!) { + createCategory(input: { name: $name }) { + category { id name order default includeInUpdate includeInDownload } + } + } +` + +export const UPDATE_CATEGORY = ` + mutation UpdateCategory($id: Int!, $name: String) { + updateCategory(input: { id: $id, patch: { name: $name } }) { + category { id name order } + } + } +` + +export const DELETE_CATEGORY = ` + mutation DeleteCategory($id: Int!) { + deleteCategory(input: { categoryId: $id }) { + category { id } + } + } +` + +export const UPDATE_CATEGORY_ORDER = ` + mutation UpdateCategoryOrder($id: Int!, $position: Int!) { + updateCategoryOrder(input: { id: $id, position: $position }) { + categories { id name order default includeInUpdate includeInDownload } + } + } +` + +export const UPDATE_LIBRARY = ` + mutation UpdateLibrary { + updateLibrary(input: {}) { + updateStatus { jobsInfo { isRunning finishedJobs totalJobs } } + } + } +` + +export const UPDATE_LIBRARY_MANGA = ` + mutation UpdateLibraryManga($mangaId: Int!) { + updateLibraryManga(input: { mangaId: $mangaId }) { + updateStatus { jobsInfo { isRunning finishedJobs totalJobs } } + } + } +` + +export const SET_MANGA_META = ` + mutation SetMangaMeta($mangaId: Int!, $key: String!, $value: String!) { + setMangaMeta(input: { meta: { mangaId: $mangaId, key: $key, value: $value } }) { + meta { key value } + } + } +` + +export const DELETE_MANGA_META = ` + mutation DeleteMangaMeta($mangaId: Int!, $key: String!) { + deleteMangaMeta(input: { mangaId: $mangaId, key: $key }) { + meta { key value } + } + } +` + +export const CREATE_BACKUP = ` + mutation CreateBackup { + createBackup(input: {}) { url } + } +` + +export const RESTORE_BACKUP = ` + mutation RestoreBackup($backup: Upload!) { + restoreBackup(input: { backup: $backup }) { + id + status { mangaProgress state totalManga } + } + } +` + +export const GET_RESTORE_STATUS = ` + query GetRestoreStatus($id: String!) { + restoreStatus(id: $id) { mangaProgress state totalManga } + } +` \ No newline at end of file diff --git a/src/lib/server-adapters/suwayomi/types.ts b/src/lib/server-adapters/suwayomi/types.ts new file mode 100644 index 0000000..af9576e --- /dev/null +++ b/src/lib/server-adapters/suwayomi/types.ts @@ -0,0 +1,67 @@ +import type { Manga, Chapter, Extension } from '$lib/types' +import type { DownloadItem } from '$lib/server-adapters/types' + +export interface GQLResponse { + data: T + errors?: { message: string }[] +} + +export function mapManga(raw: Record): Manga { + const inLibraryAt = raw.inLibraryAt as string | null | undefined + return { + ...(raw as unknown as Manga), + tags: raw.genre as string[] | undefined, + addedAt: inLibraryAt ? new Date(inLibraryAt).getTime() : undefined, + lastReadAt: raw.lastReadChapter ? Date.now() : undefined, + } +} + +export function mapChapter(raw: Record): Chapter { + return { + id: raw.id as number, + name: raw.name as string, + chapterNumber: raw.chapterNumber as number, + sourceOrder: raw.sourceOrder as number, + read: (raw.isRead as boolean) ?? false, + downloaded: (raw.isDownloaded as boolean) ?? false, + bookmarked: (raw.isBookmarked as boolean) ?? false, + pageCount: (raw.pageCount as number) ?? 0, + mangaId: raw.mangaId as number, + fetchedAt: raw.fetchedAt as string | undefined, + uploadDate: raw.uploadDate as string | null | undefined, + realUrl: raw.realUrl as string | null | undefined, + lastPageRead: raw.lastPageRead as number | undefined, + lastReadAt: raw.lastReadAt as string | undefined, + scanlator: raw.scanlator as string | null | undefined, + manga: raw.manga as Chapter['manga'], + } +} + +export function mapExtension(raw: Record): Extension { + return { + ...(raw as unknown as Extension), + id: raw.pkgName as string, + } +} + +export function mapDownloadItem(raw: Record): DownloadItem { + const chapter = raw.chapter as Record + const manga = chapter?.manga as Record + return { + chapterId: String(chapter?.id), + mangaId: String(chapter?.mangaId ?? manga?.id), + chapterName: chapter?.name as string, + mangaTitle: manga?.title as string, + progress: (raw.progress as number) ?? 0, + state: mapDownloadState(raw.state as string), + } +} + +function mapDownloadState(state: string): DownloadItem['state'] { + switch (state) { + case 'DOWNLOADING': return 'downloading' + case 'FINISHED': return 'finished' + case 'ERROR': return 'error' + default: return 'queued' + } +} \ No newline at end of file diff --git a/src/lib/server-adapters/types.ts b/src/lib/server-adapters/types.ts new file mode 100644 index 0000000..75d8865 --- /dev/null +++ b/src/lib/server-adapters/types.ts @@ -0,0 +1,94 @@ +import type { + Manga, + Chapter, + Extension, + Source, + Tracker, +} from '$lib/types' + +export interface ServerConfig { + baseUrl: string + credentials?: { username: string; password: string } +} + +export type ServerStatus = 'connected' | 'disconnected' | 'error' + +export interface MangaFilters { + inLibrary?: boolean + status?: MangaStatus + tags?: string[] + unread?: boolean + sourceId?: string +} + +export type MangaStatus = 'ONGOING' | 'COMPLETED' | 'LICENSED' | 'PUBLISHING_FINISHED' | 'CANCELLED' | 'ON_HIATUS' + +export interface PaginatedResult { + items: T[] + hasNextPage: boolean + total?: number +} + +export interface MangaMeta { + customTitle?: string + customCover?: string + notes?: string + [key: string]: unknown +} + +export interface Page { + index: number + url: string + imageData?: string +} + +export interface DownloadItem { + chapterId: string + mangaId: string + chapterName: string + mangaTitle: string + progress: number + state: 'queued' | 'downloading' | 'finished' | 'error' +} + +export interface UpdateResult { + mangaId: string + newChapters: number +} + +export interface ServerAdapter { + connect(config: ServerConfig): Promise + getStatus(): Promise + + getManga(id: string): Promise + getMangaList(filters: MangaFilters): Promise> + searchManga(query: string, sourceId?: string): Promise + addToLibrary(mangaId: string): Promise + removeFromLibrary(mangaId: string): Promise + updateMangaMeta(id: string, meta: Partial): Promise + + getChapters(mangaId: string): Promise + getChapter(id: string): Promise + getChapterPages(id: string): Promise + markChapterRead(id: string, read: boolean): Promise + markChaptersRead(ids: string[], read: boolean): Promise + + getDownloads(): Promise + enqueueDownload(chapterId: string): Promise + dequeueDownload(chapterId: string): Promise + clearDownloads(): Promise + + getExtensions(): Promise + installExtension(id: string): Promise + uninstallExtension(id: string): Promise + updateExtension(id: string): Promise + + getSources(): Promise + browseSource(sourceId: string, page: number): Promise> + + getTrackers(): Promise + linkTracker(mangaId: string, trackerId: string, remoteId: string): Promise + syncTracking(mangaId: string): Promise + + checkForUpdates(mangaIds?: string[]): Promise +} diff --git a/src/lib/state/app.svelte.ts b/src/lib/state/app.svelte.ts new file mode 100644 index 0000000..7e3cb71 --- /dev/null +++ b/src/lib/state/app.svelte.ts @@ -0,0 +1,10 @@ +export type AppStatus = 'booting' | 'auth' | 'ready' | 'error' + +export const appState = $state({ + status: 'booting' as AppStatus, + error: null as string | null, + serverUrl: '', + authenticated: false, + platform: 'web' as 'web' | 'tauri' | 'capacitor', + version: '', +}) diff --git a/src/lib/state/downloads.svelte.ts b/src/lib/state/downloads.svelte.ts new file mode 100644 index 0000000..65f994b --- /dev/null +++ b/src/lib/state/downloads.svelte.ts @@ -0,0 +1,16 @@ +import type { DownloadItem } from '$lib/server-adapters/types' + +export const downloadsState = $state({ + items: [] as DownloadItem[], + error: null as string | null, +}) + +export const activeDownloads = $derived( + downloadsState.items.filter(d => d.state === 'downloading') +) + +export const queuedDownloads = $derived( + downloadsState.items.filter(d => d.state === 'queued') +) + +export const downloadCount = $derived(downloadsState.items.length) diff --git a/src/lib/state/extensions.svelte.ts b/src/lib/state/extensions.svelte.ts new file mode 100644 index 0000000..69a619a --- /dev/null +++ b/src/lib/state/extensions.svelte.ts @@ -0,0 +1,36 @@ +import type { Extension, Source, Manga } from '$lib/types' + +export const extensionsState = $state({ + items: [] as Extension[], + sources: [] as Source[], + loading: false, + error: null as string | null, + + filter: { + query: '', + installed: false, + language: 'all', + }, + + browseResults: [] as Manga[], + browseLoading: false, + browseError: null as string | null, + browseHasMore: false, +}) + +export const filteredExtensions = $derived.by(() => { + let result = extensionsState.items + + if (extensionsState.filter.installed) { + result = result.filter(e => e.installed) + } + if (extensionsState.filter.language !== 'all') { + result = result.filter(e => e.lang === extensionsState.filter.language) + } + if (extensionsState.filter.query) { + const q = extensionsState.filter.query.toLowerCase() + result = result.filter(e => e.name.toLowerCase().includes(q)) + } + + return result +}) diff --git a/src/lib/state/library.svelte.ts b/src/lib/state/library.svelte.ts new file mode 100644 index 0000000..d0ecb4d --- /dev/null +++ b/src/lib/state/library.svelte.ts @@ -0,0 +1,53 @@ +import type { Manga } from '$lib/types' +import type { MangaStatus } from '$lib/server-adapters/types' + +export type LibrarySortOption = 'alphabetical' | 'unread' | 'lastRead' | 'dateAdded' + +export const libraryState = $state({ + items: [] as Manga[], + searchResults: [] as Manga[], + loading: false, + error: null as string | null, + filter: { + status: 'all' as MangaStatus | 'all', + tags: [] as string[], + unread: false, + query: '', + }, + sort: 'alphabetical' as LibrarySortOption, + sortDesc: false, + view: 'grid' as 'grid' | 'list', + selected: new Set(), +}) + +export const filteredItems = $derived.by(() => { + let result = libraryState.items + + if (libraryState.filter.unread) { + result = result.filter(m => m.unreadCount > 0) + } + if (libraryState.filter.status !== 'all') { + result = result.filter(m => m.status === libraryState.filter.status) + } + if (libraryState.filter.tags.length > 0) { + result = result.filter(m => + libraryState.filter.tags.every(tag => m.tags?.includes(tag)) + ) + } + if (libraryState.filter.query) { + const q = libraryState.filter.query.toLowerCase() + result = result.filter(m => m.title.toLowerCase().includes(q)) + } + + const sorted = [...result].sort((a, b) => { + switch (libraryState.sort) { + case 'unread': return (b.unreadCount ?? 0) - (a.unreadCount ?? 0) + case 'lastRead': return (b.lastReadAt ?? 0) - (a.lastReadAt ?? 0) + case 'dateAdded': return (b.addedAt ?? 0) - (a.addedAt ?? 0) + case 'alphabetical': + default: return a.title.localeCompare(b.title) + } + }) + + return libraryState.sortDesc ? sorted.reverse() : sorted +}) diff --git a/src/lib/state/notifications.svelte.ts b/src/lib/state/notifications.svelte.ts new file mode 100644 index 0000000..001cd65 --- /dev/null +++ b/src/lib/state/notifications.svelte.ts @@ -0,0 +1,25 @@ +export type ToastKind = 'info' | 'success' | 'error' | 'download' + +export interface Toast { + id: string + kind: ToastKind + message: string + detail?: string + duration?: number +} + +export const notificationsState = $state({ + toasts: [] as Toast[], +}) + +export function toast(kind: ToastKind, message: string, detail?: string, duration = 4000) { + const id = crypto.randomUUID() + notificationsState.toasts.push({ id, kind, message, detail, duration }) + if (duration > 0) { + setTimeout(() => dismissToast(id), duration) + } +} + +export function dismissToast(id: string) { + notificationsState.toasts = notificationsState.toasts.filter(t => t.id !== id) +} diff --git a/src/lib/state/reader.svelte.ts b/src/lib/state/reader.svelte.ts new file mode 100644 index 0000000..a5528f3 --- /dev/null +++ b/src/lib/state/reader.svelte.ts @@ -0,0 +1,41 @@ +import type { Manga, Chapter } from '$lib/types' +import type { Page } from '$lib/server-adapters/types' + +export type ReadMode = 'single' | 'strip' +export type FitMode = 'width' | 'height' | 'original' +export type ReadDirection = 'ltr' | 'rtl' + +export const readerState = $state({ + manga: null as Manga | null, + chapter: null as Chapter | null, + chapters: [] as Chapter[], + + pages: [] as Page[], + pagesLoading: false, + pagesError: null as string | null, + + currentPage: 0, + mode: 'single' as ReadMode, + fit: 'width' as FitMode, + direction: 'ltr' as ReadDirection, + zoom: 1, + + showControls: false, + showSettings: false, + fullscreen: false, +}) + +export const currentPageData = $derived( + readerState.pages[readerState.currentPage] ?? null +) + +export const progress = $derived( + readerState.pages.length > 0 + ? (readerState.currentPage + 1) / readerState.pages.length + : 0 +) + +export const hasPrev = $derived(readerState.currentPage > 0) +export const hasNext = $derived( + readerState.currentPage < readerState.pages.length - 1 +) diff --git a/src/lib/state/series.svelte.ts b/src/lib/state/series.svelte.ts new file mode 100644 index 0000000..c6cecba --- /dev/null +++ b/src/lib/state/series.svelte.ts @@ -0,0 +1,36 @@ +import type { Manga, Chapter } from '$lib/types' + +export const seriesState = $state({ + current: null as Manga | null, + loading: false, + error: null as string | null, + + chapters: [] as Chapter[], + chaptersLoading: false, + chaptersError: null as string | null, + + chapterFilter: { + unread: false, + downloaded: false, + query: '', + }, + chapterSortDesc: true, +}) + +export const filteredChapters = $derived.by(() => { + let result = seriesState.chapters + + if (seriesState.chapterFilter.unread) { + result = result.filter(c => !c.read) + } + if (seriesState.chapterFilter.downloaded) { + result = result.filter(c => c.downloaded) + } + if (seriesState.chapterFilter.query) { + const q = seriesState.chapterFilter.query.toLowerCase() + result = result.filter(c => c.name.toLowerCase().includes(q)) + } + + const sorted = [...result].sort((a, b) => a.chapterNumber - b.chapterNumber) + return seriesState.chapterSortDesc ? sorted.reverse() : sorted +}) diff --git a/src/lib/state/tracking.svelte.ts b/src/lib/state/tracking.svelte.ts new file mode 100644 index 0000000..c3a6440 --- /dev/null +++ b/src/lib/state/tracking.svelte.ts @@ -0,0 +1,8 @@ +import type { Tracker } from '$lib/types' + +export const trackingState = $state({ + trackers: [] as Tracker[], + loading: false, + error: null as string | null, + syncing: false, +}) diff --git a/src/lib/types/api.ts b/src/lib/types/api.ts new file mode 100644 index 0000000..54224e4 --- /dev/null +++ b/src/lib/types/api.ts @@ -0,0 +1,62 @@ +export interface DownloadQueueItem { + progress: number + state: "QUEUED" | "DOWNLOADING" | "FINISHED" | "ERROR" + tries: number + chapter: { + id: number + name: string + mangaId: number + pageCount: number + manga: { id: number; title: string; thumbnailUrl: string } | null + } +} + +export interface DownloadStatus { + state: "STARTED" | "STOPPED" + queue: DownloadQueueItem[] +} + +export interface Connection { + nodes: T[] +} + +export interface PageInfo { + hasNextPage: boolean +} + +export interface PaginatedConnection extends Connection { + pageInfo: PageInfo + totalCount?: number +} + +export interface MetaEntry { + key: string + value: string +} + +export interface UpdaterJobsInfo { + isRunning: boolean + finishedJobs: number + totalJobs: number + skippedMangasCount: number + skippedCategoriesCount: number +} + +export interface UpdateStatus { + jobsInfo: UpdaterJobsInfo +} + +export interface AboutServer { + name: string + version: string + buildType: string + buildTime: string + github: string + discord: string +} + +export interface ServerUpdateEntry { + channel: string + tag: string + url: string +} diff --git a/src/lib/types/chapter.ts b/src/lib/types/chapter.ts new file mode 100644 index 0000000..07363f7 --- /dev/null +++ b/src/lib/types/chapter.ts @@ -0,0 +1,19 @@ +export interface Chapter { + id: number + name: string + chapterNumber: number + sourceOrder: number + read: boolean + downloaded: boolean + bookmarked: boolean + pageCount: number + mangaId: number + fetchedAt?: string + uploadDate?: string | null + realUrl?: string | null + url?: string + lastPageRead?: number + lastReadAt?: string + scanlator?: string | null + manga?: { id: number; title: string; thumbnailUrl: string; inLibrary: boolean } | null +} diff --git a/src/lib/types/extension.ts b/src/lib/types/extension.ts new file mode 100644 index 0000000..91b1c01 --- /dev/null +++ b/src/lib/types/extension.ts @@ -0,0 +1,24 @@ +export interface Source { + id: string + name: string + lang: string + displayName: string + iconUrl: string + isNsfw: boolean + isConfigurable: boolean + supportsLatest: boolean + baseUrl?: string | null +} + +export interface Extension { + apkName: string + pkgName: string + name: string + lang: string + versionName: string + isInstalled: boolean + isObsolete: boolean + hasUpdate: boolean + iconUrl: string + id: string +} diff --git a/src/lib/types/history.ts b/src/lib/types/history.ts new file mode 100644 index 0000000..631000c --- /dev/null +++ b/src/lib/types/history.ts @@ -0,0 +1,72 @@ +export interface HistoryEntry { + mangaId: number + mangaTitle: string + thumbnailUrl: string + chapterId: number + chapterName: string + readAt: number +} + +export interface BookmarkEntry { + mangaId: number + mangaTitle: string + thumbnailUrl: string + chapterId: number + chapterName: string + pageNumber: number + savedAt: number + label?: string +} + +export type MarkerColor = "yellow" | "red" | "blue" | "green" | "purple" + +export interface MarkerEntry { + id: string + mangaId: number + mangaTitle: string + thumbnailUrl: string + chapterId: number + chapterName: string + pageNumber: number + note: string + color: MarkerColor + createdAt: number + updatedAt?: number +} + +export interface ReadLogEntry { + mangaId: number + chapterId: number + readAt: number + minutes: number +} + +export interface ReadingStats { + totalChaptersRead: number + totalMangaRead: number + totalMinutesRead: number + firstReadAt: number + lastReadAt: number + currentStreakDays: number + longestStreakDays: number + lastStreakDate: string +} + +export const DEFAULT_READING_STATS: ReadingStats = { + totalChaptersRead: 0, + totalMangaRead: 0, + totalMinutesRead: 0, + firstReadAt: 0, + lastReadAt: 0, + currentStreakDays: 0, + longestStreakDays: 0, + lastStreakDate: "", +} + +export interface LibraryUpdateEntry { + mangaId: number + mangaTitle: string + thumbnailUrl: string + newChapters: number + checkedAt: number +} diff --git a/src/lib/types/index.ts b/src/lib/types/index.ts new file mode 100644 index 0000000..2b550d6 --- /dev/null +++ b/src/lib/types/index.ts @@ -0,0 +1,539 @@ +import type { + ServerAdapter, + ServerConfig, + ServerStatus, + MangaFilters, + MangaMeta, + PaginatedResult, + Page, + DownloadItem, + UpdateResult, +} from '$lib/server-adapters/types' +import type { Manga, Chapter, Extension, Source, Tracker } from '$lib/types' + +// ─── GQL client ──────────────────────────────────────────────────────────── + +interface GQLResponse { + data: T + errors?: { message: string }[] +} + +// ─── Queries ──────────────────────────────────────────────────────────────── + +const GET_LIBRARY = ` + query GetLibrary { + mangas(condition: { inLibrary: true }) { + nodes { + id title thumbnailUrl inLibrary downloadCount unreadCount bookmarkCount + description status author artist genre inLibraryAt lastFetchedAt + source { id name displayName } + chapters { totalCount } + lastReadChapter { id chapterNumber } + firstUnreadChapter { id chapterNumber } + } + } + } +` + +const GET_MANGA = ` + query GetManga($id: Int!) { + manga(id: $id) { + id title description thumbnailUrl status author artist genre inLibrary realUrl + inLibraryAt lastFetchedAt updateStrategy + source { id name displayName } + lastReadChapter { id chapterNumber lastPageRead } + firstUnreadChapter { id chapterNumber } + highestNumberedChapter { id chapterNumber } + } + } +` + +const GET_CHAPTERS = ` + query GetChapters($mangaId: Int!) { + chapters(condition: { mangaId: $mangaId }) { + nodes { + id name chapterNumber sourceOrder isRead isDownloaded isBookmarked + pageCount mangaId uploadDate realUrl lastPageRead lastReadAt scanlator + } + } + } +` + +const GET_DOWNLOAD_STATUS = ` + query GetDownloadStatus { + downloadStatus { + state + queue { + progress state tries + chapter { + id name pageCount mangaId + manga { id title thumbnailUrl } + } + } + } + } +` + +const GET_EXTENSIONS = ` + query GetExtensions { + extensions { + nodes { + apkName pkgName name lang versionName + isInstalled isObsolete hasUpdate iconUrl + } + } + } +` + +const GET_SOURCES = ` + query GetSources { + sources { + nodes { + id name lang displayName iconUrl isNsfw + isConfigurable supportsLatest + } + } + } +` + +const GET_TRACKERS = ` + query GetTrackers { + trackers { + nodes { + id name icon isLoggedIn isTokenExpired authUrl + supportsPrivateTracking supportsReadingDates supportsTrackDeletion + scores + statuses { value name } + } + } + } +` + +// ─── Mutations ────────────────────────────────────────────────────────────── + +const FETCH_MANGA = ` + mutation FetchManga($id: Int!) { + fetchManga(input: { id: $id }) { + manga { + id title description thumbnailUrl status author artist genre inLibrary realUrl + source { id name displayName } + } + } + } +` + +const FETCH_SOURCE_MANGA = ` + mutation FetchSourceManga($source: LongString!, $type: FetchSourceMangaType!, $page: Int!, $query: String) { + fetchSourceManga(input: { source: $source, type: $type, page: $page, query: $query }) { + mangas { id title thumbnailUrl inLibrary } + hasNextPage + } + } +` + +const UPDATE_MANGA = ` + mutation UpdateManga($id: Int!, $inLibrary: Boolean) { + updateManga(input: { id: $id, patch: { inLibrary: $inLibrary } }) { + manga { id inLibrary } + } + } +` + +const SET_MANGA_META = ` + mutation SetMangaMeta($mangaId: Int!, $key: String!, $value: String!) { + setMangaMeta(input: { meta: { mangaId: $mangaId, key: $key, value: $value } }) { + meta { key value } + } + } +` + +const FETCH_CHAPTERS = ` + mutation FetchChapters($mangaId: Int!) { + fetchChapters(input: { mangaId: $mangaId }) { + chapters { + id name chapterNumber sourceOrder isRead isDownloaded isBookmarked + pageCount mangaId uploadDate realUrl lastPageRead lastReadAt scanlator + } + } + } +` + +const FETCH_CHAPTER_PAGES = ` + mutation FetchChapterPages($chapterId: Int!) { + fetchChapterPages(input: { chapterId: $chapterId }) { pages } + } +` + +const MARK_CHAPTER_READ = ` + mutation MarkChapterRead($id: Int!, $isRead: Boolean!) { + updateChapter(input: { id: $id, patch: { isRead: $isRead } }) { + chapter { id isRead } + } + } +` + +const MARK_CHAPTERS_READ = ` + mutation MarkChaptersRead($ids: [Int!]!, $isRead: Boolean!) { + updateChapters(input: { ids: $ids, patch: { isRead: $isRead } }) { + chapters { id isRead } + } + } +` + +const ENQUEUE_DOWNLOAD = ` + mutation EnqueueDownload($chapterId: Int!) { + enqueueChapterDownload(input: { id: $chapterId }) { + downloadStatus { state } + } + } +` + +const DEQUEUE_DOWNLOAD = ` + mutation DequeueDownload($chapterId: Int!) { + dequeueChapterDownload(input: { id: $chapterId }) { + downloadStatus { state } + } + } +` + +const CLEAR_DOWNLOADER = ` + mutation ClearDownloader { + clearDownloader(input: {}) { + downloadStatus { state } + } + } +` + +const FETCH_EXTENSIONS = ` + mutation FetchExtensions { + fetchExtensions(input: {}) { + extensions { + apkName pkgName name lang versionName + isInstalled isObsolete hasUpdate iconUrl + } + } + } +` + +const UPDATE_EXTENSION = ` + mutation UpdateExtension($id: String!, $install: Boolean, $uninstall: Boolean, $update: Boolean) { + updateExtension(input: { id: $id, patch: { install: $install, uninstall: $uninstall, update: $update } }) { + extension { apkName pkgName name isInstalled hasUpdate } + } + } +` + +const BIND_TRACK = ` + mutation BindTrack($mangaId: Int!, $trackerId: Int!, $remoteId: LongString!) { + bindTrack(input: { mangaId: $mangaId, trackerId: $trackerId, remoteId: $remoteId }) { + trackRecord { id trackerId remoteId } + } + } +` + +const TRACK_PROGRESS = ` + mutation TrackProgress($mangaId: Int!) { + trackProgress(input: { mangaId: $mangaId }) { + trackRecords { id trackerId lastChapterRead status } + } + } +` + +const UPDATE_LIBRARY = ` + mutation UpdateLibrary { + updateLibrary(input: {}) { + updateStatus { jobsInfo { isRunning finishedJobs totalJobs } } + } + } +` + +// ─── Mappers ──────────────────────────────────────────────────────────────── + +function mapChapter(raw: Record): Chapter { + return { + id: raw.id as number, + name: raw.name as string, + chapterNumber: raw.chapterNumber as number, + sourceOrder: raw.sourceOrder as number, + read: (raw.isRead as boolean) ?? false, + downloaded: (raw.isDownloaded as boolean) ?? false, + bookmarked: (raw.isBookmarked as boolean) ?? false, + pageCount: (raw.pageCount as number) ?? 0, + mangaId: raw.mangaId as number, + fetchedAt: raw.fetchedAt as string | undefined, + uploadDate: raw.uploadDate as string | null | undefined, + realUrl: raw.realUrl as string | null | undefined, + lastPageRead: raw.lastPageRead as number | undefined, + lastReadAt: raw.lastReadAt as string | undefined, + scanlator: raw.scanlator as string | null | undefined, + manga: raw.manga as Chapter['manga'], + } +} + +function mapManga(raw: Record): Manga { + const inLibraryAt = raw.inLibraryAt as string | null | undefined + return { + ...(raw as unknown as Manga), + tags: raw.genre as string[] | undefined, + addedAt: inLibraryAt ? new Date(inLibraryAt).getTime() : undefined, + lastReadAt: raw.lastReadChapter + ? Date.now() + : undefined, + } +} + +function mapExtension(raw: Record): Extension { + return { + ...(raw as unknown as Extension), + id: raw.pkgName as string, + } +} + +function mapDownloadItem(raw: Record): DownloadItem { + const chapter = raw.chapter as Record + const manga = chapter?.manga as Record + return { + chapterId: String(chapter?.id), + mangaId: String(chapter?.mangaId ?? manga?.id), + chapterName: chapter?.name as string, + mangaTitle: manga?.title as string, + progress: (raw.progress as number) ?? 0, + state: mapDownloadState(raw.state as string), + } +} + +function mapDownloadState(state: string): DownloadItem['state'] { + switch (state) { + case 'DOWNLOADING': return 'downloading' + case 'FINISHED': return 'finished' + case 'ERROR': return 'error' + default: return 'queued' + } +} + +// ─── Adapter ──────────────────────────────────────────────────────────────── + +export class SuwayomiAdapter implements ServerAdapter { + private baseUrl = 'http://127.0.0.1:4567' + private authHeader: string | null = null + + async connect(config: ServerConfig) { + this.baseUrl = config.baseUrl.replace(/\/$/, '') + if (config.credentials) { + const { username, password } = config.credentials + this.authHeader = 'Basic ' + btoa(`${username}:${password}`) + } + } + + async getStatus(): Promise { + try { + const res = await fetch(`${this.baseUrl}/api/graphql`, { + method: 'POST', + headers: this.headers(), + body: JSON.stringify({ query: '{ aboutServer { name } }' }), + }) + return res.ok ? 'connected' : 'error' + } catch { + return 'disconnected' + } + } + + private headers(): Record { + const h: Record = { 'Content-Type': 'application/json' } + if (this.authHeader) h['Authorization'] = this.authHeader + return h + } + + private async gql(query: string, variables?: Record): Promise { + const res = await fetch(`${this.baseUrl}/api/graphql`, { + method: 'POST', + headers: this.headers(), + body: JSON.stringify({ query, variables }), + }) + if (!res.ok) throw new Error(`Suwayomi HTTP ${res.status}`) + const json: GQLResponse = await res.json() + if (json.errors?.length) throw new Error(json.errors[0].message) + return json.data + } + + // ── Manga ────────────────────────────────────────────────────────────── + + async getManga(id: string): Promise { + const data = await this.gql<{ manga: Record }>( + GET_MANGA, { id: Number(id) } + ) + return mapManga(data.manga) + } + + async getMangaList(filters: MangaFilters): Promise> { + if (filters.inLibrary) { + const data = await this.gql<{ mangas: { nodes: Record[] } }>(GET_LIBRARY) + return { items: data.mangas.nodes.map(mapManga), hasNextPage: false } + } + const data = await this.gql<{ mangas: { nodes: Record[] } }>(GET_LIBRARY) + return { items: data.mangas.nodes.map(mapManga), hasNextPage: false } + } + + async searchManga(query: string, sourceId?: string): Promise { + if (!sourceId) return [] + const data = await this.gql<{ + fetchSourceManga: { mangas: Record[] } + }>(FETCH_SOURCE_MANGA, { + source: sourceId, + type: 'SEARCH', + page: 1, + query, + }) + return data.fetchSourceManga.mangas.map(mapManga) + } + + async addToLibrary(mangaId: string) { + await this.gql(UPDATE_MANGA, { id: Number(mangaId), inLibrary: true }) + } + + async removeFromLibrary(mangaId: string) { + await this.gql(UPDATE_MANGA, { id: Number(mangaId), inLibrary: false }) + } + + async updateMangaMeta(id: string, meta: Partial) { + for (const [key, value] of Object.entries(meta)) { + if (value === undefined) continue + await this.gql(SET_MANGA_META, { + mangaId: Number(id), + key, + value: String(value), + }) + } + } + + // ── Chapters ─────────────────────────────────────────────────────────── + + async getChapters(mangaId: string): Promise { + const data = await this.gql<{ chapters: { nodes: Record[] } }>( + GET_CHAPTERS, { mangaId: Number(mangaId) } + ) + return data.chapters.nodes.map(mapChapter) + } + + async getChapter(id: string): Promise { + const chapters = await this.gql<{ chapters: { nodes: Record[] } }>( + GET_CHAPTERS, { mangaId: 0 } + ) + const found = chapters.chapters.nodes.find(c => String(c.id) === id) + if (!found) throw new Error(`Chapter ${id} not found`) + return mapChapter(found) + } + + async getChapterPages(id: string): Promise { + const data = await this.gql<{ fetchChapterPages: { pages: string[] } }>( + FETCH_CHAPTER_PAGES, { chapterId: Number(id) } + ) + return data.fetchChapterPages.pages.map((url, index) => ({ index, url })) + } + + async markChapterRead(id: string, read: boolean) { + await this.gql(MARK_CHAPTER_READ, { id: Number(id), isRead: read }) + } + + async markChaptersRead(ids: string[], read: boolean) { + await this.gql(MARK_CHAPTERS_READ, { ids: ids.map(Number), isRead: read }) + } + + // ── Downloads ────────────────────────────────────────────────────────── + + async getDownloads(): Promise { + const data = await this.gql<{ + downloadStatus: { queue: Record[] } + }>(GET_DOWNLOAD_STATUS) + return data.downloadStatus.queue.map(mapDownloadItem) + } + + async enqueueDownload(chapterId: string) { + await this.gql(ENQUEUE_DOWNLOAD, { chapterId: Number(chapterId) }) + } + + async dequeueDownload(chapterId: string) { + await this.gql(DEQUEUE_DOWNLOAD, { chapterId: Number(chapterId) }) + } + + async clearDownloads() { + await this.gql(CLEAR_DOWNLOADER) + } + + // ── Extensions ───────────────────────────────────────────────────────── + + async getExtensions(): Promise { + await this.gql(FETCH_EXTENSIONS) + const data = await this.gql<{ extensions: { nodes: Record[] } }>( + GET_EXTENSIONS + ) + return data.extensions.nodes.map(mapExtension) + } + + async installExtension(id: string) { + await this.gql(UPDATE_EXTENSION, { id, install: true }) + } + + async uninstallExtension(id: string) { + await this.gql(UPDATE_EXTENSION, { id, uninstall: true }) + } + + async updateExtension(id: string) { + await this.gql(UPDATE_EXTENSION, { id, update: true }) + } + + async getSources(): Promise { + const data = await this.gql<{ sources: { nodes: Source[] } }>(GET_SOURCES) + return data.sources.nodes + } + + async browseSource(sourceId: string, page: number): Promise> { + const data = await this.gql<{ + fetchSourceManga: { mangas: Record[]; hasNextPage: boolean } + }>(FETCH_SOURCE_MANGA, { + source: sourceId, + type: 'LATEST', + page, + }) + return { + items: data.fetchSourceManga.mangas.map(mapManga), + hasNextPage: data.fetchSourceManga.hasNextPage, + } + } + + // ── Tracking ─────────────────────────────────────────────────────────── + + async getTrackers(): Promise { + const data = await this.gql<{ trackers: { nodes: Tracker[] } }>(GET_TRACKERS) + return data.trackers.nodes + } + + async linkTracker(mangaId: string, trackerId: string, remoteId: string) { + await this.gql(BIND_TRACK, { + mangaId: Number(mangaId), + trackerId: Number(trackerId), + remoteId, + }) + } + + async syncTracking(mangaId: string) { + await this.gql(TRACK_PROGRESS, { mangaId: Number(mangaId) }) + } + + // ── Updates ──────────────────────────────────────────────────────────── + + async checkForUpdates(mangaIds?: string[]): Promise { + if (mangaIds?.length) { + const results: UpdateResult[] = [] + for (const id of mangaIds) { + const before = await this.getChapters(id) + await this.gql(FETCH_CHAPTERS, { mangaId: Number(id) }) + const after = await this.getChapters(id) + results.push({ mangaId: id, newChapters: after.length - before.length }) + } + return results + } + await this.gql(UPDATE_LIBRARY) + return [] + } +} diff --git a/src/lib/types/manga.ts b/src/lib/types/manga.ts new file mode 100644 index 0000000..693408a --- /dev/null +++ b/src/lib/types/manga.ts @@ -0,0 +1,62 @@ +export interface Category { + id: number + name: string + order: number + default: boolean + includeInUpdate: string + includeInDownload: string + mangas?: { nodes: Manga[] } +} + +export interface ChapterRef { + id: number + chapterNumber: number + uploadDate?: string + lastPageRead?: number +} + +export interface Manga { + id: number + title: string + thumbnailUrl: string + inLibrary: boolean + initialized?: boolean + downloadCount?: number + unreadCount?: number + bookmarkCount?: number + hasDuplicateChapters?: boolean + chapters?: { totalCount: number } + description?: string | null + status?: string | null + author?: string | null + artist?: string | null + genre?: string[] + tags?: string[] + realUrl?: string | null + url?: string + sourceId?: string + inLibraryAt?: string | null + lastFetchedAt?: string | null + chaptersLastFetchedAt?: string | null + thumbnailUrlLastFetched?: string | null + addedAt?: number + lastReadAt?: number + age?: string | null + chaptersAge?: string | null + updateStrategy?: 'ALWAYS_UPDATE' | 'ONLY_FETCH_ONCE' + latestFetchedChapter?: ChapterRef | null + latestUploadedChapter?: ChapterRef | null + latestReadChapter?: ChapterRef | null + lastReadChapter?: ChapterRef | null + firstUnreadChapter?: ChapterRef | null + highestNumberedChapter?: ChapterRef | null + source?: { id: string; name: string; displayName: string } | null +} + +export interface MangaDetail extends Manga { + description: string | null + author: string | null + artist: string | null + status: string | null + genre: string[] +} diff --git a/src/lib/types/settings.ts b/src/lib/types/settings.ts new file mode 100644 index 0000000..2016bd7 --- /dev/null +++ b/src/lib/types/settings.ts @@ -0,0 +1,312 @@ +import type { Keybinds } from "$lib/core/keybinds/defaultBinds"; + +export type PageStyle = "single" | "double" | "longstrip"; +export type FitMode = "width" | "height" | "screen" | "original"; +export type LibraryFilter = "all" | "library" | "downloaded" | string; +export type ReadingDirection = "ltr" | "rtl"; +export type ChapterSortDir = "desc" | "asc"; +export type ChapterSortMode = "source" | "chapterNumber" | "uploadDate"; +export type ContentLevel = "strict" | "moderate" | "unrestricted"; + +export type LibrarySortMode = + | "az" | "unreadCount" | "totalChapters" + | "recentlyAdded" | "recentlyRead" | "latestFetched" | "latestUploaded"; + +export type LibrarySortDir = "asc" | "desc"; + +export type LibraryStatusFilter = "ALL" | "ONGOING" | "COMPLETED" | "CANCELLED" | "HIATUS" | "UNKNOWN"; +export type LibraryContentFilter = "unread" | "started" | "downloaded" | "bookmarked" | "marked"; + +export type BuiltinTheme = "original" | "dark" | "light" | "light-contrast" | "midnight" | "warm"; +export type Theme = BuiltinTheme | string; + +export interface ThemeTokens { + "bg-void": string; + "bg-base": string; + "bg-surface": string; + "bg-raised": string; + "bg-overlay": string; + "bg-subtle": string; + "border-dim": string; + "border-base": string; + "border-strong": string; + "border-focus": string; + "text-primary": string; + "text-secondary": string; + "text-muted": string; + "text-faint": string; + "text-disabled": string; + "accent": string; + "accent-dim": string; + "accent-muted": string; + "accent-fg": string; + "accent-bright": string; + "color-error": string; + "color-error-bg": string; + "color-success": string; + "color-info": string; + "color-info-bg": string; +} + +export interface CustomTheme { + id: string; + name: string; + tokens: ThemeTokens; +} + +export const DEFAULT_THEME_TOKENS: ThemeTokens = { + "bg-void": "#080808", + "bg-base": "#0c0c0c", + "bg-surface": "#101010", + "bg-raised": "#151515", + "bg-overlay": "#1a1a1a", + "bg-subtle": "#202020", + "border-dim": "#1c1c1c", + "border-base": "#242424", + "border-strong": "#2e2e2e", + "border-focus": "#4a5c4a", + "text-primary": "#f0efec", + "text-secondary": "#c8c6c0", + "text-muted": "#8a8880", + "text-faint": "#4e4d4a", + "text-disabled": "#2a2a28", + "accent": "#6b8f6b", + "accent-dim": "#2a3d2a", + "accent-muted": "#1a251a", + "accent-fg": "#a8c4a8", + "accent-bright": "#8fb88f", + "color-error": "#c47a7a", + "color-error-bg": "#1f1212", + "color-success": "#7aab7a", + "color-info": "#7a9ec4", + "color-info-bg": "#121a1f", +}; + +export interface MangaPrefs { + autoDownload: boolean; + downloadAhead: number; + deleteOnRead: boolean; + deleteDelayHours: number; + maxKeepChapters: number; + pauseUpdates: boolean; + refreshInterval: "global" | "daily" | "weekly" | "manual"; + preferredScanlator: string; + scanlatorFilter: string[]; + scanlatorBlacklist: string[]; + scanlatorForce: boolean; + autoDownloadScanlators: string[]; + coverUrl?: string; +} + +export const DEFAULT_MANGA_PREFS: MangaPrefs = { + autoDownload: false, + downloadAhead: 0, + deleteOnRead: false, + deleteDelayHours: 0, + maxKeepChapters: 0, + pauseUpdates: false, + refreshInterval: "global", + preferredScanlator: "", + scanlatorFilter: [], + scanlatorBlacklist: [], + scanlatorForce: false, + autoDownloadScanlators: [], +}; + +export interface ReaderSettings { + pageStyle: PageStyle; + fitMode: FitMode; + readingDirection: ReadingDirection; + readerZoom: number; + pageGap: boolean; + optimizeContrast: boolean; + offsetDoubleSpreads: boolean; + barPosition?: "top" | "left" | "right"; +} + +export interface ReaderPreset { + id: string; + name: string; + settings: ReaderSettings; +} + +export interface Settings { + pageStyle: PageStyle; + readingDirection: ReadingDirection; + fitMode: FitMode; + readerZoom: number; + pageGap: boolean; + optimizeContrast: boolean; + offsetDoubleSpreads: boolean; + preloadPages: number; + autoMarkRead: boolean; + autoNextChapter: boolean; + libraryCropCovers: boolean; + libraryPageSize: number; + contentLevel: ContentLevel; + sourceOverridesEnabled: boolean; + nsfwAllowedSourceIds: string[]; + nsfwBlockedSourceIds: string[]; + discordRpc: boolean; + chapterSortDir: ChapterSortDir; + chapterSortMode: ChapterSortMode; + chapterPageSize: number; + uiZoom: number; + compactSidebar: boolean; + gpuAcceleration: boolean; + serverUrl: string; + serverBinary: string; + serverBinaryArgs: string; + autoStartServer: boolean; + suwayomiWebUI: boolean; + preferredExtensionLang: string; + keybinds: Keybinds; + idleTimeoutMin?: number; + splashCards?: boolean; + storageLimitGb: number | null; + markReadOnNext: boolean; + readerDebounceMs: number; + autoBookmark: boolean; + theme: Theme; + libraryBranches: boolean; + renderLimit: number; + heroSlots: (number | null)[]; + mangaLinks: Record; + mangaPrefs: Record>; + serverAuthUser: string; + serverAuthPass: string; + serverAuthMode: "NONE" | "BASIC_AUTH" | "SIMPLE_LOGIN" | "UI_LOGIN"; + socksProxyEnabled: boolean; + socksProxyHost: string; + socksProxyPort: string; + socksProxyVersion: number; + socksProxyUsername: string; + socksProxyPassword: string; + flareSolverrEnabled: boolean; + flareSolverrUrl: string; + flareSolverrTimeout: number; + flareSolverrSessionName: string; + flareSolverrSessionTtl: number; + flareSolverrAsResponseFallback: boolean; + appLockEnabled: boolean; + appLockPin: string; + customThemes: CustomTheme[]; + hiddenCategoryIds: number[]; + defaultLibraryCategoryId: number | null; + savedIsDefaultCategory: boolean; + libraryTabSort: Record; + libraryTabStatus: Record; + libraryTabFilters: Record>>; + maxPageWidth?: number; + uiScale?: number; + extraScanDirs: string[]; + serverDownloadsPath: string; + serverLocalSourcePath: string; + qolAnimations: boolean; + libraryStatsAlways: boolean; + pinnedSourceIds: string[]; + readerPresets: ReaderPreset[]; + mangaReaderSettings: Record; + barPosition?: "top" | "left" | "right"; + trackerSyncBack: boolean; + trackerSyncBackThreshold: number | null; + trackerRespectScanlatorFilter: boolean; + pinchZoom?: boolean; + autoLinkOnOpen: boolean; + downloadToastsEnabled: boolean; + downloadAutoRetry: boolean; + hiddenLibraryTabs: string[]; + libraryPinnedTabOrder: string[]; + autoScroll?: boolean; + autoScrollSpeed?: number; + disableAutoComplete: boolean; +} + +export const DEFAULT_SETTINGS: Settings = { + pageStyle: "longstrip", + readingDirection: "ltr", + fitMode: "width", + readerZoom: 1.0, + pageGap: true, + optimizeContrast: false, + offsetDoubleSpreads: false, + preloadPages: 3, + autoMarkRead: true, + autoNextChapter: true, + libraryCropCovers: true, + libraryPageSize: 48, + contentLevel: "strict", + sourceOverridesEnabled: false, + nsfwAllowedSourceIds: [], + nsfwBlockedSourceIds: [], + discordRpc: false, + chapterSortDir: "desc", + chapterSortMode: "source", + chapterPageSize: 25, + uiZoom: 1.0, + compactSidebar: false, + gpuAcceleration: true, + serverUrl: "http://localhost:4567", + serverBinary: "", + serverBinaryArgs: "", + autoStartServer: true, + suwayomiWebUI: false, + preferredExtensionLang: "en", + keybinds: {} as Keybinds, + idleTimeoutMin: 5, + splashCards: true, + storageLimitGb: null, + markReadOnNext: true, + readerDebounceMs: 120, + autoBookmark: true, + theme: "dark", + libraryBranches: true, + renderLimit: 48, + heroSlots: [null, null, null, null], + mangaLinks: {}, + mangaPrefs: {}, + serverAuthUser: "", + serverAuthPass: "", + serverAuthMode: "NONE", + socksProxyEnabled: false, + socksProxyHost: "", + socksProxyPort: "1080", + socksProxyVersion: 5, + socksProxyUsername: "", + socksProxyPassword: "", + flareSolverrEnabled: false, + flareSolverrUrl: "http://localhost:8191", + flareSolverrTimeout: 60, + flareSolverrSessionName: "moku", + flareSolverrSessionTtl: 15, + flareSolverrAsResponseFallback: false, + appLockEnabled: false, + appLockPin: "", + customThemes: [], + hiddenCategoryIds: [], + defaultLibraryCategoryId: null, + savedIsDefaultCategory: false, + libraryTabSort: {}, + libraryTabStatus: {}, + libraryTabFilters: {}, + extraScanDirs: [], + serverDownloadsPath: "", + serverLocalSourcePath: "", + qolAnimations: true, + libraryStatsAlways: false, + pinnedSourceIds: [], + readerPresets: [], + mangaReaderSettings: {}, + trackerSyncBack: false, + trackerSyncBackThreshold: 20, + trackerRespectScanlatorFilter: true, + pinchZoom: false, + autoLinkOnOpen: false, + downloadToastsEnabled: true, + downloadAutoRetry: false, + hiddenLibraryTabs: [], + libraryPinnedTabOrder: [], + autoScroll: false, + autoScrollSpeed: 5, + disableAutoComplete: false, +}; diff --git a/src/lib/types/tracking.ts b/src/lib/types/tracking.ts new file mode 100644 index 0000000..e689a22 --- /dev/null +++ b/src/lib/types/tracking.ts @@ -0,0 +1,37 @@ +export interface TrackerStatus { + value: number + name: string +} + +export interface TrackRecord { + id: number + trackerId: number + remoteId: string + title: string + status: number + score: number + displayScore: string + lastChapterRead: number + totalChapters: number + remoteUrl: string + startDate?: string + finishDate?: string + private: boolean + libraryId?: string + manga?: { id: number; title: string; thumbnailUrl: string; inLibrary: boolean } +} + +export interface Tracker { + id: number + name: string + icon: string + isLoggedIn: boolean + isTokenExpired: boolean + authUrl: string + supportsPrivateTracking: boolean + supportsReadingDates: boolean + supportsTrackDeletion: boolean + scores: string[] + statuses: TrackerStatus[] + trackRecords?: { nodes: TrackRecord[] } +} diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte new file mode 100644 index 0000000..34f72f6 --- /dev/null +++ b/src/routes/+layout.svelte @@ -0,0 +1,7 @@ + + +{@render children()} diff --git a/src/routes/+layout.ts b/src/routes/+layout.ts new file mode 100644 index 0000000..784e857 --- /dev/null +++ b/src/routes/+layout.ts @@ -0,0 +1,2 @@ +export const ssr = false +export const prerender = true diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte new file mode 100644 index 0000000..cc88df0 --- /dev/null +++ b/src/routes/+page.svelte @@ -0,0 +1,2 @@ +

Welcome to SvelteKit

+

Visit svelte.dev/docs/kit to read the documentation

diff --git a/static/robots.txt b/static/robots.txt new file mode 100644 index 0000000..b6dd667 --- /dev/null +++ b/static/robots.txt @@ -0,0 +1,3 @@ +# allow crawling everything by default +User-agent: * +Disallow: diff --git a/svelte.config.js b/svelte.config.js new file mode 100644 index 0000000..377bf32 --- /dev/null +++ b/svelte.config.js @@ -0,0 +1,23 @@ +import adapterStatic from '@sveltejs/adapter-static'; +import adapterNode from '@sveltejs/adapter-node'; + +const target = process.env.MOKU_TARGET ?? 'static'; + +const adapter = target === 'node' + ? adapterNode() + : adapterStatic({ fallback: 'index.html' }); + +/** @type {import('@sveltejs/kit').Config} */ +const config = { + compilerOptions: { + runes: ({ filename }) => (filename.split(/[/\\]/).includes('node_modules') ? undefined : true), + }, + kit: { + adapter, + files: { + assets: 'static', + }, + }, +}; + +export default config; \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index 99b496c..95cdfd1 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,19 +1,15 @@ { + "extends": "./.svelte-kit/tsconfig.json", "compilerOptions": { - "target": "ESNext", - "useDefineForClassFields": true, - "module": "ESNext", - "moduleResolution": "bundler", - "strict": true, - "isolatedModules": true, - "verbatimModuleSyntax": true, + "rewriteRelativeImportExtensions": true, + "allowJs": true, + "checkJs": true, "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, "skipLibCheck": true, - "paths": { - "$lib/*": ["./src/lib/*"], - "$store/*": ["./src/store/*"], - "$components/*": ["./src/components/*"] - } - }, - "include": ["src/**/*.ts", "src/**/*.svelte", "vite.config.ts"] -} + "sourceMap": true, + "strict": true, + "moduleResolution": "bundler" + } +} \ No newline at end of file diff --git a/vite.config.ts b/vite.config.ts index e8a27a0..6832917 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,33 +1,20 @@ -import { defineConfig } from "vite"; -import { svelte } from "@sveltejs/vite-plugin-svelte"; -import path from "path"; +import { sveltekit } from '@sveltejs/kit/vite'; +import { defineConfig } from 'vite'; export default defineConfig({ - plugins: [svelte()], + plugins: [sveltekit()], clearScreen: false, - resolve: { - alias: { - "@features": path.resolve("./src/features"), - "@core": path.resolve("./src/core"), - "@shared": path.resolve("./src/shared"), - "@api": path.resolve("./src/api"), - "@store": path.resolve("./src/store"), - "@types": path.resolve("./src/types"), - "@design": path.resolve("./src/design"), - "@assets": path.resolve("./src/assets"), - }, - }, server: { port: 1420, strictPort: true, watch: { - ignored: ["**/src-tauri/**"], + ignored: ['**/src-tauri/**'], }, }, - envPrefix: ["VITE_", "TAURI_"], + envPrefix: ['VITE_', 'TAURI_'], build: { - target: ["es2021", "chrome100", "safari13"], - minify: !process.env.TAURI_DEBUG ? "oxc" : false, + target: ['es2021', 'chrome100', 'safari13'], + minify: !process.env.TAURI_DEBUG ? 'oxc' : false, sourcemap: !!process.env.TAURI_DEBUG, }, }); \ No newline at end of file