diff --git a/Cargo.lock b/Cargo.lock index 4eab091..4bd99e4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -122,15 +122,6 @@ dependencies = [ "thiserror 1.0.69", ] -[[package]] -name = "android-build" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8cac4c64175d504608cf239756339c07f6384a476f97f20a7043f92920b0b8fd" -dependencies = [ - "windows-sys 0.52.0", -] - [[package]] name = "android-properties" version = "0.2.2" @@ -220,12 +211,6 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" -[[package]] -name = "as-raw-xcb-connection" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "175571dd1d178ced59193a6fc02dde1b972eb0bc56c892cde9beeceac5bf0f6b" - [[package]] name = "ash" version = "0.38.0+1.3.281" @@ -235,137 +220,6 @@ dependencies = [ "libloading", ] -[[package]] -name = "async-broadcast" -version = "0.7.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "435a87a52755b8f27fcf321ac4f04b2802e337c8c4872923137471ec39c37532" -dependencies = [ - "event-listener", - "event-listener-strategy", - "futures-core", - "pin-project-lite", -] - -[[package]] -name = "async-channel" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2" -dependencies = [ - "concurrent-queue", - "event-listener-strategy", - "futures-core", - "pin-project-lite", -] - -[[package]] -name = "async-executor" -version = "1.13.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "497c00e0fd83a72a79a39fcbd8e3e2f055d6f6c7e025f3b3d91f4f8e76527fb8" -dependencies = [ - "async-task", - "concurrent-queue", - "fastrand", - "futures-lite", - "pin-project-lite", - "slab", -] - -[[package]] -name = "async-io" -version = "2.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "456b8a8feb6f42d237746d4b3e9a178494627745c3c56c6ea55d92ba50d026fc" -dependencies = [ - "autocfg", - "cfg-if", - "concurrent-queue", - "futures-io", - "futures-lite", - "parking", - "polling", - "rustix 1.1.3", - "slab", - "windows-sys 0.61.2", -] - -[[package]] -name = "async-lock" -version = "3.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "290f7f2596bd5b78a9fec8088ccd89180d7f9f55b94b0576823bbbdc72ee8311" -dependencies = [ - "event-listener", - "event-listener-strategy", - "pin-project-lite", -] - -[[package]] -name = "async-process" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc50921ec0055cdd8a16de48773bfeec5c972598674347252c0399676be7da75" -dependencies = [ - "async-channel", - "async-io", - "async-lock", - "async-signal", - "async-task", - "blocking", - "cfg-if", - "event-listener", - "futures-lite", - "rustix 1.1.3", -] - -[[package]] -name = "async-recursion" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.114", -] - -[[package]] -name = "async-signal" -version = "0.2.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43c070bbf59cd3570b6b2dd54cd772527c7c3620fce8be898406dd3ed6adc64c" -dependencies = [ - "async-io", - "async-lock", - "atomic-waker", - "cfg-if", - "futures-core", - "futures-io", - "rustix 1.1.3", - "signal-hook-registry", - "slab", - "windows-sys 0.61.2", -] - -[[package]] -name = "async-task" -version = "4.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" - -[[package]] -name = "async-trait" -version = "0.1.89" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.114", -] - [[package]] name = "atomic-waker" version = "1.1.2" @@ -407,6 +261,12 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "base64" +version = "0.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac07cdecf99051d9a5238b80f35af32cdeba5b336e55d957b318b50137e18da5" + [[package]] name = "bit-set" version = "0.8.0" @@ -468,19 +328,6 @@ dependencies = [ "objc2 0.6.3", ] -[[package]] -name = "blocking" -version = "1.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e83f8d02be6967315521be875afa792a316e28d57b5a2d401897e2a7921b7f21" -dependencies = [ - "async-channel", - "async-task", - "futures-io", - "futures-lite", - "piper", -] - [[package]] name = "bumpalo" version = "3.19.1" @@ -544,9 +391,9 @@ dependencies = [ [[package]] name = "calloop" -version = "0.14.3" +version = "0.14.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb9f6e1368bd4621d2c86baa7e37de77a938adf5221e5dd3d6133340101b309e" +checksum = "4dbf9978365bac10f54d1d4b04f7ce4427e51f71d61f2fe15e3fed5166474df7" dependencies = [ "bitflags 2.10.0", "polling", @@ -573,7 +420,7 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "138efcf0940a02ebf0cc8d1eff41a1682a46b431630f4c52450d6265876021fa" dependencies = [ - "calloop 0.14.3", + "calloop 0.14.4", "rustix 1.1.3", "wayland-backend", "wayland-client", @@ -730,16 +577,6 @@ dependencies = [ "smithay-clipboard", ] -[[package]] -name = "clipboard_x11" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd63e33452ffdafd39924c4f05a5dd1e94db646c779c6bd59148a3d95fff5ad4" -dependencies = [ - "thiserror 2.0.18", - "x11rb", -] - [[package]] name = "cmake" version = "0.1.58" @@ -1079,12 +916,6 @@ dependencies = [ "hybrid-array", ] -[[package]] -name = "ctor-lite" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b29fccfdaeb0f9bd90da5759b1d0eaa2f6cfcfe90960124cfc83141ed4e111fd" - [[package]] name = "ctutils" version = "0.4.2" @@ -1170,9 +1001,9 @@ dependencies = [ [[package]] name = "dirs" -version = "6.0.0" +version = "7.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e" +checksum = "8d57d423b3c82e89b9a24ca3091fee61f456a26edbd28d26c65906f4bc1dcd8f" dependencies = [ "dirs-sys", ] @@ -1269,33 +1100,6 @@ dependencies = [ "cfg-if", ] -[[package]] -name = "endi" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "66b7e2430c6dff6a955451e2cfc438f09cea1965a9d6f87f7e3b90decc014099" - -[[package]] -name = "enumflags2" -version = "0.7.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1027f7680c853e056ebcec683615fb6fbbc07dbaa13b4d5d9442b146ded4ecef" -dependencies = [ - "enumflags2_derive", - "serde", -] - -[[package]] -name = "enumflags2_derive" -version = "0.7.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.114", -] - [[package]] name = "env_filter" version = "2.0.0" @@ -1360,27 +1164,6 @@ dependencies = [ "num-traits", ] -[[package]] -name = "event-listener" -version = "5.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" -dependencies = [ - "concurrent-queue", - "parking", - "pin-project-lite", -] - -[[package]] -name = "event-listener-strategy" -version = "0.5.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" -dependencies = [ - "event-listener", - "pin-project-lite", -] - [[package]] name = "extended" version = "0.1.0" @@ -1401,9 +1184,9 @@ checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" [[package]] name = "fastrand" -version = "2.3.0" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" +checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223" [[package]] name = "fdeflate" @@ -1531,11 +1314,11 @@ checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" [[package]] name = "fsrs" -version = "6.6.1" +version = "6.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8a99ea3dec9af37c9ed3835463ff6820a578a0026a8e370b4c1c0db48dfab3f" +checksum = "89836c0a26b347b585b23824034600b69bfc7f0b0112d0cd656791799eb3a717" dependencies = [ - "itertools 0.14.0", + "itertools 0.15.0", "log", "ndarray", "priority-queue", @@ -1586,7 +1369,6 @@ dependencies = [ "futures-core", "futures-task", "futures-util", - "num_cpus", ] [[package]] @@ -1595,19 +1377,6 @@ version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" -[[package]] -name = "futures-lite" -version = "2.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad" -dependencies = [ - "fastrand", - "futures-core", - "futures-io", - "parking", - "pin-project-lite", -] - [[package]] name = "futures-macro" version = "0.3.31" @@ -1649,16 +1418,6 @@ dependencies = [ "slab", ] -[[package]] -name = "gethostname" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bd49230192a3797a9a4d6abe9b3eed6f7fa4c8a8a4947977c6f80025f92cbd8" -dependencies = [ - "rustix 1.1.3", - "windows-link", -] - [[package]] name = "getrandom" version = "0.2.17" @@ -1750,9 +1509,9 @@ dependencies = [ [[package]] name = "gpu-alloc" -version = "0.6.0" +version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fbcd2dba93594b227a1f57ee09b8b9da8892c34d55aa332e034a228d0fe6a171" +checksum = "45cf04b2726f02df5508c6de726acdc90cdf97ac771a9a0ffd8ba10a6e696bf9" dependencies = [ "bitflags 2.10.0", "gpu-alloc-types", @@ -1760,9 +1519,9 @@ dependencies = [ [[package]] name = "gpu-alloc-types" -version = "0.3.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "98ff03b468aa837d70984d55f5d3f846f6ec31fe34bbb97c4f85219caeee1ca4" +checksum = "b2bbed164dd10ed526c2e4fe3e721ca4a71c61730e5aafac6844b417b3227058" dependencies = [ "bitflags 2.10.0", ] @@ -2020,7 +1779,7 @@ version = "0.1.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" dependencies = [ - "base64", + "base64 0.22.1", "bytes", "futures-channel", "futures-util", @@ -2241,7 +2000,6 @@ dependencies = [ "iced_debug", "iced_program", "log", - "mundy", "rustc-hash 2.1.3", "thiserror 2.0.18", "tracing", @@ -2430,9 +2188,9 @@ dependencies = [ [[package]] name = "itertools" -version = "0.14.0" +version = "0.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +checksum = "8b4baf93f58d4425749ca49a51c50ebab072c5df6994d08fed93541c331481dc" dependencies = [ "either", ] @@ -2472,7 +2230,7 @@ dependencies = [ "sha2", "tokio", "zip", - "zstd", + "zstd 0.14.0", ] [[package]] @@ -2832,15 +2590,6 @@ dependencies = [ "libc", ] -[[package]] -name = "memoffset" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" -dependencies = [ - "autocfg", -] - [[package]] name = "metal" version = "0.32.0" @@ -2902,31 +2651,6 @@ dependencies = [ "windows-sys 0.61.2", ] -[[package]] -name = "mundy" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "523813c9e194ec43693805214eb112551f99382115b67f38600d724a692e7e8b" -dependencies = [ - "android-build", - "async-io", - "cfg-if", - "dispatch", - "futures-channel", - "futures-lite", - "jni 0.21.1", - "ndk-context", - "objc2 0.6.3", - "objc2-app-kit 0.3.2", - "objc2-foundation 0.3.2", - "pin-project-lite", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", - "windows 0.62.2", - "zbus", -] - [[package]] name = "naga" version = "27.0.3" @@ -3073,16 +2797,6 @@ dependencies = [ "libm", ] -[[package]] -name = "num_cpus" -version = "1.17.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91df4bbde75afed763b708b7eee1e8e7651e02d97f6d5dd763e89367e957b23b" -dependencies = [ - "hermit-abi", - "libc", -] - [[package]] name = "num_enum" version = "0.7.5" @@ -3149,8 +2863,8 @@ dependencies = [ "block2 0.5.1", "libc", "objc2 0.5.2", - "objc2-core-data 0.2.2", - "objc2-core-image 0.2.2", + "objc2-core-data", + "objc2-core-image", "objc2-foundation 0.2.2", "objc2-quartz-core 0.2.2", ] @@ -3163,17 +2877,8 @@ checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c" dependencies = [ "bitflags 2.10.0", "block2 0.6.2", - "libc", "objc2 0.6.3", - "objc2-cloud-kit 0.3.2", - "objc2-core-data 0.3.2", - "objc2-core-foundation", - "objc2-core-graphics", - "objc2-core-image 0.3.2", - "objc2-core-text", - "objc2-core-video", "objc2-foundation 0.3.2", - "objc2-quartz-core 0.3.2", ] [[package]] @@ -3214,17 +2919,6 @@ dependencies = [ "objc2-foundation 0.2.2", ] -[[package]] -name = "objc2-cloud-kit" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73ad74d880bb43877038da939b7427bba67e9dd42004a18b809ba7d87cee241c" -dependencies = [ - "bitflags 2.10.0", - "objc2 0.6.3", - "objc2-foundation 0.3.2", -] - [[package]] name = "objc2-contacts" version = "0.2.2" @@ -3271,17 +2965,6 @@ dependencies = [ "objc2-foundation 0.2.2", ] -[[package]] -name = "objc2-core-data" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b402a653efbb5e82ce4df10683b6b28027616a2715e90009947d50b8dd298fa" -dependencies = [ - "bitflags 2.10.0", - "objc2 0.6.3", - "objc2-foundation 0.3.2", -] - [[package]] name = "objc2-core-foundation" version = "0.3.2" @@ -3320,16 +3003,6 @@ dependencies = [ "objc2-metal", ] -[[package]] -name = "objc2-core-image" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5d563b38d2b97209f8e861173de434bd0214cf020e3423a52624cd1d989f006" -dependencies = [ - "objc2 0.6.3", - "objc2-foundation 0.3.2", -] - [[package]] name = "objc2-core-location" version = "0.2.2" @@ -3342,31 +3015,6 @@ dependencies = [ "objc2-foundation 0.2.2", ] -[[package]] -name = "objc2-core-text" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0cde0dfb48d25d2b4862161a4d5fcc0e3c24367869ad306b0c9ec0073bfed92d" -dependencies = [ - "bitflags 2.10.0", - "objc2 0.6.3", - "objc2-core-foundation", - "objc2-core-graphics", -] - -[[package]] -name = "objc2-core-video" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d425caf1df73233f29fd8a5c3e5edbc30d2d4307870f802d18f00d83dc5141a6" -dependencies = [ - "bitflags 2.10.0", - "objc2 0.6.3", - "objc2-core-foundation", - "objc2-core-graphics", - "objc2-io-surface", -] - [[package]] name = "objc2-encode" version = "4.1.0" @@ -3478,9 +3126,9 @@ dependencies = [ "bitflags 2.10.0", "block2 0.5.1", "objc2 0.5.2", - "objc2-cloud-kit 0.2.2", - "objc2-core-data 0.2.2", - "objc2-core-image 0.2.2", + "objc2-cloud-kit", + "objc2-core-data", + "objc2-core-image", "objc2-core-location", "objc2-foundation 0.2.2", "objc2-link-presentation", @@ -3559,23 +3207,13 @@ dependencies = [ [[package]] name = "ordered-float" -version = "5.1.0" +version = "5.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f4779c6901a562440c3786d08192c6fbda7c1c2060edd10006b05ee35d10f2d" +checksum = "8c7c9e0d9b23589f26070720bac724174bfec1083e82f7854cdd0267518343c0" dependencies = [ "num-traits", ] -[[package]] -name = "ordered-stream" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9aa2b01e1d916879f73a53d01d1d6cee68adbb31d6d9177a8cfce093cced1d50" -dependencies = [ - "futures-core", - "pin-project-lite", -] - [[package]] name = "ouroboros" version = "0.18.5" @@ -3619,12 +3257,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "parking" -version = "2.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" - [[package]] name = "parking_lot" version = "0.12.5" @@ -3708,17 +3340,6 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" -[[package]] -name = "piper" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96c8c490f422ef9a4efd2cb5b42b76c8613d7e7dfc1caf667b8a3350a5acc066" -dependencies = [ - "atomic-waker", - "fastrand", - "futures-io", -] - [[package]] name = "pkg-config" version = "0.3.32" @@ -4011,9 +3632,9 @@ dependencies = [ [[package]] name = "range-alloc" -version = "0.1.4" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3d6831663a5098ea164f89cff59c6284e95f4e3c76ce9848d4529f5ccca9bde" +checksum = "ca45419789ae5a7899559e9512e58ca889e41f04f1f2445e9f4b290ceccd1d08" [[package]] name = "rangemap" @@ -4139,11 +3760,11 @@ checksum = "19b30a45b0cd0bcca8037f3d0dc3421eaf95327a17cad11964fb8179b4fc4832" [[package]] name = "reqwest" -version = "0.13.4" +version = "0.13.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "219c5811de6525e5416c7d5d53bb656d3afdbc6c5af816e0802bcfa42dbdc1c3" +checksum = "16a1cfa75cc186dd73d5818e510e042e40927bccc9c236b061cea97e1eb08029" dependencies = [ - "base64", + "base64 0.23.1", "bytes", "encoding_rs", "futures-core", @@ -4236,9 +3857,9 @@ dependencies = [ [[package]] name = "rhai" -version = "1.25.1" +version = "1.26.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd4dd0f8c36625202a4ba553c416c19b719947cd2a31d1bda06126e4a5727daf" +checksum = "5e6e1be9d697d537ce450766df42ad6adf1a93f25f21d73217354ad7e3d3dae1" dependencies = [ "ahash", "bitflags 2.10.0", @@ -4598,17 +4219,6 @@ dependencies = [ "zmij", ] -[[package]] -name = "serde_repr" -version = "0.1.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.114", -] - [[package]] name = "sha1" version = "0.11.0" @@ -4637,16 +4247,6 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" -[[package]] -name = "signal-hook-registry" -version = "1.4.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" -dependencies = [ - "errno", - "libc", -] - [[package]] name = "simd-adler32" version = "0.3.10" @@ -4758,7 +4358,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0512da38f5e2b31201a93524adb8d3136276fa4fe4aafab4e1f727a82b534cc0" dependencies = [ "bitflags 2.10.0", - "calloop 0.14.3", + "calloop 0.14.4", "calloop-wayland-source 0.4.1", "cursor-icon", "libc", @@ -4835,7 +4435,6 @@ version = "0.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aac18da81ebbf05109ab275b157c22a653bb3c12cf884450179942f81bcbf6c3" dependencies = [ - "as-raw-xcb-connection", "bytemuck", "fastrand", "js-sys", @@ -4849,7 +4448,6 @@ dependencies = [ "raw-window-handle", "redox_syscall 0.5.18", "rustix 1.1.3", - "tiny-xlib", "tracing", "wasm-bindgen", "wayland-backend", @@ -4857,7 +4455,6 @@ dependencies = [ "wayland-sys", "web-sys", "windows-sys 0.61.2", - "x11rb", ] [[package]] @@ -5181,19 +4778,6 @@ dependencies = [ "libc", ] -[[package]] -name = "tempfile" -version = "3.24.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "655da9c7eb6305c55742045d5a8d2037996d61d8de95806335c7c86ce0f82e9c" -dependencies = [ - "fastrand", - "getrandom 0.3.4", - "once_cell", - "rustix 1.1.3", - "windows-sys 0.61.2", -] - [[package]] name = "termcolor" version = "1.4.1" @@ -5304,19 +4888,6 @@ dependencies = [ "strict-num", ] -[[package]] -name = "tiny-xlib" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0324504befd01cab6e0c994f34b2ffa257849ee019d3fb3b64fb2c858887d89e" -dependencies = [ - "as-raw-xcb-connection", - "ctor-lite", - "libloading", - "pkg-config", - "tracing", -] - [[package]] name = "tinystr" version = "0.8.3" @@ -5535,17 +5106,6 @@ version = "1.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40ce102ab67701b8526c123c1bab5cbe42d7040ccfd0f64af1a385808d2f43de" -[[package]] -name = "uds_windows" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89daebc3e6fd160ac4aa9fc8b3bf71e1f74fbf92367ae71fb83a037e8bf164b9" -dependencies = [ - "memoffset", - "tempfile", - "winapi", -] - [[package]] name = "unicase" version = "2.9.0" @@ -5642,7 +5202,7 @@ version = "0.45.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "80be9b06fbae3b8b303400ab20778c80bbaf338f563afe567cf3c9eea17b47ef" dependencies = [ - "base64", + "base64 0.22.1", "data-url", "flate2", "fontdb", @@ -5675,17 +5235,6 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" -[[package]] -name = "uuid" -version = "1.20.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee48d38b119b0cd71fe4141b30f5ba9c7c5d9f4e7a3a8b4a674e4b6ef789976f" -dependencies = [ - "js-sys", - "serde_core", - "wasm-bindgen", -] - [[package]] name = "vcpkg" version = "0.2.15" @@ -6222,7 +5771,6 @@ dependencies = [ "clipboard-win", "clipboard_macos", "clipboard_wayland", - "clipboard_x11", "raw-window-handle", "thiserror 2.0.18", ] @@ -6580,7 +6128,6 @@ dependencies = [ "atomic-waker", "bitflags 2.10.0", "block2 0.5.1", - "bytemuck", "calloop 0.13.0", "cfg_aliases", "concurrent-queue", @@ -6597,7 +6144,6 @@ dependencies = [ "objc2-foundation 0.2.2", "objc2-ui-kit", "orbclient", - "percent-encoding", "pin-project", "raw-window-handle", "redox_syscall 0.4.1", @@ -6616,8 +6162,6 @@ dependencies = [ "web-sys", "web-time", "windows-sys 0.52.0", - "x11-dl", - "x11rb", "xkbcommon-dl", ] @@ -6724,43 +6268,11 @@ version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" -[[package]] -name = "x11-dl" -version = "2.21.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38735924fedd5314a6e548792904ed8c6de6636285cb9fec04d5b1db85c1516f" -dependencies = [ - "libc", - "once_cell", - "pkg-config", -] - -[[package]] -name = "x11rb" -version = "0.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9993aa5be5a26815fe2c3eacfc1fde061fc1a1f094bf1ad2a18bf9c495dd7414" -dependencies = [ - "as-raw-xcb-connection", - "gethostname", - "libc", - "libloading", - "once_cell", - "rustix 1.1.3", - "x11rb-protocol", -] - -[[package]] -name = "x11rb-protocol" -version = "0.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea6fc2961e4ef194dcbfe56bb845534d0dc8098940c7e5c012a258bfec6701bd" - [[package]] name = "xcursor" -version = "0.3.10" +version = "0.3.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bec9e4a500ca8864c5b47b8b482a73d62e4237670e5b5f1d6b9e3cae50f28f2b" +checksum = "163b33ed8786455e2fa5d72f554057ce3f3182425434f756cd39c99839d88e23" [[package]] name = "xkbcommon-dl" @@ -6783,9 +6295,9 @@ checksum = "b9cc00251562a284751c9973bace760d86c0276c471b4be569fe6b068ee97a56" [[package]] name = "xml-rs" -version = "0.8.28" +version = "0.8.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ae8337f8a065cfc972643663ea4279e04e7256de865aa66fe25cec5fb912d3f" +checksum = "e450f9b2ed1dff33c94c12589a87338689467b9c4f5d8a5710bd09a847d2c8a7" [[package]] name = "xmlwriter" @@ -6828,67 +6340,6 @@ dependencies = [ "synstructure", ] -[[package]] -name = "zbus" -version = "5.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bfeff997a0aaa3eb20c4652baf788d2dfa6d2839a0ead0b3ff69ce2f9c4bdd1" -dependencies = [ - "async-broadcast", - "async-executor", - "async-io", - "async-lock", - "async-process", - "async-recursion", - "async-task", - "async-trait", - "blocking", - "enumflags2", - "event-listener", - "futures-core", - "futures-lite", - "hex", - "libc", - "ordered-stream", - "rustix 1.1.3", - "serde", - "serde_repr", - "tracing", - "uds_windows", - "uuid", - "windows-sys 0.61.2", - "winnow", - "zbus_macros", - "zbus_names", - "zvariant", -] - -[[package]] -name = "zbus_macros" -version = "5.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bbd5a90dbe8feee5b13def448427ae314ccd26a49cac47905cafefb9ff846f1" -dependencies = [ - "proc-macro-crate", - "proc-macro2", - "quote", - "syn 2.0.114", - "zbus_names", - "zvariant", - "zvariant_utils", -] - -[[package]] -name = "zbus_names" -version = "4.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ffd8af6d5b78619bab301ff3c560a5bd22426150253db278f164d6cf3b72c50f" -dependencies = [ - "serde", - "winnow", - "zvariant", -] - [[package]] name = "zeno" version = "0.3.3" @@ -6999,7 +6450,7 @@ dependencies = [ "typed-path", "zeroize", "zopfli", - "zstd", + "zstd 0.13.3", ] [[package]] @@ -7032,7 +6483,16 @@ version = "0.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a" dependencies = [ - "zstd-safe", + "zstd-safe 7.2.4", +] + +[[package]] +name = "zstd" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf06bd8162af0734b344780deb55b42a2429ae430870d13fcc12f238e880fe6e" +dependencies = [ + "zstd-safe 8.0.0", ] [[package]] @@ -7045,10 +6505,19 @@ dependencies = [ ] [[package]] -name = "zstd-sys" -version = "2.0.16+zstd.1.5.7" +name = "zstd-safe" +version = "8.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91e19ebc2adc8f83e43039e79776e3fda8ca919132d68a1fed6a5faca2683748" +checksum = "ae42c0555055784c70058d19ba8e275528e8a99a706684868ace5da4e716a4ab" +dependencies = [ + "zstd-sys", +] + +[[package]] +name = "zstd-sys" +version = "2.1.0+zstd.1.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ef0a8027ec3ee71300ab3bcbcd0393f434aa72b91ca6d635a39941deae8eea0" dependencies = [ "cc", "pkg-config", @@ -7068,43 +6537,3 @@ checksum = "29ce2c8a9384ad323cf564b67da86e21d3cfdff87908bc1223ed5c99bc792713" dependencies = [ "zune-core", ] - -[[package]] -name = "zvariant" -version = "5.9.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68b64ef4f40c7951337ddc7023dd03528a57a3ce3408ee9da5e948bd29b232c4" -dependencies = [ - "endi", - "enumflags2", - "serde", - "winnow", - "zvariant_derive", - "zvariant_utils", -] - -[[package]] -name = "zvariant_derive" -version = "5.9.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "484d5d975eb7afb52cc6b929c13d3719a20ad650fea4120e6310de3fc55e415c" -dependencies = [ - "proc-macro-crate", - "proc-macro2", - "quote", - "syn 2.0.114", - "zvariant_utils", -] - -[[package]] -name = "zvariant_utils" -version = "3.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f75c23a64ef8f40f13a6989991e643554d9bef1d682a281160cf0c1bc389c5e9" -dependencies = [ - "proc-macro2", - "quote", - "serde", - "syn 2.0.114", - "winnow", -] diff --git a/Cargo.toml b/Cargo.toml index 7f51155..21a7579 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,20 +4,20 @@ version = "0.1.3" edition = "2024" [dependencies] -iced = { version = "0.14.0", features = ["tokio", "svg", "lazy"]} +iced = { version = "0.14.0", features = ["tokio", "svg", "lazy", "crisp", "tiny-skia", "wayland", "wgpu"], default-features = false} iced_core = "0.14.0" rand = "0.10.2" -dirs = "6.0.0" +dirs = "7.0.0" serde_json = "1.0.151" -rhai = { version = "1.25.1", features = ["sync"] } +rhai = { version = "1.26.0", features = ["sync"] } chrono = "0.4.45" rusqlite = { version = "0.40.2", features = ["chrono", "bundled"] } -reqwest = { version = "0.13.4", features = ["json", "stream", "multipart"] } +reqwest = { version = "0.13.5", features = ["json", "stream", "multipart"] } rodio = "0.22.2" sha2 = "0.11.0" tokio = { version = "1.53.1", features = ["rt", "rt-multi-thread", "macros"] } hex = "0.4.3" -zstd = "0.13.3" +zstd = "0.14.0" zip = "8.6.0" rfd = "0.17.2" mimalloc = "0.1.52" @@ -26,7 +26,7 @@ rayon = "1.12.0" parking_lot = "0.12.5" env_logger = "0.11.11" log = "0.4.34" -fsrs = "6.6.1" +fsrs = "6.6.2" litemap = { version = "0.8.3", features = ["serde"] } [profile.super-release] diff --git a/sql/history.sql b/sql/history.sql index 9355ab2..8fb9af8 100644 --- a/sql/history.sql +++ b/sql/history.sql @@ -2,8 +2,8 @@ create table history ( id INTEGER primary key autoincrement, - set_id INTEGER not null references decks on delete cascade, - stat_id INTEGER not null references states on delete cascade, + deck_id INTEGER not null references decks on delete cascade, + word_id INTEGER not null references words on delete cascade, timestamp INTEGER not null, answer INTEGER not null, before INTEGER not null, diff --git a/sql/states.sql b/sql/states.sql index 32c0b1b..da3d6f0 100644 --- a/sql/states.sql +++ b/sql/states.sql @@ -5,7 +5,7 @@ create table states word_id INTEGER not null references words on delete cascade, - set_id INTEGER not null + deck_id INTEGER not null references decks on delete cascade, score INTEGER default 1 not null, diff --git a/src/app.rs b/src/app.rs index eaafa91..9254807 100644 --- a/src/app.rs +++ b/src/app.rs @@ -1,21 +1,23 @@ -use crate::data::Id; -use crate::data::entities::DeckSettings; -use crate::data::entities::{WordData, WordGroup}; -use crate::data_provider::decks::load_sets; +use crate::data::repositories::Repository; +use crate::data_provider::card_states::CardStateRepository; +use crate::data_provider::decks::DeckRepository; +use crate::data_provider::history::HistoryRepository; use crate::data_provider::settings::get_setting; -use crate::data_provider::sqlite::default_connection; -use crate::data_provider::words::{load_word_groups, load_words}; +use crate::data_provider::sqlite::SqliteConnection; +use crate::data_provider::word_groups::WordGroupRepository; +use crate::data_provider::words::WordRepository; use crate::navigation::RootMessage::Keyboard; use crate::navigation::{AppSettings, RootMessage, ScreenState}; -use chrono::NaiveDate; -use hashbrown::HashMap; use iced::{Subscription, keyboard, window}; use iced_core::window::Position; #[cfg(target_os = "linux")] use iced_core::window::settings::PlatformSpecific; use iced_core::{Font, Size, Theme}; use log::info; +use parking_lot::{Mutex, RwLock}; use rusqlite::Connection; +use std::sync::Arc; + pub const USER_FONT: Font = Font::with_name("Noto Sans JP"); pub fn start() -> iced::Result { @@ -57,13 +59,15 @@ fn subscription(_state: &ScreenState) -> Subscription { keyboard::listen().map(Keyboard) } +#[derive(Clone)] pub struct AppState { - pub dictionary: Vec, - pub decks: Vec, - pub word_groups: Vec, - pub connection: Connection, - pub sync_data: AppSettings, - pub activity: HashMap>, + pub dictionary: WordRepository, + pub decks: DeckRepository, + pub word_groups: WordGroupRepository, + pub card_states: CardStateRepository, + pub connection: Arc>, + pub sync_data: Arc>, + pub history: HistoryRepository, } impl Default for AppState { @@ -74,30 +78,34 @@ impl Default for AppState { impl AppState { pub fn new() -> Self { + let connection = Arc::new(Mutex::new(SqliteConnection::default())); Self { - dictionary: vec![], - decks: vec![], - word_groups: vec![], - connection: default_connection(), - sync_data: AppSettings { + dictionary: WordRepository::new(connection.clone()), + decks: DeckRepository::new(connection.clone()), + word_groups: WordGroupRepository::new(connection.clone()), + card_states: CardStateRepository::new(connection.clone()), + history: HistoryRepository::new(connection.clone()), + sync_data: Arc::new(RwLock::new(AppSettings { key: None, auto_web_fetch: false, - }, - activity: Default::default(), + })), + connection, } } } pub fn fill_state(state: &mut AppState) { - let list = load_words(&state.connection); - let sets = load_sets(&state.connection); - let groups = load_word_groups(&state.connection); - let setting = load_settings(&state.connection); + let connection = state.connection.lock(); - state.dictionary = list.unwrap_or_default(); - state.decks = sets.unwrap_or_default(); - state.word_groups = groups.unwrap_or_default(); - state.sync_data = setting + let setting = load_settings(&connection); + drop(connection); + state.word_groups.write().start().unwrap(); + state.dictionary.write().start().unwrap(); + state.decks.write().start().unwrap(); + state.history.write().start().unwrap(); + state.card_states.write().start().unwrap(); + + state.sync_data = Arc::new(RwLock::new(setting)) } fn load_settings(connection: &Connection) -> AppSettings { diff --git a/src/core/deck.rs b/src/core/deck.rs index b09e41c..c7e687b 100644 --- a/src/core/deck.rs +++ b/src/core/deck.rs @@ -5,134 +5,185 @@ use crate::core::srs::{ WordOpenMode, WorstWordsSRSModule, }; use crate::data::entities::*; -use crate::data_provider::card_states::{load_stats_of_deck, update_stat_score}; -use crate::data_provider::history::push_note; -use crate::notification::{MaybeError, NotificationData}; +use crate::data::repositories::Repository; +use crate::data_provider::card_states::CardStateRepositoryInner; +use crate::notification::MaybeError; use chrono::Utc; -use parking_lot::Mutex; +use log::info; +use rhai::{Engine, Scope}; use std::sync::Arc; +use std::time::Instant; #[derive(Clone)] pub struct DeckData { - pub(crate) words: Vec, - pub(crate) set: Vec, + pub(crate) words: Vec>, + pub(crate) states: Vec>, current_word_index: Option, - state: Arc>, + state: AppState, order_module: OrderModule, open_mode: OrderMode, settings: DeckSettings, } impl DeckData { - pub fn new( - settings: &DeckSettings, - state: Arc>, - ) -> Result { - let state_for = state.clone(); - let state_locked = state.lock(); - - let stats = load_stats_of_deck(settings, &state_locked.connection)?; - let words = stats - .iter() - .map(|stat| { - state_locked - .dictionary - .binary_search_by_key(&stat.word_id, |w| w.id) - .expect("No word found in dictionary") - }) - .map(|index| &state_locked.dictionary[index]) - .cloned() + pub fn new(settings: &DeckSettings, open_mode: OrderMode, state: AppState) -> Self { + let states_repo = state.card_states.read(); + let mut query = states_repo.deck_states(settings.id); + let words = query + .iter(&states_repo) + .map(|card_state| EntityHandle::from_id(card_state.word_id)) .collect(); - - Ok(Self { - set: stats, + let states = query.handles().collect::>>(); + let fsrs = Fsrs(FsrsModule::new(Self::set_links(states.as_slice(), &states_repo).as_slice())); + drop(states_repo); + Self { + states, words, current_word_index: None, - state: state_for, - order_module: Fsrs(FsrsModule::new()), - open_mode: settings.open_mode, + state, + order_module: fsrs, + open_mode, settings: settings.clone(), - }) + } } - pub fn next(&mut self) -> (WordData, CardState) { + pub fn next(&mut self) -> (EntityHandle, EntityHandle) { + if self.fsrs_finished() { + self.switch_order_engine() + } + let repo = self.state.card_states.read(); + let links = Self::set_links(self.states.as_slice(), &repo); let index = match self.order_module { - SemiRandomSRS(ref mut module) => { - if !module.initialized { - module.init(self.set.as_slice()) - } - module.next(self.set.as_slice()) - } + SemiRandomSRS(ref mut module) => module.next(links.as_slice()), - RandomSRS(ref mut module) => { - if !module.initialized { - module.init(self.set.as_slice()) - } - module.next(self.set.as_slice()) - } - WorstWordsSRS(ref mut module) => { - if !module.initialized { - module.init(self.set.as_slice()) - } - module.next(self.set.as_slice()) - } - Fsrs(ref mut module) => { - if !module.initialized { - module.init(self.set.as_slice()) - } - module.next(self.set.as_slice()) - } + RandomSRS(ref mut module) => module.next(links.as_slice()), + WorstWordsSRS(ref mut module) => module.next(links.as_slice()), + Fsrs(ref mut module) => module.next(links.as_slice()), }; self.current_word_index = Some(index); - (self.words[index].clone(), self.set[index].clone()) + (self.words[index].clone(), self.states[index].clone()) } + + fn set_links<'a>( + set: &[EntityHandle], + repo: &'a CardStateRepositoryInner, + ) -> Vec<&'a CardState> { + set.iter() + .map(|h| repo.get_ref_static(*h).unwrap()) + .collect::>() + } + pub fn open(&mut self, status: WordOpenMode) -> MaybeError { if let Some(index) = self.current_word_index { - let word: &mut CardState = &mut self.set[index]; - let old_score = word.score; - word.update(status); - let new_score = word.score; + let mut repo = self.state.card_states.write(); + let card_state_handle = &mut self.states[index]; + let card_state = repo.get_mut(card_state_handle).unwrap(); + let old_score = card_state.score; + card_state.update(status); + let new_score = card_state.score; + let mut fsrs = false; match self.order_module { SemiRandomSRS(ref mut module) => { - module.open(status, index, word); + module.open(status, index, card_state); } RandomSRS(ref mut module) => { - module.open(status, index, word); + module.open(status, index, card_state); } WorstWordsSRS(ref mut module) => { - module.open(status, index, word); + module.open(status, index, card_state); } Fsrs(ref mut module) => { - module.open(status, index, word); + module.open(status, index, card_state); + fsrs = true; } } - if let Fsrs(ref fsrs) = self.order_module - && fsrs.finished() - { - self.order_module = match self.open_mode { - OrderMode::Default => SemiRandomSRS(SemiRandomSRSModule::new()), - OrderMode::TrainWorstFirst => WorstWordsSRS(WorstWordsSRSModule::new()), - OrderMode::FullRandom => RandomSRS(RandomSRSModule::new()), - } - } - update_stat_score(word, &self.state.lock().connection)?; - let _ = push_note( - self.settings.id, - HistoryItem { - timestamp: Utc::now(), - word_id: word.word_id, - mode: status, - before: old_score, - after: new_score, - }, - ); + let word_id = card_state.word_id; + repo.save(card_state_handle)?; + + let mut history = self.state.history.write(); + history.create_now(HistoryNote { + id: Default::default(), + timestamp: Utc::now(), + deck_id: self.settings.id, + word_id, + answer: status.clone(), + before: old_score, + after: new_score, + fsrs, + })?; } Ok(()) } + + fn switch_order_engine(&mut self) { + let repo = self.state.card_states.read(); + let links = Self::set_links(self.states.as_slice(), &repo); + self.order_module = match self.open_mode { + OrderMode::Default => SemiRandomSRS(SemiRandomSRSModule::new(links.as_slice())), + OrderMode::TrainWorstFirst => WorstWordsSRS(WorstWordsSRSModule::new(links.as_slice())), + OrderMode::FullRandom => RandomSRS(RandomSRSModule::new(links.as_slice())), + } + } + + fn fsrs_finished(&self) -> bool { + if let Fsrs(ref fsrs) = self.order_module + && fsrs.finished() + { + info!("Fsrs finished. Infinite mode enabled"); + return true; + } + + false + } + pub fn len(&self) -> usize { - self.set.len() + self.states.len() + } + + pub fn list(deck: &DeckSettings, state: &AppState) -> Vec> { + let time = Instant::now(); + let list = vec![]; + let engine = Engine::new(); + + let ast = engine.compile(&deck.filter); + if let Ok(ast) = ast { + let groups = state.word_groups.read().iter().cloned().collect::>(); + let dictionary = &state.dictionary.read(); + let query = dictionary.query_par(Arc::new(move |word| { + let mut more = rhai::Map::new(); + for iced in &word.additional { + more.insert(iced.0.clone().into(), iced.1.clone().into()); + } + let mut scope = Scope::new(); + let group_name = groups[groups + .binary_search_by_key(&word.group_id, |g| g.id) + .unwrap()] + .name + .clone(); + scope + .push_constant("id", word.id.0 as i64) + .push_constant("key", word.key.clone()) + .push_constant("value", word.value.clone()) + .push_constant("tags", word.tags.clone()) + .push_constant("more", more) + .push_constant("group", group_name); + + let result = engine.eval_ast_with_scope::(&mut scope, &ast); + if let Ok(result) = result { + return result; + } + false + })); + + info!("Collecting available words is {:?}", time.elapsed()); + let handles = query.handles().collect(); + drop(query); + handles + } else { + list + } } } diff --git a/src/core/srs.rs b/src/core/srs.rs index bd85444..a7a2936 100644 --- a/src/core/srs.rs +++ b/src/core/srs.rs @@ -1,6 +1,7 @@ use crate::data::entities::CardState; use chrono::{Local, TimeDelta}; use fsrs::FSRS; +use log::{debug, info}; use rand::distr::weighted::WeightedIndex; use rand::prelude::ThreadRng; use rand::prelude::{Distribution, SliceRandom}; @@ -31,25 +32,24 @@ pub enum OrderModule { Fsrs(FsrsModule), } pub trait SRSModule { - fn next(&mut self, set: &[CardState]) -> usize; + fn new(set: &[&CardState]) -> Self; + fn next(&mut self, set: &[&CardState]) -> usize; fn open(&mut self, status: WordOpenMode, index: usize, updated_word: &mut CardState); - fn init(&mut self, set: &[CardState]); } #[derive(Clone)] pub struct RandomSRSModule { basket: Vec, - pub(crate) initialized: bool, -} -impl RandomSRSModule { - pub(crate) fn new() -> RandomSRSModule { - Self { - basket: vec![], - initialized: false, - } - } } impl SRSModule for RandomSRSModule { - fn next(&mut self, set: &[CardState]) -> usize { + fn new(set: &[&CardState]) -> Self { + let mut basket = (0..set.len()).collect::>(); + basket.shuffle(&mut rand::rng()); + Self{ + basket, + } + } + + fn next(&mut self, set: &[&CardState]) -> usize { match self.basket.pop() { Some(index) => index, None => { @@ -61,32 +61,15 @@ impl SRSModule for RandomSRSModule { } fn open(&mut self, _: WordOpenMode, _: usize, _: &mut CardState) {} - - fn init(&mut self, set: &[CardState]) { - self.initialized = true; - self.basket = (0..set.len()).collect::>(); - self.basket.shuffle(&mut rand::rng()) - } } #[derive(Clone)] pub struct SemiRandomSRSModule { history: Vec, last_weights: WeightedIndex, generator: ThreadRng, - pub(crate) initialized: bool, -} -impl SemiRandomSRSModule { - pub(crate) fn new() -> SemiRandomSRSModule { - SemiRandomSRSModule { - history: vec![], - last_weights: WeightedIndex::new([1.0]).unwrap(), - generator: rng(), - initialized: false, - } - } } impl SRSModule for SemiRandomSRSModule { - fn next(&mut self, set: &[CardState]) -> usize { + fn next(&mut self, set: &[&CardState]) -> usize { let index = self.last_weights.sample(&mut self.generator); if self.history.contains(&index) && self.history.len() != 1 { @@ -106,23 +89,27 @@ impl SRSModule for SemiRandomSRSModule { .update_weights(&[(index, &new_weight)]) .expect("update weights error"); } - fn init(&mut self, set: &[CardState]) { - self.initialized = true; + fn new(set: &[&CardState]) -> Self { + let weights = set .iter() .map(|s| (100.0 / s.calculated_score()).powf(2.0) * 2.0) .collect::>(); - self.last_weights = WeightedIndex::new(weights).expect("Incorrect weights update"); + + Self{ + generator: rng(), + last_weights: WeightedIndex::new(weights).expect("Incorrect weights update"), + history: vec![] + } } } impl SemiRandomSRSModule { - fn history_len(&self, set: &[CardState]) -> usize { + fn history_len(&self, set: &[&CardState]) -> usize { ((set.len() as f32 * MAX_HISTORY_LEN_PART) as usize).clamp(1, MAX_HISTORY_LEN) } } #[derive(Clone)] pub struct WorstWordsSRSModule { - pub(crate) initialized: bool, pool_size: usize, rounds_remaining: u8, pool: Vec, @@ -130,7 +117,7 @@ pub struct WorstWordsSRSModule { rounds_count: u8, } impl SRSModule for WorstWordsSRSModule { - fn next(&mut self, set: &[CardState]) -> usize { + fn next(&mut self, set: &[&CardState]) -> usize { if self.rounds_remaining == 0 { self.fill_pool(set); self.rounds_remaining = self.rounds_count; @@ -148,14 +135,8 @@ impl SRSModule for WorstWordsSRSModule { fn open(&mut self, _: WordOpenMode, _: usize, _: &mut CardState) {} - fn init(&mut self, _: &[CardState]) { - self.initialized = true; - } -} -impl WorstWordsSRSModule { - pub(crate) fn new() -> WorstWordsSRSModule { + fn new(_: &[&CardState]) -> Self { WorstWordsSRSModule { - initialized: false, pool_size: PARTIAL_POOL_SIZE, rounds_count: 2, rounds_remaining: 0, @@ -163,7 +144,9 @@ impl WorstWordsSRSModule { queue: vec![], } } - fn fill_pool(&mut self, set: &[CardState]) { +} +impl WorstWordsSRSModule { + fn fill_pool(&mut self, set: &[&CardState]) { let mut sorted = set.iter().enumerate().collect::>(); sorted.sort_by_key(|c| c.1.score); let mut worst = sorted[0..self.pool_size] @@ -183,11 +166,32 @@ pub struct FsrsModule { queue: Vec, rng: ThreadRng, fsrs: FSRS, - pub(crate) initialized: bool, } impl SRSModule for FsrsModule { - fn next(&mut self, _: &[CardState]) -> usize { + fn new(set: &[&CardState]) -> Self { + let today = Local::now().date_naive(); + + let mut new = Self { + today_set: set + .iter() + .enumerate() + .filter(|(_, c)| c.fsrs_open_day <= today) + .map(|(index, _)| index) + .collect(), + current_group: vec![], + queue: vec![], + solved: vec![], + rng: rng(), + fsrs: FSRS::default(), + }; + new.today_set.shuffle(&mut new.rng); + new.fill_partial_pool(); + + new + } + + fn next(&mut self, _: &[&CardState]) -> usize { if self.current_group.is_empty() { self.fill_partial_pool(); } @@ -222,45 +226,25 @@ impl SRSModule for FsrsModule { let offset = ((rng().random::() - 0.5) * 3.0).round() as i32; next_day += TimeDelta::days(offset as i64); } + + let first_time = updated_word.fsrs_memory.is_none(); + updated_word.fsrs_memory = Some(next.memory); updated_word.fsrs_open_day = next_day; updated_word.fsrs_opened = updated_word.last_open.date_naive(); - if updated_word.fsrs_open_day != today { + if (updated_word.fsrs_open_day != today && !first_time) || (first_time && status == WordOpenMode::Easy) { + info!("For today word on index {} solved", index); self.solved.push(index); let index = self.current_group.iter().position(|x| *x == index).unwrap(); self.current_group.remove(index); } } - - fn init(&mut self, set: &[CardState]) { - self.rng = rng(); - let today = Local::now().date_naive(); - self.today_set = set - .iter() - .enumerate() - .filter(|(_, c)| c.fsrs_open_day <= today) - .map(|(index, _)| index) - .collect(); - self.today_set.shuffle(&mut self.rng); - self.fill_partial_pool(); - self.initialized = true; - } } impl FsrsModule { - pub fn new() -> Self { - Self { - today_set: vec![], - current_group: vec![], - queue: vec![], - solved: vec![], - rng: Default::default(), - fsrs: FSRS::default(), - initialized: false, - } - } fn fill_partial_pool(&mut self) { + debug!("Filling partial pool"); self.current_group.extend( self.today_set .iter() @@ -281,7 +265,7 @@ impl FsrsModule { } } -#[derive(Clone, Copy)] +#[derive(Clone, Copy, PartialEq, Eq)] pub enum WordOpenMode { Easy, Ok, diff --git a/src/data.rs b/src/data.rs index f6e2374..99d4446 100644 --- a/src/data.rs +++ b/src/data.rs @@ -3,7 +3,7 @@ pub mod repositories; use rusqlite::ToSql; use rusqlite::types::{FromSql, FromSqlError, FromSqlResult, ToSqlOutput, ValueRef}; -use std::fmt::{Display, Formatter, write}; +use std::fmt::{Display, Formatter}; use std::num::ParseIntError; use std::str::FromStr; diff --git a/src/data/entities.rs b/src/data/entities.rs index 816bd02..4412064 100644 --- a/src/data/entities.rs +++ b/src/data/entities.rs @@ -1,15 +1,16 @@ -use crate::app::AppState; -use crate::core::srs::{FADE_PER_DAY, MAX_SCORE, OrderMode, WordOpenMode}; +use crate::core::srs::{FADE_PER_DAY, MAX_SCORE, WordOpenMode}; use crate::data::{INVALID_ID, Id}; -use chrono::{DateTime, NaiveDate, Utc}; +use chrono::{DateTime, Local, NaiveDate, Utc}; use fsrs::MemoryState; use litemap::LiteMap; use log::info; -use rayon::prelude::*; -use rhai::{Engine, Scope}; +use rhai::Engine; use std::cmp::Ordering; +use std::hash::{Hash, Hasher}; use std::marker::PhantomData; +use std::sync::Arc; use std::time::Instant; +use crate::data_provider::history::{HistoryRepository, HistoryRepositoryInner}; #[derive(Clone, Debug)] pub struct WordData { @@ -61,12 +62,49 @@ impl WordData { } #[derive(Clone)] -pub struct HistoryItem { +pub struct HistoryNote { + pub id: Id, pub timestamp: DateTime, + pub deck_id: Id, pub word_id: Id, - pub mode: WordOpenMode, + pub answer: WordOpenMode, pub before: u8, pub after: u8, + pub fsrs: bool +} + +impl EntityData for HistoryNote { + fn id(&self) -> Id { + self.id + } + + fn set_id(&mut self, id: Id) { + self.id = id; + } +} + +impl HistoryRepositoryInner { + pub fn activity_of_deck(&self, id: Id) -> Vec<(NaiveDate, u32)> { + let mut query = self.query(Arc::new(move |note| note.deck_id == id)); + let values = query.iter(&self).collect::>(); + let mut counts = vec![]; + if !values.is_empty() { + let mut date = values[0].timestamp.with_timezone(&Local).date_naive(); + let mut count = 0; + for note in values { + let current_date = note.timestamp.with_timezone(&Local).date_naive(); + if date == current_date{ + count += 1; + } + else { + counts.push((date, count)); + date = current_date; + count = 0; + } + } + } + counts + } } #[derive(Clone)] @@ -76,9 +114,18 @@ pub struct DeckSettings { pub forward: String, pub backward: String, pub filter: String, - pub count: Option, - pub open_mode: OrderMode, } + +impl EntityData for DeckSettings { + fn id(&self) -> Id { + self.id + } + + fn set_id(&mut self, id: Id) { + self.id = id; + } +} + impl DeckSettings { pub(crate) fn with_name(name: String) -> DeckSettings { DeckSettings { @@ -87,8 +134,6 @@ impl DeckSettings { forward: "".to_string(), backward: "".to_string(), filter: "true".to_string(), - count: None, - open_mode: OrderMode::Default, } } pub fn check_filter(&self) -> bool { @@ -98,57 +143,6 @@ impl DeckSettings { info!("Compile time is {:?}", time.elapsed()); ast.is_ok() } - pub fn get_word_list(&self, state: &AppState) -> Vec { - let time = Instant::now(); - let mut list = vec![]; - let engine = Engine::new(); - - let ast = engine.compile(&self.filter); - if let Ok(ast) = ast { - let groups = &state.word_groups; - - list = state - .dictionary - .par_iter() - .enumerate() - .filter(|(_, word)| { - let mut more = rhai::Map::new(); - for iced in &word.additional { - more.insert(iced.0.clone().into(), iced.1.clone().into()); - } - let mut scope = Scope::new(); - scope - .push_constant("id", word.id.0 as i64) - .push_constant("key", word.key.clone()) - .push_constant("value", word.value.clone()) - .push_constant("tags", word.tags.clone()) - .push_constant("more", more) - .push_constant( - "group", - groups - .iter() - .find(|g| g.id == word.group_id) - .cloned() - .expect("No group id") - .name, - ); - - let result = engine.eval_ast_with_scope::(&mut scope, &ast); - if let Ok(result) = result { - return result; - } - false - }) - .map(|(index, _)| index) - .collect(); - - info!("Collecting available words is {:?}", time.elapsed()); - - list - } else { - list - } - } pub fn require_speech(&self) -> bool { self.forward == "speech" || self.backward == "speech" } @@ -158,7 +152,7 @@ impl DeckSettings { pub struct CardState { pub id: Id, pub word_id: Id, - pub set_id: Id, + pub deck_id: Id, pub last_open: DateTime, pub score: u8, pub fsrs_opened: NaiveDate, @@ -203,7 +197,11 @@ impl CardState { } } -#[derive(Copy)] +pub struct CustomField { + pub id: Id, + pub name: String, +} + pub struct EntityHandle { pub(crate) id: Id, index: u32, @@ -219,6 +217,7 @@ impl Clone for EntityHandle { } } } +impl Copy for EntityHandle {} impl EntityHandle { pub fn new(id: Id, index: usize, real: bool) -> Self { Self { @@ -227,6 +226,11 @@ impl EntityHandle { _phantom: Default::default(), } } + + pub fn from_id(id: Id) -> Self { + Self::new(id, id.0 as usize, true) + } + pub fn index(&self) -> usize { (self.index << 1 >> 1) as usize } @@ -270,6 +274,13 @@ impl Ord for EntityHandle { } } +impl Hash for EntityHandle { + fn hash(&self, state: &mut H) { + self.id.hash(state); + self.real().hash(state); + } +} + pub trait EntityData { fn id(&self) -> Id; fn set_id(&mut self, id: Id); diff --git a/src/data/repositories.rs b/src/data/repositories.rs index 971c5df..3dd5990 100644 --- a/src/data/repositories.rs +++ b/src/data/repositories.rs @@ -1,5 +1,6 @@ use crate::data::Id; -use crate::data::entities::{EntityData, EntityHandle}; +use crate::data::entities::{CardState, EntityData, EntityHandle}; +use crate::data_provider::sqlite::SqliteConnection; use crate::helpers::into_result; use crate::notification::{MaybeError, NotificationData}; use parking_lot::{Mutex, RwLock}; @@ -8,8 +9,10 @@ use rayon::iter::IntoParallelRefIterator; use rayon::iter::ParallelIterator; use rusqlite::Connection; use std::marker::PhantomData; -use std::ops::Deref; +use std::ops::{Deref, Index, IndexMut}; use std::sync::Arc; +use std::time::Instant; +use log::debug; pub trait Repository where @@ -18,14 +21,23 @@ where fn get(&self, handle: &mut EntityHandle) -> Option; fn get_ref(&self, handle: &mut EntityHandle) -> Option<&T>; fn get_mut(&mut self, handle: &mut EntityHandle) -> Option<&mut T>; + fn get_static(&self, handle: EntityHandle) -> Option; + fn get_ref_static(&self, handle: EntityHandle) -> Option<&T>; + fn get_mut_static(&mut self, handle: EntityHandle) -> Option<&mut T>; + fn set(&mut self, handle: &mut EntityHandle, value: T); + fn set_static(&mut self, handle: EntityHandle, value: T); fn handle_by_id(&self, id: Id) -> EntityHandle; fn create(&mut self, new: T) -> EntityHandle; fn create_now(&mut self, new: T) -> Result, NotificationData>; fn save(&mut self, entity: &mut EntityHandle) -> MaybeError; fn delete(&mut self, handle: EntityHandle) -> MaybeError; + fn find(&self, predicate: F) -> Option> where F : Fn(&T) -> bool; fn init(&mut self) -> MaybeError; fn start(&mut self) -> MaybeError; fn reload(&mut self) -> MaybeError; + fn shifts(&self) -> usize; + fn iter<'a>(&'a self) -> impl Iterator where T: 'a; + fn iter_mut<'a>(&'a mut self) -> impl Iterator where T: 'a; } struct QueryManger @@ -91,32 +103,52 @@ where } } } + +#[derive(Clone)] pub struct Query where R: Repository, D: DbLayer, { cache: Vec>, - rule: Box bool + Send + Sync>, + rule: Arc bool + Send + Sync>, parallel: bool, _marker: PhantomData<(R, D)>, - shift_stamp: u16, + shift_stamp: usize, } -impl, D: DbLayer> Query { - pub fn iter<'a>(&mut self, data_repository_inner: &'a R) -> impl Iterator +impl, D: DbLayer> Query { + pub fn iter<'a>(&mut self, data_repository_inner: &'a DataRepositoryInner) -> impl Iterator where T: 'a, { + if self.shift_stamp != data_repository_inner.shifts() { + self.load(data_repository_inner); + } self.cache .iter_mut() .filter_map(move |e| data_repository_inner.get_ref(e)) } - pub fn for_each_mut(&mut self, repo: &mut R, mut f: F) + pub fn iter_with_handles<'a>(&mut self, data_repository_inner: &'a DataRepositoryInner) -> impl Iterator)> + where + T: 'a, + { + if self.shift_stamp != data_repository_inner.shifts() { + self.load(data_repository_inner); + } + self.cache + .iter_mut() + .filter_map(move |e| Some((data_repository_inner.get_ref(e)?, e.clone()))) + } + + pub fn for_each_mut(&mut self, repo: &mut DataRepositoryInner, mut f: F) where F: FnMut(&mut T), { + if self.shift_stamp != repo.shifts() { + self.load(repo); + } for handle in &mut self.cache { if let Some(entity) = repo.get_mut(handle) { f(entity); @@ -128,13 +160,14 @@ impl, D: DbLayer> Query { where T: Send + Sync, { + let time = Instant::now(); self.cache.clear(); if self.parallel { self.cache = data_repository .cache .par_iter() .enumerate() - .filter(|(_, entity)| (self.rule)(entity)) + .filter(|(_, entity)| self.rule.as_ref()(entity)) .map(|(i, entity)| EntityHandle::new(entity.id(), i, true)) .collect(); } else { @@ -156,6 +189,30 @@ impl, D: DbLayer> Query { .map(|(i, entity)| EntityHandle::new(entity.id(), i, false)) .collect::>(), ); + + debug!("Query for {} elements (filtered {}) takes {:?}", data_repository.len(), self.cache.len(), time.elapsed()); + } + + pub fn handles(&self) -> impl Iterator> { + self.cache.iter().cloned() + } + + pub fn len(&self) -> usize { + self.cache.len() + } +} + +impl, D: DbLayer> Index for Query { + type Output = EntityHandle; + + fn index(&self, index: usize) -> &Self::Output { + &self.cache[index] + } +} + +impl, D: DbLayer> IndexMut for Query { + fn index_mut(&mut self, index: usize) -> &mut Self::Output { + &mut self.cache[index] } } @@ -164,6 +221,7 @@ where D: BatchedDbLayer, T: EntityData, { + fn create_now_many(&mut self, entities: &mut [T]) -> Result>, NotificationData>; fn save_many(&mut self, handles: &mut [EntityHandle]) -> MaybeError; fn delete_many(&mut self, handles: &mut [EntityHandle]) -> MaybeError; } @@ -184,22 +242,40 @@ where } #[derive(Clone)] -pub struct DataRepository> { +pub struct DataRepository> { inner: Arc>>, } -pub struct DataRepositoryInner> { +pub struct DataRepositoryInner> { cache: Vec, temporal: Vec, next_temporal: u32, - connection: Arc>, + connection: Arc>, data_layer: D, query_manger: QueryManger, version: u16, - shifts: u16, + shifts: usize, } -impl> Deref for DataRepository { +impl + Default> DataRepository { + pub fn new(connection: Arc>) -> DataRepository { + Self{ + inner: Arc::new(RwLock::new(DataRepositoryInner{ + cache: vec![], + temporal: vec![], + next_temporal: 0, + connection, + data_layer: Default::default(), + query_manger: QueryManger { queries: vec![] }, + version: 0, + shifts: 0, + })), + } + } +} + + +impl> Deref for DataRepository { type Target = Arc>>; fn deref(&self) -> &Self::Target { @@ -207,72 +283,74 @@ impl> Deref for DataRepository< } } -impl> DataRepositoryInner { +impl> DataRepositoryInner { pub fn query_par( &self, - rule: Box bool + Send + Sync>, + rule: Arc bool + Send + Sync>, ) -> Query, D> where T: Send + Sync, { - let mut cache: Vec> = self - .cache - .par_iter() - .enumerate() - .filter(|(_, entity)| rule(entity)) - .map(|(i, entity)| EntityHandle::new(entity.id(), i, true)) - .collect(); - - cache.extend( - self.temporal - .iter() - .enumerate() - .filter(|(_, entity)| rule(entity)) - .map(|(i, entity)| EntityHandle::new(entity.id(), i, false)) - .collect::>(), - ); - - Query { - cache, + let mut query = Query { + cache: vec![], rule, parallel: true, _marker: PhantomData, shift_stamp: self.shifts, - } + }; + + query.load(self); + + query } pub fn query( &self, - rule: Box bool + Send + Sync>, + rule: Arc bool + Send + Sync>, ) -> Query, D> where T: Send + Sync, { - let mut cache: Vec> = self - .cache - .iter() - .enumerate() - .filter(|(_, entity)| rule(entity)) - .map(|(i, entity)| EntityHandle::new(entity.id(), i, true)) - .collect(); - - cache.extend( - self.temporal - .iter() - .enumerate() - .filter(|(_, entity)| rule(entity)) - .map(|(i, entity)| EntityHandle::new(entity.id(), i, false)) - .collect::>(), - ); - - Query { - cache, + let mut query = Query { + cache: vec![], rule, parallel: false, _marker: PhantomData, shift_stamp: self.shifts, + }; + + query.load(self); + + query + } + + pub fn empty_query(&self) -> Query, D> { + Query{ + cache: vec![], + rule: Arc::new(|_| false), + parallel: false, + _marker: Default::default(), + shift_stamp: 0, } } + + pub fn empty_query_par(&self) -> Query, D> { + Query{ + cache: vec![], + rule: Arc::new(|_| false), + parallel: true, + _marker: Default::default(), + shift_stamp: 0, + } + } + + pub fn full_query(&self) -> Query, D> { + self.query(Arc::new(|_| true)) + } + + pub fn len(&self) -> usize { + self.cache.len() + self.temporal.len() + } } impl> Repository for DataRepositoryInner { @@ -306,6 +384,32 @@ impl> Repository for DataRepositoryIn Some(&mut slice[index]) } + fn get_static(&self, mut handle: EntityHandle) -> Option { + self.get(&mut handle) + } + + fn get_ref_static(&self, mut handle: EntityHandle) -> Option<&T> { + self.get_ref(&mut handle) + } + + fn get_mut_static(&mut self, mut handle: EntityHandle) -> Option<&mut T> { + self.get_mut(&mut handle) + } + + fn set(&mut self, handle: &mut EntityHandle, value: T) { + let slice = if handle.real() { + self.cache.as_mut_slice() + } else { + self.temporal.as_mut_slice() + }; + let index = fast_position_of(slice, handle).unwrap(); + slice[index] = value; + } + + fn set_static(&mut self, mut handle: EntityHandle, value: T) { + self.set(&mut handle, value) + } + fn handle_by_id(&self, id: Id) -> EntityHandle { EntityHandle::new(id, id.0 as usize, true) } @@ -321,7 +425,7 @@ impl> Repository for DataRepositoryIn fn create_now(&mut self, mut new: T) -> Result, NotificationData> { self.data_layer - .add_entity(&mut new, &self.connection.read())?; + .add_entity(&mut new, &self.connection.lock())?; let index = self.cache.len(); let handle = EntityHandle::new(new.id(), index, true); self.cache.push(new); @@ -333,17 +437,18 @@ impl> Repository for DataRepositoryIn if entity.real() { let data = self.get_ref(entity).unwrap(); self.data_layer - .write_entity(data, &self.connection.read())?; + .write_entity(data, &self.connection.lock())?; } else { let index = fast_position_of(self.temporal.as_slice(), entity).unwrap(); - let mut data = self.temporal.remove(index); + let data = &mut self.temporal[index]; self.data_layer - .add_entity(&mut data, &self.connection.read())?; + .add_entity(data, &self.connection.lock())?; entity.set_real(true); entity.id = data.id(); entity.set_index(self.cache.len()); self.query_manger.handle_save(original_state, entity, &data); + let data = self.temporal.remove(index); self.cache.push(data); self.shifts += 1; } @@ -354,7 +459,7 @@ impl> Repository for DataRepositoryIn fn delete(&mut self, mut handle: EntityHandle) -> MaybeError { let vec = if handle.real() { self.data_layer - .delete_entity(handle.id, &self.connection.read())?; + .delete_entity(handle.id, &self.connection.lock())?; &mut self.cache } else { &mut self.temporal @@ -370,9 +475,25 @@ impl> Repository for DataRepositoryIn Ok(()) } + fn find(&self, predicate: F) -> Option> + where + F: Fn(&T) -> bool + { + if let Some(index) = self.cache.iter().position(|entity| predicate(entity)){ + let value = &self.cache[index]; + return Some(EntityHandle::new(value.id(), index, true)) + } + + if let Some(index) = self.temporal.iter().position(|entity| predicate(entity)){ + let value = &self.temporal[index]; + return Some(EntityHandle::new(value.id(), index, false)) + } + None + } + fn init(&mut self) -> MaybeError { self.data_layer - .load_all(&mut self.cache, &self.connection.read())?; + .load_all(&mut self.cache, &self.connection.lock())?; self.version += 1; Ok(()) } @@ -392,10 +513,36 @@ impl> Repository for DataRepositoryIn self.init()?; Ok(()) } + + fn shifts(&self) -> usize { + self.shifts + } + + fn iter<'a>(&'a self) -> impl Iterator + where + T: 'a + { + self.cache.iter().chain(self.temporal.iter()) + } + + fn iter_mut<'a>(&'a mut self) -> impl Iterator + where + T: 'a + { + self.cache.iter_mut().chain(self.temporal.iter_mut()) + } } impl + DbLayer> BatchedRepository for DataRepositoryInner { + fn create_now_many(&mut self, entities: &mut [T]) -> Result>, NotificationData> { + self.data_layer.add_many_entities(entities, &mut self.connection.lock())?; + let len = self.cache.len(); + let handles = entities.iter().enumerate().map(|(index, entity)| {EntityHandle::new(entity.id(), index + len, true)}).collect(); + self.cache.extend(entities.to_vec()); + Ok(handles) + } + fn save_many(&mut self, handles: &mut [EntityHandle]) -> MaybeError { let mut temp = Vec::with_capacity(handles.len() / 2); let mut cached = Vec::with_capacity(handles.len() / 2); @@ -425,7 +572,7 @@ impl + DbLayer> BatchedRepository }); self.data_layer - .add_many_entities(&mut temp_values, &mut self.connection.write())?; + .add_many_entities(&mut temp_values, &mut self.connection.lock())?; let start = self.cache.len(); @@ -443,7 +590,7 @@ impl + DbLayer> BatchedRepository let h = handle; let idx = fast_position_of(self.cache.as_slice(), h).unwrap(); self.data_layer - .write_entity(&self.cache[idx], &self.connection.read())?; + .write_entity(&self.cache[idx], &self.connection.lock())?; } self.shifts += 1; @@ -467,7 +614,7 @@ impl + DbLayer> BatchedRepository !remove }); self.data_layer - .delete_many_entities(cached.as_slice(), &mut self.connection.write())?; + .delete_many_entities(cached.as_slice(), &mut self.connection.lock())?; let len = cached.len(); cell = 0; self.cache.retain(|x| { diff --git a/src/data_provider/card_states.rs b/src/data_provider/card_states.rs index effeef5..266e0ab 100644 --- a/src/data_provider/card_states.rs +++ b/src/data_provider/card_states.rs @@ -1,5 +1,7 @@ +use std::sync::Arc; use crate::data::entities::{CardState, DeckSettings}; -use crate::data_provider::TableAccessQuerySet; +use crate::data::repositories::{DataRepository, DataRepositoryInner, Query}; +use crate::data_provider::{SqliteDbLayer, TableAccessQuerySet}; use crate::notification::NotificationData::{DbLoadError, DbSaveError}; use crate::notification::{MaybeError, NotificationData}; use chrono::NaiveDate; @@ -7,19 +9,26 @@ use fsrs::MemoryState; use log::info; use rusqlite::{Connection, Error, Params, Row, params}; use std::time::Instant; +use crate::data::Id; +pub type CardStateRepository = + DataRepository; +pub type CardStateRepositoryInner = + DataRepositoryInner; +pub type CardStateDbLayer = SqliteDbLayer; +#[derive(Clone)] pub struct CardStatesQuerySet; impl TableAccessQuerySet for CardStatesQuerySet { fn load() -> &'static str { - "SELECT id, word_id, set_id, score, last_opened, fsrs_opened, fsrs_memory, fsrs_open_day FROM states" + "SELECT id, word_id, deck_id, score, last_opened, fsrs_opened, fsrs_memory, fsrs_open_day FROM states" } fn save() -> &'static str { - "UPDATE states SET score = ?1, last_opened = ?2 WHERE id = ?3" + "UPDATE states SET score = ?1, last_opened = ?2, fsrs_opened = ?3, fsrs_memory = ?4, fsrs_open_day = ?5 WHERE id = ?6" } fn add() -> &'static str { - "INSERT INTO states (word_id, set_id, score, last_opened, fsrs_opened, fsrs_memory, fsrs_open_day) VALUES (?1, ?2, ?3, ?4)" + "INSERT INTO states (word_id, deck_id, score, last_opened, fsrs_opened, fsrs_memory, fsrs_open_day) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)" } fn name() -> &'static str { @@ -30,7 +39,7 @@ impl TableAccessQuerySet for CardStatesQuerySet { Ok(CardState { id: row.get(0)?, word_id: row.get(1)?, - set_id: row.get(2)?, + deck_id: row.get(2)?, score: row.get(3)?, fsrs_opened: NaiveDate::from_epoch_days(row.get::(5)?) .expect("Incorrect date encoding"), @@ -44,7 +53,7 @@ impl TableAccessQuerySet for CardStatesQuerySet { fn insert_params(entity: &CardState) -> impl Params { ( entity.word_id, - entity.set_id, + entity.deck_id, entity.score, entity.last_open.timestamp(), entity.fsrs_opened.to_epoch_days(), @@ -54,7 +63,14 @@ impl TableAccessQuerySet for CardStatesQuerySet { } fn update_params(entity: &CardState) -> impl Params { - (entity.score, entity.last_open.timestamp(), entity.id) + ( + entity.score, + entity.last_open.timestamp(), + entity.fsrs_opened.to_epoch_days(), + memory_to_i64(&entity.fsrs_memory), + entity.fsrs_open_day.to_epoch_days(), + entity.id, + ) } } pub fn load_stats_of_deck( @@ -69,7 +85,7 @@ pub fn load_stats_of_deck( Ok(CardState { id: row.get(0)?, word_id: row.get(1)?, - set_id: set.id, + deck_id: set.id, score: row.get(2)?, fsrs_opened: NaiveDate::from_epoch_days(row.get::(4)?) .expect("Incorrect date encoding"), @@ -105,74 +121,8 @@ fn memory_to_i64(value: &Option) -> i64 { value.stability.to_bits() as i64 | ((value.difficulty.to_bits() as i64) << 32) } -pub fn add_stat_list(stats: &mut [CardState], connection: &mut Connection) -> MaybeError { - let time = Instant::now(); - let tx = connection - .transaction() - .expect("Failed to create transaction"); - let count = stats.len(); - - { - let mut stmt = tx - .prepare( - "INSERT INTO states (word_id, set_id, score, last_opened, fsrs_opened, fsrs_memory, fsrs_open_day) VALUES (?1, ?2, ?3, ?4)", - ) - .expect("Failed to prepare state query"); - - for stat in stats.iter() { - stmt.execute(params![ - stat.word_id, - stat.set_id, - stat.score, - stat.last_open.timestamp(), - stat.fsrs_opened.to_epoch_days(), - memory_to_i64(&stat.fsrs_memory), - stat.fsrs_open_day.to_epoch_days() - ]) - .map_err(|_| DbSaveError)?; - } +impl CardStateRepositoryInner { + pub fn deck_states(&self, deck_id: Id) -> Query { + self.query(Arc::new(move |state| {state.deck_id == deck_id})) } - - let last_index: u32 = tx.last_insert_rowid() as u32; - tx.commit().expect("Failed to commit"); - - let start_index = last_index - (count as u32) + 1; - - for (index, id) in (start_index..=last_index).enumerate() { - stats[index].id = id.into(); - } - - info!("Added {} cards for {:?}", stats.len(), time.elapsed()); - Ok(()) -} - -pub fn update_stat_score(stat: &CardState, connection: &Connection) -> MaybeError { - let time = Instant::now(); - - connection - .execute( - "UPDATE states SET score = ?1, last_opened = ?2 WHERE id = ?3", - (&stat.score, &stat.last_open.timestamp(), &stat.id), - ) - .map_err(|_| DbSaveError)?; - info!("Updated stat: {}", time.elapsed().as_millis()); - Ok(()) -} - -pub fn delete_stats(stat: &[CardState], connection: &Connection) -> MaybeError { - if stat.is_empty() { - return Ok(()); - } - let ids = stat - .iter() - .map(|s| s.id.into()) - .collect::>() - .join(", "); - connection - .execute( - format!("DELETE FROM states WHERE id in ({ids})").as_str(), - (), - ) - .map_err(|_| DbSaveError)?; - Ok(()) -} +} \ No newline at end of file diff --git a/src/data_provider/custom_fields.rs b/src/data_provider/custom_fields.rs index 2cb9e0a..6f09efa 100644 --- a/src/data_provider/custom_fields.rs +++ b/src/data_provider/custom_fields.rs @@ -1,7 +1,10 @@ -use crate::data::entities::WordGroup; -use crate::data_provider::TableAccessQuerySet; +use crate::data::entities::{CustomField, WordGroup}; +use crate::data::repositories::DataRepository; +use crate::data_provider::{SqliteDbLayer, TableAccessQuerySet}; use rusqlite::{Error, Params, Row}; +pub type CustomFieldRepository = DataRepository>; + pub struct CustomFieldsQuerySet; impl TableAccessQuerySet for CustomFieldsQuerySet { fn load() -> &'static str { diff --git a/src/data_provider/decks.rs b/src/data_provider/decks.rs index 11526a2..d647467 100644 --- a/src/data_provider/decks.rs +++ b/src/data_provider/decks.rs @@ -1,11 +1,11 @@ -use crate::core::srs::OrderMode; -use crate::data::entities::{CardState, DeckSettings}; -use crate::data_provider::TableAccessQuerySet; -use crate::notification::NotificationData::{DbLoadError, DbSaveError}; -use crate::notification::{MaybeError, NotificationData}; -use chrono::NaiveDate; -use rusqlite::{Connection, Error, Params, Row}; +use crate::data::entities::DeckSettings; +use crate::data::repositories::DataRepository; +use crate::data_provider::{SqliteDbLayer, TableAccessQuerySet}; +use rusqlite::{Error, Params, Row}; +pub type DeckRepository = DataRepository>; + +#[derive(Clone)] pub struct DeckQuerySet; impl TableAccessQuerySet for DeckQuerySet { fn load() -> &'static str { @@ -17,7 +17,7 @@ impl TableAccessQuerySet for DeckQuerySet { } fn add() -> &'static str { - "INSERT INTO states (word_id, set_id, score, last_opened, fsrs_opened, fsrs_memory, fsrs_open_day) VALUES (?1, ?2, ?3, ?4)" + "INSERT INTO decks (name, forward, backward, filter) VALUES (?1, ?2, ?3, ?4)" } fn name() -> &'static str { @@ -31,8 +31,6 @@ impl TableAccessQuerySet for DeckQuerySet { forward: row.get(2)?, backward: row.get(3)?, filter: row.get(4)?, - count: None, - open_mode: OrderMode::Default, }) } @@ -54,75 +52,4 @@ impl TableAccessQuerySet for DeckQuerySet { entity.id, ) } -} - -pub fn load_sets(connection: &Connection) -> Result, NotificationData> { - let mut stmt = connection - .prepare("SELECT id, name, forward, backward, filter FROM decks") - .expect("Failed to prepare state query"); - let iter = stmt - .query_map([], |row| { - Ok(DeckSettings { - id: row.get(0)?, - name: row.get(1)?, - forward: row.get(2)?, - backward: row.get(3)?, - filter: row.get(4)?, - count: None, - open_mode: OrderMode::Default, - }) - }) - .map_err(|_| DbLoadError)?; - - let mut buffer = Vec::with_capacity(16); - iter.filter_map(|res| res.ok()).for_each(|i| buffer.push(i)); - Ok(buffer) -} - -pub fn add_set(set: &mut DeckSettings, connection: &Connection) -> MaybeError { - let index : u32 = connection - .query_row( - "INSERT INTO decks (name, forward, backward, filter) VALUES (?1, ?2, ?3, ?4) RETURNING id", - ( - &set.name, - &set.forward, - &set.backward, - &set.filter, - ), - |row| row.get(0) - ) - .map_err(|_| DbSaveError)?; - - set.id = index.into(); - Ok(()) -} - -pub fn update_deck(set: &mut DeckSettings, connection: &Connection) -> MaybeError { - if !set.id.is_valid() { - add_set(set, connection)?; - } else { - connection - .execute( - "UPDATE decks SET name = ?1, forward = ?2, backward = ?3, filter = ?4 WHERE id = ?5", - ( - &set.name, - &set.forward, - &set.backward, - &set.filter, - &set.id - ), - ) - .map_err(|_| DbSaveError)?; - } - Ok(()) -} - -pub fn delete_set(set: &DeckSettings, connection: &Connection) -> MaybeError { - if !set.id.is_valid() { - return Err(DbSaveError); - } - connection - .execute("DELETE FROM decks WHERE id = ?1", (&set.id,)) - .map_err(|_| DbSaveError)?; - Ok(()) -} +} \ No newline at end of file diff --git a/src/data_provider/history.rs b/src/data_provider/history.rs index c3cd324..17007b1 100644 --- a/src/data_provider/history.rs +++ b/src/data_provider/history.rs @@ -1,97 +1,95 @@ use crate::core::srs::WordOpenMode; -use crate::data::Id; -use crate::data::entities::HistoryItem; -use crate::fs::*; -use crate::notification::NotificationData::PermissionDenied; -use crate::notification::{MaybeError, NotificationData}; -use chrono::{DateTime, NaiveDate}; -use rusqlite::Connection; -use std::fs::{File, OpenOptions}; -use std::io::Write; -use std::io::{BufRead, BufReader}; -use std::path::PathBuf; +use crate::data::entities::HistoryNote; +use crate::data::repositories::{DataRepository, DataRepositoryInner}; +use crate::data_provider::{SqliteDbLayer, TableAccessQuerySet}; +use rusqlite::types::{FromSql, FromSqlResult, ToSqlOutput, ValueRef}; +use rusqlite::{Error, Params, Row, ToSql}; -pub fn get_open_count_of_set( - id: Id, - connection: &Connection, -) -> Result, NotificationData> { - todo!() -} +pub type HistoryRepository = DataRepository; +pub type HistoryRepositoryInner = DataRepositoryInner; +pub type HistoryDataLayer = SqliteDbLayer; -pub fn get_history_of_set(id: Id) -> Result, NotificationData> { - let app_dir = history_dir()?; - let head = app_dir.clone().join(format!("set_{}_history.csv", id)); - let mut lines = Vec::new(); - - append_lines_if_exists(head, &mut lines)?; - lines.reverse(); - Ok(parse_history_items(lines)) -} - -fn append_lines_if_exists(path: PathBuf, lines: &mut Vec) -> MaybeError { - if path.exists() { - let reader = BufReader::new(File::open(path).map_err(|_| PermissionDenied)?); - reader - .lines() - .map_while(Result::ok) - .for_each(|i| lines.push(i)); +#[derive(Clone)] +pub struct HistoryQuerySet; +impl TableAccessQuerySet for HistoryQuerySet { + fn load() -> &'static str { + "SELECT rowid, deck_id, word_id, timestamp, answer, before, after, fsrs FROM history" + } + + fn save() -> &'static str { + todo!("History is readonly") + } + + fn add() -> &'static str { + "INSERT INTO history (deck_id, word_id, timestamp, answer, before, after, fsrs) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)" + } + + fn name() -> &'static str { + "history" + } + + fn id_field() -> &'static str { + "rowid" + } + fn from_sql(row: &Row) -> Result { + Ok(HistoryNote { + id: row.get(0)?, + timestamp: row.get(3)?, + deck_id: row.get(1)?, + word_id: row.get(2)?, + answer: row.get(4)?, + before: row.get(5)?, + after: row.get(6)?, + fsrs: row.get(7)?, + }) + } + + fn insert_params(entity: &HistoryNote) -> impl Params { + ( + entity.deck_id, + entity.word_id, + entity.timestamp, + entity.answer, + entity.before, + entity.after, + entity.fsrs, + ) + } + + fn update_params(entity: &HistoryNote) -> impl Params { + ( + entity.deck_id, + entity.word_id, + entity.timestamp, + entity.answer, + entity.before, + entity.after, + entity.fsrs, + ) } - Ok(()) } -fn parse_history_items(strings: Vec) -> Vec { - let mut items = Vec::with_capacity(strings.len()); - for string in strings { - if let [time, word, mode, before, after] = string.split(';').collect::>()[..] { - let id = word.parse::(); - if id.is_err() { - continue; +impl FromSql for WordOpenMode { + fn column_result(value: ValueRef<'_>) -> FromSqlResult { + if let ValueRef::Integer(s) = value { + return match s { + 1 => Ok(WordOpenMode::Hard), + 2 => Ok(WordOpenMode::Ok), + 3 => Ok(WordOpenMode::Easy), + _ => Ok(WordOpenMode::None), } - let id = id.unwrap_or_default(); - if !id.is_valid() { - continue; - } - items.push(HistoryItem { - timestamp: DateTime::from_timestamp(time.parse().unwrap_or_default(), 0) - .unwrap_or_default(), - word_id: id, - mode: match mode.parse::().unwrap_or_default() { - 2 => WordOpenMode::Hard, - 3 => WordOpenMode::Ok, - 4 => WordOpenMode::Easy, - _ => WordOpenMode::None, - }, - before: before.parse().unwrap_or_default(), - after: after.parse().unwrap_or_default(), - }); } + Ok(WordOpenMode::None) } - - items } -pub fn push_note(set_id: Id, item: HistoryItem) -> MaybeError { - let app_dir = history_dir()?; - let path = app_dir.clone().join(format!("set_{}_history.csv", set_id)); - - let mut file = OpenOptions::new() - .create(true) - .append(true) - .open(path) - .map_err(|_| PermissionDenied)?; - let line_str = format!( - "{};{};{};{};{}", - item.timestamp.timestamp(), - item.word_id, - match item.mode { - WordOpenMode::Easy => 4, - WordOpenMode::Ok => 3, - WordOpenMode::Hard => 2, - WordOpenMode::None => 1, - }, - item.before, - item.after - ); - writeln!(&mut file, "{}", line_str).map_err(|_| PermissionDenied)?; - Ok(()) -} +impl ToSql for WordOpenMode { + fn to_sql(&self) -> rusqlite::Result> { + Ok(ToSqlOutput::from(match self { + WordOpenMode::Easy => 3, + WordOpenMode::Ok => 2, + WordOpenMode::Hard => 1, + WordOpenMode::None => 0 + })) + } +} \ No newline at end of file diff --git a/src/data_provider/import.rs b/src/data_provider/import.rs index 1640fbb..ebdbaaa 100644 --- a/src/data_provider/import.rs +++ b/src/data_provider/import.rs @@ -1,7 +1,7 @@ +use std::collections::BTreeMap; use crate::helpers::into_result; use crate::notification::NotificationData; use crate::notification::NotificationData::IncorrectAnkiFile; -use hashbrown::HashMap; use log::info; use rusqlite::Connection; use serde_json::Value; @@ -15,7 +15,7 @@ pub struct ImportGroup { pub name: String, pub fields: Vec, pub length: u64, - pub mapping: HashMap, + pub mapping: BTreeMap, pub imported: bool, } @@ -45,7 +45,7 @@ pub fn load_groups(connection: &Connection) -> Result>(); for (_, collection) in sets { - let fields_property = collection.get("fields"); + let fields_property = collection.get("flds"); if let Some(fields_property) = fields_property { let fields = into_result(fields_property.as_array(), IncorrectAnkiFile)? .iter() diff --git a/src/data_provider/mod.rs b/src/data_provider/mod.rs index adffc77..31e0e22 100644 --- a/src/data_provider/mod.rs +++ b/src/data_provider/mod.rs @@ -1,9 +1,9 @@ use crate::data::Id; -use crate::data::entities::{CardState, EntityData}; +use crate::data::entities::EntityData; use crate::data::repositories::{BatchedDbLayer, DbLayer}; use crate::notification::MaybeError; -use crate::notification::NotificationData::{DbLoadError, DbSaveError}; -use log::debug; +use crate::notification::NotificationData::DbSaveError; +use log::{debug, error}; use rusqlite::{Connection, Error, Params, Row}; use std::marker::PhantomData; use std::time::Instant; @@ -25,11 +25,15 @@ pub trait TableAccessQuerySet { fn save() -> &'static str; fn add() -> &'static str; fn name() -> &'static str; + fn id_field() -> &'static str { + "id" + } fn from_sql(row: &Row) -> Result; fn insert_params(entity: &T) -> impl Params; fn update_params(entity: &T) -> impl Params; } +#[derive(Clone)] pub struct SqliteDbLayer where Q: TableAccessQuerySet, @@ -37,13 +41,24 @@ where _phantom: PhantomData<(T, Q)>, } +impl> Default for SqliteDbLayer { + fn default() -> Self { + Self { + _phantom: PhantomData, + } + } +} + impl> DbLayer for SqliteDbLayer { fn write_entity(&self, entity: &T, connection: &Connection) -> MaybeError { let time = Instant::now(); connection - .execute(Q::load(), Q::update_params(entity)) - .map_err(|_| DbSaveError)?; + .execute(Q::save(), Q::update_params(entity)) + .map_err(|e| { + error!("{}", e); + DbSaveError + })?; debug!("Updated in {} for {:?}", Q::name(), time.elapsed()); Ok(()) } @@ -52,8 +67,11 @@ impl> DbLayer for SqliteDbLayer> DbLayer for SqliteDbLayer> DbLayer for SqliteDbLayer> BatchedDbLayer for SqliteDbLay format!("DELETE FROM {} WHERE id in ({})", Q::name(), stringed).as_str(), (), ) - .map_err(|_| DbSaveError)?; + .map_err(|e| { + error!("{}", e); + DbSaveError + })?; } debug!( "Deleted {} from {} for {:?}", @@ -136,8 +163,10 @@ impl> BatchedDbLayer for SqliteDbLay { let mut stmt = tx.prepare(Q::add()).expect("Failed to prepare state query"); for entity in entities.iter() { - stmt.execute(Q::insert_params(entity)) - .map_err(|_| DbSaveError)?; + stmt.execute(Q::insert_params(entity)).map_err(|e| { + error!("{}", e); + DbSaveError + })?; } } diff --git a/src/data_provider/sqlite.rs b/src/data_provider/sqlite.rs index 85821a7..7fd9fab 100644 --- a/src/data_provider/sqlite.rs +++ b/src/data_provider/sqlite.rs @@ -1,5 +1,7 @@ use crate::fs::*; use rusqlite::Connection; +use std::ops::{Deref, DerefMut}; +use log::error; pub fn create_db() { let db_file = db_path(); @@ -84,3 +86,50 @@ pub fn default_connection() -> Connection { .expect("Failed to set PRAGMA"); connection } + +pub struct SqliteConnection(Option); +impl Deref for SqliteConnection { + type Target = Connection; + fn deref(&self) -> &Self::Target { + self.0.as_ref().unwrap() + } +} + +impl DerefMut for SqliteConnection { + fn deref_mut(&mut self) -> &mut Self::Target { + self.0.as_mut().unwrap() + } +} + +impl Default for SqliteConnection { + fn default() -> Self { + SqliteConnection(Some(default_connection())) + } +} + +impl SqliteConnection { + pub fn finish(&mut self) { + let connection = self.0.take().unwrap(); + connection + .query_row("PRAGMA wal_checkpoint(TRUNCATE)", [], |_| Ok(())) + .unwrap_or_else(|e| { + error!("Ошибка при checkpoint WAL: {e}"); + }); + connection + .execute("PRAGMA optimize", []) + .unwrap_or_else(|e| { + error!("Ошибка при оптимизации БД: {e}"); + 0 + }); + + self.0 = None; + } + pub fn restart(&mut self) { + self.0 = Some(default_connection()); + } +} + +impl Drop for SqliteConnection { + +fn drop(&mut self) { self.finish(); } +} \ No newline at end of file diff --git a/src/data_provider/word_groups.rs b/src/data_provider/word_groups.rs index 2c3b5d1..450f73c 100644 --- a/src/data_provider/word_groups.rs +++ b/src/data_provider/word_groups.rs @@ -1,7 +1,13 @@ use crate::data::entities::WordGroup; -use crate::data_provider::TableAccessQuerySet; +use crate::data::repositories::{DataRepository, DataRepositoryInner}; +use crate::data_provider::{SqliteDbLayer, TableAccessQuerySet}; use rusqlite::{Error, Params, Row}; +pub type WordGroupRepository = DataRepository; +pub type WordGroupRepositoryInner = DataRepositoryInner; +pub type WordGroupDataLayer = SqliteDbLayer; + +#[derive(Clone)] pub struct WordGroupQuerySet; impl TableAccessQuerySet for WordGroupQuerySet { fn load() -> &'static str { diff --git a/src/data_provider/words.rs b/src/data_provider/words.rs index e32e066..4ea6bdf 100644 --- a/src/data_provider/words.rs +++ b/src/data_provider/words.rs @@ -1,11 +1,14 @@ use crate::data::entities::{WordData, WordGroup}; -use crate::data_provider::TableAccessQuerySet; +use crate::data::repositories::{DataRepository, DataRepositoryInner}; +use crate::data_provider::{SqliteDbLayer, TableAccessQuerySet}; use crate::notification::NotificationData::{DbLoadError, DbSaveError}; use crate::notification::{MaybeError, NotificationData}; -use litemap::LiteMap; use rusqlite::{Connection, Error, Params, Row, params}; -use std::collections::HashMap; +pub type WordRepository = DataRepository; +pub type WordRepositoryInner = DataRepositoryInner; +pub type WordDataLayer = SqliteDbLayer; +#[derive(Clone)] pub struct WordQuerySet; impl TableAccessQuerySet for WordQuerySet { fn load() -> &'static str { @@ -60,7 +63,7 @@ impl TableAccessQuerySet for WordQuerySet { pub fn add_word(word: &mut WordData, connection: &Connection) -> MaybeError { let index : u32 = connection .query_row( - "INSERT INTO words (key, value, tags, more, group_id) VALUES (?1, ?2, ?3, ?4, ?5) RETURNING id", + "INSERT INTO words (key, value, tags, more, group_id) VALUES (?1, ?2, ?3, ?4, ?5)", ( &word.key, &word.value, @@ -190,7 +193,7 @@ pub fn load_word_groups(connection: &Connection) -> Result, Notif pub fn add_group(group: &mut WordGroup, connection: &Connection) -> MaybeError { let index: u32 = connection .query_row( - "INSERT INTO word_groups (name) VALUES (?1) RETURNING id", + "INSERT INTO word_groups (name) VALUES (?1)", (&group.name,), |row| row.get(0), ) diff --git a/src/navigation.rs b/src/navigation.rs index ecd87f8..e205793 100644 --- a/src/navigation.rs +++ b/src/navigation.rs @@ -1,15 +1,11 @@ -use crate::app::{AppState, fill_state}; -use crate::data::Id; -use crate::data_provider::history::get_history_of_set; -use crate::data_provider::sqlite::{create_db, default_connection}; +use crate::app::{fill_state, AppState}; +use crate::data_provider::sqlite::{create_db}; use crate::data_provider::web_api::{ get_local_version, get_web_version, load_data, set_local_version, }; use crate::fs::*; use crate::navigation::Page::*; -use crate::navigation::RootMessage::{ - DataLoaded, Keyboard, Notification, PushNotification, UpdateData, -}; +use crate::navigation::RootMessage::{Keyboard, Notification, PushNotification, UpdateData}; use crate::notification::NotificationData::PermissionDenied; use crate::notification::{ NotificationData, NotificationManager, NotificationMessage, NotificationRequest, @@ -31,23 +27,17 @@ use crate::state_navigate_handle; use crate::state_update; use crate::view_navigation; use crate::{message_navigation, push_error, push_error_task}; -use chrono::NaiveDate; -use hashbrown::HashMap; use iced::keyboard::Event; use iced::widget::stack; use iced::{Element, Task}; use iced_core::Length::Fill; use log::{debug, info}; -use parking_lot::Mutex; -use rusqlite::Connection; -use std::sync::Arc; use std::time::Instant; impl Default for ScreenState { fn default() -> Self { let mut state = AppState::new(); fill_state(&mut state); - let state = Arc::new(Mutex::new(state)); ScreenState { stack: vec![Selector(SelectorState::new(state.clone()))], app_state: state.clone(), @@ -72,7 +62,6 @@ pub enum RootMessage { RepetitionSettings(RepetitionSettingsMessage), Import(ImportMessage), Keyboard(Event), - DataLoaded(HashMap>), PushNotification(NotificationRequest), Notification(NotificationMessage), None, @@ -99,7 +88,7 @@ pub enum Page { pub struct ScreenState { stack: Vec, - app_state: Arc>, + app_state: AppState, notification_manager: NotificationManager, } @@ -109,14 +98,11 @@ impl ScreenState { create_db(); info!("Db created"); let state = ScreenState::default(); - let reading_additional_task = Task::perform(Self::load_additional_data(), |data| { - data.unwrap_or_else(|err| push_error!(err)) - }); - let mut final_task = reading_additional_task; - let state_guard = state.app_state.lock(); - if state_guard.sync_data.auto_web_fetch - && let Some(key) = state_guard.sync_data.key.clone() + let mut final_task = Task::none(); + let state_guard = &state.app_state; + if state_guard.sync_data.read().auto_web_fetch + && let Some(key) = state_guard.sync_data.read().key.clone() { final_task = Task::batch([ final_task, @@ -133,43 +119,10 @@ impl ScreenState { ]); } - drop(state_guard); info!("Boot finished"); (state, final_task) } - async fn load_additional_data() -> Result { - let directory = history_dir()?; - - let mut map = HashMap::new(); - for file in directory - .read_dir() - .map_err(|_| PermissionDenied)? - .filter_map(Result::ok) - { - let mut vec = vec![]; - let history_file_name = match file.file_name().into_string() { - Err(_) => continue, - Ok(v) => v, - }; - - if let Ok(id) = history_file_name.split('_').collect::>()[1].parse::() - && let Ok(history) = get_history_of_set(id) - { - let by_date = history.chunk_by(|x, x1| { - x.timestamp.naive_local().date() == x1.timestamp.naive_local().date() - }); - for group in by_date { - vec.push((group[0].timestamp.naive_local().date(), group.len() as u32)); - } - map.insert(id, vec); - } else { - continue; - } - } - Ok(DataLoaded(map)) - } - async fn load_web_backup(string: String) -> Result { let local = get_local_version().await?; let web = get_web_version(string.as_str()).await?; @@ -191,16 +144,11 @@ impl ScreenState { return self.handle_keyboard(e); } - if let DataLoaded(data) = message { - self.app_state.lock().activity = data; - return Task::none(); - } - if let UpdateData = message { info!("Loading data"); - let mut state = self.app_state.lock(); + let state = &mut self.app_state; if cfg!(windows) { - state.connection = Connection::open_in_memory().unwrap(); + state.connection.lock().finish(); } let db_file = db_path(); let temp_db_file = temp_db_path(); @@ -208,8 +156,8 @@ impl ScreenState { return push_error_task!(PermissionDenied); } - state.connection = default_connection(); - fill_state(&mut state); + state.connection = Default::default(); + fill_state(state); return Task::none(); } diff --git a/src/pages/dictionary.rs b/src/pages/dictionary.rs index b800564..3a989be 100644 --- a/src/pages/dictionary.rs +++ b/src/pages/dictionary.rs @@ -1,7 +1,10 @@ use crate::app::AppState; use crate::data::Id; -use crate::data::entities::{WordData, WordGroup}; -use crate::data_provider::words::{delete_group, delete_word, update_group, update_word}; +use crate::data::entities::{EntityHandle, WordData, WordGroup}; +use crate::data::repositories::{Query, Repository}; +use crate::data_provider::word_groups::{WordGroupDataLayer, WordGroupRepositoryInner}; +use crate::data_provider::words::WordDataLayer; +use crate::data_provider::words::WordRepositoryInner; use crate::navigation::Page::{Import, Word}; use crate::navigation::{NavigatedPage, Page, RootMessage}; use crate::pages::dictionary::DictionaryMessage::*; @@ -22,37 +25,37 @@ use iced::widget::*; use iced::{Border, Color, Shadow, Task}; use iced_core::Length::Fill; use log::info; -use parking_lot::Mutex; use rand::random_range; use std::ops::Add; use std::sync::Arc; use std::time::{Duration, Instant}; -#[derive(Clone)] pub struct DictionaryState { - state: Arc>, - include_map: Vec, + state: AppState, + included: HashSet>, tag_map: HashMap, + words_tags: Vec<(EntityHandle, Vec)>, reverse: bool, search: String, no_typing: bool, - selected_group_index: usize, + selected_group: EntityHandle, reverse_list: bool, - auto_save_queue: HashMap>, + auto_save_queue: HashMap, DateTime>, + displayed_words_query: Query, + groups_query: Query, } -#[derive(Debug, Clone)] +#[derive(Clone)] pub enum DictionaryMessage { Back, - SetTags(usize, String), - SetKey(usize, String), - SetValue(usize, String), - SubmitWord(usize), - WordAction(usize), + SetTags(EntityHandle, String), + SetKey(EntityHandle, String), + SetValue(EntityHandle, String), + WordAction(EntityHandle), NewWord, - Include(usize, bool), + Include(EntityHandle, bool), IncludeTag(String, bool), - Test, + GotoTest, ResetTags, SetReverse(bool), Search(String), @@ -60,11 +63,11 @@ pub enum DictionaryMessage { CreateGroup, EditGroup(String), SaveGroup, - SelectGroup(usize), + SelectGroup(EntityHandle), DeleteGroup, ChangeDirection, - TrySave(usize), - ToImport, + TrySave(EntityHandle), + GotoImport, } impl NavigatedPage for DictionaryState { @@ -72,82 +75,71 @@ impl NavigatedPage for DictionaryState { if let Back = message { return Some(Page::PreviousPage); } - if let Test = message - && self.include_map.iter().any(|x| *x) + if let GotoTest = message + && !self.included.is_empty() { let mut words = vec![]; - let dict = &self.state.lock().dictionary; + let dict = &self.state.dictionary.read(); - words = self - .include_map - .iter() - .zip(0..self.include_map.len()) - .filter(|(flag, _)| **flag) - .map(|(_, index)| dict[index].clone()) - .collect(); + let mut query = dict.query(Arc::new(|_| true)); + let handles = query.handles(); + words = handles.filter(|h| self.included.contains(h)).collect(); return Some(Page::DictionaryQuiz(DictionaryQuizState::new( words, self.reverse, self.no_typing, + self.state.clone(), ))); } - if let WordAction(index) = message { - let word: WordData; - { - let state = self.state.lock(); - let dict = &state.dictionary; - word = dict[*index].clone(); - } - if word.id.is_valid() { - return Some(Word(WordState::new(word, *index, self.state.clone()))); - } + if let WordAction(handle) = message + && handle.real() + { + return Some(Word(WordState::new(*handle, self.state.clone()))); } - if let ToImport = message { + if let GotoImport = message { return Some(Import(ImportState::new(self.state.clone()))); } None } fn navigated(&mut self) { - let len = self.state.lock().dictionary.len(); - - self.include_map = vec![false; len]; + self.included = Default::default(); self.tag_map = Default::default(); + let groups = self.state.word_groups.read(); + self.groups_query.load(&groups); + drop(groups); self.update_tags(); } fn update(&mut self, message: DictionaryMessage) -> Task { match message { NewWord => { - let mut state = self.state.lock(); + let mut dictionary = self.state.dictionary.write(); let mut word = WordData::new(); - word.group_id = state.word_groups[self.selected_group_index].id; - - let dict = &mut state.dictionary; - dict.push(word); - self.include_map.push(false); + word.group_id = self.selected_group.id; + dictionary.create(word); + self.displayed_words_query.load(&dictionary); } - SetKey(i, v) => { - { - let dict = &mut self.state.lock().dictionary; - dict[i].key = v; - } - return self.launch_auto_save_offset(i); + SetKey(handle, v) => { + let mut dictionary = self.state.dictionary.write(); + dictionary.get_mut_static(handle).unwrap().key = v; + drop(dictionary); + return self.launch_auto_save_offset(handle); } - SetValue(i, v) => { - { - let dict = &mut self.state.lock().dictionary; - dict[i].value = v - } - return self.launch_auto_save_offset(i); + SetValue(handle, v) => { + let mut dictionary = self.state.dictionary.write(); + dictionary.get_mut_static(handle).unwrap().value = v; + drop(dictionary); + return self.launch_auto_save_offset(handle); } SetTags(i, mut v) => { { - let dict = &mut self.state.lock().dictionary; + let mut dictionary = self.state.dictionary.write(); - let current_tags_value = dict[i].tags.clone(); + let word = dictionary.get_mut_static(i).unwrap(); + let current_tags_value = word.tags.clone(); if v.ends_with(", ") && v.len() < current_tags_value.len() { v = v[..v.len() - 2].to_string() @@ -157,7 +149,7 @@ impl NavigatedPage for DictionaryState { v.remove(index); } - dict[i].tags = v; + word.tags = v; } self.update_tags(); @@ -165,94 +157,89 @@ impl NavigatedPage for DictionaryState { } WordAction(i) => { - let state = &mut self.state.lock(); - let dict = &mut state.dictionary; - let word = dict.remove(i); - self.include_map.remove(i); - self.auto_save_queue.remove(&i); - if let Err(err) = delete_word(&word, &state.connection) { + let mut dictionary = self.state.dictionary.write(); + + if let Err(err) = dictionary.delete(i) { return push_error_task!(err); } + + self.displayed_words_query.load(&dictionary); } - Include(i, b) => self.include_map[i] = b, - IncludeTag(t, v) => { - let index; - { - let state = self.state.lock(); - index = state.word_groups[self.selected_group_index].id; + Include(i, b) => { + if b { + self.included.insert(i); + } else { + self.included.remove(&i); } + } + IncludeTag(t, v) => { self.tag_map.insert(t, v); - self.update_words_include(index) + self.update_words_include(self.selected_group.id) } ResetTags => { self.tag_map.iter_mut().for_each(|(_, v)| *v = false); - self.include_map.iter_mut().for_each(|x| *x = false) + self.included.clear(); } SetReverse(v) => self.reverse = v, Search(s) => { self.search = s; } SetTyping(b) => self.no_typing = b, - SubmitWord(i) => return self.save_word(i), Back => {} - Test => {} + GotoTest => {} CreateGroup => { - let state = &mut self.state.lock(); - - state.word_groups.push(WordGroup { + let mut groups = self.state.word_groups.write(); + groups.create(WordGroup { id: 0.into(), name: format!("Группа слов {}", random_range(100..1000)), }); + self.groups_query.load(&groups); } EditGroup(new) => { - let state = &mut self.state.lock(); - let group = state.word_groups.get_mut(self.selected_group_index); + let mut groups = self.state.word_groups.write(); + + let group = groups.get_mut(&mut self.selected_group); if let Some(group) = group { - group.name = new.clone(); + group.name = new; } } SaveGroup => { - let state = &mut self.state.lock(); - let connection = &state.connection; - let mut group = state.word_groups[self.selected_group_index].clone(); + { + let mut groups = self.state.word_groups.write(); - if let Err(err) = update_group(&mut group, connection) { - return push_error_task!(err); + if let Err(err) = groups.save(&mut self.selected_group) { + return push_error_task!(err); + } + + self.groups_query.load(&groups); } - state.word_groups[self.selected_group_index] = group; + self.setup_words_query(); } SelectGroup(i) => { - self.selected_group_index = i; - let index; - { - let state = self.state.lock(); - index = state.word_groups[i].id; - } - self.update_words_include(index) + self.selected_group = i; + self.setup_words_query(); + self.update_words_include(i.id) } DeleteGroup => { - if self.selected_group_index == 0 { + if self.selected_group.id.0 == 1 { return Task::none(); } { - let state = &mut self.state.lock(); + let delete_group = self.selected_group; + self.selected_group = EntityHandle::from_id(1.into()); + self.setup_words_query(); + let mut groups = self.state.word_groups.write(); - if let Some(group) = state.word_groups.get(self.selected_group_index) { - let remove_group_id = group.id; - let connection = &state.connection; - - if let Err(err) = delete_group(group, connection) { - return push_error_task!(err); - } - - state.word_groups.remove(self.selected_group_index); - state - .dictionary - .retain(|word| word.group_id != remove_group_id); - self.selected_group_index = 0; + if let Err(err) = groups.delete(delete_group) { + return push_error_task!(err); } + + if let Err(err) = self.state.dictionary.write().reload() { + return push_error_task!(err); + } + self.groups_query.load(&groups); } self.update_tags() @@ -260,18 +247,25 @@ impl NavigatedPage for DictionaryState { ChangeDirection => { self.reverse_list = !self.reverse_list; } - TrySave(word_index) => { + TrySave(mut handle) => { let now = Utc::now(); - if !self.auto_save_queue.contains_key(&word_index) { + if !self.auto_save_queue.contains_key(&handle) { return Task::none(); } - if self.auto_save_queue[&word_index] <= now { - self.auto_save_queue.remove(&word_index); + if self.auto_save_queue[&handle] <= now { + self.auto_save_queue.remove(&handle); - return self.save_word(word_index); + let mut dictionary = self.state.dictionary.write(); + + let reload_query = handle.real() == false; + let _ = dictionary.save(&mut handle); + if reload_query { + self.displayed_words_query.load(&dictionary); + } + return Task::none(); } } - ToImport => {} + GotoImport => {} } Task::none() @@ -286,7 +280,7 @@ impl NavigatedPage for DictionaryState { row![ self.add_word_button(), horizontal().width(Fill), - button("Импорт").style(text).on_press(ToImport), + button("Импорт").style(text).on_press(GotoImport), ] ] .spacing(5), @@ -300,43 +294,51 @@ impl NavigatedPage for DictionaryState { } impl DictionaryState { - pub fn new(state: Arc>) -> Self { - let len = state.lock().dictionary.len(); - + pub fn new(state: AppState) -> Self { + let words_query = state.dictionary.read().empty_query_par(); + let groups_query = state.word_groups.read().query(Arc::new(|_| true)); let mut result = DictionaryState { - include_map: vec![false; len], - selected_group_index: 0, + included: Default::default(), + selected_group: EntityHandle::new(Id(1), 0, true), state, tag_map: HashMap::new(), + words_tags: vec![], reverse: false, search: "".to_string(), no_typing: true, reverse_list: true, auto_save_queue: HashMap::new(), + displayed_words_query: words_query, + groups_query, }; + result.init(); result.update_tags(); result } - fn save_word(&mut self, i: usize) -> Task { - let state = &mut self.state.lock(); - let connection = &state.connection; - let word = &mut state.dictionary[i].clone(); - - if let Err(err) = update_word(word, connection) { - return push_error_task!(err); - } - - state.dictionary[i] = word.clone(); - Task::none() + fn init(&mut self) { + self.displayed_words_query = self + .state + .dictionary + .read() + .query(Arc::new(|word| word.group_id == Id(1))); } - fn launch_auto_save_offset(&mut self, index: usize) -> Task { + fn setup_words_query(&mut self) { + let id = self.selected_group.id; + self.displayed_words_query = self + .state + .dictionary + .read() + .query(Arc::new(move |word| word.group_id == id)); + } + + fn launch_auto_save_offset(&mut self, handle: EntityHandle) -> Task { let save_time = Utc::now().add(TimeDelta::milliseconds(900)); - self.auto_save_queue.insert(index, save_time); - let message = RootMessage::Dictionary(TrySave(index)); + self.auto_save_queue.insert(handle, save_time); + let message = RootMessage::Dictionary(TrySave(handle)); Task::perform( async { tokio::time::sleep(Duration::from_secs(1)).await }, |_| message, @@ -347,22 +349,16 @@ impl DictionaryState { let time = Instant::now(); let mut col = Column::new().width(Fill); - let state = self.state.lock(); - let group_id = state.word_groups[self.selected_group_index].id; + let query = &self.displayed_words_query; + let dictionary = self.state.dictionary.read(); - let dict = &state.dictionary; - - let mut index = 0; - - for access_index in 0..dict.len() { + for access_index in 0..query.len() { let mut i = access_index; if self.reverse_list { - i = dict.len() - access_index - 1; - } - let word = &dict[i]; - if word.group_id != group_id { - continue; + i = query.len() - access_index - 1; } + let word_handle = &query[i]; + let word = dictionary.get_ref_static(*word_handle).unwrap(); if !self.search.is_empty() && !word.key.contains(&self.search) @@ -373,103 +369,100 @@ impl DictionaryState { } let word_line_data = WordLineState { - is_included: self.include_map[i], - key: word.key.clone(), - value: word.value.clone(), - tags: word.tags.clone(), - id: word.id, - index: i, + is_included: self.included.contains(word_handle), + key: word.key.clone().into(), + value: word.value.clone().into(), + tags: word.tags.clone().into(), + handle: word_handle.clone(), }; - index += 1; + let lazy_line = lazy(word_line_data, Self::word_line); - let lazy_line = lazy(word_line_data, move |data| { - let index = data.index; - let mut line = Row::new().width(Fill).align_y(Center); - line = line.push( - checkbox(data.is_included) - .label("") - .spacing(15) - .on_toggle(move |b| Include(index, b)), - ); + col = col.push(lazy_line); + } - line = line.push( - text_input("Слово", &data.key) - .size(ACCENT_FONT_SIZE) - .width(Fill) - .on_input(move |string| SetKey(index, string)) - .on_submit(SubmitWord(index)) - .style(|x, status| { - let mut default_style = default(x, status); - default_style.border.radius = 0.0.into(); - default_style - }), - ); - line = line.push( - text_input("Перевод", &data.value) - .size(ACCENT_FONT_SIZE) - .width(Fill) - .on_input(move |string| SetValue(index, string)) - .on_submit(SubmitWord(index)) - .style(|x, status| { - let mut default_style = default(x, status); - default_style.border.radius = 0.0.into(); - default_style - }), - ); + info!("Words rendering time: {:?}", time.elapsed()); + scrollable(col).height(Fill).into() + } - line = line.push( - text_input("Теги", &data.tags) - .size(ACCENT_FONT_SIZE) - .width(Fill) - .on_input(move |string| SetTags(index, string)) - .on_submit(SubmitWord(index)) - .style(|x, status| { - let mut default_style = default(x, status); - default_style.border.radius = 0.0.into(); - default_style - }), - ); + fn word_line<'a>(data: &WordLineState) -> Row<'a, DictionaryMessage> { + let mut line = Row::new().width(Fill).align_y(Center); + let handle = data.handle.clone(); + line = line.push( + checkbox(data.is_included) + .label("") + .spacing(15) + .on_toggle(move |b| Include(handle, b)), + ); - let line_button = || { - let action = WordAction(index); + line = line.push( + text_input("Слово", &data.key) + .size(ACCENT_FONT_SIZE) + .width(Fill) + .on_input(move |string| SetKey(handle, string)) + .style(|x, status| { + let mut default_style = default(x, status); + default_style.border.radius = 0.0.into(); + default_style + }), + ); + line = line.push( + text_input("Перевод", &data.value) + .size(ACCENT_FONT_SIZE) + .width(Fill) + .on_input(move |string| SetValue(handle, string)) + .style(|x, status| { + let mut default_style = default(x, status); + default_style.border.radius = 0.0.into(); + default_style + }), + ); - if !data.id.is_valid() { - return button("-").on_press(action).style(|_x, _status| Style { + line = line.push( + text_input("Теги", data.tags.as_ref()) + .size(ACCENT_FONT_SIZE) + .width(Fill) + .on_input(move |string| SetTags(handle, string)) + .style(|x, status| { + let mut default_style = default(x, status); + default_style.border.radius = 0.0.into(); + default_style + }), + ); + + let line_button = || { + if !data.handle.real() { + return button("-") + .on_press(WordAction(data.handle)) + .style( + |_x: &iced::Theme, _status: iced::widget::button::Status| Style { background: None, text_color: Color::BLACK, border: Border::default(), shadow: Shadow::default(), snap: false, - }); - } + }, + ) + .width(15); + } - button("").on_press(WordAction(index)).width(15) - }; - line = line.push(line_button()).push(space().width(10)); - line - }); - - col = col.push(lazy_line); - } - - info!("Drawing {index} lines"); - info!("Words rendering time: {:?}", time.elapsed()); - scrollable(col).height(Fill).into() + button("") + .on_press(WordAction(data.handle)) + .style(button::primary) + .width(15) + }; + line.push(line_button()).push(space().width(10)) } fn filters(&self) -> iced::Element<'_, DictionaryMessage> { - let dict = &self.state.lock().dictionary; + let dict = &self.state.dictionary.read(); iced::widget::column![ text_input("Поиск", &self.search) .on_input(Search) .width(Fill), text!("Всего слов: {}", dict.len()), - text!( - "Выбрано слов: {}", - self.include_map.iter().filter(|i| **i).count() - ), + text!("Выбрано слов: {}", self.included.iter().count()), self.tags_selector(), toggler(self.no_typing) .label("Без набора") @@ -479,7 +472,7 @@ impl DictionaryState { .on_toggle(SetReverse), button(text!("Тест").center().width(Fill)) .style(cta_button) - .on_press(Test) + .on_press(GotoTest) .width(Fill), ] .width(250) @@ -514,15 +507,19 @@ impl DictionaryState { } fn update_tags(&mut self) { + self.words_tags.clear(); let mut tags_list: HashSet = HashSet::new(); - let dict = &self.state.lock().dictionary; - - dict.iter().for_each(|element| { - split_with_coma(element.tags.as_str()) - .iter() + let dict = &self.state.dictionary.write(); + let mut words_query = dict.full_query(); + let handles = words_query.handles().collect::>(); + words_query.iter(dict).zip(handles).for_each(|(element, handle)| { + let tags = split_with_coma(element.tags.as_str()); + tags.iter() .for_each(|tag| { tags_list.insert(tag.clone()); }); + self.words_tags.push((handle, tags)) + }); let current_tags = self @@ -552,22 +549,27 @@ impl DictionaryState { .collect::>(); if include_tags.is_empty() { - self.include_map.iter_mut().for_each(|x| *x = false); + self.included.clear(); return; } - let dict = &self.state.lock().dictionary; + let dict = &self.state.dictionary.read(); let time = Instant::now(); - self.include_map = dict - .iter() - .map(|word| (split_with_coma(word.tags.as_str()), word.group_id)) - .map(|(tags, word_group_id)| { - !tags.is_empty() - && tags.iter().all(|t| include_tags.contains(t)) - && word_group_id == group_id - }) - .collect(); + self.included.clear(); + for (handle, tags) in &mut self.words_tags { + if let Some(word) = dict.get_ref(handle) { + if word.group_id != group_id { + continue; + } + + if !tags.is_empty() && tags.iter().all(|t| include_tags.contains(t)) { + self.included.insert(*handle); + } + } else { + continue; + } + } info!("Time {}", time.elapsed().as_micros()); } @@ -576,18 +578,19 @@ impl DictionaryState { let mut row = Row::new(); row = row.push(button("+").style(text).on_press(CreateGroup)); - let state = &self.state.lock(); - let groups = &state.word_groups; + let groups = &self.state.word_groups.read(); - for (index, group) in groups.iter().enumerate() { + for mut group in self.groups_query.handles() { + let group_name = groups.get(&mut group).unwrap().name.clone(); row = row.push( - button(text!("{}", group.name.clone())) + button(text!("{}", group_name)) .style(text) - .on_press(SelectGroup(index)), + .on_press(SelectGroup(group)), ); } - let group = state.word_groups[self.selected_group_index].clone(); + let groups = self.state.word_groups.read(); + let group = groups.get_static(self.selected_group.clone()).unwrap(); iced::widget::column![ scrollable(row).width(Fill).horizontal(), @@ -606,7 +609,7 @@ impl DictionaryState { } fn group_delete_button(&self) -> iced::Element<'_, DictionaryMessage> { - if self.selected_group_index != 0 { + if self.selected_group.real() && self.selected_group.id != 1.into() { button("Удалить").style(danger).on_press(DeleteGroup).into() } else { space().into() @@ -614,11 +617,9 @@ impl DictionaryState { } fn add_word_button(&self) -> iced::Element<'_, DictionaryMessage> { - let state = self.state.lock(); - let group_id = state.word_groups[self.selected_group_index].id; let button = button("Добавить слово").style(jl_button); - if group_id.is_valid() { + if self.selected_group.real() { return button.on_press(NewWord).into(); } @@ -633,12 +634,11 @@ pub fn split_with_coma(ts: &str) -> Vec { .collect::>() } -#[derive(Hash, PartialEq, Eq)] +#[derive(Clone, Hash, PartialEq, Eq)] struct WordLineState { is_included: bool, - key: String, - value: String, - tags: String, - id: Id, - index: usize, + key: Box, + value: Box, + tags: Box, + handle: EntityHandle, } diff --git a/src/pages/dictionary_quiz.rs b/src/pages/dictionary_quiz.rs index a2a814c..d8fc4cf 100644 --- a/src/pages/dictionary_quiz.rs +++ b/src/pages/dictionary_quiz.rs @@ -1,4 +1,4 @@ -use crate::data::entities::WordData; +use crate::data::entities::{EntityHandle, WordData}; use crate::navigation::Page::PreviousPage; use crate::navigation::RootMessage; use crate::navigation::*; @@ -12,11 +12,13 @@ use iced::widget::container::Style; use iced::widget::{Row, button, container, row, space, text, text_input}; use iced::{Border, Element, Fill, Task, Theme, alignment}; use rand::prelude::SliceRandom; +use crate::app::AppState; +use crate::data::repositories::Repository; -#[derive(Debug, Clone)] pub struct DictionaryQuizState { - words: Vec, - current_set: Vec, + state: AppState, + words: Vec>, + current_set: Vec>, answer: String, view: String, correct: String, @@ -104,8 +106,9 @@ impl NavigatedPage for DictionaryQuizState { } impl DictionaryQuizState { - pub fn new(words: Vec, reverse: bool, no_typing: bool) -> DictionaryQuizState { - DictionaryQuizState { + pub fn new(words: Vec>, reverse: bool, no_typing: bool, state: AppState) -> DictionaryQuizState { + let state = DictionaryQuizState { + state, words, current_set: Vec::new(), answer: "".to_string(), @@ -116,7 +119,9 @@ impl DictionaryQuizState { reverse, laps: 0, no_typing, - } + }; + + state } fn submit(&mut self) { @@ -172,7 +177,7 @@ impl DictionaryQuizState { self.is_help = false; self.answer = String::new(); - let next: WordData = match self.current_set.pop() { + let next: EntityHandle = match self.current_set.pop() { Some(word) => word, None => { self.update_set(); @@ -180,12 +185,14 @@ impl DictionaryQuizState { } }; + let repo = self.state.dictionary.read(); + let next = repo.get_static(next).unwrap(); if self.reverse { - self.view = next.value.clone(); - self.correct = next.key.clone(); + self.view = next.value; + self.correct = next.key; } else { - self.view = next.key.clone(); - self.correct = next.value.clone(); + self.view = next.key; + self.correct = next.value; } } diff --git a/src/pages/history.rs b/src/pages/history.rs index 4cc0558..7665efe 100644 --- a/src/pages/history.rs +++ b/src/pages/history.rs @@ -1,22 +1,20 @@ use crate::app::AppState; -use crate::data::INVALID_ID; -use crate::data::entities::{HistoryItem, WordData}; -use crate::data_provider::history::get_history_of_set; +use crate::data::entities::{HistoryNote, WordData}; +use crate::data::repositories::Repository; use crate::navigation::*; use crate::styling::*; -use HistoryMessage::Back; use chrono::Local; use iced::alignment::Horizontal::Center; use iced::widget::space::horizontal; use iced::widget::*; use iced::{Element, Fill, FillPortion, Task}; -use parking_lot::Mutex; use std::sync::Arc; +use HistoryMessage::Back; #[derive(Clone)] pub struct HistoryState { - list: Vec, - words: Vec, + list: Vec<(WordData, HistoryNote)>, + pub state: AppState, } impl NavigatedPage for HistoryState { @@ -39,66 +37,70 @@ impl NavigatedPage for HistoryState { .width(FillPortion(5)), horizontal().width(FillPortion(1)) ] - .height(Fill) - .width(Fill) - .into(), + .height(Fill) + .width(Fill) + .into(), Back, ) } } impl HistoryState { - pub fn new(id: crate::data::Id, state: Arc>) -> Self { - let state = state.lock(); - let history = get_history_of_set(id).unwrap_or_default(); + pub fn new(id: crate::data::Id, state: AppState) -> Self { + let dict = &state.dictionary.read(); + let history_repo = &mut state.history.write(); + history_repo.init().expect("TODO: panic message"); + let id = id.clone(); + let mut history = history_repo.query(Arc::new(move |h| h.deck_id == id)); let words = history - .iter() + .iter(history_repo) .map( - |item| match state.dictionary.iter().find(|w| w.id == item.word_id) { - None => WordData { - id, + |item| {let handle = dict.handle_by_id(item.word_id); + let word = dict.get_static(handle).unwrap_or(WordData { + id: Default::default(), + group_id: Default::default(), key: "Удаленное слово".to_string(), - value: "Удаленное слово".to_string(), - tags: "Удаленное слово".to_string(), + value: "".to_string(), + tags: "".to_string(), additional: Default::default(), - group_id: INVALID_ID, - }, - Some(word) => word.clone(), - }, + }); + (word, item.clone()) + } ) .collect(); - Self { - list: history, - words, + list: words, + state: state.clone(), } } fn history_lines(&self) -> Column<'_, HistoryMessage> { let mut column = Column::new(); - for (index, item) in self.list.iter().enumerate() { + for (index, item) in &self.list { + + let activity = item; column = column.push( row![ iced::widget::column![ - text!("{}", self.words[index].key) + text!("{}", index.key) .width(Fill) .align_x(Center), - text!("{}", self.words[index].value) + text!("{}", index.value) .width(Fill) .align_x(Center), ] .align_x(Center), iced::widget::column![ - text!("{} ➞ {}", item.before, item.after), + text!("{} ➞ {}", activity.before, activity.after), text!( "{}", - item.timestamp.with_timezone(&Local).format("%d.%m %H:%M") + activity.timestamp.with_timezone(&Local).format("%d.%m %H:%M") ) ] .spacing(5) .align_x(Center) ] - .spacing(5), + .spacing(5), ); column = column.push(rule::horizontal(1)); } diff --git a/src/pages/import.rs b/src/pages/import.rs index 5a4de5d..f444e7f 100644 --- a/src/pages/import.rs +++ b/src/pages/import.rs @@ -1,7 +1,8 @@ use crate::app::AppState; -use crate::data::entities::{WordData, WordGroup}; +use crate::data::entities::EntityData; +use crate::data::entities::WordData; +use crate::data::entities::WordGroup; use crate::data_provider::import::{ImportData, ImportGroup, get_words_of_group, load_groups}; -use crate::data_provider::words::{add_group, add_words}; use crate::fs::*; use crate::navigation::{NavigatedPage, Page, RootMessage}; use crate::notification::NotificationData::OpenFileError; @@ -29,12 +30,13 @@ use std::path::PathBuf; use std::sync::Arc; use tokio::task::spawn_blocking; use zip::ZipArchive; +use crate::data::repositories::{BatchedRepository, Repository}; const DEFAULT_FIELDS: [&str; 6] = ["key", "value", "tags", "reading", "context", "description"]; #[derive(Clone)] pub struct ImportState { - state: Arc>, + state: AppState, path: Option, import_data: Option, selected_index: usize, @@ -318,7 +320,7 @@ impl ImportState { } impl ImportState { - pub fn new(state: Arc>) -> ImportState { + pub fn new(state: AppState) -> ImportState { ImportState { state, path: None, @@ -433,22 +435,22 @@ impl ImportState { async move { spawn_blocking(move || { let temp_file_path = temp_import_path(); - let connection = - Connection::open(&temp_file_path).map_err(|_| OpenFileError)?; + let connection = Connection::open(&temp_file_path).map_err(|_| OpenFileError)?; let import_list = get_words_of_group(&connection, group.id)?; let mut words_list = Vec::with_capacity(import_list.len()); let map_indices = Self::get_mapping_indices(&group); - let mut group_entity = WordGroup { - id: 0.into(), - name: group.name.clone(), - }; + let group_handle; { - let state = state.lock(); - add_group(&mut group_entity, &state.connection)?; + let group_entity = WordGroup { + id: 0.into(), + name: group.name.clone(), + }; + let mut groups_repo = state.word_groups.write(); + group_handle = groups_repo.create_now(group_entity)?; } - let group_id = group_entity.id; + let group_id = group_handle.id; for import in import_list { let mut word = WordData::new(); @@ -480,20 +482,16 @@ impl ImportState { words_list.push(word); } - let mut state = state.lock(); - let connection = &mut state.connection; + let mut words_repo = state.dictionary.write(); let mut index = 0; let total_len = words_list.len() as f32 / 1024.0; - for word in &mut words_list.chunks_mut(1024) { - add_words(word, connection)?; + for words in &mut words_list.chunks_mut(1024) { + words_repo.create_now_many(words)?; index += 1; let mut progress = progress.lock(); *progress = (index as f32 / total_len).min(1.0); } - state.dictionary.append(&mut words_list); - state.word_groups.push(group_entity); - Ok(()) }) .await diff --git a/src/pages/repetition.rs b/src/pages/repetition.rs index cbf5d91..9e277b9 100644 --- a/src/pages/repetition.rs +++ b/src/pages/repetition.rs @@ -2,28 +2,28 @@ use crate::app::AppState; use crate::core::deck::DeckData; use crate::core::srs::WordOpenMode; use crate::data::Id; -use crate::data::entities::{CardState, DeckSettings, WordData}; +use crate::data::entities::{CardState, WordData}; +use crate::data::repositories::Repository; use crate::data_provider::voice::get_voice; use crate::navigation::Page::PreviousPage; use crate::navigation::{KeyPressedPage, NavigatedPage, Page, RootMessage}; use crate::notification::{MaybeError, NotificationData}; +use crate::pages::repetitions::DeckRunData; use crate::styling::*; use crate::{push_error, push_error_task}; -use chrono::Local; use iced::alignment::Horizontal::Center; use iced::keyboard::key::Physical::Code; use iced::widget::text::Style; use iced::widget::{Column, button, column, container, row, rule, space, text, tooltip}; use iced::{Element, Fill, Task, Theme, alignment, keyboard}; -use parking_lot::Mutex; use rodio::MixerDeviceSink; use std::collections::HashSet; use std::sync::Arc; use tokio::task::spawn_blocking; pub struct RepetitionState { - state: Arc>, - pub settings: DeckSettings, + state: AppState, + pub settings: DeckRunData, pub set: DeckData, current_word: WordData, current_statistic: CardState, @@ -103,12 +103,13 @@ impl NavigatedPage for RepetitionState { impl RepetitionState { pub(crate) fn new( - set: DeckSettings, - state: Arc>, + set: DeckRunData, + state: AppState, ) -> Result { - let mut card_set = DeckData::new(&set, state.clone())?; + let mut card_set = DeckData::new(&set, set.open_mode, state.clone()); let (word, stat) = card_set.next(); - + let word = state.dictionary.read().get_static(word).unwrap(); + let stat = state.card_states.read().get_static(stat).unwrap(); let sink_handle = if set.forward.contains("speech") || set.backward.contains("speech") { rodio::DeviceSinkBuilder::open_default_sink().ok() } else { @@ -150,9 +151,11 @@ impl RepetitionState { self.open = false; self.opened.insert(self.current_word.id); - let next = self.set.next(); - self.current_word = next.0; - self.current_statistic = next.1; + let (word, state) = self.set.next(); + let word = self.state.dictionary.read().get_static(word).unwrap(); + let stat = self.state.card_states.read().get_static(state).unwrap(); + self.current_word = word; + self.current_statistic = stat; if self.settings.require_speech() && self.sink.is_some() { let sink = self.sink.clone(); @@ -162,29 +165,9 @@ impl RepetitionState { Err(err) => push_error!(err), }); } - self.increment_counter(); Task::none() } - fn increment_counter(&mut self) { - let mut state = self.state.lock(); - let id = self.settings.id; - let activity = state.activity.get_mut(&id); - - let now = Local::now().date_naive(); - - if let Some(activity) = activity { - let (last_day, counter) = activity.last_mut().unwrap(); - if *last_day == now { - *counter += 1; - } else { - activity.push((now, 1)); - } - } else { - state.activity.insert(id, vec![(now, 1)]); - } - } - fn draw_forward(&self) -> Element<'_, RepetitionMessage> { self.draw_card_view(self.settings.forward.as_str()) } diff --git a/src/pages/repetition_settings.rs b/src/pages/repetition_settings.rs index cf801bb..2fb0730 100644 --- a/src/pages/repetition_settings.rs +++ b/src/pages/repetition_settings.rs @@ -1,7 +1,7 @@ use crate::app::AppState; -use crate::data::entities::DeckSettings; -use crate::data_provider::card_states::{delete_stats, load_stats_of_deck}; -use crate::data_provider::decks::{delete_set, update_deck}; +use crate::core::deck::DeckData; +use crate::data::entities::{DeckSettings, EntityHandle}; +use crate::data::repositories::{BatchedRepository, Repository}; use crate::navigation::Page::PreviousPage; use crate::navigation::{NavigatedPage, Page, RootMessage}; use crate::notification::MaybeError; @@ -9,40 +9,41 @@ use crate::pages::repetition_settings::RepetitionSettingsMessage::*; use crate::styling::*; use crate::{push_error, push_error_task}; use iced::widget::button::danger; +use iced::widget::space::horizontal; use iced::widget::text_input::Catalog; use iced::widget::{button, column, row, scrollable, space, text, text_input}; use iced::{Element, Task}; use iced_core::Length::Fill; use iced_core::{Padding, Theme, color}; -use parking_lot::Mutex; use std::sync::Arc; use std::time::Duration; pub struct RepetitionSettingsState { - set: DeckSettings, - index: usize, - state: Arc>, + settings_handle: EntityHandle, + settings_data: DeckSettings, + state: AppState, real_delete: bool, + real_delete_states: bool, correct_filter: bool, filter_changed: bool, + filtered_words: Option, } impl RepetitionSettingsState { - pub(crate) fn new(index: usize, state: Arc>) -> RepetitionSettingsState { - let set; - { - let local_state = state.lock(); - set = local_state.decks[index].clone(); - } - let filter_state = set.check_filter(); - + pub(crate) fn new( + settings: EntityHandle, + state: AppState, + ) -> RepetitionSettingsState { + let data = state.decks.read().get_static(settings).unwrap(); RepetitionSettingsState { - index, - set, + settings_data: data, + settings_handle: settings, state, real_delete: false, - correct_filter: filter_state, + real_delete_states: false, + correct_filter: true, filter_changed: false, + filtered_words: None, } } } @@ -61,35 +62,36 @@ impl NavigatedPage for RepetitionSettingsState { match message { Back => {} Save => { - let mut state = self.state.lock(); - if let Err(e) = update_deck(&mut self.set, &state.connection) { + let mut decks = self.state.decks.write(); + decks.set(&mut self.settings_handle, self.settings_data.clone()); + + if let Err(e) = decks.save(&mut self.settings_handle) { return push_error_task!(e); } + drop(decks); if self.filter_changed - && let Err(e) = update_deck_stats(&self.set, &state) + && let Err(e) = update_deck_stats(&self.settings_data, &self.state) { return push_error_task!(e); } - state.decks[self.index] = self.set.clone(); } SetName(new) => { - self.set.name = new; + self.settings_data.name = new; } SetForward(new) => { - self.set.forward = new; + self.settings_data.forward = new; } SetBackward(backward) => { - self.set.backward = backward; + self.settings_data.backward = backward; } SetFilter(new) => { - self.set.filter = new; - self.correct_filter = self.set.check_filter(); + self.settings_data.filter = new; + self.correct_filter = self.settings_data.check_filter(); self.filter_changed = true; } TryFilter => { - let state = self.state.lock(); - let count = self.set.get_word_list(&state).len(); - self.set.count = Some(count); + let list = DeckData::list(&self.settings_data, &self.state); + self.filtered_words = Some(list.len() as u32); } DeleteSet => { if !self.real_delete { @@ -99,15 +101,33 @@ impl NavigatedPage for RepetitionSettingsState { RootMessage::RepetitionSettings(RevertDeleteSet) }); } - let mut state = self.state.lock(); - if let Err(e) = delete_set(&self.set, &state.connection) { + let mut decks = self.state.decks.write(); + if let Err(e) = decks.delete(self.settings_handle) { return push_error_task!(e); } - state.decks.remove(self.index); return Task::done(RootMessage::RepetitionSettings(Back)); } - RevertDeleteSet => self.real_delete = false, + RevertDeleteSet => { + self.real_delete = false; + self.real_delete_states = true; + } + DeleteStates => { + if !self.real_delete_states { + self.real_delete_states = true; + return Task::future(async { + tokio::time::sleep(Duration::from_millis(3000)).await; + RootMessage::RepetitionSettings(RevertDeleteSet) + }); + } + let mut states = self.state.card_states.write(); + let deck_id = self.settings_handle.id; + let mut query = states.query(Arc::new(move |state| {state.deck_id == deck_id})).handles().collect::>(); + if let Err(e) = states.delete_many(query.as_mut_slice()) { + return push_error_task!(e); + } + self.real_delete_states = false; + } } Task::none() } @@ -119,22 +139,22 @@ impl NavigatedPage for RepetitionSettingsState { column![ column![ text!("Название набора"), - text_input("", &self.set.name).on_input(SetName), + text_input("", &self.settings_data.name).on_input(SetName), ] .spacing(QUARTER_SPACING), column![ text!("Передняя сторона"), - text_input("", &self.set.forward).on_input(SetForward), + text_input("", &self.settings_data.forward).on_input(SetForward), ] .spacing(QUARTER_SPACING), column![ text!("Задняя сторона"), - text_input("", &self.set.backward).on_input(SetBackward), + text_input("", &self.settings_data.backward).on_input(SetBackward), ] .spacing(QUARTER_SPACING), column![ text!("Фильтр"), - text_input("", &self.set.filter) + text_input("", &self.settings_data.filter) .style(|x, status| { let mut primary = Theme::default()(x, status); if !self.correct_filter { @@ -146,7 +166,7 @@ impl NavigatedPage for RepetitionSettingsState { button("Проверить фильтр") .style(jl_button) .on_press(TryFilter), - self.count_view(&self.set), + self.count_view(), ] .spacing(QUARTER_SPACING), ] @@ -161,6 +181,14 @@ impl NavigatedPage for RepetitionSettingsState { .height(Fill), row![ button("Сохранить").style(jl_button).on_press(Save), + horizontal(), + button(if self.real_delete_states { + "Вы уверены?" + } else { + "Удалить прогресс?" + }) + .style(danger) + .on_press(DeleteStates), button(if self.real_delete { "Вы уверены?" } else { @@ -168,7 +196,6 @@ impl NavigatedPage for RepetitionSettingsState { }) .style(danger) .on_press(DeleteSet), - space().width(Fill), ] .spacing(DEFAULT_SPACING), ] @@ -180,22 +207,34 @@ impl NavigatedPage for RepetitionSettingsState { } fn update_deck_stats(deck: &DeckSettings, state: &AppState) -> MaybeError { - let mut db_stats = load_stats_of_deck(deck, &state.connection).expect("Wrong db operation"); - let filtered_words = deck.get_word_list(state); - filtered_words.iter().for_each(|word| { - let word_id = state.dictionary[*word].id; - if let Some(index) = db_stats.iter().position(|stat| stat.word_id == word_id) { + let states_repo = &mut state.card_states.write(); + let deck_id = deck.id; + let mut db_stats = states_repo + .query_par(Arc::new(move |s| s.deck_id == deck_id)) + .iter(&states_repo) + .collect::>(); + let filtered_words = DeckData::list(deck, state) + .iter() + .map(|w| w.id) + .collect::>(); + filtered_words.iter().for_each(|word_id| { + if let Some(index) = db_stats.iter().position(|stat_id| stat_id.id == *word_id) { db_stats.remove(index); } }); - delete_stats(db_stats.as_slice(), &state.connection)?; + let mut delete_operation = db_stats + .iter() + .map(|state| EntityHandle::new(state.id, state.id.0 as usize, true)) + .collect::>(); + states_repo.delete_many(delete_operation.as_mut_slice())?; + Ok(()) } impl RepetitionSettingsState { - fn count_view(&self, set: &DeckSettings) -> Element<'_, RepetitionSettingsMessage> { - if let Some(count) = set.count { + fn count_view(&self) -> Element<'_, RepetitionSettingsMessage> { + if let Some(count) = self.filtered_words { return text!("Количество слов: {}", count).into(); } space().into() @@ -212,5 +251,6 @@ pub enum RepetitionSettingsMessage { SetFilter(String), TryFilter, DeleteSet, + DeleteStates, RevertDeleteSet, } diff --git a/src/pages/repetitions.rs b/src/pages/repetitions.rs index 38f44d2..1e49403 100644 --- a/src/pages/repetitions.rs +++ b/src/pages/repetitions.rs @@ -1,12 +1,11 @@ use crate::app::AppState; +use crate::core::deck::DeckData; use crate::core::srs::{AppendMode, OrderMode}; -use crate::data::Id; -use crate::data::entities::{CardState, DeckSettings}; -use crate::data_provider::card_states::{add_stat_list, load_stats_of_deck}; -use crate::data_provider::decks::update_deck; +use crate::data::entities::{CardState, DeckSettings, EntityHandle, WordData}; +use crate::data::repositories::{BatchedRepository, Repository}; use crate::navigation::Page::{History, PreviousPage, Repetition, RepetitionSettings}; use crate::navigation::{NavigatedPage, Page, RootMessage}; -use crate::notification::{MaybeError, NotificationRequest}; +use crate::notification::MaybeError; use crate::pages::history::HistoryState; use crate::pages::repetition::RepetitionState; use crate::pages::repetition_settings::RepetitionSettingsState; @@ -15,7 +14,7 @@ use crate::push_error; use crate::push_error_task; use crate::styling::*; use chrono::{Days, Local, Utc}; -use hashbrown::{HashMap, HashSet}; +use hashbrown::HashSet; pub use iced::widget::button::{Catalog, Style}; use iced::widget::button::{Status, danger}; use iced::widget::tooltip::Position::Top; @@ -27,16 +26,15 @@ use iced::{Background, Border, Center, Color, Element, Fill, Length, Shadow, Tas use iced_core::Padding; use iced_core::border::Radius; use iced_core::svg::Handle; -use parking_lot::Mutex; +use std::collections::BTreeMap; use std::ops::Deref; use std::sync::Arc; #[derive(Clone)] pub struct RepetitionsState { - word_id_index_map: HashMap, - selected_deck_index: Option, - decks: Vec, - state: Arc>, + selected_deck_handle: Option>, + decks: BTreeMap, DeckRunData>, + state: AppState, } impl NavigatedPage for RepetitionsState { @@ -49,11 +47,10 @@ impl NavigatedPage for RepetitionsState { self.state.clone(), ))) } else if let GoToRepetition = message { - let clone = self.state.clone(); - + let mut state_clone = self.state.clone(); let selected_set = self.selected_deck_mut().unwrap(); if selected_set.append_mode == AppendMode::Full { - if let Err(_err) = Self::append_all_words(&mut clone.lock(), selected_set) { + if let Err(_err) = Self::append_all_words(&mut state_clone, selected_set) { return None; } selected_set.existing_words_indices = selected_set.available_words_indices.clone(); @@ -66,7 +63,7 @@ impl NavigatedPage for RepetitionsState { } if let Ok(repetition_state) = - RepetitionState::new(selected_set.general_settings.clone(), clone) + RepetitionState::new(selected_set.clone(), self.state.clone()) { return Some(Repetition(repetition_state)); } @@ -74,7 +71,7 @@ impl NavigatedPage for RepetitionsState { None } else if let GoToSettings = message { Some(RepetitionSettings(RepetitionSettingsState::new( - self.selected_deck().unwrap().index.unwrap(), + self.selected_deck_handle.unwrap(), self.state.clone(), ))) } else { @@ -82,23 +79,22 @@ impl NavigatedPage for RepetitionsState { } } fn navigated(&mut self) { - if let Some(index) = self.selected_deck_index { - let state = self.state.lock(); - if let Some(global_deck) = state.decks.get(index) { - if global_deck.id != self.selected_deck().unwrap().id { - drop(state); - self.decks.remove(index); - self.clear_selection(); - } else { - self.decks[index].general_settings = state.decks[index].clone(); - drop(state); - let _ = self.fill_deck_word_indices(); + if let Some(index) = self.selected_deck_handle { + let decks = self.state.decks.read(); + if let Some(deck) = decks.get_static(index) { + { + let stored = self.decks.get_mut(&index).unwrap(); + stored.general_settings = deck; } + drop(decks); + self.fill_deck_word_indices(); } else { - drop(state); - self.decks.remove(index); - self.clear_selection() + self.decks.remove(&index); + drop(decks); + self.clear_selection(); } + } else { + let _ = self.fill_deck_word_indices(); } } fn update(&mut self, message: RepetitionsMessage) -> Task { @@ -111,31 +107,32 @@ impl NavigatedPage for RepetitionsState { CreateSet => { let index = self.decks.len() + 1; - self.decks.push(DeckViewData { - general_settings: DeckSettings::with_name(format!("Колода #{}", index)), - existing_words_indices: Default::default(), - available_words_indices: Default::default(), - indices_ready: false, - append_mode: Default::default(), - index: None, - }); + let general = DeckSettings::with_name(format!("Колода #{}", index)); + let handle = self.state.decks.write().create(general.clone()); + self.decks.insert( + handle, + DeckRunData { + settings_handle: handle, + general_settings: general, + existing_words_indices: Default::default(), + available_words_indices: Default::default(), + indices_ready: false, + append_mode: Default::default(), + count: None, + open_mode: OrderMode::Default, + }, + ); } DeleteSet => { - let index = self.selected_deck_index.unwrap(); + let index = self.selected_deck_handle.unwrap(); self.clear_selection(); - let deck = self.decks.remove(index); - if deck.id.is_valid() { - let mut state = self.state.lock(); - state.decks.remove(index); - self.decks = self - .decks - .clone() - .into_iter() - .map(|mut deck| { - deck.index = state.decks.iter().position(|d| d.id == deck.id); - deck - }) - .collect(); + let deck = self.decks.remove(&index); + + if index.real() { + let mut repo = self.state.decks.write(); + if let Err(err) = repo.delete(index) { + return push_error_task!(err); + } } } SetName(value) => { @@ -145,29 +142,24 @@ impl NavigatedPage for RepetitionsState { SelectSet(index) => { self.select_deck(index); let deck = self.selected_deck().unwrap(); - if !deck.indices_ready - && let Err(err) = self.fill_deck_word_indices() - { - return push_error_task!(err); + if !deck.indices_ready { + self.fill_deck_word_indices() } } Save => { - let mut deck = self.selected_deck().unwrap().clone(); - let mut state = self.state.lock(); - { - if let Err(e) = update_deck(&mut deck.general_settings, &state.connection) { - return Task::done(RootMessage::PushNotification( - NotificationRequest::Error(e), - )); - } - if let Some(index) = deck.index { - state.decks[index] = deck.general_settings.clone(); - } else { - state.decks.push(deck.general_settings.clone()); - deck.index = Some(state.decks.len() - 1); - self.decks[self.selected_deck_index.unwrap()] = deck.clone(); - } + let deck = self.selected_deck().unwrap().clone(); + let handle = self.selected_deck_handle.as_mut().unwrap(); + let mut repo = self.state.decks.write(); + + repo.set(handle, deck.general_settings.clone()); + let old = handle.clone(); + if let Err(err) = repo.save(handle) { + return push_error_task!(err); } + + let mut deck = self.decks.remove(&old).unwrap(); + deck.settings_handle = *handle; + self.decks.insert(*handle, deck); } SetForward(value) => { let deck = self.selected_deck_mut().unwrap(); @@ -182,23 +174,21 @@ impl NavigatedPage for RepetitionsState { deck.general_settings.filter = value; } TryFilter => { - let state = self.state.lock(); let deck = self.selected_deck().unwrap(); - let count = deck.get_word_list(&state).len(); - drop(state); + let count = DeckData::list(&deck, &self.state).len(); let deck = self.selected_deck_mut().unwrap(); - deck.general_settings.count = Some(count); + deck.count = Some(count); } SetOpenMode(mode) => { let deck = self.selected_deck_mut().unwrap(); - deck.general_settings.open_mode = mode; + deck.open_mode = mode; } SetAppendMode(mode) => { let deck = self.selected_deck_mut().unwrap(); deck.append_mode = mode; } AppendWords(count) => { - let adding: Vec; + let adding: Vec>; { { let deck = self.selected_deck().unwrap(); @@ -220,10 +210,10 @@ impl NavigatedPage for RepetitionsState { deck.existing_words_indices.append(&mut adding.clone()); } - let deck = self.selected_deck().unwrap(); - let mut state = self.state.lock(); + let deck = self.selected_deck().unwrap().clone(); + let state = &mut self.state; if let Err(err) = - Self::append_words(&mut state, &deck.general_settings, adding.into_iter()) + Self::append_words(state, &deck.general_settings, adding.into_iter()) { return push_error_task!(err); } @@ -257,47 +247,44 @@ impl NavigatedPage for RepetitionsState { } impl RepetitionsState { - pub(crate) fn new(state: Arc>) -> RepetitionsState { - let state_ = state.lock(); - let mut map = HashMap::with_capacity(state_.dictionary.len()); - - state_ - .dictionary - .iter() - .enumerate() - .for_each(|(index, word)| { - map.insert(word.id, index); - }); - - let decks = Self::get_decks_from_state(&state_); - drop(state_); + pub(crate) fn new(state: AppState) -> RepetitionsState { + let decks = Self::get_decks_from_state(&state); RepetitionsState { - word_id_index_map: map, - selected_deck_index: None, + selected_deck_handle: None, decks, state, } } - fn get_decks_from_state(state: &AppState) -> Vec { + fn get_decks_from_state( + state: &AppState, + ) -> BTreeMap, DeckRunData> { state .decks + .read() .iter() .enumerate() - .map(|(index, deck)| DeckViewData { - general_settings: deck.clone(), - existing_words_indices: Default::default(), - available_words_indices: Default::default(), - indices_ready: false, - append_mode: Default::default(), - index: Some(index), + .map(|(index, deck)| { + ( + EntityHandle::new(deck.id, index, true), + DeckRunData { + settings_handle: EntityHandle::new(deck.id, index, true), + general_settings: deck.clone(), + existing_words_indices: Default::default(), + available_words_indices: Default::default(), + indices_ready: false, + append_mode: Default::default(), + count: None, + open_mode: OrderMode::Default, + }, + ) }) - .collect() + .collect::>() } } impl RepetitionsState { - fn launch_delete_button(&self, deck: &DeckViewData) -> Element<'_, RepetitionsMessage> { + fn launch_delete_button(&self, deck: &DeckRunData) -> Element<'_, RepetitionsMessage> { if deck.id.is_valid() { if deck.append_mode == AppendMode::Manual && deck.existing_words_indices.is_empty() { return text!("Добавьте карточки или измените фильтр").into(); @@ -329,7 +316,7 @@ impl RepetitionsState { ] .spacing(QUARTER_SPACING), { - if !deck.id.is_valid() { + if !deck.settings_handle.real() { column![ column![ text!("Передняя сторона"), @@ -412,22 +399,20 @@ impl RepetitionsState { fn activity_bar(&self, deck: &DeckSettings) -> Element<'_, RepetitionsMessage> { const MAX_DAY_COUNT: f32 = 128.0; - let state = self.state.lock(); - let history = state.activity.get(&deck.id); + let history = self.state.history.read(); + let history = history.activity_of_deck(deck.id); let mut counts: Vec = vec![0; 30 * 7]; let now = Local::now().date_naive(); - if let Some(history) = history { - for (date, activity) in history { - let distance = now - *date; - let index = counts.len() as i64 - distance.num_days() - 1; + for (date, activity) in history { + let distance = now - date; + let index = counts.len() as i64 - distance.num_days() - 1; - if index < 0 { - continue; - } - - counts[index as usize] = *activity; + if index < 0 { + continue; } + + counts[index as usize] = activity; } lazy((counts, now), |(counts, now)| { @@ -483,7 +468,7 @@ impl RepetitionsState { }) .into() } - fn word_append_panel(&self, deck: &DeckViewData) -> Element<'_, RepetitionsMessage> { + fn word_append_panel(&self, deck: &DeckRunData) -> Element<'_, RepetitionsMessage> { column![ text!("Режим добавления карточек"), row![ @@ -506,7 +491,7 @@ impl RepetitionsState { .spacing(HALF_SPACING) .into() } - fn adder_panel(&self, deck: &DeckViewData) -> Element<'_, RepetitionsMessage> { + fn adder_panel(&self, deck: &DeckRunData) -> Element<'_, RepetitionsMessage> { match deck.append_mode { AppendMode::Full => space().into(), AppendMode::Manual => row![ @@ -526,7 +511,7 @@ impl RepetitionsState { let available = selected_deck.available_words_indices.len(); text!("{} слова добавлено из {}", now, available).into() } - fn count_view(&self, deck: &DeckSettings) -> Element<'_, RepetitionsMessage> { + fn count_view(&self, deck: &DeckRunData) -> Element<'_, RepetitionsMessage> { if let Some(count) = deck.count { return text!("Количество слов: {}", count).into(); } @@ -535,10 +520,10 @@ impl RepetitionsState { fn decks_list(&self) -> Column<'_, RepetitionsMessage> { let mut column = Column::new(); - for (i, deck) in self.decks.iter().enumerate() { + for (i, (handle, deck)) in self.decks.iter().enumerate() { column = column.push( button(text!("{}", &deck.name)) - .on_press_with(move || SelectSet(i)) + .on_press_with(move || SelectSet(*handle)) .style(move |_x: &Theme, status: Status| Style { background: if status == Status::Hovered { Some(Background::Color(Color::WHITE.scale_alpha(0.2))) @@ -562,7 +547,7 @@ impl RepetitionsState { } impl RepetitionsState { - fn append_all_words(state: &mut AppState, deck: &DeckViewData) -> MaybeError { + fn append_all_words(state: &mut AppState, deck: &DeckRunData) -> MaybeError { let existing = &deck.existing_words_indices; let mut created_set = HashSet::with_capacity(existing.len()); existing.iter().for_each(|c| { @@ -579,60 +564,60 @@ impl RepetitionsState { fn append_words( state: &mut AppState, deck: &DeckSettings, - indices: impl Iterator, + indices: impl Iterator>, ) -> MaybeError { let words = &state.dictionary; let stats = &mut indices - .map(|i| { - let word = &words[i]; - CardState { - id: 0.into(), - word_id: word.id, - last_open: Utc::now(), - score: 1, - fsrs_opened: Utc::now().date_naive(), - fsrs_memory: None, - set_id: deck.id, - fsrs_open_day: Utc::now().date_naive(), - } + .map(|i| CardState { + id: 0.into(), + word_id: i.id, + last_open: Utc::now(), + score: 1, + fsrs_opened: Utc::now().date_naive(), + fsrs_memory: None, + deck_id: deck.id, + fsrs_open_day: Utc::now().date_naive(), }) .collect::>(); if !stats.is_empty() { - add_stat_list(stats, &mut state.connection)?; + let mut repo = state.card_states.write(); + repo.create_now_many(stats.as_mut_slice())?; } Ok(()) } - fn select_deck(&mut self, index: usize) { - if self.decks.get(index).is_some() { - self.selected_deck_index = Some(index); + fn select_deck(&mut self, index: EntityHandle) { + if self.decks.contains_key(&index) { + self.selected_deck_handle = Some(index); } } - fn selected_deck(&self) -> Option<&DeckViewData> { - self.decks.get(self.selected_deck_index?) + fn selected_deck(&self) -> Option<&DeckRunData> { + self.decks.get(&self.selected_deck_handle?) } - fn selected_deck_mut(&mut self) -> Option<&mut DeckViewData> { - self.decks.get_mut(self.selected_deck_index?) + fn selected_deck_mut(&mut self) -> Option<&mut DeckRunData> { + self.decks.get_mut(&self.selected_deck_handle?) } fn clear_selection(&mut self) { - self.selected_deck_index = None; + self.selected_deck_handle = None; } - fn fill_deck_word_indices(&mut self) -> MaybeError { - let state = &self.state.lock(); + fn fill_deck_word_indices(&mut self) { + let card_states = self.state.card_states.read(); + let deck_id = self.selected_deck().unwrap().id; + let added = card_states + .query_par(Arc::new(move |state| state.deck_id == deck_id)) + .iter(&card_states) + .map(|state| EntityHandle::from_id(state.word_id)) + .collect::>(); let mut deck = self.selected_deck().unwrap().clone(); - let added: Vec<_> = load_stats_of_deck(&deck, &state.connection)? - .iter() - .map(|c| self.word_id_index_map[&c.word_id]) - .collect(); - let total = deck.get_word_list(state); + let total = DeckData::list(&deck, &self.state); deck.existing_words_indices = added; deck.available_words_indices = total; - self.decks[self.selected_deck_index.unwrap()] = deck; - Ok(()) + deck.indices_ready = true; + self.decks.insert(self.selected_deck_handle.unwrap(), deck); } } #[derive(Clone)] @@ -643,7 +628,7 @@ pub enum RepetitionsMessage { CreateSet, DeleteSet, SetName(String), - SelectSet(usize), + SelectSet(EntityHandle), Save, SetForward(String), SetBackward(String), @@ -657,16 +642,18 @@ pub enum RepetitionsMessage { } #[derive(Clone)] -struct DeckViewData { - general_settings: DeckSettings, - existing_words_indices: Vec, - available_words_indices: Vec, +pub struct DeckRunData { + pub settings_handle: EntityHandle, + pub general_settings: DeckSettings, + existing_words_indices: Vec>, + available_words_indices: Vec>, indices_ready: bool, - append_mode: AppendMode, - index: Option, + pub append_mode: AppendMode, + count: Option, + pub open_mode: OrderMode, } -impl Deref for DeckViewData { +impl Deref for DeckRunData { type Target = DeckSettings; fn deref(&self) -> &Self::Target { diff --git a/src/pages/selector.rs b/src/pages/selector.rs index f641efa..b8d1cca 100644 --- a/src/pages/selector.rs +++ b/src/pages/selector.rs @@ -22,7 +22,7 @@ use std::sync::Arc; pub struct SelectorState { pub set: KanaSet, is_writing: bool, - state: Arc>, + state: AppState, } #[derive(Debug, Clone)] @@ -114,7 +114,7 @@ impl NavigatedPage for SelectorState { } impl SelectorState { - pub fn new(state: Arc>) -> Self { + pub fn new(state: AppState) -> Self { Self { set: Default::default(), is_writing: false, diff --git a/src/pages/sync.rs b/src/pages/sync.rs index e9f7900..954cc4c 100644 --- a/src/pages/sync.rs +++ b/src/pages/sync.rs @@ -1,4 +1,5 @@ -use crate::app::{AppState, fill_state}; +use crate::app::AppState; +use crate::data::repositories::Repository; use crate::data_provider::settings::{delete_settings, set_setting}; use crate::data_provider::web_api::*; use crate::navigation::Page::PreviousPage; @@ -12,8 +13,6 @@ use iced::widget::container::rounded_box; use iced::widget::{button, column, container, progress_bar, row, space, text, toggler}; use iced::{Center, Element, Fill, Font, Length, Task}; use log::error; -use parking_lot::Mutex; -use std::sync::Arc; use std::time::Duration; #[derive(Clone)] @@ -34,7 +33,7 @@ pub enum SyncMessage { } #[derive(Clone)] pub struct SyncState { - state: Arc>, + state: AppState, auto_fetch: bool, frozen: bool, progress: f32, @@ -56,8 +55,7 @@ impl NavigatedPage for SyncState { match message { Back => {} CopyKey => { - let state = self.state.lock(); - let key = state.sync_data.key.clone().unwrap(); + let key = self.state.sync_data.read().key.clone().unwrap(); return iced::clipboard::write(key) .map(|_val: String| RootMessage::Sync(KeyCopied)); } @@ -76,16 +74,20 @@ impl NavigatedPage for SyncState { if !validate_id(&new_id) { return Task::none(); } - let mut state = self.state.lock(); - state.sync_data.key = Some(new_id.clone()); - if let Err(err) = set_setting("SYNC_KEY".to_string(), new_id, &state.connection) { + let mut settings = self.state.sync_data.write(); + settings.key = Some(new_id.clone()); + if let Err(err) = set_setting( + "SYNC_KEY".to_string(), + new_id, + &self.state.connection.lock(), + ) { return push_error_task!(err); } } Send => { self.prepare_to_db_interaction(); - let id = self.state.lock().sync_data.key.clone().unwrap(); + let id = self.state.sync_data.read().key.clone().unwrap(); let tasks = Task::batch([ Task::perform( async { tokio::time::sleep(Duration::from_millis(200)).await }, @@ -97,7 +99,7 @@ impl NavigatedPage for SyncState { return tasks; } GetLast => { - let id = self.state.lock().sync_data.key.clone().unwrap(); + let id = self.state.sync_data.read().key.clone().unwrap(); let tasks = Task::batch([ Task::perform( async { tokio::time::sleep(Duration::from_millis(200)).await }, @@ -122,26 +124,27 @@ impl NavigatedPage for SyncState { } NetworkFinished => { self.frozen = false; - let mut updated_state = AppState::new(); - fill_state(&mut updated_state); + { + let mut connection = self.state.connection.lock(); + connection.restart(); + } + + if let Err(err) = self.reload_data() { + return push_error_task!(err); + } - let mut state = self.state.lock(); - state.sync_data = updated_state.sync_data; - state.connection = updated_state.connection; - state.decks = updated_state.decks; - state.dictionary = updated_state.dictionary; - state.word_groups = updated_state.word_groups; return Task::done(RootMessage::PushNotification(NotificationRequest::Info( NotificationData::DataLoaded, ))); } Disable => {} DisableSync => { - let mut state = self.state.lock(); - state.sync_data.key = None; + let mut sync_data = self.state.sync_data.write(); + let connection = self.state.connection.lock(); + sync_data.key = None; let save_result = || { - delete_settings("SYNC_KEY".to_string(), &state.connection)?; - delete_settings("AUTO_WEB_FETCH".to_string(), &state.connection)?; + delete_settings("SYNC_KEY".to_string(), &connection)?; + delete_settings("AUTO_WEB_FETCH".to_string(), &connection)?; Ok::<(), NotificationData>(()) }; @@ -151,12 +154,14 @@ impl NavigatedPage for SyncState { } SwitchAutoSync => { self.auto_fetch = !self.auto_fetch; - let mut state = self.state.lock(); - state.sync_data.auto_web_fetch = self.auto_fetch; + let mut sync_data = self.state.sync_data.write(); + let connection = self.state.connection.lock(); + + sync_data.auto_web_fetch = self.auto_fetch; if let Err(err) = set_setting( "AUTO_WEB_FETCH".to_string(), self.auto_fetch.to_string(), - &state.connection, + &connection, ) { return push_error_task!(err); } @@ -177,11 +182,9 @@ impl NavigatedPage for SyncState { } impl SyncState { - pub fn new(state: Arc>) -> SyncState { - let auto_fetch; - { - auto_fetch = state.lock().sync_data.auto_web_fetch; - } + pub fn new(state: AppState) -> SyncState { + let auto_fetch = state.sync_data.read().auto_web_fetch; + Self { auto_fetch, state, @@ -190,6 +193,15 @@ impl SyncState { } } + fn reload_data(&mut self) -> Result<(), NotificationData> { + self.state.card_states.write().reload()?; + self.state.word_groups.write().reload()?; + self.state.decks.write().reload()?; + self.state.dictionary.write().reload()?; + self.state.history.write().reload()?; + Ok(()) + } + fn sync_column(&self) -> Element<'_, SyncMessage> { let network_view: Element<'_, SyncMessage> = if self.frozen { progress_bar(0.0..=5.0, self.progress).into() @@ -197,7 +209,7 @@ impl SyncState { space().into() }; { - if let Some(key) = self.state.lock().sync_data.key.clone() { + if let Some(key) = self.state.sync_data.read().key.as_ref() { column![ text!("Ваш ключ синхронизации"), container( @@ -254,13 +266,26 @@ impl SyncState { } self.frozen = true; self.progress = 0.0; - let state = self.state.lock(); - state - .connection + let mut connection = self.state.connection.lock(); + + connection .query_row("PRAGMA wal_checkpoint(TRUNCATE)", [], |_| Ok(())) .unwrap_or_else(|e| { error!("Ошибка при checkpoint WAL: {e}"); }); + connection + .execute("VACUUM;", []) + .unwrap_or_else(|e| { + error!("Ошибка при checkpoint WAL: {e}"); + 0 + }); + connection + .execute("PRAGMA optimize;", []) + .unwrap_or_else(|e| { + error!("Ошибка при checkpoint WAL: {e}"); + 0 + }); + connection.finish(); } } diff --git a/src/pages/word.rs b/src/pages/word.rs index fa95d58..bb57c89 100644 --- a/src/pages/word.rs +++ b/src/pages/word.rs @@ -1,5 +1,5 @@ use crate::app::AppState; -use crate::data::entities::WordData; +use crate::data::entities::{EntityHandle, WordData}; use crate::data_provider::words::{delete_word, update_word}; use crate::navigation::Page::PreviousPage; use crate::navigation::{NavigatedPage, Page, RootMessage}; @@ -8,16 +8,15 @@ use crate::push_error; use crate::push_error_task; use crate::styling::*; use iced::widget::button::danger; -use iced::widget::{button, column, row, rule, scrollable, text, text_input}; +use iced::widget::{button, column, row, rule, scrollable, space, text, text_input}; use iced::{Element, Fill, Task}; use parking_lot::Mutex; use std::sync::Arc; #[derive(Clone)] pub struct WordState { - state: Arc>, - index: usize, - word: WordData, + state: AppState, + handle: EntityHandle, } impl NavigatedPage for WordState { @@ -31,113 +30,117 @@ impl NavigatedPage for WordState { fn navigated(&mut self) {} fn update(&mut self, message: WordMessage) -> Task { - match message { - Back => {} - Save => { - let mut state = self.state.lock(); - state.dictionary[self.index] = self.word.clone(); - - if let Err(err) = update_word(&mut self.word, &state.connection) { - return push_error_task!(err); - } - return Task::done(RootMessage::Word(Back)); - } - Delete => { - let mut state = self.state.lock(); - state.dictionary.remove(self.index); - if let Err(err) = delete_word(&self.word, &state.connection) { - return push_error_task!(err); - } - return Task::done(RootMessage::Word(Back)); - } - SetTags(n) => self.word.tags = n, - SetKey(n) => { - self.word.key = n; - } - SetValue(n) => { - self.word.value = n; - } - SetAdditional(key, value) => { - self.word.additional.insert(key, value.clone()); - } - AddAdditional(key) => { - self.word.additional.insert(key, "".to_string()); - } - RemoveAdditional(key) => { - self.word.additional.remove(key.as_str()); - } - } + // match message { + // Back => {} + // Save => { + // let mut state = self.state.lock(); + // state.dictionary[self.index] = self.word.clone(); + // + // if let Err(err) = update_word(&mut self.word, &state.connection) { + // return push_error_task!(err); + // } + // return Task::done(RootMessage::Word(Back)); + // } + // Delete => { + // let mut state = self.state.lock(); + // state.dictionary.remove(self.index); + // if let Err(err) = delete_word(&self.word, &state.connection) { + // return push_error_task!(err); + // } + // return Task::done(RootMessage::Word(Back)); + // } + // SetTags(n) => self.word.tags = n, + // SetKey(n) => { + // self.word.key = n; + // } + // SetValue(n) => { + // self.word.value = n; + // } + // SetAdditional(key, value) => { + // self.word.additional.insert(key, value.clone()); + // } + // AddAdditional(key) => { + // self.word.additional.insert(key, "".to_string()); + // } + // RemoveAdditional(key) => { + // self.word.additional.remove(key.as_str()); + // } + // } Task::none() } fn view(&self) -> Element<'_, WordMessage> { - let mut fast_add = row![]; - if !self.word.additional.contains_key("reading") { - fast_add = fast_add.push( - button("Чтение") - .style(button::text) - .on_press(AddAdditional("reading".to_string())), - ); - } - if !self.word.additional.contains_key("description") { - fast_add = fast_add.push( - button("Описание") - .style(button::text) - .on_press(AddAdditional("description".to_string())), - ); - } - - if !self.word.additional.contains_key("context") { - fast_add = fast_add.push( - button("В контексте") - .style(button::text) - .on_press(AddAdditional("context".to_string())), - ); - } - - let mut col = iced::widget::column![ - column![ - text!("Ключ"), - text_input("key", &self.word.key).on_input(SetKey), - ] - .spacing(QUARTER_SPACING), - column![ - text!("Значение"), - text_input("value", &self.word.value).on_input(SetValue), - ] - .spacing(QUARTER_SPACING), - column![ - text!("Теги"), - text_input("tags", &self.word.tags).on_input(SetTags), - ] - .spacing(QUARTER_SPACING), - rule::horizontal(2), - scrollable(fast_add), - ]; - - for more in &self.word.additional { - col = col.push(self.get_view_for_more(more)); - } - - back_overlay( - column![ - col.spacing(DEFAULT_SPACING / 2.0).width(Fill).height(Fill), - row![ - button("Сохранить").style(jl_button).on_press(Save), - button("Удалить").style(danger).on_press(Delete), - ] - .spacing(DEFAULT_SPACING) - ] - .spacing(DEFAULT_SPACING) - .into(), - Back, - ) + // let mut fast_add = row![]; + // if !self.word.additional.contains_key("reading") { + // fast_add = fast_add.push( + // button("Чтение") + // .style(button::text) + // .on_press(AddAdditional("reading".to_string())), + // ); + // } + // if !self.word.additional.contains_key("description") { + // fast_add = fast_add.push( + // button("Описание") + // .style(button::text) + // .on_press(AddAdditional("description".to_string())), + // ); + // } + // + // if !self.word.additional.contains_key("context") { + // fast_add = fast_add.push( + // button("В контексте") + // .style(button::text) + // .on_press(AddAdditional("context".to_string())), + // ); + // } + // + // let mut col = iced::widget::column![ + // column![ + // text!("Ключ"), + // text_input("key", &self.word.key).on_input(SetKey), + // ] + // .spacing(QUARTER_SPACING), + // column![ + // text!("Значение"), + // text_input("value", &self.word.value).on_input(SetValue), + // ] + // .spacing(QUARTER_SPACING), + // column![ + // text!("Теги"), + // text_input("tags", &self.word.tags).on_input(SetTags), + // ] + // .spacing(QUARTER_SPACING), + // rule::horizontal(2), + // scrollable(fast_add), + // ]; + // + // for more in &self.word.additional { + // col = col.push(self.get_view_for_more(more)); + // } + // + // back_overlay( + // column![ + // col.spacing(DEFAULT_SPACING / 2.0).width(Fill).height(Fill), + // row![ + // button("Сохранить").style(jl_button).on_press(Save), + // button("Удалить").style(danger).on_press(Delete), + // ] + // .spacing(DEFAULT_SPACING) + // ] + // .spacing(DEFAULT_SPACING) + // .into(), + // Back, + // ) + space().into() } } impl WordState { - pub(crate) fn new(word: WordData, index: usize, state: Arc>) -> WordState { - WordState { state, index, word } + pub fn new(handle: EntityHandle, state: AppState) -> Self { + Self{ + state, + handle, + } } }