Compare commits

...

21 Commits

Author SHA1 Message Date
Huang Xin f4908c4536 release: version 0.9.31 (#839) 2025-04-08 15:54:34 +02:00
Huang Xin 37951c2ebf feat: add book metadata search in the bookshelf, closes #100 (#838) 2025-04-08 15:37:18 +02:00
Huang Xin 915f222e08 doc: add a troubleshooting section in README (#837) 2025-04-08 18:05:22 +08:00
Huang Xin 9446df1b9c feat: add an option to configure default CJK fonts in CJK environments, closes #780 (#836) 2025-04-08 11:27:59 +02:00
Huang Xin bfcf89b093 fix: toggle the menus on the whole screen when Click-To-Flip is disabled, closes #831 (#834) 2025-04-08 08:09:46 +02:00
Huang Xin b0cf087d78 fix: support opening content URIs by copying to cache when direct access fails, closes #829 (#833) 2025-04-08 07:45:33 +02:00
Huang Xin 5863586766 feat: vertical layout for highlighting tools, closes #787 (#830) 2025-04-07 17:24:14 +02:00
Huang Xin 99182a1f9e feat: add configurable timeout to stop TTS playback automatically, closes #747 (#826) 2025-04-07 12:40:14 +02:00
Huang Xin 2599f40dfb feat: add option to keep window on top, closes #820 (#825) 2025-04-07 08:23:46 +02:00
Huang Xin aa66ba54c5 fix: set proper default theme mode, closes #823 (#824) 2025-04-07 07:16:31 +02:00
Huang Xin 4f0ef01a17 fix: tts now works in background in iOS, closes #547 (#822)
To enable background playback in Android, go to Settings > Apps & Notifications > Readest > Battery > Battery Optimization, and disable battery optimization for Readest.
2025-04-06 18:42:58 +02:00
Huang Xin 267e1656db release: version 0.9.30 (#817) 2025-04-05 11:39:37 +02:00
Huang Xin dcecda2984 fix: cache NativeFile with LRU, closes #797 (#816) 2025-04-05 11:27:07 +02:00
Huang Xin ce88f0229a fix: rounded reader widget only when sidebar is invisible , closes #809 (#815) 2025-04-05 10:08:39 +02:00
Huang Xin 316c82ea52 fix: override link color only when overriding font, closes #812 (#814) 2025-04-05 09:34:29 +02:00
Huang Xin 25be60ce2a chore: bump tauri to 2.4.1 (#813) 2025-04-05 07:40:33 +02:00
Huang Xin 4703eec045 fix: set correct filename in openFile API, closes #803 (#810) 2025-04-05 12:41:26 +08:00
Huang Xin ab22a83332 fix: open default file chooser on Android, closes #798 and closes #799 (#807) 2025-04-04 05:01:12 +02:00
Huang Xin a3d6ebe44d feat: add fixed storage quota for self-hosted instance, closes #805 (#806) 2025-04-04 03:48:43 +02:00
Huang Xin f8804b5d3c fix: revert font family, size and color when overrideFont is set, closes #792 (#804) 2025-04-03 17:15:19 +02:00
Huang Xin ee3785ad51 fix: hack to decode incorrectly encoded url in href (#796) 2025-04-02 07:35:00 +02:00
72 changed files with 1364 additions and 539 deletions
+1 -1
View File
@@ -28,7 +28,7 @@ jobs:
run: cargo fmt --check
- name: Clippy Check
working-directory: apps/readest-app/src-tauri
run: cargo clippy -- -D warnings
run: cargo clippy -p Readest --no-deps -- -D warnings
build_web_app:
runs-on: ubuntu-latest
Generated
+161 -152
View File
@@ -10,7 +10,7 @@ checksum = "fe438c63458706e03479442743baae6c88256498e6431708f6dfc520a26515d3"
[[package]]
name = "Readest"
version = "0.2.1"
version = "0.2.2"
dependencies = [
"cocoa",
"font-enumeration",
@@ -23,7 +23,7 @@ dependencies = [
"serde",
"serde_json",
"tauri",
"tauri-build 2.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
"tauri-build 2.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
"tauri-plugin-cli",
"tauri-plugin-deep-link",
"tauri-plugin-dialog",
@@ -547,9 +547,9 @@ dependencies = [
[[package]]
name = "borsh"
version = "1.5.6"
version = "1.5.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b2b74d67a0fc0af8e9823b79fd1c43a0900e5a8f0e0f4cc9210796bf3a820126"
checksum = "ad8646f98db542e39fc66e68a20b2144f6a732636df7c2354e74645faaa433ce"
dependencies = [
"borsh-derive",
"cfg_aliases",
@@ -557,9 +557,9 @@ dependencies = [
[[package]]
name = "borsh-derive"
version = "1.5.6"
version = "1.5.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2d37ed1b2c9b78421218a0b4f6d8349132d6ec2cfeba1cfb0118b0a8e268df9e"
checksum = "fdd1d3c0c2f5833f22386f252fe8ed005c7f59fdcddeef025c01b4c3b9fd9ac3"
dependencies = [
"once_cell",
"proc-macro-crate 3.3.0",
@@ -724,9 +724,9 @@ dependencies = [
[[package]]
name = "cc"
version = "1.2.17"
version = "1.2.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fcb57c740ae1daf453ae85f16e37396f672b039e00d9d866e07ddb24e328e3a"
checksum = "525046617d8376e3db1deffb079e91cef90a89fc3ca5c185bbf8c9ecdd15cd5c"
dependencies = [
"shlex",
]
@@ -795,18 +795,18 @@ dependencies = [
[[package]]
name = "clap"
version = "4.5.32"
version = "4.5.35"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6088f3ae8c3608d19260cd7445411865a485688711b78b5be70d78cd96136f83"
checksum = "d8aa86934b44c19c50f87cc2790e19f54f7a67aedb64101c2e1a2e5ecfb73944"
dependencies = [
"clap_builder",
]
[[package]]
name = "clap_builder"
version = "4.5.32"
version = "4.5.35"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "22a7ef7f676155edfb82daa97f99441f3ebf4a58d5e32f295a56259f1b6facc8"
checksum = "2414dbb2dd0695280da6ea9261e327479e9d37b0630f6b53ba2a11c60c679fd9"
dependencies = [
"anstream",
"anstyle",
@@ -1114,9 +1114,9 @@ dependencies = [
[[package]]
name = "darling"
version = "0.20.10"
version = "0.20.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6f63b86c8a8826a49b8c21f08a2d07338eec8d900540f8630dc76284be802989"
checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee"
dependencies = [
"darling_core",
"darling_macro",
@@ -1124,9 +1124,9 @@ dependencies = [
[[package]]
name = "darling_core"
version = "0.20.10"
version = "0.20.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "95133861a8032aaea082871032f5815eb9e98cef03fa916ab4500513994df9e5"
checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e"
dependencies = [
"fnv",
"ident_case",
@@ -1138,9 +1138,9 @@ dependencies = [
[[package]]
name = "darling_macro"
version = "0.20.10"
version = "0.20.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806"
checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead"
dependencies = [
"darling_core",
"quote",
@@ -1329,9 +1329,9 @@ checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813"
[[package]]
name = "dwrote"
version = "0.11.2"
version = "0.11.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "70182709525a3632b2ba96b6569225467b18ecb4a77f46d255f713a6bebf05fd"
checksum = "bfe1f192fcce01590bd8d839aca53ce0d11d803bf291b2a6c4ad925a8f0024be"
dependencies = [
"lazy_static",
"libc",
@@ -1441,9 +1441,9 @@ dependencies = [
[[package]]
name = "errno"
version = "0.3.10"
version = "0.3.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d"
checksum = "976dd42dc7e85965fe702eb8164f21f450704bdde31faefd6471dba214cb594e"
dependencies = [
"libc",
"windows-sys 0.59.0",
@@ -1462,9 +1462,9 @@ dependencies = [
[[package]]
name = "event-listener-strategy"
version = "0.5.3"
version = "0.5.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3c3e4e0dd3673c1139bf041f3008816d9cf2946bbfac2945c09e523b8d7b05b2"
checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93"
dependencies = [
"event-listener",
"pin-project-lite",
@@ -1518,9 +1518,9 @@ dependencies = [
[[package]]
name = "flate2"
version = "1.1.0"
version = "1.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "11faaf5a5236997af9848be0bef4db95824b1d534ebc64d0f0c6cf3e67bd38dc"
checksum = "7ced92e76e966ca2fd84c8f7aa01a4aea65b0eb6648d72f7c8f3e2764a67fece"
dependencies = [
"crc32fast",
"miniz_oxide",
@@ -1843,11 +1843,11 @@ dependencies = [
[[package]]
name = "gethostname"
version = "1.0.0"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4fd4b8790c0792e3b11895efdf5f289ebe8b59107a6624f1cce68f24ff8c7035"
checksum = "ed7131e57abbde63513e0e6636f76668a1ca9798dcae2df4e283cae9ee83859e"
dependencies = [
"rustix 0.38.44",
"rustix 1.0.5",
"windows-targets 0.52.6",
]
@@ -2065,7 +2065,7 @@ dependencies = [
"futures-core",
"futures-sink",
"http",
"indexmap 2.8.0",
"indexmap 2.9.0",
"slab",
"tokio",
"tokio-util",
@@ -2233,9 +2233,9 @@ dependencies = [
[[package]]
name = "hyper-util"
version = "0.1.10"
version = "0.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df2dcfbe0677734ab2f3ffa7fa7bfd4706bfdc1ef393f2ee30184aed67e631b4"
checksum = "497bbc33a26fdd4af9ed9c70d63f61cf56a938375fbb32df34db9b1cd6d643f2"
dependencies = [
"bytes",
"futures-channel",
@@ -2243,6 +2243,7 @@ dependencies = [
"http",
"http-body",
"hyper",
"libc",
"pin-project-lite",
"socket2",
"tokio",
@@ -2252,9 +2253,9 @@ dependencies = [
[[package]]
name = "iana-time-zone"
version = "0.1.62"
version = "0.1.63"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b2fd658b06e56721792c5df4475705b6cda790e9298d19d2f8af083457bcd127"
checksum = "b0c919e5debc312ad217002b8048a17b7d83f80703865bbfcfebb0458b0b27d8"
dependencies = [
"android_system_properties",
"core-foundation-sys",
@@ -2262,7 +2263,7 @@ dependencies = [
"js-sys",
"log",
"wasm-bindgen",
"windows-core 0.52.0",
"windows-core 0.61.0",
]
[[package]]
@@ -2325,9 +2326,9 @@ dependencies = [
[[package]]
name = "icu_locid_transform_data"
version = "1.5.0"
version = "1.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fdc8ff3388f852bede6b579ad4e978ab004f139284d7b28715f773507b946f6e"
checksum = "7515e6d781098bf9f7205ab3fc7e9709d34554ae0b21ddbcb5febfa4bc7df11d"
[[package]]
name = "icu_normalizer"
@@ -2349,9 +2350,9 @@ dependencies = [
[[package]]
name = "icu_normalizer_data"
version = "1.5.0"
version = "1.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8cafbf7aa791e9b22bec55a167906f9e1215fd475cd22adfcf660e03e989516"
checksum = "c5e8338228bdc8ab83303f16b797e177953730f601a96c25d10cb3ab0daa0cb7"
[[package]]
name = "icu_properties"
@@ -2370,9 +2371,9 @@ dependencies = [
[[package]]
name = "icu_properties_data"
version = "1.5.0"
version = "1.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "67a8effbc3dd3e4ba1afa8ad918d5684b8868b3b26500753effea8d2eed19569"
checksum = "85fb8799753b75aee8d2a21d7c14d9f38921b54b3dbda10f5a3c7a7b82dba5e2"
[[package]]
name = "icu_provider"
@@ -2454,9 +2455,9 @@ dependencies = [
[[package]]
name = "indexmap"
version = "2.8.0"
version = "2.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3954d50fe15b02142bf25d3b8bdadb634ec3948f103d04ffe3031bc8fe9d7058"
checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e"
dependencies = [
"equivalent",
"hashbrown 0.15.2",
@@ -2829,9 +2830,9 @@ checksum = "6367d84fb54d4242af283086402907277715b8fe46976963af5ebf173f8efba3"
[[package]]
name = "miniz_oxide"
version = "0.8.5"
version = "0.8.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e3e04debbb59698c15bacbb6d93584a8c0ca9cc3213cb423d31f760d8843ce5"
checksum = "ff70ce3e48ae43fa075863cef62e8b43b71a4f2382229920e0df362592919430"
dependencies = [
"adler2",
"simd-adler32",
@@ -3243,9 +3244,9 @@ dependencies = [
[[package]]
name = "once_cell"
version = "1.21.1"
version = "1.21.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d75b0bedcc4fe52caa0e03d9f1151a323e4aa5e2d78ba3580400cd3c9e2bc4bc"
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
[[package]]
name = "opaque-debug"
@@ -3267,9 +3268,9 @@ dependencies = [
[[package]]
name = "openssl"
version = "0.10.71"
version = "0.10.72"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e14130c6a98cd258fdcb0fb6d744152343ff729cbfcb28c656a9d12b999fbcd"
checksum = "fedfea7d58a1f73118430a55da6a286e7b044961736ce96a16a17068ea25e5da"
dependencies = [
"bitflags 2.9.0",
"cfg-if",
@@ -3308,9 +3309,9 @@ dependencies = [
[[package]]
name = "openssl-sys"
version = "0.9.106"
version = "0.9.107"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8bb61ea9811cc39e3c2069f40b8b8e2e70d8569b361f879786cc7ed48b777cdd"
checksum = "8288979acd84749c744a9014b4382d42b8f7b2592847b5afb2ed29e5d16ede07"
dependencies = [
"cc",
"libc",
@@ -3667,7 +3668,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eac26e981c03a6e53e0aee43c113e3202f5581d5360dae7bd2c70e800dd0451d"
dependencies = [
"base64 0.22.1",
"indexmap 2.8.0",
"indexmap 2.9.0",
"quick-xml",
"serde",
"time",
@@ -3936,9 +3937,9 @@ dependencies = [
[[package]]
name = "quinn-udp"
version = "0.5.10"
version = "0.5.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e46f3055866785f6b92bc6164b76be02ca8f2eb4b002c0354b28cf4c119e5944"
checksum = "541d0f57c6ec747a90738a52741d3221f7960e8ac2f0ff4b1a63680e033b4ab5"
dependencies = [
"cfg_aliases",
"libc",
@@ -4348,9 +4349,9 @@ dependencies = [
[[package]]
name = "rustix"
version = "1.0.3"
version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e56a18552996ac8d29ecc3b190b4fdbb2d91ca4ec396de7bbffaf43f3d637e96"
checksum = "d97817398dd4bb2e6da002002db259209759911da105da92bec29ccb12cf58bf"
dependencies = [
"bitflags 2.9.0",
"errno",
@@ -4393,9 +4394,9 @@ dependencies = [
[[package]]
name = "rustls-webpki"
version = "0.103.0"
version = "0.103.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0aa4eeac2588ffff23e9d7a7e9b3f971c5fb5b7ebc9452745e0c232c64f83b2f"
checksum = "fef8b8769aaccf73098557a87cd1816b4f9c7c16811c9c77142aa695c16f2c03"
dependencies = [
"ring",
"rustls-pki-types",
@@ -4646,7 +4647,7 @@ dependencies = [
"chrono",
"hex",
"indexmap 1.9.3",
"indexmap 2.8.0",
"indexmap 2.9.0",
"serde",
"serde_derive",
"serde_json",
@@ -4794,15 +4795,15 @@ dependencies = [
[[package]]
name = "smallvec"
version = "1.14.0"
version = "1.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7fcf8323ef1faaee30a44a340193b1ac6814fd9b7b4e88e9d4519a3e4abe1cfd"
checksum = "8917285742e9f3e1683f0a9c4e6b57960b7314d0b08d30d1ecd426713ee2eee9"
[[package]]
name = "socket2"
version = "0.5.8"
version = "0.5.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c970269d99b64e60ec3bd6ad27270092a5394c4e309314b18ae3fe575695fbe8"
checksum = "4f5fd57c80058a56cf5c777ab8a126398ece8e442983605d280a44ce79d0edef"
dependencies = [
"libc",
"windows-sys 0.52.0",
@@ -4893,9 +4894,9 @@ checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
[[package]]
name = "string_cache"
version = "0.8.8"
version = "0.8.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "938d512196766101d333398efde81bc1f37b00cb42c2f8350e5df639f040bbbe"
checksum = "bf776ba3fa74f83bf4b63c3dcbbf82173db2632ed8452cb2d891d33f459de70f"
dependencies = [
"new_debug_unreachable",
"parking_lot",
@@ -5099,7 +5100,7 @@ checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1"
[[package]]
name = "tauri"
version = "2.4.0"
version = "2.4.1"
dependencies = [
"anyhow",
"bytes",
@@ -5139,11 +5140,11 @@ dependencies = [
"specta",
"swift-rs",
"tauri",
"tauri-build 2.1.0",
"tauri-build 2.1.1",
"tauri-macros",
"tauri-runtime",
"tauri-runtime-wry",
"tauri-utils 2.3.0",
"tauri-utils 2.3.1",
"thiserror 2.0.12",
"tokio",
"tracing",
@@ -5159,7 +5160,7 @@ dependencies = [
[[package]]
name = "tauri-build"
version = "2.1.0"
version = "2.1.1"
dependencies = [
"anyhow",
"cargo_toml",
@@ -5173,7 +5174,7 @@ dependencies = [
"serde",
"serde_json",
"tauri-codegen",
"tauri-utils 2.3.0",
"tauri-utils 2.3.1",
"tauri-winres",
"toml",
"walkdir",
@@ -5181,9 +5182,9 @@ dependencies = [
[[package]]
name = "tauri-build"
version = "2.1.0"
version = "2.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7ffa8732a66f90903f5a585215f3cf1e87988d0359bc88c18a502efe7572c1de"
checksum = "0fd20e4661c2cce65343319e6e8da256958f5af958cafc47c0d0af66a55dcd17"
dependencies = [
"anyhow",
"cargo_toml",
@@ -5195,7 +5196,7 @@ dependencies = [
"semver",
"serde",
"serde_json",
"tauri-utils 2.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
"tauri-utils 2.3.1 (registry+https://github.com/rust-lang/crates.io-index)",
"tauri-winres",
"toml",
"walkdir",
@@ -5203,7 +5204,7 @@ dependencies = [
[[package]]
name = "tauri-codegen"
version = "2.1.0"
version = "2.1.1"
dependencies = [
"base64 0.22.1",
"brotli",
@@ -5219,7 +5220,7 @@ dependencies = [
"serde_json",
"sha2",
"syn 2.0.100",
"tauri-utils 2.3.0",
"tauri-utils 2.3.1",
"thiserror 2.0.12",
"time",
"url",
@@ -5229,21 +5230,21 @@ dependencies = [
[[package]]
name = "tauri-macros"
version = "2.1.0"
version = "2.1.1"
dependencies = [
"heck 0.5.0",
"proc-macro2",
"quote",
"syn 2.0.100",
"tauri-codegen",
"tauri-utils 2.3.0",
"tauri-utils 2.3.1",
]
[[package]]
name = "tauri-plugin"
version = "2.1.0"
version = "2.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9972871fcbddf16618f70412d965d4d845cd4b76d03fff168709961ef71e5cdf"
checksum = "a4190775d6ff73fe66d9af44c012739a2659720efd9c0e1e56a918678038699d"
dependencies = [
"anyhow",
"glob",
@@ -5251,7 +5252,7 @@ dependencies = [
"schemars",
"serde",
"serde_json",
"tauri-utils 2.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
"tauri-utils 2.3.1 (registry+https://github.com/rust-lang/crates.io-index)",
"toml",
"walkdir",
]
@@ -5273,9 +5274,9 @@ dependencies = [
[[package]]
name = "tauri-plugin-deep-link"
version = "2.2.0"
version = "2.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "35d51ffd286073414d26353bcfc9e83e3cd63f96fa7f7a912f92f2118e5de5a6"
checksum = "dba4412f30eaff6f5d210e20383c2d6835593977402092e95b72497a4f8632fa"
dependencies = [
"dunce",
"rust-ini",
@@ -5283,19 +5284,19 @@ dependencies = [
"serde_json",
"tauri",
"tauri-plugin",
"tauri-utils 2.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
"tauri-utils 2.3.1 (registry+https://github.com/rust-lang/crates.io-index)",
"thiserror 2.0.12",
"tracing",
"url",
"windows-registry 0.3.0",
"windows-result 0.2.0",
"windows-registry 0.5.1",
"windows-result",
]
[[package]]
name = "tauri-plugin-dialog"
version = "2.2.0"
version = "2.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b59fd750551b1066744ab956a1cd6b1ea3e1b3763b0b9153ac27a044d596426"
checksum = "bcaf6e5d6062423a0f711a23c2a573ccba222b6a16a9322d8499928f27e41376"
dependencies = [
"log",
"raw-window-handle",
@@ -5311,9 +5312,9 @@ dependencies = [
[[package]]
name = "tauri-plugin-fs"
version = "2.2.0"
version = "2.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1a1edf18000f02903a7c2e5997fb89aca455ecbc0acc15c6535afbb883be223"
checksum = "88371e340ad2f07409a3b68294abe73f20bc9c1bc1b631a31dc37a3d0161f682"
dependencies = [
"anyhow",
"dunce",
@@ -5325,7 +5326,7 @@ dependencies = [
"serde_repr",
"tauri",
"tauri-plugin",
"tauri-utils 2.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
"tauri-utils 2.3.1 (registry+https://github.com/rust-lang/crates.io-index)",
"thiserror 2.0.12",
"toml",
"url",
@@ -5348,10 +5349,12 @@ dependencies = [
[[package]]
name = "tauri-plugin-http"
version = "2.4.2"
version = "2.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "696ef548befeee6c6c17b80ef73e7c41205b6c2204e87ef78ccc231212389a5c"
checksum = "40dcd6c922a1885e1f0bcebc6768fec6e005bd4b9001c5d90a2f5d4cab297729"
dependencies = [
"bytes",
"cookie_store",
"data-url",
"http",
"regex",
@@ -5458,9 +5461,9 @@ dependencies = [
[[package]]
name = "tauri-plugin-process"
version = "2.2.0"
version = "2.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "40cc553ab29581c8c43dfa5fb0c9d5aee8ba962ad3b42908eea26c79610441b7"
checksum = "57da5888533e802b6206b9685091f8714aa1f5266dc80051a82388449558b773"
dependencies = [
"tauri",
"tauri-plugin",
@@ -5468,9 +5471,9 @@ dependencies = [
[[package]]
name = "tauri-plugin-shell"
version = "2.2.0"
version = "2.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bb2c50a63e60fb8925956cc5b7569f4b750ac197a4d39f13b8dd46ea8e2bad79"
checksum = "69d5eb3368b959937ad2aeaf6ef9a8f5d11e01ffe03629d3530707bbcb27ff5d"
dependencies = [
"encoding_rs",
"log",
@@ -5501,9 +5504,9 @@ dependencies = [
[[package]]
name = "tauri-plugin-single-instance"
version = "2.2.2"
version = "2.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "25bbc73eed15bba8ad290a52614f2711280df4bf575b36ce78f64367074b90b7"
checksum = "1320af4d866a7fb5f5721d299d14d0dd9e4e6bc0359ff3e263124a2bf6814efa"
dependencies = [
"serde",
"serde_json",
@@ -5516,9 +5519,9 @@ dependencies = [
[[package]]
name = "tauri-plugin-updater"
version = "2.6.1"
version = "2.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a31bfcfb4a8318008d2108ccfba439d8263cf48867baabf372cb0e9f24771896"
checksum = "d82da763248e635d60ee4aed56c862290e523acc838d83097171f9a544708387"
dependencies = [
"base64 0.22.1",
"dirs",
@@ -5548,9 +5551,9 @@ dependencies = [
[[package]]
name = "tauri-plugin-window-state"
version = "2.2.1"
version = "2.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "35e344b512b0d99d9d06225f235d87d6c66d89496a3bf323d9b578d940596e6c"
checksum = "a27a3fe49de72adbe0d84aee33c89a0b059722cd0b42aaeab29eaaee7f7535cd"
dependencies = [
"bitflags 2.9.0",
"log",
@@ -5563,7 +5566,7 @@ dependencies = [
[[package]]
name = "tauri-runtime"
version = "2.5.0"
version = "2.5.1"
dependencies = [
"cookie",
"dpi",
@@ -5573,7 +5576,7 @@ dependencies = [
"raw-window-handle",
"serde",
"serde_json",
"tauri-utils 2.3.0",
"tauri-utils 2.3.1",
"thiserror 2.0.12",
"url",
"windows",
@@ -5581,7 +5584,7 @@ dependencies = [
[[package]]
name = "tauri-runtime-wry"
version = "2.5.0"
version = "2.5.1"
dependencies = [
"gtk",
"http",
@@ -5596,7 +5599,7 @@ dependencies = [
"softbuffer",
"tao",
"tauri-runtime",
"tauri-utils 2.3.0",
"tauri-utils 2.3.1",
"tracing",
"url",
"webkit2gtk",
@@ -5607,7 +5610,7 @@ dependencies = [
[[package]]
name = "tauri-utils"
version = "2.3.0"
version = "2.3.1"
dependencies = [
"aes-gcm",
"anyhow",
@@ -5648,9 +5651,9 @@ dependencies = [
[[package]]
name = "tauri-utils"
version = "2.3.0"
version = "2.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "82dcced4014e59af9790cc22f5d271df3be09ecd6728ec68861642553c8d01b7"
checksum = "1f037e66c7638cc0a2213f61566932b9a06882b8346486579c90e4b019bac447"
dependencies = [
"anyhow",
"cargo_metadata",
@@ -5702,7 +5705,7 @@ dependencies = [
"fastrand",
"getrandom 0.3.2",
"once_cell",
"rustix 1.0.3",
"rustix 1.0.5",
"windows-sys 0.59.0",
]
@@ -5920,7 +5923,7 @@ version = "0.19.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421"
dependencies = [
"indexmap 2.8.0",
"indexmap 2.9.0",
"toml_datetime",
"winnow 0.5.40",
]
@@ -5931,7 +5934,7 @@ version = "0.20.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "70f427fce4d84c72b5b732388bf4a9f4531b53f74e2887e3ecb2481f68f66d81"
dependencies = [
"indexmap 2.8.0",
"indexmap 2.9.0",
"toml_datetime",
"winnow 0.5.40",
]
@@ -5942,7 +5945,7 @@ version = "0.22.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "17b4795ff5edd201c7cd6dca065ae59972ce77d1b80fa0a84d94950ece7d1474"
dependencies = [
"indexmap 2.8.0",
"indexmap 2.9.0",
"serde",
"serde_spanned",
"toml_datetime",
@@ -6211,9 +6214,9 @@ dependencies = [
[[package]]
name = "value-bag"
version = "1.10.0"
version = "1.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3ef4c4aa54d5d05a279399bfa921ec387b7aba77caf7a682ae8d86785b8fdad2"
checksum = "943ce29a8a743eb10d6082545d861b24f9d1b160b7d741e0f2cdf726bec909c5"
[[package]]
name = "vcpkg"
@@ -6469,7 +6472,7 @@ dependencies = [
"webview2-com-sys",
"windows",
"windows-core 0.60.1",
"windows-implement",
"windows-implement 0.59.0",
"windows-interface",
]
@@ -6563,28 +6566,32 @@ dependencies = [
"windows-core 0.60.1",
]
[[package]]
name = "windows-core"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9"
dependencies = [
"windows-targets 0.52.6",
]
[[package]]
name = "windows-core"
version = "0.60.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ca21a92a9cae9bf4ccae5cf8368dce0837100ddf6e6d57936749e85f152f6247"
dependencies = [
"windows-implement",
"windows-implement 0.59.0",
"windows-interface",
"windows-link",
"windows-result 0.3.2",
"windows-result",
"windows-strings 0.3.1",
]
[[package]]
name = "windows-core"
version = "0.61.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4763c1de310c86d75a878046489e2e5ba02c649d185f21c67d4cf8a56d098980"
dependencies = [
"windows-implement 0.60.0",
"windows-interface",
"windows-link",
"windows-result",
"windows-strings 0.4.0",
]
[[package]]
name = "windows-future"
version = "0.1.1"
@@ -6606,6 +6613,17 @@ dependencies = [
"syn 2.0.100",
]
[[package]]
name = "windows-implement"
version = "0.60.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.100",
]
[[package]]
name = "windows-interface"
version = "0.59.1"
@@ -6633,35 +6651,26 @@ dependencies = [
"windows-link",
]
[[package]]
name = "windows-registry"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bafa604f2104cf5ae2cc2db1dee84b7e6a5d11b05f737b60def0ffdc398cbc0a"
dependencies = [
"windows-result 0.2.0",
"windows-strings 0.2.0",
"windows-targets 0.52.6",
]
[[package]]
name = "windows-registry"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4286ad90ddb45071efd1a66dfa43eb02dd0dfbae1545ad6cc3c51cf34d7e8ba3"
dependencies = [
"windows-result 0.3.2",
"windows-result",
"windows-strings 0.3.1",
"windows-targets 0.53.0",
]
[[package]]
name = "windows-result"
version = "0.2.0"
name = "windows-registry"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e"
checksum = "ad1da3e436dc7653dfdf3da67332e22bff09bb0e28b0239e1624499c7830842e"
dependencies = [
"windows-targets 0.52.6",
"windows-link",
"windows-result",
"windows-strings 0.4.0",
]
[[package]]
@@ -6675,18 +6684,18 @@ dependencies = [
[[package]]
name = "windows-strings"
version = "0.2.0"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "978d65aedf914c664c510d9de43c8fd85ca745eaff1ed53edf409b479e441663"
checksum = "87fa48cc5d406560701792be122a10132491cff9d0aeb23583cc2dcafc847319"
dependencies = [
"windows-targets 0.52.6",
"windows-link",
]
[[package]]
name = "windows-strings"
version = "0.3.1"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "87fa48cc5d406560701792be122a10132491cff9d0aeb23583cc2dcafc847319"
checksum = "7a2ba9642430ee452d5a7aa78d72907ebe8cfda358e8cb7918a2050581322f97"
dependencies = [
"windows-link",
]
@@ -7118,7 +7127,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0d65cbf2f12c15564212d48f4e3dfb87923d25d611f2aed18f4cb23f0413d89e"
dependencies = [
"libc",
"rustix 1.0.3",
"rustix 1.0.5",
]
[[package]]
@@ -7301,14 +7310,14 @@ dependencies = [
[[package]]
name = "zip"
version = "2.5.0"
version = "2.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "27c03817464f64e23f6f37574b4fdc8cf65925b5bfd2b0f2aedf959791941f88"
checksum = "1dcb24d0152526ae49b9b96c1dcf71850ca1e0b882e4e28ed898a93c41334744"
dependencies = [
"arbitrary",
"crc32fast",
"crossbeam-utils",
"indexmap 2.8.0",
"indexmap 2.9.0",
"memchr",
]
+34 -1
View File
@@ -26,7 +26,7 @@
<a href="#screenshots">Screenshots</a> •
<a href="#downloads">Downloads</a> •
<a href="#getting-started">Getting Started</a> •
<a href="#contributors">Contributors</a> •
<a href="#troubleshooting">Troubleshooting</a> •
<a href="#license">License</a>
</p>
@@ -199,6 +199,39 @@ nix develop ./ops#android # enter a dev shell for the android app
Please check the [wiki][link-gh-wiki] of this project for more information on development.
## Troubleshooting
### 1. Readest Wont Launch on Windows (Missing Edge WebView2 Runtime)
**Symptom**
- When you double-click readest.exe, nothing happens. No window appears, and Task Manager does not show the process.
- This can affect both the standard installer and the portable version.
**Cause**
- Microsoft Edge WebView2 Runtime is either missing, outdated, or improperly installed on your system. Readest depends on WebView2 to render the interface on Windows.
**How to Fix**
1. Check if WebView2 is installed
- Open “Add or Remove Programs” (a.k.a. Apps & features) on Windows. Look for “Microsoft Edge WebView2 Runtime.”
2. Install or Update WebView2
- Download the WebView2 Runtime directly from Microsoft: [link](https://developer.microsoft.com/en-us/microsoft-edge/webview2?form=MA13LH).
- If you prefer an offline installer, download the offline package and run it as an Administrator.
3. Re-run Readest
- After installing/updating WebView2, launch readest.exe again.
- If you still encounter problems, reboot your PC and try again.
**Additional Tips**
- If reinstalling once doesnt work, uninstall Edge WebView2 completely, then reinstall it with Administrator privileges.
- Verify your Windows installation has the latest updates from Microsoft.
**Still Stuck?**
- See Issue [readest/readest#358](https://github.com/readest/readest/issues/358) for further details, or head over to our [Discord][link-discord] server and open a support discussion with detailed logs of your environment and the steps youve taken.
## Contributors
Readest is open-source, and contributions are welcome! Feel free to open issues, suggest features, or submit pull requests. Please **review our [contributing guidelines](CONTRIBUTING.md) before you start**. We also welcome you to join our [Discord][link-discord] community for either support or contributing guidance.
+2
View File
@@ -4,6 +4,8 @@ NEXT_PUBLIC_POSTHOG_HOST=YOUR_POSTHOG_HOST
NEXT_PUBLIC_SUPABASE_URL=YOUR_SUPABASE_URL
NEXT_PUBLIC_SUPABASE_ANON_KEY=YOUR_SUPABASE_ANON_KEY
NEXT_PUBLIC_STORAGE_FIXED_QUOTA=1073741824
SUPABASE_ADMIN_KEY=YOUR_SUPABASE_ADMIN_KEY
DEEPL_PRO_API_KEYS=YOUR_DEEPL_PRO_API_KEYS
+13 -13
View File
@@ -1,6 +1,6 @@
{
"name": "@readest/readest-app",
"version": "0.9.29",
"version": "0.9.31",
"private": true,
"scripts": {
"dev": "dotenv -e .env.tauri -- next dev",
@@ -42,19 +42,19 @@
"@supabase/auth-ui-react": "^0.4.7",
"@supabase/auth-ui-shared": "^0.1.8",
"@supabase/supabase-js": "^2.47.7",
"@tauri-apps/api": "2.3.0",
"@tauri-apps/api": "2.4.1",
"@tauri-apps/plugin-cli": "^2.2.0",
"@tauri-apps/plugin-deep-link": "^2.2.0",
"@tauri-apps/plugin-dialog": "^2.2.0",
"@tauri-apps/plugin-fs": "^2.2.0",
"@tauri-apps/plugin-haptics": "^2.2.3",
"@tauri-apps/plugin-http": "^2.3.0",
"@tauri-apps/plugin-log": "^2.2.3",
"@tauri-apps/plugin-deep-link": "^2.2.1",
"@tauri-apps/plugin-dialog": "^2.2.1",
"@tauri-apps/plugin-fs": "^2.2.1",
"@tauri-apps/plugin-haptics": "^2.2.4",
"@tauri-apps/plugin-http": "^2.4.3",
"@tauri-apps/plugin-log": "^2.3.1",
"@tauri-apps/plugin-opener": "^2.2.6",
"@tauri-apps/plugin-os": "^2.2.0",
"@tauri-apps/plugin-process": "^2.2.0",
"@tauri-apps/plugin-shell": "~2.2.0",
"@tauri-apps/plugin-updater": "^2.5.1",
"@tauri-apps/plugin-os": "^2.2.1",
"@tauri-apps/plugin-process": "^2.2.1",
"@tauri-apps/plugin-shell": "~2.2.1",
"@tauri-apps/plugin-updater": "^2.7.0",
"@zip.js/zip.js": "^2.7.53",
"aws4fetch": "^1.0.20",
"clsx": "^2.1.1",
@@ -79,7 +79,7 @@
},
"devDependencies": {
"@opennextjs/cloudflare": "^0.5.12",
"@tauri-apps/cli": "2.3.1",
"@tauri-apps/cli": "2.4.1",
"@types/cors": "^2.8.17",
"@types/cssbeautify": "^0.3.5",
"@types/node": "^22.10.1",
@@ -265,5 +265,13 @@
"Failed to parse the EPUB file.": "فشل في تحليل ملف EPUB.",
"This book format is not supported.": "هذا تنسيق الكتاب غير مدعوم.",
"Unable to fetch the translation. Please log in first and try again.": "تعذر جلب الترجمة. يرجى تسجيل الدخول أولاً ثم المحاولة مرة أخرى.",
"Group": "تجميع"
"Group": "تجميع",
"Always on Top": "دائمًا في المقدمة",
"No Timeout": "لا مهلة",
"{{value}} minute": "{{value}} دقيقة",
"{{value}} minutes": "{{value}} دقائق",
"{{value}} hour": "{{value}} ساعة",
"{{value}} hours": "{{value}} ساعات",
"CJK Font": "خط CJK",
"Clear Search": "مسح البحث"
}
@@ -265,5 +265,13 @@
"Failed to parse the EPUB file.": "Fehler beim Parsen der EPUB-Datei.",
"This book format is not supported.": "Dieses Buchformat wird nicht unterstützt.",
"Unable to fetch the translation. Please log in first and try again.": "Übersetzung konnte nicht abgerufen werden. Bitte zuerst anmelden und erneut versuchen.",
"Group": "Gruppieren"
"Group": "Gruppieren",
"Always on Top": "Immer im Vordergrund",
"No Timeout": "Kein Timeout",
"{{value}} minute": "{{value}} Minute",
"{{value}} minutes": "{{value}} Minuten",
"{{value}} hour": "{{value}} Stunde",
"{{value}} hours": "{{value}} Stunden",
"CJK Font": "CJK-Schriftart",
"Clear Search": "Suche löschen"
}
@@ -265,5 +265,13 @@
"Failed to parse the EPUB file.": "Αποτυχία ανάλυσης του αρχείου EPUB.",
"This book format is not supported.": "Αυτή η μορφή βιβλίου δεν υποστηρίζεται.",
"Unable to fetch the translation. Please log in first and try again.": "Αδυναμία λήψης της μετάφρασης. Παρακαλώ συνδεθείτε πρώτα και δοκιμάστε ξανά.",
"Group": "Ομάδα"
"Group": "Ομάδα",
"Always on Top": "Πάντα στην κορυφή",
"No Timeout": "Χωρίς χρονικό όριο",
"{{value}} minute": "{{value}} λεπτό",
"{{value}} minutes": "{{value}} λεπτά",
"{{value}} hour": "{{value}} ώρα",
"{{value}} hours": "{{value}} ώρες",
"CJK Font": "Γραμματοσειρά CJK",
"Clear Search": "Καθαρισμός αναζήτησης"
}
@@ -265,5 +265,13 @@
"Failed to parse the EPUB file.": "Error al analizar el archivo EPUB.",
"This book format is not supported.": "Este formato de libro no es compatible.",
"Unable to fetch the translation. Please log in first and try again.": "No se puede obtener la traducción. Por favor, inicia sesión primero e inténtalo de nuevo.",
"Group": "Grupo"
"Group": "Grupo",
"Always on Top": "Siempre en la parte superior",
"No Timeout": "Sin tiempo de espera",
"{{value}} minute": "{{value}} minuto",
"{{value}} minutes": "{{value}} minutos",
"{{value}} hour": "{{value}} hora",
"{{value}} hours": "{{value}} horas",
"CJK Font": "Fuente CJK",
"Clear Search": "Limpiar búsqueda"
}
@@ -265,5 +265,13 @@
"Failed to parse the EPUB file.": "Échec de l'analyse du fichier EPUB.",
"This book format is not supported.": "Ce format de livre n'est pas pris en charge.",
"Unable to fetch the translation. Please log in first and try again.": "Impossible de récupérer la traduction. Veuillez d'abord vous connecter et réessayer.",
"Group": "Groupe"
"Group": "Groupe",
"Always on Top": "Toujours au-dessus",
"No Timeout": "Aucun délai d'attente",
"{{value}} minute": "{{value}} minute",
"{{value}} minutes": "{{value}} minutes",
"{{value}} hour": "{{value}} heure",
"{{value}} hours": "{{value}} heures",
"CJK Font": "Police CJK",
"Clear Search": "Effacer la recherche"
}
@@ -265,5 +265,13 @@
"Failed to parse the EPUB file.": "EPUB फ़ाइल को पार्स करने में विफल।",
"This book format is not supported.": "यह पुस्तक प्रारूप समर्थित नहीं है।",
"Unable to fetch the translation. Please log in first and try again.": "अनुवाद प्राप्त करने में असमर्थ। कृपया पहले लॉग इन करें और पुनः प्रयास करें।",
"Group": "समूह"
"Group": "समूह",
"Always on Top": "सर्वोच्च पर हमेशा",
"No Timeout": "कोई समय समाप्त नहीं",
"{{value}} minute": "{{value}} मिनट",
"{{value}} minutes": "{{value}} मिनट",
"{{value}} hour": "{{value}} घंटा",
"{{value}} hours": "{{value}} घंटे",
"CJK Font": "CJK फ़ॉन्ट",
"Clear Search": "खोज साफ करें"
}
@@ -265,5 +265,13 @@
"Failed to parse the EPUB file.": "Gagal mengurai file EPUB.",
"This book format is not supported.": "Format buku ini tidak didukung.",
"Unable to fetch the translation. Please log in first and try again.": "Tidak dapat mengambil terjemahan. Silakan masuk terlebih dahulu dan coba lagi.",
"Group": "Grup"
"Group": "Grup",
"Always on Top": "Selalu di Atas",
"No Timeout": "Tidak Ada Waktu Habis",
"{{value}} minute": "{{value}} menit",
"{{value}} minutes": "{{value}} menit",
"{{value}} hour": "{{value}} jam",
"{{value}} hours": "{{value}} jam",
"CJK Font": "Font CJK",
"Clear Search": "Hapus Pencarian"
}
@@ -265,5 +265,13 @@
"Failed to parse the EPUB file.": "Impossibile analizzare il file EPUB.",
"This book format is not supported.": "Questo formato di libro non è supportato.",
"Unable to fetch the translation. Please log in first and try again.": "Impossibile recuperare la traduzione. Accedi prima e riprova.",
"Group": "Gruppo"
"Group": "Gruppo",
"Always on Top": "Sempre in primo piano",
"No Timeout": "Nessun timeout",
"{{value}} minute": "{{value}} minuto",
"{{value}} minutes": "{{value}} minuti",
"{{value}} hour": "{{value}} ora",
"{{value}} hours": "{{value}} ore",
"CJK Font": "Font CJK",
"Clear Search": "Cancella ricerca"
}
@@ -265,5 +265,13 @@
"Failed to parse the EPUB file.": "EPUBファイルの解析に失敗しました。",
"This book format is not supported.": "この書籍形式はサポートされていません。",
"Unable to fetch the translation. Please log in first and try again.": "翻訳を取得できません。まずログインして再試行してください。",
"Group": "グループ"
"Group": "グループ",
"Always on Top": "常に最前面",
"No Timeout": "タイムアウトなし",
"{{value}} minute": "{{value}}分",
"{{value}} minutes": "{{value}}分",
"{{value}} hour": "{{value}}時間",
"{{value}} hours": "{{value}}時間",
"CJK Font": "CJKフォント",
"Clear Search": "検索をクリア"
}
@@ -265,5 +265,13 @@
"Failed to parse the EPUB file.": "EPUB 파일 구문 분석 실패.",
"This book format is not supported.": "이 책 형식은 지원되지 않습니다.",
"Unable to fetch the translation. Please log in first and try again.": "번역을 가져올 수 없습니다. 먼저 로그인하고 다시 시도하세요.",
"Group": "그룹"
"Group": "그룹",
"Always on Top": "항상 위에",
"No Timeout": "타임아웃 없음",
"{{value}} minute": "{{value}} 분",
"{{value}} minutes": "{{value}} 분",
"{{value}} hour": "{{value}} 시간",
"{{value}} hours": "{{value}} 시간",
"CJK Font": "CJK 글꼴",
"Clear Search": "검색 지우기"
}
@@ -265,5 +265,13 @@
"Failed to parse the EPUB file.": "Nie udało się przeanalizować pliku EPUB.",
"This book format is not supported.": "Ten format książki nie jest obsługiwany.",
"Unable to fetch the translation. Please log in first and try again.": "Nie można pobrać tłumaczenia. Proszę najpierw się zalogować i spróbować ponownie.",
"Group": "Grupa"
"Group": "Grupa",
"Always on Top": "Zawsze na wierzchu",
"No Timeout": "Brak limitu czasu",
"{{value}} minute": "{{value}} minuta",
"{{value}} minutes": "{{value}} minut",
"{{value}} hour": "{{value}} godzina",
"{{value}} hours": "{{value}} godziny",
"CJK Font": "Czcionka CJK",
"Clear Search": "Wyczyść wyszukiwanie"
}
@@ -265,5 +265,13 @@
"Failed to parse the EPUB file.": "Falha ao analisar o arquivo EPUB.",
"This book format is not supported.": "Este formato de livro não é suportado.",
"Unable to fetch the translation. Please log in first and try again.": "Não foi possível buscar a tradução. Faça login primeiro e tente novamente.",
"Group": "Grupo"
"Group": "Grupo",
"Always on Top": "Sempre no Topo",
"No Timeout": "Sem Tempo Limite",
"{{value}} minute": "{{value}} minuto",
"{{value}} minutes": "{{value}} minutos",
"{{value}} hour": "{{value}} hora",
"{{value}} hours": "{{value}} horas",
"CJK Font": "Fonte CJK",
"Clear Search": "Limpar Pesquisa"
}
@@ -265,5 +265,13 @@
"Failed to parse the EPUB file.": "Не удалось разобрать файл EPUB.",
"This book format is not supported.": "Этот формат книги не поддерживается.",
"Unable to fetch the translation. Please log in first and try again.": "Не удалось получить перевод. Пожалуйста, войдите в систему и попробуйте снова.",
"Group": "Группа"
"Group": "Группа",
"Always on Top": "Всегда сверху",
"No Timeout": "Нет таймаута",
"{{value}} minute": "{{value}} минута",
"{{value}} minutes": "{{value}} минуты",
"{{value}} hour": "{{value}} час",
"{{value}} hours": "{{value}} часа",
"CJK Font": "CJK Шрифт",
"Clear Search": "Очистить поиск"
}
@@ -265,5 +265,13 @@
"Failed to parse the EPUB file.": "EPUB dosyası ayrıştırılamadı.",
"This book format is not supported.": "Bu kitap formatı desteklenmiyor.",
"Unable to fetch the translation. Please log in first and try again.": "Çeviri alınamıyor. Lütfen önce giriş yapın ve tekrar deneyin.",
"Group": "Grup"
"Group": "Grup",
"Always on Top": "Her Zaman Üstte",
"No Timeout": "Zaman Aşımı Yok",
"{{value}} minute": "{{value}} dakika",
"{{value}} minutes": "{{value}} dakika",
"{{value}} hour": "{{value}} saat",
"{{value}} hours": "{{value}} saat",
"CJK Font": "CJK Yazı Tipi",
"Clear Search": "Aramayı Temizle"
}
@@ -265,5 +265,13 @@
"Failed to parse the EPUB file.": "Не вдалося розібрати файл EPUB.",
"This book format is not supported.": "Цей формат книги не підтримується.",
"Unable to fetch the translation. Please log in first and try again.": "Не вдалося отримати переклад. Будь ласка, спочатку увійдіть у систему та спробуйте ще раз.",
"Group": "Група"
"Group": "Група",
"Always on Top": "Завжди зверху",
"No Timeout": "Без тайм-ауту",
"{{value}} minute": "{{value}} хвилина",
"{{value}} minutes": "{{value}} хвилин",
"{{value}} hour": "{{value}} година",
"{{value}} hours": "{{value}} години",
"CJK Font": "CJK шрифт",
"Clear Search": "Очистити пошук"
}
@@ -265,5 +265,13 @@
"Failed to parse the EPUB file.": "Không thể phân tích tệp EPUB.",
"This book format is not supported.": "Định dạng sách này không được hỗ trợ.",
"Unable to fetch the translation. Please log in first and try again.": "Không thể tải dịch vụ. Vui lòng đăng nhập trước và thử lại.",
"Group": "Nhóm"
"Group": "Nhóm",
"Always on Top": "Luôn ở trên cùng",
"No Timeout": "Không có thời gian chờ",
"{{value}} minute": "{{value}} phút",
"{{value}} minutes": "{{value}} phút",
"{{value}} hour": "{{value}} giờ",
"{{value}} hours": "{{value}} giờ",
"CJK Font": "Phông chữ CJK",
"Clear Search": "Xóa tìm kiếm"
}
@@ -230,7 +230,7 @@
"Maximum Column Height": "最大列高",
"Maximum Column Width": "最大列宽",
"Continuous Scroll": "连续滚动",
"Fullscreen": "全屏",
"Fullscreen": "全屏显示",
"No supported files found. Supported formats: {{formats}}": "未找到支持的文件。支持的格式:{{formats}}",
"Drop to Import Books": "拖放导入书籍",
"Custom": "自定义",
@@ -265,5 +265,13 @@
"Failed to parse the EPUB file.": "无法解析 EPUB 文件。",
"This book format is not supported.": "不支持此书籍格式。",
"Unable to fetch the translation. Please log in first and try again.": "无法获取翻译。请登录后重试。",
"Group": "分组"
"Group": "分组",
"Always on Top": "窗口置顶",
"No Timeout": "关闭定时",
"{{value}} minute": "{{value}}分钟",
"{{value}} minutes": "{{value}}分钟",
"{{value}} hour": "{{value}}小时",
"{{value}} hours": "{{value}}小时",
"CJK Font": "中文字体",
"Clear Search": "清除搜索"
}
@@ -265,5 +265,13 @@
"Failed to parse the EPUB file.": "無法解析 EPUB 文件。",
"This book format is not supported.": "不支援此書籍格式。",
"Unable to fetch the translation. Please log in first and try again.": "無法獲取翻譯。請先登入然後重試。",
"Group": "分組"
"Group": "分組",
"Always on Top": "視窗置頂",
"No Timeout": "關閉定時",
"{{value}} minute": "{{value}}分鐘",
"{{value}} minutes": "{{value}}分鐘",
"{{value}} hour": "{{value}}小時",
"{{value}} hours": "{{value}}小時",
"CJK Font": "中文字體",
"Clear Search": "清除搜索"
}
+20
View File
@@ -1,5 +1,25 @@
{
"releases": {
"0.9.31": {
"date": "2025-04-08",
"notes": [
"iOS: Text-to-Speech (TTS) now works in the background",
"Android: Import files from native file manager",
"Desktop: Add option to keep window on top",
"Vertical layout for highlighting tools",
"Added options to auto-stop TTS after timeout",
"Added book search functionality in bookshelf",
"Improved font support for CJK users"
]
},
"0.9.30": {
"date": "2025-04-05",
"notes": [
"Added fixed storage quata when running the app in self-hosted mode",
"Link colors will only change when you override fonts",
"Added cache for native files for better performance"
]
},
"0.9.29": {
"date": "2025-04-02",
"notes": [
+15 -15
View File
@@ -1,6 +1,6 @@
[package]
name = "Readest"
version = "0.2.1"
version = "0.2.2"
description = "Your online library"
authors = ["Bilingify LLC"]
license = ""
@@ -15,7 +15,7 @@ name = "readestlib"
crate-type = ["staticlib", "cdylib", "lib"]
[build-dependencies]
tauri-build = { version = "2.0.3", features = [] }
tauri-build = { version = "2.1.1", features = [] }
[dependencies]
serde_json = "1.0"
@@ -31,20 +31,20 @@ reqwest = { version = "0.12", default-features = false, features = [
"stream",
] }
# FIXME: remove the devtools feature in production
tauri = { version = "2.4.0", features = [ "protocol-asset", "devtools"] }
tauri-build = "2.1.0"
tauri = { version = "2.4.1", features = [ "protocol-asset", "devtools"] }
tauri-build = "2.1.1"
tauri-plugin-log = "2.3.1"
tauri-plugin-fs = "2.2.0"
tauri-plugin-dialog = "2.2.0"
tauri-plugin-fs = "2.2.1"
tauri-plugin-dialog = "2.2.1"
tauri-plugin-os = "2.2.1"
tauri-plugin-http = "2.4.2"
tauri-plugin-shell = "2.2.0"
tauri-plugin-process = "2.2.0"
tauri-plugin-http = "2.4.3"
tauri-plugin-shell = "2.2.1"
tauri-plugin-process = "2.2.1"
tauri-plugin-oauth = "2"
tauri-plugin-opener = "2.2.2"
tauri-plugin-deep-link = "2"
tauri-plugin-opener = "2.2.6"
tauri-plugin-deep-link = "2.2.1"
tauri-plugin-sign-in-with-apple = "1.0.2"
tauri-plugin-haptics = "2.2.3"
tauri-plugin-haptics = "2.2.4"
tauri-plugin-native-bridge = { path = "./plugins/tauri-plugin-native-bridge" }
[target."cfg(target_os = \"macos\")".dependencies]
@@ -54,7 +54,7 @@ rand = "0.8"
[target.'cfg(any(target_os = "macos", windows, target_os = "linux"))'.dependencies]
tauri-plugin-cli = "2"
tauri-plugin-single-instance = "2.2.0"
tauri-plugin-updater = "2"
tauri-plugin-window-state = "2.2.0"
tauri-plugin-single-instance = "2.2.3"
tauri-plugin-updater = "2.7.0"
tauri-plugin-window-state = "2.2.2"
font-enumeration = "0.9.0"
@@ -63,6 +63,7 @@
"core:window:allow-start-dragging",
"core:window:allow-toggle-maximize",
"core:window:allow-set-fullscreen",
"core:window:allow-set-always-on-top",
"log:default",
"shell:default",
"process:default",
@@ -2,6 +2,7 @@ const COMMANDS: &[&str] = &[
"auth_with_safari",
"auth_with_custom_tab",
"copy_uri_to_path",
"use_background_audio",
];
fn main() {
@@ -1,4 +1,6 @@
import AuthenticationServices
import AVFoundation
import MediaPlayer
import SwiftRs
import Tauri
import UIKit
@@ -8,9 +10,33 @@ class SafariAuthRequestArgs: Decodable {
let authUrl: String
}
class UseBackgroundAudioRequestArgs: Decodable {
let enabled: Bool
}
class NativeBridgePlugin: Plugin {
private var authSession: ASWebAuthenticationSession?
@objc public func use_background_audio(_ invoke: Invoke) {
do {
let args = try invoke.parseArgs(UseBackgroundAudioRequestArgs.self)
let enabled = args.enabled
let session = AVAudioSession.sharedInstance()
if enabled {
try session.setCategory(.playback, mode: .default, options: [.mixWithOthers])
try session.setActive(true)
print("AVAudioSession activated")
} else {
try session.setActive(false)
MPNowPlayingInfoCenter.default().nowPlayingInfo = nil
print("AVAudioSession deactivated")
}
invoke.resolve()
} catch {
print("Failed to set up audio session:", error)
}
}
@objc public func auth_with_safari(_ invoke: Invoke) throws {
let args = try invoke.parseArgs(SafariAuthRequestArgs.self)
let authUrl = URL(string: args.authUrl)!
@@ -0,0 +1,13 @@
# Automatically generated - DO NOT EDIT!
"$schema" = "../../schemas/schema.json"
[[permission]]
identifier = "allow-use-background-audio"
description = "Enables the use_background_audio command without any pre-configured scope."
commands.allow = ["use_background_audio"]
[[permission]]
identifier = "deny-use-background-audio"
description = "Denies the use_background_audio command without any pre-configured scope."
commands.deny = ["use_background_audio"]
@@ -2,9 +2,12 @@
Default permissions for the plugin
#### This default permission set includes the following:
- `allow-auth-with-safari`
- `allow-auth-with-custom-tab`
- `allow-copy-uri-to-path`
- `allow-use-background-audio`
## Permission Table
@@ -90,6 +93,32 @@ Enables the copy_uri_to_path command without any pre-configured scope.
Denies the copy_uri_to_path command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`native-bridge:allow-use-background-audio`
</td>
<td>
Enables the use_background_audio command without any pre-configured scope.
</td>
</tr>
<tr>
<td>
`native-bridge:deny-use-background-audio`
</td>
<td>
Denies the use_background_audio command without any pre-configured scope.
</td>
</tr>
</table>
@@ -1,3 +1,3 @@
[default]
description = "Default permissions for the plugin"
permissions = ["allow-auth-with-safari", "allow-auth-with-custom-tab", "allow-copy-uri-to-path"]
permissions = ["allow-auth-with-safari", "allow-auth-with-custom-tab", "allow-copy-uri-to-path", "allow-use-background-audio"]
@@ -297,37 +297,56 @@
{
"description": "Enables the auth_with_custom_tab command without any pre-configured scope.",
"type": "string",
"const": "allow-auth-with-custom-tab"
"const": "allow-auth-with-custom-tab",
"markdownDescription": "Enables the auth_with_custom_tab command without any pre-configured scope."
},
{
"description": "Denies the auth_with_custom_tab command without any pre-configured scope.",
"type": "string",
"const": "deny-auth-with-custom-tab"
"const": "deny-auth-with-custom-tab",
"markdownDescription": "Denies the auth_with_custom_tab command without any pre-configured scope."
},
{
"description": "Enables the auth_with_safari command without any pre-configured scope.",
"type": "string",
"const": "allow-auth-with-safari"
"const": "allow-auth-with-safari",
"markdownDescription": "Enables the auth_with_safari command without any pre-configured scope."
},
{
"description": "Denies the auth_with_safari command without any pre-configured scope.",
"type": "string",
"const": "deny-auth-with-safari"
"const": "deny-auth-with-safari",
"markdownDescription": "Denies the auth_with_safari command without any pre-configured scope."
},
{
"description": "Enables the copy_uri_to_path command without any pre-configured scope.",
"type": "string",
"const": "allow-copy-uri-to-path"
"const": "allow-copy-uri-to-path",
"markdownDescription": "Enables the copy_uri_to_path command without any pre-configured scope."
},
{
"description": "Denies the copy_uri_to_path command without any pre-configured scope.",
"type": "string",
"const": "deny-copy-uri-to-path"
"const": "deny-copy-uri-to-path",
"markdownDescription": "Denies the copy_uri_to_path command without any pre-configured scope."
},
{
"description": "Default permissions for the plugin",
"description": "Enables the use_background_audio command without any pre-configured scope.",
"type": "string",
"const": "default"
"const": "allow-use-background-audio",
"markdownDescription": "Enables the use_background_audio command without any pre-configured scope."
},
{
"description": "Denies the use_background_audio command without any pre-configured scope.",
"type": "string",
"const": "deny-use-background-audio",
"markdownDescription": "Denies the use_background_audio command without any pre-configured scope."
},
{
"description": "Default permissions for the plugin\n#### This default permission set includes:\n\n- `allow-auth-with-safari`\n- `allow-auth-with-custom-tab`\n- `allow-copy-uri-to-path`\n- `allow-use-background-audio`",
"type": "string",
"const": "default",
"markdownDescription": "Default permissions for the plugin\n#### This default permission set includes:\n\n- `allow-auth-with-safari`\n- `allow-auth-with-custom-tab`\n- `allow-copy-uri-to-path`\n- `allow-use-background-audio`"
}
]
}
@@ -27,3 +27,11 @@ pub(crate) async fn copy_uri_to_path<R: Runtime>(
) -> Result<CopyURIResponse> {
app.native_bridge().copy_uri_to_path(payload)
}
#[command]
pub(crate) async fn use_background_audio<R: Runtime>(
app: AppHandle<R>,
payload: UseBackgroundAudioRequest,
) -> Result<()> {
app.native_bridge().use_background_audio(payload)
}
@@ -25,4 +25,8 @@ impl<R: Runtime> NativeBridge<R> {
pub fn copy_uri_to_path(&self, _payload: CopyURIRequest) -> crate::Result<CopyURIResponse> {
Err(crate::Error::UnsupportedPlatformError)
}
pub fn use_background_audio(&self, _payload: UseBackgroundAudioRequest) -> crate::Result<()> {
Err(crate::Error::UnsupportedPlatformError)
}
}
@@ -40,6 +40,7 @@ pub fn init<R: Runtime>() -> TauriPlugin<R> {
commands::auth_with_safari,
commands::auth_with_custom_tab,
commands::copy_uri_to_path,
commands::use_background_audio,
])
.setup(|app, api| {
#[cfg(mobile)]
@@ -47,3 +47,11 @@ impl<R: Runtime> NativeBridge<R> {
.map_err(Into::into)
}
}
impl<R: Runtime> NativeBridge<R> {
pub fn use_background_audio(&self, payload: UseBackgroundAudioRequest) -> crate::Result<()> {
self.0
.run_mobile_plugin("use_background_audio", payload)
.map_err(Into::into)
}
}
@@ -25,3 +25,9 @@ pub struct CopyURIResponse {
pub success: bool,
pub error: Option<String>,
}
#[derive(Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct UseBackgroundAudioRequest {
pub enabled: bool,
}
@@ -10,6 +10,7 @@ import { useEnv } from '@/context/EnvContext';
import { useLibraryStore } from '@/store/libraryStore';
import { useTranslation } from '@/hooks/useTranslation';
import { navigateToLibrary, navigateToReader } from '@/utils/nav';
import { formatAuthors, formatTitle } from '@/utils/book';
import { isMd5 } from '@/utils/md5';
import Alert from '@/components/Alert';
@@ -49,6 +50,7 @@ const Bookshelf: React.FC<BookshelfProps> = ({
const [showSelectModeActions, setShowSelectModeActions] = useState(false);
const [showDeleteAlert, setShowDeleteAlert] = useState(false);
const [showGroupingModal, setShowGroupingModal] = useState(false);
const [queryTerm, setQueryTerm] = useState<string | null>(null);
const [navBooksGroup, setNavBooksGroup] = useState<BooksGroup | null>(null);
const [importBookUrl] = useState(searchParams?.get('url') || '');
const isImportingBook = useRef(false);
@@ -86,6 +88,12 @@ const Bookshelf: React.FC<BookshelfProps> = ({
useEffect(() => {
const group = searchParams?.get('group') || '';
const query = searchParams?.get('q') || '';
if (query) {
setQueryTerm(query);
} else {
setQueryTerm(null);
}
if (group) {
const booksGroup = allBookshelfItems.find(
(item) => 'name' in item && item.id === group,
@@ -93,11 +101,11 @@ const Bookshelf: React.FC<BookshelfProps> = ({
if (booksGroup) {
setNavBooksGroup(booksGroup);
} else {
navigateToLibrary(router);
navigateToLibrary(router, query ? `q=${query}` : undefined);
}
} else {
setNavBooksGroup(null);
navigateToLibrary(router);
navigateToLibrary(router, query ? `q=${query}` : undefined);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [searchParams, libraryBooks, showGroupingModal]);
@@ -137,6 +145,18 @@ const Bookshelf: React.FC<BookshelfProps> = ({
};
const currentBookshelfItems = navBooksGroup ? navBooksGroup.books : allBookshelfItems;
const bookFilter = (item: Book, queryTerm: string) => {
if (item.deletedAt) return false;
const searchTerm = new RegExp(queryTerm, 'i');
const title = formatTitle(item.title);
const authors = formatAuthors(item.author);
return searchTerm.test(title) || searchTerm.test(authors);
};
const filteredBookshelfItems = currentBookshelfItems.filter((item) => {
if ('name' in item) return item.books.some((book) => bookFilter(book, queryTerm || ''));
else if (queryTerm) return bookFilter(item, queryTerm);
return true;
});
return (
<div className='bookshelf'>
@@ -146,7 +166,7 @@ const Bookshelf: React.FC<BookshelfProps> = ({
'grid-cols-3 sm:grid-cols-4 md:grid-cols-6 xl:grid-cols-8 2xl:grid-cols-12',
)}
>
{currentBookshelfItems.map((item, index) => (
{filteredBookshelfItems.map((item, index) => (
<BookshelfItem
key={`library-item-${index}`}
item={item}
@@ -1,5 +1,5 @@
import clsx from 'clsx';
import { useRouter } from 'next/navigation';
import { useRouter, useSearchParams } from 'next/navigation';
import { navigateToLibrary, navigateToReader } from '@/utils/nav';
import { useEnv } from '@/context/EnvContext';
import { useLibraryStore } from '@/store/libraryStore';
@@ -94,6 +94,7 @@ const BookshelfItem: React.FC<BookshelfItemProps> = ({
}) => {
const _ = useTranslation();
const router = useRouter();
const searchParams = useSearchParams();
const { envConfig, appService } = useEnv();
const { settings } = useSettingsStore();
const { updateBook } = useLibraryStore();
@@ -133,7 +134,9 @@ const BookshelfItem: React.FC<BookshelfItemProps> = ({
if (isSelectMode) {
toggleSelection(group.id);
} else {
navigateToLibrary(router, `group=${group.id}`);
const params = new URLSearchParams(searchParams?.toString());
params.set('group', group.id);
navigateToLibrary(router, `${params.toString()}`);
}
};
@@ -1,17 +1,18 @@
import clsx from 'clsx';
import React, { useEffect, useRef } from 'react';
import React, { useCallback, useEffect, useRef, useState } from 'react';
import { useRouter, useSearchParams } from 'next/navigation';
import { FaSearch } from 'react-icons/fa';
import { PiPlus } from 'react-icons/pi';
import { PiSelectionAllDuotone } from 'react-icons/pi';
import { MdOutlineMenu } from 'react-icons/md';
import { MdArrowBackIosNew } from 'react-icons/md';
import { MdOutlineMenu, MdArrowBackIosNew } from 'react-icons/md';
import { IoMdCloseCircle } from 'react-icons/io';
import { useEnv } from '@/context/EnvContext';
import { useTranslation } from '@/hooks/useTranslation';
import { useResponsiveSize } from '@/hooks/useResponsiveSize';
import { useTrafficLightStore } from '@/store/trafficLightStore';
import { navigateToLibrary } from '@/utils/nav';
import { throttle } from '@/utils/throttle';
import WindowButtons from '@/components/WindowButtons';
import Dropdown from '@/components/Dropdown';
import SettingsMenu from './SettingsMenu';
@@ -40,6 +41,7 @@ const LibraryHeader: React.FC<LibraryHeaderProps> = ({
setTrafficLightVisibility,
cleanupTrafficLightListeners,
} = useTrafficLightStore();
const [searchQuery, setSearchQuery] = useState(searchParams?.get('q') ?? '');
const headerRef = useRef<HTMLDivElement>(null);
const iconSize16 = useResponsiveSize(16);
@@ -49,6 +51,26 @@ const LibraryHeader: React.FC<LibraryHeaderProps> = ({
onToggleSelectMode,
});
// eslint-disable-next-line react-hooks/exhaustive-deps
const throttledUpdateQueryParam = useCallback(
throttle((value: string) => {
const params = new URLSearchParams(searchParams?.toString());
if (value) {
params.set('q', value);
} else {
params.delete('q');
}
router.push(`?${params.toString()}`);
}, 1000),
[searchParams],
);
const handleSearchChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const newQuery = e.target.value;
setSearchQuery(newQuery);
throttledUpdateQueryParam(newQuery);
};
useEffect(() => {
if (!appService?.hasTrafficLight) return;
@@ -93,7 +115,9 @@ const LibraryHeader: React.FC<LibraryHeaderProps> = ({
</span>
<input
type='text'
value={searchQuery}
placeholder={_('Search Books...')}
onChange={handleSearchChange}
spellCheck='false'
className={clsx(
'input rounded-badge bg-base-300/50 h-9 w-full pl-10 pr-10 sm:h-7',
@@ -103,6 +127,19 @@ const LibraryHeader: React.FC<LibraryHeaderProps> = ({
/>
</div>
<div className='absolute right-4 flex items-center space-x-2 text-gray-500 sm:space-x-4'>
{searchQuery && (
<button
type='button'
onClick={() => {
setSearchQuery('');
throttledUpdateQueryParam('');
}}
className='text-gray-400 hover:text-gray-600'
aria-label={_('Clear Search')}
>
<IoMdCloseCircle className='h-4 w-4' />
</button>
)}
<span className='bg-base-content/50 mx-2 h-4 w-[0.5px]'></span>
<Dropdown
className='exclude-title-bar-mousedown dropdown-bottom flex h-6 cursor-pointer justify-center'
@@ -14,7 +14,7 @@ import { useSettingsStore } from '@/store/settingsStore';
import { useTranslation } from '@/hooks/useTranslation';
import { getStoragePlanData } from '@/utils/access';
import { navigateToLogin, navigateToProfile } from '@/utils/nav';
import { tauriHandleToggleFullScreen } from '@/utils/window';
import { tauriHandleSetAlwaysOnTop, tauriHandleToggleFullScreen } from '@/utils/window';
import { QuotaType } from '@/types/user';
import MenuItem from '@/components/MenuItem';
import Quota from '@/components/Quota';
@@ -32,6 +32,7 @@ const SettingsMenu: React.FC<BookMenuProps> = ({ setIsDropdownOpen }) => {
const [quotas, setQuotas] = React.useState<QuotaType[]>([]);
const [isAutoUpload, setIsAutoUpload] = useState(settings.autoUpload);
const [isAutoCheckUpdates, setIsAutoCheckUpdates] = useState(settings.autoCheckUpdates);
const [isAlwaysOnTop, setIsAlwaysOnTop] = useState(settings.alwaysOnTop);
const [isScreenWakeLock, setIsScreenWakeLock] = useState(settings.screenWakeLock);
const [isAutoImportBooksOnOpen, setIsAutoImportBooksOnOpen] = useState(
settings.autoImportBooksOnOpen,
@@ -66,6 +67,15 @@ const SettingsMenu: React.FC<BookMenuProps> = ({ setIsDropdownOpen }) => {
setIsDropdownOpen?.(false);
};
const toggleAlwaysOnTop = () => {
settings.alwaysOnTop = !settings.alwaysOnTop;
setSettings(settings);
saveSettings(envConfig, settings);
setIsAlwaysOnTop(settings.alwaysOnTop);
tauriHandleSetAlwaysOnTop(settings.alwaysOnTop);
setIsDropdownOpen?.(false);
};
const toggleAutoUploadBooks = () => {
settings.autoUpload = !settings.autoUpload;
setSettings(settings);
@@ -174,13 +184,20 @@ const SettingsMenu: React.FC<BookMenuProps> = ({ setIsDropdownOpen }) => {
onClick={toggleAutoCheckUpdates}
/>
)}
<hr className='border-base-200 my-1' />
{appService?.hasWindow && <MenuItem label={_('Fullscreen')} onClick={handleFullScreen} />}
{appService?.hasWindow && (
<MenuItem
label={_('Always on Top')}
icon={isAlwaysOnTop ? <MdCheck className='text-base-content' /> : undefined}
onClick={toggleAlwaysOnTop}
/>
)}
<MenuItem
label={_('Keep Screen Awake')}
icon={isScreenWakeLock ? <MdCheck className='text-base-content' /> : undefined}
onClick={toggleScreenWakeLock}
/>
<hr className='border-base-200 my-1' />
{appService?.hasWindow && <MenuItem label={_('Fullscreen')} onClick={handleFullScreen} />}
<MenuItem label={_('Reload Page')} onClick={handleReloadPage} />
<hr className='border-base-200 my-1' />
{isWebApp && <MenuItem label={_('Download Readest')} onClick={downloadReadest} />}
+18 -8
View File
@@ -31,7 +31,7 @@ import { useDemoBooks } from './hooks/useDemoBooks';
import { useBooksSync } from './hooks/useBooksSync';
import { useScreenWakeLock } from '@/hooks/useScreenWakeLock';
import { useOpenWithBooks } from '@/hooks/useOpenWithBooks';
import { tauriQuitApp } from '@/utils/window';
import { tauriHandleSetAlwaysOnTop, tauriQuitApp } from '@/utils/window';
import { AboutWindow } from '@/components/AboutWindow';
import { Toast } from '@/components/Toast';
@@ -104,6 +104,9 @@ const LibraryPageContent = ({ searchParams }: { searchParams: ReadonlyURLSearchP
await checkForAppUpdates(_);
}
};
if (settings.alwaysOnTop) {
tauriHandleSetAlwaysOnTop(settings.alwaysOnTop);
}
doCheckAppUpdates();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [settings]);
@@ -161,9 +164,11 @@ const LibraryPageContent = ({ searchParams }: { searchParams: ReadonlyURLSearchP
useEffect(() => {
const libraryPage = document.querySelector('.library-page');
libraryPage?.addEventListener('dragover', handleDragOver as unknown as EventListener);
libraryPage?.addEventListener('dragleave', handleDragLeave as unknown as EventListener);
libraryPage?.addEventListener('drop', handleDrop as unknown as EventListener);
if (!appService?.isMobile) {
libraryPage?.addEventListener('dragover', handleDragOver as unknown as EventListener);
libraryPage?.addEventListener('dragleave', handleDragLeave as unknown as EventListener);
libraryPage?.addEventListener('drop', handleDrop as unknown as EventListener);
}
if (isTauriAppPlatform()) {
const unlisten = getCurrentWebview().onDragDropEvent((event) => {
@@ -182,9 +187,11 @@ const LibraryPageContent = ({ searchParams }: { searchParams: ReadonlyURLSearchP
}
return () => {
libraryPage?.removeEventListener('dragover', handleDragOver as unknown as EventListener);
libraryPage?.removeEventListener('dragleave', handleDragLeave as unknown as EventListener);
libraryPage?.removeEventListener('drop', handleDrop as unknown as EventListener);
if (!appService?.isMobile) {
libraryPage?.removeEventListener('dragover', handleDragOver as unknown as EventListener);
libraryPage?.removeEventListener('dragleave', handleDragLeave as unknown as EventListener);
libraryPage?.removeEventListener('drop', handleDrop as unknown as EventListener);
}
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [pageRef.current]);
@@ -332,7 +339,10 @@ const LibraryPageContent = ({ searchParams }: { searchParams: ReadonlyURLSearchP
};
const selectFilesTauri = async () => {
return (await appService?.selectFiles(_('Select Books'), SUPPORTED_FILE_EXTS)) || [];
const exts = appService?.isAndroidApp ? [] : SUPPORTED_FILE_EXTS;
const files = (await appService?.selectFiles(_('Select Books'), exts)) || [];
// Cannot filter out files on Android since some content providers may not return the file name
return files;
};
const selectFilesWeb = () => {
@@ -28,7 +28,7 @@ const BooksGrid: React.FC<BooksGridProps> = ({ bookKeys, onCloseBook }) => {
const { appService } = useEnv();
const { getConfig, getBookData } = useBookDataStore();
const { getProgress, getViewState, getViewSettings } = useReaderStore();
const { sideBarBookKey } = useSidebarStore();
const { isSideBarVisible, sideBarBookKey } = useSidebarStore();
const { isFontLayoutSettingsDialogOpen, setFontLayoutSettingsDialogOpen } = useSettingsStore();
const gridTemplate = getGridTemplate(bookKeys.length, window.innerWidth / window.innerHeight);
@@ -68,7 +68,10 @@ const BooksGrid: React.FC<BooksGridProps> = ({ bookKeys, onCloseBook }) => {
<div
id={`gridcell-${bookKey}`}
key={bookKey}
className={`${appService?.hasRoundedWindow ? 'rounded-window' : ''} relative h-full w-full overflow-hidden`}
className={clsx(
'relative h-full w-full overflow-hidden',
!isSideBarVisible && appService?.hasRoundedWindow && 'rounded-window',
)}
>
{isBookmarked && <Ribbon width={`${horizontalGapPercent}%`} />}
<HeaderBar
@@ -58,9 +58,9 @@ const FoliateViewer: React.FC<{
const { detail } = event as CustomEvent;
detail.data = Promise.resolve(detail.data)
.then((data) => {
const viewSettings = getViewSettings(bookKey)!;
const viewSettings = getViewSettings(bookKey);
if (detail.type === 'text/css') return transformStylesheet(data);
if (detail.type === 'application/xhtml+xml') {
if (viewSettings && detail.type === 'application/xhtml+xml') {
const ctx = {
bookKey,
viewSettings,
@@ -78,7 +78,7 @@ const FoliateViewer: React.FC<{
const docLoadHandler = (event: Event) => {
const detail = (event as CustomEvent).detail;
console.log('doc loaded:', detail);
console.log('doc index loaded:', detail.index);
if (detail.doc) {
const writingDir = viewRef.current?.renderer.setStyles && getDirection(detail.doc);
const viewSettings = getViewSettings(bookKey)!;
@@ -13,10 +13,12 @@ import { useScreenWakeLock } from '@/hooks/useScreenWakeLock';
import { AboutWindow } from '@/components/AboutWindow';
import { Toast } from '@/components/Toast';
import ReaderContent from './ReaderContent';
import { useSidebarStore } from '@/store/sidebarStore';
const Reader: React.FC<{ ids?: string }> = ({ ids }) => {
const { envConfig, appService } = useEnv();
const { settings, setSettings } = useSettingsStore();
const { isSideBarVisible } = useSidebarStore();
const { getVisibleLibrary, setLibrary } = useLibraryStore();
const isInitiating = useRef(false);
@@ -45,7 +47,7 @@ const Reader: React.FC<{ ids?: string }> = ({ ids }) => {
<div
className={clsx(
`reader-page bg-base-100 text-base-content select-none`,
appService?.hasRoundedWindow && 'rounded-window',
!isSideBarVisible && appService?.hasRoundedWindow && 'rounded-window',
)}
>
<Suspense>
@@ -1,3 +1,4 @@
import clsx from 'clsx';
import React from 'react';
import Popup from '@/components/Popup';
import PopupButton from './PopupButton';
@@ -8,6 +9,7 @@ import { useResponsiveSize } from '@/hooks/useResponsiveSize';
interface AnnotationPopupProps {
dir: 'ltr' | 'rtl';
isVertical: boolean;
buttons: Array<{ tooltipText: string; Icon: React.ElementType; onClick: () => void }>;
position: Position;
trianglePosition: Position;
@@ -24,6 +26,7 @@ const OPTIONS_PADDING_PIX = 16;
const AnnotationPopup: React.FC<AnnotationPopupProps> = ({
dir,
isVertical,
buttons,
position,
trianglePosition,
@@ -39,17 +42,20 @@ const AnnotationPopup: React.FC<AnnotationPopupProps> = ({
return (
<div dir={dir}>
<Popup
width={popupWidth}
height={popupHeight}
width={isVertical ? popupHeight : popupWidth}
height={isVertical ? popupWidth : popupHeight}
position={position}
trianglePosition={trianglePosition}
className='selection-popup bg-gray-600 px-4 text-white'
className='selection-popup bg-gray-600 text-white'
triangleClassName='text-gray-600'
>
<div
className='selection-buttons flex items-center justify-between'
className={clsx(
'selection-buttons flex items-center justify-between p-2',
isVertical ? 'flex-col' : 'flex-row',
)}
style={{
height: popupHeight,
height: isVertical ? popupWidth : popupHeight,
}}
>
{buttons.map((button, index) => (
@@ -65,15 +71,27 @@ const AnnotationPopup: React.FC<AnnotationPopupProps> = ({
</Popup>
{highlightOptionsVisible && (
<HighlightOptions
isVertical={isVertical}
style={{
width: `${popupWidth}px`,
height: `${popupHeight}px`,
left: `${position.point.x}px`,
top: `${
position.point.y +
(highlightOptionsHeightPx + highlightOptionsPaddingPx) *
(trianglePosition.dir === 'up' ? -1 : 1)
}px`,
width: `${isVertical ? popupHeight : popupWidth}px`,
height: `${isVertical ? popupWidth : popupHeight}px`,
...(isVertical
? {
left: `${
position.point.x +
(highlightOptionsHeightPx + highlightOptionsPaddingPx) *
(trianglePosition.dir === 'left' ? -1 : 1)
}px`,
top: `${position.point.y}px`,
}
: {
left: `${position.point.x}px`,
top: `${
position.point.y +
(highlightOptionsHeightPx + highlightOptionsPaddingPx) *
(trianglePosition.dir === 'up' ? -1 : 1)
}px`,
}),
}}
selectedStyle={selectedStyle}
selectedColor={selectedColor}
@@ -259,8 +259,8 @@ const Annotator: React.FC<{ bookKey: string }> = ({ bookKey }) => {
const annotPopupPos = getPopupPosition(
triangPos,
rect,
annotPopupWidth,
annotPopupHeight,
viewSettings.vertical ? annotPopupHeight : annotPopupWidth,
viewSettings.vertical ? annotPopupWidth : annotPopupHeight,
popupPadding,
);
if (isTextSelected.current && annotPopupPos.dir === 'down' && osPlatform === 'android') {
@@ -577,6 +577,7 @@ const Annotator: React.FC<{ bookKey: string }> = ({ bookKey }) => {
{showAnnotPopup && trianglePosition && annotPopupPosition && (
<AnnotationPopup
dir={viewSettings.rtl ? 'rtl' : 'ltr'}
isVertical={viewSettings.vertical}
buttons={buttons}
position={annotPopupPosition}
trianglePosition={trianglePosition}
@@ -9,6 +9,7 @@ const styles = ['highlight', 'underline', 'squiggly'] as HighlightStyle[];
const colors = ['red', 'violet', 'blue', 'green', 'yellow'] as HighlightColor[];
interface HighlightOptionsProps {
isVertical: boolean;
style: React.CSSProperties;
selectedStyle: HighlightStyle;
selectedColor: HighlightColor;
@@ -17,6 +18,7 @@ interface HighlightOptionsProps {
const HighlightOptions: React.FC<HighlightOptionsProps> = ({
style,
isVertical,
selectedStyle: _selectedStyle,
selectedColor: _selectedColor,
onHandleHighlight,
@@ -44,13 +46,22 @@ const HighlightOptions: React.FC<HighlightOptionsProps> = ({
onHandleHighlight(true);
};
return (
<div className='highlight-options absolute flex items-center justify-between' style={style}>
<div className='flex gap-2' style={{ height: size28 }}>
<div
className={clsx(
'highlight-options absolute flex items-center justify-between',
isVertical ? 'flex-col' : 'flex-row',
)}
style={style}
>
<div
className={clsx('flex gap-2', isVertical ? 'flex-col' : 'flex-row')}
style={isVertical ? { width: size28 } : { height: size28 }}
>
{styles.map((style) => (
<button
key={style}
onClick={() => handleSelectStyle(style)}
className={`flex items-center justify-center rounded-full bg-gray-700 p-0`}
className='flex items-center justify-center rounded-full bg-gray-700 p-0'
style={{ width: size28, height: size28, minHeight: size28 }}
>
<div
@@ -78,8 +89,11 @@ const HighlightOptions: React.FC<HighlightOptionsProps> = ({
</div>
<div
className='flex items-center justify-center gap-2 rounded-3xl bg-gray-700 px-2'
style={{ height: size28 }}
className={clsx(
'flex items-center justify-center gap-2 rounded-3xl bg-gray-700',
isVertical ? 'flex-col py-2' : 'flex-row px-2',
)}
style={isVertical ? { width: size28 } : { height: size28 }}
>
{colors.map((color) => (
<button
@@ -20,7 +20,7 @@ const PopupButton: React.FC<PopupButtonProps> = ({ showTooltip, tooltipText, Ico
>
<button
onClick={handleClick}
className='my-2 flex h-8 min-h-8 w-8 items-center justify-center p-0'
className='flex h-8 min-h-8 w-8 items-center justify-center p-0'
>
<Icon />
</button>
@@ -41,7 +41,10 @@ const FontDropdown: React.FC<DropdownProps> = ({
</button>
<ul
tabIndex={0}
className='dropdown-content bgcolor-base-200 no-triangle menu rounded-box absolute right-[-32px] z-[1] mt-4 w-44 shadow sm:right-0'
className={clsx(
'dropdown-content bgcolor-base-200 no-triangle menu rounded-box absolute right-[-32px] z-[1] mt-4 w-44 shadow sm:right-0',
'inline max-h-80 overflow-y-scroll',
)}
>
{options.map(({ option, label }) => (
<li key={option} onClick={() => onSelect(option)}>
@@ -3,6 +3,8 @@ import React, { useEffect, useState } from 'react';
import {
ANDROID_FONTS,
CJK_FONTS_PATTENS,
CJK_NAMES_PATTENS,
IOS_FONTS,
LINUX_FONTS,
MACOS_FONTS,
@@ -14,7 +16,7 @@ import {
import { useReaderStore } from '@/store/readerStore';
import { useTranslation } from '@/hooks/useTranslation';
import { useEnv } from '@/context/EnvContext';
import { getOSPlatform } from '@/utils/misc';
import { getOSPlatform, isCJKEnv } from '@/utils/misc';
import { getSysFontsList } from '@/utils/font';
import { isTauriAppPlatform } from '@/services/environment';
import { saveViewSettings } from '../../utils/viewSettingsHelper';
@@ -63,8 +65,9 @@ const FontFace = ({
const FontPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
const _ = useTranslation();
const { envConfig, appService } = useEnv();
const { getViewSettings } = useReaderStore();
const { getView, getViewSettings } = useReaderStore();
const viewSettings = getViewSettings(bookKey)!;
const view = getView(bookKey)!;
const fontFamilyOptions = [
{
@@ -103,6 +106,7 @@ const FontPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
const [minFontSize, setMinFontSize] = useState(viewSettings.minimumFontSize!);
const [overrideFont, setOverrideFont] = useState(viewSettings.overrideFont!);
const [defaultFont, setDefaultFont] = useState(viewSettings.defaultFont!);
const [defaultCJKFont, setDefaultCJKFont] = useState(viewSettings.defaultCJKFont!);
const [serifFont, setSerifFont] = useState(viewSettings.serifFont!);
const [sansSerifFont, setSansSerifFont] = useState(viewSettings.sansSerifFont!);
const [monospaceFont, setMonospaceFont] = useState(viewSettings.monospaceFont!);
@@ -122,6 +126,11 @@ const FontPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [defaultFont]);
useEffect(() => {
saveViewSettings(envConfig, bookKey, 'defaultCJKFont', defaultCJKFont);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [defaultCJKFont]);
useEffect(() => {
saveViewSettings(envConfig, bookKey, 'defaultFontSize', defaultFontSize);
// eslint-disable-next-line react-hooks/exhaustive-deps
@@ -224,6 +233,19 @@ const FontPanel: React.FC<{ bookKey: string }> = ({ bookKey }) => {
/>
</div>
{(isCJKEnv() || view?.language.isCJK) && (
<FontFace
className='config-item-top'
family='serif'
label={_('CJK Font')}
options={sysFonts.filter(
(font) => CJK_FONTS_PATTENS.test(font) || CJK_NAMES_PATTENS.test(font),
)}
selected={defaultCJKFont}
onSelect={setDefaultCJKFont}
/>
)}
<div className='config-item'>
<span className=''>{_('Override Book Font')}</span>
<input
@@ -11,6 +11,7 @@ import { eventDispatcher } from '@/utils/event';
import { parseSSMLLang } from '@/utils/ssml';
import { getOSPlatform } from '@/utils/misc';
import { throttle } from '@/utils/throttle';
import { invokeUseBackgroundAudio } from '@/utils/bridge';
import Popup from '@/components/Popup';
import TTSPanel from './TTSPanel';
import TTSIcon from './TTSIcon';
@@ -33,21 +34,42 @@ const TTSControl = () => {
const [panelPosition, setPanelPosition] = useState<Position>();
const [trianglePosition, setTrianglePosition] = useState<Position>();
const [timeoutOption, setTimeoutOption] = useState(0);
const [timeoutTimestamp, setTimeoutTimestamp] = useState(0);
const [timeoutFunc, setTimeoutFunc] = useState<ReturnType<typeof setTimeout> | null>(null);
const popupWidth = useResponsiveSize(POPUP_WIDTH);
const popupHeight = useResponsiveSize(POPUP_HEIGHT);
const popupPadding = useResponsiveSize(POPUP_PADDING);
const iconRef = useRef<HTMLDivElement>(null);
const ttsControllerRef = useRef<TTSController | null>(null);
const unblockerAudioRef = useRef<HTMLAudioElement | null>(null);
// this enables WebAudio to play even when the mute toggle switch is ON
const unblockAudio = () => {
const audio = document.createElement('audio');
audio.setAttribute('x-webkit-airplay', 'deny');
audio.preload = 'auto';
audio.loop = true;
audio.src = SILENCE_DATA;
audio.play();
if (unblockerAudioRef.current) return;
unblockerAudioRef.current = document.createElement('audio');
unblockerAudioRef.current.setAttribute('x-webkit-airplay', 'deny');
unblockerAudioRef.current.preload = 'auto';
unblockerAudioRef.current.loop = true;
unblockerAudioRef.current.src = SILENCE_DATA;
unblockerAudioRef.current.play();
};
const releaseUnblockAudio = () => {
if (!unblockerAudioRef.current) return;
try {
unblockerAudioRef.current.pause();
unblockerAudioRef.current.currentTime = 0;
unblockerAudioRef.current.removeAttribute('src');
unblockerAudioRef.current.src = '';
unblockerAudioRef.current.load();
unblockerAudioRef.current = null;
console.log('Unblock audio released');
} catch (err) {
console.warn('Error releasing unblock audio:', err);
}
};
useEffect(() => {
@@ -94,6 +116,9 @@ const TTSControl = () => {
setShowIndicator(true);
try {
if (appService?.isIOSApp) {
await invokeUseBackgroundAudio({ enabled: true });
}
if (getOSPlatform() === 'ios' || appService?.isIOSApp) {
unblockAudio();
}
@@ -121,11 +146,8 @@ const TTSControl = () => {
}
};
const handleTTSStop = async (event: CustomEvent) => {
const { bookKey: stopBookKey } = event.detail;
if (bookKey === stopBookKey) {
handleStop();
}
const handleTTSStop = async () => {
handleStop();
};
const handleQueryIsSpeaking = () => {
@@ -177,6 +199,12 @@ const TTSControl = () => {
setShowPanel(false);
setShowIndicator(false);
}
if (appService?.isIOSApp) {
await invokeUseBackgroundAudio({ enabled: false });
}
if (getOSPlatform() === 'ios' || appService?.isIOSApp) {
releaseUnblockAudio();
}
};
// rate range: 0.5 - 3, 1.0 is normal speed
@@ -230,6 +258,23 @@ const TTSControl = () => {
return '';
};
const handleSelectTimeout = (value: number) => {
setTimeoutOption(value);
if (timeoutFunc) {
clearTimeout(timeoutFunc);
}
if (value > 0) {
setTimeoutFunc(
setTimeout(() => {
handleStop();
}, value * 1000),
);
setTimeoutTimestamp(Date.now() + value * 1000);
} else {
setTimeoutTimestamp(0);
}
};
const updatePanelPosition = () => {
if (iconRef.current) {
const rect = iconRef.current.getBoundingClientRect();
@@ -296,14 +341,16 @@ const TTSControl = () => {
bookKey={bookKey}
ttsLang={ttsLang}
isPlaying={isPlaying}
timeoutOption={timeoutOption}
timeoutTimestamp={timeoutTimestamp}
onTogglePlay={handleTogglePlay}
onBackward={handleBackward}
onForward={handleForward}
onStop={handleStop}
onSetRate={handleSetRate}
onGetVoices={handleGetVoices}
onSetVoice={handleSetVoice}
onGetVoiceId={handleGetVoiceId}
onSelectTimeout={handleSelectTimeout}
/>
</Popup>
)}
@@ -1,12 +1,12 @@
import clsx from 'clsx';
import React, { useState, ChangeEvent, useEffect } from 'react';
import { MdPlayCircle, MdPauseCircle, MdFastRewind, MdFastForward, MdStop } from 'react-icons/md';
import { MdPlayCircle, MdPauseCircle, MdFastRewind, MdFastForward, MdAlarm } from 'react-icons/md';
import { RiVoiceAiFill } from 'react-icons/ri';
import { MdCheck } from 'react-icons/md';
import { TTSVoice } from '@/services/tts';
import { useEnv } from '@/context/EnvContext';
import { useReaderStore } from '@/store/readerStore';
import { useTranslation } from '@/hooks/useTranslation';
import { TranslationFunc, useTranslation } from '@/hooks/useTranslation';
import { useSettingsStore } from '@/store/settingsStore';
import { useDefaultIconSize, useResponsiveSize } from '@/hooks/useResponsiveSize';
@@ -14,28 +14,104 @@ type TTSPanelProps = {
bookKey: string;
ttsLang: string;
isPlaying: boolean;
timeoutOption: number;
timeoutTimestamp: number;
onTogglePlay: () => void;
onBackward: () => void;
onForward: () => void;
onStop: () => void;
onSetRate: (rate: number) => void;
onGetVoices: (lang: string) => Promise<TTSVoice[]>;
onSetVoice: (voice: string) => void;
onGetVoiceId: () => string;
onSelectTimeout: (value: number) => void;
};
const getTTSTimeoutOptions = (_: TranslationFunc) => {
return [
{
label: _('No Timeout'),
value: 0,
},
{
label: _('{{value}} minute', { value: 1 }),
value: 60,
},
{
label: _('{{value}} minutes', { value: 3 }),
value: 180,
},
{
label: _('{{value}} minutes', { value: 5 }),
value: 300,
},
{
label: _('{{value}} minutes', { value: 10 }),
value: 600,
},
{
label: _('{{value}} minutes', { value: 20 }),
value: 1200,
},
{
label: _('{{value}} minutes', { value: 30 }),
value: 1800,
},
{
label: _('{{value}} minutes', { value: 45 }),
value: 2700,
},
{
label: _('{{value}} hour', { value: 1 }),
value: 3600,
},
{
label: _('{{value}} hours', { value: 2 }),
value: 7200,
},
{
label: _('{{value}} hours', { value: 3 }),
value: 10800,
},
{
label: _('{{value}} hours', { value: 4 }),
value: 14400,
},
{
label: _('{{value}} hours', { value: 6 }),
value: 21600,
},
{
label: _('{{value}} hours', { value: 8 }),
value: 28800,
},
];
};
const getCountdownTime = (timeout: number) => {
const now = Date.now();
if (timeout > now) {
const remainingTime = Math.floor((timeout - now) / 1000);
const minutes = Math.floor(remainingTime / 3600) * 60 + Math.floor((remainingTime % 3600) / 60);
const seconds = remainingTime % 60;
return `${minutes}:${seconds < 10 ? '0' : ''}${seconds}`;
}
return '';
};
const TTSPanel = ({
bookKey,
ttsLang,
isPlaying,
timeoutOption,
timeoutTimestamp,
onTogglePlay,
onBackward,
onForward,
onStop,
onSetRate,
onGetVoices,
onSetVoice,
onGetVoiceId,
onSelectTimeout,
}: TTSPanelProps) => {
const _ = useTranslation();
const { envConfig } = useEnv();
@@ -47,6 +123,10 @@ const TTSPanel = ({
const [rate, setRate] = useState(viewSettings?.ttsRate ?? 1.0);
const [selectedVoice, setSelectedVoice] = useState(viewSettings?.ttsVoice ?? '');
const [timeoutCountdown, setTimeoutCountdown] = useState(() => {
return getCountdownTime(timeoutTimestamp);
});
const defaultIconSize = useDefaultIconSize();
const iconSize32 = useResponsiveSize(32);
const iconSize48 = useResponsiveSize(48);
@@ -72,6 +152,23 @@ const TTSPanel = ({
setViewSettings(bookKey, viewSettings);
};
const updateTimeout = (timeout: number) => {
const now = Date.now();
if (timeout > 0 && timeout < now) {
onSelectTimeout(0);
setTimeoutCountdown('');
} else if (timeout > 0) {
setTimeoutCountdown(getCountdownTime(timeout));
}
};
useEffect(() => {
setTimeout(() => {
updateTimeout(timeoutTimestamp);
}, 1000);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [timeoutTimestamp, timeoutCountdown]);
useEffect(() => {
const voiceId = onGetVoiceId();
setSelectedVoice(voiceId);
@@ -87,6 +184,8 @@ const TTSPanel = ({
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [ttsLang]);
const timeoutOptions = getTTSTimeoutOptions(_);
return (
<div className='flex w-full flex-col items-center justify-center gap-2 rounded-2xl p-4'>
<div className='flex w-full flex-col items-center gap-0.5'>
@@ -132,9 +231,40 @@ const TTSPanel = ({
<button onClick={onForward} className='rounded-full p-1'>
<MdFastForward size={iconSize32} />
</button>
<button onClick={onStop} className='rounded-full p-1'>
<MdStop size={iconSize32} />
</button>
<div className='dropdown dropdown-top'>
<button className='flex flex-col items-center justify-center rounded-full p-1'>
<MdAlarm size={iconSize32} />
{timeoutCountdown && (
<span
className={clsx(
'absolute bottom-0 left-1/2 w-12 translate-x-[-50%] translate-y-[80%] px-1',
'bg-primary/80 text-base-100 rounded-full text-center text-xs',
)}
>
{timeoutCountdown}
</span>
)}
</button>
<ul
tabIndex={0}
className={clsx(
'dropdown-content bgcolor-base-200 no-triangle menu menu-vertical rounded-box absolute right-0 z-[1] shadow',
'mt-4 inline max-h-96 w-[250px] overflow-y-scroll',
)}
>
{timeoutOptions.map((option, index) => (
<li key={`${index}-${option.value}`} onClick={() => onSelectTimeout(option.value)}>
<div className='flex items-center px-2'>
<span style={{ minWidth: `${defaultIconSize}px` }}>
{timeoutOption === option.value && <MdCheck className='text-base-content' />}
</span>
<span className={clsx('text-base sm:text-sm')}>{option.label}</span>
</div>
</li>
))}
</ul>
</div>
<div className='dropdown dropdown-top'>
<button tabIndex={0} className='rounded-full p-1'>
<RiVoiceAiFill size={iconSize32} />
@@ -43,7 +43,10 @@ export const useClickEvent = (
if (!consumed) {
const centerStartX = viewStartX + viewRect.width * 0.375;
const centerEndX = viewStartX + viewRect.width * 0.625;
if (screenX >= centerStartX && screenX <= centerEndX) {
if (
viewSettings.disableClick! ||
(screenX >= centerStartX && screenX <= centerEndX)
) {
// toggle visibility of the header bar and the footer bar
setHoveredBookKey(hoveredBookKey ? null : bookKey);
} else {
+5 -7
View File
@@ -143,8 +143,7 @@ const hashPayload = (payload: EdgeTTSPayload): string => {
export class EdgeSpeechTTS {
static voices = genVoiceList(EDGE_TTS_VOICES);
private static audioCache = new LRUCache<string, AudioBuffer>(200);
private audioContext = new AudioContext();
private static audioCache = new LRUCache<string, ArrayBuffer>(200);
constructor() {}
@@ -267,17 +266,16 @@ export class EdgeSpeechTTS {
return this.#fetchEdgeSpeechWs(payload);
}
async createAudio(payload: EdgeTTSPayload): Promise<AudioBuffer> {
async createAudio(payload: EdgeTTSPayload): Promise<Blob> {
const cacheKey = hashPayload(payload);
if (EdgeSpeechTTS.audioCache.has(cacheKey)) {
return EdgeSpeechTTS.audioCache.get(cacheKey)!;
return new Blob([EdgeSpeechTTS.audioCache.get(cacheKey)!], { type: 'audio/mpeg' });
}
try {
const res = await this.create(payload);
const arrayBuffer = await res.arrayBuffer();
const audioBuffer = await this.audioContext.decodeAudioData(arrayBuffer.slice(0));
EdgeSpeechTTS.audioCache.set(cacheKey, audioBuffer);
return audioBuffer;
EdgeSpeechTTS.audioCache.set(cacheKey, arrayBuffer);
return new Blob([arrayBuffer], { type: 'audio/mpeg' });
} catch (error) {
throw error;
}
+19 -7
View File
@@ -1,4 +1,4 @@
import { AppPlatform, AppService, ToastType } from '@/types/system';
import { AppPlatform, AppService } from '@/types/system';
import { SystemSettings } from '@/types/settings';
import { FileSystem, BaseDir } from '@/types/system';
@@ -35,6 +35,7 @@ import {
import { getOSPlatform, isCJKEnv, isContentURI, isValidURL } from '@/utils/misc';
import { deserializeConfig, serializeConfig } from '@/utils/serializer';
import { downloadFile, uploadFile, deleteFile, createProgressHandler } from '@/libs/storage';
import { ClosableFile } from '@/utils/file';
import { ProgressHandler } from '@/utils/transfer';
import { TxtToEpubConverter } from '@/utils/txt';
import { BOOK_FILE_NOT_FOUND_ERROR } from './errors';
@@ -61,13 +62,9 @@ export abstract class BaseAppService implements AppService {
abstract getCoverImageUrl(book: Book): string;
abstract getCoverImageBlobUrl(book: Book): Promise<string>;
abstract getInitBooksDir(): Promise<string>;
abstract getCacheDir(): Promise<string>;
abstract selectDirectory(): Promise<string>;
abstract selectFiles(name: string, extensions: string[]): Promise<string[]>;
abstract showMessage(
msg: string,
kind?: ToastType,
title?: string,
okLabel?: string,
): Promise<void>;
async loadSettings(): Promise<SystemSettings> {
let settings: SystemSettings;
@@ -117,9 +114,12 @@ export abstract class BaseAppService implements AppService {
}
this.localBooksDir = settings.localBooksDir;
const cacheDir = await this.getCacheDir();
this.fs.getPrefix = (baseDir: BaseDir) => {
if (baseDir === 'Books') {
return this.localBooksDir;
} else if (baseDir === 'Cache') {
return cacheDir;
}
return null;
};
@@ -244,6 +244,10 @@ export abstract class BaseAppService implements AppService {
}
}
book.coverImageUrl = await this.generateCoverImageUrl(book);
const f = file as ClosableFile;
if (f && f.close) {
await f.close();
}
return book;
} catch (error) {
@@ -282,6 +286,10 @@ export abstract class BaseAppService implements AppService {
const file = await this.fs.openFile(lfp, 'Books', cfp);
const localFullpath = `${this.localBooksDir}/${lfp}`;
await uploadFile(file, localFullpath, handleProgress, hash);
const f = file as ClosableFile;
if (f && f.close) {
await f.close();
}
}
async uploadBook(book: Book, onProgress?: ProgressHandler): Promise<void> {
@@ -421,6 +429,10 @@ export abstract class BaseAppService implements AppService {
}
const { file } = (await this.loadBookContent(book, settings)) as BookContent;
const bookDoc = (await new DocumentLoader(file).open()).book as BookDoc;
const f = file as ClosableFile;
if (f && f.close) {
await f.close();
}
return bookDoc.metadata;
}
@@ -34,6 +34,7 @@ export const BOOK_UNGROUPED_ID = '';
export const DEFAULT_SYSTEM_SETTINGS: Partial<SystemSettings> = {
keepLogin: false,
autoUpload: true,
alwaysOnTop: false,
autoCheckUpdates: true,
screenWakeLock: true,
autoImportBooksOnOpen: false,
@@ -65,6 +66,7 @@ export const DEFAULT_BOOK_FONT: BookFont = {
sansSerifFont: 'Roboto',
monospaceFont: 'Consolas',
defaultFont: 'Serif',
defaultCJKFont: 'LXGW WenKai GB Screen',
defaultFontSize: 16,
minimumFontSize: 8,
fontWeight: 400,
@@ -180,6 +182,7 @@ export const WINDOWS_FONTS = [
'Leelawadee UI',
'Lucida Console',
'Lucida Sans Unicode',
'LXGW WenKai GB Screen',
'Malgun Gothic',
'Marlett',
'Microsoft Himalaya',
@@ -274,6 +277,7 @@ export const MACOS_FONTS = [
'Kozuka Mincho Pro',
'Lucida Grande',
'Luminari',
'LXGW WenKai GB Screen',
'Marker Felt',
'Menlo',
'Microsoft Sans Serif',
@@ -330,6 +334,7 @@ export const LINUX_FONTS = [
'Liberation Mono',
'Liberation Sans',
'Liberation Serif',
'LXGW WenKai GB Screen',
'Noto Mono',
'Noto Sans',
'Noto Sans JP',
@@ -366,6 +371,7 @@ export const IOS_FONTS = [
'Hiragino Mincho',
'Hiragino Sans',
'Kaiti',
'LXGW WenKai GB Screen',
'Palatino',
'PingFang SC',
'PingFang TC',
@@ -388,6 +394,7 @@ export const ANDROID_FONTS = [
'Georgia',
'Heiti',
'Kaiti',
'LXGW WenKai GB Screen',
'Noto Sans',
'Noto Sans CJK',
'Noto Sans JP',
@@ -405,6 +412,47 @@ export const ANDROID_FONTS = [
'XiHeiti',
];
export const CJK_NAMES_PATTENS = /[\u3040-\u30FF\u4E00-\u9FFF\uAC00-\uD7AF]/;
export const CJK_FONTS_PATTENS = new RegExp(
[
'CJK',
'TC$',
'SC$',
'HK',
'JP',
'TW',
'Sim',
'Kai',
'Hei',
'Yan',
'Min',
'Yuan',
'Song',
'Ming',
'FZ',
'FangZheng',
'WenQuanYi',
'PingFang',
'Hiragino',
'Meiryo',
'Source\\s?Han',
'Yu\\s?Gothic',
'Yu\\s?Mincho',
'Mincho',
'Gothic',
'Nanum',
'Malgun',
'Gulim',
'Dotum',
'Batang',
'Gungsuh',
'OPPO sans',
'MiSans',
'Fallback',
].join('|'),
'i',
);
export const BOOK_IDS_SEPARATOR = '+';
export const DOWNLOAD_READEST_URL = 'https://readest.com?utm_source=readest_web';
@@ -13,12 +13,12 @@ import {
WriteFileOptions,
} from '@tauri-apps/plugin-fs';
import { convertFileSrc } from '@tauri-apps/api/core';
import { open as openDialog, message } from '@tauri-apps/plugin-dialog';
import { join, appDataDir } from '@tauri-apps/api/path';
import { open as openDialog } from '@tauri-apps/plugin-dialog';
import { join, appDataDir, appCacheDir } from '@tauri-apps/api/path';
import { type as osType } from '@tauri-apps/plugin-os';
import { Book } from '@/types/book';
import { ToastType, FileSystem, BaseDir, AppPlatform } from '@/types/system';
import { FileSystem, BaseDir, AppPlatform } from '@/types/system';
import { isContentURI, isValidURL } from '@/utils/misc';
import { getCoverFilename, getFilename } from '@/utils/book';
import { copyURIToPath } from '@/utils/bridge';
@@ -80,7 +80,22 @@ export const nativeFileSystem: FileSystem = {
if (isValidURL(path)) {
return await new RemoteFile(path, name).open();
} else if (isContentURI(path)) {
return await new NativeFile(fp, fname, base ? baseDir : null).open();
if (path.includes('com.android.externalstorage')) {
// If the URI is from shared internal storage (like /storage/emulated/0),
// we can access it directly using the path — no need to copy.
return await new NativeFile(fp, fname, base ? baseDir : null).open();
} else {
// Otherwise, for content:// URIs (e.g. from MediaStore, Drive, or third-party apps),
// we cannot access the file directly — so we copy it to a temporary cache location.
const prefix = this.getPrefix('Cache');
const dst = `${prefix}/${fname}`;
const res = await copyURIToPath({ uri: path, dst });
if (!res.success) {
console.error('Failed to open file:', res);
throw new Error('Failed to open file');
}
return await new NativeFile(dst, fname, base ? baseDir : null).open();
}
} else {
const prefix = this.getPrefix(base);
if (prefix && OS_TYPE !== 'android') {
@@ -89,7 +104,7 @@ export const nativeFileSystem: FileSystem = {
// Once the bug is resolved, we should switch back to using NativeFile.
// RemoteFile is not usable on Android due to unknown issues of range fetch with Android WebView.
const absolutePath = await join(prefix, path);
return await new RemoteFile(this.getURL(absolutePath), path).open();
return await new RemoteFile(this.getURL(absolutePath), fname).open();
} else {
return await new NativeFile(fp, fname, base ? baseDir : null).open();
}
@@ -213,6 +228,18 @@ export class NativeAppService extends BaseAppService {
return join(await appDataDir(), LOCAL_BOOKS_SUBDIR);
}
async getCacheDir(): Promise<string> {
return await appCacheDir();
}
async selectDirectory(): Promise<string> {
const selected = await openDialog({
directory: true,
multiple: false,
});
return selected as string;
}
async selectFiles(name: string, extensions: string[]): Promise<string[]> {
const selected = await openDialog({
multiple: true,
@@ -221,15 +248,6 @@ export class NativeAppService extends BaseAppService {
return Array.isArray(selected) ? selected : selected ? [selected] : [];
}
async showMessage(
msg: string,
kind: ToastType = 'info',
title?: string,
okLabel?: string,
): Promise<void> {
await message(msg, { kind, title, okLabel });
}
getCoverImageUrl = (book: Book): string => {
return this.fs.getURL(`${this.localBooksDir}/${getCoverFilename(book)}`);
};
@@ -13,31 +13,19 @@ export class EdgeTTSClient implements TTSClient {
#voices: TTSVoice[] = [];
#edgeTTS: EdgeSpeechTTS;
static #audioContext: AudioContext | null;
#sourceNode: AudioBufferSourceNode | null = null;
#audioElement: HTMLAudioElement | null = null;
#isPlaying = false;
#pausedAt = 0;
#startedAt = 0;
#audioBuffer: AudioBuffer | null = null;
available = true;
constructor() {
this.#edgeTTS = new EdgeSpeechTTS();
}
async initializeAudioContext() {
if (!EdgeTTSClient.#audioContext) {
EdgeTTSClient.#audioContext = new AudioContext();
}
if (EdgeTTSClient.#audioContext.state === 'suspended') {
await EdgeTTSClient.#audioContext.resume();
}
}
async init() {
this.#voices = EdgeSpeechTTS.voices;
try {
await this.initializeAudioContext();
await this.#edgeTTS.create({
lang: 'en',
text: 'test',
@@ -115,15 +103,12 @@ export class EdgeTTSClient implements TTSClient {
break;
}
try {
this.#audioBuffer = await this.#edgeTTS.createAudio(
this.getPayload(lang, mark.text, voiceId),
);
if (!EdgeTTSClient.#audioContext) {
EdgeTTSClient.#audioContext = new AudioContext();
}
this.#sourceNode = EdgeTTSClient.#audioContext.createBufferSource();
this.#sourceNode.buffer = this.#audioBuffer;
this.#sourceNode.connect(EdgeTTSClient.#audioContext.destination);
const blob = await this.#edgeTTS.createAudio(this.getPayload(lang, mark.text, voiceId));
const url = URL.createObjectURL(blob);
this.#audioElement = new Audio(url);
const audio = this.#audioElement;
audio.setAttribute('x-webkit-airplay', 'deny');
audio.preload = 'auto';
yield {
code: 'boundary',
@@ -132,36 +117,37 @@ export class EdgeTTSClient implements TTSClient {
};
const result = await new Promise<TTSMessageEvent>((resolve) => {
if (EdgeTTSClient.#audioContext === null || this.#sourceNode === null) {
throw new Error('Audio context or source node is null');
}
this.#sourceNode.onended = (event: Event) => {
// chunk finished speaking or aborted speaking
if (signal.aborted || event.type === 'stopped') {
resolve({
code: 'error',
message: 'Aborted',
});
return;
const cleanUp = () => {
audio.onended = null;
audio.onerror = null;
audio.pause();
audio.src = '';
URL.revokeObjectURL(url);
};
audio.onended = () => {
cleanUp();
if (signal.aborted) {
resolve({ code: 'error', message: 'Aborted' });
} else {
resolve({ code: 'end', message: `Chunk finished: ${mark.name}` });
}
resolve({
code: 'end',
message: `Chunk finished: ${mark.name}`,
});
};
audio.onerror = (e) => {
cleanUp();
console.warn('Audio playback error:', e);
resolve({ code: 'error', message: 'Audio playback error' });
};
if (signal.aborted) {
resolve({
code: 'error',
message: 'Aborted',
});
cleanUp();
resolve({ code: 'error', message: 'Aborted' });
return;
}
if (EdgeTTSClient.#audioContext.state === 'suspended') {
EdgeTTSClient.#audioContext.resume();
}
this.#sourceNode.start(0);
this.#isPlaying = true;
this.#startedAt = EdgeTTSClient.#audioContext.currentTime;
audio.play().catch((err) => {
cleanUp();
console.error('Failed to play audio:', err);
resolve({ code: 'error', message: 'Playback failed: ' + err.message });
});
});
yield result;
} catch (error) {
@@ -186,17 +172,17 @@ export class EdgeTTSClient implements TTSClient {
}
async pause() {
if (!this.#isPlaying || !EdgeTTSClient.#audioContext) return;
this.#pausedAt = EdgeTTSClient.#audioContext.currentTime - this.#startedAt;
await EdgeTTSClient.#audioContext.suspend();
if (!this.#isPlaying || !this.#audioElement) return;
this.#pausedAt = this.#audioElement.currentTime - this.#startedAt;
await this.#audioElement.pause();
this.#isPlaying = false;
}
async resume() {
if (this.#isPlaying || !EdgeTTSClient.#audioContext) return;
await EdgeTTSClient.#audioContext.resume();
if (this.#isPlaying || !this.#audioElement) return;
await this.#audioElement.play();
this.#isPlaying = true;
this.#startedAt = EdgeTTSClient.#audioContext.currentTime - this.#pausedAt;
this.#startedAt = this.#audioElement.currentTime - this.#pausedAt;
}
async stop() {
@@ -207,21 +193,18 @@ export class EdgeTTSClient implements TTSClient {
this.#isPlaying = false;
this.#pausedAt = 0;
this.#startedAt = 0;
if (this.#sourceNode) {
try {
this.#sourceNode.stop();
if (this.#sourceNode?.onended) {
this.#sourceNode.onended(new Event('stopped'));
}
} catch (err) {
if (!(err instanceof Error) || err.name !== 'InvalidStateError') {
console.log('Error stopping source node:', err);
}
if (this.#audioElement) {
this.#audioElement.pause();
this.#audioElement.currentTime = 0;
if (this.#audioElement?.onended) {
this.#audioElement.onended(new Event('stopped'));
}
this.#sourceNode.disconnect();
this.#sourceNode = null;
if (this.#audioElement.src?.startsWith('blob:')) {
URL.revokeObjectURL(this.#audioElement.src);
}
this.#audioElement.src = '';
this.#audioElement = null;
}
this.#audioBuffer = null;
}
async setRate(rate: number) {
@@ -1,5 +1,5 @@
import { Book } from '@/types/book';
import { ToastType, FileSystem, BaseDir, AppPlatform } from '@/types/system';
import { FileSystem, BaseDir, AppPlatform } from '@/types/system';
import { getCoverFilename } from '@/utils/book';
import { getOSPlatform, isValidURL } from '@/utils/misc';
import { RemoteFile } from '@/utils/file';
@@ -54,12 +54,12 @@ const indexedDBFileSystem: FileSystem = {
return path;
}
},
async openFile(path: string, base: BaseDir) {
async openFile(path: string, base: BaseDir, filename?: string) {
if (isValidURL(path)) {
return await new RemoteFile(path).open();
return await new RemoteFile(path, filename).open();
} else {
const content = await this.readFile(path, base, 'binary');
return new File([content], path);
return new File([content], filename || path);
}
},
async copyFile(srcPath: string, dstPath: string, base: BaseDir) {
@@ -212,6 +212,10 @@ export class WebAppService extends BaseAppService {
return LOCAL_BOOKS_SUBDIR;
}
async getCacheDir(): Promise<string> {
return 'Cache';
}
async selectDirectory(): Promise<string> {
throw new Error('selectDirectory is not supported in browser');
}
@@ -220,10 +224,6 @@ export class WebAppService extends BaseAppService {
throw new Error('selectFiles is not supported in browser');
}
async showMessage(msg: string, kind: ToastType = 'info'): Promise<void> {
alert(`${kind.toUpperCase()}: ${msg}`);
}
getCoverImageUrl = (book: Book): string => {
return this.fs.getURL(`${LOCAL_BOOKS_SUBDIR}/${getCoverFilename(book)}`);
};
+1
View File
@@ -105,6 +105,7 @@ export interface BookFont {
sansSerifFont: string;
monospaceFont: string;
defaultFont: string;
defaultCJKFont: string;
defaultFontSize: number;
minimumFontSize: number;
fontWeight: number;
+1
View File
@@ -22,6 +22,7 @@ export interface SystemSettings {
keepLogin: boolean;
autoUpload: boolean;
alwaysOnTop: boolean;
autoCheckUpdates: boolean;
screenWakeLock: boolean;
autoImportBooksOnOpen: boolean;
+1 -3
View File
@@ -5,7 +5,6 @@ import { ProgressHandler } from '@/utils/transfer';
export type AppPlatform = 'web' | 'tauri';
export type BaseDir = 'Books' | 'Settings' | 'Data' | 'Log' | 'Cache' | 'None';
export type ToastType = 'info' | 'warning' | 'error';
export interface FileSystem {
getURL(path: string): string;
@@ -39,9 +38,8 @@ export interface AppService {
isAndroidApp: boolean;
isIOSApp: boolean;
selectDirectory(): Promise<string>;
selectFiles(name: string, extensions: string[]): Promise<string[]>;
showMessage(msg: string, kind?: ToastType, title?: string, okLabel?: string): Promise<void>;
loadSettings(): Promise<SystemSettings>;
saveSettings(settings: SystemSettings): Promise<void>;
importBook(
+2 -1
View File
@@ -19,7 +19,8 @@ export const getStoragePlanData = (token: string) => {
const data = jwtDecode<Token>(token) || {};
const plan = data['plan'] || 'free';
const usage = data['storage_usage_bytes'] || 0;
const quota = DEFAULT_STORAGE_QUOTA[plan] || DEFAULT_STORAGE_QUOTA['free'];
const fixedQuota = parseInt(process.env['NEXT_PUBLIC_STORAGE_FIXED_QUOTA'] || '0');
const quota = fixedQuota || DEFAULT_STORAGE_QUOTA[plan] || DEFAULT_STORAGE_QUOTA['free'];
return {
plan,
+10
View File
@@ -10,6 +10,10 @@ export interface CopyURIResponse {
error?: string;
}
export interface UseBackgroundAudioRequest {
enabled: boolean;
}
export async function copyURIToPath(request: CopyURIRequest): Promise<CopyURIResponse> {
const result = await invoke<CopyURIResponse>('plugin:native-bridge|copy_uri_to_path', {
payload: request,
@@ -17,3 +21,9 @@ export async function copyURIToPath(request: CopyURIRequest): Promise<CopyURIRes
return result;
}
export async function invokeUseBackgroundAudio(request: UseBackgroundAudioRequest): Promise<void> {
await invoke('plugin:native-bridge|use_background_audio', {
payload: request,
});
}
+76 -10
View File
@@ -50,7 +50,12 @@ class DeferredBlob extends Blob {
}
}
export class NativeFile extends File {
export interface ClosableFile extends File {
open(): Promise<this>;
close(): Promise<void>;
}
export class NativeFile extends File implements ClosableFile {
#handle: FileHandle | null = null;
#fp: string;
#name: string;
@@ -59,6 +64,11 @@ export class NativeFile extends File {
#size: number = -1;
#type: string = '';
static MAX_CACHE_CHUNK_SIZE = 1024 * 1024;
static MAX_CACHE_ITEMS_SIZE = 20;
#cache: Map<number, ArrayBuffer> = new Map();
#order: number[] = [];
constructor(fp: string, name?: string, baseDir: BaseDirectory | null = null, type = '') {
super([], name || fp, { type });
this.#fp = fp;
@@ -74,6 +84,15 @@ export class NativeFile extends File {
return this;
}
async close() {
if (this.#handle) {
await this.#handle.close();
this.#handle = null;
}
this.#cache.clear();
this.#order = [];
}
override get name() {
return this.#name;
}
@@ -94,10 +113,6 @@ export class NativeFile extends File {
return this.#handle?.stat();
}
async close(): Promise<void> {
return this.#handle?.close();
}
async seek(offset: number, whence: SeekMode): Promise<number> {
if (!this.#handle) {
throw new Error('File handle is not open');
@@ -112,10 +127,58 @@ export class NativeFile extends File {
}
start = Math.max(0, start);
end = Math.max(start, Math.min(this.size, end));
await this.#handle.seek(start, SeekMode.Start);
const buffer = new Uint8Array(end - start);
const size = end - start;
if (size > NativeFile.MAX_CACHE_CHUNK_SIZE) {
await this.#handle.seek(start, SeekMode.Start);
const buffer = new Uint8Array(size);
await this.#handle.read(buffer);
return buffer.buffer;
}
const cachedChunkStart = Array.from(this.#cache.keys()).find((chunkStart) => {
const buffer = this.#cache.get(chunkStart)!;
return start >= chunkStart && end <= chunkStart + buffer.byteLength;
});
if (cachedChunkStart !== undefined) {
this.#updateAccessOrder(cachedChunkStart);
const buffer = this.#cache.get(cachedChunkStart)!;
const offset = start - cachedChunkStart;
return buffer.slice(offset, offset + size);
}
const chunkStart = Math.max(0, start - 1024);
const chunkEnd = Math.min(this.size, chunkStart + NativeFile.MAX_CACHE_CHUNK_SIZE);
const chunkSize = chunkEnd - chunkStart;
await this.#handle.seek(chunkStart, SeekMode.Start);
const buffer = new Uint8Array(chunkSize);
await this.#handle.read(buffer);
return buffer.buffer;
this.#cache.set(chunkStart, buffer.buffer);
this.#updateAccessOrder(chunkStart);
this.#ensureCacheSize();
const offset = start - chunkStart;
return buffer.buffer.slice(offset, offset + size);
}
#updateAccessOrder(chunkStart: number) {
const index = this.#order.indexOf(chunkStart);
if (index > -1) {
this.#order.splice(index, 1);
}
this.#order.unshift(chunkStart);
}
#ensureCacheSize() {
while (this.#cache.size > NativeFile.MAX_CACHE_ITEMS_SIZE) {
const oldestKey = this.#order.pop();
if (oldestKey !== undefined) {
this.#cache.delete(oldestKey);
}
}
}
override slice(start = 0, end = this.size, contentType = this.type): Blob {
@@ -172,7 +235,7 @@ export class NativeFile extends File {
}
}
export class RemoteFile extends File {
export class RemoteFile extends File implements ClosableFile {
url: string;
#name: string;
#lastModified: number;
@@ -238,7 +301,10 @@ export class RemoteFile extends File {
}
}
async close(): Promise<void> {}
async close(): Promise<void> {
this.#cache.clear();
this.#order = [];
}
async fetchRangePart(start: number, end: number) {
start = Math.max(0, start);
+6 -1
View File
@@ -32,7 +32,12 @@ export const makeSafeFilename = (filename: string, replacement = '_') => {
export const getUserLang = () => navigator?.language.split('-')[0] || 'en';
export const isCJKEnv = () => ['zh', 'ja', 'ko'].includes(getUserLang());
export const isCJKEnv = () => {
const isCJKLocale = ['zh', 'ja', 'ko'].includes(getUserLang());
const uiLanguage = localStorage.getItem('i18nextLng') || '';
const isCJKUI = ['zh', 'ja', 'ko'].some((lang) => uiLanguage.startsWith(lang));
return isCJKLocale || isCJKUI;
};
export const getUserLocale = (lang: string): string | undefined => {
const languages =
+38 -29
View File
@@ -8,7 +8,6 @@ import { ViewSettings } from '@/types/book';
import {
themes,
Palette,
ThemeMode,
CustomTheme,
generateLightPalette,
generateDarkPalette,
@@ -22,21 +21,30 @@ const getFontStyles = (
sansSerif: string,
monospace: string,
defaultFont: string,
defaultCJKFont: string,
fontSize: number,
minFontSize: number,
fontWeight: number,
overrideFont: boolean,
themeCode: ThemeCode,
) => {
const lastSerifFonts = ['Georgia', 'Times New Roman'];
const { primary } = themeCode;
const lastSerifFonts = ['LXGW WenKai GB Screen', 'Georgia', 'Times New Roman'];
const serifFonts = [
serif,
...SERIF_FONTS.filter((font) => font !== serif && !lastSerifFonts.includes(font)),
...SERIF_FONTS.filter(
(font) => font !== serif && font !== defaultCJKFont && !lastSerifFonts.includes(font),
),
...(defaultCJKFont !== serif && !lastSerifFonts.includes(defaultCJKFont)
? [defaultCJKFont]
: []),
...lastSerifFonts.filter((font) => SERIF_FONTS.includes(font)),
...FALLBACK_FONTS,
];
const sansSerifFonts = [
sansSerif,
...SANS_SERIF_FONTS.filter((font) => font !== sansSerif),
...SANS_SERIF_FONTS.filter((font) => font !== sansSerif && font !== defaultCJKFont),
...(defaultCJKFont !== sansSerif ? [defaultCJKFont] : []),
...FALLBACK_FONTS,
];
const monospaceFonts = [monospace, ...MONOSPACE_FONTS.filter((font) => font !== monospace)];
@@ -73,8 +81,18 @@ const getFontStyles = (
font-size: ${fontSize * 3}px;
}
body * {
font-family: revert ${overrideFont ? '!important' : ''};
font-family: inherit;
${overrideFont ? 'font-family: revert !important;' : ''}
${overrideFont ? 'font-size: revert !important;' : ''}
${overrideFont ? 'color: revert !important;' : ''}
}
a:any-link {
${overrideFont ? `color: ${primary};` : ''}
}
/* https://github.com/whatwg/html/issues/5426 */
@media (prefers-color-scheme: dark) {
a:link {
${overrideFont ? `color: lightblue;` : ''}
}
}
`;
return fontStyles;
@@ -141,29 +159,15 @@ const getLayoutStyles = (
zoomLevel: number,
writingMode: string,
vertical: boolean,
bg: string,
fg: string,
primary: string,
isDarkMode: boolean,
) => `
themeCode: ThemeCode,
) => {
const { bg, fg, primary, isDarkMode } = themeCode;
const layoutStyle = `
@namespace epub "http://www.idpf.org/2007/ops";
html {
color-scheme: ${isDarkMode ? 'dark' : 'light'};
color: ${fg};
}
a:any-link {
color: ${primary} ${isDarkMode ? '!important' : ''};
}
aside[epub|type~="footnote"] {
display: none;
}
/* https://github.com/whatwg/html/issues/5426 */
@media (prefers-color-scheme: dark) {
a:link {
color: lightblue;
}
}
html {
--theme-bg-color: ${bg};
--theme-fg-color: ${fg};
@@ -253,6 +257,10 @@ const getLayoutStyles = (
${vertical ? `vertical-align: unset;` : ''}
}
aside[epub|type~="footnote"] {
display: none;
}
.duokan-footnote-content,
.duokan-footnote-item {
display: none;
@@ -267,6 +275,8 @@ const getLayoutStyles = (
border-color: unset;
}
`;
return layoutStyle;
};
export const getFootnoteStyles = () => `
.duokan-footnote-content,
@@ -308,7 +318,7 @@ export const getThemeCode = () => {
let customThemes: CustomTheme[] = [];
if (typeof window !== 'undefined') {
themeColor = localStorage.getItem('themeColor') || 'default';
themeMode = localStorage.getItem('themeMode') as ThemeMode;
themeMode = localStorage.getItem('themeMode') || 'auto';
customThemes = JSON.parse(localStorage.getItem('customThemes') || '[]');
systemIsDarkMode = window.matchMedia('(prefers-color-scheme: dark)').matches;
}
@@ -354,10 +364,7 @@ export const getStyles = (viewSettings: ViewSettings, themeCode?: ThemeCode) =>
viewSettings.zoomLevel! / 100.0,
viewSettings.writingMode!,
viewSettings.vertical!,
themeCode.bg,
themeCode.fg,
themeCode.primary,
themeCode.isDarkMode,
themeCode,
);
// scale the font size on-the-fly so that we can sync the same font size on different devices
const isMobile = ['ios', 'android'].includes(getOSPlatform());
@@ -367,10 +374,12 @@ export const getStyles = (viewSettings: ViewSettings, themeCode?: ThemeCode) =>
viewSettings.sansSerifFont!,
viewSettings.monospaceFont!,
viewSettings.defaultFont!,
viewSettings.defaultCJKFont!,
viewSettings.defaultFontSize! * fontScale,
viewSettings.minimumFontSize!,
viewSettings.fontWeight!,
viewSettings.overrideFont!,
themeCode,
);
const userStylesheet = viewSettings.userStylesheet!;
return `${layoutStyles}\n${fontStyles}\n${fontfacesCSS}\n${userStylesheet}`;
+10
View File
@@ -38,6 +38,16 @@ export const tauriHandleToggleFullScreen = async () => {
await currentWindow.setFullscreen(newIsFullscreen);
};
export const tauriHandleSetAlwaysOnTop = async (isAlwaysOnTop: boolean) => {
const currentWindow = getCurrentWindow();
await currentWindow.setAlwaysOnTop(isAlwaysOnTop);
};
export const tauriGetAlwaysOnTop = async () => {
const currentWindow = getCurrentWindow();
return await currentWindow.isAlwaysOnTop();
};
export const tauriHandleOnWindowFocus = async (callback: () => void) => {
const currentWindow = getCurrentWindow();
return currentWindow.listen(TauriEvent.WINDOW_FOCUS, async () => {
+124 -113
View File
@@ -54,44 +54,44 @@ importers:
specifier: ^2.47.7
version: 2.47.7
'@tauri-apps/api':
specifier: 2.3.0
version: 2.3.0
specifier: 2.4.1
version: 2.4.1
'@tauri-apps/plugin-cli':
specifier: ^2.2.0
version: 2.2.0
'@tauri-apps/plugin-deep-link':
specifier: ^2.2.0
version: 2.2.0
specifier: ^2.2.1
version: 2.2.1
'@tauri-apps/plugin-dialog':
specifier: ^2.2.0
version: 2.2.0
specifier: ^2.2.1
version: 2.2.1
'@tauri-apps/plugin-fs':
specifier: ^2.2.0
version: 2.2.0
specifier: ^2.2.1
version: 2.2.1
'@tauri-apps/plugin-haptics':
specifier: ^2.2.3
version: 2.2.3
specifier: ^2.2.4
version: 2.2.4
'@tauri-apps/plugin-http':
specifier: ^2.3.0
version: 2.3.0
specifier: ^2.4.3
version: 2.4.3
'@tauri-apps/plugin-log':
specifier: ^2.2.3
version: 2.2.3
specifier: ^2.3.1
version: 2.3.1
'@tauri-apps/plugin-opener':
specifier: ^2.2.6
version: 2.2.6
'@tauri-apps/plugin-os':
specifier: ^2.2.0
version: 2.2.0
specifier: ^2.2.1
version: 2.2.1
'@tauri-apps/plugin-process':
specifier: ^2.2.0
version: 2.2.0
specifier: ^2.2.1
version: 2.2.1
'@tauri-apps/plugin-shell':
specifier: ~2.2.0
version: 2.2.0
specifier: ~2.2.1
version: 2.2.1
'@tauri-apps/plugin-updater':
specifier: ^2.5.1
version: 2.5.1
specifier: ^2.7.0
version: 2.7.0
'@zip.js/zip.js':
specifier: ^2.7.53
version: 2.7.53
@@ -160,8 +160,8 @@ importers:
specifier: ^0.5.12
version: 0.5.12(wrangler@4.4.0)
'@tauri-apps/cli':
specifier: 2.3.1
version: 2.3.1
specifier: 2.4.1
version: 2.4.1
'@types/cors':
specifier: ^2.8.17
version: 2.8.17
@@ -2477,113 +2477,120 @@ packages:
'@swc/helpers@0.5.15':
resolution: {integrity: sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==}
'@tauri-apps/api@2.3.0':
resolution: {integrity: sha512-33Z+0lX2wgZbx1SPFfqvzI6su63hCBkbzv+5NexeYjIx7WA9htdOKoRR7Dh3dJyltqS5/J8vQFyybiRoaL0hlA==}
'@tauri-apps/api@2.4.1':
resolution: {integrity: sha512-5sYwZCSJb6PBGbBL4kt7CnE5HHbBqwH+ovmOW6ZVju3nX4E3JX6tt2kRklFEH7xMOIwR0btRkZktuLhKvyEQYg==}
'@tauri-apps/cli-darwin-arm64@2.3.1':
resolution: {integrity: sha512-TOhSdsXYt+f+asRU+Dl+Wufglj/7+CX9h8RO4hl5k7D6lR4L8yTtdhpS7btaclOMmjYC4piNfJE70GoxhOoYWw==}
'@tauri-apps/cli-darwin-arm64@2.4.1':
resolution: {integrity: sha512-QME7s8XQwy3LWClTVlIlwXVSLKkeJ/z88pr917Mtn9spYOjnBfsgHAgGdmpWD3NfJxjg7CtLbhH49DxoFL+hLg==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [darwin]
'@tauri-apps/cli-darwin-x64@2.3.1':
resolution: {integrity: sha512-LDwGg3AuBQ3aCeMAFaFwt0MSGOVFoXuXEe0z4QxQ7jZE5tdAOhKABaq4i569V5lShCgQZ6nLD/tmA5+GipvHnA==}
'@tauri-apps/cli-darwin-x64@2.4.1':
resolution: {integrity: sha512-/r89IcW6Ya1sEsFUEH7wLNruDTj7WmDWKGpPy7gATFtQr5JEY4heernqE82isjTUimnHZD8SCr0jA3NceI4ybw==}
engines: {node: '>= 10'}
cpu: [x64]
os: [darwin]
'@tauri-apps/cli-linux-arm-gnueabihf@2.3.1':
resolution: {integrity: sha512-hu3HpbbtJBvHXw5i54QHwLxOUoXWqhf7CL2YYSPOrWEEQo10NKddulP61L5gfr5z+bSSaitfLwqgTidgnaNJCA==}
'@tauri-apps/cli-linux-arm-gnueabihf@2.4.1':
resolution: {integrity: sha512-9tDijkRB+CchAGjXxYdY9l/XzFpLp1yihUtGXJz9eh+3qIoRI043n3e+6xmU8ZURr7XPnu+R4sCmXs6HD+NCEQ==}
engines: {node: '>= 10'}
cpu: [arm]
os: [linux]
'@tauri-apps/cli-linux-arm64-gnu@2.3.1':
resolution: {integrity: sha512-mEGgwkiGSKYXWHhGodo7zU9PCd2I/d6KkR+Wp1nzK+DxsCrEK6yJ5XxYLSQSDcKkM4dCxpVEPUiVMbDhmn08jg==}
'@tauri-apps/cli-linux-arm64-gnu@2.4.1':
resolution: {integrity: sha512-pnFGDEXBAzS4iDYAVxTRhAzNu3K2XPGflYyBc0czfHDBXopqRgMyj5Q9Wj7HAwv6cM8BqzXINxnb2ZJFGmbSgA==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@tauri-apps/cli-linux-arm64-musl@2.3.1':
resolution: {integrity: sha512-tqQkafikGfnc7ISnGjSYkbpnzJKEyO8XSa0YOXTAL3J8R5Pss5ZIZY7G8kq1mwQSR/dPVR1ZLTVXgZGuysjP8w==}
'@tauri-apps/cli-linux-arm64-musl@2.4.1':
resolution: {integrity: sha512-Hp0zXgeZNKmT+eoJSCxSBUm2QndNuRxR55tmIeNm3vbyUMJN/49uW7nurZ5fBPsacN4Pzwlx1dIMK+Gnr9A69w==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
libc: [musl]
'@tauri-apps/cli-linux-x64-gnu@2.3.1':
resolution: {integrity: sha512-I3puDJ2wGEauXlXbzIHn2etz78TaWs1cpN6zre02maHr6ZR7nf7euTCOGPhhfoMG0opA5mT/eLuYpVw648/VAA==}
'@tauri-apps/cli-linux-riscv64-gnu@2.4.1':
resolution: {integrity: sha512-3T3bo2E4fdYRvzcXheWUeQOVB+LunEEi92iPRgOyuSVexVE4cmHYl+MPJF+EUV28Et0hIVTsHibmDO0/04lAFg==}
engines: {node: '>= 10'}
cpu: [riscv64]
os: [linux]
libc: [glibc]
'@tauri-apps/cli-linux-x64-gnu@2.4.1':
resolution: {integrity: sha512-kLN0FdNONO+2i+OpU9+mm6oTGufRC00e197TtwjpC0N6K2K8130w7Q3FeODIM2CMyg0ov3tH+QWqKW7GNhHFzg==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
libc: [glibc]
'@tauri-apps/cli-linux-x64-musl@2.3.1':
resolution: {integrity: sha512-rbWiCOBuQN7tPySkUyBs914uUikE3mEUOqV/IFospvKESw4UC3G1DL5+ybfXH7Orb8/in3JpJuVzYQjo+OSbBA==}
'@tauri-apps/cli-linux-x64-musl@2.4.1':
resolution: {integrity: sha512-a8exvA5Ub9eg66a6hsMQKJIkf63QAf9OdiuFKOsEnKZkNN2x0NLgfvEcqdw88VY0UMs9dBoZ1AGbWMeYnLrLwQ==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
libc: [musl]
'@tauri-apps/cli-win32-arm64-msvc@2.3.1':
resolution: {integrity: sha512-PdTmUzSeTHjJuBpCV7L+V29fPhPtToU+NZU46slHKSA1aT38MiFDXBZ/6P5Zudrt9QPMfIubqnJKbK8Ivvv7Ww==}
'@tauri-apps/cli-win32-arm64-msvc@2.4.1':
resolution: {integrity: sha512-4JFrslsMCJQG1c573T9uqQSAbF3j/tMKkMWzsIssv8jvPiP++OG61A2/F+y9te9/Q/O95cKhDK63kaiO5xQaeg==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [win32]
'@tauri-apps/cli-win32-ia32-msvc@2.3.1':
resolution: {integrity: sha512-K/Xa97kspWT4UWj3t26lL2D3QsopTAxS7kWi5kObdqtAGn3qD52qBi24FH38TdvHYz4QlnLIb30TukviCgh4gw==}
'@tauri-apps/cli-win32-ia32-msvc@2.4.1':
resolution: {integrity: sha512-9eXfFORehYSCRwxg2KodfmX/mhr50CI7wyBYGbPLePCjr5z0jK/9IyW6r0tC+ZVjwpX48dkk7hKiUgI25jHjzA==}
engines: {node: '>= 10'}
cpu: [ia32]
os: [win32]
'@tauri-apps/cli-win32-x64-msvc@2.3.1':
resolution: {integrity: sha512-RgwzXbP8gAno3kQEsybMtgLp6D1Z1Nec2cftryYbPTJmoMJs6e4qgtxuTSbUz5SKnHe8rGgMiFSvEGoHvbG72Q==}
'@tauri-apps/cli-win32-x64-msvc@2.4.1':
resolution: {integrity: sha512-60a4Ov7Jrwqz2hzDltlS7301dhSAmM9dxo+IRBD3xz7yobKrgaHXYpWvnRomYItHcDd51VaKc9292H8/eE/gsw==}
engines: {node: '>= 10'}
cpu: [x64]
os: [win32]
'@tauri-apps/cli@2.3.1':
resolution: {integrity: sha512-xewcw/ZsCqgilTy2h7+pp2Baxoy7zLR2wXOV7SZLzkb6SshHVbm1BFAjn8iFATURRW85KLzl6wSGJ2dQHjVHqw==}
'@tauri-apps/cli@2.4.1':
resolution: {integrity: sha512-9Ta81jx9+57FhtU/mPIckDcOBtPTUdKM75t4+aA0X84b8Sclb0jy1xA8NplmcRzp2fsfIHNngU2NiRxsW5+yOQ==}
engines: {node: '>= 10'}
hasBin: true
'@tauri-apps/plugin-cli@2.2.0':
resolution: {integrity: sha512-rvNhMog9rHr01Xk+trBFKJ0eZICIvPkm9GX6ogB89/0hROU/lf+a/sb4vC0wtSeR7zrJuCSxwxYuvHCZheaYFA==}
'@tauri-apps/plugin-deep-link@2.2.0':
resolution: {integrity: sha512-H6mkxr2KZ3XJcKL44tiq6cOjCw9DL8OgU1xjn3j26Qsn+H/roPFiyhR7CHuB8Ar+sQFj4YVlfmJwtBajK2FETQ==}
'@tauri-apps/plugin-deep-link@2.2.1':
resolution: {integrity: sha512-8skZ6qIH/kWaV8d6jj3aPvvkIOuqkVk0APRDey9n9N3Ueu3n4MIbuxpAKR2EdoAyQxnXxPTNVyjw2D35/vfGyg==}
'@tauri-apps/plugin-dialog@2.2.0':
resolution: {integrity: sha512-6bLkYK68zyK31418AK5fNccCdVuRnNpbxquCl8IqgFByOgWFivbiIlvb79wpSXi0O+8k8RCSsIpOquebusRVSg==}
'@tauri-apps/plugin-dialog@2.2.1':
resolution: {integrity: sha512-wZmCouo4PgTosh/UoejPw9DPs6RllS5Pp3fuOV2JobCu36mR5AXU2MzU9NZiVaFi/5Zfc8RN0IhcZHnksJ1o8A==}
'@tauri-apps/plugin-fs@2.2.0':
resolution: {integrity: sha512-+08mApuONKI8/sCNEZ6AR8vf5vI9DXD4YfrQ9NQmhRxYKMLVhRW164vdW5BSLmMpuevftpQ2FVoL9EFkfG9Z+g==}
'@tauri-apps/plugin-fs@2.2.1':
resolution: {integrity: sha512-KdGzvvA4Eg0Dhw55MwczFbjxLxsTx0FvwwC/0StXlr6IxwPUxh5ziZQoaugkBFs8t+wfebdQrjBEzd8NmmDXNw==}
'@tauri-apps/plugin-haptics@2.2.3':
resolution: {integrity: sha512-tHWAOR0TSOuWIdJ4Fh/4z+L8CvSfHdg5i7XfCqjErPu63PMf+2n856VIGcC6fjF29hf/vq+BiGApWt38n66Gvg==}
'@tauri-apps/plugin-haptics@2.2.4':
resolution: {integrity: sha512-h+YapjI0o6B1daj4gL7j8qKZUoxuZ09RWq+W8C62TXO5GHxc4YaywzGNGqq/3kJTYwv/3fw5o/pWO3AsN58RuQ==}
'@tauri-apps/plugin-http@2.3.0':
resolution: {integrity: sha512-pigTvz+zzAqbIhCzRiR1GE98Jw7A03j2V+Eiexr9thBI8VfMiwFQMcbgON51xlwnVaI72LdbYKNajU84im8tlg==}
'@tauri-apps/plugin-http@2.4.3':
resolution: {integrity: sha512-Us8X+FikzpaZRNr4kH4HLwyXascHbM42p6LxAqRTQnHPrrqp1usaH4vxWAZalPvTbHJ3gBEMJPHusFJgtjGJjA==}
'@tauri-apps/plugin-log@2.2.3':
resolution: {integrity: sha512-noDkZXU0kPG1bP3qoF6om+q5TgQ65LWpsV/TnfrHyg2lEn0vE+WJWTItiIh7OUA1/xxC+hdb9ODy2bM2e+jHEw==}
'@tauri-apps/plugin-log@2.3.1':
resolution: {integrity: sha512-nnKGHENWt7teqvUlIKxd6bp2wCUrrLvCvajN6CWbyrHBNKPi/pyKELzD511siEMDEdndbiZ/GEhiK0xBtZopRg==}
'@tauri-apps/plugin-opener@2.2.6':
resolution: {integrity: sha512-bSdkuP71ZQRepPOn8BOEdBKYJQvl6+jb160QtJX/i2H9BF6ZySY/kYljh76N2Ne5fJMQRge7rlKoStYQY5Jq1w==}
'@tauri-apps/plugin-os@2.2.0':
resolution: {integrity: sha512-HszbCdbisMlu5QhCNAN8YIWyz2v33abAWha6+uvV2CKX8P5VSct/y+kEe22JeyqrxCnWlQ3DRx7s49Byg7/0EA==}
'@tauri-apps/plugin-os@2.2.1':
resolution: {integrity: sha512-cNYpNri2CCc6BaNeB6G/mOtLvg8dFyFQyCUdf2y0K8PIAKGEWdEcu8DECkydU2B+oj4OJihDPD2de5K6cbVl9A==}
'@tauri-apps/plugin-process@2.2.0':
resolution: {integrity: sha512-uypN2Crmyop9z+KRJr3zl71OyVFgTuvHFjsJ0UxxQ/J5212jVa5w4nPEYjIewcn8bUEXacRebwE6F7owgrbhSw==}
'@tauri-apps/plugin-process@2.2.1':
resolution: {integrity: sha512-cF/k8J+YjjuowhNG1AboHNTlrGiOwgX5j6NzsX6WFf9FMzyZUchkCgZMxCdSE5NIgFX0vvOgLQhODFJgbMenLg==}
'@tauri-apps/plugin-shell@2.2.0':
resolution: {integrity: sha512-iC3Ic1hLmasoboG7BO+7p+AriSoqAwKrIk+Hpk+S/bjTQdXqbl2GbdclghI4gM32X0bls7xHzIFqhRdrlvJeaA==}
'@tauri-apps/plugin-shell@2.2.1':
resolution: {integrity: sha512-G1GFYyWe/KlCsymuLiNImUgC8zGY0tI0Y3p8JgBCWduR5IEXlIJS+JuG1qtveitwYXlfJrsExt3enhv5l2/yhA==}
'@tauri-apps/plugin-updater@2.5.1':
resolution: {integrity: sha512-7fNJraKRbKkxguMY5lG2W20pBvAUkLu+cqnbu0UcK7DqeZgrAnNECcGBIDG6fJ6C+0fAp2V2dMIgznhffOBCcg==}
'@tauri-apps/plugin-updater@2.7.0':
resolution: {integrity: sha512-oBug5UCH2wOsoYk0LW5LEMAT51mszjg11s8eungRH26x/qOrEjLvnuJJoxVVr9nsWowJ6vnpXKS+lUMfFTlvHQ==}
'@tsconfig/node10@1.0.11':
resolution: {integrity: sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==}
@@ -8093,7 +8100,7 @@ snapshots:
'@fabianlars/tauri-plugin-oauth@2.0.0':
dependencies:
'@tauri-apps/api': 2.3.0
'@tauri-apps/api': 2.4.1
'@fastify/busboy@2.1.1': {}
@@ -9247,98 +9254,102 @@ snapshots:
dependencies:
tslib: 2.8.1
'@tauri-apps/api@2.3.0': {}
'@tauri-apps/api@2.4.1': {}
'@tauri-apps/cli-darwin-arm64@2.3.1':
'@tauri-apps/cli-darwin-arm64@2.4.1':
optional: true
'@tauri-apps/cli-darwin-x64@2.3.1':
'@tauri-apps/cli-darwin-x64@2.4.1':
optional: true
'@tauri-apps/cli-linux-arm-gnueabihf@2.3.1':
'@tauri-apps/cli-linux-arm-gnueabihf@2.4.1':
optional: true
'@tauri-apps/cli-linux-arm64-gnu@2.3.1':
'@tauri-apps/cli-linux-arm64-gnu@2.4.1':
optional: true
'@tauri-apps/cli-linux-arm64-musl@2.3.1':
'@tauri-apps/cli-linux-arm64-musl@2.4.1':
optional: true
'@tauri-apps/cli-linux-x64-gnu@2.3.1':
'@tauri-apps/cli-linux-riscv64-gnu@2.4.1':
optional: true
'@tauri-apps/cli-linux-x64-musl@2.3.1':
'@tauri-apps/cli-linux-x64-gnu@2.4.1':
optional: true
'@tauri-apps/cli-win32-arm64-msvc@2.3.1':
'@tauri-apps/cli-linux-x64-musl@2.4.1':
optional: true
'@tauri-apps/cli-win32-ia32-msvc@2.3.1':
'@tauri-apps/cli-win32-arm64-msvc@2.4.1':
optional: true
'@tauri-apps/cli-win32-x64-msvc@2.3.1':
'@tauri-apps/cli-win32-ia32-msvc@2.4.1':
optional: true
'@tauri-apps/cli@2.3.1':
'@tauri-apps/cli-win32-x64-msvc@2.4.1':
optional: true
'@tauri-apps/cli@2.4.1':
optionalDependencies:
'@tauri-apps/cli-darwin-arm64': 2.3.1
'@tauri-apps/cli-darwin-x64': 2.3.1
'@tauri-apps/cli-linux-arm-gnueabihf': 2.3.1
'@tauri-apps/cli-linux-arm64-gnu': 2.3.1
'@tauri-apps/cli-linux-arm64-musl': 2.3.1
'@tauri-apps/cli-linux-x64-gnu': 2.3.1
'@tauri-apps/cli-linux-x64-musl': 2.3.1
'@tauri-apps/cli-win32-arm64-msvc': 2.3.1
'@tauri-apps/cli-win32-ia32-msvc': 2.3.1
'@tauri-apps/cli-win32-x64-msvc': 2.3.1
'@tauri-apps/cli-darwin-arm64': 2.4.1
'@tauri-apps/cli-darwin-x64': 2.4.1
'@tauri-apps/cli-linux-arm-gnueabihf': 2.4.1
'@tauri-apps/cli-linux-arm64-gnu': 2.4.1
'@tauri-apps/cli-linux-arm64-musl': 2.4.1
'@tauri-apps/cli-linux-riscv64-gnu': 2.4.1
'@tauri-apps/cli-linux-x64-gnu': 2.4.1
'@tauri-apps/cli-linux-x64-musl': 2.4.1
'@tauri-apps/cli-win32-arm64-msvc': 2.4.1
'@tauri-apps/cli-win32-ia32-msvc': 2.4.1
'@tauri-apps/cli-win32-x64-msvc': 2.4.1
'@tauri-apps/plugin-cli@2.2.0':
dependencies:
'@tauri-apps/api': 2.3.0
'@tauri-apps/api': 2.4.1
'@tauri-apps/plugin-deep-link@2.2.0':
'@tauri-apps/plugin-deep-link@2.2.1':
dependencies:
'@tauri-apps/api': 2.3.0
'@tauri-apps/api': 2.4.1
'@tauri-apps/plugin-dialog@2.2.0':
'@tauri-apps/plugin-dialog@2.2.1':
dependencies:
'@tauri-apps/api': 2.3.0
'@tauri-apps/api': 2.4.1
'@tauri-apps/plugin-fs@2.2.0':
'@tauri-apps/plugin-fs@2.2.1':
dependencies:
'@tauri-apps/api': 2.3.0
'@tauri-apps/api': 2.4.1
'@tauri-apps/plugin-haptics@2.2.3':
'@tauri-apps/plugin-haptics@2.2.4':
dependencies:
'@tauri-apps/api': 2.3.0
'@tauri-apps/api': 2.4.1
'@tauri-apps/plugin-http@2.3.0':
'@tauri-apps/plugin-http@2.4.3':
dependencies:
'@tauri-apps/api': 2.3.0
'@tauri-apps/api': 2.4.1
'@tauri-apps/plugin-log@2.2.3':
'@tauri-apps/plugin-log@2.3.1':
dependencies:
'@tauri-apps/api': 2.3.0
'@tauri-apps/api': 2.4.1
'@tauri-apps/plugin-opener@2.2.6':
dependencies:
'@tauri-apps/api': 2.3.0
'@tauri-apps/api': 2.4.1
'@tauri-apps/plugin-os@2.2.0':
'@tauri-apps/plugin-os@2.2.1':
dependencies:
'@tauri-apps/api': 2.3.0
'@tauri-apps/api': 2.4.1
'@tauri-apps/plugin-process@2.2.0':
'@tauri-apps/plugin-process@2.2.1':
dependencies:
'@tauri-apps/api': 2.3.0
'@tauri-apps/api': 2.4.1
'@tauri-apps/plugin-shell@2.2.0':
'@tauri-apps/plugin-shell@2.2.1':
dependencies:
'@tauri-apps/api': 2.3.0
'@tauri-apps/api': 2.4.1
'@tauri-apps/plugin-updater@2.5.1':
'@tauri-apps/plugin-updater@2.7.0':
dependencies:
'@tauri-apps/api': 2.3.0
'@tauri-apps/api': 2.4.1
'@tsconfig/node10@1.0.11':
optional: true