Compare commits

412 Commits

Author SHA1 Message Date
Youwes09 d6ea1fab67 Fix: Handle Null Extensions on ExtensionLibrary 2026-06-13 02:50:44 -05:00
Youwes09 bf19ee02bc Fix: WebUI Splashscreen Boot & Extensions Issues 2026-06-13 02:48:46 -05:00
Youwes09 09d794da96 Chore: Fixed ServerURL & AppPin on WebUI 2026-06-13 02:25:12 -05:00
Youwes09 baece20f46 Chore: Flatpak Extra Arguments 2026-06-13 01:49:32 -05:00
Youwes09 b170a151f0 Chore: Flatpak Libayatana-AppIndicator 2026-06-12 23:38:44 -05:00
Youwes09 6a84280db0 Chore: Flatpak-Workflow V2 2026-06-12 23:31:03 -05:00
Youwes09 be38d87bec Chore: Update Versions-Script for MacOS 2026-06-12 23:26:19 -05:00
Youwes09 ab9305e6ab Fix: Workflow V2 2026-06-12 22:02:38 -05:00
Youwes09 ceb9ba12d7 Fix: pnpm Version Issues 2026-06-12 18:27:57 -05:00
Youwes09 2fa33bc928 Chore: Post-Bump for v0.10.0 2026-06-12 18:20:55 -05:00
Youwes09 5c703bdba5 Chore: Bump to v0.10.0 2026-06-12 17:59:11 -05:00
Youwes09 a041b182e5 Feat: Static & Flatpak Workflow + Recent Fix 2026-06-12 17:52:09 -05:00
Youwes09 9dad1fb329 Feat: Recent Tab (Unread State) + Bug Fixes 2026-06-12 17:27:08 -05:00
Youwes09 31a19687ce Fix: Browse Bug Fixes & Enhancements 2026-06-12 04:12:33 -05:00
Youwes09 437b52fd8b Feat: Longstrip Viewer(s) & Lag Improvements 2026-06-11 23:27:01 -05:00
Youwes09 1e159bbd73 Fix: Pass all Template Arguments on BugReporter 2026-06-10 13:12:25 -05:00
Youwes09 cb3d8d64fa Fix: DiscordRPC Toggle (Un-tested) 2026-06-10 13:06:46 -05:00
Youwes09 c0a95ff899 Fix: Github Workflows (P.2) 2026-06-10 12:59:08 -05:00
Youwes09 ddaca9d126 Fix: Workflow Failure (P.1) 2026-06-10 12:55:30 -05:00
Youwes09 77b28e97a4 Feat: Bug Reporter (Github Issue Templates) 2026-06-10 12:49:48 -05:00
Youwes09 f10b343108 Fix: Library Mappings 2026-06-09 22:57:50 -05:00
Youwes09 a8ad9034fc Fix: Reader Longstrip Bookmark + ProgressBar 2026-06-09 22:52:11 -05:00
Shozikan f99fa60e8e Merge pull request #95 from frozenKelp/main
fix: idle splash, Discord RPC, memory leaks, reader navigation, cover art, settings scroll
2026-06-09 21:11:29 -05:00
Youwes09 915ff66b2f Chore: ModalBlur Component 2026-06-09 21:08:57 -05:00
Youwes09 abd60f261f Fix: Splashscreen Idle & Dev 2026-06-09 19:24:16 -05:00
Youwes09 fc20835dde Fix: SplashScreen MemoryLeak + WebUI Bypass 2026-06-09 14:54:12 -05:00
frozenKelp 04631d93ef fix: settings modal scroll layer will-change 2026-06-09 21:45:39 +05:30
frozenKelp 5c09cd15ad fix: settings scroll parallax between text and row backgrounds 2026-06-09 20:59:52 +05:30
frozenKelp 7af69fd77c fix: manga detail page shows correct cover instead of stale one 2026-06-09 20:58:56 +05:30
frozenKelp 0b6372bd17 fix: X closes reader to origin page, not previous chapter 2026-06-09 20:58:43 +05:30
frozenKelp 32bdeb92ff fix: set rpc to idle when idle 2026-06-09 20:13:41 +05:30
frozenKelp 22c4a222d8 fix: idle splash exit animation works 2026-06-09 19:43:02 +05:30
frozenKelp 26cb16ec0f Merge branch 'main' of https://github.com/moku-project/Moku 2026-06-09 19:16:10 +05:30
Youwes09 8c2917b698 Chore: Update Server Version 2026-06-09 08:44:09 -05:00
frozenKelp 6d33fb7ae1 fix: make weel and touch passive 2026-06-09 19:10:52 +05:30
frozenKelp 0e7ff1a27c fix: add wheel and touchmove to idle activity listeners 2026-06-09 16:29:48 +05:30
frozenKelp 685bd9b9da fix: revoke page blob URLs on chapter change and reader close 2026-06-09 16:15:31 +05:30
frozenKelp 3926b5d064 chore: clean up discord RPC hooks 2026-06-09 15:16:27 +05:30
frozenKelp 9f6996dcdb fix: read idleTimeoutMin from settings; clean up idle + splash canvas fixes 2026-06-09 15:04:18 +05:30
frozenKelp 294865fe9d style: use specific imports for discord functions 2026-06-09 10:55:22 +05:30
frozenKelp 13e760594d fix: sync libraryShowAllInSaved setting to library state 2026-06-09 10:54:20 +05:30
frozenKelp b44b12ba86 fix: update discord rpc to emit presence when reading chapters 2026-06-09 10:53:34 +05:30
Youwes09 3b8c8dea38 Fix: WebUI Auth & Tauri Auth 2026-06-08 20:27:22 -05:00
Youwes09 615fa1e92f Chore: GQL Cleanup P.3 2026-06-07 18:37:52 -05:00
Youwes09 248b046627 Fix: Home-Screen Recommendations & GQL Cleanup P.2 2026-06-07 15:40:18 -05:00
Youwes09 79e5548879 Fix: Library Filtering + GQL Cleanup P.1 2026-06-07 00:18:45 -05:00
Youwes09 ed4c11ca7e Fix: Local-Source Popular Query + App-Pin Flow 2026-06-06 15:00:59 -05:00
Youwes09 5dfbc80bbe Fix: App Pin & Downloads (Filesystem Changes) 2026-06-05 17:42:32 -05:00
Youwes09 8aa92e6b54 Chore: Update Dependencies 2026-06-03 22:38:07 -05:00
Youwes09 b55dd16d0d Fix: Update to FetcherVersion 3 for NixOS 2026-06-03 22:12:42 -05:00
Youwes09 7c6aeb8f4c Chore: Re-make README + Stack Update 2026-06-03 21:49:01 -05:00
Youwes09 3e4d322fb7 Chore: Fix Nix Build (Improper) 2026-06-03 21:37:34 -05:00
Youwes09 db8a984270 Chore: Remove Old Directory (Prepare for Patches) 2026-06-02 20:04:01 -05:00
Youwes09 18027baee1 Chore: Completed Splash-Screen & Iniital Tauri Wire-Up 2026-06-02 08:27:37 -05:00
Youwes09 c5243ba30c Chore: Port over Reader & Tracking 2026-05-31 21:14:25 -05:00
Youwes09 13f2a483ca Chore: Port over Extensions & Search 2026-05-31 00:30:36 -05:00
Youwes09 6de5207ce7 Chore: Port over SeriesDetail + Panels 2026-05-29 20:07:07 -05:00
Youwes09 8c250021a0 Chore: Port over SeriesDetail (WIP Panels) 2026-05-28 23:05:02 -05:00
Zerebos 584b917f98 fix: Data-theme attribute on document body 2026-05-25 20:59:24 -04:00
Youwes09 e9929747d2 Chore: Fix Zoom & Attempt Theming 2026-05-25 12:31:23 -05:00
Youwes09 cbdf9e8be1 Fix: Stub Capacitor, Fix Tauri-Build, Fix Static-Build 2026-05-25 10:21:12 -05:00
Youwes09 d9a9427e3b Chore: Port over Settings (Barely Works) 2026-05-24 20:31:46 -05:00
Youwes09 ae5d9748c7 Chore: Port over Home & Fix Suwayomi-Server Detection on Web 2026-05-24 12:09:29 -05:00
Youwes09 6c39ef538f Fix: Splashscreen Appears on Boot 2026-05-22 21:39:29 -05:00
Youwes09 081becdd60 Chore: Basic Layout/Chrome + Stubs (WIP) 2026-05-22 21:30:40 -05:00
Youwes09 c891cb349c Chore: Implement Server Adapters & Request Manager 2026-05-22 20:44:55 -05:00
Youwes09 8cef74bb98 Chore: Restructure Repository for SvelteKit 2026-05-22 04:04:59 -05:00
Shozikan bf071dcfc7 Chore: Merge pull request #92 from zerebos/feat/update-panel
Feat/update panel
2026-05-21 22:56:37 -05:00
Youwes09 da788e90ba Feat: Manual Binary-Selection (CSS-WIP) (#91) 2026-05-21 14:37:53 -05:00
Zerebos b0efb183e8 Poll when updating on server 2026-05-21 02:43:06 -04:00
Zerebos 745b6993de Actually grab status from server 2026-05-21 02:33:06 -04:00
Zerebos bd79169f71 Basic caching 2026-05-21 02:23:09 -04:00
Zerebos 6fccf02614 Single line stats 2026-05-21 02:12:21 -04:00
Zerebos fa7cfdc4e6 Use stats boxes on history page 2026-05-21 02:12:21 -04:00
Zerebos 9c614b38f8 More parity between panels 2026-05-21 02:12:21 -04:00
Zerebos 30e50b5a1b Match update cards to download items 2026-05-21 02:12:21 -04:00
Zerebos 8ef0a14363 Add tab icons 2026-05-21 02:12:21 -04:00
Zerebos 4e2ad6cae7 Hoist toolbar into Recent, add status bar, dim read chapters, split cover click 2026-05-21 02:12:15 -04:00
Zerebos 9e56b1176c Integrate updates into recent activity page 2026-05-21 02:12:07 -04:00
Zerebos d025d07e07 Fix updates page data flow 2026-05-21 02:12:01 -04:00
Zerebos f988641446 Add updates page scaffold 2026-05-21 02:11:20 -04:00
Youwes09 3dad4bc729 Feat: Re-Arrangement of Folders (#86) 2026-05-19 20:36:44 -05:00
Youwes09 1af21efebd Feat: Download Storage Threshold Warning (#88) 2026-05-19 20:07:21 -05:00
Youwes09 b7197a09a7 Fix: Attempt to Patch UI Login (Not-Working) 2026-05-19 19:25:43 -05:00
Youwes09 50dd8d7e35 Fix: Preserve Token for NONE Path 2026-05-19 18:55:44 -05:00
Youwes09 b2eaea6552 Merge branch 'main' of github.com:moku-project/Moku 2026-05-19 18:53:47 -05:00
Shozikan 35aae6d85a Chore: Merge PR (#78)
Rework authentication for smoother switching between servers and auth mode
2026-05-19 18:52:48 -05:00
Youwes09 28e5f5625e Merge branch 'fix/auth' 2026-05-19 18:15:00 -05:00
Zerebos b99e4d9a3d Cleanup logs 2026-05-19 02:32:34 -04:00
Youwes09 d5f50c6495 Merge branch 'main' of https://github.com/Youwes09/Moku 2026-05-18 00:32:04 -05:00
Youwes09 89cfa50aff Fix PKGBUILD (V2) 2026-05-18 00:30:33 -05:00
Youwes09 5e591411e4 Chore: Patch PKGBUILD for AUR (V1) 2026-05-17 16:50:40 -05:00
Youwes09 8aaaf2451a Fix: Added ServerBinary Off & Flatpak Patches 2026-05-17 16:27:57 -05:00
Zerebos 75cc767b58 Expiry formatting change 2026-05-17 04:11:33 -04:00
Zerebos d30c623200 Better formatting for dates 2026-05-17 04:08:46 -04:00
Zerebos 017e9bc6da Authenticated fetch jwt settings 2026-05-17 04:00:34 -04:00
Zerebos 3b8088a2bf Decode ISO-8601 2026-05-17 03:50:39 -04:00
Zerebos 2c5320dd1f Some debug logging 2026-05-17 03:31:17 -04:00
Zerebos 1e35f304b6 Add auth debug in devtools 2026-05-17 03:29:27 -04:00
Zerebos 61339ea006 Implement jwt with refresh 2026-05-17 03:04:23 -04:00
Youwes09 f161fc08a2 Chore: Post-Bump 0.9.4 2026-05-17 00:14:22 -05:00
Youwes09 239960683b Chore: Bump for 0.9.4 2026-05-17 00:12:55 -05:00
Youwes09 3b5efc85d0 Fix: ReaderOverlay Draggable 2026-05-17 00:01:55 -05:00
Youwes09 7df3846e75 Feat: Improved PageLoder & Keybinds Fix 2026-05-16 23:36:15 -05:00
Youwes09 01f123f5be Fix: GlobalUIZoom Affecting MangaDisplay (#82) 2026-05-16 23:06:10 -05:00
Youwes09 0e2371096b Feat: Basic ExtensionLibrary Filter 2026-05-16 22:50:00 -05:00
Youwes09 47ae80a7d2 Fix: Cache Adjustments (WIP) 2026-05-16 22:46:45 -05:00
Youwes09 d98547d540 Fix: Cache-Boot (KCEF Corruption) 2026-05-17 03:29:39 -05:00
Youwes09 897ecfd316 Fix: Clear Moku Cache & SelectPortal Zoom (#82) 2026-05-16 22:05:13 -05:00
Youwes09 e3abc72f1b Fix: Duplicate App Instances (#83) 2026-05-16 15:41:07 -05:00
Youwes09 6b56db7cf2 Fix: Exit Button Works 2026-05-16 15:31:13 -05:00
Youwes09 93cedca6b5 Chore: Post-Bump 0.9.3 2026-05-16 08:00:11 -05:00
Youwes09 9f8bf6ffc1 Chore: Tagged 0.9.3 V2 2026-05-16 07:59:35 -05:00
Youwes09 39f813b4d7 Chore: Tagged 0.9.3 2026-05-16 07:57:26 -05:00
Youwes09 18ac38e888 Feat: Disable Auto-Complete on Moku 2026-05-16 07:56:05 -05:00
Shozikan 1e2e923eab Chore: Merge pull request #79 from zerebos/feat/page-loaders
Add circular loaders to pages
2026-05-16 07:41:28 -05:00
Youwes09 d3a40b9152 Fix: System.rs PathBuf Error 2026-05-16 03:54:05 -05:00
Zerebos b1444582a3 Add circular loaders to pages 2026-05-16 01:01:12 -04:00
Zerebos bee8117aac Don't introduce a new key 2026-05-16 00:01:21 -04:00
Zerebos 0bea9c22cb Rework auth to allow smooth switching 2026-05-15 23:50:19 -04:00
Youwes09 bf3f68b996 Feat: Minimize to Sys-Tray + MultiModal (#76) 2026-05-15 21:21:31 -05:00
Youwes09 4b728ad5b7 Feat: Middle-Click for Browser-Auto-Scroll (#70) 2026-05-15 20:41:21 -05:00
Youwes09 f3f91f1555 Feat: Auto-Scroll & Double-Tap Adjustment (#69) 2026-05-15 20:36:15 -05:00
Youwes09 062662781a Feat: Bulk-Source Migration (#66) 2026-05-15 19:49:26 -05:00
Youwes09 cbf8a7fe13 Feat: Extension Settings & Library Filtering (#73) 2026-05-15 19:29:00 -05:00
Youwes09 5af80213c7 Feat: Extension Settings & Library Filtering (#71) (#72) 2026-05-15 07:19:21 -05:00
Youwes09 17d739a1cd Fix: Drag-Region for Reader Bar (#74) 2026-05-14 08:07:14 -05:00
Youwes09 2867dc9612 Fix: Direct-Mouse Scroll (#75) 2026-05-14 08:01:17 -05:00
Youwes09 a9dc047b44 Fix: Toolbar Uniformity & SeriesDetail Redirect (#66) 2026-05-11 20:47:37 -05:00
Youwes09 ef190ae66f Fix: LibraryToolbar Folder Drag 2026-05-11 14:35:05 -05:00
Youwes09 6d921944ac Fix: Library FolderSetting Re-Vamp 2026-05-10 12:07:00 -05:00
Youwes09 244447da9b Feat: Backtracing + NavPage Store 2026-05-10 04:31:27 -05:00
Youwes09 f05f781b5b Fix: Biometric Revision V1 2026-05-10 03:00:08 -05:00
Youwes09 f7c5aebf29 Fix: PerformanceSettings RenderLimit CSS Revision (#63) 2026-05-10 02:50:48 -05:00
Youwes09 e09ae9d2e7 Fix: Respect Page-Order in Loading & Memory Eviction (#61, #63, #68) 2026-05-10 02:17:25 -05:00
Youwes09 7b2ae74c02 Fix: Trigger Recently-Fetched Data for RecentActivity (#63) 2026-05-03 13:06:02 -05:00
Youwes09 0d53e3f102 Fix: Attempt to Improve UI-Login Cache (#63) 2026-05-03 12:29:20 -05:00
Youwes09 093b395cc1 Fix: Re-Try UI Login Token + GQL Wait 2026-05-03 11:47:18 -05:00
Youwes09 efdd8ff95d Fix: Re-Register Settings Export Function (#63) 2026-05-03 11:35:09 -05:00
Youwes09 c0f0ff9bd3 Fix: TrackingSync Excludes Decimals & Respects Chapter Numbers (#63) 2026-05-02 18:14:34 -05:00
Youwes09 3f6049c12d Fix: Remove Scroll Propagation in Reader (#63) 2026-05-02 18:06:37 -05:00
Youwes09 5451a2654b Fix: Wrap ReaderControls in Scrollable (#63) 2026-05-02 17:58:16 -05:00
Youwes09 e625755c5e Fix: Library Folders Clipping (Anim Removed) (#63) 2026-05-02 17:51:54 -05:00
Youwes09 bd95bf4eb1 Fix: Added Download Toggles to Global-Store (#63) 2026-05-02 17:40:07 -05:00
Youwes09 b4d680ddd1 Fix: Error-Handling & ScrollBox on TrackingSettings (#63) 2026-05-02 17:34:54 -05:00
Youwes09 d1b7429b5d Fix: FolderSettings Revamp & Folders (#63) 2026-05-02 17:23:47 -05:00
Youwes09 000195be89 Fix: State-Based Issues & AboutSettings (WIP) 2026-05-02 16:53:50 -05:00
Youwes09 399d429142 Fix: Rust-Cleanup & Flake-SHA Patch 2026-05-01 11:32:29 -05:00
Youwes09 b79ee99e8a Fix: Linked CORS Bypass to UI-LOGIN 2026-05-01 11:09:29 -05:00
Youwes09 80c4b9d9be Chore: Update pnpm-tauri Packages 2026-05-01 01:14:39 -05:00
Youwes09 4584e6e69e Chore: Post-Bump for v0.9.2 2026-05-01 01:09:41 -05:00
Youwes09 83711c155d Chore: Prepare & Update for v0.9.2 2026-05-01 01:07:10 -05:00
Youwes09 3702a25813 Chore: Patch Biometrics 2026-05-01 05:56:39 -05:00
Youwes09 a71cc719ba Chore: Patch all Svelte-Warnings & Add Aria-Labels 2026-05-01 00:38:15 -05:00
Youwes09 1801fecdbb Feat: Windows-Hello Testing (DevTools Only) 2026-05-01 00:09:49 -05:00
Youwes09 0cd799f450 Fix: Cap ReaderSettings Zoom Value to 100 2026-04-30 23:11:39 -05:00
Youwes09 5dab7761bc Feat: Update TrackingPanel 2026-04-30 22:48:58 -05:00
Youwes09 552a11a517 Feat: Library-Refresh Overhaul & Settings Re-Wiring 2026-04-30 22:42:59 -05:00
Youwes09 c8ec6d6b90 Feat: Update Suwayomi (Stable -> Preview) + UI Login 2026-04-30 22:02:45 -05:00
Youwes09 daaeae00fe Fix: Patch Flake & PKGBUILD for Preview 2026-04-30 01:19:55 -05:00
Youwes09 79cb2f7c56 Feat: Shift from Stable to Preview (WIP) 2026-04-30 01:04:56 -05:00
Youwes09 4d3dfdbec6 Feat: Settings Reset, Data Clear, Date Fixes (#56) 2026-04-29 21:07:53 -05:00
Youwes09 78573eacb1 Feat: TouchScreen Support for SeriesDetail & Modularity Revamp (#29 2026-04-29 18:40:41 -05:00
Youwes09 1bb7da3b22 Merge branch 'main' of github.com:moku-project/Moku 2026-04-29 18:18:41 -05:00
Youwes09 dd0cf9372d Feat: Implement CSS for Chrome & Link to Context-Menu 2026-04-29 18:18:21 -05:00
Shozikan 50928c6343 Chore: Commit to Downloads in README 2026-04-29 11:22:54 -05:00
Youwes09 170493aa71 Feat: Implement Storage-based (JSON) Settings & Data-Storage (WIP) (#56) 2026-04-29 00:18:09 -05:00
Youwes09 c009bd71fc Fix: Optimized KeywordTab with BlobURL like TagTab 2026-04-28 23:45:35 -05:00
Youwes09 4df7f416a7 Feat: Revamped Content-Filtering + Levels & Source-Based Toggle 2026-04-28 23:22:29 -05:00
Youwes09 63209cb828 Fix: Futile Attempt to Implement Image-Dedupe (#55) 2026-04-28 22:45:19 -05:00
Youwes09 2c1391c378 Fix: Integrate Sync-Back on Tracker Addition (#52) 2026-04-28 22:24:37 -05:00
Youwes09 41fd4a820c Fix: Revert Logic for TrackingPanel (#58) 2026-04-28 22:17:16 -05:00
Youwes09 9f3c6d2ac3 Fix: LibraryGrid with Z-Index Applied (#57) 2026-04-28 22:09:36 -05:00
Youwes09 f5b3f76b5d Fix: Search Titles Overlay (Z-Index Applied) 2026-04-28 11:58:42 -05:00
Youwes09 528f966b1f Chore: Refactor Rust-Backend Modularity 2026-04-28 10:54:52 -05:00
Youwes09 75e8bc5986 Feat: Cover-Image Switching & Auto-Link (#55) 2026-04-28 01:22:36 -05:00
Youwes09 8123053a40 Chore: Update to Version 0.9.1 (V3) 2026-04-27 23:45:44 -05:00
Youwes09 e33464b05b Chore: Update to Version 0.9.1 (V2) 2026-04-27 21:21:08 -05:00
Youwes09 6f15e8fbc2 Chore: Update Bump & Tauri-Windows 2026-04-27 21:19:20 -05:00
Youwes09 86c6558bab Chore: Update Version to 0.9.1 2026-04-27 21:00:30 -05:00
Youwes09 c041f99c75 Feat: Download Queue Patching & UI Revisions (#54) 2026-04-27 14:40:47 -05:00
Youwes09 84c2a82c2c Feat: Touch Gestures (Pinch Zoom) for Reader (#29) 2026-04-27 13:31:10 -05:00
Youwes09 dc174bee4a Chore: Switched Zoom-Keybinds & Moku-Full SVG (#53) 2026-04-27 11:15:32 -05:00
Youwes09 72496a25e2 Feat: Hide Completed-Mangas in Default 2026-04-26 23:35:06 -05:00
Youwes09 8b074e4b97 Chore: Patch PKGBUILD for AUR (V1) 2026-04-26 22:41:34 -05:00
Youwes09 743f14f561 Chore: Build-Linux Workflow Debugging (V4) 2026-04-26 22:27:48 -05:00
Youwes09 d9ae94f0ff Chore: Build-Linux Workflow Debugging (V3) 2026-04-26 22:23:54 -05:00
Youwes09 045bcc5bc4 Chore: Build-Linux Workflow (V2) 2026-04-26 22:11:39 -05:00
Youwes09 4004a49cfb Chore: Patch for LinuxDeploy 2026-04-26 22:00:05 -05:00
Youwes09 ee72e345bd Chore: Change TauriBuild to MokuProject from moku_project 2026-04-26 21:52:11 -05:00
Youwes09 336ab0a24f Chore: Appimage-Workflow (V1) 2026-04-26 21:41:24 -05:00
Youwes09 c3b015f00f Chore: Change from Youwes09 to moku-project 2026-04-26 21:22:08 -05:00
Shozikan 22e3095cf5 Chore: Change Badge Theme in README 2026-04-26 20:51:48 -05:00
Shozikan 7bc2050971 Chore: Update README with Flatpak & Badges 2026-04-26 20:48:59 -05:00
Youwes09 d26f0b85e3 Feat: TrackingPanel + Tracking Re-design (WIP) 2026-04-26 14:28:45 -05:00
Youwes09 e8e6f18851 Fix: Library-Folder Truncation 2026-04-26 13:58:04 -05:00
Youwes09 50c5131477 Feat: Dual-Sync Tracking (#52) 2026-04-26 13:36:30 -05:00
Youwes09 c0efbba4df Feat: Automated Tracking + Proper Sync 2026-04-26 12:11:45 -05:00
Youwes09 361a145702 Chore: Update Theme-Default & Remove Light-Contrast 2026-04-26 00:29:31 -05:00
Youwes09 1c004d7e5c Chore: Re-Upload Moku-Home Image 2026-04-25 23:39:40 -05:00
Shozikan fb72e45817 Chore: Update README with new Image Layout 2026-04-25 23:38:46 -05:00
Youwes09 5c2e2b6866 Chore: Update Moku Images for 0.9.0 2026-04-25 23:33:59 -05:00
Shozikan 4b313512d4 Chore: Update README with Winget (Attribution included) 2026-04-25 23:24:30 -05:00
Youwes09 63258b2aa1 Feat: Update-All Extensions Button 2026-04-25 17:22:13 -05:00
Youwes09 b5f96a3a5c Feat: System-Default Based Custom Theme Switching (#45) 2026-04-25 15:54:21 -05:00
Youwes09 4eef03cbb1 Fix: Library Folder-Tab Adjustment Enhancement 2026-04-25 15:33:15 -05:00
Youwes09 f6118077fb Feat: Downloads Auto-Retry Button & Toast Enhancements 2026-04-25 15:17:18 -05:00
Youwes09 544792a7ad Fix: MacOS x86 Detection Logic (UNTESTED) 2026-04-25 09:45:34 -05:00
Youwes09 e063369dfb Fix: Flatpak Patches + Fix BulkMove Library 2026-04-25 09:41:11 -05:00
Youwes09 514910667b Chore: Update Tags for v0.9.0 2026-04-24 21:45:06 -05:00
Youwes09 2e9939c4a9 Feat: Per-Manga Reader Settings + Settings Access (#42 & #46) 2026-04-24 21:09:05 -05:00
Youwes09 581aea5694 Feat: Pin Sources on SourceTab (#48) 2026-04-23 22:37:42 -05:00
Youwes09 72a88b10c8 Feat: Always Display Library Stats & Library Stats Overhaul (#47) 2026-04-23 21:44:11 -05:00
Youwes09 371b4af73f Feat: Settings Button in Reader + Dropdown Overhaul + Settings Listener (#46) 2026-04-23 21:35:33 -05:00
Youwes09 634d32f372 Feat: Download Queue Move to Top/Bottom + Tooltip (#38) 2026-04-23 20:54:57 -05:00
Youwes09 4e6be5d9f5 Feat: Import & Export Store + Update Trigger 2026-04-23 20:09:50 -05:00
Youwes09 bb7256c4f8 Feat: BulkAutomationPanel & Z-Index Issue (#39 & #44) 2026-04-23 16:03:36 -05:00
Youwes09 b12ff4cbaa Fix: Derive Auto-Download List from Filter (#39) 2026-04-23 11:27:54 -05:00
Youwes09 63a829ddca Fix: Chapter Nodes in LibraryUpdater 2026-04-23 10:52:15 -05:00
Youwes09 94b14fb7f6 Fix: Patch Cargo to remove TPU (Windows Installer Patch #41) 2026-04-22 23:33:33 -05:00
Youwes09 bd2fd7a6d7 Fix: Windows Auto-Installer (WIP) 2026-04-23 03:58:20 -05:00
Youwes09 6634ad56d2 Fix: Attempt at Windows-Installer without TPU 2026-04-22 22:25:24 -05:00
Youwes09 2eb8a7662e Feat: Home Re-Design & MacOS Detection Fix 2026-04-22 22:15:13 -05:00
Youwes09 7dd4f52308 Fix: Attempt to Fix Tab Boundaries 2026-04-22 10:57:03 -05:00
Youwes09 690f59c602 Fix: Dark-Theme on Settings Slider 2026-04-22 10:34:09 -05:00
Youwes09 c025336a7e Fix: Download Notifications + Control & ContextMenu Icons (#38) 2026-04-21 11:41:02 -05:00
Youwes09 86c78689df Feat: Extension of Download Features, Batch Select, Error/Retry (#38) 2026-04-21 10:57:29 -05:00
Youwes09 2d3a4d0e57 Feat: History Page Revamp 2026-04-20 23:43:57 -05:00
Youwes09 1a5c63a607 Fix: QOL Animation on Extensions 2026-04-20 21:44:30 -05:00
Youwes09 3f7102556b Fix: Attempt at fixing Library Refresh 2026-04-20 21:29:53 -05:00
Youwes09 f5a66ab5d1 Fix: Local Source & QOL Animations 2026-04-20 20:59:42 -05:00
Youwes09 e41e8011be Fix: Dropdown in Settings 2026-04-20 11:35:41 -05:00
Youwes09 044c93a790 Fix: Zoom Values turning to NaN in Reader 2026-04-20 10:59:49 -05:00
Shozikan e49df4501f Chore: Update copyright year and owner in LICENSE file 2026-04-20 08:47:48 -05:00
Youwes09 4b97f4a6c9 Feat: Reworked ENTIRE Project for Readability 2026-04-20 00:19:22 -05:00
Youwes09 005680394e Feat: Re-did Layout & Sidebar 2026-04-17 20:43:18 -05:00
Youwes09 ecb4748414 Fix: Attempted to Patch Filesystem Issues (#32) 2026-04-16 23:14:39 -05:00
Youwes09 f0dc3446b2 Feat: QOL Animations P1 2026-04-16 22:40:22 -05:00
Youwes09 8507c34b21 Fix: MacOS Directory Scan (Patch 1) 2026-04-16 13:05:33 -05:00
Youwes09 78da5915df Fix: Optimizations for Reader 2026-04-16 11:12:08 -05:00
Youwes09 c0c486a53e Feat: Double-Tap for Reader Bar (#29) 2026-04-16 00:29:46 -05:00
Youwes09 236d6bcf08 Chore: Description for Notifications on ChapterRefresh 2026-04-16 00:19:36 -05:00
Youwes09 2b140ae022 Feat: Notifications on Source Install/Uninstall (#31) 2026-04-16 00:13:29 -05:00
Youwes09 38d407092f Chore: Descriptions for Settings 2026-04-16 00:06:04 -05:00
Shozikan 12191dfcdf Merge pull request #35 from kannachi323/dev
fix(ui): slow thumbnail loading
2026-04-15 23:53:58 -05:00
Youwes09 13a2f9ecb7 Chore: Flathub Support (Software-Level Fixed) 2026-04-15 23:53:24 -05:00
Youwes09 c573c54318 Chore: Flathub Support (Tinker V2) 2026-04-15 23:20:49 -05:00
Youwes09 ff5fcc4fc0 Chore: Flathub Support (Tinkering Around) 2026-04-15 18:24:46 -05:00
Youwes09 1aad4a1ff0 Fix: Removed Show-More for Preferential Loading using PR Methods 2026-04-15 17:30:14 -05:00
Matthew Chen 68a9331b6f fix(ui): slow thumbnail loading 2026-04-15 01:58:24 -07:00
Youwes09 64f63ceaa2 Chore: Discover Removal Finalized 2026-04-15 00:44:03 -05:00
Youwes09 6d835914ef Fix: Re-added Marker-Swatch CSS 2026-04-14 20:55:57 -05:00
Youwes09 10f5936dbd Fix: Attempt to fix Reader Page-Misfiring Bug & Optimize Loading (Auth Only) 2026-04-14 20:54:16 -05:00
Youwes09 5ddbfdbd6d Fix: Remove Discover Tab (Not Finished) 2026-04-14 11:22:20 -05:00
Youwes09 0ff148f720 Chore: Merge Discover into Search (WIP) 2026-04-14 11:09:53 -05:00
Youwes09 d98ca76036 Fix: Optimize SplashScreen when Off-Screen 2026-04-14 10:43:12 -05:00
Youwes09 35650481b0 Chore: Update Reader Picture 2026-04-14 00:19:49 -05:00
Youwes09 8b16537c35 Chore: Update README & Pictures 2026-04-14 00:13:48 -05:00
Youwes09 96639d2152 Chore: Swap Extension Icons 2026-04-13 21:31:24 -05:00
Youwes09 1c135a79ca Feat: Enforce & Block for Scanlators 2026-04-13 21:28:57 -05:00
Youwes09 6c11a9d53e Fix: Toaster Dismissal (#27) 2026-04-13 10:59:03 -05:00
Youwes09 5a2f88b806 Fix: Settings LocalHost Auth (#25) 2026-04-13 10:55:41 -05:00
Youwes09 75430305e6 Fix: Reader CSS & TitleBar Controls + WIP Feature 2026-04-13 09:50:07 -05:00
Youwes09 ea76b5fc26 Chore: Update Tags for 0.8.0 2026-04-13 00:10:12 -05:00
Youwes09 d5d9ff8b6e Chore: Language Filter on Extensions 2026-04-13 00:07:38 -05:00
Youwes09 7c9182eb4b Feat: Backup Feature & Settings Overhaul 2026-04-12 23:40:35 -05:00
Youwes09 4d6ebe8804 Fix: Infinite Scroll MAR Threshold & Pages Loaded (Bug #24) 2026-04-12 10:59:52 -05:00
Youwes09 49562c3f76 Feat: Open in File Explorer 2026-04-11 23:04:26 -05:00
Youwes09 4a299f60ac Chore: Library Changes 2026-04-11 19:29:04 -05:00
Youwes09 de397f2462 Feat: Library Manga Updates Display 2026-04-11 19:17:44 -05:00
Youwes09 af29cffdff Feat: Check for Updates (WIP) & Toaster Design Changes 2026-04-11 09:34:22 -05:00
Youwes09 f840ae6413 Feat: Continue Again (Bookmarking-based Resume) 2026-04-10 19:53:20 -05:00
Youwes09 6b8d4fc05f Fix: Reader TitleBar Controls 2026-04-10 19:37:50 -05:00
Youwes09 15079f7755 Feat: Reader Pan + Zoom 2026-04-10 19:30:51 -05:00
Youwes09 1a08d2415f Fix: RTL Keybinds Issue & Progress Bar (Untested) 2026-04-10 19:15:05 -05:00
Youwes09 7917491389 Feat: Default Library Toggle 2026-04-06 22:53:42 -05:00
Youwes09 0b6e9fbbbb Fix: Flatpak Binary Detection 2026-04-06 20:44:34 -05:00
Youwes09 023b23288b Chore: Update for 0.7.1 2026-04-05 20:02:27 -05:00
Youwes09 67a9f0b944 Fix: SplashScreen Scaling on Windows (WIP) 2026-04-06 00:39:16 -05:00
Youwes09 56392e2427 Fix: Caching Logic & Settings Warning for Auth 2026-04-05 11:54:46 -05:00
Youwes09 843e205072 Feat: Scanlator-based Filtering & Directory Changes 2026-04-05 11:36:43 -05:00
Youwes09 ee708d85d0 Fix: Persistent Security State 2026-04-05 00:59:27 -05:00
Youwes09 8005c82654 Fix: Update flake.nix Hash 2026-04-04 23:31:53 -05:00
Youwes09 d989b2d67e Fix: Tauri-Plugin-HTTP for Windows Auth Support (Major WIP) 2026-04-05 04:14:33 -05:00
Youwes09 6446a19b2d Fix: Auth Thumbnails on Windows (WIP) 2026-04-04 19:28:00 -05:00
Youwes09 5cd96abc0c Feat: Switch DRPC Plugins 2026-04-03 22:07:42 -05:00
Youwes09 db44afc4dc Chore: Bump versions for 0.7.0 2026-04-02 23:28:16 -05:00
Youwes09 4248e344ab Chore: Embed AuthURL into Images 2026-04-02 23:19:41 -05:00
Youwes09 8941bfef10 Feat: Improved ThemeEditor with ColorPicker (WIP) 2026-04-02 22:50:19 -05:00
Youwes09 11cd6ff870 Fix: CSS Issues 2026-04-02 22:46:55 -05:00
Youwes09 15adb02be3 Fix: Revise Authentication Methods & Add Edge-Case Handling for Auth 2026-04-02 22:27:39 -05:00
Youwes09 51bb6cdab9 Feat: Markers 2026-04-02 18:07:49 -05:00
Youwes09 454a674ada Merge branch 'main' of github.com:Youwes09/Moku 2026-04-02 11:05:25 -05:00
Youwes09 f146de5c02 Fix: Search Tags + Status (WIP) 2026-04-02 11:05:19 -05:00
Shozikan 04f680c3bb Fix Discord link in README
Updated Discord link in README to new URL.
2026-04-02 09:13:41 -05:00
Youwes09 f49f7e7ac1 Feat: Automation Panel (WIP) & SeriesDetail Additions 2026-04-02 00:56:27 -05:00
Youwes09 a62512bf42 Feat: Filtering in Library 2026-04-01 22:26:29 -05:00
Youwes09 d91ed2e6d1 Chore: Redesign MigrationModal Sources 2026-04-01 15:38:10 -05:00
Youwes09 61e3c4ee2f Chore: Redesign SeriesDetail Elements 2026-04-01 14:25:18 -05:00
Youwes09 9151820843 Fix: TitleBar Issue (WIP) & Allow Sources in Content Settings 2026-04-01 11:09:40 -05:00
Youwes09 63c890dadf Fix: Bookmark Notification Spam 2026-04-01 10:53:18 -05:00
Youwes09 51a33679d5 Chore: Bump for 0.6.7 2026-03-31 23:49:17 -05:00
Youwes09 82f8a9a36b Fix: Forgot Auto-Bookmark Toggle & NSFW On GenreDrill 2026-03-31 22:55:26 -05:00
Youwes09 4decce9a7f Fix: Reworked Bookmark System & Added Double Page (WIP) 2026-03-31 19:46:11 -05:00
Youwes09 a69d5eacc5 Fix: Improved Loading (WIP) 2026-03-31 11:28:00 -05:00
Youwes09 4959722759 Feat: Change Download Directory (WIP) 2026-03-30 23:14:40 -05:00
Youwes09 35ba0171c7 Fix: Zoom Issue & Sidebar Overflow 2026-03-30 00:26:04 -05:00
Youwes09 d26fa50e76 Chore: Bump to 0.6.1 2026-03-30 00:04:54 -05:00
Youwes09 fd9d216325 Fix: Emergency Push + Bookmark Feature (WIP) 2026-03-30 00:02:21 -05:00
Youwes09 581eb2adb0 Fix: Home-Screen Argument for RPC & Total Time 2026-03-29 17:35:25 -05:00
Youwes09 8aa2dc2547 Chore: Prepare for Version 0.6.0 2026-03-29 15:47:12 -05:00
Youwes09 0a11fe3982 Feat: Discord RPC 2026-03-29 15:38:39 -05:00
Youwes09 f6786def87 Fix: SeriesDetail passing Incorrect Args to Reader 2026-03-29 14:03:28 -05:00
Youwes09 262027d9f9 Feat: Added Filtering System in Library (Request: #13) 2026-03-29 13:22:08 -05:00
Youwes09 d407359973 Fix: Added Slight Border to Mitigate Windows Tab Issue (WIP) 2026-03-29 12:58:03 -05:00
Youwes09 a77572a8d4 Fix: Constrained Home-Screen Completed & SplashScreen #15 2026-03-29 12:51:17 -05:00
Youwes09 32d2fffdc5 Fix: Zoom Issue (Bug #14) 2026-03-29 12:40:28 -05:00
Youwes09 e850cbac1e Fix: Bump Update for 0.5.1 2026-03-28 20:17:14 -05:00
Youwes09 eebd1b6446 Fix: Remove Manga Drag & Drop + Libray Move System 2026-03-28 20:09:40 -05:00
Youwes09 5ed072211b Fix: Folder State & Tabs 2026-03-28 19:36:16 -05:00
Youwes09 62e41e5f07 Fix: Reader Store Refactor (Issue #11) & Feat: Drag n Drop (WIP) 2026-03-28 17:15:01 -05:00
Youwes09 4b6d0780c9 Fix: Installation Server Kill [V2] 2026-03-27 20:59:22 -05:00
Youwes09 6ef0facb89 Fix: Installation Server Kill -> Overwrite Error 2026-03-27 20:19:36 -05:00
Youwes09 34d997fc9d Feat: Chapter Organization 2026-03-27 15:46:15 -05:00
Youwes09 1f08b46919 Fix: SplashScreen Default 2026-03-27 15:37:02 -05:00
Youwes09 ac6b70fb32 Feat: Lock-Feature & Server-Authentication + Experimentals 2026-03-26 23:21:39 -05:00
Youwes09 2c93d8743d Fix: Tauri-Overlay Set-False 2026-03-26 00:02:47 -05:00
Youwes09 b9fe54c08d Fix: MacOS Tauri Conf PascalCase 2026-03-25 23:41:43 -05:00
Youwes09 3abb4bb96c Fix: MacOS TitleBar & History Reactive-Glitch 2026-03-25 23:36:18 -05:00
Youwes09 4b3493465d Fix: MacOS Directory Build Change 2026-03-25 00:01:48 -05:00
Youwes09 2163f4a8a6 Fix: Reader Rewrite 2026-03-24 23:52:39 -05:00
Youwes09 fc535f3f74 Fix: Reader Backlog-Glitch & History/Stats Rewrite 2026-03-24 11:44:53 -05:00
Shozikan c819d03222 Fix: README Logical Error 2026-03-23 23:11:16 -05:00
Youwes09 b23292cff5 Chore: README Update 2026-03-23 19:18:27 -05:00
Youwes09 6d85be751a Fix: MacOS Workflow Flatten Directory 2026-03-23 17:46:38 -05:00
Youwes09 06a9e71a90 Fix: MacOS Workflow YAML Error 2026-03-23 11:53:55 -05:00
Youwes09 1a183e7a24 Fix: MacOS Tauri Conf (Build Testing) 2026-03-23 11:46:36 -05:00
Youwes09 dcb3377349 Chore: Standardized UI & Revamped Series-Detail 2026-03-23 11:39:01 -05:00
Youwes09 077ea4dd8f Merge branch 'main' of github.com:Youwes09/Moku 2026-03-23 01:12:25 -05:00
Youwes09 6bdf59db6a Feat: Implemented Basic Tracker Support (Anilist, Mal, Etc) 2026-03-23 01:12:14 -05:00
Shozikan db9ff33c64 Fix: Updated Drafting Stage (Build-Windows) 2026-03-22 16:23:43 -07:00
Shozikan fb1b3d9789 Fix: Patch to Create latest.json 2026-03-22 16:11:13 -07:00
Youwes09 041f735a6e Fix: Windows Key Update 2026-03-22 17:57:28 -05:00
Youwes09 a27c20fabf Fix: Windows Build Type Error (Emitter) 2026-03-22 14:44:05 -05:00
Shozikan 29323c534b Fix: Update logo image in README 2026-03-22 14:39:21 -05:00
Youwes09 a3ef693ed8 Fix: Discover V2 + Windows Update System (Testing) 2026-03-22 14:36:11 -05:00
Youwes09 4691f3aed7 Fix: Discover Cache Refresh & Populating 2026-03-22 09:56:48 -05:00
Youwes09 06cb70048b Feat: Revamped Logo, QOL Home-Screen Additions, Scaling Logic Revamp 2026-03-22 01:26:40 -05:00
Youwes09 d3e62a7a08 Fix: Switch Tauri Conf to Windows Specific 2026-03-21 23:26:33 -05:00
Youwes09 b6ef2b1b3c Fix: Windows Prod-Server Launch 2026-03-21 21:13:58 -07:00
Youwes09 c13a4eb77a Fix: Improve CI Patch 2026-03-20 22:34:32 -05:00
Youwes09 bd972eccf3 Fix: Clear externalBin, Remove Linux CI 2026-03-20 22:29:24 -05:00
Youwes09 9610c0294d Fix: Clear externalBin in Base Config 2026-03-20 22:21:17 -05:00
Youwes09 406819ccca Feat: Bundle Suwayomi JRE for Windows/Linux 2026-03-20 22:13:43 -05:00
Youwes09 272e026210 Fix: Inject Bundle Resources in CI 2026-03-20 21:14:35 -05:00
Youwes09 57bf9d5fb1 Fix: Attempt #1 Windows Workflow 2026-03-20 21:07:19 -05:00
Youwes09 7df7191799 Fix: Attempt to Fix Nix/Flatpak (Testing) 2026-03-20 21:03:53 -05:00
Youwes09 e6b542cd6b Fix: Removed Update Badge from Extensions 2026-03-20 16:01:03 -05:00
Youwes09 4903b066b1 Chore: Finalized Svelte-5 Rewrite (Testing Phase) 2026-03-20 15:58:35 -05:00
Youwes09 96bac1ad2b Chore: Revamped Shared Files for Svelte 5 Rewrite 2026-03-19 23:43:43 -05:00
Youwes09 94b92d000f Chore: Revamped Lib Files for Svelte 5 Rewrite 2026-03-19 23:36:26 -05:00
Youwes09 43630ef72d Chore: Temporary Home-Page (Until Fixed with Rewrite) 2026-03-19 22:53:51 -05:00
Youwes09 161b1f9f52 Chore: Revamped Home-Page (Pending Statistics Display) 2026-03-19 22:17:23 -05:00
Youwes09 816b384d64 Feat: Implemented Series-Link 2026-03-19 22:00:24 -05:00
Youwes09 b772b94c6c Chore: Attempted De-Dupe Patch #1 & Alternative Thumbnails 2026-03-19 21:39:51 -05:00
Youwes09 deb8a5ee02 Chore: Patched Library Completed & Added Home-Page 2026-03-19 21:20:33 -05:00
Youwes09 821e13fc44 chore: migrated context + series-detail + migrate 2026-03-19 00:36:42 -05:00
Youwes09 937054d674 chore: ported over extensions & settings 2026-03-18 23:46:11 -05:00
Youwes09 4532b37201 chore: patched/rewrote reader 2026-03-18 23:16:18 -05:00
Youwes09 73b73e85d7 chore: ported over basics 2026-03-18 23:05:32 -05:00
Youwes09 697116b630 fix: tauri dev added 2026-03-18 22:31:45 -05:00
Youwes09 0e87c51801 chore: init svelte rewrite scaffold 2026-03-18 22:29:03 -05:00
Youwes09 bf38e00cf3 [V1] Fixed Mark as Read Refresh + Auto Feature 2026-03-04 00:00:12 -06:00
Youwes09 eb7360ee05 [V1] Rebased Reader to 9a0afed + Improvements 2026-02-28 18:30:00 -06:00
Youwes09 c9eba3da86 [V1] Fixed Bad State Issue on Reader (WIP) 2026-02-27 22:18:38 -06:00
Youwes09 fc68d3ac7e New Patch for Reader 2026-02-27 17:49:07 -06:00
Youwes09 1fa1c3a2e0 [V1] Search Overhaul + Tag Fixes 2026-02-26 23:55:39 -06:00
Youwes09 8c38330143 [V1] Reader Simplification & Fixes 2026-02-26 23:31:01 -06:00
Youwes09 272d7673ce [V1] Fix NixOS Build 2026-02-26 21:05:24 -06:00
Youwes09 3d074a1fb1 [V1] Attempt on Reader Optimization + Infinite Scroll Glitches 2026-02-26 19:49:48 -06:00
Youwes09 be15cb6ad8 [V1] Patched Tauri Capabilities Permissions 2026-02-25 21:56:05 -06:00
Youwes09 3aee69939b [V1] Forgot to add Binaries prefix 2026-02-25 21:52:57 -06:00
Youwes09 0557f3f2d6 [V1] Fixed Tauri Sidecar Capabilities 2026-02-25 21:51:31 -06:00
Youwes09 817af0d10a [V1] Changed Windows Auto-Detect Binary 2026-02-25 21:40:52 -06:00
Youwes09 70afb08f83 [V1] Updated Search on Workflow 2026-02-25 21:06:43 -06:00
Youwes09 f751f34c68 [V1] Updated Hashing 2026-02-25 21:01:33 -06:00
Youwes09 8c9d3fc783 [V1] Updated SHA Checker 2026-02-25 20:59:19 -06:00
Youwes09 0f0cd87e6d [V1] Windows Workflow 2026-02-25 20:53:20 -06:00
Youwes09 f5a1b13e43 [V1] Attempt to fix Apple Cert Signing 2026-02-25 20:30:06 -06:00
Youwes09 4fca379715 [V1] Fixed Tauri Cert Signing 2026-02-25 20:21:05 -06:00
Youwes09 ac5e3ae53b [V1] Requires Bundle Patch (MacOS) 2026-02-25 20:11:27 -06:00
Youwes09 6d39d5574a [V1] Removed Tauri-MacOS Patch 2026-02-25 20:06:08 -06:00
Youwes09 5e8f0d2f52 [V1] Fix Suwayomi Detection in Workflow 2026-02-25 20:02:11 -06:00
Youwes09 87e2009d4e [V1] MacOS-Patch & Fixes (WIP) 2026-02-25 19:58:37 -06:00
Youwes09 2f5103c48c [V1] Patched Tauri-Targets & Removed Bun Detection 2026-02-25 19:53:12 -06:00
Shozikan 9d9c1b61e7 [V1] Changed to MacOS-Latest & Tauri-Refactor 2026-02-25 19:49:14 -06:00
Shozikan a1a0f360d7 [V1] Fixed ENV & Download Link 2026-02-25 19:43:47 -06:00
Youwes09 9a0afed2b0 [V1] Query-Optimizations & Preparation for MacOS & Windows Compatibility 2026-02-25 19:41:14 -06:00
Youwes09 28e9e3bcf8 [V1] Redid Series Detail Download Layout 2026-02-24 22:02:53 -06:00
Youwes09 ac04c39ead [V1] Prepared for v0.3.0 Release 2026-02-24 20:18:45 -06:00
Youwes09 7d3d76fa6d [V1] Fixed SplashScreen Rasterization/Pixel-Detection 2026-02-24 19:52:17 -06:00
Youwes09 fec0e5d3f6 [V1] Patched MangaPreview & Added Themes (Contrast) 2026-02-24 18:44:19 -06:00
Youwes09 f866d4d0e9 [V1] Major Bug Fixes & Loading Screen (WIP) 2026-02-24 16:14:46 -06:00
Shozikan ac1c0520c5 Change license from MIT to Apache 2.0 2026-02-24 15:28:25 -06:00
Shozikan fff6bde8ad Merge pull request #3 from kx4x/patch-2
Bump package version to 0.2.0
2026-02-24 10:45:31 -08:00
kx4x c07fc90fc8 Bump package version to 0.2.0 2026-02-24 12:54:10 -03:00
Youwes09 523fb40538 [V1] Major Revisions to Search & New Preview + Genre Filter (WIP Commit) 2026-02-23 22:40:00 -06:00
Shozikan fb82abaf21 Merge pull request #1 from kx4x/patch-1
Update repository URL and source in PKGBUILD
2026-02-23 18:04:42 -08:00
kx4x 0a4108218d Update repository URL and source in PKGBUILD 2026-02-23 18:59:21 -03:00
Youwes09 7b61f85833 [V1] Created Toaster & Augmented Explore Tab 2026-02-23 11:36:52 -06:00
Youwes09 cd2d79f80c [V1] Addressed Laggy Single-Page (Applied Cache-Loading) 2026-02-23 10:57:52 -06:00
Youwes09 edf2af8618 [V1] Fixed Downloader Pause/Clear & Began Revision #1 Bug Fixes 2026-02-23 00:03:37 -06:00
342 changed files with 44641 additions and 15390 deletions
+78
View File
@@ -0,0 +1,78 @@
name: Bug Report
description: Something isn't working as expected
labels: ["bug"]
body:
- type: markdown
attributes:
value: |
Thanks for taking the time to report a bug. The more detail you include, the faster it gets fixed.
You can use the **Report a Bug** button in **Settings → About** to pre-fill most of this automatically.
- type: textarea
id: description
attributes:
label: Description
description: What's broken? A clear, concise summary.
placeholder: "e.g. Library card stats don't appear even with 'Always show' enabled"
validations:
required: true
- type: textarea
id: steps
attributes:
label: Steps to Reproduce
description: Exact steps to trigger the bug.
placeholder: |
1. Open Settings → Library
2. Enable "Always show card stats"
3. Return to Library
4. Unread counts are not visible
validations:
required: true
- type: textarea
id: expected
attributes:
label: Expected Behavior
placeholder: "Unread and download counts should be permanently visible on manga cards"
validations:
required: true
- type: textarea
id: actual
attributes:
label: Actual Behavior
placeholder: "Counts only appear on hover, or not at all"
validations:
required: true
- type: textarea
id: environment
attributes:
label: Environment
description: Copy this from Settings → About → Report a Bug, or fill in manually.
placeholder: |
- Moku Version: v0.9.4
- Platform: Windows / macOS / Linux / Web
- OS Version: Windows 11 24H2
- Server: Suwayomi v2.2.2196
- Server URL: localhost:4567
validations:
required: true
- type: textarea
id: settings
attributes:
label: Relevant Settings
description: Settings related to the bug (auto-filled by the in-app reporter, or paste manually).
placeholder: |
libraryStatsAlways: true
libraryCropCovers: true
libraryPageSize: 48
render: yaml
- type: textarea
id: additional
attributes:
label: Additional Context
description: Screenshots, screen recordings, console errors, anything else helpful.
+5
View File
@@ -0,0 +1,5 @@
blank_issues_enabled: false
contact_links:
- name: Discussions (Questions & Support)
url: https://github.com/moku-project/Moku/discussions
about: Not a bug? Ask questions and get help here.
@@ -0,0 +1,47 @@
name: Feature Request
description: Suggest an improvement or new feature
labels: ["enhancement"]
body:
- type: markdown
attributes:
value: |
Got an idea? Describe what you want and why it would be useful.
- type: textarea
id: problem
attributes:
label: Problem / Motivation
description: What's the gap or frustration this would address?
placeholder: "e.g. There's no way to bulk-mark chapters as read without opening each series"
validations:
required: true
- type: textarea
id: solution
attributes:
label: Proposed Solution
description: What would you like to see?
placeholder: "A 'Mark all read' option in the series long-press context menu"
validations:
required: true
- type: textarea
id: alternatives
attributes:
label: Alternatives Considered
description: Any workarounds you've tried, or other ways this could be solved.
- type: textarea
id: environment
attributes:
label: Environment
description: Optional — useful if this is platform-specific.
placeholder: |
- Moku Version: v0.9.4
- Platform: Windows / macOS / Linux / Web
- type: textarea
id: additional
attributes:
label: Additional Context
description: Mockups, references, examples from other apps, etc.
+22
View File
@@ -0,0 +1,22 @@
# Sourced by CI jobs that need versions from nix/versions.nix.
# Usage: source .github/read_versions.sh
# Exports: MOKU_VERSION SUWA_VERSION SUWA_HASH_LINUX SUWA_HASH_MACOS_ARM64 SUWA_HASH_MACOS_X64 SUWA_HASH_WINDOWS
#
# Uses only POSIX -E grep (no -P) so this works on both GNU grep (Linux/Windows)
# and BSD grep (macOS), which does not support -P/PCRE.
_nix="$( cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd )/nix/versions.nix"
_t=$(cat "$_nix")
# Match `key = "value"` with -E, then strip the surrounding quotes.
_pick() { echo "$_t" | grep -oE "${1}"'[[:space:]]*=[[:space:]]*"[^"]+"' | grep -oE '"[^"]+"' | tr -d '"'; }
export MOKU_VERSION=$(_pick "moku")
export SUWA_VERSION=$(_pick "version")
export SUWA_HASH_WINDOWS=$(_pick "windowsHash")
export SUWA_HASH_LINUX=$(_pick "linuxHash")
export SUWA_HASH_MACOS_ARM64=$(_pick "macosArm64Hash")
export SUWA_HASH_MACOS_X64=$(_pick "macosX64Hash")
unset _nix _t
unset -f _pick
-66
View File
@@ -1,66 +0,0 @@
name: Build AppImage
on:
workflow_dispatch:
inputs:
version:
description: "Version tag (e.g. 0.1.0)"
required: false
default: ""
jobs:
build:
runs-on: ubuntu-20.04
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Install system dependencies
run: |
sudo apt-get update
# ubuntu-20.04 ships webkit2gtk 2.44 by default, avoiding the
# EGL_BAD_PARAMETER crash present in 2.46+
# https://github.com/gitbutlerapp/gitbutler/issues/5282
sudo apt-get install -y \
libwebkit2gtk-4.1-dev \
libgtk-3-dev \
libayatana-appindicator3-dev \
librsvg2-dev \
libsoup-3.0-dev \
patchelf \
file
- name: Setup pnpm
uses: pnpm/action-setup@v4
with:
version: latest
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: 22
cache: pnpm
- name: Setup Rust
uses: dtolnay/rust-toolchain@stable
- name: Cache Rust dependencies
uses: Swatinem/rust-cache@v2
with:
workspaces: src-tauri
- name: Install frontend dependencies
run: pnpm install
- name: Build AppImage
run: pnpm tauri build --bundles appimage
env:
NO_STRIP: "true"
- name: Upload AppImage
uses: actions/upload-artifact@v4
with:
name: Moku-${{ github.event.inputs.version || github.sha }}-amd64.AppImage
path: src-tauri/target/release/bundle/appimage/*.AppImage
if-no-files-found: error
+115
View File
@@ -0,0 +1,115 @@
name: Build Flatpak
on:
workflow_dispatch:
inputs:
version:
description: "Version to build (e.g. 0.9.0)"
required: true
permissions:
contents: write
jobs:
flatpak:
name: Build Flatpak bundle
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Free up disk space
run: |
sudo rm -rf /usr/local/lib/android /opt/ghc /usr/share/dotnet /opt/hostedtoolcache/CodeQL
sudo docker image prune -af || true
- uses: pnpm/action-setup@v4
with: { version: 10 }
- uses: actions/setup-node@v4
with:
node-version: 22
cache: pnpm
- name: Build frontend and pack tarball
run: |
pnpm install --frozen-lockfile
pnpm build:static
tar -czf packaging/frontend-dist.tar.gz -C dist .
- name: Compute frontend-dist sha256
run: |
SHA=$(sha256sum packaging/frontend-dist.tar.gz | awk '{print $1}')
echo "FRONTEND_SHA=$SHA" >> $GITHUB_ENV
echo "frontend-dist.tar.gz sha256: $SHA"
- name: Patch frontend-dist sha256 in flatpak manifest
run: |
python3 -c "
import re, pathlib, os
p = pathlib.Path('io.github.moku_project.Moku.yml')
content = p.read_text()
# Replace the sha256 line that follows the frontend-dist.tar.gz source entry
content = re.sub(
r'(path: packaging/frontend-dist\.tar\.gz\n\s+sha256: )[0-9a-f]{64}',
r'\g<1>' + os.environ['FRONTEND_SHA'],
content
)
p.write_text(content)
"
- name: Install flatpak tooling
run: |
sudo apt-get update
sudo apt-get install -y flatpak flatpak-builder
flatpak --user remote-add --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo
- name: Cache flatpak runtimes/SDKs
uses: actions/cache@v4
with:
path: ~/.local/share/flatpak
key: flatpak-runtimes-gnome48-rust-stable
- name: Install runtime and SDK
run: |
flatpak --user install -y --noninteractive flathub \
org.gnome.Platform//48 \
org.gnome.Sdk//48
- name: Build flatpak
run: |
rm -rf build-dir repo
flatpak-builder \
--user \
--install-deps-from=flathub \
--repo=repo \
--force-clean \
build-dir \
io.github.moku_project.Moku.yml
- name: Bundle flatpak
run: |
flatpak build-bundle \
--runtime-repo=https://flathub.org/repo/flathub.flatpakrepo \
repo \
moku.flatpak \
io.github.moku_project.Moku
- name: Upload Flatpak artifact to release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
# Poll for up to 10 minutes — the release is created by the Windows workflow
# which may still be building when the flatpak bundle finishes.
for i in $(seq 1 40); do
RELEASE_ID=$(curl -s -H "Authorization: Bearer $GITHUB_TOKEN" \
"https://api.github.com/repos/moku-project/Moku/releases" \
| jq -r '.[] | select(.tag_name == "v${{ github.event.inputs.version }}") | .id' | head -1)
[ -n "$RELEASE_ID" ] && break
echo "Waiting for release... attempt $i/40"; sleep 15
done
[ -z "$RELEASE_ID" ] && { echo "ERROR: release not found after polling"; exit 1; }
curl -s -X POST \
-H "Authorization: Bearer $GITHUB_TOKEN" \
-H "Content-Type: application/octet-stream" \
--data-binary @"moku.flatpak" \
"https://uploads.github.com/repos/moku-project/Moku/releases/$RELEASE_ID/assets?name=moku.flatpak"
+128
View File
@@ -0,0 +1,128 @@
name: Build Linux
on:
workflow_dispatch:
inputs:
version:
description: "Version to build (e.g. 0.9.0)"
required: true
permissions:
contents: write
jobs:
frontend:
name: Build frontend
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
with: { version: latest }
- uses: actions/setup-node@v4
with:
node-version: 22
cache: pnpm
- run: pnpm install --frozen-lockfile
- run: pnpm build:static
- uses: actions/upload-artifact@v4
with:
name: frontend-dist-linux
path: dist/
retention-days: 1
tauri:
name: Tauri (Linux x64)
needs: frontend
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v4
- uses: actions/download-artifact@v4
with: { name: frontend-dist-linux, path: dist/ }
- name: Read versions
run: |
source .github/read_versions.sh
echo "MOKU_VERSION=$MOKU_VERSION" >> $GITHUB_ENV
echo "SUWA_VERSION=$SUWA_VERSION" >> $GITHUB_ENV
echo "SUWA_HASH=$SUWA_HASH_LINUX" >> $GITHUB_ENV
- name: Install system dependencies
run: |
sudo apt-get update
sudo apt-get install -y \
libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf libfuse2
- uses: dtolnay/rust-toolchain@stable
with: { targets: x86_64-unknown-linux-gnu }
- uses: Swatinem/rust-cache@v2
with: { workspaces: src-tauri }
- uses: pnpm/action-setup@v4
with: { version: latest }
- uses: actions/setup-node@v4
with:
node-version: 22
cache: pnpm
- run: pnpm install --frozen-lockfile
- name: Download Suwayomi (Linux x64)
run: |
curl -fsSL \
"https://github.com/Suwayomi/Suwayomi-Server-preview/releases/download/v${SUWA_VERSION}/Suwayomi-Server-v${SUWA_VERSION}-linux-x64.tar.gz" \
-o suwayomi-linux.tar.gz
echo "${SUWA_HASH} suwayomi-linux.tar.gz" | sha256sum -c -
mkdir -p suwayomi-extracted
tar -xzf suwayomi-linux.tar.gz -C suwayomi-extracted --strip-components=1
- name: Stage Suwayomi bundle
run: |
mkdir -p src-tauri/binaries
for f in suwayomi-extracted/bin/Suwayomi-Server.jar \
suwayomi-extracted/jre/bin/java \
suwayomi-extracted/bin/catch_abort.so; do
[ -e "$f" ] || { echo "ERROR: missing $f"; find suwayomi-extracted -type f | head -40; exit 1; }
done
cp -r suwayomi-extracted src-tauri/binaries/suwayomi-bundle
chmod +x src-tauri/binaries/suwayomi-bundle/jre/bin/java
- name: Stage Linux launcher sidecar
run: |
cp src-tauri/binaries/suwayomi-launcher-linux.sh \
src-tauri/binaries/suwayomi-launcher-linux-x86_64-unknown-linux-gnu
chmod +x src-tauri/binaries/suwayomi-launcher-linux-x86_64-unknown-linux-gnu
- name: Patch tauri.conf.json for CI
run: |
sed -i 's/"beforeBuildCommand": "pnpm build"/"beforeBuildCommand": ""/' src-tauri/tauri.conf.json
- name: Build Tauri app
run: pnpm tauri build --target x86_64-unknown-linux-gnu --config src-tauri/tauri.linux.conf.json --verbose
env: { NO_STRIP: "true" }
- name: Upload Linux artifacts to release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
for i in $(seq 1 12); do
RELEASE_ID=$(curl -s -H "Authorization: Bearer $GITHUB_TOKEN" \
"https://api.github.com/repos/moku-project/Moku/releases" \
| jq -r '.[] | select(.tag_name == "v${{ github.event.inputs.version }}") | .id' | head -1)
[ -n "$RELEASE_ID" ] && break
echo "Waiting for release... attempt $i"; sleep 15
done
[ -z "$RELEASE_ID" ] && { echo "ERROR: release not found"; exit 1; }
upload() {
curl -s -X POST \
-H "Authorization: Bearer $GITHUB_TOKEN" \
-H "Content-Type: application/octet-stream" \
--data-binary @"$1" \
"https://uploads.github.com/repos/moku-project/Moku/releases/$RELEASE_ID/assets?name=$2"
}
APPIMAGE=$(find src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/appimage -name "*.AppImage" | head -1)
DEB=$(find src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/deb -name "*.deb" | head -1)
[ -n "$APPIMAGE" ] && upload "$APPIMAGE" "moku-linux-x64-${{ github.event.inputs.version }}.AppImage"
[ -n "$DEB" ] && upload "$DEB" "moku-linux-x64-${{ github.event.inputs.version }}.deb"
+142
View File
@@ -0,0 +1,142 @@
name: Build macOS
on:
workflow_dispatch:
inputs:
version:
description: "Version to build (e.g. 0.9.0)"
required: true
permissions:
contents: write
jobs:
frontend:
name: Build frontend
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
with: { version: 10 }
- uses: actions/setup-node@v4
with:
node-version: 22
cache: pnpm
- run: pnpm install --frozen-lockfile
- run: pnpm build:static
- uses: actions/upload-artifact@v4
with: { name: frontend-dist, path: dist/, retention-days: 1 }
tauri:
name: Tauri (macOS)
needs: frontend
runs-on: macos-latest
steps:
- uses: actions/checkout@v4
- uses: actions/download-artifact@v4
with: { name: frontend-dist, path: dist/ }
- name: Read versions
run: |
source .github/read_versions.sh
echo "SUWA_VERSION=$SUWA_VERSION" >> $GITHUB_ENV
echo "SUWA_HASH_ARM64=$SUWA_HASH_MACOS_ARM64" >> $GITHUB_ENV
echo "SUWA_HASH_X64=$SUWA_HASH_MACOS_X64" >> $GITHUB_ENV
- uses: dtolnay/rust-toolchain@stable
with:
targets: "aarch64-apple-darwin,x86_64-apple-darwin"
- uses: Swatinem/rust-cache@v2
with: { workspaces: src-tauri }
- uses: pnpm/action-setup@v4
with: { version: 10 }
- uses: actions/setup-node@v4
with:
node-version: 22
cache: pnpm
- run: pnpm install --frozen-lockfile
- name: Download Suwayomi binaries
run: |
dl() {
local asset="$1" sha="$2" outdir="$3"
curl -fsSL \
"https://github.com/Suwayomi/Suwayomi-Server-preview/releases/download/v${SUWA_VERSION}/${asset}" \
-o "${outdir}.tar.gz"
echo "${sha} ${outdir}.tar.gz" | shasum -a 256 -c -
mkdir -p "${outdir}"
tar -xzf "${outdir}.tar.gz" -C "${outdir}" --strip-components=1
}
dl "Suwayomi-Server-v${SUWA_VERSION}-macOS-arm64.tar.gz" "$SUWA_HASH_ARM64" suwayomi-arm64
dl "Suwayomi-Server-v${SUWA_VERSION}-macOS-x64.tar.gz" "$SUWA_HASH_X64" suwayomi-x64
- name: Stage Suwayomi sidecars
run: |
mkdir -p src-tauri/binaries
stage() {
local srcdir="$1" arch="$2"
JAR=$(find "$srcdir" -name "Suwayomi-Server.jar" | head -1)
JAVA=$(find "$srcdir" -path "*/jre/bin/java" | head -1)
[ -z "$JAR" ] && { echo "ERROR: jar not found in $srcdir"; find "$srcdir" -type f | head -30; exit 1; }
[ -z "$JAVA" ] && { echo "ERROR: java not found in $srcdir"; find "$srcdir" -type f | head -30; exit 1; }
cp -r "$srcdir" "src-tauri/binaries/suwayomi-bundle-${arch}"
cp src-tauri/binaries/suwayomi-launcher.sh "src-tauri/binaries/suwayomi-server-${arch}"
chmod +x "src-tauri/binaries/suwayomi-server-${arch}"
}
stage suwayomi-arm64 aarch64-apple-darwin
stage suwayomi-x64 x86_64-apple-darwin
- name: Patch tauri.conf.json for CI
run: |
python3 -c "
import json, pathlib
p = pathlib.Path('src-tauri/tauri.conf.json')
c = json.loads(p.read_text())
c.setdefault('build', {})['beforeBuildCommand'] = ''
p.write_text(json.dumps(c, indent=2))
"
- name: Build Tauri app (aarch64)
run: |
rm -rf src-tauri/binaries/suwayomi-bundle
cp -r src-tauri/binaries/suwayomi-bundle-aarch64-apple-darwin src-tauri/binaries/suwayomi-bundle
pnpm tauri build --target aarch64-apple-darwin --config src-tauri/tauri.macos.conf.json
env:
APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY || '-' }}
- name: Build Tauri app (x86_64)
run: |
rm -rf src-tauri/binaries/suwayomi-bundle
cp -r src-tauri/binaries/suwayomi-bundle-x86_64-apple-darwin src-tauri/binaries/suwayomi-bundle
pnpm tauri build --target x86_64-apple-darwin --config src-tauri/tauri.macos.conf.json
env:
APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY || '-' }}
- name: Upload macOS artifacts to release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
for i in $(seq 1 12); do
RELEASE_ID=$(curl -s -H "Authorization: Bearer $GITHUB_TOKEN" \
"https://api.github.com/repos/moku-project/Moku/releases" \
| jq -r '.[] | select(.tag_name == "v${{ github.event.inputs.version }}") | .id' | head -1)
[ -n "$RELEASE_ID" ] && break
echo "Waiting for release... attempt $i"; sleep 15
done
[ -z "$RELEASE_ID" ] && { echo "ERROR: release not found"; exit 1; }
upload() {
curl -s -X POST \
-H "Authorization: Bearer $GITHUB_TOKEN" \
-H "Content-Type: application/octet-stream" \
--data-binary @"$1" \
"https://uploads.github.com/repos/moku-project/Moku/releases/$RELEASE_ID/assets?name=$2"
}
ARM64=$(find src-tauri/target/aarch64-apple-darwin/release/bundle/dmg -name "*.dmg" | head -1)
X64=$(find src-tauri/target/x86_64-apple-darwin/release/bundle/dmg -name "*.dmg" | head -1)
[ -n "$ARM64" ] && upload "$ARM64" "moku-macos-arm64-${{ github.event.inputs.version }}.dmg"
[ -n "$X64" ] && upload "$X64" "moku-macos-x64-${{ github.event.inputs.version }}.dmg"
+51
View File
@@ -0,0 +1,51 @@
name: Build Static WebUI
on:
workflow_dispatch:
inputs:
version:
description: "Version to build (e.g. 0.9.0)"
required: true
permissions:
contents: write
jobs:
build:
name: Build static frontend
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
with: { version: 10 }
- uses: actions/setup-node@v4
with:
node-version: 22
cache: pnpm
- run: pnpm install --frozen-lockfile
- run: pnpm build:static
- name: Zip static build
run: |
cd dist
zip -r "../moku-webui-${{ github.event.inputs.version }}.zip" .
- name: Upload WebUI artifact to release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
for i in $(seq 1 12); do
RELEASE_ID=$(curl -s -H "Authorization: Bearer $GITHUB_TOKEN" \
"https://api.github.com/repos/moku-project/Moku/releases" \
| jq -r '.[] | select(.tag_name == "v${{ github.event.inputs.version }}") | .id' | head -1)
[ -n "$RELEASE_ID" ] && break
echo "Waiting for release... attempt $i"; sleep 15
done
[ -z "$RELEASE_ID" ] && { echo "ERROR: release not found"; exit 1; }
curl -s -X POST \
-H "Authorization: Bearer $GITHUB_TOKEN" \
-H "Content-Type: application/zip" \
--data-binary @"moku-webui-${{ github.event.inputs.version }}.zip" \
"https://uploads.github.com/repos/moku-project/Moku/releases/$RELEASE_ID/assets?name=moku-webui-${{ github.event.inputs.version }}.zip"
+130
View File
@@ -0,0 +1,130 @@
name: Build Windows
on:
workflow_dispatch:
inputs:
version:
description: "Version to build (e.g. 0.9.0)"
required: true
permissions:
contents: write
jobs:
frontend:
name: Build frontend
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
with: { version: 10 }
- uses: actions/setup-node@v4
with:
node-version: 22
cache: pnpm
- run: pnpm install --frozen-lockfile
- run: pnpm build:static
- uses: actions/upload-artifact@v4
with: { name: frontend-dist-windows, path: dist/, retention-days: 1 }
tauri:
name: Tauri (Windows x64)
needs: frontend
runs-on: windows-latest
steps:
- uses: actions/checkout@v4
- uses: actions/download-artifact@v4
with: { name: frontend-dist-windows, path: dist/ }
- name: Read versions
shell: bash
run: |
source .github/read_versions.sh
echo "SUWA_VERSION=$SUWA_VERSION" >> $GITHUB_ENV
echo "SUWA_HASH=$SUWA_HASH_WINDOWS" >> $GITHUB_ENV
- uses: dtolnay/rust-toolchain@stable
with: { targets: x86_64-pc-windows-msvc }
- uses: Swatinem/rust-cache@v2
with: { workspaces: src-tauri }
- uses: pnpm/action-setup@v4
with: { version: 10 }
- uses: actions/setup-node@v4
with:
node-version: 22
cache: pnpm
- run: pnpm install --frozen-lockfile
- name: Download Suwayomi (Windows x64)
shell: bash
run: |
curl -fsSL \
"https://github.com/Suwayomi/Suwayomi-Server-preview/releases/download/v${SUWA_VERSION}/Suwayomi-Server-v${SUWA_VERSION}-windows-x64.zip" \
-o suwayomi-windows.zip
echo "${SUWA_HASH} suwayomi-windows.zip" | sha256sum -c -
unzip -q suwayomi-windows.zip -d suwayomi-raw
- name: Stage Suwayomi bundle
shell: bash
run: |
mkdir -p suwayomi-extracted
TOP_DIRS=$(find suwayomi-raw -mindepth 1 -maxdepth 1 -type d | wc -l)
TOP_FILES=$(find suwayomi-raw -mindepth 1 -maxdepth 1 -type f | wc -l)
if [ "$TOP_DIRS" -eq 1 ] && [ "$TOP_FILES" -eq 0 ]; then
cp -r "$(find suwayomi-raw -mindepth 1 -maxdepth 1 -type d | head -1)"/. suwayomi-extracted/
else
cp -r suwayomi-raw/. suwayomi-extracted/
fi
mkdir -p src-tauri/binaries
find suwayomi-extracted -path "*/jre/bin/java.exe" | grep -q . \
|| { echo "ERROR: java.exe not found"; find suwayomi-extracted -type f | head -50; exit 1; }
find suwayomi-extracted -name "Suwayomi-Server.jar" | grep -q . \
|| { echo "ERROR: Suwayomi-Server.jar not found"; find suwayomi-extracted -type f | head -50; exit 1; }
cp -r suwayomi-extracted src-tauri/binaries/suwayomi-bundle
- name: Patch tauri.conf.json for CI
shell: bash
run: |
python3 -c "
import json, pathlib
p = pathlib.Path('src-tauri/tauri.conf.json')
c = json.loads(p.read_text())
c.setdefault('build', {})['beforeBuildCommand'] = ''
p.write_text(json.dumps(c, indent=2))
"
- name: Delete existing draft release
shell: bash
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
RELEASE_ID=$(curl -s -H "Authorization: Bearer $GITHUB_TOKEN" \
"https://api.github.com/repos/moku-project/Moku/releases" \
| jq -r '.[] | select(.tag_name == "v${{ github.event.inputs.version }}" and .draft == true) | .id')
if [ -n "$RELEASE_ID" ]; then
curl -s -X DELETE -H "Authorization: Bearer $GITHUB_TOKEN" \
"https://api.github.com/repos/moku-project/Moku/releases/$RELEASE_ID"
curl -s -X DELETE -H "Authorization: Bearer $GITHUB_TOKEN" \
"https://api.github.com/repos/moku-project/Moku/git/refs/tags/v${{ github.event.inputs.version }}"
fi
- name: Build Tauri app + create draft release
uses: tauri-apps/tauri-action@v0
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tagName: v${{ github.event.inputs.version }}
releaseName: Moku v${{ github.event.inputs.version }}
releaseBody: |
Moku v${{ github.event.inputs.version }}
**Windows:** `Moku_${{ github.event.inputs.version }}_x64-setup.exe`
**macOS arm64:** `moku-macos-arm64-${{ github.event.inputs.version }}.dmg`
**macOS x64:** `moku-macos-x64-${{ github.event.inputs.version }}.dmg`
**Linux:** `moku.flatpak`
releaseDraft: true
prerelease: false
args: --target x86_64-pc-windows-msvc --config src-tauri/tauri.windows.conf.json
+22 -8
View File
@@ -1,26 +1,33 @@
# --- Build Artifacts ---
node_modules/
suwayomi-raw/
suwayomi-windows.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
@@ -30,12 +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/
./flatpak-builder
+1
View File
@@ -0,0 +1 @@
engine-strict=true
+1 -1
View File
@@ -186,7 +186,7 @@
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Copyright [2026] [@Youwes09]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
+56 -54
View File
@@ -1,10 +1,10 @@
pkgname=moku
pkgver=0.1.0
pkgver=0.10.0
pkgrel=1
pkgdesc="Native Linux manga reader frontend for Suwayomi-Server"
arch=('x86_64')
url="https://github.com/shozikan/Moku"
license=('MIT')
url="https://github.com/moku-project/Moku"
license=('Apache-2.0')
depends=(
'webkit2gtk-4.1'
'gtk3'
@@ -13,36 +13,46 @@ depends=(
)
makedepends=(
'rust'
'cargo'
'nodejs'
'pnpm'
)
source=(
"$pkgname-$pkgver.tar.gz::https://github.com/shozikan/Moku/archive/refs/tags/v$pkgver.tar.gz"
"suwayomi-server.jar::https://github.com/Suwayomi/Suwayomi-Server/releases/download/v2.1.1867/suwayomi-server-v2.1.1867.jar"
"jdk.tar.gz::https://github.com/adoptium/temurin21-binaries/releases/download/jdk-21.0.3%2B9/OpenJDK21U-jre_x64_linux_hotspot_21.0.3_9.tar.gz"
optdepends=(
'discord: Discord rich presence'
)
options=('!strip')
source=(
"$pkgname-$pkgver.tar.gz::https://github.com/moku-project/Moku/archive/refs/tags/v$pkgver.tar.gz"
"Suwayomi-Server-v2.1.2087.jar::https://github.com/Suwayomi/Suwayomi-Server-preview/releases/download/v2.1.2087/Suwayomi-Server-v2.1.2087.jar"
)
noextract=("Suwayomi-Server-v2.1.2087.jar")
sha256sums=(
'0019dfc4b32d63c1392aa264aed2253c1e0c2fb09216f8e2cc269bbfb8bb49b5'
'51e307c2581e4e1a002991ab3e3a77503c8b074c42695987a984a7382d0ac5af'
'f1af100c4afca2035f446967323230150cfe5872b5a664d98c86963e5c066e0d'
'589b389b356a48d54ad4022e68eaac165a1de654d0b98edec79ebbab2c4a1275'
'f589a422674252394c13b289a9c8be691905bf583efb7f4d5f1501ae5e91e6b3'
)
b2sums=(
'SKIP'
'SKIP'
)
prepare() {
cd "Moku-$pkgver"
pnpm install --frozen-lockfile
sed -i 's/^lto\s*=\s*true/lto = "thin"/' src-tauri/Cargo.toml
mkdir -p src-tauri/.cargo
cat > src-tauri/.cargo/config.toml << 'EOF'
[target.x86_64-unknown-linux-gnu]
linker = "x86_64-linux-gnu-gcc"
EOF
}
build() {
cd "Moku-$pkgver"
# Build frontend
pnpm build
# Repack dist for Tauri
tar -czf packaging/frontend-dist.tar.gz -C dist .
# Build Tauri binary
local fixed_cflags="${CFLAGS/-march=native/-march=x86-64}"
fixed_cflags="${fixed_cflags/-flto=auto/}"
local fixed_cxxflags="${CXXFLAGS/-march=native/-march=x86-64}"
fixed_cxxflags="${fixed_cxxflags/-flto=auto/}"
CFLAGS="$fixed_cflags" \
CXXFLAGS="$fixed_cxxflags" \
TAURI_SKIP_DEVSERVER_CHECK=true cargo build \
--release \
--manifest-path src-tauri/Cargo.toml
@@ -51,21 +61,14 @@ build() {
package() {
cd "Moku-$pkgver"
# Moku binary
install -Dm755 src-tauri/target/release/moku \
"$pkgdir/usr/bin/moku"
# Bundled JRE
install -dm755 "$pkgdir/usr/lib/moku/jre"
tar -xf "$srcdir/jdk.tar.gz" -C "$pkgdir/usr/lib/moku/jre" --strip-components=1
# Suwayomi server jar
install -Dm644 "$srcdir/suwayomi-server.jar" \
install -Dm644 "$srcdir/Suwayomi-Server-v2.1.2087.jar" \
"$pkgdir/usr/lib/moku/tachidesk/Suwayomi-Server.jar"
# tachidesk-server wrapper script
install -dm755 "$pkgdir/usr/lib/moku/tachidesk/default-conf"
cat > "$pkgdir/usr/lib/moku/tachidesk/default-conf/server.conf" << 'EOF'
cat > "$pkgdir/usr/lib/moku/tachidesk/default-conf/server.conf" << 'CONF'
server.ip = "127.0.0.1"
server.port = 4567
server.webUIEnabled = false
@@ -76,22 +79,22 @@ server.autoDownloadNewChapters = false
server.globalUpdateInterval = 12
server.maxSourcesInParallel = 6
server.extensionRepos = []
EOF
CONF
install -Dm755 /dev/stdin "$pkgdir/usr/bin/tachidesk-server" << 'EOF'
install -Dm755 /dev/stdin "$pkgdir/usr/bin/moku-suwayomi" << 'LAUNCHER'
#!/bin/sh
DATA_DIR="${XDG_DATA_HOME:-$HOME/.local/share}/moku/tachidesk"
DATA_DIR="${XDG_DATA_HOME:-$HOME/.local/share}/Tachidesk"
mkdir -p "$DATA_DIR"
if [ ! -f "$DATA_DIR/server.conf" ]; then
cp /usr/lib/moku/tachidesk/default-conf/server.conf "$DATA_DIR/server.conf"
cp /usr/lib/moku/tachidesk/default-conf/server.conf "$DATA_DIR/server.conf"
fi
sed -i \
-e 's|server\.webUIEnabled.*|server.webUIEnabled = false|' \
-e 's|server\.initialOpenInBrowserEnabled.*|server.initialOpenInBrowserEnabled = false|' \
-e 's|server\.systemTrayEnabled.*|server.systemTrayEnabled = false|' \
"$DATA_DIR/server.conf"
-e 's|server\.webUIEnabled.*|server.webUIEnabled = false|' \
-e 's|server\.initialOpenInBrowserEnabled.*|server.initialOpenInBrowserEnabled = false|' \
-e 's|server\.systemTrayEnabled.*|server.systemTrayEnabled = false|' \
"$DATA_DIR/server.conf"
grep -q 'server\.webUIEnabled' "$DATA_DIR/server.conf" || echo 'server.webUIEnabled = false' >> "$DATA_DIR/server.conf"
grep -q 'server\.initialOpenInBrowserEnabled' "$DATA_DIR/server.conf" || echo 'server.initialOpenInBrowserEnabled = false' >> "$DATA_DIR/server.conf"
@@ -102,26 +105,25 @@ unset WAYLAND_DISPLAY
export _JAVA_OPTIONS="-Djava.awt.headless=true"
export JAVA_TOOL_OPTIONS="-Djava.awt.headless=true"
exec /usr/lib/moku/jre/bin/java \
-Djava.awt.headless=true \
-Dapple.awt.UIElement=true \
-Dsun.java2d.noddraw=true \
-Dsun.awt.disablegui=true \
-Dsuwayomi.tachidesk.config.server.rootDir="$DATA_DIR" \
-jar /usr/lib/moku/tachidesk/Suwayomi-Server.jar
EOF
exec java \
-Djava.awt.headless=true \
-Dapple.awt.UIElement=true \
-Dsun.java2d.noddraw=true \
-Dsun.awt.disablegui=true \
-Dsuwayomi.tachidesk.config.server.rootDir="$DATA_DIR" \
-jar /usr/lib/moku/tachidesk/Suwayomi-Server.jar
LAUNCHER
# Desktop entry and icons
install -Dm644 packaging/dev.moku.app.desktop \
"$pkgdir/usr/share/applications/dev.moku.app.desktop"
install -Dm644 packaging/io.github.moku_project.Moku.desktop \
"$pkgdir/usr/share/applications/io.github.moku_project.Moku.desktop"
install -Dm644 src-tauri/icons/32x32.png \
"$pkgdir/usr/share/icons/hicolor/32x32/apps/dev.moku.app.png"
"$pkgdir/usr/share/icons/hicolor/32x32/apps/io.github.moku_project.Moku.png"
install -Dm644 src-tauri/icons/128x128.png \
"$pkgdir/usr/share/icons/hicolor/128x128/apps/dev.moku.app.png"
"$pkgdir/usr/share/icons/hicolor/128x128/apps/io.github.moku_project.Moku.png"
install -Dm644 src-tauri/icons/128x128@2x.png \
"$pkgdir/usr/share/icons/hicolor/256x256/apps/dev.moku.app.png"
install -Dm644 packaging/dev.moku.app.metainfo.xml \
"$pkgdir/usr/share/metainfo/dev.moku.app.metainfo.xml"
install -Dm644 LICENSE "$pkgdir/usr/share/licenses/$pkgname/LICENSE"
"$pkgdir/usr/share/icons/hicolor/256x256/apps/io.github.moku_project.Moku.png"
install -Dm644 packaging/io.github.moku_project.Moku.metainfo.xml \
"$pkgdir/usr/share/metainfo/io.github.moku_project.Moku.metainfo.xml"
install -Dm644 LICENSE \
"$pkgdir/usr/share/licenses/$pkgname/LICENSE"
}
+120 -90
View File
@@ -1,134 +1,164 @@
<div align="center">
<img src="src/assets/rounded-logo.png" width="96" />
<h1>Moku</h1>
<p>A fast, minimal manga reader for <a href="https://github.com/Suwayomi/Suwayomi-Server">Suwayomi-Server</a>.<br/>Built with Tauri v2 and React.</p>
<img src="docs/banner.svg" width="100%" alt="Moku" />
</div>
<table>
<tr>
<td><img src=".github/screenshots/Library-Page.png" width="100%" /></td>
<td><img src=".github/screenshots/Libary-Browse.png" width="100%" /></td>
<td><img src=".github/screenshots/Series-Detail.png" width="100%" /></td>
</tr>
<tr>
<td><img src=".github/screenshots/Search-Bar.png" width="100%" /></td>
<td><img src=".github/screenshots/Download-Manager.png" width="100%" /></td>
<td><img src=".github/screenshots/Settings-1.png" width="100%" /></td>
</tr>
</table>
<div align="center">
[![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)
</div>
<br/>
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
<div align="center">
<img src="docs/screenshots/Moku-Home.png" width="100%" alt="Home" />
</div>
<div align="center">
<img src="docs/screenshots/Moku-Search.png" width="49%" alt="Search" />
<img src="docs/screenshots/Moku-TagSearch.png" width="49%" alt="Tag Search" />
<img src="docs/screenshots/Moku-Settings.png" width="49%" alt="Settings" />
<img src="docs/screenshots/Moku-Preview.png" width="49%" alt="Preview" />
<img src="docs/screenshots/Moku-Downloads.png" width="49%" alt="Downloads" />
<img src="docs/screenshots/Moku-ReaderSettings.png" width="49%" alt="Reader Settings" />
</div>
<div align="center">
<a href="docs/screenshots">View all screenshots →</a>
</div>
---
## Features
### Reader
- **Single**, **double-page**, and **longstrip** reading modes
- **Infinite longstrip** — when Auto mode is enabled, the next chapter's pages are appended directly into the scroll without any re-render or gap; the entire series flows as one seamless ribbon
- Fit modes: fit width, fit height, fit screen, and 1:1 original
- Per-series zoom control via Ctrl+scroll or a slider popover
- RTL / LTR reading direction toggle
- Configurable page gaps
- Full keyboard navigation with rebindable keybinds
- UI auto-hides after 3 seconds of inactivity; reappears on cursor movement near edges
- Chapter-relative page counter that updates live as you scroll through the infinite strip
- Auto-mark chapters as read when the last page is reached
### Library
- Grid view of your entire manga collection with lazy-loaded cover art
- Filter tabs: **Saved**, **Downloaded**, and **All**
- Genre tag filter chips — multi-select to narrow by any combination of tags
- In-line search
- Context menu: open, add/remove from library
### Series Detail
- Cover, author, artist, status badge, genres, and synopsis
- Read progress bar with percentage
- Continue / Start / Re-read button that picks up exactly where you left off (including mid-chapter page)
- Chapter list with scanlator, upload date, and in-progress page indicator
- **Grid view** — displays all chapters as numbered tiles; read/unread/in-progress states are visually distinct at a glance; switches between list and grid with a single click
- Sort by newest or oldest first
- Jump-to-chapter input
- Bulk download menu: from current chapter, unread only, or all
- Per-chapter context menu: mark read/unread, mark all above as read, download, delete, bulk download from here
- Collapsible source details panel with source ID, language, and source migration
### Search
- Cross-source search running up to 3 concurrent requests
- Language filter bar (preferred language default, per-language, or all)
- Results grouped by source with skeleton loading states
### Sources & Extensions
- Browse and search installed sources, grouped by extension with per-language expansion
- Extension manager: install, update, remove, and install from external APK URL
- Repo refresh with update count badge
### Downloads
- Download queue with live progress
### History
- Reading history grouped by day with relative timestamps
- Per-entry thumbnail, chapter name, and last-read page
- Full-text search across titles and chapter names
- One-click clear
---
## Requirements
[Suwayomi-Server](https://github.com/Suwayomi/Suwayomi-Server) must be running. By default Moku expects it at `http://127.0.0.1:4567`.
> Moku will attempt to launch the server automatically on startup if the `suwayomi-server` binary is on your `PATH`.
- **Library management** — organize manga into folders, track unread counts, filter by genre
- **Per-folder sorting & filtering** — each folder has its own independent sort (unread, AZ, 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 reading (accessible from Series Detail)
- **Discord Rich Presence** — shows manga title, current chapter, and 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
**Nix (recommended)**
<div align="center">
![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)
</div>
### Windows
**winget:**
```powershell
winget install Moku.Moku
```
> Thanks to [@frozenKelp](https://github.com/frozenKelp) for setting up and maintaining the winget package through v0.9.0.
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
nix run github:Youwes09/moku
flatpak install io.github.moku_app.Moku
```
Or download the latest `moku.flatpak` from the [releases page](https://github.com/moku-project/Moku/releases/latest) and install manually:
```bash
flatpak install moku.flatpak
```
### Nix
```bash
nix run github:moku-project/Moku
```
Add to your flake:
```nix
inputs.moku.url = "github:Youwes09/moku";
inputs.moku.url = "github:moku-project/Moku";
```
**From source**
### macOS
```bash
git clone https://github.com/Youwes09/moku
cd moku
nix build
./result/bin/moku
```
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
```
> `tauri:dev` uses `src-tauri/tauri.dev.conf.json` to point at the Vite dev server, keeping the release build config clean for `nix build`.
---
## Stack
| | |
|---|---|
| [Tauri v2](https://tauri.app) | Native app shell |
| [React](https://react.dev) + [TypeScript](https://www.typescriptlang.org) | UI |
| [Vite](https://vitejs.dev) | Frontend bundler |
| [Zustand](https://zustand-demo.pmnd.rs) | State management |
| [Phosphor Icons](https://phosphoricons.com) | Icon set |
| [Crane](https://github.com/ipetkov/crane) | Nix Rust builds |
| [Svelte 5](https://svelte.dev) + [SvelteKit 2](https://kit.svelte.dev) + [TypeScript](https://www.typescriptlang.org) | UI |
| [Vite 8](https://vitejs.dev) | Frontend bundler |
| [Nixpkgs stdenv](https://nixos.org/manual/nixpkgs/stable/) | Nix 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)
---
+2 -20
View File
@@ -1,22 +1,4 @@
Todo:
1. Check all Keybind Toggles
2. Update ReadME with Comprehensive Feature List
3. Explore Manga Upscaler
4. Add Zoom-Slider for Zoom in Manga Reader
Revival of the TODO List!!!!!
Bugs:
2. Fix Auto-scroll between chapters & state when moving chapters (Implemented but not Fluidic)
3. Patch Chapters to Grid View
5. Fix Keybind Toggles
Features:
1. Frecency based Manga Suggestions
2. Proper Explore Tab
Big Revisions:
1. Anime & Novel Support
Test:
1. URL & Extension Additions
- Reminder to Completely Test Settings
-184
View File
@@ -1,184 +0,0 @@
#!/usr/bin/env bash
# build-scripts/release.sh
# ─────────────────────────────────────────────────────────────────────────────
# Usage:
# ./build-scripts/release.sh 0.2.0 — full release (AUR + Flatpak)
# ./build-scripts/release.sh 0.2.0 --aur — AUR bin package only
# ./build-scripts/release.sh 0.2.0 --flatpak — Flatpak sources + bundle only
#
# Requires: nix, podman (for AUR .SRCINFO generation in Arch container)
set -euo pipefail
# ── Colour helpers ─────────────────────────────────────────────────────────────
RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'
CYAN='\033[0;36m'; BOLD='\033[1m'; RESET='\033[0m'
info() { echo -e "${CYAN}${RESET} $*"; }
success() { echo -e "${GREEN}${RESET} $*"; }
warn() { echo -e "${YELLOW}${RESET} $*"; }
die() { echo -e "${RED}${RESET} $*" >&2; exit 1; }
section() { echo -e "\n${BOLD}── $* ──${RESET}"; }
# ── Args ───────────────────────────────────────────────────────────────────────
[[ $# -lt 1 ]] && die "Usage: $0 <version> [--aur|--flatpak]"
VERSION="$1"
MODE="${2:-all}"
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
AUR_DIR="${REPO_ROOT}/../moku-bin"
TARBALL="moku-${VERSION}-x86_64.tar.gz"
FLATPAK_MANIFEST="${REPO_ROOT}/dev.moku.app.yml"
# ── Sanity checks ──────────────────────────────────────────────────────────────
section "Pre-flight"
command -v nix &>/dev/null || die "nix not found"
if [[ "$MODE" == "all" || "$MODE" == "--aur" ]]; then
command -v podman &>/dev/null || die "podman not found — needed for Arch container (makepkg)"
[[ -d "$AUR_DIR" ]] || die "AUR dir not found at $AUR_DIR\nClone it first:\n git clone ssh://aur@aur.archlinux.org/moku-bin.git ../moku-bin"
[[ -f "${AUR_DIR}/PKGBUILD" ]] || die "PKGBUILD not found in $AUR_DIR"
fi
success "OK"
# ── Bump versions ──────────────────────────────────────────────────────────────
section "Bumping version → ${VERSION}"
sed -i "s/\"version\": \"[^\"]*\"/\"version\": \"${VERSION}\"/" \
"${REPO_ROOT}/src-tauri/tauri.conf.json"
success "tauri.conf.json → ${VERSION}"
sed -i "0,/^version = \"[^\"]*\"/s//version = \"${VERSION}\"/" \
"${REPO_ROOT}/src-tauri/Cargo.toml"
success "Cargo.toml → ${VERSION}"
# ── Build frontend ─────────────────────────────────────────────────────────────
section "Building frontend"
cd "$REPO_ROOT"
nix develop --command pnpm install --frozen-lockfile
nix develop --command pnpm build
success "Frontend built → dist/"
# ── Build Rust binary ──────────────────────────────────────────────────────────
section "Building Rust binary"
nix develop --command cargo build --release --manifest-path src-tauri/Cargo.toml
BINARY="${REPO_ROOT}/src-tauri/target/release/moku"
[[ -f "$BINARY" ]] || die "Binary not found: $BINARY"
success "Binary → $BINARY"
# ── Flatpak ────────────────────────────────────────────────────────────────────
if [[ "$MODE" == "all" || "$MODE" == "--flatpak" ]]; then
section "Regenerating cargo-sources.json"
cd "$REPO_ROOT"
nix-shell \
-p "python311.withPackages(ps: [ ps.aiohttp ps.tomlkit ])" \
--run "python3 packaging/flatpak-cargo-generator.py src-tauri/Cargo.lock -o packaging/cargo-sources.json"
success "cargo-sources.json updated"
section "Rebuilding frontend-dist.tar.gz"
tar -czf packaging/frontend-dist.tar.gz -C dist .
FRONTEND_SHA=$(sha256sum packaging/frontend-dist.tar.gz | awk '{print $1}')
success "frontend-dist.tar.gz rebuilt sha256: ${FRONTEND_SHA}"
# Patch the sha256 in dev.moku.app.yml automatically via a temp script
PATCH_SCRIPT=$(mktemp /tmp/patch-sha256-XXXXXX.py)
cat > "$PATCH_SCRIPT" << PYEOF
import re, sys
path = "${FLATPAK_MANIFEST}"
new_sha = "${FRONTEND_SHA}"
text = open(path).read()
pattern = r'(path:\s*packaging/frontend-dist\.tar\.gz\s*\n\s*sha256:\s*)[0-9a-f]+'
replacement = r'\g<1>' + new_sha
updated, n = re.subn(pattern, replacement, text)
if n == 0:
sys.exit("Could not find frontend-dist sha256 in dev.moku.app.yml")
open(path, 'w').write(updated)
PYEOF
nix-shell -p python3 --run "python3 '$PATCH_SCRIPT'"
rm -f "$PATCH_SCRIPT"
success "dev.moku.app.yml sha256 updated"
section "Building Flatpak bundle"
rm -rf "${REPO_ROOT}/build-dir" "${REPO_ROOT}/repo"
nix shell nixpkgs#appstream nixpkgs#flatpak-builder --command \
flatpak-builder \
--repo="${REPO_ROOT}/repo" \
--force-clean \
"${REPO_ROOT}/build-dir" \
"$FLATPAK_MANIFEST"
flatpak build-bundle \
"${REPO_ROOT}/repo" \
"${REPO_ROOT}/moku.flatpak" \
dev.moku.app
# Clean up intermediate build artefacts — keep only moku.flatpak
rm -rf "${REPO_ROOT}/build-dir" "${REPO_ROOT}/repo"
success "moku.flatpak created"
fi
# ── AUR tarball + PKGBUILD ─────────────────────────────────────────────────────
if [[ "$MODE" == "all" || "$MODE" == "--aur" ]]; then
section "Assembling release tarball"
cd "$REPO_ROOT"
STAGE="release-${VERSION}"
rm -rf "$STAGE"
install -Dm755 "$BINARY" "${STAGE}/usr/bin/moku"
install -Dm644 packaging/dev.moku.app.desktop "${STAGE}/usr/share/applications/dev.moku.app.desktop"
install -Dm644 src-tauri/icons/32x32.png "${STAGE}/usr/share/icons/hicolor/32x32/apps/dev.moku.app.png"
install -Dm644 src-tauri/icons/128x128.png "${STAGE}/usr/share/icons/hicolor/128x128/apps/dev.moku.app.png"
install -Dm644 "src-tauri/icons/128x128@2x.png" "${STAGE}/usr/share/icons/hicolor/256x256/apps/dev.moku.app.png"
install -Dm644 packaging/dev.moku.app.metainfo.xml "${STAGE}/usr/share/metainfo/dev.moku.app.metainfo.xml"
tar -czf "$TARBALL" "$STAGE/"
AUR_SHA=$(sha256sum "$TARBALL" | awk '{print $1}')
rm -rf "$STAGE"
success "Tarball: ${TARBALL} sha256: ${AUR_SHA}"
section "Patching PKGBUILD"
PKGBUILD="${AUR_DIR}/PKGBUILD"
sed -i "s/^pkgver=.*/pkgver=${VERSION}/" "$PKGBUILD"
sed -i "s/^pkgrel=.*/pkgrel=1/" "$PKGBUILD"
sed -i "s/sha256sums=('[^']*')/sha256sums=('${AUR_SHA}')/" "$PKGBUILD"
success "PKGBUILD patched"
# Tarball is only needed for the GitHub upload — remind user then it can go
info "Tarball kept at ${REPO_ROOT}/${TARBALL} — upload it to GitHub, then it can be deleted"
section "Generating .SRCINFO (Arch container)"
# Mount only the AUR dir into a throwaway Arch container and run makepkg
podman run --rm \
--volume "${AUR_DIR}:/aur:z" \
--workdir /aur \
archlinux:latest \
bash -c "
pacman -Sy --noconfirm pacman >/dev/null 2>&1
source PKGBUILD
makepkg --printsrcinfo > .SRCINFO
"
success ".SRCINFO generated"
section "Next steps"
echo ""
echo -e " ${BOLD}1. Upload tarball to GitHub:${RESET}"
echo -e " ${CYAN}gh release create v${VERSION} '${REPO_ROOT}/${TARBALL}' --title 'v${VERSION}' --generate-notes${RESET}"
echo ""
echo -e " ${BOLD}2. Push AUR:${RESET}"
echo -e " ${CYAN}cd ${AUR_DIR}${RESET}"
echo -e " ${CYAN}git add PKGBUILD .SRCINFO${RESET}"
echo -e " ${CYAN}git commit -m 'Update to ${VERSION}'${RESET}"
echo -e " ${CYAN}git push origin master${RESET}"
echo ""
echo -e " ${BOLD}3. Clean up:${RESET}"
echo -e " ${CYAN}rm -f ${REPO_ROOT}/${TARBALL}${RESET}"
fi
echo ""
success "v${VERSION} ready"
+99
View File
@@ -0,0 +1,99 @@
#Requires -Version 7
param(
[switch]$SkipFrontend,
[switch]$SkipSuwayomi
)
Set-StrictMode -Version Latest
$ErrorActionPreference = "Stop"
function Step($msg) { Write-Host "`n==> $msg" -ForegroundColor Cyan }
function Need($cmd) {
if (-not (Get-Command $cmd -ErrorAction SilentlyContinue)) {
Write-Error "Required tool not found: $cmd"
}
}
$Root = Split-Path -Parent $MyInvocation.MyCommand.Path
Set-Location $Root
Step "Reading nix/versions.nix"
$nix = Get-Content "$Root\nix\versions.nix" -Raw
$MOKU_VERSION = if ($nix -match 'moku\s*=\s*"([^"]+)"') { $Matches[1] } else { Write-Error "moku version not found" }
$SUWA_VERSION = if ($nix -match 'version\s*=\s*"([^"]+)"') { $Matches[1] } else { Write-Error "suwayomi version not found" }
$SUWA_HASH = if ($nix -match 'windowsHash\s*=\s*"([^"]+)"') { $Matches[1] } else { Write-Error "windowsHash not found" }
Write-Host " moku=$MOKU_VERSION suwayomi=$SUWA_VERSION"
Need "pnpm"; Need "cargo"; Need "node"
if (-not $SkipFrontend) {
Step "pnpm install"
pnpm install --frozen-lockfile
if ($LASTEXITCODE -ne 0) { Write-Error "pnpm install failed" }
Step "Frontend build"
$env:MOKU_TARGET = "static"
pnpm build:static
if ($LASTEXITCODE -ne 0) { Write-Error "Frontend build failed" }
}
$BundleDir = "$Root\src-tauri\binaries\suwayomi-bundle"
$ZipPath = "$env:TEMP\suwayomi-windows-$SUWA_VERSION.zip"
$ExtractDir = "$env:TEMP\suwayomi-extracted-$SUWA_VERSION"
if (-not $SkipSuwayomi) {
Step "Downloading Suwayomi v$SUWA_VERSION"
$ZipUrl = "https://github.com/Suwayomi/Suwayomi-Server-preview/releases/download/v${SUWA_VERSION}/Suwayomi-Server-v${SUWA_VERSION}-windows-x64.zip"
if (-not (Test-Path $ZipPath)) {
Invoke-WebRequest -Uri $ZipUrl -OutFile $ZipPath -UseBasicParsing
}
$actual = (Get-FileHash $ZipPath -Algorithm SHA256).Hash.ToLower()
if ($actual -ne $SUWA_HASH.ToLower()) {
Write-Error "Hash mismatch`n expected: $SUWA_HASH`n got: $actual"
}
Step "Staging bundle"
if (Test-Path $ExtractDir) { Remove-Item $ExtractDir -Recurse -Force }
Expand-Archive -Path $ZipPath -DestinationPath $ExtractDir
$topDirs = @(Get-ChildItem $ExtractDir -Directory)
$topFiles = @(Get-ChildItem $ExtractDir -File)
$SrcDir = if ($topDirs.Count -eq 1 -and $topFiles.Count -eq 0) { $topDirs[0].FullName } else { $ExtractDir }
if (Test-Path $BundleDir) { Remove-Item $BundleDir -Recurse -Force }
Copy-Item $SrcDir $BundleDir -Recurse
$java = Get-ChildItem $BundleDir -Recurse -Filter "java.exe" | Where-Object { $_.FullName -match "jre.bin" } | Select-Object -First 1
$jar = Get-ChildItem $BundleDir -Recurse -Filter "Suwayomi-Server.jar" | Select-Object -First 1
if (-not $java) { Write-Error "java.exe not found in staged bundle" }
if (-not $jar) { Write-Error "Suwayomi-Server.jar not found in staged bundle" }
Write-Host " java: $($java.FullName)"
Write-Host " jar: $($jar.FullName)"
} elseif (-not (Test-Path $BundleDir)) {
Write-Error "Bundle dir missing at $BundleDir — run without -SkipSuwayomi first"
}
Step "Patching tauri.conf.json"
$tauriConf = "$Root\src-tauri\tauri.conf.json"
$original = Get-Content $tauriConf -Raw
Set-Content $tauriConf ($original -replace '"beforeBuildCommand":\s*"pnpm build"', '"beforeBuildCommand": ""') -NoNewline
Step "Tauri build"
$env:TAURI_SKIP_DEVSERVER_CHECK = "true"
pnpm tauri build --target x86_64-pc-windows-msvc --config src-tauri/tauri.windows.conf.json
$buildExit = $LASTEXITCODE
Set-Content $tauriConf $original -NoNewline
if ($buildExit -ne 0) { Write-Error "Tauri build failed (exit $buildExit)" }
Step "Artifacts"
$out = "$Root\src-tauri\target\x86_64-pc-windows-msvc\release\bundle"
$msi = Get-ChildItem "$out\msi" -Filter "*.msi" -ErrorAction SilentlyContinue | Select-Object -First 1
$exe = Get-ChildItem "$out\nsis" -Filter "*.exe" -ErrorAction SilentlyContinue | Select-Object -First 1
Write-Host "`nDone — Moku $MOKU_VERSION" -ForegroundColor Green
if ($msi) { Write-Host " MSI: $($msi.FullName)" -ForegroundColor Yellow }
if ($exe) { Write-Host " EXE: $($exe.FullName)" -ForegroundColor Yellow }
if (-not $msi -and -not $exe) { Write-Host " No artifacts found in $out" -ForegroundColor Red }
+52
View File
@@ -0,0 +1,52 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1280 320" width="1280" height="320">
<defs>
<linearGradient id="leafHero" x1="0.3" y1="0" x2="0.7" y2="1">
<stop offset="0%" stop-color="#52b888"/>
<stop offset="100%" stop-color="#1e5840"/>
</linearGradient>
<clipPath id="roundedBounds">
<rect width="1280" height="320" rx="18" ry="18"/>
</clipPath>
</defs>
<g clip-path="url(#roundedBounds)">
<rect width="1280" height="320" fill="#070e09"/>
<!-- Icon — rotate(7) from moku-icon-splash.svg -->
<g transform="translate(640, 148) rotate(7) scale(0.065,-0.065) translate(-5000,-4800)"
fill="url(#leafHero)" opacity="0.97">
<path d="M4908 6615 c-597 -629 -935 -1264 -934 -1755 0 -508 195 -778 790
-1098 l194 -104 22 -114 c33 -164 76 -271 140 -347 80 -94 106 -62 77 94 -47
255 43 443 294 613 797 539 714 1392 -250 2551 -230 278 -224 275 -333 160z
m52 -945 c0 -472 -8 -850 -17 -850 -36 0 -693 703 -692 740 2 48 167 321 309
509 175 233 359 451 380 451 13 0 20 -304 20 -850z m183 775 c120 -126 357
-447 356 -482 0 -18 -47 -83 -105 -144 -57 -61 -151 -163 -208 -225 -151 -166
-146 -180 -146 406 0 286 7 520 16 520 9 0 48 -34 87 -75z m541 -750 c86 -150
196 -391 196 -430 0 -8 -25 -42 -55 -75 -31 -33 -149 -163 -264 -290 -339
-374 -480 -520 -501 -520 -13 0 -20 167 -20 465 l1 465 151 170 c83 94 193
217 244 275 122 138 137 135 248 -60z m-1098 -633 l374 -378 0 -462 c0 -254
-5 -462 -11 -462 -6 0 -55 23 -110 50 l-99 51 0 208 c0 259 -11 288 -176 457
-196 200 -188 199 -326 62 l-117 -116 -35 79 c-71 161 -39 569 64 816 42 100
20 116 436 -305z m1349 23 c109 -390 -87 -848 -475 -1111 -207 -139 -305 -262
-338 -420 -7 -31 -21 -56 -32 -56 -36 -1 -46 86 -48 405 l-2 313 123 137 c68
75 214 236 325 357 454 493 421 466 447 375z m-1292 -785 c12 -29 15 -118 7
-215 l-14 -166 -73 44 c-174 104 -403 341 -403 416 0 15 47 70 105 123 l104
98 127 -125 c70 -69 136 -147 147 -175z"/>
</g>
<!-- Stack text pinned to bottom -->
<text
x="640" y="300"
text-anchor="middle"
font-family="'SF Mono', 'JetBrains Mono', 'Fira Code', monospace"
font-size="14"
letter-spacing="5"
fill="#a8c4a8"
opacity="0.32">TAURI v2 · SVELTE 5 · TYPESCRIPT</text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 140 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 287 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 163 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 MiB

Generated
+12 -100
View File
@@ -1,46 +1,15 @@
{
"nodes": {
"crane": {
"locked": {
"lastModified": 1771438068,
"narHash": "sha256-nGBbXvEZVe/egCPVPFcu89RFtd8Rf6J+4RFoVCFec0A=",
"owner": "ipetkov",
"repo": "crane",
"rev": "b5090e53e9d68c523a4bb9ad42b4737ee6747597",
"type": "github"
},
"original": {
"owner": "ipetkov",
"repo": "crane",
"type": "github"
}
},
"flake-compat": {
"flake": false,
"locked": {
"lastModified": 1733328505,
"narHash": "sha256-NeCCThCEP3eCl2l/+27kNNK7QrwZB1IJCrXfrbv5oqU=",
"owner": "edolstra",
"repo": "flake-compat",
"rev": "ff81ac966bb2cae68946d5ed5fc4994f96d0ffec",
"type": "github"
},
"original": {
"owner": "edolstra",
"repo": "flake-compat",
"type": "github"
}
},
"flake-parts": {
"inputs": {
"nixpkgs-lib": "nixpkgs-lib"
},
"locked": {
"lastModified": 1769996383,
"narHash": "sha256-AnYjnFWgS49RlqX7LrC4uA+sCCDBj0Ry/WOJ5XWAsa0=",
"lastModified": 1778716662,
"narHash": "sha256-m1Yf0wZ8j1OHjTc2UwHwyQRSnNeSgLJOd7q5Y45hzi4=",
"owner": "hercules-ci",
"repo": "flake-parts",
"rev": "57928607ea566b5db3ad13af0e57e921e6b12381",
"rev": "f7c1a2d347e4c52d5fb8d10cb4d94b5884e546fb",
"type": "github"
},
"original": {
@@ -49,53 +18,13 @@
"type": "github"
}
},
"flake-utils": {
"inputs": {
"systems": "systems"
},
"locked": {
"lastModified": 1731533236,
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"nix-appimage": {
"inputs": {
"flake-compat": "flake-compat",
"flake-utils": "flake-utils",
"nixpkgs": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1757920913,
"narHash": "sha256-jd0QwCVz4O1sHHkeaZILD/7D6oyalceEJ4EFnWCgm0k=",
"owner": "ralismark",
"repo": "nix-appimage",
"rev": "7946addbc0d97e358a6d7aefe5e82310f0fe6b18",
"type": "github"
},
"original": {
"owner": "ralismark",
"repo": "nix-appimage",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1771369470,
"narHash": "sha256-0NBlEBKkN3lufyvFegY4TYv5mCNHbi5OmBDrzihbBMQ=",
"lastModified": 1780243769,
"narHash": "sha256-x5UQuRsH3MqI0U9afaXSNqzTPSeZlRLvFAav2Ux1pNw=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "0182a361324364ae3f436a63005877674cf45efb",
"rev": "331800de5053fcebacf6813adb5db9c9dca22a0c",
"type": "github"
},
"original": {
@@ -107,11 +36,11 @@
},
"nixpkgs-lib": {
"locked": {
"lastModified": 1769909678,
"narHash": "sha256-cBEymOf4/o3FD5AZnzC3J9hLbiZ+QDT/KDuyHXVJOpM=",
"lastModified": 1777168982,
"narHash": "sha256-GOkGPcboWE9BmGCRMLX3worL4EMnsnG8MyKmXNeYuhQ=",
"owner": "nix-community",
"repo": "nixpkgs.lib",
"rev": "72716169fe93074c333e8d0173151350670b824c",
"rev": "f5901329dade4a6ea039af1433fb087bd9c1fe14",
"type": "github"
},
"original": {
@@ -122,9 +51,7 @@
},
"root": {
"inputs": {
"crane": "crane",
"flake-parts": "flake-parts",
"nix-appimage": "nix-appimage",
"nixpkgs": "nixpkgs",
"rust-overlay": "rust-overlay"
}
@@ -136,11 +63,11 @@
]
},
"locked": {
"lastModified": 1771556776,
"narHash": "sha256-zKprqMQDl3xVfhSSYvgru1IGXjFdxryWk+KqK0I20Xk=",
"lastModified": 1780543271,
"narHash": "sha256-oPJ7eJN1sM37v92Rp/eyQL7/rUm0BOvXEBAoq/zN0cM=",
"owner": "oxalica",
"repo": "rust-overlay",
"rev": "8b3f46b8a6d17ab46e533a5e3d5b1cc2ff228860",
"rev": "c30ca201c5093540cf792f6982f81ba1aa0f3514",
"type": "github"
},
"original": {
@@ -148,21 +75,6 @@
"repo": "rust-overlay",
"type": "github"
}
},
"systems": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
}
},
"root": "root",
+51 -99
View File
@@ -4,19 +4,14 @@
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
flake-parts.url = "github:hercules-ci/flake-parts";
crane.url = "github:ipetkov/crane";
rust-overlay = {
url = "github:oxalica/rust-overlay";
inputs.nixpkgs.follows = "nixpkgs";
};
nix-appimage = {
url = "github:ralismark/nix-appimage";
inputs.nixpkgs.follows = "nixpkgs";
};
};
outputs =
inputs@{ flake-parts, crane, rust-overlay, nix-appimage, ... }:
inputs@{ flake-parts, rust-overlay, ... }:
flake-parts.lib.mkFlake { inherit inputs; } {
systems = [
"x86_64-linux"
@@ -24,22 +19,23 @@
];
perSystem =
{ system, pkgs, lib, ... }:
{ system, lib, ... }:
let
pkgs' = import inputs.nixpkgs {
versions = import ./nix/versions.nix;
version = versions.moku;
pkgs = import inputs.nixpkgs {
inherit system;
overlays = [ rust-overlay.overlays.default ];
};
rustToolchain = pkgs'.rust-bin.stable.latest.default.override {
rustToolchain = pkgs.rust-bin.stable.latest.default.override {
extensions = [
"rust-src"
"rust-analyzer"
];
};
craneLib = (crane.mkLib pkgs').overrideToolchain rustToolchain;
runtimeLibs = with pkgs; [
webkitgtk_4_1
gtk3
@@ -55,94 +51,54 @@
gsettings-desktop-schemas
];
frontendSrc = lib.cleanSourceWith {
src = lib.cleanSourceWith {
src = ./.;
filter = path: type:
let base = builtins.baseNameOf path;
filter =
path: type:
let
base = builtins.baseNameOf path;
in
(lib.hasInfix "/src" path)
|| (lib.hasInfix "/src-tauri/src" path)
|| (lib.hasInfix "/src-tauri/icons" path)
|| (lib.hasInfix "/src-tauri/capabilities" path)
|| (lib.hasInfix "/static" path)
|| base == "index.html"
|| base == "package.json"
|| base == "pnpm-lock.yaml"
|| base == "pnpm-workspace.yaml"
|| base == "tsconfig.json"
|| base == "tsconfig.node.json"
|| base == "vite.config.ts"
|| base == "postcss.config.js"
|| base == "postcss.config.cjs"
|| base == "tailwind.config.js"
|| base == "tailwind.config.ts";
|| base == "svelte.config.js"
|| base == "Cargo.toml"
|| base == "Cargo.lock"
|| base == "build.rs"
|| base == "tauri.conf.json";
};
frontend = pkgs.stdenv.mkDerivation {
pname = "moku-frontend";
version = "0.1.0";
src = frontendSrc;
suwayomiServer = pkgs.callPackage ./nix/server.nix { inherit versions; };
nativeBuildInputs = with pkgs; [
nodejs_22
pnpm
pnpmConfigHook
];
pnpmDeps = pkgs.fetchPnpmDeps {
pname = "moku-frontend";
version = "0.1.0";
src = frontendSrc;
fetcherVersion = 1;
hash = "sha256-bpGYsB534RPNNAcYR9BA61vvFpSG6Xu2hY923PakCyY=";
};
buildPhase = "pnpm build";
installPhase = "cp -r dist $out";
moku = pkgs.callPackage ./nix/moku.nix {
inherit lib pkgs rustToolchain runtimeLibs suwayomiServer version src versions;
appIcon = ./src/lib/assets/moku-icon.svg;
};
cargoSrc = lib.cleanSourceWith {
src = ./src-tauri;
filter = path: type:
(craneLib.filterCargoSources path type)
|| (lib.hasInfix "/icons/" path)
|| (lib.hasInfix "/capabilities/" path)
|| (builtins.baseNameOf path == "tauri.conf.json");
};
commonArgs = {
src = cargoSrc;
cargoToml = ./src-tauri/Cargo.toml;
cargoLock = ./src-tauri/Cargo.lock;
strictDeps = true;
buildInputs = runtimeLibs;
nativeBuildInputs = with pkgs; [
pkg-config
wrapGAppsHook3
];
preBuild = ''
cp -r ${frontend} ../dist
'';
WEBKIT_DISABLE_COMPOSITING_MODE = "1";
};
cargoArtifacts = craneLib.buildDepsOnly commonArgs;
moku = craneLib.buildPackage (commonArgs // {
inherit cargoArtifacts;
meta.mainProgram = "moku";
postInstall = ''
wrapProgram $out/bin/moku \
--prefix XDG_DATA_DIRS : "${lib.makeSearchPath "share/gsettings-schemas" [
pkgs.gsettings-desktop-schemas
pkgs.gtk3
]}" \
--prefix LD_LIBRARY_PATH : "${lib.makeLibraryPath runtimeLibs}" \
--prefix PATH : "${lib.makeBinPath [ pkgs.suwayomi-server ]}"
'';
});
scripts = import ./nix/scripts.nix { inherit pkgs rustToolchain version versions; };
in
{
packages = {
inherit moku frontend;
inherit moku suwayomiServer;
default = moku;
appimage = nix-appimage.bundlers."${system}".default moku;
};
apps = {
default = { type = "app"; program = "${moku}/bin/moku"; };
moku = { type = "app"; program = "${moku}/bin/moku"; };
bump = { type = "app"; program = "${scripts.bump}/bin/moku-bump"; };
update = { type = "app"; program = "${scripts.update}/bin/moku-update"; };
flatpak = { type = "app"; program = "${scripts.flatpak}/bin/moku-flatpak"; };
tunnel = { type = "app"; program = "${scripts.tunnel}/bin/moku-tunnel"; };
};
devShells.default = pkgs.mkShell {
@@ -153,31 +109,27 @@
wrapGAppsHook3
nodejs_22
pnpm
suwayomi-server
suwayomiServer
cloudflared
xdg-utils
(python3.withPackages (ps: [
ps.aiohttp
ps.tomlkit
]))
];
shellHook = ''
export WEBKIT_DISABLE_COMPOSITING_MODE=1
export APPIMAGE_EXTRACT_AND_RUN=1
export NO_STRIP=true
export PKG_CONFIG_PATH="${pkgs.openssl.dev}/lib/pkgconfig''${PKG_CONFIG_PATH:+:$PKG_CONFIG_PATH}"
export XDG_DATA_DIRS="${pkgs.gsettings-desktop-schemas}/share/gsettings-schemas/${pkgs.gsettings-desktop-schemas.name}:${pkgs.gtk3}/share/gsettings-schemas/${pkgs.gtk3.name}''${XDG_DATA_DIRS:+:$XDG_DATA_DIRS}"
export LD_LIBRARY_PATH="${pkgs.lib.makeLibraryPath runtimeLibs}''${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}"
if [ ! -e /usr/bin/xdg-open ]; then
sudo ln -sf ${pkgs.xdg-utils}/bin/xdg-open /usr/bin/xdg-open
fi
LINUXDEPLOY="$HOME/.cache/tauri/linuxdeploy-x86_64.AppImage"
LINUXDEPLOY_REAL="$HOME/.cache/tauri/linuxdeploy-x86_64.AppImage.real"
if [ -f "$LINUXDEPLOY" ] && [ ! -f "$LINUXDEPLOY_REAL" ]; then
mv "$LINUXDEPLOY" "$LINUXDEPLOY_REAL"
printf '#!/bin/sh\nexec ${pkgs.appimage-run}/bin/appimage-run "%s" "$@"\n' "$LINUXDEPLOY_REAL" > "$LINUXDEPLOY"
chmod +x "$LINUXDEPLOY"
echo "linuxdeploy wrapped with appimage-run"
fi
echo "Moku dev shell"
echo " pnpm install && pnpm tauri:dev"
echo "Moku dev shell pnpm install && pnpm tauri:dev"
echo ""
echo " nix run .#bump -- <ver> bump version + rebuild frontend"
echo " git commit && git tag && git push"
echo " nix run .#update -- <ver> fetch hashes + patch all configs"
echo " nix run .#flatpak build flatpak bundle"
echo " nix run .#tunnel -- [port] expose local server via cloudflare"
'';
};
+2 -2
View File
@@ -6,7 +6,7 @@
<title>Moku</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>
@@ -1,4 +1,4 @@
app-id: dev.moku.app
app-id: io.github.moku_project.Moku
runtime: org.gnome.Platform
runtime-version: '48'
sdk: org.gnome.Sdk
@@ -9,16 +9,22 @@ separate-locales: false
finish-args:
- --socket=wayland
- --socket=x11
- --socket=fallback-x11
- --share=ipc
- --device=dri
- --share=network
- --socket=session-bus
- --socket=system-bus
- --filesystem=home
- --talk-name=org.freedesktop.Notifications
- --talk-name=org.freedesktop.portal.Desktop
- --talk-name=org.freedesktop.portal.FileTransfer
- --talk-name=org.kde.StatusNotifierWatcher
- --talk-name=com.canonical.AppMenu.Registrar
- --talk-name=com.canonical.indicator.application
- --filesystem=xdg-run/discord-ipc-0:ro
- --filesystem=xdg-data/moku:create
- --talk-name=org.freedesktop.Flatpak
- --filesystem=xdg-download
build-options:
append-path: /usr/lib/sdk/rust-stable/bin
@@ -26,6 +32,83 @@ build-options:
CARGO_HOME: /run/build/moku/cargo
modules:
- name: intltool
buildsystem: autotools
sources:
- type: archive
url: https://launchpad.net/intltool/trunk/0.51.0/+download/intltool-0.51.0.tar.gz
sha256: 67c74d94196b153b774ab9f89b2fa6c6ba79352407037c8c14d5aeb334e959cd
- name: libdbusmenu
buildsystem: autotools
build-options:
cflags: -Wno-error
env:
HAVE_VALGRIND_FALSE: '#'
HAVE_VALGRIND_TRUE: ''
config-opts:
- --with-gtk=3
- --disable-static
- --disable-dumper
- --disable-tests
- --disable-gtk-doc
- --disable-vala
- --disable-introspection
cleanup:
- /include
- /libexec
- /lib/pkgconfig
- /lib/*.la
- /share/doc
- /share/libdbusmenu
- /share/gtk-doc
- /share/gir-1.0
sources:
- type: archive
url: https://launchpad.net/libdbusmenu/16.04/16.04.0/+download/libdbusmenu-16.04.0.tar.gz
sha256: b9cc4a2acd74509435892823607d966d424bd9ad5d0b00938f27240a1bfa878a
- name: libayatana-ido
buildsystem: cmake-ninja
config-opts:
- -DENABLE_TESTS=OFF
- -DGSETTINGS_COMPILE=OFF
sources:
- type: git
url: https://github.com/AyatanaIndicators/ayatana-ido.git
tag: 0.10.3
- name: libayatana-indicator
buildsystem: cmake-ninja
build-options:
env:
PKG_CONFIG_PATH: /app/lib/pkgconfig:/app/share/pkgconfig
config-opts:
- -DENABLE_TESTS=OFF
- -DGSETTINGS_COMPILE=OFF
sources:
- type: git
url: https://github.com/AyatanaIndicators/libayatana-indicator.git
tag: 0.9.4
- name: libayatana-appindicator
buildsystem: cmake-ninja
build-options:
env:
PKG_CONFIG_PATH: /app/lib/pkgconfig:/app/share/pkgconfig
config-opts:
- -DENABLE_TESTS=OFF
- -DENABLE_BINDINGS_MONO=OFF
- -DENABLE_BINDINGS_VALA=OFF
- -DGSETTINGS_COMPILE=OFF
sources:
- type: git
url: https://github.com/AyatanaIndicators/libayatana-appindicator.git
tag: 0.5.93
- type: shell
commands:
- sed -i '/add_subdirectory(docs)/d' CMakeLists.txt
- name: openjdk
buildsystem: simple
build-commands:
@@ -33,13 +116,10 @@ modules:
- tar -xf jdk.tar.gz -C /app/jre --strip-components=1
sources:
- type: file
url: https://github.com/adoptium/temurin21-binaries/releases/download/jdk-21.0.3%2B9/OpenJDK21U-jre_x64_linux_hotspot_21.0.3_9.tar.gz
sha256: f1af100c4afca2035f446967323230150cfe5872b5a664d98c86963e5c066e0d
url: https://github.com/adoptium/temurin21-binaries/releases/download/jdk-21.0.5%2B11/OpenJDK21U-jre_x64_linux_hotspot_21.0.5_11.tar.gz
sha256: 553dda64b3b1c3c16f8afe402377ffebe64fb4a1721a46ed426a91fd18185e62
dest-filename: jdk.tar.gz
# catch_abort.so — intercepts SIGTRAP/SIGILL from KCEF's CEF subprocess and
# exits just that thread instead of killing the whole JVM. Official Suwayomi
# fix for headless environments. Source inlined to avoid upstream drift.
- name: catch-abort
buildsystem: simple
build-commands:
@@ -49,9 +129,6 @@ modules:
- type: inline
dest-filename: catch_abort.c
contents: |
// Linux only:
// Attempts to catch SIGTRAP and exit the thread instead of bringing down the whole process
#define _GNU_SOURCE
#include <stdio.h>
#include <dlfcn.h>
@@ -92,12 +169,12 @@ modules:
cat > /app/tachidesk/default-conf/server.conf << 'EOF'
server.ip = "127.0.0.1"
server.port = 4567
server.webUIEnabled = false
server.webUIEnabled = true
server.initialOpenInBrowserEnabled = false
server.systemTrayEnabled = false
server.webUIInterface = "browser"
server.webUIFlavor = "WebUI"
server.webUIChannel = "stable"
server.webUIChannel = "PREVIEW"
server.electronPath = ""
server.debugLogsEnabled = false
server.downloadAsCbz = true
@@ -111,24 +188,20 @@ modules:
cat > /app/bin/tachidesk-server << 'EOF'
#!/bin/sh
DATA_DIR="${XDG_DATA_HOME:-$HOME/.local/share}/moku/tachidesk"
DATA_DIR="${XDG_DATA_HOME:-$HOME/.local/share}/Tachidesk"
mkdir -p "$DATA_DIR"
# Seed conf on first run
if [ ! -f "$DATA_DIR/server.conf" ]; then
cp /app/tachidesk/default-conf/server.conf "$DATA_DIR/server.conf"
fi
# Force-patch the three keys that cause JCEF/GUI crashes every launch.
# Suwayomi ignores -D JVM flags when a conf file exists on disk.
sed -i \
-e 's|server\.webUIEnabled.*|server.webUIEnabled = false|' \
-e 's|server\.initialOpenInBrowserEnabled.*|server.initialOpenInBrowserEnabled = false|' \
-e 's|server\.systemTrayEnabled.*|server.systemTrayEnabled = false|' \
"$DATA_DIR/server.conf"
# Append keys if absent
grep -q 'server\.webUIEnabled' "$DATA_DIR/server.conf" || echo 'server.webUIEnabled = false' >> "$DATA_DIR/server.conf"
grep -q 'server\.webUIEnabled' "$DATA_DIR/server.conf" || echo 'server.webUIEnabled = true' >> "$DATA_DIR/server.conf"
grep -q 'server\.initialOpenInBrowserEnabled' "$DATA_DIR/server.conf" || echo 'server.initialOpenInBrowserEnabled = false' >> "$DATA_DIR/server.conf"
grep -q 'server\.systemTrayEnabled' "$DATA_DIR/server.conf" || echo 'server.systemTrayEnabled = false' >> "$DATA_DIR/server.conf"
@@ -138,8 +211,6 @@ modules:
export _JAVA_OPTIONS="-Djava.awt.headless=true"
export JAVA_TOOL_OPTIONS="-Djava.awt.headless=true"
# Intercepts SIGTRAP/SIGILL from KCEF's CEF subprocess, exits just
# that thread instead of crashing the whole JVM process.
export LD_PRELOAD="/app/lib/catch_abort.so"
exec /app/jre/bin/java \
@@ -155,8 +226,8 @@ modules:
sources:
- type: file
url: https://github.com/Suwayomi/Suwayomi-Server/releases/download/v2.1.1867/suwayomi-server-v2.1.1867.jar
sha256: 51e307c2581e4e1a002991ab3e3a77503c8b074c42695987a984a7382d0ac5af
url: https://github.com/Suwayomi/Suwayomi-Server-preview/releases/download/v2.2.2196/Suwayomi-Server-v2.2.2196.jar
sha256: 8e7244c269456661a87705f746f0d87275770aa976bab7c6920e4d513e97c3f6
dest-filename: Suwayomi-Server.jar
- name: moku
@@ -166,22 +237,24 @@ modules:
CARGO_HOME: /run/build/moku/cargo
XDG_DATA_HOME: /run/build/moku/xdg-data
TAURI_SKIP_DEVSERVER_CHECK: 'true'
PKG_CONFIG_PATH: /usr/lib/pkgconfig:/usr/share/pkgconfig
PKG_CONFIG_PATH: /usr/lib/pkgconfig:/usr/share/pkgconfig:/app/lib/pkgconfig
TAURI_CONFIG: '{"build":{"devUrl":null,"frontendDist":"../dist"},"app":{"windows":[{"devtools":false}]},"bundle":{"externalBin":[]}}'
build-commands:
- tar -xzf frontend-dist.tar.gz
- . /usr/lib/sdk/rust-stable/enable.sh && PKG_CONFIG_PATH=/usr/lib/pkgconfig:/usr/share/pkgconfig cargo build --release --manifest-path src-tauri/Cargo.toml
- . /usr/lib/sdk/rust-stable/enable.sh && PKG_CONFIG_PATH=/usr/lib/pkgconfig:/usr/share/pkgconfig:/app/lib/pkgconfig cargo build --release --manifest-path src-tauri/Cargo.toml
- install -Dm755 src-tauri/target/release/moku /app/bin/moku
- install -Dm644 packaging/dev.moku.app.desktop /app/share/applications/dev.moku.app.desktop
- install -Dm644 src-tauri/icons/32x32.png /app/share/icons/hicolor/32x32/apps/dev.moku.app.png
- install -Dm644 src-tauri/icons/128x128.png /app/share/icons/hicolor/128x128/apps/dev.moku.app.png
- install -Dm644 src-tauri/icons/128x128@2x.png /app/share/icons/hicolor/256x256/apps/dev.moku.app.png
- install -Dm644 packaging/dev.moku.app.metainfo.xml /app/share/metainfo/dev.moku.app.metainfo.xml
- install -Dm644 packaging/io.github.moku_project.Moku.desktop /app/share/applications/io.github.moku_project.Moku.desktop
- install -Dm644 src-tauri/icons/32x32.png /app/share/icons/hicolor/32x32/apps/io.github.moku_project.Moku.png
- install -Dm644 src-tauri/icons/128x128.png /app/share/icons/hicolor/128x128/apps/io.github.moku_project.Moku.png
- install -Dm644 src-tauri/icons/128x128@2x.png /app/share/icons/hicolor/256x256/apps/io.github.moku_project.Moku.png
- install -Dm644 packaging/io.github.moku_project.Moku.metainfo.xml /app/share/metainfo/io.github.moku_project.Moku.metainfo.xml
sources:
- type: dir
path: .
- type: git
url: https://github.com/moku-project/Moku.git
commit: baece20f467d2c7d4cebaa9ea8892980aa93aa10
- type: file
path: packaging/frontend-dist.tar.gz
sha256: ac23bf503533711b19b7fd4b3ec04e081928f2f41b66d8391af1a9e36681548a
sha256: 676ec2273ffd9a69248849c5d51dc4d59a5d5b68fbba7a4fe7e7b572a5f25f14
- packaging/cargo-sources.json
- type: inline
dest: src-tauri/.cargo
+99
View File
@@ -0,0 +1,99 @@
{
lib,
pkgs,
rustToolchain,
runtimeLibs,
suwayomiServer,
version,
versions,
src,
appIcon,
}:
pkgs.stdenv.mkDerivation {
pname = "moku";
inherit version src;
nativeBuildInputs = with pkgs; [
rustToolchain
nodejs_22
pnpm
pnpmConfigHook
pkg-config
wrapGAppsHook3
rustPlatform.cargoSetupHook
];
buildInputs = runtimeLibs;
pnpmDeps = pkgs.fetchPnpmDeps {
pname = "moku";
inherit version src;
fetcherVersion = 3;
hash = versions.frontend.pnpmHash;
};
cargoDeps = pkgs.rustPlatform.importCargoLock {
lockFile = ../src-tauri/Cargo.lock;
outputHashes = {
"tauri-plugin-discord-rpc-0.1.0" = versions.gitDeps.tauri-plugin-discord-rpc;
};
};
env = {
PKG_CONFIG_PATH = "${pkgs.openssl.dev}/lib/pkgconfig";
TAURI_SKIP_DEVSERVER_CHECK = "true";
cargoRoot = "src-tauri";
};
buildPhase = ''
export HOME=$(mktemp -d)
pnpm tauri:build
'';
installPhase = ''
install -Dm755 src-tauri/target/release/moku $out/bin/moku
mkdir -p "$out/share/applications"
cat > "$out/share/applications/moku.desktop" << EOF2
[Desktop Entry]
Version=1.0
Type=Application
Name=Moku
Comment=Manga reader frontend for Suwayomi
Exec=$out/bin/moku
Icon=moku
Terminal=false
Categories=Graphics;Viewer;
Keywords=manga;comic;reader;suwayomi;
StartupWMClass=moku
EOF2
for size in 32x32 128x128 256x256 512x512; do
f="src-tauri/icons/$size.png"
[ -f "$f" ] && install -Dm644 "$f" \
"$out/share/icons/hicolor/$size/apps/moku.png"
done
for size in 128x128 256x256; do
f="src-tauri/icons/''${size}@2x.png"
[ -f "$f" ] && install -Dm644 "$f" \
"$out/share/icons/hicolor/''${size}@2/apps/moku.png"
done
install -Dm644 "${appIcon}" \
"$out/share/icons/hicolor/scalable/apps/moku.svg"
wrapProgram $out/bin/moku \
--prefix XDG_DATA_DIRS : "${lib.makeSearchPath "share/gsettings-schemas" [
pkgs.gsettings-desktop-schemas
pkgs.gtk3
]}" \
--prefix LD_LIBRARY_PATH : "${lib.makeLibraryPath runtimeLibs}" \
--prefix PATH : "${lib.makeBinPath [ suwayomiServer ]}" \
--set GDK_BACKEND wayland \
--set WEBKIT_DISABLE_SANDBOX_THIS_IS_DANGEROUS 1
'';
meta.mainProgram = "moku";
}
+157
View File
@@ -0,0 +1,157 @@
{ pkgs, rustToolchain, version, versions }:
{
bump = pkgs.writeShellApplication {
name = "moku-bump";
runtimeInputs = with pkgs; [
gnused
coreutils
git
xxd
rustToolchain
nodejs_22
pnpm
(python3.withPackages (ps: [ ps.aiohttp ps.tomlkit ]))
];
text = ''
[[ $# -lt 1 ]] && { echo "Usage: nix run .#bump -- <version>"; exit 1; }
VERSION="$1"
REPO="$(git rev-parse --show-toplevel)"
sed -i "s/moku = \"[^\"]*\"/moku = \"$VERSION\"/" "$REPO/nix/versions.nix"
sed -i "s/\"version\": \"[^\"]*\"/\"version\": \"$VERSION\"/" "$REPO/src-tauri/tauri.conf.json"
sed -i "0,/^version = \"[^\"]*\"/s//version = \"$VERSION\"/" "$REPO/src-tauri/Cargo.toml"
sed -i "s/^pkgver=.*/pkgver=$VERSION/" "$REPO/PKGBUILD"
sed -i "s/^pkgrel=.*/pkgrel=1/" "$REPO/PKGBUILD"
(cd "$REPO/src-tauri" && cargo generate-lockfile)
cd "$REPO"
pnpm install --frozen-lockfile
pnpm build:static
tar -czf "$REPO/packaging/frontend-dist.tar.gz" -C "$REPO" dist
FRONTEND_SHA_HEX=$(sha256sum "$REPO/packaging/frontend-dist.tar.gz" | awk '{print $1}')
FRONTEND_SHA_SRI=$(echo "$FRONTEND_SHA_HEX" | xxd -r -p | base64 -w0 | sed 's/^/sha256-/')
sed -i "s|distHash = \"[^\"]*\"|distHash = \"$FRONTEND_SHA_HEX\"|" "$REPO/nix/versions.nix"
sed -i "s|distHashSri = \"[^\"]*\"|distHashSri = \"$FRONTEND_SHA_SRI\"|" "$REPO/nix/versions.nix"
MANIFEST="$REPO/io.github.moku_project.Moku.yml"
sed -i "s/tag: v[^[:space:]]*/tag: v$VERSION/" "$MANIFEST"
python3 - "$MANIFEST" "$FRONTEND_SHA_HEX" <<'PYEOF'
import re, sys
path, sha = sys.argv[1], sys.argv[2]
text = open(path).read()
updated, n = re.subn(
r'(path:\s*packaging/frontend-dist\.tar\.gz\s*\n\s*sha256:\s*)[0-9a-f]+',
r'\g<1>' + sha, text)
if n == 0:
sys.exit("ERROR: could not find frontend-dist sha256 in manifest")
open(path, 'w').write(updated)
PYEOF
python3 "$REPO/packaging/flatpak-cargo-generator.py" \
"$REPO/src-tauri/Cargo.lock" \
-o "$REPO/packaging/cargo-sources.json"
echo "Bumped to v$VERSION commit, tag, push, then: nix run .#update -- $VERSION"
'';
};
update = pkgs.writeShellApplication {
name = "moku-update";
runtimeInputs = with pkgs; [ gnused coreutils git curl nix xxd ];
text = ''
REPO="$(git rev-parse --show-toplevel)"
VERSIONS="$REPO/nix/versions.nix"
MANIFEST="$REPO/io.github.moku_project.Moku.yml"
PKGBUILD="$REPO/PKGBUILD"
if [[ $# -ge 1 ]]; then
VERSION="$1"
else
VERSION=$(grep 'moku = "' "$VERSIONS" | head -1 | sed 's/.*"\(.*\)".*/\1/')
fi
COMMIT=$(git ls-remote https://github.com/moku-project/Moku.git "refs/tags/v$VERSION" | awk '{print $1}')
[[ -z "$COMMIT" ]] && { echo "ERROR: tag v$VERSION not found on remote"; exit 1; }
sed -i "s/gitCommit = \"[^\"]*\"/gitCommit = \"$COMMIT\"/" "$VERSIONS"
sed -i "s/commit: [0-9a-f]\{40\}/commit: $COMMIT/" "$MANIFEST"
TARBALL_URL="https://github.com/moku-project/Moku/archive/refs/tags/v$VERSION.tar.gz"
TARBALL_SHA=$(curl -fsSL "$TARBALL_URL" | sha256sum | awk '{print $1}')
sed -i "s/tarballHash = \"[^\"]*\"/tarballHash = \"$TARBALL_SHA\"/" "$VERSIONS"
sed -i "/sha256sums=/,/)/{ 0,/'/s/'[^']*'/'$TARBALL_SHA'/ }" "$PKGBUILD"
if [[ $# -ge 2 ]]; then
SUWA_VER="$2"
BASE="https://github.com/Suwayomi/Suwayomi-Server-preview/releases/download/v''${SUWA_VER}"
echo "Fetching Suwayomi v''${SUWA_VER} hashes (5 downloads)..."
sha_of() { curl -fsSL "$1" | sha256sum | awk '{print $1}'; }
to_sri() { echo "$1" | xxd -r -p | base64 -w0 | sed 's/^/sha256-/'; }
JAR_SHA=$(sha_of "''${BASE}/Suwayomi-Server-v''${SUWA_VER}.jar")
WIN_SHA=$(sha_of "''${BASE}/Suwayomi-Server-v''${SUWA_VER}-windows-x64.zip")
LINUX_SHA=$(sha_of "''${BASE}/Suwayomi-Server-v''${SUWA_VER}-linux-x64.tar.gz")
ARM64_SHA=$(sha_of "''${BASE}/Suwayomi-Server-v''${SUWA_VER}-macOS-arm64.tar.gz")
X64_SHA=$(sha_of "''${BASE}/Suwayomi-Server-v''${SUWA_VER}-macOS-x64.tar.gz")
JAR_SRI=$(to_sri "$JAR_SHA")
sed -i "s/version = \"[^\"]*\"/version = \"''${SUWA_VER}\"/" "$VERSIONS"
sed -i "s|hash = \"sha256-[^\"]*\"|hash = \"''${JAR_SRI}\"|" "$VERSIONS"
sed -i "s|windowsHash = \"[^\"]*\"|windowsHash = \"''${WIN_SHA}\"|" "$VERSIONS"
sed -i "s|linuxHash = \"[^\"]*\"|linuxHash = \"''${LINUX_SHA}\"|" "$VERSIONS"
sed -i "s|macosArm64Hash = \"[^\"]*\"|macosArm64Hash = \"''${ARM64_SHA}\"|" "$VERSIONS"
sed -i "s|macosX64Hash = \"[^\"]*\"|macosX64Hash = \"''${X64_SHA}\"|" "$VERSIONS"
sed -i "s|Suwayomi-Server-preview/releases/download/v[^/]*/|Suwayomi-Server-preview/releases/download/v''${SUWA_VER}/|" "$MANIFEST"
sed -i "s|Suwayomi-Server-v[0-9.]*\.jar|Suwayomi-Server-v''${SUWA_VER}.jar|g" "$MANIFEST"
python3 - "$MANIFEST" "$JAR_SHA" <<'PYEOF'
import re, sys
path, sha = sys.argv[1], sys.argv[2]
text = open(path).read()
updated, n = re.subn(
r'(dest-filename:\s*Suwayomi-Server\.jar\s*\n\s*sha256:\s*)[0-9a-f]+',
r'\g<1>' + sha, text)
if n == 0:
sys.exit("ERROR: could not find Suwayomi jar sha256 in manifest")
open(path, 'w').write(updated)
PYEOF
echo "Suwayomi hashes written."
fi
echo "Done versions.nix, flatpak manifest, and PKGBUILD patched for v$VERSION"
'';
};
flatpak = pkgs.writeShellApplication {
name = "moku-flatpak";
runtimeInputs = with pkgs; [ coreutils git appstream flatpak-builder flatpak ];
text = ''
REPO="$(git rev-parse --show-toplevel)"
rm -rf "$REPO/build-dir" "$REPO/repo"
flatpak-builder \
--repo="$REPO/repo" \
--force-clean \
"$REPO/build-dir" \
"$REPO/io.github.moku_project.Moku.yml"
flatpak build-bundle "$REPO/repo" "$REPO/moku.flatpak" io.github.moku_project.Moku
rm -rf "$REPO/build-dir" "$REPO/repo"
echo "moku.flatpak created"
'';
};
tunnel = pkgs.writeShellApplication {
name = "moku-tunnel";
runtimeInputs = with pkgs; [ cloudflared ];
text = ''
PORT="''${1:-4567}"
cloudflared tunnel --url "http://localhost:$PORT"
'';
};
}
+48
View File
@@ -0,0 +1,48 @@
{
lib,
stdenvNoCC,
fetchurl,
makeWrapper,
jdk21_headless,
versions,
}:
let
jdk = jdk21_headless;
ver = versions.suwayomi;
in
stdenvNoCC.mkDerivation {
pname = "suwayomi-server";
version = ver.version;
src = fetchurl {
url = "https://github.com/Suwayomi/Suwayomi-Server-preview/releases/download/v${ver.version}/Suwayomi-Server-v${ver.version}.jar";
hash = ver.hash;
};
nativeBuildInputs = [ makeWrapper ];
dontUnpack = true;
buildPhase = ''
runHook preBuild
install -Dm644 $src $out/share/suwayomi-server/suwayomi-server.jar
makeWrapper ${jdk}/bin/java $out/bin/suwayomi-server \
--add-flags "-Dsuwayomi.tachidesk.config.server.initialOpenInBrowserEnabled=false" \
--add-flags "-jar $out/share/suwayomi-server/suwayomi-server.jar"
runHook postBuild
'';
meta = {
description = "Free and open source manga reader server that runs extensions built for Mihon (Tachiyomi)";
homepage = "https://github.com/Suwayomi/Suwayomi-Server";
downloadPage = "https://github.com/Suwayomi/Suwayomi-Server-preview/releases";
changelog = "https://github.com/Suwayomi/Suwayomi-Server-preview/releases/tag/v${ver.version}";
license = lib.licenses.mpl20;
platforms = jdk.meta.platforms;
sourceProvenance = [ lib.sourceTypes.binaryBytecode ];
mainProgram = "suwayomi-server";
};
}
+25
View File
@@ -0,0 +1,25 @@
{
moku = "0.10.0";
suwayomi = {
version = "2.2.2196";
hash = "sha256-jnJEwmlFZmGodwX3RvDYcnV3Cql2urfGkg5NUT6Xw/Y=";
windowsHash = "457ca4a64a57e0d274a87203d25e962103bcb456ee30ada3ea47328a3093329d";
linuxHash = "e13d63ceb7e2b15e83d0a78281e8c1c04ac4a833caa73e5a2b68fbaf0cb20c1f";
macosArm64Hash = "9e3dbebc7475707e8d11c56a473385c00b09bde0103d013bc1cb3d06c89e5c43";
macosX64Hash = "eadee02060b780a5febfb8dada2f89c7bd7db5905cfd20d47eaca02fcde8c9c5";
};
frontend = {
pnpmHash = "sha256-fBkNpQXEeGZNbrpx7+0xVYYtQ6dGvpgRflCGPoxvnVY=";
distHash = "7db288b4b54277aa82b6ec5b21fc31a1e71f8246c50a74777500083b806c1fa5";
distHashSri = "sha256-Z27CJz/9mmkkiEnF1R3E1ZpdW2j7unpP5+e1cqXyXxQ=";
};
gitDeps = {
tauri-plugin-discord-rpc = "sha256-xq0qyK2NrwSAFDhXo0vbvcygRD2/7uqBaLpqfpfxkrc=";
};
gitCommit = "239960683b6c7f1347e1798b0e179a8a46628728";
tarballHash = "589b389b356a48d54ad4022e68eaac165a1de654d0b98edec79ebbab2c4a1275";
}
+37 -29
View File
@@ -1,41 +1,49 @@
{
"name": "moku",
"private": true,
"version": "0.1.0",
"version": "0.9.4",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"dev": "vite dev",
"preview": "vite preview",
"tauri": "tauri",
"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:android": "MOKU_TARGET=static vite build",
"tauri:dev": "tauri dev --config src-tauri/tauri.dev.conf.json",
"tauri:build": "tauri build"
},
"dependencies": {
"@phosphor-icons/react": "^2.1.10",
"@radix-ui/react-dropdown-menu": "^2.1.16",
"@radix-ui/react-progress": "^1.1.8",
"@radix-ui/react-scroll-area": "^1.2.10",
"@radix-ui/react-tooltip": "^1.2.8",
"@tanstack/react-virtual": "^3.13.18",
"@tauri-apps/api": "^2.0.0",
"@tauri-apps/plugin-shell": "~2",
"clsx": "^2.1.1",
"lucide-react": "^0.575.0",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-router-dom": "^6.26.0",
"zustand": "^5.0.0"
},
"devDependencies": {
"@tauri-apps/cli": "^2.0.0",
"@types/react": "^18.3.3",
"@types/react-dom": "^18.3.0",
"@vitejs/plugin-react": "^4.3.1",
"autoprefixer": "^10.4.20",
"postcss": "^8.4.40",
"tailwindcss": "^3.4.7",
"typescript": "^5.5.3",
"vite": "^5.4.0"
"@sveltejs/adapter-node": "^5.5.4",
"@sveltejs/adapter-static": "^3.0.10",
"@sveltejs/kit": "^2.62.0",
"@sveltejs/vite-plugin-svelte": "^7.1.2",
"@tauri-apps/cli": "^2.11.2",
"@types/node": "^25.9.3",
"phosphor-svelte": "^3.1.0",
"svelte": "^5.56.1",
"svelte-check": "^4.5.0",
"typescript": "^6.0.3",
"vite": "^8.0.16"
},
"dependencies": {
"@capacitor/app": "^8.1.0",
"@capacitor/browser": "^8.0.3",
"@capacitor/core": "^8.4.0",
"@capacitor/filesystem": "^8.1.2",
"@capacitor/preferences": "^8.0.1",
"@tauri-apps/api": "^2.11.0",
"@tauri-apps/plugin-dialog": "^2.7.1",
"@tauri-apps/plugin-fs": "^2.5.1",
"@tauri-apps/plugin-http": "^2.5.9",
"@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.3",
"capacitor-native-biometric": "^4.2.2",
"clsx": "^2.1.1",
"tauri-plugin-discord-rpc-api": "github:Youwes09/tauri-plugin-discord-rpc"
}
}
+1996 -1010
View File
File diff suppressed because it is too large Load Diff
-36
View File
@@ -1,36 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<component type="desktop-application">
<id>dev.moku.app</id>
<metadata_license>MIT</metadata_license>
<project_license>MIT</project_license>
<name>Moku</name>
<summary>Manga reader powered by Suwayomi</summary>
<description>
<p>
Moku is a desktop manga reader built on top of Suwayomi-Server (Tachidesk),
providing a clean native interface for browsing, reading, and managing your
manga library across hundreds of sources.
</p>
</description>
<launchable type="desktop-id">dev.moku.app.desktop</launchable>
<url type="homepage">https://github.com/shozikan/Moku</url>
<url type="bugtracker">https://github.com/shozikan/Moku/issues</url>
<provides>
<binary>moku</binary>
</provides>
<content_rating type="oars-1.1" />
<releases>
<release version="0.1.0" date="2025-01-01">
<description>
<p>Initial release.</p>
</description>
</release>
</releases>
</component>
Binary file not shown.
@@ -2,7 +2,7 @@
Name=Moku
Comment=Manga reader powered by Suwayomi
Exec=moku
Icon=dev.moku.app
Icon=io.github.moku_project.Moku
Terminal=false
Type=Application
Categories=Graphics;Viewer;
@@ -0,0 +1,73 @@
<?xml version="1.0" encoding="UTF-8"?>
<component type="desktop-application">
<id>io.github.moku_project.Moku</id>
<metadata_license>MIT</metadata_license>
<project_license>MIT</project_license>
<name>Moku</name>
<summary>Manga reader powered by Suwayomi</summary>
<description>
<p>
Moku is a desktop manga reader built on top of Suwayomi-Server (Tachidesk),
providing a clean native interface for browsing, reading, and managing your
manga library across hundreds of sources.
</p>
<p>
Features include library management, chapter tracking, extension support,
reading history, notifications, and Discord Rich Presence integration.
</p>
</description>
<launchable type="desktop-id">io.github.moku_project.Moku.desktop</launchable>
<url type="homepage">https://github.com/moku-project/Moku</url>
<url type="bugtracker">https://github.com/moku-project/Moku/issues</url>
<screenshots>
<screenshot type="default">
<image>https://raw.githubusercontent.com/moku-project/Moku/main/docs/screenshots/Moku-Home.png</image>
<caption>Home screen showing your manga library</caption>
</screenshot>
<screenshot>
<image>https://raw.githubusercontent.com/moku-project/Moku/main/docs/screenshots/Moku-Reader.png</image>
<caption>Built-in manga reader</caption>
</screenshot>
<screenshot>
<image>https://raw.githubusercontent.com/moku-project/Moku/main/docs/screenshots/Moku-Discover.png</image>
<caption>Discover new manga across hundreds of sources</caption>
</screenshot>
<screenshot>
<image>https://raw.githubusercontent.com/moku-project/Moku/main/docs/screenshots/Moku-Downloads.png</image>
<caption>Download manager</caption>
</screenshot>
<screenshot>
<image>https://raw.githubusercontent.com/moku-project/Moku/main/docs/screenshots/Moku-Settings.png</image>
<caption>Settings</caption>
</screenshot>
</screenshots>
<provides>
<binary>moku</binary>
</provides>
<content_rating type="oars-1.1" />
<releases>
<release version="0.9.0" date="2025-04-01">
<description>
<p>Latest release with improved stability and UI refinements.</p>
</description>
</release>
<release version="0.8.0" date="2025-04-01">
<description>
<p>Old release with improved stability and UI refinements.</p>
</description>
</release>
<release version="0.4.0" date="2025-03-22">
<description>
<p>Svelte rewrite with improved UI, bundled server, and cross-platform fixes.</p>
</description>
</release>
</releases>
</component>
+1145 -2234
View File
File diff suppressed because it is too large Load Diff
+2
View File
@@ -0,0 +1,2 @@
onlyBuiltDependencies:
- esbuild
+1617 -700
View File
File diff suppressed because it is too large Load Diff
+28 -13
View File
@@ -1,11 +1,11 @@
[package]
name = "moku"
version = "0.2.0"
version = "0.10.0"
edition = "2021"
[lib]
name = "moku_lib"
crate-type = ["staticlib", "cdylib", "rlib"]
crate-type = ["cdylib", "rlib"]
[[bin]]
name = "moku"
@@ -15,17 +15,32 @@ path = "src/main.rs"
tauri-build = { version = "2.0", features = [] }
[dependencies]
tauri = { version = "2.0", features = [] }
tauri-plugin-shell = "2"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
walkdir = "2"
nix = { version = "0.29", features = ["fs"] }
dirs = "5"
tauri = { version = "2.0", features = ["tray-icon"] }
tauri-plugin-shell = "2"
tauri-plugin-process = "2"
tauri-plugin-http = "2"
tauri-plugin-dialog = "2"
tauri-plugin-os = "2.3.2"
tauri-plugin-store = "2"
tauri-plugin-discord-rpc = { git = "https://github.com/Youwes09/tauri-plugin-discord-rpc" }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
walkdir = "2"
sysinfo = "0.32"
dirs = "5"
urlencoding = "2"
tokio = { version = "1", features = ["rt-multi-thread"] }
reqwest = { version = "0.12", features = ["blocking"] }
[target.'cfg(target_os = "windows")'.dependencies]
windows = { version = "0.58", features = [
"Security_Credentials_UI",
"Win32_UI_WindowsAndMessaging",
] }
[profile.release]
codegen-units = 1
lto = true
opt-level = "s"
panic = "abort"
strip = true
lto = true
opt-level = "s"
panic = "abort"
strip = true
@@ -0,0 +1,112 @@
#!/bin/sh
# — Suwayomi launcher for Linux AppImage/deb.
# Tauri resolves this via resolve_server_binary() in lib.rs, which looks for
# "suwayomi-launcher" or "suwayomi-launcher.sh" in the resource directory.
set -e
# ── Locate our resource directory ─────────────────────────────────────────────
# In an AppImage: resources sit at <mountpoint>/resources/
# In a deb install: /usr/lib/moku/resources/ (Tauri's default)
# We resolve relative to this script's own location.
SELF="$0"
while [ -L "$SELF" ]; do
SELF="$(readlink "$SELF")"
done
SCRIPT_DIR="$(cd "$(dirname "$SELF")" && pwd)"
# Tauri places resources one level up from the binary on Linux.
# Try a few candidates so this works in both AppImage and installed layouts.
find_resource() {
for candidate in \
"${SCRIPT_DIR}" \
"${SCRIPT_DIR}/../resources" \
"${SCRIPT_DIR}/resources"
do
if [ -f "${candidate}/Suwayomi-Server.jar" ]; then
echo "$(cd "$candidate" && pwd)"
return 0
fi
done
return 1
}
RESOURCE_DIR=$(find_resource) || {
echo "[launcher] ERROR: cannot locate Suwayomi-Server.jar relative to $SCRIPT_DIR" >&2
exit 1
}
JAR="${RESOURCE_DIR}/Suwayomi-Server.jar"
JAVA="${RESOURCE_DIR}/jre/bin/java"
CATCH_ABORT="${RESOURCE_DIR}/catch_abort.so"
echo "[launcher] RESOURCE_DIR=$RESOURCE_DIR" >&2
echo "[launcher] JAVA=$JAVA" >&2
echo "[launcher] JAR=$JAR" >&2
if [ ! -x "$JAVA" ]; then
echo "[launcher] ERROR: java not executable at $JAVA" >&2
exit 1
fi
if [ ! -f "$JAR" ]; then
echo "[launcher] ERROR: jar not found at $JAR" >&2
exit 1
fi
# ── Data directory ─────────────────────────────────────────────────────────────
DATA_DIR="${XDG_DATA_HOME:-$HOME/.local/share}/Tachidesk"
mkdir -p "$DATA_DIR"
# ── Seed server.conf on first run ──────────────────────────────────────────────
if [ ! -f "$DATA_DIR/server.conf" ]; then
cat > "$DATA_DIR/server.conf" << 'EOF'
server.ip = "127.0.0.1"
server.port = 4567
server.webUIEnabled = true
server.initialOpenInBrowserEnabled = false
server.systemTrayEnabled = false
server.webUIInterface = "browser"
server.webUIFlavor = "WebUI"
server.webUIChannel = "PREVIEW"
server.electronPath = ""
server.debugLogsEnabled = false
server.downloadAsCbz = true
server.autoDownloadNewChapters = false
server.globalUpdateInterval = 12
server.maxSourcesInParallel = 6
server.extensionRepos = []
EOF
fi
# ── Force-patch the three keys that cause JCEF/GUI crashes ────────────────────
sed -i \
-e 's|server\.webUIEnabled.*|server.webUIEnabled = true|' \
-e 's|server\.initialOpenInBrowserEnabled.*|server.initialOpenInBrowserEnabled = false|' \
-e 's|server\.systemTrayEnabled.*|server.systemTrayEnabled = false|' \
"$DATA_DIR/server.conf"
# Append keys if absent (e.g. user-managed conf missing them)
grep -q 'server\.webUIEnabled' "$DATA_DIR/server.conf" || echo 'server.webUIEnabled = true' >> "$DATA_DIR/server.conf"
grep -q 'server\.initialOpenInBrowserEnabled' "$DATA_DIR/server.conf" || echo 'server.initialOpenInBrowserEnabled = false' >> "$DATA_DIR/server.conf"
grep -q 'server\.systemTrayEnabled' "$DATA_DIR/server.conf" || echo 'server.systemTrayEnabled = false' >> "$DATA_DIR/server.conf"
# ── Suppress any GUI environment that would confuse the JVM ───────────────────
unset DISPLAY
unset WAYLAND_DISPLAY
export _JAVA_OPTIONS="-Djava.awt.headless=true"
export JAVA_TOOL_OPTIONS="-Djava.awt.headless=true"
# ── LD_PRELOAD catch_abort.so if present ──────────────────────────────────────
# Catches SIGTRAP/SIGILL from KCEF/Webview so a bad extension can't
# bring down the whole server process (mirrors the Flatpak build).
if [ -f "$CATCH_ABORT" ]; then
export LD_PRELOAD="${CATCH_ABORT}${LD_PRELOAD:+:$LD_PRELOAD}"
fi
exec "$JAVA" \
-Djava.awt.headless=true \
-Dapple.awt.UIElement=true \
-Dsun.java2d.noddraw=true \
-Dsun.awt.disablegui=true \
-Dsuwayomi.tachidesk.config.server.rootDir="$DATA_DIR" \
-jar "$JAR"
+66
View File
@@ -0,0 +1,66 @@
#!/bin/sh
# Moku — Suwayomi launcher sidecar for macOS.
# Tauri calls this script directly as a sidecar (Contents/MacOS/suwayomi-server-{arch}).
# The Suwayomi bundle is placed by Tauri into Contents/Resources/suwayomi-bundle/.
set -e
# Resolve the real directory of this script, following symlinks.
SELF="$0"
while [ -L "$SELF" ]; do
SELF="$(readlink "$SELF")"
done
DIR="$(cd "$(dirname "$SELF")" && pwd)"
# ── Locate the bundle ─────────────────────────────────────────────────────────
# Inside .app: sidecar = Contents/MacOS/suwayomi-server-{arch}
# bundle = Contents/Resources/suwayomi-bundle/
# Dev / flat layout: bundle sits next to the sidecar, or one level up.
find_bundle() {
local base="$1"
for candidate in \
"${base}/../Resources/suwayomi-bundle" \
"${base}/suwayomi-bundle" \
"${base}/../suwayomi-bundle"
do
# The jar lives at <bundle>/bin/Suwayomi-Server.jar
if [ -f "${candidate}/bin/Suwayomi-Server.jar" ]; then
# Canonicalise (no readlink -f on older macOS sh, use cd trick)
echo "$(cd "$candidate" && pwd)"
return 0
fi
done
return 1
}
BUNDLE=$(find_bundle "$DIR") || {
echo "[sidecar] ERROR: cannot locate suwayomi-bundle relative to $DIR" >&2
echo "[sidecar] Tried:" >&2
echo " $DIR/../Resources/suwayomi-bundle" >&2
echo " $DIR/suwayomi-bundle" >&2
echo " $DIR/../suwayomi-bundle" >&2
exit 1
}
JAVA="${BUNDLE}/jre/bin/java"
JAR="${BUNDLE}/bin/Suwayomi-Server.jar"
echo "[sidecar] BUNDLE=$BUNDLE" >&2
echo "[sidecar] JAVA=$JAVA" >&2
echo "[sidecar] JAR=$JAR" >&2
if [ ! -x "$JAVA" ]; then
echo "[sidecar] ERROR: java not executable at $JAVA" >&2
exit 1
fi
if [ ! -f "$JAR" ]; then
echo "[sidecar] ERROR: jar not found at $JAR" >&2
exit 1
fi
# "$@" will contain the -Dsuwayomi.tachidesk.config.server.rootDir=... flag
# prepended by spawn_server in lib.rs, followed by -jar <path>.
# We call java directly so all JVM flags reach it properly.
exec "$JAVA" \
-Djava.awt.headless=true \
"$@" \
-jar "$JAR"
+42 -11
View File
@@ -1,19 +1,50 @@
{
"$schema": "../gen/schemas/desktop-schema.json",
"identifier": "default",
"description": "Allow launching tachidesk-server",
"windows": ["main"],
"description": "Default permissions for Moku",
"windows": [
"main"
],
"permissions": [
"core:default",
"core:tray:default",
"core:app:allow-default-window-icon",
"core:window:allow-hide",
"core:window:allow-show",
"shell:allow-open",
{
"identifier": "shell:allow-spawn",
"allow": [
{
"name": "tachidesk-server",
"cmd": "tachidesk-server"
}
]
}
"shell:allow-kill",
"shell:allow-spawn",
"shell:allow-execute",
"core:window:allow-minimize",
"core:window:allow-unminimize",
"core:window:allow-maximize",
"core:window:allow-unmaximize",
"core:window:allow-toggle-maximize",
"core:window:allow-close",
"core:window:allow-start-dragging",
"core:window:allow-set-focus",
"core:window:allow-set-fullscreen",
"core:window:allow-is-fullscreen",
"core:window:allow-is-maximized",
"core:window:allow-is-minimized",
"core:window:allow-inner-size",
"core:window:allow-outer-size",
"core:window:allow-inner-position",
"core:window:allow-outer-position",
"core:window:allow-scale-factor",
"process:default",
"process:allow-exit",
"process:allow-restart",
"http:default",
"http:allow-fetch",
"store:default",
"discord-rpc:default",
"discord-rpc:allow-connect",
"discord-rpc:allow-disconnect",
"discord-rpc:allow-set-activity",
"discord-rpc:allow-clear-activity",
"discord-rpc:allow-is-running",
"dialog:default",
"dialog:allow-open"
]
}
+17
View File
@@ -0,0 +1,17 @@
{
"$schema": "../gen/schemas/desktop-schema.json",
"identifier": "http-scope",
"description": "HTTP fetch scope",
"windows": ["main"],
"permissions": [
{
"identifier": "http:default",
"allow": [
{ "url": "http://*:*/*" },
{ "url": "https://*:*/*" },
{ "url": "http://*/*" },
{ "url": "https://*/*" }
]
}
]
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 KiB

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.8 KiB

After

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 803 B

After

Width:  |  Height:  |  Size: 740 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 706 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.8 KiB

After

Width:  |  Height:  |  Size: 16 KiB

+100
View File
@@ -0,0 +1,100 @@
use std::path::PathBuf;
use tauri::Manager;
fn backup_dir(app: &tauri::AppHandle) -> PathBuf {
app.path()
.app_data_dir()
.unwrap_or_else(|_| PathBuf::from("."))
.join("backups")
}
fn unix_now() -> u64 {
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_secs()
}
#[tauri::command]
pub async fn export_app_data(app: tauri::AppHandle, bytes: Vec<u8>) -> Result<(), String> {
use tauri_plugin_dialog::DialogExt;
let filename = format!("moku-backup-{}.zip", unix_now());
let path = app
.dialog()
.file()
.set_title("Save Moku app data backup")
.set_file_name(&filename)
.add_filter("Moku Backup", &["zip"])
.blocking_save_file()
.ok_or("Cancelled")?;
std::fs::write(PathBuf::from(path.to_string()), &bytes).map_err(|e| e.to_string())
}
#[tauri::command]
pub async fn import_app_data(app: tauri::AppHandle) -> Result<Vec<u8>, String> {
use tauri_plugin_dialog::DialogExt;
let path = app
.dialog()
.file()
.set_title("Open Moku app data backup")
.add_filter("Moku Backup", &["zip"])
.blocking_pick_file()
.ok_or("Cancelled")?;
std::fs::read(PathBuf::from(path.to_string())).map_err(|e| e.to_string())
}
#[tauri::command]
pub fn auto_backup_app_data(app: tauri::AppHandle, bytes: Vec<u8>) -> Result<(), String> {
let dir = backup_dir(&app);
std::fs::create_dir_all(&dir).map_err(|e| e.to_string())?;
let dest = dir.join(format!("auto-moku-backup-{}.zip", unix_now()));
std::fs::write(&dest, &bytes).map_err(|e| e.to_string())?;
let mut entries: Vec<_> = std::fs::read_dir(&dir)
.map_err(|e| e.to_string())?
.filter_map(|e| e.ok())
.filter(|e| {
e.file_name()
.to_string_lossy()
.starts_with("auto-moku-backup-")
})
.collect();
entries.sort_by_key(|e| e.file_name());
for old in entries.iter().take(entries.len().saturating_sub(5)) {
let _ = std::fs::remove_file(old.path());
}
Ok(())
}
#[tauri::command]
pub fn get_auto_backup_dir(app: tauri::AppHandle) -> String {
let dir = backup_dir(&app);
let _ = std::fs::create_dir_all(&dir);
dir.to_string_lossy().into_owned()
}
#[tauri::command]
pub fn read_store_files(app: tauri::AppHandle, names: Vec<String>) -> Vec<(String, String)> {
let base = app
.path()
.app_local_data_dir()
.unwrap_or_else(|_| PathBuf::from("."));
names
.into_iter()
.map(|name| {
let content = std::fs::read_to_string(base.join(&name))
.unwrap_or_else(|_| "{}".to_string());
(name, content)
})
.collect()
}
+103
View File
@@ -0,0 +1,103 @@
#[cfg(target_os = "windows")]
mod windows_hello {
use windows::{
core::HSTRING,
Security::Credentials::UI::{UserConsentVerificationResult, UserConsentVerifier},
Win32::UI::WindowsAndMessaging::{
BringWindowToTop, FindWindowW, IsIconic, SetForegroundWindow, ShowWindow, SW_RESTORE,
},
};
fn to_wide(s: &str) -> Vec<u16> {
use std::os::windows::ffi::OsStrExt;
std::ffi::OsStr::new(s)
.encode_wide()
.chain(std::iter::once(0))
.collect()
}
fn try_focus_hello_dialog() -> bool {
let cls = to_wide("Credential Dialog Xaml Host");
unsafe {
let Ok(hwnd) = FindWindowW(
windows::core::PCWSTR(cls.as_ptr()),
windows::core::PCWSTR::null(),
) else {
return false;
};
if IsIconic(hwnd).as_bool() {
let _ = ShowWindow(hwnd, SW_RESTORE);
}
let _ = BringWindowToTop(hwnd);
let _ = SetForegroundWindow(hwnd);
true
}
}
fn nudge_focus(retries: u32, delay_ms: u64) {
std::thread::spawn(move || {
std::thread::sleep(std::time::Duration::from_millis(delay_ms));
for _ in 0..retries {
if try_focus_hello_dialog() {
break;
}
std::thread::sleep(std::time::Duration::from_millis(delay_ms));
}
});
}
pub fn authenticate(reason: &str) -> Result<(), String> {
let reason = reason.to_owned();
let (tx, rx) = std::sync::mpsc::channel();
std::thread::spawn(move || {
nudge_focus(5, 250);
let outcome = UserConsentVerifier::RequestVerificationAsync(&HSTRING::from(reason.as_str()))
.and_then(|op| {
nudge_focus(5, 250);
op.get()
});
let _ = tx.send(outcome);
});
let result = rx
.recv()
.map_err(|e| format!("internalError:{e:?}"))?
.map_err(|e| format!("internalError:{e:?}"))?;
match result {
UserConsentVerificationResult::Verified => Ok(()),
UserConsentVerificationResult::Canceled => Err("userCancel".into()),
UserConsentVerificationResult::RetriesExhausted => Err("biometryLockout".into()),
UserConsentVerificationResult::DeviceBusy => Err("systemCancel".into()),
UserConsentVerificationResult::DeviceNotPresent => Err("biometryNotAvailable".into()),
UserConsentVerificationResult::DisabledByPolicy => Err("biometryNotAvailable".into()),
UserConsentVerificationResult::NotConfiguredForUser => Err("biometryNotEnrolled".into()),
_ => Err("authenticationFailed".into()),
}
}
pub fn is_available() -> bool {
use windows::Security::Credentials::UI::UserConsentVerifierAvailability;
UserConsentVerifier::CheckAvailabilityAsync()
.and_then(|op| op.get())
.map(|a| a == UserConsentVerifierAvailability::Available)
.unwrap_or(false)
}
}
#[tauri::command]
pub fn windows_hello_authenticate(_reason: String) -> Result<(), String> {
#[cfg(target_os = "windows")]
return windows_hello::authenticate(&_reason);
#[cfg(not(target_os = "windows"))]
Err("notSupported".into())
}
#[tauri::command]
pub fn windows_hello_available() -> bool {
#[cfg(target_os = "windows")]
return windows_hello::is_available();
#[cfg(not(target_os = "windows"))]
false
}
+6
View File
@@ -0,0 +1,6 @@
pub mod backup;
pub mod biometric;
pub mod server;
pub mod storage;
pub mod system;
pub mod updater;
+97
View File
@@ -0,0 +1,97 @@
use crate::server::{self, resolve::suwayomi_data_dir, SpawnError};
use crate::ServerState;
use tauri::Manager;
#[tauri::command]
pub fn spawn_server(
binary: String,
binary_args: Option<String>,
web_ui_enabled: bool,
app: tauri::AppHandle,
) -> Result<(), SpawnError> {
{
let state = app.state::<ServerState>();
if state.0.lock().unwrap().is_some() {
return Ok(());
}
}
let data_dir = suwayomi_data_dir();
let log_path = data_dir.join("moku-spawn.log");
let _ = std::fs::create_dir_all(&data_dir);
let mut log = std::fs::OpenOptions::new()
.create(true)
.append(true)
.open(&log_path)
.ok();
let binary_args = binary_args.unwrap_or_default();
server::do_log(
&mut log,
&format!(
"[spawn_server] binary={:?} binary_args={:?} web_ui_enabled={} data_dir={:?}",
binary, binary_args, web_ui_enabled, data_dir
),
);
server::conf::seed_server_conf(&data_dir, web_ui_enabled);
let mut invocation =
server::resolve::resolve_server_binary(&binary, &app, &mut log).map_err(|e| {
server::do_log(&mut log, &format!("[spawn_server] resolve failed: {:?}", e));
e
})?;
if !binary_args.trim().is_empty() {
let extra: Vec<String> = binary_args.split_whitespace().map(|s| s.to_string()).collect();
let mut merged = extra;
merged.extend(invocation.args);
invocation.args = merged;
}
if invocation.bin.ends_with("java") || invocation.bin.ends_with("java.exe") {
let rootdir_flag = format!(
"-Dsuwayomi.tachidesk.config.server.rootDir={}",
data_dir.to_string_lossy()
);
invocation.args.insert(0, rootdir_flag);
}
let working_dir = invocation
.working_dir
.unwrap_or_else(|| std::env::current_dir().unwrap_or_default());
server::do_log(
&mut log,
&format!(
"[spawn_server] bin={:?} args={:?} cwd={:?}",
invocation.bin, invocation.args, working_dir
),
);
use tauri_plugin_shell::ShellExt;
let cmd = app
.shell()
.command(&invocation.bin)
.env("JAVA_TOOL_OPTIONS", "-Djava.awt.headless=true")
.args(&invocation.args)
.current_dir(&working_dir);
match cmd.spawn() {
Ok((_rx, child)) => {
*app.state::<ServerState>().0.lock().unwrap() = Some(child);
Ok(())
}
Err(e) => {
server::do_log(&mut log, &format!("[spawn_server] spawn failed: {}", e));
Err(SpawnError::SpawnFailed(e.to_string()))
}
}
}
#[tauri::command]
pub fn kill_server(app: tauri::AppHandle) -> Result<(), String> {
server::kill_tachidesk(&app);
Ok(())
}
+178
View File
@@ -0,0 +1,178 @@
use serde::Serialize;
use std::path::PathBuf;
use sysinfo::Disks;
use tauri::Emitter;
use tauri_plugin_store::StoreExt;
use walkdir::WalkDir;
use crate::server::resolve::suwayomi_data_dir;
// ── Key-value store (used by the frontend via platformService) ────────────────
#[tauri::command]
pub fn load_store(app: tauri::AppHandle, key: String) -> Result<Option<String>, String> {
let store = app
.store(format!("{}.json", key))
.map_err(|e| e.to_string())?;
let value = store.get(&key);
Ok(value.map(|v| v.to_string()))
}
#[tauri::command]
pub fn save_store(app: tauri::AppHandle, key: String, value: String) -> Result<(), String> {
let store = app
.store(format!("{}.json", key))
.map_err(|e| e.to_string())?;
let parsed: serde_json::Value =
serde_json::from_str(&value).map_err(|e| e.to_string())?;
store.set(key, parsed);
store.save().map_err(|e| e.to_string())
}
// ── Credential store (PIN-encrypted vault, auth tokens) ──────────────────────
#[tauri::command]
pub fn store_credential(app: tauri::AppHandle, key: String, value: String) -> Result<(), String> {
let store = app
.store("credentials.json")
.map_err(|e| e.to_string())?;
if value.is_empty() {
store.delete(&key);
} else {
store.set(&key, serde_json::Value::String(value));
}
store.save().map_err(|e| e.to_string())
}
#[tauri::command]
pub fn get_credential(app: tauri::AppHandle, key: String) -> Result<Option<String>, String> {
let store = app
.store("credentials.json")
.map_err(|e| e.to_string())?;
Ok(store.get(&key).and_then(|v| v.as_str().map(|s| s.to_owned())))
}
// ── Disk / downloads storage ─────────────────────────────────────────────────
#[derive(Serialize)]
pub struct StorageInfo {
pub manga_bytes: u64,
pub total_bytes: u64,
pub free_bytes: u64,
pub path: String,
}
fn resolve_downloads_path(downloads_path: &str) -> PathBuf {
if !downloads_path.trim().is_empty() {
return PathBuf::from(downloads_path.trim());
}
suwayomi_data_dir().join("downloads")
}
#[tauri::command]
pub fn get_storage_info(downloads_path: String) -> Result<StorageInfo, String> {
let path = resolve_downloads_path(&downloads_path);
let manga_bytes = if path.exists() {
WalkDir::new(&path)
.into_iter()
.filter_map(|e| e.ok())
.filter_map(|e| e.metadata().ok())
.filter(|m| m.is_file())
.map(|m| m.len())
.sum()
} else {
0
};
let stat_path = if path.exists() {
path.clone()
} else {
dirs::home_dir().unwrap_or_else(|| PathBuf::from("/"))
};
let disks = Disks::new_with_refreshed_list();
let disk = disks
.iter()
.filter(|d| stat_path.starts_with(d.mount_point()))
.max_by_key(|d| d.mount_point().as_os_str().len())
.ok_or_else(|| "Could not find disk for path".to_string())?;
Ok(StorageInfo {
manga_bytes,
total_bytes: disk.total_space(),
free_bytes: disk.available_space(),
path: path.to_string_lossy().into_owned(),
})
}
#[tauri::command]
pub fn get_default_downloads_path() -> String {
resolve_downloads_path("").to_string_lossy().into_owned()
}
#[tauri::command]
pub fn check_path_exists(path: String) -> bool {
std::path::Path::new(path.trim()).is_dir()
}
#[tauri::command]
pub fn create_directory(path: String) -> Result<(), String> {
std::fs::create_dir_all(path.trim()).map_err(|e| e.to_string())
}
#[tauri::command]
pub async fn migrate_downloads(
app: tauri::AppHandle,
src: String,
dst: String,
) -> Result<(), String> {
use std::fs;
let src_path = PathBuf::from(src.trim());
let dst_path = PathBuf::from(dst.trim());
if !src_path.is_dir() {
return Ok(());
}
let total: u64 = WalkDir::new(&src_path)
.into_iter()
.filter_map(|e| e.ok())
.filter(|e| e.file_type().is_file())
.count() as u64;
let _ = app.emit(
"migrate_progress",
serde_json::json!({ "done": 0u64, "total": total, "current": "" }),
);
let mut done: u64 = 0;
for entry in WalkDir::new(&src_path).into_iter().filter_map(|e| e.ok()) {
let rel = entry
.path()
.strip_prefix(&src_path)
.map_err(|e| e.to_string())?;
let target = dst_path.join(rel);
if entry.file_type().is_dir() {
fs::create_dir_all(&target).map_err(|e| e.to_string())?;
} else {
if let Some(parent) = target.parent() {
fs::create_dir_all(parent).map_err(|e| e.to_string())?;
}
fs::copy(entry.path(), &target).map_err(|e| e.to_string())?;
done += 1;
let _ = app.emit(
"migrate_progress",
serde_json::json!({
"done": done, "total": total, "current": rel.to_string_lossy()
}),
);
}
}
fs::remove_dir_all(&src_path).map_err(|e| e.to_string())?;
Ok(())
}
+192
View File
@@ -0,0 +1,192 @@
#[cfg(target_os = "windows")]
use crate::server::resolve::strip_unc;
use std::path::PathBuf;
use tauri::Manager;
#[tauri::command]
pub fn get_platform_ui_scale(window: tauri::Window) -> f64 {
window.scale_factor().unwrap_or(1.0)
}
#[tauri::command]
pub fn restart_app(app: tauri::AppHandle) {
tauri::process::restart(&app.env());
}
#[tauri::command]
pub fn open_path(path: String) -> Result<(), String> {
#[cfg(target_os = "windows")]
{
let p = strip_unc(PathBuf::from(path.trim()));
std::process::Command::new("explorer")
.arg(p)
.spawn()
.map_err(|e| e.to_string())?;
}
#[cfg(target_os = "macos")]
{
let p = std::path::Path::new(path.trim());
std::process::Command::new("open")
.arg(p)
.spawn()
.map_err(|e| e.to_string())?;
}
#[cfg(not(any(target_os = "windows", target_os = "macos")))]
{
let p = std::path::Path::new(path.trim());
std::process::Command::new("xdg-open")
.arg(p)
.spawn()
.map_err(|e| e.to_string())?;
}
Ok(())
}
#[tauri::command]
pub async fn pick_downloads_folder(app: tauri::AppHandle) -> Option<String> {
use tauri_plugin_dialog::DialogExt;
app.dialog()
.file()
.set_title("Choose Downloads Folder")
.blocking_pick_folder()
.map(|p| p.to_string())
}
#[tauri::command]
pub async fn pick_server_binary(app: tauri::AppHandle) -> Option<String> {
use tauri_plugin_dialog::DialogExt;
#[cfg(target_os = "windows")]
let dialog = app
.dialog()
.file()
.set_title("Choose Server Binary")
.add_filter("Executable", &["exe", "jar", "bat", "cmd"]);
#[cfg(target_os = "macos")]
let dialog = app
.dialog()
.file()
.set_title("Choose Server Binary")
.add_filter("Executable or JAR", &["jar", "command", "sh", "app"]);
#[cfg(not(any(target_os = "windows", target_os = "macos")))]
let dialog = app
.dialog()
.file()
.set_title("Choose Server Binary")
.add_filter("Executable or JAR", &["jar", "sh"]);
dialog.blocking_pick_file().map(|p| p.to_string())
}
#[tauri::command]
pub fn exit_app(app: tauri::AppHandle) {
app.exit(0);
}
fn remove_dir_best_effort(path: &std::path::Path) {
if path.is_file() {
if let Err(e) = std::fs::remove_file(path) {
if e.raw_os_error() == Some(32) {
return;
}
}
} else if path.is_dir() {
if let Ok(entries) = std::fs::read_dir(path) {
for entry in entries.flatten() {
remove_dir_best_effort(&entry.path());
}
}
let _ = std::fs::remove_dir(path);
}
}
fn wait_until_deletable(path: &std::path::Path, timeout_secs: u64) -> bool {
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(timeout_secs);
while std::time::Instant::now() < deadline {
let locked = if path.is_file() {
std::fs::OpenOptions::new().write(true).open(path).is_err()
} else if path.is_dir() {
std::fs::read_dir(path).is_err()
} else {
return true;
};
if !locked {
return true;
}
std::thread::sleep(std::time::Duration::from_millis(200));
}
false
}
#[tauri::command]
pub async fn clear_moku_cache(app: tauri::AppHandle) -> Result<(), String> {
let window = app.get_webview_window("main").ok_or("no main window")?;
let (tx, rx) = tokio::sync::oneshot::channel::<Result<(), String>>();
window
.with_webview(move |_wv| {
let _ = tx.send(Ok(()));
})
.map_err(|e| e.to_string())?;
rx.await.map_err(|e| e.to_string())??;
let cache_dir = app.path().app_cache_dir().map_err(|e| e.to_string())?;
if cache_dir.exists() {
wait_until_deletable(&cache_dir, 3);
remove_dir_best_effort(&cache_dir);
std::fs::create_dir_all(&cache_dir).map_err(|e| e.to_string())?;
}
Ok(())
}
#[tauri::command]
pub fn clear_suwayomi_cache() -> Result<(), String> {
use crate::server::resolve::suwayomi_data_dir;
let data_dir = suwayomi_data_dir();
for dir in &["cache/kcef", "logs"] {
let p = data_dir.join(dir);
if p.exists() {
remove_dir_best_effort(&p);
}
}
for dir in &["downloads/thumbnails"] {
let p = data_dir.join(dir);
if p.exists() {
remove_dir_best_effort(&p);
let _ = std::fs::create_dir_all(&p);
}
}
Ok(())
}
#[tauri::command]
pub fn reset_suwayomi_data(app: tauri::AppHandle) -> Result<(), String> {
use crate::server::resolve::suwayomi_data_dir;
crate::server::kill_tachidesk(&app);
let data_dir = suwayomi_data_dir();
let targets = ["database.mv.db", "extensions", "settings", "logs", "local"];
for entry_name in &targets {
let p = data_dir.join(entry_name);
if p.exists() {
wait_until_deletable(&p, 10);
}
}
for entry_name in &targets {
let p = data_dir.join(entry_name);
if p.is_dir() {
std::fs::remove_dir_all(&p).map_err(|e| format!("{entry_name}: {e}"))?;
} else if p.exists() {
std::fs::remove_file(&p).map_err(|e| format!("{entry_name}: {e}"))?;
}
}
Ok(())
}
+150
View File
@@ -0,0 +1,150 @@
use serde::{Deserialize, Serialize};
#[derive(Serialize, Clone)]
pub struct ReleaseInfo {
pub tag_name: String,
pub name: String,
pub body: String,
pub published_at: String,
pub html_url: String,
}
#[derive(Clone, Serialize)]
#[cfg_attr(not(target_os = "windows"), allow(dead_code))]
struct UpdateProgress {
downloaded: u64,
total: Option<u64>,
}
#[tauri::command]
pub async fn list_releases() -> Result<Vec<ReleaseInfo>, String> {
use tauri_plugin_http::reqwest;
#[derive(Deserialize)]
struct GhRelease {
tag_name: String,
name: Option<String>,
body: Option<String>,
published_at: Option<String>,
html_url: String,
}
let client = reqwest::Client::builder()
.user_agent("Moku")
.build()
.map_err(|e| e.to_string())?;
let resp = client
.get("https://api.github.com/repos/moku-project/Moku/releases?per_page=30")
.send()
.await
.map_err(|e| e.to_string())?;
if !resp.status().is_success() {
return Err(format!("GitHub API returned {}", resp.status()));
}
let releases: Vec<GhRelease> =
serde_json::from_str(&resp.text().await.map_err(|e| e.to_string())?)
.map_err(|e| e.to_string())?;
Ok(releases
.into_iter()
.map(|r| ReleaseInfo {
tag_name: r.tag_name.clone(),
name: r.name.unwrap_or_else(|| r.tag_name.clone()),
body: r.body.unwrap_or_default(),
published_at: r.published_at.unwrap_or_default(),
html_url: r.html_url,
})
.collect())
}
#[tauri::command]
#[allow(unused_variables)]
pub async fn download_and_install_update(app: tauri::AppHandle, tag: String) -> Result<(), String> {
#[cfg(not(target_os = "windows"))]
return Err("Native install is Windows-only; open the GitHub release page instead.".into());
#[cfg(target_os = "windows")]
{
use std::io::Write;
use tauri::Emitter;
use tauri_plugin_http::reqwest;
#[derive(Deserialize)]
struct Asset {
name: String,
browser_download_url: String,
size: u64,
}
#[derive(Deserialize)]
struct Release {
assets: Vec<Asset>,
}
let client = reqwest::Client::builder()
.user_agent("Moku")
.build()
.map_err(|e| e.to_string())?;
let resp = client
.get(format!(
"https://api.github.com/repos/moku-project/Moku/releases/tags/{}",
tag
))
.send()
.await
.map_err(|e| e.to_string())?;
if !resp.status().is_success() {
return Err(format!(
"GitHub API returned {} for tag {}",
resp.status(),
tag
));
}
let release: Release = serde_json::from_str(&resp.text().await.map_err(|e| e.to_string())?)
.map_err(|e| e.to_string())?;
let asset = release
.assets
.into_iter()
.find(|a| a.name.ends_with("_x64-setup.exe"))
.ok_or_else(|| format!("No x64-setup.exe asset found in release {}", tag))?;
let total = if asset.size > 0 {
Some(asset.size)
} else {
None
};
let mut resp = client
.get(&asset.browser_download_url)
.send()
.await
.map_err(|e| e.to_string())?;
let tmp_path = std::env::temp_dir().join(&asset.name);
let mut file = std::fs::File::create(&tmp_path).map_err(|e| e.to_string())?;
let mut downloaded: u64 = 0;
while let Some(chunk) = resp.chunk().await.map_err(|e| e.to_string())? {
file.write_all(&chunk).map_err(|e| e.to_string())?;
downloaded += chunk.len() as u64;
let _ = app.emit("update-progress", UpdateProgress { downloaded, total });
}
drop(file);
use std::os::windows::process::CommandExt;
const CREATE_NO_WINDOW: u32 = 0x08000000;
std::process::Command::new(&tmp_path)
.creation_flags(CREATE_NO_WINDOW)
.spawn()
.map_err(|e| e.to_string())?;
let _ = app.emit("update-launching", ());
Ok(())
}
}
+141 -70
View File
@@ -1,97 +1,168 @@
use std::path::PathBuf;
mod commands;
mod server;
use std::sync::Mutex;
use nix::sys::statvfs::statvfs;
use serde::Serialize;
use tauri::Manager;
use tauri_plugin_shell::{ShellExt, process::CommandChild};
use walkdir::WalkDir;
use std::io::{Read, Write};
use std::net::{TcpListener, TcpStream};
use tauri::{
menu::{Menu, MenuItem, PredefinedMenuItem},
tray::{MouseButton, MouseButtonState, TrayIconBuilder, TrayIconEvent},
Manager, WindowEvent,
};
use tauri_plugin_shell::process::CommandChild;
struct ServerState(Mutex<Option<CommandChild>>);
pub struct ServerState(pub Mutex<Option<CommandChild>>);
#[derive(Serialize)]
pub struct StorageInfo {
manga_bytes: u64,
total_bytes: u64,
free_bytes: u64,
path: String,
const IPC_PORT: u16 = 47823;
const HANDSHAKE: &[u8] = b"MOKU:1\n";
const FOCUS_CMD: &[u8] = b"focus\n";
fn do_quit(app: &tauri::AppHandle) {
server::kill_tachidesk(app);
app.exit(0);
}
fn resolve_downloads_path(downloads_path: &str) -> PathBuf {
if !downloads_path.trim().is_empty() {
return PathBuf::from(downloads_path);
fn start_instance_listener(app: tauri::AppHandle) {
std::thread::spawn(move || {
let Ok(listener) = TcpListener::bind(("127.0.0.1", IPC_PORT)) else {
return;
};
for stream in listener.incoming().flatten() {
handle_ipc_connection(stream, &app);
}
});
}
fn handle_ipc_connection(mut stream: TcpStream, app: &tauri::AppHandle) {
let mut buf = [0u8; 32];
let Ok(n) = stream.read(&mut buf) else { return };
let msg = &buf[..n];
if !msg.starts_with(HANDSHAKE) {
return;
}
let cmd = &msg[HANDSHAKE.len()..];
if cmd.starts_with(b"focus") {
let _ = stream.write_all(b"ok\n");
if let Some(win) = app.get_webview_window("main") {
let _ = win.show();
let _ = win.unminimize();
let _ = win.set_focus();
}
}
let base = std::env::var("XDG_DATA_HOME")
.map(PathBuf::from)
.unwrap_or_else(|_| {
dirs::home_dir()
.unwrap_or_else(|| PathBuf::from("/"))
.join(".local/share")
});
base.join("Tachidesk/downloads")
}
#[tauri::command]
fn get_storage_info(downloads_path: String) -> Result<StorageInfo, String> {
let path = resolve_downloads_path(&downloads_path);
let manga_bytes = if path.exists() {
WalkDir::new(&path)
.into_iter()
.filter_map(|e| e.ok())
.filter_map(|e| e.metadata().ok())
.filter(|m| m.is_file())
.map(|m| m.len())
.sum()
} else {
0
fn signal_existing_instance() -> bool {
let Ok(mut stream) = TcpStream::connect(("127.0.0.1", IPC_PORT)) else {
return false;
};
stream.set_read_timeout(Some(std::time::Duration::from_millis(500))).ok();
let stat_path = if path.exists() { path.clone() } else {
dirs::home_dir().unwrap_or_else(|| PathBuf::from("/"))
};
let vfs = statvfs(&stat_path).map_err(|e| e.to_string())?;
let mut msg = Vec::new();
msg.extend_from_slice(HANDSHAKE);
msg.extend_from_slice(FOCUS_CMD);
// f_frsize is the fundamental block size used for block counts.
// f_bsize (block_size()) is just the preferred I/O size and must not be
// used with blocks()/blocks_free() — that gives wildly wrong numbers.
let frsize = vfs.fragment_size() as u64;
let total_bytes = vfs.blocks() * frsize;
let free_bytes = vfs.blocks_available() * frsize;
if stream.write_all(&msg).is_err() {
return false;
}
Ok(StorageInfo {
manga_bytes,
total_bytes,
free_bytes,
path: path.to_string_lossy().into_owned(),
})
let mut resp = [0u8; 4];
matches!(stream.read(&mut resp), Ok(n) if resp[..n].starts_with(b"ok"))
}
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
if signal_existing_instance() {
std::process::exit(0);
}
tauri::Builder::default()
.plugin(tauri_plugin_store::Builder::new().build())
.plugin(tauri_plugin_discord_rpc::init())
.plugin(tauri_plugin_dialog::init())
.plugin(tauri_plugin_os::init())
.plugin(tauri_plugin_shell::init())
.plugin(tauri_plugin_http::init())
.plugin(tauri_plugin_process::init())
.manage(ServerState(Mutex::new(None)))
.invoke_handler(tauri::generate_handler![get_storage_info])
.invoke_handler(tauri::generate_handler![
commands::storage::get_storage_info,
commands::storage::get_default_downloads_path,
commands::storage::check_path_exists,
commands::storage::create_directory,
commands::storage::migrate_downloads,
commands::server::spawn_server,
commands::server::kill_server,
commands::system::get_platform_ui_scale,
commands::system::restart_app,
commands::system::exit_app,
commands::system::clear_moku_cache,
commands::system::clear_suwayomi_cache,
commands::system::reset_suwayomi_data,
commands::system::open_path,
commands::system::pick_downloads_folder,
commands::system::pick_server_binary,
commands::backup::export_app_data,
commands::backup::import_app_data,
commands::backup::auto_backup_app_data,
commands::backup::get_auto_backup_dir,
commands::backup::read_store_files,
commands::storage::load_store,
commands::storage::save_store,
commands::storage::store_credential,
commands::storage::get_credential,
commands::updater::list_releases,
commands::updater::download_and_install_update,
commands::biometric::windows_hello_authenticate,
commands::biometric::windows_hello_available,
])
.setup(|app| {
let shell = app.shell();
let app_handle = app.handle().clone();
start_instance_listener(app.handle().clone());
let status = shell.command("tachidesk-server").spawn();
let show = MenuItem::with_id(app, "show", "Show Moku", true, None::<&str>)?;
let sep = PredefinedMenuItem::separator(app)?;
let quit = MenuItem::with_id(app, "quit", "Quit Moku", true, None::<&str>)?;
let menu = Menu::with_items(app, &[&show, &sep, &quit])?;
match status {
Ok((_rx, child)) => {
println!("Tachidesk server process spawned successfully.");
let state = app_handle.state::<ServerState>();
let mut guard = state.0.lock().unwrap();
*guard = Some(child);
}
Err(e) => {
eprintln!("Failed to spawn Tachidesk server: {}", e);
}
}
TrayIconBuilder::new()
.icon(app.default_window_icon().unwrap().clone())
.menu(&menu)
.show_menu_on_left_click(false)
.tooltip("Moku")
.on_menu_event(|app, event| match event.id.as_ref() {
"show" => {
if let Some(win) = app.get_webview_window("main") {
let _ = win.show();
let _ = win.set_focus();
}
}
"quit" => do_quit(app),
_ => {}
})
.on_tray_icon_event(|tray, event| {
if let TrayIconEvent::Click {
button: MouseButton::Left,
button_state: MouseButtonState::Up,
..
} = event
{
let app = tray.app_handle();
if let Some(win) = app.get_webview_window("main") {
let _ = win.show();
let _ = win.set_focus();
}
}
})
.build(app)?;
Ok(())
})
.on_window_event(|window, event| {
if let WindowEvent::Destroyed = event {
server::kill_tachidesk(window.app_handle());
}
})
.run(tauri::generate_context!())
.expect("error while running moku");
.expect("error while running moku")
}
+82
View File
@@ -0,0 +1,82 @@
use std::path::PathBuf;
const DEFAULT_SERVER_CONF: &str = r#"server.ip = "127.0.0.1"
server.port = 4567
server.webUIEnabled = false
server.initialOpenInBrowserEnabled = false
server.systemTrayEnabled = false
server.webUIInterface = "browser"
server.webUIFlavor = "WebUI"
server.webUIChannel = "preview"
server.electronPath = ""
server.debugLogsEnabled = false
server.downloadAsCbz = true
server.autoDownloadNewChapters = false
server.globalUpdateInterval = 12
server.maxSourcesInParallel = 6
server.extensionRepos = []
"#;
pub fn seed_server_conf(data_dir: &PathBuf, web_ui_enabled: bool) {
let conf_path = data_dir.join("server.conf");
if !conf_path.exists() {
if let Err(e) = std::fs::create_dir_all(data_dir) {
eprintln!("Could not create Suwayomi data dir: {e}");
return;
}
let initial = patch_conf_key(
DEFAULT_SERVER_CONF.to_string(),
"server.webUIEnabled",
if web_ui_enabled { "true" } else { "false" },
);
if let Err(e) = std::fs::write(&conf_path, initial) {
eprintln!("Could not write server.conf: {e}");
}
return;
}
let Ok(contents) = std::fs::read_to_string(&conf_path) else {
return;
};
let patched = patch_conf_key(
patch_conf_key(
patch_conf_key(
contents,
"server.webUIEnabled",
if web_ui_enabled { "true" } else { "false" },
),
"server.initialOpenInBrowserEnabled",
"false",
),
"server.systemTrayEnabled",
"false",
);
let _ = std::fs::write(&conf_path, patched);
}
fn patch_conf_key(text: String, key: &str, value: &str) -> String {
let replacement = format!("{key} = {value}");
let lines: Vec<&str> = text.lines().collect();
if let Some(pos) = lines.iter().position(|l| l.trim_start().starts_with(key)) {
let mut out = lines
.iter()
.enumerate()
.map(|(i, l)| if i == pos { replacement.as_str() } else { l })
.collect::<Vec<_>>()
.join("\n");
out.push('\n');
return out;
}
let mut out = text;
if !out.ends_with('\n') {
out.push('\n');
}
out.push_str(&replacement);
out.push('\n');
out
}
+53
View File
@@ -0,0 +1,53 @@
pub mod conf;
pub mod resolve;
use std::io::Write;
use tauri::Manager;
use crate::ServerState;
pub use resolve::SpawnError;
pub fn do_log(log: &mut Option<std::fs::File>, msg: &str) {
eprintln!("{}", msg);
if let Some(f) = log {
let _ = writeln!(f, "{}", msg);
}
}
pub fn kill_tachidesk(app: &tauri::AppHandle) {
let state = app.state::<ServerState>();
if let Some(child) = state.0.lock().unwrap().take() {
let _ = child.kill();
}
#[cfg(target_os = "windows")]
{
use std::os::windows::process::CommandExt;
const CREATE_NO_WINDOW: u32 = 0x08000000;
let _ = std::process::Command::new("taskkill")
.args(["/F", "/FI", "IMAGENAME eq java.exe"])
.creation_flags(CREATE_NO_WINDOW)
.status();
for _ in 0..30 {
let still_running = std::process::Command::new("tasklist")
.args(["/FI", "IMAGENAME eq java.exe", "/NH"])
.creation_flags(CREATE_NO_WINDOW)
.output()
.map(|o| String::from_utf8_lossy(&o.stdout).contains("java.exe"))
.unwrap_or(false);
if !still_running {
break;
}
std::thread::sleep(std::time::Duration::from_millis(100));
}
}
#[cfg(not(target_os = "windows"))]
let _ = std::process::Command::new("pkill")
.args(["-f", "tachidesk"])
.status();
}
+240
View File
@@ -0,0 +1,240 @@
use crate::server::do_log;
use serde::Serialize;
use std::path::PathBuf;
use tauri::Manager;
#[derive(Serialize, Debug)]
#[serde(tag = "kind", content = "message")]
pub enum SpawnError {
NotConfigured(String),
SpawnFailed(String),
}
pub struct ServerInvocation {
pub bin: String,
pub args: Vec<String>,
pub working_dir: Option<PathBuf>,
}
pub fn suwayomi_data_dir() -> PathBuf {
#[cfg(target_os = "windows")]
{
dirs::data_dir()
.unwrap_or_else(|| PathBuf::from("C:\\ProgramData"))
.join("Tachidesk")
}
#[cfg(target_os = "macos")]
{
dirs::data_dir()
.unwrap_or_else(|| dirs::home_dir().unwrap_or_else(|| PathBuf::from("~")))
.join("Tachidesk")
}
#[cfg(not(any(target_os = "windows", target_os = "macos")))]
{
let base = std::env::var("XDG_DATA_HOME")
.map(PathBuf::from)
.unwrap_or_else(|_| dirs::data_dir().unwrap_or_else(|| PathBuf::from("/tmp")));
base.join("Tachidesk")
}
}
pub fn strip_unc(path: PathBuf) -> PathBuf {
let s = path.to_string_lossy();
if let Some(stripped) = s.strip_prefix(r"\\?\") {
PathBuf::from(stripped)
} else {
path
}
}
fn java_bin_name() -> &'static str {
if cfg!(target_os = "windows") { "java.exe" } else { "java" }
}
fn find_java_in_bundle(bundle_dir: &PathBuf, log: &mut Option<std::fs::File>) -> Option<PathBuf> {
let java = bundle_dir.join("jre").join("bin").join(java_bin_name());
do_log(log, &format!("[find_java] {:?} exists={}", java, java.exists()));
if java.exists() { Some(java) } else { None }
}
fn data_root_args() -> Vec<String> {
vec!["--dataRoot".to_string(), suwayomi_data_dir().to_string_lossy().into_owned()]
}
fn jar_data_root_flag() -> String {
format!("-Dsuwayomi.server.rootDir={}", suwayomi_data_dir().to_string_lossy())
}
fn jar_invocation(java: PathBuf, jar: PathBuf, working_dir: PathBuf) -> ServerInvocation {
ServerInvocation {
bin: java.to_string_lossy().into_owned(),
args: vec![
jar_data_root_flag(),
"-jar".to_string(),
jar.to_string_lossy().into_owned(),
],
working_dir: Some(working_dir),
}
}
pub fn resolve_server_binary(
binary: &str,
app: &tauri::AppHandle,
log: &mut Option<std::fs::File>,
) -> Result<ServerInvocation, SpawnError> {
do_log(log, &format!("[resolve] binary={:?}", binary));
if !binary.trim().is_empty() {
let path = strip_unc(PathBuf::from(binary.trim()));
do_log(log, &format!("[resolve] user path={:?} exists={}", path, path.exists()));
if path.exists() {
let working_dir = path.parent().map(|p| p.to_path_buf());
return Ok(ServerInvocation {
bin: path.to_string_lossy().into_owned(),
args: data_root_args(),
working_dir,
});
}
do_log(log, "[resolve] user path not found, falling through");
}
if let Ok(exe) = std::env::current_exe() {
if let Some(bin_dir) = exe.parent() {
for name in &["tachidesk-server", "suwayomi-launcher"] {
let p = bin_dir.join(name);
do_log(log, &format!("[resolve] sibling={:?} exists={}", p, p.exists()));
if p.exists() {
return Ok(ServerInvocation {
bin: p.to_string_lossy().into_owned(),
args: data_root_args(),
working_dir: Some(bin_dir.to_path_buf()),
});
}
}
}
}
#[cfg(not(target_os = "macos"))]
{
let resource_dir = {
let raw = app.path().resource_dir().unwrap_or_default();
let stripped = strip_unc(raw);
do_log(log, &format!("[resolve] resource_dir={:?}", stripped));
stripped
};
let bundle_dir = resource_dir.join("binaries").join("suwayomi-bundle");
let jar = bundle_dir.join("bin").join("Suwayomi-Server.jar");
do_log(log, &format!("[resolve] bundle_dir={:?} exists={}", bundle_dir, bundle_dir.exists()));
do_log(log, &format!("[resolve] jar={:?} exists={}", jar, jar.exists()));
if let Some(java) = find_java_in_bundle(&bundle_dir, log) {
if jar.exists() {
do_log(log, "[resolve] using bundled JRE + jar");
return Ok(jar_invocation(java, jar, bundle_dir));
}
}
for name in &["suwayomi-launcher", "suwayomi-launcher.sh", "tachidesk-server"] {
let p = resource_dir.join(name);
do_log(log, &format!("[resolve] sidecar={:?} exists={}", p, p.exists()));
if p.exists() {
return Ok(ServerInvocation {
bin: p.to_string_lossy().into_owned(),
args: data_root_args(),
working_dir: Some(resource_dir.clone()),
});
}
}
if let Some(java) = find_java_in_bundle(&resource_dir, log) {
let jar = std::fs::read_dir(&resource_dir)
.ok()
.and_then(|mut rd| {
rd.find(|e| e.as_ref().map(|e| e.file_name().to_string_lossy().ends_with(".jar")).unwrap_or(false))
.and_then(|e| e.ok())
.map(|e| e.path())
});
if let Some(jar_path) = jar {
do_log(log, &format!("[resolve] generic JRE java={:?} jar={:?}", java, jar_path));
return Ok(jar_invocation(java, jar_path, resource_dir));
}
}
}
#[cfg(target_os = "macos")]
{
let resource_dir = app.path().resource_dir().unwrap_or_default();
let bundle_dir = resource_dir.join("suwayomi-bundle");
do_log(log, &format!("[resolve] macOS resource_dir={:?}", resource_dir));
do_log(log, &format!("[resolve] macOS bundle_dir={:?} exists={}", bundle_dir, bundle_dir.exists()));
let java = bundle_dir.join("jre").join("bin").join("java");
let jar = bundle_dir.join("bin").join("Suwayomi-Server.jar");
let launcher_sh = bundle_dir.join("Suwayomi Launcher.command");
let launcher_jar = bundle_dir.join("Suwayomi-Launcher.jar");
do_log(log, &format!("[resolve] java={:?} exists={}", java, java.exists()));
do_log(log, &format!("[resolve] jar={:?} exists={}", jar, jar.exists()));
do_log(log, &format!("[resolve] launcher.command={:?} exists={}", launcher_sh, launcher_sh.exists()));
do_log(log, &format!("[resolve] launcher.jar={:?} exists={}", launcher_jar, launcher_jar.exists()));
if java.exists() && jar.exists() {
do_log(log, "[resolve] macOS using bundled JRE + Suwayomi-Server.jar");
return Ok(jar_invocation(java, jar, bundle_dir));
}
if launcher_sh.exists() {
use std::os::unix::fs::PermissionsExt;
let _ = std::fs::set_permissions(&launcher_sh, std::fs::Permissions::from_mode(0o755));
do_log(log, "[resolve] macOS using Suwayomi Launcher.command");
return Ok(ServerInvocation {
bin: launcher_sh.to_string_lossy().into_owned(),
args: vec![],
working_dir: Some(bundle_dir),
});
}
if java.exists() && launcher_jar.exists() {
do_log(log, "[resolve] macOS using bundled JRE + Suwayomi-Launcher.jar");
return Ok(jar_invocation(java, launcher_jar, bundle_dir));
}
do_log(log, "[resolve] macOS bundle not found, falling through to PATH");
}
for name in &["suwayomi-server", "tachidesk-server"] {
#[cfg(target_os = "windows")]
let resolved = std::process::Command::new("where")
.arg(name)
.output()
.ok()
.filter(|o| o.status.success())
.and_then(|o| String::from_utf8(o.stdout).ok())
.and_then(|s| s.lines().next().map(|l| l.trim().to_string()));
#[cfg(not(target_os = "windows"))]
let resolved = std::process::Command::new("which")
.arg(name)
.output()
.ok()
.filter(|o| o.status.success())
.and_then(|o| String::from_utf8(o.stdout).ok())
.and_then(|s| s.lines().next().map(|l| l.trim().to_string()));
if let Some(bin_path) = resolved {
do_log(log, &format!("[resolve] found on PATH: {:?}", bin_path));
return Ok(ServerInvocation {
bin: bin_path,
args: vec![],
working_dir: None,
});
}
}
Err(SpawnError::NotConfigured(
"Server binary not found. Install Suwayomi-Server or set the path in Settings.".to_string(),
))
}
+16 -7
View File
@@ -1,11 +1,11 @@
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "Moku",
"version": "0.2.0",
"identifier": "dev.moku.app",
"version": "0.10.0",
"identifier": "io.github.MokuProject.Moku",
"build": {
"frontendDist": "../dist",
"beforeBuildCommand": "pnpm build"
"beforeBuildCommand": "pnpm build:static"
},
"app": {
"windows": [
@@ -17,7 +17,8 @@
"minHeight": 600,
"resizable": true,
"fullscreen": false,
"decorations": false
"decorations": false,
"center": true
}
],
"security": {
@@ -26,14 +27,22 @@
},
"bundle": {
"active": true,
"targets": ["appimage"],
"targets": ["nsis"],
"icon": [
"icons/32x32.png",
"icons/128x128.png",
"icons/128x128@2x.png",
"icons/icon.icns",
"icons/icon.ico"
]
"icons/icon.ico",
"icons/icon.png"
],
"externalBin": [],
"windows": {
"nsis": {
"installerIcon": "icons/icon.ico",
"installMode": "currentUser"
}
}
},
"plugins": {
"shell": {
+3
View File
@@ -9,5 +9,8 @@
"devtools": true
}
]
},
"bundle": {
"externalBin": []
}
}
+13
View File
@@ -0,0 +1,13 @@
{
"bundle": {
"targets": ["appimage", "deb"],
"externalBin": [
"binaries/suwayomi-launcher-linux"
],
"resources": {
"binaries/suwayomi-bundle/bin/Suwayomi-Server.jar": "Suwayomi-Server.jar",
"binaries/suwayomi-bundle/bin/catch_abort.so": "catch_abort.so",
"binaries/suwayomi-bundle/jre": "jre"
}
}
}
+25
View File
@@ -0,0 +1,25 @@
{
"app": {
"windows": [
{
"decorations": true,
"titleBarStyle": "Overlay",
"hiddenTitle": true
}
]
},
"bundle": {
"targets": ["dmg"],
"externalBin": [
"binaries/suwayomi-server"
],
"resources": {
"binaries/suwayomi-bundle": "suwayomi-bundle"
},
"macOS": {
"minimumSystemVersion": "11.0",
"exceptionDomain": "localhost",
"frameworks": []
}
}
}
+8
View File
@@ -0,0 +1,8 @@
{
"bundle": {
"resources": [
"binaries/suwayomi-bundle/bin/Suwayomi-Server.jar",
"binaries/suwayomi-bundle/jre/**/*"
]
}
}
-12
View File
@@ -1,12 +0,0 @@
.root {
display: flex;
flex-direction: column;
height: 100%;
overflow: hidden;
}
.content {
flex: 1;
overflow: hidden;
min-height: 0;
}
-54
View File
@@ -1,54 +0,0 @@
import { useEffect } from "react";
import { invoke } from "@tauri-apps/api/core";
import { listen } from "@tauri-apps/api/event";
import "./styles/global.css";
import { useStore } from "./store";
import Layout from "./components/layout/Layout";
import Reader from "./components/pages/Reader";
import Settings from "./components/settings/Settings";
import TitleBar from "./components/layout/TitleBar";
import s from "./App.module.css";
export default function App() {
const activeChapter = useStore((s) => s.activeChapter);
const settingsOpen = useStore((s) => s.settingsOpen);
const settings = useStore((s) => s.settings);
const setActiveDownloads = useStore((s) => s.setActiveDownloads);
useEffect(() => {
document.documentElement.style.zoom = `${settings.uiScale}%`;
}, [settings.uiScale]);
useEffect(() => {
const prevent = (e: MouseEvent) => e.preventDefault();
document.addEventListener("contextmenu", prevent);
return () => document.removeEventListener("contextmenu", prevent);
}, []);
useEffect(() => {
if (!settings.autoStartServer) return;
invoke("spawn_server", { binary: settings.serverBinary }).catch((err) =>
console.warn("Could not start server:", err)
);
return () => { invoke("kill_server").catch(() => {}); };
}, [settings.autoStartServer, settings.serverBinary]);
// Global Tauri download-progress listener — no polling, always current
useEffect(() => {
type DlPayload = { chapterId: number; mangaId: number; progress: number }[];
const unsub = listen<DlPayload>("download-progress", (e) => {
setActiveDownloads(e.payload);
});
return () => { unsub.then((fn) => fn()); };
}, [setActiveDownloads]);
return (
<div className={s.root}>
{!activeChapter && <TitleBar />}
<div className={s.content}>
{activeChapter ? <Reader /> : <Layout />}
</div>
{settingsOpen && <Settings />}
</div>
);
}
+203
View File
@@ -0,0 +1,203 @@
@import '$lib/components/settings/Settings.css';
@import '$lib/styles/themes.css';
: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;
}
*, *::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);
}
+5
View File
@@ -0,0 +1,5 @@
declare global {
namespace App {}
const __APP_VERSION__: string
}
export {}
+12
View File
@@ -0,0 +1,12 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" href="%sveltekit.assets%/favicon.svg" />
%sveltekit.head%
</head>
<body data-theme="dark">
<div id="svelte">%sveltekit.body%</div>
</body>
</html>
Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

-27
View File
@@ -1,27 +0,0 @@
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
width="512.000000pt" height="512.000000pt" viewBox="0 0 500.000000 500.000000"
preserveAspectRatio="xMidYMid meet">
<g transform="translate(0.000000,500.000000) scale(0.050000,-0.050000)"
fill="#2d7a5f" stroke="none">
<path d="M4908 6615 c-597 -629 -935 -1264 -934 -1755 0 -508 195 -778 790
-1098 l194 -104 22 -114 c33 -164 76 -271 140 -347 80 -94 106 -62 77 94 -47
255 43 443 294 613 797 539 714 1392 -250 2551 -230 278 -224 275 -333 160z
m52 -945 c0 -472 -8 -850 -17 -850 -36 0 -693 703 -692 740 2 48 167 321 309
509 175 233 359 451 380 451 13 0 20 -304 20 -850z m183 775 c120 -126 357
-447 356 -482 0 -18 -47 -83 -105 -144 -57 -61 -151 -163 -208 -225 -151 -166
-146 -180 -146 406 0 286 7 520 16 520 9 0 48 -34 87 -75z m541 -750 c86 -150
196 -391 196 -430 0 -8 -25 -42 -55 -75 -31 -33 -149 -163 -264 -290 -339
-374 -480 -520 -501 -520 -13 0 -20 167 -20 465 l1 465 151 170 c83 94 193
217 244 275 122 138 137 135 248 -60z m-1098 -633 l374 -378 0 -462 c0 -254
-5 -462 -11 -462 -6 0 -55 23 -110 50 l-99 51 0 208 c0 259 -11 288 -176 457
-196 200 -188 199 -326 62 l-117 -116 -35 79 c-71 161 -39 569 64 816 42 100
20 116 436 -305z m1349 23 c109 -390 -87 -848 -475 -1111 -207 -139 -305 -262
-338 -420 -7 -31 -21 -56 -32 -56 -36 -1 -46 86 -48 405 l-2 313 123 137 c68
75 214 236 325 357 454 493 421 466 447 375z m-1292 -785 c12 -29 15 -118 7
-215 l-14 -166 -73 44 c-174 104 -403 341 -403 416 0 15 47 70 105 123 l104
98 127 -125 c70 -69 136 -147 147 -175z"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

@@ -1,83 +0,0 @@
.menu {
position: fixed;
z-index: 200;
background: var(--bg-raised);
border: 1px solid var(--border-base);
border-radius: var(--radius-lg);
padding: var(--sp-1);
min-width: 190px;
box-shadow:
0 0 0 1px rgba(0,0,0,0.08),
0 4px 12px rgba(0,0,0,0.35),
0 16px 40px rgba(0,0,0,0.25);
animation: scaleIn 0.1s ease both;
transform-origin: top left;
}
.item {
display: flex;
align-items: center;
gap: var(--sp-2);
width: 100%;
padding: 5px var(--sp-2);
border-radius: var(--radius-md);
font-size: var(--text-sm);
color: var(--text-secondary);
text-align: left;
cursor: pointer;
transition: background var(--t-fast), color var(--t-fast);
border: none;
background: none;
outline: none;
}
.item:hover:not(:disabled),
.itemFocused:not(:disabled) {
background: var(--bg-overlay);
color: var(--text-primary);
}
/* Icon area — fixed-width column so labels align */
.itemIconWrap {
display: flex;
align-items: center;
justify-content: center;
width: 18px;
height: 18px;
flex-shrink: 0;
color: var(--text-faint);
transition: color var(--t-fast);
border-radius: var(--radius-sm);
}
.item:hover .itemIconWrap,
.itemFocused .itemIconWrap {
color: var(--text-muted);
}
.itemLabel {
flex: 1;
line-height: 1.3;
}
/* Danger variant */
.itemDanger { color: var(--color-error); }
.itemDanger:hover:not(:disabled),
.itemDanger.itemFocused:not(:disabled) {
background: var(--color-error-bg);
color: var(--color-error);
}
.itemIconDanger { color: var(--color-error) !important; opacity: 0.7; }
/* Disabled */
.itemDisabled {
opacity: 0.3;
cursor: default;
pointer-events: none;
}
.separator {
height: 1px;
background: var(--border-dim);
margin: 3px var(--sp-1);
}
-133
View File
@@ -1,133 +0,0 @@
import { useEffect, useRef, useCallback, useState } from "react";
import { createPortal } from "react-dom";
import s from "./ContextMenu.module.css";
export interface ContextMenuItem {
label: string;
icon?: React.ReactNode;
onClick: () => void;
danger?: boolean;
disabled?: boolean;
separator?: never;
}
export interface ContextMenuSeparator {
separator: true;
label?: never;
icon?: never;
onClick?: never;
danger?: never;
disabled?: never;
}
export type ContextMenuEntry = ContextMenuItem | ContextMenuSeparator;
interface Props {
x: number;
y: number;
items: ContextMenuEntry[];
onClose: () => void;
}
export default function ContextMenu({ x, y, items, onClose }: Props) {
const menuRef = useRef<HTMLDivElement>(null);
const [focused, setFocused] = useState<number>(-1);
// Build list of actionable (non-separator, non-disabled) indices for keyboard nav
const actionable = items
.map((_, i) => i)
.filter((i) => !("separator" in items[i]) && !(items[i] as ContextMenuItem).disabled);
useEffect(() => {
function onDown(e: MouseEvent) {
if (menuRef.current && !menuRef.current.contains(e.target as Node)) onClose();
}
function onKey(e: KeyboardEvent) {
if (e.key === "Escape") { e.stopPropagation(); onClose(); return; }
if (e.key === "ArrowDown") {
e.preventDefault();
setFocused((prev) => {
const cur = actionable.indexOf(prev);
return actionable[(cur + 1) % actionable.length] ?? actionable[0];
});
return;
}
if (e.key === "ArrowUp") {
e.preventDefault();
setFocused((prev) => {
const cur = actionable.indexOf(prev);
return actionable[(cur - 1 + actionable.length) % actionable.length] ?? actionable[0];
});
return;
}
if (e.key === "Enter" && focused >= 0) {
e.preventDefault();
const item = items[focused] as ContextMenuItem;
if (item && !item.disabled) { item.onClick(); onClose(); }
return;
}
}
document.addEventListener("mousedown", onDown, true);
document.addEventListener("keydown", onKey, true);
return () => {
document.removeEventListener("mousedown", onDown, true);
document.removeEventListener("keydown", onKey, true);
};
}, [onClose, focused, actionable, items]);
// Focus first item on open
useEffect(() => {
if (actionable.length) setFocused(actionable[0]);
}, []);
const getPosition = useCallback(() => {
const zoom = parseFloat(document.documentElement.style.zoom || "1") / 100 || 1;
const scaledX = x / zoom;
const scaledY = y / zoom;
const menuW = 200;
const menuH = items.length * 34;
const vw = window.innerWidth / zoom;
const vh = window.innerHeight / zoom;
const left = scaledX + menuW > vw ? scaledX - menuW : scaledX;
const top = scaledY + menuH > vh ? scaledY - menuH : scaledY;
return { left: Math.max(4, left), top: Math.max(4, top) };
}, [x, y, items.length]);
return createPortal(
<div
ref={menuRef}
className={s.menu}
style={getPosition()}
onContextMenu={(e) => e.preventDefault()}
>
{items.map((item, i) => {
if ("separator" in item && item.separator) {
return <div key={i} className={s.separator} />;
}
const mi = item as ContextMenuItem;
const isFocused = focused === i;
return (
<button
key={i}
className={[
s.item,
mi.danger ? s.itemDanger : "",
mi.disabled ? s.itemDisabled : "",
isFocused ? s.itemFocused : "",
].filter(Boolean).join(" ")}
onClick={() => { if (!mi.disabled) { mi.onClick(); onClose(); } }}
onMouseEnter={() => !mi.disabled && setFocused(i)}
onMouseLeave={() => setFocused(-1)}
disabled={mi.disabled}
>
<span className={[s.itemIconWrap, mi.danger ? s.itemIconDanger : ""].filter(Boolean).join(" ")}>
{mi.icon ?? null}
</span>
<span className={s.itemLabel}>{mi.label}</span>
</button>
);
})}
</div>,
document.body
);
}
@@ -1,200 +0,0 @@
.root {
padding: var(--sp-6);
overflow-y: auto;
height: 100%;
animation: fadeIn 0.14s ease both;
}
.header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: var(--sp-5);
}
.heading {
font-family: var(--font-ui);
font-size: var(--text-xs);
font-weight: var(--weight-normal);
color: var(--text-faint);
letter-spacing: var(--tracking-wider);
text-transform: uppercase;
}
.headerActions { display: flex; gap: var(--sp-2); }
.iconBtn {
display: flex;
align-items: center;
justify-content: center;
width: 28px;
height: 28px;
border-radius: var(--radius-md);
border: 1px solid var(--border-dim);
color: var(--text-muted);
transition: color var(--t-base), border-color var(--t-base), background var(--t-base);
}
.iconBtn:hover { color: var(--text-secondary); border-color: var(--border-strong); background: var(--bg-raised); }
.iconBtn:disabled { opacity: 0.3; cursor: default; }
.statusBar {
display: flex;
align-items: center;
gap: var(--sp-3);
padding: var(--sp-3);
background: var(--bg-raised);
border: 1px solid var(--border-dim);
border-radius: var(--radius-md);
margin-bottom: var(--sp-4);
}
.statusDot {
width: 6px;
height: 6px;
border-radius: 50%;
background: var(--text-faint);
flex-shrink: 0;
}
.statusDotActive {
background: var(--accent);
animation: pulse 1.6s ease infinite;
}
.statusText {
font-family: var(--font-ui);
font-size: var(--text-xs);
color: var(--text-muted);
flex: 1;
letter-spacing: var(--tracking-wide);
}
.statusCount {
font-family: var(--font-ui);
font-size: var(--text-xs);
color: var(--text-faint);
letter-spacing: var(--tracking-wide);
}
.list { display: flex; flex-direction: column; gap: var(--sp-2); }
.row {
display: flex;
align-items: center;
gap: var(--sp-3);
padding: var(--sp-3);
background: var(--bg-raised);
border: 1px solid var(--border-dim);
border-radius: var(--radius-md);
transition: border-color var(--t-fast);
}
.rowActive { border-color: var(--accent-dim); }
/* Thumbnail */
.thumb {
width: 36px;
height: 54px;
border-radius: var(--radius-sm);
overflow: hidden;
background: var(--bg-overlay);
flex-shrink: 0;
border: 1px solid var(--border-dim);
}
.thumbImg {
width: 100%;
height: 100%;
object-fit: cover;
}
/* Info block */
.info {
flex: 1;
display: flex;
flex-direction: column;
gap: 3px;
overflow: hidden;
min-width: 0;
}
.mangaTitle {
font-size: var(--text-sm);
font-weight: var(--weight-medium);
color: var(--text-secondary);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.chapterName {
font-size: var(--text-xs);
color: var(--text-muted);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.pagesLabel {
font-family: var(--font-ui);
font-size: var(--text-2xs);
color: var(--text-faint);
letter-spacing: var(--tracking-wide);
}
.progressWrap {
height: 2px;
background: var(--border-base);
border-radius: var(--radius-full);
overflow: hidden;
margin-top: 4px;
}
.progressBar {
height: 100%;
background: var(--accent);
border-radius: var(--radius-full);
transition: width 0.4s ease;
}
/* Right side */
.rowRight {
display: flex;
flex-direction: column;
align-items: flex-end;
gap: var(--sp-1);
flex-shrink: 0;
}
.stateLabel {
font-family: var(--font-ui);
font-size: var(--text-2xs);
color: var(--text-faint);
letter-spacing: var(--tracking-wider);
text-transform: uppercase;
}
.removeBtn {
display: flex;
align-items: center;
justify-content: center;
width: 20px;
height: 20px;
border-radius: var(--radius-sm);
color: var(--text-faint);
transition: color var(--t-base), background var(--t-base);
}
.removeBtn:hover { color: var(--color-error); background: var(--color-error-bg); }
.empty {
display: flex;
align-items: center;
justify-content: center;
height: 160px;
color: var(--text-faint);
font-family: var(--font-ui);
font-size: var(--text-xs);
letter-spacing: var(--tracking-wide);
}
-152
View File
@@ -1,152 +0,0 @@
import { useEffect, useState } from "react";
import { Play, Pause, Trash, CircleNotch, X } from "@phosphor-icons/react";
import { gql, thumbUrl } from "../../lib/client";
import {
GET_DOWNLOAD_STATUS, START_DOWNLOADER, STOP_DOWNLOADER,
CLEAR_DOWNLOADER, DEQUEUE_DOWNLOAD,
} from "../../lib/queries";
import { useStore } from "../../store";
import type { DownloadStatus } from "../../lib/types";
import s from "./DownloadQueue.module.css";
export default function DownloadQueue() {
const [status, setStatus] = useState<DownloadStatus | null>(null);
const [loading, setLoading] = useState(true);
const setActiveDownloads = useStore((s) => s.setActiveDownloads);
async function poll() {
gql<{ downloadStatus: DownloadStatus }>(GET_DOWNLOAD_STATUS)
.then((d) => {
setStatus(d.downloadStatus);
setActiveDownloads(
d.downloadStatus.queue.map((item) => ({
chapterId: item.chapter.id,
mangaId: item.chapter.mangaId,
progress: item.progress,
}))
);
})
.catch(console.error)
.finally(() => setLoading(false));
}
useEffect(() => {
poll();
const id = setInterval(poll, 1500);
return () => clearInterval(id);
}, []);
async function start() { await gql(START_DOWNLOADER).catch(console.error); poll(); }
async function stop() { await gql(STOP_DOWNLOADER).catch(console.error); poll(); }
async function clear() { await gql(CLEAR_DOWNLOADER).catch(console.error); poll(); }
async function dequeue(chapterId: number) {
await gql(DEQUEUE_DOWNLOAD, { chapterId }).catch(console.error);
poll();
}
const queue = status?.queue ?? [];
const isRunning = status?.state === "STARTED";
function pagesDownloaded(progress: number, pageCount: number): number {
return Math.round(progress * pageCount);
}
return (
<div className={s.root}>
<div className={s.header}>
<h1 className={s.heading}>Downloads</h1>
<div className={s.headerActions}>
{isRunning ? (
<button className={s.iconBtn} onClick={stop} title="Pause">
<Pause size={14} weight="fill" />
</button>
) : (
<button className={s.iconBtn} onClick={start} disabled={queue.length === 0} title="Resume">
<Play size={14} weight="fill" />
</button>
)}
<button className={s.iconBtn} onClick={clear} disabled={queue.length === 0} title="Clear queue">
<Trash size={14} weight="regular" />
</button>
</div>
</div>
<div className={s.statusBar}>
<div className={[s.statusDot, isRunning ? s.statusDotActive : ""].join(" ").trim()} />
<span className={s.statusText}>{isRunning ? "Downloading" : "Paused"}</span>
<span className={s.statusCount}>{queue.length} queued</span>
</div>
{loading ? (
<div className={s.empty}>
<CircleNotch size={16} weight="light" className="anim-spin" style={{ color: "var(--text-faint)" }} />
</div>
) : queue.length === 0 ? (
<div className={s.empty}>Queue is empty.</div>
) : (
<div className={s.list}>
{queue.map((item, i) => {
const isActive = i === 0 && isRunning;
const pages = item.chapter.pageCount ?? 0;
const done = pagesDownloaded(item.progress, pages);
const manga = item.chapter.manga;
return (
<div
key={item.chapter.id}
className={[s.row, isActive ? s.rowActive : ""].join(" ").trim()}
>
{manga?.thumbnailUrl && (
<div className={s.thumb}>
<img
src={thumbUrl(manga.thumbnailUrl)}
alt={manga.title}
className={s.thumbImg}
loading="lazy"
decoding="async"
/>
</div>
)}
<div className={s.info}>
{manga?.title && (
<span className={s.mangaTitle}>{manga.title}</span>
)}
<span className={s.chapterName}>{item.chapter.name}</span>
{pages > 0 && (
<span className={s.pagesLabel}>
{isActive ? `${done} / ${pages} pages` : `${pages} pages`}
</span>
)}
{isActive && (
<div className={s.progressWrap}>
<div
className={s.progressBar}
style={{ width: `${Math.round(item.progress * 100)}%` }}
/>
</div>
)}
</div>
<div className={s.rowRight}>
<span className={s.stateLabel}>{item.state}</span>
{!isActive && (
<button
className={s.removeBtn}
onClick={() => dequeue(item.chapter.id)}
title="Remove from queue"
>
<X size={12} weight="light" />
</button>
)}
</div>
</div>
);
})}
</div>
)}
</div>
);
}
@@ -1,278 +0,0 @@
.root {
display: flex; flex-direction: column; height: 100%;
overflow: hidden; animation: fadeIn 0.14s ease both;
}
.header {
display: flex; align-items: center; justify-content: space-between;
padding: var(--sp-5) var(--sp-6) var(--sp-3); flex-shrink: 0;
}
.heading {
font-family: var(--font-ui); font-size: var(--text-xs); font-weight: var(--weight-normal);
color: var(--text-faint); letter-spacing: var(--tracking-wider); text-transform: uppercase;
}
.headerActions { display: flex; gap: var(--sp-1); }
.iconBtn {
display: flex; align-items: center; justify-content: center;
width: 28px; height: 28px; border-radius: var(--radius-md);
color: var(--text-muted); transition: color var(--t-base), background var(--t-base);
}
.iconBtn:hover:not(:disabled) { color: var(--text-primary); background: var(--bg-raised); }
.iconBtn:disabled { opacity: 0.4; }
.iconBtnActive { color: var(--accent-fg); background: var(--accent-muted); }
.iconBtnActive:hover:not(:disabled) { color: var(--accent-fg); background: var(--accent-muted); filter: brightness(1.1); }
.externalPanel {
display: flex; flex-direction: column; gap: var(--sp-2);
padding: 0 var(--sp-6) var(--sp-3); flex-shrink: 0;
animation: fadeIn 0.1s ease both;
}
.externalHeader {
display: flex; align-items: center; justify-content: space-between;
}
.externalTitle {
font-family: var(--font-ui); font-size: var(--text-xs);
color: var(--text-muted); letter-spacing: var(--tracking-wide);
}
.externalRow {
display: flex; gap: var(--sp-2);
}
.externalInput {
flex: 1; background: var(--bg-raised); border: 1px solid var(--border-strong);
border-radius: var(--radius-md); padding: 6px var(--sp-3);
color: var(--text-primary); font-size: var(--text-sm); outline: none;
transition: border-color var(--t-base);
}
.externalInput:focus { border-color: var(--border-focus); }
.externalInput:disabled { opacity: 0.5; }
.externalInputError { border-color: var(--color-error) !important; }
.externalError {
font-family: var(--font-ui); font-size: var(--text-xs);
color: var(--color-error); letter-spacing: var(--tracking-wide);
padding: 0 2px;
}
.installBtn {
display: flex; align-items: center; gap: var(--sp-1);
font-family: var(--font-ui); font-size: var(--text-xs); letter-spacing: var(--tracking-wide);
padding: 6px 14px; border-radius: var(--radius-md);
background: var(--accent-muted); color: var(--accent-fg);
border: 1px solid var(--accent-dim); cursor: pointer; flex-shrink: 0;
transition: filter var(--t-base), opacity var(--t-base);
white-space: nowrap;
}
.installBtn:hover:not(:disabled) { filter: brightness(1.1); }
.installBtn:disabled { opacity: 0.5; cursor: default; }
.installBtnSuccess {
background: var(--color-success, #2d6a3f); border-color: var(--color-success, #2d6a3f);
color: #fff;
}
.controls {
display: flex; align-items: center; justify-content: space-between;
padding: 0 var(--sp-6) var(--sp-3); gap: var(--sp-3); flex-shrink: 0;
}
.tabs { display: flex; gap: 2px; }
.tab {
font-family: var(--font-ui); font-size: var(--text-xs); letter-spacing: var(--tracking-wide);
padding: 4px 10px; border-radius: var(--radius-md); border: none;
background: none; color: var(--text-muted); cursor: pointer;
transition: background var(--t-base), color var(--t-base);
}
.tab:hover { background: var(--bg-raised); color: var(--text-secondary); }
.tabActive { background: var(--accent-muted); color: var(--accent-fg); }
.tabActive:hover { background: var(--accent-muted); color: var(--accent-fg); }
.searchWrap { position: relative; display: flex; align-items: center; }
.searchIcon { position: absolute; left: 9px; color: var(--text-faint); pointer-events: none; }
.search {
background: var(--bg-raised); border: 1px solid var(--border-dim);
border-radius: var(--radius-md); padding: 5px 10px 5px 26px;
color: var(--text-primary); font-size: var(--text-sm); width: 160px; outline: none;
transition: border-color var(--t-base);
}
.search::placeholder { color: var(--text-faint); }
.search:focus { border-color: var(--border-strong); }
.list { flex: 1; overflow-y: auto; padding: 0 var(--sp-4) var(--sp-4); display: flex; flex-direction: column; gap: 1px; }
.group { display: flex; flex-direction: column; }
.row {
display: flex; align-items: center; gap: var(--sp-3);
padding: 8px var(--sp-3); border-radius: var(--radius-md);
border: 1px solid transparent;
transition: background var(--t-fast), border-color var(--t-fast);
}
.row:hover { background: var(--bg-raised); border-color: var(--border-dim); }
.icon {
width: 32px; height: 32px; border-radius: var(--radius-md);
object-fit: cover; flex-shrink: 0; background: var(--bg-raised);
}
.info { flex: 1; display: flex; flex-direction: column; gap: 2px; overflow: hidden; min-width: 0; }
.name {
font-size: var(--text-base); font-weight: var(--weight-medium);
color: var(--text-secondary); white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.meta {
display: flex; align-items: center; gap: var(--sp-2);
font-family: var(--font-ui); font-size: var(--text-2xs); color: var(--text-faint);
letter-spacing: var(--tracking-wide);
}
.langTag {
background: var(--bg-overlay); border: 1px solid var(--border-dim);
border-radius: var(--radius-sm); padding: 1px 5px;
font-family: var(--font-ui); font-size: var(--text-2xs);
color: var(--text-muted); letter-spacing: var(--tracking-wider);
}
.nsfwTag {
background: transparent; border: 1px solid var(--color-error);
border-radius: var(--radius-sm); padding: 1px 5px;
font-family: var(--font-ui); font-size: var(--text-2xs);
color: var(--color-error); letter-spacing: var(--tracking-wider);
}
.updateBadge {
font-family: var(--font-ui); font-size: var(--text-2xs); letter-spacing: var(--tracking-wide);
background: var(--accent-muted); color: var(--accent-fg);
border: 1px solid var(--accent-dim); border-radius: var(--radius-sm);
padding: 2px 6px; flex-shrink: 0;
}
.updateBadgeSmall {
font-family: var(--font-ui); font-size: var(--text-2xs);
color: var(--accent-fg); flex-shrink: 0;
}
.rowActions { display: flex; gap: var(--sp-1); flex-shrink: 0; }
.actionBtn {
font-family: var(--font-ui); font-size: var(--text-xs); letter-spacing: var(--tracking-wide);
padding: 4px 10px; border-radius: var(--radius-md);
background: var(--accent-muted); color: var(--accent-fg);
border: 1px solid var(--accent-dim); cursor: pointer; flex-shrink: 0;
transition: filter var(--t-base);
}
.actionBtn:hover { filter: brightness(1.1); }
.actionBtnDim {
font-family: var(--font-ui); font-size: var(--text-xs); letter-spacing: var(--tracking-wide);
padding: 4px 10px; border-radius: var(--radius-md);
background: none; color: var(--text-faint);
border: 1px solid var(--border-dim); cursor: pointer; flex-shrink: 0;
transition: color var(--t-base), border-color var(--t-base);
}
.actionBtnDim:hover { color: var(--color-error); border-color: var(--color-error); }
.expandBtn {
display: flex; align-items: center; gap: 3px;
padding: 4px 6px; border-radius: var(--radius-sm);
color: var(--text-faint); flex-shrink: 0;
transition: color var(--t-base), background var(--t-base);
}
.expandBtn:hover { color: var(--text-muted); background: var(--bg-overlay); }
.expandCount {
font-family: var(--font-ui); font-size: var(--text-2xs);
letter-spacing: var(--tracking-wide);
}
.variants {
display: flex; flex-direction: column; gap: 1px;
margin: 1px 0 2px calc(32px + var(--sp-3) + var(--sp-3));
padding-left: var(--sp-3);
border-left: 1px solid var(--border-dim);
animation: fadeIn 0.1s ease both;
}
.variantRow {
display: flex; align-items: center; gap: var(--sp-2);
padding: 5px var(--sp-2); border-radius: var(--radius-md);
transition: background var(--t-fast);
}
.variantRow:hover { background: var(--bg-raised); }
.variantName {
flex: 1; font-size: var(--text-sm); color: var(--text-muted);
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.variantVersion {
font-family: var(--font-ui); font-size: var(--text-2xs);
color: var(--text-faint); letter-spacing: var(--tracking-wide); flex-shrink: 0;
}
.variantActions { flex-shrink: 0; }
.empty {
display: flex; align-items: center; justify-content: center;
flex: 1; color: var(--text-faint);
font-family: var(--font-ui); font-size: var(--text-xs); letter-spacing: var(--tracking-wide);
}
/* ── Panel shared styles ── */
.externalPanel {
display: flex; flex-direction: column; gap: var(--sp-2);
padding: 0 var(--sp-6) var(--sp-3); flex-shrink: 0;
animation: fadeIn 0.1s ease both;
}
.panelHeader {
display: flex; align-items: center; justify-content: space-between;
}
.panelTitle {
font-family: var(--font-ui); font-size: var(--text-xs);
color: var(--text-muted); letter-spacing: var(--tracking-wide);
}
.panelError {
font-family: var(--font-ui); font-size: var(--text-xs);
color: var(--color-error); letter-spacing: var(--tracking-wide);
padding: 0 2px;
}
.externalRow { display: flex; gap: var(--sp-2); }
.externalInput {
flex: 1; background: var(--bg-raised); border: 1px solid var(--border-strong);
border-radius: var(--radius-md); padding: 6px var(--sp-3);
color: var(--text-primary); font-size: var(--text-sm); outline: none;
transition: border-color var(--t-base);
}
.externalInput:focus { border-color: var(--border-focus); }
.externalInput:disabled { opacity: 0.5; }
.externalInputError { border-color: var(--color-error) !important; }
.installBtn {
display: flex; align-items: center; gap: var(--sp-1);
font-family: var(--font-ui); font-size: var(--text-xs); letter-spacing: var(--tracking-wide);
padding: 6px 14px; border-radius: var(--radius-md);
background: var(--accent-muted); color: var(--accent-fg);
border: 1px solid var(--accent-dim); cursor: pointer; flex-shrink: 0;
transition: filter var(--t-base), opacity var(--t-base);
white-space: nowrap;
}
.installBtn:hover:not(:disabled) { filter: brightness(1.1); }
.installBtn:disabled { opacity: 0.5; cursor: default; }
.installBtnSuccess {
background: color-mix(in srgb, var(--accent-fg) 20%, transparent);
border-color: var(--accent-fg); color: var(--accent-fg);
}
/* ── Repo list ── */
.repoLoading {
display: flex; align-items: center; justify-content: center;
padding: var(--sp-3);
}
.repoEmpty {
font-family: var(--font-ui); font-size: var(--text-xs);
color: var(--text-faint); letter-spacing: var(--tracking-wide);
padding: var(--sp-1) 2px;
}
.repoList {
display: flex; flex-direction: column; gap: 2px;
}
.repoRow {
display: flex; align-items: center; gap: var(--sp-2);
padding: 5px var(--sp-2); border-radius: var(--radius-md);
background: var(--bg-raised); border: 1px solid var(--border-dim);
}
.repoUrl {
flex: 1; font-family: var(--font-mono, monospace); font-size: var(--text-2xs);
color: var(--text-muted); white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
letter-spacing: 0;
}
.repoRemoveBtn {
display: flex; align-items: center; justify-content: center;
width: 20px; height: 20px; border-radius: var(--radius-sm);
color: var(--text-faint); flex-shrink: 0;
transition: color var(--t-base), background var(--t-base);
}
.repoRemoveBtn:hover:not(:disabled) { color: var(--color-error); background: var(--bg-overlay); }
.repoRemoveBtn:disabled { opacity: 0.4; }
-407
View File
@@ -1,407 +0,0 @@
import { useEffect, useState, useMemo } from "react";
import { MagnifyingGlass, ArrowsClockwise, Plus, CircleNotch, CaretRight, CaretDown, X, Check, GitBranch } from "@phosphor-icons/react";
import { gql, thumbUrl } from "../../lib/client";
import {
GET_EXTENSIONS, FETCH_EXTENSIONS, UPDATE_EXTENSION, INSTALL_EXTERNAL_EXTENSION,
GET_SETTINGS, SET_EXTENSION_REPOS,
} from "../../lib/queries";
import { useStore } from "../../store";
import type { Extension } from "../../lib/types";
import s from "./ExtensionList.module.css";
type Filter = "installed" | "available" | "updates" | "all";
type Panel = null | "apk" | "repos";
function baseName(name: string): string {
return name.replace(/\s*\([A-Z0-9-]{2,10}\)\s*$/, "").trim();
}
interface ExtGroup {
base: string;
primary: Extension;
variants: Extension[];
}
export default function ExtensionList() {
const [extensions, setExtensions] = useState<Extension[]>([]);
const [loading, setLoading] = useState(true);
const [refreshing, setRefreshing] = useState(false);
const [filter, setFilter] = useState<Filter>("installed");
const [search, setSearch] = useState("");
const [working, setWorking] = useState<Set<string>>(new Set());
const [expanded, setExpanded] = useState<Set<string>>(new Set());
const [panel, setPanel] = useState<Panel>(null);
// APK install state
const [externalUrl, setExternalUrl] = useState("");
const [installing, setInstalling] = useState(false);
const [installError, setInstallError] = useState<string | null>(null);
const [installSuccess, setInstallSuccess] = useState(false);
// Repo management state
const [repos, setRepos] = useState<string[]>([]);
const [reposLoading, setReposLoading] = useState(false);
const [newRepoUrl, setNewRepoUrl] = useState("");
const [repoError, setRepoError] = useState<string | null>(null);
const [savingRepos, setSavingRepos] = useState(false);
const preferredLang = useStore((s) => s.settings.preferredExtensionLang);
async function load() {
return gql<{ extensions: { nodes: Extension[] } }>(GET_EXTENSIONS)
.then((d) => setExtensions(d.extensions.nodes))
.catch(console.error);
}
async function fetchFromRepo() {
setRefreshing(true);
return gql<{ fetchExtensions: { extensions: Extension[] } }>(FETCH_EXTENSIONS)
.then((d) => setExtensions(d.fetchExtensions.extensions))
.catch(console.error)
.finally(() => setRefreshing(false));
}
async function loadRepos() {
setReposLoading(true);
try {
const d = await gql<{ settings: { extensionRepos: string[] } }>(GET_SETTINGS);
setRepos(d.settings.extensionRepos ?? []);
} catch (e) {
console.error(e);
} finally {
setReposLoading(false);
}
}
async function saveRepos(updated: string[]) {
setSavingRepos(true);
try {
const d = await gql<{ setSettings: { settings: { extensionRepos: string[] } } }>(
SET_EXTENSION_REPOS, { repos: updated }
);
setRepos(d.setSettings.settings.extensionRepos);
} catch (e: unknown) {
setRepoError(e instanceof Error ? e.message : "Failed to save");
} finally {
setSavingRepos(false);
}
}
function addRepo() {
const url = newRepoUrl.trim();
if (!url) return;
if (!url.startsWith("http://") && !url.startsWith("https://")) {
setRepoError("URL must start with http:// or https://");
return;
}
if (repos.includes(url)) {
setRepoError("Repo already added");
return;
}
setRepoError(null);
setNewRepoUrl("");
saveRepos([...repos, url]);
}
function removeRepo(url: string) {
saveRepos(repos.filter((r) => r !== url));
}
const mutate = async (fn: () => Promise<unknown>, pkgName: string) => {
setWorking((p) => new Set(p).add(pkgName));
await fn().catch(console.error);
await load();
setWorking((p) => { const n = new Set(p); n.delete(pkgName); return n; });
};
async function installExternal() {
const url = externalUrl.trim();
if (!url) return;
if (!url.startsWith("http://") && !url.startsWith("https://")) {
setInstallError("URL must start with http:// or https://");
return;
}
if (!url.endsWith(".apk")) {
setInstallError("URL must point to an .apk file");
return;
}
setInstalling(true);
setInstallError(null);
setInstallSuccess(false);
try {
await gql(INSTALL_EXTERNAL_EXTENSION, { url });
setInstallSuccess(true);
setExternalUrl("");
await load();
setTimeout(() => {
setPanel(null);
setInstallSuccess(false);
}, 1500);
} catch (e: unknown) {
setInstallError(e instanceof Error ? e.message : "Install failed");
} finally {
setInstalling(false);
}
}
function openPanel(p: Panel) {
if (panel === p) {
setPanel(null);
return;
}
setPanel(p);
setInstallError(null);
setInstallSuccess(false);
setExternalUrl("");
setRepoError(null);
setNewRepoUrl("");
if (p === "repos") loadRepos();
}
useEffect(() => {
fetchFromRepo().finally(() => setLoading(false));
}, []);
const filtered = extensions.filter((e) => {
const q = search.toLowerCase();
const matchSearch = e.name.toLowerCase().includes(q) || e.lang.toLowerCase().includes(q);
const matchFilter =
filter === "installed" ? e.isInstalled :
filter === "available" ? !e.isInstalled :
filter === "updates" ? e.hasUpdate : true;
return matchSearch && matchFilter;
});
const groups = useMemo<ExtGroup[]>(() => {
const map = new Map<string, Extension[]>();
for (const ext of filtered) {
const key = baseName(ext.name);
if (!map.has(key)) map.set(key, []);
map.get(key)!.push(ext);
}
return Array.from(map.entries()).map(([base, all]) => {
const primary =
all.find((v) => v.lang === preferredLang) ??
all.find((v) => v.lang === "en") ??
all[0];
const variants = all.filter((v) => v.pkgName !== primary.pkgName);
return { base, primary, variants };
});
}, [filtered, preferredLang]);
const updateCount = extensions.filter((e) => e.hasUpdate).length;
const FILTERS: { id: Filter; label: string }[] = [
{ id: "installed", label: "Installed" },
{ id: "available", label: "Available" },
{ id: "updates", label: updateCount > 0 ? `Updates (${updateCount})` : "Updates" },
{ id: "all", label: "All" },
];
function toggleExpand(base: string) {
setExpanded((p) => {
const n = new Set(p);
n.has(base) ? n.delete(base) : n.add(base);
return n;
});
}
function renderActions(ext: Extension) {
if (working.has(ext.pkgName))
return <CircleNotch size={14} weight="light" className="anim-spin" style={{ color: "var(--text-faint)" }} />;
if (ext.hasUpdate) return (
<div className={s.rowActions}>
<button className={s.actionBtn} onClick={() => mutate(() => gql(UPDATE_EXTENSION, { id: ext.pkgName, update: true }), ext.pkgName)}>Update</button>
<button className={s.actionBtnDim} onClick={() => mutate(() => gql(UPDATE_EXTENSION, { id: ext.pkgName, uninstall: true }), ext.pkgName)}>Remove</button>
</div>
);
if (ext.isInstalled)
return <button className={s.actionBtnDim} onClick={() => mutate(() => gql(UPDATE_EXTENSION, { id: ext.pkgName, uninstall: true }), ext.pkgName)}>Remove</button>;
return <button className={s.actionBtn} onClick={() => mutate(() => gql(UPDATE_EXTENSION, { id: ext.pkgName, install: true }), ext.pkgName)}>Install</button>;
}
return (
<div className={s.root}>
<div className={s.header}>
<h1 className={s.heading}>Extensions</h1>
<div className={s.headerActions}>
<button
className={[s.iconBtn, panel === "repos" ? s.iconBtnActive : ""].join(" ").trim()}
onClick={() => openPanel("repos")} title="Manage repos">
<GitBranch size={14} weight="light" />
</button>
<button
className={[s.iconBtn, panel === "apk" ? s.iconBtnActive : ""].join(" ").trim()}
onClick={() => openPanel("apk")} title="Install from URL">
<Plus size={14} weight="light" />
</button>
<button className={s.iconBtn} onClick={fetchFromRepo} disabled={refreshing} title="Refresh repo">
<ArrowsClockwise size={14} weight="light" className={refreshing ? "anim-spin" : ""} />
</button>
</div>
</div>
{/* ── APK install panel ── */}
{panel === "apk" && (
<div className={s.externalPanel}>
<div className={s.panelHeader}>
<span className={s.panelTitle}>Install from APK URL</span>
<button className={s.iconBtn} onClick={() => setPanel(null)}><X size={14} weight="light" /></button>
</div>
<div className={s.externalRow}>
<input
className={[s.externalInput, installError ? s.externalInputError : ""].join(" ").trim()}
placeholder="https://example.com/extension.apk"
value={externalUrl}
onChange={(e) => { setExternalUrl(e.target.value); setInstallError(null); }}
onKeyDown={(e) => e.key === "Enter" && !installing && installExternal()}
autoFocus
disabled={installing}
/>
<button
className={[s.installBtn, installSuccess ? s.installBtnSuccess : ""].join(" ").trim()}
onClick={installExternal}
disabled={installing || !externalUrl.trim()}
>
{installing
? <CircleNotch size={13} weight="light" className="anim-spin" />
: installSuccess
? <><Check size={13} weight="bold" /> Done</>
: "Install"}
</button>
</div>
{installError && <div className={s.panelError}>{installError}</div>}
</div>
)}
{/* ── Repo management panel ── */}
{panel === "repos" && (
<div className={s.externalPanel}>
<div className={s.panelHeader}>
<span className={s.panelTitle}>Extension Repositories</span>
<button className={s.iconBtn} onClick={() => setPanel(null)}><X size={14} weight="light" /></button>
</div>
{reposLoading ? (
<div className={s.repoLoading}>
<CircleNotch size={14} weight="light" className="anim-spin" style={{ color: "var(--text-faint)" }} />
</div>
) : (
<>
{repos.length === 0 ? (
<div className={s.repoEmpty}>No repos configured.</div>
) : (
<div className={s.repoList}>
{repos.map((url) => (
<div key={url} className={s.repoRow}>
<span className={s.repoUrl}>{url}</span>
<button
className={s.repoRemoveBtn}
onClick={() => removeRepo(url)}
disabled={savingRepos}
title="Remove repo"
>
{savingRepos
? <CircleNotch size={12} weight="light" className="anim-spin" />
: <X size={12} weight="bold" />}
</button>
</div>
))}
</div>
)}
<div className={s.externalRow} style={{ marginTop: "var(--sp-2)" }}>
<input
className={[s.externalInput, repoError ? s.externalInputError : ""].join(" ").trim()}
placeholder="https://example.com/index.min.json"
value={newRepoUrl}
onChange={(e) => { setNewRepoUrl(e.target.value); setRepoError(null); }}
onKeyDown={(e) => e.key === "Enter" && !savingRepos && addRepo()}
disabled={savingRepos}
/>
<button
className={s.installBtn}
onClick={addRepo}
disabled={savingRepos || !newRepoUrl.trim()}
>
{savingRepos
? <CircleNotch size={13} weight="light" className="anim-spin" />
: "Add"}
</button>
</div>
{repoError && <div className={s.panelError}>{repoError}</div>}
</>
)}
</div>
)}
<div className={s.controls}>
<div className={s.tabs}>
{FILTERS.map((f) => (
<button key={f.id} onClick={() => setFilter(f.id)}
className={[s.tab, filter === f.id ? s.tabActive : ""].join(" ").trim()}>
{f.label}
</button>
))}
</div>
<div className={s.searchWrap}>
<MagnifyingGlass size={12} className={s.searchIcon} weight="light" />
<input className={s.search} placeholder="Search"
value={search} onChange={(e) => setSearch(e.target.value)} />
</div>
</div>
{loading ? (
<div className={s.empty}>
<CircleNotch size={16} weight="light" className="anim-spin" style={{ color: "var(--text-faint)" }} />
</div>
) : groups.length === 0 ? (
<div className={s.empty}>No extensions found.</div>
) : (
<div className={s.list}>
{groups.map(({ base, primary, variants }) => {
const isExpanded = expanded.has(base);
const hasVariants = variants.length > 0;
return (
<div key={base} className={s.group}>
<div className={s.row}>
<img src={thumbUrl(primary.iconUrl)} alt={primary.name} className={s.icon}
onError={(e) => { (e.target as HTMLImageElement).style.display = "none"; }} />
<div className={s.info}>
<span className={s.name}>{base}</span>
<span className={s.meta}>
<span className={s.langTag}>{primary.lang.toUpperCase()}</span>
{" "}v{primary.versionName}
</span>
</div>
{primary.hasUpdate && <span className={s.updateBadge}>Update</span>}
{renderActions(primary)}
{hasVariants && (
<button className={s.expandBtn} onClick={() => toggleExpand(base)}
title={`${variants.length + 1} languages`}>
{isExpanded ? <CaretDown size={12} weight="light" /> : <CaretRight size={12} weight="light" />}
<span className={s.expandCount}>{variants.length + 1}</span>
</button>
)}
</div>
{isExpanded && hasVariants && (
<div className={s.variants}>
{variants.map((v) => (
<div key={v.pkgName} className={s.variantRow}>
<span className={s.langTag}>{v.lang.toUpperCase()}</span>
<span className={s.variantName}>{v.name}</span>
<span className={s.variantVersion}>v{v.versionName}</span>
{v.hasUpdate && <span className={s.updateBadgeSmall}></span>}
<div className={s.variantActions}>{renderActions(v)}</div>
</div>
))}
</div>
)}
</div>
);
})}
</div>
)}
</div>
);
}
-15
View File
@@ -1,15 +0,0 @@
.root {
display: flex;
height: 100%;
background: var(--bg-base);
overflow: hidden;
}
.main {
flex: 1;
overflow: hidden;
background: var(--bg-surface);
/* GPU layer for main content area */
transform: translateZ(0);
contain: layout style;
}
-36
View File
@@ -1,36 +0,0 @@
import { useStore } from "../../store";
import Sidebar from "./Sidebar";
import Library from "../pages/Library";
import SeriesDetail from "../pages/SeriesDetail";
import History from "../pages/History";
import Search from "../pages/Search";
import Explore from "../sources/Explore";
import DownloadQueue from "../downloads/DownloadQueue";
import ExtensionList from "../extensions/ExtensionList";
import s from "./Layout.module.css";
export default function Layout() {
const navPage = useStore((s) => s.navPage);
const activeManga = useStore((s) => s.activeManga);
function renderContent() {
if (navPage === "library" && activeManga) return <SeriesDetail />;
switch (navPage) {
case "library": return <Library />;
case "search": return <Search />;
case "history": return <History />;
case "sources": return <Explore />;
case "explore": return <Explore />;
case "downloads": return <DownloadQueue />;
case "extensions": return <ExtensionList />;
default: return <Library />;
}
}
return (
<div className={s.root}>
<Sidebar />
<main className={s.main}>{renderContent()}</main>
</div>
);
}

Some files were not shown because too many files have changed in this diff Show More