diff --git a/.github/ISSUE_TEMPLATE/bug_report.yaml b/.github/ISSUE_TEMPLATE/bug_report.yaml
index f83a72de9..d8781bb0c 100644
--- a/.github/ISSUE_TEMPLATE/bug_report.yaml
+++ b/.github/ISSUE_TEMPLATE/bug_report.yaml
@@ -6,7 +6,7 @@ body:
     id: desc
     attributes:
       label: Bug Description
-      description: A clear and concise description of what the bug is
+      description: A clear and concise description of what the bug is (if it's a keyboard issue, provide the keyboard mode you're using. e.g. legacy, map, translate)
     validations:
       required: true
   - type: textarea
diff --git a/Cargo.lock b/Cargo.lock
index 1029bfed4..c48a69e8d 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -4,9 +4,9 @@ version = 3
 
 [[package]]
 name = "addr2line"
-version = "0.17.0"
+version = "0.19.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b9ecd88a8c8378ca913a680cd98f0f13ac67383d35993f86c90a70e3f137816b"
+checksum = "a76fd60b23679b7d19bd066031410fb7e458ccc5e958eb5c325888ce4baedc97"
 dependencies = [
  "gimli",
 ]
@@ -17,12 +17,6 @@ version = "1.0.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
 
-[[package]]
-name = "adler32"
-version = "1.2.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "aae1277d39aeec15cb388266ecc24b11c80469deae6067e17a1a7aa9e5c1f234"
-
 [[package]]
 name = "ahash"
 version = "0.7.6"
@@ -111,12 +105,12 @@ dependencies = [
 
 [[package]]
 name = "android_logger"
-version = "0.11.1"
+version = "0.11.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b5e9dd62f37dea550caf48c77591dc50bd1a378ce08855be1a0c42a97b7550fb"
+checksum = "8619b80c242aa7bd638b5c7ddd952addeecb71f69c75e33f1d47b2804f8f883a"
 dependencies = [
  "android_log-sys",
- "env_logger 0.9.3",
+ "env_logger 0.10.0",
  "log",
  "once_cell",
 ]
@@ -141,23 +135,24 @@ dependencies = [
 
 [[package]]
 name = "anyhow"
-version = "1.0.66"
+version = "1.0.69"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "216261ddc8289130e551ddcd5ce8a064710c0d064a4d2895c67151c92b5443f6"
+checksum = "224afbd727c3d6e4b90103ece64b8d1b67fbb1973b1046c2281eed3f3803f800"
 
 [[package]]
 name = "arboard"
-version = "2.1.1"
+version = "3.2.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "dc120354d1b5ec6d7aaf4876b602def75595937b5e15d356eb554ab5177e08bb"
+checksum = "d6041616acea41d67c4a984709ddab1587fd0b10efe5cc563fee954d2f011854"
 dependencies = [
  "clipboard-win",
  "core-graphics 0.22.3",
- "image 0.23.14",
+ "image",
  "log",
  "objc",
  "objc-foundation",
  "objc_id",
+ "once_cell",
  "parking_lot 0.12.1",
  "thiserror",
  "winapi 0.3.9",
@@ -166,13 +161,12 @@ dependencies = [
 
 [[package]]
 name = "async-broadcast"
-version = "0.4.1"
+version = "0.5.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6d26004fe83b2d1cd3a97609b21e39f9a31535822210fe83205d2ce48866ea61"
+checksum = "7c48ccdbf6ca6b121e0f586cbc0e73ae440e56c67c30fa0873b4e110d9c26d2b"
 dependencies = [
  "event-listener",
  "futures-core",
- "parking_lot 0.12.1",
 ]
 
 [[package]]
@@ -200,6 +194,18 @@ dependencies = [
  "slab",
 ]
 
+[[package]]
+name = "async-fs"
+version = "1.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "279cf904654eeebfa37ac9bb1598880884924aab82e290aa65c9e77a0e142e06"
+dependencies = [
+ "async-lock",
+ "autocfg 1.1.0",
+ "blocking",
+ "futures-lite",
+]
+
 [[package]]
 name = "async-io"
 version = "1.12.0"
@@ -215,19 +221,18 @@ dependencies = [
  "parking",
  "polling",
  "slab",
- "socket2 0.4.7",
+ "socket2 0.4.9",
  "waker-fn",
  "windows-sys 0.42.0",
 ]
 
 [[package]]
 name = "async-lock"
-version = "2.6.0"
+version = "2.7.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c8101efe8695a6c17e02911402145357e718ac92d3ff88ae8419e84b1707b685"
+checksum = "fa24f727524730b077666307f2734b4a1a1c57acb79193127dcc8914d5242dd7"
 dependencies = [
  "event-listener",
- "futures-lite",
 ]
 
 [[package]]
@@ -250,13 +255,13 @@ dependencies = [
 
 [[package]]
 name = "async-recursion"
-version = "1.0.0"
+version = "1.0.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2cda8f4bcc10624c4e85bc66b3f452cca98cfa5ca002dc83a16aad2367641bea"
+checksum = "3b015a331cc64ebd1774ba119538573603427eaace0a1950c423ab971f903796"
 dependencies = [
- "proc-macro2 1.0.47",
- "quote 1.0.21",
- "syn 1.0.105",
+ "proc-macro2 1.0.51",
+ "quote 1.0.23",
+ "syn 1.0.109",
 ]
 
 [[package]]
@@ -267,13 +272,13 @@ checksum = "7a40729d2133846d9ed0ea60a8b9541bccddab49cd30f0715a1da672fe9a2524"
 
 [[package]]
 name = "async-trait"
-version = "0.1.59"
+version = "0.1.66"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "31e6e93155431f3931513b243d371981bb2770112b370c82745a1d19d2f99364"
+checksum = "b84f9ebcc6c1f5b8cb160f6990096a5c127f423fcb6e1ccc46c370cbdfb75dfc"
 dependencies = [
- "proc-macro2 1.0.47",
- "quote 1.0.21",
- "syn 1.0.105",
+ "proc-macro2 1.0.51",
+ "quote 1.0.23",
+ "syn 1.0.109",
 ]
 
 [[package]]
@@ -284,7 +289,7 @@ checksum = "39991bc421ddf72f70159011b323ff49b0f783cc676a7287c59453da2e2531cf"
 dependencies = [
  "atk-sys",
  "bitflags",
- "glib 0.16.5",
+ "glib 0.16.7",
  "libc",
 ]
 
@@ -311,9 +316,9 @@ dependencies = [
 
 [[package]]
 name = "atomic-waker"
-version = "1.0.0"
+version = "1.1.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "065374052e7df7ee4047b1160cca5e1467a12351a40b3da123c870ba0b8eda2a"
+checksum = "debc29dde2e69f9e47506b525f639ed42300fc014a3e007832592448fa8e4599"
 
 [[package]]
 name = "atty"
@@ -321,7 +326,7 @@ version = "0.2.14"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8"
 dependencies = [
- "hermit-abi",
+ "hermit-abi 0.1.19",
  "libc",
  "winapi 0.3.9",
 ]
@@ -343,24 +348,30 @@ checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
 
 [[package]]
 name = "backtrace"
-version = "0.3.66"
+version = "0.3.67"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cab84319d616cfb654d03394f38ab7e6f0919e181b1b57e1fd15e7fb4077d9a7"
+checksum = "233d376d6d185f2a3093e58f283f60f880315b6c60075b01f36b3b85154564ca"
 dependencies = [
  "addr2line",
  "cc",
  "cfg-if 1.0.0",
  "libc",
- "miniz_oxide 0.5.4",
+ "miniz_oxide",
  "object",
  "rustc-demangle",
 ]
 
 [[package]]
-name = "base64"
-version = "0.13.1"
+name = "base-x"
+version = "0.2.11"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8"
+checksum = "4cbbc9d0964165b47557570cce6c952866c2678457aca742aafc9fb771d30270"
+
+[[package]]
+name = "base64"
+version = "0.21.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a4a4ddaa51a5bc52a6948f74c06d20aaaddb71924eab79b8c97a8c556e942d6a"
 
 [[package]]
 name = "bindgen"
@@ -377,12 +388,12 @@ dependencies = [
  "lazycell",
  "log",
  "peeking_take_while",
- "proc-macro2 1.0.47",
- "quote 1.0.21",
+ "proc-macro2 1.0.51",
+ "quote 1.0.23",
  "regex",
  "rustc-hash",
  "shlex",
- "which 4.3.0",
+ "which",
 ]
 
 [[package]]
@@ -397,19 +408,41 @@ dependencies = [
  "lazy_static",
  "lazycell",
  "peeking_take_while",
- "proc-macro2 1.0.47",
- "quote 1.0.21",
+ "proc-macro2 1.0.51",
+ "quote 1.0.23",
  "regex",
  "rustc-hash",
  "shlex",
- "syn 1.0.105",
+ "syn 1.0.109",
+]
+
+[[package]]
+name = "bindgen"
+version = "0.64.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c4243e6031260db77ede97ad86c27e501d646a27ab57b59a574f725d98ab1fb4"
+dependencies = [
+ "bitflags",
+ "cexpr",
+ "clang-sys",
+ "lazy_static",
+ "lazycell",
+ "log",
+ "peeking_take_while",
+ "proc-macro2 1.0.51",
+ "quote 1.0.23",
+ "regex",
+ "rustc-hash",
+ "shlex",
+ "syn 1.0.109",
+ "which",
 ]
 
 [[package]]
 name = "bit_field"
-version = "0.10.1"
+version = "0.10.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "dcb6dd1c2376d2e096796e234a70e17e94cc2d5d54ff8ce42b28cef1d0d359a4"
+checksum = "dc827186963e592360843fb5ba4b973e145841266c1357f7180c43526f2e5b61"
 
 [[package]]
 name = "bitflags"
@@ -437,9 +470,9 @@ checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a"
 
 [[package]]
 name = "block-buffer"
-version = "0.10.3"
+version = "0.10.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "69cce20737498f97b993470a6e536b8523f0af7892a4f928cceb1ac5e52ebe7e"
+checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
 dependencies = [
  "generic-array",
 ]
@@ -471,9 +504,9 @@ dependencies = [
 
 [[package]]
 name = "brotli-decompressor"
-version = "2.3.2"
+version = "2.3.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "59ad2d4653bf5ca36ae797b1f4bb4dbddb60ce49ca4aed8a2ce4829f60425b80"
+checksum = "4b6561fd3f895a11e8f72af2cb7d22e08366bebc2b6b57f7744c4bda27034744"
 dependencies = [
  "alloc-no-stdlib",
  "alloc-stdlib",
@@ -487,15 +520,15 @@ checksum = "832133bbabbbaa9fbdba793456a2827627a7d2b8fb96032fa1e7666d7895832b"
 
 [[package]]
 name = "bumpalo"
-version = "3.11.1"
+version = "3.12.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "572f695136211188308f16ad2ca5c851a712c464060ae6974944458eb83880ba"
+checksum = "0d261e256854913907f67ed06efbc3338dfe6179796deefc1ff763fc1aee5535"
 
 [[package]]
 name = "bytemuck"
-version = "1.12.3"
+version = "1.13.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "aaa3a8d9a1ca92e282c96a32d6511b695d7d994d1d102ba85d279f9b2756947f"
+checksum = "17febce684fd15d89027105661fec94afb475cb995fbc59d2865198446ba2eea"
 
 [[package]]
 name = "byteorder"
@@ -505,11 +538,11 @@ checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610"
 
 [[package]]
 name = "bytes"
-version = "1.3.0"
+version = "1.4.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "dfb24e866b15a1af2a1b663f10c6b6b8f397a84aadb828f12e5b289ec23a3a3c"
+checksum = "89b2fd2a0dcf38d7971e2194b6b6eebab45ae01067456a7fd93d5547a61b70be"
 dependencies = [
- "serde 1.0.149",
+ "serde 1.0.154",
 ]
 
 [[package]]
@@ -520,7 +553,7 @@ checksum = "f3125b15ec28b84c238f6f476c6034016a5f6cc0221cb514ca46c532139fc97d"
 dependencies = [
  "bitflags",
  "cairo-sys-rs",
- "glib 0.16.5",
+ "glib 0.16.7",
  "libc",
  "once_cell",
  "thiserror",
@@ -549,11 +582,11 @@ dependencies = [
 
 [[package]]
 name = "camino"
-version = "1.1.1"
+version = "1.1.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "88ad0e1e3e88dd237a156ab9f571021b8a158caa0ae44b1968a241efb5144c1e"
+checksum = "6031a462f977dd38968b6f23378356512feeace69cef817e1a4475108093cec3"
 dependencies = [
- "serde 1.0.149",
+ "serde 1.0.154",
 ]
 
 [[package]]
@@ -562,7 +595,7 @@ version = "0.1.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "cbdb825da8a5df079a43676dbe042702f1707b1109f713a01420fbb4cc71fa27"
 dependencies = [
- "serde 1.0.149",
+ "serde 1.0.154",
 ]
 
 [[package]]
@@ -573,9 +606,9 @@ checksum = "4acbb09d9ee8e23699b9634375c72795d095bf268439da88562cf9b501f181fa"
 dependencies = [
  "camino",
  "cargo-platform",
- "semver 1.0.14",
- "serde 1.0.149",
- "serde_json 1.0.89",
+ "semver 1.0.16",
+ "serde 1.0.154",
+ "serde_json 1.0.94",
 ]
 
 [[package]]
@@ -585,23 +618,23 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "a6358dedf60f4d9b8db43ad187391afe959746101346fe51bb978126bec61dfb"
 dependencies = [
  "clap 3.2.23",
- "heck 0.4.0",
+ "heck 0.4.1",
  "indexmap",
  "log",
- "proc-macro2 1.0.47",
- "quote 1.0.21",
- "serde 1.0.149",
- "serde_json 1.0.89",
- "syn 1.0.105",
+ "proc-macro2 1.0.51",
+ "quote 1.0.23",
+ "serde 1.0.154",
+ "serde_json 1.0.94",
+ "syn 1.0.109",
  "tempfile",
- "toml",
+ "toml 0.5.11",
 ]
 
 [[package]]
 name = "cc"
-version = "1.0.77"
+version = "1.0.79"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e9f73505338f7d905b19d18738976aae232eb46b8efc15554ffc56deb5d9ebe4"
+checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f"
 dependencies = [
  "jobserver",
 ]
@@ -659,9 +692,9 @@ dependencies = [
 
 [[package]]
 name = "cidr-utils"
-version = "0.5.9"
+version = "0.5.10"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "355d5b5df67e58b523953d0c1a8d3d2c05f5af51f1332b0199b9c92263614ed0"
+checksum = "fdfa36f04861d39453affe1cf084ce2d6554021a84eb6f31ebdeafb6fb92a01c"
 dependencies = [
  "debug-helper",
  "num-bigint",
@@ -672,9 +705,9 @@ dependencies = [
 
 [[package]]
 name = "clang-sys"
-version = "1.4.0"
+version = "1.6.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fa2e27ae6ab525c3d369ded447057bca5438d86dc3a68f6faafb8269ba82ebf3"
+checksum = "77ed9a53e5d4d9c573ae844bfac6872b159cb1d1585a83b29e7a64b7eef7332a"
 dependencies = [
  "glob",
  "libc",
@@ -705,7 +738,7 @@ dependencies = [
  "atty",
  "bitflags",
  "clap_derive",
- "clap_lex",
+ "clap_lex 0.2.4",
  "indexmap",
  "once_cell",
  "strsim 0.10.0",
@@ -713,17 +746,30 @@ dependencies = [
  "textwrap 0.16.0",
 ]
 
+[[package]]
+name = "clap"
+version = "4.1.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c3d7ae14b20b94cb02149ed21a86c423859cbe18dc7ed69845cace50e52b40a5"
+dependencies = [
+ "bitflags",
+ "clap_lex 0.3.2",
+ "is-terminal",
+ "strsim 0.10.0",
+ "termcolor",
+]
+
 [[package]]
 name = "clap_derive"
 version = "3.2.18"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "ea0c8bce528c4be4da13ea6fead8965e95b6073585a2f05204bd8f4119f82a65"
 dependencies = [
- "heck 0.4.0",
+ "heck 0.4.1",
  "proc-macro-error",
- "proc-macro2 1.0.47",
- "quote 1.0.21",
- "syn 1.0.105",
+ "proc-macro2 1.0.51",
+ "quote 1.0.23",
+ "syn 1.0.109",
 ]
 
 [[package]]
@@ -735,6 +781,15 @@ dependencies = [
  "os_str_bytes",
 ]
 
+[[package]]
+name = "clap_lex"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "350b9cf31731f9957399229e9b2adc51eeabdfbe9d71d9a0552275fd12710d09"
+dependencies = [
+ "os_str_bytes",
+]
+
 [[package]]
 name = "clipboard"
 version = "0.1.0"
@@ -742,16 +797,16 @@ dependencies = [
  "cc",
  "hbb_common",
  "lazy_static",
- "serde 1.0.149",
+ "serde 1.0.154",
  "serde_derive",
  "thiserror",
 ]
 
 [[package]]
 name = "clipboard-win"
-version = "4.4.2"
+version = "4.5.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c4ab1b92798304eedc095b53942963240037c0516452cb11aeba709d420b2219"
+checksum = "7191c27c2357d9b7ef96baac1773290d4ca63b24205b82a3fd8a0637afcf0362"
 dependencies = [
  "error-code",
  "str-buf",
@@ -823,6 +878,17 @@ version = "1.1.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b"
 
+[[package]]
+name = "colored"
+version = "1.9.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f4ffc801dacf156c5854b9df4f425a626539c3a6ef7893cc0c5084a23f0b6c59"
+dependencies = [
+ "atty",
+ "lazy_static",
+ "winapi 0.3.9",
+]
+
 [[package]]
 name = "combine"
 version = "4.6.6"
@@ -835,9 +901,9 @@ dependencies = [
 
 [[package]]
 name = "concurrent-queue"
-version = "2.0.0"
+version = "2.1.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bd7bef69dc86e3c610e4e7aed41035e2a7ed12e72dd7530f61327a6579a4390b"
+checksum = "c278839b831783b70278b14df4d45e1beb1aad306c07bb796637de9a0e323e8e"
 dependencies = [
  "crossbeam-utils",
 ]
@@ -848,9 +914,9 @@ version = "0.4.0"
 source = "git+https://github.com/open-trade/confy#630cc28a396cb7d01eefdd9f3824486fe4d8554b"
 dependencies = [
  "directories-next",
- "serde 1.0.149",
+ "serde 1.0.154",
  "thiserror",
- "toml",
+ "toml 0.5.11",
 ]
 
 [[package]]
@@ -972,27 +1038,26 @@ dependencies = [
 
 [[package]]
 name = "cpal"
-version = "0.13.5"
+version = "0.14.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "74117836a5124f3629e4b474eed03e479abaf98988b4bb317e29f08cfe0e4116"
+checksum = "f342c1b63e185e9953584ff2199726bf53850d96610a310e3aca09e9405a2d0b"
 dependencies = [
  "alsa",
  "core-foundation-sys 0.8.3",
  "coreaudio-rs",
  "jni 0.19.0",
  "js-sys",
- "lazy_static",
  "libc",
  "mach",
- "ndk 0.6.0",
- "ndk-glue 0.6.2",
- "nix 0.23.2",
+ "ndk 0.7.0",
+ "ndk-context",
  "oboe",
- "parking_lot 0.11.2",
+ "once_cell",
+ "parking_lot 0.12.1",
  "stdweb",
  "thiserror",
  "web-sys",
- "winapi 0.3.9",
+ "windows 0.37.0",
 ]
 
 [[package]]
@@ -1015,9 +1080,9 @@ dependencies = [
 
 [[package]]
 name = "crossbeam-channel"
-version = "0.5.6"
+version = "0.5.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c2dd04ddaf88237dc3b8d8f9a3c1004b506b54b3313403944054d23c0870c521"
+checksum = "cf2b3e8478797446514c91ef04bafcb59faba183e621ad488df88983cc14128c"
 dependencies = [
  "cfg-if 1.0.0",
  "crossbeam-utils",
@@ -1025,9 +1090,9 @@ dependencies = [
 
 [[package]]
 name = "crossbeam-deque"
-version = "0.8.2"
+version = "0.8.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "715e8152b692bba2d374b53d4875445368fdf21a94751410af607a5ac677d1fc"
+checksum = "ce6fd6f855243022dcecf8702fef0c297d4338e226845fe067f6341ad9fa0cef"
 dependencies = [
  "cfg-if 1.0.0",
  "crossbeam-epoch",
@@ -1036,14 +1101,14 @@ dependencies = [
 
 [[package]]
 name = "crossbeam-epoch"
-version = "0.9.13"
+version = "0.9.14"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "01a9af1f4c2ef74bb8aa1f7e19706bc72d03598c8a570bb5de72243c7a9d9d5a"
+checksum = "46bd5f3f85273295a9d14aedfb86f6aadbff6d8f5295c4a9edb08e819dcf5695"
 dependencies = [
  "autocfg 1.1.0",
  "cfg-if 1.0.0",
  "crossbeam-utils",
- "memoffset 0.7.1",
+ "memoffset 0.8.0",
  "scopeguard",
 ]
 
@@ -1059,9 +1124,9 @@ dependencies = [
 
 [[package]]
 name = "crossbeam-utils"
-version = "0.8.14"
+version = "0.8.15"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4fb766fa798726286dbbb842f174001dab8abc7b627a1dd86e0b7222a95d929f"
+checksum = "3c063cd8cc95f5c377ed0d4b49a4b21f632396ff690e8470c29b3359b346984b"
 dependencies = [
  "cfg-if 1.0.0",
 ]
@@ -1084,12 +1149,12 @@ dependencies = [
 
 [[package]]
 name = "ctrlc"
-version = "3.2.4"
+version = "3.2.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1631ca6e3c59112501a9d87fd86f21591ff77acd31331e8a73f8d80a65bbdd71"
+checksum = "bbcf33c2a618cbe41ee43ae6e9f2e48368cd9f9db2896f10167d8d762679f639"
 dependencies = [
- "nix 0.26.1",
- "windows-sys 0.42.0",
+ "nix 0.26.2",
+ "windows-sys 0.45.0",
 ]
 
 [[package]]
@@ -1100,9 +1165,9 @@ checksum = "b365fabc795046672053e29c954733ec3b05e4be654ab130fe8f1f94d7051f35"
 
 [[package]]
 name = "cxx"
-version = "1.0.83"
+version = "1.0.92"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bdf07d07d6531bfcdbe9b8b739b104610c6508dcc4d63b410585faf338241daf"
+checksum = "9a140f260e6f3f79013b8bfc65e7ce630c9ab4388c6a89c71e07226f49487b72"
 dependencies = [
  "cc",
  "cxxbridge-flags",
@@ -1112,34 +1177,34 @@ dependencies = [
 
 [[package]]
 name = "cxx-build"
-version = "1.0.83"
+version = "1.0.92"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d2eb5b96ecdc99f72657332953d4d9c50135af1bac34277801cc3937906ebd39"
+checksum = "da6383f459341ea689374bf0a42979739dc421874f112ff26f829b8040b8e613"
 dependencies = [
  "cc",
  "codespan-reporting",
  "once_cell",
- "proc-macro2 1.0.47",
- "quote 1.0.21",
+ "proc-macro2 1.0.51",
+ "quote 1.0.23",
  "scratch",
- "syn 1.0.105",
+ "syn 1.0.109",
 ]
 
 [[package]]
 name = "cxxbridge-flags"
-version = "1.0.83"
+version = "1.0.92"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ac040a39517fd1674e0f32177648334b0f4074625b5588a64519804ba0553b12"
+checksum = "90201c1a650e95ccff1c8c0bb5a343213bdd317c6e600a93075bca2eff54ec97"
 
 [[package]]
 name = "cxxbridge-macro"
-version = "1.0.83"
+version = "1.0.92"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1362b0ddcfc4eb0a1f57b68bd77dd99f0e826958a96abd0ae9bd092e114ffed6"
+checksum = "0b75aed41bb2e6367cae39e6326ef817a851db13c13e4f3263714ca3cfb8de56"
 dependencies = [
- "proc-macro2 1.0.47",
- "quote 1.0.21",
- "syn 1.0.105",
+ "proc-macro2 1.0.51",
+ "quote 1.0.23",
+ "syn 1.0.109",
 ]
 
 [[package]]
@@ -1187,10 +1252,10 @@ checksum = "f0c960ae2da4de88a91b2d920c2a7233b400bc33cb28453a2987822d8392519b"
 dependencies = [
  "fnv",
  "ident_case",
- "proc-macro2 1.0.47",
- "quote 1.0.21",
+ "proc-macro2 1.0.51",
+ "quote 1.0.23",
  "strsim 0.9.3",
- "syn 1.0.105",
+ "syn 1.0.109",
 ]
 
 [[package]]
@@ -1201,10 +1266,10 @@ checksum = "859d65a907b6852c9361e3185c862aae7fafd2887876799fa55f5f99dc40d610"
 dependencies = [
  "fnv",
  "ident_case",
- "proc-macro2 1.0.47",
- "quote 1.0.21",
+ "proc-macro2 1.0.51",
+ "quote 1.0.23",
  "strsim 0.10.0",
- "syn 1.0.105",
+ "syn 1.0.109",
 ]
 
 [[package]]
@@ -1214,8 +1279,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "d9b5a2f4ac4969822c62224815d069952656cadc7084fdca9751e6d959189b72"
 dependencies = [
  "darling_core 0.10.2",
- "quote 1.0.21",
- "syn 1.0.105",
+ "quote 1.0.23",
+ "syn 1.0.109",
 ]
 
 [[package]]
@@ -1225,8 +1290,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "9c972679f83bdf9c42bd905396b6c3588a843a17f0f16dfcfa3e2c5d57441835"
 dependencies = [
  "darling_core 0.13.4",
- "quote 1.0.21",
- "syn 1.0.105",
+ "quote 1.0.23",
+ "syn 1.0.109",
+]
+
+[[package]]
+name = "dart-sys"
+version = "4.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8d8b5680b5c2cc52f50acb2457d9b3a3b58adcca785db13a0e3655626f601de6"
+dependencies = [
+ "cc",
 ]
 
 [[package]]
@@ -1350,9 +1424,9 @@ dependencies = [
 
 [[package]]
 name = "dbus"
-version = "0.9.6"
+version = "0.9.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6f8bcdd56d2e5c4ed26a529c5a9029f5db8290d433497506f958eae3be148eb6"
+checksum = "1bb21987b9fb1613058ba3843121dd18b163b254d8a6e797e144cbac14d96d1b"
 dependencies = [
  "libc",
  "libdbus-sys",
@@ -1361,9 +1435,9 @@ dependencies = [
 
 [[package]]
 name = "dbus-crossroads"
-version = "0.5.1"
+version = "0.5.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "554114296d012b33fdaf362a733db6dc5f73c4c9348b8b620ddd42e61b406e30"
+checksum = "3a4c83437187544ba5142427746835061b330446ca8902eabd70e4afb8f76de0"
 dependencies = [
  "dbus",
 ]
@@ -1392,25 +1466,15 @@ dependencies = [
  "windows 0.30.0",
 ]
 
-[[package]]
-name = "deflate"
-version = "0.8.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "73770f8e1fe7d64df17ca66ad28994a0a623ea497fa69486e14984e715c5d174"
-dependencies = [
- "adler32",
- "byteorder",
-]
-
 [[package]]
 name = "delegate"
 version = "0.8.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "082a24a9967533dc5d743c602157637116fc1b52806d694a5a45e6f32567fcdd"
 dependencies = [
- "proc-macro2 1.0.47",
- "quote 1.0.21",
- "syn 1.0.105",
+ "proc-macro2 1.0.51",
+ "quote 1.0.23",
+ "syn 1.0.109",
 ]
 
 [[package]]
@@ -1419,9 +1483,9 @@ version = "2.2.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b"
 dependencies = [
- "proc-macro2 1.0.47",
- "quote 1.0.21",
- "syn 1.0.105",
+ "proc-macro2 1.0.51",
+ "quote 1.0.23",
+ "syn 1.0.109",
 ]
 
 [[package]]
@@ -1431,9 +1495,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "f1cf41b4580a37cca5ef2ada2cc43cf5d6be3983f4522e83010d67ab6925e84b"
 dependencies = [
  "darling 0.10.2",
- "proc-macro2 1.0.47",
- "quote 1.0.21",
- "syn 1.0.105",
+ "proc-macro2 1.0.51",
+ "quote 1.0.23",
+ "syn 1.0.109",
 ]
 
 [[package]]
@@ -1513,6 +1577,12 @@ dependencies = [
  "winapi 0.3.9",
 ]
 
+[[package]]
+name = "discard"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "212d0f5754cb6769937f4501cc0e67f4f4483c8d2c3e1e922ee9edbe4ab4c7c0"
+
 [[package]]
 name = "dispatch"
 version = "0.2.0"
@@ -1565,7 +1635,7 @@ checksum = "7f3f119846c823f9eafcf953a8f6ffb6ed69bf6240883261a7f13b634579a51f"
 dependencies = [
  "lazy_static",
  "regex",
- "serde 1.0.149",
+ "serde 1.0.154",
  "strsim 0.10.0",
 ]
 
@@ -1588,25 +1658,25 @@ dependencies = [
  "cc",
  "hbb_common",
  "lazy_static",
- "serde 1.0.149",
+ "serde 1.0.154",
  "serde_derive",
  "thiserror",
 ]
 
 [[package]]
 name = "ed25519"
-version = "1.5.2"
+version = "1.5.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1e9c280362032ea4203659fc489832d0204ef09f247a0506f170dafcac08c369"
+checksum = "91cff35c70bba8a626e3185d8cd48cc11b5437e1a5bcd15b9b5fa3c64b6dfee7"
 dependencies = [
  "signature",
 ]
 
 [[package]]
 name = "either"
-version = "1.8.0"
+version = "1.8.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "90e5c1c8368803113bf0c9584fc495a58b86dc8a29edbf8fe877d21d9507e797"
+checksum = "7fcaabb2fef8c910e7f4c7ce9f67a1283a1715879a7c230ca9d6d1ae31f16d91"
 
 [[package]]
 name = "embed-resource"
@@ -1616,16 +1686,16 @@ checksum = "e62abb876c07e4754fae5c14cafa77937841f01740637e17d78dc04352f32a5e"
 dependencies = [
  "cc",
  "rustc_version 0.4.0",
- "toml",
+ "toml 0.5.11",
  "vswhom",
  "winreg 0.10.1",
 ]
 
 [[package]]
 name = "encoding_rs"
-version = "0.8.31"
+version = "0.8.32"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9852635589dc9f9ea1b6fe9f05b50ef208c85c834a562f0c6abb1c475736ec2b"
+checksum = "071a31f4ee85403370b58aca746f01041ede6f0da2730960ad001edc2b71b394"
 dependencies = [
  "cfg-if 1.0.0",
 ]
@@ -1640,7 +1710,7 @@ dependencies = [
  "objc",
  "pkg-config",
  "rdev",
- "serde 1.0.149",
+ "serde 1.0.154",
  "serde_derive",
  "tfc",
  "unicode-segmentation",
@@ -1649,34 +1719,34 @@ dependencies = [
 
 [[package]]
 name = "enum-map"
-version = "2.4.1"
+version = "2.4.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f5a56d54c8dd9b3ad34752ed197a4eb2a6601bc010808eb097a04a58ae4c43e1"
+checksum = "50c25992259941eb7e57b936157961b217a4fc8597829ddef0596d6c3cd86e1a"
 dependencies = [
  "enum-map-derive",
 ]
 
 [[package]]
 name = "enum-map-derive"
-version = "0.10.0"
+version = "0.11.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a9045e2676cd5af83c3b167d917b0a5c90a4d8e266e2683d6631b235c457fc27"
+checksum = "2a4da76b3b6116d758c7ba93f7ec6a35d2e2cf24feda76c6e38a375f4d5c59f2"
 dependencies = [
- "proc-macro2 1.0.47",
- "quote 1.0.21",
- "syn 1.0.105",
+ "proc-macro2 1.0.51",
+ "quote 1.0.23",
+ "syn 1.0.109",
 ]
 
 [[package]]
 name = "enum_dispatch"
-version = "0.3.8"
+version = "0.3.11"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0eb359f1476bf611266ac1f5355bc14aeca37b299d0ebccc038ee7058891c9cb"
+checksum = "11f36e95862220b211a6e2aa5eca09b4fa391b13cd52ceb8035a24bf65a79de2"
 dependencies = [
  "once_cell",
- "proc-macro2 1.0.47",
- "quote 1.0.21",
- "syn 1.0.105",
+ "proc-macro2 1.0.51",
+ "quote 1.0.23",
+ "syn 1.0.109",
 ]
 
 [[package]]
@@ -1686,7 +1756,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "e75d4cd21b95383444831539909fbb14b9dc3fdceb2a6f5d36577329a1f55ccb"
 dependencies = [
  "enumflags2_derive",
- "serde 1.0.149",
+ "serde 1.0.154",
 ]
 
 [[package]]
@@ -1695,9 +1765,9 @@ version = "0.7.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "f58dc3c5e468259f19f2d46304a6b28f1c3d034442e14b322d2b850e36f6d5ae"
 dependencies = [
- "proc-macro2 1.0.47",
- "quote 1.0.21",
- "syn 1.0.105",
+ "proc-macro2 1.0.51",
+ "quote 1.0.23",
+ "syn 1.0.109",
 ]
 
 [[package]]
@@ -1723,6 +1793,19 @@ dependencies = [
  "termcolor",
 ]
 
+[[package]]
+name = "env_logger"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "85cdab6a89accf66733ad5a1693a4dcced6aeff64602b634530dd73c1f3ee9f0"
+dependencies = [
+ "humantime",
+ "is-terminal",
+ "log",
+ "regex",
+ "termcolor",
+]
+
 [[package]]
 name = "epoll"
 version = "4.3.1"
@@ -1740,10 +1823,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "c34a887c8df3ed90498c1c437ce21f211c8e27672921a8ffa293cb8d6d4caa9e"
 dependencies = [
  "proc-macro-error",
- "proc-macro2 1.0.47",
- "quote 1.0.21",
+ "proc-macro2 1.0.51",
+ "quote 1.0.23",
  "rustversion",
- "syn 1.0.105",
+ "syn 1.0.109",
  "synstructure",
 ]
 
@@ -1758,6 +1841,17 @@ dependencies = [
  "winapi 0.3.9",
 ]
 
+[[package]]
+name = "errno"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "50d6a0976c999d473fe89ad888d5a284e55366d9dc9038b1ba2aa15128c4afa0"
+dependencies = [
+ "errno-dragonfly",
+ "libc",
+ "windows-sys 0.45.0",
+]
+
 [[package]]
 name = "errno-dragonfly"
 version = "0.1.2"
@@ -1804,62 +1898,52 @@ dependencies = [
  "flume",
  "half",
  "lebe",
- "miniz_oxide 0.6.2",
+ "miniz_oxide",
  "smallvec",
  "threadpool",
  "zune-inflate",
 ]
 
-[[package]]
-name = "extend"
-version = "1.1.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5c5216e387a76eebaaf11f6d871ec8a4aae0b25f05456ee21f228e024b1b3610"
-dependencies = [
- "proc-macro-error",
- "proc-macro2 1.0.47",
- "quote 1.0.21",
- "syn 1.0.105",
-]
-
-[[package]]
-name = "failure"
-version = "0.1.8"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d32e9bd16cc02eae7db7ef620b392808b89f6a5e16bb3497d159c6b92a0f4f86"
-dependencies = [
- "backtrace",
-]
-
 [[package]]
 name = "fastrand"
-version = "1.8.0"
+version = "1.9.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a7a407cfaa3385c4ae6b23e84623d48c2798d06e3e6a1878f7f59f17b3f86499"
+checksum = "e51093e27b0797c359783294ca4f0a911c270184cb10f85783b118614a1501be"
 dependencies = [
  "instant",
 ]
 
 [[package]]
-name = "field-offset"
-version = "0.3.4"
+name = "fern"
+version = "0.6.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1e1c54951450cbd39f3dbcf1005ac413b49487dabf18a720ad2383eccfeffb92"
+checksum = "3bdd7b0849075e79ee9a1836df22c717d1eba30451796fdc631b04565dd11e2a"
 dependencies = [
- "memoffset 0.6.5",
- "rustc_version 0.3.3",
+ "chrono",
+ "colored",
+ "log",
+]
+
+[[package]]
+name = "field-offset"
+version = "0.3.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a3cf3a800ff6e860c863ca6d4b16fd999db8b752819c1606884047b73e468535"
+dependencies = [
+ "memoffset 0.8.0",
+ "rustc_version 0.4.0",
 ]
 
 [[package]]
 name = "filetime"
-version = "0.2.19"
+version = "0.2.20"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4e884668cd0c7480504233e951174ddc3b382f7c2666e3b7310b5c4e7b0c37f9"
+checksum = "8a3de6e8d11b22ff9edc6d916f890800597d60f8b2da1caf2955c274638d6412"
 dependencies = [
  "cfg-if 1.0.0",
  "libc",
  "redox_syscall",
- "windows-sys 0.42.0",
+ "windows-sys 0.45.0",
 ]
 
 [[package]]
@@ -1869,27 +1953,25 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "a8a2db397cb1c8772f31494cb8917e48cd1e64f0fa7efac59fbd741a0a8ce841"
 dependencies = [
  "crc32fast",
- "miniz_oxide 0.6.2",
+ "miniz_oxide",
 ]
 
 [[package]]
 name = "flexi_logger"
-version = "0.22.6"
+version = "0.25.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0c76a80dd14a27fc3d8bc696502132cb52b3f227256fd8601166c3a35e45f409"
+checksum = "6eae57842a8221ef13f1f207632d786a175dd13bd8fbdc8be9d852f7c9cf1046"
 dependencies = [
- "ansi_term",
- "atty",
  "chrono",
  "crossbeam-channel",
  "crossbeam-queue",
  "glob",
+ "is-terminal",
  "lazy_static",
  "log",
+ "nu-ansi-term",
  "regex",
- "rustversion",
  "thiserror",
- "time 0.3.9",
 ]
 
 [[package]]
@@ -1907,9 +1989,9 @@ dependencies = [
 
 [[package]]
 name = "flutter_rust_bridge"
-version = "1.61.1"
+version = "1.68.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8079119bbe8fb63d7ebb731fa2aa68c6c8375f4ac95ca26d5868e64c0f4b9244"
+checksum = "54f0d71ff30fc2ae7c18508b517488a89051d81e3bfc4d48d4a6cf54b5dab789"
 dependencies = [
  "allo-isolate",
  "anyhow",
@@ -1918,6 +2000,7 @@ dependencies = [
  "cc",
  "chrono",
  "console_error_panic_hook",
+ "dart-sys",
  "flutter_rust_bridge_macros",
  "js-sys",
  "lazy_static",
@@ -1931,39 +2014,41 @@ dependencies = [
 
 [[package]]
 name = "flutter_rust_bridge_codegen"
-version = "1.61.1"
+version = "1.68.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "efd7396bc479eae8aa24243e4c0e3d3dbda1909134f8de6bde4f080d262c9a0d"
+checksum = "2a2a75a72411f0c5b480e4671417f52780172053128cf87d5614a9757d7680a0"
 dependencies = [
  "anyhow",
+ "atty",
  "cargo_metadata",
  "cbindgen",
+ "chrono",
  "clap 3.2.23",
  "convert_case",
  "delegate",
  "enum_dispatch",
- "env_logger 0.9.3",
- "extend",
+ "fern",
  "itertools 0.10.5",
  "lazy_static",
  "log",
  "pathdiff",
- "quote 1.0.21",
+ "quote 1.0.23",
  "regex",
- "serde 1.0.149",
+ "serde 1.0.154",
  "serde_yaml",
- "syn 1.0.105",
+ "strum_macros 0.24.3",
+ "syn 1.0.109",
  "tempfile",
  "thiserror",
- "toml",
+ "toml 0.5.11",
  "topological-sort",
 ]
 
 [[package]]
 name = "flutter_rust_bridge_macros"
-version = "1.61.1"
+version = "1.68.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8d5cd827645690ef378be57a890d0581e17c28d07b712872af7d744f454fd27d"
+checksum = "f6187d1635afede47c23a9979f85c3dc57e3391eb9c690b7fe95715bf945da72"
 
 [[package]]
 name = "fnv"
@@ -2038,9 +2123,9 @@ checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c"
 
 [[package]]
 name = "futures"
-version = "0.3.25"
+version = "0.3.26"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "38390104763dc37a5145a53c29c63c1290b5d316d6086ec32c293f6736051bb0"
+checksum = "13e2792b0ff0340399d58445b88fd9770e3489eff258a4cbc1523418f12abf84"
 dependencies = [
  "futures-channel",
  "futures-core",
@@ -2053,9 +2138,9 @@ dependencies = [
 
 [[package]]
 name = "futures-channel"
-version = "0.3.25"
+version = "0.3.26"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "52ba265a92256105f45b719605a571ffe2d1f0fea3807304b522c1d778f79eed"
+checksum = "2e5317663a9089767a1ec00a487df42e0ca174b61b4483213ac24448e4664df5"
 dependencies = [
  "futures-core",
  "futures-sink",
@@ -2063,15 +2148,15 @@ dependencies = [
 
 [[package]]
 name = "futures-core"
-version = "0.3.25"
+version = "0.3.26"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "04909a7a7e4633ae6c4a9ab280aeb86da1236243a77b694a49eacd659a4bd3ac"
+checksum = "ec90ff4d0fe1f57d600049061dc6bb68ed03c7d2fbd697274c41805dcb3f8608"
 
 [[package]]
 name = "futures-executor"
-version = "0.3.25"
+version = "0.3.26"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7acc85df6714c176ab5edf386123fafe217be88c0840ec11f199441134a074e2"
+checksum = "e8de0a35a6ab97ec8869e32a2473f4b1324459e14c29275d14b10cb1fd19b50e"
 dependencies = [
  "futures-core",
  "futures-task",
@@ -2080,9 +2165,9 @@ dependencies = [
 
 [[package]]
 name = "futures-io"
-version = "0.3.25"
+version = "0.3.26"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "00f5fb52a06bdcadeb54e8d3671f8888a39697dcb0b81b23b55174030427f4eb"
+checksum = "bfb8371b6fb2aeb2d280374607aeabfc99d95c72edfe51692e42d3d7f0d08531"
 
 [[package]]
 name = "futures-lite"
@@ -2101,32 +2186,32 @@ dependencies = [
 
 [[package]]
 name = "futures-macro"
-version = "0.3.25"
+version = "0.3.26"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bdfb8ce053d86b91919aad980c220b1fb8401a9394410e1c289ed7e66b61835d"
+checksum = "95a73af87da33b5acf53acfebdc339fe592ecf5357ac7c0a7734ab9d8c876a70"
 dependencies = [
- "proc-macro2 1.0.47",
- "quote 1.0.21",
- "syn 1.0.105",
+ "proc-macro2 1.0.51",
+ "quote 1.0.23",
+ "syn 1.0.109",
 ]
 
 [[package]]
 name = "futures-sink"
-version = "0.3.25"
+version = "0.3.26"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "39c15cf1a4aa79df40f1bb462fb39676d0ad9e366c2a33b590d7c66f4f81fcf9"
+checksum = "f310820bb3e8cfd46c80db4d7fb8353e15dfff853a127158425f31e0be6c8364"
 
 [[package]]
 name = "futures-task"
-version = "0.3.25"
+version = "0.3.26"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2ffb393ac5d9a6eaa9d3fdf37ae2776656b706e200c8e16b1bdb227f5198e6ea"
+checksum = "dcf79a1bf610b10f42aea489289c5a2c478a786509693b80cd39c44ccd936366"
 
 [[package]]
 name = "futures-util"
-version = "0.3.25"
+version = "0.3.26"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "197676987abd2f9cadff84926f410af1c183608d36641465df73ae8211dc65d6"
+checksum = "9c1d6de3acfef38d2be4b1f543f553131788603495be83da675e180c8d6b7bd1"
 dependencies = [
  "futures-channel",
  "futures-core",
@@ -2140,15 +2225,6 @@ dependencies = [
  "slab",
 ]
 
-[[package]]
-name = "fxhash"
-version = "0.2.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c"
-dependencies = [
- "byteorder",
-]
-
 [[package]]
 name = "gdk"
 version = "0.16.2"
@@ -2160,7 +2236,7 @@ dependencies = [
  "gdk-pixbuf",
  "gdk-sys",
  "gio",
- "glib 0.16.5",
+ "glib 0.16.7",
  "libc",
  "pango",
 ]
@@ -2174,7 +2250,7 @@ dependencies = [
  "bitflags",
  "gdk-pixbuf-sys",
  "gio",
- "glib 0.16.5",
+ "glib 0.16.7",
  "libc",
 ]
 
@@ -2232,7 +2308,7 @@ dependencies = [
  "glib-sys 0.16.3",
  "libc",
  "system-deps 6.0.3",
- "x11 2.20.1",
+ "x11 2.21.0",
 ]
 
 [[package]]
@@ -2280,9 +2356,9 @@ dependencies = [
 
 [[package]]
 name = "gimli"
-version = "0.26.2"
+version = "0.27.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "22030e2c5a68ec659fde1e949a745124b48e6fa8b045b7ed5bd1fe4ccc5c4e5d"
+checksum = "ad0a93d233ebf96623465aad4046a8d3aa4da22d4f4beba5388838c8a434bbb4"
 
 [[package]]
 name = "gio"
@@ -2296,7 +2372,7 @@ dependencies = [
  "futures-io",
  "futures-util",
  "gio-sys",
- "glib 0.16.5",
+ "glib 0.16.7",
  "libc",
  "once_cell",
  "pin-project-lite",
@@ -2338,9 +2414,9 @@ dependencies = [
 
 [[package]]
 name = "glib"
-version = "0.16.5"
+version = "0.16.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0cd04d150a2c63e6779f43aec7e04f5374252479b7bed5f45146d9c0e821f161"
+checksum = "ddd4df61a866ed7259d6189b8bcb1464989a77f1d85d25d002279bbe9dd38b2f"
 dependencies = [
  "bitflags",
  "futures-channel",
@@ -2369,9 +2445,9 @@ dependencies = [
  "itertools 0.9.0",
  "proc-macro-crate 0.1.5",
  "proc-macro-error",
- "proc-macro2 1.0.47",
- "quote 1.0.21",
- "syn 1.0.105",
+ "proc-macro2 1.0.51",
+ "quote 1.0.23",
+ "syn 1.0.109",
 ]
 
 [[package]]
@@ -2381,12 +2457,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "e084807350b01348b6d9dbabb724d1a0bb987f47a2c85de200e98e12e30733bf"
 dependencies = [
  "anyhow",
- "heck 0.4.0",
- "proc-macro-crate 1.2.1",
+ "heck 0.4.1",
+ "proc-macro-crate 1.3.1",
  "proc-macro-error",
- "proc-macro2 1.0.47",
- "quote 1.0.21",
- "syn 1.0.105",
+ "proc-macro2 1.0.51",
+ "quote 1.0.23",
+ "syn 1.0.109",
 ]
 
 [[package]]
@@ -2411,9 +2487,9 @@ dependencies = [
 
 [[package]]
 name = "glob"
-version = "0.3.0"
+version = "0.3.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9b919933a397b79c37e33b77bb2aa3dc8eb6e165ad809e58ff75bc7db2e34574"
+checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b"
 
 [[package]]
 name = "gobject-sys"
@@ -2584,7 +2660,7 @@ dependencies = [
  "gdk",
  "gdk-pixbuf",
  "gio",
- "glib 0.16.5",
+ "glib 0.16.7",
  "gtk-sys",
  "gtk3-macros",
  "libc",
@@ -2618,18 +2694,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "8cfd6557b1018b773e43c8de9d0d13581d6b36190d0501916cbec4731db5ccff"
 dependencies = [
  "anyhow",
- "proc-macro-crate 1.2.1",
+ "proc-macro-crate 1.3.1",
  "proc-macro-error",
- "proc-macro2 1.0.47",
- "quote 1.0.21",
- "syn 1.0.105",
+ "proc-macro2 1.0.51",
+ "quote 1.0.23",
+ "syn 1.0.109",
 ]
 
 [[package]]
 name = "h2"
-version = "0.3.15"
+version = "0.3.16"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5f9f29bc9dda355256b2916cf526ab02ce0aeaaaf2bad60d65ef3f12f11dd0f4"
+checksum = "5be7b54589b581f624f566bf5d8eb2bab1db736c51528720b6bd36b96b55924d"
 dependencies = [
  "bytes",
  "fnv",
@@ -2673,7 +2749,7 @@ dependencies = [
  "confy",
  "directories-next",
  "dirs-next",
- "env_logger 0.9.3",
+ "env_logger 0.10.0",
  "filetime",
  "flexi_logger",
  "futures",
@@ -2689,16 +2765,16 @@ dependencies = [
  "quinn",
  "rand 0.8.5",
  "regex",
- "serde 1.0.149",
+ "serde 1.0.154",
  "serde_derive",
- "serde_json 1.0.89",
+ "serde_json 1.0.94",
  "socket2 0.3.19",
  "sodiumoxide",
  "sysinfo",
  "tokio",
  "tokio-socks",
  "tokio-util",
- "toml",
+ "toml 0.7.2",
  "winapi 0.3.9",
  "zstd",
 ]
@@ -2714,9 +2790,9 @@ dependencies = [
 
 [[package]]
 name = "heck"
-version = "0.4.0"
+version = "0.4.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2540771e65fc8cb83cd6e8a237f70c319bd5c29f78ed1084ba5d50eeac86f7f9"
+checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8"
 
 [[package]]
 name = "hermit-abi"
@@ -2727,6 +2803,21 @@ dependencies = [
  "libc",
 ]
 
+[[package]]
+name = "hermit-abi"
+version = "0.2.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ee512640fe35acbfb4bb779db6f0d80704c2cacfa2e39b601ef3e3f47d1ae4c7"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "hermit-abi"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fed44880c466736ef9a5c5b5facefb5ed0785676d0c02d612db14e54f0d84286"
+
 [[package]]
 name = "hex"
 version = "0.4.3"
@@ -2741,13 +2832,13 @@ checksum = "4d13cdbd5dbb29f9c88095bbdc2590c9cba0d0a1269b983fef6b2cdd7e9f4db1"
 
 [[package]]
 name = "http"
-version = "0.2.8"
+version = "0.2.9"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "75f43d41e26995c17e71ee126451dd3941010b0514a81a9d11f3b341debc2399"
+checksum = "bd6effc99afb63425aff9b05836f029929e345a6148a14b7ecd5ab67af944482"
 dependencies = [
  "bytes",
  "fnv",
- "itoa 1.0.4",
+ "itoa 1.0.6",
 ]
 
 [[package]]
@@ -2782,21 +2873,21 @@ checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4"
 [[package]]
 name = "hwcodec"
 version = "0.1.0"
-source = "git+https://github.com/21pages/hwcodec#64f885b3787694b16dfcff08256750b0376b2eba"
+source = "git+https://github.com/21pages/hwcodec#d55f7761ef692fae738259d8c14506d901eb824c"
 dependencies = [
  "bindgen 0.59.2",
  "cc",
  "log",
- "serde 1.0.149",
+ "serde 1.0.154",
  "serde_derive",
- "serde_json 1.0.89",
+ "serde_json 1.0.94",
 ]
 
 [[package]]
 name = "hyper"
-version = "0.14.23"
+version = "0.14.24"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "034711faac9d2166cb1baf1a2fb0b60b1f277f8492fd72176c17f3515e1abd3c"
+checksum = "5e011372fa0b68db8350aa7a248930ecc7839bf46d8485577d69f117a75f164c"
 dependencies = [
  "bytes",
  "futures-channel",
@@ -2807,9 +2898,9 @@ dependencies = [
  "http-body",
  "httparse",
  "httpdate",
- "itoa 1.0.4",
+ "itoa 1.0.6",
  "pin-project-lite",
- "socket2 0.4.7",
+ "socket2 0.4.9",
  "tokio",
  "tower-service",
  "tracing",
@@ -2869,22 +2960,6 @@ dependencies = [
  "unicode-normalization",
 ]
 
-[[package]]
-name = "image"
-version = "0.23.14"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "24ffcb7e7244a9bf19d35bf2883b9c080c4ced3c07a9895572178cdb8f13f6a1"
-dependencies = [
- "bytemuck",
- "byteorder",
- "color_quant",
- "num-iter",
- "num-rational 0.3.2",
- "num-traits 0.2.15",
- "png 0.16.8",
- "tiff 0.6.1",
-]
-
 [[package]]
 name = "image"
 version = "0.24.5"
@@ -2896,12 +2971,12 @@ dependencies = [
  "color_quant",
  "exr",
  "gif",
- "jpeg-decoder 0.3.0",
+ "jpeg-decoder",
  "num-rational 0.4.1",
  "num-traits 0.2.15",
- "png 0.17.7",
+ "png",
  "scoped_threadpool",
- "tiff 0.8.1",
+ "tiff",
 ]
 
 [[package]]
@@ -2927,8 +3002,8 @@ version = "0.7.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "b139284b5cf57ecfa712bcc66950bb635b31aff41c188e8a4cfc758eca374a3f"
 dependencies = [
- "proc-macro2 1.0.47",
- "quote 1.0.21",
+ "proc-macro2 1.0.51",
+ "quote 1.0.23",
 ]
 
 [[package]]
@@ -2973,6 +3048,16 @@ dependencies = [
  "web-sys",
 ]
 
+[[package]]
+name = "io-lifetimes"
+version = "1.0.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cfa919a82ea574332e2de6e74b4c36e74d41982b335080fa59d4ef31be20fdf3"
+dependencies = [
+ "libc",
+ "windows-sys 0.45.0",
+]
+
 [[package]]
 name = "iovec"
 version = "0.1.4"
@@ -2984,9 +3069,21 @@ dependencies = [
 
 [[package]]
 name = "ipnet"
-version = "2.6.0"
+version = "2.7.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ec947b7a4ce12e3b87e353abae7ce124d025b6c7d6c5aea5cc0bcf92e9510ded"
+checksum = "30e22bd8629359895450b59ea7a776c850561b96a3b1d31321c1949d9e6c9146"
+
+[[package]]
+name = "is-terminal"
+version = "0.4.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "21b6b32576413a8e69b90e952e4a026476040d81017b80445deda5f2d3921857"
+dependencies = [
+ "hermit-abi 0.3.1",
+ "io-lifetimes",
+ "rustix",
+ "windows-sys 0.45.0",
+]
 
 [[package]]
 name = "itertools"
@@ -3014,9 +3111,9 @@ checksum = "8324a32baf01e2ae060e9de58ed0bc2320c9a2833491ee36cd3b4c414de4db8c"
 
 [[package]]
 name = "itoa"
-version = "1.0.4"
+version = "1.0.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4217ad341ebadf8d8e724e264f13e593e0648f5b3e94b3896a5df283be015ecc"
+checksum = "453ad9f582a441959e5f0d088b02ce04cfe8d51a8eaf077f12ac6d3e94164ca6"
 
 [[package]]
 name = "jni"
@@ -3054,19 +3151,13 @@ checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130"
 
 [[package]]
 name = "jobserver"
-version = "0.1.25"
+version = "0.1.26"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "068b1ee6743e4d11fb9c6a1e6064b3693a1b600e7f5f5988047d98b3dc9fb90b"
+checksum = "936cfd212a0155903bcbc060e316fb6cc7cbf2e1907329391ebadc1fe0ce77c2"
 dependencies = [
  "libc",
 ]
 
-[[package]]
-name = "jpeg-decoder"
-version = "0.1.22"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "229d53d58899083193af11e15917b5640cd40b29ff475a1fe4ef725deb02d0f2"
-
 [[package]]
 name = "jpeg-decoder"
 version = "0.3.0"
@@ -3078,9 +3169,9 @@ dependencies = [
 
 [[package]]
 name = "js-sys"
-version = "0.3.60"
+version = "0.3.61"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "49409df3e3bf0856b916e2ceaca09ee28e6871cf7d9ce97a692cacfdb2a25a47"
+checksum = "445dde2150c55e483f3d8416706b97ec8e8237c307e5b7b4b8dd15e6af2a0730"
 dependencies = [
  "wasm-bindgen",
 ]
@@ -3102,7 +3193,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "0b7668b7cff6a51fe61cdde64cd27c8a220786f399501b57ebe36f7d8112fd68"
 dependencies = [
  "bitflags",
- "serde 1.0.149",
+ "serde 1.0.154",
  "unicode-segmentation",
 ]
 
@@ -3130,7 +3221,7 @@ version = "0.8.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "89e1edfdc9b0853358306c6dfb4b77c79c779174256fe93d80c0b5ebca451a2f"
 dependencies = [
- "glib 0.16.5",
+ "glib 0.16.7",
  "gtk",
  "gtk-sys",
  "libappindicator-sys",
@@ -3150,15 +3241,15 @@ dependencies = [
 
 [[package]]
 name = "libc"
-version = "0.2.138"
+version = "0.2.139"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "db6d7e329c562c5dfab7a46a2afabc8b987ab9a4834c9d1ca04dc54c1546cef8"
+checksum = "201de327520df007757c1f0adce6e827fe8562fbc28bfd9c15571c66ca1f5f79"
 
 [[package]]
 name = "libdbus-sys"
-version = "0.2.2"
+version = "0.2.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c185b5b7ad900923ef3a8ff594083d4d9b5aea80bb4f32b8342363138c0d456b"
+checksum = "9f8d7ae751e1cb825c840ae5e682f59b098cdfd213c350ac268b61449a5f58a0"
 dependencies = [
  "pkg-config",
 ]
@@ -3175,9 +3266,9 @@ dependencies = [
 
 [[package]]
 name = "libpulse-binding"
-version = "2.26.0"
+version = "2.27.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "17be42160017e0ae993c03bfdab4ecb6f82ce3f8d515bd8da8fdf18d10703663"
+checksum = "1745b20bfc194ac12ef828f144f0ec2d4a7fe993281fa3567a0bd4969aee6890"
 dependencies = [
  "bitflags",
  "libc",
@@ -3189,9 +3280,9 @@ dependencies = [
 
 [[package]]
 name = "libpulse-simple-binding"
-version = "2.25.0"
+version = "2.27.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7cbf1a1dfd69a48cb60906399fa1d17f1b75029ef51c0789597be792dfd0bcd5"
+checksum = "5ced94199e6e44133431374e4043f34e1f0697ebfb7b7d6c244a65bfaedf0e31"
 dependencies = [
  "libpulse-binding",
  "libpulse-simple-sys",
@@ -3200,9 +3291,9 @@ dependencies = [
 
 [[package]]
 name = "libpulse-simple-sys"
-version = "1.19.2"
+version = "1.20.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7c73f96f9ca34809692c4760cfe421225860aa000de50edab68a16221fd27cc1"
+checksum = "84e423d9c619c908ce9b4916080e65ab586ca55b8c4939379f15e6e72fb43842"
 dependencies = [
  "libpulse-sys",
  "pkg-config",
@@ -3210,9 +3301,9 @@ dependencies = [
 
 [[package]]
 name = "libpulse-sys"
-version = "1.19.3"
+version = "1.20.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "991e6bd0efe2a36e6534e136e7996925e4c1a8e35b7807fe533f2beffff27c30"
+checksum = "2191e6880818d1df4cf72eac8e91dce7a5a52ba0da4b2a5cdafabc22b937eadb"
 dependencies = [
  "libc",
  "num-derive",
@@ -3258,14 +3349,23 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "db23b9e7e2b7831bbd8aac0bbeeeb7b68cbebc162b227e7052e8e55829a09212"
 dependencies = [
  "libc",
- "x11 2.20.1",
+ "x11 2.21.0",
+]
+
+[[package]]
+name = "line-wrap"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f30344350a2a51da54c1d53be93fade8a237e545dbcc4bdbe635413f2117cab9"
+dependencies = [
+ "safemem",
 ]
 
 [[package]]
 name = "link-cplusplus"
-version = "1.0.7"
+version = "1.0.8"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9272ab7b96c9046fbc5bc56c06c117cb639fe2d509df0c421cad82d2915cf369"
+checksum = "ecd207c9c713c34f95a097a5b029ac2ce6010530c7b49d7fea24d977dede04f5"
 dependencies = [
  "cc",
 ]
@@ -3276,6 +3376,12 @@ version = "0.5.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f"
 
+[[package]]
+name = "linux-raw-sys"
+version = "0.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f051f77a7c8e6957c0696eac88f26b0117e54f52d3fc682ab19397a8812846a4"
+
 [[package]]
 name = "lock_api"
 version = "0.4.9"
@@ -3386,6 +3492,15 @@ dependencies = [
  "autocfg 1.1.0",
 ]
 
+[[package]]
+name = "memoffset"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d61c719bcfbcf5d62b3a09efa6088de8c54bc0bfcd3ea7ae39fcc186108b8de1"
+dependencies = [
+ "autocfg 1.1.0",
+]
+
 [[package]]
 name = "mime"
 version = "0.3.16"
@@ -3398,34 +3513,6 @@ version = "0.2.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
 
-[[package]]
-name = "miniz_oxide"
-version = "0.3.7"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "791daaae1ed6889560f8c4359194f56648355540573244a5448a83ba1ecc7435"
-dependencies = [
- "adler32",
-]
-
-[[package]]
-name = "miniz_oxide"
-version = "0.4.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a92518e98c078586bc6c934028adcca4c92a53d6a958196de835170a01d84e4b"
-dependencies = [
- "adler",
- "autocfg 1.1.0",
-]
-
-[[package]]
-name = "miniz_oxide"
-version = "0.5.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "96590ba8f175222643a85693f33d26e9c8a015f599c216509b1a6894af675d34"
-dependencies = [
- "adler",
-]
-
 [[package]]
 name = "miniz_oxide"
 version = "0.6.2"
@@ -3456,14 +3543,14 @@ dependencies = [
 
 [[package]]
 name = "mio"
-version = "0.8.5"
+version = "0.8.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e5d732bc30207a6423068df043e3d02e0735b155ad7ce1a6f76fe2baa5b158de"
+checksum = "5b9d9a46eff5b4ff64b45a9e316a6d1e0bc719ef429cbec4dc630684212bfdf9"
 dependencies = [
  "libc",
  "log",
  "wasi 0.11.0+wasi-snapshot-preview1",
- "windows-sys 0.42.0",
+ "windows-sys 0.45.0",
 ]
 
 [[package]]
@@ -3518,9 +3605,9 @@ dependencies = [
 
 [[package]]
 name = "muda"
-version = "0.4.1"
+version = "0.4.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c66365a21dc5e322c6b6ba25c735d00153c57dd2eb377926aa50e3caf547b6f6"
+checksum = "0ee091608fe840d80c6b25e8f838964b264ee8e02e82ae0a38b2d900464d1582"
 dependencies = [
  "cocoa",
  "crossbeam-channel",
@@ -3531,7 +3618,7 @@ dependencies = [
  "libxdo",
  "objc",
  "once_cell",
- "png 0.17.7",
+ "png",
  "thiserror",
  "windows-sys 0.45.0",
 ]
@@ -3587,7 +3674,7 @@ dependencies = [
  "jni-sys",
  "ndk-sys 0.4.1+23.1.7779620",
  "num_enum",
- "raw-window-handle 0.5.0",
+ "raw-window-handle 0.5.1",
  "thiserror",
 ]
 
@@ -3612,21 +3699,6 @@ dependencies = [
  "ndk-sys 0.2.2",
 ]
 
-[[package]]
-name = "ndk-glue"
-version = "0.6.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0d0c4a7b83860226e6b4183edac21851f05d5a51756e97a1144b7f5a6b63e65f"
-dependencies = [
- "lazy_static",
- "libc",
- "log",
- "ndk 0.6.0",
- "ndk-context",
- "ndk-macro",
- "ndk-sys 0.3.0",
-]
-
 [[package]]
 name = "ndk-macro"
 version = "0.3.0"
@@ -3634,10 +3706,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "0df7ac00c4672f9d5aece54ee3347520b7e20f158656c7db2e6de01902eb7a6c"
 dependencies = [
  "darling 0.13.4",
- "proc-macro-crate 1.2.1",
- "proc-macro2 1.0.47",
- "quote 1.0.21",
- "syn 1.0.105",
+ "proc-macro-crate 1.3.1",
+ "proc-macro2 1.0.51",
+ "quote 1.0.23",
+ "syn 1.0.109",
 ]
 
 [[package]]
@@ -3715,35 +3787,23 @@ dependencies = [
 
 [[package]]
 name = "nix"
-version = "0.25.1"
+version = "0.26.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f346ff70e7dbfd675fe90590b92d59ef2de15a8779ae305ebcbfd3f0caf59be4"
+checksum = "bfdda3d196821d6af13126e40375cdf7da646a96114af134d5f417a9a1dc8e1a"
 dependencies = [
- "autocfg 1.1.0",
  "bitflags",
  "cfg-if 1.0.0",
  "libc",
- "memoffset 0.6.5",
+ "memoffset 0.7.1",
  "pin-utils",
-]
-
-[[package]]
-name = "nix"
-version = "0.26.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "46a58d1d356c6597d08cde02c2f09d785b09e28711837b1ed667dc652c08a694"
-dependencies = [
- "bitflags",
- "cfg-if 1.0.0",
- "libc",
  "static_assertions",
 ]
 
 [[package]]
 name = "nom"
-version = "7.1.1"
+version = "7.1.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a8903e5a29a317527874d0402f867152a3d21c908bb0b933e416c65e301d4c36"
+checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a"
 dependencies = [
  "memchr",
  "minimal-lexical",
@@ -3751,13 +3811,23 @@ dependencies = [
 
 [[package]]
 name = "ntapi"
-version = "0.3.7"
+version = "0.4.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c28774a7fd2fbb4f0babd8237ce554b73af68021b5f695a3cebd6c59bac0980f"
+checksum = "bc51db7b362b205941f71232e56c625156eb9a929f8cf74a428fd5bc094a4afc"
 dependencies = [
  "winapi 0.3.9",
 ]
 
+[[package]]
+name = "nu-ansi-term"
+version = "0.46.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84"
+dependencies = [
+ "overload",
+ "winapi 0.3.9",
+]
+
 [[package]]
 name = "num-bigint"
 version = "0.4.3"
@@ -3771,9 +3841,9 @@ dependencies = [
 
 [[package]]
 name = "num-complex"
-version = "0.4.2"
+version = "0.4.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7ae39348c8bc5fbd7f40c727a9925f03517afd2ab27d46702108b6a7e5414c19"
+checksum = "02e0d21255c828d6f128a1e41534206671e8c3ea0c62f32291e808dc82cff17d"
 dependencies = [
  "num-traits 0.2.15",
 ]
@@ -3784,9 +3854,9 @@ version = "0.3.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "876a53fff98e03a936a674b29568b0e605f06b29372c2489ff4de23f1949743d"
 dependencies = [
- "proc-macro2 1.0.47",
- "quote 1.0.21",
- "syn 1.0.105",
+ "proc-macro2 1.0.51",
+ "quote 1.0.23",
+ "syn 1.0.109",
 ]
 
 [[package]]
@@ -3799,17 +3869,6 @@ dependencies = [
  "num-traits 0.2.15",
 ]
 
-[[package]]
-name = "num-iter"
-version = "0.1.43"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7d03e6c028c5dc5cac6e2dec0efda81fc887605bb3d884578bb6d6bf7514e252"
-dependencies = [
- "autocfg 1.1.0",
- "num-integer",
- "num-traits 0.2.15",
-]
-
 [[package]]
 name = "num-rational"
 version = "0.3.2"
@@ -3852,42 +3911,33 @@ dependencies = [
 
 [[package]]
 name = "num_cpus"
-version = "1.14.0"
+version = "1.15.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f6058e64324c71e02bc2b150e4f3bc8286db6c83092132ffa3f6b1eab0f9def5"
+checksum = "0fac9e2da13b5eb447a6ce3d392f23a29d8694bff781bf03a16cd9ac8697593b"
 dependencies = [
- "hermit-abi",
+ "hermit-abi 0.2.6",
  "libc",
 ]
 
 [[package]]
 name = "num_enum"
-version = "0.5.7"
+version = "0.5.11"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cf5395665662ef45796a4ff5486c5d41d29e0c09640af4c5f17fd94ee2c119c9"
+checksum = "1f646caf906c20226733ed5b1374287eb97e3c2a5c227ce668c1f2ce20ae57c9"
 dependencies = [
  "num_enum_derive",
 ]
 
 [[package]]
 name = "num_enum_derive"
-version = "0.5.7"
+version = "0.5.11"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3b0498641e53dd6ac1a4f22547548caa6864cc4933784319cd1775271c5a46ce"
+checksum = "dcbff9bc912032c62bf65ef1d5aea88983b420f4f839db1e9b0c281a25c9c799"
 dependencies = [
- "proc-macro-crate 1.2.1",
- "proc-macro2 1.0.47",
- "quote 1.0.21",
- "syn 1.0.105",
-]
-
-[[package]]
-name = "num_threads"
-version = "0.1.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2819ce041d2ee131036f4fc9d6ae7ae125a3a40e97ba64d04fe799ad9dabbb44"
-dependencies = [
- "libc",
+ "proc-macro-crate 1.3.1",
+ "proc-macro2 1.0.51",
+ "quote 1.0.23",
+ "syn 1.0.109",
 ]
 
 [[package]]
@@ -3931,9 +3981,9 @@ dependencies = [
 
 [[package]]
 name = "object"
-version = "0.29.0"
+version = "0.30.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "21158b2c33aa6d4561f1c0a6ea283ca92bc54802a93b263e910746d679a7eb53"
+checksum = "ea86265d3d3dcb6a27fc51bd29a4bf387fae9d2986b823079d4986af253eb439"
 dependencies = [
  "memchr",
 ]
@@ -3963,9 +4013,9 @@ dependencies = [
 
 [[package]]
 name = "once_cell"
-version = "1.17.0"
+version = "1.17.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6f61fba1741ea2b3d6a1e3178721804bb716a68a6aeba1149b5d52e3d464ea66"
+checksum = "b7e5500299e16ebb147ae15a00a942af264cf3688f47923b8fc2cd5858f23ad3"
 
 [[package]]
 name = "openssl-probe"
@@ -3985,14 +4035,26 @@ dependencies = [
 
 [[package]]
 name = "ordered-stream"
-version = "0.1.2"
+version = "0.2.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "01ca8c99d73c6e92ac1358f9f692c22c0bfd9c4701fa086f5d365c0d4ea818ea"
+checksum = "9aa2b01e1d916879f73a53d01d1d6cee68adbb31d6d9177a8cfce093cced1d50"
 dependencies = [
  "futures-core",
  "pin-project-lite",
 ]
 
+[[package]]
+name = "os-version"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5a8a1fed76ac765e39058ca106b6229a93c5a60292a1bd4b602ce2be11e1c020"
+dependencies = [
+ "anyhow",
+ "plist",
+ "uname",
+ "winapi 0.3.9",
+]
+
 [[package]]
 name = "os_str_bytes"
 version = "6.4.1"
@@ -4005,11 +4067,17 @@ version = "0.3.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "38731fa859ef679f1aec66ca9562165926b442f298467f76f5990f431efe87dc"
 dependencies = [
- "serde 1.0.149",
+ "serde 1.0.154",
  "serde_derive",
- "serde_json 1.0.89",
+ "serde_json 1.0.94",
 ]
 
+[[package]]
+name = "overload"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39"
+
 [[package]]
 name = "pango"
 version = "0.16.5"
@@ -4018,7 +4086,7 @@ checksum = "cdff66b271861037b89d028656184059e03b0b6ccb36003820be19f7200b1e94"
 dependencies = [
  "bitflags",
  "gio",
- "glib 0.16.5",
+ "glib 0.16.7",
  "libc",
  "once_cell",
  "pango-sys",
@@ -4065,7 +4133,7 @@ checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99"
 dependencies = [
  "instant",
  "lock_api",
- "parking_lot_core 0.8.5",
+ "parking_lot_core 0.8.6",
 ]
 
 [[package]]
@@ -4075,14 +4143,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f"
 dependencies = [
  "lock_api",
- "parking_lot_core 0.9.5",
+ "parking_lot_core 0.9.7",
 ]
 
 [[package]]
 name = "parking_lot_core"
-version = "0.8.5"
+version = "0.8.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d76e8e1493bcac0d2766c42737f34458f1c8c50c0d23bcb24ea953affb273216"
+checksum = "60a2cfe6f0ad2bfc16aefa463b497d5c7a5ecd44a23efa72aa342d90177356dc"
 dependencies = [
  "cfg-if 1.0.0",
  "instant",
@@ -4094,22 +4162,22 @@ dependencies = [
 
 [[package]]
 name = "parking_lot_core"
-version = "0.9.5"
+version = "0.9.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7ff9f3fef3968a3ec5945535ed654cb38ff72d7495a25619e2247fb15a2ed9ba"
+checksum = "9069cbb9f99e3a5083476ccb29ceb1de18b9118cafa53e90c9551235de2b9521"
 dependencies = [
  "cfg-if 1.0.0",
  "libc",
  "redox_syscall",
  "smallvec",
- "windows-sys 0.42.0",
+ "windows-sys 0.45.0",
 ]
 
 [[package]]
 name = "paste"
-version = "1.0.9"
+version = "1.0.12"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b1de2e551fb905ac83f73f7aedf2f0cb4a0da7e35efa24a202a936269f1f18e1"
+checksum = "9f746c4065a8fa3fe23974dd82f15431cc8d40779821001404d10d2e79ca7d79"
 
 [[package]]
 name = "pathdiff"
@@ -4129,16 +4197,6 @@ version = "2.2.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e"
 
-[[package]]
-name = "pest"
-version = "2.5.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cc8bed3549e0f9b0a2a78bf7c0018237a2cdf085eecbbc048e52612438e4e9d0"
-dependencies = [
- "thiserror",
- "ucd-trie",
-]
-
 [[package]]
 name = "phf"
 version = "0.7.24"
@@ -4192,9 +4250,9 @@ version = "1.0.12"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "069bdb1e05adc7a8990dce9cc75370895fbe4e3d58b9b73bf1aee56359344a55"
 dependencies = [
- "proc-macro2 1.0.47",
- "quote 1.0.21",
- "syn 1.0.105",
+ "proc-macro2 1.0.51",
+ "quote 1.0.23",
+ "syn 1.0.109",
 ]
 
 [[package]]
@@ -4216,15 +4274,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "6ac9a59f73473f1b8d852421e59e64809f025994837ef743615c6d0c5b305160"
 
 [[package]]
-name = "png"
-version = "0.16.8"
+name = "plist"
+version = "1.4.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3c3287920cb847dee3de33d301c463fba14dda99db24214ddf93f83d3021f4c6"
+checksum = "ffac6a51110e97610dd3ac73e34a65b27e56a1e305df41bad1f616d8e1cb22f4"
 dependencies = [
- "bitflags",
- "crc32fast",
- "deflate",
- "miniz_oxide 0.3.7",
+ "base64",
+ "indexmap",
+ "line-wrap",
+ "quick-xml",
+ "serde 1.0.154",
+ "time 0.3.20",
 ]
 
 [[package]]
@@ -4236,21 +4296,23 @@ dependencies = [
  "bitflags",
  "crc32fast",
  "flate2",
- "miniz_oxide 0.6.2",
+ "miniz_oxide",
 ]
 
 [[package]]
 name = "polling"
-version = "2.5.1"
+version = "2.6.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "166ca89eb77fd403230b9c156612965a81e094ec6ec3aa13663d4c8b113fa748"
+checksum = "7e1f879b2998099c2d69ab9605d145d5b661195627eccc680002c4918a7fb6fa"
 dependencies = [
  "autocfg 1.1.0",
+ "bitflags",
  "cfg-if 1.0.0",
+ "concurrent-queue",
  "libc",
  "log",
- "wepoll-ffi",
- "windows-sys 0.42.0",
+ "pin-project-lite",
+ "windows-sys 0.45.0",
 ]
 
 [[package]]
@@ -4280,18 +4342,17 @@ version = "0.1.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "1d6ea3c4595b96363c13943497db34af4460fb474a95c43f4446ad341b8c9785"
 dependencies = [
- "toml",
+ "toml 0.5.11",
 ]
 
 [[package]]
 name = "proc-macro-crate"
-version = "1.2.1"
+version = "1.3.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "eda0fc3b0fb7c975631757e14d9049da17374063edb6ebbcbc54d880d4fe94e9"
+checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919"
 dependencies = [
  "once_cell",
- "thiserror",
- "toml",
+ "toml_edit",
 ]
 
 [[package]]
@@ -4301,9 +4362,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c"
 dependencies = [
  "proc-macro-error-attr",
- "proc-macro2 1.0.47",
- "quote 1.0.21",
- "syn 1.0.105",
+ "proc-macro2 1.0.51",
+ "quote 1.0.23",
+ "syn 1.0.109",
  "version_check",
 ]
 
@@ -4313,8 +4374,8 @@ version = "1.0.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869"
 dependencies = [
- "proc-macro2 1.0.47",
- "quote 1.0.21",
+ "proc-macro2 1.0.51",
+ "quote 1.0.23",
  "version_check",
 ]
 
@@ -4329,9 +4390,9 @@ dependencies = [
 
 [[package]]
 name = "proc-macro2"
-version = "1.0.47"
+version = "1.0.51"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5ea3d908b0e36316caf9e9e2c4625cdde190a7e6f440d794667ed17a1855e725"
+checksum = "5d727cae5b39d21da60fa540906919ad737832fe0b1c165da3a34d6548c849d6"
 dependencies = [
  "unicode-ident",
 ]
@@ -4376,7 +4437,7 @@ dependencies = [
  "protobuf-support",
  "tempfile",
  "thiserror",
- "which 4.3.0",
+ "which",
 ]
 
 [[package]]
@@ -4402,17 +4463,25 @@ dependencies = [
 ]
 
 [[package]]
-name = "quinn"
-version = "0.8.5"
+name = "quick-xml"
+version = "0.27.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5b435e71d9bfa0d8889927231970c51fb89c58fa63bffcab117c9c7a41e5ef8f"
+checksum = "ffc053f057dd768a56f62cd7e434c42c831d296968997e9ac1f76ea7c2d14c41"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "quinn"
+version = "0.9.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "445cbfe2382fa023c4f2f3c7e1c95c03dcc1df2bf23cebcb2b13e1402c4394d1"
 dependencies = [
  "bytes",
- "futures-channel",
- "futures-util",
- "fxhash",
+ "pin-project-lite",
  "quinn-proto",
  "quinn-udp",
+ "rustc-hash",
  "rustls",
  "thiserror",
  "tokio",
@@ -4422,17 +4491,16 @@ dependencies = [
 
 [[package]]
 name = "quinn-proto"
-version = "0.8.4"
+version = "0.9.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3fce546b9688f767a57530652488420d419a8b1f44a478b451c3d1ab6d992a55"
+checksum = "72ef4ced82a24bb281af338b9e8f94429b6eca01b4e66d899f40031f074e74c9"
 dependencies = [
  "bytes",
- "fxhash",
  "rand 0.8.5",
  "ring",
+ "rustc-hash",
  "rustls",
  "rustls-native-certs",
- "rustls-pemfile 0.2.1",
  "slab",
  "thiserror",
  "tinyvec",
@@ -4442,16 +4510,15 @@ dependencies = [
 
 [[package]]
 name = "quinn-udp"
-version = "0.1.4"
+version = "0.3.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b07946277141531aea269befd949ed16b2c85a780ba1043244eda0969e538e54"
+checksum = "641538578b21f5e5c8ea733b736895576d0fe329bb883b937db6f4d163dbaaf4"
 dependencies = [
- "futures-util",
  "libc",
  "quinn-proto",
- "socket2 0.4.7",
- "tokio",
+ "socket2 0.4.9",
  "tracing",
+ "windows-sys 0.42.0",
 ]
 
 [[package]]
@@ -4465,11 +4532,11 @@ dependencies = [
 
 [[package]]
 name = "quote"
-version = "1.0.21"
+version = "1.0.23"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bbe448f377a7d6961e30f5955f9b8d106c3f5e449d493ee1b125c1d43c2b5179"
+checksum = "8856d8364d252a14d474036ea1358d63c9e6965c8e5c1885c18f73d70bff9c7b"
 dependencies = [
- "proc-macro2 1.0.47",
+ "proc-macro2 1.0.51",
 ]
 
 [[package]]
@@ -4625,18 +4692,15 @@ dependencies = [
 
 [[package]]
 name = "raw-window-handle"
-version = "0.5.0"
+version = "0.5.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ed7e3d950b66e19e0c372f3fa3fbbcf85b1746b571f74e0c2af6042a5c93420a"
-dependencies = [
- "cty",
-]
+checksum = "4f851a03551ceefd30132e447f07f96cb7011d6b658374f3aed847333adb5559"
 
 [[package]]
 name = "rayon"
-version = "1.6.1"
+version = "1.7.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6db3a213adf02b3bcfd2d3846bb41cb22857d131789e01df434fb7e7bc0759b7"
+checksum = "1d2df5196e37bcc87abebc0053e20787d73847bb33134a69841207dd0a47f03b"
 dependencies = [
  "either",
  "rayon-core",
@@ -4644,9 +4708,9 @@ dependencies = [
 
 [[package]]
 name = "rayon-core"
-version = "1.10.1"
+version = "1.11.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cac410af5d00ab6884528b4ab69d1e8e146e8d471201800fa1b4524126de6ad3"
+checksum = "4b8f95bd6966f5c87776639160a66bd8ab9895d9d4ab01ddba9fc60661aebe8d"
 dependencies = [
  "crossbeam-channel",
  "crossbeam-deque",
@@ -4670,12 +4734,12 @@ dependencies = [
  "lazy_static",
  "libc",
  "log",
- "mio 0.8.5",
+ "mio 0.8.6",
  "strum 0.24.1",
  "strum_macros 0.24.3",
  "widestring 1.0.2",
  "winapi 0.3.9",
- "x11 2.20.1",
+ "x11 2.21.0",
 ]
 
 [[package]]
@@ -4718,9 +4782,9 @@ dependencies = [
 
 [[package]]
 name = "regex"
-version = "1.7.0"
+version = "1.7.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e076559ef8e241f2ae3479e36f97bd5741c0330689e217ad51ce2c76808b868a"
+checksum = "48aaa5748ba571fb95cd2c85c09f629215d3a6ece942baa100950af03a34f733"
 dependencies = [
  "aho-corasick",
  "memchr",
@@ -4733,15 +4797,6 @@ version = "0.6.28"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "456c603be3e8d448b072f410900c09faf164fbce2d480456f50eea6e25f9c848"
 
-[[package]]
-name = "remove_dir_all"
-version = "0.5.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7"
-dependencies = [
- "winapi 0.3.9",
-]
-
 [[package]]
 name = "repng"
 version = "0.2.2"
@@ -4754,9 +4809,9 @@ dependencies = [
 
 [[package]]
 name = "reqwest"
-version = "0.11.13"
+version = "0.11.14"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "68cc60575865c7831548863cc02356512e3f1dc2f3f82cb837d7fc4cc8f3c97c"
+checksum = "21eed90ec8570952d53b772ecf8f206aa1ec9a3d76b2521c56c42973f2d91ee9"
 dependencies = [
  "base64",
  "bytes",
@@ -4776,9 +4831,9 @@ dependencies = [
  "percent-encoding",
  "pin-project-lite",
  "rustls",
- "rustls-pemfile 1.0.1",
- "serde 1.0.149",
- "serde_json 1.0.89",
+ "rustls-pemfile",
+ "serde 1.0.154",
+ "serde_json 1.0.94",
  "serde_urlencoded",
  "tokio",
  "tokio-rustls",
@@ -4852,12 +4907,12 @@ dependencies = [
 
 [[package]]
 name = "runas"
-version = "0.2.1"
+version = "1.0.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a620b0994a180cdfa25c0439e6d58c0628272571501880d626ffff58e96a0799"
+checksum = "ed87390fefd18965ff20baae5aeb9913bcf82d2b59dc04c0f6d8f17f7be56ff2"
 dependencies = [
  "cc",
- "which 3.1.1",
+ "which",
 ]
 
 [[package]]
@@ -4892,11 +4947,11 @@ checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2"
 
 [[package]]
 name = "rustc_version"
-version = "0.3.3"
+version = "0.2.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f0dfe2087c51c460008730de8b57e6a320782fbfb312e1f4d520e6c6fae155ee"
+checksum = "138e3e0acb6c9fb258b19b67cb8abd63c00679d2851805ea151465464fe9030a"
 dependencies = [
- "semver 0.11.0",
+ "semver 0.9.0",
 ]
 
 [[package]]
@@ -4905,14 +4960,14 @@ version = "0.4.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366"
 dependencies = [
- "semver 1.0.14",
+ "semver 1.0.16",
 ]
 
 [[package]]
 name = "rustdesk"
 version = "1.2.0"
 dependencies = [
- "android_logger 0.11.1",
+ "android_logger 0.11.3",
  "arboard",
  "async-process",
  "async-trait",
@@ -4922,7 +4977,7 @@ dependencies = [
  "cfg-if 1.0.0",
  "chrono",
  "cidr-utils",
- "clap 3.2.23",
+ "clap 4.1.8",
  "clipboard",
  "cocoa",
  "core-foundation 0.9.3",
@@ -4937,7 +4992,7 @@ dependencies = [
  "dispatch",
  "dlopen",
  "enigo",
- "errno",
+ "errno 0.3.0",
  "evdev",
  "flutter_rust_bridge",
  "flutter_rust_bridge_codegen",
@@ -4945,7 +5000,7 @@ dependencies = [
  "hbb_common",
  "hex",
  "hound",
- "image 0.24.5",
+ "image",
  "impersonate_system",
  "include_dir",
  "jni 0.19.0",
@@ -4959,6 +5014,7 @@ dependencies = [
  "num_cpus",
  "objc",
  "objc_id",
+ "os-version",
  "parity-tokio-ipc",
  "rdev",
  "repng",
@@ -4970,9 +5026,9 @@ dependencies = [
  "samplerate",
  "sciter-rs",
  "scrap",
- "serde 1.0.149",
+ "serde 1.0.154",
  "serde_derive",
- "serde_json 1.0.89",
+ "serde_json 1.0.94",
  "sha2",
  "shared_memory",
  "shutdown_hooks",
@@ -5021,10 +5077,24 @@ dependencies = [
 ]
 
 [[package]]
-name = "rustls"
-version = "0.20.7"
+name = "rustix"
+version = "0.36.9"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "539a2bfe908f471bfa933876bd1eb6a19cf2176d375f82ef7f99530a40e48c2c"
+checksum = "fd5c6ff11fecd55b40746d1995a02f2eb375bf8c00d192d521ee09f42bef37bc"
+dependencies = [
+ "bitflags",
+ "errno 0.2.8",
+ "io-lifetimes",
+ "libc",
+ "linux-raw-sys",
+ "windows-sys 0.45.0",
+]
+
+[[package]]
+name = "rustls"
+version = "0.20.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fff78fc74d175294f4e83b28343315ffcfb114b156f0185e9741cb5570f50e2f"
 dependencies = [
  "log",
  "ring",
@@ -5039,40 +5109,37 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "0167bac7a9f490495f3c33013e7722b53cb087ecbe082fb0c6387c96f634ea50"
 dependencies = [
  "openssl-probe",
- "rustls-pemfile 1.0.1",
+ "rustls-pemfile",
  "schannel",
  "security-framework",
 ]
 
 [[package]]
 name = "rustls-pemfile"
-version = "0.2.1"
+version = "1.0.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5eebeaeb360c87bfb72e84abdb3447159c0eaececf1bef2aecd65a8be949d1c9"
-dependencies = [
- "base64",
-]
-
-[[package]]
-name = "rustls-pemfile"
-version = "1.0.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0864aeff53f8c05aa08d86e5ef839d3dfcf07aeba2db32f12db0ef716e87bd55"
+checksum = "d194b56d58803a43635bdc398cd17e383d6f71f9182b9a192c127ca42494a59b"
 dependencies = [
  "base64",
 ]
 
 [[package]]
 name = "rustversion"
-version = "1.0.9"
+version = "1.0.12"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "97477e48b4cf8603ad5f7aaf897467cf42ab4218a38ef76fb14c2d6773a6d6a8"
+checksum = "4f3208ce4d8448b3f3e7d168a73f5e0c43a61e32930de3bceeccedb388b6bf06"
 
 [[package]]
 name = "ryu"
-version = "1.0.11"
+version = "1.0.13"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4501abdff3ae82a1c1b477a17252eb69cee9e66eb915c1abaa4f44d873df9f09"
+checksum = "f91339c0467de62360649f8d3e185ca8de4224ff281f66000de5eb2a77a79041"
+
+[[package]]
+name = "safemem"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ef703b7cb59335eae2eb93ceb664c0eb7ea6bf567079d843e09420219668e072"
 
 [[package]]
 name = "same-file"
@@ -5094,12 +5161,11 @@ dependencies = [
 
 [[package]]
 name = "schannel"
-version = "0.1.20"
+version = "0.1.21"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "88d6731146462ea25d9244b2ed5fd1d716d25c52e4d54aa4fb0f3c4e9854dbe2"
+checksum = "713cfb06c7059f3588fb8044c0fad1d09e3c01d225e25b9220dbfdcf16dbb1b3"
 dependencies = [
- "lazy_static",
- "windows-sys 0.36.1",
+ "windows-sys 0.42.0",
 ]
 
 [[package]]
@@ -5136,7 +5202,7 @@ name = "scrap"
 version = "0.5.0"
 dependencies = [
  "android_logger 0.10.1",
- "bindgen 0.59.2",
+ "bindgen 0.64.0",
  "block",
  "cfg-if 1.0.0",
  "dbus",
@@ -5153,8 +5219,8 @@ dependencies = [
  "num_cpus",
  "quest",
  "repng",
- "serde 1.0.149",
- "serde_json 1.0.89",
+ "serde 1.0.154",
+ "serde_json 1.0.94",
  "target_build_utils",
  "tracing",
  "webm",
@@ -5163,9 +5229,9 @@ dependencies = [
 
 [[package]]
 name = "scratch"
-version = "1.0.2"
+version = "1.0.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9c8132065adcfd6e02db789d9285a0deb2f3fcb04002865ab67d5fb103533898"
+checksum = "1792db035ce95be60c3f8853017b3999209281c24e2ba5bc8e59bf97a0c590c1"
 
 [[package]]
 name = "sct"
@@ -5179,9 +5245,9 @@ dependencies = [
 
 [[package]]
 name = "security-framework"
-version = "2.7.0"
+version = "2.8.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2bc1bb97804af6631813c55739f771071e0f2ed33ee20b68c86ec505d906356c"
+checksum = "a332be01508d814fed64bf28f798a146d73792121129962fdf335bb3c49a4254"
 dependencies = [
  "bitflags",
  "core-foundation 0.9.3",
@@ -5192,9 +5258,9 @@ dependencies = [
 
 [[package]]
 name = "security-framework-sys"
-version = "2.6.1"
+version = "2.8.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0160a13a177a45bfb43ce71c01580998474f556ad854dcbca936dd2841a5c556"
+checksum = "31c9bb296072e961fcbd8853511dd39c2d8be2deb1e17c6860b1d30732b323b4"
 dependencies = [
  "core-foundation-sys 0.8.3",
  "libc",
@@ -5202,30 +5268,27 @@ dependencies = [
 
 [[package]]
 name = "semver"
-version = "0.11.0"
+version = "0.9.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f301af10236f6df4160f7c3f04eec6dbc70ace82d23326abad5edee88801c6b6"
+checksum = "1d7eb9ef2c18661902cc47e535f9bc51b78acd254da71d375c2f6720d9a40403"
 dependencies = [
  "semver-parser",
 ]
 
 [[package]]
 name = "semver"
-version = "1.0.14"
+version = "1.0.16"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e25dfac463d778e353db5be2449d1cce89bd6fd23c9f1ea21310ce6e5a1b29c4"
+checksum = "58bc9567378fc7690d6b2addae4e60ac2eeea07becb2c64b9f218b53865cba2a"
 dependencies = [
- "serde 1.0.149",
+ "serde 1.0.154",
 ]
 
 [[package]]
 name = "semver-parser"
-version = "0.10.2"
+version = "0.7.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "00b0bef5b7f9e0df16536d3961cfb6e84331c065b4066afb39768d0e319411f7"
-dependencies = [
- "pest",
-]
+checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3"
 
 [[package]]
 name = "serde"
@@ -5235,22 +5298,22 @@ checksum = "34b623917345a631dc9608d5194cc206b3fe6c3554cd1c75b937e55e285254af"
 
 [[package]]
 name = "serde"
-version = "1.0.149"
+version = "1.0.154"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "256b9932320c590e707b94576e3cc1f7c9024d0ee6612dfbcf1cb106cbe8e055"
+checksum = "8cdd151213925e7f1ab45a9bbfb129316bd00799784b174b7cc7bcd16961c49e"
 dependencies = [
  "serde_derive",
 ]
 
 [[package]]
 name = "serde_derive"
-version = "1.0.149"
+version = "1.0.154"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b4eae9b04cbffdfd550eb462ed33bc6a1b68c935127d008b27444d08380f94e4"
+checksum = "4fc80d722935453bcafdc2c9a73cd6fac4dc1938f0346035d84bf99fa9e33217"
 dependencies = [
- "proc-macro2 1.0.47",
- "quote 1.0.21",
- "syn 1.0.105",
+ "proc-macro2 1.0.51",
+ "quote 1.0.23",
+ "syn 1.0.109",
 ]
 
 [[package]]
@@ -5267,24 +5330,33 @@ dependencies = [
 
 [[package]]
 name = "serde_json"
-version = "1.0.89"
+version = "1.0.94"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "020ff22c755c2ed3f8cf162dbb41a7268d934702f3ed3631656ea597e08fc3db"
+checksum = "1c533a59c9d8a93a09c6ab31f0fd5e5f4dd1b8fc9434804029839884765d04ea"
 dependencies = [
- "itoa 1.0.4",
+ "itoa 1.0.6",
  "ryu",
- "serde 1.0.149",
+ "serde 1.0.154",
 ]
 
 [[package]]
 name = "serde_repr"
-version = "0.1.9"
+version = "0.1.11"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1fe39d9fbb0ebf5eb2c7cb7e2a47e4f462fad1379f1166b8ae49ad9eae89a7ca"
+checksum = "395627de918015623b32e7669714206363a7fc00382bf477e72c1f7533e8eafc"
 dependencies = [
- "proc-macro2 1.0.47",
- "quote 1.0.21",
- "syn 1.0.105",
+ "proc-macro2 1.0.51",
+ "quote 1.0.23",
+ "syn 1.0.109",
+]
+
+[[package]]
+name = "serde_spanned"
+version = "0.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0efd8caf556a6cebd3b285caf480045fcc1ac04f6bd786b09a6f11af30c4fcf4"
+dependencies = [
+ "serde 1.0.154",
 ]
 
 [[package]]
@@ -5294,9 +5366,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd"
 dependencies = [
  "form_urlencoded",
- "itoa 1.0.4",
+ "itoa 1.0.6",
  "ryu",
- "serde 1.0.149",
+ "serde 1.0.154",
 ]
 
 [[package]]
@@ -5307,10 +5379,19 @@ checksum = "578a7433b776b56a35785ed5ce9a7e777ac0598aac5a6dd1b4b18a307c7fc71b"
 dependencies = [
  "indexmap",
  "ryu",
- "serde 1.0.149",
+ "serde 1.0.154",
  "yaml-rust",
 ]
 
+[[package]]
+name = "sha1"
+version = "0.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c1da05c97445caa12d05e848c4a4fcbbea29e748ac28f7e80e9b010392063770"
+dependencies = [
+ "sha1_smol",
+]
+
 [[package]]
 name = "sha1"
 version = "0.10.5"
@@ -5322,6 +5403,12 @@ dependencies = [
  "digest",
 ]
 
+[[package]]
+name = "sha1_smol"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ae1a47186c03a32177042e55dbc5fd5aee900b8e0069a8d70fba96a9375cd012"
+
 [[package]]
 name = "sha2"
 version = "0.10.6"
@@ -5360,9 +5447,9 @@ checksum = "6057adedbec913419c92996f395ba69931acbd50b7d56955394cd3f7bedbfa45"
 
 [[package]]
 name = "signal-hook"
-version = "0.3.14"
+version = "0.3.15"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a253b5e89e2698464fc26b545c9edceb338e18a89effeeecfea192c3025be29d"
+checksum = "732768f1176d21d09e076c23a93123d40bba92d50c4058da34d45c8de8e682b9"
 dependencies = [
  "libc",
  "signal-hook-registry",
@@ -5370,9 +5457,9 @@ dependencies = [
 
 [[package]]
 name = "signal-hook-registry"
-version = "1.4.0"
+version = "1.4.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e51e73328dc4ac0c7ccbda3a494dfa03df1de2f46018127f60c693f2648455b0"
+checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1"
 dependencies = [
  "libc",
 ]
@@ -5395,7 +5482,7 @@ version = "0.1.0"
 dependencies = [
  "confy",
  "hbb_common",
- "serde 1.0.149",
+ "serde 1.0.154",
  "serde_derive",
  "walkdir",
 ]
@@ -5408,9 +5495,9 @@ checksum = "0b8de496cf83d4ed58b6be86c3a275b8602f6ffe98d3024a869e124147a9a3ac"
 
 [[package]]
 name = "slab"
-version = "0.4.7"
+version = "0.4.8"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4614a76b2a8be0058caa9dbbaf66d988527d86d003c11a94fbd335d7661edcef"
+checksum = "6528351c9bc8ab22353f9d776db39a20288e8d6c37ef8cfe3317cf875eecfc2d"
 dependencies = [
  "autocfg 1.1.0",
 ]
@@ -5453,9 +5540,9 @@ dependencies = [
 
 [[package]]
 name = "socket2"
-version = "0.4.7"
+version = "0.4.9"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "02e2d2db9033d13a1567121ddd7a095ee144db4e1ca1b1bda3419bc0da294ebd"
+checksum = "64a4a911eed85daf18834cfaa86a79b7d266ff93ff5ba14005426219480ed662"
 dependencies = [
  "libc",
  "winapi 0.3.9",
@@ -5470,7 +5557,7 @@ dependencies = [
  "ed25519",
  "libc",
  "libsodium-sys",
- "serde 1.0.149",
+ "serde 1.0.154",
 ]
 
 [[package]]
@@ -5496,9 +5583,52 @@ checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
 
 [[package]]
 name = "stdweb"
-version = "0.1.3"
+version = "0.4.20"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ef5430c8e36b713e13b48a9f709cc21e046723fe44ce34587b73a830203b533e"
+checksum = "d022496b16281348b52d0e30ae99e01a73d737b2f45d38fed4edf79f9325a1d5"
+dependencies = [
+ "discard",
+ "rustc_version 0.2.3",
+ "stdweb-derive",
+ "stdweb-internal-macros",
+ "stdweb-internal-runtime",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "stdweb-derive"
+version = "0.5.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c87a60a40fccc84bef0652345bbbbbe20a605bf5d0ce81719fc476f5c03b50ef"
+dependencies = [
+ "proc-macro2 1.0.51",
+ "quote 1.0.23",
+ "serde 1.0.154",
+ "serde_derive",
+ "syn 1.0.109",
+]
+
+[[package]]
+name = "stdweb-internal-macros"
+version = "0.2.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "58fa5ff6ad0d98d1ffa8cb115892b6e69d67799f6763e162a1c9db421dc22e11"
+dependencies = [
+ "base-x",
+ "proc-macro2 1.0.51",
+ "quote 1.0.23",
+ "serde 1.0.154",
+ "serde_derive",
+ "serde_json 1.0.94",
+ "sha1 0.6.1",
+ "syn 1.0.109",
+]
+
+[[package]]
+name = "stdweb-internal-runtime"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "213701ba3370744dcd1a12960caa4843b3d68b4d1c0a5d575e0d65b2ee9d16c0"
 
 [[package]]
 name = "str-buf"
@@ -5549,9 +5679,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "87c85aa3f8ea653bfd3ddf25f7ee357ee4d204731f6aa9ad04002306f6e2774c"
 dependencies = [
  "heck 0.3.3",
- "proc-macro2 1.0.47",
- "quote 1.0.21",
- "syn 1.0.105",
+ "proc-macro2 1.0.51",
+ "quote 1.0.23",
+ "syn 1.0.109",
 ]
 
 [[package]]
@@ -5560,11 +5690,11 @@ version = "0.24.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "1e385be0d24f186b4ce2f9982191e7101bb737312ad61c1f2f984f34bcf85d59"
 dependencies = [
- "heck 0.4.0",
- "proc-macro2 1.0.47",
- "quote 1.0.21",
+ "heck 0.4.1",
+ "proc-macro2 1.0.51",
+ "quote 1.0.23",
  "rustversion",
- "syn 1.0.105",
+ "syn 1.0.109",
 ]
 
 [[package]]
@@ -5580,12 +5710,12 @@ dependencies = [
 
 [[package]]
 name = "syn"
-version = "1.0.105"
+version = "1.0.109"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "60b9b43d45702de4c839cb9b51d9f529c5dd26a4aff255b42b1ebc03e88ee908"
+checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237"
 dependencies = [
- "proc-macro2 1.0.47",
- "quote 1.0.21",
+ "proc-macro2 1.0.51",
+ "quote 1.0.23",
  "unicode-ident",
 ]
 
@@ -5595,30 +5725,30 @@ version = "0.12.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "f36bdaa60a83aca3921b5259d5400cbf5e90fc51931376a9bd4a0eb79aa7210f"
 dependencies = [
- "proc-macro2 1.0.47",
- "quote 1.0.21",
- "syn 1.0.105",
+ "proc-macro2 1.0.51",
+ "quote 1.0.23",
+ "syn 1.0.109",
  "unicode-xid 0.2.4",
 ]
 
 [[package]]
 name = "sys-locale"
-version = "0.2.3"
+version = "0.2.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3358acbb4acd4146138b9bda219e904a6bb5aaaa237f8eed06f4d6bc1580ecee"
+checksum = "f8a11bd9c338fdba09f7881ab41551932ad42e405f61d01e8406baea71c07aee"
 dependencies = [
  "js-sys",
  "libc",
  "wasm-bindgen",
  "web-sys",
- "winapi 0.3.9",
+ "windows-sys 0.45.0",
 ]
 
 [[package]]
 name = "sysinfo"
-version = "0.24.7"
+version = "0.26.9"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "54cb4ebf3d49308b99e6e9dc95e989e2fdbdc210e4f67c39db0bb89ba927001c"
+checksum = "5c18a6156d1f27a9592ee18c1a846ca8dd5c258b7179fc193ae87c74ebb666f5"
 dependencies = [
  "cfg-if 1.0.0",
  "core-foundation-sys 0.8.3",
@@ -5661,7 +5791,7 @@ dependencies = [
  "strum 0.18.0",
  "strum_macros 0.18.0",
  "thiserror",
- "toml",
+ "toml 0.5.11",
  "version-compare 0.0.10",
 ]
 
@@ -5672,19 +5802,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "2955b1fe31e1fa2fbd1976b71cc69a606d7d4da16f6de3333d0c92d51419aeff"
 dependencies = [
  "cfg-expr",
- "heck 0.4.0",
+ "heck 0.4.1",
  "pkg-config",
- "toml",
+ "toml 0.5.11",
  "version-compare 0.1.1",
 ]
 
 [[package]]
 name = "system_shutdown"
-version = "3.0.0"
+version = "4.0.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "035e081d603551d8d78db27d2232913269c749ea67648c369100049820406a14"
+checksum = "7567f71160af5e9abfb4f5a21532cf2174cefe91ac5c336419295685a695cc66"
 dependencies = [
- "winapi 0.3.9",
+ "windows 0.44.0",
+ "zbus",
 ]
 
 [[package]]
@@ -5706,10 +5837,10 @@ dependencies = [
  "gdkwayland-sys",
  "gdkx11-sys",
  "gio",
- "glib 0.16.5",
+ "glib 0.16.7",
  "glib-sys 0.16.3",
  "gtk",
- "image 0.24.5",
+ "image",
  "instant",
  "jni 0.20.0",
  "lazy_static",
@@ -5721,8 +5852,8 @@ dependencies = [
  "objc",
  "once_cell",
  "parking_lot 0.12.1",
- "png 0.17.7",
- "raw-window-handle 0.5.0",
+ "png",
+ "raw-window-handle 0.5.1",
  "scopeguard",
  "tao-macros",
  "unicode-segmentation",
@@ -5737,9 +5868,9 @@ name = "tao-macros"
 version = "0.1.0"
 source = "git+https://github.com/tauri-apps/tao?branch=muda#676bd90a80286b893d8850cc4e3813a0c4a27dcf"
 dependencies = [
- "proc-macro2 1.0.47",
- "quote 1.0.21",
- "syn 1.0.105",
+ "proc-macro2 1.0.51",
+ "quote 1.0.23",
+ "syn 1.0.109",
 ]
 
 [[package]]
@@ -5761,23 +5892,22 @@ dependencies = [
 
 [[package]]
 name = "tempfile"
-version = "3.3.0"
+version = "3.4.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5cdb1ef4eaeeaddc8fbd371e5017057064af0911902ef36b39801f67cc6d79e4"
+checksum = "af18f7ae1acd354b992402e9ec5864359d693cd8a79dcbef59f76891701c1e95"
 dependencies = [
  "cfg-if 1.0.0",
  "fastrand",
- "libc",
  "redox_syscall",
- "remove_dir_all",
- "winapi 0.3.9",
+ "rustix",
+ "windows-sys 0.42.0",
 ]
 
 [[package]]
 name = "termcolor"
-version = "1.1.3"
+version = "1.2.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bab24d30b911b2376f3a13cc2cd443142f0c81dda04c118693e35b3835757755"
+checksum = "be55cf8942feac5c765c2c993422806843c9a9a45d4d5c407ad6dd2ea95eb9b6"
 dependencies = [
  "winapi-util",
 ]
@@ -5809,8 +5939,9 @@ checksum = "222a222a5bfe1bba4a77b45ec488a741b3cb8872e5e499451fd7d0129c9c7c3d"
 [[package]]
 name = "tfc"
 version = "0.6.1"
-source = "git+https://github.com/fufesou/The-Fat-Controller#a5f13e6ef80327eb8d860aeb26b0af93eb5aee2b"
+source = "git+https://github.com/fufesou/The-Fat-Controller#102f2ec2cb2bbbd64413d20d28323e5e77e0fe71"
 dependencies = [
+ "anyhow",
  "core-graphics 0.22.3",
  "unicode-segmentation",
  "winapi 0.3.9",
@@ -5819,22 +5950,22 @@ dependencies = [
 
 [[package]]
 name = "thiserror"
-version = "1.0.37"
+version = "1.0.39"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "10deb33631e3c9018b9baf9dcbbc4f737320d2b576bac10f6aefa048fa407e3e"
+checksum = "a5ab016db510546d856297882807df8da66a16fb8c4101cb8b30054b0d5b2d9c"
 dependencies = [
  "thiserror-impl",
 ]
 
 [[package]]
 name = "thiserror-impl"
-version = "1.0.37"
+version = "1.0.39"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "982d17546b47146b28f7c22e3d08465f6b8903d0ea13c1660d9d84a6e7adcdbb"
+checksum = "5420d42e90af0c38c3290abcca25b9b3bdf379fc9f55c528f53a269d9c9a267e"
 dependencies = [
- "proc-macro2 1.0.47",
- "quote 1.0.21",
- "syn 1.0.105",
+ "proc-macro2 1.0.51",
+ "quote 1.0.23",
+ "syn 1.0.109",
 ]
 
 [[package]]
@@ -5846,17 +5977,6 @@ dependencies = [
  "num_cpus",
 ]
 
-[[package]]
-name = "tiff"
-version = "0.6.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9a53f4706d65497df0c4349241deddf35f84cee19c87ed86ea8ca590f4464437"
-dependencies = [
- "jpeg-decoder 0.1.22",
- "miniz_oxide 0.4.4",
- "weezl",
-]
-
 [[package]]
 name = "tiff"
 version = "0.8.1"
@@ -5864,7 +5984,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "7449334f9ff2baf290d55d73983a7d6fa15e01198faef72af07e2a8db851e471"
 dependencies = [
  "flate2",
- "jpeg-decoder 0.3.0",
+ "jpeg-decoder",
  "weezl",
 ]
 
@@ -5881,21 +6001,30 @@ dependencies = [
 
 [[package]]
 name = "time"
-version = "0.3.9"
+version = "0.3.20"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c2702e08a7a860f005826c6815dcac101b19b5eb330c27fe4a5928fec1d20ddd"
+checksum = "cd0cbfecb4d19b5ea75bb31ad904eb5b9fa13f21079c3b92017ebdf4999a5890"
 dependencies = [
- "itoa 1.0.4",
- "libc",
- "num_threads",
+ "itoa 1.0.6",
+ "serde 1.0.154",
+ "time-core",
  "time-macros",
 ]
 
 [[package]]
-name = "time-macros"
-version = "0.2.4"
+name = "time-core"
+version = "0.1.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "42657b1a6f4d817cda8e7a0ace261fe0cc946cf3a80314390b22cc61ae080792"
+checksum = "2e153e1f1acaef8acc537e68b44906d2db6436e2b35ac2c6b42640fff91f00fd"
+
+[[package]]
+name = "time-macros"
+version = "0.2.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fd80a657e71da814b8e5d60d3374fc6d35045062245d80224748ae522dd76f36"
+dependencies = [
+ "time-core",
+]
 
 [[package]]
 name = "tinyvec"
@@ -5908,28 +6037,28 @@ dependencies = [
 
 [[package]]
 name = "tinyvec_macros"
-version = "0.1.0"
+version = "0.1.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c"
+checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
 
 [[package]]
 name = "tokio"
-version = "1.23.0"
+version = "1.26.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "eab6d665857cc6ca78d6e80303a02cea7a7851e85dfbd77cbdc09bd129f1ef46"
+checksum = "03201d01c3c27a29c8a5cee5b55a93ddae1ccf6f08f65365c2c918f8c1b76f64"
 dependencies = [
  "autocfg 1.1.0",
  "bytes",
  "libc",
  "memchr",
- "mio 0.8.5",
+ "mio 0.8.6",
  "num_cpus",
  "parking_lot 0.12.1",
  "pin-project-lite",
  "signal-hook-registry",
- "socket2 0.4.7",
+ "socket2 0.4.9",
  "tokio-macros",
- "windows-sys 0.42.0",
+ "windows-sys 0.45.0",
 ]
 
 [[package]]
@@ -5938,9 +6067,9 @@ version = "1.8.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "d266c00fde287f55d3f1c3e96c500c362a2b8c695076ec180f27918820bc6df8"
 dependencies = [
- "proc-macro2 1.0.47",
- "quote 1.0.21",
- "syn 1.0.105",
+ "proc-macro2 1.0.51",
+ "quote 1.0.23",
+ "syn 1.0.109",
 ]
 
 [[package]]
@@ -5972,9 +6101,9 @@ dependencies = [
 
 [[package]]
 name = "tokio-util"
-version = "0.7.4"
+version = "0.7.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0bb2e075f03b3d66d8d8785356224ba688d2906a371015e225beeb65ca92c740"
+checksum = "5427d89453009325de0d8f342c9490009f76e999cb7672d77e46267448f7e6b2"
 dependencies = [
  "bytes",
  "futures-core",
@@ -5990,11 +6119,45 @@ dependencies = [
 
 [[package]]
 name = "toml"
-version = "0.5.9"
+version = "0.5.11"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8d82e1a7758622a465f8cee077614c73484dac5b836c02ff6a40d5d1010324d7"
+checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234"
 dependencies = [
- "serde 1.0.149",
+ "serde 1.0.154",
+]
+
+[[package]]
+name = "toml"
+version = "0.7.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f7afcae9e3f0fe2c370fd4657108972cbb2fa9db1b9f84849cefd80741b01cb6"
+dependencies = [
+ "serde 1.0.154",
+ "serde_spanned",
+ "toml_datetime",
+ "toml_edit",
+]
+
+[[package]]
+name = "toml_datetime"
+version = "0.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3ab8ed2edee10b50132aed5f331333428b011c99402b5a534154ed15746f9622"
+dependencies = [
+ "serde 1.0.154",
+]
+
+[[package]]
+name = "toml_edit"
+version = "0.19.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9a1eb0622d28f4b9c90adc4ea4b2b46b47663fde9ac5fafcb14a1369d5508825"
+dependencies = [
+ "indexmap",
+ "serde 1.0.154",
+ "serde_spanned",
+ "toml_datetime",
+ "winnow",
 ]
 
 [[package]]
@@ -6027,9 +6190,9 @@ version = "0.1.23"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "4017f8f45139870ca7e672686113917c71c7a6e02d4924eda67186083c03081a"
 dependencies = [
- "proc-macro2 1.0.47",
- "quote 1.0.21",
- "syn 1.0.105",
+ "proc-macro2 1.0.51",
+ "quote 1.0.23",
+ "syn 1.0.109",
 ]
 
 [[package]]
@@ -6053,9 +6216,9 @@ dependencies = [
 
 [[package]]
 name = "tray-icon"
-version = "0.4.2"
+version = "0.4.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d62801a4da61bb100b8d3174a5a46fed7b6ea03cc2ae93ee7340793b09a94ce3"
+checksum = "f87445e3a107818c17d87e8369db30a6fc25539bface8351efe2132b22e47dbc"
 dependencies = [
  "cocoa",
  "core-graphics 0.22.3",
@@ -6065,7 +6228,7 @@ dependencies = [
  "muda",
  "objc",
  "once_cell",
- "png 0.17.7",
+ "png",
  "thiserror",
  "windows-sys 0.45.0",
 ]
@@ -6081,9 +6244,9 @@ dependencies = [
 
 [[package]]
 name = "try-lock"
-version = "0.2.3"
+version = "0.2.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "59547bce71d9c38b83d9c0e92b6066c4253371f15005def0c30d9657f50c7642"
+checksum = "3528ecfd12c466c6f163363caf2d02a71161dd5e1cc6ae7b34207ea2d42d81ed"
 
 [[package]]
 name = "typenum"
@@ -6091,12 +6254,6 @@ version = "1.16.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba"
 
-[[package]]
-name = "ucd-trie"
-version = "0.1.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9e79c4d996edb816c91e4308506774452e55e95c3c9de07b6729e17e15a5ef81"
-
 [[package]]
 name = "uds_windows"
 version = "1.0.2"
@@ -6108,16 +6265,25 @@ dependencies = [
 ]
 
 [[package]]
-name = "unicode-bidi"
-version = "0.3.8"
+name = "uname"
+version = "0.1.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "099b7128301d285f79ddd55b9a83d5e6b9e97c92e0ea0daebee7263e932de992"
+checksum = "b72f89f0ca32e4db1c04e2a72f5345d59796d4866a1ee0609084569f73683dc8"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "unicode-bidi"
+version = "0.3.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "524b68aca1d05e03fdf03fcdce2c6c94b6daf6d16861ddaa7e4f2b6638a9052c"
 
 [[package]]
 name = "unicode-ident"
-version = "1.0.5"
+version = "1.0.8"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6ceab39d59e4c9499d4e5a8ee0e2735b891bb7308ac83dfb4e80cad195c9f6f3"
+checksum = "e5464a87b239f13a63a501f2701565754bae92d243d4bb7eb12f6d57d2269bf4"
 
 [[package]]
 name = "unicode-normalization"
@@ -6130,9 +6296,9 @@ dependencies = [
 
 [[package]]
 name = "unicode-segmentation"
-version = "1.10.0"
+version = "1.10.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0fdbf052a0783de01e944a6ce7a8cb939e295b1e7be835a1112c3b9a7f047a5a"
+checksum = "1dd624098567895118886609431a7c3b8f516e41d30e0643f03d94592a147e36"
 
 [[package]]
 name = "unicode-width"
@@ -6167,7 +6333,7 @@ dependencies = [
  "form_urlencoded",
  "idna",
  "percent-encoding",
- "serde 1.0.149",
+ "serde 1.0.154",
 ]
 
 [[package]]
@@ -6273,9 +6439,9 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
 
 [[package]]
 name = "wasm-bindgen"
-version = "0.2.83"
+version = "0.2.84"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "eaf9f5aceeec8be17c128b2e93e031fb8a4d469bb9c4ae2d7dc1888b26887268"
+checksum = "31f8dcbc21f30d9b8f2ea926ecb58f6b91192c17e9d33594b3df58b2007ca53b"
 dependencies = [
  "cfg-if 1.0.0",
  "wasm-bindgen-macro",
@@ -6283,24 +6449,24 @@ dependencies = [
 
 [[package]]
 name = "wasm-bindgen-backend"
-version = "0.2.83"
+version = "0.2.84"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4c8ffb332579b0557b52d268b91feab8df3615f265d5270fec2a8c95b17c1142"
+checksum = "95ce90fd5bcc06af55a641a86428ee4229e44e07033963a2290a8e241607ccb9"
 dependencies = [
  "bumpalo",
  "log",
  "once_cell",
- "proc-macro2 1.0.47",
- "quote 1.0.21",
- "syn 1.0.105",
+ "proc-macro2 1.0.51",
+ "quote 1.0.23",
+ "syn 1.0.109",
  "wasm-bindgen-shared",
 ]
 
 [[package]]
 name = "wasm-bindgen-futures"
-version = "0.4.33"
+version = "0.4.34"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "23639446165ca5a5de86ae1d8896b737ae80319560fbaa4c2887b7da6e7ebd7d"
+checksum = "f219e0d211ba40266969f6dbdd90636da12f75bee4fc9d6c23d1260dadb51454"
 dependencies = [
  "cfg-if 1.0.0",
  "js-sys",
@@ -6310,32 +6476,32 @@ dependencies = [
 
 [[package]]
 name = "wasm-bindgen-macro"
-version = "0.2.83"
+version = "0.2.84"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "052be0f94026e6cbc75cdefc9bae13fd6052cdcaf532fa6c45e7ae33a1e6c810"
+checksum = "4c21f77c0bedc37fd5dc21f897894a5ca01e7bb159884559461862ae90c0b4c5"
 dependencies = [
- "quote 1.0.21",
+ "quote 1.0.23",
  "wasm-bindgen-macro-support",
 ]
 
 [[package]]
 name = "wasm-bindgen-macro-support"
-version = "0.2.83"
+version = "0.2.84"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "07bc0c051dc5f23e307b13285f9d75df86bfdf816c5721e573dec1f9b8aa193c"
+checksum = "2aff81306fcac3c7515ad4e177f521b5c9a15f2b08f4e32d823066102f35a5f6"
 dependencies = [
- "proc-macro2 1.0.47",
- "quote 1.0.21",
- "syn 1.0.105",
+ "proc-macro2 1.0.51",
+ "quote 1.0.23",
+ "syn 1.0.109",
  "wasm-bindgen-backend",
  "wasm-bindgen-shared",
 ]
 
 [[package]]
 name = "wasm-bindgen-shared"
-version = "0.2.83"
+version = "0.2.84"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1c38c045535d93ec4f0b4defec448e4291638ee608530863b1e2ba115d4fff7f"
+checksum = "0046fef7e28c3804e5e38bfa31ea2a0f73905319b677e57ebe37e49358989b5d"
 
 [[package]]
 name = "wayland-client"
@@ -6394,8 +6560,8 @@ version = "0.29.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "8f4303d8fa22ab852f789e75a967f0a2cdc430a607751c0499bada3e451cbd53"
 dependencies = [
- "proc-macro2 1.0.47",
- "quote 1.0.21",
+ "proc-macro2 1.0.51",
+ "quote 1.0.23",
  "xml-rs",
 ]
 
@@ -6412,9 +6578,9 @@ dependencies = [
 
 [[package]]
 name = "web-sys"
-version = "0.3.60"
+version = "0.3.61"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bcda906d8be16e728fd5adc5b729afad4e444e106ab28cd1c7256e54fa61510f"
+checksum = "e33b99f4b23ba3eec1a53ac264e35a755f00e966e0065077d6027c0f575b0b97"
 dependencies = [
  "js-sys",
  "wasm-bindgen",
@@ -6463,30 +6629,11 @@ version = "0.1.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "9193164d4de03a926d909d3bc7c30543cecb35400c02114792c2cae20d5e2dbb"
 
-[[package]]
-name = "wepoll-ffi"
-version = "0.1.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d743fdedc5c64377b5fc2bc036b01c7fd642205a0d96356034ae3404d49eb7fb"
-dependencies = [
- "cc",
-]
-
 [[package]]
 name = "which"
-version = "3.1.1"
+version = "4.4.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d011071ae14a2f6671d0b74080ae0cd8ebf3a6f8c9589a2cd45f23126fe29724"
-dependencies = [
- "failure",
- "libc",
-]
-
-[[package]]
-name = "which"
-version = "4.3.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1c831fbbee9e129a8cf93e7747a82da9d95ba8e16621cae60ec2cdc849bacb7b"
+checksum = "2441c784c52b289a054b7201fc93253e288f094e2f4be9058343127c4226a269"
 dependencies = [
  "either",
  "libc",
@@ -6495,11 +6642,10 @@ dependencies = [
 
 [[package]]
 name = "whoami"
-version = "1.2.3"
+version = "1.3.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d6631b6a2fd59b1841b622e8f1a7ad241ef0a46f2d580464ce8140ac94cbd571"
+checksum = "45dbc71f0cdca27dc261a9bd37ddec174e4a0af2b900b890f378460f745426e3"
 dependencies = [
- "bumpalo",
  "wasm-bindgen",
  "web-sys",
 ]
@@ -6603,6 +6749,19 @@ dependencies = [
  "windows_x86_64_msvc 0.34.0",
 ]
 
+[[package]]
+name = "windows"
+version = "0.37.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "57b543186b344cc61c85b5aab0d2e3adf4e0f99bc076eff9aa5927bcc0b8a647"
+dependencies = [
+ "windows_aarch64_msvc 0.37.0",
+ "windows_i686_gnu 0.37.0",
+ "windows_i686_msvc 0.37.0",
+ "windows_x86_64_gnu 0.37.0",
+ "windows_x86_64_msvc 0.37.0",
+]
+
 [[package]]
 name = "windows"
 version = "0.44.0"
@@ -6620,9 +6779,9 @@ version = "0.44.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "6ce87ca8e3417b02dc2a8a22769306658670ec92d78f1bd420d6310a67c245c6"
 dependencies = [
- "proc-macro2 1.0.47",
- "quote 1.0.21",
- "syn 1.0.105",
+ "proc-macro2 1.0.51",
+ "quote 1.0.23",
+ "syn 1.0.109",
 ]
 
 [[package]]
@@ -6631,9 +6790,9 @@ version = "0.44.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "853f69a591ecd4f810d29f17e902d40e349fb05b0b11fff63b08b826bfe39c7f"
 dependencies = [
- "proc-macro2 1.0.47",
- "quote 1.0.21",
- "syn 1.0.105",
+ "proc-macro2 1.0.51",
+ "quote 1.0.23",
+ "syn 1.0.109",
 ]
 
 [[package]]
@@ -6661,19 +6820,6 @@ dependencies = [
  "windows_x86_64_msvc 0.28.0",
 ]
 
-[[package]]
-name = "windows-sys"
-version = "0.36.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ea04155a16a59f9eab786fe12a4a450e75cdb175f9e0d80da1e17db09f55b8d2"
-dependencies = [
- "windows_aarch64_msvc 0.36.1",
- "windows_i686_gnu 0.36.1",
- "windows_i686_msvc 0.36.1",
- "windows_x86_64_gnu 0.36.1",
- "windows_x86_64_msvc 0.36.1",
-]
-
 [[package]]
 name = "windows-sys"
 version = "0.42.0"
@@ -6739,9 +6885,9 @@ checksum = "17cffbe740121affb56fad0fc0e421804adf0ae00891205213b5cecd30db881d"
 
 [[package]]
 name = "windows_aarch64_msvc"
-version = "0.36.1"
+version = "0.37.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9bb8c3fd39ade2d67e9874ac4f3db21f0d710bee00fe7cab16949ec184eeaa47"
+checksum = "2623277cb2d1c216ba3b578c0f3cf9cdebeddb6e66b1b218bb33596ea7769c3a"
 
 [[package]]
 name = "windows_aarch64_msvc"
@@ -6769,9 +6915,9 @@ checksum = "2564fde759adb79129d9b4f54be42b32c89970c18ebf93124ca8870a498688ed"
 
 [[package]]
 name = "windows_i686_gnu"
-version = "0.36.1"
+version = "0.37.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "180e6ccf01daf4c426b846dfc66db1fc518f074baa793aa7d9b9aaeffad6a3b6"
+checksum = "d3925fd0b0b804730d44d4b6278c50f9699703ec49bcd628020f46f4ba07d9e1"
 
 [[package]]
 name = "windows_i686_gnu"
@@ -6799,9 +6945,9 @@ checksum = "9cd9d32ba70453522332c14d38814bceeb747d80b3958676007acadd7e166956"
 
 [[package]]
 name = "windows_i686_msvc"
-version = "0.36.1"
+version = "0.37.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e2e7917148b2812d1eeafaeb22a97e4813dfa60a3f8f78ebe204bcc88f12f024"
+checksum = "ce907ac74fe331b524c1298683efbf598bb031bc84d5e274db2083696d07c57c"
 
 [[package]]
 name = "windows_i686_msvc"
@@ -6829,9 +6975,9 @@ checksum = "cfce6deae227ee8d356d19effc141a509cc503dfd1f850622ec4b0f84428e1f4"
 
 [[package]]
 name = "windows_x86_64_gnu"
-version = "0.36.1"
+version = "0.37.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4dcd171b8776c41b97521e5da127a2d86ad280114807d0b2ab1e462bc764d9e1"
+checksum = "2babfba0828f2e6b32457d5341427dcbb577ceef556273229959ac23a10af33d"
 
 [[package]]
 name = "windows_x86_64_gnu"
@@ -6865,9 +7011,9 @@ checksum = "d19538ccc21819d01deaf88d6a17eae6596a12e9aafdbb97916fb49896d89de9"
 
 [[package]]
 name = "windows_x86_64_msvc"
-version = "0.36.1"
+version = "0.37.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c811ca4a8c853ef420abd8592ba53ddbbac90410fab6903b3e79972a631f7680"
+checksum = "f4dd6dc7df2d84cf7b33822ed5b86318fb1781948e9663bacd047fc9dd52259d"
 
 [[package]]
 name = "windows_x86_64_msvc"
@@ -6891,9 +7037,9 @@ dependencies = [
  "lazy_static",
  "libc",
  "log",
- "mio 0.8.5",
+ "mio 0.8.6",
  "ndk 0.5.0",
- "ndk-glue 0.5.2",
+ "ndk-glue",
  "ndk-sys 0.2.2",
  "objc",
  "parking_lot 0.11.2",
@@ -6908,6 +7054,15 @@ dependencies = [
  "x11-dl",
 ]
 
+[[package]]
+name = "winnow"
+version = "0.3.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ee7b2c67f962bf5042bfd8b6a916178df33a26eec343ae064cb8e069f638fa6f"
+dependencies = [
+ "memchr",
+]
+
 [[package]]
 name = "winreg"
 version = "0.6.2"
@@ -6932,17 +7087,14 @@ version = "0.1.12"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "b68db261ef59e9e52806f688020631e987592bd83619edccda9c47d42cde4f6c"
 dependencies = [
- "toml",
+ "toml 0.5.11",
 ]
 
 [[package]]
 name = "wol-rs"
-version = "0.9.1"
+version = "1.0.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d7f97e69b28b256ccfb02472c25057132e234aa8368fea3bb0268def564ce1f2"
-dependencies = [
- "clap 3.2.23",
-]
+checksum = "48dc5e486e34a31515518d370cdd8bf59ec696323fe8f92b858e43942e84a765"
 
 [[package]]
 name = "ws2_32-sys"
@@ -6974,9 +7126,9 @@ dependencies = [
 
 [[package]]
 name = "x11"
-version = "2.20.1"
+version = "2.21.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c2638d5b9c17ac40575fb54bb461a4b1d2a8d1b4ffcc4ff237d254ec59ddeb82"
+checksum = "502da5464ccd04011667b11c435cb992822c2c0dbde1770c988480d312a0db2e"
 dependencies = [
  "libc",
  "pkg-config",
@@ -6995,14 +7147,24 @@ dependencies = [
 
 [[package]]
 name = "x11rb"
-version = "0.9.0"
+version = "0.10.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6e99be55648b3ae2a52342f9a870c0e138709a3493261ce9b469afe6e4df6d8a"
+checksum = "592b4883219f345e712b3209c62654ebda0bb50887f330cbd018d0f654bfd507"
 dependencies = [
  "gethostname",
- "nix 0.22.3",
+ "nix 0.24.3",
  "winapi 0.3.9",
  "winapi-wsapoll",
+ "x11rb-protocol",
+]
+
+[[package]]
+name = "x11rb-protocol"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "56b245751c0ac9db0e006dc812031482784e434630205a93c73cfefcaabeac67"
+dependencies = [
+ "nix 0.24.3",
 ]
 
 [[package]]
@@ -7027,7 +7189,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "5af43ba661cee58bd86b9f81a899e45a15ac7f42fa4401340f73c0c2950030c1"
 dependencies = [
  "derive_setters",
- "serde 1.0.149",
+ "serde 1.0.154",
 ]
 
 [[package]]
@@ -7041,13 +7203,13 @@ dependencies = [
 
 [[package]]
 name = "zbus"
-version = "3.6.2"
+version = "3.11.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "938ea6da98c75c2c37a86007bd17fd8e208cbec24e086108c87ece98e9edec0d"
+checksum = "20aae5dd5b051971cd2f49f9f3b860e57b2b495ba5ba254eaec42d34ede57e97"
 dependencies = [
  "async-broadcast",
- "async-channel",
  "async-executor",
+ "async-fs",
  "async-io",
  "async-lock",
  "async-recursion",
@@ -7062,13 +7224,13 @@ dependencies = [
  "futures-sink",
  "futures-util",
  "hex",
- "nix 0.25.1",
+ "nix 0.26.2",
  "once_cell",
  "ordered-stream",
  "rand 0.8.5",
- "serde 1.0.149",
+ "serde 1.0.154",
  "serde_repr",
- "sha1",
+ "sha1 0.10.5",
  "static_assertions",
  "tracing",
  "uds_windows",
@@ -7080,24 +7242,25 @@ dependencies = [
 
 [[package]]
 name = "zbus_macros"
-version = "3.6.2"
+version = "3.11.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "45066039ebf3330820e495e854f8b312abb68f0a39e97972d092bd72e8bb3e8e"
+checksum = "9264b3a1bcf5503d4e0348b6e7efe1da58d4f92a913c15ed9e63b52de85faaa1"
 dependencies = [
- "proc-macro-crate 1.2.1",
- "proc-macro2 1.0.47",
- "quote 1.0.21",
+ "proc-macro-crate 1.3.1",
+ "proc-macro2 1.0.51",
+ "quote 1.0.23",
  "regex",
- "syn 1.0.105",
+ "syn 1.0.109",
+ "zvariant_utils",
 ]
 
 [[package]]
 name = "zbus_names"
-version = "2.4.0"
+version = "2.5.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6c737644108627748a660d038974160e0cbb62605536091bdfa28fd7f64d43c8"
+checksum = "f34f314916bd89bdb9934154627fab152f4f28acdda03e7c4c68181b214fe7e3"
 dependencies = [
- "serde 1.0.149",
+ "serde 1.0.154",
  "static_assertions",
  "zvariant",
 ]
@@ -7133,35 +7296,47 @@ dependencies = [
 
 [[package]]
 name = "zune-inflate"
-version = "0.2.42"
+version = "0.2.51"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c473377c11c4a3ac6a2758f944cd336678e9c977aa0abf54f6450cf77e902d6d"
+checksum = "a01728b79fb9b7e28a8c11f715e1cd8dc2cda7416a007d66cac55cebb3a8ac6b"
 dependencies = [
  "simd-adler32",
 ]
 
 [[package]]
 name = "zvariant"
-version = "3.9.0"
+version = "3.12.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "56f8c89c183461e11867ded456db252eae90874bc6769b7adbea464caa777e51"
+checksum = "46fe4914a985446d6fd287019b5fceccce38303d71407d9e6e711d44954a05d8"
 dependencies = [
  "byteorder",
  "enumflags2",
  "libc",
- "serde 1.0.149",
+ "serde 1.0.154",
  "static_assertions",
  "zvariant_derive",
 ]
 
 [[package]]
 name = "zvariant_derive"
-version = "3.9.0"
+version = "3.12.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "155247a5d1ab55e335421c104ccd95d64f17cebbd02f50cdbc1c33385f9c4d81"
+checksum = "34c20260af4b28b3275d6676c7e2a6be0d4332e8e0aba4616d34007fd84e462a"
 dependencies = [
- "proc-macro-crate 1.2.1",
- "proc-macro2 1.0.47",
- "quote 1.0.21",
- "syn 1.0.105",
+ "proc-macro-crate 1.3.1",
+ "proc-macro2 1.0.51",
+ "quote 1.0.23",
+ "syn 1.0.109",
+ "zvariant_utils",
+]
+
+[[package]]
+name = "zvariant_utils"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "53b22993dbc4d128a17a3b6c92f1c63872dd67198537ee728d8b5d7c40640a8b"
+dependencies = [
+ "proc-macro2 1.0.51",
+ "quote 1.0.23",
+ "syn 1.0.109",
 ]
diff --git a/Cargo.toml b/Cargo.toml
index 47c2bb0e7..7ad979f8c 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -45,22 +45,22 @@ lazy_static = "1.4"
 sha2 = "0.10"
 repng = "0.2"
 parity-tokio-ipc = { git = "https://github.com/open-trade/parity-tokio-ipc" }
-runas = "0.2"
+runas = "1.0"
 magnum-opus = { git = "https://github.com/rustdesk/magnum-opus" }
 dasp = { version = "0.11", features = ["signal", "interpolate-linear", "interpolate"], optional = true }
 rubato = { version = "0.12", optional = true }
 samplerate = { version = "0.2", optional = true }
 async-trait = "0.1"
 uuid = { version = "1.0", features = ["v4"] }
-clap = "3.0"
+clap = "4.1"
 rpassword = "7.0"
-base64 = "0.13"
+base64 = "0.21"
 num_cpus = "1.13"
 bytes = { version = "1.2", features = ["serde"] }
 default-net = "0.12.0"
-wol-rs = "0.9.1"
+wol-rs = "1.0"
 flutter_rust_bridge = { version = "1.61.1", optional = true }
-errno = "0.2.8"
+errno = "0.3"
 rdev = { git = "https://github.com/fufesou/rdev" }
 url = { version = "2.1", features = ["serde"] }
 dlopen = "0.1"
@@ -71,7 +71,7 @@ chrono = "0.4.23"
 cidr-utils = "0.5.9"
 
 [target.'cfg(not(any(target_os = "android", target_os = "linux")))'.dependencies]
-cpal = "0.13.5"
+cpal = "0.14"
 
 [target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies]
 machine-uid = "0.2"
@@ -81,9 +81,9 @@ sys-locale = "0.2"
 enigo = { path = "libs/enigo", features = [ "with_serde" ] }
 clipboard = { path = "libs/clipboard" }
 ctrlc = "3.2"
-arboard = "2.0"
+arboard = "3.2"
 #minreq = { version = "2.4", features = ["punycode", "https-native"] }
-system_shutdown = "3.0.0"
+system_shutdown = "4.0"
 
 [target.'cfg(target_os = "windows")'.dependencies]
 trayicon = { git = "https://github.com/open-trade/trayicon-rs", features = ["winit"] }
@@ -148,6 +148,7 @@ cc = "1.0"
 hbb_common = { path = "libs/hbb_common" }
 simple_rc = { path = "libs/simple_rc", optional = true }
 flutter_rust_bridge_codegen = "1.61.1"
+os-version = "0.2"
 
 [dev-dependencies]
 hound = "3.5"
diff --git a/build.rs b/build.rs
index d15f27424..bf141e539 100644
--- a/build.rs
+++ b/build.rs
@@ -9,7 +9,14 @@ fn build_windows() {
 #[cfg(target_os = "macos")]
 fn build_mac() {
     let file = "src/platform/macos.mm";
-    cc::Build::new().file(file).compile("macos");
+    let mut b = cc::Build::new();
+    if let Ok(os_version::OsVersion::MacOS(v)) = os_version::detect() {
+        let v = v.version;
+        if v.contains("10.14") {
+            b.flag("-DNO_InputMonitoringAuthStatus=1");
+        }
+    }
+    b.file(file).compile("macos");
     println!("cargo:rerun-if-changed={}", file);
 }
 
diff --git a/docs/README-GR.md b/docs/README-GR.md
index b4a7d5c47..8ec98030d 100644
--- a/docs/README-GR.md
+++ b/docs/README-GR.md
@@ -37,9 +37,9 @@
 | Σεούλ | AWS lightsail | 1 vCPU / 0.5GB RAM |
 | Γερμανία | Hetzner | 2 vCPU / 4GB RAM |
 | Γερμανία | Codext | 4 vCPU / 8GB RAM |
-| Φινλανδία (Ελσίνκι) | 0x101 Cyber Security | 4 vCPU / 8GB RAM |
-| ΗΠΑ (Άσμπερν) | 0x101 Cyber Security | 4 vCPU / 8GB RAM |
-| Ουκρανία (Κίεβο) | dc.volia (2VM) | 2 vCPU / 4GB RAM |
+| Φινλανδία (Ελσίνκι) | [Netlock](https://netlockendpoint.com) | 4 vCPU / 8GB RAM |
+| ΗΠΑ (Άσμπερν) | [Netlock](https://netlockendpoint.com) | 4 vCPU / 8GB RAM |
+| Ουκρανία (Κίεβο) | [dc.volia](https://dc.volia.com) | 2 vCPU / 4GB RAM |
 
 ## Dev Container
 
diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart
index 666eab0ba..d9c052723 100644
--- a/flutter/lib/common.dart
+++ b/flutter/lib/common.dart
@@ -430,7 +430,7 @@ final ButtonStyle flatButtonStyle = TextButton.styleFrom(
 );
 
 List<Locale> supportedLocales = const [
-  // specify CN/TW to fix CJK issue in flutter
+  Locale('en', 'US'),
   Locale('zh', 'CN'),
   Locale('zh', 'TW'),
   Locale('zh', 'SG'),
@@ -452,7 +452,7 @@ List<Locale> supportedLocales = const [
   Locale('vi'),
   Locale('pl'),
   Locale('kz'),
-  Locale('en', 'US'),
+  Locale('es'),
 ];
 
 String formatDurationToTime(Duration duration) {
diff --git a/flutter/lib/common/widgets/peers_view.dart b/flutter/lib/common/widgets/peers_view.dart
index 640f7d085..197e55376 100644
--- a/flutter/lib/common/widgets/peers_view.dart
+++ b/flutter/lib/common/widgets/peers_view.dart
@@ -68,7 +68,7 @@ class _PeersViewState extends State<_PeersView> with WindowListener {
   var _lastChangeTime = DateTime.now();
   var _lastQueryPeers = <String>{};
   var _lastQueryTime = DateTime.now().subtract(const Duration(hours: 1));
-  var _queryCoun = 0;
+  var _queryCount = 0;
   var _exit = false;
 
   late final mobileWidth = () {
@@ -101,12 +101,12 @@ class _PeersViewState extends State<_PeersView> with WindowListener {
 
   @override
   void onWindowFocus() {
-    _queryCoun = 0;
+    _queryCount = 0;
   }
 
   @override
   void onWindowMinimize() {
-    _queryCoun = _maxQueryCount;
+    _queryCount = _maxQueryCount;
   }
 
   @override
@@ -123,6 +123,19 @@ class _PeersViewState extends State<_PeersView> with WindowListener {
     );
   }
 
+  onVisibilityChanged(VisibilityInfo info) {
+    final peerId = _peerId((info.key as ValueKey).value);
+    if (info.visibleFraction > 0.00001) {
+      _curPeers.add(peerId);
+    } else {
+      _curPeers.remove(peerId);
+    }
+    _lastChangeTime = DateTime.now();
+  }
+
+  String _cardId(String id) => widget.peers.name + id;
+  String _peerId(String cardId) => cardId.replaceAll(widget.peers.name, '');
+
   Widget _buildPeersView(Peers peers) {
     final body = ObxValue<RxList>((filters) {
       return FutureBuilder<List<Peer>>(
@@ -132,16 +145,8 @@ class _PeersViewState extends State<_PeersView> with WindowListener {
             final cards = <Widget>[];
             for (final peer in peers) {
               final visibilityChild = VisibilityDetector(
-                key: ValueKey(peer.id),
-                onVisibilityChanged: (info) {
-                  final peerId = (info.key as ValueKey).value;
-                  if (info.visibleFraction > 0.00001) {
-                    _curPeers.add(peerId);
-                  } else {
-                    _curPeers.remove(peerId);
-                  }
-                  _lastChangeTime = DateTime.now();
-                },
+                key: ValueKey(_cardId(peer.id)),
+                onVisibilityChanged: onVisibilityChanged,
                 child: widget.peerCardBuilder(peer),
               );
               cards.add(isDesktop
@@ -172,6 +177,7 @@ class _PeersViewState extends State<_PeersView> with WindowListener {
   // ignore: todo
   // TODO: variables walk through async tasks?
   void _startCheckOnlines() {
+    final queryInterval = const Duration(seconds: 20);
     () async {
       while (!_exit) {
         final now = DateTime.now();
@@ -181,18 +187,18 @@ class _PeersViewState extends State<_PeersView> with WindowListener {
               platformFFI.ffiBind
                   .queryOnlines(ids: _curPeers.toList(growable: false));
               _lastQueryPeers = {..._curPeers};
-              _lastQueryTime = DateTime.now();
-              _queryCoun = 0;
+              _lastQueryTime = DateTime.now().subtract(queryInterval);
+              _queryCount = 0;
             }
           }
         } else {
-          if (_queryCoun < _maxQueryCount) {
-            if (now.difference(_lastQueryTime) > const Duration(seconds: 20)) {
+          if (_queryCount < _maxQueryCount) {
+            if (now.difference(_lastQueryTime) >= queryInterval) {
               if (_curPeers.isNotEmpty) {
                 platformFFI.ffiBind
                     .queryOnlines(ids: _curPeers.toList(growable: false));
                 _lastQueryTime = DateTime.now();
-                _queryCoun += 1;
+                _queryCount += 1;
               }
             }
           }
diff --git a/flutter/lib/desktop/pages/file_manager_page.dart b/flutter/lib/desktop/pages/file_manager_page.dart
index 8bb57145f..44def46c2 100644
--- a/flutter/lib/desktop/pages/file_manager_page.dart
+++ b/flutter/lib/desktop/pages/file_manager_page.dart
@@ -15,7 +15,6 @@ import 'package:flutter_hbb/desktop/widgets/tabbar_widget.dart';
 import 'package:flutter_hbb/models/file_model.dart';
 import 'package:flutter_svg/flutter_svg.dart';
 import 'package:get/get.dart';
-import 'package:provider/provider.dart';
 import 'package:wakelock/wakelock.dart';
 
 import '../../consts.dart';
@@ -61,52 +60,15 @@ class FileManagerPage extends StatefulWidget {
 
 class _FileManagerPageState extends State<FileManagerPage>
     with AutomaticKeepAliveClientMixin {
-  final _localSelectedItems = SelectedItems();
-  final _remoteSelectedItems = SelectedItems();
-
-  final _locationStatusLocal = LocationStatus.bread.obs;
-  final _locationStatusRemote = LocationStatus.bread.obs;
-  final _locationNodeLocal = FocusNode(debugLabel: "locationNodeLocal");
-  final _locationNodeRemote = FocusNode(debugLabel: "locationNodeRemote");
-  final _locationBarKeyLocal = GlobalKey(debugLabel: "locationBarKeyLocal");
-  final _locationBarKeyRemote = GlobalKey(debugLabel: "locationBarKeyRemote");
-  final _searchTextLocal = "".obs;
-  final _searchTextRemote = "".obs;
-  final _breadCrumbScrollerLocal = ScrollController();
-  final _breadCrumbScrollerRemote = ScrollController();
   final _mouseFocusScope = Rx<MouseFocusScope>(MouseFocusScope.none);
-  final _keyboardNodeLocal = FocusNode(debugLabel: "keyboardNodeLocal");
-  final _keyboardNodeRemote = FocusNode(debugLabel: "keyboardNodeRemote");
-  final _listSearchBufferLocal = TimeoutStringBuffer();
-  final _listSearchBufferRemote = TimeoutStringBuffer();
-  final _nameColWidthLocal = kDesktopFileTransferNameColWidth.obs;
-  final _modifiedColWidthLocal = kDesktopFileTransferModifiedColWidth.obs;
-  final _nameColWidthRemote = kDesktopFileTransferNameColWidth.obs;
-  final _modifiedColWidthRemote = kDesktopFileTransferModifiedColWidth.obs;
-
-  /// [_lastClickTime], [_lastClickEntry] help to handle double click
-  int _lastClickTime =
-      DateTime.now().millisecondsSinceEpoch - bind.getDoubleClickTime() - 1000;
-  Entry? _lastClickEntry;
 
   final _dropMaskVisible = false.obs; // TODO impl drop mask
   final _overlayKeyState = OverlayKeyState();
 
-  ScrollController getBreadCrumbScrollController(bool isLocal) {
-    return isLocal ? _breadCrumbScrollerLocal : _breadCrumbScrollerRemote;
-  }
-
-  GlobalKey getLocationBarKey(bool isLocal) {
-    return isLocal ? _locationBarKeyLocal : _locationBarKeyRemote;
-  }
-
   late FFI _ffi;
 
   FileModel get model => _ffi.fileModel;
-
-  SelectedItems getSelectedItems(bool isLocal) {
-    return isLocal ? _localSelectedItems : _remoteSelectedItems;
-  }
+  JobController get jobController => model.jobController;
 
   @override
   void initState() {
@@ -122,448 +84,61 @@ class _FileManagerPageState extends State<FileManagerPage>
       Wakelock.enable();
     }
     debugPrint("File manager page init success with id ${widget.id}");
-    model.onDirChanged = breadCrumbScrollToEnd;
-    // register location listener
-    _locationNodeLocal.addListener(onLocalLocationFocusChanged);
-    _locationNodeRemote.addListener(onRemoteLocationFocusChanged);
     _ffi.dialogManager.setOverlayState(_overlayKeyState);
   }
 
   @override
   void dispose() {
-    model.onClose().whenComplete(() {
+    model.close().whenComplete(() {
       _ffi.close();
       _ffi.dialogManager.dismissAll();
       if (!Platform.isLinux) {
         Wakelock.disable();
       }
       Get.delete<FFI>(tag: 'ft_${widget.id}');
-      _locationNodeLocal.removeListener(onLocalLocationFocusChanged);
-      _locationNodeRemote.removeListener(onRemoteLocationFocusChanged);
-      _locationNodeLocal.dispose();
-      _locationNodeRemote.dispose();
     });
     super.dispose();
   }
 
+  @override
+  bool get wantKeepAlive => true;
+
   @override
   Widget build(BuildContext context) {
     super.build(context);
     return Overlay(key: _overlayKeyState.key, initialEntries: [
       OverlayEntry(builder: (_) {
-        return ChangeNotifierProvider.value(
-            value: _ffi.fileModel,
-            child: Consumer<FileModel>(builder: (context, model, child) {
-              return Scaffold(
-                backgroundColor: Theme.of(context).scaffoldBackgroundColor,
-                body: Row(
-                  children: [
-                    Flexible(flex: 3, child: body(isLocal: true)),
-                    Flexible(flex: 3, child: body(isLocal: false)),
-                    Flexible(flex: 2, child: statusList())
-                  ],
-                ),
-              );
-            }));
+        return Scaffold(
+          backgroundColor: Theme.of(context).scaffoldBackgroundColor,
+          body: Row(
+            children: [
+              Flexible(
+                  flex: 3,
+                  child: dropArea(FileManagerView(
+                      model.localController, _ffi, _mouseFocusScope))),
+              Flexible(
+                  flex: 3,
+                  child: dropArea(FileManagerView(
+                      model.remoteController, _ffi, _mouseFocusScope))),
+              Flexible(flex: 2, child: statusList())
+            ],
+          ),
+        );
       })
     ]);
   }
 
-  Widget menu({bool isLocal = false}) {
-    var menuPos = RelativeRect.fill;
-
-    final List<MenuEntryBase<String>> items = [
-      MenuEntrySwitch<String>(
-        switchType: SwitchType.scheckbox,
-        text: translate("Show Hidden Files"),
-        getter: () async {
-          return model.getCurrentShowHidden(isLocal);
-        },
-        setter: (bool v) async {
-          model.toggleShowHidden(local: isLocal);
-        },
-        padding: kDesktopMenuPadding,
-        dismissOnClicked: true,
-      ),
-      MenuEntryButton(
-          childBuilder: (style) => Text(translate("Select All"), style: style),
-          proc: () => setState(() => getSelectedItems(isLocal)
-              .selectAll(model.getCurrentDir(isLocal).entries)),
-          padding: kDesktopMenuPadding,
-          dismissOnClicked: true),
-      MenuEntryButton(
-          childBuilder: (style) =>
-              Text(translate("Unselect All"), style: style),
-          proc: () => setState(() => getSelectedItems(isLocal).clear()),
-          padding: kDesktopMenuPadding,
-          dismissOnClicked: true)
-    ];
-
-    return Listener(
-      onPointerDown: (e) {
-        final x = e.position.dx;
-        final y = e.position.dy;
-        menuPos = RelativeRect.fromLTRB(x, y, x, y);
-      },
-      child: MenuButton(
-        onPressed: () => mod_menu.showMenu(
-          context: context,
-          position: menuPos,
-          items: items
-              .map(
-                (e) => e.build(
-                  context,
-                  MenuConfig(
-                      commonColor: CustomPopupMenuTheme.commonColor,
-                      height: CustomPopupMenuTheme.height,
-                      dividerHeight: CustomPopupMenuTheme.dividerHeight),
-                ),
-              )
-              .expand((i) => i)
-              .toList(),
-          elevation: 8,
-        ),
-        child: SvgPicture.asset(
-          "assets/dots.svg",
-          color: Theme.of(context).tabBarTheme.labelColor,
-        ),
-        color: Theme.of(context).cardColor,
-        hoverColor: Theme.of(context).hoverColor,
-      ),
-    );
-  }
-
-  Widget body({bool isLocal = false}) {
-    final scrollController = ScrollController();
-    return Container(
-      margin: const EdgeInsets.all(16.0),
-      padding: const EdgeInsets.all(8.0),
-      child: DropTarget(
-        onDragDone: (detail) => handleDragDone(detail, isLocal),
+  Widget dropArea(FileManagerView fileView) {
+    return DropTarget(
+        onDragDone: (detail) =>
+            handleDragDone(detail, fileView.controller.isLocal),
         onDragEntered: (enter) {
           _dropMaskVisible.value = true;
         },
         onDragExited: (exit) {
           _dropMaskVisible.value = false;
         },
-        child: Column(
-          crossAxisAlignment: CrossAxisAlignment.start,
-          children: [
-            headTools(isLocal),
-            Expanded(
-              child: Row(
-                crossAxisAlignment: CrossAxisAlignment.start,
-                children: [
-                  Expanded(
-                    child: _buildFileList(context, isLocal, scrollController),
-                  )
-                ],
-              ),
-            ),
-          ],
-        ),
-      ),
-    );
-  }
-
-  Widget _buildFileList(
-      BuildContext context, bool isLocal, ScrollController scrollController) {
-    final fd = model.getCurrentDir(isLocal);
-    final entries = fd.entries;
-    final selectedEntries = getSelectedItems(isLocal);
-
-    return MouseRegion(
-      onEnter: (evt) {
-        _mouseFocusScope.value =
-            isLocal ? MouseFocusScope.local : MouseFocusScope.remote;
-        if (isLocal) {
-          _keyboardNodeLocal.requestFocus();
-        } else {
-          _keyboardNodeRemote.requestFocus();
-        }
-      },
-      onExit: (evt) {
-        _mouseFocusScope.value = MouseFocusScope.none;
-      },
-      child: ListSearchActionListener(
-        node: isLocal ? _keyboardNodeLocal : _keyboardNodeRemote,
-        buffer: isLocal ? _listSearchBufferLocal : _listSearchBufferRemote,
-        onNext: (buffer) {
-          debugPrint("searching next for $buffer");
-          assert(buffer.length == 1);
-          assert(selectedEntries.length <= 1);
-          var skipCount = 0;
-          if (selectedEntries.items.isNotEmpty) {
-            final index = entries.indexOf(selectedEntries.items.first);
-            if (index < 0) {
-              return;
-            }
-            skipCount = index + 1;
-          }
-          var searchResult = entries.skip(skipCount).where(
-              (element) => element.name.toLowerCase().startsWith(buffer));
-          if (searchResult.isEmpty) {
-            // cannot find next, lets restart search from head
-            debugPrint("restart search from head");
-            searchResult = entries.where(
-                (element) => element.name.toLowerCase().startsWith(buffer));
-          }
-          if (searchResult.isEmpty) {
-            setState(() {
-              getSelectedItems(isLocal).clear();
-            });
-            return;
-          }
-          _jumpToEntry(isLocal, searchResult.first, scrollController,
-              kDesktopFileTransferRowHeight);
-        },
-        onSearch: (buffer) {
-          debugPrint("searching for $buffer");
-          final selectedEntries = getSelectedItems(isLocal);
-          final searchResult = entries.where(
-              (element) => element.name.toLowerCase().startsWith(buffer));
-          selectedEntries.clear();
-          if (searchResult.isEmpty) {
-            setState(() {
-              getSelectedItems(isLocal).clear();
-            });
-            return;
-          }
-          _jumpToEntry(isLocal, searchResult.first, scrollController,
-              kDesktopFileTransferRowHeight);
-        },
-        child: ObxValue<RxString>(
-          (searchText) {
-            final filteredEntries = searchText.isNotEmpty
-                ? entries.where((element) {
-                    return element.name.contains(searchText.value);
-                  }).toList(growable: false)
-                : entries;
-            final rows = filteredEntries.map((entry) {
-              final sizeStr =
-                  entry.isFile ? readableFileSize(entry.size.toDouble()) : "";
-              final lastModifiedStr = entry.isDrive
-                  ? " "
-                  : "${entry.lastModified().toString().replaceAll(".000", "")}   ";
-              final isSelected = selectedEntries.contains(entry);
-              return Padding(
-                padding: EdgeInsets.symmetric(vertical: 1),
-                child: Container(
-                    decoration: BoxDecoration(
-                      color: isSelected
-                          ? Theme.of(context).hoverColor
-                          : Theme.of(context).cardColor,
-                      borderRadius: BorderRadius.all(
-                        Radius.circular(5.0),
-                      ),
-                    ),
-                    key: ValueKey(entry.name),
-                    height: kDesktopFileTransferRowHeight,
-                    child: Column(
-                      mainAxisAlignment: MainAxisAlignment.spaceAround,
-                      children: [
-                        Expanded(
-                          child: InkWell(
-                            child: Row(
-                              children: [
-                                GestureDetector(
-                                  child: Obx(
-                                    () => Container(
-                                        width: isLocal
-                                            ? _nameColWidthLocal.value
-                                            : _nameColWidthRemote.value,
-                                        child: Tooltip(
-                                          waitDuration:
-                                              Duration(milliseconds: 500),
-                                          message: entry.name,
-                                          child: Row(children: [
-                                            entry.isDrive
-                                                ? Image(
-                                                        image: iconHardDrive,
-                                                        fit: BoxFit.scaleDown,
-                                                        color: Theme.of(context)
-                                                            .iconTheme
-                                                            .color
-                                                            ?.withOpacity(0.7))
-                                                    .paddingAll(4)
-                                                : SvgPicture.asset(
-                                                    entry.isFile
-                                                        ? "assets/file.svg"
-                                                        : "assets/folder.svg",
-                                                    color: Theme.of(context)
-                                                        .tabBarTheme
-                                                        .labelColor,
-                                                  ),
-                                            Expanded(
-                                                child: Text(
-                                                    entry.name.nonBreaking,
-                                                    overflow:
-                                                        TextOverflow.ellipsis))
-                                          ]),
-                                        )),
-                                  ),
-                                  onTap: () {
-                                    final items = getSelectedItems(isLocal);
-                                    // handle double click
-                                    if (_checkDoubleClick(entry)) {
-                                      openDirectory(entry.path,
-                                          isLocal: isLocal);
-                                      items.clear();
-                                      return;
-                                    }
-                                    _onSelectedChanged(
-                                        items, filteredEntries, entry, isLocal);
-                                  },
-                                ),
-                                SizedBox(
-                                  width: 2.0,
-                                ),
-                                GestureDetector(
-                                  child: Obx(
-                                    () => SizedBox(
-                                      width: isLocal
-                                          ? _modifiedColWidthLocal.value
-                                          : _modifiedColWidthRemote.value,
-                                      child: Tooltip(
-                                          waitDuration:
-                                              Duration(milliseconds: 500),
-                                          message: lastModifiedStr,
-                                          child: Text(
-                                            lastModifiedStr,
-                                            overflow: TextOverflow.ellipsis,
-                                            style: TextStyle(
-                                              fontSize: 12,
-                                              color: MyTheme.darkGray,
-                                            ),
-                                          )),
-                                    ),
-                                  ),
-                                ),
-                                // Divider from header.
-                                SizedBox(
-                                  width: 2.0,
-                                ),
-                                Expanded(
-                                  // width: 100,
-                                  child: GestureDetector(
-                                    child: Tooltip(
-                                      waitDuration: Duration(milliseconds: 500),
-                                      message: sizeStr,
-                                      child: Text(
-                                        sizeStr,
-                                        overflow: TextOverflow.ellipsis,
-                                        style: TextStyle(
-                                            fontSize: 10,
-                                            color: MyTheme.darkGray),
-                                      ),
-                                    ),
-                                  ),
-                                ),
-                              ],
-                            ),
-                          ),
-                        ),
-                      ],
-                    )),
-              );
-            }).toList(growable: false);
-
-            return Column(
-              children: [
-                // Header
-                Row(
-                  children: [
-                    Expanded(child: _buildFileBrowserHeader(context, isLocal)),
-                  ],
-                ),
-                // Body
-                Expanded(
-                  child: ListView.builder(
-                    controller: scrollController,
-                    itemExtent: kDesktopFileTransferRowHeight,
-                    itemBuilder: (context, index) {
-                      return rows[index];
-                    },
-                    itemCount: rows.length,
-                  ),
-                ),
-              ],
-            );
-          },
-          isLocal ? _searchTextLocal : _searchTextRemote,
-        ),
-      ),
-    );
-  }
-
-  void _jumpToEntry(bool isLocal, Entry entry,
-      ScrollController scrollController, double rowHeight) {
-    final entries = model.getCurrentDir(isLocal).entries;
-    final index = entries.indexOf(entry);
-    if (index == -1) {
-      debugPrint("entry is not valid: ${entry.path}");
-    }
-    final selectedEntries = getSelectedItems(isLocal);
-    final searchResult = entries.where((element) => element == entry);
-    selectedEntries.clear();
-    if (searchResult.isEmpty) {
-      return;
-    }
-    final offset = min(
-        max(scrollController.position.minScrollExtent,
-            entries.indexOf(searchResult.first) * rowHeight),
-        scrollController.position.maxScrollExtent);
-    scrollController.jumpTo(offset);
-    setState(() {
-      selectedEntries.add(isLocal, searchResult.first);
-      debugPrint("focused on ${searchResult.first.name}");
-    });
-  }
-
-  void _onSelectedChanged(SelectedItems selectedItems, List<Entry> entries,
-      Entry entry, bool isLocal) {
-    final isCtrlDown = RawKeyboard.instance.keysPressed
-        .contains(LogicalKeyboardKey.controlLeft);
-    final isShiftDown =
-        RawKeyboard.instance.keysPressed.contains(LogicalKeyboardKey.shiftLeft);
-    if (isCtrlDown) {
-      if (selectedItems.contains(entry)) {
-        selectedItems.remove(entry);
-      } else {
-        selectedItems.add(isLocal, entry);
-      }
-    } else if (isShiftDown) {
-      final List<int> indexGroup = [];
-      for (var selected in selectedItems.items) {
-        indexGroup.add(entries.indexOf(selected));
-      }
-      indexGroup.add(entries.indexOf(entry));
-      indexGroup.removeWhere((e) => e == -1);
-      final maxIndex = indexGroup.reduce(max);
-      final minIndex = indexGroup.reduce(min);
-      selectedItems.clear();
-      entries
-          .getRange(minIndex, maxIndex + 1)
-          .forEach((e) => selectedItems.add(isLocal, e));
-    } else {
-      selectedItems.clear();
-      selectedItems.add(isLocal, entry);
-    }
-    setState(() {});
-  }
-
-  bool _checkDoubleClick(Entry entry) {
-    final current = DateTime.now().millisecondsSinceEpoch;
-    final elapsed = current - _lastClickTime;
-    _lastClickTime = current;
-    if (_lastClickEntry == entry) {
-      if (elapsed < bind.getDoubleClickTime()) {
-        return true;
-      }
-    } else {
-      _lastClickEntry = entry;
-    }
-    return false;
+        child: fileView);
   }
 
   Widget generateCard(Widget child) {
@@ -581,179 +156,281 @@ class _FileManagerPageState extends State<FileManagerPage>
   /// transfer status list
   /// watch transfer status
   Widget statusList() {
+    statusListView(List<JobProgress> jobs) => ListView.builder(
+          controller: ScrollController(),
+          itemBuilder: (BuildContext context, int index) {
+            final item = jobs[index];
+            return Padding(
+              padding: const EdgeInsets.only(bottom: 5),
+              child: generateCard(
+                Column(
+                  mainAxisSize: MainAxisSize.min,
+                  children: [
+                    Row(
+                      crossAxisAlignment: CrossAxisAlignment.center,
+                      children: [
+                        Transform.rotate(
+                          angle: item.isRemoteToLocal ? pi : 0,
+                          child: SvgPicture.asset(
+                            "assets/arrow.svg",
+                            color: Theme.of(context).tabBarTheme.labelColor,
+                          ),
+                        ).paddingOnly(left: 15),
+                        const SizedBox(
+                          width: 16.0,
+                        ),
+                        Expanded(
+                          child: Column(
+                            mainAxisSize: MainAxisSize.min,
+                            crossAxisAlignment: CrossAxisAlignment.start,
+                            children: [
+                              Tooltip(
+                                waitDuration: Duration(milliseconds: 500),
+                                message: item.jobName,
+                                child: Text(
+                                  item.fileName,
+                                  maxLines: 1,
+                                  overflow: TextOverflow.ellipsis,
+                                ).paddingSymmetric(vertical: 10),
+                              ),
+                              Text(
+                                '${translate("Total")} ${readableFileSize(item.totalSize.toDouble())}',
+                                style: TextStyle(
+                                  fontSize: 12,
+                                  color: MyTheme.darkGray,
+                                ),
+                              ),
+                              Offstage(
+                                offstage: item.state != JobState.inProgress,
+                                child: Text(
+                                  '${translate("Speed")} ${readableFileSize(item.speed)}/s',
+                                  style: TextStyle(
+                                    fontSize: 12,
+                                    color: MyTheme.darkGray,
+                                  ),
+                                ),
+                              ),
+                              Offstage(
+                                offstage: item.state == JobState.inProgress,
+                                child: Text(
+                                  translate(
+                                    item.display(),
+                                  ),
+                                  style: TextStyle(
+                                    fontSize: 12,
+                                    color: MyTheme.darkGray,
+                                  ),
+                                ),
+                              ),
+                              Offstage(
+                                offstage: item.state != JobState.inProgress,
+                                child: LinearPercentIndicator(
+                                  padding: EdgeInsets.only(right: 15),
+                                  animateFromLastPercent: true,
+                                  center: Text(
+                                    '${(item.finishedSize / item.totalSize * 100).toStringAsFixed(0)}%',
+                                  ),
+                                  barRadius: Radius.circular(15),
+                                  percent: item.finishedSize / item.totalSize,
+                                  progressColor: MyTheme.accent,
+                                  backgroundColor: Theme.of(context).hoverColor,
+                                  lineHeight: kDesktopFileTransferRowHeight,
+                                ).paddingSymmetric(vertical: 15),
+                              ),
+                            ],
+                          ),
+                        ),
+                        Row(
+                          mainAxisAlignment: MainAxisAlignment.end,
+                          children: [
+                            Offstage(
+                              offstage: item.state != JobState.paused,
+                              child: MenuButton(
+                                onPressed: () {
+                                  jobController.resumeJob(item.id);
+                                },
+                                child: SvgPicture.asset(
+                                  "assets/refresh.svg",
+                                  color: Colors.white,
+                                ),
+                                color: MyTheme.accent,
+                                hoverColor: MyTheme.accent80,
+                              ),
+                            ),
+                            MenuButton(
+                              padding: EdgeInsets.only(right: 15),
+                              child: SvgPicture.asset(
+                                "assets/close.svg",
+                                color: Colors.white,
+                              ),
+                              onPressed: () {
+                                jobController.jobTable.removeAt(index);
+                                jobController.cancelJob(item.id);
+                              },
+                              color: MyTheme.accent,
+                              hoverColor: MyTheme.accent80,
+                            ),
+                          ],
+                        ),
+                      ],
+                    ),
+                  ],
+                ).paddingSymmetric(vertical: 10),
+              ),
+            );
+          },
+          itemCount: jobController.jobTable.length,
+        );
+
     return PreferredSize(
       preferredSize: const Size(200, double.infinity),
       child: Container(
-        margin: const EdgeInsets.only(top: 16.0, bottom: 16.0, right: 16.0),
-        padding: const EdgeInsets.all(8.0),
-        child: model.jobTable.isEmpty
-            ? generateCard(
-                Center(
-                  child: Column(
-                    mainAxisAlignment: MainAxisAlignment.center,
-                    children: [
-                      SvgPicture.asset(
-                        "assets/transfer.svg",
-                        color: Theme.of(context).tabBarTheme.labelColor,
-                        height: 40,
-                      ).paddingOnly(bottom: 10),
-                      Text(
-                        translate("No transfers in progress"),
-                        textAlign: TextAlign.center,
-                        textScaleFactor: 1.20,
-                        style: TextStyle(
-                            color: Theme.of(context).tabBarTheme.labelColor),
+          margin: const EdgeInsets.only(top: 16.0, bottom: 16.0, right: 16.0),
+          padding: const EdgeInsets.all(8.0),
+          child: Obx(
+            () => jobController.jobTable.isEmpty
+                ? generateCard(
+                    Center(
+                      child: Column(
+                        mainAxisAlignment: MainAxisAlignment.center,
+                        children: [
+                          SvgPicture.asset(
+                            "assets/transfer.svg",
+                            color: Theme.of(context).tabBarTheme.labelColor,
+                            height: 40,
+                          ).paddingOnly(bottom: 10),
+                          Text(
+                            translate("No transfers in progress"),
+                            textAlign: TextAlign.center,
+                            textScaleFactor: 1.20,
+                            style: TextStyle(
+                                color:
+                                    Theme.of(context).tabBarTheme.labelColor),
+                          ),
+                        ],
                       ),
-                    ],
-                  ),
-                ),
-              )
-            : Obx(
-                () => ListView.builder(
-                  controller: ScrollController(),
-                  itemBuilder: (BuildContext context, int index) {
-                    final item = model.jobTable[index];
-                    return Padding(
-                      padding: const EdgeInsets.only(bottom: 5),
-                      child: generateCard(
-                        Column(
-                          mainAxisSize: MainAxisSize.min,
-                          children: [
-                            Row(
-                              crossAxisAlignment: CrossAxisAlignment.center,
-                              children: [
-                                Transform.rotate(
-                                  angle: item.isRemote ? pi : 0,
-                                  child: SvgPicture.asset(
-                                    "assets/arrow.svg",
-                                    color: Theme.of(context)
-                                        .tabBarTheme
-                                        .labelColor,
-                                  ),
-                                ).paddingOnly(left: 15),
-                                const SizedBox(
-                                  width: 16.0,
-                                ),
-                                Expanded(
-                                  child: Column(
-                                    mainAxisSize: MainAxisSize.min,
-                                    crossAxisAlignment:
-                                        CrossAxisAlignment.start,
-                                    children: [
-                                      Tooltip(
-                                        waitDuration:
-                                            Duration(milliseconds: 500),
-                                        message: item.jobName,
-                                        child: Text(
-                                          item.fileName,
-                                          maxLines: 1,
-                                          overflow: TextOverflow.ellipsis,
-                                        ).paddingSymmetric(vertical: 10),
-                                      ),
-                                      Text(
-                                        '${translate("Total")} ${readableFileSize(item.totalSize.toDouble())}',
-                                        style: TextStyle(
-                                          fontSize: 12,
-                                          color: MyTheme.darkGray,
-                                        ),
-                                      ),
-                                      Offstage(
-                                        offstage:
-                                            item.state != JobState.inProgress,
-                                        child: Text(
-                                          '${translate("Speed")} ${readableFileSize(item.speed)}/s',
-                                          style: TextStyle(
-                                            fontSize: 12,
-                                            color: MyTheme.darkGray,
-                                          ),
-                                        ),
-                                      ),
-                                      Offstage(
-                                        offstage:
-                                            item.state == JobState.inProgress,
-                                        child: Text(
-                                          translate(
-                                            item.display(),
-                                          ),
-                                          style: TextStyle(
-                                            fontSize: 12,
-                                            color: MyTheme.darkGray,
-                                          ),
-                                        ),
-                                      ),
-                                      Offstage(
-                                        offstage:
-                                            item.state != JobState.inProgress,
-                                        child: LinearPercentIndicator(
-                                          padding: EdgeInsets.only(right: 15),
-                                          animateFromLastPercent: true,
-                                          center: Text(
-                                            '${(item.finishedSize / item.totalSize * 100).toStringAsFixed(0)}%',
-                                          ),
-                                          barRadius: Radius.circular(15),
-                                          percent: item.finishedSize /
-                                              item.totalSize,
-                                          progressColor: MyTheme.accent,
-                                          backgroundColor:
-                                              Theme.of(context).hoverColor,
-                                          lineHeight:
-                                              kDesktopFileTransferRowHeight,
-                                        ).paddingSymmetric(vertical: 15),
-                                      ),
-                                    ],
-                                  ),
-                                ),
-                                Row(
-                                  mainAxisAlignment: MainAxisAlignment.end,
-                                  children: [
-                                    Offstage(
-                                      offstage: item.state != JobState.paused,
-                                      child: MenuButton(
-                                        onPressed: () {
-                                          model.resumeJob(item.id);
-                                        },
-                                        child: SvgPicture.asset(
-                                          "assets/refresh.svg",
-                                          color: Colors.white,
-                                        ),
-                                        color: MyTheme.accent,
-                                        hoverColor: MyTheme.accent80,
-                                      ),
-                                    ),
-                                    MenuButton(
-                                      padding: EdgeInsets.only(right: 15),
-                                      child: SvgPicture.asset(
-                                        "assets/close.svg",
-                                        color: Colors.white,
-                                      ),
-                                      onPressed: () {
-                                        model.jobTable.removeAt(index);
-                                        model.cancelJob(item.id);
-                                      },
-                                      color: MyTheme.accent,
-                                      hoverColor: MyTheme.accent80,
-                                    ),
-                                  ],
-                                ),
-                              ],
-                            ),
-                          ],
-                        ).paddingSymmetric(vertical: 10),
-                      ),
-                    );
+                    ),
+                  )
+                : statusListView(jobController.jobTable),
+          )),
+    );
+  }
+
+  void handleDragDone(DropDoneDetails details, bool isLocal) {
+    if (isLocal) {
+      // ignore local
+      return;
+    }
+    final items = SelectedItems(isLocal: false);
+    for (var file in details.files) {
+      final f = File(file.path);
+      items.add(Entry()
+        ..path = file.path
+        ..name = file.name
+        ..size = FileSystemEntity.isDirectorySync(f.path) ? 0 : f.lengthSync());
+    }
+    final otherSideData = model.localController.directoryData();
+    model.remoteController.sendFiles(items, otherSideData);
+  }
+}
+
+class FileManagerView extends StatefulWidget {
+  final FileController controller;
+  final FFI _ffi;
+  final Rx<MouseFocusScope> _mouseFocusScope;
+
+  FileManagerView(this.controller, this._ffi, this._mouseFocusScope);
+
+  @override
+  State<StatefulWidget> createState() => _FileManagerViewState();
+}
+
+class _FileManagerViewState extends State<FileManagerView> {
+  final _locationStatus = LocationStatus.bread.obs;
+  final _locationNode = FocusNode();
+  final _locationBarKey = GlobalKey();
+  final _searchText = "".obs;
+  final _breadCrumbScroller = ScrollController();
+  final _keyboardNode = FocusNode();
+  final _listSearchBuffer = TimeoutStringBuffer();
+  final _nameColWidth = kDesktopFileTransferNameColWidth.obs;
+  final _modifiedColWidth = kDesktopFileTransferModifiedColWidth.obs;
+  final _fileListScrollController = ScrollController();
+
+  /// [_lastClickTime], [_lastClickEntry] help to handle double click
+  var _lastClickTime =
+      DateTime.now().millisecondsSinceEpoch - bind.getDoubleClickTime() - 1000;
+  Entry? _lastClickEntry;
+
+  FileController get controller => widget.controller;
+  bool get isLocal => widget.controller.isLocal;
+  FFI get _ffi => widget._ffi;
+  SelectedItems get selectedItems => controller.selectedItems;
+
+  @override
+  void initState() {
+    super.initState();
+    // register location listener
+    _locationNode.addListener(onLocationFocusChanged);
+    controller.directory.listen((e) => breadCrumbScrollToEnd());
+  }
+
+  @override
+  void dispose() {
+    _locationNode.removeListener(onLocationFocusChanged);
+    _locationNode.dispose();
+    _keyboardNode.dispose();
+    _breadCrumbScroller.dispose();
+    _fileListScrollController.dispose();
+    super.dispose();
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return Container(
+      margin: const EdgeInsets.all(16.0),
+      padding: const EdgeInsets.all(8.0),
+      child: Column(
+        crossAxisAlignment: CrossAxisAlignment.start,
+        children: [
+          headTools(),
+          Expanded(
+            child: Row(
+              crossAxisAlignment: CrossAxisAlignment.start,
+              children: [
+                Expanded(
+                    child: MouseRegion(
+                  onEnter: (evt) {
+                    widget._mouseFocusScope.value = isLocal
+                        ? MouseFocusScope.local
+                        : MouseFocusScope.remote;
+                    _keyboardNode.requestFocus();
                   },
-                  itemCount: model.jobTable.length,
-                ),
-              ),
+                  onExit: (evt) =>
+                      widget._mouseFocusScope.value = MouseFocusScope.none,
+                  child: _buildFileList(context, _fileListScrollController),
+                ))
+              ],
+            ),
+          ),
+        ],
       ),
     );
   }
 
-  Widget headTools(bool isLocal) {
-    final locationStatus =
-        isLocal ? _locationStatusLocal : _locationStatusRemote;
-    final locationFocus = isLocal ? _locationNodeLocal : _locationNodeRemote;
-    final selectedItems = getSelectedItems(isLocal);
+  void onLocationFocusChanged() {
+    debugPrint("focus changed on local");
+    if (_locationNode.hasFocus) {
+      // ignore
+    } else {
+      // lost focus, change to bread
+      if (_locationStatus.value != LocationStatus.fileSearchBar) {
+        _locationStatus.value = LocationStatus.bread;
+      }
+    }
+  }
+
+  Widget headTools() {
     return Container(
       child: Column(
         children: [
@@ -813,7 +490,7 @@ class _FileManagerPageState extends State<FileManagerPage>
                     hoverColor: Theme.of(context).hoverColor,
                     onPressed: () {
                       selectedItems.clear();
-                      model.goBack(isLocal: isLocal);
+                      controller.goBack();
                     },
                   ),
                   MenuButton(
@@ -828,7 +505,7 @@ class _FileManagerPageState extends State<FileManagerPage>
                     hoverColor: Theme.of(context).hoverColor,
                     onPressed: () {
                       selectedItems.clear();
-                      model.goToParentDirectory(isLocal: isLocal);
+                      controller.goToParentDirectory();
                     },
                   ),
                 ],
@@ -847,14 +524,14 @@ class _FileManagerPageState extends State<FileManagerPage>
                       padding: EdgeInsets.symmetric(vertical: 2.5),
                       child: GestureDetector(
                         onTap: () {
-                          locationStatus.value =
-                              locationStatus.value == LocationStatus.bread
+                          _locationStatus.value =
+                              _locationStatus.value == LocationStatus.bread
                                   ? LocationStatus.pathLocation
                                   : LocationStatus.bread;
                           Future.delayed(Duration.zero, () {
-                            if (locationStatus.value ==
+                            if (_locationStatus.value ==
                                 LocationStatus.pathLocation) {
-                              locationFocus.requestFocus();
+                              _locationNode.requestFocus();
                             }
                           });
                         },
@@ -863,10 +540,10 @@ class _FileManagerPageState extends State<FileManagerPage>
                             child: Row(
                               children: [
                                 Expanded(
-                                    child: locationStatus.value ==
+                                    child: _locationStatus.value ==
                                             LocationStatus.bread
-                                        ? buildBread(isLocal)
-                                        : buildPathLocation(isLocal)),
+                                        ? buildBread()
+                                        : buildPathLocation()),
                               ],
                             ),
                           ),
@@ -877,15 +554,13 @@ class _FileManagerPageState extends State<FileManagerPage>
                 ),
               ),
               Obx(() {
-                switch (locationStatus.value) {
+                switch (_locationStatus.value) {
                   case LocationStatus.bread:
                     return MenuButton(
                       onPressed: () {
-                        locationStatus.value = LocationStatus.fileSearchBar;
-                        final focusNode =
-                            isLocal ? _locationNodeLocal : _locationNodeRemote;
+                        _locationStatus.value = LocationStatus.fileSearchBar;
                         Future.delayed(
-                            Duration.zero, () => focusNode.requestFocus());
+                            Duration.zero, () => _locationNode.requestFocus());
                       },
                       child: SvgPicture.asset(
                         "assets/search.svg",
@@ -908,7 +583,7 @@ class _FileManagerPageState extends State<FileManagerPage>
                     return MenuButton(
                       onPressed: () {
                         onSearchText("", isLocal);
-                        locationStatus.value = LocationStatus.bread;
+                        _locationStatus.value = LocationStatus.bread;
                       },
                       child: SvgPicture.asset(
                         "assets/close.svg",
@@ -924,7 +599,7 @@ class _FileManagerPageState extends State<FileManagerPage>
                   left: 3,
                 ),
                 onPressed: () {
-                  model.refresh(isLocal: isLocal);
+                  controller.refresh();
                 },
                 child: SvgPicture.asset(
                   "assets/refresh.svg",
@@ -948,7 +623,7 @@ class _FileManagerPageState extends State<FileManagerPage>
                         right: 3,
                       ),
                       onPressed: () {
-                        model.goHome(isLocal: isLocal);
+                        controller.goToHomeDirectory();
                       },
                       child: SvgPicture.asset(
                         "assets/home.svg",
@@ -963,12 +638,11 @@ class _FileManagerPageState extends State<FileManagerPage>
                         _ffi.dialogManager.show((setState, close) {
                           submit() {
                             if (name.value.text.isNotEmpty) {
-                              model.createDir(
-                                  PathUtil.join(
-                                      model.getCurrentDir(isLocal).path,
-                                      name.value.text,
-                                      model.getCurrentIsWindows(isLocal)),
-                                  isLocal: isLocal);
+                              controller.createDir(PathUtil.join(
+                                controller.directory.value.path,
+                                name.value.text,
+                                controller.options.value.isWindows,
+                              ));
                               close();
                             }
                           }
@@ -1026,86 +700,93 @@ class _FileManagerPageState extends State<FileManagerPage>
                       color: Theme.of(context).cardColor,
                       hoverColor: Theme.of(context).hoverColor,
                     ),
-                    MenuButton(
-                      onPressed: validItems(selectedItems)
-                          ? () async {
-                              await (model.removeAction(selectedItems,
-                                  isLocal: isLocal));
-                              selectedItems.clear();
-                            }
-                          : null,
-                      child: SvgPicture.asset(
-                        "assets/trash.svg",
-                        color: Theme.of(context).tabBarTheme.labelColor,
-                      ),
-                      color: Theme.of(context).cardColor,
-                      hoverColor: Theme.of(context).hoverColor,
-                    ),
+                    Obx(() => MenuButton(
+                          onPressed: SelectedItems.valid(selectedItems.items)
+                              ? () async {
+                                  await (controller
+                                      .removeAction(selectedItems));
+                                  selectedItems.clear();
+                                }
+                              : null,
+                          child: SvgPicture.asset(
+                            "assets/trash.svg",
+                            color: Theme.of(context).tabBarTheme.labelColor,
+                          ),
+                          color: Theme.of(context).cardColor,
+                          hoverColor: Theme.of(context).hoverColor,
+                        )),
                     menu(isLocal: isLocal),
                   ],
                 ),
               ),
-              ElevatedButton.icon(
-                style: ButtonStyle(
-                  padding: MaterialStateProperty.all<EdgeInsetsGeometry>(isLocal
-                      ? EdgeInsets.only(left: 10)
-                      : EdgeInsets.only(right: 10)),
-                  backgroundColor: MaterialStateProperty.all(
-                    selectedItems.length == 0
-                        ? MyTheme.accent80
-                        : MyTheme.accent,
-                  ),
-                ),
-                onPressed: validItems(selectedItems)
-                    ? () {
-                        model.sendFiles(selectedItems, isRemote: !isLocal);
-                        selectedItems.clear();
-                      }
-                    : null,
-                icon: isLocal
-                    ? Text(
-                        translate('Send'),
-                        textAlign: TextAlign.right,
-                        style: TextStyle(
-                          color: selectedItems.length == 0
-                              ? Theme.of(context).brightness == Brightness.light
-                                  ? MyTheme.grayBg
-                                  : MyTheme.darkGray
-                              : Colors.white,
-                        ),
-                      )
-                    : RotatedBox(
-                        quarterTurns: 2,
-                        child: SvgPicture.asset(
-                          "assets/arrow.svg",
-                          color: selectedItems.length == 0
-                              ? Theme.of(context).brightness == Brightness.light
-                                  ? MyTheme.grayBg
-                                  : MyTheme.darkGray
-                              : Colors.white,
-                          alignment: Alignment.bottomRight,
-                        ),
+              Obx(() => ElevatedButton.icon(
+                    style: ButtonStyle(
+                      padding: MaterialStateProperty.all<EdgeInsetsGeometry>(
+                          isLocal
+                              ? EdgeInsets.only(left: 10)
+                              : EdgeInsets.only(right: 10)),
+                      backgroundColor: MaterialStateProperty.all(
+                        selectedItems.items.isEmpty
+                            ? MyTheme.accent80
+                            : MyTheme.accent,
                       ),
-                label: isLocal
-                    ? SvgPicture.asset(
-                        "assets/arrow.svg",
-                        color: selectedItems.length == 0
-                            ? Theme.of(context).brightness == Brightness.light
-                                ? MyTheme.grayBg
-                                : MyTheme.darkGray
-                            : Colors.white,
-                      )
-                    : Text(
-                        translate('Receive'),
-                        style: TextStyle(
-                          color: selectedItems.length == 0
-                              ? Theme.of(context).brightness == Brightness.light
-                                  ? MyTheme.grayBg
-                                  : MyTheme.darkGray
-                              : Colors.white,
-                        ),
-                      ),
-              ),
+                    ),
+                    onPressed: SelectedItems.valid(selectedItems.items)
+                        ? () {
+                            final otherSideData =
+                                controller.getOtherSideDirectoryData();
+                            controller.sendFiles(selectedItems, otherSideData);
+                            selectedItems.clear();
+                          }
+                        : null,
+                    icon: isLocal
+                        ? Text(
+                            translate('Send'),
+                            textAlign: TextAlign.right,
+                            style: TextStyle(
+                              color: selectedItems.items.isEmpty
+                                  ? Theme.of(context).brightness ==
+                                          Brightness.light
+                                      ? MyTheme.grayBg
+                                      : MyTheme.darkGray
+                                  : Colors.white,
+                            ),
+                          )
+                        : RotatedBox(
+                            quarterTurns: 2,
+                            child: SvgPicture.asset(
+                              "assets/arrow.svg",
+                              color: selectedItems.items.isEmpty
+                                  ? Theme.of(context).brightness ==
+                                          Brightness.light
+                                      ? MyTheme.grayBg
+                                      : MyTheme.darkGray
+                                  : Colors.white,
+                              alignment: Alignment.bottomRight,
+                            ),
+                          ),
+                    label: isLocal
+                        ? SvgPicture.asset(
+                            "assets/arrow.svg",
+                            color: selectedItems.items.isEmpty
+                                ? Theme.of(context).brightness ==
+                                        Brightness.light
+                                    ? MyTheme.grayBg
+                                    : MyTheme.darkGray
+                                : Colors.white,
+                          )
+                        : Text(
+                            translate('Receive'),
+                            style: TextStyle(
+                              color: selectedItems.items.isEmpty
+                                  ? Theme.of(context).brightness ==
+                                          Brightness.light
+                                      ? MyTheme.grayBg
+                                      : MyTheme.darkGray
+                                  : Colors.white,
+                            ),
+                          ),
+                  )),
             ],
           ).marginOnly(top: 8.0)
         ],
@@ -1113,55 +794,443 @@ class _FileManagerPageState extends State<FileManagerPage>
     );
   }
 
-  bool validItems(SelectedItems items) {
-    if (items.length > 0) {
-      // exclude DirDrive type
-      return items.items.any((item) => !item.isDrive);
+  Widget menu({bool isLocal = false}) {
+    var menuPos = RelativeRect.fill;
+
+    final List<MenuEntryBase<String>> items = [
+      MenuEntrySwitch<String>(
+        switchType: SwitchType.scheckbox,
+        text: translate("Show Hidden Files"),
+        getter: () async {
+          return controller.options.value.isWindows;
+        },
+        setter: (bool v) async {
+          controller.toggleShowHidden();
+        },
+        padding: kDesktopMenuPadding,
+        dismissOnClicked: true,
+      ),
+      MenuEntryButton(
+          childBuilder: (style) => Text(translate("Select All"), style: style),
+          proc: () => setState(() =>
+              selectedItems.selectAll(controller.directory.value.entries)),
+          padding: kDesktopMenuPadding,
+          dismissOnClicked: true),
+      MenuEntryButton(
+          childBuilder: (style) =>
+              Text(translate("Unselect All"), style: style),
+          proc: () => selectedItems.clear(),
+          padding: kDesktopMenuPadding,
+          dismissOnClicked: true)
+    ];
+
+    return Listener(
+      onPointerDown: (e) {
+        final x = e.position.dx;
+        final y = e.position.dy;
+        menuPos = RelativeRect.fromLTRB(x, y, x, y);
+      },
+      child: MenuButton(
+        onPressed: () => mod_menu.showMenu(
+          context: context,
+          position: menuPos,
+          items: items
+              .map(
+                (e) => e.build(
+                  context,
+                  MenuConfig(
+                      commonColor: CustomPopupMenuTheme.commonColor,
+                      height: CustomPopupMenuTheme.height,
+                      dividerHeight: CustomPopupMenuTheme.dividerHeight),
+                ),
+              )
+              .expand((i) => i)
+              .toList(),
+          elevation: 8,
+        ),
+        child: SvgPicture.asset(
+          "assets/dots.svg",
+          color: Theme.of(context).tabBarTheme.labelColor,
+        ),
+        color: Theme.of(context).cardColor,
+        hoverColor: Theme.of(context).hoverColor,
+      ),
+    );
+  }
+
+  Widget _buildFileList(
+      BuildContext context, ScrollController scrollController) {
+    final fd = controller.directory.value;
+    final entries = fd.entries;
+
+    return ListSearchActionListener(
+      node: _keyboardNode,
+      buffer: _listSearchBuffer,
+      onNext: (buffer) {
+        debugPrint("searching next for $buffer");
+        assert(buffer.length == 1);
+        assert(selectedItems.items.length <= 1);
+        var skipCount = 0;
+        if (selectedItems.items.isNotEmpty) {
+          final index = entries.indexOf(selectedItems.items.first);
+          if (index < 0) {
+            return;
+          }
+          skipCount = index + 1;
+        }
+        var searchResult = entries
+            .skip(skipCount)
+            .where((element) => element.name.toLowerCase().startsWith(buffer));
+        if (searchResult.isEmpty) {
+          // cannot find next, lets restart search from head
+          debugPrint("restart search from head");
+          searchResult = entries.where(
+              (element) => element.name.toLowerCase().startsWith(buffer));
+        }
+        if (searchResult.isEmpty) {
+          selectedItems.clear();
+          return;
+        }
+        _jumpToEntry(isLocal, searchResult.first, scrollController,
+            kDesktopFileTransferRowHeight);
+      },
+      onSearch: (buffer) {
+        debugPrint("searching for $buffer");
+        final selectedEntries = selectedItems;
+        final searchResult = entries
+            .where((element) => element.name.toLowerCase().startsWith(buffer));
+        selectedEntries.clear();
+        if (searchResult.isEmpty) {
+          selectedItems.clear();
+          return;
+        }
+        _jumpToEntry(isLocal, searchResult.first, scrollController,
+            kDesktopFileTransferRowHeight);
+      },
+      child: Obx(() {
+        final entries = controller.directory.value.entries;
+        final filteredEntries = _searchText.isNotEmpty
+            ? entries.where((element) {
+                return element.name.contains(_searchText.value);
+              }).toList(growable: false)
+            : entries;
+        final rows = filteredEntries.map((entry) {
+          final sizeStr =
+              entry.isFile ? readableFileSize(entry.size.toDouble()) : "";
+          final lastModifiedStr = entry.isDrive
+              ? " "
+              : "${entry.lastModified().toString().replaceAll(".000", "")}   ";
+          return Padding(
+            padding: EdgeInsets.symmetric(vertical: 1),
+            child: Obx(() => Container(
+                decoration: BoxDecoration(
+                  color: selectedItems.items.contains(entry)
+                      ? Theme.of(context).hoverColor
+                      : Theme.of(context).cardColor,
+                  borderRadius: BorderRadius.all(
+                    Radius.circular(5.0),
+                  ),
+                ),
+                key: ValueKey(entry.name),
+                height: kDesktopFileTransferRowHeight,
+                child: Column(
+                  mainAxisAlignment: MainAxisAlignment.spaceAround,
+                  children: [
+                    Expanded(
+                      child: InkWell(
+                        child: Row(
+                          children: [
+                            GestureDetector(
+                              child: Obx(
+                                () => Container(
+                                    width: _nameColWidth.value,
+                                    child: Tooltip(
+                                      waitDuration: Duration(milliseconds: 500),
+                                      message: entry.name,
+                                      child: Row(children: [
+                                        entry.isDrive
+                                            ? Image(
+                                                    image: iconHardDrive,
+                                                    fit: BoxFit.scaleDown,
+                                                    color: Theme.of(context)
+                                                        .iconTheme
+                                                        .color
+                                                        ?.withOpacity(0.7))
+                                                .paddingAll(4)
+                                            : SvgPicture.asset(
+                                                entry.isFile
+                                                    ? "assets/file.svg"
+                                                    : "assets/folder.svg",
+                                                color: Theme.of(context)
+                                                    .tabBarTheme
+                                                    .labelColor,
+                                              ),
+                                        Expanded(
+                                            child: Text(entry.name.nonBreaking,
+                                                overflow:
+                                                    TextOverflow.ellipsis))
+                                      ]),
+                                    )),
+                              ),
+                              onTap: () {
+                                final items = selectedItems;
+                                // handle double click
+                                if (_checkDoubleClick(entry)) {
+                                  controller.openDirectory(entry.path);
+                                  items.clear();
+                                  return;
+                                }
+                                _onSelectedChanged(
+                                    items, filteredEntries, entry, isLocal);
+                              },
+                            ),
+                            SizedBox(
+                              width: 2.0,
+                            ),
+                            GestureDetector(
+                              child: Obx(
+                                () => SizedBox(
+                                  width: _modifiedColWidth.value,
+                                  child: Tooltip(
+                                      waitDuration: Duration(milliseconds: 500),
+                                      message: lastModifiedStr,
+                                      child: Text(
+                                        lastModifiedStr,
+                                        overflow: TextOverflow.ellipsis,
+                                        style: TextStyle(
+                                          fontSize: 12,
+                                          color: MyTheme.darkGray,
+                                        ),
+                                      )),
+                                ),
+                              ),
+                            ),
+                            // Divider from header.
+                            SizedBox(
+                              width: 2.0,
+                            ),
+                            Expanded(
+                              // width: 100,
+                              child: GestureDetector(
+                                child: Tooltip(
+                                  waitDuration: Duration(milliseconds: 500),
+                                  message: sizeStr,
+                                  child: Text(
+                                    sizeStr,
+                                    overflow: TextOverflow.ellipsis,
+                                    style: TextStyle(
+                                        fontSize: 10, color: MyTheme.darkGray),
+                                  ),
+                                ),
+                              ),
+                            ),
+                          ],
+                        ),
+                      ),
+                    ),
+                  ],
+                ))),
+          );
+        }).toList(growable: false);
+
+        return Column(
+          children: [
+            // Header
+            Row(
+              children: [
+                Expanded(child: _buildFileBrowserHeader(context)),
+              ],
+            ),
+            // Body
+            Expanded(
+              child: ListView.builder(
+                controller: scrollController,
+                itemExtent: kDesktopFileTransferRowHeight,
+                itemBuilder: (context, index) {
+                  return rows[index];
+                },
+                itemCount: rows.length,
+              ),
+            ),
+          ],
+        );
+      }),
+    );
+  }
+
+  onSearchText(String searchText, bool isLocal) {
+    selectedItems.clear();
+    _searchText.value = searchText;
+  }
+
+  void _jumpToEntry(bool isLocal, Entry entry,
+      ScrollController scrollController, double rowHeight) {
+    final entries = controller.directory.value.entries;
+    final index = entries.indexOf(entry);
+    if (index == -1) {
+      debugPrint("entry is not valid: ${entry.path}");
+    }
+    final selectedEntries = selectedItems;
+    final searchResult = entries.where((element) => element == entry);
+    selectedEntries.clear();
+    if (searchResult.isEmpty) {
+      return;
+    }
+    final offset = min(
+        max(scrollController.position.minScrollExtent,
+            entries.indexOf(searchResult.first) * rowHeight),
+        scrollController.position.maxScrollExtent);
+    scrollController.jumpTo(offset);
+    selectedEntries.add(searchResult.first);
+    debugPrint("focused on ${searchResult.first.name}");
+  }
+
+  void _onSelectedChanged(SelectedItems selectedItems, List<Entry> entries,
+      Entry entry, bool isLocal) {
+    final isCtrlDown = RawKeyboard.instance.keysPressed
+        .contains(LogicalKeyboardKey.controlLeft);
+    final isShiftDown =
+        RawKeyboard.instance.keysPressed.contains(LogicalKeyboardKey.shiftLeft);
+    if (isCtrlDown) {
+      if (selectedItems.items.contains(entry)) {
+        selectedItems.remove(entry);
+      } else {
+        selectedItems.add(entry);
+      }
+    } else if (isShiftDown) {
+      final List<int> indexGroup = [];
+      for (var selected in selectedItems.items) {
+        indexGroup.add(entries.indexOf(selected));
+      }
+      indexGroup.add(entries.indexOf(entry));
+      indexGroup.removeWhere((e) => e == -1);
+      final maxIndex = indexGroup.reduce(max);
+      final minIndex = indexGroup.reduce(min);
+      selectedItems.clear();
+      entries
+          .getRange(minIndex, maxIndex + 1)
+          .forEach((e) => selectedItems.add(e));
+    } else {
+      selectedItems.clear();
+      selectedItems.add(entry);
+    }
+    setState(() {});
+  }
+
+  bool _checkDoubleClick(Entry entry) {
+    final current = DateTime.now().millisecondsSinceEpoch;
+    final elapsed = current - _lastClickTime;
+    _lastClickTime = current;
+    if (_lastClickEntry == entry) {
+      if (elapsed < bind.getDoubleClickTime()) {
+        return true;
+      }
+    } else {
+      _lastClickEntry = entry;
     }
     return false;
   }
 
-  @override
-  bool get wantKeepAlive => true;
-
-  void onLocalLocationFocusChanged() {
-    debugPrint("focus changed on local");
-    if (_locationNodeLocal.hasFocus) {
-      // ignore
-    } else {
-      // lost focus, change to bread
-      if (_locationStatusLocal.value != LocationStatus.fileSearchBar) {
-        _locationStatusLocal.value = LocationStatus.bread;
-      }
-    }
+  Widget _buildFileBrowserHeader(BuildContext context) {
+    final padding = EdgeInsets.all(1.0);
+    return SizedBox(
+      height: kDesktopFileTransferHeaderHeight,
+      child: Row(
+        children: [
+          Obx(
+            () => headerItemFunc(
+                _nameColWidth.value, SortBy.name, translate("Name")),
+          ),
+          DraggableDivider(
+            axis: Axis.vertical,
+            onPointerMove: (dx) {
+              _nameColWidth.value += dx;
+              _nameColWidth.value = min(kDesktopFileTransferMaximumWidth,
+                  max(kDesktopFileTransferMinimumWidth, _nameColWidth.value));
+            },
+            padding: padding,
+          ),
+          Obx(
+            () => headerItemFunc(_modifiedColWidth.value, SortBy.modified,
+                translate("Modified")),
+          ),
+          DraggableDivider(
+              axis: Axis.vertical,
+              onPointerMove: (dx) {
+                _modifiedColWidth.value += dx;
+                _modifiedColWidth.value = min(
+                    kDesktopFileTransferMaximumWidth,
+                    max(kDesktopFileTransferMinimumWidth,
+                        _modifiedColWidth.value));
+              },
+              padding: padding),
+          Expanded(child: headerItemFunc(null, SortBy.size, translate("Size")))
+        ],
+      ),
+    );
   }
 
-  void onRemoteLocationFocusChanged() {
-    debugPrint("focus changed on remote");
-    if (_locationNodeRemote.hasFocus) {
-      // ignore
-    } else {
-      // lost focus, change to bread
-      if (_locationStatusRemote.value != LocationStatus.fileSearchBar) {
-        _locationStatusRemote.value = LocationStatus.bread;
+  Widget headerItemFunc(double? width, SortBy sortBy, String name) {
+    final headerTextStyle =
+        Theme.of(context).dataTableTheme.headingTextStyle ?? TextStyle();
+    return ObxValue<Rx<bool?>>(
+        (ascending) => InkWell(
+              onTap: () {
+                if (ascending.value == null) {
+                  ascending.value = true;
+                } else {
+                  ascending.value = !ascending.value!;
+                }
+                controller.changeSortStyle(sortBy,
+                    isLocal: isLocal, ascending: ascending.value!);
+              },
+              child: SizedBox(
+                width: width,
+                height: kDesktopFileTransferHeaderHeight,
+                child: Row(
+                  children: [
+                    Flexible(
+                      flex: 2,
+                      child: Text(
+                        name,
+                        style: headerTextStyle,
+                        overflow: TextOverflow.ellipsis,
+                      ).marginSymmetric(horizontal: 4),
+                    ),
+                    Flexible(
+                        flex: 1,
+                        child: ascending.value != null
+                            ? Icon(
+                                ascending.value!
+                                    ? Icons.keyboard_arrow_up_rounded
+                                    : Icons.keyboard_arrow_down_rounded,
+                              )
+                            : const Offstage())
+                  ],
+                ),
+              ),
+            ), () {
+      if (controller.sortBy.value == sortBy) {
+        return controller.sortAscending.obs;
+      } else {
+        return Rx<bool?>(null);
       }
-    }
+    }());
   }
 
-  Widget buildBread(bool isLocal) {
+  Widget buildBread() {
     final items = getPathBreadCrumbItems(isLocal, (list) {
       var path = "";
       for (var item in list) {
-        path = PathUtil.join(path, item, model.getCurrentIsWindows(isLocal));
+        path = PathUtil.join(path, item, controller.options.value.isWindows);
       }
-      openDirectory(path, isLocal: isLocal);
+      controller.openDirectory(path);
     });
-    final locationBarKey = getLocationBarKey(isLocal);
 
     return items.isEmpty
         ? Offstage()
         : Row(
-            key: locationBarKey,
+            key: _locationBarKey,
             mainAxisAlignment: MainAxisAlignment.spaceBetween,
             children: [
                 Expanded(
@@ -1169,7 +1238,7 @@ class _FileManagerPageState extends State<FileManagerPage>
                     // handle mouse wheel
                     onPointerSignal: (e) {
                       if (e is PointerScrollEvent) {
-                        final sc = getBreadCrumbScrollController(isLocal);
+                        final sc = _breadCrumbScroller;
                         final scale = Platform.isWindows ? 2 : 4;
                         sc.jumpTo(sc.offset + e.scrollDelta.dy / scale);
                       }
@@ -1178,7 +1247,7 @@ class _FileManagerPageState extends State<FileManagerPage>
                       items: items,
                       divider: const Icon(Icons.keyboard_arrow_right_rounded),
                       overflow: ScrollableOverflow(
-                        controller: getBreadCrumbScrollController(isLocal),
+                        controller: _breadCrumbScroller,
                       ),
                     ),
                   ),
@@ -1187,9 +1256,9 @@ class _FileManagerPageState extends State<FileManagerPage>
                   message: "",
                   icon: Icons.keyboard_arrow_down_rounded,
                   onTap: () async {
-                    final renderBox = locationBarKey.currentContext
+                    final renderBox = _locationBarKey.currentContext
                         ?.findRenderObject() as RenderBox;
-                    locationBarKey.currentContext?.size;
+                    _locationBarKey.currentContext?.size;
 
                     final size = renderBox.size;
                     final offset = renderBox.localToGlobal(Offset.zero);
@@ -1197,17 +1266,17 @@ class _FileManagerPageState extends State<FileManagerPage>
                     final x = offset.dx;
                     final y = offset.dy + size.height + 1;
 
-                    final isPeerWindows = model.getCurrentIsWindows(isLocal);
+                    final isPeerWindows = controller.options.value.isWindows;
                     final List<MenuEntryBase> menuItems = [
                       MenuEntryButton(
                           childBuilder: (TextStyle? style) => isPeerWindows
-                              ? buildWindowsThisPC(style)
+                              ? buildWindowsThisPC(context, style)
                               : Text(
                                   '/',
                                   style: style,
                                 ),
                           proc: () {
-                            openDirectory('/', isLocal: isLocal);
+                            controller.openDirectory('/');
                           },
                           dismissOnClicked: true),
                       MenuEntryDivider()
@@ -1218,8 +1287,9 @@ class _FileManagerPageState extends State<FileManagerPage>
                         loadingTag = _ffi.dialogManager.showLoading("Waiting");
                       }
                       try {
-                        final fd =
-                            await model.fetchDirectory("/", isLocal, isLocal);
+                        final showHidden = controller.options.value.showHidden;
+                        final fd = await controller.fileFetcher
+                            .fetchDirectory("/", isLocal, showHidden);
                         for (var entry in fd.entries) {
                           menuItems.add(MenuEntryButton(
                               childBuilder: (TextStyle? style) =>
@@ -1238,8 +1308,7 @@ class _FileManagerPageState extends State<FileManagerPage>
                                     )
                                   ]),
                               proc: () {
-                                openDirectory('${entry.name}\\',
-                                    isLocal: isLocal);
+                                controller.openDirectory('${entry.name}\\');
                               },
                               dismissOnClicked: true));
                         }
@@ -1274,24 +1343,15 @@ class _FileManagerPageState extends State<FileManagerPage>
               ]);
   }
 
-  Widget buildWindowsThisPC([TextStyle? textStyle]) {
-    final color = Theme.of(context).iconTheme.color?.withOpacity(0.7);
-    return Row(children: [
-      Icon(Icons.computer, size: 20, color: color),
-      SizedBox(width: 10),
-      Text(translate('This PC'), style: textStyle)
-    ]);
-  }
-
   List<BreadCrumbItem> getPathBreadCrumbItems(
       bool isLocal, void Function(List<String>) onPressed) {
-    final path = model.getCurrentDir(isLocal).path;
+    final path = controller.directory.value.path;
     final breadCrumbList = List<BreadCrumbItem>.empty(growable: true);
-    final isWindows = model.getCurrentIsWindows(isLocal);
+    final isWindows = controller.options.value.isWindows;
     if (isWindows && path == '/') {
       breadCrumbList.add(BreadCrumbItem(
           content: TextButton(
-                  child: buildWindowsThisPC(),
+                  child: buildWindowsThisPC(context),
                   style: ButtonStyle(
                       minimumSize: MaterialStateProperty.all(Size(0, 0))),
                   onPressed: () => onPressed(['/']))
@@ -1319,39 +1379,34 @@ class _FileManagerPageState extends State<FileManagerPage>
     return breadCrumbList;
   }
 
-  breadCrumbScrollToEnd(bool isLocal) {
+  breadCrumbScrollToEnd() {
     Future.delayed(Duration(milliseconds: 200), () {
-      final breadCrumbScroller = getBreadCrumbScrollController(isLocal);
-      if (breadCrumbScroller.hasClients) {
-        breadCrumbScroller.animateTo(
-            breadCrumbScroller.position.maxScrollExtent,
+      if (_breadCrumbScroller.hasClients) {
+        _breadCrumbScroller.animateTo(
+            _breadCrumbScroller.position.maxScrollExtent,
             duration: Duration(milliseconds: 200),
             curve: Curves.fastLinearToSlowEaseIn);
       }
     });
   }
 
-  Widget buildPathLocation(bool isLocal) {
-    final searchTextObs = isLocal ? _searchTextLocal : _searchTextRemote;
-    final locationStatus =
-        isLocal ? _locationStatusLocal : _locationStatusRemote;
-    final focusNode = isLocal ? _locationNodeLocal : _locationNodeRemote;
-    final text = locationStatus.value == LocationStatus.pathLocation
-        ? model.getCurrentDir(isLocal).path
-        : searchTextObs.value;
+  Widget buildPathLocation() {
+    final text = _locationStatus.value == LocationStatus.pathLocation
+        ? controller.directory.value.path
+        : _searchText.value;
     final textController = TextEditingController(text: text)
       ..selection = TextSelection.collapsed(offset: text.length);
     return Row(
       children: [
         SvgPicture.asset(
-          locationStatus.value == LocationStatus.pathLocation
+          _locationStatus.value == LocationStatus.pathLocation
               ? "assets/folder.svg"
               : "assets/search.svg",
           color: Theme.of(context).tabBarTheme.labelColor,
         ),
         Expanded(
           child: TextField(
-            focusNode: focusNode,
+            focusNode: _locationNode,
             decoration: InputDecoration(
               border: InputBorder.none,
               isDense: true,
@@ -1361,9 +1416,9 @@ class _FileManagerPageState extends State<FileManagerPage>
             ),
             controller: textController,
             onSubmitted: (path) {
-              openDirectory(path, isLocal: isLocal);
+              controller.openDirectory(path);
             },
-            onChanged: locationStatus.value == LocationStatus.fileSearchBar
+            onChanged: _locationStatus.value == LocationStatus.fileSearchBar
                 ? (searchText) => onSearchText(searchText, isLocal)
                 : null,
           ),
@@ -1372,139 +1427,16 @@ class _FileManagerPageState extends State<FileManagerPage>
     );
   }
 
-  onSearchText(String searchText, bool isLocal) {
-    if (isLocal) {
-      _localSelectedItems.clear();
-      _searchTextLocal.value = searchText;
-    } else {
-      _remoteSelectedItems.clear();
-      _searchTextRemote.value = searchText;
-    }
-  }
-
-  openDirectory(String path, {bool isLocal = false}) {
-    model.openDirectory(path, isLocal: isLocal);
-  }
-
-  void handleDragDone(DropDoneDetails details, bool isLocal) {
-    if (isLocal) {
-      // ignore local
-      return;
-    }
-    var items = SelectedItems();
-    for (var file in details.files) {
-      final f = File(file.path);
-      items.add(
-          true,
-          Entry()
-            ..path = file.path
-            ..name = file.name
-            ..size =
-                FileSystemEntity.isDirectorySync(f.path) ? 0 : f.lengthSync());
-    }
-    model.sendFiles(items, isRemote: false);
-  }
-
-  void refocusKeyboardListener(bool isLocal) {
-    Future.delayed(Duration.zero, () {
-      if (isLocal) {
-        _keyboardNodeLocal.requestFocus();
-      } else {
-        _keyboardNodeRemote.requestFocus();
-      }
-    });
-  }
-
-  Widget headerItemFunc(
-      double? width, SortBy sortBy, String name, bool isLocal) {
-    final headerTextStyle =
-        Theme.of(context).dataTableTheme.headingTextStyle ?? TextStyle();
-    return ObxValue<Rx<bool?>>(
-        (ascending) => InkWell(
-              onTap: () {
-                if (ascending.value == null) {
-                  ascending.value = true;
-                } else {
-                  ascending.value = !ascending.value!;
-                }
-                model.changeSortStyle(sortBy,
-                    isLocal: isLocal, ascending: ascending.value!);
-              },
-              child: SizedBox(
-                width: width,
-                height: kDesktopFileTransferHeaderHeight,
-                child: Row(
-                  children: [
-                    Flexible(
-                      flex: 2,
-                      child: Text(
-                        name,
-                        style: headerTextStyle,
-                        overflow: TextOverflow.ellipsis,
-                      ).marginSymmetric(horizontal: 4),
-                    ),
-                    Flexible(
-                        flex: 1,
-                        child: ascending.value != null
-                            ? Icon(
-                                ascending.value!
-                                    ? Icons.keyboard_arrow_up_rounded
-                                    : Icons.keyboard_arrow_down_rounded,
-                              )
-                            : const Offstage())
-                  ],
-                ),
-              ),
-            ), () {
-      if (model.getSortStyle(isLocal) == sortBy) {
-        return model.getSortAscending(isLocal).obs;
-      } else {
-        return Rx<bool?>(null);
-      }
-    }());
-  }
-
-  Widget _buildFileBrowserHeader(BuildContext context, bool isLocal) {
-    final nameColWidth = isLocal ? _nameColWidthLocal : _nameColWidthRemote;
-    final modifiedColWidth =
-        isLocal ? _modifiedColWidthLocal : _modifiedColWidthRemote;
-    final padding = EdgeInsets.all(1.0);
-    return SizedBox(
-      height: kDesktopFileTransferHeaderHeight,
-      child: Row(
-        children: [
-          Obx(
-            () => headerItemFunc(
-                nameColWidth.value, SortBy.name, translate("Name"), isLocal),
-          ),
-          DraggableDivider(
-            axis: Axis.vertical,
-            onPointerMove: (dx) {
-              nameColWidth.value += dx;
-              nameColWidth.value = min(kDesktopFileTransferMaximumWidth,
-                  max(kDesktopFileTransferMinimumWidth, nameColWidth.value));
-            },
-            padding: padding,
-          ),
-          Obx(
-            () => headerItemFunc(modifiedColWidth.value, SortBy.modified,
-                translate("Modified"), isLocal),
-          ),
-          DraggableDivider(
-              axis: Axis.vertical,
-              onPointerMove: (dx) {
-                modifiedColWidth.value += dx;
-                modifiedColWidth.value = min(
-                    kDesktopFileTransferMaximumWidth,
-                    max(kDesktopFileTransferMinimumWidth,
-                        modifiedColWidth.value));
-              },
-              padding: padding),
-          Expanded(
-              child:
-                  headerItemFunc(null, SortBy.size, translate("Size"), isLocal))
-        ],
-      ),
-    );
-  }
+  // openDirectory(String path, {bool isLocal = false}) {
+  //   model.openDirectory(path, isLocal: isLocal);
+  // }
+}
+
+Widget buildWindowsThisPC(BuildContext context, [TextStyle? textStyle]) {
+  final color = Theme.of(context).iconTheme.color?.withOpacity(0.7);
+  return Row(children: [
+    Icon(Icons.computer, size: 20, color: color),
+    SizedBox(width: 10),
+    Text(translate('This PC'), style: textStyle)
+  ]);
 }
diff --git a/flutter/lib/mobile/pages/file_manager_page.dart b/flutter/lib/mobile/pages/file_manager_page.dart
index 7aa9a0005..c6ba42d31 100644
--- a/flutter/lib/mobile/pages/file_manager_page.dart
+++ b/flutter/lib/mobile/pages/file_manager_page.dart
@@ -3,7 +3,7 @@ import 'dart:async';
 import 'package:flutter/material.dart';
 import 'package:flutter_breadcrumb/flutter_breadcrumb.dart';
 import 'package:flutter_hbb/models/file_model.dart';
-import 'package:provider/provider.dart';
+import 'package:get/get.dart';
 import 'package:toggle_switch/toggle_switch.dart';
 import 'package:wakelock/wakelock.dart';
 
@@ -18,10 +18,51 @@ class FileManagerPage extends StatefulWidget {
   State<StatefulWidget> createState() => _FileManagerPageState();
 }
 
+enum SelectMode { local, remote, none }
+
+extension SelectModeEq on SelectMode {
+  bool eq(bool? currentIsLocal) {
+    if (currentIsLocal == null) {
+      return false;
+    }
+    if (currentIsLocal) {
+      return this == SelectMode.local;
+    } else {
+      return this == SelectMode.remote;
+    }
+  }
+}
+
+extension SelectModeExt on Rx<SelectMode> {
+  void toggle(bool currentIsLocal) {
+    switch (value) {
+      case SelectMode.local:
+        value = SelectMode.none;
+        break;
+      case SelectMode.remote:
+        value = SelectMode.none;
+        break;
+      case SelectMode.none:
+        if (currentIsLocal) {
+          value = SelectMode.local;
+        } else {
+          value = SelectMode.remote;
+        }
+        break;
+    }
+  }
+}
+
 class _FileManagerPageState extends State<FileManagerPage> {
   final model = gFFI.fileModel;
-  final _selectedItems = SelectedItems();
-  final _breadCrumbScroller = ScrollController();
+  final selectMode = SelectMode.none.obs;
+
+  var showLocal = true;
+
+  FileController get currentFileController =>
+      showLocal ? model.localController : model.remoteController;
+  FileDirectory get currentDir => currentFileController.directory.value;
+  DirectoryOptions get currentOptions => currentFileController.options.value;
 
   @override
   void initState() {
@@ -32,13 +73,12 @@ class _FileManagerPageState extends State<FileManagerPage> {
           .showLoading(translate('Connecting...'), onCancel: closeConnection);
     });
     gFFI.ffiModel.updateEventListener(widget.id);
-    model.onDirChanged = (_) => breadCrumbScrollToEnd();
     Wakelock.enable();
   }
 
   @override
   void dispose() {
-    model.onClose().whenComplete(() {
+    model.close().whenComplete(() {
       gFFI.close();
       gFFI.dialogManager.dismissAll();
       Wakelock.disable();
@@ -47,288 +87,455 @@ class _FileManagerPageState extends State<FileManagerPage> {
   }
 
   @override
-  Widget build(BuildContext context) => ChangeNotifierProvider.value(
-      value: model,
-      child: Consumer<FileModel>(builder: (_context, _model, _child) {
-        return WillPopScope(
-            onWillPop: () async {
-              if (model.selectMode) {
-                model.toggleSelectMode();
-              } else {
-                model.goBack();
+  Widget build(BuildContext context) => WillPopScope(
+      onWillPop: () async {
+        if (selectMode.value != SelectMode.none) {
+          selectMode.value = SelectMode.none;
+          setState(() {});
+        } else {
+          currentFileController.goBack();
+        }
+        return false;
+      },
+      child: Scaffold(
+        // backgroundColor: MyTheme.grayBg,
+        appBar: AppBar(
+          leading: Row(children: [
+            IconButton(
+                icon: Icon(Icons.close),
+                onPressed: () => clientClose(widget.id, gFFI.dialogManager)),
+          ]),
+          centerTitle: true,
+          title: ToggleSwitch(
+            initialLabelIndex: showLocal ? 0 : 1,
+            activeBgColor: [MyTheme.idColor],
+            inactiveBgColor: Theme.of(context).brightness == Brightness.light
+                ? MyTheme.grayBg
+                : null,
+            inactiveFgColor: Theme.of(context).brightness == Brightness.light
+                ? Colors.black54
+                : null,
+            totalSwitches: 2,
+            minWidth: 100,
+            fontSize: 15,
+            iconSize: 18,
+            labels: [translate("Local"), translate("Remote")],
+            icons: [Icons.phone_android_sharp, Icons.screen_share],
+            onToggle: (index) {
+              final current = showLocal ? 0 : 1;
+              if (index != current) {
+                setState(() => showLocal = !showLocal);
               }
-              return false;
             },
-            child: Scaffold(
-              // backgroundColor: MyTheme.grayBg,
-              appBar: AppBar(
-                leading: Row(children: [
-                  IconButton(
-                      icon: Icon(Icons.close),
-                      onPressed: () =>
-                          clientClose(widget.id, gFFI.dialogManager)),
-                ]),
-                centerTitle: true,
-                title: ToggleSwitch(
-                  initialLabelIndex: model.isLocal ? 0 : 1,
-                  activeBgColor: [MyTheme.idColor],
-                  inactiveBgColor:
-                      Theme.of(context).brightness == Brightness.light
-                          ? MyTheme.grayBg
-                          : null,
-                  inactiveFgColor:
-                      Theme.of(context).brightness == Brightness.light
-                          ? Colors.black54
-                          : null,
-                  totalSwitches: 2,
-                  minWidth: 100,
-                  fontSize: 15,
-                  iconSize: 18,
-                  labels: [translate("Local"), translate("Remote")],
-                  icons: [Icons.phone_android_sharp, Icons.screen_share],
-                  onToggle: (index) {
-                    final current = model.isLocal ? 0 : 1;
-                    if (index != current) {
-                      model.togglePage();
-                    }
-                  },
-                ),
-                actions: [
-                  PopupMenuButton<String>(
-                      icon: Icon(Icons.more_vert),
-                      itemBuilder: (context) {
-                        return [
-                          PopupMenuItem(
-                            child: Row(
-                              children: [
-                                Icon(Icons.refresh,
-                                    color: Theme.of(context).iconTheme.color),
-                                SizedBox(width: 5),
-                                Text(translate("Refresh File"))
-                              ],
-                            ),
-                            value: "refresh",
-                          ),
-                          PopupMenuItem(
-                            enabled: model.currentDir.path != "/",
-                            child: Row(
-                              children: [
-                                Icon(Icons.check,
-                                    color: Theme.of(context).iconTheme.color),
-                                SizedBox(width: 5),
-                                Text(translate("Multi Select"))
-                              ],
-                            ),
-                            value: "select",
-                          ),
-                          PopupMenuItem(
-                            enabled: model.currentDir.path != "/",
-                            child: Row(
-                              children: [
-                                Icon(Icons.folder_outlined,
-                                    color: Theme.of(context).iconTheme.color),
-                                SizedBox(width: 5),
-                                Text(translate("Create Folder"))
-                              ],
-                            ),
-                            value: "folder",
-                          ),
-                          PopupMenuItem(
-                            enabled: model.currentDir.path != "/",
-                            child: Row(
-                              children: [
-                                Icon(
-                                    model.getCurrentShowHidden()
-                                        ? Icons.check_box_outlined
-                                        : Icons.check_box_outline_blank,
-                                    color: Theme.of(context).iconTheme.color),
-                                SizedBox(width: 5),
-                                Text(translate("Show Hidden Files"))
-                              ],
-                            ),
-                            value: "hidden",
-                          )
-                        ];
-                      },
-                      onSelected: (v) {
-                        if (v == "refresh") {
-                          model.refresh();
-                        } else if (v == "select") {
-                          _selectedItems.clear();
-                          model.toggleSelectMode();
-                        } else if (v == "folder") {
-                          final name = TextEditingController();
-                          gFFI.dialogManager
-                              .show((setState, close) => CustomAlertDialog(
-                                      title: Text(translate("Create Folder")),
-                                      content: Column(
-                                        mainAxisSize: MainAxisSize.min,
-                                        children: [
-                                          TextFormField(
-                                            decoration: InputDecoration(
-                                              labelText: translate(
-                                                  "Please enter the folder name"),
-                                            ),
-                                            controller: name,
-                                          ),
-                                        ],
+          ),
+          actions: [
+            PopupMenuButton<String>(
+                icon: Icon(Icons.more_vert),
+                itemBuilder: (context) {
+                  return [
+                    PopupMenuItem(
+                      child: Row(
+                        children: [
+                          Icon(Icons.refresh,
+                              color: Theme.of(context).iconTheme.color),
+                          SizedBox(width: 5),
+                          Text(translate("Refresh File"))
+                        ],
+                      ),
+                      value: "refresh",
+                    ),
+                    PopupMenuItem(
+                      enabled: currentDir.path != "/",
+                      child: Row(
+                        children: [
+                          Icon(Icons.check,
+                              color: Theme.of(context).iconTheme.color),
+                          SizedBox(width: 5),
+                          Text(translate("Multi Select"))
+                        ],
+                      ),
+                      value: "select",
+                    ),
+                    PopupMenuItem(
+                      enabled: currentDir.path != "/",
+                      child: Row(
+                        children: [
+                          Icon(Icons.folder_outlined,
+                              color: Theme.of(context).iconTheme.color),
+                          SizedBox(width: 5),
+                          Text(translate("Create Folder"))
+                        ],
+                      ),
+                      value: "folder",
+                    ),
+                    PopupMenuItem(
+                      enabled: currentDir.path != "/",
+                      child: Row(
+                        children: [
+                          Icon(
+                              currentOptions.showHidden
+                                  ? Icons.check_box_outlined
+                                  : Icons.check_box_outline_blank,
+                              color: Theme.of(context).iconTheme.color),
+                          SizedBox(width: 5),
+                          Text(translate("Show Hidden Files"))
+                        ],
+                      ),
+                      value: "hidden",
+                    )
+                  ];
+                },
+                onSelected: (v) {
+                  if (v == "refresh") {
+                    currentFileController.refresh();
+                  } else if (v == "select") {
+                    model.localController.selectedItems.clear();
+                    model.remoteController.selectedItems.clear();
+                    selectMode.toggle(showLocal);
+                    setState(() {});
+                  } else if (v == "folder") {
+                    final name = TextEditingController();
+                    gFFI.dialogManager
+                        .show((setState, close) => CustomAlertDialog(
+                                title: Text(translate("Create Folder")),
+                                content: Column(
+                                  mainAxisSize: MainAxisSize.min,
+                                  children: [
+                                    TextFormField(
+                                      decoration: InputDecoration(
+                                        labelText: translate(
+                                            "Please enter the folder name"),
                                       ),
-                                      actions: [
-                                        dialogButton("Cancel",
-                                            onPressed: () => close(false),
-                                            isOutline: true),
-                                        dialogButton("OK", onPressed: () {
-                                          if (name.value.text.isNotEmpty) {
-                                            model.createDir(PathUtil.join(
-                                                model.currentDir.path,
-                                                name.value.text,
-                                                model.getCurrentIsWindows()));
-                                            close();
-                                          }
-                                        })
-                                      ]));
-                        } else if (v == "hidden") {
-                          model.toggleShowHidden();
-                        }
-                      }),
-                ],
+                                      controller: name,
+                                    ),
+                                  ],
+                                ),
+                                actions: [
+                                  dialogButton("Cancel",
+                                      onPressed: () => close(false),
+                                      isOutline: true),
+                                  dialogButton("OK", onPressed: () {
+                                    if (name.value.text.isNotEmpty) {
+                                      currentFileController.createDir(
+                                          PathUtil.join(
+                                              currentDir.path,
+                                              name.value.text,
+                                              currentOptions.isWindows));
+                                      close();
+                                    }
+                                  })
+                                ]));
+                  } else if (v == "hidden") {
+                    currentFileController.toggleShowHidden();
+                  }
+                }),
+          ],
+        ),
+        body: showLocal
+            ? FileManagerView(
+                controller: model.localController,
+                selectMode: selectMode,
+              )
+            : FileManagerView(
+                controller: model.remoteController,
+                selectMode: selectMode,
               ),
-              body: body(),
-              bottomSheet: bottomSheet(),
-            ));
-      }));
+        bottomSheet: bottomSheet(),
+      ));
 
-  bool showCheckBox() {
-    if (!model.selectMode) {
-      return false;
-    }
-    return !_selectedItems.isOtherPage(model.isLocal);
+  Widget? bottomSheet() {
+    return Obx(() {
+      final selectedItems = getActiveSelectedItems();
+      final jobTable = model.jobController.jobTable;
+
+      final localLabel = selectedItems?.isLocal == null
+          ? ""
+          : " [${selectedItems!.isLocal ? translate("Local") : translate("Remote")}]";
+      if (!(selectMode.value == SelectMode.none)) {
+        final selectedItemsLen =
+            "${selectedItems?.items.length ?? 0} ${translate("items")}";
+        if (selectedItems == null ||
+            selectedItems.items.isEmpty ||
+            selectMode.value.eq(showLocal)) {
+          return BottomSheetBody(
+              leading: Icon(Icons.check),
+              title: translate("Selected"),
+              text: selectedItemsLen + localLabel,
+              onCanceled: () {
+                selectedItems?.items.clear();
+                selectMode.value = SelectMode.none;
+                setState(() {});
+              },
+              actions: [
+                IconButton(
+                  icon: Icon(Icons.compare_arrows),
+                  onPressed: () => setState(() => showLocal = !showLocal),
+                ),
+                IconButton(
+                  icon: Icon(Icons.delete_forever),
+                  onPressed: selectedItems != null
+                      ? () async {
+                          if (selectedItems.items.isNotEmpty) {
+                            await currentFileController
+                                .removeAction(selectedItems);
+                            selectedItems.items.clear();
+                            selectMode.value = SelectMode.none;
+                          }
+                        }
+                      : null,
+                )
+              ]);
+        } else {
+          return BottomSheetBody(
+              leading: Icon(Icons.input),
+              title: translate("Paste here?"),
+              text: selectedItemsLen + localLabel,
+              onCanceled: () {
+                selectedItems.items.clear();
+                selectMode.value = SelectMode.none;
+                setState(() {});
+              },
+              actions: [
+                IconButton(
+                  icon: Icon(Icons.compare_arrows),
+                  onPressed: () => setState(() => showLocal = !showLocal),
+                ),
+                IconButton(
+                  icon: Icon(Icons.paste),
+                  onPressed: () {
+                    selectMode.value = SelectMode.none;
+                    final otherSide = showLocal
+                        ? model.remoteController
+                        : model.localController;
+                    final thisSideData =
+                        DirectoryData(currentDir, currentOptions);
+                    otherSide.sendFiles(selectedItems, thisSideData);
+                    selectedItems.items.clear();
+                    selectMode.value = SelectMode.none;
+                  },
+                )
+              ]);
+        }
+      }
+
+      if (jobTable.isEmpty) {
+        return Offstage();
+      }
+
+      switch (jobTable.last.state) {
+        case JobState.inProgress:
+          return BottomSheetBody(
+            leading: CircularProgressIndicator(),
+            title: translate("Waiting"),
+            text:
+                "${translate("Speed")}:  ${readableFileSize(jobTable.last.speed)}/s",
+            onCanceled: () {
+              model.jobController.cancelJob(jobTable.last.id);
+              jobTable.clear();
+            },
+          );
+        case JobState.done:
+          return BottomSheetBody(
+            leading: Icon(Icons.check),
+            title: "${translate("Successful")}!",
+            text: jobTable.last.display(),
+            onCanceled: () => jobTable.clear(),
+          );
+        case JobState.error:
+          return BottomSheetBody(
+            leading: Icon(Icons.error),
+            title: "${translate("Error")}!",
+            text: "",
+            onCanceled: () => jobTable.clear(),
+          );
+        case JobState.none:
+          break;
+        case JobState.paused:
+          // TODO: Handle this case.
+          break;
+      }
+      return Offstage();
+    });
   }
 
-  Widget body() {
-    final isLocal = model.isLocal;
-    final fd = model.currentDir;
-    final entries = fd.entries;
+  SelectedItems? getActiveSelectedItems() {
+    final localSelectedItems = model.localController.selectedItems;
+    final remoteSelectedItems = model.remoteController.selectedItems;
+
+    if (localSelectedItems.items.isNotEmpty &&
+        remoteSelectedItems.items.isNotEmpty) {
+      // assert unreachable
+      debugPrint("Wrong SelectedItems state, reset");
+      localSelectedItems.clear();
+      remoteSelectedItems.clear();
+    }
+
+    if (localSelectedItems.items.isEmpty && remoteSelectedItems.items.isEmpty) {
+      return null;
+    }
+
+    if (localSelectedItems.items.length > remoteSelectedItems.items.length) {
+      return localSelectedItems;
+    } else {
+      return remoteSelectedItems;
+    }
+  }
+}
+
+class FileManagerView extends StatefulWidget {
+  final FileController controller;
+  final Rx<SelectMode> selectMode;
+
+  FileManagerView({required this.controller, required this.selectMode});
+
+  @override
+  State<StatefulWidget> createState() => _FileManagerViewState();
+}
+
+class _FileManagerViewState extends State<FileManagerView> {
+  final _listScrollController = ScrollController();
+  final _breadCrumbScroller = ScrollController();
+
+  bool get isLocal => widget.controller.isLocal;
+  FileController get controller => widget.controller;
+  SelectedItems get _selectedItems => widget.controller.selectedItems;
+
+  @override
+  void initState() {
+    super.initState();
+    controller.directory.listen((e) => breadCrumbScrollToEnd());
+  }
+
+  @override
+  Widget build(BuildContext context) {
     return Column(children: [
       headTools(),
-      Expanded(
-          child: ListView.builder(
-        controller: ScrollController(),
-        itemCount: entries.length + 1,
-        itemBuilder: (context, index) {
-          if (index >= entries.length) {
-            return listTail();
-          }
-          var selected = false;
-          if (model.selectMode) {
-            selected = _selectedItems.contains(entries[index]);
-          }
+      Expanded(child: Obx(() {
+        final entries = controller.directory.value.entries;
+        return ListView.builder(
+          controller: _listScrollController,
+          itemCount: entries.length + 1,
+          itemBuilder: (context, index) {
+            if (index >= entries.length) {
+              return listTail();
+            }
+            var selected = false;
+            if (widget.selectMode.value != SelectMode.none) {
+              selected = _selectedItems.items.contains(entries[index]);
+            }
 
-          final sizeStr = entries[index].isFile
-              ? readableFileSize(entries[index].size.toDouble())
-              : "";
-          return Card(
-            child: ListTile(
-              leading: entries[index].isDrive
-                  ? Padding(
-                      padding: EdgeInsets.symmetric(vertical: 8),
-                      child: Image(
-                          image: iconHardDrive,
-                          fit: BoxFit.scaleDown,
-                          color: Theme.of(context)
-                              .iconTheme
-                              .color
-                              ?.withOpacity(0.7)))
-                  : Icon(
-                      entries[index].isFile
-                          ? Icons.feed_outlined
-                          : Icons.folder,
-                      size: 40),
-              title: Text(entries[index].name),
-              selected: selected,
-              subtitle: entries[index].isDrive
-                  ? null
-                  : Text(
-                      "${entries[index].lastModified().toString().replaceAll(".000", "")}   $sizeStr",
-                      style: TextStyle(fontSize: 12, color: MyTheme.darkGray),
-                    ),
-              trailing: entries[index].isDrive
-                  ? null
-                  : showCheckBox()
-                      ? Checkbox(
-                          value: selected,
-                          onChanged: (v) {
-                            if (v == null) return;
-                            if (v && !selected) {
-                              _selectedItems.add(isLocal, entries[index]);
-                            } else if (!v && selected) {
-                              _selectedItems.remove(entries[index]);
-                            }
-                            setState(() {});
-                          })
-                      : PopupMenuButton<String>(
-                          icon: Icon(Icons.more_vert),
-                          itemBuilder: (context) {
-                            return [
-                              PopupMenuItem(
-                                child: Text(translate("Delete")),
-                                value: "delete",
-                              ),
-                              PopupMenuItem(
-                                child: Text(translate("Multi Select")),
-                                value: "multi_select",
-                              ),
-                              PopupMenuItem(
-                                child: Text(translate("Properties")),
-                                value: "properties",
-                                enabled: false,
-                              )
-                            ];
-                          },
-                          onSelected: (v) {
-                            if (v == "delete") {
-                              final items = SelectedItems();
-                              items.add(isLocal, entries[index]);
-                              model.removeAction(items);
-                            } else if (v == "multi_select") {
-                              _selectedItems.clear();
-                              model.toggleSelectMode();
-                            }
-                          }),
-              onTap: () {
-                if (model.selectMode && !_selectedItems.isOtherPage(isLocal)) {
-                  if (selected) {
-                    _selectedItems.remove(entries[index]);
-                  } else {
-                    _selectedItems.add(isLocal, entries[index]);
+            final sizeStr = entries[index].isFile
+                ? readableFileSize(entries[index].size.toDouble())
+                : "";
+
+            final showCheckBox = () {
+              return widget.selectMode.value != SelectMode.none &&
+                  widget.selectMode.value.eq(controller.selectedItems.isLocal);
+            }();
+            return Card(
+              child: ListTile(
+                leading: entries[index].isDrive
+                    ? Padding(
+                        padding: EdgeInsets.symmetric(vertical: 8),
+                        child: Image(
+                            image: iconHardDrive,
+                            fit: BoxFit.scaleDown,
+                            color: Theme.of(context)
+                                .iconTheme
+                                .color
+                                ?.withOpacity(0.7)))
+                    : Icon(
+                        entries[index].isFile
+                            ? Icons.feed_outlined
+                            : Icons.folder,
+                        size: 40),
+                title: Text(entries[index].name),
+                selected: selected,
+                subtitle: entries[index].isDrive
+                    ? null
+                    : Text(
+                        "${entries[index].lastModified().toString().replaceAll(".000", "")}   $sizeStr",
+                        style: TextStyle(fontSize: 12, color: MyTheme.darkGray),
+                      ),
+                trailing: entries[index].isDrive
+                    ? null
+                    : showCheckBox
+                        ? Checkbox(
+                            value: selected,
+                            onChanged: (v) {
+                              if (v == null) return;
+                              if (v && !selected) {
+                                _selectedItems.add(entries[index]);
+                              } else if (!v && selected) {
+                                _selectedItems.remove(entries[index]);
+                              }
+                              setState(() {});
+                            })
+                        : PopupMenuButton<String>(
+                            icon: Icon(Icons.more_vert),
+                            itemBuilder: (context) {
+                              return [
+                                PopupMenuItem(
+                                  child: Text(translate("Delete")),
+                                  value: "delete",
+                                ),
+                                PopupMenuItem(
+                                  child: Text(translate("Multi Select")),
+                                  value: "multi_select",
+                                ),
+                                PopupMenuItem(
+                                  child: Text(translate("Properties")),
+                                  value: "properties",
+                                  enabled: false,
+                                )
+                              ];
+                            },
+                            onSelected: (v) {
+                              if (v == "delete") {
+                                final items = SelectedItems(isLocal: isLocal);
+                                items.add(entries[index]);
+                                controller.removeAction(items);
+                              } else if (v == "multi_select") {
+                                _selectedItems.clear();
+                                widget.selectMode.toggle(isLocal);
+                                setState(() {});
+                              }
+                            }),
+                onTap: () {
+                  if (showCheckBox) {
+                    if (selected) {
+                      _selectedItems.remove(entries[index]);
+                    } else {
+                      _selectedItems.add(entries[index]);
+                    }
+                    setState(() {});
+                    return;
                   }
-                  setState(() {});
-                  return;
-                }
-                if (entries[index].isDirectory || entries[index].isDrive) {
-                  model.openDirectory(entries[index].path);
-                } else {
-                  // Perform file-related tasks.
-                }
-              },
-              onLongPress: entries[index].isDrive
-                  ? null
-                  : () {
-                      _selectedItems.clear();
-                      model.toggleSelectMode();
-                      if (model.selectMode) {
-                        _selectedItems.add(isLocal, entries[index]);
-                      }
-                      setState(() {});
-                    },
-            ),
-          );
-        },
-      ))
+                  if (entries[index].isDirectory || entries[index].isDrive) {
+                    controller.openDirectory(entries[index].path);
+                  } else {
+                    // Perform file-related tasks.
+                  }
+                },
+                onLongPress: entries[index].isDrive
+                    ? null
+                    : () {
+                        _selectedItems.clear();
+                        widget.selectMode.toggle(isLocal);
+                        if (widget.selectMode.value != SelectMode.none) {
+                          _selectedItems.add(entries[index]);
+                        }
+                        setState(() {});
+                      },
+              ),
+            );
+          },
+        );
+      }))
     ]);
   }
 
-  breadCrumbScrollToEnd() {
+  void breadCrumbScrollToEnd() {
     Future.delayed(Duration(milliseconds: 200), () {
       if (_breadCrumbScroller.hasClients) {
         _breadCrumbScroller.animateTo(
@@ -342,35 +549,39 @@ class _FileManagerPageState extends State<FileManagerPage> {
   Widget headTools() => Container(
           child: Row(
         children: [
-          Expanded(
-              child: BreadCrumb(
-            items: getPathBreadCrumbItems(() => model.goHome(), (list) {
-              var path = "";
-              if (model.currentHome.startsWith(list[0])) {
-                // absolute path
-                for (var item in list) {
-                  path = PathUtil.join(path, item, model.getCurrentIsWindows());
+          Expanded(child: Obx(() {
+            final home = controller.options.value.home;
+            final isWindows = controller.options.value.isWindows;
+            return BreadCrumb(
+              items: getPathBreadCrumbItems(controller.shortPath, isWindows,
+                  () => controller.goToHomeDirectory(), (list) {
+                var path = "";
+                if (home.startsWith(list[0])) {
+                  // absolute path
+                  for (var item in list) {
+                    path = PathUtil.join(path, item, isWindows);
+                  }
+                } else {
+                  path += home;
+                  for (var item in list) {
+                    path = PathUtil.join(path, item, isWindows);
+                  }
                 }
-              } else {
-                path += model.currentHome;
-                for (var item in list) {
-                  path = PathUtil.join(path, item, model.getCurrentIsWindows());
-                }
-              }
-              model.openDirectory(path);
-            }),
-            divider: Icon(Icons.chevron_right),
-            overflow: ScrollableOverflow(controller: _breadCrumbScroller),
-          )),
+                controller.openDirectory(path);
+              }),
+              divider: Icon(Icons.chevron_right),
+              overflow: ScrollableOverflow(controller: _breadCrumbScroller),
+            );
+          })),
           Row(
             children: [
               IconButton(
                 icon: Icon(Icons.arrow_back),
-                onPressed: model.goBack,
+                onPressed: controller.goBack,
               ),
               IconButton(
                 icon: Icon(Icons.arrow_upward),
-                onPressed: model.goToParentDirectory,
+                onPressed: controller.goToParentDirectory,
               ),
               PopupMenuButton<SortBy>(
                   icon: Icon(Icons.sort),
@@ -382,123 +593,37 @@ class _FileManagerPageState extends State<FileManagerPage> {
                             ))
                         .toList();
                   },
-                  onSelected: model.changeSortStyle),
+                  onSelected: controller.changeSortStyle),
             ],
           )
         ],
       ));
 
-  Widget listTail() {
-    return Container(
-      height: 100,
-      child: Column(
-        children: [
-          Padding(
-            padding: EdgeInsets.fromLTRB(30, 5, 30, 0),
-            child: Text(
-              model.currentDir.path,
-              style: TextStyle(color: MyTheme.darkGray),
-            ),
-          ),
-          Padding(
-            padding: EdgeInsets.all(2),
-            child: Text(
-              "${translate("Total")}: ${model.currentDir.entries.length} ${translate("items")}",
-              style: TextStyle(color: MyTheme.darkGray),
-            ),
-          )
-        ],
-      ),
-    );
-  }
-
-  Widget? bottomSheet() {
-    final state = model.jobState;
-    final isOtherPage = _selectedItems.isOtherPage(model.isLocal);
-    final selectedItemsLen = "${_selectedItems.length} ${translate("items")}";
-    final local = _selectedItems.isLocal == null
-        ? ""
-        : " [${_selectedItems.isLocal! ? translate("Local") : translate("Remote")}]";
-
-    if (model.selectMode) {
-      if (_selectedItems.length == 0 || !isOtherPage) {
-        return BottomSheetBody(
-            leading: Icon(Icons.check),
-            title: translate("Selected"),
-            text: selectedItemsLen + local,
-            onCanceled: () => model.toggleSelectMode(),
-            actions: [
-              IconButton(
-                icon: Icon(Icons.compare_arrows),
-                onPressed: model.togglePage,
+  Widget listTail() => Obx(() => Container(
+        height: 100,
+        child: Column(
+          children: [
+            Padding(
+              padding: EdgeInsets.fromLTRB(30, 5, 30, 0),
+              child: Text(
+                controller.directory.value.path,
+                style: TextStyle(color: MyTheme.darkGray),
               ),
-              IconButton(
-                icon: Icon(Icons.delete_forever),
-                onPressed: () {
-                  if (_selectedItems.length > 0) {
-                    model.removeAction(_selectedItems);
-                  }
-                },
-              )
-            ]);
-      } else {
-        return BottomSheetBody(
-            leading: Icon(Icons.input),
-            title: translate("Paste here?"),
-            text: selectedItemsLen + local,
-            onCanceled: () => model.toggleSelectMode(),
-            actions: [
-              IconButton(
-                icon: Icon(Icons.compare_arrows),
-                onPressed: model.togglePage,
+            ),
+            Padding(
+              padding: EdgeInsets.all(2),
+              child: Text(
+                "${translate("Total")}: ${controller.directory.value.entries.length} ${translate("items")}",
+                style: TextStyle(color: MyTheme.darkGray),
               ),
-              IconButton(
-                icon: Icon(Icons.paste),
-                onPressed: () {
-                  model.toggleSelectMode();
-                  model.sendFiles(_selectedItems);
-                },
-              )
-            ]);
-      }
-    }
+            )
+          ],
+        ),
+      ));
 
-    switch (state) {
-      case JobState.inProgress:
-        return BottomSheetBody(
-          leading: CircularProgressIndicator(),
-          title: translate("Waiting"),
-          text:
-              "${translate("Speed")}:  ${readableFileSize(model.jobProgress.speed)}/s",
-          onCanceled: () => model.cancelJob(model.jobProgress.id),
-        );
-      case JobState.done:
-        return BottomSheetBody(
-          leading: Icon(Icons.check),
-          title: "${translate("Successful")}!",
-          text: model.jobProgress.display(),
-          onCanceled: () => model.jobReset(),
-        );
-      case JobState.error:
-        return BottomSheetBody(
-          leading: Icon(Icons.error),
-          title: "${translate("Error")}!",
-          text: "",
-          onCanceled: () => model.jobReset(),
-        );
-      case JobState.none:
-        break;
-      case JobState.paused:
-        // TODO: Handle this case.
-        break;
-    }
-    return null;
-  }
-
-  List<BreadCrumbItem> getPathBreadCrumbItems(
+  List<BreadCrumbItem> getPathBreadCrumbItems(String shortPath, bool isWindows,
       void Function() onHome, void Function(List<String>) onPressed) {
-    final path = model.currentShortPath;
-    final list = PathUtil.split(path, model.getCurrentIsWindows());
+    final list = PathUtil.split(shortPath, isWindows);
     final breadCrumbList = [
       BreadCrumbItem(
           content: IconButton(
diff --git a/flutter/lib/models/file_model.dart b/flutter/lib/models/file_model.dart
index feaecd356..4170a5461 100644
--- a/flutter/lib/models/file_model.dart
+++ b/flutter/lib/models/file_model.dart
@@ -24,298 +24,83 @@ enum SortBy {
   }
 }
 
-class FileModel extends ChangeNotifier {
-  /// mobile, current selected page show on mobile screen
-  var _isSelectedLocal = false;
-
-  /// mobile, select mode state
-  var _selectMode = false;
-
-  final _localOption = DirectoryOption();
-  final _remoteOption = DirectoryOption();
-
-  List<String> localHistory = [];
-  List<String> remoteHistory = [];
-
-  var _jobId = 0;
-
-  final _jobProgress = JobProgress(); // from rust update
-
-  /// JobTable <jobId, JobProgress>
-  final _jobTable = List<JobProgress>.empty(growable: true).obs;
-
-  /// `isLocal` bool
-  Function(bool)? onDirChanged;
-
-  RxList<JobProgress> get jobTable => _jobTable;
-
-  bool get isLocal => _isSelectedLocal;
-
-  bool get selectMode => _selectMode;
-
-  JobProgress get jobProgress => _jobProgress;
-
-  JobState get jobState => _jobProgress.state;
-
-  SortBy _sortStyle = SortBy.name;
-
-  SortBy get sortStyle => _sortStyle;
-
-  SortBy _localSortStyle = SortBy.name;
-
-  bool _localSortAscending = true;
-
-  bool _remoteSortAscending = true;
-
-  SortBy _remoteSortStyle = SortBy.name;
-
-  bool get localSortAscending => _localSortAscending;
-
-  SortBy getSortStyle(bool isLocal) {
-    return isLocal ? _localSortStyle : _remoteSortStyle;
+class JobID {
+  int _count = 0;
+  int next() {
+    _count++;
+    return _count;
   }
+}
 
-  bool getSortAscending(bool isLocal) {
-    return isLocal ? _localSortAscending : _remoteSortAscending;
-  }
-
-  FileDirectory _currentLocalDir = FileDirectory();
-
-  FileDirectory get currentLocalDir => _currentLocalDir;
-
-  FileDirectory _currentRemoteDir = FileDirectory();
-
-  FileDirectory get currentRemoteDir => _currentRemoteDir;
-
-  FileDirectory get currentDir =>
-      _isSelectedLocal ? currentLocalDir : currentRemoteDir;
-
-  FileDirectory getCurrentDir(bool isLocal) {
-    return isLocal ? currentLocalDir : currentRemoteDir;
-  }
-
-  String getCurrentShortPath(bool isLocal) {
-    final currentDir = getCurrentDir(isLocal);
-    final currentHome = getCurrentHome(isLocal);
-    if (currentDir.path.startsWith(currentHome)) {
-      var path = currentDir.path.replaceFirst(currentHome, "");
-      if (path.isEmpty) return "";
-      if (path[0] == "/" || path[0] == "\\") {
-        // remove more '/' or '\'
-        path = path.replaceFirst(path[0], "");
-      }
-      return path;
-    } else {
-      return currentDir.path.replaceFirst(currentHome, "");
-    }
-  }
-
-  String get currentHome =>
-      _isSelectedLocal ? _localOption.home : _remoteOption.home;
-
-  String getCurrentHome(bool isLocal) {
-    return isLocal ? _localOption.home : _remoteOption.home;
-  }
-
-  int getJob(int id) {
-    return jobTable.indexWhere((element) => element.id == id);
-  }
-
-  String get currentShortPath {
-    if (currentDir.path.startsWith(currentHome)) {
-      var path = currentDir.path.replaceFirst(currentHome, "");
-      if (path.isEmpty) return "";
-      if (path[0] == "/" || path[0] == "\\") {
-        // remove more '/' or '\'
-        path = path.replaceFirst(path[0], "");
-      }
-      return path;
-    } else {
-      return currentDir.path.replaceFirst(currentHome, "");
-    }
-  }
-
-  String shortPath(bool isLocal) {
-    final dir = isLocal ? currentLocalDir : currentRemoteDir;
-    if (dir.path.startsWith(currentHome)) {
-      var path = dir.path.replaceFirst(currentHome, "");
-      if (path.isEmpty) return "";
-      if (path[0] == "/" || path[0] == "\\") {
-        // remove more '/' or '\'
-        path = path.replaceFirst(path[0], "");
-      }
-      return path;
-    } else {
-      return dir.path.replaceFirst(currentHome, "");
-    }
-  }
-
-  bool getCurrentShowHidden([bool? isLocal]) {
-    final isLocal_ = isLocal ?? _isSelectedLocal;
-    return isLocal_ ? _localOption.showHidden : _remoteOption.showHidden;
-  }
-
-  bool getCurrentIsWindows([bool? isLocal]) {
-    final isLocal_ = isLocal ?? _isSelectedLocal;
-    return isLocal_ ? _localOption.isWindows : _remoteOption.isWindows;
-  }
-
-  final _fileFetcher = FileFetcher();
-
-  final _jobResultListener = JobResultListener<Map<String, dynamic>>();
+typedef GetSessionID = String Function();
 
+class FileModel {
   final WeakReference<FFI> parent;
+  // late final String sessionID;
+  late final FileFetcher fileFetcher;
+  late final JobController jobController;
 
-  FileModel(this.parent);
+  late final FileController localController;
+  late final FileController remoteController;
 
-  toggleSelectMode() {
-    if (jobState == JobState.inProgress) {
-      return;
-    }
-    _selectMode = !_selectMode;
-    notifyListeners();
+  late final GetSessionID getSessionID;
+  String get sessionID => getSessionID();
+
+  FileModel(this.parent) {
+    getSessionID = () => parent.target?.id ?? "";
+    fileFetcher = FileFetcher(getSessionID);
+    jobController = JobController(getSessionID);
+    localController = FileController(
+        isLocal: true,
+        getSessionID: getSessionID,
+        dialogManager: parent.target?.dialogManager,
+        jobController: jobController,
+        fileFetcher: fileFetcher,
+        getOtherSideDirectoryData: () => remoteController.directoryData());
+    remoteController = FileController(
+        isLocal: false,
+        getSessionID: getSessionID,
+        dialogManager: parent.target?.dialogManager,
+        jobController: jobController,
+        fileFetcher: fileFetcher,
+        getOtherSideDirectoryData: () => localController.directoryData());
   }
 
-  togglePage() {
-    _isSelectedLocal = !_isSelectedLocal;
-    notifyListeners();
+  Future<void> onReady() async {
+    await localController.onReady();
+    await remoteController.onReady();
   }
 
-  toggleShowHidden({bool? showHidden, bool? local}) {
-    final isLocal = local ?? _isSelectedLocal;
-    if (isLocal) {
-      _localOption.showHidden = showHidden ?? !_localOption.showHidden;
-    } else {
-      _remoteOption.showHidden = showHidden ?? !_remoteOption.showHidden;
-    }
-    refresh(isLocal: local);
+  Future<void> close() async {
+    parent.target?.dialogManager.dismissAll();
+    await localController.close();
+    await remoteController.close();
   }
 
-  tryUpdateJobProgress(Map<String, dynamic> evt) {
-    try {
-      int id = int.parse(evt['id']);
-      if (!isDesktop) {
-        _jobProgress.id = id;
-        _jobProgress.fileNum = int.parse(evt['file_num']);
-        _jobProgress.speed = double.parse(evt['speed']);
-        _jobProgress.finishedSize = int.parse(evt['finished_size']);
-      } else {
-        // Desktop uses jobTable
-        // id = index + 1
-        final jobIndex = getJob(id);
-        if (jobIndex >= 0 && _jobTable.length > jobIndex) {
-          final job = _jobTable[jobIndex];
-          job.fileNum = int.parse(evt['file_num']);
-          job.speed = double.parse(evt['speed']);
-          job.finishedSize = int.parse(evt['finished_size']);
-          debugPrint("update job $id with $evt");
-        }
-      }
-      notifyListeners();
-    } catch (e) {
-      debugPrint("Failed to tryUpdateJobProgress,evt:${evt.toString()}");
-    }
+  Future<void> refreshAll() async {
+    await localController.refresh();
+    await remoteController.refresh();
   }
 
-  receiveFileDir(Map<String, dynamic> evt) {
+  void receiveFileDir(Map<String, dynamic> evt) {
     if (evt['is_local'] == "false") {
-      // init remote home, the connection will automatic read remote home when established,
-      try {
-        final fd = FileDirectory.fromJson(jsonDecode(evt['value']));
-        fd.format(_remoteOption.isWindows, sort: _sortStyle);
-        if (fd.id > 0) {
-          final jobIndex = getJob(fd.id);
-          if (jobIndex != -1) {
-            final job = jobTable[jobIndex];
-            var totalSize = 0;
-            var fileCount = fd.entries.length;
-            for (var element in fd.entries) {
-              totalSize += element.size;
-            }
-            job.totalSize = totalSize;
-            job.fileCount = fileCount;
-            debugPrint("update receive details:${fd.path}");
-          }
-        } else if (_remoteOption.home.isEmpty) {
-          _remoteOption.home = fd.path;
-          debugPrint("init remote home:${fd.path}");
-          _currentRemoteDir = fd;
-        }
-      } catch (e) {
-        debugPrint("receiveFileDir err=$e");
-      }
+      // init remote home, the remote connection will send one dir event when established. TODO opt
+      remoteController.initDirAndHome(evt);
     }
-    _fileFetcher.tryCompleteTask(evt['value'], evt['is_local']);
-    notifyListeners();
+    fileFetcher.tryCompleteTask(evt['value'], evt['is_local']);
   }
 
-  jobDone(Map<String, dynamic> evt) async {
-    if (_jobResultListener.isListening) {
-      _jobResultListener.complete(evt);
-      return;
-    }
-    if (!isDesktop) {
-      _selectMode = false;
-      _jobProgress.state = JobState.done;
-    } else {
-      int id = int.parse(evt['id']);
-      final jobIndex = getJob(id);
-      if (jobIndex != -1) {
-        final job = jobTable[jobIndex];
-        job.finishedSize = job.totalSize;
-        job.state = JobState.done;
-        job.fileNum = int.parse(evt['file_num']);
-      }
-    }
-    await Future.wait([
-      refresh(isLocal: false),
-      refresh(isLocal: true),
-    ]);
-  }
-
-  jobError(Map<String, dynamic> evt) {
-    final err = evt['err'].toString();
-    if (!isDesktop) {
-      if (_jobResultListener.isListening) {
-        _jobResultListener.complete(evt);
-        return;
-      }
-      _selectMode = false;
-      _jobProgress.clear();
-      _jobProgress.err = err;
-      _jobProgress.state = JobState.error;
-      _jobProgress.fileNum = int.parse(evt['file_num']);
-      if (err == "skipped") {
-        _jobProgress.state = JobState.done;
-        _jobProgress.finishedSize = _jobProgress.totalSize;
-      }
-    } else {
-      int jobIndex = getJob(int.parse(evt['id']));
-      if (jobIndex != -1) {
-        final job = jobTable[jobIndex];
-        job.state = JobState.error;
-        job.err = err;
-        job.fileNum = int.parse(evt['file_num']);
-        if (err == "skipped") {
-          job.state = JobState.done;
-          job.finishedSize = job.totalSize;
-        }
-      }
-    }
-    debugPrint("jobError $evt");
-    notifyListeners();
-  }
-
-  overrideFileConfirm(Map<String, dynamic> evt) async {
+  void overrideFileConfirm(Map<String, dynamic> evt) async {
     final resp = await showFileConfirmDialog(
         translate("Overwrite"), "${evt['read_path']}", true);
     final id = int.tryParse(evt['id']) ?? 0;
     if (false == resp) {
-      final jobIndex = getJob(id);
+      final jobIndex = jobController.getJob(id);
       if (jobIndex != -1) {
-        cancelJob(id);
-        final job = jobTable[jobIndex];
+        jobController.cancelJob(id);
+        final job = jobController.jobTable[jobIndex];
         job.state = JobState.done;
+        jobController.jobTable.refresh();
       }
     } else {
       var need_override = false;
@@ -327,7 +112,7 @@ class FileModel extends ChangeNotifier {
         need_override = true;
       }
       bind.sessionSetConfirmOverrideFile(
-          id: parent.target?.id ?? "",
+          id: sessionID,
           actId: id,
           fileNum: int.parse(evt['file_num']),
           needOverride: need_override,
@@ -336,367 +121,6 @@ class FileModel extends ChangeNotifier {
     }
   }
 
-  jobReset() {
-    _jobProgress.clear();
-    notifyListeners();
-  }
-
-  onReady() async {
-    _localOption.home = await bind.mainGetHomeDir();
-    _localOption.showHidden = (await bind.sessionGetPeerOption(
-            id: parent.target?.id ?? "", name: "local_show_hidden"))
-        .isNotEmpty;
-    _localOption.isWindows = Platform.isWindows;
-
-    _remoteOption.showHidden = (await bind.sessionGetPeerOption(
-            id: parent.target?.id ?? "", name: "remote_show_hidden"))
-        .isNotEmpty;
-    _remoteOption.isWindows =
-        parent.target?.ffiModel.pi.platform == kPeerPlatformWindows;
-
-    await Future.delayed(Duration(milliseconds: 100));
-
-    final local = (await bind.sessionGetPeerOption(
-        id: parent.target?.id ?? "", name: "local_dir"));
-    final remote = (await bind.sessionGetPeerOption(
-        id: parent.target?.id ?? "", name: "remote_dir"));
-    openDirectory(local.isEmpty ? _localOption.home : local, isLocal: true);
-    openDirectory(remote.isEmpty ? _remoteOption.home : remote, isLocal: false);
-    await Future.delayed(Duration(seconds: 1));
-    if (_currentLocalDir.path.isEmpty) {
-      openDirectory(_localOption.home, isLocal: true);
-    }
-    if (_currentRemoteDir.path.isEmpty) {
-      openDirectory(_remoteOption.home, isLocal: false);
-    }
-  }
-
-  Future<void> onClose() async {
-    parent.target?.dialogManager.dismissAll();
-    jobReset();
-
-    onDirChanged = null;
-
-    // save config
-    Map<String, String> msgMap = {};
-
-    msgMap["local_dir"] = _currentLocalDir.path;
-    msgMap["local_show_hidden"] = _localOption.showHidden ? "Y" : "";
-    msgMap["remote_dir"] = _currentRemoteDir.path;
-    msgMap["remote_show_hidden"] = _remoteOption.showHidden ? "Y" : "";
-    final id = parent.target?.id ?? "";
-    for (final msg in msgMap.entries) {
-      await bind.sessionPeerOption(id: id, name: msg.key, value: msg.value);
-    }
-    _currentLocalDir.clear();
-    _currentRemoteDir.clear();
-    _localOption.clear();
-    _remoteOption.clear();
-  }
-
-  Future refresh({bool? isLocal}) async {
-    if (isDesktop) {
-      isLocal = isLocal ?? _isSelectedLocal;
-      isLocal
-          ? await openDirectory(currentLocalDir.path, isLocal: isLocal)
-          : await openDirectory(currentRemoteDir.path, isLocal: isLocal);
-    } else {
-      await openDirectory(currentDir.path);
-    }
-  }
-
-  openDirectory(String path, {bool? isLocal, bool isBack = false}) async {
-    isLocal = isLocal ?? _isSelectedLocal;
-    if (path == ".") {
-      refresh(isLocal: isLocal);
-      return;
-    }
-    if (path == "..") {
-      goToParentDirectory(isLocal: isLocal);
-      return;
-    }
-    if (!isBack) {
-      pushHistory(isLocal);
-    }
-    final showHidden = getCurrentShowHidden(isLocal);
-    final isWindows = getCurrentIsWindows(isLocal);
-    // process /C:\ -> C:\ on Windows
-    if (isWindows && path.length > 1 && path[0] == '/') {
-      path = path.substring(1);
-      if (path[path.length - 1] != '\\') {
-        path = "$path\\";
-      }
-    }
-    try {
-      final fd = await _fileFetcher.fetchDirectory(path, isLocal, showHidden);
-      fd.format(isWindows, sort: _sortStyle);
-      if (isLocal) {
-        _currentLocalDir = fd;
-      } else {
-        _currentRemoteDir = fd;
-      }
-      notifyListeners();
-      onDirChanged?.call(isLocal);
-    } catch (e) {
-      debugPrint("Failed to openDirectory $path: $e");
-    }
-  }
-
-  Future<FileDirectory> fetchDirectory(path, isLocal, showHidden) async {
-    return await _fileFetcher.fetchDirectory(path, isLocal, showHidden);
-  }
-
-  void pushHistory(bool isLocal) {
-    final history = isLocal ? localHistory : remoteHistory;
-    final currPath = isLocal ? currentLocalDir.path : currentRemoteDir.path;
-    if (history.isNotEmpty && history.last == currPath) {
-      return;
-    }
-    history.add(currPath);
-  }
-
-  goHome({bool? isLocal}) {
-    isLocal = isLocal ?? _isSelectedLocal;
-    openDirectory(getCurrentHome(isLocal), isLocal: isLocal);
-  }
-
-  goBack({bool? isLocal}) {
-    isLocal = isLocal ?? _isSelectedLocal;
-    final history = isLocal ? localHistory : remoteHistory;
-    if (history.isEmpty) return;
-    final path = history.removeAt(history.length - 1);
-    if (path.isEmpty) return;
-    final currPath = isLocal ? currentLocalDir.path : currentRemoteDir.path;
-    if (currPath == path) {
-      goBack(isLocal: isLocal);
-      return;
-    }
-    openDirectory(path, isLocal: isLocal, isBack: true);
-  }
-
-  goToParentDirectory({bool? isLocal}) {
-    isLocal = isLocal ?? _isSelectedLocal;
-    final isWindows =
-        isLocal ? _localOption.isWindows : _remoteOption.isWindows;
-    final currDir = isLocal ? currentLocalDir : currentRemoteDir;
-    var parent = PathUtil.dirname(currDir.path, isWindows);
-    // specially for C:\, D:\, goto '/'
-    if (parent == currDir.path && isWindows) {
-      openDirectory('/', isLocal: isLocal);
-      return;
-    }
-    openDirectory(parent, isLocal: isLocal);
-  }
-
-  /// isRemote only for desktop now, [isRemote == true] means [remote -> local]
-  sendFiles(SelectedItems items, {bool isRemote = false}) {
-    if (isDesktop) {
-      // desktop sendFiles
-      final toPath = isRemote ? currentLocalDir.path : currentRemoteDir.path;
-      final isWindows =
-          isRemote ? _localOption.isWindows : _remoteOption.isWindows;
-      final showHidden =
-          isRemote ? _localOption.showHidden : _remoteOption.showHidden;
-      for (var from in items.items) {
-        final jobId = ++_jobId;
-        _jobTable.add(JobProgress()
-          ..fileName = path.basename(from.path)
-          ..jobName = from.path
-          ..totalSize = from.size
-          ..state = JobState.inProgress
-          ..id = jobId
-          ..isRemote = isRemote);
-        bind.sessionSendFiles(
-            id: '${parent.target?.id}',
-            actId: _jobId,
-            path: from.path,
-            to: PathUtil.join(toPath, from.name, isWindows),
-            fileNum: 0,
-            includeHidden: showHidden,
-            isRemote: isRemote);
-        debugPrint(
-            "path:${from.path}, toPath:$toPath, to:${PathUtil.join(toPath, from.name, isWindows)}");
-      }
-    } else {
-      if (items.isLocal == null) {
-        debugPrint("Failed to sendFiles ,wrong path state");
-        return;
-      }
-      _jobProgress.state = JobState.inProgress;
-      final toPath =
-          items.isLocal! ? currentRemoteDir.path : currentLocalDir.path;
-      final isWindows =
-          items.isLocal! ? _localOption.isWindows : _remoteOption.isWindows;
-      final showHidden =
-          items.isLocal! ? _localOption.showHidden : _remoteOption.showHidden;
-      items.items.forEach((from) async {
-        _jobId++;
-        await bind.sessionSendFiles(
-            id: '${parent.target?.id}',
-            actId: _jobId,
-            path: from.path,
-            to: PathUtil.join(toPath, from.name, isWindows),
-            fileNum: 0,
-            includeHidden: showHidden,
-            isRemote: !(items.isLocal!));
-      });
-    }
-  }
-
-  bool removeCheckboxRemember = false;
-
-  removeAction(SelectedItems items, {bool? isLocal}) async {
-    isLocal = isLocal ?? _isSelectedLocal;
-    removeCheckboxRemember = false;
-    if (items.isLocal == null) {
-      debugPrint("Failed to removeFile, wrong path state");
-      return;
-    }
-    final isWindows =
-        items.isLocal! ? _localOption.isWindows : _remoteOption.isWindows;
-    await Future.forEach(items.items, (Entry item) async {
-      _jobId++;
-      var title = "";
-      var content = "";
-      late final List<Entry> entries;
-      if (item.isFile) {
-        title = translate("Are you sure you want to delete this file?");
-        content = item.name;
-        entries = [item];
-      } else if (item.isDirectory) {
-        title = translate("Not an empty directory");
-        parent.target?.dialogManager.showLoading(translate("Waiting"));
-        final fd = await _fileFetcher.fetchDirectoryRecursive(
-            _jobId, item.path, items.isLocal!, true);
-        if (fd.path.isEmpty) {
-          fd.path = item.path;
-        }
-        fd.format(isWindows);
-        parent.target?.dialogManager.dismissAll();
-        if (fd.entries.isEmpty) {
-          final confirm = await showRemoveDialog(
-              translate(
-                  "Are you sure you want to delete this empty directory?"),
-              item.name,
-              false);
-          if (confirm == true) {
-            sendRemoveEmptyDir(item.path, 0, items.isLocal!);
-          }
-          return;
-        }
-        entries = fd.entries;
-      } else {
-        entries = [];
-      }
-
-      for (var i = 0; i < entries.length; i++) {
-        final dirShow = item.isDirectory
-            ? "${translate("Are you sure you want to delete the file of this directory?")}\n"
-            : "";
-        final count = entries.length > 1 ? "${i + 1}/${entries.length}" : "";
-        content = "$dirShow\n\n${entries[i].path}".trim();
-        final confirm = await showRemoveDialog(
-          count.isEmpty ? title : "$title ($count)",
-          content,
-          item.isDirectory,
-        );
-        try {
-          if (confirm == true) {
-            sendRemoveFile(entries[i].path, i, items.isLocal!);
-            final res = await _jobResultListener.start();
-            // handle remove res;
-            if (item.isDirectory &&
-                res['file_num'] == (entries.length - 1).toString()) {
-              sendRemoveEmptyDir(item.path, i, items.isLocal!);
-            }
-          }
-          if (removeCheckboxRemember) {
-            if (confirm == true) {
-              for (var j = i + 1; j < entries.length; j++) {
-                sendRemoveFile(entries[j].path, j, items.isLocal!);
-                final res = await _jobResultListener.start();
-                if (item.isDirectory &&
-                    res['file_num'] == (entries.length - 1).toString()) {
-                  sendRemoveEmptyDir(item.path, i, items.isLocal!);
-                }
-              }
-            }
-            break;
-          }
-        } catch (e) {
-          print("remove error: $e");
-        }
-      }
-    });
-    _selectMode = false;
-    refresh(isLocal: isLocal);
-  }
-
-  Future<bool?> showRemoveDialog(
-      String title, String content, bool showCheckbox) async {
-    return await parent.target?.dialogManager.show<bool>(
-        (setState, Function(bool v) close) {
-      cancel() => close(false);
-      submit() => close(true);
-      return CustomAlertDialog(
-        title: Row(
-          mainAxisAlignment: MainAxisAlignment.center,
-          children: [
-            const Icon(Icons.warning_rounded, color: Colors.red),
-            Text(title).paddingOnly(
-              left: 10,
-            ),
-          ],
-        ),
-        contentBoxConstraints:
-            BoxConstraints(minHeight: 100, minWidth: 400, maxWidth: 400),
-        content: Column(
-          crossAxisAlignment: CrossAxisAlignment.start,
-          children: [
-            Text(content),
-            Text(
-              translate("This is irreversible!"),
-              style: const TextStyle(
-                fontWeight: FontWeight.bold,
-                color: Colors.red,
-              ),
-            ).paddingOnly(top: 20),
-            showCheckbox
-                ? CheckboxListTile(
-                    contentPadding: const EdgeInsets.all(0),
-                    dense: true,
-                    controlAffinity: ListTileControlAffinity.leading,
-                    title: Text(
-                      translate("Do this for all conflicts"),
-                    ),
-                    value: removeCheckboxRemember,
-                    onChanged: (v) {
-                      if (v == null) return;
-                      setState(() => removeCheckboxRemember = v);
-                    },
-                  )
-                : const SizedBox.shrink()
-          ],
-        ),
-        actions: [
-          dialogButton(
-            "Cancel",
-            icon: Icon(Icons.close_rounded),
-            onPressed: cancel,
-            isOutline: true,
-          ),
-          dialogButton(
-            "OK",
-            icon: Icon(Icons.done_rounded),
-            onPressed: submit,
-          ),
-        ],
-        onSubmit: submit,
-        onCancel: cancel,
-      );
-    }, useAnimation: false);
-  }
-
   bool fileConfirmCheckboxRemember = false;
 
   Future<bool?> showFileConfirmDialog(
@@ -765,65 +189,542 @@ class FileModel extends ChangeNotifier {
       );
     }, useAnimation: false);
   }
+}
 
-  sendRemoveFile(String path, int fileNum, bool isLocal) {
+class DirectoryData {
+  final DirectoryOptions options;
+  final FileDirectory directory;
+  DirectoryData(this.directory, this.options);
+}
+
+class FileController {
+  final bool isLocal;
+  final GetSessionID getSessionID;
+  String get sessionID => getSessionID();
+
+  final FileFetcher fileFetcher;
+
+  final options = DirectoryOptions().obs;
+  final directory = FileDirectory().obs;
+
+  final history = RxList<String>.empty(growable: true);
+  final sortBy = SortBy.name.obs;
+  var sortAscending = true;
+  final JobController jobController;
+  final OverlayDialogManager? dialogManager;
+
+  final DirectoryData Function() getOtherSideDirectoryData;
+  late final SelectedItems selectedItems = SelectedItems(isLocal: isLocal);
+
+  FileController(
+      {required this.isLocal,
+      required this.getSessionID,
+      required this.dialogManager,
+      required this.jobController,
+      required this.fileFetcher,
+      required this.getOtherSideDirectoryData});
+
+  String get homePath => options.value.home;
+
+  String get shortPath {
+    final dirPath = directory.value.path;
+    if (dirPath.startsWith(homePath)) {
+      var path = dirPath.replaceFirst(homePath, "");
+      if (path.isEmpty) return "";
+      if (path[0] == "/" || path[0] == "\\") {
+        // remove more '/' or '\'
+        path = path.replaceFirst(path[0], "");
+      }
+      return path;
+    } else {
+      return dirPath.replaceFirst(homePath, "");
+    }
+  }
+
+  DirectoryData directoryData() {
+    return DirectoryData(directory.value, options.value);
+  }
+
+  Future<void> onReady() async {
+    options.value.home = await bind.mainGetHomeDir();
+    options.value.showHidden = (await bind.sessionGetPeerOption(
+            id: sessionID,
+            name: isLocal ? "local_show_hidden" : "remote_show_hidden"))
+        .isNotEmpty;
+    options.value.isWindows = Platform.isWindows;
+
+    await Future.delayed(Duration(milliseconds: 100));
+
+    final dir = (await bind.sessionGetPeerOption(
+        id: sessionID, name: isLocal ? "local_dir" : "remote_dir"));
+    openDirectory(dir.isEmpty ? options.value.home : dir);
+
+    await Future.delayed(Duration(seconds: 1));
+
+    if (directory.value.path.isEmpty) {
+      openDirectory(options.value.home);
+    }
+  }
+
+  Future<void> close() async {
+    // save config
+    Map<String, String> msgMap = {};
+    msgMap[isLocal ? "local_dir" : "remote_dir"] = directory.value.path;
+    msgMap[isLocal ? "local_show_hidden" : "remote_show_hidden"] =
+        options.value.showHidden ? "Y" : "";
+    for (final msg in msgMap.entries) {
+      await bind.sessionPeerOption(
+          id: sessionID, name: msg.key, value: msg.value);
+    }
+    directory.value.clear();
+    options.value.clear();
+  }
+
+  void toggleShowHidden({bool? showHidden}) {
+    options.value.showHidden = showHidden ?? !options.value.showHidden;
+    refresh();
+  }
+
+  void changeSortStyle(SortBy sort, {bool? isLocal, bool ascending = true}) {
+    sortBy.value = sort;
+    sortAscending = ascending;
+    directory.update((dir) {
+      dir?.changeSortStyle(sort, ascending: ascending);
+    });
+  }
+
+  Future<void> refresh() async {
+    await openDirectory(directory.value.path);
+  }
+
+  Future<void> openDirectory(String path, {bool isBack = false}) async {
+    if (path == ".") {
+      refresh();
+      return;
+    }
+    if (path == "..") {
+      goToParentDirectory();
+      return;
+    }
+    if (!isBack) {
+      pushHistory();
+    }
+    final showHidden = options.value.showHidden;
+    final isWindows = options.value.isWindows;
+    // process /C:\ -> C:\ on Windows
+    if (isWindows && path.length > 1 && path[0] == '/') {
+      path = path.substring(1);
+      if (path[path.length - 1] != '\\') {
+        path = "$path\\";
+      }
+    }
+    try {
+      final fd = await fileFetcher.fetchDirectory(path, isLocal, showHidden);
+      fd.format(isWindows, sort: sortBy.value);
+      directory.value = fd;
+    } catch (e) {
+      debugPrint("Failed to openDirectory $path: $e");
+    }
+  }
+
+  void pushHistory() {
+    if (history.isNotEmpty && history.last == directory.value.path) {
+      return;
+    }
+    history.add(directory.value.path);
+  }
+
+  void goToHomeDirectory() {
+    openDirectory(homePath);
+  }
+
+  void goBack() {
+    if (history.isEmpty) return;
+    final path = history.removeAt(history.length - 1);
+    if (path.isEmpty) return;
+    if (directory.value.path == path) {
+      goBack();
+      return;
+    }
+    openDirectory(path, isBack: true);
+  }
+
+  void goToParentDirectory() {
+    final isWindows = options.value.isWindows;
+    final dirPath = directory.value.path;
+    var parent = PathUtil.dirname(dirPath, isWindows);
+    // specially for C:\, D:\, goto '/'
+    if (parent == dirPath && isWindows) {
+      openDirectory('/');
+      return;
+    }
+    openDirectory(parent);
+  }
+
+  // TODO deprecated this
+  void initDirAndHome(Map<String, dynamic> evt) {
+    try {
+      final fd = FileDirectory.fromJson(jsonDecode(evt['value']));
+      fd.format(options.value.isWindows, sort: sortBy.value);
+      if (fd.id > 0) {
+        final jobIndex = jobController.getJob(fd.id);
+        if (jobIndex != -1) {
+          final job = jobController.jobTable[jobIndex];
+          var totalSize = 0;
+          var fileCount = fd.entries.length;
+          for (var element in fd.entries) {
+            totalSize += element.size;
+          }
+          job.totalSize = totalSize;
+          job.fileCount = fileCount;
+          debugPrint("update receive details:${fd.path}");
+          jobController.jobTable.refresh();
+        }
+      } else if (options.value.home.isEmpty) {
+        options.value.home = fd.path;
+        debugPrint("init remote home:${fd.path}");
+        directory.value = fd;
+      }
+    } catch (e) {
+      debugPrint("initDirAndHome err=$e");
+    }
+  }
+
+  /// sendFiles from current side (FileController.isLocal) to other side (SelectedItems).
+  void sendFiles(SelectedItems items, DirectoryData otherSideData) {
+    /// ignore wrong items side status
+    if (items.isLocal != isLocal) {
+      return;
+    }
+
+    // alias
+    final isRemoteToLocal = !isLocal;
+
+    final toPath = otherSideData.directory.path;
+    final isWindows = otherSideData.options.isWindows;
+    final showHidden = otherSideData.options.showHidden;
+    for (var from in items.items) {
+      final jobID = jobController.add(from, isRemoteToLocal);
+      bind.sessionSendFiles(
+          id: sessionID,
+          actId: jobID,
+          path: from.path,
+          to: PathUtil.join(toPath, from.name, isWindows),
+          fileNum: 0,
+          includeHidden: showHidden,
+          isRemote: isRemoteToLocal);
+      debugPrint(
+          "path:${from.path}, toPath:$toPath, to:${PathUtil.join(toPath, from.name, isWindows)}");
+    }
+  }
+
+  bool _removeCheckboxRemember = false;
+
+  Future<void> removeAction(SelectedItems items) async {
+    _removeCheckboxRemember = false;
+    if (items.isLocal != isLocal) {
+      debugPrint("Failed to removeFile, wrong files");
+      return;
+    }
+    final isWindows = options.value.isWindows;
+    await Future.forEach(items.items, (Entry item) async {
+      final jobID = JobController.jobID.next();
+      var title = "";
+      var content = "";
+      late final List<Entry> entries;
+      if (item.isFile) {
+        title = translate("Are you sure you want to delete this file?");
+        content = item.name;
+        entries = [item];
+      } else if (item.isDirectory) {
+        title = translate("Not an empty directory");
+        dialogManager?.showLoading(translate("Waiting"));
+        final fd = await fileFetcher.fetchDirectoryRecursive(
+            jobID, item.path, items.isLocal!, true);
+        if (fd.path.isEmpty) {
+          fd.path = item.path;
+        }
+        fd.format(isWindows);
+        dialogManager?.dismissAll();
+        if (fd.entries.isEmpty) {
+          final confirm = await showRemoveDialog(
+              translate(
+                  "Are you sure you want to delete this empty directory?"),
+              item.name,
+              false);
+          if (confirm == true) {
+            sendRemoveEmptyDir(item.path, 0);
+          }
+          return;
+        }
+        entries = fd.entries;
+      } else {
+        entries = [];
+      }
+
+      for (var i = 0; i < entries.length; i++) {
+        final dirShow = item.isDirectory
+            ? "${translate("Are you sure you want to delete the file of this directory?")}\n"
+            : "";
+        final count = entries.length > 1 ? "${i + 1}/${entries.length}" : "";
+        content = "$dirShow\n\n${entries[i].path}".trim();
+        final confirm = await showRemoveDialog(
+          count.isEmpty ? title : "$title ($count)",
+          content,
+          item.isDirectory,
+        );
+        try {
+          if (confirm == true) {
+            sendRemoveFile(entries[i].path, i);
+            final res = await jobController.jobResultListener.start();
+            // handle remove res;
+            if (item.isDirectory &&
+                res['file_num'] == (entries.length - 1).toString()) {
+              sendRemoveEmptyDir(item.path, i);
+            }
+          }
+          if (_removeCheckboxRemember) {
+            if (confirm == true) {
+              for (var j = i + 1; j < entries.length; j++) {
+                sendRemoveFile(entries[j].path, j);
+                final res = await jobController.jobResultListener.start();
+                if (item.isDirectory &&
+                    res['file_num'] == (entries.length - 1).toString()) {
+                  sendRemoveEmptyDir(item.path, i);
+                }
+              }
+            }
+            break;
+          }
+        } catch (e) {
+          print("remove error: $e");
+        }
+      }
+    });
+    refresh();
+  }
+
+  Future<bool?> showRemoveDialog(
+      String title, String content, bool showCheckbox) async {
+    return await dialogManager?.show<bool>((setState, Function(bool v) close) {
+      cancel() => close(false);
+      submit() => close(true);
+      return CustomAlertDialog(
+        title: Row(
+          mainAxisAlignment: MainAxisAlignment.center,
+          children: [
+            const Icon(Icons.warning_rounded, color: Colors.red),
+            Text(title).paddingOnly(
+              left: 10,
+            ),
+          ],
+        ),
+        contentBoxConstraints:
+            BoxConstraints(minHeight: 100, minWidth: 400, maxWidth: 400),
+        content: Column(
+          crossAxisAlignment: CrossAxisAlignment.start,
+          children: [
+            Text(content),
+            Text(
+              translate("This is irreversible!"),
+              style: const TextStyle(
+                fontWeight: FontWeight.bold,
+                color: Colors.red,
+              ),
+            ).paddingOnly(top: 20),
+            showCheckbox
+                ? CheckboxListTile(
+                    contentPadding: const EdgeInsets.all(0),
+                    dense: true,
+                    controlAffinity: ListTileControlAffinity.leading,
+                    title: Text(
+                      translate("Do this for all conflicts"),
+                    ),
+                    value: _removeCheckboxRemember,
+                    onChanged: (v) {
+                      if (v == null) return;
+                      setState(() => _removeCheckboxRemember = v);
+                    },
+                  )
+                : const SizedBox.shrink()
+          ],
+        ),
+        actions: [
+          dialogButton(
+            "Cancel",
+            icon: Icon(Icons.close_rounded),
+            onPressed: cancel,
+            isOutline: true,
+          ),
+          dialogButton(
+            "OK",
+            icon: Icon(Icons.done_rounded),
+            onPressed: submit,
+          ),
+        ],
+        onSubmit: submit,
+        onCancel: cancel,
+      );
+    }, useAnimation: false);
+  }
+
+  void sendRemoveFile(String path, int fileNum) {
     bind.sessionRemoveFile(
-        id: '${parent.target?.id}',
-        actId: _jobId,
+        id: sessionID,
+        actId: JobController.jobID.next(),
         path: path,
         isRemote: !isLocal,
         fileNum: fileNum);
   }
 
-  sendRemoveEmptyDir(String path, int fileNum, bool isLocal) {
-    final history = isLocal ? localHistory : remoteHistory;
+  void sendRemoveEmptyDir(String path, int fileNum) {
     history.removeWhere((element) => element.contains(path));
     bind.sessionRemoveAllEmptyDirs(
-        id: '${parent.target?.id}',
-        actId: _jobId,
+        id: sessionID,
+        actId: JobController.jobID.next(),
         path: path,
         isRemote: !isLocal);
   }
 
-  createDir(String path, {bool? isLocal}) async {
-    isLocal = isLocal ?? this.isLocal;
-    _jobId++;
+  Future<void> createDir(String path) async {
     bind.sessionCreateDir(
-        id: '${parent.target?.id}',
-        actId: _jobId,
+        id: sessionID,
+        actId: JobController.jobID.next(),
         path: path,
         isRemote: !isLocal);
   }
+}
 
-  cancelJob(int id) async {
-    bind.sessionCancelJob(id: '${parent.target?.id}', actId: id);
-    jobReset();
+class JobController {
+  static final JobID jobID = JobID();
+  final jobTable = List<JobProgress>.empty(growable: true).obs;
+  final jobResultListener = JobResultListener<Map<String, dynamic>>();
+  final GetSessionID getSessionID;
+  String get sessionID => getSessionID();
+
+  JobController(this.getSessionID);
+
+  int getJob(int id) {
+    return jobTable.indexWhere((element) => element.id == id);
   }
 
-  changeSortStyle(SortBy sort, {bool? isLocal, bool ascending = true}) {
-    _sortStyle = sort;
-    if (isLocal == null) {
-      // compatible for mobile logic
-      _currentLocalDir.changeSortStyle(sort, ascending: ascending);
-      _currentRemoteDir.changeSortStyle(sort, ascending: ascending);
-      _localSortStyle = sort;
-      _localSortAscending = ascending;
-      _remoteSortStyle = sort;
-      _remoteSortAscending = ascending;
-    } else if (isLocal) {
-      _currentLocalDir.changeSortStyle(sort, ascending: ascending);
-      _localSortStyle = sort;
-      _localSortAscending = ascending;
-    } else {
-      _currentRemoteDir.changeSortStyle(sort, ascending: ascending);
-      _remoteSortStyle = sort;
-      _remoteSortAscending = ascending;
+  // JobProgress? getJob(int id) {
+  //   return jobTable.firstWhere((element) => element.id == id);
+  // }
+
+  // return jobID
+  int add(Entry from, bool isRemoteToLocal) {
+    final jobID = JobController.jobID.next();
+    jobTable.add(JobProgress()
+      ..fileName = path.basename(from.path)
+      ..jobName = from.path
+      ..totalSize = from.size
+      ..state = JobState.inProgress
+      ..id = jobID
+      ..isRemoteToLocal = isRemoteToLocal);
+    return jobID;
+  }
+
+  void tryUpdateJobProgress(Map<String, dynamic> evt) {
+    try {
+      int id = int.parse(evt['id']);
+      // id = index + 1
+      final jobIndex = getJob(id);
+      if (jobIndex >= 0 && jobTable.length > jobIndex) {
+        final job = jobTable[jobIndex];
+        job.fileNum = int.parse(evt['file_num']);
+        job.speed = double.parse(evt['speed']);
+        job.finishedSize = int.parse(evt['finished_size']);
+        debugPrint("update job $id with $evt");
+        jobTable.refresh();
+      }
+    } catch (e) {
+      debugPrint("Failed to tryUpdateJobProgress,evt:${evt.toString()}");
     }
-    notifyListeners();
   }
 
-  initFileFetcher() {
-    _fileFetcher.id = parent.target?.id;
+  void jobDone(Map<String, dynamic> evt) async {
+    if (jobResultListener.isListening) {
+      jobResultListener.complete(evt);
+      return;
+    }
+
+    int id = int.parse(evt['id']);
+    final jobIndex = getJob(id);
+    if (jobIndex != -1) {
+      final job = jobTable[jobIndex];
+      job.finishedSize = job.totalSize;
+      job.state = JobState.done;
+      job.fileNum = int.parse(evt['file_num']);
+      jobTable.refresh();
+    }
+  }
+
+  void jobError(Map<String, dynamic> evt) {
+    final err = evt['err'].toString();
+    int jobIndex = getJob(int.parse(evt['id']));
+    if (jobIndex != -1) {
+      final job = jobTable[jobIndex];
+      job.state = JobState.error;
+      job.err = err;
+      job.fileNum = int.parse(evt['file_num']);
+      if (err == "skipped") {
+        job.state = JobState.done;
+        job.finishedSize = job.totalSize;
+      }
+      jobTable.refresh();
+    }
+    debugPrint("jobError $evt");
+  }
+
+  void cancelJob(int id) async {
+    bind.sessionCancelJob(id: sessionID, actId: id);
+  }
+
+  void loadLastJob(Map<String, dynamic> evt) {
+    debugPrint("load last job: $evt");
+    Map<String, dynamic> jobDetail = json.decode(evt['value']);
+    // int id = int.parse(jobDetail['id']);
+    String remote = jobDetail['remote'];
+    String to = jobDetail['to'];
+    bool showHidden = jobDetail['show_hidden'];
+    int fileNum = jobDetail['file_num'];
+    bool isRemote = jobDetail['is_remote'];
+    final currJobId = JobController.jobID.next();
+    String fileName = path.basename(isRemote ? remote : to);
+    var jobProgress = JobProgress()
+      ..fileName = fileName
+      ..jobName = isRemote ? remote : to
+      ..id = currJobId
+      ..isRemoteToLocal = isRemote
+      ..fileNum = fileNum
+      ..remote = remote
+      ..to = to
+      ..showHidden = showHidden
+      ..state = JobState.paused;
+    jobTable.add(jobProgress);
+    bind.sessionAddJob(
+      id: sessionID,
+      isRemote: isRemote,
+      includeHidden: showHidden,
+      actId: currJobId,
+      path: isRemote ? remote : to,
+      to: isRemote ? to : remote,
+      fileNum: fileNum,
+    );
+  }
+
+  void resumeJob(int jobId) {
+    final jobIndex = getJob(jobId);
+    if (jobIndex != -1) {
+      final job = jobTable[jobIndex];
+      bind.sessionResumeJob(
+          id: sessionID, actId: job.id, isRemote: job.isRemoteToLocal);
+      job.state = JobState.inProgress;
+      jobTable.refresh();
+    } else {
+      debugPrint("jobId $jobId is not exists");
+    }
   }
 
   void updateFolderFiles(Map<String, dynamic> evt) {
@@ -837,57 +738,9 @@ class FileModel extends ChangeNotifier {
       final job = jobTable[jobIndex];
       job.fileCount = num_entries;
       job.totalSize = total_size.toInt();
+      jobTable.refresh();
     }
     debugPrint("update folder files: $info");
-    notifyListeners();
-  }
-
-  bool get remoteSortAscending => _remoteSortAscending;
-
-  void loadLastJob(Map<String, dynamic> evt) {
-    debugPrint("load last job: $evt");
-    Map<String, dynamic> jobDetail = json.decode(evt['value']);
-    // int id = int.parse(jobDetail['id']);
-    String remote = jobDetail['remote'];
-    String to = jobDetail['to'];
-    bool showHidden = jobDetail['show_hidden'];
-    int fileNum = jobDetail['file_num'];
-    bool isRemote = jobDetail['is_remote'];
-    final currJobId = _jobId++;
-    String fileName = path.basename(isRemote ? remote : to);
-    var jobProgress = JobProgress()
-      ..fileName = fileName
-      ..jobName = isRemote ? remote : to
-      ..id = currJobId
-      ..isRemote = isRemote
-      ..fileNum = fileNum
-      ..remote = remote
-      ..to = to
-      ..showHidden = showHidden
-      ..state = JobState.paused;
-    jobTable.add(jobProgress);
-    bind.sessionAddJob(
-      id: '${parent.target?.id}',
-      isRemote: isRemote,
-      includeHidden: showHidden,
-      actId: currJobId,
-      path: isRemote ? remote : to,
-      to: isRemote ? to : remote,
-      fileNum: fileNum,
-    );
-  }
-
-  resumeJob(int jobId) {
-    final jobIndex = getJob(jobId);
-    if (jobIndex != -1) {
-      final job = jobTable[jobIndex];
-      bind.sessionResumeJob(
-          id: '${parent.target?.id}', actId: job.id, isRemote: job.isRemote);
-      job.state = JobState.inProgress;
-    } else {
-      debugPrint("jobId $jobId is not exists");
-    }
-    notifyListeners();
   }
 }
 
@@ -936,10 +789,10 @@ class FileFetcher {
   Map<String, Completer<FileDirectory>> remoteTasks = {};
   Map<int, Completer<FileDirectory>> readRecursiveTasks = {};
 
-  String? id;
+  final GetSessionID getSessionID;
+  String get sessionID => getSessionID();
 
-  // if id == null, means to fetch global FFI
-  FFI get _ffi => ffi(id ?? "");
+  FileFetcher(this.getSessionID);
 
   Future<FileDirectory> registerReadTask(bool isLocal, String path) {
     // final jobs = isLocal?localJobs:remoteJobs; // maybe we will use read local dir async later
@@ -958,16 +811,16 @@ class FileFetcher {
     return c.future;
   }
 
-  Future<FileDirectory> registerReadRecursiveTask(int id) {
+  Future<FileDirectory> registerReadRecursiveTask(int actID) {
     final tasks = readRecursiveTasks;
-    if (tasks.containsKey(id)) {
+    if (tasks.containsKey(actID)) {
       throw "Failed to registerRemoveTask, already have same ReadRecursive job";
     }
     final c = Completer<FileDirectory>();
-    tasks[id] = c;
+    tasks[actID] = c;
 
     Timer(Duration(seconds: 2), () {
-      tasks.remove(id);
+      tasks.remove(actID);
       if (c.isCompleted) return;
       c.completeError("Failed to read dir,timeout");
     });
@@ -1002,12 +855,12 @@ class FileFetcher {
     try {
       if (isLocal) {
         final res = await bind.sessionReadLocalDirSync(
-            id: id ?? "", path: path, showHidden: showHidden);
+            id: sessionID ?? "", path: path, showHidden: showHidden);
         final fd = FileDirectory.fromJson(jsonDecode(res));
         return fd;
       } else {
         await bind.sessionReadRemoteDir(
-            id: id ?? "", path: path, includeHidden: showHidden);
+            id: sessionID ?? "", path: path, includeHidden: showHidden);
         return registerReadTask(isLocal, path);
       }
     } catch (e) {
@@ -1016,16 +869,16 @@ class FileFetcher {
   }
 
   Future<FileDirectory> fetchDirectoryRecursive(
-      int id, String path, bool isLocal, bool showHidden) async {
+      int actID, String path, bool isLocal, bool showHidden) async {
     // TODO test Recursive is show hidden default?
     try {
       await bind.sessionReadDirRecursive(
-          id: _ffi.id,
-          actId: id,
+          id: sessionID,
+          actId: actID,
           path: path,
           isRemote: !isLocal,
           showHidden: showHidden);
-      return registerReadRecursiveTask(id);
+      return registerReadRecursiveTask(actID);
     } catch (e) {
       return Future.error(e);
     }
@@ -1122,7 +975,10 @@ class JobProgress {
   var finishedSize = 0;
   var totalSize = 0;
   var fileCount = 0;
-  var isRemote = false;
+  // [isRemote == true] means [remote -> local]
+  // var isRemote = false;
+  // to-do use enum
+  var isRemoteToLocal = false;
   var jobName = "";
   var fileName = "";
   var remote = "";
@@ -1179,12 +1035,12 @@ class PathUtil {
   }
 }
 
-class DirectoryOption {
+class DirectoryOptions {
   String home;
   bool showHidden;
   bool isWindows;
 
-  DirectoryOption(
+  DirectoryOptions(
       {this.home = "", this.showHidden = false, this.isWindows = false});
 
   clear() {
@@ -1195,53 +1051,37 @@ class DirectoryOption {
 }
 
 class SelectedItems {
-  bool? _isLocal;
-  final List<Entry> _items = [];
+  final bool isLocal;
+  final items = RxList<Entry>.empty(growable: true);
 
-  List<Entry> get items => _items;
+  SelectedItems({required this.isLocal});
 
-  int get length => _items.length;
-
-  bool? get isLocal => _isLocal;
-
-  add(bool isLocal, Entry e) {
+  void add(Entry e) {
     if (e.isDrive) return;
-    _isLocal ??= isLocal;
-    if (_isLocal != null && _isLocal != isLocal) {
-      return;
-    }
-    if (!_items.contains(e)) {
-      _items.add(e);
+    if (!items.contains(e)) {
+      items.add(e);
     }
   }
 
-  bool contains(Entry e) {
-    return _items.contains(e);
+  void remove(Entry e) {
+    items.remove(e);
   }
 
-  remove(Entry e) {
-    _items.remove(e);
-    if (_items.isEmpty) {
-      _isLocal = null;
-    }
-  }
-
-  bool isOtherPage(bool currentIsLocal) {
-    if (_isLocal == null) {
-      return false;
-    } else {
-      return _isLocal != currentIsLocal;
-    }
-  }
-
-  clear() {
-    _items.clear();
-    _isLocal = null;
+  void clear() {
+    items.clear();
   }
 
   void selectAll(List<Entry> entries) {
-    _items.clear();
-    _items.addAll(entries);
+    items.clear();
+    items.addAll(entries);
+  }
+
+  static bool valid(RxList<Entry> items) {
+    if (items.isNotEmpty) {
+      // exclude DirDrive type
+      return items.any((item) => !item.isDrive);
+    }
+    return false;
   }
 }
 
diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart
index 802a18a52..94e28ea21 100644
--- a/flutter/lib/models/model.dart
+++ b/flutter/lib/models/model.dart
@@ -166,17 +166,18 @@ class FfiModel with ChangeNotifier {
       } else if (name == 'file_dir') {
         parent.target?.fileModel.receiveFileDir(evt);
       } else if (name == 'job_progress') {
-        parent.target?.fileModel.tryUpdateJobProgress(evt);
+        parent.target?.fileModel.jobController.tryUpdateJobProgress(evt);
       } else if (name == 'job_done') {
-        parent.target?.fileModel.jobDone(evt);
+        parent.target?.fileModel.jobController.jobDone(evt);
+        parent.target?.fileModel.refreshAll();
       } else if (name == 'job_error') {
-        parent.target?.fileModel.jobError(evt);
+        parent.target?.fileModel.jobController.jobError(evt);
       } else if (name == 'override_file_confirm') {
         parent.target?.fileModel.overrideFileConfirm(evt);
       } else if (name == 'load_last_job') {
-        parent.target?.fileModel.loadLastJob(evt);
+        parent.target?.fileModel.jobController.loadLastJob(evt);
       } else if (name == 'update_folder_files') {
-        parent.target?.fileModel.updateFolderFiles(evt);
+        parent.target?.fileModel.jobController.updateFolderFiles(evt);
       } else if (name == 'add_connection') {
         parent.target?.serverModel.addConnection(evt);
       } else if (name == 'on_client_remove') {
@@ -371,7 +372,11 @@ class FfiModel with ChangeNotifier {
 
   _updateSessionWidthHeight(String id) {
     parent.target?.canvasModel.updateViewStyle();
-    bind.sessionSetSize(id: id, width: display.width, height: display.height);
+    if (display.width <= 0 || display.height <= 0) {
+      debugPrintStack(label: 'invalid display size (${display.width},${display.height})');
+    } else {
+      bind.sessionSetSize(id: id, width: display.width, height: display.height);
+    }
   }
 
   /// Handle the peer info event based on [evt].
@@ -1571,9 +1576,6 @@ class FFI {
     }();
     // every instance will bind a stream
     this.id = id;
-    if (isFileTransfer) {
-      fileModel.initFileFetcher();
-    }
   }
 
   /// Login with [password], choose if the client should [remember] it.
diff --git a/flutter/lib/models/state_model.dart b/flutter/lib/models/state_model.dart
index aa4fab86e..187b1ffc5 100644
--- a/flutter/lib/models/state_model.dart
+++ b/flutter/lib/models/state_model.dart
@@ -28,10 +28,9 @@ class StateGlobal {
 
   setWindowId(int id) => _windowId = id;
   setMaximize(bool v) {
-    if (_maximize != v) {
+    if (_maximize != v && !_fullscreen) {
       _maximize = v;
-      _resizeEdgeSize.value =
-          _maximize ? kMaximizeEdgeSize : kWindowEdgeSize;
+      _resizeEdgeSize.value = _maximize ? kMaximizeEdgeSize : kWindowEdgeSize;
     }
   }
   setFullscreen(bool v) {
@@ -39,7 +38,13 @@ class StateGlobal {
       _fullscreen = v;
       _showTabBar.value = !_fullscreen;
       _resizeEdgeSize.value =
-          fullscreen ? kFullScreenEdgeSize : kWindowEdgeSize;
+          fullscreen
+          ? kFullScreenEdgeSize
+          : _maximize
+              ? kMaximizeEdgeSize
+              : kWindowEdgeSize;
+      print(
+          "fullscreen: ${fullscreen}, resizeEdgeSize: ${_resizeEdgeSize.value}");
       _windowBorderWidth.value = fullscreen ? 0 : kWindowBorderWidth;
       WindowController.fromWindowId(windowId)
           .setFullscreen(_fullscreen)
diff --git a/flutter/pubspec.yaml b/flutter/pubspec.yaml
index a212a5093..46278bf0d 100644
--- a/flutter/pubspec.yaml
+++ b/flutter/pubspec.yaml
@@ -59,7 +59,7 @@ dependencies:
   desktop_multi_window:
     git:
       url: https://github.com/Kingtous/rustdesk_desktop_multi_window
-      ref: 3e2655677c54f421f9e378680d8171b95a211e0f
+      ref: e3947d4b4f8edaa655de63cd47f2a59a6e024218
   freezed_annotation: ^2.0.3
   flutter_custom_cursor: ^0.0.4
   window_size:
diff --git a/libs/hbb_common/Cargo.toml b/libs/hbb_common/Cargo.toml
index c77f11eb5..fd02639ed 100644
--- a/libs/hbb_common/Cargo.toml
+++ b/libs/hbb_common/Cargo.toml
@@ -7,17 +7,17 @@ edition = "2018"
 # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
 
 [dependencies]
-flexi_logger = { version = "0.22", features = ["async", "use_chrono_for_offset"] }
+flexi_logger = { version = "0.25", features = ["async"] }
 protobuf = { version = "3.1", features = ["with-bytes"] }
 tokio = { version = "1.20", features = ["full"] }
 tokio-util = { version = "0.7", features = ["full"] }
 futures = "0.3"
 bytes = { version = "1.2", features = ["serde"] }
 log = "0.4"
-env_logger = "0.9"
+env_logger = "0.10"
 socket2 = { version = "0.3", features = ["reuseport"] }
 zstd = "0.9"
-quinn = {version = "0.8", optional = true }
+quinn = {version = "0.9", optional = true }
 anyhow = "1.0"
 futures-util = "0.3"
 directories-next = "2.0"
@@ -34,7 +34,7 @@ tokio-socks = { git = "https://github.com/open-trade/tokio-socks" }
 chrono = "0.4"
 backtrace = "0.3"
 libc = "0.2"
-sysinfo = "0.24"
+sysinfo = "0.26"
 
 [target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies]
 mac_address = "1.1"
@@ -54,5 +54,5 @@ winapi = { version = "0.3", features = ["winuser"] }
 osascript = "0.3.0"
 
 [dev-dependencies]
-toml = "0.5"
-serde_json = "1.0"
+toml = "0.7"
+serde_json = "1.0"
\ No newline at end of file
diff --git a/libs/hbb_common/src/config.rs b/libs/hbb_common/src/config.rs
index ed7270a85..4ce2cf07e 100644
--- a/libs/hbb_common/src/config.rs
+++ b/libs/hbb_common/src/config.rs
@@ -4,7 +4,7 @@ use std::{
     net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr},
     path::{Path, PathBuf},
     sync::{Arc, Mutex, RwLock},
-    time::SystemTime,
+    time::{Duration, Instant, SystemTime},
 };
 
 use anyhow::Result;
@@ -51,6 +51,7 @@ lazy_static::lazy_static! {
     pub static ref APP_NAME: Arc<RwLock<String>> = Arc::new(RwLock::new("RustDesk".to_owned()));
     static ref KEY_PAIR: Arc<Mutex<Option<KeyPair>>> = Default::default();
     static ref HW_CODEC_CONFIG: Arc<RwLock<HwCodecConfig>> = Arc::new(RwLock::new(HwCodecConfig::load()));
+    static ref USER_DEFAULT_CONFIG: Arc<RwLock<(UserDefaultConfig, Instant)>> = Arc::new(RwLock::new((UserDefaultConfig::load(), Instant::now())));
 }
 
 lazy_static::lazy_static! {
@@ -123,7 +124,7 @@ macro_rules! serde_field_bool {
         }
         impl $struct_name {
             pub fn $func() -> bool {
-                UserDefaultConfig::load().get($field_name) == "Y"
+                UserDefaultConfig::read().get($field_name) == "Y"
             }
         }
     };
@@ -980,21 +981,21 @@ impl PeerConfig {
     serde_field_string!(
         default_view_style,
         deserialize_view_style,
-        UserDefaultConfig::load().get("view_style")
+        UserDefaultConfig::read().get("view_style")
     );
     serde_field_string!(
         default_scroll_style,
         deserialize_scroll_style,
-        UserDefaultConfig::load().get("scroll_style")
+        UserDefaultConfig::read().get("scroll_style")
     );
     serde_field_string!(
         default_image_quality,
         deserialize_image_quality,
-        UserDefaultConfig::load().get("image_quality")
+        UserDefaultConfig::read().get("image_quality")
     );
 
     fn default_custom_image_quality() -> Vec<i32> {
-        let f: f64 = UserDefaultConfig::load()
+        let f: f64 = UserDefaultConfig::read()
             .get("custom_image_quality")
             .parse()
             .unwrap_or(50.0);
@@ -1020,15 +1021,15 @@ impl PeerConfig {
         let mut mp: HashMap<String, String> = de::Deserialize::deserialize(deserializer)?;
         let mut key = "codec-preference";
         if !mp.contains_key(key) {
-            mp.insert(key.to_owned(), UserDefaultConfig::load().get(key));
+            mp.insert(key.to_owned(), UserDefaultConfig::read().get(key));
         }
         key = "custom-fps";
         if !mp.contains_key(key) {
-            mp.insert(key.to_owned(), UserDefaultConfig::load().get(key));
+            mp.insert(key.to_owned(), UserDefaultConfig::read().get(key));
         }
         key = "zoom-cursor";
         if !mp.contains_key(key) {
-            mp.insert(key.to_owned(), UserDefaultConfig::load().get(key));
+            mp.insert(key.to_owned(), UserDefaultConfig::read().get(key));
         }
         Ok(mp)
     }
@@ -1046,7 +1047,12 @@ serde_field_bool!(
     default_show_quality_monitor,
     "ShowQualityMonitor::default_show_quality_monitor"
 );
-serde_field_bool!(DisableAudio, "disable_audio", default_disable_audio, "DisableAudio::default_disable_audio");
+serde_field_bool!(
+    DisableAudio,
+    "disable_audio",
+    default_disable_audio,
+    "DisableAudio::default_disable_audio"
+);
 serde_field_bool!(
     EnableFileTransfer,
     "enable_file_transfer",
@@ -1065,9 +1071,19 @@ serde_field_bool!(
     default_lock_after_session_end,
     "LockAfterSessionEnd::default_lock_after_session_end"
 );
-serde_field_bool!(PrivacyMode, "privacy_mode", default_privacy_mode, "PrivacyMode::default_privacy_mode");
+serde_field_bool!(
+    PrivacyMode,
+    "privacy_mode",
+    default_privacy_mode,
+    "PrivacyMode::default_privacy_mode"
+);
 
-serde_field_bool!(AllowSwapKey, "allow_swap_key", default_allow_swap_key, "AllowSwapKey::default_allow_swap_key");
+serde_field_bool!(
+    AllowSwapKey,
+    "allow_swap_key",
+    default_allow_swap_key,
+    "AllowSwapKey::default_allow_swap_key"
+);
 
 #[derive(Debug, Default, Serialize, Deserialize, Clone)]
 pub struct LocalConfig {
@@ -1282,6 +1298,14 @@ pub struct UserDefaultConfig {
 }
 
 impl UserDefaultConfig {
+    pub fn read() -> UserDefaultConfig {
+        let mut cfg = USER_DEFAULT_CONFIG.write().unwrap();
+        if cfg.1.elapsed() > Duration::from_secs(1) {
+            *cfg = (Self::load(), Instant::now());
+        }
+        cfg.0.clone()
+    }
+
     pub fn load() -> UserDefaultConfig {
         Config::load_::<UserDefaultConfig>("_default")
     }
diff --git a/libs/scrap/Cargo.toml b/libs/scrap/Cargo.toml
index 82cb88faf..99e15aeef 100644
--- a/libs/scrap/Cargo.toml
+++ b/libs/scrap/Cargo.toml
@@ -42,7 +42,7 @@ quest = "0.3"
 
 [build-dependencies]
 target_build_utils = "0.3"
-bindgen = "0.59"
+bindgen = "0.64"
 
 [target.'cfg(target_os = "linux")'.dependencies]
 dbus = { version = "0.9", optional = true }
diff --git a/libs/scrap/src/common/quartz.rs b/libs/scrap/src/common/quartz.rs
index a02d55ebb..dc59edac1 100644
--- a/libs/scrap/src/common/quartz.rs
+++ b/libs/scrap/src/common/quartz.rs
@@ -83,7 +83,7 @@ impl crate::TraitCapturer for Capturer {
     }
 }
 
-pub struct Frame<'a>(quartz::Frame, PhantomData<&'a [u8]>);
+pub struct Frame<'a>(pub quartz::Frame, PhantomData<&'a [u8]>);
 
 impl<'a> ops::Deref for Frame<'a> {
     type Target = [u8];
diff --git a/src/client.rs b/src/client.rs
index ab27c0185..705b387a8 100644
--- a/src/client.rs
+++ b/src/client.rs
@@ -708,6 +708,7 @@ pub struct AudioHandler {
     audio_stream: Option<Box<dyn StreamTrait>>,
     channels: u16,
     latency_controller: Arc<Mutex<LatencyController>>,
+    ignore_count: i32,
 }
 
 impl AudioHandler {
@@ -810,7 +811,11 @@ impl AudioHandler {
                 .check_audio(frame.timestamp)
                 .not()
             {
-                log::debug!("audio frame {} is ignored", frame.timestamp);
+                self.ignore_count += 1;
+                if self.ignore_count == 100 {
+                    self.ignore_count = 0;
+                    log::debug!("100 audio frames are ignored");
+                }
                 return;
             }
         }
@@ -2233,7 +2238,7 @@ fn get_pk(pk: &[u8]) -> Option<[u8; 32]> {
 
 #[inline]
 fn get_rs_pk(str_base64: &str) -> Option<sign::PublicKey> {
-    if let Ok(pk) = base64::decode(str_base64) {
+    if let Ok(pk) = crate::decode64(str_base64) {
         get_pk(&pk).map(|x| sign::PublicKey(x))
     } else {
         None
diff --git a/src/common.rs b/src/common.rs
index 28d8dddc4..0b475805d 100644
--- a/src/common.rs
+++ b/src/common.rs
@@ -787,3 +787,15 @@ pub fn handle_url_scheme(url: String) {
         let _ = crate::run_me(vec![url]);
     }
 }
+
+#[inline]
+pub fn encode64<T: AsRef<[u8]>>(input: T) -> String {
+    #[allow(deprecated)]
+    base64::encode(input)
+}
+
+#[inline]
+pub fn decode64<T: AsRef<[u8]>>(input: T) -> Result<Vec<u8>, base64::DecodeError> {
+    #[allow(deprecated)]
+    base64::decode(input)
+}
diff --git a/src/lang/el.rs b/src/lang/el.rs
index 052ce1cfd..6599e7a59 100644
--- a/src/lang/el.rs
+++ b/src/lang/el.rs
@@ -461,9 +461,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
         ("Resolution", "Ανάλυση"),
         ("No transfers in progress", "Δεν υπάρχει μεταφορά σε εξέλιξη"),
         ("Set one-time password length", "Μέγεθος κωδικού μιας χρήσης"),
-        ("idd_driver_tip", ""),
-        ("confirm_idd_driver_tip", ""),
-        ("RDP Settings", ""),
+        ("idd_driver_tip", "Εγκαταστήστε το πρόγραμμα οδήγησης εικονικής οθόνης που χρησιμοποιείται όταν δεν έχετε φυσικές οθόνες."),
+        ("confirm_idd_driver_tip", "Είναι ενεργοποιημένη η επιλογή εγκατάστασης του προγράμματος οδήγησης εικονικής οθόνης. Λάβετε υπόψη ότι θα εγκατασταθεί ένα δοκιμαστικό πιστοποιητικό για το πρόγραμμα οδήγησης εικονικής οθόνης. Αυτό το πιστοποιητικό θα χρησιμοποιηθεί μόνο για την πιστοποίηση των προγραμμάτων οδήγησης του Rustdesk."),
+        ("RDP Settings", "Ρυθμίσεις RDP"),
         ("Sort by", ""),
     ].iter().cloned().collect();
 }
diff --git a/src/lang/es.rs b/src/lang/es.rs
index b644add49..85239cdd1 100644
--- a/src/lang/es.rs
+++ b/src/lang/es.rs
@@ -461,9 +461,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
         ("Resolution", "Resolución"),
         ("No transfers in progress", "No hay transferencias en curso"),
         ("Set one-time password length", "Establecer contraseña de un solo uso"),
-        ("idd_driver_tip", ""),
-        ("confirm_idd_driver_tip", ""),
-        ("RDP Settings", ""),
+        ("idd_driver_tip", "Instalar controlador virtual de pantalla a usar cuando no hay pantalla física."),
+        ("confirm_idd_driver_tip", "La opción de instalar el controlador de pantalla virtual está marcada. Hay que tener en cuenta que se instalará un certificado de prueba para confirar en el controlador de pantalla. Este certificado solo se usará para confiar en controladores Rustdesk."),
+        ("RDP Settings", "Ajustes RDP"),
         ("Sort by", ""),
     ].iter().cloned().collect();
 }
diff --git a/src/lang/ru.rs b/src/lang/ru.rs
index ca4ac9e63..dc714ec72 100644
--- a/src/lang/ru.rs
+++ b/src/lang/ru.rs
@@ -461,9 +461,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
         ("Resolution", "Разрешение"),
         ("No transfers in progress", "Передача не осуществляется"),
         ("Set one-time password length", "Установить длину одноразового пароля"),
-        ("idd_driver_tip", ""),
-        ("confirm_idd_driver_tip", ""),
-        ("RDP Settings", ""),
+        ("idd_driver_tip", "Установите драйвер виртуального дисплея, который используется при отсутствии физических дисплеев."),
+        ("confirm_idd_driver_tip", "Включена функция установки драйвера виртуального дисплея. Обратите внимание, что для доверия к драйверу будет установлен тестовый сертификат. Этот сертификат будет использоваться только для подтверждения доверия драйверам Rustdesk."),
+        ("RDP Settings", "Настройки RDP"),
         ("Sort by", ""),
     ].iter().cloned().collect();
 }
diff --git a/src/license.rs b/src/license.rs
index f8f5d27d5..8875d2b64 100644
--- a/src/license.rs
+++ b/src/license.rs
@@ -1,3 +1,4 @@
+use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine as _};
 use hbb_common::{bail, sodiumoxide::crypto::sign, ResultType};
 use serde_derive::{Deserialize, Serialize};
 
@@ -18,7 +19,7 @@ fn get_license_from_string_(s: &str) -> ResultType<License> {
         12, 46, 129, 83, 17, 84, 193, 119, 197, 130, 103,
     ];
     let pk = sign::PublicKey(*PK);
-    let data = base64::decode_config(tmp, base64::URL_SAFE_NO_PAD)?;
+    let data = URL_SAFE_NO_PAD.decode(tmp)?;
     if let Ok(lic) = serde_json::from_slice::<License>(&data) {
         return Ok(lic);
     }
diff --git a/src/naming.rs b/src/naming.rs
index 38b514f86..53e675d92 100644
--- a/src/naming.rs
+++ b/src/naming.rs
@@ -1,10 +1,11 @@
 mod license;
+use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine as _};
 use hbb_common::ResultType;
 use license::*;
 
 fn gen_name(lic: &License) -> ResultType<String> {
     let tmp = serde_json::to_vec::<License>(lic)?;
-    let tmp = base64::encode_config(tmp, base64::URL_SAFE_NO_PAD);
+    let tmp = URL_SAFE_NO_PAD.encode(&tmp);
     let tmp: String = tmp.chars().rev().collect();
     Ok(tmp)
 }
diff --git a/src/platform/macos.mm b/src/platform/macos.mm
index 3c90981c4..a252a9a8f 100644
--- a/src/platform/macos.mm
+++ b/src/platform/macos.mm
@@ -8,6 +8,9 @@
 // https://github.com/codebytere/node-mac-permissions/blob/main/permissions.mm
 
 extern "C" bool InputMonitoringAuthStatus(bool prompt) {
+    #ifdef NO_InputMonitoringAuthStatus
+    return true;
+    #else
     if (floor(NSAppKitVersionNumber) >= NSAppKitVersionNumber10_15) {
         IOHIDAccessType theType = IOHIDCheckAccess(kIOHIDRequestTypeListenEvent);
         NSLog(@"IOHIDCheckAccess = %d, kIOHIDAccessTypeGranted = %d", theType, kIOHIDAccessTypeGranted);
@@ -36,6 +39,7 @@ extern "C" bool InputMonitoringAuthStatus(bool prompt) {
         return true;
     }
     return false;
+    #endif
 }
 
 extern "C" bool MacCheckAdminAuthorization() {
diff --git a/src/server/connection.rs b/src/server/connection.rs
index 898939b62..3d419ef08 100644
--- a/src/server/connection.rs
+++ b/src/server/connection.rs
@@ -735,7 +735,7 @@ impl Connection {
         let url = self.server_audit_conn.clone();
         let mut v = v;
         v["id"] = json!(Config::get_id());
-        v["uuid"] = json!(base64::encode(hbb_common::get_uuid()));
+        v["uuid"] = json!(crate::encode64(hbb_common::get_uuid()));
         v["conn_id"] = json!(self.inner.id);
         tokio::spawn(async move {
             allow_err!(Self::post_audit_async(url, v).await);
@@ -765,7 +765,7 @@ impl Connection {
         info["files"] = json!(files);
         let v = json!({
             "id":json!(Config::get_id()),
-            "uuid":json!(base64::encode(hbb_common::get_uuid())),
+            "uuid":json!(crate::encode64(hbb_common::get_uuid())),
             "peer_id":json!(self.lr.my_id),
             "type": r#type as i8,
             "path":path,
@@ -788,7 +788,7 @@ impl Connection {
         }
         let mut v = Value::default();
         v["id"] = json!(Config::get_id());
-        v["uuid"] = json!(base64::encode(hbb_common::get_uuid()));
+        v["uuid"] = json!(crate::encode64(hbb_common::get_uuid()));
         v["typ"] = json!(typ as i8);
         v["from_remote"] = json!(from_remote);
         v["info"] = serde_json::Value::String(info.to_string());
diff --git a/src/ui_cm_interface.rs b/src/ui_cm_interface.rs
index f5c575d43..bd6eab3bf 100644
--- a/src/ui_cm_interface.rs
+++ b/src/ui_cm_interface.rs
@@ -848,7 +848,7 @@ pub fn elevate_portable(_id: i32) {
 #[cfg(any(target_os = "android", target_os = "ios", feature = "flutter"))]
 #[inline]
 pub fn handle_incoming_voice_call(id: i32, accept: bool) {
-    if let Some(client) = CLIENTS.write().unwrap().get_mut(&id) {
+    if let Some(client) = CLIENTS.read().unwrap().get(&id) {
         allow_err!(client.tx.send(Data::VoiceCallResponse(accept)));
     };
 }
@@ -856,7 +856,7 @@ pub fn handle_incoming_voice_call(id: i32, accept: bool) {
 #[cfg(any(target_os = "android", target_os = "ios", feature = "flutter"))]
 #[inline]
 pub fn close_voice_call(id: i32) {
-    if let Some(client) = CLIENTS.write().unwrap().get_mut(&id) {
+    if let Some(client) = CLIENTS.read().unwrap().get(&id) {
         allow_err!(client.tx.send(Data::CloseVoiceCall("".to_owned())));
     };
 }
diff --git a/src/ui_interface.rs b/src/ui_interface.rs
index ec570b42c..fd97f41b0 100644
--- a/src/ui_interface.rs
+++ b/src/ui_interface.rs
@@ -605,7 +605,7 @@ pub fn remove_discovered(id: String) {
 
 #[inline]
 pub fn get_uuid() -> String {
-    base64::encode(hbb_common::get_uuid())
+    crate::encode64(hbb_common::get_uuid())
 }
 
 #[cfg(any(target_os = "android", target_os = "ios", feature = "flutter"))]
@@ -876,7 +876,7 @@ pub async fn change_id_shared(id: String, old_id: String) -> &'static str {
     #[cfg(not(any(target_os = "android", target_os = "ios")))]
     let uuid = machine_uid::get().unwrap_or("".to_owned());
     #[cfg(any(target_os = "android", target_os = "ios"))]
-    let uuid = base64::encode(hbb_common::get_uuid());
+    let uuid = crate::encode64(hbb_common::get_uuid());
 
     if uuid.is_empty() {
         log::error!("Failed to change id, uuid is_empty");