Add support for book import
This commit is contained in:
@@ -13,6 +13,7 @@
|
||||
"@tauri-apps/api": "2.0.0-rc.5",
|
||||
"@tauri-apps/plugin-dialog": "2.0.0-rc.1",
|
||||
"@tauri-apps/plugin-fs": "2.0.0-rc.2",
|
||||
"@tauri-apps/plugin-http": "^2.0.0",
|
||||
"@tauri-apps/plugin-log": "2.0.0-rc.1",
|
||||
"@tauri-apps/plugin-os": "2.0.0-rc.1",
|
||||
"@zip.js/zip.js": "^2.7.52",
|
||||
|
||||
Generated
+329
-1
@@ -688,6 +688,34 @@ version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e"
|
||||
|
||||
[[package]]
|
||||
name = "cookie"
|
||||
version = "0.18.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747"
|
||||
dependencies = [
|
||||
"percent-encoding",
|
||||
"time",
|
||||
"version_check",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cookie_store"
|
||||
version = "0.21.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4934e6b7e8419148b6ef56950d277af8561060b56afd59e2aadf98b59fce6baa"
|
||||
dependencies = [
|
||||
"cookie",
|
||||
"idna 0.5.0",
|
||||
"log",
|
||||
"publicsuffix",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"serde_json",
|
||||
"time",
|
||||
"url",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "core-foundation"
|
||||
version = "0.9.4"
|
||||
@@ -877,6 +905,12 @@ dependencies = [
|
||||
"syn 2.0.77",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "data-url"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5c297a1c74b71ae29df00c3e22dd9534821d60eb9af5a0192823fa2acea70c2a"
|
||||
|
||||
[[package]]
|
||||
name = "deranged"
|
||||
version = "0.3.11"
|
||||
@@ -1027,6 +1061,15 @@ version = "1.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4ef6b89e5b37196644d8796de5268852ff179b44e96276cf4290264843743bb7"
|
||||
|
||||
[[package]]
|
||||
name = "encoding_rs"
|
||||
version = "0.8.34"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b45de904aa0b010bce2ab45264d0631681847fa7b6f2eaa7dab7619943bc4f59"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "endi"
|
||||
version = "1.1.0"
|
||||
@@ -1624,6 +1667,25 @@ dependencies = [
|
||||
"syn 2.0.77",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "h2"
|
||||
version = "0.4.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "524e8ac6999421f49a846c2d4411f337e53497d8ec55d67753beffa43c5d9205"
|
||||
dependencies = [
|
||||
"atomic-waker",
|
||||
"bytes",
|
||||
"fnv",
|
||||
"futures-core",
|
||||
"futures-sink",
|
||||
"http",
|
||||
"indexmap 2.5.0",
|
||||
"slab",
|
||||
"tokio",
|
||||
"tokio-util",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.12.3"
|
||||
@@ -1738,6 +1800,7 @@ dependencies = [
|
||||
"bytes",
|
||||
"futures-channel",
|
||||
"futures-util",
|
||||
"h2",
|
||||
"http",
|
||||
"http-body",
|
||||
"httparse",
|
||||
@@ -1748,6 +1811,24 @@ dependencies = [
|
||||
"want",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hyper-rustls"
|
||||
version = "0.27.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "08afdbb5c31130e3034af566421053ab03787c640246a446327f550d11bcb333"
|
||||
dependencies = [
|
||||
"futures-util",
|
||||
"http",
|
||||
"hyper",
|
||||
"hyper-util",
|
||||
"rustls",
|
||||
"rustls-pki-types",
|
||||
"tokio",
|
||||
"tokio-rustls",
|
||||
"tower-service",
|
||||
"webpki-roots",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hyper-util"
|
||||
version = "0.1.8"
|
||||
@@ -1807,6 +1888,16 @@ version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
|
||||
|
||||
[[package]]
|
||||
name = "idna"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e14ddfc70884202db2244c223200c204c2bda1bc6e0998d11b5e024d657209e6"
|
||||
dependencies = [
|
||||
"unicode-bidi",
|
||||
"unicode-normalization",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "idna"
|
||||
version = "0.5.0"
|
||||
@@ -2846,6 +2937,12 @@ dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "psl-types"
|
||||
version = "2.0.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "33cb294fe86a74cbcf50d4445b37da762029549ebeea341421c7c70370f86cac"
|
||||
|
||||
[[package]]
|
||||
name = "ptr_meta"
|
||||
version = "0.1.4"
|
||||
@@ -2866,6 +2963,16 @@ dependencies = [
|
||||
"syn 1.0.109",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "publicsuffix"
|
||||
version = "2.2.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "96a8c1bda5ae1af7f99a2962e49df150414a43d62404644d98dd5c3a93d07457"
|
||||
dependencies = [
|
||||
"idna 0.3.0",
|
||||
"psl-types",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quick-xml"
|
||||
version = "0.32.0"
|
||||
@@ -2875,6 +2982,54 @@ dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quinn"
|
||||
version = "0.11.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8c7c5fdde3cdae7203427dc4f0a68fe0ed09833edc525a03456b153b79828684"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"pin-project-lite",
|
||||
"quinn-proto",
|
||||
"quinn-udp",
|
||||
"rustc-hash",
|
||||
"rustls",
|
||||
"socket2",
|
||||
"thiserror",
|
||||
"tokio",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quinn-proto"
|
||||
version = "0.11.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fadfaed2cd7f389d0161bb73eeb07b7b78f8691047a6f3e73caaeae55310a4a6"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"rand 0.8.5",
|
||||
"ring",
|
||||
"rustc-hash",
|
||||
"rustls",
|
||||
"slab",
|
||||
"thiserror",
|
||||
"tinyvec",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quinn-udp"
|
||||
version = "0.5.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4fe68c2e9e1a1234e218683dbdf9f9dfcb094113c5ac2b938dfcb9bab4c4140b"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"once_cell",
|
||||
"socket2",
|
||||
"tracing",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.37"
|
||||
@@ -2988,6 +3143,7 @@ dependencies = [
|
||||
"tauri-build",
|
||||
"tauri-plugin-dialog",
|
||||
"tauri-plugin-fs",
|
||||
"tauri-plugin-http",
|
||||
"tauri-plugin-log",
|
||||
"tauri-plugin-os",
|
||||
]
|
||||
@@ -3073,12 +3229,17 @@ checksum = "f8f4955649ef5c38cc7f9e8aa41761d48fb9677197daea9984dc54f56aad5e63"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"bytes",
|
||||
"cookie",
|
||||
"cookie_store",
|
||||
"encoding_rs",
|
||||
"futures-core",
|
||||
"futures-util",
|
||||
"h2",
|
||||
"http",
|
||||
"http-body",
|
||||
"http-body-util",
|
||||
"hyper",
|
||||
"hyper-rustls",
|
||||
"hyper-util",
|
||||
"ipnet",
|
||||
"js-sys",
|
||||
@@ -3087,11 +3248,17 @@ dependencies = [
|
||||
"once_cell",
|
||||
"percent-encoding",
|
||||
"pin-project-lite",
|
||||
"quinn",
|
||||
"rustls",
|
||||
"rustls-pemfile",
|
||||
"rustls-pki-types",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_urlencoded",
|
||||
"sync_wrapper",
|
||||
"system-configuration",
|
||||
"tokio",
|
||||
"tokio-rustls",
|
||||
"tokio-util",
|
||||
"tower-service",
|
||||
"url",
|
||||
@@ -3099,6 +3266,7 @@ dependencies = [
|
||||
"wasm-bindgen-futures",
|
||||
"wasm-streams",
|
||||
"web-sys",
|
||||
"webpki-roots",
|
||||
"windows-registry",
|
||||
]
|
||||
|
||||
@@ -3126,6 +3294,21 @@ dependencies = [
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ring"
|
||||
version = "0.17.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"cfg-if",
|
||||
"getrandom 0.2.15",
|
||||
"libc",
|
||||
"spin",
|
||||
"untrusted",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rkyv"
|
||||
version = "0.7.45"
|
||||
@@ -3177,6 +3360,12 @@ version = "0.1.24"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f"
|
||||
|
||||
[[package]]
|
||||
name = "rustc-hash"
|
||||
version = "2.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "583034fd73374156e66797ed8e5b0d5690409c9226b22d87cb7f19821c05d152"
|
||||
|
||||
[[package]]
|
||||
name = "rustc_version"
|
||||
version = "0.4.1"
|
||||
@@ -3199,6 +3388,46 @@ dependencies = [
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls"
|
||||
version = "0.23.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "415d9944693cb90382053259f89fbb077ea730ad7273047ec63b19bc9b160ba8"
|
||||
dependencies = [
|
||||
"once_cell",
|
||||
"ring",
|
||||
"rustls-pki-types",
|
||||
"rustls-webpki",
|
||||
"subtle",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls-pemfile"
|
||||
version = "2.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50"
|
||||
dependencies = [
|
||||
"rustls-pki-types",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls-pki-types"
|
||||
version = "1.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0e696e35370c65c9c541198af4543ccd580cf17fc25d8e05c5a242b202488c55"
|
||||
|
||||
[[package]]
|
||||
name = "rustls-webpki"
|
||||
version = "0.102.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "64ca1bc8749bd4cf37b5ce386cc146580777b4e8572c7b97baf22c83f444bee9"
|
||||
dependencies = [
|
||||
"ring",
|
||||
"rustls-pki-types",
|
||||
"untrusted",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustversion"
|
||||
version = "1.0.17"
|
||||
@@ -3578,6 +3807,12 @@ dependencies = [
|
||||
"system-deps",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "spin"
|
||||
version = "0.9.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67"
|
||||
|
||||
[[package]]
|
||||
name = "stable_deref_trait"
|
||||
version = "1.2.0"
|
||||
@@ -3631,6 +3866,12 @@ version = "0.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
|
||||
|
||||
[[package]]
|
||||
name = "subtle"
|
||||
version = "2.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
|
||||
|
||||
[[package]]
|
||||
name = "swift-rs"
|
||||
version = "1.0.7"
|
||||
@@ -3694,6 +3935,27 @@ dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "system-configuration"
|
||||
version = "0.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b"
|
||||
dependencies = [
|
||||
"bitflags 2.6.0",
|
||||
"core-foundation 0.9.4",
|
||||
"system-configuration-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "system-configuration-sys"
|
||||
version = "0.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4"
|
||||
dependencies = [
|
||||
"core-foundation-sys",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "system-deps"
|
||||
version = "6.2.2"
|
||||
@@ -3940,6 +4202,28 @@ dependencies = [
|
||||
"uuid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tauri-plugin-http"
|
||||
version = "2.0.0-rc.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cf6e753f6b98a326ab5e2d604973e761a7970dec0c0bcd5ba83ce7392623dfa8"
|
||||
dependencies = [
|
||||
"data-url",
|
||||
"http",
|
||||
"regex",
|
||||
"reqwest",
|
||||
"schemars",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tauri",
|
||||
"tauri-plugin",
|
||||
"tauri-plugin-fs",
|
||||
"thiserror",
|
||||
"tokio",
|
||||
"url",
|
||||
"urlpattern",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tauri-plugin-log"
|
||||
version = "2.0.0-rc.2"
|
||||
@@ -4191,10 +4475,33 @@ dependencies = [
|
||||
"pin-project-lite",
|
||||
"signal-hook-registry",
|
||||
"socket2",
|
||||
"tokio-macros",
|
||||
"tracing",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-macros"
|
||||
version = "2.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.77",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-rustls"
|
||||
version = "0.26.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0c7bc40d0e5a97695bb96e27995cd3a08538541b0a846f65bba7a359f36700d4"
|
||||
dependencies = [
|
||||
"rustls",
|
||||
"rustls-pki-types",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-util"
|
||||
version = "0.7.12"
|
||||
@@ -4473,6 +4780,12 @@ version = "1.12.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493"
|
||||
|
||||
[[package]]
|
||||
name = "untrusted"
|
||||
version = "0.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
|
||||
|
||||
[[package]]
|
||||
name = "url"
|
||||
version = "2.5.2"
|
||||
@@ -4480,7 +4793,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "22784dbdf76fdde8af1aeda5622b546b422b6fc585325248a2bf9f5e41e94d6c"
|
||||
dependencies = [
|
||||
"form_urlencoded",
|
||||
"idna",
|
||||
"idna 0.5.0",
|
||||
"percent-encoding",
|
||||
"serde",
|
||||
]
|
||||
@@ -4727,6 +5040,15 @@ dependencies = [
|
||||
"system-deps",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "webpki-roots"
|
||||
version = "0.26.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "841c67bff177718f1d4dfefde8d8f0e78f9b6589319ba88312f567fc5841a958"
|
||||
dependencies = [
|
||||
"rustls-pki-types",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "webview2-com"
|
||||
version = "0.33.0"
|
||||
@@ -5303,6 +5625,12 @@ dependencies = [
|
||||
"syn 2.0.77",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zeroize"
|
||||
version = "1.8.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde"
|
||||
|
||||
[[package]]
|
||||
name = "zvariant"
|
||||
version = "4.0.0"
|
||||
|
||||
@@ -27,3 +27,4 @@ tauri-plugin-log = "2.0.0-rc"
|
||||
tauri-plugin-fs = "2.0.0-rc"
|
||||
tauri-plugin-dialog = "2.0.0-rc"
|
||||
tauri-plugin-os = "2.0.0-rc"
|
||||
tauri-plugin-http = "2.0.0-rc"
|
||||
|
||||
@@ -34,6 +34,7 @@
|
||||
]
|
||||
},
|
||||
"dialog:default",
|
||||
"os:default"
|
||||
"os:default",
|
||||
"http:default"
|
||||
]
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||
pub fn run() {
|
||||
tauri::Builder::default()
|
||||
.plugin(tauri_plugin_http::init())
|
||||
.plugin(tauri_plugin_os::init())
|
||||
.plugin(tauri_plugin_dialog::init())
|
||||
.plugin(tauri_plugin_fs::init())
|
||||
|
||||
@@ -16,7 +16,7 @@ const LibraryPage = () => {
|
||||
const { envConfig } = useEnv();
|
||||
const [appState, setAppState] = useState<AppState>('Init');
|
||||
const [libraryBooks, setLibraryBooks] = useState<Book[]>([]);
|
||||
const [loading, setLoading] = useState<boolean>(false);
|
||||
const [loading, setLoading] = useState<boolean>(true);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (appState !== 'Init') return;
|
||||
@@ -47,7 +47,7 @@ const LibraryPage = () => {
|
||||
appService.selectFiles('Select Books', ['epub', 'pdf']).then(async (files) => {
|
||||
setLoading(true);
|
||||
for (const file of files) {
|
||||
await dav?.addBook(file, libraryBooks).then(() => {});
|
||||
await appService.importBook(file, libraryBooks);
|
||||
setLibraryBooks(libraryBooks);
|
||||
}
|
||||
setLoading(false);
|
||||
@@ -62,7 +62,7 @@ const LibraryPage = () => {
|
||||
<Spinner loading={loading} />
|
||||
<Bookshelf libraryBooks={libraryBooks} onImportBooks={handleImportBooks} />
|
||||
</div>
|
||||
{libraryBooks.length === 0 && (
|
||||
{!loading && libraryBooks.length === 0 && (
|
||||
<div className='hero min-h-screen'>
|
||||
<div className='hero-content text-neutral-content text-center'>
|
||||
<div className='max-w-md'>
|
||||
|
||||
@@ -41,6 +41,15 @@ export interface BookDoc {
|
||||
getCover(): Promise<Blob | null>;
|
||||
}
|
||||
|
||||
export const EXTS: Record<BookFormat, string> = {
|
||||
EPUB: 'epub',
|
||||
PDF: 'pdf',
|
||||
MOBI: 'mobi',
|
||||
CBZ: 'cbz',
|
||||
FB2: 'fb2',
|
||||
FBZ: 'fbz',
|
||||
};
|
||||
|
||||
export class DocumentLoader {
|
||||
private file: DocumentFile;
|
||||
|
||||
@@ -86,18 +95,22 @@ export class DocumentLoader {
|
||||
}
|
||||
|
||||
private isCBZ(): boolean {
|
||||
return this.file.type === 'application/vnd.comicbook+zip' || this.file.name.endsWith('.cbz');
|
||||
return (
|
||||
this.file.type === 'application/vnd.comicbook+zip' || this.file.name.endsWith(`.${EXTS.CBZ}`)
|
||||
);
|
||||
}
|
||||
|
||||
private isFB2(): boolean {
|
||||
return this.file.type === 'application/x-fictionbook+xml' || this.file.name.endsWith('.fb2');
|
||||
return (
|
||||
this.file.type === 'application/x-fictionbook+xml' || this.file.name.endsWith(`.${EXTS.FB2}`)
|
||||
);
|
||||
}
|
||||
|
||||
private isFBZ(): boolean {
|
||||
return (
|
||||
this.file.type === 'application/x-zip-compressed-fb2' ||
|
||||
this.file.name.endsWith('.fb2.zip') ||
|
||||
this.file.name.endsWith('.fbz')
|
||||
this.file.name.endsWith(`.${EXTS.FBZ}`)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -116,7 +129,7 @@ export class DocumentLoader {
|
||||
book = makeComicBook(loader, this.file);
|
||||
format = 'CBZ';
|
||||
} else if (this.isFBZ()) {
|
||||
const entry = entries.find((entry) => entry.filename.endsWith('.fb2'));
|
||||
const entry = entries.find((entry) => entry.filename.endsWith(`.${EXTS.FB2}`));
|
||||
const blob = await loader.loadBlob((entry ?? entries[0]!).filename);
|
||||
const { makeFB2 } = await import('foliate-js/fb2.js');
|
||||
book = await makeFB2(blob);
|
||||
@@ -1,117 +1,31 @@
|
||||
import {
|
||||
exists,
|
||||
mkdir,
|
||||
readTextFile,
|
||||
readFile,
|
||||
writeTextFile,
|
||||
writeFile,
|
||||
readDir,
|
||||
remove,
|
||||
BaseDirectory,
|
||||
} from '@tauri-apps/plugin-fs';
|
||||
import {
|
||||
join,
|
||||
appConfigDir,
|
||||
appDataDir,
|
||||
appCacheDir,
|
||||
appLogDir,
|
||||
documentDir,
|
||||
} from '@tauri-apps/api/path';
|
||||
import { convertFileSrc } from '@tauri-apps/api/core';
|
||||
import { open, message } from '@tauri-apps/plugin-dialog';
|
||||
import { join, documentDir } from '@tauri-apps/api/path';
|
||||
|
||||
import { Book } from '../types/book';
|
||||
import { Book, BookConfig, BookFormat } from '../types/book';
|
||||
import { SystemSettings } from '../types/settings';
|
||||
import { AppService, BaseDir, ToastType } from '../types/system';
|
||||
import { AppService, ToastType } from '../types/system';
|
||||
import { LOCAL_BOOKS_SUBDIR } from './constants';
|
||||
import { resolvePath, nativeFileSystem } from './nativeFileSystem';
|
||||
import {
|
||||
getBaseFilename,
|
||||
getConfigFilename,
|
||||
getCoverFilename,
|
||||
getDir,
|
||||
getFilename,
|
||||
getLibraryFilename,
|
||||
INIT_BOOK_CONFIG,
|
||||
} from '@/utils/book';
|
||||
import { BookDoc, DocumentLoader } from '@/libs/document';
|
||||
import { RemoteFile } from '@/utils/file';
|
||||
import { partialMD5 } from '@/utils/md5';
|
||||
|
||||
let BOOKS_DIR = '';
|
||||
|
||||
function resolvePath(
|
||||
fp: string,
|
||||
base: BaseDir,
|
||||
): { baseDir: number; base: BaseDir; fp: string; dir: () => Promise<string> } {
|
||||
switch (base) {
|
||||
case 'Settings':
|
||||
return { baseDir: BaseDirectory.AppConfig, fp, base, dir: appConfigDir };
|
||||
case 'Data':
|
||||
return { baseDir: BaseDirectory.AppData, fp, base, dir: appDataDir };
|
||||
case 'Cache':
|
||||
return { baseDir: BaseDirectory.AppCache, fp, base, dir: appCacheDir };
|
||||
case 'Log':
|
||||
return { baseDir: BaseDirectory.AppLog, fp, base, dir: appLogDir };
|
||||
case 'Books':
|
||||
return {
|
||||
baseDir: BaseDirectory.Document,
|
||||
fp: `${LOCAL_BOOKS_SUBDIR}/${fp}`,
|
||||
base,
|
||||
dir: () => new Promise((r) => r(`${BOOKS_DIR}/`)),
|
||||
};
|
||||
default:
|
||||
return {
|
||||
baseDir: BaseDirectory.Temp,
|
||||
fp,
|
||||
base,
|
||||
dir: () => new Promise((r) => r('')),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const SETTINGS_PATH = resolvePath('settings.json', 'Settings');
|
||||
|
||||
export const nativeAppService: AppService = {
|
||||
fs: {
|
||||
async readFile(path: string, base: BaseDir, mode: 'text' | 'binary') {
|
||||
const { fp, baseDir } = resolvePath(path, base);
|
||||
|
||||
return mode === 'text'
|
||||
? (readTextFile(fp, base && { baseDir }) as Promise<string>)
|
||||
: ((await readFile(fp, base && { baseDir })).buffer as ArrayBuffer);
|
||||
},
|
||||
async writeFile(path: string, base: BaseDir, content: string | ArrayBuffer) {
|
||||
const { fp, baseDir } = resolvePath(path, base);
|
||||
|
||||
return typeof content === 'string'
|
||||
? writeTextFile(fp, content, base && { baseDir })
|
||||
: writeFile(fp, new Uint8Array(content), base && { baseDir });
|
||||
},
|
||||
async removeFile(path: string, base: BaseDir) {
|
||||
const { fp, baseDir } = resolvePath(path, base);
|
||||
|
||||
return remove(fp, base && { baseDir });
|
||||
},
|
||||
async createDir(path: string, base: BaseDir, recursive = false) {
|
||||
const { fp, baseDir } = resolvePath(path, base);
|
||||
|
||||
await mkdir(fp, base && { baseDir, recursive });
|
||||
},
|
||||
async removeDir(path: string, base: BaseDir, recursive = false) {
|
||||
const { fp, baseDir } = resolvePath(path, base);
|
||||
|
||||
await remove(fp, base && { baseDir, recursive });
|
||||
},
|
||||
async readDir(path: string, base: BaseDir) {
|
||||
const { fp, baseDir } = resolvePath(path, base);
|
||||
|
||||
const list = await readDir(fp, base && { baseDir });
|
||||
return list.map((entity) => {
|
||||
return {
|
||||
path: entity.name,
|
||||
isDir: entity.isDirectory,
|
||||
};
|
||||
});
|
||||
},
|
||||
async exists(path: string, base: BaseDir) {
|
||||
const { fp, baseDir } = resolvePath(path, base);
|
||||
|
||||
try {
|
||||
const res = await exists(fp, base && { baseDir });
|
||||
return res;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
},
|
||||
fs: nativeFileSystem,
|
||||
loadSettings: async () => {
|
||||
let settings: SystemSettings;
|
||||
const { fp, base } = SETTINGS_PATH;
|
||||
@@ -172,22 +86,114 @@ export const nativeAppService: AppService = {
|
||||
showMessage: async (msg: string, kind: ToastType = 'info', title?: string, okLabel?: string) => {
|
||||
await message(msg, { kind, title, okLabel });
|
||||
},
|
||||
importBook: async (
|
||||
file: string | File,
|
||||
books: Book[],
|
||||
overwrite: boolean = false,
|
||||
): Promise<Book[]> => {
|
||||
try {
|
||||
let loadedBook: BookDoc;
|
||||
let format: BookFormat;
|
||||
let filename: string;
|
||||
let fileobj: File;
|
||||
|
||||
try {
|
||||
if (typeof file === 'string') {
|
||||
filename = file;
|
||||
fileobj = await new RemoteFile(nativeAppService.fs.getURL(file), file).open();
|
||||
} else {
|
||||
filename = file.name;
|
||||
fileobj = file;
|
||||
}
|
||||
({ book: loadedBook, format } = await new DocumentLoader(fileobj).open());
|
||||
if (!loadedBook.metadata.title) {
|
||||
loadedBook.metadata.title = getBaseFilename(filename);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
throw new Error(`Failed to open the book: ${(error as Error).message || error}`);
|
||||
}
|
||||
|
||||
const hash = await partialMD5(fileobj);
|
||||
const existingBook = books.filter((b) => b.hash === hash)[0];
|
||||
if (existingBook) {
|
||||
if (existingBook.isRemoved) {
|
||||
delete existingBook.isRemoved;
|
||||
}
|
||||
existingBook.lastUpdated = Date.now();
|
||||
}
|
||||
|
||||
const book: Book = {
|
||||
hash,
|
||||
format,
|
||||
title: loadedBook.metadata.title,
|
||||
author: loadedBook.metadata.author,
|
||||
lastUpdated: Date.now(),
|
||||
};
|
||||
book.coverImageUrl = nativeAppService.getCoverImageUrl(book);
|
||||
|
||||
if (!(await nativeAppService.fs.exists(getDir(book), 'Books'))) {
|
||||
await nativeAppService.fs.createDir(getDir(book), 'Books');
|
||||
}
|
||||
if (!(await nativeAppService.fs.exists(getFilename(book), 'Books')) || overwrite) {
|
||||
if (typeof file === 'string') {
|
||||
await nativeAppService.fs.copyFile(file, getFilename(book), 'Books');
|
||||
} else {
|
||||
await nativeAppService.fs.writeFile(getFilename(book), 'Books', await file.arrayBuffer());
|
||||
}
|
||||
}
|
||||
if (!(await nativeAppService.fs.exists(getCoverFilename(book), 'Books')) || overwrite) {
|
||||
const cover = await loadedBook.getCover();
|
||||
if (cover) {
|
||||
await nativeAppService.fs.writeFile(
|
||||
getCoverFilename(book),
|
||||
'Books',
|
||||
await cover.arrayBuffer(),
|
||||
);
|
||||
}
|
||||
}
|
||||
// Never overwrite the config file only when it's not existed
|
||||
if (!existingBook) {
|
||||
await nativeAppService.saveBookConfig(book, INIT_BOOK_CONFIG);
|
||||
books.splice(0, 0, book);
|
||||
}
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
return books;
|
||||
},
|
||||
loadBookConfig: async (book: Book): Promise<BookConfig> => {
|
||||
try {
|
||||
const str = await nativeAppService.fs.readFile(getConfigFilename(book), 'Books', 'text');
|
||||
return JSON.parse(str as string);
|
||||
} catch {
|
||||
return INIT_BOOK_CONFIG;
|
||||
}
|
||||
},
|
||||
saveBookConfig: async (book: Book, config: BookConfig) => {
|
||||
await nativeAppService.fs.writeFile(getConfigFilename(book), 'Books', JSON.stringify(config));
|
||||
},
|
||||
loadLibraryBooks: async () => {
|
||||
let books: Book[] = [];
|
||||
const libraryFilename = getLibraryFilename();
|
||||
try {
|
||||
const txt = await nativeAppService.fs.readFile('books.json', 'Books', 'text');
|
||||
const txt = await nativeAppService.fs.readFile(libraryFilename, 'Books', 'text');
|
||||
books = JSON.parse(txt as string);
|
||||
} catch {
|
||||
await nativeAppService.fs.writeFile('books.json', 'Books', '[]');
|
||||
await nativeAppService.fs.writeFile(libraryFilename, 'Books', '[]');
|
||||
}
|
||||
|
||||
books.forEach((book) => {
|
||||
book.coverImageUrl = nativeAppService.generateCoverUrl(book);
|
||||
book.coverImageUrl = nativeAppService.getCoverImageUrl(book);
|
||||
});
|
||||
|
||||
return books;
|
||||
},
|
||||
generateCoverUrl: (book: Book) => {
|
||||
return convertFileSrc(`${BOOKS_DIR}/${book.hash}/cover.png`);
|
||||
saveLibraryBooks: async (books: Book[]) => {
|
||||
await nativeAppService.fs.writeFile(getLibraryFilename(), 'Books', JSON.stringify(books));
|
||||
},
|
||||
getCoverImageUrl: (book: Book) => {
|
||||
return convertFileSrc(`${BOOKS_DIR}/${getCoverFilename(book)}`);
|
||||
},
|
||||
};
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
import {
|
||||
exists,
|
||||
mkdir,
|
||||
readTextFile,
|
||||
readFile,
|
||||
writeTextFile,
|
||||
writeFile,
|
||||
readDir,
|
||||
remove,
|
||||
BaseDirectory,
|
||||
copyFile,
|
||||
} from '@tauri-apps/plugin-fs';
|
||||
import { convertFileSrc } from '@tauri-apps/api/core';
|
||||
|
||||
import { FileSystem, BaseDir } from '../types/system';
|
||||
import { LOCAL_BOOKS_SUBDIR } from './constants';
|
||||
|
||||
export const resolvePath = (
|
||||
fp: string,
|
||||
base: BaseDir,
|
||||
): { baseDir: number; base: BaseDir; fp: string } => {
|
||||
switch (base) {
|
||||
case 'Settings':
|
||||
return { baseDir: BaseDirectory.AppConfig, fp, base };
|
||||
case 'Data':
|
||||
return { baseDir: BaseDirectory.AppData, fp, base };
|
||||
case 'Cache':
|
||||
return { baseDir: BaseDirectory.AppCache, fp, base };
|
||||
case 'Log':
|
||||
return { baseDir: BaseDirectory.AppLog, fp, base };
|
||||
case 'Books':
|
||||
return {
|
||||
baseDir: BaseDirectory.Document,
|
||||
fp: `${LOCAL_BOOKS_SUBDIR}/${fp}`,
|
||||
base,
|
||||
};
|
||||
default:
|
||||
return {
|
||||
baseDir: BaseDirectory.Temp,
|
||||
fp,
|
||||
base,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
export const nativeFileSystem: FileSystem = {
|
||||
getURL(path: string) {
|
||||
return convertFileSrc(path);
|
||||
},
|
||||
async copyFile(srcPath: string, dstPath: string, base: BaseDir) {
|
||||
const { fp, baseDir } = resolvePath(dstPath, base);
|
||||
await copyFile(srcPath, fp, base && { toPathBaseDir: baseDir });
|
||||
},
|
||||
async readFile(path: string, base: BaseDir, mode: 'text' | 'binary') {
|
||||
const { fp, baseDir } = resolvePath(path, base);
|
||||
|
||||
return mode === 'text'
|
||||
? (readTextFile(fp, base && { baseDir }) as Promise<string>)
|
||||
: ((await readFile(fp, base && { baseDir })).buffer as ArrayBuffer);
|
||||
},
|
||||
async writeFile(path: string, base: BaseDir, content: string | ArrayBuffer) {
|
||||
const { fp, baseDir } = resolvePath(path, base);
|
||||
|
||||
return typeof content === 'string'
|
||||
? writeTextFile(fp, content, base && { baseDir })
|
||||
: writeFile(fp, new Uint8Array(content), base && { baseDir });
|
||||
},
|
||||
async removeFile(path: string, base: BaseDir) {
|
||||
const { fp, baseDir } = resolvePath(path, base);
|
||||
|
||||
return remove(fp, base && { baseDir });
|
||||
},
|
||||
async createDir(path: string, base: BaseDir, recursive = false) {
|
||||
const { fp, baseDir } = resolvePath(path, base);
|
||||
|
||||
await mkdir(fp, base && { baseDir, recursive });
|
||||
},
|
||||
async removeDir(path: string, base: BaseDir, recursive = false) {
|
||||
const { fp, baseDir } = resolvePath(path, base);
|
||||
|
||||
await remove(fp, base && { baseDir, recursive });
|
||||
},
|
||||
async readDir(path: string, base: BaseDir) {
|
||||
const { fp, baseDir } = resolvePath(path, base);
|
||||
|
||||
const list = await readDir(fp, base && { baseDir });
|
||||
return list.map((entity) => {
|
||||
return {
|
||||
path: entity.name,
|
||||
isDir: entity.isDirectory,
|
||||
};
|
||||
});
|
||||
},
|
||||
async exists(path: string, base: BaseDir) {
|
||||
const { fp, baseDir } = resolvePath(path, base);
|
||||
|
||||
try {
|
||||
const res = await exists(fp, base && { baseDir });
|
||||
return res;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
};
|
||||
@@ -1,10 +1,12 @@
|
||||
import { SystemSettings } from './settings';
|
||||
import { Book } from './book';
|
||||
import { Book, BookConfig } from './book';
|
||||
|
||||
export type BaseDir = 'Books' | 'Settings' | 'Data' | 'Log' | 'Cache' | 'None';
|
||||
export type ToastType = 'info' | 'warning' | 'error';
|
||||
|
||||
export interface FileSystem {
|
||||
getURL(path: string): string;
|
||||
copyFile(srcPath: string, dstPath: string, base: BaseDir): Promise<void>;
|
||||
readFile(path: string, base: BaseDir, mode: 'text' | 'binary'): Promise<string | ArrayBuffer>;
|
||||
writeFile(path: string, base: BaseDir, content: string | ArrayBuffer): Promise<void>;
|
||||
removeFile(path: string, base: BaseDir): Promise<void>;
|
||||
@@ -23,6 +25,10 @@ export interface AppService {
|
||||
selectFiles(name: string, extensions: string[]): Promise<string[]>;
|
||||
showMessage(msg: string, kind?: ToastType, title?: string, okLabel?: string): Promise<void>;
|
||||
|
||||
importBook(file: string | File, books: Book[], overwrite?: boolean): Promise<Book[]>;
|
||||
loadBookConfig(book: Book): Promise<BookConfig>;
|
||||
saveBookConfig(book: Book, config: BookConfig): Promise<void>;
|
||||
loadLibraryBooks(): Promise<Book[]>;
|
||||
generateCoverUrl(book: Book): string;
|
||||
saveLibraryBooks(books: Book[]): Promise<void>;
|
||||
getCoverImageUrl(book: Book): string;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
import { EXTS } from '@/libs/document';
|
||||
import { Book, BookConfig } from '@/types/book';
|
||||
|
||||
export const getDir = (book: Book) => {
|
||||
return `${book.hash}`;
|
||||
};
|
||||
export const getLibraryFilename = () => {
|
||||
return 'library.json';
|
||||
};
|
||||
export const getFilename = (book: Book) => {
|
||||
return `${book.hash}/${book.title}.${EXTS[book.format]}`;
|
||||
};
|
||||
export const getCoverFilename = (book: Book) => {
|
||||
return `${book.hash}/cover.png`;
|
||||
};
|
||||
export const getConfigFilename = (book: Book) => {
|
||||
return `${book.hash}/config.json`;
|
||||
};
|
||||
export const isBookFile = (filename: string) => {
|
||||
return Object.values(EXTS).includes(filename.split('.').pop()!);
|
||||
};
|
||||
export const getBaseFilename = (filename: string) => {
|
||||
const normalizedPath = filename.replace(/\\/g, '/');
|
||||
const baseName = normalizedPath.split('/').pop()?.split('.').slice(0, -1).join('.') || '';
|
||||
return baseName;
|
||||
};
|
||||
export const INIT_BOOK_CONFIG: BookConfig = {
|
||||
lastUpdated: 0,
|
||||
remoteProgress: 0,
|
||||
localProgress: 0,
|
||||
bookmarks: [],
|
||||
annotations: [],
|
||||
};
|
||||
@@ -0,0 +1,188 @@
|
||||
class RemoteBlobSlice extends Blob {
|
||||
#dataPromise: Promise<ArrayBuffer>;
|
||||
#type: string;
|
||||
|
||||
constructor(dataPromise: Promise<ArrayBuffer>, type: string) {
|
||||
super();
|
||||
this.#dataPromise = dataPromise;
|
||||
this.#type = type;
|
||||
}
|
||||
|
||||
override async arrayBuffer() {
|
||||
const data = await this.#dataPromise;
|
||||
return data;
|
||||
}
|
||||
|
||||
override async text() {
|
||||
const data = await this.#dataPromise;
|
||||
return new TextDecoder().decode(data);
|
||||
}
|
||||
|
||||
override stream() {
|
||||
return new ReadableStream({
|
||||
start: async (controller) => {
|
||||
const data = await this.#dataPromise;
|
||||
const reader = new ReadableStream({
|
||||
start(controller) {
|
||||
controller.enqueue(new Uint8Array(data));
|
||||
controller.close();
|
||||
},
|
||||
}).getReader();
|
||||
const pump = () =>
|
||||
reader.read().then(({ done, value }): Promise<void> => {
|
||||
if (done) {
|
||||
controller.close();
|
||||
return Promise.resolve();
|
||||
}
|
||||
controller.enqueue(value);
|
||||
return pump();
|
||||
});
|
||||
return pump();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
override get type() {
|
||||
return this.#type;
|
||||
}
|
||||
}
|
||||
|
||||
export class RemoteFile extends File {
|
||||
url: string;
|
||||
#name: string;
|
||||
#lastModified: number;
|
||||
#size: number = -1;
|
||||
#type: string = '';
|
||||
#cache: Map<number, ArrayBuffer> = new Map(); // LRU cache
|
||||
#order: number[] = [];
|
||||
|
||||
static MAX_CACHE_CHUNK_SIZE = 1024 * 128;
|
||||
static MAX_CACHE_ITEMS_SIZE: number = 10;
|
||||
|
||||
constructor(url: string, name = 'remote-file', type = '', lastModified = Date.now()) {
|
||||
super([], name, { type: type, lastModified });
|
||||
this.url = url;
|
||||
this.#name = name;
|
||||
this.#type = type;
|
||||
this.#lastModified = lastModified;
|
||||
}
|
||||
|
||||
override get name() {
|
||||
return this.#name;
|
||||
}
|
||||
|
||||
override get type() {
|
||||
return this.#type;
|
||||
}
|
||||
|
||||
override get size() {
|
||||
return this.#size;
|
||||
}
|
||||
|
||||
override get lastModified() {
|
||||
return this.#lastModified;
|
||||
}
|
||||
|
||||
async open() {
|
||||
const response = await fetch(this.url, { method: 'HEAD' });
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch file size: ${response.status}`);
|
||||
}
|
||||
this.#size = Number(response.headers.get('content-length'));
|
||||
this.#type = response.headers.get('content-type') || '';
|
||||
return this;
|
||||
}
|
||||
|
||||
async fetchRangePart(start: number, end: number) {
|
||||
start = Math.max(0, start);
|
||||
end = Math.min(this.size - 1, end);
|
||||
// console.log(`Fetching range: ${start}-${end}, size: ${end - start + 1}`);
|
||||
const response = await fetch(this.url, { headers: { Range: `bytes=${start}-${end}` } });
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch range: ${response.status}`);
|
||||
}
|
||||
return response.arrayBuffer();
|
||||
}
|
||||
|
||||
async fetchRange(start: number, end: number): Promise<ArrayBuffer> {
|
||||
const rangeSize = end - start + 1;
|
||||
const MAX_RANGE_LEN = 1024 * 1000;
|
||||
|
||||
if (rangeSize > MAX_RANGE_LEN) {
|
||||
const buffers: ArrayBuffer[] = [];
|
||||
for (let currentStart = start; currentStart <= end; currentStart += MAX_RANGE_LEN) {
|
||||
const currentEnd = Math.min(currentStart + MAX_RANGE_LEN - 1, end);
|
||||
buffers.push(await this.fetchRangePart(currentStart, currentEnd));
|
||||
}
|
||||
const totalSize = buffers.reduce((sum, buffer) => sum + buffer.byteLength, 0);
|
||||
const combinedBuffer = new Uint8Array(totalSize);
|
||||
let offset = 0;
|
||||
for (const buffer of buffers) {
|
||||
combinedBuffer.set(new Uint8Array(buffer), offset);
|
||||
offset += buffer.byteLength;
|
||||
}
|
||||
return combinedBuffer.buffer;
|
||||
} else if (rangeSize > RemoteFile.MAX_CACHE_CHUNK_SIZE) {
|
||||
return this.fetchRangePart(start, end);
|
||||
} else {
|
||||
let cachedChunkStart = Array.from(this.#cache.keys()).find((chunkStart) => {
|
||||
const buffer = this.#cache.get(chunkStart)!;
|
||||
const bufferSize = buffer.byteLength;
|
||||
return start >= chunkStart && end <= chunkStart + bufferSize;
|
||||
});
|
||||
if (cachedChunkStart !== undefined) {
|
||||
this.#updateAccessOrder(cachedChunkStart);
|
||||
const buffer = this.#cache.get(cachedChunkStart)!;
|
||||
const offset = start - cachedChunkStart;
|
||||
return buffer.slice(offset, offset + rangeSize);
|
||||
}
|
||||
cachedChunkStart = await this.#fetchAndCacheChunk(start, end);
|
||||
const buffer = this.#cache.get(cachedChunkStart)!;
|
||||
const offset = start - cachedChunkStart;
|
||||
return buffer.slice(offset, offset + rangeSize);
|
||||
}
|
||||
}
|
||||
|
||||
async #fetchAndCacheChunk(start: number, end: number): Promise<number> {
|
||||
const chunkStart = Math.max(0, start - 1024);
|
||||
const chunkEnd = Math.max(end, start + RemoteFile.MAX_CACHE_CHUNK_SIZE - 1024 - 1);
|
||||
this.#cache.set(chunkStart, await this.fetchRangePart(chunkStart, chunkEnd));
|
||||
this.#updateAccessOrder(chunkStart);
|
||||
this.#ensureCacheSize();
|
||||
return chunkStart;
|
||||
}
|
||||
|
||||
#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 > RemoteFile.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 {
|
||||
// console.log(`Slicing: ${start}-${end}, size: ${end - start}`);
|
||||
const dataPromise = this.fetchRange(start, end - 1);
|
||||
|
||||
return new RemoteBlobSlice(dataPromise, contentType);
|
||||
}
|
||||
|
||||
override async text() {
|
||||
const blob = this.slice(0, this.size);
|
||||
return blob.text();
|
||||
}
|
||||
|
||||
override async arrayBuffer() {
|
||||
const blob = this.slice(0, this.size);
|
||||
return blob.arrayBuffer();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import { md5 } from 'js-md5';
|
||||
|
||||
export async function partialMD5(file: File): Promise<string> {
|
||||
const step = 1024;
|
||||
const size = 1024;
|
||||
const hasher = md5.create();
|
||||
|
||||
for (let i = -1; i <= 10; i++) {
|
||||
const start = Math.min(file.size, step << (2 * i));
|
||||
const end = Math.min(start + size, file.size);
|
||||
|
||||
if (start >= file.size) break;
|
||||
|
||||
const blobSlice = file.slice(start, end);
|
||||
const arrayBuffer = await blobSlice.arrayBuffer();
|
||||
const uint8Array = new Uint8Array(arrayBuffer);
|
||||
|
||||
hasher.update(uint8Array);
|
||||
}
|
||||
|
||||
return hasher.hex();
|
||||
}
|
||||
Generated
+15
@@ -44,6 +44,9 @@ importers:
|
||||
'@tauri-apps/plugin-fs':
|
||||
specifier: 2.0.0-rc.2
|
||||
version: 2.0.0-rc.2
|
||||
'@tauri-apps/plugin-http':
|
||||
specifier: ^2.0.0
|
||||
version: 2.0.0
|
||||
'@tauri-apps/plugin-log':
|
||||
specifier: 2.0.0-rc.1
|
||||
version: 2.0.0-rc.1
|
||||
@@ -291,6 +294,9 @@ packages:
|
||||
'@tauri-apps/api@2.0.0-rc.5':
|
||||
resolution: {integrity: sha512-JWs69pE9NsQdcqTpEVBepAZ08+jgQWuthIiTiaWRq/YlCLgnqq0KfC9sDem55uJ0YpgsytZuyj+m6b6q8oiD2g==}
|
||||
|
||||
'@tauri-apps/api@2.0.2':
|
||||
resolution: {integrity: sha512-3wSwmG+1kr6WrgAFKK5ijkNFPp8TT3FLj3YHUb5EwMO+3FxX4uWlfSWkeeBy+Kc1RsKzugtYLuuya+98Flj+3w==}
|
||||
|
||||
'@tauri-apps/cli-darwin-arm64@2.0.0-rc.16':
|
||||
resolution: {integrity: sha512-lISZU4gG0c9PbY7h/j/gW7nJLxZEygNBrYEET6zN8R99Znf5rSO+CfjenaMcJUUj6yTAd8gzdakRpLqNSAWegA==}
|
||||
engines: {node: '>= 10'}
|
||||
@@ -366,6 +372,9 @@ packages:
|
||||
'@tauri-apps/plugin-fs@2.0.0-rc.2':
|
||||
resolution: {integrity: sha512-TFjCfso3tN4b5s2EBjqP8N2gYrPh93Ds3VNKj8pCXv4wbvnItyfG0aHO0haUsedBOHQryDwv9vDAdPX6/T0a+g==}
|
||||
|
||||
'@tauri-apps/plugin-http@2.0.0':
|
||||
resolution: {integrity: sha512-UfKAICL25ayluV/SjiEQujz8q/2uyAzp3u9uaHFkaIyKS5usBL8DoqSwi4eKz2mEjkbxTwldhDEXG4CEfTE0JQ==}
|
||||
|
||||
'@tauri-apps/plugin-log@2.0.0-rc.1':
|
||||
resolution: {integrity: sha512-+Tz0zo4FDtC/5j7neeIq5ievgKbUXBV2+X5HtbaR8ZZ2bcksCp8UqeHd6cyyN+FSk4qaU01LIGkuExtxk1h/FA==}
|
||||
|
||||
@@ -2136,6 +2145,8 @@ snapshots:
|
||||
|
||||
'@tauri-apps/api@2.0.0-rc.5': {}
|
||||
|
||||
'@tauri-apps/api@2.0.2': {}
|
||||
|
||||
'@tauri-apps/cli-darwin-arm64@2.0.0-rc.16':
|
||||
optional: true
|
||||
|
||||
@@ -2187,6 +2198,10 @@ snapshots:
|
||||
dependencies:
|
||||
'@tauri-apps/api': 2.0.0-rc.5
|
||||
|
||||
'@tauri-apps/plugin-http@2.0.0':
|
||||
dependencies:
|
||||
'@tauri-apps/api': 2.0.2
|
||||
|
||||
'@tauri-apps/plugin-log@2.0.0-rc.1':
|
||||
dependencies:
|
||||
'@tauri-apps/api': 2.0.0-rc.5
|
||||
|
||||
Reference in New Issue
Block a user