diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index aa1f5595b..39fca8c5d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -78,10 +78,45 @@ jobs: shell: bash run: | case ${{ matrix.job.target }} in - x86_64-unknown-linux-gnu) sudo apt-get -y update ; sudo apt install -y g++ gcc git curl wget nasm yasm libgtk-3-dev clang libxcb-randr0-dev libxdo-dev libxfixes-dev libxcb-shape0-dev libxcb-xfixes0-dev libasound2-dev libpulse-dev cmake libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev ;; + x86_64-unknown-linux-gnu) sudo apt-get -y update ; sudo apt install -y g++ gcc git curl wget nasm yasm libgtk-3-dev clang libxcb-randr0-dev libxdo-dev libxfixes-dev libxcb-shape0-dev libxcb-xfixes0-dev libasound2-dev libpulse-dev cmake libclang-dev ninja-build libappindicator3-dev libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev;; # arm-unknown-linux-*) sudo apt-get -y update ; sudo apt-get -y install gcc-arm-linux-gnueabihf ;; # aarch64-unknown-linux-gnu) sudo apt-get -y update ; sudo apt-get -y install gcc-aarch64-linux-gnu ;; esac + + - name: Install flutter + uses: subosito/flutter-action@v2 + with: + channel: 'stable' + + - name: Install Rust toolchain + uses: actions-rs/toolchain@v1 + with: + toolchain: stable + target: ${{ matrix.job.target }} + override: true + profile: minimal # minimal component installation (ie, no documentation) + + - name: Install flutter rust bridge deps + run: | + dart pub global activate ffigen --version 5.0.1 + # flutter_rust_bridge + pushd /tmp && git clone https://github.com/SoLongAndThanksForAllThePizza/flutter_rust_bridge --depth=1 && popd + pushd /tmp/flutter_rust_bridge/frb_codegen && cargo install --path . && popd + pushd flutter && flutter pub get && popd + ~/.cargo/bin/flutter_rust_bridge_codegen --rust-input ./src/flutter_ffi.rs --dart-output ./flutter/lib/generated_bridge.dart + + - name: Install corrosion + run: | + mkdir /tmp/corrosion + pushd /tmp/corrosion + git clone https://github.com/corrosion-rs/corrosion.git + # Optionally, specify -DCMAKE_INSTALL_PREFIX=. You can install Corrosion anyway + cmake -Scorrosion -Bbuild -DCMAKE_BUILD_TYPE=Release + cmake --build build --config Release + # This next step may require sudo or admin privileges if you're installing to a system location, + # which is the default. + sudo cmake --install build --config Release + popd - name: Restore from cache and install vcpkg uses: lukka/run-vcpkg@v7 @@ -92,15 +127,7 @@ jobs: - name: Install vcpkg dependencies run: | $VCPKG_ROOT/vcpkg install libvpx libyuv opus - shell: bash - - - name: Install Rust toolchain - uses: actions-rs/toolchain@v1 - with: - toolchain: stable - target: ${{ matrix.job.target }} - override: true - profile: minimal # minimal component installation (ie, no documentation) + shell: bash - name: Show version information (Rust, cargo, GCC) shell: bash @@ -114,12 +141,19 @@ jobs: - uses: Swatinem/rust-cache@v1 - - name: Build - uses: actions-rs/cargo@v1 - with: - use-cross: ${{ matrix.job.use-cross }} - command: build - args: --locked --release --target=${{ matrix.job.target }} +# - name: Build +# uses: actions-rs/cargo@v1 +# with: +# use-cross: ${{ matrix.job.use-cross }} +# command: build +# args: --locked --release --target=${{ matrix.job.target }} --features flutter -v + + - name: Build Flutter + run: | + pushd flutter + flutter pub get + flutter build linux --release -v + popd # - name: Strip debug information from executable # id: strip diff --git a/.gitignore b/.gitignore index 53bd9cf94..da52e1dc4 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ +/build /target .vscode .idea diff --git a/128x128.png b/128x128.png index 045d8f894..cd35a0bc8 100644 Binary files a/128x128.png and b/128x128.png differ diff --git a/128x128@2x.png b/128x128@2x.png index 39e2b23cf..3da699f1d 100644 Binary files a/128x128@2x.png and b/128x128@2x.png differ diff --git a/32x32.png b/32x32.png index bba85feb6..21440d422 100644 Binary files a/32x32.png and b/32x32.png differ diff --git a/Cargo.lock b/Cargo.lock index 6b5654a74..71c2268a9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -29,17 +29,6 @@ version = "0.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "739f4a8db6605981345c5654f3a85b056ce52f37a39d34da03f25bf2151ea16e" -[[package]] -name = "ahash" -version = "0.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47" -dependencies = [ - "getrandom", - "once_cell", - "version_check", -] - [[package]] name = "aho-corasick" version = "0.7.18" @@ -121,9 +110,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.58" +version = "1.0.57" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb07d2053ccdbe10e2af2995a2f116c1330396493dc1269f6a91d0ae82e19704" +checksum = "08f9b8508dccb7687a1d6c4ce66b2b0ecef467c94667de27d8d7fe1f8d2a9cdc" [[package]] name = "arboard" @@ -238,15 +227,15 @@ dependencies = [ [[package]] name = "async-task" -version = "4.3.0" +version = "4.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a40729d2133846d9ed0ea60a8b9541bccddab49cd30f0715a1da672fe9a2524" +checksum = "30696a84d817107fc028e049980e09d5e140e8da8f1caeb17e8e950658a3cea9" [[package]] name = "async-trait" -version = "0.1.56" +version = "0.1.53" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96cf8829f67d2eab0b2dfa42c5d0ef737e0724e4a82b01b3e292456202b19716" +checksum = "ed6aa3524a2dfcf9fe180c51eae2b58738348d819517ceadf95789c51fff7600" dependencies = [ "proc-macro2", "quote", @@ -261,7 +250,7 @@ checksum = "2c3d816ce6f0e2909a96830d6911c2aff044370b1ef92d7f267b43bae5addedd" dependencies = [ "atk-sys", "bitflags", - "glib 0.15.12", + "glib 0.15.11", "libc", ] @@ -320,9 +309,9 @@ checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" [[package]] name = "backtrace" -version = "0.3.66" +version = "0.3.65" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cab84319d616cfb654d03394f38ab7e6f0919e181b1b57e1fd15e7fb4077d9a7" +checksum = "11a17d453482a265fd5f8479f2a3f405566e6ca627837aaddb85af8b1ab8ef61" dependencies = [ "addr2line", "cc", @@ -411,15 +400,15 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.10.0" +version = "3.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37ccbd214614c6783386c1af30caf03192f17891059cecc394b4fb119e363de3" +checksum = "a4a45a46ab1f2412e53d3a0ade76ffad2025804294569aae387231a0cd6e0899" [[package]] name = "bytemuck" -version = "1.10.0" +version = "1.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c53dfa917ec274df8ed3c572698f381a24eef2efba9492d797301b72b6db408a" +checksum = "cdead85bdec19c194affaeeb670c0e41fe23de31459efd1c174d049269cf02cc" [[package]] name = "byteorder" @@ -429,11 +418,11 @@ checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" [[package]] name = "bytes" -version = "1.2.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0b3de4a0c5e67e16066a0715723abd91edc2f9001d09c46e1dca929351e130e" +checksum = "ec8a7b6a70fde80372154c65702f00a0f56f3e1c36abbc6c440484be248856db" dependencies = [ - "serde 1.0.139", + "serde 1.0.137", ] [[package]] @@ -444,13 +433,13 @@ checksum = "c1db59621ec70f09c5e9b597b220c7a2b43611f4710dc03ceb8748637775692c" [[package]] name = "cairo-rs" -version = "0.15.12" +version = "0.15.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c76ee391b03d35510d9fa917357c7f1855bd9a6659c95a1b392e33f49b3369bc" +checksum = "62be3562254e90c1c6050a72aa638f6315593e98c5cdaba9017cedbabf0a5dee" dependencies = [ "bitflags", "cairo-sys-rs", - "glib 0.15.12", + "glib 0.15.11", "libc", "thiserror", ] @@ -482,7 +471,7 @@ version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "869119e97797867fd90f5e22af7d0bd274bd4635ebb9eb68c04f3f513ae6c412" dependencies = [ - "serde 1.0.139", + "serde 1.0.137", ] [[package]] @@ -491,7 +480,7 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cbdb825da8a5df079a43676dbe042702f1707b1109f713a01420fbb4cc71fa27" dependencies = [ - "serde 1.0.139", + "serde 1.0.137", ] [[package]] @@ -502,9 +491,9 @@ checksum = "4acbb09d9ee8e23699b9634375c72795d095bf268439da88562cf9b501f181fa" dependencies = [ "camino", "cargo-platform", - "semver 1.0.12", - "serde 1.0.139", - "serde_json 1.0.82", + "semver 1.0.9", + "serde 1.0.137", + "serde_json 1.0.81", ] [[package]] @@ -513,14 +502,14 @@ version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b6d248e3ca02f3fbfabcb9284464c596baec223a26d91bbf44a5a62ddb0d900" dependencies = [ - "clap 3.2.12", + "clap 3.1.18", "heck 0.4.0", "indexmap", "log", "proc-macro2", "quote", - "serde 1.0.139", - "serde_json 1.0.82", + "serde 1.0.137", + "serde_json 1.0.81", "syn", "tempfile", "toml", @@ -611,16 +600,16 @@ dependencies = [ [[package]] name = "clap" -version = "3.2.12" +version = "3.1.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab8b79fe3946ceb4a0b1c080b4018992b8d27e9ff363644c1c9b6387c854614d" +checksum = "d2dbdf4bdacb33466e854ce889eee8dfd5729abf7ccd7664d0a2d60cd384440b" dependencies = [ "atty", "bitflags", "clap_derive", "clap_lex", "indexmap", - "once_cell", + "lazy_static", "strsim 0.10.0", "termcolor", "textwrap 0.15.0", @@ -628,9 +617,9 @@ dependencies = [ [[package]] name = "clap_derive" -version = "3.2.7" +version = "3.1.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "759bf187376e1afa7b85b959e6a664a3e7a95203415dba952ad19139e798f902" +checksum = "25320346e922cffe59c0bbc5410c8d8784509efb321488971081313cb1e1a33c" dependencies = [ "heck 0.4.0", "proc-macro-error", @@ -641,9 +630,9 @@ dependencies = [ [[package]] name = "clap_lex" -version = "0.2.4" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2850f2f5a82cbf437dd5af4d49848fbdfc27c157c3d010345776f952765261c5" +checksum = "a37c35f1112dad5e6e0b1adaff798507497a18fceeb30cceb3bae7d1427b9213" dependencies = [ "os_str_bytes", ] @@ -655,16 +644,16 @@ dependencies = [ "cc", "hbb_common", "lazy_static", - "serde 1.0.139", + "serde 1.0.137", "serde_derive", "thiserror", ] [[package]] name = "clipboard-win" -version = "4.4.2" +version = "4.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4ab1b92798304eedc095b53942963240037c0516452cb11aeba709d420b2219" +checksum = "2f3e1238132dc01f081e1cbb9dace14e5ef4c3a51ee244bd982275fb514605db" dependencies = [ "error-code", "str-buf", @@ -738,9 +727,9 @@ dependencies = [ [[package]] name = "concurrent-queue" -version = "1.2.3" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83827793632c72fa4f73c2edb31e7a997527dd8ffe7077344621fc62c5478157" +checksum = "30ed07550be01594c6026cff2a1d7fe9c8f683caa798e12b68694ac9e88286a3" dependencies = [ "cache-padded", ] @@ -751,7 +740,7 @@ version = "0.4.0" source = "git+https://github.com/open-trade/confy#630cc28a396cb7d01eefdd9f3824486fe4d8554b" dependencies = [ "directories-next", - "serde 1.0.139", + "serde 1.0.137", "thiserror", "toml", ] @@ -907,10 +896,24 @@ dependencies = [ ] [[package]] -name = "crossbeam-channel" -version = "0.5.5" +name = "crossbeam" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c02a4d71819009c192cf4872265391563fd6a84c81ff2c0f2a7026ca4c1d85c" +checksum = "4ae5588f6b3c3cb05239e90bd110f257254aecd01e4635400391aeae07497845" +dependencies = [ + "cfg-if 1.0.0", + "crossbeam-channel", + "crossbeam-deque", + "crossbeam-epoch", + "crossbeam-queue", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-channel" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5aaa7bd5fb665c6864b5f963dd9097905c54125909c7aa94c9e18507cdbe6c53" dependencies = [ "cfg-if 1.0.0", "crossbeam-utils", @@ -929,15 +932,15 @@ dependencies = [ [[package]] name = "crossbeam-epoch" -version = "0.9.9" +version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07db9d94cbd326813772c968ccd25999e5f8ae22f4f8d1b11effa37ef6ce281d" +checksum = "1145cf131a2c6ba0615079ab6a638f7e1973ac9c2634fcbeaaad6114246efe8c" dependencies = [ "autocfg 1.1.0", "cfg-if 1.0.0", "crossbeam-utils", + "lazy_static", "memoffset", - "once_cell", "scopeguard", ] @@ -953,19 +956,19 @@ dependencies = [ [[package]] name = "crossbeam-utils" -version = "0.8.10" +version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d82ee10ce34d7bc12c2122495e7593a9c41347ecdd64185af4ecf72cb1a7f83" +checksum = "0bf124c720b7686e3c2663cf54062ab0f68a88af2fb6a030e87e30bf721fcb38" dependencies = [ "cfg-if 1.0.0", - "once_cell", + "lazy_static", ] [[package]] name = "crypto-common" -version = "0.1.6" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +checksum = "57952ca27b5e3606ff4dd79b0020231aaf9d6aa76dc05fd30137538c50bd3ce8" dependencies = [ "generic-array", "typenum", @@ -987,7 +990,7 @@ version = "3.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b37feaa84e6861e00a1f5e5aa8da3ee56d605c9992d33e082786754828e20865" dependencies = [ - "nix 0.24.2", + "nix 0.24.1", "winapi 0.3.9", ] @@ -1170,9 +1173,9 @@ dependencies = [ [[package]] name = "dbus" -version = "0.9.6" +version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f8bcdd56d2e5c4ed26a529c5a9029f5db8290d433497506f958eae3be148eb6" +checksum = "de0a745c25b32caa56b82a3950f5fec7893a960f4c10ca3b02060b0c38d8c2ce" dependencies = [ "libc", "libdbus-sys", @@ -1317,7 +1320,7 @@ checksum = "7f3f119846c823f9eafcf953a8f6ffb6ed69bf6240883261a7f13b634579a51f" dependencies = [ "lazy_static", "regex", - "serde 1.0.139", + "serde 1.0.137", "strsim 0.10.0", ] @@ -1344,9 +1347,9 @@ dependencies = [ [[package]] name = "either" -version = "1.7.0" +version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f107b87b6afc2a64fd13cac55fe06d6c8859f12d4b14cbcdd2c67d0976781be" +checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457" [[package]] name = "encoding_rs" @@ -1368,7 +1371,7 @@ dependencies = [ "objc", "pkg-config", "rdev", - "serde 1.0.139", + "serde 1.0.137", "serde_derive", "unicode-segmentation", "winapi 0.3.9", @@ -1413,7 +1416,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e75d4cd21b95383444831539909fbb14b9dc3fdceb2a6f5d36577329a1f55ccb" dependencies = [ "enumflags2_derive", - "serde 1.0.139", + "serde 1.0.137", ] [[package]] @@ -1464,6 +1467,27 @@ dependencies = [ "synstructure", ] +[[package]] +name = "errno" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f639046355ee4f37944e44f60642c6f3a7efa3cf6b78c78a0d989a8ce6c396a1" +dependencies = [ + "errno-dragonfly", + "libc", + "winapi 0.3.9", +] + +[[package]] +name = "errno-dragonfly" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" +dependencies = [ + "cc", + "libc", +] + [[package]] name = "error-code" version = "2.3.1" @@ -1520,14 +1544,14 @@ dependencies = [ [[package]] name = "filetime" -version = "0.2.17" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e94a7bbaa59354bc20dd75b67f23e2797b4490e9d6928203fb105c79e448c86c" +checksum = "c0408e2626025178a6a7f7ffc05a25bc47103229f19c113755de7bf63816290c" dependencies = [ "cfg-if 1.0.0", "libc", "redox_syscall", - "windows-sys 0.36.1", + "winapi 0.3.9", ] [[package]] @@ -1542,15 +1566,14 @@ dependencies = [ [[package]] name = "flexi_logger" -version = "0.22.5" +version = "0.22.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee9a6796ff68a1014f6665dac55341820f26e63ec706e58bfaee468cf0ac174f" +checksum = "969940c39bc718475391e53a3a59b0157e64929c80cf83ad5dde5f770ecdc423" dependencies = [ "ansi_term", "atty", "chrono", - "crossbeam-channel", - "crossbeam-queue", + "crossbeam", "glob", "lazy_static", "log", @@ -1562,9 +1585,8 @@ dependencies = [ [[package]] name = "flutter_rust_bridge" -version = "1.30.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7e7e4af55d6a36aad9573737a12fba774999e4d6dd5e668e29c25bb473f85f3" +version = "1.32.0" +source = "git+https://github.com/SoLongAndThanksForAllThePizza/flutter_rust_bridge#e5adce55eea0b74d3680e66a2c5252edf17b07e1" dependencies = [ "allo-isolate", "anyhow", @@ -1576,9 +1598,8 @@ dependencies = [ [[package]] name = "flutter_rust_bridge_codegen" -version = "1.30.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3209735fd687b06b8d770ec008874119b91f7f46b4a73d17226d5c337435bb74" +version = "1.32.0" +source = "git+https://github.com/SoLongAndThanksForAllThePizza/flutter_rust_bridge#e5adce55eea0b74d3680e66a2c5252edf17b07e1" dependencies = [ "anyhow", "cargo_metadata", @@ -1591,7 +1612,7 @@ dependencies = [ "pathdiff", "quote", "regex", - "serde 1.0.139", + "serde 1.0.137", "serde_yaml", "structopt", "syn", @@ -1602,9 +1623,8 @@ dependencies = [ [[package]] name = "flutter_rust_bridge_macros" -version = "1.38.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13652b9b71bc3bf4ea3bbb5cadc9bc2350fe0fba5145f6a949309fc452576d6d" +version = "1.32.0" +source = "git+https://github.com/SoLongAndThanksForAllThePizza/flutter_rust_bridge#e5adce55eea0b74d3680e66a2c5252edf17b07e1" [[package]] name = "fnv" @@ -1789,7 +1809,7 @@ dependencies = [ "gdk-pixbuf", "gdk-sys", "gio", - "glib 0.15.12", + "glib 0.15.11", "libc", "pango", ] @@ -1803,7 +1823,7 @@ dependencies = [ "bitflags", "gdk-pixbuf-sys", "gio", - "glib 0.15.12", + "glib 0.15.11", "libc", ] @@ -1859,33 +1879,33 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.7" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4eb1a864a501629691edf6c15a593b7a51eebaa1e8468e9ddc623de7c9b58ec6" +checksum = "9be70c98951c83b8d2f8f60d7065fa6d5146873094452a1008da8c2f1e4205ad" dependencies = [ "cfg-if 1.0.0", "libc", - "wasi", + "wasi 0.10.2+wasi-snapshot-preview1", ] [[package]] name = "gimli" -version = "0.26.2" +version = "0.26.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22030e2c5a68ec659fde1e949a745124b48e6fa8b045b7ed5bd1fe4ccc5c4e5d" +checksum = "78cc372d058dcf6d5ecd98510e7fbc9e5aec4d21de70f65fea8fecebcd881bd4" [[package]] name = "gio" -version = "0.15.12" +version = "0.15.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68fdbc90312d462781a395f7a16d96a2b379bb6ef8cd6310a2df272771c4283b" +checksum = "0f132be35e05d9662b9fa0fee3f349c6621f7782e0105917f4cc73c1bf47eceb" dependencies = [ "bitflags", "futures-channel", "futures-core", "futures-io", "gio-sys", - "glib 0.15.12", + "glib 0.15.11", "libc", "once_cell", "thiserror", @@ -1925,9 +1945,9 @@ dependencies = [ [[package]] name = "glib" -version = "0.15.12" +version = "0.15.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edb0306fbad0ab5428b0ca674a23893db909a98582969c9b537be4ced78c505d" +checksum = "bd124026a2fa8c33a3d17a3fe59c103f2d9fa5bd92c19e029e037736729abeab" dependencies = [ "bitflags", "futures-channel", @@ -2169,7 +2189,7 @@ dependencies = [ "gdk", "gdk-pixbuf", "gio", - "glib 0.15.12", + "glib 0.15.11", "gtk-sys", "gtk3-macros", "libc", @@ -2225,7 +2245,7 @@ dependencies = [ "indexmap", "slab", "tokio", - "tokio-util", + "tokio-util 0.7.2", "tracing", ] @@ -2235,17 +2255,14 @@ version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d7afe4a420e3fe79967a00898cc1f4db7c8a49a9333a29f8a4bd76a253d5cd04" dependencies = [ - "ahash 0.4.7", + "ahash", ] [[package]] name = "hashbrown" -version = "0.12.3" +version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" -dependencies = [ - "ahash 0.7.6", -] +checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e" [[package]] name = "hbb_common" @@ -2269,15 +2286,15 @@ dependencies = [ "quinn", "rand 0.8.5", "regex", - "serde 1.0.139", + "serde 1.0.137", "serde_derive", - "serde_json 1.0.82", + "serde_json 1.0.81", "serde_with", "socket2 0.3.19", "sodiumoxide", "tokio", "tokio-socks", - "tokio-util", + "tokio-util 0.7.2", "toml", "winapi 0.3.9", "zstd", @@ -2321,9 +2338,9 @@ checksum = "8a164bb2ceaeff4f42542bdb847c41517c78a60f5649671b2a07312b6e117549" [[package]] name = "http" -version = "0.2.8" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75f43d41e26995c17e71ee126451dd3941010b0514a81a9d11f3b341debc2399" +checksum = "ff8670570af52249509a86f5e3e18a08c60b177071826898fde8997cf5f6bfbb" dependencies = [ "bytes", "fnv", @@ -2362,21 +2379,21 @@ checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" [[package]] name = "hwcodec" version = "0.1.0" -source = "git+https://github.com/21pages/hwcodec#91d1cd327c88490f917457072aeef0676ddb2be7" +source = "git+https://github.com/21pages/hwcodec#890204e0703a3d361fc7a45f035fe75c0575bb1d" dependencies = [ "bindgen", "cc", "log", - "serde 1.0.139", + "serde 1.0.137", "serde_derive", - "serde_json 1.0.82", + "serde_json 1.0.81", ] [[package]] name = "hyper" -version = "0.14.20" +version = "0.14.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02c929dc5c39e335a03c405292728118860721b10190d98c2a0f0efd5baafbac" +checksum = "42dc3c131584288d375f2d07f822b0cb012d8c6fb899a5b9fdb3cb7eb9b6004f" dependencies = [ "bytes", "futures-channel", @@ -2463,12 +2480,12 @@ dependencies = [ [[package]] name = "indexmap" -version = "1.9.1" +version = "1.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10a35a97730320ffe8e2d410b5d3b69279b98d2c14bdb8b70ea89ecf7888d41e" +checksum = "e6012d540c5baa3589337a98ce73408de9b5a25ec9fc2c6fd6be8f0d39e0ca5a" dependencies = [ "autocfg 1.1.0", - "hashbrown 0.12.3", + "hashbrown 0.11.2", ] [[package]] @@ -2556,9 +2573,9 @@ checksum = "229d53d58899083193af11e15917b5640cd40b29ff475a1fe4ef725deb02d0f2" [[package]] name = "js-sys" -version = "0.3.58" +version = "0.3.57" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3fac17f7123a73ca62df411b1bf727ccc805daa070338fda671c86dac1bdc27" +checksum = "671a26f820db17c2a2750743f1dd03bafd15b98c9f30c7c2628c024c05d73397" dependencies = [ "wasm-bindgen", ] @@ -2587,11 +2604,11 @@ checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" [[package]] name = "libappindicator" -version = "0.7.1" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db2d3cb96d092b4824cb306c9e544c856a4cb6210c1081945187f7f1924b47e8" +checksum = "97b29fab3280d59f3d06725f75da9ef9a1b001b2c748b1abfebd1c966c61d7de" dependencies = [ - "glib 0.15.12", + "glib 0.15.11", "gtk", "gtk-sys", "libappindicator-sys", @@ -2600,9 +2617,9 @@ dependencies = [ [[package]] name = "libappindicator-sys" -version = "0.7.3" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1b3b6681973cea8cc3bce7391e6d7d5502720b80a581c9a95c9cbaf592826aa" +checksum = "d83c2227727d7950ada2ae554613d35fd4e55b87f0a29b86d2368267d19b1d99" dependencies = [ "gtk-sys", "libloading", @@ -2705,9 +2722,9 @@ dependencies = [ [[package]] name = "linked-hash-map" -version = "0.5.6" +version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" +checksum = "7fb9b38af92608140b86b693604b9ffcc5824240a484d1ecd4795bacb2fe88f3" [[package]] name = "lock_api" @@ -2759,7 +2776,7 @@ dependencies = [ [[package]] name = "magnum-opus" version = "0.4.0" -source = "git+https://github.com/open-trade/magnum-opus#3c3d0b86ae95c84930bebffe4bcb03b3bd83342b" +source = "git+https://github.com/SoLongAndThanksForAllThePizza/magnum-opus#6247071a64af7b18e2d553e235729e6865f63ece" dependencies = [ "bindgen", "target_build_utils", @@ -2871,13 +2888,13 @@ dependencies = [ [[package]] name = "mio" -version = "0.8.4" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57ee1c23c7c63b0c9250c339ffdc69255f110b298b901b9f6c82547b7b87caaf" +checksum = "713d550d9b44d89174e066b7a6217ae06234c10cb47819a88290d2b353c31799" dependencies = [ "libc", "log", - "wasi", + "wasi 0.11.0+wasi-snapshot-preview1", "windows-sys 0.36.1", ] @@ -3067,9 +3084,9 @@ dependencies = [ [[package]] name = "nix" -version = "0.24.2" +version = "0.24.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "195cdbc1741b8134346d515b3a56a1c94b0912758009cfd53f99ea0f57b065fc" +checksum = "8f17df307904acd05aa8e32e97bb20f2a0df1728bbc2d771ae8f9a90463441e9" dependencies = [ "bitflags", "cfg-if 1.0.0", @@ -3097,9 +3114,9 @@ dependencies = [ [[package]] name = "num-complex" -version = "0.4.2" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ae39348c8bc5fbd7f40c727a9925f03517afd2ab27d46702108b6a7e5414c19" +checksum = "97fbc387afefefd5e9e39493299f3069e14a140dd34dc19b4c1c1a8fddb6a790" dependencies = [ "num-traits 0.2.15", ] @@ -3236,9 +3253,9 @@ dependencies = [ [[package]] name = "object" -version = "0.29.0" +version = "0.28.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21158b2c33aa6d4561f1c0a6ea283ca92bc54802a93b263e910746d679a7eb53" +checksum = "e42c982f2d955fac81dd7e1d0e1426a7d702acd9c98d19ab01083a6a0328c424" dependencies = [ "memchr", ] @@ -3268,9 +3285,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.13.0" +version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18a6dbe30758c9f83eb00cbea4ac95966305f5a7772f3f42ebfc7fc7eddbd8e1" +checksum = "074864da206b4973b84eb91683020dbefd6a8c3f0f38e054d93954e891935e4e" [[package]] name = "openssl-probe" @@ -3300,9 +3317,9 @@ dependencies = [ [[package]] name = "os_str_bytes" -version = "6.2.0" +version = "6.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "648001efe5d5c0102d8cea768e348da85d90af8ba91f0bea908f157951493cd4" +checksum = "21326818e99cfe6ce1e524c2a805c189a99b5ae555a35d19f9a284b427d86afa" [[package]] name = "padlock" @@ -3317,7 +3334,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "22e4045548659aee5313bde6c582b0d83a627b7904dd20dc2d9ef0895d414e4f" dependencies = [ "bitflags", - "glib 0.15.12", + "glib 0.15.11", "libc", "once_cell", "pango-sys", @@ -3337,8 +3354,8 @@ dependencies = [ [[package]] name = "parity-tokio-ipc" -version = "0.7.3-1" -source = "git+https://github.com/open-trade/parity-tokio-ipc#20b2895910161605210657f3e751edd55321f698" +version = "0.7.3" +source = "git+https://github.com/open-trade/parity-tokio-ipc#52515618bd30ea8101bf46f6c7835e88cec9187f" dependencies = [ "futures", "libc", @@ -3477,18 +3494,18 @@ dependencies = [ [[package]] name = "pin-project" -version = "1.0.11" +version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78203e83c48cffbe01e4a2d35d566ca4de445d79a85372fc64e378bfc812a260" +checksum = "58ad3879ad3baf4e44784bc6a718a8698867bb991f8ce24d1bcbe2cfb4c3a75e" dependencies = [ "pin-project-internal", ] [[package]] name = "pin-project-internal" -version = "1.0.11" +version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "710faf75e1b33345361201d36d04e98ac1ed8909151a017ed384700836104c74" +checksum = "744b6f092ba29c3650faf274db506afd39944f48420f6c86b17cfe0ee1cb36bb" dependencies = [ "proc-macro2", "quote", @@ -3604,9 +3621,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.40" +version = "1.0.39" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd96a1e8ed2596c337f8eae5f24924ec83f5ad5ab21ea8e455d3566c69fbcaf7" +checksum = "c54b25569025b7fc9651de43004ae593a75ad88543b17178aa5e1b9c4f15f56f" dependencies = [ "unicode-ident", ] @@ -3731,9 +3748,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.20" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3bcdf212e9776fbcb2d23ab029360416bb1706b1aea2d1a5ba002727cbcab804" +checksum = "a1feb54ed693b93a84e14094943b84b7c4eae204c512b7ccb95ab0c66d278ad1" dependencies = [ "proc-macro2", ] @@ -3970,9 +3987,9 @@ dependencies = [ [[package]] name = "regex" -version = "1.6.0" +version = "1.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c4eb3267174b8c6c2f654116623910a0fef09c4753f8dd83db29c48a0df988b" +checksum = "d83f127d94bdbcda4c8cc2e50f6f84f4b611f69c902699ca385a39c3a75f9ff1" dependencies = [ "aho-corasick", "memchr", @@ -3981,9 +3998,9 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.6.27" +version = "0.6.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3f87b73ce11b1619a3c6332f45341e0047173771e8b8b73f87bfeefb7b56244" +checksum = "49b3de9ec5dc0a3417da371aab17d729997c15010e7fd24ff707773a33bddb64" [[package]] name = "remove_dir_all" @@ -4006,9 +4023,9 @@ dependencies = [ [[package]] name = "reqwest" -version = "0.11.11" +version = "0.11.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b75aa69a3f06bbcc66ede33af2af253c6f7a86b1ca0033f60c580a27074fbf92" +checksum = "46a1f7aa4f35e5e8b4160449f51afc758f0ce6454315a9fa7d0d113e958c41eb" dependencies = [ "base64", "bytes", @@ -4028,13 +4045,12 @@ dependencies = [ "percent-encoding", "pin-project-lite", "rustls", - "rustls-pemfile 1.0.0", - "serde 1.0.139", - "serde_json 1.0.82", + "rustls-pemfile 0.3.0", + "serde 1.0.137", + "serde_json 1.0.81", "serde_urlencoded", "tokio", "tokio-rustls", - "tower-service", "url", "wasm-bindgen", "wasm-bindgen-futures", @@ -4152,7 +4168,7 @@ dependencies = [ "bytes", "cc", "cfg-if 1.0.0", - "clap 3.2.12", + "clap 3.1.18", "clipboard", "cocoa", "core-foundation 0.9.3", @@ -4164,6 +4180,7 @@ dependencies = [ "default-net", "dispatch", "enigo", + "errno", "evdev", "flexi_logger", "flutter_rust_bridge", @@ -4194,9 +4211,9 @@ dependencies = [ "samplerate", "sciter-rs", "scrap", - "serde 1.0.139", + "serde 1.0.137", "serde_derive", - "serde_json 1.0.82", + "serde_json 1.0.81", "sha2", "simple_rc", "sys-locale", @@ -4263,6 +4280,15 @@ dependencies = [ "base64", ] +[[package]] +name = "rustls-pemfile" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ee86d63972a7c661d1536fefe8c3c8407321c3df668891286de28abcd087360" +dependencies = [ + "base64", +] + [[package]] name = "rustls-pemfile" version = "1.0.0" @@ -4274,9 +4300,9 @@ dependencies = [ [[package]] name = "rustversion" -version = "1.0.8" +version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24c8ad4f0c00e1eb5bc7614d236a7f1300e3dbd76b68cac8e06fb00b015ad8d8" +checksum = "f2cc38e8fa666e2de3c4aba7edeb5ffc5246c1c2ed0e3d17e560aeeba736b23f" [[package]] name = "ryu" @@ -4357,8 +4383,8 @@ dependencies = [ "num_cpus", "quest", "repng", - "serde 1.0.139", - "serde_json 1.0.82", + "serde 1.0.137", + "serde_json 1.0.81", "target_build_utils", "tracing", "webm", @@ -4409,11 +4435,11 @@ dependencies = [ [[package]] name = "semver" -version = "1.0.12" +version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2333e6df6d6598f2b1974829f853c2b4c5f4a6e503c10af918081aa6f8564e1" +checksum = "8cb243bdfdb5936c8dc3c45762a19d12ab4550cdc753bc247637d4ec35a040fd" dependencies = [ - "serde 1.0.139", + "serde 1.0.137", ] [[package]] @@ -4433,18 +4459,18 @@ checksum = "34b623917345a631dc9608d5194cc206b3fe6c3554cd1c75b937e55e285254af" [[package]] name = "serde" -version = "1.0.139" +version = "1.0.137" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0171ebb889e45aa68b44aee0859b3eede84c6f5f5c228e6f140c0b2a0a46cad6" +checksum = "61ea8d54c77f8315140a05f4c7237403bf38b72704d031543aa1d16abbf517d1" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.139" +version = "1.0.137" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc1d3230c1de7932af58ad8ffbe1d784bd55efd5a9d84ac24f69c72d83543dfb" +checksum = "1f26faba0c3959972377d3b2d306ee9f71faee9714294e41bb777f83f88578be" dependencies = [ "proc-macro2", "quote", @@ -4465,13 +4491,13 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.82" +version = "1.0.81" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82c2c1fdcd807d1098552c5b9a36e425e42e9fbd7c6a37a8425f390f781f7fa7" +checksum = "9b7ce2b32a1aed03c558dc61a5cd328f15aff2dbc17daad8fb8af04d2100e15c" dependencies = [ "itoa 1.0.2", "ryu", - "serde 1.0.139", + "serde 1.0.137", ] [[package]] @@ -4494,7 +4520,7 @@ dependencies = [ "form_urlencoded", "itoa 1.0.2", "ryu", - "serde 1.0.139", + "serde 1.0.137", ] [[package]] @@ -4503,7 +4529,7 @@ version = "1.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "678b5a069e50bf00ecd22d0cd8ddf7c236f68581b03db652061ed5eb13a312ff" dependencies = [ - "serde 1.0.139", + "serde 1.0.137", "serde_with_macros", ] @@ -4521,13 +4547,13 @@ dependencies = [ [[package]] name = "serde_yaml" -version = "0.8.26" +version = "0.8.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "578a7433b776b56a35785ed5ce9a7e777ac0598aac5a6dd1b4b18a307c7fc71b" +checksum = "707d15895415db6628332b737c838b88c598522e4dc70647e59b72312924aebc" dependencies = [ "indexmap", "ryu", - "serde 1.0.139", + "serde 1.0.137", "yaml-rust", ] @@ -4594,7 +4620,7 @@ version = "0.1.0" dependencies = [ "confy", "hbb_common", - "serde 1.0.139", + "serde 1.0.137", "serde_derive", "walkdir", ] @@ -4613,9 +4639,9 @@ checksum = "eb703cfe953bccee95685111adeedb76fabe4e97549a58d16f03ea7b9367bb32" [[package]] name = "smallvec" -version = "1.9.0" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fd0db749597d91ff862fd1d55ea87f7855a744a8425a64695b6fca237d1dad1" +checksum = "f2dd574626839106c320a323308629dcb1acfc96e32a8cba364ddc61ac23ee83" [[package]] name = "smithay-client-toolkit" @@ -4666,7 +4692,7 @@ dependencies = [ "ed25519", "libc", "libsodium-sys", - "serde 1.0.139", + "serde 1.0.137", ] [[package]] @@ -4755,9 +4781,9 @@ dependencies = [ [[package]] name = "syn" -version = "1.0.98" +version = "1.0.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c50aef8a904de4c23c788f104b7dddc7d6f79c647c7c8ce4cc8f73eb0ca773dd" +checksum = "fbaf6116ab8924f39d52792136fb74fd60a80194cf1b1c6ffa6453eef1c3f942" dependencies = [ "proc-macro2", "quote", @@ -4778,24 +4804,22 @@ dependencies = [ [[package]] name = "sys-locale" -version = "0.2.1" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "658ee915b6c7b73ec4c1ffcd838506b5c5a4087eadc1ec8f862f1066cf2c8132" +checksum = "3913c5a3d30054d7f77cf07cdd800c8103ace15c6e44437c5db66a43dd3a92cf" dependencies = [ "cc", "cstr_core", - "js-sys", "libc", - "wasm-bindgen", "web-sys", "winapi 0.3.9", ] [[package]] name = "sysinfo" -version = "0.24.6" +version = "0.24.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b6e19da72a8d75be4d40e4dd4686afca31507f26c3ffdf6bd3073278d9de0a0" +checksum = "54cb4ebf3d49308b99e6e9dc95e989e2fdbdc210e4f67c39db0bb89ba927001c" dependencies = [ "cfg-if 1.0.0", "core-foundation-sys 0.8.3", @@ -4980,9 +5004,9 @@ dependencies = [ [[package]] name = "time" -version = "0.3.11" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72c91f41dcb2f096c05f0873d667dceec1087ce5bcf984ec8ffb19acddbb3217" +checksum = "c2702e08a7a860f005826c6815dcac101b19b5eb330c27fe4a5928fec1d20ddd" dependencies = [ "itoa 1.0.2", "libc", @@ -5013,15 +5037,15 @@ checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" [[package]] name = "tokio" -version = "1.20.0" +version = "1.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57aec3cfa4c296db7255446efb4928a6be304b431a806216105542a67b6ca82e" +checksum = "7a8325f63a7d4774dd041e363b2409ed1c5cbbd0f867795e661df066b2b0a581" dependencies = [ "autocfg 1.1.0", "bytes", "libc", "memchr", - "mio 0.8.4", + "mio 0.8.3", "num_cpus", "once_cell", "parking_lot 0.12.1", @@ -5034,9 +5058,9 @@ dependencies = [ [[package]] name = "tokio-macros" -version = "1.8.0" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9724f9a975fb987ef7a3cd9be0350edcbe130698af5b8f7a631e23d42d052484" +checksum = "b557f72f448c511a979e2564e55d74e6c4432fc96ff4f6241bc6bded342643b7" dependencies = [ "proc-macro2", "quote", @@ -5056,8 +5080,8 @@ dependencies = [ [[package]] name = "tokio-socks" -version = "0.5.1-1" -source = "git+https://github.com/open-trade/tokio-socks#7034e79263ce25c348be072808d7601d82cd892d" +version = "0.5.1" +source = "git+https://github.com/open-trade/tokio-socks#3de8300fbce37e2cdaef042e016aa95058d007cf" dependencies = [ "bytes", "either", @@ -5067,21 +5091,34 @@ dependencies = [ "pin-project", "thiserror", "tokio", - "tokio-util", + "tokio-util 0.6.10", ] [[package]] name = "tokio-util" -version = "0.7.3" +version = "0.6.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc463cd8deddc3770d20f9852143d50bf6094e640b485cb2e189a2099085ff45" +checksum = "36943ee01a6d67977dd3f84a5a1d2efeb4ada3a1ae771cadfaa535d9d9fc6507" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "log", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f988a1a1adc2fb21f9c12aa96441da33a1728193ae0b95d2be22dbd17fcb4e5c" dependencies = [ "bytes", "futures-core", "futures-io", "futures-sink", "futures-util", - "hashbrown 0.12.3", "pin-project-lite", "slab", "tokio", @@ -5094,20 +5131,20 @@ version = "0.5.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8d82e1a7758622a465f8cee077614c73484dac5b836c02ff6a40d5d1010324d7" dependencies = [ - "serde 1.0.139", + "serde 1.0.137", ] [[package]] name = "tower-service" -version = "0.3.2" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" +checksum = "360dfd1d6d30e05fda32ace2c8c70e9c0a9da713275777f5a4dbb8a1893930c6" [[package]] name = "tracing" -version = "0.1.35" +version = "0.1.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a400e31aa60b9d44a52a8ee0343b5b18566b03a8321e0d321f695cf56e940160" +checksum = "5d0ecdcb44a79f0fe9844f0c4f33a342cbcbb5117de8001e6ba0dc2351327d09" dependencies = [ "cfg-if 1.0.0", "pin-project-lite", @@ -5117,9 +5154,9 @@ dependencies = [ [[package]] name = "tracing-attributes" -version = "0.1.22" +version = "0.1.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11c75893af559bc8e10716548bdef5cb2b983f8e637db9d0e15126b61b484ee2" +checksum = "cc6b8ad3567499f98a1db7a752b07a7c8c7c7c34c332ec00effb2b0027974b7c" dependencies = [ "proc-macro2", "quote", @@ -5128,11 +5165,11 @@ dependencies = [ [[package]] name = "tracing-core" -version = "0.1.28" +version = "0.1.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b7358be39f2f274f322d2aaed611acc57f382e8eb1e5b48cb9ae30933495ce7" +checksum = "f54c8ca710e81886d498c2fd3331b56c93aa248d49de2222ad2742247c60072f" dependencies = [ - "once_cell", + "lazy_static", ] [[package]] @@ -5186,9 +5223,9 @@ checksum = "dcf81ac59edc17cc8697ff311e8f5ef2d99fcbd9817b34cec66f90b6c3dfd987" [[package]] name = "ucd-trie" -version = "0.1.4" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89570599c4fe5585de2b388aab47e99f7fa4e9238a1399f707a02e356058141c" +checksum = "56dee185309b50d1f11bfedef0fe6d036842e3fb77413abef29f8f8d1c5d4c1c" [[package]] name = "uds_windows" @@ -5208,15 +5245,15 @@ checksum = "099b7128301d285f79ddd55b9a83d5e6b9e97c92e0ea0daebee7263e932de992" [[package]] name = "unicode-ident" -version = "1.0.2" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15c61ba63f9235225a22310255a29b806b907c9b8c964bcbd0a2c70f3f2deea7" +checksum = "d22af068fba1eb5edcb4aea19d382b2a3deb4c8f9d475c589b6ada9e0fd493ee" [[package]] name = "unicode-normalization" -version = "0.1.21" +version = "0.1.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "854cbdc4f7bc6ae19c820d44abdc3277ac3e1b2b93db20a636825d9322fb60e6" +checksum = "d54590932941a9e9266f0832deed84ebe1bf2e4c9e4a3554d393d18f5e854bf9" dependencies = [ "tinyvec", ] @@ -5259,9 +5296,9 @@ dependencies = [ [[package]] name = "uuid" -version = "1.1.2" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd6469f4314d5f1ffec476e05f17cc9a78bc7a27a6a857842170bdf8d6f98d2f" +checksum = "c6d5d669b51467dcf7b2f1a796ce0f955f05f01cafda6c19d6e95f730df29238" dependencies = [ "getrandom", ] @@ -5297,7 +5334,7 @@ dependencies = [ "cc", "hbb_common", "lazy_static", - "serde 1.0.139", + "serde 1.0.137", "serde_derive", "thiserror", ] @@ -5329,6 +5366,12 @@ dependencies = [ "try-lock", ] +[[package]] +name = "wasi" +version = "0.10.2+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6" + [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" @@ -5337,9 +5380,9 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" -version = "0.2.81" +version = "0.2.80" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c53b543413a17a202f4be280a7e5c62a1c69345f5de525ee64f8cfdbc954994" +checksum = "27370197c907c55e3f1a9fbe26f44e937fe6451368324e009cba39e139dc08ad" dependencies = [ "cfg-if 1.0.0", "wasm-bindgen-macro", @@ -5347,9 +5390,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-backend" -version = "0.2.81" +version = "0.2.80" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5491a68ab4500fa6b4d726bd67408630c3dbe9c4fe7bda16d5c82a1fd8c7340a" +checksum = "53e04185bfa3a779273da532f5025e33398409573f348985af9a1cbf3774d3f4" dependencies = [ "bumpalo", "lazy_static", @@ -5362,9 +5405,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.31" +version = "0.4.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de9a9cec1733468a8c657e57fa2413d2ae2c0129b95e87c5b72b8ace4d13f31f" +checksum = "6f741de44b75e14c35df886aff5f1eb73aa114fa5d4d00dcd37b5e01259bf3b2" dependencies = [ "cfg-if 1.0.0", "js-sys", @@ -5374,9 +5417,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.81" +version = "0.2.80" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c441e177922bc58f1e12c022624b6216378e5febc2f0533e41ba443d505b80aa" +checksum = "17cae7ff784d7e83a2fe7611cfe766ecf034111b49deb850a3dc7699c08251f5" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -5384,9 +5427,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.81" +version = "0.2.80" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d94ac45fcf608c1f45ef53e748d35660f168490c10b23704c7779ab8f5c3048" +checksum = "99ec0dc7a4756fffc231aab1b9f2f578d23cd391390ab27f952ae0c9b3ece20b" dependencies = [ "proc-macro2", "quote", @@ -5397,9 +5440,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.81" +version = "0.2.80" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a89911bd99e5f3659ec4acf9c4d93b0a90fe4a2a11f15328472058edc5261be" +checksum = "d554b7f530dee5964d9a9468d95c1f8b8acae4f282807e7d27d4b03099a46744" [[package]] name = "wayland-client" @@ -5476,9 +5519,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.58" +version = "0.3.57" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fed94beee57daf8dd7d51f2b15dc2bcde92d7a72304cdf662a4371008b71b90" +checksum = "7b17e741662c70c8bd24ac5c5b18de314a2c26c32bf8346ee1e6f53de919c283" dependencies = [ "js-sys", "wasm-bindgen", @@ -5514,18 +5557,18 @@ dependencies = [ [[package]] name = "webpki-roots" -version = "0.22.4" +version = "0.22.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1c760f0d366a6c24a02ed7816e23e691f5d92291f94d15e836006fd11b04daf" +checksum = "44d8de8415c823c8abd270ad483c6feeac771fad964890779f9a8cb24fbbc1bf" dependencies = [ "webpki", ] [[package]] name = "weezl" -version = "0.1.7" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9193164d4de03a926d909d3bc7c30543cecb35400c02114792c2cae20d5e2dbb" +checksum = "9c97e489d8f836838d497091de568cf16b117486d529ec5579233521065bd5e4" [[package]] name = "wepoll-ffi" @@ -5788,7 +5831,7 @@ dependencies = [ "lazy_static", "libc", "log", - "mio 0.8.4", + "mio 0.8.3", "ndk 0.5.0", "ndk-glue 0.5.2", "ndk-sys 0.2.2", @@ -5847,7 +5890,7 @@ version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d7f97e69b28b256ccfb02472c25057132e234aa8368fea3bb0268def564ce1f2" dependencies = [ - "clap 3.2.12", + "clap 3.1.18", ] [[package]] @@ -5954,7 +5997,7 @@ dependencies = [ "once_cell", "ordered-stream", "rand 0.8.5", - "serde 1.0.139", + "serde 1.0.137", "serde_repr", "sha1", "static_assertions", @@ -5985,7 +6028,7 @@ version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "41a408fd8a352695690f53906dc7fd036be924ec51ea5e05666ff42685ed0af5" dependencies = [ - "serde 1.0.139", + "serde 1.0.137", "static_assertions", "zvariant", ] @@ -6028,7 +6071,7 @@ dependencies = [ "byteorder", "enumflags2", "libc", - "serde 1.0.139", + "serde 1.0.137", "static_assertions", "zvariant_derive", ] diff --git a/Cargo.toml b/Cargo.toml index 5e6d509e5..708e6660b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,8 +24,9 @@ appimage = [] use_samplerate = ["samplerate"] use_rubato = ["rubato"] use_dasp = ["dasp"] +flutter = ["flutter_rust_bridge"] +default = ["use_dasp","flutter"] hwcodec = ["scrap/hwcodec"] -default = ["use_dasp"] # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html @@ -44,7 +45,7 @@ libc = "0.2" parity-tokio-ipc = { git = "https://github.com/open-trade/parity-tokio-ipc" } flexi_logger = { version = "0.22", features = ["async", "use_chrono_for_offset"] } runas = "0.2" -magnum-opus = { git = "https://github.com/open-trade/magnum-opus" } +magnum-opus = { git = "https://github.com/SoLongAndThanksForAllThePizza/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 } @@ -58,6 +59,8 @@ num_cpus = "1.13" bytes = { version = "1.2", features = ["serde"] } default-net = "0.11.0" wol-rs = "0.9.1" +flutter_rust_bridge = { git = "https://github.com/SoLongAndThanksForAllThePizza/flutter_rust_bridge", optional = true } +errno = "0.2.8" [target.'cfg(not(target_os = "linux"))'.dependencies] reqwest = { version = "0.11", features = ["json", "rustls-tls"], default-features=false } @@ -113,7 +116,7 @@ android_logger = "0.11" jni = "0.19" [target.'cfg(any(target_os = "android", target_os = "ios"))'.dependencies] -flutter_rust_bridge = "=1.30.0" +flutter_rust_bridge = { git = "https://github.com/SoLongAndThanksForAllThePizza/flutter_rust_bridge" } [workspace] members = ["libs/scrap", "libs/hbb_common", "libs/enigo", "libs/clipboard", "libs/virtual_display", "libs/simple_rc"] @@ -131,7 +134,7 @@ winapi = { version = "0.3", features = [ "winnt" ] } cc = "1.0" hbb_common = { path = "libs/hbb_common" } simple_rc = { path = "libs/simple_rc", optional = true } -flutter_rust_bridge_codegen = "=1.30.0" +flutter_rust_bridge_codegen = { git = "https://github.com/SoLongAndThanksForAllThePizza/flutter_rust_bridge" } [dev-dependencies] hound = "3.4" diff --git a/PKGBUILD b/PKGBUILD index 0d67a28b6..6fb65d48b 100644 --- a/PKGBUILD +++ b/PKGBUILD @@ -1,5 +1,5 @@ pkgname=rustdesk -pkgver=1.1.9 +pkgver=1.1.10 pkgrel=0 epoch= pkgdesc="" @@ -27,5 +27,5 @@ package() { install -Dm 644 $HBB/rustdesk.service -t "${pkgdir}/usr/share/rustdesk/files" install -Dm 644 $HBB/rustdesk.desktop -t "${pkgdir}/usr/share/rustdesk/files" install -Dm 644 $HBB/pynput_service.py -t "${pkgdir}/usr/share/rustdesk/files" - install -Dm 644 $HBB/256-no-margin.png "${pkgdir}/usr/share/rustdesk/files/rustdesk.png" + install -Dm 644 $HBB/128x128@2x.png "${pkgdir}/usr/share/rustdesk/files/rustdesk.png" } diff --git a/README.md b/README.md index 79a4b18d3..456862af5 100644 --- a/README.md +++ b/README.md @@ -39,14 +39,14 @@ Below are the servers you are using for free, it may change along the time. If y ## Dependencies -Desktop versions use [sciter](https://sciter.com/) for GUI, please download sciter dynamic library yourself. +Desktop versions use [sciter](https://sciter.com/) or Flutter for GUI, this tutorial is for Sciter only. + +Please download sciter dynamic library yourself. [Windows](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.win/x64/sciter.dll) | [Linux](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.lnx/x64/libsciter-gtk.so) | [MacOS](https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.osx/libsciter.dylib) -Mobile versions use Flutter. We will migrate desktop version from Sciter to Flutter. - ## Raw steps to build - Prepare your Rust development env and C++ build env diff --git a/build.py b/build.py index 341f4f4e6..6b03cb57b 100755 --- a/build.py +++ b/build.py @@ -66,6 +66,8 @@ def make_parser(): default='', help='Integrate features, windows only.' 'Available: IddDriver, PrivacyMode. Special value is "ALL" and empty "". Default is empty.') + parser.add_argument('--flutter', action='store_true', + help='Build flutter package', default=False) parser.add_argument( '--hwcodec', action='store_true', @@ -114,9 +116,58 @@ def get_features(args): features.extend(get_rc_features(args)) if args.hwcodec: features.append('hwcodec') + if args.flutter: + features.append('flutter') print("features:", features) return features + +def build_flutter_deb(version): + os.chdir('flutter') + os.system('dpkg-deb -R rustdesk.deb tmpdeb') + # os.system('flutter build linux --release') + os.system('rm tmpdeb/usr/bin/rustdesk') + os.system('strip build/linux/x64/release/liblibrustdesk.so') + os.system('mkdir -p tmpdeb/usr/lib/rustdesk') + os.system('mkdir -p tmpdeb/usr/share/rustdesk/files/systemd/') + os.system( + 'cp -r build/linux/x64/release/bundle/* tmpdeb/usr/lib/rustdesk/') + os.system( + 'pushd tmpdeb && ln -s /usr/lib/rustdesk/flutter_hbb usr/bin/rustdesk && popd') + os.system( + 'cp build/linux/x64/release/liblibrustdesk.so tmpdeb/usr/lib/rustdesk/librustdesk.so') + os.system( + 'cp rustdesk.service tmpdeb/usr/share/rustdesk/files/systemd/') + os.system( + 'cp rustdesk.service.user tmpdeb/usr/share/rustdesk/files/systemd/') + os.system( + 'cp ../pynput_service.py tmpdeb/usr/share/rustdesk/files/') + os.system( + 'cp ../128x128@2x.png tmpdeb/usr/share/rustdesk/files/rustdesk.png') + os.system( + 'cp rustdesk.desktop tmpdeb/usr/share/applications/rustdesk.desktop') + os.system('mkdir -p tmpdeb/DEBIAN') + os.system('cp -a ../DEBIAN/* tmpdeb/DEBIAN/') + md5_file('usr/share/rustdesk/files/systemd/rustdesk.service') + md5_file('usr/share/rustdesk/files/systemd/rustdesk.service.user') + md5_file('usr/share/rustdesk/files/pynput_service.py') + os.system('dpkg-deb -b tmpdeb rustdesk.deb; /bin/rm -rf tmpdeb/') + os.rename('rustdesk.deb', '../rustdesk-%s.deb' % version) + os.chdir("..") + + +def build_flutter_arch_manjaro(version): + os.chdir('flutter') + os.system('flutter build linux --release') + os.system('strip build/linux/x64/release/liblibrustdesk.so') + os.system("sed -i 's/pkgver=.*/pkgver=%s/g' PKGBUILD" % version) + # pacman -S -needed base-devel + os.system('HBB=`pwd` makepkg -f') + os.system( + 'mv rustdesk-%s-0-x86_64.pkg.tar.zst ../rustdesk-%s-manjaro-arch.pkg.tar.zst' % (version, version)) + os.chdir('..') + + def main(): parser = make_parser() args = parser.parse_args() @@ -135,6 +186,7 @@ def main(): os.system('git checkout src/ui/common.tis') version = get_version() features = ",".join(get_features(args)) + flutter = args.flutter if windows: os.system('cargo build --release --features ' + features) # os.system('upx.exe target/release/rustdesk.exe') @@ -146,15 +198,19 @@ def main(): else: print('Not signed') os.system(f'cp -rf target/release/RustDesk.exe rustdesk-{version}-setdown.exe') - elif os.path.isfile('/usr/bin/pacman'): - os.system('cargo build --release --features ' + features) - os.system('git checkout src/ui/common.tis') - os.system('strip target/release/rustdesk') - os.system("sed -i 's/pkgver=.*/pkgver=%s/g' PKGBUILD" % version) - # pacman -S -needed base-devel - os.system('HBB=`pwd` makepkg -f') - os.system('mv rustdesk-%s-0-x86_64.pkg.tar.zst rustdesk-%s-manjaro-arch.pkg.tar.zst' % (version, version)) - # pacman -U ./rustdesk.pkg.tar.zst + elif os.path.isfile('/usr/bin/pacman1'): + if flutter: + build_flutter_arch_manjaro(version) + else: + # os.system('cargo build --release --features ' + features) + os.system('git checkout src/ui/common.tis') + os.system('strip target/release/rustdesk') + os.system("sed -i 's/pkgver=.*/pkgver=%s/g' PKGBUILD" % version) + # pacman -S -needed base-devel + os.system('HBB=`pwd` makepkg -f') + os.system( + 'mv rustdesk-%s-0-x86_64.pkg.tar.zst rustdesk-%s-manjaro-arch.pkg.tar.zst' % (version, version)) + # pacman -U ./rustdesk.pkg.tar.zst elif os.path.isfile('/usr/bin/yum'): os.system('cargo build --release --features ' + features) os.system('strip target/release/rustdesk') @@ -172,62 +228,75 @@ def main(): # yum localinstall rustdesk.rpm else: os.system('cargo bundle --release --features ' + features) - if osx: - os.system( - 'strip target/release/bundle/osx/RustDesk.app/Contents/MacOS/rustdesk') - os.system( - 'cp libsciter.dylib target/release/bundle/osx/RustDesk.app/Contents/MacOS/') - # https://github.com/sindresorhus/create-dmg - os.system('/bin/rm -rf *.dmg') - plist = "target/release/bundle/osx/RustDesk.app/Contents/Info.plist" - txt = open(plist).read() - with open(plist, "wt") as fh: - fh.write(txt.replace("", """ - LSUIElement - 1 -""")) - pa = os.environ.get('P') - if pa: - os.system(''' -# buggy: rcodesign sign ... path/*, have to sign one by one -#rcodesign sign --p12-file ~/.p12/rustdesk-developer-id.p12 --p12-password-file ~/.p12/.cert-pass --code-signature-flags runtime ./target/release/bundle/osx/RustDesk.app/Contents/MacOS/rustdesk -#rcodesign sign --p12-file ~/.p12/rustdesk-developer-id.p12 --p12-password-file ~/.p12/.cert-pass --code-signature-flags runtime ./target/release/bundle/osx/RustDesk.app/Contents/MacOS/libsciter.dylib -#rcodesign sign --p12-file ~/.p12/rustdesk-developer-id.p12 --p12-password-file ~/.p12/.cert-pass --code-signature-flags runtime ./target/release/bundle/osx/RustDesk.app -# goto "Keychain Access" -> "My Certificates" for below id which starts with "Developer ID Application:" -codesign -s "Developer ID Application: {0}" --force --options runtime ./target/release/bundle/osx/RustDesk.app/Contents/MacOS/* -codesign -s "Developer ID Application: {0}" --force --options runtime ./target/release/bundle/osx/RustDesk.app -'''.format(pa)) - os.system('create-dmg target/release/bundle/osx/RustDesk.app') - os.rename('RustDesk %s.dmg' % version, 'rustdesk-%s.dmg' % version) - if pa: - os.system(''' -#rcodesign sign --p12-file ~/.p12/rustdesk-developer-id.p12 --p12-password-file ~/.p12/.cert-pass --code-signature-flags runtime ./rustdesk-{1}.dmg -codesign -s "Developer ID Application: {0}" --force --options runtime ./rustdesk-{1}.dmg -# https://pyoxidizer.readthedocs.io/en/latest/apple_codesign_rcodesign.html -rcodesign notarize --api-issuer 69a6de7d-2907-47e3-e053-5b8c7c11a4d1 --api-key 9JBRHG3JHT --staple ./rustdesk-{1}.dmg -# verify: spctl -a -t exec -v /Applications/RustDesk.app -'''.format(pa, version)) + if flutter: + if osx: + # todo: OSX build + pass else: - print('Not signed') + os.system( + 'mv target/release/bundle/deb/rustdesk*.deb ./flutter/rustdesk.deb') + build_flutter_deb(version) else: - os.system('mv target/release/bundle/deb/rustdesk*.deb ./rustdesk.deb') - os.system('dpkg-deb -R rustdesk.deb tmpdeb') - os.system('mkdir -p tmpdeb/usr/share/rustdesk/files/systemd/') - os.system( - 'cp rustdesk.service tmpdeb/usr/share/rustdesk/files/systemd/') - os.system( - 'cp rustdesk.service.user tmpdeb/usr/share/rustdesk/files/systemd/') - os.system('cp pynput_service.py tmpdeb/usr/share/rustdesk/files/') - os.system('cp -a DEBIAN/* tmpdeb/DEBIAN/') - os.system('strip tmpdeb/usr/bin/rustdesk') - os.system('mkdir -p tmpdeb/usr/lib/rustdesk') - os.system('cp libsciter-gtk.so tmpdeb/usr/lib/rustdesk/') - md5_file('usr/share/rustdesk/files/systemd/rustdesk.service') - md5_file('usr/share/rustdesk/files/systemd/rustdesk.service.user') - md5_file('usr/share/rustdesk/files/pynput_service.py') - md5_file('usr/lib/rustdesk/libsciter-gtk.so') - os.system('dpkg-deb -b tmpdeb rustdesk.deb; /bin/rm -rf tmpdeb/') - os.rename('rustdesk.deb', 'rustdesk-%s.deb' % version) + if osx: + os.system( + 'strip target/release/bundle/osx/RustDesk.app/Contents/MacOS/rustdesk') + os.system( + 'cp libsciter.dylib target/release/bundle/osx/RustDesk.app/Contents/MacOS/') + # https://github.com/sindresorhus/create-dmg + os.system('/bin/rm -rf *.dmg') + plist = "target/release/bundle/osx/RustDesk.app/Contents/Info.plist" + txt = open(plist).read() + with open(plist, "wt") as fh: + fh.write(txt.replace("", """ + LSUIElement + 1 + """)) + pa = os.environ.get('P') + if pa: + os.system(''' + # buggy: rcodesign sign ... path/*, have to sign one by one + #rcodesign sign --p12-file ~/.p12/rustdesk-developer-id.p12 --p12-password-file ~/.p12/.cert-pass --code-signature-flags runtime ./target/release/bundle/osx/RustDesk.app/Contents/MacOS/rustdesk + #rcodesign sign --p12-file ~/.p12/rustdesk-developer-id.p12 --p12-password-file ~/.p12/.cert-pass --code-signature-flags runtime ./target/release/bundle/osx/RustDesk.app/Contents/MacOS/libsciter.dylib + #rcodesign sign --p12-file ~/.p12/rustdesk-developer-id.p12 --p12-password-file ~/.p12/.cert-pass --code-signature-flags runtime ./target/release/bundle/osx/RustDesk.app + # goto "Keychain Access" -> "My Certificates" for below id which starts with "Developer ID Application:" + codesign -s "Developer ID Application: {0}" --force --options runtime ./target/release/bundle/osx/RustDesk.app/Contents/MacOS/* + codesign -s "Developer ID Application: {0}" --force --options runtime ./target/release/bundle/osx/RustDesk.app + '''.format(pa)) + os.system('create-dmg target/release/bundle/osx/RustDesk.app') + os.rename('RustDesk %s.dmg' % + version, 'rustdesk-%s.dmg' % version) + if pa: + os.system(''' + #rcodesign sign --p12-file ~/.p12/rustdesk-developer-id.p12 --p12-password-file ~/.p12/.cert-pass --code-signature-flags runtime ./rustdesk-{1}.dmg + codesign -s "Developer ID Application: {0}" --force --options runtime ./rustdesk-{1}.dmg + # https://pyoxidizer.readthedocs.io/en/latest/apple_codesign_rcodesign.html + rcodesign notarize --api-issuer 69a6de7d-2907-47e3-e053-5b8c7c11a4d1 --api-key 9JBRHG3JHT --staple ./rustdesk-{1}.dmg + # verify: spctl -a -t exec -v /Applications/RustDesk.app + '''.format(pa, version)) + else: + print('Not signed') + else: + # buid deb package + os.system( + 'mv target/release/bundle/deb/rustdesk*.deb ./rustdesk.deb') + os.system('dpkg-deb -R rustdesk.deb tmpdeb') + os.system('mkdir -p tmpdeb/usr/share/rustdesk/files/systemd/') + os.system( + 'cp rustdesk.service tmpdeb/usr/share/rustdesk/files/systemd/') + os.system( + 'cp rustdesk.service.user tmpdeb/usr/share/rustdesk/files/systemd/') + os.system( + 'cp pynput_service.py tmpdeb/usr/share/rustdesk/files/') + os.system('cp -a DEBIAN/* tmpdeb/DEBIAN/') + os.system('strip tmpdeb/usr/bin/rustdesk') + os.system('mkdir -p tmpdeb/usr/lib/rustdesk') + os.system('cp libsciter-gtk.so tmpdeb/usr/lib/rustdesk/') + md5_file('usr/share/rustdesk/files/systemd/rustdesk.service') + md5_file('usr/share/rustdesk/files/systemd/rustdesk.service.user') + md5_file('usr/share/rustdesk/files/pynput_service.py') + md5_file('usr/lib/rustdesk/libsciter-gtk.so') + os.system('dpkg-deb -b tmpdeb rustdesk.deb; /bin/rm -rf tmpdeb/') + os.rename('rustdesk.deb', 'rustdesk-%s.deb' % version) os.system("mv Cargo.toml.bk Cargo.toml") os.system("mv src/main.rs.bk src/main.rs") diff --git a/build.rs b/build.rs index 4f7821012..860ebae77 100644 --- a/build.rs +++ b/build.rs @@ -77,15 +77,22 @@ fn install_oboe() { } fn gen_flutter_rust_bridge() { + let llvm_path = match std::env::var("LLVM_HOME") { + Ok(path) => Some(vec![path]), + Err(_) => None, + }; // Tell Cargo that if the given file changes, to rerun this build script. - println!("cargo:rerun-if-changed=src/mobile_ffi.rs"); + println!("cargo:rerun-if-changed=src/flutter_ffi.rs"); // settings for fbr_codegen let opts = lib_flutter_rust_bridge_codegen::Opts { // Path of input Rust code - rust_input: "src/mobile_ffi.rs".to_string(), + rust_input: "src/flutter_ffi.rs".to_string(), // Path of output generated Dart code dart_output: "flutter/lib/generated_bridge.dart".to_string(), + // Path of output generated C header + c_output: Some(vec!["flutter/macos/Runner/bridge_generated.h".to_string()]), // for other options lets use default + llvm_path, ..Default::default() }; // run fbr_codegen @@ -96,11 +103,11 @@ fn main() { hbb_common::gen_version(); install_oboe(); // there is problem with cfg(target_os) in build.rs, so use our workaround - let target_os = std::env::var("CARGO_CFG_TARGET_OS").unwrap(); - if target_os == "android" || target_os == "ios" { - gen_flutter_rust_bridge(); - return; - } + // let target_os = std::env::var("CARGO_CFG_TARGET_OS").unwrap(); + // if target_os == "android" || target_os == "ios" { + gen_flutter_rust_bridge(); + // return; + // } #[cfg(all(windows, feature = "with_rc"))] build_rc_source(); #[cfg(all(windows, feature = "inline"))] diff --git a/flutter/.gitignore b/flutter/.gitignore index aa592ad7a..e5db34d22 100644 --- a/flutter/.gitignore +++ b/flutter/.gitignore @@ -45,15 +45,14 @@ jniLibs # flutter rust bridge lib/generated_bridge.dart +lib/generated_bridge.freezed.dart # Flutter Generated Files -linux/flutter/generated_plugin_registrant.cc -linux/flutter/generated_plugin_registrant.h -linux/flutter/generated_plugins.cmake -macos/Flutter/GeneratedPluginRegistrant.swift -windows/flutter/generated_plugin_registrant.cc -windows/flutter/generated_plugin_registrant.h -windows/flutter/generated_plugins.cmake +**/flutter/GeneratedPluginRegistrant.swift +**/flutter/generated_plugin_registrant.cc +**/flutter/generated_plugin_registrant.h +**/flutter/generated_plugins.cmake +**/Runner/bridge_generated.h flutter_export_environment.sh Flutter-Generated.xcconfig key.jks diff --git a/flutter/.metadata b/flutter/.metadata index 107fcb7b5..8b4892cfb 100644 --- a/flutter/.metadata +++ b/flutter/.metadata @@ -1,10 +1,36 @@ # This file tracks properties of this Flutter project. # Used by Flutter tool to assess capabilities and perform upgrades etc. # -# This file should be version controlled and should not be manually edited. +# This file should be version controlled. version: - revision: 8874f21e79d7ec66d0457c7ab338348e31b17f1d + revision: ee4e09cce01d6f2d7f4baebd247fde02e5008851 channel: stable project_type: app + +# Tracks metadata for the flutter migrate command +migration: + platforms: + - platform: root + create_revision: ee4e09cce01d6f2d7f4baebd247fde02e5008851 + base_revision: ee4e09cce01d6f2d7f4baebd247fde02e5008851 + - platform: linux + create_revision: ee4e09cce01d6f2d7f4baebd247fde02e5008851 + base_revision: ee4e09cce01d6f2d7f4baebd247fde02e5008851 + - platform: macos + create_revision: ee4e09cce01d6f2d7f4baebd247fde02e5008851 + base_revision: ee4e09cce01d6f2d7f4baebd247fde02e5008851 + - platform: windows + create_revision: ee4e09cce01d6f2d7f4baebd247fde02e5008851 + base_revision: ee4e09cce01d6f2d7f4baebd247fde02e5008851 + + # User provided section + + # List of Local paths (relative to this file) that should be + # ignored by the migrate tool. + # + # Files that are not part of the templates will be ignored by default. + unmanaged_files: + - 'lib/main.dart' + - 'ios/Runner.xcodeproj/project.pbxproj' diff --git a/flutter/README.md b/flutter/README.md new file mode 100644 index 000000000..ca73a12b2 --- /dev/null +++ b/flutter/README.md @@ -0,0 +1,16 @@ +# flutter_hbb + +A new Flutter project. + +## Getting Started + +This project is a starting point for a Flutter application. + +A few resources to get you started if this is your first Flutter project: + +- [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab) +- [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook) + +For help getting started with Flutter development, view the +[online documentation](https://docs.flutter.dev/), which offers tutorials, +samples, guidance on mobile development, and a full API reference. diff --git a/flutter/analysis_options.yaml b/flutter/analysis_options.yaml new file mode 100644 index 000000000..61b6c4de1 --- /dev/null +++ b/flutter/analysis_options.yaml @@ -0,0 +1,29 @@ +# This file configures the analyzer, which statically analyzes Dart code to +# check for errors, warnings, and lints. +# +# The issues identified by the analyzer are surfaced in the UI of Dart-enabled +# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be +# invoked from the command line by running `flutter analyze`. + +# The following line activates a set of recommended lints for Flutter apps, +# packages, and plugins designed to encourage good coding practices. +include: package:flutter_lints/flutter.yaml + +linter: + # The lint rules applied to this project can be customized in the + # section below to disable rules from the `package:flutter_lints/flutter.yaml` + # included above or to enable additional rules. A list of all available lints + # and their documentation is published at + # https://dart-lang.github.io/linter/lints/index.html. + # + # Instead of disabling a lint rule for the entire project in the + # section below, it can also be suppressed for a single line of code + # or a specific dart file by using the `// ignore: name_of_lint` and + # `// ignore_for_file: name_of_lint` syntax on the line or in the file + # producing the lint. + rules: + # avoid_print: false # Uncomment to disable the `avoid_print` rule + # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule + +# Additional information about this file can be found at +# https://dart.dev/guides/language/analysis-options diff --git a/flutter/assets/logo.ico b/flutter/assets/logo.ico new file mode 100644 index 000000000..5ebc02809 Binary files /dev/null and b/flutter/assets/logo.ico differ diff --git a/flutter/assets/peer_searchbar.ttf b/flutter/assets/peer_searchbar.ttf new file mode 100644 index 000000000..7f87e48ce Binary files /dev/null and b/flutter/assets/peer_searchbar.ttf differ diff --git a/flutter/assets/tabbar.ttf b/flutter/assets/tabbar.ttf new file mode 100644 index 000000000..a9220f348 Binary files /dev/null and b/flutter/assets/tabbar.ttf differ diff --git a/flutter/lib/cm_main.dart b/flutter/lib/cm_main.dart new file mode 100644 index 000000000..bf72849e8 --- /dev/null +++ b/flutter/lib/cm_main.dart @@ -0,0 +1,30 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_hbb/consts.dart'; +import 'package:flutter_hbb/main.dart'; +import 'package:get/get.dart'; +import 'package:window_manager/window_manager.dart'; + +import 'common.dart'; +import 'desktop/pages/server_page.dart'; +import 'models/server_model.dart'; + +/// -t lib/cm_main.dart to test cm +void main(List args) async { + WidgetsFlutterBinding.ensureInitialized(); + await windowManager.ensureInitialized(); + await windowManager.setSize(Size(400, 600)); + await windowManager.setAlignment(Alignment.topRight); + await initEnv(kAppTypeMain); + gFFI.serverModel.clients + .add(Client(0, false, false, "UserA", "123123123", true, false, false)); + gFFI.serverModel.clients + .add(Client(1, false, false, "UserB", "221123123", true, false, false)); + gFFI.serverModel.clients + .add(Client(2, false, false, "UserC", "331123123", true, false, false)); + gFFI.serverModel.clients + .add(Client(3, false, false, "UserD", "441123123", true, false, false)); + runApp(GetMaterialApp( + debugShowCheckedModeBanner: false, + theme: getCurrentTheme(), + home: DesktopServerPage())); +} diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart index dbd15d436..17e45ba95 100644 --- a/flutter/lib/common.dart +++ b/flutter/lib/common.dart @@ -1,25 +1,136 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; +import 'dart:typed_data'; + +import 'package:back_button_interceptor/back_button_interceptor.dart'; +import 'package:desktop_multi_window/desktop_multi_window.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; -import 'dart:async'; -import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; +import 'package:flutter_hbb/desktop/widgets/tabbar_widget.dart'; +import 'package:flutter_hbb/models/peer_model.dart'; +import 'package:get/get.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:window_manager/window_manager.dart'; import 'models/model.dart'; +import 'models/platform_model.dart'; final globalKey = GlobalKey(); final navigationBarKey = GlobalKey(); -var isAndroid = false; -var isIOS = false; +final isAndroid = Platform.isAndroid; +final isIOS = Platform.isIOS; +final isDesktop = Platform.isWindows || Platform.isMacOS || Platform.isLinux; var isWeb = false; -var isDesktop = false; +var isWebDesktop = false; var version = ""; int androidVersion = 0; typedef F = String Function(String); typedef FMethod = String Function(String, dynamic); -class Translator { - static late F call; +late final iconKeyboard = MemoryImage(Uint8List.fromList(base64Decode( + "iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAMAAABEpIrGAAAAgVBMVEUAAAD///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////9d3yJTAAAAKnRSTlMA0Gd/0y8ILZgbJffDPUwV2nvzt+TMqZxyU7CMb1pYQyzsvKunkXE4AwJnNC24AAAA+0lEQVQ4y83O2U7DMBCF4ZMxk9rZk26kpQs7nPd/QJy4EiLbLf01N5Y/2YP/qxDFQvGB5NPC/ZpVnfJx4b5xyGfF95rkHvNCWH1u+N6J6T0sC7gqRy8uGPfBLEbozPXUjlkQKwGaFPNizwQbwkx0TDvhCii34ExZCSQVBdzIOEOyeclSHgBGXkpeygXSQgStACtWx4Z8rr8COHOvfEP/IbbsQAToFUAAV1M408IIjIGYAPoCSNRP7DQutfQTqxuAiH7UUg1FaJR2AGrrx52sK2ye28LZ0wBAEyR6y8X+NADhm1B4fgiiHXbRrTrxpwEY9RdM9wsepnvFHfUDwYEeiwAJr/gAAAAASUVORK5CYII="))); +late final iconClipboard = MemoryImage(Uint8List.fromList(base64Decode( + 'iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAMAAABEpIrGAAAAjVBMVEUAAAD///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////8DizOFAAAALnRSTlMAnIsyZy8YZF3NSAuabRL34cq6trCScyZ4qI9CQDwV+fPl2tnTwzkeB+m/pIFK/Xx0ewAAAQlJREFUOMudktduhDAQRWep69iY3tle0+7/f16Qg7MsJUQ5Dwh8jzRzhemJPIaf3GiW7eFQfOwDPp1ek/iMnKgBi5PrhJAhZAa1lCxE9pw5KWMswOMAQXuQOvqTB7tLFJ36wimKLrufZTzUaoRtdthqRA2vEwS+tR4qguiElRKk1YMrYfUQRkwLmwVBYDMvJKF8R0o3V2MOhNrfo+hXSYYjPn1L/S+n438t8gWh+q1F+cYFBMm1Jh8Ia7y2OWXQxMMRLqr2eTc1crSD84cWfEGwYM4LlaACEee2ZjsQXJxR3qmYb+GpC8ZfNM5oh3yxxbxgQE7lEkb3ZvvH1BiRHn1bu02ICcKGWr4AudUkyYxmvywAAAAASUVORK5CYII='))); +late final iconAudio = MemoryImage(Uint8List.fromList(base64Decode( + 'iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAMAAABEpIrGAAAAk1BMVEUAAAD////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////ROyVeAAAAMHRSTlMAgfz08DDqCAThvraZjEcoGA751JxzbGdfTRP25NrIpaGTcEM+HAvMuKinhXhWNx9Yzm/gAAABFUlEQVQ4y82S2XLCMAxFheMsQNghCQFalkL39vz/11V4GpNk0r629+Va1pmxPFfyh1ravOP2Y1ydJmBO0lYP3r+PyQ62s2Y7fgF6VRXOYdToT++ogIuoVhCUtX7YpwJG3F8f6V8rr3WABwwUahlEvr8y3IBniGKdKYBQ5OGQpukQakBpIVcfwptIhJcf8hWGakdndAAhBInIGHbdQGJg6jjbDUgEE5EpmB+AAM4uj6gb+AQT6wdhITLvAHJ4VCtgoAlG1tpNA0gWON/f4ioHdSADc1bfgt+PZFkDlD6ojWF+kVoaHlhvFjPHuVRrefohY1GdcFm1N8JvwEyrJ/X2Th2rIoVgIi3Fo6Xf0z5k8psKu5f/oi+nHjjI92o36AAAAABJRU5ErkJggg=='))); +late final iconFile = MemoryImage(Uint8List.fromList(base64Decode( + 'iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAMAAADVRocKAAAAUVBMVEUAAAD///////////////////////////////////////////////////////////////////////////////////////////////////////8IN+deAAAAGnRSTlMAH+CAESEN8jyZkcIb5N/ONy3vmHhmiGjUm7UwS+YAAAHZSURBVGje7dnbboMwDIBhBwgQoFAO7Ta//4NOqCAXYZQstatq4r+r5ubrgQSpg8iyC4ZURa+PlIpQYGiwrzyeHtYZjAL8T05O4H8BbbKvFgRa4NoBU8pXeYEkDDgaaLQBcwJrmeErJQB/7wes3QBWGnCIX0+AQycL1PO6BMwPa0nA4ZxbgTvOjUYMGPHRnZkQAY4mxPZBjmy53E7ukSkFKYB/D4XsWZQx64sCeYebOogGsoOBYvv6/UCb8F0IOBZ0TlP6lEYdANY350AJqB9/qPVuOI5evw4A1hgLigAlepnyxW80bcCcwN++A2s82Vcu02ta+ceq9BoL5KGTTRwQPlpqA3gCnwWU2kCDgeWRQPj2jAPCDxgCMjhI6uZnToDpvd/BJeFrJQB/fsAa02gCt3mi1wNuy8GgBNDZlysBNNSrADVSjcJl6vCpUn6jOdx0kz0q6PMhQRa4465SFKhx35cgUCBTwj2/NHwZAb71qR8GEP2H1XcmAtBPTEO67GP6FUUAIKGABbDLQ0EArhN2sAIGesRO+iyy+RMAjckVTlMCKFVAbh/4Af9OPgG61SkDVco3BQGT3GXaDAnTIAcYZDuBTwGsAGDxuBFeAQqIqwoFMlAVLrHr/wId5MPt0nilGgAAAABJRU5ErkJggg=='))); +late final iconRestart = MemoryImage(Uint8List.fromList(base64Decode( + 'iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAACXBIWXMAAB7BAAAewQHDaVRTAAAAGXRFWHRTb2Z0d2FyZQB3d3cuaW5rc2NhcGUub3Jnm+48GgAAAbhJREFUWIXVlrFqFGEUhb+7UYxaWCQKlrKKxaZSQVGDJih2tj6MD2DnMwiWvoAIRnENIpZiYxEro6IooiS7SPwsMgNLkk3mjmYmnmb45/73nMNwz/x/qH3gMu2gH6rAU+Blw+Lngau4jpmGxVF7qp1iPWjaQKnZ2WnXbuP/NqAeUPc3ZkA9XDwvqc+BVWCgPlJ7tRwUKThZce819b46VH+pfXVRXVO/q2cSul3VOgZUl0ejq86r39TXI8mqZKDuDEwCw3IREQvAbWAGmMsQZQ0sAl3gHPB1Q+0e8BuYzRDuy2yOiFVgaUxtRf0ETGc4syk4rc6PqU0Cx9j8Zf6dAeAK8Fi9sUXtFjABvEgxJlNwRP2svlNPjbw/q35U36oTFbnyMSwabxb/gB/qA3VBHagrauV7RW0DRfP1IvMlXqkXkhz1DYyQTKtHa/Z2VVMx3IiI+PI3/bCHjuOpFrSnAMpL6QfgTcMGesDx0kBr2BMzsNyi/vtQu8CJlgwsRbZDnWP90NkKaxHxJMOXMqAeAn5u0ydwMCKGY+qbkB3C2W3EKWoXk5zVoHbUZ+6Mh7tl4G4F8RJ3qvL+AfV3r5Vdpj70AAAAAElFTkSuQmCC'))); + +class IconFont { + static const _family1 = 'Tabbar'; + static const _family2 = 'PeerSearchbar'; + IconFont._(); + + static const IconData max = IconData(0xe606, fontFamily: _family1); + static const IconData restore = IconData(0xe607, fontFamily: _family1); + static const IconData close = IconData(0xe668, fontFamily: _family1); + static const IconData min = IconData(0xe609, fontFamily: _family1); + static const IconData add = IconData(0xe664, fontFamily: _family1); + static const IconData menu = IconData(0xe628, fontFamily: _family1); + static const IconData search = IconData(0xe6a4, fontFamily: _family2); + static const IconData round_close = IconData(0xe6ed, fontFamily: _family2); +} + +class ColorThemeExtension extends ThemeExtension { + const ColorThemeExtension({ + required this.bg, + required this.grayBg, + required this.text, + required this.lightText, + required this.lighterText, + required this.placeholder, + required this.border, + }); + + final Color? bg; + final Color? grayBg; + final Color? text; + final Color? lightText; + final Color? lighterText; + final Color? placeholder; + final Color? border; + + static const light = ColorThemeExtension( + bg: Color(0xFFFFFFFF), + grayBg: Color(0xFFEEEEEE), + text: Color(0xFF222222), + lightText: Color(0xFF666666), + lighterText: Color(0xFF888888), + placeholder: Color(0xFFAAAAAA), + border: Color(0xFFCCCCCC), + ); + + static const dark = ColorThemeExtension( + bg: Color(0xFF252525), + grayBg: Color(0xFF141414), + text: Color(0xFFFFFFFF), + lightText: Color(0xFF999999), + lighterText: Color(0xFF777777), + placeholder: Color(0xFF555555), + border: Color(0xFF555555), + ); + + @override + ThemeExtension copyWith( + {Color? bg, + Color? grayBg, + Color? text, + Color? lightText, + Color? lighterText, + Color? placeholder, + Color? border}) { + return ColorThemeExtension( + bg: bg ?? this.bg, + grayBg: grayBg ?? this.grayBg, + text: text ?? this.text, + lightText: lightText ?? this.lightText, + lighterText: lighterText ?? this.lighterText, + placeholder: placeholder ?? this.placeholder, + border: border ?? this.border, + ); + } + + @override + ThemeExtension lerp( + ThemeExtension? other, double t) { + if (other is! ColorThemeExtension) { + return this; + } + return ColorThemeExtension( + bg: Color.lerp(bg, other.bg, t), + grayBg: Color.lerp(grayBg, other.grayBg, t), + text: Color.lerp(text, other.text, t), + lightText: Color.lerp(lightText, other.lightText, t), + lighterText: Color.lerp(lighterText, other.lighterText, t), + placeholder: Color.lerp(placeholder, other.placeholder, t), + border: Color.lerp(border, other.border, t), + ); + } } class MyTheme { @@ -34,6 +145,48 @@ class MyTheme { static const Color border = Color(0xFFCCCCCC); static const Color idColor = Color(0xFF00B6F0); static const Color darkGray = Color(0xFFB9BABC); + static const Color cmIdColor = Color(0xFF21790B); + static const Color dark = Colors.black87; + static const Color button = Color(0xFF2C8CFF); + static const Color hoverBorder = Color(0xFF999999); + + static ThemeData lightTheme = ThemeData( + brightness: Brightness.light, + primarySwatch: Colors.blue, + visualDensity: VisualDensity.adaptivePlatformDensity, + tabBarTheme: TabBarTheme( + labelColor: Colors.black87, + ), + splashColor: Colors.transparent, + highlightColor: Colors.transparent, + ).copyWith( + extensions: >[ + ColorThemeExtension.light, + ], + ); + static ThemeData darkTheme = ThemeData( + brightness: Brightness.dark, + primarySwatch: Colors.blue, + visualDensity: VisualDensity.adaptivePlatformDensity, + tabBarTheme: TabBarTheme( + labelColor: Colors.white70, + ), + splashColor: Colors.transparent, + highlightColor: Colors.transparent, + ).copyWith( + extensions: >[ + ColorThemeExtension.dark, + ], + ); + + static ColorThemeExtension color(BuildContext context) { + return Theme.of(context).extension()!; + } +} + +bool isDarkTheme() { + final isDark = "Y" == Get.find().getString("darkTheme"); + return isDark; } final ButtonStyle flatButtonStyle = TextButton.styleFrom( @@ -44,17 +197,151 @@ final ButtonStyle flatButtonStyle = TextButton.styleFrom( ), ); -void showToast(String text, {Duration? duration}) { - SmartDialog.showToast(text, displayTime: duration); +String formatDurationToTime(Duration duration) { + var totalTime = duration.inSeconds; + final secs = totalTime % 60; + totalTime = (totalTime - secs) ~/ 60; + final mins = totalTime % 60; + totalTime = (totalTime - mins) ~/ 60; + return "${totalTime.toString().padLeft(2, "0")}:${mins.toString().padLeft(2, "0")}:${secs.toString().padLeft(2, "0")}"; } -void showLoading(String text, {bool clickMaskDismiss = false}) { - SmartDialog.dismiss(); - SmartDialog.showLoading( - clickMaskDismiss: false, - builder: (context) { - return Container( - color: MyTheme.white, +closeConnection({String? id}) { + if (isAndroid || isIOS) { + Navigator.popUntil(globalKey.currentContext!, ModalRoute.withName("/")); + } else { + final controller = Get.find(); + controller.closeBy(id); + } +} + +void window_on_top(int? id) { + if (id == null) { + // main window + windowManager.restore(); + windowManager.show(); + windowManager.focus(); + } else { + WindowController.fromWindowId(id) + ..focus() + ..show(); + } +} + +typedef DialogBuilder = CustomAlertDialog Function( + StateSetter setState, void Function([dynamic]) close); + +class Dialog { + OverlayEntry? entry; + Completer completer = Completer(); + + Dialog(); + + void complete(T? res) { + try { + if (!completer.isCompleted) { + completer.complete(res); + } + } catch (e) { + debugPrint("Dialog complete catch error: $e"); + } finally { + entry?.remove(); + } + } +} + +class OverlayDialogManager { + OverlayState? _overlayState; + Map _dialogs = Map(); + int _tagCount = 0; + + /// By default OverlayDialogManager use global overlay + OverlayDialogManager() { + _overlayState = globalKey.currentState?.overlay; + } + + void setOverlayState(OverlayState? overlayState) { + _overlayState = overlayState; + } + + void dismissAll() { + _dialogs.forEach((key, value) { + value.complete(null); + BackButtonInterceptor.removeByName(key); + }); + _dialogs.clear(); + } + + void dismissByTag(String tag) { + _dialogs[tag]?.complete(null); + _dialogs.remove(tag); + BackButtonInterceptor.removeByName(tag); + } + + Future show(DialogBuilder builder, + {bool clickMaskDismiss = false, + bool backDismiss = false, + String? tag, + bool useAnimation = true, + bool forceGlobal = false}) { + final overlayState = + forceGlobal ? globalKey.currentState?.overlay : _overlayState; + + if (overlayState == null) { + return Future.error( + "[OverlayDialogManager] Failed to show dialog, _overlayState is null, call [setOverlayState] first"); + } + + final _tag; + if (tag != null) { + _tag = tag; + } else { + _tag = _tagCount.toString(); + _tagCount++; + } + + final dialog = Dialog(); + _dialogs[_tag] = dialog; + + final close = ([res]) { + _dialogs.remove(_tag); + dialog.complete(res); + BackButtonInterceptor.removeByName(_tag); + }; + dialog.entry = OverlayEntry(builder: (_) { + bool innerClicked = false; + return Listener( + onPointerUp: (_) { + if (!innerClicked && clickMaskDismiss) { + close(); + } + innerClicked = false; + }, + child: Container( + color: Colors.black12, + child: StatefulBuilder(builder: (context, setState) { + return Listener( + onPointerUp: (_) => innerClicked = true, + child: builder(setState, close), + ); + }))); + }); + overlayState.insert(dialog.entry!); + BackButtonInterceptor.add((stopDefaultButtonEvent, routeInfo) { + if (backDismiss) { + close(); + } + return true; + }, name: _tag); + return dialog.completer.future; + } + + void showLoading(String text, + {bool clickMaskDismiss = false, + bool showCancel = true, + VoidCallback? onCancel}) { + show((setState, close) => CustomAlertDialog( + content: Container( constraints: BoxConstraints(maxWidth: 240), child: Column( mainAxisSize: MainAxisSize.min, @@ -64,74 +351,64 @@ void showLoading(String text, {bool clickMaskDismiss = false}) { Center(child: CircularProgressIndicator()), SizedBox(height: 20), Center( - child: Text(Translator.call(text), + child: Text(translate(text), style: TextStyle(fontSize: 15))), SizedBox(height: 20), - Center( - child: TextButton( - style: flatButtonStyle, - onPressed: () { - SmartDialog.dismiss(); - backToHome(); - }, - child: Text(Translator.call('Cancel'), - style: TextStyle(color: MyTheme.accent)))) - ])); - }); + Offstage( + offstage: !showCancel, + child: Center( + child: TextButton( + style: flatButtonStyle, + onPressed: () { + dismissAll(); + if (onCancel != null) { + onCancel(); + } + }, + child: Text(translate('Cancel'), + style: TextStyle(color: MyTheme.accent))))) + ])))); + } } -backToHome() { - Navigator.popUntil(globalKey.currentContext!, ModalRoute.withName("/")); -} - -typedef DialogBuilder = CustomAlertDialog Function( - StateSetter setState, void Function([dynamic]) close); - -class DialogManager { - static int _tag = 0; - - static dismissByTag(String tag, [result]) { - SmartDialog.dismiss(tag: tag, result: result); - } - - static Future show(DialogBuilder builder, - {bool clickMaskDismiss = false, - bool backDismiss = false, - String? tag, - bool useAnimation = true}) async { - final t; - if (tag != null) { - t = tag; - } else { - _tag += 1; - t = _tag.toString(); - } - SmartDialog.dismiss(status: SmartStatus.allToast); - SmartDialog.dismiss(status: SmartStatus.loading); - final close = ([res]) { - SmartDialog.dismiss(tag: t, result: res); - }; - final res = await SmartDialog.show( - tag: t, - clickMaskDismiss: clickMaskDismiss, - backDismiss: backDismiss, - useAnimation: useAnimation, - builder: (_) => StatefulBuilder( - builder: (_, setState) => builder(setState, close))); - return res; - } +void showToast(String text, {Duration timeout = const Duration(seconds: 2)}) { + final overlayState = globalKey.currentState?.overlay; + if (overlayState == null) return; + final entry = OverlayEntry(builder: (_) { + return IgnorePointer( + child: Align( + alignment: Alignment(0.0, 0.8), + child: Container( + decoration: BoxDecoration( + color: Colors.black.withOpacity(0.6), + borderRadius: BorderRadius.all( + Radius.circular(20), + ), + ), + padding: EdgeInsets.symmetric(horizontal: 20, vertical: 5), + child: Text( + text, + style: TextStyle( + decoration: TextDecoration.none, + fontWeight: FontWeight.w300, + fontSize: 18, + color: Colors.white), + ), + ))); + }); + overlayState.insert(entry); + Future.delayed(timeout, () { + entry.remove(); + }); } class CustomAlertDialog extends StatelessWidget { CustomAlertDialog( - {required this.title, - required this.content, - required this.actions, - this.contentPadding}); + {this.title, required this.content, this.actions, this.contentPadding}); - final Widget title; + final Widget? title; final Widget content; - final List actions; + final List? actions; final double? contentPadding; @override @@ -147,7 +424,9 @@ class CustomAlertDialog extends StatelessWidget { } } -void msgBox(String type, String title, String text, {bool? hasCancel}) { +void msgBox( + String type, String title, String text, OverlayDialogManager dialogManager, + {bool? hasCancel}) { var wrap = (String text, void Function() onPressed) => ButtonTheme( padding: EdgeInsets.symmetric(horizontal: 20, vertical: 10), materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, @@ -158,29 +437,43 @@ void msgBox(String type, String title, String text, {bool? hasCancel}) { child: TextButton( style: flatButtonStyle, onPressed: onPressed, - child: Text(Translator.call(text), - style: TextStyle(color: MyTheme.accent)))); + child: + Text(translate(text), style: TextStyle(color: MyTheme.accent)))); - SmartDialog.dismiss(); - final buttons = [ - wrap(Translator.call('OK'), () { - SmartDialog.dismiss(); - backToHome(); - }) - ]; + dialogManager.dismissAll(); + List buttons = []; + if (type != "connecting" && type != "success" && type.indexOf("nook") < 0) { + buttons.insert( + 0, + wrap(translate('OK'), () { + dialogManager.dismissAll(); + closeConnection(); + })); + } if (hasCancel == null) { - hasCancel = type != 'error'; + // hasCancel = type != 'error'; + hasCancel = type.indexOf("error") < 0 && + type.indexOf("nocancel") < 0 && + type != "restarting"; } if (hasCancel) { buttons.insert( 0, - wrap(Translator.call('Cancel'), () { - SmartDialog.dismiss(); + wrap(translate('Cancel'), () { + dialogManager.dismissAll(); })); } - DialogManager.show((setState, close) => CustomAlertDialog( + // TODO: test this button + if (type.indexOf("hasclose") >= 0) { + buttons.insert( + 0, + wrap(translate('Close'), () { + dialogManager.dismissAll(); + })); + } + dialogManager.show((setState, close) => CustomAlertDialog( title: Text(translate(title), style: TextStyle(fontSize: 21)), - content: Text(Translator.call(text), style: TextStyle(fontSize: 15)), + content: Text(translate(text), style: TextStyle(fontSize: 15)), actions: buttons)); } @@ -275,21 +568,28 @@ class PermissionManager { } static Future check(String type) { + if (isDesktop) { + return Future.value(true); + } if (!permissions.contains(type)) return Future.error("Wrong permission!$type"); - return FFI.invokeMethod("check_permission", type); + return gFFI.invokeMethod("check_permission", type); } static Future request(String type) { + if (isDesktop) { + return Future.value(true); + } if (!permissions.contains(type)) return Future.error("Wrong permission!$type"); - FFI.invokeMethod("request_permission", type); + gFFI.invokeMethod("request_permission", type); if (type == "ignore_battery_optimizations") { return Future.value(false); } _current = type; _completer = Completer(); + gFFI.invokeMethod("request_permission", type); // timeout _timer?.cancel(); @@ -325,3 +625,118 @@ RadioListTile getRadio( dense: true, ); } + +CheckboxListTile getToggle( + String id, void Function(void Function()) setState, option, name, + {FFI? ffi}) { + final opt = bind.sessionGetToggleOptionSync(id: id, arg: option); + return CheckboxListTile( + value: opt, + onChanged: (v) { + setState(() { + bind.sessionToggleOption(id: id, value: option); + }); + if (option == "show-quality-monitor") { + (ffi ?? gFFI).qualityMonitorModel.checkShowQualityMonitor(id); + } + }, + dense: true, + title: Text(translate(name))); +} + +/// find ffi, tag is Remote ID +/// for session specific usage +FFI ffi(String? tag) { + return Get.find(tag: tag); +} + +/// Global FFI object +late FFI _globalFFI; + +FFI get gFFI => _globalFFI; + +Future initGlobalFFI() async { + debugPrint("_globalFFI init"); + _globalFFI = FFI(); + debugPrint("_globalFFI init end"); + // after `put`, can also be globally found by Get.find(); + Get.put(_globalFFI, permanent: true); + // trigger connection status updater + await bind.mainCheckConnectStatus(); + // global shared preference + await Get.putAsync(() => SharedPreferences.getInstance()); +} + +String translate(String name) { + if (name.startsWith('Failed to') && name.contains(': ')) { + return name.split(': ').map((x) => translate(x)).join(': '); + } + return platformFFI.translate(name, localeName); +} + +bool option2bool(String option, String value) { + bool res; + if (option.startsWith("enable-")) { + res = value != "N"; + } else if (option.startsWith("allow-") || + option == "stop-service" || + option == "direct-server" || + option == "stop-rendezvous-service") { + res = value == "Y"; + } else { + assert(false); + res = value != "N"; + } + return res; +} + +String bool2option(String option, bool b) { + String res; + if (option.startsWith('enable-')) { + res = b ? '' : 'N'; + } else if (option.startsWith('allow-') || + option == "stop-service" || + option == "direct-server" || + option == "stop-rendezvous-service") { + res = b ? 'Y' : ''; + } else { + assert(false); + res = b ? 'Y' : 'N'; + } + return res; +} + +Future matchPeer(String searchText, Peer peer) async { + if (searchText.isEmpty) { + return true; + } + if (peer.id.toLowerCase().contains(searchText)) { + return true; + } + if (peer.hostname.toLowerCase().contains(searchText) || + peer.username.toLowerCase().contains(searchText)) { + return true; + } + final alias = await bind.mainGetPeerOption(id: peer.id, key: 'alias'); + if (alias.isEmpty) { + return false; + } + return alias.toLowerCase().contains(searchText); +} + +Future>? matchPeers(String searchText, List peers) async { + searchText = searchText.trim(); + if (searchText.isEmpty) { + return peers; + } + searchText = searchText.toLowerCase(); + final matches = + await Future.wait(peers.map((peer) => matchPeer(searchText, peer))); + final filteredList = List.empty(growable: true); + for (var i = 0; i < peers.length; i++) { + if (matches[i]) { + filteredList.add(peers[i]); + } + } + return filteredList; +} diff --git a/flutter/lib/common/formatter/id_formatter.dart b/flutter/lib/common/formatter/id_formatter.dart new file mode 100644 index 000000000..29aea84ff --- /dev/null +++ b/flutter/lib/common/formatter/id_formatter.dart @@ -0,0 +1,4 @@ +import 'package:flutter/material.dart'; + +/// TODO: Divide every 3 number to display ID +class IdFormController extends TextEditingController {} diff --git a/flutter/lib/consts.dart b/flutter/lib/consts.dart new file mode 100644 index 000000000..000a1cb54 --- /dev/null +++ b/flutter/lib/consts.dart @@ -0,0 +1,11 @@ +const double kDesktopRemoteTabBarHeight = 28.0; + +/// [kAppTypeMain] used by 'Desktop Main Page' , 'Mobile (Client and Server)' , 'Desktop CM Page' +const String kAppTypeMain = "main"; +const String kAppTypeDesktopRemote = "remote"; +const String kAppTypeDesktopFileTransfer = "file transfer"; +const String kTabLabelHomePage = "Home"; +const String kTabLabelSettingPage = "Settings"; + +const int kDefaultDisplayWidth = 1280; +const int kDefaultDisplayHeight = 720; diff --git a/flutter/lib/desktop/pages/connection_page.dart b/flutter/lib/desktop/pages/connection_page.dart new file mode 100644 index 000000000..29219df2a --- /dev/null +++ b/flutter/lib/desktop/pages/connection_page.dart @@ -0,0 +1,1167 @@ +import 'dart:async'; +import 'dart:convert'; + +import 'package:contextmenu/contextmenu.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_hbb/desktop/pages/desktop_home_page.dart'; +import 'package:flutter_hbb/desktop/widgets/peer_widget.dart'; +import 'package:flutter_hbb/desktop/widgets/peercard_widget.dart'; +import 'package:flutter_hbb/utils/multi_window_manager.dart'; +import 'package:get/get.dart'; +import 'package:provider/provider.dart'; +import 'package:url_launcher/url_launcher_string.dart'; + +import '../../common.dart'; +import '../../mobile/pages/scan_page.dart'; +import '../../mobile/pages/settings_page.dart'; +import '../../models/model.dart'; +import '../../models/platform_model.dart'; + +// enum RemoteType { recently, favorite, discovered, addressBook } + +/// Connection page for connecting to a remote peer. +class ConnectionPage extends StatefulWidget { + ConnectionPage({Key? key}) : super(key: key); + + @override + _ConnectionPageState createState() => _ConnectionPageState(); +} + +/// State for the connection page. +class _ConnectionPageState extends State { + /// Controller for the id input bar. + final _idController = TextEditingController(); + + /// Update url. If it's not null, means an update is available. + var _updateUrl = ''; + + Timer? _updateTimer; + + @override + void initState() { + super.initState(); + if (_idController.text.isEmpty) { + () async { + final lastRemoteId = await bind.mainGetLastRemoteId(); + if (lastRemoteId != _idController.text) { + setState(() { + _idController.text = lastRemoteId; + }); + } + }(); + } + _updateTimer = Timer.periodic(Duration(seconds: 1), (timer) { + updateStatus(); + }); + } + + @override + Widget build(BuildContext context) { + return Container( + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + mainAxisSize: MainAxisSize.max, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: Column( + children: [ + getUpdateUI(), + Row( + children: [ + getSearchBarUI(context), + ], + ).marginOnly(top: 22), + SizedBox(height: 12), + Divider(), + Expanded( + child: _PeerTabbedPage( + tabs: [ + translate('Recent Sessions'), + translate('Favorites'), + translate('Discovered'), + translate('Address Book') + ], + children: [ + RecentPeerWidget(), + FavoritePeerWidget(), + DiscoveredPeerWidget(), + FutureBuilder( + future: buildAddressBook(context), + builder: (context, snapshot) { + if (snapshot.hasData) { + return snapshot.data!; + } else { + return Offstage(); + } + }), + ], + )), + ], + ).marginSymmetric(horizontal: 22), + ), + Divider(), + SizedBox(height: 50, child: Obx(() => buildStatus())) + .paddingSymmetric(horizontal: 12.0) + ]), + ); + } + + /// Callback for the connect button. + /// Connects to the selected peer. + void onConnect({bool isFileTransfer = false}) { + var id = _idController.text.trim(); + connect(id, isFileTransfer: isFileTransfer); + } + + /// Connect to a peer with [id]. + /// If [isFileTransfer], starts a session only for file transfer. + void connect(String id, {bool isFileTransfer = false}) async { + if (id == '') return; + id = id.replaceAll(' ', ''); + if (isFileTransfer) { + await rustDeskWinManager.new_file_transfer(id); + } else { + await rustDeskWinManager.new_remote_desktop(id); + } + FocusScopeNode currentFocus = FocusScope.of(context); + if (!currentFocus.hasPrimaryFocus) { + currentFocus.unfocus(); + } + } + + /// UI for software update. + /// If [_updateUrl] is not empty, shows a button to update the software. + Widget getUpdateUI() { + return _updateUrl.isEmpty + ? SizedBox(height: 0) + : InkWell( + onTap: () async { + final url = _updateUrl + '.apk'; + if (await canLaunchUrlString(url)) { + await launchUrlString(url); + } + }, + child: Container( + alignment: AlignmentDirectional.center, + width: double.infinity, + color: Colors.pinkAccent, + padding: EdgeInsets.symmetric(vertical: 12), + child: Text(translate('Download new version'), + style: TextStyle( + color: Colors.white, fontWeight: FontWeight.bold)))); + } + + /// UI for the search bar. + /// Search for a peer and connect to it if the id exists. + Widget getSearchBarUI(BuildContext context) { + RxBool ftHover = false.obs; + RxBool ftPressed = false.obs; + RxBool connHover = false.obs; + RxBool connPressed = false.obs; + RxBool inputFocused = false.obs; + FocusNode focusNode = FocusNode(); + focusNode.addListener(() { + inputFocused.value = focusNode.hasFocus; + }); + var w = Container( + width: 320 + 20 * 2, + padding: EdgeInsets.fromLTRB(20, 24, 20, 22), + decoration: BoxDecoration( + color: MyTheme.color(context).bg, + borderRadius: const BorderRadius.all(Radius.circular(13)), + ), + child: Ink( + child: Column( + children: [ + Row( + children: [ + Text( + translate('Control Remote Desktop'), + style: TextStyle(fontSize: 19, height: 1), + ), + ], + ).marginOnly(bottom: 15), + Row( + children: [ + Expanded( + child: TextField( + autocorrect: false, + enableSuggestions: false, + keyboardType: TextInputType.visiblePassword, + style: TextStyle( + fontFamily: 'WorkSans', + fontSize: 22, + height: 1, + ), + decoration: InputDecoration( + hintText: translate('Enter Remote ID'), + hintStyle: TextStyle( + color: MyTheme.color(context).placeholder), + border: OutlineInputBorder( + borderRadius: BorderRadius.zero, + borderSide: BorderSide( + color: MyTheme.color(context).placeholder!)), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.zero, + borderSide: + BorderSide(color: MyTheme.button, width: 3), + ), + isDense: true, + contentPadding: + EdgeInsets.symmetric(horizontal: 10, vertical: 12)), + controller: _idController, + onSubmitted: (s) { + onConnect(); + }, + ), + ), + ], + ), + Padding( + padding: const EdgeInsets.only(top: 13.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Obx(() => InkWell( + onTapDown: (_) => ftPressed.value = true, + onTapUp: (_) => ftPressed.value = false, + onTapCancel: () => ftPressed.value = false, + onHover: (value) => ftHover.value = value, + onTap: () { + onConnect(isFileTransfer: true); + }, + child: Container( + height: 24, + width: 72, + alignment: Alignment.center, + decoration: BoxDecoration( + color: ftPressed.value + ? MyTheme.accent + : Colors.transparent, + border: Border.all( + color: ftPressed.value + ? MyTheme.accent + : ftHover.value + ? MyTheme.hoverBorder + : MyTheme.border, + ), + borderRadius: BorderRadius.circular(5), + ), + child: Text( + translate( + "Transfer File", + ), + style: TextStyle( + fontSize: 12, + color: ftPressed.value + ? MyTheme.color(context).bg + : MyTheme.color(context).text), + ), + ), + )), + SizedBox( + width: 17, + ), + Obx( + () => InkWell( + onTapDown: (_) => connPressed.value = true, + onTapUp: (_) => connPressed.value = false, + onTapCancel: () => connPressed.value = false, + onHover: (value) => connHover.value = value, + onTap: onConnect, + child: Container( + height: 24, + width: 65, + decoration: BoxDecoration( + color: connPressed.value + ? MyTheme.accent + : MyTheme.button, + border: Border.all( + color: connPressed.value + ? MyTheme.accent + : connHover.value + ? MyTheme.hoverBorder + : MyTheme.button, + ), + borderRadius: BorderRadius.circular(5), + ), + child: Center( + child: Text( + translate( + "Connection", + ), + style: TextStyle( + fontSize: 12, color: MyTheme.color(context).bg), + ), + ), + ), + ), + ), + ], + ), + ) + ], + ), + ), + ); + return Center( + child: Container(constraints: BoxConstraints(maxWidth: 600), child: w)); + } + + @override + void dispose() { + _idController.dispose(); + _updateTimer?.cancel(); + super.dispose(); + } + + /// Get the image for the current [platform]. + Widget getPlatformImage(String platform) { + platform = platform.toLowerCase(); + if (platform == 'mac os') + platform = 'mac'; + else if (platform != 'linux' && platform != 'android') platform = 'win'; + return Image.asset('assets/$platform.png', height: 50); + } + + bool hitTag(List selectedTags, List idents) { + if (selectedTags.isEmpty) { + return true; + } + if (idents.isEmpty) { + return false; + } + for (final tag in selectedTags) { + if (!idents.contains(tag)) { + return false; + } + } + return true; + } + + // /// Show the peer menu and handle user's choice. + // /// User might remove the peer or send a file to the peer. + // void showPeerMenu(BuildContext context, String id, RemoteType rType) async { + // var items = [ + // PopupMenuItem( + // child: Text(translate('Connect')), value: 'connect'), + // PopupMenuItem( + // child: Text(translate('Transfer File')), value: 'file'), + // PopupMenuItem( + // child: Text(translate('TCP Tunneling')), value: 'tcp-tunnel'), + // PopupMenuItem(child: Text(translate('Rename')), value: 'rename'), + // rType == RemoteType.addressBook + // ? PopupMenuItem( + // child: Text(translate('Remove')), value: 'ab-delete') + // : PopupMenuItem( + // child: Text(translate('Remove')), value: 'remove'), + // PopupMenuItem( + // child: Text(translate('Unremember Password')), + // value: 'unremember-password'), + // ]; + // if (rType == RemoteType.favorite) { + // items.add(PopupMenuItem( + // child: Text(translate('Remove from Favorites')), + // value: 'remove-fav')); + // } else if (rType != RemoteType.addressBook) { + // items.add(PopupMenuItem( + // child: Text(translate('Add to Favorites')), value: 'add-fav')); + // } else { + // items.add(PopupMenuItem( + // child: Text(translate('Edit Tag')), value: 'ab-edit-tag')); + // } + // var value = await showMenu( + // context: context, + // position: this._menuPos, + // items: items, + // elevation: 8, + // ); + // if (value == 'remove') { + // setState(() => gFFI.setByName('remove', '$id')); + // () async { + // removePreference(id); + // }(); + // } else if (value == 'file') { + // connect(id, isFileTransfer: true); + // } else if (value == 'add-fav') { + // } else if (value == 'connect') { + // connect(id, isFileTransfer: false); + // } else if (value == 'ab-delete') { + // gFFI.abModel.deletePeer(id); + // await gFFI.abModel.updateAb(); + // setState(() {}); + // } else if (value == 'ab-edit-tag') { + // abEditTag(id); + // } + // } + + var svcStopped = false.obs; + var svcStatusCode = 0.obs; + var svcIsUsingPublicServer = true.obs; + + Widget buildStatus() { + final light = Container( + height: 8, + width: 8, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(20), + color: svcStopped.value ? Colors.redAccent : Colors.green, + ), + ).paddingSymmetric(horizontal: 10.0); + if (svcStopped.value) { + return Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + light, + Text(translate("Service is not running")), + TextButton( + onPressed: () => + bind.mainSetOption(key: "stop-service", value: ""), + child: Text(translate("Start Service"))) + ], + ); + } else { + if (svcStatusCode.value == 0) { + return Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [light, Text(translate("connecting_status"))], + ); + } else if (svcStatusCode.value == -1) { + return Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [light, Text(translate("not_ready_status"))], + ); + } + } + return Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + light, + Text("${translate('Ready')}"), + svcIsUsingPublicServer.value + ? InkWell( + onTap: onUsePublicServerGuide, + child: Text( + ', ${translate('setup_server_tip')}', + style: TextStyle(decoration: TextDecoration.underline), + ), + ) + : Offstage() + ], + ); + } + + void onUsePublicServerGuide() { + final url = "https://rustdesk.com/blog/id-relay-set/"; + canLaunchUrlString(url).then((can) { + if (can) { + launchUrlString(url); + } + }); + } + + updateStatus() async { + svcStopped.value = await bind.mainGetOption(key: "stop-service") == "Y"; + final status = + jsonDecode(await bind.mainGetConnectStatus()) as Map; + svcStatusCode.value = status["status_num"]; + svcIsUsingPublicServer.value = await bind.mainIsUsingPublicServer(); + } + + handleLogin() { + loginDialog().then((success) { + if (success) { + setState(() {}); + } + }); + } + + Future buildAddressBook(BuildContext context) async { + final token = await bind.mainGetLocalOption(key: 'access_token'); + if (token.trim().isEmpty) { + return Center( + child: InkWell( + onTap: handleLogin, + child: Text( + translate("Login"), + style: TextStyle(decoration: TextDecoration.underline), + ), + ), + ); + } + final model = gFFI.abModel; + return FutureBuilder( + future: model.getAb(), + builder: (context, snapshot) { + if (snapshot.hasData) { + return _buildAddressBook(context); + } else if (snapshot.hasError) { + return Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text(translate("${snapshot.error}")), + TextButton( + onPressed: () { + setState(() {}); + }, + child: Text(translate("Retry"))) + ], + ); + } else { + if (model.abLoading) { + return Center( + child: CircularProgressIndicator(), + ); + } else if (model.abError.isNotEmpty) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text(translate("${model.abError}")), + TextButton( + onPressed: () { + setState(() {}); + }, + child: Text(translate("Retry"))) + ], + ), + ); + } else { + return Offstage(); + } + } + }); + } + + Widget _buildAddressBook(BuildContext context) { + return Row( + children: [ + Card( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(20), + side: BorderSide(color: MyTheme.grayBg)), + child: Container( + width: 200, + height: double.infinity, + padding: EdgeInsets.symmetric(horizontal: 12.0, vertical: 8.0), + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text(translate('Tags')), + InkWell( + child: PopupMenuButton( + itemBuilder: (context) => [ + PopupMenuItem( + child: Text(translate("Add ID")), + value: 'add-id', + ), + PopupMenuItem( + child: Text(translate("Add Tag")), + value: 'add-tag', + ), + PopupMenuItem( + child: Text(translate("Unselect all tags")), + value: 'unset-all-tag', + ), + ], + onSelected: handleAbOp, + child: Icon(Icons.more_vert_outlined)), + ) + ], + ), + Expanded( + child: Container( + width: double.infinity, + height: double.infinity, + decoration: BoxDecoration( + border: Border.all(color: MyTheme.darkGray)), + child: Obx( + () => Wrap( + children: gFFI.abModel.tags + .map((e) => buildTag(e, gFFI.abModel.selectedTags, + onTap: () { + // + if (gFFI.abModel.selectedTags.contains(e)) { + gFFI.abModel.selectedTags.remove(e); + } else { + gFFI.abModel.selectedTags.add(e); + } + })) + .toList(), + ), + ), + ).marginSymmetric(vertical: 8.0), + ) + ], + ), + ), + ).marginOnly(right: 8.0), + Expanded( + child: Align( + alignment: Alignment.topLeft, child: AddressBookPeerWidget()), + ) + ], + ); + } + + Widget buildTag(String tagName, RxList rxTags, {Function()? onTap}) { + return ContextMenuArea( + width: 100, + builder: (context) => [ + ListTile( + title: Text(translate("Delete")), + onTap: () { + gFFI.abModel.deleteTag(tagName); + gFFI.abModel.updateAb(); + Future.delayed(Duration.zero, () => Get.back()); + }, + ) + ], + child: GestureDetector( + onTap: onTap, + child: Obx( + () => Container( + decoration: BoxDecoration( + color: rxTags.contains(tagName) ? Colors.blue : null, + border: Border.all(color: MyTheme.darkGray), + borderRadius: BorderRadius.circular(10)), + margin: EdgeInsets.symmetric(horizontal: 4.0, vertical: 8.0), + padding: EdgeInsets.symmetric(vertical: 2.0, horizontal: 8.0), + child: Text( + tagName, + style: TextStyle( + color: rxTags.contains(tagName) ? MyTheme.white : null), + ), + ), + ), + ), + ); + } + + /// tag operation + void handleAbOp(String value) { + if (value == 'add-id') { + abAddId(); + } else if (value == 'add-tag') { + abAddTag(); + } else if (value == 'unset-all-tag') { + gFFI.abModel.unsetSelectedTags(); + } + } + + void abAddId() async { + var field = ""; + var msg = ""; + var isInProgress = false; + gFFI.dialogManager.show((setState, close) { + return CustomAlertDialog( + title: Text(translate("Add ID")), + content: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(translate("whitelist_sep")), + SizedBox( + height: 8.0, + ), + Row( + children: [ + Expanded( + child: TextField( + onChanged: (s) { + field = s; + }, + maxLines: null, + decoration: InputDecoration( + border: OutlineInputBorder(), + errorText: msg.isEmpty ? null : translate(msg), + ), + controller: TextEditingController(text: field), + ), + ), + ], + ), + SizedBox( + height: 4.0, + ), + Offstage(offstage: !isInProgress, child: LinearProgressIndicator()) + ], + ), + actions: [ + TextButton( + onPressed: () { + close(); + }, + child: Text(translate("Cancel"))), + TextButton( + onPressed: () async { + setState(() { + msg = ""; + isInProgress = true; + }); + field = field.trim(); + if (field.isEmpty) { + // pass + } else { + final ids = field.trim().split(RegExp(r"[\s,;\n]+")); + field = ids.join(','); + for (final newId in ids) { + if (gFFI.abModel.idContainBy(newId)) { + continue; + } + gFFI.abModel.addId(newId); + } + await gFFI.abModel.updateAb(); + this.setState(() {}); + // final currentPeers + } + close(); + }, + child: Text(translate("OK"))), + ], + ); + }); + } + + void abAddTag() async { + var field = ""; + var msg = ""; + var isInProgress = false; + gFFI.dialogManager.show((setState, close) { + return CustomAlertDialog( + title: Text(translate("Add Tag")), + content: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(translate("whitelist_sep")), + SizedBox( + height: 8.0, + ), + Row( + children: [ + Expanded( + child: TextField( + onChanged: (s) { + field = s; + }, + maxLines: null, + decoration: InputDecoration( + border: OutlineInputBorder(), + errorText: msg.isEmpty ? null : translate(msg), + ), + controller: TextEditingController(text: field), + ), + ), + ], + ), + SizedBox( + height: 4.0, + ), + Offstage(offstage: !isInProgress, child: LinearProgressIndicator()) + ], + ), + actions: [ + TextButton( + onPressed: () { + close(); + }, + child: Text(translate("Cancel"))), + TextButton( + onPressed: () async { + setState(() { + msg = ""; + isInProgress = true; + }); + field = field.trim(); + if (field.isEmpty) { + // pass + } else { + final tags = field.trim().split(RegExp(r"[\s,;\n]+")); + field = tags.join(','); + for (final tag in tags) { + gFFI.abModel.addTag(tag); + } + await gFFI.abModel.updateAb(); + // final currentPeers + } + close(); + }, + child: Text(translate("OK"))), + ], + ); + }); + } + + void abEditTag(String id) { + var isInProgress = false; + + final tags = List.of(gFFI.abModel.tags); + var selectedTag = gFFI.abModel.getPeerTags(id).obs; + + gFFI.dialogManager.show((setState, close) { + return CustomAlertDialog( + title: Text(translate("Edit Tag")), + content: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + padding: EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0), + child: Wrap( + children: tags + .map((e) => buildTag(e, selectedTag, onTap: () { + if (selectedTag.contains(e)) { + selectedTag.remove(e); + } else { + selectedTag.add(e); + } + })) + .toList(growable: false), + ), + ), + Offstage(offstage: !isInProgress, child: LinearProgressIndicator()) + ], + ), + actions: [ + TextButton( + onPressed: () { + close(); + }, + child: Text(translate("Cancel"))), + TextButton( + onPressed: () async { + setState(() { + isInProgress = true; + }); + gFFI.abModel.changeTagForPeer(id, selectedTag); + await gFFI.abModel.updateAb(); + close(); + }, + child: Text(translate("OK"))), + ], + ); + }); + } +} + +class WebMenu extends StatefulWidget { + @override + _WebMenuState createState() => _WebMenuState(); +} + +class _WebMenuState extends State { + String? username; + String url = ""; + + @override + void initState() { + super.initState(); + () async { + final usernameRes = await getUsername(); + final urlRes = await getUrl(); + var update = false; + if (usernameRes != username) { + username = usernameRes; + update = true; + } + if (urlRes != url) { + url = urlRes; + update = true; + } + + if (update) { + setState(() {}); + } + }(); + } + + @override + Widget build(BuildContext context) { + Provider.of(context); + return PopupMenuButton( + icon: Icon(Icons.more_vert), + itemBuilder: (context) { + return (isIOS + ? [ + PopupMenuItem( + child: Icon(Icons.qr_code_scanner, color: Colors.black), + value: "scan", + ) + ] + : >[]) + + [ + PopupMenuItem( + child: Text(translate('ID/Relay Server')), + value: "server", + ) + ] + + (url.contains('admin.rustdesk.com') + ? >[] + : [ + PopupMenuItem( + child: Text(username == null + ? translate("Login") + : translate("Logout") + ' ($username)'), + value: "login", + ) + ]) + + [ + PopupMenuItem( + child: Text(translate('About') + ' RustDesk'), + value: "about", + ) + ]; + }, + onSelected: (value) { + if (value == 'server') { + showServerSettings(gFFI.dialogManager); + } + if (value == 'about') { + showAbout(gFFI.dialogManager); + } + if (value == 'login') { + if (username == null) { + showLogin(gFFI.dialogManager); + } else { + logout(gFFI.dialogManager); + } + } + if (value == 'scan') { + Navigator.push( + context, + MaterialPageRoute( + builder: (BuildContext context) => ScanPage(), + ), + ); + } + }); + } +} + +class _PeerTabbedPage extends StatefulWidget { + final List tabs; + final List children; + const _PeerTabbedPage({required this.tabs, required this.children, Key? key}) + : super(key: key); + @override + _PeerTabbedPageState createState() => _PeerTabbedPageState(); +} + +class _PeerTabbedPageState extends State<_PeerTabbedPage> + with SingleTickerProviderStateMixin { + late PageController _pageController = PageController(); + RxInt _tabIndex = 0.obs; + + @override + void initState() { + super.initState(); + } + + // hard code for now + void _handleTabSelection(int index) { + // reset search text + peerSearchText.value = ""; + peerSearchTextController.clear(); + _tabIndex.value = index; + _pageController.jumpToPage(index); + switch (index) { + case 0: + bind.mainLoadRecentPeers(); + break; + case 1: + bind.mainLoadFavPeers(); + break; + case 2: + bind.mainDiscover(); + break; + case 3: + break; + } + } + + @override + void dispose() { + _pageController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Column( + textBaseline: TextBaseline.ideographic, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + height: 28, + child: Row( + children: [ + Expanded(child: _createTabBar(context)), + _createSearchBar(context), + _createPeerViewTypeSwitch(context), + ], + ), + ), + _createTabBarView(), + ], + ); + } + + Widget _createTabBar(BuildContext context) { + return ListView( + scrollDirection: Axis.horizontal, + shrinkWrap: true, + children: super.widget.tabs.asMap().entries.map((t) { + return Obx(() => GestureDetector( + child: Container( + padding: EdgeInsets.symmetric(horizontal: 8), + decoration: BoxDecoration( + color: _tabIndex.value == t.key + ? MyTheme.color(context).bg + : null, + borderRadius: BorderRadius.circular(2), + ), + child: Align( + alignment: Alignment.center, + child: Text( + t.value, + textAlign: TextAlign.center, + style: TextStyle( + height: 1, + fontSize: 14, + color: _tabIndex.value == t.key + ? MyTheme.color(context).text + : MyTheme.color(context).lightText), + ), + )), + onTap: () => _handleTabSelection(t.key), + )); + }).toList()); + } + + Widget _createTabBarView() { + return Expanded( + child: PageView( + controller: _pageController, children: super.widget.children) + .marginSymmetric(vertical: 12)); + } + + _createSearchBar(BuildContext context) { + RxBool focused = false.obs; + FocusNode focusNode = FocusNode(); + focusNode.addListener(() => focused.value = focusNode.hasFocus); + RxBool rowHover = false.obs; + RxBool clearHover = false.obs; + return Container( + width: 120, + height: 25, + margin: EdgeInsets.only(right: 13), + decoration: BoxDecoration(color: MyTheme.color(context).bg), + child: Obx(() => Row( + children: [ + Expanded( + child: MouseRegion( + onEnter: (_) => rowHover.value = true, + onExit: (_) => rowHover.value = false, + child: Row( + children: [ + Icon( + IconFont.search, + size: 16, + color: MyTheme.color(context).placeholder, + ).marginSymmetric(horizontal: 4), + Expanded( + child: TextField( + controller: peerSearchTextController, + onChanged: (searchText) { + peerSearchText.value = searchText; + }, + focusNode: focusNode, + textAlign: TextAlign.start, + maxLines: 1, + cursorColor: MyTheme.color(context).lightText, + cursorHeight: 18, + cursorWidth: 1, + style: TextStyle(fontSize: 14), + decoration: InputDecoration( + contentPadding: EdgeInsets.symmetric(vertical: 6), + hintText: + focused.value ? null : translate("Search ID"), + hintStyle: TextStyle( + fontSize: 14, + color: MyTheme.color(context).placeholder), + border: InputBorder.none, + isDense: true, + ), + ), + ), + ], + ), + ), + ), + Offstage( + offstage: !(peerSearchText.value.isNotEmpty && + (rowHover.value || clearHover.value)), + child: InkWell( + onHover: (value) => clearHover.value = value, + child: Icon( + IconFont.round_close, + size: 16, + color: clearHover.value + ? MyTheme.color(context).text + : MyTheme.color(context).placeholder, + ).marginSymmetric(horizontal: 4), + onTap: () { + peerSearchTextController.clear(); + peerSearchText.value = ""; + }), + ) + ], + )), + ); + } + + _createPeerViewTypeSwitch(BuildContext context) { + final activeDeco = BoxDecoration(color: MyTheme.color(context).bg); + return Row( + children: [ + Obx( + () => Container( + padding: EdgeInsets.all(4.0), + decoration: + peerCardUiType.value == PeerUiType.grid ? activeDeco : null, + child: InkWell( + onTap: () { + peerCardUiType.value = PeerUiType.grid; + }, + child: Icon( + Icons.grid_view_rounded, + size: 18, + color: peerCardUiType.value == PeerUiType.grid + ? MyTheme.color(context).text + : MyTheme.color(context).lightText, + )), + ), + ), + Obx( + () => Container( + padding: EdgeInsets.all(4.0), + decoration: + peerCardUiType.value == PeerUiType.list ? activeDeco : null, + child: InkWell( + onTap: () { + peerCardUiType.value = PeerUiType.list; + }, + child: Icon( + Icons.list, + size: 18, + color: peerCardUiType.value == PeerUiType.list + ? MyTheme.color(context).text + : MyTheme.color(context).lightText, + )), + ), + ), + ], + ); + } +} diff --git a/flutter/lib/desktop/pages/connection_tab_page.dart b/flutter/lib/desktop/pages/connection_tab_page.dart new file mode 100644 index 000000000..be7c76f2a --- /dev/null +++ b/flutter/lib/desktop/pages/connection_tab_page.dart @@ -0,0 +1,126 @@ +import 'dart:convert'; + +import 'package:desktop_multi_window/desktop_multi_window.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_hbb/common.dart'; +import 'package:flutter_hbb/consts.dart'; +import 'package:flutter_hbb/desktop/pages/remote_page.dart'; +import 'package:flutter_hbb/desktop/widgets/tabbar_widget.dart'; +import 'package:flutter_hbb/utils/multi_window_manager.dart'; +import 'package:get/get.dart'; + +import '../../models/model.dart'; + +class ConnectionTabPage extends StatefulWidget { + final Map params; + + const ConnectionTabPage({Key? key, required this.params}) : super(key: key); + + @override + State createState() => _ConnectionTabPageState(params); +} + +class _ConnectionTabPageState extends State { + final tabController = Get.put(DesktopTabController()); + static final Rx _fullscreenID = "".obs; + static final IconData selectedIcon = Icons.desktop_windows_sharp; + static final IconData unselectedIcon = Icons.desktop_windows_outlined; + + var connectionMap = RxList.empty(growable: true); + + _ConnectionTabPageState(Map params) { + if (params['id'] != null) { + tabController.add(TabInfo( + key: params['id'], + label: params['id'], + selectedIcon: selectedIcon, + unselectedIcon: unselectedIcon, + page: RemotePage( + key: ValueKey(params['id']), + id: params['id'], + tabBarHeight: + _fullscreenID.value.isNotEmpty ? 0 : kDesktopRemoteTabBarHeight, + fullscreenID: _fullscreenID, + ))); + } + } + + @override + void initState() { + super.initState(); + + tabController.onRemove = (_, id) => onRemoveId(id); + + rustDeskWinManager.setMethodHandler((call, fromWindowId) async { + print( + "call ${call.method} with args ${call.arguments} from window ${fromWindowId}"); + // for simplify, just replace connectionId + if (call.method == "new_remote_desktop") { + final args = jsonDecode(call.arguments); + final id = args['id']; + window_on_top(windowId()); + tabController.add(TabInfo( + key: id, + label: id, + selectedIcon: selectedIcon, + unselectedIcon: unselectedIcon, + page: RemotePage( + key: ValueKey(id), + id: id, + tabBarHeight: _fullscreenID.value.isNotEmpty + ? 0 + : kDesktopRemoteTabBarHeight, + fullscreenID: _fullscreenID, + ))); + } else if (call.method == "onDestroy") { + tabController.state.value.tabs.forEach((tab) { + print("executing onDestroy hook, closing ${tab.label}}"); + final tag = tab.label; + ffi(tag).close().then((_) { + Get.delete(tag: tag); + }); + }); + Get.back(); + } + }); + } + + @override + Widget build(BuildContext context) { + final theme = isDarkTheme() ? TarBarTheme.dark() : TarBarTheme.light(); + return SubWindowDragToResizeArea( + windowId: windowId(), + child: Container( + decoration: BoxDecoration( + border: Border.all(color: MyTheme.color(context).border!)), + child: Scaffold( + backgroundColor: MyTheme.color(context).bg, + body: Obx(() => DesktopTab( + controller: tabController, + theme: theme, + isMainWindow: false, + showTabBar: _fullscreenID.value.isEmpty, + tail: AddButton( + theme: theme, + ).paddingOnly(left: 10), + pageViewBuilder: (pageView) { + WindowController.fromWindowId(windowId()) + .setFullscreen(_fullscreenID.value.isNotEmpty); + return pageView; + }, + ))), + ), + ); + } + + void onRemoveId(String id) { + ffi(id).close(); + if (tabController.state.value.tabs.length == 0) { + WindowController.fromWindowId(windowId()).close(); + } + } + + int windowId() { + return widget.params["windowId"]; + } +} diff --git a/flutter/lib/desktop/pages/desktop_home_page.dart b/flutter/lib/desktop/pages/desktop_home_page.dart new file mode 100644 index 000000000..12f17c95e --- /dev/null +++ b/flutter/lib/desktop/pages/desktop_home_page.dart @@ -0,0 +1,1034 @@ +import 'dart:async'; +import 'dart:io'; + +import 'package:flutter/material.dart' hide MenuItem; +import 'package:flutter/services.dart'; +import 'package:flutter_hbb/common.dart'; +import 'package:flutter_hbb/desktop/pages/connection_page.dart'; +import 'package:flutter_hbb/desktop/pages/desktop_setting_page.dart'; +import 'package:flutter_hbb/models/platform_model.dart'; +import 'package:flutter_hbb/models/server_model.dart'; +import 'package:flutter_hbb/utils/multi_window_manager.dart'; +import 'package:get/get.dart'; +import 'package:provider/provider.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:tray_manager/tray_manager.dart'; +import 'package:url_launcher/url_launcher_string.dart'; +import 'package:window_manager/window_manager.dart'; + +class DesktopHomePage extends StatefulWidget { + DesktopHomePage({Key? key}) : super(key: key); + + @override + State createState() => _DesktopHomePageState(); +} + +const borderColor = Color(0xFF2F65BA); + +class _DesktopHomePageState extends State + with TrayListener, WindowListener, AutomaticKeepAliveClientMixin { + @override + bool get wantKeepAlive => true; + + @override + void onWindowClose() async { + super.onWindowClose(); + // close all sub windows + if (await windowManager.isPreventClose()) { + try { + await rustDeskWinManager.closeAllSubWindows(); + } catch (err) { + debugPrint("$err"); + } finally { + await windowManager.setPreventClose(false); + await windowManager.close(); + } + } + } + + @override + Widget build(BuildContext context) { + super.build(context); + return Row( + children: [ + buildServerInfo(context), + VerticalDivider( + width: 1, + thickness: 1, + ), + Expanded( + child: buildServerBoard(context), + ), + ], + ); + } + + buildServerInfo(BuildContext context) { + return ChangeNotifierProvider.value( + value: gFFI.serverModel, + child: Container( + width: 200, + color: MyTheme.color(context).bg, + child: Column( + children: [ + buildTip(context), + buildIDBoard(context), + buildPasswordBoard(context), + ], + ), + ), + ); + } + + buildServerBoard(BuildContext context) { + return Container( + color: MyTheme.color(context).grayBg, + child: ConnectionPage(), + ); + } + + buildIDBoard(BuildContext context) { + final model = gFFI.serverModel; + return Container( + margin: EdgeInsets.only(left: 20, right: 16), + height: 52, + child: Row( + crossAxisAlignment: CrossAxisAlignment.baseline, + textBaseline: TextBaseline.alphabetic, + children: [ + Container( + width: 2, + decoration: BoxDecoration(color: MyTheme.accent), + ), + Expanded( + child: Padding( + padding: const EdgeInsets.only(left: 8.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + height: 25, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + translate("ID"), + style: TextStyle( + fontSize: 14, + color: MyTheme.color(context).lightText), + ), + buildPopupMenu(context) + ], + ), + ), + Flexible( + child: GestureDetector( + onDoubleTap: () { + Clipboard.setData( + ClipboardData(text: model.serverId.text)); + showToast(translate("Copied")); + }, + child: TextFormField( + controller: model.serverId, + readOnly: true, + decoration: InputDecoration( + border: InputBorder.none, + contentPadding: EdgeInsets.only(bottom: 18), + ), + style: TextStyle( + fontSize: 22, + ), + ), + ), + ) + ], + ), + ), + ), + ], + ), + ); + } + + Widget buildPopupMenu(BuildContext context) { + var position; + RxBool hover = false.obs; + return InkWell( + onTapDown: (detail) { + final x = detail.globalPosition.dx; + final y = detail.globalPosition.dy; + position = RelativeRect.fromLTRB(x, y, x, y); + }, + onTap: () async { + final userName = await gFFI.userModel.getUserName(); + final enabledInput = await bind.mainGetOption(key: 'enable-audio'); + final defaultInput = await gFFI.getDefaultAudioInput(); + var menu = [ + await genEnablePopupMenuItem( + translate("Enable Keyboard/Mouse"), + 'enable-keyboard', + ), + await genEnablePopupMenuItem( + translate("Enable Clipboard"), + 'enable-clipboard', + ), + await genEnablePopupMenuItem( + translate("Enable File Transfer"), + 'enable-file-transfer', + ), + await genEnablePopupMenuItem( + translate("Enable TCP Tunneling"), + 'enable-tunnel', + ), + genAudioInputPopupMenuItem(enabledInput != "N", defaultInput), + PopupMenuDivider(), + PopupMenuItem( + child: Text(translate("ID/Relay Server")), + value: 'custom-server', + ), + PopupMenuItem( + child: Text(translate("IP Whitelisting")), + value: 'whitelist', + ), + PopupMenuItem( + child: Text(translate("Socks5 Proxy")), + value: 'socks5-proxy', + ), + PopupMenuDivider(), + await genEnablePopupMenuItem( + translate("Enable Service"), + 'stop-service', + ), + // TODO: direct server + await genEnablePopupMenuItem( + translate("Always connected via relay"), + 'allow-always-relay', + ), + await genEnablePopupMenuItem( + translate("Start ID/relay service"), + 'stop-rendezvous-service', + ), + PopupMenuDivider(), + userName.isEmpty + ? PopupMenuItem( + child: Text(translate("Login")), + value: 'login', + ) + : PopupMenuItem( + child: Text("${translate("Logout")} $userName"), + value: 'logout', + ), + PopupMenuItem( + child: Text(translate("Change ID")), + value: 'change-id', + ), + PopupMenuDivider(), + await genEnablePopupMenuItem( + translate("Dark Theme"), + 'allow-darktheme', + ), + PopupMenuItem( + child: Text(translate("About")), + value: 'about', + ), + ]; + final v = + await showMenu(context: context, position: position, items: menu); + if (v != null) { + onSelectMenu(v); + } + }, + child: Obx( + () => CircleAvatar( + radius: 12, + backgroundColor: hover.value + ? MyTheme.color(context).grayBg! + : MyTheme.color(context).bg!, + child: Icon( + Icons.more_vert_outlined, + size: 20, + color: hover.value + ? MyTheme.color(context).text + : MyTheme.color(context).lightText, + ), + ), + ), + onHover: (value) => hover.value = value, + ); + } + + buildPasswordBoard(BuildContext context) { + final model = gFFI.serverModel; + RxBool refreshHover = false.obs; + return Container( + margin: EdgeInsets.only(left: 20.0, right: 16, top: 13, bottom: 13), + child: Row( + crossAxisAlignment: CrossAxisAlignment.baseline, + textBaseline: TextBaseline.alphabetic, + children: [ + Container( + width: 2, + height: 52, + decoration: BoxDecoration(color: MyTheme.accent), + ), + Expanded( + child: Padding( + padding: const EdgeInsets.only(left: 8.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + translate("Password"), + style: TextStyle( + fontSize: 14, color: MyTheme.color(context).lightText), + ), + Row( + children: [ + Expanded( + child: GestureDetector( + onDoubleTap: () { + if (model.verificationMethod != + kUsePermanentPassword) { + Clipboard.setData( + ClipboardData(text: model.serverPasswd.text)); + showToast(translate("Copied")); + } + }, + child: TextFormField( + controller: model.serverPasswd, + readOnly: true, + decoration: InputDecoration( + border: InputBorder.none, + contentPadding: EdgeInsets.only(bottom: 8), + ), + style: TextStyle(fontSize: 15), + ), + ), + ), + InkWell( + child: Obx( + () => Icon( + Icons.refresh, + color: refreshHover.value + ? MyTheme.color(context).text + : Color(0xFFDDDDDD), + size: 22, + ).marginOnly(right: 10, bottom: 8), + ), + onTap: () => bind.mainUpdateTemporaryPassword(), + onHover: (value) => refreshHover.value = value, + ), + FutureBuilder( + future: buildPasswordPopupMenu(context), + builder: (context, snapshot) { + if (snapshot.hasError) { + print("${snapshot.error}"); + } + if (snapshot.hasData) { + return snapshot.data!; + } else { + return Offstage(); + } + }) + ], + ), + ], + ), + ), + ), + ], + ), + ); + } + + Future buildPasswordPopupMenu(BuildContext context) async { + var position; + RxBool editHover = false.obs; + return InkWell( + onTapDown: (detail) { + final x = detail.globalPosition.dx; + final y = detail.globalPosition.dy; + position = RelativeRect.fromLTRB(x, y, x, y); + }, + onTap: () async { + var method = (String text, String value) => PopupMenuItem( + child: Row( + children: [ + Offstage( + offstage: gFFI.serverModel.verificationMethod != value, + child: Icon(Icons.check)), + Text( + text, + ), + ], + ), + onTap: () => gFFI.serverModel.verificationMethod = value, + ); + final temporary_enabled = + gFFI.serverModel.verificationMethod != kUsePermanentPassword; + var menu = [ + method(translate("Use temporary password"), kUseTemporaryPassword), + method(translate("Use permanent password"), kUsePermanentPassword), + method(translate("Use both passwords"), kUseBothPasswords), + PopupMenuDivider(), + PopupMenuItem( + child: Text(translate("Set permanent password")), + value: 'set-permanent-password', + enabled: gFFI.serverModel.verificationMethod != + kUseTemporaryPassword), + PopupMenuItem( + child: PopupMenuButton( + padding: EdgeInsets.zero, + child: Text( + translate("Set temporary password length"), + ), + itemBuilder: (context) => ["6", "8", "10"] + .map((e) => PopupMenuItem( + child: Row( + children: [ + Offstage( + offstage: gFFI.serverModel + .temporaryPasswordLength != + e, + child: Icon(Icons.check)), + Text( + e, + ), + ], + ), + onTap: () { + if (gFFI.serverModel.temporaryPasswordLength != + e) { + gFFI.serverModel.temporaryPasswordLength = e; + bind.mainUpdateTemporaryPassword(); + } + }, + )) + .toList(), + enabled: temporary_enabled, + ), + enabled: temporary_enabled), + ]; + final v = + await showMenu(context: context, position: position, items: menu); + if (v == "set-permanent-password") { + setPasswordDialog(); + } + }, + onHover: (value) => editHover.value = value, + child: Obx(() => Icon(Icons.edit, + size: 22, + color: editHover.value + ? MyTheme.color(context).text + : Color(0xFFDDDDDD)) + .marginOnly(bottom: 8))); + } + + buildTip(BuildContext context) { + return Padding( + padding: + const EdgeInsets.only(left: 20.0, right: 16, top: 16.0, bottom: 14), + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + translate("Your Desktop"), + style: TextStyle(fontWeight: FontWeight.normal, fontSize: 19), + ), + SizedBox( + height: 10.0, + ), + Text( + translate("desk_tip"), + overflow: TextOverflow.clip, + style: TextStyle( + fontSize: 12, + color: MyTheme.color(context).lighterText, + height: 1.25), + ) + ], + ), + ); + } + + @override + void onTrayMenuItemClick(MenuItem menuItem) { + print("click ${menuItem.key}"); + switch (menuItem.key) { + case "quit": + exit(0); + case "show": + // windowManager.show(); + break; + default: + break; + } + } + + @override + void initState() { + super.initState(); + trayManager.addListener(this); + windowManager.addListener(this); + rustDeskWinManager.setMethodHandler((call, fromWindowId) async { + print( + "call ${call.method} with args ${call.arguments} from window ${fromWindowId}"); + if (call.method == "main_window_on_top") { + window_on_top(null); + } + }); + } + + @override + void dispose() { + trayManager.removeListener(this); + windowManager.removeListener(this); + super.dispose(); + } + + void changeTheme(String choice) async { + if (choice == "Y") { + Get.changeTheme(MyTheme.darkTheme); + } else { + Get.changeTheme(MyTheme.lightTheme); + } + Get.find().setString("darkTheme", choice); + Get.forceAppUpdate(); + } + + void onSelectMenu(String key) async { + if (key.startsWith('enable-')) { + final option = await bind.mainGetOption(key: key); + bind.mainSetOption(key: key, value: option == "N" ? "" : "N"); + } else if (key.startsWith('allow-')) { + final option = await bind.mainGetOption(key: key); + final choice = option == "Y" ? "" : "Y"; + bind.mainSetOption(key: key, value: choice); + if (key == "allow-darktheme") changeTheme(choice); + } else if (key == "stop-service") { + final option = await bind.mainGetOption(key: key); + bind.mainSetOption(key: key, value: option == "Y" ? "" : "Y"); + } else if (key == "change-id") { + changeId(); + } else if (key == "custom-server") { + changeServer(); + } else if (key == "whitelist") { + changeWhiteList(); + } else if (key == "socks5-proxy") { + changeSocks5Proxy(); + } else if (key == "about") { + about(); + } else if (key == "logout") { + logOut(); + } else if (key == "login") { + login(); + } + } + + Future> genEnablePopupMenuItem( + String label, String key) async { + final v = await bind.mainGetOption(key: key); + bool enable; + if (key == "stop-service") { + enable = v != "Y"; + } else if (key.startsWith("allow-")) { + enable = v == "Y"; + } else { + enable = v != "N"; + } + + return PopupMenuItem( + child: Row( + children: [ + Icon(Icons.check, + color: enable ? null : MyTheme.accent.withAlpha(00)), + Text( + label, + style: genTextStyle(enable), + ), + ], + ), + value: key, + ); + } + + TextStyle genTextStyle(bool isPositive) { + return isPositive + ? TextStyle() + : TextStyle( + color: Colors.redAccent, decoration: TextDecoration.lineThrough); + } + + PopupMenuItem genAudioInputPopupMenuItem( + bool enableInput, String defaultAudioInput) { + final defaultInput = defaultAudioInput.obs; + final enabled = enableInput.obs; + + return PopupMenuItem( + child: FutureBuilder>( + future: gFFI.getAudioInputs(), + builder: (context, snapshot) { + if (snapshot.hasData) { + final inputs = snapshot.data!.toList(); + if (Platform.isWindows) { + inputs.insert(0, translate("System Sound")); + } + var inputList = inputs + .map((e) => PopupMenuItem( + child: Row( + children: [ + Obx(() => Offstage( + offstage: defaultInput.value != e, + child: Icon(Icons.check))), + Expanded( + child: Tooltip( + message: e, + child: Text( + "$e", + maxLines: 1, + overflow: TextOverflow.ellipsis, + ))), + ], + ), + value: e, + )) + .toList(); + inputList.insert( + 0, + PopupMenuItem( + child: Row( + children: [ + Obx(() => Offstage( + offstage: enabled.value, child: Icon(Icons.check))), + Expanded(child: Text(translate("Mute"))), + ], + ), + value: "Mute", + )); + return PopupMenuButton( + padding: EdgeInsets.zero, + child: Container( + alignment: Alignment.centerLeft, + child: Text(translate("Audio Input"))), + itemBuilder: (context) => inputList, + onSelected: (dev) async { + if (dev == "Mute") { + await bind.mainSetOption( + key: 'enable-audio', value: enabled.value ? '' : 'N'); + enabled.value = + await bind.mainGetOption(key: 'enable-audio') != 'N'; + } else if (dev != await gFFI.getDefaultAudioInput()) { + gFFI.setDefaultAudioInput(dev); + defaultInput.value = dev; + } + }, + ); + } else { + return Text("..."); + } + }, + ), + value: 'audio-input', + ); + } + + /// change local ID + void changeId() { + var newId = ""; + var msg = ""; + var isInProgress = false; + gFFI.dialogManager.show((setState, close) { + return CustomAlertDialog( + title: Text(translate("Change ID")), + content: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(translate("id_change_tip")), + SizedBox( + height: 8.0, + ), + Row( + children: [ + Text("ID:").marginOnly(bottom: 16.0), + SizedBox( + width: 24.0, + ), + Expanded( + child: TextField( + onChanged: (s) { + newId = s; + }, + decoration: InputDecoration( + border: OutlineInputBorder(), + errorText: msg.isEmpty ? null : translate(msg)), + inputFormatters: [ + LengthLimitingTextInputFormatter(16), + // FilteringTextInputFormatter(RegExp(r"[a-zA-z][a-zA-z0-9\_]*"), allow: true) + ], + maxLength: 16, + ), + ), + ], + ), + SizedBox( + height: 4.0, + ), + Offstage(offstage: !isInProgress, child: LinearProgressIndicator()) + ], + ), + actions: [ + TextButton( + onPressed: () { + close(); + }, + child: Text(translate("Cancel"))), + TextButton( + onPressed: () async { + setState(() { + msg = ""; + isInProgress = true; + bind.mainChangeId(newId: newId); + }); + + var status = await bind.mainGetAsyncStatus(); + while (status == " ") { + await Future.delayed(Duration(milliseconds: 100)); + status = await bind.mainGetAsyncStatus(); + } + if (status.isEmpty) { + // ok + close(); + return; + } + setState(() { + isInProgress = false; + msg = translate(status); + }); + }, + child: Text(translate("OK"))), + ], + ); + }); + } + + void about() async { + final appName = await bind.mainGetAppName(); + final license = await bind.mainGetLicense(); + final version = await bind.mainGetVersion(); + final linkStyle = TextStyle(decoration: TextDecoration.underline); + gFFI.dialogManager.show((setState, close) { + return CustomAlertDialog( + title: Text("About $appName"), + content: ConstrainedBox( + constraints: BoxConstraints(minWidth: 500), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + height: 8.0, + ), + Text("Version: $version").marginSymmetric(vertical: 4.0), + InkWell( + onTap: () { + launchUrlString("https://rustdesk.com/privacy"); + }, + child: Text( + "Privacy Statement", + style: linkStyle, + ).marginSymmetric(vertical: 4.0)), + InkWell( + onTap: () { + launchUrlString("https://rustdesk.com"); + }, + child: Text( + "Website", + style: linkStyle, + ).marginSymmetric(vertical: 4.0)), + Container( + decoration: BoxDecoration(color: Color(0xFF2c8cff)), + padding: EdgeInsets.symmetric(vertical: 24, horizontal: 8), + child: Row( + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Copyright © 2022 Purslane Ltd.\n$license", + style: TextStyle(color: Colors.white), + ), + Text( + "Made with heart in this chaotic world!", + style: TextStyle( + fontWeight: FontWeight.w800, + color: Colors.white), + ) + ], + ), + ), + ], + ), + ).marginSymmetric(vertical: 4.0) + ], + ), + ), + actions: [ + TextButton( + onPressed: () async { + close(); + }, + child: Text(translate("OK"))), + ], + ); + }); + } + + void login() { + loginDialog().then((success) { + if (success) { + // refresh frame + setState(() {}); + } + }); + } + + void logOut() { + gFFI.userModel.logOut().then((_) => {setState(() {})}); + } +} + +/// common login dialog for desktop +/// call this directly +Future loginDialog() async { + String userName = ""; + var userNameMsg = ""; + String pass = ""; + var passMsg = ""; + + var isInProgress = false; + var completer = Completer(); + gFFI.dialogManager.show((setState, close) { + return CustomAlertDialog( + title: Text(translate("Login")), + content: ConstrainedBox( + constraints: BoxConstraints(minWidth: 500), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + height: 8.0, + ), + Row( + children: [ + ConstrainedBox( + constraints: BoxConstraints(minWidth: 100), + child: Text( + "${translate('Username')}:", + textAlign: TextAlign.start, + ).marginOnly(bottom: 16.0)), + SizedBox( + width: 24.0, + ), + Expanded( + child: TextField( + onChanged: (s) { + userName = s; + }, + decoration: InputDecoration( + border: OutlineInputBorder(), + errorText: userNameMsg.isNotEmpty ? userNameMsg : null), + controller: TextEditingController(text: userName), + ), + ), + ], + ), + SizedBox( + height: 8.0, + ), + Row( + children: [ + ConstrainedBox( + constraints: BoxConstraints(minWidth: 100), + child: Text("${translate('Password')}:") + .marginOnly(bottom: 16.0)), + SizedBox( + width: 24.0, + ), + Expanded( + child: TextField( + obscureText: true, + onChanged: (s) { + pass = s; + }, + decoration: InputDecoration( + border: OutlineInputBorder(), + errorText: passMsg.isNotEmpty ? passMsg : null), + controller: TextEditingController(text: pass), + ), + ), + ], + ), + SizedBox( + height: 4.0, + ), + Offstage(offstage: !isInProgress, child: LinearProgressIndicator()) + ], + ), + ), + actions: [ + TextButton( + onPressed: () { + completer.complete(false); + close(); + }, + child: Text(translate("Cancel"))), + TextButton( + onPressed: () async { + setState(() { + userNameMsg = ""; + passMsg = ""; + isInProgress = true; + }); + final cancel = () { + setState(() { + isInProgress = false; + }); + }; + userName = userName; + pass = pass; + if (userName.isEmpty) { + userNameMsg = translate("Username missed"); + cancel(); + return; + } + if (pass.isEmpty) { + passMsg = translate("Password missed"); + cancel(); + return; + } + try { + final resp = await gFFI.userModel.login(userName, pass); + if (resp.containsKey('error')) { + passMsg = resp['error']; + cancel(); + return; + } + // {access_token: eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJndWlkIjoiMDFkZjQ2ZjgtZjg3OS00MDE0LTk5Y2QtMGMwYzM2MmViZGJlIiwiZXhwIjoxNjYxNDg2NzYwfQ.GZpe1oI8TfM5yTYNrpcwbI599P4Z_-b2GmnwNl2Lr-w, + // token_type: Bearer, user: {id: , name: admin, email: null, note: null, status: null, grp: null, is_admin: true}} + debugPrint("$resp"); + completer.complete(true); + } catch (err) { + print(err.toString()); + cancel(); + return; + } + close(); + }, + child: Text(translate("OK"))), + ], + ); + }); + return completer.future; +} + +void setPasswordDialog() async { + final pw = await bind.mainGetPermanentPassword(); + final p0 = TextEditingController(text: pw); + final p1 = TextEditingController(text: pw); + var errMsg0 = ""; + var errMsg1 = ""; + + gFFI.dialogManager.show((setState, close) { + return CustomAlertDialog( + title: Text(translate("Set Password")), + content: ConstrainedBox( + constraints: BoxConstraints(minWidth: 500), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + height: 8.0, + ), + Row( + children: [ + ConstrainedBox( + constraints: BoxConstraints(minWidth: 100), + child: Text( + "${translate('Password')}:", + textAlign: TextAlign.start, + ).marginOnly(bottom: 16.0)), + SizedBox( + width: 24.0, + ), + Expanded( + child: TextField( + obscureText: true, + decoration: InputDecoration( + border: OutlineInputBorder(), + errorText: errMsg0.isNotEmpty ? errMsg0 : null), + controller: p0, + ), + ), + ], + ), + SizedBox( + height: 8.0, + ), + Row( + children: [ + ConstrainedBox( + constraints: BoxConstraints(minWidth: 100), + child: Text("${translate('Confirmation')}:") + .marginOnly(bottom: 16.0)), + SizedBox( + width: 24.0, + ), + Expanded( + child: TextField( + obscureText: true, + decoration: InputDecoration( + border: OutlineInputBorder(), + errorText: errMsg1.isNotEmpty ? errMsg1 : null), + controller: p1, + ), + ), + ], + ), + ], + ), + ), + actions: [ + TextButton( + onPressed: () { + close(); + }, + child: Text(translate("Cancel"))), + TextButton( + onPressed: () { + setState(() { + errMsg0 = ""; + errMsg1 = ""; + }); + final pass = p0.text.trim(); + if (pass.length < 6) { + setState(() { + errMsg0 = translate("Too short, at least 6 characters."); + }); + return; + } + if (p1.text.trim() != pass) { + setState(() { + errMsg1 = translate("The confirmation is not identical."); + }); + return; + } + bind.mainSetPermanentPassword(password: pass); + close(); + }, + child: Text(translate("OK"))), + ], + ); + }); +} diff --git a/flutter/lib/desktop/pages/desktop_setting_page.dart b/flutter/lib/desktop/pages/desktop_setting_page.dart new file mode 100644 index 000000000..4f86974f1 --- /dev/null +++ b/flutter/lib/desktop/pages/desktop_setting_page.dart @@ -0,0 +1,1455 @@ +import 'dart:convert'; +import 'dart:ui'; + +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_hbb/common.dart'; +import 'package:flutter_hbb/desktop/pages/desktop_home_page.dart'; +import 'package:flutter_hbb/models/platform_model.dart'; +import 'package:flutter_hbb/models/server_model.dart'; +import 'package:get/get.dart'; +import 'package:provider/provider.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:url_launcher/url_launcher_string.dart'; + +const double _kTabWidth = 235; +const double _kTabHeight = 42; +const double _kCardFixedWidth = 560; +const double _kCardLeftMargin = 15; +const double _kContentHMargin = 15; +const double _kContentHSubMargin = _kContentHMargin + 33; +const double _kCheckBoxLeftMargin = 10; +const double _kRadioLeftMargin = 10; +const double _kListViewBottomMargin = 15; +const double _kTitleFontSize = 20; +const double _kContentFontSize = 15; +const Color _accentColor = MyTheme.accent; + +class _TabInfo { + late final String label; + late final IconData unselected; + late final IconData selected; + _TabInfo(this.label, this.unselected, this.selected); +} + +class DesktopSettingPage extends StatefulWidget { + DesktopSettingPage({Key? key}) : super(key: key); + + @override + State createState() => _DesktopSettingPageState(); +} + +class _DesktopSettingPageState extends State + with TickerProviderStateMixin, AutomaticKeepAliveClientMixin { + final List<_TabInfo> _setting_tabs = <_TabInfo>[ + _TabInfo('User Interface', Icons.language_outlined, Icons.language_sharp), + _TabInfo('Security', Icons.enhanced_encryption_outlined, + Icons.enhanced_encryption_sharp), + _TabInfo( + 'Display', Icons.desktop_windows_outlined, Icons.desktop_windows_sharp), + _TabInfo('Audio', Icons.volume_up_outlined, Icons.volume_up_sharp), + _TabInfo('Connection', Icons.link_outlined, Icons.link_sharp), + _TabInfo('About RustDesk', Icons.info_outline, Icons.info_sharp) + ]; + + late PageController controller; + RxInt _selectedIndex = 0.obs; + + @override + bool get wantKeepAlive => true; + + @override + void initState() { + super.initState(); + controller = PageController(); + } + + @override + Widget build(BuildContext context) { + super.build(context); + return Scaffold( + backgroundColor: MyTheme.color(context).bg, + body: Row( + children: [ + Container( + width: _kTabWidth, + child: Column( + children: [ + _header(), + Flexible(child: _listView(tabs: _setting_tabs)), + ], + ), + ), + const VerticalDivider(thickness: 1, width: 1), + Expanded( + child: Container( + color: MyTheme.color(context).grayBg, + child: PageView( + controller: controller, + children: [ + _UserInterface(), + _Safety(), + _Display(), + _Audio(), + _Connection(), + _About(), + ], + ), + ), + ) + ], + ), + ); + } + + Widget _header() { + return Row( + children: [ + SizedBox( + height: 62, + child: Text( + translate('Settings'), + textAlign: TextAlign.left, + style: TextStyle( + color: _accentColor, + fontSize: _kTitleFontSize, + fontWeight: FontWeight.w400, + ), + ), + ).marginOnly(left: 20, top: 10), + Spacer(), + ], + ); + } + + Widget _listView({required List<_TabInfo> tabs}) { + return ListView( + children: tabs + .asMap() + .entries + .map((tab) => _listItem(tab: tab.value, index: tab.key)) + .toList(), + ); + } + + Widget _listItem({required _TabInfo tab, required int index}) { + return Obx(() { + bool selected = index == _selectedIndex.value; + return Container( + width: _kTabWidth, + height: _kTabHeight, + child: InkWell( + onTap: () { + if (_selectedIndex.value != index) { + controller.jumpToPage(index); + } + _selectedIndex.value = index; + }, + child: Row(children: [ + Container( + width: 4, + height: _kTabHeight * 0.7, + color: selected ? _accentColor : null, + ), + Icon( + selected ? tab.selected : tab.unselected, + color: selected ? _accentColor : null, + size: 20, + ).marginOnly(left: 13, right: 10), + Text( + translate(tab.label), + style: TextStyle( + color: selected ? _accentColor : null, + fontWeight: FontWeight.w400, + fontSize: _kContentFontSize), + ), + ]), + ), + ); + }); + } +} + +//#region pages + +class _UserInterface extends StatefulWidget { + _UserInterface({Key? key}) : super(key: key); + + @override + State<_UserInterface> createState() => _UserInterfaceState(); +} + +class _UserInterfaceState extends State<_UserInterface> + with AutomaticKeepAliveClientMixin { + @override + bool get wantKeepAlive => true; + + @override + Widget build(BuildContext context) { + super.build(context); + return ListView( + children: [ + _Card(title: 'Language', children: [language()]), + _Card(title: 'Theme', children: [theme()]), + ], + ).marginOnly(bottom: _kListViewBottomMargin); + } + + Widget language() { + return _futureBuilder(future: () async { + String langs = await bind.mainGetLangs(); + String lang = await bind.mainGetLocalOption(key: "lang"); + return {"langs": langs, "lang": lang}; + }(), hasData: (res) { + Map data = res as Map; + List langsList = jsonDecode(data["langs"]!); + Map langsMap = {for (var v in langsList) v[0]: v[1]}; + List keys = langsMap.keys.toList(); + List values = langsMap.values.toList(); + keys.insert(0, "default"); + values.insert(0, "Default"); + String currentKey = data["lang"]!; + if (!keys.contains(currentKey)) { + currentKey = "default"; + } + return _ComboBox( + keys: keys, + values: values, + initialKey: currentKey, + onChanged: (key) async { + await bind.mainSetLocalOption(key: "lang", value: key); + Get.forceAppUpdate(); + }, + ).marginOnly(left: _kContentHMargin); + }); + } + + Widget theme() { + var change = () { + bool dark = !isDarkTheme(); + Get.changeTheme(dark ? MyTheme.darkTheme : MyTheme.lightTheme); + Get.find().setString("darkTheme", dark ? "Y" : ""); + Get.forceAppUpdate(); + }; + + return GestureDetector( + child: Row( + children: [ + Checkbox(value: isDarkTheme(), onChanged: (_) => change()), + Expanded(child: Text(translate('Dark Theme'))), + ], + ).marginOnly(left: _kCheckBoxLeftMargin), + onTap: change, + ); + } +} + +class _Safety extends StatefulWidget { + const _Safety({Key? key}) : super(key: key); + + @override + State<_Safety> createState() => _SafetyState(); +} + +class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin { + @override + bool get wantKeepAlive => true; + bool locked = true; + + @override + Widget build(BuildContext context) { + super.build(context); + return ListView( + children: [ + Column( + children: [ + _lock(locked, 'Unlock Security Settings', () { + locked = false; + setState(() => {}); + }), + AbsorbPointer( + absorbing: locked, + child: Column(children: [ + permissions(context), + password(context), + whitelist(), + ]), + ), + ], + ) + ], + ).marginOnly(bottom: _kListViewBottomMargin); + } + + Widget permissions(context) { + bool enabled = !locked; + return _Card(title: 'Permissions', children: [ + _OptionCheckBox(context, 'Enable Keyboard/Mouse', 'enable-keyboard', + enabled: enabled), + _OptionCheckBox(context, 'Enable Clipboard', 'enable-clipboard', + enabled: enabled), + _OptionCheckBox(context, 'Enable File Transfer', 'enable-file-transfer', + enabled: enabled), + _OptionCheckBox(context, 'Enable Audio', 'enable-audio', + enabled: enabled), + _OptionCheckBox(context, 'Enable Remote Restart', 'enable-remote-restart', + enabled: enabled), + _OptionCheckBox(context, 'Enable remote configuration modification', + 'allow-remote-config-modification', + enabled: enabled), + ]); + } + + Widget password(BuildContext context) { + return ChangeNotifierProvider.value( + value: gFFI.serverModel, + child: Consumer(builder: ((context, model, child) { + List keys = [ + kUseTemporaryPassword, + kUsePermanentPassword, + kUseBothPasswords, + ]; + List values = [ + translate("Use temporary password"), + translate("Use permanent password"), + translate("Use both passwords"), + ]; + bool tmp_enabled = model.verificationMethod != kUsePermanentPassword; + bool perm_enabled = model.verificationMethod != kUseTemporaryPassword; + String currentValue = values[keys.indexOf(model.verificationMethod)]; + List radios = values + .map((value) => _Radio( + context, + value: value, + groupValue: currentValue, + label: value, + onChanged: ((value) { + model.verificationMethod = keys[values.indexOf(value)]; + }), + enabled: !locked, + )) + .toList(); + + var onChanged = tmp_enabled && !locked + ? (value) { + if (value != null) + model.temporaryPasswordLength = value.toString(); + } + : null; + List lengthRadios = ['6', '8', '10'] + .map((value) => GestureDetector( + child: Row( + children: [ + Radio( + value: value, + groupValue: model.temporaryPasswordLength, + onChanged: onChanged), + Text( + value, + style: TextStyle( + color: _disabledTextColor( + context, onChanged != null)), + ), + ], + ).paddingSymmetric(horizontal: 10), + onTap: () => onChanged?.call(value), + )) + .toList(); + + return _Card(title: 'Password', children: [ + radios[0], + _SubLabeledWidget( + 'Temporary Password Length', + Row( + children: [ + ...lengthRadios, + ], + ), + enabled: tmp_enabled && !locked), + radios[1], + _SubButton('Set permanent password', setPasswordDialog, + perm_enabled && !locked), + radios[2], + ]); + }))); + } + + Widget whitelist() { + return _Card(title: 'IP Whitelisting', children: [ + _Button('IP Whitelisting', changeWhiteList, + tip: 'whitelist_tip', enabled: !locked) + ]); + } +} + +class _Connection extends StatefulWidget { + const _Connection({Key? key}) : super(key: key); + + @override + State<_Connection> createState() => _ConnectionState(); +} + +class _ConnectionState extends State<_Connection> + with AutomaticKeepAliveClientMixin { + @override + bool get wantKeepAlive => true; + bool locked = true; + + @override + Widget build(BuildContext context) { + super.build(context); + bool enabled = !locked; + return ListView(children: [ + Column( + children: [ + _lock(locked, 'Unlock Connection Settings', () { + locked = false; + setState(() => {}); + }), + AbsorbPointer( + absorbing: locked, + child: Column(children: [ + _Card(title: 'Server', children: [ + _Button('ID/Relay Server', changeServer, enabled: enabled), + ]), + _Card(title: 'Service', children: [ + _OptionCheckBox(context, 'Enable Service', 'stop-service', + reverse: true, enabled: enabled), + // TODO: Not implemented + // _option_check('Always connected via relay', 'allow-always-relay', enabled: enabled), + // _option_check('Start ID/relay service', 'stop-rendezvous-service', + // reverse: true, enabled: enabled), + ]), + _Card(title: 'TCP Tunneling', children: [ + _OptionCheckBox( + context, 'Enable TCP Tunneling', 'enable-tunnel', + enabled: enabled), + ]), + direct_ip(context), + _Card(title: 'Proxy', children: [ + _Button('Socks5 Proxy', changeSocks5Proxy, enabled: enabled), + ]), + ]), + ), + ], + ) + ]).marginOnly(bottom: _kListViewBottomMargin); + } + + Widget direct_ip(BuildContext context) { + TextEditingController controller = TextEditingController(); + var update = () => setState(() {}); + RxBool apply_enabled = false.obs; + return _Card(title: 'Direct IP Access', children: [ + _OptionCheckBox(context, 'Enable Direct IP Access', 'direct-server', + update: update, enabled: !locked), + _futureBuilder( + future: () async { + String enabled = await bind.mainGetOption(key: 'direct-server'); + String port = await bind.mainGetOption(key: 'direct-access-port'); + return {'enabled': enabled, 'port': port}; + }(), + hasData: (data) { + bool enabled = + option2bool('direct-server', data['enabled'].toString()); + if (!enabled) apply_enabled.value = false; + controller.text = data['port'].toString(); + return Row(children: [ + _SubLabeledWidget( + 'Port', + Container( + width: 80, + child: TextField( + controller: controller, + enabled: enabled && !locked, + onChanged: (_) => apply_enabled.value = true, + inputFormatters: [ + FilteringTextInputFormatter.allow(RegExp( + '\^([0-9]|[1-9]\\d|[1-9]\\d{2}|[1-9]\\d{3}|[1-5]\\d{4}|6[0-4]\\d{3}|65[0-4]\\d{2}|655[0-2]\\d|6553[0-5])\$')), + ], + textAlign: TextAlign.end, + decoration: InputDecoration( + hintText: '21118', + border: InputBorder.none, + contentPadding: EdgeInsets.only(right: 5), + isCollapsed: true, + ), + ), + ), + enabled: enabled && !locked, + ).marginOnly(left: 5), + Obx(() => ElevatedButton( + onPressed: apply_enabled.value && enabled && !locked + ? () async { + apply_enabled.value = false; + await bind.mainSetOption( + key: 'direct-access-port', + value: controller.text); + } + : null, + child: Text( + translate('Apply'), + ), + ).marginOnly(left: 20)) + ]); + }, + ), + ]); + } +} + +class _Display extends StatefulWidget { + const _Display({Key? key}) : super(key: key); + + @override + State<_Display> createState() => _DisplayState(); +} + +class _DisplayState extends State<_Display> with AutomaticKeepAliveClientMixin { + @override + bool get wantKeepAlive => true; + + @override + Widget build(BuildContext context) { + super.build(context); + return ListView( + children: [ + _Card(title: 'Adaptive Bitrate', children: [ + _OptionCheckBox(context, 'Adaptive Bitrate', 'enable-abr'), + ]), + hwcodec(), + ], + ).marginOnly(bottom: _kListViewBottomMargin); + } + + Widget hwcodec() { + return _futureBuilder( + future: bind.mainHasHwcodec(), + hasData: (data) { + return Offstage( + offstage: !(data as bool), + child: _Card(title: 'Hardware Codec', children: [ + _OptionCheckBox( + context, 'Enable hardware codec', 'enable-hwcodec'), + ]), + ); + }); + } +} + +class _Audio extends StatefulWidget { + const _Audio({Key? key}) : super(key: key); + + @override + State<_Audio> createState() => _AudioState(); +} + +enum _AudioInputType { + Mute, + Standard, + Specify, +} + +class _AudioState extends State<_Audio> with AutomaticKeepAliveClientMixin { + @override + bool get wantKeepAlive => true; + + @override + Widget build(BuildContext context) { + super.build(context); + var update = () => setState(() {}); + var set_enabled = (bool enabled) => bind.mainSetOption( + key: 'enable-audio', value: bool2option('enable-audio', enabled)); + var set_device = (String device) => + bind.mainSetOption(key: 'audio-input', value: device); + return ListView(children: [ + _Card( + title: 'Audio Input', + children: [ + _futureBuilder(future: () async { + List devices = await bind.mainGetSoundInputs(); + String current = await bind.mainGetOption(key: 'audio-input'); + String enabled = await bind.mainGetOption(key: 'enable-audio'); + return {'devices': devices, 'current': current, 'enabled': enabled}; + }(), hasData: (data) { + bool mute = + !option2bool('enable-audio', data['enabled'].toString()); + String currentDevice = data['current']; + List devices = (data['devices'] as List).toList(); + _AudioInputType groupValue; + if (mute) { + groupValue = _AudioInputType.Mute; + } else if (devices.contains(currentDevice)) { + groupValue = _AudioInputType.Specify; + } else { + groupValue = _AudioInputType.Standard; + } + List deviceWidget = [].toList(); + if (devices.isNotEmpty) { + var combo = _ComboBox( + keys: devices, + values: devices, + initialKey: devices.contains(currentDevice) + ? currentDevice + : devices[0], + onChanged: (key) { + set_device(key); + }, + enabled: groupValue == _AudioInputType.Specify, + ); + deviceWidget.addAll([ + _Radio<_AudioInputType>( + context, + value: _AudioInputType.Specify, + groupValue: groupValue, + label: 'Specify device', + onChanged: (value) { + set_device(combo.current); + set_enabled(true); + update(); + }, + ), + combo.marginOnly(left: _kContentHSubMargin, top: 5), + ]); + } + return Column(children: [ + _Radio<_AudioInputType>( + context, + value: _AudioInputType.Mute, + groupValue: groupValue, + label: 'Mute', + onChanged: (value) { + set_enabled(false); + update(); + }, + ), + _Radio( + context, + value: _AudioInputType.Standard, + groupValue: groupValue, + label: 'Use standard device', + onChanged: (value) { + set_device(''); + set_enabled(true); + update(); + }, + ), + ...deviceWidget, + ]); + }), + ], + ) + ]).marginOnly(bottom: _kListViewBottomMargin); + } +} + +class _About extends StatefulWidget { + const _About({Key? key}) : super(key: key); + + @override + State<_About> createState() => _AboutState(); +} + +class _AboutState extends State<_About> { + @override + Widget build(BuildContext context) { + return _futureBuilder(future: () async { + final license = await bind.mainGetLicense(); + final version = await bind.mainGetVersion(); + return {'license': license, 'version': version}; + }(), hasData: (data) { + final license = data['license'].toString(); + final version = data['version'].toString(); + final linkStyle = TextStyle(decoration: TextDecoration.underline); + return ListView(children: [ + _Card(title: "About RustDesk", children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + height: 8.0, + ), + Text("Version: $version").marginSymmetric(vertical: 4.0), + InkWell( + onTap: () { + launchUrlString("https://rustdesk.com/privacy"); + }, + child: Text( + "Privacy Statement", + style: linkStyle, + ).marginSymmetric(vertical: 4.0)), + InkWell( + onTap: () { + launchUrlString("https://rustdesk.com"); + }, + child: Text( + "Website", + style: linkStyle, + ).marginSymmetric(vertical: 4.0)), + Container( + decoration: BoxDecoration(color: Color(0xFF2c8cff)), + padding: EdgeInsets.symmetric(vertical: 24, horizontal: 8), + child: Row( + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Copyright © 2022 Purslane Ltd.\n$license", + style: TextStyle(color: Colors.white), + ), + Text( + "Made with heart in this chaotic world!", + style: TextStyle( + fontWeight: FontWeight.w800, + color: Colors.white), + ) + ], + ), + ), + ], + ), + ).marginSymmetric(vertical: 4.0) + ], + ).marginOnly(left: _kContentHMargin) + ]), + ]).marginOnly(left: _kCardLeftMargin); + }); + } +} + +//#endregion + +//#region components + +Widget _Card({required String title, required List children}) { + return Row( + children: [ + Container( + width: _kCardFixedWidth, + child: Card( + child: Column( + children: [ + Row( + children: [ + Text( + translate(title), + textAlign: TextAlign.start, + style: TextStyle( + fontSize: _kTitleFontSize, + ), + ), + Spacer(), + ], + ).marginOnly(left: _kContentHMargin, top: 10, bottom: 10), + ...children + .map((e) => e.marginOnly(top: 4, right: _kContentHMargin)), + ], + ).marginOnly(bottom: 10), + ).marginOnly(left: _kCardLeftMargin, top: 15), + ), + ], + ); +} + +Color? _disabledTextColor(BuildContext context, bool enabled) { + return enabled ? null : MyTheme.color(context).lighterText; +} + +Widget _OptionCheckBox(BuildContext context, String label, String key, + {Function()? update = null, bool reverse = false, bool enabled = true}) { + return _futureBuilder( + future: bind.mainGetOption(key: key), + hasData: (data) { + bool value = option2bool(key, data.toString()); + if (reverse) value = !value; + var ref = value.obs; + var onChanged = (option) async { + if (option != null) { + ref.value = option; + if (reverse) option = !option; + String value = bool2option(key, option); + bind.mainSetOption(key: key, value: value); + update?.call(); + } + }; + return GestureDetector( + child: Obx( + () => Row( + children: [ + Checkbox( + value: ref.value, onChanged: enabled ? onChanged : null) + .marginOnly(right: 10), + Expanded( + child: Text( + translate(label), + style: TextStyle(color: _disabledTextColor(context, enabled)), + )) + ], + ), + ).marginOnly(left: _kCheckBoxLeftMargin), + onTap: () { + onChanged(!ref.value); + }, + ); + }); +} + +Widget _Radio(BuildContext context, + {required T value, + required T groupValue, + required String label, + required Function(T value) onChanged, + bool enabled = true}) { + var on_change = enabled + ? (T? value) { + if (value != null) { + onChanged(value); + } + } + : null; + return GestureDetector( + child: Row( + children: [ + Radio(value: value, groupValue: groupValue, onChanged: on_change), + Expanded( + child: Text(translate(label), + style: TextStyle( + fontSize: _kContentFontSize, + color: _disabledTextColor(context, enabled))) + .marginOnly(left: 5), + ), + ], + ).marginOnly(left: _kRadioLeftMargin), + onTap: () => on_change?.call(value), + ); +} + +Widget _Button(String label, Function() onPressed, + {bool enabled = true, String? tip}) { + var button = ElevatedButton( + onPressed: enabled ? onPressed : null, + child: Container( + child: Text( + translate(label), + ).marginSymmetric(horizontal: 15), + )); + var child; + if (tip == null) { + child = button; + } else { + child = Tooltip(message: translate(tip), child: button); + } + return Row(children: [ + child, + ]).marginOnly(left: _kContentHMargin); +} + +Widget _SubButton(String label, Function() onPressed, [bool enabled = true]) { + return Row( + children: [ + ElevatedButton( + onPressed: enabled ? onPressed : null, + child: Container( + child: Text( + translate(label), + ).marginSymmetric(horizontal: 15), + )), + ], + ).marginOnly(left: _kContentHSubMargin); +} + +Widget _SubLabeledWidget(String label, Widget child, {bool enabled = true}) { + RxBool hover = false.obs; + return Row( + children: [ + MouseRegion( + onEnter: (_) => hover.value = true, + onExit: (_) => hover.value = false, + child: Obx( + () { + return Container( + height: 32, + decoration: BoxDecoration( + border: Border.all( + color: hover.value && enabled + ? Color(0xFFD7D7D7) + : Color(0xFFCBCBCB), + width: hover.value && enabled ? 2 : 1)), + child: Row( + children: [ + Container( + height: 28, + color: (hover.value && enabled) + ? Color(0xFFD7D7D7) + : Color(0xFFCBCBCB), + child: Text( + label + ': ', + style: TextStyle(fontWeight: FontWeight.w300), + ), + alignment: Alignment.center, + padding: + EdgeInsets.symmetric(horizontal: 5, vertical: 2), + ).paddingAll(2), + child, + ], + )); + }, + )), + ], + ).marginOnly(left: _kContentHSubMargin); +} + +Widget _futureBuilder( + {required Future? future, required Widget Function(dynamic data) hasData}) { + return FutureBuilder( + future: future, + builder: (BuildContext context, AsyncSnapshot snapshot) { + if (snapshot.hasData) { + return hasData(snapshot.data!); + } else { + if (snapshot.hasError) { + print(snapshot.error.toString()); + } + return Container(); + } + }); +} + +Widget _lock( + bool locked, + String label, + Function() onUnlock, +) { + return Offstage( + offstage: !locked, + child: Row( + children: [ + Container( + width: _kCardFixedWidth, + child: Card( + child: ElevatedButton( + child: Container( + height: 25, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.security_sharp, + size: 20, + ), + Text(translate(label)).marginOnly(left: 5), + ]).marginSymmetric(vertical: 2)), + onPressed: () async { + bool checked = await bind.mainCheckSuperUserPermission(); + if (checked) { + onUnlock(); + } + }, + ).marginSymmetric(horizontal: 2, vertical: 4), + ).marginOnly(left: _kCardLeftMargin), + ).marginOnly(top: 10), + ], + )); +} + +// ignore: must_be_immutable +class _ComboBox extends StatelessWidget { + late final List keys; + late final List values; + late final String initialKey; + late final Function(String key) onChanged; + late final bool enabled; + late String current; + + _ComboBox({ + Key? key, + required this.keys, + required this.values, + required this.initialKey, + required this.onChanged, + this.enabled = true, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + var index = keys.indexOf(initialKey); + if (index < 0) { + assert(false); + index = 0; + } + var ref = values[index].obs; + current = keys[index]; + return Container( + decoration: BoxDecoration(border: Border.all(color: MyTheme.border)), + height: 30, + child: Obx(() => DropdownButton( + isExpanded: true, + value: ref.value, + elevation: 16, + underline: Container( + height: 25, + ), + icon: Icon( + Icons.expand_more_sharp, + size: 20, + ), + onChanged: enabled + ? (String? newValue) { + if (newValue != null && newValue != ref.value) { + ref.value = newValue; + current = newValue; + onChanged(keys[values.indexOf(newValue)]); + } + } + : null, + items: values.map>((String value) { + return DropdownMenuItem( + value: value, + child: Text( + value, + style: TextStyle(fontSize: _kContentFontSize), + overflow: TextOverflow.ellipsis, + ).marginOnly(left: 5), + ); + }).toList(), + )), + ); + } +} + +//#endregion + +//#region dialogs + +void changeServer() async { + Map oldOptions = jsonDecode(await bind.mainGetOptions()); + print("${oldOptions}"); + String idServer = oldOptions['custom-rendezvous-server'] ?? ""; + var idServerMsg = ""; + String relayServer = oldOptions['relay-server'] ?? ""; + var relayServerMsg = ""; + String apiServer = oldOptions['api-server'] ?? ""; + var apiServerMsg = ""; + var key = oldOptions['key'] ?? ""; + + var isInProgress = false; + gFFI.dialogManager.show((setState, close) { + return CustomAlertDialog( + title: Text(translate("ID/Relay Server")), + content: ConstrainedBox( + constraints: BoxConstraints(minWidth: 500), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + height: 8.0, + ), + Row( + children: [ + ConstrainedBox( + constraints: BoxConstraints(minWidth: 100), + child: Text("${translate('ID Server')}:") + .marginOnly(bottom: 16.0)), + SizedBox( + width: 24.0, + ), + Expanded( + child: TextField( + onChanged: (s) { + idServer = s; + }, + decoration: InputDecoration( + border: OutlineInputBorder(), + errorText: idServerMsg.isNotEmpty ? idServerMsg : null), + controller: TextEditingController(text: idServer), + ), + ), + ], + ), + SizedBox( + height: 8.0, + ), + Row( + children: [ + ConstrainedBox( + constraints: BoxConstraints(minWidth: 100), + child: Text("${translate('Relay Server')}:") + .marginOnly(bottom: 16.0)), + SizedBox( + width: 24.0, + ), + Expanded( + child: TextField( + onChanged: (s) { + relayServer = s; + }, + decoration: InputDecoration( + border: OutlineInputBorder(), + errorText: + relayServerMsg.isNotEmpty ? relayServerMsg : null), + controller: TextEditingController(text: relayServer), + ), + ), + ], + ), + SizedBox( + height: 8.0, + ), + Row( + children: [ + ConstrainedBox( + constraints: BoxConstraints(minWidth: 100), + child: Text("${translate('API Server')}:") + .marginOnly(bottom: 16.0)), + SizedBox( + width: 24.0, + ), + Expanded( + child: TextField( + onChanged: (s) { + apiServer = s; + }, + decoration: InputDecoration( + border: OutlineInputBorder(), + errorText: + apiServerMsg.isNotEmpty ? apiServerMsg : null), + controller: TextEditingController(text: apiServer), + ), + ), + ], + ), + SizedBox( + height: 8.0, + ), + Row( + children: [ + ConstrainedBox( + constraints: BoxConstraints(minWidth: 100), + child: + Text("${translate('Key')}:").marginOnly(bottom: 16.0)), + SizedBox( + width: 24.0, + ), + Expanded( + child: TextField( + onChanged: (s) { + key = s; + }, + decoration: InputDecoration( + border: OutlineInputBorder(), + ), + controller: TextEditingController(text: key), + ), + ), + ], + ), + SizedBox( + height: 4.0, + ), + Offstage(offstage: !isInProgress, child: LinearProgressIndicator()) + ], + ), + ), + actions: [ + TextButton( + onPressed: () { + close(); + }, + child: Text(translate("Cancel"))), + TextButton( + onPressed: () async { + setState(() { + [idServerMsg, relayServerMsg, apiServerMsg].forEach((element) { + element = ""; + }); + isInProgress = true; + }); + final cancel = () { + setState(() { + isInProgress = false; + }); + }; + idServer = idServer.trim(); + relayServer = relayServer.trim(); + apiServer = apiServer.trim(); + key = key.trim(); + + if (idServer.isNotEmpty) { + idServerMsg = translate( + await bind.mainTestIfValidServer(server: idServer)); + if (idServerMsg.isEmpty) { + oldOptions['custom-rendezvous-server'] = idServer; + } else { + cancel(); + return; + } + } else { + oldOptions['custom-rendezvous-server'] = ""; + } + + if (relayServer.isNotEmpty) { + relayServerMsg = translate( + await bind.mainTestIfValidServer(server: relayServer)); + if (relayServerMsg.isEmpty) { + oldOptions['relay-server'] = relayServer; + } else { + cancel(); + return; + } + } else { + oldOptions['relay-server'] = ""; + } + + if (apiServer.isNotEmpty) { + if (apiServer.startsWith('http://') || + apiServer.startsWith("https://")) { + oldOptions['api-server'] = apiServer; + return; + } else { + apiServerMsg = translate("invalid_http"); + cancel(); + return; + } + } else { + oldOptions['api-server'] = ""; + } + // ok + oldOptions['key'] = key; + await bind.mainSetOptions(json: jsonEncode(oldOptions)); + close(); + }, + child: Text(translate("OK"))), + ], + ); + }); +} + +void changeWhiteList() async { + Map oldOptions = jsonDecode(await bind.mainGetOptions()); + var newWhiteList = ((oldOptions['whitelist'] ?? "") as String).split(','); + var newWhiteListField = newWhiteList.join('\n'); + var msg = ""; + var isInProgress = false; + gFFI.dialogManager.show((setState, close) { + return CustomAlertDialog( + title: Text(translate("IP Whitelisting")), + content: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(translate("whitelist_sep")), + SizedBox( + height: 8.0, + ), + Row( + children: [ + Expanded( + child: TextField( + onChanged: (s) { + newWhiteListField = s; + }, + maxLines: null, + decoration: InputDecoration( + border: OutlineInputBorder(), + errorText: msg.isEmpty ? null : translate(msg), + ), + controller: TextEditingController(text: newWhiteListField), + ), + ), + ], + ), + SizedBox( + height: 4.0, + ), + Offstage(offstage: !isInProgress, child: LinearProgressIndicator()) + ], + ), + actions: [ + TextButton( + onPressed: () { + close(); + }, + child: Text(translate("Cancel"))), + TextButton( + onPressed: () async { + setState(() { + msg = ""; + isInProgress = true; + }); + newWhiteListField = newWhiteListField.trim(); + var newWhiteList = ""; + if (newWhiteListField.isEmpty) { + // pass + } else { + final ips = + newWhiteListField.trim().split(RegExp(r"[\s,;\n]+")); + // test ip + final ipMatch = RegExp(r"^\d+\.\d+\.\d+\.\d+$"); + for (final ip in ips) { + if (!ipMatch.hasMatch(ip)) { + msg = translate("Invalid IP") + " $ip"; + setState(() { + isInProgress = false; + }); + return; + } + } + newWhiteList = ips.join(','); + } + oldOptions['whitelist'] = newWhiteList; + await bind.mainSetOptions(json: jsonEncode(oldOptions)); + close(); + }, + child: Text(translate("OK"))), + ], + ); + }); +} + +void changeSocks5Proxy() async { + var socks = await bind.mainGetSocks(); + + String proxy = ""; + String proxyMsg = ""; + String username = ""; + String password = ""; + if (socks.length == 3) { + proxy = socks[0]; + username = socks[1]; + password = socks[2]; + } + + var isInProgress = false; + gFFI.dialogManager.show((setState, close) { + return CustomAlertDialog( + title: Text(translate("Socks5 Proxy")), + content: ConstrainedBox( + constraints: BoxConstraints(minWidth: 500), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + height: 8.0, + ), + Row( + children: [ + ConstrainedBox( + constraints: BoxConstraints(minWidth: 100), + child: Text("${translate('Hostname')}:") + .marginOnly(bottom: 16.0)), + SizedBox( + width: 24.0, + ), + Expanded( + child: TextField( + onChanged: (s) { + proxy = s; + }, + decoration: InputDecoration( + border: OutlineInputBorder(), + errorText: proxyMsg.isNotEmpty ? proxyMsg : null), + controller: TextEditingController(text: proxy), + ), + ), + ], + ), + SizedBox( + height: 8.0, + ), + Row( + children: [ + ConstrainedBox( + constraints: BoxConstraints(minWidth: 100), + child: Text("${translate('Username')}:") + .marginOnly(bottom: 16.0)), + SizedBox( + width: 24.0, + ), + Expanded( + child: TextField( + onChanged: (s) { + username = s; + }, + decoration: InputDecoration( + border: OutlineInputBorder(), + ), + controller: TextEditingController(text: username), + ), + ), + ], + ), + SizedBox( + height: 8.0, + ), + Row( + children: [ + ConstrainedBox( + constraints: BoxConstraints(minWidth: 100), + child: Text("${translate('Password')}:") + .marginOnly(bottom: 16.0)), + SizedBox( + width: 24.0, + ), + Expanded( + child: TextField( + onChanged: (s) { + password = s; + }, + decoration: InputDecoration( + border: OutlineInputBorder(), + ), + controller: TextEditingController(text: password), + ), + ), + ], + ), + SizedBox( + height: 8.0, + ), + Offstage(offstage: !isInProgress, child: LinearProgressIndicator()) + ], + ), + ), + actions: [ + TextButton( + onPressed: () { + close(); + }, + child: Text(translate("Cancel"))), + TextButton( + onPressed: () async { + setState(() { + proxyMsg = ""; + isInProgress = true; + }); + final cancel = () { + setState(() { + isInProgress = false; + }); + }; + proxy = proxy.trim(); + username = username.trim(); + password = password.trim(); + + if (proxy.isNotEmpty) { + proxyMsg = + translate(await bind.mainTestIfValidServer(server: proxy)); + if (proxyMsg.isEmpty) { + // ignore + } else { + cancel(); + return; + } + } + await bind.mainSetSocks( + proxy: proxy, username: username, password: password); + close(); + }, + child: Text(translate("OK"))), + ], + ); + }); +} + +//#endregion diff --git a/flutter/lib/desktop/pages/desktop_tab_page.dart b/flutter/lib/desktop/pages/desktop_tab_page.dart new file mode 100644 index 000000000..4a2fdb7d2 --- /dev/null +++ b/flutter/lib/desktop/pages/desktop_tab_page.dart @@ -0,0 +1,66 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_hbb/common.dart'; +import 'package:flutter_hbb/consts.dart'; +import 'package:flutter_hbb/desktop/pages/desktop_home_page.dart'; +import 'package:flutter_hbb/desktop/pages/desktop_setting_page.dart'; +import 'package:flutter_hbb/desktop/widgets/tabbar_widget.dart'; +import 'package:window_manager/window_manager.dart'; + +class DesktopTabPage extends StatefulWidget { + const DesktopTabPage({Key? key}) : super(key: key); + + @override + State createState() => _DesktopTabPageState(); +} + +class _DesktopTabPageState extends State { + final tabController = DesktopTabController(); + + @override + void initState() { + super.initState(); + tabController.add(TabInfo( + key: kTabLabelHomePage, + label: kTabLabelHomePage, + selectedIcon: Icons.home_sharp, + unselectedIcon: Icons.home_outlined, + closable: false, + page: DesktopHomePage( + key: const ValueKey(kTabLabelHomePage), + ))); + } + + @override + Widget build(BuildContext context) { + final dark = isDarkTheme(); + return DragToResizeArea( + child: Container( + decoration: BoxDecoration( + border: Border.all(color: MyTheme.color(context).border!)), + child: Scaffold( + backgroundColor: MyTheme.color(context).bg, + body: DesktopTab( + controller: tabController, + theme: dark ? TarBarTheme.dark() : TarBarTheme.light(), + isMainWindow: true, + tail: ActionIcon( + message: 'Settings', + icon: IconFont.menu, + theme: dark ? TarBarTheme.dark() : TarBarTheme.light(), + onTap: onAddSetting, + is_close: false, + ), + )), + ), + ); + } + + void onAddSetting() { + tabController.add(TabInfo( + key: kTabLabelSettingPage, + label: kTabLabelSettingPage, + selectedIcon: Icons.build_sharp, + unselectedIcon: Icons.build_outlined, + page: DesktopSettingPage(key: const ValueKey(kTabLabelSettingPage)))); + } +} diff --git a/flutter/lib/desktop/pages/file_manager_page.dart b/flutter/lib/desktop/pages/file_manager_page.dart new file mode 100644 index 000000000..4a2f11553 --- /dev/null +++ b/flutter/lib/desktop/pages/file_manager_page.dart @@ -0,0 +1,867 @@ +import 'dart:io'; +import 'dart:math'; + +import 'package:desktop_drop/desktop_drop.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_breadcrumb/flutter_breadcrumb.dart'; +import 'package:flutter_hbb/mobile/pages/file_manager_page.dart'; +import 'package:flutter_hbb/models/file_model.dart'; +import 'package:get/get.dart'; +import 'package:provider/provider.dart'; +import 'package:wakelock/wakelock.dart'; + +import '../../common.dart'; +import '../../models/model.dart'; +import '../../models/platform_model.dart'; + +enum LocationStatus { bread, textField } + +class FileManagerPage extends StatefulWidget { + FileManagerPage({Key? key, required this.id}) : super(key: key); + final String id; + + @override + State createState() => _FileManagerPageState(); +} + +class _FileManagerPageState extends State + with AutomaticKeepAliveClientMixin { + final _localSelectedItems = SelectedItems(); + final _remoteSelectedItems = SelectedItems(); + + final _locationStatusLocal = LocationStatus.bread.obs; + final _locationStatusRemote = LocationStatus.bread.obs; + final FocusNode _locationNodeLocal = + FocusNode(debugLabel: "locationNodeLocal"); + final FocusNode _locationNodeRemote = + FocusNode(debugLabel: "locationNodeRemote"); + final _searchTextLocal = "".obs; + final _searchTextRemote = "".obs; + final _breadCrumbScrollerLocal = ScrollController(); + final _breadCrumbScrollerRemote = ScrollController(); + + final _dropMaskVisible = false.obs; + + ScrollController getBreadCrumbScrollController(bool isLocal) { + return isLocal ? _breadCrumbScrollerLocal : _breadCrumbScrollerRemote; + } + + late FFI _ffi; + + FileModel get model => _ffi.fileModel; + + SelectedItems getSelectedItem(bool isLocal) { + return isLocal ? _localSelectedItems : _remoteSelectedItems; + } + + @override + void initState() { + super.initState(); + _ffi = FFI(); + _ffi.connect(widget.id, isFileTransfer: true); + Get.put(_ffi, tag: 'ft_${widget.id}'); + // _ffi.ffiModel.updateEventListener(widget.id); + if (!Platform.isLinux) { + Wakelock.enable(); + } + print("init success with id ${widget.id}"); + // register location listener + _locationNodeLocal.addListener(onLocalLocationFocusChanged); + _locationNodeRemote.addListener(onRemoteLocationFocusChanged); + } + + @override + void dispose() { + model.onClose(); + _ffi.close(); + _ffi.dialogManager.dismissAll(); + if (!Platform.isLinux) { + Wakelock.disable(); + } + Get.delete(tag: 'ft_${widget.id}'); + _locationNodeLocal.removeListener(onLocalLocationFocusChanged); + _locationNodeRemote.removeListener(onRemoteLocationFocusChanged); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + super.build(context); + return Overlay(initialEntries: [ + OverlayEntry(builder: (context) { + _ffi.dialogManager.setOverlayState(Overlay.of(context)); + return ChangeNotifierProvider.value( + value: _ffi.fileModel, + child: Consumer(builder: (_context, _model, _child) { + return WillPopScope( + onWillPop: () async { + if (model.selectMode) { + model.toggleSelectMode(); + } + return false; + }, + child: Scaffold( + backgroundColor: MyTheme.color(context).bg, + body: Row( + children: [ + Flexible(flex: 3, child: body(isLocal: true)), + Flexible(flex: 3, child: body(isLocal: false)), + Flexible(flex: 2, child: statusList()) + ], + ), + )); + })); + }) + ]); + } + + Widget menu({bool isLocal = false}) { + return PopupMenuButton( + icon: Icon(Icons.more_vert), + itemBuilder: (context) { + return [ + PopupMenuItem( + child: Row( + children: [ + Icon( + model.getCurrentShowHidden(isLocal) + ? Icons.check_box_outlined + : Icons.check_box_outline_blank, + color: Colors.black), + SizedBox(width: 5), + Text(translate("Show Hidden Files")) + ], + ), + value: "hidden", + ) + ]; + }, + onSelected: (v) { + if (v == "hidden") { + model.toggleShowHidden(local: isLocal); + } + }); + } + + Widget body({bool isLocal = false}) { + final fd = model.getCurrentDir(isLocal); + final entries = fd.entries; + final sortIndex = (SortBy style) { + switch (style) { + case SortBy.Name: + return 1; + case SortBy.Type: + return 0; + case SortBy.Modified: + return 2; + case SortBy.Size: + return 3; + } + }(model.getSortStyle(isLocal)); + final sortAscending = + isLocal ? model.localSortAscending : model.remoteSortAscending; + return Container( + decoration: BoxDecoration(border: Border.all(color: Colors.black26)), + margin: const EdgeInsets.all(16.0), + padding: const EdgeInsets.all(8.0), + child: DropTarget( + onDragDone: (detail) => handleDragDone(detail, 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: SingleChildScrollView( + child: ObxValue( + (searchText) { + final filteredEntries = searchText.isEmpty + ? entries.where((element) { + if (searchText.isEmpty) { + return true; + } else { + return element.name.contains(searchText.value); + } + }).toList(growable: false) + : entries; + return DataTable( + key: ValueKey(isLocal ? 0 : 1), + showCheckboxColumn: true, + dataRowHeight: 25, + headingRowHeight: 30, + columnSpacing: 8, + showBottomBorder: true, + sortColumnIndex: sortIndex, + sortAscending: sortAscending, + columns: [ + DataColumn(label: Text(translate(" "))), // icon + DataColumn( + label: Text( + translate("Name"), + ), + onSort: (columnIndex, ascending) { + model.changeSortStyle(SortBy.Name, + isLocal: isLocal, ascending: ascending); + }), + DataColumn( + label: Text( + translate("Modified"), + ), + onSort: (columnIndex, ascending) { + model.changeSortStyle(SortBy.Modified, + isLocal: isLocal, ascending: ascending); + }), + DataColumn( + label: Text(translate("Size")), + onSort: (columnIndex, ascending) { + model.changeSortStyle(SortBy.Size, + isLocal: isLocal, ascending: ascending); + }), + ], + rows: filteredEntries.map((entry) { + final sizeStr = entry.isFile + ? readableFileSize(entry.size.toDouble()) + : ""; + return DataRow( + key: ValueKey(entry.name), + onSelectChanged: (s) { + if (s != null) { + if (s) { + getSelectedItem(isLocal) + .add(isLocal, entry); + } else { + getSelectedItem(isLocal).remove(entry); + } + setState(() {}); + } + }, + selected: + getSelectedItem(isLocal).contains(entry), + cells: [ + DataCell(Icon( + entry.isFile + ? Icons.feed_outlined + : Icons.folder, + size: 25)), + DataCell( + ConstrainedBox( + constraints: + BoxConstraints(maxWidth: 100), + child: Tooltip( + message: entry.name, + child: Text(entry.name, + overflow: TextOverflow.ellipsis), + )), onTap: () { + if (entry.isDirectory) { + openDirectory(entry.path, isLocal: isLocal); + if (isLocal) { + _localSelectedItems.clear(); + } else { + _remoteSelectedItems.clear(); + } + } else { + // Perform file-related tasks. + final _selectedItems = + getSelectedItem(isLocal); + if (_selectedItems.contains(entry)) { + _selectedItems.remove(entry); + } else { + _selectedItems.add(isLocal, entry); + } + setState(() {}); + } + }), + DataCell(Text( + entry + .lastModified() + .toString() + .replaceAll(".000", "") + + " ", + style: TextStyle( + fontSize: 12, color: MyTheme.darkGray), + )), + DataCell(Text( + sizeStr, + style: TextStyle( + fontSize: 12, color: MyTheme.darkGray), + )), + ]); + }).toList(growable: false), + ); + }, + isLocal ? _searchTextLocal : _searchTextRemote, + ), + ), + ) + ], + )), + // Center(child: listTail(isLocal: isLocal)), + // Expanded( + // child: ListView.builder( + // itemCount: entries.length + 1, + // itemBuilder: (context, index) { + // if (index >= entries.length) { + // return listTail(isLocal: isLocal); + // } + // var selected = false; + // if (model.selectMode) { + // selected = _selectedItems.contains(entries[index]); + // } + // + // final sizeStr = entries[index].isFile + // ? readableFileSize(entries[index].size.toDouble()) + // : ""; + // return Card( + // child: ListTile( + // leading: Icon( + // entries[index].isFile ? Icons.feed_outlined : Icons.folder, + // size: 40), + // title: Text(entries[index].name), + // selected: selected, + // subtitle: Text( + // entries[index] + // .lastModified() + // .toString() + // .replaceAll(".000", "") + + // " " + + // sizeStr, + // style: TextStyle(fontSize: 12, color: MyTheme.darkGray), + // ), + // trailing: needShowCheckBox() + // ? 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( + // 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]); + // } + // setState(() {}); + // return; + // } + // if (entries[index].isDirectory) { + // openDirectory(entries[index].path, isLocal: isLocal); + // breadCrumbScrollToEnd(isLocal); + // } else { + // // Perform file-related tasks. + // } + // }, + // onLongPress: () { + // _selectedItems.clear(); + // model.toggleSelectMode(); + // if (model.selectMode) { + // _selectedItems.add(isLocal, entries[index]); + // } + // setState(() {}); + // }, + // ), + // ); + // }, + // )) + ]), + ), + ); + } + + /// transfer status list + /// watch transfer status + Widget statusList() { + return PreferredSize( + child: Container( + margin: const EdgeInsets.only(top: 16.0, bottom: 16.0, right: 16.0), + padding: const EdgeInsets.all(8.0), + decoration: BoxDecoration(border: Border.all(color: Colors.grey)), + child: Obx( + () => ListView.builder( + itemBuilder: (BuildContext context, int index) { + final item = model.jobTable[index]; + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Transform.rotate( + angle: item.isRemote ? pi : 0, + child: Icon(Icons.send)), + SizedBox( + width: 16.0, + ), + Expanded( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Tooltip( + message: item.jobName, + child: Text( + '${item.jobName}', + maxLines: 1, + overflow: TextOverflow.ellipsis, + )), + Wrap( + children: [ + Text( + '${item.state.display()} ${max(0, item.fileNum)}/${item.fileCount} '), + Text( + '${translate("files")} ${readableFileSize(item.totalSize.toDouble())} '), + Offstage( + offstage: + item.state != JobState.inProgress, + child: Text( + '${readableFileSize(item.speed) + "/s"} ')), + Offstage( + offstage: item.totalSize <= 0, + child: Text( + '${(item.finishedSize.toDouble() * 100 / item.totalSize.toDouble()).toStringAsFixed(2)}%'), + ), + ], + ), + ], + ), + ), + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Offstage( + offstage: item.state != JobState.paused, + child: IconButton( + onPressed: () { + model.resumeJob(item.id); + }, + icon: Icon(Icons.restart_alt_rounded)), + ), + IconButton( + icon: Icon(Icons.delete), + onPressed: () { + model.jobTable.removeAt(index); + model.cancelJob(item.id); + }, + ), + ], + ) + ], + ), + SizedBox( + height: 8.0, + ), + Divider( + height: 2.0, + ) + ], + ); + }, + itemCount: model.jobTable.length, + ), + ), + ), + preferredSize: Size(200, double.infinity)); + } + + goBack({bool? isLocal}) { + model.goToParentDirectory(isLocal: isLocal); + } + + Widget headTools(bool isLocal) { + final _locationStatus = + isLocal ? _locationStatusLocal : _locationStatusRemote; + final _locationFocus = isLocal ? _locationNodeLocal : _locationNodeRemote; + final _searchTextObs = isLocal ? _searchTextLocal : _searchTextRemote; + return Container( + child: Column( + children: [ + // symbols + PreferredSize( + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Container( + width: 50, + height: 50, + decoration: BoxDecoration(color: Colors.blue), + padding: EdgeInsets.all(8.0), + child: FutureBuilder( + future: bind.sessionGetPlatform( + id: _ffi.id, isRemote: !isLocal), + builder: (context, snapshot) { + if (snapshot.hasData && snapshot.data!.isNotEmpty) { + return getPlatformImage('${snapshot.data}'); + } else { + return CircularProgressIndicator( + color: Colors.white, + ); + } + })), + Text(isLocal + ? translate("Local Computer") + : translate("Remote Computer")) + .marginOnly(left: 8.0) + ], + ), + preferredSize: Size(double.infinity, 70)), + // buttons + Row( + children: [ + Row( + children: [ + IconButton( + onPressed: () { + model.goHome(isLocal: isLocal); + }, + icon: Icon(Icons.home_outlined)), + IconButton( + icon: Icon(Icons.arrow_upward), + onPressed: () { + goBack(isLocal: isLocal); + }, + ), + menu(isLocal: isLocal), + ], + ), + Expanded( + child: GestureDetector( + onTap: () { + _locationStatus.value = + _locationStatus.value == LocationStatus.bread + ? LocationStatus.textField + : LocationStatus.bread; + Future.delayed(Duration.zero, () { + if (_locationStatus.value == LocationStatus.textField) { + _locationFocus.requestFocus(); + } + }); + }, + child: Container( + decoration: + BoxDecoration(border: Border.all(color: Colors.black12)), + child: Row( + children: [ + Expanded( + child: Obx(() => + _locationStatus.value == LocationStatus.bread + ? buildBread(isLocal) + : buildPathLocation(isLocal))), + DropdownButton( + isDense: true, + underline: Offstage(), + items: [ + // TODO: favourite + DropdownMenuItem( + child: Text('/'), + value: '/', + ) + ], + onChanged: (path) { + if (path is String && path.isNotEmpty) { + openDirectory(path, isLocal: isLocal); + } + }) + ], + )), + )), + PopupMenuButton( + itemBuilder: (context) => [ + PopupMenuItem( + enabled: false, + child: ConstrainedBox( + constraints: BoxConstraints(minWidth: 200), + child: TextField( + controller: + TextEditingController(text: _searchTextObs.value), + autofocus: true, + decoration: + InputDecoration(prefixIcon: Icon(Icons.search)), + onChanged: (searchText) => + onSearchText(searchText, isLocal), + ), + )) + ], + child: Icon(Icons.search), + ), + IconButton( + onPressed: () { + model.refresh(isLocal: isLocal); + }, + icon: Icon(Icons.refresh)), + ], + ), + Row( + textDirection: isLocal ? TextDirection.ltr : TextDirection.rtl, + children: [ + Expanded( + child: Row( + mainAxisAlignment: + isLocal ? MainAxisAlignment.start : MainAxisAlignment.end, + children: [ + IconButton( + onPressed: () { + final name = TextEditingController(); + _ffi.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: [ + TextButton( + style: flatButtonStyle, + onPressed: () => close(false), + child: Text(translate("Cancel"))), + ElevatedButton( + style: flatButtonStyle, + onPressed: () { + if (name.value.text.isNotEmpty) { + model.createDir( + PathUtil.join( + model + .getCurrentDir( + isLocal) + .path, + name.value.text, + model.getCurrentIsWindows( + isLocal)), + isLocal: isLocal); + close(); + } + }, + child: Text(translate("OK"))) + ])); + }, + icon: Icon(Icons.create_new_folder_outlined)), + IconButton( + onPressed: () async { + final items = isLocal + ? _localSelectedItems + : _remoteSelectedItems; + await (model.removeAction(items, isLocal: isLocal)); + items.clear(); + }, + icon: Icon(Icons.delete_forever_outlined)), + ], + ), + ), + TextButton.icon( + onPressed: () { + final items = getSelectedItem(isLocal); + model.sendFiles(items, isRemote: !isLocal); + items.clear(); + }, + icon: Transform.rotate( + angle: isLocal ? 0 : pi, + child: Icon( + Icons.send, + ), + ), + label: Text( + isLocal ? translate('Send') : translate('Receive'), + )), + ], + ).marginOnly(top: 8.0) + ], + )); + } + + Widget listTail({bool isLocal = false}) { + final dir = isLocal ? model.currentLocalDir : model.currentRemoteDir; + return Container( + height: 100, + child: Column( + children: [ + Padding( + padding: EdgeInsets.fromLTRB(30, 5, 30, 0), + child: Text( + dir.path, + style: TextStyle(color: MyTheme.darkGray), + ), + ), + Padding( + padding: EdgeInsets.all(2), + child: Text( + "${translate("Total")}: ${dir.entries.length} ${translate("items")}", + style: TextStyle(color: MyTheme.darkGray), + ), + ) + ], + ), + ); + } + + @override + bool get wantKeepAlive => true; + + /// Get the image for the current [platform]. + Widget getPlatformImage(String platform) { + platform = platform.toLowerCase(); + if (platform == 'mac os') + platform = 'mac'; + else if (platform != 'linux' && platform != 'android') platform = 'win'; + return Image.asset('assets/$platform.png', width: 25, height: 25); + } + + void onLocalLocationFocusChanged() { + debugPrint("focus changed on local"); + if (_locationNodeLocal.hasFocus) { + // ignore + } else { + // lost focus, change to bread + _locationStatusLocal.value = LocationStatus.bread; + } + } + + void onRemoteLocationFocusChanged() { + debugPrint("focus changed on remote"); + if (_locationNodeRemote.hasFocus) { + // ignore + } else { + // lost focus, change to bread + _locationStatusRemote.value = LocationStatus.bread; + } + } + + Widget buildBread(bool isLocal) { + final items = getPathBreadCrumbItems(isLocal, (list) { + var path = ""; + for (var item in list) { + path = PathUtil.join(path, item, model.getCurrentIsWindows(isLocal)); + } + openDirectory(path, isLocal: isLocal); + }); + return items.isEmpty + ? Offstage() + : BreadCrumb( + items: items, + divider: Text("/").paddingSymmetric(horizontal: 4.0), + overflow: ScrollableOverflow( + controller: getBreadCrumbScrollController(isLocal)), + ); + } + + List getPathBreadCrumbItems( + bool isLocal, void Function(List) onPressed) { + final path = model.getCurrentDir(isLocal).path; + final list = PathUtil.split(path, model.getCurrentIsWindows(isLocal)); + final breadCrumbList = List.empty(growable: true); + breadCrumbList.addAll(list.asMap().entries.map((e) => BreadCrumbItem( + content: TextButton( + child: Text(e.value), + style: + ButtonStyle(minimumSize: MaterialStateProperty.all(Size(0, 0))), + onPressed: () => onPressed(list.sublist(0, e.key + 1)))))); + return breadCrumbList; + } + + breadCrumbScrollToEnd(bool isLocal) { + Future.delayed(Duration(milliseconds: 200), () { + final _breadCrumbScroller = getBreadCrumbScrollController(isLocal); + _breadCrumbScroller.animateTo( + _breadCrumbScroller.position.maxScrollExtent, + duration: Duration(milliseconds: 200), + curve: Curves.fastLinearToSlowEaseIn); + }); + } + + Widget buildPathLocation(bool isLocal) { + return TextField( + focusNode: isLocal ? _locationNodeLocal : _locationNodeRemote, + decoration: InputDecoration( + border: InputBorder.none, + isDense: true, + prefix: Padding(padding: EdgeInsets.only(left: 4.0)), + ), + controller: + TextEditingController(text: model.getCurrentDir(isLocal).path), + onSubmitted: (path) { + openDirectory(path, isLocal: isLocal); + }, + ); + } + + onSearchText(String searchText, bool isLocal) { + if (isLocal) { + _searchTextLocal.value = searchText; + } else { + _searchTextRemote.value = searchText; + } + } + + openDirectory(String path, {bool isLocal = false}) { + model.openDirectory(path, isLocal: isLocal).then((_) { + print("scroll"); + breadCrumbScrollToEnd(isLocal); + }); + } + + void handleDragDone(DropDoneDetails details, bool isLocal) { + if (isLocal) { + // ignore local + return; + } + var items = SelectedItems(); + details.files.forEach((file) { + 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); + } +} diff --git a/flutter/lib/desktop/pages/file_manager_tab_page.dart b/flutter/lib/desktop/pages/file_manager_tab_page.dart new file mode 100644 index 000000000..09577128f --- /dev/null +++ b/flutter/lib/desktop/pages/file_manager_tab_page.dart @@ -0,0 +1,102 @@ +import 'dart:convert'; + +import 'package:desktop_multi_window/desktop_multi_window.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_hbb/common.dart'; +import 'package:flutter_hbb/desktop/pages/file_manager_page.dart'; +import 'package:flutter_hbb/desktop/widgets/tabbar_widget.dart'; +import 'package:flutter_hbb/models/model.dart'; +import 'package:flutter_hbb/utils/multi_window_manager.dart'; +import 'package:get/get.dart'; + +/// File Transfer for multi tabs +class FileManagerTabPage extends StatefulWidget { + final Map params; + + const FileManagerTabPage({Key? key, required this.params}) : super(key: key); + + @override + State createState() => _FileManagerTabPageState(params); +} + +class _FileManagerTabPageState extends State { + final tabController = Get.put(DesktopTabController()); + + static final IconData selectedIcon = Icons.file_copy_sharp; + static final IconData unselectedIcon = Icons.file_copy_outlined; + + _FileManagerTabPageState(Map params) { + tabController.add(TabInfo( + key: params['id'], + label: params['id'], + selectedIcon: selectedIcon, + unselectedIcon: unselectedIcon, + page: FileManagerPage(key: ValueKey(params['id']), id: params['id']))); + } + + @override + void initState() { + super.initState(); + + tabController.onRemove = (_, id) => onRemoveId(id); + + rustDeskWinManager.setMethodHandler((call, fromWindowId) async { + print( + "call ${call.method} with args ${call.arguments} from window ${fromWindowId}"); + // for simplify, just replace connectionId + if (call.method == "new_file_transfer") { + final args = jsonDecode(call.arguments); + final id = args['id']; + window_on_top(windowId()); + tabController.add(TabInfo( + key: id, + label: id, + selectedIcon: selectedIcon, + unselectedIcon: unselectedIcon, + page: FileManagerPage(key: ValueKey(id), id: id))); + } else if (call.method == "onDestroy") { + tabController.state.value.tabs.forEach((tab) { + print("executing onDestroy hook, closing ${tab.label}}"); + final tag = tab.label; + ffi(tag).close().then((_) { + Get.delete(tag: tag); + }); + }); + Get.back(); + } + }); + } + + @override + Widget build(BuildContext context) { + final theme = isDarkTheme() ? TarBarTheme.dark() : TarBarTheme.light(); + return SubWindowDragToResizeArea( + windowId: windowId(), + child: Container( + decoration: BoxDecoration( + border: Border.all(color: MyTheme.color(context).border!)), + child: Scaffold( + backgroundColor: MyTheme.color(context).bg, + body: DesktopTab( + controller: tabController, + theme: theme, + isMainWindow: false, + tail: AddButton( + theme: theme, + ).paddingOnly(left: 10), + )), + ), + ); + } + + void onRemoveId(String id) { + ffi("ft_$id").close(); + if (tabController.state.value.tabs.length == 0) { + WindowController.fromWindowId(windowId()).close(); + } + } + + int windowId() { + return widget.params["windowId"]; + } +} diff --git a/flutter/lib/desktop/pages/remote_page.dart b/flutter/lib/desktop/pages/remote_page.dart new file mode 100644 index 000000000..025db279f --- /dev/null +++ b/flutter/lib/desktop/pages/remote_page.dart @@ -0,0 +1,1254 @@ +import 'dart:async'; +import 'dart:io'; +import 'dart:ui' as ui; + +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_hbb/models/chat_model.dart'; +import 'package:get/get.dart'; +import 'package:provider/provider.dart'; +import 'package:wakelock/wakelock.dart'; + +// import 'package:window_manager/window_manager.dart'; + +import '../../common.dart'; +import '../../mobile/widgets/dialog.dart'; +import '../../mobile/widgets/overlay.dart'; +import '../../models/model.dart'; +import '../../models/platform_model.dart'; + +final initText = '\1' * 1024; + +class RemotePage extends StatefulWidget { + RemotePage( + {Key? key, + required this.id, + required this.tabBarHeight, + required this.fullscreenID}) + : super(key: key); + + final String id; + final double tabBarHeight; + final Rx fullscreenID; + + @override + _RemotePageState createState() => _RemotePageState(); +} + +class _RemotePageState extends State + with AutomaticKeepAliveClientMixin { + Timer? _timer; + bool _showBar = !isWebDesktop; + String _value = ''; + var _cursorOverImage = false.obs; + + final FocusNode _mobileFocusNode = FocusNode(); + final FocusNode _physicalFocusNode = FocusNode(); + var _isPhysicalMouse = false; + var _imageFocused = false; + + late FFI _ffi; + + @override + void initState() { + super.initState(); + _ffi = FFI(); + _ffi.canvasModel.tabBarHeight = super.widget.tabBarHeight; + Get.put(_ffi, tag: widget.id); + _ffi.connect(widget.id, tabBarHeight: super.widget.tabBarHeight); + WidgetsBinding.instance.addPostFrameCallback((_) { + SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, overlays: []); + _ffi.dialogManager + .showLoading(translate('Connecting...'), onCancel: closeConnection); + }); + if (!Platform.isLinux) { + Wakelock.enable(); + } + _physicalFocusNode.requestFocus(); + _ffi.ffiModel.updateEventListener(widget.id); + _ffi.listenToMouse(true); + _ffi.qualityMonitorModel.checkShowQualityMonitor(widget.id); + // WindowManager.instance.addListener(this); + } + + @override + void dispose() { + print("REMOTE PAGE dispose ${widget.id}"); + hideMobileActionsOverlay(); + _ffi.listenToMouse(false); + _mobileFocusNode.dispose(); + _physicalFocusNode.dispose(); + _ffi.close(); + _timer?.cancel(); + _ffi.dialogManager.dismissAll(); + SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, + overlays: SystemUiOverlay.values); + if (!Platform.isLinux) { + Wakelock.disable(); + } + // WindowManager.instance.removeListener(this); + Get.delete(tag: widget.id); + super.dispose(); + } + + void resetTool() { + _ffi.resetModifiers(); + } + + // handle mobile virtual keyboard + void handleInput(String newValue) { + var oldValue = _value; + _value = newValue; + if (isIOS) { + var i = newValue.length - 1; + for (; i >= 0 && newValue[i] != '\1'; --i) {} + var j = oldValue.length - 1; + for (; j >= 0 && oldValue[j] != '\1'; --j) {} + if (i < j) j = i; + newValue = newValue.substring(j + 1); + oldValue = oldValue.substring(j + 1); + var common = 0; + for (; + common < oldValue.length && + common < newValue.length && + newValue[common] == oldValue[common]; + ++common); + for (i = 0; i < oldValue.length - common; ++i) { + _ffi.inputKey('VK_BACK'); + } + if (newValue.length > common) { + var s = newValue.substring(common); + if (s.length > 1) { + bind.sessionInputString(id: widget.id, value: s); + } else { + inputChar(s); + } + } + return; + } + if (oldValue.length > 0 && + newValue.length > 0 && + oldValue[0] == '\1' && + newValue[0] != '\1') { + // clipboard + oldValue = ''; + } + if (newValue.length == oldValue.length) { + // ? + } else if (newValue.length < oldValue.length) { + final char = 'VK_BACK'; + _ffi.inputKey(char); + } else { + final content = newValue.substring(oldValue.length); + if (content.length > 1) { + if (oldValue != '' && + content.length == 2 && + (content == '""' || + content == '()' || + content == '[]' || + content == '<>' || + content == "{}" || + content == '”“' || + content == '《》' || + content == '()' || + content == '【】')) { + // can not only input content[0], because when input ], [ are also auo insert, which cause ] never be input + bind.sessionInputString(id: widget.id, value: content); + return; + } + bind.sessionInputString(id: widget.id, value: content); + } else { + inputChar(content); + } + } + } + + void inputChar(String char) { + if (char == '\n') { + char = 'VK_RETURN'; + } else if (char == ' ') { + char = 'VK_SPACE'; + } + _ffi.inputKey(char); + } + + void sendRawKey(RawKeyEvent e, {bool? down, bool? press}) { + // for maximum compatibility + final label = _logicalKeyMap[e.logicalKey.keyId] ?? + _physicalKeyMap[e.physicalKey.usbHidUsage] ?? + e.logicalKey.keyLabel; + _ffi.inputKey(label, down: down, press: press ?? false); + } + + Widget buildBody(BuildContext context, FfiModel ffiModel) { + final hasDisplays = ffiModel.pi.displays.length > 0; + final keyboard = ffiModel.permissions['keyboard'] != false; + return Scaffold( + backgroundColor: MyTheme.color(context).bg, + // resizeToAvoidBottomInset: true, + floatingActionButton: _showBar + ? null + : FloatingActionButton( + mini: true, + child: Icon(Icons.expand_less), + backgroundColor: MyTheme.accent, + onPressed: () { + setState(() { + _showBar = !_showBar; + }); + }), + bottomNavigationBar: + _showBar && hasDisplays ? getBottomAppBar(ffiModel) : null, + body: Overlay( + initialEntries: [ + OverlayEntry(builder: (context) { + _ffi.chatModel.setOverlayState(Overlay.of(context)); + _ffi.dialogManager.setOverlayState(Overlay.of(context)); + return Container( + color: Colors.black, + child: getRawPointerAndKeyBody( + getBodyForDesktop(context, keyboard))); + }) + ], + )); + } + + @override + Widget build(BuildContext context) { + super.build(context); + return WillPopScope( + onWillPop: () async { + clientClose(_ffi.dialogManager); + return false; + }, + child: MultiProvider( + providers: [ + ChangeNotifierProvider.value(value: _ffi.ffiModel), + ChangeNotifierProvider.value(value: _ffi.imageModel), + ChangeNotifierProvider.value(value: _ffi.cursorModel), + ChangeNotifierProvider.value(value: _ffi.canvasModel), + ], + child: Consumer( + builder: (context, ffiModel, _child) => + buildBody(context, ffiModel)))); + } + + Widget getRawPointerAndKeyBody(Widget child) { + return Consumer( + builder: (context, FfiModel, _child) => MouseRegion( + cursor: FfiModel.permissions['keyboard'] != false + ? SystemMouseCursors.none + : MouseCursor.defer, + child: FocusScope( + autofocus: true, + child: Focus( + autofocus: true, + canRequestFocus: true, + focusNode: _physicalFocusNode, + onFocusChange: (bool v) { + _imageFocused = v; + }, + onKey: (data, e) { + final key = e.logicalKey; + if (e is RawKeyDownEvent) { + if (e.repeat) { + sendRawKey(e, press: true); + } else { + if (e.isAltPressed && !_ffi.alt) { + _ffi.alt = true; + } else if (e.isControlPressed && !_ffi.ctrl) { + _ffi.ctrl = true; + } else if (e.isShiftPressed && !_ffi.shift) { + _ffi.shift = true; + } else if (e.isMetaPressed && !_ffi.command) { + _ffi.command = true; + } + sendRawKey(e, down: true); + } + } + if (e is RawKeyUpEvent) { + if (key == LogicalKeyboardKey.altLeft || + key == LogicalKeyboardKey.altRight) { + _ffi.alt = false; + } else if (key == LogicalKeyboardKey.controlLeft || + key == LogicalKeyboardKey.controlRight) { + _ffi.ctrl = false; + } else if (key == LogicalKeyboardKey.shiftRight || + key == LogicalKeyboardKey.shiftLeft) { + _ffi.shift = false; + } else if (key == LogicalKeyboardKey.metaLeft || + key == LogicalKeyboardKey.metaRight) { + _ffi.command = false; + } + sendRawKey(e); + } + return KeyEventResult.handled; + }, + child: child)))); + } + + Widget? getBottomAppBar(FfiModel ffiModel) { + return MouseRegion( + cursor: SystemMouseCursors.basic, + child: BottomAppBar( + elevation: 10, + color: MyTheme.accent, + child: Row( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + IconButton( + color: Colors.white, + icon: Icon(Icons.clear), + onPressed: () { + clientClose(_ffi.dialogManager); + }, + ) + ] + + [ + IconButton( + color: Colors.white, + icon: Icon(Icons.tv), + onPressed: () { + _ffi.dialogManager.dismissAll(); + showOptions(widget.id); + }, + ) + ] + + (isWebDesktop + ? [] + : [ + IconButton( + color: Colors.white, + icon: Icon(widget.fullscreenID.value.isEmpty + ? Icons.fullscreen + : Icons.close_fullscreen), + onPressed: () { + if (widget.fullscreenID.value.isEmpty) { + widget.fullscreenID.value = widget.id; + } else { + widget.fullscreenID.value = ""; + } + }, + ) + ]) + + (isWebDesktop + ? [] + : _ffi.ffiModel.isPeerAndroid + ? [ + IconButton( + color: Colors.white, + icon: Icon(Icons.build), + onPressed: () { + if (mobileActionsOverlayEntry == null) { + showMobileActionsOverlay(); + } else { + hideMobileActionsOverlay(); + } + }, + ) + ] + : []) + + (isWeb + ? [] + : [ + IconButton( + color: Colors.white, + icon: Icon(Icons.message), + onPressed: () { + _ffi.chatModel + .changeCurrentID(ChatModel.clientModeID); + _ffi.chatModel.toggleChatOverlay(); + }, + ) + ]) + + [ + IconButton( + color: Colors.white, + icon: Icon(Icons.more_vert), + onPressed: () { + showActions(widget.id, ffiModel); + }, + ), + ]), + IconButton( + color: Colors.white, + icon: Icon(Icons.expand_more), + onPressed: () { + setState(() => _showBar = !_showBar); + }), + ], + ), + )); + } + + /// touchMode only: + /// LongPress -> right click + /// OneFingerPan -> start/end -> left down start/end + /// onDoubleTapDown -> move to + /// onLongPressDown => move to + /// + /// mouseMode only: + /// DoubleFiner -> right click + /// HoldDrag -> left drag + + void _onPointHoverImage(PointerHoverEvent e) { + if (e.kind != ui.PointerDeviceKind.mouse) return; + if (!_isPhysicalMouse) { + setState(() { + _isPhysicalMouse = true; + }); + } + if (_isPhysicalMouse) { + _ffi.handleMouse(getEvent(e, 'mousemove'), + tabBarHeight: super.widget.tabBarHeight); + } + } + + void _onPointDownImage(PointerDownEvent e) { + if (e.kind != ui.PointerDeviceKind.mouse) { + if (_isPhysicalMouse) { + setState(() { + _isPhysicalMouse = false; + }); + } + } + if (_isPhysicalMouse) { + _ffi.handleMouse(getEvent(e, 'mousedown'), + tabBarHeight: super.widget.tabBarHeight); + } + } + + void _onPointUpImage(PointerUpEvent e) { + if (e.kind != ui.PointerDeviceKind.mouse) return; + if (_isPhysicalMouse) { + _ffi.handleMouse(getEvent(e, 'mouseup'), + tabBarHeight: super.widget.tabBarHeight); + } + } + + void _onPointMoveImage(PointerMoveEvent e) { + if (e.kind != ui.PointerDeviceKind.mouse) return; + if (_isPhysicalMouse) { + _ffi.handleMouse(getEvent(e, 'mousemove'), + tabBarHeight: super.widget.tabBarHeight); + } + } + + void _onPointerSignalImage(PointerSignalEvent e) { + if (e is PointerScrollEvent) { + var dx = e.scrollDelta.dx.toInt(); + var dy = e.scrollDelta.dy.toInt(); + if (dx > 0) + dx = -1; + else if (dx < 0) dx = 1; + if (dy > 0) + dy = -1; + else if (dy < 0) dy = 1; + bind.sessionSendMouse( + id: widget.id, msg: '{"type": "wheel", "x": "$dx", "y": "$dy"}'); + } + } + + Widget _buildImageListener(Widget child) { + return Listener( + onPointerHover: _onPointHoverImage, + onPointerDown: _onPointDownImage, + onPointerUp: _onPointUpImage, + onPointerMove: _onPointMoveImage, + onPointerSignal: _onPointerSignalImage, + child: MouseRegion( + onEnter: (evt) { + if (!_imageFocused) { + _physicalFocusNode.requestFocus(); + } + _cursorOverImage.value = true; + }, + onExit: (evt) { + _cursorOverImage.value = false; + }, + child: child)); + } + + Widget getBodyForDesktop(BuildContext context, bool keyboard) { + var paints = [ + MouseRegion(onEnter: (evt) { + bind.hostStopSystemKeyPropagate(stopped: false); + }, onExit: (evt) { + bind.hostStopSystemKeyPropagate(stopped: true); + }, child: Container( + child: LayoutBuilder(builder: (context, constraints) { + Future.delayed(Duration.zero, () { + Provider.of(context, listen: false).updateViewStyle(); + }); + return ImagePaint( + id: widget.id, + cursorOverImage: _cursorOverImage, + listenerBuilder: _buildImageListener, + ); + }), + )) + ]; + final cursor = bind.sessionGetToggleOptionSync( + id: widget.id, arg: 'show-remote-cursor'); + if (keyboard || cursor) { + paints.add(CursorPaint( + id: widget.id, + )); + } + paints.add(QualityMonitor(_ffi.qualityMonitorModel)); + return Stack( + children: paints, + ); + } + + int lastMouseDownButtons = 0; + + Map getEvent(PointerEvent evt, String type) { + final Map out = {}; + out['type'] = type; + out['x'] = evt.position.dx; + out['y'] = evt.position.dy; + if (_ffi.alt) out['alt'] = 'true'; + if (_ffi.shift) out['shift'] = 'true'; + if (_ffi.ctrl) out['ctrl'] = 'true'; + if (_ffi.command) out['command'] = 'true'; + out['buttons'] = evt + .buttons; // left button: 1, right button: 2, middle button: 4, 1 | 2 = 3 (left + right) + if (evt.buttons != 0) { + lastMouseDownButtons = evt.buttons; + } else { + out['buttons'] = lastMouseDownButtons; + } + return out; + } + + void showActions(String id, FfiModel ffiModel) async { + final size = MediaQuery.of(context).size; + final x = 120.0; + final y = size.height - super.widget.tabBarHeight; + final more = >[]; + final pi = _ffi.ffiModel.pi; + final perms = _ffi.ffiModel.permissions; + if (pi.version.isNotEmpty) { + more.add(PopupMenuItem( + child: Text(translate('Refresh')), value: 'refresh')); + } + more.add(PopupMenuItem( + child: Row( + children: ([ + Text(translate('OS Password')), + TextButton( + style: flatButtonStyle, + onPressed: () { + showSetOSPassword(widget.id, false, _ffi.dialogManager); + }, + child: Icon(Icons.edit, color: MyTheme.accent), + ) + ])), + value: 'enter_os_password')); + if (!isWebDesktop) { + if (perms['keyboard'] != false && perms['clipboard'] != false) { + more.add(PopupMenuItem( + child: Text(translate('Paste')), value: 'paste')); + } + more.add(PopupMenuItem( + child: Text(translate('Reset canvas')), value: 'reset_canvas')); + } + if (perms['keyboard'] != false) { + if (pi.platform == 'Linux' || pi.sasEnabled) { + more.add(PopupMenuItem( + child: Text(translate('Insert') + ' Ctrl + Alt + Del'), + value: 'cad')); + } + more.add(PopupMenuItem( + child: Text(translate('Insert Lock')), value: 'lock')); + if (pi.platform == 'Windows' && + await bind.sessionGetToggleOption(id: id, arg: 'privacy-mode') != + true) { + more.add(PopupMenuItem( + child: Text(translate( + (ffiModel.inputBlocked ? 'Unb' : 'B') + 'lock user input')), + value: 'block-input')); + } + } + if (gFFI.ffiModel.permissions["restart"] != false && + (pi.platform == "Linux" || + pi.platform == "Windows" || + pi.platform == "Mac OS")) { + more.add(PopupMenuItem( + child: Text(translate('Restart Remote Device')), value: 'restart')); + } + () async { + var value = await showMenu( + context: context, + position: RelativeRect.fromLTRB(x, y, x, y), + items: more, + elevation: 8, + ); + if (value == 'cad') { + bind.sessionCtrlAltDel(id: widget.id); + } else if (value == 'lock') { + bind.sessionLockScreen(id: widget.id); + } else if (value == 'block-input') { + bind.sessionToggleOption( + id: widget.id, + value: (_ffi.ffiModel.inputBlocked ? 'un' : '') + 'block-input'); + _ffi.ffiModel.inputBlocked = !_ffi.ffiModel.inputBlocked; + } else if (value == 'refresh') { + bind.sessionRefresh(id: widget.id); + } else if (value == 'paste') { + () async { + ClipboardData? data = await Clipboard.getData(Clipboard.kTextPlain); + if (data != null && data.text != null) { + bind.sessionInputString(id: widget.id, value: data.text ?? ""); + } + }(); + } else if (value == 'enter_os_password') { + // FIXME: + // TODO icon diff + // null means no session of id + // empty string means no password + var password = await bind.sessionGetOption(id: id, arg: "os-password"); + if (password != null) { + bind.sessionInputOsPassword(id: widget.id, value: password); + } else { + showSetOSPassword(widget.id, true, _ffi.dialogManager); + } + } else if (value == 'reset_canvas') { + _ffi.cursorModel.reset(); + } else if (value == 'restart') { + showRestartRemoteDevice(pi, widget.id, gFFI.dialogManager); + } + }(); + } + + @override + void onWindowEvent(String eventName) { + print("window event: $eventName"); + switch (eventName) { + case 'resize': + _ffi.canvasModel.updateViewStyle(); + break; + case 'maximize': + Future.delayed(Duration(milliseconds: 100), () { + _ffi.canvasModel.updateViewStyle(); + }); + break; + } + } + + @override + bool get wantKeepAlive => true; +} + +class ImagePaint extends StatelessWidget { + final String id; + final Rx cursorOverImage; + final Widget Function(Widget)? listenerBuilder; + final ScrollController _horizontal = ScrollController(); + final ScrollController _vertical = ScrollController(); + + ImagePaint( + {Key? key, + required this.id, + required this.cursorOverImage, + this.listenerBuilder = null}) + : super(key: key); + + @override + Widget build(BuildContext context) { + final m = Provider.of(context); + var c = Provider.of(context); + final s = c.scale; + if (c.scrollStyle == ScrollStyle.scrollbar) { + final imageWidget = SizedBox( + width: c.getDisplayWidth() * s, + height: c.getDisplayHeight() * s, + child: CustomPaint( + painter: new ImagePainter(image: m.image, x: 0, y: 0, scale: s), + )); + return Center( + child: NotificationListener( + onNotification: (_notification) { + final percentX = _horizontal.position.extentBefore / + (_horizontal.position.extentBefore + + _horizontal.position.extentInside + + _horizontal.position.extentAfter); + final percentY = _vertical.position.extentBefore / + (_vertical.position.extentBefore + + _vertical.position.extentInside + + _vertical.position.extentAfter); + c.setScrollPercent(percentX, percentY); + return false; + }, + child: Obx(() => MouseRegion( + cursor: cursorOverImage.value + ? SystemMouseCursors.none + : SystemMouseCursors.basic, + child: _buildCrossScrollbar(_buildListener(imageWidget)))), + )); + } else { + final imageWidget = SizedBox( + width: c.size.width, + height: c.size.height, + child: CustomPaint( + painter: new ImagePainter( + image: m.image, x: c.x / s, y: c.y / s, scale: s), + )); + return _buildListener(imageWidget); + } + } + + Widget _buildCrossScrollbar(Widget child) { + final physicsVertical = + cursorOverImage.value ? const NeverScrollableScrollPhysics() : null; + final physicsHorizontal = + cursorOverImage.value ? const NeverScrollableScrollPhysics() : null; + return Scrollbar( + controller: _vertical, + thumbVisibility: true, + trackVisibility: true, + child: Scrollbar( + controller: _horizontal, + thumbVisibility: true, + trackVisibility: true, + notificationPredicate: (notif) => notif.depth == 1, + child: SingleChildScrollView( + controller: _vertical, + physics: physicsVertical, + child: SingleChildScrollView( + controller: _horizontal, + scrollDirection: Axis.horizontal, + physics: physicsHorizontal, + child: child, + ), + ), + )); + } + + Widget _buildListener(Widget child) { + if (listenerBuilder != null) { + return listenerBuilder!(child); + } else { + return child; + } + } +} + +class CursorPaint extends StatelessWidget { + final String id; + + const CursorPaint({Key? key, required this.id}) : super(key: key); + + @override + Widget build(BuildContext context) { + final m = Provider.of(context); + final c = Provider.of(context); + // final adjust = m.adjustForKeyboard(); + var s = c.scale; + return CustomPaint( + painter: new ImagePainter( + image: m.image, + x: m.x * s - m.hotx + c.x, + y: m.y * s - m.hoty + c.y, + scale: 1), + ); + } +} + +class ImagePainter extends CustomPainter { + ImagePainter({ + required this.image, + required this.x, + required this.y, + required this.scale, + }); + + ui.Image? image; + double x; + double y; + double scale; + + @override + void paint(Canvas canvas, Size size) { + if (image == null) return; + canvas.scale(scale, scale); + // https://github.com/flutter/flutter/issues/76187#issuecomment-784628161 + // https://api.flutter-io.cn/flutter/dart-ui/FilterQuality.html + var paint = new Paint(); + paint.filterQuality = FilterQuality.medium; + if (scale > 10.00000) { + paint.filterQuality = FilterQuality.high; + } + canvas.drawImage(image!, new Offset(x, y), paint); + } + + @override + bool shouldRepaint(CustomPainter oldDelegate) { + return oldDelegate != this; + } +} + +class QualityMonitor extends StatelessWidget { + final QualityMonitorModel qualityMonitorModel; + QualityMonitor(this.qualityMonitorModel); + + @override + Widget build(BuildContext context) => ChangeNotifierProvider.value( + value: qualityMonitorModel, + child: Consumer( + builder: (context, qualityMonitorModel, child) => Positioned( + top: 10, + right: 10, + child: qualityMonitorModel.show + ? Container( + padding: EdgeInsets.all(8), + color: MyTheme.canvasColor.withAlpha(120), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Speed: ${qualityMonitorModel.data.speed ?? ''}", + style: TextStyle(color: MyTheme.grayBg), + ), + Text( + "FPS: ${qualityMonitorModel.data.fps ?? ''}", + style: TextStyle(color: MyTheme.grayBg), + ), + Text( + "Delay: ${qualityMonitorModel.data.delay ?? ''} ms", + style: TextStyle(color: MyTheme.grayBg), + ), + Text( + "Target Bitrate: ${qualityMonitorModel.data.targetBitrate ?? ''}kb", + style: TextStyle(color: MyTheme.grayBg), + ), + Text( + "Codec: ${qualityMonitorModel.data.codecFormat ?? ''}", + style: TextStyle(color: MyTheme.grayBg), + ), + ], + ), + ) + : SizedBox.shrink()))); +} + +void showOptions(String id) async { + final _ffi = ffi(id); + String quality = await bind.sessionGetImageQuality(id: id) ?? 'balanced'; + if (quality == '') quality = 'balanced'; + String viewStyle = + await bind.sessionGetOption(id: id, arg: 'view-style') ?? ''; + String scrollStyle = + await bind.sessionGetOption(id: id, arg: 'scroll-style') ?? ''; + var displays = []; + final pi = _ffi.ffiModel.pi; + final image = _ffi.ffiModel.getConnectionImage(); + if (image != null) + displays.add(Padding(padding: const EdgeInsets.only(top: 8), child: image)); + if (pi.displays.length > 1) { + final cur = pi.currentDisplay; + final children = []; + for (var i = 0; i < pi.displays.length; ++i) + children.add(InkWell( + onTap: () { + if (i == cur) return; + bind.sessionSwitchDisplay(id: id, value: i); + _ffi.dialogManager.dismissAll(); + }, + child: Ink( + width: 40, + height: 40, + decoration: BoxDecoration( + border: Border.all(color: Colors.black87), + color: i == cur ? Colors.black87 : Colors.white), + child: Center( + child: Text((i + 1).toString(), + style: TextStyle( + color: i == cur ? Colors.white : Colors.black87)))))); + displays.add(Padding( + padding: const EdgeInsets.only(top: 8), + child: Wrap( + alignment: WrapAlignment.center, + spacing: 8, + children: children, + ))); + } + if (displays.isNotEmpty) { + displays.add(Divider(color: MyTheme.border)); + } + final perms = _ffi.ffiModel.permissions; + + _ffi.dialogManager.show((setState, close) { + final more = []; + if (perms['audio'] != false) { + more.add(getToggle(id, setState, 'disable-audio', 'Mute')); + } + if (perms['keyboard'] != false) { + if (perms['clipboard'] != false) + more.add( + getToggle(id, setState, 'disable-clipboard', 'Disable clipboard')); + more.add(getToggle( + id, setState, 'lock-after-session-end', 'Lock after session end')); + if (pi.platform == 'Windows') { + more.add(Consumer( + builder: (_context, _ffiModel, _child) => () { + return getToggle( + id, setState, 'privacy-mode', 'Privacy mode'); + }())); + } + } + var setQuality = (String? value) { + if (value == null) return; + setState(() { + quality = value; + bind.sessionSetImageQuality(id: id, value: value); + }); + }; + var setViewStyle = (String? value) { + if (value == null) return; + setState(() { + viewStyle = value; + bind.sessionPeerOption(id: id, name: "view-style", value: value); + _ffi.canvasModel.updateViewStyle(); + }); + }; + var setScrollStyle = (String? value) { + if (value == null) return; + setState(() { + scrollStyle = value; + bind.sessionPeerOption(id: id, name: "scroll-style", value: value); + _ffi.canvasModel.updateScrollStyle(); + }); + }; + return CustomAlertDialog( + title: SizedBox.shrink(), + content: Column( + mainAxisSize: MainAxisSize.min, + children: displays + + [ + getRadio('Original', 'original', viewStyle, setViewStyle), + getRadio('Shrink', 'shrink', viewStyle, setViewStyle), + getRadio('Stretch', 'stretch', viewStyle, setViewStyle), + Divider(color: MyTheme.border), + getRadio( + 'ScrollAuto', 'scrollauto', scrollStyle, setScrollStyle), + getRadio('Scrollbar', 'scrollbar', scrollStyle, setScrollStyle), + Divider(color: MyTheme.border), + getRadio('Good image quality', 'best', quality, setQuality), + getRadio('Balanced', 'balanced', quality, setQuality), + getRadio('Optimize reaction time', 'low', quality, setQuality), + Divider(color: MyTheme.border), + getToggle( + id, setState, 'show-remote-cursor', 'Show remote cursor'), + getToggle(id, setState, 'show-quality-monitor', + 'Show quality monitor', + ffi: _ffi), + ] + + more), + actions: [], + contentPadding: 0, + ); + }, clickMaskDismiss: true, backDismiss: true); +} + +void showSetOSPassword( + String id, bool login, OverlayDialogManager dialogManager) async { + final controller = TextEditingController(); + var password = await bind.sessionGetOption(id: id, arg: "os-password") ?? ""; + var autoLogin = await bind.sessionGetOption(id: id, arg: "auto-login") != ""; + controller.text = password; + dialogManager.show((setState, close) { + return CustomAlertDialog( + title: Text(translate('OS Password')), + content: Column(mainAxisSize: MainAxisSize.min, children: [ + PasswordWidget(controller: controller), + CheckboxListTile( + contentPadding: const EdgeInsets.all(0), + dense: true, + controlAffinity: ListTileControlAffinity.leading, + title: Text( + translate('Auto Login'), + ), + value: autoLogin, + onChanged: (v) { + if (v == null) return; + setState(() => autoLogin = v); + }, + ), + ]), + actions: [ + TextButton( + style: flatButtonStyle, + onPressed: () { + close(); + }, + child: Text(translate('Cancel')), + ), + TextButton( + style: flatButtonStyle, + onPressed: () { + var text = controller.text.trim(); + bind.sessionPeerOption(id: id, name: "os-password", value: text); + bind.sessionPeerOption( + id: id, name: "auto-login", value: autoLogin ? 'Y' : ''); + if (text != "" && login) { + bind.sessionInputOsPassword(id: id, value: text); + } + close(); + }, + child: Text(translate('OK')), + ), + ]); + }); +} + +void sendPrompt(String id, bool isMac, String key) { + FFI _ffi = ffi(id); + final old = isMac ? _ffi.command : _ffi.ctrl; + if (isMac) { + _ffi.command = true; + } else { + _ffi.ctrl = true; + } + _ffi.inputKey(key); + if (isMac) { + _ffi.command = old; + } else { + _ffi.ctrl = old; + } +} + +/// flutter/packages/flutter/lib/src/services/keyboard_key.dart -> _keyLabels +/// see [LogicalKeyboardKey.keyLabel] +const Map _logicalKeyMap = { + 0x00000000020: 'VK_SPACE', + 0x00000000022: 'VK_QUOTE', + 0x0000000002c: 'VK_COMMA', + 0x0000000002d: 'VK_MINUS', + 0x0000000002f: 'VK_SLASH', + 0x00000000030: 'VK_0', + 0x00000000031: 'VK_1', + 0x00000000032: 'VK_2', + 0x00000000033: 'VK_3', + 0x00000000034: 'VK_4', + 0x00000000035: 'VK_5', + 0x00000000036: 'VK_6', + 0x00000000037: 'VK_7', + 0x00000000038: 'VK_8', + 0x00000000039: 'VK_9', + 0x0000000003b: 'VK_SEMICOLON', + 0x0000000003d: 'VK_PLUS', // it is = + 0x0000000005b: 'VK_LBRACKET', + 0x0000000005c: 'VK_BACKSLASH', + 0x0000000005d: 'VK_RBRACKET', + 0x00000000061: 'VK_A', + 0x00000000062: 'VK_B', + 0x00000000063: 'VK_C', + 0x00000000064: 'VK_D', + 0x00000000065: 'VK_E', + 0x00000000066: 'VK_F', + 0x00000000067: 'VK_G', + 0x00000000068: 'VK_H', + 0x00000000069: 'VK_I', + 0x0000000006a: 'VK_J', + 0x0000000006b: 'VK_K', + 0x0000000006c: 'VK_L', + 0x0000000006d: 'VK_M', + 0x0000000006e: 'VK_N', + 0x0000000006f: 'VK_O', + 0x00000000070: 'VK_P', + 0x00000000071: 'VK_Q', + 0x00000000072: 'VK_R', + 0x00000000073: 'VK_S', + 0x00000000074: 'VK_T', + 0x00000000075: 'VK_U', + 0x00000000076: 'VK_V', + 0x00000000077: 'VK_W', + 0x00000000078: 'VK_X', + 0x00000000079: 'VK_Y', + 0x0000000007a: 'VK_Z', + 0x00100000008: 'VK_BACK', + 0x00100000009: 'VK_TAB', + 0x0010000000d: 'VK_ENTER', + 0x0010000001b: 'VK_ESCAPE', + 0x0010000007f: 'VK_DELETE', + 0x00100000104: 'VK_CAPITAL', + 0x00100000301: 'VK_DOWN', + 0x00100000302: 'VK_LEFT', + 0x00100000303: 'VK_RIGHT', + 0x00100000304: 'VK_UP', + 0x00100000305: 'VK_END', + 0x00100000306: 'VK_HOME', + 0x00100000307: 'VK_NEXT', + 0x00100000308: 'VK_PRIOR', + 0x00100000401: 'VK_CLEAR', + 0x00100000407: 'VK_INSERT', + 0x00100000504: 'VK_CANCEL', + 0x00100000506: 'VK_EXECUTE', + 0x00100000508: 'VK_HELP', + 0x00100000509: 'VK_PAUSE', + 0x0010000050c: 'VK_SELECT', + 0x00100000608: 'VK_PRINT', + 0x00100000705: 'VK_CONVERT', + 0x00100000706: 'VK_FINAL', + 0x00100000711: 'VK_HANGUL', + 0x00100000712: 'VK_HANJA', + 0x00100000713: 'VK_JUNJA', + 0x00100000718: 'VK_KANA', + 0x00100000719: 'VK_KANJI', + 0x00100000801: 'VK_F1', + 0x00100000802: 'VK_F2', + 0x00100000803: 'VK_F3', + 0x00100000804: 'VK_F4', + 0x00100000805: 'VK_F5', + 0x00100000806: 'VK_F6', + 0x00100000807: 'VK_F7', + 0x00100000808: 'VK_F8', + 0x00100000809: 'VK_F9', + 0x0010000080a: 'VK_F10', + 0x0010000080b: 'VK_F11', + 0x0010000080c: 'VK_F12', + 0x00100000d2b: 'Apps', + 0x00200000002: 'VK_SLEEP', + 0x00200000100: 'VK_CONTROL', + 0x00200000101: 'RControl', + 0x00200000102: 'VK_SHIFT', + 0x00200000103: 'RShift', + 0x00200000104: 'VK_MENU', + 0x00200000105: 'RAlt', + 0x002000001f0: 'VK_CONTROL', + 0x002000001f2: 'VK_SHIFT', + 0x002000001f4: 'VK_MENU', + 0x002000001f6: 'Meta', + 0x0020000022a: 'VK_MULTIPLY', + 0x0020000022b: 'VK_ADD', + 0x0020000022d: 'VK_SUBTRACT', + 0x0020000022e: 'VK_DECIMAL', + 0x0020000022f: 'VK_DIVIDE', + 0x00200000230: 'VK_NUMPAD0', + 0x00200000231: 'VK_NUMPAD1', + 0x00200000232: 'VK_NUMPAD2', + 0x00200000233: 'VK_NUMPAD3', + 0x00200000234: 'VK_NUMPAD4', + 0x00200000235: 'VK_NUMPAD5', + 0x00200000236: 'VK_NUMPAD6', + 0x00200000237: 'VK_NUMPAD7', + 0x00200000238: 'VK_NUMPAD8', + 0x00200000239: 'VK_NUMPAD9', +}; + +/// flutter/packages/flutter/lib/src/services/keyboard_key.dart -> _debugName +/// see [PhysicalKeyboardKey.debugName] -> _debugName +const Map _physicalKeyMap = { + 0x00010082: 'VK_SLEEP', + 0x00070004: 'VK_A', + 0x00070005: 'VK_B', + 0x00070006: 'VK_C', + 0x00070007: 'VK_D', + 0x00070008: 'VK_E', + 0x00070009: 'VK_F', + 0x0007000a: 'VK_G', + 0x0007000b: 'VK_H', + 0x0007000c: 'VK_I', + 0x0007000d: 'VK_J', + 0x0007000e: 'VK_K', + 0x0007000f: 'VK_L', + 0x00070010: 'VK_M', + 0x00070011: 'VK_N', + 0x00070012: 'VK_O', + 0x00070013: 'VK_P', + 0x00070014: 'VK_Q', + 0x00070015: 'VK_R', + 0x00070016: 'VK_S', + 0x00070017: 'VK_T', + 0x00070018: 'VK_U', + 0x00070019: 'VK_V', + 0x0007001a: 'VK_W', + 0x0007001b: 'VK_X', + 0x0007001c: 'VK_Y', + 0x0007001d: 'VK_Z', + 0x0007001e: 'VK_1', + 0x0007001f: 'VK_2', + 0x00070020: 'VK_3', + 0x00070021: 'VK_4', + 0x00070022: 'VK_5', + 0x00070023: 'VK_6', + 0x00070024: 'VK_7', + 0x00070025: 'VK_8', + 0x00070026: 'VK_9', + 0x00070027: 'VK_0', + 0x00070028: 'VK_ENTER', + 0x00070029: 'VK_ESCAPE', + 0x0007002a: 'VK_BACK', + 0x0007002b: 'VK_TAB', + 0x0007002c: 'VK_SPACE', + 0x0007002d: 'VK_MINUS', + 0x0007002e: 'VK_PLUS', // it is = + 0x0007002f: 'VK_LBRACKET', + 0x00070030: 'VK_RBRACKET', + 0x00070033: 'VK_SEMICOLON', + 0x00070034: 'VK_QUOTE', + 0x00070036: 'VK_COMMA', + 0x00070038: 'VK_SLASH', + 0x00070039: 'VK_CAPITAL', + 0x0007003a: 'VK_F1', + 0x0007003b: 'VK_F2', + 0x0007003c: 'VK_F3', + 0x0007003d: 'VK_F4', + 0x0007003e: 'VK_F5', + 0x0007003f: 'VK_F6', + 0x00070040: 'VK_F7', + 0x00070041: 'VK_F8', + 0x00070042: 'VK_F9', + 0x00070043: 'VK_F10', + 0x00070044: 'VK_F11', + 0x00070045: 'VK_F12', + 0x00070049: 'VK_INSERT', + 0x0007004a: 'VK_HOME', + 0x0007004b: 'VK_PRIOR', // Page Up + 0x0007004c: 'VK_DELETE', + 0x0007004d: 'VK_END', + 0x0007004e: 'VK_NEXT', // Page Down + 0x0007004f: 'VK_RIGHT', + 0x00070050: 'VK_LEFT', + 0x00070051: 'VK_DOWN', + 0x00070052: 'VK_UP', + 0x00070053: 'Num Lock', // TODO rust not impl + 0x00070054: 'VK_DIVIDE', // numpad + 0x00070055: 'VK_MULTIPLY', + 0x00070056: 'VK_SUBTRACT', + 0x00070057: 'VK_ADD', + 0x00070058: 'VK_ENTER', // num enter + 0x00070059: 'VK_NUMPAD0', + 0x0007005a: 'VK_NUMPAD1', + 0x0007005b: 'VK_NUMPAD2', + 0x0007005c: 'VK_NUMPAD3', + 0x0007005d: 'VK_NUMPAD4', + 0x0007005e: 'VK_NUMPAD5', + 0x0007005f: 'VK_NUMPAD6', + 0x00070060: 'VK_NUMPAD7', + 0x00070061: 'VK_NUMPAD8', + 0x00070062: 'VK_NUMPAD9', + 0x00070063: 'VK_DECIMAL', + 0x00070075: 'VK_HELP', + 0x00070077: 'VK_SELECT', + 0x00070088: 'VK_KANA', + 0x0007008a: 'VK_CONVERT', + 0x000700e0: 'VK_CONTROL', + 0x000700e1: 'VK_SHIFT', + 0x000700e2: 'VK_MENU', + 0x000700e3: 'Meta', + 0x000700e4: 'RControl', + 0x000700e5: 'RShift', + 0x000700e6: 'RAlt', + 0x000700e7: 'RWin', + 0x000c00b1: 'VK_PAUSE', + 0x000c00cd: 'VK_PAUSE', + 0x000c019e: 'LOCK_SCREEN', + 0x000c0208: 'VK_PRINT', +}; diff --git a/flutter/lib/desktop/pages/server_page.dart b/flutter/lib/desktop/pages/server_page.dart new file mode 100644 index 000000000..d96efc710 --- /dev/null +++ b/flutter/lib/desktop/pages/server_page.dart @@ -0,0 +1,574 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:flutter_hbb/desktop/widgets/tabbar_widget.dart'; +import 'package:flutter_hbb/mobile/pages/chat_page.dart'; +import 'package:flutter_hbb/models/chat_model.dart'; +import 'package:get/get.dart'; +import 'package:provider/provider.dart'; +import 'package:window_manager/window_manager.dart'; + +import '../../common.dart'; +import '../../models/platform_model.dart'; +import '../../models/server_model.dart'; + +class DesktopServerPage extends StatefulWidget { + @override + State createState() => _DesktopServerPageState(); +} + +class _DesktopServerPageState extends State + with WindowListener, AutomaticKeepAliveClientMixin { + @override + void initState() { + gFFI.ffiModel.updateEventListener(""); + windowManager.addListener(this); + super.initState(); + } + + @override + void dispose() { + windowManager.removeListener(this); + super.dispose(); + } + + @override + void onWindowClose() { + gFFI.serverModel.closeAll(); + gFFI.close(); + super.onWindowClose(); + } + + Widget build(BuildContext context) { + super.build(context); + return MultiProvider( + providers: [ + ChangeNotifierProvider.value(value: gFFI.serverModel), + ChangeNotifierProvider.value(value: gFFI.chatModel), + ], + child: Consumer( + builder: (context, serverModel, child) => Container( + decoration: BoxDecoration( + border: + Border.all(color: MyTheme.color(context).border!)), + child: Scaffold( + backgroundColor: MyTheme.color(context).bg, + body: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Expanded(child: ConnectionManager()), + SizedBox.fromSize(size: Size(0, 15.0)), + ], + ), + ), + ), + ))); + } + + @override + bool get wantKeepAlive => true; +} + +class ConnectionManager extends StatefulWidget { + @override + State createState() => ConnectionManagerState(); +} + +class ConnectionManagerState extends State { + @override + void initState() { + gFFI.serverModel.updateClientState(); + gFFI.serverModel.tabController.onSelected = (index) => + gFFI.chatModel.changeCurrentID(gFFI.serverModel.clients[index].id); + // test + // gFFI.serverModel.clients.forEach((client) { + // DesktopTabBar.onAdd( + // gFFI.serverModel.tabs, + // TabInfo( + // key: client.id.toString(), label: client.name, closable: false)); + // }); + super.initState(); + } + + @override + Widget build(BuildContext context) { + final serverModel = Provider.of(context); + return serverModel.clients.isEmpty + ? Column( + children: [ + buildTitleBar(Offstage()), + Expanded( + child: Center( + child: Text(translate("Waiting")), + ), + ), + ], + ) + : DesktopTab( + theme: isDarkTheme() ? TarBarTheme.dark() : TarBarTheme.light(), + showTitle: false, + showMaximize: false, + showMinimize: false, + controller: serverModel.tabController, + isMainWindow: true, + pageViewBuilder: (pageView) => Row(children: [ + Expanded(child: pageView), + Consumer( + builder: (_, model, child) => model.isShowChatPage + ? Expanded(child: Scaffold(body: ChatPage())) + : Offstage()) + ])); + } + + Widget buildTitleBar(Widget middle) { + return GestureDetector( + onPanDown: (d) { + windowManager.startDragging(); + }, + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + _AppIcon(), + Expanded(child: middle), + const SizedBox( + width: 4.0, + ), + _CloseButton() + ], + ), + ); + } + + Widget buildTab(Client client) { + return Tab( + child: Row( + children: [ + SizedBox( + width: 80, + child: Text( + "${client.name}", + maxLines: 1, + overflow: TextOverflow.ellipsis, + textAlign: TextAlign.center, + )), + ], + ), + ); + } +} + +Widget buildConnectionCard(Client client) { + return Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + key: ValueKey(client.id), + children: [ + _CmHeader(client: client), + client.isFileTransfer ? Offstage() : _PrivilegeBoard(client: client), + Expanded( + child: Align( + alignment: Alignment.bottomCenter, + child: _CmControlPanel(client: client), + )) + ], + ).paddingSymmetric(vertical: 8.0, horizontal: 8.0); +} + +class _AppIcon extends StatelessWidget { + const _AppIcon({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Container( + margin: EdgeInsets.symmetric(horizontal: 4.0), + child: Image.asset( + 'assets/logo.ico', + width: 30, + height: 30, + ), + ); + } +} + +class _CloseButton extends StatelessWidget { + const _CloseButton({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Ink( + child: InkWell( + onTap: () { + windowManager.close(); + }, + child: Icon( + Icons.close, + size: 30, + )), + ); + } +} + +class _CmHeader extends StatefulWidget { + final Client client; + + const _CmHeader({Key? key, required this.client}) : super(key: key); + + @override + State<_CmHeader> createState() => _CmHeaderState(); +} + +class _CmHeaderState extends State<_CmHeader> + with AutomaticKeepAliveClientMixin { + Client get client => widget.client; + + var _time = 0.obs; + Timer? _timer; + + @override + void initState() { + super.initState(); + _timer = Timer.periodic(Duration(seconds: 1), (_) { + _time.value = _time.value + 1; + }); + } + + @override + void dispose() { + _timer?.cancel(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + super.build(context); + return Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // icon + Container( + width: 90, + height: 90, + alignment: Alignment.center, + decoration: BoxDecoration(color: str2color(client.name)), + child: Text( + "${client.name[0]}", + style: TextStyle( + fontWeight: FontWeight.bold, color: Colors.white, fontSize: 65), + ), + ).marginOnly(left: 4.0, right: 8.0), + Expanded( + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + FittedBox( + child: Text( + "${client.name}", + style: TextStyle( + color: MyTheme.cmIdColor, + fontWeight: FontWeight.bold, + fontSize: 20, + overflow: TextOverflow.ellipsis, + ), + maxLines: 1, + )), + FittedBox( + child: Text("(${client.peerId})", + style: + TextStyle(color: MyTheme.cmIdColor, fontSize: 14))), + SizedBox( + height: 16.0, + ), + FittedBox( + child: Row( + children: [ + Text("${translate("Connected")}").marginOnly(right: 8.0), + Obx(() => Text( + "${formatDurationToTime(Duration(seconds: _time.value))}")) + ], + )) + ], + ), + ), + Offstage( + offstage: client.isFileTransfer, + child: IconButton( + onPressed: () => gFFI.chatModel.toggleCMChatPage(client.id), + icon: Icon(Icons.message_outlined), + ), + ) + ], + ); + } + + @override + bool get wantKeepAlive => true; +} + +class _PrivilegeBoard extends StatefulWidget { + final Client client; + + const _PrivilegeBoard({Key? key, required this.client}) : super(key: key); + + @override + State createState() => _PrivilegeBoardState(); +} + +class _PrivilegeBoardState extends State<_PrivilegeBoard> { + late final client = widget.client; + Widget buildPermissionIcon(bool enabled, ImageProvider icon, + Function(bool)? onTap, String? tooltip) { + return Tooltip( + message: tooltip ?? "", + child: Ink( + decoration: + BoxDecoration(color: enabled ? MyTheme.accent80 : Colors.grey), + padding: EdgeInsets.all(4.0), + child: InkWell( + onTap: () => onTap?.call(!enabled), + child: Image( + image: icon, + width: 50, + height: 50, + fit: BoxFit.scaleDown, + ), + ), + ).marginSymmetric(horizontal: 4.0), + ); + } + + @override + Widget build(BuildContext context) { + return Container( + margin: EdgeInsets.only(top: 16.0, bottom: 8.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + translate("Permissions"), + style: TextStyle(fontSize: 16), + ).marginOnly(left: 4.0), + SizedBox( + height: 8.0, + ), + FittedBox( + child: Row( + children: [ + buildPermissionIcon(client.keyboard, iconKeyboard, (enabled) { + bind.cmSwitchPermission( + connId: client.id, name: "keyboard", enabled: enabled); + setState(() { + client.keyboard = enabled; + }); + }, null), + buildPermissionIcon(client.clipboard, iconClipboard, (enabled) { + bind.cmSwitchPermission( + connId: client.id, name: "clipboard", enabled: enabled); + setState(() { + client.clipboard = enabled; + }); + }, null), + buildPermissionIcon(client.audio, iconAudio, (enabled) { + bind.cmSwitchPermission( + connId: client.id, name: "audio", enabled: enabled); + setState(() { + client.audio = enabled; + }); + }, null), + buildPermissionIcon(client.file, iconFile, (enabled) { + bind.cmSwitchPermission( + connId: client.id, name: "file", enabled: enabled); + setState(() { + client.file = enabled; + }); + }, null), + buildPermissionIcon(client.restart, iconRestart, (enabled) { + bind.cmSwitchPermission( + connId: client.id, name: "restart", enabled: enabled); + setState(() { + client.restart = enabled; + }); + }, null), + ], + )), + ], + ), + ); + } +} + +class _CmControlPanel extends StatelessWidget { + final Client client; + + const _CmControlPanel({Key? key, required this.client}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Consumer(builder: (_, model, child) { + return client.authorized + ? buildAuthorized(context) + : buildUnAuthorized(context); + }); + } + + buildAuthorized(BuildContext context) { + return Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Ink( + width: 200, + height: 40, + decoration: BoxDecoration( + color: Colors.redAccent, borderRadius: BorderRadius.circular(10)), + child: InkWell( + onTap: () => handleDisconnect(context), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + translate("Disconnect"), + style: TextStyle(color: Colors.white), + ), + ], + )), + ) + ], + ); + } + + buildUnAuthorized(BuildContext context) { + return Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Ink( + width: 100, + height: 40, + decoration: BoxDecoration( + color: MyTheme.accent, borderRadius: BorderRadius.circular(10)), + child: InkWell( + onTap: () => handleAccept(context), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + translate("Accept"), + style: TextStyle(color: Colors.white), + ), + ], + )), + ), + SizedBox( + width: 30, + ), + Ink( + width: 100, + height: 40, + decoration: BoxDecoration( + color: Colors.transparent, + borderRadius: BorderRadius.circular(10), + border: Border.all(color: Colors.grey)), + child: InkWell( + onTap: () => handleDisconnect(context), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + translate("Cancel"), + style: TextStyle(), + ), + ], + )), + ) + ], + ); + } + + void handleDisconnect(BuildContext context) { + bind.cmCloseConnection(connId: client.id); + } + + void handleAccept(BuildContext context) { + final model = Provider.of(context, listen: false); + model.sendLoginResponse(client, true); + } +} + +class PaddingCard extends StatelessWidget { + PaddingCard({required this.child, this.title, this.titleIcon}); + + final String? title; + final IconData? titleIcon; + final Widget child; + + @override + Widget build(BuildContext context) { + final children = [child]; + if (title != null) { + children.insert( + 0, + Padding( + padding: EdgeInsets.symmetric(vertical: 5.0), + child: Row( + children: [ + titleIcon != null + ? Padding( + padding: EdgeInsets.only(right: 10), + child: Icon(titleIcon, + color: MyTheme.accent80, size: 30)) + : SizedBox.shrink(), + Text( + title!, + style: TextStyle( + fontFamily: 'WorkSans', + fontWeight: FontWeight.bold, + fontSize: 20, + color: MyTheme.accent80, + ), + ) + ], + ))); + } + return Container( + width: double.maxFinite, + child: Card( + margin: EdgeInsets.fromLTRB(15.0, 15.0, 15.0, 0), + child: Padding( + padding: EdgeInsets.symmetric(vertical: 15.0, horizontal: 30.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: children, + ), + ), + )); + } +} + +Widget clientInfo(Client client) { + return Padding( + padding: EdgeInsets.symmetric(vertical: 8), + child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ + Row( + children: [ + Expanded( + flex: -1, + child: Padding( + padding: EdgeInsets.only(right: 12), + child: CircleAvatar( + child: Text(client.name[0]), + backgroundColor: MyTheme.border))), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text(client.name, + style: TextStyle(color: MyTheme.idColor, fontSize: 18)), + SizedBox(width: 8), + Text(client.peerId, + style: TextStyle(color: MyTheme.idColor, fontSize: 10)) + ])) + ], + ), + ])); +} diff --git a/flutter/lib/desktop/screen/desktop_file_transfer_screen.dart b/flutter/lib/desktop/screen/desktop_file_transfer_screen.dart new file mode 100644 index 000000000..694f18ace --- /dev/null +++ b/flutter/lib/desktop/screen/desktop_file_transfer_screen.dart @@ -0,0 +1,29 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_hbb/common.dart'; +import 'package:flutter_hbb/desktop/pages/file_manager_tab_page.dart'; +import 'package:provider/provider.dart'; + +/// multi-tab file transfer remote screen +class DesktopFileTransferScreen extends StatelessWidget { + final Map params; + + const DesktopFileTransferScreen({Key? key, required this.params}) + : super(key: key); + + @override + Widget build(BuildContext context) { + return MultiProvider( + providers: [ + ChangeNotifierProvider.value(value: gFFI.ffiModel), + ChangeNotifierProvider.value(value: gFFI.imageModel), + ChangeNotifierProvider.value(value: gFFI.cursorModel), + ChangeNotifierProvider.value(value: gFFI.canvasModel), + ], + child: Scaffold( + body: FileManagerTabPage( + params: params, + ), + ), + ); + } +} diff --git a/flutter/lib/desktop/screen/desktop_remote_screen.dart b/flutter/lib/desktop/screen/desktop_remote_screen.dart new file mode 100644 index 000000000..4e941ed7c --- /dev/null +++ b/flutter/lib/desktop/screen/desktop_remote_screen.dart @@ -0,0 +1,27 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_hbb/common.dart'; +import 'package:flutter_hbb/desktop/pages/connection_tab_page.dart'; +import 'package:provider/provider.dart'; + +/// multi-tab desktop remote screen +class DesktopRemoteScreen extends StatelessWidget { + final Map params; + + const DesktopRemoteScreen({Key? key, required this.params}) : super(key: key); + + @override + Widget build(BuildContext context) { + return MultiProvider( + providers: [ + ChangeNotifierProvider.value(value: gFFI.ffiModel), + ChangeNotifierProvider.value(value: gFFI.imageModel), + ChangeNotifierProvider.value(value: gFFI.cursorModel), + ChangeNotifierProvider.value(value: gFFI.canvasModel), + ], + child: Scaffold( + body: ConnectionTabPage( + params: params, + ), + )); + } +} diff --git a/flutter/lib/desktop/widgets/peer_widget.dart b/flutter/lib/desktop/widgets/peer_widget.dart new file mode 100644 index 000000000..3bfff60bf --- /dev/null +++ b/flutter/lib/desktop/widgets/peer_widget.dart @@ -0,0 +1,275 @@ +import 'dart:async'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:provider/provider.dart'; +import 'package:visibility_detector/visibility_detector.dart'; +import 'package:window_manager/window_manager.dart'; + +import '../../common.dart'; +import '../../models/peer_model.dart'; +import '../../models/platform_model.dart'; +import 'peercard_widget.dart'; + +typedef OffstageFunc = bool Function(Peer peer); +typedef PeerCardWidgetFunc = Widget Function(Peer peer); + +/// for peer search text, global obs value +final peerSearchText = "".obs; +final peerSearchTextController = + TextEditingController(text: peerSearchText.value); + +class _PeerWidget extends StatefulWidget { + late final _peers; + late final OffstageFunc _offstageFunc; + late final PeerCardWidgetFunc _peerCardWidgetFunc; + + _PeerWidget(Peers peers, OffstageFunc offstageFunc, + PeerCardWidgetFunc peerCardWidgetFunc, + {Key? key}) + : super(key: key) { + _peers = peers; + _offstageFunc = offstageFunc; + _peerCardWidgetFunc = peerCardWidgetFunc; + } + + @override + _PeerWidgetState createState() => _PeerWidgetState(); +} + +/// State for the peer widget. +class _PeerWidgetState extends State<_PeerWidget> with WindowListener { + static const int _maxQueryCount = 3; + + var _curPeers = Set(); + var _lastChangeTime = DateTime.now(); + var _lastQueryPeers = Set(); + var _lastQueryTime = DateTime.now().subtract(Duration(hours: 1)); + var _queryCoun = 0; + var _exit = false; + + _PeerWidgetState() { + _startCheckOnlines(); + } + + @override + void initState() { + windowManager.addListener(this); + super.initState(); + } + + @override + void dispose() { + windowManager.removeListener(this); + _exit = true; + super.dispose(); + } + + @override + void onWindowFocus() { + _queryCoun = 0; + } + + @override + void onWindowMinimize() { + _queryCoun = _maxQueryCount; + } + + @override + Widget build(BuildContext context) { + final space = 12.0; + return ChangeNotifierProvider( + create: (context) => super.widget._peers, + child: Consumer( + builder: (context, peers, child) => peers.peers.isEmpty + ? Center( + child: Text(translate("Empty")), + ) + : SingleChildScrollView( + child: ObxValue((searchText) { + return FutureBuilder>( + builder: (context, snapshot) { + if (snapshot.hasData) { + final peers = snapshot.data!; + final cards = []; + for (final peer in peers) { + cards.add(Offstage( + key: ValueKey("off${peer.id}"), + offstage: super.widget._offstageFunc(peer), + child: Obx( + () => SizedBox( + width: 220, + height: + peerCardUiType.value == PeerUiType.grid + ? 140 + : 42, + child: 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(); + }, + child: super + .widget + ._peerCardWidgetFunc(peer), + ), + ), + ))); + } + return Wrap( + spacing: space, + runSpacing: space, + children: cards); + } else { + return const Center( + child: CircularProgressIndicator(), + ); + } + }, + future: matchPeers(searchText.value, peers.peers), + ); + }, peerSearchText), + )), + ); + } + + // ignore: todo + // TODO: variables walk through async tasks? + void _startCheckOnlines() { + () async { + while (!_exit) { + final now = DateTime.now(); + if (!setEquals(_curPeers, _lastQueryPeers)) { + if (now.difference(_lastChangeTime) > Duration(seconds: 1)) { + if (_curPeers.length > 0) { + platformFFI.ffiBind + .queryOnlines(ids: _curPeers.toList(growable: false)); + _lastQueryPeers = {..._curPeers}; + _lastQueryTime = DateTime.now(); + _queryCoun = 0; + } + } + } else { + if (_queryCoun < _maxQueryCount) { + if (now.difference(_lastQueryTime) > Duration(seconds: 20)) { + if (_curPeers.length > 0) { + platformFFI.ffiBind + .queryOnlines(ids: _curPeers.toList(growable: false)); + _lastQueryTime = DateTime.now(); + _queryCoun += 1; + } + } + } + } + await Future.delayed(Duration(milliseconds: 300)); + } + }(); + } +} + +abstract class BasePeerWidget extends StatelessWidget { + late final _name; + late final _loadEvent; + late final OffstageFunc _offstageFunc; + late final PeerCardWidgetFunc _peerCardWidgetFunc; + late final List _initPeers; + + BasePeerWidget({Key? key}) : super(key: key) {} + + @override + Widget build(BuildContext context) { + return _PeerWidget(Peers(_name, _loadEvent, _initPeers), _offstageFunc, + _peerCardWidgetFunc); + } +} + +class RecentPeerWidget extends BasePeerWidget { + RecentPeerWidget({Key? key}) : super(key: key) { + super._name = "recent peer"; + super._loadEvent = "load_recent_peers"; + super._offstageFunc = (Peer _peer) => false; + super._peerCardWidgetFunc = (Peer peer) => RecentPeerCard( + peer: peer, + ); + super._initPeers = []; + } + + @override + Widget build(BuildContext context) { + final widget = super.build(context); + bind.mainLoadRecentPeers(); + return widget; + } +} + +class FavoritePeerWidget extends BasePeerWidget { + FavoritePeerWidget({Key? key}) : super(key: key) { + super._name = "favorite peer"; + super._loadEvent = "load_fav_peers"; + super._offstageFunc = (Peer _peer) => false; + super._peerCardWidgetFunc = (Peer peer) => FavoritePeerCard(peer: peer); + super._initPeers = []; + } + + @override + Widget build(BuildContext context) { + final widget = super.build(context); + bind.mainLoadFavPeers(); + return widget; + } +} + +class DiscoveredPeerWidget extends BasePeerWidget { + DiscoveredPeerWidget({Key? key}) : super(key: key) { + super._name = "discovered peer"; + super._loadEvent = "load_lan_peers"; + super._offstageFunc = (Peer _peer) => false; + super._peerCardWidgetFunc = (Peer peer) => DiscoveredPeerCard(peer: peer); + super._initPeers = []; + } + + @override + Widget build(BuildContext context) { + final widget = super.build(context); + bind.mainLoadLanPeers(); + return widget; + } +} + +class AddressBookPeerWidget extends BasePeerWidget { + AddressBookPeerWidget({Key? key}) : super(key: key) { + super._name = "address book peer"; + super._offstageFunc = + (Peer peer) => !_hitTag(gFFI.abModel.selectedTags, peer.tags); + super._peerCardWidgetFunc = (Peer peer) => AddressBookPeerCard(peer: peer); + super._initPeers = _loadPeers(); + } + + List _loadPeers() { + return gFFI.abModel.peers.map((e) { + return Peer.fromJson(e['id'], e); + }).toList(); + } + + bool _hitTag(List selectedTags, List idents) { + if (selectedTags.isEmpty) { + return true; + } + if (idents.isEmpty) { + return false; + } + for (final tag in selectedTags) { + if (!idents.contains(tag)) { + return false; + } + } + return true; + } +} diff --git a/flutter/lib/desktop/widgets/peercard_widget.dart b/flutter/lib/desktop/widgets/peercard_widget.dart new file mode 100644 index 000000000..433ca9284 --- /dev/null +++ b/flutter/lib/desktop/widgets/peercard_widget.dart @@ -0,0 +1,666 @@ +import 'package:contextmenu/contextmenu.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_hbb/utils/multi_window_manager.dart'; +import 'package:get/get.dart'; + +import '../../common.dart'; +import '../../models/model.dart'; +import '../../models/peer_model.dart'; +import '../../models/platform_model.dart'; + +typedef PopupMenuItemsFunc = Future>> Function(); + +enum PeerType { recent, fav, discovered, ab } + +enum PeerUiType { grid, list } + +final peerCardUiType = PeerUiType.grid.obs; + +class _PeerCard extends StatefulWidget { + final Peer peer; + final PopupMenuItemsFunc popupMenuItemsFunc; + final PeerType type; + + _PeerCard( + {required this.peer, + required this.popupMenuItemsFunc, + Key? key, + required this.type}) + : super(key: key); + + @override + _PeerCardState createState() => _PeerCardState(); +} + +/// State for the connection page. +class _PeerCardState extends State<_PeerCard> + with AutomaticKeepAliveClientMixin { + var _menuPos = RelativeRect.fill; + final double _cardRadis = 20; + final double _borderWidth = 2; + final RxBool _iconMoreHover = false.obs; + + @override + Widget build(BuildContext context) { + super.build(context); + final peer = super.widget.peer; + var deco = Rx(BoxDecoration( + border: Border.all(color: Colors.transparent, width: _borderWidth), + borderRadius: peerCardUiType.value == PeerUiType.grid + ? BorderRadius.circular(_cardRadis) + : null)); + return MouseRegion( + onEnter: (evt) { + deco.value = BoxDecoration( + border: Border.all(color: MyTheme.button, width: _borderWidth), + borderRadius: peerCardUiType.value == PeerUiType.grid + ? BorderRadius.circular(_cardRadis) + : null); + }, + onExit: (evt) { + deco.value = BoxDecoration( + border: Border.all(color: Colors.transparent, width: _borderWidth), + borderRadius: peerCardUiType.value == PeerUiType.grid + ? BorderRadius.circular(_cardRadis) + : null); + }, + child: GestureDetector( + onDoubleTap: () => _connect(peer.id), + child: Obx(() => peerCardUiType.value == PeerUiType.grid + ? _buildPeerCard(context, peer, deco) + : _buildPeerTile(context, peer, deco))), + ); + } + + Widget _buildPeerTile( + BuildContext context, Peer peer, Rx deco) { + final greyStyle = + TextStyle(fontSize: 12, color: MyTheme.color(context).lighterText); + return Obx( + () => Container( + foregroundDecoration: deco.value, + child: Row( + mainAxisSize: MainAxisSize.max, + children: [ + Container( + decoration: BoxDecoration( + color: str2color('${peer.id}${peer.platform}', 0x7f), + ), + alignment: Alignment.center, + child: _getPlatformImage('${peer.platform}', 30).paddingAll(6), + ), + Expanded( + child: Container( + decoration: BoxDecoration(color: MyTheme.color(context).bg), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + Row(children: [ + Padding( + padding: EdgeInsets.fromLTRB(0, 4, 4, 4), + child: CircleAvatar( + radius: 5, + backgroundColor: peer.online + ? Colors.green + : Colors.yellow)), + Text( + '${peer.id}', + style: TextStyle(fontWeight: FontWeight.w400), + ), + ]), + Align( + alignment: Alignment.centerLeft, + child: FutureBuilder( + future: bind.mainGetPeerOption( + id: peer.id, key: 'alias'), + builder: (_, snapshot) { + if (snapshot.hasData) { + final name = snapshot.data!.isEmpty + ? '${peer.username}@${peer.hostname}' + : snapshot.data!; + return Tooltip( + message: name, + waitDuration: Duration(seconds: 1), + child: Text( + name, + style: greyStyle, + textAlign: TextAlign.start, + overflow: TextOverflow.ellipsis, + ), + ); + } else { + // alias has not arrived + return Text( + '${peer.username}@${peer.hostname}', + style: greyStyle, + textAlign: TextAlign.start, + overflow: TextOverflow.ellipsis, + ); + } + }, + ), + ), + ], + ), + ), + _actionMore(peer), + ], + ).paddingSymmetric(horizontal: 4.0), + ), + ) + ], + ), + ), + ); + } + + Widget _buildPeerCard( + BuildContext context, Peer peer, Rx deco) { + return Card( + color: Colors.transparent, + elevation: 0, + margin: EdgeInsets.zero, + child: Obx( + () => Container( + foregroundDecoration: deco.value, + child: ClipRRect( + borderRadius: BorderRadius.circular(_cardRadis - _borderWidth), + child: Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Expanded( + child: Container( + color: str2color('${peer.id}${peer.platform}', 0x7f), + child: Row( + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Container( + padding: const EdgeInsets.all(6), + child: + _getPlatformImage('${peer.platform}', 60), + ), + Row( + children: [ + Expanded( + child: FutureBuilder( + future: bind.mainGetPeerOption( + id: peer.id, key: 'alias'), + builder: (_, snapshot) { + if (snapshot.hasData) { + final name = snapshot.data!.isEmpty + ? '${peer.username}@${peer.hostname}' + : snapshot.data!; + return Tooltip( + message: name, + waitDuration: Duration(seconds: 1), + child: Text( + name, + style: TextStyle( + color: Colors.white70, + fontSize: 12), + textAlign: TextAlign.center, + overflow: TextOverflow.ellipsis, + ), + ); + } else { + // alias has not arrived + return Center( + child: Text( + '${peer.username}@${peer.hostname}', + style: TextStyle( + color: Colors.white70, + fontSize: 12), + textAlign: TextAlign.center, + overflow: TextOverflow.ellipsis, + )); + } + }, + ), + ), + ], + ), + ], + ).paddingAll(4.0), + ), + ], + ), + ), + ), + Container( + color: MyTheme.color(context).bg, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row(children: [ + Padding( + padding: EdgeInsets.fromLTRB(0, 4, 8, 4), + child: CircleAvatar( + radius: 5, + backgroundColor: peer.online + ? Colors.green + : Colors.yellow)), + Text('${peer.id}') + ]).paddingSymmetric(vertical: 8), + _actionMore(peer), + ], + ).paddingSymmetric(horizontal: 12.0), + ) + ], + ), + ), + ), + ), + ); + } + + Widget _actionMore(Peer peer) => Listener( + onPointerDown: (e) { + final x = e.position.dx; + final y = e.position.dy; + _menuPos = RelativeRect.fromLTRB(x, y, x, y); + }, + onPointerUp: (_) => _showPeerMenu(context, peer.id), + child: MouseRegion( + onEnter: (_) => _iconMoreHover.value = true, + onExit: (_) => _iconMoreHover.value = false, + child: CircleAvatar( + radius: 14, + backgroundColor: _iconMoreHover.value + ? MyTheme.color(context).grayBg! + : MyTheme.color(context).bg!, + child: Icon(Icons.more_vert, + size: 18, + color: _iconMoreHover.value + ? MyTheme.color(context).text + : MyTheme.color(context).lightText)))); + + /// Connect to a peer with [id]. + /// If [isFileTransfer], starts a session only for file transfer. + void _connect(String id, {bool isFileTransfer = false}) async { + if (id == '') return; + id = id.replaceAll(' ', ''); + if (isFileTransfer) { + await rustDeskWinManager.new_file_transfer(id); + } else { + await rustDeskWinManager.new_remote_desktop(id); + } + FocusScopeNode currentFocus = FocusScope.of(context); + if (!currentFocus.hasPrimaryFocus) { + currentFocus.unfocus(); + } + } + + /// Show the peer menu and handle user's choice. + /// User might remove the peer or send a file to the peer. + void _showPeerMenu(BuildContext context, String id) async { + var value = await showMenu( + context: context, + position: _menuPos, + items: await super.widget.popupMenuItemsFunc(), + elevation: 8, + ); + if (value == 'remove') { + await bind.mainRemovePeer(id: id); + removePreference(id); + Get.forceAppUpdate(); // TODO use inner model / state + } else if (value == 'file') { + _connect(id, isFileTransfer: true); + } else if (value == 'add-fav') { + final favs = (await bind.mainGetFav()).toList(); + if (favs.indexOf(id) < 0) { + favs.add(id); + bind.mainStoreFav(favs: favs); + } + } else if (value == 'remove-fav') { + final favs = (await bind.mainGetFav()).toList(); + if (favs.remove(id)) { + bind.mainStoreFav(favs: favs); + Get.forceAppUpdate(); // TODO use inner model / state + } + } else if (value == 'connect') { + _connect(id, isFileTransfer: false); + } else if (value == 'ab-delete') { + gFFI.abModel.deletePeer(id); + await gFFI.abModel.updateAb(); + setState(() {}); + } else if (value == 'ab-edit-tag') { + _abEditTag(id); + } else if (value == 'rename') { + _rename(id); + } else if (value == 'unremember-password') { + await bind.mainForgetPassword(id: id); + } else if (value == 'force-always-relay') { + String value; + String oldValue = + await bind.mainGetPeerOption(id: id, key: 'force-always-relay'); + if (oldValue.isEmpty) { + value = 'Y'; + } else { + value = ''; + } + await bind.mainSetPeerOption( + id: id, key: 'force-always-relay', value: value); + } + } + + Widget _buildTag(String tagName, RxList rxTags, + {Function()? onTap}) { + return ContextMenuArea( + width: 100, + builder: (context) => [ + ListTile( + title: Text(translate("Delete")), + onTap: () { + gFFI.abModel.deleteTag(tagName); + gFFI.abModel.updateAb(); + Future.delayed(Duration.zero, () => Get.back()); + }, + ) + ], + child: GestureDetector( + onTap: onTap, + child: Obx( + () => Container( + decoration: BoxDecoration( + color: rxTags.contains(tagName) ? Colors.blue : null, + border: Border.all(color: MyTheme.darkGray), + borderRadius: BorderRadius.circular(10)), + margin: EdgeInsets.symmetric(horizontal: 4.0, vertical: 8.0), + padding: EdgeInsets.symmetric(vertical: 2.0, horizontal: 8.0), + child: Text( + tagName, + style: TextStyle( + color: rxTags.contains(tagName) ? MyTheme.white : null), + ), + ), + ), + ), + ); + } + + /// Get the image for the current [platform]. + Widget _getPlatformImage(String platform, double size) { + platform = platform.toLowerCase(); + if (platform == 'mac os') + platform = 'mac'; + else if (platform != 'linux' && platform != 'android') platform = 'win'; + return Image.asset('assets/$platform.png', height: size, width: size); + } + + void _abEditTag(String id) { + var isInProgress = false; + + final tags = List.of(gFFI.abModel.tags); + var selectedTag = gFFI.abModel.getPeerTags(id).obs; + + gFFI.dialogManager.show((setState, close) { + return CustomAlertDialog( + title: Text(translate("Edit Tag")), + content: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + padding: EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0), + child: Wrap( + children: tags + .map((e) => _buildTag(e, selectedTag, onTap: () { + if (selectedTag.contains(e)) { + selectedTag.remove(e); + } else { + selectedTag.add(e); + } + })) + .toList(growable: false), + ), + ), + Offstage(offstage: !isInProgress, child: LinearProgressIndicator()) + ], + ), + actions: [ + TextButton( + onPressed: () { + close(); + }, + child: Text(translate("Cancel"))), + TextButton( + onPressed: () async { + setState(() { + isInProgress = true; + }); + gFFI.abModel.changeTagForPeer(id, selectedTag); + await gFFI.abModel.updateAb(); + close(); + }, + child: Text(translate("OK"))), + ], + ); + }); + } + + void _rename(String id) async { + var isInProgress = false; + var name = await bind.mainGetPeerOption(id: id, key: 'alias'); + if (widget.type == PeerType.ab) { + final peer = gFFI.abModel.peers.firstWhere((p) => id == p['id']); + if (peer == null) { + // this should not happen + } else { + name = peer['alias'] ?? ""; + } + } + final k = GlobalKey(); + gFFI.dialogManager.show((setState, close) { + return CustomAlertDialog( + title: Text(translate("Rename")), + content: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + padding: EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0), + child: Form( + key: k, + child: TextFormField( + controller: TextEditingController(text: name), + decoration: InputDecoration(border: OutlineInputBorder()), + onChanged: (newStr) { + name = newStr; + }, + validator: (s) { + if (s == null || s.isEmpty) { + return translate("Empty"); + } + return null; + }, + onSaved: (s) { + name = s ?? "unnamed"; + }, + ), + ), + ), + Offstage(offstage: !isInProgress, child: LinearProgressIndicator()) + ], + ), + actions: [ + TextButton( + onPressed: () { + close(); + }, + child: Text(translate("Cancel"))), + TextButton( + onPressed: () async { + setState(() { + isInProgress = true; + }); + if (k.currentState != null) { + if (k.currentState!.validate()) { + k.currentState!.save(); + await bind.mainSetPeerOption( + id: id, key: 'alias', value: name); + if (widget.type == PeerType.ab) { + gFFI.abModel.setPeerOption(id, 'alias', name); + await gFFI.abModel.updateAb(); + } else { + Future.delayed(Duration.zero, () { + this.setState(() {}); + }); + } + close(); + } + } + setState(() { + isInProgress = false; + }); + }, + child: Text(translate("OK"))), + ], + ); + }); + } + + @override + bool get wantKeepAlive => true; +} + +abstract class BasePeerCard extends StatelessWidget { + final Peer peer; + final PeerType type; + + BasePeerCard({required this.peer, required this.type, Key? key}) + : super(key: key); + + @override + Widget build(BuildContext context) { + return _PeerCard( + peer: peer, + popupMenuItemsFunc: _getPopupMenuItems, + type: type, + ); + } + + @protected + Future>> _getPopupMenuItems(); +} + +class RecentPeerCard extends BasePeerCard { + RecentPeerCard({required Peer peer, Key? key}) + : super(peer: peer, key: key, type: PeerType.recent); + + Future>> _getPopupMenuItems() async { + return [ + PopupMenuItem( + child: Text(translate('Connect')), value: 'connect'), + PopupMenuItem( + child: Text(translate('Transfer File')), value: 'file'), + PopupMenuItem( + child: Text(translate('TCP Tunneling')), value: 'tcp-tunnel'), + await _forceAlwaysRelayMenuItem(peer.id), + PopupMenuItem(child: Text(translate('Rename')), value: 'rename'), + PopupMenuItem(child: Text(translate('Remove')), value: 'remove'), + PopupMenuItem( + child: Text(translate('Unremember Password')), + value: 'unremember-password'), + PopupMenuItem( + child: Text(translate('Add to Favorites')), value: 'add-fav'), + ]; + } +} + +class FavoritePeerCard extends BasePeerCard { + FavoritePeerCard({required Peer peer, Key? key}) + : super(peer: peer, key: key, type: PeerType.fav); + + Future>> _getPopupMenuItems() async { + return [ + PopupMenuItem( + child: Text(translate('Connect')), value: 'connect'), + PopupMenuItem( + child: Text(translate('Transfer File')), value: 'file'), + PopupMenuItem( + child: Text(translate('TCP Tunneling')), value: 'tcp-tunnel'), + await _forceAlwaysRelayMenuItem(peer.id), + PopupMenuItem(child: Text(translate('Rename')), value: 'rename'), + PopupMenuItem(child: Text(translate('Remove')), value: 'remove'), + PopupMenuItem( + child: Text(translate('Unremember Password')), + value: 'unremember-password'), + PopupMenuItem( + child: Text(translate('Remove from Favorites')), value: 'remove-fav'), + ]; + } +} + +class DiscoveredPeerCard extends BasePeerCard { + DiscoveredPeerCard({required Peer peer, Key? key}) + : super(peer: peer, key: key, type: PeerType.discovered); + + Future>> _getPopupMenuItems() async { + return [ + PopupMenuItem( + child: Text(translate('Connect')), value: 'connect'), + PopupMenuItem( + child: Text(translate('Transfer File')), value: 'file'), + PopupMenuItem( + child: Text(translate('TCP Tunneling')), value: 'tcp-tunnel'), + await _forceAlwaysRelayMenuItem(peer.id), + PopupMenuItem(child: Text(translate('Rename')), value: 'rename'), + PopupMenuItem(child: Text(translate('Remove')), value: 'remove'), + PopupMenuItem( + child: Text(translate('Unremember Password')), + value: 'unremember-password'), + PopupMenuItem( + child: Text(translate('Add to Favorites')), value: 'add-fav'), + ]; + } +} + +class AddressBookPeerCard extends BasePeerCard { + AddressBookPeerCard({required Peer peer, Key? key}) + : super(peer: peer, key: key, type: PeerType.ab); + + Future>> _getPopupMenuItems() async { + return [ + PopupMenuItem( + child: Text(translate('Connect')), value: 'connect'), + PopupMenuItem( + child: Text(translate('Transfer File')), value: 'file'), + PopupMenuItem( + child: Text(translate('TCP Tunneling')), value: 'tcp-tunnel'), + await _forceAlwaysRelayMenuItem(peer.id), + PopupMenuItem(child: Text(translate('Rename')), value: 'rename'), + PopupMenuItem( + child: Text(translate('Remove')), value: 'ab-delete'), + PopupMenuItem( + child: Text(translate('Unremember Password')), + value: 'unremember-password'), + PopupMenuItem( + child: Text(translate('Add to Favorites')), value: 'add-fav'), + PopupMenuItem( + child: Text(translate('Edit Tag')), value: 'ab-edit-tag'), + ]; + } +} + +Future> _forceAlwaysRelayMenuItem(String id) async { + bool force_always_relay = + (await bind.mainGetPeerOption(id: id, key: 'force-always-relay')) + .isNotEmpty; + return PopupMenuItem( + child: Row( + children: [ + Offstage( + offstage: !force_always_relay, + child: Icon(Icons.check), + ), + Text(translate('Always connect via relay')), + ], + ), + value: 'force-always-relay'); +} diff --git a/flutter/lib/desktop/widgets/tabbar_widget.dart b/flutter/lib/desktop/widgets/tabbar_widget.dart new file mode 100644 index 000000000..09f1ee4b5 --- /dev/null +++ b/flutter/lib/desktop/widgets/tabbar_widget.dart @@ -0,0 +1,589 @@ +import 'dart:math'; + +import 'package:desktop_multi_window/desktop_multi_window.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_hbb/common.dart'; +import 'package:flutter_hbb/consts.dart'; +import 'package:flutter_hbb/main.dart'; +import 'package:get/get.dart'; +import 'package:window_manager/window_manager.dart'; +import 'package:scroll_pos/scroll_pos.dart'; + +import '../../utils/multi_window_manager.dart'; + +const double _kTabBarHeight = kDesktopRemoteTabBarHeight; +const double _kIconSize = 18; +const double _kDividerIndent = 10; +const double _kActionIconSize = 12; + +class TabInfo { + final String key; + final String label; + final IconData? selectedIcon; + final IconData? unselectedIcon; + final bool closable; + final Widget page; + + TabInfo( + {required this.key, + required this.label, + this.selectedIcon, + this.unselectedIcon, + this.closable = true, + required this.page}); +} + +class DesktopTabState { + final List tabs = []; + final ScrollPosController scrollController = + ScrollPosController(itemCount: 0); + final PageController pageController = PageController(); + int selected = 0; + + DesktopTabState() { + scrollController.itemCount = tabs.length; + } +} + +class DesktopTabController { + final state = DesktopTabState().obs; + + /// index, key + Function(int, String)? onRemove; + + Function(int)? onSelected; + + void add(TabInfo tab) { + if (!isDesktop) return; + final index = state.value.tabs.indexWhere((e) => e.key == tab.key); + int toIndex; + if (index >= 0) { + toIndex = index; + } else { + state.update((val) { + val!.tabs.add(tab); + }); + toIndex = state.value.tabs.length - 1; + assert(toIndex >= 0); + } + try { + jumpTo(toIndex); + } catch (e) { + // call before binding controller will throw + debugPrint("Failed to jumpTo: $e"); + } + } + + void remove(int index) { + if (!isDesktop) return; + final len = state.value.tabs.length; + if (index < 0 || index > len - 1) return; + final key = state.value.tabs[index].key; + final currentSelected = state.value.selected; + int toIndex = 0; + if (index == len - 1) { + toIndex = max(0, currentSelected - 1); + } else if (index < len - 1 && index < currentSelected) { + toIndex = max(0, currentSelected - 1); + } + state.value.tabs.removeAt(index); + state.value.scrollController.itemCount = state.value.tabs.length; + jumpTo(toIndex); + onRemove?.call(index, key); + } + + void jumpTo(int index) { + state.update((val) { + val!.selected = index; + val.pageController.jumpToPage(index); + val.scrollController.scrollToItem(index, center: true, animate: true); + }); + onSelected?.call(index); + } + + void closeBy(String? key) { + if (!isDesktop) return; + assert(onRemove != null); + if (key == null) { + if (state.value.selected < state.value.tabs.length) { + remove(state.value.selected); + } + } else { + state.value.tabs.indexWhere((tab) => tab.key == key); + remove(state.value.selected); + } + } +} + +class DesktopTab extends StatelessWidget { + final Function(String)? onTabClose; + final TarBarTheme theme; + final bool isMainWindow; + final bool showTabBar; + final bool showLogo; + final bool showTitle; + final bool showMinimize; + final bool showMaximize; + final bool showClose; + final Widget Function(Widget pageView)? pageViewBuilder; + final Widget? tail; + + final DesktopTabController controller; + late final state = controller.state; + + DesktopTab( + {required this.controller, + required this.isMainWindow, + this.theme = const TarBarTheme.light(), + this.onTabClose, + this.showTabBar = true, + this.showLogo = true, + this.showTitle = true, + this.showMinimize = true, + this.showMaximize = true, + this.showClose = true, + this.pageViewBuilder, + this.tail}); + + @override + Widget build(BuildContext context) { + return Column(children: [ + Offstage( + offstage: !showTabBar, + child: Container( + height: _kTabBarHeight, + child: Column( + children: [ + Container( + height: _kTabBarHeight - 1, + child: _buildBar(), + ), + Divider( + height: 1, + thickness: 1, + ), + ], + ), + )), + Expanded( + child: pageViewBuilder != null + ? pageViewBuilder!(_buildPageView()) + : _buildPageView()) + ]); + } + + Widget _buildPageView() { + return Obx(() => PageView( + controller: state.value.pageController, + children: + state.value.tabs.map((tab) => tab.page).toList(growable: false))); + } + + Widget _buildBar() { + return Row( + children: [ + Expanded( + child: Row( + children: [ + Row(children: [ + Offstage( + offstage: !showLogo, + child: Image.asset( + 'assets/logo.ico', + width: 20, + height: 20, + )), + Offstage( + offstage: !showTitle, + child: Text( + "RustDesk", + style: TextStyle(fontSize: 13), + ).marginOnly(left: 2)) + ]).marginOnly( + left: 5, + right: 10, + ), + Expanded( + child: GestureDetector( + onPanStart: (_) { + if (isMainWindow) { + windowManager.startDragging(); + } else { + WindowController.fromWindowId(windowId!) + .startDragging(); + } + }, + child: _ListView( + controller: controller, + onTabClose: onTabClose, + theme: theme, + )), + ), + ], + ), + ), + Offstage(offstage: tail == null, child: tail), + WindowActionPanel( + mainTab: isMainWindow, + theme: theme, + showMinimize: showMinimize, + showMaximize: showMaximize, + showClose: showClose, + ) + ], + ); + } +} + +class WindowActionPanel extends StatelessWidget { + final bool mainTab; + final TarBarTheme theme; + + final bool showMinimize; + final bool showMaximize; + final bool showClose; + + const WindowActionPanel( + {Key? key, + required this.mainTab, + required this.theme, + this.showMinimize = true, + this.showMaximize = true, + this.showClose = true}) + : super(key: key); + + @override + Widget build(BuildContext context) { + return Row( + children: [ + Offstage( + offstage: !showMinimize, + child: ActionIcon( + message: 'Minimize', + icon: IconFont.min, + theme: theme, + onTap: () { + if (mainTab) { + windowManager.minimize(); + } else { + WindowController.fromWindowId(windowId!).minimize(); + } + }, + is_close: false, + )), + // TODO: drag makes window restore + Offstage( + offstage: !showMaximize, + child: FutureBuilder(builder: (context, snapshot) { + RxBool is_maximized = false.obs; + if (mainTab) { + windowManager.isMaximized().then((maximized) { + is_maximized.value = maximized; + }); + } else { + final wc = WindowController.fromWindowId(windowId!); + wc.isMaximized().then((maximized) { + is_maximized.value = maximized; + }); + } + return Obx( + () => ActionIcon( + message: is_maximized.value ? "Restore" : "Maximize", + icon: is_maximized.value ? IconFont.restore : IconFont.max, + theme: theme, + onTap: () { + if (mainTab) { + if (is_maximized.value) { + windowManager.unmaximize(); + } else { + windowManager.maximize(); + } + } else { + // TODO: subwindow is maximized but first query result is not maximized. + final wc = WindowController.fromWindowId(windowId!); + if (is_maximized.value) { + wc.unmaximize(); + } else { + wc.maximize(); + } + } + is_maximized.value = !is_maximized.value; + }, + is_close: false, + ), + ); + })), + Offstage( + offstage: !showClose, + child: ActionIcon( + message: 'Close', + icon: IconFont.close, + theme: theme, + onTap: () { + if (mainTab) { + windowManager.close(); + } else { + WindowController.fromWindowId(windowId!).close(); + } + }, + is_close: true, + )), + ], + ); + } +} + +// ignore: must_be_immutable +class _ListView extends StatelessWidget { + final DesktopTabController controller; + late final Rx state; + final Function(String key)? onTabClose; + final TarBarTheme theme; + + _ListView( + {required this.controller, required this.onTabClose, required this.theme}) + : this.state = controller.state; + + @override + Widget build(BuildContext context) { + return Obx(() => ListView( + controller: state.value.scrollController, + scrollDirection: Axis.horizontal, + shrinkWrap: true, + physics: BouncingScrollPhysics(), + children: state.value.tabs.asMap().entries.map((e) { + final index = e.key; + final tab = e.value; + return _Tab( + index: index, + label: tab.label, + selectedIcon: tab.selectedIcon, + unselectedIcon: tab.unselectedIcon, + closable: tab.closable, + selected: state.value.selected, + onClose: () => controller.remove(index), + onSelected: () => controller.jumpTo(index), + theme: theme, + ); + }).toList())); + } +} + +class _Tab extends StatelessWidget { + late final int index; + late final String label; + late final IconData? selectedIcon; + late final IconData? unselectedIcon; + late final bool closable; + late final int selected; + late final Function() onClose; + late final Function() onSelected; + final RxBool _hover = false.obs; + late final TarBarTheme theme; + + _Tab( + {Key? key, + required this.index, + required this.label, + this.selectedIcon, + this.unselectedIcon, + required this.closable, + required this.selected, + required this.onClose, + required this.onSelected, + required this.theme}) + : super(key: key); + + @override + Widget build(BuildContext context) { + bool show_icon = selectedIcon != null && unselectedIcon != null; + bool is_selected = index == selected; + bool show_divider = index != selected - 1 && index != selected; + return Ink( + child: InkWell( + onHover: (hover) => _hover.value = hover, + onTap: () => onSelected(), + child: Row( + children: [ + Container( + height: _kTabBarHeight, + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Offstage( + offstage: !show_icon, + child: Icon( + is_selected ? selectedIcon : unselectedIcon, + size: _kIconSize, + color: is_selected + ? theme.selectedtabIconColor + : theme.unSelectedtabIconColor, + ).paddingOnly(right: 5)), + Text( + translate(label), + textAlign: TextAlign.center, + style: TextStyle( + color: is_selected + ? theme.selectedTextColor + : theme.unSelectedTextColor), + ), + ], + ), + Offstage( + offstage: !closable, + child: Obx((() => _CloseButton( + visiable: _hover.value, + tabSelected: is_selected, + onClose: () => onClose(), + theme: theme, + ))), + ) + ])).paddingSymmetric(horizontal: 10), + Offstage( + offstage: !show_divider, + child: VerticalDivider( + width: 1, + indent: _kDividerIndent, + endIndent: _kDividerIndent, + color: theme.dividerColor, + thickness: 1, + ), + ) + ], + ), + ), + ); + } +} + +class _CloseButton extends StatelessWidget { + final bool visiable; + final bool tabSelected; + final Function onClose; + late final TarBarTheme theme; + + _CloseButton({ + Key? key, + required this.visiable, + required this.tabSelected, + required this.onClose, + required this.theme, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return SizedBox( + width: _kIconSize, + child: Offstage( + offstage: !visiable, + child: InkWell( + customBorder: RoundedRectangleBorder(), + onTap: () => onClose(), + child: Icon( + Icons.close, + size: _kIconSize, + color: tabSelected + ? theme.selectedIconColor + : theme.unSelectedIconColor, + ), + ), + )).paddingOnly(left: 5); + } +} + +class ActionIcon extends StatelessWidget { + final String message; + final IconData icon; + final TarBarTheme theme; + final Function() onTap; + final bool is_close; + const ActionIcon({ + Key? key, + required this.message, + required this.icon, + required this.theme, + required this.onTap, + required this.is_close, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + RxBool hover = false.obs; + return Obx(() => Tooltip( + message: translate(message), + waitDuration: Duration(seconds: 1), + child: InkWell( + hoverColor: + is_close ? Color.fromARGB(255, 196, 43, 28) : theme.hoverColor, + onHover: (value) => hover.value = value, + child: Container( + height: _kTabBarHeight - 1, + width: _kTabBarHeight - 1, + child: Icon( + icon, + color: hover.value && is_close + ? Colors.white + : theme.unSelectedIconColor, + size: _kActionIconSize, + ), + ), + onTap: onTap, + ), + )); + } +} + +class AddButton extends StatelessWidget { + late final TarBarTheme theme; + + AddButton({ + Key? key, + required this.theme, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return ActionIcon( + message: 'New Connection', + icon: IconFont.add, + theme: theme, + onTap: () => + rustDeskWinManager.call(WindowType.Main, "main_window_on_top", ""), + is_close: false); + } +} + +class TarBarTheme { + final Color unSelectedtabIconColor; + final Color selectedtabIconColor; + final Color selectedTextColor; + final Color unSelectedTextColor; + final Color selectedIconColor; + final Color unSelectedIconColor; + final Color dividerColor; + final Color hoverColor; + + const TarBarTheme.light() + : unSelectedtabIconColor = const Color.fromARGB(255, 162, 203, 241), + selectedtabIconColor = MyTheme.accent, + selectedTextColor = const Color.fromARGB(255, 26, 26, 26), + unSelectedTextColor = const Color.fromARGB(255, 96, 96, 96), + selectedIconColor = const Color.fromARGB(255, 26, 26, 26), + unSelectedIconColor = const Color.fromARGB(255, 96, 96, 96), + dividerColor = const Color.fromARGB(255, 238, 238, 238), + hoverColor = const Color.fromARGB( + 51, 158, 158, 158); // Colors.grey; //0xFF9E9E9E + + const TarBarTheme.dark() + : unSelectedtabIconColor = const Color.fromARGB(255, 30, 65, 98), + selectedtabIconColor = MyTheme.accent, + selectedTextColor = const Color.fromARGB(255, 255, 255, 255), + unSelectedTextColor = const Color.fromARGB(255, 207, 207, 207), + selectedIconColor = const Color.fromARGB(255, 215, 215, 215), + unSelectedIconColor = const Color.fromARGB(255, 255, 255, 255), + dividerColor = const Color.fromARGB(255, 64, 64, 64), + hoverColor = Colors.black26; +} diff --git a/flutter/lib/desktop/widgets/titlebar_widget.dart b/flutter/lib/desktop/widgets/titlebar_widget.dart new file mode 100644 index 000000000..475b4cb86 --- /dev/null +++ b/flutter/lib/desktop/widgets/titlebar_widget.dart @@ -0,0 +1,70 @@ +import 'package:flutter/material.dart'; + +const sidebarColor = Color(0xFF0C6AF6); +const backgroundStartColor = Color(0xFF0583EA); +const backgroundEndColor = Color(0xFF0697EA); + +class DesktopTitleBar extends StatelessWidget { + final Widget? child; + + const DesktopTitleBar({Key? key, this.child}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Container( + decoration: const BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [backgroundStartColor, backgroundEndColor], + stops: [0.0, 1.0]), + ), + child: Row( + children: [ + Expanded( + child: child ?? Offstage(), + ) + // const WindowButtons() + ], + ), + ); + } +} + +// final buttonColors = WindowButtonColors( +// iconNormal: const Color(0xFF805306), +// mouseOver: const Color(0xFFF6A00C), +// mouseDown: const Color(0xFF805306), +// iconMouseOver: const Color(0xFF805306), +// iconMouseDown: const Color(0xFFFFD500)); +// +// final closeButtonColors = WindowButtonColors( +// mouseOver: const Color(0xFFD32F2F), +// mouseDown: const Color(0xFFB71C1C), +// iconNormal: const Color(0xFF805306), +// iconMouseOver: Colors.white); +// +// class WindowButtons extends StatelessWidget { +// const WindowButtons({Key? key}) : super(key: key); +// +// @override +// Widget build(BuildContext context) { +// return Row( +// children: [ +// MinimizeWindowButton(colors: buttonColors, onPressed: () { +// windowManager.minimize(); +// },), +// MaximizeWindowButton(colors: buttonColors, onPressed: () async { +// if (await windowManager.isMaximized()) { +// windowManager.restore(); +// } else { +// windowManager.maximize(); +// } +// },), +// CloseWindowButton(colors: closeButtonColors, onPressed: () { +// windowManager.close(); +// },), +// ], +// ); +// } +// } diff --git a/flutter/lib/main.dart b/flutter/lib/main.dart index 9ca9039f0..9682f19d1 100644 --- a/flutter/lib/main.dart +++ b/flutter/lib/main.dart @@ -1,55 +1,211 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; -import 'package:provider/provider.dart'; -import 'package:firebase_analytics/firebase_analytics.dart'; -import 'package:firebase_core/firebase_core.dart'; -import 'common.dart'; -import 'models/model.dart'; -import 'pages/home_page.dart'; -import 'pages/server_page.dart'; -import 'pages/settings_page.dart'; +import 'dart:convert'; -Future main() async { +import 'package:desktop_multi_window/desktop_multi_window.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_hbb/desktop/pages/desktop_tab_page.dart'; +import 'package:flutter_hbb/desktop/pages/server_page.dart'; +import 'package:flutter_hbb/desktop/screen/desktop_file_transfer_screen.dart'; +import 'package:flutter_hbb/desktop/screen/desktop_remote_screen.dart'; +import 'package:flutter_hbb/utils/multi_window_manager.dart'; +import 'package:get/get.dart'; +import 'package:provider/provider.dart'; +import 'package:window_manager/window_manager.dart'; + +// import 'package:window_manager/window_manager.dart'; + +import 'common.dart'; +import 'consts.dart'; +import 'mobile/pages/home_page.dart'; +import 'mobile/pages/server_page.dart'; +import 'mobile/pages/settings_page.dart'; +import 'models/platform_model.dart'; + +int? windowId; + +Future main(List args) async { WidgetsFlutterBinding.ensureInitialized(); - var a = FFI.ffiModel.init(); - var b = Firebase.initializeApp(); - await a; - await b; + print("launch args: $args"); + + if (!isDesktop) { + runMobileApp(); + return; + } + // main window + if (args.isNotEmpty && args.first == 'multi_window') { + windowId = int.parse(args[1]); + WindowController.fromWindowId(windowId!).showTitleBar(false); + final argument = args[2].isEmpty + ? Map() + : jsonDecode(args[2]) as Map; + int type = argument['type'] ?? -1; + argument['windowId'] = windowId; + WindowType wType = type.windowType; + switch (wType) { + case WindowType.RemoteDesktop: + runRemoteScreen(argument); + break; + case WindowType.FileTransfer: + runFileTransferScreen(argument); + break; + default: + break; + } + } else if (args.isNotEmpty && args.first == '--cm') { + print("--cm started"); + await windowManager.ensureInitialized(); + runConnectionManagerScreen(); + } else { + await windowManager.ensureInitialized(); + windowManager.setPreventClose(true); + runMainApp(true); + } +} + +ThemeData getCurrentTheme() { + return isDarkTheme() ? MyTheme.darkTheme : MyTheme.lightTheme; +} + +Future initEnv(String appType) async { + await platformFFI.init(appType); + // global FFI, use this **ONLY** for global configuration + // for convenience, use global FFI on mobile platform + // focus on multi-ffi on desktop first + await initGlobalFFI(); + // await Firebase.initializeApp(); refreshCurrentUser(); - toAndroidChannelInit(); +} + +void runMainApp(bool startService) async { + WindowOptions windowOptions = getHiddenTitleBarWindowOptions(Size(1280, 720)); + await Future.wait([ + initEnv(kAppTypeMain), + windowManager.waitUntilReadyToShow(windowOptions, () async { + await windowManager.show(); + await windowManager.focus(); + }) + ]); + if (startService) { + // await windowManager.ensureInitialized(); + // disable tray + // initTray(); + gFFI.serverModel.startService(); + } runApp(App()); } +void runMobileApp() async { + await initEnv(kAppTypeMain); + if (isAndroid) androidChannelInit(); + runApp(App()); +} + +void runRemoteScreen(Map argument) async { + await initEnv(kAppTypeDesktopRemote); + runApp(GetMaterialApp( + navigatorKey: globalKey, + debugShowCheckedModeBanner: false, + title: 'RustDesk - Remote Desktop', + theme: getCurrentTheme(), + home: DesktopRemoteScreen( + params: argument, + ), + navigatorObservers: [ + // FirebaseAnalyticsObserver(analytics: analytics), + ], + builder: _keepScaleBuilder(), + )); +} + +void runFileTransferScreen(Map argument) async { + await initEnv(kAppTypeDesktopFileTransfer); + runApp( + GetMaterialApp( + navigatorKey: globalKey, + debugShowCheckedModeBanner: false, + title: 'RustDesk - File Transfer', + theme: getCurrentTheme(), + home: DesktopFileTransferScreen(params: argument), + navigatorObservers: [ + // FirebaseAnalyticsObserver(analytics: analytics), + ], + builder: _keepScaleBuilder(), + ), + ); +} + +void runConnectionManagerScreen() async { + // initialize window + WindowOptions windowOptions = getHiddenTitleBarWindowOptions(Size(300, 400)); + await Future.wait([ + initEnv(kAppTypeMain), + windowManager.waitUntilReadyToShow(windowOptions, () async { + await windowManager.setAlignment(Alignment.topRight); + await windowManager.show(); + await windowManager.focus(); + }) + ]); + runApp(GetMaterialApp( + debugShowCheckedModeBanner: false, + theme: getCurrentTheme(), + home: DesktopServerPage(), + builder: _keepScaleBuilder())); +} + +WindowOptions getHiddenTitleBarWindowOptions(Size size) { + return WindowOptions( + size: size, + center: true, + backgroundColor: Colors.transparent, + skipTaskbar: false, + titleBarStyle: TitleBarStyle.hidden, + ); +} + class App extends StatelessWidget { @override Widget build(BuildContext context) { - final analytics = FirebaseAnalytics.instance; + // final analytics = FirebaseAnalytics.instance; return MultiProvider( providers: [ - ChangeNotifierProvider.value(value: FFI.ffiModel), - ChangeNotifierProvider.value(value: FFI.imageModel), - ChangeNotifierProvider.value(value: FFI.cursorModel), - ChangeNotifierProvider.value(value: FFI.canvasModel), + // global configuration + // use session related FFI when in remote control or file transfer page + ChangeNotifierProvider.value(value: gFFI.ffiModel), + ChangeNotifierProvider.value(value: gFFI.imageModel), + ChangeNotifierProvider.value(value: gFFI.cursorModel), + ChangeNotifierProvider.value(value: gFFI.canvasModel), + ChangeNotifierProvider.value(value: gFFI.abModel), + ChangeNotifierProvider.value(value: gFFI.userModel), ], - child: MaterialApp( - navigatorKey: globalKey, - debugShowCheckedModeBanner: false, - title: 'RustDesk', - theme: ThemeData( - primarySwatch: Colors.blue, - visualDensity: VisualDensity.adaptivePlatformDensity, - ), - home: !isAndroid ? WebHomePage() : HomePage(key: homeKey), - navigatorObservers: [ - FirebaseAnalyticsObserver(analytics: analytics), - FlutterSmartDialog.observer - ], - builder: FlutterSmartDialog.init( - builder: isAndroid - ? (_, child) => AccessibilityListener( - child: child, - ) - : null)), + child: GetMaterialApp( + navigatorKey: globalKey, + debugShowCheckedModeBanner: false, + title: 'RustDesk', + theme: getCurrentTheme(), + home: isDesktop + ? DesktopTabPage() + : !isAndroid + ? WebHomePage() + : HomePage(), + navigatorObservers: [ + // FirebaseAnalyticsObserver(analytics: analytics), + ], + builder: isAndroid + ? (_, child) => AccessibilityListener( + child: child, + ) + : _keepScaleBuilder(), + ), ); } } + +_keepScaleBuilder() { + return (BuildContext context, Widget? child) { + return MediaQuery( + data: MediaQuery.of(context).copyWith( + textScaleFactor: 1.0, + ), + child: child ?? Container(), + ); + }; +} diff --git a/flutter/lib/pages/chat_page.dart b/flutter/lib/mobile/pages/chat_page.dart similarity index 58% rename from flutter/lib/pages/chat_page.dart rename to flutter/lib/mobile/pages/chat_page.dart index ce139d062..738f34e89 100644 --- a/flutter/lib/pages/chat_page.dart +++ b/flutter/lib/mobile/pages/chat_page.dart @@ -3,10 +3,16 @@ import 'package:flutter/material.dart'; import 'package:flutter_hbb/common.dart'; import 'package:flutter_hbb/models/chat_model.dart'; import 'package:provider/provider.dart'; -import '../models/model.dart'; + import 'home_page.dart'; class ChatPage extends StatelessWidget implements PageShape { + late final ChatModel chatModel; + + ChatPage({ChatModel? chatModel}) { + this.chatModel = chatModel ?? gFFI.chatModel; + } + @override final title = translate("Chat"); @@ -18,7 +24,8 @@ class ChatPage extends StatelessWidget implements PageShape { PopupMenuButton( icon: Icon(Icons.group), itemBuilder: (context) { - final chatModel = FFI.chatModel; + // only mobile need [appBarActions], just bind gFFI.chatModel + final chatModel = gFFI.chatModel; return chatModel.messages.entries.map((entry) { final id = entry.key; final user = entry.value.chatUser; @@ -29,40 +36,43 @@ class ChatPage extends StatelessWidget implements PageShape { }).toList(); }, onSelected: (id) { - FFI.chatModel.changeCurrentID(id); + gFFI.chatModel.changeCurrentID(id); }) ]; @override Widget build(BuildContext context) { return ChangeNotifierProvider.value( - value: FFI.chatModel, + value: chatModel, child: Container( color: MyTheme.grayBg, child: Consumer(builder: (context, chatModel, child) { final currentUser = chatModel.currentUser; return Stack( children: [ - DashChat( - onSend: (chatMsg) { - chatModel.send(chatMsg); - }, - currentUser: chatModel.me, - messages: - chatModel.messages[chatModel.currentID]?.chatMessages ?? - [], - messageOptions: MessageOptions( - showOtherUsersAvatar: false, - showTime: true, - messageDecorationBuilder: (_, __, ___) => - defaultMessageDecoration( - color: MyTheme.accent80, - borderTopLeft: 8, - borderTopRight: 8, - borderBottomRight: 8, - borderBottomLeft: 8, - )), - ), + LayoutBuilder(builder: (context, constraints) { + return DashChat( + onSend: (chatMsg) { + chatModel.send(chatMsg); + }, + currentUser: chatModel.me, + messages: chatModel + .messages[chatModel.currentID]?.chatMessages ?? + [], + messageOptions: MessageOptions( + showOtherUsersAvatar: false, + showTime: true, + maxWidth: constraints.maxWidth * 0.7, + messageDecorationBuilder: (_, __, ___) => + defaultMessageDecoration( + color: MyTheme.accent80, + borderTopLeft: 8, + borderTopRight: 8, + borderBottomRight: 8, + borderBottomLeft: 8, + )), + ); + }), chatModel.currentID == ChatModel.clientModeID ? SizedBox.shrink() : Padding( diff --git a/flutter/lib/pages/connection_page.dart b/flutter/lib/mobile/pages/connection_page.dart similarity index 64% rename from flutter/lib/pages/connection_page.dart rename to flutter/lib/mobile/pages/connection_page.dart index 90f290136..ba34b31e8 100644 --- a/flutter/lib/pages/connection_page.dart +++ b/flutter/lib/mobile/pages/connection_page.dart @@ -1,15 +1,20 @@ +import 'dart:async'; + import 'package:flutter/material.dart'; -import 'package:flutter_hbb/pages/file_manager_page.dart'; +import 'package:flutter_hbb/mobile/pages/file_manager_page.dart'; import 'package:provider/provider.dart'; import 'package:url_launcher/url_launcher.dart'; -import 'dart:async'; -import '../common.dart'; -import '../models/model.dart'; + +import '../../common.dart'; +import '../../models/model.dart'; +import '../../models/peer_model.dart'; +import '../../models/platform_model.dart'; import 'home_page.dart'; import 'remote_page.dart'; -import 'settings_page.dart'; import 'scan_page.dart'; +import 'settings_page.dart'; +/// Connection page for connecting to a remote peer. class ConnectionPage extends StatefulWidget implements PageShape { ConnectionPage({Key? key}) : super(key: key); @@ -26,17 +31,32 @@ class ConnectionPage extends StatefulWidget implements PageShape { _ConnectionPageState createState() => _ConnectionPageState(); } +/// State for the connection page. class _ConnectionPageState extends State { + /// Controller for the id input bar. final _idController = TextEditingController(); + + /// Update url. If it's not null, means an update is available. var _updateUrl = ''; var _menuPos; @override void initState() { super.initState(); + if (_idController.text.isEmpty) { + () async { + final lastRemoteId = await bind.mainGetLastRemoteId(); + if (lastRemoteId != _idController.text) { + setState(() { + _idController.text = lastRemoteId; + }); + } + }(); + } if (isAndroid) { - Timer(Duration(seconds: 5), () { - _updateUrl = FFI.getByName('software_update_url'); + Timer(Duration(seconds: 5), () async { + _updateUrl = await bind.mainGetSoftwareUpdateUrl(); + ; if (_updateUrl.isNotEmpty) setState(() {}); }); } @@ -45,7 +65,6 @@ class _ConnectionPageState extends State { @override Widget build(BuildContext context) { Provider.of(context); - if (_idController.text.isEmpty) _idController.text = FFI.getId(); return SingleChildScrollView( child: Column( mainAxisAlignment: MainAxisAlignment.start, @@ -60,11 +79,15 @@ class _ConnectionPageState extends State { ); } + /// Callback for the connect button. + /// Connects to the selected peer. void onConnect() { var id = _idController.text.trim(); connect(id); } + /// Connect to a peer with [id]. + /// If [isFileTransfer], starts a session only for file transfer. void connect(String id, {bool isFileTransfer = false}) async { if (id == '') return; id = id.replaceAll(' ', ''); @@ -94,6 +117,8 @@ class _ConnectionPageState extends State { } } + /// UI for software update. + /// If [_updateUrl] is not empty, shows a button to update the software. Widget getUpdateUI() { return _updateUrl.isEmpty ? SizedBox(height: 0) @@ -114,6 +139,8 @@ class _ConnectionPageState extends State { color: Colors.white, fontWeight: FontWeight.bold)))); } + /// UI for the search bar. + /// Search for a peer and connect to it if the id exists. Widget getSearchBarUI() { var w = Padding( padding: const EdgeInsets.fromLTRB(16.0, 8.0, 16.0, 0.0), @@ -187,6 +214,7 @@ class _ConnectionPageState extends State { super.dispose(); } + /// Get the image for the current [platform]. Widget getPlatformImage(String platform) { platform = platform.toLowerCase(); if (platform == 'mac os') @@ -195,55 +223,66 @@ class _ConnectionPageState extends State { return Image.asset('assets/$platform.png', width: 24, height: 24); } + /// Get all the saved peers. Widget getPeers() { - final size = MediaQuery.of(context).size; + final windowWidth = MediaQuery.of(context).size.width; final space = 8.0; - var width = size.width - 2 * space; + var width = windowWidth - 2 * space; final minWidth = 320.0; - if (size.width > minWidth + 2 * space) { - final n = (size.width / (minWidth + 2 * space)).floor(); - width = size.width / n - 2 * space; + if (windowWidth > minWidth + 2 * space) { + final n = (windowWidth / (minWidth + 2 * space)).floor(); + width = windowWidth / n - 2 * space; } - final cards = []; - var peers = FFI.peers(); - peers.forEach((p) { - cards.add(Container( - width: width, - child: Card( - child: GestureDetector( - onTap: !isDesktop ? () => connect('${p.id}') : null, - onDoubleTap: isDesktop ? () => connect('${p.id}') : null, - onLongPressStart: (details) { - final x = details.globalPosition.dx; - final y = details.globalPosition.dy; - _menuPos = RelativeRect.fromLTRB(x, y, x, y); - showPeerMenu(context, p.id); - }, - child: ListTile( - contentPadding: const EdgeInsets.only(left: 12), - subtitle: Text('${p.username}@${p.hostname}'), - title: Text('${p.id}'), - leading: Container( - padding: const EdgeInsets.all(6), - child: getPlatformImage('${p.platform}'), - color: str2color('${p.id}${p.platform}', 0x7f)), - trailing: InkWell( - child: Padding( - padding: const EdgeInsets.all(12), - child: Icon(Icons.more_vert)), - onTapDown: (e) { - final x = e.globalPosition.dx; - final y = e.globalPosition.dy; - _menuPos = RelativeRect.fromLTRB(x, y, x, y); - }, - onTap: () { - showPeerMenu(context, p.id); - }), - ))))); - }); - return Wrap(children: cards, spacing: space, runSpacing: space); + return FutureBuilder>( + future: gFFI.peers(), + builder: (context, snapshot) { + final cards = []; + if (snapshot.hasData) { + final peers = snapshot.data!; + peers.forEach((p) { + cards.add(Container( + width: width, + child: Card( + child: GestureDetector( + onTap: + !isWebDesktop ? () => connect('${p.id}') : null, + onDoubleTap: + isWebDesktop ? () => connect('${p.id}') : null, + onLongPressStart: (details) { + final x = details.globalPosition.dx; + final y = details.globalPosition.dy; + _menuPos = RelativeRect.fromLTRB(x, y, x, y); + showPeerMenu(context, p.id); + }, + child: ListTile( + contentPadding: const EdgeInsets.only(left: 12), + subtitle: Text('${p.username}@${p.hostname}'), + title: Text('${p.id}'), + leading: Container( + padding: const EdgeInsets.all(6), + child: getPlatformImage('${p.platform}'), + color: str2color('${p.id}${p.platform}', 0x7f)), + trailing: InkWell( + child: Padding( + padding: const EdgeInsets.all(12), + child: Icon(Icons.more_vert)), + onTapDown: (e) { + final x = e.globalPosition.dx; + final y = e.globalPosition.dy; + _menuPos = RelativeRect.fromLTRB(x, y, x, y); + }, + onTap: () { + showPeerMenu(context, p.id); + }), + ))))); + }); + } + return Wrap(children: cards, spacing: space, runSpacing: space); + }); } + /// Show the peer menu and handle user's choice. + /// User might remove the peer or send a file to the peer. void showPeerMenu(BuildContext context, String id) async { var value = await showMenu( context: context, @@ -261,7 +300,7 @@ class _ConnectionPageState extends State { elevation: 8, ); if (value == 'remove') { - setState(() => FFI.setByName('remove', '$id')); + setState(() => bind.mainRemovePeer(id: id)); () async { removePreference(id); }(); @@ -277,10 +316,34 @@ class WebMenu extends StatefulWidget { } class _WebMenuState extends State { + String? username; + String url = ""; + + @override + void initState() { + super.initState(); + () async { + final usernameRes = await getUsername(); + final urlRes = await getUrl(); + var update = false; + if (usernameRes != username) { + username = usernameRes; + update = true; + } + if (urlRes != url) { + url = urlRes; + update = true; + } + + if (update) { + setState(() {}); + } + }(); + } + @override Widget build(BuildContext context) { Provider.of(context); - final username = getUsername(); return PopupMenuButton( icon: Icon(Icons.more_vert), itemBuilder: (context) { @@ -298,7 +361,7 @@ class _WebMenuState extends State { value: "server", ) ] + - (getUrl().contains('admin.rustdesk.com') + (url.contains('admin.rustdesk.com') ? >[] : [ PopupMenuItem( @@ -317,16 +380,16 @@ class _WebMenuState extends State { }, onSelected: (value) { if (value == 'server') { - showServerSettings(); + showServerSettings(gFFI.dialogManager); } if (value == 'about') { - showAbout(); + showAbout(gFFI.dialogManager); } if (value == 'login') { if (username == null) { - showLogin(); + showLogin(gFFI.dialogManager); } else { - logout(); + logout(gFFI.dialogManager); } } if (value == 'scan') { diff --git a/flutter/lib/pages/file_manager_page.dart b/flutter/lib/mobile/pages/file_manager_page.dart similarity index 96% rename from flutter/lib/pages/file_manager_page.dart rename to flutter/lib/mobile/pages/file_manager_page.dart index 7e9c39a4e..87169b987 100644 --- a/flutter/lib/pages/file_manager_page.dart +++ b/flutter/lib/mobile/pages/file_manager_page.dart @@ -1,14 +1,13 @@ import 'dart:async'; -import 'package:flutter/material.dart'; -import 'package:flutter_hbb/models/file_model.dart'; -import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; -import 'package:provider/provider.dart'; -import 'package:flutter_breadcrumb/flutter_breadcrumb.dart'; -import 'package:wakelock/wakelock.dart'; -import 'package:toggle_switch/toggle_switch.dart'; -import '../common.dart'; -import '../models/model.dart'; +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:toggle_switch/toggle_switch.dart'; +import 'package:wakelock/wakelock.dart'; + +import '../../common.dart'; import '../widgets/dialog.dart'; class FileManagerPage extends StatefulWidget { @@ -20,31 +19,34 @@ class FileManagerPage extends StatefulWidget { } class _FileManagerPageState extends State { - final model = FFI.fileModel; + final model = gFFI.fileModel; final _selectedItems = SelectedItems(); final _breadCrumbScroller = ScrollController(); @override void initState() { super.initState(); - FFI.connect(widget.id, isFileTransfer: true); - showLoading(translate('Connecting...')); - FFI.ffiModel.updateEventListener(widget.id); + gFFI.connect(widget.id, isFileTransfer: true); + WidgetsBinding.instance.addPostFrameCallback((_) { + gFFI.dialogManager + .showLoading(translate('Connecting...'), onCancel: closeConnection); + }); + gFFI.ffiModel.updateEventListener(widget.id); Wakelock.enable(); } @override void dispose() { model.onClose(); - FFI.close(); - SmartDialog.dismiss(); + gFFI.close(); + gFFI.dialogManager.dismissAll(); Wakelock.disable(); super.dispose(); } @override Widget build(BuildContext context) => ChangeNotifierProvider.value( - value: FFI.fileModel, + value: gFFI.fileModel, child: Consumer(builder: (_context, _model, _child) { return WillPopScope( onWillPop: () async { @@ -59,7 +61,9 @@ class _FileManagerPageState extends State { backgroundColor: MyTheme.grayBg, appBar: AppBar( leading: Row(children: [ - IconButton(icon: Icon(Icons.close), onPressed: clientClose), + IconButton( + icon: Icon(Icons.close), + onPressed: () => clientClose(gFFI.dialogManager)), ]), centerTitle: true, title: ToggleSwitch( @@ -140,8 +144,8 @@ class _FileManagerPageState extends State { model.toggleSelectMode(); } else if (v == "folder") { final name = TextEditingController(); - DialogManager.show( - (setState, close) => CustomAlertDialog( + gFFI.dialogManager + .show((setState, close) => CustomAlertDialog( title: Text(translate("Create Folder")), content: Column( mainAxisSize: MainAxisSize.min, diff --git a/flutter/lib/pages/home_page.dart b/flutter/lib/mobile/pages/home_page.dart similarity index 85% rename from flutter/lib/pages/home_page.dart rename to flutter/lib/mobile/pages/home_page.dart index fbf4fa0d2..05a6d6b51 100644 --- a/flutter/lib/pages/home_page.dart +++ b/flutter/lib/mobile/pages/home_page.dart @@ -1,9 +1,8 @@ import 'package:flutter/material.dart'; -import 'package:flutter_hbb/pages/chat_page.dart'; -import 'package:flutter_hbb/pages/server_page.dart'; -import 'package:flutter_hbb/pages/settings_page.dart'; -import '../common.dart'; -import '../widgets/overlay.dart'; +import 'package:flutter_hbb/mobile/pages/chat_page.dart'; +import 'package:flutter_hbb/mobile/pages/server_page.dart'; +import 'package:flutter_hbb/mobile/pages/settings_page.dart'; +import '../../common.dart'; import 'connection_page.dart'; abstract class PageShape extends Widget { @@ -12,10 +11,10 @@ abstract class PageShape extends Widget { final List appBarActions = []; } -final homeKey = GlobalKey<_HomePageState>(); - class HomePage extends StatefulWidget { - HomePage({Key? key}) : super(key: key); + static final homeKey = GlobalKey<_HomePageState>(); + + HomePage() : super(key: homeKey); @override _HomePageState createState() => _HomePageState(); @@ -79,8 +78,8 @@ class _HomePageState extends State { onTap: (index) => setState(() { // close chat overlay when go chat page if (index == 1 && _selectedIndex != index) { - hideChatIconOverlay(); - hideChatWindowOverlay(); + gFFI.chatModel.hideChatIconOverlay(); + gFFI.chatModel.hideChatWindowOverlay(); } _selectedIndex = index; }), diff --git a/flutter/lib/pages/remote_page.dart b/flutter/lib/mobile/pages/remote_page.dart similarity index 79% rename from flutter/lib/pages/remote_page.dart rename to flutter/lib/mobile/pages/remote_page.dart index a308496ea..ceb3df0ff 100644 --- a/flutter/lib/pages/remote_page.dart +++ b/flutter/lib/mobile/pages/remote_page.dart @@ -1,17 +1,19 @@ +import 'dart:async'; +import 'dart:ui' as ui; + import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_hbb/models/chat_model.dart'; -import 'package:flutter_hbb/widgets/gesture_help.dart'; -import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; -import 'package:provider/provider.dart'; import 'package:flutter/services.dart'; -import 'dart:ui' as ui; -import 'dart:async'; +import 'package:flutter_hbb/mobile/widgets/gesture_help.dart'; +import 'package:flutter_hbb/models/chat_model.dart'; +import 'package:provider/provider.dart'; import 'package:wakelock/wakelock.dart'; -import '../common.dart'; -import '../widgets/gestures.dart'; -import '../models/model.dart'; + +import '../../common.dart'; +import '../../models/model.dart'; +import '../../models/platform_model.dart'; import '../widgets/dialog.dart'; +import '../widgets/gestures.dart'; import '../widgets/overlay.dart'; final initText = '\1' * 1024; @@ -28,7 +30,7 @@ class RemotePage extends StatefulWidget { class _RemotePageState extends State { Timer? _interval; Timer? _timer; - bool _showBar = !isDesktop; + bool _showBar = !isWebDesktop; double _bottom = 0; String _value = ''; double _scale = 1; @@ -45,30 +47,32 @@ class _RemotePageState extends State { @override void initState() { super.initState(); - FFI.connect(widget.id); + gFFI.connect(widget.id); WidgetsBinding.instance.addPostFrameCallback((_) { SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, overlays: []); - showLoading(translate('Connecting...')); + gFFI.dialogManager + .showLoading(translate('Connecting...'), onCancel: closeConnection); _interval = Timer.periodic(Duration(milliseconds: 30), (timer) => interval()); }); Wakelock.enable(); _physicalFocusNode.requestFocus(); - FFI.ffiModel.updateEventListener(widget.id); - FFI.listenToMouse(true); + gFFI.ffiModel.updateEventListener(widget.id); + gFFI.listenToMouse(true); + gFFI.qualityMonitorModel.checkShowQualityMonitor(widget.id); } @override void dispose() { hideMobileActionsOverlay(); - FFI.listenToMouse(false); - FFI.invokeMethod("enable_soft_keyboard", true); + gFFI.listenToMouse(false); + gFFI.invokeMethod("enable_soft_keyboard", true); _mobileFocusNode.dispose(); _physicalFocusNode.dispose(); - FFI.close(); + gFFI.close(); _interval?.cancel(); _timer?.cancel(); - SmartDialog.dismiss(); + gFFI.dialogManager.dismissAll(); SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, overlays: SystemUiOverlay.values); Wakelock.disable(); @@ -76,7 +80,7 @@ class _RemotePageState extends State { } void resetTool() { - FFI.resetModifiers(); + gFFI.resetModifiers(); } bool isKeyboardShown() { @@ -93,10 +97,10 @@ class _RemotePageState extends State { if (v < 100) { SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, overlays: []); - // [pi.version.isNotEmpty] -> check ready or not,avoid login without soft-keyboard - if (chatWindowOverlayEntry == null && - FFI.ffiModel.pi.version.isNotEmpty) { - FFI.invokeMethod("enable_soft_keyboard", false); + // [pi.version.isNotEmpty] -> check ready or not, avoid login without soft-keyboard + if (gFFI.chatModel.chatWindowOverlayEntry == null && + gFFI.ffiModel.pi.version.isNotEmpty) { + gFFI.invokeMethod("enable_soft_keyboard", false); } } }); @@ -128,12 +132,12 @@ class _RemotePageState extends State { newValue[common] == oldValue[common]; ++common) {} for (i = 0; i < oldValue.length - common; ++i) { - FFI.inputKey('VK_BACK'); + gFFI.inputKey('VK_BACK'); } if (newValue.length > common) { var s = newValue.substring(common); if (s.length > 1) { - FFI.setByName('input_string', s); + bind.sessionInputString(id: widget.id, value: s); } else { inputChar(s); } @@ -151,7 +155,7 @@ class _RemotePageState extends State { // ? } else if (newValue.length < oldValue.length) { final char = 'VK_BACK'; - FFI.inputKey(char); + gFFI.inputKey(char); } else { final content = newValue.substring(oldValue.length); if (content.length > 1) { @@ -167,11 +171,11 @@ class _RemotePageState extends State { content == '()' || content == '【】')) { // can not only input content[0], because when input ], [ are also auo insert, which cause ] never be input - FFI.setByName('input_string', content); + bind.sessionInputString(id: widget.id, value: content); openKeyboard(); return; } - FFI.setByName('input_string', content); + bind.sessionInputString(id: widget.id, value: content); } else { inputChar(content); } @@ -184,11 +188,11 @@ class _RemotePageState extends State { } else if (char == ' ') { char = 'VK_SPACE'; } - FFI.inputKey(char); + gFFI.inputKey(char); } void openKeyboard() { - FFI.invokeMethod("enable_soft_keyboard", true); + gFFI.invokeMethod("enable_soft_keyboard", true); // destroy first, so that our _value trick can work _value = initText; setState(() => _showEdit = false); @@ -211,7 +215,7 @@ class _RemotePageState extends State { final label = _logicalKeyMap[e.logicalKey.keyId] ?? _physicalKeyMap[e.physicalKey.usbHidUsage] ?? e.logicalKey.keyLabel; - FFI.inputKey(label, down: down, press: press ?? false); + gFFI.inputKey(label, down: down, press: press ?? false); } @override @@ -219,11 +223,11 @@ class _RemotePageState extends State { final pi = Provider.of(context).pi; final hideKeyboard = isKeyboardShown() && _showEdit; final showActionButton = !_showBar || hideKeyboard; - final keyboard = FFI.ffiModel.permissions['keyboard'] != false; + final keyboard = gFFI.ffiModel.permissions['keyboard'] != false; return WillPopScope( onWillPop: () async { - clientClose(); + clientClose(gFFI.dialogManager); return false; }, child: getRawPointerAndKeyBody( @@ -241,7 +245,7 @@ class _RemotePageState extends State { setState(() { if (hideKeyboard) { _showEdit = false; - FFI.invokeMethod("enable_soft_keyboard", false); + gFFI.invokeMethod("enable_soft_keyboard", false); _mobileFocusNode.unfocus(); _physicalFocusNode.requestFocus(); } else { @@ -257,7 +261,7 @@ class _RemotePageState extends State { OverlayEntry(builder: (context) { return Container( color: Colors.black, - child: isDesktop + child: isWebDesktop ? getBodyForDesktopWithListener(keyboard) : SafeArea(child: OrientationBuilder(builder: (ctx, orientation) { @@ -265,7 +269,7 @@ class _RemotePageState extends State { Timer(Duration(milliseconds: 200), () { resetMobileActionsOverlay(); _currentOrientation = orientation; - FFI.canvasModel.updateViewStyle(); + gFFI.canvasModel.updateViewStyle(); }); } return Container( @@ -290,7 +294,7 @@ class _RemotePageState extends State { }); } if (_isPhysicalMouse) { - FFI.handleMouse(getEvent(e, 'mousemove')); + gFFI.handleMouse(getEvent(e, 'mousemove')); } }, onPointerDown: (e) { @@ -302,19 +306,19 @@ class _RemotePageState extends State { } } if (_isPhysicalMouse) { - FFI.handleMouse(getEvent(e, 'mousedown')); + gFFI.handleMouse(getEvent(e, 'mousedown')); } }, onPointerUp: (e) { if (e.kind != ui.PointerDeviceKind.mouse) return; if (_isPhysicalMouse) { - FFI.handleMouse(getEvent(e, 'mouseup')); + gFFI.handleMouse(getEvent(e, 'mouseup')); } }, onPointerMove: (e) { if (e.kind != ui.PointerDeviceKind.mouse) return; if (_isPhysicalMouse) { - FFI.handleMouse(getEvent(e, 'mousemove')); + gFFI.handleMouse(getEvent(e, 'mousemove')); } }, onPointerSignal: (e) { @@ -327,8 +331,9 @@ class _RemotePageState extends State { if (dy > 0) dy = -1; else if (dy < 0) dy = 1; - FFI.setByName( - 'send_mouse', '{"type": "wheel", "x": "$dx", "y": "$dy"}'); + bind.sessionSendMouse( + id: widget.id, + msg: '{"type": "wheel", "x": "$dx", "y": "$dy"}'); } }, child: MouseRegion( @@ -350,14 +355,14 @@ class _RemotePageState extends State { sendRawKey(e, press: true); } else { sendRawKey(e, down: true); - if (e.isAltPressed && !FFI.alt) { - FFI.alt = true; - } else if (e.isControlPressed && !FFI.ctrl) { - FFI.ctrl = true; - } else if (e.isShiftPressed && !FFI.shift) { - FFI.shift = true; - } else if (e.isMetaPressed && !FFI.command) { - FFI.command = true; + if (e.isAltPressed && !gFFI.alt) { + gFFI.alt = true; + } else if (e.isControlPressed && !gFFI.ctrl) { + gFFI.ctrl = true; + } else if (e.isShiftPressed && !gFFI.shift) { + gFFI.shift = true; + } else if (e.isMetaPressed && !gFFI.command) { + gFFI.command = true; } } } @@ -365,16 +370,16 @@ class _RemotePageState extends State { if (!_showEdit && e is RawKeyUpEvent) { if (key == LogicalKeyboardKey.altLeft || key == LogicalKeyboardKey.altRight) { - FFI.alt = false; + gFFI.alt = false; } else if (key == LogicalKeyboardKey.controlLeft || key == LogicalKeyboardKey.controlRight) { - FFI.ctrl = false; + gFFI.ctrl = false; } else if (key == LogicalKeyboardKey.shiftRight || key == LogicalKeyboardKey.shiftLeft) { - FFI.shift = false; + gFFI.shift = false; } else if (key == LogicalKeyboardKey.metaLeft || key == LogicalKeyboardKey.metaRight) { - FFI.command = false; + gFFI.command = false; } sendRawKey(e); } @@ -397,7 +402,7 @@ class _RemotePageState extends State { color: Colors.white, icon: Icon(Icons.clear), onPressed: () { - clientClose(); + clientClose(gFFI.dialogManager); }, ) ] + @@ -407,13 +412,13 @@ class _RemotePageState extends State { icon: Icon(Icons.tv), onPressed: () { setState(() => _showEdit = false); - showOptions(); + showOptions(widget.id, gFFI.dialogManager); }, ) ] + - (isDesktop + (isWebDesktop ? [] - : FFI.ffiModel.isPeerAndroid + : gFFI.ffiModel.isPeerAndroid ? [ IconButton( color: Colors.white, @@ -434,7 +439,7 @@ class _RemotePageState extends State { onPressed: openKeyboard), IconButton( color: Colors.white, - icon: Icon(FFI.ffiModel.touchMode + icon: Icon(gFFI.ffiModel.touchMode ? Icons.touch_app : Icons.mouse), onPressed: changeTouchMode, @@ -447,9 +452,9 @@ class _RemotePageState extends State { color: Colors.white, icon: Icon(Icons.message), onPressed: () { - FFI.chatModel + gFFI.chatModel .changeCurrentID(ChatModel.clientModeID); - toggleChatOverlay(); + gFFI.chatModel.toggleChatOverlay(); }, ) ]) + @@ -459,7 +464,7 @@ class _RemotePageState extends State { icon: Icon(Icons.more_vert), onPressed: () { setState(() => _showEdit = false); - showActions(); + showActions(widget.id); }, ), ]), @@ -486,101 +491,102 @@ class _RemotePageState extends State { Offset _cacheLongPressPosition = Offset(0, 0); Widget getBodyForMobileWithGesture() { - final touchMode = FFI.ffiModel.touchMode; + final touchMode = gFFI.ffiModel.touchMode; return getMixinGestureDetector( child: getBodyForMobile(), onTapUp: (d) { if (touchMode) { - FFI.cursorModel.touch( + gFFI.cursorModel.touch( d.localPosition.dx, d.localPosition.dy, MouseButtons.left); } else { - FFI.tap(MouseButtons.left); + gFFI.tap(MouseButtons.left); } }, onDoubleTapDown: (d) { if (touchMode) { - FFI.cursorModel.move(d.localPosition.dx, d.localPosition.dy); + gFFI.cursorModel.move(d.localPosition.dx, d.localPosition.dy); } }, onDoubleTap: () { - FFI.tap(MouseButtons.left); - FFI.tap(MouseButtons.left); + gFFI.tap(MouseButtons.left); + gFFI.tap(MouseButtons.left); }, onLongPressDown: (d) { if (touchMode) { + gFFI.cursorModel.move(d.localPosition.dx, d.localPosition.dy); _cacheLongPressPosition = d.localPosition; } }, onLongPress: () { if (touchMode) { - FFI.cursorModel + gFFI.cursorModel .move(_cacheLongPressPosition.dx, _cacheLongPressPosition.dy); } - FFI.tap(MouseButtons.right); + gFFI.tap(MouseButtons.right); }, onDoubleFinerTap: (d) { if (!touchMode) { - FFI.tap(MouseButtons.right); + gFFI.tap(MouseButtons.right); } }, onHoldDragStart: (d) { if (!touchMode) { - FFI.sendMouse('down', MouseButtons.left); + gFFI.sendMouse('down', MouseButtons.left); } }, onHoldDragUpdate: (d) { if (!touchMode) { - FFI.cursorModel.updatePan(d.delta.dx, d.delta.dy, touchMode); + gFFI.cursorModel.updatePan(d.delta.dx, d.delta.dy, touchMode); } }, onHoldDragEnd: (_) { if (!touchMode) { - FFI.sendMouse('up', MouseButtons.left); + gFFI.sendMouse('up', MouseButtons.left); } }, onOneFingerPanStart: (d) { if (touchMode) { - FFI.cursorModel.move(d.localPosition.dx, d.localPosition.dy); - FFI.sendMouse('down', MouseButtons.left); + gFFI.cursorModel.move(d.localPosition.dx, d.localPosition.dy); + gFFI.sendMouse('down', MouseButtons.left); } else { - final cursorX = FFI.cursorModel.x; - final cursorY = FFI.cursorModel.y; + final cursorX = gFFI.cursorModel.x; + final cursorY = gFFI.cursorModel.y; final visible = - FFI.cursorModel.getVisibleRect().inflate(1); // extend edges + gFFI.cursorModel.getVisibleRect().inflate(1); // extend edges final size = MediaQueryData.fromWindow(ui.window).size; if (!visible.contains(Offset(cursorX, cursorY))) { - FFI.cursorModel.move(size.width / 2, size.height / 2); + gFFI.cursorModel.move(size.width / 2, size.height / 2); } } }, onOneFingerPanUpdate: (d) { - FFI.cursorModel.updatePan(d.delta.dx, d.delta.dy, touchMode); + gFFI.cursorModel.updatePan(d.delta.dx, d.delta.dy, touchMode); }, onOneFingerPanEnd: (d) { if (touchMode) { - FFI.sendMouse('up', MouseButtons.left); + gFFI.sendMouse('up', MouseButtons.left); } }, // scale + pan event onTwoFingerScaleUpdate: (d) { - FFI.canvasModel.updateScale(d.scale / _scale); + gFFI.canvasModel.updateScale(d.scale / _scale); _scale = d.scale; - FFI.canvasModel.panX(d.focalPointDelta.dx); - FFI.canvasModel.panY(d.focalPointDelta.dy); + gFFI.canvasModel.panX(d.focalPointDelta.dx); + gFFI.canvasModel.panY(d.focalPointDelta.dy); }, onTwoFingerScaleEnd: (d) { _scale = 1; - FFI.setByName('peer_option', '{"name": "view-style", "value": ""}'); + bind.sessionPeerOption(id: widget.id, name: "view-style", value: ""); }, - onThreeFingerVerticalDragUpdate: FFI.ffiModel.isPeerAndroid + onThreeFingerVerticalDragUpdate: gFFI.ffiModel.isPeerAndroid ? null : (d) { _mouseScrollIntegral += d.delta.dy / 4; if (_mouseScrollIntegral > 1) { - FFI.scroll(1); + gFFI.scroll(1); _mouseScrollIntegral = 0; } else if (_mouseScrollIntegral < -1) { - FFI.scroll(-1); + gFFI.scroll(-1); _mouseScrollIntegral = 0; } }); @@ -617,8 +623,9 @@ class _RemotePageState extends State { Widget getBodyForDesktopWithListener(bool keyboard) { var paints = [ImagePaint()]; - if (keyboard || - FFI.getByName('toggle_option', 'show-remote-cursor') == 'true') { + final cursor = bind.sessionGetToggleOptionSync( + id: widget.id, arg: 'show-remote-cursor'); + if (keyboard || cursor) { paints.add(CursorPaint()); } return Container( @@ -626,15 +633,16 @@ class _RemotePageState extends State { } int lastMouseDownButtons = 0; + Map getEvent(PointerEvent evt, String type) { final Map out = {}; out['type'] = type; out['x'] = evt.position.dx; out['y'] = evt.position.dy; - if (FFI.alt) out['alt'] = 'true'; - if (FFI.shift) out['shift'] = 'true'; - if (FFI.ctrl) out['ctrl'] = 'true'; - if (FFI.command) out['command'] = 'true'; + if (gFFI.alt) out['alt'] = 'true'; + if (gFFI.shift) out['shift'] = 'true'; + if (gFFI.ctrl) out['ctrl'] = 'true'; + if (gFFI.command) out['command'] = 'true'; out['buttons'] = evt .buttons; // left button: 1, right button: 2, middle button: 4, 1 | 2 = 3 (left + right) if (evt.buttons != 0) { @@ -645,13 +653,13 @@ class _RemotePageState extends State { return out; } - void showActions() { + void showActions(String id) async { final size = MediaQuery.of(context).size; final x = 120.0; final y = size.height; final more = >[]; - final pi = FFI.ffiModel.pi; - final perms = FFI.ffiModel.permissions; + final pi = gFFI.ffiModel.pi; + final perms = gFFI.ffiModel.permissions; if (pi.version.isNotEmpty) { more.add(PopupMenuItem( child: Text(translate('Refresh')), value: 'refresh')); @@ -663,14 +671,13 @@ class _RemotePageState extends State { TextButton( style: flatButtonStyle, onPressed: () { - Navigator.pop(context); - showSetOSPassword(false); + showSetOSPassword(id, false, gFFI.dialogManager); }, child: Icon(Icons.edit, color: MyTheme.accent), ) ])), value: 'enter_os_password')); - if (!isDesktop) { + if (!isWebDesktop) { if (perms['keyboard'] != false && perms['clipboard'] != false) { more.add(PopupMenuItem( child: Text(translate('Paste')), value: 'paste')); @@ -687,14 +694,15 @@ class _RemotePageState extends State { more.add(PopupMenuItem( child: Text(translate('Insert Lock')), value: 'lock')); if (pi.platform == 'Windows' && - FFI.getByName('toggle_option', 'privacy-mode') != 'true') { + await bind.sessionGetToggleOption(id: id, arg: 'privacy-mode') != + true) { more.add(PopupMenuItem( - child: Text(translate( - (FFI.ffiModel.inputBlocked ? 'Unb' : 'B') + 'lock user input')), + child: Text(translate((gFFI.ffiModel.inputBlocked ? 'Unb' : 'B') + + 'lock user input')), value: 'block-input')); } } - if (FFI.ffiModel.permissions["restart"] != false && + if (gFFI.ffiModel.permissions["restart"] != false && (pi.platform == "Linux" || pi.platform == "Windows" || pi.platform == "Mac OS")) { @@ -709,33 +717,37 @@ class _RemotePageState extends State { elevation: 8, ); if (value == 'cad') { - FFI.setByName('ctrl_alt_del'); + bind.sessionCtrlAltDel(id: widget.id); } else if (value == 'lock') { - FFI.setByName('lock_screen'); + bind.sessionLockScreen(id: widget.id); } else if (value == 'block-input') { - FFI.setByName('toggle_option', - (FFI.ffiModel.inputBlocked ? 'un' : '') + 'block-input'); - FFI.ffiModel.inputBlocked = !FFI.ffiModel.inputBlocked; + bind.sessionToggleOption( + id: widget.id, + value: (gFFI.ffiModel.inputBlocked ? 'un' : '') + 'block-input'); + gFFI.ffiModel.inputBlocked = !gFFI.ffiModel.inputBlocked; } else if (value == 'refresh') { - FFI.setByName('refresh'); + bind.sessionRefresh(id: widget.id); } else if (value == 'paste') { () async { ClipboardData? data = await Clipboard.getData(Clipboard.kTextPlain); if (data != null && data.text != null) { - FFI.setByName('input_string', '${data.text}'); + bind.sessionInputString(id: widget.id, value: data.text ?? ""); } }(); } else if (value == 'enter_os_password') { - var password = FFI.getByName('peer_option', "os-password"); - if (password != "") { - FFI.setByName('input_os_password', password); + // FIXME: + // null means no session of id + // empty string means no password + var password = await bind.sessionGetOption(id: id, arg: "os-password"); + if (password != null) { + bind.sessionInputOsPassword(id: widget.id, value: password); } else { - showSetOSPassword(true); + showSetOSPassword(id, true, gFFI.dialogManager); } } else if (value == 'reset_canvas') { - FFI.cursorModel.reset(); + gFFI.cursorModel.reset(); } else if (value == 'restart') { - showRestartRemoteDevice(pi, widget.id); + showRestartRemoteDevice(pi, widget.id, gFFI.dialogManager); } }(); } @@ -754,12 +766,12 @@ class _RemotePageState extends State { return SingleChildScrollView( padding: EdgeInsets.symmetric(vertical: 10), child: GestureHelp( - touchMode: FFI.ffiModel.touchMode, + touchMode: gFFI.ffiModel.touchMode, onTouchModeChange: (t) { - FFI.ffiModel.toggleTouchMode(); - final v = FFI.ffiModel.touchMode ? 'Y' : ''; - FFI.setByName('peer_option', - '{"name": "touch-mode", "value": "$v"}'); + gFFI.ffiModel.toggleTouchMode(); + final v = gFFI.ffiModel.touchMode ? 'Y' : ''; + bind.sessionPeerOption( + id: widget.id, name: "touch", value: v); })); })); } @@ -790,21 +802,21 @@ class _RemotePageState extends State { style: TextStyle(color: Colors.white, fontSize: 11)), onPressed: onPressed); }; - final pi = FFI.ffiModel.pi; + final pi = gFFI.ffiModel.pi; final isMac = pi.platform == "Mac OS"; final modifiers = [ wrap('Ctrl ', () { - setState(() => FFI.ctrl = !FFI.ctrl); - }, FFI.ctrl), + setState(() => gFFI.ctrl = !gFFI.ctrl); + }, gFFI.ctrl), wrap(' Alt ', () { - setState(() => FFI.alt = !FFI.alt); - }, FFI.alt), + setState(() => gFFI.alt = !gFFI.alt); + }, gFFI.alt), wrap('Shift', () { - setState(() => FFI.shift = !FFI.shift); - }, FFI.shift), + setState(() => gFFI.shift = !gFFI.shift); + }, gFFI.shift), wrap(isMac ? ' Cmd ' : ' Win ', () { - setState(() => FFI.command = !FFI.command); - }, FFI.command), + setState(() => gFFI.command = !gFFI.command); + }, gFFI.command), ]; final keys = [ wrap( @@ -836,44 +848,44 @@ class _RemotePageState extends State { for (var i = 1; i <= 12; ++i) { final name = 'F' + i.toString(); fn.add(wrap(name, () { - FFI.inputKey('VK_' + name); + gFFI.inputKey('VK_' + name); })); } final more = [ SizedBox(width: 9999), wrap('Esc', () { - FFI.inputKey('VK_ESCAPE'); + gFFI.inputKey('VK_ESCAPE'); }), wrap('Tab', () { - FFI.inputKey('VK_TAB'); + gFFI.inputKey('VK_TAB'); }), wrap('Home', () { - FFI.inputKey('VK_HOME'); + gFFI.inputKey('VK_HOME'); }), wrap('End', () { - FFI.inputKey('VK_END'); + gFFI.inputKey('VK_END'); }), wrap('Del', () { - FFI.inputKey('VK_DELETE'); + gFFI.inputKey('VK_DELETE'); }), wrap('PgUp', () { - FFI.inputKey('VK_PRIOR'); + gFFI.inputKey('VK_PRIOR'); }), wrap('PgDn', () { - FFI.inputKey('VK_NEXT'); + gFFI.inputKey('VK_NEXT'); }), SizedBox(width: 9999), wrap('', () { - FFI.inputKey('VK_LEFT'); + gFFI.inputKey('VK_LEFT'); }, false, Icons.keyboard_arrow_left), wrap('', () { - FFI.inputKey('VK_UP'); + gFFI.inputKey('VK_UP'); }, false, Icons.keyboard_arrow_up), wrap('', () { - FFI.inputKey('VK_DOWN'); + gFFI.inputKey('VK_DOWN'); }, false, Icons.keyboard_arrow_down), wrap('', () { - FFI.inputKey('VK_RIGHT'); + gFFI.inputKey('VK_RIGHT'); }, false, Icons.keyboard_arrow_right), wrap(isMac ? 'Cmd+C' : 'Ctrl+C', () { sendPrompt(isMac, 'VK_C'); @@ -906,7 +918,7 @@ class ImagePaint extends StatelessWidget { Widget build(BuildContext context) { final m = Provider.of(context); final c = Provider.of(context); - final adjust = FFI.cursorModel.adjustForKeyboard(); + final adjust = gFFI.cursorModel.adjustForKeyboard(); var s = c.scale; return CustomPaint( painter: new ImagePainter( @@ -920,7 +932,7 @@ class CursorPaint extends StatelessWidget { Widget build(BuildContext context) { final m = Provider.of(context); final c = Provider.of(context); - final adjust = FFI.cursorModel.adjustForKeyboard(); + final adjust = gFFI.cursorModel.adjustForKeyboard(); var s = c.scale; return CustomPaint( painter: new ImagePainter( @@ -961,7 +973,7 @@ class ImagePainter extends CustomPainter { class QualityMonitor extends StatelessWidget { @override Widget build(BuildContext context) => ChangeNotifierProvider.value( - value: FFI.qualityMonitorModel, + value: gFFI.qualityMonitorModel, child: Consumer( builder: (context, qualityMonitorModel, child) => Positioned( top: 10, @@ -974,23 +986,23 @@ class QualityMonitor extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - "Speed: ${qualityMonitorModel.data.speed}", + "Speed: ${qualityMonitorModel.data.speed ?? ''}", style: TextStyle(color: MyTheme.grayBg), ), Text( - "FPS: ${qualityMonitorModel.data.fps}", + "FPS: ${qualityMonitorModel.data.fps ?? ''}", style: TextStyle(color: MyTheme.grayBg), ), Text( - "Delay: ${qualityMonitorModel.data.delay} ms", + "Delay: ${qualityMonitorModel.data.delay ?? ''} ms", style: TextStyle(color: MyTheme.grayBg), ), Text( - "Target Bitrate: ${qualityMonitorModel.data.targetBitrate}kb", + "Target Bitrate: ${qualityMonitorModel.data.targetBitrate ?? ''}kb", style: TextStyle(color: MyTheme.grayBg), ), Text( - "Codec: ${qualityMonitorModel.data.codecFormat}", + "Codec: ${qualityMonitorModel.data.codecFormat ?? ''}", style: TextStyle(color: MyTheme.grayBg), ), ], @@ -999,29 +1011,14 @@ class QualityMonitor extends StatelessWidget { : SizedBox.shrink()))); } -CheckboxListTile getToggle( - void Function(void Function()) setState, option, name) { - return CheckboxListTile( - value: FFI.getByName('toggle_option', option) == 'true', - onChanged: (v) { - setState(() { - FFI.setByName('toggle_option', option); - }); - if (option == "show-quality-monitor") { - FFI.qualityMonitorModel.checkShowQualityMonitor(); - } - }, - dense: true, - title: Text(translate(name))); -} - -void showOptions() { - String quality = FFI.getByName('image_quality'); +void showOptions(String id, OverlayDialogManager dialogManager) async { + String quality = await bind.sessionGetImageQuality(id: id) ?? 'balanced'; if (quality == '') quality = 'balanced'; - String viewStyle = FFI.getByName('peer_option', 'view-style'); + String viewStyle = + await bind.sessionGetOption(id: id, arg: 'view-style') ?? ''; var displays = []; - final pi = FFI.ffiModel.pi; - final image = FFI.ffiModel.getConnectionImage(); + final pi = gFFI.ffiModel.pi; + final image = gFFI.ffiModel.getConnectionImage(); if (image != null) displays.add(Padding(padding: const EdgeInsets.only(top: 8), child: image)); if (pi.displays.length > 1) { @@ -1031,8 +1028,8 @@ void showOptions() { children.add(InkWell( onTap: () { if (i == cur) return; - FFI.setByName('switch_display', i.toString()); - SmartDialog.dismiss(); + bind.sessionSwitchDisplay(id: id, value: i); + gFFI.dialogManager.dismissAll(); }, child: Ink( width: 40, @@ -1055,36 +1052,36 @@ void showOptions() { if (displays.isNotEmpty) { displays.add(Divider(color: MyTheme.border)); } - final perms = FFI.ffiModel.permissions; + final perms = gFFI.ffiModel.permissions; - DialogManager.show((setState, close) { + dialogManager.show((setState, close) { final more = []; if (perms['audio'] != false) { - more.add(getToggle(setState, 'disable-audio', 'Mute')); + more.add(getToggle(id, setState, 'disable-audio', 'Mute')); } if (perms['keyboard'] != false) { if (perms['clipboard'] != false) - more.add(getToggle(setState, 'disable-clipboard', 'Disable clipboard')); + more.add( + getToggle(id, setState, 'disable-clipboard', 'Disable clipboard')); more.add(getToggle( - setState, 'lock-after-session-end', 'Lock after session end')); + id, setState, 'lock-after-session-end', 'Lock after session end')); if (pi.platform == 'Windows') { - more.add(getToggle(setState, 'privacy-mode', 'Privacy mode')); + more.add(getToggle(id, setState, 'privacy-mode', 'Privacy mode')); } } var setQuality = (String? value) { if (value == null) return; setState(() { quality = value; - FFI.setByName('image_quality', value); + bind.sessionSetImageQuality(id: id, value: value); }); }; var setViewStyle = (String? value) { if (value == null) return; setState(() { viewStyle = value; - FFI.setByName( - 'peer_option', '{"name": "view-style", "value": "$value"}'); - FFI.canvasModel.updateViewStyle(); + bind.sessionPeerOption(id: id, name: "view-style", value: value); + gFFI.canvasModel.updateViewStyle(); }); }; return CustomAlertDialog( @@ -1101,9 +1098,10 @@ void showOptions() { getRadio('Balanced', 'balanced', quality, setQuality), getRadio('Optimize reaction time', 'low', quality, setQuality), Divider(color: MyTheme.border), - getToggle(setState, 'show-remote-cursor', 'Show remote cursor'), getToggle( - setState, 'show-quality-monitor', 'Show quality monitor'), + id, setState, 'show-remote-cursor', 'Show remote cursor'), + getToggle(id, setState, 'show-quality-monitor', + 'Show quality monitor'), ] + more), actions: [], @@ -1112,33 +1110,13 @@ void showOptions() { }, clickMaskDismiss: true, backDismiss: true); } -void showRestartRemoteDevice(PeerInfo pi, String id) async { - final res = - await DialogManager.show((setState, close) => CustomAlertDialog( - title: Row(children: [ - Icon(Icons.warning_amber_sharp, - color: Colors.redAccent, size: 28), - SizedBox(width: 10), - Text(translate("Restart Remote Device")), - ]), - content: Text( - "${translate('Are you sure you want to restart')} \n${pi.username}@${pi.hostname}($id) ?"), - actions: [ - TextButton( - onPressed: () => close(), child: Text(translate("Cancel"))), - ElevatedButton( - onPressed: () => close(true), child: Text(translate("OK"))), - ], - )); - if (res == true) FFI.setByName('restart_remote_device'); -} - -void showSetOSPassword(bool login) { +void showSetOSPassword( + String id, bool login, OverlayDialogManager dialogManager) async { final controller = TextEditingController(); - var password = FFI.getByName('peer_option', "os-password"); - var autoLogin = FFI.getByName('peer_option', "auto-login") != ""; + var password = await bind.sessionGetOption(id: id, arg: "os-password") ?? ""; + var autoLogin = await bind.sessionGetOption(id: id, arg: "auto-login") != ""; controller.text = password; - DialogManager.show((setState, close) { + dialogManager.show((setState, close) { return CustomAlertDialog( title: Text(translate('OS Password')), content: Column(mainAxisSize: MainAxisSize.min, children: [ @@ -1169,12 +1147,11 @@ void showSetOSPassword(bool login) { style: flatButtonStyle, onPressed: () { var text = controller.text.trim(); - FFI.setByName( - 'peer_option', '{"name": "os-password", "value": "$text"}'); - FFI.setByName('peer_option', - '{"name": "auto-login", "value": "${autoLogin ? 'Y' : ''}"}'); + bind.sessionPeerOption(id: id, name: "os-password", value: text); + bind.sessionPeerOption( + id: id, name: "auto-login", value: autoLogin ? 'Y' : ''); if (text != "" && login) { - FFI.setByName('input_os_password', text); + bind.sessionInputOsPassword(id: id, value: text); } close(); }, @@ -1185,17 +1162,17 @@ void showSetOSPassword(bool login) { } void sendPrompt(bool isMac, String key) { - final old = isMac ? FFI.command : FFI.ctrl; + final old = isMac ? gFFI.command : gFFI.ctrl; if (isMac) { - FFI.command = true; + gFFI.command = true; } else { - FFI.ctrl = true; + gFFI.ctrl = true; } - FFI.inputKey(key); + gFFI.inputKey(key); if (isMac) { - FFI.command = old; + gFFI.command = old; } else { - FFI.ctrl = old; + gFFI.ctrl = old; } } diff --git a/flutter/lib/pages/scan_page.dart b/flutter/lib/mobile/pages/scan_page.dart similarity index 64% rename from flutter/lib/pages/scan_page.dart rename to flutter/lib/mobile/pages/scan_page.dart index 0bc6dfb21..2487c0f58 100644 --- a/flutter/lib/pages/scan_page.dart +++ b/flutter/lib/mobile/pages/scan_page.dart @@ -1,13 +1,15 @@ -import 'package:flutter/material.dart'; -import 'package:qr_code_scanner/qr_code_scanner.dart'; -import 'package:image_picker/image_picker.dart'; -import 'package:image/image.dart' as img; -import 'package:zxing2/qrcode.dart'; -import 'dart:io'; import 'dart:async'; import 'dart:convert'; -import '../common.dart'; -import '../models/model.dart'; +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:image/image.dart' as img; +import 'package:image_picker/image_picker.dart'; +import 'package:qr_code_scanner/qr_code_scanner.dart'; +import 'package:zxing2/qrcode.dart'; + +import '../../common.dart'; +import '../../models/platform_model.dart'; class ScanPage extends StatefulWidget { @override @@ -119,7 +121,7 @@ class _ScanPageState extends State { void _onPermissionSet(BuildContext context, QRViewController ctrl, bool p) { if (!p) { - showToast('No permisssion'); + showToast('No permission'); } } @@ -130,7 +132,7 @@ class _ScanPageState extends State { } void showServerSettingFromQr(String data) async { - backToHome(); + closeConnection(); await controller?.pauseCamera(); if (!data.startsWith('config=')) { showToast('Invalid QR code'); @@ -142,7 +144,7 @@ class _ScanPageState extends State { var key = values['key'] != null ? values['key'] as String : ''; var api = values['api'] != null ? values['api'] as String : ''; Timer(Duration(milliseconds: 60), () { - showServerSettingsWithValue(host, '', key, api); + showServerSettingsWithValue(host, '', key, api, gFFI.dialogManager); }); } catch (e) { showToast('Invalid QR code'); @@ -150,55 +152,81 @@ class _ScanPageState extends State { } } -void showServerSettingsWithValue( - String id, String relay, String key, String api) { - final formKey = GlobalKey(); - final id0 = FFI.getByName('option', 'custom-rendezvous-server'); - final relay0 = FFI.getByName('option', 'relay-server'); - final api0 = FFI.getByName('option', 'api-server'); - final key0 = FFI.getByName('option', 'key'); - DialogManager.show((setState, close) { +void showServerSettingsWithValue(String id, String relay, String key, + String api, OverlayDialogManager dialogManager) async { + Map oldOptions = jsonDecode(await bind.mainGetOptions()); + String id0 = oldOptions['custom-rendezvous-server'] ?? ""; + String relay0 = oldOptions['relay-server'] ?? ""; + String api0 = oldOptions['api-server'] ?? ""; + String key0 = oldOptions['key'] ?? ""; + var isInProgress = false; + final idController = TextEditingController(text: id); + final relayController = TextEditingController(text: relay); + final apiController = TextEditingController(text: api); + + String? idServerMsg; + String? relayServerMsg; + String? apiServerMsg; + + dialogManager.show((setState, close) { + Future validate() async { + if (idController.text != id) { + final res = await validateAsync(idController.text); + setState(() => idServerMsg = res); + if (idServerMsg != null) return false; + id = idController.text; + } + if (relayController.text != relay) { + relayServerMsg = await validateAsync(relayController.text); + if (relayServerMsg != null) return false; + relay = relayController.text; + } + if (apiController.text != relay) { + apiServerMsg = await validateAsync(apiController.text); + if (apiServerMsg != null) return false; + api = apiController.text; + } + return true; + } + return CustomAlertDialog( title: Text(translate('ID/Relay Server')), content: Form( - key: formKey, child: Column( mainAxisSize: MainAxisSize.min, children: [ TextFormField( - initialValue: id, + controller: idController, decoration: InputDecoration( - labelText: translate('ID Server'), - ), - validator: validate, - onSaved: (String? value) { - if (value != null) id = value.trim(); - }, + labelText: translate('ID Server'), + errorText: idServerMsg), ) ] + (isAndroid ? [ TextFormField( - initialValue: relay, + controller: relayController, decoration: InputDecoration( - labelText: translate('Relay Server'), - ), - validator: validate, - onSaved: (String? value) { - if (value != null) relay = value.trim(); - }, + labelText: translate('Relay Server'), + errorText: relayServerMsg), ) ] : []) + [ TextFormField( - initialValue: api, + controller: apiController, decoration: InputDecoration( labelText: translate('API Server'), ), - validator: validate, - onSaved: (String? value) { - if (value != null) api = value.trim(); + autovalidateMode: AutovalidateMode.onUserInteraction, + validator: (v) { + if (v != null && v.length > 0) { + if (!(v.startsWith('http://') || + v.startsWith("https://"))) { + return translate("invalid_http"); + } + } + return apiServerMsg; }, ), TextFormField( @@ -206,11 +234,13 @@ void showServerSettingsWithValue( decoration: InputDecoration( labelText: 'Key', ), - validator: null, - onSaved: (String? value) { + onChanged: (String? value) { if (value != null) key = value.trim(); }, ), + Offstage( + offstage: !isInProgress, + child: LinearProgressIndicator()) ])), actions: [ TextButton( @@ -222,24 +252,28 @@ void showServerSettingsWithValue( ), TextButton( style: flatButtonStyle, - onPressed: () { - if (formKey.currentState != null && - formKey.currentState!.validate()) { - formKey.currentState!.save(); - if (id != id0) - FFI.setByName('option', - '{"name": "custom-rendezvous-server", "value": "$id"}'); + onPressed: () async { + setState(() { + idServerMsg = null; + relayServerMsg = null; + apiServerMsg = null; + isInProgress = true; + }); + if (await validate()) { + if (id != id0) { + bind.mainSetOption(key: "custom-rendezvous-server", value: id); + } if (relay != relay0) - FFI.setByName( - 'option', '{"name": "relay-server", "value": "$relay"}'); - if (key != key0) - FFI.setByName('option', '{"name": "key", "value": "$key"}'); + bind.mainSetOption(key: "relay-server", value: relay); + if (key != key0) bind.mainSetOption(key: "key", value: key); if (api != api0) - FFI.setByName( - 'option', '{"name": "api-server", "value": "$api"}'); - FFI.ffiModel.updateUser(); + bind.mainSetOption(key: "api-server", value: api); + gFFI.ffiModel.updateUser(); close(); } + setState(() { + isInProgress = false; + }); }, child: Text(translate('OK')), ), @@ -248,11 +282,11 @@ void showServerSettingsWithValue( }); } -String? validate(value) { +Future validateAsync(String value) async { value = value.trim(); if (value.isEmpty) { return null; } - final res = FFI.getByName('test_if_valid_server', value); + final res = await bind.mainTestIfValidServer(server: value); return res.isEmpty ? null : res; } diff --git a/flutter/lib/pages/server_page.dart b/flutter/lib/mobile/pages/server_page.dart similarity index 87% rename from flutter/lib/pages/server_page.dart rename to flutter/lib/mobile/pages/server_page.dart index 8e79466d6..00c433fd8 100644 --- a/flutter/lib/pages/server_page.dart +++ b/flutter/lib/mobile/pages/server_page.dart @@ -1,17 +1,13 @@ -import 'dart:convert'; - import 'package:flutter/material.dart'; -import 'package:flutter_hbb/models/model.dart'; -import 'package:flutter_hbb/widgets/dialog.dart'; -import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; +import 'package:flutter_hbb/mobile/widgets/dialog.dart'; import 'package:provider/provider.dart'; -import '../common.dart'; -import '../models/server_model.dart'; +import '../../common.dart'; +import '../../models/platform_model.dart'; +import '../../models/server_model.dart'; import 'home_page.dart'; -import '../models/model.dart'; -class ServerPage extends StatelessWidget implements PageShape { +class ServerPage extends StatefulWidget implements PageShape { @override final title = translate("Share Screen"); @@ -35,14 +31,14 @@ class ServerPage extends StatelessWidget implements PageShape { padding: EdgeInsets.symmetric(horizontal: 16.0), value: "setPermanentPassword", enabled: - FFI.serverModel.verificationMethod != kUseTemporaryPassword, + gFFI.serverModel.verificationMethod != kUseTemporaryPassword, ), PopupMenuItem( child: Text(translate("Set temporary password length")), padding: EdgeInsets.symmetric(horizontal: 16.0), value: "setTemporaryPasswordLength", enabled: - FFI.serverModel.verificationMethod != kUsePermanentPassword, + gFFI.serverModel.verificationMethod != kUsePermanentPassword, ), const PopupMenuDivider(), PopupMenuItem( @@ -53,7 +49,7 @@ class ServerPage extends StatelessWidget implements PageShape { title: Text(translate("Use temporary password")), trailing: Icon( Icons.check, - color: FFI.serverModel.verificationMethod == + color: gFFI.serverModel.verificationMethod == kUseTemporaryPassword ? null : Color(0xFFFFFFFF), @@ -66,7 +62,7 @@ class ServerPage extends StatelessWidget implements PageShape { title: Text(translate("Use permanent password")), trailing: Icon( Icons.check, - color: FFI.serverModel.verificationMethod == + color: gFFI.serverModel.verificationMethod == kUsePermanentPassword ? null : Color(0xFFFFFFFF), @@ -79,9 +75,9 @@ class ServerPage extends StatelessWidget implements PageShape { title: Text(translate("Use both passwords")), trailing: Icon( Icons.check, - color: FFI.serverModel.verificationMethod != + color: gFFI.serverModel.verificationMethod != kUseTemporaryPassword && - FFI.serverModel.verificationMethod != + gFFI.serverModel.verificationMethod != kUsePermanentPassword ? null : Color(0xFFFFFFFF), @@ -93,29 +89,37 @@ class ServerPage extends StatelessWidget implements PageShape { if (value == "changeID") { // TODO } else if (value == "setPermanentPassword") { - setPermanentPasswordDialog(); + setPermanentPasswordDialog(gFFI.dialogManager); } else if (value == "setTemporaryPasswordLength") { - setTemporaryPasswordLengthDialog(); + setTemporaryPasswordLengthDialog(gFFI.dialogManager); } else if (value == kUsePermanentPassword || value == kUseTemporaryPassword || value == kUseBothPasswords) { - Map msg = Map() - ..["name"] = "verification-method" - ..["value"] = value; - FFI.setByName('option', jsonEncode(msg)); - FFI.serverModel.updatePasswordModel(); + bind.mainSetOption(key: "verification-method", value: value); + gFFI.serverModel.updatePasswordModel(); } }) ]; + @override + State createState() => _ServerPageState(); +} + +class _ServerPageState extends State { + @override + void initState() { + super.initState(); + gFFI.serverModel.checkAndroidPermission(); + } + @override Widget build(BuildContext context) { checkService(); return ChangeNotifierProvider.value( - value: FFI.serverModel, + value: gFFI.serverModel, child: Consumer( builder: (context, serverModel, child) => SingleChildScrollView( - controller: FFI.serverModel.controller, + controller: gFFI.serverModel.controller, child: Center( child: Column( mainAxisAlignment: MainAxisAlignment.start, @@ -132,16 +136,16 @@ class ServerPage extends StatelessWidget implements PageShape { } void checkService() async { - FFI.invokeMethod("check_service"); // jvm + gFFI.invokeMethod("check_service"); // jvm // for Android 10/11,MANAGE_EXTERNAL_STORAGE permission from a system setting page - if (PermissionManager.isWaitingFile() && !FFI.serverModel.fileOk) { + if (PermissionManager.isWaitingFile() && !gFFI.serverModel.fileOk) { PermissionManager.complete("file", await PermissionManager.check("file")); debugPrint("file permission finished"); } } class ServerInfo extends StatelessWidget { - final model = FFI.serverModel; + final model = gFFI.serverModel; final emptyController = TextEditingController(text: "-"); @override @@ -183,9 +187,8 @@ class ServerInfo extends StatelessWidget { ? null : IconButton( icon: const Icon(Icons.refresh), - onPressed: () { - FFI.setByName("temporary_password"); - })), + onPressed: () => + bind.mainUpdateTemporaryPassword())), onSaved: (String? value) {}, ), ], @@ -356,12 +359,12 @@ class ConnectionManager extends StatelessWidget { Widget build(BuildContext context) { final serverModel = Provider.of(context); return Column( - children: serverModel.clients.entries - .map((entry) => PaddingCard( - title: translate(entry.value.isFileTransfer + children: serverModel.clients + .map((client) => PaddingCard( + title: translate(client.isFileTransfer ? "File Connection" : "Screen Connection"), - titleIcon: entry.value.isFileTransfer + titleIcon: client.isFileTransfer ? Icons.folder_outlined : Icons.mobile_screen_share, child: Column( @@ -370,16 +373,14 @@ class ConnectionManager extends StatelessWidget { Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Expanded(child: clientInfo(entry.value)), + Expanded(child: clientInfo(client)), Expanded( flex: -1, - child: entry.value.isFileTransfer || - !entry.value.authorized + child: client.isFileTransfer || !client.authorized ? SizedBox.shrink() : IconButton( onPressed: () { - FFI.chatModel - .changeCurrentID(entry.value.id); + gFFI.chatModel.changeCurrentID(client.id); final bar = navigationBarKey.currentWidget; if (bar != null) { @@ -393,37 +394,35 @@ class ConnectionManager extends StatelessWidget { ))) ], ), - entry.value.authorized + client.authorized ? SizedBox.shrink() : Text( translate("android_new_connection_tip"), style: TextStyle(color: Colors.black54), ), - entry.value.authorized + client.authorized ? ElevatedButton.icon( style: ButtonStyle( backgroundColor: MaterialStateProperty.all(Colors.red)), icon: Icon(Icons.close), onPressed: () { - FFI.setByName("close_conn", entry.key.toString()); - FFI.invokeMethod( - "cancel_notification", entry.key); + bind.cmCloseConnection(connId: client.id); + gFFI.invokeMethod( + "cancel_notification", client.id); }, label: Text(translate("Close"))) : Row(children: [ TextButton( child: Text(translate("Dismiss")), onPressed: () { - serverModel.sendLoginResponse( - entry.value, false); + serverModel.sendLoginResponse(client, false); }), SizedBox(width: 20), ElevatedButton( child: Text(translate("Accept")), onPressed: () { - serverModel.sendLoginResponse( - entry.value, true); + serverModel.sendLoginResponse(client, true); }), ]), ], @@ -511,15 +510,15 @@ Widget clientInfo(Client client) { ])); } -void toAndroidChannelInit() { - FFI.setMethodCallHandler((method, arguments) { +void androidChannelInit() { + gFFI.setMethodCallHandler((method, arguments) { debugPrint("flutter got android msg,$method,$arguments"); try { switch (method) { case "start_capture": { - SmartDialog.dismiss(); - FFI.serverModel.updateClientState(); + gFFI.dialogManager.dismissAll(); + gFFI.serverModel.updateClientState(); break; } case "on_state_changed": @@ -527,7 +526,7 @@ void toAndroidChannelInit() { var name = arguments["name"] as String; var value = arguments["value"] as String == "true"; debugPrint("from jvm:on_state_changed,$name:$value"); - FFI.serverModel.changeStatue(name, value); + gFFI.serverModel.changeStatue(name, value); break; } case "on_android_permission_result": @@ -539,7 +538,7 @@ void toAndroidChannelInit() { } case "on_media_projection_canceled": { - FFI.serverModel.stopService(); + gFFI.serverModel.stopService(); break; } } diff --git a/flutter/lib/pages/settings_page.dart b/flutter/lib/mobile/pages/settings_page.dart similarity index 77% rename from flutter/lib/pages/settings_page.dart rename to flutter/lib/mobile/pages/settings_page.dart index bb1238494..be8403427 100644 --- a/flutter/lib/pages/settings_page.dart +++ b/flutter/lib/mobile/pages/settings_page.dart @@ -1,14 +1,16 @@ import 'dart:async'; - -import 'package:settings_ui/settings_ui.dart'; -import 'package:flutter/material.dart'; -import 'package:url_launcher/url_launcher.dart'; -import 'package:provider/provider.dart'; import 'dart:convert'; + +import 'package:flutter/material.dart'; import 'package:http/http.dart' as http; -import '../common.dart'; +import 'package:provider/provider.dart'; +import 'package:settings_ui/settings_ui.dart'; +import 'package:url_launcher/url_launcher.dart'; + +import '../../common.dart'; +import '../../models/model.dart'; +import '../../models/platform_model.dart'; import '../widgets/dialog.dart'; -import '../models/model.dart'; import 'home_page.dart'; import 'scan_page.dart'; @@ -29,15 +31,38 @@ class SettingsPage extends StatefulWidget implements PageShape { const url = 'https://rustdesk.com/'; final _hasIgnoreBattery = androidVersion >= 26; var _ignoreBatteryOpt = false; +var _enableAbr = false; class _SettingsState extends State with WidgetsBindingObserver { + String? username; + @override void initState() { super.initState(); WidgetsBinding.instance.addObserver(this); - if (_hasIgnoreBattery) { - updateIgnoreBatteryStatus(); - } + + () async { + var update = false; + if (_hasIgnoreBattery) { + update = await updateIgnoreBatteryStatus(); + } + + final usernameRes = await getUsername(); + if (usernameRes != username) { + update = true; + username = usernameRes; + } + + final enableAbrRes = await bind.mainGetOption(key: "enable-abr") != "N"; + if (enableAbrRes != _enableAbr) { + update = true; + _enableAbr = enableAbrRes; + } + + if (update) { + setState(() {}); + } + }(); } @override @@ -49,16 +74,18 @@ class _SettingsState extends State with WidgetsBindingObserver { @override void didChangeAppLifecycleState(AppLifecycleState state) { if (state == AppLifecycleState.resumed) { - updateIgnoreBatteryStatus(); + () async { + if (await updateIgnoreBatteryStatus()) { + setState(() {}); + } + }(); } } Future updateIgnoreBatteryStatus() async { final res = await PermissionManager.check("ignore_battery_optimizations"); if (_ignoreBatteryOpt != res) { - setState(() { - _ignoreBatteryOpt = res; - }); + _ignoreBatteryOpt = res; return true; } else { return false; @@ -68,21 +95,15 @@ class _SettingsState extends State with WidgetsBindingObserver { @override Widget build(BuildContext context) { Provider.of(context); - final username = getUsername(); - final enableAbr = FFI.getByName("option", "enable-abr") != 'N'; final enhancementsTiles = [ SettingsTile.switchTile( - title: Text(translate('Adaptive Bitrate') + '(beta)'), - initialValue: enableAbr, + title: Text(translate('Adaptive Bitrate') + ' (beta)'), + initialValue: _enableAbr, onToggle: (v) { - final msg = Map() - ..["name"] = "enable-abr" - ..["value"] = ""; - if (!v) { - msg["value"] = "N"; - } - FFI.setByName("option", json.encode(msg)); - setState(() {}); + bind.mainSetOption(key: "enable-abr", value: v ? "" : "N"); + setState(() { + _enableAbr = !_enableAbr; + }); }, ) ]; @@ -98,8 +119,8 @@ class _SettingsState extends State with WidgetsBindingObserver { if (v) { PermissionManager.request("ignore_battery_optimizations"); } else { - final res = await DialogManager.show( - (setState, close) => CustomAlertDialog( + final res = await gFFI.dialogManager + .show((setState, close) => CustomAlertDialog( title: Text(translate("Open System Setting")), content: Text(translate( "android_open_battery_optimizations_tip")), @@ -132,9 +153,9 @@ class _SettingsState extends State with WidgetsBindingObserver { leading: Icon(Icons.person), onPressed: (context) { if (username == null) { - showLogin(); + showLogin(gFFI.dialogManager); } else { - logout(); + logout(gFFI.dialogManager); } }, ), @@ -145,13 +166,13 @@ class _SettingsState extends State with WidgetsBindingObserver { title: Text(translate('ID/Relay Server')), leading: Icon(Icons.cloud), onPressed: (context) { - showServerSettings(); + showServerSettings(gFFI.dialogManager); }), SettingsTile.navigation( title: Text(translate('Language')), leading: Icon(Icons.translate), onPressed: (context) { - showLanguageSettings(); + showLanguageSettings(gFFI.dialogManager); }) ]), SettingsSection( @@ -183,29 +204,27 @@ class _SettingsState extends State with WidgetsBindingObserver { } } -void showServerSettings() { - final id = FFI.getByName('option', 'custom-rendezvous-server'); - final relay = FFI.getByName('option', 'relay-server'); - final api = FFI.getByName('option', 'api-server'); - final key = FFI.getByName('option', 'key'); - showServerSettingsWithValue(id, relay, key, api); +void showServerSettings(OverlayDialogManager dialogManager) async { + Map options = jsonDecode(await bind.mainGetOptions()); + String id = options['custom-rendezvous-server'] ?? ""; + String relay = options['relay-server'] ?? ""; + String api = options['api-server'] ?? ""; + String key = options['key'] ?? ""; + showServerSettingsWithValue(id, relay, key, api, dialogManager); } -void showLanguageSettings() { +void showLanguageSettings(OverlayDialogManager dialogManager) async { try { - final langs = json.decode(FFI.getByName('langs')) as List; - var lang = FFI.getByName('local_option', 'lang'); - DialogManager.show((setState, close) { + final langs = json.decode(await bind.mainGetLangs()) as List; + var lang = await bind.mainGetLocalOption(key: "lang"); + dialogManager.show((setState, close) { final setLang = (v) { if (lang != v) { setState(() { lang = v; }); - final msg = Map() - ..['name'] = 'lang' - ..['value'] = v; - FFI.setByName('local_option', json.encode(msg)); - homeKey.currentState?.refreshPages(); + bind.mainSetLocalOption(key: "lang", value: v); + HomePage.homeKey.currentState?.refreshPages(); Future.delayed(Duration(milliseconds: 200), close); } }; @@ -227,8 +246,8 @@ void showLanguageSettings() { } catch (_e) {} } -void showAbout() { - DialogManager.show((setState, close) { +void showAbout(OverlayDialogManager dialogManager) { + dialogManager.show((setState, close) { return CustomAlertDialog( title: Text(translate('About') + ' RustDesk'), content: Wrap(direction: Axis.vertical, spacing: 12, children: [ @@ -276,8 +295,8 @@ fetch('http://localhost:21114/api/login', { final body = { 'username': name, 'password': pass, - 'id': FFI.getByName('server_id'), - 'uuid': FFI.getByName('uuid') + 'id': bind.mainGetMyId(), + 'uuid': bind.mainGetUuid() }; try { final response = await http.post(Uri.parse('$url/api/login'), @@ -297,25 +316,22 @@ String parseResp(String body) { } final token = data['access_token']; if (token != null) { - FFI.setByName('option', '{"name": "access_token", "value": "$token"}'); + bind.mainSetOption(key: "access_token", value: token); } final info = data['user']; if (info != null) { final value = json.encode(info); - FFI.setByName('option', json.encode({"name": "user_info", "value": value})); - FFI.ffiModel.updateUser(); + bind.mainSetOption(key: "user_info", value: value); + gFFI.ffiModel.updateUser(); } return ''; } void refreshCurrentUser() async { - final token = FFI.getByName("option", "access_token"); + final token = await bind.mainGetOption(key: "access_token"); if (token == '') return; final url = getUrl(); - final body = { - 'id': FFI.getByName('server_id'), - 'uuid': FFI.getByName('uuid') - }; + final body = {'id': bind.mainGetMyId(), 'uuid': bind.mainGetUuid()}; try { final response = await http.post(Uri.parse('$url/api/currentUser'), headers: { @@ -334,14 +350,11 @@ void refreshCurrentUser() async { } } -void logout() async { - final token = FFI.getByName("option", "access_token"); +void logout(OverlayDialogManager dialogManager) async { + final token = await bind.mainGetOption(key: "access_token"); if (token == '') return; final url = getUrl(); - final body = { - 'id': FFI.getByName('server_id'), - 'uuid': FFI.getByName('uuid') - }; + final body = {'id': bind.mainGetMyId(), 'uuid': bind.mainGetUuid()}; try { await http.post(Uri.parse('$url/api/logout'), headers: { @@ -355,16 +368,16 @@ void logout() async { resetToken(); } -void resetToken() { - FFI.setByName('option', '{"name": "access_token", "value": ""}'); - FFI.setByName('option', '{"name": "user_info", "value": ""}'); - FFI.ffiModel.updateUser(); +void resetToken() async { + await bind.mainSetOption(key: "access_token", value: ""); + await bind.mainSetOption(key: "user_info", value: ""); + gFFI.ffiModel.updateUser(); } -String getUrl() { - var url = FFI.getByName('option', 'api-server'); +Future getUrl() async { + var url = await bind.mainGetOption(key: "api-server"); if (url == '') { - url = FFI.getByName('option', 'custom-rendezvous-server'); + url = await bind.mainGetOption(key: "custom-rendezvous-server"); if (url != '') { if (url.contains(':')) { final tmp = url.split(':'); @@ -383,12 +396,12 @@ String getUrl() { return url; } -void showLogin() { +void showLogin(OverlayDialogManager dialogManager) { final passwordController = TextEditingController(); final nameController = TextEditingController(); var loading = false; var error = ''; - DialogManager.show((setState, close) { + dialogManager.show((setState, close) { return CustomAlertDialog( title: Text(translate('Login')), content: Column(mainAxisSize: MainAxisSize.min, children: [ @@ -453,11 +466,11 @@ void showLogin() { }); } -String? getUsername() { - final token = FFI.getByName("option", "access_token"); +Future getUsername() async { + final token = await bind.mainGetOption(key: "access_token"); String? username; if (token != "") { - final info = FFI.getByName("option", "user_info"); + final info = await bind.mainGetOption(key: "user_info"); if (info != "") { try { Map tmp = json.decode(info); diff --git a/flutter/lib/widgets/dialog.dart b/flutter/lib/mobile/widgets/dialog.dart similarity index 70% rename from flutter/lib/widgets/dialog.dart rename to flutter/lib/mobile/widgets/dialog.dart index 699e30696..d648cd497 100644 --- a/flutter/lib/widgets/dialog.dart +++ b/flutter/lib/mobile/widgets/dialog.dart @@ -1,33 +1,51 @@ import 'dart:async'; -import 'dart:convert'; - import 'package:flutter/material.dart'; -import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; -import '../common.dart'; -import '../models/model.dart'; -void clientClose() { - msgBox('', 'Close', 'Are you sure to close the connection?'); +import '../../common.dart'; +import '../../models/model.dart'; +import '../../models/platform_model.dart'; + +void clientClose(OverlayDialogManager dialogManager) { + msgBox('', 'Close', 'Are you sure to close the connection?', dialogManager); } -const SEC1 = Duration(seconds: 1); -void showSuccess({Duration duration = SEC1}) { - SmartDialog.dismiss(); - showToast(translate("Successful"), duration: SEC1); +void showSuccess() { + showToast(translate("Successful")); } -void showError({Duration duration = SEC1}) { - SmartDialog.dismiss(); - showToast(translate("Error"), duration: SEC1); +void showError() { + showToast(translate("Error")); } -void setPermanentPasswordDialog() { - final pw = FFI.getByName("permanent_password"); +void showRestartRemoteDevice( + PeerInfo pi, String id, OverlayDialogManager dialogManager) async { + final res = + await dialogManager.show((setState, close) => CustomAlertDialog( + title: Row(children: [ + Icon(Icons.warning_amber_sharp, + color: Colors.redAccent, size: 28), + SizedBox(width: 10), + Text(translate("Restart Remote Device")), + ]), + content: Text( + "${translate('Are you sure you want to restart')} \n${pi.username}@${pi.hostname}($id) ?"), + actions: [ + TextButton( + onPressed: () => close(), child: Text(translate("Cancel"))), + ElevatedButton( + onPressed: () => close(true), child: Text(translate("OK"))), + ], + )); + if (res == true) bind.sessionRestartRemoteDevice(id: id); +} + +void setPermanentPasswordDialog(OverlayDialogManager dialogManager) async { + final pw = await bind.mainGetPermanentPassword(); final p0 = TextEditingController(text: pw); final p1 = TextEditingController(text: pw); var validateLength = false; var validateSame = false; - DialogManager.show((setState, close) { + dialogManager.show((setState, close) { return CustomAlertDialog( title: Text(translate('Set your own password')), content: Form( @@ -87,10 +105,12 @@ void setPermanentPasswordDialog() { onPressed: (validateLength && validateSame) ? () async { close(); - showLoading(translate("Waiting")); - if (await FFI.serverModel.setPermanentPassword(p0.text)) { + dialogManager.showLoading(translate("Waiting")); + if (await gFFI.serverModel.setPermanentPassword(p0.text)) { + dialogManager.dismissAll(); showSuccess(); } else { + dialogManager.dismissAll(); showError(); } } @@ -102,24 +122,22 @@ void setPermanentPasswordDialog() { }); } -void setTemporaryPasswordLengthDialog() { +void setTemporaryPasswordLengthDialog( + OverlayDialogManager dialogManager) async { List lengths = ['6', '8', '10']; - String length = FFI.getByName('option', 'temporary-password-length'); + String length = await bind.mainGetOption(key: "temporary-password-length"); var index = lengths.indexOf(length); if (index < 0) index = 0; length = lengths[index]; - DialogManager.show((setState, close) { + dialogManager.show((setState, close) { final setLength = (newValue) { final oldValue = length; if (oldValue == newValue) return; setState(() { length = newValue; }); - Map msg = Map() - ..["name"] = "temporary-password-length" - ..["value"] = newValue; - FFI.setByName("option", jsonEncode(msg)); - FFI.setByName("temporary_password"); + bind.mainSetOption(key: "temporary-password-length", value: newValue); + bind.mainUpdateTemporaryPassword(); Future.delayed(Duration(milliseconds: 200), () { close(); showSuccess(); @@ -137,10 +155,11 @@ void setTemporaryPasswordLengthDialog() { }, backDismiss: true, clickMaskDismiss: true); } -void enterPasswordDialog(String id) { +void enterPasswordDialog(String id, OverlayDialogManager dialogManager) async { final controller = TextEditingController(); - var remember = FFI.getByName('remember', id) == 'true'; - DialogManager.show((setState, close) { + var remember = await bind.sessionGetRemember(id: id) ?? false; + dialogManager.dismissAll(); + dialogManager.show((setState, close) { return CustomAlertDialog( title: Text(translate('Password Required')), content: Column(mainAxisSize: MainAxisSize.min, children: [ @@ -165,7 +184,7 @@ void enterPasswordDialog(String id) { style: flatButtonStyle, onPressed: () { close(); - backToHome(); + closeConnection(); }, child: Text(translate('Cancel')), ), @@ -174,9 +193,10 @@ void enterPasswordDialog(String id) { onPressed: () { var text = controller.text.trim(); if (text == '') return; - FFI.login(text, remember); + gFFI.login(id, text, remember); close(); - showLoading(translate('Logging in...')); + dialogManager.showLoading(translate('Logging in...'), + onCancel: closeConnection); }, child: Text(translate('OK')), ), @@ -185,8 +205,8 @@ void enterPasswordDialog(String id) { }); } -void wrongPasswordDialog(String id) { - DialogManager.show((setState, close) => CustomAlertDialog( +void wrongPasswordDialog(String id, OverlayDialogManager dialogManager) { + dialogManager.show((setState, close) => CustomAlertDialog( title: Text(translate('Wrong Password')), content: Text(translate('Do you want to enter again?')), actions: [ @@ -194,14 +214,14 @@ void wrongPasswordDialog(String id) { style: flatButtonStyle, onPressed: () { close(); - backToHome(); + closeConnection(); }, child: Text(translate('Cancel')), ), TextButton( style: flatButtonStyle, onPressed: () { - enterPasswordDialog(id); + enterPasswordDialog(id, dialogManager); }, child: Text(translate('Retry')), ), @@ -243,8 +263,8 @@ class _PasswordWidgetState extends State { //This will obscure text dynamically keyboardType: TextInputType.visiblePassword, decoration: InputDecoration( - labelText: Translator.call('Password'), - hintText: Translator.call('Enter your password'), + labelText: translate('Password'), + hintText: translate('Enter your password'), // Here is key idea suffixIcon: IconButton( icon: Icon( diff --git a/flutter/lib/widgets/gesture_help.dart b/flutter/lib/mobile/widgets/gesture_help.dart similarity index 99% rename from flutter/lib/widgets/gesture_help.dart rename to flutter/lib/mobile/widgets/gesture_help.dart index e907890b0..37cc77c8f 100644 --- a/flutter/lib/widgets/gesture_help.dart +++ b/flutter/lib/mobile/widgets/gesture_help.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_hbb/common.dart'; import 'package:toggle_switch/toggle_switch.dart'; -import '../models/model.dart'; +import '../../models/model.dart'; class GestureIcons { static const String _family = 'gestureicons'; diff --git a/flutter/lib/widgets/gestures.dart b/flutter/lib/mobile/widgets/gestures.dart similarity index 100% rename from flutter/lib/widgets/gestures.dart rename to flutter/lib/mobile/widgets/gestures.dart diff --git a/flutter/lib/widgets/overlay.dart b/flutter/lib/mobile/widgets/overlay.dart similarity index 75% rename from flutter/lib/widgets/overlay.dart rename to flutter/lib/mobile/widgets/overlay.dart index b2ee54410..976d9bb73 100644 --- a/flutter/lib/widgets/overlay.dart +++ b/flutter/lib/mobile/widgets/overlay.dart @@ -1,22 +1,23 @@ -import 'package:draggable_float_widget/draggable_float_widget.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hbb/common.dart'; -import '../models/model.dart'; +import '../../models/chat_model.dart'; +import '../../models/model.dart'; import '../pages/chat_page.dart'; -OverlayEntry? chatIconOverlayEntry; -OverlayEntry? chatWindowOverlayEntry; - OverlayEntry? mobileActionsOverlayEntry; class DraggableChatWindow extends StatelessWidget { DraggableChatWindow( - {this.position = Offset.zero, required this.width, required this.height}); + {this.position = Offset.zero, + required this.width, + required this.height, + required this.chatModel}); final Offset position; final double width; final double height; + final ChatModel chatModel; @override Widget build(BuildContext context) { @@ -27,7 +28,7 @@ class DraggableChatWindow extends StatelessWidget { height: height, builder: (_, onPanUpdate) { return isIOS - ? ChatPage() + ? ChatPage(chatModel: chatModel) : Scaffold( resizeToAvoidBottomInset: false, appBar: CustomAppBar( @@ -53,13 +54,13 @@ class DraggableChatWindow extends StatelessWidget { children: [ IconButton( onPressed: () { - hideChatWindowOverlay(); + chatModel.hideChatWindowOverlay(); }, icon: Icon(Icons.keyboard_arrow_down)), IconButton( onPressed: () { - hideChatWindowOverlay(); - hideChatIconOverlay(); + chatModel.hideChatWindowOverlay(); + chatModel.hideChatIconOverlay(); }, icon: Icon(Icons.close)) ], @@ -68,7 +69,7 @@ class DraggableChatWindow extends StatelessWidget { ), ), ), - body: ChatPage(), + body: ChatPage(chatModel: chatModel), ); }); } @@ -91,81 +92,6 @@ class CustomAppBar extends StatelessWidget implements PreferredSizeWidget { Size get preferredSize => new Size.fromHeight(kToolbarHeight); } -showChatIconOverlay({Offset offset = const Offset(200, 50)}) { - if (chatIconOverlayEntry != null) { - chatIconOverlayEntry!.remove(); - } - if (globalKey.currentState == null || globalKey.currentState!.overlay == null) - return; - final bar = navigationBarKey.currentWidget; - if (bar != null) { - if ((bar as BottomNavigationBar).currentIndex == 1) { - return; - } - } - final globalOverlayState = globalKey.currentState!.overlay!; - - final overlay = OverlayEntry(builder: (context) { - return DraggableFloatWidget( - config: DraggableFloatWidgetBaseConfig( - initPositionYInTop: false, - initPositionYMarginBorder: 100, - borderTopContainTopBar: true, - ), - child: FloatingActionButton( - onPressed: () { - if (chatWindowOverlayEntry == null) { - showChatWindowOverlay(); - } else { - hideChatWindowOverlay(); - } - }, - child: Icon(Icons.message))); - }); - globalOverlayState.insert(overlay); - chatIconOverlayEntry = overlay; -} - -hideChatIconOverlay() { - if (chatIconOverlayEntry != null) { - chatIconOverlayEntry!.remove(); - chatIconOverlayEntry = null; - } -} - -showChatWindowOverlay() { - if (chatWindowOverlayEntry != null) return; - if (globalKey.currentState == null || globalKey.currentState!.overlay == null) - return; - final globalOverlayState = globalKey.currentState!.overlay!; - - final overlay = OverlayEntry(builder: (context) { - return DraggableChatWindow( - position: Offset(20, 80), width: 250, height: 350); - }); - globalOverlayState.insert(overlay); - chatWindowOverlayEntry = overlay; -} - -hideChatWindowOverlay() { - if (chatWindowOverlayEntry != null) { - chatWindowOverlayEntry!.remove(); - chatWindowOverlayEntry = null; - return; - } -} - -toggleChatOverlay() { - if (chatIconOverlayEntry == null || chatWindowOverlayEntry == null) { - FFI.invokeMethod("enable_soft_keyboard", true); - showChatIconOverlay(); - showChatWindowOverlay(); - } else { - hideChatIconOverlay(); - hideChatWindowOverlay(); - } -} - /// floating buttons of back/home/recent actions for android class DraggableMobileActions extends StatelessWidget { DraggableMobileActions( @@ -254,12 +180,12 @@ showMobileActionsOverlay() { position: Offset(left, top), width: overlayW, height: overlayH, - onBackPressed: () => FFI.tap(MouseButtons.right), - onHomePressed: () => FFI.tap(MouseButtons.wheel), + onBackPressed: () => gFFI.tap(MouseButtons.right), + onHomePressed: () => gFFI.tap(MouseButtons.wheel), onRecentPressed: () async { - FFI.sendMouse('down', MouseButtons.wheel); + gFFI.sendMouse('down', MouseButtons.wheel); await Future.delayed(Duration(milliseconds: 500)); - FFI.sendMouse('up', MouseButtons.wheel); + gFFI.sendMouse('up', MouseButtons.wheel); }, ); }); diff --git a/flutter/lib/models/ab_model.dart b/flutter/lib/models/ab_model.dart new file mode 100644 index 000000000..18bb73c3f --- /dev/null +++ b/flutter/lib/models/ab_model.dart @@ -0,0 +1,160 @@ +import 'dart:convert'; + +import 'package:flutter/material.dart'; +import 'package:flutter_hbb/models/model.dart'; +import 'package:flutter_hbb/models/platform_model.dart'; +import 'package:get/get.dart'; +import 'package:http/http.dart' as http; + +class AbModel with ChangeNotifier { + var abLoading = false; + var abError = ""; + var tags = [].obs; + var peers = [].obs; + + var selectedTags = List.empty(growable: true).obs; + + WeakReference parent; + + AbModel(this.parent); + + FFI? get _ffi => parent.target; + + Future getAb() async { + abLoading = true; + notifyListeners(); + // request + final api = "${await getApiServer()}/api/ab/get"; + try { + final resp = + await http.post(Uri.parse(api), headers: await _getHeaders()); + Map json = jsonDecode(resp.body); + if (json.containsKey('error')) { + abError = json['error']; + } else if (json.containsKey('data')) { + final data = jsonDecode(json['data']); + tags.value = data['tags']; + peers.value = data['peers']; + } + return resp.body; + } catch (err) { + abError = err.toString(); + } finally { + abLoading = false; + } + notifyListeners(); + return null; + } + + Future getApiServer() async { + return await bind.mainGetApiServer(); + } + + void reset() { + tags.clear(); + peers.clear(); + notifyListeners(); + } + + Future>? _getHeaders() { + return _ffi?.getHttpHeaders(); + } + + /// + void addId(String id) async { + if (idContainBy(id)) { + return; + } + peers.add({"id": id}); + notifyListeners(); + } + + void addTag(String tag) async { + if (tagContainBy(tag)) { + return; + } + tags.add(tag); + notifyListeners(); + } + + void changeTagForPeer(String id, List tags) { + final it = peers.where((element) => element['id'] == id); + if (it.isEmpty) { + return; + } + it.first['tags'] = tags; + } + + Future updateAb() async { + abLoading = true; + notifyListeners(); + final api = "${await getApiServer()}/api/ab"; + var authHeaders = await _getHeaders() ?? Map(); + authHeaders['Content-Type'] = "application/json"; + final body = jsonEncode({ + "data": jsonEncode({"tags": tags, "peers": peers}) + }); + final resp = + await http.post(Uri.parse(api), headers: authHeaders, body: body); + abLoading = false; + await getAb(); + notifyListeners(); + debugPrint("resp: ${resp.body}"); + } + + bool idContainBy(String id) { + return peers.where((element) => element['id'] == id).isNotEmpty; + } + + bool tagContainBy(String tag) { + return tags.where((element) => element == tag).isNotEmpty; + } + + void deletePeer(String id) { + peers.removeWhere((element) => element['id'] == id); + notifyListeners(); + } + + void deleteTag(String tag) { + tags.removeWhere((element) => element == tag); + for (var peer in peers) { + if (peer['tags'] == null) { + continue; + } + if (((peer['tags']) as List).contains(tag)) { + ((peer['tags']) as List).remove(tag); + } + } + notifyListeners(); + } + + void unsetSelectedTags() { + selectedTags.clear(); + notifyListeners(); + } + + List getPeerTags(String id) { + final it = peers.where((p0) => p0['id'] == id); + if (it.isEmpty) { + return []; + } else { + return it.first['tags'] ?? []; + } + } + + void setPeerOption(String id, String key, String value) { + final it = peers.where((p0) => p0['id'] == id); + if (it.isEmpty) { + debugPrint("${id} is not exists"); + return; + } else { + it.first[key] = value; + } + } + + void clear() { + peers.clear(); + tags.clear(); + notifyListeners(); + } +} diff --git a/flutter/lib/models/chat_model.dart b/flutter/lib/models/chat_model.dart index 149b60a46..de949c782 100644 --- a/flutter/lib/models/chat_model.dart +++ b/flutter/lib/models/chat_model.dart @@ -1,9 +1,11 @@ -import 'dart:convert'; - import 'package:dash_chat_2/dash_chat_2.dart'; +import 'package:draggable_float_widget/draggable_float_widget.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_hbb/models/platform_model.dart'; +import 'package:window_manager/window_manager.dart'; -import '../widgets/overlay.dart'; +import '../../mobile/widgets/overlay.dart'; +import '../common.dart'; import 'model.dart'; class MessageBody { @@ -23,6 +25,14 @@ class MessageBody { class ChatModel with ChangeNotifier { static final clientModeID = -1; + /// _overlayState: + /// Desktop: store session overlay by using [setOverlayState]. + /// Mobile: always null, use global overlay. + /// see [_getOverlayState] in [showChatIconOverlay] or [showChatWindowOverlay] + OverlayState? _overlayState; + OverlayEntry? chatIconOverlayEntry; + OverlayEntry? chatWindowOverlayEntry; + final ChatUser me = ChatUser( id: "", firstName: "Me", @@ -32,11 +42,19 @@ class ChatModel with ChangeNotifier { ..[clientModeID] = MessageBody(me, []); var _currentID = clientModeID; + late bool _isShowChatPage = false; Map get messages => _messages; int get currentID => _currentID; + bool get isShowChatPage => _isShowChatPage; + + WeakReference _ffi; + + /// Constructor + ChatModel(this._ffi); + ChatUser get currentUser { final user = messages[currentID]?.chatUser; if (user == null) { @@ -47,12 +65,117 @@ class ChatModel with ChangeNotifier { } } + setOverlayState(OverlayState? os) { + _overlayState = os; + } + + OverlayState? _getOverlayState() { + if (_overlayState == null) { + if (globalKey.currentState == null || + globalKey.currentState!.overlay == null) return null; + return globalKey.currentState!.overlay; + } else { + return _overlayState; + } + } + + showChatIconOverlay({Offset offset = const Offset(200, 50)}) { + if (chatIconOverlayEntry != null) { + chatIconOverlayEntry!.remove(); + } + // mobile check navigationBar + final bar = navigationBarKey.currentWidget; + if (bar != null) { + if ((bar as BottomNavigationBar).currentIndex == 1) { + return; + } + } + + final overlayState = _getOverlayState(); + if (overlayState == null) return; + + final overlay = OverlayEntry(builder: (context) { + return DraggableFloatWidget( + config: DraggableFloatWidgetBaseConfig( + initPositionYInTop: false, + initPositionYMarginBorder: 100, + borderTopContainTopBar: true, + ), + child: FloatingActionButton( + onPressed: () { + if (chatWindowOverlayEntry == null) { + showChatWindowOverlay(); + } else { + hideChatWindowOverlay(); + } + }, + child: Icon(Icons.message))); + }); + overlayState.insert(overlay); + chatIconOverlayEntry = overlay; + } + + hideChatIconOverlay() { + if (chatIconOverlayEntry != null) { + chatIconOverlayEntry!.remove(); + chatIconOverlayEntry = null; + } + } + + showChatWindowOverlay() { + if (chatWindowOverlayEntry != null) return; + final overlayState = _getOverlayState(); + if (overlayState == null) return; + final overlay = OverlayEntry(builder: (context) { + return DraggableChatWindow( + position: Offset(20, 80), width: 250, height: 350, chatModel: this); + }); + overlayState.insert(overlay); + chatWindowOverlayEntry = overlay; + } + + hideChatWindowOverlay() { + if (chatWindowOverlayEntry != null) { + chatWindowOverlayEntry!.remove(); + chatWindowOverlayEntry = null; + return; + } + } + + toggleChatOverlay() { + if (chatIconOverlayEntry == null || chatWindowOverlayEntry == null) { + gFFI.invokeMethod("enable_soft_keyboard", true); + showChatIconOverlay(); + showChatWindowOverlay(); + } else { + hideChatIconOverlay(); + hideChatWindowOverlay(); + } + } + + toggleCMChatPage(int id) async { + if (gFFI.chatModel.currentID != id) { + gFFI.chatModel.changeCurrentID(id); + } + if (_isShowChatPage) { + _isShowChatPage = !_isShowChatPage; + notifyListeners(); + await windowManager.setSize(Size(400, 600)); + } else { + await windowManager.setSize(Size(800, 600)); + await Future.delayed(Duration(milliseconds: 100)); + _isShowChatPage = !_isShowChatPage; + notifyListeners(); + } + } + changeCurrentID(int id) { if (_messages.containsKey(id)) { _currentID = id; notifyListeners(); } else { - final client = FFI.serverModel.clients[id]; + final client = _ffi.target?.serverModel.clients + .firstWhere((client) => client.id == id); if (client == null) { return debugPrint( "Failed to changeCurrentID,remote user doesn't exist"); @@ -67,20 +190,26 @@ class ChatModel with ChangeNotifier { } } - receive(int id, String text) { + receive(int id, String text) async { if (text.isEmpty) return; - // first message show overlay icon + // mobile: first message show overlay icon if (chatIconOverlayEntry == null) { showChatIconOverlay(); } + // desktop: show chat page + if (!_isShowChatPage) { + toggleCMChatPage(id); + } + _ffi.target?.serverModel.jumpTo(id); + late final chatUser; if (id == clientModeID) { chatUser = ChatUser( - firstName: FFI.ffiModel.pi.username, - id: FFI.getId(), + firstName: _ffi.target?.ffiModel.pi.username, + id: await bind.mainGetLastRemoteId(), ); } else { - final client = FFI.serverModel.clients[id]; + final client = _ffi.target?.serverModel.clients[id]; if (client == null) { return debugPrint("Failed to receive msg,user doesn't exist"); } @@ -100,12 +229,11 @@ class ChatModel with ChangeNotifier { if (message.text.isNotEmpty) { _messages[_currentID]?.insert(message); if (_currentID == clientModeID) { - FFI.setByName("chat_client_mode", message.text); + if (_ffi.target != null) { + bind.sessionSendChat(id: _ffi.target!.id, text: message.text); + } } else { - final msg = Map() - ..["id"] = _currentID - ..["text"] = message.text; - FFI.setByName("chat_server_mode", jsonEncode(msg)); + bind.cmSendChat(connId: _currentID, msg: message.text); } } notifyListeners(); @@ -114,6 +242,7 @@ class ChatModel with ChangeNotifier { close() { hideChatIconOverlay(); hideChatWindowOverlay(); + _overlayState = null; notifyListeners(); } diff --git a/flutter/lib/models/file_model.dart b/flutter/lib/models/file_model.dart index 4a96e6e73..74c2cd515 100644 --- a/flutter/lib/models/file_model.dart +++ b/flutter/lib/models/file_model.dart @@ -1,12 +1,14 @@ import 'dart:async'; import 'dart:convert'; -import 'package:flutter_hbb/common.dart'; -import 'package:flutter_hbb/pages/file_manager_page.dart'; + import 'package:flutter/material.dart'; -import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; +import 'package:flutter_hbb/common.dart'; +import 'package:flutter_hbb/mobile/pages/file_manager_page.dart'; +import 'package:get/get.dart'; import 'package:path/path.dart' as Path; import 'model.dart'; +import 'platform_model.dart'; enum SortBy { Name, Type, Modified, Size } @@ -21,6 +23,11 @@ class FileModel extends ChangeNotifier { var _jobProgress = JobProgress(); // from rust update + /// JobTable + final _jobTable = List.empty(growable: true).obs; + + RxList get jobTable => _jobTable; + bool get isLocal => _isLocal; bool get selectMode => _selectMode; @@ -33,6 +40,20 @@ class FileModel extends ChangeNotifier { 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; + } + FileDirectory _currentLocalDir = FileDirectory(); FileDirectory get currentLocalDir => _currentLocalDir; @@ -43,8 +64,36 @@ class FileModel extends ChangeNotifier { FileDirectory get currentDir => _isLocal ? 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.length == 0) 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 => _isLocal ? _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, ""); @@ -59,16 +108,43 @@ class FileModel extends ChangeNotifier { } } + String shortPath(bool isLocal) { + final dir = isLocal ? currentLocalDir : currentRemoteDir; + if (dir.path.startsWith(currentHome)) { + var path = dir.path.replaceFirst(currentHome, ""); + if (path.length == 0) return ""; + if (path[0] == "/" || path[0] == "\\") { + // remove more '/' or '\' + path = path.replaceFirst(path[0], ""); + } + return path; + } else { + return dir.path.replaceFirst(currentHome, ""); + } + } + bool get currentShowHidden => _isLocal ? _localOption.showHidden : _remoteOption.showHidden; + bool getCurrentShowHidden(bool isLocal) { + return isLocal ? _localOption.showHidden : _remoteOption.showHidden; + } + bool get currentIsWindows => _isLocal ? _localOption.isWindows : _remoteOption.isWindows; + bool getCurrentIsWindows(bool isLocal) { + return isLocal ? _localOption.isWindows : _remoteOption.isWindows; + } + final _fileFetcher = FileFetcher(); final _jobResultListener = JobResultListener>(); + final WeakReference parent; + + FileModel(this.parent); + toggleSelectMode() { if (jobState == JobState.inProgress) { return; @@ -89,16 +165,29 @@ class FileModel extends ChangeNotifier { } else { _remoteOption.showHidden = showHidden ?? !_remoteOption.showHidden; } - refresh(); + refresh(isLocal: local); } tryUpdateJobProgress(Map evt) { try { int id = int.parse(evt['id']); - _jobProgress.id = id; - _jobProgress.fileNum = int.parse(evt['file_num']); - _jobProgress.speed = double.parse(evt['speed']); - _jobProgress.finishedSize = int.parse(evt['finished_size']); + 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()}"); @@ -106,63 +195,107 @@ class FileModel extends ChangeNotifier { } receiveFileDir(Map evt) { - if (_remoteOption.home.isEmpty && evt['is_local'] == "false") { + debugPrint("recv file dir:${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); - _remoteOption.home = fd.path; - debugPrint("init remote home:${fd.path}"); - _currentRemoteDir = fd; - notifyListeners(); - return; + if (fd.id > 0) { + final jobIndex = getJob(fd.id); + if (jobIndex != -1) { + final job = jobTable[jobIndex]; + var totalSize = 0; + var fileCount = fd.entries.length; + fd.entries.forEach((element) { + 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; + } } finally {} } _fileFetcher.tryCompleteTask(evt['value'], evt['is_local']); + notifyListeners(); } - jobDone(Map evt) { + jobDone(Map evt) async { if (_jobResultListener.isListening) { _jobResultListener.complete(evt); return; } - _selectMode = false; - _jobProgress.state = JobState.done; - refresh(); + 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 evt) { - if (_jobResultListener.isListening) { - _jobResultListener.complete(evt); - return; + if (!isDesktop) { + if (_jobResultListener.isListening) { + _jobResultListener.complete(evt); + return; + } + _selectMode = false; + _jobProgress.clear(); + _jobProgress.state = JobState.error; + } else { + int jobIndex = getJob(int.parse(evt['id'])); + if (jobIndex != -1) { + final job = jobTable[jobIndex]; + job.state = JobState.error; + } } - debugPrint("jobError $evt"); - _selectMode = false; - _jobProgress.clear(); - _jobProgress.state = JobState.error; notifyListeners(); } overrideFileConfirm(Map evt) async { final resp = await showFileConfirmDialog( translate("Overwrite"), "${evt['read_path']}", true); + final id = int.tryParse(evt['id']) ?? 0; if (false == resp) { - cancelJob(int.tryParse(evt['id']) ?? 0); + final jobIndex = getJob(id); + if (jobIndex != -1) { + cancelJob(id); + final job = jobTable[jobIndex]; + job.state = JobState.done; + } } else { - var msg = Map() - ..['id'] = evt['id'] - ..['file_num'] = evt['file_num'] - ..['is_upload'] = evt['is_upload'] - ..['remember'] = fileConfirmCheckboxRemember.toString(); + var need_override = false; if (resp == null) { // skip - msg['need_override'] = 'false'; + need_override = false; } else { // overwrite - msg['need_override'] = 'true'; + need_override = true; } - FFI.setByName("set_confirm_override_file", jsonEncode(msg)); + bind.sessionSetConfirmOverrideFile( + id: parent.target?.id ?? "", + actId: id, + fileNum: int.parse(evt['file_num']), + needOverride: need_override, + remember: fileConfirmCheckboxRemember, + isUpload: evt['is_upload'] == "true"); } } @@ -172,20 +305,24 @@ class FileModel extends ChangeNotifier { } onReady() async { - _localOption.home = FFI.getByName("get_home_dir"); - _localOption.showHidden = - FFI.getByName("peer_option", "local_show_hidden").isNotEmpty; + _localOption.home = await bind.mainGetHomeDir(); + _localOption.showHidden = (await bind.sessionGetPeerOption( + id: parent.target?.id ?? "", name: "local_show_hidden")) + .isNotEmpty; - _remoteOption.showHidden = - FFI.getByName("peer_option", "remote_show_hidden").isNotEmpty; - _remoteOption.isWindows = FFI.ffiModel.pi.platform == "Windows"; + _remoteOption.showHidden = (await bind.sessionGetPeerOption( + id: parent.target?.id ?? "", name: "remote_show_hidden")) + .isNotEmpty; + _remoteOption.isWindows = parent.target?.ffiModel.pi.platform == "Windows"; - debugPrint("remote platform: ${FFI.ffiModel.pi.platform}"); + debugPrint("remote platform: ${parent.target?.ffiModel.pi.platform}"); await Future.delayed(Duration(milliseconds: 100)); - final local = FFI.getByName("peer_option", "local_dir"); - final remote = FFI.getByName("peer_option", "remote_dir"); + 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)); @@ -195,38 +332,40 @@ class FileModel extends ChangeNotifier { if (_currentRemoteDir.path.isEmpty) { openDirectory(_remoteOption.home, isLocal: false); } + // load last transfer jobs + await bind.sessionLoadLastTransferJobs(id: '${parent.target?.id}'); } onClose() { - SmartDialog.dismiss(); + parent.target?.dialogManager.dismissAll(); jobReset(); // save config - Map msg = Map(); + Map msgMap = Map(); - msg["name"] = "local_dir"; - msg["value"] = _currentLocalDir.path; - FFI.setByName('peer_option', jsonEncode(msg)); - - msg["name"] = "local_show_hidden"; - msg["value"] = _localOption.showHidden ? "Y" : ""; - FFI.setByName('peer_option', jsonEncode(msg)); - - msg["name"] = "remote_dir"; - msg["value"] = _currentRemoteDir.path; - FFI.setByName('peer_option', jsonEncode(msg)); - - msg["name"] = "remote_show_hidden"; - msg["value"] = _remoteOption.showHidden ? "Y" : ""; - FFI.setByName('peer_option', jsonEncode(msg)); + 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) { + bind.sessionPeerOption(id: id, name: msg.key, value: msg.value); + } _currentLocalDir.clear(); _currentRemoteDir.clear(); _localOption.clear(); _remoteOption.clear(); } - refresh() { - openDirectory(currentDir.path); + Future refresh({bool? isLocal}) async { + if (isDesktop) { + isLocal = isLocal ?? _isLocal; + await isLocal + ? openDirectory(currentLocalDir.path, isLocal: isLocal) + : openDirectory(currentRemoteDir.path, isLocal: isLocal); + } else { + await openDirectory(currentDir.path); + } } openDirectory(String path, {bool? isLocal}) async { @@ -235,6 +374,15 @@ class FileModel extends ChangeNotifier { isLocal ? _localOption.showHidden : _remoteOption.showHidden; final isWindows = isLocal ? _localOption.isWindows : _remoteOption.isWindows; + // process /C:\ -> C:\ on Windows + if (isLocal + ? _localOption.isWindows + : _remoteOption.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); @@ -245,48 +393,87 @@ class FileModel extends ChangeNotifier { } notifyListeners(); } catch (e) { - debugPrint("Failed to openDirectory :$e"); + debugPrint("Failed to openDirectory ${path} :$e"); } } - goHome() { - openDirectory(currentHome); + goHome({bool? isLocal}) { + isLocal = isLocal ?? _isLocal; + openDirectory(getCurrentHome(isLocal), isLocal: isLocal); } - goToParentDirectory() { - final parent = PathUtil.dirname(currentDir.path, currentIsWindows); - openDirectory(parent); - } - - sendFiles(SelectedItems items) { - if (items.isLocal == null) { - debugPrint("Failed to sendFiles ,wrong path state"); + goToParentDirectory({bool? isLocal}) { + isLocal = isLocal ?? _isLocal; + 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; } - _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) { - _jobId++; - final msg = { - "id": _jobId.toString(), - "path": from.path, - "to": PathUtil.join(toPath, from.name, isWindows), - "file_num": "0", - "show_hidden": showHidden.toString(), - "is_remote": (!(items.isLocal!)).toString() - }; - FFI.setByName("send_files", jsonEncode(msg)); - }); + 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; + items.items.forEach((from) async { + final jobId = ++_jobId; + _jobTable.add(JobProgress() + ..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); + print( + "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: await bind.mainGetLastRemoteId(), + 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) async { + removeAction(SelectedItems items, {bool? isLocal}) async { + isLocal = isLocal ?? _isLocal; removeCheckboxRemember = false; if (items.isLocal == null) { debugPrint("Failed to removeFile, wrong path state"); @@ -305,14 +492,14 @@ class FileModel extends ChangeNotifier { entries = [item]; } else if (item.isDirectory) { title = translate("Not an empty directory"); - showLoading(translate("Waiting")); + 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); - SmartDialog.dismiss(); + parent.target?.dialogManager.dismissAll(); if (fd.entries.isEmpty) { final confirm = await showRemoveDialog( translate( @@ -360,16 +547,18 @@ class FileModel extends ChangeNotifier { } break; } - } catch (e) {} + } catch (e) { + print("remove error: ${e}"); + } } }); _selectMode = false; - refresh(); + refresh(isLocal: isLocal); } Future showRemoveDialog( String title, String content, bool showCheckbox) async { - return await DialogManager.show( + return await parent.target?.dialogManager.show( (setState, Function(bool v) close) => CustomAlertDialog( title: Row( children: [ @@ -420,7 +609,7 @@ class FileModel extends ChangeNotifier { Future showFileConfirmDialog( String title, String content, bool showCheckbox) async { fileConfirmCheckboxRemember = false; - return await DialogManager.show( + return await parent.target?.dialogManager.show( (setState, Function(bool? v) close) => CustomAlertDialog( title: Row( children: [ @@ -473,43 +662,122 @@ class FileModel extends ChangeNotifier { } sendRemoveFile(String path, int fileNum, bool isLocal) { - final msg = { - "id": _jobId.toString(), - "path": path, - "file_num": fileNum.toString(), - "is_remote": (!(isLocal)).toString() - }; - FFI.setByName("remove_file", jsonEncode(msg)); + bind.sessionRemoveFile( + id: '${parent.target?.id}', + actId: _jobId, + path: path, + isRemote: !isLocal, + fileNum: fileNum); } sendRemoveEmptyDir(String path, int fileNum, bool isLocal) { - final msg = { - "id": _jobId.toString(), - "path": path, - "is_remote": (!isLocal).toString() - }; - FFI.setByName("remove_all_empty_dirs", jsonEncode(msg)); + bind.sessionRemoveAllEmptyDirs( + id: '${parent.target?.id}', + actId: _jobId, + path: path, + isRemote: !isLocal); } - createDir(String path) { + createDir(String path, {bool? isLocal}) async { + isLocal = isLocal ?? this.isLocal; _jobId++; - final msg = { - "id": _jobId.toString(), - "path": path, - "is_remote": (!isLocal).toString() - }; - FFI.setByName("create_dir", jsonEncode(msg)); + bind.sessionCreateDir( + id: '${parent.target?.id}', + actId: _jobId, + path: path, + isRemote: !isLocal); } - cancelJob(int id) { - FFI.setByName("cancel_job", id.toString()); + cancelJob(int id) async { + bind.sessionCancelJob(id: '${parent.target?.id}', actId: id); jobReset(); } - changeSortStyle(SortBy sort) { + changeSortStyle(SortBy sort, {bool? isLocal, bool ascending = true}) { _sortStyle = sort; - _currentLocalDir.changeSortStyle(sort); - _currentRemoteDir.changeSortStyle(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; + } + notifyListeners(); + } + + initFileFetcher() { + _fileFetcher.id = parent.target?.id; + } + + void updateFolderFiles(Map evt) { + // ret: "{\"id\":1,\"num_entries\":12,\"total_size\":1264822.0}" + Map info = json.decode(evt['info']); + int id = info['id']; + int num_entries = info['num_entries']; + double total_size = info['total_size']; + final jobIndex = getJob(id); + if (jobIndex != -1) { + final job = jobTable[jobIndex]; + job.fileCount = num_entries; + job.totalSize = total_size.toInt(); + } + debugPrint("update folder files: ${info}"); + notifyListeners(); + } + + bool get remoteSortAscending => _remoteSortAscending; + + void loadLastJob(Map evt) { + debugPrint("load last job: ${evt}"); + Map 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++; + var jobProgress = JobProgress() + ..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(); } } @@ -559,6 +827,17 @@ class FileFetcher { Map> remoteTasks = Map(); Map> readRecursiveTasks = Map(); + String? _id; + + String? get id => _id; + + set id(String? id) { + _id = id; + } + + // if id == null, means to fetch global FFI + FFI get _ffi => ffi(_id ?? ""); + Future registerReadTask(bool isLocal, String path) { // final jobs = isLocal?localJobs:remoteJobs; // maybe we will use read local dir async later final tasks = remoteTasks; // bypass now @@ -618,13 +897,14 @@ class FileFetcher { Future fetchDirectory( String path, bool isLocal, bool showHidden) async { try { - final msg = {"path": path, "show_hidden": showHidden.toString()}; if (isLocal) { - final res = FFI.getByName("read_local_dir_sync", jsonEncode(msg)); + final res = await bind.sessionReadLocalDirSync( + id: id ?? "", path: path, showHidden: showHidden); final fd = FileDirectory.fromJson(jsonDecode(res)); return fd; } else { - FFI.setByName("read_remote_dir", jsonEncode(msg)); + await bind.sessionReadRemoteDir( + id: id ?? "", path: path, includeHidden: showHidden); return registerReadTask(isLocal, path); } } catch (e) { @@ -636,13 +916,12 @@ class FileFetcher { int id, String path, bool isLocal, bool showHidden) async { // TODO test Recursive is show hidden default? try { - final msg = { - "id": id.toString(), - "path": path, - "show_hidden": showHidden.toString(), - "is_remote": (!isLocal).toString() - }; - FFI.setByName("read_dir_recursive", jsonEncode(msg)); + await bind.sessionReadDirRecursive( + id: _ffi.id, + actId: id, + path: path, + isRemote: !isLocal, + showHidden: showHidden); return registerReadRecursiveTask(id); } catch (e) { return Future.error(e); @@ -675,8 +954,8 @@ class FileDirectory { } } - changeSortStyle(SortBy sort) { - entries = _sortList(entries, sort); + changeSortStyle(SortBy sort, {bool ascending = true}) { + entries = _sortList(entries, sort, ascending); } clear() { @@ -711,7 +990,24 @@ class Entry { } } -enum JobState { none, inProgress, done, error } +enum JobState { none, inProgress, done, error, paused } + +extension JobStateDisplay on JobState { + String display() { + switch (this) { + case JobState.none: + return translate("Waiting"); + case JobState.inProgress: + return translate("Transfer File"); + case JobState.done: + return translate("Finished"); + case JobState.error: + return translate("Error"); + default: + return ""; + } + } +} class JobProgress { JobState state = JobState.none; @@ -719,6 +1015,13 @@ class JobProgress { var fileNum = 0; var speed = 0.0; var finishedSize = 0; + var totalSize = 0; + var fileCount = 0; + var isRemote = false; + var jobName = ""; + var remote = ""; + var to = ""; + var showHidden = false; clear() { state = JobState.none; @@ -726,6 +1029,10 @@ class JobProgress { fileNum = 0; speed = 0; finishedSize = 0; + jobName = ""; + fileCount = 0; + remote = ""; + to = ""; } } @@ -772,7 +1079,7 @@ class DirectoryOption { } // code from file_manager pkg after edit -List _sortList(List list, SortBy sortType) { +List _sortList(List list, SortBy sortType, bool ascending) { if (sortType == SortBy.Name) { // making list of only folders. final dirs = list.where((element) => element.isDirectory).toList(); @@ -785,7 +1092,9 @@ List _sortList(List list, SortBy sortType) { files.sort((a, b) => a.name.toLowerCase().compareTo(b.name.toLowerCase())); // first folders will go to list (if available) then files will go to list. - return [...dirs, ...files]; + return ascending + ? [...dirs, ...files] + : [...dirs.reversed.toList(), ...files.reversed.toList()]; } else if (sortType == SortBy.Modified) { // making the list of Path & DateTime List<_PathStat> _pathStat = []; @@ -800,7 +1109,7 @@ List _sortList(List list, SortBy sortType) { list.sort((a, b) => _pathStat .indexWhere((element) => element.path == a.name) .compareTo(_pathStat.indexWhere((element) => element.path == b.name))); - return list; + return ascending ? list : list.reversed.toList(); } else if (sortType == SortBy.Type) { // making list of only folders. final dirs = list.where((element) => element.isDirectory).toList(); @@ -817,7 +1126,9 @@ List _sortList(List list, SortBy sortType) { .split('.') .last .compareTo(b.name.toLowerCase().split('.').last)); - return [...dirs, ...files]; + return ascending + ? [...dirs, ...files] + : [...dirs.reversed.toList(), ...files.reversed.toList()]; } else if (sortType == SortBy.Size) { // create list of path and size Map _sizeMap = {}; @@ -842,7 +1153,9 @@ List _sortList(List list, SortBy sortType) { .indexWhere((element) => element.key == a.name) .compareTo( _sizeMapList.indexWhere((element) => element.key == b.name))); - return [...dirs, ...files]; + return ascending + ? [...dirs, ...files] + : [...dirs.reversed.toList(), ...files.reversed.toList()]; } return []; } diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart index 4e1c76e4a..dda22a779 100644 --- a/flutter/lib/models/model.dart +++ b/flutter/lib/models/model.dart @@ -1,20 +1,26 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; +import 'dart:math'; +import 'dart:typed_data'; +import 'dart:ui' as ui; + +import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:flutter_hbb/generated_bridge.dart'; +import 'package:flutter_hbb/models/ab_model.dart'; import 'package:flutter_hbb/models/chat_model.dart'; import 'package:flutter_hbb/models/file_model.dart'; import 'package:flutter_hbb/models/server_model.dart'; -import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; +import 'package:flutter_hbb/models/user_model.dart'; import 'package:shared_preferences/shared_preferences.dart'; -import 'dart:math'; -import 'dart:convert'; -import 'dart:typed_data'; -import 'dart:ui' as ui; -import 'package:flutter/material.dart'; import 'package:tuple/tuple.dart'; -import 'dart:async'; + import '../common.dart'; -import '../widgets/dialog.dart'; -import '../widgets/overlay.dart'; -import 'native_model.dart' if (dart.library.html) 'web_model.dart'; +import '../mobile/widgets/dialog.dart'; +import '../mobile/widgets/overlay.dart'; +import 'peer_model.dart'; +import 'platform_model.dart'; typedef HandleMsgBox = void Function(Map evt, String id); bool _waitForImage = false; @@ -22,6 +28,7 @@ bool _waitForImage = false; class FfiModel with ChangeNotifier { PeerInfo _pi = PeerInfo(); Display _display = Display(); + var _inputBlocked = false; final _permissions = Map(); bool? _secure; @@ -29,6 +36,7 @@ class FfiModel with ChangeNotifier { bool _touchMode = false; Timer? _timer; var _reconnects = 1; + WeakReference parent; Map get permissions => _permissions; @@ -50,15 +58,10 @@ class FfiModel with ChangeNotifier { _inputBlocked = v; } - FfiModel() { - Translator.call = translate; + FfiModel(this.parent) { clear(); } - Future init() async { - await PlatformFFI.init(); - } - void toggleTouchMode() { if (!isPeerAndroid) { _touchMode = !_touchMode; @@ -119,54 +122,121 @@ class FfiModel with ChangeNotifier { _permissions.clear(); } + void Function(Map) startEventListener(String peerId) { + return (evt) { + var name = evt['name']; + if (name == 'msgbox') { + handleMsgBox(evt, peerId); + } else if (name == 'peer_info') { + handlePeerInfo(evt, peerId); + } else if (name == 'connection_ready') { + setConnectionType(evt['secure'] == 'true', evt['direct'] == 'true'); + } else if (name == 'switch_display') { + handleSwitchDisplay(evt); + } else if (name == 'cursor_data') { + parent.target?.cursorModel.updateCursorData(evt); + } else if (name == 'cursor_id') { + parent.target?.cursorModel.updateCursorId(evt); + } else if (name == 'cursor_position') { + parent.target?.cursorModel.updateCursorPosition(evt); + } else if (name == 'clipboard') { + Clipboard.setData(ClipboardData(text: evt['content'])); + } else if (name == 'permission') { + parent.target?.ffiModel.updatePermission(evt); + } else if (name == 'chat_client_mode') { + parent.target?.chatModel + .receive(ChatModel.clientModeID, evt['text'] ?? ""); + } else if (name == 'chat_server_mode') { + parent.target?.chatModel + .receive(int.parse(evt['id'] as String), evt['text'] ?? ""); + } else if (name == 'file_dir') { + parent.target?.fileModel.receiveFileDir(evt); + } else if (name == 'job_progress') { + parent.target?.fileModel.tryUpdateJobProgress(evt); + } else if (name == 'job_done') { + parent.target?.fileModel.jobDone(evt); + } else if (name == 'job_error') { + parent.target?.fileModel.jobError(evt); + } else if (name == 'override_file_confirm') { + parent.target?.fileModel.overrideFileConfirm(evt); + } else if (name == 'load_last_job') { + parent.target?.fileModel.loadLastJob(evt); + } else if (name == 'update_folder_files') { + parent.target?.fileModel.updateFolderFiles(evt); + } else if (name == 'try_start_without_auth') { + parent.target?.serverModel.loginRequest(evt); + } else if (name == 'on_client_authorized') { + parent.target?.serverModel.onClientAuthorized(evt); + } else if (name == 'on_client_remove') { + parent.target?.serverModel.onClientRemove(evt); + } else if (name == 'update_quality_status') { + parent.target?.qualityMonitorModel.updateQualityStatus(evt); + } else if (name == 'update_block_input_state') { + updateBlockInputState(evt); + } else if (name == 'update_privacy_mode') { + updatePrivacyMode(evt); + } + }; + } + + /// Bind the event listener to receive events from the Rust core. void updateEventListener(String peerId) { final void Function(Map) cb = (evt) { var name = evt['name']; if (name == 'msgbox') { handleMsgBox(evt, peerId); } else if (name == 'peer_info') { - handlePeerInfo(evt); + handlePeerInfo(evt, peerId); } else if (name == 'connection_ready') { - FFI.ffiModel.setConnectionType( + parent.target?.ffiModel.setConnectionType( evt['secure'] == 'true', evt['direct'] == 'true'); } else if (name == 'switch_display') { handleSwitchDisplay(evt); } else if (name == 'cursor_data') { - FFI.cursorModel.updateCursorData(evt); + parent.target?.cursorModel.updateCursorData(evt); } else if (name == 'cursor_id') { - FFI.cursorModel.updateCursorId(evt); + parent.target?.cursorModel.updateCursorId(evt); } else if (name == 'cursor_position') { - FFI.cursorModel.updateCursorPosition(evt); + parent.target?.cursorModel.updateCursorPosition(evt); } else if (name == 'clipboard') { Clipboard.setData(ClipboardData(text: evt['content'])); } else if (name == 'permission') { - FFI.ffiModel.updatePermission(evt); + parent.target?.ffiModel.updatePermission(evt); } else if (name == 'chat_client_mode') { - FFI.chatModel.receive(ChatModel.clientModeID, evt['text'] ?? ""); + parent.target?.chatModel + .receive(ChatModel.clientModeID, evt['text'] ?? ""); } else if (name == 'chat_server_mode') { - FFI.chatModel + parent.target?.chatModel .receive(int.parse(evt['id'] as String), evt['text'] ?? ""); } else if (name == 'file_dir') { - FFI.fileModel.receiveFileDir(evt); + parent.target?.fileModel.receiveFileDir(evt); } else if (name == 'job_progress') { - FFI.fileModel.tryUpdateJobProgress(evt); + parent.target?.fileModel.tryUpdateJobProgress(evt); } else if (name == 'job_done') { - FFI.fileModel.jobDone(evt); + parent.target?.fileModel.jobDone(evt); } else if (name == 'job_error') { - FFI.fileModel.jobError(evt); + parent.target?.fileModel.jobError(evt); } else if (name == 'override_file_confirm') { - FFI.fileModel.overrideFileConfirm(evt); + parent.target?.fileModel.overrideFileConfirm(evt); + } else if (name == 'load_last_job') { + parent.target?.fileModel.loadLastJob(evt); + } else if (name == 'update_folder_files') { + parent.target?.fileModel.updateFolderFiles(evt); } else if (name == 'try_start_without_auth') { - FFI.serverModel.loginRequest(evt); + parent.target?.serverModel.loginRequest(evt); } else if (name == 'on_client_authorized') { - FFI.serverModel.onClientAuthorized(evt); + parent.target?.serverModel.onClientAuthorized(evt); } else if (name == 'on_client_remove') { - FFI.serverModel.onClientRemove(evt); + parent.target?.serverModel.onClientRemove(evt); } else if (name == 'update_quality_status') { - FFI.qualityMonitorModel.updateQualityStatus(evt); + parent.target?.qualityMonitorModel.updateQualityStatus(evt); + } else if (name == 'update_block_input_state') { + updateBlockInputState(evt); + } else if (name == 'update_privacy_mode') { + updatePrivacyMode(evt); } }; - PlatformFFI.setEventCallback(cb); + platformFFI.setEventCallback(cb); } void handleSwitchDisplay(Map evt) { @@ -178,39 +248,46 @@ class FfiModel with ChangeNotifier { _display.width = int.parse(evt['width']); _display.height = int.parse(evt['height']); if (old != _pi.currentDisplay) - FFI.cursorModel.updateDisplayOrigin(_display.x, _display.y); + parent.target?.cursorModel.updateDisplayOrigin(_display.x, _display.y); // remote is mobile, and orientation changed if ((_display.width > _display.height) != oldOrientation) { - FFI.canvasModel.updateViewStyle(); + gFFI.canvasModel.updateViewStyle(); } notifyListeners(); } + /// Handle the message box event based on [evt] and [id]. void handleMsgBox(Map evt, String id) { + if (parent.target == null) return; + final dialogManager = parent.target!.dialogManager; var type = evt['type']; var title = evt['title']; var text = evt['text']; if (type == 're-input-password') { - wrongPasswordDialog(id); + wrongPasswordDialog(id, dialogManager); } else if (type == 'input-password') { - enterPasswordDialog(id); + enterPasswordDialog(id, dialogManager); } else if (type == 'restarting') { - showMsgBox(type, title, text, false, hasCancel: false); + showMsgBox(id, type, title, text, false, dialogManager, hasCancel: false); } else { var hasRetry = evt['hasRetry'] == 'true'; - showMsgBox(type, title, text, hasRetry); + showMsgBox(id, type, title, text, hasRetry, dialogManager); } } - void showMsgBox(String type, String title, String text, bool hasRetry, + /// Show a message box with [type], [title] and [text]. + void showMsgBox(String id, String type, String title, String text, + bool hasRetry, OverlayDialogManager dialogManager, {bool? hasCancel}) { - msgBox(type, title, text, hasCancel: hasCancel); + msgBox(type, title, text, dialogManager, hasCancel: hasCancel); _timer?.cancel(); if (hasRetry) { _timer = Timer(Duration(seconds: _reconnects), () { - FFI.reconnect(); - showLoading(translate('Connecting...')); + bind.sessionReconnect(id: id); + clearPermissions(); + dialogManager.showLoading(translate('Connecting...'), + onCancel: closeConnection); }); _reconnects *= 2; } else { @@ -218,8 +295,9 @@ class FfiModel with ChangeNotifier { } } - void handlePeerInfo(Map evt) { - SmartDialog.dismiss(); + /// Handle the peer info event based on [evt]. + void handlePeerInfo(Map evt, String peerId) async { + parent.target?.dialogManager.dismissAll(); _pi.version = evt['version']; _pi.username = evt['username']; _pi.hostname = evt['hostname']; @@ -229,15 +307,16 @@ class FfiModel with ChangeNotifier { if (isPeerAndroid) { _touchMode = true; - if (FFI.ffiModel.permissions['keyboard'] != false) { + if (parent.target?.ffiModel.permissions['keyboard'] != false) { Timer(Duration(milliseconds: 100), showMobileActionsOverlay); } } else { - _touchMode = FFI.getByName('peer_option', "touch-mode") != ''; + _touchMode = + await bind.sessionGetOption(id: peerId, arg: "touch-mode") != ''; } if (evt['is_file_transfer'] == "true") { - FFI.fileModel.onReady(); + parent.target?.fileModel.onReady(); } else { _pi.displays = []; List displays = json.decode(evt['displays']); @@ -254,13 +333,24 @@ class FfiModel with ChangeNotifier { _display = _pi.displays[_pi.currentDisplay]; } if (displays.length > 0) { - showLoading(translate('Connected, waiting for image...')); + parent.target?.dialogManager.showLoading( + translate('Connected, waiting for image...'), + onCancel: closeConnection); _waitForImage = true; _reconnects = 1; } } notifyListeners(); } + + updateBlockInputState(Map evt) { + _inputBlocked = evt['input_state'] == 'on'; + notifyListeners(); + } + + updatePrivacyMode(Map evt) { + notifyListeners(); + } } class ImageModel with ChangeNotifier { @@ -268,45 +358,53 @@ class ImageModel with ChangeNotifier { ui.Image? get image => _image; - ImageModel() { - PlatformFFI.setRgbaCallback((rgba) { - if (_waitForImage) { - _waitForImage = false; - SmartDialog.dismiss(); + String _id = ""; + + WeakReference parent; + + ImageModel(this.parent); + + void onRgba(Uint8List rgba, double tabBarHeight) { + if (_waitForImage) { + _waitForImage = false; + parent.target?.dialogManager.dismissAll(); + } + final pid = parent.target?.id; + ui.decodeImageFromPixels( + rgba, + parent.target?.ffiModel.display.width ?? 0, + parent.target?.ffiModel.display.height ?? 0, + isWeb ? ui.PixelFormat.rgba8888 : ui.PixelFormat.bgra8888, (image) { + if (parent.target?.id != pid) return; + try { + // my throw exception, because the listener maybe already dispose + update(image, tabBarHeight); + } catch (e) { + print('update image: $e'); } - final pid = FFI.id; - ui.decodeImageFromPixels( - rgba, - FFI.ffiModel.display.width, - FFI.ffiModel.display.height, - isWeb ? ui.PixelFormat.rgba8888 : ui.PixelFormat.bgra8888, (image) { - if (FFI.id != pid) return; - try { - // my throw exception, because the listener maybe already dispose - FFI.imageModel.update(image); - } catch (e) { - print('update image: $e'); - } - }); }); } - void update(ui.Image? image) { + void update(ui.Image? image, double tabBarHeight) { if (_image == null && image != null) { - if (isDesktop) { - FFI.canvasModel.updateViewStyle(); + if (isWebDesktop || isDesktop) { + parent.target?.canvasModel.updateViewStyle(); + parent.target?.canvasModel.updateScrollStyle(); } else { final size = MediaQueryData.fromWindow(ui.window).size; - final xscale = size.width / image.width; - final yscale = size.height / image.height; - FFI.canvasModel.scale = max(xscale, yscale); + final canvasWidth = size.width; + final canvasHeight = size.height - tabBarHeight; + final xscale = canvasWidth / image.width; + final yscale = canvasHeight / image.height; + parent.target?.canvasModel.scale = min(xscale, yscale); + } + if (parent.target != null) { + initializeCursorAndCanvas(parent.target!); } - initializeCursorAndCanvas(); Future.delayed(Duration(milliseconds: 1), () { - if (FFI.ffiModel.isPeerAndroid) { - FFI.setByName( - 'peer_option', '{"name": "view-style", "value": "shrink"}'); - FFI.canvasModel.updateViewStyle(); + if (parent.target?.ffiModel.isPeerAndroid ?? false) { + bind.sessionPeerOption(id: _id, name: "view-style", value: "shrink"); + parent.target?.canvasModel.updateViewStyle(); } }); } @@ -314,6 +412,8 @@ class ImageModel with ChangeNotifier { if (image != null) notifyListeners(); } + // mobile only + // for desktop, height should minus tabbar height double get maxScale { if (_image == null) return 1.5; final size = MediaQueryData.fromWindow(ui.window).size; @@ -322,6 +422,8 @@ class ImageModel with ChangeNotifier { return max(1.5, max(xscale, yscale)); } + // mobile only + // for desktop, height should minus tabbar height double get minScale { if (_image == null) return 1.5; final size = MediaQueryData.fromWindow(ui.window).size; @@ -331,39 +433,98 @@ class ImageModel with ChangeNotifier { } } +enum ScrollStyle { + scrollbar, + scrollauto, +} + class CanvasModel with ChangeNotifier { + // scroll offset x percent + double _scrollX = 0.0; + // scroll offset y percent + double _scrollY = 0.0; double _x = 0; double _y = 0; double _scale = 1.0; + double _tabBarHeight = 0.0; + String id = ""; // TODO multi canvas model + ScrollStyle _scrollStyle = ScrollStyle.scrollauto; - CanvasModel(); + WeakReference parent; + + CanvasModel(this.parent); double get x => _x; - double get y => _y; - double get scale => _scale; + ScrollStyle get scrollStyle => _scrollStyle; - void updateViewStyle() { - final s = FFI.getByName('peer_option', 'view-style'); - final size = MediaQueryData.fromWindow(ui.window).size; - final s1 = size.width / FFI.ffiModel.display.width; - final s2 = size.height / FFI.ffiModel.display.height; - if (s == 'shrink') { + setScrollPercent(double x, double y) { + _scrollX = x; + _scrollY = y; + } + + double get scrollX => _scrollX; + double get scrollY => _scrollY; + + set tabBarHeight(double h) => _tabBarHeight = h; + double get tabBarHeight => _tabBarHeight; + + void updateViewStyle() async { + final style = await bind.sessionGetOption(id: id, arg: 'view-style'); + if (style == null) { + return; + } + + final s1 = size.width / (parent.target?.ffiModel.display.width ?? 720); + final s2 = size.height / (parent.target?.ffiModel.display.height ?? 1280); + + // Closure to perform shrink operation. + final shrinkOp = () { final s = s1 < s2 ? s1 : s2; if (s < 1) { _scale = s; } - } else if (s == 'stretch') { - final s = s1 > s2 ? s1 : s2; + }; + // Closure to perform stretch operation. + final stretchOp = () { + final s = s1 < s2 ? s1 : s2; if (s > 1) { _scale = s; } - } else { - _scale = 1; + }; + // Closure to perform default operation(set the scale to 1.0). + final defaultOp = () { + _scale = 1.0; + }; + + // // On desktop, shrink is the default behavior. + // if (isDesktop) { + // shrinkOp(); + // } else { + defaultOp(); + // } + + if (style == 'shrink') { + shrinkOp(); + } else if (style == 'stretch') { + stretchOp(); + } + + _x = (size.width - getDisplayWidth() * _scale) / 2; + _y = (size.height - getDisplayHeight() * _scale) / 2; + notifyListeners(); + } + + updateScrollStyle() async { + final style = await bind.sessionGetOption(id: id, arg: 'scroll-style'); + if (style == 'scrollbar') { + _scrollStyle = ScrollStyle.scrollbar; + _scrollX = 0.0; + _scrollY = 0.0; + } else { + _scrollStyle = ScrollStyle.scrollauto; } - _x = (size.width - FFI.ffiModel.display.width * _scale) / 2; - _y = (size.height - FFI.ffiModel.display.height * _scale) / 2; notifyListeners(); } @@ -374,10 +535,24 @@ class CanvasModel with ChangeNotifier { notifyListeners(); } - void moveDesktopMouse(double x, double y) { + int getDisplayWidth() { + return parent.target?.ffiModel.display.width ?? 1080; + } + + int getDisplayHeight() { + return parent.target?.ffiModel.display.height ?? 720; + } + + Size get size { final size = MediaQueryData.fromWindow(ui.window).size; - final dw = FFI.ffiModel.display.width * _scale; - final dh = FFI.ffiModel.display.height * _scale; + return Size(size.width, size.height - _tabBarHeight); + } + + void moveDesktopMouse(double x, double y) { + // On mobile platforms, move the canvas with the cursor. + //if (!isDesktop) { + final dw = getDisplayWidth() * _scale; + final dh = getDisplayHeight() * _scale; var dxOffset = 0; var dyOffset = 0; if (dw > size.width) { @@ -391,7 +566,8 @@ class CanvasModel with ChangeNotifier { if (dxOffset != 0 || dyOffset != 0) { notifyListeners(); } - FFI.cursorModel.moveLocal(x, y); + //} + parent.target?.cursorModel.moveLocal(x, y); } set scale(v) { @@ -405,11 +581,14 @@ class CanvasModel with ChangeNotifier { } void resetOffset() { - if (isDesktop) { + if (isWebDesktop) { updateViewStyle(); } else { - _x = 0; - _y = 0; + final size = MediaQueryData.fromWindow(ui.window).size; + final canvasWidth = size.width; + final canvasHeight = size.height - _tabBarHeight; + _x = (canvasWidth - getDisplayWidth() * _scale) / 2; + _y = (canvasHeight - getDisplayHeight() * _scale) / 2; } notifyListeners(); } @@ -420,17 +599,17 @@ class CanvasModel with ChangeNotifier { } void updateScale(double v) { - if (FFI.imageModel.image == null) return; - final offset = FFI.cursorModel.offset; - var r = FFI.cursorModel.getVisibleRect(); + if (parent.target?.imageModel.image == null) return; + final offset = parent.target?.cursorModel.offset ?? Offset(0, 0); + var r = parent.target?.cursorModel.getVisibleRect() ?? Rect.zero; final px0 = (offset.dx - r.left) * _scale; final py0 = (offset.dy - r.top) * _scale; _scale *= v; - final maxs = FFI.imageModel.maxScale; - final mins = FFI.imageModel.minScale; + final maxs = parent.target?.imageModel.maxScale ?? 1; + final mins = parent.target?.imageModel.minScale ?? 1; if (_scale > maxs) _scale = maxs; if (_scale < mins) _scale = mins; - r = FFI.cursorModel.getVisibleRect(); + r = parent.target?.cursorModel.getVisibleRect() ?? Rect.zero; final px1 = (offset.dx - r.left) * _scale; final py1 = (offset.dy - r.top) * _scale; _x -= px1 - px0; @@ -455,6 +634,8 @@ class CursorModel with ChangeNotifier { double _hoty = 0; double _displayOriginX = 0; double _displayOriginY = 0; + String id = ""; // TODO multi cursor model + WeakReference parent; ui.Image? get image => _image; @@ -468,12 +649,14 @@ class CursorModel with ChangeNotifier { double get hoty => _hoty; + CursorModel(this.parent); + // remote physical display coordinate Rect getVisibleRect() { final size = MediaQueryData.fromWindow(ui.window).size; - final xoffset = FFI.canvasModel.x; - final yoffset = FFI.canvasModel.y; - final scale = FFI.canvasModel.scale; + final xoffset = parent.target?.canvasModel.x ?? 0; + final yoffset = parent.target?.canvasModel.y ?? 0; + final scale = parent.target?.canvasModel.scale ?? 1; final x0 = _displayOriginX - xoffset / scale; final y0 = _displayOriginY - yoffset / scale; return Rect.fromLTWH(x0, y0, size.width / scale, size.height / scale); @@ -484,7 +667,7 @@ class CursorModel with ChangeNotifier { var keyboardHeight = m.viewInsets.bottom; final size = m.size; if (keyboardHeight < 100) return 0; - final s = FFI.canvasModel.scale; + final s = parent.target?.canvasModel.scale ?? 1.0; final thresh = (size.height - keyboardHeight) / 2; var h = (_y - getVisibleRect().top) * s; // local physical display height return h - thresh; @@ -492,19 +675,19 @@ class CursorModel with ChangeNotifier { void touch(double x, double y, MouseButtons button) { moveLocal(x, y); - FFI.moveMouse(_x, _y); - FFI.tap(button); + parent.target?.moveMouse(_x, _y); + parent.target?.tap(button); } void move(double x, double y) { moveLocal(x, y); - FFI.moveMouse(_x, _y); + parent.target?.moveMouse(_x, _y); } void moveLocal(double x, double y) { - final scale = FFI.canvasModel.scale; - final xoffset = FFI.canvasModel.x; - final yoffset = FFI.canvasModel.y; + final scale = parent.target?.canvasModel.scale ?? 1.0; + final xoffset = parent.target?.canvasModel.x ?? 0; + final yoffset = parent.target?.canvasModel.y ?? 0; _x = (x - xoffset) / scale + _displayOriginX; _y = (y - yoffset) / scale + _displayOriginY; notifyListeners(); @@ -513,22 +696,22 @@ class CursorModel with ChangeNotifier { void reset() { _x = _displayOriginX; _y = _displayOriginY; - FFI.moveMouse(_x, _y); - FFI.canvasModel.clear(true); + parent.target?.moveMouse(_x, _y); + parent.target?.canvasModel.clear(true); notifyListeners(); } void updatePan(double dx, double dy, bool touchMode) { - if (FFI.imageModel.image == null) return; + if (parent.target?.imageModel.image == null) return; if (touchMode) { - final scale = FFI.canvasModel.scale; + final scale = parent.target?.canvasModel.scale ?? 1.0; _x += dx / scale; _y += dy / scale; - FFI.moveMouse(_x, _y); + parent.target?.moveMouse(_x, _y); notifyListeners(); return; } - final scale = FFI.canvasModel.scale; + final scale = parent.target?.canvasModel.scale ?? 1.0; dx /= scale; dy /= scale; final r = getVisibleRect(); @@ -537,7 +720,7 @@ class CursorModel with ChangeNotifier { var tryMoveCanvasX = false; if (dx > 0) { final maxCanvasCanMove = _displayOriginX + - FFI.imageModel.image!.width - + (parent.target?.imageModel.image!.width ?? 1280) - r.right.roundToDouble(); tryMoveCanvasX = _x + dx > cx && maxCanvasCanMove > 0; if (tryMoveCanvasX) { @@ -559,7 +742,7 @@ class CursorModel with ChangeNotifier { var tryMoveCanvasY = false; if (dy > 0) { final mayCanvasCanMove = _displayOriginY + - FFI.imageModel.image!.height - + (parent.target?.imageModel.image!.height ?? 720) - r.bottom.roundToDouble(); tryMoveCanvasY = _y + dy > cy && mayCanvasCanMove > 0; if (tryMoveCanvasY) { @@ -583,13 +766,13 @@ class CursorModel with ChangeNotifier { _x += dx; _y += dy; if (tryMoveCanvasX && dx != 0) { - FFI.canvasModel.panX(-dx); + parent.target?.canvasModel.panX(-dx); } if (tryMoveCanvasY && dy != 0) { - FFI.canvasModel.panY(-dy); + parent.target?.canvasModel.panY(-dy); } - FFI.moveMouse(_x, _y); + parent.target?.moveMouse(_x, _y); notifyListeners(); } @@ -601,10 +784,10 @@ class CursorModel with ChangeNotifier { var height = int.parse(evt['height']); List colors = json.decode(evt['colors']); final rgba = Uint8List.fromList(colors.map((s) => s as int).toList()); - var pid = FFI.id; + var pid = parent.target?.id; ui.decodeImageFromPixels(rgba, width, height, ui.PixelFormat.rgba8888, (image) { - if (FFI.id != pid) return; + if (parent.target?.id != pid) return; _image = image; _images[id] = Tuple3(image, _hotx, _hoty); try { @@ -626,6 +809,7 @@ class CursorModel with ChangeNotifier { } } + /// Update the cursor position. void updateCursorPosition(Map evt) { _x = double.parse(evt['x']); _y = double.parse(evt['y']); @@ -637,8 +821,8 @@ class CursorModel with ChangeNotifier { _displayOriginY = y; _x = x + 1; _y = y + 1; - FFI.moveMouse(x, y); - FFI.canvasModel.resetOffset(); + parent.target?.moveMouse(x, y); + parent.target?.canvasModel.resetOffset(); notifyListeners(); } @@ -648,7 +832,7 @@ class CursorModel with ChangeNotifier { _displayOriginY = y; _x = xCursor; _y = yCursor; - FFI.moveMouse(x, y); + parent.target?.moveMouse(x, y); notifyListeners(); } @@ -669,15 +853,19 @@ class QualityMonitorData { } class QualityMonitorModel with ChangeNotifier { - var _show = FFI.getByName('toggle_option', 'show-quality-monitor') == 'true'; + WeakReference parent; + + QualityMonitorModel(this.parent); + var _show = false; final _data = QualityMonitorData(); bool get show => _show; QualityMonitorData get data => _data; - checkShowQualityMonitor() { - final show = - FFI.getByName('toggle_option', 'show-quality-monitor') == 'true'; + checkShowQualityMonitor(String id) async { + final show = await bind.sessionGetToggleOption( + id: id, arg: 'show-quality-monitor') == + true; if (_show != show) { _show = show; notifyListeners(); @@ -698,6 +886,7 @@ class QualityMonitorModel with ChangeNotifier { } } +/// Mouse button enum. enum MouseButtons { left, right, wheel } extension ToString on MouseButtons { @@ -713,46 +902,69 @@ extension ToString on MouseButtons { } } +/// FFI class for communicating with the Rust core. class FFI { - static var id = ""; - static var shift = false; - static var ctrl = false; - static var alt = false; - static var command = false; - static var version = ""; - static final imageModel = ImageModel(); - static final ffiModel = FfiModel(); - static final cursorModel = CursorModel(); - static final canvasModel = CanvasModel(); - static final serverModel = ServerModel(); - static final chatModel = ChatModel(); - static final fileModel = FileModel(); - static final qualityMonitorModel = QualityMonitorModel(); + var id = ""; + var shift = false; + var ctrl = false; + var alt = false; + var command = false; + var version = ""; - static String getId() { - return getByName('remote_id'); + /// dialogManager use late to ensure init after main page binding [globalKey] + late final dialogManager = OverlayDialogManager(); + + late final ImageModel imageModel; // session + late final FfiModel ffiModel; // session + late final CursorModel cursorModel; // session + late final CanvasModel canvasModel; // session + late final ServerModel serverModel; // global + late final ChatModel chatModel; // session + late final FileModel fileModel; // session + late final AbModel abModel; // global + late final UserModel userModel; // global + late final QualityMonitorModel qualityMonitorModel; // session + + FFI() { + this.imageModel = ImageModel(WeakReference(this)); + this.ffiModel = FfiModel(WeakReference(this)); + this.cursorModel = CursorModel(WeakReference(this)); + this.canvasModel = CanvasModel(WeakReference(this)); + this.serverModel = ServerModel(WeakReference(this)); // use global FFI + this.chatModel = ChatModel(WeakReference(this)); + this.fileModel = FileModel(WeakReference(this)); + this.abModel = AbModel(WeakReference(this)); + this.userModel = UserModel(WeakReference(this)); + this.qualityMonitorModel = QualityMonitorModel(WeakReference(this)); } - static void tap(MouseButtons button) { + /// Send a mouse tap event(down and up). + void tap(MouseButtons button) { sendMouse('down', button); sendMouse('up', button); } - static void scroll(int y) { - setByName('send_mouse', - json.encode(modify({'type': 'wheel', 'y': y.toString()}))); + /// Send scroll event with scroll distance [y]. + void scroll(int y) { + bind.sessionSendMouse( + id: id, + msg: json + .encode(modify({'id': id, 'type': 'wheel', 'y': y.toString()}))); } - static void reconnect() { - setByName('reconnect'); - FFI.ffiModel.clearPermissions(); - } + /// Reconnect to the remote peer. + // static void reconnect() { + // setByName('reconnect'); + // parent.target?.ffiModel.clearPermissions(); + // } - static void resetModifiers() { + /// Reset key modifiers to false, including [shift], [ctrl], [alt] and [command]. + void resetModifiers() { shift = ctrl = alt = command = false; } - static Map modify(Map evt) { + /// Modify the given modifier map [evt] based on current modifier key status. + Map modify(Map evt) { if (ctrl) evt['ctrl'] = 'true'; if (shift) evt['shift'] = 'true'; if (alt) evt['alt'] = 'true'; @@ -760,37 +972,55 @@ class FFI { return evt; } - static void sendMouse(String type, MouseButtons button) { + /// Send mouse press event. + void sendMouse(String type, MouseButtons button) { if (!ffiModel.keyboard()) return; - setByName('send_mouse', - json.encode(modify({'type': type, 'buttons': button.value}))); + bind.sessionSendMouse( + id: id, + msg: json.encode(modify({'type': type, 'buttons': button.value}))); } - static void inputKey(String name, {bool? down, bool? press}) { + /// Send key stroke event. + /// [down] indicates the key's state(down or up). + /// [press] indicates a click event(down and up). + void inputKey(String name, {bool? down, bool? press}) { if (!ffiModel.keyboard()) return; - final Map out = Map(); - out['name'] = name; - // default: down = false - if (down == true) { - out['down'] = "true"; - } - // default: press = true - if (press != false) { - out['press'] = "true"; - } - setByName('input_key', json.encode(modify(out))); + // final Map out = Map(); + // out['name'] = name; + // // default: down = false + // if (down == true) { + // out['down'] = "true"; + // } + // // default: press = true + // if (press != false) { + // out['press'] = "true"; + // } + // setByName('input_key', json.encode(modify(out))); + // TODO id + bind.sessionInputKey( + id: id, + name: name, + down: down ?? false, + press: press ?? true, + alt: alt, + ctrl: ctrl, + shift: shift, + command: command); } - static void moveMouse(double x, double y) { + /// Send mouse movement event with distance in [x] and [y]. + void moveMouse(double x, double y) { if (!ffiModel.keyboard()) return; var x2 = x.toInt(); var y2 = y.toInt(); - setByName('send_mouse', json.encode(modify({'x': '$x2', 'y': '$y2'}))); + bind.sessionSendMouse( + id: id, msg: json.encode(modify({'x': '$x2', 'y': '$y2'}))); } - static List peers() { + /// List the saved peers. + Future> peers() async { try { - var str = getByName('peers'); + var str = await bind.mainGetRecentPeers(); if (str == "") return []; List peers = json.decode(str); return peers @@ -804,61 +1034,73 @@ class FFI { return []; } - static void connect(String id, {bool isFileTransfer = false}) { + /// Connect with the given [id]. Only transfer file if [isFileTransfer]. + void connect(String id, + {bool isFileTransfer = false, double tabBarHeight = 0.0}) { + if (!isFileTransfer) { + chatModel.resetClientMode(); + canvasModel.id = id; + imageModel._id = id; + cursorModel.id = id; + } + id = isFileTransfer ? 'ft_${id}' : id; + final stream = bind.sessionConnect(id: id, isFileTransfer: isFileTransfer); + final cb = ffiModel.startEventListener(id); + () async { + await for (final message in stream) { + if (message is Event) { + try { + Map event = json.decode(message.field0); + cb(event); + } catch (e) { + print('json.decode fail(): $e'); + } + } else if (message is Rgba) { + imageModel.onRgba(message.field0, tabBarHeight); + } + } + }(); + // every instance will bind a stream + this.id = id; if (isFileTransfer) { - setByName('connect_file_transfer', id); - } else { - FFI.chatModel.resetClientMode(); - setByName('connect', id); + this.fileModel.initFileFetcher(); } - FFI.id = id; } - static Map? popEvent() { - var s = getByName('event'); - if (s == '') return null; - try { - Map event = json.decode(s); - return event; - } catch (e) { - print('popEvent(): $e'); - } - return null; + /// Login with [password], choose if the client should [remember] it. + void login(String id, String password, bool remember) { + bind.sessionLogin(id: id, password: password, remember: remember); } - static void login(String password, bool remember) { - setByName( - 'login', - json.encode({ - 'password': password, - 'remember': remember ? 'true' : 'false', - })); - } - - static void close() { + /// Close the remote session. + Future close() async { chatModel.close(); - if (FFI.imageModel.image != null && !isDesktop) { - savePreference(id, cursorModel.x, cursorModel.y, canvasModel.x, + if (imageModel.image != null && !isWebDesktop) { + await savePreference(id, cursorModel.x, cursorModel.y, canvasModel.x, canvasModel.y, canvasModel.scale, ffiModel.pi.currentDisplay); } + bind.sessionClose(id: id); id = ""; - setByName('close', ''); - imageModel.update(null); + imageModel.update(null, 0.0); cursorModel.clear(); ffiModel.clear(); canvasModel.clear(); resetModifiers(); + print("model closed"); } - static String getByName(String name, [String arg = '']) { - return PlatformFFI.getByName(name, arg); - } + /// Send **get** command to the Rust core based on [name] and [arg]. + /// Return the result as a string. + // String getByName(String name, [String arg = '']) { + // return platformFFI.getByName(name, arg); + // } - static void setByName(String name, [String value = '']) { - PlatformFFI.setByName(name, value); - } + /// Send **set** command to the Rust core based on [name] and [value]. + // void setByName(String name, [String value = '']) { + // platformFFI.setByName(name, value); + // } - static handleMouse(Map evt) { + handleMouse(Map evt, {double tabBarHeight = 0.0}) { var type = ''; var isMove = false; switch (evt['type']) { @@ -876,18 +1118,34 @@ class FFI { } evt['type'] = type; var x = evt['x']; - var y = evt['y']; + var y = max(0.0, (evt['y'] as double) - tabBarHeight); if (isMove) { - FFI.canvasModel.moveDesktopMouse(x, y); + canvasModel.moveDesktopMouse(x, y); } - final d = FFI.ffiModel.display; - x -= FFI.canvasModel.x; - y -= FFI.canvasModel.y; + final d = ffiModel.display; + if (canvasModel.scrollStyle == ScrollStyle.scrollbar) { + final imageWidth = d.width * canvasModel.scale; + final imageHeight = d.height * canvasModel.scale; + x += imageWidth * canvasModel.scrollX; + y += imageHeight * canvasModel.scrollY; + + // boxed size is a center widget + if (canvasModel.size.width > imageWidth) { + x -= ((canvasModel.size.width - imageWidth) / 2); + } + if (canvasModel.size.height > imageHeight) { + y -= ((canvasModel.size.height - imageHeight) / 2); + } + } else { + x -= canvasModel.x; + y -= canvasModel.y; + } + if (!isMove && (x < 0 || x > d.width || y < 0 || y > d.height)) { return; } - x /= FFI.canvasModel.scale; - y /= FFI.canvasModel.scale; + x /= canvasModel.scale; + y /= canvasModel.scale; x += d.x; y += d.y; if (type != '') { @@ -909,37 +1167,47 @@ class FFI { break; } evt['buttons'] = buttons; - setByName('send_mouse', json.encode(evt)); + bind.sessionSendMouse(id: id, msg: json.encode(evt)); } - static listenToMouse(bool yesOrNo) { + listenToMouse(bool yesOrNo) { if (yesOrNo) { - PlatformFFI.startDesktopWebListener(); + platformFFI.startDesktopWebListener(); } else { - PlatformFFI.stopDesktopWebListener(); + platformFFI.stopDesktopWebListener(); } } - static void setMethodCallHandler(FMethod callback) { - PlatformFFI.setMethodCallHandler(callback); + void setMethodCallHandler(FMethod callback) { + platformFFI.setMethodCallHandler(callback); } - static Future invokeMethod(String method, [dynamic arguments]) async { - return await PlatformFFI.invokeMethod(method, arguments); + Future invokeMethod(String method, [dynamic arguments]) async { + return await platformFFI.invokeMethod(method, arguments); } -} -class Peer { - final String id; - final String username; - final String hostname; - final String platform; + Future> getAudioInputs() async { + return await bind.mainGetSoundInputs(); + } - Peer.fromJson(String id, Map json) - : id = id, - username = json['username'], - hostname = json['hostname'], - platform = json['platform']; + Future getDefaultAudioInput() async { + final input = await bind.mainGetOption(key: 'audio-input'); + if (input.isEmpty && Platform.isWindows) { + return "System Sound"; + } + return input; + } + + void setDefaultAudioInput(String input) { + bind.mainSetOption(key: 'audio-input', value: input); + } + + Future> getHttpHeaders() async { + return { + "Authorization": + "Bearer " + await bind.mainGetLocalOption(key: "access_token") + }; + } } class Display { @@ -959,8 +1227,8 @@ class PeerInfo { List displays = []; } -void savePreference(String id, double xCursor, double yCursor, double xCanvas, - double yCanvas, double scale, int currentDisplay) async { +Future savePreference(String id, double xCursor, double yCursor, + double xCanvas, double yCanvas, double scale, int currentDisplay) async { SharedPreferences prefs = await SharedPreferences.getInstance(); final p = Map(); p['xCursor'] = xCursor; @@ -973,7 +1241,7 @@ void savePreference(String id, double xCursor, double yCursor, double xCanvas, } Future?> getPreference(String id) async { - if (!isDesktop) return null; + if (!isWebDesktop) return null; SharedPreferences prefs = await SharedPreferences.getInstance(); var p = prefs.getString('peer' + id); if (p == null) return null; @@ -986,15 +1254,15 @@ void removePreference(String id) async { prefs.remove('peer' + id); } -void initializeCursorAndCanvas() async { - var p = await getPreference(FFI.id); +void initializeCursorAndCanvas(FFI ffi) async { + var p = await getPreference(ffi.id); int currentDisplay = 0; if (p != null) { currentDisplay = p['currentDisplay']; } - if (p == null || currentDisplay != FFI.ffiModel.pi.currentDisplay) { - FFI.cursorModel - .updateDisplayOrigin(FFI.ffiModel.display.x, FFI.ffiModel.display.y); + if (p == null || currentDisplay != ffi.ffiModel.pi.currentDisplay) { + ffi.cursorModel + .updateDisplayOrigin(ffi.ffiModel.display.x, ffi.ffiModel.display.y); return; } double xCursor = p['xCursor']; @@ -1002,16 +1270,20 @@ void initializeCursorAndCanvas() async { double xCanvas = p['xCanvas']; double yCanvas = p['yCanvas']; double scale = p['scale']; - FFI.cursorModel.updateDisplayOriginWithCursor( - FFI.ffiModel.display.x, FFI.ffiModel.display.y, xCursor, yCursor); - FFI.canvasModel.update(xCanvas, yCanvas, scale); + ffi.cursorModel.updateDisplayOriginWithCursor( + ffi.ffiModel.display.x, ffi.ffiModel.display.y, xCursor, yCursor); + ffi.canvasModel.update(xCanvas, yCanvas, scale); } -String translate(String name) { - if (name.startsWith('Failed to') && name.contains(': ')) { - return name.split(': ').map((x) => translate(x)).join(': '); - } - var a = 'translate'; - var b = '{"locale": "$localeName", "text": "$name"}'; - return FFI.getByName(a, b); -} +/// Translate text based on the pre-defined dictionary. +/// note: params [FFI?] can be used to replace global FFI implementation +/// for example: during global initialization, gFFI not exists yet. +// String translate(String name, {FFI? ffi}) { +// if (name.startsWith('Failed to') && name.contains(': ')) { +// return name.split(': ').map((x) => translate(x)).join(': '); +// } +// var a = 'translate'; +// var b = '{"locale": "$localeName", "text": "$name"}'; +// +// return (ffi ?? gFFI).getByName(a, b); +// } diff --git a/flutter/lib/models/native_model.dart b/flutter/lib/models/native_model.dart index a600e372e..55f2d0e79 100644 --- a/flutter/lib/models/native_model.dart +++ b/flutter/lib/models/native_model.dart @@ -1,15 +1,18 @@ import 'dart:convert'; +import 'dart:ffi'; import 'dart:io'; import 'dart:typed_data'; -import 'dart:ffi'; -import 'package:ffi/ffi.dart'; -import 'package:path_provider/path_provider.dart'; -import 'package:device_info/device_info.dart'; -import 'package:package_info/package_info.dart'; + +import 'package:device_info_plus/device_info_plus.dart'; import 'package:external_path/external_path.dart'; +import 'package:ffi/ffi.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; -import '../generated_bridge.dart'; +import 'package:package_info_plus/package_info_plus.dart'; +import 'package:path_provider/path_provider.dart'; + import '../common.dart'; +import '../generated_bridge.dart'; class RgbaFrame extends Struct { @Uint32() @@ -19,58 +22,100 @@ class RgbaFrame extends Struct { typedef F2 = Pointer Function(Pointer, Pointer); typedef F3 = void Function(Pointer, Pointer); +typedef HandleEvent = void Function(Map evt); +/// FFI wrapper around the native Rust core. +/// Hides the platform differences. class PlatformFFI { - static String _dir = ''; - static String _homeDir = ''; - static F2? _getByName; - static F3? _setByName; - static void Function(Map)? _eventCallback; - static void Function(Uint8List)? _rgbaCallback; + String _dir = ''; + String _homeDir = ''; + F2? _translate; + var _eventHandlers = Map>(); + late RustdeskImpl _ffiBind; + late String _appType; + void Function(Map)? _eventCallback; + + PlatformFFI._(); + + static final PlatformFFI instance = PlatformFFI._(); + final _toAndroidChannel = MethodChannel("mChannel"); + + RustdeskImpl get ffiBind => _ffiBind; + + static get localeName => Platform.localeName; static Future getVersion() async { PackageInfo packageInfo = await PackageInfo.fromPlatform(); return packageInfo.version; } - static String getByName(String name, [String arg = '']) { - if (_getByName == null) return ''; + bool registerEventHandler( + String event_name, String handler_name, HandleEvent handler) { + debugPrint('registerEventHandler $event_name $handler_name'); + var handlers = _eventHandlers[event_name]; + if (handlers == null) { + _eventHandlers[event_name] = {handler_name: handler}; + return true; + } else { + if (handlers.containsKey(handler_name)) { + return false; + } else { + handlers[handler_name] = handler; + return true; + } + } + } + + void unregisterEventHandler(String event_name, String handler_name) { + debugPrint('unregisterEventHandler $event_name $handler_name'); + var handlers = _eventHandlers[event_name]; + if (handlers != null) { + handlers.remove(handler_name); + } + } + + String translate(String name, String locale) { + if (_translate == null) return name; var a = name.toNativeUtf8(); - var b = arg.toNativeUtf8(); - var p = _getByName!(a, b); + var b = locale.toNativeUtf8(); + var p = _translate!(a, b); assert(p != nullptr); - var res = p.toDartString(); + final res = p.toDartString(); calloc.free(p); calloc.free(a); calloc.free(b); return res; } - static void setByName(String name, [String value = '']) { - if (_setByName == null) return; - var a = name.toNativeUtf8(); - var b = value.toNativeUtf8(); - _setByName!(a, b); - calloc.free(a); - calloc.free(b); - } - - static Future init() async { - isIOS = Platform.isIOS; - isAndroid = Platform.isAndroid; + /// Init the FFI class, loads the native Rust core library. + Future init(String appType) async { + _appType = appType; + // if (isDesktop) { + // // TODO + // return; + // } final dylib = Platform.isAndroid ? DynamicLibrary.open('librustdesk.so') - : DynamicLibrary.process(); - print('initializing FFI'); + : Platform.isLinux + ? DynamicLibrary.open("/usr/lib/rustdesk/librustdesk.so") + : Platform.isWindows + ? DynamicLibrary.open("librustdesk.dll") + : Platform.isMacOS + ? DynamicLibrary.open("librustdesk.dylib") + : DynamicLibrary.process(); + debugPrint('initializing FFI ${_appType}'); try { - _getByName = dylib.lookupFunction('get_by_name'); - _setByName = - dylib.lookupFunction, Pointer), F3>( - 'set_by_name'); + _translate = dylib.lookupFunction('translate'); _dir = (await getApplicationDocumentsDirectory()).path; - _startListenEvent(RustdeskImpl(dylib)); + _ffiBind = RustdeskImpl(dylib); + _startListenEvent(_ffiBind); // global event try { - _homeDir = (await ExternalPath.getExternalStorageDirectories())[0]; + if (isAndroid) { + // only support for android + _homeDir = (await ExternalPath.getExternalStorageDirectories())[0]; + } else { + _homeDir = (await getDownloadsDirectory())?.path ?? ""; + } } catch (e) { print(e); } @@ -81,71 +126,91 @@ class PlatformFFI { AndroidDeviceInfo androidInfo = await deviceInfo.androidInfo; name = '${androidInfo.brand}-${androidInfo.model}'; id = androidInfo.id.hashCode.toString(); - androidVersion = androidInfo.version.sdkInt; - } else { + androidVersion = androidInfo.version.sdkInt ?? 0; + } else if (Platform.isIOS) { IosDeviceInfo iosInfo = await deviceInfo.iosInfo; - name = iosInfo.utsname.machine; + name = iosInfo.utsname.machine ?? ""; id = iosInfo.identifierForVendor.hashCode.toString(); + } else if (Platform.isLinux) { + LinuxDeviceInfo linuxInfo = await deviceInfo.linuxInfo; + name = linuxInfo.name; + id = linuxInfo.machineId ?? linuxInfo.id; + } else if (Platform.isWindows) { + WindowsDeviceInfo winInfo = await deviceInfo.windowsInfo; + name = winInfo.computerName; + id = winInfo.computerName; + } else if (Platform.isMacOS) { + MacOsDeviceInfo macOsInfo = await deviceInfo.macOsInfo; + name = macOsInfo.computerName; + id = macOsInfo.systemGUID ?? ""; } - print("info1-id:$id,info2-name:$name,dir:$_dir,homeDir:$_homeDir"); - setByName('info1', id); - setByName('info2', name); - setByName('home_dir', _homeDir); - setByName('init', _dir); + print( + "_appType:$_appType,info1-id:$id,info2-name:$name,dir:$_dir,homeDir:$_homeDir"); + await _ffiBind.mainDeviceId(id: id); + await _ffiBind.mainDeviceName(name: name); + await _ffiBind.mainSetHomeDir(home: _homeDir); + await _ffiBind.mainInit(appDir: _dir); } catch (e) { print(e); } version = await getVersion(); } - static void _startListenEvent(RustdeskImpl rustdeskImpl) { - () async { - await for (final message in rustdeskImpl.startEventStream()) { - if (_eventCallback != null) { - try { - Map event = json.decode(message); - _eventCallback!(event); - } catch (e) { - print('json.decode fail(): $e'); - } + bool _tryHandle(Map evt) { + final name = evt['name']; + if (name != null) { + final handlers = _eventHandlers[name]; + if (handlers != null) { + if (handlers.isNotEmpty) { + handlers.values.forEach((handler) { + handler(evt); + }); + return true; } } - }(); + } + return false; + } + + /// Start listening to the Rust core's events and frames. + void _startListenEvent(RustdeskImpl rustdeskImpl) { () async { - await for (final rgba in rustdeskImpl.startRgbaStream()) { - if (_rgbaCallback != null) { - _rgbaCallback!(rgba); - } else { - rgba.clear(); + await for (final message + in rustdeskImpl.startGlobalEventStream(appType: _appType)) { + try { + Map event = json.decode(message); + // _tryHandle here may be more flexible than _eventCallback + if (!_tryHandle(event)) { + if (_eventCallback != null) { + _eventCallback!(event); + } + } + } catch (e) { + print('json.decode fail(): $e'); } } }(); } - static void setEventCallback(void Function(Map) fun) async { + void setEventCallback(void Function(Map) fun) async { _eventCallback = fun; } - static void setRgbaCallback(void Function(Uint8List) fun) async { - _rgbaCallback = fun; - } + void setRgbaCallback(void Function(Uint8List) fun) async {} - static void startDesktopWebListener() {} + void startDesktopWebListener() {} - static void stopDesktopWebListener() {} + void stopDesktopWebListener() {} - static void setMethodCallHandler(FMethod callback) { - toAndroidChannel.setMethodCallHandler((call) async { + void setMethodCallHandler(FMethod callback) { + _toAndroidChannel.setMethodCallHandler((call) async { callback(call.method, call.arguments); return null; }); } - static invokeMethod(String method, [dynamic arguments]) async { + invokeMethod(String method, [dynamic arguments]) async { if (!isAndroid) return Future(() => false); - return await toAndroidChannel.invokeMethod(method, arguments); + return await _toAndroidChannel.invokeMethod(method, arguments); } } - -final localeName = Platform.localeName; -final toAndroidChannel = MethodChannel("mChannel"); diff --git a/flutter/lib/models/peer_model.dart b/flutter/lib/models/peer_model.dart new file mode 100644 index 000000000..5c889e60f --- /dev/null +++ b/flutter/lib/models/peer_model.dart @@ -0,0 +1,128 @@ +import 'dart:convert'; +import 'package:flutter/foundation.dart'; +import 'platform_model.dart'; + +class Peer { + final String id; + final String username; + final String hostname; + final String platform; + final List tags; + bool online = false; + + Peer.fromJson(String id, Map json) + : id = id, + username = json['username'] ?? '', + hostname = json['hostname'] ?? '', + platform = json['platform'] ?? '', + tags = json['tags'] ?? []; + + Peer({ + required this.id, + required this.username, + required this.hostname, + required this.platform, + required this.tags, + }); + + Peer.loading() + : this( + id: '...', + username: '...', + hostname: '...', + platform: '...', + tags: []); +} + +class Peers extends ChangeNotifier { + late String _name; + late List _peers; + late final _loadEvent; + static const _cbQueryOnlines = 'callback_query_onlines'; + + Peers(String name, String loadEvent, List _initPeers) { + _name = name; + _loadEvent = loadEvent; + _peers = _initPeers; + platformFFI.registerEventHandler(_cbQueryOnlines, _name, (evt) { + _updateOnlineState(evt); + }); + platformFFI.registerEventHandler(_loadEvent, _name, (evt) { + _updatePeers(evt); + }); + } + + List get peers => _peers; + + @override + void dispose() { + platformFFI.unregisterEventHandler(_cbQueryOnlines, _name); + platformFFI.unregisterEventHandler(_loadEvent, _name); + super.dispose(); + } + + Peer getByIndex(int index) { + if (index < _peers.length) { + return _peers[index]; + } else { + return Peer.loading(); + } + } + + int getPeersCount() { + return _peers.length; + } + + void _updateOnlineState(Map evt) { + evt['onlines'].split(',').forEach((online) { + for (var i = 0; i < _peers.length; i++) { + if (_peers[i].id == online) { + _peers[i].online = true; + } + } + }); + + evt['offlines'].split(',').forEach((offline) { + for (var i = 0; i < _peers.length; i++) { + if (_peers[i].id == offline) { + _peers[i].online = false; + } + } + }); + + notifyListeners(); + } + + void _updatePeers(Map evt) { + final onlineStates = _getOnlineStates(); + _peers = _decodePeers(evt['peers']); + _peers.forEach((peer) { + final state = onlineStates[peer.id]; + peer.online = state != null && state != false; + }); + notifyListeners(); + } + + Map _getOnlineStates() { + var onlineStates = new Map(); + _peers.forEach((peer) { + onlineStates[peer.id] = peer.online; + }); + return onlineStates; + } + + List _decodePeers(String peersStr) { + try { + if (peersStr == "") return []; + List peers = json.decode(peersStr); + return peers + .map((s) => s as List) + .map((s) => + Peer.fromJson(s[0] as String, s[1] as Map)) + .toList(); + } catch (e) { + print('peers(): $e'); + } + return []; + } +} diff --git a/flutter/lib/models/platform_model.dart b/flutter/lib/models/platform_model.dart new file mode 100644 index 000000000..d2b8fa765 --- /dev/null +++ b/flutter/lib/models/platform_model.dart @@ -0,0 +1,7 @@ +import 'package:flutter_hbb/generated_bridge.dart'; +import 'native_model.dart' if (dart.library.html) 'web_model.dart'; + +final platformFFI = PlatformFFI.instance; +final localeName = PlatformFFI.localeName; + +RustdeskImpl get bind => platformFFI.ffiBind; diff --git a/flutter/lib/models/server_model.dart b/flutter/lib/models/server_model.dart index 2cf41725b..fa7f15e54 100644 --- a/flutter/lib/models/server_model.dart +++ b/flutter/lib/models/server_model.dart @@ -1,13 +1,18 @@ import 'dart:async'; import 'dart:convert'; +import 'dart:io'; + import 'package:flutter/material.dart'; +import 'package:flutter_hbb/models/platform_model.dart'; import 'package:wakelock/wakelock.dart'; + import '../common.dart'; -import '../pages/server_page.dart'; +import '../desktop/pages/server_page.dart' as Desktop; +import '../desktop/widgets/tabbar_widget.dart'; +import '../mobile/pages/server_page.dart'; import 'model.dart'; -const loginDialogTag = "LOGIN"; -final _emptyIdShow = translate("Generating ..."); +const KLoginDialogTag = "LOGIN"; const kUseTemporaryPassword = "use-temporary-password"; const kUsePermanentPassword = "use-permanent-password"; @@ -21,11 +26,15 @@ class ServerModel with ChangeNotifier { bool _fileOk = false; int _connectStatus = 0; // Rendezvous Server status String _verificationMethod = ""; + String _temporaryPasswordLength = ""; - final _serverId = TextEditingController(text: _emptyIdShow); + late String _emptyIdShow; + late final TextEditingController _serverId; final _serverPasswd = TextEditingController(text: ""); - Map _clients = {}; + final tabController = DesktopTabController(); + + List _clients = []; bool get isStart => _isStart; @@ -39,57 +48,50 @@ class ServerModel with ChangeNotifier { int get connectStatus => _connectStatus; - String get verificationMethod => _verificationMethod; + String get verificationMethod { + final index = [ + kUseTemporaryPassword, + kUsePermanentPassword, + kUseBothPasswords + ].indexOf(_verificationMethod); + if (index < 0) { + return kUseBothPasswords; + } + return _verificationMethod; + } + + set verificationMethod(String method) { + bind.mainSetOption(key: "verification-method", value: method); + } + + String get temporaryPasswordLength { + final lengthIndex = ["6", "8", "10"].indexOf(_temporaryPasswordLength); + if (lengthIndex < 0) { + return "6"; + } + return _temporaryPasswordLength; + } + + set temporaryPasswordLength(String length) { + bind.mainSetOption(key: "temporary-password-length", value: length); + } TextEditingController get serverId => _serverId; TextEditingController get serverPasswd => _serverPasswd; - Map get clients => _clients; + List get clients => _clients; final controller = ScrollController(); - ServerModel() { - () async { - /** - * 1. check android permission - * 2. check config - * audio true by default (if permission on) (false default < Android 10) - * file true by default (if permission on) - */ - await Future.delayed(Duration(seconds: 1)); + WeakReference parent; - // audio - if (androidVersion < 30 || !await PermissionManager.check("audio")) { - _audioOk = false; - FFI.setByName( - 'option', - jsonEncode(Map() - ..["name"] = "enable-audio" - ..["value"] = "N")); - } else { - final audioOption = FFI.getByName('option', 'enable-audio'); - _audioOk = audioOption.isEmpty; - } + ServerModel(this.parent) { + _emptyIdShow = translate("Generating ..."); + _serverId = TextEditingController(text: this._emptyIdShow); - // file - if (!await PermissionManager.check("file")) { - _fileOk = false; - FFI.setByName( - 'option', - jsonEncode(Map() - ..["name"] = "enable-file-transfer" - ..["value"] = "N")); - } else { - final fileOption = FFI.getByName('option', 'enable-file-transfer'); - _fileOk = fileOption.isEmpty; - } - - notifyListeners(); - }(); - - Timer.periodic(Duration(seconds: 1), (timer) { - var status = int.tryParse(FFI.getByName('connect_statue')) ?? 0; + Timer.periodic(Duration(seconds: 1), (timer) async { + var status = await bind.mainGetOnlineStatue(); if (status > 0) { status = 1; } @@ -97,9 +99,8 @@ class ServerModel with ChangeNotifier { _connectStatus = status; notifyListeners(); } - final res = - FFI.getByName('check_clients_length', _clients.length.toString()); - if (res.isNotEmpty) { + final res = await bind.mainCheckClientsLength(length: _clients.length); + if (res != null) { debugPrint("clients not match!"); updateClientState(res); } @@ -108,19 +109,59 @@ class ServerModel with ChangeNotifier { }); } - updatePasswordModel() { - var update = false; - final temporaryPassword = FFI.getByName("temporary_password"); - final verificationMethod = FFI.getByName("option", "verification-method"); - if (_serverPasswd.text != temporaryPassword) { - _serverPasswd.text = temporaryPassword; - update = true; + /// 1. check android permission + /// 2. check config + /// audio true by default (if permission on) (false default < Android 10) + /// file true by default (if permission on) + checkAndroidPermission() async { + // audio + if (androidVersion < 30 || !await PermissionManager.check("audio")) { + _audioOk = false; + bind.mainSetOption(key: "enable-audio", value: "N"); + } else { + final audioOption = await bind.mainGetOption(key: 'enable-audio'); + _audioOk = audioOption.isEmpty; } + // file + if (!await PermissionManager.check("file")) { + _fileOk = false; + bind.mainSetOption(key: "enable-file-transfer", value: "N"); + } else { + final fileOption = await bind.mainGetOption(key: 'enable-file-transfer'); + _fileOk = fileOption.isEmpty; + } + + // input (mouse control) false by default + bind.mainSetOption(key: "enable-keyboard", value: "N"); + notifyListeners(); + } + + updatePasswordModel() async { + var update = false; + final temporaryPassword = await bind.mainGetTemporaryPassword(); + final verificationMethod = + await bind.mainGetOption(key: "verification-method"); + final temporaryPasswordLength = + await bind.mainGetOption(key: "temporary-password-length"); + final oldPwdText = _serverPasswd.text; + if (_serverPasswd.text != temporaryPassword) { + _serverPasswd.text = temporaryPassword; + } + if (verificationMethod == kUsePermanentPassword) { + _serverPasswd.text = '-'; + } + if (oldPwdText != _serverPasswd.text) { + update = true; + } if (_verificationMethod != verificationMethod) { _verificationMethod = verificationMethod; update = true; } + if (_temporaryPasswordLength != temporaryPasswordLength) { + _temporaryPasswordLength = temporaryPasswordLength; + update = true; + } if (update) { notifyListeners(); } @@ -136,10 +177,7 @@ class ServerModel with ChangeNotifier { } _audioOk = !_audioOk; - Map res = Map() - ..["name"] = "enable-audio" - ..["value"] = _audioOk ? '' : 'N'; - FFI.setByName('option', jsonEncode(res)); + bind.mainSetOption(key: "enable-audio", value: _audioOk ? '' : 'N'); notifyListeners(); } @@ -153,25 +191,25 @@ class ServerModel with ChangeNotifier { } _fileOk = !_fileOk; - Map res = Map() - ..["name"] = "enable-file-transfer" - ..["value"] = _fileOk ? '' : 'N'; - FFI.setByName('option', jsonEncode(res)); + bind.mainSetOption(key: "enable-file-transfer", value: _fileOk ? '' : 'N'); notifyListeners(); } toggleInput() { if (_inputOk) { - FFI.invokeMethod("stop_input"); + parent.target?.invokeMethod("stop_input"); } else { - showInputWarnAlert(); + if (parent.target != null) { + showInputWarnAlert(parent.target!); + } } } + /// Toggle the screen sharing service. toggleService() async { if (_isStart) { - final res = - await DialogManager.show((setState, close) => CustomAlertDialog( + final res = await parent.target?.dialogManager + .show((setState, close) => CustomAlertDialog( title: Row(children: [ Icon(Icons.warning_amber_sharp, color: Colors.redAccent, size: 28), @@ -192,8 +230,8 @@ class ServerModel with ChangeNotifier { stopService(); } } else { - final res = - await DialogManager.show((setState, close) => CustomAlertDialog( + final res = await parent.target?.dialogManager + .show((setState, close) => CustomAlertDialog( title: Row(children: [ Icon(Icons.warning_amber_sharp, color: Colors.redAccent, size: 28), @@ -216,34 +254,44 @@ class ServerModel with ChangeNotifier { } } + /// Start the screen sharing service. Future startService() async { _isStart = true; notifyListeners(); - FFI.ffiModel.updateEventListener(""); - await FFI.invokeMethod("init_service"); - FFI.setByName("start_service"); + // TODO + parent.target?.ffiModel.updateEventListener(""); + await parent.target?.invokeMethod("init_service"); + await bind.mainStartService(); _fetchID(); updateClientState(); - Wakelock.enable(); + if (!Platform.isLinux) { + // current linux is not supported + Wakelock.enable(); + } } + /// Stop the screen sharing service. Future stopService() async { _isStart = false; - FFI.serverModel.closeAll(); - await FFI.invokeMethod("stop_service"); - FFI.setByName("stop_service"); + // TODO + closeAll(); + await parent.target?.invokeMethod("stop_service"); + await bind.mainStopService(); notifyListeners(); - Wakelock.disable(); + if (!Platform.isLinux) { + // current linux is not supported + Wakelock.disable(); + } } Future initInput() async { - await FFI.invokeMethod("init_input"); + await parent.target?.invokeMethod("init_input"); } Future setPermanentPassword(String newPW) async { - FFI.setByName("permanent_password", newPW); + await bind.mainSetPermanentPassword(password: newPW); await Future.delayed(Duration(milliseconds: 500)); - final pw = FFI.getByName("permanent_password", newPW); + final pw = await bind.mainGetPermanentPassword(); if (newPW == pw) { return true; } else { @@ -257,7 +305,7 @@ class ServerModel with ChangeNotifier { const maxCount = 10; while (count < maxCount) { await Future.delayed(Duration(seconds: 1)); - final id = FFI.getByName("server_id"); + final id = await bind.mainGetMyId(); if (id.isEmpty) { continue; } else { @@ -284,10 +332,7 @@ class ServerModel with ChangeNotifier { break; case "input": if (_inputOk != value) { - Map res = Map() - ..["name"] = "enable-keyboard" - ..["value"] = value ? '' : 'N'; - FFI.setByName('option', jsonEncode(res)); + bind.mainSetOption(key: "enable-keyboard", value: value ? '' : 'N'); } _inputOk = value; break; @@ -297,13 +342,25 @@ class ServerModel with ChangeNotifier { notifyListeners(); } - updateClientState([String? json]) { - var res = json ?? FFI.getByName("clients_state"); + // force + updateClientState([String? json]) async { + var res = await bind.mainGetClientsState(); try { final List clientsJson = jsonDecode(res); + if (isDesktop && clientsJson.isEmpty && _clients.isNotEmpty) { + // exit cm when >1 peers to no peers + exit(0); + } + _clients.clear(); + tabController.state.value.tabs.clear(); for (var clientJson in clientsJson) { final client = Client.fromJson(clientJson); - _clients[client.id] = client; + _clients.add(client); + tabController.add(TabInfo( + key: client.id.toString(), + label: client.name, + closable: false, + page: Desktop.buildConnectionCard(client))); } notifyListeners(); } catch (e) { @@ -314,20 +371,25 @@ class ServerModel with ChangeNotifier { void loginRequest(Map evt) { try { final client = Client.fromJson(jsonDecode(evt["client"])); - if (_clients.containsKey(client.id)) { + if (_clients.any((c) => c.id == client.id)) { return; } - _clients[client.id] = client; + _clients.add(client); + tabController.add(TabInfo( + key: client.id.toString(), + label: client.name, + closable: false, + page: Desktop.buildConnectionCard(client))); scrollToBottom(); notifyListeners(); - showLoginDialog(client); + if (isAndroid) showLoginDialog(client); } catch (e) { debugPrint("Failed to call loginRequest,error:$e"); } } void showLoginDialog(Client client) { - DialogManager.show( + parent.target?.dialogManager.show( (setState, close) => CustomAlertDialog( title: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, @@ -373,6 +435,7 @@ class ServerModel with ChangeNotifier { } scrollToBottom() { + if (isDesktop) return; Future.delayed(Duration(milliseconds: 200), () { controller.animateTo(controller.position.maxScrollExtent, duration: Duration(milliseconds: 200), @@ -380,30 +443,39 @@ class ServerModel with ChangeNotifier { }); } - void sendLoginResponse(Client client, bool res) { - final Map response = Map(); - response["id"] = client.id; - response["res"] = res; + void sendLoginResponse(Client client, bool res) async { if (res) { - FFI.setByName("login_res", jsonEncode(response)); + bind.cmLoginRes(connId: client.id, res: res); if (!client.isFileTransfer) { - FFI.invokeMethod("start_capture"); + parent.target?.invokeMethod("start_capture"); } - FFI.invokeMethod("cancel_notification", client.id); - _clients[client.id]?.authorized = true; + parent.target?.invokeMethod("cancel_notification", client.id); + client.authorized = true; notifyListeners(); } else { - FFI.setByName("login_res", jsonEncode(response)); - FFI.invokeMethod("cancel_notification", client.id); - _clients.remove(client.id); + bind.cmLoginRes(connId: client.id, res: res); + parent.target?.invokeMethod("cancel_notification", client.id); + final index = _clients.indexOf(client); + tabController.remove(index); + _clients.remove(client); } } void onClientAuthorized(Map evt) { try { final client = Client.fromJson(jsonDecode(evt['client'])); - DialogManager.dismissByTag(getLoginDialogTag(client.id)); - _clients[client.id] = client; + parent.target?.dialogManager.dismissByTag(getLoginDialogTag(client.id)); + final index = _clients.indexWhere((c) => c.id == client.id); + if (index < 0) { + _clients.add(client); + } else { + _clients[index].authorized = true; + } + tabController.add(TabInfo( + key: client.id.toString(), + label: client.name, + closable: false, + page: Desktop.buildConnectionCard(client))); scrollToBottom(); notifyListeners(); } catch (e) {} @@ -412,10 +484,12 @@ class ServerModel with ChangeNotifier { void onClientRemove(Map evt) { try { final id = int.parse(evt['id'] as String); - if (_clients.containsKey(id)) { - _clients.remove(id); - DialogManager.dismissByTag(getLoginDialogTag(id)); - FFI.invokeMethod("cancel_notification", id); + if (_clients.any((c) => c.id == id)) { + final index = _clients.indexWhere((client) => client.id == id); + _clients.removeAt(index); + tabController.remove(index); + parent.target?.dialogManager.dismissByTag(getLoginDialogTag(id)); + parent.target?.invokeMethod("cancel_notification", id); } notifyListeners(); } catch (e) { @@ -424,10 +498,16 @@ class ServerModel with ChangeNotifier { } closeAll() { - _clients.forEach((id, client) { - FFI.setByName("close_conn", id.toString()); + _clients.forEach((client) { + bind.cmCloseConnection(connId: client.id); }); _clients.clear(); + tabController.state.value.tabs.clear(); + } + + void jumpTo(int id) { + final index = _clients.indexWhere((client) => client.id == id); + tabController.jumpTo(index); } } @@ -440,8 +520,10 @@ class Client { bool keyboard = false; bool clipboard = false; bool audio = false; + bool file = false; + bool restart = false; - Client(this.authorized, this.isFileTransfer, this.name, this.peerId, + Client(this.id, this.authorized, this.isFileTransfer, this.name, this.peerId, this.keyboard, this.clipboard, this.audio); Client.fromJson(Map json) { @@ -453,6 +535,8 @@ class Client { keyboard = json['keyboard']; clipboard = json['clipboard']; audio = json['audio']; + file = json['file']; + restart = json['restart']; } Map toJson() { @@ -470,11 +554,11 @@ class Client { } String getLoginDialogTag(int id) { - return loginDialogTag + id.toString(); + return KLoginDialogTag + id.toString(); } -showInputWarnAlert() { - DialogManager.show((setState, close) => CustomAlertDialog( +showInputWarnAlert(FFI ffi) { + ffi.dialogManager.show((setState, close) => CustomAlertDialog( title: Text(translate("How to get Android input permission?")), content: Column( mainAxisSize: MainAxisSize.min, @@ -489,7 +573,7 @@ showInputWarnAlert() { ElevatedButton( child: Text(translate("Open System Setting")), onPressed: () { - FFI.serverModel.initInput(); + ffi.serverModel.initInput(); close(); }), ], diff --git a/flutter/lib/models/user_model.dart b/flutter/lib/models/user_model.dart new file mode 100644 index 000000000..b43b4510b --- /dev/null +++ b/flutter/lib/models/user_model.dart @@ -0,0 +1,75 @@ +import 'dart:async'; +import 'dart:convert'; + +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:http/http.dart' as http; + +import 'model.dart'; +import 'platform_model.dart'; + +class UserModel extends ChangeNotifier { + var userName = "".obs; + WeakReference parent; + + UserModel(this.parent); + + Future getUserName() async { + if (userName.isNotEmpty) { + return userName.value; + } + final userInfo = await bind.mainGetLocalOption(key: 'user_info'); + if (userInfo.trim().isEmpty) { + return ""; + } + final m = jsonDecode(userInfo); + userName.value = m['name'] ?? ''; + return userName.value; + } + + Future logOut() async { + debugPrint("start logout"); + final url = await bind.mainGetApiServer(); + final _ = await http.post(Uri.parse("$url/api/logout"), + body: { + "id": await bind.mainGetMyId(), + "uuid": await bind.mainGetUuid(), + }, + headers: await _getHeaders()); + await Future.wait([ + bind.mainSetLocalOption(key: 'access_token', value: ''), + bind.mainSetLocalOption(key: 'user_info', value: ''), + bind.mainSetLocalOption(key: 'selected-tags', value: ''), + ]); + parent.target?.abModel.clear(); + userName.value = ""; + notifyListeners(); + } + + Future>? _getHeaders() { + return parent.target?.getHttpHeaders(); + } + + Future> login(String userName, String pass) async { + final url = await bind.mainGetApiServer(); + try { + final resp = await http.post(Uri.parse("$url/api/login"), + headers: {"Content-Type": "application/json"}, + body: jsonEncode({ + "username": userName, + "password": pass, + "id": await bind.mainGetMyId(), + "uuid": await bind.mainGetUuid() + })); + final body = jsonDecode(resp.body); + bind.mainSetLocalOption( + key: "access_token", value: body['access_token'] ?? ""); + bind.mainSetLocalOption( + key: "user_info", value: jsonEncode(body['user'])); + this.userName.value = body['user']?['name'] ?? ""; + return body; + } catch (err) { + return {"error": "$err"}; + } + } +} diff --git a/flutter/lib/models/web_model.dart b/flutter/lib/models/web_model.dart index 3ec6c9b9f..d3f1bacad 100644 --- a/flutter/lib/models/web_model.dart +++ b/flutter/lib/models/web_model.dart @@ -20,9 +20,14 @@ class PlatformFFI { context.callMethod('setByName', [name, value]); } - static Future init() async { + PlatformFFI._(); + static final PlatformFFI instance = PlatformFFI._(); + + static get localeName => window.navigator.language; + + static Future init(String _appType) async { isWeb = true; - isDesktop = !context.callMethod('isMobile'); + isWebDesktop = !context.callMethod('isMobile'); context.callMethod('init'); version = getByName('version'); } @@ -68,5 +73,3 @@ class PlatformFFI { return true; } } - -final localeName = window.navigator.language; diff --git a/flutter/lib/utils/multi_window_manager.dart b/flutter/lib/utils/multi_window_manager.dart new file mode 100644 index 000000000..9b26870c0 --- /dev/null +++ b/flutter/lib/utils/multi_window_manager.dart @@ -0,0 +1,145 @@ +import 'dart:convert'; +import 'dart:ui'; + +import 'package:desktop_multi_window/desktop_multi_window.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +/// must keep the order +enum WindowType { Main, RemoteDesktop, FileTransfer, PortForward, Unknown } + +extension Index on int { + WindowType get windowType { + switch (this) { + case 0: + return WindowType.Main; + case 1: + return WindowType.RemoteDesktop; + case 2: + return WindowType.FileTransfer; + case 3: + return WindowType.PortForward; + default: + return WindowType.Unknown; + } + } +} + +/// Window Manager +/// mainly use it in `Main Window` +/// use it in sub window is not recommended +class RustDeskMultiWindowManager { + RustDeskMultiWindowManager._(); + + static final instance = RustDeskMultiWindowManager._(); + + int? _remoteDesktopWindowId; + int? _fileTransferWindowId; + + Future new_remote_desktop(String remote_id) async { + final msg = + jsonEncode({"type": WindowType.RemoteDesktop.index, "id": remote_id}); + + try { + final ids = await DesktopMultiWindow.getAllSubWindowIds(); + if (!ids.contains(_remoteDesktopWindowId)) { + _remoteDesktopWindowId = null; + } + } on Error { + _remoteDesktopWindowId = null; + } + if (_remoteDesktopWindowId == null) { + final remoteDesktopController = + await DesktopMultiWindow.createWindow(msg); + remoteDesktopController + ..setFrame(const Offset(0, 0) & const Size(1280, 720)) + ..center() + ..setTitle("rustdesk - remote desktop") + ..show(); + _remoteDesktopWindowId = remoteDesktopController.windowId; + } else { + return call(WindowType.RemoteDesktop, "new_remote_desktop", msg); + } + } + + Future new_file_transfer(String remote_id) async { + final msg = + jsonEncode({"type": WindowType.FileTransfer.index, "id": remote_id}); + + try { + final ids = await DesktopMultiWindow.getAllSubWindowIds(); + if (!ids.contains(_fileTransferWindowId)) { + _fileTransferWindowId = null; + } + } on Error { + _fileTransferWindowId = null; + } + if (_fileTransferWindowId == null) { + final fileTransferController = await DesktopMultiWindow.createWindow(msg); + fileTransferController + ..setFrame(const Offset(0, 0) & const Size(1280, 720)) + ..center() + ..setTitle("rustdesk - file transfer") + ..show(); + _fileTransferWindowId = fileTransferController.windowId; + } else { + return call(WindowType.FileTransfer, "new_file_transfer", msg); + } + } + + Future call(WindowType type, String methodName, dynamic args) async { + int? windowId = findWindowByType(type); + if (windowId == null) { + return; + } + return await DesktopMultiWindow.invokeMethod(windowId, methodName, args); + } + + int? findWindowByType(WindowType type) { + switch (type) { + case WindowType.Main: + return 0; + case WindowType.RemoteDesktop: + return _remoteDesktopWindowId; + case WindowType.FileTransfer: + return _fileTransferWindowId; + case WindowType.PortForward: + break; + case WindowType.Unknown: + break; + } + return null; + } + + void setMethodHandler( + Future Function(MethodCall call, int fromWindowId)? handler) { + DesktopMultiWindow.setMethodHandler(handler); + } + + Future closeAllSubWindows() async { + await Future.wait(WindowType.values.map((e) => closeWindows(e))); + } + + Future closeWindows(WindowType type) async { + if (type == WindowType.Main) { + // skip main window, use window manager instead + return; + } + int? wId = findWindowByType(type); + if (wId != null) { + debugPrint("closing multi window: ${type.toString()}"); + try { + final ids = await DesktopMultiWindow.getAllSubWindowIds(); + if (!ids.contains(wId)) { + // no such window already + return; + } + await WindowController.fromWindowId(wId).close(); + } on Error { + return; + } + } + } +} + +final rustDeskWinManager = RustDeskMultiWindowManager.instance; diff --git a/flutter/lib/utils/tray_manager.dart b/flutter/lib/utils/tray_manager.dart new file mode 100644 index 000000000..f0422f554 --- /dev/null +++ b/flutter/lib/utils/tray_manager.dart @@ -0,0 +1,23 @@ +import 'dart:io'; + +import 'package:tray_manager/tray_manager.dart'; + +import '../common.dart'; + +Future initTray({List? extra_item}) async { + List items = [ + MenuItem(key: "show", label: translate("show rustdesk")), + MenuItem.separator(), + MenuItem(key: "quit", label: translate("quit rustdesk")), + ]; + if (extra_item != null) { + items.insertAll(0, extra_item); + } + await Future.wait([ + trayManager + .setIcon(Platform.isWindows ? "assets/logo.ico" : "assets/logo.png"), + trayManager.setContextMenu(Menu(items: items)), + trayManager.setToolTip("rustdesk"), + trayManager.setTitle("rustdesk") + ]); +} diff --git a/flutter/linux/.gitignore b/flutter/linux/.gitignore new file mode 100644 index 000000000..d3896c984 --- /dev/null +++ b/flutter/linux/.gitignore @@ -0,0 +1 @@ +flutter/ephemeral diff --git a/flutter/linux/CMakeLists.txt b/flutter/linux/CMakeLists.txt new file mode 100644 index 000000000..9f6d0ce52 --- /dev/null +++ b/flutter/linux/CMakeLists.txt @@ -0,0 +1,158 @@ +# Project-level configuration. +cmake_minimum_required(VERSION 3.12) +project(runner LANGUAGES CXX) + +# The name of the executable created for the application. Change this to change +# the on-disk name of your application. +set(BINARY_NAME "flutter_hbb") +# The unique GTK application identifier for this application. See: +# https://wiki.gnome.org/HowDoI/ChooseApplicationID +set(APPLICATION_ID "com.carriez.flutter_hbb") + +# Explicitly opt in to modern CMake behaviors to avoid warnings with recent +# versions of CMake. +cmake_policy(SET CMP0063 NEW) + +# Load bundled libraries from the lib/ directory relative to the binary. +set(CMAKE_INSTALL_RPATH "$ORIGIN/lib") + +# Root filesystem for cross-building. +if(FLUTTER_TARGET_PLATFORM_SYSROOT) + set(CMAKE_SYSROOT ${FLUTTER_TARGET_PLATFORM_SYSROOT}) + set(CMAKE_FIND_ROOT_PATH ${CMAKE_SYSROOT}) + set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) + set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY) + set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) + set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) +endif() + +# Define build configuration options. +if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) + set(CMAKE_BUILD_TYPE "Debug" CACHE + STRING "Flutter build mode" FORCE) + set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS + "Debug" "Profile" "Release") +endif() + +# Compilation settings that should be applied to most targets. +# +# Be cautious about adding new options here, as plugins use this function by +# default. In most cases, you should add new options to specific targets instead +# of modifying this function. +function(APPLY_STANDARD_SETTINGS TARGET) + target_compile_features(${TARGET} PUBLIC cxx_std_14) + target_compile_options(${TARGET} PRIVATE -Wall -Werror) + target_compile_options(${TARGET} PRIVATE "$<$>:-O3>") + target_compile_definitions(${TARGET} PRIVATE "$<$>:NDEBUG>") +endfunction() + +# Flutter library and tool build rules. +set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") +add_subdirectory(${FLUTTER_MANAGED_DIR}) + +# System-level dependencies. +find_package(PkgConfig REQUIRED) +pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) + +add_definitions(-DAPPLICATION_ID="${APPLICATION_ID}") + +# flutter_rust_bridge +find_package(Corrosion REQUIRED) + +corrosion_import_crate(MANIFEST_PATH ../../Cargo.toml + # Equivalent to --all-features passed to cargo build +# [ALL_FEATURES] + # Equivalent to --no-default-features passed to cargo build +# [NO_DEFAULT_FEATURES] + # Disable linking of standard libraries (required for no_std crates). +# [NO_STD] + # Specify cargo build profile (e.g. release or a custom profile) +# [PROFILE ] + # Only import the specified crates from a workspace +# [CRATES ... ] + # Enable the specified features +# [FEATURES ... ] +) + +# Define the application target. To change its name, change BINARY_NAME above, +# not the value here, or `flutter run` will no longer work. +# +# Any new source files that you add to the application should be added here. +add_executable(${BINARY_NAME} + "main.cc" + "my_application.cc" + "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" +) + +# Apply the standard set of build settings. This can be removed for applications +# that need different build settings. +apply_standard_settings(${BINARY_NAME}) + +# Add dependency libraries. Add any application-specific dependencies here. +target_link_libraries(${BINARY_NAME} PRIVATE flutter) +target_link_libraries(${BINARY_NAME} PRIVATE PkgConfig::GTK) + +target_link_libraries(${BINARY_NAME} PRIVATE librustdesk) + +# Run the Flutter tool portions of the build. This must not be removed. +add_dependencies(${BINARY_NAME} flutter_assemble) + +# Only the install-generated bundle's copy of the executable will launch +# correctly, since the resources must in the right relative locations. To avoid +# people trying to run the unbundled copy, put it in a subdirectory instead of +# the default top-level location. +set_target_properties(${BINARY_NAME} + PROPERTIES + RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/intermediates_do_not_run" +) + +# Generated plugin build rules, which manage building the plugins and adding +# them to the application. +include(flutter/generated_plugins.cmake) + + +# === Installation === +# By default, "installing" just makes a relocatable bundle in the build +# directory. +set(BUILD_BUNDLE_DIR "${PROJECT_BINARY_DIR}/bundle") +#if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) + set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) +#endif() + +# Start with a clean build bundle directory every time. +install(CODE " + file(REMOVE_RECURSE \"${BUILD_BUNDLE_DIR}/\") + " COMPONENT Runtime) + +set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") +set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}/lib") + +install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + +foreach(bundled_library ${PLUGIN_BUNDLED_LIBRARIES}) + install(FILES "${bundled_library}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endforeach(bundled_library) + +# Fully re-copy the assets directory on each build to avoid having stale files +# from a previous install. +set(FLUTTER_ASSET_DIR_NAME "flutter_assets") +install(CODE " + file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\") + " COMPONENT Runtime) +install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}" + DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) + +# Install the AOT library on non-Debug builds only. +if(NOT CMAKE_BUILD_TYPE MATCHES "Debug") + install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endif() diff --git a/flutter/linux/flutter/CMakeLists.txt b/flutter/linux/flutter/CMakeLists.txt new file mode 100644 index 000000000..d5bd01648 --- /dev/null +++ b/flutter/linux/flutter/CMakeLists.txt @@ -0,0 +1,88 @@ +# This file controls Flutter-level build steps. It should not be edited. +cmake_minimum_required(VERSION 3.10) + +set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") + +# Configuration provided via flutter tool. +include(${EPHEMERAL_DIR}/generated_config.cmake) + +# TODO: Move the rest of this into files in ephemeral. See +# https://github.com/flutter/flutter/issues/57146. + +# Serves the same purpose as list(TRANSFORM ... PREPEND ...), +# which isn't available in 3.10. +function(list_prepend LIST_NAME PREFIX) + set(NEW_LIST "") + foreach(element ${${LIST_NAME}}) + list(APPEND NEW_LIST "${PREFIX}${element}") + endforeach(element) + set(${LIST_NAME} "${NEW_LIST}" PARENT_SCOPE) +endfunction() + +# === Flutter Library === +# System-level dependencies. +find_package(PkgConfig REQUIRED) +pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) +pkg_check_modules(GLIB REQUIRED IMPORTED_TARGET glib-2.0) +pkg_check_modules(GIO REQUIRED IMPORTED_TARGET gio-2.0) + +set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/libflutter_linux_gtk.so") + +# Published to parent scope for install step. +set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) +set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) +set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) +set(AOT_LIBRARY "${PROJECT_DIR}/build/lib/libapp.so" PARENT_SCOPE) + +list(APPEND FLUTTER_LIBRARY_HEADERS + "fl_basic_message_channel.h" + "fl_binary_codec.h" + "fl_binary_messenger.h" + "fl_dart_project.h" + "fl_engine.h" + "fl_json_message_codec.h" + "fl_json_method_codec.h" + "fl_message_codec.h" + "fl_method_call.h" + "fl_method_channel.h" + "fl_method_codec.h" + "fl_method_response.h" + "fl_plugin_registrar.h" + "fl_plugin_registry.h" + "fl_standard_message_codec.h" + "fl_standard_method_codec.h" + "fl_string_codec.h" + "fl_value.h" + "fl_view.h" + "flutter_linux.h" +) +list_prepend(FLUTTER_LIBRARY_HEADERS "${EPHEMERAL_DIR}/flutter_linux/") +add_library(flutter INTERFACE) +target_include_directories(flutter INTERFACE + "${EPHEMERAL_DIR}" +) +target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}") +target_link_libraries(flutter INTERFACE + PkgConfig::GTK + PkgConfig::GLIB + PkgConfig::GIO +) +add_dependencies(flutter flutter_assemble) + +# === Flutter tool backend === +# _phony_ is a non-existent file to force this command to run every time, +# since currently there's no way to get a full input/output list from the +# flutter tool. +add_custom_command( + OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} + ${CMAKE_CURRENT_BINARY_DIR}/_phony_ + COMMAND ${CMAKE_COMMAND} -E env + ${FLUTTER_TOOL_ENVIRONMENT} + "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.sh" + ${FLUTTER_TARGET_PLATFORM} ${CMAKE_BUILD_TYPE} + VERBATIM +) +add_custom_target(flutter_assemble DEPENDS + "${FLUTTER_LIBRARY}" + ${FLUTTER_LIBRARY_HEADERS} +) diff --git a/flutter/linux/main.cc b/flutter/linux/main.cc new file mode 100644 index 000000000..55fb650bc --- /dev/null +++ b/flutter/linux/main.cc @@ -0,0 +1,28 @@ +#include +#include "my_application.h" + +#define RUSTDESK_LIB_PATH "/usr/lib/rustdesk/librustdesk.so" +typedef bool (*RustDeskCoreMain)(); + +bool flutter_rustdesk_core_main() { + void* librustdesk = dlopen(RUSTDESK_LIB_PATH, RTLD_LAZY); + if (!librustdesk) { + fprintf(stderr,"load librustdesk.so failed\n"); + return true; + } + auto core_main = (RustDeskCoreMain) dlsym(librustdesk,"rustdesk_core_main"); + char* error; + if ((error = dlerror()) != nullptr) { + fprintf(stderr, "error finding rustdesk_core_main: %s", error); + return true; + } + return core_main(); +} + +int main(int argc, char** argv) { + if (!flutter_rustdesk_core_main()) { + return 0; + } + g_autoptr(MyApplication) app = my_application_new(); + return g_application_run(G_APPLICATION(app), argc, argv); +} diff --git a/flutter/linux/my_application.cc b/flutter/linux/my_application.cc new file mode 100644 index 000000000..20513032d --- /dev/null +++ b/flutter/linux/my_application.cc @@ -0,0 +1,106 @@ +#include "my_application.h" + +#include +#ifdef GDK_WINDOWING_X11 +#include +#endif + +#include "flutter/generated_plugin_registrant.h" + +struct _MyApplication { + GtkApplication parent_instance; + char** dart_entrypoint_arguments; +}; + +G_DEFINE_TYPE(MyApplication, my_application, GTK_TYPE_APPLICATION) + +// Implements GApplication::activate. +static void my_application_activate(GApplication* application) { + MyApplication* self = MY_APPLICATION(application); + GtkWindow* window = + GTK_WINDOW(gtk_application_window_new(GTK_APPLICATION(application))); + + // Use a header bar when running in GNOME as this is the common style used + // by applications and is the setup most users will be using (e.g. Ubuntu + // desktop). + // If running on X and not using GNOME then just use a traditional title bar + // in case the window manager does more exotic layout, e.g. tiling. + // If running on Wayland assume the header bar will work (may need changing + // if future cases occur). + gboolean use_header_bar = TRUE; +#ifdef GDK_WINDOWING_X11 + GdkScreen* screen = gtk_window_get_screen(window); + if (GDK_IS_X11_SCREEN(screen)) { + const gchar* wm_name = gdk_x11_screen_get_window_manager_name(screen); + if (g_strcmp0(wm_name, "GNOME Shell") != 0) { + use_header_bar = FALSE; + } + } +#endif + if (use_header_bar) { + GtkHeaderBar* header_bar = GTK_HEADER_BAR(gtk_header_bar_new()); + gtk_widget_show(GTK_WIDGET(header_bar)); + gtk_header_bar_set_title(header_bar, "rustdesk"); + gtk_header_bar_set_show_close_button(header_bar, TRUE); + gtk_window_set_titlebar(window, GTK_WIDGET(header_bar)); + } else { + gtk_window_set_title(window, "rustdesk"); + } + + // auto bdw = bitsdojo_window_from(window); // <--- add this line + // bdw->setCustomFrame(true); // <-- add this line + gtk_window_set_default_size(window, 1280, 720); // <-- comment this line + gtk_widget_show(GTK_WIDGET(window)); + + g_autoptr(FlDartProject) project = fl_dart_project_new(); + fl_dart_project_set_dart_entrypoint_arguments(project, self->dart_entrypoint_arguments); + + FlView* view = fl_view_new(project); + gtk_widget_show(GTK_WIDGET(view)); + gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(view)); + + fl_register_plugins(FL_PLUGIN_REGISTRY(view)); + + gtk_widget_grab_focus(GTK_WIDGET(view)); +} + +// Implements GApplication::local_command_line. +static gboolean my_application_local_command_line(GApplication* application, gchar*** arguments, int* exit_status) { + MyApplication* self = MY_APPLICATION(application); + // Strip out the first argument as it is the binary name. + self->dart_entrypoint_arguments = g_strdupv(*arguments + 1); + + g_autoptr(GError) error = nullptr; + if (!g_application_register(application, nullptr, &error)) { + g_warning("Failed to register: %s", error->message); + *exit_status = 1; + return TRUE; + } + + g_application_activate(application); + *exit_status = 0; + + return TRUE; +} + +// Implements GObject::dispose. +static void my_application_dispose(GObject* object) { + MyApplication* self = MY_APPLICATION(object); + g_clear_pointer(&self->dart_entrypoint_arguments, g_strfreev); + G_OBJECT_CLASS(my_application_parent_class)->dispose(object); +} + +static void my_application_class_init(MyApplicationClass* klass) { + G_APPLICATION_CLASS(klass)->activate = my_application_activate; + G_APPLICATION_CLASS(klass)->local_command_line = my_application_local_command_line; + G_OBJECT_CLASS(klass)->dispose = my_application_dispose; +} + +static void my_application_init(MyApplication* self) {} + +MyApplication* my_application_new() { + return MY_APPLICATION(g_object_new(my_application_get_type(), + "application-id", APPLICATION_ID, + "flags", G_APPLICATION_NON_UNIQUE, + nullptr)); +} diff --git a/flutter/linux/my_application.h b/flutter/linux/my_application.h new file mode 100644 index 000000000..72271d5e4 --- /dev/null +++ b/flutter/linux/my_application.h @@ -0,0 +1,18 @@ +#ifndef FLUTTER_MY_APPLICATION_H_ +#define FLUTTER_MY_APPLICATION_H_ + +#include + +G_DECLARE_FINAL_TYPE(MyApplication, my_application, MY, APPLICATION, + GtkApplication) + +/** + * my_application_new: + * + * Creates a new Flutter-based application. + * + * Returns: a new #MyApplication. + */ +MyApplication* my_application_new(); + +#endif // FLUTTER_MY_APPLICATION_H_ diff --git a/flutter/macos/.gitignore b/flutter/macos/.gitignore new file mode 100644 index 000000000..746adbb6b --- /dev/null +++ b/flutter/macos/.gitignore @@ -0,0 +1,7 @@ +# Flutter-related +**/Flutter/ephemeral/ +**/Pods/ + +# Xcode-related +**/dgph +**/xcuserdata/ diff --git a/flutter/macos/Flutter/Flutter-Debug.xcconfig b/flutter/macos/Flutter/Flutter-Debug.xcconfig new file mode 100644 index 000000000..4b81f9b2d --- /dev/null +++ b/flutter/macos/Flutter/Flutter-Debug.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" +#include "ephemeral/Flutter-Generated.xcconfig" diff --git a/flutter/macos/Flutter/Flutter-Release.xcconfig b/flutter/macos/Flutter/Flutter-Release.xcconfig new file mode 100644 index 000000000..5caa9d157 --- /dev/null +++ b/flutter/macos/Flutter/Flutter-Release.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" +#include "ephemeral/Flutter-Generated.xcconfig" diff --git a/flutter/macos/Podfile b/flutter/macos/Podfile new file mode 100644 index 000000000..22d9caad2 --- /dev/null +++ b/flutter/macos/Podfile @@ -0,0 +1,40 @@ +platform :osx, '10.12' + +# CocoaPods analytics sends network stats synchronously affecting flutter build latency. +ENV['COCOAPODS_DISABLE_STATS'] = 'true' + +project 'Runner', { + 'Debug' => :debug, + 'Profile' => :release, + 'Release' => :release, +} + +def flutter_root + generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'ephemeral', 'Flutter-Generated.xcconfig'), __FILE__) + unless File.exist?(generated_xcode_build_settings_path) + raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure \"flutter pub get\" is executed first" + end + + File.foreach(generated_xcode_build_settings_path) do |line| + matches = line.match(/FLUTTER_ROOT\=(.*)/) + return matches[1].strip if matches + end + raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Flutter-Generated.xcconfig, then run \"flutter pub get\"" +end + +require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) + +flutter_macos_podfile_setup + +target 'Runner' do + use_frameworks! + use_modular_headers! + + flutter_install_all_macos_pods File.dirname(File.realpath(__FILE__)) +end + +post_install do |installer| + installer.pods_project.targets.each do |target| + flutter_additional_macos_build_settings(target) + end +end diff --git a/flutter/macos/Podfile.lock b/flutter/macos/Podfile.lock new file mode 100644 index 000000000..a83616180 --- /dev/null +++ b/flutter/macos/Podfile.lock @@ -0,0 +1,196 @@ +PODS: + - bitsdojo_window_macos (0.0.1): + - FlutterMacOS + - desktop_multi_window (0.0.1): + - FlutterMacOS + - device_info_plus_macos (0.0.1): + - FlutterMacOS + - Firebase/Analytics (8.15.0): + - Firebase/Core + - Firebase/Core (8.15.0): + - Firebase/CoreOnly + - FirebaseAnalytics (~> 8.15.0) + - Firebase/CoreOnly (8.15.0): + - FirebaseCore (= 8.15.0) + - firebase_analytics (9.1.9): + - Firebase/Analytics (= 8.15.0) + - firebase_core + - FlutterMacOS + - firebase_core (1.17.1): + - Firebase/CoreOnly (~> 8.15.0) + - FlutterMacOS + - FirebaseAnalytics (8.15.0): + - FirebaseAnalytics/AdIdSupport (= 8.15.0) + - FirebaseCore (~> 8.0) + - FirebaseInstallations (~> 8.0) + - GoogleUtilities/AppDelegateSwizzler (~> 7.7) + - GoogleUtilities/MethodSwizzler (~> 7.7) + - GoogleUtilities/Network (~> 7.7) + - "GoogleUtilities/NSData+zlib (~> 7.7)" + - nanopb (~> 2.30908.0) + - FirebaseAnalytics/AdIdSupport (8.15.0): + - FirebaseCore (~> 8.0) + - FirebaseInstallations (~> 8.0) + - GoogleAppMeasurement (= 8.15.0) + - GoogleUtilities/AppDelegateSwizzler (~> 7.7) + - GoogleUtilities/MethodSwizzler (~> 7.7) + - GoogleUtilities/Network (~> 7.7) + - "GoogleUtilities/NSData+zlib (~> 7.7)" + - nanopb (~> 2.30908.0) + - FirebaseCore (8.15.0): + - FirebaseCoreDiagnostics (~> 8.0) + - GoogleUtilities/Environment (~> 7.7) + - GoogleUtilities/Logger (~> 7.7) + - FirebaseCoreDiagnostics (8.15.0): + - GoogleDataTransport (~> 9.1) + - GoogleUtilities/Environment (~> 7.7) + - GoogleUtilities/Logger (~> 7.7) + - nanopb (~> 2.30908.0) + - FirebaseInstallations (8.15.0): + - FirebaseCore (~> 8.0) + - GoogleUtilities/Environment (~> 7.7) + - GoogleUtilities/UserDefaults (~> 7.7) + - PromisesObjC (< 3.0, >= 1.2) + - FlutterMacOS (1.0.0) + - GoogleAppMeasurement (8.15.0): + - GoogleAppMeasurement/AdIdSupport (= 8.15.0) + - GoogleUtilities/AppDelegateSwizzler (~> 7.7) + - GoogleUtilities/MethodSwizzler (~> 7.7) + - GoogleUtilities/Network (~> 7.7) + - "GoogleUtilities/NSData+zlib (~> 7.7)" + - nanopb (~> 2.30908.0) + - GoogleAppMeasurement/AdIdSupport (8.15.0): + - GoogleAppMeasurement/WithoutAdIdSupport (= 8.15.0) + - GoogleUtilities/AppDelegateSwizzler (~> 7.7) + - GoogleUtilities/MethodSwizzler (~> 7.7) + - GoogleUtilities/Network (~> 7.7) + - "GoogleUtilities/NSData+zlib (~> 7.7)" + - nanopb (~> 2.30908.0) + - GoogleAppMeasurement/WithoutAdIdSupport (8.15.0): + - GoogleUtilities/AppDelegateSwizzler (~> 7.7) + - GoogleUtilities/MethodSwizzler (~> 7.7) + - GoogleUtilities/Network (~> 7.7) + - "GoogleUtilities/NSData+zlib (~> 7.7)" + - nanopb (~> 2.30908.0) + - GoogleDataTransport (9.1.4): + - GoogleUtilities/Environment (~> 7.7) + - nanopb (< 2.30910.0, >= 2.30908.0) + - PromisesObjC (< 3.0, >= 1.2) + - GoogleUtilities/AppDelegateSwizzler (7.7.0): + - GoogleUtilities/Environment + - GoogleUtilities/Logger + - GoogleUtilities/Network + - GoogleUtilities/Environment (7.7.0): + - PromisesObjC (< 3.0, >= 1.2) + - GoogleUtilities/Logger (7.7.0): + - GoogleUtilities/Environment + - GoogleUtilities/MethodSwizzler (7.7.0): + - GoogleUtilities/Logger + - GoogleUtilities/Network (7.7.0): + - GoogleUtilities/Logger + - "GoogleUtilities/NSData+zlib" + - GoogleUtilities/Reachability + - "GoogleUtilities/NSData+zlib (7.7.0)" + - GoogleUtilities/Reachability (7.7.0): + - GoogleUtilities/Logger + - GoogleUtilities/UserDefaults (7.7.0): + - GoogleUtilities/Logger + - nanopb (2.30908.0): + - nanopb/decode (= 2.30908.0) + - nanopb/encode (= 2.30908.0) + - nanopb/decode (2.30908.0) + - nanopb/encode (2.30908.0) + - package_info_plus_macos (0.0.1): + - FlutterMacOS + - path_provider_macos (0.0.1): + - FlutterMacOS + - PromisesObjC (2.1.0) + - shared_preferences_macos (0.0.1): + - FlutterMacOS + - url_launcher_macos (0.0.1): + - FlutterMacOS + - wakelock_macos (0.0.1): + - FlutterMacOS + - window_manager (0.2.0): + - FlutterMacOS + +DEPENDENCIES: + - bitsdojo_window_macos (from `Flutter/ephemeral/.symlinks/plugins/bitsdojo_window_macos/macos`) + - desktop_multi_window (from `Flutter/ephemeral/.symlinks/plugins/desktop_multi_window/macos`) + - device_info_plus_macos (from `Flutter/ephemeral/.symlinks/plugins/device_info_plus_macos/macos`) + - firebase_analytics (from `Flutter/ephemeral/.symlinks/plugins/firebase_analytics/macos`) + - firebase_core (from `Flutter/ephemeral/.symlinks/plugins/firebase_core/macos`) + - FlutterMacOS (from `Flutter/ephemeral`) + - package_info_plus_macos (from `Flutter/ephemeral/.symlinks/plugins/package_info_plus_macos/macos`) + - path_provider_macos (from `Flutter/ephemeral/.symlinks/plugins/path_provider_macos/macos`) + - shared_preferences_macos (from `Flutter/ephemeral/.symlinks/plugins/shared_preferences_macos/macos`) + - url_launcher_macos (from `Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos`) + - wakelock_macos (from `Flutter/ephemeral/.symlinks/plugins/wakelock_macos/macos`) + - window_manager (from `Flutter/ephemeral/.symlinks/plugins/window_manager/macos`) + +SPEC REPOS: + trunk: + - Firebase + - FirebaseAnalytics + - FirebaseCore + - FirebaseCoreDiagnostics + - FirebaseInstallations + - GoogleAppMeasurement + - GoogleDataTransport + - GoogleUtilities + - nanopb + - PromisesObjC + +EXTERNAL SOURCES: + bitsdojo_window_macos: + :path: Flutter/ephemeral/.symlinks/plugins/bitsdojo_window_macos/macos + desktop_multi_window: + :path: Flutter/ephemeral/.symlinks/plugins/desktop_multi_window/macos + device_info_plus_macos: + :path: Flutter/ephemeral/.symlinks/plugins/device_info_plus_macos/macos + firebase_analytics: + :path: Flutter/ephemeral/.symlinks/plugins/firebase_analytics/macos + firebase_core: + :path: Flutter/ephemeral/.symlinks/plugins/firebase_core/macos + FlutterMacOS: + :path: Flutter/ephemeral + package_info_plus_macos: + :path: Flutter/ephemeral/.symlinks/plugins/package_info_plus_macos/macos + path_provider_macos: + :path: Flutter/ephemeral/.symlinks/plugins/path_provider_macos/macos + shared_preferences_macos: + :path: Flutter/ephemeral/.symlinks/plugins/shared_preferences_macos/macos + url_launcher_macos: + :path: Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos + wakelock_macos: + :path: Flutter/ephemeral/.symlinks/plugins/wakelock_macos/macos + window_manager: + :path: Flutter/ephemeral/.symlinks/plugins/window_manager/macos + +SPEC CHECKSUMS: + bitsdojo_window_macos: 44e3b8fe3dd463820e0321f6256c5b1c16bb6a00 + desktop_multi_window: 566489c048b501134f9d7fb6a2354c60a9126486 + device_info_plus_macos: 1ad388a1ef433505c4038e7dd9605aadd1e2e9c7 + Firebase: 5f8193dff4b5b7c5d5ef72ae54bb76c08e2b841d + firebase_analytics: d448483150504ed84f25c5437a34af2591a7929e + firebase_core: 7b87364e2d1eae70018a60698e89e7d6f5320bad + FirebaseAnalytics: 7761cbadb00a717d8d0939363eb46041526474fa + FirebaseCore: 5743c5785c074a794d35f2fff7ecc254a91e08b1 + FirebaseCoreDiagnostics: 92e07a649aeb66352b319d43bdd2ee3942af84cb + FirebaseInstallations: 40bd9054049b2eae9a2c38ef1c3dd213df3605cd + FlutterMacOS: 57701585bf7de1b3fc2bb61f6378d73bbdea8424 + GoogleAppMeasurement: 4c19f031220c72464d460c9daa1fb5d1acce958e + GoogleDataTransport: 5fffe35792f8b96ec8d6775f5eccd83c998d5a3b + GoogleUtilities: e0913149f6b0625b553d70dae12b49fc62914fd1 + nanopb: a0ba3315591a9ae0a16a309ee504766e90db0c96 + package_info_plus_macos: f010621b07802a241d96d01876d6705f15e77c1c + path_provider_macos: 3c0c3b4b0d4a76d2bf989a913c2de869c5641a19 + PromisesObjC: 99b6f43f9e1044bd87a95a60beff28c2c44ddb72 + shared_preferences_macos: a64dc611287ed6cbe28fd1297898db1336975727 + url_launcher_macos: 597e05b8e514239626bcf4a850fcf9ef5c856ec3 + wakelock_macos: bc3f2a9bd8d2e6c89fee1e1822e7ddac3bd004a9 + window_manager: 3a1844359a6295ab1e47659b1a777e36773cd6e8 + +PODFILE CHECKSUM: c7161fcf45d4fd9025dc0f48a76d6e64e52f8176 + +COCOAPODS: 1.11.3 diff --git a/flutter/macos/Runner.xcodeproj/project.pbxproj b/flutter/macos/Runner.xcodeproj/project.pbxproj new file mode 100644 index 000000000..23549954b --- /dev/null +++ b/flutter/macos/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,730 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 51; + objects = { + +/* Begin PBXAggregateTarget section */ + 33CC111A2044C6BA0003C045 /* Flutter Assemble */ = { + isa = PBXAggregateTarget; + buildConfigurationList = 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */; + buildPhases = ( + 33CC111E2044C6BF0003C045 /* ShellScript */, + ); + dependencies = ( + ); + name = "Flutter Assemble"; + productName = FLX; + }; +/* End PBXAggregateTarget section */ + +/* Begin PBXBuildFile section */ + 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */; }; + 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC10F02044A3C60003C045 /* AppDelegate.swift */; }; + 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; }; + 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; }; + 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; }; + C5E54335B73C89F72DB1B606 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 26C84465887F29AE938039CB /* Pods_Runner.framework */; }; + CC13D44B2847D53E00EF8B54 /* librustdesk.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = CC13D4362847C8C200EF8B54 /* librustdesk.dylib */; }; + CC13D4502847D5E800EF8B54 /* librustdesk.dylib in Bundle Framework */ = {isa = PBXBuildFile; fileRef = CC13D4362847C8C200EF8B54 /* librustdesk.dylib */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 33CC10E52044A3C60003C045 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 33CC111A2044C6BA0003C045; + remoteInfo = FLX; + }; + CC13D4352847C8C200EF8B54 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = CC13D42E2847C8C200EF8B54 /* rustdesk.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = CA6071B5A0F5A7A3EF2297AA; + remoteInfo = "librustdesk-cdylib"; + }; + CC13D4372847C8C200EF8B54 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = CC13D42E2847C8C200EF8B54 /* rustdesk.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = CA604C7415FB2A3731F5016A; + remoteInfo = "librustdesk-staticlib"; + }; + CC13D4392847C8C200EF8B54 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = CC13D42E2847C8C200EF8B54 /* rustdesk.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = CA60D3BC5386D3D7DBD96893; + remoteInfo = "naming-bin"; + }; + CC13D43B2847C8C200EF8B54 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = CC13D42E2847C8C200EF8B54 /* rustdesk.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = CA60D3BC5386B357B2AB834F; + remoteInfo = "rustdesk-bin"; + }; + CC13D43D2847C8CB00EF8B54 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = CC13D42E2847C8C200EF8B54 /* rustdesk.xcodeproj */; + proxyType = 1; + remoteGlobalIDString = CA6071B5A0F5D6691E4C3FF1; + remoteInfo = "librustdesk-cdylib"; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 33CC110E2044A8840003C045 /* Bundle Framework */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + CC13D4502847D5E800EF8B54 /* librustdesk.dylib in Bundle Framework */, + ); + name = "Bundle Framework"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 26C84465887F29AE938039CB /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 295AD07E63F13855C270A0E0 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; + 333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = ""; }; + 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = ""; }; + 33CC10ED2044A3C60003C045 /* flutter_hbb.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = flutter_hbb.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 33CC10F02044A3C60003C045 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = ""; }; + 33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = ""; }; + 33CC10F72044A3C60003C045 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = Info.plist; path = Runner/Info.plist; sourceTree = ""; }; + 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainFlutterWindow.swift; sourceTree = ""; }; + 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Debug.xcconfig"; sourceTree = ""; }; + 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Release.xcconfig"; sourceTree = ""; }; + 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = "Flutter-Generated.xcconfig"; path = "ephemeral/Flutter-Generated.xcconfig"; sourceTree = ""; }; + 33E51913231747F40026EE4D /* DebugProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DebugProfile.entitlements; sourceTree = ""; }; + 33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = ""; }; + 33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = ""; }; + 7436B85D94E8F7B5A9324869 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = ""; }; + 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = ""; }; + C3BB669FF6190AE1B11BCAEA /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; + CC13D42E2847C8C200EF8B54 /* rustdesk.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; path = rustdesk.xcodeproj; sourceTree = SOURCE_ROOT; }; + CCB6FE9A2848A6B800E58D48 /* bridge_generated.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = bridge_generated.h; path = Runner/bridge_generated.h; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 33CC10EA2044A3C60003C045 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + CC13D44B2847D53E00EF8B54 /* librustdesk.dylib in Frameworks */, + C5E54335B73C89F72DB1B606 /* Pods_Runner.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 33BA886A226E78AF003329D5 /* Configs */ = { + isa = PBXGroup; + children = ( + 33E5194F232828860026EE4D /* AppInfo.xcconfig */, + 9740EEB21CF90195004384FC /* Debug.xcconfig */, + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, + 333000ED22D3DE5D00554162 /* Warnings.xcconfig */, + ); + path = Configs; + sourceTree = ""; + }; + 33CC10E42044A3C60003C045 = { + isa = PBXGroup; + children = ( + CCB6FE9A2848A6B800E58D48 /* bridge_generated.h */, + 33FAB671232836740065AC1E /* Runner */, + 33CEB47122A05771004F2AC0 /* Flutter */, + 33CC10EE2044A3C60003C045 /* Products */, + D73912EC22F37F3D000D13A0 /* Frameworks */, + A6C450E1C32EC39A23170131 /* Pods */, + ); + sourceTree = ""; + }; + 33CC10EE2044A3C60003C045 /* Products */ = { + isa = PBXGroup; + children = ( + 33CC10ED2044A3C60003C045 /* flutter_hbb.app */, + ); + name = Products; + sourceTree = ""; + }; + 33CC11242044D66E0003C045 /* Resources */ = { + isa = PBXGroup; + children = ( + 33CC10F22044A3C60003C045 /* Assets.xcassets */, + 33CC10F42044A3C60003C045 /* MainMenu.xib */, + 33CC10F72044A3C60003C045 /* Info.plist */, + ); + name = Resources; + path = ..; + sourceTree = ""; + }; + 33CEB47122A05771004F2AC0 /* Flutter */ = { + isa = PBXGroup; + children = ( + 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */, + 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */, + 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */, + 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */, + ); + path = Flutter; + sourceTree = ""; + }; + 33FAB671232836740065AC1E /* Runner */ = { + isa = PBXGroup; + children = ( + CC13D42E2847C8C200EF8B54 /* rustdesk.xcodeproj */, + 33CC10F02044A3C60003C045 /* AppDelegate.swift */, + 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */, + 33E51913231747F40026EE4D /* DebugProfile.entitlements */, + 33E51914231749380026EE4D /* Release.entitlements */, + 33CC11242044D66E0003C045 /* Resources */, + 33BA886A226E78AF003329D5 /* Configs */, + ); + path = Runner; + sourceTree = ""; + }; + A6C450E1C32EC39A23170131 /* Pods */ = { + isa = PBXGroup; + children = ( + 7436B85D94E8F7B5A9324869 /* Pods-Runner.debug.xcconfig */, + 295AD07E63F13855C270A0E0 /* Pods-Runner.release.xcconfig */, + C3BB669FF6190AE1B11BCAEA /* Pods-Runner.profile.xcconfig */, + ); + path = Pods; + sourceTree = ""; + }; + CC13D42F2847C8C200EF8B54 /* Products */ = { + isa = PBXGroup; + children = ( + CC13D4362847C8C200EF8B54 /* librustdesk.dylib */, + CC13D4382847C8C200EF8B54 /* liblibrustdesk_static.a */, + CC13D43A2847C8C200EF8B54 /* naming */, + CC13D43C2847C8C200EF8B54 /* rustdesk */, + ); + name = Products; + sourceTree = ""; + }; + D73912EC22F37F3D000D13A0 /* Frameworks */ = { + isa = PBXGroup; + children = ( + 26C84465887F29AE938039CB /* Pods_Runner.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 33CC10EC2044A3C60003C045 /* Runner */ = { + isa = PBXNativeTarget; + buildConfigurationList = 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */; + buildPhases = ( + 610B125EE2B990E4D4B30D05 /* [CP] Check Pods Manifest.lock */, + 33CC10E92044A3C60003C045 /* Sources */, + 33CC10EA2044A3C60003C045 /* Frameworks */, + 33CC10EB2044A3C60003C045 /* Resources */, + 33CC110E2044A8840003C045 /* Bundle Framework */, + 3399D490228B24CF009A79C7 /* ShellScript */, + 4688A20DD8E4F3E900927B2C /* [CP] Embed Pods Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + CC13D43E2847C8CB00EF8B54 /* PBXTargetDependency */, + 33CC11202044C79F0003C045 /* PBXTargetDependency */, + ); + name = Runner; + productName = Runner; + productReference = 33CC10ED2044A3C60003C045 /* flutter_hbb.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 33CC10E52044A3C60003C045 /* Project object */ = { + isa = PBXProject; + attributes = { + LastSwiftUpdateCheck = 0920; + LastUpgradeCheck = 1300; + ORGANIZATIONNAME = ""; + TargetAttributes = { + 33CC10EC2044A3C60003C045 = { + CreatedOnToolsVersion = 9.2; + LastSwiftMigration = 1100; + ProvisioningStyle = Automatic; + SystemCapabilities = { + com.apple.Sandbox = { + enabled = 1; + }; + }; + }; + 33CC111A2044C6BA0003C045 = { + CreatedOnToolsVersion = 9.2; + ProvisioningStyle = Manual; + }; + }; + }; + buildConfigurationList = 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 33CC10E42044A3C60003C045; + productRefGroup = 33CC10EE2044A3C60003C045 /* Products */; + projectDirPath = ""; + projectReferences = ( + { + ProductGroup = CC13D42F2847C8C200EF8B54 /* Products */; + ProjectRef = CC13D42E2847C8C200EF8B54 /* rustdesk.xcodeproj */; + }, + ); + projectRoot = ""; + targets = ( + 33CC10EC2044A3C60003C045 /* Runner */, + 33CC111A2044C6BA0003C045 /* Flutter Assemble */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXReferenceProxy section */ + CC13D4362847C8C200EF8B54 /* librustdesk.dylib */ = { + isa = PBXReferenceProxy; + fileType = "compiled.mach-o.dylib"; + path = librustdesk.dylib; + remoteRef = CC13D4352847C8C200EF8B54 /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; + CC13D4382847C8C200EF8B54 /* liblibrustdesk_static.a */ = { + isa = PBXReferenceProxy; + fileType = archive.ar; + path = liblibrustdesk_static.a; + remoteRef = CC13D4372847C8C200EF8B54 /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; + CC13D43A2847C8C200EF8B54 /* naming */ = { + isa = PBXReferenceProxy; + fileType = "compiled.mach-o.executable"; + path = naming; + remoteRef = CC13D4392847C8C200EF8B54 /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; + CC13D43C2847C8C200EF8B54 /* rustdesk */ = { + isa = PBXReferenceProxy; + fileType = "compiled.mach-o.executable"; + path = rustdesk; + remoteRef = CC13D43B2847C8C200EF8B54 /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; +/* End PBXReferenceProxy section */ + +/* Begin PBXResourcesBuildPhase section */ + 33CC10EB2044A3C60003C045 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */, + 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 3399D490228B24CF009A79C7 /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "echo \"$PRODUCT_NAME.app\" > \"$PROJECT_DIR\"/Flutter/ephemeral/.app_filename && \"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh embed\n"; + }; + 33CC111E2044C6BF0003C045 /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + Flutter/ephemeral/FlutterInputs.xcfilelist, + ); + inputPaths = ( + Flutter/ephemeral/tripwire, + ); + outputFileListPaths = ( + Flutter/ephemeral/FlutterOutputs.xcfilelist, + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh && touch Flutter/ephemeral/tripwire"; + }; + 4688A20DD8E4F3E900927B2C /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; + 610B125EE2B990E4D4B30D05 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 33CC10E92044A3C60003C045 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */, + 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */, + 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 33CC11202044C79F0003C045 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 33CC111A2044C6BA0003C045 /* Flutter Assemble */; + targetProxy = 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */; + }; + CC13D43E2847C8CB00EF8B54 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + name = "librustdesk-cdylib"; + targetProxy = CC13D43D2847C8CB00EF8B54 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + 33CC10F42044A3C60003C045 /* MainMenu.xib */ = { + isa = PBXVariantGroup; + children = ( + 33CC10F52044A3C60003C045 /* Base */, + ); + name = MainMenu.xib; + path = Runner; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 338D0CE9231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.11; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + }; + name = Profile; + }; + 338D0CEA231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_VERSION = 5.0; + }; + name = Profile; + }; + 338D0CEB231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Manual; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Profile; + }; + 33CC10F92044A3C60003C045 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.11; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = macosx; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + 33CC10FA2044A3C60003C045 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.11; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + }; + name = Release; + }; + 33CC10FC2044A3C60003C045 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + "SWIFT_OBJC_BRIDGING_HEADER[arch=*]" = Runner/bridge_generated.h; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + }; + name = Debug; + }; + 33CC10FD2044A3C60003C045 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/Release.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + "SWIFT_OBJC_BRIDGING_HEADER[arch=*]" = Runner/bridge_generated.h; + SWIFT_VERSION = 5.0; + }; + name = Release; + }; + 33CC111C2044C6BA0003C045 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Manual; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Debug; + }; + 33CC111D2044C6BA0003C045 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC10F92044A3C60003C045 /* Debug */, + 33CC10FA2044A3C60003C045 /* Release */, + 338D0CE9231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC10FC2044A3C60003C045 /* Debug */, + 33CC10FD2044A3C60003C045 /* Release */, + 338D0CEA231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC111C2044C6BA0003C045 /* Debug */, + 33CC111D2044C6BA0003C045 /* Release */, + 338D0CEB231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 33CC10E52044A3C60003C045 /* Project object */; +} diff --git a/flutter/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/flutter/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 000000000..18d981003 --- /dev/null +++ b/flutter/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/flutter/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/flutter/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 000000000..85831efcf --- /dev/null +++ b/flutter/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,87 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/flutter/macos/Runner.xcworkspace/contents.xcworkspacedata b/flutter/macos/Runner.xcworkspace/contents.xcworkspacedata new file mode 100644 index 000000000..21a3cc14c --- /dev/null +++ b/flutter/macos/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + + diff --git a/flutter/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/flutter/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 000000000..18d981003 --- /dev/null +++ b/flutter/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/flutter/macos/Runner/AppDelegate.swift b/flutter/macos/Runner/AppDelegate.swift new file mode 100644 index 000000000..156e0c79b --- /dev/null +++ b/flutter/macos/Runner/AppDelegate.swift @@ -0,0 +1,10 @@ +import Cocoa +import FlutterMacOS + +@NSApplicationMain +class AppDelegate: FlutterAppDelegate { + override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { + dummy_method_to_enforce_bundling() + return true + } +} diff --git a/flutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/flutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 000000000..a2ec33f19 --- /dev/null +++ b/flutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,68 @@ +{ + "images" : [ + { + "size" : "16x16", + "idiom" : "mac", + "filename" : "app_icon_16.png", + "scale" : "1x" + }, + { + "size" : "16x16", + "idiom" : "mac", + "filename" : "app_icon_32.png", + "scale" : "2x" + }, + { + "size" : "32x32", + "idiom" : "mac", + "filename" : "app_icon_32.png", + "scale" : "1x" + }, + { + "size" : "32x32", + "idiom" : "mac", + "filename" : "app_icon_64.png", + "scale" : "2x" + }, + { + "size" : "128x128", + "idiom" : "mac", + "filename" : "app_icon_128.png", + "scale" : "1x" + }, + { + "size" : "128x128", + "idiom" : "mac", + "filename" : "app_icon_256.png", + "scale" : "2x" + }, + { + "size" : "256x256", + "idiom" : "mac", + "filename" : "app_icon_256.png", + "scale" : "1x" + }, + { + "size" : "256x256", + "idiom" : "mac", + "filename" : "app_icon_512.png", + "scale" : "2x" + }, + { + "size" : "512x512", + "idiom" : "mac", + "filename" : "app_icon_512.png", + "scale" : "1x" + }, + { + "size" : "512x512", + "idiom" : "mac", + "filename" : "app_icon_1024.png", + "scale" : "2x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/flutter/macos/Runner/Base.lproj/MainMenu.xib b/flutter/macos/Runner/Base.lproj/MainMenu.xib new file mode 100644 index 000000000..80e867a4e --- /dev/null +++ b/flutter/macos/Runner/Base.lproj/MainMenu.xib @@ -0,0 +1,343 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/flutter/macos/Runner/Configs/AppInfo.xcconfig b/flutter/macos/Runner/Configs/AppInfo.xcconfig new file mode 100644 index 000000000..3c862dee9 --- /dev/null +++ b/flutter/macos/Runner/Configs/AppInfo.xcconfig @@ -0,0 +1,14 @@ +// Application-level settings for the Runner target. +// +// This may be replaced with something auto-generated from metadata (e.g., pubspec.yaml) in the +// future. If not, the values below would default to using the project name when this becomes a +// 'flutter create' template. + +// The application's name. By default this is also the title of the Flutter window. +PRODUCT_NAME = flutter_hbb + +// The application's bundle identifier +PRODUCT_BUNDLE_IDENTIFIER = com.carriez.flutterHbb + +// The copyright displayed in application information +PRODUCT_COPYRIGHT = Copyright © 2022 com.carriez. All rights reserved. diff --git a/flutter/macos/Runner/Configs/Debug.xcconfig b/flutter/macos/Runner/Configs/Debug.xcconfig new file mode 100644 index 000000000..36b0fd946 --- /dev/null +++ b/flutter/macos/Runner/Configs/Debug.xcconfig @@ -0,0 +1,2 @@ +#include "../../Flutter/Flutter-Debug.xcconfig" +#include "Warnings.xcconfig" diff --git a/flutter/macos/Runner/Configs/Release.xcconfig b/flutter/macos/Runner/Configs/Release.xcconfig new file mode 100644 index 000000000..dff4f4956 --- /dev/null +++ b/flutter/macos/Runner/Configs/Release.xcconfig @@ -0,0 +1,2 @@ +#include "../../Flutter/Flutter-Release.xcconfig" +#include "Warnings.xcconfig" diff --git a/flutter/macos/Runner/Configs/Warnings.xcconfig b/flutter/macos/Runner/Configs/Warnings.xcconfig new file mode 100644 index 000000000..42bcbf478 --- /dev/null +++ b/flutter/macos/Runner/Configs/Warnings.xcconfig @@ -0,0 +1,13 @@ +WARNING_CFLAGS = -Wall -Wconditional-uninitialized -Wnullable-to-nonnull-conversion -Wmissing-method-return-type -Woverlength-strings +GCC_WARN_UNDECLARED_SELECTOR = YES +CLANG_UNDEFINED_BEHAVIOR_SANITIZER_NULLABILITY = YES +CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE +CLANG_WARN__DUPLICATE_METHOD_MATCH = YES +CLANG_WARN_PRAGMA_PACK = YES +CLANG_WARN_STRICT_PROTOTYPES = YES +CLANG_WARN_COMMA = YES +GCC_WARN_STRICT_SELECTOR_MATCH = YES +CLANG_WARN_OBJC_REPEATED_USE_OF_WEAK = YES +CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES +GCC_WARN_SHADOW = YES +CLANG_WARN_UNREACHABLE_CODE = YES diff --git a/flutter/macos/Runner/DebugProfile.entitlements b/flutter/macos/Runner/DebugProfile.entitlements new file mode 100644 index 000000000..dddb8a30c --- /dev/null +++ b/flutter/macos/Runner/DebugProfile.entitlements @@ -0,0 +1,12 @@ + + + + + com.apple.security.app-sandbox + + com.apple.security.cs.allow-jit + + com.apple.security.network.server + + + diff --git a/flutter/macos/Runner/Info.plist b/flutter/macos/Runner/Info.plist new file mode 100644 index 000000000..4789daa6a --- /dev/null +++ b/flutter/macos/Runner/Info.plist @@ -0,0 +1,32 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIconFile + + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + LSMinimumSystemVersion + $(MACOSX_DEPLOYMENT_TARGET) + NSHumanReadableCopyright + $(PRODUCT_COPYRIGHT) + NSMainNibFile + MainMenu + NSPrincipalClass + NSApplication + + diff --git a/flutter/macos/Runner/MainFlutterWindow.swift b/flutter/macos/Runner/MainFlutterWindow.swift new file mode 100644 index 000000000..688292371 --- /dev/null +++ b/flutter/macos/Runner/MainFlutterWindow.swift @@ -0,0 +1,24 @@ +import Cocoa +import FlutterMacOS +// import bitsdojo_window_macos + +class MainFlutterWindow: NSWindow { + override func awakeFromNib() { + if (!rustdesk_core_main()){ + print("Rustdesk core returns false, exiting without launching Flutter app") + NSApplication.shared.terminate(self) + } + let flutterViewController = FlutterViewController.init() + let windowFrame = self.frame + self.contentViewController = flutterViewController + self.setFrame(windowFrame, display: true) + + RegisterGeneratedPlugins(registry: flutterViewController) + + super.awakeFromNib() + } + +// override func bitsdojo_window_configure() -> UInt { +// return BDW_CUSTOM_FRAME | BDW_HIDE_ON_STARTUP +// } +} diff --git a/flutter/macos/Runner/Release.entitlements b/flutter/macos/Runner/Release.entitlements new file mode 100644 index 000000000..852fa1a47 --- /dev/null +++ b/flutter/macos/Runner/Release.entitlements @@ -0,0 +1,8 @@ + + + + + com.apple.security.app-sandbox + + + diff --git a/flutter/macos/rustdesk.xcodeproj/project.pbxproj b/flutter/macos/rustdesk.xcodeproj/project.pbxproj new file mode 100644 index 000000000..bed41ae67 --- /dev/null +++ b/flutter/macos/rustdesk.xcodeproj/project.pbxproj @@ -0,0 +1,439 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 53; + objects = { + +/* Begin PBXBuildFile section */ + CA6061C6409F12977AAB839F /* Cargo.toml in Sources */ = {isa = PBXBuildFile; fileRef = CA603C4309E13EF4668187A5 /* Cargo.toml */; settings = {COMPILER_FLAGS = "--lib"; }; }; + CA6061C6409FC858B7409EE3 /* Cargo.toml in Sources */ = {isa = PBXBuildFile; fileRef = CA603C4309E13EF4668187A5 /* Cargo.toml */; settings = {COMPILER_FLAGS = "--bin naming"; }; }; + CA6061C6409FC9FA710A2219 /* Cargo.toml in Sources */ = {isa = PBXBuildFile; fileRef = CA603C4309E13EF4668187A5 /* Cargo.toml */; settings = {COMPILER_FLAGS = "--bin rustdesk"; }; }; + CA6061C6409FD6691E4C3FF1 /* Cargo.toml in Sources */ = {isa = PBXBuildFile; fileRef = CA603C4309E13EF4668187A5 /* Cargo.toml */; settings = {COMPILER_FLAGS = "--lib"; }; }; +/* End PBXBuildFile section */ + +/* Begin PBXBuildRule section */ + CA603C4309E1AC6C1400ACA8 /* PBXBuildRule */ = { + isa = PBXBuildRule; + compilerSpec = com.apple.compilers.proxy.script; + dependencyFile = "$(DERIVED_FILE_DIR)/$(CARGO_XCODE_TARGET_ARCH)-$(EXECUTABLE_NAME).d"; + filePatterns = "*/Cargo.toml"; + fileType = pattern.proxy; + inputFiles = ( + ); + isEditable = 0; + name = "Cargo project build"; + outputFiles = ( + "$(OBJECT_FILE_DIR)/$(CARGO_XCODE_TARGET_ARCH)-$(EXECUTABLE_NAME)", + ); + script = "# generated with cargo-xcode 1.4.1\n\nset -eu; export PATH=$PATH:~/.cargo/bin:/usr/local/bin;\nif [ \"${IS_MACCATALYST-NO}\" = YES ]; then\n CARGO_XCODE_TARGET_TRIPLE=\"${CARGO_XCODE_TARGET_ARCH}-apple-ios-macabi\"\nelse\n CARGO_XCODE_TARGET_TRIPLE=\"${CARGO_XCODE_TARGET_ARCH}-apple-${CARGO_XCODE_TARGET_OS}\"\nfi\nif [ \"$CARGO_XCODE_TARGET_OS\" != \"darwin\" ]; then\n PATH=\"${PATH/\\/Contents\\/Developer\\/Toolchains\\/XcodeDefault.xctoolchain\\/usr\\/bin:/xcode-provided-ld-cant-link-lSystem-for-the-host-build-script:}\"\nfi\nPATH=\"$PATH:/opt/homebrew/bin\" # Rust projects often depend on extra tools like nasm, which Xcode lacks\nif [ \"$CARGO_XCODE_BUILD_MODE\" == release ]; then\n OTHER_INPUT_FILE_FLAGS=\"${OTHER_INPUT_FILE_FLAGS} --release\"\nfi\nif command -v rustup &> /dev/null; then\n if ! rustup target list --installed | egrep -q \"${CARGO_XCODE_TARGET_TRIPLE}\"; then\n echo \"warning: this build requires rustup toolchain for $CARGO_XCODE_TARGET_TRIPLE, but it isn't installed\"\n rustup target add \"${CARGO_XCODE_TARGET_TRIPLE}\" || echo >&2 \"warning: can't install $CARGO_XCODE_TARGET_TRIPLE\"\n fi\nfi\nif [ \"$ACTION\" = clean ]; then\n ( set -x; cargo clean --manifest-path=\"$SCRIPT_INPUT_FILE\" ${OTHER_INPUT_FILE_FLAGS} --target=\"${CARGO_XCODE_TARGET_TRIPLE}\"; );\nelse\n ( set -x; cargo build --manifest-path=\"$SCRIPT_INPUT_FILE\" --features=\"${CARGO_XCODE_FEATURES:-}\" ${OTHER_INPUT_FILE_FLAGS} --target=\"${CARGO_XCODE_TARGET_TRIPLE}\"; );\nfi\n# it's too hard to explain Cargo's actual exe path to Xcode build graph, so hardlink to a known-good path instead\nBUILT_SRC=\"${CARGO_TARGET_DIR}/${CARGO_XCODE_TARGET_TRIPLE}/${CARGO_XCODE_BUILD_MODE}/${CARGO_XCODE_CARGO_FILE_NAME}\"\nln -f -- \"$BUILT_SRC\" \"$SCRIPT_OUTPUT_FILE_0\"\n\n# xcode generates dep file, but for its own path, so append our rename to it\nDEP_FILE_SRC=\"${CARGO_TARGET_DIR}/${CARGO_XCODE_TARGET_TRIPLE}/${CARGO_XCODE_BUILD_MODE}/${CARGO_XCODE_CARGO_DEP_FILE_NAME}\"\nif [ -f \"$DEP_FILE_SRC\" ]; then\n DEP_FILE_DST=\"${DERIVED_FILE_DIR}/${CARGO_XCODE_TARGET_ARCH}-${EXECUTABLE_NAME}.d\"\n cp -f \"$DEP_FILE_SRC\" \"$DEP_FILE_DST\"\n echo >> \"$DEP_FILE_DST\" \"$SCRIPT_OUTPUT_FILE_0: $BUILT_SRC\"\nfi\n\n# lipo script needs to know all the platform-specific files that have been built\n# archs is in the file name, so that paths don't stay around after archs change\n# must match input for LipoScript\nFILE_LIST=\"${DERIVED_FILE_DIR}/${ARCHS}-${EXECUTABLE_NAME}.xcfilelist\"\ntouch \"$FILE_LIST\"\nif ! egrep -q \"$SCRIPT_OUTPUT_FILE_0\" \"$FILE_LIST\" ; then\n echo >> \"$FILE_LIST\" \"$SCRIPT_OUTPUT_FILE_0\"\nfi\n"; + }; +/* End PBXBuildRule section */ + +/* Begin PBXFileReference section */ + ADDEDBA66A6E1 /* libresolv.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libresolv.tbd; path = usr/lib/libresolv.tbd; sourceTree = SDKROOT; }; + CA603C4309E13EF4668187A5 /* Cargo.toml */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; name = Cargo.toml; path = /Users/ruizruiz/Work/Code/Projects/RustDesk/rustdesk/Cargo.toml; sourceTree = ""; }; + CA604C7415FB2A3731F5016A /* liblibrustdesk_static.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = liblibrustdesk_static.a; sourceTree = BUILT_PRODUCTS_DIR; }; + CA6071B5A0F5A7A3EF2297AA /* librustdesk.dylib */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.dylib"; includeInIndex = 0; path = librustdesk.dylib; sourceTree = BUILT_PRODUCTS_DIR; }; + CA60D3BC5386B357B2AB834F /* rustdesk */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.executable"; includeInIndex = 0; path = rustdesk; sourceTree = BUILT_PRODUCTS_DIR; }; + CA60D3BC5386D3D7DBD96893 /* naming */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.executable"; includeInIndex = 0; path = naming; sourceTree = BUILT_PRODUCTS_DIR; }; +/* End PBXFileReference section */ + +/* Begin PBXGroup section */ + ADDEDBA66A6E2 /* Required for static linking */ = { + isa = PBXGroup; + children = ( + ADDEDBA66A6E1 /* libresolv.tbd */, + ); + name = "Required for static linking"; + sourceTree = ""; + }; + CA603C4309E122869D176AE5 /* Products */ = { + isa = PBXGroup; + children = ( + CA6071B5A0F5A7A3EF2297AA /* librustdesk.dylib */, + CA604C7415FB2A3731F5016A /* liblibrustdesk_static.a */, + CA60D3BC5386D3D7DBD96893 /* naming */, + CA60D3BC5386B357B2AB834F /* rustdesk */, + ); + name = Products; + sourceTree = ""; + }; + CA603C4309E198AF0B5890DB /* Frameworks */ = { + isa = PBXGroup; + children = ( + ADDEDBA66A6E2 /* Required for static linking */, + ); + name = Frameworks; + sourceTree = ""; + }; + CA603C4309E1D65BC3C892A8 = { + isa = PBXGroup; + children = ( + CA603C4309E13EF4668187A5 /* Cargo.toml */, + CA603C4309E122869D176AE5 /* Products */, + CA603C4309E198AF0B5890DB /* Frameworks */, + ); + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + CA604C7415FB12977AAB839F /* librustdesk-staticlib */ = { + isa = PBXNativeTarget; + buildConfigurationList = CA6028B9540B12977AAB839F /* Build configuration list for PBXNativeTarget "librustdesk-staticlib" */; + buildPhases = ( + CA6033723F8212977AAB839F /* Sources */, + CA603C4309E1AF6EBB7F357C /* Universal Binary lipo */, + ); + buildRules = ( + CA603C4309E1AC6C1400ACA8 /* PBXBuildRule */, + ); + dependencies = ( + ); + name = "librustdesk-staticlib"; + productName = liblibrustdesk_static.a; + productReference = CA604C7415FB2A3731F5016A /* liblibrustdesk_static.a */; + productType = "com.apple.product-type.library.static"; + }; + CA6071B5A0F5D6691E4C3FF1 /* librustdesk-cdylib */ = { + isa = PBXNativeTarget; + buildConfigurationList = CA6028B9540BD6691E4C3FF1 /* Build configuration list for PBXNativeTarget "librustdesk-cdylib" */; + buildPhases = ( + CA6033723F82D6691E4C3FF1 /* Sources */, + CA603C4309E1AF6EBB7F357C /* Universal Binary lipo */, + ); + buildRules = ( + CA603C4309E1AC6C1400ACA8 /* PBXBuildRule */, + ); + dependencies = ( + ); + name = "librustdesk-cdylib"; + productName = librustdesk.dylib; + productReference = CA6071B5A0F5A7A3EF2297AA /* librustdesk.dylib */; + productType = "com.apple.product-type.library.dynamic"; + }; + CA60D3BC5386C858B7409EE3 /* naming-bin */ = { + isa = PBXNativeTarget; + buildConfigurationList = CA6028B9540BC858B7409EE3 /* Build configuration list for PBXNativeTarget "naming-bin" */; + buildPhases = ( + CA6033723F82C858B7409EE3 /* Sources */, + CA603C4309E1AF6EBB7F357C /* Universal Binary lipo */, + ); + buildRules = ( + CA603C4309E1AC6C1400ACA8 /* PBXBuildRule */, + ); + dependencies = ( + ); + name = "naming-bin"; + productName = naming; + productReference = CA60D3BC5386D3D7DBD96893 /* naming */; + productType = "com.apple.product-type.tool"; + }; + CA60D3BC5386C9FA710A2219 /* rustdesk-bin */ = { + isa = PBXNativeTarget; + buildConfigurationList = CA6028B9540BC9FA710A2219 /* Build configuration list for PBXNativeTarget "rustdesk-bin" */; + buildPhases = ( + CA6033723F82C9FA710A2219 /* Sources */, + CA603C4309E1AF6EBB7F357C /* Universal Binary lipo */, + ); + buildRules = ( + CA603C4309E1AC6C1400ACA8 /* PBXBuildRule */, + ); + dependencies = ( + ); + name = "rustdesk-bin"; + productName = rustdesk; + productReference = CA60D3BC5386B357B2AB834F /* rustdesk */; + productType = "com.apple.product-type.tool"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + CA603C4309E1E04653AD465F /* Project object */ = { + isa = PBXProject; + attributes = { + LastUpgradeCheck = 1300; + TargetAttributes = { + CA604C7415FB12977AAB839F = { + CreatedOnToolsVersion = 9.2; + ProvisioningStyle = Automatic; + }; + CA6071B5A0F5D6691E4C3FF1 = { + CreatedOnToolsVersion = 9.2; + ProvisioningStyle = Automatic; + }; + CA60D3BC5386C858B7409EE3 = { + CreatedOnToolsVersion = 9.2; + ProvisioningStyle = Automatic; + }; + CA60D3BC5386C9FA710A2219 = { + CreatedOnToolsVersion = 9.2; + ProvisioningStyle = Automatic; + }; + }; + }; + buildConfigurationList = CA603C4309E180E02D6C7F57 /* Build configuration list for PBXProject "rustdesk" */; + compatibilityVersion = "Xcode 11.4"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = CA603C4309E1D65BC3C892A8; + productRefGroup = CA603C4309E122869D176AE5 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + CA6071B5A0F5D6691E4C3FF1 /* librustdesk-cdylib */, + CA604C7415FB12977AAB839F /* librustdesk-staticlib */, + CA60D3BC5386C858B7409EE3 /* naming-bin */, + CA60D3BC5386C9FA710A2219 /* rustdesk-bin */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXShellScriptBuildPhase section */ + CA603C4309E1AF6EBB7F357C /* Universal Binary lipo */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "$(DERIVED_FILE_DIR)/$(ARCHS)-$(EXECUTABLE_NAME).xcfilelist", + ); + name = "Universal Binary lipo"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(TARGET_BUILD_DIR)/$(EXECUTABLE_PATH)", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "# generated with cargo-xcode 1.4.1\nset -eux; cat \"$DERIVED_FILE_DIR/$ARCHS-$EXECUTABLE_NAME.xcfilelist\" | tr '\\n' '\\0' | xargs -0 lipo -create -output \"$TARGET_BUILD_DIR/$EXECUTABLE_PATH\""; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + CA6033723F8212977AAB839F /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + CA6061C6409F12977AAB839F /* Cargo.toml in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + CA6033723F82C858B7409EE3 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + CA6061C6409FC858B7409EE3 /* Cargo.toml in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + CA6033723F82C9FA710A2219 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + CA6061C6409FC9FA710A2219 /* Cargo.toml in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + CA6033723F82D6691E4C3FF1 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + CA6061C6409FD6691E4C3FF1 /* Cargo.toml in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + CA604B55B26012977AAB839F /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CARGO_XCODE_CARGO_DEP_FILE_NAME = liblibrustdesk.d; + CARGO_XCODE_CARGO_FILE_NAME = liblibrustdesk.a; + INSTALL_GROUP = ""; + INSTALL_MODE_FLAG = ""; + INSTALL_OWNER = ""; + PRODUCT_NAME = librustdesk_static; + SKIP_INSTALL = YES; + SUPPORTED_PLATFORMS = "macosx iphonesimulator iphoneos appletvsimulator appletvos"; + }; + name = Debug; + }; + CA604B55B260C858B7409EE3 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CARGO_XCODE_CARGO_DEP_FILE_NAME = naming.d; + CARGO_XCODE_CARGO_FILE_NAME = naming; + PRODUCT_NAME = naming; + SUPPORTED_PLATFORMS = macosx; + }; + name = Debug; + }; + CA604B55B260C9FA710A2219 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CARGO_XCODE_CARGO_DEP_FILE_NAME = rustdesk.d; + CARGO_XCODE_CARGO_FILE_NAME = rustdesk; + PRODUCT_NAME = rustdesk; + SUPPORTED_PLATFORMS = macosx; + }; + name = Debug; + }; + CA604B55B260D6691E4C3FF1 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CARGO_XCODE_CARGO_DEP_FILE_NAME = liblibrustdesk.d; + CARGO_XCODE_CARGO_FILE_NAME = liblibrustdesk.dylib; + PRODUCT_NAME = librustdesk; + SUPPORTED_PLATFORMS = macosx; + }; + name = Debug; + }; + CA60583BB9CE12977AAB839F /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CARGO_XCODE_CARGO_DEP_FILE_NAME = liblibrustdesk.d; + CARGO_XCODE_CARGO_FILE_NAME = liblibrustdesk.a; + INSTALL_GROUP = ""; + INSTALL_MODE_FLAG = ""; + INSTALL_OWNER = ""; + PRODUCT_NAME = librustdesk_static; + SKIP_INSTALL = YES; + SUPPORTED_PLATFORMS = "macosx iphonesimulator iphoneos appletvsimulator appletvos"; + }; + name = Release; + }; + CA60583BB9CEC858B7409EE3 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CARGO_XCODE_CARGO_DEP_FILE_NAME = naming.d; + CARGO_XCODE_CARGO_FILE_NAME = naming; + PRODUCT_NAME = naming; + SUPPORTED_PLATFORMS = macosx; + }; + name = Release; + }; + CA60583BB9CEC9FA710A2219 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CARGO_XCODE_CARGO_DEP_FILE_NAME = rustdesk.d; + CARGO_XCODE_CARGO_FILE_NAME = rustdesk; + PRODUCT_NAME = rustdesk; + SUPPORTED_PLATFORMS = macosx; + }; + name = Release; + }; + CA60583BB9CED6691E4C3FF1 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CARGO_XCODE_CARGO_DEP_FILE_NAME = liblibrustdesk.d; + CARGO_XCODE_CARGO_FILE_NAME = liblibrustdesk.dylib; + PRODUCT_NAME = librustdesk; + SUPPORTED_PLATFORMS = macosx; + }; + name = Release; + }; + CA608F3F78EE228BE02872F8 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CARGO_TARGET_DIR = "$(PROJECT_TEMP_DIR)/cargo_target"; + CARGO_XCODE_BUILD_MODE = debug; + CARGO_XCODE_FEATURES = ""; + "CARGO_XCODE_TARGET_ARCH[arch=arm64*]" = aarch64; + "CARGO_XCODE_TARGET_ARCH[arch=i386]" = i686; + "CARGO_XCODE_TARGET_ARCH[arch=x86_64*]" = x86_64; + "CARGO_XCODE_TARGET_OS[sdk=appletvos*]" = tvos; + "CARGO_XCODE_TARGET_OS[sdk=appletvsimulator*]" = tvos; + "CARGO_XCODE_TARGET_OS[sdk=iphoneos*]" = ios; + "CARGO_XCODE_TARGET_OS[sdk=iphonesimulator*]" = "ios-sim"; + "CARGO_XCODE_TARGET_OS[sdk=macosx*]" = darwin; + ONLY_ACTIVE_ARCH = YES; + PRODUCT_NAME = rustdesk; + SDKROOT = macosx; + SUPPORTS_MACCATALYST = YES; + }; + name = Debug; + }; + CA608F3F78EE3CC16B37690B /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CARGO_TARGET_DIR = "$(PROJECT_TEMP_DIR)/cargo_target"; + CARGO_XCODE_BUILD_MODE = release; + CARGO_XCODE_FEATURES = ""; + "CARGO_XCODE_TARGET_ARCH[arch=arm64*]" = aarch64; + "CARGO_XCODE_TARGET_ARCH[arch=i386]" = i686; + "CARGO_XCODE_TARGET_ARCH[arch=x86_64*]" = x86_64; + "CARGO_XCODE_TARGET_OS[sdk=appletvos*]" = tvos; + "CARGO_XCODE_TARGET_OS[sdk=appletvsimulator*]" = tvos; + "CARGO_XCODE_TARGET_OS[sdk=iphoneos*]" = ios; + "CARGO_XCODE_TARGET_OS[sdk=iphonesimulator*]" = "ios-sim"; + "CARGO_XCODE_TARGET_OS[sdk=macosx*]" = darwin; + PRODUCT_NAME = rustdesk; + SDKROOT = macosx; + SUPPORTS_MACCATALYST = YES; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + CA6028B9540B12977AAB839F /* Build configuration list for PBXNativeTarget "librustdesk-staticlib" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + CA60583BB9CE12977AAB839F /* Release */, + CA604B55B26012977AAB839F /* Debug */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + CA6028B9540BC858B7409EE3 /* Build configuration list for PBXNativeTarget "naming-bin" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + CA60583BB9CEC858B7409EE3 /* Release */, + CA604B55B260C858B7409EE3 /* Debug */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + CA6028B9540BC9FA710A2219 /* Build configuration list for PBXNativeTarget "rustdesk-bin" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + CA60583BB9CEC9FA710A2219 /* Release */, + CA604B55B260C9FA710A2219 /* Debug */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + CA6028B9540BD6691E4C3FF1 /* Build configuration list for PBXNativeTarget "librustdesk-cdylib" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + CA60583BB9CED6691E4C3FF1 /* Release */, + CA604B55B260D6691E4C3FF1 /* Debug */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + CA603C4309E180E02D6C7F57 /* Build configuration list for PBXProject "rustdesk" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + CA608F3F78EE3CC16B37690B /* Release */, + CA608F3F78EE228BE02872F8 /* Debug */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = CA603C4309E1E04653AD465F /* Project object */; +} diff --git a/flutter/pubspec.lock b/flutter/pubspec.lock index 8af54209e..c4492cace 100644 --- a/flutter/pubspec.lock +++ b/flutter/pubspec.lock @@ -1,221 +1,393 @@ # Generated by pub # See https://dart.dev/tools/pub/glossary#lockfile packages: + _fe_analyzer_shared: + dependency: transitive + description: + name: _fe_analyzer_shared + url: "https://pub.dartlang.org" + source: hosted + version: "46.0.0" + after_layout: + dependency: transitive + description: + name: after_layout + url: "https://pub.dartlang.org" + source: hosted + version: "1.2.0" + analyzer: + dependency: transitive + description: + name: analyzer + url: "https://pub.dartlang.org" + source: hosted + version: "4.6.0" + animations: + dependency: transitive + description: + name: animations + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.4" archive: dependency: transitive description: name: archive - url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + url: "https://pub.dartlang.org" source: hosted version: "3.3.1" args: dependency: transitive description: name: args - url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + url: "https://pub.dartlang.org" source: hosted version: "2.3.1" async: dependency: transitive description: name: async - url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + url: "https://pub.dartlang.org" source: hosted version: "2.8.2" + back_button_interceptor: + dependency: "direct main" + description: + name: back_button_interceptor + url: "https://pub.dartlang.org" + source: hosted + version: "6.0.1" boolean_selector: dependency: transitive description: name: boolean_selector - url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + url: "https://pub.dartlang.org" source: hosted version: "2.1.0" + build: + dependency: transitive + description: + name: build + url: "https://pub.dartlang.org" + source: hosted + version: "2.3.0" + build_config: + dependency: transitive + description: + name: build_config + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.0" + build_daemon: + dependency: transitive + description: + name: build_daemon + url: "https://pub.dartlang.org" + source: hosted + version: "3.1.0" + build_resolvers: + dependency: transitive + description: + name: build_resolvers + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.9" + build_runner: + dependency: "direct dev" + description: + name: build_runner + url: "https://pub.dartlang.org" + source: hosted + version: "2.2.0" + build_runner_core: + dependency: transitive + description: + name: build_runner_core + url: "https://pub.dartlang.org" + source: hosted + version: "7.2.3" + built_collection: + dependency: transitive + description: + name: built_collection + url: "https://pub.dartlang.org" + source: hosted + version: "5.1.1" + built_value: + dependency: transitive + description: + name: built_value + url: "https://pub.dartlang.org" + source: hosted + version: "8.4.1" cached_network_image: dependency: transitive description: name: cached_network_image - url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + url: "https://pub.dartlang.org" source: hosted version: "3.2.1" cached_network_image_platform_interface: dependency: transitive description: name: cached_network_image_platform_interface - url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + url: "https://pub.dartlang.org" source: hosted version: "1.0.0" cached_network_image_web: dependency: transitive description: name: cached_network_image_web - url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + url: "https://pub.dartlang.org" source: hosted version: "1.0.1" characters: dependency: transitive description: name: characters - url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + url: "https://pub.dartlang.org" source: hosted version: "1.2.0" charcode: dependency: transitive description: name: charcode - url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + url: "https://pub.dartlang.org" source: hosted version: "1.3.1" + checked_yaml: + dependency: transitive + description: + name: checked_yaml + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.1" clock: dependency: transitive description: name: clock - url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + url: "https://pub.dartlang.org" source: hosted version: "1.1.0" + code_builder: + dependency: transitive + description: + name: code_builder + url: "https://pub.dartlang.org" + source: hosted + version: "4.2.0" collection: dependency: transitive description: name: collection - url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + url: "https://pub.dartlang.org" source: hosted version: "1.16.0" + contextmenu: + dependency: "direct main" + description: + name: contextmenu + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.0" + convert: + dependency: transitive + description: + name: convert + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.2" cross_file: dependency: transitive description: name: cross_file - url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + url: "https://pub.dartlang.org" source: hosted version: "0.3.3+1" crypto: dependency: transitive description: name: crypto - url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + url: "https://pub.dartlang.org" source: hosted version: "3.0.2" csslib: dependency: transitive description: name: csslib - url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + url: "https://pub.dartlang.org" source: hosted version: "0.17.2" cupertino_icons: dependency: "direct main" description: name: cupertino_icons - url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + url: "https://pub.dartlang.org" source: hosted version: "1.0.5" + dart_style: + dependency: transitive + description: + name: dart_style + url: "https://pub.dartlang.org" + source: hosted + version: "2.2.3" dash_chat_2: dependency: "direct main" description: - name: dash_chat_2 - url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" - source: hosted + path: "." + ref: feat_maxWidth + resolved-ref: "3946ecf86d3600b54632fd80d0eb0ef0e74f2d6a" + url: "https://github.com/fufesou/Dash-Chat-2" + source: git version: "0.0.12" - device_info: + desktop_drop: dependency: "direct main" description: - name: device_info - url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + name: desktop_drop + url: "https://pub.dartlang.org" source: hosted - version: "2.0.3" - device_info_platform_interface: + version: "0.3.3" + desktop_multi_window: + dependency: "direct main" + description: + path: "." + ref: e013c81d75320bbf28adddeaadf462264ee6039d + resolved-ref: e013c81d75320bbf28adddeaadf462264ee6039d + url: "https://github.com/Kingtous/rustdesk_desktop_multi_window" + source: git + version: "0.1.0" + device_info_plus: + dependency: "direct main" + description: + name: device_info_plus + url: "https://pub.dartlang.org" + source: hosted + version: "4.1.2" + device_info_plus_linux: dependency: transitive description: - name: device_info_platform_interface - url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + name: device_info_plus_linux + url: "https://pub.dartlang.org" source: hosted - version: "2.0.1" + version: "3.0.0" + device_info_plus_macos: + dependency: transitive + description: + name: device_info_plus_macos + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.0" + device_info_plus_platform_interface: + dependency: transitive + description: + name: device_info_plus_platform_interface + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.0" + device_info_plus_web: + dependency: transitive + description: + name: device_info_plus_web + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.0" + device_info_plus_windows: + dependency: transitive + description: + name: device_info_plus_windows + url: "https://pub.dartlang.org" + source: hosted + version: "4.0.0" draggable_float_widget: dependency: "direct main" description: name: draggable_float_widget - url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + url: "https://pub.dartlang.org" source: hosted version: "0.0.2" event_bus: dependency: transitive description: name: event_bus - url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + url: "https://pub.dartlang.org" source: hosted version: "2.0.0" external_path: dependency: "direct main" description: name: external_path - url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + url: "https://pub.dartlang.org" source: hosted version: "1.0.1" fake_async: dependency: transitive description: name: fake_async - url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + url: "https://pub.dartlang.org" source: hosted version: "1.3.0" ffi: dependency: "direct main" description: name: ffi - url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + url: "https://pub.dartlang.org" source: hosted - version: "1.2.1" + version: "2.0.1" file: dependency: transitive description: name: file - url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + url: "https://pub.dartlang.org" source: hosted - version: "6.1.2" + version: "6.1.4" firebase_analytics: dependency: "direct main" description: name: firebase_analytics - url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + url: "https://pub.dartlang.org" source: hosted - version: "9.3.0" + version: "9.3.3" firebase_analytics_platform_interface: dependency: transitive description: name: firebase_analytics_platform_interface - url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + url: "https://pub.dartlang.org" source: hosted - version: "3.3.0" + version: "3.3.3" firebase_analytics_web: dependency: transitive description: name: firebase_analytics_web - url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + url: "https://pub.dartlang.org" source: hosted - version: "0.4.2" + version: "0.4.2+3" firebase_core: dependency: transitive description: name: firebase_core - url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + url: "https://pub.dartlang.org" source: hosted - version: "1.20.0" + version: "1.21.1" firebase_core_platform_interface: dependency: transitive description: name: firebase_core_platform_interface - url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + url: "https://pub.dartlang.org" source: hosted - version: "4.5.0" + version: "4.5.1" firebase_core_web: dependency: transitive description: name: firebase_core_web - url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + url: "https://pub.dartlang.org" source: hosted - version: "1.7.1" + version: "1.7.2" fixnum: dependency: transitive description: name: fixnum - url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + url: "https://pub.dartlang.org" source: hosted version: "1.0.1" flutter: @@ -227,58 +399,60 @@ packages: dependency: transitive description: name: flutter_blurhash - url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + url: "https://pub.dartlang.org" source: hosted version: "0.7.0" flutter_breadcrumb: dependency: "direct main" description: name: flutter_breadcrumb - url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + url: "https://pub.dartlang.org" source: hosted version: "1.0.1" flutter_cache_manager: dependency: transitive description: name: flutter_cache_manager - url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + url: "https://pub.dartlang.org" source: hosted version: "3.3.0" flutter_launcher_icons: dependency: "direct dev" description: name: flutter_launcher_icons - url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + url: "https://pub.dartlang.org" source: hosted version: "0.9.3" + flutter_lints: + dependency: "direct dev" + description: + name: flutter_lints + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.1" flutter_parsed_text: dependency: transitive description: name: flutter_parsed_text - url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + url: "https://pub.dartlang.org" source: hosted version: "2.2.1" flutter_plugin_android_lifecycle: dependency: transitive description: name: flutter_plugin_android_lifecycle - url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + url: "https://pub.dartlang.org" source: hosted version: "2.0.7" flutter_rust_bridge: dependency: "direct main" description: - name: flutter_rust_bridge - url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" - source: hosted - version: "1.41.0" - flutter_smart_dialog: - dependency: "direct main" - description: - name: flutter_smart_dialog - url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" - source: hosted - version: "4.5.4+1" + path: frb_dart + ref: master + resolved-ref: e5adce55eea0b74d3680e66a2c5252edf17b07e1 + url: "https://github.com/SoLongAndThanksForAllThePizza/flutter_rust_bridge" + source: git + version: "1.32.0" flutter_test: dependency: "direct dev" description: flutter @@ -289,569 +463,818 @@ packages: description: flutter source: sdk version: "0.0.0" + freezed: + dependency: "direct dev" + description: + name: freezed + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.0+1" + freezed_annotation: + dependency: "direct main" + description: + name: freezed_annotation + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.0" + frontend_server_client: + dependency: transitive + description: + name: frontend_server_client + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.3" + get: + dependency: "direct main" + description: + name: get + url: "https://pub.dartlang.org" + source: hosted + version: "4.6.5" + glob: + dependency: transitive + description: + name: glob + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.0" + graphs: + dependency: transitive + description: + name: graphs + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.0" html: dependency: transitive description: name: html - url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + url: "https://pub.dartlang.org" source: hosted version: "0.15.0" http: dependency: "direct main" description: name: http - url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + url: "https://pub.dartlang.org" source: hosted version: "0.13.5" + http_multi_server: + dependency: transitive + description: + name: http_multi_server + url: "https://pub.dartlang.org" + source: hosted + version: "3.2.1" http_parser: dependency: transitive description: name: http_parser - url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + url: "https://pub.dartlang.org" source: hosted version: "4.0.1" image: dependency: "direct main" description: name: image - url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + url: "https://pub.dartlang.org" source: hosted version: "3.2.0" image_picker: dependency: "direct main" description: name: image_picker - url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + url: "https://pub.dartlang.org" source: hosted version: "0.8.5+3" image_picker_android: dependency: transitive description: name: image_picker_android - url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + url: "https://pub.dartlang.org" source: hosted version: "0.8.5+2" image_picker_for_web: dependency: transitive description: name: image_picker_for_web - url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + url: "https://pub.dartlang.org" source: hosted version: "2.1.8" image_picker_ios: dependency: transitive description: name: image_picker_ios - url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + url: "https://pub.dartlang.org" source: hosted version: "0.8.5+6" image_picker_platform_interface: dependency: transitive description: name: image_picker_platform_interface - url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + url: "https://pub.dartlang.org" source: hosted version: "2.6.1" intl: dependency: transitive description: name: intl - url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + url: "https://pub.dartlang.org" source: hosted version: "0.17.0" + io: + dependency: transitive + description: + name: io + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.3" js: dependency: transitive description: name: js - url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + url: "https://pub.dartlang.org" source: hosted version: "0.6.4" + json_annotation: + dependency: transitive + description: + name: json_annotation + url: "https://pub.dartlang.org" + source: hosted + version: "4.6.0" + lints: + dependency: transitive + description: + name: lints + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.0" + logging: + dependency: transitive + description: + name: logging + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.2" matcher: dependency: transitive description: name: matcher - url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + url: "https://pub.dartlang.org" source: hosted version: "0.12.11" material_color_utilities: dependency: transitive description: name: material_color_utilities - url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + url: "https://pub.dartlang.org" source: hosted version: "0.1.4" + menu_base: + dependency: transitive + description: + name: menu_base + url: "https://pub.dartlang.org" + source: hosted + version: "0.1.1" meta: dependency: transitive description: name: meta - url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + url: "https://pub.dartlang.org" source: hosted version: "1.7.0" + mime: + dependency: transitive + description: + name: mime + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.2" nested: dependency: transitive description: name: nested - url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + url: "https://pub.dartlang.org" source: hosted version: "1.0.0" octo_image: dependency: transitive description: name: octo_image - url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + url: "https://pub.dartlang.org" source: hosted version: "1.0.2" - package_info: + package_config: + dependency: transitive + description: + name: package_config + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.0" + package_info_plus: dependency: "direct main" description: - name: package_info - url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + name: package_info_plus + url: "https://pub.dartlang.org" source: hosted - version: "2.0.2" + version: "1.4.3+1" + package_info_plus_linux: + dependency: transitive + description: + name: package_info_plus_linux + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.5" + package_info_plus_macos: + dependency: transitive + description: + name: package_info_plus_macos + url: "https://pub.dartlang.org" + source: hosted + version: "1.3.0" + package_info_plus_platform_interface: + dependency: transitive + description: + name: package_info_plus_platform_interface + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.2" + package_info_plus_web: + dependency: transitive + description: + name: package_info_plus_web + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.5" + package_info_plus_windows: + dependency: transitive + description: + name: package_info_plus_windows + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.0" path: dependency: transitive description: name: path - url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + url: "https://pub.dartlang.org" source: hosted version: "1.8.1" path_provider: dependency: "direct main" description: name: path_provider - url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + url: "https://pub.dartlang.org" source: hosted version: "2.0.11" path_provider_android: dependency: transitive description: name: path_provider_android - url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + url: "https://pub.dartlang.org" source: hosted - version: "2.0.17" + version: "2.0.20" path_provider_ios: dependency: transitive description: name: path_provider_ios - url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + url: "https://pub.dartlang.org" source: hosted version: "2.0.11" path_provider_linux: dependency: transitive description: name: path_provider_linux - url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + url: "https://pub.dartlang.org" source: hosted version: "2.1.7" path_provider_macos: dependency: transitive description: name: path_provider_macos - url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + url: "https://pub.dartlang.org" source: hosted version: "2.0.6" path_provider_platform_interface: dependency: transitive description: name: path_provider_platform_interface - url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + url: "https://pub.dartlang.org" source: hosted version: "2.0.4" path_provider_windows: dependency: transitive description: name: path_provider_windows - url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + url: "https://pub.dartlang.org" source: hosted - version: "2.0.7" + version: "2.1.2" pedantic: dependency: transitive description: name: pedantic - url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + url: "https://pub.dartlang.org" source: hosted version: "1.11.1" petitparser: dependency: transitive description: name: petitparser - url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + url: "https://pub.dartlang.org" source: hosted version: "5.0.0" platform: dependency: transitive description: name: platform - url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + url: "https://pub.dartlang.org" source: hosted version: "3.1.0" plugin_platform_interface: dependency: transitive description: name: plugin_platform_interface - url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + url: "https://pub.dartlang.org" source: hosted version: "2.1.2" + pool: + dependency: transitive + description: + name: pool + url: "https://pub.dartlang.org" + source: hosted + version: "1.5.1" process: dependency: transitive description: name: process - url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + url: "https://pub.dartlang.org" source: hosted version: "4.2.4" provider: dependency: "direct main" description: name: provider - url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + url: "https://pub.dartlang.org" source: hosted version: "6.0.3" + pub_semver: + dependency: transitive + description: + name: pub_semver + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.1" + pubspec_parse: + dependency: transitive + description: + name: pubspec_parse + url: "https://pub.dartlang.org" + source: hosted + version: "1.2.1" qr_code_scanner: dependency: "direct main" description: name: qr_code_scanner - url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + url: "https://pub.dartlang.org" source: hosted - version: "1.0.0" + version: "1.0.1" quiver: dependency: transitive description: name: quiver - url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + url: "https://pub.dartlang.org" source: hosted version: "3.1.0" rxdart: dependency: transitive description: name: rxdart - url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + url: "https://pub.dartlang.org" source: hosted version: "0.27.5" + screen_retriever: + dependency: transitive + description: + name: screen_retriever + url: "https://pub.dartlang.org" + source: hosted + version: "0.1.2" + scroll_pos: + dependency: "direct main" + description: + name: scroll_pos + url: "https://pub.dartlang.org" + source: hosted + version: "0.3.0" settings_ui: dependency: "direct main" description: name: settings_ui - url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + url: "https://pub.dartlang.org" source: hosted version: "2.0.2" shared_preferences: dependency: "direct main" description: name: shared_preferences - url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + url: "https://pub.dartlang.org" source: hosted version: "2.0.15" shared_preferences_android: dependency: transitive description: name: shared_preferences_android - url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + url: "https://pub.dartlang.org" source: hosted version: "2.0.12" shared_preferences_ios: dependency: transitive description: name: shared_preferences_ios - url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + url: "https://pub.dartlang.org" source: hosted version: "2.1.1" shared_preferences_linux: dependency: transitive description: name: shared_preferences_linux - url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + url: "https://pub.dartlang.org" source: hosted version: "2.1.1" shared_preferences_macos: dependency: transitive description: name: shared_preferences_macos - url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + url: "https://pub.dartlang.org" source: hosted version: "2.0.4" shared_preferences_platform_interface: dependency: transitive description: name: shared_preferences_platform_interface - url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + url: "https://pub.dartlang.org" source: hosted version: "2.0.0" shared_preferences_web: dependency: transitive description: name: shared_preferences_web - url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + url: "https://pub.dartlang.org" source: hosted version: "2.0.4" shared_preferences_windows: dependency: transitive description: name: shared_preferences_windows - url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + url: "https://pub.dartlang.org" source: hosted version: "2.1.1" + shelf: + dependency: transitive + description: + name: shelf + url: "https://pub.dartlang.org" + source: hosted + version: "1.3.2" + shelf_web_socket: + dependency: transitive + description: + name: shelf_web_socket + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.2" + shortid: + dependency: transitive + description: + name: shortid + url: "https://pub.dartlang.org" + source: hosted + version: "0.1.2" sky_engine: dependency: transitive description: flutter source: sdk version: "0.0.99" + source_gen: + dependency: transitive + description: + name: source_gen + url: "https://pub.dartlang.org" + source: hosted + version: "1.2.2" source_span: dependency: transitive description: name: source_span - url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + url: "https://pub.dartlang.org" source: hosted version: "1.8.2" sqflite: dependency: transitive description: name: sqflite - url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + url: "https://pub.dartlang.org" source: hosted version: "2.0.3+1" sqflite_common: dependency: transitive description: name: sqflite_common - url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + url: "https://pub.dartlang.org" source: hosted version: "2.2.1+1" stack_trace: dependency: transitive description: name: stack_trace - url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + url: "https://pub.dartlang.org" source: hosted version: "1.10.0" stream_channel: dependency: transitive description: name: stream_channel - url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + url: "https://pub.dartlang.org" source: hosted version: "2.1.0" + stream_transform: + dependency: transitive + description: + name: stream_transform + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.0" string_scanner: dependency: transitive description: name: string_scanner - url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + url: "https://pub.dartlang.org" source: hosted version: "1.1.0" synchronized: dependency: transitive description: name: synchronized - url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + url: "https://pub.dartlang.org" source: hosted version: "3.0.0+2" term_glyph: dependency: transitive description: name: term_glyph - url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + url: "https://pub.dartlang.org" source: hosted version: "1.2.0" test_api: dependency: transitive description: name: test_api - url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + url: "https://pub.dartlang.org" source: hosted version: "0.4.9" + timing: + dependency: transitive + description: + name: timing + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.0" toggle_switch: dependency: "direct main" description: name: toggle_switch - url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + url: "https://pub.dartlang.org" source: hosted version: "1.4.0" + tray_manager: + dependency: "direct main" + description: + path: "." + ref: "3aa37c86e47ea748e7b5507cbe59f2c54ebdb23a" + resolved-ref: "3aa37c86e47ea748e7b5507cbe59f2c54ebdb23a" + url: "https://github.com/Kingtous/rustdesk_tray_manager" + source: git + version: "0.1.8" tuple: dependency: "direct main" description: name: tuple - url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + url: "https://pub.dartlang.org" source: hosted version: "2.0.0" typed_data: dependency: transitive description: name: typed_data - url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + url: "https://pub.dartlang.org" source: hosted version: "1.3.1" url_launcher: dependency: "direct main" description: name: url_launcher - url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + url: "https://pub.dartlang.org" source: hosted version: "6.1.5" url_launcher_android: dependency: transitive description: name: url_launcher_android - url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + url: "https://pub.dartlang.org" source: hosted version: "6.0.17" url_launcher_ios: dependency: transitive description: name: url_launcher_ios - url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + url: "https://pub.dartlang.org" source: hosted version: "6.0.17" url_launcher_linux: dependency: transitive description: name: url_launcher_linux - url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + url: "https://pub.dartlang.org" source: hosted version: "3.0.1" url_launcher_macos: dependency: transitive description: name: url_launcher_macos - url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + url: "https://pub.dartlang.org" source: hosted version: "3.0.1" url_launcher_platform_interface: dependency: transitive description: name: url_launcher_platform_interface - url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + url: "https://pub.dartlang.org" source: hosted version: "2.1.0" url_launcher_web: dependency: transitive description: name: url_launcher_web - url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + url: "https://pub.dartlang.org" source: hosted version: "2.0.13" url_launcher_windows: dependency: transitive description: name: url_launcher_windows - url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + url: "https://pub.dartlang.org" source: hosted version: "3.0.1" uuid: dependency: transitive description: name: uuid - url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + url: "https://pub.dartlang.org" source: hosted version: "3.0.6" vector_math: dependency: transitive description: name: vector_math - url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + url: "https://pub.dartlang.org" source: hosted version: "2.1.2" video_player: dependency: transitive description: name: video_player - url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + url: "https://pub.dartlang.org" source: hosted - version: "2.4.6" + version: "2.4.7" video_player_android: dependency: transitive description: name: video_player_android - url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + url: "https://pub.dartlang.org" source: hosted - version: "2.3.8" + version: "2.3.9" video_player_avfoundation: dependency: transitive description: name: video_player_avfoundation - url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + url: "https://pub.dartlang.org" source: hosted version: "2.3.5" video_player_platform_interface: dependency: transitive description: name: video_player_platform_interface - url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + url: "https://pub.dartlang.org" source: hosted version: "5.1.4" video_player_web: dependency: transitive description: name: video_player_web - url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + url: "https://pub.dartlang.org" source: hosted version: "2.0.12" + visibility_detector: + dependency: "direct main" + description: + name: visibility_detector + url: "https://pub.dartlang.org" + source: hosted + version: "0.3.3" wakelock: dependency: "direct main" description: name: wakelock - url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + url: "https://pub.dartlang.org" source: hosted version: "0.5.6" wakelock_macos: dependency: transitive description: name: wakelock_macos - url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + url: "https://pub.dartlang.org" source: hosted version: "0.4.0" wakelock_platform_interface: dependency: transitive description: name: wakelock_platform_interface - url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + url: "https://pub.dartlang.org" source: hosted version: "0.3.0" wakelock_web: dependency: transitive description: name: wakelock_web - url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + url: "https://pub.dartlang.org" source: hosted version: "0.4.0" wakelock_windows: dependency: transitive description: name: wakelock_windows - url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + url: "https://pub.dartlang.org" source: hosted version: "0.2.0" + watcher: + dependency: transitive + description: + name: watcher + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.1" + web_socket_channel: + dependency: transitive + description: + name: web_socket_channel + url: "https://pub.dartlang.org" + source: hosted + version: "2.2.0" win32: dependency: transitive description: name: win32 - url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + url: "https://pub.dartlang.org" source: hosted - version: "2.6.1" + version: "2.7.0" + window_manager: + dependency: "direct main" + description: + path: "." + ref: "799ef079e87938c3f4340591b4330c2598f38bb9" + resolved-ref: "799ef079e87938c3f4340591b4330c2598f38bb9" + url: "https://github.com/Kingtous/rustdesk_window_manager" + source: git + version: "0.2.6" xdg_directories: dependency: transitive description: name: xdg_directories - url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + url: "https://pub.dartlang.org" source: hosted - version: "0.2.0+1" + version: "0.2.0+2" xml: dependency: transitive description: name: xml - url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + url: "https://pub.dartlang.org" source: hosted version: "6.1.0" yaml: dependency: transitive description: name: yaml - url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + url: "https://pub.dartlang.org" source: hosted version: "3.1.1" zxing2: dependency: "direct main" description: name: zxing2 - url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + url: "https://pub.dartlang.org" source: hosted version: "0.1.0" sdks: diff --git a/flutter/pubspec.yaml b/flutter/pubspec.yaml index 2ee7ec1ac..93c2f64b2 100644 --- a/flutter/pubspec.yaml +++ b/flutter/pubspec.yaml @@ -3,7 +3,7 @@ description: Your Remote Desktop Software # The following line prevents the package from being accidentally published to # pub.dev using `pub publish`. This is preferred for private packages. -publish_to: 'none' # Remove this line if you wish to publish to pub.dev +publish_to: "none" # Remove this line if you wish to publish to pub.dev # The following defines the version and build number for your application. # A version number is three numbers separated by dots, like 1.2.43 @@ -19,93 +19,127 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev version: 1.1.10-1+28 environment: - sdk: ">=2.16.1" + sdk: ">=2.16.1" dependencies: - flutter: - sdk: flutter + flutter: + sdk: flutter - - # The following adds the Cupertino Icons font to your application. - # Use with the CupertinoIcons class for iOS style icons. - cupertino_icons: ^1.0.3 - ffi: ^1.1.2 - path_provider: ^2.0.2 - external_path: ^1.0.1 - provider: ^6.0.3 - tuple: ^2.0.0 - wakelock: ^0.5.2 - device_info: ^2.0.2 - firebase_analytics: ^9.1.5 - package_info: ^2.0.2 - url_launcher: ^6.0.9 - shared_preferences: ^2.0.6 - toggle_switch: ^1.4.0 - dash_chat_2: ^0.0.12 - draggable_float_widget: ^0.0.2 - settings_ui: ^2.0.2 - flutter_breadcrumb: ^1.0.1 - http: ^0.13.4 - qr_code_scanner: ^1.0.0 - zxing2: ^0.1.0 - image_picker: ^0.8.5 - image: ^3.1.3 - flutter_smart_dialog: ^4.3.1 - flutter_rust_bridge: ^1.30.0 + # The following adds the Cupertino Icons font to your application. + # Use with the CupertinoIcons class for iOS style icons. + cupertino_icons: ^1.0.3 + ffi: ^2.0.1 + path_provider: ^2.0.2 + external_path: ^1.0.1 + provider: ^6.0.3 + tuple: ^2.0.0 + wakelock: ^0.5.2 + device_info_plus: ^4.0.2 + firebase_analytics: ^9.1.5 + package_info_plus: ^1.4.2 + url_launcher: ^6.0.9 + shared_preferences: ^2.0.6 + toggle_switch: ^1.4.0 + dash_chat_2: + git: + url: https://github.com/fufesou/Dash-Chat-2 + ref: feat_maxWidth + draggable_float_widget: ^0.0.2 + settings_ui: ^2.0.2 + flutter_breadcrumb: ^1.0.1 + http: ^0.13.4 + qr_code_scanner: ^1.0.0 + zxing2: ^0.1.0 + image_picker: ^0.8.5 + image: ^3.1.3 + back_button_interceptor: ^6.0.1 + flutter_rust_bridge: + git: + url: https://github.com/SoLongAndThanksForAllThePizza/flutter_rust_bridge + ref: master + path: frb_dart + window_manager: + git: + url: https://github.com/Kingtous/rustdesk_window_manager + ref: 799ef079e87938c3f4340591b4330c2598f38bb9 + desktop_multi_window: + git: + url: https://github.com/Kingtous/rustdesk_desktop_multi_window + ref: e013c81d75320bbf28adddeaadf462264ee6039d + freezed_annotation: ^2.0.3 + tray_manager: + git: + url: https://github.com/Kingtous/rustdesk_tray_manager + ref: 3aa37c86e47ea748e7b5507cbe59f2c54ebdb23a + get: ^4.6.5 + visibility_detector: ^0.3.3 + contextmenu: ^3.0.0 + desktop_drop: ^0.3.3 + scroll_pos: ^0.3.0 dev_dependencies: - flutter_launcher_icons: ^0.9.1 - flutter_test: - sdk: flutter + flutter_launcher_icons: ^0.9.1 + flutter_test: + sdk: flutter + build_runner: ^2.1.11 + freezed: ^2.0.3 + flutter_lints: ^2.0.0 # rerun: flutter pub run flutter_launcher_icons:main flutter_icons: - android: "ic_launcher" - ios: true - image_path: "../1024-rec.png" + android: "ic_launcher" + ios: true + image_path: "../1024-rec.png" # For information on the generic Dart part of this file, see the # following page: https://dart.dev/tools/pub/pubspec # The following section is specific to Flutter. flutter: + # The following line ensures that the Material Icons font is + # included with your application, so that you can use the icons in + # the material Icons class. + uses-material-design: true - # The following line ensures that the Material Icons font is - # included with your application, so that you can use the icons in - # the material Icons class. - uses-material-design: true + # To add assets to your application, add an assets section, like this: + assets: + - assets/ - # To add assets to your application, add an assets section, like this: - assets: - - assets/ + fonts: + - family: GestureIcons + fonts: + - asset: assets/gestures.ttf + - family: Tabbar + fonts: + - asset: assets/tabbar.ttf + - family: PeerSearchbar + fonts: + - asset: assets/peer_searchbar.ttf - fonts: - - family: GestureIcons - fonts: - - asset: assets/gestures.ttf + - # An image asset can refer to one or more resolution-specific "variants", see - # https://flutter.dev/assets-and-images/#resolution-aware. + # An image asset can refer to one or more resolution-specific "variants", see + # https://flutter.dev/assets-and-images/#resolution-aware. - # For details regarding adding assets from package dependencies, see - # https://flutter.dev/assets-and-images/#from-packages + # For details regarding adding assets from package dependencies, see + # https://flutter.dev/assets-and-images/#from-packages - # To add custom fonts to your application, add a fonts section here, - # in this "flutter" section. Each entry in this list should have a - # "family" key with the font family name, and a "fonts" key with a - # list giving the asset and other descriptors for the font. For - # example: - # fonts: - # - family: Schyler - # fonts: - # - asset: fonts/Schyler-Regular.ttf - # - asset: fonts/Schyler-Italic.ttf - # style: italic - # - family: Trajan Pro - # fonts: - # - asset: fonts/TrajanPro.ttf - # - asset: fonts/TrajanPro_Bold.ttf - # weight: 700 - # - # For details regarding fonts from package dependencies, - # see https://flutter.dev/custom-fonts/#from-packages + # To add custom fonts to your application, add a fonts section here, + # in this "flutter" section. Each entry in this list should have a + # "family" key with the font family name, and a "fonts" key with a + # list giving the asset and other descriptors for the font. For + # example: + # fonts: + # - family: Schyler + # fonts: + # - asset: fonts/Schyler-Regular.ttf + # - asset: fonts/Schyler-Italic.ttf + # style: italic + # - family: Trajan Pro + # fonts: + # - asset: fonts/TrajanPro.ttf + # - asset: fonts/TrajanPro_Bold.ttf + # weight: 700 + # + # For details regarding fonts from package dependencies, + # see https://flutter.dev/custom-fonts/#from-packages diff --git a/flutter/rustdesk.desktop b/flutter/rustdesk.desktop new file mode 100644 index 000000000..c94285bbd --- /dev/null +++ b/flutter/rustdesk.desktop @@ -0,0 +1,19 @@ +[Desktop Entry] +Version=1.1.10 +Name=RustDesk +GenericName=Remote Desktop +Comment=Remote Desktop +Exec=/usr/lib/rustdesk/flutter_hbb %u +Icon=/usr/share/rustdesk/files/rustdesk.png +Terminal=false +Type=Application +StartupNotify=true +Categories=Network;RemoteAccess;GTK; +Keywords=internet; +Actions=new-window; + +X-Desktop-File-Install-Version=0.23 + +[Desktop Action new-window] +Name=Open a New Window + diff --git a/flutter/rustdesk.service b/flutter/rustdesk.service new file mode 100644 index 000000000..422d9e387 --- /dev/null +++ b/flutter/rustdesk.service @@ -0,0 +1,16 @@ +[Unit] +Description=RustDesk +Requires=network.target +After=systemd-user-sessions.service + +[Service] +Type=simple +ExecStart=/usr/lib/rustdesk/flutter_hbb --service +PIDFile=/run/rustdesk.pid +KillMode=mixed +TimeoutStopSec=30 +User=root +LimitNOFILE=100000 + +[Install] +WantedBy=multi-user.target diff --git a/flutter/windows/.gitignore b/flutter/windows/.gitignore new file mode 100644 index 000000000..d492d0d98 --- /dev/null +++ b/flutter/windows/.gitignore @@ -0,0 +1,17 @@ +flutter/ephemeral/ + +# Visual Studio user-specific files. +*.suo +*.user +*.userosscache +*.sln.docstates + +# Visual Studio build-related files. +x64/ +x86/ + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ diff --git a/flutter/windows/CMakeLists.txt b/flutter/windows/CMakeLists.txt new file mode 100644 index 000000000..3d4e30586 --- /dev/null +++ b/flutter/windows/CMakeLists.txt @@ -0,0 +1,101 @@ +# Project-level configuration. +cmake_minimum_required(VERSION 3.14) +project(flutter_hbb LANGUAGES CXX) + +# The name of the executable created for the application. Change this to change +# the on-disk name of your application. +set(BINARY_NAME "flutter_hbb") + +# Explicitly opt in to modern CMake behaviors to avoid warnings with recent +# versions of CMake. +cmake_policy(SET CMP0063 NEW) + +# Define build configuration option. +get_property(IS_MULTICONFIG GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG) +if(IS_MULTICONFIG) + set(CMAKE_CONFIGURATION_TYPES "Debug;Profile;Release" + CACHE STRING "" FORCE) +else() + if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) + set(CMAKE_BUILD_TYPE "Debug" CACHE + STRING "Flutter build mode" FORCE) + set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS + "Debug" "Profile" "Release") + endif() +endif() +# Define settings for the Profile build mode. +set(CMAKE_EXE_LINKER_FLAGS_PROFILE "${CMAKE_EXE_LINKER_FLAGS_RELEASE}") +set(CMAKE_SHARED_LINKER_FLAGS_PROFILE "${CMAKE_SHARED_LINKER_FLAGS_RELEASE}") +set(CMAKE_C_FLAGS_PROFILE "${CMAKE_C_FLAGS_RELEASE}") +set(CMAKE_CXX_FLAGS_PROFILE "${CMAKE_CXX_FLAGS_RELEASE}") + +# Use Unicode for all projects. +add_definitions(-DUNICODE -D_UNICODE) + +# Compilation settings that should be applied to most targets. +# +# Be cautious about adding new options here, as plugins use this function by +# default. In most cases, you should add new options to specific targets instead +# of modifying this function. +function(APPLY_STANDARD_SETTINGS TARGET) + target_compile_features(${TARGET} PUBLIC cxx_std_17) + target_compile_options(${TARGET} PRIVATE /W4 /WX /wd"4100") + target_compile_options(${TARGET} PRIVATE /EHsc) + target_compile_definitions(${TARGET} PRIVATE "_HAS_EXCEPTIONS=0") + target_compile_definitions(${TARGET} PRIVATE "$<$:_DEBUG>") +endfunction() + +# Flutter library and tool build rules. +set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") +add_subdirectory(${FLUTTER_MANAGED_DIR}) + +# Application build; see runner/CMakeLists.txt. +add_subdirectory("runner") + +# Generated plugin build rules, which manage building the plugins and adding +# them to the application. +include(flutter/generated_plugins.cmake) + + +# === Installation === +# Support files are copied into place next to the executable, so that it can +# run in place. This is done instead of making a separate bundle (as on Linux) +# so that building and running from within Visual Studio will work. +set(BUILD_BUNDLE_DIR "$") +# Make the "install" step default, as it's required to run. +set(CMAKE_VS_INCLUDE_INSTALL_TO_DEFAULT_BUILD 1) +if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) + set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) +endif() + +set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") +set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}") + +install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + +if(PLUGIN_BUNDLED_LIBRARIES) + install(FILES "${PLUGIN_BUNDLED_LIBRARIES}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endif() + +# Fully re-copy the assets directory on each build to avoid having stale files +# from a previous install. +set(FLUTTER_ASSET_DIR_NAME "flutter_assets") +install(CODE " + file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\") + " COMPONENT Runtime) +install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}" + DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) + +# Install the AOT library on non-Debug builds only. +install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + CONFIGURATIONS Profile;Release + COMPONENT Runtime) diff --git a/flutter/windows/flutter/CMakeLists.txt b/flutter/windows/flutter/CMakeLists.txt new file mode 100644 index 000000000..930d2071a --- /dev/null +++ b/flutter/windows/flutter/CMakeLists.txt @@ -0,0 +1,104 @@ +# This file controls Flutter-level build steps. It should not be edited. +cmake_minimum_required(VERSION 3.14) + +set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") + +# Configuration provided via flutter tool. +include(${EPHEMERAL_DIR}/generated_config.cmake) + +# TODO: Move the rest of this into files in ephemeral. See +# https://github.com/flutter/flutter/issues/57146. +set(WRAPPER_ROOT "${EPHEMERAL_DIR}/cpp_client_wrapper") + +# === Flutter Library === +set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/flutter_windows.dll") + +# Published to parent scope for install step. +set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) +set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) +set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) +set(AOT_LIBRARY "${PROJECT_DIR}/build/windows/app.so" PARENT_SCOPE) + +list(APPEND FLUTTER_LIBRARY_HEADERS + "flutter_export.h" + "flutter_windows.h" + "flutter_messenger.h" + "flutter_plugin_registrar.h" + "flutter_texture_registrar.h" +) +list(TRANSFORM FLUTTER_LIBRARY_HEADERS PREPEND "${EPHEMERAL_DIR}/") +add_library(flutter INTERFACE) +target_include_directories(flutter INTERFACE + "${EPHEMERAL_DIR}" +) +target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}.lib") +add_dependencies(flutter flutter_assemble) + +# === Wrapper === +list(APPEND CPP_WRAPPER_SOURCES_CORE + "core_implementations.cc" + "standard_codec.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_CORE PREPEND "${WRAPPER_ROOT}/") +list(APPEND CPP_WRAPPER_SOURCES_PLUGIN + "plugin_registrar.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_PLUGIN PREPEND "${WRAPPER_ROOT}/") +list(APPEND CPP_WRAPPER_SOURCES_APP + "flutter_engine.cc" + "flutter_view_controller.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_APP PREPEND "${WRAPPER_ROOT}/") + +# Wrapper sources needed for a plugin. +add_library(flutter_wrapper_plugin STATIC + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_PLUGIN} +) +apply_standard_settings(flutter_wrapper_plugin) +set_target_properties(flutter_wrapper_plugin PROPERTIES + POSITION_INDEPENDENT_CODE ON) +set_target_properties(flutter_wrapper_plugin PROPERTIES + CXX_VISIBILITY_PRESET hidden) +target_link_libraries(flutter_wrapper_plugin PUBLIC flutter) +target_include_directories(flutter_wrapper_plugin PUBLIC + "${WRAPPER_ROOT}/include" +) +add_dependencies(flutter_wrapper_plugin flutter_assemble) + +# Wrapper sources needed for the runner. +add_library(flutter_wrapper_app STATIC + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_APP} +) +apply_standard_settings(flutter_wrapper_app) +target_link_libraries(flutter_wrapper_app PUBLIC flutter) +target_include_directories(flutter_wrapper_app PUBLIC + "${WRAPPER_ROOT}/include" +) +add_dependencies(flutter_wrapper_app flutter_assemble) + +# === Flutter tool backend === +# _phony_ is a non-existent file to force this command to run every time, +# since currently there's no way to get a full input/output list from the +# flutter tool. +set(PHONY_OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/_phony_") +set_source_files_properties("${PHONY_OUTPUT}" PROPERTIES SYMBOLIC TRUE) +add_custom_command( + OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} + ${CPP_WRAPPER_SOURCES_CORE} ${CPP_WRAPPER_SOURCES_PLUGIN} + ${CPP_WRAPPER_SOURCES_APP} + ${PHONY_OUTPUT} + COMMAND ${CMAKE_COMMAND} -E env + ${FLUTTER_TOOL_ENVIRONMENT} + "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.bat" + windows-x64 $ + VERBATIM +) +add_custom_target(flutter_assemble DEPENDS + "${FLUTTER_LIBRARY}" + ${FLUTTER_LIBRARY_HEADERS} + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_PLUGIN} + ${CPP_WRAPPER_SOURCES_APP} +) diff --git a/flutter/windows/runner/CMakeLists.txt b/flutter/windows/runner/CMakeLists.txt new file mode 100644 index 000000000..bcaa06d73 --- /dev/null +++ b/flutter/windows/runner/CMakeLists.txt @@ -0,0 +1,51 @@ +cmake_minimum_required(VERSION 3.14) +project(runner LANGUAGES CXX) + +# Define the application target. To change its name, change BINARY_NAME in the +# top-level CMakeLists.txt, not the value here, or `flutter run` will no longer +# work. +# +# Any new source files that you add to the application should be added here. +add_executable(${BINARY_NAME} WIN32 + "flutter_window.cpp" + "main.cpp" + "utils.cpp" + "win32_window.cpp" + "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" + "Runner.rc" + "runner.exe.manifest" +) + +# flutter_rust_bridge with Corrosion +find_package(Corrosion REQUIRED) + +corrosion_import_crate(MANIFEST_PATH ../../../Cargo.toml + # Equivalent to --all-features passed to cargo build +# [ALL_FEATURES] + # Equivalent to --no-default-features passed to cargo build +# [NO_DEFAULT_FEATURES] + # Disable linking of standard libraries (required for no_std crates). +# [NO_STD] + # Specify cargo build profile (e.g. release or a custom profile) +# [PROFILE ] + # Only import the specified crates from a workspace +# [CRATES ... ] + # Enable the specified features +# [FEATURES ... ] +) +target_link_libraries(${BINARY_NAME} PRIVATE librustdesk) + +# Apply the standard set of build settings. This can be removed for applications +# that need different build settings. +apply_standard_settings(${BINARY_NAME}) + +# Disable Windows macros that collide with C++ standard library functions. +target_compile_definitions(${BINARY_NAME} PRIVATE "NOMINMAX") + +# Add dependency libraries and include directories. Add any application-specific +# dependencies here. +target_link_libraries(${BINARY_NAME} PRIVATE flutter flutter_wrapper_app) +target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}") + +# Run the Flutter tool portions of the build. This must not be removed. +add_dependencies(${BINARY_NAME} flutter_assemble) diff --git a/flutter/windows/runner/Runner.rc b/flutter/windows/runner/Runner.rc new file mode 100644 index 000000000..d10e3f411 --- /dev/null +++ b/flutter/windows/runner/Runner.rc @@ -0,0 +1,121 @@ +// Microsoft Visual C++ generated resource script. +// +#pragma code_page(65001) +#include "resource.h" + +#define APSTUDIO_READONLY_SYMBOLS +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 2 resource. +// +#include "winres.h" + +///////////////////////////////////////////////////////////////////////////// +#undef APSTUDIO_READONLY_SYMBOLS + +///////////////////////////////////////////////////////////////////////////// +// English (United States) resources + +#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU) +LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US + +#ifdef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// TEXTINCLUDE +// + +1 TEXTINCLUDE +BEGIN + "resource.h\0" +END + +2 TEXTINCLUDE +BEGIN + "#include ""winres.h""\r\n" + "\0" +END + +3 TEXTINCLUDE +BEGIN + "\r\n" + "\0" +END + +#endif // APSTUDIO_INVOKED + + +///////////////////////////////////////////////////////////////////////////// +// +// Icon +// + +// Icon with lowest ID value placed first to ensure application icon +// remains consistent on all systems. +IDI_APP_ICON ICON "resources\\app_icon.ico" + + +///////////////////////////////////////////////////////////////////////////// +// +// Version +// + +#ifdef FLUTTER_BUILD_NUMBER +#define VERSION_AS_NUMBER FLUTTER_BUILD_NUMBER +#else +#define VERSION_AS_NUMBER 1,0,0 +#endif + +#ifdef FLUTTER_BUILD_NAME +#define VERSION_AS_STRING #FLUTTER_BUILD_NAME +#else +#define VERSION_AS_STRING "1.0.0" +#endif + +VS_VERSION_INFO VERSIONINFO + FILEVERSION VERSION_AS_NUMBER + PRODUCTVERSION VERSION_AS_NUMBER + FILEFLAGSMASK VS_FFI_FILEFLAGSMASK +#ifdef _DEBUG + FILEFLAGS VS_FF_DEBUG +#else + FILEFLAGS 0x0L +#endif + FILEOS VOS__WINDOWS32 + FILETYPE VFT_APP + FILESUBTYPE 0x0L +BEGIN + BLOCK "StringFileInfo" + BEGIN + BLOCK "040904e4" + BEGIN + VALUE "CompanyName", "com.carriez" "\0" + VALUE "FileDescription", "flutter_hbb" "\0" + VALUE "FileVersion", VERSION_AS_STRING "\0" + VALUE "InternalName", "flutter_hbb" "\0" + VALUE "LegalCopyright", "Copyright (C) 2022 com.carriez. All rights reserved." "\0" + VALUE "OriginalFilename", "flutter_hbb.exe" "\0" + VALUE "ProductName", "flutter_hbb" "\0" + VALUE "ProductVersion", VERSION_AS_STRING "\0" + END + END + BLOCK "VarFileInfo" + BEGIN + VALUE "Translation", 0x409, 1252 + END +END + +#endif // English (United States) resources +///////////////////////////////////////////////////////////////////////////// + + + +#ifndef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 3 resource. +// + + +///////////////////////////////////////////////////////////////////////////// +#endif // not APSTUDIO_INVOKED diff --git a/flutter/windows/runner/flutter_window.cpp b/flutter/windows/runner/flutter_window.cpp new file mode 100644 index 000000000..b43b9095e --- /dev/null +++ b/flutter/windows/runner/flutter_window.cpp @@ -0,0 +1,61 @@ +#include "flutter_window.h" + +#include + +#include "flutter/generated_plugin_registrant.h" + +FlutterWindow::FlutterWindow(const flutter::DartProject& project) + : project_(project) {} + +FlutterWindow::~FlutterWindow() {} + +bool FlutterWindow::OnCreate() { + if (!Win32Window::OnCreate()) { + return false; + } + + RECT frame = GetClientArea(); + + // The size here must match the window dimensions to avoid unnecessary surface + // creation / destruction in the startup path. + flutter_controller_ = std::make_unique( + frame.right - frame.left, frame.bottom - frame.top, project_); + // Ensure that basic setup of the controller was successful. + if (!flutter_controller_->engine() || !flutter_controller_->view()) { + return false; + } + RegisterPlugins(flutter_controller_->engine()); + SetChildContent(flutter_controller_->view()->GetNativeWindow()); + return true; +} + +void FlutterWindow::OnDestroy() { + if (flutter_controller_) { + flutter_controller_ = nullptr; + } + + Win32Window::OnDestroy(); +} + +LRESULT +FlutterWindow::MessageHandler(HWND hwnd, UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + // Give Flutter, including plugins, an opportunity to handle window messages. + if (flutter_controller_) { + std::optional result = + flutter_controller_->HandleTopLevelWindowProc(hwnd, message, wparam, + lparam); + if (result) { + return *result; + } + } + + switch (message) { + case WM_FONTCHANGE: + flutter_controller_->engine()->ReloadSystemFonts(); + break; + } + + return Win32Window::MessageHandler(hwnd, message, wparam, lparam); +} diff --git a/flutter/windows/runner/flutter_window.h b/flutter/windows/runner/flutter_window.h new file mode 100644 index 000000000..6da0652f0 --- /dev/null +++ b/flutter/windows/runner/flutter_window.h @@ -0,0 +1,33 @@ +#ifndef RUNNER_FLUTTER_WINDOW_H_ +#define RUNNER_FLUTTER_WINDOW_H_ + +#include +#include + +#include + +#include "win32_window.h" + +// A window that does nothing but host a Flutter view. +class FlutterWindow : public Win32Window { + public: + // Creates a new FlutterWindow hosting a Flutter view running |project|. + explicit FlutterWindow(const flutter::DartProject& project); + virtual ~FlutterWindow(); + + protected: + // Win32Window: + bool OnCreate() override; + void OnDestroy() override; + LRESULT MessageHandler(HWND window, UINT const message, WPARAM const wparam, + LPARAM const lparam) noexcept override; + + private: + // The project to run. + flutter::DartProject project_; + + // The Flutter instance hosted by this window. + std::unique_ptr flutter_controller_; +}; + +#endif // RUNNER_FLUTTER_WINDOW_H_ diff --git a/flutter/windows/runner/main.cpp b/flutter/windows/runner/main.cpp new file mode 100644 index 000000000..f84fc1861 --- /dev/null +++ b/flutter/windows/runner/main.cpp @@ -0,0 +1,71 @@ +#include +#include +#include +#include + +#include "flutter_window.h" +#include "utils.h" +// #include + +typedef bool (*FUNC_RUSTDESK_CORE_MAIN)(void); + +// auto bdw = bitsdojo_window_configure(BDW_CUSTOM_FRAME | BDW_HIDE_ON_STARTUP); +int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, + _In_ wchar_t *command_line, _In_ int show_command) +{ + HINSTANCE hInstance = LoadLibraryA("librustdesk.dll"); + if (!hInstance) + { + std::cout << "Failed to load librustdesk.dll" << std::endl; + return EXIT_FAILURE; + } + FUNC_RUSTDESK_CORE_MAIN rustdesk_core_main = + (FUNC_RUSTDESK_CORE_MAIN)GetProcAddress(hInstance, "rustdesk_core_main"); + if (!rustdesk_core_main) + { + std::cout << "Failed to get rustdesk_core_main" << std::endl; + return EXIT_FAILURE; + } + if (!rustdesk_core_main()) + { + std::cout << "Rustdesk core returns false, exiting without launching Flutter app" << std::endl; + return EXIT_SUCCESS; + } + + // Attach to console when present (e.g., 'flutter run') or create a + // new console when running with a debugger. + if (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent()) + { + CreateAndAttachConsole(); + } + + // Initialize COM, so that it is available for use in the library and/or + // plugins. + ::CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED); + + flutter::DartProject project(L"data"); + + std::vector command_line_arguments = + GetCommandLineArguments(); + + project.set_dart_entrypoint_arguments(std::move(command_line_arguments)); + + FlutterWindow window(project); + Win32Window::Point origin(10, 10); + Win32Window::Size size(1280, 720); + if (!window.CreateAndShow(L"flutter_hbb", origin, size)) + { + return EXIT_FAILURE; + } + window.SetQuitOnClose(true); + + ::MSG msg; + while (::GetMessage(&msg, nullptr, 0, 0)) + { + ::TranslateMessage(&msg); + ::DispatchMessage(&msg); + } + + ::CoUninitialize(); + return EXIT_SUCCESS; +} diff --git a/flutter/windows/runner/resource.h b/flutter/windows/runner/resource.h new file mode 100644 index 000000000..66a65d1e4 --- /dev/null +++ b/flutter/windows/runner/resource.h @@ -0,0 +1,16 @@ +//{{NO_DEPENDENCIES}} +// Microsoft Visual C++ generated include file. +// Used by Runner.rc +// +#define IDI_APP_ICON 101 + +// Next default values for new objects +// +#ifdef APSTUDIO_INVOKED +#ifndef APSTUDIO_READONLY_SYMBOLS +#define _APS_NEXT_RESOURCE_VALUE 102 +#define _APS_NEXT_COMMAND_VALUE 40001 +#define _APS_NEXT_CONTROL_VALUE 1001 +#define _APS_NEXT_SYMED_VALUE 101 +#endif +#endif diff --git a/flutter/windows/runner/resources/app_icon.ico b/flutter/windows/runner/resources/app_icon.ico new file mode 100644 index 000000000..c04e20caf Binary files /dev/null and b/flutter/windows/runner/resources/app_icon.ico differ diff --git a/flutter/windows/runner/runner.exe.manifest b/flutter/windows/runner/runner.exe.manifest new file mode 100644 index 000000000..c977c4a42 --- /dev/null +++ b/flutter/windows/runner/runner.exe.manifest @@ -0,0 +1,20 @@ + + + + + PerMonitorV2 + + + + + + + + + + + + + + + diff --git a/flutter/windows/runner/utils.cpp b/flutter/windows/runner/utils.cpp new file mode 100644 index 000000000..f5bf9fa0f --- /dev/null +++ b/flutter/windows/runner/utils.cpp @@ -0,0 +1,64 @@ +#include "utils.h" + +#include +#include +#include +#include + +#include + +void CreateAndAttachConsole() { + if (::AllocConsole()) { + FILE *unused; + if (freopen_s(&unused, "CONOUT$", "w", stdout)) { + _dup2(_fileno(stdout), 1); + } + if (freopen_s(&unused, "CONOUT$", "w", stderr)) { + _dup2(_fileno(stdout), 2); + } + std::ios::sync_with_stdio(); + FlutterDesktopResyncOutputStreams(); + } +} + +std::vector GetCommandLineArguments() { + // Convert the UTF-16 command line arguments to UTF-8 for the Engine to use. + int argc; + wchar_t** argv = ::CommandLineToArgvW(::GetCommandLineW(), &argc); + if (argv == nullptr) { + return std::vector(); + } + + std::vector command_line_arguments; + + // Skip the first argument as it's the binary name. + for (int i = 1; i < argc; i++) { + command_line_arguments.push_back(Utf8FromUtf16(argv[i])); + } + + ::LocalFree(argv); + + return command_line_arguments; +} + +std::string Utf8FromUtf16(const wchar_t* utf16_string) { + if (utf16_string == nullptr) { + return std::string(); + } + int target_length = ::WideCharToMultiByte( + CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, + -1, nullptr, 0, nullptr, nullptr); + std::string utf8_string; + if (target_length == 0 || target_length > utf8_string.max_size()) { + return utf8_string; + } + utf8_string.resize(target_length); + int converted_length = ::WideCharToMultiByte( + CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, + -1, utf8_string.data(), + target_length, nullptr, nullptr); + if (converted_length == 0) { + return std::string(); + } + return utf8_string; +} diff --git a/flutter/windows/runner/utils.h b/flutter/windows/runner/utils.h new file mode 100644 index 000000000..3879d5475 --- /dev/null +++ b/flutter/windows/runner/utils.h @@ -0,0 +1,19 @@ +#ifndef RUNNER_UTILS_H_ +#define RUNNER_UTILS_H_ + +#include +#include + +// Creates a console for the process, and redirects stdout and stderr to +// it for both the runner and the Flutter library. +void CreateAndAttachConsole(); + +// Takes a null-terminated wchar_t* encoded in UTF-16 and returns a std::string +// encoded in UTF-8. Returns an empty std::string on failure. +std::string Utf8FromUtf16(const wchar_t* utf16_string); + +// Gets the command line arguments passed in as a std::vector, +// encoded in UTF-8. Returns an empty std::vector on failure. +std::vector GetCommandLineArguments(); + +#endif // RUNNER_UTILS_H_ diff --git a/flutter/windows/runner/win32_window.cpp b/flutter/windows/runner/win32_window.cpp new file mode 100644 index 000000000..c10f08dc7 --- /dev/null +++ b/flutter/windows/runner/win32_window.cpp @@ -0,0 +1,245 @@ +#include "win32_window.h" + +#include + +#include "resource.h" + +namespace { + +constexpr const wchar_t kWindowClassName[] = L"FLUTTER_RUNNER_WIN32_WINDOW"; + +// The number of Win32Window objects that currently exist. +static int g_active_window_count = 0; + +using EnableNonClientDpiScaling = BOOL __stdcall(HWND hwnd); + +// Scale helper to convert logical scaler values to physical using passed in +// scale factor +int Scale(int source, double scale_factor) { + return static_cast(source * scale_factor); +} + +// Dynamically loads the |EnableNonClientDpiScaling| from the User32 module. +// This API is only needed for PerMonitor V1 awareness mode. +void EnableFullDpiSupportIfAvailable(HWND hwnd) { + HMODULE user32_module = LoadLibraryA("User32.dll"); + if (!user32_module) { + return; + } + auto enable_non_client_dpi_scaling = + reinterpret_cast( + GetProcAddress(user32_module, "EnableNonClientDpiScaling")); + if (enable_non_client_dpi_scaling != nullptr) { + enable_non_client_dpi_scaling(hwnd); + FreeLibrary(user32_module); + } +} + +} // namespace + +// Manages the Win32Window's window class registration. +class WindowClassRegistrar { + public: + ~WindowClassRegistrar() = default; + + // Returns the singleton registar instance. + static WindowClassRegistrar* GetInstance() { + if (!instance_) { + instance_ = new WindowClassRegistrar(); + } + return instance_; + } + + // Returns the name of the window class, registering the class if it hasn't + // previously been registered. + const wchar_t* GetWindowClass(); + + // Unregisters the window class. Should only be called if there are no + // instances of the window. + void UnregisterWindowClass(); + + private: + WindowClassRegistrar() = default; + + static WindowClassRegistrar* instance_; + + bool class_registered_ = false; +}; + +WindowClassRegistrar* WindowClassRegistrar::instance_ = nullptr; + +const wchar_t* WindowClassRegistrar::GetWindowClass() { + if (!class_registered_) { + WNDCLASS window_class{}; + window_class.hCursor = LoadCursor(nullptr, IDC_ARROW); + window_class.lpszClassName = kWindowClassName; + window_class.style = CS_HREDRAW | CS_VREDRAW; + window_class.cbClsExtra = 0; + window_class.cbWndExtra = 0; + window_class.hInstance = GetModuleHandle(nullptr); + window_class.hIcon = + LoadIcon(window_class.hInstance, MAKEINTRESOURCE(IDI_APP_ICON)); + window_class.hbrBackground = 0; + window_class.lpszMenuName = nullptr; + window_class.lpfnWndProc = Win32Window::WndProc; + RegisterClass(&window_class); + class_registered_ = true; + } + return kWindowClassName; +} + +void WindowClassRegistrar::UnregisterWindowClass() { + UnregisterClass(kWindowClassName, nullptr); + class_registered_ = false; +} + +Win32Window::Win32Window() { + ++g_active_window_count; +} + +Win32Window::~Win32Window() { + --g_active_window_count; + Destroy(); +} + +bool Win32Window::CreateAndShow(const std::wstring& title, + const Point& origin, + const Size& size) { + Destroy(); + + const wchar_t* window_class = + WindowClassRegistrar::GetInstance()->GetWindowClass(); + + const POINT target_point = {static_cast(origin.x), + static_cast(origin.y)}; + HMONITOR monitor = MonitorFromPoint(target_point, MONITOR_DEFAULTTONEAREST); + UINT dpi = FlutterDesktopGetDpiForMonitor(monitor); + double scale_factor = dpi / 96.0; + + HWND window = CreateWindow( + window_class, title.c_str(), WS_OVERLAPPEDWINDOW | WS_VISIBLE, + Scale(origin.x, scale_factor), Scale(origin.y, scale_factor), + Scale(size.width, scale_factor), Scale(size.height, scale_factor), + nullptr, nullptr, GetModuleHandle(nullptr), this); + + if (!window) { + return false; + } + + return OnCreate(); +} + +// static +LRESULT CALLBACK Win32Window::WndProc(HWND const window, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + if (message == WM_NCCREATE) { + auto window_struct = reinterpret_cast(lparam); + SetWindowLongPtr(window, GWLP_USERDATA, + reinterpret_cast(window_struct->lpCreateParams)); + + auto that = static_cast(window_struct->lpCreateParams); + EnableFullDpiSupportIfAvailable(window); + that->window_handle_ = window; + } else if (Win32Window* that = GetThisFromHandle(window)) { + return that->MessageHandler(window, message, wparam, lparam); + } + + return DefWindowProc(window, message, wparam, lparam); +} + +LRESULT +Win32Window::MessageHandler(HWND hwnd, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + switch (message) { + case WM_DESTROY: + window_handle_ = nullptr; + Destroy(); + if (quit_on_close_) { + PostQuitMessage(0); + } + return 0; + + case WM_DPICHANGED: { + auto newRectSize = reinterpret_cast(lparam); + LONG newWidth = newRectSize->right - newRectSize->left; + LONG newHeight = newRectSize->bottom - newRectSize->top; + + SetWindowPos(hwnd, nullptr, newRectSize->left, newRectSize->top, newWidth, + newHeight, SWP_NOZORDER | SWP_NOACTIVATE); + + return 0; + } + case WM_SIZE: { + RECT rect = GetClientArea(); + if (child_content_ != nullptr) { + // Size and position the child window. + MoveWindow(child_content_, rect.left, rect.top, rect.right - rect.left, + rect.bottom - rect.top, TRUE); + } + return 0; + } + + case WM_ACTIVATE: + if (child_content_ != nullptr) { + SetFocus(child_content_); + } + return 0; + } + + return DefWindowProc(window_handle_, message, wparam, lparam); +} + +void Win32Window::Destroy() { + OnDestroy(); + + if (window_handle_) { + DestroyWindow(window_handle_); + window_handle_ = nullptr; + } + if (g_active_window_count == 0) { + WindowClassRegistrar::GetInstance()->UnregisterWindowClass(); + } +} + +Win32Window* Win32Window::GetThisFromHandle(HWND const window) noexcept { + return reinterpret_cast( + GetWindowLongPtr(window, GWLP_USERDATA)); +} + +void Win32Window::SetChildContent(HWND content) { + child_content_ = content; + SetParent(content, window_handle_); + RECT frame = GetClientArea(); + + MoveWindow(content, frame.left, frame.top, frame.right - frame.left, + frame.bottom - frame.top, true); + + SetFocus(child_content_); +} + +RECT Win32Window::GetClientArea() { + RECT frame; + GetClientRect(window_handle_, &frame); + return frame; +} + +HWND Win32Window::GetHandle() { + return window_handle_; +} + +void Win32Window::SetQuitOnClose(bool quit_on_close) { + quit_on_close_ = quit_on_close; +} + +bool Win32Window::OnCreate() { + // No-op; provided for subclasses. + return true; +} + +void Win32Window::OnDestroy() { + // No-op; provided for subclasses. +} diff --git a/flutter/windows/runner/win32_window.h b/flutter/windows/runner/win32_window.h new file mode 100644 index 000000000..17ba43112 --- /dev/null +++ b/flutter/windows/runner/win32_window.h @@ -0,0 +1,98 @@ +#ifndef RUNNER_WIN32_WINDOW_H_ +#define RUNNER_WIN32_WINDOW_H_ + +#include + +#include +#include +#include + +// A class abstraction for a high DPI-aware Win32 Window. Intended to be +// inherited from by classes that wish to specialize with custom +// rendering and input handling +class Win32Window { + public: + struct Point { + unsigned int x; + unsigned int y; + Point(unsigned int x, unsigned int y) : x(x), y(y) {} + }; + + struct Size { + unsigned int width; + unsigned int height; + Size(unsigned int width, unsigned int height) + : width(width), height(height) {} + }; + + Win32Window(); + virtual ~Win32Window(); + + // Creates and shows a win32 window with |title| and position and size using + // |origin| and |size|. New windows are created on the default monitor. Window + // sizes are specified to the OS in physical pixels, hence to ensure a + // consistent size to will treat the width height passed in to this function + // as logical pixels and scale to appropriate for the default monitor. Returns + // true if the window was created successfully. + bool CreateAndShow(const std::wstring& title, + const Point& origin, + const Size& size); + + // Release OS resources associated with window. + void Destroy(); + + // Inserts |content| into the window tree. + void SetChildContent(HWND content); + + // Returns the backing Window handle to enable clients to set icon and other + // window properties. Returns nullptr if the window has been destroyed. + HWND GetHandle(); + + // If true, closing this window will quit the application. + void SetQuitOnClose(bool quit_on_close); + + // Return a RECT representing the bounds of the current client area. + RECT GetClientArea(); + + protected: + // Processes and route salient window messages for mouse handling, + // size change and DPI. Delegates handling of these to member overloads that + // inheriting classes can handle. + virtual LRESULT MessageHandler(HWND window, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept; + + // Called when CreateAndShow is called, allowing subclass window-related + // setup. Subclasses should return false if setup fails. + virtual bool OnCreate(); + + // Called when Destroy is called. + virtual void OnDestroy(); + + private: + friend class WindowClassRegistrar; + + // OS callback called by message pump. Handles the WM_NCCREATE message which + // is passed when the non-client area is being created and enables automatic + // non-client DPI scaling so that the non-client area automatically + // responsponds to changes in DPI. All other messages are handled by + // MessageHandler. + static LRESULT CALLBACK WndProc(HWND const window, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept; + + // Retrieves a class instance pointer for |window| + static Win32Window* GetThisFromHandle(HWND const window) noexcept; + + bool quit_on_close_ = false; + + // window handle for top level window. + HWND window_handle_ = nullptr; + + // window handle for hosted content. + HWND child_content_ = nullptr; +}; + +#endif // RUNNER_WIN32_WINDOW_H_ diff --git a/libs/hbb_common/protos/rendezvous.proto b/libs/hbb_common/protos/rendezvous.proto index 2c5f1b3ba..1ac60f3f3 100644 --- a/libs/hbb_common/protos/rendezvous.proto +++ b/libs/hbb_common/protos/rendezvous.proto @@ -148,6 +148,15 @@ message PeerDiscovery { string misc = 7; } +message OnlineRequest { + string id = 1; + repeated string peers = 2; +} + +message OnlineResponse { + bytes states = 1; +} + message RendezvousMessage { oneof union { RegisterPeer register_peer = 6; @@ -167,5 +176,7 @@ message RendezvousMessage { TestNatRequest test_nat_request = 20; TestNatResponse test_nat_response = 21; PeerDiscovery peer_discovery = 22; + OnlineRequest online_request = 23; + OnlineResponse online_response = 24; } } diff --git a/libs/hbb_common/src/config.rs b/libs/hbb_common/src/config.rs index 26871a958..9354b4079 100644 --- a/libs/hbb_common/src/config.rs +++ b/libs/hbb_common/src/config.rs @@ -1,15 +1,3 @@ -use crate::{ - log, - password_security::{ - decrypt_str_or_original, decrypt_vec_or_original, encrypt_str_or_original, - encrypt_vec_or_original, - }, -}; -use anyhow::Result; -use directories_next::ProjectDirs; -use rand::Rng; -use serde_derive::{Deserialize, Serialize}; -use sodiumoxide::crypto::sign; use std::{ collections::HashMap, fs, @@ -19,8 +7,23 @@ use std::{ time::SystemTime, }; +use anyhow::Result; +use directories_next::ProjectDirs; +use rand::Rng; +use serde_derive::{Deserialize, Serialize}; +use sodiumoxide::crypto::sign; + +use crate::{ + log, + password_security::{ + decrypt_str_or_original, decrypt_vec_or_original, encrypt_str_or_original, + encrypt_vec_or_original, + }, +}; + pub const RENDEZVOUS_TIMEOUT: u64 = 12_000; pub const CONNECT_TIMEOUT: u64 = 18_000; +pub const READ_TIMEOUT: u64 = 30_000; pub const REG_INTERVAL: i64 = 12_000; pub const COMPRESS_LEVEL: i32 = 3; const SERIAL: i32 = 3; @@ -48,16 +51,10 @@ lazy_static::lazy_static! { pub static ref APP_NAME: Arc> = Arc::new(RwLock::new("RustDesk".to_owned())); static ref KEY_PAIR: Arc, Vec)>>> = Default::default(); } -#[cfg(target_os = "android")] -lazy_static::lazy_static! { - pub static ref APP_DIR: Arc> = Arc::new(RwLock::new("/data/user/0/com.carriez.flutter_hbb/app_flutter".to_owned())); -} -#[cfg(target_os = "ios")] + +// #[cfg(any(target_os = "android", target_os = "ios"))] lazy_static::lazy_static! { pub static ref APP_DIR: Arc> = Default::default(); -} -#[cfg(any(target_os = "android", target_os = "ios"))] -lazy_static::lazy_static! { pub static ref APP_HOME_DIR: Arc> = Default::default(); } const CHARS: &'static [char] = &[ diff --git a/libs/scrap/build.rs b/libs/scrap/build.rs index 93ea41ca7..b59dc03f3 100644 --- a/libs/scrap/build.rs +++ b/libs/scrap/build.rs @@ -3,9 +3,8 @@ use std::{ path::{Path, PathBuf}, }; -fn find_package(name: &str) -> Vec { - let vcpkg_root = std::env::var("VCPKG_ROOT").unwrap(); - let mut path: PathBuf = vcpkg_root.into(); +/// Link vcppkg package. +fn link_vcpkg(mut path: PathBuf, name: &str) -> PathBuf { let target_os = std::env::var("CARGO_CFG_TARGET_OS").unwrap(); let mut target_arch = std::env::var("CARGO_CFG_TARGET_ARCH").unwrap(); if target_arch == "x86_64" { @@ -26,8 +25,13 @@ fn find_package(name: &str) -> Vec { println!("cargo:info={}", target); path.push("installed"); path.push(target); - let lib = name.trim_start_matches("lib").to_string(); - println!("{}", format!("cargo:rustc-link-lib=static={}", lib)); + println!( + "{}", + format!( + "cargo:rustc-link-lib=static={}", + name.trim_start_matches("lib") + ) + ); println!( "{}", format!( @@ -37,7 +41,68 @@ fn find_package(name: &str) -> Vec { ); let include = path.join("include"); println!("{}", format!("cargo:include={}", include.to_str().unwrap())); - vec![include] + include +} + +/// Link homebrew package(for Mac M1). +fn link_homebrew_m1(name: &str) -> PathBuf { + let target_os = std::env::var("CARGO_CFG_TARGET_OS").unwrap(); + let target_arch = std::env::var("CARGO_CFG_TARGET_ARCH").unwrap(); + if target_os != "macos" || target_arch != "aarch64" { + panic!("Couldn't find VCPKG_ROOT, also can't fallback to homebrew because it's only for macos aarch64."); + } + let mut path = PathBuf::from("/opt/homebrew/Cellar"); + path.push(name); + let entries = if let Ok(dir) = std::fs::read_dir(&path) { + dir + } else { + panic!("Could not find package in {}. Make sure your homebrew and package {} are all installed.", path.to_str().unwrap(),&name); + }; + let mut directories = entries + .into_iter() + .filter(|x| x.is_ok()) + .map(|x| x.unwrap().path()) + .filter(|x| x.is_dir()) + .collect::>(); + // Find the newest version. + directories.sort_unstable(); + if directories.is_empty() { + panic!( + "There's no installed version of {} in /opt/homebrew/Cellar", + name + ); + } + path.push(directories.pop().unwrap()); + // Link the library. + println!( + "{}", + format!( + "cargo:rustc-link-lib=static={}", + name.trim_start_matches("lib") + ) + ); + // Add the library path. + println!( + "{}", + format!( + "cargo:rustc-link-search={}", + path.join("lib").to_str().unwrap() + ) + ); + // Add the include path. + let include = path.join("include"); + println!("{}", format!("cargo:include={}", include.to_str().unwrap())); + include +} + +/// Find package. By default, it will try to find vcpkg first, then homebrew(currently only for Mac M1). +fn find_package(name: &str) -> Vec { + if let Ok(vcpkg_root) = std::env::var("VCPKG_ROOT") { + vec![link_vcpkg(vcpkg_root.into(), name)] + } else { + // Try using homebrew + vec![link_homebrew_m1(name)] + } } fn generate_bindings( diff --git a/rpm-suse.spec b/rpm-suse.spec index 16c81ae90..73a610c11 100644 --- a/rpm-suse.spec +++ b/rpm-suse.spec @@ -23,7 +23,7 @@ mkdir -p %{buildroot}/usr/share/rustdesk/files/ install -m 755 $HBB/target/release/rustdesk %{buildroot}/usr/bin/rustdesk install $HBB/libsciter-gtk.so %{buildroot}/usr/lib/rustdesk/libsciter-gtk.so install $HBB/rustdesk.service %{buildroot}/usr/share/rustdesk/files/ -install $HBB/256-no-margin.png %{buildroot}/usr/share/rustdesk/files/rustdesk.png +install $HBB/128x128@2x.png %{buildroot}/usr/share/rustdesk/files/rustdesk.png install $HBB/rustdesk.desktop %{buildroot}/usr/share/rustdesk/files/ install $HBB/pynput_service.py %{buildroot}/usr/share/rustdesk/files/ diff --git a/rpm.spec b/rpm.spec index 707f0381a..c61db5d0b 100644 --- a/rpm.spec +++ b/rpm.spec @@ -23,7 +23,7 @@ mkdir -p %{buildroot}/usr/share/rustdesk/files/ install -m 755 $HBB/target/release/rustdesk %{buildroot}/usr/bin/rustdesk install $HBB/libsciter-gtk.so %{buildroot}/usr/lib/rustdesk/libsciter-gtk.so install $HBB/rustdesk.service %{buildroot}/usr/share/rustdesk/files/ -install $HBB/256-no-margin.png %{buildroot}/usr/share/rustdesk/files/rustdesk.png +install $HBB/128x128@2x.png %{buildroot}/usr/share/rustdesk/files/rustdesk.png install $HBB/rustdesk.desktop %{buildroot}/usr/share/rustdesk/files/ install $HBB/pynput_service.py %{buildroot}/usr/share/rustdesk/files/ diff --git a/src/client.rs b/src/client.rs index 6a5db19e2..d7f0bf4fa 100644 --- a/src/client.rs +++ b/src/client.rs @@ -12,19 +12,18 @@ use cpal::{ Device, Host, StreamConfig, }; use magnum_opus::{Channels::*, Decoder as AudioDecoder}; -use scrap::{ - codec::{Decoder, DecoderCfg}, - VpxDecoderConfig, VpxVideoCodecId, -}; - use sha2::{Digest, Sha256}; use uuid::Uuid; +pub use file_trait::FileManager; use hbb_common::{ allow_err, anyhow::{anyhow, Context}, bail, - config::{Config, PeerConfig, PeerInfoSerde, CONNECT_TIMEOUT, RELAY_PORT, RENDEZVOUS_TIMEOUT}, + config::{ + Config, PeerConfig, PeerInfoSerde, CONNECT_TIMEOUT, READ_TIMEOUT, RELAY_PORT, + RENDEZVOUS_TIMEOUT, + }, log, message_proto::{option_message::BoolOption, *}, protobuf::Message as _, @@ -36,14 +35,22 @@ use hbb_common::{ tokio::time::Duration, AddrMangle, ResultType, Stream, }; +pub use helper::LatencyController; +pub use helper::*; +use scrap::Image; +use scrap::{ + codec::{Decoder, DecoderCfg}, + VpxDecoderConfig, VpxVideoCodecId, +}; pub use super::lang::*; + pub mod file_trait; -pub use file_trait::FileManager; pub mod helper; -pub use helper::*; + pub const SEC30: Duration = Duration::from_secs(30); +/// Client of the remote desktop. pub struct Client; #[cfg(not(any(target_os = "android", target_os = "linux")))] @@ -111,13 +118,15 @@ impl Drop for OboePlayer { } impl Client { + /// Start a new connection. pub async fn start( peer: &str, key: &str, token: &str, conn_type: ConnType, + interface: impl Interface, ) -> ResultType<(Stream, bool)> { - match Self::_start(peer, key, token, conn_type).await { + match Self::_start(peer, key, token, conn_type, interface).await { Err(err) => { let err_str = err.to_string(); if err_str.starts_with("Failed") { @@ -130,11 +139,13 @@ impl Client { } } + /// Start a new connection. async fn _start( peer: &str, key: &str, token: &str, conn_type: ConnType, + interface: impl Interface, ) -> ResultType<(Stream, bool)> { // to-do: remember the port for each peer, so that we can retry easier let any_addr = Config::get_any_listen_addr(); @@ -181,7 +192,11 @@ impl Client { log::info!("#{} punch attempt with {}, id: {}", i, my_addr, peer); let mut msg_out = RendezvousMessage::new(); use hbb_common::protobuf::Enum; - let nat_type = NatType::from_i32(my_nat_type).unwrap_or(NatType::UNKNOWN_NAT); + let nat_type = if interface.is_force_relay() { + NatType::SYMMETRIC + } else { + NatType::from_i32(my_nat_type).unwrap_or(NatType::UNKNOWN_NAT) + }; msg_out.set_punch_hole_request(PunchHoleRequest { id: peer.to_owned(), token: token.to_owned(), @@ -233,7 +248,15 @@ impl Client { let mut conn = Self::create_relay(peer, rr.uuid, rr.relay_server, key, conn_type) .await?; - Self::secure_connection(peer, signed_id_pk, key, &mut conn).await?; + Self::secure_connection( + peer, + signed_id_pk, + key, + &mut conn, + false, + interface, + ) + .await?; return Ok((conn, false)); } _ => { @@ -274,10 +297,12 @@ impl Client { key, token, conn_type, + interface, ) .await } + /// Connect to the peer. async fn connect( local_addr: SocketAddr, peer: SocketAddr, @@ -292,6 +317,7 @@ impl Client { key: &str, token: &str, conn_type: ConnType, + interface: impl Interface, ) -> ResultType<(Stream, bool)> { let direct_failures = PeerConfig::load(peer_id).direct_failures; let mut connect_timeout = 0; @@ -329,8 +355,8 @@ impl Client { let start = std::time::Instant::now(); // NOTICE: Socks5 is be used event in intranet. Which may be not a good way. let mut conn = socket_client::connect_tcp(peer, local_addr, connect_timeout).await; - let direct = !conn.is_err(); - if conn.is_err() { + let mut direct = !conn.is_err(); + if interface.is_force_relay() || conn.is_err() { if !relay_server.is_empty() { conn = Self::request_relay( peer_id, @@ -348,6 +374,7 @@ impl Client { conn.err().unwrap() ); } + direct = false; } else { bail!("Failed to make direct connection to remote desktop"); } @@ -360,15 +387,18 @@ impl Client { } let mut conn = conn?; log::info!("{:?} used to establish connection", start.elapsed()); - Self::secure_connection(peer_id, signed_id_pk, key, &mut conn).await?; + Self::secure_connection(peer_id, signed_id_pk, key, &mut conn, direct, interface).await?; Ok((conn, direct)) } + /// Establish secure connection with the server. async fn secure_connection( peer_id: &str, signed_id_pk: Vec, key: &str, conn: &mut Stream, + direct: bool, + mut interface: impl Interface, ) -> ResultType<()> { let rs_pk = get_rs_pk(if key.is_empty() { hbb_common::config::RS_PUB_KEY @@ -394,9 +424,15 @@ impl Client { return Ok(()); } }; - match timeout(CONNECT_TIMEOUT, conn.next()).await? { + match timeout(READ_TIMEOUT, conn.next()).await? { Some(res) => { - let bytes = res?; + let bytes = match res { + Ok(bytes) => bytes, + Err(err) => { + interface.set_force_relay(direct, false); + bail!("{}", err); + } + }; if let Ok(msg_in) = Message::parse_from_bytes(&bytes) { if let Some(message::Union::SignedId(si)) = msg_in.union { if let Ok((id, their_pk_b)) = decode_id_pk(&si.id, &sign_pk) { @@ -441,6 +477,7 @@ impl Client { Ok(()) } + /// Request a relay connection to the server. async fn request_relay( peer: &str, relay_server: String, @@ -497,6 +534,7 @@ impl Client { Self::create_relay(peer, uuid, relay_server, key, conn_type).await } + /// Create a relay connection to the server. async fn create_relay( peer: &str, uuid: String, @@ -524,6 +562,7 @@ impl Client { } } +/// Audio handler for the [`Client`]. #[derive(Default)] pub struct AudioHandler { audio_decoder: Option<(AudioDecoder, Vec)>, @@ -541,6 +580,7 @@ pub struct AudioHandler { } impl AudioHandler { + /// Create a new audio handler. pub fn new(latency_controller: Arc>) -> Self { AudioHandler { latency_controller, @@ -548,6 +588,7 @@ impl AudioHandler { } } + /// Start the audio playback. #[cfg(target_os = "linux")] fn start_audio(&mut self, format0: AudioFormat) -> ResultType<()> { use psimple::Simple; @@ -577,6 +618,7 @@ impl AudioHandler { Ok(()) } + /// Start the audio playback. #[cfg(target_os = "android")] fn start_audio(&mut self, format0: AudioFormat) -> ResultType<()> { self.oboe = Some(OboePlayer::new( @@ -587,6 +629,7 @@ impl AudioHandler { Ok(()) } + /// Start the audio playback. #[cfg(not(any(target_os = "android", target_os = "linux")))] fn start_audio(&mut self, format0: AudioFormat) -> ResultType<()> { let device = AUDIO_HOST @@ -611,6 +654,7 @@ impl AudioHandler { Ok(()) } + /// Handle audio format and create an audio decoder. pub fn handle_format(&mut self, f: AudioFormat) { match AudioDecoder::new(f.sample_rate, if f.channels > 1 { Stereo } else { Mono }) { Ok(d) => { @@ -625,6 +669,7 @@ impl AudioHandler { } } + /// Handle audio frame and play it. pub fn handle_frame(&mut self, frame: AudioFrame) { if frame.timestamp != 0 { if self @@ -692,6 +737,7 @@ impl AudioHandler { }); } + /// Build audio output stream for current device. #[cfg(not(any(target_os = "android", target_os = "linux")))] fn build_output_stream( &mut self, @@ -727,6 +773,7 @@ impl AudioHandler { } } +/// Video handler for the [`Client`]. pub struct VideoHandler { decoder: Decoder, latency_controller: Arc>, @@ -734,6 +781,7 @@ pub struct VideoHandler { } impl VideoHandler { + /// Create a new video handler. pub fn new(latency_controller: Arc>) -> Self { VideoHandler { decoder: Decoder::new(DecoderCfg { @@ -747,8 +795,10 @@ impl VideoHandler { } } + /// Handle a new video frame. pub fn handle_frame(&mut self, vf: VideoFrame) -> ResultType { if vf.timestamp != 0 { + // Update the lantency controller with the latest timestamp. self.latency_controller .lock() .unwrap() @@ -760,6 +810,28 @@ impl VideoHandler { } } + /// Handle a VP9S frame. + // pub fn handle_vp9s(&mut self, vp9s: &VP9s) -> ResultType { + // let mut last_frame = Image::new(); + // for vp9 in vp9s.frames.iter() { + // for frame in self.decoder.decode(&vp9.data)? { + // drop(last_frame); + // last_frame = frame; + // } + // } + // for frame in self.decoder.flush()? { + // drop(last_frame); + // last_frame = frame; + // } + // if last_frame.is_null() { + // Ok(false) + // } else { + // last_frame.rgb(1, true, &mut self.rgb); + // Ok(true) + // } + // } + + /// Reset the decoder. pub fn reset(&mut self) { self.decoder = Decoder::new(DecoderCfg { vpx: VpxDecoderConfig { @@ -770,6 +842,7 @@ impl VideoHandler { } } +/// Login config handler for [`Client`]. #[derive(Default)] pub struct LoginConfigHandler { id: String, @@ -786,6 +859,7 @@ pub struct LoginConfigHandler { session_id: u64, pub supported_encoding: Option<(bool, bool)>, pub restarting_remote_device: bool, + pub force_relay: bool, } impl Deref for LoginConfigHandler { @@ -796,12 +870,24 @@ impl Deref for LoginConfigHandler { } } +/// Load [`PeerConfig`] from id. +/// +/// # Arguments +/// +/// * `id` - id of peer #[inline] pub fn load_config(id: &str) -> PeerConfig { PeerConfig::load(id) } impl LoginConfigHandler { + /// Initialize the login config handler. + /// + /// # Arguments + /// + /// * `id` - id of peer + /// * `is_file_transfer` - Whether the connection is file transfer. + /// * `is_port_forward` - Whether the connection is port forward. pub fn initialize(&mut self, id: String, is_file_transfer: bool, is_port_forward: bool) { self.id = id; self.is_file_transfer = is_file_transfer; @@ -812,8 +898,11 @@ impl LoginConfigHandler { self.session_id = rand::random(); self.supported_encoding = None; self.restarting_remote_device = false; + self.force_relay = !self.get_option("force-always-relay").is_empty(); } + /// Check if the client should auto login. + /// Return password if the client should auto login, otherwise return empty string. pub fn should_auto_login(&self) -> String { let l = self.lock_after_session_end; let a = !self.get_option("auto-login").is_empty(); @@ -825,27 +914,49 @@ impl LoginConfigHandler { } } + /// Load [`PeerConfig`]. fn load_config(&self) -> PeerConfig { load_config(&self.id) } + /// Save a [`PeerConfig`] into the handler. + /// + /// # Arguments + /// + /// * `config` - [`PeerConfig`] to save. pub fn save_config(&mut self, config: PeerConfig) { config.store(&self.id); self.config = config; } + /// Set an option for handler's [`PeerConfig`]. + /// + /// # Arguments + /// + /// * `k` - key of option + /// * `v` - value of option pub fn set_option(&mut self, k: String, v: String) { let mut config = self.load_config(); config.options.insert(k, v); self.save_config(config); } + /// Save view style to the current config. + /// + /// # Arguments + /// + /// * `value` - The view style to be saved. pub fn save_view_style(&mut self, value: String) { let mut config = self.load_config(); config.view_style = value; self.save_config(config); } + /// Toggle an option in the handler. + /// + /// # Arguments + /// + /// * `name` - The name of the option to toggle. pub fn toggle_option(&mut self, name: String) -> Option { let mut option = OptionMessage::default(); let mut config = self.load_config(); @@ -923,6 +1034,19 @@ impl LoginConfigHandler { Some(msg_out) } + /// Get [`PeerConfig`] of the current [`LoginConfigHandler`]. + /// + /// # Arguments + pub fn get_config(&mut self) -> &mut PeerConfig { + &mut self.config + } + + /// Get [`OptionMessage`] of the current [`LoginConfigHandler`]. + /// Return `None` if there's no option, for example, when the session is only for file transfer. + /// + /// # Arguments + /// + /// * `ignore_default` - If `true`, ignore the default value of the option. fn get_option_message(&self, ignore_default: bool) -> Option { if self.is_port_forward || self.is_file_transfer { return None; @@ -986,6 +1110,13 @@ impl LoginConfigHandler { } } + /// Parse the image quality option. + /// Return [`ImageQuality`] if the option is valid, otherwise return `None`. + /// + /// # Arguments + /// + /// * `q` - The image quality option. + /// * `ignore_default` - Ignore the default value. fn get_image_quality_enum(&self, q: &str, ignore_default: bool) -> Option { if q == "low" { Some(ImageQuality::Low) @@ -1002,6 +1133,11 @@ impl LoginConfigHandler { } } + /// Get the status of a toggle option. + /// + /// # Arguments + /// + /// * `name` - The name of the toggle option. pub fn get_toggle_option(&self, name: &str) -> bool { if name == "show-remote-cursor" { self.config.show_remote_cursor @@ -1030,6 +1166,7 @@ impl LoginConfigHandler { } } + /// Create a [`Message`] for refreshing video. pub fn refresh() -> Message { let mut misc = Misc::new(); misc.set_refresh_video(true); @@ -1038,6 +1175,12 @@ impl LoginConfigHandler { msg_out } + /// Create a [`Message`] for saving custom image quality. + /// + /// # Arguments + /// + /// * `bitrate` - The given bitrate. + /// * `quantizer` - The given quantizer. pub fn save_custom_image_quality(&mut self, image_quality: i32) -> Message { let mut misc = Misc::new(); misc.set_option(OptionMessage { @@ -1053,6 +1196,11 @@ impl LoginConfigHandler { msg_out } + /// Save the given image quality to the config. + /// Return a [`Message`] that contains image quality, or `None` if the image quality is not valid. + /// # Arguments + /// + /// * `value` - The image quality. pub fn save_image_quality(&mut self, value: String) -> Option { let mut res = None; if let Some(q) = self.get_image_quality_enum(&value, false) { @@ -1079,6 +1227,8 @@ impl LoginConfigHandler { } } + /// Handle login error. + /// Return true if the password is wrong, return false if there's an actual error. pub fn handle_login_error(&mut self, err: &str, interface: &impl Interface) -> bool { if err == "Wrong Password" { self.password = Default::default(); @@ -1090,6 +1240,12 @@ impl LoginConfigHandler { } } + /// Get user name. + /// Return the name of the given peer. If the peer has no name, return the name in the config. + /// + /// # Arguments + /// + /// * `pi` - peer info. pub fn get_username(&self, pi: &PeerInfo) -> String { return if pi.username.is_empty() { self.info.username.clone() @@ -1098,6 +1254,12 @@ impl LoginConfigHandler { }; } + /// Handle peer info. + /// + /// # Arguments + /// + /// * `username` - The name of the peer. + /// * `pi` - The peer info. pub fn handle_peer_info(&mut self, username: String, pi: PeerInfo) { if !pi.version.is_empty() { self.version = hbb_common::get_version_number(&pi.version); @@ -1152,9 +1314,10 @@ impl LoginConfigHandler { serde_json::to_string::>(&x).unwrap_or_default() } + /// Create a [`Message`] for login. fn create_login_msg(&self, password: Vec) -> Message { #[cfg(any(target_os = "android", target_os = "ios"))] - let my_id = Config::get_id_or(crate::common::MOBILE_INFO1.lock().unwrap().clone()); + let my_id = Config::get_id_or(crate::common::DEVICE_ID.lock().unwrap().clone()); #[cfg(not(any(target_os = "android", target_os = "ios")))] let my_id = Config::get_id(); let mut lr = LoginRequest { @@ -1206,6 +1369,7 @@ impl LoginConfigHandler { } } +/// Media data. pub enum MediaData { VideoFrame(VideoFrame), AudioFrame(AudioFrame), @@ -1215,6 +1379,12 @@ pub enum MediaData { pub type MediaSender = mpsc::Sender; +/// Start video and audio thread. +/// Return two [`MediaSender`], they should be given to the media producer. +/// +/// # Arguments +/// +/// * `video_callback` - The callback for video frame. Being called when a video frame is ready. pub fn start_video_audio_threads(video_callback: F) -> (MediaSender, MediaSender) where F: 'static + FnMut(&[u8]) + Send, @@ -1271,6 +1441,12 @@ where return (video_sender, audio_sender); } +/// Handle latency test. +/// +/// # Arguments +/// +/// * `t` - The latency test message. +/// * `peer` - The peer. pub async fn handle_test_delay(t: TestDelay, peer: &mut Stream) { if !t.from_client { let mut msg_out = Message::new(); @@ -1279,9 +1455,21 @@ pub async fn handle_test_delay(t: TestDelay, peer: &mut Stream) { } } -// mask = buttons << 3 | type -// type, 1: down, 2: up, 3: wheel -// buttons, 1: left, 2: right, 4: middle +/// Send mouse data. +/// +/// # Arguments +/// +/// * `mask` - Mouse event. +/// * mask = buttons << 3 | type +/// * type, 1: down, 2: up, 3: wheel +/// * buttons, 1: left, 2: right, 4: middle +/// * `x` - X coordinate. +/// * `y` - Y coordinate. +/// * `alt` - Whether the alt key is pressed. +/// * `ctrl` - Whether the ctrl key is pressed. +/// * `shift` - Whether the shift key is pressed. +/// * `command` - Whether the command key is pressed. +/// * `interface` - The interface for sending data. #[inline] pub fn send_mouse( mask: i32, @@ -1316,6 +1504,11 @@ pub fn send_mouse( interface.send(Data::Message(msg_out)); } +/// Avtivate OS by sending mouse movement. +/// +/// # Arguments +/// +/// * `interface` - The interface for sending data. fn activate_os(interface: &impl Interface) { send_mouse(0, 0, 0, false, false, false, false, interface); std::thread::sleep(Duration::from_millis(50)); @@ -1334,12 +1527,26 @@ fn activate_os(interface: &impl Interface) { */ } +/// Input the OS's password. +/// +/// # Arguments +/// +/// * `p` - The password. +/// * `avtivate` - Whether to activate OS. +/// * `interface` - The interface for sending data. pub fn input_os_password(p: String, activate: bool, interface: impl Interface) { std::thread::spawn(move || { _input_os_password(p, activate, interface); }); } +/// Input the OS's password. +/// +/// # Arguments +/// +/// * `p` - The password. +/// * `avtivate` - Whether to activate OS. +/// * `interface` - The interface for sending data. fn _input_os_password(p: String, activate: bool, interface: impl Interface) { if activate { activate_os(&interface); @@ -1356,6 +1563,15 @@ fn _input_os_password(p: String, activate: bool, interface: impl Interface) { interface.send(Data::Message(msg_out)); } +/// Handle hash message sent by peer. +/// Hash will be used for login. +/// +/// # Arguments +/// +/// * `lc` - Login config. +/// * `hash` - Hash sent by peer. +/// * `interface` - [`Interface`] for sending data. +/// * `peer` - [`Stream`] for communicating with peer. pub async fn handle_hash( lc: Arc>, password_preset: &str, @@ -1389,11 +1605,26 @@ pub async fn handle_hash( lc.write().unwrap().hash = hash; } +/// Send login message to peer. +/// +/// # Arguments +/// +/// * `lc` - Login config. +/// * `password` - Password. +/// * `peer` - [`Stream`] for communicating with peer. async fn send_login(lc: Arc>, password: Vec, peer: &mut Stream) { let msg_out = lc.read().unwrap().create_login_msg(password); allow_err!(peer.send(&msg_out).await); } +/// Handle login request made from ui. +/// +/// # Arguments +/// +/// * `lc` - Login config. +/// * `password` - Password. +/// * `remember` - Whether to remember password. +/// * `peer` - [`Stream`] for communicating with peer. pub async fn handle_login_from_ui( lc: Arc>, password: String, @@ -1412,24 +1643,28 @@ pub async fn handle_login_from_ui( send_login(lc.clone(), hasher2.finalize()[..].into(), peer).await; } +/// Interface for client to send data and commands. #[async_trait] pub trait Interface: Send + Clone + 'static + Sized { fn send(&self, data: Data); fn msgbox(&self, msgtype: &str, title: &str, text: &str); fn handle_login_error(&mut self, err: &str) -> bool; fn handle_peer_info(&mut self, pi: PeerInfo); + fn set_force_relay(&mut self, direct: bool, received: bool); + fn is_force_relay(&self) -> bool; async fn handle_hash(&mut self, pass: &str, hash: Hash, peer: &mut Stream); async fn handle_login_from_ui(&mut self, password: String, remember: bool, peer: &mut Stream); async fn handle_test_delay(&mut self, t: TestDelay, peer: &mut Stream); } +/// Data used by the client interface. #[derive(Clone)] pub enum Data { Close, Login((String, bool)), Message(Message), SendFiles((i32, String, String, i32, bool, bool)), - RemoveDirAll((i32, String, bool)), + RemoveDirAll((i32, String, bool, bool)), ConfirmDeleteFiles((i32, i32)), SetNoConfirm(i32), RemoveDir((i32, String)), @@ -1445,6 +1680,7 @@ pub enum Data { ResumeJob((i32, bool)), } +/// Keycode for key events. #[derive(Clone)] pub enum Key { ControlKey(ControlKey), @@ -1575,18 +1811,27 @@ lazy_static::lazy_static! { ].iter().cloned().collect(); } +/// Check if the given message is an error and can be retried. +/// +/// # Arguments +/// +/// * `msgtype` - The message type. +/// * `title` - The title of the message. +/// * `text` - The text of the message. #[inline] pub fn check_if_retry(msgtype: &str, title: &str, text: &str) -> bool { msgtype == "error" && title == "Connection Error" - && !text.to_lowercase().contains("offline") - && !text.to_lowercase().contains("exist") - && !text.to_lowercase().contains("handshake") - && !text.to_lowercase().contains("failed") - && !text.to_lowercase().contains("resolve") - && !text.to_lowercase().contains("mismatch") - && !text.to_lowercase().contains("manually") - && !text.to_lowercase().contains("not allowed") + && (text.contains("10054") + || text.contains("104") + || (!text.to_lowercase().contains("offline") + && !text.to_lowercase().contains("exist") + && !text.to_lowercase().contains("handshake") + && !text.to_lowercase().contains("failed") + && !text.to_lowercase().contains("resolve") + && !text.to_lowercase().contains("mismatch") + && !text.to_lowercase().contains("manually") + && !text.to_lowercase().contains("not allowed"))) } #[inline] diff --git a/src/client/file_trait.rs b/src/client/file_trait.rs index 9d107edb8..cc149c53f 100644 --- a/src/client/file_trait.rs +++ b/src/client/file_trait.rs @@ -1,16 +1,14 @@ +use hbb_common::{fs, message_proto::*}; + use super::{Data, Interface}; -use hbb_common::{ - fs, - message_proto::*, -}; pub trait FileManager: Interface { - fn get_home_dir(&self) -> String{ + fn get_home_dir(&self) -> String { fs::get_home_as_string() } #[cfg(not(any(target_os = "android", target_os = "ios", feature = "cli")))] - fn read_dir(&self,path: String, include_hidden: bool) -> sciter::Value { + fn read_dir(&self, path: String, include_hidden: bool) -> sciter::Value { match fs::read_dir(&fs::get_path(&path), include_hidden) { Err(_) => sciter::Value::null(), Ok(fd) => { @@ -23,15 +21,15 @@ pub trait FileManager: Interface { } #[cfg(any(target_os = "android", target_os = "ios", feature = "cli"))] - fn read_dir(&self,path: &str, include_hidden: bool) -> String { + fn read_dir(&self, path: &str, include_hidden: bool) -> String { use crate::common::make_fd_to_json; - match fs::read_dir(&fs::get_path(path), include_hidden){ + match fs::read_dir(&fs::get_path(path), include_hidden) { Ok(fd) => make_fd_to_json(fd), - Err(_)=>"".into() + Err(_) => "".into(), } } - fn cancel_job(&mut self, id: i32) { + fn cancel_job(&self, id: i32) { self.send(Data::CancelJob(id)); } @@ -47,23 +45,23 @@ pub trait FileManager: Interface { self.send(Data::Message(msg_out)); } - fn remove_file(&mut self, id: i32, path: String, file_num: i32, is_remote: bool) { + fn remove_file(&self, id: i32, path: String, file_num: i32, is_remote: bool) { self.send(Data::RemoveFile((id, path, file_num, is_remote))); } - fn remove_dir_all(&mut self, id: i32, path: String, is_remote: bool) { - self.send(Data::RemoveDirAll((id, path, is_remote))); + fn remove_dir_all(&self, id: i32, path: String, is_remote: bool, include_hidden: bool) { + self.send(Data::RemoveDirAll((id, path, is_remote, include_hidden))); } - fn confirm_delete_files(&mut self, id: i32, file_num: i32) { + fn confirm_delete_files(&self, id: i32, file_num: i32) { self.send(Data::ConfirmDeleteFiles((id, file_num))); } - fn set_no_confirm(&mut self, id: i32) { + fn set_no_confirm(&self, id: i32) { self.send(Data::SetNoConfirm(id)); } - fn remove_dir(&mut self, id: i32, path: String, is_remote: bool) { + fn remove_dir(&self, id: i32, path: String, is_remote: bool) { if is_remote { self.send(Data::RemoveDir((id, path))); } else { @@ -71,12 +69,12 @@ pub trait FileManager: Interface { } } - fn create_dir(&mut self, id: i32, path: String, is_remote: bool) { + fn create_dir(&self, id: i32, path: String, is_remote: bool) { self.send(Data::CreateDir((id, path, is_remote))); } fn send_files( - &mut self, + &self, id: i32, path: String, to: String, @@ -84,11 +82,18 @@ pub trait FileManager: Interface { include_hidden: bool, is_remote: bool, ) { - self.send(Data::SendFiles((id, path, to, file_num, include_hidden, is_remote))); + self.send(Data::SendFiles(( + id, + path, + to, + file_num, + include_hidden, + is_remote, + ))); } fn add_job( - &mut self, + &self, id: i32, path: String, to: String, @@ -96,10 +101,17 @@ pub trait FileManager: Interface { include_hidden: bool, is_remote: bool, ) { - self.send(Data::AddJob((id, path, to, file_num, include_hidden, is_remote))); + self.send(Data::AddJob(( + id, + path, + to, + file_num, + include_hidden, + is_remote, + ))); } - fn resume_job(&mut self, id: i32, is_remote: bool){ - self.send(Data::ResumeJob((id,is_remote))); + fn resume_job(&self, id: i32, is_remote: bool) { + self.send(Data::ResumeJob((id, is_remote))); } } diff --git a/src/client/helper.rs b/src/client/helper.rs index 5274a7c55..d38fbf223 100644 --- a/src/client/helper.rs +++ b/src/client/helper.rs @@ -11,8 +11,8 @@ use hbb_common::{ const MAX_LATENCY: i64 = 500; const MIN_LATENCY: i64 = 100; -// based on video frame time, fix audio latency relatively. -// only works on audio, can't fix video latency. +/// Latency controller for syncing audio with the video stream. +/// Only sync the audio to video, not the other way around. #[derive(Debug)] pub struct LatencyController { last_video_remote_ts: i64, // generated on remote deivce @@ -31,21 +31,23 @@ impl Default for LatencyController { } impl LatencyController { + /// Create a new latency controller. pub fn new() -> Arc> { Arc::new(Mutex::new(LatencyController::default())) } - // first, receive new video frame and update time + /// Update the latency controller with the latest video timestamp. pub fn update_video(&mut self, timestamp: i64) { self.last_video_remote_ts = timestamp; self.update_time = Instant::now(); } - // second, compute audio latency - // set MAX and MIN, avoid fixing too frequently. + /// Check if the audio should be played based on the current latency. pub fn check_audio(&mut self, timestamp: i64) -> bool { + // Compute audio latency. let expected = self.update_time.elapsed().as_millis() as i64 + self.last_video_remote_ts; let latency = expected - timestamp; + // Set MAX and MIN, avoid fixing too frequently. if self.allow_audio { if latency.abs() > MAX_LATENCY { log::debug!("LATENCY > {}ms cut off, latency:{}", MAX_LATENCY, latency); diff --git a/src/common.rs b/src/common.rs index 1cb540ca8..5c387c07e 100644 --- a/src/common.rs +++ b/src/common.rs @@ -1,5 +1,9 @@ +use std::sync::{Arc, Mutex}; + #[cfg(not(any(target_os = "android", target_os = "ios")))] pub use arboard::Clipboard as ClipboardContext; +use serde_json::json; + use hbb_common::{ allow_err, anyhow::bail, @@ -7,13 +11,13 @@ use hbb_common::{ config::{self, Config, COMPRESS_LEVEL, RENDEZVOUS_TIMEOUT}, get_version_number, log, message_proto::*, - protobuf::Message as _, protobuf::Enum, + protobuf::Message as _, rendezvous_proto::*, sleep, socket_client, tokio, ResultType, }; +// #[cfg(any(target_os = "android", target_os = "ios", feature = "cli"))] use hbb_common::{config::RENDEZVOUS_PORT, futures::future::join_all}; -use std::sync::{Arc, Mutex}; pub const CLIPBOARD_NAME: &'static str = "clipboard"; pub const CLIPBOARD_INTERVAL: u64 = 333; @@ -23,10 +27,9 @@ lazy_static::lazy_static! { pub static ref SOFTWARE_UPDATE_URL: Arc> = Default::default(); } -#[cfg(any(target_os = "android", target_os = "ios"))] lazy_static::lazy_static! { - pub static ref MOBILE_INFO1: Arc> = Default::default(); - pub static ref MOBILE_INFO2: Arc> = Default::default(); + pub static ref DEVICE_ID: Arc> = Default::default(); + pub static ref DEVICE_NAME: Arc> = Default::default(); } #[inline] @@ -48,7 +51,7 @@ pub fn create_clipboard_msg(content: String) -> Message { let mut msg = Message::new(); msg.set_clipboard(Clipboard { compress, - content:content.into(), + content: content.into(), ..Default::default() }); msg @@ -101,6 +104,19 @@ pub fn update_clipboard(clipboard: Clipboard, old: Option<&Arc>>) } } +pub async fn send_opts_after_login( + config: &crate::client::LoginConfigHandler, + peer: &mut hbb_common::tcp::FramedStream, +) { + if let Some(opts) = config.get_option_message_after_login() { + let mut misc = Misc::new(); + misc.set_option(opts); + let mut msg_out = Message::new(); + msg_out.set_misc(misc); + allow_err!(peer.send(&msg_out).await); + } +} + #[cfg(feature = "use_rubato")] pub fn resample_channels( data: &[f32], @@ -367,6 +383,7 @@ pub async fn get_nat_type(ms_timeout: u64) -> i32 { crate::ipc::get_nat_type(ms_timeout).await } +// #[cfg(any(target_os = "android", target_os = "ios", feature = "cli"))] #[tokio::main(flavor = "current_thread")] async fn test_rendezvous_server_() { let servers = Config::get_rendezvous_servers(); @@ -393,6 +410,7 @@ async fn test_rendezvous_server_() { join_all(futs).await; } +// #[cfg(any(target_os = "android", target_os = "ios", feature = "cli"))] pub fn test_rendezvous_server() { std::thread::spawn(test_rendezvous_server_); } @@ -436,7 +454,7 @@ pub fn username() -> String { #[cfg(not(any(target_os = "android", target_os = "ios")))] return whoami::username().trim_end_matches('\0').to_owned(); #[cfg(any(target_os = "android", target_os = "ios"))] - return MOBILE_INFO2.lock().unwrap().clone(); + return DEVICE_NAME.lock().unwrap().clone(); } #[inline] @@ -667,3 +685,30 @@ pub fn make_fd_to_json(fd: FileDirectory) -> String { fd_json.insert("entries".into(), json!(entries)); serde_json::to_string(&fd_json).unwrap_or("".into()) } + +pub fn make_fd_flutter(id: i32, entries: &Vec, only_count: bool) -> String { + let mut m = serde_json::Map::new(); + m.insert("id".into(), json!(id)); + let mut a = vec![]; + let mut n: u64 = 0; + for entry in entries { + n += entry.size; + if only_count { + continue; + } + let mut e = serde_json::Map::new(); + e.insert("name".into(), json!(entry.name.to_owned())); + let tmp = entry.entry_type.value(); + e.insert("type".into(), json!(if tmp == 0 { 1 } else { tmp })); + e.insert("time".into(), json!(entry.modified_time as f64)); + e.insert("size".into(), json!(entry.size as f64)); + a.push(e); + } + if only_count { + m.insert("num_entries".into(), json!(entries.len() as i32)); + } else { + m.insert("entries".into(), json!(a)); + } + m.insert("total_size".into(), json!(n as f64)); + serde_json::to_string(&m).unwrap_or("".into()) +} diff --git a/src/core_main.rs b/src/core_main.rs new file mode 100644 index 000000000..c780a1cb0 --- /dev/null +++ b/src/core_main.rs @@ -0,0 +1,28 @@ +use hbb_common::log; + +use crate::{start_os_service, flutter::connection_manager}; + +/// Main entry of the RustDesk Core. +/// Return true if the app should continue running with UI(possibly Flutter), false if the app should exit. +pub fn core_main() -> bool { + let args = std::env::args().collect::>(); + // TODO: implement core_main() + if args.len() > 1 { + if args[1] == "--cm" { + // call connection manager to establish connections + // meanwhile, return true to call flutter window to show control panel + connection_manager::start_listen_ipc_thread(); + return true; + } + if args[1] == "--service" { + log::info!("start --service"); + start_os_service(); + return false; + } + if args[1] == "--server" { + // TODO: server + return false; + } + } + true +} diff --git a/src/mobile.rs b/src/flutter.rs similarity index 60% rename from src/mobile.rs rename to src/flutter.rs index 44161189c..ca00807f1 100644 --- a/src/mobile.rs +++ b/src/flutter.rs @@ -1,6 +1,15 @@ -use crate::client::*; -use crate::common::make_fd_to_json; +use std::{ + collections::HashMap, + sync::{ + atomic::{AtomicBool, AtomicUsize, Ordering}, + Arc, Mutex, RwLock, + }, +}; + use flutter_rust_bridge::{StreamSink, ZeroCopyBuffer}; + +use hbb_common::config::{PeerConfig, TransferSerde}; +use hbb_common::fs::get_job; use hbb_common::{ allow_err, compress::decompress, @@ -21,175 +30,202 @@ use hbb_common::{ }, Stream, }; -use std::sync::atomic::{AtomicUsize, Ordering}; -use std::{ - collections::{HashMap, VecDeque}, - sync::{Arc, Mutex, RwLock}, -}; + +use crate::common::{self, make_fd_to_json, CLIPBOARD_INTERVAL}; + +#[cfg(not(any(target_os = "android", target_os = "ios")))] +use crate::common::{check_clipboard, update_clipboard, ClipboardContext}; + +use crate::{client::*, flutter_ffi::EventToUI, make_fd_flutter}; + +pub(super) const APP_TYPE_MAIN: &str = "main"; +pub(super) const APP_TYPE_DESKTOP_REMOTE: &str = "remote"; +pub(super) const APP_TYPE_DESKTOP_FILE_TRANSFER: &str = "file transfer"; lazy_static::lazy_static! { - static ref SESSION: Arc>> = Default::default(); - pub static ref EVENT_STREAM: RwLock>> = Default::default(); // rust to dart event channel - pub static ref RGBA_STREAM: RwLock>>>> = Default::default(); // rust to dart rgba (big u8 list) channel + // static ref SESSION: Arc>> = Default::default(); + pub static ref SESSIONS: RwLock> = Default::default(); + pub static ref GLOBAL_EVENT_STREAM: RwLock>> = Default::default(); // rust to dart event channel } -#[derive(Clone, Default)] +static SERVER_CLIPBOARD_ENABLED: AtomicBool = AtomicBool::new(true); +static SERVER_KEYBOARD_ENABLED: AtomicBool = AtomicBool::new(true); + +// pub fn get_session<'a>(id: &str) -> Option<&'a Session> { +// SESSIONS.read().unwrap().get(id) +// } + +#[derive(Clone)] pub struct Session { id: String, - sender: Arc>>>, + sender: Arc>>>, // UI to rust lc: Arc>, - events2ui: Arc>>, + events2ui: Arc>>, } impl Session { - pub fn start(id: &str, is_file_transfer: bool) { - LocalConfig::set_remote_id(id); - Self::close(); - let mut session = Session::default(); + /// Create a new remote session with the given id. + /// + /// # Arguments + /// + /// * `id` - The identifier of the remote session with prefix. Regex: [\w]*[\_]*[\d]+ + /// * `is_file_transfer` - If the session is used for file transfer. + pub fn start(identifier: &str, is_file_transfer: bool, events2ui: StreamSink) { + // TODO check same id + let session_id = get_session_id(identifier.to_owned()); + LocalConfig::set_remote_id(&session_id); + // TODO close + // Self::close(); + let events2ui = Arc::new(RwLock::new(events2ui)); + let session = Session { + id: session_id.clone(), + sender: Default::default(), + lc: Default::default(), + events2ui, + }; session .lc .write() .unwrap() - .initialize(id.to_owned(), false, false); - session.id = id.to_owned(); - *SESSION.write().unwrap() = Some(session.clone()); + .initialize(session_id.clone(), is_file_transfer, false); + SESSIONS + .write() + .unwrap() + .insert(identifier.to_owned(), session.clone()); std::thread::spawn(move || { Connection::start(session, is_file_transfer); }); } - pub fn get() -> Arc>> { - SESSION.clone() - } + /// Get the current session instance. + // pub fn get() -> Arc>> { + // SESSION.clone() + // } - pub fn get_option(name: &str) -> String { - if let Some(session) = SESSION.read().unwrap().as_ref() { - if name == "remote_dir" { - return session.lc.read().unwrap().get_remote_dir(); - } - return session.lc.read().unwrap().get_option(name); + /// Get the option of the current session. + /// + /// # Arguments + /// + /// * `name` - The name of the option to get. Currently only `remote_dir` is supported. + pub fn get_option(&self, name: &str) -> String { + if name == "remote_dir" { + return self.lc.read().unwrap().get_remote_dir(); } - "".to_owned() + self.lc.read().unwrap().get_option(name) } - pub fn set_option(name: String, value: String) { - if let Some(session) = SESSION.read().unwrap().as_ref() { - let mut value = value; - if name == "remote_dir" { - value = session.lc.write().unwrap().get_all_remote_dir(value); - } - return session.lc.write().unwrap().set_option(name, value); + /// Set the option of the current session. + /// + /// # Arguments + /// + /// * `name` - The name of the option to set. Currently only `remote_dir` is supported. + /// * `value` - The value of the option to set. + pub fn set_option(&self, name: String, value: String) { + let mut value = value; + let mut lc = self.lc.write().unwrap(); + if name == "remote_dir" { + value = lc.get_all_remote_dir(value); + } + lc.set_option(name, value); + } + + /// Input the OS password. + pub fn input_os_password(&self, pass: String, activate: bool) { + input_os_password(pass, activate, self.clone()); + } + + pub fn restart_remote_device(&self) { + let mut lc = self.lc.write().unwrap(); + lc.restarting_remote_device = true; + let msg = lc.restart_remote_device(); + self.send_msg(msg); + } + + /// Toggle an option. + pub fn toggle_option(&self, name: &str) { + let msg = self.lc.write().unwrap().toggle_option(name.to_owned()); + if let Some(msg) = msg { + self.send_msg(msg); } } - pub fn input_os_password(pass: String, activate: bool) { - if let Some(session) = SESSION.read().unwrap().as_ref() { - input_os_password(pass, activate, session.clone()); + /// Send a refresh command. + pub fn refresh(&self) { + self.send(Data::Message(LoginConfigHandler::refresh())); + } + + /// Get image quality. + pub fn get_image_quality(&self) -> String { + self.lc.read().unwrap().image_quality.clone() + } + + /// Set image quality. + pub fn set_image_quality(&self, value: &str) { + let msg = self + .lc + .write() + .unwrap() + .save_image_quality(value.to_owned()); + if let Some(msg) = msg { + self.send_msg(msg); } } - pub fn restart_remote_device() { - if let Some(session) = SESSION.write().unwrap().as_ref() { - let mut lc = session.lc.write().unwrap(); - lc.restarting_remote_device = true; - let msg = lc.restart_remote_device(); - session.send(Data::Message(msg)); - } + /// Get the status of a toggle option. + /// Return `None` if the option is not found. + /// + /// # Arguments + /// + /// * `name` - The name of the option to get. + pub fn get_toggle_option(&self, name: &str) -> bool { + self.lc.write().unwrap().get_toggle_option(name) } - fn send(data: Data) { - if let Some(session) = SESSION.read().unwrap().as_ref() { - session.send(data); - } + /// Login. + /// + /// # Arguments + /// + /// * `password` - The password to login. + /// * `remember` - If the password should be remembered. + pub fn login(&self, password: &str, remember: bool) { + self.send(Data::Login((password.to_owned(), remember))); } - pub fn pop_event() -> Option { - if let Some(session) = SESSION.read().unwrap().as_ref() { - session.events2ui.write().unwrap().pop_front() - } else { - None - } + /// Close the session. + pub fn close(&self) { + self.send(Data::Close); } - pub fn toggle_option(name: &str) { - if let Some(session) = SESSION.read().unwrap().as_ref() { - let msg = session.lc.write().unwrap().toggle_option(name.to_owned()); - if let Some(msg) = msg { - session.send_msg(msg); - } - } + /// Reconnect to the current session. + pub fn reconnect(&self) { + self.send(Data::Close); + let session = self.clone(); + std::thread::spawn(move || { + Connection::start(session, false); + }); } - pub fn refresh() { - Self::send(Data::Message(LoginConfigHandler::refresh())); - } - - pub fn get_image_quality() -> String { - if let Some(session) = SESSION.read().unwrap().as_ref() { - session.lc.read().unwrap().image_quality.clone() - } else { - "".to_owned() - } - } - - pub fn set_image_quality(value: &str) { - if let Some(session) = SESSION.read().unwrap().as_ref() { - let msg = session - .lc - .write() - .unwrap() - .save_image_quality(value.to_owned()); - if let Some(msg) = msg { - session.send_msg(msg); - } - } - } - - pub fn get_toggle_option(name: &str) -> Option { - if let Some(session) = SESSION.read().unwrap().as_ref() { - Some(session.lc.write().unwrap().get_toggle_option(name)) - } else { - None - } - } - - pub fn login(password: &str, remember: bool) { - Session::send(Data::Login((password.to_owned(), remember))); - } - - pub fn close() { - Session::send(Data::Close); - SESSION.write().unwrap().take(); - } - - pub fn reconnect() { - if let Some(session) = SESSION.read().unwrap().as_ref() { - if let Some(sender) = session.sender.read().unwrap().as_ref() { - sender.send(Data::Close).ok(); - } - let session = session.clone(); - std::thread::spawn(move || { - Connection::start(session, false); - }); - } - } - - pub fn get_remember() -> bool { - if let Some(session) = SESSION.read().unwrap().as_ref() { - session.lc.read().unwrap().remember - } else { - false - } + /// Get `remember` flag in [`LoginConfigHandler`]. + pub fn get_remember(&self) -> bool { + self.lc.read().unwrap().remember } + /// Send message over the current session. + /// + /// # Arguments + /// + /// * `msg` - The message to send. #[inline] pub fn send_msg(&self, msg: Message) { - if let Some(sender) = self.sender.read().unwrap().as_ref() { - sender.send(Data::Message(msg)).ok(); - } + self.send(Data::Message(msg)); } - pub fn send_chat(text: String) { + /// Send chat message over the current session. + /// + /// # Arguments + /// + /// * `text` - The message to send. + pub fn send_chat(&self, text: String) { let mut misc = Misc::new(); misc.set_chat_message(ChatMessage { text, @@ -197,84 +233,98 @@ impl Session { }); let mut msg_out = Message::new(); msg_out.set_misc(misc); - Self::send_msg_static(msg_out); + self.send_msg(msg_out); } - pub fn send_files( - id: i32, - path: String, - to: String, - file_num: i32, - include_hidden: bool, - is_remote: bool, - ) { - if let Some(session) = SESSION.write().unwrap().as_mut() { - session.send_files(id, path, to, file_num, include_hidden, is_remote); - } - } + // file trait + /// Send file over the current session. + // pub fn send_files( + // id: i32, + // path: String, + // to: String, + // file_num: i32, + // include_hidden: bool, + // is_remote: bool, + // ) { + // if let Some(session) = SESSION.write().unwrap().as_mut() { + // session.send_files(id, path, to, file_num, include_hidden, is_remote); + // } + // } + // TODO into file trait + /// Confirm file override. pub fn set_confirm_override_file( + &self, id: i32, file_num: i32, need_override: bool, remember: bool, is_upload: bool, ) { - if let Some(session) = SESSION.read().unwrap().as_ref() { - if let Some(sender) = session.sender.read().unwrap().as_ref() { - log::info!( - "confirm file transfer, job: {}, need_override: {}", - id, - need_override - ); - sender - .send(Data::SetConfirmOverrideFile(( - id, - file_num, - need_override, - remember, - is_upload, - ))) - .ok(); - } - } + log::info!( + "confirm file transfer, job: {}, need_override: {}", + id, + need_override + ); + self.send(Data::SetConfirmOverrideFile(( + id, + file_num, + need_override, + remember, + is_upload, + ))); } - #[inline] - pub fn send_msg_static(msg: Message) { - if let Some(session) = SESSION.read().unwrap().as_ref() { - session.send_msg(msg); - } - } + /// Static method to send message over the current session. + /// + /// # Arguments + /// + /// * `msg` - The message to send. + // #[inline] + // pub fn send_msg_static(msg: Message) { + // if let Some(session) = SESSION.read().unwrap().as_ref() { + // session.send_msg(msg); + // } + // } + /// Push an event to the event queue. + /// An event is stored as json in the event queue. + /// + /// # Arguments + /// + /// * `name` - The name of the event. + /// * `event` - Fields of the event content. fn push_event(&self, name: &str, event: Vec<(&str, &str)>) { let mut h: HashMap<&str, &str> = event.iter().cloned().collect(); assert!(h.get("name").is_none()); h.insert("name", name); - - if let Some(s) = EVENT_STREAM.read().unwrap().as_ref() { - s.add(serde_json::ser::to_string(&h).unwrap_or("".to_owned())); - }; + let out = serde_json::ser::to_string(&h).unwrap_or("".to_owned()); + self.events2ui.read().unwrap().add(EventToUI::Event(out)); } + /// Get platform of peer. #[inline] fn peer_platform(&self) -> String { self.lc.read().unwrap().info.platform.clone() } - pub fn ctrl_alt_del() { - if let Some(session) = SESSION.read().unwrap().as_ref() { - if session.peer_platform() == "Windows" { - let k = Key::ControlKey(ControlKey::CtrlAltDel); - session.key_down_or_up(1, k, false, false, false, false); - } else { - let k = Key::ControlKey(ControlKey::Delete); - session.key_down_or_up(3, k, true, true, false, false); - } + /// Quick method for sending a ctrl_alt_del command. + pub fn ctrl_alt_del(&self) { + if self.peer_platform() == "Windows" { + let k = Key::ControlKey(ControlKey::CtrlAltDel); + self.key_down_or_up(1, k, false, false, false, false); + } else { + let k = Key::ControlKey(ControlKey::Delete); + self.key_down_or_up(3, k, true, true, false, false); } } - pub fn switch_display(display: i32) { + /// Switch the display. + /// + /// # Arguments + /// + /// * `display` - The display to switch to. + pub fn switch_display(&self, display: i32) { let mut misc = Misc::new(); misc.set_switch_display(SwitchDisplay { display, @@ -282,17 +332,28 @@ impl Session { }); let mut msg_out = Message::new(); msg_out.set_misc(misc); - Self::send_msg_static(msg_out); + self.send_msg(msg_out); } - pub fn lock_screen() { - if let Some(session) = SESSION.read().unwrap().as_ref() { - let k = Key::ControlKey(ControlKey::LockScreen); - session.key_down_or_up(1, k, false, false, false, false); - } + /// Send lock screen command. + pub fn lock_screen(&self) { + let k = Key::ControlKey(ControlKey::LockScreen); + self.key_down_or_up(1, k, false, false, false, false); } + /// Send key input command. + /// + /// # Arguments + /// + /// * `name` - The name of the key. + /// * `down` - Whether the key is down or up. + /// * `press` - If the key is simply being pressed(Down+Up). + /// * `alt` - If the alt key is also pressed. + /// * `ctrl` - If the ctrl key is also pressed. + /// * `shift` - If the shift key is also pressed. + /// * `command` - If the command key is also pressed. pub fn input_key( + &self, name: &str, down: bool, press: bool, @@ -301,25 +362,29 @@ impl Session { shift: bool, command: bool, ) { - if let Some(session) = SESSION.read().unwrap().as_ref() { - let chars: Vec = name.chars().collect(); - if chars.len() == 1 { - let key = Key::_Raw(chars[0] as _); - session._input_key(key, down, press, alt, ctrl, shift, command); - } else { - if let Some(key) = KEY_MAP.get(name) { - session._input_key(key.clone(), down, press, alt, ctrl, shift, command); - } + let chars: Vec = name.chars().collect(); + if chars.len() == 1 { + let key = Key::_Raw(chars[0] as _); + self._input_key(key, down, press, alt, ctrl, shift, command); + } else { + if let Some(key) = KEY_MAP.get(name) { + self._input_key(key.clone(), down, press, alt, ctrl, shift, command); } } } - pub fn input_string(value: &str) { + /// Input a string of text. + /// String is parsed into individual key presses. + /// + /// # Arguments + /// + /// * `value` - The text to input. TODO &str -> String + pub fn input_string(&self, value: &str) { let mut key_event = KeyEvent::new(); key_event.set_seq(value.to_owned()); let mut msg_out = Message::new(); msg_out.set_key_event(key_event); - Self::send_msg_static(msg_out); + self.send_msg(msg_out); } fn _input_key( @@ -343,6 +408,7 @@ impl Session { } pub fn send_mouse( + &self, mask: i32, x: i32, y: i32, @@ -351,9 +417,7 @@ impl Session { shift: bool, command: bool, ) { - if let Some(session) = SESSION.read().unwrap().as_ref() { - send_mouse(mask, x, y, alt, ctrl, shift, command, session); - } + send_mouse(mask, x, y, alt, ctrl, shift, command, self); } fn key_down_or_up( @@ -387,7 +451,6 @@ impl Session { key_event.set_chr(raw); } } - _ => {} } if alt { key_event.modifiers.push(ControlKey::Alt.into()); @@ -412,6 +475,45 @@ impl Session { self.send_msg(msg_out); } + pub fn load_config(&self) -> PeerConfig { + load_config(&self.id) + } + + pub fn save_config(&self, config: &PeerConfig) { + config.store(&self.id); + } + + pub fn get_platform(&self, is_remote: bool) -> String { + if is_remote { + self.lc.read().unwrap().info.platform.clone() + } else { + whoami::platform().to_string() + } + } + + pub fn load_last_jobs(&self) { + let pc = self.load_config(); + if pc.transfer.write_jobs.is_empty() && pc.transfer.read_jobs.is_empty() { + // no last jobs + return; + } + let mut cnt = 1; + for job_str in pc.transfer.read_jobs.iter() { + if !job_str.is_empty() { + self.push_event("load_last_job", vec![("value", job_str)]); + cnt += 1; + println!("restore read_job: {:?}", job_str); + } + } + for job_str in pc.transfer.write_jobs.iter() { + if !job_str.is_empty() { + self.push_event("load_last_job", vec![("value", job_str)]); + cnt += 1; + println!("restore write_job: {:?}", job_str); + } + } + } + fn update_quality_status(&self, status: QualityStatus) { const NULL: String = String::new(); self.push_event( @@ -516,6 +618,24 @@ impl Interface for Session { } } + fn set_force_relay(&mut self, direct: bool, received: bool) { + let mut lc = self.lc.write().unwrap(); + lc.force_relay = false; + if direct && !received { + let errno = errno::errno().0; + log::info!("errno is {}", errno); + // TODO: check mac and ios + if cfg!(windows) && errno == 10054 || !cfg!(windows) && errno == 104 { + lc.force_relay = true; + lc.set_option("force-always-relay".to_owned(), "Y".to_owned()); + } + } + } + + fn is_force_relay(&self) -> bool { + self.lc.read().unwrap().force_relay + } + async fn handle_hash(&mut self, pass: &str, hash: Hash, peer: &mut Stream) { handle_hash(self.lc.clone(), pass, hash, self, peer).await; } @@ -553,10 +673,60 @@ struct Connection { } impl Connection { + // TODO: Similar to remote::start_clipboard + // merge the code + fn start_clipboard( + tx_protobuf: mpsc::UnboundedSender, + lc: Arc>, + ) -> Option> { + let (tx, rx) = std::sync::mpsc::channel(); + #[cfg(not(any(target_os = "android", target_os = "ios")))] + match ClipboardContext::new() { + Ok(mut ctx) => { + let old_clipboard: Arc> = Default::default(); + // ignore clipboard update before service start + check_clipboard(&mut ctx, Some(&old_clipboard)); + std::thread::spawn(move || loop { + std::thread::sleep(Duration::from_millis(CLIPBOARD_INTERVAL)); + match rx.try_recv() { + Ok(_) | Err(std::sync::mpsc::TryRecvError::Disconnected) => { + log::debug!("Exit clipboard service of client"); + break; + } + _ => {} + } + if !SERVER_CLIPBOARD_ENABLED.load(Ordering::SeqCst) + || !SERVER_KEYBOARD_ENABLED.load(Ordering::SeqCst) + || lc.read().unwrap().disable_clipboard + { + continue; + } + if let Some(msg) = check_clipboard(&mut ctx, Some(&old_clipboard)) { + tx_protobuf.send(Data::Message(msg)).ok(); + } + }); + } + Err(err) => { + log::error!("Failed to start clipboard service of client: {}", err); + } + } + Some(tx) + } + + /// Create a new connection. + /// + /// # Arguments + /// + /// * `session` - The session to create a new connection for. + /// * `is_file_transfer` - Whether the connection is for file transfer. #[tokio::main(flavor = "current_thread")] async fn start(session: Session, is_file_transfer: bool) { let mut last_recv_time = Instant::now(); let (sender, mut receiver) = mpsc::unbounded_channel::(); + let mut stop_clipboard = None; + if !is_file_transfer { + stop_clipboard = Self::start_clipboard(sender.clone(), session.lc.clone()); + } *session.sender.write().unwrap() = Some(sender); let conn_type = if is_file_transfer { session.lc.write().unwrap().is_file_transfer = true; @@ -583,8 +753,11 @@ impl Connection { let key = Config::get_option("key"); let token = Config::get_option("access_token"); - match Client::start(&session.id, &key, &token, conn_type).await { + match Client::start(&session.id, &key, &token, conn_type, session.clone()).await { Ok((mut peer, direct)) => { + SERVER_KEYBOARD_ENABLED.store(true, Ordering::SeqCst); + SERVER_CLIPBOARD_ENABLED.store(true, Ordering::SeqCst); + session.push_event( "connection_ready", vec![ @@ -638,7 +811,7 @@ impl Connection { } if !conn.read_jobs.is_empty() { if let Err(err) = fs::handle_read_jobs(&mut conn.read_jobs, &mut peer).await { - log::debug!("Connection Error"); + log::debug!("Connection Error: {}", err); break; } conn.update_jobs_status(); @@ -664,14 +837,25 @@ impl Connection { session.msgbox("error", "Connection Error", &err.to_string()); } } + + if let Some(stop) = stop_clipboard { + stop.send(()).ok(); + } + SERVER_KEYBOARD_ENABLED.store(false, Ordering::SeqCst); + SERVER_CLIPBOARD_ENABLED.store(false, Ordering::SeqCst); } + /// Handle message from peer. + /// Return false if the connection should be closed. + /// + /// The message is handled by [`Message`], see [`message::Union`] for possible types. async fn handle_msg_from_peer(&mut self, data: &[u8], peer: &mut Stream) -> bool { if let Ok(msg_in) = Message::parse_from_bytes(&data) { match msg_in.union { Some(message::Union::VideoFrame(vf)) => { if !self.first_frame { self.first_frame = true; + common::send_opts_after_login(&self.session.lc.read().unwrap(), peer).await; } let incomming_format = CodecFormat::from(&vf); if self.video_format != incomming_format { @@ -681,12 +865,12 @@ impl Connection { ..Default::default() }) }; - if let (Ok(true), Some(s)) = ( - self.video_handler.handle_frame(vf), - RGBA_STREAM.read().unwrap().as_ref(), - ) { + if let Ok(true) = self.video_handler.handle_frame(vf) { + let stream = self.session.events2ui.read().unwrap(); self.frame_count.fetch_add(1, Ordering::Relaxed); - s.add(ZeroCopyBuffer(self.video_handler.rgb.clone())); + stream.add(EventToUI::Rgba(ZeroCopyBuffer( + self.video_handler.rgb.clone(), + ))); } } Some(message::Union::Hash(hash)) => { @@ -748,7 +932,7 @@ impl Connection { Some(file_response::Union::Dir(fd)) => { let mut entries = fd.entries.to_vec(); if self.session.peer_platform() == "Windows" { - fs::transform_windows_path(&mut entries); + transform_windows_path(&mut entries); } let id = fd.id; self.session.push_event( @@ -914,6 +1098,11 @@ impl Connection { self.session.msgbox("error", "Connection Error", &c); return false; } + Some(misc::Union::BackNotification(notification)) => { + if !self.handle_back_notification(notification).await { + return false; + } + } _ => {} }, Some(message::Union::TestDelay(t)) => { @@ -938,9 +1127,134 @@ impl Connection { true } + async fn handle_back_notification(&mut self, notification: BackNotification) -> bool { + match notification.union { + Some(back_notification::Union::BlockInputState(state)) => { + self.handle_back_msg_block_input( + state.enum_value_or(back_notification::BlockInputState::BlkStateUnknown), + ) + .await; + } + Some(back_notification::Union::PrivacyModeState(state)) => { + if !self + .handle_back_msg_privacy_mode( + state.enum_value_or(back_notification::PrivacyModeState::PrvStateUnknown), + ) + .await + { + return false; + } + } + _ => {} + } + true + } + + #[inline(always)] + fn update_block_input_state(&mut self, on: bool) { + self.session.push_event( + "update_block_input_state", + [("input_state", if on { "on" } else { "off" })].into(), + ); + } + + async fn handle_back_msg_block_input(&mut self, state: back_notification::BlockInputState) { + match state { + back_notification::BlockInputState::BlkOnSucceeded => { + self.update_block_input_state(true); + } + back_notification::BlockInputState::BlkOnFailed => { + self.session + .msgbox("custom-error", "Block user input", "Failed"); + self.update_block_input_state(false); + } + back_notification::BlockInputState::BlkOffSucceeded => { + self.update_block_input_state(false); + } + back_notification::BlockInputState::BlkOffFailed => { + self.session + .msgbox("custom-error", "Unblock user input", "Failed"); + } + _ => {} + } + } + + #[inline(always)] + fn update_privacy_mode(&mut self, on: bool) { + let mut config = self.session.load_config(); + config.privacy_mode = on; + self.session.save_config(&config); + self.session.lc.write().unwrap().get_config().privacy_mode = on; + self.session.push_event("update_privacy_mode", [].into()); + } + + async fn handle_back_msg_privacy_mode( + &mut self, + state: back_notification::PrivacyModeState, + ) -> bool { + match state { + back_notification::PrivacyModeState::PrvOnByOther => { + self.session.msgbox( + "error", + "Connecting...", + "Someone turns on privacy mode, exit", + ); + return false; + } + back_notification::PrivacyModeState::PrvNotSupported => { + self.session + .msgbox("custom-error", "Privacy mode", "Unsupported"); + self.update_privacy_mode(false); + } + back_notification::PrivacyModeState::PrvOnSucceeded => { + self.session + .msgbox("custom-nocancel", "Privacy mode", "In privacy mode"); + self.update_privacy_mode(true); + } + back_notification::PrivacyModeState::PrvOnFailedDenied => { + self.session + .msgbox("custom-error", "Privacy mode", "Peer denied"); + self.update_privacy_mode(false); + } + back_notification::PrivacyModeState::PrvOnFailedPlugin => { + self.session + .msgbox("custom-error", "Privacy mode", "Please install plugins"); + self.update_privacy_mode(false); + } + back_notification::PrivacyModeState::PrvOnFailed => { + self.session + .msgbox("custom-error", "Privacy mode", "Failed"); + self.update_privacy_mode(false); + } + back_notification::PrivacyModeState::PrvOffSucceeded => { + self.session + .msgbox("custom-nocancel", "Privacy mode", "Out privacy mode"); + self.update_privacy_mode(false); + } + back_notification::PrivacyModeState::PrvOffByPeer => { + self.session + .msgbox("custom-error", "Privacy mode", "Peer exit"); + self.update_privacy_mode(false); + } + back_notification::PrivacyModeState::PrvOffFailed => { + self.session + .msgbox("custom-error", "Privacy mode", "Failed to turn off"); + } + back_notification::PrivacyModeState::PrvOffUnknown => { + self.session + .msgbox("custom-error", "Privacy mode", "Turned off"); + // log::error!("Privacy mode is turned off with unknown reason"); + self.update_privacy_mode(false); + } + _ => {} + } + true + } + async fn handle_msg_from_ui(&mut self, data: Data, peer: &mut Stream) -> bool { match data { Data::Close => { + self.sync_jobs_status_to_local().await; return false; } Data::Login((password, remember)) => { @@ -990,6 +1304,9 @@ impl Connection { to, job.files().len() ); + let m = make_fd_flutter(id, job.files(), true); + self.session + .push_event("update_folder_files", vec![("info", &m)]); let files = job.files().clone(); self.read_jobs.push(job); self.timer = time::interval(MILLI1); @@ -998,20 +1315,20 @@ impl Connection { } } } - Data::RemoveDirAll((id, path, is_remote)) => { + Data::RemoveDirAll((id, path, is_remote, include_hidden)) => { if is_remote { let mut msg_out = Message::new(); let mut file_action = FileAction::new(); file_action.set_all_files(ReadAllFiles { id, path: path.clone(), - include_hidden: true, + include_hidden, ..Default::default() }); msg_out.set_file_action(file_action); allow_err!(peer.send(&msg_out).await); } else { - match fs::get_recursive_files(&path, true) { + match fs::get_recursive_files(&path, include_hidden) { Ok(entries) => { let mut fd = FileDirectory::new(); fd.id = id; @@ -1139,6 +1456,87 @@ impl Connection { } } } + Data::AddJob((id, path, to, file_num, include_hidden, is_remote)) => { + let od = can_enable_overwrite_detection(self.session.lc.read().unwrap().version); + if is_remote { + log::debug!( + "new write waiting job {}, write to {} from remote {}", + id, + to, + path + ); + let mut job = fs::TransferJob::new_write( + id, + path.clone(), + to, + file_num, + include_hidden, + is_remote, + Vec::new(), + od, + ); + job.is_last_job = true; + self.write_jobs.push(job); + } else { + match fs::TransferJob::new_read( + id, + to.clone(), + path.clone(), + file_num, + include_hidden, + is_remote, + od, + ) { + Err(err) => { + self.handle_job_status(id, -1, Some(err.to_string())); + } + Ok(mut job) => { + log::debug!( + "new read waiting job {}, read {} to remote {}, {} files", + id, + path, + to, + job.files().len() + ); + let m = make_fd_flutter(job.id(), job.files(), true); + self.session + .push_event("update_folder_files", vec![("info", &m)]); + job.is_last_job = true; + self.read_jobs.push(job); + self.timer = time::interval(MILLI1); + } + } + } + } + Data::ResumeJob((id, is_remote)) => { + if is_remote { + if let Some(job) = get_job(id, &mut self.write_jobs) { + job.is_last_job = false; + allow_err!( + peer.send(&fs::new_send( + id, + job.remote.clone(), + job.file_num, + job.show_hidden + )) + .await + ); + } + } else { + if let Some(job) = get_job(id, &mut self.read_jobs) { + job.is_last_job = false; + allow_err!( + peer.send(&fs::new_receive( + id, + job.path.to_string_lossy().to_string(), + job.file_num, + job.files.clone() + )) + .await + ); + } + } + } _ => {} } true @@ -1228,22 +1626,41 @@ impl Connection { ], ); } + + async fn sync_jobs_status_to_local(&mut self) -> bool { + log::info!("sync transfer job status"); + let mut config: PeerConfig = self.session.load_config(); + let mut transfer_metas = TransferSerde::default(); + for job in self.read_jobs.iter() { + let json_str = serde_json::to_string(&job.gen_meta()).unwrap(); + transfer_metas.read_jobs.push(json_str); + } + for job in self.write_jobs.iter() { + let json_str = serde_json::to_string(&job.gen_meta()).unwrap(); + transfer_metas.write_jobs.push(json_str); + } + log::info!("meta: {:?}", transfer_metas); + config.transfer = transfer_metas; + self.session.save_config(&config); + true + } } // Server Side // TODO connection_manager need use struct and trait,impl default method -#[cfg(target_os = "android")] +#[cfg(not(any(target_os = "ios")))] pub mod connection_manager { use std::{ collections::HashMap, iter::FromIterator, - rc::{Rc, Weak}, - sync::{Mutex, RwLock}, + sync::{ + atomic::{AtomicI64, Ordering}, + RwLock, + }, }; - use crate::ipc; - use crate::ipc::Data; - use crate::server::Connection as Conn; + use serde_derive::Serialize; + use hbb_common::{ allow_err, config::Config, @@ -1254,14 +1671,17 @@ pub mod connection_manager { protobuf::Message as _, tokio::{ self, - sync::mpsc::{UnboundedReceiver, UnboundedSender}, + sync::mpsc::{self, UnboundedReceiver, UnboundedSender}, task::spawn_blocking, }, }; + #[cfg(any(target_os = "android"))] use scrap::android::call_main_service_set_by_name; - use serde_derive::Serialize; - use super::EVENT_STREAM; + use crate::ipc::Data; + use crate::ipc::{self, new_listener, Connection}; + + use super::GLOBAL_EVENT_STREAM; #[derive(Debug, Serialize, Clone)] struct Client { @@ -1273,80 +1693,195 @@ pub mod connection_manager { keyboard: bool, clipboard: bool, audio: bool, + file: bool, + restart: bool, #[serde(skip)] tx: UnboundedSender, } lazy_static::lazy_static! { static ref CLIENTS: RwLock> = Default::default(); - static ref WRITE_JOBS: Mutex> = Mutex::new(Vec::new()); } + static CLICK_TIME: AtomicI64 = AtomicI64::new(0); + + // // TODO clipboard_file + // enum ClipboardFileData { + // #[cfg(windows)] + // Clip((i32, ipc::ClipbaordFile)), + // Enable((i32, bool)), + // } + + #[cfg(not(any(target_os = "android", target_os = "ios")))] + pub fn start_listen_ipc_thread() { + std::thread::spawn(move || start_ipc()); + } + + #[cfg(not(any(target_os = "android", target_os = "ios")))] + #[tokio::main(flavor = "current_thread")] + async fn start_ipc() { + // TODO clipboard_file + // let (tx_file, _rx_file) = mpsc::unbounded_channel::(); + // #[cfg(windows)] + // let cm_clip = cm.clone(); + // #[cfg(windows)] + // std::thread::spawn(move || start_clipboard_file(cm_clip, _rx_file)); + + #[cfg(windows)] + std::thread::spawn(move || { + log::info!("try create privacy mode window"); + #[cfg(windows)] + { + if let Err(e) = crate::platform::windows::check_update_broker_process() { + log::warn!( + "Failed to check update broker process. Privacy mode may not work properly. {}", + e + ); + } + } + allow_err!(crate::ui::win_privacy::start()); + }); + + match new_listener("_cm").await { + Ok(mut incoming) => { + while let Some(result) = incoming.next().await { + match result { + Ok(stream) => { + log::debug!("Got new connection"); + let mut stream = Connection::new(stream); + // let tx_file = tx_file.clone(); + tokio::spawn(async move { + // for tmp use, without real conn id + let conn_id_tmp = -1; + let mut conn_id: i32 = 0; + let (tx, mut rx) = mpsc::unbounded_channel::(); + let mut write_jobs: Vec = Vec::new(); + loop { + tokio::select! { + res = stream.next() => { + match res { + Err(err) => { + log::info!("cm ipc connection closed: {}", err); + break; + } + Ok(Some(data)) => { + match data { + Data::Login{id, is_file_transfer, port_forward, peer_id, name, authorized, keyboard, clipboard, audio, file, file_transfer_enabled, restart} => { + log::debug!("conn_id: {}", id); + conn_id = id; + // tx_file.send(ClipboardFileData::Enable((id, file_transfer_enabled))).ok(); + on_login(id, is_file_transfer, port_forward, peer_id, name, authorized, keyboard, clipboard, audio, file, restart, tx.clone()); + } + Data::Close => { + // tx_file.send(ClipboardFileData::Enable((conn_id, false))).ok(); + log::info!("cm ipc connection closed from connection request"); + break; + } + Data::PrivacyModeState((_, _)) => { + conn_id = conn_id_tmp; + allow_err!(tx.send(data)); + } + Data::ClickTime(ms) => { + CLICK_TIME.store(ms, Ordering::SeqCst); + } + Data::ChatMessage { text } => { + handle_chat(conn_id, text); + } + Data::FS(fs) => { + handle_fs(fs, &mut write_jobs, &tx).await; + } + // TODO ClipbaordFile + // #[cfg(windows)] + // Data::ClipbaordFile(_clip) => { + // tx_file + // .send(ClipboardFileData::Clip((id, _clip))) + // .ok(); + // } + // #[cfg(windows)] + // Data::ClipboardFileEnabled(enabled) => { + // tx_file + // .send(ClipboardFileData::Enable((id, enabled))) + // .ok(); + // } + _ => {} + } + } + _ => {} + } + } + Some(data) = rx.recv() => { + if stream.send(&data).await.is_err() { + break; + } + } + } + } + if conn_id != conn_id_tmp { + remove_connection(conn_id); + } + }); + } + Err(err) => { + log::error!("Couldn't get cm client: {:?}", err); + } + } + } + } + Err(err) => { + log::error!("Failed to start cm ipc server: {}", err); + } + } + // crate::platform::quit_gui(); + // TODO flutter quit_gui + } + + #[cfg(target_os = "android")] pub fn start_channel(rx: UnboundedReceiver, tx: UnboundedSender) { std::thread::spawn(move || start_listen(rx, tx)); } + #[cfg(target_os = "android")] #[tokio::main(flavor = "current_thread")] async fn start_listen(mut rx: UnboundedReceiver, tx: UnboundedSender) { let mut current_id = 0; + let mut write_jobs: Vec = Vec::new(); loop { match rx.recv().await { Some(Data::Login { id, is_file_transfer, + port_forward, peer_id, name, authorized, keyboard, clipboard, audio, + file, + restart, .. }) => { current_id = id; - let mut client = Client { + on_login( id, - authorized, is_file_transfer, - name: name.clone(), - peer_id: peer_id.clone(), + port_forward, + peer_id, + name, + authorized, keyboard, clipboard, audio, - tx: tx.clone(), - }; - if authorized { - client.authorized = true; - let client_json = serde_json::to_string(&client).unwrap_or("".into()); - // send to Android service,active notification no matter UI is shown or not. - if let Err(e) = call_main_service_set_by_name( - "on_client_authorized", - Some(&client_json), - None, - ) { - log::debug!("call_service_set_by_name fail,{}", e); - } - // send to UI,refresh widget - push_event("on_client_authorized", vec![("client", &client_json)]); - } else { - let client_json = serde_json::to_string(&client).unwrap_or("".into()); - // send to Android service,active notification no matter UI is shown or not. - if let Err(e) = call_main_service_set_by_name( - "try_start_without_auth", - Some(&client_json), - None, - ) { - log::debug!("call_service_set_by_name fail,{}", e); - } - // send to UI,refresh widget - push_event("try_start_without_auth", vec![("client", &client_json)]); - } - CLIENTS.write().unwrap().insert(id, client); + file, + restart, + tx.clone(), + ); } Some(Data::ChatMessage { text }) => { handle_chat(current_id, text); } Some(Data::FS(fs)) => { - handle_fs(fs, &tx).await; + handle_fs(fs, &mut write_jobs, &tx).await; } Some(Data::Close) => { break; @@ -1360,16 +1895,90 @@ pub mod connection_manager { remove_connection(current_id); } + fn on_login( + id: i32, + is_file_transfer: bool, + _port_forward: String, + peer_id: String, + name: String, + authorized: bool, + keyboard: bool, + clipboard: bool, + audio: bool, + file: bool, + restart: bool, + tx: mpsc::UnboundedSender, + ) { + let mut client = Client { + id, + authorized, + is_file_transfer, + name: name.clone(), + peer_id: peer_id.clone(), + keyboard, + clipboard, + audio, + file, + restart, + tx, + }; + if authorized { + client.authorized = true; + let client_json = serde_json::to_string(&client).unwrap_or("".into()); + // send to Android service, active notification no matter UI is shown or not. + #[cfg(any(target_os = "android"))] + if let Err(e) = + call_main_service_set_by_name("on_client_authorized", Some(&client_json), None) + { + log::debug!("call_service_set_by_name fail,{}", e); + } + // send to UI, refresh widget + push_event("on_client_authorized", vec![("client", &client_json)]); + } else { + let client_json = serde_json::to_string(&client).unwrap_or("".into()); + // send to Android service, active notification no matter UI is shown or not. + #[cfg(any(target_os = "android"))] + if let Err(e) = + call_main_service_set_by_name("try_start_without_auth", Some(&client_json), None) + { + log::debug!("call_service_set_by_name fail,{}", e); + } + // send to UI, refresh widget + push_event("try_start_without_auth", vec![("client", &client_json)]); + } + CLIENTS.write().unwrap().insert(id, client); + } + fn push_event(name: &str, event: Vec<(&str, &str)>) { let mut h: HashMap<&str, &str> = event.iter().cloned().collect(); assert!(h.get("name").is_none()); h.insert("name", name); - if let Some(s) = EVENT_STREAM.read().unwrap().as_ref() { + if let Some(s) = GLOBAL_EVENT_STREAM + .read() + .unwrap() + .get(super::APP_TYPE_MAIN) + { s.add(serde_json::ser::to_string(&h).unwrap_or("".to_owned())); }; } + pub fn get_click_time() -> i64 { + CLICK_TIME.load(Ordering::SeqCst) + } + + pub fn check_click_time(id: i32) { + if let Some(client) = CLIENTS.read().unwrap().get(&id) { + allow_err!(client.tx.send(Data::ClickTime(0))); + }; + } + + pub fn switch_permission(id: i32, name: String, enabled: bool) { + if let Some(client) = CLIENTS.read().unwrap().get(&id) { + allow_err!(client.tx.send(Data::SwitchPermission { name, enabled })); + }; + } + pub fn get_clients_state() -> String { let clients = CLIENTS.read().unwrap(); let res = Vec::from_iter(clients.values().cloned()); @@ -1382,7 +1991,7 @@ pub mod connection_manager { } pub fn close_conn(id: i32) { - if let Some(client) = CLIENTS.write().unwrap().get(&id) { + if let Some(client) = CLIENTS.read().unwrap().get(&id) { allow_err!(client.tx.send(Data::Close)); }; } @@ -1404,10 +2013,11 @@ pub mod connection_manager { if clients .iter() - .filter(|(k, v)| !v.is_file_transfer) + .filter(|(_k, v)| !v.is_file_transfer) .next() .is_none() { + #[cfg(any(target_os = "android"))] if let Err(e) = call_main_service_set_by_name("stop_capture", None, None) { log::debug!("stop_capture err:{}", e); } @@ -1426,14 +2036,18 @@ pub mod connection_manager { // server mode send chat to peer pub fn send_chat(id: i32, text: String) { - let mut clients = CLIENTS.read().unwrap(); + let clients = CLIENTS.read().unwrap(); if let Some(client) = clients.get(&id) { allow_err!(client.tx.send(Data::ChatMessage { text })); } } // handle FS server - async fn handle_fs(fs: ipc::FS, tx: &UnboundedSender) { + async fn handle_fs( + fs: ipc::FS, + write_jobs: &mut Vec, + tx: &UnboundedSender, + ) { match fs { ipc::FS::ReadDir { dir, @@ -1459,9 +2073,9 @@ pub mod connection_manager { id, file_num, mut files, - overwrite_detection + overwrite_detection, } => { - WRITE_JOBS.lock().unwrap().push(fs::TransferJob::new_write( + write_jobs.push(fs::TransferJob::new_write( id, "".to_string(), path, @@ -1476,18 +2090,16 @@ pub mod connection_manager { ..Default::default() }) .collect(), - overwrite_detection + overwrite_detection, )); } ipc::FS::CancelWrite { id } => { - let write_jobs = &mut *WRITE_JOBS.lock().unwrap(); if let Some(job) = fs::get_job(id, write_jobs) { job.remove_download_file(); fs::remove_job(id, write_jobs); } } ipc::FS::WriteDone { id, file_num } => { - let write_jobs = &mut *WRITE_JOBS.lock().unwrap(); if let Some(job) = fs::get_job(id, write_jobs) { job.modify_time(); send_raw(fs::new_done(id, file_num), tx); @@ -1500,7 +2112,7 @@ pub mod connection_manager { data, compressed, } => { - if let Some(job) = fs::get_job(id, &mut *WRITE_JOBS.lock().unwrap()) { + if let Some(job) = fs::get_job(id, write_jobs) { if let Err(err) = job .write( FileTransferBlock { @@ -1525,7 +2137,7 @@ pub mod connection_manager { last_modified, is_upload, } => { - if let Some(job) = fs::get_job(id, &mut *WRITE_JOBS.lock().unwrap()) { + if let Some(job) = fs::get_job(id, write_jobs) { let mut req = FileTransferSendConfirmRequest { id, file_num, @@ -1658,3 +2270,12 @@ pub mod connection_manager { } } } + +#[inline] +pub fn get_session_id(id: String) -> String { + return if let Some(index) = id.find('_') { + id[index + 1..].to_string() + } else { + id + }; +} diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs new file mode 100644 index 000000000..aa46e4faf --- /dev/null +++ b/src/flutter_ffi.rs @@ -0,0 +1,849 @@ +use std::{ + collections::HashMap, + ffi::{CStr, CString}, + os::raw::c_char, +}; + +use flutter_rust_bridge::{StreamSink, SyncReturn, ZeroCopyBuffer}; +use serde_json::{json, Number, Value}; + +use hbb_common::{ + config::{self, Config, LocalConfig, PeerConfig, ONLINE}, + fs, log, +}; +use hbb_common::{password_security, ResultType}; + +use crate::client::file_trait::FileManager; +use crate::common::make_fd_to_json; +use crate::flutter::connection_manager::{self, get_clients_length, get_clients_state}; +use crate::flutter::{self, Session, SESSIONS}; +use crate::start_server; +use crate::ui_interface; +#[cfg(not(any(target_os = "android", target_os = "ios")))] +use crate::ui_interface::{change_id, check_connect_status, is_ok_change_id}; +use crate::ui_interface::{ + check_super_user_permission, discover, forget_password, get_api_server, get_app_name, + get_async_job_status, get_connect_status, get_fav, get_id, get_lan_peers, get_langs, + get_license, get_local_option, get_option, get_options, get_peer, get_peer_option, get_socks, + get_sound_inputs, get_uuid, get_version, has_hwcodec, has_rendezvous_service, post_request, + set_local_option, set_option, set_options, set_peer_option, set_permanent_password, set_socks, + store_fav, test_if_valid_server, update_temporary_password, using_public_server, +}; + +fn initialize(app_dir: &str) { + *config::APP_DIR.write().unwrap() = app_dir.to_owned(); + #[cfg(feature = "cli")] + { + #[cfg(any(target_os = "android", target_os = "ios"))] + { + crate::common::test_rendezvous_server(); + crate::common::test_nat_type(); + } + } + #[cfg(target_os = "android")] + { + android_logger::init_once( + android_logger::Config::default() + .with_min_level(log::Level::Debug) // limit log level + .with_tag("ffi"), // logs will show under mytag tag + ); + } + #[cfg(target_os = "ios")] + { + use hbb_common::env_logger::*; + init_from_env(Env::default().filter_or(DEFAULT_FILTER_ENV, "debug")); + } + #[cfg(target_os = "android")] + { + crate::common::check_software_update(); + } + #[cfg(any(target_os = "windows", target_os = "linux", target_os = "macos"))] + { + use hbb_common::env_logger::*; + if let Err(e) = try_init_from_env(Env::default().filter_or(DEFAULT_FILTER_ENV, "debug")) { + log::debug!("{}", e); + } + } +} + +/// FFI for rustdesk core's main entry. +/// Return true if the app should continue running with UI(possibly Flutter), false if the app should exit. +#[no_mangle] +pub extern "C" fn rustdesk_core_main() -> bool { + #[cfg(not(any(target_os = "android", target_os = "ios")))] + return crate::core_main::core_main(); + #[cfg(any(target_os = "android", target_os = "ios"))] + false +} + +pub enum EventToUI { + Event(String), + Rgba(ZeroCopyBuffer>), +} + +pub fn start_global_event_stream(s: StreamSink, app_type: String) -> ResultType<()> { + if let Some(_) = flutter::GLOBAL_EVENT_STREAM + .write() + .unwrap() + .insert(app_type.clone(), s) + { + log::warn!( + "Global event stream of type {} is started before, but now removed", + app_type + ); + } + Ok(()) +} + +pub fn stop_global_event_stream(app_type: String) { + let _ = flutter::GLOBAL_EVENT_STREAM + .write() + .unwrap() + .remove(&app_type); +} + +pub fn host_stop_system_key_propagate(stopped: bool) { + #[cfg(windows)] + crate::platform::windows::stop_system_key_propagate(stopped); +} + +pub fn session_connect( + events2ui: StreamSink, + id: String, + is_file_transfer: bool, +) -> ResultType<()> { + Session::start(&id, is_file_transfer, events2ui); + Ok(()) +} + +pub fn session_get_remember(id: String) -> Option { + if let Some(session) = SESSIONS.read().unwrap().get(&id) { + Some(session.get_remember()) + } else { + None + } +} + +pub fn session_get_toggle_option(id: String, arg: String) -> Option { + if let Some(session) = SESSIONS.read().unwrap().get(&id) { + Some(session.get_toggle_option(&arg)) + } else { + None + } +} + +pub fn session_get_toggle_option_sync(id: String, arg: String) -> SyncReturn { + let res = session_get_toggle_option(id, arg) == Some(true); + SyncReturn(res) +} + +pub fn session_get_image_quality(id: String) -> Option { + if let Some(session) = SESSIONS.read().unwrap().get(&id) { + Some(session.get_image_quality()) + } else { + None + } +} + +pub fn session_get_option(id: String, arg: String) -> Option { + if let Some(session) = SESSIONS.read().unwrap().get(&id) { + Some(session.get_option(&arg)) + } else { + None + } +} + +pub fn session_login(id: String, password: String, remember: bool) { + if let Some(session) = SESSIONS.read().unwrap().get(&id) { + session.login(&password, remember); + } +} + +pub fn session_close(id: String) { + if let Some(session) = SESSIONS.read().unwrap().get(&id) { + session.close(); + } + let _ = SESSIONS.write().unwrap().remove(&id); +} + +pub fn session_refresh(id: String) { + if let Some(session) = SESSIONS.read().unwrap().get(&id) { + session.refresh(); + } +} + +pub fn session_reconnect(id: String) { + if let Some(session) = SESSIONS.read().unwrap().get(&id) { + session.reconnect(); + } +} + +pub fn session_toggle_option(id: String, value: String) { + if let Some(session) = SESSIONS.read().unwrap().get(&id) { + session.toggle_option(&value); + } +} + +pub fn session_set_image_quality(id: String, value: String) { + if let Some(session) = SESSIONS.read().unwrap().get(&id) { + session.set_image_quality(&value); + } +} + +pub fn session_lock_screen(id: String) { + if let Some(session) = SESSIONS.read().unwrap().get(&id) { + session.lock_screen(); + } +} + +pub fn session_ctrl_alt_del(id: String) { + if let Some(session) = SESSIONS.read().unwrap().get(&id) { + session.ctrl_alt_del(); + } +} + +pub fn session_switch_display(id: String, value: i32) { + if let Some(session) = SESSIONS.read().unwrap().get(&id) { + session.switch_display(value); + } +} + +pub fn session_input_key( + id: String, + name: String, + down: bool, + press: bool, + alt: bool, + ctrl: bool, + shift: bool, + command: bool, +) { + if let Some(session) = SESSIONS.read().unwrap().get(&id) { + session.input_key(&name, down, press, alt, ctrl, shift, command); + } +} + +pub fn session_input_string(id: String, value: String) { + if let Some(session) = SESSIONS.read().unwrap().get(&id) { + session.input_string(&value); + } +} + +// chat_client_mode +pub fn session_send_chat(id: String, text: String) { + if let Some(session) = SESSIONS.read().unwrap().get(&id) { + session.send_chat(text); + } +} + +pub fn session_peer_option(id: String, name: String, value: String) { + if let Some(session) = SESSIONS.read().unwrap().get(&id) { + session.set_option(name, value); + } +} + +pub fn session_get_peer_option(id: String, name: String) -> String { + if let Some(session) = SESSIONS.read().unwrap().get(&id) { + return session.get_option(&name); + } + "".to_string() +} + +pub fn session_input_os_password(id: String, value: String) { + if let Some(session) = SESSIONS.read().unwrap().get(&id) { + session.input_os_password(value, true); + } +} + +// File Action +pub fn session_read_remote_dir(id: String, path: String, include_hidden: bool) { + if let Some(session) = SESSIONS.read().unwrap().get(&id) { + session.read_remote_dir(path, include_hidden); + } +} + +pub fn session_send_files( + id: String, + act_id: i32, + path: String, + to: String, + file_num: i32, + include_hidden: bool, + is_remote: bool, +) { + if let Some(session) = SESSIONS.read().unwrap().get(&id) { + session.send_files(act_id, path, to, file_num, include_hidden, is_remote); + } +} + +pub fn session_set_confirm_override_file( + id: String, + act_id: i32, + file_num: i32, + need_override: bool, + remember: bool, + is_upload: bool, +) { + if let Some(session) = SESSIONS.read().unwrap().get(&id) { + session.set_confirm_override_file(act_id, file_num, need_override, remember, is_upload); + } +} + +pub fn session_remove_file(id: String, act_id: i32, path: String, file_num: i32, is_remote: bool) { + if let Some(session) = SESSIONS.read().unwrap().get(&id) { + session.remove_file(act_id, path, file_num, is_remote); + } +} + +pub fn session_read_dir_recursive( + id: String, + act_id: i32, + path: String, + is_remote: bool, + show_hidden: bool, +) { + if let Some(session) = SESSIONS.read().unwrap().get(&id) { + session.remove_dir_all(act_id, path, is_remote, show_hidden); + } +} + +pub fn session_remove_all_empty_dirs(id: String, act_id: i32, path: String, is_remote: bool) { + if let Some(session) = SESSIONS.read().unwrap().get(&id) { + session.remove_dir(act_id, path, is_remote); + } +} + +pub fn session_cancel_job(id: String, act_id: i32) { + if let Some(session) = SESSIONS.read().unwrap().get(&id) { + session.cancel_job(act_id); + } +} + +pub fn session_create_dir(id: String, act_id: i32, path: String, is_remote: bool) { + if let Some(session) = SESSIONS.read().unwrap().get(&id) { + session.create_dir(act_id, path, is_remote); + } +} + +pub fn session_read_local_dir_sync(id: String, path: String, show_hidden: bool) -> String { + if let Some(session) = SESSIONS.read().unwrap().get(&id) { + if let Ok(fd) = fs::read_dir(&fs::get_path(&path), show_hidden) { + return make_fd_to_json(fd); + } + } + "".to_string() +} + +pub fn session_get_platform(id: String, is_remote: bool) -> String { + if let Some(session) = SESSIONS.read().unwrap().get(&id) { + return session.get_platform(is_remote); + } + "".to_string() +} + +pub fn session_load_last_transfer_jobs(id: String) { + if let Some(session) = SESSIONS.read().unwrap().get(&id) { + return session.load_last_jobs(); + } else { + // a tip for flutter dev + eprintln!( + "cannot load last transfer job from non-existed session. Please ensure session \ + is connected before calling load last transfer jobs." + ); + } +} + +pub fn session_add_job( + id: String, + act_id: i32, + path: String, + to: String, + file_num: i32, + include_hidden: bool, + is_remote: bool, +) { + if let Some(session) = SESSIONS.read().unwrap().get(&id) { + session.add_job(act_id, path, to, file_num, include_hidden, is_remote); + } +} + +pub fn session_resume_job(id: String, act_id: i32, is_remote: bool) { + if let Some(session) = SESSIONS.read().unwrap().get(&id) { + session.resume_job(act_id, is_remote); + } +} + +pub fn main_get_sound_inputs() -> Vec { + get_sound_inputs() +} + +pub fn main_change_id(new_id: String) { + #[cfg(not(any(target_os = "android", target_os = "ios")))] + change_id(new_id) +} + +pub fn main_get_async_status() -> String { + get_async_job_status() +} + +pub fn main_get_option(key: String) -> String { + get_option(key) +} + +pub fn main_set_option(key: String, value: String) { + if key.eq("custom-rendezvous-server") { + set_option(key, value); + #[cfg(target_os = "android")] + crate::rendezvous_mediator::RendezvousMediator::restart(); + #[cfg(any(target_os = "android", target_os = "ios", feature = "cli"))] + crate::common::test_rendezvous_server(); + } else { + set_option(key, value); + } +} + +pub fn main_get_options() -> String { + get_options() +} + +pub fn main_set_options(json: String) { + let map: HashMap = serde_json::from_str(&json).unwrap_or(HashMap::new()); + if !map.is_empty() { + set_options(map) + } +} + +pub fn main_test_if_valid_server(server: String) -> String { + test_if_valid_server(server) +} + +pub fn main_set_socks(proxy: String, username: String, password: String) { + set_socks(proxy, username, password) +} + +pub fn main_get_socks() -> Vec { + get_socks() +} + +pub fn main_get_app_name() -> String { + get_app_name() +} + +pub fn main_get_license() -> String { + get_license() +} + +pub fn main_get_version() -> String { + get_version() +} + +pub fn main_get_fav() -> Vec { + get_fav() +} + +pub fn main_store_fav(favs: Vec) { + store_fav(favs) +} + +pub fn main_get_peer(id: String) -> String { + let conf = get_peer(id); + serde_json::to_string(&conf).unwrap_or("".to_string()) +} + +pub fn main_get_lan_peers() -> String { + serde_json::to_string(&get_lan_peers()).unwrap_or_default() +} + +pub fn main_get_connect_status() -> String { + let status = get_connect_status(); + // (status_num, key_confirmed, mouse_time, id) + let mut m = serde_json::Map::new(); + m.insert("status_num".to_string(), json!(status.0)); + m.insert("key_confirmed".to_string(), json!(status.1)); + m.insert("mouse_time".to_string(), json!(status.2)); + m.insert("id".to_string(), json!(status.3)); + serde_json::to_string(&m).unwrap_or("".to_string()) +} + +pub fn main_check_connect_status() { + #[cfg(not(any(target_os = "android", target_os = "ios")))] + check_connect_status(true); +} + +pub fn main_is_using_public_server() -> bool { + using_public_server() +} + +pub fn main_discover() { + discover(); +} + +pub fn main_has_rendezvous_service() -> bool { + has_rendezvous_service() +} + +pub fn main_get_api_server() -> String { + get_api_server() +} + +pub fn main_post_request(url: String, body: String, header: String) { + post_request(url, body, header) +} + +pub fn main_get_local_option(key: String) -> String { + get_local_option(key) +} + +pub fn main_set_local_option(key: String, value: String) { + set_local_option(key, value) +} + +pub fn main_get_my_id() -> String { + get_id() +} + +pub fn main_get_uuid() -> String { + get_uuid() +} + +pub fn main_get_peer_option(id: String, key: String) -> String { + get_peer_option(id, key) +} + +pub fn main_set_peer_option(id: String, key: String, value: String) { + set_peer_option(id, key, value) +} + +pub fn main_forget_password(id: String) { + forget_password(id) +} + +// TODO APP_DIR & ui_interface +pub fn main_get_recent_peers() -> String { + if !config::APP_DIR.read().unwrap().is_empty() { + let peers: Vec<(String, config::PeerInfoSerde)> = PeerConfig::peers() + .drain(..) + .map(|(id, _, p)| (id, p.info)) + .collect(); + serde_json::ser::to_string(&peers).unwrap_or("".to_owned()) + } else { + String::new() + } +} + +pub fn main_load_recent_peers() { + if !config::APP_DIR.read().unwrap().is_empty() { + let peers: Vec<(String, config::PeerInfoSerde)> = PeerConfig::peers() + .drain(..) + .map(|(id, _, p)| (id, p.info)) + .collect(); + if let Some(s) = flutter::GLOBAL_EVENT_STREAM + .read() + .unwrap() + .get(flutter::APP_TYPE_MAIN) + { + let data = HashMap::from([ + ("name", "load_recent_peers".to_owned()), + ( + "peers", + serde_json::ser::to_string(&peers).unwrap_or("".to_owned()), + ), + ]); + s.add(serde_json::ser::to_string(&data).unwrap_or("".to_owned())); + }; + } +} + +pub fn main_load_fav_peers() { + if !config::APP_DIR.read().unwrap().is_empty() { + let favs = get_fav(); + let peers: Vec<(String, config::PeerInfoSerde)> = PeerConfig::peers() + .into_iter() + .filter_map(|(id, _, peer)| { + if favs.contains(&id) { + Some((id, peer.info)) + } else { + None + } + }) + .collect(); + if let Some(s) = flutter::GLOBAL_EVENT_STREAM + .read() + .unwrap() + .get(flutter::APP_TYPE_MAIN) + { + let data = HashMap::from([ + ("name", "load_fav_peers".to_owned()), + ( + "peers", + serde_json::ser::to_string(&peers).unwrap_or("".to_owned()), + ), + ]); + s.add(serde_json::ser::to_string(&data).unwrap_or("".to_owned())); + }; + } +} + +pub fn main_load_lan_peers() { + if let Some(s) = flutter::GLOBAL_EVENT_STREAM + .read() + .unwrap() + .get(flutter::APP_TYPE_MAIN) + { + let data = HashMap::from([ + ("name", "load_lan_peers".to_owned()), + ("peers", serde_json::to_string(&get_lan_peers()).unwrap_or_default()), + ]); + s.add(serde_json::ser::to_string(&data).unwrap_or("".to_owned())); + }; +} + +pub fn main_get_last_remote_id() -> String { + // if !config::APP_DIR.read().unwrap().is_empty() { + // res = LocalConfig::get_remote_id(); + // } + LocalConfig::get_remote_id() +} + +pub fn main_get_software_update_url() -> String { + crate::common::SOFTWARE_UPDATE_URL.lock().unwrap().clone() +} + +pub fn main_get_home_dir() -> String { + fs::get_home_as_string() +} + +pub fn main_get_langs() -> String { + get_langs() +} + +pub fn main_get_temporary_password() -> String { + ui_interface::temporary_password() +} + +pub fn main_get_permanent_password() -> String { + ui_interface::permanent_password() +} + +pub fn main_get_online_statue() -> i64 { + ONLINE.lock().unwrap().values().max().unwrap_or(&0).clone() +} + +pub fn main_get_clients_state() -> String { + get_clients_state() +} + +pub fn main_check_clients_length(length: usize) -> Option { + if length != get_clients_length() { + Some(get_clients_state()) + } else { + None + } +} + +pub fn main_init(app_dir: String) { + initialize(&app_dir); +} + +pub fn main_device_id(id: String) { + *crate::common::DEVICE_ID.lock().unwrap() = id; +} + +pub fn main_device_name(name: String) { + *crate::common::DEVICE_NAME.lock().unwrap() = name; +} + +pub fn main_remove_peer(id: String) { + PeerConfig::remove(&id); +} + +pub fn main_has_hwcodec() -> bool { + has_hwcodec() +} + +// TODO +pub fn session_send_mouse(id: String, msg: String) { + if let Ok(m) = serde_json::from_str::>(&msg) { + let alt = m.get("alt").is_some(); + let ctrl = m.get("ctrl").is_some(); + let shift = m.get("shift").is_some(); + let command = m.get("command").is_some(); + let x = m + .get("x") + .map(|x| x.parse::().unwrap_or(0)) + .unwrap_or(0); + let y = m + .get("y") + .map(|x| x.parse::().unwrap_or(0)) + .unwrap_or(0); + let mut mask = 0; + if let Some(_type) = m.get("type") { + mask = match _type.as_str() { + "down" => 1, + "up" => 2, + "wheel" => 3, + _ => 0, + }; + } + if let Some(buttons) = m.get("buttons") { + mask |= match buttons.as_str() { + "left" => 1, + "right" => 2, + "wheel" => 4, + _ => 0, + } << 3; + } + if let Some(session) = SESSIONS.read().unwrap().get(&id) { + session.send_mouse(mask, x, y, alt, ctrl, shift, command); + } + } +} + +pub fn session_restart_remote_device(id: String) { + if let Some(session) = SESSIONS.read().unwrap().get(&id) { + session.restart_remote_device(); + } +} + +pub fn main_set_home_dir(home: String) { + *config::APP_HOME_DIR.write().unwrap() = home; +} + +pub fn main_stop_service() { + #[cfg(target_os = "android")] + { + Config::set_option("stop-service".into(), "Y".into()); + crate::rendezvous_mediator::RendezvousMediator::restart(); + } +} + +pub fn main_start_service() { + #[cfg(target_os = "android")] + { + Config::set_option("stop-service".into(), "".into()); + crate::rendezvous_mediator::RendezvousMediator::restart(); + } + #[cfg(not(target_os = "android"))] + std::thread::spawn(move || start_server(true)); +} + +pub fn main_update_temporary_password() { + update_temporary_password(); +} + +pub fn main_set_permanent_password(password: String) { + set_permanent_password(password); +} + +pub fn main_check_super_user_permission() -> bool { + check_super_user_permission() +} + +pub fn cm_send_chat(conn_id: i32, msg: String) { + connection_manager::send_chat(conn_id, msg); +} + +pub fn cm_login_res(conn_id: i32, res: bool) { + connection_manager::on_login_res(conn_id, res); +} + +pub fn cm_close_connection(conn_id: i32) { + connection_manager::close_conn(conn_id); +} + +pub fn cm_check_click_time(conn_id: i32) { + connection_manager::check_click_time(conn_id) +} + +pub fn cm_get_click_time() -> f64 { + connection_manager::get_click_time() as _ +} + +pub fn cm_switch_permission(conn_id: i32, name: String, enabled: bool) { + connection_manager::switch_permission(conn_id, name, enabled) +} + +pub fn main_get_icon() -> String { + #[cfg(not(any(target_os = "android", target_os = "ios", feature = "cli")))] + return ui_interface::get_icon(); + #[cfg(any(target_os = "android", target_os = "ios", feature = "cli"))] + return String::new(); +} + +#[no_mangle] +unsafe extern "C" fn translate(name: *const c_char, locale: *const c_char) -> *const c_char { + let name = CStr::from_ptr(name); + let locale = CStr::from_ptr(locale); + let res = if let (Ok(name), Ok(locale)) = (name.to_str(), locale.to_str()) { + crate::client::translate_locale(name.to_owned(), locale) + } else { + String::new() + }; + CString::from_vec_unchecked(res.into_bytes()).into_raw() +} + +fn handle_query_onlines(onlines: Vec, offlines: Vec) { + if let Some(s) = flutter::GLOBAL_EVENT_STREAM + .read() + .unwrap() + .get(flutter::APP_TYPE_MAIN) + { + let data = HashMap::from([ + ("name", "callback_query_onlines".to_owned()), + ("onlines", onlines.join(",")), + ("offlines", offlines.join(",")), + ]); + s.add(serde_json::ser::to_string(&data).unwrap_or("".to_owned())); + }; +} + +pub fn query_onlines(ids: Vec) { + crate::rendezvous_mediator::query_online_states(ids, handle_query_onlines) +} + +#[cfg(target_os = "android")] +pub mod server_side { + use jni::{ + objects::{JClass, JString}, + sys::jstring, + JNIEnv, + }; + + use hbb_common::{config::Config, log}; + + use crate::start_server; + + #[no_mangle] + pub unsafe extern "system" fn Java_com_carriez_flutter_1hbb_MainService_startServer( + env: JNIEnv, + _class: JClass, + ) { + log::debug!("startServer from java"); + std::thread::spawn(move || start_server(true)); + } + + #[no_mangle] + pub unsafe extern "system" fn Java_com_carriez_flutter_1hbb_MainService_translateLocale( + env: JNIEnv, + _class: JClass, + locale: JString, + input: JString, + ) -> jstring { + let res = if let (Ok(input), Ok(locale)) = (env.get_string(input), env.get_string(locale)) { + let input: String = input.into(); + let locale: String = locale.into(); + crate::client::translate_locale(input, &locale) + } else { + "".into() + }; + return env.new_string(res).unwrap_or(input).into_inner(); + } + + #[no_mangle] + pub unsafe extern "system" fn Java_com_carriez_flutter_1hbb_MainService_refreshScreen( + _env: JNIEnv, + _class: JClass, + ) { + crate::server::video_service::refresh() + } +} diff --git a/src/ipc.rs b/src/ipc.rs index 13ad67283..0bdc3f43b 100644 --- a/src/ipc.rs +++ b/src/ipc.rs @@ -1,5 +1,13 @@ -use crate::rendezvous_mediator::RendezvousMediator; +use std::{collections::HashMap, sync::atomic::Ordering}; +#[cfg(not(windows))] +use std::{fs::File, io::prelude::*}; + use bytes::Bytes; +use parity_tokio_ipc::{ + Connection as Conn, ConnectionClient as ConnClient, Endpoint, Incoming, SecurityAttributes, +}; +use serde_derive::{Deserialize, Serialize}; + #[cfg(not(any(target_os = "android", target_os = "ios")))] pub use clipboard::ClipbaordFile; use hbb_common::{ @@ -13,13 +21,8 @@ use hbb_common::{ tokio_util::codec::Framed, ResultType, }; -use parity_tokio_ipc::{ - Connection as Conn, ConnectionClient as ConnClient, Endpoint, Incoming, SecurityAttributes, -}; -use serde_derive::{Deserialize, Serialize}; -use std::{collections::HashMap, sync::atomic::Ordering}; -#[cfg(not(windows))] -use std::{fs::File, io::prelude::*}; + +use crate::rendezvous_mediator::RendezvousMediator; // State with timestamp, because std::time::Instant cannot be serialized #[derive(Debug, Serialize, Deserialize, Copy, Clone)] @@ -410,6 +413,83 @@ pub async fn connect(ms_timeout: u64, postfix: &str) -> ResultType { + loop { + if let Some(result) = incoming.next().await { + match result { + Ok(stream) => { + let mut stream = Connection::new(stream); + let mut device: String = "".to_owned(); + if let Some(Ok(Some(Data::Config((_, Some(x)))))) = + stream.next_timeout2(1000).await + { + device = x; + } + if !device.is_empty() { + device = crate::platform::linux::get_pa_source_name(&device); + } + if device.is_empty() { + device = crate::platform::linux::get_pa_monitor(); + } + if device.is_empty() { + continue; + } + let spec = pulse::sample::Spec { + format: pulse::sample::Format::F32le, + channels: 2, + rate: crate::platform::PA_SAMPLE_RATE, + }; + log::info!("pa monitor: {:?}", device); + // systemctl --user status pulseaudio.service + let mut buf: Vec = vec![0; AUDIO_DATA_SIZE_U8]; + match psimple::Simple::new( + None, // Use the default server + &crate::get_app_name(), // Our application’s name + pulse::stream::Direction::Record, // We want a record stream + Some(&device), // Use the default device + "record", // Description of our stream + &spec, // Our sample format + None, // Use default channel map + None, // Use default buffering attributes + ) { + Ok(s) => loop { + if let Ok(_) = s.read(&mut buf) { + let out = + if buf.iter().filter(|x| **x != 0).next().is_none() { + vec![] + } else { + buf.clone() + }; + if let Err(err) = stream.send_raw(out.into()).await { + log::error!("Failed to send audio data:{}", err); + break; + } + } + }, + Err(err) => { + log::error!("Could not create simple pulse: {}", err); + } + } + } + Err(err) => { + log::error!("Couldn't get pa client: {:?}", err); + } + } + } + } + } + Err(err) => { + log::error!("Failed to start pa ipc server: {}", err); + } + } +} + #[inline] #[cfg(not(windows))] fn get_pid_file(postfix: &str) -> String { diff --git a/src/lan.rs b/src/lan.rs index 733e271a9..30af1de6b 100644 --- a/src/lan.rs +++ b/src/lan.rs @@ -276,6 +276,8 @@ async fn handle_received_peers(mut rx: UnboundedReceiver) peers.insert(0, peer); if last_write_time.elapsed().as_millis() > 300 { config::LanPeers::store(&peers); + #[cfg(feature = "flutter")] + crate::flutter_ffi::main_load_lan_peers(); last_write_time = Instant::now(); } } @@ -287,5 +289,7 @@ async fn handle_received_peers(mut rx: UnboundedReceiver) } config::LanPeers::store(&peers); + #[cfg(feature = "flutter")] + crate::flutter_ffi::main_load_lan_peers(); Ok(()) } diff --git a/src/lang/cn.rs b/src/lang/cn.rs index 9a5a76250..764868f35 100644 --- a/src/lang/cn.rs +++ b/src/lang/cn.rs @@ -103,6 +103,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Original", "原始比例"), ("Shrink", "收缩"), ("Stretch", "伸展"), + ("Scrollbar", "滚动条"), + ("ScrollAuto", "自动滚动"), ("Good image quality", "好画质"), ("Balanced", "一般画质"), ("Optimize reaction time", "优化反应时间"), @@ -302,5 +304,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Are you sure you want to restart", "确定要重启"), ("Restarting Remote Device", "正在重启远程设备"), ("remote_restarting_tip", "远程设备正在重启, 请关闭当前提示框, 并在一段时间后使用永久密码重新连接"), + ("Copied", "已复制"), ].iter().cloned().collect(); } diff --git a/src/lang/cs.rs b/src/lang/cs.rs index aa03e1d23..a05bb7c3d 100644 --- a/src/lang/cs.rs +++ b/src/lang/cs.rs @@ -103,6 +103,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Original", "Původní"), ("Shrink", "Oříznout"), ("Stretch", "Roztáhnout"), + ("Scrollbar", "Posuvník"), + ("ScrollAuto", "Rolovať Auto"), ("Good image quality", "Dobrá kvalita obrazu"), ("Balanced", "Vyvážené"), ("Optimize reaction time", "Optimalizovat pro co nejnižší prodlevu odezvy"), @@ -302,5 +304,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Are you sure you want to restart", ""), ("Restarting Remote Device", ""), ("remote_restarting_tip", ""), + ("Copied", ""), ].iter().cloned().collect(); } diff --git a/src/lang/da.rs b/src/lang/da.rs index 364e552a3..941fff2c5 100644 --- a/src/lang/da.rs +++ b/src/lang/da.rs @@ -103,6 +103,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Original", "Original"), ("Shrink", "Krymp"), ("Stretch", "Strak"), + ("Scrollbar", "Rullebar"), + ("ScrollAuto", "Rul Auto"), ("Good image quality", "God billedkvalitet"), ("Balanced", "Afbalanceret"), ("Optimize reaction time", "Optimeret responstid"), @@ -302,5 +304,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Are you sure you want to restart", ""), ("Restarting Remote Device", ""), ("remote_restarting_tip", ""), + ("Copied", ""), ].iter().cloned().collect(); } diff --git a/src/lang/de.rs b/src/lang/de.rs index f9558bed9..3c8e43bac 100644 --- a/src/lang/de.rs +++ b/src/lang/de.rs @@ -103,6 +103,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Original", "Original"), ("Shrink", "Verkleinern"), ("Stretch", "Strecken"), + ("Scrollbar", "Scrollleiste"), + ("ScrollAuto", "Automatisch scrollen"), ("Good image quality", "Schöner"), ("Balanced", "Ausgeglichen"), ("Optimize reaction time", "Schneller"), @@ -302,5 +304,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Are you sure you want to restart", "Möchten Sie das entfernte Gerät wirklich neu starten?"), ("Restarting Remote Device", "Entferntes Gerät wird neu gestartet"), ("remote_restarting_tip", "Entferntes Gerät startet neu, bitte schließen Sie diese Meldung und verbinden Sie sich mit dem dauerhaften Passwort erneut."), + ("Copied", ""), ].iter().cloned().collect(); } diff --git a/src/lang/eo.rs b/src/lang/eo.rs index ad4a2f93a..866bbb1a7 100644 --- a/src/lang/eo.rs +++ b/src/lang/eo.rs @@ -103,6 +103,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Original", "Originala rilatumo"), ("Shrink", "Ŝrumpi"), ("Stretch", "Streĉi"), + ("Scrollbar", "Rulumbreto"), + ("ScrollAuto", "Rulumu Aŭtomate"), ("Good image quality", "Bona bilda kvalito"), ("Balanced", "Normala bilda kvalito"), ("Optimize reaction time", "Optimigi reakcia tempo"), @@ -302,5 +304,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Are you sure you want to restart", ""), ("Restarting Remote Device", ""), ("remote_restarting_tip", ""), + ("Copied", ""), ].iter().cloned().collect(); } diff --git a/src/lang/es.rs b/src/lang/es.rs index 73a38dacd..53852a051 100644 --- a/src/lang/es.rs +++ b/src/lang/es.rs @@ -103,6 +103,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Original", "Original"), ("Shrink", "Encogerse"), ("Stretch", "Estirar"), + ("Scrollbar", "Barra de desplazamiento"), + ("ScrollAuto", "Desplazamiento automático"), ("Good image quality", "Buena calidad de imagen"), ("Balanced", "Equilibrado"), ("Optimize reaction time", "Optimizar el tiempo de reacción"), @@ -315,5 +317,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Are you sure you want to restart", "Esta Seguro que desea reiniciar?"), ("Restarting Remote Device", "Reiniciando dispositivo remoto"), ("remote_restarting_tip", "Dispositivo remoto reiniciando, favor de cerrar este mensaje y reconectarse con la contraseña permamente despues de un momento."), + ("Copied", ""), ].iter().cloned().collect(); } diff --git a/src/lang/fr.rs b/src/lang/fr.rs index 3553a5b17..61c0c9a10 100644 --- a/src/lang/fr.rs +++ b/src/lang/fr.rs @@ -103,6 +103,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Original", "Ratio d'origine"), ("Shrink", "Rétrécir"), ("Stretch", "Étirer"), + ("Scrollbar", "Barre de défilement"), + ("ScrollAuto", "Défilement automatique"), ("Good image quality", "Bonne qualité d'image"), ("Balanced", "Qualité d'image normale"), ("Optimize reaction time", "Optimiser le temps de réaction"), @@ -302,5 +304,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Are you sure you want to restart", ""), ("Restarting Remote Device", ""), ("remote_restarting_tip", ""), + ("Copied", ""), ].iter().cloned().collect(); } diff --git a/src/lang/hu.rs b/src/lang/hu.rs index 8a57378ea..3f135380f 100644 --- a/src/lang/hu.rs +++ b/src/lang/hu.rs @@ -103,6 +103,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Original", "Eredeti"), ("Shrink", "Zsugorított"), ("Stretch", "Nyújtott"), + ("Scrollbar", "Görgetősáv"), + ("ScrollAuto", "Görgessen Auto"), ("Good image quality", "Jó képminőség"), ("Balanced", "Balanszolt"), ("Optimize reaction time", "Válaszidő optimializálása"), @@ -302,5 +304,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Are you sure you want to restart", ""), ("Restarting Remote Device", ""), ("remote_restarting_tip", ""), + ("Copied", ""), ].iter().cloned().collect(); } diff --git a/src/lang/id.rs b/src/lang/id.rs index 422851c77..39b07d976 100644 --- a/src/lang/id.rs +++ b/src/lang/id.rs @@ -103,6 +103,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Original", "Original"), ("Shrink", "Susutkan"), ("Stretch", "Regangkan"), + ("Scrollbar", "Scroll bar"), + ("ScrollAuto", "Gulir Otomatis"), ("Good image quality", "Kualitas Gambar Baik"), ("Balanced", "Seimbang"), ("Optimize reaction time", "Optimalkan waktu reaksi"), @@ -315,5 +317,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Are you sure you want to restart", "Apakah Anda yakin untuk memulai ulang"), ("Restarting Remote Device", "Memulai Ulang Perangkat Jarak Jauh"), ("remote_restarting_tip", ""), + ("Copied", ""), ].iter().cloned().collect(); } diff --git a/src/lang/it.rs b/src/lang/it.rs index 244cb9047..45a7d45c6 100644 --- a/src/lang/it.rs +++ b/src/lang/it.rs @@ -101,8 +101,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Unblock user input", "Sbloccare l'input dell'utente"), ("Adjust Window", "Adatta la finestra"), ("Original", "Originale"), - ("Shrink", "Scala"), - ("Stretch", "Adatta"), + ("Shrink", "Restringi"), + ("Stretch", "Allarga"), + ("Scrollbar", "Barra di scorrimento"), + ("ScrollAuto", "Scorri automaticamente"), ("Good image quality", "Buona qualità immagine"), ("Balanced", "Bilanciato"), ("Optimize reaction time", "Ottimizza il tempo di reazione"), diff --git a/src/lang/pl.rs b/src/lang/pl.rs index 71b753ac3..81eaddfaf 100644 --- a/src/lang/pl.rs +++ b/src/lang/pl.rs @@ -103,6 +103,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Original", "Oryginał"), ("Shrink", "Zmniejsz"), ("Stretch", "Zwiększ"), + ("Scrollbar", "Pasek przewijania"), + ("ScrollAuto", "Przewijanie automatyczne"), ("Good image quality", "Dobra jakość obrazu"), ("Balanced", "Zrównoważony"), ("Optimize reaction time", "Zoptymalizuj czas reakcji"), @@ -300,5 +302,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Activate onetime password", "Aktywuj hasło jednorazowe"), ("Set security password", "Ustaw hasło zabezpieczające"), ("Connection not allowed", "Połączenie niedozwolone"), + ("Copied", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ptbr.rs b/src/lang/ptbr.rs index dea8197b2..b98875d91 100644 --- a/src/lang/ptbr.rs +++ b/src/lang/ptbr.rs @@ -103,6 +103,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Original", "Original"), ("Shrink", "Reduzir"), ("Stretch", "Aumentar"), + ("Scrollbar", "Barra de rolagem"), + ("ScrollAuto", "Rolagem automática"), ("Good image quality", "Qualidade visual boa"), ("Balanced", "Balanceada"), ("Optimize reaction time", "Otimizar tempo de reação"), @@ -302,5 +304,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Are you sure you want to restart", ""), ("Restarting Remote Device", ""), ("remote_restarting_tip", ""), + ("Copied", ""), ].iter().cloned().collect(); } diff --git a/src/lang/ru.rs b/src/lang/ru.rs index 490fb32e0..beb9f7fb9 100644 --- a/src/lang/ru.rs +++ b/src/lang/ru.rs @@ -103,6 +103,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Original", "Оригинал"), ("Shrink", "Уменьшить"), ("Stretch", "Растянуть"), + ("Scrollbar", "Полоса прокрутки"), + ("ScrollAuto", "Прокрутка Авто"), ("Good image quality", "Хорошее качество изображения"), ("Balanced", "Сбалансированный"), ("Optimize reaction time", "Оптимизировать время реакции"), @@ -302,5 +304,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Are you sure you want to restart", "Вы уверены, что хотите выполнить перезапуск?"), ("Restarting Remote Device", "Перезагрузка удаленного устройства"), ("remote_restarting_tip", "Удаленное устройство перезапускается. Пожалуйста, закройте это сообщение и через некоторое время переподключитесь, используя постоянный пароль."), + ("Copied", ""), ].iter().cloned().collect(); } diff --git a/src/lang/sk.rs b/src/lang/sk.rs index 7a78e2090..2a1a1f12f 100644 --- a/src/lang/sk.rs +++ b/src/lang/sk.rs @@ -103,6 +103,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Original", "Pôvodný"), ("Shrink", "Zmenšené"), ("Stretch", "Roztiahnuté"), + ("Scrollbar", "Posuvník"), + ("ScrollAuto", "Rolovať Auto"), ("Good image quality", "Dobrá kvalita obrazu"), ("Balanced", "Vyvážené"), ("Optimize reaction time", "Optimalizované pre čas odozvy"), @@ -302,5 +304,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Are you sure you want to restart", ""), ("Restarting Remote Device", ""), ("remote_restarting_tip", ""), + ("Copied", ""), ].iter().cloned().collect(); } diff --git a/src/lang/template.rs b/src/lang/template.rs index f37d84b60..c7a22eb84 100644 --- a/src/lang/template.rs +++ b/src/lang/template.rs @@ -103,6 +103,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Original", ""), ("Shrink", ""), ("Stretch", ""), + ("Scrollbar", ""), + ("ScrollAuto", ""), ("Good image quality", ""), ("Balanced", ""), ("Optimize reaction time", ""), @@ -302,5 +304,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Are you sure you want to restart", ""), ("Restarting Remote Device", ""), ("remote_restarting_tip", ""), + ("Copied", ""), ].iter().cloned().collect(); } diff --git a/src/lang/tr.rs b/src/lang/tr.rs index 4b0d6ccb3..2b79d43e1 100644 --- a/src/lang/tr.rs +++ b/src/lang/tr.rs @@ -103,6 +103,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Original", "Orjinal"), ("Shrink", "Küçült"), ("Stretch", "Uzat"), + ("Scrollbar", "Kaydırma çubuğu"), + ("ScrollAuto", "Otomatik Kaydır"), ("Good image quality", "İyi görüntü kalitesi"), ("Balanced", "Dengelenmiş"), ("Optimize reaction time", "Tepki süresini optimize et"), @@ -315,5 +317,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Are you sure you want to restart", "Yeniden başlatmak istediğinize emin misin?"), ("Restarting Remote Device", "Uzaktan yeniden başlatılıyor"), ("remote_restarting_tip", ""), + ("Copied", ""), ].iter().cloned().collect(); } diff --git a/src/lang/tw.rs b/src/lang/tw.rs index a294acac2..162ca6da8 100644 --- a/src/lang/tw.rs +++ b/src/lang/tw.rs @@ -103,6 +103,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Original", "原始"), ("Shrink", "縮減"), ("Stretch", "延展"), + ("Scrollbar", "滾動條"), + ("ScrollAuto", "自動滾動"), ("Good image quality", "畫面品質良好"), ("Balanced", "平衡"), ("Optimize reaction time", "回應速度最佳化"), @@ -302,5 +304,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Are you sure you want to restart", "确定要重启"), ("Restarting Remote Device", "正在重啓遠程設備"), ("remote_restarting_tip", "遠程設備正在重啓,請關閉當前提示框,並在一段時間後使用永久密碼重新連接"), + ("Copied", "已複製"), ].iter().cloned().collect(); } diff --git a/src/lang/vn.rs b/src/lang/vn.rs index 55570dc40..3266496b2 100644 --- a/src/lang/vn.rs +++ b/src/lang/vn.rs @@ -103,6 +103,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Original", "Gốc"), ("Shrink", "Thu nhỏ"), ("Stretch", "Kéo dãn"), + ("Scrollbar", "Thanh cuộn"), + ("ScrollAuto", "Tự động cuộn"), ("Good image quality", "Chất lượng hình ảnh tốt"), ("Balanced", "Cân bằng"), ("Optimize reaction time", "Thời gian phản ứng tối ưu"), @@ -302,5 +304,6 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> = ("Are you sure you want to restart", "Bạn có chắc bạn muốn khởi động lại không"), ("Restarting Remote Device", "Đang khởi động lại thiết bị từ xa"), ("remote_restarting_tip", "Thiết bị từ xa đang khởi động lại, hãy đóng cửa sổ tin nhắn này và kết nối lại với mật khẩu vĩnh viễn sau một khoảng thời gian"), + ("Copied", ""), ].iter().cloned().collect(); } diff --git a/src/lib.rs b/src/lib.rs index 715a59b5c..b7d1883c8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -23,13 +23,18 @@ pub mod ipc; pub mod ui; mod version; pub use version::*; -#[cfg(any(target_os = "android", target_os = "ios"))] +#[cfg(any(target_os = "android", target_os = "ios", feature = "flutter"))] mod bridge_generated; -#[cfg(any(target_os = "android", target_os = "ios"))] -pub mod mobile; -#[cfg(any(target_os = "android", target_os = "ios"))] -pub mod mobile_ffi; +#[cfg(any(target_os = "android", target_os = "ios", feature = "flutter"))] +pub mod flutter; +#[cfg(any(target_os = "android", target_os = "ios", feature = "flutter"))] +pub mod flutter_ffi; use common::*; +#[cfg(all( + not(any(target_os = "android", target_os = "ios")), + feature = "flutter" +))] +pub mod core_main; #[cfg(feature = "cli")] pub mod cli; #[cfg(all(windows, feature = "hbbs"))] @@ -42,6 +47,8 @@ mod port_forward; #[cfg(windows)] mod tray; +mod ui_interface; + #[cfg(windows)] pub mod clipboard_file; diff --git a/src/main.rs b/src/main.rs index aecc2ec4a..0acdde68f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -205,7 +205,7 @@ fn main() { .about("RustDesk command line tool") .args_from_usage(&args) .get_matches(); - use hbb_common::env_logger::*; + use hbb_common::{env_logger::*, config::LocalConfig}; init_from_env(Env::default().filter_or(DEFAULT_FILTER_ENV, "info")); if let Some(p) = matches.value_of("port-forward") { let options: Vec = p.split(":").map(|x| x.to_owned()).collect(); @@ -232,6 +232,7 @@ fn main() { remote_host = options[3].clone(); } let key = matches.value_of("key").unwrap_or("").to_owned(); - cli::start_one_port_forward(options[0].clone(), port, remote_host, remote_port, key); + let token = LocalConfig::get_option("access_token"); + cli::start_one_port_forward(options[0].clone(), port, remote_host, remote_port, key, token); } } diff --git a/src/mobile_ffi.rs b/src/mobile_ffi.rs deleted file mode 100644 index b9ef571bb..000000000 --- a/src/mobile_ffi.rs +++ /dev/null @@ -1,579 +0,0 @@ -use crate::client::file_trait::FileManager; -use crate::common::make_fd_to_json; -use crate::mobile::connection_manager::{self, get_clients_length, get_clients_state}; -use crate::mobile::{self, Session}; -use flutter_rust_bridge::{StreamSink, ZeroCopyBuffer}; -use hbb_common::{ - config::{self, Config, LocalConfig, PeerConfig, ONLINE}, - fs, log, password_security as password, ResultType, -}; -use serde_json::{Number, Value}; -use std::{ - collections::HashMap, - ffi::{CStr, CString}, - os::raw::c_char, -}; - -fn initialize(app_dir: &str) { - #[cfg(target_os = "android")] - { - android_logger::init_once( - android_logger::Config::default() - .with_min_level(log::Level::Debug) // limit log level - .with_tag("ffi"), // logs will show under mytag tag - ); - } - #[cfg(target_os = "ios")] - { - use hbb_common::env_logger::*; - init_from_env(Env::default().filter_or(DEFAULT_FILTER_ENV, "debug")); - } - *config::APP_DIR.write().unwrap() = app_dir.to_owned(); - crate::common::test_rendezvous_server(); - crate::common::test_nat_type(); - #[cfg(target_os = "android")] - crate::common::check_software_update(); -} - -pub fn start_event_stream(s: StreamSink) -> ResultType<()> { - let _ = mobile::EVENT_STREAM.write().unwrap().insert(s); - Ok(()) -} - -pub fn start_rgba_stream(s: StreamSink>>) -> ResultType<()> { - let _ = mobile::RGBA_STREAM.write().unwrap().insert(s); - Ok(()) -} - -#[no_mangle] -unsafe extern "C" fn get_by_name(name: *const c_char, arg: *const c_char) -> *const c_char { - let mut res = "".to_owned(); - let arg: &CStr = CStr::from_ptr(arg); - let name: &CStr = CStr::from_ptr(name); - if let Ok(name) = name.to_str() { - match name { - "peers" => { - if !config::APP_DIR.read().unwrap().is_empty() { - let peers: Vec<(String, config::PeerInfoSerde)> = PeerConfig::peers() - .drain(..) - .map(|(id, _, p)| (id, p.info)) - .collect(); - res = serde_json::ser::to_string(&peers).unwrap_or("".to_owned()); - } - } - "remote_id" => { - if !config::APP_DIR.read().unwrap().is_empty() { - res = LocalConfig::get_remote_id(); - } - } - "remember" => { - res = Session::get_remember().to_string(); - } - "event" => { - if let Some(e) = Session::pop_event() { - res = e; - } - } - "toggle_option" => { - if let Ok(arg) = arg.to_str() { - if let Some(v) = Session::get_toggle_option(arg) { - res = v.to_string(); - } - } - } - "test_if_valid_server" => { - if let Ok(arg) = arg.to_str() { - res = hbb_common::socket_client::test_if_valid_server(arg); - } - } - "option" => { - if let Ok(arg) = arg.to_str() { - res = Config::get_option(arg); - } - } - "image_quality" => { - res = Session::get_image_quality(); - } - "software_update_url" => { - res = crate::common::SOFTWARE_UPDATE_URL.lock().unwrap().clone() - } - "translate" => { - if let Ok(arg) = arg.to_str() { - if let Ok(m) = serde_json::from_str::>(arg) { - if let Some(locale) = m.get("locale") { - if let Some(text) = m.get("text") { - res = crate::client::translate_locale(text.to_owned(), locale); - } - } - } - } - } - "peer_option" => { - if let Ok(arg) = arg.to_str() { - res = Session::get_option(arg); - } - } - "local_option" => { - if let Ok(arg) = arg.to_str() { - res = LocalConfig::get_option(arg); - } - } - "langs" => { - res = crate::lang::LANGS.to_string(); - } - // File Action - "get_home_dir" => { - res = fs::get_home_as_string(); - } - "read_local_dir_sync" => { - if let Ok(value) = arg.to_str() { - if let Ok(m) = serde_json::from_str::>(value) { - if let (Some(path), Some(show_hidden)) = - (m.get("path"), m.get("show_hidden")) - { - if let Ok(fd) = - fs::read_dir(&fs::get_path(path), show_hidden.eq("true")) - { - res = make_fd_to_json(fd); - } - } - } - } - } - // Server Side - "server_id" => { - res = Config::get_id(); - } - "permanent_password" => { - res = Config::get_permanent_password(); - } - "temporary_password" => { - res = password::temporary_password(); - } - "connect_statue" => { - res = ONLINE - .lock() - .unwrap() - .values() - .max() - .unwrap_or(&0) - .clone() - .to_string(); - } - #[cfg(target_os = "android")] - "clients_state" => { - res = get_clients_state(); - } - #[cfg(target_os = "android")] - "check_clients_length" => { - if let Ok(value) = arg.to_str() { - if value.parse::().unwrap_or(usize::MAX) != get_clients_length() { - res = get_clients_state() - } - } - } - "uuid" => { - res = base64::encode(hbb_common::get_uuid()); - } - _ => { - log::error!("Unknown name of get_by_name: {}", name); - } - } - } - CString::from_vec_unchecked(res.into_bytes()).into_raw() -} - -#[no_mangle] -unsafe extern "C" fn set_by_name(name: *const c_char, value: *const c_char) { - let value: &CStr = CStr::from_ptr(value); - if let Ok(value) = value.to_str() { - let name: &CStr = CStr::from_ptr(name); - if let Ok(name) = name.to_str() { - match name { - "init" => { - initialize(value); - } - "info1" => { - *crate::common::MOBILE_INFO1.lock().unwrap() = value.to_owned(); - } - "info2" => { - *crate::common::MOBILE_INFO2.lock().unwrap() = value.to_owned(); - } - "connect" => { - Session::start(value, false); - } - "connect_file_transfer" => { - Session::start(value, true); - } - "login" => { - if let Ok(m) = serde_json::from_str::>(value) { - if let Some(password) = m.get("password") { - if let Some(remember) = m.get("remember") { - Session::login(password, remember == "true"); - } - } - } - } - "close" => { - Session::close(); - } - "refresh" => { - Session::refresh(); - } - "reconnect" => { - Session::reconnect(); - } - "toggle_option" => { - Session::toggle_option(value); - } - "image_quality" => { - Session::set_image_quality(value); - } - "lock_screen" => { - Session::lock_screen(); - } - "ctrl_alt_del" => { - Session::ctrl_alt_del(); - } - "switch_display" => { - if let Ok(v) = value.parse::() { - Session::switch_display(v); - } - } - "remove" => { - PeerConfig::remove(value); - } - "input_key" => { - if let Ok(m) = serde_json::from_str::>(value) { - let alt = m.get("alt").is_some(); - let ctrl = m.get("ctrl").is_some(); - let shift = m.get("shift").is_some(); - let command = m.get("command").is_some(); - let down = m.get("down").is_some(); - let press = m.get("press").is_some(); - if let Some(name) = m.get("name") { - Session::input_key(name, down, press, alt, ctrl, shift, command); - } - } - } - "input_string" => { - Session::input_string(value); - } - "chat_client_mode" => { - Session::send_chat(value.to_owned()); - } - "send_mouse" => { - if let Ok(m) = serde_json::from_str::>(value) { - let alt = m.get("alt").is_some(); - let ctrl = m.get("ctrl").is_some(); - let shift = m.get("shift").is_some(); - let command = m.get("command").is_some(); - let x = m - .get("x") - .map(|x| x.parse::().unwrap_or(0)) - .unwrap_or(0); - let y = m - .get("y") - .map(|x| x.parse::().unwrap_or(0)) - .unwrap_or(0); - let mut mask = 0; - if let Some(_type) = m.get("type") { - mask = match _type.as_str() { - "down" => 1, - "up" => 2, - "wheel" => 3, - _ => 0, - }; - } - if let Some(buttons) = m.get("buttons") { - mask |= match buttons.as_str() { - "left" => 1, - "right" => 2, - "wheel" => 4, - _ => 0, - } << 3; - } - Session::send_mouse(mask, x, y, alt, ctrl, shift, command); - } - } - "option" => { - if let Ok(m) = serde_json::from_str::>(value) { - if let Some(name) = m.get("name") { - if let Some(value) = m.get("value") { - Config::set_option(name.to_owned(), value.to_owned()); - if name == "custom-rendezvous-server" { - #[cfg(target_os = "android")] - crate::rendezvous_mediator::RendezvousMediator::restart(); - crate::common::test_rendezvous_server(); - } - } - } - } - } - "peer_option" => { - if let Ok(m) = serde_json::from_str::>(value) { - if let Some(name) = m.get("name") { - if let Some(value) = m.get("value") { - Session::set_option(name.to_owned(), value.to_owned()); - } - } - } - } - "local_option" => { - if let Ok(m) = serde_json::from_str::>(value) { - if let Some(name) = m.get("name") { - if let Some(value) = m.get("value") { - LocalConfig::set_option(name.to_owned(), value.to_owned()); - } - } - } - } - "input_os_password" => { - Session::input_os_password(value.to_owned(), true); - } - "restart_remote_device" => { - Session::restart_remote_device(); - } - // File Action - "read_remote_dir" => { - if let Ok(m) = serde_json::from_str::>(value) { - if let (Some(path), Some(show_hidden), Some(session)) = ( - m.get("path"), - m.get("show_hidden"), - Session::get().read().unwrap().as_ref(), - ) { - session.read_remote_dir(path.to_owned(), show_hidden.eq("true")); - } - } - } - "send_files" => { - if let Ok(m) = serde_json::from_str::>(value) { - if let ( - Some(id), - Some(path), - Some(to), - Some(file_num), - Some(show_hidden), - Some(is_remote), - ) = ( - m.get("id"), - m.get("path"), - m.get("to"), - m.get("file_num"), - m.get("show_hidden"), - m.get("is_remote"), - ) { - Session::send_files( - id.parse().unwrap_or(0), - path.to_owned(), - to.to_owned(), - file_num.parse().unwrap_or(0), - show_hidden.eq("true"), - is_remote.eq("true"), - ); - } - } - } - "set_confirm_override_file" => { - if let Ok(m) = serde_json::from_str::>(value) { - if let ( - Some(id), - Some(file_num), - Some(need_override), - Some(remember), - Some(is_upload), - ) = ( - m.get("id"), - m.get("file_num"), - m.get("need_override"), - m.get("remember"), - m.get("is_upload"), - ) { - Session::set_confirm_override_file( - id.parse().unwrap_or(0), - file_num.parse().unwrap_or(0), - need_override.eq("true"), - remember.eq("true"), - is_upload.eq("true"), - ); - } - } - } - "remove_file" => { - if let Ok(m) = serde_json::from_str::>(value) { - if let ( - Some(id), - Some(path), - Some(file_num), - Some(is_remote), - Some(session), - ) = ( - m.get("id"), - m.get("path"), - m.get("file_num"), - m.get("is_remote"), - Session::get().write().unwrap().as_mut(), - ) { - session.remove_file( - id.parse().unwrap_or(0), - path.to_owned(), - file_num.parse().unwrap_or(0), - is_remote.eq("true"), - ); - } - } - } - "read_dir_recursive" => { - if let Ok(m) = serde_json::from_str::>(value) { - if let (Some(id), Some(path), Some(is_remote), Some(session)) = ( - m.get("id"), - m.get("path"), - m.get("is_remote"), - Session::get().write().unwrap().as_mut(), - ) { - session.remove_dir_all( - id.parse().unwrap_or(0), - path.to_owned(), - is_remote.eq("true"), - ); - } - } - } - "remove_all_empty_dirs" => { - if let Ok(m) = serde_json::from_str::>(value) { - if let (Some(id), Some(path), Some(is_remote), Some(session)) = ( - m.get("id"), - m.get("path"), - m.get("is_remote"), - Session::get().write().unwrap().as_mut(), - ) { - session.remove_dir( - id.parse().unwrap_or(0), - path.to_owned(), - is_remote.eq("true"), - ); - } - } - } - "cancel_job" => { - if let (Ok(id), Some(session)) = - (value.parse(), Session::get().write().unwrap().as_mut()) - { - session.cancel_job(id); - } - } - "create_dir" => { - if let Ok(m) = serde_json::from_str::>(value) { - if let (Some(id), Some(path), Some(is_remote), Some(session)) = ( - m.get("id"), - m.get("path"), - m.get("is_remote"), - Session::get().write().unwrap().as_mut(), - ) { - session.create_dir( - id.parse().unwrap_or(0), - path.to_owned(), - is_remote.eq("true"), - ); - } - } - } - // Server Side - "permanent_password" => Config::set_permanent_password(value), - "temporary_password" => { - password::update_temporary_password(); - } - #[cfg(target_os = "android")] - "chat_server_mode" => { - if let Ok(m) = serde_json::from_str::>(value) { - if let (Some(Value::Number(id)), Some(Value::String(text))) = - (m.get("id"), m.get("text")) - { - let id = id.as_i64().unwrap_or(0); - connection_manager::send_chat(id as i32, text.to_owned()); - } - } - } - "home_dir" => { - *config::APP_HOME_DIR.write().unwrap() = value.to_owned(); - } - #[cfg(target_os = "android")] - "login_res" => { - if let Ok(m) = serde_json::from_str::>(value) { - if let (Some(Value::Number(id)), Some(Value::Bool(res))) = - (m.get("id"), m.get("res")) - { - let id = id.as_i64().unwrap_or(0); - connection_manager::on_login_res(id as i32, *res); - } - } - } - #[cfg(target_os = "android")] - "stop_service" => { - Config::set_option("stop-service".into(), "Y".into()); - crate::rendezvous_mediator::RendezvousMediator::restart(); - } - #[cfg(target_os = "android")] - "start_service" => { - Config::set_option("stop-service".into(), "".into()); - crate::rendezvous_mediator::RendezvousMediator::restart(); - } - #[cfg(target_os = "android")] - "close_conn" => { - if let Ok(id) = value.parse::() { - connection_manager::close_conn(id); - }; - } - _ => { - log::error!("Unknown name of set_by_name: {}", name); - } - } - } - } -} - -#[cfg(target_os = "android")] -pub mod server_side { - use hbb_common::{config::Config, log}; - use jni::{ - objects::{JClass, JString}, - sys::jstring, - JNIEnv, - }; - - use crate::start_server; - - #[no_mangle] - pub unsafe extern "system" fn Java_com_carriez_flutter_1hbb_MainService_startServer( - env: JNIEnv, - _class: JClass, - ) { - log::debug!("startServer from java"); - std::thread::spawn(move || start_server(true)); - } - - #[no_mangle] - pub unsafe extern "system" fn Java_com_carriez_flutter_1hbb_MainService_translateLocale( - env: JNIEnv, - _class: JClass, - locale: JString, - input: JString, - ) -> jstring { - let res = if let (Ok(input), Ok(locale)) = (env.get_string(input), env.get_string(locale)) { - let input: String = input.into(); - let locale: String = locale.into(); - crate::client::translate_locale(input, &locale) - } else { - "".into() - }; - return env.new_string(res).unwrap_or(input).into_inner(); - } - - #[no_mangle] - pub unsafe extern "system" fn Java_com_carriez_flutter_1hbb_MainService_refreshScreen( - _env: JNIEnv, - _class: JClass, - ) { - crate::server::video_service::refresh() - } -} diff --git a/src/platform/linux.rs b/src/platform/linux.rs index 85947a143..0ead52f31 100644 --- a/src/platform/linux.rs +++ b/src/platform/linux.rs @@ -629,3 +629,9 @@ extern "C" { pub fn quit_gui() { unsafe { gtk_main_quit() }; } + +pub fn check_super_user_permission() -> ResultType { + // TODO: replace echo with a rustdesk's program, which is location-fixed and non-gui. + let status = std::process::Command::new("pkexec").arg("echo").status()?; + Ok(status.success() && status.code() == Some(0)) +} diff --git a/src/platform/windows.rs b/src/platform/windows.rs index cb0fd778f..fa9fb5b10 100644 --- a/src/platform/windows.rs +++ b/src/platform/windows.rs @@ -8,7 +8,7 @@ use hbb_common::{ }; use std::io::prelude::*; use std::{ - ffi::OsString, + ffi::{CString, OsString}, fs, io, mem, sync::{Arc, Mutex}, time::{Duration, Instant}, @@ -17,7 +17,8 @@ use winapi::{ shared::{minwindef::*, ntdef::NULL, windef::*}, um::{ errhandlingapi::GetLastError, handleapi::CloseHandle, minwinbase::STILL_ACTIVE, - processthreadsapi::GetExitCodeProcess, winbase::*, wingdi::*, winnt::HANDLE, winuser::*, + processthreadsapi::GetExitCodeProcess, shellapi::ShellExecuteA, winbase::*, wingdi::*, + winnt::HANDLE, winuser::*, }, }; use windows_service::{ @@ -1418,3 +1419,17 @@ pub fn get_user_token(session_id: u32, as_user: bool) -> HANDLE { } } } + +pub fn check_super_user_permission() -> ResultType { + unsafe { + let ret = ShellExecuteA( + NULL as _, + CString::new("runas")?.as_ptr() as _, + CString::new("cmd")?.as_ptr() as _, + CString::new("/c /q")?.as_ptr() as _, + NULL as _, + SW_SHOWNORMAL, + ); + return Ok(ret as i32 > 32); + } +} diff --git a/src/port_forward.rs b/src/port_forward.rs index a17ee8259..9a697da42 100644 --- a/src/port_forward.rs +++ b/src/port_forward.rs @@ -1,7 +1,7 @@ use crate::client::*; use hbb_common::{ allow_err, bail, - config::CONNECT_TIMEOUT, + config::READ_TIMEOUT, futures::{SinkExt, StreamExt}, log, message_proto::*, @@ -105,22 +105,61 @@ async fn connect_and_login( key: &str, token: &str, is_rdp: bool, +) -> ResultType> { + let mut res = connect_and_login_2( + id, + password, + ui_receiver, + interface.clone(), + forward, + key, + token, + is_rdp, + ) + .await; + if res.is_err() && interface.is_force_relay() { + res = connect_and_login_2( + id, + password, + ui_receiver, + interface, + forward, + key, + token, + is_rdp, + ) + .await; + } + res +} + +async fn connect_and_login_2( + id: &str, + password: &str, + ui_receiver: &mut mpsc::UnboundedReceiver, + interface: impl Interface, + forward: &mut Framed, + key: &str, + token: &str, + is_rdp: bool, ) -> ResultType> { let conn_type = if is_rdp { ConnType::RDP } else { ConnType::PORT_FORWARD }; - let (mut stream, _) = Client::start(id, key, token, conn_type).await?; + let (mut stream, direct) = Client::start(id, key, token, conn_type, interface.clone()).await?; let mut interface = interface; let mut buffer = Vec::new(); + let mut received = false; loop { tokio::select! { - res = timeout(CONNECT_TIMEOUT, stream.next()) => match res { + res = timeout(READ_TIMEOUT, stream.next()) => match res { Err(_) => { bail!("Timeout"); } Ok(Some(Ok(bytes))) => { + received = true; let msg_in = Message::parse_from_bytes(&bytes)?; match msg_in.union { Some(message::Union::Hash(hash)) => { @@ -143,6 +182,11 @@ async fn connect_and_login( _ => {} } } + Ok(Some(Err(err))) => { + log::error!("Connection closed: {}", err); + interface.set_force_relay(direct, received); + bail!("Connection closed: {}", err); + } _ => { bail!("Reset by the peer"); } diff --git a/src/rendezvous_mediator.rs b/src/rendezvous_mediator.rs index 097eceb45..08a1316f0 100644 --- a/src/rendezvous_mediator.rs +++ b/src/rendezvous_mediator.rs @@ -1,7 +1,21 @@ -use crate::server::{check_zombie, new as new_server, ServerPtr}; +use std::collections::HashMap; +use std::{ + net::SocketAddr, + sync::{ + atomic::{AtomicBool, Ordering}, + Arc, Mutex, + }, + time::Instant, +}; + +use uuid::Uuid; + +use hbb_common::config::DiscoveryPeer; +use hbb_common::tcp::FramedStream; use hbb_common::{ allow_err, anyhow::bail, + config, config::{Config, REG_INTERVAL, RENDEZVOUS_PORT, RENDEZVOUS_TIMEOUT}, futures::future::join_all, log, @@ -15,15 +29,8 @@ use hbb_common::{ udp::FramedSocket, AddrMangle, IntoTargetAddr, ResultType, TargetAddr, }; -use std::{ - net::SocketAddr, - sync::{ - atomic::{AtomicBool, Ordering}, - Arc, Mutex, - }, - time::Instant, -}; -use uuid::Uuid; + +use crate::server::{check_zombie, new as new_server, ServerPtr}; type Message = RendezvousMessage; @@ -353,7 +360,14 @@ impl RendezvousMediator { { let uuid = Uuid::new_v4().to_string(); return self - .create_relay(ph.socket_addr.into(), relay_server, uuid, server, true, true) + .create_relay( + ph.socket_addr.into(), + relay_server, + uuid, + server, + true, + true, + ) .await; } let peer_addr = AddrMangle::decode(&ph.socket_addr); @@ -540,3 +554,188 @@ async fn direct_server(server: ServerPtr) { } } } + +#[inline] +pub fn get_broadcast_port() -> u16 { + (RENDEZVOUS_PORT + 3) as _ +} + +pub fn get_mac() -> String { + #[cfg(not(any(target_os = "android", target_os = "ios")))] + if let Ok(Some(mac)) = mac_address::get_mac_address() { + mac.to_string() + } else { + "".to_owned() + } + #[cfg(any(target_os = "android", target_os = "ios"))] + "".to_owned() +} + +fn lan_discovery() -> ResultType<()> { + let addr = SocketAddr::from(([0, 0, 0, 0], get_broadcast_port())); + let socket = std::net::UdpSocket::bind(addr)?; + socket.set_read_timeout(Some(std::time::Duration::from_millis(1000)))?; + log::info!("lan discovery listener started"); + loop { + let mut buf = [0; 2048]; + if let Ok((len, addr)) = socket.recv_from(&mut buf) { + if let Ok(msg_in) = Message::parse_from_bytes(&buf[0..len]) { + match msg_in.union { + Some(rendezvous_message::Union::PeerDiscovery(p)) => { + if p.cmd == "ping" { + let mut msg_out = Message::new(); + let peer = PeerDiscovery { + cmd: "pong".to_owned(), + mac: get_mac(), + id: Config::get_id(), + hostname: whoami::hostname(), + username: crate::platform::get_active_username(), + platform: whoami::platform().to_string(), + ..Default::default() + }; + msg_out.set_peer_discovery(peer); + socket.send_to(&msg_out.write_to_bytes()?, addr).ok(); + } + } + _ => {} + } + } + } + } +} + +#[tokio::main(flavor = "current_thread")] +pub async fn query_online_states, Vec)>(ids: Vec, f: F) { + let test = false; + if test { + sleep(1.5).await; + let mut onlines = ids; + let offlines = onlines.drain((onlines.len() / 2)..).collect(); + f(onlines, offlines) + } else { + let query_begin = Instant::now(); + let query_timeout = std::time::Duration::from_millis(3_000); + loop { + if SHOULD_EXIT.load(Ordering::SeqCst) { + break; + } + match query_online_states_(&ids, query_timeout).await { + Ok((onlines, offlines)) => { + f(onlines, offlines); + break; + } + Err(e) => { + log::debug!("{}", &e); + } + } + + if query_begin.elapsed() > query_timeout { + log::debug!("query onlines timeout {:?}", query_timeout); + break; + } + + sleep(1.5).await; + } + } +} + +async fn create_online_stream() -> ResultType { + let (mut rendezvous_server, servers, contained) = crate::get_rendezvous_server(1_000).await; + let tmp: Vec<&str> = rendezvous_server.split(":").collect(); + if tmp.len() != 2 { + bail!("Invalid server address: {}", rendezvous_server); + } + let port: u16 = tmp[1].parse()?; + if port == 0 { + bail!("Invalid server address: {}", rendezvous_server); + } + let online_server = format!("{}:{}", tmp[0], port - 1); + let server_addr = socket_client::get_target_addr(&online_server)?; + socket_client::connect_tcp( + server_addr, + Config::get_any_listen_addr(), + RENDEZVOUS_TIMEOUT, + ) + .await +} + +async fn query_online_states_( + ids: &Vec, + timeout: std::time::Duration, +) -> ResultType<(Vec, Vec)> { + let query_begin = Instant::now(); + + let mut msg_out = RendezvousMessage::new(); + msg_out.set_online_request(OnlineRequest { + id: Config::get_id(), + peers: ids.clone(), + ..Default::default() + }); + + loop { + if SHOULD_EXIT.load(Ordering::SeqCst) { + // No need to care about onlines + return Ok((Vec::new(), Vec::new())); + } + + let mut socket = create_online_stream().await?; + socket.send(&msg_out).await?; + match socket.next_timeout(RENDEZVOUS_TIMEOUT).await { + Some(Ok(bytes)) => { + if let Ok(msg_in) = RendezvousMessage::parse_from_bytes(&bytes) { + match msg_in.union { + Some(rendezvous_message::Union::OnlineResponse(online_response)) => { + let states = online_response.states; + let mut onlines = Vec::new(); + let mut offlines = Vec::new(); + for i in 0..ids.len() { + // bytes index from left to right + let bit_value = 0x01 << (7 - i % 8); + if (states[i / 8] & bit_value) == bit_value { + onlines.push(ids[i].clone()); + } else { + offlines.push(ids[i].clone()); + } + } + return Ok((onlines, offlines)); + } + _ => { + // ignore + } + } + } + } + Some(Err(e)) => { + log::error!("Failed to receive {e}"); + } + None => { + // TODO: Make sure socket closed? + bail!("Online stream receives None"); + } + } + + if query_begin.elapsed() > timeout { + bail!("Try query onlines timeout {:?}", &timeout); + } + + sleep(300.0).await; + } +} + +#[cfg(test)] +mod tests { + #[test] + fn test_query_onlines() { + super::query_online_states( + vec![ + "152183996".to_owned(), + "165782066".to_owned(), + "155323351".to_owned(), + "460952777".to_owned(), + ], + |onlines: Vec, offlines: Vec| { + println!("onlines: {:?}, offlines: {:?}", &onlines, &offlines); + }, + ); + } +} diff --git a/src/server.rs b/src/server.rs index b9bd13e95..12b5afe56 100644 --- a/src/server.rs +++ b/src/server.rs @@ -307,12 +307,26 @@ pub fn check_zombie() { }); } +/// Start the host server that allows the remote peer to control the current machine. +/// +/// # Arguments +/// +/// * `is_server` - Whether the current client is definitely the server. +/// If true, the server will be started. +/// Otherwise, client will check if there's already a server and start one if not. #[cfg(any(target_os = "android", target_os = "ios"))] #[tokio::main] pub async fn start_server(is_server: bool) { crate::RendezvousMediator::start_all().await; } +/// Start the host server that allows the remote peer to control the current machine. +/// +/// # Arguments +/// +/// * `is_server` - Whether the current client is definitely the server. +/// If true, the server will be started. +/// Otherwise, client will check if there's already a server and start one if not. #[cfg(not(any(target_os = "android", target_os = "ios")))] #[tokio::main] pub async fn start_server(is_server: bool) { diff --git a/src/server/connection.rs b/src/server/connection.rs index 90548afc7..85b10a67d 100644 --- a/src/server/connection.rs +++ b/src/server/connection.rs @@ -5,8 +5,8 @@ use crate::clipboard_file::*; use crate::common::update_clipboard; use crate::video_service; #[cfg(any(target_os = "android", target_os = "ios"))] -use crate::{common::MOBILE_INFO2, mobile::connection_manager::start_channel}; -use crate::{ipc}; +use crate::{common::DEVICE_NAME, flutter::connection_manager::start_channel}; +use crate::{ipc, VERSION}; use hbb_common::{ config::Config, fs, @@ -644,7 +644,7 @@ impl Connection { } #[cfg(target_os = "android")] { - pi.hostname = MOBILE_INFO2.lock().unwrap().clone(); + pi.hostname = DEVICE_NAME.lock().unwrap().clone(); pi.platform = "Android".into(); } #[cfg(feature = "hwcodec")] diff --git a/src/ui.rs b/src/ui.rs index 6b3e8b292..78654e9ec 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -1,13 +1,12 @@ -mod cm; -#[cfg(feature = "inline")] -mod inline; -#[cfg(target_os = "macos")] -mod macos; -pub mod remote; -#[cfg(target_os = "windows")] -pub mod win_privacy; -use crate::common::SOFTWARE_UPDATE_URL; -use crate::ipc; +use std::{ + collections::HashMap, + iter::FromIterator, + process::Child, + sync::{Arc, Mutex}, +}; + +use sciter::Value; + use hbb_common::{ allow_err, config::{self, Config, LocalConfig, PeerConfig, RENDEZVOUS_PORT, RENDEZVOUS_TIMEOUT}, @@ -19,14 +18,35 @@ use hbb_common::{ tcp::FramedStream, tokio::{self, sync::mpsc, time}, }; -use sciter::Value; -use std::{ - collections::HashMap, - iter::FromIterator, - process::Child, - sync::{Arc, Mutex}, + +use crate::common::{get_app_name, SOFTWARE_UPDATE_URL}; +use crate::ipc; +use crate::ui_interface::{ + check_mouse_time, closing, create_shortcut, current_is_wayland, fix_login_wayland, + forget_password, get_api_server, get_async_job_status, get_connect_status, get_error, get_fav, + get_icon, get_lan_peers, get_langs, get_license, get_local_option, get_mouse_time, + get_new_version, get_option, get_options, get_peer, get_peer_option, get_recent_sessions, + get_remote_id, get_size, get_socks, get_software_ext, get_software_store_path, + get_software_update_url, get_uuid, get_version, goto_install, has_hwcodec, + has_rendezvous_service, install_me, install_path, is_can_screen_recording, is_installed, + is_installed_daemon, is_installed_lower_version, is_login_wayland, is_ok_change_id, + is_process_trusted, is_rdp_service_open, is_share_rdp, is_xfce, modify_default_login, + new_remote, open_url, peer_has_password, permanent_password, post_request, + recent_sessions_updated, remove_peer, run_without_install, set_local_option, set_option, + set_options, set_peer_option, set_permanent_password, set_remote_id, set_share_rdp, set_socks, + show_run_without_install, store_fav, t, temporary_password, test_if_valid_server, update_me, + update_temporary_password, using_public_server, }; +mod cm; +#[cfg(feature = "inline")] +mod inline; +#[cfg(target_os = "macos")] +mod macos; +pub mod remote; +#[cfg(target_os = "windows")] +pub mod win_privacy; + type Message = RendezvousMessage; pub type Childs = Arc)>>; @@ -37,15 +57,6 @@ lazy_static::lazy_static! { static ref STUPID_VALUES: Mutex>>> = Default::default(); } -struct UI( - Childs, - Arc>, - Arc>>, - Arc>, - mpsc::UnboundedSender, - Arc>, -); - struct UIHostHandler; pub fn start(args: &mut [String]) { @@ -102,16 +113,14 @@ pub fn start(args: &mut [String]) { args[1] = id; } if args.is_empty() { - let childs: Childs = Default::default(); - let cloned = childs.clone(); - std::thread::spawn(move || check_zombie(cloned)); + let child: Childs = Default::default(); + std::thread::spawn(move || check_zombie(child)); crate::common::check_software_update(); - frame.event_handler(UI::new(childs)); + frame.event_handler(UI {}); frame.sciter_handler(UIHostHandler {}); page = "index.html"; } else if args[0] == "--install" { - let childs: Childs = Default::default(); - frame.event_handler(UI::new(childs)); + frame.event_handler(UI {}); frame.sciter_handler(UIHostHandler {}); page = "install.html"; } else if args[0] == "--cm" { @@ -173,20 +182,11 @@ pub fn start(args: &mut [String]) { frame.run_app(); } -impl UI { - fn new(childs: Childs) -> Self { - let res = check_connect_status(true); - Self(childs, res.0, res.1, Default::default(), res.2, res.3) - } +struct UI {} - fn recent_sessions_updated(&mut self) -> bool { - let mut lock = self.0.lock().unwrap(); - if lock.0 { - lock.0 = false; - true - } else { - false - } +impl UI { + fn recent_sessions_updated(&self) -> bool { + recent_sessions_updated() } fn get_id(&self) -> String { @@ -194,182 +194,104 @@ impl UI { } fn temporary_password(&mut self) -> String { - self.5.lock().unwrap().clone() + temporary_password() } fn update_temporary_password(&self) { - allow_err!(ipc::update_temporary_password()); + update_temporary_password() } fn permanent_password(&self) -> String { - ipc::get_permanent_password() + permanent_password() } fn set_permanent_password(&self, password: String) { - allow_err!(ipc::set_permanent_password(password)); + set_permanent_password(password); } fn get_remote_id(&mut self) -> String { - LocalConfig::get_remote_id() + get_remote_id() } fn set_remote_id(&mut self, id: String) { - LocalConfig::set_remote_id(&id); + set_remote_id(id); } fn goto_install(&mut self) { - allow_err!(crate::run_me(vec!["--install"])); + goto_install(); } fn install_me(&mut self, _options: String, _path: String) { - #[cfg(windows)] - std::thread::spawn(move || { - allow_err!(crate::platform::windows::install_me( - &_options, _path, false, false - )); - std::process::exit(0); - }); + install_me(_options, _path, false, false); } fn update_me(&self, _path: String) { - #[cfg(target_os = "linux")] - { - std::process::Command::new("pkexec") - .args(&["apt", "install", "-f", &_path]) - .spawn() - .ok(); - std::fs::remove_file(&_path).ok(); - crate::run_me(Vec::<&str>::new()).ok(); - } - #[cfg(windows)] - { - let mut path = _path; - if path.is_empty() { - if let Ok(tmp) = std::env::current_exe() { - path = tmp.to_string_lossy().to_string(); - } - } - std::process::Command::new(path) - .arg("--update") - .spawn() - .ok(); - std::process::exit(0); - } + update_me(_path); } fn run_without_install(&self) { - crate::run_me(vec!["--noinstall"]).ok(); - std::process::exit(0); + run_without_install(); } fn show_run_without_install(&self) -> bool { - let mut it = std::env::args(); - if let Some(tmp) = it.next() { - if crate::is_setup(&tmp) { - return it.next() == None; - } - } - false + show_run_without_install() } fn has_rendezvous_service(&self) -> bool { - #[cfg(all(windows, feature = "hbbs"))] - return crate::platform::is_win_server() - && crate::platform::windows::get_license().is_some(); - return false; + has_rendezvous_service() } fn get_license(&self) -> String { - #[cfg(windows)] - if let Some(lic) = crate::platform::windows::get_license() { - return format!( - "
Key: {}
Host: {} Api: {}", - lic.key, lic.host, lic.api - ); - } - Default::default() + get_license() } fn get_option(&self, key: String) -> String { - self.get_option_(&key) - } - - fn get_option_(&self, key: &str) -> String { - if let Some(v) = self.2.lock().unwrap().get(key) { - v.to_owned() - } else { - "".to_owned() - } + get_option(key) } fn get_local_option(&self, key: String) -> String { - LocalConfig::get_option(&key) + get_local_option(key) } fn set_local_option(&self, key: String, value: String) { - LocalConfig::set_option(key, value); + set_local_option(key, value); } fn peer_has_password(&self, id: String) -> bool { - !PeerConfig::load(&id).password.is_empty() + peer_has_password(id) } fn forget_password(&self, id: String) { - let mut c = PeerConfig::load(&id); - c.password.clear(); - c.store(&id); + forget_password(id) } fn get_peer_option(&self, id: String, name: String) -> String { - let c = PeerConfig::load(&id); - c.options.get(&name).unwrap_or(&"".to_owned()).to_owned() + get_peer_option(id, name) } fn set_peer_option(&self, id: String, name: String, value: String) { - let mut c = PeerConfig::load(&id); - if value.is_empty() { - c.options.remove(&name); - } else { - c.options.insert(name, value); - } - c.store(&id); + set_peer_option(id, name, value) } fn using_public_server(&self) -> bool { - crate::get_custom_rendezvous_server(self.get_option_("custom-rendezvous-server")).is_empty() + using_public_server() } fn get_options(&self) -> Value { + let hashmap: HashMap = serde_json::from_str(&get_options()).unwrap(); let mut m = Value::map(); - for (k, v) in self.2.lock().unwrap().iter() { + for (k, v) in hashmap { m.set_item(k, v); } m } fn test_if_valid_server(&self, host: String) -> String { - hbb_common::socket_client::test_if_valid_server(&host) + test_if_valid_server(host) } fn get_sound_inputs(&self) -> Value { - let mut a = Value::array(0); - #[cfg(windows)] - { - let inputs = Arc::new(Mutex::new(Vec::new())); - let cloned = inputs.clone(); - // can not call below in UI thread, because conflict with sciter sound com initialization - std::thread::spawn(move || *cloned.lock().unwrap() = get_sound_inputs()) - .join() - .ok(); - for name in inputs.lock().unwrap().drain(..) { - a.push(name); - } - } - #[cfg(not(windows))] - for name in get_sound_inputs() { - a.push(name); - } - a + Value::from_iter(get_sound_inputs()) } fn set_options(&self, v: Value) { @@ -383,119 +305,64 @@ impl UI { } } } - - *self.2.lock().unwrap() = m.clone(); - ipc::set_options(m).ok(); + set_options(m); } fn set_option(&self, key: String, value: String) { - #[cfg(target_os = "macos")] - if &key == "stop-service" { - let is_stop = value == "Y"; - if is_stop && crate::platform::macos::uninstall() { - return; - } - } - let mut options = self.2.lock().unwrap(); - if value.is_empty() { - options.remove(&key); - } else { - options.insert(key.clone(), value.clone()); - } - ipc::set_options(options.clone()).ok(); + set_option(key, value); } fn install_path(&mut self) -> String { - #[cfg(windows)] - return crate::platform::windows::get_install_info().1; - #[cfg(not(windows))] - return "".to_owned(); + install_path() } fn get_socks(&self) -> Value { - let s = ipc::get_socks(); - match s { - None => Value::null(), - Some(s) => { - let mut v = Value::array(0); - v.push(s.proxy); - v.push(s.username); - v.push(s.password); - v - } - } + Value::from_iter(get_socks()) } fn set_socks(&self, proxy: String, username: String, password: String) { - ipc::set_socks(config::Socks5Server { - proxy, - username, - password, - }) - .ok(); + set_socks(proxy, username, password) } fn is_installed(&self) -> bool { - crate::platform::is_installed() + is_installed() } fn is_rdp_service_open(&self) -> bool { - #[cfg(windows)] - return self.is_installed() && crate::platform::windows::is_rdp_service_open(); - #[cfg(not(windows))] - return false; + is_rdp_service_open() } fn is_share_rdp(&self) -> bool { - #[cfg(windows)] - return crate::platform::windows::is_share_rdp(); - #[cfg(not(windows))] - return false; + is_share_rdp() } fn set_share_rdp(&self, _enable: bool) { - #[cfg(windows)] - crate::platform::windows::set_share_rdp(_enable); + set_share_rdp(_enable); } fn is_installed_lower_version(&self) -> bool { - #[cfg(not(windows))] - return false; - #[cfg(windows)] - { - let installed_version = crate::platform::windows::get_installed_version(); - let a = hbb_common::get_version_number(crate::VERSION); - let b = hbb_common::get_version_number(&installed_version); - return a > b; - } + is_installed_lower_version() } fn closing(&mut self, x: i32, y: i32, w: i32, h: i32) { - crate::server::input_service::fix_key_down_timeout_at_exit(); - LocalConfig::set_size(x, y, w, h); + closing(x, y, w, h) } fn get_size(&mut self) -> Value { - let s = LocalConfig::get_size(); - let mut v = Value::array(0); - v.push(s.0); - v.push(s.1); - v.push(s.2); - v.push(s.3); - v + Value::from_iter(get_size()) } fn get_mouse_time(&self) -> f64 { - self.1.lock().unwrap().2 as _ + get_mouse_time() } fn check_mouse_time(&self) { - allow_err!(self.4.send(ipc::Data::MouseMoveTime(0))); + check_mouse_time() } fn get_connect_status(&mut self) -> Value { let mut v = Value::array(0); - let x = self.1.lock().unwrap().clone(); + let x = get_connect_status(); v.push(x.0); v.push(x.1); v.push(x.3); @@ -515,12 +382,12 @@ impl UI { } fn get_peer(&self, id: String) -> Value { - let c = PeerConfig::load(&id); + let c = get_peer(id.clone()); Self::get_peer_value(id, c) } fn get_fav(&self) -> Value { - Value::from_iter(LocalConfig::get_fav()) + Value::from_iter(get_fav()) } fn store_fav(&self, fav: Value) { @@ -532,12 +399,12 @@ impl UI { } } }); - LocalConfig::set_fav(tmp); + store_fav(tmp); } fn get_recent_sessions(&mut self) -> Value { // to-do: limit number of recent sessions, and remove old peer file - let peers: Vec = PeerConfig::peers() + let peers: Vec = get_recent_sessions() .drain(..) .map(|p| Self::get_peer_value(p.0, p.2)) .collect(); @@ -545,11 +412,11 @@ impl UI { } fn get_icon(&mut self) -> String { - crate::get_icon() + get_icon() } fn remove_peer(&mut self, id: String) { - PeerConfig::remove(&id); + remove_peer(id) } fn remove_discovered(&mut self, id: String) { @@ -563,145 +430,67 @@ impl UI { } fn new_remote(&mut self, id: String, remote_type: String) { - let mut lock = self.0.lock().unwrap(); - let args = vec![format!("--{}", remote_type), id.clone()]; - let key = (id.clone(), remote_type.clone()); - if let Some(c) = lock.1.get_mut(&key) { - if let Ok(Some(_)) = c.try_wait() { - lock.1.remove(&key); - } else { - if remote_type == "rdp" { - allow_err!(c.kill()); - std::thread::sleep(std::time::Duration::from_millis(30)); - c.try_wait().ok(); - lock.1.remove(&key); - } else { - return; - } - } - } - match crate::run_me(args) { - Ok(child) => { - lock.1.insert(key, child); - } - Err(err) => { - log::error!("Failed to spawn remote: {}", err); - } - } + new_remote(id, remote_type) } fn is_process_trusted(&mut self, _prompt: bool) -> bool { - #[cfg(target_os = "macos")] - return crate::platform::macos::is_process_trusted(_prompt); - #[cfg(not(target_os = "macos"))] - return true; + is_process_trusted(_prompt) } fn is_can_screen_recording(&mut self, _prompt: bool) -> bool { - #[cfg(target_os = "macos")] - return crate::platform::macos::is_can_screen_recording(_prompt); - #[cfg(not(target_os = "macos"))] - return true; + is_can_screen_recording(_prompt) } fn is_installed_daemon(&mut self, _prompt: bool) -> bool { - #[cfg(target_os = "macos")] - return crate::platform::macos::is_installed_daemon(_prompt); - #[cfg(not(target_os = "macos"))] - return true; + is_installed_daemon(_prompt) } fn get_error(&mut self) -> String { - #[cfg(target_os = "linux")] - { - let dtype = crate::platform::linux::get_display_server(); - if "wayland" == dtype { - return "".to_owned(); - } - if dtype != "x11" { - return format!( - "{} {}, {}", - self.t("Unsupported display server ".to_owned()), - dtype, - self.t("x11 expected".to_owned()), - ); - } - } - return "".to_owned(); + get_error() } fn is_login_wayland(&mut self) -> bool { - #[cfg(target_os = "linux")] - return crate::platform::linux::is_login_wayland(); - #[cfg(not(target_os = "linux"))] - return false; + is_login_wayland() } fn fix_login_wayland(&mut self) { - /* - #[cfg(target_os = "linux")] - crate::platform::linux::fix_login_wayland(); - */ + fix_login_wayland() } fn current_is_wayland(&mut self) -> bool { - #[cfg(target_os = "linux")] - return crate::platform::linux::current_is_wayland(); - #[cfg(not(target_os = "linux"))] - return false; + current_is_wayland() } fn modify_default_login(&mut self) -> String { - /* - #[cfg(target_os = "linux")] - return crate::platform::linux::modify_default_login(); - #[cfg(not(target_os = "linux"))] - */ - return "".to_owned(); + modify_default_login() } fn get_software_update_url(&self) -> String { - SOFTWARE_UPDATE_URL.lock().unwrap().clone() + get_software_update_url() } fn get_new_version(&self) -> String { - hbb_common::get_version_from_url(&*SOFTWARE_UPDATE_URL.lock().unwrap()) + get_new_version() } fn get_version(&self) -> String { - crate::VERSION.to_owned() + get_version() } fn get_app_name(&self) -> String { - crate::get_app_name() + get_app_name() } fn get_software_ext(&self) -> String { - #[cfg(windows)] - let p = "exe"; - #[cfg(target_os = "macos")] - let p = "dmg"; - #[cfg(target_os = "linux")] - let p = "deb"; - p.to_owned() + get_software_ext() } fn get_software_store_path(&self) -> String { - let mut p = std::env::temp_dir(); - let name = SOFTWARE_UPDATE_URL - .lock() - .unwrap() - .split("/") - .last() - .map(|x| x.to_owned()) - .unwrap_or(crate::get_app_name()); - p.push(name); - format!("{}.{}", p.to_string_lossy(), self.get_software_ext()) + get_software_store_path() } fn create_shortcut(&self, _id: String) { - #[cfg(windows)] - crate::platform::windows::create_shortcut(&_id).ok(); + create_shortcut(_id) } fn discover(&self) { @@ -711,79 +500,56 @@ impl UI { } fn get_lan_peers(&self) -> String { - serde_json::to_string(&config::LanPeers::load().peers).unwrap_or_default() + let peers = get_lan_peers() + .into_iter() + .map(|(id, peer)| (id, peer.username, peer.hostname, peer.platform)) + .collect::>(); + serde_json::to_string(&peers).unwrap_or_default() } fn get_uuid(&self) -> String { - base64::encode(hbb_common::get_uuid()) + get_uuid() } fn open_url(&self, url: String) { - #[cfg(windows)] - let p = "explorer"; - #[cfg(target_os = "macos")] - let p = "open"; - #[cfg(target_os = "linux")] - let p = if std::path::Path::new("/usr/bin/firefox").exists() { - "firefox" - } else { - "xdg-open" - }; - allow_err!(std::process::Command::new(p).arg(url).spawn()); + open_url(url) } fn change_id(&self, id: String) { - let status = self.3.clone(); - *status.lock().unwrap() = " ".to_owned(); let old_id = self.get_id(); - std::thread::spawn(move || { - *status.lock().unwrap() = change_id(id, old_id).to_owned(); - }); + change_id(id, old_id); } fn post_request(&self, url: String, body: String, header: String) { - let status = self.3.clone(); - *status.lock().unwrap() = " ".to_owned(); - std::thread::spawn(move || { - *status.lock().unwrap() = match crate::post_request_sync(url, body, &header) { - Err(err) => err.to_string(), - Ok(text) => text, - }; - }); + post_request(url, body, header) } fn is_ok_change_id(&self) -> bool { - machine_uid::get().is_ok() + is_ok_change_id() } fn get_async_job_status(&self) -> String { - self.3.clone().lock().unwrap().clone() + get_async_job_status() } fn t(&self, name: String) -> String { - crate::client::translate(name) + t(name) } fn is_xfce(&self) -> bool { - crate::platform::is_xfce() + is_xfce() } fn get_api_server(&self) -> String { - crate::get_api_server( - self.get_option_("api-server"), - self.get_option_("custom-rendezvous-server"), - ) + get_api_server() } fn has_hwcodec(&self) -> bool { - #[cfg(not(feature = "hwcodec"))] - return false; - #[cfg(feature = "hwcodec")] - return true; + has_hwcodec() } fn get_langs(&self) -> String { - crate::lang::LANGS.to_string() + get_langs() } } diff --git a/src/ui/ab.tis b/src/ui/ab.tis index 28fa62352..ac2efb7dd 100644 --- a/src/ui/ab.tis +++ b/src/ui/ab.tis @@ -245,7 +245,7 @@ class SearchBar: Reactor.Component { } event change $(input) (_, el) { - this.onChange(el.value.trim()); + this.onChange(el.value.trim().toLowerCase()); } function onChange(v) { @@ -297,8 +297,13 @@ class SessionList: Reactor.Component { if (!p) return this.sessions; var tmp = []; this.sessions.map(function(s) { - var name = s[4] || s.alias || s[0] || s.id || ""; - if (name.indexOf(p) >= 0) tmp.push(s); + var name = (s[4] || s.alias || "").toLowerCase(); + var id = (s[0] || s.id || "").toLowerCase(); + var user = (s[1] || "").toLowerCase(); + var hostname = (s[2] || "").toLowerCase(); + if (name.indexOf(p) >= 0 || id.indexOf(p) >= 0 || user.indexOf(p) >= 0 || hostname.indexOf(p) >= 0) { + tmp.push(s); + } }); return tmp; } @@ -316,7 +321,7 @@ class SessionList: Reactor.Component {
  • {translate('Connect')}
  • {translate('Transfer File')}
  • {translate('TCP Tunneling')}
  • - {false && !handler.using_public_server() &&
  • {svg_checkmark}{translate('Always connect via relay')}
  • } +
  • {svg_checkmark}{translate('Always connect via relay')}
  • RDP
  • {translate('WOL')}
  • @@ -396,7 +401,6 @@ class SessionList: Reactor.Component { if (el) { var force = handler.get_peer_option(id, "force-always-relay"); el.attributes.toggleClass("selected", force == "Y"); - el.attributes.toggleClass("line-through", force != "Y"); } var conn = this.$(menu #connect); if (conn) { diff --git a/src/ui/cm.rs b/src/ui/cm.rs index e2d912c63..3200d51b4 100644 --- a/src/ui/cm.rs +++ b/src/ui/cm.rs @@ -1,3 +1,5 @@ +#[cfg(target_os = "linux")] +use crate::ipc::start_pa; use crate::ipc::{self, new_listener, Connection, Data}; #[cfg(windows)] use clipboard::{ @@ -159,7 +161,7 @@ impl ConnectionManager { id, file_num, mut files, - overwrite_detection + overwrite_detection, } => { // cm has no show_hidden context // dummy remote, show_hidden, is_remote @@ -436,7 +438,7 @@ impl sciter::EventHandler for ConnectionManager { } } -enum ClipboardFileData { +pub enum ClipboardFileData { #[cfg(windows)] Clip((i32, ipc::ClipbaordFile)), Enable((i32, bool)), @@ -538,86 +540,9 @@ async fn start_ipc(cm: ConnectionManager) { crate::platform::quit_gui(); } -#[cfg(target_os = "linux")] -#[tokio::main(flavor = "current_thread")] -async fn start_pa() { - use crate::audio_service::AUDIO_DATA_SIZE_U8; - - match new_listener("_pa").await { - Ok(mut incoming) => { - loop { - if let Some(result) = incoming.next().await { - match result { - Ok(stream) => { - let mut stream = Connection::new(stream); - let mut device: String = "".to_owned(); - if let Some(Ok(Some(Data::Config((_, Some(x)))))) = - stream.next_timeout2(1000).await - { - device = x; - } - if !device.is_empty() { - device = crate::platform::linux::get_pa_source_name(&device); - } - if device.is_empty() { - device = crate::platform::linux::get_pa_monitor(); - } - if device.is_empty() { - continue; - } - let spec = pulse::sample::Spec { - format: pulse::sample::Format::F32le, - channels: 2, - rate: crate::platform::PA_SAMPLE_RATE, - }; - log::info!("pa monitor: {:?}", device); - // systemctl --user status pulseaudio.service - let mut buf: Vec = vec![0; AUDIO_DATA_SIZE_U8]; - match psimple::Simple::new( - None, // Use the default server - &crate::get_app_name(), // Our application’s name - pulse::stream::Direction::Record, // We want a record stream - Some(&device), // Use the default device - "record", // Description of our stream - &spec, // Our sample format - None, // Use default channel map - None, // Use default buffering attributes - ) { - Ok(s) => loop { - if let Ok(_) = s.read(&mut buf) { - let out = - if buf.iter().filter(|x| **x != 0).next().is_none() { - vec![] - } else { - buf.clone() - }; - if let Err(err) = stream.send_raw(out.into()).await { - log::error!("Failed to send audio data:{}", err); - break; - } - } - }, - Err(err) => { - log::error!("Could not create simple pulse: {}", err); - } - } - } - Err(err) => { - log::error!("Couldn't get pa client: {:?}", err); - } - } - } - } - } - Err(err) => { - log::error!("Failed to start pa ipc server: {}", err); - } - } -} - #[cfg(windows)] #[tokio::main(flavor = "current_thread")] -async fn start_clipboard_file( +pub async fn start_clipboard_file( cm: ConnectionManager, mut rx: mpsc::UnboundedReceiver, ) { diff --git a/src/ui/file_transfer.tis b/src/ui/file_transfer.tis index 7d50bdf7a..f32540b33 100644 --- a/src/ui/file_transfer.tis +++ b/src/ui/file_transfer.tis @@ -188,7 +188,8 @@ class JobTable: Reactor.Component { job.confirmed = true; return; }else if (job.type == "del-dir"){ - handler.remove_dir_all(job.id, job.path, job.is_remote); + // TODO: include_hidden is always true + handler.remove_dir_all(job.id, job.path, job.is_remote, true); job.confirmed = true; return; } diff --git a/src/ui/remote.rs b/src/ui/remote.rs index bcb79c522..9294c6e2b 100644 --- a/src/ui/remote.rs +++ b/src/ui/remote.rs @@ -25,8 +25,12 @@ use clipboard::{ use enigo::{self, Enigo, KeyboardControllable}; use hbb_common::{ allow_err, - config::{Config, LocalConfig, PeerConfig}, - fs, log, + config::{Config, LocalConfig, PeerConfig, TransferSerde}, + fs::{ + self, can_enable_overwrite_detection, get_job, get_string, new_send_confirm, + DigestCheckResult, RemoveJobMeta, TransferJobMeta, + }, + get_version_number, log, message_proto::{permission_info::Permission, *}, protobuf::Message as _, rendezvous_proto::ConnType, @@ -54,6 +58,7 @@ use crate::{ client::*, common::{self, check_clipboard, update_clipboard, ClipboardContext, CLIPBOARD_INTERVAL}, }; +use errno; type Video = AssetPtr; @@ -208,7 +213,7 @@ impl sciter::EventHandler for Handler { fn read_remote_dir(String, bool); fn send_chat(String); fn switch_display(i32); - fn remove_dir_all(i32, String, bool); + fn remove_dir_all(i32, String, bool, bool); fn confirm_delete_files(i32, i32); fn set_no_confirm(i32); fn cancel_job(i32); @@ -1653,12 +1658,21 @@ impl Remote { async fn io_loop(&mut self, key: &str, token: &str) { let stop_clipboard = self.start_clipboard(); let mut last_recv_time = Instant::now(); + let mut received = false; let conn_type = if self.handler.is_file_transfer() { ConnType::FILE_TRANSFER } else { ConnType::default() }; - match Client::start(&self.handler.id, key, token, conn_type).await { + match Client::start( + &self.handler.id, + key, + token, + conn_type, + self.handler.clone(), + ) + .await + { Ok((mut peer, direct)) => { SERVER_KEYBOARD_ENABLED.store(true, Ordering::SeqCst); SERVER_CLIPBOARD_ENABLED.store(true, Ordering::SeqCst); @@ -1681,11 +1695,13 @@ impl Remote { match res { Err(err) => { log::error!("Connection closed: {}", err); + self.handler.set_force_relay(direct, received); self.handler.msgbox("error", "Connection Error", &err.to_string()); break; } Ok(ref bytes) => { last_recv_time = Instant::now(); + received = true; self.data_count.fetch_add(bytes.len(), Ordering::Relaxed); if !self.handle_msg_from_peer(bytes, &mut peer).await { break @@ -2093,7 +2109,7 @@ impl Remote { } } } - Data::RemoveDirAll((id, path, is_remote)) => { + Data::RemoveDirAll((id, path, is_remote, include_hidden)) => { let sep = self.handler.get_path_sep(is_remote); if is_remote { let mut msg_out = Message::new(); @@ -2101,7 +2117,7 @@ impl Remote { file_action.set_all_files(ReadAllFiles { id, path: path.clone(), - include_hidden: true, + include_hidden, ..Default::default() }); msg_out.set_file_action(file_action); @@ -2109,7 +2125,7 @@ impl Remote { self.remove_jobs .insert(id, RemoveJob::new(Vec::new(), path, sep, is_remote)); } else { - match fs::get_recursive_files(&path, true) { + match fs::get_recursive_files(&path, include_hidden) { Ok(entries) => { let m = make_fd(id, &entries, true); self.handler.call("updateFolderFiles", &make_args!(m)); @@ -2270,18 +2286,18 @@ impl Remote { async fn send_opts_after_login(&self, peer: &mut Stream) { if let Some(opts) = self - .handler - .lc - .read() - .unwrap() - .get_option_message_after_login() - { - let mut misc = Misc::new(); - misc.set_option(opts); - let mut msg_out = Message::new(); - msg_out.set_misc(misc); - allow_err!(peer.send(&msg_out).await); - } + .handler + .lc + .read() + .unwrap() + .get_option_message_after_login() + { + let mut misc = Misc::new(); + misc.set_option(opts); + let mut msg_out = Message::new(); + msg_out.set_misc(misc); + allow_err!(peer.send(&msg_out).await); + } } async fn handle_msg_from_peer(&mut self, data: &[u8], peer: &mut Stream) -> bool { @@ -2892,6 +2908,24 @@ impl Interface for Handler { handle_test_delay(t, peer).await; } } + + fn set_force_relay(&mut self, direct: bool, received: bool) { + let mut lc = self.lc.write().unwrap(); + lc.force_relay = false; + if direct && !received { + let errno = errno::errno().0; + log::info!("errno is {}", errno); + // TODO: check mac and ios + if cfg!(windows) && errno == 10054 || !cfg!(windows) && errno == 104 { + lc.force_relay = true; + lc.set_option("force-always-relay".to_owned(), "Y".to_owned()); + } + } + } + + fn is_force_relay(&self) -> bool { + self.lc.read().unwrap().force_relay + } } impl Handler { diff --git a/src/ui_interface.rs b/src/ui_interface.rs new file mode 100644 index 000000000..a8e3be980 --- /dev/null +++ b/src/ui_interface.rs @@ -0,0 +1,874 @@ +use std::{ + collections::HashMap, + process::Child, + sync::{Arc, Mutex}, + time::SystemTime, +}; + +#[cfg(any(target_os = "android", target_os = "ios"))] +use hbb_common::password_security; +use hbb_common::{ + allow_err, + config::{self, Config, LocalConfig, PeerConfig, RENDEZVOUS_PORT, RENDEZVOUS_TIMEOUT}, + futures::future::join_all, + log, + protobuf::Message as _, + rendezvous_proto::*, + sleep, + tcp::FramedStream, + tokio::{self, sync::mpsc, time}, +}; + +use crate::common::SOFTWARE_UPDATE_URL; +use crate::ipc; + +type Message = RendezvousMessage; + +pub type Childs = Arc)>>; +type Status = (i32, bool, i64, String); // (status_num, key_confirmed, mouse_time, id) + +lazy_static::lazy_static! { + pub static ref CHILDS : Childs = Default::default(); + pub static ref UI_STATUS : Arc> = Arc::new(Mutex::new((0, false, 0, "".to_owned()))); + pub static ref OPTIONS : Arc>> = Arc::new(Mutex::new(Config::get_options())); + pub static ref ASYNC_JOB_STATUS : Arc> = Default::default(); + pub static ref TEMPORARY_PASSWD : Arc> = Arc::new(Mutex::new("".to_owned())); +} + +#[cfg(not(any(target_os = "android", target_os = "ios")))] +lazy_static::lazy_static! { + pub static ref SENDER : Mutex> = Mutex::new(check_connect_status(true)); +} + +pub fn recent_sessions_updated() -> bool { + let mut childs = CHILDS.lock().unwrap(); + if childs.0 { + childs.0 = false; + true + } else { + false + } +} + +pub fn get_id() -> String { + #[cfg(any(target_os = "android", target_os = "ios"))] + return Config::get_id(); + #[cfg(not(any(target_os = "android", target_os = "ios")))] + return ipc::get_id(); +} + +pub fn get_remote_id() -> String { + LocalConfig::get_remote_id() +} + +pub fn set_remote_id(id: String) { + LocalConfig::set_remote_id(&id); +} + +pub fn goto_install() { + allow_err!(crate::run_me(vec!["--install"])); +} + +pub fn install_me(_options: String, _path: String, silent: bool, debug: bool) { + #[cfg(windows)] + std::thread::spawn(move || { + allow_err!(crate::platform::windows::install_me( + &_options, _path, silent, debug + )); + std::process::exit(0); + }); +} + +pub fn update_me(_path: String) { + #[cfg(target_os = "linux")] + { + std::process::Command::new("pkexec") + .args(&["apt", "install", "-f", &_path]) + .spawn() + .ok(); + std::fs::remove_file(&_path).ok(); + crate::run_me(Vec::<&str>::new()).ok(); + } + #[cfg(windows)] + { + let mut path = _path; + if path.is_empty() { + if let Ok(tmp) = std::env::current_exe() { + path = tmp.to_string_lossy().to_string(); + } + } + std::process::Command::new(path) + .arg("--update") + .spawn() + .ok(); + std::process::exit(0); + } +} + +pub fn run_without_install() { + crate::run_me(vec!["--noinstall"]).ok(); + std::process::exit(0); +} + +pub fn show_run_without_install() -> bool { + let mut it = std::env::args(); + if let Some(tmp) = it.next() { + if crate::is_setup(&tmp) { + return it.next() == None; + } + } + false +} + +pub fn has_rendezvous_service() -> bool { + #[cfg(all(windows, feature = "hbbs"))] + return crate::platform::is_win_server() && crate::platform::windows::get_license().is_some(); + return false; +} + +pub fn get_license() -> String { + #[cfg(windows)] + if let Some(lic) = crate::platform::windows::get_license() { + return format!( + "
    Key: {}
    Host: {} Api: {}", + lic.key, lic.host, lic.api + ); + } + Default::default() +} + +pub fn get_option(key: String) -> String { + get_option_(&key) + // #[cfg(any(target_os = "android", target_os = "ios"))] + // return Config::get_option(&key); + // #[cfg(not(any(target_os = "android", target_os = "ios")))] + // return get_option_(&key); +} + +fn get_option_(key: &str) -> String { + let map = OPTIONS.lock().unwrap(); + if let Some(v) = map.get(key) { + v.to_owned() + } else { + "".to_owned() + } +} + +pub fn get_local_option(key: String) -> String { + LocalConfig::get_option(&key) +} + +pub fn set_local_option(key: String, value: String) { + LocalConfig::set_option(key, value); +} + +pub fn peer_has_password(id: String) -> bool { + !PeerConfig::load(&id).password.is_empty() +} + +pub fn forget_password(id: String) { + let mut c = PeerConfig::load(&id); + c.password.clear(); + c.store(&id); +} + +pub fn get_peer_option(id: String, name: String) -> String { + let c = PeerConfig::load(&id); + c.options.get(&name).unwrap_or(&"".to_owned()).to_owned() +} + +pub fn set_peer_option(id: String, name: String, value: String) { + let mut c = PeerConfig::load(&id); + if value.is_empty() { + c.options.remove(&name); + } else { + c.options.insert(name, value); + } + c.store(&id); +} + +pub fn using_public_server() -> bool { + crate::get_custom_rendezvous_server(get_option_("custom-rendezvous-server")).is_empty() +} + +pub fn get_options() -> String { + let options = OPTIONS.lock().unwrap(); + let mut m = serde_json::Map::new(); + for (k, v) in options.iter() { + m.insert(k.into(), v.to_owned().into()); + } + serde_json::to_string(&m).unwrap() +} + +pub fn test_if_valid_server(host: String) -> String { + hbb_common::socket_client::test_if_valid_server(&host) +} + +pub fn get_sound_inputs() -> Vec { + let mut a = Vec::new(); + #[cfg(windows)] + { + // TODO TEST + fn get_sound_inputs_() -> Vec { + let mut out = Vec::new(); + use cpal::traits::{DeviceTrait, HostTrait}; + let host = cpal::default_host(); + if let Ok(devices) = host.devices() { + for device in devices { + if device.default_input_config().is_err() { + continue; + } + if let Ok(name) = device.name() { + out.push(name); + } + } + } + out + } + + let inputs = Arc::new(Mutex::new(Vec::new())); + let cloned = inputs.clone(); + // can not call below in UI thread, because conflict with sciter sound com initialization + std::thread::spawn(move || *cloned.lock().unwrap() = get_sound_inputs_()) + .join() + .ok(); + for name in inputs.lock().unwrap().drain(..) { + a.push(name); + } + } + #[cfg(target_os = "linux")] // TODO + { + let inputs: Vec = crate::platform::linux::get_pa_sources() + .drain(..) + .map(|x| x.1) + .collect(); + + for name in inputs { + a.push(name); + } + } + a +} + +pub fn set_options(m: HashMap) { + *OPTIONS.lock().unwrap() = m.clone(); + #[cfg(not(any(target_os = "android", target_os = "ios")))] + ipc::set_options(m).ok(); + #[cfg(any(target_os = "android", target_os = "ios"))] + Config::set_options(m); +} + +pub fn set_option(key: String, value: String) { + let mut options = OPTIONS.lock().unwrap(); + #[cfg(target_os = "macos")] + if &key == "stop-service" { + let is_stop = value == "Y"; + if is_stop && crate::platform::macos::uninstall() { + return; + } + } + if value.is_empty() { + options.remove(&key); + } else { + options.insert(key.clone(), value.clone()); + } + #[cfg(not(any(target_os = "android", target_os = "ios")))] + ipc::set_options(options.clone()).ok(); + #[cfg(any(target_os = "android", target_os = "ios"))] + Config::set_option(key, value); +} + +pub fn install_path() -> String { + #[cfg(windows)] + return crate::platform::windows::get_install_info().1; + #[cfg(not(windows))] + return "".to_owned(); +} + +pub fn get_socks() -> Vec { + #[cfg(any(target_os = "android", target_os = "ios"))] + return Vec::new(); + #[cfg(not(any(target_os = "android", target_os = "ios")))] + { + let s = ipc::get_socks(); + match s { + None => Vec::new(), + Some(s) => { + let mut v = Vec::new(); + v.push(s.proxy); + v.push(s.username); + v.push(s.password); + v + } + } + } +} + +pub fn set_socks(proxy: String, username: String, password: String) { + #[cfg(not(any(target_os = "android", target_os = "ios")))] + ipc::set_socks(config::Socks5Server { + proxy, + username, + password, + }) + .ok(); +} + +#[cfg(not(any(target_os = "android", target_os = "ios")))] +pub fn is_installed() -> bool { + crate::platform::is_installed() +} + +pub fn is_rdp_service_open() -> bool { + #[cfg(windows)] + return is_installed() && crate::platform::windows::is_rdp_service_open(); + #[cfg(not(windows))] + return false; +} + +pub fn is_share_rdp() -> bool { + #[cfg(windows)] + return crate::platform::windows::is_share_rdp(); + #[cfg(not(windows))] + return false; +} + +pub fn set_share_rdp(_enable: bool) { + #[cfg(windows)] + crate::platform::windows::set_share_rdp(_enable); +} + +pub fn is_installed_lower_version() -> bool { + #[cfg(not(windows))] + return false; + #[cfg(windows)] + { + let installed_version = crate::platform::windows::get_installed_version(); + let a = hbb_common::get_version_number(crate::VERSION); + let b = hbb_common::get_version_number(&installed_version); + return a > b; + } +} + +pub fn closing(x: i32, y: i32, w: i32, h: i32) { + #[cfg(not(any(target_os = "android", target_os = "ios")))] + crate::server::input_service::fix_key_down_timeout_at_exit(); + LocalConfig::set_size(x, y, w, h); +} + +pub fn get_size() -> Vec { + let s = LocalConfig::get_size(); + let mut v = Vec::new(); + v.push(s.0); + v.push(s.1); + v.push(s.2); + v.push(s.3); + v +} + +pub fn get_mouse_time() -> f64 { + let ui_status = UI_STATUS.lock().unwrap(); + let res = ui_status.2 as f64; + return res; +} + +#[cfg(not(any(target_os = "android", target_os = "ios")))] +pub fn check_mouse_time() { + let sender = SENDER.lock().unwrap(); + allow_err!(sender.send(ipc::Data::MouseMoveTime(0))); +} + +pub fn get_connect_status() -> Status { + let ui_statue = UI_STATUS.lock().unwrap(); + let res = ui_statue.clone(); + res +} + +pub fn temporary_password() -> String { + #[cfg(any(target_os = "android", target_os = "ios"))] + return password_security::temporary_password(); + #[cfg(not(any(target_os = "android", target_os = "ios")))] + return TEMPORARY_PASSWD.lock().unwrap().clone(); +} + +pub fn update_temporary_password() { + #[cfg(any(target_os = "android", target_os = "ios"))] + password_security::update_temporary_password(); + #[cfg(not(any(target_os = "android", target_os = "ios")))] + allow_err!(ipc::update_temporary_password()); +} + +pub fn permanent_password() -> String { + #[cfg(any(target_os = "android", target_os = "ios"))] + return Config::get_permanent_password(); + #[cfg(not(any(target_os = "android", target_os = "ios")))] + return ipc::get_permanent_password(); +} + +pub fn set_permanent_password(password: String) { + #[cfg(any(target_os = "android", target_os = "ios"))] + Config::set_permanent_password(&password); + #[cfg(not(any(target_os = "android", target_os = "ios")))] + allow_err!(ipc::set_permanent_password(password)); +} + +pub fn get_peer(id: String) -> PeerConfig { + PeerConfig::load(&id) +} + +pub fn get_fav() -> Vec { + LocalConfig::get_fav() +} + +pub fn store_fav(fav: Vec) { + LocalConfig::set_fav(fav); +} + +pub fn get_recent_sessions() -> Vec<(String, SystemTime, PeerConfig)> { + PeerConfig::peers() +} + +#[cfg(not(any(target_os = "android", target_os = "ios", feature = "cli")))] +pub fn get_icon() -> String { + crate::get_icon() +} + +pub fn remove_peer(id: String) { + PeerConfig::remove(&id); +} + +pub fn new_remote(id: String, remote_type: String) { + let mut lock = CHILDS.lock().unwrap(); + let args = vec![format!("--{}", remote_type), id.clone()]; + let key = (id.clone(), remote_type.clone()); + if let Some(c) = lock.1.get_mut(&key) { + if let Ok(Some(_)) = c.try_wait() { + lock.1.remove(&key); + } else { + if remote_type == "rdp" { + allow_err!(c.kill()); + std::thread::sleep(std::time::Duration::from_millis(30)); + c.try_wait().ok(); + lock.1.remove(&key); + } else { + return; + } + } + } + match crate::run_me(args) { + Ok(child) => { + lock.1.insert(key, child); + } + Err(err) => { + log::error!("Failed to spawn remote: {}", err); + } + } +} + +pub fn is_process_trusted(_prompt: bool) -> bool { + #[cfg(target_os = "macos")] + return crate::platform::macos::is_process_trusted(_prompt); + #[cfg(not(target_os = "macos"))] + return true; +} + +pub fn is_can_screen_recording(_prompt: bool) -> bool { + #[cfg(target_os = "macos")] + return crate::platform::macos::is_can_screen_recording(_prompt); + #[cfg(not(target_os = "macos"))] + return true; +} + +pub fn is_installed_daemon(_prompt: bool) -> bool { + #[cfg(target_os = "macos")] + return crate::platform::macos::is_installed_daemon(_prompt); + #[cfg(not(target_os = "macos"))] + return true; +} + +pub fn get_error() -> String { + #[cfg(not(any(feature = "cli")))] + #[cfg(target_os = "linux")] + { + let dtype = crate::platform::linux::get_display_server(); + if "wayland" == dtype { + return "".to_owned(); + } + if dtype != "x11" { + return format!( + "{} {}, {}", + t("Unsupported display server ".to_owned()), + dtype, + t("x11 expected".to_owned()), + ); + } + } + return "".to_owned(); +} + +pub fn is_login_wayland() -> bool { + #[cfg(target_os = "linux")] + return crate::platform::linux::is_login_wayland(); + #[cfg(not(target_os = "linux"))] + return false; +} + +pub fn fix_login_wayland() { + #[cfg(target_os = "linux")] + crate::platform::linux::fix_login_wayland(); +} + +pub fn current_is_wayland() -> bool { + #[cfg(target_os = "linux")] + return crate::platform::linux::current_is_wayland(); + #[cfg(not(target_os = "linux"))] + return false; +} + +pub fn modify_default_login() -> String { + #[cfg(target_os = "linux")] + return crate::platform::linux::modify_default_login(); + #[cfg(not(target_os = "linux"))] + return "".to_owned(); +} + +pub fn get_software_update_url() -> String { + SOFTWARE_UPDATE_URL.lock().unwrap().clone() +} + +pub fn get_new_version() -> String { + hbb_common::get_version_from_url(&*SOFTWARE_UPDATE_URL.lock().unwrap()) +} + +pub fn get_version() -> String { + crate::VERSION.to_owned() +} + +pub fn get_app_name() -> String { + crate::get_app_name() +} + +#[cfg(not(any(target_os = "android", target_os = "ios")))] +pub fn get_software_ext() -> String { + #[cfg(windows)] + let p = "exe"; + #[cfg(target_os = "macos")] + let p = "dmg"; + #[cfg(target_os = "linux")] + let p = "deb"; + p.to_owned() +} + +#[cfg(not(any(target_os = "android", target_os = "ios")))] +pub fn get_software_store_path() -> String { + let mut p = std::env::temp_dir(); + let name = SOFTWARE_UPDATE_URL + .lock() + .unwrap() + .split("/") + .last() + .map(|x| x.to_owned()) + .unwrap_or(crate::get_app_name()); + p.push(name); + format!("{}.{}", p.to_string_lossy(), get_software_ext()) +} + +pub fn create_shortcut(_id: String) { + #[cfg(windows)] + crate::platform::windows::create_shortcut(&_id).ok(); +} + +pub fn discover() { + std::thread::spawn(move || { + allow_err!(crate::lan::discover()); + }); +} + +pub fn get_lan_peers() -> Vec<(String, config::PeerInfoSerde)> { + config::LanPeers::load() + .peers + .iter() + .map(|peer| { + ( + peer.id.clone(), + config::PeerInfoSerde { + username: peer.username.clone(), + hostname: peer.hostname.clone(), + platform: peer.platform.clone(), + }, + ) + }) + .collect() +} + +pub fn get_uuid() -> String { + base64::encode(hbb_common::get_uuid()) +} + +#[cfg(not(any(target_os = "android", target_os = "ios", feature = "cli")))] +pub fn open_url(url: String) { + #[cfg(windows)] + let p = "explorer"; + #[cfg(target_os = "macos")] + let p = "open"; + #[cfg(target_os = "linux")] + let p = if std::path::Path::new("/usr/bin/firefox").exists() { + "firefox" + } else { + "xdg-open" + }; + allow_err!(std::process::Command::new(p).arg(url).spawn()); +} + +#[cfg(not(any(target_os = "android", target_os = "ios")))] +pub fn change_id(id: String) { + *ASYNC_JOB_STATUS.lock().unwrap() = " ".to_owned(); + let old_id = get_id(); + std::thread::spawn(move || { + *ASYNC_JOB_STATUS.lock().unwrap() = change_id_(id, old_id).to_owned(); + }); +} + +pub fn post_request(url: String, body: String, header: String) { + *ASYNC_JOB_STATUS.lock().unwrap() = " ".to_owned(); + std::thread::spawn(move || { + *ASYNC_JOB_STATUS.lock().unwrap() = match crate::post_request_sync(url, body, &header) { + Err(err) => err.to_string(), + Ok(text) => text, + }; + }); +} + +#[cfg(not(any(target_os = "android", target_os = "ios")))] +pub fn is_ok_change_id() -> bool { + machine_uid::get().is_ok() +} + +pub fn get_async_job_status() -> String { + ASYNC_JOB_STATUS.lock().unwrap().clone() +} + +#[cfg(not(any(target_os = "android", target_os = "ios", feature = "cli")))] +pub fn t(name: String) -> String { + crate::client::translate(name) +} + +pub fn get_langs() -> String { + crate::lang::LANGS.to_string() +} + +pub fn is_xfce() -> bool { + crate::platform::is_xfce() +} + +pub fn get_api_server() -> String { + crate::get_api_server( + get_option_("api-server"), + get_option_("custom-rendezvous-server"), + ) +} + +pub fn has_hwcodec() -> bool { + #[cfg(not(feature = "hwcodec"))] + return false; + #[cfg(feature = "hwcodec")] + return true; +} + +pub fn check_super_user_permission() -> bool { + #[cfg(any(windows, target_os = "linux"))] + return crate::platform::check_super_user_permission().unwrap_or(false); + #[cfg(not(any(windows, target_os = "linux")))] + true +} + +pub fn check_zombie(childs: Childs) { + let mut deads = Vec::new(); + loop { + let mut lock = childs.lock().unwrap(); + let mut n = 0; + for (id, c) in lock.1.iter_mut() { + if let Ok(Some(_)) = c.try_wait() { + deads.push(id.clone()); + n += 1; + } + } + for ref id in deads.drain(..) { + lock.1.remove(id); + } + if n > 0 { + lock.0 = true; + } + drop(lock); + std::thread::sleep(std::time::Duration::from_millis(100)); + } +} + +pub(crate) fn check_connect_status(reconnect: bool) -> mpsc::UnboundedSender { + let (tx, rx) = mpsc::unbounded_channel::(); + std::thread::spawn(move || check_connect_status_(reconnect, rx)); + tx +} + +// notice: avoiding create ipc connecton repeatly, +// because windows named pipe has serious memory leak issue. +#[tokio::main(flavor = "current_thread")] +async fn check_connect_status_(reconnect: bool, rx: mpsc::UnboundedReceiver) { + let mut key_confirmed = false; + let mut rx = rx; + let mut mouse_time = 0; + let mut id = "".to_owned(); + loop { + if let Ok(mut c) = ipc::connect(1000, "").await { + let mut timer = time::interval(time::Duration::from_secs(1)); + loop { + tokio::select! { + res = c.next() => { + match res { + Err(err) => { + log::error!("ipc connection closed: {}", err); + break; + } + Ok(Some(ipc::Data::MouseMoveTime(v))) => { + mouse_time = v; + UI_STATUS.lock().unwrap().2 = v; + } + Ok(Some(ipc::Data::Options(Some(v)))) => { + *OPTIONS.lock().unwrap() = v + } + Ok(Some(ipc::Data::Config((name, Some(value))))) => { + if name == "id" { + id = value; + } else if name == "temporary-password" { + *TEMPORARY_PASSWD.lock().unwrap() = value; + } + } + Ok(Some(ipc::Data::OnlineStatus(Some((mut x, c))))) => { + if x > 0 { + x = 1 + } + key_confirmed = c; + *UI_STATUS.lock().unwrap() = (x as _, key_confirmed, mouse_time, id.clone()); + } + _ => {} + } + } + Some(data) = rx.recv() => { + allow_err!(c.send(&data).await); + } + _ = timer.tick() => { + c.send(&ipc::Data::OnlineStatus(None)).await.ok(); + c.send(&ipc::Data::Options(None)).await.ok(); + c.send(&ipc::Data::Config(("id".to_owned(), None))).await.ok(); + c.send(&ipc::Data::Config(("temporary-password".to_owned(), None))).await.ok(); + } + } + } + } + if !reconnect { + OPTIONS + .lock() + .unwrap() + .insert("ipc-closed".to_owned(), "Y".to_owned()); + break; + } + *UI_STATUS.lock().unwrap() = (-1, key_confirmed, mouse_time, id.clone()); + sleep(1.).await; + } +} + +const INVALID_FORMAT: &'static str = "Invalid format"; +const UNKNOWN_ERROR: &'static str = "Unknown error"; + +#[cfg(not(any(target_os = "android", target_os = "ios")))] +#[tokio::main(flavor = "current_thread")] +async fn change_id_(id: String, old_id: String) -> &'static str { + if !hbb_common::is_valid_custom_id(&id) { + return INVALID_FORMAT; + } + let uuid = machine_uid::get().unwrap_or("".to_owned()); + if uuid.is_empty() { + return UNKNOWN_ERROR; + } + let rendezvous_servers = crate::ipc::get_rendezvous_servers(1_000).await; + let mut futs = Vec::new(); + let err: Arc> = Default::default(); + for rendezvous_server in rendezvous_servers { + let err = err.clone(); + let id = id.to_owned(); + let uuid = uuid.clone(); + let old_id = old_id.clone(); + futs.push(tokio::spawn(async move { + let tmp = check_id(rendezvous_server, old_id, id, uuid).await; + if !tmp.is_empty() { + *err.lock().unwrap() = tmp; + } + })); + } + join_all(futs).await; + let err = *err.lock().unwrap(); + if err.is_empty() { + crate::ipc::set_config_async("id", id.to_owned()).await.ok(); + } + err +} + +async fn check_id( + rendezvous_server: String, + old_id: String, + id: String, + uuid: String, +) -> &'static str { + let any_addr = Config::get_any_listen_addr(); + if let Ok(mut socket) = FramedStream::new( + crate::check_port(rendezvous_server, RENDEZVOUS_PORT), + any_addr, + RENDEZVOUS_TIMEOUT, + ) + .await + { + let mut msg_out = Message::new(); + msg_out.set_register_pk(RegisterPk { + old_id, + id, + uuid: uuid.into(), + ..Default::default() + }); + let mut ok = false; + if socket.send(&msg_out).await.is_ok() { + if let Some(Ok(bytes)) = socket.next_timeout(3_000).await { + if let Ok(msg_in) = RendezvousMessage::parse_from_bytes(&bytes) { + match msg_in.union { + Some(rendezvous_message::Union::RegisterPkResponse(rpr)) => { + match rpr.result.enum_value_or_default() { + register_pk_response::Result::OK => { + ok = true; + } + register_pk_response::Result::ID_EXISTS => { + return "Not available"; + } + register_pk_response::Result::TOO_FREQUENT => { + return "Too frequent"; + } + register_pk_response::Result::NOT_SUPPORT => { + return "server_not_support"; + } + register_pk_response::Result::INVALID_ID_FORMAT => { + return INVALID_FORMAT; + } + _ => {} + } + } + _ => {} + } + } + } + } + if !ok { + return UNKNOWN_ERROR; + } + } else { + return "Failed to connect to rendezvous server"; + } + "" +}