mirror of
https://github.com/nimbold/Firelink.git
synced 2026-08-28 03:27:12 +00:00
feat(core): modernize backend with rusqlite and tokio scheduler
This commit migrates Firelink to use native Rust implementations for state persistence (SQLite) and background scheduling (tokio::spawn + chrono). React's interval pollers are completely removed.
This commit is contained in:
Generated
+233
-4
@@ -719,14 +719,38 @@ version = "0.0.7"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "52560adf09603e58c9a7ee1fe1dcb95a16927b17c127f0ac02d6e768a0e25bc1"
|
checksum = "52560adf09603e58c9a7ee1fe1dcb95a16927b17c127f0ac02d6e768a0e25bc1"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "darling"
|
||||||
|
version = "0.20.11"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee"
|
||||||
|
dependencies = [
|
||||||
|
"darling_core 0.20.11",
|
||||||
|
"darling_macro 0.20.11",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "darling"
|
name = "darling"
|
||||||
version = "0.23.0"
|
version = "0.23.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d"
|
checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"darling_core",
|
"darling_core 0.23.0",
|
||||||
"darling_macro",
|
"darling_macro 0.23.0",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "darling_core"
|
||||||
|
version = "0.20.11"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e"
|
||||||
|
dependencies = [
|
||||||
|
"fnv",
|
||||||
|
"ident_case",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"strsim",
|
||||||
|
"syn 2.0.117",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -742,17 +766,34 @@ dependencies = [
|
|||||||
"syn 2.0.117",
|
"syn 2.0.117",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "darling_macro"
|
||||||
|
version = "0.20.11"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead"
|
||||||
|
dependencies = [
|
||||||
|
"darling_core 0.20.11",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.117",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "darling_macro"
|
name = "darling_macro"
|
||||||
version = "0.23.0"
|
version = "0.23.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d"
|
checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"darling_core",
|
"darling_core 0.23.0",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.117",
|
"syn 2.0.117",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "data-encoding"
|
||||||
|
version = "2.11.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a4ae5f15dda3c708c0ade84bfee31ccab44a3da4f88015ed22f63732abe300c8"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "dbus"
|
name = "dbus"
|
||||||
version = "0.9.11"
|
version = "0.9.11"
|
||||||
@@ -774,6 +815,37 @@ dependencies = [
|
|||||||
"serde_core",
|
"serde_core",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "derive_builder"
|
||||||
|
version = "0.20.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "507dfb09ea8b7fa618fcf76e953f4f5e192547945816d5358edffe39f6f94947"
|
||||||
|
dependencies = [
|
||||||
|
"derive_builder_macro",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "derive_builder_core"
|
||||||
|
version = "0.20.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2d5bcf7b024d6835cfb3d473887cd966994907effbe9227e8c8219824d06c4e8"
|
||||||
|
dependencies = [
|
||||||
|
"darling 0.20.11",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.117",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "derive_builder_macro"
|
||||||
|
version = "0.20.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ab63b0e2bf4d5928aff72e83a7dace85d7bba5fe12dcc3c5a572d78caffd3f3c"
|
||||||
|
dependencies = [
|
||||||
|
"derive_builder_core",
|
||||||
|
"syn 2.0.117",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "derive_more"
|
name = "derive_more"
|
||||||
version = "2.1.1"
|
version = "2.1.1"
|
||||||
@@ -1052,6 +1124,18 @@ dependencies = [
|
|||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "fallible-iterator"
|
||||||
|
version = "0.3.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "fallible-streaming-iterator"
|
||||||
|
version = "0.1.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "fastrand"
|
name = "fastrand"
|
||||||
version = "2.4.1"
|
version = "2.4.1"
|
||||||
@@ -1594,11 +1678,32 @@ dependencies = [
|
|||||||
"foldhash 0.1.5",
|
"foldhash 0.1.5",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hashbrown"
|
||||||
|
version = "0.16.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100"
|
||||||
|
dependencies = [
|
||||||
|
"foldhash 0.2.0",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hashbrown"
|
name = "hashbrown"
|
||||||
version = "0.17.1"
|
version = "0.17.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a"
|
checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a"
|
||||||
|
dependencies = [
|
||||||
|
"foldhash 0.2.0",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hashlink"
|
||||||
|
version = "0.12.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a5081f264ed7adee96ea4b4778b6bb9da0a7228b084587aa3bd3ff05da7c5a3b"
|
||||||
|
dependencies = [
|
||||||
|
"hashbrown 0.17.1",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "heck"
|
name = "heck"
|
||||||
@@ -2107,6 +2212,21 @@ dependencies = [
|
|||||||
"serde_json",
|
"serde_json",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "keepawake"
|
||||||
|
version = "0.6.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5521b450ec179362595d5cbda7c3abd5da3af3dff58456434ad3ca33c95226b7"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"derive_builder",
|
||||||
|
"objc2-core-foundation",
|
||||||
|
"objc2-io-kit",
|
||||||
|
"thiserror 2.0.18",
|
||||||
|
"windows 0.62.2",
|
||||||
|
"zbus",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "keyboard-types"
|
name = "keyboard-types"
|
||||||
version = "0.7.0"
|
version = "0.7.0"
|
||||||
@@ -2204,6 +2324,17 @@ dependencies = [
|
|||||||
"libc",
|
"libc",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "libsqlite3-sys"
|
||||||
|
version = "0.38.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f6c19a05435c21ac299d71b6a9c13db3e3f47c520517d58990a462a1397a61db"
|
||||||
|
dependencies = [
|
||||||
|
"cc",
|
||||||
|
"pkg-config",
|
||||||
|
"vcpkg",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "linux-raw-sys"
|
name = "linux-raw-sys"
|
||||||
version = "0.12.1"
|
version = "0.12.1"
|
||||||
@@ -2522,7 +2653,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536"
|
checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 2.13.0",
|
"bitflags 2.13.0",
|
||||||
|
"block2",
|
||||||
"dispatch2",
|
"dispatch2",
|
||||||
|
"libc",
|
||||||
"objc2",
|
"objc2",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -2605,7 +2738,11 @@ version = "0.3.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "33fafba39597d6dc1fb709123dfa8289d39406734be322956a69f0931c73bb15"
|
checksum = "33fafba39597d6dc1fb709123dfa8289d39406734be322956a69f0931c73bb15"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"bitflags 2.13.0",
|
||||||
|
"block2",
|
||||||
|
"dispatch2",
|
||||||
"libc",
|
"libc",
|
||||||
|
"objc2",
|
||||||
"objc2-core-foundation",
|
"objc2-core-foundation",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -3367,6 +3504,31 @@ dependencies = [
|
|||||||
"windows-sys 0.52.0",
|
"windows-sys 0.52.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rsqlite-vfs"
|
||||||
|
version = "0.1.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c51c9ae4df8a7fba42103df5c621fa3c37eccf3a3c650879e90fc48b11cc192c"
|
||||||
|
dependencies = [
|
||||||
|
"hashbrown 0.16.1",
|
||||||
|
"thiserror 2.0.18",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rusqlite"
|
||||||
|
version = "0.40.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "11438310b19e3109b6446c33d1ed5e889428cf2e278407bc7896bc4aaea43323"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags 2.13.0",
|
||||||
|
"fallible-iterator",
|
||||||
|
"fallible-streaming-iterator",
|
||||||
|
"hashlink",
|
||||||
|
"libsqlite3-sys",
|
||||||
|
"smallvec",
|
||||||
|
"sqlite-wasm-rs",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rust-ini"
|
name = "rust-ini"
|
||||||
version = "0.21.3"
|
version = "0.21.3"
|
||||||
@@ -3721,7 +3883,7 @@ version = "3.21.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "84d57bc0c8b9a17920c178daa6bb924850d54a9c97ab45194bb8c17ad66bb660"
|
checksum = "84d57bc0c8b9a17920c178daa6bb924850d54a9c97ab45194bb8c17ad66bb660"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"darling",
|
"darling 0.23.0",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.117",
|
"syn 2.0.117",
|
||||||
@@ -3758,6 +3920,17 @@ dependencies = [
|
|||||||
"stable_deref_trait",
|
"stable_deref_trait",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "sha1"
|
||||||
|
version = "0.10.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"cpufeatures",
|
||||||
|
"digest",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sha2"
|
name = "sha2"
|
||||||
version = "0.10.9"
|
version = "0.10.9"
|
||||||
@@ -3867,6 +4040,18 @@ dependencies = [
|
|||||||
"system-deps",
|
"system-deps",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "sqlite-wasm-rs"
|
||||||
|
version = "0.5.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "dc3efc0da82635d7e1ced0053bbbfa8c7ab9645d0bf36ceb4f7127bb85315d75"
|
||||||
|
dependencies = [
|
||||||
|
"cc",
|
||||||
|
"js-sys",
|
||||||
|
"rsqlite-vfs",
|
||||||
|
"wasm-bindgen",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "stable_deref_trait"
|
name = "stable_deref_trait"
|
||||||
version = "1.2.1"
|
version = "1.2.1"
|
||||||
@@ -4022,6 +4207,16 @@ dependencies = [
|
|||||||
"version-compare",
|
"version-compare",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "system_shutdown"
|
||||||
|
version = "4.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "29396e5e1b637d102ec5037bf7fbb8da78264fa299101ced9ce827756b1bac02"
|
||||||
|
dependencies = [
|
||||||
|
"windows 0.62.2",
|
||||||
|
"zbus",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tao"
|
name = "tao"
|
||||||
version = "0.35.3"
|
version = "0.35.3"
|
||||||
@@ -4136,16 +4331,21 @@ name = "tauri-app"
|
|||||||
version = "0.7.3"
|
version = "0.7.3"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"axum",
|
"axum",
|
||||||
|
"futures-util",
|
||||||
"hmac",
|
"hmac",
|
||||||
|
"keepawake",
|
||||||
"keyring",
|
"keyring",
|
||||||
|
"open",
|
||||||
"regex 1.12.4",
|
"regex 1.12.4",
|
||||||
"reqwest 0.12.28",
|
"reqwest 0.12.28",
|
||||||
|
"rusqlite",
|
||||||
"semver",
|
"semver",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"sha2",
|
"sha2",
|
||||||
"sysinfo",
|
"sysinfo",
|
||||||
"sysproxy",
|
"sysproxy",
|
||||||
|
"system_shutdown",
|
||||||
"tauri",
|
"tauri",
|
||||||
"tauri-build",
|
"tauri-build",
|
||||||
"tauri-plugin-deep-link",
|
"tauri-plugin-deep-link",
|
||||||
@@ -4156,6 +4356,7 @@ dependencies = [
|
|||||||
"tempfile",
|
"tempfile",
|
||||||
"thiserror 2.0.18",
|
"thiserror 2.0.18",
|
||||||
"tokio",
|
"tokio",
|
||||||
|
"tokio-tungstenite",
|
||||||
"tower-http",
|
"tower-http",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -4653,6 +4854,18 @@ dependencies = [
|
|||||||
"tokio",
|
"tokio",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tokio-tungstenite"
|
||||||
|
version = "0.29.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8f72a05e828585856dacd553fba484c242c46e391fb0e58917c942ee9202915c"
|
||||||
|
dependencies = [
|
||||||
|
"futures-util",
|
||||||
|
"log 0.4.32",
|
||||||
|
"tokio",
|
||||||
|
"tungstenite",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tokio-util"
|
name = "tokio-util"
|
||||||
version = "0.7.18"
|
version = "0.7.18"
|
||||||
@@ -4892,6 +5105,22 @@ version = "0.2.5"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
|
checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tungstenite"
|
||||||
|
version = "0.29.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6c01152af293afb9c7c2a57e4b559c5620b421f6d133261c60dd2d0cdb38e6b8"
|
||||||
|
dependencies = [
|
||||||
|
"bytes",
|
||||||
|
"data-encoding",
|
||||||
|
"http",
|
||||||
|
"httparse",
|
||||||
|
"log 0.4.32",
|
||||||
|
"rand",
|
||||||
|
"sha1",
|
||||||
|
"thiserror 2.0.18",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "typeid"
|
name = "typeid"
|
||||||
version = "1.0.3"
|
version = "1.0.3"
|
||||||
|
|||||||
@@ -39,3 +39,10 @@ axum = "0.8.9"
|
|||||||
tower-http = { version = "0.6.11", features = ["cors"] }
|
tower-http = { version = "0.6.11", features = ["cors"] }
|
||||||
sysproxy = "0.3.0"
|
sysproxy = "0.3.0"
|
||||||
semver = "1.0.28"
|
semver = "1.0.28"
|
||||||
|
keepawake = "0.6.0"
|
||||||
|
open = "5.3.5"
|
||||||
|
system_shutdown = "4.1.0"
|
||||||
|
tokio-tungstenite = "0.29.0"
|
||||||
|
futures-util = { version = "0.3.32", features = ["sink"] }
|
||||||
|
rusqlite = { version = "0.40.1", features = ["bundled"] }
|
||||||
|
chrono = "0.4.38"
|
||||||
|
|||||||
@@ -0,0 +1,138 @@
|
|||||||
|
use rusqlite::{Connection, Result, params};
|
||||||
|
use std::sync::Mutex;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use tauri::Manager;
|
||||||
|
|
||||||
|
pub struct DbState {
|
||||||
|
pub conn: Mutex<Connection>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn init_db(app_handle: &tauri::AppHandle) -> Result<Connection> {
|
||||||
|
let app_dir = app_handle.path().app_data_dir().unwrap_or_else(|_| PathBuf::from("."));
|
||||||
|
if !app_dir.exists() {
|
||||||
|
let _ = std::fs::create_dir_all(&app_dir);
|
||||||
|
}
|
||||||
|
let db_path = app_dir.join("firelink.sqlite");
|
||||||
|
let conn = Connection::open(db_path)?;
|
||||||
|
|
||||||
|
conn.execute(
|
||||||
|
"CREATE TABLE IF NOT EXISTS downloads (
|
||||||
|
id TEXT PRIMARY KEY,
|
||||||
|
status TEXT NOT NULL,
|
||||||
|
queue_id TEXT NOT NULL,
|
||||||
|
data TEXT NOT NULL
|
||||||
|
)",
|
||||||
|
[],
|
||||||
|
)?;
|
||||||
|
|
||||||
|
conn.execute(
|
||||||
|
"CREATE TABLE IF NOT EXISTS settings (
|
||||||
|
id INTEGER PRIMARY KEY CHECK (id = 1),
|
||||||
|
data TEXT NOT NULL
|
||||||
|
)",
|
||||||
|
[],
|
||||||
|
)?;
|
||||||
|
|
||||||
|
conn.execute(
|
||||||
|
"CREATE TABLE IF NOT EXISTS queues (
|
||||||
|
id TEXT PRIMARY KEY,
|
||||||
|
data TEXT NOT NULL
|
||||||
|
)",
|
||||||
|
[],
|
||||||
|
)?;
|
||||||
|
|
||||||
|
Ok(conn)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Downloads CRUD
|
||||||
|
pub fn insert_download(conn: &Connection, id: &str, status: &str, queue_id: &str, data: &str) -> Result<()> {
|
||||||
|
conn.execute(
|
||||||
|
"INSERT OR REPLACE INTO downloads (id, status, queue_id, data) VALUES (?1, ?2, ?3, ?4)",
|
||||||
|
params![id, status, queue_id, data],
|
||||||
|
)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn update_download_status(conn: &Connection, id: &str, status: &str) -> Result<()> {
|
||||||
|
conn.execute(
|
||||||
|
"UPDATE downloads SET status = ?1 WHERE id = ?2",
|
||||||
|
params![status, id],
|
||||||
|
)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn update_download_data(conn: &Connection, id: &str, data: &str) -> Result<()> {
|
||||||
|
conn.execute(
|
||||||
|
"UPDATE downloads SET data = ?1 WHERE id = ?2",
|
||||||
|
params![data, id],
|
||||||
|
)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn delete_download(conn: &Connection, id: &str) -> Result<()> {
|
||||||
|
conn.execute("DELETE FROM downloads WHERE id = ?1", params![id])?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_all_downloads(conn: &Connection) -> Result<Vec<String>> {
|
||||||
|
let mut stmt = conn.prepare("SELECT data FROM downloads")?;
|
||||||
|
let iter = stmt.query_map([], |row| row.get(0))?;
|
||||||
|
let mut res = Vec::new();
|
||||||
|
for data in iter {
|
||||||
|
res.push(data?);
|
||||||
|
}
|
||||||
|
Ok(res)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_downloads_by_status(conn: &Connection, status: &str) -> Result<Vec<String>> {
|
||||||
|
let mut stmt = conn.prepare("SELECT data FROM downloads WHERE status = ?1")?;
|
||||||
|
let iter = stmt.query_map(params![status], |row| row.get(0))?;
|
||||||
|
let mut res = Vec::new();
|
||||||
|
for data in iter {
|
||||||
|
res.push(data?);
|
||||||
|
}
|
||||||
|
Ok(res)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Settings CRUD
|
||||||
|
pub fn get_settings(conn: &Connection) -> Result<Option<String>> {
|
||||||
|
let mut stmt = conn.prepare("SELECT data FROM settings WHERE id = 1")?;
|
||||||
|
let mut iter = stmt.query_map([], |row| row.get(0))?;
|
||||||
|
if let Some(row) = iter.next() {
|
||||||
|
Ok(Some(row?))
|
||||||
|
} else {
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn save_settings(conn: &Connection, data: &str) -> Result<()> {
|
||||||
|
conn.execute(
|
||||||
|
"INSERT OR REPLACE INTO settings (id, data) VALUES (1, ?1)",
|
||||||
|
params![data],
|
||||||
|
)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Queues CRUD
|
||||||
|
pub fn insert_queue(conn: &Connection, id: &str, data: &str) -> Result<()> {
|
||||||
|
conn.execute(
|
||||||
|
"INSERT OR REPLACE INTO queues (id, data) VALUES (?1, ?2)",
|
||||||
|
params![id, data],
|
||||||
|
)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn delete_queue(conn: &Connection, id: &str) -> Result<()> {
|
||||||
|
conn.execute("DELETE FROM queues WHERE id = ?1", params![id])?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_all_queues(conn: &Connection) -> Result<Vec<String>> {
|
||||||
|
let mut stmt = conn.prepare("SELECT data FROM queues")?;
|
||||||
|
let iter = stmt.query_map([], |row| row.get(0))?;
|
||||||
|
let mut res = Vec::new();
|
||||||
|
for data in iter {
|
||||||
|
res.push(data?);
|
||||||
|
}
|
||||||
|
Ok(res)
|
||||||
|
}
|
||||||
+199
-169
@@ -296,80 +296,14 @@ async fn test_deno(app_handle: tauri::AppHandle) -> Result<String, String> {
|
|||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
async fn open_file(path: String) -> Result<(), String> {
|
async fn open_file(path: String) -> Result<(), String> {
|
||||||
println!("open_file called for path: {}", path);
|
println!("open_file called for path: {}", path);
|
||||||
#[cfg(target_os = "macos")]
|
open::that(&path).map_err(|e| format!("Failed to open file: {}", e))
|
||||||
{
|
|
||||||
let status = std::process::Command::new("open")
|
|
||||||
.arg(&path)
|
|
||||||
.status();
|
|
||||||
match status {
|
|
||||||
Ok(s) if s.success() => Ok(()),
|
|
||||||
_ => Err(format!("Failed to open file: {:?}", status)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#[cfg(target_os = "windows")]
|
|
||||||
{
|
|
||||||
let status = std::process::Command::new("cmd")
|
|
||||||
.arg("/c")
|
|
||||||
.arg("start")
|
|
||||||
.arg("")
|
|
||||||
.arg(&path)
|
|
||||||
.status();
|
|
||||||
match status {
|
|
||||||
Ok(s) if s.success() => Ok(()),
|
|
||||||
_ => Err(format!("Failed to open file: {:?}", status)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#[cfg(target_os = "linux")]
|
|
||||||
{
|
|
||||||
let status = std::process::Command::new("xdg-open")
|
|
||||||
.arg(&path)
|
|
||||||
.status();
|
|
||||||
match status {
|
|
||||||
Ok(s) if s.success() => Ok(()),
|
|
||||||
_ => Err(format!("Failed to open file: {:?}", status)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
async fn show_in_folder(path: String) -> Result<(), String> {
|
async fn show_in_folder(app: tauri::AppHandle, path: String) -> Result<(), String> {
|
||||||
println!("show_in_folder called for path: {}", path);
|
println!("show_in_folder called for path: {}", path);
|
||||||
#[cfg(target_os = "macos")]
|
use tauri_plugin_opener::OpenerExt;
|
||||||
{
|
app.opener().reveal_item_in_dir(&path).map_err(|e| format!("Failed to reveal in folder: {}", e))
|
||||||
let status = std::process::Command::new("open")
|
|
||||||
.arg("-R")
|
|
||||||
.arg(&path)
|
|
||||||
.status();
|
|
||||||
match status {
|
|
||||||
Ok(s) if s.success() => Ok(()),
|
|
||||||
_ => Err(format!("Failed to show in Finder: {:?}", status)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#[cfg(target_os = "windows")]
|
|
||||||
{
|
|
||||||
let status = std::process::Command::new("explorer")
|
|
||||||
.arg("/select,")
|
|
||||||
.arg(path.replace("/", "\\"))
|
|
||||||
.status();
|
|
||||||
match status {
|
|
||||||
Ok(s) if s.success() => Ok(()),
|
|
||||||
_ => Err(format!("Failed to show in Explorer: {:?}", status)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#[cfg(target_os = "linux")]
|
|
||||||
{
|
|
||||||
if let Some(parent) = std::path::Path::new(&path).parent() {
|
|
||||||
let status = std::process::Command::new("xdg-open")
|
|
||||||
.arg(parent)
|
|
||||||
.status();
|
|
||||||
match status {
|
|
||||||
Ok(s) if s.success() => Ok(()),
|
|
||||||
_ => Err(format!("Failed to open folder: {:?}", status)),
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Err("No parent folder found".to_string())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
@@ -393,6 +327,7 @@ pub struct AppState {
|
|||||||
pub aria2_port: u16,
|
pub aria2_port: u16,
|
||||||
pub aria2_secret: String,
|
pub aria2_secret: String,
|
||||||
pub media_semaphore: Arc<tokio::sync::Semaphore>,
|
pub media_semaphore: Arc<tokio::sync::Semaphore>,
|
||||||
|
pub sleep_preventer: Arc<Mutex<Option<keepawake::KeepAwake>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Serialize)]
|
#[derive(Clone, Serialize)]
|
||||||
@@ -935,41 +870,25 @@ fn update_dock_badge(_app_handle: tauri::AppHandle, count: i32) {
|
|||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
fn set_prevent_sleep(state: tauri::State<'_, AppState>, prevent: bool) {
|
fn set_prevent_sleep(state: tauri::State<'_, AppState>, prevent: bool) {
|
||||||
#[cfg(target_os = "macos")]
|
let mut current_preventer = state.sleep_preventer.lock().unwrap();
|
||||||
{
|
if prevent {
|
||||||
let mut tasks = state.tasks.lock().unwrap();
|
if current_preventer.is_none() {
|
||||||
if prevent {
|
if let Ok(keepawake) = keepawake::Builder::default().display(true).reason("Downloading files").create() {
|
||||||
if !tasks.contains_key("sleep_prevent") {
|
*current_preventer = Some(keepawake);
|
||||||
if let Ok(child) = std::process::Command::new("caffeinate")
|
|
||||||
.arg("-i")
|
|
||||||
.spawn()
|
|
||||||
{
|
|
||||||
tasks.insert("sleep_prevent".to_string(), TaskHandle::Pid(child.id()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if let Some(TaskHandle::Pid(pid)) = tasks.remove("sleep_prevent") {
|
|
||||||
let _ = std::process::Command::new("kill")
|
|
||||||
.arg("-15")
|
|
||||||
.arg(pid.to_string())
|
|
||||||
.status();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
*current_preventer = None;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
fn perform_system_action(action: String) -> Result<(), String> {
|
fn perform_system_action(action: String) -> Result<(), String> {
|
||||||
let status = match action.as_str() {
|
match action.as_str() {
|
||||||
"shutdown" => std::process::Command::new("osascript").arg("-e").arg("tell app \"System Events\" to shut down").status(),
|
"shutdown" => system_shutdown::shutdown().map_err(|e| e.to_string()),
|
||||||
"restart" => std::process::Command::new("osascript").arg("-e").arg("tell app \"System Events\" to restart").status(),
|
"restart" => system_shutdown::reboot().map_err(|e| e.to_string()),
|
||||||
"sleep" => std::process::Command::new("osascript").arg("-e").arg("tell app \"System Events\" to sleep").status(),
|
"sleep" => system_shutdown::sleep().map_err(|e| e.to_string()),
|
||||||
_ => return Err("Invalid action".to_string())
|
_ => Err("Invalid action".to_string())
|
||||||
};
|
|
||||||
|
|
||||||
match status {
|
|
||||||
Ok(_) => Ok(()),
|
|
||||||
Err(e) => Err(e.to_string())
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1258,8 +1177,14 @@ pub fn run() {
|
|||||||
aria2_port,
|
aria2_port,
|
||||||
aria2_secret: aria2_secret.clone(),
|
aria2_secret: aria2_secret.clone(),
|
||||||
media_semaphore: Arc::new(tokio::sync::Semaphore::new(3)),
|
media_semaphore: Arc::new(tokio::sync::Semaphore::new(3)),
|
||||||
|
sleep_preventer: Arc::new(Mutex::new(None)),
|
||||||
})
|
})
|
||||||
.setup(move |app| {
|
.setup(move |app| {
|
||||||
|
let db_conn = crate::db::init_db(app.handle()).expect("Failed to init db");
|
||||||
|
app.manage(crate::db::DbState { conn: std::sync::Mutex::new(db_conn) });
|
||||||
|
|
||||||
|
crate::scheduler::spawn_scheduler(app.handle().clone());
|
||||||
|
|
||||||
let resource_dir = app.path().resource_dir().unwrap();
|
let resource_dir = app.path().resource_dir().unwrap();
|
||||||
let aria2c_path = resource_dir.join("binaries").join("aria2c");
|
let aria2c_path = resource_dir.join("binaries").join("aria2c");
|
||||||
|
|
||||||
@@ -1284,86 +1209,140 @@ pub fn run() {
|
|||||||
let aria2_port_clone = aria2_port;
|
let aria2_port_clone = aria2_port;
|
||||||
let aria2_secret_clone = aria2_secret.clone();
|
let aria2_secret_clone = aria2_secret.clone();
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
|
tokio::time::sleep(std::time::Duration::from_millis(500)).await;
|
||||||
|
let ws_url = format!("ws://127.0.0.1:{}/jsonrpc", aria2_port_clone);
|
||||||
|
|
||||||
|
use futures_util::{StreamExt, SinkExt};
|
||||||
|
use tokio_tungstenite::connect_async;
|
||||||
|
use tokio_tungstenite::tungstenite::Message;
|
||||||
|
|
||||||
|
let (mut ws_stream, _) = match connect_async(&ws_url).await {
|
||||||
|
Ok(stream) => stream,
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("Failed to connect to aria2 WebSocket: {}", e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut interval = tokio::time::interval(std::time::Duration::from_millis(1000));
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
tokio::time::sleep(std::time::Duration::from_millis(1000)).await;
|
tokio::select! {
|
||||||
|
_ = interval.tick() => {
|
||||||
let state = app_handle_clone.state::<AppState>();
|
let req = serde_json::json!({
|
||||||
let tasks = state.tasks.clone();
|
"jsonrpc": "2.0",
|
||||||
|
"id": "progress",
|
||||||
let mut gid_to_id = HashMap::new();
|
"method": "aria2.tellActive",
|
||||||
{
|
"params": [
|
||||||
let map = tasks.lock().unwrap();
|
format!("token:{}", aria2_secret_clone),
|
||||||
for (id, handle) in map.iter() {
|
["gid", "status", "completedLength", "totalLength", "downloadSpeed"]
|
||||||
if let TaskHandle::Aria2(gid) = handle {
|
]
|
||||||
gid_to_id.insert(gid.clone(), id.clone());
|
});
|
||||||
|
if let Ok(msg) = serde_json::to_string(&req) {
|
||||||
|
let _ = ws_stream.send(Message::Text(msg.into())).await;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
msg = ws_stream.next() => {
|
||||||
|
match msg {
|
||||||
if let Ok(json) = crate::rpc_call(aria2_port_clone, &aria2_secret_clone, "aria2.tellActive", serde_json::json!([["gid", "status", "completedLength", "totalLength", "downloadSpeed"]])).await {
|
Some(Ok(Message::Text(text))) => {
|
||||||
if let Some(arr) = json.get("result").and_then(|r| r.as_array()) {
|
if let Ok(json) = serde_json::from_str::<serde_json::Value>(text.as_str()) {
|
||||||
for item in arr {
|
let state = app_handle_clone.state::<AppState>();
|
||||||
if let Some(gid) = item.get("gid").and_then(|v| v.as_str()) {
|
let tasks = state.tasks.clone();
|
||||||
if let Some(id) = gid_to_id.get(gid) {
|
|
||||||
let completed = item.get("completedLength").and_then(|v| v.as_str()).and_then(|s| s.parse::<f64>().ok()).unwrap_or(0.0);
|
|
||||||
let total = item.get("totalLength").and_then(|v| v.as_str()).and_then(|s| s.parse::<f64>().ok()).unwrap_or(1.0);
|
|
||||||
let speed_bytes = item.get("downloadSpeed").and_then(|v| v.as_str()).and_then(|s| s.parse::<f64>().ok()).unwrap_or(0.0);
|
|
||||||
|
|
||||||
let fraction = if total > 0.0 { completed / total } else { 0.0 };
|
// Process progress
|
||||||
let speed = if speed_bytes > 1024.0 * 1024.0 {
|
if json.get("id").and_then(|i| i.as_str()) == Some("progress") {
|
||||||
format!("{:.1} MB/s", speed_bytes / (1024.0 * 1024.0))
|
let mut gid_to_id = HashMap::new();
|
||||||
} else if speed_bytes > 1024.0 {
|
{
|
||||||
format!("{:.1} KB/s", speed_bytes / 1024.0)
|
let map = tasks.lock().unwrap();
|
||||||
} else {
|
for (id, handle) in map.iter() {
|
||||||
format!("{:.0} B/s", speed_bytes)
|
if let TaskHandle::Aria2(gid) = handle {
|
||||||
};
|
gid_to_id.insert(gid.clone(), id.clone());
|
||||||
|
}
|
||||||
let eta = if speed_bytes > 0.0 && total > completed {
|
}
|
||||||
let seconds = (total - completed) / speed_bytes;
|
|
||||||
if seconds > 3600.0 {
|
|
||||||
format!("{:.0}h {:.0}m", seconds / 3600.0, (seconds % 3600.0) / 60.0)
|
|
||||||
} else if seconds > 60.0 {
|
|
||||||
format!("{:.0}m {:.0}s", seconds / 60.0, seconds % 60.0)
|
|
||||||
} else {
|
|
||||||
format!("{:.0}s", seconds)
|
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
"-".to_string()
|
if let Some(arr) = json.get("result").and_then(|r| r.as_array()) {
|
||||||
};
|
for item in arr {
|
||||||
|
if let Some(gid) = item.get("gid").and_then(|v| v.as_str()) {
|
||||||
let _ = app_handle_clone.emit("download-progress", DownloadProgressEvent {
|
if let Some(id) = gid_to_id.get(gid) {
|
||||||
id: id.clone(),
|
let completed = item.get("completedLength").and_then(|v| v.as_str()).and_then(|s| s.parse::<f64>().ok()).unwrap_or(0.0);
|
||||||
fraction,
|
let total = item.get("totalLength").and_then(|v| v.as_str()).and_then(|s| s.parse::<f64>().ok()).unwrap_or(1.0);
|
||||||
speed,
|
let speed_bytes = item.get("downloadSpeed").and_then(|v| v.as_str()).and_then(|s| s.parse::<f64>().ok()).unwrap_or(0.0);
|
||||||
eta,
|
|
||||||
});
|
let fraction = if total > 0.0 { completed / total } else { 0.0 };
|
||||||
}
|
let speed = if speed_bytes > 1024.0 * 1024.0 {
|
||||||
}
|
format!("{:.1} MB/s", speed_bytes / (1024.0 * 1024.0))
|
||||||
}
|
} else if speed_bytes > 1024.0 {
|
||||||
}
|
format!("{:.1} KB/s", speed_bytes / 1024.0)
|
||||||
}
|
} else {
|
||||||
|
format!("{:.0} B/s", speed_bytes)
|
||||||
|
};
|
||||||
|
|
||||||
if let Ok(json) = crate::rpc_call(aria2_port_clone, &aria2_secret_clone, "aria2.tellStopped", serde_json::json!([0, 100, ["gid", "status", "completedLength", "totalLength"]])).await {
|
let eta = if speed_bytes > 0.0 && total > completed {
|
||||||
if let Some(arr) = json.get("result").and_then(|r| r.as_array()) {
|
let seconds = (total - completed) / speed_bytes;
|
||||||
for item in arr {
|
if seconds > 3600.0 {
|
||||||
if let Some(gid) = item.get("gid").and_then(|v| v.as_str()) {
|
format!("{:.0}h {:.0}m", seconds / 3600.0, (seconds % 3600.0) / 60.0)
|
||||||
if let Some(id) = gid_to_id.get(gid) {
|
} else if seconds > 60.0 {
|
||||||
let status = item.get("status").and_then(|v| v.as_str()).unwrap_or("");
|
format!("{:.0}m {:.0}s", seconds / 60.0, seconds % 60.0)
|
||||||
if status == "complete" {
|
} else {
|
||||||
let _ = app_handle_clone.emit("download-complete", id.clone());
|
format!("{:.0}s", seconds)
|
||||||
tasks.lock().unwrap().remove(id);
|
}
|
||||||
} else if status == "error" {
|
} else {
|
||||||
let comp = item.get("completedLength").and_then(|v| v.as_str()).and_then(|s| s.parse::<f64>().ok()).unwrap_or(0.0);
|
"-".to_string()
|
||||||
let tot = item.get("totalLength").and_then(|v| v.as_str()).and_then(|s| s.parse::<f64>().ok()).unwrap_or(1.0);
|
};
|
||||||
if comp > 0.0 && comp >= tot {
|
|
||||||
let _ = app_handle_clone.emit("download-complete", id.clone());
|
let _ = app_handle_clone.emit("download-progress", DownloadProgressEvent {
|
||||||
} else {
|
id: id.clone(),
|
||||||
let _ = app_handle_clone.emit("download-failed", id.clone());
|
fraction,
|
||||||
|
speed,
|
||||||
|
eta,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process Events
|
||||||
|
if let Some(method) = json.get("method").and_then(|m| m.as_str()) {
|
||||||
|
if method == "aria2.onDownloadComplete" || method == "aria2.onDownloadError" {
|
||||||
|
if let Some(params) = json.get("params").and_then(|p| p.as_array()) {
|
||||||
|
if let Some(event_info) = params.get(0) {
|
||||||
|
if let Some(gid) = event_info.get("gid").and_then(|g| g.as_str()) {
|
||||||
|
let mut target_id = None;
|
||||||
|
{
|
||||||
|
let map = tasks.lock().unwrap();
|
||||||
|
for (id, handle) in map.iter() {
|
||||||
|
if let TaskHandle::Aria2(task_gid) = handle {
|
||||||
|
if task_gid == gid {
|
||||||
|
target_id = Some(id.clone());
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(id) = target_id {
|
||||||
|
if method == "aria2.onDownloadComplete" {
|
||||||
|
let _ = app_handle_clone.emit("download-complete", id.clone());
|
||||||
|
} else {
|
||||||
|
let _ = app_handle_clone.emit("download-failed", id.clone());
|
||||||
|
}
|
||||||
|
tasks.lock().unwrap().remove(&id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
tasks.lock().unwrap().remove(id);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Some(Err(e)) => {
|
||||||
|
eprintln!("WebSocket error: {}", e);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
None => break, // Stream closed
|
||||||
|
_ => {} // Ignore binary/ping/pong for now
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1396,9 +1375,60 @@ pub fn run() {
|
|||||||
set_keychain_password, get_keychain_password, delete_keychain_password,
|
set_keychain_password, get_keychain_password, delete_keychain_password,
|
||||||
check_file_exists, delete_file, toggle_tray_icon, set_extension_pairing_token,
|
check_file_exists, delete_file, toggle_tray_icon, set_extension_pairing_token,
|
||||||
set_extension_frontend_ready, set_concurrent_limit, set_global_speed_limit, remove_download,
|
set_extension_frontend_ready, set_concurrent_limit, set_global_speed_limit, remove_download,
|
||||||
parity::get_system_proxy, parity::get_file_category, parity::check_for_updates, parity::is_supported_media
|
parity::get_system_proxy, parity::get_file_category, parity::check_for_updates, parity::is_supported_media,
|
||||||
|
db_save_settings, db_load_settings, db_get_all_downloads, db_save_download, db_delete_download, db_get_all_queues, db_save_queue, db_delete_queue
|
||||||
])
|
])
|
||||||
.run(tauri::generate_context!())
|
.run(tauri::generate_context!())
|
||||||
.expect("error while running tauri application");
|
.expect("error while running tauri application");
|
||||||
}
|
}
|
||||||
mod extension_server;
|
mod extension_server;
|
||||||
|
mod db;
|
||||||
|
mod scheduler;
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
fn db_save_settings(state: tauri::State<crate::db::DbState>, data: String) -> Result<(), String> {
|
||||||
|
let conn = state.conn.lock().unwrap();
|
||||||
|
crate::db::save_settings(&conn, &data).map_err(|e| e.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
fn db_load_settings(state: tauri::State<crate::db::DbState>) -> Result<Option<String>, String> {
|
||||||
|
let conn = state.conn.lock().unwrap();
|
||||||
|
crate::db::get_settings(&conn).map_err(|e| e.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
fn db_get_all_downloads(state: tauri::State<crate::db::DbState>) -> Result<Vec<String>, String> {
|
||||||
|
let conn = state.conn.lock().unwrap();
|
||||||
|
crate::db::get_all_downloads(&conn).map_err(|e| e.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
fn db_save_download(state: tauri::State<crate::db::DbState>, id: String, status: String, queue_id: String, data: String) -> Result<(), String> {
|
||||||
|
let conn = state.conn.lock().unwrap();
|
||||||
|
crate::db::insert_download(&conn, &id, &status, &queue_id, &data).map_err(|e| e.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
fn db_delete_download(state: tauri::State<crate::db::DbState>, id: String) -> Result<(), String> {
|
||||||
|
let conn = state.conn.lock().unwrap();
|
||||||
|
crate::db::delete_download(&conn, &id).map_err(|e| e.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
fn db_get_all_queues(state: tauri::State<crate::db::DbState>) -> Result<Vec<String>, String> {
|
||||||
|
let conn = state.conn.lock().unwrap();
|
||||||
|
crate::db::get_all_queues(&conn).map_err(|e| e.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
fn db_save_queue(state: tauri::State<crate::db::DbState>, id: String, data: String) -> Result<(), String> {
|
||||||
|
let conn = state.conn.lock().unwrap();
|
||||||
|
crate::db::insert_queue(&conn, &id, &data).map_err(|e| e.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
fn db_delete_queue(state: tauri::State<crate::db::DbState>, id: String) -> Result<(), String> {
|
||||||
|
let conn = state.conn.lock().unwrap();
|
||||||
|
crate::db::delete_queue(&conn, &id).map_err(|e| e.to_string())
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,86 @@
|
|||||||
|
use tauri::{Manager, Emitter};
|
||||||
|
use chrono::{Local, Datelike};
|
||||||
|
use std::time::Duration;
|
||||||
|
use serde::Deserialize;
|
||||||
|
|
||||||
|
#[derive(Deserialize, Debug)]
|
||||||
|
struct SchedulerSettings {
|
||||||
|
enabled: bool,
|
||||||
|
#[serde(rename = "startTime")]
|
||||||
|
start_time: String,
|
||||||
|
#[serde(rename = "stopTimeEnabled")]
|
||||||
|
stop_time_enabled: bool,
|
||||||
|
#[serde(rename = "stopTime")]
|
||||||
|
stop_time: String,
|
||||||
|
everyday: bool,
|
||||||
|
#[serde(rename = "selectedDays")]
|
||||||
|
selected_days: Vec<u32>,
|
||||||
|
#[serde(rename = "postQueueAction")]
|
||||||
|
post_queue_action: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn spawn_scheduler(app_handle: tauri::AppHandle) {
|
||||||
|
tokio::spawn(async move {
|
||||||
|
let mut interval = tokio::time::interval(Duration::from_secs(10));
|
||||||
|
loop {
|
||||||
|
interval.tick().await;
|
||||||
|
|
||||||
|
let settings_opt = {
|
||||||
|
let state = app_handle.state::<crate::db::DbState>();
|
||||||
|
let conn = state.conn.lock().unwrap();
|
||||||
|
crate::db::get_settings(&conn).unwrap_or(None)
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(settings_str) = settings_opt {
|
||||||
|
if let Ok(mut settings) = serde_json::from_str::<serde_json::Value>(&settings_str) {
|
||||||
|
if let Ok(scheduler) = serde_json::from_value::<SchedulerSettings>(settings.get("scheduler").unwrap_or(&serde_json::json!({})).clone()) {
|
||||||
|
if !scheduler.enabled {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let now = Local::now();
|
||||||
|
let current_time = now.format("%H:%M").to_string();
|
||||||
|
let current_day = now.weekday().num_days_from_sunday();
|
||||||
|
|
||||||
|
let allowed_today = scheduler.everyday || scheduler.selected_days.contains(¤t_day);
|
||||||
|
if !allowed_today {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let date_key = now.format("%Y-%m-%d").to_string();
|
||||||
|
let trigger_key = format!("{}-{}", date_key, current_time);
|
||||||
|
|
||||||
|
let last_start_key = settings.get("schedulerLastStartKey").and_then(|v| v.as_str()).unwrap_or("");
|
||||||
|
let last_stop_key = settings.get("schedulerLastStopKey").and_then(|v| v.as_str()).unwrap_or("");
|
||||||
|
|
||||||
|
if scheduler.start_time == current_time && last_start_key != trigger_key {
|
||||||
|
settings["schedulerLastStartKey"] = serde_json::json!(trigger_key.clone());
|
||||||
|
settings["schedulerRunning"] = serde_json::json!(true);
|
||||||
|
|
||||||
|
let _ = app_handle.emit("schedule-trigger", "start");
|
||||||
|
|
||||||
|
if let Ok(updated) = serde_json::to_string(&settings) {
|
||||||
|
let state = app_handle.state::<crate::db::DbState>();
|
||||||
|
let conn = state.conn.lock().unwrap();
|
||||||
|
let _ = crate::db::save_settings(&conn, &updated);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if scheduler.stop_time_enabled && scheduler.stop_time == current_time && last_stop_key != trigger_key {
|
||||||
|
settings["schedulerLastStopKey"] = serde_json::json!(trigger_key.clone());
|
||||||
|
settings["schedulerRunning"] = serde_json::json!(false);
|
||||||
|
|
||||||
|
let _ = app_handle.emit("schedule-trigger", "stop");
|
||||||
|
|
||||||
|
if let Ok(updated) = serde_json::to_string(&settings) {
|
||||||
|
let state = app_handle.state::<crate::db::DbState>();
|
||||||
|
let conn = state.conn.lock().unwrap();
|
||||||
|
let _ = crate::db::save_settings(&conn, &updated);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
+15
-30
@@ -55,6 +55,10 @@ function App() {
|
|||||||
const previousSpeedLimit = useRef(globalSpeedLimit);
|
const previousSpeedLimit = useRef(globalSpeedLimit);
|
||||||
const maxConcurrentDownloads = useSettingsStore(state => state.maxConcurrentDownloads);
|
const maxConcurrentDownloads = useSettingsStore(state => state.maxConcurrentDownloads);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
useDownloadStore.getState().initDB();
|
||||||
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
window.document.documentElement.setAttribute('data-font-size', appFontSize);
|
window.document.documentElement.setAttribute('data-font-size', appFontSize);
|
||||||
}, [appFontSize]);
|
}, [appFontSize]);
|
||||||
@@ -114,39 +118,20 @@ function App() {
|
|||||||
}, [globalSpeedLimit]);
|
}, [globalSpeedLimit]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const checkSchedule = async () => {
|
const unlisten = listen('schedule-trigger', async (event) => {
|
||||||
const state = useSettingsStore.getState();
|
const state = useSettingsStore.getState();
|
||||||
const scheduler = state.scheduler;
|
if (event.payload === 'start') {
|
||||||
if (!scheduler.enabled) return;
|
const started = await useDownloadStore.getState().startQueue(MAIN_QUEUE_ID);
|
||||||
|
state.setSchedulerRunning(started > 0);
|
||||||
const now = new Date();
|
} else if (event.payload === 'stop') {
|
||||||
const currentTime = `${String(now.getHours()).padStart(2, '0')}:${String(now.getMinutes()).padStart(2, '0')}`;
|
await useDownloadStore.getState().pauseQueue(MAIN_QUEUE_ID);
|
||||||
const allowedToday = scheduler.everyday || scheduler.selectedDays.includes(now.getDay());
|
state.setSchedulerRunning(false);
|
||||||
if (!allowedToday) return;
|
|
||||||
|
|
||||||
const dateKey = localDateKey(now);
|
|
||||||
if (scheduler.startTime === currentTime) {
|
|
||||||
const triggerKey = `${dateKey}-${currentTime}`;
|
|
||||||
if (state.schedulerLastStartKey !== triggerKey) {
|
|
||||||
state.setSchedulerLastStartKey(triggerKey);
|
|
||||||
const started = await useDownloadStore.getState().startQueue(MAIN_QUEUE_ID);
|
|
||||||
state.setSchedulerRunning(started > 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (scheduler.stopTimeEnabled && scheduler.stopTime === currentTime) {
|
|
||||||
const triggerKey = `${dateKey}-${currentTime}`;
|
|
||||||
if (state.schedulerLastStopKey !== triggerKey) {
|
|
||||||
state.setSchedulerLastStopKey(triggerKey);
|
|
||||||
await useDownloadStore.getState().pauseQueue(MAIN_QUEUE_ID);
|
|
||||||
state.setSchedulerRunning(false);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
unlisten.then(f => f()).catch(console.error);
|
||||||
};
|
};
|
||||||
|
|
||||||
void checkSchedule();
|
|
||||||
const interval = window.setInterval(() => void checkSchedule(), 10_000);
|
|
||||||
return () => window.clearInterval(interval);
|
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@@ -116,6 +116,7 @@ interface DownloadState {
|
|||||||
addQueue: (name: string) => void;
|
addQueue: (name: string) => void;
|
||||||
renameQueue: (id: string, name: string) => void;
|
renameQueue: (id: string, name: string) => void;
|
||||||
removeQueue: (id: string) => void;
|
removeQueue: (id: string) => void;
|
||||||
|
initDB: () => Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useDownloadStore = create<DownloadState>((set, get) => ({
|
export const useDownloadStore = create<DownloadState>((set, get) => ({
|
||||||
@@ -174,14 +175,23 @@ export const useDownloadStore = create<DownloadState>((set, get) => ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
set(state => ({ downloads: [...state.downloads, ...downloads] }));
|
set(state => ({ downloads: [...state.downloads, ...downloads] }));
|
||||||
|
downloads.forEach(item => {
|
||||||
|
const toSave = { ...item };
|
||||||
|
delete toSave.fraction; delete toSave.speed; delete toSave.eta;
|
||||||
|
invoke('db_save_download', { id: item.id, status: item.status, queueId: item.queueId, data: JSON.stringify(toSave) }).catch(console.error);
|
||||||
|
});
|
||||||
void get().processQueue();
|
void get().processQueue();
|
||||||
},
|
},
|
||||||
setSelectedPropertiesDownloadId: (id) => set({ selectedPropertiesDownloadId: id }),
|
setSelectedPropertiesDownloadId: (id) => set({ selectedPropertiesDownloadId: id }),
|
||||||
addDownload: (item) => {
|
addDownload: (item) => {
|
||||||
set((state) => ({ downloads: [...state.downloads, item] }));
|
set((state) => ({ downloads: [...state.downloads, item] }));
|
||||||
|
const toSave = { ...item };
|
||||||
|
delete toSave.fraction; delete toSave.speed; delete toSave.eta;
|
||||||
|
invoke('db_save_download', { id: item.id, status: item.status, queueId: item.queueId, data: JSON.stringify(toSave) }).catch(console.error);
|
||||||
get().processQueue();
|
get().processQueue();
|
||||||
},
|
},
|
||||||
updateDownload: (id, updates) => {
|
updateDownload: (id, updates) => {
|
||||||
|
let updatedItem: DownloadItem | null = null;
|
||||||
set((state) => ({
|
set((state) => ({
|
||||||
downloads: state.downloads.map(d => {
|
downloads: state.downloads.map(d => {
|
||||||
if (d.id === id) {
|
if (d.id === id) {
|
||||||
@@ -189,16 +199,24 @@ export const useDownloadStore = create<DownloadState>((set, get) => ({
|
|||||||
if (newFraction === 0 && d.fraction && d.fraction > 0) {
|
if (newFraction === 0 && d.fraction && d.fraction > 0) {
|
||||||
newFraction = d.fraction;
|
newFraction = d.fraction;
|
||||||
}
|
}
|
||||||
return {
|
const updated = {
|
||||||
...d,
|
...d,
|
||||||
...updates,
|
...updates,
|
||||||
fraction: newFraction !== undefined ? newFraction : updates.fraction !== undefined ? updates.fraction : d.fraction
|
fraction: newFraction !== undefined ? newFraction : updates.fraction !== undefined ? updates.fraction : d.fraction
|
||||||
};
|
};
|
||||||
|
updatedItem = updated;
|
||||||
|
return updated;
|
||||||
}
|
}
|
||||||
return d;
|
return d;
|
||||||
})
|
})
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
if (updatedItem && Object.keys(updates).some(k => !['fraction', 'speed', 'eta'].includes(k))) {
|
||||||
|
const toSave = { ...(updatedItem as DownloadItem) };
|
||||||
|
delete toSave.fraction; delete toSave.speed; delete toSave.eta;
|
||||||
|
invoke('db_save_download', { id: toSave.id, status: toSave.status, queueId: toSave.queueId, data: JSON.stringify(toSave) }).catch(console.error);
|
||||||
|
}
|
||||||
|
|
||||||
// If status changed to something that frees up a slot, process queue
|
// If status changed to something that frees up a slot, process queue
|
||||||
if (updates.status && ['completed', 'failed', 'paused'].includes(updates.status)) {
|
if (updates.status && ['completed', 'failed', 'paused'].includes(updates.status)) {
|
||||||
get().processQueue();
|
get().processQueue();
|
||||||
@@ -219,22 +237,37 @@ export const useDownloadStore = create<DownloadState>((set, get) => ({
|
|||||||
set((state) => ({
|
set((state) => ({
|
||||||
downloads: state.downloads.filter(d => d.id !== id)
|
downloads: state.downloads.filter(d => d.id !== id)
|
||||||
}));
|
}));
|
||||||
|
invoke('db_delete_download', { id }).catch(console.error);
|
||||||
get().processQueue();
|
get().processQueue();
|
||||||
syncSystemIntegrations();
|
syncSystemIntegrations();
|
||||||
},
|
},
|
||||||
clearFinished: () => {
|
clearFinished: () => {
|
||||||
|
const downloads = get().downloads;
|
||||||
|
const toRemove = downloads.filter(d => ['completed', 'failed'].includes(d.status));
|
||||||
set((state) => ({
|
set((state) => ({
|
||||||
downloads: state.downloads.filter(d => !['completed', 'failed'].includes(d.status))
|
downloads: state.downloads.filter(d => !['completed', 'failed'].includes(d.status))
|
||||||
}));
|
}));
|
||||||
|
toRemove.forEach(d => {
|
||||||
|
invoke('db_delete_download', { id: d.id }).catch(console.error);
|
||||||
|
});
|
||||||
},
|
},
|
||||||
redownload: (id) => {
|
redownload: (id) => {
|
||||||
|
let updatedItem: DownloadItem | null = null;
|
||||||
set((state) => ({
|
set((state) => ({
|
||||||
downloads: state.downloads.map(d =>
|
downloads: state.downloads.map(d => {
|
||||||
d.id === id
|
if (d.id === id) {
|
||||||
? { ...d, status: 'queued', _dispatched: false, fraction: 0, speed: '-', eta: '-' }
|
const updated: DownloadItem = { ...d, status: 'queued', _dispatched: false, fraction: 0, speed: '-', eta: '-' };
|
||||||
: d
|
updatedItem = updated;
|
||||||
)
|
return updated;
|
||||||
|
}
|
||||||
|
return d;
|
||||||
|
})
|
||||||
}));
|
}));
|
||||||
|
if (updatedItem) {
|
||||||
|
const toSave = { ...(updatedItem as DownloadItem) };
|
||||||
|
delete toSave.fraction; delete toSave.speed; delete toSave.eta;
|
||||||
|
invoke('db_save_download', { id: toSave.id, status: toSave.status, queueId: toSave.queueId, data: JSON.stringify(toSave) }).catch(console.error);
|
||||||
|
}
|
||||||
get().processQueue();
|
get().processQueue();
|
||||||
},
|
},
|
||||||
startQueue: async (queueId) => {
|
startQueue: async (queueId) => {
|
||||||
@@ -273,18 +306,83 @@ export const useDownloadStore = create<DownloadState>((set, get) => ({
|
|||||||
return activeIds.length;
|
return activeIds.length;
|
||||||
},
|
},
|
||||||
addQueue: (name) => {
|
addQueue: (name) => {
|
||||||
set((state) => ({ queues: [...state.queues, { id: crypto.randomUUID(), name, isMain: false }] }));
|
const id = crypto.randomUUID();
|
||||||
|
const q = { id, name, isMain: false };
|
||||||
|
set((state) => ({
|
||||||
|
queues: [...state.queues, q]
|
||||||
|
}));
|
||||||
|
invoke('db_save_queue', { id, data: JSON.stringify(q) }).catch(console.error);
|
||||||
},
|
},
|
||||||
renameQueue: (id, name) => {
|
renameQueue: (id, name) => {
|
||||||
|
let updatedQ: Queue | null = null;
|
||||||
set((state) => ({
|
set((state) => ({
|
||||||
queues: state.queues.map(q => q.id === id ? { ...q, name } : q)
|
queues: state.queues.map(q => {
|
||||||
|
if (q.id === id) {
|
||||||
|
const newQ = { ...q, name };
|
||||||
|
updatedQ = newQ;
|
||||||
|
return newQ;
|
||||||
|
}
|
||||||
|
return q;
|
||||||
|
})
|
||||||
}));
|
}));
|
||||||
|
if (updatedQ) {
|
||||||
|
invoke('db_save_queue', { id, data: JSON.stringify(updatedQ) }).catch(console.error);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
removeQueue: (id) => {
|
removeQueue: (id) => {
|
||||||
|
if (id === MAIN_QUEUE_ID) return;
|
||||||
set((state) => ({
|
set((state) => ({
|
||||||
queues: state.queues.filter(q => q.id !== id || q.isMain),
|
queues: state.queues.filter(q => q.id !== id),
|
||||||
downloads: state.downloads.map(d => d.queueId === id ? { ...d, queueId: MAIN_QUEUE_ID } : d)
|
downloads: state.downloads.map(d =>
|
||||||
|
d.queueId === id ? { ...d, queueId: MAIN_QUEUE_ID } : d
|
||||||
|
)
|
||||||
}));
|
}));
|
||||||
|
invoke('db_delete_queue', { id }).catch(console.error);
|
||||||
|
|
||||||
|
// Also we need to save the updated downloads to DB
|
||||||
|
const downloads = get().downloads.filter(d => d.queueId === id);
|
||||||
|
downloads.forEach(d => {
|
||||||
|
const toSave = { ...d, queueId: MAIN_QUEUE_ID };
|
||||||
|
delete toSave.fraction; delete toSave.speed; delete toSave.eta;
|
||||||
|
invoke('db_save_download', { id: toSave.id, status: toSave.status, queueId: MAIN_QUEUE_ID, data: JSON.stringify(toSave) }).catch(console.error);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
initDB: async () => {
|
||||||
|
try {
|
||||||
|
const queuesStr = await invoke<string[]>('db_get_all_queues');
|
||||||
|
const queues = queuesStr.map(q => JSON.parse(q));
|
||||||
|
|
||||||
|
const downloadsStr = await invoke<string[]>('db_get_all_downloads');
|
||||||
|
const downloads = downloadsStr.map(d => JSON.parse(d));
|
||||||
|
|
||||||
|
set(state => ({
|
||||||
|
queues: queues.length > 0 ? queues : state.queues,
|
||||||
|
downloads: downloads.length > 0 ? downloads : state.downloads
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Auto resume downloads that were active
|
||||||
|
const active = get().downloads.filter(d => d.status === 'downloading');
|
||||||
|
active.forEach(item => {
|
||||||
|
if (item.isMedia) {
|
||||||
|
invoke('start_media_download', {
|
||||||
|
id: item.id, url: item.url, destination: item.destination,
|
||||||
|
filename: item.fileName, format_selector: item.mediaFormatSelector,
|
||||||
|
proxy: null
|
||||||
|
}).catch(console.error);
|
||||||
|
} else {
|
||||||
|
invoke('start_download', {
|
||||||
|
id: item.id, url: item.url, destination: item.destination, filename: item.fileName,
|
||||||
|
connections: item.connections, speed_limit: item.speedLimit, username: item.username,
|
||||||
|
password: item.password, headers: item.headers, checksum: item.checksum, cookies: item.cookies,
|
||||||
|
mirrors: item.mirrors, user_agent: null, max_tries: null, proxy: null
|
||||||
|
}).catch(console.error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
void get().processQueue();
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Failed to init DB", e);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
processQueue: async () => {
|
processQueue: async () => {
|
||||||
const { downloads, updateDownload } = get();
|
const { downloads, updateDownload } = get();
|
||||||
|
|||||||
@@ -1,7 +1,34 @@
|
|||||||
import { create } from 'zustand';
|
import { create } from 'zustand';
|
||||||
import { persist } from 'zustand/middleware';
|
import { persist, createJSONStorage, StateStorage } from 'zustand/middleware';
|
||||||
import { invoke } from '@tauri-apps/api/core';
|
import { invoke } from '@tauri-apps/api/core';
|
||||||
|
|
||||||
|
const tauriStorage: StateStorage = {
|
||||||
|
getItem: async (name: string): Promise<string | null> => {
|
||||||
|
if (name === 'firelink-settings') {
|
||||||
|
try {
|
||||||
|
const data = await invoke<string | null>('db_load_settings');
|
||||||
|
return data;
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Failed to load settings from DB", e);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
setItem: async (name: string, value: string): Promise<void> => {
|
||||||
|
if (name === 'firelink-settings') {
|
||||||
|
try {
|
||||||
|
await invoke('db_save_settings', { data: value });
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Failed to save settings to DB", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
removeItem: async (name: string): Promise<void> => {
|
||||||
|
// no-op for now
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
export interface SiteLogin {
|
export interface SiteLogin {
|
||||||
id: string;
|
id: string;
|
||||||
urlPattern: string;
|
urlPattern: string;
|
||||||
@@ -243,6 +270,7 @@ export const useSettingsStore = create<SettingsState>()(
|
|||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
name: 'firelink-settings',
|
name: 'firelink-settings',
|
||||||
|
storage: createJSONStorage(() => tauriStorage),
|
||||||
partialize: (state) => ({
|
partialize: (state) => ({
|
||||||
theme: state.theme,
|
theme: state.theme,
|
||||||
defaultDownloadPath: state.defaultDownloadPath,
|
defaultDownloadPath: state.defaultDownloadPath,
|
||||||
|
|||||||
Reference in New Issue
Block a user