diff --git a/.github/ISSUE_TEMPLATE/task.yaml b/.github/ISSUE_TEMPLATE/task.yaml
index abc6f3397..0190e3573 100644
--- a/.github/ISSUE_TEMPLATE/task.yaml
+++ b/.github/ISSUE_TEMPLATE/task.yaml
@@ -5,10 +5,10 @@ labels: [Task]
 body:
 - type: checkboxes
   attributes:
-    label: Is there an existing issue for this?
-    description: Please search to see if an issue related to this already exists.
+    label: Are you our team member?
+    description: If you are not our team member, please go to discussions.
     options:
-    - label: I have searched the existing issues
+    - label: Yes, I am?
       required: true
 - type: textarea
   attributes:
diff --git a/.github/workflows/bridge.yml b/.github/workflows/bridge.yml
index 31d1daa59..b9285a87a 100644
--- a/.github/workflows/bridge.yml
+++ b/.github/workflows/bridge.yml
@@ -6,7 +6,7 @@ on:
   workflow_call:
 
 env:
-  FLUTTER_VERSION: "3.10.5"
+  FLUTTER_VERSION: "3.10.6"
   FLUTTER_RUST_BRIDGE_VERSION: "1.75.3"
   
 jobs:
@@ -42,7 +42,6 @@ jobs:
       - uses: Swatinem/rust-cache@v2
         with:
           prefix-key: bridge-${{ matrix.job.os }}
-          workspace: "/tmp/flutter_rust_bridge/frb_codegen"
 
       - name: Cache Bridge
         id: cache-bridge
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 11cefa4e1..028df3860 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -13,8 +13,6 @@ on:
   push:
     branches:
       - master
-    tags:
-      - '*'
     paths-ignore:
       - ".github/**"
       - "docs/**"
@@ -126,7 +124,12 @@ jobs:
       with:
         use-cross: ${{ matrix.job.use-cross }}
         command: build
-        args: --locked --release --target=${{ matrix.job.target }}
+        args: --locked --target=${{ matrix.job.target }}
+
+    - name: clean
+      shell: bash
+      run: |
+        cargo clean
 
     # - name: Strip debug information from executable
     #   id: strip
diff --git a/.github/workflows/flutter-build.yml b/.github/workflows/flutter-build.yml
index 5042cc97e..f6609ce4f 100644
--- a/.github/workflows/flutter-build.yml
+++ b/.github/workflows/flutter-build.yml
@@ -6,20 +6,23 @@ on:
       upload-artifact:
         type: boolean
         default: true
+      upload-tag:
+        type: string
+        default: "nightly"
 
 env:
   CARGO_NDK_VERSION: "3.1.2"
   LLVM_VERSION: "15.0.6"
-  FLUTTER_VERSION: "3.10.5"
+  FLUTTER_VERSION: "3.10.6"
   FLUTTER_RUST_BRIDGE_VERSION: "1.75.3"
   # for arm64 linux
-  FLUTTER_ELINUX_VERSION: "3.10.5"
+  FLUTTER_ELINUX_VERSION: "3.10.6"
   FLUTTER_ELINUX_COMMIT_ID: "410b3ca42f2cd0c485edf517a1666652bab442d4"
-  TAG_NAME: "nightly"
+  TAG_NAME: "${{ inputs.upload-tag }}"
   # vcpkg version: 2023.04.15
   # for multiarch gcc compatibility
   VCPKG_COMMIT_ID: "501db0f17ef6df184fcdbfbe0f87cde2313b6ab1"
-  VERSION: "1.2.0"
+  VERSION: "1.2.3"
   NDK_VERSION: "r25c"
   #signing keys env variable checks
   ANDROID_SIGNING_KEY: '${{ secrets.ANDROID_SIGNING_KEY }}'
@@ -93,6 +96,22 @@ jobs:
           VCPKG_ROOT: C:\rustdesk_thirdpary_lib\vcpkg
         run: python3 .\build.py --portable --hwcodec --flutter --feature IddDriver
 
+      - name: find Runner.res
+        # Windows: find Runner.res (compiled from ./flutter/windows/runner/Runner.rc), copy to ./Runner.res
+        # Runner.rc does not contain actual version, but Runner.res does
+        continue-on-error: true
+        shell: bash
+        run: |
+          runner_res=$(find . -name "Runner.res");
+          if [ "$runner_res" == "" ]; then
+            echo "Runner.res: not found";
+          else
+            echo "Runner.res: $runner_res";
+            cp $runner_res ./libs/portable/Runner.res;
+            echo "list ./libs/portable/Runner.res";
+            ls -l ./libs/portable/Runner.res;
+          fi
+
       - name: Sign rustdesk files
         uses: GermanBluefox/code-sign-action@v7
         if: env.UPLOAD_ARTIFACT == 'true'
@@ -195,6 +214,22 @@ jobs:
           curl -LJ -o ./Release/sciter.dll https://github.com/c-smile/sciter-sdk/raw/master/bin.win/x32/sciter.dll
           echo "output_folder=./Release" >> $GITHUB_OUTPUT
 
+      - name: find Runner.res
+        # Windows: find Runner.res (compiled from ./flutter/windows/runner/Runner.rc), copy to ./Runner.res
+        # Runner.rc does not contain actual version, but Runner.res does
+        continue-on-error: true
+        shell: bash
+        run: |
+          runner_res=$(find . -name "Runner.res");
+          if [ "$runner_res" == "" ]; then
+            echo "Runner.res: not found";
+          else
+            echo "Runner.res: $runner_res";
+            cp $runner_res ./libs/portable/Runner.res;
+            echo "list ./libs/portable/Runner.res";
+            ls -l ./libs/portable/Runner.res;
+          fi
+
       - name: Sign rustdesk files
         uses: GermanBluefox/code-sign-action@v7
         if: env.UPLOAD_ARTIFACT == 'true'
@@ -374,6 +409,7 @@ jobs:
     uses: ./.github/workflows/bridge.yml
 
   build-rustdesk-ios:
+    if: ${{ inputs.upload-artifact }}
     needs: [generate-bridge-linux]
     name: build rustdesk ios ipa ${{ matrix.job.target }} (${{ matrix.job.os }}) [${{ matrix.job.extra-build-features }}]
     runs-on: ${{ matrix.job.os }}
@@ -424,6 +460,13 @@ jobs:
           prefix-key: rustdesk-lib-cache
           key: ${{ matrix.job.target }}-${{ matrix.job.extra-build-features }}
 
+      - name: Install flutter rust bridge deps
+        shell: bash
+        run: |
+          cargo install flutter_rust_bridge_codegen --version ${{ env.FLUTTER_RUST_BRIDGE_VERSION }} --features "uuid"
+          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 --c-output ./flutter/ios/Runner/bridge_generated.h
+
       - name: Build rustdesk lib
         env:
           VCPKG_ROOT: /opt/rustdesk_thirdparty_lib/vcpkg
@@ -435,7 +478,9 @@ jobs:
         shell: bash
         run: |
           pushd flutter
-          flutter build ipa --release --obfuscate --split-debug-info=./split-debug-info --no-codesign
+          # flutter build ipa --release --obfuscate --split-debug-info=./split-debug-info --no-codesign
+          # for easy debugging
+          flutter build ipa --release --no-codesign
       
       # - name: Upload Artifacts
       #   # if: env.ANDROID_SIGNING_KEY != null && env.UPLOAD_ARTIFACT == 'true'
@@ -650,7 +695,7 @@ jobs:
           sudo rm -rf /usr/local/lib/android
           sudo rm -rf /usr/share/dotnet
           sudo apt update -y
-          sudo apt install qemu-user-static
+          sudo apt install qemu-user-static -y
 
       - name: Checkout source code
         uses: actions/checkout@v3
@@ -662,7 +707,8 @@ jobs:
 
       - name: Free Space
         run: |
-          df
+          df -h
+          free -m
 
       - name: Install Rust toolchain
         uses: actions-rs/toolchain@v1
@@ -671,27 +717,11 @@ jobs:
           target: ${{ matrix.job.target }}
           override: true
           profile: minimal # minimal component installation (ie, no documentation)
-
-      - uses: Swatinem/rust-cache@v2
-        with:
-          prefix-key: rustdesk-lib-cache
-          key: ${{ matrix.job.target }}-${{ matrix.job.extra-build-features }}
-          cache-directories: "/opt/rust-registry"
-
-      - name: Install local registry
+      
+      - name: Save Rust toolchain version
         run: |
-          mkdir -p /opt/rust-registry
-          cargo install cargo-local-registry
-
-      - name: Build local registry
-        uses: nick-fields/retry@v2
-        id: build-local-registry
-        continue-on-error: true
-        with:
-          max_attempts: 3
-          timeout_minutes: 15
-          retry_on: error
-          command: cargo local-registry --sync ./Cargo.lock /opt/rust-registry
+          RUST_TOOLCHAIN_VERSION=$(cargo --version | awk '{print $2}')
+          echo "RUST_TOOLCHAIN_VERSION=$RUST_TOOLCHAIN_VERSION" >> $GITHUB_ENV
 
       - name: Disable rust bridge build
         run: |
@@ -725,7 +755,6 @@ jobs:
           dockerRunArgs: |
             --volume "${PWD}:/workspace"
             --volume "/opt/artifacts:/opt/artifacts"
-            --volume "/opt/rust-registry:/opt/rust-registry"
           shell: /bin/bash
           install: |
             apt update -y
@@ -742,19 +771,15 @@ jobs:
             # rust
             pushd /opt
             # do not use rustup, because memory overflow in qemu
-            wget -O rust.tar.gz https://static.rust-lang.org/dist/rust-1.70.0-${{ matrix.job.target }}.tar.gz
+            wget -O rust.tar.gz https://static.rust-lang.org/dist/rust-${{env.RUST_TOOLCHAIN_VERSION}}-${{ matrix.job.target }}.tar.gz
             tar -zxvf rust.tar.gz > /dev/null && rm rust.tar.gz
-            cd rust-1.70.0-${{ matrix.job.target }} && ./install.sh
-            rm -rf rust-1.70.0-${{ matrix.job.target }}
+            cd rust-${{env.RUST_TOOLCHAIN_VERSION}}-${{ matrix.job.target }} && ./install.sh
+            rm -rf rust-${{env.RUST_TOOLCHAIN_VERSION}}-${{ matrix.job.target }}
             # edit config
             mkdir -p ~/.cargo/
             echo """
               [source.crates-io]
               registry = 'https://github.com/rust-lang/crates.io-index'
-              replace-with = 'local-registry'
-
-              [source.local-registry]
-              local-registry = '/opt/rust-registry/'
             """ > ~/.cargo/config
             cat ~/.cargo/config
             # start build
@@ -779,9 +804,10 @@ jobs:
           path: target/release/liblibrustdesk.so
 
   build-rustdesk-lib-linux-arm:
+    if: ${{ inputs.upload-artifact }}
     needs: [generate-bridge-linux, build-vcpkg-deps-linux]
     name: build-rust-lib ${{ matrix.job.target }} (${{ matrix.job.os }}) [${{ matrix.job.extra-build-features }}]
-    runs-on: ${{ matrix.job.os }}
+    runs-on: [self-hosted, Linux, ARM64]
     strategy:
       fail-fast: false
       matrix:
@@ -814,25 +840,26 @@ jobs:
           # - { arch: armv7, target: armv7-unknown-linux-gnueabihf , os: ubuntu-20.04, use-cross: true, extra-build-features: "appimage" }
           # - { target: arm-unknown-linux-musleabihf, os: ubuntu-20.04, use-cross: true }
     steps:
-      - name: Maximize build space
-        run: |
-          sudo rm -rf /opt/ghc
-          sudo rm -rf /usr/local/lib/android
-          sudo rm -rf /usr/share/dotnet
-          sudo apt update -y
-          sudo apt install qemu-user-static
+      #- name: Maximize build space
+      #  run: |
+      #    sudo rm -rf /opt/ghc
+      #    sudo rm -rf /usr/local/lib/android
+      #    sudo rm -rf /usr/share/dotnet
+      #    sudo apt update -y
+      #    sudo apt install qemu-user-static -y
 
       - name: Checkout source code
         uses: actions/checkout@v3
 
-      - name: Set Swap Space
-        uses: pierotofy/set-swap-space@master
-        with:
-          swap-size-gb: 12
+      #- name: Set Swap Space
+      #  uses: pierotofy/set-swap-space@master
+      #  with:
+      #    swap-size-gb: 12
 
       - name: Free Space
         run: |
-          df
+          df -h
+          free -m
 
       - name: Install Rust toolchain
         uses: actions-rs/toolchain@v1
@@ -841,27 +868,11 @@ jobs:
           target: ${{ matrix.job.target }}
           override: true
           profile: minimal # minimal component installation (ie, no documentation)
-
-      - uses: Swatinem/rust-cache@v2
-        with:
-          prefix-key: rustdesk-lib-cache
-          key: ${{ matrix.job.target }}-${{ matrix.job.extra-build-features }}
-          cache-directories: "/opt/rust-registry"
-
-      - name: Install local registry
+      
+      - name: Save Rust toolchain version
         run: |
-          mkdir -p /opt/rust-registry
-          cargo install cargo-local-registry
-
-      - name: Build local registry
-        uses: nick-fields/retry@v2
-        id: build-local-registry
-        continue-on-error: true
-        with:
-          max_attempts: 3
-          timeout_minutes: 15
-          retry_on: error
-          command: cargo local-registry --sync ./Cargo.lock /opt/rust-registry
+          RUST_TOOLCHAIN_VERSION=$(cargo --version | awk '{print $2}')
+          echo "RUST_TOOLCHAIN_VERSION=$RUST_TOOLCHAIN_VERSION" >> $GITHUB_ENV
 
       - name: Disable rust bridge build
         run: |
@@ -893,7 +904,6 @@ jobs:
           dockerRunArgs: |
             --volume "${PWD}:/workspace"
             --volume "/opt/artifacts:/opt/artifacts"
-            --volume "/opt/rust-registry:/opt/rust-registry"
           shell: /bin/bash
           install: |
             apt update -y
@@ -910,19 +920,15 @@ jobs:
             # rust
             pushd /opt
             # do not use rustup, because memory overflow in qemu
-            wget -O rust.tar.gz https://static.rust-lang.org/dist/rust-1.70.0-${{ matrix.job.target }}.tar.gz
+            wget -O rust.tar.gz https://static.rust-lang.org/dist/rust-${{env.RUST_TOOLCHAIN_VERSION}}-${{ matrix.job.target }}.tar.gz
             tar -zxvf rust.tar.gz > /dev/null && rm rust.tar.gz
-            cd rust-1.70.0-${{ matrix.job.target }} && ./install.sh
-            rm -rf rust-1.70.0-${{ matrix.job.target }}
+            cd rust-${{env.RUST_TOOLCHAIN_VERSION}}-${{ matrix.job.target }} && ./install.sh
+            rm -rf rust-${{env.RUST_TOOLCHAIN_VERSION}}-${{ matrix.job.target }}
             # edit config
             mkdir -p ~/.cargo/
             echo """
               [source.crates-io]
               registry = 'https://github.com/rust-lang/crates.io-index'
-              replace-with = 'local-registry'
-
-              [source.local-registry]
-              local-registry = '/opt/rust-registry/'
             """ > ~/.cargo/config
             cat ~/.cargo/config
             # start build
@@ -932,6 +938,7 @@ jobs:
             if ${{ matrix.job.enable-headless }}; then
               export DEFAULT_FEAT=linux_headless
             fi
+            export CARGO_INCREMENTAL=0
             cargo build --lib --features flutter,flutter_texture_render,${{ matrix.job.extra-build-features }},$DEFAULT_FEAT --release
 
       - name: Upload Artifacts
@@ -941,9 +948,10 @@ jobs:
           path: target/release/liblibrustdesk.so
 
   build-rustdesk-sciter-arm:
+    if: ${{ inputs.upload-artifact }}
     needs: [build-vcpkg-deps-linux]
     name: build-rustdesk(sciter) ${{ matrix.job.target }} (${{ matrix.job.os }}) [${{ matrix.job.extra-build-features }}]
-    runs-on: ${{ matrix.job.os }}
+    runs-on: [self-hosted, Linux, ARM64]
     strategy:
       fail-fast: false
       matrix:
@@ -962,25 +970,26 @@ jobs:
           # - { target: arm-unknown-linux-musleabihf, os: ubuntu-20.04, use-cross: true }
     steps:
 
-      - name: Maximize build space
-        run: |
-          sudo rm -rf /opt/ghc
-          sudo rm -rf /usr/local/lib/android
-          sudo rm -rf /usr/share/dotnet
-          sudo apt update -y
-          sudo apt install qemu-user-static
+      #- name: Maximize build space
+      #  run: |
+      #    sudo rm -rf /opt/ghc
+      #    sudo rm -rf /usr/local/lib/android
+      #    sudo rm -rf /usr/share/dotnet
+      #    sudo apt update -y
+      #    sudo apt install qemu-user-static -y
 
       - name: Checkout source code
         uses: actions/checkout@v3
 
-      - name: Set Swap Space
-        uses: pierotofy/set-swap-space@master
-        with:
-          swap-size-gb: 12
+      #- name: Set Swap Space
+      #  uses: pierotofy/set-swap-space@master
+      #  with:
+      #    swap-size-gb: 12
 
       - name: Free Space
         run: |
-          df
+          df -h
+          free -m
 
       - name: Install Rust toolchain
         uses: actions-rs/toolchain@v1
@@ -989,27 +998,11 @@ jobs:
           target: ${{ matrix.job.target }}
           override: true
           profile: minimal # minimal component installation (ie, no documentation)
-
-      - uses: Swatinem/rust-cache@v2
-        with:
-          prefix-key: rustdesk-lib-cache
-          key: ${{ matrix.job.target }}-${{ matrix.job.extra-build-features }}
-          cache-directories: "/opt/rust-registry"
-
-      - name: Install local registry
+      
+      - name: Save Rust toolchain version
         run: |
-          mkdir -p /opt/rust-registry
-          cargo install cargo-local-registry
-
-      - name: Build local registry
-        uses: nick-fields/retry@v2
-        id: build-local-registry
-        continue-on-error: true
-        with:
-          max_attempts: 3
-          timeout_minutes: 15
-          retry_on: error
-          command: cargo local-registry --sync ./Cargo.lock /opt/rust-registry
+          RUST_TOOLCHAIN_VERSION=$(cargo --version | awk '{print $2}')
+          echo "RUST_TOOLCHAIN_VERSION=$RUST_TOOLCHAIN_VERSION" >> $GITHUB_ENV
 
       - name: Restore vcpkg files
         uses: actions/download-artifact@master
@@ -1029,7 +1022,6 @@ jobs:
           dockerRunArgs: |
             --volume "${PWD}:/workspace"
             --volume "/opt/artifacts:/opt/artifacts"
-            --volume "/opt/rust-registry:/opt/rust-registry"
           shell: /bin/bash
           install: |
             apt update -y
@@ -1042,19 +1034,15 @@ jobs:
             # rust
             pushd /opt
             # do not use rustup, because memory overflow in qemu
-            wget -O rust.tar.gz https://static.rust-lang.org/dist/rust-1.70.0-${{ matrix.job.target }}.tar.gz
+            wget -O rust.tar.gz https://static.rust-lang.org/dist/rust-${{env.RUST_TOOLCHAIN_VERSION}}-${{ matrix.job.target }}.tar.gz
             tar -zxvf rust.tar.gz > /dev/null && rm rust.tar.gz
-            cd rust-1.70.0-${{ matrix.job.target }} && ./install.sh
-            rm -rf rust-1.70.0-${{ matrix.job.target }}
+            cd rust-${{env.RUST_TOOLCHAIN_VERSION}}-${{ matrix.job.target }} && ./install.sh
+            rm -rf rust-${{env.RUST_TOOLCHAIN_VERSION}}-${{ matrix.job.target }}
             # edit config
             mkdir -p ~/.cargo/
             echo """
               [source.crates-io]
               registry = 'https://github.com/rust-lang/crates.io-index'
-              replace-with = 'local-registry'
-
-              [source.local-registry]
-              local-registry = '/opt/rust-registry/'
             """ > ~/.cargo/config
             cat ~/.cargo/config
             
@@ -1064,6 +1052,7 @@ jobs:
             export VCPKG_ROOT=/opt/artifacts/vcpkg
             export ARCH=armhf
             export DEFAULT_FEAT=""
+            export CARGO_INCREMENTAL=0
             if ${{ matrix.job.enable-headless }}; then
               export DEFAULT_FEAT=linux_headless
             fi
@@ -1099,6 +1088,7 @@ jobs:
           path: rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}-sciter.deb
 
   build-rustdesk-linux-arm:
+    if: ${{ inputs.upload-artifact }}
     needs: [build-rustdesk-lib-linux-arm]
     name: build-rustdesk ${{ matrix.job.target }} (${{ matrix.job.os }}) [${{ matrix.job.extra-build-features }}]
     runs-on: ubuntu-20.04 # 20.04 has more performance on arm build
@@ -1209,6 +1199,7 @@ jobs:
                 sed -i "s/x64\/release/arm\/release/g" ./build.py
               ;;
             esac
+            export CARGO_INCREMENTAL=0
             python3 ./build.py --flutter --hwcodec --skip-cargo
             # rpm package
             echo -e "start packaging fedora package"
@@ -1226,7 +1217,7 @@ jobs:
             pushd ~/rpmbuild/RPMS/${{ matrix.job.arch }}
             mkdir -p /opt/artifacts/rpm
             for name in rustdesk*??.rpm; do
-                mv "$name" "/opt/artifacts/rpm/${name%%.rpm}-fedora28-centos8.rpm" 
+                mv "$name" "/opt/artifacts/rpm/${name%%.rpm}.rpm" 
             done
             # rpm suse package
             echo -e "start packaging suse package"
@@ -1351,7 +1342,7 @@ jobs:
       #     files: |
       #       res/rustdesk*.zst
 
-      - name: Publish fedora28/centos8 package
+      - name: Publish fedora package
         if: matrix.job.extra-build-features == '' && env.UPLOAD_ARTIFACT == 'true'
         uses: softprops/action-gh-release@v1
         with:
@@ -1450,7 +1441,7 @@ jobs:
             pushd ~/rpmbuild/RPMS/${{ matrix.job.arch }}
             mkdir -p /opt/artifacts/rpm
             for name in rustdesk*??.rpm; do
-                mv "$name" "/opt/artifacts/rpm/${name%%.rpm}-fedora28-centos8.rpm" 
+                mv "$name" "/opt/artifacts/rpm/${name%%.rpm}.rpm" 
             done
             # rpm suse package
             pushd /workspace
@@ -1565,7 +1556,7 @@ jobs:
           files: |
             ./appimage/rustdesk-${{ env.VERSION }}-*.AppImage
 
-      - name: Publish fedora28/centos8 package
+      - name: Publish fedora package
         if: matrix.job.extra-build-features == '' && env.UPLOAD_ARTIFACT == 'true'
         uses: softprops/action-gh-release@v1
         with:
diff --git a/.github/workflows/flutter-ci.yml b/.github/workflows/flutter-ci.yml
index d40d6f736..6dab5f7d4 100644
--- a/.github/workflows/flutter-ci.yml
+++ b/.github/workflows/flutter-ci.yml
@@ -9,8 +9,6 @@ on:
   push:
     branches:
       - master
-    tags:
-      - '*'
     paths-ignore:
       - ".github/**"
       - "docs/**"
@@ -21,4 +19,4 @@ jobs:
     uses: ./.github/workflows/flutter-build.yml
     with:
       upload-artifact: false
-  
\ No newline at end of file
+  
diff --git a/.github/workflows/flutter-nightly.yml b/.github/workflows/flutter-nightly.yml
index c251a2b25..67dc14ae9 100644
--- a/.github/workflows/flutter-nightly.yml
+++ b/.github/workflows/flutter-nightly.yml
@@ -12,3 +12,4 @@ jobs:
     secrets: inherit
     with:
       upload-artifact: true
+      upload-tag: "nightly" 
diff --git a/.github/workflows/flutter-tag.yml b/.github/workflows/flutter-tag.yml
new file mode 100644
index 000000000..4925f26c8
--- /dev/null
+++ b/.github/workflows/flutter-tag.yml
@@ -0,0 +1,18 @@
+name: Flutter Tag Build
+
+on:
+  workflow_dispatch:
+  push:
+    tags:
+      - 'v[0-9]+.[0-9]+.[0-9]+'
+      - '[0-9]+.[0-9]+.[0-9]+'
+      - 'v[0-9]+.[0-9]+.[0-9]+-[0-9]+'
+      - '[0-9]+.[0-9]+.[0-9]+-[0-9]+'
+
+jobs:
+  run-flutter-tag-build:
+    uses: ./.github/workflows/flutter-build.yml
+    secrets: inherit
+    with:
+      upload-artifact: true
+      upload-tag: "1.2.3" 
diff --git a/.github/workflows/history.yml b/.github/workflows/history.yml
index b1ca74721..6921ea4cd 100644
--- a/.github/workflows/history.yml
+++ b/.github/workflows/history.yml
@@ -4,30 +4,28 @@ on: [workflow_dispatch]
 
 env:
   LLVM_VERSION: "10.0"
-  # Note: currently 3.0.5 does not support arm64 officially, we use latest stable version first.
-  FLUTTER_VERSION: "3.0.5"
+  FLUTTER_VERSION: "3.10.6"
   TAG_NAME: "tmp"
+  FLUTTER_RUST_BRIDGE_VERSION: "1.75.3"
   # vcpkg version: 2022.05.10
   # for multiarch gcc compatibility
-  VCPKG_COMMIT_ID: "14e7bb4ae24616ec54ff6b2f6ef4e8659434ea44"
-  VERSION: "1.2.0"
+  VCPKG_COMMIT_ID: "501db0f17ef6df184fcdbfbe0f87cde2313b6ab1"
+  VERSION: "1.2.3"
 
 jobs:
-  build-for-windows-2022-12-05:
-    name: ${{ matrix.job.target }} (${{ matrix.job.os }})
+  build-for-history-windows:
+    name: ${{ matrix.job.date }}
     runs-on: ${{ matrix.job.os }}
     strategy:
       fail-fast: false
       matrix:
         job:
-          # - { target: i686-pc-windows-msvc        , os: windows-2019                  }
-          # - { target: x86_64-pc-windows-gnu       , os: windows-2019                  }
-          - { target: x86_64-pc-windows-msvc, os: windows-2019 }
+          - { target: x86_64-pc-windows-msvc, os: windows-2019, arch: x86_64, date: 2023-08-04, ref: 72c198a1e94cc1e0242fce88f92b3f3caedcd0c3 }
     steps:
       - name: Checkout source code
         uses: actions/checkout@v3
         with:
-          ref: '8d1254cf14b69f545c9cefa026c5eeb0e7dd3e7c'
+          ref: ${{ matrix.job.ref }}
 
       - name: Install LLVM and Clang
         uses: KyleMayes/install-llvm-action@v1
@@ -41,49 +39,30 @@ jobs:
           flutter-version: ${{ env.FLUTTER_VERSION }}
           cache: true
 
-      - name: Replace engine with rustdesk custom flutter engine
-        run: |
-          flutter doctor -v
-          flutter precache --windows
-          Invoke-WebRequest -Uri https://github.com/Kingtous/engine/releases/download/v3.0.5-rustdesk.2/windows-x64-flutter-release.zip -OutFile windows-x64-flutter-release.zip
-          Expand-Archive windows-x64-flutter-release.zip -DestinationPath engine
-          mv -Force engine/*  C:/hostedtoolcache/windows/flutter/stable-3.0.5-x64/bin/cache/artifacts/engine/windows-x64-release/
       - name: Install Rust toolchain
         uses: actions-rs/toolchain@v1
         with:
-          toolchain: "1.62"
+          toolchain: stable
           target: ${{ matrix.job.target }}
           override: true
           components: rustfmt
           profile: minimal # minimal component installation (ie, no documentation)
 
-      - uses: Swatinem/rust-cache@v2
-        with:
-          prefix-key: ${{ matrix.job.os }}
-
       - name: Install flutter rust bridge deps
         run: |
-          dart pub global activate ffigen --version 5.0.1
-          $exists = Test-Path ~/.cargo/bin/flutter_rust_bridge_codegen.exe
-          Push-Location ..
-          git clone https://github.com/SoLongAndThanksForAllThePizza/flutter_rust_bridge --depth=1
-          Push-Location flutter_rust_bridge/frb_codegen ; cargo install --path . ; Pop-Location
-          Pop-Location
+          cargo install flutter_rust_bridge_codegen --version ${{ env.FLUTTER_RUST_BRIDGE_VERSION }} --features "uuid"
           Push-Location flutter ; flutter pub get ; Pop-Location
           ~/.cargo/bin/flutter_rust_bridge_codegen --rust-input ./src/flutter_ffi.rs --dart-output ./flutter/lib/generated_bridge.dart
-      - name: Restore from cache and install vcpkg
-        uses: lukka/run-vcpkg@v7
-        with:
-          setupOnly: true
-          vcpkgGitCommitId: ${{ env.VCPKG_COMMIT_ID }}
 
       - name: Install vcpkg dependencies
         run: |
-          $VCPKG_ROOT/vcpkg install libvpx:x64-windows-static libyuv:x64-windows-static opus:x64-windows-static
-        shell: bash
-
+          cd C:\
+          git clone https://github.com/Kingtous/rustdesk_thirdpary_lib --depth=1 
+          
       - name: Build rustdesk
-        run: python3 .\build.py --portable --hwcodec --flutter
+        env:
+          VCPKG_ROOT: C:\rustdesk_thirdpary_lib\vcpkg
+        run: python3 .\build.py --portable --hwcodec --flutter --feature IddDriver
 
       - name: Build self-extracted executable
         shell: bash
@@ -92,274 +71,7 @@ jobs:
           python3 ./generate.py -f ../../flutter/build/windows/runner/Release/ -o . -e ../../flutter/build/windows/runner/Release/rustdesk.exe
           popd
           mkdir -p ./SignOutput
-          mv ./target/release/rustdesk-portable-packer.exe ./SignOutput/rustdesk-2022-12-05-${{ matrix.job.target }}.exe
-          
-      - name: Publish Release
-        uses: softprops/action-gh-release@v1
-        with:
-          prerelease: true
-          tag_name: ${{ env.TAG_NAME }}
-          files: |
-            ./SignOutput/rustdesk-*.exe
-
-  build-for-windows-2022-12-12:
-    name: ${{ matrix.job.target }} (${{ matrix.job.os }})
-    runs-on: ${{ matrix.job.os }}
-    strategy:
-      fail-fast: false
-      matrix:
-        job:
-          # - { target: i686-pc-windows-msvc        , os: windows-2019                  }
-          # - { target: x86_64-pc-windows-gnu       , os: windows-2019                  }
-          - { target: x86_64-pc-windows-msvc, os: windows-2019 }
-    steps:
-      - name: Checkout source code
-        uses: actions/checkout@v3
-        with:
-          ref: '3dd43b79ec0409fc38103bed0c7eb0bc3cd993d5'
-
-      - name: Install LLVM and Clang
-        uses: KyleMayes/install-llvm-action@v1
-        with:
-          version: ${{ env.LLVM_VERSION }}
-
-      - name: Install flutter
-        uses: subosito/flutter-action@v2
-        with:
-          channel: "stable"
-          flutter-version: ${{ env.FLUTTER_VERSION }}
-          cache: true
-
-      - name: Replace engine with rustdesk custom flutter engine
-        run: |
-          flutter doctor -v
-          flutter precache --windows
-          Invoke-WebRequest -Uri https://github.com/Kingtous/engine/releases/download/v3.0.5-rustdesk.2/windows-x64-flutter-release.zip -OutFile windows-x64-flutter-release.zip
-          Expand-Archive windows-x64-flutter-release.zip -DestinationPath engine
-          mv -Force engine/*  C:/hostedtoolcache/windows/flutter/stable-3.0.5-x64/bin/cache/artifacts/engine/windows-x64-release/
-      - name: Install Rust toolchain
-        uses: actions-rs/toolchain@v1
-        with:
-          toolchain: "1.62"
-          target: ${{ matrix.job.target }}
-          override: true
-          components: rustfmt
-          profile: minimal # minimal component installation (ie, no documentation)
-
-      - uses: Swatinem/rust-cache@v2
-        with:
-          prefix-key: ${{ matrix.job.os }}
-
-      - name: Install flutter rust bridge deps
-        run: |
-          dart pub global activate ffigen --version 5.0.1
-          $exists = Test-Path ~/.cargo/bin/flutter_rust_bridge_codegen.exe
-          Push-Location ..
-          git clone https://github.com/SoLongAndThanksForAllThePizza/flutter_rust_bridge --depth=1
-          Push-Location flutter_rust_bridge/frb_codegen ; cargo install --path . ; Pop-Location
-          Pop-Location
-          Push-Location flutter ; flutter pub get ; Pop-Location
-          ~/.cargo/bin/flutter_rust_bridge_codegen --rust-input ./src/flutter_ffi.rs --dart-output ./flutter/lib/generated_bridge.dart
-      - name: Restore from cache and install vcpkg
-        uses: lukka/run-vcpkg@v7
-        with:
-          setupOnly: true
-          vcpkgGitCommitId: ${{ env.VCPKG_COMMIT_ID }}
-
-      - name: Install vcpkg dependencies
-        run: |
-          $VCPKG_ROOT/vcpkg install libvpx:x64-windows-static libyuv:x64-windows-static opus:x64-windows-static
-        shell: bash
-
-      - name: Build rustdesk
-        run: python3 .\build.py --portable --hwcodec --flutter
-
-      - name: Build self-extracted executable
-        shell: bash
-        run: |
-          pushd ./libs/portable
-          python3 ./generate.py -f ../../flutter/build/windows/runner/Release/ -o . -e ../../flutter/build/windows/runner/Release/rustdesk.exe
-          popd
-          mkdir -p ./SignOutput
-          mv ./target/release/rustdesk-portable-packer.exe ./SignOutput/rustdesk-2022-12-12-${{ matrix.job.target }}.exe
-          
-      - name: Publish Release
-        uses: softprops/action-gh-release@v1
-        with:
-          prerelease: true
-          tag_name: ${{ env.TAG_NAME }}
-          files: |
-            ./SignOutput/rustdesk-*.exe
-
-  build-for-windows-2022-12-19:
-    name: ${{ matrix.job.target }} (${{ matrix.job.os }})
-    runs-on: ${{ matrix.job.os }}
-    strategy:
-      fail-fast: false
-      matrix:
-        job:
-          # - { target: i686-pc-windows-msvc        , os: windows-2019                  }
-          # - { target: x86_64-pc-windows-gnu       , os: windows-2019                  }
-          - { target: x86_64-pc-windows-msvc, os: windows-2019 }
-    steps:
-      - name: Checkout source code
-        uses: actions/checkout@v3
-        with:
-          ref: '1054715891c4e73ad9b164acec6dadecfc599a65'
-
-      - name: Install LLVM and Clang
-        uses: KyleMayes/install-llvm-action@v1
-        with:
-          version: ${{ env.LLVM_VERSION }}
-
-      - name: Install flutter
-        uses: subosito/flutter-action@v2
-        with:
-          channel: "stable"
-          flutter-version: ${{ env.FLUTTER_VERSION }}
-          cache: true
-
-      - name: Replace engine with rustdesk custom flutter engine
-        run: |
-          flutter doctor -v
-          flutter precache --windows
-          Invoke-WebRequest -Uri https://github.com/Kingtous/engine/releases/download/v3.0.5-rustdesk.2/windows-x64-flutter-release.zip -OutFile windows-x64-flutter-release.zip
-          Expand-Archive windows-x64-flutter-release.zip -DestinationPath engine
-          mv -Force engine/*  C:/hostedtoolcache/windows/flutter/stable-3.0.5-x64/bin/cache/artifacts/engine/windows-x64-release/
-      - name: Install Rust toolchain
-        uses: actions-rs/toolchain@v1
-        with:
-          toolchain: "1.62"
-          target: ${{ matrix.job.target }}
-          override: true
-          components: rustfmt
-          profile: minimal # minimal component installation (ie, no documentation)
-
-      - uses: Swatinem/rust-cache@v2
-        with:
-          prefix-key: ${{ matrix.job.os }}
-
-      - name: Install flutter rust bridge deps
-        run: |
-          dart pub global activate ffigen --version 5.0.1
-          $exists = Test-Path ~/.cargo/bin/flutter_rust_bridge_codegen.exe
-          Push-Location ..
-          git clone https://github.com/SoLongAndThanksForAllThePizza/flutter_rust_bridge --depth=1
-          Push-Location flutter_rust_bridge/frb_codegen ; cargo install --path . ; Pop-Location
-          Pop-Location
-          Push-Location flutter ; flutter pub get ; Pop-Location
-          ~/.cargo/bin/flutter_rust_bridge_codegen --rust-input ./src/flutter_ffi.rs --dart-output ./flutter/lib/generated_bridge.dart
-      - name: Restore from cache and install vcpkg
-        uses: lukka/run-vcpkg@v7
-        with:
-          setupOnly: true
-          vcpkgGitCommitId: ${{ env.VCPKG_COMMIT_ID }}
-
-      - name: Install vcpkg dependencies
-        run: |
-          $VCPKG_ROOT/vcpkg install libvpx:x64-windows-static libyuv:x64-windows-static opus:x64-windows-static
-        shell: bash
-
-      - name: Build rustdesk
-        run: python3 .\build.py --portable --hwcodec --flutter
-
-      - name: Build self-extracted executable
-        shell: bash
-        run: |
-          pushd ./libs/portable
-          python3 ./generate.py -f ../../flutter/build/windows/runner/Release/ -o . -e ../../flutter/build/windows/runner/Release/rustdesk.exe
-          popd
-          mkdir -p ./SignOutput
-          mv ./target/release/rustdesk-portable-packer.exe ./SignOutput/rustdesk-2022-12-19-${{ matrix.job.target }}.exe
-          
-      - name: Publish Release
-        uses: softprops/action-gh-release@v1
-        with:
-          prerelease: true
-          tag_name: ${{ env.TAG_NAME }}
-          files: |
-            ./SignOutput/rustdesk-*.exe
-
-  build-for-windows-2022-12-26:
-    name: ${{ matrix.job.target }} (${{ matrix.job.os }})
-    runs-on: ${{ matrix.job.os }}
-    strategy:
-      fail-fast: false
-      matrix:
-        job:
-          # - { target: i686-pc-windows-msvc        , os: windows-2019                  }
-          # - { target: x86_64-pc-windows-gnu       , os: windows-2019                  }
-          - { target: x86_64-pc-windows-msvc, os: windows-2019 }
-    steps:
-      - name: Checkout source code
-        uses: actions/checkout@v3
-        with:
-          ref: 'b241925fe093dc4da804a5aac419375f4ca7653f'
-
-      - name: Install LLVM and Clang
-        uses: KyleMayes/install-llvm-action@v1
-        with:
-          version: ${{ env.LLVM_VERSION }}
-
-      - name: Install flutter
-        uses: subosito/flutter-action@v2
-        with:
-          channel: "stable"
-          flutter-version: ${{ env.FLUTTER_VERSION }}
-          cache: true
-
-      - name: Replace engine with rustdesk custom flutter engine
-        run: |
-          flutter doctor -v
-          flutter precache --windows
-          Invoke-WebRequest -Uri https://github.com/Kingtous/engine/releases/download/v3.0.5-rustdesk.2/windows-x64-flutter-release.zip -OutFile windows-x64-flutter-release.zip
-          Expand-Archive windows-x64-flutter-release.zip -DestinationPath engine
-          mv -Force engine/*  C:/hostedtoolcache/windows/flutter/stable-3.0.5-x64/bin/cache/artifacts/engine/windows-x64-release/
-      - name: Install Rust toolchain
-        uses: actions-rs/toolchain@v1
-        with:
-          toolchain: "1.62"
-          target: ${{ matrix.job.target }}
-          override: true
-          components: rustfmt
-          profile: minimal # minimal component installation (ie, no documentation)
-
-      - uses: Swatinem/rust-cache@v2
-        with:
-          prefix-key: ${{ matrix.job.os }}
-
-      - name: Install flutter rust bridge deps
-        run: |
-          dart pub global activate ffigen --version 5.0.1
-          $exists = Test-Path ~/.cargo/bin/flutter_rust_bridge_codegen.exe
-          Push-Location ..
-          git clone https://github.com/SoLongAndThanksForAllThePizza/flutter_rust_bridge --depth=1
-          Push-Location flutter_rust_bridge/frb_codegen ; cargo install --path . ; Pop-Location
-          Pop-Location
-          Push-Location flutter ; flutter pub get ; Pop-Location
-          ~/.cargo/bin/flutter_rust_bridge_codegen --rust-input ./src/flutter_ffi.rs --dart-output ./flutter/lib/generated_bridge.dart
-      - name: Restore from cache and install vcpkg
-        uses: lukka/run-vcpkg@v7
-        with:
-          setupOnly: true
-          vcpkgGitCommitId: ${{ env.VCPKG_COMMIT_ID }}
-
-      - name: Install vcpkg dependencies
-        run: |
-          $VCPKG_ROOT/vcpkg install libvpx:x64-windows-static libyuv:x64-windows-static opus:x64-windows-static
-        shell: bash
-
-      - name: Build rustdesk
-        run: python3 .\build.py --portable --hwcodec --flutter
-
-      - name: Build self-extracted executable
-        shell: bash
-        run: |
-          pushd ./libs/portable
-          python3 ./generate.py -f ../../flutter/build/windows/runner/Release/ -o . -e ../../flutter/build/windows/runner/Release/rustdesk.exe
-          popd
-          mkdir -p ./SignOutput
-          mv ./target/release/rustdesk-portable-packer.exe ./SignOutput/rustdesk-2022-12-26-${{ matrix.job.target }}.exe
+          mv ./target/release/rustdesk-portable-packer.exe ./SignOutput/rustdesk-${{ matrix.job.date }}-${{ matrix.job.target }}.exe
           
       - name: Publish Release
         uses: softprops/action-gh-release@v1
diff --git a/Cargo.lock b/Cargo.lock
index d382f9353..2b5074999 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1064,7 +1064,7 @@ dependencies = [
 [[package]]
 name = "confy"
 version = "0.4.0-2"
-source = "git+https://github.com/open-trade/confy#9f231b2039cf8a8f8cdf6b829c5ac0016e146077"
+source = "git+https://github.com/open-trade/confy#7855cd3c32b1a60b44e5076ee8f6b4131da10350"
 dependencies = [
  "directories-next",
  "serde 1.0.163",
@@ -1207,7 +1207,7 @@ dependencies = [
  "js-sys",
  "libc",
  "mach2",
- "ndk 0.7.0",
+ "ndk",
  "ndk-context",
  "oboe",
  "once_cell",
@@ -3041,8 +3041,8 @@ checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4"
 
 [[package]]
 name = "hwcodec"
-version = "0.1.0"
-source = "git+https://github.com/21pages/hwcodec?branch=stable#3ea79865a10387b7e1b7630c2ae068bd2081f680"
+version = "0.1.1"
+source = "git+https://github.com/21pages/hwcodec?branch=stable#82cdc15457e42feaf14e1b38622506b2d54baf76"
 dependencies = [
  "bindgen 0.59.2",
  "cc",
@@ -3652,9 +3652,10 @@ dependencies = [
 [[package]]
 name = "machine-uid"
 version = "0.3.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "26141aceb9f046065617266f41a4a652e4583da643472a10a89b4b3664d99eb6"
+source = "git+https://github.com/21pages/machine-uid#381ff579c1dc3a6c54db9dfec47c44bcb0246542"
 dependencies = [
+ "bindgen 0.59.2",
+ "cc",
  "winreg 0.11.0",
 ]
 
@@ -3817,19 +3818,6 @@ dependencies = [
  "getrandom",
 ]
 
-[[package]]
-name = "ndk"
-version = "0.6.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2032c77e030ddee34a6787a64166008da93f6a352b629261d0fee232b8742dd4"
-dependencies = [
- "bitflags",
- "jni-sys",
- "ndk-sys 0.3.0",
- "num_enum",
- "thiserror",
-]
-
 [[package]]
 name = "ndk"
 version = "0.7.0"
@@ -3838,7 +3826,7 @@ checksum = "451422b7e4718271c8b5b3aadf5adedba43dc76312454b387e98fae0fc951aa0"
 dependencies = [
  "bitflags",
  "jni-sys",
- "ndk-sys 0.4.1+23.1.7779620",
+ "ndk-sys",
  "num_enum",
  "raw-window-handle",
  "thiserror",
@@ -3850,15 +3838,6 @@ version = "0.1.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b"
 
-[[package]]
-name = "ndk-sys"
-version = "0.3.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6e5a6ae77c8ee183dcbbba6150e2e6b9f3f4196a7666c02a715a95692ec1fa97"
-dependencies = [
- "jni-sys",
-]
-
 [[package]]
 name = "ndk-sys"
 version = "0.4.1+23.1.7779620"
@@ -4169,7 +4148,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "8868cc237ee02e2d9618539a23a8d228b9bb3fc2e7a5b11eed3831de77c395d0"
 dependencies = [
  "jni 0.20.0",
- "ndk 0.7.0",
+ "ndk",
  "ndk-context",
  "num-derive",
  "num-traits 0.2.15",
@@ -4507,7 +4486,7 @@ dependencies = [
  "base64",
  "indexmap",
  "line-wrap",
- "quick-xml",
+ "quick-xml 0.28.2",
  "serde 1.0.163",
  "time 0.3.21",
 ]
@@ -4707,6 +4686,15 @@ dependencies = [
  "winapi 0.3.9",
 ]
 
+[[package]]
+name = "quick-xml"
+version = "0.23.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "11bafc859c6815fbaffbbbf4229ecb767ac913fecb27f9ad4343662e9ef099ea"
+dependencies = [
+ "memchr",
+]
+
 [[package]]
 name = "quick-xml"
 version = "0.28.2"
@@ -4957,7 +4945,7 @@ dependencies = [
 [[package]]
 name = "rdev"
 version = "0.5.0-2"
-source = "git+https://github.com/fufesou/rdev#ab48d5798c86303b9398727684509b1b43ecfdab"
+source = "git+https://github.com/fufesou/rdev#ee3057bd97c91529e8b9daf2ca133a5c49f0c0eb"
 dependencies = [
  "cocoa",
  "core-foundation",
@@ -5209,7 +5197,7 @@ dependencies = [
 
 [[package]]
 name = "rustdesk"
-version = "1.2.0"
+version = "1.2.3"
 dependencies = [
  "android_logger",
  "arboard",
@@ -5255,7 +5243,6 @@ dependencies = [
  "libpulse-binding",
  "libpulse-simple-binding",
  "mac_address",
- "machine-uid",
  "magnum-opus",
  "mouce",
  "num_cpus",
@@ -5285,6 +5272,7 @@ dependencies = [
  "sys-locale",
  "system_shutdown",
  "tao",
+ "tauri-winrt-notification",
  "tray-icon",
  "url",
  "users 0.11.0",
@@ -5473,7 +5461,7 @@ dependencies = [
  "jni 0.21.1",
  "lazy_static",
  "log",
- "ndk 0.7.0",
+ "ndk",
  "num_cpus",
  "pkg-config",
  "quest",
@@ -5905,9 +5893,9 @@ dependencies = [
 
 [[package]]
 name = "sysinfo"
-version = "0.29.0"
+version = "0.29.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "02f1dc6930a439cc5d154221b5387d153f8183529b07c19aca24ea31e0a167e1"
+checksum = "c7cb97a5a85a136d84e75d5c3cf89655090602efb1be0d8d5337b7e386af2908"
 dependencies = [
  "cfg-if 1.0.0",
  "core-foundation-sys 0.8.4",
@@ -5979,8 +5967,8 @@ dependencies = [
 
 [[package]]
 name = "tao"
-version = "0.19.1"
-source = "git+https://github.com/rustdesk-org/tao?branch=muda#173f128608d282dc4036f213c1c42137464ff096"
+version = "0.22.2"
+source = "git+https://github.com/rustdesk-org/tao?branch=dev#1e5b97258cf42a30f80f85a6aa0b1a4aece1977e"
 dependencies = [
  "bitflags",
  "cairo-rs",
@@ -6001,13 +5989,13 @@ dependencies = [
  "gtk",
  "image",
  "instant",
- "jni 0.20.0",
+ "jni 0.21.1",
  "lazy_static",
  "libc",
  "log",
- "ndk 0.6.0",
+ "ndk",
  "ndk-context",
- "ndk-sys 0.3.0",
+ "ndk-sys",
  "objc",
  "once_cell",
  "parking_lot",
@@ -6016,16 +6004,18 @@ dependencies = [
  "scopeguard",
  "tao-macros",
  "unicode-segmentation",
+ "url",
  "uuid",
- "windows 0.44.0",
+ "windows 0.48.0",
  "windows-implement",
  "x11-dl",
+ "zbus",
 ]
 
 [[package]]
 name = "tao-macros"
-version = "0.1.1"
-source = "git+https://github.com/rustdesk-org/tao?branch=muda#173f128608d282dc4036f213c1c42137464ff096"
+version = "0.1.2"
+source = "git+https://github.com/rustdesk-org/tao?branch=dev#1e5b97258cf42a30f80f85a6aa0b1a4aece1977e"
 dependencies = [
  "proc-macro2 1.0.63",
  "quote 1.0.27",
@@ -6055,6 +6045,16 @@ dependencies = [
  "serde_json 0.9.10",
 ]
 
+[[package]]
+name = "tauri-winrt-notification"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4f5bff1d532fead7c43324a0fa33643b8621a47ce2944a633be4cb6c0240898f"
+dependencies = [
+ "quick-xml 0.23.1",
+ "windows 0.39.0",
+]
+
 [[package]]
 name = "tempfile"
 version = "3.5.0"
@@ -6917,14 +6917,25 @@ dependencies = [
  "windows_x86_64_msvc 0.34.0",
 ]
 
+[[package]]
+name = "windows"
+version = "0.39.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f1c4bd0a50ac6020f65184721f758dba47bb9fbc2133df715ec74a237b26794a"
+dependencies = [
+ "windows_aarch64_msvc 0.39.0",
+ "windows_i686_gnu 0.39.0",
+ "windows_i686_msvc 0.39.0",
+ "windows_x86_64_gnu 0.39.0",
+ "windows_x86_64_msvc 0.39.0",
+]
+
 [[package]]
 name = "windows"
 version = "0.44.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "9e745dab35a0c4c77aa3ce42d595e13d2003d6902d6b08c9ef5fc326d08da12b"
 dependencies = [
- "windows-implement",
- "windows-interface",
  "windows-targets 0.42.2",
 ]
 
@@ -6943,14 +6954,16 @@ version = "0.48.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "e686886bc078bc1b0b600cac0147aadb815089b6e4da64016cbd754b6342700f"
 dependencies = [
+ "windows-implement",
+ "windows-interface",
  "windows-targets 0.48.0",
 ]
 
 [[package]]
 name = "windows-implement"
-version = "0.44.0"
+version = "0.48.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6ce87ca8e3417b02dc2a8a22769306658670ec92d78f1bd420d6310a67c245c6"
+checksum = "5e2ee588991b9e7e6c8338edf3333fbe4da35dc72092643958ebb43f0ab2c49c"
 dependencies = [
  "proc-macro2 1.0.63",
  "quote 1.0.27",
@@ -6959,9 +6972,9 @@ dependencies = [
 
 [[package]]
 name = "windows-interface"
-version = "0.44.0"
+version = "0.48.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "853f69a591ecd4f810d29f17e902d40e349fb05b0b11fff63b08b826bfe39c7f"
+checksum = "e6fb8df20c9bcaa8ad6ab513f7b40104840c8867d5751126e4df3b08388d0cc7"
 dependencies = [
  "proc-macro2 1.0.63",
  "quote 1.0.27",
@@ -7066,6 +7079,12 @@ version = "0.34.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "17cffbe740121affb56fad0fc0e421804adf0ae00891205213b5cecd30db881d"
 
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.39.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ec7711666096bd4096ffa835238905bb33fb87267910e154b18b44eaabb340f2"
+
 [[package]]
 name = "windows_aarch64_msvc"
 version = "0.42.2"
@@ -7090,6 +7109,12 @@ version = "0.34.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "2564fde759adb79129d9b4f54be42b32c89970c18ebf93124ca8870a498688ed"
 
+[[package]]
+name = "windows_i686_gnu"
+version = "0.39.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "763fc57100a5f7042e3057e7e8d9bdd7860d330070251a73d003563a3bb49e1b"
+
 [[package]]
 name = "windows_i686_gnu"
 version = "0.42.2"
@@ -7114,6 +7139,12 @@ version = "0.34.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "9cd9d32ba70453522332c14d38814bceeb747d80b3958676007acadd7e166956"
 
+[[package]]
+name = "windows_i686_msvc"
+version = "0.39.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7bc7cbfe58828921e10a9f446fcaaf649204dcfe6c1ddd712c5eebae6bda1106"
+
 [[package]]
 name = "windows_i686_msvc"
 version = "0.42.2"
@@ -7138,6 +7169,12 @@ version = "0.34.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "cfce6deae227ee8d356d19effc141a509cc503dfd1f850622ec4b0f84428e1f4"
 
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.39.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6868c165637d653ae1e8dc4d82c25d4f97dd6605eaa8d784b5c6e0ab2a252b65"
+
 [[package]]
 name = "windows_x86_64_gnu"
 version = "0.42.2"
@@ -7174,6 +7211,12 @@ version = "0.34.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "d19538ccc21819d01deaf88d6a17eae6596a12e9aafdbb97916fb49896d89de9"
 
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.39.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5e4d40883ae9cae962787ca76ba76390ffa29214667a111db9e0a1ad8377e809"
+
 [[package]]
 name = "windows_x86_64_msvc"
 version = "0.42.2"
diff --git a/Cargo.toml b/Cargo.toml
index 6f7e8c08f..9e8224fc1 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,6 +1,6 @@
 [package]
 name = "rustdesk"
-version = "1.2.0"
+version = "1.2.3"
 authors = ["rustdesk <info@rustdesk.com>"]
 edition = "2021"
 build= "build.rs"
@@ -48,7 +48,7 @@ lazy_static = "1.4"
 sha2 = "0.10"
 repng = "0.2"
 parity-tokio-ipc = { git = "https://github.com/open-trade/parity-tokio-ipc" }
-runas = "1.0"
+runas = "=1.0" # https://github.com/mitsuhiko/rust-runas/issues/13
 magnum-opus = { git = "https://github.com/rustdesk/magnum-opus" }
 dasp = { version = "0.11", features = ["signal", "interpolate-linear", "interpolate"], optional = true }
 rubato = { version = "0.12", optional = true }
@@ -80,7 +80,6 @@ cpal = "0.15"
 ringbuf = "0.3"
 
 [target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies]
-machine-uid = "0.3"
 mac_address = "1.1"
 sciter-rs = { git = "https://github.com/open-trade/rust-sciter", branch = "dyn" }
 sys-locale = "0.3"
@@ -98,6 +97,7 @@ virtual_display = { path = "libs/virtual_display", optional = true }
 impersonate_system = { git = "https://github.com/21pages/impersonate-system" }
 shared_memory = "0.12"
 shutdown_hooks = "0.1"
+tauri-winrt-notification = "0.1.2"
 
 [target.'cfg(target_os = "macos")'.dependencies]
 objc = "0.2"
@@ -112,7 +112,7 @@ objc_id = "0.1"
 
 [target.'cfg(any(target_os = "macos", target_os = "linux", target_os = "windows"))'.dependencies]
 tray-icon = { git = "https://github.com/rustdesk-org/tray-icon" }
-tao = { git = "https://github.com/rustdesk-org/tao", branch = "muda" }
+tao = { git = "https://github.com/rustdesk-org/tao", branch = "dev" }
 image = "0.24"
 
 [target.'cfg(any(target_os = "macos", target_os = "linux"))'.dependencies]
diff --git a/Dockerfile b/Dockerfile
index f8b0c0f77..8e44adb74 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,15 +1,15 @@
 FROM debian
 
 WORKDIR /
-RUN apt update -y && 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 unzip zip sudo libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev cmake ninja-build
+RUN apt update -y && apt install -y g++ gcc git curl nasm yasm libgtk-3-dev clang libxcb-randr0-dev libxdo-dev libxfixes-dev libxcb-shape0-dev libxcb-xfixes0-dev libasound2-dev libpulse-dev cmake unzip zip sudo libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev cmake ninja-build && rm -rf /var/lib/apt/lists/*
 
-RUN git clone https://github.com/microsoft/vcpkg && cd vcpkg && git checkout 2023.04.15
+RUN git clone --branch 2023.04.15 --depth=1 https://github.com/microsoft/vcpkg
 RUN /vcpkg/bootstrap-vcpkg.sh -disableMetrics
 RUN /vcpkg/vcpkg --disable-metrics install libvpx libyuv opus aom
 
 RUN groupadd -r user && useradd -r -g user user --home /home/user && mkdir -p /home/user && chown user /home/user && echo "user  ALL=(ALL) NOPASSWD:ALL" | sudo tee /etc/sudoers.d/user
 WORKDIR /home/user
-RUN wget https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.lnx/x64/libsciter-gtk.so
+RUN curl -LO https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.lnx/x64/libsciter-gtk.so
 USER user
 RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs > rustup.sh
 RUN chmod +x rustup.sh
diff --git a/README.md b/README.md
index 61b848c55..5cf17bf4d 100644
--- a/README.md
+++ b/README.md
@@ -5,7 +5,7 @@
   <a href="#how-to-build-with-docker">Docker</a> •
   <a href="#file-structure">Structure</a> •
   <a href="#snapshot">Snapshot</a><br>
-  [<a href="docs/README-UA.md">Українська</a>] | [<a href="docs/README-CS.md">česky</a>] | [<a href="docs/README-ZH.md">中文</a>] | [<a href="docs/README-HU.md">Magyar</a>] | [<a href="docs/README-ES.md">Español</a>] | [<a href="docs/README-FA.md">فارسی</a>] | [<a href="docs/README-FR.md">Français</a>] | [<a href="docs/README-DE.md">Deutsch</a>] | [<a href="docs/README-PL.md">Polski</a>] | [<a href="docs/README-ID.md">Indonesian</a>] | [<a href="docs/README-FI.md">Suomi</a>] | [<a href="docs/README-ML.md">മലയാളം</a>] | [<a href="docs/README-JP.md">日本語</a>] | [<a href="docs/README-NL.md">Nederlands</a>] | [<a href="docs/README-IT.md">Italiano</a>] | [<a href="docs/README-RU.md">Русский</a>] | [<a href="docs/README-PTBR.md">Português (Brasil)</a>] | [<a href="docs/README-EO.md">Esperanto</a>] | [<a href="docs/README-KR.md">한국어</a>] | [<a href="docs/README-AR.md">العربي</a>] | [<a href="docs/README-VN.md">Tiếng Việt</a>] | [<a href="docs/README-DA.md">Dansk</a>] | [<a href="docs/README-GR.md">Ελληνικά</a>]<br>
+  [<a href="docs/README-UA.md">Українська</a>] | [<a href="docs/README-CS.md">česky</a>] | [<a href="docs/README-ZH.md">中文</a>] | [<a href="docs/README-HU.md">Magyar</a>] | [<a href="docs/README-ES.md">Español</a>] | [<a href="docs/README-FA.md">فارسی</a>] | [<a href="docs/README-FR.md">Français</a>] | [<a href="docs/README-DE.md">Deutsch</a>] | [<a href="docs/README-PL.md">Polski</a>] | [<a href="docs/README-ID.md">Indonesian</a>] | [<a href="docs/README-FI.md">Suomi</a>] | [<a href="docs/README-ML.md">മലയാളം</a>] | [<a href="docs/README-JP.md">日本語</a>] | [<a href="docs/README-NL.md">Nederlands</a>] | [<a href="docs/README-IT.md">Italiano</a>] | [<a href="docs/README-RU.md">Русский</a>] | [<a href="docs/README-PTBR.md">Português (Brasil)</a>] | [<a href="docs/README-EO.md">Esperanto</a>] | [<a href="docs/README-KR.md">한국어</a>] | [<a href="docs/README-AR.md">العربي</a>] | [<a href="docs/README-VN.md">Tiếng Việt</a>] | [<a href="docs/README-DA.md">Dansk</a>] | [<a href="docs/README-GR.md">Ελληνικά</a>] | [<a href="docs/README-TR.md">Türkçe</a>]<br>
   <b>We need your help to translate this README, <a href="https://github.com/rustdesk/rustdesk/tree/master/src/lang">RustDesk UI</a> and <a href="https://github.com/rustdesk/doc.rustdesk.com">RustDesk Doc</a> to your native language</b>
 </p>
 
@@ -34,11 +34,7 @@ RustDesk welcomes contribution from everyone. See [CONTRIBUTING.md](docs/CONTRIB
 Below are the servers you are using for free, they may change over time. If you are not close to one of these, your network may be slow.
 | Location | Vendor | Specification |
 | --------- | ------------- | ------------------ |
-| South Korea (Seoul) | [AWS lightsail](https://aws.amazon.com) | 1 vCPU / 0.5 GB RAM |
 | Germany | [Hetzner](https://www.hetzner.com) | 2 vCPU / 4 GB RAM |
-| Germany | [Codext](https://codext.de) | 4 vCPU / 8 GB RAM |
-| Finland (Helsinki) | [Netlock](https://netlockendpoint.com) | 4 vCPU / 8 GB RAM |
-| USA (Ashburn) | [Netlock](https://netlockendpoint.com) | 4 vCPU / 8 GB RAM |
 | Ukraine (Kyiv) | [dc.volia](https://dc.volia.com) | 2 vCPU / 4 GB RAM |
 
 ## Dev Container
diff --git a/appimage/AppImageBuilder-aarch64.yml b/appimage/AppImageBuilder-aarch64.yml
index 87c854d5d..337e022be 100644
--- a/appimage/AppImageBuilder-aarch64.yml
+++ b/appimage/AppImageBuilder-aarch64.yml
@@ -2,12 +2,13 @@
 version: 1
 script:
  - rm -rf ./AppDir || true
- - bsdtar -zxvf ../rustdesk-1.2.0.deb
+ - bsdtar -zxvf ../rustdesk-1.2.3.deb
  - tar -xvf ./data.tar.xz
  - mkdir ./AppDir
  - mv ./usr ./AppDir/usr
  # 32x32 icon
  - for i in {32,64,128}; do mkdir -p ./AppDir/usr/share/icons/hicolor/$i\x$i/apps/; cp ../res/$i\x$i.png ./AppDir/usr/share/icons/hicolor/$i\x$i/apps/rustdesk.png; done
+ - mkdir -p ./AppDir/usr/share/icons/hicolor/scalable/apps/; cp ../res/scalable.svg ./AppDir/usr/share/icons/hicolor/scalable/apps/rustdesk.svg
  # desktop file
  # - sed -i "s/Icon=\/usr\/share\/rustdesk\/files\/rustdesk.png/Icon=rustdesk/g" ./AppDir/usr/share/applications/rustdesk.desktop
  - rm -rf ./AppDir/usr/share/applications
@@ -17,7 +18,7 @@ AppDir:
     id: rustdesk
     name: rustdesk
     icon: rustdesk
-    version: 1.2.0
+    version: 1.2.3
     exec: usr/lib/rustdesk/rustdesk
     exec_args: $@
   apt:
diff --git a/appimage/AppImageBuilder-x86_64.yml b/appimage/AppImageBuilder-x86_64.yml
index 8f72afd08..650d2f201 100644
--- a/appimage/AppImageBuilder-x86_64.yml
+++ b/appimage/AppImageBuilder-x86_64.yml
@@ -2,12 +2,13 @@
 version: 1
 script:
  - rm -rf ./AppDir || true
- - bsdtar -zxvf ../rustdesk-1.2.0.deb
+ - bsdtar -zxvf ../rustdesk-1.2.3.deb
  - tar -xvf ./data.tar.xz
  - mkdir ./AppDir
  - mv ./usr ./AppDir/usr
  # 32x32 icon
  - for i in {32,64,128}; do mkdir -p ./AppDir/usr/share/icons/hicolor/$i\x$i/apps/; cp ../res/$i\x$i.png ./AppDir/usr/share/icons/hicolor/$i\x$i/apps/rustdesk.png; done
+ - mkdir -p ./AppDir/usr/share/icons/hicolor/scalable/apps/; cp ../res/scalable.svg ./AppDir/usr/share/icons/hicolor/scalable/apps/rustdesk.svg
  # desktop file
  # - sed -i "s/Icon=\/usr\/share\/rustdesk\/files\/rustdesk.png/Icon=rustdesk/g" ./AppDir/usr/share/applications/rustdesk.desktop
  - rm -rf ./AppDir/usr/share/applications
@@ -17,7 +18,7 @@ AppDir:
     id: rustdesk
     name: rustdesk
     icon: rustdesk
-    version: 1.2.0
+    version: 1.2.3
     exec: usr/lib/rustdesk/rustdesk
     exec_args: $@
   apt:
@@ -52,6 +53,7 @@ AppDir:
     - libva-x11-2
     - libvdpau1
     - libgstreamer-plugins-base1.0-0
+    - gstreamer1.0-pipewire
     - libwayland-cursor0
     - libwayland-egl1
     - libpulse0
diff --git a/build.py b/build.py
index 9e19e10e7..42dc8d6de 100755
--- a/build.py
+++ b/build.py
@@ -71,14 +71,14 @@ def parse_rc_features(feature):
             return 'osx' in platforms
         else:
             return 'linux' in platforms
-        
+
     def get_all_features():
         features = []
         for (feat, feat_info) in available_features.items():
             if platform_check(feat_info['platform']):
                 features.append(feat)
         return features
-    
+
     if isinstance(feature, str) and feature.upper() == 'ALL':
         return get_all_features()
     elif isinstance(feature, list):
@@ -285,7 +285,7 @@ Version: %s
 Architecture: %s
 Maintainer: rustdesk <info@rustdesk.com>
 Homepage: https://rustdesk.com
-Depends: libgtk-3-0, libxcb-randr0, libxdo3, libxfixes3, libxcb-shape0, libxcb-xfixes0, libasound2, libsystemd0, curl, libva-drm2, libva-x11-2, libvdpau1, libgstreamer-plugins-base1.0-0, libpam0g
+Depends: libgtk-3-0, libxcb-randr0, libxdo3, libxfixes3, libxcb-shape0, libxcb-xfixes0, libasound2, libsystemd0, curl, libva-drm2, libva-x11-2, libvdpau1, libgstreamer-plugins-base1.0-0, libpam0g, libappindicator3-1, gstreamer1.0-pipewire
 Description: A remote control software.
 
 """ % (version, get_arch())
@@ -311,6 +311,8 @@ def build_flutter_deb(version, features):
     system2('mkdir -p tmpdeb/etc/rustdesk/')
     system2('mkdir -p tmpdeb/etc/pam.d/')
     system2('mkdir -p tmpdeb/usr/share/rustdesk/files/systemd/')
+    system2('mkdir -p tmpdeb/usr/share/icons/hicolor/256x256/apps/')
+    system2('mkdir -p tmpdeb/usr/share/icons/hicolor/scalable/apps/')
     system2('mkdir -p tmpdeb/usr/share/applications/')
     system2('mkdir -p tmpdeb/usr/share/polkit-1/actions')
     system2('rm tmpdeb/usr/bin/rustdesk || true')
@@ -319,7 +321,9 @@ def build_flutter_deb(version, features):
     system2(
         'cp ../res/rustdesk.service tmpdeb/usr/share/rustdesk/files/systemd/')
     system2(
-        'cp ../res/128x128@2x.png tmpdeb/usr/share/rustdesk/files/rustdesk.png')
+        'cp ../res/128x128@2x.png tmpdeb/usr/share/icons/hicolor/256x256/apps/rustdesk.png')
+    system2(
+        'cp ../res/scalable.svg tmpdeb/usr/share/icons/hicolor/scalable/apps/rustdesk.svg')
     system2(
         'cp ../res/rustdesk.desktop tmpdeb/usr/share/applications/rustdesk.desktop')
     system2(
@@ -351,6 +355,8 @@ def build_deb_from_folder(version, binary_folder):
     system2('mkdir -p tmpdeb/usr/bin/')
     system2('mkdir -p tmpdeb/usr/lib/rustdesk')
     system2('mkdir -p tmpdeb/usr/share/rustdesk/files/systemd/')
+    system2('mkdir -p tmpdeb/usr/share/icons/hicolor/256x256/apps/')
+    system2('mkdir -p tmpdeb/usr/share/icons/hicolor/scalable/apps/')
     system2('mkdir -p tmpdeb/usr/share/applications/')
     system2('mkdir -p tmpdeb/usr/share/polkit-1/actions')
     system2('rm tmpdeb/usr/bin/rustdesk || true')
@@ -359,7 +365,9 @@ def build_deb_from_folder(version, binary_folder):
     system2(
         'cp ../res/rustdesk.service tmpdeb/usr/share/rustdesk/files/systemd/')
     system2(
-        'cp ../res/128x128@2x.png tmpdeb/usr/share/rustdesk/files/rustdesk.png')
+        'cp ../res/128x128@2x.png tmpdeb/usr/share/icons/hicolor/256x256/apps/rustdesk.png')
+    system2(
+        'cp ../res/scalable.svg tmpdeb/usr/share/icons/hicolor/scalable/apps/rustdesk.svg')
     system2(
         'cp ../res/rustdesk.desktop tmpdeb/usr/share/applications/rustdesk.desktop')
     system2(
@@ -537,13 +545,6 @@ def main():
                     'cp libsciter.dylib target/release/bundle/osx/RustDesk.app/Contents/MacOS/')
                 # https://github.com/sindresorhus/create-dmg
                 system2('/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("</dict>", """
-    <key>LSUIElement</key>
-    <string>1</string>
-    </dict>"""))
                 pa = os.environ.get('P')
                 if pa:
                     system2('''
@@ -556,7 +557,7 @@ def main():
     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))
-                system2('create-dmg target/release/bundle/osx/RustDesk.app')
+                system2('create-dmg "RustDesk %s.dmg" "target/release/bundle/osx/RustDesk.app"' % version)
                 os.rename('RustDesk %s.dmg' %
                           version, 'rustdesk-%s.dmg' % version)
                 if pa:
@@ -581,10 +582,14 @@ def main():
                     'mv target/release/bundle/deb/rustdesk*.deb ./rustdesk.deb')
                 system2('dpkg-deb -R rustdesk.deb tmpdeb')
                 system2('mkdir -p tmpdeb/usr/share/rustdesk/files/systemd/')
+                system2('mkdir -p tmpdeb/usr/share/icons/hicolor/256x256/apps/')
+                system2('mkdir -p tmpdeb/usr/share/icons/hicolor/scalable/apps/')
                 system2(
                     'cp res/rustdesk.service tmpdeb/usr/share/rustdesk/files/systemd/')
                 system2(
-                    'cp res/128x128@2x.png tmpdeb/usr/share/rustdesk/files/rustdesk.png')
+                    'cp res/128x128@2x.png tmpdeb/usr/share/icons/hicolor/256x256/apps/rustdesk.png')
+                system2(
+                    'cp res/scalable.svg tmpdeb/usr/share/icons/hicolor/scalable/apps/rustdesk.svg')
                 system2(
                     'cp res/rustdesk.desktop tmpdeb/usr/share/applications/rustdesk.desktop')
                 system2(
diff --git a/docs/CODE_OF_CONDUCT-TR.md b/docs/CODE_OF_CONDUCT-TR.md
new file mode 100644
index 000000000..76088bd95
--- /dev/null
+++ b/docs/CODE_OF_CONDUCT-TR.md
@@ -0,0 +1,89 @@
+# Katkıda Bulunanların Davranış Kuralları
+
+## Taahhüdümüz
+
+Biz üyeler, katkıda bulunanlar ve liderler olarak, yaş, beden büyüklüğü, görünür veya görünmez engellilik, etnik köken, cinsiyet özellikleri, cinsiyet kimliği ve ifadesi, deneyim seviyesi, eğitim, sosyo-ekonomik durum, milliyet, kişisel görünüm, ırk, din veya cinsel kimlik ve yönelim ayrımı gözetmeksizin herkes için topluluğumuzdaki katılımı taciz içermeyen bir deneyim haline getirmeyi taahhüt ederiz.
+
+Açık, hoşgörülü, çeşitli, kapsayıcı ve sağlıklı bir topluluğa katkıda bulunacak şekillerde hareket etmeyi ve etkileşimde bulunmayı taahhüt ederiz.
+
+## Standartlarımız
+
+Topluluğumuz için olumlu bir ortam yaratmaya katkıda bulunan davranış örnekleri şunlardır:
+
+* Diğer insanlara empati ve nezaket göstermek
+* Farklı görüşlere, bakış açılarına ve deneyimlere saygılı olmak
+* Yapıcı eleştiriyi vermek ve zarifçe kabul etmek
+* Hatalarımızdan etkilenenlere sorumluluk kabul etmek, özür dilemek ve deneyimden öğrenmek
+* Sadece bireyler olarak değil, aynı zamanda genel topluluk için en iyisi üzerine odaklanmak
+
+Kabul edilemez davranış örnekleri şunları içerir:
+
+* Cinselleştirilmiş dil veya imgelerin kullanımı ve cinsel ilgi veya herhangi bir türdeki yaklaşımlar
+* Trollük, aşağılayıcı veya hakaret içeren yorumlar ve kişisel veya siyasi saldırılar
+* Kamuoyu veya özel taciz
+* Başkalarının fiziksel veya e-posta adresi gibi özel bilgilerini, açık izinleri olmadan yayınlamak
+* Profesyonel bir ortamda makul bir şekilde uygunsuz kabul edilebilecek diğer davranışlar
+
+## Uygulama Sorumlulukları
+
+Topluluk liderleri, kabul edilebilir davranış standartlarımızı açıklığa kavuşturmak ve uygulamakla sorumludur ve uygunsuz, tehditkar, saldırgan veya zarar verici herhangi bir davranışa yanıt olarak uygun ve adil düzeltici önlemler alacaklardır.
+
+Topluluk liderleri, bu Davranış Kurallarına uyumlu olmayan yorumları, taahhütlerini veya kodu, wiki düzenlemelerini, sorunları ve diğer katkıları kaldırma, düzenleme veya reddetme hakkına sahiptir. Denetim kararlarının nedenlerini uygun olduğunda ileteceklerdir.
+
+## Kapsam
+
+Bu Davranış Kuralları, tüm topluluk alanlarında geçerlidir ve aynı zamanda birey resmi olarak topluluğu halka açık alanlarda temsil ettiğinde de geçerlidir. Topluluğumuzu temsil etme örnekleri, resmi bir e-posta adresi kullanmak, resmi bir sosyal medya hesabı üzerinden gönderi yapmak veya çevrimiçi veya çevrimdışı bir etkinlikte atanmış bir temsilci olarak hareket etmeyi içerir.
+
+## Uygulama
+
+Taciz edici, rahatsız edici veya başka türlü kabul edilemez davranış örnekleri, [info@rustdesk.com](mailto:info@rustdesk.com) adresindeki uygulama sorumlularına bildirilebilir. Tüm şikayetler hızlı ve adil bir şekilde incelenecek ve araştırılacaktır.
+
+Tüm topluluk liderleri, olayın raporlayıcısının gizliliğine ve güvenliğine saygı gösterme yükümlülüğündedir.
+
+## Uygulama Kılavuzları
+
+Topluluk liderleri, bu Davranış Kurallarını ihlal olarak değerlendirdikleri herhangi bir eylem için bu Topluluk Etkisi Kılavuzlarını izleyeceklerdir:
+
+### 1. Düzeltme
+
+**Topluluk Etkisi**: Topluluk içinde profesyonel veya hoşgörülü olmayan uygun olmayan dil veya diğer davranışların kullanımı.
+
+**Sonuç**: Topluluk liderlerinden özel ve yazılı bir uyarı almak, ihlalin niteliği ve davranışın nedeninin açıklığa kavuşturulması. Bir kamu özrü istenebilir.
+
+### 2. Uyarı
+
+**Topluluk Etkisi**: Tek bir olay veya dizi aracılığıyla bir ihlal.
+
+**Sonuç**: Devam eden davranış için sonuçları olan bir uyarı. Topluluk liderleri de dahil olmak üzere ihlalle ilgili kişilerle etkileşim, belirli bir süre boyunca önerilmez. Bu, topluluk alanlarında ve sosyal medya gibi harici kanallarda etkileşimleri içerir. Bu koşulları ihlal etmek geçici veya kalıcı bir yasağa yol açabilir.
+
+### 3. Geçici Yasak
+
+**Topluluk Etkisi**: Sürekli uygunsuz davranış da dahil olmak üzere topluluk standartlarının ciddi bir ihlali.
+
+**Sonuç**: Belirli bir süre için toplulukla herhangi bir türdeki etkileşim veya halka açık iletişimden geçici bir yasak. Bu dönem boyunca, toplul
+
+ukla veya uygulama kurallarını uygulayanlarla her türlü kamuoyu veya özel etkileşim izin verilmez. Bu koşulları ihlal etmek geçici veya kalıcı bir yasağa yol açabilir.
+
+### 4. Kalıcı Yasak
+
+**Topluluk Etkisi**: Topluluk standartlarının ihlalinde sürekli bir desen sergilemek, bireye sürekli olarak uygun olmayan davranışlarda bulunmak, bir bireye tacizde bulunmak veya birey sınıflarına karşı saldırganlık veya aşağılama yapmak.
+
+**Sonuç**: Topluluk içinde her türlü halka açık etkileşimden kalıcı bir yasak.
+
+## Atıf
+
+Bu Davranış Kuralları, [Contributor Covenant][anasayfa], 2.0 sürümünden uyarlanmıştır ve
+[https://www.contributor-covenant.org/version/2/0/code_of_conduct.html][v2.0] adresinde bulunmaktadır.
+
+Topluluk Etkisi Kılavuzları, 
+[Mozilla'nın davranış kuralları uygulama merdiveni][Mozilla DK] tarafından ilham alınarak oluşturulmuştur.
+
+Bu davranış kuralları hakkında yaygın soruların cevapları için, SSS'ye göz atın:
+[https://www.contributor-covenant.org/faq][SSS]. Çeviriler, 
+[https://www.contributor-covenant.org/translations][çeviriler] adresinde bulunabilir.
+
+[anasayfa]: https://www.contributor-covenant.org
+[v2.0]: https://www.contributor-covenant.org/version/2/0/code_of_conduct.html
+[Mozilla DK]: https://github.com/mozilla/diversity
+[SSS]: https://www.contributor-covenant.org/faq
+[çeviriler]: https://www.contributor-covenant.org/translations
diff --git a/docs/CONTRIBUTING-ID.md b/docs/CONTRIBUTING-ID.md
new file mode 100644
index 000000000..cdff6c01f
--- /dev/null
+++ b/docs/CONTRIBUTING-ID.md
@@ -0,0 +1,31 @@
+# Berkontribusi dalam pengembangan RustDesk
+
+RustDesk mengajak semua orang untuk ikut berkontribusi. Berikut ini adalah panduan jika kamu sedang mempertimbangkan untuk memberikan bantuan kepada kami:
+
+## Kontirbusi
+
+Untuk melakukan kontribusi pada RustDesk atau dependensinya, sebaiknya dilakukan dalam bentuk pull request di GitHub. Setiap permintaan pull request akan ditinjau oleh kontributor utama atau seseorang yang memiliki wewenang untuk menggabungkan perubahan kode, baik yang sudah dimasukkan ke dalam struktur utama ataupun memberikan umpan balik untuk perubahan yang akan diperlukan. Setiap kontribusi harus sesuai dengan format ini, juga termasuk yang berasal dari kontributor utama.
+
+Apabila kamu ingin mengatasi sebuah masalah yang sudah ada di daftar issue, harap klaim terlebih dahulu dengan memberikan komentar pada GitHub issue yang ingin kamu kerjakan. Hal ini dilakukan untuk mencegah terjadinya duplikasi dari kontributor pada daftar issue yang sama.
+
+## Pemeriksaan Pull Request
+
+- Branch yang menjadi acuan adalah branch master dari repositori utama dan, jika diperlukan, lakukan rebase ke branch  master yang terbaru sebelum kamu mengirim pull request. Apabila terdapat masalah kita melakukan proses merge ke branch master kemungkinan kamu akan diminta untuk melakukan rebase pada perubahan yang sudah dibuat.
+
+- Sebaiknya buatlah commit seminimal mungkin, sambil memastikan bahwa setiap commit yang dibuat sudah benar (contohnya, setiap commit harus bisa di kompilasi dan berhasil melewati tahap test).
+
+- Setiap commit harus disertai dengan tanda tangan Sertifikat Asal Pengembang (Developer Certificate of Origin) (<http://developercertificate.org>), yang mengindikasikan bahwa kamu (and your employer if applicable) bersedia untuk patuh terhadap persyaratan dari [lisensi projek](../LICENCE). Di git bash, ini adalah opsi parameter `-s` pada `git commit`
+
+- Jika perubahan yang kamu buat tidak mendapat tinjauan atau kamu membutuhkan orang tertentu untuk meninjaunya, kamu bisa @-reply seorang reviewer meminta peninjauan dalam permintaan pull request atau komentar, atau kamu bisa meminta tinjauan melalui [email](mailto:info@rustdesk.com).
+
+- Sertakan test yang relevan terhadap bug atau fitur baru yang sudah dikerjakan.
+
+Untuk instruksi Git yang lebih lanjut, cek disini [GitHub workflow 101](https://github.com/servo/servo/wiki/GitHub-workflow).
+
+## Tindakan
+
+<https://github.com/rustdesk/rustdesk/blob/master/docs/CODE_OF_CONDUCT-ID.md>
+
+## Komunikasi
+
+Kontributor RustDesk sering berkunjung ke [Discord](https://discord.gg/nDceKgxnkV).
diff --git a/docs/CONTRIBUTING-IT.md b/docs/CONTRIBUTING-IT.md
new file mode 100644
index 000000000..a3a5fd2b6
--- /dev/null
+++ b/docs/CONTRIBUTING-IT.md
@@ -0,0 +1,37 @@
+# Contribuzione a RustDesk
+
+RustDesk accoglie con favore il contributo di tutti.
+Ecco le linee guida se stai pensando di aiutarci.
+
+## Contribuzione
+
+I contributi a RustDesk o alle sue dipendenze dovrebbero essere forniti sotto forma di richieste pull GitHub. 
+Ogni richiesta pull verr� esaminata da un collaboratore principale (qualcuno con il permesso di applicare) ed � abilitato all'uso dell'albero principale o dare un feedback per le modifiche che sarebbero necessarie. 
+Tutti i contributi dovrebbero seguire questo formato, anche quelli dei contributori principali.
+
+Se desideri lavorare su un problema, rivendicalo prima commentando
+il problema di GitHub su cui vuoi lavorare. 
+Questo per evitare duplicati sforzi dei contributori sullo stesso problema.
+
+## Elenco di controllo delle richieste pull
+
+- Branch del master branch e, se necessario, rebase al master attuale branch prima di inviare la richiesta pull. 
+  Se l'unione non � in mod pulito con il master ti potrebbe essere chiesto di effettuare il rebase delle modifiche.
+
+- Le modifiche dovrebbero essere le pi� piccole possibile, assicurando al tempo stesso che ogni modifica sia corretta in modo indipendente (ovvero, ogni modifica dovrebbe essere compilabile e superare i test).
+
+- Le modifiche devono essere accompagnati da un certificato di origine per sviluppatori firmato (http://developercertificate.org), che indica che tu (e il tuo datore di lavoro se applicabile) accetti di essere vincolato dai termini della [licenza progetto](../LICENCE). In git, questa � l'opzione `-s` di `git commit`
+
+- Se la tua patch non viene esaminata o hai bisogno che una persona specifica la esamini, puoi @-rispondere ad un revisore chiedendo una revisione nella richiesta pull o un commento, oppure puoi chiedere una revisione tramite [email](mailto:info@rustdesk.com).
+
+- Aggiungi test relativi al bug corretto o alla nuova funzionalit�.
+
+Per istruzioni specifiche su git, vedi [Workflow GitHub - 101](https://github.com/servo/servo/wiki/GitHub-workflow).
+
+## Condotta
+
+https://github.com/rustdesk/rustdesk/blob/master/docs/CODE_OF_CONDUCT-IT.md
+
+## Comunicazioni
+
+I contributori di RustDesk frequentano [Discord](https://discord.gg/nDceKgxnkV).
diff --git a/docs/CONTRIBUTING-TR.md b/docs/CONTRIBUTING-TR.md
new file mode 100644
index 000000000..6e9e3f3ed
--- /dev/null
+++ b/docs/CONTRIBUTING-TR.md
@@ -0,0 +1,31 @@
+# RustDesk'a Katkı Sağlamak
+
+RustDesk, herkesten katkıyı memnuniyetle karşılar. Eğer bize yardımcı olmayı düşünüyorsanız, işte rehberlik eden kurallar:
+
+## Katkılar
+
+RustDesk veya bağımlılıklarına yapılan katkılar, GitHub pull istekleri şeklinde yapılmalıdır. Her bir pull isteği, çekirdek katkıcı tarafından gözden geçirilecek (yamaları kabul etme izni olan biri) ve ana ağaca kabul edilecek veya gerekli değişiklikler için geri bildirim verilecektir. Tüm katkılar bu formata uymalıdır, çekirdek katkıcılardan gelenler bile.
+
+Eğer bir konu üzerinde çalışmak isterseniz, önce üzerinde çalışmak istediğinizi belirten bir yorum yaparak konuyu talep ediniz. Bu, katkı sağlayanların aynı konuda çift çalışmasını engellemek içindir.
+
+## Pull İstek Kontrol Listesi
+
+- Master dalından dallandırın ve gerekiyorsa pull isteğinizi göndermeden önce mevcut master dalına rebase yapın. Eğer master ile temiz bir şekilde birleşmezse, değişikliklerinizi rebase yapmanız istenebilir.
+
+- Her bir commit mümkün olduğunca küçük olmalıdır, ancak her commit'in bağımsız olarak doğru olduğundan emin olun (örneğin, her commit derlenebilir ve testleri geçmelidir).
+
+- Commit'ler, bir Geliştirici Sertifikası ile desteklenmelidir (http://developercertificate.org). Bu, [proje lisansının](../LICENCE) koşullarına uymayı kabul ettiğinizi gösteren bir onaydır. Git'te bunu `git commit` seçeneği olarak `-s` seçeneği ile yapabilirsiniz.
+
+- Yamalarınız gözden geçirilmiyorsa veya belirli bir kişinin gözden geçirmesine ihtiyacınız varsa, çekme isteği veya yorum içinde bir gözden geçirmeyi istemek için bir inceleyiciyi @etiketleyebilir veya inceleme için [e-posta](mailto:info@rustdesk.com) ile talep edebilirsiniz.
+
+- Düzelttiğiniz hatanın veya eklediğiniz yeni özelliğin ilgili testlerini ekleyin.
+
+Daha spesifik git talimatları için, [GitHub iş akışı 101](https://github.com/servo/servo/wiki/GitHub-workflow)'e bakınız.
+
+## Davranış
+
+https://github.com/rustdesk/rustdesk/blob/master/docs/CODE_OF_CONDUCT-TR.md
+
+## İletişim
+
+RustDesk katkı sağlayıcıları, [Discord](https://discord.gg/nDceKgxnkV) kanalını sık sık ziyaret ederler.
diff --git a/docs/DEVCONTAINER-IT.md b/docs/DEVCONTAINER-IT.md
new file mode 100644
index 000000000..713c6fc37
--- /dev/null
+++ b/docs/DEVCONTAINER-IT.md
@@ -0,0 +1,14 @@
+
+Dopo l'avvio di devcontainer nel contenitore docker, viene creato un binario linux in modalità debug.
+
+Attualmente devcontainer consente creazione build Linux e Android sia in modalità debug che in modalità rilascio.
+
+Di seguito è riportata la tabella dei comandi da eseguire dalla root del progetto per la creazione di build specifiche.
+
+Comando|Tipo build|Modo
+-|-|-|
+`.devcontainer/build.sh --debug linux`|Linux|debug
+`.devcontainer/build.sh --release linux`|Linux|release
+`.devcontainer/build.sh --debug android`|android-arm64|debug
+`.devcontainer/build.sh --release android`|android-arm64|release
+
diff --git a/docs/DEVCONTAINER-TR.md b/docs/DEVCONTAINER-TR.md
new file mode 100644
index 000000000..7fc14ce5e
--- /dev/null
+++ b/docs/DEVCONTAINER-TR.md
@@ -0,0 +1,12 @@
+Docker konteynerinde devcontainer'ın başlatılmasından sonra, hata ayıklama modunda bir Linux ikili dosyası oluşturulur.
+
+Şu anda devcontainer, hata ayıklama ve sürüm modunda hem Linux hem de Android derlemeleri sunmaktadır.
+
+Aşağıda, belirli derlemeler oluşturmak için projenin kökünden çalıştırılması gereken komutlar yer almaktadır.
+
+Komut | Derleme Türü | Mod
+-|-|-
+`.devcontainer/build.sh --debug linux` | Linux | hata ayıklama
+`.devcontainer/build.sh --release linux` | Linux | sürüm
+`.devcontainer/build.sh --debug android` | Android-arm64 | hata ayıklama
+`.devcontainer/build.sh --release android` | Android-arm64 | sürüm
diff --git a/docs/README-AR.md b/docs/README-AR.md
index 65a6da3d4..80948fb39 100644
--- a/docs/README-AR.md
+++ b/docs/README-AR.md
@@ -32,11 +32,8 @@
 فيما يلي الخوادم التي تستخدمها مجانًا، وقد تتغير طوال الوقت. إذا لم تكن قريبًا من أحد هؤلاء، فقد تكون شبكتك بطيئة.
 | الموقع | المورد | المواصفات |
 | --------- | ------------- | ------------------ |
-| Seoul | AWS lightsail | 1 vCPU / 0.5GB RAM |
 | Germany | Hetzner | 2 vCPU / 4GB RAM |
-| Germany | Codext | 4 vCPU / 8GB RAM |
-| Finland (Helsinki) | 0x101 Cyber Security | 4 vCPU / 8GB RAM |
-| USA (Ashburn) | 0x101 Cyber Security | 4 vCPU / 8GB RAM |
+| Ukraine (Kyiv) | [dc.volia](https://dc.volia.com) | 2 vCPU / 4 GB RAM |
 
 ## التبعيات
 
diff --git a/docs/README-CS.md b/docs/README-CS.md
index b3e2cb0d2..15e576c47 100644
--- a/docs/README-CS.md
+++ b/docs/README-CS.md
@@ -27,11 +27,8 @@ Projekt RustDesk vítá přiložení ruky k dílu od každého. Jak začít se d
 Níže jsou uvedeny servery zdarma k vašemu použití (údaje se mohou v čase měnit). Pokud se nenacházíte v oblastech světa poblíž nich, spojení může být pomalé.
 | umístění | dodavatel | parametry |
 | --------- | ------------- | ------------------ |
-| Seoul | AWS lightsail | 1 vCPU / 0.5GB RAM |
 | Germany | Hetzner | 2 vCPU / 4GB RAM |
-| Germany | Codext | 4 vCPU / 8GB RAM |
-| Finland (Helsinki) | 0x101 Cyber Security | 4 vCPU / 8GB RAM |
-| USA (Ashburn) | 0x101 Cyber Security | 4 vCPU / 8GB RAM |
+| Ukraine (Kyiv) | [dc.volia](https://dc.volia.com) | 2 vCPU / 4 GB RAM |
 
 ## Softwarové součásti, na kterých závisí
 
diff --git a/docs/README-DA.md b/docs/README-DA.md
index f17b3abfd..af2f5937f 100644
--- a/docs/README-DA.md
+++ b/docs/README-DA.md
@@ -25,11 +25,7 @@ Nedenfor er de servere, du bruger gratis, det kan ændre sig med tiden. Hvis du
 
 | Beliggenhed | Udbyder | Specifikation |
 | ---------- | ------------- | ------------------ |
-| Seoul | AWS lightsail | 1 vCPU / 0.5GB RAM |
 | Germany | Hetzner | 2 vCPU / 4GB RAM |
-| Germany | Codext | 4 vCPU / 8GB RAM |
-| Finland (Helsinki) | 0x101 Cyber Security | 4 vCPU / 8GB RAM |
-| USA (Ashburn) | 0x101 Cyber Security | 4 vCPU / 8GB RAM |
 
 ## Afhængigheder
 
diff --git a/docs/README-DE.md b/docs/README-DE.md
index 991fa721e..5ac370a87 100644
--- a/docs/README-DE.md
+++ b/docs/README-DE.md
@@ -34,11 +34,7 @@ RustDesk heißt jegliche Mitarbeit willkommen. Schauen Sie sich [CONTRIBUTING-DE
 Nachfolgend sind die Server gelistet, die Sie kostenlos nutzen können. Es kann sein, dass sich diese Liste immer mal wieder ändert. Falls Sie nicht in der Nähe einer dieser Server sind, kann es sein, dass Ihre Verbindung langsam sein wird.
 | Standort | Anbieter | Spezifikation |
 | --------- | ------------- | ------------------ |
-| Südkorea (Seoul) | [AWS lightsail](https://aws.amazon.com/de/) | 1 vCPU / 0,5 GB RAM |
 | Deutschland | [Hetzner](https://www.hetzner.com/de/) | 2 vCPU / 4 GB RAM |
-| Deutschland | [Codext](https://codext.de/) | 4 vCPU / 8 GB RAM |
-| Finnland (Helsinki) | [Netlock](https://netlockendpoint.com/de/index.html) | 4 vCPU / 8 GB RAM |
-| USA (Ashburn) | [Netlock](https://netlockendpoint.com/de/index.html) | 4 vCPU / 8 GB RAM |
 | Ukraine (Kiew) | [dc.volia](https://dc.volia.com) | 2 vCPU / 4 GB RAM |
 
 ## Dev-Container
diff --git a/docs/README-EO.md b/docs/README-EO.md
index 56acb4e6c..be1538089 100644
--- a/docs/README-EO.md
+++ b/docs/README-EO.md
@@ -24,11 +24,7 @@ RustDesk bonvenigas kontribuon de ĉiuj. Vidu [`docs/CONTRIBUTING.md`](CONTRIBUT
 Malsupre estas la serviloj, kiuj vi uzas senpage, ĝi povas ŝanĝi laŭlonge de la tempo. Se vi ne estas proksima de unu de tiuj, via reto povas esti malrapida.
 | Situo | Vendanto | Detaloj |
 | --------- | ------------- | ------------------ |
-| Seoul | AWS lightsail | 1 vCPU / 0.5GB RAM |
 | Germany | Hetzner | 2 vCPU / 4GB RAM |
-| Germany | Codext | 4 vCPU / 8GB RAM |
-| Finland (Helsinki) | [Netlock](https://netlockendpoint.com) | 4 vCPU / 8GB RAM |
-| USA (Ashburn) | [Netlock](https://netlockendpoint.com) | 4 vCPU / 8GB RAM |
 | Ukraine (Kyiv) | [dc.volia](https://dc.volia.com) | 2 vCPU / 4GB RAM |
 
 ## Dependantaĵoj
diff --git a/docs/README-ES.md b/docs/README-ES.md
index 6c5ebd83c..19a7335d3 100644
--- a/docs/README-ES.md
+++ b/docs/README-ES.md
@@ -31,11 +31,7 @@ A continuación se muestran los servidores gratuitos, pueden cambiar a medida qu
 
 | Ubicación | Compañía | Especificación |
 | --------- | ------------- | ------------------ |
-| Seoul | AWS lightsail | 1 vCPU / 0.5GB RAM |
 | Germany | Hetzner | 2 vCPU / 4GB RAM |
-| Germany | Codext | 4 vCPU / 8GB RAM |
-| Finland (Helsinki) | [Netlock](https://netlockendpoint.com) | 4 vCPU / 8GB RAM |
-| USA (Ashburn) | [Netlock](https://netlockendpoint.com) | 4 vCPU / 8GB RAM |
 | Ukraine (Kyiv) | [dc.volia](https://dc.volia.com) | 2 vCPU / 4GB RAM |
 
 ## Dependencias
diff --git a/docs/README-FA.md b/docs/README-FA.md
index a23d91930..989b0047c 100644
--- a/docs/README-FA.md
+++ b/docs/README-FA.md
@@ -30,11 +30,7 @@
 شما مي‌توانید از سرورهای زیر به رایگان استفاده کنید. این لیست ممکن است به مرور زمان تغییر می‌کند. اگر به این سرورها نزدیک نیستید، ممکن است اتصال شما کند باشد.
 | موقعیت | سرویس دهنده | مشخصات |
 | --------- | ------------- | ------------------ |
-| کره‌ی جنوبی، سئول | AWS lightsail | 1 vCPU / 0.5GB RAM |
 | آلمان | Hetzner | 2 vCPU / 4GB RAM |
-| آلمان | Codext | 4 vCPU / 8GB RAM |
-| فنلاند، هلسینکی | 0x101 Cyber Security | 4 vCPU / 8GB RAM |
-| ایالات متحده، اَشبرن | 0x101 Cyber Security | 4 vCPU / 8GB RAM |
 
 ## وابستگی ها
 
diff --git a/docs/README-FI.md b/docs/README-FI.md
index b99d469bb..195d29c84 100644
--- a/docs/README-FI.md
+++ b/docs/README-FI.md
@@ -24,11 +24,7 @@ RustDesk toivottaa avustukset tervetulleiksi kaikilta. Katso lisätietoja [`docs
 Alla on palvelimia, joita voit käyttää ilmaiseksi, ne saattavat muuttua ajan mittaan. Jos et ole lähellä yhtä näistä, verkkosi voi olla hidas.
 | Sijainti | Myyjä | Määrittely |
 | --------- | ------------- | ------------------ |
-| Seoul | AWS lightsail | 1 vCPU / 0.5GB RAM |
 | Germany | Hetzner | 2 vCPU / 4GB RAM |
-| Germany | Codext | 4 vCPU / 8GB RAM |
-| Finland (Helsinki) | [Netlock](https://netlockendpoint.com) | 4 vCPU / 8GB RAM |
-| USA (Ashburn) | [Netlock](https://netlockendpoint.com) | 4 vCPU / 8GB RAM |
 | Ukraine (Kyiv) | [dc.volia](https://dc.volia.com) | 2 vCPU / 4GB RAM |
 
 ## Riippuvuudet
diff --git a/docs/README-FR.md b/docs/README-FR.md
index ce4c45c57..39f09a625 100644
--- a/docs/README-FR.md
+++ b/docs/README-FR.md
@@ -25,11 +25,7 @@ Ci-dessous se trouvent les serveurs que vous utilisez gratuitement, cela peut ch
 
 | Location | Vendor | Specification |
 | --------- | ------------- | ------------------ |
-| Seoul | AWS lightsail | 1 vCPU / 0.5GB RAM |
 | Germany | Hetzner | 2 vCPU / 4GB RAM |
-| Germany | Codext | 4 vCPU / 8GB RAM |
-| Finland (Helsinki) | 0x101 Cyber Security | 4 vCPU / 8GB RAM |
-| USA (Ashburn) | 0x101 Cyber Security | 4 vCPU / 8GB RAM |
 
 ## Dépendances
 
diff --git a/docs/README-GR.md b/docs/README-GR.md
index cf9bb0f9d..c720dd823 100644
--- a/docs/README-GR.md
+++ b/docs/README-GR.md
@@ -34,11 +34,7 @@
 Παρακάτω είναι οι διακομιστές που χρησιμοποιούνται δωρεάν, ενδέχεται να αλλάξουν με την πάροδο του χρόνου. Εάν δεν είστε κοντά σε ένα από αυτούς, το δίκτυό σας ίσως να είναι αργό.
 | Περιοχή | Πάροχος | Προδιαγραφές |
 | --------- | ------------- | ------------------ |
-| Σεούλ | AWS lightsail | 1 vCPU / 0.5GB RAM |
 | Γερμανία | Hetzner | 2 vCPU / 4GB RAM |
-| Γερμανία | Codext | 4 vCPU / 8GB RAM |
-| Φινλανδία (Ελσίνκι) | [Netlock](https://netlockendpoint.com) | 4 vCPU / 8GB RAM |
-| ΗΠΑ (Άσμπερν) | [Netlock](https://netlockendpoint.com) | 4 vCPU / 8GB RAM |
 | Ουκρανία (Κίεβο) | [dc.volia](https://dc.volia.com) | 2 vCPU / 4GB RAM |
 
 ## Dev Container
diff --git a/docs/README-HU.md b/docs/README-HU.md
index e70ae7aad..8965a9b1e 100644
--- a/docs/README-HU.md
+++ b/docs/README-HU.md
@@ -32,11 +32,7 @@ A RustDesk szívesen fogad minden contributiont, támogatást mindenkitől. Lás
 Ezalatt az üzenet alatt találhatóak azok a publikus szerverek, amelyeket ingyen használhatsz. Ezek a szerverek változhatnak a jövőben, illetve a hálózatuk lehet hogy lassú lehet.
 | Hely | Host | Specifikáció |
 | --------- | ------------- | ------------------ |
-| Seoul | AWS lightsail | 1 vCPU / 0.5GB RAM |
 | Germany | Hetzner | 2 vCPU / 4GB RAM |
-| Germany | Codext | 4 vCPU / 8GB RAM |
-| Finland (Helsinki) | [Netlock](https://netlockendpoint.com) | 4 vCPU / 8GB RAM |
-| USA (Ashburn) | [Netlock](https://netlockendpoint.com) | 4 vCPU / 8GB RAM |
 | Ukraine (Kyiv) | [dc.volia](https://dc.volia.com) | 2 vCPU / 4GB RAM |
 
 ## Dependencies
diff --git a/docs/README-ID.md b/docs/README-ID.md
index 2704758be..78aac233a 100644
--- a/docs/README-ID.md
+++ b/docs/README-ID.md
@@ -6,56 +6,64 @@
   <a href="#file-structure">Structure</a> •
   <a href="#snapshot">Snapshot</a><br>
   [<a href="../README.md">English</a>] | [<a href="README-UA.md">Українська</a>] | [<a href="README-CS.md">česky</a>] | [<a href="README-ZH.md">中文</a>] | [<a href="README-HU.md">Magyar</a>] | [<a href="README-ES.md">Español</a>] | [<a href="README-FA.md">فارسی</a>] | [<a href="README-FR.md">Français</a>] | [<a href="README-DE.md">Deutsch</a>] | [<a href="README-PL.md">Polski</a>] | [<a href="README-FI.md">Suomi</a>] | [<a href="README-ML.md">മലയാളം</a>] | [<a href="README-JP.md">日本語</a>] | [<a href="README-NL.md">Nederlands</a>] | [<a href="README-IT.md">Italiano</a>] | [<a href="README-RU.md">Русский</a>] | [<a href="README-PTBR.md">Português (Brasil)</a>] | [<a href="README-EO.md">Esperanto</a>] | [<a href="README-KR.md">한국어</a>] | [<a href="README-AR.md">العربي</a>] | [<a href="README-VN.md">Tiếng Việt</a>] | [<a href="README-GR.md">Ελληνικά</a>]<br>
-  <b>Kami membutuhkan bantuan Anda untuk menerjemahkan README ini dan <a href="https://github.com/rustdesk/rustdesk/tree/master/src/lang">RustDesk UI</a> ke bahasa asli anda</b>
+  <b>Kami membutuhkan bantuanmu untuk menterjemahkan file README dan <a href="https://github.com/rustdesk/rustdesk/tree/master/src/lang">RustDesk UI</a> ke Bahasa Indonesia</b>
 </p>
 
-Birbincang bersama kami: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk)
+Mari mengobrol bersama kami: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk)
 
 [![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/I2I04VU09)
 
-Perangkat lunak desktop jarak jauh lainnya, ditulis dengan Rust. Bekerja begitu saja, tidak memerlukan konfigurasi. Anda memiliki kendali penuh atas data Anda, tanpa khawatir tentang keamanan. Anda dapat menggunakan server rendezvous/relay kami, [konfigurasi server sendiri](https://rustdesk.com/server), or [tulis rendezvous/relay server anda sendiri](https://github.com/rustdesk/rustdesk-server-demo).
+Merupakan perangkat lunak Remote Desktop yang baru, dan dibangun dengan Rust. Bahkan kamu bisa langsung menggunakannya tanpa perlu melakukan konfigurasi tambahan. Serta memiliki kontrol penuh terhadap semua data, tanpa perlu merasa was-was tentang isu keamanan, dan yang lebih menarik adalah memiliki opsi untuk menggunakan server rendezvous/relay milik kami, [konfigurasi server sendiri](https://rustdesk.com/server), atau [tulis rendezvous/relay server anda sendiri](https://github.com/rustdesk/rustdesk-server-demo).
 
-RustDesk menyambut baik kontribusi dari semua orang. Lihat [`docs/CONTRIBUTING.md`](CONTRIBUTING.md) untuk membantu sebelum memulai.
+RustDesk mengajak semua orang untuk ikut berkontribusi. Lihat [`docs/CONTRIBUTING-ID.md`](CONTRIBUTING-ID.md) untuk melihat panduan.
 
-[**BINARY DOWNLOAD**](https://github.com/rustdesk/rustdesk/releases)
+[**UNDUH BINARY**](https://github.com/rustdesk/rustdesk/releases)
 
-## Publik Server Gratis
+## Server Publik Gratis
 
-Di bawah ini adalah server yang bisa Anda gunakan secara gratis, dapat berubah seiring waktu. Jika Anda tidak dekat dengan salah satu dari ini, jaringan Anda mungkin lambat.
-| Lokasi | Vendor | Spesifikasi |
+Di bawah ini merupakan server gratis yang bisa kamu gunakan, seiring dengan waktu mungkin akan terjadi perubahan spesifikasi pada setiap server yang ada. Jika lokasi kamu berada jauh dengan salah satu server yang tersedia, kemungkinan koneksi akan terasa lambat ketika melakukan proses remote.
+| Lokasi | Penyedia | Spesifikasi |
 | --------- | ------------- | ------------------ |
-| Seoul | AWS lightsail | 1 vCPU / 0.5GB RAM |
-| Germany | Hetzner | 2 vCPU / 4GB RAM |
-| Germany | Codext | 4 vCPU / 8GB RAM |
-| Finland (Helsinki) | [Netlock](https://netlockendpoint.com) | 4 vCPU / 8GB RAM |
-| USA (Ashburn) | [Netlock](https://netlockendpoint.com) | 4 vCPU / 8GB RAM |
-| Ukraine (Kyiv) | [dc.volia](https://dc.volia.com) | 2 vCPU / 4GB RAM |
+| Jerman | [Hetzner](https://www.hetzner.com) | 2 vCPU / 4GB RAM |
+| Ukraina (Kyiv) | [dc.volia](https://dc.volia.com) | 2 vCPU / 4GB RAM |
 
-## Dependencies
+## Dev Container
 
-Versi desktop menggunakan [sciter](https://sciter.com/) untuk GUI, silahkan download sendiri sciter dynamic library.
+[![Open in Dev Containers](https://img.shields.io/static/v1?label=Dev%20Container&message=Open&color=blue&logo=visualstudiocode)](https://vscode.dev/redirect?url=vscode://ms-vscode-remote.remote-containers/cloneInVolume?url=https://github.com/rustdesk/rustdesk)
+
+Apabila PC kamu sudah terinstal VS Code dan Docker, kamu bisa mengklik badge yang ada diatas untuk memulainya. Dengan mengklik badge tersebut secara otomatis akan menginstal ekstensi pada VS Code, lakukan kloning (clone) source code kedalam container volume, dan aktifkan dev container untuk menggunakannya.
+
+## Dependensi
+
+Pada versi desktop, antarmuka pengguna (GUI) menggunakan [Sciter](https://sciter.com/) atau flutter
+
+Kamu bisa mengunduh Sciter dynamic library disini.
 
 [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)
 
-## Langkah untuk RAW Build
+## Langkah awal untuk memulai
 
-- Siapkan env pengembangan Rust dan C++ build env
+- Siapkan env development Rust dan env build C++
 
-- Install [vcpkg](https://github.com/microsoft/vcpkg), dan arahkan `VCPKG_ROOT` env variable dengan benar
+- Install [vcpkg](https://github.com/microsoft/vcpkg), dan atur variabel env `VCPKG_ROOT` dengan benar
 
   - Windows: vcpkg install libvpx:x64-windows-static libyuv:x64-windows-static opus:x64-windows-static aom:x64-windows-static
   - Linux/MacOS: vcpkg install libvpx libyuv opus aom
 
 - jalankan `cargo run`
 
-## Bagaimana Build di Linux
+## [Build](https://rustdesk.com/docs/en/dev/build/)
+
+## Cara Build di Linux
 
 ### Ubuntu 18 (Debian 10)
 
 ```sh
-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
+sudo apt install -y zip 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 make \
+        libclang-dev ninja-build libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev
 ```
 
 ### Fedora 28 (CentOS 8)
@@ -82,7 +90,7 @@ export VCPKG_ROOT=$HOME/vcpkg
 vcpkg/vcpkg install libvpx libyuv opus aom
 ```
 
-### Perbaiki libvpx (Untuk Fedora)
+### Mengatasi masalah libvpx (Untuk Fedora)
 
 ```sh
 cd vcpkg/buildtrees/libvpx/src
@@ -108,13 +116,40 @@ mv libsciter-gtk.so target/debug
 VCPKG_ROOT=$HOME/vcpkg cargo run
 ```
 
-### Ubah Wayland menjadi X11 (Xorg)
+### Mengubah Wayland ke X11 (Xorg)
 
-RustDesk tidak mendukung Wayland. Cek [ini](https://docs.fedoraproject.org/en-US/quick-docs/configuring-xorg-as-default-gnome-session/) untuk mengonfigurasi Xorg sebagai sesi GNOME default.
+RustDesk tidak mendukung Wayland. Cek [ini](https://docs.fedoraproject.org/en-US/quick-docs/configuring-xorg-as-default-gnome-session/) untuk mengonfigurasi Xorg sebagai sesi standar di GNOME.
 
-## Bagaimana build dengan Docker
+## Kompatibilitas dengan Wayland
 
-Mulailah dengan mengkloning repositori dan build dengan docker container:
+Sepertinya Wayland tidak memiliki API untuk mengirimkan ketukan tombol ke jendela lain. Maka dari itu, RustDesk menggunakan API dari level yang lebih rendah, lebih tepatnya perangkat `/dev/uinput` (linux kernel level)
+
+Saat Wayland menjadi sisi yang dikendalikan atau sisi yang sedang diremote, kamu harus memulai dengan cara ini
+
+```bash
+# Start uinput service
+$ sudo rustdesk --service
+$ rustdesk
+```
+
+**Harap Diperhatikan**: Saat Perekaman layar menggunakan Wayland antarmuka (UI) yang ditampilkan akan berbeda. Untuk saat ini RustDesk hanya mendukung org.freedesktop.portal.ScreenCast.
+
+```bash
+$ dbus-send --session --print-reply       \
+  --dest=org.freedesktop.portal.Desktop \
+  /org/freedesktop/portal/desktop       \
+  org.freedesktop.DBus.Properties.Get   \
+  string:org.freedesktop.portal.ScreenCast string:version
+# Not support
+Error org.freedesktop.DBus.Error.InvalidArgs: No such interface “org.freedesktop.portal.ScreenCast”
+# Support
+method return time=1662544486.931020 sender=:1.54 -> destination=:1.139 serial=257 reply_serial=2
+   variant       uint32 4
+```
+
+## Cara Build dengan Docker
+
+Mulailah dengan melakukan kloning (clone) repositori dan build dengan docker container:
 
 ```sh
 git clone https://github.com/rustdesk/rustdesk
@@ -122,25 +157,25 @@ cd rustdesk
 docker build -t "rustdesk-builder" .
 ```
 
-Kemudian, setiap kali Anda perlu build aplikasi, jalankan perintah berikut:
+Selanjutnya, setiap kali ketika kamu akan melakukan build aplikasi, jalankan perintah berikut:
 
 ```sh
 docker run --rm -it -v $PWD:/home/user/rustdesk -v rustdesk-git-cache:/home/user/.cargo/git -v rustdesk-registry-cache:/home/user/.cargo/registry -e PUID="$(id -u)" -e PGID="$(id -g)" rustdesk-builder
 ```
 
-Perhatikan bahwa build pertama mungkin memerlukan waktu lebih lama sebelum dependensi di-cache, build berikutnya akan lebih cepat. Selain itu, jika Anda perlu menentukan argumen yang berbeda untuk perintah build, Anda dapat melakukannya di akhir perintah di posisi `<OPTIONAL-ARGS>`. Misalnya, jika Anda ingin membangun versi rilis yang dioptimalkan, Anda akan menjalankan perintah di atas diikuti oleh `--release`. Hasil eksekusi akan tersedia pada target folder di sistem anda, dan dapat dijalankan dengan:
+Perlu diingat bahwa pada saat build pertama kali, mungkin memerlukan waktu lebih lama sebelum dependensi di-cache, build berikutnya akan lebih cepat. Selain itu, jika perlu menentukan argumen yang berbeda untuk perintah build, kamu dapat melakukannya di akhir perintah di posisi `<OPTIONAL-ARGS>`. Misalnya, jika ingin membangun versi rilis yang dioptimalkan, jalankan perintah di atas dan tambahkan `--release`. Hasil eksekusi perintah tersebut akan tersimpan pada target folder di sistem kamu, dan dapat dijalankan dengan:
 
 ```sh
 target/debug/rustdesk
 ```
 
-Atau, jika Anda menjalankan rilis yang dapat dieksekusi:
+Atau, jika kamu menjalankan rilis yang dapat dieksekusi:
 
 ```sh
 target/release/rustdesk
 ```
 
-Harap pastikan bahwa Anda menjalankan perintah ini dari root repositori RustDesk, jika tidak, aplikasi mungkin tidak dapat menemukan sumber daya yang diperlukan. Perhatikan juga perintah cargo seperti `install` atau `run` saat ini tidak didukung melalui metode ini karena mereka akan menginstal atau menjalankan program di dalam container bukan pada host.
+Harap pastikan bahwa kamu menjalankan perintah ini dari repositori root RustDesk, jika tidak demikian, aplikasi mungkin tidak dapat menemukan sumber yang diperlukan. Dan juga, perintah cargo seperti `install` atau `run` saat ini tidak didukung melalui metode ini karena, proses menginstal atau menjalankan program terjadi di dalam container bukan pada host.
 
 ## Struktur File
 
diff --git a/docs/README-IT.md b/docs/README-IT.md
index be4e4d54a..c1c46bfed 100644
--- a/docs/README-IT.md
+++ b/docs/README-IT.md
@@ -1,39 +1,40 @@
 <p align="center">
-  <img src="../res/logo-header.svg" alt="RustDesk - Your remote desktop"><br>
-  <a href="#server-pubblici-gratuiti">Servers</a> •
+  <img src="../res/logo-header.svg" alt="RustDesk - il tuo desktop remoto"><br>
+  <a href="#server-pubblici-gratuiti">Server</a> •
   <a href="#passaggi-per-la-compilazione">Compilazione</a> •
   <a href="#come-compilare-con-docker">Docker</a> •
   <a href="#struttura-dei-file">Struttura</a> •
-  <a href="#screenshots">Screenshots</a><br>
+  <a href="#screenshots">Schermate</a><br>
   [<a href="../README.md">English</a>] | [<a href="README-UA.md">Українська</a>] | [<a href="README-CS.md">česky</a>] | [<a href="README-ZH.md">中文</a>] | [<a href="README-HU.md">Magyar</a>] | [<a href="README-ES.md">Español</a>] | [<a href="README-FA.md">فارسی</a>] | [<a href="README-FR.md">Français</a>] | [<a href="README-DE.md">Deutsch</a>] | [<a href="README-PL.md">Polski</a>] | [<a href="README-ID.md">Indonesian</a>] | [<a href="README-FI.md">Suomi</a>] | [<a href="README-ML.md">മലയാളം</a>] | [<a href="README-JP.md">日本語</a>] | [<a href="README-NL.md">Nederlands</a>] | [<a href="README-RU.md">Русский</a>] | [<a href="README-PTBR.md">Português (Brasil)</a>] | [<a href="README-EO.md">Esperanto</a>] | [<a href="README-KR.md">한국어</a>] | [<a href="README-AR.md">العربي</a>] | [<a href="README-VN.md">Tiếng Việt</a>] | [<a href="README-GR.md">Ελληνικά</a>]<br>
-  <b>Abbiamo bisogno del tuo aiuto per tradurre questo README e la <a href="https://github.com/rustdesk/rustdesk/tree/master/src/lang">RustDesk UI</a> nella tua lingua nativa</b>
+  <b>Abbiamo bisogno del tuo aiuto per tradurre questo file README e la <a href="https://github.com/rustdesk/rustdesk/tree/master/src/lang">UI RustDesk</a> nella tua lingua nativa</b>
 </p>
 
-Chatta con noi: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk)
+Chatta con noi su: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk)
 
 [![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/I2I04VU09)
 
-Ancora un altro software per il controllo remoto del desktop, scritto in Rust. Funziona immediatamente, nessuna configurazione richiesta. Hai il pieno controllo dei tuoi dati, senza preoccupazioni per la sicurezza. Puoi utilizzare il nostro server rendezvous/relay, [configurare il tuo](https://rustdesk.com/server) o [scrivere il tuo rendezvous/relay server](https://github.com/rustdesk/rustdesk-server-demo).
+Ancora un altro software per il controllo remoto del desktop, scritto in Rust. 
+Funziona immediatamente, nessuna configurazione richiesta. Hai il pieno controllo dei tuoi dati, senza preoccupazioni per la sicurezza. 
+Puoi usare il nostro server rendezvous/relay, [configurare il tuo server](https://rustdesk.com/server) o [realizzare il tuo server rendezvous/relay](https://github.com/rustdesk/rustdesk-server-demo).
 
-RustDesk accoglie il contributo di tutti. Per ulteriori informazioni su come inizare a contribuire, vedere [`docs/CONTRIBUTING.md`](CONTRIBUTING.md).
+RustDesk accoglie il contributo di tutti. 
+Per ulteriori informazioni su come iniziare a contribuire, vedi [`docs/CONTRIBUTING-IT.md`](CONTRIBUTING.md).
 
-[**BINARY DOWNLOAD**](https://github.com/rustdesk/rustdesk/releases)
+[**DOWNLOAD PROGRAMMA**](https://github.com/rustdesk/rustdesk/releases)
 
 ## Server pubblici gratuiti
 
-Qui sotto trovate i server che possono essere usati gratuitamente, la lista potrebbe cambiare nel tempo. Se non si è vicini a uno di questi server, la vostra connessione potrebbe essere lenta.
-| Posizione | Vendor | Specifiche |
+Qui sotto trovi i server che possono essere usati gratuitamente, la lista potrebbe cambiare nel tempo. 
+Se non sei vicino a uno di questi server, la connessione potrebbe essere lenta.
+
+| Posizione | Venditore | Specifiche |
 | --------- | ------------- | ------------------ |
-| Seoul | AWS lightsail | 1 vCPU / 0.5GB RAM |
-| Germany | Hetzner | 2 vCPU / 4GB RAM |
-| Germany | Codext | 4 vCPU / 8GB RAM |
-| Finland (Helsinki) | [Netlock](https://netlockendpoint.com) | 4 vCPU / 8GB RAM |
-| USA (Ashburn) | [Netlock](https://netlockendpoint.com) | 4 vCPU / 8GB RAM |
-| Ukraine (Kyiv) | [dc.volia](https://dc.volia.com) | 2 vCPU / 4GB RAM |
+| Germania | Hetzner | 2 vCPU / 4GB RAM |
+| Ucraina (Kyev) | [dc.volia](https://dc.volia.com) | 2 vCPU / 4GB RAM |
 
 ## Dipendenze
 
-La versione Desktop utilizza [sciter](https://sciter.com/) per la GUI, per favore scarica sciter dynamic library.
+La versione Desktop usa per la GUI [sciter](https://sciter.com/), per favore scarica la libreria dinamica sciter.
 
 [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) |
@@ -50,7 +51,7 @@ La versione Desktop utilizza [sciter](https://sciter.com/) per la GUI, per favor
 
 - Esegui `cargo run`
 
-## Come compilare su Linux
+## Come compilare in Linux
 
 ### Ubuntu 18 (Debian 10)
 
@@ -70,7 +71,7 @@ sudo yum -y install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-
 sudo pacman -Syu --needed unzip git cmake gcc curl wget yasm nasm zip make pkg-config clang gtk3 xdotool libxcb libxfixes alsa-lib pipewire
 ```
 
-### Installare vcpkg
+### Installa vcpkg
 
 ```sh
 git clone https://github.com/microsoft/vcpkg
@@ -82,7 +83,7 @@ export VCPKG_ROOT=$HOME/vcpkg
 vcpkg/vcpkg install libvpx libyuv opus aom
 ```
 
-### Fix libvpx (Per Fedora)
+### Correzione libvpx (per Fedora)
 
 ```sh
 cd vcpkg/buildtrees/libvpx/src
@@ -108,13 +109,14 @@ mv libsciter-gtk.so target/debug
 VCPKG_ROOT=$HOME/vcpkg cargo run
 ```
 
-### Cambiare Wayland a X11 (Xorg)
+### Cambiare Wayland in X11 (Xorg)
 
-RustDesk non supporta Wayland. Controlla [questo](https://docs.fedoraproject.org/en-US/quick-docs/configuring-xorg-as-default-gnome-session/) per configurare Xorg come sessione di default di GNOME.
+RustDesk non supporta Wayland. 
+Controlla [qui](https://docs.fedoraproject.org/en-US/quick-docs/configuring-xorg-as-default-gnome-session/) per configurare Xorg come sessione predefinita di GNOME.
 
 ## Come compilare con Docker
 
-Cominciare clonando il repository e compilare i container docker:
+Clona il repository e compila i container docker:
 
 ```sh
 git clone https://github.com/rustdesk/rustdesk
@@ -122,38 +124,42 @@ cd rustdesk
 docker build -t "rustdesk-builder" .
 ```
 
-Quindi, ogni volta che devi compilare l'applicazione, esegui il comando seguente:
+Quindi, ogni volta che devi compilare l'applicazione, esegui il seguente comando:
 
 ```sh
 docker run --rm -it -v $PWD:/home/user/rustdesk -v rustdesk-git-cache:/home/user/.cargo/git -v rustdesk-registry-cache:/home/user/.cargo/registry -e PUID="$(id -u)" -e PGID="$(id -g)" rustdesk-builder
 ```
 
-Tieni presente che la prima build potrebbe richiedere più tempo prima che le dipendenze vengano memorizzate nella cache, le build successive saranno più veloci. Inoltre, se hai bisogno di specificare argomenti diversi per il comando build, puoi farlo alla fine del comando nella posizione `<OPTIONAL-ARGS>`. Ad esempio, se si desidera creare una versione di rilascio ottimizzata, eseguire il comando sopra seguito da `--release`. L'eseguibile generato sarà creato nella cartella di destinazione del proprio sistema e può essere eseguito con:
+Tieni presente che la prima build potrebbe richiedere più tempo prima che le dipendenze vengano memorizzate nella cache, le build successive saranno più veloci. 
+Inoltre, se hai bisogno di specificare argomenti diversi per il comando build, puoi farlo alla fine del comando nella posizione `<OPTIONAL-ARGS>`. 
+Ad esempio, se vuoi creare una versione di rilascio ottimizzata, esegui il comando precedentemente indicato seguito da `--release`. 
+L'eseguibile generato sarà creato nella cartella destinazione del sistema e può essere eseguito con:
 
 ```sh
 target/debug/rustdesk
 ```
 
-Oppure, se si sta eseguendo un eseguibile di rilascio:
+Oppure, se stai avviando un eseguibile di rilascio:
 
 ```sh
 target/release/rustdesk
 ```
 
-Assicurati di eseguire questi comandi dalla radice del repository RustDesk, altrimenti l'applicazione potrebbe non essere in grado di trovare le risorse richieste. Notare inoltre che altri sottocomandi cargo come `install` o `run` non sono attualmente supportati tramite questo metodo poiché installerebbero o eseguirebbero il programma all'interno del container anziché nell'host.
+Assicurati di eseguire questi comandi dalla radice del repository RustDesk, altrimenti l'applicazione potrebbe non essere in grado di trovare le risorse richieste. 
+Nota inoltre che altri sottocomandi cargo come `install` o `run` non sono attualmente supportati tramite questo metodo poiché installerebbero o eseguirebbero il programma all'interno del container anziché nell'host.
 
 ## Struttura dei file
 
-- **[libs/hbb_common](https://github.com/rustdesk/rustdesk/tree/master/libs/hbb_common)**: video codec, config, tcp/udp wrapper, protobuf, fs funzioni per il trasferimento file, e altre funzioni utili.
+- **[libs/hbb_common](https://github.com/rustdesk/rustdesk/tree/master/libs/hbb_common)**: codec video, config, wrapper tcp/udp, protobuf, funzioni per il trasferimento file, e altre funzioni utili.
 - **[libs/scrap](https://github.com/rustdesk/rustdesk/tree/master/libs/scrap)**: cattura dello schermo
 - **[libs/enigo](https://github.com/rustdesk/rustdesk/tree/master/libs/enigo)**: controllo tastiera/mouse specifico della piattaforma
 - **[src/ui](https://github.com/rustdesk/rustdesk/tree/master/src/ui)**: GUI
 - **[src/server](https://github.com/rustdesk/rustdesk/tree/master/src/server)**: servizi audio/appunti/input/video e connessioni di rete
-- **[src/client.rs](https://github.com/rustdesk/rustdesk/tree/master/src/client.rs)**: avviare una connessione peer
+- **[src/client.rs](https://github.com/rustdesk/rustdesk/tree/master/src/client.rs)**: avvio di una connessione peer
 - **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: Comunica con [rustdesk-server](https://github.com/rustdesk/rustdesk-server), attende la connessione remota diretta (TCP hole punching) oppure indiretta (relayed)
 - **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: codice specifico della piattaforma
 
-## Screenshots
+## Schermate
 
 ![image](https://user-images.githubusercontent.com/71636191/113112362-ae4deb80-923b-11eb-957d-ff88daad4f06.png)
 
diff --git a/docs/README-JP.md b/docs/README-JP.md
index 709d41547..44f811eec 100644
--- a/docs/README-JP.md
+++ b/docs/README-JP.md
@@ -29,11 +29,7 @@ RustDeskは誰からの貢献も歓迎します。 貢献するには [`docs/CON
 下記のサーバーは、無料で使用できますが、後々変更されることがあります。これらのサーバーから遠い場合、接続が遅い可能性があります。
 | Location | Vendor | Specification |
 | --------- | ------------- | ------------------ |
-| Seoul | AWS lightsail | 1 vCPU / 0.5GB RAM |
 | Germany | Hetzner | 2 vCPU / 4GB RAM |
-| Germany | Codext | 4 vCPU / 8GB RAM |
-| Finland (Helsinki) | 0x101 Cyber Security | 4 vCPU / 8GB RAM |
-| USA (Ashburn) | 0x101 Cyber Security | 4 vCPU / 8GB RAM |
 
 ## 依存関係
 
diff --git a/docs/README-KR.md b/docs/README-KR.md
index 7c6326cc8..dacb092e7 100644
--- a/docs/README-KR.md
+++ b/docs/README-KR.md
@@ -29,11 +29,7 @@ RustDesk는 모든 기여를 환영합니다. 기여하고자 한다면 [`docs/C
 표에 있는 서버는 무료로 사용할 수 있지만 추후 변경될 수도 있습니다. 이 서버에서 멀다면, 네트워크가 느려질 가능성도 있습니다.
 | Location | Vendor | Specification |
 | --------- | ------------- | ------------------ |
-| Seoul | AWS lightsail | 1 vCPU / 0.5GB RAM |
 | Germany | Hetzner | 2 vCPU / 4GB RAM |
-| Germany | Codext | 4 vCPU / 8GB RAM |
-| Finland (Helsinki) | 0x101 Cyber Security | 4 vCPU / 8GB RAM |
-| USA (Ashburn) | 0x101 Cyber Security | 4 vCPU / 8GB RAM |
 
 ## 의존관계
 
diff --git a/docs/README-ML.md b/docs/README-ML.md
index 9f3ed88a3..a73fd7815 100644
--- a/docs/README-ML.md
+++ b/docs/README-ML.md
@@ -24,11 +24,7 @@
 നിങ്ങൾ സൗജന്യമായി ഉപയോഗിക്കുന്ന സെർവറുകൾ ചുവടെയുണ്ട്, അത് സമയത്തിനനുസരിച്ച് മാറിയേക്കാം. നിങ്ങൾ ഇവയിലൊന്നിനോട് അടുത്തല്ലെങ്കിൽ, നിങ്ങളുടെ നെറ്റ്‌വർക്ക് സ്ലോ ആയേക്കാം.
 | സ്ഥാനം | കച്ചവടക്കാരൻ | വിവരണം |
 | --------- | ------------- | ------------------ |
-| Seoul | AWS lightsail | 1 vCPU / 0.5GB RAM |
 | Germany | Hetzner | 2 vCPU / 4GB RAM |
-| Germany | Codext | 4 vCPU / 8GB RAM |
-| Finland (Helsinki) | 0x101 Cyber Security | 4 vCPU / 8GB RAM |
-| USA (Ashburn) | 0x101 Cyber Security | 4 vCPU / 8GB RAM |
 
 ## ഡിപെൻഡൻസികൾ
 
diff --git a/docs/README-NL.md b/docs/README-NL.md
index 2214adeac..bec83a285 100644
--- a/docs/README-NL.md
+++ b/docs/README-NL.md
@@ -32,11 +32,7 @@ RustDesk verwelkomt bijdragen van iedereen. Zie [`docs/CONTRIBUTING.md`](CONTRIB
 Hieronder staan de servers die u gratis gebruikt, ze kunnen in de loop van de tijd veranderen. Als u niet in de buurt van een van deze servers bevindt, kan uw vervinding langzamer zijn.
 | Locatie | Aanbieder | Specificaties |
 | --------- | ------------- | ------------------ |
-| Seoul | AWS lightsail | 1 vCPU / 0.5GB RAM |
 | Duitsland | Hetzner | 2 vCPU / 4GB RAM |
-| Duitsland | Codext | 4 vCPU / 8GB RAM |
-| Finland (Helsinki) | [Netlock](https://netlockendpoint.com) | 4 vCPU / 8GB RAM |
-| USA (Ashburn) | [Netlock](https://netlockendpoint.com) | 4 vCPU / 8GB RAM |
 | Oekraine (Kyiv) | [dc.volia](https://dc.volia.com) | 2 vCPU / 4GB RAM |
 
 ## Dev Container
diff --git a/docs/README-PL.md b/docs/README-PL.md
index f44542581..ba27af04d 100644
--- a/docs/README-PL.md
+++ b/docs/README-PL.md
@@ -34,11 +34,7 @@ RustDesk zaprasza do współpracy każdego. Zobacz [`docs/CONTRIBUTING-PL.md`](C
 Poniżej znajdują się serwery, z których można korzystać za darmo, może się to zmienić z upływem czasu. Jeśli nie znajdujesz się w pobliżu jednego z nich, Twoja prędkość połączenia może być niska.
 | Lokalizacja | Dostawca | Specyfikacja |
 | --------- | ------------- | ------------------ |
-| Korea Płd. (Seul) | AWS lightsail | 1 vCPU / 0.5GB RAM |
 | Niemcy | Hetzner | 2 vCPU / 4GB RAM |
-| Niemcy | Codext | 4 vCPU / 8GB RAM |
-| Finlandia (Helsinki) | [Netlock](https://netlockendpoint.com) | 4 vCPU / 8GB RAM |
-| USA (Ashburn) | [Netlock](https://netlockendpoint.com) | 4 vCPU / 8GB RAM |
 | Ukraina (Kijów) | [dc.volia](https://dc.volia.com) | 2 vCPU / 4GB RAM |
 
 ## Konterner Programisty (Dev Container)
diff --git a/docs/README-PTBR.md b/docs/README-PTBR.md
index 7e8a2d2e7..6e6f01fce 100644
--- a/docs/README-PTBR.md
+++ b/docs/README-PTBR.md
@@ -25,11 +25,7 @@ Abaixo estão os servidores que você está utilizando de graça, ele pode mudar
 
 | Localização | Fornecedor    | Especificações     |
 | ----------- | ------------- | ------------------ |
-| Seoul | AWS lightsail | 1 vCPU / 0.5GB RAM |
 | Germany | Hetzner | 2 vCPU / 4GB RAM |
-| Germany | Codext | 4 vCPU / 8GB RAM |
-| Finland (Helsinki) | 0x101 Cyber Security | 4 vCPU / 8GB RAM |
-| USA (Ashburn) | 0x101 Cyber Security | 4 vCPU / 8GB RAM |
 
 ## Dependências
 
diff --git a/docs/README-RU.md b/docs/README-RU.md
index 282617ace..01710f084 100644
--- a/docs/README-RU.md
+++ b/docs/README-RU.md
@@ -33,13 +33,7 @@ RustDesk приветствует вклад каждого. Ознакомьт
 Ниже приведены бесплатные публичные сервера, используемые по умолчанию. Имейте ввиду, они могут меняться со временем. Также стоит отметить, что скорость работы сети зависит от вашего местоположения и расстояния до серверов. Подключение происходит к ближайшему доступному.
 | Расположение | Поставщик | Технические характеристики |
 | --------- | ------------- | ------------------ |
-| Сеул | AWS lightsail | 1 vCPU / 0.5GB RAM |
-| Сингапур | Vultr | 1 vCPU / 1GB RAM |
-| Даллас | Vultr | 1 vCPU / 1GB RAM |
 | Германия | Hetzner | 2 vCPU / 4GB RAM |
-| Германия | Codext | 4 vCPU / 8GB RAM |
-| Финляндия (Хельсинки) | 0x101 Cyber Security | 4 vCPU / 8GB RAM |
-| США (Эшберн) | 0x101 Cyber Security | 4 vCPU / 8GB RAM |
 
 ## Зависимости
 
diff --git a/docs/README-TR.md b/docs/README-TR.md
new file mode 100644
index 000000000..590ead0df
--- /dev/null
+++ b/docs/README-TR.md
@@ -0,0 +1,223 @@
+
+<p align="center">
+  <img src="../res/logo-header.svg" alt="RustDesk - Uzak masaüstü uygulamanız"><br>
+  <a href="#free-public-servers">Sunucular</a> •
+  <a href="#raw-steps-to-build">Derleme</a> •
+  <a href="#how-to-build-with-docker">Docker ile Derleme</a> •
+  <a href="#file-structure">Dosya Yapısı</a> •
+  <a href="#snapshot">Ekran Görüntüleri</a><br>
+  [<a href="docs/README-UA.md">Українська</a>] | [<a href="docs/README-CS.md">česky</a>] | [<a href="docs/README-ZH.md">中文</a>] | [<a href="docs/README-HU.md">Magyar</a>] | [<a href="docs/README-ES.md">Español</a>] | [<a href="docs/README-FA.md">فارسی</a>] | [<a href="docs/README-FR.md">Français</a>] | [<a href="docs/README-DE.md">Deutsch</a>] | [<a href="docs/README-PL.md">Polski</a>] | [<a href="docs/README-ID.md">Indonesian</a>] | [<a href="docs/README-FI.md">Suomi</a>] | [<a href="docs/README-ML.md">മലയാളം</a>] | [<a href="docs/README-JP.md">日本語</a>] | [<a href="docs/README-NL.md">Nederlands</a>] | [<a href="docs/README-IT.md">Italiano</a>] | [<a href="docs/README-RU.md">Русский</a>] | [<a href="docs/README-PTBR.md">Português (Brasil)</a>] | [<a href="docs/README-EO.md">Esperanto</a>] | [<a href="docs/README-KR.md">한국어</a>] | [<a href="docs/README-AR.md">العربي</a>] | [<a href="docs/README-VN.md">Tiếng Việt</a>] | [<a href="docs/README-DA.md">Dansk</a>] | [<a href="docs/README-GR.md">Ελληνικά</a>]<br>
+  <b>README, <a href="https://github.com/rustdesk/rustdesk/tree/master/src/lang">RustDesk UI</a> ve <a href="https://github.com/rustdesk/doc.rustdesk.com">RustDesk Belge</a>'sini ana dilinize çevirmemiz için yardımınıza ihtiyacımız var</b>
+</p>
+
+Bizimle sohbet edin: [Discord](https://discord.gg/nDceKgxnkV) | [Twitter](https://twitter.com/rustdesk) | [Reddit](https://www.reddit.com/r/rustdesk)
+
+[![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/I2I04VU09)
+
+Başka bir uzak masaüstü yazılımı daha, Rust dilinde yazılmış. Hemen kullanıma hazır, hiçbir yapılandırma gerektirmez. Verilerinizin tam kontrolünü elinizde tutarsınız ve güvenlikle ilgili endişeleriniz olmaz. Kendi buluş/iletme sunucumuzu kullanabilirsiniz, [kendi sunucunuzu kurabilirsiniz](https://rustdesk.com/server) veya [kendi buluş/iletme sunucunuzu yazabilirsiniz](https://github.com/rustdesk/rustdesk-server-demo).
+
+![image](https://user-images.githubusercontent.com/71636191/171661982-430285f0-2e12-4b1d-9957-4a58e375304d.png)
+
+RustDesk, herkesten katkıyı kabul eder. Başlamak için [CONTRIBUTING.md](docs/CONTRIBUTING-TR.md) belgesine göz atın.
+
+[**SSS**](https://github.com/rustdesk/rustdesk/wiki/FAQ)
+
+[**BİNARİ İNDİR**](https://github.com/rustdesk/rustdesk/releases)
+
+[**NİGHTLY DERLEME**](https://github.com/rustdesk/rustdesk/releases/tag/nightly)
+
+[<img src="https://fdroid.gitlab.io/artwork/badge/get-it-on.png"
+    alt="F-Droid'de Alın"
+    height="80">](https://f-droid.org/en/packages/com.carriez.flutter_hbb)
+
+## Ücretsiz Genel Sunucular
+
+Aşağıda ücretsiz olarak kullandığınız sunucular listelenmiştir, zaman içinde değişebilirler. Eğer bunlardan birine yakın değilseniz, ağınız yavaş olabilir.
+| Konum | Sağlayıcı | Özellikler |
+| --------- | ------------- | ------------------ |
+| Almanya | [Hetzner](https://www.hetzner.com) | 2 vCPU / 4 GB RAM |
+| Almanya | [Codext](https://codext.de) | 4 vCPU / 8 GB RAM |
+| Ukrayna (Kiev) | [dc.volia](https://dc.volia.com) | 2 vCPU / 4 GB RAM |
+
+## Geliştirici Konteyneri
+
+[![Open in Dev Containers](https://img.shields.io/static/v1?label=Dev%20Container&message=Open&color=blue&logo=visualstudiocode)](https://vscode.dev/redirect?url=vscode://ms-vscode-remote.remote-containers/cloneInVolume?url=https://github.com/rustdesk/rustdesk)
+
+Eğer zaten VS Code ve Docker kurulu ise yukarıdaki rozete tıklayarak başlayabilirsiniz. Tıklamak, VS Code'un gerektiğinde Dev Konteyner eklentisini otomatik olarak yüklemesine, kaynak kodunu bir konteyner hacmine klonlamasına ve kullanım için bir geliştirici konteyneri başlatmasına neden olur.
+
+Daha fazla bilgi için [DEVCONTAINER.md](docs/DEVCONTAINER-TR.md) belgesine bakabilirsiniz.
+
+## Bağımlılıklar
+
+Masaüstü sürümleri GUI için
+
+ [Sciter](https://sciter.com/) veya Flutter kullanır, bu kılavuz sadece Sciter içindir.
+
+Lütfen Sciter dinamik kütüphanesini kendiniz indirin.
+
+[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)
+
+## Temel Derleme Adımları
+
+- Rust geliştirme ortamınızı ve C++ derleme ortamınızı hazırlayın.
+
+- [vcpkg](https://github.com/microsoft/vcpkg) yükleyin ve `VCPKG_ROOT` çevresel değişkenini doğru bir şekilde ayarlayın.
+
+  - Windows: vcpkg install libvpx:x64-windows-static libyuv:x64-windows-static opus:x64-windows-static aom:x64-windows-static
+  - Linux/macOS: vcpkg install libvpx libyuv opus aom
+
+- `cargo run` komutunu çalıştırın.
+
+## [Derleme](https://rustdesk.com/docs/en/dev/build/)
+
+## Linux Üzerinde Derleme Nasıl Yapılır
+
+### Ubuntu 18 (Debian 10)
+
+```sh
+sudo apt install -y zip 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 make \
+        libclang-dev ninja-build libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev
+```
+
+### openSUSE Tumbleweed 
+
+```sh
+sudo zypper install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libXfixes-devel cmake alsa-lib-devel gstreamer-devel gstreamer-plugins-base-devel xdotool-devel
+```
+
+### Fedora 28 (CentOS 8)
+
+```sh
+sudo yum -y install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libxdo-devel libXfixes-devel pulseaudio-libs-devel cmake alsa-lib-devel
+```
+
+### Arch (Manjaro)
+
+```sh
+sudo pacman -Syu --needed unzip git cmake gcc curl wget yasm nasm zip make pkg-config clang gtk3 xdotool libxcb libxfixes alsa-lib pipewire
+```
+
+### vcpkg'yi Yükleyin
+
+```sh
+git clone https://github.com/microsoft/vcpkg
+cd vcpkg
+git checkout 2023.04.15
+cd ..
+vcpkg/bootstrap-vcpkg.sh
+export VCPKG_ROOT=$HOME/vcpkg
+vcpkg/vcpkg install libvpx libyuv opus aom
+```
+
+### libvpx'i Düzeltin (Fedora için)
+
+```sh
+cd vcpkg/buildtrees/libvpx/src
+cd *
+./configure
+sed -i 's/CFLAGS+=-I/CFLAGS+=-fPIC -I/g' Makefile
+sed -i 's/CXXFLAGS+=-I/CXXFLAGS+=-fPIC -I/g' Makefile
+make
+cp libvpx.a $HOME/vcpkg/installed/x64-linux/lib/
+cd
+```
+
+### Derleme
+
+```sh
+curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
+source $HOME/.cargo/env
+git clone https://github.com/rustdesk/rustdesk
+cd rustdesk
+mkdir -p target/debug
+wget https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.lnx/x64/libsciter-gtk.so
+mv libsciter-gtk.so target/debug
+VCPKG_ROOT=$HOME/vcpkg cargo run
+```
+
+### Wayland'ı X11 (Xorg) Olarak Değiştirme
+
+RustDesk, Wayland'ı desteklemez. Xorg'u GNOME oturumu olarak varsayılan olarak ayarlamak için [burayı](https://docs.fedoraproject.org/en-US/quick-docs/configuring-xorg-as-default-gnome-session/) kontrol edin.
+
+## Wayland Desteği
+
+Wayland'ın diğer pencerelere tuş vuruşu göndermek için herhangi bir API sağlamadığı görünmektedir. Bu nedenle, RustDesk daha düşük bir seviyeden, yani Linux çekirdek seviyesindeki `/dev/uinput` cihazının API'sini kullanır.
+
+Wayland tarafı kontrol edildiğinde, aşağıdaki şekilde başlatmanız gerekir:
+```bash
+# uinput servisini başlatın
+$ sudo rustdesk --service
+$ rustdesk
+```
+**Uyarı**: Wayland ekran kaydı farklı arayüzler kullanır. RustDesk şu anda yalnızca org.freedesktop.portal.ScreenCast'ı destekler.
+```bash
+$ dbus-send --session --print-reply       \
+  --dest=org.freedesktop.portal.Desktop \
+  /org/freedesktop/portal/desktop       \
+  org.freedesktop.DBus.Properties.Get   \
+  string:org.freedesktop.portal.ScreenCast string:version
+# Desteklenmez
+Error org.freedesktop.DBus.Error.InvalidArgs: No such interface “org.freedesktop.portal.ScreenCast”
+# Desteklenir
+method return time=1662544486.931020 sender=:1.54 -> destination=:1.139 serial=257 reply_serial=2
+   variant       uint32 4
+```
+
+## Docker ile Derleme Nasıl Yapılır
+
+Öncelikle deposunu klonlayın ve Docker konteynerini oluşturun:
+
+```sh
+git clone https://github.com/rustdesk/rustdesk
+cd rustdesk
+docker build -t "rustdesk-builder" .
+```
+
+Ardından, uygulamayı derlemek için her seferinde aşağıdaki komutu çalıştırın:
+
+```sh
+docker run --rm -it -v $PWD:/home/user/rustdesk -v rustdesk-git-cache:/home/user/.cargo/git -v rustdesk-registry-cache:/home/user/.cargo/registry -e PUID="$(id -u)" -e PGID="$(id -g)" rustdesk-builder
+```
+
+İlk derleme, bağımlılıklar önbelleğe alınmadan önce daha uzun sürebilir, sonraki derlemeler daha hızlı olacaktır. Ayrıca, derleme komutuna isteğe bağlı argümanlar belirtmeniz gerekiyorsa, bunu
+
+ komutun sonunda `<İSTEĞE BAĞLI-ARGÜMANLAR>` pozisyonunda yapabilirsiniz. Örneğin, optimize edilmiş bir sürümü derlemek isterseniz, yukarıdaki komutu çalıştırdıktan sonra `--release` ekleyebilirsiniz. Oluşan yürütülebilir dosya sisteminizdeki hedef klasöründe bulunacak ve şu komutla çalıştırılabilir:
+
+```sh
+target/debug/rustdesk
+```
+
+Veya, yayın yürütülebilir dosyası çalıştırılıyorsa:
+
+```sh
+target/release/rustdesk
+```
+
+Lütfen bu komutları RustDesk deposunun kökünden çalıştırdığınızdan emin olun, aksi takdirde uygulama gereken kaynakları bulamayabilir. Ayrıca, `install` veya `run` gibi diğer cargo altkomutları şu anda bu yöntem aracılığıyla desteklenmemektedir, çünkü bunlar programı konteyner içinde kurar veya çalıştırır ve ana makinede değil.
+ 
+## Dosya Yapısı
+
+- **[libs/hbb_common](https://github.com/rustdesk/rustdesk/tree/master/libs/hbb_common)**: video kodlayıcı, yapılandırma, tcp/udp sarmalayıcı, protobuf, dosya transferi için fs işlevleri ve diğer bazı yardımcı işlevler
+- **[libs/scrap](https://github.com/rustdesk/rustdesk/tree/master/libs/scrap)**: ekran yakalama
+- **[libs/enigo](https://github.com/rustdesk/rustdesk/tree/master/libs/enigo)**: platforma özgü klavye/fare kontrolü
+- **[src/ui](https://github.com/rustdesk/rustdesk/tree/master/src/ui)**: GUI
+- **[src/server](https://github.com/rustdesk/rustdesk/tree/master/src/server)**: ses/pasta/klavye/video hizmetleri ve ağ bağlantıları
+- **[src/client.rs](https://github.com/rustdesk/rustdesk/tree/master/src/client.rs)**: bir eş bağlantısı başlatır
+- **[src/rendezvous_mediator.rs](https://github.com/rustdesk/rustdesk/tree/master/src/rendezvous_mediator.rs)**: [rustdesk-server](https://github.com/rustdesk/rustdesk-server) ile iletişim kurar, uzak doğrudan (TCP delik vurma) veya iletme bağlantısını bekler
+- **[src/platform](https://github.com/rustdesk/rustdesk/tree/master/src/platform)**: platforma özgü kod
+- **[flutter](https://github.com/rustdesk/rustdesk/tree/master/flutter)**: mobil için Flutter kodu
+- **[flutter/web/js](https://github.com/rustdesk/rustdesk/tree/master/flutter/web/js)**: Flutter web istemcisi için JavaScript
+
+## Ekran Görüntüleri
+
+![image](https://user-images.githubusercontent.com/71636191/113112362-ae4deb80-923b-11eb-957d-ff88daad4f06.png)
+
+![image](https://user-images.githubusercontent.com/71636191/113112619-f705a480-923b-11eb-911d-97e984ef52b6.png)
+
+![image](https://user-images.githubusercontent.com/71636191/113112857-3fbd5d80-923c-11eb-9836-768325faf906.png)
+
+![image](https://user-images.githubusercontent.com/71636191/135385039-38fdbd72-379a-422d-b97f-33df71fb1cec.png)
+```
diff --git a/docs/README-UA.md b/docs/README-UA.md
index c6c8e66f6..01914cfc2 100644
--- a/docs/README-UA.md
+++ b/docs/README-UA.md
@@ -34,13 +34,7 @@ RustDesk вітає внесок кожного. Дивіться [`docs/CONTRIB
 Нижче наведені сервери, для безкоштовного використання, вони можуть змінюватися з часом. Якщо ви не перебуваєте поруч з одним із них, ваша мережа може працювати повільно.
 | Місцезнаходження | Постачальник | Технічні характеристики |
 | --------- | ------------- | ------------------ |
-| Південна Корея (Сеул) | AWS lightsail | 1 vCPU / 0.5GB RAM |
-| Сінгапур | Vultr | 1 vCPU / 1GB RAM |
-| США (Даллас) | Vultr | 1 vCPU / 1GB RAM
 | Німеччина | Hetzner | 2 VCPU / 4GB RAM |
-| Німеччина | Codext | 4 vCPU / 8GB RAM |
-| Фінляндія (Гельсінкі) | [Netlock](https://netlockendpoint.com) | 4 vCPU / 8GB RAM |
-| США (Ешберн) | [Netlock](https://netlockendpoint.com) | 4 vCPU / 8GB RAM |
 | Україна (Київ) | [dc.volia](https://dc.volia.com) | 2 vCPU / 4GB RAM |
 
 ## Dev Container
diff --git a/docs/README-VN.md b/docs/README-VN.md
index 38a5df533..ea2c62ead 100644
--- a/docs/README-VN.md
+++ b/docs/README-VN.md
@@ -33,11 +33,7 @@ Dưới đây là những máy chủ mà bạn có thể sử dụng mà không
 
 | Địa điểm | Nhà cung cấp | Cấu hình |
 | --------- | ------------- | ------------------ |
-| Seoul | AWS lightsail | 1 vCPU / 0.5GB RAM |
 | Germany | Hetzner | 2 vCPU / 4GB RAM |
-| Germany | Codext | 4 vCPU / 8GB RAM |
-| Finland (Helsinki) | 0x101 Cyber Security | 4 vCPU / 8GB RAM |
-| USA (Ashburn) | 0x101 Cyber Security | 4 vCPU / 8GB RAM |
 
 ## Dependencies
 
diff --git a/docs/README-ZH.md b/docs/README-ZH.md
index b5ee85e72..7967f7d30 100644
--- a/docs/README-ZH.md
+++ b/docs/README-ZH.md
@@ -1,8 +1,8 @@
 <p align="center">
   <img src="../res/logo-header.svg" alt="RustDesk - Your remote desktop"><br>
-  <a href="#免费公共服务器">服务器</a> •
+  <a href="#免费的公共服务器">服务器</a> •
   <a href="#基本构建步骤">编译</a> •
-  <a href="#使用Docker编译">Docker</a> •
+  <a href="#使用-Docker-编译">Docker</a> •
   <a href="#文件结构">结构</a> •
   <a href="#截图">截图</a><br>
   [<a href="../README.md">English</a>] | [<a href="README-UA.md">Українська</a>] | [<a href="README-CS.md">česky</a>] | [<a href="README-HU.md">Magyar</a>] | [<a href="README-ES.md">Español</a>] | [<a href="README-FA.md">فارسی</a>] | [<a href="README-FR.md">Français</a>] | [<a href="README-DE.md">Deutsch</a>] | [<a href="README-PL.md">Polski</a>] | [<a href="README-ID.md">Indonesian</a>] | [<a href="README-FI.md">Suomi</a>] | [<a href="README-ML.md">മലയാളം</a>] | [<a href="README-JP.md">日本語</a>] | [<a href="README-NL.md">Nederlands</a>] | [<a href="README-IT.md">Italiano</a>] | [<a href="README-RU.md">Русский</a>] | [<a href="README-PTBR.md">Português (Brasil)</a>] | [<a href="README-EO.md">Esperanto</a>] | [<a href="README-KR.md">한국어</a>] | [<a href="README-AR.md">العربي</a>] | [<a href="README-VN.md">Tiếng Việt</a>] | [<a href="README-GR.md">Ελληνικά</a>]<br>
@@ -16,9 +16,19 @@ Chat with us: [知乎](https://www.zhihu.com/people/rustdesk) | [Discord](https:
 或者[自己设置](https://rustdesk.com/server),
 亦或者[开发您的版本](https://github.com/rustdesk/rustdesk-server-demo)。
 
-欢迎大家贡献代码, 请看 [`docs/CONTRIBUTING.md`](CONTRIBUTING.md).
+![image](https://user-images.githubusercontent.com/71636191/171661982-430285f0-2e12-4b1d-9957-4a58e375304d.png)
 
-[**可执行程序下载**](https://github.com/rustdesk/rustdesk/releases)
+RustDesk 期待各位的贡献. 如何参与开发? 详情请看 [CONTRIBUTING.md](docs/CONTRIBUTING.md).
+
+[**FAQ**](https://github.com/rustdesk/rustdesk/wiki/FAQ)
+
+[**BINARY DOWNLOAD**](https://github.com/rustdesk/rustdesk/releases)
+
+[**NIGHTLY BUILD**](https://github.com/rustdesk/rustdesk/releases/tag/nightly)
+
+[<img src="https://fdroid.gitlab.io/artwork/badge/get-it-on.png"
+    alt="Get it on F-Droid"
+    height="80">](https://f-droid.org/en/packages/com.carriez.flutter_hbb)
 
 ## 免费的公共服务器
 
@@ -26,11 +36,16 @@ Chat with us: [知乎](https://www.zhihu.com/people/rustdesk) | [Discord](https:
 
 | Location | Vendor | Specification |
 | --------- | ------------- | ------------------ |
-| Seoul | AWS lightsail | 1 vCPU / 0.5GB RAM |
-| Germany | Hetzner | 2 vCPU / 4GB RAM |
-| Germany | Codext | 4 vCPU / 8GB RAM |
-| Finland (Helsinki) | 0x101 Cyber Security | 4 vCPU / 8GB RAM |
-| USA (Ashburn) | 0x101 Cyber Security | 4 vCPU / 8GB RAM |
+| Germany | [Hetzner](https://www.hetzner.com) | 2 vCPU / 4 GB RAM |
+| Ukraine (Kyiv) | [dc.volia](https://dc.volia.com) | 2 vCPU / 4 GB RAM |
+
+## Dev Container
+
+[![在 Dev Containers 中打开](https://img.shields.io/static/v1?label=Dev%20Container&message=Open&color=blue&logo=visualstudiocode)](https://vscode.dev/redirect?url=vscode://ms-vscode-remote.remote-containers/cloneInVolume?url=https://github.com/rustdesk/rustdesk)
+
+如果你已经安装了 VS Code 和 Docker, 你可以点击上面的徽章开始使用. 点击后, VS Code 将自动安装 Dev Containers 扩展(如果需要),将源代码克隆到容器卷中, 并启动一个 Dev 容器供使用.
+
+Go through [DEVCONTAINER.md](docs/DEVCONTAINER.md) for more info.
 
 ## 依赖
 
@@ -40,16 +55,14 @@ Chat with us: [知乎](https://www.zhihu.com/people/rustdesk) | [Discord](https:
 [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)
 
-移动版本使用Flutter,未来会将桌面版本从Sciter迁移到Flutter。
-
 ## 基本构建步骤
 
-- 请准备好 Rust 开发环境和 C++编译环境
+- 请准备好 Rust 开发环境和 C++ 编译环境
 
-- 安装[vcpkg](https://github.com/microsoft/vcpkg), 正确设置`VCPKG_ROOT`环境变量
+- 安装 [vcpkg](https://github.com/microsoft/vcpkg), 正确设置 `VCPKG_ROOT` 环境变量
 
   - Windows: vcpkg install libvpx:x64-windows-static libyuv:x64-windows-static opus:x64-windows-static aom:x64-windows-static
-  - Linux/Osx: vcpkg install libvpx libyuv opus aom
+  - Linux/macOS: vcpkg install libvpx libyuv opus aom
 
 - 运行 `cargo run`
 
@@ -60,7 +73,15 @@ Chat with us: [知乎](https://www.zhihu.com/people/rustdesk) | [Discord](https:
 ### Ubuntu 18 (Debian 10)
 
 ```sh
-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
+sudo apt install -y zip 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 make \
+        libclang-dev ninja-build libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev
+```
+
+### openSUSE Tumbleweed 
+
+```sh
+sudo zypper install gcc-c++ git curl wget nasm yasm gcc gtk3-devel clang libxcb-devel libXfixes-devel cmake alsa-lib-devel gstreamer-devel gstreamer-plugins-base-devel xdotool-devel
 ```
 
 ### Fedora 28 (CentOS 8)
@@ -110,24 +131,52 @@ cd rustdesk
 mkdir -p target/debug
 wget https://raw.githubusercontent.com/c-smile/sciter-sdk/master/bin.lnx/x64/libsciter-gtk.so
 mv libsciter-gtk.so target/debug
-cargo run
+VCPKG_ROOT=$HOME/vcpkg cargo run
 ```
 
 ### 把 Wayland 修改成 X11 (Xorg)
 
 RustDesk 暂时不支持 Wayland,不过正在积极开发中。
 > [点我](https://docs.fedoraproject.org/en-US/quick-docs/configuring-xorg-as-default-gnome-session/)
-查看 如何将Xorg设置成默认的GNOME session
+查看如何将 Xorg 设置成默认的 GNOME session.
+
+## Wayland 支持
+
+Wayland 似乎没有提供任何将按键发送到其他窗口的 API. 因此, RustDesk 使用较低级别的 API, 即 `/dev/uinput` devices (Linux kernal level).
+
+当 Wayland 是受控方时,您必须以下列方式开始操作:
+
+```bash
+# Start uinput service
+$ sudo rustdesk --service
+$ rustdesk
+```
+
+**Notice**: Wayland 屏幕录制使用不同的接口. RustDesk 目前只支持 org.freedesktop.portal.ScreenCast.
+
+```bash
+$ dbus-send --session --print-reply       \
+  --dest=org.freedesktop.portal.Desktop \
+  /org/freedesktop/portal/desktop       \
+  org.freedesktop.DBus.Properties.Get   \
+  string:org.freedesktop.portal.ScreenCast string:version
+# Not support
+Error org.freedesktop.DBus.Error.InvalidArgs: No such interface “org.freedesktop.portal.ScreenCast”
+# Support
+method return time=1662544486.931020 sender=:1.54 -> destination=:1.139 serial=257 reply_serial=2
+   variant       uint32 4
+```
 
 ## 使用 Docker 编译
 
-### 构建Docker容器
+克隆版本库并构建 Docker 容器:
 
 ```sh
 git clone https://github.com/rustdesk/rustdesk # 克隆Github存储库
 cd rustdesk # 进入文件夹
 docker build -t "rustdesk-builder" . # 构建容器
 ```
+
 请注意:
 * 针对国内网络访问问题,可以做以下几点优化:  
    1. Dockerfile 中修改系统的源到国内镜像
@@ -166,8 +215,9 @@ docker build -t "rustdesk-builder" . # 构建容器
       docker build -t "rustdesk-builder" . --build-arg http_proxy=http://host:port --build-arg https_proxy=http://host:port
       ```
 
-### 构建RustDesk程序
-容器构建完成后,运行下列指令以完成对RustDesk应用程序的构建:
+### 构建 RustDesk 程序
+
+然后, 每次需要构建应用程序时, 运行以下命令:
 
 ```sh
 docker run --rm -it -v $PWD:/home/user/rustdesk -v rustdesk-git-cache:/home/user/.cargo/git -v rustdesk-registry-cache:/home/user/.cargo/registry -e PUID="$(id -u)" -e PGID="$(id -g)" rustdesk-builder
@@ -182,25 +232,25 @@ docker run --rm -it -v $PWD:/home/user/rustdesk -v rustdesk-git-cache:/home/user
    groupmod: Permission denied.
    groupmod: cannot lock /etc/group; try again later.
    ```
-   > **原因:** 容器的entrypoint脚本会检测UID和GID,在度判和给定的环境变量的不一致时,会强行修改user的UID和GID并重新运行。但在重启后读不到环境中的UID和GID,然后再次进入判错重启环节
+   > **原因:** 容器的 entrypoint 脚本会检测 UID 和 GID,在度判和给定的环境变量的不一致时,会强行修改 user 的 UID 和 GID 并重新运行。但在重启后读不到环境中的 UID 和 GID,然后再次进入判错重启环节
 
 
-### 运行RustDesk程序
+### 运行 RustDesk 程序
 
-生成的可执行程序在target目录下,可直接通过指令运行调试(Debug)版本的RustDesk:
+生成的可执行程序在 target 目录下,可直接通过指令运行调试 (Debug) 版本的 RustDesk:
 ```sh
 target/debug/rustdesk
 ```
 
-或者您想运行发行(Release)版本:
+或者您想运行发行 (Release) 版本:
 
 ```sh
 target/release/rustdesk
 ```
 
 请注意:
-* 请保证您运行的目录是在RustDesk库的根目录内,否则软件会读不到文件。
-* `install`、`run`等Cargo的子指令在容器内不可用,宿主机才行。
+* 请保证您运行的目录是在 RustDesk 库的根目录内,否则软件会读不到文件。
+* `install`、`run`等 Cargo 的子指令在容器内不可用,宿主机才行。
 
 ## 文件结构
 
diff --git a/docs/SECURITY-IT.md b/docs/SECURITY-IT.md
new file mode 100644
index 000000000..91573dcf7
--- /dev/null
+++ b/docs/SECURITY-IT.md
@@ -0,0 +1,11 @@
+# Policy sicurezza
+
+## Segnalazione di una vulnerabilità
+
+Attribuiamo grande importanza alla sicurezza del progetto. 
+Incoraggiamo tutti gli utenti a segnalare eventuali vulnerabilità di sicurezza che ci scoprono.
+Se trovi una vulnerabilità nel progetto RustDesk, segnalala responsabilmente inviando un'email a info@rustdesk.com.
+
+Al momento non abbiamo un programma di taglia sui bug.
+Siamo una piccola squadra che cerca di risolvere un grosso problema. 
+Ti esortiamo a segnalare responsabilmente tutte le vulnerabilità in modo da poter continuare a sviluppare un'applicazione sicura per l'intera comunità.
diff --git a/docs/SECURITY-TR.md b/docs/SECURITY-TR.md
new file mode 100644
index 000000000..88037acb2
--- /dev/null
+++ b/docs/SECURITY-TR.md
@@ -0,0 +1,9 @@
+# Güvenlik Politikası
+
+## Bir Güvenlik Açığı Bildirme
+
+Projemiz için güvenliği çok önemsiyoruz. Kullanıcıların keşfettikleri herhangi bir güvenlik açığını bize bildirmelerini teşvik ediyoruz.
+Eğer RustDesk projesinde bir güvenlik açığı bulursanız, lütfen info@rustdesk.com adresine sorumlu bir şekilde bildirin.
+
+Şu an için bir hata ödül programımız bulunmamaktadır. Büyük bir sorunu çözmeye çalışan küçük bir ekibiz. Herhangi bir güvenlik açığını sorumlu bir şekilde bildirmenizi rica ederiz,
+böylece tüm topluluk için güvenli bir uygulama oluşturmaya devam edebiliriz.
diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/1.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/1.png
index e84ed4d21..9b0125f21 100644
Binary files a/fastlane/metadata/android/en-US/images/phoneScreenshots/1.png and b/fastlane/metadata/android/en-US/images/phoneScreenshots/1.png differ
diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/2.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/2.png
index 5a83dc1f0..caffe504b 100644
Binary files a/fastlane/metadata/android/en-US/images/phoneScreenshots/2.png and b/fastlane/metadata/android/en-US/images/phoneScreenshots/2.png differ
diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/3.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/3.png
index 629631ac7..05b6afabb 100644
Binary files a/fastlane/metadata/android/en-US/images/phoneScreenshots/3.png and b/fastlane/metadata/android/en-US/images/phoneScreenshots/3.png differ
diff --git a/fastlane/metadata/android/en-US/images/sevenInchScreenshots/6.png b/fastlane/metadata/android/en-US/images/sevenInchScreenshots/6.png
index 8e0a83a6a..bbef90fb9 100644
Binary files a/fastlane/metadata/android/en-US/images/sevenInchScreenshots/6.png and b/fastlane/metadata/android/en-US/images/sevenInchScreenshots/6.png differ
diff --git a/fastlane/metadata/android/en-US/images/sevenInchScreenshots/7.png b/fastlane/metadata/android/en-US/images/sevenInchScreenshots/7.png
index 0618ae0b6..132736514 100644
Binary files a/fastlane/metadata/android/en-US/images/sevenInchScreenshots/7.png and b/fastlane/metadata/android/en-US/images/sevenInchScreenshots/7.png differ
diff --git a/fastlane/metadata/android/en-US/images/sevenInchScreenshots/8.png b/fastlane/metadata/android/en-US/images/sevenInchScreenshots/8.png
index 560902b03..946956b2b 100644
Binary files a/fastlane/metadata/android/en-US/images/sevenInchScreenshots/8.png and b/fastlane/metadata/android/en-US/images/sevenInchScreenshots/8.png differ
diff --git a/flatpak/rustdesk.json b/flatpak/rustdesk.json
index 3cfca4d30..4d2e297cc 100644
--- a/flatpak/rustdesk.json
+++ b/flatpak/rustdesk.json
@@ -4,7 +4,7 @@
   "runtime-version": "21.08",
   "sdk": "org.freedesktop.Sdk",
   "command": "rustdesk",
-  "icon": "share/rustdesk/files/rustdesk.png",
+  "icon": "share/icons/hicolor/scalable/apps/rustdesk.svg",
   "modules": [
     "shared-modules/libappindicator/libappindicator-gtk3-12.10.json",
     "xdotool.json",
@@ -12,20 +12,21 @@
       "name": "rustdesk",
       "buildsystem": "simple",
       "build-commands": [
-        "bsdtar -zxvf rustdesk-1.2.0.deb",
+        "bsdtar -zxvf rustdesk-1.2.3.deb",
         "tar -xvf ./data.tar.xz",
         "cp -r ./usr/*  /app/",
         "mkdir -p /app/bin && ln -s /app/lib/rustdesk/rustdesk /app/bin/rustdesk",
         "mv /app/share/applications/rustdesk.desktop /app/share/applications/com.rustdesk.RustDesk.desktop",
         "sed -i '/^Icon=/ c\\Icon=com.rustdesk.RustDesk' /app/share/applications/com.rustdesk.RustDesk.desktop",
         "sed -i '/^Icon=/ c\\Icon=com.rustdesk.RustDesk' /app/share/applications/rustdesk-link.desktop",
+        "mv /app/share/icons/hicolor/scalable/apps/rustdesk.svg /app/share/icons/hicolor/scalable/apps/com.rustdesk.RustDesk.svg",
         "for size in 16 24 32 48 64 128 256 512; do\n rsvg-convert -w $size -h $size -f png -o $size.png logo.svg\n install -Dm644 $size.png /app/share/icons/hicolor/${size}x${size}/apps/com.rustdesk.RustDesk.png\n done"
       ],
       "cleanup": ["/include", "/lib/pkgconfig", "/share/gtk-doc"],
       "sources": [
         {
           "type": "file",
-          "path": "../rustdesk-1.2.0.deb"
+          "path": "../rustdesk-1.2.3.deb"
         },
         {
           "type": "file",
diff --git a/flutter/android/app/build.gradle b/flutter/android/app/build.gradle
index 326689e5e..f4dc69e41 100644
--- a/flutter/android/app/build.gradle
+++ b/flutter/android/app/build.gradle
@@ -46,7 +46,7 @@ android {
         // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
         applicationId "com.carriez.flutter_hbb"
         minSdkVersion 21
-        targetSdkVersion 31
+        targetSdkVersion 33
         versionCode flutterVersionCode.toInteger()
         versionName flutterVersionName
     }
diff --git a/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/InputService.kt b/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/InputService.kt
index 905a2734d..203558968 100644
--- a/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/InputService.kt
+++ b/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/InputService.kt
@@ -26,6 +26,13 @@ const val WHEEL_BUTTON_UP = 34
 const val WHEEL_DOWN = 523331
 const val WHEEL_UP = 963
 
+const val TOUCH_SCALE_START = 1
+const val TOUCH_SCALE = 2
+const val TOUCH_SCALE_END = 3
+const val TOUCH_PAN_START = 4
+const val TOUCH_PAN_UPDATE = 5
+const val TOUCH_PAN_END = 6
+
 const val WHEEL_STEP = 120
 const val WHEEL_DURATION = 50L
 const val LONG_TAP_DELAY = 200L
@@ -167,6 +174,30 @@ class InputService : AccessibilityService() {
         }
     }
 
+    @RequiresApi(Build.VERSION_CODES.N)
+    fun onTouchInput(mask: Int, _x: Int, _y: Int) {
+        when (mask) {
+            TOUCH_PAN_UPDATE -> {
+                mouseX -= _x * SCREEN_INFO.scale
+                mouseY -= _y * SCREEN_INFO.scale
+                mouseX = max(0, mouseX);
+                mouseY = max(0, mouseY);
+                continueGesture(mouseX, mouseY)
+            }
+            TOUCH_PAN_START -> {
+                mouseX = max(0, _x) * SCREEN_INFO.scale
+                mouseY = max(0, _y) * SCREEN_INFO.scale
+                startGesture(mouseX, mouseY)
+            }
+            TOUCH_PAN_END -> {
+                endGesture(mouseX, mouseY)
+                mouseX = max(0, _x) * SCREEN_INFO.scale
+                mouseY = max(0, _y) * SCREEN_INFO.scale
+            }
+            else -> {}
+        }
+    }
+
     @RequiresApi(Build.VERSION_CODES.N)
     private fun consumeWheelActions() {
         if (isWheelActionsPolling) {
diff --git a/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/MainService.kt b/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/MainService.kt
index 78e4e451e..535a3f8c3 100644
--- a/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/MainService.kt
+++ b/flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/MainService.kt
@@ -71,17 +71,26 @@ class MainService : Service() {
 
     @Keep
     @RequiresApi(Build.VERSION_CODES.N)
-    fun rustMouseInput(mask: Int, x: Int, y: Int) {
+    fun rustPointerInput(kind: String, mask: Int, x: Int, y: Int) {
         // turn on screen with LIFT_DOWN when screen off
-        if (!powerManager.isInteractive && mask == LIFT_DOWN) {
+        if (!powerManager.isInteractive && (kind == "touch" || mask == LIFT_DOWN)) {
             if (wakeLock.isHeld) {
-                Log.d(logTag,"Turn on Screen, WakeLock release")
+                Log.d(logTag, "Turn on Screen, WakeLock release")
                 wakeLock.release()
             }
             Log.d(logTag,"Turn on Screen")
             wakeLock.acquire(5000)
         } else {
-            InputService.ctx?.onMouseInput(mask,x,y)
+            when (kind) {
+                "touch" -> {
+                    InputService.ctx?.onTouchInput(mask, x, y)
+                }
+                "mouse" -> {
+                    InputService.ctx?.onMouseInput(mask, x, y)
+                }
+                else -> {
+                }
+            }
         }
     }
 
diff --git a/flutter/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/flutter/android/app/src/main/res/mipmap-hdpi/ic_launcher.png
index d05404d3a..116904a84 100644
Binary files a/flutter/android/app/src/main/res/mipmap-hdpi/ic_launcher.png and b/flutter/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/flutter/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png b/flutter/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png
index 3742f241f..7c8a2be8d 100644
Binary files a/flutter/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png and b/flutter/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png differ
diff --git a/flutter/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png b/flutter/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
index 964c5faa0..c8aef7fd5 100644
Binary files a/flutter/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png and b/flutter/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png differ
diff --git a/flutter/android/app/src/main/res/mipmap-hdpi/ic_stat_logo.png b/flutter/android/app/src/main/res/mipmap-hdpi/ic_stat_logo.png
index 79a814f59..42e74fe0b 100644
Binary files a/flutter/android/app/src/main/res/mipmap-hdpi/ic_stat_logo.png and b/flutter/android/app/src/main/res/mipmap-hdpi/ic_stat_logo.png differ
diff --git a/flutter/android/app/src/main/res/mipmap-ldpi/ic_launcher.png b/flutter/android/app/src/main/res/mipmap-ldpi/ic_launcher.png
index 814ba4549..60e95748c 100644
Binary files a/flutter/android/app/src/main/res/mipmap-ldpi/ic_launcher.png and b/flutter/android/app/src/main/res/mipmap-ldpi/ic_launcher.png differ
diff --git a/flutter/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/flutter/android/app/src/main/res/mipmap-mdpi/ic_launcher.png
index f16b3d61d..7dca207d8 100644
Binary files a/flutter/android/app/src/main/res/mipmap-mdpi/ic_launcher.png and b/flutter/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/flutter/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png b/flutter/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png
index de17ccbda..0faadfca0 100644
Binary files a/flutter/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png and b/flutter/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png differ
diff --git a/flutter/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png b/flutter/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
index 2136a2f3c..246d6ee7a 100644
Binary files a/flutter/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png and b/flutter/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png differ
diff --git a/flutter/android/app/src/main/res/mipmap-mdpi/ic_stat_logo.png b/flutter/android/app/src/main/res/mipmap-mdpi/ic_stat_logo.png
index c179bf053..d643a4fac 100644
Binary files a/flutter/android/app/src/main/res/mipmap-mdpi/ic_stat_logo.png and b/flutter/android/app/src/main/res/mipmap-mdpi/ic_stat_logo.png differ
diff --git a/flutter/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/flutter/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
index d9bd8fdfe..33a40ed83 100644
Binary files a/flutter/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png and b/flutter/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/flutter/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png b/flutter/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png
index f8ced45f1..e083cecf1 100644
Binary files a/flutter/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png and b/flutter/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png differ
diff --git a/flutter/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/flutter/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
index 415eca622..ab9c356dc 100644
Binary files a/flutter/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png and b/flutter/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png differ
diff --git a/flutter/android/app/src/main/res/mipmap-xhdpi/ic_stat_logo.png b/flutter/android/app/src/main/res/mipmap-xhdpi/ic_stat_logo.png
index d82d1a81b..e02182dd1 100644
Binary files a/flutter/android/app/src/main/res/mipmap-xhdpi/ic_stat_logo.png and b/flutter/android/app/src/main/res/mipmap-xhdpi/ic_stat_logo.png differ
diff --git a/flutter/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/flutter/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
index eba179347..4585230d3 100644
Binary files a/flutter/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png and b/flutter/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/flutter/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png b/flutter/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png
index 0f46fafaf..b9975190c 100644
Binary files a/flutter/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png and b/flutter/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png differ
diff --git a/flutter/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/flutter/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
index 87889c953..79eab3d84 100644
Binary files a/flutter/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png and b/flutter/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png differ
diff --git a/flutter/android/app/src/main/res/mipmap-xxhdpi/ic_stat_logo.png b/flutter/android/app/src/main/res/mipmap-xxhdpi/ic_stat_logo.png
index 2cbe6eaf1..f7b57b329 100644
Binary files a/flutter/android/app/src/main/res/mipmap-xxhdpi/ic_stat_logo.png and b/flutter/android/app/src/main/res/mipmap-xxhdpi/ic_stat_logo.png differ
diff --git a/flutter/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/flutter/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
index a8d80d2a2..463d20eeb 100644
Binary files a/flutter/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png and b/flutter/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/flutter/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png b/flutter/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png
index 88eafe8dd..d781e2595 100644
Binary files a/flutter/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png and b/flutter/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png differ
diff --git a/flutter/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/flutter/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
index 00709a815..3cae0ce40 100644
Binary files a/flutter/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png and b/flutter/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png differ
diff --git a/flutter/android/app/src/main/res/mipmap-xxxhdpi/ic_stat_logo.png b/flutter/android/app/src/main/res/mipmap-xxxhdpi/ic_stat_logo.png
index 209c5f977..9c2153e9e 100644
Binary files a/flutter/android/app/src/main/res/mipmap-xxxhdpi/ic_stat_logo.png and b/flutter/android/app/src/main/res/mipmap-xxxhdpi/ic_stat_logo.png differ
diff --git a/flutter/assets/GitHub.svg b/flutter/assets/GitHub.svg
deleted file mode 100644
index ef0bb12a7..000000000
--- a/flutter/assets/GitHub.svg
+++ /dev/null
@@ -1 +0,0 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24"><path fill="#fff" d="M12 0C5.374 0 0 5.373 0 12c0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23A11.509 11.509 0 0 1 12 5.803c1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576C20.566 21.797 24 17.3 24 12c0-6.627-5.373-12-12-12z"/></svg>
\ No newline at end of file
diff --git a/flutter/assets/Google.svg b/flutter/assets/Google.svg
deleted file mode 100644
index df394a84f..000000000
--- a/flutter/assets/Google.svg
+++ /dev/null
@@ -1 +0,0 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="400" height="400"><g fill-rule="evenodd"><path fill="#5cc46c" d="M63.8 266c-19.359 19.36-35.424 35.2-35.699 35.2-.695 0-.614.432.299 1.6.43.55.786 1.225.791 1.5.005.275.167.5.36.5.194 0 .576.54.849 1.2.273.66.655 1.2.849 1.2.193 0 .351.236.351.525 0 .288.315.873.7 1.3.385.426.955 1.27 1.268 1.875.312.605.717 1.1.9 1.1.182 0 .332.236.332.525 0 .288.36.911.8 1.383.44.472.8 1.046.8 1.275 0 .229.18.417.4.417.22 0 .4.27.4.6 0 .33.141.6.314.6.172 0 .606.495.965 1.1.358.605.993 1.494 1.411 1.975a34.055 34.055 0 0 1 1.535 1.944c.426.588.958 1.275 1.181 1.525.223.251 1.033 1.265 1.8 2.255 1.914 2.469 2.734 3.461 4.594 5.554.88.991 2.05 2.335 2.6 2.988 2.992 3.552 19.052 19.043 20.135 19.422.256.089.735.449 1.065.801 1.076 1.145 5.282 4.436 5.67 4.436.207 0 .266.18.13.4-.136.22.035.4.38.4s1.192.54 1.883 1.2c.691.66 1.459 1.2 1.708 1.2.249 0 .712.36 1.029.8.317.44.807.8 1.088.8.282 0 .512.18.512.4 0 .22.27.4.6.4.33 0 .6.15.6.332 0 .183.495.588 1.1.9.605.313 1.449.883 1.875 1.268.427.385.997.7 1.267.7.27 0 .551.163.625.363.073.199 2.113 1.553 4.533 3.008 2.42 1.456 4.67 2.829 5 3.052.33.223 1.095.619 1.7.879s1.1.614 1.1.786c0 .171.36.312.8.312.44 0 .8.18.8.4 0 .22.36.4.8.4.44 0 .8.18.8.4 0 .22.36.4.8.4.44 0 .8.146.8.325 0 .179.63.573 1.4.875.77.302 1.4.696 1.4.875 0 .179.36.325.8.325.44 0 .8.18.8.4 0 .22.36.4.8.4.44 0 .8.18.8.4 0 .22.36.4.8.4.44 0 .8.18.8.4 0 .22.253.4.562.4.485 0 1.758.552 4.733 2.05.382.193.967.35 1.3.35.333 0 .605.18.605.4 0 .22.272.4.604.4.333 0 1.458.378 2.5.84 1.043.462 2.256.978 2.696 1.146.44.169 1.25.499 1.8.734s2.17.812 3.6 1.283c1.43.47 2.87.959 3.2 1.086 1.092.422 1.784.588 3.3.793.825.111 1.5.363 1.5.56 0 .197.529.358 1.176.358.647 0 1.288.18 1.424.4.136.22.766.4 1.4.4.634 0 1.264.18 1.4.4.136.22.766.4 1.4.4.634 0 1.264.18 1.4.4.136.22.856.4 1.6.4.744 0 1.464.18 1.6.4.136.22.946.4 1.8.4.854 0 1.664.18 1.8.4.136.22.946.4 1.8.4.854 0 1.664.18 1.8.4.136.22 1.137.4 2.224.4 1.087 0 1.976.18 1.976.4 0 .22.979.4 2.176.4s2.288.18 2.424.4c.136.22 1.461.4 2.944.4 1.483 0 2.888.192 3.123.427.271.271 4.73.512 12.28.662l11.853.236v-94.797l-1.019-.464c-.605-.275-3.114-.464-6.176-.464-3.007 0-5.261-.167-5.405-.4-.136-.22-1.306-.4-2.6-.4s-2.464-.18-2.6-.4c-.136-.22-1.036-.4-2-.4s-1.864-.18-2-.4c-.136-.22-.946-.4-1.8-.4-.854 0-1.664-.18-1.8-.4-.136-.22-.777-.4-1.424-.4-.647 0-1.176-.16-1.176-.356 0-.196-.585-.458-1.3-.581-3.012-.521-3.9-.773-3.9-1.108 0-.195-.439-.355-.976-.355-.537 0-1.066-.144-1.175-.321-.109-.176-1.11-.569-2.224-.872-1.114-.303-2.025-.699-2.025-.879 0-.181-.236-.328-.525-.328-.805 0-9.99-4.409-10.208-4.9-.074-.165-.479-.3-.9-.3-.422 0-.767-.18-.767-.4 0-.22-.32-.4-.712-.4-.391 0-.971-.36-1.288-.8-.317-.44-.897-.8-1.288-.8-.392 0-.712-.15-.712-.332 0-.183-.492-.588-1.094-.9-1.086-.564-2.109-1.307-3.122-2.268-.29-.275-.72-.5-.955-.5-.236 0-.429-.18-.429-.4 0-.22-.255-.4-.567-.4-.311 0-.626-.135-.7-.3-.073-.165-.792-.84-1.598-1.5l-3.171-2.6c-3.684-3.022-11.345-10.93-14.349-14.81-.762-.985-1.527-1.85-1.7-1.923-.173-.074-.315-.349-.315-.612s-.36-.738-.8-1.055c-.44-.317-.8-.78-.8-1.029 0-.249-.54-1.017-1.2-1.708-.66-.691-1.2-1.527-1.2-1.859 0-.332-.18-.604-.4-.604-.22 0-.4-.236-.4-.525 0-.288-.36-.911-.8-1.383-.44-.472-.8-1.121-.8-1.442 0-.321-.158-.643-.351-.717-.194-.073-1.555-2.653-3.025-5.733-1.47-3.08-2.842-5.913-3.049-6.295-.206-.382-.375-.967-.375-1.3 0-.333-.18-.605-.4-.605-.22 0-.4-.414-.4-.92s-.149-1.091-.33-1.3c-.182-.209-.575-1.19-.872-2.18-.549-1.823-1.089-2.8-1.548-2.8-.138 0-16.091 15.84-35.45 35.2"/><path fill="#f85412" d="M179.003.276c-1.978.087-3.769.33-3.98.541-.211.211-1.607.383-3.103.383-1.547 0-2.72.173-2.72.4 0 .222-1.067.4-2.4.4s-2.4.178-2.4.4c0 .22-.799.4-1.776.4s-1.888.18-2.024.4c-.136.22-1.047.4-2.024.4-.977 0-1.776.18-1.776.4 0 .22-.72.4-1.6.4-.88 0-1.6.18-1.6.4 0 .22-.72.4-1.6.4-.88 0-1.6.18-1.6.4 0 .22-.72.4-1.6.4-.88 0-1.6.18-1.6.4 0 .22-.529.4-1.176.4-.647 0-1.288.18-1.424.4-.136.22-.777.4-1.424.4-.647 0-1.176.18-1.176.4 0 .22-.429.4-.954.4-1.024 0-4.799 1.04-5.941 1.637-.382.2-.969.363-1.305.363-.336 0-.923.159-1.305.353-.716.364-2.341 1.058-4.095 1.747-.55.216-1.313.552-1.695.747-.382.194-.967.353-1.3.353-.333 0-.605.18-.605.4 0 .22-.439.4-.976.4-.537 0-1.088.18-1.224.4-.136.22-.597.4-1.024.4-.427 0-.776.18-.776.4 0 .22-.36.4-.8.4-.44 0-.8.18-.8.4 0 .22-.36.4-.8.4-.44 0-.8.18-.8.4 0 .22-.349.4-.776.4-.427 0-.888.18-1.024.4-.136.22-.597.4-1.024.4-.427 0-.776.18-.776.4 0 .22-.36.4-.8.4-.44 0-.8.18-.8.4 0 .22-.36.4-.8.4-.44 0-.8.18-.8.4 0 .22-.345.4-.767.4-.421 0-.826.135-.9.3-.167.376-2.297 1.675-3.233 1.971-.385.122-.7.381-.7.576 0 .194-.36.353-.8.353-.44 0-.8.18-.8.4 0 .22-.242.4-.539.4-.588 0-3.139 1.178-3.461 1.598-.11.144-1.01.698-2 1.232-.99.533-1.89 1.06-2 1.171-.11.11-.695.455-1.3.767-.605.312-1.1.717-1.1.9 0 .182-.27.332-.6.332-.33 0-.6.158-.6.351 0 .194-.54.576-1.2.849-.66.273-1.2.655-1.2.849 0 .193-.243.351-.541.351-.297 0-1.105.54-1.796 1.2-.691.66-1.488 1.2-1.771 1.2-.283 0-.775.36-1.092.8-.317.44-.825.8-1.128.8-.303 0-1.055.54-1.672 1.2-.617.66-1.292 1.2-1.501 1.2-.209 0-.945.54-1.636 1.2-.691.66-1.4 1.2-1.576 1.2-.176 0-.943.586-1.704 1.303a31.376 31.376 0 0 1-2.783 2.295c-.77.545-2.141 1.714-3.046 2.597-1.863 1.818-3.27 3.005-3.56 3.005-.483 0-14.594 14.533-14.594 15.031 0 .121-.86 1.064-1.911 2.095-1.051 1.031-2.354 2.504-2.896 3.274a27.29 27.29 0 0 1-2.121 2.593c-.625.657-1.431 1.692-1.791 2.3-.359.609-.795 1.107-.967 1.107-.173 0-.314.27-.314.6 0 .33-.18.6-.4.6-.22 0-.4.198-.4.44 0 .241-.54.943-1.2 1.56-.66.617-1.2 1.369-1.2 1.672 0 .303-.36.811-.8 1.128-.44.317-.8.815-.8 1.106 0 .292-.436.966-.969 1.499-.533.533-1.085 1.335-1.227 1.782-.142.447-.426.813-.631.813-.205 0-.373.236-.373.525 0 .288-.36.911-.8 1.383-.44.472-.8 1.018-.8 1.213 0 .195-.36.681-.8 1.079-.44.398-.8.966-.8 1.262 0 .296-.18.538-.4.538-.22 0-.404.225-.409.5-.005.275-.361.95-.791 1.5-.898 1.149-1.026 2-.299 2 .275 0 16.34 15.84 35.699 35.2 19.88 19.881 35.503 35.2 35.899 35.2.512 0 .701-.323.701-1.2 0-.66.18-1.2.4-1.2.22 0 .4-.54.4-1.2 0-.66.18-1.2.4-1.2.22 0 .4-.349.4-.776 0-.427.18-.888.4-1.024.22-.136.4-.687.4-1.224 0-.537.18-.976.4-.976.22 0 .4-.36.4-.8 0-.44.18-.8.4-.8.22 0 .4-.265.4-.588 0-.323.36-1.229.8-2.012.44-.783.8-1.674.8-1.979 0-.305.135-.614.3-.688.452-.201 2.5-4.239 2.5-4.93 0-.332.18-.603.4-.603.22 0 .4-.32.4-.712 0-.391.36-.971.8-1.288.44-.317.8-.857.8-1.2 0-.343.36-.883.8-1.2.44-.317.8-.857.8-1.2 0-.343.36-.883.8-1.2.44-.317.8-.767.8-1 0-.233.36-.683.8-1 .44-.317.8-.769.8-1.004 0-.236.72-1.224 1.6-2.196.88-.972 1.6-1.914 1.6-2.093 0-.178.945-1.289 2.1-2.469a966.789 966.789 0 0 0 4.554-4.691c3.401-3.531 6.778-6.629 7.847-7.2.494-.264 1.376-.99 1.959-1.614.584-.623 1.259-1.133 1.5-1.133.242 0 .44-.18.44-.4 0-.22.23-.4.512-.4.281 0 .771-.36 1.088-.8.317-.44.767-.8 1-.8.233 0 .683-.36 1-.8.317-.44.857-.8 1.2-.8.343 0 .883-.36 1.2-.8.317-.44.897-.8 1.288-.8.392 0 .712-.18.712-.4 0-.22.236-.4.525-.4.288 0 .911-.36 1.383-.8.472-.44 1.062-.8 1.312-.8.249 0 2.021-.81 3.938-1.8 1.917-.99 3.7-1.8 3.964-1.8.263 0 .478-.18.478-.4 0-.22.36-.4.8-.4.44 0 .8-.18.8-.4 0-.22.349-.4.776-.4.427 0 .888-.18 1.024-.4.136-.22.687-.4 1.224-.4.537 0 .976-.18.976-.4 0-.22.362-.4.805-.4.778 0 1.164-.142 3.09-1.138.492-.254 1.347-.462 1.9-.462.553 0 1.005-.18 1.005-.4 0-.22.529-.4 1.176-.4.647 0 1.288-.18 1.424-.4.136-.22.867-.4 1.624-.4.757 0 1.376-.18 1.376-.4 0-.22.72-.4 1.6-.4.88 0 1.6-.18 1.6-.4 0-.22.9-.4 2-.4s2-.18 2-.4c0-.226 1.133-.4 2.6-.4s2.6-.174 2.6-.4c0-.249 2.256-.4 5.967-.4 4.382 0 6.028-.133 6.2-.5.186-.4.28-.4.466 0 .171.366 1.773.5 5.977.5 3.397 0 5.844.163 5.99.4.136.22 1.407.4 2.824.4 1.451 0 2.576.175 2.576.4 0 .22.9.4 2 .4s2 .18 2 .4c0 .22.709.4 1.576.4.867 0 1.688.18 1.824.4.136.22.766.4 1.4.4.634 0 1.264.18 1.4.4.136.22.867.4 1.624.4.757 0 1.376.18 1.376.4 0 .22.54.4 1.2.4.66 0 1.2.18 1.2.4 0 .22.439.4.976.4.537 0 1.088.18 1.224.4.136.22.597.4 1.024.4.427 0 .776.18.776.4 0 .22.54.4 1.2.4.66 0 1.2.18 1.2.4 0 .22.36.4.8.4.44 0 .8.18.8.4 0 .22.36.4.8.4.44 0 .8.18.8.4 0 .22.253.4.562.4.308 0 2.097.81 3.974 1.8 1.876.99 3.694 1.8 4.038 1.8.344 0 .626.18.626.4 0 .22.32.4.712.4.391 0 .971.36 1.288.8.317.44.857.8 1.2.8.343 0 .883.36 1.2.8.317.44.857.8 1.2.8.343 0 .883.36 1.2.8.317.44.767.8 1 .8.233 0 .683.36 1 .8.317.44.857.8 1.2.8.343 0 .883.36 1.2.8.317.44.886.8 1.265.8.378 0 .799.18.935.4.326.528 9.274.528 9.6 0 .136-.22.508-.4.828-.4.319 0 2.603-1.983 5.076-4.406 2.473-2.423 9.427-9.218 15.454-15.1C315.751 79.168 330 64.859 330 64.311c0-.294.36-.794.8-1.111.44-.317.8-.897.8-1.288 0-.392.18-.712.4-.712.243 0 .4-1.8.4-4.6s-.157-4.6-.4-4.6c-.22 0-.4-.474-.4-1.053s-.315-1.347-.7-1.706c-.385-.359-.892-.998-1.127-1.42-.234-.422-.769-.96-1.188-1.194-.418-.235-1.216-.878-1.773-1.428-.557-.551-1.732-1.532-2.612-2.18-.88-.648-1.891-1.502-2.247-1.898-.356-.397-.869-.721-1.139-.721-.271 0-.997-.54-1.614-1.2-.617-.66-1.369-1.2-1.672-1.2-.303 0-.811-.36-1.128-.8-.317-.44-.78-.8-1.029-.8-.249 0-1.017-.54-1.708-1.2-.691-.66-1.488-1.2-1.771-1.2-.283 0-.775-.36-1.092-.8-.317-.44-.831-.8-1.141-.8-.311 0-.856-.316-1.212-.703-.356-.386-1.187-.971-1.847-1.3-.66-.328-1.549-.912-1.975-1.297-.427-.385-.997-.7-1.267-.7-.27 0-.551-.135-.625-.3-.18-.405-2.121-1.538-4.433-2.587-1.045-.475-1.9-1.009-1.9-1.188 0-.179-.36-.325-.8-.325-.44 0-.8-.18-.8-.4 0-.22-.36-.4-.8-.4-.44 0-.8-.18-.8-.4 0-.22-.36-.4-.8-.4-.44 0-.8-.18-.8-.4 0-.22-.27-.4-.6-.4-.33 0-.6-.18-.6-.4 0-.22-.36-.4-.8-.4-.44 0-.8-.18-.8-.4 0-.22-.36-.4-.8-.4-.44 0-.8-.18-.8-.4 0-.22-.36-.4-.8-.4-.44 0-.8-.18-.8-.4 0-.22-.36-.4-.8-.4-.44 0-.8-.18-.8-.4 0-.22-.36-.4-.8-.4-.44 0-.8-.18-.8-.4 0-.22-.439-.4-.976-.4-.537 0-1.088-.18-1.224-.4-.136-.22-.597-.4-1.024-.4-.427 0-.776-.18-.776-.4 0-.22-.36-.4-.8-.4-.44 0-.8-.18-.8-.4 0-.22-.272-.4-.605-.4-.333 0-.918-.159-1.3-.353a28.338 28.338 0 0 0-1.795-.8l-2.2-.894a28.338 28.338 0 0 1-1.795-.8c-.382-.194-1.057-.353-1.5-.353-.443 0-.805-.18-.805-.4 0-.22-.54-.4-1.2-.4-.66 0-1.2-.18-1.2-.4 0-.22-.54-.4-1.2-.4-.66 0-1.2-.159-1.2-.354 0-.195-.99-.478-2.2-.629-1.21-.151-2.2-.442-2.2-.646 0-.204-.529-.371-1.176-.371-.647 0-1.288-.18-1.424-.4-.136-.22-.867-.4-1.624-.4-.757 0-1.376-.18-1.376-.4 0-.22-.72-.4-1.6-.4-.88 0-1.6-.18-1.6-.4 0-.22-.72-.4-1.6-.4-.88 0-1.6-.18-1.6-.4 0-.22-.72-.4-1.6-.4-.88 0-1.6-.18-1.6-.4 0-.22-.81-.4-1.8-.4s-1.8-.18-1.8-.4c0-.22-.799-.4-1.776-.4s-1.888-.18-2.024-.4c-.136-.22-1.317-.4-2.624-.4-1.317 0-2.376-.178-2.376-.4 0-.224-1.107-.4-2.52-.4-1.454 0-2.727-.207-3.01-.49-.484-.484-36.495-.828-45.467-.434"/><path fill="#4474dc" d="M194.58 153.339c-1.577.887-4.18 3.133-4.18 3.606 0 .263-.36.738-.8 1.055-.44.317-.8.897-.8 1.288 0 .392-.18.712-.4.712-.264 0-.4 13.6-.4 40s.136 40 .4 40c.22 0 .4.32.4.712 0 .391.36.971.8 1.288.44.317.8.798.8 1.068s.675.994 1.5 1.609 1.59 1.209 1.7 1.321c1.162 1.175 2.29 1.202 50.595 1.202 26.733 0 48.605.099 48.605.219 0 .121-.36.965-.8 1.875-.44.911-.8 1.892-.8 2.181 0 .289-.18.525-.4.525-.22 0-.4.32-.4.712 0 .391-.36.971-.8 1.288-.44.317-.8.897-.8 1.288 0 .392-.15.712-.332.712-.183 0-.587.495-.898 1.1-.31.605-.881 1.391-1.267 1.747-.387.356-.703.811-.703 1.012 0 .2-.36.624-.8.941-.44.317-.8.813-.8 1.102 0 .288-.63 1.155-1.4 1.925-.77.77-1.4 1.564-1.4 1.764 0 .345-1.421 1.895-7.048 7.688-3.383 3.483-7.819 7.521-8.263 7.521-.202 0-.872.54-1.489 1.2-.617.66-1.291 1.2-1.498 1.2-.208 0-.705.361-1.104.802-.714.79-.187 1.341 32.825 34.362 18.453 18.457 33.617 33.756 33.697 33.997.28.839 1.352.43 2.811-1.074.808-.832 2.099-1.955 2.869-2.496 1.634-1.147 14.8-14.275 14.8-14.757 0-.18 1.26-1.635 2.8-3.234 1.54-1.599 2.8-3.069 2.8-3.267 0-.198.63-.99 1.4-1.76.77-.77 1.405-1.529 1.411-1.686.007-.158.727-1.09 1.6-2.073.874-.982 1.589-1.928 1.589-2.103 0-.175.54-.883 1.2-1.574.66-.691 1.2-1.459 1.2-1.708 0-.249.36-.712.8-1.029.44-.317.8-.857.8-1.2 0-.343.36-.883.8-1.2.44-.317.8-.857.8-1.2 0-.343.36-.883.8-1.2.44-.317.8-.857.8-1.2 0-.343.36-.883.8-1.2.44-.317.8-.807.8-1.088 0-.282.18-.512.4-.512.22 0 .4-.27.4-.6 0-.33.18-.6.4-.6.22 0 .4-.27.4-.6 0-.33.159-.6.353-.6.195 0 .454-.315.576-.7.217-.685 1.475-2.904 1.871-3.3.517-.517 1.6-2.792 1.6-3.362 0-.351.18-.638.4-.638.22 0 .4-.36.4-.8 0-.44.18-.8.4-.8.22 0 .4-.36.4-.8 0-.44.18-.8.4-.8.22 0 .4-.278.4-.617 0-.339.36-1.003.8-1.475.44-.472.8-1.095.8-1.383 0-.289.18-.525.4-.525.22 0 .4-.36.4-.8 0-.44.18-.8.4-.8.22 0 .4-.349.4-.776 0-.427.18-.888.4-1.024.22-.136.4-.597.4-1.024 0-.427.18-.776.4-.776.22 0 .4-.36.4-.8 0-.44.18-.8.4-.8.22 0 .4-.36.4-.8 0-.44.18-.8.4-.8.22 0 .4-.36.4-.8 0-.44.18-.8.4-.8.22 0 .4-.54.4-1.2 0-.66.18-1.2.4-1.2.22 0 .4-.36.4-.8 0-.44.18-.8.4-.8.22 0 .4-.279.4-.619 0-.341.36-1.365.8-2.275.44-.911.8-1.859.8-2.106 0-.247.36-1.195.8-2.106.44-.91.8-1.923.8-2.251 0-.328.18-.707.4-.843.22-.136.4-.676.4-1.2 0-.524.167-1.056.372-1.183.205-.126.495-1.217.646-2.423.151-1.207.433-2.194.628-2.194s.354-.63.354-1.4c0-.77.18-1.4.4-1.4.22 0 .4-.54.4-1.2 0-.66.18-1.2.4-1.2.22 0 .4-.72.4-1.6 0-.88.18-1.6.4-1.6.22 0 .4-.72.4-1.6 0-.88.18-1.6.4-1.6.22 0 .4-.72.4-1.6 0-.88.18-1.6.4-1.6.22 0 .4-.911.4-2.024 0-1.247.153-1.928.4-1.776.247.152.4-.529.4-1.776 0-1.113.18-2.024.4-2.024.222 0 .4-1.067.4-2.4s.178-2.4.4-2.4c.228 0 .4-1.189.4-2.767 0-1.857.164-2.821.5-2.933.393-.131.5-5.452.5-24.813 0-19.184-.111-24.758-.5-25.147-.302-.302-.5-1.577-.5-3.22 0-1.547-.173-2.72-.4-2.72-.222 0-.4-1.067-.4-2.4s-.178-2.4-.4-2.4c-.22 0-.4-.799-.4-1.776s-.18-1.888-.4-2.024c-.22-.136-.4-.597-.4-1.024 0-.427-.18-.776-.4-.776-.22 0-.4-.184-.4-.409 0-.647-4.317-4.791-4.991-4.791-.335 0-.609-.18-.609-.4 0-.609-194.335-.47-195.42.139"/><path fill="#04a46c" d="M107.6 249.2c.449.88.906 1.6 1.016 1.6.11 0-.167-.72-.616-1.6-.449-.88-.906-1.6-1.016-1.6-.11 0 .167.72.616 1.6m152.8 36c0 .22-.23.4-.512.4-.281 0-.771.36-1.088.8-.317.44-.857.8-1.2.8-.343 0-.883.36-1.2.8-.317.44-.857.8-1.2.8-.343 0-.883.36-1.2.8-.317.44-.897.8-1.288.8-.392 0-.712.18-.712.4 0 .22-.242.4-.539.4-.698 0-3.064 1.147-3.961 1.921-.385.331-.7.488-.7.348 0-.141-.635.102-1.412.538-.777.436-1.677.793-2 .793-.323 0-.588.18-.588.4 0 .22-.36.4-.8.4-.44 0-.8.18-.8.4 0 .22-.36.4-.8.4-.44 0-.8.18-.8.4 0 .22-.54.4-1.2.4-.66 0-1.2.18-1.2.4 0 .22-.349.4-.776.4-.427 0-.888.18-1.024.4-.136.22-.687.4-1.224.4-.537 0-.976.18-.976.4 0 .22-.529.4-1.176.4-.647 0-1.288.18-1.424.4-.136.22-.766.4-1.4.4-.634 0-1.264.18-1.4.4-.136.22-.867.4-1.624.4-.757 0-1.376.18-1.376.4 0 .22-.72.4-1.6.4-.88 0-1.6.18-1.6.4 0 .22-.9.4-2 .4s-2 .18-2 .4c0 .225-1.125.4-2.576.4-1.417 0-2.688.18-2.824.4-.146.237-2.59.4-5.981.4-4.859 0-5.77.096-5.976.633-.134.348-.243 21.886-.243 47.862v47.23l12.051-.234c7.733-.15 12.205-.387 12.48-.662.236-.236 1.563-.429 2.949-.429 1.413 0 2.52-.176 2.52-.4 0-.222 1.059-.4 2.376-.4 1.307 0 2.488-.18 2.624-.4.136-.22 1.047-.4 2.024-.4.977 0 1.776-.18 1.776-.4 0-.22.81-.4 1.8-.4s1.8-.18 1.8-.4c0-.22.9-.4 2-.4s2-.18 2-.4c0-.22.529-.4 1.176-.4.647 0 1.288-.18 1.424-.4.136-.22.867-.4 1.624-.4.757 0 1.376-.18 1.376-.4 0-.22.72-.4 1.6-.4.88 0 1.6-.18 1.6-.4 0-.22.54-.4 1.2-.4.66 0 1.2-.167 1.2-.371s.992-.495 2.205-.647c1.213-.152 2.304-.434 2.424-.629.12-.194.658-.353 1.195-.353s.976-.18.976-.4c0-.22.54-.4 1.2-.4.66 0 1.2-.18 1.2-.4 0-.22.362-.4.805-.4.443 0 1.118-.159 1.5-.353.716-.364 2.341-1.058 4.095-1.747.55-.216 1.313-.552 1.695-.747.382-.194.967-.353 1.3-.353.333 0 .605-.18.605-.4 0-.22.36-.4.8-.4.44 0 .8-.18.8-.4 0-.22.349-.4.776-.4.427 0 .888-.18 1.024-.4.136-.22.687-.4 1.224-.4.537 0 .976-.18.976-.4 0-.22.36-.4.8-.4.44 0 .8-.18.8-.4 0-.22.36-.4.8-.4.44 0 .8-.18.8-.4 0-.22.36-.4.8-.4.44 0 .8-.18.8-.4 0-.22.36-.4.8-.4.44 0 .8-.18.8-.4 0-.22.36-.4.8-.4.44 0 .8-.18.8-.4 0-.22.27-.4.6-.4.33 0 .6-.18.6-.4 0-.22.36-.4.8-.4.44 0 .8-.18.8-.4 0-.22.36-.4.8-.4.44 0 .8-.18.8-.4 0-.22.283-.4.629-.4.345 0 .898-.27 1.228-.6.33-.33.947-.6 1.372-.6.424 0 .774-.135.777-.3.004-.165.904-.74 2-1.279 1.097-.538 1.994-1.123 1.994-1.3 0-.176.236-.321.525-.321.288 0 .873-.315 1.3-.7.426-.385 1.315-.969 1.975-1.297.66-.329 1.491-.914 1.847-1.3.356-.387.901-.703 1.212-.703.31 0 .824-.36 1.141-.8.317-.44.857-.8 1.2-.8.343 0 .883-.36 1.2-.8.317-.44.78-.8 1.029-.8.249 0 1.017-.54 1.708-1.2.691-.66 1.455-1.2 1.699-1.2s.947-.54 1.564-1.2c.617-.66 1.409-1.2 1.76-1.2.352 0 .641-.135.643-.3.001-.165.652-.742 1.446-1.283.793-.541 2.261-1.756 3.261-2.7.999-.944 2.059-1.717 2.354-1.717.669 0 .68-.299.036-1.006-7.993-8.769-68.3-68.276-68.3-67.394"/><path fill="#fcdc2c" d="M25.899 101.091c-.605 1.27-1.1 2.534-1.1 2.809.001.275-.179.5-.399.5-.22 0-.4.36-.4.8 0 .44-.15.8-.333.8-.323 0-1.673 2.419-4.131 7.4a147.916 147.916 0 0 1-1.56 3.095c-.207.382-.376.967-.376 1.3 0 .333-.18.605-.4.605-.22 0-.4.36-.4.8 0 .44-.18.8-.4.8-.22 0-.4.272-.4.605 0 .333-.18.963-.4 1.4l-.8 1.59c-.22.437-.4 1.067-.4 1.4 0 .333-.18.605-.4.605-.22 0-.4.36-.4.8 0 .44-.18.8-.4.8-.22 0-.4.273-.4.606 0 .97-.966 3.794-1.297 3.794-.167 0-.303.362-.303.805 0 .443-.176 1.118-.391 1.5-.685 1.217-2.409 6.874-2.409 7.903 0 .546-.18.992-.4.992-.22 0-.4.529-.4 1.176 0 .647-.18 1.288-.4 1.424-.22.136-.4.777-.4 1.424 0 .647-.18 1.176-.4 1.176-.22 0-.4.72-.4 1.6 0 .88-.18 1.6-.4 1.6-.22 0-.4.72-.4 1.6 0 .88-.18 1.6-.4 1.6-.22 0-.4.72-.4 1.6 0 .88-.18 1.6-.4 1.6-.22 0-.4.81-.4 1.8s-.18 1.8-.4 1.8c-.22 0-.4.9-.4 2s-.18 2-.4 2c-.222 0-.4 1.059-.4 2.376 0 1.307-.18 2.488-.4 2.624-.22.136-.4 1.371-.4 2.744 0 1.463-.207 2.703-.5 2.996-.73.731-.73 48.117 0 48.36.33.11.5 1.031.5 2.71 0 1.398.18 2.654.4 2.79.22.136.4 1.317.4 2.624 0 1.317.178 2.376.4 2.376.22 0 .4.9.4 2s.18 2 .4 2c.22 0 .4.81.4 1.8s.18 1.8.4 1.8c.22 0 .4.72.4 1.6 0 .88.18 1.6.4 1.6.22 0 .4.72.4 1.6 0 .88.18 1.6.4 1.6.22 0 .4.72.4 1.6 0 .88.18 1.6.4 1.6.22 0 .4.529.4 1.176 0 .647.18 1.288.4 1.424.22.136.4.777.4 1.424 0 .647.18 1.176.4 1.176.22 0 .4.446.4.992 0 1.029 1.724 6.686 2.409 7.903.215.382.391 1.057.391 1.5 0 .443.141.805.313.805.172 0 .557.829.856 1.842.513 1.736 1.584 4.299 2.446 5.853.212.382.385 1.057.385 1.5 0 .443.18.805.4.805.22 0 .4.36.4.8 0 .44.18.8.4.8.22 0 .4.36.4.8 0 .44.18.8.4.8.22 0 .4.349.4.776 0 .427.18.888.4 1.024.22.136.4.607.4 1.047 0 .44.18.689.4.553.22-.136.4.126.4.582 0 .744.665 2.296 1.208 2.818.115.11.816 1.415 1.56 2.9.743 1.485 1.505 2.7 1.692 2.7.187 0 .34.36.34.8 0 .44.18.8.4.8.22 0 .4.225.399.5-.001 1.514 2.229 5.114 3.104 5.009.588-.071 13.229-12.434 36.3-35.504 34.894-34.892 36.422-36.524 34.997-37.405-.22-.136-.4-.777-.4-1.424 0-.647-.18-1.176-.4-1.176-.22 0-.4-.9-.4-2s-.18-2-.4-2c-.22 0-.4-.799-.4-1.776s-.18-1.888-.4-2.024c-.22-.136-.4-1.392-.4-2.79 0-1.399-.186-2.605-.412-2.681-.57-.19-.57-24.468 0-24.658.23-.077.412-1.368.412-2.928 0-1.758.148-2.699.4-2.543.247.152.4-.529.4-1.776 0-1.113.145-2.024.322-2.024.177 0 .366-.765.421-1.7.055-.935.27-1.871.478-2.08.209-.209.379-.873.379-1.476s.18-1.208.4-1.344c1.425-.881-.103-2.513-34.997-37.405-23.071-23.07-35.712-35.433-36.3-35.504-.745-.09-1.096.296-2.004 2.2"/></g></svg>
\ No newline at end of file
diff --git a/flutter/assets/auth-apple.svg b/flutter/assets/auth-apple.svg
new file mode 100644
index 000000000..6933fbc3b
--- /dev/null
+++ b/flutter/assets/auth-apple.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024"><circle cx="512" cy="512" r="512" fill="#999"/><path fill="#fff" d="M407.2 722.1c-10.1-6.7-19-15-26.5-24.5-8.2-9.9-15.7-20.3-22.7-31-16.3-23.9-29.1-50-38-77.5-10.7-32-15.8-62.7-15.8-92.7 0-33.5 7.2-62.7 21.4-87.2 10.4-19.2 26-35.2 44.8-46.5 18.1-11.3 39.2-17.5 60.6-17.9 7.5 0 15.6 1.1 24.1 3.2 6.2 1.7 13.6 4.5 22.8 7.9 11.7 4.5 18.1 7.2 20.3 7.9 6.8 2.6 12.6 3.6 17.1 3.6 3.4 0 8.3-1.1 13.8-2.8 3.1-1.1 9-3 17.3-6.6 8.2-3 14.8-5.5 19.9-7.5 7.9-2.3 15.5-4.5 22.4-5.5 8.3-1.3 16.6-1.7 24.5-1.1 15.1 1.1 29 4.3 41.4 9 21.7 8.7 39.3 22.4 52.4 41.8-5.5 3.4-10.7 7.4-15.5 11.7-10.4 9.2-19.2 20-26.2 32.1-9.2 16.4-13.9 35-13.7 53.7.3 23.1 6.2 43.4 17.9 61 8.3 12.8 19.3 23.8 32.7 32.7 6.6 4.5 12.4 7.6 17.9 9.6-2.6 8-5.4 15.8-8.6 23.5-7.4 17.2-16.2 33.7-26.7 49.3-9.2 13.4-16.5 23.5-22 30.1-8.6 10.2-16.8 17.9-25.2 23.4-9.2 6.1-19.9 9.3-31 9.3-7.5.3-14.9-.6-22-2.7-6.2-2-12.3-4.3-18.3-6.9-6.2-2.9-12.7-5.3-19.3-7.2-8.1-2.1-16.4-3.2-24.8-3.1-8.5 0-16.8 1.1-24.7 3.1-6.6 1.9-13 4.2-19.3 6.9-9 3.7-14.8 6.2-18.2 7.2-6.9 2-14 3.3-21.1 3.7-11.1 0-21.4-3.2-31.7-9.6zm146.1-393.6c-14.5 7.2-28.3 10.3-42.1 9.3-2.1-13.8 0-27.9 5.8-43.4 5.1-13.2 11.9-25.2 21.3-35.8 9.8-11.1 21.5-20.3 34.8-26.9 14.1-7.2 27.5-11.1 40.3-11.7 1.7 14.5 0 28.8-5.3 44.1-4.9 13.6-12.1 26.2-21.3 37.5-9.3 11.1-20.8 20.3-33.8 26.9z"/></svg>
\ No newline at end of file
diff --git a/flutter/assets/auth-auth0.svg b/flutter/assets/auth-auth0.svg
new file mode 100644
index 000000000..dbe3ed236
--- /dev/null
+++ b/flutter/assets/auth-auth0.svg
@@ -0,0 +1 @@
+<svg width="32" height="32"><path d="M29.307 9.932 26.161 0H5.796L2.692 9.932c-1.802 5.75.042 12.271 5.089 16.021L16.01 32l8.208-6.068c5.005-3.75 6.911-10.25 5.089-16.021l-8.214 6.104 3.12 9.938-8.208-6.13-8.208 6.104 3.141-9.911-8.25-6.063 10.177-.063 3.146-9.891 3.141 9.87z"/></svg>
\ No newline at end of file
diff --git a/flutter/assets/auth-azure.svg b/flutter/assets/auth-azure.svg
new file mode 100644
index 000000000..b7435604d
--- /dev/null
+++ b/flutter/assets/auth-azure.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="256" height="199"><path fill="#0089d6" d="M118.432 187.698c32.89-5.81 60.055-10.618 60.367-10.684l.568-.12-31.052-36.935c-17.078-20.314-31.051-37.014-31.051-37.11 0-.182 32.063-88.477 32.243-88.792.06-.105 21.88 37.567 52.893 91.32 29.035 50.323 52.973 91.815 53.195 92.203l.405.707-98.684-.012-98.684-.013 59.8-10.564zM0 176.435c0-.052 14.631-25.451 32.514-56.442l32.514-56.347 37.891-31.799C123.76 14.358 140.867.027 140.935.001c.069-.026-.205.664-.609 1.534s-18.919 40.582-41.145 88.25l-40.41 86.67-29.386.037c-16.162.02-29.385-.005-29.385-.057z"/></svg>
\ No newline at end of file
diff --git a/flutter/assets/auth-default.svg b/flutter/assets/auth-default.svg
new file mode 100644
index 000000000..bf5fa9073
--- /dev/null
+++ b/flutter/assets/auth-default.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="120" height="120"><path d="M142.554 52.81c0-4.113 1.078-6.374 5.369-11.26 17.207-19.593 57.193-19.593 74.4 0 4.291 4.886 5.37 7.147 5.37 11.26v5.145h-85.14zm71.239-42.863 6.676-6.692 10.462 10.74 25.49-25.453 6.133 6.543-31.536 32.356-17.225-17.494Zm-34.474 3.377c-15.027-5.337-19.348-22.264-8.57-33.575 10.85-11.387 29.85-6.099 34.149 9.503 2.523 9.161-4.38 21.951-12.951 23.995-4.39 1.58-8.73 1.433-12.628.077z" style="fill:#024eff;fill-opacity:1;stroke-width:.999998" transform="translate(-142.554 44.365)"/></svg>
\ No newline at end of file
diff --git a/flutter/assets/auth-facebook.svg b/flutter/assets/auth-facebook.svg
new file mode 100644
index 000000000..f58725000
--- /dev/null
+++ b/flutter/assets/auth-facebook.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><rect width="512" height="512" fill="#1877f2" rx="76.8"/><path fill="#fff" d="m355.6 330 11.4-74h-71v-48c0-20.2 9.9-40 41.7-40H370v-63s-29.3-5-57.3-5c-58.5 0-96.7 35.4-96.7 99.6V256h-65v74h65v182h80V330z"/></svg>
\ No newline at end of file
diff --git a/flutter/assets/auth-github.svg b/flutter/assets/auth-github.svg
new file mode 100644
index 000000000..778b7b341
--- /dev/null
+++ b/flutter/assets/auth-github.svg
@@ -0,0 +1 @@
+<svg viewBox="0 0 24 24"><path fill="#231f20" d="M12 1A10.89 10.89 0 0 0 1 11.77 10.79 10.79 0 0 0 8.52 22c.55.1.75-.23.75-.52v-1.83c-3.06.65-3.71-1.44-3.71-1.44a2.86 2.86 0 0 0-1.22-1.58c-1-.66.08-.65.08-.65a2.31 2.31 0 0 1 1.68 1.11 2.37 2.37 0 0 0 3.2.89 2.33 2.33 0 0 1 .7-1.44c-2.44-.27-5-1.19-5-5.32a4.15 4.15 0 0 1 1.11-2.91 3.78 3.78 0 0 1 .11-2.84s.93-.29 3 1.1a10.68 10.68 0 0 1 5.5 0c2.1-1.39 3-1.1 3-1.1a3.78 3.78 0 0 1 .11 2.84A4.15 4.15 0 0 1 19 11.2c0 4.14-2.58 5.05-5 5.32a2.5 2.5 0 0 1 .75 2v2.95s.2.63.75.52A10.8 10.8 0 0 0 23 11.77 10.89 10.89 0 0 0 12 1"/></svg>
\ No newline at end of file
diff --git a/flutter/assets/auth-gitlab.svg b/flutter/assets/auth-gitlab.svg
new file mode 100644
index 000000000..9402e1329
--- /dev/null
+++ b/flutter/assets/auth-gitlab.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="256" height="236" preserveAspectRatio="xMidYMid"><path fill="#e24329" d="m128.075 236.075 47.104-144.97H80.97z"/><path fill="#fc6d26" d="M128.075 236.074 80.97 91.104H14.956z"/><path fill="#fca326" d="M14.956 91.104.642 135.16a9.752 9.752 0 0 0 3.542 10.903l123.891 90.012z"/><path fill="#e24329" d="M14.956 91.105H80.97L52.601 3.79c-1.46-4.493-7.816-4.492-9.275 0z"/><path fill="#fc6d26" d="m128.075 236.074 47.104-144.97h66.015z"/><path fill="#fca326" d="m241.194 91.104 14.314 44.056a9.752 9.752 0 0 1-3.543 10.903l-123.89 90.012z"/><path fill="#e24329" d="M241.194 91.105h-66.015l28.37-87.315c1.46-4.493 7.816-4.492 9.275 0z"/></svg>
\ No newline at end of file
diff --git a/flutter/assets/auth-google.svg b/flutter/assets/auth-google.svg
new file mode 100644
index 000000000..18970f31a
--- /dev/null
+++ b/flutter/assets/auth-google.svg
@@ -0,0 +1 @@
+<svg width="48" height="48"><path fill="#ffc107" d="M43.611 20.083H42V20H24v8h11.303c-1.649 4.657-6.08 8-11.303 8-6.627 0-12-5.373-12-12s5.373-12 12-12c3.059 0 5.842 1.154 7.961 3.039l5.657-5.657C34.046 6.053 29.268 4 24 4 12.955 4 4 12.955 4 24s8.955 20 20 20 20-8.955 20-20c0-1.341-.138-2.65-.389-3.917z"/><path fill="#ff3d00" d="m6.306 14.691 6.571 4.819C14.655 15.108 18.961 12 24 12c3.059 0 5.842 1.154 7.961 3.039l5.657-5.657C34.046 6.053 29.268 4 24 4 16.318 4 9.656 8.337 6.306 14.691z"/><path fill="#4caf50" d="M24 44c5.166 0 9.86-1.977 13.409-5.192l-6.19-5.238A11.91 11.91 0 0 1 24 36c-5.202 0-9.619-3.317-11.283-7.946l-6.522 5.025C9.505 39.556 16.227 44 24 44z"/><path fill="#1976d2" d="M43.611 20.083H42V20H24v8h11.303a12.04 12.04 0 0 1-4.087 5.571l.003-.002 6.19 5.238C36.971 39.205 44 34 44 24c0-1.341-.138-2.65-.389-3.917z"/></svg>
\ No newline at end of file
diff --git a/flutter/assets/Okta.svg b/flutter/assets/auth-okta.svg
similarity index 100%
rename from flutter/assets/Okta.svg
rename to flutter/assets/auth-okta.svg
diff --git a/flutter/assets/checkbox.ttf b/flutter/assets/checkbox.ttf
new file mode 100644
index 000000000..70ddde698
Binary files /dev/null and b/flutter/assets/checkbox.ttf differ
diff --git a/flutter/assets/file_transfer.svg b/flutter/assets/file_transfer.svg
new file mode 100644
index 000000000..e1d8ccbec
--- /dev/null
+++ b/flutter/assets/file_transfer.svg
@@ -0,0 +1 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1694049173782" class="icon" viewBox="0 0 1024 1024" width="24" height="24" fill="#fff" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="992" xmlns:xlink="http://www.w3.org/1999/xlink" width="32" height="32"><path d="M891.64 184.73H620.41c-27.41 0-54.41-7.77-77.32-22.5L428.13 87.36C402.77 71 372.91 62 342.64 62H131.95C93.5 62 62 93.5 62 132.36v759.68C62 930.91 93.5 962 131.95 962h759.68c38.86 0 70.36-31.09 70.36-69.96V255.09c0.01-38.86-31.49-70.36-70.35-70.36zM480.5 753.77c0 16.77-13.5 30.68-30.68 30.68-16.77 0-30.68-13.91-30.68-30.68V523.04l-31.91 55.64c-8.59 14.32-27.41 19.64-42.14 11.04-14.32-8.59-19.64-27.41-11.05-41.73l89.18-154.64c6.96-12.27 21.27-18 34.77-14.32 13.09 3.27 22.5 15.55 22.5 29.45v345.29z m209.04-139.5l-89.18 154.64c-5.32 9.82-15.55 15.55-26.59 15.55-2.46 0-5.32-0.41-7.77-1.23-13.5-3.68-22.91-15.55-22.91-29.46V408.5c0-16.77 13.91-30.68 30.68-30.68 17.18 0 30.68 13.91 30.68 30.68v230.73l31.91-55.64c8.59-14.73 27.41-19.64 42.14-11.05 14.73 8.6 19.64 27.01 11.04 41.73z" p-id="993"></path></svg>
\ No newline at end of file
diff --git a/flutter/build_ios.sh b/flutter/build_ios.sh
index 6d0d627ac..a6468a0a8 100755
--- a/flutter/build_ios.sh
+++ b/flutter/build_ios.sh
@@ -1,2 +1,5 @@
 #!/usr/bin/env bash
-flutter build ipa --release --obfuscate --split-debug-info=./split-debug-info
+# https://docs.flutter.dev/deployment/ios
+# flutter build ipa --release --obfuscate --split-debug-info=./split-debug-info
+# no obfuscate, because no easy to check errors
+flutter build ipa --release
diff --git a/flutter/ios/Podfile.lock b/flutter/ios/Podfile.lock
index 76d0bac73..1ad5f6360 100644
--- a/flutter/ios/Podfile.lock
+++ b/flutter/ios/Podfile.lock
@@ -75,7 +75,7 @@ DEPENDENCIES:
   - flutter_keyboard_visibility (from `.symlinks/plugins/flutter_keyboard_visibility/ios`)
   - image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`)
   - package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
-  - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/ios`)
+  - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
   - qr_code_scanner (from `.symlinks/plugins/qr_code_scanner/ios`)
   - sqflite (from `.symlinks/plugins/sqflite/ios`)
   - uni_links (from `.symlinks/plugins/uni_links/ios`)
@@ -106,7 +106,7 @@ EXTERNAL SOURCES:
   package_info_plus:
     :path: ".symlinks/plugins/package_info_plus/ios"
   path_provider_foundation:
-    :path: ".symlinks/plugins/path_provider_foundation/ios"
+    :path: ".symlinks/plugins/path_provider_foundation/darwin"
   qr_code_scanner:
     :path: ".symlinks/plugins/qr_code_scanner/ios"
   sqflite:
@@ -141,6 +141,6 @@ SPEC CHECKSUMS:
   video_player_avfoundation: e489aac24ef5cf7af82702979ed16f2a5ef84cff
   wakelock: d0fc7c864128eac40eba1617cb5264d9c940b46f
 
-PODFILE CHECKSUM: c649b4e69a3086d323110011d04604e416ad0dcd
+PODFILE CHECKSUM: 2aff76ba0ac13439479560d1d03e9b4479f5c9e1
 
-COCOAPODS: 1.12.0
+COCOAPODS: 1.12.1
diff --git a/flutter/ios/Runner.xcodeproj/project.pbxproj b/flutter/ios/Runner.xcodeproj/project.pbxproj
index a3bc7d43d..0813abb11 100644
--- a/flutter/ios/Runner.xcodeproj/project.pbxproj
+++ b/flutter/ios/Runner.xcodeproj/project.pbxproj
@@ -208,6 +208,7 @@
 			files = (
 			);
 			inputPaths = (
+				"${TARGET_BUILD_DIR}/${INFOPLIST_PATH}",
 			);
 			name = "Thin Binary";
 			outputPaths = (
@@ -437,6 +438,7 @@
 				);
 				PRODUCT_BUNDLE_IDENTIFIER = com.carriez.flutterHbb;
 				PRODUCT_NAME = "$(TARGET_NAME)";
+				STRIP_STYLE = "non-global";
 				SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
 				SWIFT_VERSION = 5.0;
 				VERSIONING_SYSTEM = "apple-generic";
@@ -634,6 +636,7 @@
 				);
 				PRODUCT_BUNDLE_IDENTIFIER = com.carriez.flutterHbb;
 				PRODUCT_NAME = "$(TARGET_NAME)";
+				STRIP_STYLE = "non-global";
 				SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
 				SWIFT_OPTIMIZATION_LEVEL = "-Onone";
 				SWIFT_VERSION = 5.0;
@@ -723,6 +726,7 @@
 				);
 				PRODUCT_BUNDLE_IDENTIFIER = com.carriez.flutterHbb;
 				PRODUCT_NAME = "$(TARGET_NAME)";
+				STRIP_STYLE = "non-global";
 				SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
 				SWIFT_VERSION = 5.0;
 				VERSIONING_SYSTEM = "apple-generic";
diff --git a/flutter/ios/Runner/AppDelegate.swift b/flutter/ios/Runner/AppDelegate.swift
index 06a9a2695..730c9adcb 100644
--- a/flutter/ios/Runner/AppDelegate.swift
+++ b/flutter/ios/Runner/AppDelegate.swift
@@ -13,9 +13,7 @@ import Flutter
   }
     
   public func dummyMethodToEnforceBundling() {
-    get_rgba();
-  //  free_rgba(nil);
-  //  get_by_name("", "");
-  //  set_by_name("", "");
+      dummy_method_to_enforce_bundling();
+    session_get_rgba(nil);
   }
 }
diff --git a/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json
index eabd8512d..53611299a 100644
--- a/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json
+++ b/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json
@@ -1,122 +1,122 @@
 {
-  "images": [
+  "images" : [
     {
-      "filename": "Icon-App-20x20@2x.png",
-      "idiom": "iphone",
-      "scale": "2x",
-      "size": "20x20"
+      "filename" : "Icon-App-20x20@2x.png",
+      "idiom" : "iphone",
+      "scale" : "2x",
+      "size" : "20x20"
     },
     {
-      "filename": "Icon-App-20x20@3x.png",
-      "idiom": "iphone",
-      "scale": "3x",
-      "size": "20x20"
+      "filename" : "Icon-App-20x20@3x.png",
+      "idiom" : "iphone",
+      "scale" : "3x",
+      "size" : "20x20"
     },
     {
-      "filename": "Icon-App-29x29@1x.png",
-      "idiom": "iphone",
-      "scale": "1x",
-      "size": "29x29"
+      "filename" : "Icon-App-29x29@1x.png",
+      "idiom" : "iphone",
+      "scale" : "1x",
+      "size" : "29x29"
     },
     {
-      "filename": "Icon-App-29x29@2x.png",
-      "idiom": "iphone",
-      "scale": "2x",
-      "size": "29x29"
+      "filename" : "Icon-App-29x29@2x.png",
+      "idiom" : "iphone",
+      "scale" : "2x",
+      "size" : "29x29"
     },
     {
-      "filename": "Icon-App-29x29@3x.png",
-      "idiom": "iphone",
-      "scale": "3x",
-      "size": "29x29"
+      "filename" : "Icon-App-29x29@3x.png",
+      "idiom" : "iphone",
+      "scale" : "3x",
+      "size" : "29x29"
     },
     {
-      "filename": "Icon-App-40x40@2x.png",
-      "idiom": "iphone",
-      "scale": "2x",
-      "size": "40x40"
+      "filename" : "Icon-App-40x40@2x.png",
+      "idiom" : "iphone",
+      "scale" : "2x",
+      "size" : "40x40"
     },
     {
-      "filename": "Icon-App-40x40@3x.png",
-      "idiom": "iphone",
-      "scale": "3x",
-      "size": "40x40"
+      "filename" : "Icon-App-40x40@3x.png",
+      "idiom" : "iphone",
+      "scale" : "3x",
+      "size" : "40x40"
     },
     {
-      "filename": "Icon-App-60x60@2x.png",
-      "idiom": "iphone",
-      "scale": "2x",
-      "size": "60x60"
+      "filename" : "Icon-App-60x60@2x.png",
+      "idiom" : "iphone",
+      "scale" : "2x",
+      "size" : "60x60"
     },
     {
-      "filename": "Icon-App-60x60@3x.png",
-      "idiom": "iphone",
-      "scale": "3x",
-      "size": "60x60"
+      "filename" : "Icon-App-60x60@3x.png",
+      "idiom" : "iphone",
+      "scale" : "3x",
+      "size" : "60x60"
     },
     {
-      "filename": "Icon-App-20x20@1x.png",
-      "idiom": "ipad",
-      "scale": "1x",
-      "size": "20x20"
+      "filename" : "Icon-App-20x20@1x.png",
+      "idiom" : "ipad",
+      "scale" : "1x",
+      "size" : "20x20"
     },
     {
-      "filename": "Icon-App-20x20@2x.png",
-      "idiom": "ipad",
-      "scale": "2x",
-      "size": "20x20"
+      "filename" : "Icon-App-20x20@2x.png",
+      "idiom" : "ipad",
+      "scale" : "2x",
+      "size" : "20x20"
     },
     {
-      "filename": "Icon-App-29x29@1x.png",
-      "idiom": "ipad",
-      "scale": "1x",
-      "size": "29x29"
+      "filename" : "Icon-App-29x29@1x.png",
+      "idiom" : "ipad",
+      "scale" : "1x",
+      "size" : "29x29"
     },
     {
-      "filename": "Icon-App-29x29@2x.png",
-      "idiom": "ipad",
-      "scale": "2x",
-      "size": "29x29"
+      "filename" : "Icon-App-29x29@2x.png",
+      "idiom" : "ipad",
+      "scale" : "2x",
+      "size" : "29x29"
     },
     {
-      "filename": "Icon-App-40x40@1x.png",
-      "idiom": "ipad",
-      "scale": "1x",
-      "size": "40x40"
+      "filename" : "Icon-App-40x40@1x.png",
+      "idiom" : "ipad",
+      "scale" : "1x",
+      "size" : "40x40"
     },
     {
-      "filename": "Icon-App-40x40@2x.png",
-      "idiom": "ipad",
-      "scale": "2x",
-      "size": "40x40"
+      "filename" : "Icon-App-40x40@2x.png",
+      "idiom" : "ipad",
+      "scale" : "2x",
+      "size" : "40x40"
     },
     {
-      "filename": "Icon-App-76x76@1x.png",
-      "idiom": "ipad",
-      "scale": "1x",
-      "size": "76x76"
+      "filename" : "Icon-App-76x76@1x.png",
+      "idiom" : "ipad",
+      "scale" : "1x",
+      "size" : "76x76"
     },
     {
-      "filename": "Icon-App-76x76@2x.png",
-      "idiom": "ipad",
-      "scale": "2x",
-      "size": "76x76"
+      "filename" : "Icon-App-76x76@2x.png",
+      "idiom" : "ipad",
+      "scale" : "2x",
+      "size" : "76x76"
     },
     {
-      "filename": "Icon-App-83.5x83.5@2x.png",
-      "idiom": "ipad",
-      "scale": "2x",
-      "size": "83.5x83.5"
+      "filename" : "Icon-App-83.5x83.5@2x.png",
+      "idiom" : "ipad",
+      "scale" : "2x",
+      "size" : "83.5x83.5"
     },
     {
-      "filename": "Icon-App-1024x1024@1x.png",
-      "idiom": "ios-marketing",
-      "scale": "1x",
-      "size": "1024x1024"
+      "filename" : "Icon-App-1024x1024@1x.png",
+      "idiom" : "ios-marketing",
+      "scale" : "1x",
+      "size" : "1024x1024"
     }
   ],
-  "info": {
-    "author": "icons_launcher",
-    "version": 1
+  "info" : {
+    "author" : "xcode",
+    "version" : 1
   }
-}
\ No newline at end of file
+}
diff --git a/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png
index 16cef3177..fa4ecad0a 100644
Binary files a/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png and b/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png differ
diff --git a/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png
index 298f4d9af..080f311fc 100644
Binary files a/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png and b/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png differ
diff --git a/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png
index fd3b01b6d..8daf5718b 100644
Binary files a/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png and b/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png differ
diff --git a/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png
index 18ebaab69..fa5b04f7f 100644
Binary files a/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png and b/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png differ
diff --git a/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png
index a8ee14a31..a4bc6dfd7 100644
Binary files a/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png and b/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png differ
diff --git a/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png
index a83f88b05..97831dc82 100644
Binary files a/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png and b/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png differ
diff --git a/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png
index 331e72531..cfaff2b9b 100644
Binary files a/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png and b/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png differ
diff --git a/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png
index fd3b01b6d..8daf5718b 100644
Binary files a/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png and b/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png differ
diff --git a/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png
index aee7e4321..85365e185 100644
Binary files a/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png and b/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png differ
diff --git a/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png
index 2d0da17b1..bafaf7e07 100644
Binary files a/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png and b/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png differ
diff --git a/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png
index 2d0da17b1..bafaf7e07 100644
Binary files a/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png and b/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png differ
diff --git a/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png
index 7ee56922e..e5755e5fb 100644
Binary files a/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png and b/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png differ
diff --git a/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png
index 76abd423b..b60a754df 100644
Binary files a/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png and b/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png differ
diff --git a/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png
index e08138333..0ddd120e6 100644
Binary files a/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png and b/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png differ
diff --git a/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png
index 46de51af6..22628c536 100644
Binary files a/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png and b/flutter/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png differ
diff --git a/flutter/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json b/flutter/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json
index 0bedcf2fd..00cabce83 100644
--- a/flutter/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json
+++ b/flutter/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json
@@ -1,23 +1,23 @@
 {
   "images" : [
     {
-      "idiom" : "universal",
       "filename" : "LaunchImage.png",
+      "idiom" : "universal",
       "scale" : "1x"
     },
     {
-      "idiom" : "universal",
       "filename" : "LaunchImage@2x.png",
+      "idiom" : "universal",
       "scale" : "2x"
     },
     {
-      "idiom" : "universal",
       "filename" : "LaunchImage@3x.png",
+      "idiom" : "universal",
       "scale" : "3x"
     }
   ],
   "info" : {
-    "version" : 1,
-    "author" : "xcode"
+    "author" : "xcode",
+    "version" : 1
   }
 }
diff --git a/flutter/ios/Runner/Base.lproj/Main.storyboard b/flutter/ios/Runner/Base.lproj/Main.storyboard
index f3c28516f..d68a3a7a5 100644
--- a/flutter/ios/Runner/Base.lproj/Main.storyboard
+++ b/flutter/ios/Runner/Base.lproj/Main.storyboard
@@ -1,8 +1,10 @@
-<?xml version="1.0" encoding="UTF-8" standalone="no"?>
-<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="10117" systemVersion="15F34" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" initialViewController="BYZ-38-t0r">
+<?xml version="1.0" encoding="UTF-8"?>
+<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="21701" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES" initialViewController="BYZ-38-t0r">
+    <device id="retina6_12" orientation="portrait" appearance="light"/>
     <dependencies>
         <deployment identifier="iOS"/>
-        <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="10085"/>
+        <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="21679"/>
+        <capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
     </dependencies>
     <scenes>
         <!--Flutter View Controller-->
@@ -14,13 +16,14 @@
                         <viewControllerLayoutGuide type="bottom" id="wfy-db-euE"/>
                     </layoutGuides>
                     <view key="view" contentMode="scaleToFill" id="8bC-Xf-vdC">
-                        <rect key="frame" x="0.0" y="0.0" width="600" height="600"/>
+                        <rect key="frame" x="0.0" y="0.0" width="393" height="852"/>
                         <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
-                        <color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="calibratedWhite"/>
+                        <color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
                     </view>
                 </viewController>
                 <placeholder placeholderIdentifier="IBFirstResponder" id="dkx-z0-nzr" sceneMemberID="firstResponder"/>
             </objects>
+            <point key="canvasLocation" x="48" y="-2"/>
         </scene>
     </scenes>
 </document>
diff --git a/flutter/ios/Runner/Runner-Bridging-Header.h b/flutter/ios/Runner/Runner-Bridging-Header.h
index a8c447418..e930a3997 100644
--- a/flutter/ios/Runner/Runner-Bridging-Header.h
+++ b/flutter/ios/Runner/Runner-Bridging-Header.h
@@ -1,3 +1,3 @@
 #import "GeneratedPluginRegistrant.h"
 
-#import "ffi.h"
+#import "bridge_generated.h"
diff --git a/flutter/ios/Runner/ffi.h b/flutter/ios/Runner/ffi.h
deleted file mode 100644
index 701ec4b09..000000000
--- a/flutter/ios/Runner/ffi.h
+++ /dev/null
@@ -1,4 +0,0 @@
-void* get_rgba();
-void free_rgba(void*);
-void set_by_name(const char*, const char*);
-const char* get_by_name(const char*, const char*);
diff --git a/flutter/lib/common.dart b/flutter/lib/common.dart
index 41fa826b8..33321c81a 100644
--- a/flutter/lib/common.dart
+++ b/flutter/lib/common.dart
@@ -11,15 +11,16 @@ import 'package:flutter/foundation.dart';
 import 'package:flutter/gestures.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter/services.dart';
+import 'package:flutter_hbb/common/formatter/id_formatter.dart';
 import 'package:flutter_hbb/desktop/widgets/refresh_wrapper.dart';
 import 'package:flutter_hbb/desktop/widgets/tabbar_widget.dart';
 import 'package:flutter_hbb/main.dart';
 import 'package:flutter_hbb/models/peer_model.dart';
+import 'package:flutter_hbb/models/state_model.dart';
 import 'package:flutter_hbb/utils/multi_window_manager.dart';
 import 'package:flutter_hbb/utils/platform_channel.dart';
 import 'package:flutter_svg/flutter_svg.dart';
 import 'package:get/get.dart';
-import 'package:texture_rgba_renderer/texture_rgba_renderer.dart';
 import 'package:uni_links/uni_links.dart';
 import 'package:url_launcher/url_launcher.dart';
 import 'package:uuid/uuid.dart';
@@ -47,11 +48,6 @@ var isMobile = isAndroid || isIOS;
 var version = "";
 int androidVersion = 0;
 
-/// Incriment count for textureId.
-int _textureId = 0;
-int get newTextureId => _textureId++;
-final textureRenderer = TextureRgbaRenderer();
-
 /// only available for Windows target
 int windowsBuildNumber = 0;
 DesktopType? desktopType;
@@ -95,6 +91,7 @@ class IconFont {
   static const IconData roundClose = IconData(0xe6ed, fontFamily: _family2);
   static const IconData addressBook =
       IconData(0xe602, fontFamily: "AddressBook");
+  static const IconData checkbox = IconData(0xe7d6, fontFamily: "CheckBox");
 }
 
 class ColorThemeExtension extends ThemeExtension<ColorThemeExtension> {
@@ -176,6 +173,10 @@ class MyTheme {
   static const Color dark = Colors.black87;
   static const Color button = Color(0xFF2C8CFF);
   static const Color hoverBorder = Color(0xFF999999);
+  static const Color bordDark = Colors.white24;
+  static const Color bordLight = Colors.black26;
+  static const Color dividerDark = Colors.white38;
+  static const Color dividerLight = Colors.black38;
 
   // ListTile
   static const ListTileThemeData listTileTheme = ListTileThemeData(
@@ -219,6 +220,13 @@ class MyTheme {
     ),
   );
 
+  //tooltip
+  static TooltipThemeData tooltipTheme() {
+    return TooltipThemeData(
+      waitDuration: Duration(seconds: 1, milliseconds: 500),
+    );
+  }
+
   // Dialogs
   static const double dialogPadding = 24;
 
@@ -288,6 +296,7 @@ class MyTheme {
     tabBarTheme: const TabBarTheme(
       labelColor: Colors.black87,
     ),
+    tooltipTheme: tooltipTheme(),
     splashColor: isDesktop ? Colors.transparent : null,
     highlightColor: isDesktop ? Colors.transparent : null,
     splashFactory: isDesktop ? NoSplash.splashFactory : null,
@@ -377,6 +386,7 @@ class MyTheme {
     scrollbarTheme: ScrollbarThemeData(
       thumbColor: MaterialStateProperty.all(Colors.grey[500]),
     ),
+    tooltipTheme: tooltipTheme(),
     splashColor: isDesktop ? Colors.transparent : null,
     highlightColor: isDesktop ? Colors.transparent : null,
     splashFactory: isDesktop ? NoSplash.splashFactory : null,
@@ -545,17 +555,19 @@ closeConnection({String? id}) {
   }
 }
 
-void window_on_top(int? id) {
+void windowOnTop(int? id) async {
   if (!isDesktop) {
     return;
   }
+  print("Bring window '$id' on top");
   if (id == null) {
-    print("Bring window on top");
     // main window
-    windowManager.restore();
-    windowManager.show();
-    windowManager.focus();
-    rustDeskWinManager.registerActiveWindow(kWindowMainId);
+    if (stateGlobal.isMinimized) {
+      await windowManager.restore();
+    }
+    await windowManager.show();
+    await windowManager.focus();
+    await rustDeskWinManager.registerActiveWindow(kWindowMainId);
   } else {
     WindowController.fromWindowId(id)
       ..focus()
@@ -602,6 +614,7 @@ class OverlayDialogManager {
   int _tagCount = 0;
 
   OverlayEntry? _mobileActionsOverlayEntry;
+  RxBool mobileActionsOverlayVisible = false.obs;
 
   void setOverlayState(OverlayKeyState overlayKeyState) {
     _overlayKeyState = overlayKeyState;
@@ -685,9 +698,12 @@ class OverlayDialogManager {
   String showLoading(String text,
       {bool clickMaskDismiss = false,
       bool showCancel = true,
-      VoidCallback? onCancel}) {
-    final tag = _tagCount.toString();
-    _tagCount++;
+      VoidCallback? onCancel,
+      String? tag}) {
+    if (tag == null) {
+      tag = _tagCount.toString();
+      _tagCount++;
+    }
     show((setState, close, context) {
       cancel() {
         dismissAll();
@@ -765,12 +781,14 @@ class OverlayDialogManager {
     });
     overlayState.insert(overlay);
     _mobileActionsOverlayEntry = overlay;
+    mobileActionsOverlayVisible.value = true;
   }
 
   void hideMobileActionsOverlay() {
     if (_mobileActionsOverlayEntry != null) {
       _mobileActionsOverlayEntry!.remove();
       _mobileActionsOverlayEntry = null;
+      mobileActionsOverlayVisible.value = false;
       return;
     }
   }
@@ -1062,6 +1080,45 @@ Color str2color(String str, [alpha = 0xFF]) {
   return Color((hash & 0xFF7FFF) | (alpha << 24));
 }
 
+Color str2color2(String str, {List<int> existing = const []}) {
+  Map<String, Color> colorMap = {
+    "red": Colors.red,
+    "green": Colors.green,
+    "blue": Colors.blue,
+    "orange": Colors.orange,
+    "purple": Colors.purple,
+    "grey": Colors.grey,
+    "cyan": Colors.cyan,
+    "lime": Colors.lime,
+    "teal": Colors.teal,
+    "pink": Colors.pink[200]!,
+    "indigo": Colors.indigo,
+    "brown": Colors.brown,
+  };
+  final color = colorMap[str.toLowerCase()];
+  if (color != null) {
+    return color.withAlpha(0xFF);
+  }
+  if (str.toLowerCase() == 'yellow') {
+    return Colors.yellow.withAlpha(0xFF);
+  }
+  var hash = 0;
+  for (var i = 0; i < str.length; i++) {
+    hash += str.codeUnitAt(i);
+  }
+  List<Color> colorList = colorMap.values.toList();
+  hash = hash % colorList.length;
+  var result = colorList[hash].withAlpha(0xFF);
+  if (existing.contains(result.value)) {
+    Color? notUsed =
+        colorList.firstWhereOrNull((e) => !existing.contains(e.value));
+    if (notUsed != null) {
+      result = notUsed;
+    }
+  }
+  return result;
+}
+
 const K = 1024;
 const M = K * K;
 const G = M * K;
@@ -1218,7 +1275,7 @@ FFI get gFFI => _globalFFI;
 
 Future<void> initGlobalFFI() async {
   debugPrint("_globalFFI init");
-  _globalFFI = FFI();
+  _globalFFI = FFI(null);
   debugPrint("_globalFFI init end");
   // after `put`, can also be globally found by Get.find<FFI>();
   Get.put(_globalFFI, permanent: true);
@@ -1239,7 +1296,7 @@ bool option2bool(String option, String value) {
       option == "stop-service" ||
       option == "direct-server" ||
       option == "stop-rendezvous-service" ||
-      option == "force-always-relay") {
+      option == kOptionForceAlwaysRelay) {
     res = value == "Y";
   } else {
     assert(false);
@@ -1256,7 +1313,7 @@ String bool2option(String option, bool b) {
       option == "stop-service" ||
       option == "direct-server" ||
       option == "stop-rendezvous-service" ||
-      option == "force-always-relay") {
+      option == kOptionForceAlwaysRelay) {
     res = b ? 'Y' : '';
   } else {
     assert(false);
@@ -1265,6 +1322,36 @@ String bool2option(String option, bool b) {
   return res;
 }
 
+mainSetBoolOption(String key, bool value) async {
+  String v = bool2option(key, value);
+  await bind.mainSetOption(key: key, value: v);
+}
+
+Future<bool> mainGetBoolOption(String key) async {
+  return option2bool(key, await bind.mainGetOption(key: key));
+}
+
+bool mainGetBoolOptionSync(String key) {
+  return option2bool(key, bind.mainGetOptionSync(key: key));
+}
+
+mainSetLocalBoolOption(String key, bool value) async {
+  String v = bool2option(key, value);
+  await bind.mainSetLocalOption(key: key, value: v);
+}
+
+bool mainGetLocalBoolOptionSync(String key) {
+  return option2bool(key, bind.mainGetLocalOption(key: key));
+}
+
+bool mainGetPeerBoolOptionSync(String id, String key) {
+  return option2bool(key, bind.mainGetPeerOptionSync(id: id, key: key));
+}
+
+mainSetPeerBoolOptionSync(String id, String key, bool v) {
+  bind.mainSetPeerOptionSync(id: id, key: key, value: bool2option(key, v));
+}
+
 Future<bool> matchPeer(String searchText, Peer peer) async {
   if (searchText.isEmpty) {
     return true;
@@ -1276,7 +1363,7 @@ Future<bool> matchPeer(String searchText, Peer peer) async {
       peer.username.toLowerCase().contains(searchText)) {
     return true;
   }
-  final alias = await bind.mainGetPeerOption(id: peer.id, key: 'alias');
+  final alias = peer.alias;
   if (alias.isEmpty) {
     return false;
   }
@@ -1305,9 +1392,10 @@ class LastWindowPosition {
   double? offsetWidth;
   double? offsetHeight;
   bool? isMaximized;
+  bool? isFullscreen;
 
   LastWindowPosition(this.width, this.height, this.offsetWidth,
-      this.offsetHeight, this.isMaximized);
+      this.offsetHeight, this.isMaximized, this.isFullscreen);
 
   Map<String, dynamic> toJson() {
     return <String, dynamic>{
@@ -1316,6 +1404,7 @@ class LastWindowPosition {
       "offsetWidth": offsetWidth,
       "offsetHeight": offsetHeight,
       "isMaximized": isMaximized,
+      "isFullscreen": isFullscreen,
     };
   }
 
@@ -1331,9 +1420,11 @@ class LastWindowPosition {
     try {
       final m = jsonDecode(content);
       return LastWindowPosition(m["width"], m["height"], m["offsetWidth"],
-          m["offsetHeight"], m["isMaximized"]);
+          m["offsetHeight"], m["isMaximized"], m["isFullscreen"]);
     } catch (e) {
-      debugPrintStack(label: e.toString());
+      debugPrintStack(
+          label:
+              'Failed to load LastWindowPosition "$content" ${e.toString()}');
       return null;
     }
   }
@@ -1346,18 +1437,32 @@ Future<void> saveWindowPosition(WindowType type, {int? windowId}) async {
     debugPrint(
         "Error: windowId cannot be null when saving positions for sub window");
   }
+
+  late Offset position;
+  late Size sz;
+  late bool isMaximized;
+  bool isFullscreen = stateGlobal.fullscreen ||
+      (Platform.isMacOS && stateGlobal.closeOnFullscreen);
+  setFrameIfMaximized() {
+    if (isMaximized) {
+      final pos = bind.getLocalFlutterOption(k: kWindowPrefix + type.name);
+      var lpos = LastWindowPosition.loadFromString(pos);
+      position = Offset(
+          lpos?.offsetWidth ?? position.dx, lpos?.offsetHeight ?? position.dy);
+      sz = Size(lpos?.width ?? sz.width, lpos?.height ?? sz.height);
+    }
+  }
+
   switch (type) {
     case WindowType.Main:
-      final position = await windowManager.getPosition();
-      final sz = await windowManager.getSize();
-      final isMaximized = await windowManager.isMaximized();
-      final pos = LastWindowPosition(
-          sz.width, sz.height, position.dx, position.dy, isMaximized);
-      await bind.setLocalFlutterConfig(
-          k: kWindowPrefix + type.name, v: pos.toString());
+      isMaximized = await windowManager.isMaximized();
+      position = await windowManager.getPosition();
+      sz = await windowManager.getSize();
+      setFrameIfMaximized();
       break;
     default:
       final wc = WindowController.fromWindowId(windowId!);
+      isMaximized = await wc.isMaximized();
       final Rect frame;
       try {
         frame = await wc.getFrame();
@@ -1365,38 +1470,74 @@ Future<void> saveWindowPosition(WindowType type, {int? windowId}) async {
         debugPrint("Failed to get frame of window $windowId, it may be hidden");
         return;
       }
-      final position = frame.topLeft;
-      final sz = frame.size;
-      final isMaximized = await wc.isMaximized();
-      final pos = LastWindowPosition(
-          sz.width, sz.height, position.dx, position.dy, isMaximized);
-      debugPrint(
-          "saving frame: $windowId: ${pos.width}/${pos.height}, offset:${pos.offsetWidth}/${pos.offsetHeight}");
-      await bind.setLocalFlutterConfig(
-          k: kWindowPrefix + type.name, v: pos.toString());
+      position = frame.topLeft;
+      sz = frame.size;
+      setFrameIfMaximized();
       break;
   }
+  if (Platform.isWindows) {
+    const kMinOffset = -10000;
+    const kMaxOffset = 10000;
+    if (position.dx < kMinOffset ||
+        position.dy < kMinOffset ||
+        position.dx > kMaxOffset ||
+        position.dy > kMaxOffset) {
+      debugPrint("Invalid position: $position, ignore saving position");
+      return;
+    }
+  }
+
+  final pos = LastWindowPosition(
+      sz.width, sz.height, position.dx, position.dy, isMaximized, isFullscreen);
+  debugPrint(
+      "Saving frame: $windowId: ${pos.width}/${pos.height}, offset:${pos.offsetWidth}/${pos.offsetHeight}, isMaximized:${pos.isMaximized}, isFullscreen:${pos.isFullscreen}");
+
+  await bind.setLocalFlutterOption(
+      k: kWindowPrefix + type.name, v: pos.toString());
+
+  if (type == WindowType.RemoteDesktop && windowId != null) {
+    await _saveSessionWindowPosition(
+        type, windowId, isMaximized, isFullscreen, pos);
+  }
+}
+
+Future _saveSessionWindowPosition(WindowType windowType, int windowId,
+    bool isMaximized, bool isFullscreen, LastWindowPosition pos) async {
+  final remoteList = await DesktopMultiWindow.invokeMethod(
+      windowId, kWindowEventGetRemoteList, null);
+  getPeerPos(String peerId) {
+    if (isMaximized) {
+      final peerPos = bind.mainGetPeerFlutterOptionSync(
+          id: peerId, k: kWindowPrefix + windowType.name);
+      var lpos = LastWindowPosition.loadFromString(peerPos);
+      return LastWindowPosition(
+              lpos?.width ?? pos.offsetWidth,
+              lpos?.height ?? pos.offsetHeight,
+              lpos?.offsetWidth ?? pos.offsetWidth,
+              lpos?.offsetHeight ?? pos.offsetHeight,
+              isMaximized,
+              isFullscreen)
+          .toString();
+    } else {
+      return pos.toString();
+    }
+  }
+
+  if (remoteList != null) {
+    for (final peerId in remoteList.split(',')) {
+      bind.mainSetPeerFlutterOptionSync(
+          id: peerId,
+          k: kWindowPrefix + windowType.name,
+          v: getPeerPos(peerId));
+    }
+  }
 }
 
 Future<Size> _adjustRestoreMainWindowSize(double? width, double? height) async {
-  const double minWidth = 600;
-  const double minHeight = 100;
-  double maxWidth = (((isDesktop || isWebDesktop)
-          ? kDesktopMaxDisplayWidth
-          : kMobileMaxDisplayWidth))
-      .toDouble();
-  double maxHeight = ((isDesktop || isWebDesktop)
-          ? kDesktopMaxDisplayHeight
-          : kMobileMaxDisplayHeight)
-      .toDouble();
-
-  if (isDesktop || isWebDesktop) {
-    final screen = (await window_size.getWindowInfo()).screen;
-    if (screen != null) {
-      maxWidth = screen.visibleFrame.width;
-      maxHeight = screen.visibleFrame.height;
-    }
-  }
+  const double minWidth = 1;
+  const double minHeight = 1;
+  const double maxWidth = 6480;
+  const double maxHeight = 6480;
 
   final defaultWidth =
       ((isDesktop || isWebDesktop) ? 1280 : kMobileDefaultDisplayWidth)
@@ -1408,64 +1549,79 @@ Future<Size> _adjustRestoreMainWindowSize(double? width, double? height) async {
   double restoreHeight = height ?? defaultHeight;
 
   if (restoreWidth < minWidth) {
-    restoreWidth = minWidth;
+    restoreWidth = defaultWidth;
   }
   if (restoreHeight < minHeight) {
-    restoreHeight = minHeight;
+    restoreHeight = defaultHeight;
   }
   if (restoreWidth > maxWidth) {
-    restoreWidth = maxWidth;
+    restoreWidth = defaultWidth;
   }
   if (restoreHeight > maxHeight) {
-    restoreHeight = maxHeight;
+    restoreHeight = defaultHeight;
   }
   return Size(restoreWidth, restoreHeight);
 }
 
 /// return null means center
 Future<Offset?> _adjustRestoreMainWindowOffset(
-    double? left, double? top) async {
-  if (left == null || top == null) {
-    await windowManager.center();
-  } else {
-    double windowLeft = max(0.0, left);
-    double windowTop = max(0.0, top);
+  double? left,
+  double? top,
+  double? width,
+  double? height,
+) async {
+  if (left == null || top == null || width == null || height == null) {
+    return null;
+  }
 
-    double frameLeft = double.infinity;
-    double frameTop = double.infinity;
-    double frameRight = ((isDesktop || isWebDesktop)
-            ? kDesktopMaxDisplayWidth
-            : kMobileMaxDisplayWidth)
-        .toDouble();
-    double frameBottom = ((isDesktop || isWebDesktop)
-            ? kDesktopMaxDisplayHeight
-            : kMobileMaxDisplayHeight)
-        .toDouble();
+  double? frameLeft;
+  double? frameTop;
+  double? frameRight;
+  double? frameBottom;
 
-    if (isDesktop || isWebDesktop) {
-      for (final screen in await window_size.getScreenList()) {
-        frameLeft = min(screen.visibleFrame.left, frameLeft);
-        frameTop = min(screen.visibleFrame.top, frameTop);
-        frameRight = max(screen.visibleFrame.right, frameRight);
-        frameBottom = max(screen.visibleFrame.bottom, frameBottom);
-      }
-    }
-
-    if (windowLeft < frameLeft ||
-        windowLeft > frameRight ||
-        windowTop < frameTop ||
-        windowTop > frameBottom) {
-      return null;
-    } else {
-      return Offset(windowLeft, windowTop);
+  if (isDesktop || isWebDesktop) {
+    for (final screen in await window_size.getScreenList()) {
+      frameLeft = frameLeft == null
+          ? screen.visibleFrame.left
+          : min(screen.visibleFrame.left, frameLeft);
+      frameTop = frameTop == null
+          ? screen.visibleFrame.top
+          : min(screen.visibleFrame.top, frameTop);
+      frameRight = frameRight == null
+          ? screen.visibleFrame.right
+          : max(screen.visibleFrame.right, frameRight);
+      frameBottom = frameBottom == null
+          ? screen.visibleFrame.bottom
+          : max(screen.visibleFrame.bottom, frameBottom);
     }
   }
-  return null;
+  if (frameLeft == null) {
+    frameLeft = 0.0;
+    frameTop = 0.0;
+    frameRight = ((isDesktop || isWebDesktop)
+            ? kDesktopMaxDisplaySize
+            : kMobileMaxDisplaySize)
+        .toDouble();
+    frameBottom = ((isDesktop || isWebDesktop)
+            ? kDesktopMaxDisplaySize
+            : kMobileMaxDisplaySize)
+        .toDouble();
+  }
+  final minWidth = 10.0;
+  if ((left + minWidth) > frameRight! ||
+      (top + minWidth) > frameBottom! ||
+      (left + width - minWidth) < frameLeft ||
+      top < frameTop!) {
+    return null;
+  } else {
+    return Offset(left, top);
+  }
 }
 
 /// Restore window position and size on start
 /// Note that windowId must be provided if it's subwindow
-Future<bool> restoreWindowPosition(WindowType type, {int? windowId}) async {
+Future<bool> restoreWindowPosition(WindowType type,
+    {int? windowId, String? peerId}) async {
   if (bind
       .mainGetEnv(key: "DISABLE_RUSTDESK_RESTORE_WINDOW_POSITION")
       .isNotEmpty) {
@@ -1474,42 +1630,74 @@ Future<bool> restoreWindowPosition(WindowType type, {int? windowId}) async {
   if (type != WindowType.Main && windowId == null) {
     debugPrint(
         "Error: windowId cannot be null when saving positions for sub window");
+    return false;
   }
-  final pos = bind.getLocalFlutterConfig(k: kWindowPrefix + type.name);
+
+  bool isRemotePeerPos = false;
+  String? pos;
+  // No need to check mainGetLocalBoolOptionSync(kOptionOpenNewConnInTabs)
+  // Though "open in tabs" is true and the new window restore peer position, it's ok.
+  if (type == WindowType.RemoteDesktop && windowId != null && peerId != null) {
+    // If the restore position is called by main window, and the peer id is not null
+    // then we may need to get the position by reading the peer config.
+    // Because the session may not be read at this time.
+    if (desktopType == DesktopType.main) {
+      pos = bind.mainGetPeerFlutterOptionSync(
+          id: peerId, k: kWindowPrefix + type.name);
+    } else {
+      pos = await bind.sessionGetFlutterOptionByPeerId(
+          id: peerId, k: kWindowPrefix + type.name);
+    }
+    isRemotePeerPos = pos != null;
+  }
+  pos ??= bind.getLocalFlutterOption(k: kWindowPrefix + type.name);
+
   var lpos = LastWindowPosition.loadFromString(pos);
   if (lpos == null) {
     debugPrint("no window position saved, ignoring position restoration");
     return false;
   }
+  if (type == WindowType.RemoteDesktop &&
+      !isRemotePeerPos &&
+      windowId != null) {
+    if (lpos.offsetWidth != null) {
+      lpos.offsetWidth = lpos.offsetWidth! + windowId * 20;
+    }
+    if (lpos.offsetHeight != null) {
+      lpos.offsetHeight = lpos.offsetHeight! + windowId * 20;
+    }
+  }
+
+  final size = await _adjustRestoreMainWindowSize(lpos.width, lpos.height);
+  final offset = await _adjustRestoreMainWindowOffset(
+    lpos.offsetWidth,
+    lpos.offsetHeight,
+    size.width,
+    size.height,
+  );
+  debugPrint(
+      "restore lpos: ${size.width}/${size.height}, offset:${offset?.dx}/${offset?.dy}");
 
   switch (type) {
     case WindowType.Main:
-      if (lpos.isMaximized == true) {
-        await windowManager.maximize();
-      } else {
-        final size =
-            await _adjustRestoreMainWindowSize(lpos.width, lpos.height);
-        final offset = await _adjustRestoreMainWindowOffset(
-            lpos.offsetWidth, lpos.offsetHeight);
-        await windowManager.setSize(size);
+      restorePos() async {
         if (offset == null) {
           await windowManager.center();
         } else {
           await windowManager.setPosition(offset);
         }
       }
+      if (lpos.isMaximized == true) {
+        await restorePos();
+        await windowManager.maximize();
+      } else {
+        await windowManager.setSize(size);
+        await restorePos();
+      }
       return true;
     default:
       final wc = WindowController.fromWindowId(windowId!);
-      if (lpos.isMaximized == true) {
-        await wc.maximize();
-      } else {
-        final size =
-            await _adjustRestoreMainWindowSize(lpos.width, lpos.height);
-        final offset = await _adjustRestoreMainWindowOffset(
-            lpos.offsetWidth, lpos.offsetHeight);
-        debugPrint(
-            "restore lpos: ${size.width}/${size.height}, offset:${offset?.dx}/${offset?.dy}");
+      restoreFrame() async {
         if (offset == null) {
           await wc.center();
         } else {
@@ -1518,6 +1706,21 @@ Future<bool> restoreWindowPosition(WindowType type, {int? windowId}) async {
           await wc.setFrame(frame);
         }
       }
+      if (lpos.isFullscreen == true) {
+        await restoreFrame();
+        // An duration is needed to avoid the window being restored after fullscreen.
+        Future.delayed(Duration(milliseconds: 300), () async {
+          stateGlobal.setFullscreen(true);
+        });
+      } else if (lpos.isMaximized == true) {
+        await restoreFrame();
+        // An duration is needed to avoid the window being restored after maximized.
+        Future.delayed(Duration(milliseconds: 300), () async {
+          await wc.maximize();
+        });
+      } else {
+        await restoreFrame();
+      }
       break;
   }
   return false;
@@ -1538,7 +1741,7 @@ Future<bool> initUniLinks() async {
     if (initialLink == null) {
       return false;
     }
-    return parseRustdeskUri(initialLink);
+    return handleUriLink(uriString: initialLink);
   } catch (err) {
     debugPrintStack(label: "$err");
     return false;
@@ -1556,10 +1759,10 @@ StreamSubscription? listenUniLinks({handleByFlutter = true}) {
   }
 
   final sub = uriLinkStream.listen((Uri? uri) {
-    debugPrint("A uri was received: $uri.");
+    debugPrint("A uri was received: $uri. handleByFlutter $handleByFlutter");
     if (uri != null) {
       if (handleByFlutter) {
-        callUniLinksUriHandler(uri);
+        handleUriLink(uri: uri);
       } else {
         bind.sendUrlScheme(url: uri.toString());
       }
@@ -1572,97 +1775,166 @@ StreamSubscription? listenUniLinks({handleByFlutter = true}) {
   return sub;
 }
 
-/// Handle command line arguments
-///
-/// * Returns true if we successfully handle the startup arguments.
-bool checkArguments() {
-  if (kBootArgs.isNotEmpty) {
-    final ret = parseRustdeskUri(kBootArgs.first);
-    if (ret) {
-      return true;
+enum UriLinkType {
+  remoteDesktop,
+  fileTransfer,
+  portForward,
+  rdp,
+}
+
+// uri link handler
+bool handleUriLink({List<String>? cmdArgs, Uri? uri, String? uriString}) {
+  List<String>? args;
+  if (cmdArgs != null) {
+    args = cmdArgs;
+    // rustdesk <uri link>
+    if (args.isNotEmpty && args[0].startsWith(kUniLinksPrefix)) {
+      final uri = Uri.tryParse(args[0]);
+      if (uri != null) {
+        args = urlLinkToCmdArgs(uri);
+      }
+    }
+  } else if (uri != null) {
+    args = urlLinkToCmdArgs(uri);
+  } else if (uriString != null) {
+    final uri = Uri.tryParse(uriString);
+    if (uri != null) {
+      args = urlLinkToCmdArgs(uri);
     }
   }
-  // bootArgs:[--connect, 362587269, --switch_uuid, e3d531cc-5dce-41e0-bd06-5d4a2b1eec05]
-  // check connect args
-  var connectIndex = kBootArgs.indexOf("--connect");
-  if (connectIndex == -1) {
+  if (args == null) {
     return false;
   }
-  String? id =
-      kBootArgs.length <= connectIndex + 1 ? null : kBootArgs[connectIndex + 1];
-  String? password =
-      kBootArgs.length <= connectIndex + 2 ? null : kBootArgs[connectIndex + 2];
-  if (password != null && password.startsWith("--")) {
-    password = null;
+
+  if (args.isEmpty) {
+    windowOnTop(null);
+    return true;
   }
-  final switchUuidIndex = kBootArgs.indexOf("--switch_uuid");
-  String? switchUuid = kBootArgs.length <= switchUuidIndex + 1
-      ? null
-      : kBootArgs[switchUuidIndex + 1];
-  if (id != null) {
-    if (id.startsWith(kUniLinksPrefix)) {
-      return parseRustdeskUri(id);
-    } else {
-      // remove "--connect xxx" in the `bootArgs` array
-      kBootArgs.removeAt(connectIndex);
-      kBootArgs.removeAt(connectIndex);
-      // fallback to peer id
-      Future.delayed(Duration.zero, () {
-        rustDeskWinManager.newRemoteDesktop(id,
-            password: password, switch_uuid: switchUuid);
-      });
-      return true;
+
+  UriLinkType? type;
+  String? id;
+  String? password;
+  String? switchUuid;
+  bool? forceRelay;
+  for (int i = 0; i < args.length; i++) {
+    switch (args[i]) {
+      case '--connect':
+      case '--play':
+        type = UriLinkType.remoteDesktop;
+        id = args[i + 1];
+        i++;
+        break;
+      case '--file-transfer':
+        type = UriLinkType.fileTransfer;
+        id = args[i + 1];
+        i++;
+        break;
+      case '--port-forward':
+        type = UriLinkType.portForward;
+        id = args[i + 1];
+        i++;
+        break;
+      case '--rdp':
+        type = UriLinkType.rdp;
+        id = args[i + 1];
+        i++;
+        break;
+      case '--password':
+        password = args[i + 1];
+        i++;
+        break;
+      case '--switch_uuid':
+        switchUuid = args[i + 1];
+        i++;
+        break;
+      case '--relay':
+        forceRelay = true;
+        break;
+      default:
+        break;
     }
   }
+  if (type != null && id != null) {
+    switch (type) {
+      case UriLinkType.remoteDesktop:
+        Future.delayed(Duration.zero, () {
+          rustDeskWinManager.newRemoteDesktop(id!,
+              password: password,
+              switchUuid: switchUuid,
+              forceRelay: forceRelay);
+        });
+        break;
+      case UriLinkType.fileTransfer:
+        Future.delayed(Duration.zero, () {
+          rustDeskWinManager.newFileTransfer(id!,
+              password: password, forceRelay: forceRelay);
+        });
+        break;
+      case UriLinkType.portForward:
+        Future.delayed(Duration.zero, () {
+          rustDeskWinManager.newPortForward(id!, false,
+              password: password, forceRelay: forceRelay);
+        });
+        break;
+      case UriLinkType.rdp:
+        Future.delayed(Duration.zero, () {
+          rustDeskWinManager.newPortForward(id!, true,
+              password: password, forceRelay: forceRelay);
+        });
+        break;
+    }
+
+    return true;
+  }
+
   return false;
 }
 
-/// Parse `rustdesk://` unilinks
-///
-/// Returns true if we successfully handle the uri provided.
-/// [Functions]
-/// 1. New Connection: rustdesk://connection/new/your_peer_id
-bool parseRustdeskUri(String uriPath) {
-  final uri = Uri.tryParse(uriPath);
-  if (uri == null) {
-    debugPrint("uri is not valid: $uriPath");
-    return false;
-  }
-  return callUniLinksUriHandler(uri);
-}
-
-/// uri handler
-///
-/// Returns true if we successfully handle the uri provided.
-bool callUniLinksUriHandler(Uri uri) {
-  debugPrint("uni links called: $uri");
-  // new connection
-  String peerId;
-  if (uri.authority == "connection" && uri.path.startsWith("/new/")) {
-    peerId = uri.path.substring("/new/".length);
-  } else if (uri.authority == "connect") {
-    peerId = uri.path.substring(1);
+List<String>? urlLinkToCmdArgs(Uri uri) {
+  String? command;
+  String? id;
+  if (uri.authority.isEmpty &&
+      uri.path.split('').every((char) => char == '/')) {
+    return [];
+  } else if (uri.authority == "connection" && uri.path.startsWith("/new/")) {
+    // For compatibility
+    command = '--connect';
+    id = uri.path.substring("/new/".length);
+  } else if (['connect', "play", 'file-transfer', 'port-forward', 'rdp']
+      .contains(uri.authority)) {
+    command = '--${uri.authority}';
+    if (uri.path.length > 1) {
+      id = uri.path.substring(1);
+    }
   } else if (uri.authority.length > 2 && uri.path.length <= 1) {
-    // "/" or ""
-    peerId = uri.authority;
-  } else {
-    return false;
+    // rustdesk://<connect-id>
+    command = '--connect';
+    id = uri.authority;
   }
-  var param = uri.queryParameters;
-  String? switch_uuid = param["switch_uuid"];
-  String? password = param["password"];
-  Future.delayed(Duration.zero, () {
-    rustDeskWinManager.newRemoteDesktop(peerId,
-        password: password, switch_uuid: switch_uuid);
-  });
-  return true;
+
+  List<String> args = List.empty(growable: true);
+  if (command != null && id != null) {
+    args.add(command);
+    args.add(id);
+    var param = uri.queryParameters;
+    String? password = param["password"];
+    if (password != null) args.addAll(['--password', password]);
+    String? switch_uuid = param["switch_uuid"];
+    if (switch_uuid != null) args.addAll(['--switch_uuid', switch_uuid]);
+    if (param["relay"] != null) args.add("--relay");
+    return args;
+  }
+
+  return null;
 }
 
-connectMainDesktop(String id,
-    {required bool isFileTransfer,
-    required bool isTcpTunneling,
-    required bool isRDP,
-    bool? forceRelay}) async {
+connectMainDesktop(
+  String id, {
+  required bool isFileTransfer,
+  required bool isTcpTunneling,
+  required bool isRDP,
+  bool? forceRelay,
+}) async {
   if (isFileTransfer) {
     await rustDeskWinManager.newFileTransfer(id, forceRelay: forceRelay);
   } else if (isTcpTunneling || isRDP) {
@@ -1676,11 +1948,22 @@ connectMainDesktop(String id,
 /// If [isFileTransfer], starts a session only for file transfer.
 /// If [isTcpTunneling], starts a session only for tcp tunneling.
 /// If [isRDP], starts a session only for rdp.
-connect(BuildContext context, String id,
-    {bool isFileTransfer = false,
-    bool isTcpTunneling = false,
-    bool isRDP = false}) async {
+connect(
+  BuildContext context,
+  String id, {
+  bool isFileTransfer = false,
+  bool isTcpTunneling = false,
+  bool isRDP = false,
+}) async {
   if (id == '') return;
+  if (!isDesktop || desktopType == DesktopType.main) {
+    try {
+      if (Get.isRegistered<IDTextEditingController>()) {
+        final idController = Get.find<IDTextEditingController>();
+        idController.text = formatID(id);
+      }
+    } catch (_) {}
+  }
   id = id.replaceAll(' ', '');
   final oldId = id;
   id = await bind.mainHandleRelayId(id: id);
@@ -1690,18 +1973,20 @@ connect(BuildContext context, String id,
 
   if (isDesktop) {
     if (desktopType == DesktopType.main) {
-      await connectMainDesktop(id,
-          isFileTransfer: isFileTransfer,
-          isTcpTunneling: isTcpTunneling,
-          isRDP: isRDP,
-          forceRelay: forceRelay);
+      await connectMainDesktop(
+        id,
+        isFileTransfer: isFileTransfer,
+        isTcpTunneling: isTcpTunneling,
+        isRDP: isRDP,
+        forceRelay: forceRelay,
+      );
     } else {
       await rustDeskWinManager.call(WindowType.Main, kWindowConnect, {
         'id': id,
         'isFileTransfer': isFileTransfer,
         'isTcpTunneling': isTcpTunneling,
         'isRDP': isRDP,
-        "forceRelay": forceRelay,
+        'forceRelay': forceRelay,
       });
     }
   } else {
@@ -1790,10 +2075,14 @@ Future<void> onActiveWindowChanged() async {
   if (rustDeskWinManager.getActiveWindows().isEmpty) {
     // close all sub windows
     try {
-      await Future.wait([
-        saveWindowPosition(WindowType.Main),
-        rustDeskWinManager.closeAllSubWindows()
-      ]);
+      if (Platform.isLinux) {
+        await Future.wait([
+          saveWindowPosition(WindowType.Main),
+          rustDeskWinManager.closeAllSubWindows()
+        ]);
+      } else {
+        await rustDeskWinManager.closeAllSubWindows();
+      }
     } catch (err) {
       debugPrintStack(label: "$err");
     } finally {
@@ -2093,11 +2382,87 @@ void onCopyFingerprint(String value) {
   }
 }
 
+Future<bool> callMainCheckSuperUserPermission() async {
+  bool checked = await bind.mainCheckSuperUserPermission();
+  if (Platform.isMacOS) {
+    await windowManager.show();
+  }
+  return checked;
+}
+
 Future<void> start_service(bool is_start) async {
   bool checked = !bind.mainIsInstalled() ||
       !Platform.isMacOS ||
-      await bind.mainCheckSuperUserPermission();
+      await callMainCheckSuperUserPermission();
   if (checked) {
     bind.mainSetOption(key: "stop-service", value: is_start ? "" : "Y");
   }
 }
+
+typedef Future<bool> WhetherUseRemoteBlock();
+Widget buildRemoteBlock({required Widget child, WhetherUseRemoteBlock? use}) {
+  var block = false.obs;
+  return Obx(() => MouseRegion(
+        onEnter: (_) async {
+          if (use != null && !await use()) {
+            block.value = false;
+            return;
+          }
+          var time0 = DateTime.now().millisecondsSinceEpoch;
+          await bind.mainCheckMouseTime();
+          Timer(const Duration(milliseconds: 120), () async {
+            var d = time0 - await bind.mainGetMouseTime();
+            if (d < 120) {
+              block.value = true;
+            }
+          });
+        },
+        onExit: (event) => block.value = false,
+        child: Stack(children: [
+          child,
+          Offstage(
+              offstage: !block.value,
+              child: Container(
+                color: Colors.black.withOpacity(0.5),
+              )),
+        ]),
+      ));
+}
+
+Widget unreadMessageCountBuilder(RxInt? count,
+    {double? size, double? fontSize}) {
+  return Obx(() => Offstage(
+      offstage: !((count?.value ?? 0) > 0),
+      child: Container(
+        width: size ?? 16,
+        height: size ?? 16,
+        decoration: BoxDecoration(
+          color: Colors.red,
+          shape: BoxShape.circle,
+        ),
+        child: Center(
+          child: Text("${count?.value ?? 0}",
+              maxLines: 1,
+              style: TextStyle(color: Colors.white, fontSize: fontSize ?? 10)),
+        ),
+      )));
+}
+
+Widget unreadTopRightBuilder(RxInt? count, {Widget? icon}) {
+  return Stack(
+    children: [
+      icon ?? Icon(Icons.chat),
+      Positioned(
+          top: 0,
+          right: 0,
+          child: unreadMessageCountBuilder(count, size: 12, fontSize: 8))
+    ],
+  );
+}
+
+String toCapitalized(String s) {
+  if (s.isEmpty) {
+    return s;
+  }
+  return s.substring(0, 1).toUpperCase() + s.substring(1);
+}
diff --git a/flutter/lib/common/formatter/id_formatter.dart b/flutter/lib/common/formatter/id_formatter.dart
index a9e4893a6..c2329d53f 100644
--- a/flutter/lib/common/formatter/id_formatter.dart
+++ b/flutter/lib/common/formatter/id_formatter.dart
@@ -26,6 +26,8 @@ class IDTextInputFormatter extends TextInputFormatter {
         selection: TextSelection.collapsed(
           offset: newID.length - selectionIndexFromTheRight,
         ),
+        // https://github.com/flutter/flutter/issues/78066#issuecomment-797869906
+        composing: newValue.composing,
       );
     }
   }
@@ -33,6 +35,11 @@ class IDTextInputFormatter extends TextInputFormatter {
 
 String formatID(String id) {
   String id2 = id.replaceAll(' ', '');
+  String suffix = '';
+  if (id2.endsWith(r'\r') || id2.endsWith(r'/r')) {
+    suffix = id2.substring(id2.length - 2, id2.length);
+    id2 = id2.substring(0, id2.length - 2);
+  }
   if (int.tryParse(id2) == null) return id;
   String newID = '';
   if (id2.length <= 3) {
@@ -45,7 +52,7 @@ String formatID(String id) {
       newID += " ${id2.substring(i, i + 3)}";
     }
   }
-  return newID;
+  return newID + suffix;
 }
 
 String trimID(String id) {
diff --git a/flutter/lib/common/hbbs/hbbs.dart b/flutter/lib/common/hbbs/hbbs.dart
index d102d9f02..8e5c2d02a 100644
--- a/flutter/lib/common/hbbs/hbbs.dart
+++ b/flutter/lib/common/hbbs/hbbs.dart
@@ -1,4 +1,5 @@
-import 'dart:io';
+import 'dart:convert';
+import 'package:flutter/material.dart';
 
 import 'package:flutter_hbb/models/peer_model.dart';
 
@@ -70,16 +71,6 @@ class PeerPayload {
   }
 }
 
-class DeviceInfo {
-  static Map<String, dynamic> toJson() {
-    final Map<String, dynamic> data = <String, dynamic>{};
-    data['os'] = Platform.operatingSystem;
-    data['type'] = "client";
-    data['name'] = bind.mainGetHostname();
-    return data;
-  }
-}
-
 class LoginRequest {
   String? username;
   String? password;
@@ -88,7 +79,6 @@ class LoginRequest {
   bool? autoLogin;
   String? type;
   String? verificationCode;
-  Map<String, dynamic> deviceInfo = DeviceInfo.toJson();
 
   LoginRequest(
       {this.username,
@@ -110,6 +100,13 @@ class LoginRequest {
     if (verificationCode != null) {
       data['verificationCode'] = verificationCode;
     }
+
+    Map<String, dynamic> deviceInfo = {};
+    try {
+      deviceInfo = jsonDecode(bind.mainGetLoginDeviceInfo());
+    } catch (e) {
+      debugPrint('Failed to decode get device info: $e');
+    }
     data['deviceInfo'] = deviceInfo;
     return data;
   }
diff --git a/flutter/lib/common/shared_state.dart b/flutter/lib/common/shared_state.dart
index e4711ddf8..d5ce29190 100644
--- a/flutter/lib/common/shared_state.dart
+++ b/flutter/lib/common/shared_state.dart
@@ -285,6 +285,29 @@ class PeerStringOption {
       Get.find<RxString>(tag: tag(id, opt));
 }
 
+class UnreadChatCountState {
+  static String tag(id) => 'unread_chat_count_$id';
+
+  static void init(String id) {
+    final key = tag(id);
+    if (!Get.isRegistered(tag: key)) {
+      final RxInt state = RxInt(0);
+      Get.put(state, tag: key);
+    } else {
+      Get.find<RxInt>(tag: key).value = 0;
+    }
+  }
+
+  static void delete(String id) {
+    final key = tag(id);
+    if (Get.isRegistered(tag: key)) {
+      Get.delete(tag: key);
+    }
+  }
+
+  static RxInt find(String id) => Get.find<RxInt>(tag: tag(id));
+}
+
 initSharedStates(String id) {
   PrivacyModeState.init(id);
   BlockInputState.init(id);
@@ -294,6 +317,7 @@ initSharedStates(String id) {
   RemoteCursorMovedState.init(id);
   FingerprintState.init(id);
   PeerBoolOption.init(id, 'zoom-cursor', () => false);
+  UnreadChatCountState.init(id);
 }
 
 removeSharedStates(String id) {
@@ -305,4 +329,5 @@ removeSharedStates(String id) {
   RemoteCursorMovedState.delete(id);
   FingerprintState.delete(id);
   PeerBoolOption.delete(id, 'zoom-cursor');
+  UnreadChatCountState.delete(id);
 }
diff --git a/flutter/lib/common/widgets/address_book.dart b/flutter/lib/common/widgets/address_book.dart
index 58acc9ace..4af74e319 100644
--- a/flutter/lib/common/widgets/address_book.dart
+++ b/flutter/lib/common/widgets/address_book.dart
@@ -3,11 +3,14 @@ import 'package:flutter_hbb/common/formatter/id_formatter.dart';
 import 'package:flutter_hbb/common/widgets/peer_card.dart';
 import 'package:flutter_hbb/common/widgets/peers_view.dart';
 import 'package:flutter_hbb/desktop/widgets/popup_menu.dart';
-import '../../consts.dart';
+import 'package:flutter_hbb/models/ab_model.dart';
+import 'package:flutter_hbb/models/platform_model.dart';
 import '../../desktop/widgets/material_mod_popup_menu.dart' as mod_menu;
 import 'package:get/get.dart';
+import 'package:flex_color_picker/flex_color_picker.dart';
 
 import '../../common.dart';
+import 'dialog.dart';
 import 'login.dart';
 
 final hideAbTagsPanel = false.obs;
@@ -37,63 +40,115 @@ class _AddressBookState extends State<AddressBook> {
               child: ElevatedButton(
                   onPressed: loginDialog, child: Text(translate("Login"))));
         } else {
-          if (gFFI.abModel.abLoading.value) {
+          if (gFFI.abModel.abLoading.value && gFFI.abModel.emtpy) {
             return const Center(
               child: CircularProgressIndicator(),
             );
           }
-          if (gFFI.abModel.abError.isNotEmpty) {
-            return _buildShowError(gFFI.abModel.abError.value);
-          }
-          return isDesktop
-              ? _buildAddressBookDesktop()
-              : _buildAddressBookMobile();
+          return Column(
+            children: [
+              // NOT use Offstage to wrap LinearProgressIndicator
+              if (gFFI.abModel.retrying.value) LinearProgressIndicator(),
+              _buildErrorBanner(
+                  err: gFFI.abModel.pullError,
+                  retry: null,
+                  close: () => gFFI.abModel.pullError.value = ''),
+              _buildErrorBanner(
+                  err: gFFI.abModel.pushError,
+                  retry: () => gFFI.abModel.pushAb(isRetry: true),
+                  close: () => gFFI.abModel.pushError.value = ''),
+              Expanded(
+                  child: isDesktop
+                      ? _buildAddressBookDesktop()
+                      : _buildAddressBookMobile())
+            ],
+          );
         }
       });
 
-  Widget _buildShowError(String error) {
-    return Center(
-        child: Column(
-      mainAxisAlignment: MainAxisAlignment.center,
-      children: [
-        Text(translate(error)),
-        TextButton(
-            onPressed: () {
-              gFFI.abModel.pullAb();
-            },
-            child: Text(translate("Retry")))
-      ],
-    ));
+  Widget _buildErrorBanner(
+      {required RxString err,
+      required Function? retry,
+      required Function close}) {
+    const double height = 25;
+    return Obx(() => Offstage(
+          offstage: !(!gFFI.abModel.abLoading.value && err.value.isNotEmpty),
+          child: Center(
+              child: Container(
+            height: height,
+            color: Color.fromARGB(255, 253, 238, 235),
+            child: Row(
+              mainAxisAlignment: MainAxisAlignment.center,
+              crossAxisAlignment: CrossAxisAlignment.center,
+              children: [
+                FittedBox(
+                  child: Icon(
+                    Icons.info,
+                    color: Color.fromARGB(255, 249, 81, 81),
+                  ),
+                ).marginAll(4),
+                Flexible(
+                  child: Align(
+                      alignment: Alignment.centerLeft,
+                      child: Tooltip(
+                        message: translate(err.value),
+                        child: Text(
+                          translate(err.value),
+                          overflow: TextOverflow.ellipsis,
+                        ),
+                      )).marginSymmetric(vertical: 2),
+                ),
+                if (retry != null)
+                  InkWell(
+                      onTap: () {
+                        retry.call();
+                      },
+                      child: Text(
+                        translate("Retry"),
+                        style: TextStyle(color: MyTheme.accent),
+                      )).marginSymmetric(horizontal: 5),
+                FittedBox(
+                  child: InkWell(
+                    onTap: () {
+                      close.call();
+                    },
+                    child: Icon(Icons.close).marginSymmetric(horizontal: 5),
+                  ),
+                ).marginAll(4)
+              ],
+            ),
+          )).marginOnly(bottom: 14),
+        ));
   }
 
   Widget _buildAddressBookDesktop() {
     return Row(
       children: [
         Offstage(
-          offstage: hideAbTagsPanel.value,
-          child: Container(
-          decoration: BoxDecoration(
-              borderRadius: BorderRadius.circular(12),
-              border:
-                  Border.all(color: Theme.of(context).colorScheme.background)),
-          child: Container(
-            width: 150,
-            height: double.infinity,
-            padding: const EdgeInsets.all(8.0),
-            child: Column(
-              children: [
-                _buildTagHeader().marginOnly(left: 8.0, right: 0),
-                Expanded(
-                  child: Container(
-                    width: double.infinity,
-                    height: double.infinity,
-                    child: _buildTags(),
-                  ),
-                )
-              ],
-            ),
-          ),
-        ).marginOnly(right: 12.0)),
+            offstage: hideAbTagsPanel.value,
+            child: Container(
+              decoration: BoxDecoration(
+                  borderRadius: BorderRadius.circular(12),
+                  border: Border.all(
+                      color: Theme.of(context).colorScheme.background)),
+              child: Container(
+                width: 150,
+                height: double.infinity,
+                padding: const EdgeInsets.all(8.0),
+                child: Column(
+                  children: [
+                    _buildTagHeader().marginOnly(left: 8.0, right: 0),
+                    Expanded(
+                      child: Container(
+                        width: double.infinity,
+                        height: double.infinity,
+                        child: _buildTags(),
+                      ),
+                    )
+                  ],
+                ),
+              ),
+            ).marginOnly(right: 12.0)),
         _buildPeersViews()
       ],
     );
@@ -102,25 +157,27 @@ class _AddressBookState extends State<AddressBook> {
   Widget _buildAddressBookMobile() {
     return Column(
       children: [
-        Container(
-          decoration: BoxDecoration(
-              borderRadius: BorderRadius.circular(6),
-              border:
-                  Border.all(color: Theme.of(context).colorScheme.background)),
-          child: Container(
-            padding: const EdgeInsets.all(8.0),
-            child: Column(
-              mainAxisSize: MainAxisSize.min,
-              children: [
-                _buildTagHeader().marginOnly(left: 8.0, right: 0),
-                Container(
-                  width: double.infinity,
-                  child: _buildTags(),
+        Offstage(
+            offstage: hideAbTagsPanel.value,
+            child: Container(
+              decoration: BoxDecoration(
+                  borderRadius: BorderRadius.circular(6),
+                  border: Border.all(
+                      color: Theme.of(context).colorScheme.background)),
+              child: Container(
+                padding: const EdgeInsets.all(8.0),
+                child: Column(
+                  mainAxisSize: MainAxisSize.min,
+                  children: [
+                    _buildTagHeader().marginOnly(left: 8.0, right: 0),
+                    Container(
+                      width: double.infinity,
+                      child: _buildTags(),
+                    ),
+                  ],
                 ),
-              ],
-            ),
-          ),
-        ).marginOnly(bottom: 12.0),
+              ),
+            ).marginOnly(bottom: 12.0)),
         _buildPeersViews()
       ],
     );
@@ -144,9 +201,16 @@ class _AddressBookState extends State<AddressBook> {
   }
 
   Widget _buildTags() {
-    return Obx(
-      () => Wrap(
-        children: gFFI.abModel.tags
+    return Obx(() {
+      final List tags;
+      if (gFFI.abModel.sortTags.value) {
+        tags = gFFI.abModel.tags.toList();
+        tags.sort();
+      } else {
+        tags = gFFI.abModel.tags;
+      }
+      return Wrap(
+        children: tags
             .map((e) => AddressBookTag(
                 name: e,
                 tags: gFFI.abModel.selectedTags,
@@ -158,8 +222,8 @@ class _AddressBookState extends State<AddressBook> {
                   }
                 }))
             .toList(),
-      ),
-    );
+      );
+    });
   }
 
   Widget _buildPeersViews() {
@@ -174,11 +238,44 @@ class _AddressBookState extends State<AddressBook> {
     );
   }
 
+  @protected
+  MenuEntryBase<String> syncMenuItem() {
+    return MenuEntrySwitch<String>(
+      switchType: SwitchType.scheckbox,
+      text: translate('Sync with recent sessions'),
+      getter: () async {
+        return shouldSyncAb();
+      },
+      setter: (bool v) async {
+        bind.mainSetLocalOption(key: syncAbOption, value: v ? 'Y' : '');
+      },
+      dismissOnClicked: true,
+    );
+  }
+
+  @protected
+  MenuEntryBase<String> sortMenuItem() {
+    return MenuEntrySwitch<String>(
+      switchType: SwitchType.scheckbox,
+      text: translate('Sort tags'),
+      getter: () async {
+        return shouldSortTags();
+      },
+      setter: (bool v) async {
+        bind.mainSetLocalOption(key: sortAbTagsOption, value: v ? 'Y' : '');
+        gFFI.abModel.sortTags.value = v;
+      },
+      dismissOnClicked: true,
+    );
+  }
+
   void _showMenu(RelativeRect pos) {
     final items = [
       getEntry(translate("Add ID"), abAddId),
       getEntry(translate("Add Tag"), abAddTag),
       getEntry(translate("Unselect all tags"), gFFI.abModel.unsetSelectedTags),
+      sortMenuItem(),
+      syncMenuItem(),
     ];
 
     mod_menu.showMenu(
@@ -198,6 +295,9 @@ class _AddressBookState extends State<AddressBook> {
   }
 
   void abAddId() async {
+    if (gFFI.abModel.isFull(true)) {
+      return;
+    }
     var isInProgress = false;
     IDTextEditingController idController = IDTextEditingController(text: '');
     TextEditingController aliasController = TextEditingController(text: '');
@@ -224,13 +324,14 @@ class _AddressBookState extends State<AddressBook> {
             return;
           }
           gFFI.abModel.addId(id, aliasController.text.trim(), selectedTag);
-          await gFFI.abModel.pushAb();
+          gFFI.abModel.pushAb();
           this.setState(() {});
           // final currentPeers
         }
         close();
       }
 
+      double marginBottom = 4;
       return CustomAlertDialog(
         title: Text(translate("Add ID")),
         content: Column(
@@ -252,7 +353,7 @@ class _AddressBookState extends State<AddressBook> {
                       ),
                     ],
                   ),
-                ),
+                ).marginOnly(bottom: marginBottom),
                 TextField(
                   controller: idController,
                   inputFormatters: [IDTextInputFormatter()],
@@ -264,7 +365,7 @@ class _AddressBookState extends State<AddressBook> {
                     translate('Alias'),
                     style: style,
                   ),
-                ).marginOnly(top: 8, bottom: 2),
+                ).marginOnly(top: 8, bottom: marginBottom),
                 TextField(
                   controller: aliasController,
                 ),
@@ -274,8 +375,9 @@ class _AddressBookState extends State<AddressBook> {
                     translate('Tags'),
                     style: style,
                   ),
-                ).marginOnly(top: 8),
-                Container(
+                ).marginOnly(top: 8, bottom: marginBottom),
+                Align(
+                  alignment: Alignment.centerLeft,
                   child: Wrap(
                     children: tags
                         .map((e) => AddressBookTag(
@@ -297,8 +399,8 @@ class _AddressBookState extends State<AddressBook> {
             const SizedBox(
               height: 4.0,
             ),
-            Offstage(
-                offstage: !isInProgress, child: const LinearProgressIndicator())
+            // NOT use Offstage to wrap LinearProgressIndicator
+            if (isInProgress) const LinearProgressIndicator(),
           ],
         ),
         actions: [
@@ -331,7 +433,7 @@ class _AddressBookState extends State<AddressBook> {
           for (final tag in tags) {
             gFFI.abModel.addTag(tag);
           }
-          await gFFI.abModel.pushAb();
+          gFFI.abModel.pushAb();
           // final currentPeers
         }
         close();
@@ -363,8 +465,8 @@ class _AddressBookState extends State<AddressBook> {
             const SizedBox(
               height: 4.0,
             ),
-            Offstage(
-                offstage: !isInProgress, child: const LinearProgressIndicator())
+            // NOT use Offstage to wrap LinearProgressIndicator
+            if (isInProgress) const LinearProgressIndicator(),
           ],
         ),
         actions: [
@@ -402,31 +504,95 @@ class AddressBookTag extends StatelessWidget {
       pos = RelativeRect.fromLTRB(x, y, x, y);
     }
 
+    const double radius = 8;
     return GestureDetector(
       onTap: onTap,
       onTapDown: showActionMenu ? setPosition : null,
       onSecondaryTapDown: showActionMenu ? setPosition : null,
       onSecondaryTap: showActionMenu ? () => _showMenu(context, pos) : null,
       onLongPress: showActionMenu ? () => _showMenu(context, pos) : null,
-      child: Obx(
-        () => Container(
-          decoration: BoxDecoration(
-              color: tags.contains(name)
-                  ? Colors.blue
-                  : Theme.of(context).colorScheme.background,
-              borderRadius: BorderRadius.circular(6)),
-          margin: const EdgeInsets.symmetric(horizontal: 4.0, vertical: 8.0),
-          padding: const EdgeInsets.symmetric(vertical: 2.0, horizontal: 8.0),
-          child: Text(name,
-              style:
-                  TextStyle(color: tags.contains(name) ? Colors.white : null)),
-        ),
-      ),
+      child: Obx(() => Container(
+            decoration: BoxDecoration(
+                color: tags.contains(name)
+                    ? gFFI.abModel.getTagColor(name)
+                    : Theme.of(context).colorScheme.background,
+                borderRadius: BorderRadius.circular(4)),
+            margin: const EdgeInsets.symmetric(horizontal: 4.0, vertical: 4.0),
+            padding: const EdgeInsets.symmetric(vertical: 2.0, horizontal: 6.0),
+            child: IntrinsicWidth(
+              child: Row(
+                children: [
+                  Container(
+                    width: radius,
+                    height: radius,
+                    decoration: BoxDecoration(
+                        shape: BoxShape.circle,
+                        color: tags.contains(name)
+                            ? Colors.white
+                            : gFFI.abModel.getTagColor(name)),
+                  ).marginOnly(right: radius / 2),
+                  Expanded(
+                    child: Text(name,
+                        style: TextStyle(
+                            overflow: TextOverflow.ellipsis,
+                            color: tags.contains(name) ? Colors.white : null)),
+                  ),
+                ],
+              ),
+            ),
+          )),
     );
   }
 
   void _showMenu(BuildContext context, RelativeRect pos) {
     final items = [
+      getEntry(translate("Rename"), () {
+        renameDialog(
+            oldName: name,
+            validator: (String? newName) {
+              if (newName == null || newName.isEmpty) {
+                return translate('Can not be empty');
+              }
+              if (newName != name && gFFI.abModel.tags.contains(newName)) {
+                return translate('Already exists');
+              }
+              return null;
+            },
+            onSubmit: (String newName) {
+              if (name != newName) {
+                gFFI.abModel.renameTag(name, newName);
+                gFFI.abModel.pushAb();
+              }
+              Future.delayed(Duration.zero, () => Get.back());
+            },
+            onCancel: () {
+              Future.delayed(Duration.zero, () => Get.back());
+            });
+      }),
+      getEntry(translate(translate('Change Color')), () async {
+        final model = gFFI.abModel;
+        Color oldColor = model.getTagColor(name);
+        Color newColor = await showColorPickerDialog(
+          context,
+          oldColor,
+          pickersEnabled: {
+            ColorPickerType.accent: false,
+            ColorPickerType.wheel: true,
+          },
+          pickerTypeLabels: {
+            ColorPickerType.primary: translate("Primary Color"),
+            ColorPickerType.wheel: translate("HSV Color"),
+          },
+          actionButtons: ColorPickerActionButtons(
+              dialogOkButtonLabel: translate("OK"),
+              dialogCancelButtonLabel: translate("Cancel")),
+          showColorCode: true,
+        );
+        if (oldColor != newColor) {
+          model.setTagColor(name, newColor);
+          model.pushAb();
+        }
+      }),
       getEntry(translate("Delete"), () {
         gFFI.abModel.deleteTag(name);
         gFFI.abModel.pushAb();
@@ -458,7 +624,6 @@ MenuEntryButton<String> getEntry(String title, VoidCallback proc) {
       style: style,
     ),
     proc: proc,
-    padding: kDesktopMenuPadding,
     dismissOnClicked: true,
   );
 }
diff --git a/flutter/lib/common/widgets/animated_rotation_widget.dart b/flutter/lib/common/widgets/animated_rotation_widget.dart
index 525508e44..0efc71552 100644
--- a/flutter/lib/common/widgets/animated_rotation_widget.dart
+++ b/flutter/lib/common/widgets/animated_rotation_widget.dart
@@ -1,11 +1,17 @@
 import 'package:flutter/material.dart';
+import 'package:get/get.dart';
 
 class AnimatedRotationWidget extends StatefulWidget {
   final VoidCallback onPressed;
   final ValueChanged<bool>? onHover;
   final Widget child;
+  final RxBool? spinning;
   const AnimatedRotationWidget(
-      {super.key, required this.onPressed, required this.child, this.onHover});
+      {super.key,
+      required this.onPressed,
+      required this.child,
+      this.spinning,
+      this.onHover});
 
   @override
   State<AnimatedRotationWidget> createState() => AnimatedRotationWidgetState();
@@ -14,14 +20,31 @@ class AnimatedRotationWidget extends StatefulWidget {
 class AnimatedRotationWidgetState extends State<AnimatedRotationWidget> {
   double turns = 0.0;
 
+  @override
+  void initState() {
+    super.initState();
+    widget.spinning?.listen((v) {
+      if (v && mounted) {
+        setState(() {
+          turns += 1;
+        });
+      }
+    });
+  }
+
   @override
   Widget build(BuildContext context) {
     return AnimatedRotation(
         turns: turns,
         duration: const Duration(milliseconds: 200),
+        onEnd: () {
+          if (widget.spinning?.value == true && mounted) {
+            setState(() => turns += 1.0);
+          }
+        },
         child: InkWell(
             onTap: () {
-              setState(() => turns += 1.0);
+              if (mounted) setState(() => turns += 1.0);
               widget.onPressed();
             },
             onHover: widget.onHover,
diff --git a/flutter/lib/common/widgets/chat_page.dart b/flutter/lib/common/widgets/chat_page.dart
index 0e6be569e..b6611d3ed 100644
--- a/flutter/lib/common/widgets/chat_page.dart
+++ b/flutter/lib/common/widgets/chat_page.dart
@@ -7,10 +7,16 @@ import 'package:provider/provider.dart';
 
 import '../../mobile/pages/home_page.dart';
 
+enum ChatPageType {
+  mobileMain,
+  desktopCM,
+}
+
 class ChatPage extends StatelessWidget implements PageShape {
   late final ChatModel chatModel;
+  final ChatPageType? type;
 
-  ChatPage({ChatModel? chatModel}) {
+  ChatPage({ChatModel? chatModel, this.type}) {
     this.chatModel = chatModel ?? gFFI.chatModel;
   }
 
@@ -18,27 +24,53 @@ class ChatPage extends StatelessWidget implements PageShape {
   final title = translate("Chat");
 
   @override
-  final icon = Icon(Icons.chat);
+  final icon = unreadTopRightBuilder(gFFI.chatModel.mobileUnreadSum);
 
   @override
   final appBarActions = [
-    PopupMenuButton<int>(
+    PopupMenuButton<MessageKey>(
         tooltip: "",
-        icon: Icon(Icons.group),
+        icon: unreadTopRightBuilder(gFFI.chatModel.mobileUnreadSum,
+            icon: Icon(Icons.group)),
         itemBuilder: (context) {
           // only mobile need [appBarActions], just bind gFFI.chatModel
           final chatModel = gFFI.chatModel;
           return chatModel.messages.entries.map((entry) {
-            final id = entry.key;
+            final key = entry.key;
             final user = entry.value.chatUser;
-            return PopupMenuItem<int>(
-              child: Text("${user.firstName}   ${user.id}"),
-              value: id,
+            final client = gFFI.serverModel.clients
+                .firstWhereOrNull((e) => e.id == key.connId);
+            final connected =
+                gFFI.serverModel.clients.any((e) => e.id == key.connId);
+            return PopupMenuItem<MessageKey>(
+              child: Row(
+                children: [
+                  Icon(
+                          key.isOut
+                              ? Icons.call_made_rounded
+                              : Icons.call_received_rounded,
+                          color: MyTheme.accent)
+                      .marginOnly(right: 6),
+                  Text("${user.firstName}   ${user.id}"),
+                  if (connected)
+                    Container(
+                      width: 10,
+                      height: 10,
+                      decoration: BoxDecoration(
+                          shape: BoxShape.circle,
+                          color: Color.fromARGB(255, 46, 205, 139)),
+                    ).marginSymmetric(horizontal: 2),
+                  if (client != null)
+                    unreadMessageCountBuilder(client.unreadChatMessageCount)
+                        .marginOnly(left: 4)
+                ],
+              ),
+              value: key,
             );
           }).toList();
         },
-        onSelected: (id) {
-          gFFI.chatModel.changeCurrentID(id);
+        onSelected: (key) {
+          gFFI.chatModel.changeCurrentKey(key);
         })
   ];
 
@@ -50,16 +82,27 @@ class ChatPage extends StatelessWidget implements PageShape {
         color: Theme.of(context).scaffoldBackgroundColor,
         child: Consumer<ChatModel>(
           builder: (context, chatModel, child) {
-            final currentUser = chatModel.currentUser;
+            final readOnly = type == ChatPageType.mobileMain &&
+                    (chatModel.currentKey.connId == ChatModel.clientModeID ||
+                        gFFI.serverModel.clients.every((e) =>
+                            e.id != chatModel.currentKey.connId ||
+                            chatModel.currentUser == null)) ||
+                type == ChatPageType.desktopCM &&
+                    gFFI.serverModel.clients
+                            .firstWhereOrNull(
+                                (e) => e.id == chatModel.currentKey.connId)
+                            ?.disconnected ==
+                        true;
             return Stack(
               children: [
                 LayoutBuilder(builder: (context, constraints) {
                   final chat = DashChat(
                     onSend: chatModel.send,
                     currentUser: chatModel.me,
-                    messages:
-                        chatModel.messages[chatModel.currentID]?.chatMessages ??
-                            [],
+                    messages: chatModel
+                            .messages[chatModel.currentKey]?.chatMessages ??
+                        [],
+                    readOnly: readOnly,
                     inputOptions: InputOptions(
                       focusNode: chatModel.inputNode,
                       textController: chatModel.textController,
@@ -127,22 +170,6 @@ class ChatPage extends StatelessWidget implements PageShape {
                   );
                   return SelectionArea(child: chat);
                 }),
-                desktopType == DesktopType.cm ||
-                        chatModel.currentID == ChatModel.clientModeID
-                    ? SizedBox.shrink()
-                    : Padding(
-                        padding: EdgeInsets.all(12),
-                        child: Row(
-                          children: [
-                            Icon(Icons.account_circle, color: MyTheme.accent80),
-                            SizedBox(width: 5),
-                            Text(
-                              "${currentUser.firstName}   ${currentUser.id}",
-                              style: TextStyle(color: MyTheme.accent),
-                            ),
-                          ],
-                        ),
-                      ),
               ],
             ).paddingOnly(bottom: 8);
           },
diff --git a/flutter/lib/common/widgets/dialog.dart b/flutter/lib/common/widgets/dialog.dart
index 2e60304be..a2a4e2b23 100644
--- a/flutter/lib/common/widgets/dialog.dart
+++ b/flutter/lib/common/widgets/dialog.dart
@@ -1,14 +1,15 @@
 import 'dart:async';
 
-import 'package:debounce_throttle/debounce_throttle.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter/services.dart';
 import 'package:flutter_hbb/common/shared_state.dart';
+import 'package:flutter_hbb/common/widgets/setting_widgets.dart';
 import 'package:get/get.dart';
 
 import '../../common.dart';
 import '../../models/model.dart';
 import '../../models/platform_model.dart';
+import 'address_book.dart';
 
 void clientClose(SessionID sessionId, OverlayDialogManager dialogManager) {
   msgBox(sessionId, 'info', 'Close', 'Are you sure to close the connection?',
@@ -155,8 +156,8 @@ void changeIdDialog() {
                     }).toList(),
                   )).marginOnly(bottom: 8)
               : SizedBox.shrink(),
-          Offstage(
-              offstage: !isInProgress, child: const LinearProgressIndicator())
+          // NOT use Offstage to wrap LinearProgressIndicator
+          if (isInProgress) const LinearProgressIndicator(),
         ],
       ),
       actions: [
@@ -201,8 +202,8 @@ void changeWhiteList({Function()? callback}) async {
           const SizedBox(
             height: 4.0,
           ),
-          Offstage(
-              offstage: !isInProgress, child: const LinearProgressIndicator())
+          // NOT use Offstage to wrap LinearProgressIndicator
+          if (isInProgress) const LinearProgressIndicator(),
         ],
       ),
       actions: [
@@ -811,6 +812,8 @@ void showRequestElevationDialog(
       } else {
         bind.sessionElevateDirect(sessionId: sessionId);
       }
+      close();
+      showWaitUacDialog(sessionId, dialogManager, "wait-uac");
     }
 
     return CustomAlertDialog(
@@ -943,16 +946,23 @@ showSetOSPassword(
   SessionID sessionId,
   bool login,
   OverlayDialogManager dialogManager,
+  String? osPassword,
+  Function()? closeCallback,
 ) async {
   final controller = TextEditingController();
-  var password =
+  osPassword ??=
       await bind.sessionGetOption(sessionId: sessionId, arg: 'os-password') ??
           '';
   var autoLogin =
       await bind.sessionGetOption(sessionId: sessionId, arg: 'auto-login') !=
           '';
-  controller.text = password;
+  controller.text = osPassword;
   dialogManager.show((setState, close, context) {
+    closeWithCallback([dynamic]) {
+      close();
+      if (closeCallback != null) closeCallback();
+    }
+
     submit() {
       var text = controller.text.trim();
       bind.sessionPeerOption(
@@ -964,7 +974,7 @@ showSetOSPassword(
       if (text != '' && login) {
         bind.sessionInputOsPassword(sessionId: sessionId, value: text);
       }
-      close();
+      closeWithCallback();
     }
 
     return CustomAlertDialog(
@@ -998,7 +1008,7 @@ showSetOSPassword(
         dialogButton(
           "Cancel",
           icon: Icon(Icons.close_rounded),
-          onPressed: close,
+          onPressed: closeWithCallback,
           isOutline: true,
         ),
         dialogButton(
@@ -1008,7 +1018,7 @@ showSetOSPassword(
         ),
       ],
       onSubmit: submit,
-      onCancel: close,
+      onCancel: closeWithCallback,
     );
   });
 }
@@ -1215,50 +1225,9 @@ customImageQualityDialog(SessionID sessionId, String id, FFI ffi) async {
   final quality = await bind.sessionGetCustomImageQuality(sessionId: sessionId);
   qualityInitValue =
       quality != null && quality.isNotEmpty ? quality[0].toDouble() : 50.0;
-  const qualityMinValue = 10.0;
-  const qualityMaxValue = 100.0;
-  if (qualityInitValue < qualityMinValue) {
-    qualityInitValue = qualityMinValue;
+  if (qualityInitValue < 10 || qualityInitValue > 2000) {
+    qualityInitValue = 50;
   }
-  if (qualityInitValue > qualityMaxValue) {
-    qualityInitValue = qualityMaxValue;
-  }
-  final RxDouble qualitySliderValue = RxDouble(qualityInitValue);
-  final debouncerQuality = Debouncer<double>(
-    Duration(milliseconds: 1000),
-    onChanged: (double v) {
-      setCustomValues(quality: v);
-    },
-    initialValue: qualityInitValue,
-  );
-  final qualitySlider = Obx(() => Row(
-        children: [
-          Expanded(
-              flex: 3,
-              child: Slider(
-                value: qualitySliderValue.value,
-                min: qualityMinValue,
-                max: qualityMaxValue,
-                divisions: 18,
-                onChanged: (double value) {
-                  qualitySliderValue.value = value;
-                  debouncerQuality.value = value;
-                },
-              )),
-          Expanded(
-              flex: 1,
-              child: Text(
-                '${qualitySliderValue.value.round()}%',
-                style: const TextStyle(fontSize: 15),
-              )),
-          Expanded(
-              flex: 2,
-              child: Text(
-                translate('Bitrate'),
-                style: const TextStyle(fontSize: 15),
-              )),
-        ],
-      ));
   // fps
   final fpsOption =
       await bind.sessionGetOption(sessionId: sessionId, arg: 'custom-fps');
@@ -1266,54 +1235,187 @@ customImageQualityDialog(SessionID sessionId, String id, FFI ffi) async {
   if (fpsInitValue < 5 || fpsInitValue > 120) {
     fpsInitValue = 30;
   }
-  final RxDouble fpsSliderValue = RxDouble(fpsInitValue);
-  final debouncerFps = Debouncer<double>(
-    Duration(milliseconds: 1000),
-    onChanged: (double v) {
-      setCustomValues(fps: v);
-    },
-    initialValue: qualityInitValue,
-  );
   bool? direct;
   try {
     direct =
         ConnectionTypeState.find(id).direct.value == ConnectionType.strDirect;
   } catch (_) {}
-  final fpsSlider = Offstage(
-    offstage: (await bind.mainIsUsingPublicServer() && direct != true) ||
-        version_cmp(ffi.ffiModel.pi.version, '1.2.0') < 0,
-    child: Row(
-      children: [
-        Expanded(
-            flex: 3,
-            child: Obx((() => Slider(
-                  value: fpsSliderValue.value,
-                  min: 5,
-                  max: 120,
-                  divisions: 23,
-                  onChanged: (double value) {
-                    fpsSliderValue.value = value;
-                    debouncerFps.value = value;
-                  },
-                )))),
-        Expanded(
-            flex: 1,
-            child: Obx(() => Text(
-                  '${fpsSliderValue.value.round()}',
-                  style: const TextStyle(fontSize: 15),
-                ))),
-        Expanded(
-            flex: 2,
-            child: Text(
-              translate('FPS'),
-              style: const TextStyle(fontSize: 15),
-            ))
-      ],
-    ),
-  );
+  bool notShowFps = (await bind.mainIsUsingPublicServer() && direct != true) ||
+      version_cmp(ffi.ffiModel.pi.version, '1.2.0') < 0;
 
-  final content = Column(
-    children: [qualitySlider, fpsSlider],
-  );
+  final content = customImageQualityWidget(
+      initQuality: qualityInitValue,
+      initFps: fpsInitValue,
+      setQuality: (v) => setCustomValues(quality: v),
+      setFps: (v) => setCustomValues(fps: v),
+      showFps: !notShowFps);
   msgBoxCommon(ffi.dialogManager, 'Custom Image Quality', content, [btnClose]);
 }
+
+void deletePeerConfirmDialog(Function onSubmit, String title) async {
+  gFFI.dialogManager.show(
+    (setState, close, context) {
+      submit() async {
+        await onSubmit();
+        close();
+      }
+
+      return CustomAlertDialog(
+        title: Row(
+          mainAxisAlignment: MainAxisAlignment.center,
+          children: [
+            Icon(
+              Icons.delete_rounded,
+              color: Colors.red,
+            ),
+            Expanded(
+              child: Text(title, overflow: TextOverflow.ellipsis).paddingOnly(
+                left: 10,
+              ),
+            ),
+          ],
+        ),
+        content: SizedBox.shrink(),
+        actions: [
+          dialogButton(
+            "Cancel",
+            icon: Icon(Icons.close_rounded),
+            onPressed: close,
+            isOutline: true,
+          ),
+          dialogButton(
+            "OK",
+            icon: Icon(Icons.done_rounded),
+            onPressed: submit,
+          ),
+        ],
+        onSubmit: submit,
+        onCancel: close,
+      );
+    },
+  );
+}
+
+void editAbTagDialog(
+    List<dynamic> currentTags, Function(List<dynamic>) onSubmit) {
+  var isInProgress = false;
+
+  final tags = List.of(gFFI.abModel.tags);
+  var selectedTag = currentTags.obs;
+
+  gFFI.dialogManager.show((setState, close, context) {
+    submit() async {
+      setState(() {
+        isInProgress = true;
+      });
+      await onSubmit(selectedTag);
+      close();
+    }
+
+    return CustomAlertDialog(
+      title: Text(translate("Edit Tag")),
+      content: Column(
+        crossAxisAlignment: CrossAxisAlignment.start,
+        children: [
+          Container(
+            padding: const EdgeInsets.symmetric(vertical: 8.0),
+            child: Wrap(
+              children: tags
+                  .map((e) => AddressBookTag(
+                      name: e,
+                      tags: selectedTag,
+                      onTap: () {
+                        if (selectedTag.contains(e)) {
+                          selectedTag.remove(e);
+                        } else {
+                          selectedTag.add(e);
+                        }
+                      },
+                      showActionMenu: false))
+                  .toList(growable: false),
+            ),
+          ),
+          // NOT use Offstage to wrap LinearProgressIndicator
+          if (isInProgress) const LinearProgressIndicator(),
+        ],
+      ),
+      actions: [
+        dialogButton("Cancel", onPressed: close, isOutline: true),
+        dialogButton("OK", onPressed: submit),
+      ],
+      onSubmit: submit,
+      onCancel: close,
+    );
+  });
+}
+
+void renameDialog(
+    {required String oldName,
+    FormFieldValidator<String>? validator,
+    required ValueChanged<String> onSubmit,
+    Function? onCancel}) async {
+  RxBool isInProgress = false.obs;
+  var controller = TextEditingController(text: oldName);
+  final formKey = GlobalKey<FormState>();
+  gFFI.dialogManager.show((setState, close, context) {
+    submit() async {
+      String text = controller.text.trim();
+      if (validator != null && formKey.currentState?.validate() == false) {
+        return;
+      }
+      isInProgress.value = true;
+      onSubmit(text);
+      close();
+      isInProgress.value = false;
+    }
+
+    cancel() {
+      onCancel?.call();
+      close();
+    }
+
+    return CustomAlertDialog(
+      title: Row(
+        mainAxisAlignment: MainAxisAlignment.center,
+        children: [
+          Icon(Icons.edit_rounded, color: MyTheme.accent),
+          Text(translate('Rename')).paddingOnly(left: 10),
+        ],
+      ),
+      content: Column(
+        crossAxisAlignment: CrossAxisAlignment.start,
+        children: [
+          Container(
+            child: Form(
+              key: formKey,
+              child: TextFormField(
+                controller: controller,
+                autofocus: true,
+                decoration: InputDecoration(labelText: translate('Name')),
+                validator: validator,
+              ),
+            ),
+          ),
+          // NOT use Offstage to wrap LinearProgressIndicator
+          Obx(() =>
+              isInProgress.value ? const LinearProgressIndicator() : Offstage())
+        ],
+      ),
+      actions: [
+        dialogButton(
+          "Cancel",
+          icon: Icon(Icons.close_rounded),
+          onPressed: cancel,
+          isOutline: true,
+        ),
+        dialogButton(
+          "OK",
+          icon: Icon(Icons.done_rounded),
+          onPressed: submit,
+        ),
+      ],
+      onSubmit: submit,
+      onCancel: cancel,
+    );
+  });
+}
diff --git a/flutter/lib/mobile/widgets/gestures.dart b/flutter/lib/common/widgets/gestures.dart
similarity index 96%
rename from flutter/lib/mobile/widgets/gestures.dart
rename to flutter/lib/common/widgets/gestures.dart
index 77f9c42fd..aeff15041 100644
--- a/flutter/lib/mobile/widgets/gestures.dart
+++ b/flutter/lib/common/widgets/gestures.dart
@@ -113,13 +113,14 @@ class CustomTouchGestureRecognizer extends ScaleGestureRecognizer {
   }
 
   void onOneFingerStartDebounce(ScaleUpdateDetails d) {
-    final start = (ScaleUpdateDetails d) {
+    start(ScaleUpdateDetails d) {
       _currentState = GestureState.oneFingerPan;
       if (onOneFingerPanStart != null) {
         onOneFingerPanStart!(DragStartDetails(
             localPosition: d.localFocalPoint, globalPosition: d.focalPoint));
       }
-    };
+    }
+
     if (_currentState != GestureState.none) {
       _debounceTimer = Timer(Duration(milliseconds: 200), () {
         start(d);
@@ -132,13 +133,14 @@ class CustomTouchGestureRecognizer extends ScaleGestureRecognizer {
   }
 
   void onTwoFingerStartDebounce(ScaleUpdateDetails d) {
-    final start = (ScaleUpdateDetails d) {
+    start(ScaleUpdateDetails d) {
       _currentState = GestureState.twoFingerScale;
       if (onTwoFingerScaleStart != null) {
         onTwoFingerScaleStart!(ScaleStartDetails(
             localFocalPoint: d.localFocalPoint, focalPoint: d.focalPoint));
       }
-    };
+    }
+
     if (_currentState == GestureState.threeFingerVerticalDrag) {
       _debounceTimer = Timer(Duration(milliseconds: 200), () {
         start(d);
@@ -182,6 +184,8 @@ class HoldTapMoveGestureRecognizer extends GestureRecognizer {
   _TapTracker? _firstTap;
   _TapTracker? _secondTap;
 
+  PointerDownEvent? _lastPointerDownEvent;
+
   final Map<int, _TapTracker> _trackers = <int, _TapTracker>{};
 
   @override
@@ -236,6 +240,7 @@ class HoldTapMoveGestureRecognizer extends GestureRecognizer {
       gestureSettings: gestureSettings,
     );
     _trackers[event.pointer] = tracker;
+    _lastPointerDownEvent = event;
     tracker.startTrackingPointer(_handleEvent, event.transform);
   }
 
@@ -246,7 +251,11 @@ class HoldTapMoveGestureRecognizer extends GestureRecognizer {
         _registerFirstTap(tracker);
       } else if (_secondTap != null) {
         if (event.pointer == _secondTap!.pointer) {
-          if (onHoldDragEnd != null) onHoldDragEnd!(DragEndDetails());
+          if (onHoldDragEnd != null) {
+            onHoldDragEnd!(DragEndDetails());
+            _secondTap = null;
+            _isStart = false;
+          }
         }
       } else {
         _reject(tracker);
@@ -266,11 +275,12 @@ class HoldTapMoveGestureRecognizer extends GestureRecognizer {
           if (!_isStart) {
             _resolve();
           }
-          if (onHoldDragUpdate != null)
+          if (onHoldDragUpdate != null) {
             onHoldDragUpdate!(DragUpdateDetails(
                 globalPosition: event.position,
                 localPosition: event.localPosition,
                 delta: event.delta));
+          }
         }
       }
     } else if (event is PointerCancelEvent) {
@@ -300,7 +310,11 @@ class HoldTapMoveGestureRecognizer extends GestureRecognizer {
     _secondTap?.entry.resolve(GestureDisposition.accepted);
     _isStart = true;
     // TODO start details
-    if (onHoldDragStart != null) onHoldDragStart!(DragStartDetails());
+    if (onHoldDragStart != null) {
+      onHoldDragStart!(DragStartDetails(
+        kind: _lastPointerDownEvent?.kind,
+      ));
+    }
   }
 
   void _reject(_TapTracker tracker) {
@@ -432,6 +446,8 @@ class DoubleFinerTapGestureRecognizer extends GestureRecognizer {
   Timer? _firstTapTimer;
   _TapTracker? _firstTap;
 
+  PointerDownEvent? _lastPointerDownEvent;
+
   var _isStart = false;
 
   final Set<int> _upTap = {};
@@ -473,6 +489,7 @@ class DoubleFinerTapGestureRecognizer extends GestureRecognizer {
     } else {
       // first tap
       _isStart = true;
+      _lastPointerDownEvent = event;
       _startFirstTapDownTimer();
     }
     _trackTap(event);
@@ -498,8 +515,9 @@ class DoubleFinerTapGestureRecognizer extends GestureRecognizer {
       debugPrint("PointerUpEvent");
       _upTap.add(tracker.pointer);
     } else if (event is PointerMoveEvent) {
-      if (!tracker.isWithinGlobalTolerance(event, kDoubleTapTouchSlop))
+      if (!tracker.isWithinGlobalTolerance(event, kDoubleTapTouchSlop)) {
         _reject(tracker);
+      }
     } else if (event is PointerCancelEvent) {
       _reject(tracker);
     }
@@ -587,7 +605,11 @@ class DoubleFinerTapGestureRecognizer extends GestureRecognizer {
 
   void _resolve() {
     // TODO tap down details
-    if (onDoubleFinerTap != null) onDoubleFinerTap!(TapDownDetails());
+    if (onDoubleFinerTap != null) {
+      onDoubleFinerTap!(TapDownDetails(
+        kind: _lastPointerDownEvent?.kind,
+      ));
+    }
     _trackers.forEach((key, value) {
       value.entry.resolve(GestureDisposition.accepted);
     });
diff --git a/flutter/lib/common/widgets/login.dart b/flutter/lib/common/widgets/login.dart
index d5e2a9ba3..b26397b94 100644
--- a/flutter/lib/common/widgets/login.dart
+++ b/flutter/lib/common/widgets/login.dart
@@ -12,25 +12,43 @@ import 'package:url_launcher/url_launcher.dart';
 import '../../common.dart';
 import './dialog.dart';
 
+const kOpSvgList = [
+  'github',
+  'gitlab',
+  'google',
+  'apple',
+  'okta',
+  'facebook',
+  'azure',
+  'auth0'
+];
+
 class _IconOP extends StatelessWidget {
-  final String icon;
-  final double iconWidth;
+  final String op;
+  final String? icon;
   final EdgeInsets margin;
   const _IconOP(
       {Key? key,
+      required this.op,
       required this.icon,
-      required this.iconWidth,
       this.margin = const EdgeInsets.symmetric(horizontal: 4.0)})
       : super(key: key);
 
   @override
   Widget build(BuildContext context) {
+    final svgFile =
+        kOpSvgList.contains(op.toLowerCase()) ? op.toLowerCase() : 'default';
     return Container(
       margin: margin,
-      child: SvgPicture.asset(
-        'assets/$icon.svg',
-        width: iconWidth,
-      ),
+      child: icon == null
+          ? SvgPicture.asset(
+              'assets/auth-$svgFile.svg',
+              width: 20,
+            )
+          : SvgPicture.string(
+              icon!,
+              width: 20,
+            ),
     );
   }
 }
@@ -38,7 +56,7 @@ class _IconOP extends StatelessWidget {
 class ButtonOP extends StatelessWidget {
   final String op;
   final RxString curOP;
-  final double iconWidth;
+  final String? icon;
   final Color primaryColor;
   final double height;
   final Function() onTap;
@@ -47,7 +65,7 @@ class ButtonOP extends StatelessWidget {
     Key? key,
     required this.op,
     required this.curOP,
-    required this.iconWidth,
+    required this.icon,
     required this.primaryColor,
     required this.height,
     required this.onTap,
@@ -55,13 +73,18 @@ class ButtonOP extends StatelessWidget {
 
   @override
   Widget build(BuildContext context) {
+    final opLabel = {
+          'github': 'GitHub',
+          'gitlab': 'GitLab'
+        }[op.toLowerCase()] ??
+        toCapitalized(op);
     return Row(children: [
       Container(
         height: height,
         width: 200,
         child: Obx(() => ElevatedButton(
             style: ElevatedButton.styleFrom(
-              primary: curOP.value.isEmpty || curOP.value == op
+              backgroundColor: curOP.value.isEmpty || curOP.value == op
                   ? primaryColor
                   : Colors.grey,
             ).copyWith(elevation: ButtonStyleButton.allOrNull(0.0)),
@@ -69,17 +92,20 @@ class ButtonOP extends StatelessWidget {
             child: Row(
               children: [
                 SizedBox(
-                    width: 30,
-                    child: _IconOP(
-                      icon: op,
-                      iconWidth: iconWidth,
-                      margin: EdgeInsets.only(right: 5),
-                    )),
+                  width: 30,
+                  child: _IconOP(
+                    op: op,
+                    icon: icon,
+                    margin: EdgeInsets.only(right: 5),
+                  ),
+                ),
                 Expanded(
-                    child: FittedBox(
-                        fit: BoxFit.scaleDown,
-                        child: Center(
-                            child: Text('${translate("Continue with")} $op')))),
+                  child: FittedBox(
+                    fit: BoxFit.scaleDown,
+                    child: Center(
+                        child: Text('${translate("Continue with")} $opLabel')),
+                  ),
+                ),
               ],
             ))),
       ),
@@ -89,8 +115,8 @@ class ButtonOP extends StatelessWidget {
 
 class ConfigOP {
   final String op;
-  final double iconWidth;
-  ConfigOP({required this.op, required this.iconWidth});
+  final String? icon;
+  ConfigOP({required this.op, required this.icon});
 }
 
 class WidgetOP extends StatefulWidget {
@@ -182,7 +208,7 @@ class _WidgetOPState extends State<WidgetOP> {
         ButtonOP(
           op: widget.config.op,
           curOP: widget.curOP,
-          iconWidth: widget.config.iconWidth,
+          icon: widget.config.icon,
           primaryColor: str2color(widget.config.op, 0x7f),
           height: 36,
           onTap: () async {
@@ -333,9 +359,8 @@ class LoginWidgetUserPass extends StatelessWidget {
               autoFocus: false,
               errorText: passMsg,
             ),
-            Offstage(
-                offstage: !isInProgress,
-                child: const LinearProgressIndicator()),
+            // NOT use Offstage to wrap LinearProgressIndicator
+            if (isInProgress) const LinearProgressIndicator(),
             const SizedBox(height: 12.0),
             FittedBox(
                 child:
@@ -380,7 +405,7 @@ Future<bool?> loginDialog() async {
 
   final loginOptions = [].obs;
   Future.delayed(Duration.zero, () async {
-    loginOptions.value = await UserModel.queryLoginOptions();
+    loginOptions.value = await UserModel.queryOidcLoginOptions();
   });
 
   final res = await gFFI.dialogManager.show<bool>((setState, close, context) {
@@ -434,11 +459,16 @@ Future<bool?> loginDialog() async {
             }
             break;
           case HttpType.kAuthResTypeEmailCheck:
-            setState(() => isInProgress = false);
-            final res = await verificationCodeDialog(resp.user);
-            if (res == true) {
+            if (isMobile) {
               close(true);
-              return;
+              verificationCodeDialog(resp.user);
+            } else {
+              setState(() => isInProgress = false);
+              final res = await verificationCodeDialog(resp.user);
+              if (res == true) {
+                close(true);
+                return;
+              }
             }
             break;
           default:
@@ -455,12 +485,8 @@ Future<bool?> loginDialog() async {
     }
 
     thirdAuthWidget() => Obx(() {
-          final oidcOptions = loginOptions
-              .where((opt) => opt.startsWith(kAuthReqTypeOidc))
-              .map((opt) => opt.substring(kAuthReqTypeOidc.length))
-              .toList();
           return Offstage(
-            offstage: oidcOptions.isEmpty,
+            offstage: loginOptions.isEmpty,
             child: Column(
               children: [
                 const SizedBox(
@@ -475,12 +501,8 @@ Future<bool?> loginDialog() async {
                   height: 8.0,
                 ),
                 LoginWidgetOP(
-                  ops: [
-                    ConfigOP(op: 'GitHub', iconWidth: 20),
-                    ConfigOP(op: 'Google', iconWidth: 20),
-                    ConfigOP(op: 'Okta', iconWidth: 38),
-                  ]
-                      .where((op) => oidcOptions.contains(op.op.toLowerCase()))
+                  ops: loginOptions
+                      .map((e) => ConfigOP(op: e['name'], icon: e['icon']))
                       .toList(),
                   curOP: curOP,
                   cbLogin: (Map<String, dynamic> authBody) {
@@ -506,14 +528,22 @@ Future<bool?> loginDialog() async {
         Text(
           translate('Login'),
         ).marginOnly(top: MyTheme.dialogPadding),
-        TextButton(
+        InkWell(
           child: Icon(
             Icons.close,
-            size: 20,
-            color: Colors.black54,
+            size: 25,
+            // No need to handle the branch of null.
+            // Because we can ensure the color is not null when debug.
+            color: Theme.of(context)
+                .textTheme
+                .titleLarge
+                ?.color
+                ?.withOpacity(0.55),
           ),
-          onPressed: onDialogCancel,
-        ).marginOnly(top: 5),
+          onTap: onDialogCancel,
+          hoverColor: Colors.red,
+          borderRadius: BorderRadius.circular(5),
+        ).marginOnly(top: 10, right: 15),
       ],
     );
     final titlePadding = EdgeInsets.fromLTRB(MyTheme.dialogPadding, 0, 0, 0);
@@ -647,9 +677,8 @@ Future<bool?> verificationCodeDialog(UserPayload? user) async {
               },
             ),
             */
-            Offstage(
-                offstage: !isInProgress,
-                child: const LinearProgressIndicator()),
+            // NOT use Offstage to wrap LinearProgressIndicator
+            if (isInProgress) const LinearProgressIndicator(),
           ],
         ),
         onCancel: close,
@@ -662,3 +691,22 @@ Future<bool?> verificationCodeDialog(UserPayload? user) async {
 
   return res;
 }
+
+void logOutConfirmDialog() {
+  gFFI.dialogManager.show((setState, close, context) {
+    submit() {
+      close();
+      gFFI.userModel.logOut();
+    }
+
+    return CustomAlertDialog(
+      content: Text(translate("logout_tip")),
+      actions: [
+        dialogButton(translate("Cancel"), onPressed: close, isOutline: true),
+        dialogButton(translate("OK"), onPressed: submit),
+      ],
+      onSubmit: submit,
+      onCancel: close,
+    );
+  });
+}
diff --git a/flutter/lib/common/widgets/overlay.dart b/flutter/lib/common/widgets/overlay.dart
index 73210b5fe..b19034c68 100644
--- a/flutter/lib/common/widgets/overlay.dart
+++ b/flutter/lib/common/widgets/overlay.dart
@@ -26,15 +26,32 @@ class DraggableChatWindow extends StatelessWidget {
 
   @override
   Widget build(BuildContext context) {
-    return Draggable(
+    return isIOS
+    ? IOSDraggable (
+      position: position,
+      chatModel: chatModel,
+      width: width,
+      height: height,
+      builder: (context) {
+    return Column(
+      children: [
+        _buildMobileAppBar(context),
+        Expanded(
+          child: ChatPage(chatModel: chatModel),
+              ),
+            ],
+          );
+        },
+      )
+    : Draggable(
         checkKeyboard: true,
         position: position,
         width: width,
         height: height,
+        chatModel: chatModel,
         builder: (context, onPanUpdate) {
-          return isIOS
-              ? ChatPage(chatModel: chatModel)
-              : Scaffold(
+          final child = 
+              Scaffold(
                   resizeToAvoidBottomInset: false,
                   appBar: CustomAppBar(
                     onPanUpdate: onPanUpdate,
@@ -44,6 +61,10 @@ class DraggableChatWindow extends StatelessWidget {
                   ),
                   body: ChatPage(chatModel: chatModel),
                 );
+          return Container(
+              decoration:
+                  BoxDecoration(border: Border.all(color: MyTheme.border)),
+              child: child);
         });
   }
 
@@ -222,6 +243,7 @@ class Draggable extends StatefulWidget {
       this.position = Offset.zero,
       required this.width,
       required this.height,
+      this.chatModel,
       required this.builder})
       : super(key: key);
 
@@ -230,6 +252,7 @@ class Draggable extends StatefulWidget {
   final Offset position;
   final double width;
   final double height;
+  final ChatModel? chatModel;
   final Widget Function(BuildContext, GestureDragUpdateCallback) builder;
 
   @override
@@ -238,6 +261,7 @@ class Draggable extends StatefulWidget {
 
 class _DraggableState extends State<Draggable> {
   late Offset _position;
+  late ChatModel? _chatModel;
   bool _keyboardVisible = false;
   double _saveHeight = 0;
   double _lastBottomHeight = 0;
@@ -246,6 +270,7 @@ class _DraggableState extends State<Draggable> {
   void initState() {
     super.initState();
     _position = widget.position;
+    _chatModel = widget.chatModel;
   }
 
   void onPanUpdate(DragUpdateDetails d) {
@@ -272,6 +297,7 @@ class _DraggableState extends State<Draggable> {
     setState(() {
       _position = Offset(x, y);
     });
+    _chatModel?.setChatWindowPosition(_position);
   }
 
   checkScreenSize() {}
@@ -327,6 +353,107 @@ class _DraggableState extends State<Draggable> {
   }
 }
 
+class IOSDraggable extends StatefulWidget {
+  const IOSDraggable({
+    Key? key,
+    this.position = Offset.zero,
+    this.chatModel,
+    required this.width,
+    required this.height,
+    required this.builder})
+    : super(key: key);
+
+  final Offset position;
+  final ChatModel? chatModel;
+  final double width;
+  final double height;
+  final Widget Function(BuildContext) builder;
+
+  @override
+  _IOSDraggableState createState() => _IOSDraggableState();
+}
+
+class _IOSDraggableState extends State<IOSDraggable> {
+  late Offset _position;
+  late ChatModel? _chatModel;
+  late double _width;
+  late double _height;
+  bool _keyboardVisible = false;
+  double _saveHeight = 0;
+  double _lastBottomHeight = 0;
+
+  @override
+  void initState() {
+    super.initState();
+    _position = widget.position;
+    _chatModel = widget.chatModel;
+    _width = widget.width;
+    _height = widget.height;
+  }
+
+  checkKeyboard() {
+    final bottomHeight = MediaQuery.of(context).viewInsets.bottom;
+    final currentVisible = bottomHeight != 0;
+
+    // save
+    if (!_keyboardVisible && currentVisible) {
+      _saveHeight = _position.dy;
+    }
+
+    // reset
+    if (_lastBottomHeight > 0 && bottomHeight == 0) {
+      setState(() {
+        _position = Offset(_position.dx, _saveHeight);
+      });
+    }
+
+    // onKeyboardVisible
+    if (_keyboardVisible && currentVisible) {
+      final sumHeight = bottomHeight + _height;
+      final contextHeight = MediaQuery.of(context).size.height;
+      if (sumHeight + _position.dy > contextHeight) {
+        final y = contextHeight - sumHeight;
+        setState(() {
+          _position = Offset(_position.dx, y);
+        });
+      }
+    }
+
+    _keyboardVisible = currentVisible;
+    _lastBottomHeight = bottomHeight;
+  }
+
+@override
+  Widget build(BuildContext context) {
+    checkKeyboard();
+    return Stack(
+      children: [
+        Positioned(
+          left: _position.dx,
+          top: _position.dy,
+          child: GestureDetector(
+            onPanUpdate: (details) {
+              setState(() {
+                _position += details.delta;
+              });
+              _chatModel?.setChatWindowPosition(_position);
+            },
+            child: Material(
+              child:
+            Container(
+              width: _width,
+              height: _height,
+              decoration: BoxDecoration(border: Border.all(color: MyTheme.border)),
+              child: widget.builder(context),
+              ),
+            ),
+          ),
+        ),
+      ],
+    );
+  }
+}
+
 class QualityMonitor extends StatelessWidget {
   final QualityMonitorModel qualityMonitorModel;
   QualityMonitor(this.qualityMonitorModel);
diff --git a/flutter/lib/common/widgets/peer_card.dart b/flutter/lib/common/widgets/peer_card.dart
index 09d1cb521..f5af94220 100644
--- a/flutter/lib/common/widgets/peer_card.dart
+++ b/flutter/lib/common/widgets/peer_card.dart
@@ -2,9 +2,11 @@ import 'dart:io';
 
 import 'package:flutter/material.dart';
 import 'package:flutter/services.dart';
-import 'package:flutter_hbb/common/widgets/address_book.dart';
+import 'package:flutter_hbb/common/widgets/dialog.dart';
 import 'package:flutter_hbb/consts.dart';
+import 'package:flutter_hbb/models/peer_tab_model.dart';
 import 'package:get/get.dart';
+import 'package:provider/provider.dart';
 
 import '../../common.dart';
 import '../../common/formatter/id_formatter.dart';
@@ -12,6 +14,7 @@ import '../../models/peer_model.dart';
 import '../../models/platform_model.dart';
 import '../../desktop/widgets/material_mod_popup_menu.dart' as mod_menu;
 import '../../desktop/widgets/popup_menu.dart';
+import 'dart:math' as math;
 
 typedef PopupMenuEntryBuilder = Future<List<mod_menu.PopupMenuEntry<String>>>
     Function(BuildContext);
@@ -22,11 +25,13 @@ final peerCardUiType = PeerUiType.grid.obs;
 
 class _PeerCard extends StatefulWidget {
   final Peer peer;
+  final PeerTabIndex tab;
   final Function(BuildContext, String) connect;
   final PopupMenuEntryBuilder popupMenuEntryBuilder;
 
   const _PeerCard(
       {required this.peer,
+      required this.tab,
       required this.connect,
       required this.popupMenuEntryBuilder,
       Key? key})
@@ -56,65 +61,33 @@ class _PeerCardState extends State<_PeerCard>
 
   Widget _buildMobile() {
     final peer = super.widget.peer;
-    final name =
-        '${peer.username}${peer.username.isNotEmpty && peer.hostname.isNotEmpty ? '@' : ''}${peer.hostname}';
-
+    final PeerTabModel peerTabModel = Provider.of(context);
     return Card(
         margin: EdgeInsets.symmetric(horizontal: 2),
         child: GestureDetector(
-            onTap: !isWebDesktop ? () => connect(context, peer.id) : null,
-            onDoubleTap: isWebDesktop ? () => connect(context, peer.id) : null,
-            onLongPressStart: (details) {
-              final x = details.globalPosition.dx;
-              final y = details.globalPosition.dy;
-              _menuPos = RelativeRect.fromLTRB(x, y, x, y);
-              _showPeerMenu(peer.id);
-            },
-            child: Container(
+          onTap: () {
+            if (peerTabModel.multiSelectionMode) {
+              peerTabModel.select(peer);
+            } else {
+              if (!isWebDesktop) {
+                connectInPeerTab(context, peer.id, widget.tab);
+              }
+            }
+          },
+          onDoubleTap: isWebDesktop
+              ? () => connectInPeerTab(context, peer.id, widget.tab)
+              : null,
+          onLongPress: () {
+            peerTabModel.select(peer);
+          },
+          child: Container(
               padding: EdgeInsets.only(left: 12, top: 8, bottom: 8),
-              child: Row(
-                children: [
-                  Container(
-                      width: 50,
-                      height: 50,
-                      decoration: BoxDecoration(
-                        color: str2color('${peer.id}${peer.platform}', 0x7f),
-                        borderRadius: BorderRadius.circular(4),
-                      ),
-                      padding: const EdgeInsets.all(6),
-                      child: getPlatformImage(peer.platform)),
-                  Expanded(
-                    child: Column(
-                      crossAxisAlignment: CrossAxisAlignment.start,
-                      children: [
-                        Row(children: [
-                          getOnline(4, peer.online),
-                          Text(peer.alias.isEmpty
-                              ? formatID(peer.id)
-                              : peer.alias)
-                        ]),
-                        Text(name)
-                      ],
-                    ).paddingOnly(left: 8.0),
-                  ),
-                  InkWell(
-                      child: const Padding(
-                          padding: 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(peer.id);
-                      })
-                ],
-              ),
-            )));
+              child: _buildPeerTile(context, peer, null)),
+        ));
   }
 
   Widget _buildDesktop() {
+    final PeerTabModel peerTabModel = Provider.of(context);
     final peer = super.widget.peer;
     var deco = Rx<BoxDecoration?>(
       BoxDecoration(
@@ -144,7 +117,12 @@ class _PeerCardState extends State<_PeerCard>
         );
       },
       child: GestureDetector(
-          onDoubleTap: () => widget.connect(context, peer.id),
+          onDoubleTap:
+              peerTabModel.multiSelectionMode || peerTabModel.isShiftDown
+                  ? null
+                  : () => widget.connect(context, peer.id),
+          onTap: () => peerTabModel.select(peer),
+          onLongPress: () => peerTabModel.select(peer),
           child: Obx(() => peerCardUiType.value == PeerUiType.grid
               ? _buildPeerCard(context, peer, deco)
               : _buildPeerTile(context, peer, deco))),
@@ -152,74 +130,101 @@ class _PeerCardState extends State<_PeerCard>
   }
 
   Widget _buildPeerTile(
-      BuildContext context, Peer peer, Rx<BoxDecoration?> deco) {
+      BuildContext context, Peer peer, Rx<BoxDecoration?>? deco) {
     final name =
         '${peer.username}${peer.username.isNotEmpty && peer.hostname.isNotEmpty ? '@' : ''}${peer.hostname}';
     final greyStyle = TextStyle(
         fontSize: 11,
         color: Theme.of(context).textTheme.titleLarge?.color?.withOpacity(0.6));
-    final alias = bind.mainGetPeerOptionSync(id: peer.id, key: 'alias');
-    return Obx(
-      () => Container(
-        foregroundDecoration: deco.value,
-        child: Row(
-          mainAxisSize: MainAxisSize.max,
-          children: [
-            Container(
-              decoration: BoxDecoration(
-                color: str2color('${peer.id}${peer.platform}', 0x7f),
-                borderRadius: BorderRadius.only(
-                  topLeft: Radius.circular(_tileRadius),
-                  bottomLeft: Radius.circular(_tileRadius),
-                ),
-              ),
-              alignment: Alignment.center,
-              width: 42,
-              child: getPlatformImage(peer.platform, size: 30).paddingAll(6),
-            ),
-            Expanded(
-              child: Container(
-                decoration: BoxDecoration(
-                  color: Theme.of(context).colorScheme.background,
-                  borderRadius: BorderRadius.only(
-                    topRight: Radius.circular(_tileRadius),
-                    bottomRight: Radius.circular(_tileRadius),
+    final child = Row(
+      mainAxisSize: MainAxisSize.max,
+      children: [
+        Container(
+          decoration: BoxDecoration(
+            color: str2color('${peer.id}${peer.platform}', 0x7f),
+            borderRadius: isMobile
+                ? BorderRadius.circular(_tileRadius)
+                : BorderRadius.only(
+                    topLeft: Radius.circular(_tileRadius),
+                    bottomLeft: Radius.circular(_tileRadius),
                   ),
-                ),
-                child: Row(
-                  children: [
-                    Expanded(
-                      child: Column(
-                        children: [
-                          Row(children: [
-                            getOnline(8, peer.online),
-                            Expanded(
-                                child: Text(
-                              alias.isEmpty ? formatID(peer.id) : alias,
-                              overflow: TextOverflow.ellipsis,
-                              style: Theme.of(context).textTheme.titleSmall,
-                            )),
-                          ]).marginOnly(bottom: 2),
-                          Align(
-                            alignment: Alignment.centerLeft,
-                            child: Text(
-                              name,
-                              style: greyStyle,
-                              textAlign: TextAlign.start,
-                              overflow: TextOverflow.ellipsis,
-                            ),
-                          ),
-                        ],
-                      ).marginOnly(top: 2),
-                    ),
-                    _actionMore(peer),
-                  ],
-                ).paddingOnly(left: 10.0, top: 3.0),
-              ),
-            )
-          ],
+          ),
+          alignment: Alignment.center,
+          width: isMobile ? 50 : 42,
+          height: isMobile ? 50 : null,
+          child: getPlatformImage(peer.platform, size: isMobile ? 38 : 30)
+              .paddingAll(6),
         ),
-      ),
+        Expanded(
+          child: Container(
+            decoration: BoxDecoration(
+              color: Theme.of(context).colorScheme.background,
+              borderRadius: BorderRadius.only(
+                topRight: Radius.circular(_tileRadius),
+                bottomRight: Radius.circular(_tileRadius),
+              ),
+            ),
+            child: Row(
+              children: [
+                Expanded(
+                  child: Column(
+                    children: [
+                      Row(children: [
+                        getOnline(isMobile ? 4 : 8, peer.online),
+                        Expanded(
+                            child: Text(
+                          peer.alias.isEmpty ? formatID(peer.id) : peer.alias,
+                          overflow: TextOverflow.ellipsis,
+                          style: Theme.of(context).textTheme.titleSmall,
+                        )),
+                      ]).marginOnly(top: isMobile ? 0 : 2),
+                      Align(
+                        alignment: Alignment.centerLeft,
+                        child: Text(
+                          name,
+                          style: isMobile ? null : greyStyle,
+                          textAlign: TextAlign.start,
+                          overflow: TextOverflow.ellipsis,
+                        ),
+                      ),
+                    ],
+                  ).marginOnly(top: 2),
+                ),
+                isMobile
+                    ? checkBoxOrActionMoreMobile(peer)
+                    : checkBoxOrActionMoreDesktop(peer, isTile: true),
+              ],
+            ).paddingOnly(left: 10.0, top: 3.0),
+          ),
+        )
+      ],
+    );
+    final colors =
+        _frontN(peer.tags, 25).map((e) => gFFI.abModel.getTagColor(e)).toList();
+    return Tooltip(
+      message: isMobile
+          ? ''
+          : peer.tags.isNotEmpty
+              ? '${translate('Tags')}: ${peer.tags.join(', ')}'
+              : '',
+      child: Stack(children: [
+        deco == null
+            ? child
+            : Obx(
+                () => Container(
+                  foregroundDecoration: deco.value,
+                  child: child,
+                ),
+              ),
+        if (colors.isNotEmpty)
+          Positioned(
+            top: 2,
+            right: isMobile ? 20 : 10,
+            child: CustomPaint(
+              painter: TagPainter(radius: 3, colors: colors),
+            ),
+          )
+      ]),
     );
   }
 
@@ -227,7 +232,7 @@ class _PeerCardState extends State<_PeerCard>
       BuildContext context, Peer peer, Rx<BoxDecoration?> deco) {
     final name =
         '${peer.username}${peer.username.isNotEmpty && peer.hostname.isNotEmpty ? '@' : ''}${peer.hostname}';
-    return Card(
+    final child = Card(
       color: Colors.transparent,
       elevation: 0,
       margin: EdgeInsets.zero,
@@ -253,7 +258,7 @@ class _PeerCardState extends State<_PeerCard>
                                 padding: const EdgeInsets.all(6),
                                 child:
                                     getPlatformImage(peer.platform, size: 60),
-                              ),
+                              ).marginOnly(top: 4),
                               Row(
                                 children: [
                                   Expanded(
@@ -294,7 +299,7 @@ class _PeerCardState extends State<_PeerCard>
                           style: Theme.of(context).textTheme.titleSmall,
                         )),
                       ]).paddingSymmetric(vertical: 8)),
-                      _actionMore(peer),
+                      checkBoxOrActionMoreDesktop(peer, isTile: false),
                     ],
                   ).paddingSymmetric(horizontal: 12.0),
                 )
@@ -304,6 +309,87 @@ class _PeerCardState extends State<_PeerCard>
         ),
       ),
     );
+
+    final colors =
+        _frontN(peer.tags, 25).map((e) => gFFI.abModel.getTagColor(e)).toList();
+    return Tooltip(
+      message: peer.tags.isNotEmpty
+          ? '${translate('Tags')}: ${peer.tags.join(', ')}'
+          : '',
+      child: Stack(children: [
+        child,
+        if (colors.isNotEmpty)
+          Positioned(
+            top: 4,
+            right: 12,
+            child: CustomPaint(
+              painter: TagPainter(radius: 4, colors: colors),
+            ),
+          )
+      ]),
+    );
+  }
+
+  List _frontN<T>(List list, int n) {
+    if (list.length <= n) {
+      return list;
+    } else {
+      return list.sublist(0, n);
+    }
+  }
+
+  Widget checkBoxOrActionMoreMobile(Peer peer) {
+    final PeerTabModel peerTabModel = Provider.of(context);
+    final selected = peerTabModel.isPeerSelected(peer.id);
+    if (peerTabModel.multiSelectionMode) {
+      return Padding(
+        padding: const EdgeInsets.all(12),
+        child: selected
+            ? Icon(
+                Icons.check_box,
+                color: MyTheme.accent,
+              )
+            : Icon(Icons.check_box_outline_blank),
+      );
+    } else {
+      return InkWell(
+          child: const Padding(
+              padding: 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(peer.id);
+          });
+    }
+  }
+
+  Widget checkBoxOrActionMoreDesktop(Peer peer, {required bool isTile}) {
+    final PeerTabModel peerTabModel = Provider.of(context);
+    final selected = peerTabModel.isPeerSelected(peer.id);
+    if (peerTabModel.multiSelectionMode) {
+      final icon = selected
+          ? Icon(
+              Icons.check_box,
+              color: MyTheme.accent,
+            )
+          : Icon(Icons.check_box_outline_blank);
+      bool last = peerTabModel.isShiftDown && peer.id == peerTabModel.lastId;
+      double right = isTile ? 4 : 0;
+      if (last) {
+        return Container(
+          decoration: BoxDecoration(
+              border: Border.all(color: MyTheme.accent, width: 1)),
+          child: icon,
+        ).marginOnly(right: right);
+      } else {
+        return icon.marginOnly(right: right);
+      }
+    } else {
+      return _actionMore(peer);
+    }
   }
 
   Widget _actionMore(Peer peer) => Listener(
@@ -332,16 +418,20 @@ class _PeerCardState extends State<_PeerCard>
 
 abstract class BasePeerCard extends StatelessWidget {
   final Peer peer;
+  final PeerTabIndex tab;
   final EdgeInsets? menuPadding;
 
-  BasePeerCard({required this.peer, this.menuPadding, Key? key})
+  BasePeerCard(
+      {required this.peer, required this.tab, this.menuPadding, Key? key})
       : super(key: key);
 
   @override
   Widget build(BuildContext context) {
     return _PeerCard(
       peer: peer,
-      connect: (BuildContext context, String id) => connect(context, id),
+      tab: tab,
+      connect: (BuildContext context, String id) =>
+          connectInPeerTab(context, id, tab),
       popupMenuEntryBuilder: _buildPopupMenuEntry,
     );
   }
@@ -362,19 +452,23 @@ abstract class BasePeerCard extends StatelessWidget {
   Future<List<MenuEntryBase<String>>> _buildMenuItems(BuildContext context);
 
   MenuEntryBase<String> _connectCommonAction(
-      BuildContext context, String id, String title,
-      {bool isFileTransfer = false,
-      bool isTcpTunneling = false,
-      bool isRDP = false}) {
+    BuildContext context,
+    String id,
+    String title, {
+    bool isFileTransfer = false,
+    bool isTcpTunneling = false,
+    bool isRDP = false,
+  }) {
     return MenuEntryButton<String>(
       childBuilder: (TextStyle? style) => Text(
         title,
         style: style,
       ),
       proc: () {
-        connect(
+        connectInPeerTab(
           context,
           peer.id,
+          tab,
           isFileTransfer: isFileTransfer,
           isTcpTunneling: isTcpTunneling,
           isRDP: isRDP,
@@ -388,11 +482,12 @@ abstract class BasePeerCard extends StatelessWidget {
   @protected
   MenuEntryBase<String> _connectAction(BuildContext context, Peer peer) {
     return _connectCommonAction(
-        context,
-        peer.id,
-        peer.alias.isEmpty
-            ? translate('Connect')
-            : "${translate('Connect')} ${peer.id}");
+      context,
+      peer.id,
+      (peer.alias.isEmpty
+          ? translate('Connect')
+          : '${translate('Connect')} ${peer.id}'),
+    );
   }
 
   @protected
@@ -446,7 +541,7 @@ abstract class BasePeerCard extends StatelessWidget {
             ],
           )),
       proc: () {
-        connect(context, id, isRDP: true);
+        connectInPeerTab(context, id, tab, isRDP: true);
       },
       padding: menuPadding,
       dismissOnClicked: true,
@@ -478,21 +573,48 @@ abstract class BasePeerCard extends StatelessWidget {
       ),
       proc: () {
         bind.mainCreateShortcut(id: id);
+        showToast(translate('Successful'));
       },
       padding: menuPadding,
       dismissOnClicked: true,
     );
   }
 
+  Future<MenuEntryBase<String>> _openNewConnInAction(
+      String id, String label, String key) async {
+    return MenuEntrySwitch<String>(
+      switchType: SwitchType.scheckbox,
+      text: translate(label),
+      getter: () async => mainGetPeerBoolOptionSync(id, key),
+      setter: (bool v) async {
+        await bind.mainSetPeerOption(
+            id: id, key: key, value: bool2option(key, v));
+        showToast(translate('Successful'));
+      },
+      padding: menuPadding,
+      dismissOnClicked: true,
+    );
+  }
+
+  _openInTabsAction(String id) async =>
+      await _openNewConnInAction(id, 'Open in New Tab', kOptionOpenInTabs);
+
+  _openInWindowsAction(String id) async => await _openNewConnInAction(
+      id, 'Open in New Window', kOptionOpenInWindows);
+
+  _openNewConnInOptAction(String id) async =>
+      mainGetLocalBoolOptionSync(kOptionOpenNewConnInTabs)
+          ? await _openInWindowsAction(id)
+          : await _openInTabsAction(id);
+
   @protected
   Future<bool> _isForceAlwaysRelay(String id) async {
-    return (await bind.mainGetPeerOption(id: id, key: 'force-always-relay'))
+    return (await bind.mainGetPeerOption(id: id, key: kOptionForceAlwaysRelay))
         .isNotEmpty;
   }
 
   @protected
   Future<MenuEntryBase<String>> _forceAlwaysRelayAction(String id) async {
-    const option = 'force-always-relay';
     return MenuEntrySwitch<String>(
       switchType: SwitchType.scheckbox,
       text: translate('Always connect via relay'),
@@ -500,9 +622,11 @@ abstract class BasePeerCard extends StatelessWidget {
         return await _isForceAlwaysRelay(id);
       },
       setter: (bool v) async {
-        gFFI.abModel.setPeerForceAlwaysRelay(id, v);
         await bind.mainSetPeerOption(
-            id: id, key: option, value: bool2option(option, v));
+            id: id,
+            key: kOptionForceAlwaysRelay,
+            value: bool2option(kOptionForceAlwaysRelay, v));
+        showToast(translate('Successful'));
       },
       padding: menuPadding,
       dismissOnClicked: true,
@@ -516,8 +640,23 @@ abstract class BasePeerCard extends StatelessWidget {
         translate('Rename'),
         style: style,
       ),
-      proc: () {
-        _rename(id);
+      proc: () async {
+        String oldName = await _getAlias(id);
+        renameDialog(
+            oldName: oldName,
+            onSubmit: (String newName) async {
+              if (newName != oldName) {
+                if (tab == PeerTabIndex.ab) {
+                  gFFI.abModel.changeAlias(id: id, alias: newName);
+                  await bind.mainSetPeerAlias(id: id, alias: newName);
+                  gFFI.abModel.pushAb();
+                } else {
+                  await bind.mainSetPeerAlias(id: id, alias: newName);
+                  showToast(translate('Successful'));
+                  _update();
+                }
+              }
+            });
       },
       padding: menuPadding,
       dismissOnClicked: true,
@@ -525,9 +664,7 @@ abstract class BasePeerCard extends StatelessWidget {
   }
 
   @protected
-  MenuEntryBase<String> _removeAction(
-      String id, Future<void> Function() reloadFunc,
-      {bool isLan = false}) {
+  MenuEntryBase<String> _removeAction(String id) {
     return MenuEntryButton<String>(
       childBuilder: (TextStyle? style) => Row(
         children: [
@@ -546,7 +683,40 @@ abstract class BasePeerCard extends StatelessWidget {
         ],
       ),
       proc: () {
-        _delete(id, isLan, reloadFunc);
+        onSubmit() async {
+          switch (tab) {
+            case PeerTabIndex.recent:
+              await bind.mainRemovePeer(id: id);
+              await bind.mainLoadRecentPeers();
+              break;
+            case PeerTabIndex.fav:
+              final favs = (await bind.mainGetFav()).toList();
+              if (favs.remove(id)) {
+                await bind.mainStoreFav(favs: favs);
+                await bind.mainLoadFavPeers();
+              }
+              break;
+            case PeerTabIndex.lan:
+              await bind.mainRemoveDiscovered(id: id);
+              await bind.mainLoadLanPeers();
+              break;
+            case PeerTabIndex.ab:
+              gFFI.abModel.deletePeer(id);
+              final future = gFFI.abModel.pushAb();
+              if (await bind.mainPeerExists(id: peer.id)) {
+                gFFI.abModel.reSyncToast(future);
+              }
+              break;
+            case PeerTabIndex.group:
+              break;
+          }
+          if (tab != PeerTabIndex.ab) {
+            showToast(translate('Successful'));
+          }
+        }
+
+        deletePeerConfirmDialog(onSubmit,
+            '${translate('Delete')} "${peer.alias.isEmpty ? formatID(peer.id) : peer.alias}"?');
       },
       padding: menuPadding,
       dismissOnClicked: true,
@@ -560,8 +730,15 @@ abstract class BasePeerCard extends StatelessWidget {
         translate('Unremember Password'),
         style: style,
       ),
-      proc: () {
-        bind.mainForgetPassword(id: id);
+      proc: () async {
+        bool result = gFFI.abModel.changePassword(id, '');
+        await bind.mainForgetPassword(id: id);
+        bool toast = false;
+        if (result) {
+          toast = tab == PeerTabIndex.ab;
+          gFFI.abModel.pushAb(toastIfFail: toast, toastIfSucc: toast);
+        }
+        if (!toast) showToast(translate('Successful'));
       },
       padding: menuPadding,
       dismissOnClicked: true,
@@ -594,6 +771,7 @@ abstract class BasePeerCard extends StatelessWidget {
             favs.add(id);
             await bind.mainStoreFav(favs: favs);
           }
+          showToast(translate('Successful'));
         }();
       },
       padding: menuPadding,
@@ -628,6 +806,7 @@ abstract class BasePeerCard extends StatelessWidget {
             await bind.mainStoreFav(favs: favs);
             await reloadFunc();
           }
+          showToast(translate('Successful'));
         }();
       },
       padding: menuPadding,
@@ -644,9 +823,12 @@ abstract class BasePeerCard extends StatelessWidget {
       ),
       proc: () {
         () async {
+          if (gFFI.abModel.isFull(true)) {
+            return;
+          }
           if (!gFFI.abModel.idContainBy(peer.id)) {
             gFFI.abModel.addPeer(peer);
-            await gFFI.abModel.pushAb();
+            gFFI.abModel.pushAb();
           }
         }();
       },
@@ -659,123 +841,17 @@ abstract class BasePeerCard extends StatelessWidget {
   Future<String> _getAlias(String id) async =>
       await bind.mainGetPeerOption(id: id, key: 'alias');
 
-  void _rename(String id) async {
-    RxBool isInProgress = false.obs;
-    String name = await _getAlias(id);
-    var controller = TextEditingController(text: name);
-    gFFI.dialogManager.show((setState, close, context) {
-      submit() async {
-        isInProgress.value = true;
-        String name = controller.text.trim();
-        await bind.mainSetPeerAlias(id: id, alias: name);
-        gFFI.abModel.setPeerAlias(id, name);
-        _update();
-        close();
-        isInProgress.value = false;
-      }
-
-      return CustomAlertDialog(
-        title: Row(
-          mainAxisAlignment: MainAxisAlignment.center,
-          children: [
-            Icon(Icons.edit_rounded, color: MyTheme.accent),
-            Text(translate('Rename')).paddingOnly(left: 10),
-          ],
-        ),
-        content: Column(
-          crossAxisAlignment: CrossAxisAlignment.start,
-          children: [
-            Container(
-              child: Form(
-                child: TextFormField(
-                  controller: controller,
-                  autofocus: true,
-                  decoration: InputDecoration(labelText: translate('Name')),
-                ),
-              ),
-            ),
-            Obx(() => Offstage(
-                offstage: isInProgress.isFalse,
-                child: const LinearProgressIndicator())),
-          ],
-        ),
-        actions: [
-          dialogButton(
-            "Cancel",
-            icon: Icon(Icons.close_rounded),
-            onPressed: close,
-            isOutline: true,
-          ),
-          dialogButton(
-            "OK",
-            icon: Icon(Icons.done_rounded),
-            onPressed: submit,
-          ),
-        ],
-        onSubmit: submit,
-        onCancel: close,
-      );
-    });
-  }
-
   @protected
   void _update();
-
-  void _delete(String id, bool isLan, Function reloadFunc) async {
-    gFFI.dialogManager.show(
-      (setState, close, context) {
-        submit() async {
-          if (isLan) {
-            await bind.mainRemoveDiscovered(id: id);
-          } else {
-            final favs = (await bind.mainGetFav()).toList();
-            if (favs.remove(id)) {
-              await bind.mainStoreFav(favs: favs);
-            }
-            await bind.mainRemovePeer(id: id);
-          }
-          await reloadFunc();
-          close();
-        }
-
-        return CustomAlertDialog(
-          title: Row(
-            mainAxisAlignment: MainAxisAlignment.center,
-            children: [
-              Icon(
-                Icons.delete_rounded,
-                color: Colors.red,
-              ),
-              Text(translate('Delete')).paddingOnly(
-                left: 10,
-              ),
-            ],
-          ),
-          content: SizedBox.shrink(),
-          actions: [
-            dialogButton(
-              "Cancel",
-              icon: Icon(Icons.close_rounded),
-              onPressed: close,
-              isOutline: true,
-            ),
-            dialogButton(
-              "OK",
-              icon: Icon(Icons.done_rounded),
-              onPressed: submit,
-            ),
-          ],
-          onSubmit: submit,
-          onCancel: close,
-        );
-      },
-    );
-  }
 }
 
 class RecentPeerCard extends BasePeerCard {
   RecentPeerCard({required Peer peer, EdgeInsets? menuPadding, Key? key})
-      : super(peer: peer, menuPadding: menuPadding, key: key);
+      : super(
+            peer: peer,
+            tab: PeerTabIndex.recent,
+            menuPadding: menuPadding,
+            key: key);
 
   @override
   Future<List<MenuEntryBase<String>>> _buildMenuItems(
@@ -790,11 +866,11 @@ class RecentPeerCard extends BasePeerCard {
     if (isDesktop && peer.platform != 'Android') {
       menuItems.add(_tcpTunnelingAction(context, peer.id));
     }
+    // menuItems.add(await _openNewConnInOptAction(peer.id));
     menuItems.add(await _forceAlwaysRelayAction(peer.id));
     if (peer.platform == 'Windows') {
       menuItems.add(_rdpAction(context, peer.id));
     }
-    menuItems.add(_wolAction(peer.id));
     if (Platform.isWindows) {
       menuItems.add(_createShortCutAction(peer.id));
     }
@@ -817,9 +893,7 @@ class RecentPeerCard extends BasePeerCard {
     }
 
     menuItems.add(MenuEntryDivider());
-    menuItems.add(_removeAction(peer.id, () async {
-      await bind.mainLoadRecentPeers();
-    }));
+    menuItems.add(_removeAction(peer.id));
     return menuItems;
   }
 
@@ -830,7 +904,11 @@ class RecentPeerCard extends BasePeerCard {
 
 class FavoritePeerCard extends BasePeerCard {
   FavoritePeerCard({required Peer peer, EdgeInsets? menuPadding, Key? key})
-      : super(peer: peer, menuPadding: menuPadding, key: key);
+      : super(
+            peer: peer,
+            tab: PeerTabIndex.fav,
+            menuPadding: menuPadding,
+            key: key);
 
   @override
   Future<List<MenuEntryBase<String>>> _buildMenuItems(
@@ -842,11 +920,11 @@ class FavoritePeerCard extends BasePeerCard {
     if (isDesktop && peer.platform != 'Android') {
       menuItems.add(_tcpTunnelingAction(context, peer.id));
     }
+    // menuItems.add(await _openNewConnInOptAction(peer.id));
     menuItems.add(await _forceAlwaysRelayAction(peer.id));
     if (peer.platform == 'Windows') {
       menuItems.add(_rdpAction(context, peer.id));
     }
-    menuItems.add(_wolAction(peer.id));
     if (Platform.isWindows) {
       menuItems.add(_createShortCutAction(peer.id));
     }
@@ -866,9 +944,7 @@ class FavoritePeerCard extends BasePeerCard {
     }
 
     menuItems.add(MenuEntryDivider());
-    menuItems.add(_removeAction(peer.id, () async {
-      await bind.mainLoadFavPeers();
-    }));
+    menuItems.add(_removeAction(peer.id));
     return menuItems;
   }
 
@@ -879,7 +955,11 @@ class FavoritePeerCard extends BasePeerCard {
 
 class DiscoveredPeerCard extends BasePeerCard {
   DiscoveredPeerCard({required Peer peer, EdgeInsets? menuPadding, Key? key})
-      : super(peer: peer, menuPadding: menuPadding, key: key);
+      : super(
+            peer: peer,
+            tab: PeerTabIndex.lan,
+            menuPadding: menuPadding,
+            key: key);
 
   @override
   Future<List<MenuEntryBase<String>>> _buildMenuItems(
@@ -894,6 +974,7 @@ class DiscoveredPeerCard extends BasePeerCard {
     if (isDesktop && peer.platform != 'Android') {
       menuItems.add(_tcpTunnelingAction(context, peer.id));
     }
+    // menuItems.add(await _openNewConnInOptAction(peer.id));
     menuItems.add(await _forceAlwaysRelayAction(peer.id));
     if (peer.platform == 'Windows') {
       menuItems.add(_rdpAction(context, peer.id));
@@ -916,11 +997,7 @@ class DiscoveredPeerCard extends BasePeerCard {
     }
 
     menuItems.add(MenuEntryDivider());
-    menuItems.add(
-      _removeAction(peer.id, () async {
-        await bind.mainLoadLanPeers();
-      }, isLan: true),
-    );
+    menuItems.add(_removeAction(peer.id));
     return menuItems;
   }
 
@@ -931,7 +1008,11 @@ class DiscoveredPeerCard extends BasePeerCard {
 
 class AddressBookPeerCard extends BasePeerCard {
   AddressBookPeerCard({required Peer peer, EdgeInsets? menuPadding, Key? key})
-      : super(peer: peer, menuPadding: menuPadding, key: key);
+      : super(
+            peer: peer,
+            tab: PeerTabIndex.ab,
+            menuPadding: menuPadding,
+            key: key);
 
   @override
   Future<List<MenuEntryBase<String>>> _buildMenuItems(
@@ -943,17 +1024,17 @@ class AddressBookPeerCard extends BasePeerCard {
     if (isDesktop && peer.platform != 'Android') {
       menuItems.add(_tcpTunnelingAction(context, peer.id));
     }
+    // menuItems.add(await _openNewConnInOptAction(peer.id));
     menuItems.add(await _forceAlwaysRelayAction(peer.id));
     if (peer.platform == 'Windows') {
       menuItems.add(_rdpAction(context, peer.id));
     }
-    menuItems.add(_wolAction(peer.id));
     if (Platform.isWindows) {
       menuItems.add(_createShortCutAction(peer.id));
     }
     menuItems.add(MenuEntryDivider());
     menuItems.add(_renameAction(peer.id));
-    if (await bind.mainPeerHasPassword(id: peer.id)) {
+    if (peer.hash.isNotEmpty) {
       menuItems.add(_unrememberPasswordAction(peer.id));
     }
     if (gFFI.abModel.tags.isNotEmpty) {
@@ -961,44 +1042,13 @@ class AddressBookPeerCard extends BasePeerCard {
     }
 
     menuItems.add(MenuEntryDivider());
-    menuItems.add(_removeAction(peer.id, () async {}));
+    menuItems.add(_removeAction(peer.id));
     return menuItems;
   }
 
   @protected
   @override
-  Future<bool> _isForceAlwaysRelay(String id) async =>
-      gFFI.abModel.find(id)?.forceAlwaysRelay ?? false;
-
-  @protected
-  @override
-  Future<String> _getAlias(String id) async =>
-      gFFI.abModel.find(id)?.alias ?? '';
-
-  @protected
-  @override
-  void _update() => gFFI.abModel.pullAb();
-
-  @protected
-  @override
-  MenuEntryBase<String> _removeAction(
-      String id, Future<void> Function() reloadFunc,
-      {bool isLan = false}) {
-    return MenuEntryButton<String>(
-      childBuilder: (TextStyle? style) => Text(
-        translate('Remove'),
-        style: style,
-      ),
-      proc: () {
-        () async {
-          gFFI.abModel.deletePeer(id);
-          await gFFI.abModel.pushAb();
-        }();
-      },
-      padding: super.menuPadding,
-      dismissOnClicked: true,
-    );
-  }
+  void _update() => gFFI.abModel.pullAb(quiet: true);
 
   @protected
   MenuEntryBase<String> _editTagAction(String id) {
@@ -1008,70 +1058,29 @@ class AddressBookPeerCard extends BasePeerCard {
         style: style,
       ),
       proc: () {
-        _abEditTag(id);
+        editAbTagDialog(gFFI.abModel.getPeerTags(id), (selectedTag) async {
+          gFFI.abModel.changeTagForPeer(id, selectedTag);
+          gFFI.abModel.pushAb();
+        });
       },
       padding: super.menuPadding,
       dismissOnClicked: true,
     );
   }
 
-  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, context) {
-      submit() async {
-        setState(() {
-          isInProgress = true;
-        });
-        gFFI.abModel.changeTagForPeer(id, selectedTag);
-        await gFFI.abModel.pushAb();
-        close();
-      }
-
-      return CustomAlertDialog(
-        title: Text(translate("Edit Tag")),
-        content: Column(
-          crossAxisAlignment: CrossAxisAlignment.start,
-          children: [
-            Container(
-              padding: const EdgeInsets.symmetric(vertical: 8.0),
-              child: Wrap(
-                children: tags
-                    .map((e) => AddressBookTag(
-                        name: e,
-                        tags: selectedTag,
-                        onTap: () {
-                          if (selectedTag.contains(e)) {
-                            selectedTag.remove(e);
-                          } else {
-                            selectedTag.add(e);
-                          }
-                        },
-                        showActionMenu: false))
-                    .toList(growable: false),
-              ),
-            ),
-            Offstage(
-                offstage: !isInProgress, child: const LinearProgressIndicator())
-          ],
-        ),
-        actions: [
-          dialogButton("Cancel", onPressed: close, isOutline: true),
-          dialogButton("OK", onPressed: submit),
-        ],
-        onSubmit: submit,
-        onCancel: close,
-      );
-    });
-  }
+  @protected
+  @override
+  Future<String> _getAlias(String id) async =>
+      gFFI.abModel.find(id)?.alias ?? '';
 }
 
 class MyGroupPeerCard extends BasePeerCard {
   MyGroupPeerCard({required Peer peer, EdgeInsets? menuPadding, Key? key})
-      : super(peer: peer, menuPadding: menuPadding, key: key);
+      : super(
+            peer: peer,
+            tab: PeerTabIndex.group,
+            menuPadding: menuPadding,
+            key: key);
 
   @override
   Future<List<MenuEntryBase<String>>> _buildMenuItems(
@@ -1083,11 +1092,11 @@ class MyGroupPeerCard extends BasePeerCard {
     if (isDesktop && peer.platform != 'Android') {
       menuItems.add(_tcpTunnelingAction(context, peer.id));
     }
+    // menuItems.add(await _openNewConnInOptAction(peer.id));
     menuItems.add(await _forceAlwaysRelayAction(peer.id));
     if (peer.platform == 'Windows') {
       menuItems.add(_rdpAction(context, peer.id));
     }
-    menuItems.add(_wolAction(peer.id));
     if (Platform.isWindows) {
       menuItems.add(_createShortCutAction(peer.id));
     }
@@ -1123,7 +1132,7 @@ void _rdpDialog(String id) async {
           id: id, key: 'rdp_username', value: username);
       await bind.mainSetPeerOption(
           id: id, key: 'rdp_password', value: password);
-      gFFI.abModel.setRdp(id, port, username);
+      showToast(translate('Successful'));
       close();
     }
 
@@ -1251,3 +1260,64 @@ Widget build_more(BuildContext context, {bool invert = false}) {
                       ?.color
                       ?.withOpacity(0.5)))));
 }
+
+class TagPainter extends CustomPainter {
+  final double radius;
+  late final List<Color> colors;
+
+  TagPainter({required this.radius, required List<Color> colors}) {
+    this.colors = colors.reversed.toList();
+  }
+
+  @override
+  void paint(Canvas canvas, Size size) {
+    double x = 0;
+    double y = radius;
+    for (int i = 0; i < colors.length; i++) {
+      Paint paint = Paint();
+      paint.color = colors[i];
+      x -= radius + 1;
+      if (i == colors.length - 1) {
+        canvas.drawCircle(Offset(x, y), radius, paint);
+      } else {
+        Path path = Path();
+        path.addArc(Rect.fromCircle(center: Offset(x, y), radius: radius),
+            math.pi * 4 / 3, math.pi * 4 / 3);
+        path.addArc(
+            Rect.fromCircle(center: Offset(x - radius, y), radius: radius),
+            math.pi * 5 / 3,
+            math.pi * 2 / 3);
+        path.fillType = PathFillType.evenOdd;
+        canvas.drawPath(path, paint);
+      }
+    }
+  }
+
+  @override
+  bool shouldRepaint(covariant CustomPainter oldDelegate) {
+    return true;
+  }
+}
+
+void connectInPeerTab(BuildContext context, String id, PeerTabIndex tab,
+    {bool isFileTransfer = false,
+    bool isTcpTunneling = false,
+    bool isRDP = false}) async {
+  if (tab == PeerTabIndex.ab) {
+    // If recent peer's alias is empty, set it to ab's alias
+    // Because the platform is not set, it may not take effect, but it is more important not to display if the connection is not successful
+    Peer? p = gFFI.abModel.find(id);
+    if (p != null &&
+        p.alias.isNotEmpty &&
+        (await bind.mainGetPeerOption(id: id, key: "alias")).isEmpty) {
+      await bind.mainSetPeerAlias(
+        id: id,
+        alias: p.alias,
+      );
+    }
+  }
+  connect(context, id,
+      isFileTransfer: isFileTransfer,
+      isTcpTunneling: isTcpTunneling,
+      isRDP: isRDP);
+}
diff --git a/flutter/lib/common/widgets/peer_tab_page.dart b/flutter/lib/common/widgets/peer_tab_page.dart
index ab85b2960..cb5413ba1 100644
--- a/flutter/lib/common/widgets/peer_tab_page.dart
+++ b/flutter/lib/common/widgets/peer_tab_page.dart
@@ -1,11 +1,12 @@
 import 'package:flutter/material.dart';
 import 'package:flutter_hbb/common/widgets/address_book.dart';
+import 'package:flutter_hbb/common/widgets/dialog.dart';
 import 'package:flutter_hbb/common/widgets/my_group.dart';
 import 'package:flutter_hbb/common/widgets/peers_view.dart';
 import 'package:flutter_hbb/common/widgets/peer_card.dart';
-import 'package:flutter_hbb/common/widgets/animated_rotation_widget.dart';
 import 'package:flutter_hbb/consts.dart';
 import 'package:flutter_hbb/desktop/widgets/popup_menu.dart';
+import 'package:flutter_hbb/models/ab_model.dart';
 
 import 'package:flutter_hbb/models/peer_tab_model.dart';
 import 'package:get/get.dart';
@@ -63,7 +64,7 @@ class _PeerTabPageState extends State<PeerTabPage>
 
   @override
   void initState() {
-    final uiType = bind.getLocalFlutterConfig(k: 'peer-card-ui-type');
+    final uiType = bind.getLocalFlutterOption(k: 'peer-card-ui-type');
     if (uiType != '') {
       peerCardUiType.value = int.parse(uiType) == PeerUiType.list.index
           ? PeerUiType.list
@@ -83,6 +84,11 @@ class _PeerTabPageState extends State<PeerTabPage>
 
   @override
   Widget build(BuildContext context) {
+    final model = Provider.of<PeerTabModel>(context);
+    Widget selectionWrap(Widget widget) {
+      return model.multiSelectionMode ? createMultiSelectionBar() : widget;
+    }
+
     return Column(
       textBaseline: TextBaseline.ideographic,
       crossAxisAlignment: CrossAxisAlignment.start,
@@ -91,43 +97,41 @@ class _PeerTabPageState extends State<PeerTabPage>
           height: 32,
           child: Container(
             padding: isDesktop ? null : EdgeInsets.symmetric(horizontal: 2),
-            child: Row(
+            child: selectionWrap(Row(
               crossAxisAlignment: CrossAxisAlignment.center,
               children: [
                 Expanded(child: _createSwitchBar(context)),
                 const PeerSearchBar().marginOnly(right: isMobile ? 0 : 13),
                 _createRefresh(),
+                _createMultiSelection(),
                 Offstage(
                     offstage: !isDesktop,
                     child: _createPeerViewTypeSwitch(context)),
                 Offstage(
                   offstage: gFFI.peerTabModel.currentTab == 0,
-                  child: PeerSortDropdown().marginOnly(left: 8),
+                  child: PeerSortDropdown(),
                 ),
                 Offstage(
                   offstage: gFFI.peerTabModel.currentTab != 3,
-                  child: InkWell(
-                    child: Obx(() => Container(
-                        padding: EdgeInsets.all(4.0),
-                        decoration: hideAbTagsPanel.value
-                            ? null
-                            : BoxDecoration(
-                                color: Theme.of(context).colorScheme.background,
-                                borderRadius: BorderRadius.circular(6)),
+                  child: _hoverAction(
+                    context: context,
+                    hoverableWhenfalse: hideAbTagsPanel,
+                    child: Tooltip(
+                        message: translate('Toggle Tags'),
                         child: Icon(
                           Icons.tag_rounded,
                           size: 18,
-                        ))),
+                        )),
                     onTap: () async {
                       await bind.mainSetLocalOption(
                           key: "hideAbTagsPanel",
                           value: hideAbTagsPanel.value ? "" : "Y");
                       hideAbTagsPanel.value = !hideAbTagsPanel.value;
                     },
-                  ).marginOnly(left: 8),
+                  ),
                 ),
               ],
-            ),
+            )),
           ),
         ),
         _createPeersView(),
@@ -167,7 +171,7 @@ class _PeerTabPageState extends State<PeerTabPage>
                 ).paddingSymmetric(horizontal: 4),
                 onTap: () async {
                   await handleTabSelection(t);
-                  await bind.setLocalFlutterConfig(
+                  await bind.setLocalFlutterOption(
                       k: 'peer-tab-index', v: t.toString());
                 },
                 onHover: (value) => hover.value = value,
@@ -199,58 +203,258 @@ class _PeerTabPageState extends State<PeerTabPage>
   Widget _createRefresh() {
     final textColor = Theme.of(context).textTheme.titleLarge?.color;
     return Offstage(
-      offstage: gFFI.peerTabModel.currentTab < 3, // local tab can't see effect
-      child: Container(
-        padding: EdgeInsets.all(4.0),
-        child: AnimatedRotationWidget(
-            onPressed: () {
-              if (gFFI.peerTabModel.currentTab < entries.length) {
-                entries[gFFI.peerTabModel.currentTab].load();
-              }
-            },
-            child: RotatedBox(
-                quarterTurns: 2,
-                child: Icon(
-                  Icons.refresh,
-                  size: 18,
-                  color: textColor,
-                ))),
-      ),
+      offstage: gFFI.peerTabModel.currentTab != PeerTabIndex.ab.index,
+      child: RefreshWidget(
+          onPressed: () {
+            if (gFFI.peerTabModel.currentTab < entries.length) {
+              entries[gFFI.peerTabModel.currentTab].load();
+            }
+          },
+          spinning: gFFI.abModel.abLoading,
+          child: RotatedBox(
+              quarterTurns: 2,
+              child: Tooltip(
+                  message: translate('Refresh'),
+                  child: Icon(
+                    Icons.refresh,
+                    size: 18,
+                    color: textColor,
+                  )))),
     );
   }
 
   Widget _createPeerViewTypeSwitch(BuildContext context) {
     final textColor = Theme.of(context).textTheme.titleLarge?.color;
     final types = [PeerUiType.grid, PeerUiType.list];
-    final hover = false.obs;
-    final deco = BoxDecoration(
-      color: Theme.of(context).colorScheme.background,
-      borderRadius: BorderRadius.circular(5),
-    );
 
-    return Obx(
-      () => Container(
-        padding: EdgeInsets.all(4.0),
-        decoration: hover.value ? deco : null,
-        child: InkWell(
-            onHover: (value) => hover.value = value,
-            onTap: () async {
-              final type = types.elementAt(
-                  peerCardUiType.value == types.elementAt(0) ? 1 : 0);
-              await bind.setLocalFlutterConfig(
-                  k: 'peer-card-ui-type', v: type.index.toString());
-              peerCardUiType.value = type;
-            },
+    return Obx(() => _hoverAction(
+        context: context,
+        onTap: () async {
+          final type = types
+              .elementAt(peerCardUiType.value == types.elementAt(0) ? 1 : 0);
+          await bind.setLocalFlutterOption(
+              k: 'peer-card-ui-type', v: type.index.toString());
+          peerCardUiType.value = type;
+        },
+        child: Tooltip(
+            message: peerCardUiType.value == PeerUiType.grid
+                ? translate('List View')
+                : translate('Grid View'),
             child: Icon(
               peerCardUiType.value == PeerUiType.grid
                   ? Icons.view_list_rounded
                   : Icons.grid_view_rounded,
               size: 18,
               color: textColor,
-            )),
-      ),
+            ))));
+  }
+
+  Widget _createMultiSelection() {
+    final textColor = Theme.of(context).textTheme.titleLarge?.color;
+    final model = Provider.of<PeerTabModel>(context);
+    if (model.currentTabCachedPeers.isEmpty) return Offstage();
+    return _hoverAction(
+      context: context,
+      onTap: () {
+        model.setMultiSelectionMode(true);
+      },
+      child: Tooltip(
+          message: translate('Select'),
+          child: Icon(
+            IconFont.checkbox,
+            size: 18,
+            color: textColor,
+          )),
     );
   }
+
+  Widget createMultiSelectionBar() {
+    final model = Provider.of<PeerTabModel>(context);
+    return Row(
+      children: [
+        deleteSelection(),
+        addSelectionToFav(),
+        addSelectionToAb(),
+        editSelectionTags(),
+        Expanded(child: Container()),
+        selectionCount(model.selectedPeers.length),
+        selectAll(),
+        closeSelection(),
+      ],
+    );
+  }
+
+  Widget deleteSelection() {
+    final model = Provider.of<PeerTabModel>(context);
+    return _hoverAction(
+        context: context,
+        onTap: () {
+          onSubmit() async {
+            final peers = model.selectedPeers;
+            switch (model.currentTab) {
+              case 0:
+                peers.map((p) async {
+                  await bind.mainRemovePeer(id: p.id);
+                }).toList();
+                await bind.mainLoadRecentPeers();
+                break;
+              case 1:
+                final favs = (await bind.mainGetFav()).toList();
+                peers.map((p) {
+                  favs.remove(p.id);
+                }).toList();
+                await bind.mainStoreFav(favs: favs);
+                await bind.mainLoadFavPeers();
+                break;
+              case 2:
+                peers.map((p) async {
+                  await bind.mainRemoveDiscovered(id: p.id);
+                }).toList();
+                await bind.mainLoadLanPeers();
+                break;
+              case 3:
+                {
+                  bool hasSynced = false;
+                  if (shouldSyncAb()) {
+                    for (var p in peers) {
+                      if (await bind.mainPeerExists(id: p.id)) {
+                        hasSynced = true;
+                      }
+                    }
+                  }
+                  gFFI.abModel.deletePeers(peers.map((p) => p.id).toList());
+                  final future = gFFI.abModel.pushAb();
+                  if (hasSynced) {
+                    gFFI.abModel.reSyncToast(future);
+                  }
+                }
+                break;
+              default:
+                break;
+            }
+            gFFI.peerTabModel.setMultiSelectionMode(false);
+            if (model.currentTab != 3) showToast(translate('Successful'));
+          }
+
+          deletePeerConfirmDialog(onSubmit, translate('Delete'));
+        },
+        child: Tooltip(
+            message: translate('Delete'),
+            child: Icon(Icons.delete, color: Colors.red)));
+  }
+
+  Widget addSelectionToFav() {
+    final model = Provider.of<PeerTabModel>(context);
+    return Offstage(
+      offstage:
+          model.currentTab != PeerTabIndex.recent.index, // show based on recent
+      child: _hoverAction(
+        context: context,
+        onTap: () async {
+          final peers = model.selectedPeers;
+          final favs = (await bind.mainGetFav()).toList();
+          for (var p in peers) {
+            if (!favs.contains(p.id)) {
+              favs.add(p.id);
+            }
+          }
+          await bind.mainStoreFav(favs: favs);
+          model.setMultiSelectionMode(false);
+          showToast(translate('Successful'));
+        },
+        child: Tooltip(
+            message: translate('Add to Favorites'),
+            child: Icon(model.icons[PeerTabIndex.fav.index])),
+      ).marginOnly(left: isMobile ? 11 : 6),
+    );
+  }
+
+  Widget addSelectionToAb() {
+    final model = Provider.of<PeerTabModel>(context);
+    return Offstage(
+      offstage:
+          !gFFI.userModel.isLogin || model.currentTab == PeerTabIndex.ab.index,
+      child: _hoverAction(
+        context: context,
+        onTap: () {
+          if (gFFI.abModel.isFull(true)) {
+            return;
+          }
+          final peers = model.selectedPeers;
+          gFFI.abModel.addPeers(peers);
+          final future = gFFI.abModel.pushAb();
+          model.setMultiSelectionMode(false);
+          Future.delayed(Duration.zero, () async {
+            await future;
+            await Future.delayed(Duration(seconds: 2)); // toast
+            gFFI.abModel.isFull(true);
+          });
+        },
+        child: Tooltip(
+            message: translate('Add to Address Book'),
+            child: Icon(model.icons[PeerTabIndex.ab.index])),
+      ).marginOnly(left: isMobile ? 11 : 6),
+    );
+  }
+
+  Widget editSelectionTags() {
+    final model = Provider.of<PeerTabModel>(context);
+    return Offstage(
+      offstage: !gFFI.userModel.isLogin ||
+          model.currentTab != PeerTabIndex.ab.index ||
+          gFFI.abModel.tags.isEmpty,
+      child: _hoverAction(
+              context: context,
+              onTap: () {
+                editAbTagDialog(List.empty(), (selectedTags) async {
+                  final peers = model.selectedPeers;
+                  gFFI.abModel.changeTagForPeers(
+                      peers.map((p) => p.id).toList(), selectedTags);
+                  gFFI.abModel.pushAb();
+                  model.setMultiSelectionMode(false);
+                  showToast(translate('Successful'));
+                });
+              },
+              child: Tooltip(
+                  message: translate('Edit Tag'), child: Icon(Icons.tag)))
+          .marginOnly(left: isMobile ? 11 : 6),
+    );
+  }
+
+  Widget selectionCount(int count) {
+    return Align(
+      alignment: Alignment.center,
+      child: Text('$count ${translate('Selected')}'),
+    );
+  }
+
+  Widget selectAll() {
+    final model = Provider.of<PeerTabModel>(context);
+    return Offstage(
+      offstage:
+          model.selectedPeers.length >= model.currentTabCachedPeers.length,
+      child: _hoverAction(
+        context: context,
+        onTap: () {
+          model.selectAll();
+        },
+        child: Tooltip(
+            message: translate('Select All'), child: Icon(Icons.select_all)),
+      ).marginOnly(left: 6),
+    );
+  }
+
+  Widget closeSelection() {
+    final model = Provider.of<PeerTabModel>(context);
+    return _hoverAction(
+            context: context,
+            onTap: () {
+              model.setMultiSelectionMode(false);
+            },
+            child:
+                Tooltip(message: translate('Close'), child: Icon(Icons.clear)))
+        .marginOnly(left: 6);
+  }
 }
 
 class PeerSearchBar extends StatefulWidget {
@@ -267,18 +471,20 @@ class _PeerSearchBarState extends State<PeerSearchBar> {
   Widget build(BuildContext context) {
     return drawer
         ? _buildSearchBar()
-        : IconButton(
-            alignment: Alignment.centerRight,
+        : _hoverAction(
+            context: context,
             padding: const EdgeInsets.only(right: 2),
-            onPressed: () {
+            onTap: () {
               setState(() {
                 drawer = true;
               });
             },
-            icon: Icon(
-              Icons.search_rounded,
-              color: Theme.of(context).hintColor,
-            ));
+            child: Tooltip(
+                message: translate('Search'),
+                child: Icon(
+                  Icons.search_rounded,
+                  color: Theme.of(context).hintColor,
+                )));
   }
 
   Widget _buildSearchBar() {
@@ -291,7 +497,7 @@ class _PeerSearchBarState extends State<PeerSearchBar> {
           extentOffset: peerSearchTextController.value.text.length);
     });
     return Container(
-      width: 120,
+      width: isMobile ? 120 : 140,
       decoration: BoxDecoration(
         color: Theme.of(context).colorScheme.background,
         borderRadius: BorderRadius.circular(6),
@@ -337,19 +543,22 @@ class _PeerSearchBarState extends State<PeerSearchBar> {
                     ),
                     // Icon(Icons.close),
                     IconButton(
-                        alignment: Alignment.centerRight,
-                        padding: const EdgeInsets.only(right: 2),
-                        onPressed: () {
-                          setState(() {
-                            peerSearchTextController.clear();
-                            peerSearchText.value = "";
-                            drawer = false;
-                          });
-                        },
-                        icon: Icon(
-                          Icons.close,
-                          color: Theme.of(context).hintColor,
-                        )),
+                      alignment: Alignment.centerRight,
+                      padding: const EdgeInsets.only(right: 2),
+                      onPressed: () {
+                        setState(() {
+                          peerSearchTextController.clear();
+                          peerSearchText.value = "";
+                          drawer = false;
+                        });
+                      },
+                      icon: Tooltip(
+                          message: translate('Close'),
+                          child: Icon(
+                            Icons.close,
+                            color: Theme.of(context).hintColor,
+                          )),
+                    ),
                   ],
                 ),
               )
@@ -371,7 +580,7 @@ class _PeerSortDropdownState extends State<PeerSortDropdown> {
   void initState() {
     if (!PeerSortType.values.contains(peerSort.value)) {
       peerSort.value = PeerSortType.remoteId;
-      bind.setLocalFlutterConfig(
+      bind.setLocalFlutterOption(
         k: "peer-sorting",
         v: peerSort.value,
       );
@@ -401,7 +610,7 @@ class _PeerSortDropdownState extends State<PeerSortDropdown> {
                       dense: true, (String? v) async {
                     if (v != null) {
                       peerSort.value = v;
-                      await bind.setLocalFlutterConfig(
+                      await bind.setLocalFlutterOption(
                         k: "peer-sorting",
                         v: peerSort.value,
                       );
@@ -412,11 +621,14 @@ class _PeerSortDropdownState extends State<PeerSortDropdown> {
     }
 
     var menuPos = RelativeRect.fromLTRB(0, 0, 0, 0);
-    return InkWell(
-      child: Icon(
-        Icons.sort_rounded,
-        size: 18,
-      ),
+    return _hoverAction(
+      context: context,
+      child: Tooltip(
+          message: translate('Sort by'),
+          child: Icon(
+            Icons.sort_rounded,
+            size: 18,
+          )),
       onTapDown: (details) {
         final x = details.globalPosition.dx;
         final y = details.globalPosition.dy;
@@ -431,3 +643,90 @@ class _PeerSortDropdownState extends State<PeerSortDropdown> {
     );
   }
 }
+
+class RefreshWidget extends StatefulWidget {
+  final VoidCallback onPressed;
+  final Widget child;
+  final RxBool? spinning;
+  const RefreshWidget(
+      {super.key, required this.onPressed, required this.child, this.spinning});
+
+  @override
+  State<RefreshWidget> createState() => RefreshWidgetState();
+}
+
+class RefreshWidgetState extends State<RefreshWidget> {
+  double turns = 0.0;
+  bool hover = false;
+
+  @override
+  void initState() {
+    super.initState();
+    widget.spinning?.listen((v) {
+      if (v && mounted) {
+        setState(() {
+          turns += 1;
+        });
+      }
+    });
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    final deco = BoxDecoration(
+      color: Theme.of(context).colorScheme.background,
+      borderRadius: BorderRadius.circular(6),
+    );
+    return AnimatedRotation(
+        turns: turns,
+        duration: const Duration(milliseconds: 200),
+        onEnd: () {
+          if (widget.spinning?.value == true && mounted) {
+            setState(() => turns += 1.0);
+          }
+        },
+        child: Container(
+          padding: EdgeInsets.all(4.0),
+          margin: EdgeInsets.symmetric(horizontal: 1),
+          decoration: hover ? deco : null,
+          child: InkWell(
+              onTap: () {
+                if (mounted) setState(() => turns += 1.0);
+                widget.onPressed();
+              },
+              onHover: (value) {
+                if (mounted) {
+                  setState(() {
+                    hover = value;
+                  });
+                }
+              },
+              child: widget.child),
+        ));
+  }
+}
+
+Widget _hoverAction(
+    {required BuildContext context,
+    required Widget child,
+    required Function() onTap,
+    GestureTapDownCallback? onTapDown,
+    RxBool? hoverableWhenfalse,
+    EdgeInsetsGeometry padding = const EdgeInsets.all(4.0)}) {
+  final hover = false.obs;
+  final deco = BoxDecoration(
+    color: Theme.of(context).colorScheme.background,
+    borderRadius: BorderRadius.circular(6),
+  );
+  return Obx(
+    () => Container(
+        margin: EdgeInsets.symmetric(horizontal: 1),
+        decoration:
+            (hover.value || hoverableWhenfalse?.value == false) ? deco : null,
+        child: InkWell(
+            onHover: (value) => hover.value = value,
+            onTap: onTap,
+            onTapDown: onTapDown,
+            child: Container(padding: padding, child: child))),
+  );
+}
diff --git a/flutter/lib/common/widgets/peers_view.dart b/flutter/lib/common/widgets/peers_view.dart
index 95099bcc8..0e4898fc2 100644
--- a/flutter/lib/common/widgets/peers_view.dart
+++ b/flutter/lib/common/widgets/peers_view.dart
@@ -41,7 +41,7 @@ class LoadEvent {
 final peerSearchText = "".obs;
 
 /// for peer sort, global obs value
-final peerSort = bind.getLocalFlutterConfig(k: 'peer-sorting').obs;
+final peerSort = bind.getLocalFlutterOption(k: 'peer-sorting').obs;
 
 // list for listener
 final obslist = [peerSearchText, peerSort].obs;
@@ -124,31 +124,34 @@ class _PeersViewState extends State<_PeersView> with WindowListener {
   Widget build(BuildContext context) {
     return ChangeNotifierProvider<Peers>(
       create: (context) => widget.peers,
-      child: Consumer<Peers>(
-        builder: (context, peers, child) => peers.peers.isEmpty
-            ? Center(
-                child: Column(
-                  mainAxisAlignment: MainAxisAlignment.center,
-                  children: [
-                    Icon(
-                      Icons.sentiment_very_dissatisfied_rounded,
-                      color: Theme.of(context).tabBarTheme.labelColor,
-                      size: 40,
-                    ).paddingOnly(bottom: 10),
-                    Text(
-                      translate(
-                        _emptyMessages[widget.peers.loadEvent] ?? 'Empty',
-                      ),
-                      textAlign: TextAlign.center,
-                      style: TextStyle(
-                        color: Theme.of(context).tabBarTheme.labelColor,
-                      ),
-                    ),
-                  ],
+      child: Consumer<Peers>(builder: (context, peers, child) {
+        if (peers.peers.isEmpty) {
+          gFFI.peerTabModel.setCurrentTabCachedPeers([]);
+          return Center(
+            child: Column(
+              mainAxisAlignment: MainAxisAlignment.center,
+              children: [
+                Icon(
+                  Icons.sentiment_very_dissatisfied_rounded,
+                  color: Theme.of(context).tabBarTheme.labelColor,
+                  size: 40,
+                ).paddingOnly(bottom: 10),
+                Text(
+                  translate(
+                    _emptyMessages[widget.peers.loadEvent] ?? 'Empty',
+                  ),
+                  textAlign: TextAlign.center,
+                  style: TextStyle(
+                    color: Theme.of(context).tabBarTheme.labelColor,
+                  ),
                 ),
-              )
-            : _buildPeersView(peers),
-      ),
+              ],
+            ),
+          );
+        } else {
+          return _buildPeersView(peers);
+        }
+      }),
     );
   }
 
@@ -172,6 +175,7 @@ class _PeersViewState extends State<_PeersView> with WindowListener {
         builder: (context, snapshot) {
           if (snapshot.hasData) {
             final peers = snapshot.data!;
+            gFFI.peerTabModel.setCurrentTabCachedPeers(peers);
             final cards = <Widget>[];
             for (final peer in peers) {
               final visibilityChild = VisibilityDetector(
@@ -260,7 +264,7 @@ class _PeersViewState extends State<_PeersView> with WindowListener {
     // fallback to id sorting
     if (!PeerSortType.values.contains(sortedBy)) {
       sortedBy = PeerSortType.remoteId;
-      bind.setLocalFlutterConfig(
+      bind.setLocalFlutterOption(
         k: "peer-sorting",
         v: sortedBy,
       );
@@ -417,15 +421,12 @@ class AddressBookPeersView extends BasePeersView {
     if (selectedTags.isEmpty) {
       return true;
     }
-    if (idents.isEmpty) {
-      return false;
-    }
     for (final tag in selectedTags) {
-      if (!idents.contains(tag)) {
-        return false;
+      if (idents.contains(tag)) {
+        return true;
       }
     }
-    return true;
+    return false;
   }
 }
 
diff --git a/flutter/lib/common/widgets/remote_input.dart b/flutter/lib/common/widgets/remote_input.dart
index dd39cbdfd..b00cd1fb4 100644
--- a/flutter/lib/common/widgets/remote_input.dart
+++ b/flutter/lib/common/widgets/remote_input.dart
@@ -1,7 +1,16 @@
+import 'dart:convert';
+
 import 'package:flutter/material.dart';
 import 'package:flutter/services.dart';
+import 'package:flutter/gestures.dart';
 
-import '../../models/input_model.dart';
+import 'package:flutter_hbb/models/platform_model.dart';
+import 'package:flutter_hbb/common.dart';
+import 'package:flutter_hbb/consts.dart';
+import 'package:flutter_hbb/models/model.dart';
+import 'package:flutter_hbb/models/input_model.dart';
+
+import './gestures.dart';
 
 class RawKeyFocusScope extends StatelessWidget {
   final FocusNode? focusNode;
@@ -30,6 +39,334 @@ class RawKeyFocusScope extends StatelessWidget {
   }
 }
 
+class RawTouchGestureDetectorRegion extends StatefulWidget {
+  final Widget child;
+  final FFI ffi;
+
+  late final InputModel inputModel = ffi.inputModel;
+  late final FfiModel ffiModel = ffi.ffiModel;
+
+  RawTouchGestureDetectorRegion({
+    required this.child,
+    required this.ffi,
+  });
+
+  @override
+  State<RawTouchGestureDetectorRegion> createState() =>
+      _RawTouchGestureDetectorRegionState();
+}
+
+/// 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
+class _RawTouchGestureDetectorRegionState
+    extends State<RawTouchGestureDetectorRegion> {
+  Offset _cacheLongPressPosition = Offset(0, 0);
+  double _mouseScrollIntegral = 0; // mouse scroll speed controller
+  double _scale = 1;
+
+  PointerDeviceKind? lastDeviceKind;
+
+  FFI get ffi => widget.ffi;
+  FfiModel get ffiModel => widget.ffiModel;
+  InputModel get inputModel => widget.inputModel;
+  bool get handleTouch => isDesktop || ffiModel.touchMode;
+  SessionID get sessionId => ffi.sessionId;
+
+  @override
+  Widget build(BuildContext context) {
+    return RawGestureDetector(
+      child: widget.child,
+      gestures: makeGestures(context),
+    );
+  }
+
+  onTapDown(TapDownDetails d) {
+    lastDeviceKind = d.kind;
+    if (lastDeviceKind != PointerDeviceKind.touch) {
+      return;
+    }
+    if (handleTouch) {
+      ffi.cursorModel.move(d.localPosition.dx, d.localPosition.dy);
+      inputModel.tapDown(MouseButtons.left);
+    }
+  }
+
+  onTapUp(TapUpDetails d) {
+    if (lastDeviceKind != PointerDeviceKind.touch) {
+      return;
+    }
+    if (handleTouch) {
+      ffi.cursorModel.move(d.localPosition.dx, d.localPosition.dy);
+      inputModel.tapUp(MouseButtons.left);
+    }
+  }
+
+  onTap() {
+    if (lastDeviceKind != PointerDeviceKind.touch) {
+      return;
+    }
+    inputModel.tap(MouseButtons.left);
+  }
+
+  onDoubleTapDown(TapDownDetails d) {
+    lastDeviceKind = d.kind;
+    if (lastDeviceKind != PointerDeviceKind.touch) {
+      return;
+    }
+    if (handleTouch) {
+      ffi.cursorModel.move(d.localPosition.dx, d.localPosition.dy);
+    }
+  }
+
+  onDoubleTap() {
+    if (lastDeviceKind != PointerDeviceKind.touch) {
+      return;
+    }
+    inputModel.tap(MouseButtons.left);
+    inputModel.tap(MouseButtons.left);
+  }
+
+  onLongPressDown(LongPressDownDetails d) {
+    lastDeviceKind = d.kind;
+    if (lastDeviceKind != PointerDeviceKind.touch) {
+      return;
+    }
+    if (handleTouch) {
+      ffi.cursorModel.move(d.localPosition.dx, d.localPosition.dy);
+      _cacheLongPressPosition = d.localPosition;
+    }
+  }
+
+  onLongPressUp() {
+    if (lastDeviceKind != PointerDeviceKind.touch) {
+      return;
+    }
+    if (handleTouch) {
+      inputModel.tapUp(MouseButtons.left);
+    }
+  }
+
+  // for mobiles
+  onLongPress() {
+    if (lastDeviceKind != PointerDeviceKind.touch) {
+      return;
+    }
+    if (handleTouch) {
+      ffi.cursorModel
+          .move(_cacheLongPressPosition.dx, _cacheLongPressPosition.dy);
+    }
+    inputModel.tap(MouseButtons.right);
+  }
+
+  onDoubleFinerTapDown(TapDownDetails d) {
+    lastDeviceKind = d.kind;
+    if (lastDeviceKind != PointerDeviceKind.touch) {
+      return;
+    }
+    // ignore for desktop and mobile
+  }
+
+  onDoubleFinerTap(TapDownDetails d) {
+    lastDeviceKind = d.kind;
+    if (lastDeviceKind != PointerDeviceKind.touch) {
+      return;
+    }
+    if (isDesktop || !ffiModel.touchMode) {
+      inputModel.tap(MouseButtons.right);
+    }
+  }
+
+  onHoldDragStart(DragStartDetails d) {
+    lastDeviceKind = d.kind;
+    if (lastDeviceKind != PointerDeviceKind.touch) {
+      return;
+    }
+    if (!handleTouch) {
+      inputModel.sendMouse('down', MouseButtons.left);
+    }
+  }
+
+  onHoldDragUpdate(DragUpdateDetails d) {
+    if (lastDeviceKind != PointerDeviceKind.touch) {
+      return;
+    }
+    if (!handleTouch) {
+      ffi.cursorModel.updatePan(d.delta.dx, d.delta.dy, handleTouch);
+    }
+  }
+
+  onHoldDragEnd(DragEndDetails d) {
+    if (lastDeviceKind != PointerDeviceKind.touch) {
+      return;
+    }
+    if (!handleTouch) {
+      inputModel.sendMouse('up', MouseButtons.left);
+    }
+  }
+
+  onOneFingerPanStart(BuildContext context, DragStartDetails d) {
+    lastDeviceKind = d.kind ?? lastDeviceKind;
+    if (lastDeviceKind != PointerDeviceKind.touch) {
+      return;
+    }
+    if (handleTouch) {
+      inputModel.sendMouse('down', MouseButtons.left);
+      ffi.cursorModel.move(d.localPosition.dx, d.localPosition.dy);
+    } else {
+      final offset = ffi.cursorModel.offset;
+      final cursorX = offset.dx;
+      final cursorY = offset.dy;
+      final visible =
+          ffi.cursorModel.getVisibleRect().inflate(1); // extend edges
+      final size = MediaQueryData.fromView(View.of(context)).size;
+      if (!visible.contains(Offset(cursorX, cursorY))) {
+        ffi.cursorModel.move(size.width / 2, size.height / 2);
+      }
+    }
+  }
+
+  onOneFingerPanUpdate(DragUpdateDetails d) {
+    if (lastDeviceKind != PointerDeviceKind.touch) {
+      return;
+    }
+    ffi.cursorModel.updatePan(d.delta.dx, d.delta.dy, handleTouch);
+  }
+
+  onOneFingerPanEnd(DragEndDetails d) {
+    if (lastDeviceKind != PointerDeviceKind.touch) {
+      return;
+    }
+    inputModel.sendMouse('up', MouseButtons.left);
+  }
+
+  // scale + pan event
+  onTwoFingerScaleStart(ScaleStartDetails d) {
+    if (lastDeviceKind != PointerDeviceKind.touch) {
+      return;
+    }
+  }
+
+  onTwoFingerScaleUpdate(ScaleUpdateDetails d) {
+    if (lastDeviceKind != PointerDeviceKind.touch) {
+      return;
+    }
+    if (isDesktop) {
+      final scale = ((d.scale - _scale) * 1000).toInt();
+      _scale = d.scale;
+
+      if (scale != 0) {
+        bind.sessionSendPointer(
+            sessionId: sessionId,
+            msg: json.encode(
+                PointerEventToRust(kPointerEventKindTouch, 'scale', scale)
+                    .toJson()));
+      }
+    } else {
+      // mobile
+      ffi.canvasModel.updateScale(d.scale / _scale);
+      _scale = d.scale;
+      ffi.canvasModel.panX(d.focalPointDelta.dx);
+      ffi.canvasModel.panY(d.focalPointDelta.dy);
+    }
+  }
+
+  onTwoFingerScaleEnd(ScaleEndDetails d) {
+    if (lastDeviceKind != PointerDeviceKind.touch) {
+      return;
+    }
+    if (isDesktop) {
+      bind.sessionSendPointer(
+          sessionId: sessionId,
+          msg: json.encode(
+              PointerEventToRust(kPointerEventKindTouch, 'scale', 0).toJson()));
+    } else {
+      // mobile
+      _scale = 1;
+      bind.sessionSetViewStyle(sessionId: sessionId, value: "");
+    }
+    inputModel.sendMouse('up', MouseButtons.left);
+  }
+
+  get onHoldDragCancel => null;
+  get onThreeFingerVerticalDragUpdate => ffi.ffiModel.isPeerAndroid
+      ? null
+      : (d) {
+          _mouseScrollIntegral += d.delta.dy / 4;
+          if (_mouseScrollIntegral > 1) {
+            inputModel.scroll(1);
+            _mouseScrollIntegral = 0;
+          } else if (_mouseScrollIntegral < -1) {
+            inputModel.scroll(-1);
+            _mouseScrollIntegral = 0;
+          }
+        };
+
+  makeGestures(BuildContext context) {
+    return <Type, GestureRecognizerFactory>{
+      // Official
+      TapGestureRecognizer:
+          GestureRecognizerFactoryWithHandlers<TapGestureRecognizer>(
+              () => TapGestureRecognizer(), (instance) {
+        instance
+          ..onTapDown = onTapDown
+          ..onTapUp = onTapUp
+          ..onTap = onTap;
+      }),
+      DoubleTapGestureRecognizer:
+          GestureRecognizerFactoryWithHandlers<DoubleTapGestureRecognizer>(
+              () => DoubleTapGestureRecognizer(), (instance) {
+        instance
+          ..onDoubleTapDown = onDoubleTapDown
+          ..onDoubleTap = onDoubleTap;
+      }),
+      LongPressGestureRecognizer:
+          GestureRecognizerFactoryWithHandlers<LongPressGestureRecognizer>(
+              () => LongPressGestureRecognizer(), (instance) {
+        instance
+          ..onLongPressDown = onLongPressDown
+          ..onLongPressUp = onLongPressUp
+          ..onLongPress = onLongPress;
+      }),
+      // Customized
+      HoldTapMoveGestureRecognizer:
+          GestureRecognizerFactoryWithHandlers<HoldTapMoveGestureRecognizer>(
+              () => HoldTapMoveGestureRecognizer(),
+              (instance) => instance
+                ..onHoldDragStart = onHoldDragStart
+                ..onHoldDragUpdate = onHoldDragUpdate
+                ..onHoldDragCancel = onHoldDragCancel
+                ..onHoldDragEnd = onHoldDragEnd),
+      DoubleFinerTapGestureRecognizer:
+          GestureRecognizerFactoryWithHandlers<DoubleFinerTapGestureRecognizer>(
+              () => DoubleFinerTapGestureRecognizer(), (instance) {
+        instance
+          ..onDoubleFinerTap = onDoubleFinerTap
+          ..onDoubleFinerTapDown = onDoubleFinerTapDown;
+      }),
+      CustomTouchGestureRecognizer:
+          GestureRecognizerFactoryWithHandlers<CustomTouchGestureRecognizer>(
+              () => CustomTouchGestureRecognizer(), (instance) {
+        instance.onOneFingerPanStart =
+            (DragStartDetails d) => onOneFingerPanStart(context, d);
+        instance
+          ..onOneFingerPanUpdate = onOneFingerPanUpdate
+          ..onOneFingerPanEnd = onOneFingerPanEnd
+          ..onTwoFingerScaleStart = onTwoFingerScaleStart
+          ..onTwoFingerScaleUpdate = onTwoFingerScaleUpdate
+          ..onTwoFingerScaleEnd = onTwoFingerScaleEnd
+          ..onThreeFingerVerticalDragUpdate = onThreeFingerVerticalDragUpdate;
+      }),
+    };
+  }
+}
+
 class RawPointerMouseRegion extends StatelessWidget {
   final InputModel inputModel;
   final Widget child;
@@ -39,36 +376,39 @@ class RawPointerMouseRegion extends StatelessWidget {
   final PointerDownEventListener? onPointerDown;
   final PointerUpEventListener? onPointerUp;
 
-  RawPointerMouseRegion(
-      {this.onEnter,
-      this.onExit,
-      this.cursor,
-      this.onPointerDown,
-      this.onPointerUp,
-      required this.inputModel,
-      required this.child});
+  RawPointerMouseRegion({
+    this.onEnter,
+    this.onExit,
+    this.cursor,
+    this.onPointerDown,
+    this.onPointerUp,
+    required this.inputModel,
+    required this.child,
+  });
 
   @override
   Widget build(BuildContext context) {
     return Listener(
-        onPointerHover: inputModel.onPointHoverImage,
-        onPointerDown: (evt) {
-          onPointerDown?.call(evt);
-          inputModel.onPointDownImage(evt);
-        },
-        onPointerUp: (evt) {
-          onPointerUp?.call(evt);
-          inputModel.onPointUpImage(evt);
-        },
-        onPointerMove: inputModel.onPointMoveImage,
-        onPointerSignal: inputModel.onPointerSignalImage,
-        onPointerPanZoomStart: inputModel.onPointerPanZoomStart,
-        onPointerPanZoomUpdate: inputModel.onPointerPanZoomUpdate,
-        onPointerPanZoomEnd: inputModel.onPointerPanZoomEnd,
-        child: MouseRegion(
-            cursor: cursor ?? MouseCursor.defer,
-            onEnter: onEnter,
-            onExit: onExit,
-            child: child));
+      onPointerHover: inputModel.onPointHoverImage,
+      onPointerDown: (evt) {
+        onPointerDown?.call(evt);
+        inputModel.onPointDownImage(evt);
+      },
+      onPointerUp: (evt) {
+        onPointerUp?.call(evt);
+        inputModel.onPointUpImage(evt);
+      },
+      onPointerMove: inputModel.onPointMoveImage,
+      onPointerSignal: inputModel.onPointerSignalImage,
+      onPointerPanZoomStart: inputModel.onPointerPanZoomStart,
+      onPointerPanZoomUpdate: inputModel.onPointerPanZoomUpdate,
+      onPointerPanZoomEnd: inputModel.onPointerPanZoomEnd,
+      child: MouseRegion(
+        cursor: cursor ?? MouseCursor.defer,
+        onEnter: onEnter,
+        onExit: onExit,
+        child: child,
+      ),
+    );
   }
 }
diff --git a/flutter/lib/common/widgets/setting_widgets.dart b/flutter/lib/common/widgets/setting_widgets.dart
new file mode 100644
index 000000000..771b65ab5
--- /dev/null
+++ b/flutter/lib/common/widgets/setting_widgets.dart
@@ -0,0 +1,277 @@
+import 'package:debounce_throttle/debounce_throttle.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter/services.dart';
+import 'package:flutter_hbb/common.dart';
+import 'package:flutter_hbb/models/platform_model.dart';
+import 'package:get/get.dart';
+
+customImageQualityWidget(
+    {required double initQuality,
+    required double initFps,
+    required Function(double) setQuality,
+    required Function(double) setFps,
+    required bool showFps}) {
+  final qualityValue = initQuality.obs;
+  final fpsValue = initFps.obs;
+
+  final RxBool moreQualityChecked = RxBool(qualityValue.value > 100);
+  final debouncerQuality = Debouncer<double>(
+    Duration(milliseconds: 1000),
+    onChanged: (double v) {
+      setQuality(v);
+    },
+    initialValue: qualityValue.value,
+  );
+  final debouncerFps = Debouncer<double>(
+    Duration(milliseconds: 1000),
+    onChanged: (double v) {
+      setFps(v);
+    },
+    initialValue: fpsValue.value,
+  );
+
+  onMoreChanged(bool? value) {
+    if (value == null) return;
+    moreQualityChecked.value = value;
+    if (!value && qualityValue.value > 100) {
+      qualityValue.value = 100;
+    }
+    debouncerQuality.value = qualityValue.value;
+  }
+
+  return Column(
+    children: [
+      Obx(() => Row(
+            children: [
+              Expanded(
+                flex: 3,
+                child: Slider(
+                  value: qualityValue.value,
+                  min: 10.0,
+                  max: moreQualityChecked.value ? 2000 : 100,
+                  divisions: moreQualityChecked.value ? 199 : 18,
+                  onChanged: (double value) async {
+                    qualityValue.value = value;
+                    debouncerQuality.value = value;
+                  },
+                ),
+              ),
+              Expanded(
+                  flex: 1,
+                  child: Text(
+                    '${qualityValue.value.round()}%',
+                    style: const TextStyle(fontSize: 15),
+                  )),
+              Expanded(
+                  flex: isMobile ? 2 : 1,
+                  child: Text(
+                    translate('Bitrate'),
+                    style: const TextStyle(fontSize: 15),
+                  )),
+              // mobile doesn't have enough space
+              if (!isMobile)
+                Expanded(
+                    flex: 1,
+                    child: Row(
+                      children: [
+                        Checkbox(
+                          value: moreQualityChecked.value,
+                          onChanged: onMoreChanged,
+                        ),
+                        Expanded(
+                          child: Text(translate('More')),
+                        )
+                      ],
+                    ))
+            ],
+          )),
+      if (isMobile)
+        Obx(() => Row(
+              children: [
+                Expanded(
+                  child: Align(
+                    alignment: Alignment.centerRight,
+                    child: Checkbox(
+                      value: moreQualityChecked.value,
+                      onChanged: onMoreChanged,
+                    ),
+                  ),
+                ),
+                Expanded(
+                  child: Text(translate('More')),
+                )
+              ],
+            )),
+      if (showFps)
+        Obx(() => Row(
+              children: [
+                Expanded(
+                  flex: 3,
+                  child: Slider(
+                    value: fpsValue.value,
+                    min: 5.0,
+                    max: 120.0,
+                    divisions: 23,
+                    onChanged: (double value) async {
+                      fpsValue.value = value;
+                      debouncerFps.value = value;
+                    },
+                  ),
+                ),
+                Expanded(
+                    flex: 1,
+                    child: Text(
+                      '${fpsValue.value.round()}',
+                      style: const TextStyle(fontSize: 15),
+                    )),
+                Expanded(
+                    flex: 2,
+                    child: Text(
+                      translate('FPS'),
+                      style: const TextStyle(fontSize: 15),
+                    ))
+              ],
+            )),
+    ],
+  );
+}
+
+customImageQualitySetting() {
+  final qualityKey = 'custom_image_quality';
+  final fpsKey = 'custom-fps';
+
+  var initQuality =
+      (double.tryParse(bind.mainGetUserDefaultOption(key: qualityKey)) ?? 50.0);
+  if (initQuality < 10 || initQuality > 2000) {
+    initQuality = 50;
+  }
+  var initFps =
+      (double.tryParse(bind.mainGetUserDefaultOption(key: fpsKey)) ?? 30.0);
+  if (initFps < 5 || initFps > 120) {
+    initFps = 30;
+  }
+
+  return customImageQualityWidget(
+      initQuality: initQuality,
+      initFps: initFps,
+      setQuality: (v) {
+        bind.mainSetUserDefaultOption(key: qualityKey, value: v.toString());
+      },
+      setFps: (v) {
+        bind.mainSetUserDefaultOption(key: fpsKey, value: v.toString());
+      },
+      showFps: true);
+}
+
+Future<bool> setServerConfig(
+  List<TextEditingController> controllers,
+  List<RxString> errMsgs,
+  ServerConfig config,
+) async {
+  config.idServer = config.idServer.trim();
+  config.relayServer = config.relayServer.trim();
+  config.apiServer = config.apiServer.trim();
+  config.key = config.key.trim();
+  // id
+  if (config.idServer.isNotEmpty) {
+    errMsgs[0].value =
+        translate(await bind.mainTestIfValidServer(server: config.idServer));
+    if (errMsgs[0].isNotEmpty) {
+      return false;
+    }
+  }
+  // relay
+  if (config.relayServer.isNotEmpty) {
+    errMsgs[1].value =
+        translate(await bind.mainTestIfValidServer(server: config.relayServer));
+    if (errMsgs[1].isNotEmpty) {
+      return false;
+    }
+  }
+  // api
+  if (config.apiServer.isNotEmpty) {
+    if (!config.apiServer.startsWith('http://') &&
+        !config.apiServer.startsWith('https://')) {
+      errMsgs[2].value =
+          '${translate("API Server")}: ${translate("invalid_http")}';
+      return false;
+    }
+  }
+  final oldApiServer = await bind.mainGetApiServer();
+
+  // should set one by one
+  await bind.mainSetOption(
+      key: 'custom-rendezvous-server', value: config.idServer);
+  await bind.mainSetOption(key: 'relay-server', value: config.relayServer);
+  await bind.mainSetOption(key: 'api-server', value: config.apiServer);
+  await bind.mainSetOption(key: 'key', value: config.key);
+
+  final newApiServer = await bind.mainGetApiServer();
+  if (oldApiServer.isNotEmpty &&
+      oldApiServer != newApiServer &&
+      gFFI.userModel.isLogin) {
+    gFFI.userModel.logOut(apiServer: oldApiServer);
+  }
+  return true;
+}
+
+List<Widget> ServerConfigImportExportWidgets(
+  List<TextEditingController> controllers,
+  List<RxString> errMsgs,
+) {
+  import() {
+    Clipboard.getData(Clipboard.kTextPlain).then((value) {
+      final text = value?.text;
+      if (text != null && text.isNotEmpty) {
+        try {
+          final sc = ServerConfig.decode(text);
+          if (sc.idServer.isNotEmpty) {
+            controllers[0].text = sc.idServer;
+            controllers[1].text = sc.relayServer;
+            controllers[2].text = sc.apiServer;
+            controllers[3].text = sc.key;
+            Future<bool> success = setServerConfig(controllers, errMsgs, sc);
+            success.then((value) {
+              if (value) {
+                showToast(
+                    translate('Import server configuration successfully'));
+              } else {
+                showToast(translate('Invalid server configuration'));
+              }
+            });
+          } else {
+            showToast(translate('Invalid server configuration'));
+          }
+        } catch (e) {
+          showToast(translate('Invalid server configuration'));
+        }
+      } else {
+        showToast(translate('Clipboard is empty'));
+      }
+    });
+  }
+
+  export() {
+    final text = ServerConfig(
+            idServer: controllers[0].text.trim(),
+            relayServer: controllers[1].text.trim(),
+            apiServer: controllers[2].text.trim(),
+            key: controllers[3].text.trim())
+        .encode();
+    debugPrint("ServerConfig export: $text");
+    Clipboard.setData(ClipboardData(text: text));
+    showToast(translate('Export server configuration successfully'));
+  }
+
+  return [
+    Tooltip(
+      message: translate('Import Server Config'),
+      child: IconButton(
+          icon: Icon(Icons.paste, color: Colors.grey), onPressed: import),
+    ),
+    Tooltip(
+        message: translate('Export Server Config'),
+        child: IconButton(
+            icon: Icon(Icons.copy, color: Colors.grey), onPressed: export))
+  ];
+}
diff --git a/flutter/lib/common/widgets/toolbar.dart b/flutter/lib/common/widgets/toolbar.dart
index 98621a87e..4cf6fd3ea 100644
--- a/flutter/lib/common/widgets/toolbar.dart
+++ b/flutter/lib/common/widgets/toolbar.dart
@@ -11,6 +11,8 @@ import 'package:flutter_hbb/models/model.dart';
 import 'package:flutter_hbb/models/platform_model.dart';
 import 'package:get/get.dart';
 
+bool isEditOsPassword = false;
+
 class TTextMenu {
   final Widget child;
   final VoidCallback onPressed;
@@ -44,6 +46,28 @@ class TToggleMenu {
       {required this.child, required this.value, required this.onChanged});
 }
 
+handleOsPasswordEditIcon(
+    SessionID sessionId, OverlayDialogManager dialogManager) {
+  isEditOsPassword = true;
+  showSetOSPassword(sessionId, false, dialogManager, null, () => isEditOsPassword = false);
+}
+
+handleOsPasswordAction(
+    SessionID sessionId, OverlayDialogManager dialogManager) async {
+  if (isEditOsPassword) {
+    isEditOsPassword = false;
+    return;
+  }
+  final password =
+      await bind.sessionGetOption(sessionId: sessionId, arg: 'os-password') ??
+          '';
+  if (password.isEmpty) {
+    showSetOSPassword(sessionId, true, dialogManager, password, () => isEditOsPassword = false);
+  } else {
+    bind.sessionInputOsPassword(sessionId: sessionId, value: password);
+  }
+}
+
 List<TTextMenu> toolbarControls(BuildContext context, String id, FFI ffi) {
   final ffiModel = ffi.ffiModel;
   final pi = ffiModel.pi;
@@ -63,17 +87,26 @@ List<TTextMenu> toolbarControls(BuildContext context, String id, FFI ffi) {
   // osAccount / osPassword
   v.add(
     TTextMenu(
-        child: Row(children: [
-          Text(translate(pi.is_headless ? 'OS Account' : 'OS Password')),
-          Offstage(
-              offstage: isDesktop,
-              child:
-                  Icon(Icons.edit, color: MyTheme.accent).marginOnly(left: 12))
-        ]),
-        trailingIcon: Transform.scale(scale: 0.8, child: Icon(Icons.edit)),
-        onPressed: () => pi.is_headless
-            ? showSetOSAccount(sessionId, ffi.dialogManager)
-            : showSetOSPassword(sessionId, false, ffi.dialogManager)),
+      child: Row(children: [
+        Text(translate(pi.is_headless ? 'OS Account' : 'OS Password')),
+        Offstage(
+          offstage: isDesktop,
+          child: Icon(Icons.edit, color: MyTheme.accent).marginOnly(left: 12),
+        )
+      ]),
+      trailingIcon: Transform.scale(
+        scale: 0.8,
+        child: InkWell(
+          onTap: () => pi.is_headless
+              ? showSetOSAccount(sessionId, ffi.dialogManager)
+              : handleOsPasswordEditIcon(sessionId, ffi.dialogManager),
+          child: Icon(Icons.edit),
+        ),
+      ),
+      onPressed: () => pi.is_headless
+          ? showSetOSAccount(sessionId, ffi.dialogManager)
+          : handleOsPasswordAction(sessionId, ffi.dialogManager),
+    ),
   );
   // paste
   if (isMobile && perms['keyboard'] != false && perms['clipboard'] != false) {
diff --git a/flutter/lib/consts.dart b/flutter/lib/consts.dart
index 3e664c484..7fcc7b3a7 100644
--- a/flutter/lib/consts.dart
+++ b/flutter/lib/consts.dart
@@ -5,6 +5,7 @@ import 'package:flutter_hbb/common.dart';
 import 'package:flutter_hbb/models/state_model.dart';
 
 const double kDesktopRemoteTabBarHeight = 28.0;
+const int kInvalidWindowId = -1;
 const int kMainWindowId = 0;
 
 const String kPeerPlatformWindows = "Windows";
@@ -30,6 +31,21 @@ const String kWindowEventHide = "hide";
 const String kWindowEventShow = "show";
 const String kWindowConnect = "connect";
 
+const String kWindowEventNewRemoteDesktop = "new_remote_desktop";
+const String kWindowEventNewFileTransfer = "new_file_transfer";
+const String kWindowEventNewPortForward = "new_port_forward";
+const String kWindowEventActiveSession = "active_session";
+const String kWindowEventGetRemoteList = "get_remote_list";
+const String kWindowEventGetSessionIdList = "get_session_id_list";
+
+const String kWindowEventMoveTabToNewWindow = "move_tab_to_new_window";
+const String kWindowEventGetCachedSessionData = "get_cached_session_data";
+
+const String kOptionOpenNewConnInTabs = "enable-open-new-connections-in-tabs";
+const String kOptionOpenInTabs = "allow-open-in-tabs";
+const String kOptionOpenInWindows = "allow-open-in-windows";
+const String kOptionForceAlwaysRelay = "force-always-relay";
+
 const String kUniLinksPrefix = "rustdesk://";
 const String kUrlActionClose = "close";
 
@@ -39,6 +55,9 @@ const String kTabLabelSettingPage = "Settings";
 const String kWindowPrefix = "wm_";
 const int kWindowMainId = 0;
 
+const String kPointerEventKindTouch = "touch";
+const String kPointerEventKindMouse = "mouse";
+
 // the executable name of the portable version
 const String kEnvPortableExecutable = "RUSTDESK_APPNAME";
 
@@ -50,11 +69,8 @@ const int kMobileDefaultDisplayHeight = 1280;
 const int kDesktopDefaultDisplayWidth = 1080;
 const int kDesktopDefaultDisplayHeight = 720;
 
-const int kMobileMaxDisplayWidth = 720;
-const int kMobileMaxDisplayHeight = 1280;
-
-const int kDesktopMaxDisplayWidth = 1920;
-const int kDesktopMaxDisplayHeight = 1080;
+const int kMobileMaxDisplaySize = 1280;
+const int kDesktopMaxDisplaySize = 3840;
 
 const double kDesktopFileTransferNameColWidth = 200;
 const double kDesktopFileTransferModifiedColWidth = 120;
@@ -65,7 +81,7 @@ const double kDesktopFileTransferHeaderHeight = 25.0;
 
 EdgeInsets get kDragToResizeAreaPadding =>
     !kUseCompatibleUiMode && Platform.isLinux
-        ? stateGlobal.fullscreen || stateGlobal.maximize
+        ? stateGlobal.fullscreen || stateGlobal.isMaximized.value
             ? EdgeInsets.zero
             : EdgeInsets.all(5.0)
         : EdgeInsets.zero;
diff --git a/flutter/lib/desktop/pages/connection_page.dart b/flutter/lib/desktop/pages/connection_page.dart
index dc04b2f4d..6d53ecc78 100644
--- a/flutter/lib/desktop/pages/connection_page.dart
+++ b/flutter/lib/desktop/pages/connection_page.dart
@@ -9,7 +9,6 @@ import 'package:flutter/material.dart';
 import 'package:flutter_hbb/consts.dart';
 import 'package:flutter_hbb/desktop/widgets/scroll_wrapper.dart';
 import 'package:flutter_hbb/models/state_model.dart';
-import 'package:flutter_hbb/models/user_model.dart';
 import 'package:get/get.dart';
 import 'package:url_launcher/url_launcher_string.dart';
 import 'package:window_manager/window_manager.dart';
@@ -69,6 +68,7 @@ class _ConnectionPageState extends State<ConnectionPage>
       _idController.selection = TextSelection(
           baseOffset: 0, extentOffset: _idController.value.text.length);
     });
+    Get.put<IDTextEditingController>(_idController);
     windowManager.addListener(this);
   }
 
@@ -77,6 +77,9 @@ class _ConnectionPageState extends State<ConnectionPage>
     _idController.dispose();
     _updateTimer?.cancel();
     windowManager.removeListener(this);
+    if (Get.isRegistered<IDTextEditingController>()) {
+      Get.delete<IDTextEditingController>();
+    }
     super.dispose();
   }
 
@@ -103,7 +106,8 @@ class _ConnectionPageState extends State<ConnectionPage>
   @override
   void onWindowLeaveFullScreen() {
     // Restore edge border to default edge size.
-    stateGlobal.resizeEdgeSize.value = kWindowEdgeSize;
+    stateGlobal.resizeEdgeSize.value =
+        stateGlobal.isMaximized.isTrue ? kMaximizeEdgeSize : kWindowEdgeSize;
   }
 
   @override
diff --git a/flutter/lib/desktop/pages/desktop_home_page.dart b/flutter/lib/desktop/pages/desktop_home_page.dart
index e074f7598..d458402d3 100644
--- a/flutter/lib/desktop/pages/desktop_home_page.dart
+++ b/flutter/lib/desktop/pages/desktop_home_page.dart
@@ -185,11 +185,13 @@ class _DesktopHomePageState extends State<DesktopHomePage>
           backgroundColor: hover.value
               ? Theme.of(context).scaffoldBackgroundColor
               : Theme.of(context).colorScheme.background,
-          child: Icon(
-            Icons.more_vert_outlined,
-            size: 20,
-            color: hover.value ? textColor : textColor?.withOpacity(0.5),
-          ),
+          child: Tooltip(
+            message: translate('Settings'),
+            child: Icon(
+              Icons.more_vert_outlined,
+              size: 20,
+              color: hover.value ? textColor : textColor?.withOpacity(0.5),
+            )),
         ),
       ),
       onHover: (value) => hover.value = value,
@@ -252,23 +254,28 @@ class _DesktopHomePageState extends State<DesktopHomePage>
                         onPressed: () => bind.mainUpdateTemporaryPassword(),
                         child: Obx(() => RotatedBox(
                             quarterTurns: 2,
-                            child: Icon(
-                              Icons.refresh,
-                              color: refreshHover.value
-                                  ? textColor
-                                  : Color(0xFFDDDDDD),
-                              size: 22,
-                            ))),
+                            child: Tooltip(
+                              message: translate('Refresh Password'),
+                              child: Icon(
+                                Icons.refresh,
+                                color: refreshHover.value
+                                    ? textColor
+                                    : Color(0xFFDDDDDD),
+                                size: 22,
+                              ))
+                            )),
                         onHover: (value) => refreshHover.value = value,
                       ).marginOnly(right: 8, top: 4),
                       InkWell(
                         child: Obx(
-                          () => Icon(
-                            Icons.edit,
-                            color:
-                                editHover.value ? textColor : Color(0xFFDDDDDD),
-                            size: 22,
-                          ).marginOnly(right: 8, top: 4),
+                          () => Tooltip(
+                            message: translate('Change Password'),
+                            child: Icon(
+                              Icons.edit,
+                              color:
+                                  editHover.value ? textColor : Color(0xFFDDDDDD),
+                              size: 22,
+                            )).marginOnly(right: 8, top: 4),
                         ),
                         onTap: () => DesktopSettingPage.switch2page(1),
                         onHover: (value) => editHover.value = value,
@@ -319,7 +326,7 @@ class _DesktopHomePageState extends State<DesktopHomePage>
           "Status",
           "There is a newer version of ${bind.mainGetAppNameSync()} ${bind.mainGetNewVersion()} available.",
           "Click to download", () async {
-        final Uri url = Uri.parse('https://rustdesk.com');
+        final Uri url = Uri.parse('https://rustdesk.com/download');
         await launchUrl(url);
       });
     }
@@ -372,7 +379,7 @@ class _DesktopHomePageState extends State<DesktopHomePage>
     } else if (Platform.isLinux) {
       if (bind.mainCurrentIsWayland()) {
         return buildInstallCard(
-            "Warning", translate("wayland_experiment_tip"), "", () async {},
+            "Warning", "wayland_experiment_tip", "", () async {},
             help: 'Help',
             link: 'https://rustdesk.com/docs/en/manual/linux/#x11-required');
       } else if (bind.mainIsLoginWayland()) {
@@ -527,7 +534,7 @@ class _DesktopHomePageState extends State<DesktopHomePage>
       debugPrint(
           "[Main] call ${call.method} with args ${call.arguments} from window $fromWindowId");
       if (call.method == kWindowMainWindowOnTop) {
-        window_on_top(null);
+        windowOnTop(null);
       } else if (call.method == kWindowGetWindowInfo) {
         final screen = (await window_size.getWindowInfo()).screen;
         if (screen == null) {
@@ -554,7 +561,7 @@ class _DesktopHomePageState extends State<DesktopHomePage>
       } else if (call.method == kWindowEventShow) {
         await rustDeskWinManager.registerActiveWindow(call.arguments["id"]);
       } else if (call.method == kWindowEventHide) {
-        await rustDeskWinManager.unregisterActiveWindow(call.arguments["id"]);
+        await rustDeskWinManager.unregisterActiveWindow(call.arguments['id']);
       } else if (call.method == kWindowConnect) {
         await connectMainDesktop(
           call.arguments['id'],
@@ -563,6 +570,17 @@ class _DesktopHomePageState extends State<DesktopHomePage>
           isRDP: call.arguments['isRDP'],
           forceRelay: call.arguments['forceRelay'],
         );
+      } else if (call.method == kWindowEventMoveTabToNewWindow) {
+        final args = call.arguments.split(',');
+        int? windowId;
+        try {
+          windowId = int.parse(args[0]);
+        } catch (e) {
+          debugPrint("Failed to parse window id '${call.arguments}': $e");
+        }
+        if (windowId != null) {
+          await rustDeskWinManager.moveTabToNewWindow(windowId, args[1], args[2]);
+        }
       }
     });
     _uniLinksSubscription = listenUniLinks();
diff --git a/flutter/lib/desktop/pages/desktop_setting_page.dart b/flutter/lib/desktop/pages/desktop_setting_page.dart
index fe617140a..3a33c7e5b 100644
--- a/flutter/lib/desktop/pages/desktop_setting_page.dart
+++ b/flutter/lib/desktop/pages/desktop_setting_page.dart
@@ -5,6 +5,7 @@ import 'package:file_picker/file_picker.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter/services.dart';
 import 'package:flutter_hbb/common.dart';
+import 'package:flutter_hbb/common/widgets/setting_widgets.dart';
 import 'package:flutter_hbb/consts.dart';
 import 'package:flutter_hbb/desktop/pages/desktop_home_page.dart';
 import 'package:flutter_hbb/desktop/pages/desktop_tab_page.dart';
@@ -17,7 +18,6 @@ import 'package:provider/provider.dart';
 import 'package:url_launcher/url_launcher.dart';
 import 'package:url_launcher/url_launcher_string.dart';
 import 'package:flutter_hbb/desktop/widgets/scroll_wrapper.dart';
-import 'package:window_manager/window_manager.dart';
 
 import '../../common/widgets/dialog.dart';
 import '../../common/widgets/login.dart';
@@ -248,7 +248,7 @@ class _General extends StatefulWidget {
 
 class _GeneralState extends State<_General> {
   final RxBool serviceStop = Get.find<RxBool>(tag: 'stop-service');
-  RxBool serviceBtnEabled = true.obs;
+  RxBool serviceBtnEnabled = true.obs;
 
   @override
   Widget build(BuildContext context) {
@@ -300,32 +300,41 @@ class _GeneralState extends State<_General> {
     return _Card(title: 'Service', children: [
       Obx(() => _Button(serviceStop.value ? 'Start' : 'Stop', () {
             () async {
-              serviceBtnEabled.value = false;
+              serviceBtnEnabled.value = false;
               await start_service(serviceStop.value);
               // enable the button after 1 second
               Future.delayed(const Duration(seconds: 1), () {
-                serviceBtnEabled.value = true;
+                serviceBtnEnabled.value = true;
               });
             }();
-          }, enabled: serviceBtnEabled.value))
+          }, enabled: serviceBtnEnabled.value))
     ]);
   }
 
   Widget other() {
-    return _Card(title: 'Other', children: [
+    final children = [
       _OptionCheckBox(context, 'Confirm before closing multiple tabs',
-          'enable-confirm-closing-tabs'),
-      _OptionCheckBox(context, 'Adaptive Bitrate', 'enable-abr'),
-      if (Platform.isLinux)
-        Tooltip(
-          message: translate('software_render_tip'),
-          child: _OptionCheckBox(
-            context,
-            "Always use software rendering",
-            'allow-always-software-render',
-          ),
-        )
-    ]);
+          'enable-confirm-closing-tabs',
+          isServer: false),
+      _OptionCheckBox(context, 'Adaptive bitrate', 'enable-abr'),
+      _OptionCheckBox(
+        context,
+        'Open connection in new tab',
+        kOptionOpenNewConnInTabs,
+        isServer: false,
+      ),
+    ];
+    // though this is related to GUI, but opengl problem affects all users, so put in config rather than local
+    children.add(Tooltip(
+      message: translate('software_render_tip'),
+      child: _OptionCheckBox(context, "Always use software rendering",
+          'allow-always-software-render'),
+    ));
+    if (bind.mainShowOption(key: 'allow-linux-headless')) {
+      children.add(_OptionCheckBox(
+          context, 'Allow linux headless', 'allow-linux-headless'));
+    }
+    return _Card(title: 'Other', children: children);
   }
 
   Widget hwcodec() {
@@ -385,20 +394,17 @@ class _GeneralState extends State<_General> {
 
   Widget record(BuildContext context) {
     return futureBuilder(future: () async {
-      String customDirectory =
-          await bind.mainGetOption(key: 'video-save-directory');
       String defaultDirectory = await bind.mainDefaultVideoSaveDirectory();
-      String dir;
-      if (customDirectory.isNotEmpty) {
-        dir = customDirectory;
-      } else {
-        dir = defaultDirectory;
-      }
       // canLaunchUrl blocked on windows portable, user SYSTEM
-      return {'dir': dir, 'canlaunch': true};
+      return {'dir': defaultDirectory, 'canlaunch': true};
     }(), hasData: (data) {
       Map<String, dynamic> map = data as Map<String, dynamic>;
       String dir = map['dir']!;
+      String customDirectory =
+          bind.mainGetOptionSync(key: 'video-save-directory');
+      if (customDirectory.isNotEmpty) {
+        dir = customDirectory;
+      }
       bool canlaunch = map['canlaunch']! as bool;
 
       return _Card(title: 'Recording', children: [
@@ -444,8 +450,7 @@ class _GeneralState extends State<_General> {
   Widget language() {
     return futureBuilder(future: () async {
       String langs = await bind.mainGetLangs();
-      String lang = bind.mainGetLocalOption(key: kCommConfKeyLang);
-      return {'langs': langs, 'lang': lang};
+      return {'langs': langs};
     }(), hasData: (res) {
       Map<String, String> data = res as Map<String, String>;
       List<dynamic> langsList = jsonDecode(data['langs']!);
@@ -454,7 +459,7 @@ class _GeneralState extends State<_General> {
       List<String> values = langsMap.values.toList();
       keys.insert(0, '');
       values.insert(0, translate('Default'));
-      String currentKey = data['lang']!;
+      String currentKey = bind.mainGetLocalOption(key: kCommConfKeyLang);
       if (!keys.contains(currentKey)) {
         currentKey = '';
       }
@@ -529,10 +534,9 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin {
 
   Widget permissions(context) {
     bool enabled = !locked;
-    return futureBuilder(future: () async {
-      return await bind.mainGetOption(key: 'access-mode');
-    }(), hasData: (data) {
-      String accessMode = data! as String;
+    // Simple temp wrapper for PR check
+    tmpWrapper() {
+      String accessMode = bind.mainGetOptionSync(key: 'access-mode');
       _AccessMode mode;
       if (accessMode == 'full') {
         mode = _AccessMode.full;
@@ -601,7 +605,9 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin {
           ],
         ),
       ]);
-    });
+    }
+
+    return tmpWrapper();
   }
 
   Widget password(BuildContext context) {
@@ -702,8 +708,8 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin {
             if (usePassword)
               _SubButton('Set permanent password', setPasswordDialog,
                   permEnabled && !locked),
-            if (usePassword)
-              hide_cm(!locked).marginOnly(left: _kContentHSubMargin - 6),
+            // if (usePassword)
+            //   hide_cm(!locked).marginOnly(left: _kContentHSubMargin - 6),
             if (usePassword) radios[2],
           ]);
         })));
@@ -759,17 +765,13 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin {
     return [
       _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());
+      () {
+        // Simple temp wrapper for PR check
+        tmpWrapper() {
+          bool enabled = option2bool(
+              'direct-server', bind.mainGetOptionSync(key: 'direct-server'));
           if (!enabled) applyEnabled.value = false;
-          controller.text = data['port'].toString();
+          controller.text = bind.mainGetOptionSync(key: 'direct-access-port');
           return Offstage(
             offstage: !enabled,
             child: _SubLabeledWidget(
@@ -810,20 +812,22 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin {
               enabled: enabled && !locked,
             ),
           );
-        },
-      ),
+        }
+
+        return tmpWrapper();
+      }(),
     ];
   }
 
   Widget whitelist() {
     bool enabled = !locked;
-    return futureBuilder(future: () async {
-      return await bind.mainGetOption(key: 'whitelist');
-    }(), hasData: (data) {
-      RxBool hasWhitelist = (data as String).isNotEmpty.obs;
+    // Simple temp wrapper for PR check
+    tmpWrapper() {
+      RxBool hasWhitelist =
+          bind.mainGetOptionSync(key: 'whitelist').isNotEmpty.obs;
       update() async {
         hasWhitelist.value =
-            (await bind.mainGetOption(key: 'whitelist')).isNotEmpty;
+            bind.mainGetOptionSync(key: 'whitelist').isNotEmpty;
       }
 
       onChanged(bool? checked) async {
@@ -858,7 +862,9 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin {
           onChanged(!hasWhitelist.value);
         },
       ).marginOnly(left: _kCheckBoxLeftMargin);
-    });
+    }
+
+    return tmpWrapper();
   }
 
   Widget hide_cm(bool enabled) {
@@ -943,11 +949,11 @@ class _NetworkState extends State<_Network> with AutomaticKeepAliveClientMixin {
   }
 
   server(bool enabled) {
-    return futureBuilder(future: () async {
-      return await bind.mainGetOptions();
-    }(), hasData: (data) {
+    // Simple temp wrapper for PR check
+    tmpWrapper() {
       // Setting page is not modal, oldOptions should only be used when getting options, never when setting.
-      Map<String, dynamic> oldOptions = jsonDecode(data! as String);
+      Map<String, dynamic> oldOptions =
+          jsonDecode(bind.mainGetOptionsSync() as String);
       old(String key) {
         return (oldOptions[key] ?? '').trim();
       }
@@ -960,51 +966,27 @@ class _NetworkState extends State<_Network> with AutomaticKeepAliveClientMixin {
       var relayController = TextEditingController(text: old('relay-server'));
       var apiController = TextEditingController(text: old('api-server'));
       var keyController = TextEditingController(text: old('key'));
-
-      set(String idServer, String relayServer, String apiServer,
-          String key) async {
-        idServer = idServer.trim();
-        relayServer = relayServer.trim();
-        apiServer = apiServer.trim();
-        key = key.trim();
-        if (idServer.isNotEmpty) {
-          idErrMsg.value =
-              translate(await bind.mainTestIfValidServer(server: idServer));
-          if (idErrMsg.isNotEmpty) {
-            return false;
-          }
-        }
-        if (relayServer.isNotEmpty) {
-          relayErrMsg.value =
-              translate(await bind.mainTestIfValidServer(server: relayServer));
-          if (relayErrMsg.isNotEmpty) {
-            return false;
-          }
-        }
-        if (apiServer.isNotEmpty) {
-          if (!apiServer.startsWith('http://') &&
-              !apiServer.startsWith('https://')) {
-            apiErrMsg.value =
-                '${translate("API Server")}: ${translate("invalid_http")}';
-            return false;
-          }
-        }
-        final old = await bind.mainGetOption(key: 'custom-rendezvous-server');
-        if (old.isNotEmpty && old != idServer) {
-          await gFFI.userModel.logOut();
-        }
-        // should set one by one
-        await bind.mainSetOption(
-            key: 'custom-rendezvous-server', value: idServer);
-        await bind.mainSetOption(key: 'relay-server', value: relayServer);
-        await bind.mainSetOption(key: 'api-server', value: apiServer);
-        await bind.mainSetOption(key: 'key', value: key);
-        return true;
-      }
+      final controllers = [
+        idController,
+        relayController,
+        apiController,
+        keyController,
+      ];
+      final errMsgs = [
+        idErrMsg,
+        relayErrMsg,
+        apiErrMsg,
+      ];
 
       submit() async {
-        bool result = await set(idController.text, relayController.text,
-            apiController.text, keyController.text);
+        bool result = await setServerConfig(
+            controllers,
+            errMsgs,
+            ServerConfig(
+                idServer: idController.text,
+                relayServer: relayController.text,
+                apiServer: apiController.text,
+                key: keyController.text));
         if (result) {
           setState(() {});
           showToast(translate('Successful'));
@@ -1013,84 +995,31 @@ class _NetworkState extends State<_Network> with AutomaticKeepAliveClientMixin {
         }
       }
 
-      import() {
-        Clipboard.getData(Clipboard.kTextPlain).then((value) {
-          final text = value?.text;
-          if (text != null && text.isNotEmpty) {
-            try {
-              final sc = ServerConfig.decode(text);
-              if (sc.idServer.isNotEmpty) {
-                idController.text = sc.idServer;
-                relayController.text = sc.relayServer;
-                apiController.text = sc.apiServer;
-                keyController.text = sc.key;
-                Future<bool> success =
-                    set(sc.idServer, sc.relayServer, sc.apiServer, sc.key);
-                success.then((value) {
-                  if (value) {
-                    showToast(
-                        translate('Import server configuration successfully'));
-                  } else {
-                    showToast(translate('Invalid server configuration'));
-                  }
-                });
-              } else {
-                showToast(translate('Invalid server configuration'));
-              }
-            } catch (e) {
-              showToast(translate('Invalid server configuration'));
-            }
-          } else {
-            showToast(translate('Clipboard is empty'));
-          }
-        });
-      }
-
-      export() {
-        final text = ServerConfig(
-                idServer: idController.text,
-                relayServer: relayController.text,
-                apiServer: apiController.text,
-                key: keyController.text)
-            .encode();
-        debugPrint("ServerConfig export: $text");
-
-        Clipboard.setData(ClipboardData(text: text));
-        showToast(translate('Export server configuration successfully'));
-      }
-
       bool secure = !enabled;
-      return _Card(title: 'ID/Relay Server', title_suffix: [
-        Tooltip(
-          message: translate('Import Server Config'),
-          child: IconButton(
-              icon: Icon(Icons.paste, color: Colors.grey),
-              onPressed: enabled ? import : null),
-        ),
-        Tooltip(
-            message: translate('Export Server Config'),
-            child: IconButton(
-                icon: Icon(Icons.copy, color: Colors.grey),
-                onPressed: enabled ? export : null)),
-      ], children: [
-        Column(
+      return _Card(
+          title: 'ID/Relay Server',
+          title_suffix: ServerConfigImportExportWidgets(controllers, errMsgs),
           children: [
-            Obx(() => _LabeledTextField(context, 'ID Server', idController,
-                idErrMsg.value, enabled, secure)),
-            Obx(() => _LabeledTextField(context, 'Relay Server',
-                relayController, relayErrMsg.value, enabled, secure)),
-            Obx(() => _LabeledTextField(context, 'API Server', apiController,
-                apiErrMsg.value, enabled, secure)),
-            _LabeledTextField(
-                context, 'Key', keyController, '', enabled, secure),
-            Row(
-              mainAxisAlignment: MainAxisAlignment.end,
-              children: [_Button('Apply', submit, enabled: enabled)],
-            ).marginOnly(top: 10),
-          ],
-        )
-      ]);
-    });
+            Column(
+              children: [
+                Obx(() => _LabeledTextField(context, 'ID Server', idController,
+                    idErrMsg.value, enabled, secure)),
+                Obx(() => _LabeledTextField(context, 'Relay Server',
+                    relayController, relayErrMsg.value, enabled, secure)),
+                Obx(() => _LabeledTextField(context, 'API Server',
+                    apiController, apiErrMsg.value, enabled, secure)),
+                _LabeledTextField(
+                    context, 'Key', keyController, '', enabled, secure),
+                Row(
+                  mainAxisAlignment: MainAxisAlignment.end,
+                  children: [_Button('Apply', submit, enabled: enabled)],
+                ).marginOnly(top: 10),
+              ],
+            )
+          ]);
+    }
+
+    return tmpWrapper();
   }
 }
 
@@ -1171,15 +1100,6 @@ class _DisplayState extends State<_Display> {
     }
 
     final groupValue = bind.mainGetUserDefaultOption(key: key);
-    final qualityKey = 'custom_image_quality';
-    final qualityValue =
-        (double.tryParse(bind.mainGetUserDefaultOption(key: qualityKey)) ??
-                50.0)
-            .obs;
-    final fpsKey = 'custom-fps';
-    final fpsValue =
-        (double.tryParse(bind.mainGetUserDefaultOption(key: fpsKey)) ?? 30.0)
-            .obs;
     return _Card(title: 'Default Image Quality', children: [
       _Radio(context,
           value: kRemoteImageQualityBest,
@@ -1203,64 +1123,7 @@ class _DisplayState extends State<_Display> {
           onChanged: onChanged),
       Offstage(
         offstage: groupValue != kRemoteImageQualityCustom,
-        child: Column(
-          children: [
-            Obx(() => Row(
-                  children: [
-                    Slider(
-                      value: qualityValue.value,
-                      min: 10.0,
-                      max: 100.0,
-                      divisions: 18,
-                      onChanged: (double value) async {
-                        qualityValue.value = value;
-                        await bind.mainSetUserDefaultOption(
-                            key: qualityKey, value: value.toString());
-                      },
-                    ),
-                    SizedBox(
-                        width: 40,
-                        child: Text(
-                          '${qualityValue.value.round()}%',
-                          style: const TextStyle(fontSize: 15),
-                        )),
-                    SizedBox(
-                        width: 50,
-                        child: Text(
-                          translate('Bitrate'),
-                          style: const TextStyle(fontSize: 15),
-                        ))
-                  ],
-                )),
-            Obx(() => Row(
-                  children: [
-                    Slider(
-                      value: fpsValue.value,
-                      min: 5.0,
-                      max: 120.0,
-                      divisions: 23,
-                      onChanged: (double value) async {
-                        fpsValue.value = value;
-                        await bind.mainSetUserDefaultOption(
-                            key: fpsKey, value: value.toString());
-                      },
-                    ),
-                    SizedBox(
-                        width: 40,
-                        child: Text(
-                          '${fpsValue.value.round()}',
-                          style: const TextStyle(fontSize: 15),
-                        )),
-                    SizedBox(
-                        width: 50,
-                        child: Text(
-                          translate('FPS'),
-                          style: const TextStyle(fontSize: 15),
-                        ))
-                  ],
-                )),
-          ],
-        ),
+        child: customImageQualitySetting(),
       )
     ]);
   }
@@ -1385,7 +1248,7 @@ class _AccountState extends State<_Account> {
         () => {
               gFFI.userModel.userName.value.isEmpty
                   ? loginDialog()
-                  : gFFI.userModel.logOut()
+                  : logOutConfirmDialog()
             }));
   }
 
@@ -1503,7 +1366,7 @@ class _PluginState extends State<_Plugin> {
         () => {
               gFFI.userModel.userName.value.isEmpty
                   ? loginDialog()
-                  : gFFI.userModel.logOut()
+                  : logOutConfirmDialog()
             }));
   }
 }
@@ -1559,7 +1422,7 @@ class _AboutState extends State<_About> {
                           .marginSymmetric(vertical: 4.0)),
                   InkWell(
                       onTap: () {
-                        launchUrlString('https://rustdesk.com/privacy');
+                        launchUrlString('https://rustdesk.com/privacy.html');
                       },
                       child: Text(
                         translate('Privacy Statement'),
@@ -1661,54 +1524,54 @@ Widget _OptionCheckBox(BuildContext context, String label, String key,
     bool reverse = false,
     bool enabled = true,
     Icon? checkedIcon,
-    bool? fakeValue}) {
-  return futureBuilder(
-      future: bind.mainGetOption(key: key),
-      hasData: (data) {
-        bool value = option2bool(key, data.toString());
-        if (reverse) value = !value;
-        var ref = value.obs;
-        onChanged(option) async {
-          if (option != null) {
-            ref.value = option;
-            if (reverse) option = !option;
-            String value = bool2option(key, option);
-            await bind.mainSetOption(key: key, value: value);
-            update?.call();
+    bool? fakeValue,
+    bool isServer = true}) {
+  bool value =
+      isServer ? mainGetBoolOptionSync(key) : mainGetLocalBoolOptionSync(key);
+  if (reverse) value = !value;
+  var ref = value.obs;
+  onChanged(option) async {
+    if (option != null) {
+      if (reverse) option = !option;
+      isServer
+          ? await mainSetBoolOption(key, option)
+          : await mainSetLocalBoolOption(key, option);
+      ref.value = isServer
+          ? mainGetBoolOptionSync(key)
+          : mainGetLocalBoolOptionSync(key);
+      update?.call();
+    }
+  }
+
+  if (fakeValue != null) {
+    ref.value = fakeValue;
+    enabled = false;
+  }
+
+  return GestureDetector(
+    child: Obx(
+      () => Row(
+        children: [
+          Checkbox(value: ref.value, onChanged: enabled ? onChanged : null)
+              .marginOnly(right: 5),
+          Offstage(
+            offstage: !ref.value || checkedIcon == null,
+            child: checkedIcon?.marginOnly(right: 5),
+          ),
+          Expanded(
+              child: Text(
+            translate(label),
+            style: TextStyle(color: _disabledTextColor(context, enabled)),
+          ))
+        ],
+      ),
+    ).marginOnly(left: _kCheckBoxLeftMargin),
+    onTap: enabled
+        ? () {
+            onChanged(!ref.value);
           }
-        }
-
-        if (fakeValue != null) {
-          ref.value = fakeValue;
-          enabled = false;
-        }
-
-        return GestureDetector(
-          child: Obx(
-            () => Row(
-              children: [
-                Checkbox(
-                        value: ref.value, onChanged: enabled ? onChanged : null)
-                    .marginOnly(right: 5),
-                Offstage(
-                  offstage: !ref.value || checkedIcon == null,
-                  child: checkedIcon?.marginOnly(right: 5),
-                ),
-                Expanded(
-                    child: Text(
-                  translate(label),
-                  style: TextStyle(color: _disabledTextColor(context, enabled)),
-                ))
-              ],
-            ),
-          ).marginOnly(left: _kCheckBoxLeftMargin),
-          onTap: enabled
-              ? () {
-                  onChanged(!ref.value);
-                }
-              : null,
-        );
-      });
+        : null,
+  );
 }
 
 // ignore: non_constant_identifier_names
@@ -1821,13 +1684,10 @@ Widget _lock(
                             Text(translate(label)).marginOnly(left: 5),
                           ]).marginSymmetric(vertical: 2)),
                   onPressed: () async {
-                    bool checked = await bind.mainCheckSuperUserPermission();
+                    bool checked = await callMainCheckSuperUserPermission();
                     if (checked) {
                       onUnlock();
                     }
-                    if (Platform.isMacOS) {
-                      await windowManager.show();
-                    }
                   },
                 ).marginSymmetric(horizontal: 2, vertical: 4),
               ).marginOnly(left: _kCardLeftMargin),
@@ -2057,9 +1917,9 @@ void changeSocks5Proxy() async {
                 ),
               ],
             ),
-            Offstage(
-                offstage: !isInProgress,
-                child: const LinearProgressIndicator().marginOnly(top: 8))
+            // NOT use Offstage to wrap LinearProgressIndicator
+            if (isInProgress)
+              const LinearProgressIndicator().marginOnly(top: 8),
           ],
         ),
       ),
diff --git a/flutter/lib/desktop/pages/file_manager_page.dart b/flutter/lib/desktop/pages/file_manager_page.dart
index eae3f1d69..d684d1535 100644
--- a/flutter/lib/desktop/pages/file_manager_page.dart
+++ b/flutter/lib/desktop/pages/file_manager_page.dart
@@ -52,10 +52,12 @@ class FileManagerPage extends StatefulWidget {
   const FileManagerPage(
       {Key? key,
       required this.id,
+      required this.password,
       required this.tabController,
       this.forceRelay})
       : super(key: key);
   final String id;
+  final String? password;
   final bool? forceRelay;
   final DesktopTabController tabController;
 
@@ -78,8 +80,11 @@ class _FileManagerPageState extends State<FileManagerPage>
   @override
   void initState() {
     super.initState();
-    _ffi = FFI();
-    _ffi.start(widget.id, isFileTransfer: true, forceRelay: widget.forceRelay);
+    _ffi = FFI(null);
+    _ffi.start(widget.id,
+        isFileTransfer: true,
+        password: widget.password,
+        forceRelay: widget.forceRelay);
     WidgetsBinding.instance.addPostFrameCallback((_) {
       _ffi.dialogManager
           .showLoading(translate('Connecting...'), onCancel: closeConnection);
diff --git a/flutter/lib/desktop/pages/file_manager_tab_page.dart b/flutter/lib/desktop/pages/file_manager_tab_page.dart
index d41397833..1e349c6f0 100644
--- a/flutter/lib/desktop/pages/file_manager_tab_page.dart
+++ b/flutter/lib/desktop/pages/file_manager_tab_page.dart
@@ -44,6 +44,7 @@ class _FileManagerTabPageState extends State<FileManagerTabPage> {
         page: FileManagerPage(
           key: ValueKey(params['id']),
           id: params['id'],
+          password: params['password'],
           tabController: tabController,
           forceRelay: params['forceRelay'],
         )));
@@ -59,10 +60,10 @@ class _FileManagerTabPageState extends State<FileManagerTabPage> {
       print(
           "[FileTransfer] call ${call.method} with args ${call.arguments} from window $fromWindowId to ${windowId()}");
       // for simplify, just replace connectionId
-      if (call.method == "new_file_transfer") {
+      if (call.method == kWindowEventNewFileTransfer) {
         final args = jsonDecode(call.arguments);
         final id = args['id'];
-        window_on_top(windowId());
+        windowOnTop(windowId());
         tabController.add(TabInfo(
             key: id,
             label: id,
@@ -72,6 +73,7 @@ class _FileManagerTabPageState extends State<FileManagerTabPage> {
             page: FileManagerPage(
               key: ValueKey(id),
               id: id,
+              password: args['password'],
               tabController: tabController,
               forceRelay: args['forceRelay'],
             )));
@@ -127,7 +129,7 @@ class _FileManagerTabPageState extends State<FileManagerTabPage> {
     } else {
       final opt = "enable-confirm-closing-tabs";
       final bool res;
-      if (!option2bool(opt, await bind.mainGetOption(key: opt))) {
+      if (!option2bool(opt, bind.mainGetLocalOption(key: opt))) {
         res = true;
       } else {
         res = await closeConfirmDialog();
diff --git a/flutter/lib/desktop/pages/install_page.dart b/flutter/lib/desktop/pages/install_page.dart
index 74452c7ee..44ba06263 100644
--- a/flutter/lib/desktop/pages/install_page.dart
+++ b/flutter/lib/desktop/pages/install_page.dart
@@ -160,7 +160,7 @@ class _InstallPageBodyState extends State<_InstallPageBody>
               Option(desktopicon, label: 'Create desktop icon'),
               Offstage(
                 offstage: !Platform.isWindows,
-                child: Option(driverCert, label: 'idd_driver_tip'),
+                child: Option(driverCert, label: 'install_cert_tip'),
               ).marginOnly(top: 7),
               Container(
                   padding: EdgeInsets.all(12),
@@ -182,10 +182,10 @@ class _InstallPageBodyState extends State<_InstallPageBody>
                               .marginOnly(bottom: em),
                           InkWell(
                             hoverColor: Colors.transparent,
-                            onTap: () =>
-                                launchUrlString('https://rustdesk.com/privacy'),
+                            onTap: () => launchUrlString(
+                                'https://rustdesk.com/privacy.html'),
                             child: Tooltip(
-                              message: 'https://rustdesk.com/privacy',
+                              message: 'https://rustdesk.com/privacy.html',
                               child: Row(children: [
                                 Icon(Icons.launch_outlined, size: 16)
                                     .marginOnly(right: 5),
@@ -204,11 +204,10 @@ class _InstallPageBodyState extends State<_InstallPageBody>
               Row(
                 children: [
                   Expanded(
-                    child: Obx(() => Offstage(
-                          offstage: !showProgress.value,
-                          child:
-                              LinearProgressIndicator().marginOnly(right: 10),
-                        )),
+                    // NOT use Offstage to wrap LinearProgressIndicator
+                    child: Obx(() => showProgress.value
+                        ? LinearProgressIndicator().marginOnly(right: 10)
+                        : Offstage()),
                   ),
                   Obx(
                     () => OutlinedButton.icon(
@@ -282,7 +281,7 @@ class _InstallPageBodyState extends State<_InstallPageBody>
           title: null,
           content: SelectionArea(
               child:
-                  msgboxContent('info', 'Warning', 'confirm_idd_driver_tip')),
+                  msgboxContent('info', 'Warning', 'confirm_install_cert_tip')),
           actions: btns,
           onCancel: close,
         ),
diff --git a/flutter/lib/desktop/pages/port_forward_page.dart b/flutter/lib/desktop/pages/port_forward_page.dart
index 3a16ffbe1..2a173c53b 100644
--- a/flutter/lib/desktop/pages/port_forward_page.dart
+++ b/flutter/lib/desktop/pages/port_forward_page.dart
@@ -1,5 +1,4 @@
 import 'dart:convert';
-import 'dart:io';
 
 import 'package:flutter/material.dart';
 import 'package:flutter/services.dart';
@@ -8,7 +7,6 @@ import 'package:flutter_hbb/desktop/widgets/tabbar_widget.dart';
 import 'package:flutter_hbb/models/model.dart';
 import 'package:flutter_hbb/models/platform_model.dart';
 import 'package:get/get.dart';
-import 'package:wakelock/wakelock.dart';
 
 const double _kColumn1Width = 30;
 const double _kColumn4Width = 100;
@@ -30,11 +28,13 @@ class PortForwardPage extends StatefulWidget {
   const PortForwardPage(
       {Key? key,
       required this.id,
+      required this.password,
       required this.tabController,
       required this.isRDP,
       this.forceRelay})
       : super(key: key);
   final String id;
+  final String? password;
   final DesktopTabController tabController;
   final bool isRDP;
   final bool? forceRelay;
@@ -54,15 +54,13 @@ class _PortForwardPageState extends State<PortForwardPage>
   @override
   void initState() {
     super.initState();
-    _ffi = FFI();
+    _ffi = FFI(null);
     _ffi.start(widget.id,
         isPortForward: true,
+        password: widget.password,
         forceRelay: widget.forceRelay,
         isRdp: widget.isRDP);
     Get.put(_ffi, tag: 'pf_${widget.id}');
-    if (!Platform.isLinux) {
-      Wakelock.enable();
-    }
     debugPrint("Port forward page init success with id ${widget.id}");
     widget.tabController.onSelected?.call(widget.id);
   }
@@ -71,9 +69,6 @@ class _PortForwardPageState extends State<PortForwardPage>
   void dispose() {
     _ffi.close();
     _ffi.dialogManager.dismissAll();
-    if (!Platform.isLinux) {
-      Wakelock.disable();
-    }
     Get.delete<FFI>(tag: 'pf_${widget.id}');
     super.dispose();
   }
diff --git a/flutter/lib/desktop/pages/port_forward_tab_page.dart b/flutter/lib/desktop/pages/port_forward_tab_page.dart
index 751fc696c..621f393e0 100644
--- a/flutter/lib/desktop/pages/port_forward_tab_page.dart
+++ b/flutter/lib/desktop/pages/port_forward_tab_page.dart
@@ -43,6 +43,7 @@ class _PortForwardTabPageState extends State<PortForwardTabPage> {
         page: PortForwardPage(
           key: ValueKey(params['id']),
           id: params['id'],
+          password: params['password'],
           tabController: tabController,
           isRDP: isRDP,
           forceRelay: params['forceRelay'],
@@ -59,11 +60,11 @@ class _PortForwardTabPageState extends State<PortForwardTabPage> {
       debugPrint(
           "[Port Forward] call ${call.method} with args ${call.arguments} from window $fromWindowId");
       // for simplify, just replace connectionId
-      if (call.method == "new_port_forward") {
+      if (call.method == kWindowEventNewPortForward) {
         final args = jsonDecode(call.arguments);
         final id = args['id'];
         final isRDP = args['isRDP'];
-        window_on_top(windowId());
+        windowOnTop(windowId());
         if (tabController.state.value.tabs.indexWhere((e) => e.key == id) >=
             0) {
           debugPrint("port forward $id exists");
@@ -77,6 +78,7 @@ class _PortForwardTabPageState extends State<PortForwardTabPage> {
             page: PortForwardPage(
               key: ValueKey(args['id']),
               id: id,
+              password: args['password'],
               isRDP: isRDP,
               tabController: tabController,
               forceRelay: args['forceRelay'],
diff --git a/flutter/lib/desktop/pages/remote_page.dart b/flutter/lib/desktop/pages/remote_page.dart
index 849971a41..f265f1895 100644
--- a/flutter/lib/desktop/pages/remote_page.dart
+++ b/flutter/lib/desktop/pages/remote_page.dart
@@ -18,6 +18,7 @@ import '../../common/widgets/remote_input.dart';
 import '../../common.dart';
 import '../../common/widgets/dialog.dart';
 import '../../models/model.dart';
+import '../../models/desktop_render_texture.dart';
 import '../../models/platform_model.dart';
 import '../../common/shared_state.dart';
 import '../../utils/image.dart';
@@ -27,10 +28,14 @@ import '../widgets/tabbar_widget.dart';
 
 final SimpleWrapper<bool> _firstEnterImage = SimpleWrapper(false);
 
+final Map<String, bool> closeSessionOnDispose = {};
+
 class RemotePage extends StatefulWidget {
   RemotePage({
     Key? key,
     required this.id,
+    required this.sessionId,
+    required this.tabWindowId,
     required this.password,
     required this.toolbarState,
     required this.tabController,
@@ -39,6 +44,8 @@ class RemotePage extends StatefulWidget {
   }) : super(key: key);
 
   final String id;
+  final SessionID? sessionId;
+  final int? tabWindowId;
   final String? password;
   final ToolbarState toolbarState;
   final String? switchUuid;
@@ -66,9 +73,7 @@ class _RemotePageState extends State<RemotePage>
   late RxBool _zoomCursor;
   late RxBool _remoteCursorMoved;
   late RxBool _keyboardEnabled;
-  late RxInt _textureId;
-  late int _textureKey;
-  final useTextureRender = bind.mainUseTextureRender();
+  late RenderTexture _renderTexture;
 
   final _blockableOverlayState = BlockableOverlayState();
 
@@ -86,15 +91,13 @@ class _RemotePageState extends State<RemotePage>
     _showRemoteCursor = ShowRemoteCursorState.find(id);
     _keyboardEnabled = KeyboardEnabledState.find(id);
     _remoteCursorMoved = RemoteCursorMovedState.find(id);
-    _textureKey = newTextureId;
-    _textureId = RxInt(-1);
   }
 
   @override
   void initState() {
     super.initState();
     _initStates(widget.id);
-    _ffi = FFI();
+    _ffi = FFI(widget.sessionId);
     Get.put(_ffi, tag: widget.id);
     _ffi.imageModel.addCallbackOnFirstImage((String peerId) {
       showKBLayoutTypeChooserIfNeeded(
@@ -105,6 +108,7 @@ class _RemotePageState extends State<RemotePage>
       password: widget.password,
       switchUuid: widget.switchUuid,
       forceRelay: widget.forceRelay,
+      tabWindowId: widget.tabWindowId,
     );
     WidgetsBinding.instance.addPostFrameCallback((_) {
       SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, overlays: []);
@@ -115,17 +119,9 @@ class _RemotePageState extends State<RemotePage>
       Wakelock.enable();
     }
     // Register texture.
-    _textureId.value = -1;
-    if (useTextureRender) {
-      textureRenderer.createTexture(_textureKey).then((id) async {
-        debugPrint("id: $id, texture_key: $_textureKey");
-        if (id != -1) {
-          final ptr = await textureRenderer.getTexturePtr(_textureKey);
-          platformFFI.registerTexture(sessionId, ptr);
-          _textureId.value = id;
-        }
-      });
-    }
+    _renderTexture = RenderTexture();
+    _renderTexture.create(sessionId);
+
     _ffi.ffiModel.updateEventListener(sessionId, widget.id);
     bind.pluginSyncUi(syncTo: kAppTypeDesktopRemote);
     _ffi.qualityMonitorModel.checkShowQualityMonitor(sessionId);
@@ -206,26 +202,25 @@ class _RemotePageState extends State<RemotePage>
 
   @override
   Future<void> dispose() async {
+    final closeSession = closeSessionOnDispose.remove(widget.id) ?? true;
+
     // https://github.com/flutter/flutter/issues/64935
     super.dispose();
-    debugPrint("REMOTE PAGE dispose ${widget.id}");
-    if (useTextureRender) {
-      platformFFI.registerTexture(sessionId, 0);
-      // sleep for a while to avoid the texture is used after it's unregistered.
-      await Future.delayed(Duration(milliseconds: 100));
-      await textureRenderer.closeTexture(_textureKey);
-    }
+    debugPrint("REMOTE PAGE dispose session $sessionId ${widget.id}");
+    await _renderTexture.destroy(closeSession);
     // ensure we leave this session, this is a double check
     bind.sessionEnterOrLeave(sessionId: sessionId, enter: false);
     DesktopMultiWindow.removeListener(this);
     _ffi.dialogManager.hideMobileActionsOverlay();
     _ffi.recordingModel.onClose();
     _rawKeyFocusNode.dispose();
-    await _ffi.close();
+    await _ffi.close(closeSession: closeSession);
     _timer?.cancel();
     _ffi.dialogManager.dismissAll();
-    await SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual,
-        overlays: SystemUiOverlay.values);
+    if (closeSession) {
+      await SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual,
+          overlays: SystemUiOverlay.values);
+    }
     if (!Platform.isLinux) {
       await Wakelock.disable();
     }
@@ -233,49 +228,70 @@ class _RemotePageState extends State<RemotePage>
     removeSharedStates(widget.id);
   }
 
-  Widget buildBody(BuildContext context) {
-    return Scaffold(
-      backgroundColor: Theme.of(context).colorScheme.background,
-
-      /// the Overlay key will be set with _blockableOverlayState in BlockableOverlay
-      /// see override build() in [BlockableOverlay]
-      body: BlockableOverlay(
+  Widget emptyOverlay() => BlockableOverlay(
+        /// the Overlay key will be set with _blockableOverlayState in BlockableOverlay
+        /// see override build() in [BlockableOverlay]
         state: _blockableOverlayState,
         underlying: Container(
-            color: Colors.black,
-            child: RawKeyFocusScope(
-                focusNode: _rawKeyFocusNode,
-                onFocusChange: (bool imageFocused) {
-                  debugPrint(
-                      "onFocusChange(window active:${!_isWindowBlur}) $imageFocused");
-                  // See [onWindowBlur].
-                  if (Platform.isWindows) {
-                    if (_isWindowBlur) {
-                      imageFocused = false;
-                      Future.delayed(Duration.zero, () {
-                        _rawKeyFocusNode.unfocus();
-                      });
+          color: Colors.transparent,
+        ),
+      );
+
+  Widget buildBody(BuildContext context) {
+    remoteToolbar(BuildContext context) => RemoteToolbar(
+          id: widget.id,
+          ffi: _ffi,
+          state: widget.toolbarState,
+          onEnterOrLeaveImageSetter: (func) =>
+              _onEnterOrLeaveImage4Toolbar = func,
+          onEnterOrLeaveImageCleaner: () => _onEnterOrLeaveImage4Toolbar = null,
+        );
+    return Scaffold(
+      backgroundColor: Theme.of(context).colorScheme.background,
+      body: Stack(
+        children: [
+          Container(
+              color: Colors.black,
+              child: RawKeyFocusScope(
+                  focusNode: _rawKeyFocusNode,
+                  onFocusChange: (bool imageFocused) {
+                    debugPrint(
+                        "onFocusChange(window active:${!_isWindowBlur}) $imageFocused");
+                    // See [onWindowBlur].
+                    if (Platform.isWindows) {
+                      if (_isWindowBlur) {
+                        imageFocused = false;
+                        Future.delayed(Duration.zero, () {
+                          _rawKeyFocusNode.unfocus();
+                        });
+                      }
+                      if (imageFocused) {
+                        _ffi.inputModel.enterOrLeave(true);
+                      } else {
+                        _ffi.inputModel.enterOrLeave(false);
+                      }
                     }
-                    if (imageFocused) {
-                      _ffi.inputModel.enterOrLeave(true);
-                    } else {
-                      _ffi.inputModel.enterOrLeave(false);
-                    }
-                  }
-                },
-                inputModel: _ffi.inputModel,
-                child: getBodyForDesktop(context))),
-        upperLayer: [
-          OverlayEntry(
-              builder: (context) => RemoteToolbar(
-                    id: widget.id,
-                    ffi: _ffi,
-                    state: widget.toolbarState,
-                    onEnterOrLeaveImageSetter: (func) =>
-                        _onEnterOrLeaveImage4Toolbar = func,
-                    onEnterOrLeaveImageCleaner: () =>
-                        _onEnterOrLeaveImage4Toolbar = null,
-                  ))
+                  },
+                  inputModel: _ffi.inputModel,
+                  child: getBodyForDesktop(context))),
+          Obx(() => Stack(
+                children: [
+                  _ffi.ffiModel.pi.isSet.isTrue &&
+                          _ffi.ffiModel.waitForFirstImage.isTrue
+                      ? emptyOverlay()
+                      : () {
+                          _ffi.ffiModel.tryShowAndroidActionsOverlay();
+                          return Offstage();
+                        }(),
+                  // Use Overlay to enable rebuild every time on menu button click.
+                  _ffi.ffiModel.pi.isSet.isTrue
+                      ? Overlay(initialEntries: [
+                          OverlayEntry(builder: remoteToolbar)
+                        ])
+                      : remoteToolbar(context),
+                  _ffi.ffiModel.pi.isSet.isFalse ? emptyOverlay() : Offstage(),
+                ],
+              )),
         ],
       ),
     );
@@ -337,6 +353,17 @@ class _RemotePageState extends State<RemotePage>
     }
   }
 
+  Widget _buildRawTouchAndPointerRegion(
+    Widget child,
+    PointerEnterEventListener? onEnter,
+    PointerExitEventListener? onExit,
+  ) {
+    return RawTouchGestureDetectorRegion(
+      child: _buildRawPointerMouseRegion(child, onEnter, onExit),
+      ffi: _ffi,
+    );
+  }
+
   Widget _buildRawPointerMouseRegion(
     Widget child,
     PointerEnterEventListener? onEnter,
@@ -381,10 +408,10 @@ class _RemotePageState extends State<RemotePage>
           cursorOverImage: _cursorOverImage,
           keyboardEnabled: _keyboardEnabled,
           remoteCursorMoved: _remoteCursorMoved,
-          textureId: _textureId,
-          useTextureRender: useTextureRender,
+          textureId: _renderTexture.textureId,
+          useTextureRender: _renderTexture.useTextureRender,
           listenerBuilder: (child) =>
-              _buildRawPointerMouseRegion(child, enterView, leaveView),
+              _buildRawTouchAndPointerRegion(child, enterView, leaveView),
         );
       }))
     ];
@@ -401,7 +428,7 @@ class _RemotePageState extends State<RemotePage>
       Positioned(
         top: 10,
         right: 10,
-        child: _buildRawPointerMouseRegion(
+        child: _buildRawTouchAndPointerRegion(
             QualityMonitor(_ffi.qualityMonitorModel), null, null),
       ),
     );
diff --git a/flutter/lib/desktop/pages/remote_tab_page.dart b/flutter/lib/desktop/pages/remote_tab_page.dart
index 2333e343c..063fe49d8 100644
--- a/flutter/lib/desktop/pages/remote_tab_page.dart
+++ b/flutter/lib/desktop/pages/remote_tab_page.dart
@@ -1,10 +1,10 @@
 import 'dart:convert';
+import 'dart:async';
 import 'dart:io';
 import 'dart:ui' as ui;
 
 import 'package:desktop_multi_window/desktop_multi_window.dart';
 import 'package:flutter/material.dart';
-import 'package:flutter/services.dart';
 import 'package:flutter_hbb/common.dart';
 import 'package:flutter_hbb/common/shared_state.dart';
 import 'package:flutter_hbb/consts.dart';
@@ -20,6 +20,7 @@ import 'package:flutter_svg/flutter_svg.dart';
 import 'package:get/get.dart';
 import 'package:bot_toast/bot_toast.dart';
 
+import '../../common/widgets/dialog.dart';
 import '../../models/platform_model.dart';
 
 class _MenuTheme {
@@ -46,35 +47,39 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
   static const IconData unselectedIcon = Icons.desktop_windows_outlined;
 
   late ToolbarState _toolbarState;
+  String? peerId;
 
   var connectionMap = RxList<Widget>.empty(growable: true);
 
   _ConnectionTabPageState(Map<String, dynamic> params) {
     _toolbarState = ToolbarState();
     RemoteCountState.init();
-    final peerId = params['id'];
+    peerId = params['id'];
+    final sessionId = params['session_id'];
+    final tabWindowId = params['tab_window_id'];
     if (peerId != null) {
-      ConnectionTypeState.init(peerId);
+      ConnectionTypeState.init(peerId!);
       tabController.onSelected = (id) {
-        final remotePage = tabController.state.value.tabs
-            .firstWhereOrNull((tab) => tab.key == id)
-            ?.page;
+        final remotePage = tabController.widget(id);
         if (remotePage is RemotePage) {
           final ffi = remotePage.ffi;
           bind.setCurSessionId(sessionId: ffi.sessionId);
         }
         WindowController.fromWindowId(windowId())
             .setTitle(getWindowNameWithId(id));
+        UnreadChatCountState.find(id).value = 0;
       };
       tabController.add(TabInfo(
-        key: peerId,
-        label: peerId,
+        key: peerId!,
+        label: peerId!,
         selectedIcon: selectedIcon,
         unselectedIcon: unselectedIcon,
         onTabCloseButton: () => tabController.closeBy(peerId),
         page: RemotePage(
           key: ValueKey(peerId),
-          id: peerId,
+          id: peerId!,
+          sessionId: sessionId == null ? null : SessionID(sessionId),
+          tabWindowId: tabWindowId,
           password: params['password'],
           toolbarState: _toolbarState,
           tabController: tabController,
@@ -96,12 +101,20 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
       print(
           "[Remote Page] call ${call.method} with args ${call.arguments} from window $fromWindowId");
 
+      dynamic returnValue;
       // for simplify, just replace connectionId
-      if (call.method == "new_remote_desktop") {
+      if (call.method == kWindowEventNewRemoteDesktop) {
         final args = jsonDecode(call.arguments);
         final id = args['id'];
         final switchUuid = args['switch_uuid'];
-        window_on_top(windowId());
+        final sessionId = args['session_id'];
+        final tabWindowId = args['tab_window_id'];
+        windowOnTop(windowId());
+        if (tabController.length == 0) {
+          if (Platform.isMacOS && stateGlobal.closeOnFullscreen) {
+            stateGlobal.setFullscreen(true);
+          }
+        }
         ConnectionTypeState.init(id);
         _toolbarState.setShow(
             bind.mainGetUserDefaultOption(key: 'collapse_toolbar') != 'Y');
@@ -114,6 +127,8 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
           page: RemotePage(
             key: ValueKey(id),
             id: id,
+            sessionId: sessionId == null ? null : SessionID(sessionId),
+            tabWindowId: tabWindowId,
             password: args['password'],
             toolbarState: _toolbarState,
             tabController: tabController,
@@ -127,11 +142,49 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
         tabController.clear();
       } else if (call.method == kWindowActionRebuild) {
         reloadCurrentWindow();
+      } else if (call.method == kWindowEventActiveSession) {
+        final jumpOk = tabController.jumpToByKey(call.arguments);
+        if (jumpOk) {
+          windowOnTop(windowId());
+        }
+        return jumpOk;
+      } else if (call.method == kWindowEventGetRemoteList) {
+        return tabController.state.value.tabs
+            .map((e) => e.key)
+            .toList()
+            .join(',');
+      } else if (call.method == kWindowEventGetSessionIdList) {
+        return tabController.state.value.tabs
+            .map((e) => '${e.key},${(e.page as RemotePage).ffi.sessionId}')
+            .toList()
+            .join(';');
+      } else if (call.method == kWindowEventGetCachedSessionData) {
+        // Ready to show new window and close old tab.
+        final peerId = call.arguments;
+        try {
+          final remotePage = tabController.state.value.tabs
+              .firstWhere((tab) => tab.key == peerId)
+              .page as RemotePage;
+          returnValue = remotePage.ffi.ffiModel.cachedPeerData.toString();
+        } catch (e) {
+          debugPrint('Failed to get cached session data: $e');
+        }
+        if (returnValue != null) {
+          closeSessionOnDispose[peerId] = false;
+          tabController.closeBy(peerId);
+        }
       }
       _update_remote_count();
+      return returnValue;
     });
     Future.delayed(Duration.zero, () {
-      restoreWindowPosition(WindowType.RemoteDesktop, windowId: windowId());
+      restoreWindowPosition(
+        WindowType.RemoteDesktop,
+        windowId: windowId(),
+        peerId: tabController.state.value.tabs.isEmpty
+            ? null
+            : tabController.state.value.tabs[0].key,
+      );
     });
   }
 
@@ -173,7 +226,7 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
                     connectionType.secure.value == ConnectionType.strSecure;
                 bool direct =
                     connectionType.direct.value == ConnectionType.strDirect;
-                var msgConn;
+                String msgConn;
                 if (secure && direct) {
                   msgConn = translate("Direct and encrypted connection");
                 } else if (secure && !direct) {
@@ -185,6 +238,9 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
                 }
                 var msgFingerprint = '${translate('Fingerprint')}:\n';
                 var fingerprint = FingerprintState.find(key).value;
+                if (fingerprint.isEmpty) {
+                  fingerprint = 'N/A';
+                }
                 if (fingerprint.length > 5 * 8) {
                   var first = fingerprint.substring(0, 39);
                   var second = fingerprint.substring(40);
@@ -206,6 +262,8 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
                       ).paddingOnly(right: 5),
                     ),
                     label,
+                    unreadMessageCountBuilder(UnreadChatCountState.find(key))
+                        .marginOnly(left: 4),
                   ],
                 );
 
@@ -214,7 +272,11 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
                     if (e.kind != ui.PointerDeviceKind.mouse) {
                       return;
                     }
-                    if (e.buttons == 2) {
+                    final remotePage = tabController.state.value.tabs
+                        .firstWhere((tab) => tab.key == key)
+                        .page as RemotePage;
+                    if (remotePage.ffi.ffiModel.pi.isSet.isTrue &&
+                        e.buttons == 2) {
                       showRightMenu(
                         (CancelFunc cancelFunc) {
                           return _tabMenuBuilder(key, cancelFunc);
@@ -255,17 +317,6 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
     final perms = ffi.ffiModel.permissions;
     final sessionId = ffi.sessionId;
     menu.addAll([
-      MenuEntryButton<String>(
-        childBuilder: (TextStyle? style) => Text(
-          translate('Close'),
-          style: style,
-        ),
-        proc: () {
-          tabController.closeBy(key);
-          cancelFunc();
-        },
-        padding: padding,
-      ),
       MenuEntryButton<String>(
         childBuilder: (TextStyle? style) => Obx(() => Text(
               translate(
@@ -278,33 +329,42 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
         },
         padding: padding,
       ),
-      MenuEntryDivider<String>(),
-      RemoteMenuEntry.viewStyle(
-        key,
-        ffi,
-        padding,
-        dismissFunc: cancelFunc,
-      ),
     ]);
 
-    if (!ffi.canvasModel.cursorEmbedded &&
-        !ffi.ffiModel.viewOnly &&
-        !pi.is_wayland) {
-      menu.add(MenuEntryDivider<String>());
-      menu.add(RemoteMenuEntry.showRemoteCursor(
-        key,
-        sessionId,
-        padding,
-        dismissFunc: cancelFunc,
+    if (tabController.state.value.tabs.length > 1) {
+      final splitAction = MenuEntryButton<String>(
+        childBuilder: (TextStyle? style) => Text(
+          translate('Move tab to new window'),
+          style: style,
+        ),
+        proc: () async {
+          await DesktopMultiWindow.invokeMethod(kMainWindowId,
+              kWindowEventMoveTabToNewWindow, '${windowId()},$key,$sessionId');
+          cancelFunc();
+        },
+        padding: padding,
+      );
+      menu.insert(1, splitAction);
+    }
+
+    if (perms['restart'] != false &&
+        (pi.platform == kPeerPlatformLinux ||
+            pi.platform == kPeerPlatformWindows ||
+            pi.platform == kPeerPlatformMacOS)) {
+      menu.add(MenuEntryButton<String>(
+        childBuilder: (TextStyle? style) => Text(
+          translate('Restart Remote Device'),
+          style: style,
+        ),
+        proc: () => showRestartRemoteDevice(
+            pi, peerId ?? '', sessionId, ffi.dialogManager),
+        padding: padding,
+        dismissOnClicked: true,
+        dismissCallback: cancelFunc,
       ));
     }
 
     if (perms['keyboard'] != false && !ffi.ffiModel.viewOnly) {
-      if (perms['clipboard'] != false) {
-        menu.add(RemoteMenuEntry.disableClipboard(sessionId, padding,
-            dismissFunc: cancelFunc));
-      }
-
       menu.add(RemoteMenuEntry.insertLock(sessionId, padding,
           dismissFunc: cancelFunc));
 
@@ -314,16 +374,30 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
       }
     }
 
-    menu.add(MenuEntryButton<String>(
-      childBuilder: (TextStyle? style) => Text(
-        translate('Copy Fingerprint'),
-        style: style,
+    menu.addAll([
+      MenuEntryDivider<String>(),
+      MenuEntryButton<String>(
+        childBuilder: (TextStyle? style) => Text(
+          translate('Copy Fingerprint'),
+          style: style,
+        ),
+        proc: () => onCopyFingerprint(FingerprintState.find(key).value),
+        padding: padding,
+        dismissOnClicked: true,
+        dismissCallback: cancelFunc,
       ),
-      proc: () => onCopyFingerprint(FingerprintState.find(key).value),
-      padding: padding,
-      dismissOnClicked: true,
-      dismissCallback: cancelFunc,
-    ));
+      MenuEntryButton<String>(
+        childBuilder: (TextStyle? style) => Text(
+          translate('Close'),
+          style: style,
+        ),
+        proc: () {
+          tabController.closeBy(key);
+          cancelFunc();
+        },
+        padding: padding,
+      )
+    ]);
 
     return mod_menu.PopupMenu<String>(
       items: menu
@@ -342,6 +416,7 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
   void onRemoveId(String id) async {
     if (tabController.state.value.tabs.isEmpty) {
       await WindowController.fromWindowId(windowId()).close();
+      stateGlobal.setFullscreen(false, procWnd: false);
     }
     ConnectionTypeState.delete(id);
     _update_remote_count();
@@ -359,7 +434,7 @@ class _ConnectionTabPageState extends State<ConnectionTabPage> {
     } else {
       final opt = "enable-confirm-closing-tabs";
       final bool res;
-      if (!option2bool(opt, await bind.mainGetOption(key: opt))) {
+      if (!option2bool(opt, bind.mainGetLocalOption(key: opt))) {
         res = true;
       } else {
         res = await closeConfirmDialog();
diff --git a/flutter/lib/desktop/pages/server_page.dart b/flutter/lib/desktop/pages/server_page.dart
index 3ea735d25..2ef04b244 100644
--- a/flutter/lib/desktop/pages/server_page.dart
+++ b/flutter/lib/desktop/pages/server_page.dart
@@ -2,6 +2,7 @@
 
 import 'dart:async';
 import 'dart:io';
+import 'dart:math';
 
 import 'package:flutter/material.dart';
 import 'package:flutter_hbb/consts.dart';
@@ -9,12 +10,14 @@ import 'package:flutter_hbb/desktop/widgets/tabbar_widget.dart';
 import 'package:flutter_hbb/models/chat_model.dart';
 import 'package:flutter_hbb/utils/platform_channel.dart';
 import 'package:get/get.dart';
+import 'package:percent_indicator/linear_percent_indicator.dart';
 import 'package:provider/provider.dart';
 import 'package:window_manager/window_manager.dart';
 import 'package:flutter_svg/flutter_svg.dart';
 
 import '../../common.dart';
 import '../../common/widgets/chat_page.dart';
+import '../../models/file_model.dart';
 import '../../models/platform_model.dart';
 import '../../models/server_model.dart';
 
@@ -32,6 +35,7 @@ class _DesktopServerPageState extends State<DesktopServerPage>
   void initState() {
     gFFI.ffiModel.updateEventListener(gFFI.sessionId, "");
     windowManager.addListener(this);
+    Get.put(tabController);
     tabController.onRemoved = (_, id) {
       onRemoveId(id);
     };
@@ -100,11 +104,18 @@ class ConnectionManagerState extends State<ConnectionManager> {
     gFFI.serverModel.tabController.onSelected = (client_id_str) {
       final client_id = int.tryParse(client_id_str);
       if (client_id != null) {
-        gFFI.chatModel.changeCurrentID(client_id);
         final client =
             gFFI.serverModel.clients.firstWhereOrNull((e) => e.id == client_id);
         if (client != null) {
+          gFFI.chatModel.changeCurrentKey(MessageKey(client.peerId, client.id));
+          if (client.unreadChatMessageCount.value > 0) {
+            Future.delayed(Duration.zero, () {
+              client.unreadChatMessageCount.value = 0;
+              gFFI.chatModel.showChatPage(MessageKey(client.peerId, client.id));
+            });
+          }
           windowManager.setTitle(getWindowNameWithId(client.peerId));
+          gFFI.cmFileModel.updateCurrentClientId(client.id);
         }
       }
     };
@@ -144,10 +155,11 @@ class ConnectionManagerState extends State<ConnectionManager> {
               showClose: true,
               onWindowCloseButton: handleWindowCloseButton,
               controller: serverModel.tabController,
+              selectedBorderColor: MyTheme.accent,
               maxLabelWidth: 100,
               tail: buildScrollJumper(),
               selectedTabBackgroundColor:
-                  Theme.of(context).hintColor.withOpacity(0.2),
+                  Theme.of(context).hintColor.withOpacity(0),
               tabBuilder: (key, icon, label, themeConf) {
                 final client = serverModel.clients
                     .firstWhereOrNull((client) => client.id.toString() == key);
@@ -158,19 +170,25 @@ class ConnectionManagerState extends State<ConnectionManager> {
                         message: key,
                         waitDuration: Duration(seconds: 1),
                         child: label),
-                    Obx(() => Offstage(
-                        offstage:
-                            !(client?.hasUnreadChatMessage.value ?? false),
-                        child: Icon(Icons.circle, color: Colors.red, size: 10)))
+                    unreadMessageCountBuilder(client?.unreadChatMessageCount)
+                        .marginOnly(left: 4),
                   ],
                 );
               },
               pageViewBuilder: (pageView) => Row(
                 children: [
                   Consumer<ChatModel>(
-                    builder: (_, model, child) => model.isShowCMChatPage
+                    builder: (_, model, child) => model.isShowCMSidePage
                         ? Expanded(
-                            child: ChatPage(),
+                            child: buildRemoteBlock(
+                              child: Container(
+                                  decoration: BoxDecoration(
+                                      border: Border(
+                                          right: BorderSide(
+                                              color: Theme.of(context)
+                                                  .dividerColor))),
+                                  child: buildSidePage()),
+                            ),
                             flex: (kConnectionManagerWindowSizeOpenChat.width -
                                     kConnectionManagerWindowSizeClosedChat
                                         .width)
@@ -190,6 +208,19 @@ class ConnectionManagerState extends State<ConnectionManager> {
           );
   }
 
+  Widget buildSidePage() {
+    final selected = gFFI.serverModel.tabController.state.value.selected;
+    if (selected < 0 || selected >= gFFI.serverModel.clients.length) {
+      return Offstage();
+    }
+    final clientType = gFFI.serverModel.clients[selected].type_();
+    if (clientType == ClientType.file) {
+      return _FileTransferLogPage();
+    } else {
+      return ChatPage(type: ChatPageType.desktopCM);
+    }
+  }
+
   Widget buildTitleBar() {
     return SizedBox(
       height: kDesktopRemoteTabBarHeight,
@@ -240,7 +271,7 @@ class ConnectionManagerState extends State<ConnectionManager> {
     } else {
       final opt = "enable-confirm-closing-tabs";
       final bool res;
-      if (!option2bool(opt, await bind.mainGetOption(key: opt))) {
+      if (!option2bool(opt, bind.mainGetLocalOption(key: opt))) {
         res = true;
       } else {
         res = await closeConfirmDialog();
@@ -433,13 +464,21 @@ class _CmHeaderState extends State<_CmHeader>
             ),
           ),
           Offstage(
-            offstage: !client.authorized || client.type_() != ClientType.remote,
+            offstage: !client.authorized ||
+                (client.type_() != ClientType.remote &&
+                    client.type_() != ClientType.file),
             child: IconButton(
-              onPressed: () => checkClickTime(
-                client.id,
-                () => gFFI.chatModel.toggleCMChatPage(client.id),
-              ),
-              icon: SvgPicture.asset('assets/chat2.svg'),
+              onPressed: () => checkClickTime(client.id, () {
+                if (client.type_() != ClientType.file) {
+                  gFFI.chatModel.toggleCMSidePage();
+                } else {
+                  gFFI.chatModel
+                      .toggleCMChatPage(MessageKey(client.peerId, client.id));
+                }
+              }),
+              icon: SvgPicture.asset(client.type_() == ClientType.file
+                  ? 'assets/file_transfer.svg'
+                  : 'assets/chat2.svg'),
               splashRadius: kDesktopIconButtonSplashRadius,
             ),
           )
@@ -897,3 +936,182 @@ void checkClickTime(int id, Function() callback) async {
     if (d > 120) callback();
   });
 }
+
+class _FileTransferLogPage extends StatefulWidget {
+  _FileTransferLogPage({Key? key}) : super(key: key);
+
+  @override
+  State<_FileTransferLogPage> createState() => __FileTransferLogPageState();
+}
+
+class __FileTransferLogPageState extends State<_FileTransferLogPage> {
+  @override
+  Widget build(BuildContext context) {
+    return statusList();
+  }
+
+  Widget generateCard(Widget child) {
+    return Container(
+      decoration: BoxDecoration(
+        color: Theme.of(context).cardColor,
+        borderRadius: BorderRadius.all(
+          Radius.circular(15.0),
+        ),
+      ),
+      child: child,
+    );
+  }
+
+  Widget statusList() {
+    return PreferredSize(
+      preferredSize: const Size(200, double.infinity),
+      child: Container(
+          margin: const EdgeInsets.only(top: 16.0, bottom: 16.0, right: 16.0),
+          padding: const EdgeInsets.all(8.0),
+          child: Obx(
+            () {
+              final jobTable = gFFI.cmFileModel.currentJobTable;
+              statusListView(List<JobProgress> jobs) => ListView.builder(
+                    controller: ScrollController(),
+                    itemBuilder: (BuildContext context, int index) {
+                      final item = jobs[index];
+                      return Padding(
+                        padding: const EdgeInsets.only(bottom: 5),
+                        child: generateCard(
+                          Column(
+                            mainAxisSize: MainAxisSize.min,
+                            children: [
+                              Row(
+                                crossAxisAlignment: CrossAxisAlignment.center,
+                                children: [
+                                  SizedBox(
+                                    width: 50,
+                                    child: Column(
+                                      children: [
+                                        Transform.rotate(
+                                          angle: item.isRemoteToLocal ? 0 : pi,
+                                          child: SvgPicture.asset(
+                                            "assets/arrow.svg",
+                                            color: Theme.of(context)
+                                                .tabBarTheme
+                                                .labelColor,
+                                          ),
+                                        ),
+                                        Text(item.isRemoteToLocal
+                                            ? translate('Send')
+                                            : translate('Receive'))
+                                      ],
+                                    ),
+                                  ).paddingOnly(left: 15),
+                                  const SizedBox(
+                                    width: 16.0,
+                                  ),
+                                  Expanded(
+                                    child: Column(
+                                      mainAxisSize: MainAxisSize.min,
+                                      crossAxisAlignment:
+                                          CrossAxisAlignment.start,
+                                      children: [
+                                        Text(
+                                          item.fileName,
+                                        ).paddingSymmetric(vertical: 10),
+                                        if (item.totalSize > 0)
+                                          Text(
+                                            '${translate("Total")} ${readableFileSize(item.totalSize.toDouble())}',
+                                            style: TextStyle(
+                                              fontSize: 12,
+                                              color: MyTheme.darkGray,
+                                            ),
+                                          ),
+                                        if (item.totalSize > 0)
+                                          Offstage(
+                                            offstage: item.state !=
+                                                JobState.inProgress,
+                                            child: Text(
+                                              '${translate("Speed")} ${readableFileSize(item.speed)}/s',
+                                              style: TextStyle(
+                                                fontSize: 12,
+                                                color: MyTheme.darkGray,
+                                              ),
+                                            ),
+                                          ),
+                                        Offstage(
+                                          offstage:
+                                              item.state == JobState.inProgress,
+                                          child: Text(
+                                            translate(
+                                              item.display(),
+                                            ),
+                                            style: TextStyle(
+                                              fontSize: 12,
+                                              color: MyTheme.darkGray,
+                                            ),
+                                          ),
+                                        ),
+                                        if (item.totalSize > 0)
+                                          Offstage(
+                                            offstage: item.state !=
+                                                JobState.inProgress,
+                                            child: LinearPercentIndicator(
+                                              padding:
+                                                  EdgeInsets.only(right: 15),
+                                              animateFromLastPercent: true,
+                                              center: Text(
+                                                '${(item.finishedSize / item.totalSize * 100).toStringAsFixed(0)}%',
+                                              ),
+                                              barRadius: Radius.circular(15),
+                                              percent: item.finishedSize /
+                                                  item.totalSize,
+                                              progressColor: MyTheme.accent,
+                                              backgroundColor:
+                                                  Theme.of(context).hoverColor,
+                                              lineHeight:
+                                                  kDesktopFileTransferRowHeight,
+                                            ).paddingSymmetric(vertical: 15),
+                                          ),
+                                      ],
+                                    ),
+                                  ),
+                                  Row(
+                                    mainAxisAlignment: MainAxisAlignment.end,
+                                    children: [],
+                                  ),
+                                ],
+                              ),
+                            ],
+                          ).paddingSymmetric(vertical: 10),
+                        ),
+                      );
+                    },
+                    itemCount: jobTable.length,
+                  );
+
+              return jobTable.isEmpty
+                  ? generateCard(
+                      Center(
+                        child: Column(
+                          mainAxisAlignment: MainAxisAlignment.center,
+                          children: [
+                            SvgPicture.asset(
+                              "assets/transfer.svg",
+                              color: Theme.of(context).tabBarTheme.labelColor,
+                              height: 40,
+                            ).paddingOnly(bottom: 10),
+                            Text(
+                              translate("No transfers in progress"),
+                              textAlign: TextAlign.center,
+                              textScaleFactor: 1.20,
+                              style: TextStyle(
+                                  color:
+                                      Theme.of(context).tabBarTheme.labelColor),
+                            ),
+                          ],
+                        ),
+                      ),
+                    )
+                  : statusListView(jobTable);
+            },
+          )),
+    );
+  }
+}
diff --git a/flutter/lib/desktop/widgets/remote_toolbar.dart b/flutter/lib/desktop/widgets/remote_toolbar.dart
index f332f31dd..b59ae3736 100644
--- a/flutter/lib/desktop/widgets/remote_toolbar.dart
+++ b/flutter/lib/desktop/widgets/remote_toolbar.dart
@@ -1,5 +1,6 @@
 import 'dart:convert';
-import 'dart:ui' as ui;
+import 'dart:async';
+import 'dart:io';
 
 import 'package:flutter/material.dart';
 import 'package:flutter/services.dart';
@@ -35,7 +36,7 @@ class ToolbarState {
   late RxBool _pin;
 
   ToolbarState() {
-    final s = bind.getLocalFlutterConfig(k: kStoreKey);
+    final s = bind.getLocalFlutterOption(k: kStoreKey);
     if (s.isEmpty) {
       _initSet(false, false);
       return;
@@ -88,7 +89,7 @@ class ToolbarState {
   }
 
   _savePin() async {
-    bind.setLocalFlutterConfig(
+    bind.setLocalFlutterOption(
         k: kStoreKey, v: jsonEncode({'pin': _pin.value}));
   }
 
@@ -100,6 +101,9 @@ class ToolbarState {
 class _ToolbarTheme {
   static const Color blueColor = MyTheme.button;
   static const Color hoverBlueColor = MyTheme.accent;
+  static Color inactiveColor =  Colors.grey[800]!;
+  static Color hoverInactiveColor = Colors.grey[850]!;
+
   static const Color redColor = Colors.redAccent;
   static const Color hoverRedColor = Colors.red;
   // kMinInteractiveDimension
@@ -111,6 +115,36 @@ class _ToolbarTheme {
   static const double buttonVMargin = 6;
   static const double iconRadius = 8;
   static const double elevation = 3;
+
+  static const Color bordDark = MyTheme.bordDark;
+  static const Color bordLight = MyTheme.bordLight;
+
+  static const Color dividerDark = MyTheme.dividerDark;
+  static const Color dividerLight = MyTheme.dividerLight;
+  static double dividerSpaceToAction = Platform.isWindows ? 8 : 14;
+
+  static double menuBorderRadius = Platform.isWindows ? 5.0 : 7.0;
+  static EdgeInsets menuPadding = Platform.isWindows
+      ? EdgeInsets.fromLTRB(4, 12, 4, 12)
+      : EdgeInsets.fromLTRB(6, 14, 6, 14);
+  static const double menuButtonBorderRadius = 3.0;
+
+  static final defaultMenuStyle = MenuStyle(
+    side: MaterialStateProperty.all(BorderSide(
+      width: 1,
+      color: MyTheme.currentThemeMode() == ThemeMode.light
+          ? _ToolbarTheme.bordLight
+          : _ToolbarTheme.bordDark,
+    )),
+    shape: MaterialStatePropertyAll(RoundedRectangleBorder(
+        borderRadius: BorderRadius.circular(_ToolbarTheme.menuBorderRadius))),
+    padding: MaterialStateProperty.all(_ToolbarTheme.menuPadding),
+  );
+  static final defaultMenuButtonStyle = ButtonStyle(
+    backgroundColor: MaterialStatePropertyAll(Colors.transparent),
+    padding: MaterialStatePropertyAll(EdgeInsets.zero),
+    overlayColor: MaterialStatePropertyAll(Colors.transparent),
+  );
 }
 
 typedef DismissFunc = void Function();
@@ -328,6 +362,9 @@ class _RemoteToolbarState extends State<RemoteToolbar> {
 
   triggerAutoHide() => _debouncerHide.value = _debouncerHide.value + 1;
 
+  void _minimize() async =>
+      await WindowController.fromWindowId(windowId).minimize();
+
   @override
   initState() {
     super.initState();
@@ -397,6 +434,8 @@ class _RemoteToolbarState extends State<RemoteToolbar> {
               dragging: _dragging,
               fractionX: _fractionX,
               show: show,
+              setFullscreen: _setFullscreen,
+              setMinimize: _minimize,
             ),
           ),
         ),
@@ -408,8 +447,6 @@ class _RemoteToolbarState extends State<RemoteToolbar> {
     final List<Widget> toolbarItems = [];
     if (!isWebDesktop) {
       toolbarItems.add(_PinMenu(state: widget.state));
-      toolbarItems.add(
-          _FullscreenMenu(state: widget.state, setFullscreen: _setFullscreen));
       toolbarItems.add(_MobileActionMenu(ffi: widget.ffi));
     }
 
@@ -475,9 +512,17 @@ class _RemoteToolbarState extends State<RemoteToolbar> {
           textStyle: MaterialStatePropertyAll(
             TextStyle(fontWeight: FontWeight.normal),
           ),
+          shape: MaterialStatePropertyAll(RoundedRectangleBorder(
+              borderRadius:
+                  BorderRadius.circular(_ToolbarTheme.menuButtonBorderRadius))),
         ),
       ),
-      dividerTheme: DividerThemeData(space: 4),
+      dividerTheme: DividerThemeData(
+        space: _ToolbarTheme.dividerSpaceToAction,
+        color: MyTheme.currentThemeMode() == ThemeMode.light
+            ? _ToolbarTheme.dividerLight
+            : _ToolbarTheme.dividerDark,
+      ),
       menuBarTheme: MenuBarThemeData(
           style: MenuStyle(
         padding: MaterialStatePropertyAll(EdgeInsets.zero),
@@ -501,35 +546,14 @@ class _PinMenu extends StatelessWidget {
         assetName: state.pin ? "assets/pinned.svg" : "assets/unpinned.svg",
         tooltip: state.pin ? 'Unpin Toolbar' : 'Pin Toolbar',
         onPressed: state.switchPin,
-        color: state.pin ? _ToolbarTheme.blueColor : Colors.grey[800]!,
+        color: state.pin ? _ToolbarTheme.blueColor : _ToolbarTheme.inactiveColor,
         hoverColor:
-            state.pin ? _ToolbarTheme.hoverBlueColor : Colors.grey[850]!,
+            state.pin ? _ToolbarTheme.hoverBlueColor : _ToolbarTheme.hoverInactiveColor,
       ),
     );
   }
 }
 
-class _FullscreenMenu extends StatelessWidget {
-  final ToolbarState state;
-  final Function(bool) setFullscreen;
-  bool get isFullscreen => stateGlobal.fullscreen;
-  const _FullscreenMenu(
-      {Key? key, required this.state, required this.setFullscreen})
-      : super(key: key);
-
-  @override
-  Widget build(BuildContext context) {
-    return _IconMenuButton(
-      assetName:
-          isFullscreen ? "assets/fullscreen_exit.svg" : "assets/fullscreen.svg",
-      tooltip: isFullscreen ? 'Exit Fullscreen' : 'Fullscreen',
-      onPressed: () => setFullscreen(!isFullscreen),
-      color: _ToolbarTheme.blueColor,
-      hoverColor: _ToolbarTheme.hoverBlueColor,
-    );
-  }
-}
-
 class _MobileActionMenu extends StatelessWidget {
   final FFI ffi;
   const _MobileActionMenu({Key? key, required this.ffi}) : super(key: key);
@@ -537,13 +561,15 @@ class _MobileActionMenu extends StatelessWidget {
   @override
   Widget build(BuildContext context) {
     if (!ffi.ffiModel.isPeerAndroid) return Offstage();
-    return _IconMenuButton(
+    return Obx(()=>_IconMenuButton(
       assetName: 'assets/actions_mobile.svg',
       tooltip: 'Mobile Actions',
       onPressed: () => ffi.dialogManager.toggleMobileActionsOverlay(ffi: ffi),
-      color: _ToolbarTheme.blueColor,
-      hoverColor: _ToolbarTheme.hoverBlueColor,
-    );
+      color: ffi.dialogManager.mobileActionsOverlayVisible.isTrue
+          ? _ToolbarTheme.blueColor : _ToolbarTheme.inactiveColor,
+      hoverColor: ffi.dialogManager.mobileActionsOverlayVisible.isTrue
+          ? _ToolbarTheme.hoverBlueColor : _ToolbarTheme.hoverInactiveColor,
+    ));
   }
 }
 
@@ -574,7 +600,7 @@ class _MonitorMenu extends StatelessWidget {
       children: [
         SvgPicture.asset(
           "assets/screen.svg",
-          color: Colors.white,
+          colorFilter: ColorFilter.mode(Colors.white, BlendMode.srcIn),
         ),
         Obx(() {
           RxInt display = CurrentDisplayState.find(id);
@@ -599,7 +625,7 @@ class _MonitorMenu extends StatelessWidget {
         topLevel: false,
         color: _ToolbarTheme.blueColor,
         hoverColor: _ToolbarTheme.hoverBlueColor,
-        tooltip: "",
+        tooltip: "#${i + 1} monitor",
         hMargin: 6,
         vMargin: 12,
         icon: Container(
@@ -610,7 +636,7 @@ class _MonitorMenu extends StatelessWidget {
             children: [
               SvgPicture.asset(
                 "assets/screen.svg",
-                color: Colors.white,
+                colorFilter: ColorFilter.mode(Colors.white, BlendMode.srcIn),
               ),
               Text(
                 (i + 1).toString(),
@@ -681,7 +707,7 @@ class ScreenAdjustor {
   bool get isFullscreen => stateGlobal.fullscreen;
   int get windowId => stateGlobal.windowId;
 
-  adjustWindow() {
+  adjustWindow(BuildContext context) {
     return futureBuilder(
         future: isWindowCanBeAdjusted(),
         hasData: (data) {
@@ -691,7 +717,7 @@ class ScreenAdjustor {
             children: [
               MenuButton(
                   child: Text(translate('Adjust Window')),
-                  onPressed: doAdjustWindow,
+                  onPressed: () => doAdjustWindow(context),
                   ffi: ffi),
               Divider(),
             ],
@@ -699,20 +725,19 @@ class ScreenAdjustor {
         });
   }
 
-  doAdjustWindow() async {
+  doAdjustWindow(BuildContext context) async {
     await updateScreen();
     if (_screen != null) {
       cbExitFullscreen();
       double scale = _screen!.scaleFactor;
       final wndRect = await WindowController.fromWindowId(windowId).getFrame();
-      final mediaSize = MediaQueryData.fromWindow(ui.window).size;
+      final mediaSize = MediaQueryData.fromView(View.of(context)).size;
       // On windows, wndRect is equal to GetWindowRect and mediaSize is equal to GetClientRect.
       // https://stackoverflow.com/a/7561083
       double magicWidth =
           wndRect.right - wndRect.left - mediaSize.width * scale;
       double magicHeight =
           wndRect.bottom - wndRect.top - mediaSize.height * scale;
-
       final canvasModel = ffi.canvasModel;
       final width = (canvasModel.getDisplayWidth() * canvasModel.scale +
                   CanvasModel.leftToEdge +
@@ -751,7 +776,7 @@ class ScreenAdjustor {
   updateScreen() async {
     final v = await rustDeskWinManager.call(
         WindowType.Main, kWindowGetWindowInfo, '');
-    final String valueStr = v;
+    final String valueStr = v.result;
     if (valueStr.isEmpty) {
       _screen = null;
     } else {
@@ -855,7 +880,7 @@ class _DisplayMenuState extends State<_DisplayMenu> {
         color: _ToolbarTheme.blueColor,
         hoverColor: _ToolbarTheme.hoverBlueColor,
         menuChildren: [
-          _screenAdjustor.adjustWindow(),
+          _screenAdjustor.adjustWindow(context),
           viewStyle(),
           scrollStyle(),
           imageQuality(),
@@ -1042,9 +1067,9 @@ class _ResolutionsMenuState extends State<_ResolutionsMenu> {
     return _SubmenuButton(
       ffi: widget.ffi,
       menuChildren: <Widget>[
-            _OriginalResolutionMenuButton(showOriginalBtn),
-            _FitLocalResolutionMenuButton(showFitLocalBtn),
-            _customResolutionMenuButton(isVirtualDisplay),
+            _OriginalResolutionMenuButton(context, showOriginalBtn),
+            _FitLocalResolutionMenuButton(context, showFitLocalBtn),
+            _customResolutionMenuButton(context, isVirtualDisplay),
             _menuDivider(showOriginalBtn, showFitLocalBtn, isVirtualDisplay),
           ] +
           _supportedResolutionMenuButtons(),
@@ -1085,7 +1110,7 @@ class _ResolutionsMenuState extends State<_ResolutionsMenu> {
     }
   }
 
-  _onChanged(String? value) async {
+  _onChanged(BuildContext context, String? value) async {
     stateGlobal.setLastResolutionGroupValue(
         widget.id, pi.currentDisplay, value);
     if (value == null) return;
@@ -1105,12 +1130,12 @@ class _ResolutionsMenuState extends State<_ResolutionsMenu> {
 
     if (w != null && h != null) {
       if (w != display.width || h != display.height) {
-        await _changeResolution(w, h);
+        await _changeResolution(context, w, h);
       }
     }
   }
 
-  _changeResolution(int w, int h) async {
+  _changeResolution(BuildContext context, int w, int h) async {
     await bind.sessionChangeResolution(
       sessionId: ffi.sessionId,
       display: pi.currentDisplay,
@@ -1121,18 +1146,19 @@ class _ResolutionsMenuState extends State<_ResolutionsMenu> {
       final display = ffiModel.display;
       if (w == display.width && h == display.height) {
         if (await widget.screenAdjustor.isWindowCanBeAdjusted()) {
-          widget.screenAdjustor.doAdjustWindow();
+          widget.screenAdjustor.doAdjustWindow(context);
         }
       }
     });
   }
 
-  Widget _OriginalResolutionMenuButton(bool showOriginalBtn) {
+  Widget _OriginalResolutionMenuButton(
+      BuildContext context, bool showOriginalBtn) {
     return Offstage(
       offstage: !showOriginalBtn,
       child: MenuButton(
-        onPressed: () =>
-            _changeResolution(display.originalWidth, display.originalHeight),
+        onPressed: () => _changeResolution(
+            context, display.originalWidth, display.originalHeight),
         ffi: widget.ffi,
         child: Text(
             '${translate('resolution_original_tip')} ${display.originalWidth}x${display.originalHeight}'),
@@ -1140,14 +1166,15 @@ class _ResolutionsMenuState extends State<_ResolutionsMenu> {
     );
   }
 
-  Widget _FitLocalResolutionMenuButton(bool showFitLocalBtn) {
+  Widget _FitLocalResolutionMenuButton(
+      BuildContext context, bool showFitLocalBtn) {
     return Offstage(
       offstage: !showFitLocalBtn,
       child: MenuButton(
         onPressed: () {
           final resolution = _getBestFitResolution();
           if (resolution != null) {
-            _changeResolution(resolution.width, resolution.height);
+            _changeResolution(context, resolution.width, resolution.height);
           }
         },
         ffi: widget.ffi,
@@ -1157,13 +1184,13 @@ class _ResolutionsMenuState extends State<_ResolutionsMenu> {
     );
   }
 
-  Widget _customResolutionMenuButton(isVirtualDisplay) {
+  Widget _customResolutionMenuButton(BuildContext context, isVirtualDisplay) {
     return Offstage(
       offstage: !isVirtualDisplay,
       child: RdoMenuButton(
         value: _kCustomResolutionValue,
         groupValue: _groupValue,
-        onChanged: _onChanged,
+        onChanged: (String? value) => _onChanged(context, value),
         ffi: widget.ffi,
         child: Row(
           children: [
@@ -1204,7 +1231,7 @@ class _ResolutionsMenuState extends State<_ResolutionsMenu> {
       .map((e) => RdoMenuButton(
           value: '${e.width}x${e.height}',
           groupValue: _groupValue,
-          onChanged: _onChanged,
+          onChanged: (String? value) => _onChanged(context, value),
           ffi: widget.ffi,
           child: Text('${e.width}x${e.height}')))
       .toList();
@@ -1413,7 +1440,8 @@ class _ChatMenuState extends State<_ChatMenu> {
             initPos = Offset(pos.dx, pos.dy + _ToolbarTheme.dividerHeight);
           }
 
-          widget.ffi.chatModel.changeCurrentID(ChatModel.clientModeID);
+          widget.ffi.chatModel.changeCurrentKey(
+              MessageKey(widget.ffi.id, ChatModel.clientModeID));
           widget.ffi.chatModel.toggleChatOverlay(chatInitPos: initPos);
         });
   }
@@ -1523,7 +1551,7 @@ class _CloseMenu extends StatelessWidget {
 class _IconMenuButton extends StatefulWidget {
   final String? assetName;
   final Widget? icon;
-  final String? tooltip;
+  final String tooltip;
   final Color color;
   final Color hoverColor;
   final VoidCallback? onPressed;
@@ -1534,7 +1562,7 @@ class _IconMenuButton extends StatefulWidget {
     Key? key,
     this.assetName,
     this.icon,
-    this.tooltip,
+    required this.tooltip,
     required this.color,
     required this.hoverColor,
     required this.onPressed,
@@ -1556,11 +1584,11 @@ class _IconMenuButtonState extends State<_IconMenuButton> {
     final icon = widget.icon ??
         SvgPicture.asset(
           widget.assetName!,
-          color: Colors.white,
+          colorFilter: ColorFilter.mode(Colors.white, BlendMode.srcIn),
           width: _ToolbarTheme.buttonSize,
           height: _ToolbarTheme.buttonSize,
         );
-    final button = SizedBox(
+    var button = SizedBox(
       width: _ToolbarTheme.buttonSize,
       height: _ToolbarTheme.buttonSize,
       child: MenuItemButton(
@@ -1572,18 +1600,25 @@ class _IconMenuButtonState extends State<_IconMenuButton> {
           hover = value;
         }),
         onPressed: widget.onPressed,
-        child: Material(
-            type: MaterialType.transparency,
-            child: Ink(
-                decoration: BoxDecoration(
-                  borderRadius: BorderRadius.circular(_ToolbarTheme.iconRadius),
-                  color: hover ? widget.hoverColor : widget.color,
-                ),
-                child: icon)),
+        child: Tooltip(
+          message: translate(widget.tooltip),
+          child: Material(
+              type: MaterialType.transparency,
+              child: Ink(
+                  decoration: BoxDecoration(
+                    borderRadius: BorderRadius.circular(_ToolbarTheme.iconRadius),
+                    color: hover ? widget.hoverColor : widget.color,
+                  ),
+                  child: icon)),
+        )
       ),
     ).marginSymmetric(
         horizontal: widget.hMargin ?? _ToolbarTheme.buttonHMargin,
         vertical: widget.vMargin ?? _ToolbarTheme.buttonVMargin);
+    button = Tooltip(
+      message: widget.tooltip,
+      child: button,
+    );
     if (widget.topLevel) {
       return MenuBar(children: [button]);
     } else {
@@ -1627,7 +1662,7 @@ class _IconSubmenuButtonState extends State<_IconSubmenuButton> {
     final icon = widget.icon ??
         SvgPicture.asset(
           widget.svg!,
-          color: Colors.white,
+          colorFilter: ColorFilter.mode(Colors.white, BlendMode.srcIn),
           width: _ToolbarTheme.buttonSize,
           height: _ToolbarTheme.buttonSize,
         );
@@ -1635,23 +1670,23 @@ class _IconSubmenuButtonState extends State<_IconSubmenuButton> {
         width: _ToolbarTheme.buttonSize,
         height: _ToolbarTheme.buttonSize,
         child: SubmenuButton(
-            menuStyle: widget.menuStyle,
-            style: ButtonStyle(
-                backgroundColor: MaterialStatePropertyAll(Colors.transparent),
-                padding: MaterialStatePropertyAll(EdgeInsets.zero),
-                overlayColor: MaterialStatePropertyAll(Colors.transparent)),
+            menuStyle: widget.menuStyle ?? _ToolbarTheme.defaultMenuStyle,
+            style: _ToolbarTheme.defaultMenuButtonStyle,
             onHover: (value) => setState(() {
                   hover = value;
                 }),
-            child: Material(
-                type: MaterialType.transparency,
-                child: Ink(
-                    decoration: BoxDecoration(
-                      borderRadius:
+                child: Tooltip(
+                  message: translate(widget.tooltip),
+                  child: Material(
+                    type: MaterialType.transparency,
+                    child: Ink(
+                      decoration: BoxDecoration(
+                        borderRadius:
                           BorderRadius.circular(_ToolbarTheme.iconRadius),
-                      color: hover ? widget.hoverColor : widget.color,
+                          color: hover ? widget.hoverColor : widget.color,
+                      ),
+                      child: icon))
                     ),
-                    child: icon)),
             menuChildren: widget.menuChildren
                 .map((e) => _buildPointerTrackWidget(e, widget.ffi))
                 .toList()));
@@ -1681,6 +1716,7 @@ class _SubmenuButton extends StatelessWidget {
       child: child,
       menuChildren:
           menuChildren.map((e) => _buildPointerTrackWidget(e, ffi)).toList(),
+      menuStyle: _ToolbarTheme.defaultMenuStyle,
     );
   }
 }
@@ -1778,12 +1814,18 @@ class _DraggableShowHide extends StatefulWidget {
   final RxDouble fractionX;
   final RxBool dragging;
   final RxBool show;
+
+  final Function(bool) setFullscreen;
+  final Function() setMinimize;
+
   const _DraggableShowHide({
     Key? key,
     required this.sessionId,
     required this.fractionX,
     required this.dragging,
     required this.show,
+    required this.setFullscreen,
+    required this.setMinimize,
   }) : super(key: key);
 
   @override
@@ -1837,7 +1879,7 @@ class _DraggableShowHideState extends State<_DraggableShowHide> {
         widget.dragging.value = true;
       }),
       onDragEnd: (details) {
-        final mediaSize = MediaQueryData.fromWindow(ui.window).size;
+        final mediaSize = MediaQueryData.fromView(View.of(context)).size;
         widget.fractionX.value +=
             (details.offset.dx - position.dx) / (mediaSize.width - size.width);
         if (widget.fractionX.value < left) {
@@ -1862,17 +1904,49 @@ class _DraggableShowHideState extends State<_DraggableShowHide> {
       minimumSize: MaterialStateProperty.all(const Size(0, 0)),
       padding: MaterialStateProperty.all(EdgeInsets.zero),
     );
+    final isFullscreen = stateGlobal.fullscreen;
+    const double iconSize = 20;
     final child = Row(
       mainAxisSize: MainAxisSize.min,
       children: [
         _buildDraggable(context),
+        TextButton(
+          onPressed: () {
+            widget.setFullscreen(!isFullscreen);
+            setState(() {});
+          },
+          child: Tooltip(
+            message: translate(isFullscreen ? 'Exit Fullscreen' : 'Fullscreen'),
+            child: Icon(
+              isFullscreen ? Icons.fullscreen_exit : Icons.fullscreen,
+              size: iconSize,
+            ),
+          ),
+        ),
+        Offstage(
+          offstage: !isFullscreen,
+          child: TextButton(
+            onPressed: () => widget.setMinimize(),
+            child: Tooltip(
+              message: translate('Minimize'),
+              child: Icon(
+                Icons.remove,
+                size: iconSize,
+              ),
+            ),
+          ),
+        ),
         TextButton(
           onPressed: () => setState(() {
             widget.show.value = !widget.show.value;
           }),
-          child: Obx((() => Icon(
-                widget.show.isTrue ? Icons.expand_less : Icons.expand_more,
-                size: 20,
+          child: Obx((() => Tooltip(
+                message: translate(
+                    widget.show.isTrue ? 'Hide Toolbar' : 'Show Toolbar'),
+                child: Icon(
+                  widget.show.isTrue ? Icons.expand_less : Icons.expand_more,
+                  size: iconSize,
+                ),
               ))),
         ),
       ],
@@ -1938,6 +2012,7 @@ class _MultiMonitorMenu extends StatelessWidget {
         Obx(() {
           RxInt display = CurrentDisplayState.find(id);
           return _IconMenuButton(
+            tooltip: "",
             topLevel: false,
             color: i == display.value
                 ? _ToolbarTheme.blueColor
@@ -1954,7 +2029,8 @@ class _MultiMonitorMenu extends StatelessWidget {
                 children: [
                   SvgPicture.asset(
                     "assets/screen.svg",
-                    color: Colors.white,
+                    colorFilter:
+                        ColorFilter.mode(Colors.white, BlendMode.srcIn),
                   ),
                   Obx(
                     () => Text(
diff --git a/flutter/lib/desktop/widgets/tabbar_widget.dart b/flutter/lib/desktop/widgets/tabbar_widget.dart
index ab36728cd..be089559b 100644
--- a/flutter/lib/desktop/widgets/tabbar_widget.dart
+++ b/flutter/lib/desktop/widgets/tabbar_widget.dart
@@ -77,7 +77,7 @@ CancelFunc showRightMenu(ToastBuilder builder,
     targetContext: context,
     verticalOffset: 0,
     horizontalOffset: 0,
-    duration: Duration(seconds: 4),
+    duration: Duration(seconds: 300),
     animationDuration: Duration(milliseconds: 0),
     animationReverseDuration: Duration(milliseconds: 0),
     preferDirection: PreferDirection.rightTop,
@@ -146,8 +146,10 @@ class DesktopTabController {
 
   /// For addTab, tabPage has not been initialized, set [callOnSelected] to false,
   /// and call [onSelected] at the end of initState
-  void jumpTo(int index, {bool callOnSelected = true}) {
-    if (!isDesktop || index < 0) return;
+  bool jumpTo(int index, {bool callOnSelected = true}) {
+    if (!isDesktop || index < 0) {
+      return false;
+    }
     state.update((val) {
       val!.selected = index;
       Future.delayed(Duration(milliseconds: 100), (() {
@@ -168,8 +170,13 @@ class DesktopTabController {
         onSelected?.call(key);
       }
     }
+    return true;
   }
 
+  bool jumpToByKey(String key, {bool callOnSelected = true}) =>
+      jumpTo(state.value.tabs.indexWhere((tab) => tab.key == key),
+          callOnSelected: callOnSelected);
+
   void closeBy(String? key) {
     if (!isDesktop) return;
     assert(onRemoved != null);
@@ -187,6 +194,10 @@ class DesktopTabController {
     state.value.tabs.clear();
     state.refresh();
   }
+
+  Widget? widget(String key) {
+    return state.value.tabs.firstWhereOrNull((tab) => tab.key == key)?.page;
+  }
 }
 
 class TabThemeConf {
@@ -221,11 +232,11 @@ class DesktopTab extends StatelessWidget {
   final double? maxLabelWidth;
   final Color? selectedTabBackgroundColor;
   final Color? unSelectedTabBackgroundColor;
+  final Color? selectedBorderColor;
 
   final DesktopTabController controller;
 
   Rx<DesktopTabState> get state => controller.state;
-  final isMaximized = false.obs;
   final _scrollDebounce = Debouncer(delay: Duration(milliseconds: 50));
 
   late final DesktopTabType tabType;
@@ -248,6 +259,7 @@ class DesktopTab extends StatelessWidget {
     this.maxLabelWidth,
     this.selectedTabBackgroundColor,
     this.unSelectedTabBackgroundColor,
+    this.selectedBorderColor,
   }) : super(key: key) {
     tabType = controller.tabType;
     isMainWindow = tabType == DesktopTabType.main ||
@@ -295,37 +307,16 @@ class DesktopTab extends StatelessWidget {
     if (tabType != DesktopTabType.main) {
       return child;
     }
-    var block = false.obs;
-    return Obx(() => MouseRegion(
-          onEnter: (_) async {
-            var access_mode = await bind.mainGetOption(key: 'access-mode');
-            var option = option2bool(
-                'allow-remote-config-modification',
-                await bind.mainGetOption(
-                    key: 'allow-remote-config-modification'));
-            if (access_mode == 'view' || (access_mode.isEmpty && !option)) {
-              var time0 = DateTime.now().millisecondsSinceEpoch;
-              await bind.mainCheckMouseTime();
-              Timer(const Duration(milliseconds: 120), () async {
-                var d = time0 - await bind.mainGetMouseTime();
-                if (d < 120) {
-                  block.value = true;
-                }
-              });
-            }
-          },
-          onExit: (_) => block.value = false,
-          child: Stack(
-            children: [
-              child,
-              Offstage(
-                  offstage: !block.value,
-                  child: Container(
-                    color: Colors.black.withOpacity(0.5),
-                  )),
-            ],
-          ),
-        ));
+    return buildRemoteBlock(
+        child: child,
+        use: () async {
+          var access_mode = await bind.mainGetOption(key: 'access-mode');
+          var option = option2bool(
+              'allow-remote-config-modification',
+              await bind.mainGetOption(
+                  key: 'allow-remote-config-modification'));
+          return access_mode == 'view' || (access_mode.isEmpty && !option);
+        });
   }
 
   List<Widget> _tabWidgets = [];
@@ -381,7 +372,7 @@ class DesktopTab extends StatelessWidget {
                         if (elapsed < bind.getDoubleClickTime()) {
                           // onDoubleTap
                           toggleMaximize(isMainWindow)
-                              .then((value) => isMaximized.value = value);
+                              .then((value) => stateGlobal.setMaximized(value));
                         }
                       }
                     : null,
@@ -430,15 +421,17 @@ class DesktopTab extends StatelessWidget {
                               }
                             },
                             child: _ListView(
-                                controller: controller,
-                                tabBuilder: tabBuilder,
-                                tabMenuBuilder: tabMenuBuilder,
-                                labelGetter: labelGetter,
-                                maxLabelWidth: maxLabelWidth,
-                                selectedTabBackgroundColor:
-                                    selectedTabBackgroundColor,
-                                unSelectedTabBackgroundColor:
-                                    unSelectedTabBackgroundColor))),
+                              controller: controller,
+                              tabBuilder: tabBuilder,
+                              tabMenuBuilder: tabMenuBuilder,
+                              labelGetter: labelGetter,
+                              maxLabelWidth: maxLabelWidth,
+                              selectedTabBackgroundColor:
+                                  selectedTabBackgroundColor,
+                              unSelectedTabBackgroundColor:
+                                  unSelectedTabBackgroundColor,
+                              selectedBorderColor: selectedBorderColor,
+                            ))),
                   ],
                 ))),
         // hide simulated action buttons when we in compatible ui mode, because of reusing system title bar.
@@ -447,7 +440,6 @@ class DesktopTab extends StatelessWidget {
           tabType: tabType,
           state: state,
           tail: tail,
-          isMaximized: isMaximized,
           showMinimize: showMinimize,
           showMaximize: showMaximize,
           showClose: showClose,
@@ -462,7 +454,6 @@ class WindowActionPanel extends StatefulWidget {
   final bool isMainWindow;
   final DesktopTabType tabType;
   final Rx<DesktopTabState> state;
-  final RxBool isMaximized;
 
   final bool showMinimize;
   final bool showMaximize;
@@ -475,7 +466,6 @@ class WindowActionPanel extends StatefulWidget {
       required this.isMainWindow,
       required this.tabType,
       required this.state,
-      required this.isMaximized,
       this.tail,
       this.showMinimize = true,
       this.showMaximize = true,
@@ -491,6 +481,10 @@ class WindowActionPanel extends StatefulWidget {
 
 class WindowActionPanelState extends State<WindowActionPanel>
     with MultiWindowListener, WindowListener {
+  final _saveFrameDebounce = Debouncer(delay: Duration(seconds: 1));
+  Timer? _macOSCheckRestoreTimer;
+  int _macOSCheckRestoreCounter = 0;
+
   @override
   void initState() {
     super.initState();
@@ -500,18 +494,18 @@ class WindowActionPanelState extends State<WindowActionPanel>
     Future.delayed(Duration(milliseconds: 500), () {
       if (widget.isMainWindow) {
         windowManager.isMaximized().then((maximized) {
-          if (widget.isMaximized.value != maximized) {
+          if (stateGlobal.isMaximized.value != maximized) {
             WidgetsBinding.instance.addPostFrameCallback(
-                (_) => setState(() => widget.isMaximized.value = maximized));
+                (_) => setState(() => stateGlobal.setMaximized(maximized)));
           }
         });
       } else {
         final wc = WindowController.fromWindowId(kWindowId!);
         wc.isMaximized().then((maximized) {
           debugPrint("isMaximized $maximized");
-          if (widget.isMaximized.value != maximized) {
+          if (stateGlobal.isMaximized.value != maximized) {
             WidgetsBinding.instance.addPostFrameCallback(
-                (_) => setState(() => widget.isMaximized.value = maximized));
+                (_) => setState(() => stateGlobal.setMaximized(maximized)));
           }
         });
       }
@@ -522,36 +516,86 @@ class WindowActionPanelState extends State<WindowActionPanel>
   void dispose() {
     DesktopMultiWindow.removeListener(this);
     windowManager.removeListener(this);
+    _macOSCheckRestoreTimer?.cancel();
     super.dispose();
   }
 
-  void _setMaximize(bool maximize) {
-    stateGlobal.setMaximize(maximize);
+  void _setMaximized(bool maximize) {
+    stateGlobal.setMaximized(maximize);
+    _saveFrameDebounce.call(_saveFrame);
     setState(() {});
   }
 
+  @override
+  void onWindowMinimize() {
+    stateGlobal.setMinimized(true);
+    stateGlobal.setMaximized(false);
+    super.onWindowMinimize();
+  }
+
   @override
   void onWindowMaximize() {
-    // catch maximize from system
-    if (!widget.isMaximized.value) {
-      widget.isMaximized.value = true;
-    }
-    _setMaximize(true);
+    stateGlobal.setMinimized(false);
+    _setMaximized(true);
     super.onWindowMaximize();
   }
 
   @override
   void onWindowUnmaximize() {
-    // catch unmaximize from system
-    if (widget.isMaximized.value) {
-      widget.isMaximized.value = false;
-    }
-    _setMaximize(false);
+    stateGlobal.setMinimized(false);
+    _setMaximized(false);
     super.onWindowUnmaximize();
   }
 
+  _saveFrame() async {
+    if (widget.tabType == DesktopTabType.main) {
+      await saveWindowPosition(WindowType.Main);
+    } else if (kWindowType != null && kWindowId != null) {
+      await saveWindowPosition(kWindowType!, windowId: kWindowId);
+    }
+  }
+
+  @override
+  void onWindowMoved() {
+    _saveFrameDebounce.call(_saveFrame);
+    super.onWindowMoved();
+  }
+
+  @override
+  void onWindowResized() {
+    _saveFrameDebounce.call(_saveFrame);
+    super.onWindowMoved();
+  }
+
   @override
   void onWindowClose() async {
+    mainWindowClose() async => await windowManager.hide();
+    notMainWindowClose(WindowController controller) async {
+      await controller.hide();
+      await Future.wait([
+        rustDeskWinManager
+            .call(WindowType.Main, kWindowEventHide, {"id": kWindowId!}),
+        widget.onClose?.call() ?? Future.microtask(() => null)
+      ]);
+    }
+
+    macOSWindowClose(
+        Future<void> Function() restoreFunc,
+        Future<bool> Function() checkFullscreen,
+        Future<void> Function() closeFunc) async {
+      await restoreFunc();
+      _macOSCheckRestoreCounter = 0;
+      _macOSCheckRestoreTimer =
+          Timer.periodic(Duration(milliseconds: 30), (timer) async {
+        _macOSCheckRestoreCounter++;
+        if (!await checkFullscreen() || _macOSCheckRestoreCounter >= 30) {
+          _macOSCheckRestoreTimer?.cancel();
+          _macOSCheckRestoreTimer = null;
+          Timer(Duration(milliseconds: 700), () async => await closeFunc());
+        }
+      });
+    }
+
     // hide window on close
     if (widget.isMainWindow) {
       if (rustDeskWinManager.getActiveWindows().contains(kMainWindowId)) {
@@ -559,23 +603,28 @@ class WindowActionPanelState extends State<WindowActionPanel>
       }
       // macOS specific workaround, the window is not hiding when in fullscreen.
       if (Platform.isMacOS && await windowManager.isFullScreen()) {
-        await windowManager.setFullScreen(false);
-        await Future.delayed(Duration(seconds: 1));
+        stateGlobal.closeOnFullscreen = true;
+        await macOSWindowClose(
+            () async => await windowManager.setFullScreen(false),
+            () async => await windowManager.isFullScreen(),
+            mainWindowClose);
+      } else {
+        stateGlobal.closeOnFullscreen = false;
+        await mainWindowClose();
       }
-      await windowManager.hide();
     } else {
       // it's safe to hide the subwindow
       final controller = WindowController.fromWindowId(kWindowId!);
       if (Platform.isMacOS && await controller.isFullScreen()) {
-        await controller.setFullscreen(false);
-        await Future.delayed(Duration(seconds: 1));
+        stateGlobal.closeOnFullscreen = true;
+        await macOSWindowClose(
+            () async => await controller.setFullscreen(false),
+            () async => await controller.isFullScreen(),
+            () async => await notMainWindowClose(controller));
+      } else {
+        stateGlobal.closeOnFullscreen = false;
+        await notMainWindowClose(controller);
       }
-      await controller.hide();
-      await Future.wait([
-        rustDeskWinManager
-            .call(WindowType.Main, kWindowEventHide, {"id": kWindowId!}),
-        widget.onClose?.call() ?? Future.microtask(() => null)
-      ]);
     }
     super.onWindowClose();
   }
@@ -607,9 +656,10 @@ class WindowActionPanelState extends State<WindowActionPanel>
               Offstage(
                   offstage: !widget.showMaximize || Platform.isMacOS,
                   child: Obx(() => ActionIcon(
-                        message:
-                            widget.isMaximized.value ? 'Restore' : 'Maximize',
-                        icon: widget.isMaximized.value
+                        message: stateGlobal.isMaximized.isTrue
+                            ? 'Restore'
+                            : 'Maximize',
+                        icon: stateGlobal.isMaximized.isTrue
                             ? IconFont.restore
                             : IconFont.max,
                         onTap: _toggleMaximize,
@@ -646,10 +696,8 @@ class WindowActionPanelState extends State<WindowActionPanel>
 
   void _toggleMaximize() {
     toggleMaximize(widget.isMainWindow).then((maximize) {
-      if (widget.isMaximized.value != maximize) {
-        // update state for sub window, wc.unmaximize/maximize() will not invoke onWindowMaximize/Unmaximize
-        widget.isMaximized.value = maximize;
-      }
+      // update state for sub window, wc.unmaximize/maximize() will not invoke onWindowMaximize/Unmaximize
+      stateGlobal.setMaximized(maximize);
     });
   }
 }
@@ -691,7 +739,7 @@ Future<bool> closeConfirmDialog() async {
     submit() {
       final opt = "enable-confirm-closing-tabs";
       String value = bool2option(opt, confirm);
-      bind.mainSetOption(key: opt, value: value);
+      bind.mainSetLocalOption(key: opt, value: value);
       close(true);
     }
 
@@ -741,6 +789,7 @@ class _ListView extends StatelessWidget {
   final LabelGetter? labelGetter;
   final double? maxLabelWidth;
   final Color? selectedTabBackgroundColor;
+  final Color? selectedBorderColor;
   final Color? unSelectedTabBackgroundColor;
 
   Rx<DesktopTabState> get state => controller.state;
@@ -753,6 +802,7 @@ class _ListView extends StatelessWidget {
     this.maxLabelWidth,
     this.selectedTabBackgroundColor,
     this.unSelectedTabBackgroundColor,
+    this.selectedBorderColor,
   });
 
   /// Check whether to show ListView
@@ -805,6 +855,7 @@ class _ListView extends StatelessWidget {
                   selectedTabBackgroundColor: selectedTabBackgroundColor ??
                       MyTheme.tabbar(context).selectedTabBackgroundColor,
                   unSelectedTabBackgroundColor: unSelectedTabBackgroundColor,
+                  selectedBorderColor: selectedBorderColor,
                 );
               }).toList()));
   }
@@ -825,6 +876,7 @@ class _Tab extends StatefulWidget {
   final double? maxLabelWidth;
   final Color? selectedTabBackgroundColor;
   final Color? unSelectedTabBackgroundColor;
+  final Color? selectedBorderColor;
 
   const _Tab({
     Key? key,
@@ -842,6 +894,7 @@ class _Tab extends StatefulWidget {
     this.maxLabelWidth,
     this.selectedTabBackgroundColor,
     this.unSelectedTabBackgroundColor,
+    this.selectedBorderColor,
   }) : super(key: key);
 
   @override
@@ -932,35 +985,46 @@ class _TabState extends State<_Tab> with RestorationMixin {
         },
         onTap: () => widget.onTap(),
         child: Container(
-          color: isSelected
-              ? widget.selectedTabBackgroundColor
-              : widget.unSelectedTabBackgroundColor,
-          child: Row(
-            children: [
-              SizedBox(
-                  height: _kTabBarHeight,
-                  child: Row(
-                      crossAxisAlignment: CrossAxisAlignment.center,
-                      children: [
-                        _buildTabContent(),
-                        Obx((() => _CloseButton(
-                              visible: hover.value && widget.closable,
-                              tabSelected: isSelected,
-                              onClose: () => widget.onClose(),
-                            )))
-                      ])).paddingOnly(left: 10, right: 5),
-              Offstage(
-                offstage: !showDivider,
-                child: VerticalDivider(
-                  width: 1,
-                  indent: _kDividerIndent,
-                  endIndent: _kDividerIndent,
-                  color: MyTheme.tabbar(context).dividerColor,
-                ),
-              )
-            ],
-          ),
-        ),
+            decoration: isSelected && widget.selectedBorderColor != null
+                ? BoxDecoration(
+                    border: Border(
+                      bottom: BorderSide(
+                        color: widget.selectedBorderColor!,
+                        width: 1,
+                      ),
+                    ),
+                  )
+                : null,
+            child: Container(
+              color: isSelected
+                  ? widget.selectedTabBackgroundColor
+                  : widget.unSelectedTabBackgroundColor,
+              child: Row(
+                children: [
+                  SizedBox(
+                      height: _kTabBarHeight,
+                      child: Row(
+                          crossAxisAlignment: CrossAxisAlignment.center,
+                          children: [
+                            _buildTabContent(),
+                            Obx((() => _CloseButton(
+                                  visible: hover.value && widget.closable,
+                                  tabSelected: isSelected,
+                                  onClose: () => widget.onClose(),
+                                )))
+                          ])).paddingOnly(left: 10, right: 5),
+                  Offstage(
+                    offstage: !showDivider,
+                    child: VerticalDivider(
+                      width: 1,
+                      indent: _kDividerIndent,
+                      endIndent: _kDividerIndent,
+                      color: MyTheme.tabbar(context).dividerColor,
+                    ),
+                  )
+                ],
+              ),
+            )),
       ),
     );
   }
@@ -1116,14 +1180,14 @@ class TabbarTheme extends ThemeExtension<TabbarTheme> {
       selectedIconColor: Color.fromARGB(255, 26, 26, 26),
       unSelectedIconColor: Color.fromARGB(255, 96, 96, 96),
       dividerColor: Color.fromARGB(255, 238, 238, 238),
-      hoverColor: Color.fromARGB(51, 158, 158, 158),
-      closeHoverColor: Color.fromARGB(255, 224, 224, 224),
-      selectedTabBackgroundColor: Color.fromARGB(255, 240, 240, 240));
+      hoverColor: Colors.white54,
+      closeHoverColor: Colors.white,
+      selectedTabBackgroundColor: Colors.white54);
 
   static const dark = TabbarTheme(
       selectedTabIconColor: MyTheme.accent,
       unSelectedTabIconColor: Color.fromARGB(255, 30, 65, 98),
-      selectedTextColor: Color.fromARGB(255, 255, 255, 255),
+      selectedTextColor: Colors.white,
       unSelectedTextColor: Color.fromARGB(255, 192, 192, 192),
       selectedIconColor: Color.fromARGB(255, 192, 192, 192),
       unSelectedIconColor: Color.fromARGB(255, 255, 255, 255),
diff --git a/flutter/lib/main.dart b/flutter/lib/main.dart
index eb4901686..43273c547 100644
--- a/flutter/lib/main.dart
+++ b/flutter/lib/main.dart
@@ -5,6 +5,7 @@ import 'dart:io';
 import 'package:bot_toast/bot_toast.dart';
 import 'package:desktop_multi_window/desktop_multi_window.dart';
 import 'package:flutter/material.dart';
+import 'package:flutter/services.dart';
 import 'package:flutter_hbb/desktop/pages/desktop_tab_page.dart';
 import 'package:flutter_hbb/desktop/pages/install_page.dart';
 import 'package:flutter_hbb/desktop/pages/server_page.dart';
@@ -124,6 +125,7 @@ void runMainApp(bool startService) async {
     bind.pluginSyncUi(syncTo: kAppTypeMain);
     bind.pluginListReload();
   }
+  gFFI.abModel.loadCache();
   gFFI.userModel.refreshCurrentUser();
   runApp(App());
   // Set window option.
@@ -134,7 +136,7 @@ void runMainApp(bool startService) async {
     // Check the startup argument, if we successfully handle the argument, we keep the main window hidden.
     final handledByUniLinks = await initUniLinks();
     debugPrint("handled by uni links: $handledByUniLinks");
-    if (handledByUniLinks || checkArguments()) {
+    if (handledByUniLinks || handleUriLink(cmdArgs: kBootArgs)) {
       windowManager.hide();
     } else {
       windowManager.show();
@@ -151,6 +153,7 @@ void runMobileApp() async {
   await initEnv(kAppTypeMain);
   if (isAndroid) androidChannelInit();
   platformFFI.syncAndroidServiceAppDirConfigPath();
+  gFFI.abModel.loadCache();
   gFFI.userModel.refreshCurrentUser();
   runApp(App());
 }
@@ -196,7 +199,7 @@ void runMultiWindow(
   switch (appType) {
     case kAppTypeDesktopRemote:
       await restoreWindowPosition(WindowType.RemoteDesktop,
-          windowId: kWindowId!);
+          windowId: kWindowId!, peerId: argument['id'] as String?);
       break;
     case kAppTypeDesktopFileTransfer:
       await restoreWindowPosition(WindowType.FileTransfer,
@@ -220,11 +223,13 @@ void runConnectionManagerScreen(bool hide) async {
     const DesktopServerPage(),
     MyTheme.currentThemeMode(),
   );
+  gFFI.serverModel.hideCm = hide;
   if (hide) {
     await hideCmWindow(isStartup: true);
   } else {
     await showCmWindow(isStartup: true);
   }
+  windowManager.setResizable(false);
   // Start the uni links handler and redirect links to Native, not for Flutter.
   listenUniLinks(handleByFlutter: false);
 }
@@ -248,7 +253,7 @@ showCmWindow({bool isStartup = false}) async {
       await windowManager.minimize(); //needed
       await windowManager.setSizeAlignment(
           kConnectionManagerWindowSizeClosedChat, Alignment.topRight);
-      window_on_top(null);
+      windowOnTop(null);
     }
   }
 }
@@ -264,10 +269,12 @@ hideCmWindow({bool isStartup = false}) async {
       await windowManager.hide();
     });
   } else {
-    await windowManager.setOpacity(0);
-    bind.mainHideDocker();
-    await windowManager.minimize();
-    await windowManager.hide();
+    if (await windowManager.getOpacity() != 0) {
+      await windowManager.setOpacity(0);
+      bind.mainHideDocker();
+      await windowManager.minimize();
+      await windowManager.hide();
+    }
   }
 }
 
@@ -391,7 +398,7 @@ class _AppState extends State<App> {
           themeMode: MyTheme.currentThemeMode(),
           home: isDesktop
               ? const DesktopTabPage()
-              : !isAndroid
+              : isWeb
                   ? WebHomePage()
                   : HomePage(),
           localizationsDelegates: const [
@@ -416,6 +423,9 @@ class _AppState extends State<App> {
               : (context, child) {
                   child = _keepScaleBuilder(context, child);
                   child = botToastBuilder(context, child);
+                  if (isDesktop && desktopType == DesktopType.main) {
+                    child = keyListenerBuilder(context, child);
+                  }
                   return child;
                 },
         ),
@@ -452,3 +462,19 @@ _registerEventHandler() {
     });
   }
 }
+
+Widget keyListenerBuilder(BuildContext context, Widget? child) {
+  return RawKeyboardListener(
+    focusNode: FocusNode(),
+    child: child ?? Container(),
+    onKey: (RawKeyEvent event) {
+      if (event.logicalKey == LogicalKeyboardKey.shiftLeft) {
+        if (event is RawKeyDownEvent) {
+          gFFI.peerTabModel.setShiftDown(true);
+        } else if (event is RawKeyUpEvent) {
+          gFFI.peerTabModel.setShiftDown(false);
+        }
+      }
+    },
+  );
+}
diff --git a/flutter/lib/mobile/pages/connection_page.dart b/flutter/lib/mobile/pages/connection_page.dart
index 3f8fdd32e..4a14f8466 100644
--- a/flutter/lib/mobile/pages/connection_page.dart
+++ b/flutter/lib/mobile/pages/connection_page.dart
@@ -1,5 +1,6 @@
 import 'dart:async';
 
+import 'package:auto_size_text_field/auto_size_text_field.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter_hbb/common/formatter/id_formatter.dart';
 import 'package:get/get.dart';
@@ -27,7 +28,7 @@ class ConnectionPage extends StatefulWidget implements PageShape {
   final title = translate("Connection");
 
   @override
-  final appBarActions = !isAndroid ? <Widget>[const WebMenu()] : <Widget>[];
+  final appBarActions = isWeb ? <Widget>[const WebMenu()] : <Widget>[];
 
   @override
   State<ConnectionPage> createState() => _ConnectionPageState();
@@ -37,6 +38,7 @@ class ConnectionPage extends StatefulWidget implements PageShape {
 class _ConnectionPageState extends State<ConnectionPage> {
   /// Controller for the id input bar.
   final _idController = IDTextEditingController();
+  final RxBool _idEmpty = true.obs;
 
   /// Update url. If it's not null, means an update is available.
   var _updateUrl = '';
@@ -60,6 +62,11 @@ class _ConnectionPageState extends State<ConnectionPage> {
         if (_updateUrl.isNotEmpty) setState(() {});
       });
     }
+
+    _idController.addListener(() {
+      _idEmpty.value = _idController.text.isEmpty;
+    });
+    Get.put<IDTextEditingController>(_idController);
   }
 
   @override
@@ -94,7 +101,7 @@ class _ConnectionPageState extends State<ConnectionPage> {
         ? const SizedBox(height: 0)
         : InkWell(
             onTap: () async {
-              final url = '$_updateUrl.apk';
+              final url = 'https://rustdesk.com/download';
               if (await canLaunchUrl(Uri.parse(url))) {
                 await launchUrl(Uri.parse(url));
               }
@@ -126,7 +133,8 @@ class _ConnectionPageState extends State<ConnectionPage> {
               Expanded(
                 child: Container(
                   padding: const EdgeInsets.only(left: 16, right: 16),
-                  child: TextField(
+                  child: AutoSizeTextField(
+                    minFontSize: 18,
                     autocorrect: false,
                     enableSuggestions: false,
                     keyboardType: TextInputType.visiblePassword,
@@ -158,6 +166,14 @@ class _ConnectionPageState extends State<ConnectionPage> {
                   ),
                 ),
               ),
+              Obx(() => Offstage(
+                    offstage: _idEmpty.value,
+                    child: IconButton(
+                        onPressed: () {
+                          _idController.clear();
+                        },
+                        icon: Icon(Icons.clear, color: MyTheme.darkGray)),
+                  )),
               SizedBox(
                 width: 60,
                 height: 60,
@@ -180,6 +196,9 @@ class _ConnectionPageState extends State<ConnectionPage> {
   @override
   void dispose() {
     _idController.dispose();
+    if (Get.isRegistered<IDTextEditingController>()) {
+      Get.delete<IDTextEditingController>();
+    }
     super.dispose();
   }
 }
@@ -192,25 +211,6 @@ class WebMenu extends StatefulWidget {
 }
 
 class _WebMenuState extends State<WebMenu> {
-  String url = "";
-
-  @override
-  void initState() {
-    super.initState();
-    () async {
-      final urlRes = await bind.mainGetApiServer();
-      var update = false;
-      if (urlRes != url) {
-        url = urlRes;
-        update = true;
-      }
-
-      if (update) {
-        setState(() {});
-      }
-    }();
-  }
-
   @override
   Widget build(BuildContext context) {
     Provider.of<FfiModel>(context);
@@ -232,16 +232,14 @@ class _WebMenuState extends State<WebMenu> {
                   child: Text(translate('ID/Relay Server')),
                 )
               ] +
-              (url.contains('admin.rustdesk.com')
-                  ? <PopupMenuItem<String>>[]
-                  : [
-                      PopupMenuItem(
-                        value: "login",
-                        child: Text(gFFI.userModel.userName.value.isEmpty
-                            ? translate("Login")
-                            : '${translate("Logout")} (${gFFI.userModel.userName.value})'),
-                      )
-                    ]) +
+              [
+                PopupMenuItem(
+                  value: "login",
+                  child: Text(gFFI.userModel.userName.value.isEmpty
+                      ? translate("Login")
+                      : '${translate("Logout")} (${gFFI.userModel.userName.value})'),
+                )
+              ] +
               [
                 PopupMenuItem(
                   value: "about",
@@ -260,7 +258,7 @@ class _WebMenuState extends State<WebMenu> {
             if (gFFI.userModel.userName.value.isEmpty) {
               loginDialog();
             } else {
-              gFFI.userModel.logOut();
+              logOutConfirmDialog();
             }
           }
           if (value == 'scan') {
diff --git a/flutter/lib/mobile/pages/home_page.dart b/flutter/lib/mobile/pages/home_page.dart
index f806c2576..c04e03244 100644
--- a/flutter/lib/mobile/pages/home_page.dart
+++ b/flutter/lib/mobile/pages/home_page.dart
@@ -1,13 +1,14 @@
 import 'package:flutter/material.dart';
 import 'package:flutter_hbb/mobile/pages/server_page.dart';
 import 'package:flutter_hbb/mobile/pages/settings_page.dart';
+import 'package:get/get.dart';
 import '../../common.dart';
 import '../../common/widgets/chat_page.dart';
 import 'connection_page.dart';
 
 abstract class PageShape extends Widget {
   final String title = "";
-  final Icon icon = Icon(null);
+  final Widget icon = Icon(null);
   final List<Widget> appBarActions = [];
 }
 
@@ -22,7 +23,11 @@ class HomePage extends StatefulWidget {
 
 class _HomePageState extends State<HomePage> {
   var _selectedIndex = 0;
+  int get selectedIndex => _selectedIndex;
   final List<PageShape> _pages = [];
+  bool get isChatPageCurrentTab => isAndroid
+      ? _selectedIndex == 1
+      : false; // change this when ios have chat page
 
   void refreshPages() {
     setState(() {
@@ -40,7 +45,7 @@ class _HomePageState extends State<HomePage> {
     _pages.clear();
     _pages.add(ConnectionPage());
     if (isAndroid) {
-      _pages.addAll([ChatPage(), ServerPage()]);
+      _pages.addAll([ChatPage(type: ChatPageType.mobileMain), ServerPage()]);
     }
     _pages.add(SettingsPage());
   }
@@ -62,7 +67,7 @@ class _HomePageState extends State<HomePage> {
           // backgroundColor: MyTheme.grayBg,
           appBar: AppBar(
             centerTitle: true,
-            title: Text("RustDesk"),
+            title: appTitle(),
             actions: _pages.elementAt(_selectedIndex).appBarActions,
           ),
           bottomNavigationBar: BottomNavigationBar(
@@ -77,16 +82,67 @@ class _HomePageState extends State<HomePage> {
             unselectedItemColor: MyTheme.darkGray,
             onTap: (index) => setState(() {
               // close chat overlay when go chat page
-              if (index == 1 && _selectedIndex != index) {
-                gFFI.chatModel.hideChatIconOverlay();
-                gFFI.chatModel.hideChatWindowOverlay();
+              if (_selectedIndex != index) {
+                _selectedIndex = index;
+                if (isChatPageCurrentTab) {
+                  gFFI.chatModel.hideChatIconOverlay();
+                  gFFI.chatModel.hideChatWindowOverlay();
+                  gFFI.chatModel.mobileClearClientUnread(
+                      gFFI.chatModel.currentKey.connId);
+                }
               }
-              _selectedIndex = index;
             }),
           ),
           body: _pages.elementAt(_selectedIndex),
         ));
   }
+
+  Widget appTitle() {
+    final currentUser = gFFI.chatModel.currentUser;
+    final currentKey = gFFI.chatModel.currentKey;
+    if (isChatPageCurrentTab &&
+        currentUser != null &&
+        currentKey.peerId.isNotEmpty) {
+      final connected =
+          gFFI.serverModel.clients.any((e) => e.id == currentKey.connId);
+      return Row(
+        mainAxisAlignment: MainAxisAlignment.center,
+        children: [
+          Tooltip(
+            message: currentKey.isOut
+                ? translate('Outgoing connection')
+                : translate('Incoming connection'),
+            child: Icon(
+              currentKey.isOut
+                  ? Icons.call_made_rounded
+                  : Icons.call_received_rounded,
+            ),
+          ),
+          Expanded(
+            child: Center(
+              child: Row(
+                mainAxisAlignment: MainAxisAlignment.center,
+                children: [
+                  Text(
+                    "${currentUser.firstName}   ${currentUser.id}",
+                  ),
+                  if (connected)
+                    Container(
+                      width: 10,
+                      height: 10,
+                      decoration: BoxDecoration(
+                          shape: BoxShape.circle,
+                          color: Color.fromARGB(255, 133, 246, 199)),
+                    ).marginSymmetric(horizontal: 2),
+                ],
+              ),
+            ),
+          ),
+        ],
+      );
+    }
+    return Text("RustDesk");
+  }
 }
 
 class WebHomePage extends StatelessWidget {
diff --git a/flutter/lib/mobile/pages/remote_page.dart b/flutter/lib/mobile/pages/remote_page.dart
index 028fa2ed9..838545d0c 100644
--- a/flutter/lib/mobile/pages/remote_page.dart
+++ b/flutter/lib/mobile/pages/remote_page.dart
@@ -1,5 +1,4 @@
 import 'dart:async';
-import 'dart:ui' as ui;
 
 import 'package:flutter/material.dart';
 import 'package:flutter/services.dart';
@@ -21,9 +20,8 @@ import '../../models/input_model.dart';
 import '../../models/model.dart';
 import '../../models/platform_model.dart';
 import '../../utils/image.dart';
-import '../widgets/gestures.dart';
 
-final initText = '\1' * 1024;
+final initText = '1' * 1024;
 
 class RemotePage extends StatefulWidget {
   RemotePage({Key? key, required this.id}) : super(key: key);
@@ -39,8 +37,6 @@ class _RemotePageState extends State<RemotePage> {
   bool _showBar = !isWebDesktop;
   bool _showGestureHelp = false;
   String _value = '';
-  double _scale = 1;
-  double _mouseScrollIntegral = 0; // mouse scroll speed controller
   Orientation? _currentOrientation;
 
   final _blockableOverlayState = BlockableOverlayState();
@@ -70,8 +66,11 @@ class _RemotePageState extends State<RemotePage> {
     gFFI.qualityMonitorModel.checkShowQualityMonitor(sessionId);
     keyboardSubscription =
         keyboardVisibilityController.onChange.listen(onSoftKeyboardChanged);
-    _blockableOverlayState.applyFfi(gFFI);
     initSharedStates(widget.id);
+    gFFI.chatModel
+        .changeCurrentKey(MessageKey(widget.id, ChatModel.clientModeID));
+
+    _blockableOverlayState.applyFfi(gFFI);
   }
 
   @override
@@ -93,6 +92,19 @@ class _RemotePageState extends State<RemotePage> {
     removeSharedStates(widget.id);
   }
 
+  // to-do: It should be better to use transparent color instead of the bgColor.
+  // But for now, the transparent color will cause the canvas to be white.
+  // I'm sure that the white color is caused by the Overlay widget in BlockableOverlay.
+  // But I don't know why and how to fix it.
+  Widget emptyOverlay(Color bgColor) => BlockableOverlay(
+        /// the Overlay key will be set with _blockableOverlayState in BlockableOverlay
+        /// see override build() in [BlockableOverlay]
+        state: _blockableOverlayState,
+        underlying: Container(
+          color: bgColor,
+        ),
+      );
+
   void onSoftKeyboardChanged(bool visible) {
     if (!visible) {
       SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, overlays: []);
@@ -203,13 +215,19 @@ class _RemotePageState extends State<RemotePage> {
     });
   }
 
+  bool get keyboard => gFFI.ffiModel.permissions['keyboard'] != false;
+
+  Widget _bottomWidget() => _showGestureHelp
+      ? getGestureHelp()
+      : (_showBar && gFFI.ffiModel.pi.displays.isNotEmpty
+          ? getBottomAppBar(keyboard)
+          : Offstage());
+
   @override
   Widget build(BuildContext context) {
-    final pi = Provider.of<FfiModel>(context).pi;
     final keyboardIsVisible =
         keyboardVisibilityController.isVisible && _showEdit;
     final showActionButton = !_showBar || keyboardIsVisible || _showGestureHelp;
-    final keyboard = gFFI.ffiModel.permissions['keyboard'] != false;
 
     return WillPopScope(
       onWillPop: () async {
@@ -246,11 +264,20 @@ class _RemotePageState extends State<RemotePage> {
                       }
                     });
                   }),
-          bottomNavigationBar: _showGestureHelp
-              ? getGestureHelp()
-              : (_showBar && pi.displays.isNotEmpty
-                  ? getBottomAppBar(keyboard)
-                  : null),
+          bottomNavigationBar: Obx(() => Stack(
+                alignment: Alignment.bottomCenter,
+                children: [
+                  gFFI.ffiModel.pi.isSet.isTrue &&
+                          gFFI.ffiModel.waitForFirstImage.isTrue
+                      ? emptyOverlay(MyTheme.canvasColor)
+                      : () {
+                          gFFI.ffiModel.tryShowAndroidActionsOverlay();
+                          return Offstage();
+                        }(),
+                  _bottomWidget(),
+                  gFFI.ffiModel.pi.isSet.isFalse ? emptyOverlay(MyTheme.canvasColor) : Offstage(),
+                ],
+              )),
           body: Overlay(
             initialEntries: [
               OverlayEntry(builder: (context) {
@@ -268,11 +295,17 @@ class _RemotePageState extends State<RemotePage> {
                                 gFFI.canvasModel.updateViewStyle();
                               });
                             }
-                            return Obx(() => Container(
+                            return Obx(
+                              () => Container(
                                 color: MyTheme.canvasColor,
                                 child: inputModel.isPhysicalMouse.value
                                     ? getBodyForMobile()
-                                    : getBodyForMobileWithGesture()));
+                                    : RawTouchGestureDetectorRegion(
+                                        child: getBodyForMobile(),
+                                        ffi: gFFI,
+                                      ),
+                              ),
+                            );
                           })));
               })
             ],
@@ -351,8 +384,8 @@ class _RemotePageState extends State<RemotePage> {
                             color: Colors.white,
                             icon: Icon(Icons.message),
                             onPressed: () {
-                              gFFI.chatModel
-                                  .changeCurrentID(ChatModel.clientModeID);
+                              gFFI.chatModel.changeCurrentKey(MessageKey(
+                                  widget.id, ChatModel.clientModeID));
                               gFFI.chatModel.toggleChatOverlay();
                             },
                           )
@@ -367,131 +400,20 @@ class _RemotePageState extends State<RemotePage> {
                       },
                     ),
                   ]),
-          IconButton(
-              color: Colors.white,
-              icon: Icon(Icons.expand_more),
-              onPressed: () {
-                setState(() => _showBar = !_showBar);
-              }),
+          Obx(() => IconButton(
+                color: Colors.white,
+                icon: Icon(Icons.expand_more),
+                onPressed: gFFI.ffiModel.waitForFirstImage.isTrue
+                    ? null
+                    : () {
+                        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
-
-  Offset _cacheLongPressPosition = Offset(0, 0);
-  Widget getBodyForMobileWithGesture() {
-    final touchMode = gFFI.ffiModel.touchMode;
-    return getMixinGestureDetector(
-        child: getBodyForMobile(),
-        onTapUp: (d) {
-          if (touchMode) {
-            gFFI.cursorModel.move(d.localPosition.dx, d.localPosition.dy);
-            inputModel.tap(MouseButtons.left);
-          } else {
-            inputModel.tap(MouseButtons.left);
-          }
-        },
-        onDoubleTapDown: (d) {
-          if (touchMode) {
-            gFFI.cursorModel.move(d.localPosition.dx, d.localPosition.dy);
-          }
-        },
-        onDoubleTap: () {
-          inputModel.tap(MouseButtons.left);
-          inputModel.tap(MouseButtons.left);
-        },
-        onLongPressDown: (d) {
-          if (touchMode) {
-            gFFI.cursorModel.move(d.localPosition.dx, d.localPosition.dy);
-            _cacheLongPressPosition = d.localPosition;
-          }
-        },
-        onLongPress: () {
-          if (touchMode) {
-            gFFI.cursorModel
-                .move(_cacheLongPressPosition.dx, _cacheLongPressPosition.dy);
-          }
-          inputModel.tap(MouseButtons.right);
-        },
-        onDoubleFinerTap: (d) {
-          if (!touchMode) {
-            inputModel.tap(MouseButtons.right);
-          }
-        },
-        onHoldDragStart: (d) {
-          if (!touchMode) {
-            inputModel.sendMouse('down', MouseButtons.left);
-          }
-        },
-        onHoldDragUpdate: (d) {
-          if (!touchMode) {
-            gFFI.cursorModel.updatePan(d.delta.dx, d.delta.dy, touchMode);
-          }
-        },
-        onHoldDragEnd: (_) {
-          if (!touchMode) {
-            inputModel.sendMouse('up', MouseButtons.left);
-          }
-        },
-        onOneFingerPanStart: (d) {
-          if (touchMode) {
-            gFFI.cursorModel.move(d.localPosition.dx, d.localPosition.dy);
-            inputModel.sendMouse('down', MouseButtons.left);
-          } else {
-            final offset = gFFI.cursorModel.offset;
-            final cursorX = offset.dx;
-            final cursorY = offset.dy;
-            final visible =
-                gFFI.cursorModel.getVisibleRect().inflate(1); // extend edges
-            final size = MediaQueryData.fromWindow(ui.window).size;
-            if (!visible.contains(Offset(cursorX, cursorY))) {
-              gFFI.cursorModel.move(size.width / 2, size.height / 2);
-            }
-          }
-        },
-        onOneFingerPanUpdate: (d) {
-          gFFI.cursorModel.updatePan(d.delta.dx, d.delta.dy, touchMode);
-        },
-        onOneFingerPanEnd: (d) {
-          if (touchMode) {
-            inputModel.sendMouse('up', MouseButtons.left);
-          }
-        },
-        // scale + pan event
-        onTwoFingerScaleUpdate: (d) {
-          gFFI.canvasModel.updateScale(d.scale / _scale);
-          _scale = d.scale;
-          gFFI.canvasModel.panX(d.focalPointDelta.dx);
-          gFFI.canvasModel.panY(d.focalPointDelta.dy);
-        },
-        onTwoFingerScaleEnd: (d) {
-          _scale = 1;
-          bind.sessionSetViewStyle(sessionId: sessionId, value: "");
-        },
-        onThreeFingerVerticalDragUpdate: gFFI.ffiModel.isPeerAndroid
-            ? null
-            : (d) {
-                _mouseScrollIntegral += d.delta.dy / 4;
-                if (_mouseScrollIntegral > 1) {
-                  inputModel.scroll(1);
-                  _mouseScrollIntegral = 0;
-                } else if (_mouseScrollIntegral < -1) {
-                  inputModel.scroll(-1);
-                  _mouseScrollIntegral = 0;
-                }
-              });
-  }
-
   Widget getBodyForMobile() {
     final keyboardIsVisible = keyboardVisibilityController.isVisible;
     return Container(
@@ -579,7 +501,7 @@ class _RemotePageState extends State<RemotePage> {
                   gFFI.ffiModel.toggleTouchMode();
                   final v = gFFI.ffiModel.touchMode ? 'Y' : '';
                   bind.sessionPeerOption(
-                      sessionId: sessionId, name: "touch", value: v);
+                      sessionId: sessionId, name: "touch-mode", value: v);
                 })));
   }
 
diff --git a/flutter/lib/mobile/pages/server_page.dart b/flutter/lib/mobile/pages/server_page.dart
index 8eadcaa2b..a8255180b 100644
--- a/flutter/lib/mobile/pages/server_page.dart
+++ b/flutter/lib/mobile/pages/server_page.dart
@@ -3,6 +3,7 @@ import 'dart:async';
 import 'package:flutter/material.dart';
 import 'package:flutter/services.dart';
 import 'package:flutter_hbb/mobile/widgets/dialog.dart';
+import 'package:flutter_hbb/models/chat_model.dart';
 import 'package:get/get.dart';
 import 'package:provider/provider.dart';
 
@@ -419,14 +420,16 @@ class ConnectionManager extends StatelessWidget {
                               ? const SizedBox.shrink()
                               : IconButton(
                                   onPressed: () {
-                                    gFFI.chatModel.changeCurrentID(client.id);
+                                    gFFI.chatModel.changeCurrentKey(
+                                        MessageKey(client.peerId, client.id));
                                     final bar = navigationBarKey.currentWidget;
                                     if (bar != null) {
                                       bar as BottomNavigationBar;
                                       bar.onTap!(1);
                                     }
                                   },
-                                  icon: const Icon(Icons.chat)))
+                                  icon: unreadTopRightBuilder(
+                                      client.unreadChatMessageCount)))
                     ],
                   ),
                   client.authorized
diff --git a/flutter/lib/mobile/pages/settings_page.dart b/flutter/lib/mobile/pages/settings_page.dart
index bdaa42ef9..1cdfa9609 100644
--- a/flutter/lib/mobile/pages/settings_page.dart
+++ b/flutter/lib/mobile/pages/settings_page.dart
@@ -2,11 +2,12 @@ import 'dart:async';
 import 'dart:convert';
 
 import 'package:flutter/material.dart';
-import 'package:flutter/services.dart';
+import 'package:flutter_hbb/common/widgets/setting_widgets.dart';
 import 'package:get/get.dart';
 import 'package:provider/provider.dart';
 import 'package:settings_ui/settings_ui.dart';
 import 'package:url_launcher/url_launcher.dart';
+import 'package:url_launcher/url_launcher_string.dart';
 
 import '../../common.dart';
 import '../../common/widgets/dialog.dart';
@@ -242,7 +243,7 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
         },
       ),
       SettingsTile.switchTile(
-        title: Text('${translate('Adaptive Bitrate')} (beta)'),
+        title: Text('${translate('Adaptive bitrate')} (beta)'),
         initialValue: _enableAbr,
         onToggle: (v) async {
           await bind.mainSetOption(key: "enable-abr", value: v ? "" : "N");
@@ -383,7 +384,7 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
         SettingsSection(
           title: Text(translate('Account')),
           tiles: [
-            SettingsTile.navigation(
+            SettingsTile(
               title: Obx(() => Text(gFFI.userModel.userName.value.isEmpty
                   ? translate('Login')
                   : '${translate('Logout')} (${gFFI.userModel.userName.value})')),
@@ -392,26 +393,26 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
                 if (gFFI.userModel.userName.value.isEmpty) {
                   loginDialog();
                 } else {
-                  gFFI.userModel.logOut();
+                  logOutConfirmDialog();
                 }
               },
             ),
           ],
         ),
         SettingsSection(title: Text(translate("Settings")), tiles: [
-          SettingsTile.navigation(
+          SettingsTile(
               title: Text(translate('ID/Relay Server')),
               leading: Icon(Icons.cloud),
               onPressed: (context) {
                 showServerSettings(gFFI.dialogManager);
               }),
-          SettingsTile.navigation(
+          SettingsTile(
               title: Text(translate('Language')),
               leading: Icon(Icons.translate),
               onPressed: (context) {
                 showLanguageSettings(gFFI.dialogManager);
               }),
-          SettingsTile.navigation(
+          SettingsTile(
             title: Text(translate(
                 Theme.of(context).brightness == Brightness.light
                     ? 'Dark Theme'
@@ -424,45 +425,50 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
             },
           )
         ]),
-        SettingsSection(
-          title: Text(translate("Recording")),
-          tiles: [
-            SettingsTile.switchTile(
-              title: Text(translate('Automatically record incoming sessions')),
-              leading: Icon(Icons.videocam),
-              description: FutureBuilder(
-                  builder: (ctx, data) => Offstage(
-                      offstage: !data.hasData,
-                      child: Text("${translate("Directory")}: ${data.data}")),
-                  future: bind.mainDefaultVideoSaveDirectory()),
-              initialValue: _autoRecordIncomingSession,
-              onToggle: (v) async {
-                await bind.mainSetOption(
-                    key: "allow-auto-record-incoming",
-                    value: bool2option("allow-auto-record-incoming", v));
-                final newValue = option2bool(
-                    'allow-auto-record-incoming',
-                    await bind.mainGetOption(
-                        key: 'allow-auto-record-incoming'));
-                setState(() {
-                  _autoRecordIncomingSession = newValue;
-                });
-              },
-            ),
-          ],
-        ),
-        SettingsSection(
-          title: Text(translate("Share Screen")),
-          tiles: shareScreenTiles,
-        ),
-        SettingsSection(
-          title: Text(translate("Enhancements")),
-          tiles: enhancementsTiles,
-        ),
+        if (isAndroid)
+          SettingsSection(
+            title: Text(translate("Recording")),
+            tiles: [
+              SettingsTile.switchTile(
+                title:
+                    Text(translate('Automatically record incoming sessions')),
+                leading: Icon(Icons.videocam),
+                description: FutureBuilder(
+                    builder: (ctx, data) => Offstage(
+                        offstage: !data.hasData,
+                        child: Text("${translate("Directory")}: ${data.data}")),
+                    future: bind.mainDefaultVideoSaveDirectory()),
+                initialValue: _autoRecordIncomingSession,
+                onToggle: (v) async {
+                  await bind.mainSetOption(
+                      key: "allow-auto-record-incoming",
+                      value: bool2option("allow-auto-record-incoming", v));
+                  final newValue = option2bool(
+                      'allow-auto-record-incoming',
+                      await bind.mainGetOption(
+                          key: 'allow-auto-record-incoming'));
+                  setState(() {
+                    _autoRecordIncomingSession = newValue;
+                  });
+                },
+              ),
+            ],
+          ),
+        if (isAndroid)
+          SettingsSection(
+            title: Text(translate("Share Screen")),
+            tiles: shareScreenTiles,
+          ),
+        defaultDisplaySection(),
+        if (isAndroid)
+          SettingsSection(
+            title: Text(translate("Enhancements")),
+            tiles: enhancementsTiles,
+          ),
         SettingsSection(
           title: Text(translate("About")),
           tiles: [
-            SettingsTile.navigation(
+            SettingsTile(
                 onPressed: (context) async {
                   if (await canLaunchUrl(Uri.parse(url))) {
                     await launchUrl(Uri.parse(url));
@@ -477,21 +483,28 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
                       )),
                 ),
                 leading: Icon(Icons.info)),
-            SettingsTile.navigation(
+            SettingsTile(
                 title: Text(translate("Build Date")),
                 value: Padding(
                   padding: EdgeInsets.symmetric(vertical: 8),
                   child: Text(_buildDate),
                 ),
                 leading: Icon(Icons.query_builder)),
-            SettingsTile.navigation(
-                onPressed: (context) => onCopyFingerprint(_fingerprint),
-                title: Text(translate("Fingerprint")),
-                value: Padding(
-                  padding: EdgeInsets.symmetric(vertical: 8),
-                  child: Text(_fingerprint),
-                ),
-                leading: Icon(Icons.fingerprint)),
+            if (isAndroid)
+              SettingsTile(
+                  onPressed: (context) => onCopyFingerprint(_fingerprint),
+                  title: Text(translate("Fingerprint")),
+                  value: Padding(
+                    padding: EdgeInsets.symmetric(vertical: 8),
+                    child: Text(_fingerprint),
+                  ),
+                  leading: Icon(Icons.fingerprint)),
+            SettingsTile(
+              title: Text(translate("Privacy Statement")),
+              onPressed: (context) =>
+                  launchUrlString('https://rustdesk.com/privacy.html'),
+              leading: Icon(Icons.privacy_tip),
+            )
           ],
         ),
       ],
@@ -508,6 +521,23 @@ class _SettingsState extends State<SettingsPage> with WidgetsBindingObserver {
     }
     return true;
   }
+
+  defaultDisplaySection() {
+    return SettingsSection(
+      title: Text(translate("Display Settings")),
+      tiles: [
+        SettingsTile(
+            title: Text(translate('Display Settings')),
+            leading: Icon(Icons.desktop_windows_outlined),
+            trailing: Icon(Icons.arrow_forward_ios),
+            onPressed: (context) {
+              Navigator.push(context, MaterialPageRoute(builder: (context) {
+                return _DisplayPage();
+              }));
+            })
+      ],
+    );
+  }
 }
 
 void showServerSettings(OverlayDialogManager dialogManager) async {
@@ -618,3 +648,181 @@ class ScanButton extends StatelessWidget {
     );
   }
 }
+
+class _DisplayPage extends StatefulWidget {
+  const _DisplayPage({super.key});
+
+  @override
+  State<_DisplayPage> createState() => __DisplayPageState();
+}
+
+class __DisplayPageState extends State<_DisplayPage> {
+  @override
+  Widget build(BuildContext context) {
+    final Map codecsJson = jsonDecode(bind.mainSupportedHwdecodings());
+    final h264 = codecsJson['h264'] ?? false;
+    final h265 = codecsJson['h265'] ?? false;
+    var codecList = [
+      _RadioEntry('Auto', 'auto'),
+      _RadioEntry('VP8', 'vp8'),
+      _RadioEntry('VP9', 'vp9'),
+      _RadioEntry('AV1', 'av1'),
+      if (h264) _RadioEntry('H264', 'h264'),
+      if (h265) _RadioEntry('H265', 'h265')
+    ];
+    RxBool showCustomImageQuality = false.obs;
+    return Scaffold(
+      appBar: AppBar(
+        leading: IconButton(
+            onPressed: () => Navigator.pop(context),
+            icon: Icon(Icons.arrow_back_ios)),
+        title: Text(translate('Display Settings')),
+        centerTitle: true,
+      ),
+      body: SettingsList(sections: [
+        SettingsSection(
+          tiles: [
+            _getPopupDialogRadioEntry(
+              title: 'Default View Style',
+              list: [
+                _RadioEntry('Scale original', kRemoteViewStyleOriginal),
+                _RadioEntry('Scale adaptive', kRemoteViewStyleAdaptive)
+              ],
+              getter: () => bind.mainGetUserDefaultOption(key: 'view_style'),
+              asyncSetter: (value) async {
+                await bind.mainSetUserDefaultOption(
+                    key: 'view_style', value: value);
+              },
+            ),
+            _getPopupDialogRadioEntry(
+              title: 'Default Image Quality',
+              list: [
+                _RadioEntry('Good image quality', kRemoteImageQualityBest),
+                _RadioEntry('Balanced', kRemoteImageQualityBalanced),
+                _RadioEntry('Optimize reaction time', kRemoteImageQualityLow),
+                _RadioEntry('Custom', kRemoteImageQualityCustom),
+              ],
+              getter: () {
+                final v = bind.mainGetUserDefaultOption(key: 'image_quality');
+                showCustomImageQuality.value = v == kRemoteImageQualityCustom;
+                return v;
+              },
+              asyncSetter: (value) async {
+                await bind.mainSetUserDefaultOption(
+                    key: 'image_quality', value: value);
+                showCustomImageQuality.value =
+                    value == kRemoteImageQualityCustom;
+              },
+              tail: customImageQualitySetting(),
+              showTail: showCustomImageQuality,
+              notCloseValue: kRemoteImageQualityCustom,
+            ),
+            _getPopupDialogRadioEntry(
+              title: 'Default Codec',
+              list: codecList,
+              getter: () =>
+                  bind.mainGetUserDefaultOption(key: 'codec-preference'),
+              asyncSetter: (value) async {
+                await bind.mainSetUserDefaultOption(
+                    key: 'codec-preference', value: value);
+              },
+            ),
+          ],
+        ),
+        SettingsSection(
+          title: Text(translate('Other Default Options')),
+          tiles: [
+            otherRow('Show remote cursor', 'show_remote_cursor'),
+            otherRow('Show quality monitor', 'show_quality_monitor'),
+            otherRow('Mute', 'disable_audio'),
+            otherRow('Disable clipboard', 'disable_clipboard'),
+            otherRow('Lock after session end', 'lock_after_session_end'),
+            otherRow('Privacy mode', 'privacy_mode'),
+            otherRow('Touch mode', 'touch-mode'),
+          ],
+        ),
+      ]),
+    );
+  }
+
+  otherRow(String label, String key) {
+    final value = bind.mainGetUserDefaultOption(key: key) == 'Y';
+    return SettingsTile.switchTile(
+      initialValue: value,
+      title: Text(translate(label)),
+      onToggle: (b) async {
+        await bind.mainSetUserDefaultOption(key: key, value: b ? 'Y' : '');
+        setState(() {});
+      },
+    );
+  }
+}
+
+class _RadioEntry {
+  final String label;
+  final String value;
+  _RadioEntry(this.label, this.value);
+}
+
+typedef _RadioEntryGetter = String Function();
+typedef _RadioEntrySetter = Future<void> Function(String);
+
+_getPopupDialogRadioEntry({
+  required String title,
+  required List<_RadioEntry> list,
+  required _RadioEntryGetter getter,
+  required _RadioEntrySetter asyncSetter,
+  Widget? tail,
+  RxBool? showTail,
+  String? notCloseValue,
+}) {
+  RxString groupValue = ''.obs;
+  RxString valueText = ''.obs;
+
+  init() {
+    groupValue.value = getter();
+    final e = list.firstWhereOrNull((e) => e.value == groupValue.value);
+    if (e != null) {
+      valueText.value = e.label;
+    }
+  }
+
+  init();
+
+  void showDialog() async {
+    gFFI.dialogManager.show((setState, close, context) {
+      onChanged(String? value) async {
+        if (value == null) return;
+        await asyncSetter(value);
+        init();
+        if (value != notCloseValue) {
+          close();
+        }
+      }
+
+      return CustomAlertDialog(
+          content: Obx(
+        () => Column(children: [
+          ...list
+              .map((e) => getRadio(Text(translate(e.label)), e.value,
+                  groupValue.value, (String? value) => onChanged(value)))
+              .toList(),
+          Offstage(
+            offstage:
+                !(tail != null && showTail != null && showTail.value == true),
+            child: tail,
+          ),
+        ]),
+      ));
+    }, backDismiss: true, clickMaskDismiss: true);
+  }
+
+  return SettingsTile(
+    title: Text(translate(title)),
+    onPressed: (context) => showDialog(),
+    value: Padding(
+      padding: EdgeInsets.symmetric(vertical: 8),
+      child: Obx(() => Text(translate(valueText.value))),
+    ),
+  );
+}
diff --git a/flutter/lib/mobile/widgets/dialog.dart b/flutter/lib/mobile/widgets/dialog.dart
index 8c321e785..3e745ecce 100644
--- a/flutter/lib/mobile/widgets/dialog.dart
+++ b/flutter/lib/mobile/widgets/dialog.dart
@@ -1,6 +1,6 @@
 import 'dart:async';
-import 'dart:convert';
 import 'package:flutter/material.dart';
+import 'package:flutter_hbb/common/widgets/setting_widgets.dart';
 import 'package:get/get.dart';
 
 import '../../common.dart';
@@ -147,59 +147,72 @@ void setTemporaryPasswordLengthDialog(
 
 void showServerSettingsWithValue(
     ServerConfig serverConfig, OverlayDialogManager dialogManager) async {
-  Map<String, dynamic> oldOptions = jsonDecode(await bind.mainGetOptions());
-  final oldCfg = ServerConfig.fromOptions(oldOptions);
-
   var isInProgress = false;
   final idCtrl = TextEditingController(text: serverConfig.idServer);
   final relayCtrl = TextEditingController(text: serverConfig.relayServer);
   final apiCtrl = TextEditingController(text: serverConfig.apiServer);
   final keyCtrl = TextEditingController(text: serverConfig.key);
 
-  String? idServerMsg;
-  String? relayServerMsg;
-  String? apiServerMsg;
+  RxString idServerMsg = ''.obs;
+  RxString relayServerMsg = ''.obs;
+  RxString apiServerMsg = ''.obs;
+
+  final controllers = [idCtrl, relayCtrl, apiCtrl, keyCtrl];
+  final errMsgs = [
+    idServerMsg,
+    relayServerMsg,
+    apiServerMsg,
+  ];
 
   dialogManager.show((setState, close, context) {
-    Future<bool> validate() async {
-      if (idCtrl.text != oldCfg.idServer) {
-        final res = await validateAsync(idCtrl.text);
-        setState(() => idServerMsg = res);
-        if (idServerMsg != null) return false;
-      }
-      if (relayCtrl.text != oldCfg.relayServer) {
-        relayServerMsg = await validateAsync(relayCtrl.text);
-        if (relayServerMsg != null) return false;
-      }
-      if (apiCtrl.text != oldCfg.apiServer) {
-        if (apiServerMsg != null) return false;
-      }
-      return true;
+    Future<bool> submit() async {
+      setState(() {
+        isInProgress = true;
+      });
+      bool ret = await setServerConfig(
+          controllers,
+          errMsgs,
+          ServerConfig(
+              idServer: idCtrl.text.trim(),
+              relayServer: relayCtrl.text.trim(),
+              apiServer: apiCtrl.text.trim(),
+              key: keyCtrl.text.trim()));
+      setState(() {
+        isInProgress = false;
+      });
+      return ret;
     }
 
     return CustomAlertDialog(
-      title: Text(translate('ID/Relay Server')),
+      title: Row(
+        children: [
+          Expanded(child: Text(translate('ID/Relay Server'))),
+          ...ServerConfigImportExportWidgets(controllers, errMsgs),
+        ],
+      ),
       content: Form(
-          child: Column(
+          child: Obx(() => Column(
               mainAxisSize: MainAxisSize.min,
               children: <Widget>[
                     TextFormField(
                       controller: idCtrl,
                       decoration: InputDecoration(
                           labelText: translate('ID Server'),
-                          errorText: idServerMsg),
+                          errorText: idServerMsg.value.isEmpty
+                              ? null
+                              : idServerMsg.value),
+                    )
+                  ] +
+                  [
+                    TextFormField(
+                      controller: relayCtrl,
+                      decoration: InputDecoration(
+                          labelText: translate('Relay Server'),
+                          errorText: relayServerMsg.value.isEmpty
+                              ? null
+                              : relayServerMsg.value),
                     )
                   ] +
-                  (isAndroid
-                      ? [
-                          TextFormField(
-                            controller: relayCtrl,
-                            decoration: InputDecoration(
-                                labelText: translate('Relay Server'),
-                                errorText: relayServerMsg),
-                          )
-                        ]
-                      : []) +
                   [
                     TextFormField(
                       controller: apiCtrl,
@@ -214,7 +227,7 @@ void showServerSettingsWithValue(
                             return translate("invalid_http");
                           }
                         }
-                        return apiServerMsg;
+                        return null;
                       },
                     ),
                     TextFormField(
@@ -223,10 +236,9 @@ void showServerSettingsWithValue(
                         labelText: 'Key',
                       ),
                     ),
-                    Offstage(
-                        offstage: !isInProgress,
-                        child: LinearProgressIndicator())
-                  ])),
+                    // NOT use Offstage to wrap LinearProgressIndicator
+                    if (isInProgress) const LinearProgressIndicator(),
+                  ]))),
       actions: [
         dialogButton('Cancel', onPressed: () {
           close();
@@ -234,35 +246,12 @@ void showServerSettingsWithValue(
         dialogButton(
           'OK',
           onPressed: () async {
-            setState(() {
-              idServerMsg = null;
-              relayServerMsg = null;
-              apiServerMsg = null;
-              isInProgress = true;
-            });
-            if (await validate()) {
-              if (idCtrl.text != oldCfg.idServer) {
-                if (oldCfg.idServer.isNotEmpty) {
-                  await gFFI.userModel.logOut();
-                }
-                bind.mainSetOption(
-                    key: "custom-rendezvous-server", value: idCtrl.text);
-              }
-              if (relayCtrl.text != oldCfg.relayServer) {
-                bind.mainSetOption(key: "relay-server", value: relayCtrl.text);
-              }
-              if (keyCtrl.text != oldCfg.key) {
-                bind.mainSetOption(key: "key", value: keyCtrl.text);
-              }
-              if (apiCtrl.text != oldCfg.apiServer) {
-                bind.mainSetOption(key: "api-server", value: apiCtrl.text);
-              }
+            if (await submit()) {
               close();
               showToast(translate('Successful'));
+            } else {
+              showToast(translate('Failed'));
             }
-            setState(() {
-              isInProgress = false;
-            });
           },
         ),
       ],
diff --git a/flutter/lib/models/ab_model.dart b/flutter/lib/models/ab_model.dart
index 31078dd8a..cbb7f7471 100644
--- a/flutter/lib/models/ab_model.dart
+++ b/flutter/lib/models/ab_model.dart
@@ -1,8 +1,11 @@
+import 'dart:async';
 import 'dart:convert';
+import 'dart:io';
 
 import 'package:flutter/material.dart';
 import 'package:flutter_hbb/models/model.dart';
 import 'package:flutter_hbb/models/peer_model.dart';
+import 'package:flutter_hbb/models/peer_tab_model.dart';
 import 'package:flutter_hbb/models/platform_model.dart';
 import 'package:get/get.dart';
 import 'package:bot_toast/bot_toast.dart';
@@ -10,67 +13,114 @@ import 'package:http/http.dart' as http;
 
 import '../common.dart';
 
+final syncAbOption = 'sync-ab-with-recent-sessions';
+bool shouldSyncAb() {
+  return bind.mainGetLocalOption(key: syncAbOption).isNotEmpty;
+}
+
+final sortAbTagsOption = 'sync-ab-tags';
+bool shouldSortTags() {
+  return bind.mainGetLocalOption(key: sortAbTagsOption).isNotEmpty;
+}
+
 class AbModel {
   final abLoading = false.obs;
-  final abError = "".obs;
+  final pullError = "".obs;
+  final pushError = "".obs;
   final tags = [].obs;
+  final RxMap<String, int> tagColors = Map<String, int>.fromEntries([]).obs;
   final peers = List<Peer>.empty(growable: true).obs;
+  final sortTags = shouldSortTags().obs;
+  final retrying = false.obs;
+  bool get emtpy => peers.isEmpty && tags.isEmpty;
 
   final selectedTags = List<String>.empty(growable: true).obs;
   var initialized = false;
+  var licensedDevices = 0;
+  var _syncAllFromRecent = true;
+  var _syncFromRecentLock = false;
+  var _timerCounter = 0;
+  var _cacheLoadOnceFlag = false;
 
   WeakReference<FFI> parent;
 
-  AbModel(this.parent);
+  AbModel(this.parent) {
+    if (desktopType == DesktopType.main) {
+      Timer.periodic(Duration(milliseconds: 500), (timer) async {
+        if (_timerCounter++ % 6 == 0) {
+          if (!gFFI.userModel.isLogin) return;
+          syncFromRecent();
+        }
+      });
+    }
+  }
 
   Future<void> pullAb({force = true, quiet = false}) async {
-    if (gFFI.userModel.userName.isEmpty) return;
+    debugPrint("pullAb, force:$force, quiet:$quiet");
+    if (!gFFI.userModel.isLogin) return;
     if (abLoading.value) return;
     if (!force && initialized) return;
+    if (pushError.isNotEmpty) {
+      try {
+        // push to retry
+        await pushAb(toastIfFail: false, toastIfSucc: false);
+      } catch (_) {}
+    }
     if (!quiet) {
       abLoading.value = true;
-      abError.value = "";
+      pullError.value = "";
     }
-    final api = "${await bind.mainGetApiServer()}/api/ab/get";
+    final api = "${await bind.mainGetApiServer()}/api/ab";
+    int? statusCode;
     try {
       var authHeaders = getHttpHeaders();
       authHeaders['Content-Type'] = "application/json";
-      final resp = await http.post(Uri.parse(api), headers: authHeaders);
-      if (resp.body.isNotEmpty && resp.body.toLowerCase() != "null") {
-        Map<String, dynamic> json = jsonDecode(resp.body);
+      authHeaders['Accept-Encoding'] = "gzip";
+      final resp = await http.get(Uri.parse(api), headers: authHeaders);
+      statusCode = resp.statusCode;
+      if (resp.body.toLowerCase() == "null") {
+        // normal reply, emtpy ab return null
+        tags.clear();
+        tagColors.clear();
+        peers.clear();
+      } else if (resp.body.isNotEmpty) {
+        Map<String, dynamic> json =
+            _jsonDecodeResp(utf8.decode(resp.bodyBytes), resp.statusCode);
         if (json.containsKey('error')) {
-          abError.value = json['error'];
+          throw json['error'];
         } else if (json.containsKey('data')) {
+          try {
+            gFFI.abModel.licensedDevices = json['licensed_devices'];
+            // ignore: empty_catches
+          } catch (e) {}
           final data = jsonDecode(json['data']);
           if (data != null) {
-            tags.clear();
-            peers.clear();
-            if (data['tags'] is List) {
-              tags.value = data['tags'];
-            }
-            if (data['peers'] is List) {
-              for (final peer in data['peers']) {
-                peers.add(Peer.fromJson(peer));
-              }
-            }
+            _deserialize(data);
+            _saveCache(); // save on success
           }
         }
       }
     } catch (err) {
-      abError.value = err.toString();
+      if (!quiet) {
+        pullError.value =
+            '${translate('pull_ab_failed_tip')}: ${translate(err.toString())}';
+        if (gFFI.peerTabModel.currentTab != PeerTabIndex.ab.index) {
+          BotToast.showText(contentColor: Colors.red, text: pullError.value);
+        }
+      }
     } finally {
       abLoading.value = false;
       initialized = true;
+      _syncAllFromRecent = true;
+      _timerCounter = 0;
+      if (pullError.isNotEmpty) {
+        if (statusCode == 401) {
+          gFFI.userModel.reset(clearAbCache: true);
+        }
+      }
     }
   }
 
-  Future<void> reset() async {
-    await bind.mainSetLocalOption(key: "selected-tags", value: '');
-    tags.clear();
-    peers.clear();
-    initialized = false;
-  }
-
   void addId(String id, String alias, List<dynamic> tags) {
     if (idContainBy(id)) {
       return;
@@ -83,9 +133,34 @@ class AbModel {
     peers.add(peer);
   }
 
+  bool isFull(bool warn) {
+    final res = licensedDevices > 0 && peers.length >= licensedDevices;
+    if (res && warn) {
+      BotToast.showText(
+          contentColor: Colors.red, text: translate("exceed_max_devices"));
+    }
+    return res;
+  }
+
   void addPeer(Peer peer) {
-    peers.removeWhere((e) => e.id == peer.id);
-    peers.add(peer);
+    final index = peers.indexWhere((e) => e.id == peer.id);
+    if (index >= 0) {
+      merge(peer, peers[index]);
+    } else {
+      peers.add(peer);
+    }
+  }
+
+  bool addPeers(List<Peer> ps) {
+    bool allAdded = true;
+    for (var p in ps) {
+      if (!isFull(false)) {
+        addPeer(p);
+      } else {
+        allAdded = false;
+      }
+    }
+    return allAdded;
   }
 
   void addTag(String tag) async {
@@ -103,20 +178,101 @@ class AbModel {
     it.first.tags = tags;
   }
 
-  Future<void> pushAb() async {
-    final api = "${await bind.mainGetApiServer()}/api/ab";
-    var authHeaders = getHttpHeaders();
-    authHeaders['Content-Type'] = "application/json";
-    final peersJsonData = peers.map((e) => e.toJson()).toList();
-    final body = jsonEncode({
-      "data": jsonEncode({"tags": tags, "peers": peersJsonData})
-    });
+  void changeTagForPeers(List<String> ids, List<dynamic> tags) {
+    peers.map((e) {
+      if (ids.contains(e.id)) {
+        e.tags = tags;
+      }
+    }).toList();
+  }
+
+  void changeAlias({required String id, required String alias}) {
+    final it = peers.where((element) => element.id == id);
+    if (it.isEmpty) {
+      return;
+    }
+    it.first.alias = alias;
+  }
+
+  bool changePassword(String id, String hash) {
+    final it = peers.where((element) => element.id == id);
+    if (it.isNotEmpty) {
+      if (it.first.hash != hash) {
+        it.first.hash = hash;
+        return true;
+      }
+    }
+    return false;
+  }
+
+  Future<bool> pushAb(
+      {bool toastIfFail = true,
+      bool toastIfSucc = true,
+      bool isRetry = false}) async {
+    debugPrint(
+        "pushAb: toastIfFail:$toastIfFail, toastIfSucc:$toastIfSucc, isRetry:$isRetry");
+    if (!gFFI.userModel.isLogin) return false;
+    pushError.value = '';
+    if (isRetry) retrying.value = true;
+    DateTime startTime = DateTime.now();
+    bool ret = false;
     try {
-      await http.post(Uri.parse(api), headers: authHeaders, body: body);
-      await pullAb(quiet: true);
+      // avoid double pushes in a row
+      _syncAllFromRecent = true;
+      await syncFromRecent(push: false);
+      //https: //stackoverflow.com/questions/68249333/flutter-getx-updating-item-in-children-list-is-not-reactive
+      peers.refresh();
+      final api = "${await bind.mainGetApiServer()}/api/ab";
+      var authHeaders = getHttpHeaders();
+      authHeaders['Content-Type'] = "application/json";
+      final body = jsonEncode({"data": jsonEncode(_serialize())});
+      http.Response resp;
+      // support compression
+      if (licensedDevices > 0 && body.length > 1024) {
+        authHeaders['Content-Encoding'] = "gzip";
+        resp = await http.post(Uri.parse(api),
+            headers: authHeaders, body: GZipCodec().encode(utf8.encode(body)));
+      } else {
+        resp =
+            await http.post(Uri.parse(api), headers: authHeaders, body: body);
+      }
+      if (resp.statusCode == 200 &&
+          (resp.body.isEmpty || resp.body.toLowerCase() == 'null')) {
+        ret = true;
+        _saveCache();
+      } else {
+        Map<String, dynamic> json = _jsonDecodeResp(resp.body, resp.statusCode);
+        if (json.containsKey('error')) {
+          throw json['error'];
+        } else if (resp.statusCode == 200) {
+          ret = true;
+          _saveCache();
+        } else {
+          throw 'HTTP ${resp.statusCode}';
+        }
+      }
     } catch (e) {
-      BotToast.showText(contentColor: Colors.red, text: e.toString());
-    } finally {}
+      pushError.value =
+          '${translate('push_ab_failed_tip')}: ${translate(e.toString())}';
+    }
+    _syncAllFromRecent = true;
+    if (isRetry) {
+      var ms =
+          (Duration(milliseconds: 200) - DateTime.now().difference(startTime))
+              .inMilliseconds;
+      ms = ms > 0 ? ms : 0;
+      Future.delayed(Duration(milliseconds: ms), () {
+        retrying.value = false;
+      });
+    }
+
+    if (!ret && toastIfFail) {
+      BotToast.showText(contentColor: Colors.red, text: pushError.value);
+    }
+    if (ret && toastIfSucc) {
+      showToast(translate('Successful'));
+    }
+    return ret;
   }
 
   Peer? find(String id) {
@@ -135,9 +291,14 @@ class AbModel {
     peers.removeWhere((element) => element.id == id);
   }
 
+  void deletePeers(List<String> ids) {
+    peers.removeWhere((e) => ids.contains(e.id));
+  }
+
   void deleteTag(String tag) {
     gFFI.abModel.selectedTags.remove(tag);
     tags.removeWhere((element) => element == tag);
+    tagColors.remove(tag);
     for (var peer in peers) {
       if (peer.tags.isEmpty) {
         continue;
@@ -148,6 +309,38 @@ class AbModel {
     }
   }
 
+  void renameTag(String oldTag, String newTag) {
+    if (tags.contains(newTag)) return;
+    tags.value = tags.map((e) {
+      if (e == oldTag) {
+        return newTag;
+      } else {
+        return e;
+      }
+    }).toList();
+    selectedTags.value = selectedTags.map((e) {
+      if (e == oldTag) {
+        return newTag;
+      } else {
+        return e;
+      }
+    }).toList();
+    for (var peer in peers) {
+      peer.tags = peer.tags.map((e) {
+        if (e == oldTag) {
+          return newTag;
+        } else {
+          return e;
+        }
+      }).toList();
+    }
+    int? oldColor = tagColors[oldTag];
+    if (oldColor != null) {
+      tagColors.remove(oldTag);
+      tagColors.addAll({newTag: oldColor});
+    }
+  }
+
   void unsetSelectedTags() {
     selectedTags.clear();
   }
@@ -161,28 +354,211 @@ class AbModel {
     }
   }
 
-  Future<void> setPeerAlias(String id, String value) async {
-    final it = peers.where((p0) => p0.id == id);
-    if (it.isNotEmpty) {
-      it.first.alias = value;
-      await pushAb();
+  Color getTagColor(String tag) {
+    int? colorValue = tagColors[tag];
+    if (colorValue != null) {
+      return Color(colorValue);
+    }
+    return str2color2(tag, existing: tagColors.values.toList());
+  }
+
+  setTagColor(String tag, Color color) {
+    if (tags.contains(tag)) {
+      tagColors[tag] = color.value;
     }
   }
 
-  Future<void> setPeerForceAlwaysRelay(String id, bool value) async {
-    final it = peers.where((p0) => p0.id == id);
-    if (it.isNotEmpty) {
-      it.first.forceAlwaysRelay = value;
-      await pushAb();
+  void merge(Peer r, Peer p) {
+    p.hash = r.hash.isEmpty ? p.hash : r.hash;
+    p.username = r.username.isEmpty ? p.username : r.username;
+    p.hostname = r.hostname.isEmpty ? p.hostname : r.hostname;
+    p.alias = p.alias.isEmpty ? r.alias : p.alias;
+    p.forceAlwaysRelay = r.forceAlwaysRelay;
+    p.rdpPort = r.rdpPort;
+    p.rdpUsername = r.rdpUsername;
+  }
+
+  Future<void> syncFromRecent({bool push = true}) async {
+    if (!_syncFromRecentLock) {
+      _syncFromRecentLock = true;
+      await _syncFromRecentWithoutLock(push: push);
+      _syncFromRecentLock = false;
     }
   }
 
-  Future<void> setRdp(String id, String port, String username) async {
-    final it = peers.where((p0) => p0.id == id);
-    if (it.isNotEmpty) {
-      it.first.rdpPort = port;
-      it.first.rdpUsername = username;
-      await pushAb();
+  Future<void> _syncFromRecentWithoutLock({bool push = true}) async {
+    bool peerSyncEqual(Peer a, Peer b) {
+      return a.hash == b.hash &&
+          a.username == b.username &&
+          a.platform == b.platform &&
+          a.hostname == b.hostname &&
+          a.alias == b.alias;
     }
+
+    Future<List<Peer>> getRecentPeers() async {
+      try {
+        List<String> filteredPeerIDs;
+        if (_syncAllFromRecent) {
+          _syncAllFromRecent = false;
+          filteredPeerIDs = [];
+        } else {
+          final new_stored_str = await bind.mainGetNewStoredPeers();
+          if (new_stored_str.isEmpty) return [];
+          filteredPeerIDs = (jsonDecode(new_stored_str) as List<dynamic>)
+              .map((e) => e.toString())
+              .toList();
+          if (filteredPeerIDs.isEmpty) return [];
+        }
+        final loadStr = await bind.mainLoadRecentPeersForAb(
+            filter: jsonEncode(filteredPeerIDs));
+        if (loadStr.isEmpty) {
+          return [];
+        }
+        List<dynamic> mapPeers = jsonDecode(loadStr);
+        List<Peer> recents = List.empty(growable: true);
+        for (var m in mapPeers) {
+          if (m is Map<String, dynamic>) {
+            recents.add(Peer.fromJson(m));
+          }
+        }
+        return recents;
+      } catch (e) {
+        debugPrint('getRecentPeers:$e');
+      }
+      return [];
+    }
+
+    try {
+      if (!shouldSyncAb()) return;
+      final recents = await getRecentPeers();
+      if (recents.isEmpty) return;
+      bool uiChanged = false;
+      bool needSync = false;
+      for (var i = 0; i < recents.length; i++) {
+        var r = recents[i];
+        var index = peers.indexWhere((e) => e.id == r.id);
+        if (index < 0) {
+          if (!isFull(false)) {
+            peers.add(r);
+            uiChanged = true;
+            needSync = true;
+          }
+        } else {
+          Peer old = Peer.copy(peers[index]);
+          merge(r, peers[index]);
+          if (!peerSyncEqual(peers[index], old)) {
+            needSync = true;
+          }
+          if (!old.equal(peers[index])) {
+            uiChanged = true;
+          }
+        }
+      }
+      // Be careful with loop calls
+      if (needSync && push) {
+        pushAb(toastIfSucc: false, toastIfFail: false);
+      } else if (uiChanged) {
+        peers.refresh();
+      }
+    } catch (e) {
+      debugPrint('syncFromRecent:$e');
+    }
+  }
+
+  _saveCache() {
+    try {
+      var m = _serialize();
+      m.addAll(<String, dynamic>{
+        "access_token": bind.mainGetLocalOption(key: 'access_token'),
+      });
+      bind.mainSaveAb(json: jsonEncode(m));
+    } catch (e) {
+      debugPrint('ab save:$e');
+    }
+  }
+
+  loadCache() async {
+    try {
+      if (_cacheLoadOnceFlag || abLoading.value) return;
+      _cacheLoadOnceFlag = true;
+      final access_token = bind.mainGetLocalOption(key: 'access_token');
+      if (access_token.isEmpty) return;
+      final cache = await bind.mainLoadAb();
+      final data = jsonDecode(cache);
+      if (data == null || data['access_token'] != access_token) return;
+      _deserialize(data);
+    } catch (e) {
+      debugPrint("load ab cache: $e");
+    }
+  }
+
+  Map<String, dynamic> _jsonDecodeResp(String body, int statusCode) {
+    try {
+      Map<String, dynamic> json = jsonDecode(body);
+      return json;
+    } catch (e) {
+      final err = body.isNotEmpty && body.length < 128 ? body : e.toString();
+      if (statusCode != 200) {
+        throw 'HTTP $statusCode, $err';
+      }
+      throw err;
+    }
+  }
+
+  Map<String, dynamic> _serialize() {
+    final peersJsonData = peers.map((e) => e.toAbUploadJson()).toList();
+    final tagColorJsonData = jsonEncode(tagColors);
+    return {
+      "tags": tags,
+      "peers": peersJsonData,
+      "tag_colors": tagColorJsonData
+    };
+  }
+
+  _deserialize(dynamic data) {
+    if (data == null) return;
+    final oldOnlineIDs = peers.where((e) => e.online).map((e) => e.id).toList();
+    tags.clear();
+    tagColors.clear();
+    peers.clear();
+    if (data['tags'] is List) {
+      tags.value = data['tags'];
+    }
+    if (data['peers'] is List) {
+      for (final peer in data['peers']) {
+        peers.add(Peer.fromJson(peer));
+      }
+    }
+    if (isFull(false)) {
+      peers.removeRange(licensedDevices, peers.length);
+    }
+    // restore online
+    peers
+        .where((e) => oldOnlineIDs.contains(e.id))
+        .map((e) => e.online = true)
+        .toList();
+    if (data['tag_colors'] is String) {
+      Map<String, dynamic> map = jsonDecode(data['tag_colors']);
+      tagColors.value = Map<String, int>.from(map);
+    }
+    // add color to tag
+    final tagsWithoutColor =
+        tags.toList().where((e) => !tagColors.containsKey(e)).toList();
+    for (var t in tagsWithoutColor) {
+      tagColors[t] = str2color2(t, existing: tagColors.values.toList()).value;
+    }
+  }
+
+  reSyncToast(Future<bool> future) {
+    if (!shouldSyncAb()) return;
+    Future.delayed(Duration.zero, () async {
+      final succ = await future;
+      if (succ) {
+        await Future.delayed(Duration(seconds: 2)); // success msg
+        BotToast.showText(
+            contentColor: Colors.lightBlue,
+            text: translate('synced_peer_readded_tip'));
+      }
+    });
   }
 }
diff --git a/flutter/lib/models/chat_model.dart b/flutter/lib/models/chat_model.dart
index 8c1e0a9a4..ed216e500 100644
--- a/flutter/lib/models/chat_model.dart
+++ b/flutter/lib/models/chat_model.dart
@@ -1,12 +1,17 @@
 import 'dart:async';
 
 import 'package:dash_chat_2/dash_chat_2.dart';
+import 'package:desktop_multi_window/desktop_multi_window.dart';
 import 'package:draggable_float_widget/draggable_float_widget.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter/services.dart';
+import 'package:flutter_hbb/common/shared_state.dart';
+import 'package:flutter_hbb/desktop/widgets/tabbar_widget.dart';
+import 'package:flutter_hbb/mobile/pages/home_page.dart';
 import 'package:flutter_hbb/models/platform_model.dart';
-import 'package:get/get_rx/src/rx_types/rx_types.dart';
+import 'package:flutter_hbb/models/state_model.dart';
 import 'package:get/get.dart';
+import 'package:uuid/uuid.dart';
 import 'package:window_manager/window_manager.dart';
 import 'package:flutter_svg/flutter_svg.dart';
 
@@ -16,6 +21,24 @@ import '../common/widgets/overlay.dart';
 import '../main.dart';
 import 'model.dart';
 
+class MessageKey {
+  final String peerId;
+  final int connId;
+  bool get isOut => connId == ChatModel.clientModeID;
+
+  MessageKey(this.peerId, this.connId);
+
+  @override
+  bool operator ==(other) {
+    return other is MessageKey &&
+        other.peerId == peerId &&
+        other.isOut == isOut;
+  }
+
+  @override
+  int get hashCode => peerId.hashCode ^ isOut.hashCode;
+}
+
 class MessageBody {
   ChatUser chatUser;
   List<ChatMessage> chatMessages;
@@ -39,12 +62,21 @@ class ChatModel with ChangeNotifier {
   bool isConnManager = false;
 
   RxBool isWindowFocus = true.obs;
-  BlockableOverlayState? _blockableOverlayState;
+  BlockableOverlayState _blockableOverlayState = BlockableOverlayState();
   final Rx<VoiceCallStatus> _voiceCallStatus = Rx(VoiceCallStatus.notStarted);
 
   Rx<VoiceCallStatus> get voiceCallStatus => _voiceCallStatus;
 
   TextEditingController textController = TextEditingController();
+  RxInt mobileUnreadSum = 0.obs;
+  MessageKey? latestReceivedKey;
+
+  Offset chatWindowPosition = Offset(20, 80);
+
+  void setChatWindowPosition(Offset position) {
+    chatWindowPosition = position;
+    notifyListeners();
+  }
 
   @override
   void dispose() {
@@ -53,21 +85,20 @@ class ChatModel with ChangeNotifier {
   }
 
   final ChatUser me = ChatUser(
-    id: "",
+    id: Uuid().v4().toString(),
     firstName: translate("Me"),
   );
 
-  late final Map<int, MessageBody> _messages = {}..[clientModeID] =
-      MessageBody(me, []);
+  late final Map<MessageKey, MessageBody> _messages = {};
 
-  var _currentID = clientModeID;
-  late bool _isShowCMChatPage = false;
+  MessageKey _currentKey = MessageKey('', -2); // -2 is invalid value
+  late bool _isShowCMSidePage = false;
 
-  Map<int, MessageBody> get messages => _messages;
+  Map<MessageKey, MessageBody> get messages => _messages;
 
-  int get currentID => _currentID;
+  MessageKey get currentKey => _currentKey;
 
-  bool get isShowCMChatPage => _isShowCMChatPage;
+  bool get isShowCMSidePage => _isShowCMSidePage;
 
   void setOverlayState(BlockableOverlayState blockableOverlayState) {
     _blockableOverlayState = blockableOverlayState;
@@ -115,15 +146,7 @@ class ChatModel with ChangeNotifier {
     );
   }
 
-  ChatUser get currentUser {
-    final user = messages[currentID]?.chatUser;
-    if (user == null) {
-      _currentID = clientModeID;
-      return me;
-    } else {
-      return user;
-    }
-  }
+  ChatUser? get currentUser => _messages[_currentKey]?.chatUser;
 
   showChatIconOverlay({Offset offset = const Offset(200, 50)}) {
     if (chatIconOverlayEntry != null) {
@@ -137,7 +160,7 @@ class ChatModel with ChangeNotifier {
       }
     }
 
-    final overlayState = _blockableOverlayState?.state;
+    final overlayState = _blockableOverlayState.state;
     if (overlayState == null) return;
 
     final overlay = OverlayEntry(builder: (context) {
@@ -178,6 +201,12 @@ class ChatModel with ChangeNotifier {
 
     final overlayState = _blockableOverlayState?.state;
     if (overlayState == null) return;
+    if (isMobile &&
+        !gFFI.chatModel.currentKey.isOut && // not in remote page
+        gFFI.chatModel.latestReceivedKey != null) {
+      gFFI.chatModel.changeCurrentKey(gFFI.chatModel.latestReceivedKey!);
+      gFFI.chatModel.mobileClearClientUnread(gFFI.chatModel.currentKey.connId);
+    }
     final overlay = OverlayEntry(builder: (context) {
       return Listener(
           onPointerDown: (_) {
@@ -187,7 +216,7 @@ class ChatModel with ChangeNotifier {
             }
           },
           child: DraggableChatWindow(
-              position: chatInitPos ?? Offset(20, 80),
+              position: chatInitPos ?? chatWindowPosition,
               width: 250,
               height: 350,
               chatModel: this));
@@ -229,24 +258,36 @@ class ChatModel with ChangeNotifier {
     }
   }
 
-  showChatPage(int id) async {
-    if (isConnManager) {
-      if (!_isShowCMChatPage) {
-        await toggleCMChatPage(id);
+  showChatPage(MessageKey key) async {
+    if (isDesktop) {
+      if (isConnManager) {
+        if (!_isShowCMSidePage) {
+          await toggleCMChatPage(key);
+        }
+      } else {
+        if (_isChatOverlayHide()) {
+          await toggleChatOverlay();
+        }
       }
     } else {
-      if (_isChatOverlayHide()) {
-        await toggleChatOverlay();
+      if (key.connId == clientModeID) {
+        if (_isChatOverlayHide()) {
+          await toggleChatOverlay();
+        }
       }
     }
   }
 
-  toggleCMChatPage(int id) async {
-    if (gFFI.chatModel.currentID != id) {
-      gFFI.chatModel.changeCurrentID(id);
+  toggleCMChatPage(MessageKey key) async {
+    if (gFFI.chatModel.currentKey != key) {
+      gFFI.chatModel.changeCurrentKey(key);
     }
-    if (_isShowCMChatPage) {
-      _isShowCMChatPage = !_isShowCMChatPage;
+    await toggleCMSidePage();
+  }
+
+  toggleCMSidePage() async {
+    if (_isShowCMSidePage) {
+      _isShowCMSidePage = !_isShowCMSidePage;
       notifyListeners();
       await windowManager.show();
       await windowManager.setSizeAlignment(
@@ -256,30 +297,35 @@ class ChatModel with ChangeNotifier {
       await windowManager.show();
       await windowManager.setSizeAlignment(
           kConnectionManagerWindowSizeOpenChat, Alignment.topRight);
-      _isShowCMChatPage = !_isShowCMChatPage;
+      _isShowCMSidePage = !_isShowCMSidePage;
       notifyListeners();
     }
   }
 
-  changeCurrentID(int id) {
-    if (_messages.containsKey(id)) {
-      _currentID = id;
-      notifyListeners();
+  changeCurrentKey(MessageKey key) {
+    updateConnIdOfKey(key);
+    String? peerName;
+    if (key.connId == clientModeID) {
+      peerName = parent.target?.ffiModel.pi.username;
     } else {
-      final client = parent.target?.serverModel.clients
-          .firstWhere((client) => client.id == id);
-      if (client == null) {
-        return debugPrint(
-            "Failed to changeCurrentID,remote user doesn't exist");
-      }
-      final chatUser = ChatUser(
-        id: client.peerId,
-        firstName: client.name,
-      );
-      _messages[id] = MessageBody(chatUser, []);
-      _currentID = id;
-      notifyListeners();
+      peerName = parent.target?.serverModel.clients
+          .firstWhereOrNull((client) => client.peerId == key.peerId)
+          ?.name;
     }
+    if (!_messages.containsKey(key)) {
+      final chatUser = ChatUser(
+        id: key.peerId,
+        firstName: peerName,
+      );
+      _messages[key] = MessageBody(chatUser, []);
+    } else {
+      if (peerName != null && peerName.isNotEmpty) {
+        _messages[key]?.chatUser.firstName = peerName;
+      }
+    }
+    _currentKey = key;
+    notifyListeners();
+    mobileClearClientUnread(key.connId);
   }
 
   receive(int id, String text) async {
@@ -292,49 +338,90 @@ class ChatModel with ChangeNotifier {
     if (desktopType == DesktopType.cm) {
       await showCmWindow();
     }
+    String? peerId;
+    if (id == clientModeID) {
+      peerId = session.id;
+    } else {
+      peerId = session.serverModel.clients
+          .firstWhereOrNull((e) => e.id == id)
+          ?.peerId;
+    }
+    if (peerId == null) {
+      debugPrint("Failed to receive msg, peerId is null");
+      return;
+    }
+
+    final messagekey = MessageKey(peerId, id);
 
     // mobile: first message show overlay icon
     if (!isDesktop && chatIconOverlayEntry == null) {
       showChatIconOverlay();
     }
     // show chat page
-    await showChatPage(id);
-
-    int toId = currentID;
-
+    await showChatPage(messagekey);
     late final ChatUser chatUser;
     if (id == clientModeID) {
       chatUser = ChatUser(
         firstName: session.ffiModel.pi.username,
-        id: session.id,
+        id: peerId,
       );
-      toId = id;
-    } else {
-      final client =
-          session.serverModel.clients.firstWhere((client) => client.id == id);
+
       if (isDesktop) {
-        window_on_top(null);
+        if (Get.isRegistered<DesktopTabController>()) {
+          DesktopTabController tabController = Get.find<DesktopTabController>();
+          var index = tabController.state.value.tabs
+              .indexWhere((e) => e.key == session.id);
+          final notSelected =
+              index >= 0 && tabController.state.value.selected != index;
+          // minisized: top and switch tab
+          // not minisized: add count
+          if (await WindowController.fromWindowId(stateGlobal.windowId)
+              .isMinimized()) {
+            windowOnTop(stateGlobal.windowId);
+            if (notSelected) {
+              tabController.jumpTo(index);
+            }
+          } else {
+            if (notSelected) {
+              UnreadChatCountState.find(peerId).value += 1;
+            }
+          }
+        }
+      }
+    } else {
+      final client = session.serverModel.clients
+          .firstWhereOrNull((client) => client.id == id);
+      if (client == null) {
+        debugPrint("Failed to receive msg, client is null");
+        return;
+      }
+      if (isDesktop) {
+        windowOnTop(null);
         // disable auto jumpTo other tab when hasFocus, and mark unread message
         final currentSelectedTab =
             session.serverModel.tabController.state.value.selectedTabInfo;
         if (currentSelectedTab.key != id.toString() && inputNode.hasFocus) {
-          client.hasUnreadChatMessage.value = true;
+          client.unreadChatMessageCount.value += 1;
         } else {
           parent.target?.serverModel.jumpTo(id);
-          toId = id;
         }
       } else {
-        toId = id;
+        if (HomePage.homeKey.currentState?.isChatPageCurrentTab != true ||
+            _currentKey != messagekey) {
+          client.unreadChatMessageCount.value += 1;
+          mobileUpdateUnreadSum();
+        }
       }
       chatUser = ChatUser(id: client.peerId, firstName: client.name);
     }
-
-    if (!_messages.containsKey(id)) {
-      _messages[id] = MessageBody(chatUser, []);
-    }
-    _messages[id]!.insert(
+    insertMessage(messagekey,
         ChatMessage(text: text, user: chatUser, createdAt: DateTime.now()));
-    _currentID = toId;
+    if (id == clientModeID || _currentKey.peerId.isEmpty) {
+      // client or invalid
+      _currentKey = messagekey;
+      mobileClearClientUnread(messagekey.connId);
+    }
+    latestReceivedKey = messagekey;
     notifyListeners();
   }
 
@@ -344,17 +431,63 @@ class ChatModel with ChangeNotifier {
       return;
     }
     message.text = trimmedText;
-    _messages[_currentID]?.insert(message);
-    if (_currentID == clientModeID && parent.target != null) {
+    insertMessage(_currentKey, message);
+    if (_currentKey.connId == clientModeID && parent.target != null) {
       bind.sessionSendChat(sessionId: sessionId, text: message.text);
     } else {
-      bind.cmSendChat(connId: _currentID, msg: message.text);
+      bind.cmSendChat(connId: _currentKey.connId, msg: message.text);
     }
 
     notifyListeners();
     inputNode.requestFocus();
   }
 
+  insertMessage(MessageKey key, ChatMessage message) {
+    updateConnIdOfKey(key);
+    if (!_messages.containsKey(key)) {
+      _messages[key] = MessageBody(message.user, []);
+    }
+    _messages[key]?.insert(message);
+  }
+
+  updateConnIdOfKey(MessageKey key) {
+    if (_messages.keys
+            .toList()
+            .firstWhereOrNull((e) => e == key && e.connId != key.connId) !=
+        null) {
+      final value = _messages.remove(key);
+      if (value != null) {
+        _messages[key] = value;
+      }
+    }
+    if (_currentKey == key || _currentKey.peerId.isEmpty) {
+      _currentKey = key; // hash != assign
+    }
+  }
+
+  void mobileUpdateUnreadSum() {
+    if (!isMobile) return;
+    var sum = 0;
+    parent.target?.serverModel.clients
+        .map((e) => sum += e.unreadChatMessageCount.value)
+        .toList();
+    Future.delayed(Duration.zero, () {
+      mobileUnreadSum.value = sum;
+    });
+  }
+
+  void mobileClearClientUnread(int id) {
+    if (!isMobile) return;
+    final client = parent.target?.serverModel.clients
+        .firstWhereOrNull((client) => client.id == id);
+    if (client != null) {
+      Future.delayed(Duration.zero, () {
+        client.unreadChatMessageCount.value = 0;
+        mobileUpdateUnreadSum();
+      });
+    }
+  }
+
   close() {
     hideChatIconOverlay();
     hideChatWindowOverlay();
diff --git a/flutter/lib/models/cm_file_model.dart b/flutter/lib/models/cm_file_model.dart
new file mode 100644
index 000000000..d372db6c3
--- /dev/null
+++ b/flutter/lib/models/cm_file_model.dart
@@ -0,0 +1,142 @@
+import 'dart:collection';
+import 'dart:convert';
+
+import 'package:flutter/material.dart';
+import 'package:flutter_hbb/common.dart';
+import 'package:flutter_hbb/models/model.dart';
+import 'package:flutter_hbb/models/server_model.dart';
+import 'package:get/get.dart';
+import 'file_model.dart';
+
+class CmFileModel {
+  final WeakReference<FFI> parent;
+  final currentJobTable = RxList<JobProgress>();
+  final _jobTables = HashMap<int, RxList<JobProgress>>.fromEntries([]);
+  Stopwatch stopwatch = Stopwatch();
+  int _lastElapsed = 0;
+
+  CmFileModel(this.parent);
+
+  void updateCurrentClientId(int id) {
+    if (_jobTables[id] == null) {
+      _jobTables[id] = RxList<JobProgress>();
+    }
+    Future.delayed(Duration.zero, () {
+      currentJobTable.value = _jobTables[id]!;
+    });
+  }
+
+  onFileTransferLog(dynamic log) {
+    try {
+      dynamic d = jsonDecode(log);
+      if (!stopwatch.isRunning) stopwatch.start();
+      bool calcSpeed = stopwatch.elapsedMilliseconds - _lastElapsed >= 1000;
+      if (calcSpeed) {
+        _lastElapsed = stopwatch.elapsedMilliseconds;
+      }
+      if (d is List<dynamic>) {
+        for (var l in d) {
+          _dealOneJob(l, calcSpeed);
+        }
+      } else {
+        _dealOneJob(d, calcSpeed);
+      }
+      currentJobTable.refresh();
+    } catch (e) {
+      debugPrint("onFileTransferLog:$e");
+    }
+  }
+
+  _dealOneJob(dynamic l, bool calcSpeed) {
+    final data = TransferJobSerdeData.fromJson(l);
+    Client? client =
+        gFFI.serverModel.clients.firstWhereOrNull((e) => e.id == data.connId);
+    var jobTable = _jobTables[data.connId];
+    if (jobTable == null) {
+      debugPrint("jobTable should not be null");
+      return;
+    }
+    JobProgress? job = jobTable.firstWhereOrNull((e) => e.id == data.id);
+    if (job == null) {
+      job = JobProgress();
+      jobTable.add(job);
+      final currentSelectedTab =
+          gFFI.serverModel.tabController.state.value.selectedTabInfo;
+      if (!(gFFI.chatModel.isShowCMSidePage &&
+          currentSelectedTab.key == data.connId.toString())) {
+        client?.unreadChatMessageCount.value += 1;
+      }
+    }
+    job.id = data.id;
+    job.isRemoteToLocal = data.isRemote;
+    job.fileName = data.path;
+    job.totalSize = data.totalSize;
+    job.finishedSize = data.finishedSize;
+    if (job.finishedSize > data.totalSize) {
+      job.finishedSize = data.totalSize;
+    }
+    job.isRemoteToLocal = data.isRemote;
+
+    if (job.finishedSize > 0) {
+      if (job.finishedSize < job.totalSize) {
+        job.state = JobState.inProgress;
+      } else {
+        job.state = JobState.done;
+      }
+    }
+    if (data.done) {
+      job.state = JobState.done;
+    } else if (data.cancel || data.error == 'skipped') {
+      job.state = JobState.done;
+      job.err = 'skipped';
+    } else if (data.error.isNotEmpty) {
+      job.state = JobState.error;
+      job.err = data.error;
+    }
+    if (calcSpeed) {
+      job.speed = (data.transferred - job.lastTransferredSize) * 1.0;
+      job.lastTransferredSize = data.transferred;
+    }
+    jobTable.refresh();
+  }
+}
+
+class TransferJobSerdeData {
+  int connId;
+  int id;
+  String path;
+  bool isRemote;
+  int totalSize;
+  int finishedSize;
+  int transferred;
+  bool done;
+  bool cancel;
+  String error;
+
+  TransferJobSerdeData({
+    required this.connId,
+    required this.id,
+    required this.path,
+    required this.isRemote,
+    required this.totalSize,
+    required this.finishedSize,
+    required this.transferred,
+    required this.done,
+    required this.cancel,
+    required this.error,
+  });
+
+  TransferJobSerdeData.fromJson(dynamic d)
+      : this(
+          connId: d['connId'] ?? 0,
+          id: int.tryParse(d['id'].toString()) ?? 0,
+          path: d['path'] ?? '',
+          isRemote: d['isRemote'] ?? false,
+          totalSize: d['totalSize'] ?? 0,
+          finishedSize: d['finishedSize'] ?? 0,
+          transferred: d['transferred'] ?? 0,
+          done: d['done'] ?? false,
+          cancel: d['cancel'] ?? false,
+          error: d['error'] ?? '',
+        );
+}
diff --git a/flutter/lib/models/desktop_render_texture.dart b/flutter/lib/models/desktop_render_texture.dart
new file mode 100644
index 000000000..f8456e339
--- /dev/null
+++ b/flutter/lib/models/desktop_render_texture.dart
@@ -0,0 +1,41 @@
+import 'package:get/get.dart';
+import 'package:texture_rgba_renderer/texture_rgba_renderer.dart';
+
+import '../../common.dart';
+import './platform_model.dart';
+
+class RenderTexture {
+  final RxInt textureId = RxInt(-1);
+  int _textureKey = -1;
+  SessionID? _sessionId;
+  final useTextureRender = bind.mainUseTextureRender();
+
+  final textureRenderer = TextureRgbaRenderer();
+
+  RenderTexture();
+
+  create(SessionID sessionId) {
+    if (useTextureRender) {
+      _textureKey = bind.getNextTextureKey();
+      _sessionId = sessionId;
+
+      textureRenderer.createTexture(_textureKey).then((id) async {
+        if (id != -1) {
+          final ptr = await textureRenderer.getTexturePtr(_textureKey);
+          platformFFI.registerTexture(sessionId, ptr);
+          textureId.value = id;
+        }
+      });
+    }
+  }
+
+  destroy(bool unregisterTexture) async {
+    if (useTextureRender && _textureKey != -1 && _sessionId != null) {
+      if (unregisterTexture) {
+        platformFFI.registerTexture(_sessionId!, 0);
+      }
+      await textureRenderer.closeTexture(_textureKey);
+      _textureKey = -1;
+    }
+  }
+}
diff --git a/flutter/lib/models/file_model.dart b/flutter/lib/models/file_model.dart
index 95a4cad0c..108c76f1e 100644
--- a/flutter/lib/models/file_model.dart
+++ b/flutter/lib/models/file_model.dart
@@ -556,8 +556,10 @@ class FileController {
           mainAxisAlignment: MainAxisAlignment.center,
           children: [
             const Icon(Icons.warning_rounded, color: Colors.red),
-            Text(title).paddingOnly(
-              left: 10,
+            Expanded(
+              child: Text(title).paddingOnly(
+                left: 10,
+              ),
             ),
           ],
         ),
@@ -1027,6 +1029,7 @@ class JobProgress {
   var to = "";
   var showHidden = false;
   var err = "";
+  int lastTransferredSize = 0;
 
   clear() {
     state = JobState.none;
diff --git a/flutter/lib/models/group_model.dart b/flutter/lib/models/group_model.dart
index bade00442..f2592fa2b 100644
--- a/flutter/lib/models/group_model.dart
+++ b/flutter/lib/models/group_model.dart
@@ -126,7 +126,7 @@ class GroupModel {
         throw error;
       }
       groupName.value = data['name'] ?? '';
-      groupId.value = data['id'] ?? '';
+      groupId.value = data['guid'] ?? '';
       return groupId.value.isNotEmpty && groupName.isNotEmpty;
     } catch (e) {
       debugPrint('$e');
diff --git a/flutter/lib/models/input_model.dart b/flutter/lib/models/input_model.dart
index e7ce5c585..971bbb7e5 100644
--- a/flutter/lib/models/input_model.dart
+++ b/flutter/lib/models/input_model.dart
@@ -35,6 +35,24 @@ extension ToString on MouseButtons {
   }
 }
 
+class PointerEventToRust {
+  final String kind;
+  final String type;
+  final dynamic value;
+
+  PointerEventToRust(this.kind, this.type, this.value);
+
+  Map<String, dynamic> toJson() {
+    return {
+      'k': kind,
+      'v': {
+        't': type,
+        'v': value,
+      }
+    };
+  }
+}
+
 class InputModel {
   final WeakReference<FFI> parent;
   String keyboardMode = "legacy";
@@ -55,16 +73,18 @@ class InputModel {
   final _trackpadSpeed = 0.06;
   var _trackpadScrollUnsent = Offset.zero;
 
+  var _lastScale = 1.0;
+
   // mouse
   final isPhysicalMouse = false.obs;
   int _lastButtons = 0;
   Offset lastMousePos = Offset.zero;
 
-  get id => parent.target?.id ?? "";
-
   late final SessionID sessionId;
 
   bool get keyboardPerm => parent.target!.ffiModel.keyboard;
+  String get id => parent.target?.id ?? '';
+  String? get peerPlatform => parent.target?.ffiModel.pi.platform;
 
   InputModel(this.parent) {
     sessionId = parent.target!.sessionId;
@@ -221,14 +241,8 @@ class InputModel {
         command: command);
   }
 
-  Map<String, dynamic> getEvent(PointerEvent evt, String type) {
+  Map<String, dynamic> _getMouseEvent(PointerEvent evt, String type) {
     final Map<String, dynamic> out = {};
-    out['x'] = evt.position.dx;
-    out['y'] = evt.position.dy;
-    if (alt) out['alt'] = 'true';
-    if (shift) out['shift'] = 'true';
-    if (ctrl) out['ctrl'] = 'true';
-    if (command) out['command'] = 'true';
 
     // Check update event type and set buttons to be sent.
     int buttons = _lastButtons;
@@ -258,7 +272,6 @@ class InputModel {
 
     out['buttons'] = buttons;
     out['type'] = type;
-
     return out;
   }
 
@@ -268,6 +281,14 @@ class InputModel {
     sendMouse('up', button);
   }
 
+  void tapDown(MouseButtons button) {
+    sendMouse('down', button);
+  }
+
+  void tapUp(MouseButtons button) {
+    sendMouse('up', button);
+  }
+
   /// Send scroll event with scroll distance [y].
   void scroll(int y) {
     bind.sessionSendMouse(
@@ -282,7 +303,7 @@ class InputModel {
   }
 
   /// Modify the given modifier map [evt] based on current modifier key status.
-  Map<String, String> modify(Map<String, String> evt) {
+  Map<String, dynamic> modify(Map<String, dynamic> evt) {
     if (ctrl) evt['ctrl'] = 'true';
     if (shift) evt['shift'] = 'true';
     if (alt) evt['alt'] = 'true';
@@ -324,23 +345,41 @@ class InputModel {
       isPhysicalMouse.value = true;
     }
     if (isPhysicalMouse.value) {
-      handleMouse(getEvent(e, _kMouseEventMove));
+      handleMouse(_getMouseEvent(e, _kMouseEventMove), e.position);
     }
   }
 
   void onPointerPanZoomStart(PointerPanZoomStartEvent e) {
+    _lastScale = 1.0;
     _stopFling = true;
+
+    if (peerPlatform == kPeerPlatformAndroid) {
+      handlePointerEvent('touch', 'pan_start', e.position);
+    }
   }
 
   // https://docs.flutter.dev/release/breaking-changes/trackpad-gestures
-  // TODO(support zoom in/out)
   void onPointerPanZoomUpdate(PointerPanZoomUpdateEvent e) {
+    if (peerPlatform != kPeerPlatformAndroid) {
+      final scale = ((e.scale - _lastScale) * 1000).toInt();
+      _lastScale = e.scale;
+
+      if (scale != 0) {
+        bind.sessionSendPointer(
+            sessionId: sessionId,
+            msg: json.encode(
+                PointerEventToRust(kPointerEventKindTouch, 'scale', scale)
+                    .toJson()));
+        return;
+      }
+    }
+
     final delta = e.panDelta;
     _trackpadLastDelta = delta;
 
     var x = delta.dx.toInt();
     var y = delta.dy.toInt();
-    if (parent.target?.ffiModel.pi.platform == kPeerPlatformLinux) {
+    if (peerPlatform == kPeerPlatformLinux) {
       _trackpadScrollUnsent += (delta * _trackpadSpeed);
       x = _trackpadScrollUnsent.dx.truncate();
       y = _trackpadScrollUnsent.dy.truncate();
@@ -356,9 +395,13 @@ class InputModel {
       }
     }
     if (x != 0 || y != 0) {
-      bind.sessionSendMouse(
-          sessionId: sessionId,
-          msg: '{"type": "trackpad", "x": "$x", "y": "$y"}');
+      if (peerPlatform == kPeerPlatformAndroid) {
+        handlePointerEvent('touch', 'pan_update', Offset(x.toDouble(), y.toDouble()));
+      } else {
+        bind.sessionSendMouse(
+            sessionId: sessionId,
+            msg: '{"type": "trackpad", "x": "$x", "y": "$y"}');
+      }
     }
   }
 
@@ -414,6 +457,16 @@ class InputModel {
   }
 
   void onPointerPanZoomEnd(PointerPanZoomEndEvent e) {
+    if (peerPlatform == kPeerPlatformAndroid) {
+      handlePointerEvent('touch', 'pan_end', e.position);
+      return;
+    }
+
+    bind.sessionSendPointer(
+        sessionId: sessionId,
+        msg: json.encode(
+            PointerEventToRust(kPointerEventKindTouch, 'scale', 0).toJson()));
+
     waitLastFlingDone();
     _stopFling = false;
 
@@ -429,7 +482,7 @@ class InputModel {
   }
 
   void onPointDownImage(PointerDownEvent e) {
-    debugPrint("onPointDownImage");
+    debugPrint("onPointDownImage ${e.kind}");
     _stopFling = true;
     if (e.kind != ui.PointerDeviceKind.mouse) {
       if (isPhysicalMouse.value) {
@@ -437,21 +490,21 @@ class InputModel {
       }
     }
     if (isPhysicalMouse.value) {
-      handleMouse(getEvent(e, _kMouseEventDown));
+      handleMouse(_getMouseEvent(e, _kMouseEventDown), e.position);
     }
   }
 
   void onPointUpImage(PointerUpEvent e) {
     if (e.kind != ui.PointerDeviceKind.mouse) return;
     if (isPhysicalMouse.value) {
-      handleMouse(getEvent(e, _kMouseEventUp));
+      handleMouse(_getMouseEvent(e, _kMouseEventUp), e.position);
     }
   }
 
   void onPointMoveImage(PointerMoveEvent e) {
     if (e.kind != ui.PointerDeviceKind.mouse) return;
     if (isPhysicalMouse.value) {
-      handleMouse(getEvent(e, _kMouseEventMove));
+      handleMouse(_getMouseEvent(e, _kMouseEventMove), e.position);
     }
   }
 
@@ -476,19 +529,16 @@ class InputModel {
   }
 
   void refreshMousePos() => handleMouse({
-        'x': lastMousePos.dx,
-        'y': lastMousePos.dy,
         'buttons': 0,
         'type': _kMouseEventMove,
-      });
+      }, lastMousePos);
 
   void tryMoveEdgeOnExit(Offset pos) => handleMouse(
         {
-          'x': pos.dx,
-          'y': pos.dy,
           'buttons': 0,
           'type': _kMouseEventMove,
         },
+        pos,
         onExit: true,
       );
 
@@ -522,17 +572,49 @@ class InputModel {
     return Offset(x, y);
   }
 
-  void handleMouse(
-    Map<String, dynamic> evt, {
-    bool onExit = false,
-  }) {
-    double x = evt['x'];
-    double y = max(0.0, evt['y']);
-    final cursorModel = parent.target!.cursorModel;
+  void handlePointerEvent(String kind, String type, Offset offset) {
+    double x = offset.dx;
+    double y = offset.dy;
+    if (_checkPeerControlProtected(x, y)) {
+      return;
+    }
+    // Only touch events are handled for now. So we can just ignore buttons.
+    // to-do: handle mouse events
 
+    late final dynamic evtValue;
+    if (type == 'pan_update') {
+      evtValue = {
+        'x': x.toInt(),
+        'y': y.toInt(),
+      };
+    } else {
+      final isMoveTypes = ['pan_start', 'pan_end'];
+      final pos = handlePointerDevicePos(
+        kPointerEventKindTouch,
+        x,
+        y,
+        isMoveTypes.contains(type),
+        type,
+      );
+      if (pos == null) {
+        return;
+      }
+      evtValue = {
+        'x': pos.x,
+        'y': pos.y,
+      };
+    }
+
+    final evt = PointerEventToRust(kind, type, evtValue).toJson();
+    bind.sessionSendPointer(
+        sessionId: sessionId, msg: json.encode(modify(evt)));
+  }
+
+  bool _checkPeerControlProtected(double x, double y) {
+    final cursorModel = parent.target!.cursorModel;
     if (cursorModel.isPeerControlProtected) {
       lastMousePos = ui.Offset(x, y);
-      return;
+      return true;
     }
 
     if (!cursorModel.gotMouseControl) {
@@ -543,10 +625,23 @@ class InputModel {
         cursorModel.gotMouseControl = true;
       } else {
         lastMousePos = ui.Offset(x, y);
-        return;
+        return true;
       }
     }
     lastMousePos = ui.Offset(x, y);
+    return false;
+  }
+
+  void handleMouse(
+    Map<String, dynamic> evt,
+    Offset offset, {
+    bool onExit = false,
+  }) {
+    double x = offset.dx;
+    double y = max(0.0, offset.dy);
+    if (_checkPeerControlProtected(x, y)) {
+      return;
+    }
 
     var type = '';
     var isMove = false;
@@ -564,17 +659,58 @@ class InputModel {
         return;
     }
     evt['type'] = type;
+
+    final pos = handlePointerDevicePos(
+      kPointerEventKindMouse,
+      x,
+      y,
+      isMove,
+      type,
+      onExit: onExit,
+      buttons: evt['buttons'],
+    );
+    if (pos == null) {
+      return;
+    }
+    if (type != '') {
+      evt['x'] = '0';
+      evt['y'] = '0';
+    } else {
+      evt['x'] = '${pos.x}';
+      evt['y'] = '${pos.y}';
+    }
+
+    Map<int, String> mapButtons = {
+      kPrimaryMouseButton: 'left',
+      kSecondaryMouseButton: 'right',
+      kMiddleMouseButton: 'wheel',
+      kBackMouseButton: 'back',
+      kForwardMouseButton: 'forward'
+    };
+    evt['buttons'] = mapButtons[evt['buttons']] ?? '';
+    bind.sessionSendMouse(sessionId: sessionId, msg: json.encode(modify(evt)));
+  }
+
+  Point? handlePointerDevicePos(
+    String kind,
+    double x,
+    double y,
+    bool isMove,
+    String evtType, {
+    bool onExit = false,
+    int buttons = kPrimaryMouseButton,
+  }) {
     y -= CanvasModel.topToEdge;
     x -= CanvasModel.leftToEdge;
     final canvasModel = parent.target!.canvasModel;
-    final nearThr = 3;
-    var nearRight = (canvasModel.size.width - x) < nearThr;
-    var nearBottom = (canvasModel.size.height - y) < nearThr;
-
     final ffiModel = parent.target!.ffiModel;
     if (isMove) {
       canvasModel.moveDesktopMouse(x, y);
     }
+
+    final nearThr = 3;
+    var nearRight = (canvasModel.size.width - x) < nearThr;
+    var nearBottom = (canvasModel.size.height - y) < nearThr;
     final d = ffiModel.display;
     final imageWidth = d.width * canvasModel.scale;
     final imageHeight = d.height * canvasModel.scale;
@@ -622,7 +758,7 @@ class InputModel {
     } catch (e) {
       debugPrintStack(
           label: 'canvasModel.scale value ${canvasModel.scale}, $e');
-      return;
+      return null;
     }
 
     int minX = d.x.toInt();
@@ -631,40 +767,16 @@ class InputModel {
     int maxY = (d.y + d.height).toInt() - 1;
     evtX = trySetNearestRange(evtX, minX, maxX, 5);
     evtY = trySetNearestRange(evtY, minY, maxY, 5);
-    if (evtX < minX || evtY < minY || evtX > maxX || evtY > maxY) {
-      // If left mouse up, no early return.
-      if (evt['buttons'] != kPrimaryMouseButton || type != 'up') {
-        return;
+    if (kind == kPointerEventKindMouse) {
+      if (evtX < minX || evtY < minY || evtX > maxX || evtY > maxY) {
+        // If left mouse up, no early return.
+        if (!(buttons == kPrimaryMouseButton && evtType == 'up')) {
+          return null;
+        }
       }
     }
 
-    if (type != '') {
-      evtX = 0;
-      evtY = 0;
-    }
-
-    evt['x'] = '$evtX';
-    evt['y'] = '$evtY';
-    var buttons = '';
-    switch (evt['buttons']) {
-      case kPrimaryMouseButton:
-        buttons = 'left';
-        break;
-      case kSecondaryMouseButton:
-        buttons = 'right';
-        break;
-      case kMiddleMouseButton:
-        buttons = 'wheel';
-        break;
-      case kBackMouseButton:
-        buttons = 'back';
-        break;
-      case kForwardMouseButton:
-        buttons = 'forward';
-        break;
-    }
-    evt['buttons'] = buttons;
-    bind.sessionSendMouse(sessionId: sessionId, msg: json.encode(evt));
+    return Point(evtX, evtY);
   }
 
   /// Web only
diff --git a/flutter/lib/models/model.dart b/flutter/lib/models/model.dart
index 1fa3255c9..67f195dd6 100644
--- a/flutter/lib/models/model.dart
+++ b/flutter/lib/models/model.dart
@@ -4,12 +4,14 @@ import 'dart:io';
 import 'dart:math';
 import 'dart:ui' as ui;
 
+import 'package:desktop_multi_window/desktop_multi_window.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter/services.dart';
 import 'package:flutter_hbb/consts.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/cm_file_model.dart';
 import 'package:flutter_hbb/models/file_model.dart';
 import 'package:flutter_hbb/models/group_model.dart';
 import 'package:flutter_hbb/models/peer_tab_model.dart';
@@ -37,10 +39,52 @@ import 'platform_model.dart';
 
 typedef HandleMsgBox = Function(Map<String, dynamic> evt, String id);
 typedef ReconnectHandle = Function(OverlayDialogManager, SessionID, bool);
-final _waitForImage = <UuidValue, bool>{};
 final _constSessionId = Uuid().v4obj();
 
+class CachedPeerData {
+  Map<String, dynamic> updatePrivacyMode = {};
+  Map<String, dynamic> peerInfo = {};
+  List<Map<String, dynamic>> cursorDataList = [];
+  Map<String, dynamic> lastCursorId = {};
+  bool secure = false;
+  bool direct = false;
+
+  CachedPeerData();
+
+  @override
+  String toString() {
+    return jsonEncode({
+      'updatePrivacyMode': updatePrivacyMode,
+      'peerInfo': peerInfo,
+      'cursorDataList': cursorDataList,
+      'lastCursorId': lastCursorId,
+      'secure': secure,
+      'direct': direct,
+    });
+  }
+
+  static CachedPeerData? fromString(String s) {
+    try {
+      final map = jsonDecode(s);
+      final data = CachedPeerData();
+      data.updatePrivacyMode = map['updatePrivacyMode'];
+      data.peerInfo = map['peerInfo'];
+      for (final cursorData in map['cursorDataList']) {
+        data.cursorDataList.add(cursorData);
+      }
+      data.lastCursorId = map['lastCursorId'];
+      data.secure = map['secure'];
+      data.direct = map['direct'];
+      return data;
+    } catch (e) {
+      debugPrint('Failed to parse CachedPeerData: $e');
+      return null;
+    }
+  }
+}
+
 class FfiModel with ChangeNotifier {
+  CachedPeerData cachedPeerData = CachedPeerData();
   PeerInfo _pi = PeerInfo();
   Display _display = Display();
 
@@ -55,6 +99,10 @@ class FfiModel with ChangeNotifier {
   WeakReference<FFI> parent;
   late final SessionID sessionId;
 
+  RxBool waitForImageDialogShow = true.obs;
+  Timer? waitForImageTimer;
+  RxBool waitForFirstImage = true.obs;
+
   Map<String, bool> get permissions => _permissions;
 
   Display get display => _display;
@@ -113,9 +161,12 @@ class FfiModel with ChangeNotifier {
     _timer?.cancel();
     _timer = null;
     clearPermissions();
+    waitForImageTimer?.cancel();
   }
 
   setConnectionType(String peerId, bool secure, bool direct) {
+    cachedPeerData.secure = secure;
+    cachedPeerData.direct = direct;
     _secure = secure;
     _direct = direct;
     try {
@@ -142,6 +193,22 @@ class FfiModel with ChangeNotifier {
     _permissions.clear();
   }
 
+  handleCachedPeerData(CachedPeerData data, String peerId) async {
+    handleMsgBox({
+      'type': 'success',
+      'title': 'Successful',
+      'text': 'Connected, waiting for image...',
+      'link': '',
+    }, sessionId, peerId);
+    updatePrivacyMode(data.updatePrivacyMode, sessionId, peerId);
+    setConnectionType(peerId, data.secure, data.direct);
+    handlePeerInfo(data.peerInfo, peerId);
+    for (var element in data.cursorDataList) {
+      handleCursorData(element);
+    }
+    handleCursorId(data.lastCursorId);
+  }
+
   // todo: why called by two position
   StreamEventHandler startEventListener(SessionID sessionId, String peerId) {
     return (evt) async {
@@ -158,9 +225,9 @@ class FfiModel with ChangeNotifier {
       } else if (name == 'switch_display') {
         handleSwitchDisplay(evt, sessionId, peerId);
       } else if (name == 'cursor_data') {
-        await parent.target?.cursorModel.updateCursorData(evt);
+        await handleCursorData(evt);
       } else if (name == 'cursor_id') {
-        await parent.target?.cursorModel.updateCursorId(evt);
+        await handleCursorId(evt);
       } else if (name == 'cursor_position') {
         await parent.target?.cursorModel.updateCursorPosition(evt, peerId);
       } else if (name == 'clipboard') {
@@ -240,6 +307,21 @@ class FfiModel with ChangeNotifier {
         handleReloading(evt);
       } else if (name == 'plugin_option') {
         handleOption(evt);
+      } else if (name == "sync_peer_password_to_ab") {
+        if (desktopType == DesktopType.main) {
+          final id = evt['id'];
+          final password = evt['password'];
+          if (id != null && password != null) {
+            if (gFFI.abModel
+                .changePassword(id.toString(), password.toString())) {
+              gFFI.abModel.pushAb(toastIfFail: false, toastIfSucc: false);
+            }
+          }
+        }
+      } else if (name == "cm_file_transfer_log") {
+        if (isDesktop) {
+          gFFI.cmFileModel.onFileTransferLog(evt['log']);
+        }
       } else {
         debugPrint('Unknown event name: $name');
       }
@@ -248,7 +330,7 @@ class FfiModel with ChangeNotifier {
 
   onUrlSchemeReceived(Map<String, dynamic> evt) {
     final url = evt['url'].toString().trim();
-    if (url.startsWith(kUniLinksPrefix) && parseRustdeskUri(url)) {
+    if (url.startsWith(kUniLinksPrefix) && handleUriLink(uriString: url)) {
       return;
     }
     switch (url) {
@@ -260,7 +342,7 @@ class FfiModel with ChangeNotifier {
         });
         break;
       default:
-        window_on_top(null);
+        windowOnTop(null);
         break;
     }
   }
@@ -350,7 +432,9 @@ class FfiModel with ChangeNotifier {
     } else if (type == 'elevation-error') {
       showElevationError(sessionId, type, title, text, dialogManager);
     } else if (type == 'relay-hint') {
-      showRelayHintDialog(sessionId, type, title, text, dialogManager);
+      showRelayHintDialog(sessionId, type, title, text, dialogManager, peerId);
+    } else if (text == 'Connected, waiting for image...') {
+      showConnectedWaitingForImage(dialogManager, sessionId, type, title, text);
     } else {
       var hasRetry = evt['hasRetry'] == 'true';
       showMsgBox(sessionId, type, title, text, link, hasRetry, dialogManager);
@@ -383,7 +467,7 @@ class FfiModel with ChangeNotifier {
   }
 
   void showRelayHintDialog(SessionID sessionId, String type, String title,
-      String text, OverlayDialogManager dialogManager) {
+      String text, OverlayDialogManager dialogManager, String peerId) {
     dialogManager.show(tag: '$sessionId-$type', (setState, close, context) {
       onClose() {
         closeConnection();
@@ -392,31 +476,56 @@ class FfiModel with ChangeNotifier {
 
       final style =
           ElevatedButton.styleFrom(backgroundColor: Colors.green[700]);
+      var hint = "\n\n${translate('relay_hint_tip')}";
+      if (text.contains("10054") || text.contains("104")) {
+        hint = "";
+      }
+      final alreadyForceAlwaysRelay = bind
+          .mainGetPeerOptionSync(id: peerId, key: 'force-always-relay')
+          .isNotEmpty;
       return CustomAlertDialog(
         title: null,
-        content: msgboxContent(type, title,
-            "${translate(text)}\n\n${translate('relay_hint_tip')}"),
+        content: msgboxContent(type, title, "${translate(text)}$hint"),
         actions: [
           dialogButton('Close', onPressed: onClose, isOutline: true),
           dialogButton('Retry',
               onPressed: () => reconnect(dialogManager, sessionId, false)),
-          dialogButton('Connect via relay',
-              onPressed: () => reconnect(dialogManager, sessionId, true),
-              buttonStyle: style),
-          dialogButton('Always connect via relay', onPressed: () {
-            const option = 'force-always-relay';
-            bind.sessionPeerOption(
-                sessionId: sessionId,
-                name: option,
-                value: bool2option(option, true));
-            reconnect(dialogManager, sessionId, true);
-          }, buttonStyle: style),
+          if (!alreadyForceAlwaysRelay)
+            dialogButton('Connect via relay',
+                onPressed: () => reconnect(dialogManager, sessionId, true),
+                buttonStyle: style),
         ],
         onCancel: onClose,
       );
     });
   }
 
+  void showConnectedWaitingForImage(OverlayDialogManager dialogManager,
+      SessionID sessionId, String type, String title, String text) {
+    onClose() {
+      closeConnection();
+    }
+
+    if (waitForFirstImage.isFalse) return;
+    dialogManager.show(
+      (setState, close, context) => CustomAlertDialog(
+          title: null,
+          content: SelectionArea(child: msgboxContent(type, title, text)),
+          actions: [
+            dialogButton("Cancel", onPressed: onClose, isOutline: true)
+          ],
+          onCancel: onClose),
+      tag: '$sessionId-waiting-for-image',
+    );
+    waitForImageDialogShow.value = true;
+    waitForImageTimer = Timer(Duration(milliseconds: 1500), () {
+      if (waitForFirstImage.isTrue) {
+        bind.sessionInputOsPassword(sessionId: sessionId, value: '');
+      }
+    });
+    bind.sessionOnWaitingForImageDialogShow(sessionId: sessionId);
+  }
+
   _updateSessionWidthHeight(SessionID sessionId) {
     parent.target?.canvasModel.updateViewStyle();
     if (display.width <= 0 || display.height <= 0) {
@@ -430,6 +539,8 @@ class FfiModel with ChangeNotifier {
 
   /// Handle the peer info event based on [evt].
   handlePeerInfo(Map<String, dynamic> evt, String peerId) async {
+    cachedPeerData.peerInfo = evt;
+
     // recent peer updated by handle_peer_info(ui_session_interface.rs) --> handle_peer_info(client.rs) --> save_config(client.rs)
     bind.mainLoadRecentPeers();
 
@@ -448,23 +559,13 @@ class FfiModel with ChangeNotifier {
     }
 
     final connType = parent.target?.connType;
-
     if (isPeerAndroid) {
       _touchMode = true;
-      if (connType == ConnType.defaultConn &&
-          parent.target != null &&
-          parent.target!.ffiModel.permissions['keyboard'] != false) {
-        Timer(
-            const Duration(milliseconds: 100),
-            () => parent.target!.dialogManager
-                .showMobileActionsOverlay(ffi: parent.target!));
-      }
     } else {
       _touchMode = await bind.sessionGetOption(
               sessionId: sessionId, arg: 'touch-mode') !=
           '';
     }
-
     if (connType == ConnType.fileTransfer) {
       parent.target?.fileModel.onReady();
     } else if (connType == ConnType.defaultConn) {
@@ -479,11 +580,8 @@ class FfiModel with ChangeNotifier {
         _updateSessionWidthHeight(sessionId);
       }
       if (displays.isNotEmpty) {
-        parent.target?.dialogManager.showLoading(
-            translate('Connected, waiting for image...'),
-            onCancel: closeConnection);
-        _waitForImage[sessionId] = true;
         _reconnects = 1;
+        waitForFirstImage.value = true;
       }
       Map<String, dynamic> features = json.decode(evt['features']);
       _pi.features.privacyMode = features['privacy_mode'] == 1;
@@ -507,11 +605,25 @@ class FfiModel with ChangeNotifier {
       }
     }
 
+    _pi.isSet.value = true;
     stateGlobal.resetLastResolutionGroupValues(peerId);
 
     notifyListeners();
   }
 
+  tryShowAndroidActionsOverlay({int delayMSecs = 10}) {
+    if (isPeerAndroid) {
+      if (parent.target?.connType == ConnType.defaultConn &&
+          parent.target != null &&
+          parent.target!.ffiModel.permissions['keyboard'] != false) {
+        Timer(
+            Duration(milliseconds: delayMSecs),
+            () => parent.target!.dialogManager
+                .showMobileActionsOverlay(ffi: parent.target!));
+      }
+    }
+  }
+
   handleResolutions(String id, dynamic resolutions) {
     try {
       final List<dynamic> dynamicArray = jsonDecode(resolutions as String);
@@ -548,9 +660,20 @@ class FfiModel with ChangeNotifier {
     return d;
   }
 
+  handleCursorId(Map<String, dynamic> evt) async {
+    cachedPeerData.lastCursorId = evt;
+    await parent.target?.cursorModel.updateCursorId(evt);
+  }
+
+  handleCursorData(Map<String, dynamic> evt) async {
+    cachedPeerData.cursorDataList.add(evt);
+    await parent.target?.cursorModel.updateCursorData(evt);
+  }
+
   /// Handle the peer info synchronization event based on [evt].
   handleSyncPeerInfo(Map<String, dynamic> evt, SessionID sessionId) async {
     if (evt['displays'] != null) {
+      cachedPeerData.peerInfo['displays'] = evt['displays'];
       List<dynamic> displays = json.decode(evt['displays']);
       List<Display> newDisplays = [];
       for (int i = 0; i < displays.length; ++i) {
@@ -628,22 +751,6 @@ class ImageModel with ChangeNotifier {
   addCallbackOnFirstImage(Function(String) cb) => callbacksOnFirstImage.add(cb);
 
   onRgba(Uint8List rgba) {
-    final waitforImage = _waitForImage[sessionId];
-    if (waitforImage == null) {
-      debugPrint('Exception, peer $id not found for waiting image');
-      return;
-    }
-
-    if (waitforImage == true) {
-      _waitForImage[sessionId] = false;
-      parent.target?.dialogManager.dismissAll();
-      if (isDesktop) {
-        for (final cb in callbacksOnFirstImage) {
-          cb(id);
-        }
-      }
-    }
-
     final pid = parent.target?.id;
     img.decodeImageFromPixels(
         rgba,
@@ -1266,7 +1373,6 @@ class CursorModel with ChangeNotifier {
   }
 
   updatePan(double dx, double dy, bool touchMode) {
-    if (parent.target?.imageModel.image == null) return;
     if (touchMode) {
       final scale = parent.target?.canvasModel.scale ?? 1.0;
       _x += dx / scale;
@@ -1275,6 +1381,7 @@ class CursorModel with ChangeNotifier {
       notifyListeners();
       return;
     }
+    if (parent.target?.imageModel.image == null) return;
     final scale = parent.target?.canvasModel.scale ?? 1.0;
     dx /= scale;
     dy /= scale;
@@ -1528,12 +1635,13 @@ class RecordingModel with ChangeNotifier {
         sessionId: sessionId, start: true, width: width, height: height);
   }
 
-  toggle() {
+  toggle() async {
     if (isIOS) return;
     final sessionId = parent.target?.sessionId;
     if (sessionId == null) return;
     _start = !_start;
     notifyListeners();
+    await bind.sessionRecordStatus(sessionId: sessionId, status: _start);
     if (_start) {
       bind.sessionRefresh(sessionId: sessionId);
     } else {
@@ -1596,9 +1704,10 @@ class FFI {
   late final RecordingModel recordingModel; // session
   late final InputModel inputModel; // session
   late final ElevationModel elevationModel; // session
+  late final CmFileModel cmFileModel; // cm
 
-  FFI() {
-    sessionId = isDesktop ? Uuid().v4obj() : _constSessionId;
+  FFI(SessionID? sId) {
+    sessionId = sId ?? (isDesktop ? Uuid().v4obj() : _constSessionId);
     imageModel = ImageModel(WeakReference(this));
     ffiModel = FfiModel(WeakReference(this));
     cursorModel = CursorModel(WeakReference(this));
@@ -1614,6 +1723,15 @@ class FFI {
     recordingModel = RecordingModel(WeakReference(this));
     inputModel = InputModel(WeakReference(this));
     elevationModel = ElevationModel(WeakReference(this));
+    cmFileModel = CmFileModel(WeakReference(this));
+  }
+
+  /// Mobile reuse FFI
+  void mobileReset() {
+    ffiModel.waitForFirstImage.value = true;
+    ffiModel.waitForImageDialogShow.value = true;
+    ffiModel.waitForImageTimer?.cancel();
+    ffiModel.waitForImageTimer = null;
   }
 
   /// Start with the given [id]. Only transfer file if [isFileTransfer], only port forward if [isPortForward].
@@ -1623,9 +1741,11 @@ class FFI {
       bool isRdp = false,
       String? switchUuid,
       String? password,
-      bool? forceRelay}) {
+      bool? forceRelay,
+      int? tabWindowId}) {
     closed = false;
     auditNote = '';
+    if (isMobile) mobileReset();
     assert(!(isFileTransfer && isPortForward), 'more than one connect type');
     if (isFileTransfer) {
       connType = ConnType.fileTransfer;
@@ -1638,23 +1758,50 @@ class FFI {
       imageModel.id = id;
       cursorModel.id = id;
     }
-    // ignore: unused_local_variable
-    final addRes = bind.sessionAddSync(
-      sessionId: sessionId,
-      id: id,
-      isFileTransfer: isFileTransfer,
-      isPortForward: isPortForward,
-      isRdp: isRdp,
-      switchUuid: switchUuid ?? "",
-      forceRelay: forceRelay ?? false,
-      password: password ?? "",
-    );
+    // If tabWindowId != null, this session is a "tab -> window" one.
+    // Else this session is a new one.
+    if (tabWindowId == null) {
+      // ignore: unused_local_variable
+      final addRes = bind.sessionAddSync(
+        sessionId: sessionId,
+        id: id,
+        isFileTransfer: isFileTransfer,
+        isPortForward: isPortForward,
+        isRdp: isRdp,
+        switchUuid: switchUuid ?? '',
+        forceRelay: forceRelay ?? false,
+        password: password ?? '',
+      );
+    }
     final stream = bind.sessionStart(sessionId: sessionId, id: id);
     final cb = ffiModel.startEventListener(sessionId, id);
     final useTextureRender = bind.mainUseTextureRender();
+
+    final SimpleWrapper<bool> isToNewWindowNotified = SimpleWrapper(false);
     // Preserved for the rgba data.
     stream.listen((message) {
       if (closed) return;
+      if (tabWindowId != null && !isToNewWindowNotified.value) {
+        // Session is read to be moved to a new window.
+        // Get the cached data and handle the cached data.
+        Future.delayed(Duration.zero, () async {
+          final cachedData = await DesktopMultiWindow.invokeMethod(
+              tabWindowId, kWindowEventGetCachedSessionData, id);
+          if (cachedData == null) {
+            // unreachable
+            debugPrint('Unreachable, the cached data is empty.');
+            return;
+          }
+          final data = CachedPeerData.fromString(cachedData);
+          if (data == null) {
+            debugPrint('Unreachable, the cached data cannot be decoded.');
+            return;
+          }
+          ffiModel.handleCachedPeerData(data, id);
+          await bind.sessionRefresh(sessionId: sessionId);
+        });
+        isToNewWindowNotified.value = true;
+      }
       () async {
         if (message is EventToUI_Event) {
           if (message.field0 == "close") {
@@ -1674,23 +1821,16 @@ class FFI {
           }
         } else if (message is EventToUI_Rgba) {
           if (useTextureRender) {
-            if (_waitForImage[sessionId]!) {
-              _waitForImage[sessionId] = false;
-              dialogManager.dismissAll();
-              for (final cb in imageModel.callbacksOnFirstImage) {
-                cb(id);
-              }
-              await canvasModel.updateViewStyle();
-              await canvasModel.updateScrollStyle();
-            }
+            onEvent2UIRgba();
           } else {
             // Fetch the image buffer from rust codes.
             final sz = platformFFI.getRgbaSize(sessionId);
-            if (sz == null || sz == 0) {
+            if (sz == 0) {
               return;
             }
             final rgba = platformFFI.getRgba(sessionId, sz);
             if (rgba != null) {
+              onEvent2UIRgba();
               imageModel.onRgba(rgba);
             }
           }
@@ -1701,6 +1841,23 @@ class FFI {
     this.id = id;
   }
 
+  void onEvent2UIRgba() async {
+    if (ffiModel.waitForImageDialogShow.isTrue) {
+      ffiModel.waitForImageDialogShow.value = false;
+      ffiModel.waitForImageTimer?.cancel();
+      clearWaitingForImage(dialogManager, sessionId);
+    }
+    if (ffiModel.waitForFirstImage.value == true) {
+      ffiModel.waitForFirstImage.value = false;
+      dialogManager.dismissAll();
+      await canvasModel.updateViewStyle();
+      await canvasModel.updateScrollStyle();
+      for (final cb in imageModel.callbacksOnFirstImage) {
+        cb(id);
+      }
+    }
+  }
+
   /// Login with [password], choose if the client should [remember] it.
   void login(String osUsername, String osPassword, SessionID sessionId,
       String password, bool remember) {
@@ -1713,7 +1870,7 @@ class FFI {
   }
 
   /// Close the remote session.
-  Future<void> close() async {
+  Future<void> close({bool closeSession = true}) async {
     closed = true;
     chatModel.close();
     if (imageModel.image != null && !isWebDesktop) {
@@ -1731,7 +1888,9 @@ class FFI {
     ffiModel.clear();
     canvasModel.clear();
     inputModel.resetModifiers();
-    await bind.sessionClose(sessionId: sessionId);
+    if (closeSession) {
+      await bind.sessionClose(sessionId: sessionId);
+    }
     debugPrint('model $id closed');
     id = '';
   }
@@ -1804,7 +1963,7 @@ class Features {
   bool privacyMode = false;
 }
 
-class PeerInfo {
+class PeerInfo with ChangeNotifier {
   String version = '';
   String username = '';
   String hostname = '';
@@ -1816,6 +1975,8 @@ class PeerInfo {
   List<Resolution> resolutions = [];
   Map<String, dynamic> platform_additions = {};
 
+  RxBool isSet = false.obs;
+
   bool get is_wayland => platform_additions['is_wayland'] == true;
   bool get is_headless => platform_additions['headless'] == true;
 }
@@ -1837,14 +1998,14 @@ Future<void> setCanvasConfig(
   p['yCanvas'] = yCanvas;
   p['scale'] = scale;
   p['currentDisplay'] = currentDisplay;
-  await bind.sessionSetFlutterConfig(
+  await bind.sessionSetFlutterOption(
       sessionId: sessionId, k: canvasKey, v: jsonEncode(p));
 }
 
 Future<Map<String, dynamic>?> getCanvasConfig(SessionID sessionId) async {
   if (!isWebDesktop) return null;
   var p =
-      await bind.sessionGetFlutterConfig(sessionId: sessionId, k: canvasKey);
+      await bind.sessionGetFlutterOption(sessionId: sessionId, k: canvasKey);
   if (p == null || p.isEmpty) return null;
   try {
     Map<String, dynamic> m = json.decode(p);
@@ -1874,3 +2035,7 @@ Future<void> initializeCursorAndCanvas(FFI ffi) async {
       ffi.ffiModel.display.x, ffi.ffiModel.display.y, xCursor, yCursor);
   ffi.canvasModel.update(xCanvas, yCanvas, scale);
 }
+
+clearWaitingForImage(OverlayDialogManager? dialogManager, SessionID sessionId) {
+  dialogManager?.dismissByTag('$sessionId-waiting-for-image');
+}
diff --git a/flutter/lib/models/native_model.dart b/flutter/lib/models/native_model.dart
index 309c30f68..80809309a 100644
--- a/flutter/lib/models/native_model.dart
+++ b/flutter/lib/models/native_model.dart
@@ -8,6 +8,7 @@ import 'package:ffi/ffi.dart';
 import 'package:flutter/foundation.dart';
 import 'package:flutter/services.dart';
 import 'package:flutter_hbb/consts.dart';
+import 'package:flutter_hbb/main.dart';
 import 'package:package_info_plus/package_info_plus.dart';
 import 'package:path_provider/path_provider.dart';
 
@@ -20,16 +21,8 @@ class RgbaFrame extends Struct {
   external Pointer<Uint8> data;
 }
 
-typedef F2 = Pointer<Utf8> Function(Pointer<Utf8>, Pointer<Utf8>);
 typedef F3 = Pointer<Uint8> Function(Pointer<Utf8>);
-typedef F4 = Uint64 Function(Pointer<Utf8>);
-typedef F4Dart = int Function(Pointer<Utf8>);
-typedef F5 = Void Function(Pointer<Utf8>);
-typedef F5Dart = void Function(Pointer<Utf8>);
 typedef HandleEvent = Future<void> Function(Map<String, dynamic> evt);
-// pub fn session_register_texture(id: *const char, ptr: usize)
-typedef F6 = Void Function(Pointer<Utf8>, Uint64);
-typedef F6Dart = void Function(Pointer<Utf8>, int);
 
 /// FFI wrapper around the native Rust core.
 /// Hides the platform differences.
@@ -37,7 +30,6 @@ class PlatformFFI {
   String _dir = '';
   // _homeDir is only needed for Android and IOS.
   String _homeDir = '';
-  F2? _translate;
   final _eventHandlers = <String, Map<String, HandleEvent>>{};
   late RustdeskImpl _ffiBind;
   late String _appType;
@@ -50,9 +42,6 @@ class PlatformFFI {
 
   RustdeskImpl get ffiBind => _ffiBind;
   F3? _session_get_rgba;
-  F4Dart? _session_get_rgba_size;
-  F5Dart? _session_next_rgba;
-  F6Dart? _session_register_texture;
 
   static get localeName => Platform.localeName;
 
@@ -88,18 +77,8 @@ class PlatformFFI {
     }
   }
 
-  String translate(String name, String locale) {
-    if (_translate == null) return name;
-    var a = name.toNativeUtf8();
-    var b = locale.toNativeUtf8();
-    var p = _translate!(a, b);
-    assert(p != nullptr);
-    final res = p.toDartString();
-    calloc.free(p);
-    calloc.free(a);
-    calloc.free(b);
-    return res;
-  }
+  String translate(String name, String locale) =>
+      _ffiBind.translate(name: name, locale: locale);
 
   Uint8List? getRgba(SessionID sessionId, int bufSize) {
     if (_session_get_rgba == null) return null;
@@ -117,30 +96,11 @@ class PlatformFFI {
     }
   }
 
-  int? getRgbaSize(SessionID sessionId) {
-    if (_session_get_rgba_size == null) return null;
-    final sessionIdStr = sessionId.toString();
-    var a = sessionIdStr.toNativeUtf8();
-    final bufferSize = _session_get_rgba_size!(a);
-    malloc.free(a);
-    return bufferSize;
-  }
-
-  void nextRgba(SessionID sessionId) {
-    if (_session_next_rgba == null) return;
-    final sessionIdStr = sessionId.toString();
-    final a = sessionIdStr.toNativeUtf8();
-    _session_next_rgba!(a);
-    malloc.free(a);
-  }
-
-  void registerTexture(SessionID sessionId, int ptr) {
-    if (_session_register_texture == null) return;
-    final sessionIdStr = sessionId.toString();
-    final a = sessionIdStr.toNativeUtf8();
-    _session_register_texture!(a, ptr);
-    malloc.free(a);
-  }
+  int getRgbaSize(SessionID sessionId) =>
+      _ffiBind.sessionGetRgbaSize(sessionId: sessionId);
+  void nextRgba(SessionID sessionId) => _ffiBind.sessionNextRgba(sessionId: sessionId);
+  void registerTexture(SessionID sessionId, int ptr) =>
+      _ffiBind.sessionRegisterTexture(sessionId: sessionId, ptr: ptr);
 
   /// Init the FFI class, loads the native Rust core library.
   Future<void> init(String appType) async {
@@ -156,14 +116,7 @@ class PlatformFFI {
                     : DynamicLibrary.process();
     debugPrint('initializing FFI $_appType');
     try {
-      _translate = dylib.lookupFunction<F2, F2>('translate');
       _session_get_rgba = dylib.lookupFunction<F3, F3>("session_get_rgba");
-      _session_get_rgba_size =
-          dylib.lookupFunction<F4, F4Dart>("session_get_rgba_size");
-      _session_next_rgba =
-          dylib.lookupFunction<F5, F5Dart>("session_next_rgba");
-      _session_register_texture =
-          dylib.lookupFunction<F6, F6Dart>("session_register_texture");
       try {
         // SYSTEM user failed
         _dir = (await getApplicationDocumentsDirectory()).path;
@@ -233,7 +186,7 @@ class PlatformFFI {
             '_appType:$_appType,info1-id:$id,info2-name:$name,dir:$_dir');
       }
       if (desktopType == DesktopType.cm) {
-        await _ffiBind.cmStartListenIpcThread();
+        await _ffiBind.cmInit();
       }
       await _ffiBind.mainDeviceId(id: id);
       await _ffiBind.mainDeviceName(name: name);
@@ -263,7 +216,8 @@ class PlatformFFI {
 
   /// Start listening to the Rust core's events and frames.
   void _startListenEvent(RustdeskImpl rustdeskImpl) {
-    var sink = rustdeskImpl.startGlobalEventStream(appType: _appType);
+    final appType = _appType == kAppTypeDesktopRemote ? '$_appType,$kWindowId' : _appType;
+    var sink = rustdeskImpl.startGlobalEventStream(appType: appType);
     sink.listen((message) {
       () async {
         try {
diff --git a/flutter/lib/models/peer_model.dart b/flutter/lib/models/peer_model.dart
index 596355adc..4d7ac3b28 100644
--- a/flutter/lib/models/peer_model.dart
+++ b/flutter/lib/models/peer_model.dart
@@ -1,12 +1,15 @@
 import 'dart:convert';
 import 'package:flutter/foundation.dart';
 import 'platform_model.dart';
+// ignore: depend_on_referenced_packages
+import 'package:collection/collection.dart';
 
 class Peer {
   final String id;
-  final String username;
-  final String hostname;
-  final String platform;
+  String hash;
+  String username;
+  String hostname;
+  String platform;
   String alias;
   List<dynamic> tags;
   bool forceAlwaysRelay = false;
@@ -23,6 +26,7 @@ class Peer {
 
   Peer.fromJson(Map<String, dynamic> json)
       : id = json['id'] ?? '',
+        hash = json['hash'] ?? '',
         username = json['username'] ?? '',
         hostname = json['hostname'] ?? '',
         platform = json['platform'] ?? '',
@@ -35,6 +39,7 @@ class Peer {
   Map<String, dynamic> toJson() {
     return <String, dynamic>{
       "id": id,
+      "hash": hash,
       "username": username,
       "hostname": hostname,
       "platform": platform,
@@ -46,8 +51,21 @@ class Peer {
     };
   }
 
+  Map<String, dynamic> toAbUploadJson() {
+    return <String, dynamic>{
+      "id": id,
+      "hash": hash,
+      "username": username,
+      "hostname": hostname,
+      "platform": platform,
+      "alias": alias,
+      "tags": tags,
+    };
+  }
+
   Peer({
     required this.id,
+    required this.hash,
     required this.username,
     required this.hostname,
     required this.platform,
@@ -61,6 +79,7 @@ class Peer {
   Peer.loading()
       : this(
           id: '...',
+          hash: '',
           username: '...',
           hostname: '...',
           platform: '...',
@@ -70,6 +89,31 @@ class Peer {
           rdpPort: '',
           rdpUsername: '',
         );
+  bool equal(Peer other) {
+    return id == other.id &&
+        hash == other.hash &&
+        username == other.username &&
+        hostname == other.hostname &&
+        platform == other.platform &&
+        alias == other.alias &&
+        tags.equals(other.tags) &&
+        forceAlwaysRelay == other.forceAlwaysRelay &&
+        rdpPort == other.rdpPort &&
+        rdpUsername == other.rdpUsername;
+  }
+
+  Peer.copy(Peer other)
+      : this(
+            id: other.id,
+            hash: other.hash,
+            username: other.username,
+            hostname: other.hostname,
+            platform: other.platform,
+            alias: other.alias,
+            tags: other.tags.toList(),
+            forceAlwaysRelay: other.forceAlwaysRelay,
+            rdpPort: other.rdpPort,
+            rdpUsername: other.rdpUsername);
 }
 
 enum UpdateEvent { online, load }
diff --git a/flutter/lib/models/peer_tab_model.dart b/flutter/lib/models/peer_tab_model.dart
index 5c3c6b01e..2e65e64bd 100644
--- a/flutter/lib/models/peer_tab_model.dart
+++ b/flutter/lib/models/peer_tab_model.dart
@@ -1,10 +1,21 @@
+import 'dart:math';
+
 import 'package:flutter/material.dart';
+import 'package:flutter_hbb/models/peer_model.dart';
 import 'package:flutter_hbb/models/platform_model.dart';
+import 'package:get/get.dart';
 
 import '../common.dart';
 import 'model.dart';
 
-const int groupTabIndex = 4;
+enum PeerTabIndex {
+  recent,
+  fav,
+  lan,
+  ab,
+  group,
+}
+
 const String defaultGroupTabname = 'Group';
 
 class PeerTabModel with ChangeNotifier {
@@ -26,11 +37,21 @@ class PeerTabModel with ChangeNotifier {
     Icons.group,
   ];
   List<int> get indexs => List.generate(tabNames.length, (index) => index);
+  List<Peer> _selectedPeers = List.empty(growable: true);
+  List<Peer> get selectedPeers => _selectedPeers;
+  bool _multiSelectionMode = false;
+  bool get multiSelectionMode => _multiSelectionMode;
+  List<Peer> _currentTabCachedPeers = List.empty(growable: true);
+  List<Peer> get currentTabCachedPeers => _currentTabCachedPeers;
+  bool _isShiftDown = false;
+  bool get isShiftDown => _isShiftDown;
+  String _lastId = '';
+  String get lastId => _lastId;
 
   PeerTabModel(this.parent) {
     // init currentTab
     _currentTab =
-        int.tryParse(bind.getLocalFlutterConfig(k: 'peer-tab-index')) ?? 0;
+        int.tryParse(bind.getLocalFlutterOption(k: 'peer-tab-index')) ?? 0;
     if (_currentTab < 0 || _currentTab >= tabNames.length) {
       _currentTab = 0;
     }
@@ -45,7 +66,7 @@ class PeerTabModel with ChangeNotifier {
 
   String tabTooltip(int index, String groupName) {
     if (index >= 0 && index < tabNames.length) {
-      if (index == groupTabIndex) {
+      if (index == PeerTabIndex.group.index) {
         if (gFFI.userModel.isAdmin.value || groupName.isEmpty) {
           return translate(defaultGroupTabname);
         } else {
@@ -66,4 +87,75 @@ class PeerTabModel with ChangeNotifier {
     assert(false);
     return Icons.help;
   }
+
+  setMultiSelectionMode(bool mode) {
+    _multiSelectionMode = mode;
+    if (!mode) {
+      _selectedPeers.clear();
+      _lastId = '';
+    }
+    notifyListeners();
+  }
+
+  select(Peer peer) {
+    if (!_multiSelectionMode) {
+      // https://github.com/flutter/flutter/issues/101275#issuecomment-1604541700
+      // After onTap, the shift key should be pressed for a while when not in multiselection mode,
+      // because onTap is delayed when onDoubleTap is not null
+      if (isDesktop && !_isShiftDown) return;
+      _multiSelectionMode = true;
+    }
+    final cached = _currentTabCachedPeers.map((e) => e.id).toList();
+    int thisIndex = cached.indexOf(peer.id);
+    int lastIndex = cached.indexOf(_lastId);
+    if (_isShiftDown && thisIndex >= 0 && lastIndex >= 0) {
+      int start = min(thisIndex, lastIndex);
+      int end = max(thisIndex, lastIndex);
+      bool remove = isPeerSelected(peer.id);
+      for (var i = start; i <= end; i++) {
+        if (remove) {
+          if (isPeerSelected(cached[i])) {
+            _selectedPeers.removeWhere((p) => p.id == cached[i]);
+          }
+        } else {
+          if (!isPeerSelected(cached[i])) {
+            _selectedPeers.add(_currentTabCachedPeers[i]);
+          }
+        }
+      }
+    } else {
+      if (isPeerSelected(peer.id)) {
+        _selectedPeers.removeWhere((p) => p.id == peer.id);
+      } else {
+        _selectedPeers.add(peer);
+      }
+    }
+    _lastId = peer.id;
+    notifyListeners();
+  }
+
+  setCurrentTabCachedPeers(List<Peer> peers) {
+    Future.delayed(Duration.zero, () {
+      _currentTabCachedPeers = peers;
+      notifyListeners();
+    });
+  }
+
+  selectAll() {
+    _selectedPeers = _currentTabCachedPeers.toList();
+    notifyListeners();
+  }
+
+  bool isPeerSelected(String id) {
+    return selectedPeers.firstWhereOrNull((p) => p.id == id) != null;
+  }
+
+  setShiftDown(bool v) {
+    if (_isShiftDown != v) {
+      _isShiftDown = v;
+      if (_multiSelectionMode) {
+        notifyListeners();
+      }
+    }
+  }
 }
diff --git a/flutter/lib/models/server_model.dart b/flutter/lib/models/server_model.dart
index 9a6b52e7e..9fb91f463 100644
--- a/flutter/lib/models/server_model.dart
+++ b/flutter/lib/models/server_model.dart
@@ -5,6 +5,7 @@ import 'dart:io';
 import 'package:flutter/material.dart';
 import 'package:flutter_hbb/consts.dart';
 import 'package:flutter_hbb/main.dart';
+import 'package:flutter_hbb/models/chat_model.dart';
 import 'package:flutter_hbb/models/platform_model.dart';
 import 'package:get/get.dart';
 import 'package:wakelock/wakelock.dart';
@@ -30,11 +31,12 @@ class ServerModel with ChangeNotifier {
   bool _audioOk = false;
   bool _fileOk = false;
   bool _showElevation = false;
-  bool _hideCm = false;
+  bool hideCm = false;
   int _connectStatus = 0; // Rendezvous Server status
   String _verificationMethod = "";
   String _temporaryPasswordLength = "";
   String _approveMode = "";
+  int _zeroClientLengthCounter = 0;
 
   late String _emptyIdShow;
   late final IDTextEditingController _serverId;
@@ -59,8 +61,6 @@ class ServerModel with ChangeNotifier {
 
   bool get showElevation => _showElevation;
 
-  bool get hideCm => _hideCm;
-
   int get connectStatus => _connectStatus;
 
   String get verificationMethod {
@@ -119,6 +119,19 @@ class ServerModel with ChangeNotifier {
     _emptyIdShow = translate("Generating ...");
     _serverId = IDTextEditingController(text: _emptyIdShow);
 
+    /*
+    // initital _hideCm at startup
+    final verificationMethod =
+        bind.mainGetOptionSync(key: "verification-method");
+    final approveMode = bind.mainGetOptionSync(key: 'approve-mode');
+    _hideCm = option2bool(
+        'allow-hide-cm', bind.mainGetOptionSync(key: 'allow-hide-cm'));
+    if (!(approveMode == 'password' &&
+        verificationMethod == kUsePermanentPassword)) {
+      _hideCm = false;
+    }
+    */
+
     timerCallback() async {
       final connectionStatus =
           jsonDecode(await bind.mainGetConnectStatus()) as Map<String, dynamic>;
@@ -133,6 +146,17 @@ class ServerModel with ChangeNotifier {
         if (res != null) {
           debugPrint("clients not match!");
           updateClientState(res);
+        } else {
+          if (_clients.isEmpty) {
+            hideCmWindow();
+            if (_zeroClientLengthCounter++ == 12) {
+              // 6 second
+              windowManager.close();
+            }
+          } else {
+            _zeroClientLengthCounter = 0;
+            if (!hideCm) showCmWindow();
+          }
         }
       }
 
@@ -186,27 +210,30 @@ class ServerModel with ChangeNotifier {
     final temporaryPasswordLength =
         await bind.mainGetOption(key: "temporary-password-length");
     final approveMode = await bind.mainGetOption(key: 'approve-mode');
+    /*
     var hideCm = option2bool(
         'allow-hide-cm', await bind.mainGetOption(key: 'allow-hide-cm'));
     if (!(approveMode == 'password' &&
         verificationMethod == kUsePermanentPassword)) {
       hideCm = false;
     }
+    */
     if (_approveMode != approveMode) {
       _approveMode = approveMode;
       update = true;
     }
-    final oldPwdText = _serverPasswd.text;
-    if (_serverPasswd.text != temporaryPassword &&
-        temporaryPassword.isNotEmpty) {
-      _serverPasswd.text = temporaryPassword;
-    }
     var stopped = option2bool(
         "stop-service", await bind.mainGetOption(key: "stop-service"));
+    final oldPwdText = _serverPasswd.text;
     if (stopped ||
         verificationMethod == kUsePermanentPassword ||
         _approveMode == 'click') {
       _serverPasswd.text = '-';
+    } else {
+      if (_serverPasswd.text != temporaryPassword &&
+          temporaryPassword.isNotEmpty) {
+        _serverPasswd.text = temporaryPassword;
+      }
     }
     if (oldPwdText != _serverPasswd.text) {
       update = true;
@@ -216,9 +243,13 @@ class ServerModel with ChangeNotifier {
       update = true;
     }
     if (_temporaryPasswordLength != temporaryPasswordLength) {
+      if (_temporaryPasswordLength.isNotEmpty) {
+        bind.mainUpdateTemporaryPassword();
+      }
       _temporaryPasswordLength = temporaryPasswordLength;
       update = true;
     }
+    /*
     if (_hideCm != hideCm) {
       _hideCm = hideCm;
       if (desktopType == DesktopType.cm) {
@@ -230,6 +261,7 @@ class ServerModel with ChangeNotifier {
       }
       update = true;
     }
+    */
     if (update) {
       notifyListeners();
     }
@@ -409,18 +441,36 @@ class ServerModel with ChangeNotifier {
   updateClientState([String? json]) async {
     if (isTest) return;
     var res = await bind.cmGetClientsState();
+    List<dynamic> clientsJson;
     try {
-      final List clientsJson = jsonDecode(res);
-      _clients.clear();
-      tabController.state.value.tabs.clear();
-      for (var clientJson in clientsJson) {
+      clientsJson = jsonDecode(res);
+    } catch (e) {
+      debugPrint("Failed to decode clientsJson: '$res', error $e");
+      return;
+    }
+
+    final oldClientLenght = _clients.length;
+    _clients.clear();
+    tabController.state.value.tabs.clear();
+
+    for (var clientJson in clientsJson) {
+      try {
         final client = Client.fromJson(clientJson);
         _clients.add(client);
         _addTab(client);
+      } catch (e) {
+        debugPrint("Failed to decode clientJson '$clientJson', error $e");
       }
+    }
+    if (desktopType == DesktopType.cm) {
+      if (_clients.isEmpty) {
+        hideCmWindow();
+      } else if (!hideCm) {
+        showCmWindow();
+      }
+    }
+    if (_clients.length != oldClientLenght) {
       notifyListeners();
-    } catch (e) {
-      debugPrint("Failed to updateClientState:$e");
     }
   }
 
@@ -449,6 +499,9 @@ class ServerModel with ChangeNotifier {
         _clients.removeAt(index_disconnected);
         tabController.remove(index_disconnected);
       }
+      if (desktopType == DesktopType.cm && !hideCm) {
+        showCmWindow();
+      }
       scrollToBottom();
       notifyListeners();
       if (isAndroid && !client.authorized) showLoginDialog(client);
@@ -462,16 +515,10 @@ class ServerModel with ChangeNotifier {
         key: client.id.toString(),
         label: client.name,
         closable: false,
-        onTap: () {
-          if (client.hasUnreadChatMessage.value) {
-            client.hasUnreadChatMessage.value = false;
-            final chatModel = parent.target!.chatModel;
-            chatModel.showChatPage(client.id);
-          }
-        },
+        onTap: () {},
         page: desktop.buildConnectionCard(client)));
     Future.delayed(Duration.zero, () async {
-      if (!hideCm) window_on_top(null);
+      if (!hideCm) windowOnTop(null);
     });
     // Only do the hidden task when on Desktop.
     if (client.authorized && isDesktop) {
@@ -480,6 +527,8 @@ class ServerModel with ChangeNotifier {
         cmHiddenTimer = null;
       });
     }
+    parent.target?.chatModel
+        .updateConnIdOfKey(MessageKey(client.peerId, client.id));
   }
 
   void showLoginDialog(Client client) {
@@ -573,6 +622,9 @@ class ServerModel with ChangeNotifier {
         parent.target?.dialogManager.dismissByTag(getLoginDialogTag(id));
         parent.target?.invokeMethod("cancel_notification", id);
       }
+      if (desktopType == DesktopType.cm && _clients.isEmpty) {
+        hideCmWindow();
+      }
       notifyListeners();
     } catch (e) {
       debugPrint("onClientRemove failed,error:$e");
@@ -608,7 +660,7 @@ class ServerModel with ChangeNotifier {
         if (client.incomingVoiceCall) {
           // Has incoming phone call, let's set the window on top.
           Future.delayed(Duration.zero, () {
-            window_on_top(null);
+            windowOnTop(null);
           });
         }
         notifyListeners();
@@ -643,7 +695,7 @@ class Client {
   bool inVoiceCall = false;
   bool incomingVoiceCall = false;
 
-  RxBool hasUnreadChatMessage = false.obs;
+  RxInt unreadChatMessageCount = 0.obs;
 
   Client(this.id, this.authorized, this.isFileTransfer, this.name, this.peerId,
       this.keyboard, this.clipboard, this.audio);
diff --git a/flutter/lib/models/state_model.dart b/flutter/lib/models/state_model.dart
index f7b4f8cc2..e36bef924 100644
--- a/flutter/lib/models/state_model.dart
+++ b/flutter/lib/models/state_model.dart
@@ -10,22 +10,25 @@ enum SvcStatus { notReady, connecting, ready }
 
 class StateGlobal {
   int _windowId = -1;
-  bool _fullscreen = false;
-  bool _maximize = false;
   bool grabKeyboard = false;
+  bool _fullscreen = false;
+  bool _isMinimized = false;
+  final RxBool isMaximized = false.obs;
   final RxBool _showTabBar = true.obs;
   final RxDouble _resizeEdgeSize = RxDouble(kWindowEdgeSize);
   final RxDouble _windowBorderWidth = RxDouble(kWindowBorderWidth);
   final RxBool showRemoteToolBar = false.obs;
   final RxInt displaysCount = 0.obs;
   final svcStatus = SvcStatus.notReady.obs;
+  // Only used for macOS
+  bool closeOnFullscreen = false;
 
   // Use for desktop -> remote toolbar -> resolution
   final Map<String, Map<int, String?>> _lastResolutionGroupValues = {};
 
   int get windowId => _windowId;
   bool get fullscreen => _fullscreen;
-  bool get maximize => _maximize;
+  bool get isMinimized => _isMinimized;
   double get tabBarHeight => fullscreen ? 0 : kDesktopRemoteTabBarHeight;
   RxBool get showTabBar => _showTabBar;
   RxDouble get resizeEdgeSize => _resizeEdgeSize;
@@ -48,39 +51,49 @@ class StateGlobal {
   }
 
   setWindowId(int id) => _windowId = id;
-  setMaximize(bool v) {
-    if (_maximize != v && !_fullscreen) {
-      _maximize = v;
-      _resizeEdgeSize.value = _maximize ? kMaximizeEdgeSize : kWindowEdgeSize;
+  setMaximized(bool v) {
+    if (!_fullscreen) {
+      if (isMaximized.value != v) {
+        isMaximized.value = v;
+        _resizeEdgeSize.value =
+            isMaximized.isTrue ? kMaximizeEdgeSize : kWindowEdgeSize;
+      }
+      if (!Platform.isMacOS) {
+        _windowBorderWidth.value = v ? 0 : kWindowBorderWidth;
+      }
     }
   }
 
-  setFullscreen(bool v) {
+  setMinimized(bool v) => _isMinimized = v;
+
+  setFullscreen(bool v, {bool procWnd = true}) {
     if (_fullscreen != v) {
       _fullscreen = v;
       _showTabBar.value = !_fullscreen;
       _resizeEdgeSize.value = fullscreen
           ? kFullScreenEdgeSize
-          : _maximize
+          : isMaximized.isTrue
               ? kMaximizeEdgeSize
               : kWindowEdgeSize;
       print(
-          "fullscreen: ${fullscreen}, resizeEdgeSize: ${_resizeEdgeSize.value}");
+          "fullscreen: $fullscreen, resizeEdgeSize: ${_resizeEdgeSize.value}");
       _windowBorderWidth.value = fullscreen ? 0 : kWindowBorderWidth;
-      WindowController.fromWindowId(windowId)
-          .setFullscreen(_fullscreen)
-          .then((_) {
-        // https://github.com/leanflutter/window_manager/issues/131#issuecomment-1111587982
-        if (Platform.isWindows && !v) {
-          Future.delayed(Duration.zero, () async {
-            final frame =
-                await WindowController.fromWindowId(windowId).getFrame();
-            final newRect = Rect.fromLTWH(
-                frame.left, frame.top, frame.width + 1, frame.height + 1);
-            await WindowController.fromWindowId(windowId).setFrame(newRect);
-          });
-        }
-      });
+      if (procWnd) {
+        WindowController.fromWindowId(windowId)
+            .setFullscreen(_fullscreen)
+            .then((_) {
+          // https://github.com/leanflutter/window_manager/issues/131#issuecomment-1111587982
+          if (Platform.isWindows && !v) {
+            Future.delayed(Duration.zero, () async {
+              final frame =
+                  await WindowController.fromWindowId(windowId).getFrame();
+              final newRect = Rect.fromLTWH(
+                  frame.left, frame.top, frame.width + 1, frame.height + 1);
+              await WindowController.fromWindowId(windowId).setFrame(newRect);
+            });
+          }
+        });
+      }
     }
   }
 
diff --git a/flutter/lib/models/user_model.dart b/flutter/lib/models/user_model.dart
index 5730f5054..559e8be36 100644
--- a/flutter/lib/models/user_model.dart
+++ b/flutter/lib/models/user_model.dart
@@ -1,6 +1,7 @@
 import 'dart:async';
 import 'dart:convert';
 
+import 'package:bot_toast/bot_toast.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter_hbb/common/hbbs/hbbs.dart';
 import 'package:get/get.dart';
@@ -15,6 +16,7 @@ bool refreshingUser = false;
 class UserModel {
   final RxString userName = ''.obs;
   final RxBool isAdmin = false.obs;
+  bool get isLogin => userName.isNotEmpty;
   WeakReference<FFI> parent;
 
   UserModel(this.parent);
@@ -43,7 +45,7 @@ class UserModel {
       refreshingUser = false;
       final status = response.statusCode;
       if (status == 401 || status == 400) {
-        reset();
+        reset(clearAbCache: status == 401);
         return;
       }
       final data = json.decode(utf8.decode(response.bodyBytes));
@@ -82,10 +84,10 @@ class UserModel {
     }
   }
 
-  Future<void> reset() async {
+  Future<void> reset({bool clearAbCache = false}) async {
     await bind.mainSetLocalOption(key: 'access_token', value: '');
     await bind.mainSetLocalOption(key: 'user_info', value: '');
-    await gFFI.abModel.reset();
+    if (clearAbCache) await bind.mainClearAb();
     await gFFI.groupModel.reset();
     userName.value = '';
   }
@@ -93,6 +95,7 @@ class UserModel {
   _parseAndUpdateUser(UserPayload user) {
     userName.value = user.name;
     isAdmin.value = user.isAdmin;
+    bind.mainSetLocalOption(key: 'user_info', value: jsonEncode(user));
   }
 
   // update ab and group status
@@ -100,10 +103,10 @@ class UserModel {
     await Future.wait([gFFI.abModel.pullAb(), gFFI.groupModel.pull()]);
   }
 
-  Future<void> logOut() async {
+  Future<void> logOut({String? apiServer}) async {
     final tag = gFFI.dialogManager.showLoading(translate('Waiting'));
     try {
-      final url = await bind.mainGetApiServer();
+      final url = apiServer ?? await bind.mainGetApiServer();
       final authHeaders = getHttpHeaders();
       authHeaders['Content-Type'] = "application/json";
       await http
@@ -117,7 +120,7 @@ class UserModel {
     } catch (e) {
       debugPrint("request /api/logout failed: err=$e");
     } finally {
-      await reset();
+      await reset(clearAbCache: true);
       gFFI.dialogManager.dismissByTag(tag);
     }
   }
@@ -134,6 +137,10 @@ class UserModel {
       body = jsonDecode(utf8.decode(resp.bodyBytes));
     } catch (e) {
       debugPrint("login: jsonDecode resp body failed: ${e.toString()}");
+      if (resp.statusCode != 200) {
+        BotToast.showText(
+            contentColor: Colors.red, text: 'HTTP ${resp.statusCode}');
+      }
       rethrow;
     }
     if (resp.statusCode != 200) {
@@ -162,15 +169,27 @@ class UserModel {
     return loginResponse;
   }
 
-  static Future<List<dynamic>> queryLoginOptions() async {
+  static Future<List<dynamic>> queryOidcLoginOptions() async {
     try {
       final url = await bind.mainGetApiServer();
       if (url.trim().isEmpty) return [];
       final resp = await http.get(Uri.parse('$url/api/login-options'));
-      return jsonDecode(resp.body);
+      final List<String> ops = [];
+      for (final item in jsonDecode(resp.body)) {
+        ops.add(item as String);
+      }
+      for (final item in ops) {
+        if (item.startsWith('common-oidc/')) {
+          return jsonDecode(item.substring('common-oidc/'.length));
+        }
+      }
+      return ops
+          .where((item) => item.startsWith('oidc/'))
+          .map((item) => {'name': item.substring('oidc/'.length)})
+          .toList();
     } catch (e) {
       debugPrint(
-          "queryLoginOptions: jsonDecode resp body failed: ${e.toString()}");
+          "queryOidcLoginOptions: jsonDecode resp body failed: ${e.toString()}");
       return [];
     }
   }
diff --git a/flutter/lib/utils/multi_window_manager.dart b/flutter/lib/utils/multi_window_manager.dart
index ccd976fa6..a8be78c74 100644
--- a/flutter/lib/utils/multi_window_manager.dart
+++ b/flutter/lib/utils/multi_window_manager.dart
@@ -5,6 +5,7 @@ import 'package:desktop_multi_window/desktop_multi_window.dart';
 import 'package:flutter/foundation.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter/services.dart';
+import 'package:flutter_hbb/consts.dart';
 import 'package:flutter_hbb/common.dart';
 
 /// must keep the order
@@ -27,6 +28,13 @@ extension Index on int {
   }
 }
 
+class MultiWindowCallResult {
+  int windowId;
+  dynamic result;
+
+  MultiWindowCallResult(this.windowId, this.result);
+}
+
 /// Window Manager
 /// mainly use it in `Main Window`
 /// use it in sub window is not recommended
@@ -35,143 +43,197 @@ class RustDeskMultiWindowManager {
 
   static final instance = RustDeskMultiWindowManager._();
 
-  final List<int> _activeWindows = List.empty(growable: true);
+  final Set<int> _inactiveWindows = {};
+  final Set<int> _activeWindows = {};
   final List<AsyncCallback> _windowActiveCallbacks = List.empty(growable: true);
-  int? _remoteDesktopWindowId;
-  int? _fileTransferWindowId;
-  int? _portForwardWindowId;
+  final List<int> _remoteDesktopWindows = List.empty(growable: true);
+  final List<int> _fileTransferWindows = List.empty(growable: true);
+  final List<int> _portForwardWindows = List.empty(growable: true);
 
-  Future<dynamic> newRemoteDesktop(
-    String remoteId, {
+  moveTabToNewWindow(int windowId, String peerId, String sessionId) async {
+    var params = {
+      'type': WindowType.RemoteDesktop.index,
+      'id': peerId,
+      'tab_window_id': windowId,
+      'session_id': sessionId,
+    };
+    await _newSession(
+      false,
+      WindowType.RemoteDesktop,
+      kWindowEventNewRemoteDesktop,
+      peerId,
+      _remoteDesktopWindows,
+      jsonEncode(params),
+    );
+  }
+
+  Future<int> newSessionWindow(
+      WindowType type, String remoteId, String msg, List<int> windows) async {
+    final windowController = await DesktopMultiWindow.createWindow(msg);
+    final windowId = windowController.windowId;
+    windowController
+      ..setFrame(
+          const Offset(0, 0) & Size(1280 + windowId * 20, 720 + windowId * 20))
+      ..center()
+      ..setTitle(getWindowNameWithId(
+        remoteId,
+        overrideType: type,
+      ));
+    if (Platform.isMacOS) {
+      Future.microtask(() => windowController.show());
+    }
+    registerActiveWindow(windowId);
+    windows.add(windowId);
+    return windowId;
+  }
+
+  Future<MultiWindowCallResult> _newSession(
+    bool openInTabs,
+    WindowType type,
+    String methodName,
+    String remoteId,
+    List<int> windows,
+    String msg,
+  ) async {
+    if (openInTabs) {
+      if (windows.isEmpty) {
+        final windowId = await newSessionWindow(type, remoteId, msg, windows);
+        return MultiWindowCallResult(windowId, null);
+      } else {
+        return call(type, methodName, msg);
+      }
+    } else {
+      if (_inactiveWindows.isNotEmpty) {
+        for (final windowId in windows) {
+          if (_inactiveWindows.contains(windowId)) {
+            await restoreWindowPosition(type,
+                windowId: windowId, peerId: remoteId);
+            await DesktopMultiWindow.invokeMethod(windowId, methodName, msg);
+            WindowController.fromWindowId(windowId).show();
+            registerActiveWindow(windowId);
+            return MultiWindowCallResult(windowId, null);
+          }
+        }
+      }
+      final windowId = await newSessionWindow(type, remoteId, msg, windows);
+      return MultiWindowCallResult(windowId, null);
+    }
+  }
+
+  Future<MultiWindowCallResult> newSession(
+    WindowType type,
+    String methodName,
+    String remoteId,
+    List<int> windows, {
     String? password,
-    String? switch_uuid,
     bool? forceRelay,
+    String? switchUuid,
+    bool? isRDP,
   }) async {
     var params = {
-      "type": WindowType.RemoteDesktop.index,
+      "type": type.index,
       "id": remoteId,
       "password": password,
       "forceRelay": forceRelay
     };
-    if (switch_uuid != null) {
-      params['switch_uuid'] = switch_uuid;
+    if (switchUuid != null) {
+      params['switch_uuid'] = switchUuid;
+    }
+    if (isRDP != null) {
+      params['isRDP'] = isRDP;
     }
     final msg = jsonEncode(params);
 
-    try {
-      final ids = await DesktopMultiWindow.getAllSubWindowIds();
-      if (!ids.contains(_remoteDesktopWindowId)) {
-        _remoteDesktopWindowId = null;
+    // separate window for file transfer is not supported
+    bool openInTabs = type != WindowType.RemoteDesktop ||
+        mainGetLocalBoolOptionSync(kOptionOpenNewConnInTabs);
+
+    if (windows.length > 1 || !openInTabs) {
+      for (final windowId in windows) {
+        if (await DesktopMultiWindow.invokeMethod(
+            windowId, kWindowEventActiveSession, remoteId)) {
+          return MultiWindowCallResult(windowId, 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(getWindowNameWithId(remoteId,
-            overrideType: WindowType.RemoteDesktop));
-      if (Platform.isMacOS) {
-        Future.microtask(() => remoteDesktopController.show());
-      }
-      registerActiveWindow(remoteDesktopController.windowId);
-      _remoteDesktopWindowId = remoteDesktopController.windowId;
-    } else {
-      return call(WindowType.RemoteDesktop, "new_remote_desktop", msg);
     }
+
+    return _newSession(openInTabs, type, methodName, remoteId, windows, msg);
   }
 
-  Future<dynamic> newFileTransfer(String remoteId, {bool? forceRelay}) async {
-    var msg = jsonEncode({
-      "type": WindowType.FileTransfer.index,
-      "id": remoteId,
-      "forceRelay": forceRelay,
-    });
-
-    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(getWindowNameWithId(remoteId,
-            overrideType: WindowType.FileTransfer));
-      if (Platform.isMacOS) {
-        Future.microtask(() => fileTransferController.show());
-      }
-      registerActiveWindow(fileTransferController.windowId);
-      _fileTransferWindowId = fileTransferController.windowId;
-    } else {
-      return call(WindowType.FileTransfer, "new_file_transfer", msg);
-    }
+  Future<MultiWindowCallResult> newRemoteDesktop(
+    String remoteId, {
+    String? password,
+    String? switchUuid,
+    bool? forceRelay,
+  }) async {
+    return await newSession(
+      WindowType.RemoteDesktop,
+      kWindowEventNewRemoteDesktop,
+      remoteId,
+      _remoteDesktopWindows,
+      password: password,
+      forceRelay: forceRelay,
+      switchUuid: switchUuid,
+    );
   }
 
-  Future<dynamic> newPortForward(String remoteId, bool isRDP,
-      {bool? forceRelay}) async {
-    final msg = jsonEncode({
-      "type": WindowType.PortForward.index,
-      "id": remoteId,
-      "isRDP": isRDP,
-      "forceRelay": forceRelay,
-    });
-
-    try {
-      final ids = await DesktopMultiWindow.getAllSubWindowIds();
-      if (!ids.contains(_portForwardWindowId)) {
-        _portForwardWindowId = null;
-      }
-    } on Error {
-      _portForwardWindowId = null;
-    }
-    if (_portForwardWindowId == null) {
-      final portForwardController = await DesktopMultiWindow.createWindow(msg);
-      portForwardController
-        ..setFrame(const Offset(0, 0) & const Size(1280, 720))
-        ..center()
-        ..setTitle(getWindowNameWithId(remoteId,
-            overrideType: WindowType.PortForward));
-      if (Platform.isMacOS) {
-        Future.microtask(() => portForwardController.show());
-      }
-      registerActiveWindow(portForwardController.windowId);
-      _portForwardWindowId = portForwardController.windowId;
-    } else {
-      return call(WindowType.PortForward, "new_port_forward", msg);
-    }
+  Future<MultiWindowCallResult> newFileTransfer(String remoteId,
+      {String? password, bool? forceRelay}) async {
+    return await newSession(
+      WindowType.FileTransfer,
+      kWindowEventNewFileTransfer,
+      remoteId,
+      _fileTransferWindows,
+      password: password,
+      forceRelay: forceRelay,
+    );
   }
 
-  Future<dynamic> call(WindowType type, String methodName, dynamic args) async {
-    int? windowId = findWindowByType(type);
-    if (windowId == null) {
-      return;
-    }
-    return await DesktopMultiWindow.invokeMethod(windowId, methodName, args);
+  Future<MultiWindowCallResult> newPortForward(String remoteId, bool isRDP,
+      {String? password, bool? forceRelay}) async {
+    return await newSession(
+      WindowType.PortForward,
+      kWindowEventNewPortForward,
+      remoteId,
+      _portForwardWindows,
+      password: password,
+      forceRelay: forceRelay,
+      isRDP: isRDP,
+    );
   }
 
-  int? findWindowByType(WindowType type) {
+  Future<MultiWindowCallResult> call(
+      WindowType type, String methodName, dynamic args) async {
+    final wnds = _findWindowsByType(type);
+    if (wnds.isEmpty) {
+      return MultiWindowCallResult(kInvalidWindowId, null);
+    }
+    for (final windowId in wnds) {
+      if (_activeWindows.contains(windowId)) {
+        final res =
+            await DesktopMultiWindow.invokeMethod(windowId, methodName, args);
+        return MultiWindowCallResult(windowId, res);
+      }
+    }
+    final res =
+        await DesktopMultiWindow.invokeMethod(wnds[0], methodName, args);
+    return MultiWindowCallResult(wnds[0], res);
+  }
+
+  List<int> _findWindowsByType(WindowType type) {
     switch (type) {
       case WindowType.Main:
-        return 0;
+        return [kMainWindowId];
       case WindowType.RemoteDesktop:
-        return _remoteDesktopWindowId;
+        return _remoteDesktopWindows;
       case WindowType.FileTransfer:
-        return _fileTransferWindowId;
+        return _fileTransferWindows;
       case WindowType.PortForward:
-        return _portForwardWindowId;
+        return _portForwardWindows;
       case WindowType.Unknown:
         break;
     }
-    return null;
+    return [];
   }
 
   void clearWindowType(WindowType type) {
@@ -179,13 +241,13 @@ class RustDeskMultiWindowManager {
       case WindowType.Main:
         return;
       case WindowType.RemoteDesktop:
-        _remoteDesktopWindowId = null;
+        _remoteDesktopWindows.clear();
         break;
       case WindowType.FileTransfer:
-        _fileTransferWindowId = null;
+        _fileTransferWindows.clear();
         break;
       case WindowType.PortForward:
-        _portForwardWindowId = null;
+        _portForwardWindows.clear();
         break;
       case WindowType.Unknown:
         break;
@@ -206,27 +268,37 @@ class RustDeskMultiWindowManager {
       // skip main window, use window manager instead
       return;
     }
-    int? wId = findWindowByType(type);
-    if (wId != null) {
+
+    List<int> windows = [];
+    try {
+      windows = await DesktopMultiWindow.getAllSubWindowIds();
+    } catch (e) {
+      debugPrint('Failed to getAllSubWindowIds of $type, $e');
+      return;
+    }
+
+    if (windows.isEmpty) {
+      return;
+    }
+    for (final wId in windows) {
       debugPrint("closing multi window: ${type.toString()}");
       await saveWindowPosition(type, windowId: wId);
       try {
-        final ids = await DesktopMultiWindow.getAllSubWindowIds();
-        if (!ids.contains(wId)) {
-          // no such window already
-          return;
-        }
+        // final ids = await DesktopMultiWindow.getAllSubWindowIds();
+        // if (!ids.contains(wId)) {
+        //   // no such window already
+        //   return;
+        // }
         await WindowController.fromWindowId(wId).setPreventClose(false);
         await WindowController.fromWindowId(wId).close();
-        // unregister the sub window in the main window.
-        unregisterActiveWindow(wId);
+        _activeWindows.remove(wId);
       } catch (e) {
         debugPrint("$e");
         return;
-      } finally {
-        clearWindowType(type);
       }
     }
+    await _notifyActiveWindow();
+    clearWindowType(type);
   }
 
   Future<List<int>> getAllSubWindowIds() async {
@@ -242,7 +314,7 @@ class RustDeskMultiWindowManager {
     }
   }
 
-  List<int> getActiveWindows() {
+  Set<int> getActiveWindows() {
     return _activeWindows;
   }
 
@@ -253,14 +325,19 @@ class RustDeskMultiWindowManager {
   }
 
   Future<void> registerActiveWindow(int windowId) async {
-    if (_activeWindows.contains(windowId)) {
-      // ignore
-    } else {
-      _activeWindows.add(windowId);
-    }
+    _activeWindows.add(windowId);
+    _inactiveWindows.remove(windowId);
     await _notifyActiveWindow();
   }
 
+  Future<void> destroyWindow(int windowId) async {
+    await WindowController.fromWindowId(windowId).setPreventClose(false);
+    await WindowController.fromWindowId(windowId).close();
+    _remoteDesktopWindows.remove(windowId);
+    _fileTransferWindows.remove(windowId);
+    _portForwardWindows.remove(windowId);
+  }
+
   /// Remove active window which has [`windowId`]
   ///
   /// [Availability]
@@ -268,10 +345,9 @@ class RustDeskMultiWindowManager {
   /// For other windows, please post a unregister(hide) event to main window handler:
   /// `rustDeskWinManager.call(WindowType.Main, kWindowEventHide, {"id": windowId!});`
   Future<void> unregisterActiveWindow(int windowId) async {
-    if (!_activeWindows.contains(windowId)) {
-      // ignore
-    } else {
-      _activeWindows.remove(windowId);
+    _activeWindows.remove(windowId);
+    if (windowId != kMainWindowId) {
+      _inactiveWindows.add(windowId);
     }
     await _notifyActiveWindow();
   }
diff --git a/flutter/linux/my_application.cc b/flutter/linux/my_application.cc
index 21e25fa28..b96c16f3f 100644
--- a/flutter/linux/my_application.cc
+++ b/flutter/linux/my_application.cc
@@ -64,7 +64,7 @@ static void my_application_activate(GApplication* application) {
   int width = 800, height = 600;
   if (gIsConnectionManager) {
     width = 300;
-    height = 400;
+    height = 490;
   }
   gtk_window_set_default_size(window, width, height);   // <-- comment this line
   gtk_widget_show(GTK_WIDGET(window));
diff --git a/flutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png b/flutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png
index fc39cb2ff..e42ee2c42 100644
Binary files a/flutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png and b/flutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png differ
diff --git a/flutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png b/flutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png
index 3bd2b7ede..6452b8c17 100644
Binary files a/flutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png and b/flutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png differ
diff --git a/flutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png b/flutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png
index 88f2eee49..80e9e0785 100644
Binary files a/flutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png and b/flutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png differ
diff --git a/flutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png b/flutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png
index 18151e82b..14a4a5900 100644
Binary files a/flutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png and b/flutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png differ
diff --git a/flutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png b/flutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png
index f8d7befb3..4653354f1 100644
Binary files a/flutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png and b/flutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png differ
diff --git a/flutter/macos/Runner/Info.plist b/flutter/macos/Runner/Info.plist
index 96616e8c4..ff9322417 100644
--- a/flutter/macos/Runner/Info.plist
+++ b/flutter/macos/Runner/Info.plist
@@ -37,8 +37,6 @@
 	<string>$(FLUTTER_BUILD_NUMBER)</string>
 	<key>LSMinimumSystemVersion</key>
 	<string>$(MACOSX_DEPLOYMENT_TARGET)</string>
-	<key>LSUIElement</key>
-	<string>1</string>
 	<key>NSHumanReadableCopyright</key>
 	<string>$(PRODUCT_COPYRIGHT)</string>
 	<key>NSMainNibFile</key>
diff --git a/flutter/pubspec.lock b/flutter/pubspec.lock
index 127e72993..b6aca5429 100644
--- a/flutter/pubspec.lock
+++ b/flutter/pubspec.lock
@@ -5,10 +5,10 @@ packages:
     dependency: transitive
     description:
       name: _fe_analyzer_shared
-      sha256: "0c80aeab9bc807ab10022cd3b2f4cf2ecdf231949dc1ddd9442406a003f19201"
+      sha256: eb376e9acf6938204f90eb3b1f00b578640d3188b4c8a8ec054f9f479af8d051
       url: "https://pub.dev"
     source: hosted
-    version: "52.0.0"
+    version: "64.0.0"
   after_layout:
     dependency: transitive
     description:
@@ -21,10 +21,10 @@ packages:
     dependency: transitive
     description:
       name: analyzer
-      sha256: cd8ee83568a77f3ae6b913a36093a1c9b1264e7cb7f834d9ddd2311dade9c1f4
+      sha256: "69f54f967773f6c26c7dcb13e93d7ccee8b17a641689da39e878d5cf13b06893"
       url: "https://pub.dev"
     source: hosted
-    version: "5.4.0"
+    version: "6.2.0"
   animations:
     dependency: transitive
     description:
@@ -65,6 +65,14 @@ packages:
       url: "https://pub.dev"
     source: hosted
     version: "3.0.0"
+  auto_size_text_field:
+    dependency: "direct main"
+    description:
+      name: auto_size_text_field
+      sha256: "8967129167193fefbb7a8707ade1bb71f9e52b9a5cf6da0132b7f6b7946c5a3f"
+      url: "https://pub.dev"
+    source: hosted
+    version: "2.2.1"
   back_button_interceptor:
     dependency: "direct main"
     description:
@@ -85,10 +93,10 @@ packages:
     dependency: transitive
     description:
       name: build
-      sha256: "3fbda25365741f8251b39f3917fb3c8e286a96fd068a5a242e11c2012d495777"
+      sha256: "80184af8b6cb3e5c1c4ec6d8544d27711700bc3e6d2efad04238c7b5290889f0"
       url: "https://pub.dev"
     source: hosted
-    version: "2.3.1"
+    version: "2.4.1"
   build_cli_annotations:
     dependency: transitive
     description:
@@ -109,26 +117,26 @@ packages:
     dependency: transitive
     description:
       name: build_daemon
-      sha256: "6bc5544ea6ce4428266e7ea680e945c68806c4aae2da0eb5e9ccf38df8d6acbf"
+      sha256: "5f02d73eb2ba16483e693f80bee4f088563a820e47d1027d4cdfe62b5bb43e65"
       url: "https://pub.dev"
     source: hosted
-    version: "3.1.0"
+    version: "4.0.0"
   build_resolvers:
     dependency: transitive
     description:
       name: build_resolvers
-      sha256: "7c35a3a7868626257d8aee47b51c26b9dba11eaddf3431117ed2744951416aab"
+      sha256: "6c4dd11d05d056e76320b828a1db0fc01ccd376922526f8e9d6c796a5adbac20"
       url: "https://pub.dev"
     source: hosted
-    version: "2.1.0"
+    version: "2.2.1"
   build_runner:
     dependency: "direct dev"
     description:
       name: build_runner
-      sha256: b0a8a7b8a76c493e85f1b84bffa0588859a06197863dba8c9036b15581fd9727
+      sha256: "10c6bcdbf9d049a0b666702cf1cee4ddfdc38f02a19d35ae392863b47519848b"
       url: "https://pub.dev"
     source: hosted
-    version: "2.3.3"
+    version: "2.4.6"
   build_runner_core:
     dependency: transitive
     description:
@@ -285,18 +293,19 @@ packages:
     dependency: transitive
     description:
       name: dart_style
-      sha256: "7a03456c3490394c8e7665890333e91ae8a49be43542b616e414449ac358acd4"
+      sha256: "1efa911ca7086affd35f463ca2fc1799584fb6aa89883cf0af8e3664d6a02d55"
       url: "https://pub.dev"
     source: hosted
-    version: "2.2.4"
+    version: "2.3.2"
   dash_chat_2:
     dependency: "direct main"
     description:
-      name: dash_chat_2
-      sha256: e9e08b2a030d340d60f7adbeb977d3d6481db1f172b51440bfa02488b92fa19c
-      url: "https://pub.dev"
-    source: hosted
-    version: "0.0.17"
+      path: "."
+      ref: HEAD
+      resolved-ref: bd6b5b41254e57c5bcece202ebfb234de63e6487
+      url: "https://github.com/rustdesk-org/Dash-Chat-2"
+    source: git
+    version: "0.0.18"
   debounce_throttle:
     dependency: "direct main"
     description:
@@ -319,7 +328,7 @@ packages:
     description:
       path: "."
       ref: HEAD
-      resolved-ref: "30518303e28702bf6b8110465293c05d21bc4cd2"
+      resolved-ref: "6c4181330f4ed80c1cb5670bd61aa75115f9f748"
       url: "https://github.com/rustdesk-org/rustdesk_desktop_multi_window"
     source: git
     version: "0.1.0"
@@ -443,6 +452,22 @@ packages:
       url: "https://pub.dev"
     source: hosted
     version: "1.0.1"
+  flex_color_picker:
+    dependency: "direct main"
+    description:
+      name: flex_color_picker
+      sha256: f37476ab3e80dcaca94e428e159944d465dd16312fda9ff41e07e86f04bfa51c
+      url: "https://pub.dev"
+    source: hosted
+    version: "3.3.0"
+  flex_seed_scheme:
+    dependency: transitive
+    description:
+      name: flex_seed_scheme
+      sha256: "29c12aba221eb8a368a119685371381f8035011d18de5ba277ad11d7dfb8657f"
+      url: "https://pub.dev"
+    source: hosted
+    version: "1.4.0"
   flutter:
     dependency: "direct main"
     description: flutter
@@ -549,10 +574,10 @@ packages:
     dependency: "direct dev"
     description:
       name: flutter_lints
-      sha256: aeb0b80a8b3709709c9cc496cdc027c5b3216796bc0af0ce1007eaf24464fd4c
+      sha256: "2118df84ef0c3ca93f96123a616ae8540879991b8b57af2f81b76a7ada49b2a4"
       url: "https://pub.dev"
     source: hosted
-    version: "2.0.1"
+    version: "2.0.2"
   flutter_localizations:
     dependency: "direct main"
     description: flutter
@@ -599,18 +624,18 @@ packages:
     dependency: "direct dev"
     description:
       name: freezed
-      sha256: e819441678f1679b719008ff2ff0ef045d66eed9f9ec81166ca0d9b02a187454
+      sha256: "83462cfc33dc9680533a7f3a4a6ab60aa94f287db5f4ee6511248c22833c497f"
       url: "https://pub.dev"
     source: hosted
-    version: "2.3.2"
+    version: "2.4.2"
   freezed_annotation:
     dependency: "direct main"
     description:
       name: freezed_annotation
-      sha256: aeac15850ef1b38ee368d4c53ba9a847e900bb2c53a4db3f6881cbb3cb684338
+      sha256: c3fd9336eb55a38cc1bbd79ab17573113a8deccd0ecbbf926cca3c62803b5c2d
       url: "https://pub.dev"
     source: hosted
-    version: "2.2.0"
+    version: "2.4.1"
   frontend_server_client:
     dependency: transitive
     description:
@@ -1109,10 +1134,10 @@ packages:
     dependency: transitive
     description:
       name: source_gen
-      sha256: "2d79738b6bbf38a43920e2b8d189e9a3ce6cc201f4b8fc76be5e4fe377b1c38d"
+      sha256: fc0da689e5302edb6177fdd964efcb7f58912f43c28c2047a808f5bfff643d16
       url: "https://pub.dev"
     source: hosted
-    version: "1.2.6"
+    version: "1.4.0"
   source_span:
     dependency: transitive
     description:
@@ -1374,10 +1399,10 @@ packages:
     dependency: transitive
     description:
       name: video_player
-      sha256: "59f7f31c919c59cbedd37c617317045f5f650dc0eeb568b0b0de9a36472bdb28"
+      sha256: d3910a8cefc0de8a432a4411dcf85030e885d8fef3ddea291f162253a05dbf01
       url: "https://pub.dev"
     source: hosted
-    version: "2.5.1"
+    version: "2.7.1"
   video_player_android:
     dependency: transitive
     description:
@@ -1398,10 +1423,10 @@ packages:
     dependency: transitive
     description:
       name: video_player_platform_interface
-      sha256: "42bb75de5e9b79e1f20f1d95f688fac0f95beac4d89c6eb2cd421724d4432dae"
+      sha256: be72301bf2c0150ab35a8c34d66e5a99de525f6de1e8d27c0672b836fe48f73a
       url: "https://pub.dev"
     source: hosted
-    version: "6.0.1"
+    version: "6.2.1"
   video_player_web:
     dependency: transitive
     description:
@@ -1495,7 +1520,7 @@ packages:
     description:
       path: "."
       ref: HEAD
-      resolved-ref: "0728bd25ba1d625181573c26d3868efe21122190"
+      resolved-ref: "2c4b242e668acf4e652b09b13f650bcfbbaa3871"
       url: "https://github.com/rustdesk-org/window_manager"
     source: git
     version: "0.3.4"
@@ -1549,5 +1574,5 @@ packages:
     source: hosted
     version: "0.2.0"
 sdks:
-  dart: ">=3.0.0-0 <4.0.0"
-  flutter: ">=3.7.0-0"
+  dart: ">=3.0.0 <4.0.0"
+  flutter: ">=3.10.0"
diff --git a/flutter/pubspec.yaml b/flutter/pubspec.yaml
index f8541c12d..ba90bef3c 100644
--- a/flutter/pubspec.yaml
+++ b/flutter/pubspec.yaml
@@ -16,7 +16,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev
 # Read more about iOS versioning at
 # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
 # 1.1.9-1 works for android, but for ios it becomes 1.1.91, need to set it to 1.1.9-a.1 for iOS, will get 1.1.9.1, but iOS store not allow 4 numbers
-version: 1.2.0
+version: 1.2.3+39
 
 environment:
   sdk: ">=2.17.0"
@@ -39,7 +39,9 @@ dependencies:
   package_info_plus: ^3.1.2
   url_launcher: ^6.0.9
   toggle_switch: ^2.1.0
-  dash_chat_2: ^0.0.17
+  dash_chat_2: 
+    git:
+      url: https://github.com/rustdesk-org/Dash-Chat-2
   draggable_float_widget: ^0.0.2
   settings_ui: ^2.0.2
   flutter_breadcrumb: ^1.0.1
@@ -96,14 +98,16 @@ dependencies:
   percent_indicator: ^4.2.2
   dropdown_button2: ^2.0.0
   uuid: ^3.0.7
+  auto_size_text_field: ^2.2.1
+  flex_color_picker: ^3.3.0
 
 dev_dependencies:
   icons_launcher: ^2.0.4
   #flutter_test:
   #sdk: flutter
-  build_runner: ^2.1.11
-  freezed: ^2.0.3
-  flutter_lints: ^2.0.0
+  build_runner: ^2.4.6
+  freezed: ^2.4.2
+  flutter_lints: ^2.0.2
   ffigen: ^7.2.4
 
 # rerun: flutter pub run flutter_launcher_icons
@@ -148,6 +152,9 @@ flutter:
     - family: AddressBook
       fonts:
         - asset: assets/address_book.ttf
+    - family: CheckBox
+      fonts:
+        - asset: assets/checkbox.ttf
 
   # An image asset can refer to one or more resolution-specific "variants", see
   # https://flutter.dev/assets-and-images/#resolution-aware.
diff --git a/flutter/web/icons/Icon-192.png b/flutter/web/icons/Icon-192.png
index e8c754f4a..2ff3bb45c 100644
Binary files a/flutter/web/icons/Icon-192.png and b/flutter/web/icons/Icon-192.png differ
diff --git a/flutter/web/icons/Icon-512.png b/flutter/web/icons/Icon-512.png
index 2f8929e26..4d94e3123 100644
Binary files a/flutter/web/icons/Icon-512.png and b/flutter/web/icons/Icon-512.png differ
diff --git a/flutter/web/icons/Icon-maskable-192.png b/flutter/web/icons/Icon-maskable-192.png
index e8c754f4a..2ff3bb45c 100644
Binary files a/flutter/web/icons/Icon-maskable-192.png and b/flutter/web/icons/Icon-maskable-192.png differ
diff --git a/flutter/web/icons/Icon-maskable-512.png b/flutter/web/icons/Icon-maskable-512.png
index 2f8929e26..4d94e3123 100644
Binary files a/flutter/web/icons/Icon-maskable-512.png and b/flutter/web/icons/Icon-maskable-512.png differ
diff --git a/flutter/windows/runner/Runner.rc b/flutter/windows/runner/Runner.rc
index 32d01bf44..03776ded4 100644
--- a/flutter/windows/runner/Runner.rc
+++ b/flutter/windows/runner/Runner.rc
@@ -93,7 +93,7 @@ BEGIN
             VALUE "FileDescription", "rustdesk" "\0"
             VALUE "FileVersion", VERSION_AS_STRING "\0"
             VALUE "InternalName", "rustdesk" "\0"
-            VALUE "LegalCopyright", "Copyright (C) 2022 com.carriez. All rights reserved." "\0"
+            VALUE "LegalCopyright", "Copyright (C) 2023 com.carriez. All rights reserved." "\0"
             VALUE "OriginalFilename", "rustdesk.exe" "\0"
             VALUE "ProductName", "rustdesk" "\0"
             VALUE "ProductVersion", VERSION_AS_STRING "\0"
diff --git a/libs/clipboard/src/platform/windows.rs b/libs/clipboard/src/platform/windows.rs
index 1ab1aa839..1df5e09a0 100644
--- a/libs/clipboard/src/platform/windows.rs
+++ b/libs/clipboard/src/platform/windows.rs
@@ -610,12 +610,15 @@ fn ret_to_result(ret: u32) -> Result<(), CliprdrError> {
         e => Err(CliprdrError::Unknown(e)),
     }
 }
-
-fn empty_clipboard(context: &mut CliprdrClientContext, conn_id: i32) -> bool {
-    unsafe { TRUE == empty_cliprdr(context, conn_id as u32) }
+pub fn empty_clipboard(context: &mut CliprdrClientContext, conn_id: i32) -> bool {
+    unsafe { TRUE == cliprdr::empty_cliprdr(context, conn_id as u32) }
 }
 
-fn server_clip_file(context: &mut CliprdrClientContext, conn_id: i32, msg: ClipboardFile) -> u32 {
+pub fn server_clip_file(
+    context: &mut CliprdrClientContext,
+    conn_id: i32,
+    msg: ClipboardFile,
+) -> u32 {
     let mut ret = 0;
     match msg {
         ClipboardFile::NotifyCallback { .. } => {
@@ -737,7 +740,7 @@ fn server_clip_file(context: &mut CliprdrClientContext, conn_id: i32, msg: Clipb
     ret
 }
 
-fn server_monitor_ready(context: &mut CliprdrClientContext, conn_id: i32) -> u32 {
+pub fn server_monitor_ready(context: &mut CliprdrClientContext, conn_id: i32) -> u32 {
     unsafe {
         let monitor_ready = CLIPRDR_MONITOR_READY {
             connID: conn_id as UINT32,
@@ -754,7 +757,7 @@ fn server_monitor_ready(context: &mut CliprdrClientContext, conn_id: i32) -> u32
     }
 }
 
-fn server_format_list(
+pub fn server_format_list(
     context: &mut CliprdrClientContext,
     conn_id: i32,
     format_list: Vec<(i32, String)>,
@@ -808,7 +811,7 @@ fn server_format_list(
     }
 }
 
-fn server_format_list_response(
+pub fn server_format_list_response(
     context: &mut CliprdrClientContext,
     conn_id: i32,
     msg_flags: i32,
@@ -829,7 +832,7 @@ fn server_format_list_response(
     }
 }
 
-fn server_format_data_request(
+pub fn server_format_data_request(
     context: &mut CliprdrClientContext,
     conn_id: i32,
     requested_format_id: i32,
@@ -850,7 +853,7 @@ fn server_format_data_request(
     }
 }
 
-fn server_format_data_response(
+pub fn server_format_data_response(
     context: &mut CliprdrClientContext,
     conn_id: i32,
     msg_flags: i32,
@@ -872,7 +875,7 @@ fn server_format_data_response(
     }
 }
 
-fn server_file_contents_request(
+pub fn server_file_contents_request(
     context: &mut CliprdrClientContext,
     conn_id: i32,
     stream_id: i32,
@@ -907,7 +910,7 @@ fn server_file_contents_request(
     }
 }
 
-fn server_file_contents_response(
+pub fn server_file_contents_response(
     context: &mut CliprdrClientContext,
     conn_id: i32,
     msg_flags: i32,
@@ -932,12 +935,12 @@ fn server_file_contents_response(
     }
 }
 
-pub(super) fn create_cliprdr_context(
+pub fn create_cliprdr_context(
     enable_files: bool,
     enable_others: bool,
     response_wait_timeout_secs: u32,
-) -> ResultType<Box<dyn CliprdrServiceContext>> {
-    let boxed = CliprdrClientContext::create(
+) -> ResultType<Box<CliprdrClientContext>> {
+    Ok(CliprdrClientContext::create(
         enable_files,
         enable_others,
         response_wait_timeout_secs,
@@ -948,9 +951,7 @@ pub(super) fn create_cliprdr_context(
         Some(client_format_data_response),
         Some(client_file_contents_request),
         Some(client_file_contents_response),
-    )? as Box<dyn CliprdrServiceContext>;
-
-    Ok(boxed)
+    )?)
 }
 
 extern "C" fn notify_callback(conn_id: UINT32, msg: *const NOTIFICATION_MESSAGE) -> UINT {
diff --git a/libs/hbb_common/Cargo.toml b/libs/hbb_common/Cargo.toml
index 864bdded0..5c93ccd04 100644
--- a/libs/hbb_common/Cargo.toml
+++ b/libs/hbb_common/Cargo.toml
@@ -24,6 +24,7 @@ directories-next = "2.0"
 rand = "0.8"
 serde_derive = "1.0"
 serde = "1.0"
+serde_json = "1.0"
 lazy_static = "1.4"
 confy = { git = "https://github.com/open-trade/confy" }
 dirs-next = "2.0"
@@ -37,11 +38,11 @@ libc = "0.2"
 dlopen = "0.1"
 toml = "0.7"
 uuid = { version = "1.3", features = ["v4"] }
+sysinfo = "0.29"
 
 [target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies]
 mac_address = "1.1"
-machine-uid = "0.3"
-sysinfo = "0.29"
+machine-uid = { git = "https://github.com/21pages/machine-uid" }
 
 [features]
 quic = []
@@ -56,5 +57,3 @@ winapi = { version = "0.3", features = ["winuser"] }
 [target.'cfg(target_os = "macos")'.dependencies]
 osascript = "0.3"
 
-[dev-dependencies]
-serde_json = "1.0"
diff --git a/libs/hbb_common/protos/message.proto b/libs/hbb_common/protos/message.proto
index d89370cc7..47de31e45 100644
--- a/libs/hbb_common/protos/message.proto
+++ b/libs/hbb_common/protos/message.proto
@@ -97,7 +97,6 @@ message PeerInfo {
   int32 current_display = 5;
   bool sas_enabled = 6;
   string version = 7;
-  int32 conn_id = 8;
   Features features = 9;
   SupportedEncoding encoding = 10;
   SupportedResolutions resolutions = 11;
@@ -112,6 +111,46 @@ message LoginResponse {
   }
 }
 
+message TouchScaleUpdate {
+  // The delta scale factor relative to the previous scale.
+  // delta * 1000
+  // 0 means scale end
+  int32 scale = 1;
+}
+
+message TouchPanStart {
+  int32 x = 1;
+  int32 y = 2;
+}
+
+message TouchPanUpdate {
+  // The delta x position relative to the previous position.
+  int32 x = 1;
+  // The delta y position relative to the previous position.
+  int32 y = 2;
+}
+
+message TouchPanEnd {
+  int32 x = 1;
+  int32 y = 2;
+}
+
+message TouchEvent {
+  oneof union {
+    TouchScaleUpdate scale_update = 1;
+    TouchPanStart pan_start = 2;
+    TouchPanUpdate pan_update = 3;
+    TouchPanEnd pan_end = 4;
+  }
+}
+
+message PointerDeviceEvent {
+  oneof union {
+    TouchEvent touch_event = 1;
+  }
+  repeated ControlKey modifiers = 2;
+}
+
 message MouseEvent {
   int32 mask = 1;
   sint32 x = 2;
@@ -358,6 +397,7 @@ message FileTransferReceiveRequest {
   string path = 2; // path written to
   repeated FileEntry files = 3;
   int32 file_num = 4;
+  uint64 total_size = 5;
 }
 
 message FileRemoveDir {
@@ -643,6 +683,9 @@ message Misc {
     Resolution change_resolution = 24;
     PluginRequest plugin_request = 25;
     PluginFailure plugin_failure = 26;
+    uint32 full_speed_fps = 27;
+    uint32 auto_adjust_fps = 28;
+    bool client_record_status = 29;
   }
 }
 
@@ -683,5 +726,6 @@ message Message {
     VoiceCallRequest voice_call_request = 23;
     VoiceCallResponse voice_call_response = 24;
     PeerInfo peer_info = 25;
+    PointerDeviceEvent pointer_device_event = 26;
   }
 }
diff --git a/libs/hbb_common/src/config.rs b/libs/hbb_common/src/config.rs
index 785be96ee..a48da5ff0 100644
--- a/libs/hbb_common/src/config.rs
+++ b/libs/hbb_common/src/config.rs
@@ -1,6 +1,7 @@
 use std::{
-    collections::HashMap,
+    collections::{HashMap, HashSet},
     fs,
+    io::{Read, Write},
     net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr},
     ops::{Deref, DerefMut},
     path::{Path, PathBuf},
@@ -13,14 +14,16 @@ use rand::Rng;
 use regex::Regex;
 use serde as de;
 use serde_derive::{Deserialize, Serialize};
+use serde_json;
 use sodiumoxide::base64;
 use sodiumoxide::crypto::sign;
 
 use crate::{
+    compress::{compress, decompress},
     log,
     password_security::{
         decrypt_str_or_original, decrypt_vec_or_original, encrypt_str_or_original,
-        encrypt_vec_or_original,
+        encrypt_vec_or_original, symmetric_crypt,
     },
 };
 
@@ -31,6 +34,11 @@ pub const REG_INTERVAL: i64 = 12_000;
 pub const COMPRESS_LEVEL: i32 = 3;
 const SERIAL: i32 = 3;
 const PASSWORD_ENC_VERSION: &str = "00";
+const ENCRYPT_MAX_LEN: usize = 128;
+
+// config2 options
+#[cfg(target_os = "linux")]
+pub const CONFIG_OPTION_ALLOW_LINUX_HEADLESS: &str = "allow-linux-headless";
 
 #[cfg(target_os = "macos")]
 lazy_static::lazy_static! {
@@ -49,10 +57,11 @@ lazy_static::lazy_static! {
         Some(key) if !key.is_empty() => key,
         _ => "",
     }.to_owned()));
+    pub static ref EXE_RENDEZVOUS_SERVER: Arc<RwLock<String>> = Default::default();
     pub static ref APP_NAME: Arc<RwLock<String>> = Arc::new(RwLock::new("RustDesk".to_owned()));
     static ref KEY_PAIR: Arc<Mutex<Option<KeyPair>>> = Default::default();
-    static ref HW_CODEC_CONFIG: Arc<RwLock<HwCodecConfig>> = Arc::new(RwLock::new(HwCodecConfig::load()));
     static ref USER_DEFAULT_CONFIG: Arc<RwLock<(UserDefaultConfig, Instant)>> = Arc::new(RwLock::new((UserDefaultConfig::load(), Instant::now())));
+    pub static ref NEW_STORED_PEER_CONFIG: Arc<Mutex<HashSet<String>>> = Default::default();
 }
 
 lazy_static::lazy_static! {
@@ -81,11 +90,7 @@ const CHARS: &[char] = &[
     'm', 'n', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z',
 ];
 
-pub const RENDEZVOUS_SERVERS: &[&str] = &[
-    "rs-ny.rustdesk.com",
-    "rs-sg.rustdesk.com",
-    "rs-cn.rustdesk.com",
-];
+pub const RENDEZVOUS_SERVERS: &[&str] = &["rs-ny.rustdesk.com"];
 
 pub const RS_PUB_KEY: &str = match option_env!("RS_PUB_KEY") {
     Some(key) if !key.is_empty() => key,
@@ -209,7 +214,7 @@ pub struct Resolution {
     pub h: i32,
 }
 
-#[derive(Debug, Default, Serialize, Deserialize, Clone, PartialEq)]
+#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
 pub struct PeerConfig {
     #[serde(default, deserialize_with = "deserialize_vec_u8")]
     pub password: Vec<u8>,
@@ -291,6 +296,38 @@ pub struct PeerConfig {
     pub transfer: TransferSerde,
 }
 
+impl Default for PeerConfig {
+    fn default() -> Self {
+        Self {
+            password: Default::default(),
+            size: Default::default(),
+            size_ft: Default::default(),
+            size_pf: Default::default(),
+            view_style: Self::default_view_style(),
+            scroll_style: Self::default_scroll_style(),
+            image_quality: Self::default_image_quality(),
+            custom_image_quality: Self::default_custom_image_quality(),
+            show_remote_cursor: Default::default(),
+            lock_after_session_end: Default::default(),
+            privacy_mode: Default::default(),
+            allow_swap_key: Default::default(),
+            port_forwards: Default::default(),
+            direct_failures: Default::default(),
+            disable_audio: Default::default(),
+            disable_clipboard: Default::default(),
+            enable_file_transfer: Default::default(),
+            show_quality_monitor: Default::default(),
+            keyboard_mode: Default::default(),
+            view_only: Default::default(),
+            custom_resolutions: Default::default(),
+            options: Self::default_options(),
+            ui_flutter: Default::default(),
+            info: Default::default(),
+            transfer: Default::default(),
+        }
+    }
+}
+
 #[derive(Debug, PartialEq, Default, Serialize, Deserialize, Clone)]
 pub struct PeerInfoSerde {
     #[serde(default, deserialize_with = "deserialize_string")]
@@ -310,7 +347,7 @@ pub struct TransferSerde {
 }
 
 #[inline]
-pub fn get_online_statue() -> i64 {
+pub fn get_online_state() -> i64 {
     *ONLINE.lock().unwrap().values().max().unwrap_or(&0)
 }
 
@@ -376,7 +413,8 @@ impl Config2 {
     fn store(&self) {
         let mut config = self.clone();
         if let Some(mut socks) = config.socks {
-            socks.password = encrypt_str_or_original(&socks.password, PASSWORD_ENC_VERSION);
+            socks.password =
+                encrypt_str_or_original(&socks.password, PASSWORD_ENC_VERSION, ENCRYPT_MAX_LEN);
             config.socks = Some(socks);
         }
         Config::store_(&config, "2");
@@ -400,10 +438,15 @@ impl Config2 {
 pub fn load_path<T: serde::Serialize + serde::de::DeserializeOwned + Default + std::fmt::Debug>(
     file: PathBuf,
 ) -> T {
-    let cfg = match confy::load_path(file) {
+    let cfg = match confy::load_path(&file) {
         Ok(config) => config,
         Err(err) => {
-            log::error!("Failed to load config: {}", err);
+            if let confy::ConfyError::GeneralLoadError(err) = &err {
+                if err.kind() == std::io::ErrorKind::NotFound {
+                    return T::default();
+                }
+            }
+            log::error!("Failed to load config '{}': {}", file.display(), err);
             T::default()
         }
     };
@@ -480,8 +523,9 @@ impl Config {
 
     fn store(&self) {
         let mut config = self.clone();
-        config.password = encrypt_str_or_original(&config.password, PASSWORD_ENC_VERSION);
-        config.enc_id = encrypt_str_or_original(&config.id, PASSWORD_ENC_VERSION);
+        config.password =
+            encrypt_str_or_original(&config.password, PASSWORD_ENC_VERSION, ENCRYPT_MAX_LEN);
+        config.enc_id = encrypt_str_or_original(&config.id, PASSWORD_ENC_VERSION, ENCRYPT_MAX_LEN);
         config.id = "".to_owned();
         Config::store_(&config, "");
     }
@@ -608,7 +652,10 @@ impl Config {
     }
 
     pub fn get_rendezvous_server() -> String {
-        let mut rendezvous_server = Self::get_option("custom-rendezvous-server");
+        let mut rendezvous_server = EXE_RENDEZVOUS_SERVER.read().unwrap().clone();
+        if rendezvous_server.is_empty() {
+            rendezvous_server = Self::get_option("custom-rendezvous-server");
+        }
         if rendezvous_server.is_empty() {
             rendezvous_server = PROD_RENDEZVOUS_SERVER.read().unwrap().clone();
         }
@@ -628,6 +675,10 @@ impl Config {
     }
 
     pub fn get_rendezvous_servers() -> Vec<String> {
+        let s = EXE_RENDEZVOUS_SERVER.read().unwrap().clone();
+        if !s.is_empty() {
+            return vec![s];
+        }
         let s = Self::get_option("custom-rendezvous-server");
         if !s.is_empty() {
             return vec![s];
@@ -959,7 +1010,12 @@ impl PeerConfig {
                 config
             }
             Err(err) => {
-                log::error!("Failed to load config: {}", err);
+                if let confy::ConfyError::GeneralLoadError(err) = &err {
+                    if err.kind() == std::io::ErrorKind::NotFound {
+                        return Default::default();
+                    }
+                }
+                log::error!("Failed to load peer config '{}': {}", id, err);
                 Default::default()
             }
         }
@@ -968,15 +1024,17 @@ impl PeerConfig {
     pub fn store(&self, id: &str) {
         let _lock = CONFIG.read().unwrap();
         let mut config = self.clone();
-        config.password = encrypt_vec_or_original(&config.password, PASSWORD_ENC_VERSION);
+        config.password =
+            encrypt_vec_or_original(&config.password, PASSWORD_ENC_VERSION, ENCRYPT_MAX_LEN);
         for opt in ["rdp_password", "os-username", "os-password"] {
             if let Some(v) = config.options.get_mut(opt) {
-                *v = encrypt_str_or_original(v, PASSWORD_ENC_VERSION)
+                *v = encrypt_str_or_original(v, PASSWORD_ENC_VERSION, ENCRYPT_MAX_LEN)
             }
         }
         if let Err(err) = store_path(Self::path(id), config) {
             log::error!("Failed to store config: {}", err);
         }
+        NEW_STORED_PEER_CONFIG.lock().unwrap().insert(id.to_owned());
     }
 
     pub fn remove(id: &str) {
@@ -1002,7 +1060,7 @@ impl PeerConfig {
         Config::with_extension(Config::path(path))
     }
 
-    pub fn peers() -> Vec<(String, SystemTime, PeerConfig)> {
+    pub fn peers(id_filters: Option<Vec<String>>) -> Vec<(String, SystemTime, PeerConfig)> {
         if let Ok(peers) = Config::path(PEERS).read_dir() {
             if let Ok(peers) = peers
                 .map(|res| res.map(|e| e.path()))
@@ -1015,7 +1073,6 @@ impl PeerConfig {
                             && p.extension().map(|p| p.to_str().unwrap_or("")) == Some("toml")
                     })
                     .map(|p| {
-                        let t = crate::get_modified_time(p);
                         let id = p
                             .file_stem()
                             .map(|p| p.to_str().unwrap_or(""))
@@ -1029,12 +1086,21 @@ impl PeerConfig {
                         } else {
                             id
                         };
-
-                        let c = PeerConfig::load(&id_decoded_string);
+                        (id_decoded_string, p)
+                    })
+                    .filter(|(id, _)| {
+                        let Some(filters) = &id_filters else {
+                            return true;
+                        };
+                        filters.contains(id)
+                    })
+                    .map(|(id, p)| {
+                        let t = crate::get_modified_time(p);
+                        let c = PeerConfig::load(&id);
                         if c.info.platform.is_empty() {
                             fs::remove_file(p).ok();
                         }
-                        (id_decoded_string, t, c)
+                        (id, t, c)
                     })
                     .filter(|p| !p.2.info.platform.is_empty())
                     .collect();
@@ -1045,6 +1111,10 @@ impl PeerConfig {
         Default::default()
     }
 
+    pub fn exists(id: &str) -> bool {
+        Self::path(id).exists()
+    }
+
     serde_field_string!(
         default_view_style,
         deserialize_view_style,
@@ -1074,7 +1144,7 @@ impl PeerConfig {
         D: de::Deserializer<'de>,
     {
         let v: Vec<i32> = de::Deserialize::deserialize(deserializer)?;
-        if v.len() == 1 && v[0] >= 10 && v[0] <= 100 {
+        if v.len() == 1 && v[0] >= 10 && v[0] <= 0xFFF {
             Ok(v)
         } else {
             Ok(Self::default_custom_image_quality())
@@ -1086,6 +1156,17 @@ impl PeerConfig {
         D: de::Deserializer<'de>,
     {
         let mut mp: HashMap<String, String> = de::Deserialize::deserialize(deserializer)?;
+        Self::insert_default_options(&mut mp);
+        Ok(mp)
+    }
+
+    fn default_options() -> HashMap<String, String> {
+        let mut mp: HashMap<String, String> = Default::default();
+        Self::insert_default_options(&mut mp);
+        return mp;
+    }
+
+    fn insert_default_options(mp: &mut HashMap<String, String>) {
         let mut key = "codec-preference";
         if !mp.contains_key(key) {
             mp.insert(key.to_owned(), UserDefaultConfig::read().get(key));
@@ -1098,7 +1179,10 @@ impl PeerConfig {
         if !mp.contains_key(key) {
             mp.insert(key.to_owned(), UserDefaultConfig::read().get(key));
         }
-        Ok(mp)
+        key = "touch-mode";
+        if !mp.contains_key(key) {
+            mp.insert(key.to_owned(), UserDefaultConfig::read().get(key));
+        }
     }
 }
 
@@ -1256,7 +1340,7 @@ impl LocalConfig {
         }
     }
 
-    pub fn get_flutter_config(k: &str) -> String {
+    pub fn get_flutter_option(k: &str) -> String {
         if let Some(v) = LOCAL_CONFIG.read().unwrap().ui_flutter.get(k) {
             v.clone()
         } else {
@@ -1264,7 +1348,7 @@ impl LocalConfig {
         }
     }
 
-    pub fn set_flutter_config(k: String, v: String) {
+    pub fn set_flutter_option(k: String, v: String) {
         let mut config = LOCAL_CONFIG.write().unwrap();
         let v2 = if v.is_empty() { None } else { Some(&v) };
         if v2 != config.ui_flutter.get(&k) {
@@ -1351,18 +1435,8 @@ impl HwCodecConfig {
         Config::store_(self, "_hwcodec");
     }
 
-    pub fn remove() {
-        std::fs::remove_file(Config::file_("_hwcodec")).ok();
-    }
-
-    /// refresh current global HW_CODEC_CONFIG, usually uesd after HwCodecConfig::remove()
-    pub fn refresh() {
-        *HW_CODEC_CONFIG.write().unwrap() = HwCodecConfig::load();
-        log::debug!("HW_CODEC_CONFIG refreshed successfully");
-    }
-
-    pub fn get() -> HwCodecConfig {
-        return HW_CODEC_CONFIG.read().unwrap().clone();
+    pub fn clear() {
+        HwCodecConfig::default().store();
     }
 }
 
@@ -1398,7 +1472,7 @@ impl UserDefaultConfig {
             "codec-preference" => {
                 self.get_string(key, "auto", vec!["vp8", "vp9", "av1", "h264", "h265"])
             }
-            "custom_image_quality" => self.get_double_string(key, 50.0, 10.0, 100.0),
+            "custom_image_quality" => self.get_double_string(key, 50.0, 10.0, 0xFFF as f64),
             "custom-fps" => self.get_double_string(key, 30.0, 5.0, 120.0),
             _ => self
                 .options
@@ -1443,6 +1517,109 @@ impl UserDefaultConfig {
     }
 }
 
+#[derive(Debug, Default, Serialize, Deserialize, Clone)]
+pub struct AbPeer {
+    #[serde(
+        default,
+        deserialize_with = "deserialize_string",
+        skip_serializing_if = "String::is_empty"
+    )]
+    pub id: String,
+    #[serde(
+        default,
+        deserialize_with = "deserialize_string",
+        skip_serializing_if = "String::is_empty"
+    )]
+    pub hash: String,
+    #[serde(
+        default,
+        deserialize_with = "deserialize_string",
+        skip_serializing_if = "String::is_empty"
+    )]
+    pub username: String,
+    #[serde(
+        default,
+        deserialize_with = "deserialize_string",
+        skip_serializing_if = "String::is_empty"
+    )]
+    pub hostname: String,
+    #[serde(
+        default,
+        deserialize_with = "deserialize_string",
+        skip_serializing_if = "String::is_empty"
+    )]
+    pub platform: String,
+    #[serde(
+        default,
+        deserialize_with = "deserialize_string",
+        skip_serializing_if = "String::is_empty"
+    )]
+    pub alias: String,
+    #[serde(default, deserialize_with = "deserialize_vec_string")]
+    pub tags: Vec<String>,
+}
+
+#[derive(Debug, Default, Serialize, Deserialize, Clone)]
+pub struct Ab {
+    #[serde(
+        default,
+        deserialize_with = "deserialize_string",
+        skip_serializing_if = "String::is_empty"
+    )]
+    pub access_token: String,
+    #[serde(default, deserialize_with = "deserialize_vec_abpeer")]
+    pub peers: Vec<AbPeer>,
+    #[serde(default, deserialize_with = "deserialize_vec_string")]
+    pub tags: Vec<String>,
+    #[serde(
+        default,
+        deserialize_with = "deserialize_string",
+        skip_serializing_if = "String::is_empty"
+    )]
+    pub tag_colors: String,
+}
+
+impl Ab {
+    fn path() -> PathBuf {
+        let filename = format!("{}_ab", APP_NAME.read().unwrap().clone());
+        Config::path(filename)
+    }
+
+    pub fn store(json: String) {
+        if let Ok(mut file) = std::fs::File::create(Self::path()) {
+            let data = compress(json.as_bytes());
+            let max_len = 64 * 1024 * 1024;
+            if data.len() > max_len {
+                // maxlen of function decompress
+                return;
+            }
+            if let Ok(data) = symmetric_crypt(&data, true) {
+                file.write_all(&data).ok();
+            }
+        };
+    }
+
+    pub fn load() -> Ab {
+        if let Ok(mut file) = std::fs::File::open(Self::path()) {
+            let mut data = vec![];
+            if file.read_to_end(&mut data).is_ok() {
+                if let Ok(data) = symmetric_crypt(&data, false) {
+                    let data = decompress(&data);
+                    if let Ok(ab) = serde_json::from_str::<Ab>(&String::from_utf8_lossy(&data)) {
+                        return ab;
+                    }
+                }
+            }
+        };
+        Self::remove();
+        Ab::default()
+    }
+
+    pub fn remove() {
+        std::fs::remove_file(Self::path()).ok();
+    }
+}
+
 // use default value when field type is wrong
 macro_rules! deserialize_default {
     ($func_name:ident, $return_type:ty) => {
@@ -1462,6 +1639,7 @@ deserialize_default!(deserialize_vec_u8, Vec<u8>);
 deserialize_default!(deserialize_vec_string, Vec<String>);
 deserialize_default!(deserialize_vec_i32_string_i32, Vec<(i32, String, i32)>);
 deserialize_default!(deserialize_vec_discoverypeer, Vec<DiscoveryPeer>);
+deserialize_default!(deserialize_vec_abpeer, Vec<AbPeer>);
 deserialize_default!(deserialize_keypair, KeyPair);
 deserialize_default!(deserialize_size, Size);
 deserialize_default!(deserialize_hashmap_string_string, HashMap<String, String>);
diff --git a/libs/hbb_common/src/fs.rs b/libs/hbb_common/src/fs.rs
index 6b8205acf..b7ea836b7 100644
--- a/libs/hbb_common/src/fs.rs
+++ b/libs/hbb_common/src/fs.rs
@@ -4,9 +4,10 @@ use std::path::{Path, PathBuf};
 use std::time::{Duration, SystemTime, UNIX_EPOCH};
 
 use serde_derive::{Deserialize, Serialize};
+use serde_json::json;
 use tokio::{fs::File, io::*};
 
-use crate::{bail, get_version_number, message_proto::*, ResultType, Stream};
+use crate::{anyhow::anyhow, bail, get_version_number, message_proto::*, ResultType, Stream};
 // https://doc.rust-lang.org/std/os/windows/fs/trait.MetadataExt.html
 use crate::{
     compress::{compress, decompress},
@@ -194,7 +195,8 @@ pub fn can_enable_overwrite_detection(version: i64) -> bool {
     version >= get_version_number("1.1.10")
 }
 
-#[derive(Default)]
+#[derive(Default, Serialize, Debug)]
+#[serde(rename_all = "camelCase")]
 pub struct TransferJob {
     pub id: i32,
     pub remote: String,
@@ -203,10 +205,13 @@ pub struct TransferJob {
     pub is_remote: bool,
     pub is_last_job: bool,
     pub file_num: i32,
+    #[serde(skip_serializing)]
     pub files: Vec<FileEntry>,
+    pub conn_id: i32, // server only
 
+    #[serde(skip_serializing)]
     file: Option<File>,
-    total_size: u64,
+    pub total_size: u64,
     finished_size: u64,
     transferred: u64,
     enable_overwrite_detection: bool,
@@ -403,10 +408,18 @@ impl TransferJob {
         }
         if block.compressed {
             let tmp = decompress(&block.data);
-            self.file.as_mut().unwrap().write_all(&tmp).await?;
+            self.file
+                .as_mut()
+                .ok_or(anyhow!("file is None"))?
+                .write_all(&tmp)
+                .await?;
             self.finished_size += tmp.len() as u64;
         } else {
-            self.file.as_mut().unwrap().write_all(&block.data).await?;
+            self.file
+                .as_mut()
+                .ok_or(anyhow!("file is None"))?
+                .write_all(&block.data)
+                .await?;
             self.finished_size += block.data.len() as u64;
         }
         self.transferred += block.data.len() as u64;
@@ -456,7 +469,13 @@ impl TransferJob {
         let mut compressed = false;
         let mut offset: usize = 0;
         loop {
-            match self.file.as_mut().unwrap().read(&mut buf[offset..]).await {
+            match self
+                .file
+                .as_mut()
+                .ok_or(anyhow!("file is None"))?
+                .read(&mut buf[offset..])
+                .await
+            {
                 Err(err) => {
                     self.file_num += 1;
                     self.file = None;
@@ -501,7 +520,12 @@ impl TransferJob {
     async fn send_current_digest(&mut self, stream: &mut Stream) -> ResultType<()> {
         let mut msg = Message::new();
         let mut resp = FileResponse::new();
-        let meta = self.file.as_ref().unwrap().metadata().await?;
+        let meta = self
+            .file
+            .as_ref()
+            .ok_or(anyhow!("file is None"))?
+            .metadata()
+            .await?;
         let last_modified = meta
             .modified()?
             .duration_since(SystemTime::UNIX_EPOCH)?
@@ -676,13 +700,20 @@ pub fn new_send_confirm(r: FileTransferSendConfirmRequest) -> Message {
 }
 
 #[inline]
-pub fn new_receive(id: i32, path: String, file_num: i32, files: Vec<FileEntry>) -> Message {
+pub fn new_receive(
+    id: i32,
+    path: String,
+    file_num: i32,
+    files: Vec<FileEntry>,
+    total_size: u64,
+) -> Message {
     let mut action = FileAction::new();
     action.set_receive(FileTransferReceiveRequest {
         id,
         path,
         files,
         file_num,
+        total_size,
         ..Default::default()
     });
     let mut msg_out = Message::new();
@@ -729,10 +760,16 @@ pub fn get_job(id: i32, jobs: &mut [TransferJob]) -> Option<&mut TransferJob> {
     jobs.iter_mut().find(|x| x.id() == id)
 }
 
+#[inline]
+pub fn get_job_immutable(id: i32, jobs: &[TransferJob]) -> Option<&TransferJob> {
+    jobs.iter().find(|x| x.id() == id)
+}
+
 pub async fn handle_read_jobs(
     jobs: &mut Vec<TransferJob>,
     stream: &mut crate::Stream,
-) -> ResultType<()> {
+) -> ResultType<String> {
+    let mut job_log = Default::default();
     let mut finished = Vec::new();
     for job in jobs.iter_mut() {
         if job.is_last_job {
@@ -749,14 +786,16 @@ pub async fn handle_read_jobs(
             }
             Ok(None) => {
                 if job.job_completed() {
+                    job_log = serialize_transfer_job(job, true, false, "");
                     finished.push(job.id());
-                    let err = job.job_error();
-                    if err.is_some() {
-                        stream
-                            .send(&new_error(job.id(), err.unwrap(), job.file_num()))
-                            .await?;
-                    } else {
-                        stream.send(&new_done(job.id(), job.file_num())).await?;
+                    match job.job_error() {
+                        Some(err) => {
+                            job_log = serialize_transfer_job(job, false, false, &err);
+                            stream
+                                .send(&new_error(job.id(), err, job.file_num()))
+                                .await?
+                        }
+                        None => stream.send(&new_done(job.id(), job.file_num())).await?,
                     }
                 } else {
                     // waiting confirmation.
@@ -767,7 +806,7 @@ pub async fn handle_read_jobs(
     for id in finished {
         remove_job(id, jobs);
     }
-    Ok(())
+    Ok(job_log)
 }
 
 pub fn remove_all_empty_dir(path: &PathBuf) -> ResultType<()> {
@@ -842,3 +881,20 @@ pub fn is_write_need_confirmation(
         Ok(DigestCheckResult::NoSuchFile)
     }
 }
+
+pub fn serialize_transfer_jobs(jobs: &[TransferJob]) -> String {
+    let mut v = vec![];
+    for job in jobs {
+        let value = serde_json::to_value(job).unwrap_or_default();
+        v.push(value);
+    }
+    serde_json::to_string(&v).unwrap_or_default()
+}
+
+pub fn serialize_transfer_job(job: &TransferJob, done: bool, cancel: bool, error: &str) -> String {
+    let mut value = serde_json::to_value(job).unwrap_or_default();
+    value["done"] = json!(done);
+    value["cancel"] = json!(cancel);
+    value["error"] = json!(error);
+    serde_json::to_string(&value).unwrap_or_default()
+}
diff --git a/libs/hbb_common/src/lib.rs b/libs/hbb_common/src/lib.rs
index 192ee8a65..b8a7f4f2f 100644
--- a/libs/hbb_common/src/lib.rs
+++ b/libs/hbb_common/src/lib.rs
@@ -45,6 +45,7 @@ pub mod keyboard;
 #[cfg(not(any(target_os = "android", target_os = "ios")))]
 pub use dlopen;
 #[cfg(not(any(target_os = "android", target_os = "ios")))]
+pub use machine_uid;
 pub use sysinfo;
 pub use toml;
 pub use uuid;
@@ -126,7 +127,7 @@ impl AddrMangle {
             SocketAddr::V4(addr_v4) => {
                 let tm = (SystemTime::now()
                     .duration_since(UNIX_EPOCH)
-                    .unwrap()
+                    .unwrap_or(std::time::Duration::ZERO)
                     .as_micros() as u32) as u128;
                 let ip = u32::from_le_bytes(addr_v4.ip().octets()) as u128;
                 let port = addr.port() as u128;
@@ -159,9 +160,9 @@ impl AddrMangle {
             if bytes.len() != 18 {
                 return Config::get_any_listen_addr(false);
             }
-            let tmp: [u8; 2] = bytes[16..].try_into().unwrap();
+            let tmp: [u8; 2] = bytes[16..].try_into().unwrap_or_default();
             let port = u16::from_le_bytes(tmp);
-            let tmp: [u8; 16] = bytes[..16].try_into().unwrap();
+            let tmp: [u8; 16] = bytes[..16].try_into().unwrap_or_default();
             let ip = std::net::Ipv6Addr::from(tmp);
             return SocketAddr::new(IpAddr::V6(ip), port);
         }
@@ -289,16 +290,24 @@ pub fn get_time() -> i64 {
 
 #[inline]
 pub fn is_ipv4_str(id: &str) -> bool {
-    regex::Regex::new(r"^(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)(:\d+)?$")
-        .unwrap()
-        .is_match(id)
+    if let Ok(reg) = regex::Regex::new(
+        r"^(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)(:\d+)?$",
+    ) {
+        reg.is_match(id)
+    } else {
+        false
+    }
 }
 
 #[inline]
 pub fn is_ipv6_str(id: &str) -> bool {
-    regex::Regex::new(r"^((([a-fA-F0-9]{1,4}:{1,2})+[a-fA-F0-9]{1,4})|(\[([a-fA-F0-9]{1,4}:{1,2})+[a-fA-F0-9]{1,4}\]:\d+))$")
-        .unwrap()
-        .is_match(id)
+    if let Ok(reg) = regex::Regex::new(
+        r"^((([a-fA-F0-9]{1,4}:{1,2})+[a-fA-F0-9]{1,4})|(\[([a-fA-F0-9]{1,4}:{1,2})+[a-fA-F0-9]{1,4}\]:\d+))$",
+    ) {
+        reg.is_match(id)
+    } else {
+        false
+    }
 }
 
 #[inline]
@@ -311,11 +320,13 @@ pub fn is_domain_port_str(id: &str) -> bool {
     // modified regex for RFC1123 hostname. check https://stackoverflow.com/a/106223 for original version for hostname.
     // according to [TLD List](https://data.iana.org/TLD/tlds-alpha-by-domain.txt) version 2023011700,
     // there is no digits in TLD, and length is 2~63.
-    regex::Regex::new(
+    if let Ok(reg) = regex::Regex::new(
         r"(?i)^([a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z][a-z-]{0,61}[a-z]:\d{1,5}$",
-    )
-    .unwrap()
-    .is_match(id)
+    ) {
+        reg.is_match(id)
+    } else {
+        false
+    }
 }
 
 pub fn init_log(_is_async: bool, _name: &str) -> Option<flexi_logger::LoggerHandle> {
diff --git a/libs/hbb_common/src/password_security.rs b/libs/hbb_common/src/password_security.rs
index ddfe28baa..9584ab6c0 100644
--- a/libs/hbb_common/src/password_security.rs
+++ b/libs/hbb_common/src/password_security.rs
@@ -84,13 +84,16 @@ pub fn hide_cm() -> bool {
 
 const VERSION_LEN: usize = 2;
 
-pub fn encrypt_str_or_original(s: &str, version: &str) -> String {
+pub fn encrypt_str_or_original(s: &str, version: &str, max_len: usize) -> String {
     if decrypt_str_or_original(s, version).1 {
         log::error!("Duplicate encryption!");
         return s.to_owned();
     }
+    if s.bytes().len() > max_len {
+        return String::default();
+    }
     if version == "00" {
-        if let Ok(s) = encrypt(s.as_bytes()) {
+        if let Ok(s) = encrypt(s.as_bytes(), max_len) {
             return version.to_owned() + &s;
         }
     }
@@ -100,15 +103,16 @@ pub fn encrypt_str_or_original(s: &str, version: &str) -> String {
 // String: password
 // bool: whether decryption is successful
 // bool: whether should store to re-encrypt when load
+// note: s.len() return length in bytes, s.chars().count() return char count
+//       &[..2] return the left 2 bytes, s.chars().take(2) return the left 2 chars
 pub fn decrypt_str_or_original(s: &str, current_version: &str) -> (String, bool, bool) {
     if s.len() > VERSION_LEN {
-        let version = &s[..VERSION_LEN];
-        if version == "00" {
+        if s.starts_with("00") {
             if let Ok(v) = decrypt(s[VERSION_LEN..].as_bytes()) {
                 return (
                     String::from_utf8_lossy(&v).to_string(),
                     true,
-                    version != current_version,
+                    "00" != current_version,
                 );
             }
         }
@@ -117,13 +121,16 @@ pub fn decrypt_str_or_original(s: &str, current_version: &str) -> (String, bool,
     (s.to_owned(), false, !s.is_empty())
 }
 
-pub fn encrypt_vec_or_original(v: &[u8], version: &str) -> Vec<u8> {
+pub fn encrypt_vec_or_original(v: &[u8], version: &str, max_len: usize) -> Vec<u8> {
     if decrypt_vec_or_original(v, version).1 {
         log::error!("Duplicate encryption!");
         return v.to_owned();
     }
+    if v.len() > max_len {
+        return vec![];
+    }
     if version == "00" {
-        if let Ok(s) = encrypt(v) {
+        if let Ok(s) = encrypt(v, max_len) {
             let mut version = version.to_owned().into_bytes();
             version.append(&mut s.into_bytes());
             return version;
@@ -148,8 +155,8 @@ pub fn decrypt_vec_or_original(v: &[u8], current_version: &str) -> (Vec<u8>, boo
     (v.to_owned(), false, !v.is_empty())
 }
 
-fn encrypt(v: &[u8]) -> Result<String, ()> {
-    if !v.is_empty() {
+fn encrypt(v: &[u8], max_len: usize) -> Result<String, ()> {
+    if !v.is_empty() && v.len() <= max_len {
         symmetric_crypt(v, true).map(|v| base64::encode(v, base64::Variant::Original))
     } else {
         Err(())
@@ -164,7 +171,7 @@ fn decrypt(v: &[u8]) -> Result<Vec<u8>, ()> {
     }
 }
 
-fn symmetric_crypt(data: &[u8], encrypt: bool) -> Result<Vec<u8>, ()> {
+pub fn symmetric_crypt(data: &[u8], encrypt: bool) -> Result<Vec<u8>, ()> {
     use sodiumoxide::crypto::secretbox;
     use std::convert::TryInto;
 
@@ -185,12 +192,15 @@ mod test {
     #[test]
     fn test() {
         use super::*;
+        use rand::{thread_rng, Rng};
+        use std::time::Instant;
 
         let version = "00";
+        let max_len = 128;
 
         println!("test str");
-        let data = "Hello World";
-        let encrypted = encrypt_str_or_original(data, version);
+        let data = "1ü1111";
+        let encrypted = encrypt_str_or_original(data, version, max_len);
         let (decrypted, succ, store) = decrypt_str_or_original(&encrypted, version);
         println!("data: {data}");
         println!("encrypted: {encrypted}");
@@ -202,11 +212,14 @@ mod test {
         let (_, _, store) = decrypt_str_or_original(&encrypted, "99");
         assert!(store);
         assert!(!decrypt_str_or_original(&decrypted, version).1);
-        assert_eq!(encrypt_str_or_original(&encrypted, version), encrypted);
+        assert_eq!(
+            encrypt_str_or_original(&encrypted, version, max_len),
+            encrypted
+        );
 
         println!("test vec");
-        let data: Vec<u8> = vec![1, 2, 3, 4, 5, 6];
-        let encrypted = encrypt_vec_or_original(&data, version);
+        let data: Vec<u8> = "1ü1111".as_bytes().to_vec();
+        let encrypted = encrypt_vec_or_original(&data, version, max_len);
         let (decrypted, succ, store) = decrypt_vec_or_original(&encrypted, version);
         println!("data: {data:?}");
         println!("encrypted: {encrypted:?}");
@@ -218,7 +231,10 @@ mod test {
         let (_, _, store) = decrypt_vec_or_original(&encrypted, "99");
         assert!(store);
         assert!(!decrypt_vec_or_original(&decrypted, version).1);
-        assert_eq!(encrypt_vec_or_original(&encrypted, version), encrypted);
+        assert_eq!(
+            encrypt_vec_or_original(&encrypted, version, max_len),
+            encrypted
+        );
 
         println!("test original");
         let data = version.to_string() + "Hello World";
@@ -238,5 +254,42 @@ mod test {
         let (_, succ, store) = decrypt_vec_or_original(&[], version);
         assert!(!store);
         assert!(!succ);
+        let data = "1ü1111";
+        assert_eq!(decrypt_str_or_original(data, version).0, data);
+        let data: Vec<u8> = "1ü1111".as_bytes().to_vec();
+        assert_eq!(decrypt_vec_or_original(&data, version).0, data);
+
+        println!("test speed");
+        let test_speed = |len: usize, name: &str| {
+            let mut data: Vec<u8> = vec![];
+            let mut rng = thread_rng();
+            for _ in 0..len {
+                data.push(rng.gen_range(0..255));
+            }
+            let start: Instant = Instant::now();
+            let encrypted = encrypt_vec_or_original(&data, version, len);
+            assert_ne!(data, decrypted);
+            let t1 = start.elapsed();
+            let start = Instant::now();
+            let (decrypted, _, _) = decrypt_vec_or_original(&encrypted, version);
+            let t2 = start.elapsed();
+            assert_eq!(data, decrypted);
+            println!("{name}");
+            println!("encrypt:{:?}, decrypt:{:?}", t1, t2);
+
+            let start: Instant = Instant::now();
+            let encrypted = base64::encode(&data, base64::Variant::Original);
+            let t1 = start.elapsed();
+            let start = Instant::now();
+            let decrypted = base64::decode(&encrypted, base64::Variant::Original).unwrap();
+            let t2 = start.elapsed();
+            assert_eq!(data, decrypted);
+            println!("base64, encrypt:{:?}, decrypt:{:?}", t1, t2,);
+        };
+        test_speed(128, "128");
+        test_speed(1024, "1k");
+        test_speed(1024 * 1024, "1M");
+        test_speed(10 * 1024 * 1024, "10M");
+        test_speed(100 * 1024 * 1024, "100M");
     }
 }
diff --git a/libs/hbb_common/src/platform/linux.rs b/libs/hbb_common/src/platform/linux.rs
index 89c96799d..536ac3091 100644
--- a/libs/hbb_common/src/platform/linux.rs
+++ b/libs/hbb_common/src/platform/linux.rs
@@ -183,6 +183,15 @@ pub fn is_active(sid: &str) -> bool {
     }
 }
 
+pub fn is_active_and_seat0(sid: &str) -> bool {
+    if let Ok(output) = run_loginctl(Some(vec!["show-session", sid])) {
+        String::from_utf8_lossy(&output.stdout).contains("State=active")
+            && String::from_utf8_lossy(&output.stdout).contains("Seat=seat0")
+    } else {
+        false
+    }
+}
+
 pub fn run_cmds(cmds: &str) -> ResultType<String> {
     let output = std::process::Command::new("sh")
         .args(vec!["-c", cmds])
diff --git a/libs/hbb_common/src/platform/mod.rs b/libs/hbb_common/src/platform/mod.rs
index 137868e12..5dc004a81 100644
--- a/libs/hbb_common/src/platform/mod.rs
+++ b/libs/hbb_common/src/platform/mod.rs
@@ -4,6 +4,9 @@ pub mod linux;
 #[cfg(target_os = "macos")]
 pub mod macos;
 
+#[cfg(target_os = "windows")]
+pub mod windows;
+
 #[cfg(not(debug_assertions))]
 use crate::{config::Config, log};
 #[cfg(not(debug_assertions))]
@@ -28,6 +31,7 @@ extern "C" fn breakdown_signal_handler(sig: i32) {
         s.contains(&"nouveau_pushbuf_kick")
             || s.to_lowercase().contains("nvidia")
             || s.contains("gdk_window_end_draw_frame")
+            || s.contains("glGetString")
     }) {
         Config::set_option("allow-always-software-render".to_string(), "Y".to_string());
         info = "Always use software rendering will be set.".to_string();
diff --git a/libs/hbb_common/src/platform/windows.rs b/libs/hbb_common/src/platform/windows.rs
new file mode 100644
index 000000000..26cd907ab
--- /dev/null
+++ b/libs/hbb_common/src/platform/windows.rs
@@ -0,0 +1,154 @@
+use std::{
+    collections::VecDeque,
+    os::windows::raw::HANDLE,
+    sync::{Arc, Mutex},
+    time::Instant,
+};
+use winapi::{
+    shared::minwindef::{DWORD, FALSE},
+    um::{
+        handleapi::CloseHandle,
+        pdh::{
+            PdhAddCounterA, PdhCloseQuery, PdhCollectQueryData, PdhCollectQueryDataEx,
+            PdhGetFormattedCounterValue, PdhOpenQueryA, PDH_FMT_COUNTERVALUE, PDH_FMT_DOUBLE,
+            PDH_HCOUNTER, PDH_HQUERY,
+        },
+        synchapi::{CreateEventA, WaitForSingleObject},
+        winbase::{INFINITE, WAIT_OBJECT_0},
+    },
+};
+
+lazy_static::lazy_static! {
+    static ref CPU_USAGE_ONE_MINUTE: Arc<Mutex<Option<(f64, Instant)>>> = Arc::new(Mutex::new(None));
+}
+
+// https://github.com/mgostIH/process_list/blob/master/src/windows/mod.rs
+#[repr(transparent)]
+pub struct RAIIHandle(pub HANDLE);
+
+impl Drop for RAIIHandle {
+    fn drop(&mut self) {
+        // This never gives problem except when running under a debugger.
+        unsafe { CloseHandle(self.0) };
+    }
+}
+
+#[repr(transparent)]
+pub(self) struct RAIIPDHQuery(pub PDH_HQUERY);
+
+impl Drop for RAIIPDHQuery {
+    fn drop(&mut self) {
+        unsafe { PdhCloseQuery(self.0) };
+    }
+}
+
+pub fn start_cpu_performance_monitor() {
+    // Code from:
+    // https://learn.microsoft.com/en-us/windows/win32/perfctrs/collecting-performance-data
+    // https://learn.microsoft.com/en-us/windows/win32/api/pdh/nf-pdh-pdhcollectquerydataex
+    // Why value lower than taskManager:
+    // https://aaron-margosis.medium.com/task-managers-cpu-numbers-are-all-but-meaningless-2d165b421e43
+    // Therefore we should compare with Precess Explorer rather than taskManager
+
+    let f = || unsafe {
+        // load avg or cpu usage, test with prime95.
+        // Prefer cpu usage because we can get accurate value from Precess Explorer.
+        // const COUNTER_PATH: &'static str = "\\System\\Processor Queue Length\0";
+        const COUNTER_PATH: &'static str = "\\Processor(_total)\\% Processor Time\0";
+        const SAMPLE_INTERVAL: DWORD = 2; // 2 second
+
+        let mut ret;
+        let mut query: PDH_HQUERY = std::mem::zeroed();
+        ret = PdhOpenQueryA(std::ptr::null() as _, 0, &mut query);
+        if ret != 0 {
+            log::error!("PdhOpenQueryA failed: 0x{:X}", ret);
+            return;
+        }
+        let _query = RAIIPDHQuery(query);
+        let mut counter: PDH_HCOUNTER = std::mem::zeroed();
+        ret = PdhAddCounterA(query, COUNTER_PATH.as_ptr() as _, 0, &mut counter);
+        if ret != 0 {
+            log::error!("PdhAddCounterA failed: 0x{:X}", ret);
+            return;
+        }
+        ret = PdhCollectQueryData(query);
+        if ret != 0 {
+            log::error!("PdhCollectQueryData failed: 0x{:X}", ret);
+            return;
+        }
+        let mut _counter_type: DWORD = 0;
+        let mut counter_value: PDH_FMT_COUNTERVALUE = std::mem::zeroed();
+        let event = CreateEventA(std::ptr::null_mut(), FALSE, FALSE, std::ptr::null() as _);
+        if event.is_null() {
+            log::error!("CreateEventA failed");
+            return;
+        }
+        let _event: RAIIHandle = RAIIHandle(event);
+        ret = PdhCollectQueryDataEx(query, SAMPLE_INTERVAL, event);
+        if ret != 0 {
+            log::error!("PdhCollectQueryDataEx failed: 0x{:X}", ret);
+            return;
+        }
+
+        let mut queue: VecDeque<f64> = VecDeque::new();
+        let mut recent_valid: VecDeque<bool> = VecDeque::new();
+        loop {
+            // latest one minute
+            if queue.len() == 31 {
+                queue.pop_front();
+            }
+            if recent_valid.len() == 31 {
+                recent_valid.pop_front();
+            }
+            // allow get value within one minute
+            if queue.len() > 0 && recent_valid.iter().filter(|v| **v).count() > queue.len() / 2 {
+                let sum: f64 = queue.iter().map(|f| f.to_owned()).sum();
+                let avg = sum / (queue.len() as f64);
+                *CPU_USAGE_ONE_MINUTE.lock().unwrap() = Some((avg, Instant::now()));
+            } else {
+                *CPU_USAGE_ONE_MINUTE.lock().unwrap() = None;
+            }
+            if WAIT_OBJECT_0 != WaitForSingleObject(event, INFINITE) {
+                recent_valid.push_back(false);
+                continue;
+            }
+            if PdhGetFormattedCounterValue(
+                counter,
+                PDH_FMT_DOUBLE,
+                &mut _counter_type,
+                &mut counter_value,
+            ) != 0
+                || counter_value.CStatus != 0
+            {
+                recent_valid.push_back(false);
+                continue;
+            }
+            queue.push_back(counter_value.u.doubleValue().clone());
+            recent_valid.push_back(true);
+        }
+    };
+    use std::sync::Once;
+    static ONCE: Once = Once::new();
+    ONCE.call_once(|| {
+        std::thread::spawn(f);
+    });
+}
+
+pub fn cpu_uage_one_minute() -> Option<f64> {
+    let v = CPU_USAGE_ONE_MINUTE.lock().unwrap().clone();
+    if let Some((v, instant)) = v {
+        if instant.elapsed().as_secs() < 30 {
+            return Some(v);
+        }
+    }
+    None
+}
+
+pub fn sync_cpu_usage(cpu_usage: Option<f64>) {
+    let v = match cpu_usage {
+        Some(cpu_usage) => Some((cpu_usage, Instant::now())),
+        None => None,
+    };
+    *CPU_USAGE_ONE_MINUTE.lock().unwrap() = v;
+    log::info!("cpu usage synced: {:?}", cpu_usage);
+}
diff --git a/libs/portable/build.rs b/libs/portable/build.rs
index 2450e16cd..4f33273f5 100644
--- a/libs/portable/build.rs
+++ b/libs/portable/build.rs
@@ -1,5 +1,10 @@
 extern crate embed_resource;
+use std::fs;
 
 fn main() {
-    embed_resource::compile("icon.rc", embed_resource::NONE);
+    let runner_res_path = "Runner.res";
+    match fs::metadata(runner_res_path) {
+        Ok(_) => println!("cargo:rustc-link-lib=dylib:+verbatim=./libs/portable/Runner.res"),
+        Err(_) => embed_resource::compile("icon.rc", embed_resource::NONE),
+    }
 }
diff --git a/libs/portable/src/main.rs b/libs/portable/src/main.rs
index 6a000f5a1..dc6f7bc84 100644
--- a/libs/portable/src/main.rs
+++ b/libs/portable/src/main.rs
@@ -54,7 +54,7 @@ fn execute(path: PathBuf, args: Vec<String>) {
         .stdin(Stdio::inherit())
         .stdout(Stdio::inherit())
         .stderr(Stdio::inherit())
-        .output()
+        .spawn()
         .ok();
 }
 
diff --git a/libs/scrap/Cargo.toml b/libs/scrap/Cargo.toml
index ffce4ab97..4956403a9 100644
--- a/libs/scrap/Cargo.toml
+++ b/libs/scrap/Cargo.toml
@@ -15,7 +15,6 @@ mediacodec = ["ndk"]
 linux-pkg-config = ["dep:pkg-config"]
 
 [dependencies]
-block = "0.1"
 cfg-if = "1.0"
 num_cpus = "1.15"
 lazy_static = "1.4"
@@ -27,6 +26,9 @@ version = "0.3"
 default-features = true
 features = ["dxgi", "dxgi1_2", "dxgi1_5", "d3d11", "winuser", "winerror", "errhandlingapi", "libloaderapi"]
 
+[target.'cfg(target_os = "macos")'.dependencies]
+block = "0.1"
+
 [target.'cfg(target_os = "android")'.dependencies]
 android_logger = "0.13"
 jni = "0.21"
diff --git a/libs/scrap/examples/benchmark.rs b/libs/scrap/examples/benchmark.rs
index dd1380995..83fba99d7 100644
--- a/libs/scrap/examples/benchmark.rs
+++ b/libs/scrap/examples/benchmark.rs
@@ -1,8 +1,8 @@
 use docopt::Docopt;
 use hbb_common::env_logger::{init_from_env, Env, DEFAULT_FILTER_ENV};
 use scrap::{
-    aom::{AomDecoder, AomDecoderConfig, AomEncoder, AomEncoderConfig},
-    codec::{EncoderApi, EncoderCfg},
+    aom::{AomDecoder, AomEncoder, AomEncoderConfig},
+    codec::{EncoderApi, EncoderCfg, Quality as Q},
     Capturer, Display, TraitCapturer, VpxDecoder, VpxDecoderConfig, VpxEncoder, VpxEncoderConfig,
     VpxVideoCodecId::{self, *},
     STRIDE_ALIGN,
@@ -15,13 +15,14 @@ const USAGE: &'static str = "
 Codec benchmark.
 
 Usage:
-  benchmark [--count=COUNT] [--bitrate=KBS] [--hw-pixfmt=PIXFMT]
+  benchmark [--count=COUNT] [--quality=QUALITY] [--hw-pixfmt=PIXFMT]
   benchmark (-h | --help)
 
 Options:
   -h --help             Show this screen.
   --count=COUNT         Capture frame count [default: 100].
-  --bitrate=KBS         Video bitrate in kilobits per second [default: 5000].
+  --quality=QUALITY     Video quality [default: Balanced].
+                        Valid values: Best, Balanced, Low.
   --hw-pixfmt=PIXFMT    Hardware codec pixfmt. [default: i420]
                         Valid values: i420, nv12.
 ";
@@ -29,7 +30,7 @@ Options:
 #[derive(Debug, serde::Deserialize)]
 struct Args {
     flag_count: usize,
-    flag_bitrate: usize,
+    flag_quality: Quality,
     flag_hw_pixfmt: Pixfmt,
 }
 
@@ -39,20 +40,32 @@ enum Pixfmt {
     NV12,
 }
 
+#[derive(Debug, serde::Deserialize)]
+enum Quality {
+    Best,
+    Balanced,
+    Low,
+}
+
 fn main() {
     init_from_env(Env::default().filter_or(DEFAULT_FILTER_ENV, "info"));
     let args: Args = Docopt::new(USAGE)
         .and_then(|d| d.deserialize())
         .unwrap_or_else(|e| e.exit());
-    let bitrate_k = args.flag_bitrate;
+    let quality = args.flag_quality;
     let yuv_count = args.flag_count;
     let (yuvs, width, height) = capture_yuv(yuv_count);
     println!(
-        "benchmark {}x{} bitrate:{}k hw_pixfmt:{:?}",
-        width, height, bitrate_k, args.flag_hw_pixfmt
+        "benchmark {}x{} quality:{:?}k hw_pixfmt:{:?}",
+        width, height, quality, args.flag_hw_pixfmt
     );
-    [VP8, VP9].map(|c| test_vpx(c, &yuvs, width, height, bitrate_k, yuv_count));
-    test_av1(&yuvs, width, height, bitrate_k, yuv_count);
+    let quality = match quality {
+        Quality::Best => Q::Best,
+        Quality::Balanced => Q::Balanced,
+        Quality::Low => Q::Low,
+    };
+    [VP8, VP9].map(|c| test_vpx(c, &yuvs, width, height, quality, yuv_count));
+    test_av1(&yuvs, width, height, quality, yuv_count);
     #[cfg(feature = "hwcodec")]
     {
         use hwcodec::AVPixelFormat;
@@ -61,7 +74,7 @@ fn main() {
             Pixfmt::NV12 => AVPixelFormat::AV_PIX_FMT_NV12,
         };
         let yuvs = hw::vpx_yuv_to_hw_yuv(yuvs, width, height, hw_pixfmt);
-        hw::test(&yuvs, width, height, bitrate_k, yuv_count, hw_pixfmt);
+        hw::test(&yuvs, width, height, quality, yuv_count, hw_pixfmt);
     }
 }
 
@@ -95,14 +108,15 @@ fn test_vpx(
     yuvs: &Vec<Vec<u8>>,
     width: usize,
     height: usize,
-    bitrate_k: usize,
+    quality: Q,
     yuv_count: usize,
 ) {
     let config = EncoderCfg::VPX(VpxEncoderConfig {
         width: width as _,
         height: height as _,
-        bitrate: bitrate_k as _,
+        quality,
         codec: codec_id,
+        keyframe_interval: None,
     });
     let mut encoder = VpxEncoder::new(config).unwrap();
     let mut vpxs = vec![];
@@ -129,11 +143,7 @@ fn test_vpx(
         size / yuv_count
     );
 
-    let mut decoder = VpxDecoder::new(VpxDecoderConfig {
-        codec: codec_id,
-        num_threads: (num_cpus::get() / 2) as _,
-    })
-    .unwrap();
+    let mut decoder = VpxDecoder::new(VpxDecoderConfig { codec: codec_id }).unwrap();
     let start = Instant::now();
     for vpx in vpxs {
         let _ = decoder.decode(&vpx);
@@ -146,11 +156,12 @@ fn test_vpx(
     );
 }
 
-fn test_av1(yuvs: &Vec<Vec<u8>>, width: usize, height: usize, bitrate_k: usize, yuv_count: usize) {
+fn test_av1(yuvs: &Vec<Vec<u8>>, width: usize, height: usize, quality: Q, yuv_count: usize) {
     let config = EncoderCfg::AOM(AomEncoderConfig {
         width: width as _,
         height: height as _,
-        bitrate: bitrate_k as _,
+        quality,
+        keyframe_interval: None,
     });
     let mut encoder = AomEncoder::new(config).unwrap();
     let start = Instant::now();
@@ -171,10 +182,7 @@ fn test_av1(yuvs: &Vec<Vec<u8>>, width: usize, height: usize, bitrate_k: usize,
         start.elapsed() / yuv_count as _,
         size / yuv_count
     );
-    let mut decoder = AomDecoder::new(AomDecoderConfig {
-        num_threads: (num_cpus::get() / 2) as _,
-    })
-    .unwrap();
+    let mut decoder = AomDecoder::new().unwrap();
     let start = Instant::now();
     for av1 in av1s {
         let _ = decoder.decode(&av1);
@@ -195,6 +203,7 @@ mod hw {
         RateControl::*,
     };
     use scrap::{
+        codec::codec_thread_num,
         convert::{
             hw::{hw_bgra_to_i420, hw_bgra_to_nv12},
             i420_to_bgra,
@@ -206,21 +215,23 @@ mod hw {
         yuvs: &Vec<Vec<u8>>,
         width: usize,
         height: usize,
-        bitrate_k: usize,
+        quality: Q,
         yuv_count: usize,
         pixfmt: AVPixelFormat,
     ) {
+        let bitrate = scrap::hwcodec::HwEncoder::convert_quality(quality);
         let ctx = EncodeContext {
             name: String::from(""),
             width: width as _,
             height: height as _,
             pixfmt,
             align: 0,
-            bitrate: (bitrate_k * 1000) as _,
+            bitrate: bitrate as i32 * 1000,
             timebase: [1, 30],
             gop: 60,
             quality: Quality_Default,
             rc: RC_DEFAULT,
+            thread_count: codec_thread_num() as _,
         };
 
         let encoders = Encoder::available_encoders(ctx.clone());
@@ -273,6 +284,7 @@ mod hw {
         let ctx = DecodeContext {
             name: info.name,
             device_type: info.hwdevice,
+            thread_count: codec_thread_num() as _,
         };
 
         let mut decoder = Decoder::new(ctx.clone()).unwrap();
diff --git a/libs/scrap/examples/record-screen.rs b/libs/scrap/examples/record-screen.rs
index 48f73052a..6640d5698 100644
--- a/libs/scrap/examples/record-screen.rs
+++ b/libs/scrap/examples/record-screen.rs
@@ -13,7 +13,7 @@ use std::time::{Duration, Instant};
 use std::{io, thread};
 
 use docopt::Docopt;
-use scrap::codec::{EncoderApi, EncoderCfg};
+use scrap::codec::{EncoderApi, EncoderCfg, Quality as Q};
 use webm::mux;
 use webm::mux::Track;
 
@@ -24,17 +24,18 @@ const USAGE: &'static str = "
 Simple WebM screen capture.
 
 Usage:
-  record-screen <path> [--time=<s>] [--fps=<fps>] [--bv=<kbps>] [--ba=<kbps>] [--codec CODEC]
+  record-screen <path> [--time=<s>] [--fps=<fps>] [--quality=<quality>] [--ba=<kbps>] [--codec CODEC]
   record-screen (-h | --help)
 
 Options:
-  -h --help      Show this screen.
-  --time=<s>     Recording duration in seconds.
-  --fps=<fps>    Frames per second [default: 30].
-  --bv=<kbps>    Video bitrate in kilobits per second [default: 5000].
-  --ba=<kbps>    Audio bitrate in kilobits per second [default: 96].
-  --codec CODEC  Configure the codec used. [default: vp9]
-                 Valid values: vp8, vp9.
+  -h --help                 Show this screen.
+  --time=<s>                Recording duration in seconds.
+  --fps=<fps>               Frames per second [default: 30].
+  --quality=<quality>       Video quality [default: Balanced].
+                            Valid values: Best, Balanced, Low.
+  --ba=<kbps>               Audio bitrate in kilobits per second [default: 96].
+  --codec CODEC             Configure the codec used. [default: vp9]
+                            Valid values: vp8, vp9.
 ";
 
 #[derive(Debug, serde::Deserialize)]
@@ -43,7 +44,14 @@ struct Args {
     flag_codec: Codec,
     flag_time: Option<u64>,
     flag_fps: u64,
-    flag_bv: u32,
+    flag_quality: Quality,
+}
+
+#[derive(Debug, serde::Deserialize)]
+enum Quality {
+    Best,
+    Balanced,
+    Low,
 }
 
 #[derive(Debug, serde::Deserialize)]
@@ -97,12 +105,17 @@ fn main() -> io::Result<()> {
     let mut vt = webm.add_video_track(width, height, None, mux_codec);
 
     // Setup the encoder.
-
+    let quality = match args.flag_quality {
+        Quality::Best => Q::Best,
+        Quality::Balanced => Q::Balanced,
+        Quality::Low => Q::Low,
+    };
     let mut vpx = vpx_encode::VpxEncoder::new(EncoderCfg::VPX(vpx_encode::VpxEncoderConfig {
         width,
         height,
-        bitrate: args.flag_bv,
+        quality,
         codec: vpx_codec,
+        keyframe_interval: None,
     }))
     .unwrap();
 
diff --git a/libs/scrap/src/android/ffi.rs b/libs/scrap/src/android/ffi.rs
index 97d677272..e9c60ef93 100644
--- a/libs/scrap/src/android/ffi.rs
+++ b/libs/scrap/src/android/ffi.rs
@@ -99,9 +99,11 @@ pub extern "system" fn Java_com_carriez_flutter_1hbb_MainService_onVideoFrameUpd
     buffer: JObject,
 ) {
     let jb = JByteBuffer::from(buffer);
-    let data = env.get_direct_buffer_address(&jb).unwrap();
-    let len = env.get_direct_buffer_capacity(&jb).unwrap();
-    VIDEO_RAW.lock().unwrap().update(data, len);
+    if let Ok(data) = env.get_direct_buffer_address(&jb) {
+        if let Ok(len) = env.get_direct_buffer_capacity(&jb) {
+            VIDEO_RAW.lock().unwrap().update(data, len);
+        }
+    }
 }
 
 #[no_mangle]
@@ -111,9 +113,11 @@ pub extern "system" fn Java_com_carriez_flutter_1hbb_MainService_onAudioFrameUpd
     buffer: JObject,
 ) {
     let jb = JByteBuffer::from(buffer);
-    let data = env.get_direct_buffer_address(&jb).unwrap();
-    let len = env.get_direct_buffer_capacity(&jb).unwrap();
-    AUDIO_RAW.lock().unwrap().update(data, len);
+    if let Ok(data) = env.get_direct_buffer_address(&jb) {
+        if let Ok(len) = env.get_direct_buffer_capacity(&jb) {
+            AUDIO_RAW.lock().unwrap().update(data, len);
+        }
+    }
 }
 
 #[no_mangle]
@@ -142,25 +146,26 @@ pub extern "system" fn Java_com_carriez_flutter_1hbb_MainService_init(
     ctx: JObject,
 ) {
     log::debug!("MainService init from java");
-    let jvm = env.get_java_vm().unwrap();
-
-    *JVM.write().unwrap() = Some(jvm);
-
-    let context = env.new_global_ref(ctx).unwrap();
-    *MAIN_SERVICE_CTX.write().unwrap() = Some(context);
+    if let Ok(jvm) = env.get_java_vm() {
+        *JVM.write().unwrap() = Some(jvm);
+        if let Ok(context) = env.new_global_ref(ctx) {
+            *MAIN_SERVICE_CTX.write().unwrap() = Some(context);
+        }
+    }
 }
 
-pub fn call_main_service_mouse_input(mask: i32, x: i32, y: i32) -> JniResult<()> {
+pub fn call_main_service_pointer_input(kind: &str, mask: i32, x: i32, y: i32) -> JniResult<()> {
     if let (Some(jvm), Some(ctx)) = (
         JVM.read().unwrap().as_ref(),
         MAIN_SERVICE_CTX.read().unwrap().as_ref(),
     ) {
         let mut env = jvm.attach_current_thread_as_daemon()?;
+        let kind = env.new_string(kind)?;
         env.call_method(
             ctx,
-            "rustMouseInput",
-            "(III)V",
-            &[JValue::Int(mask), JValue::Int(x), JValue::Int(y)],
+            "rustPointerInput",
+            "(Ljava/lang/String;III)V",
+            &[JValue::Object(&JObject::from(kind)), JValue::Int(mask), JValue::Int(x), JValue::Int(y)],
         )?;
         return Ok(());
     } else {
diff --git a/libs/scrap/src/common/aom.rs b/libs/scrap/src/common/aom.rs
index 2c614feec..dcb4968e6 100644
--- a/libs/scrap/src/common/aom.rs
+++ b/libs/scrap/src/common/aom.rs
@@ -6,6 +6,7 @@
 
 include!(concat!(env!("OUT_DIR"), "/aom_ffi.rs"));
 
+use crate::codec::{base_bitrate, codec_thread_num, Quality};
 use crate::{codec::EncoderApi, EncodeFrame, STRIDE_ALIGN};
 use crate::{common::GoogleImage, generate_call_macro, generate_call_ptr_macro, Error, Result};
 use hbb_common::{
@@ -43,7 +44,8 @@ impl Default for aom_image_t {
 pub struct AomEncoderConfig {
     pub width: u32,
     pub height: u32,
-    pub bitrate: u32,
+    pub quality: Quality,
+    pub keyframe_interval: Option<usize>,
 }
 
 pub struct AomEncoder {
@@ -56,7 +58,6 @@ pub struct AomEncoder {
 mod webrtc {
     use super::*;
 
-    const kQpMin: u32 = 10;
     const kUsageProfile: u32 = AOM_USAGE_REALTIME;
     const kMinQindex: u32 = 145; // Min qindex threshold for QP scaling.
     const kMaxQindex: u32 = 205; // Max qindex threshold for QP scaling.
@@ -65,26 +66,8 @@ mod webrtc {
     const kRtpTicksPerSecond: i32 = 90000;
     const kMinimumFrameRate: f64 = 1.0;
 
-    const kQpMax: u32 = 25; // to-do: webrtc use dynamic value, no more than 63
-
-    fn number_of_threads(width: u32, height: u32, number_of_cores: usize) -> u32 {
-        // Keep the number of encoder threads equal to the possible number of
-        // column/row tiles, which is (1, 2, 4, 8). See comments below for
-        // AV1E_SET_TILE_COLUMNS/ROWS.
-        if width * height >= 640 * 360 && number_of_cores > 4 {
-            return 4;
-        } else if width * height >= 320 * 180 && number_of_cores > 2 {
-            return 2;
-        } else {
-            // Use 2 threads for low res on ARM.
-            #[cfg(any(target_arch = "arm", target_arch = "aarch64", target_os = "android"))]
-            if width * height >= 320 * 180 && number_of_cores > 2 {
-                return 2;
-            }
-            // 1 thread less than VGA.
-            return 1;
-        }
-    }
+    pub const DEFAULT_Q_MAX: u32 = 56; // no more than 63
+    pub const DEFAULT_Q_MIN: u32 = 12; // no more than 63, litter than q_max
 
     // Only positive speeds, range for real-time coding currently is: 6 - 8.
     // Lower means slower/better quality, higher means fastest/lower quality.
@@ -119,14 +102,31 @@ mod webrtc {
         // Overwrite default config with input encoder settings & RTC-relevant values.
         c.g_w = cfg.width;
         c.g_h = cfg.height;
-        c.g_threads = number_of_threads(cfg.width, cfg.height, num_cpus::get());
+        c.g_threads = codec_thread_num() as _;
         c.g_timebase.num = 1;
         c.g_timebase.den = kRtpTicksPerSecond;
-        c.rc_target_bitrate = cfg.bitrate; // kilobits/sec.
         c.g_input_bit_depth = kBitDepth;
-        c.kf_mode = aom_kf_mode::AOM_KF_DISABLED;
-        c.rc_min_quantizer = kQpMin;
-        c.rc_max_quantizer = kQpMax;
+        if let Some(keyframe_interval) = cfg.keyframe_interval {
+            c.kf_min_dist = 0;
+            c.kf_max_dist = keyframe_interval as _;
+        } else {
+            c.kf_mode = aom_kf_mode::AOM_KF_DISABLED;
+        }
+        let (q_min, q_max, b) = AomEncoder::convert_quality(cfg.quality);
+        if q_min > 0 && q_min < q_max && q_max < 64 {
+            c.rc_min_quantizer = q_min;
+            c.rc_max_quantizer = q_max;
+        } else {
+            c.rc_min_quantizer = DEFAULT_Q_MIN;
+            c.rc_max_quantizer = DEFAULT_Q_MAX;
+        }
+        let base_bitrate = base_bitrate(cfg.width as _, cfg.height as _);
+        let bitrate = base_bitrate * b / 100;
+        if bitrate > 0 {
+            c.rc_target_bitrate = bitrate;
+        } else {
+            c.rc_target_bitrate = base_bitrate;
+        }
         c.rc_undershoot_pct = 50;
         c.rc_overshoot_pct = 50;
         c.rc_buf_initial_sz = 600;
@@ -259,11 +259,24 @@ impl EncoderApi for AomEncoder {
         true
     }
 
-    fn set_bitrate(&mut self, bitrate: u32) -> ResultType<()> {
-        let mut new_enc_cfg = unsafe { *self.ctx.config.enc.to_owned() };
-        new_enc_cfg.rc_target_bitrate = bitrate;
-        call_aom!(aom_codec_enc_config_set(&mut self.ctx, &new_enc_cfg));
-        return Ok(());
+    fn set_quality(&mut self, quality: Quality) -> ResultType<()> {
+        let mut c = unsafe { *self.ctx.config.enc.to_owned() };
+        let (q_min, q_max, b) = Self::convert_quality(quality);
+        if q_min > 0 && q_min < q_max && q_max < 64 {
+            c.rc_min_quantizer = q_min;
+            c.rc_max_quantizer = q_max;
+        }
+        let bitrate = base_bitrate(self.width as _, self.height as _) * b / 100;
+        if bitrate > 0 {
+            c.rc_target_bitrate = bitrate;
+        }
+        call_aom!(aom_codec_enc_config_set(&mut self.ctx, &c));
+        Ok(())
+    }
+
+    fn bitrate(&self) -> u32 {
+        let c = unsafe { *self.ctx.config.enc.to_owned() };
+        c.rc_target_bitrate
     }
 }
 
@@ -319,6 +332,35 @@ impl AomEncoder {
             ..Default::default()
         }
     }
+
+    pub fn convert_quality(quality: Quality) -> (u32, u32, u32) {
+        // we can use lower bitrate for av1
+        match quality {
+            Quality::Best => (12, 25, 100),
+            Quality::Balanced => (12, 35, 100 * 2 / 3),
+            Quality::Low => (18, 45, 50),
+            Quality::Custom(b) => {
+                let (q_min, q_max) = Self::calc_q_values(b);
+                (q_min, q_max, b)
+            }
+        }
+    }
+
+    #[inline]
+    fn calc_q_values(b: u32) -> (u32, u32) {
+        let b = std::cmp::min(b, 200);
+        let q_min1: i32 = 24;
+        let q_min2 = 5;
+        let q_max1 = 45;
+        let q_max2 = 25;
+
+        let t = b as f32 / 200.0;
+
+        let q_min: u32 = ((1.0 - t) * q_min1 as f32 + t * q_min2 as f32).round() as u32;
+        let q_max = ((1.0 - t) * q_max1 as f32 + t * q_max2 as f32).round() as u32;
+
+        (q_min, q_max)
+    }
 }
 
 impl Drop for AomEncoder {
@@ -360,24 +402,16 @@ impl<'a> Iterator for EncodeFrames<'a> {
     }
 }
 
-pub struct AomDecoderConfig {
-    pub num_threads: u32,
-}
-
 pub struct AomDecoder {
     ctx: aom_codec_ctx_t,
 }
 
 impl AomDecoder {
-    pub fn new(cfg: AomDecoderConfig) -> Result<Self> {
+    pub fn new() -> Result<Self> {
         let i = call_aom_ptr!(aom_codec_av1_dx());
         let mut ctx = Default::default();
         let cfg = aom_codec_dec_cfg_t {
-            threads: if cfg.num_threads == 0 {
-                num_cpus::get() as _
-            } else {
-                cfg.num_threads
-            },
+            threads: codec_thread_num() as _,
             w: 0,
             h: 0,
             allow_lowbitdepth: 1,
diff --git a/libs/scrap/src/common/codec.rs b/libs/scrap/src/common/codec.rs
index 03e8a19a2..2c3cbff6c 100644
--- a/libs/scrap/src/common/codec.rs
+++ b/libs/scrap/src/common/codec.rs
@@ -11,22 +11,23 @@ use crate::mediacodec::{
     MediaCodecDecoder, MediaCodecDecoders, H264_DECODER_SUPPORT, H265_DECODER_SUPPORT,
 };
 use crate::{
-    aom::{self, AomDecoder, AomDecoderConfig, AomEncoder, AomEncoderConfig},
+    aom::{self, AomDecoder, AomEncoder, AomEncoderConfig},
     common::GoogleImage,
     vpxcodec::{self, VpxDecoder, VpxDecoderConfig, VpxEncoder, VpxEncoderConfig, VpxVideoCodecId},
     CodecName, ImageRgb,
 };
 
-#[cfg(not(any(target_os = "android", target_os = "ios")))]
-use hbb_common::sysinfo::{System, SystemExt};
 use hbb_common::{
     anyhow::anyhow,
+    bail,
     config::PeerConfig,
     log,
     message_proto::{
         supported_decoding::PreferCodec, video_frame, EncodedVideoFrames, Message,
         SupportedDecoding, SupportedEncoding,
     },
+    sysinfo::{System, SystemExt},
+    tokio::time::Instant,
     ResultType,
 };
 #[cfg(any(feature = "hwcodec", feature = "mediacodec"))]
@@ -35,6 +36,7 @@ use hbb_common::{config::Config2, lazy_static};
 lazy_static::lazy_static! {
     static ref PEER_DECODINGS: Arc<Mutex<HashMap<i32, SupportedDecoding>>> = Default::default();
     static ref CODEC_NAME: Arc<Mutex<CodecName>> = Arc::new(Mutex::new(CodecName::VP9));
+    static ref THREAD_LOG_TIME: Arc<Mutex<Option<Instant>>> = Arc::new(Mutex::new(None));
 }
 
 #[derive(Debug, Clone)]
@@ -42,7 +44,8 @@ pub struct HwEncoderConfig {
     pub name: String,
     pub width: usize,
     pub height: usize,
-    pub bitrate: i32,
+    pub quality: Quality,
+    pub keyframe_interval: Option<usize>,
 }
 
 #[derive(Debug, Clone)]
@@ -61,7 +64,9 @@ pub trait EncoderApi {
 
     fn use_yuv(&self) -> bool;
 
-    fn set_bitrate(&mut self, bitrate: u32) -> ResultType<()>;
+    fn set_quality(&mut self, quality: Quality) -> ResultType<()>;
+
+    fn bitrate(&self) -> u32;
 }
 
 pub struct Encoder {
@@ -83,9 +88,9 @@ impl DerefMut for Encoder {
 }
 
 pub struct Decoder {
-    vp8: VpxDecoder,
-    vp9: VpxDecoder,
-    av1: AomDecoder,
+    vp8: Option<VpxDecoder>,
+    vp9: Option<VpxDecoder>,
+    av1: Option<AomDecoder>,
     #[cfg(feature = "hwcodec")]
     hw: HwDecoders,
     #[cfg(feature = "hwcodec")]
@@ -190,7 +195,6 @@ impl Encoder {
 
         #[allow(unused_mut)]
         let mut auto_codec = CodecName::VP9;
-        #[cfg(not(any(target_os = "android", target_os = "ios")))]
         if vp8_useable && System::new_all().total_memory() <= 4 * 1024 * 1024 * 1024 {
             // 4 Gb
             auto_codec = CodecName::VP8
@@ -274,18 +278,13 @@ impl Decoder {
     pub fn new() -> Decoder {
         let vp8 = VpxDecoder::new(VpxDecoderConfig {
             codec: VpxVideoCodecId::VP8,
-            num_threads: (num_cpus::get() / 2) as _,
         })
-        .unwrap();
+        .ok();
         let vp9 = VpxDecoder::new(VpxDecoderConfig {
             codec: VpxVideoCodecId::VP9,
-            num_threads: (num_cpus::get() / 2) as _,
         })
-        .unwrap();
-        let av1 = AomDecoder::new(AomDecoderConfig {
-            num_threads: (num_cpus::get() / 2) as _,
-        })
-        .unwrap();
+        .ok();
+        let av1 = AomDecoder::new().ok();
         Decoder {
             vp8,
             vp9,
@@ -315,13 +314,25 @@ impl Decoder {
     ) -> ResultType<bool> {
         match frame {
             video_frame::Union::Vp8s(vp8s) => {
-                Decoder::handle_vpxs_video_frame(&mut self.vp8, vp8s, rgb)
+                if let Some(vp8) = &mut self.vp8 {
+                    Decoder::handle_vpxs_video_frame(vp8, vp8s, rgb)
+                } else {
+                    bail!("vp8 decoder not available");
+                }
             }
             video_frame::Union::Vp9s(vp9s) => {
-                Decoder::handle_vpxs_video_frame(&mut self.vp9, vp9s, rgb)
+                if let Some(vp9) = &mut self.vp9 {
+                    Decoder::handle_vpxs_video_frame(vp9, vp9s, rgb)
+                } else {
+                    bail!("vp9 decoder not available");
+                }
             }
             video_frame::Union::Av1s(av1s) => {
-                Decoder::handle_av1s_video_frame(&mut self.av1, av1s, rgb)
+                if let Some(av1) = &mut self.av1 {
+                    Decoder::handle_av1s_video_frame(av1, av1s, rgb)
+                } else {
+                    bail!("av1 decoder not available");
+                }
             }
             #[cfg(feature = "hwcodec")]
             video_frame::Union::H264s(h264s) => {
@@ -471,3 +482,72 @@ fn enable_hwcodec_option() -> bool {
     }
     return true; // default is true
 }
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub enum Quality {
+    Best,
+    Balanced,
+    Low,
+    Custom(u32),
+}
+
+impl Default for Quality {
+    fn default() -> Self {
+        Self::Balanced
+    }
+}
+
+pub fn base_bitrate(width: u32, height: u32) -> u32 {
+    #[allow(unused_mut)]
+    let mut base_bitrate = ((width * height) / 1000) as u32; // same as 1.1.9
+    if base_bitrate == 0 {
+        base_bitrate = 1920 * 1080 / 1000;
+    }
+    #[cfg(target_os = "android")]
+    {
+        // fix when android screen shrinks
+        let fix = crate::Display::fix_quality() as u32;
+        log::debug!("Android screen, fix quality:{}", fix);
+        base_bitrate = base_bitrate * fix;
+    }
+    base_bitrate
+}
+
+pub fn codec_thread_num() -> usize {
+    let max: usize = num_cpus::get();
+    let mut res;
+    let info;
+    #[cfg(windows)]
+    {
+        res = 0;
+        let percent = hbb_common::platform::windows::cpu_uage_one_minute();
+        info = format!("cpu usage:{:?}", percent);
+        if let Some(pecent) = percent {
+            if pecent < 100.0 {
+                res = ((100.0 - pecent) * (max as f64) / 200.0).round() as usize;
+            }
+        }
+    }
+    #[cfg(not(windows))]
+    {
+        let s = System::new_all();
+        // https://man7.org/linux/man-pages/man3/getloadavg.3.html
+        let avg = s.load_average();
+        info = format!("cpu loadavg:{}", avg.one);
+        res = (((max as f64) - avg.one) * 0.5).round() as usize;
+    }
+    res = std::cmp::min(res, max / 2);
+    if res == 0 {
+        res = 1;
+    }
+    // avoid frequent log
+    let log = match THREAD_LOG_TIME.lock().unwrap().clone() {
+        Some(instant) => instant.elapsed().as_secs() > 1,
+        None => true,
+    };
+    if log {
+        log::info!("cpu num:{max}, {info}, codec thread:{res}");
+        *THREAD_LOG_TIME.lock().unwrap() = Some(Instant::now());
+    }
+    res
+}
diff --git a/libs/scrap/src/common/hwcodec.rs b/libs/scrap/src/common/hwcodec.rs
index aba0d55d0..c1fbdfa6e 100644
--- a/libs/scrap/src/common/hwcodec.rs
+++ b/libs/scrap/src/common/hwcodec.rs
@@ -1,5 +1,5 @@
 use crate::{
-    codec::{EncoderApi, EncoderCfg},
+    codec::{base_bitrate, codec_thread_num, EncoderApi, EncoderCfg},
     hw, ImageFormat, ImageRgb, HW_STRIDE_ALIGN,
 };
 use hbb_common::{
@@ -34,6 +34,9 @@ pub struct HwEncoder {
     yuv: Vec<u8>,
     pub format: DataFormat,
     pub pixfmt: AVPixelFormat,
+    width: u32,
+    height: u32,
+    bitrate: u32, //kbs
 }
 
 impl EncoderApi for HwEncoder {
@@ -43,17 +46,25 @@ impl EncoderApi for HwEncoder {
     {
         match cfg {
             EncoderCfg::HW(config) => {
+                let b = Self::convert_quality(config.quality);
+                let base_bitrate = base_bitrate(config.width as _, config.height as _);
+                let mut bitrate = base_bitrate * b / 100;
+                if base_bitrate <= 0 {
+                    bitrate = base_bitrate;
+                }
+                let gop = config.keyframe_interval.unwrap_or(DEFAULT_GOP as _) as i32;
                 let ctx = EncodeContext {
                     name: config.name.clone(),
                     width: config.width as _,
                     height: config.height as _,
                     pixfmt: DEFAULT_PIXFMT,
                     align: HW_STRIDE_ALIGN as _,
-                    bitrate: config.bitrate * 1000,
+                    bitrate: bitrate as i32 * 1000,
                     timebase: DEFAULT_TIME_BASE,
-                    gop: DEFAULT_GOP,
+                    gop,
                     quality: DEFAULT_HW_QUALITY,
                     rc: DEFAULT_RC,
+                    thread_count: codec_thread_num() as _, // ffmpeg's thread_count is used for cpu
                 };
                 let format = match Encoder::format_from_name(config.name.clone()) {
                     Ok(format) => format,
@@ -70,6 +81,9 @@ impl EncoderApi for HwEncoder {
                         yuv: vec![],
                         format,
                         pixfmt: ctx.pixfmt,
+                        width: ctx.width as _,
+                        height: ctx.height as _,
+                        bitrate,
                     }),
                     Err(_) => Err(anyhow!(format!("Failed to create encoder"))),
                 }
@@ -114,10 +128,19 @@ impl EncoderApi for HwEncoder {
         false
     }
 
-    fn set_bitrate(&mut self, bitrate: u32) -> ResultType<()> {
-        self.encoder.set_bitrate((bitrate * 1000) as _).ok();
+    fn set_quality(&mut self, quality: crate::codec::Quality) -> ResultType<()> {
+        let b = Self::convert_quality(quality);
+        let bitrate = base_bitrate(self.width as _, self.height as _) * b / 100;
+        if bitrate > 0 {
+            self.encoder.set_bitrate((bitrate * 1000) as _).ok();
+            self.bitrate = bitrate;
+        }
         Ok(())
     }
+
+    fn bitrate(&self) -> u32 {
+        self.bitrate
+    }
 }
 
 impl HwEncoder {
@@ -159,6 +182,16 @@ impl HwEncoder {
             Err(_) => Ok(Vec::<EncodeFrame>::new()),
         }
     }
+
+    pub fn convert_quality(quality: crate::codec::Quality) -> u32 {
+        use crate::codec::Quality;
+        match quality {
+            Quality::Best => 150,
+            Quality::Balanced => 100,
+            Quality::Low => 50,
+            Quality::Custom(b) => b,
+        }
+    }
 }
 
 pub struct HwDecoder {
@@ -208,6 +241,7 @@ impl HwDecoder {
         let ctx = DecodeContext {
             name: info.name.clone(),
             device_type: info.hwdevice.clone(),
+            thread_count: codec_thread_num() as _,
         };
         match Decoder::new(ctx) {
             Ok(decoder) => Ok(HwDecoder { decoder, info }),
@@ -281,7 +315,7 @@ impl HwDecoderImage<'_> {
 }
 
 fn get_config(k: &str) -> ResultType<CodecInfos> {
-    let v = HwCodecConfig::get()
+    let v = HwCodecConfig::load()
         .options
         .get(k)
         .unwrap_or(&"".to_owned())
@@ -304,6 +338,7 @@ pub fn check_config() {
         gop: DEFAULT_GOP,
         quality: DEFAULT_HW_QUALITY,
         rc: DEFAULT_RC,
+        thread_count: 4,
     };
     let encoders = CodecInfo::score(Encoder::available_encoders(ctx));
     let decoders = CodecInfo::score(Decoder::available_decoders());
@@ -329,37 +364,28 @@ pub fn check_config() {
 }
 
 pub fn check_config_process() {
-    use hbb_common::sysinfo::{ProcessExt, System, SystemExt};
-
-    std::thread::spawn(move || {
-        // Remove to avoid checking process errors
+    use std::sync::Once;
+    let f = || {
+        // Clear to avoid checking process errors
         // But when the program is just started, the configuration file has not been updated, and the new connection will read an empty configuration
-        HwCodecConfig::remove();
+        HwCodecConfig::clear();
         if let Ok(exe) = std::env::current_exe() {
-            if let Some(file_name) = exe.file_name().to_owned() {
-                let s = System::new_all();
+            if let Some(_) = exe.file_name().to_owned() {
                 let arg = "--check-hwcodec-config";
-                for process in s.processes_by_name(&file_name.to_string_lossy().to_string()) {
-                    if process.cmd().iter().any(|cmd| cmd.contains(arg)) {
-                        log::warn!("already have process {}", arg);
-                        return;
-                    }
-                }
                 if let Ok(mut child) = std::process::Command::new(exe).arg(arg).spawn() {
                     // wait up to 10 seconds
                     for _ in 0..10 {
                         std::thread::sleep(std::time::Duration::from_secs(1));
-                        if let Ok(Some(status)) = child.try_wait() {
-                            if status.success() {
-                                HwCodecConfig::refresh();
-                            }
+                        if let Ok(Some(_)) = child.try_wait() {
                             break;
                         }
                     }
                     allow_err!(child.kill());
                     std::thread::sleep(std::time::Duration::from_millis(30));
                     match child.try_wait() {
-                        Ok(Some(status)) => log::info!("Check hwcodec config, exit with: {status}"),
+                        Ok(Some(status)) => {
+                            log::info!("Check hwcodec config, exit with: {status}")
+                        }
                         Ok(None) => {
                             log::info!(
                                 "Check hwcodec config, status not ready yet, let's really wait"
@@ -371,9 +397,12 @@ pub fn check_config_process() {
                             log::error!("Check hwcodec config, error attempting to wait: {e}")
                         }
                     }
-                    HwCodecConfig::refresh();
                 }
             }
         };
+    };
+    static ONCE: Once = Once::new();
+    ONCE.call_once(|| {
+        std::thread::spawn(f);
     });
 }
diff --git a/libs/scrap/src/common/vpxcodec.rs b/libs/scrap/src/common/vpxcodec.rs
index 54ff14d3f..1bdde0ca6 100644
--- a/libs/scrap/src/common/vpxcodec.rs
+++ b/libs/scrap/src/common/vpxcodec.rs
@@ -7,19 +7,21 @@ use hbb_common::log;
 use hbb_common::message_proto::{EncodedVideoFrame, EncodedVideoFrames, Message, VideoFrame};
 use hbb_common::ResultType;
 
-use crate::codec::EncoderApi;
+use crate::codec::{base_bitrate, codec_thread_num, EncoderApi, Quality};
 use crate::{GoogleImage, STRIDE_ALIGN};
 
-use super::vpx::{vpx_codec_err_t::*, *};
+use super::vpx::{vp8e_enc_control_id::*, vpx_codec_err_t::*, *};
 use crate::{generate_call_macro, generate_call_ptr_macro, Error, Result};
 use hbb_common::bytes::Bytes;
-use std::os::raw::c_uint;
+use std::os::raw::{c_int, c_uint};
 use std::{ptr, slice};
 
 generate_call_macro!(call_vpx, false);
-generate_call_macro!(call_vpx_allow_err, true);
 generate_call_ptr_macro!(call_vpx_ptr);
 
+const DEFAULT_QP_MAX: u32 = 56; // no more than 63
+const DEFAULT_QP_MIN: u32 = 12; // no more than 63
+
 #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
 pub enum VpxVideoCodecId {
     VP8,
@@ -54,11 +56,56 @@ impl EncoderApi for VpxEncoder {
                     VpxVideoCodecId::VP8 => call_vpx_ptr!(vpx_codec_vp8_cx()),
                     VpxVideoCodecId::VP9 => call_vpx_ptr!(vpx_codec_vp9_cx()),
                 };
+                let mut c = unsafe { std::mem::MaybeUninit::zeroed().assume_init() };
+                call_vpx!(vpx_codec_enc_config_default(i, &mut c, 0));
 
-                let c = match config.codec {
-                    VpxVideoCodecId::VP8 => webrtc::vp8::enc_cfg(i, &config)?,
-                    VpxVideoCodecId::VP9 => webrtc::vp9::enc_cfg(i, &config)?,
-                };
+                // https://www.webmproject.org/docs/encoder-parameters/
+                // default: c.rc_min_quantizer = 0, c.rc_max_quantizer = 63
+                // try rc_resize_allowed later
+
+                c.g_w = config.width;
+                c.g_h = config.height;
+                c.g_timebase.num = 1;
+                c.g_timebase.den = 1000; // Output timestamp precision
+                c.rc_undershoot_pct = 95;
+                // When the data buffer falls below this percentage of fullness, a dropped frame is indicated. Set the threshold to zero (0) to disable this feature.
+                // In dynamic scenes, low bitrate gets low fps while high bitrate gets high fps.
+                c.rc_dropframe_thresh = 25;
+                c.g_threads = codec_thread_num() as _;
+                c.g_error_resilient = VPX_ERROR_RESILIENT_DEFAULT;
+                // https://developers.google.com/media/vp9/bitrate-modes/
+                // Constant Bitrate mode (CBR) is recommended for live streaming with VP9.
+                c.rc_end_usage = vpx_rc_mode::VPX_CBR;
+                if let Some(keyframe_interval) = config.keyframe_interval {
+                    c.kf_min_dist = 0;
+                    c.kf_max_dist = keyframe_interval as _;
+                } else {
+                    c.kf_mode = vpx_kf_mode::VPX_KF_DISABLED; // reduce bandwidth a lot
+                }
+
+                let (q_min, q_max, b) = Self::convert_quality(config.quality);
+                if q_min > 0 && q_min < q_max && q_max < 64 {
+                    c.rc_min_quantizer = q_min;
+                    c.rc_max_quantizer = q_max;
+                } else {
+                    c.rc_min_quantizer = DEFAULT_QP_MIN;
+                    c.rc_max_quantizer = DEFAULT_QP_MAX;
+                }
+                let base_bitrate = base_bitrate(config.width as _, config.height as _);
+                let bitrate = base_bitrate * b / 100;
+                if bitrate > 0 {
+                    c.rc_target_bitrate = bitrate;
+                } else {
+                    c.rc_target_bitrate = base_bitrate;
+                }
+
+                /*
+                The VPX encoder supports two-pass encoding for rate control purposes.
+                In two-pass encoding, the entire encoding process is performed twice.
+                The first pass generates new control parameters for the second pass.
+
+                This approach enables the best PSNR at the same bit rate.
+                */
 
                 let mut ctx = Default::default();
                 call_vpx!(vpx_codec_enc_init_ver(
@@ -68,9 +115,50 @@ impl EncoderApi for VpxEncoder {
                     0,
                     VPX_ENCODER_ABI_VERSION as _
                 ));
-                match config.codec {
-                    VpxVideoCodecId::VP8 => webrtc::vp8::set_control(&mut ctx, &c)?,
-                    VpxVideoCodecId::VP9 => webrtc::vp9::set_control(&mut ctx, &c)?,
+
+                if config.codec == VpxVideoCodecId::VP9 {
+                    // set encoder internal speed settings
+                    // in ffmpeg, it is --speed option
+                    /*
+                    set to 0 or a positive value 1-16, the codec will try to adapt its
+                    complexity depending on the time it spends encoding. Increasing this
+                    number will make the speed go up and the quality go down.
+                    Negative values mean strict enforcement of this
+                    while positive values are adaptive
+                    */
+                    /* https://developers.google.com/media/vp9/live-encoding
+                    Speed 5 to 8 should be used for live / real-time encoding.
+                    Lower numbers (5 or 6) are higher quality but require more CPU power.
+                    Higher numbers (7 or 8) will be lower quality but more manageable for lower latency
+                    use cases and also for lower CPU power devices such as mobile.
+                    */
+                    call_vpx!(vpx_codec_control_(&mut ctx, VP8E_SET_CPUUSED as _, 7,));
+                    // set row level multi-threading
+                    /*
+                    as some people in comments and below have already commented,
+                    more recent versions of libvpx support -row-mt 1 to enable tile row
+                    multi-threading. This can increase the number of tiles by up to 4x in VP9
+                    (since the max number of tile rows is 4, regardless of video height).
+                    To enable this, use -tile-rows N where N is the number of tile rows in
+                    log2 units (so -tile-rows 1 means 2 tile rows and -tile-rows 2 means 4 tile
+                    rows). The total number of active threads will then be equal to
+                    $tile_rows * $tile_columns
+                    */
+                    call_vpx!(vpx_codec_control_(
+                        &mut ctx,
+                        VP9E_SET_ROW_MT as _,
+                        1 as c_int
+                    ));
+
+                    call_vpx!(vpx_codec_control_(
+                        &mut ctx,
+                        VP9E_SET_TILE_COLUMNS as _,
+                        4 as c_int
+                    ));
+                } else if config.codec == VpxVideoCodecId::VP8 {
+                    // https://github.com/webmproject/libvpx/blob/972149cafeb71d6f08df89e91a0130d6a38c4b15/vpx/vp8cx.h#L172
+                    // https://groups.google.com/a/webmproject.org/g/webm-discuss/c/DJhSrmfQ61M
+                    call_vpx!(vpx_codec_control_(&mut ctx, VP8E_SET_CPUUSED as _, 12,));
                 }
 
                 Ok(Self {
@@ -108,11 +196,24 @@ impl EncoderApi for VpxEncoder {
         true
     }
 
-    fn set_bitrate(&mut self, bitrate: u32) -> ResultType<()> {
-        let mut new_enc_cfg = unsafe { *self.ctx.config.enc.to_owned() };
-        new_enc_cfg.rc_target_bitrate = bitrate;
-        call_vpx!(vpx_codec_enc_config_set(&mut self.ctx, &new_enc_cfg));
-        return Ok(());
+    fn set_quality(&mut self, quality: Quality) -> ResultType<()> {
+        let mut c = unsafe { *self.ctx.config.enc.to_owned() };
+        let (q_min, q_max, b) = Self::convert_quality(quality);
+        if q_min > 0 && q_min < q_max && q_max < 64 {
+            c.rc_min_quantizer = q_min;
+            c.rc_max_quantizer = q_max;
+        }
+        let bitrate = base_bitrate(self.width as _, self.height as _) * b / 100;
+        if bitrate > 0 {
+            c.rc_target_bitrate = bitrate;
+        }
+        call_vpx!(vpx_codec_enc_config_set(&mut self.ctx, &c));
+        Ok(())
+    }
+
+    fn bitrate(&self) -> u32 {
+        let c = unsafe { *self.ctx.config.enc.to_owned() };
+        c.rc_target_bitrate
     }
 }
 
@@ -189,6 +290,34 @@ impl VpxEncoder {
             ..Default::default()
         }
     }
+
+    fn convert_quality(quality: Quality) -> (u32, u32, u32) {
+        match quality {
+            Quality::Best => (6, 45, 150),
+            Quality::Balanced => (12, 56, 100 * 2 / 3),
+            Quality::Low => (18, 56, 50),
+            Quality::Custom(b) => {
+                let (q_min, q_max) = Self::calc_q_values(b);
+                (q_min, q_max, b)
+            }
+        }
+    }
+
+    #[inline]
+    fn calc_q_values(b: u32) -> (u32, u32) {
+        let b = std::cmp::min(b, 200);
+        let q_min1: i32 = 36;
+        let q_min2 = 0;
+        let q_max1 = 56;
+        let q_max2 = 37;
+
+        let t = b as f32 / 200.0;
+
+        let q_min: u32 = ((1.0 - t) * q_min1 as f32 + t * q_min2 as f32).round() as u32;
+        let q_max = ((1.0 - t) * q_max1 as f32 + t * q_max2 as f32).round() as u32;
+
+        (q_min, q_max)
+    }
 }
 
 impl Drop for VpxEncoder {
@@ -218,16 +347,17 @@ pub struct VpxEncoderConfig {
     pub width: c_uint,
     /// The height (in pixels).
     pub height: c_uint,
-    /// The target bitrate (in kilobits per second).
-    pub bitrate: c_uint,
+    /// The image quality
+    pub quality: Quality,
     /// The codec
     pub codec: VpxVideoCodecId,
+    /// keyframe interval
+    pub keyframe_interval: Option<usize>,
 }
 
 #[derive(Clone, Copy, Debug)]
 pub struct VpxDecoderConfig {
     pub codec: VpxVideoCodecId,
-    pub num_threads: u32,
 }
 
 pub struct EncodeFrames<'a> {
@@ -274,11 +404,7 @@ impl VpxDecoder {
         };
         let mut ctx = Default::default();
         let cfg = vpx_codec_dec_cfg_t {
-            threads: if config.num_threads == 0 {
-                num_cpus::get() as _
-            } else {
-                config.num_threads
-            },
+            threads: codec_thread_num() as _,
             w: 0,
             h: 0,
         };
@@ -417,371 +543,3 @@ impl Drop for Image {
 }
 
 unsafe impl Send for vpx_codec_ctx_t {}
-
-mod webrtc {
-    use super::*;
-
-    const K_QP_MAX: u32 = 25; // worth adjusting
-    const MODE: VideoCodecMode = VideoCodecMode::KScreensharing;
-    const K_RTP_TICKS_PER_SECOND: i32 = 90000;
-    const NUMBER_OF_TEMPORAL_LAYERS: u32 = 1;
-    const DENOISING_ON: bool = true;
-    const FRAME_DROP_ENABLED: bool = false;
-
-    #[allow(dead_code)]
-    #[derive(Debug, PartialEq, Eq)]
-    enum VideoCodecMode {
-        KRealtimeVideo,
-        KScreensharing,
-    }
-
-    #[allow(dead_code)]
-    #[derive(Debug, PartialEq, Eq)]
-    enum VideoCodecComplexity {
-        KComplexityLow = -1,
-        KComplexityNormal = 0,
-        KComplexityHigh = 1,
-        KComplexityHigher = 2,
-        KComplexityMax = 3,
-    }
-
-    // https://webrtc.googlesource.com/src/+/refs/heads/main/modules/video_coding/codecs/vp9/libvpx_vp9_encoder.cc
-    pub mod vp9 {
-        use super::*;
-        const SVC: bool = false;
-        // https://webrtc.googlesource.com/src/+/refs/heads/main/api/video_codecs/video_encoder.cc#35
-        const KEY_FRAME_INTERVAL: u32 = 3000;
-        const ADAPTIVE_QP_MODE: bool = true;
-
-        pub fn enc_cfg(
-            i: *const vpx_codec_iface_t,
-            cfg: &VpxEncoderConfig,
-        ) -> ResultType<vpx_codec_enc_cfg_t> {
-            let mut c: vpx_codec_enc_cfg_t =
-                unsafe { std::mem::MaybeUninit::zeroed().assume_init() };
-            call_vpx!(vpx_codec_enc_config_default(i, &mut c, 0));
-
-            // kProfile0
-            c.g_bit_depth = vpx_bit_depth::VPX_BITS_8;
-            c.g_profile = 0;
-            c.g_input_bit_depth = 8;
-
-            c.g_w = cfg.width;
-            c.g_h = cfg.height;
-            c.rc_target_bitrate = cfg.bitrate; // in kbit/s
-            c.g_error_resilient = if SVC { VPX_ERROR_RESILIENT_DEFAULT } else { 0 };
-            c.g_timebase.num = 1;
-            c.g_timebase.den = K_RTP_TICKS_PER_SECOND;
-            c.g_lag_in_frames = 0;
-            c.rc_dropframe_thresh = if FRAME_DROP_ENABLED { 30 } else { 0 };
-            c.rc_end_usage = vpx_rc_mode::VPX_CBR;
-            c.g_pass = vpx_enc_pass::VPX_RC_ONE_PASS;
-            c.rc_min_quantizer = if MODE == VideoCodecMode::KScreensharing {
-                8
-            } else {
-                2
-            };
-            c.rc_max_quantizer = K_QP_MAX;
-            c.rc_undershoot_pct = 50;
-            c.rc_overshoot_pct = 50;
-            c.rc_buf_initial_sz = 500;
-            c.rc_buf_optimal_sz = 600;
-            c.rc_buf_sz = 1000;
-            // Key-frame interval is enforced manually by this wrapper.
-            c.kf_mode = vpx_kf_mode::VPX_KF_DISABLED;
-            // TODO(webm:1592): work-around for libvpx issue, as it can still
-            // put some key-frames at will even in VPX_KF_DISABLED kf_mode.
-            c.kf_max_dist = KEY_FRAME_INTERVAL;
-            c.kf_min_dist = c.kf_max_dist;
-            c.rc_resize_allowed = 0;
-            // Determine number of threads based on the image size and #cores.
-            c.g_threads = number_of_threads(c.g_w, c.g_h, num_cpus::get());
-
-            c.temporal_layering_mode =
-                vp9e_temporal_layering_mode::VP9E_TEMPORAL_LAYERING_MODE_NOLAYERING as _;
-            c.ts_number_layers = 1;
-            c.ts_rate_decimator[0] = 1;
-            c.ts_periodicity = 1;
-            c.ts_layer_id[0] = 0;
-
-            Ok(c)
-        }
-
-        pub fn set_control(ctx: *mut vpx_codec_ctx_t, cfg: &vpx_codec_enc_cfg_t) -> ResultType<()> {
-            use vp8e_enc_control_id::*;
-
-            macro_rules! call_ctl {
-                ($ctx:expr, $vpxe:expr, $arg:expr) => {{
-                    call_vpx_allow_err!(vpx_codec_control_($ctx, $vpxe as i32, $arg));
-                }};
-            }
-            call_ctl!(
-                ctx,
-                VP8E_SET_MAX_INTRA_BITRATE_PCT,
-                max_intra_target(cfg.rc_buf_optimal_sz)
-            );
-            call_ctl!(ctx, VP9E_SET_AQ_MODE, if ADAPTIVE_QP_MODE { 3 } else { 0 });
-            call_ctl!(ctx, VP9E_SET_FRAME_PARALLEL_DECODING, 0);
-            #[cfg(not(any(target_arch = "arm", target_arch = "aarch64", target_os = "android")))]
-            call_ctl!(ctx, VP9E_SET_SVC_GF_TEMPORAL_REF, 0);
-            call_ctl!(
-                ctx,
-                VP8E_SET_CPUUSED,
-                get_default_performance_flags(cfg.g_w, cfg.g_h).0
-            );
-            call_ctl!(ctx, VP9E_SET_TILE_COLUMNS, cfg.g_threads >> 1);
-            // Turn on row-based multithreading.
-            call_ctl!(ctx, VP9E_SET_ROW_MT, 1);
-            let denoising = DENOISING_ON
-                && allow_denoising()
-                && get_default_performance_flags(cfg.g_w, cfg.g_h).1;
-            call_ctl!(
-                ctx,
-                VP9E_SET_NOISE_SENSITIVITY,
-                if denoising { 1 } else { 0 }
-            );
-            if MODE == VideoCodecMode::KScreensharing {
-                call_ctl!(ctx, VP9E_SET_TUNE_CONTENT, 1);
-            }
-            // Enable encoder skip of static/low content blocks.
-            call_ctl!(ctx, VP8E_SET_STATIC_THRESHOLD, 1);
-
-            Ok(())
-        }
-
-        // return (base_layer_speed, allow_denoising)
-        fn get_default_performance_flags(width: u32, height: u32) -> (u32, bool) {
-            if cfg!(any(
-                target_arch = "arm",
-                target_arch = "aarch64",
-                target_os = "android"
-            )) {
-                (8, true)
-            } else if width * height < 352 * 288 {
-                (5, true)
-            } else if width * height < 1920 * 1080 {
-                (7, true)
-            } else {
-                (9, false)
-            }
-        }
-
-        fn allow_denoising() -> bool {
-            // Do not enable the denoiser on ARM since optimization is pending.
-            // Denoiser is on by default on other platforms.
-            if cfg!(any(
-                target_arch = "arm",
-                target_arch = "aarch64",
-                target_os = "android"
-            )) {
-                false
-            } else {
-                true
-            }
-        }
-
-        fn number_of_threads(width: u32, height: u32, number_of_cores: usize) -> u32 {
-            // Keep the number of encoder threads equal to the possible number of column
-            // tiles, which is (1, 2, 4, 8). See comments below for VP9E_SET_TILE_COLUMNS.
-            if width * height >= 1280 * 720 && number_of_cores > 4 {
-                return 4;
-            } else if width * height >= 640 * 360 && number_of_cores > 2 {
-                return 2;
-            } else {
-                // Use 2 threads for low res on ARM.
-                #[cfg(any(target_arch = "arm", target_arch = "aarch64", target_os = "android"))]
-                if width * height >= 320 * 180 && number_of_cores > 2 {
-                    return 2;
-                }
-                // 1 thread less than VGA.
-                return 1;
-            }
-        }
-    }
-
-    // https://webrtc.googlesource.com/src/+/refs/heads/main/modules/video_coding/codecs/vp8/libvpx_vp8_encoder.cc
-    pub mod vp8 {
-        use super::*;
-        // https://webrtc.googlesource.com/src/+/refs/heads/main/api/video_codecs/video_encoder.cc#23
-        const DISABLE_KEY_FRAME_INTERVAL: bool = true;
-        const KEY_FRAME_INTERVAL: u32 = 3000;
-        const COMPLEXITY: VideoCodecComplexity = VideoCodecComplexity::KComplexityNormal;
-        const K_TOKEN_PARTITIONS: vp8e_token_partitions =
-            vp8e_token_partitions::VP8_ONE_TOKENPARTITION;
-
-        pub fn enc_cfg(
-            i: *const vpx_codec_iface_t,
-            cfg: &VpxEncoderConfig,
-        ) -> ResultType<vpx_codec_enc_cfg_t> {
-            let mut c: vpx_codec_enc_cfg_t =
-                unsafe { std::mem::MaybeUninit::zeroed().assume_init() };
-            call_vpx!(vpx_codec_enc_config_default(i, &mut c, 0));
-
-            c.g_w = cfg.width;
-            c.g_h = cfg.height;
-            c.g_timebase.num = 1;
-            c.g_timebase.den = K_RTP_TICKS_PER_SECOND;
-            c.g_lag_in_frames = 0;
-            c.g_error_resilient = if NUMBER_OF_TEMPORAL_LAYERS > 1 {
-                VPX_ERROR_RESILIENT_DEFAULT
-            } else {
-                0
-            };
-            c.rc_end_usage = vpx_rc_mode::VPX_CBR;
-            c.g_pass = vpx_enc_pass::VPX_RC_ONE_PASS;
-            c.rc_resize_allowed = 0;
-            c.rc_min_quantizer = if MODE == VideoCodecMode::KScreensharing {
-                12
-            } else {
-                2
-            };
-            c.rc_max_quantizer = K_QP_MAX;
-            c.rc_undershoot_pct = 100;
-            c.rc_overshoot_pct = 15;
-            c.rc_buf_initial_sz = 500;
-            c.rc_buf_optimal_sz = 600;
-            c.rc_buf_sz = 1000;
-            if !DISABLE_KEY_FRAME_INTERVAL && KEY_FRAME_INTERVAL > 0 {
-                c.kf_mode = vpx_kf_mode::VPX_KF_AUTO;
-                c.kf_max_dist = KEY_FRAME_INTERVAL;
-            } else {
-                c.kf_mode = vpx_kf_mode::VPX_KF_DISABLED;
-            }
-            c.g_threads = number_of_threads(c.g_w, c.g_h, num_cpus::get());
-            c.rc_target_bitrate = cfg.bitrate;
-            c.rc_dropframe_thresh = if FRAME_DROP_ENABLED { 30 } else { 0 };
-
-            Ok(c)
-        }
-
-        pub fn set_control(ctx: *mut vpx_codec_ctx_t, cfg: &vpx_codec_enc_cfg_t) -> ResultType<()> {
-            use vp8e_enc_control_id::*;
-
-            macro_rules! call_ctl {
-                ($ctx:expr, $vpxe:expr, $arg:expr) => {{
-                    call_vpx_allow_err!(vpx_codec_control_($ctx, $vpxe as i32, $arg));
-                }};
-            }
-            call_ctl!(
-                ctx,
-                VP8E_SET_STATIC_THRESHOLD,
-                if MODE == VideoCodecMode::KScreensharing {
-                    100
-                } else {
-                    1
-                }
-            );
-            call_ctl!(
-                ctx,
-                VP8E_SET_CPUUSED,
-                get_cpu_speed(cfg.g_w, cfg.g_h, num_cpus::get())
-            );
-
-            call_ctl!(ctx, VP8E_SET_TOKEN_PARTITIONS, K_TOKEN_PARTITIONS);
-            call_ctl!(
-                ctx,
-                VP8E_SET_MAX_INTRA_BITRATE_PCT,
-                max_intra_target(cfg.rc_buf_optimal_sz)
-            );
-            call_ctl!(
-                ctx,
-                VP8E_SET_SCREEN_CONTENT_MODE,
-                if MODE == VideoCodecMode::KScreensharing {
-                    2 // On with more aggressive rate control.
-                } else {
-                    0
-                }
-            );
-
-            Ok(())
-        }
-
-        fn get_cpu_speed_default() -> i32 {
-            match COMPLEXITY {
-                VideoCodecComplexity::KComplexityHigh => -5,
-                VideoCodecComplexity::KComplexityHigher => -4,
-                VideoCodecComplexity::KComplexityMax => -3,
-                _ => -6,
-            }
-        }
-
-        fn get_cpu_speed(width: u32, height: u32, number_of_cores: usize) -> i32 {
-            if cfg!(any(
-                target_arch = "arm",
-                target_arch = "aarch64",
-                target_os = "android"
-            )) {
-                if number_of_cores <= 3 {
-                    -12
-                } else if width * height <= 352 * 288 {
-                    -8
-                } else if width * height <= 640 * 480 {
-                    -10
-                } else {
-                    -12
-                }
-            } else {
-                let cpu_speed_default = get_cpu_speed_default();
-                if width * height < 352 * 288 {
-                    if cpu_speed_default < -4 {
-                        -4
-                    } else {
-                        cpu_speed_default
-                    }
-                } else {
-                    cpu_speed_default
-                }
-            }
-        }
-
-        fn number_of_threads(width: u32, height: u32, cpus: usize) -> u32 {
-            if cfg!(target_os = "android") {
-                if width * height >= 320 * 180 {
-                    if cpus >= 4 {
-                        // 3 threads for CPUs with 4 and more cores since most of times only 4
-                        // cores will be active.
-                        3
-                    } else if cpus == 3 || cpus == 2 {
-                        2
-                    } else {
-                        1
-                    }
-                } else {
-                    1
-                }
-            } else {
-                if width * height >= 1920 * 1080 && cpus > 8 {
-                    8 // 8 threads for 1080p on high perf machines.
-                } else if width * height > 1280 * 960 && cpus >= 6 {
-                    // 3 threads for 1080p.
-                    return 3;
-                } else if width * height > 640 * 480 && cpus >= 3 {
-                    // Default 2 threads for qHD/HD, but allow 3 if core count is high enough,
-                    // as this will allow more margin for high-core/low clock machines or if
-                    // not built with highest optimization.
-                    if cpus >= 6 {
-                        3
-                    } else {
-                        2
-                    }
-                } else {
-                    // 1 thread for VGA or less.
-                    1
-                }
-            }
-        }
-    }
-
-    fn max_intra_target(optimal_buffer_size: u32) -> u32 {
-        const MAX_FRAMERATE: u32 = 60; // TODO
-        let scale_par: f32 = 0.5;
-        let target_pct: u32 =
-            ((optimal_buffer_size as f32) * scale_par * MAX_FRAMERATE as f32 / 10.0) as u32;
-        let min_intra_size: u32 = 300;
-        if target_pct < min_intra_size {
-            min_intra_size
-        } else {
-            target_pct
-        }
-    }
-}
diff --git a/libs/scrap/src/dxgi/mag.rs b/libs/scrap/src/dxgi/mag.rs
index 62e90c08b..ac94472c0 100644
--- a/libs/scrap/src/dxgi/mag.rs
+++ b/libs/scrap/src/dxgi/mag.rs
@@ -129,7 +129,7 @@ impl MagInterface {
         unsafe {
             // load lib
             let lib_file_name = "Magnification.dll";
-            let lib_file_name_c = CString::new(lib_file_name).unwrap();
+            let lib_file_name_c = CString::new(lib_file_name)?;
             s.lib_handle = LoadLibraryExA(
                 lib_file_name_c.as_ptr() as _,
                 NULL,
@@ -189,7 +189,7 @@ impl MagInterface {
     }
 
     unsafe fn load_func(lib_module: HMODULE, func_name: &str) -> Result<FARPROC> {
-        let func_name_c = CString::new(func_name).unwrap();
+        let func_name_c = CString::new(func_name)?;
         let func = GetProcAddress(lib_module, func_name_c.as_ptr() as _);
         if func.is_null() {
             return Err(Error::new(
@@ -442,7 +442,7 @@ impl CapturerMag {
     }
 
     pub(crate) fn exclude(&mut self, cls: &str, name: &str) -> Result<bool> {
-        let name_c = CString::new(name).unwrap();
+        let name_c = CString::new(name)?;
         unsafe {
             let mut hwnd = if cls.len() == 0 {
                 FindWindowExA(NULL as _, NULL as _, NULL as _, name_c.as_ptr())
diff --git a/libs/scrap/src/wayland/pipewire.rs b/libs/scrap/src/wayland/pipewire.rs
index 5583dfef5..f7eb84537 100644
--- a/libs/scrap/src/wayland/pipewire.rs
+++ b/libs/scrap/src/wayland/pipewire.rs
@@ -594,13 +594,14 @@ fn request_screen_cast(
     }
     let fd_res = fd_res.lock().unwrap();
     let streams_res = streams_res.lock().unwrap();
-    if fd_res.is_some() && !streams_res.is_empty() {
-        Ok((conn, fd_res.clone().unwrap(), streams_res.clone()))
-    } else {
-        Err(Box::new(DBusError(
-            "Failed to obtain screen capture.".into(),
-        )))
+    if let Some(fd_res) = fd_res.clone() {
+        if !streams_res.is_empty() {
+            return Ok((conn, fd_res, streams_res.clone()));
+        }
     }
+    Err(Box::new(DBusError(
+        "Failed to obtain screen capture.".into(),
+    )))
 }
 
 pub fn get_capturables(capture_cursor: bool) -> Result<Vec<PipeWireCapturable>, Box<dyn Error>> {
diff --git a/libs/virtual_display/dylib/examples/idd_controller.rs b/libs/virtual_display/dylib/examples/idd_controller.rs
index c9a3fbbab..4d3bdc5e5 100644
--- a/libs/virtual_display/dylib/examples/idd_controller.rs
+++ b/libs/virtual_display/dylib/examples/idd_controller.rs
@@ -30,7 +30,12 @@ fn prompt_input() -> u8 {
 unsafe fn plug_in(index: idd::UINT, edid: idd::UINT) {
     println!("Plug in monitor begin");
     if idd::FALSE == idd::MonitorPlugIn(index, edid, 25) {
-        println!("{}", CStr::from_ptr(idd::GetLastMsg()).to_str().unwrap());
+        println!(
+            "{}",
+            CStr::from_ptr(idd::GetLastMsg())
+                .to_str()
+                .unwrap_or_default()
+        );
     } else {
         println!("Plug in monitor done");
 
@@ -46,7 +51,12 @@ unsafe fn plug_in(index: idd::UINT, edid: idd::UINT) {
             sync: 60 as idd::DWORD,
         });
         if idd::FALSE == idd::MonitorModesUpdate(index, modes.len() as u32, modes.as_mut_ptr()) {
-            println!("{}", CStr::from_ptr(idd::GetLastMsg()).to_str().unwrap());
+            println!(
+                "{}",
+                CStr::from_ptr(idd::GetLastMsg())
+                    .to_str()
+                    .unwrap_or_default()
+            );
         }
     }
 }
@@ -55,7 +65,12 @@ unsafe fn plug_in(index: idd::UINT, edid: idd::UINT) {
 unsafe fn plug_out(index: idd::UINT) {
     println!("Plug out monitor begin");
     if idd::FALSE == idd::MonitorPlugOut(index) {
-        println!("{}", CStr::from_ptr(idd::GetLastMsg()).to_str().unwrap());
+        println!(
+            "{}",
+            CStr::from_ptr(idd::GetLastMsg())
+                .to_str()
+                .unwrap_or_default()
+        );
     } else {
         println!("Plug out monitor done");
     }
@@ -64,7 +79,13 @@ unsafe fn plug_out(index: idd::UINT) {
 fn main() {
     #[cfg(windows)]
     {
-        let abs_path = Path::new(DRIVER_INSTALL_PATH).canonicalize().unwrap();
+        let abs_path = match Path::new(DRIVER_INSTALL_PATH).canonicalize() {
+            Ok(p) => p,
+            Err(e) => {
+                println!("Failed to get absolute path of driver install: {:?}", e);
+                return;
+            }
+        };
 
         unsafe {
             let invalid_device = 0 as idd::HSWDEVICE;
@@ -86,7 +107,12 @@ fn main() {
                         if idd::InstallUpdate(full_inf_path.as_ptr() as _, &mut reboot_required)
                             == idd::FALSE
                         {
-                            println!("{}", CStr::from_ptr(idd::GetLastMsg()).to_str().unwrap());
+                            println!(
+                                "{}",
+                                CStr::from_ptr(idd::GetLastMsg())
+                                    .to_str()
+                                    .unwrap_or_default()
+                            );
                         } else {
                             println!(
                                 "Install or update driver done, reboot is {} required",
@@ -104,7 +130,12 @@ fn main() {
                         if idd::Uninstall(full_inf_path.as_ptr() as _, &mut reboot_required)
                             == idd::FALSE
                         {
-                            println!("{}", CStr::from_ptr(idd::GetLastMsg()).to_str().unwrap());
+                            println!(
+                                "{}",
+                                CStr::from_ptr(idd::GetLastMsg())
+                                    .to_str()
+                                    .unwrap_or_default()
+                            );
                         } else {
                             println!(
                                 "Uninstall driver done, reboot is {} required",
@@ -123,7 +154,12 @@ fn main() {
                             continue;
                         }
                         if idd::FALSE == idd::DeviceCreate(&mut h_sw_device) {
-                            println!("{}", CStr::from_ptr(idd::GetLastMsg()).to_str().unwrap());
+                            println!(
+                                "{}",
+                                CStr::from_ptr(idd::GetLastMsg())
+                                    .to_str()
+                                    .unwrap_or_default()
+                            );
                             idd::DeviceClose(h_sw_device);
                             h_sw_device = invalid_device;
                         } else {
diff --git a/res/128x128@2x.png b/res/128x128@2x.png
index 9bccf65bb..89abf23a6 100644
Binary files a/res/128x128@2x.png and b/res/128x128@2x.png differ
diff --git a/res/PKGBUILD b/res/PKGBUILD
index 7aa50d563..0e83b6891 100644
--- a/res/PKGBUILD
+++ b/res/PKGBUILD
@@ -1,5 +1,5 @@
 pkgname=rustdesk
-pkgver=1.2.0
+pkgver=1.2.3
 pkgrel=0
 epoch=
 pkgdesc=""
@@ -30,5 +30,6 @@ package() {
   install -Dm 644 $HBB/res/rustdesk.service -t "${pkgdir}/usr/share/rustdesk/files"
   install -Dm 644 $HBB/res/rustdesk.desktop -t "${pkgdir}/usr/share/rustdesk/files"
   install -Dm 644 $HBB/res/rustdesk-link.desktop -t "${pkgdir}/usr/share/rustdesk/files"
-  install -Dm 644 $HBB/res/128x128@2x.png "${pkgdir}/usr/share/rustdesk/files/rustdesk.png"
+  install -Dm 644 $HBB/res/128x128@2x.png "${pkgdir}/usr/share/icons/hicolor/256x256/apps/rustdesk.png"
+  install -Dm 644 $HBB/res/scalable.svg "${pkgdir}/usr/share/icons/hicolor/scalable/apps/rustdesk.svg"
 }
diff --git a/res/bump.sh b/res/bump.sh
new file mode 100644
index 000000000..d1142d58c
--- /dev/null
+++ b/res/bump.sh
@@ -0,0 +1,3 @@
+#! /usr/bin/env bash
+sed -i "s/$1/$2/g" res/*spec res/PKGBUILD flutter/pubspec.yaml Cargo.toml .github/workflows/*yml flatpak/*json appimage/*yml
+cargo run # to bump version in cargo lock
diff --git a/res/icon.png b/res/icon.png
index 2575d80e7..00bcf5a5f 100644
Binary files a/res/icon.png and b/res/icon.png differ
diff --git a/res/mac-icon.png b/res/mac-icon.png
index fc39cb2ff..e42ee2c42 100644
Binary files a/res/mac-icon.png and b/res/mac-icon.png differ
diff --git a/res/mac-tray-dark-x2.png b/res/mac-tray-dark-x2.png
index 595b850ae..d1ba59123 100644
Binary files a/res/mac-tray-dark-x2.png and b/res/mac-tray-dark-x2.png differ
diff --git a/res/mac-tray-light-x2.png b/res/mac-tray-light-x2.png
index 2e2711888..11e780cad 100644
Binary files a/res/mac-tray-light-x2.png and b/res/mac-tray-light-x2.png differ
diff --git a/res/osx-dist.sh b/res/osx-dist.sh
new file mode 100755
index 000000000..3e616e558
--- /dev/null
+++ b/res/osx-dist.sh
@@ -0,0 +1,14 @@
+#!/usr/bin/env bash
+
+echo $MACOS_CODESIGN_IDENTITY
+cargo install flutter_rust_bridge_codegen --version 1.75.3 --features uuid
+cd flutter; flutter pub get; cd -
+~/.cargo/bin/flutter_rust_bridge_codegen --rust-input ./src/flutter_ffi.rs --dart-output ./flutter/lib/generated_bridge.dart --c-output ./flutter/macos/Runner/bridge_generated.h
+./build.py --flutter
+rm rustdesk-$VERSION.dmg
+# security find-identity -v
+codesign --force --options runtime -s $MACOS_CODESIGN_IDENTITY --deep --strict ./flutter/build/macos/Build/Products/Release/RustDesk.app -vvv
+create-dmg --icon "RustDesk.app" 200 190 --hide-extension "RustDesk.app" --window-size 800 400 --app-drop-link 600 185 rustdesk-$VERSION.dmg ./flutter/build/macos/Build/Products/Release/RustDesk.app
+codesign --force --options runtime -s $MACOS_CODESIGN_IDENTITY --deep --strict rustdesk-$VERSION.dmg -vvv
+# notarize the rustdesk-${{ env.VERSION }}.dmg
+rcodesign notary-submit --api-key-path ~/.p12/api-key.json  --staple rustdesk-$VERSION.dmg
diff --git a/res/rpm-flutter-suse.spec b/res/rpm-flutter-suse.spec
index 0ac93b716..8c71fa7de 100644
--- a/res/rpm-flutter-suse.spec
+++ b/res/rpm-flutter-suse.spec
@@ -1,5 +1,5 @@
-Name:       rustdesk 
-Version:    1.2.0
+Name:       rustdesk
+Version:    1.2.3
 Release:    0
 Summary:    RPM package
 License:    GPL-3.0
@@ -7,7 +7,7 @@ Requires:   gtk3 libxcb1 xdotool libXfixes3 alsa-utils libXtst6 libappindicator-
 Provides:   libdesktop_drop_plugin.so()(64bit), libdesktop_multi_window_plugin.so()(64bit), libflutter_custom_cursor_plugin.so()(64bit), libflutter_linux_gtk.so()(64bit), libscreen_retriever_plugin.so()(64bit), libtray_manager_plugin.so()(64bit), liburl_launcher_linux_plugin.so()(64bit), libwindow_manager_plugin.so()(64bit), libwindow_size_plugin.so()(64bit), libtexture_rgba_renderer_plugin.so()(64bit)
 
 %description
-The best open-source remote desktop client software, written in Rust. 
+The best open-source remote desktop client software, written in Rust.
 
 %prep
 # we have no source, so nothing here
@@ -24,12 +24,14 @@ mkdir -p "%{buildroot}/usr/bin"
 install -Dm 644 $HBB/res/rustdesk.service -t "%{buildroot}/usr/share/rustdesk/files"
 install -Dm 644 $HBB/res/rustdesk.desktop -t "%{buildroot}/usr/share/rustdesk/files"
 install -Dm 644 $HBB/res/rustdesk-link.desktop -t "%{buildroot}/usr/share/rustdesk/files"
-install -Dm 644 $HBB/res/128x128@2x.png "%{buildroot}/usr/share/rustdesk/files/rustdesk.png"
+install -Dm 644 $HBB/res/128x128@2x.png "%{buildroot}/usr/share/icons/hicolor/256x256/apps/rustdesk.png"
+install -Dm 644 $HBB/res/scalable.svg "%{buildroot}/usr/share/icons/hicolor/scalable/apps/rustdesk.svg"
 
 %files
 /usr/lib/rustdesk/*
 /usr/share/rustdesk/files/rustdesk.service
-/usr/share/rustdesk/files/rustdesk.png
+/usr/share/icons/hicolor/256x256/apps/rustdesk.png
+/usr/share/icons/hicolor/scalable/apps/rustdesk.svg
 /usr/share/rustdesk/files/rustdesk.desktop
 /usr/share/rustdesk/files/rustdesk-link.desktop
 
@@ -53,7 +55,7 @@ esac
 cp /usr/share/rustdesk/files/rustdesk.service /etc/systemd/system/rustdesk.service
 cp /usr/share/rustdesk/files/rustdesk.desktop /usr/share/applications/
 cp /usr/share/rustdesk/files/rustdesk-link.desktop /usr/share/applications/
-ln -s /usr/lib/rustdesk/rustdesk /usr/bin/rustdesk 
+ln -s /usr/lib/rustdesk/rustdesk /usr/bin/rustdesk
 systemctl daemon-reload
 systemctl enable rustdesk
 systemctl start rustdesk
diff --git a/res/rpm-flutter.spec b/res/rpm-flutter.spec
index 937d36d5a..ca63093eb 100644
--- a/res/rpm-flutter.spec
+++ b/res/rpm-flutter.spec
@@ -1,5 +1,5 @@
-Name:       rustdesk 
-Version:    1.2.0
+Name:       rustdesk
+Version:    1.2.3
 Release:    0
 Summary:    RPM package
 License:    GPL-3.0
@@ -7,7 +7,7 @@ Requires:   gtk3 libxcb libxdo libXfixes alsa-lib libappindicator-gtk3 libvdpau
 Provides:   libdesktop_drop_plugin.so()(64bit), libdesktop_multi_window_plugin.so()(64bit), libflutter_custom_cursor_plugin.so()(64bit), libflutter_linux_gtk.so()(64bit), libscreen_retriever_plugin.so()(64bit), libtray_manager_plugin.so()(64bit), liburl_launcher_linux_plugin.so()(64bit), libwindow_manager_plugin.so()(64bit), libwindow_size_plugin.so()(64bit), libtexture_rgba_renderer_plugin.so()(64bit)
 
 %description
-The best open-source remote desktop client software, written in Rust. 
+The best open-source remote desktop client software, written in Rust.
 
 %prep
 # we have no source, so nothing here
@@ -24,12 +24,14 @@ mkdir -p "%{buildroot}/usr/bin"
 install -Dm 644 $HBB/res/rustdesk.service -t "%{buildroot}/usr/share/rustdesk/files"
 install -Dm 644 $HBB/res/rustdesk.desktop -t "%{buildroot}/usr/share/rustdesk/files"
 install -Dm 644 $HBB/res/rustdesk-link.desktop -t "%{buildroot}/usr/share/rustdesk/files"
-install -Dm 644 $HBB/res/128x128@2x.png "%{buildroot}/usr/share/rustdesk/files/rustdesk.png"
+install -Dm 644 $HBB/res/128x128@2x.png "%{buildroot}/usr/share/icons/hicolor/256x256/apps/rustdesk.png"
+install -Dm 644 $HBB/res/scalable.svg "%{buildroot}/usr/share/icons/hicolor/scalable/apps/rustdesk.svg"
 
 %files
 /usr/lib/rustdesk/*
 /usr/share/rustdesk/files/rustdesk.service
-/usr/share/rustdesk/files/rustdesk.png
+/usr/share/icons/hicolor/256x256/apps/rustdesk.png
+/usr/share/icons/hicolor/scalable/apps/rustdesk.svg
 /usr/share/rustdesk/files/rustdesk.desktop
 /usr/share/rustdesk/files/rustdesk-link.desktop
 
@@ -53,7 +55,7 @@ esac
 cp /usr/share/rustdesk/files/rustdesk.service /etc/systemd/system/rustdesk.service
 cp /usr/share/rustdesk/files/rustdesk.desktop /usr/share/applications/
 cp /usr/share/rustdesk/files/rustdesk-link.desktop /usr/share/applications/
-ln -s /usr/lib/rustdesk/rustdesk /usr/bin/rustdesk 
+ln -s /usr/lib/rustdesk/rustdesk /usr/bin/rustdesk
 systemctl daemon-reload
 systemctl enable rustdesk
 systemctl start rustdesk
diff --git a/res/rpm-suse.spec b/res/rpm-suse.spec
index 8309599b9..d84e14812 100644
--- a/res/rpm-suse.spec
+++ b/res/rpm-suse.spec
@@ -1,4 +1,4 @@
-Name:       rustdesk 
+Name:       rustdesk
 Version:    1.1.9
 Release:    0
 Summary:    RPM package
@@ -6,7 +6,7 @@ License:    GPL-3.0
 Requires:   gtk3 libxcb1 xdotool libXfixes3 alsa-utils libXtst6 libayatana-appindicator3-1 libvdpau1 libva2 pam gstreamer-plugins-base gstreamer-plugin-pipewire
 
 %description
-The best open-source remote desktop client software, written in Rust. 
+The best open-source remote desktop client software, written in Rust.
 
 %prep
 # we have no source, so nothing here
@@ -20,10 +20,13 @@ The best open-source remote desktop client software, written in Rust.
 mkdir -p %{buildroot}/usr/bin/
 mkdir -p %{buildroot}/usr/lib/rustdesk/
 mkdir -p %{buildroot}/usr/share/rustdesk/files/
+mkdir -p %{buildroot}/usr/share/icons/hicolor/256x256/apps/
+mkdir -p %{buildroot}/usr/share/icons/hicolor/scalable/apps/
 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/res/rustdesk.service %{buildroot}/usr/share/rustdesk/files/
-install $HBB/res/128x128@2x.png %{buildroot}/usr/share/rustdesk/files/rustdesk.png
+install $HBB/res/128x128@2x.png %{buildroot}/usr/share/icons/hicolor/256x256/apps/rustdesk.png
+install $HBB/res/scalable.svg %{buildroot}/usr/share/icons/hicolor/scalable/apps/rustdesk.svg
 install $HBB/res/rustdesk.desktop %{buildroot}/usr/share/rustdesk/files/
 install $HBB/res/rustdesk-link.desktop %{buildroot}/usr/share/rustdesk/files/
 
@@ -31,7 +34,8 @@ install $HBB/res/rustdesk-link.desktop %{buildroot}/usr/share/rustdesk/files/
 /usr/bin/rustdesk
 /usr/lib/rustdesk/libsciter-gtk.so
 /usr/share/rustdesk/files/rustdesk.service
-/usr/share/rustdesk/files/rustdesk.png
+/usr/share/icons/hicolor/256x256/apps/rustdesk.png
+/usr/share/icons/hicolor/scalable/apps/rustdesk.svg
 /usr/share/rustdesk/files/rustdesk.desktop
 /usr/share/rustdesk/files/rustdesk-link.desktop
 
diff --git a/res/rpm.spec b/res/rpm.spec
index 517da6872..c92ad904a 100644
--- a/res/rpm.spec
+++ b/res/rpm.spec
@@ -1,12 +1,12 @@
-Name:       rustdesk 
-Version:    1.2.0
+Name:       rustdesk
+Version:    1.2.3
 Release:    0
 Summary:    RPM package
 License:    GPL-3.0
 Requires:   gtk3 libxcb libxdo libXfixes alsa-lib libappindicator libvdpau1 libva2 pam gstreamer1-plugins-base
 
 %description
-The best open-source remote desktop client software, written in Rust. 
+The best open-source remote desktop client software, written in Rust.
 
 %prep
 # we have no source, so nothing here
@@ -20,10 +20,13 @@ The best open-source remote desktop client software, written in Rust.
 mkdir -p %{buildroot}/usr/bin/
 mkdir -p %{buildroot}/usr/lib/rustdesk/
 mkdir -p %{buildroot}/usr/share/rustdesk/files/
+mkdir -p %{buildroot}/usr/share/icons/hicolor/256x256/apps/
+mkdir -p %{buildroot}/usr/share/icons/hicolor/scalable/apps/
 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/res/rustdesk.service %{buildroot}/usr/share/rustdesk/files/
-install $HBB/res/128x128@2x.png %{buildroot}/usr/share/rustdesk/files/rustdesk.png
+install $HBB/res/128x128@2x.png %{buildroot}/usr/share/icons/hicolor/256x256/apps/rustdesk.png
+install $HBB/res/scalable.svg %{buildroot}/usr/share/icons/hicolor/scalable/apps/rustdesk.svg
 install $HBB/res/rustdesk.desktop %{buildroot}/usr/share/rustdesk/files/
 install $HBB/res/rustdesk-link.desktop %{buildroot}/usr/share/rustdesk/files/
 
@@ -31,7 +34,8 @@ install $HBB/res/rustdesk-link.desktop %{buildroot}/usr/share/rustdesk/files/
 /usr/bin/rustdesk
 /usr/lib/rustdesk/libsciter-gtk.so
 /usr/share/rustdesk/files/rustdesk.service
-/usr/share/rustdesk/files/rustdesk.png
+/usr/share/icons/hicolor/256x256/apps/rustdesk.png
+/usr/share/icons/hicolor/scalable/apps/rustdesk.svg
 /usr/share/rustdesk/files/rustdesk.desktop
 /usr/share/rustdesk/files/rustdesk-link.desktop
 /usr/share/rustdesk/files/__pycache__/*
diff --git a/res/rustdesk.desktop b/res/rustdesk.desktop
index e6ee6476a..cc72ff449 100644
--- a/res/rustdesk.desktop
+++ b/res/rustdesk.desktop
@@ -3,7 +3,7 @@ Name=RustDesk
 GenericName=Remote Desktop
 Comment=Remote Desktop
 Exec=rustdesk %u
-Icon=/usr/share/rustdesk/files/rustdesk.png
+Icon=rustdesk
 Terminal=false
 Type=Application
 StartupNotify=true
diff --git a/res/scalable.svg b/res/scalable.svg
new file mode 100644
index 000000000..50cab67a3
--- /dev/null
+++ b/res/scalable.svg
@@ -0,0 +1,88 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+   style="isolation:isolate"
+   viewBox="66.993 897.484 32 32.000001"
+   version="1.1"
+   id="svg11"
+   sodipodi:docname="design.svg"
+   xml:space="preserve"
+   inkscape:export-filename="216333102-4d10c195-be66-4fa0-97ca-70a71756b25e.png"
+   inkscape:export-xdpi="3733.2917"
+   inkscape:export-ydpi="3733.2917"
+   inkscape:version="1.2.2 (b0a84865, 2022-12-01)"
+   width="32"
+   height="32"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns:xlink="http://www.w3.org/1999/xlink"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:svg="http://www.w3.org/2000/svg"><defs
+     id="defs15"><linearGradient
+       inkscape:collect="always"
+       xlink:href="#a"
+       id="linearGradient765"
+       gradientUnits="userSpaceOnUse"
+       gradientTransform="matrix(26.000475,0,0,25.999358,17.984526,891.74869)"
+       x1="0.14773831"
+       y1="0.85134232"
+       x2="0.84543866"
+       y2="0.15443686" /></defs><sodipodi:namedview
+     id="namedview13"
+     pagecolor="#ffffff"
+     bordercolor="#000000"
+     borderopacity="0.25"
+     inkscape:showpageshadow="2"
+     inkscape:pageopacity="0.0"
+     inkscape:pagecheckerboard="0"
+     inkscape:deskcolor="#d1d1d1"
+     showgrid="false"
+     inkscape:zoom="1.8436908"
+     inkscape:cx="11.119001"
+     inkscape:cy="90.307984"
+     inkscape:window-width="1440"
+     inkscape:window-height="847"
+     inkscape:window-x="0"
+     inkscape:window-y="25"
+     inkscape:window-maximized="1"
+     inkscape:current-layer="svg11"
+     showguides="true" /><linearGradient
+     id="a"
+     x1="0.14773831"
+     x2="0.84543866"
+     y1="0.85134232"
+     y2="0.15443686"
+     gradientTransform="matrix(26.301,0,0,26.331,90.673535,911.7572)"
+     gradientUnits="userSpaceOnUse"><stop
+       offset="0"
+       stop-color="#004ba6"
+       id="stop4"
+       style="stop-color:#0071ff;stop-opacity:1;" /><stop
+       offset="1"
+       stop-color="#00bfe1"
+       id="stop6"
+       style="stop-color:#00bfe1;stop-opacity:1;" /></linearGradient><g
+     id="g763"
+     inkscape:export-filename="../../Desktop/path9.png"
+     inkscape:export-xdpi="768"
+     inkscape:export-ydpi="768"
+     transform="translate(52.008497,8.73577)"><g
+       id="g761"
+       inkscape:export-filename="./g369.png"
+       inkscape:export-xdpi="3733.2917"
+       inkscape:export-ydpi="3733.2917"><rect
+         style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:1;stroke-dasharray:none;stroke-opacity:1;paint-order:fill markers stroke"
+         id="rect757"
+         width="32"
+         height="32"
+         x="14.984506"
+         y="888.74823"
+         rx="5"
+         ry="5" /><path
+         fill="url(#a)"
+         d="m 40.309479,897.8163 -2.13532,2.12189 c -0.37566,0.3367 -0.557321,0.87878 -0.34675,1.33694 1.422559,2.97602 0.882559,6.52382 -1.45146,8.85602 -2.33502,2.33131 -5.88696,2.8707 -8.866524,1.44881 -0.43892,-0.1965 -0.953964,-0.03 -1.292057,0.31269 l -2.169919,2.16641 c -0.255052,0.2498 -0.3806,0.6023 -0.34007,0.95579 0.04054,0.3545 0.243189,0.6695 0.54767,0.8541 5.1129,3.09451 11.678999,2.30561 15.911089,-1.9116 4.232081,-4.2162 5.03876,-10.77258 1.9554,-15.88729 -0.17696,-0.31103 -0.48935,-0.52234 -0.84425,-0.5717 -0.35489,-0.0503 -0.71276,0.0681 -0.967809,0.31794 z M 21.84293,895.5107 c -4.252844,4.20042 -5.086212,10.75775 -2.019657,15.88535 0.176955,0.312 0.488356,0.5233 0.843254,0.5727 0.354897,0.05 0.712761,-0.0679 0.968802,-0.319 l 2.123457,-2.1091 c 0.384554,-0.3367 0.572151,-0.8847 0.358619,-1.3488 -1.422557,-2.976 -0.883552,-6.52373 1.451458,-8.85593 2.334022,-2.33127 5.886947,-2.87085 8.865525,-1.44997 0.433981,0.19451 0.94211,0.033 1.281191,-0.29971 l 2.181779,-2.1792 c 0.255051,-0.24884 0.380601,-0.60134 0.340071,-0.95581 -0.04149,-0.35349 -0.24319,-0.66847 -0.54767,-0.85411 -5.121801,-3.06787 -11.678015,-2.25523 -15.893292,1.97185 z"
+         id="path759"
+         inkscape:export-filename="../../Desktop/path9.png"
+         inkscape:export-xdpi="2599.3848"
+         inkscape:export-ydpi="2599.3848"
+         style="fill:url(#linearGradient765);stroke-width:0.987991"
+         sodipodi:nodetypes="ccccccccccccccccccccccccccc" /></g></g></svg>
diff --git a/src/cli.rs b/src/cli.rs
index 13e70987b..0f49c5530 100644
--- a/src/cli.rs
+++ b/src/cli.rs
@@ -56,9 +56,15 @@ impl Interface for Session {
             }
             "re-input-password" => {
                 log::error!("{}: {}", title, text);
-                let password = rpassword::prompt_password("Enter password: ").unwrap();
-                let login_data = Data::Login((password, true));
-                self.sender.send(login_data).ok();
+                match rpassword::prompt_password("Enter password: ") {
+                    Ok(password) => {
+                        let login_data = Data::Login((password, true));
+                        self.sender.send(login_data).ok();
+                    }
+                    Err(e) => {
+                        log::error!("reinput password failed, {:?}", e);
+                    }
+                }
             }
             msg if msg.contains("error") => {
                 log::error!("{}: {}: {}", msgtype, title, text);
@@ -85,8 +91,23 @@ impl Interface for Session {
         handle_hash(self.lc.clone(), &pass, hash, self, peer).await;
     }
 
-    async fn handle_login_from_ui(&mut self, os_username: String, os_password: String, password: String, remember: bool, peer: &mut Stream) {
-        handle_login_from_ui(self.lc.clone(), os_username, os_password, password, remember, peer).await;
+    async fn handle_login_from_ui(
+        &mut self,
+        os_username: String,
+        os_password: String,
+        password: String,
+        remember: bool,
+        peer: &mut Stream,
+    ) {
+        handle_login_from_ui(
+            self.lc.clone(),
+            os_username,
+            os_password,
+            password,
+            remember,
+            peer,
+        )
+        .await;
     }
 
     async fn handle_test_delay(&mut self, t: TestDelay, peer: &mut Stream) {
@@ -117,13 +138,14 @@ pub async fn connect_test(id: &str, key: String, token: String) {
                             break;
                         }
                         Ok(Some(Ok(bytes))) => {
-                            let msg_in = Message::parse_from_bytes(&bytes).unwrap();
-                            match msg_in.union {
-                                Some(message::Union::Hash(hash)) => {
-                                    log::info!("Got hash");
-                                    break;
+                            if let Ok(msg_in) = Message::parse_from_bytes(&bytes) {
+                                match msg_in.union {
+                                    Some(message::Union::Hash(hash)) => {
+                                        log::info!("Got hash");
+                                        break;
+                                    }
+                                    _ => {}
                                 }
-                                _ => {}
                             }
                         }
                         _ => {}
diff --git a/src/client.rs b/src/client.rs
index d726b713b..0bfcfb655 100644
--- a/src/client.rs
+++ b/src/client.rs
@@ -24,6 +24,8 @@ use sha2::{Digest, Sha256};
 use uuid::Uuid;
 
 pub use file_trait::FileManager;
+#[cfg(windows)]
+use hbb_common::tokio;
 #[cfg(not(feature = "flutter"))]
 #[cfg(not(any(target_os = "android", target_os = "ios")))]
 use hbb_common::tokio::sync::mpsc::UnboundedSender;
@@ -32,7 +34,8 @@ use hbb_common::{
     anyhow::{anyhow, Context},
     bail,
     config::{
-        Config, PeerConfig, PeerInfoSerde, Resolution, CONNECT_TIMEOUT, READ_TIMEOUT, RELAY_PORT,
+        Config, LocalConfig, PeerConfig, PeerInfoSerde, Resolution, CONNECT_TIMEOUT, READ_TIMEOUT,
+        RELAY_PORT,
     },
     get_version_number, log,
     message_proto::{option_message::BoolOption, *},
@@ -40,6 +43,7 @@ use hbb_common::{
     rand,
     rendezvous_proto::*,
     socket_client,
+    sodiumoxide::base64,
     sodiumoxide::crypto::{box_, secretbox, sign},
     tcp::FramedStream,
     timeout,
@@ -53,13 +57,16 @@ use scrap::{
     ImageFormat, ImageRgb,
 };
 
-use crate::is_keyboard_mode_supported;
+use crate::{
+    common::input::{MOUSE_BUTTON_LEFT, MOUSE_BUTTON_RIGHT, MOUSE_TYPE_DOWN, MOUSE_TYPE_UP},
+    is_keyboard_mode_supported,
+};
 
-#[cfg(not(any(target_os = "android", target_os = "ios")))]
-use crate::{check_clipboard, ClipboardContext, CLIPBOARD_INTERVAL};
 #[cfg(not(feature = "flutter"))]
 #[cfg(not(any(target_os = "android", target_os = "ios")))]
 use crate::ui_session_interface::SessionPermissionConfig;
+#[cfg(not(any(target_os = "android", target_os = "ios")))]
+use crate::{check_clipboard, ClipboardContext, CLIPBOARD_INTERVAL};
 
 pub use super::lang::*;
 
@@ -88,6 +95,7 @@ pub const LOGIN_MSG_PASSWORD_EMPTY: &str = "Empty Password";
 pub const LOGIN_MSG_PASSWORD_WRONG: &str = "Wrong Password";
 pub const LOGIN_MSG_NO_PASSWORD_ACCESS: &str = "No Password Access";
 pub const LOGIN_MSG_OFFLINE: &str = "Offline";
+pub const LOGIN_SCREEN_WAYLAND: &str = "Wayland login screen is not supported";
 #[cfg(target_os = "linux")]
 pub const SCRAP_UBUNTU_HIGHER_REQUIRED: &str = "Wayland requires Ubuntu 21.04 or higher version.";
 #[cfg(target_os = "linux")]
@@ -213,6 +221,8 @@ impl Client {
         conn_type: ConnType,
         interface: impl Interface,
     ) -> ResultType<(Stream, bool, Option<Vec<u8>>)> {
+        interface.update_direct(None);
+        interface.update_received(false);
         match Self::_start(peer, key, token, conn_type, interface).await {
             Err(err) => {
                 let err_str = err.to_string();
@@ -313,19 +323,20 @@ impl Client {
                             if !ph.other_failure.is_empty() {
                                 bail!(ph.other_failure);
                             }
-                            match ph.failure.enum_value_or_default() {
-                                punch_hole_response::Failure::ID_NOT_EXIST => {
+                            match ph.failure.enum_value() {
+                                Ok(punch_hole_response::Failure::ID_NOT_EXIST) => {
                                     bail!("ID does not exist");
                                 }
-                                punch_hole_response::Failure::OFFLINE => {
+                                Ok(punch_hole_response::Failure::OFFLINE) => {
                                     bail!("Remote desktop is offline");
                                 }
-                                punch_hole_response::Failure::LICENSE_MISMATCH => {
+                                Ok(punch_hole_response::Failure::LICENSE_MISMATCH) => {
                                     bail!("Key mismatch");
                                 }
-                                punch_hole_response::Failure::LICENSE_OVERUSE => {
+                                Ok(punch_hole_response::Failure::LICENSE_OVERUSE) => {
                                     bail!("Key overuse");
                                 }
+                                _ => bail!("other punch hole failure"),
                             }
                         } else {
                             peer_nat_type = ph.nat_type();
@@ -353,15 +364,8 @@ impl Client {
                             my_addr.is_ipv4(),
                         )
                         .await?;
-                        let pk = Self::secure_connection(
-                            peer,
-                            signed_id_pk,
-                            key,
-                            &mut conn,
-                            false,
-                            interface,
-                        )
-                        .await?;
+                        let pk =
+                            Self::secure_connection(peer, signed_id_pk, key, &mut conn).await?;
                         return Ok((conn, false, pk));
                     }
                     _ => {
@@ -459,6 +463,7 @@ impl Client {
         let mut conn =
             socket_client::connect_tcp_local(peer, Some(local_addr), connect_timeout).await;
         let mut direct = !conn.is_err();
+        interface.update_direct(Some(direct));
         if interface.is_force_relay() || conn.is_err() {
             if !relay_server.is_empty() {
                 conn = Self::request_relay(
@@ -471,11 +476,9 @@ impl Client {
                     conn_type,
                 )
                 .await;
-                if conn.is_err() {
-                    bail!(
-                        "Failed to connect via relay server: {}",
-                        conn.err().unwrap()
-                    );
+                interface.update_direct(Some(false));
+                if let Err(e) = conn {
+                    bail!("Failed to connect via relay server: {}", e);
                 }
                 direct = false;
             } else {
@@ -490,8 +493,7 @@ impl Client {
         }
         let mut conn = conn?;
         log::info!("{:?} used to establish connection", start.elapsed());
-        let pk = Self::secure_connection(peer_id, signed_id_pk, key, &mut conn, direct, interface)
-            .await?;
+        let pk = Self::secure_connection(peer_id, signed_id_pk, key, &mut conn).await?;
         Ok((conn, direct, pk))
     }
 
@@ -501,8 +503,6 @@ impl Client {
         signed_id_pk: Vec<u8>,
         key: &str,
         conn: &mut Stream,
-        direct: bool,
-        interface: impl Interface,
     ) -> ResultType<Option<Vec<u8>>> {
         let rs_pk = get_rs_pk(if key.is_empty() {
             hbb_common::config::RS_PUB_KEY
@@ -511,11 +511,13 @@ impl Client {
         });
         let mut sign_pk = None;
         let mut option_pk = None;
-        if !signed_id_pk.is_empty() && rs_pk.is_some() {
-            if let Ok((id, pk)) = decode_id_pk(&signed_id_pk, &rs_pk.unwrap()) {
-                if id == peer_id {
-                    sign_pk = Some(sign::PublicKey(pk));
-                    option_pk = Some(pk.to_vec());
+        if !signed_id_pk.is_empty() {
+            if let Some(rs_pk) = rs_pk {
+                if let Ok((id, pk)) = decode_id_pk(&signed_id_pk, &rs_pk) {
+                    if id == peer_id {
+                        sign_pk = Some(sign::PublicKey(pk));
+                        option_pk = Some(pk.to_vec());
+                    }
                 }
             }
             if sign_pk.is_none() {
@@ -532,13 +534,7 @@ impl Client {
         };
         match timeout(READ_TIMEOUT, conn.next()).await? {
             Some(res) => {
-                let bytes = match res {
-                    Ok(bytes) => bytes,
-                    Err(err) => {
-                        interface.set_force_relay(direct, false);
-                        bail!("{}", err);
-                    }
-                };
+                let bytes = res?;
                 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) {
@@ -1073,17 +1069,15 @@ pub struct LoginConfigHandler {
     config: PeerConfig,
     pub port_forward: (String, i32),
     pub version: i64,
-    pub conn_id: i32,
     features: Option<Features>,
-    session_id: u64,
+    pub session_id: u64, // used for local <-> server communication
     pub supported_encoding: SupportedEncoding,
     pub restarting_remote_device: bool,
     pub force_relay: bool,
     pub direct: Option<bool>,
     pub received: bool,
     switch_uuid: Option<String>,
-    pub success_time: Option<hbb_common::tokio::time::Instant>,
-    pub direct_error_counter: usize,
+    pub save_ab_password_to_recent: bool, // true: connected with ab password
 }
 
 impl Deref for LoginConfigHandler {
@@ -1123,15 +1117,18 @@ impl LoginConfigHandler {
         let config = self.load_config();
         self.remember = !config.password.is_empty();
         self.config = config;
-        self.session_id = rand::random();
+        let mut sid = rand::random();
+        if sid == 0 {
+            // you won the lottery
+            sid = 1;
+        }
+        self.session_id = sid;
         self.supported_encoding = Default::default();
         self.restarting_remote_device = false;
         self.force_relay = !self.get_option("force-always-relay").is_empty() || force_relay;
         self.direct = None;
         self.received = false;
         self.switch_uuid = switch_uuid;
-        self.success_time = None;
-        self.direct_error_counter = 0;
     }
 
     /// Check if the client should auto login.
@@ -1217,7 +1214,11 @@ impl LoginConfigHandler {
     /// * `v` - value of option
     pub fn save_ui_flutter(&mut self, k: String, v: String) {
         let mut config = self.load_config();
-        config.ui_flutter.insert(k, v);
+        if v.is_empty() {
+            config.ui_flutter.remove(&k);
+        } else {
+            config.ui_flutter.insert(k, v);
+        }
         self.save_config(config);
     }
 
@@ -1650,11 +1651,26 @@ impl LoginConfigHandler {
                 log::debug!("remember password of {}", self.id);
             }
         } else {
-            if !password0.is_empty() {
+            if self.save_ab_password_to_recent {
+                config.password = password;
+                log::debug!("save ab password of {} to recent", self.id);
+            } else if !password0.is_empty() {
                 config.password = Default::default();
                 log::debug!("remove password of {}", self.id);
             }
         }
+        #[cfg(feature = "flutter")]
+        {
+            // sync ab password with PeerConfig password
+            let password = base64::encode(config.password.clone(), base64::Variant::Original);
+            let evt: HashMap<&str, String> = HashMap::from([
+                ("name", "sync_peer_password_to_ab".to_string()),
+                ("id", self.id.clone()),
+                ("password", password),
+            ]);
+            let evt = serde_json::ser::to_string(&evt).unwrap_or("".to_owned());
+            crate::flutter::push_global_event(crate::flutter::APP_TYPE_MAIN, evt);
+        }
         if config.keyboard_mode.is_empty() {
             if is_keyboard_mode_supported(&KeyboardMode::Map, get_version_number(&pi.version)) {
                 config.keyboard_mode = KeyboardMode::Map.to_string();
@@ -1669,7 +1685,6 @@ impl LoginConfigHandler {
                 config.keyboard_mode = KeyboardMode::Legacy.to_string();
             }
         }
-        self.conn_id = pi.conn_id;
         // no matter if change, for update file time
         self.save_config(config);
         self.supported_encoding = pi.encoding.clone().unwrap_or_default();
@@ -1759,18 +1774,6 @@ impl LoginConfigHandler {
         msg_out.set_misc(misc);
         msg_out
     }
-
-    pub fn set_force_relay(&mut self, direct: bool, received: bool) {
-        self.force_relay = false;
-        if direct && !received {
-            let errno = errno::errno().0;
-            // TODO: check mac and ios
-            if cfg!(windows) && errno == 10054 || !cfg!(windows) && errno == 104 {
-                self.force_relay = true;
-                self.set_option("force-always-relay".to_owned(), "Y".to_owned());
-            }
-        }
-    }
 }
 
 /// Media data.
@@ -1813,6 +1816,8 @@ where
     let mut skip_beginning = 0;
 
     std::thread::spawn(move || {
+        #[cfg(windows)]
+        sync_cpu_usage();
         let mut video_handler = VideoHandler::new();
         loop {
             if let Ok(data) = video_receiver.recv() {
@@ -1845,7 +1850,7 @@ where
                                 );
                             }
                             // Clear to get real-time fps
-                            if count > 300 {
+                            if count > 150 {
                                 count = 0;
                                 duration = Duration::ZERO;
                             }
@@ -1896,6 +1901,39 @@ pub fn start_audio_thread() -> MediaSender {
     audio_sender
 }
 
+#[cfg(windows)]
+fn sync_cpu_usage() {
+    use std::sync::Once;
+    static ONCE: Once = Once::new();
+    ONCE.call_once(|| {
+        let t = std::thread::spawn(do_sync_cpu_usage);
+        t.join().ok();
+    });
+}
+
+#[cfg(windows)]
+#[tokio::main(flavor = "current_thread")]
+async fn do_sync_cpu_usage() {
+    use crate::ipc::{connect, Data};
+    let start = std::time::Instant::now();
+    match connect(50, "").await {
+        Ok(mut conn) => {
+            if conn.send(&&Data::SyncWinCpuUsage(None)).await.is_ok() {
+                if let Ok(Some(data)) = conn.next_timeout(50).await {
+                    match data {
+                        Data::SyncWinCpuUsage(cpu_usage) => {
+                            hbb_common::platform::windows::sync_cpu_usage(cpu_usage);
+                        }
+                        _ => {}
+                    }
+                }
+            }
+        }
+        _ => {}
+    }
+    log::info!("{:?} used to sync cpu usage", start.elapsed());
+}
+
 /// Handle latency test.
 ///
 /// # Arguments
@@ -1991,18 +2029,56 @@ pub fn send_mouse(
     interface.send(Data::Message(msg_out));
 }
 
+#[inline]
+pub fn send_pointer_device_event(
+    mut evt: PointerDeviceEvent,
+    alt: bool,
+    ctrl: bool,
+    shift: bool,
+    command: bool,
+    interface: &impl Interface,
+) {
+    let mut msg_out = Message::new();
+    if alt {
+        evt.modifiers.push(ControlKey::Alt.into());
+    }
+    if shift {
+        evt.modifiers.push(ControlKey::Shift.into());
+    }
+    if ctrl {
+        evt.modifiers.push(ControlKey::Control.into());
+    }
+    if command {
+        evt.modifiers.push(ControlKey::Meta.into());
+    }
+    msg_out.set_pointer_device_event(evt);
+    interface.send(Data::Message(msg_out));
+}
+
 /// Activate OS by sending mouse movement.
 ///
 /// # Arguments
 ///
 /// * `interface` - The interface for sending data.
-fn activate_os(interface: &impl Interface) {
+/// * `send_left_click` - Whether to send a click event.
+fn activate_os(interface: &impl Interface, send_left_click: bool) {
+    let left_down = MOUSE_BUTTON_LEFT << 3 | MOUSE_TYPE_DOWN;
+    let left_up = MOUSE_BUTTON_LEFT << 3 | MOUSE_TYPE_UP;
+    let right_down = MOUSE_BUTTON_RIGHT << 3 | MOUSE_TYPE_DOWN;
+    let right_up = MOUSE_BUTTON_RIGHT << 3 | MOUSE_TYPE_UP;
+    send_mouse(left_up, 0, 0, false, false, false, false, interface);
+    std::thread::sleep(Duration::from_millis(50));
     send_mouse(0, 0, 0, false, false, false, false, interface);
     std::thread::sleep(Duration::from_millis(50));
     send_mouse(0, 3, 3, false, false, false, false, interface);
+    let (click_down, click_up) = if send_left_click {
+        (left_down, left_up)
+    } else {
+        (right_down, right_up)
+    };
     std::thread::sleep(Duration::from_millis(50));
-    send_mouse(1 | 1 << 3, 0, 0, false, false, false, false, interface);
-    send_mouse(2 | 1 << 3, 0, 0, false, false, false, false, interface);
+    send_mouse(click_down, 0, 0, false, false, false, false, interface);
+    send_mouse(click_up, 0, 0, false, false, false, false, interface);
     /*
     let mut key_event = KeyEvent::new();
     // do not use Esc, which has problem with Linux
@@ -2035,10 +2111,15 @@ pub fn input_os_password(p: String, activate: bool, interface: impl Interface) {
 /// * `activate` - Whether to activate OS.
 /// * `interface` - The interface for sending data.
 fn _input_os_password(p: String, activate: bool, interface: impl Interface) {
+    let input_password = !p.is_empty();
     if activate {
-        activate_os(&interface);
+        // Click event is used to bring up the password input box.
+        activate_os(&interface, input_password);
         std::thread::sleep(Duration::from_millis(1200));
     }
+    if !input_password {
+        return;
+    }
     let mut key_event = KeyEvent::new();
     key_event.press = true;
     let mut msg_out = Message::new();
@@ -2062,7 +2143,13 @@ struct LoginErrorMsgBox {
 lazy_static::lazy_static! {
     static ref LOGIN_ERROR_MAP: Arc<HashMap<&'static str, LoginErrorMsgBox>> = {
         use hbb_common::config::LINK_HEADLESS_LINUX_SUPPORT;
-        let map = HashMap::from([(LOGIN_MSG_DESKTOP_SESSION_NOT_READY, LoginErrorMsgBox{
+        let map = HashMap::from([(LOGIN_SCREEN_WAYLAND, LoginErrorMsgBox{
+            msgtype: "error",
+            title: "Login Error",
+            text: "Login screen using Wayland is not supported",
+            link: "https://rustdesk.com/docs/en/manual/linux/#login-screen",
+            try_again: true,
+        }), (LOGIN_MSG_DESKTOP_SESSION_NOT_READY, LoginErrorMsgBox{
             msgtype: "session-login",
             title: "",
             text: "",
@@ -2122,6 +2209,7 @@ pub fn handle_login_error(
     err: &str,
     interface: &impl Interface,
 ) -> bool {
+    lc.write().unwrap().save_ab_password_to_recent = false;
     if err == LOGIN_MSG_PASSWORD_EMPTY {
         lc.write().unwrap().password = Default::default();
         interface.msgbox("input-password", "Password Required", "", "");
@@ -2190,6 +2278,26 @@ pub async fn handle_hash(
     if password.is_empty() {
         password = lc.read().unwrap().config.password.clone();
     }
+    if password.is_empty() {
+        let access_token = LocalConfig::get_option("access_token");
+        let ab = hbb_common::config::Ab::load();
+        if !access_token.is_empty() && access_token == ab.access_token {
+            let id = lc.read().unwrap().id.clone();
+            if let Some(p) = ab
+                .peers
+                .iter()
+                .find_map(|p| if p.id == id { Some(p) } else { None })
+            {
+                if let Ok(hash) = base64::decode(p.hash.clone(), base64::Variant::Original) {
+                    if !hash.is_empty() {
+                        password = hash;
+                        lc.write().unwrap().save_ab_password_to_recent = true;
+                    }
+                }
+            }
+        }
+    }
+    lc.write().unwrap().password = password.clone();
     let password = if password.is_empty() {
         // login without password, the remote side can click accept
         interface.msgbox("input-password", "Password Required", "", "");
@@ -2261,9 +2369,9 @@ pub async fn handle_login_from_ui(
         hasher.update(&lc.read().unwrap().hash.salt);
         let res = hasher.finalize();
         lc.write().unwrap().remember = remember;
-        lc.write().unwrap().password = res[..].into();
         res[..].into()
     };
+    lc.write().unwrap().password = hash_password.clone();
     let mut hasher2 = Sha256::new();
     hasher2.update(&hash_password[..]);
     hasher2.update(&lc.read().unwrap().hash.challenge);
@@ -2315,16 +2423,48 @@ pub trait Interface: Send + Clone + 'static + Sized {
     async fn handle_test_delay(&mut self, t: TestDelay, peer: &mut Stream);
 
     fn get_login_config_handler(&self) -> Arc<RwLock<LoginConfigHandler>>;
-    fn set_force_relay(&self, direct: bool, received: bool) {
-        self.get_login_config_handler()
-            .write()
-            .unwrap()
-            .set_force_relay(direct, received);
-    }
+
     fn is_force_relay(&self) -> bool {
         self.get_login_config_handler().read().unwrap().force_relay
     }
     fn swap_modifier_mouse(&self, _msg: &mut hbb_common::protos::message::MouseEvent) {}
+
+    fn update_direct(&self, direct: Option<bool>) {
+        self.get_login_config_handler().write().unwrap().direct = direct;
+    }
+
+    fn update_received(&self, received: bool) {
+        self.get_login_config_handler().write().unwrap().received = received;
+    }
+
+    fn on_establish_connection_error(&self, err: String) {
+        log::error!("Connection closed: {}", err);
+        let title = "Connection Error";
+        let text = err.to_string();
+        let lc = self.get_login_config_handler();
+        let direct = lc.read().unwrap().direct;
+        let received = lc.read().unwrap().received;
+        let relay_condition = direct == Some(true) && !received;
+
+        // force relay
+        let errno = errno::errno().0;
+        if relay_condition
+            && (cfg!(windows) && (errno == 10054 || err.contains("10054"))
+                || !cfg!(windows) && (errno == 104 || err.contains("104")))
+        {
+            lc.write().unwrap().force_relay = true;
+            lc.write()
+                .unwrap()
+                .set_option("force-always-relay".to_owned(), "Y".to_owned());
+        }
+
+        // relay-hint
+        if cfg!(feature = "flutter") && relay_condition {
+            self.msgbox("relay-hint", title, &text, "");
+        } else {
+            self.msgbox("error", title, &text, "");
+        }
+    }
 }
 
 /// Data used by the client interface.
diff --git a/src/client/io_loop.rs b/src/client/io_loop.rs
index 46c0a319d..107302c32 100644
--- a/src/client/io_loop.rs
+++ b/src/client/io_loop.rs
@@ -56,6 +56,7 @@ pub struct Remote<T: InvokeUiSession> {
     remove_jobs: HashMap<i32, RemoveJob>,
     timer: Interval,
     last_update_jobs_status: (Instant, HashMap<i32, u64>),
+    is_connected: bool,
     first_frame: bool,
     #[cfg(any(target_os = "windows", target_os = "linux"))]
     client_conn_id: i32, // used for file clipboard
@@ -90,6 +91,7 @@ impl<T: InvokeUiSession> Remote<T> {
             remove_jobs: Default::default(),
             timer: time::interval(SEC30),
             last_update_jobs_status: (Instant::now(), Default::default()),
+            is_connected: false,
             first_frame: false,
             #[cfg(any(target_os = "windows", target_os = "linux"))]
             client_conn_id: 0,
@@ -124,7 +126,7 @@ impl<T: InvokeUiSession> Remote<T> {
         {
             Ok((mut peer, direct, pk)) => {
                 self.handler.set_connection_type(peer.is_secured(), direct); // flutter -> connection_ready
-                self.handler.set_connection_info(direct, false);
+                self.handler.update_direct(Some(direct));
                 if conn_type == ConnType::DEFAULT_CONN {
                     self.handler
                         .set_fingerprint(crate::common::pk_to_fingerprint(pk.unwrap_or_default()));
@@ -160,24 +162,14 @@ impl<T: InvokeUiSession> Remote<T> {
                             if let Some(res) = res {
                                 match res {
                                     Err(err) => {
-                                        log::error!("Connection closed: {}", err);
-                                        self.handler.set_force_relay(direct, received);
-                                        let msgtype = "error";
-                                        let title = "Connection Error";
-                                        let text = err.to_string();
-                                        let show_relay_hint = self.handler.show_relay_hint(last_recv_time, msgtype, title, &text);
-                                        if show_relay_hint{
-                                            self.handler.msgbox("relay-hint", title, &text, "");
-                                        } else {
-                                            self.handler.msgbox(msgtype, title, &text, "");
-                                        }
+                                        self.handler.on_establish_connection_error(err.to_string());
                                         break;
                                     }
                                     Ok(ref bytes) => {
                                         last_recv_time = Instant::now();
                                         if !received {
                                             received = true;
-                                            self.handler.set_connection_info(direct, true);
+                                            self.handler.update_received(true);
                                         }
                                         self.data_count.fetch_add(bytes.len(), Ordering::Relaxed);
                                         if !self.handle_msg_from_peer(bytes, &mut peer).await {
@@ -205,28 +197,7 @@ impl<T: InvokeUiSession> Remote<T> {
                         }
                         _msg = rx_clip_client.recv() => {
                             #[cfg(any(target_os="windows", target_os="linux"))]
-                            match _msg {
-                                Some(clip) => match clip {
-                                    clipboard::ClipboardFile::NotifyCallback{r#type, title, text} => {
-                                        self.handler.msgbox(&r#type, &title, &text, "");
-                                    }
-                                    _ => {
-                                        let is_stopping_allowed = clip.is_stopping_allowed();
-                                        let server_file_transfer_enabled = *self.handler.server_file_transfer_enabled.read().unwrap();
-                                        let file_transfer_enabled = self.handler.lc.read().unwrap().enable_file_transfer.v;
-                                        let stop = is_stopping_allowed && !(server_file_transfer_enabled && file_transfer_enabled);
-                                        log::debug!("Process clipboard message from system, stop: {}, is_stopping_allowed: {}, server_file_transfer_enabled: {}, file_transfer_enabled: {}", stop, is_stopping_allowed, server_file_transfer_enabled, file_transfer_enabled);
-                                        if stop {
-                                            ContextSend::set_is_stopped();
-                                        } else {
-                                            allow_err!(peer.send(&crate::clipboard_file::clip_2_msg(clip)).await);
-                                        }
-                                    }
-                                }
-                                None => {
-                                    // unreachable!()
-                                }
-                            }
+                           self.handle_local_clipboard_msg(&mut peer, _msg).await;
                         }
                         _ = self.timer.tick() => {
                             if last_recv_time.elapsed() >= SEC30 {
@@ -244,7 +215,7 @@ impl<T: InvokeUiSession> Remote<T> {
                             }
                         }
                         _ = status_timer.tick() => {
-                            self.fps_control();
+                            self.fps_control(direct);
                             let elapsed = fps_instant.elapsed().as_millis();
                             if elapsed < 1000 {
                                 continue;
@@ -271,8 +242,7 @@ impl<T: InvokeUiSession> Remote<T> {
                 }
             }
             Err(err) => {
-                self.handler
-                    .msgbox("error", "Connection Error", &err.to_string(), "");
+                self.handler.on_establish_connection_error(err.to_string());
             }
         }
         #[cfg(not(any(target_os = "android", target_os = "ios")))]
@@ -288,6 +258,44 @@ impl<T: InvokeUiSession> Remote<T> {
         }
     }
 
+    #[cfg(any(target_os = "windows", target_os = "linux"))]
+    async fn handle_local_clipboard_msg(
+        &self,
+        peer: &mut crate::client::FramedStream,
+        msg: Option<clipboard::ClipboardFile>,
+    ) {
+        match msg {
+            Some(clip) => match clip {
+                clipboard::ClipboardFile::NotifyCallback {
+                    r#type,
+                    title,
+                    text,
+                } => {
+                    self.handler.msgbox(&r#type, &title, &text, "");
+                }
+                _ => {
+                    let is_stopping_allowed = clip.is_stopping_allowed();
+                    let server_file_transfer_enabled =
+                        *self.handler.server_file_transfer_enabled.read().unwrap();
+                    let file_transfer_enabled =
+                        self.handler.lc.read().unwrap().enable_file_transfer.v;
+                    let stop = is_stopping_allowed
+                        && (!self.is_connected
+                            || !(server_file_transfer_enabled && file_transfer_enabled));
+                    log::debug!("Process clipboard message from system, stop: {}, is_stopping_allowed: {}, server_file_transfer_enabled: {}, file_transfer_enabled: {}", stop, is_stopping_allowed, server_file_transfer_enabled, file_transfer_enabled);
+                    if stop {
+                        ContextSend::set_is_stopped();
+                    } else {
+                        allow_err!(peer.send(&crate::clipboard_file::clip_2_msg(clip)).await);
+                    }
+                }
+            },
+            None => {
+                // unreachable!()
+            }
+        }
+    }
+
     fn handle_job_status(&mut self, id: i32, file_num: i32, err: Option<String>) {
         if let Some(job) = self.remove_jobs.get_mut(&id) {
             if job.no_confirm {
@@ -475,9 +483,13 @@ impl<T: InvokeUiSession> Remote<T> {
                                 // peer is not windows, need transform \ to /
                                 fs::transform_windows_path(&mut files);
                             }
+                            let total_size = job.total_size();
                             self.read_jobs.push(job);
                             self.timer = time::interval(MILLI1);
-                            allow_err!(peer.send(&fs::new_receive(id, to, file_num, files)).await);
+                            allow_err!(
+                                peer.send(&fs::new_receive(id, to, file_num, files, total_size))
+                                    .await
+                            );
                         }
                     }
                 }
@@ -560,7 +572,8 @@ impl<T: InvokeUiSession> Remote<T> {
                                 id,
                                 job.path.to_string_lossy().to_string(),
                                 job.file_num,
-                                job.files.clone()
+                                job.files.clone(),
+                                job.total_size(),
                             ))
                             .await
                         );
@@ -843,8 +856,10 @@ impl<T: InvokeUiSession> Remote<T> {
             transfer_metas.write_jobs.push(json_str);
         }
         log::info!("meta: {:?}", transfer_metas);
-        config.transfer = transfer_metas;
-        self.handler.save_config(config);
+        if config.transfer != transfer_metas {
+            config.transfer = transfer_metas;
+            self.handler.save_config(config);
+        }
         true
     }
 
@@ -875,38 +890,77 @@ impl<T: InvokeUiSession> Remote<T> {
         }
     }
     #[inline]
-    fn fps_control(&mut self) {
+    fn fps_control(&mut self, direct: bool) {
         let len = self.video_queue.len();
         let ctl = &mut self.fps_control;
         // Current full speed decoding fps
         let decode_fps = self.decode_fps.load(std::sync::atomic::Ordering::Relaxed);
-        // 500ms
-        let debounce = if decode_fps > 10 { decode_fps / 2 } else { 5 };
-        if len < debounce || decode_fps == 0 {
+        if decode_fps == 0 {
             return;
         }
-        // First setting , or the length of the queue still increases after setting, or exceed the size of the last setting again
-        if ctl.set_times < 10 // enough
-            && (ctl.set_times == 0
-                || (len > ctl.last_queue_size && ctl.last_set_instant.elapsed().as_secs() > 30))
+        let limited_fps = if direct {
+            decode_fps * 9 / 10 // 30 got 27
+        } else {
+            decode_fps * 4 / 5 // 30 got 24
+        };
+        // send full speed fps
+        let version = self.handler.lc.read().unwrap().version;
+        let max_encode_speed = 144 * 10 / 9;
+        if version >= hbb_common::get_version_number("1.2.1")
+            && (ctl.last_full_speed_fps.is_none() // First time
+                || ((ctl.last_full_speed_fps.unwrap_or_default() - decode_fps as i32).abs() >= 5 // diff 5
+                    && !(decode_fps > max_encode_speed // already exceed max encoding speed
+                        && ctl.last_full_speed_fps.unwrap_or_default() > max_encode_speed as i32)))
         {
-            // 80% fps to ensure decoding is faster than encoding
-            let mut custom_fps = decode_fps as i32 * 4 / 5;
+            let mut misc = Misc::new();
+            misc.set_full_speed_fps(decode_fps as _);
+            let mut msg = Message::new();
+            msg.set_misc(misc);
+            self.sender.send(Data::Message(msg)).ok();
+            ctl.last_full_speed_fps = Some(decode_fps as _);
+        }
+        // decrease judgement
+        let debounce = if decode_fps > 10 { decode_fps / 2 } else { 5 }; // 500ms
+        let should_decrease = len >= debounce // exceed debounce
+            && len > ctl.last_queue_size + 5 // still caching
+            && !ctl.last_custom_fps.unwrap_or(i32::MAX) < limited_fps as i32; // NOT already set a smaller one
+
+        // increase judgement
+        if len <= 1 {
+            ctl.idle_counter += 1;
+        } else {
+            ctl.idle_counter = 0;
+        }
+        let mut should_increase = false;
+        if let Some(last_custom_fps) = ctl.last_custom_fps {
+            // ever set
+            if last_custom_fps + 5 < limited_fps as i32 && ctl.idle_counter > 3 {
+                // limited_fps is 5 larger than last set, and idle time is more than 3 seconds
+                should_increase = true;
+            }
+        }
+        if should_decrease || should_increase {
+            // limited_fps to ensure decoding is faster than encoding
+            let mut custom_fps = limited_fps as i32;
             if custom_fps < 1 {
                 custom_fps = 1;
             }
             // send custom fps
             let mut misc = Misc::new();
-            misc.set_option(OptionMessage {
-                custom_fps,
-                ..Default::default()
-            });
+            if version > hbb_common::get_version_number("1.2.1") {
+                // avoid confusion with custom image quality fps
+                misc.set_auto_adjust_fps(custom_fps as _);
+            } else {
+                misc.set_option(OptionMessage {
+                    custom_fps,
+                    ..Default::default()
+                });
+            }
             let mut msg = Message::new();
             msg.set_misc(misc);
             self.sender.send(Data::Message(msg)).ok();
             ctl.last_queue_size = len;
-            ctl.set_times += 1;
-            ctl.last_set_instant = Instant::now();
+            ctl.last_custom_fps = Some(custom_fps);
         }
         // send refresh
         if ctl.refresh_times < 10 // enough
@@ -1002,6 +1056,8 @@ impl<T: InvokeUiSession> Remote<T> {
                         if self.handler.is_file_transfer() {
                             self.handler.load_last_jobs();
                         }
+
+                        self.is_connected = true;
                     }
                     _ => {}
                 },
@@ -1415,12 +1471,9 @@ impl<T: InvokeUiSession> Remote<T> {
                         }
                     }
                 }
-                Some(message::Union::PeerInfo(pi)) => match pi.conn_id {
-                    crate::SYNC_PEER_INFO_DISPLAYS => {
-                        self.handler.set_displays(&pi.displays);
-                    }
-                    _ => {}
-                },
+                Some(message::Union::PeerInfo(pi)) => {
+                    self.handler.set_displays(&pi.displays);
+                }
                 _ => {}
             }
         }
@@ -1620,20 +1673,22 @@ impl RemoveJob {
 
 struct FpsControl {
     last_queue_size: usize,
-    set_times: usize,
     refresh_times: usize,
-    last_set_instant: Instant,
     last_refresh_instant: Instant,
+    last_full_speed_fps: Option<i32>,
+    last_custom_fps: Option<i32>,
+    idle_counter: usize,
 }
 
 impl Default for FpsControl {
     fn default() -> Self {
         Self {
             last_queue_size: Default::default(),
-            set_times: Default::default(),
             refresh_times: Default::default(),
-            last_set_instant: Instant::now(),
             last_refresh_instant: Instant::now(),
+            last_full_speed_fps: None,
+            last_custom_fps: None,
+            idle_counter: 0,
         }
     }
 }
diff --git a/src/common.rs b/src/common.rs
index ab3e087a3..8477c9e62 100644
--- a/src/common.rs
+++ b/src/common.rs
@@ -25,7 +25,7 @@ use hbb_common::{
     protobuf::Enum,
     protobuf::Message as _,
     rendezvous_proto::*,
-    sleep, socket_client,
+    socket_client,
     tcp::FramedStream,
     tokio, ResultType,
 };
@@ -39,8 +39,6 @@ pub type NotifyMessageBox = fn(String, String, String, String) -> dyn Future<Out
 pub const CLIPBOARD_NAME: &'static str = "clipboard";
 pub const CLIPBOARD_INTERVAL: u64 = 333;
 
-pub const SYNC_PEER_INFO_DISPLAYS: i32 = 1;
-
 #[cfg(all(target_os = "macos", feature = "flutter_texture_render"))]
 // https://developer.apple.com/forums/thread/712709
 // Memory alignment should be multiple of 64.
@@ -51,6 +49,11 @@ pub const DST_STRIDE_RGBA: usize = 1;
 // the executable name of the portable version
 pub const PORTABLE_APPNAME_RUNTIME_ENV_KEY: &str = "RUSTDESK_APPNAME";
 
+pub const PLATFORM_WINDOWS: &str = "Windows";
+pub const PLATFORM_LINUX: &str = "Linux";
+pub const PLATFORM_MACOS: &str = "Mac OS";
+pub const PLATFORM_ANDROID: &str = "Android";
+
 pub mod input {
     pub const MOUSE_TYPE_MOVE: i32 = 0;
     pub const MOUSE_TYPE_DOWN: i32 = 1;
@@ -627,6 +630,12 @@ pub async fn get_rendezvous_server(ms_timeout: u64) -> (String, Vec<String>, boo
     let (mut a, mut b) = get_rendezvous_server_(ms_timeout);
     #[cfg(not(any(target_os = "android", target_os = "ios")))]
     let (mut a, mut b) = get_rendezvous_server_(ms_timeout).await;
+    #[cfg(windows)]
+    if let Ok(lic) = crate::platform::get_license_from_exe_name() {
+        if !lic.host.is_empty() {
+            a = lic.host;
+        }
+    }
     let mut b: Vec<String> = b
         .drain(..)
         .map(|x| socket_client::check_port(x, config::RENDEZVOUS_PORT))
@@ -721,7 +730,7 @@ pub fn run_me<T: AsRef<std::ffi::OsStr>>(args: Vec<T>) -> std::io::Result<std::p
     }
     #[cfg(feature = "appimage")]
     {
-        let appdir = std::env::var("APPDIR").unwrap();
+        let appdir = std::env::var("APPDIR").map_err(|_| std::io::ErrorKind::Other)?;
         let appimage_cmd = std::path::Path::new(&appdir).join("AppRun");
         log::info!("path: {:?}", appimage_cmd);
         return std::process::Command::new(appimage_cmd).args(&args).spawn();
@@ -745,6 +754,45 @@ pub fn hostname() -> String {
     return DEVICE_NAME.lock().unwrap().clone();
 }
 
+#[inline]
+pub fn get_sysinfo() -> serde_json::Value {
+    use hbb_common::sysinfo::{CpuExt, System, SystemExt};
+    let system = System::new_all();
+    let memory = system.total_memory();
+    let memory = (memory as f64 / 1024. / 1024. / 1024. * 100.).round() / 100.;
+    let cpus = system.cpus();
+    let cpu_name = cpus.first().map(|x| x.brand()).unwrap_or_default();
+    let cpu_name = cpu_name.trim_end();
+    let cpu_freq = cpus.first().map(|x| x.frequency()).unwrap_or_default();
+    let cpu_freq = (cpu_freq as f64 / 1024. * 100.).round() / 100.;
+    let cpu = if cpu_freq > 0. {
+        format!("{}, {}GHz, ", cpu_name, cpu_freq)
+    } else {
+        "".to_owned() // android
+    };
+    let num_cpus = num_cpus::get();
+    let num_pcpus = num_cpus::get_physical();
+    let mut os = system.distribution_id();
+    os = format!("{} / {}", os, system.long_os_version().unwrap_or_default());
+    #[cfg(windows)]
+    {
+        os = format!("{os} - {}", system.os_version().unwrap_or_default());
+    }
+    let hostname = hostname(); // sys.hostname() return localhost on android in my test
+    use serde_json::json;
+    let mut out = json!({
+        "cpu": format!("{cpu}{num_cpus}/{num_pcpus} cores"),
+        "memory": format!("{memory}GB"),
+        "os": os,
+        "hostname": hostname,
+    });
+    #[cfg(not(any(target_os = "android", target_os = "ios")))] 
+    {
+        out["username"] = json!(crate::platform::get_active_username());
+    }
+    out
+}
+
 #[inline]
 pub fn check_port<T: std::string::ToString>(host: T, port: i32) -> String {
     hbb_common::socket_client::check_port(host, port)
@@ -789,30 +837,19 @@ pub fn check_software_update() {
 
 #[tokio::main(flavor = "current_thread")]
 async fn check_software_update_() -> hbb_common::ResultType<()> {
-    sleep(3.).await;
+    let url = "https://github.com/rustdesk/rustdesk/releases/latest";
+    let latest_release_response = reqwest::get(url).await?;
+    let latest_release_version = latest_release_response
+        .url()
+        .path()
+        .rsplit('/')
+        .next()
+        .unwrap();
 
-    let rendezvous_server = format!("rs-sg.rustdesk.com:{}", config::RENDEZVOUS_PORT);
-    let (mut socket, rendezvous_server) =
-        socket_client::new_udp_for(&rendezvous_server, CONNECT_TIMEOUT).await?;
+    let response_url = latest_release_response.url().to_string();
 
-    let mut msg_out = RendezvousMessage::new();
-    msg_out.set_software_update(SoftwareUpdate {
-        url: crate::VERSION.to_owned(),
-        ..Default::default()
-    });
-    socket.send(&msg_out, rendezvous_server).await?;
-    use hbb_common::protobuf::Message;
-    for _ in 0..2 {
-        if let Some(Ok((bytes, _))) = socket.next_timeout(READ_TIMEOUT).await {
-            if let Ok(msg_in) = RendezvousMessage::parse_from_bytes(&bytes) {
-                if let Some(rendezvous_message::Union::SoftwareUpdate(su)) = msg_in.union {
-                    let version = hbb_common::get_version_from_url(&su.url);
-                    if get_version_number(&version) > get_version_number(crate::VERSION) {
-                        *SOFTWARE_UPDATE_URL.lock().unwrap() = su.url;
-                    }
-                }
-            }
-        }
+    if get_version_number(&latest_release_version) > get_version_number(crate::VERSION) {
+        *SOFTWARE_UPDATE_URL.lock().unwrap() = response_url;
     }
     Ok(())
 }
@@ -835,15 +872,15 @@ pub fn is_setup(name: &str) -> bool {
 }
 
 pub fn get_custom_rendezvous_server(custom: String) -> String {
-    if !custom.is_empty() {
-        return custom;
-    }
     #[cfg(windows)]
-    if let Some(lic) = crate::platform::windows::get_license() {
+    if let Ok(lic) = crate::platform::windows::get_license_from_exe_name() {
         if !lic.host.is_empty() {
             return lic.host.clone();
         }
     }
+    if !custom.is_empty() {
+        return custom;
+    }
     if !config::PROD_RENDEZVOUS_SERVER.read().unwrap().is_empty() {
         return config::PROD_RENDEZVOUS_SERVER.read().unwrap().clone();
     }
@@ -851,15 +888,15 @@ pub fn get_custom_rendezvous_server(custom: String) -> String {
 }
 
 pub fn get_api_server(api: String, custom: String) -> String {
-    if !api.is_empty() {
-        return api.to_owned();
-    }
     #[cfg(windows)]
-    if let Some(lic) = crate::platform::windows::get_license() {
+    if let Ok(lic) = crate::platform::windows::get_license_from_exe_name() {
         if !lic.api.is_empty() {
             return lic.api.clone();
         }
     }
+    if !api.is_empty() {
+        return api.to_owned();
+    }
     let api = option_env!("API_SERVER").unwrap_or_default();
     if !api.is_empty() {
         return api.into();
@@ -984,6 +1021,12 @@ pub fn decode64<T: AsRef<[u8]>>(input: T) -> Result<Vec<u8>, base64::DecodeError
 }
 
 pub async fn get_key(sync: bool) -> String {
+    #[cfg(windows)]
+    if let Ok(lic) = crate::platform::windows::get_license_from_exe_name() {
+        if !lic.key.is_empty() {
+            return lic.key;
+        }
+    }
     #[cfg(target_os = "ios")]
     let mut key = Config::get_option("key");
     #[cfg(not(target_os = "ios"))]
@@ -993,12 +1036,6 @@ pub async fn get_key(sync: bool) -> String {
         let mut options = crate::ipc::get_options_async().await;
         options.remove("key").unwrap_or_default()
     };
-    if key.is_empty() {
-        #[cfg(windows)]
-        if let Some(lic) = crate::platform::windows::get_license() {
-            return lic.key;
-        }
-    }
     if key.is_empty() && !option_env!("RENDEZVOUS_SERVER").unwrap_or("").is_empty() {
         key = config::RS_PUB_KEY.to_owned();
     }
@@ -1070,6 +1107,7 @@ pub fn check_process(arg: &str, same_uid: bool) -> bool {
     if let Ok(linked) = path.read_link() {
         path = linked;
     }
+    let path = path.to_string_lossy().to_lowercase();
     let my_uid = sys
         .process((std::process::id() as usize).into())
         .map(|x| x.user_id())
@@ -1079,7 +1117,7 @@ pub fn check_process(arg: &str, same_uid: bool) -> bool {
         if let Ok(linked) = cur_path.read_link() {
             cur_path = linked;
         }
-        if cur_path != path {
+        if cur_path.to_string_lossy().to_lowercase() != path {
             continue;
         }
         if p.pid().to_string() == std::process::id().to_string() {
diff --git a/src/core_main.rs b/src/core_main.rs
index f8741a72f..3fe1a1630 100644
--- a/src/core_main.rs
+++ b/src/core_main.rs
@@ -1,3 +1,5 @@
+#[cfg(not(any(target_os = "android", target_os = "ios")))]
+use crate::client::translate;
 #[cfg(not(debug_assertions))]
 #[cfg(not(any(target_os = "android", target_os = "ios")))]
 use crate::platform::breakdown_callback;
@@ -5,6 +7,28 @@ use hbb_common::log;
 #[cfg(not(debug_assertions))]
 #[cfg(not(any(target_os = "android", target_os = "ios")))]
 use hbb_common::platform::register_breakdown_handler;
+#[cfg(windows)]
+use tauri_winrt_notification::{Duration, Sound, Toast};
+
+#[macro_export]
+macro_rules! my_println{
+    ($($arg:tt)*) => {
+        #[cfg(not(windows))]
+        println!("{}", format_args!($($arg)*));
+        #[cfg(windows)]
+        crate::platform::message_box(
+            &format!("{}", format_args!($($arg)*))
+        );
+    };
+}
+
+#[inline]
+fn is_empty_uni_link(arg: &str) -> bool {
+    if !arg.starts_with("rustdesk://") {
+        return false;
+    }
+    arg["rustdesk://".len()..].chars().all(|c| c == '/')
+}
 
 /// shared by flutter and sciter main function
 ///
@@ -13,21 +37,31 @@ use hbb_common::platform::register_breakdown_handler;
 /// If it returns [`Some`], then the process will continue, and flutter gui will be started.
 #[cfg(not(any(target_os = "android", target_os = "ios")))]
 pub fn core_main() -> Option<Vec<String>> {
+    #[cfg(windows)]
+    crate::platform::windows::bootstrap();
     let mut args = Vec::new();
     let mut flutter_args = Vec::new();
     let mut i = 0;
     let mut _is_elevate = false;
     let mut _is_run_as_system = false;
     let mut _is_quick_support = false;
-    let mut _is_flutter_connect = false;
+    let mut _is_flutter_invoke_new_connection = false;
     let mut arg_exe = Default::default();
     for arg in std::env::args() {
         if i == 0 {
             arg_exe = arg;
         } else if i > 0 {
             #[cfg(feature = "flutter")]
-            if arg == "--connect" || arg == "--play" {
-                _is_flutter_connect = true;
+            if [
+                "--connect",
+                "--play",
+                "--file-transfer",
+                "--port-forward",
+                "--rdp",
+            ]
+            .contains(&arg.as_str())
+            {
+                _is_flutter_invoke_new_connection = true;
             }
             if arg == "--elevate" {
                 _is_elevate = true;
@@ -43,9 +77,9 @@ pub fn core_main() -> Option<Vec<String>> {
     }
     #[cfg(any(target_os = "linux", target_os = "windows"))]
     if args.is_empty() {
-        #[cfg(target_os = "linux")]
-        hbb_common::allow_err!(crate::platform::check_autostart_config());
         if crate::check_process("--server", false) && !crate::check_process("--tray", true) {
+            #[cfg(target_os = "linux")]
+            hbb_common::allow_err!(crate::platform::check_autostart_config());
             hbb_common::allow_err!(crate::run_me(vec!["--tray"]));
         }
     }
@@ -55,15 +89,19 @@ pub fn core_main() -> Option<Vec<String>> {
     #[cfg(target_os = "linux")]
     #[cfg(feature = "flutter")]
     {
-        let (k, v) = ("LIBGL_ALWAYS_SOFTWARE", "true");
+        let (k, v) = ("LIBGL_ALWAYS_SOFTWARE", "1");
         if !hbb_common::config::Config::get_option("allow-always-software-render").is_empty() {
             std::env::set_var(k, v);
         } else {
             std::env::remove_var(k);
         }
     }
+    #[cfg(windows)]
+    if args.contains(&"--connect".to_string()) {
+        hbb_common::platform::windows::start_cpu_performance_monitor();
+    }
     #[cfg(feature = "flutter")]
-    if _is_flutter_connect {
+    if _is_flutter_invoke_new_connection {
         return core_main_invoke_new_connection(std::env::args());
     }
     let click_setup = cfg!(windows) && args.is_empty() && crate::common::is_setup(&arg_exe);
@@ -82,7 +120,7 @@ pub fn core_main() -> Option<Vec<String>> {
     {
         _is_quick_support |= !crate::platform::is_installed()
             && args.is_empty()
-            && (arg_exe.to_lowercase().ends_with("qs.exe")
+            && (arg_exe.to_lowercase().contains("-qs-")
                 || (!click_setup && crate::platform::is_elevated(None).unwrap_or(false)));
         crate::portable_service::client::set_quick_support(_is_quick_support);
     }
@@ -121,9 +159,8 @@ pub fn core_main() -> Option<Vec<String>> {
     #[cfg(all(feature = "flutter", feature = "plugin_framework"))]
     #[cfg(not(any(target_os = "android", target_os = "ios")))]
     init_plugins(&args);
-    if args.is_empty() {
-        #[cfg(windows)]
-        clipboard::ContextSend::enable(true);
+    log::info!("main start args:{:?}", args);
+    if args.is_empty() || is_empty_uni_link(&args[0]) {
         std::thread::spawn(move || crate::start_server(false));
     } else {
         #[cfg(windows)]
@@ -145,12 +182,23 @@ pub fn core_main() -> Option<Vec<String>> {
                 }
                 return None;
             } else if args[0] == "--silent-install" {
-                hbb_common::allow_err!(platform::install_me(
+                let res = platform::install_me(
                     "desktopicon startmenu driverCert",
                     "".to_owned(),
                     true,
                     args.len() > 1,
-                ));
+                );
+                let text = match res {
+                    Ok(_) => translate("Installation Successful!".to_string()),
+                    Err(_) => translate("Installation failed!".to_string()),
+                };
+                Toast::new(Toast::POWERSHELL_APP_ID)
+                    .title(&hbb_common::config::APP_NAME.read().unwrap())
+                    .text1(&text)
+                    .sound(Some(Sound::Default))
+                    .duration(Duration::Short)
+                    .show()
+                    .ok();
                 return None;
             } else if args[0] == "--install-cert" {
                 #[cfg(windows)]
@@ -181,7 +229,16 @@ pub fn core_main() -> Option<Vec<String>> {
                 crate::tray::start_tray();
             }
             return None;
+        } else if args[0] == "--install-service" {
+            log::info!("start --install-service");
+            crate::platform::install_service();
+            return None;
+        } else if args[0] == "--uninstall-service" {
+            log::info!("start --uninstall-service");
+            crate::platform::uninstall_service(false);
         } else if args[0] == "--service" {
+            #[cfg(target_os = "macos")]
+            crate::platform::macos::hide_dock();
             log::info!("start --service");
             crate::start_os_service();
             return None;
@@ -215,18 +272,126 @@ pub fn core_main() -> Option<Vec<String>> {
             return None;
         } else if args[0] == "--password" {
             if args.len() == 2 {
-                if crate::platform::is_root() {
-                    crate::ipc::set_permanent_password(args[1].to_owned()).unwrap();
+                if crate::platform::is_installed() && is_root() {
+                    if let Err(err) = crate::ipc::set_permanent_password(args[1].to_owned()) {
+                        println!("{err}");
+                    } else {
+                        println!("Done!");
+                    }
                 } else {
-                    println!("Administrative privileges required!");
+                    println!("Installation and administrative privileges required!");
                 }
             }
             return None;
         } else if args[0] == "--get-id" {
-            if crate::platform::is_root() {
+            if crate::platform::is_installed() && is_root() {
                 println!("{}", crate::ipc::get_id());
             } else {
-                println!("Permission denied!");
+                println!("Installation and administrative privileges required!");
+            }
+            return None;
+        } else if args[0] == "--set-id" {
+            if args.len() == 2 {
+                if crate::platform::is_installed() && is_root() {
+                    let old_id = crate::ipc::get_id();
+                    let mut res = crate::ui_interface::change_id_shared(args[1].to_owned(), old_id);
+                    if res.is_empty() {
+                        res = "Done!".to_owned();
+                    }
+                    println!("{}", res);
+                } else {
+                    println!("Installation and administrative privileges required!");
+                }
+            }
+            return None;
+        } else if args[0] == "--config" {
+            if args.len() == 2 && !args[0].contains("host=") {
+                if crate::platform::is_installed() && is_root() {
+                    // encrypted string used in renaming exe.
+                    let name = if args[1].ends_with(".exe") {
+                        args[1].to_owned()
+                    } else {
+                        format!("{}.exe", args[1])
+                    };
+                    if let Ok(lic) = crate::license::get_license_from_string(&name) {
+                        if !lic.host.is_empty() {
+                            crate::ui_interface::set_option("key".into(), lic.key);
+                            crate::ui_interface::set_option(
+                                "custom-rendezvous-server".into(),
+                                lic.host,
+                            );
+                            crate::ui_interface::set_option("api-server".into(), lic.api);
+                        }
+                    }
+                } else {
+                    println!("Installation and administrative privileges required!");
+                }
+            }
+            return None;
+        } else if args[0] == "--option" {
+            if crate::platform::is_installed() && is_root() {
+                if args.len() == 2 {
+                    let options = crate::ipc::get_options();
+                    println!("{}", options.get(&args[1]).unwrap_or(&"".to_owned()));
+                } else if args.len() == 3 {
+                    crate::ipc::set_option(&args[1], &args[2]);
+                }
+            } else {
+                println!("Installation and administrative privileges required!");
+            }
+            return None;
+        } else if args[0] == "--assign" {
+            if crate::platform::is_installed() && is_root() {
+                let max = args.len() - 1;
+                let pos = args.iter().position(|x| x == "--token").unwrap_or(max);
+                if pos < max {
+                    let token = args[pos + 1].to_owned();
+                    let id = crate::ipc::get_id();
+                    let uuid = crate::encode64(hbb_common::get_uuid());
+                    let mut user_name = None;
+                    let pos = args.iter().position(|x| x == "--user_name").unwrap_or(max);
+                    if pos < max {
+                        user_name = Some(args[pos + 1].to_owned());
+                    }
+                    let mut strategy_name = None;
+                    let pos = args
+                        .iter()
+                        .position(|x| x == "--strategy_name")
+                        .unwrap_or(max);
+                    if pos < max {
+                        strategy_name = Some(args[pos + 1].to_owned());
+                    }
+                    let mut body = serde_json::json!({
+                        "id": id,
+                        "uuid": uuid,
+                    });
+                    let header = "Authorization: Bearer ".to_owned() + &token;
+                    if user_name.is_none() && strategy_name.is_none() {
+                        println!("--user_name or --strategy_name is required!");
+                    } else {
+                        if let Some(name) = user_name {
+                            body["user_name"] = serde_json::json!(name);
+                        }
+                        if let Some(name) = strategy_name {
+                            body["strategy_name"] = serde_json::json!(name);
+                        }
+                        let url = crate::ui_interface::get_api_server() + "/api/devices/cli";
+                        match crate::post_request_sync(url, body.to_string(), &header) {
+                            Err(err) => println!("{}", err),
+                            Ok(text) => {
+                                if text.is_empty() {
+                                    println!("Done!");
+                                } else {
+                                    println!("{}", text);
+                                }
+                            }
+                        }
+                    }
+                } else {
+                    println!("--token is required!");
+                }
+            } else {
+                println!("Installation and administrative privileges required!");
             }
             return None;
         } else if args[0] == "--check-hwcodec-config" {
@@ -318,38 +483,48 @@ fn import_config(path: &str) {
 /// If it returns [`Some`], then the process will continue, and flutter gui will be started.
 #[cfg(feature = "flutter")]
 fn core_main_invoke_new_connection(mut args: std::env::Args) -> Option<Vec<String>> {
-    args.position(|element| {
-        return element == "--connect" || element == "--play";
-    })?;
-    let mut peer_id = args.next().unwrap_or("".to_string());
-    if peer_id.is_empty() {
-        eprintln!("please provide a valid peer id");
-        return None;
-    }
-    let app_name = crate::get_app_name();
-    let ext = format!(".{}", app_name.to_lowercase());
-    if peer_id.ends_with(&ext) {
-        peer_id = peer_id.replace(&ext, "");
-    }
-    let mut switch_uuid = None;
-    while let Some(item) = args.next() {
-        if item == "--switch_uuid" {
-            switch_uuid = args.next();
+    let mut authority = None;
+    let mut id = None;
+    let mut param_array = vec![];
+    while let Some(arg) = args.next() {
+        match arg.as_str() {
+            "--connect" | "--play" | "--file-transfer" | "--port-forward" | "--rdp" => {
+                authority = Some((&arg.to_string()[2..]).to_owned());
+                id = args.next();
+            }
+            "--password" => {
+                if let Some(password) = args.next() {
+                    param_array.push(format!("password={password}"));
+                }
+            }
+            "--relay" => {
+                param_array.push(format!("relay=true"));
+            }
+            // inner
+            "--switch_uuid" => {
+                if let Some(switch_uuid) = args.next() {
+                    param_array.push(format!("switch_uuid={switch_uuid}"));
+                }
+            }
+            _ => {}
         }
     }
-    let mut param_array = vec![];
-    if switch_uuid.is_some() {
-        let switch_uuid = switch_uuid.map_or("".to_string(), |p| format!("switch_uuid={}", p));
-        param_array.push(switch_uuid);
+    let mut uni_links = Default::default();
+    if let Some(authority) = authority {
+        if let Some(mut id) = id {
+            let app_name = crate::get_app_name();
+            let ext = format!(".{}", app_name.to_lowercase());
+            if id.ends_with(&ext) {
+                id = id.replace(&ext, "");
+            }
+            let params = param_array.join("&");
+            let params_flag = if params.is_empty() { "" } else { "?" };
+            uni_links = format!("rustdesk://{}/{}{}{}", authority, id, params_flag, params);
+        }
+    }
+    if uni_links.is_empty() {
+        return None;
     }
-
-    let params = param_array.join("&");
-    let params_flag = if params.is_empty() { "" } else { "?" };
-    #[allow(unused)]
-    let uni_links = format!(
-        "rustdesk://connection/new/{}{}{}",
-        peer_id, params_flag, params
-    );
 
     #[cfg(target_os = "linux")]
     return try_send_by_dbus(uni_links);
@@ -391,3 +566,14 @@ fn try_send_by_dbus(uni_links: String) -> Option<Vec<String>> {
         }
     }
 }
+
+#[cfg(not(any(target_os = "android", target_os = "ios")))]
+fn is_root() -> bool {
+    #[cfg(windows)]
+    {
+        return crate::platform::is_elevated(None).unwrap_or_default()
+            || crate::platform::is_root();
+    }
+    #[allow(unreachable_code)]
+    crate::platform::is_root()
+}
diff --git a/src/flutter.rs b/src/flutter.rs
index 4008b2420..fe648ae2c 100644
--- a/src/flutter.rs
+++ b/src/flutter.rs
@@ -36,9 +36,11 @@ pub(crate) const APP_TYPE_CM: &str = "cm";
 #[cfg(any(target_os = "android", target_os = "ios"))]
 pub(crate) const APP_TYPE_CM: &str = "main";
 
-pub(crate) const APP_TYPE_DESKTOP_REMOTE: &str = "remote";
-pub(crate) const APP_TYPE_DESKTOP_FILE_TRANSFER: &str = "file transfer";
-pub(crate) const APP_TYPE_DESKTOP_PORT_FORWARD: &str = "port forward";
+// Do not remove the following constants.
+// Uncomment them when they are used.
+// pub(crate) const APP_TYPE_DESKTOP_REMOTE: &str = "remote";
+// pub(crate) const APP_TYPE_DESKTOP_FILE_TRANSFER: &str = "file transfer";
+// pub(crate) const APP_TYPE_DESKTOP_PORT_FORWARD: &str = "port forward";
 
 lazy_static::lazy_static! {
     pub(crate) static ref CUR_SESSION_ID: RwLock<SessionID> = Default::default();
@@ -104,7 +106,10 @@ fn rust_args_to_c_args(args: Vec<String>, outlen: *mut c_int) -> *mut *mut c_cha
 
     // Let's fill a vector with null-terminated strings
     for s in args {
-        v.push(CString::new(s).unwrap());
+        match CString::new(s) {
+            Ok(s) => v.push(s),
+            Err(_) => return std::ptr::null_mut() as _,
+        }
     }
 
     // Turning each null-terminated string into a pointer.
@@ -183,7 +188,7 @@ pub type FlutterRgbaRendererPluginOnRgba = unsafe extern "C" fn(
 #[derive(Clone)]
 struct VideoRenderer {
     // TextureRgba pointer in flutter native.
-    ptr: usize,
+    ptr: Arc<RwLock<usize>>,
     width: usize,
     height: usize,
     on_rgba_func: Option<Symbol<'static, FlutterRgbaRendererPluginOnRgba>>,
@@ -211,7 +216,7 @@ impl Default for VideoRenderer {
             }
         };
         Self {
-            ptr: 0,
+            ptr: Default::default(),
             width: 0,
             height: 0,
             on_rgba_func,
@@ -228,19 +233,27 @@ impl VideoRenderer {
     }
 
     pub fn on_rgba(&self, rgba: &mut scrap::ImageRgb) {
-        if self.ptr == usize::default() {
+        let ptr = self.ptr.read().unwrap();
+        if *ptr == usize::default() {
             return;
         }
 
         // It is also Ok to skip this check.
         if self.width != rgba.w || self.height != rgba.h {
+            log::error!(
+                "width/height mismatch: ({},{}) != ({},{})",
+                self.width,
+                self.height,
+                rgba.w,
+                rgba.h
+            );
             return;
         }
 
         if let Some(func) = &self.on_rgba_func {
             unsafe {
                 func(
-                    self.ptr as _,
+                    *ptr as _,
                     rgba.raw.as_ptr() as _,
                     rgba.raw.len() as _,
                     rgba.w as _,
@@ -325,7 +338,7 @@ impl FlutterHandler {
     #[inline]
     #[cfg(feature = "flutter_texture_render")]
     pub fn register_texture(&mut self, ptr: usize) {
-        self.renderer.write().unwrap().ptr = ptr;
+        *self.renderer.read().unwrap().ptr.write().unwrap() = ptr;
     }
 
     #[inline]
@@ -334,6 +347,14 @@ impl FlutterHandler {
         *self.notify_rendered.write().unwrap() = false;
         self.renderer.write().unwrap().set_size(width, height);
     }
+
+    pub fn on_waiting_for_image_dialog_show(&self) {
+        #[cfg(any(feature = "flutter_texture_render"))]
+        {
+            *self.notify_rendered.write().unwrap() = false;
+        }
+        // rgba array render will notify every frame
+    }
 }
 
 impl InvokeUiSession for FlutterHandler {
@@ -778,11 +799,15 @@ pub fn session_start_(
         );
         #[cfg(not(feature = "flutter_texture_render"))]
         log::info!("Session {} start, render by flutter paint widget", id);
+        let is_pre_added = session.event_stream.read().unwrap().is_some();
+        session.close_event_stream();
         *session.event_stream.write().unwrap() = Some(event_stream);
-        let session = session.clone();
-        std::thread::spawn(move || {
-            io_loop(session);
-        });
+        if !is_pre_added {
+            let session = session.clone();
+            std::thread::spawn(move || {
+                io_loop(session);
+            });
+        }
         Ok(())
     } else {
         bail!("No session with peer id {}", id)
@@ -882,6 +907,10 @@ pub mod connection_manager {
             let client_json = serde_json::to_string(&client).unwrap_or("".into());
             self.push_event("update_voice_call_state", vec![("client", &client_json)]);
         }
+
+        fn file_transfer_log(&self, log: String) {
+            self.push_event("cm_file_transfer_log", vec![("log", &log.to_string())]);
+        }
     }
 
     impl FlutterHandler {
@@ -910,7 +939,7 @@ pub mod connection_manager {
 
     #[inline]
     #[cfg(not(any(target_os = "android", target_os = "ios")))]
-    pub fn start_listen_ipc_thread() {
+    fn start_listen_ipc_thread() {
         start_listen_ipc(true);
     }
 
@@ -931,6 +960,12 @@ pub mod connection_manager {
         }
     }
 
+    #[inline]
+    pub fn cm_init() {
+        #[cfg(not(any(target_os = "android", target_os = "ios")))]
+        start_listen_ipc_thread();
+    }
+
     #[cfg(target_os = "android")]
     use hbb_common::tokio::sync::mpsc::{UnboundedReceiver, UnboundedSender};
 
@@ -1006,24 +1041,24 @@ fn serialize_resolutions(resolutions: &Vec<Resolution>) -> String {
 }
 
 fn char_to_session_id(c: *const char) -> ResultType<SessionID> {
+    if c.is_null() {
+        bail!("Session id ptr is null");
+    }
     let cstr = unsafe { std::ffi::CStr::from_ptr(c as _) };
     let str = cstr.to_str()?;
     SessionID::from_str(str).map_err(|e| anyhow!("{:?}", e))
 }
 
-#[no_mangle]
-pub fn session_get_rgba_size(_session_uuid_str: *const char) -> usize {
+pub fn session_get_rgba_size(_session_id: SessionID) -> usize {
     #[cfg(not(feature = "flutter_texture_render"))]
-    if let Ok(session_id) = char_to_session_id(_session_uuid_str) {
-        if let Some(session) = SESSIONS.read().unwrap().get(&session_id) {
-            return session.rgba.read().unwrap().len();
-        }
+    if let Some(session) = SESSIONS.read().unwrap().get(&_session_id) {
+        return session.rgba.read().unwrap().len();
     }
     0
 }
 
 #[no_mangle]
-pub fn session_get_rgba(session_uuid_str: *const char) -> *const u8 {
+pub extern "C" fn session_get_rgba(session_uuid_str: *const char) -> *const u8 {
     if let Ok(session_id) = char_to_session_id(session_uuid_str) {
         if let Some(session) = SESSIONS.read().unwrap().get(&session_id) {
             return session.get_rgba();
@@ -1033,23 +1068,17 @@ pub fn session_get_rgba(session_uuid_str: *const char) -> *const u8 {
     std::ptr::null()
 }
 
-#[no_mangle]
-pub fn session_next_rgba(session_uuid_str: *const char) {
-    if let Ok(session_id) = char_to_session_id(session_uuid_str) {
-        if let Some(session) = SESSIONS.read().unwrap().get(&session_id) {
-            return session.next_rgba();
-        }
+pub fn session_next_rgba(session_id: SessionID) {
+    if let Some(session) = SESSIONS.read().unwrap().get(&session_id) {
+        return session.next_rgba();
     }
 }
 
 #[inline]
-#[no_mangle]
-pub fn session_register_texture(_session_uuid_str: *const char, _ptr: usize) {
+pub fn session_register_texture(_session_id: SessionID, _ptr: usize) {
     #[cfg(feature = "flutter_texture_render")]
-    if let Ok(session_id) = char_to_session_id(_session_uuid_str) {
-        if let Some(session) = SESSIONS.write().unwrap().get_mut(&session_id) {
-            return session.register_texture(_ptr);
-        }
+    if let Some(session) = SESSIONS.write().unwrap().get_mut(&_session_id) {
+        return session.register_texture(_ptr);
     }
 }
 
@@ -1071,16 +1100,28 @@ pub fn push_global_event(channel: &str, event: String) -> Option<bool> {
     Some(GLOBAL_EVENT_STREAM.read().unwrap().get(channel)?.add(event))
 }
 
-pub fn start_global_event_stream(s: StreamSink<String>, app_type: String) -> ResultType<()> {
-    if let Some(_) = GLOBAL_EVENT_STREAM
-        .write()
+#[inline]
+pub fn get_global_event_channels() -> Vec<String> {
+    GLOBAL_EVENT_STREAM
+        .read()
         .unwrap()
-        .insert(app_type.clone(), s)
-    {
-        log::warn!(
-            "Global event stream of type {} is started before, but now removed",
-            app_type
-        );
+        .keys()
+        .cloned()
+        .collect()
+}
+
+pub fn start_global_event_stream(s: StreamSink<String>, app_type: String) -> ResultType<()> {
+    let app_type_values = app_type.split(",").collect::<Vec<&str>>();
+    let mut lock = GLOBAL_EVENT_STREAM.write().unwrap();
+    if !lock.contains_key(app_type_values[0]) {
+        lock.insert(app_type_values[0].to_string(), s);
+    } else {
+        if let Some(_) = lock.insert(app_type.clone(), s) {
+            log::warn!(
+                "Global event stream of type {} is started before, but now removed",
+                app_type
+            );
+        }
     }
     Ok(())
 }
@@ -1089,8 +1130,84 @@ pub fn stop_global_event_stream(app_type: String) {
     let _ = GLOBAL_EVENT_STREAM.write().unwrap().remove(&app_type);
 }
 
-#[no_mangle]
-unsafe extern "C" fn get_rgba() {}
+#[inline]
+fn session_send_touch_scale(
+    session_id: SessionID,
+    v: &serde_json::Value,
+    alt: bool,
+    ctrl: bool,
+    shift: bool,
+    command: bool,
+) {
+    match v.get("v").and_then(|s| s.as_i64()) {
+        Some(scale) => {
+            if let Some(session) = SESSIONS.read().unwrap().get(&session_id) {
+                session.send_touch_scale(scale as _, alt, ctrl, shift, command);
+            }
+        }
+        None => {}
+    }
+}
+
+#[inline]
+fn session_send_touch_pan(
+    session_id: SessionID,
+    v: &serde_json::Value,
+    pan_event: &str,
+    alt: bool,
+    ctrl: bool,
+    shift: bool,
+    command: bool,
+) {
+    match v.get("v") {
+        Some(v) => match (
+            v.get("x").and_then(|x| x.as_i64()),
+            v.get("y").and_then(|y| y.as_i64()),
+        ) {
+            (Some(x), Some(y)) => {
+                if let Some(session) = SESSIONS.read().unwrap().get(&session_id) {
+                    session
+                        .send_touch_pan_event(pan_event, x as _, y as _, alt, ctrl, shift, command);
+                }
+            }
+            _ => {}
+        },
+        _ => {}
+    }
+}
+
+fn session_send_touch_event(
+    session_id: SessionID,
+    v: &serde_json::Value,
+    alt: bool,
+    ctrl: bool,
+    shift: bool,
+    command: bool,
+) {
+    match v.get("t").and_then(|t| t.as_str()) {
+        Some("scale") => session_send_touch_scale(session_id, v, alt, ctrl, shift, command),
+        Some(pan_event) => {
+            session_send_touch_pan(session_id, v, pan_event, alt, ctrl, shift, command)
+        }
+        _ => {}
+    }
+}
+
+pub fn session_send_pointer(session_id: SessionID, msg: String) {
+    if let Ok(m) = serde_json::from_str::<HashMap<String, serde_json::Value>>(&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();
+        match (m.get("k"), m.get("v")) {
+            (Some(k), Some(v)) => match k.as_str() {
+                Some("touch") => session_send_touch_event(session_id, v, alt, ctrl, shift, command),
+                _ => {}
+            },
+            _ => {}
+        }
+    }
+}
 
 /// Hooks for session.
 #[derive(Clone)]
diff --git a/src/flutter_ffi.rs b/src/flutter_ffi.rs
index cc0a4f59a..eaf273d2e 100644
--- a/src/flutter_ffi.rs
+++ b/src/flutter_ffi.rs
@@ -15,7 +15,7 @@ use flutter_rust_bridge::{StreamSink, SyncReturn};
 use hbb_common::allow_err;
 use hbb_common::{
     config::{self, LocalConfig, PeerConfig, PeerInfoSerde},
-    fs, log,
+    fs, lazy_static, log,
     message_proto::KeyboardMode,
     ResultType,
 };
@@ -24,11 +24,19 @@ use std::{
     ffi::{CStr, CString},
     os::raw::c_char,
     str::FromStr,
+    sync::{
+        atomic::{AtomicI32, Ordering},
+        Arc,
+    },
     time::SystemTime,
 };
 
 pub type SessionID = uuid::Uuid;
 
+lazy_static::lazy_static! {
+    static ref TEXTURE_RENDER_KEY: Arc<AtomicI32> = Arc::new(AtomicI32::new(0));
+}
+
 fn initialize(app_dir: &str) {
     *config::APP_DIR.write().unwrap() = app_dir.to_owned();
     #[cfg(target_os = "android")]
@@ -166,6 +174,12 @@ pub fn session_record_screen(session_id: SessionID, start: bool, width: usize, h
     }
 }
 
+pub fn session_record_status(session_id: SessionID, status: bool) {
+    if let Some(session) = SESSIONS.read().unwrap().get(&session_id) {
+        session.record_status(status);
+    }
+}
+
 pub fn session_reconnect(session_id: SessionID, force_relay: bool) {
     if let Some(session) = SESSIONS.read().unwrap().get(&session_id) {
         session.reconnect(force_relay);
@@ -183,26 +197,39 @@ pub fn session_toggle_option(session_id: SessionID, value: String) {
     }
 }
 
-pub fn session_get_flutter_config(session_id: SessionID, k: String) -> Option<String> {
+pub fn session_get_flutter_option(session_id: SessionID, k: String) -> Option<String> {
     if let Some(session) = SESSIONS.read().unwrap().get(&session_id) {
-        Some(session.get_flutter_config(k))
+        Some(session.get_flutter_option(k))
     } else {
         None
     }
 }
 
-pub fn session_set_flutter_config(session_id: SessionID, k: String, v: String) {
+pub fn session_set_flutter_option(session_id: SessionID, k: String, v: String) {
     if let Some(session) = SESSIONS.write().unwrap().get_mut(&session_id) {
-        session.save_flutter_config(k, v);
+        session.save_flutter_option(k, v);
     }
 }
 
-pub fn get_local_flutter_config(k: String) -> SyncReturn<String> {
-    SyncReturn(ui_interface::get_local_flutter_config(k))
+pub fn session_get_flutter_option_by_peer_id(id: String, k: String) -> Option<String> {
+    if let Some((_, session)) = SESSIONS.read().unwrap().iter().find(|(_, s)| s.id == id) {
+        Some(session.get_flutter_option(k))
+    } else {
+        None
+    }
 }
 
-pub fn set_local_flutter_config(k: String, v: String) {
-    ui_interface::set_local_flutter_config(k, v);
+pub fn get_next_texture_key() -> SyncReturn<i32> {
+    let k = TEXTURE_RENDER_KEY.fetch_add(1, Ordering::SeqCst) + 1;
+    SyncReturn(k)
+}
+
+pub fn get_local_flutter_option(k: String) -> SyncReturn<String> {
+    SyncReturn(ui_interface::get_local_flutter_option(k))
+}
+
+pub fn set_local_flutter_option(k: String, v: String) {
+    ui_interface::set_local_flutter_option(k, v);
 }
 
 pub fn get_local_kb_layout_type() -> SyncReturn<String> {
@@ -357,6 +384,7 @@ pub fn session_enter_or_leave(_session_id: SessionID, _enter: bool) -> SyncRetur
     #[cfg(not(any(target_os = "android", target_os = "ios")))]
     if let Some(session) = SESSIONS.read().unwrap().get(&_session_id) {
         if _enter {
+            set_cur_session_id(_session_id);
             session.enter();
         } else {
             session.leave();
@@ -590,8 +618,8 @@ pub fn main_get_default_sound_input() -> Option<String> {
     None
 }
 
-pub fn main_get_hostname() -> SyncReturn<String> {
-    SyncReturn(get_hostname())
+pub fn main_get_login_device_info() -> SyncReturn<String> {
+    SyncReturn(get_login_device_info_json())
 }
 
 pub fn main_change_id(new_id: String) {
@@ -606,10 +634,23 @@ pub fn main_get_option(key: String) -> String {
     get_option(key)
 }
 
+pub fn main_get_option_sync(key: String) -> SyncReturn<String> {
+    SyncReturn(get_option(key))
+}
+
 pub fn main_get_error() -> String {
     get_error()
 }
 
+pub fn main_show_option(_key: String) -> SyncReturn<bool> {
+    #[cfg(all(target_os = "linux", feature = "linux_headless"))]
+    #[cfg(not(any(feature = "flatpak", feature = "appimage")))]
+    if _key.eq(config::CONFIG_OPTION_ALLOW_LINUX_HEADLESS) {
+        return SyncReturn(true);
+    }
+    SyncReturn(false)
+}
+
 pub fn main_set_option(key: String, value: String) {
     if key.eq("custom-rendezvous-server") {
         set_option(key, value);
@@ -626,6 +667,10 @@ pub fn main_get_options() -> String {
     get_options()
 }
 
+pub fn main_get_options_sync() -> SyncReturn<String> {
+    SyncReturn(get_options())
+}
+
 pub fn main_set_options(json: String) {
     let map: HashMap<String, String> = serde_json::from_str(&json).unwrap_or(HashMap::new());
     if !map.is_empty() {
@@ -685,7 +730,7 @@ pub fn main_get_connect_status() -> String {
     }
     #[cfg(any(target_os = "android", target_os = "ios"))]
     {
-        let mut state = hbb_common::config::get_online_statue();
+        let mut state = hbb_common::config::get_online_state();
         if state > 0 {
             state = 1;
         }
@@ -742,6 +787,17 @@ pub fn main_get_peer_option_sync(id: String, key: String) -> SyncReturn<String>
     SyncReturn(get_peer_option(id, key))
 }
 
+// Sometimes we need to get the flutter option of a peer by reading the file.
+// Because the session may not be established yet.
+pub fn main_get_peer_flutter_option_sync(id: String, k: String) -> SyncReturn<String> {
+    SyncReturn(get_peer_flutter_option(id, k))
+}
+
+pub fn main_set_peer_flutter_option_sync(id: String, k: String, v: String) -> SyncReturn<()> {
+    set_peer_flutter_option(id, k, v);
+    SyncReturn(())
+}
+
 pub fn main_set_peer_option(id: String, key: String, value: String) {
     set_peer_option(id, key, value)
 }
@@ -760,6 +816,15 @@ pub fn main_set_peer_alias(id: String, alias: String) {
     set_peer_option(id, "alias".to_owned(), alias)
 }
 
+pub fn main_get_new_stored_peers() -> String {
+    let peers: Vec<String> = config::NEW_STORED_PEER_CONFIG
+        .lock()
+        .unwrap()
+        .drain()
+        .collect();
+    serde_json::to_string(&peers).unwrap_or_default()
+}
+
 pub fn main_forget_password(id: String) {
     forget_password(id)
 }
@@ -768,9 +833,13 @@ pub fn main_peer_has_password(id: String) -> bool {
     peer_has_password(id)
 }
 
+pub fn main_peer_exists(id: String) -> bool {
+    peer_exists(&id)
+}
+
 pub fn main_load_recent_peers() {
     if !config::APP_DIR.read().unwrap().is_empty() {
-        let peers: Vec<HashMap<&str, String>> = PeerConfig::peers()
+        let peers: Vec<HashMap<&str, String>> = PeerConfig::peers(None)
             .drain(..)
             .map(|(id, _, p)| peer_to_map(id, p))
             .collect();
@@ -791,7 +860,7 @@ pub fn main_load_recent_peers() {
 
 pub fn main_load_recent_peers_sync() -> SyncReturn<String> {
     if !config::APP_DIR.read().unwrap().is_empty() {
-        let peers: Vec<HashMap<&str, String>> = PeerConfig::peers()
+        let peers: Vec<HashMap<&str, String>> = PeerConfig::peers(None)
             .drain(..)
             .map(|(id, _, p)| peer_to_map(id, p))
             .collect();
@@ -808,10 +877,27 @@ pub fn main_load_recent_peers_sync() -> SyncReturn<String> {
     SyncReturn("".to_string())
 }
 
+pub fn main_load_recent_peers_for_ab(filter: String) -> String {
+    let id_filters = serde_json::from_str::<Vec<String>>(&filter).unwrap_or_default();
+    let id_filters = if id_filters.is_empty() {
+        None
+    } else {
+        Some(id_filters)
+    };
+    if !config::APP_DIR.read().unwrap().is_empty() {
+        let peers: Vec<HashMap<&str, String>> = PeerConfig::peers(id_filters)
+            .drain(..)
+            .map(|(id, _, p)| peer_to_map(id, p))
+            .collect();
+        return serde_json::ser::to_string(&peers).unwrap_or("".to_owned());
+    }
+    "".to_string()
+}
+
 pub fn main_load_fav_peers() {
     if !config::APP_DIR.read().unwrap().is_empty() {
         let favs = get_fav();
-        let mut recent = PeerConfig::peers();
+        let mut recent = PeerConfig::peers(None);
         let mut lan = config::LanPeers::load()
             .peers
             .iter()
@@ -876,15 +962,12 @@ pub fn main_remove_discovered(id: String) {
 }
 
 fn main_broadcast_message(data: &HashMap<&str, &str>) {
-    let apps = vec![
-        flutter::APP_TYPE_DESKTOP_REMOTE,
-        flutter::APP_TYPE_DESKTOP_FILE_TRANSFER,
-        flutter::APP_TYPE_DESKTOP_PORT_FORWARD,
-    ];
-
     let event = serde_json::ser::to_string(&data).unwrap_or("".to_owned());
-    for app in apps {
-        let _res = flutter::push_global_event(app, event.clone());
+    for app in flutter::get_global_event_channels() {
+        if app == flutter::APP_TYPE_MAIN || app == flutter::APP_TYPE_CM {
+            continue;
+        }
+        let _res = flutter::push_global_event(&app, event.clone());
     }
 }
 
@@ -1069,6 +1152,28 @@ pub fn main_start_dbus_server() {
     }
 }
 
+pub fn main_save_ab(json: String) {
+    if json.len() > 1024 {
+        std::thread::spawn(|| {
+            config::Ab::store(json);
+        });
+    } else {
+        config::Ab::store(json);
+    }
+}
+
+pub fn main_clear_ab() {
+    config::Ab::remove();
+}
+
+pub fn main_load_ab() -> String {
+    serde_json::to_string(&config::Ab::load()).unwrap_or_default()
+}
+
+pub fn session_send_pointer(session_id: SessionID, msg: String) {
+    super::flutter::session_send_pointer(session_id, msg);
+}
+
 pub fn session_send_mouse(session_id: SessionID, msg: String) {
     if let Ok(m) = serde_json::from_str::<HashMap<String, String>>(&msg) {
         let alt = m.get("alt").is_some();
@@ -1146,6 +1251,12 @@ pub fn session_change_prefer_codec(session_id: SessionID) {
     }
 }
 
+pub fn session_on_waiting_for_image_dialog_show(session_id: SessionID) {
+    if let Some(session) = SESSIONS.read().unwrap().get(&session_id) {
+        session.ui_handler.on_waiting_for_image_dialog_show();
+    }
+}
+
 pub fn main_set_home_dir(_home: String) {
     #[cfg(any(target_os = "android", target_os = "ios"))]
     {
@@ -1277,18 +1388,6 @@ pub fn main_get_build_date() -> String {
     crate::BUILD_DATE.to_string()
 }
 
-#[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<String>, offlines: Vec<String>) {
     let data = HashMap::from([
         ("name", "callback_query_onlines".to_owned()),
@@ -1301,6 +1400,22 @@ fn handle_query_onlines(onlines: Vec<String>, offlines: Vec<String>) {
     );
 }
 
+pub fn translate(name: String, locale: String) -> SyncReturn<String> {
+    SyncReturn(crate::client::translate_locale(name, &locale))
+}
+
+pub fn session_get_rgba_size(session_id: SessionID) -> SyncReturn<usize> {
+    SyncReturn(super::flutter::session_get_rgba_size(session_id))
+}
+
+pub fn session_next_rgba(session_id: SessionID) -> SyncReturn<()> {
+    SyncReturn(super::flutter::session_next_rgba(session_id))
+}
+
+pub fn session_register_texture(session_id: SessionID, ptr: usize) -> SyncReturn<()> {
+    SyncReturn(super::flutter::session_register_texture(session_id, ptr))
+}
+
 pub fn query_onlines(ids: Vec<String>) {
     #[cfg(not(any(target_os = "ios")))]
     crate::rendezvous_mediator::query_online_states(ids, handle_query_onlines)
@@ -1448,9 +1563,9 @@ pub fn main_use_texture_render() -> SyncReturn<bool> {
     }
 }
 
-pub fn cm_start_listen_ipc_thread() {
+pub fn cm_init() {
     #[cfg(not(any(target_os = "android", target_os = "ios")))]
-    crate::flutter::connection_manager::start_listen_ipc_thread();
+    crate::flutter::connection_manager::cm_init();
 }
 
 /// Start an ipc server for receiving the url scheme.
diff --git a/src/hbbs_http/account.rs b/src/hbbs_http/account.rs
index bc078440a..e76dda76c 100644
--- a/src/hbbs_http/account.rs
+++ b/src/hbbs_http/account.rs
@@ -1,8 +1,5 @@
 use super::HbbHttpResponse;
-use hbb_common::{
-    config::{Config, LocalConfig},
-    log, ResultType,
-};
+use hbb_common::{config::LocalConfig, log, ResultType};
 use reqwest::blocking::Client;
 use serde_derive::{Deserialize, Serialize};
 use serde_repr::{Deserialize_repr, Serialize_repr};
@@ -14,8 +11,6 @@ use std::{
 use url::Url;
 
 lazy_static::lazy_static! {
-    static ref API_SERVER: String = crate::get_api_server(
-        Config::get_option("api-server"), Config::get_option("custom-rendezvous-server"));
     static ref OIDC_SESSION: Arc<RwLock<OidcSession>> = Arc::new(RwLock::new(OidcSession::new()));
 }
 
@@ -142,20 +137,35 @@ impl OidcSession {
         }
     }
 
-    fn auth(op: &str, id: &str, uuid: &str) -> ResultType<HbbHttpResponse<OidcAuthUrl>> {
+    fn auth(
+        api_server: &str,
+        op: &str,
+        id: &str,
+        uuid: &str,
+    ) -> ResultType<HbbHttpResponse<OidcAuthUrl>> {
         Ok(OIDC_SESSION
             .read()
             .unwrap()
             .client
-            .post(format!("{}/api/oidc/auth", *API_SERVER))
-            .json(&HashMap::from([("op", op), ("id", id), ("uuid", uuid)]))
+            .post(format!("{}/api/oidc/auth", api_server))
+            .json(&serde_json::json!({
+                "op": op,
+                "id": id,
+                "uuid": uuid,
+                "deviceInfo": crate::ui_interface::get_login_device_info(),
+            }))
             .send()?
             .try_into()?)
     }
 
-    fn query(code: &str, id: &str, uuid: &str) -> ResultType<HbbHttpResponse<AuthBody>> {
+    fn query(
+        api_server: &str,
+        code: &str,
+        id: &str,
+        uuid: &str,
+    ) -> ResultType<HbbHttpResponse<AuthBody>> {
         let url = reqwest::Url::parse_with_params(
-            &format!("{}/api/oidc/auth-query", *API_SERVER),
+            &format!("{}/api/oidc/auth-query", api_server),
             &[("code", code), ("id", id), ("uuid", uuid)],
         )?;
         Ok(OIDC_SESSION
@@ -189,8 +199,8 @@ impl OidcSession {
         std::thread::sleep(std::time::Duration::from_secs_f32(secs));
     }
 
-    fn auth_task(op: String, id: String, uuid: String, remember_me: bool) {
-        let auth_request_res = Self::auth(&op, &id, &uuid);
+    fn auth_task(api_server: String, op: String, id: String, uuid: String, remember_me: bool) {
+        let auth_request_res = Self::auth(&api_server, &op, &id, &uuid);
         log::info!("Request oidc auth result: {:?}", &auth_request_res);
         let code_url = match auth_request_res {
             Ok(HbbHttpResponse::<_>::Data(code_url)) => code_url,
@@ -226,7 +236,7 @@ impl OidcSession {
         let begin = Instant::now();
         let query_timeout = OIDC_SESSION.read().unwrap().query_timeout;
         while OIDC_SESSION.read().unwrap().keep_querying && begin.elapsed() < query_timeout {
-            match Self::query(&code_url.code, &id, &uuid) {
+            match Self::query(&api_server, &code_url.code, &id, &uuid) {
                 Ok(HbbHttpResponse::<_>::Data(auth_body)) => {
                     if remember_me {
                         LocalConfig::set_option(
@@ -289,12 +299,18 @@ impl OidcSession {
         }
     }
 
-    pub fn account_auth(op: String, id: String, uuid: String, remember_me: bool) {
+    pub fn account_auth(
+        api_server: String,
+        op: String,
+        id: String,
+        uuid: String,
+        remember_me: bool,
+    ) {
         Self::auth_cancel();
         Self::wait_stop_querying();
         OIDC_SESSION.write().unwrap().before_task();
         std::thread::spawn(move || {
-            Self::auth_task(op, id, uuid, remember_me);
+            Self::auth_task(api_server, op, id, uuid, remember_me);
             OIDC_SESSION.write().unwrap().after_task();
         });
     }
diff --git a/src/hbbs_http/sync.rs b/src/hbbs_http/sync.rs
index 251436aa7..9db6c1c93 100644
--- a/src/hbbs_http/sync.rs
+++ b/src/hbbs_http/sync.rs
@@ -1,4 +1,8 @@
-use std::{collections::HashMap, sync::Mutex, time::Duration};
+use std::{
+    collections::HashMap,
+    sync::{Arc, Mutex},
+    time::Duration,
+};
 
 #[cfg(not(any(target_os = "ios")))]
 use crate::Connection;
@@ -9,15 +13,17 @@ use hbb_common::{
 use serde::{Deserialize, Serialize};
 use serde_json::{json, Value};
 
-const TIME_HEARTBEAT: Duration = Duration::from_secs(30);
+const TIME_HEARTBEAT: Duration = Duration::from_secs(15);
+const UPLOAD_SYSINFO_TIMEOUT: Duration = Duration::from_secs(120);
 const TIME_CONN: Duration = Duration::from_secs(3);
 
 #[cfg(not(any(target_os = "ios")))]
 lazy_static::lazy_static! {
     static ref SENDER : Mutex<broadcast::Sender<Vec<i32>>> = Mutex::new(start_hbbs_sync());
+    static ref PRO: Arc<Mutex<bool>> = Default::default();
 }
 
-#[cfg(not(any(target_os = "android", target_os = "ios")))]
+#[cfg(not(any(target_os = "ios")))]
 pub fn start() {
     let _sender = SENDER.lock().unwrap();
 }
@@ -45,55 +51,84 @@ pub struct StrategyOptions {
 #[cfg(not(any(target_os = "ios")))]
 #[tokio::main(flavor = "current_thread")]
 async fn start_hbbs_sync_async() {
-    tokio::spawn(async move {
-        let mut interval = tokio::time::interval_at(Instant::now() + TIME_CONN, TIME_CONN);
-        let mut last_send = Instant::now();
-        loop {
-            tokio::select! {
-                _ = interval.tick() => {
-                    let url = heartbeat_url();
-                    let modified_at = LocalConfig::get_option("strategy_timestamp").parse::<i64>().unwrap_or(0);
-                    if !url.is_empty() {
-                        let conns = Connection::alive_conns();
-                        if conns.is_empty() && last_send.elapsed() < TIME_HEARTBEAT {
-                            continue;
+    let mut interval = tokio::time::interval_at(Instant::now() + TIME_CONN, TIME_CONN);
+    let mut last_sent: Option<Instant> = None;
+    let mut info_uploaded: (bool, String, Option<Instant>) = (false, "".to_owned(), None);
+    loop {
+        tokio::select! {
+            _ = interval.tick() => {
+                let url = heartbeat_url();
+                if url.is_empty() {
+                    *PRO.lock().unwrap() = false;
+                    continue;
+                }
+                if !Config::get_option("stop-service").is_empty() {
+                    continue;
+                }
+                let conns = Connection::alive_conns();
+                if info_uploaded.0 && url != info_uploaded.1 {
+                    info_uploaded.0 = false;
+                    *PRO.lock().unwrap() = false;
+                }
+                if !info_uploaded.0 && info_uploaded.2.map(|x| x.elapsed() >= UPLOAD_SYSINFO_TIMEOUT).unwrap_or(true){
+                    let mut v = crate::get_sysinfo();
+                    v["version"] = json!(crate::VERSION);
+                    v["id"] = json!(Config::get_id());
+                    v["uuid"] = json!(crate::encode64(hbb_common::get_uuid()));
+                    match crate::post_request(url.replace("heartbeat", "sysinfo"), v.to_string(), "").await {
+                        Ok(x)  => {
+                            if x == "SYSINFO_UPDATED" {
+                                info_uploaded = (true, url.clone(), None);
+                                hbb_common::log::info!("sysinfo updated");
+                                *PRO.lock().unwrap() = true;
+                            } else if x == "ID_NOT_FOUND" {
+                                info_uploaded.2 = None; // next heartbeat will upload sysinfo again
+                            } else {
+                                info_uploaded.2 = Some(Instant::now());
+                            }
                         }
-                        last_send = Instant::now();
-                        let mut v = Value::default();
-                        v["id"] = json!(Config::get_id());
-                        v["ver"] = json!(hbb_common::get_version_number(crate::VERSION));
-                        if !conns.is_empty() {
-                            v["conns"] = json!(conns);
+                        _ => {
+                            info_uploaded.2 = Some(Instant::now());
                         }
-                        v["modified_at"] = json!(modified_at);
-                        if let Ok(s) = crate::post_request(url.clone(), v.to_string(), "").await {
-                            if let Ok(mut rsp) = serde_json::from_str::<HashMap::<&str, Value>>(&s) {
-                                if let Some(conns)  = rsp.remove("disconnect") {
-                                        if let Ok(conns) = serde_json::from_value::<Vec<i32>>(conns) {
-                                            SENDER.lock().unwrap().send(conns).ok();
-                                        }
+                    }
+                }
+                if conns.is_empty() && last_sent.map(|x| x.elapsed() < TIME_HEARTBEAT).unwrap_or(false){
+                    continue;
+                }
+                last_sent = Some(Instant::now());
+                let mut v = Value::default();
+                v["id"] = json!(Config::get_id());
+                v["uuid"] = json!(crate::encode64(hbb_common::get_uuid()));
+                v["ver"] = json!(hbb_common::get_version_number(crate::VERSION));
+                if !conns.is_empty() {
+                    v["conns"] = json!(conns);
+                }
+                let modified_at = LocalConfig::get_option("strategy_timestamp").parse::<i64>().unwrap_or(0);
+                v["modified_at"] = json!(modified_at);
+                if let Ok(s) = crate::post_request(url.clone(), v.to_string(), "").await {
+                    if let Ok(mut rsp) = serde_json::from_str::<HashMap::<&str, Value>>(&s) {
+                        if let Some(conns)  = rsp.remove("disconnect") {
+                                if let Ok(conns) = serde_json::from_value::<Vec<i32>>(conns) {
+                                    SENDER.lock().unwrap().send(conns).ok();
                                 }
-                                if let Some(rsp_modified_at) = rsp.remove("modified_at") {
-                                    if let Ok(rsp_modified_at) = serde_json::from_value::<i64>(rsp_modified_at) {
-                                        if rsp_modified_at != modified_at {
-                                            LocalConfig::set_option("strategy_timestamp".to_string(), rsp_modified_at.to_string());
-                                        }
-                                    }
-                                }
-                                if let Some(strategy) = rsp.remove("strategy") {
-                                    if let Ok(strategy) = serde_json::from_value::<StrategyOptions>(strategy) {
-                                        handle_config_options(strategy.config_options);
-                                    }
+                        }
+                        if let Some(rsp_modified_at) = rsp.remove("modified_at") {
+                            if let Ok(rsp_modified_at) = serde_json::from_value::<i64>(rsp_modified_at) {
+                                if rsp_modified_at != modified_at {
+                                    LocalConfig::set_option("strategy_timestamp".to_string(), rsp_modified_at.to_string());
                                 }
                             }
                         }
+                        if let Some(strategy) = rsp.remove("strategy") {
+                            if let Ok(strategy) = serde_json::from_value::<StrategyOptions>(strategy) {
+                                handle_config_options(strategy.config_options);
+                            }
+                        }
                     }
                 }
             }
         }
-    })
-    .await
-    .ok();
+    }
 }
 
 fn heartbeat_url() -> String {
@@ -121,3 +156,8 @@ fn handle_config_options(config_options: HashMap<String, String>) {
         .count();
     Config::set_options(options);
 }
+
+#[cfg(not(any(target_os = "ios")))]
+pub fn is_pro() -> bool {
+    PRO.lock().unwrap().clone()
+}
diff --git a/src/ipc.rs b/src/ipc.rs
index 6a426e37d..1cbd994bb 100644
--- a/src/ipc.rs
+++ b/src/ipc.rs
@@ -70,6 +70,8 @@ pub enum FS {
         file_num: i32,
         files: Vec<(String, u64)>,
         overwrite_detection: bool,
+        total_size: u64,
+        conn_id: i32,
     },
     CancelWrite {
         id: i32,
@@ -152,6 +154,7 @@ pub enum DataPortableService {
     Pong,
     ConnCount(Option<usize>),
     Mouse((Vec<u8>, i32)),
+    Pointer((Vec<u8>, i32)),
     Key(Vec<u8>),
     RequestStart,
     WillClose,
@@ -228,6 +231,9 @@ pub enum Data {
     #[cfg(all(feature = "flutter", feature = "plugin_framework"))]
     #[cfg(not(any(target_os = "android", target_os = "ios")))]
     Plugin(Plugin),
+    #[cfg(windows)]
+    SyncWinCpuUsage(Option<f64>),
+    FileTransferLog(String),
 }
 
 #[tokio::main(flavor = "current_thread")]
@@ -347,7 +353,7 @@ async fn handle(data: Data, stream: &mut Connection) {
             }
         }
         Data::OnlineStatus(_) => {
-            let x = config::get_online_statue();
+            let x = config::get_online_state();
             let confirmed = Config::get_key_confirmed();
             allow_err!(stream.send(&Data::OnlineStatus(Some((x, confirmed)))).await);
         }
@@ -451,6 +457,16 @@ async fn handle(data: Data, stream: &mut Connection) {
                     .await
             );
         }
+        #[cfg(windows)]
+        Data::SyncWinCpuUsage(None) => {
+            allow_err!(
+                stream
+                    .send(&Data::SyncWinCpuUsage(
+                        hbb_common::platform::windows::cpu_uage_one_minute()
+                    ))
+                    .await
+            );
+        }
         Data::TestRendezvousServer => {
             crate::test_rendezvous_server();
         }
diff --git a/src/keyboard.rs b/src/keyboard.rs
index e6e9da35a..ca12cf4a1 100644
--- a/src/keyboard.rs
+++ b/src/keyboard.rs
@@ -52,6 +52,9 @@ lazy_static::lazy_static! {
 
 pub mod client {
     use super::*;
+    lazy_static::lazy_static! {
+        static ref IS_GRAB_STARTED: Arc<Mutex<bool>> = Arc::new(Mutex::new(false));
+    }
 
     pub fn get_keyboard_mode() -> String {
         #[cfg(not(any(feature = "flutter", feature = "cli")))]
@@ -70,7 +73,12 @@ pub mod client {
     }
 
     pub fn start_grab_loop() {
+        let mut lock = IS_GRAB_STARTED.lock().unwrap();
+        if *lock {
+            return;
+        }
         super::start_grab_loop();
+        *lock = true;
     }
 
     #[cfg(not(any(target_os = "android", target_os = "ios")))]
@@ -708,7 +716,7 @@ pub fn legacy_keyboard_mode(event: &Event, mut key_event: KeyEvent) -> Vec<KeyEv
         Key::Final => Some(ControlKey::Final),
         Key::Hanja => Some(ControlKey::Hanja),
         Key::Hanji => Some(ControlKey::Hanja),
-        Key::Convert => Some(ControlKey::Convert),
+        Key::Lang2 => Some(ControlKey::Convert),
         Key::Print => Some(ControlKey::Print),
         Key::Select => Some(ControlKey::Select),
         Key::Execute => Some(ControlKey::Execute),
diff --git a/src/lang.rs b/src/lang.rs
index 6de810f23..75c067e14 100644
--- a/src/lang.rs
+++ b/src/lang.rs
@@ -33,6 +33,7 @@ mod tw;
 mod ua;
 mod vn;
 mod lt;
+mod ar;
 
 pub const LANGS: &[(&str, &str)] = &[
     ("en", "English"),
@@ -68,6 +69,7 @@ pub const LANGS: &[(&str, &str)] = &[
     ("sl", "Slovenščina"),
     ("ro", "Română"),
     ("lt", "Lietuvių"),
+    ("ar", "العربية"),
 ];
 
 #[cfg(not(any(target_os = "android", target_os = "ios")))]
@@ -132,6 +134,7 @@ pub fn translate_locale(name: String, locale: &str) -> String {
         "sl" => sl::T.deref(),
         "ro" => ro::T.deref(),
         "lt" => lt::T.deref(),
+        "ar" => ar::T.deref(),
         _ => en::T.deref(),
     };
     if let Some(v) = m.get(&name as &str) {
diff --git a/src/lang/ar.rs b/src/lang/ar.rs
new file mode 100644
index 000000000..0d9bdacd3
--- /dev/null
+++ b/src/lang/ar.rs
@@ -0,0 +1,547 @@
+lazy_static::lazy_static! {
+pub static ref T: std::collections::HashMap<&'static str, &'static str> =
+    [
+        ("Status", "الحالة"),
+        ("Your Desktop", "سطح مكتبك"),
+        ("desk_tip", "يمكن الوصول لسطح مكتبك بهذا المعرف والرقم السري."),
+        ("Password", "كلمة المرور"),
+        ("Ready", "جاهز"),
+        ("Established", "تم الانشاء"),
+        ("connecting_status", "جاري الاتصال بشبكة RustDesk..."),
+        ("Enable Service", "تفعيل الخدمة"),
+        ("Start Service", "بدء الخدمة"),
+        ("Service is running", "الخدمة تعمل"),
+        ("Service is not running", "الخدمة لا تعمل"),
+        ("not_ready_status", "غير جاهز. الرجاء التأكد من الاتصال"),
+        ("Control Remote Desktop", "التحكم بسطح المكتب البعيد"),
+        ("Transfer File", "نقل ملف"),
+        ("Connect", "اتصال"),
+        ("Recent Sessions", "الجلسات الحديثة"),
+        ("Address Book", "كتاب العناوين"),
+        ("Confirmation", "التأكيد"),
+        ("TCP Tunneling", "نفق TCP"),
+        ("Remove", "ازالة"),
+        ("Refresh random password", "تحديث كلمة مرور عشوائية"),
+        ("Set your own password", "تعيين كلمة مرور خاصة بك"),
+        ("Enable Keyboard/Mouse", "تفعيل لوحة المفاتيح/الفأرة"),
+        ("Enable Clipboard", "تفعيل الحافظة"),
+        ("Enable File Transfer", "تفعيل نقل الملفات"),
+        ("Enable TCP Tunneling", "تفعيل نفق TCP"),
+        ("IP Whitelisting", "القائمة البيضاء للـ IP"),
+        ("ID/Relay Server", "معرف خادم الوسيط"),
+        ("Import Server Config", "استيراد إعدادات الخادم"),
+        ("Export Server Config", "تصدير إعدادات الخادم"),
+        ("Import server configuration successfully", "تم استيراد إعدادات الخادم بنجاح"),
+        ("Export server configuration successfully", "تم تصدير إعدادات الخادم بنجاح"),
+        ("Invalid server configuration", "إعدادات الخادم غير صحيحة"),
+        ("Clipboard is empty", "الحافظة فارغة"),
+        ("Stop service", "إيقاف الخدمة"),
+        ("Change ID", "تغيير المعرف"),
+        ("Your new ID", "معرفك الجديد"),
+        ("length %min% to %max%", "الطول من %min% الى %max%"),
+        ("starts with a letter", "يبدأ بحرف"),
+        ("allowed characters", "الحروف المسموح بها"),
+        ("id_change_tip", "فقط a-z, A-Z, 0-9 و _ مسموح بها. اول حرف يجب ان يكون a-z او A-Z. الطول بين 6 و 16."),
+        ("Website", "الموقع"),
+        ("About", "عن"),
+        ("Slogan_tip", "صنع بحب في هذا العالم الفوضوي!"),
+        ("Privacy Statement", "بيان الخصوصية"),
+        ("Mute", "كتم"),
+        ("Build Date", "تاريخ البناء"),
+        ("Version", "النسخة"),
+        ("Home", "المنزل"),
+        ("Audio Input", "مصدر الصوت"),
+        ("Enhancements", "التحسينات"),
+        ("Hardware Codec", "ترميز العتاد"),
+        ("Adaptive bitrate", "معدل بت متكيف"),
+        ("ID Server", "معرف الخادم"),
+        ("Relay Server", "خادم الوسيط"),
+        ("API Server", "خادم API"),
+        ("invalid_http", "يجب ان يبدأ بـ http:// او https://"),
+        ("Invalid IP", "عنوان IP غير صحيح"),
+        ("Invalid format", "صيغة غير صحيحة"),
+        ("server_not_support", "الخادم غير مدعوم"),
+        ("Not available", "غير متوفر"),
+        ("Too frequent", "متكرر جدا"),
+        ("Cancel", "إلغاء الأمر"),
+        ("Skip", "تجاوز"),
+        ("Close", "إغلاق"),
+        ("Retry", "إعادة المحاولة"),
+        ("OK", "موافق"),
+        ("Password Required", "كلمة المرور اجبارية"),
+        ("Please enter your password", "الرجاء كتابة كلمة المرور"),
+        ("Remember password", "تذكر كلمة المرور"),
+        ("Wrong Password", "كلمة مرور خاطئة"),
+        ("Do you want to enter again?", "هل تريد الادخال مرة اخرى؟"),
+        ("Connection Error", "خطأ غي الاتصال"),
+        ("Error", "خطأ"),
+        ("Reset by the peer", "تمت اعادة التعيين بواسطة القرين"),
+        ("Connecting...", "جاري الاتصال..."),
+        ("Connection in progress. Please wait.", "جاري الاتصال, الرجاء الانتظار..."),
+        ("Please try 1 minute later", "الرجاء المحاولة بعد دقيقة واحدة"),
+        ("Login Error", "خطأ في تسجيل الدخول"),
+        ("Successful", "نجاح"),
+        ("Connected, waiting for image...", "متصل, جاري انتظار الصورة..."),
+        ("Name", "الاسم"),
+        ("Type", "النوع"),
+        ("Modified", "آخر تعديل"),
+        ("Size", "الحجم"),
+        ("Show Hidden Files", "عرض الملفات المخفية"),
+        ("Receive", "استقبال"),
+        ("Send", "ارسال"),
+        ("Refresh File", "تحديث الملف"),
+        ("Local", "المحلي"),
+        ("Remote", "البعيد"),
+        ("Remote Computer", "الحاسب البعيد"),
+        ("Local Computer", "الحاسب المحلي"),
+        ("Confirm Delete", "تأكيد الحذف"),
+        ("Delete", "حذف"),
+        ("Properties", "الخصائص"),
+        ("Multi Select", "اختيار متعدد"),
+        ("Select All", "تحديد الكل"),
+        ("Unselect All", "الغاء تحديد الكل"),
+        ("Empty Directory", "مجلد فارغ"),
+        ("Not an empty directory", "مجلد غير فارغ"),
+        ("Are you sure you want to delete this file?", "هل انت متأكد من أنك تريد حذف هذا الملف؟"),
+        ("Are you sure you want to delete this empty directory?", "هل انت متأكد من أنك تريد حذف هذا المجلد؟"),
+        ("Are you sure you want to delete the file of this directory?", "هل انت متأكد من أنك تريد حذف ملفات هذا المجلد؟"),
+        ("Do this for all conflicts", "فعل هذا لكل التعارضات"),
+        ("This is irreversible!", "لا يمكن التراجع عن هذا!"),
+        ("Deleting", "جاري الحذف"),
+        ("files", "ملفات"),
+        ("Waiting", "انتظار"),
+        ("Finished", "انتهى"),
+        ("Speed", "السرعة"),
+        ("Custom Image Quality", "جودة صورة مخصصة"),
+        ("Privacy mode", "وضع الخصوصية"),
+        ("Block user input", "حجم ادخال المستخدم"),
+        ("Unblock user input", "الغاء حجب ادخال المستخدم"),
+        ("Adjust Window", "ضبط النافذة"),
+        ("Original", "الاصلي"),
+        ("Shrink", "تقليص"),
+        ("Stretch", "تمديد"),
+        ("Scrollbar", "شريط التمرير"),
+        ("ScrollAuto", "التمرير التلقائي"),
+        ("Good image quality", "دقة صورة جيدة"),
+        ("Balanced", "متوازن"),
+        ("Optimize reaction time", "تحسين وقت رد الفعل"),
+        ("Custom", "مخصص"),
+        ("Show remote cursor", "عرض مؤشر الجهاز البعيد"),
+        ("Show quality monitor", "عرض مراقب الجودة"),
+        ("Disable clipboard", "تعطيل الحافظة"),
+        ("Lock after session end", "القفل بعد نهاية هذه الجلسة"),
+        ("Insert", "ادخال"),
+        ("Insert Lock", "قفل الادخال"),
+        ("Refresh", "تحديث"),
+        ("ID does not exist", "المعرف غير موجود"),
+        ("Failed to connect to rendezvous server", "فشل الاتصال بخادم rendezvous"),
+        ("Please try later", "الرجاء المحاولة لاحقا"),
+        ("Remote desktop is offline", "سطح المكتب البعيد غير متصل"),
+        ("Key mismatch", "المفتاح غير متطابق"),
+        ("Timeout", "نفذ الوقت"),
+        ("Failed to connect to relay server", "فشل الاتصال بالخادم الوسيط"),
+        ("Failed to connect via rendezvous server", "فشل الاتصال عير خادم rendezvous"),
+        ("Failed to connect via relay server", "فشل الاتصال عبر الخادم الوسيط"),
+        ("Failed to make direct connection to remote desktop", "فشل في اجراء اتصال مباشر لسطح المكتب البعيد"),
+        ("Set Password", "ضبط كلمة المرور"),
+        ("OS Password", "كلمة مرور نظام التشغيل"),
+        ("install_tip", "بسبب صلاحيات تحكم حساب المستخدم. RustDesk قد لا يعمل بشكل صحيح في جهة البعيد في بعض الحالات. لتفادي ذلك. الرجاء الضغط على الزر ادناه لتثبيت RustDesk في جهازك."),
+        ("Click to upgrade", "اضغط للارتقاء"),
+        ("Click to download", "اضغط للتنزيل"),
+        ("Click to update", "ضغط للتحديث"),
+        ("Configure", "تهيئة"),
+        ("config_acc", "لتتمكن من التحكم بسطح مكتبك البعيد, تحتاج الى منح RustDesk اذونات \"امكانية الوصول\"."),
+        ("config_screen", "لتتمكن من الوصول الى سطح مكتبك البعيد, تحتاج الى منح RustDesk اذونات \"تسجيل الشاشة\"."),
+        ("Installing ...", "جاري التثبيت..."),
+        ("Install", "تثبيت"),
+        ("Installation", "التثبيت"),
+        ("Installation Path", "مسار التثبيت"),
+        ("Create start menu shortcuts", "انشاء اختصارات قائمة ابداء"),
+        ("Create desktop icon", "انشاء اختصار سطح المكتب"),
+        ("agreement_tip", "بمجرد البدء بالتثبيت, فانت قد قبلت اتفاقية الترخيص."),
+        ("Accept and Install", "الموافقة والتثبيت"),
+        ("End-user license agreement", "اتفاقية ترخيص المستخدم النهائي"),
+        ("Generating ...", "جاري الانشاء..."),
+        ("Your installation is lower version.", "انت تحاول تثبيت نسخة قديمة."),
+        ("not_close_tcp_tip", "لا تغلق هذه النافذة اثناء استخدامك للنفق"),
+        ("Listening ...", "جاري الاستماع..."),
+        ("Remote Host", "المستضيف البعيد"),
+        ("Remote Port", "منفذ المستضيف البعيد"),
+        ("Action", "فعل"),
+        ("Add", "اضافة"),
+        ("Local Port", "المنفذ المحلي"),
+        ("Local Address", "العنوان المحلي"),
+        ("Change Local Port", "تغيير المنفذ المحلي"),
+        ("setup_server_tip", "لاتصال اسرع, الرجاء اعداد خادم خاص بك"),
+        ("Too short, at least 6 characters.", "قصير جدا. يجب ان يكون على الاقل 6 خانات"),
+        ("The confirmation is not identical.", "التأكيد غير متطابق"),
+        ("Permissions", "الاذونات"),
+        ("Accept", "قبول"),
+        ("Dismiss", "صرف"),
+        ("Disconnect", "قطع الاتصال"),
+        ("Allow using keyboard and mouse", "السماح باستخدام لوحة المفاتيح والفأرة"),
+        ("Allow using clipboard", "السماح باستخدام الحافظة"),
+        ("Allow hearing sound", "السماح بسماع الصوت"),
+        ("Allow file copy and paste", "السماح بالنسخ واللصق"),
+        ("Connected", "متصل"),
+        ("Direct and encrypted connection", "اتصال مباشر مشفر"),
+        ("Relayed and encrypted connection", "اتصال غير مباشر مشفر"),
+        ("Direct and unencrypted connection", "اتصال مباشر غير مشفر"),
+        ("Relayed and unencrypted connection", "اتصال غير مباشر غير مشفر"),
+        ("Enter Remote ID", "ادخل المعرف البعيد"),
+        ("Enter your password", "ادخل كلمة المرور"),
+        ("Logging in...", "جاري تسجيل الدخول..."),
+        ("Enable RDP session sharing", "تفعيل مشاركة الجلسة باستخدام RDP"),
+        ("Auto Login", "تسجيل دخول تلقائي"),
+        ("Enable Direct IP Access", "تفعيل الوصول المباشر لعنوان IP"),
+        ("Rename", "اعادة تسمية"),
+        ("Space", "مساحة"),
+        ("Create Desktop Shortcut", "انشاء اختصار سطح مكتب"),
+        ("Change Path", "تغيير المسار"),
+        ("Create Folder", "انشاء مجلد"),
+        ("Please enter the folder name", "الرجاء ادخال اسم المجلد"),
+        ("Fix it", "اصلحه"),
+        ("Warning", "تحذير"),
+        ("Login screen using Wayland is not supported", "تسجيل الدخول باستخدام Wayland غير مدعوم"),
+        ("Reboot required", "يجب اعادة التشغيل"),
+        ("Unsupported display server", "خادم العرض غير مدعوم"),
+        ("x11 expected", "x11 متوقع"),
+        ("Port", "المنفذ"),
+        ("Settings", "الاعدادات"),
+        ("Username", "اسم المستخدم"),
+        ("Invalid port", "منفذ خاطئ"),
+        ("Closed manually by the peer", "اغلاق يدويا بواسطة القرين"),
+        ("Enable remote configuration modification", "تفعيل تعديل اعدادات البعيد"),
+        ("Run without install", "تشغيل بدون تثبيت"),
+        ("Connect via relay", "الاتصال عبر وسيط"),
+        ("Always connect via relay", "الاتصال باستخدام وسيط دائما"),
+        ("whitelist_tip", "فقط قائمة الـ IP البيضاء تستطيع الوصول لي"),
+        ("Login", "تسجيل الدخول"),
+        ("Verify", "تأكيد"),
+        ("Remember me", "تذكرني"),
+        ("Trust this device", "الوثوق بهذا الجهاز"),
+        ("Verification code", "رمز التحقق"),
+        ("verification_tip", "رمز التحقق ارسل الى بريدك الالكتروني المسجل. ادخل رمز التحقق للاستمرار بتسجيل الدخول."),
+        ("Logout", "تسجيل الخروج"),
+        ("Tags", "العلامات"),
+        ("Search ID", "البحث المعرف"),
+        ("whitelist_sep", "مفصولة بفاصلة او فاصلة منقوطة او سطر جديد"),
+        ("Add ID", "اضافة معرف"),
+        ("Add Tag", "اضافة علامة"),
+        ("Unselect all tags", "عدم تحديد كل العلامات"),
+        ("Network error", "خطأ شبكة"),
+        ("Username missed", "اسم المستخدم مفقود"),
+        ("Password missed", "كلمة المرور مفقودة"),
+        ("Wrong credentials", "اسم مستخدم او كلمة مرور خاطئة"),
+        ("The verification code is incorrect or has expired", "رمز التحقق غير صحيح او منتهي"),
+        ("Edit Tag", "تحرير علامة"),
+        ("Unremember Password", "عدم تذكر كلمة المرور"),
+        ("Favorites", "المفضلة"),
+        ("Add to Favorites", "اضافة للمفضلة"),
+        ("Remove from Favorites", "ازالة من المفضلة"),
+        ("Empty", "فارغ"),
+        ("Invalid folder name", "اسم المجلد غير صحيح"),
+        ("Socks5 Proxy", "وكيل Socks5"),
+        ("Hostname", "اسم المستضيف"),
+        ("Discovered", "المكتشفة"),
+        ("install_daemon_tip", "للبدء مع بدء تشغيل النظام. تحتاج الى تثبيت خدمة النظام."),
+        ("Remote ID", "المعرف البعيد"),
+        ("Paste", "لصق"),
+        ("Paste here?", "لصق هنا؟"),
+        ("Are you sure to close the connection?", "هل انت متاكد من انك تريد اغلاق هذا الاتصال؟"),
+        ("Download new version", "تنويل نسخة جديدة"),
+        ("Touch mode", "وضع اللمس"),
+        ("Mouse mode", "وضع الفأرة"),
+        ("One-Finger Tap", "لم اصبع واحد"),
+        ("Left Mouse", "الفأرة اليسرى"),
+        ("One-Long Tap", "لمسة واحدة طويلة"),
+        ("Two-Finger Tap", "لمس اصبعين"),
+        ("Right Mouse", "الفأرة اليمنى"),
+        ("One-Finger Move", "نقل الاصبع الواحد"),
+        ("Double Tap & Move", "لمستان ونقل"),
+        ("Mouse Drag", "سحب الفأرة"),
+        ("Three-Finger vertically", "ثلاث اصابع افقيا"),
+        ("Mouse Wheel", "عجلة الفارة"),
+        ("Two-Finger Move", "نقل الاصبعين"),
+        ("Canvas Move", ""),
+        ("Pinch to Zoom", "قرصة للتكبير"),
+        ("Canvas Zoom", ""),
+        ("Reset canvas", ""),
+        ("No permission of file transfer", "لا يوجد اذن نقل الملف"),
+        ("Note", "ملاحظة"),
+        ("Connection", "الاتصال"),
+        ("Share Screen", "مشاركة الشاشة"),
+        ("Chat", "محادثة"),
+        ("Total", "الاجمالي"),
+        ("items", "عناصر"),
+        ("Selected", "محدد"),
+        ("Screen Capture", "لقط الشاشة"),
+        ("Input Control", "تحكم الادخال"),
+        ("Audio Capture", "لقط الصوت"),
+        ("File Connection", "اتصال الملف"),
+        ("Screen Connection", "اتصال الشاشة"),
+        ("Do you accept?", "هل تقبل؟"),
+        ("Open System Setting", "فتح اعدادات النظام"),
+        ("How to get Android input permission?", "كيف تحصل على اذن الادخال في اندرويد؟"),
+        ("android_input_permission_tip1", "لكي يتمكن جهاز بعيد من التحكم بجهازك الـ Android عن طريق الفارة أو اللمس، يلزمك السماح لـ RustDesk باستخدام خدمة \"إمكانية الوصول\"."),
+        ("android_input_permission_tip2", "يرجى الانتقال إلى صفحة إعدادات النظام التالية، والعثور على [الخدمات المثبتة]، وتشغيل خدمة [RustDesk Input]."),
+        ("android_new_connection_tip", "تم استلام طلب تحكم جديد، حيث يريد التحكم بجهازك الحالي."),
+        ("android_service_will_start_tip", "تشغيل \"لقط الشاشة\" سيبدأ الخدمة تلقائيا، حيث سيسمح للاجهزة الاخرى بطلب الاتصال مع جهازك."),
+        ("android_stop_service_tip", "اغلاق الخدمة سيغلق جميع الاتصالات القائمة."),
+        ("android_version_audio_tip", "نسخة اندرويد الحالية لا تدعم خدمة لقط الصوت، الرجاء الترقية الى نسخة 10 او أحدث."),
+        ("android_start_service_tip", "اضغط تشغيل الخدمة او فعل صلاحية لقط الشاشة لبدء خدمة مشاركة الشاشة."),
+        ("android_permission_may_not_change_tip", "الاذونات الاتصالات القائمة قد لا تتغير مباشرة الا بعد اعادة الاتصال."),
+        ("Account", "الحساب"),
+        ("Overwrite", "استبدال"),
+        ("This file exists, skip or overwrite this file?", "الملف موجود, هل تريد التجاوز او الاستبدال؟"),
+        ("Quit", "خروج"),
+        ("doc_mac_permission", "https://rustdesk.com/docs/en/manual/mac/#enable-permissions"),
+        ("Help", "مساعدة"),
+        ("Failed", "فشل"),
+        ("Succeeded", "نجاح"),
+        ("Someone turns on privacy mode, exit", "شخص ما فعل وضع الخصوصية, خروج"),
+        ("Unsupported", "غير مدعوم"),
+        ("Peer denied", "القرين رفض"),
+        ("Please install plugins", "الرجاء تثبيت الاضافات"),
+        ("Peer exit", "خروج القرين"),
+        ("Failed to turn off", "فشل ايقاف التشغيل"),
+        ("Turned off", "مطفئ"),
+        ("In privacy mode", "في وضع الخصوصية"),
+        ("Out privacy mode", "الخروج من وضع الخصوصية"),
+        ("Language", "اللغة"),
+        ("Keep RustDesk background service", "ابق خدمة RustDesk تعمل في الخلفية"),
+        ("Ignore Battery Optimizations", "تجاهل تحسينات البطارية"),
+        ("android_open_battery_optimizations_tip", "اذا اردت تعطيل هذه الميزة, الرجاء الذهاب الى صفحة اعدادات تطبيق RustDesk, ابحث عن البطارية, الغ تحديد غير مقيد"),
+        ("Start on Boot", "البدء عند تشغيل النظام"),
+        ("Start the screen sharing service on boot, requires special permissions", "تشغيل خدمة مشاركة الشاشة عند بدء تشغيل النظام, يحتاج الى اذونات خاصة"),
+        ("Connection not allowed", "الاتصال غير مسموح"),
+        ("Legacy mode", "الوضع التقليدي"),
+        ("Map mode", "وضع الخريطة"),
+        ("Translate mode", "وضع الترجمة"),
+        ("Use permanent password", "استخدام كلمة مرور دائمة"),
+        ("Use both passwords", "استخدام طريقتي كلمة المرور"),
+        ("Set permanent password", "تعيين كلمة مرور دائمة"),
+        ("Enable Remote Restart", "تفعيل اعداة تشغيل البعيد"),
+        ("Allow remote restart", "السماح باعادة تشغيل البعيد"),
+        ("Restart Remote Device", "اعادة تشغيل الجهاز البعيد"),
+        ("Are you sure you want to restart", "هل انت متاكد من انك تريد اعادة التشغيل؟"),
+        ("Restarting Remote Device", "جاري اعادة تشغيل البعيد"),
+        ("remote_restarting_tip", "الجهاز البعيد يعيد التشغيل. الرجاء اغلاق هذه الرسالة واعادة الاتصال باستخدام كلمة المرور الدائمة بعد فترة بسيطة."),
+        ("Copied", "منسوخ"),
+        ("Exit Fullscreen", "الخروج من ملئ الشاشة"),
+        ("Fullscreen", "ملئ الشاشة"),
+        ("Mobile Actions", "اجراءات الهاتف"),
+        ("Select Monitor", "اختر شاشة"),
+        ("Control Actions", "اجراءات التحكم"),
+        ("Display Settings", "اعدادات العرض"),
+        ("Ratio", "النسبة"),
+        ("Image Quality", "جودة الصورة"),
+        ("Scroll Style", "شكل التمرير"),
+        ("Show Toolbar", "عرض شريط الادوات"),
+        ("Hide Toolbar", "اخفاء شريط الادوات"),
+        ("Direct Connection", "اتصال مباشر"),
+        ("Relay Connection", "اتصال الوسيط"),
+        ("Secure Connection", "اتصال آمن"),
+        ("Insecure Connection", "اتصال غير آمن"),
+        ("Scale original", "المقياس الأصلي"),
+        ("Scale adaptive", "مقياس التكيف"),
+        ("General", "عام"),
+        ("Security", "الأمان"),
+        ("Theme", "السمة"),
+        ("Dark Theme", "سمة غامقة"),
+        ("Light Theme", "سمة فاتحة"),
+        ("Dark", "غامق"),
+        ("Light", "فاتح"),
+        ("Follow System", "نفس نظام التشغيل"),
+        ("Enable hardware codec", "تفعيل ترميز العتاد"),
+        ("Unlock Security Settings", "فتح اعدادات الامان"),
+        ("Enable Audio", "تفعيل الصوت"),
+        ("Unlock Network Settings", "فتح اعدادات الشبكة"),
+        ("Server", "الخادم"),
+        ("Direct IP Access", "وصول مباشر للـ IP"),
+        ("Proxy", "الوكيل"),
+        ("Apply", "تطبيق"),
+        ("Disconnect all devices?", "قطع اتصال كل الاجهزة؟"),
+        ("Clear", "مسح"),
+        ("Audio Input Device", "جهاز ادخال الصوت"),
+        ("Use IP Whitelisting", "استخدام قائمة الـ IP البيضاء"),
+        ("Network", "الشبكة"),
+        ("Enable RDP", "تفعيل RDP"),
+        ("Pin Toolbar", "تثبيت شريط الادوات"),
+        ("Unpin Toolbar", "الغاء تثبيت شريط الادوات"),
+        ("Recording", "التسجيل"),
+        ("Directory", "المسار"),
+        ("Automatically record incoming sessions", "تسجيل الجلسات القادمة تلقائيا"),
+        ("Change", "تغيير"),
+        ("Start session recording", "بدء تسجيل الجلسة"),
+        ("Stop session recording", "ايقاف تسجيل الجلسة"),
+        ("Enable Recording Session", "تفعيل تسجيل الجلسة"),
+        ("Allow recording session", "السماح بتسجيل الجلسة"),
+        ("Enable LAN Discovery", "تفعيل اكتشاف الشبكة المحلية"),
+        ("Deny LAN Discovery", "رفض اكتشاف الشبكة المحلية"),
+        ("Write a message", "اكتب رسالة"),
+        ("Prompt", ""),
+        ("Please wait for confirmation of UAC...", "الرجاء انتظار تاكيد تحكم حساب المستخدم..."),
+        ("elevated_foreground_window_tip", "النافذة الحالية لسطح المكتب البعيد تحتاج صلاحية اعلى لتعمل, لذلك لن تستطيع استخدام الفارة ولوحة المفاتيح مؤقتا. تستطيع انت تطلب من المستخدم البعيد تصغير النافذة الحالية, او ضفط زر الارتقاء في نافذة ادارة الاتصال. لتفادي هذة المشكلة من المستحسن تثبيت البرنامج في الجهاز البعيد."),
+        ("Disconnected", "مفصول"),
+        ("Other", "اخرى"),
+        ("Confirm before closing multiple tabs", "التاكيد قبل اغلاق السنة عديدة"),
+        ("Keyboard Settings", "اعدادات لوحة المفاتيح"),
+        ("Full Access", "وصول كامل"),
+        ("Screen Share", "مشاركة الشاشة"),
+        ("Wayland requires Ubuntu 21.04 or higher version.", "Wayland يتطلب نسخة ابونتو 21.04 او اعلى."),
+        ("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "Wayland يتطلب نسخة اعلى من توزيعة لينكس. الرجاء تجربة سطح مكتب X11 او غير نظام تشغيلك."),
+        ("JumpLink", "رابط القفز"),
+        ("Please Select the screen to be shared(Operate on the peer side).", "الرجاء اختيار شاشة لمشاركتها (تعمل على جانب القرين)."),
+        ("Show RustDesk", "عرض RustDesk"),
+        ("This PC", "هذا الحاسب"),
+        ("or", "او"),
+        ("Continue with", "متابعة مع"),
+        ("Elevate", "ارتقاء"),
+        ("Zoom cursor", "تكبير المؤشر"),
+        ("Accept sessions via password", "قبول الجلسات عبر كلمة المرور"),
+        ("Accept sessions via click", "قبول الجلسات عبر الضغط"),
+        ("Accept sessions via both", "قبول الجلسات عبر الاثنين"),
+        ("Please wait for the remote side to accept your session request...", "الرجاء الانتظار حتى يقبل الطرف البعيد طلب الجلسة..."),
+        ("One-time Password", "كلمة مرور لمرة واحدة"),
+        ("Use one-time password", "استخدام كلمة مرور لمرة واحدة"),
+        ("One-time password length", "طول كلمة مرور لمرة واحدة"),
+        ("Request access to your device", "طلب الوصول إلى جهازك"),
+        ("Hide connection management window", "اخفاء نافذة ادارة الاتصال"),
+        ("hide_cm_tip", "السماح بالاخفاء فقط في حالة قبول الجلسات عبر كلمة المرور واستخدام كلمة المرور الدائمة"),
+        ("wayland_experiment_tip", "دعم Wayland لازال في المرحلة التجريبية. الرجاء استخدام X11 اذا اردت وصول كامل."),
+        ("Right click to select tabs", "الضغط بالزر الايمن لتحديد الالسنة"),
+        ("Skipped", "متجاوز"),
+        ("Add to Address Book", "اضافة الى كتاب العناوين"),
+        ("Group", "مجموعة"),
+        ("Search", "بحث"),
+        ("Closed manually by web console", "اغلق يدويا عبر طرفية الويب"),
+        ("Local keyboard type", "توع لوحة المفاتيح المحلية"),
+        ("Select local keyboard type", "اختر نوع لوحة المفاتيح الملحية"),
+        ("software_render_tip", "اذا كنت تستخدم بطاقة رسوميات Nvidia تحت لينكس والشاشة البعيد تغلق مباشرة بعد الاتصال, قم بالتبديل الى تعريفات Nouveau مفتوحة المصدر واختيار الترميز البرمجي قد يساعد. اعادة تشغيل البرناج مطلوبة."),
+        ("Always use software rendering", "استخدام الترميز البرمجي دائما"),
+        ("config_input", "لتتمكن من التحكم بسطح المكتب البعيد. يجب من RustDesk اذونات \"مراقبة المدخلات\"."),
+        ("config_microphone", "لتتمكن من التحدث. يجب منح RustDesk اذونات \"تسجيل الصوت\"."),
+        ("request_elevation_tip", "اطلب الارتقاء في حالة وجود شخص في الطرف الاخر."),
+        ("Wait", "انتظر"),
+        ("Elevation Error", "خطأ الارتقاء"),
+        ("Ask the remote user for authentication", "اسأل المستخدم البعيد التوثيق"),
+        ("Choose this if the remote account is administrator", "اختر اذا كان الحساب البعيد مدير للنظام"),
+        ("Transmit the username and password of administrator", "انقل اسم المستخدم وكلمة مرور مدير النظام"),
+        ("still_click_uac_tip", "لازال يحتاج المستخدم البعيد للضغط على موافق في نافذة تحكم حساب المستخدم في RustDesk الذي يعمل."),
+        ("Request Elevation", "طلب ارتقاء"),
+        ("wait_accept_uac_tip", "الرجاء انتظار المستخدم البعيد حتى يوافق على طلب تحكم حساب المستخدم."),
+        ("Elevate successfully", "الارتقاء بنجاح"),
+        ("uppercase", "حرف كبير"),
+        ("lowercase", "حرف صغير"),
+        ("digit", "رقم"),
+        ("special character", "رمز"),
+        ("length>=8", "الطول 8 على الاقل"),
+        ("Weak", "ضعيف"),
+        ("Medium", "متوسط"),
+        ("Strong", "قوي"),
+        ("Switch Sides", "تبديل الاماكن"),
+        ("Please confirm if you want to share your desktop?", "الرجاء التاكيد على انك تريد مشاركة سطح مكتبك؟"),
+        ("Display", "العرض"),
+        ("Default View Style", "شكل العرض الافتراضي"),
+        ("Default Scroll Style", "شكل التمرير الافتراضي"),
+        ("Default Image Quality", "دقة الصورة الافتراضية"),
+        ("Default Codec", "الترميز الاقتراضي"),
+        ("Bitrate", "معدل البت"),
+        ("FPS", "مشهد في الثانية"),
+        ("Auto", "تلقائي"),
+        ("Other Default Options", "الخيارات الافتراضية الاخرى"),
+        ("Voice call", "مكالمة صوتية"),
+        ("Text chat", "دردشة نصية"),
+        ("Stop voice call", "ايقاف المكالمة الصوتية"),
+        ("relay_hint_tip", "قد لا يكون ممكن الاتصال مباشرة. يمكنك محاولة الاتصال عبر وسيط. ايضا اذا اردت استخدام وسيط لمحاولتك الاولى اضف لاحقة \"/r\" الى المعرف او اختر \"الاتصال باستخدام وسيط دائما\" في بطاقة الجلسات الحديثة ان وجدت."),
+        ("Reconnect", "اعادة الاتصال"),
+        ("Codec", "الترميز"),
+        ("Resolution", "الدقة"),
+        ("No transfers in progress", "لا توجد عمليات نقل حاليا"),
+        ("Set one-time password length", "تعيين طول كلمة مرور المرة الواحدة"),
+        ("install_cert_tip", "تثبيت شهادة RustDesk"),
+        ("confirm_install_cert_tip", "هذه شهادة RustDesk الاختبارية, يمكنك الوثوق بها. هذه الشهادة ستستخدم للوثوق وتثبيت تعاريف RustDesk عند الحاجة."),
+        ("RDP Settings", "اعدادات RDP"),
+        ("Sort by", "ترتيب حسب"),
+        ("New Connection", "اتصال جديد"),
+        ("Restore", "استعادة"),
+        ("Minimize", "تصغير"),
+        ("Maximize", "تكبير"),
+        ("Your Device", "جهازك"),
+        ("empty_recent_tip", "للاسف. لا توجد جلسات حديثة\nحان الوقت للتخطيط لواحدة جديدة."),
+        ("empty_favorite_tip", "لا يوجد اقران مفضلين حتى الان؟\nحسنا لنبحث عن شخص للاتصال معه ومن ثم اضافته للمفضلة."),
+        ("empty_lan_tip", "اه لا, يبدو انك لم تكتشف اي قرين بعد."),
+        ("empty_address_book_tip", "يا عزيزي, يبدو انه لايوجد حاليا اي اقران في كتاب العناوين."),
+        ("eg: admin", "مثلا: admin"),
+        ("Empty Username", "اسم مستخدم فارغ"),
+        ("Empty Password", "كلمة مرور فارغة"),
+        ("Me", "انا"),
+        ("identical_file_tip", "هذا الملف مطابق لملف موجود عن القرين."),
+        ("show_monitors_tip", "عرض الشاشات في شريط الادوات"),
+        ("View Mode", "وضع العرض"),
+        ("login_linux_tip", "تحتاج الى تسجيل الدخول حساب لينكس البعيد وتفعيل جلسة سطح مكتب X"),
+        ("verify_rustdesk_password_tip", "تحقق من كلمة مرور RustDesk"),
+        ("remember_account_tip", "تذكر هذا الحساب"),
+        ("os_account_desk_tip", "هذا الحساب مستخدم لتسجيل الدخول الى سطح المكتب البعيد وتفعيل الجلسة"),
+        ("OS Account", "حساب نظام التشغيل"),
+        ("another_user_login_title_tip", "مستخدم اخر مسجل دخول حاليا"),
+        ("another_user_login_text_tip", "قطع الاتصال"),
+        ("xorg_not_found_title_tip", "Xorg غير موجود"),
+        ("xorg_not_found_text_tip", "الرجاء تثبيت Xorg"),
+        ("no_desktop_title_tip", "لا يتوفر سطح مكتب"),
+        ("no_desktop_text_tip", "الرجاء تثبيت سطح مكتب GNOME"),
+        ("No need to elevate", "لا حاجة للارتقاء"),
+        ("System Sound", "صوت النظام"),
+        ("Default", "الافتراضي"),
+        ("New RDP", "RDP جديد"),
+        ("Fingerprint", "البصمة"),
+        ("Copy Fingerprint", "نسخ البصمة"),
+        ("no fingerprints", "لا توجد بصمات اصابع"),
+        ("Select a peer", "اختر قرين"),
+        ("Select peers", "اختر الاقران"),
+        ("Plugins", "الاضافات"),
+        ("Uninstall", "الغاء التثبيت"),
+        ("Update", "تحديث"),
+        ("Enable", "تفعيل"),
+        ("Disable", "تعطيل"),
+        ("Options", "الخيارات"),
+        ("resolution_original_tip", "الدقة الأصلية"),
+        ("resolution_fit_local_tip", "تناسب الدقة المحلية"),
+        ("resolution_custom_tip", "دقة مخصصة"),
+        ("Collapse toolbar", "طي شريط الادوات"),
+        ("Accept and Elevate", "قبول وارتقاء"),
+        ("accept_and_elevate_btn_tooltip", "قبول الاتصال وارتقاء صلاحيات التحكم بصلاحيات المستخدم."),
+        ("clipboard_wait_response_timeout_tip", "انتهى وقت الانتظار لنسخ الرد."),
+        ("Incoming connection", "اتصال قادم"),
+        ("Outgoing connection", "اتصال مغادر"),
+        ("Exit", "خروج"),
+        ("Open", "فتح"),
+        ("logout_tip", "هل انت متاكد من انك تريد تسجيل الخروج"),
+        ("Service", "الخدمة"),
+        ("Start", "تشغيل"),
+        ("Stop", "ايقاف"),
+        ("exceed_max_devices", "لقد وصلت الحد الأقصى لعدد الاجهزة التي يمكن دارتها."),
+        ("Sync with recent sessions", "المزامنة مع الجلسات الحديثة"),
+        ("Sort tags", "ترتيب العلامات"),
+        ("Open connection in new tab", "فتح اتصال في لسان جديد"),
+        ("Move tab to new window", "نقل اللسان الى نافذة جديدة"),
+        ("Can not be empty", "لا يمكن ان يكون فارغ"),
+        ("Already exists", "موجود مسبقا"),
+        ("Change Password", "تغيير كلمة المرور"),
+        ("Refresh Password", "تحديث كلمة المرور"),
+        ("ID", "المعرف"),
+        ("Grid View", "عرض شبكي"),
+        ("List View", "رعض قائمة"),
+        ("Select", "اختيار"),
+        ("Toggle Tags", "تفعيل/تعطيل العلامات"),
+        ("pull_ab_failed_tip", "فشل تحديث كتاب العناوين"),
+        ("push_ab_failed_tip", "فشل مزامنة كتاب العناوين مع الخادم"),
+        ("synced_peer_readded_tip", "الاجهزة الموجودة في الجلسات الحديثة سيتم مزامنتها مع كتاب العناوين"),
+        ("Change Color", ""),
+        ("Primary Color", ""),
+        ("HSV Color", ""),
+        ("Installation Successful!", ""),
+        ("Installation failed!", ""),
+    ].iter().cloned().collect();
+}
diff --git a/src/lang/ca.rs b/src/lang/ca.rs
index b5163f68e..1ec5d78f3 100644
--- a/src/lang/ca.rs
+++ b/src/lang/ca.rs
@@ -53,7 +53,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
         ("Audio Input", "Entrada d'àudio"),
         ("Enhancements", "Millores"),
         ("Hardware Codec", "Còdec de hardware"),
-        ("Adaptive Bitrate", "Tasa de bits adaptativa"),
+        ("Adaptive bitrate", "Tasa de bits adaptativa"),
         ("ID Server", "Servidor de IDs"),
         ("Relay Server", "Servidor Relay"),
         ("API Server", "Servidor API"),
@@ -233,6 +233,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
         ("Username missed", "Nom d'usuari oblidat"),
         ("Password missed", "Contrasenya oblidada"),
         ("Wrong credentials", "Credencials incorrectes"),
+        ("The verification code is incorrect or has expired", ""),
         ("Edit Tag", "Editar tag"),
         ("Unremember Password", "Contrasenya oblidada"),
         ("Favorites", "Preferits"),
@@ -459,8 +460,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
         ("Resolution", ""),
         ("No transfers in progress", ""),
         ("Set one-time password length", ""),
-        ("idd_driver_tip", ""),
-        ("confirm_idd_driver_tip", ""),
+        ("install_cert_tip", ""),
+        ("confirm_install_cert_tip", ""),
         ("RDP Settings", ""),
         ("Sort by", ""),
         ("New Connection", ""),
@@ -512,5 +513,35 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
         ("Accept and Elevate", ""),
         ("accept_and_elevate_btn_tooltip", ""),
         ("clipboard_wait_response_timeout_tip", ""),
+        ("Incoming connection", ""),
+        ("Outgoing connection", ""),
+        ("Exit", ""),
+        ("Open", ""),
+        ("logout_tip", ""),
+        ("Service", ""),
+        ("Start", ""),
+        ("Stop", ""),
+        ("exceed_max_devices", ""),
+        ("Sync with recent sessions", ""),
+        ("Sort tags", ""),
+        ("Open connection in new tab", ""),
+        ("Move tab to new window", ""),
+        ("Can not be empty", ""),
+        ("Already exists", ""),
+        ("Change Password", ""),
+        ("Refresh Password", ""),
+        ("ID", ""),
+        ("Grid View", ""),
+        ("List View", ""),
+        ("Select", ""),
+        ("Toggle Tags", ""),
+        ("pull_ab_failed_tip", ""),
+        ("push_ab_failed_tip", ""),
+        ("synced_peer_readded_tip", ""),
+        ("Change Color", ""),
+        ("Primary Color", ""),
+        ("HSV Color", ""),
+        ("Installation Successful!", ""),
+        ("Installation failed!", ""),
     ].iter().cloned().collect();
 }
diff --git a/src/lang/cn.rs b/src/lang/cn.rs
index 7fc27f687..8e07a2965 100644
--- a/src/lang/cn.rs
+++ b/src/lang/cn.rs
@@ -53,7 +53,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
         ("Audio Input", "音频输入"),
         ("Enhancements", "增强功能"),
         ("Hardware Codec", "硬件编解码"),
-        ("Adaptive Bitrate", "自适应码率"),
+        ("Adaptive bitrate", "自适应码率"),
         ("ID Server", "ID 服务器"),
         ("Relay Server", "中继服务器"),
         ("API Server", "API 服务器"),
@@ -233,6 +233,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
         ("Username missed", "用户名没有填写"),
         ("Password missed", "密码没有填写"),
         ("Wrong credentials", "提供的登录信息错误"),
+        ("The verification code is incorrect or has expired", "验证码错误或已超时"),
         ("Edit Tag", "修改标签"),
         ("Unremember Password", "忘记密码"),
         ("Favorites", "收藏"),
@@ -453,14 +454,14 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
         ("Voice call", "语音通话"),
         ("Text chat", "文字聊天"),
         ("Stop voice call", "停止语音通话"),
-        ("relay_hint_tip", "可能无法直连,可以尝试中继连接。\n另外,如果想直接使用中继连接,可以在 ID 后面添加/r,或者在卡片选项里选择强制走中继连接。"),
+        ("relay_hint_tip", "可能无法直连,可以尝试中继连接。\n另外,如果想直接使用中继连接,可以在 ID 后面添加/r,如果最近访问里存在该卡片,也可以在卡片选项里选择强制走中继连接。"),
         ("Reconnect", "重连"),
         ("Codec", "编解码"),
         ("Resolution", "分辨率"),
         ("No transfers in progress", "无进行中的传输"),
         ("Set one-time password length", "设置一次性密码长度"),
-        ("idd_driver_tip", "安装虚拟显示器驱动,以便在没有连接显示器的情况下启动虚拟显示器进行控制。"),
-        ("confirm_idd_driver_tip", "安装虚拟显示器驱动的选项已勾选。请注意,测试证书将被安装以信任虚拟显示器驱动。测试证书仅会用于信任Rustdesk的驱动。"),
+        ("install_cert_tip", "安装 RustDesk 证书"),
+        ("confirm_install_cert_tip", "此证书为 RustDesk 测试证书,您可以信任此证书。证书将被用于信任和安装 RustDesk 驱动。"),
         ("RDP Settings", "RDP 设置"),
         ("Sort by", "排序方式"),
         ("New Connection", "新连接"),
@@ -512,5 +513,35 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
         ("Accept and Elevate", "接受并提权"),
         ("accept_and_elevate_btn_tooltip", "接受连接并提升 UAC 权限"),
         ("clipboard_wait_response_timeout_tip", "等待拷贝响应超时"),
+        ("Incoming connection", "收到的连接"),
+        ("Outgoing connection", "发起的连接"),
+        ("Exit", "退出"),
+        ("Open", "打开"),
+        ("logout_tip", "确定要退出登录吗?"),
+        ("Service", "服务"),
+        ("Start", "启动"),
+        ("Stop", "停止"),
+        ("exceed_max_devices", "管理的设备数已达到最大值"),
+        ("Sync with recent sessions", "同步最近会话"),
+        ("Sort tags", "对标签进行排序"),
+        ("Open connection in new tab", "在选项卡中打开新连接"),
+        ("Move tab to new window", "将标签页移至新窗口"),
+        ("Can not be empty", "不能为空"),
+        ("Already exists", "已经存在"),
+        ("Change Password", "更改密码"),
+        ("Refresh Password", "刷新密码"),
+        ("ID", "ID"),
+        ("Grid View", "网格视图"),
+        ("List View", "列表视图"),
+        ("Select", "选择"),
+        ("Toggle Tags", "切换标签"),
+        ("pull_ab_failed_tip", "未成功获取地址簿"),
+        ("push_ab_failed_tip", "未成功上传地址簿"),
+        ("synced_peer_readded_tip", "最近会话中存在的设备将会被重新同步到地址簿。"),
+        ("Change Color", "更改颜色"),
+        ("Primary Color", "基本色"),
+        ("HSV Color", "HSV 色"),
+        ("Installation Successful!", "安装成功!"),
+        ("Installation failed!", "安装失败!"),
     ].iter().cloned().collect();
 }
diff --git a/src/lang/cs.rs b/src/lang/cs.rs
index 5f279c7b9..5dd4dc9a3 100644
--- a/src/lang/cs.rs
+++ b/src/lang/cs.rs
@@ -3,18 +3,18 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
     [
         ("Status", "Stav"),
         ("Your Desktop", "Vaše plocha"),
-        ("desk_tip", "Pomocí tohoto identifikátoru a hesla můžete přistupovat ke své ploše."),
+        ("desk_tip", "Pomocí tohoto ID a hesla lze přistupovat k pracovní ploše."),
         ("Password", "Heslo"),
         ("Ready", "Připraveno"),
         ("Established", "Navázáno"),
-        ("connecting_status", "Připojování se k Rusdesk síti…"),
+        ("connecting_status", "Připojování k RustDesk síti..."),
         ("Enable Service", "Povolit službu"),
         ("Start Service", "Spustit službu"),
         ("Service is running", "Služba je spuštěná"),
         ("Service is not running", "Služba není spuštěná"),
         ("not_ready_status", "Nepřipraveno. Zkontrolujte své připojení."),
         ("Control Remote Desktop", "Ovládat vzdálenou plochu"),
-        ("Transfer File", "Přenést soubor"),
+        ("Transfer File", "Přenos souborů"),
         ("Connect", "Připojit"),
         ("Recent Sessions", "Nedávné relace"),
         ("Address Book", "Adresář kontaktů"),
@@ -27,38 +27,38 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
         ("Enable Clipboard", "Povolit schránku"),
         ("Enable File Transfer", "Povolit přenos souborů"),
         ("Enable TCP Tunneling", "Povolit TCP tunelování"),
-        ("IP Whitelisting", "Povolování pouze z daných IP adres)"),
-        ("ID/Relay Server", "Identifikátor / předávací (relay) server"),
+        ("IP Whitelisting", "Povolování pouze z daných IP adres"),
+        ("ID/Relay Server", "ID/předávací server"),
         ("Import Server Config", "Importovat konfiguraci serveru"),
-        ("Export Server Config", ""),
+        ("Export Server Config", "Exportovat konfiguraci serveru"),
         ("Import server configuration successfully", "Konfigurace serveru úspěšně importována"),
-        ("Export server configuration successfully", ""),
+        ("Export server configuration successfully", "Konfigurace serveru úspěšně exportována"),
         ("Invalid server configuration", "Neplatná konfigurace serveru"),
         ("Clipboard is empty", "Schránka je prázdná"),
         ("Stop service", "Zastavit službu"),
-        ("Change ID", "Změnit identifikátor"),
-        ("Your new ID", ""),
-        ("length %min% to %max%", ""),
-        ("starts with a letter", ""),
-        ("allowed characters", ""),
-        ("id_change_tip", "Použít je mozné pouze znaky a-z, A-Z, 0-9 a _ (podtržítko). Dále je třeba aby začínalo na písmeno a-z, A-Z. Délka mezi 6 a 16 znaky."),
+        ("Change ID", "Změnit ID"),
+        ("Your new ID", "Váše nové ID"),
+        ("length %min% to %max%", "délka mezi %min% a %max%"),
+        ("starts with a letter", "začíná písmenem"),
+        ("allowed characters", "povolené znaky"),
+        ("id_change_tip", "Použít je možné pouze znaky a-z, A-Z, 0-9 a _ (podtržítko). Dále je třeba aby začínalo písmenem a-z, A-Z. Délka mezi 6 a 16 znaky."),
         ("Website", "Webové stránky"),
         ("About", "O aplikaci"),
-        ("Slogan_tip", ""),
-        ("Privacy Statement", ""),
-        ("Mute", "Ztlumit"),
-        ("Build Date", ""),
-        ("Version", ""),
-        ("Home", ""),
+        ("Slogan_tip", "Vytvořeno srdcem v tomto chaotickém světě!"),
+        ("Privacy Statement", "Prohlášení o ochraně osobních údajů"),
+        ("Mute", "Ztlumit zvuk"),
+        ("Build Date", "Datum sestavení"),
+        ("Version", "Verze"),
+        ("Home", "Domů"),
         ("Audio Input", "Vstup zvuku"),
-        ("Enhancements", ""),
-        ("Hardware Codec", ""),
-        ("Adaptive Bitrate", ""),
-        ("ID Server", "Server pro identif."),
-        ("Relay Server", "Předávací (relay) server"),
-        ("API Server", "Server s API rozhraním"),
+        ("Enhancements", "Vylepšení"),
+        ("Hardware Codec", "Hardwarový kodek"),
+        ("Adaptive bitrate", "Adaptivní datový tok"),
+        ("ID Server", "ID Server"),
+        ("Relay Server", "Předávací server"),
+        ("API Server", "API Server"),
         ("invalid_http", "Je třeba, aby začínalo na http:// nebo https://"),
-        ("Invalid IP", "Neplatná IP adresa"),
+        ("Invalid IP", "Neplatná IP"),
         ("Invalid format", "Neplatný formát"),
         ("server_not_support", "Server zatím nepodporuje"),
         ("Not available", "Není k dispozici"),
@@ -75,13 +75,13 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
         ("Do you want to enter again?", "Chcete se znovu připojit?"),
         ("Connection Error", "Chyba spojení"),
         ("Error", "Chyba"),
-        ("Reset by the peer", "Resetováno protějškem"),
-        ("Connecting...", "Připojování…"),
-        ("Connection in progress. Please wait.", "Probíhá připojování – vyčkejte."),
-        ("Please try 1 minute later", "Zkuste to až za minutu či déle"),
+        ("Reset by the peer", "Resetováno protistranou"),
+        ("Connecting...", "Připojování..."),
+        ("Connection in progress. Please wait.", "Probíhá připojování, vyčkejte prosím."),
+        ("Please try 1 minute later", "Zkuste to prosím o 1 minutu později"),
         ("Login Error", "Chyba přihlášení se"),
         ("Successful", "Úspěšné"),
-        ("Connected, waiting for image...", "Připojeno, čeká se na obraz…"),
+        ("Connected, waiting for image...", "Připojeno, čeká se na obraz..."),
         ("Name", "Název"),
         ("Type", "Typ"),
         ("Modified", "Změněno"),
@@ -98,13 +98,13 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
         ("Delete", "Smazat"),
         ("Properties", "Vlastnosti"),
         ("Multi Select", "Vícenásobný výběr"),
-        ("Select All", ""),
-        ("Unselect All", ""),
+        ("Select All", "Vybrat vše"),
+        ("Unselect All", "Zrušit výběr všech"),
         ("Empty Directory", "Prázdná složka"),
         ("Not an empty directory", "Neprázdná složka"),
         ("Are you sure you want to delete this file?", "Opravdu chcete tento soubor vymazat?"),
         ("Are you sure you want to delete this empty directory?", "Opravdu chcete tuto prázdnou složku smazat?"),
-        ("Are you sure you want to delete the file of this directory?", "Opravdu chcete vymazat soubor, pocházející z této složky?"),
+        ("Are you sure you want to delete the file of this directory?", "Opravdu chcete vymazat soubor z této složky?"),
         ("Do this for all conflicts", "Naložit takto se všemi konflikty"),
         ("This is irreversible!", "Toto nelze vzít zpět"),
         ("Deleting", "Mazání"),
@@ -113,7 +113,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
         ("Finished", "Dokončeno"),
         ("Speed", "Rychlost"),
         ("Custom Image Quality", "Uživatelsky určená kvalita obrazu"),
-        ("Privacy mode", "Režim soukromí"),
+        ("Privacy mode", "Režim ochrany soukromí"),
         ("Block user input", "Blokovat vstupní zařízení uživatele"),
         ("Unblock user input", "Odblokovat vstupní zařízení uživatele"),
         ("Adjust Window", "Přizpůsobit velikost okna"),
@@ -121,79 +121,79 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
         ("Shrink", "Oříznout"),
         ("Stretch", "Roztáhnout"),
         ("Scrollbar", "Posuvník"),
-        ("ScrollAuto", "Rolovať Auto"),
+        ("ScrollAuto", "Automatické rolování"),
         ("Good image quality", "Dobrá kvalita obrazu"),
-        ("Balanced", "Vyvážené"),
-        ("Optimize reaction time", "Optimalizovat pro co nejnižší prodlevu odezvy"),
-        ("Custom", ""),
-        ("Show remote cursor", "Zobrazovat ukazatel myši z protějšku"),
-        ("Show quality monitor", ""),
+        ("Balanced", "Vyvážená"),
+        ("Optimize reaction time", "Optimalizovat reakční dobu"),
+        ("Custom", "Vlastní"),
+        ("Show remote cursor", "Zobrazit vzdálený kurzor"),
+        ("Show quality monitor", "Zobrazit monitor kvality"),
         ("Disable clipboard", "Vypnout schránku"),
         ("Lock after session end", "Po ukončení relace zamknout plochu"),
         ("Insert", "Vložit"),
-        ("Insert Lock", "Vložit zámek"),
+        ("Insert Lock", "Zamknout"),
         ("Refresh", "Načíst znovu"),
-        ("ID does not exist", "Takový identifikátor neexistuje"),
-        ("Failed to connect to rendezvous server", "Nepodařil se připojit ke zprostředkovávajícímu serveru"),
+        ("ID does not exist", "Toto ID neexistuje"),
+        ("Failed to connect to rendezvous server", "Nepodařilo se připojit ke zprostředkovávajícímu serveru"),
         ("Please try later", "Zkuste to později"),
         ("Remote desktop is offline", "Vzdálená plocha není připojená ke službě"),
         ("Key mismatch", "Neshoda klíčů"),
         ("Timeout", "Překročen časový limit pro navázání spojení"),
-        ("Failed to connect to relay server", "Nepodařilo se připojit k předávacímu (relay) serveru"),
+        ("Failed to connect to relay server", "Nepodařilo se připojit k předávacímu serveru"),
         ("Failed to connect via rendezvous server", "Nepodařilo se připojit prostřednictvím zprostředkovávajícího serveru"),
-        ("Failed to connect via relay server", "Nepodařilo se připojit prostřednictvím předávacímu (relay) serveru"),
+        ("Failed to connect via relay server", "Nepodařilo se připojit prostřednictvím předávacího serveru"),
         ("Failed to make direct connection to remote desktop", "Nepodařilo s navázat přímé připojení ke vzdálené ploše"),
         ("Set Password", "Nastavit heslo"),
         ("OS Password", "Heslo do operačního systému"),
-        ("install_tip", "Kvůli řízení oprávnění v systému (UAC), RustDesk v některých případech na protějšku nefunguje správně. Abyste se UAC vyhnuli, klikněte na níže uvedené tlačítko a nainstalujte tak RustDesk do systému."),
-        ("Click to upgrade", "Aktualizaci nainstalujete kliknutím"),
-        ("Click to download", "Stáhnete si kliknutím"),
-        ("Click to update", "Znovu načtete kliknutím"),
+        ("install_tip", "Kvůli řízení oprávnění v systému (UAC), RustDesk v některých případech na protistraně nefunguje správně. Abyste se UAC vyhnuli, klikněte na níže uvedené tlačítko a nainstalujte tak RustDesk do systému."),
+        ("Click to upgrade", "Aktualizovat"),
+        ("Click to download", "Stáhnout"),
+        ("Click to update", "Aktualizovat"),
         ("Configure", "Nastavit"),
-        ("config_acc", "Aby bylo možné na dálku ovládat vaši plochu, je třeba aplikaci RustDesk udělit oprávnění pro „Zpřístupnění pro hendikepované“."),
-        ("config_screen", "Aby bylo možné přistupovat k vaší ploše na dálku, je třeba aplikaci RustDesk udělit oprávněí pro „Nahrávání obsahu obrazovky“."),
-        ("Installing ...", "Instaluje se…"),
+        ("config_acc", "Aby bylo možné na dálku ovládat vaši plochu, je třeba aplikaci RustDesk udělit oprávnění pro \"Zpřístupnění pro hendikepované\"."),
+        ("config_screen", "Aby bylo možné přistupovat k vaší ploše na dálku, je třeba aplikaci RustDesk udělit oprávnění pro \"Nahrávání obsahu obrazovky\"."),
+        ("Installing ...", "Instaluje se ..."),
         ("Install", "Nainstalovat"),
         ("Installation", "Instalace"),
-        ("Installation Path", "Popis umístění instalace"),
+        ("Installation Path", "Umístění instalace"),
         ("Create start menu shortcuts", "Vytvořit zástupce v nabídce Start"),
         ("Create desktop icon", "Vytvořit ikonu na ploše"),
         ("agreement_tip", "Spuštěním instalace přijímáte licenční ujednání."),
         ("Accept and Install", "Přijmout a nainstalovat"),
         ("End-user license agreement", "Licencenční ujednání s koncovým uživatelem"),
-        ("Generating ...", "Vytváření…"),
+        ("Generating ...", "Vytváření ..."),
         ("Your installation is lower version.", "Máte nainstalovanou starší verzi"),
         ("not_close_tcp_tip", "Po dobu, po kterou tunel potřebujete, nezavírejte toto okno"),
-        ("Listening ...", "Očekávní spojení…"),
-        ("Remote Host", "Vzdálený stroj"),
-        ("Remote Port", "Port na protějšku"),
+        ("Listening ...", "Očekávaní spojení ..."),
+        ("Remote Host", "Vzdálený hostitel"),
+        ("Remote Port", "Vzdálený port"),
         ("Action", "Akce"),
         ("Add", "Přidat"),
         ("Local Port", "Místní port"),
-        ("Local Address", ""),
-        ("Change Local Port", ""),
+        ("Local Address", "Místní adresa"),
+        ("Change Local Port", "Změnit místní port"),
         ("setup_server_tip", "Rychlejší připojení získáte vytvořením si svého vlastního serveru"),
-        ("Too short, at least 6 characters.", "Příliš krátké – alespoň 6 znaků."),
+        ("Too short, at least 6 characters.", "Příliš krátké, alespoň 6 znaků."),
         ("The confirmation is not identical.", "Kontrolní zadání se neshoduje."),
         ("Permissions", "Oprávnění"),
         ("Accept", "Přijmout"),
         ("Dismiss", "Zahodit"),
         ("Disconnect", "Odpojit"),
-        ("Allow using keyboard and mouse", "Umožnit ovládání mé klávesnice a myši"),
-        ("Allow using clipboard", "Umožnit používání schránky"),
-        ("Allow hearing sound", "Umožnit slyšet můj zvuk"),
+        ("Allow using keyboard and mouse", "Povolit ovládání klávesnice a myši"),
+        ("Allow using clipboard", "Povolit použití schránky"),
+        ("Allow hearing sound", "Povolit slyšet zvuk"),
         ("Allow file copy and paste", "Povolit kopírování a vkládání souborů"),
         ("Connected", "Připojeno"),
         ("Direct and encrypted connection", "Přímé a šifrované spojení"),
-        ("Relayed and encrypted connection", "Předávané (relay) a šifrované spojení"),
+        ("Relayed and encrypted connection", "Předávané a šifrované spojení"),
         ("Direct and unencrypted connection", "Přímé a nešifrované spojení"),
-        ("Relayed and unencrypted connection", "Předávané (relay) a nešifrované spojení"),
-        ("Enter Remote ID", "Zadejte identifikátor protějšku"),
+        ("Relayed and unencrypted connection", "Předávané a nešifrované spojení"),
+        ("Enter Remote ID", "Zadejte ID protistrany"),
         ("Enter your password", "Zadejte své heslo"),
-        ("Logging in...", "Přihlašování se…"),
-        ("Enable RDP session sharing", "Zapnout sdílení relace RDP protokolu"),
+        ("Logging in...", "Přihlašování..."),
+        ("Enable RDP session sharing", "Povolit sdílení relace RDP"),
         ("Auto Login", "Automatické přihlášení"),
-        ("Enable Direct IP Access", "Zapnout přímý přístup na IP adresu"),
+        ("Enable Direct IP Access", "Povolit přímý přístup k IP"),
         ("Rename", "Přejmenovat"),
         ("Space", "Mezera"),
         ("Create Desktop Shortcut", "Vytvořit zástupce na ploše"),
@@ -201,54 +201,55 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
         ("Create Folder", "Vytvořit složku"),
         ("Please enter the folder name", "Zadejte název pro složku"),
         ("Fix it", "Opravit to"),
-        ("Warning", "Upozornení"),
-        ("Login screen using Wayland is not supported", "Přihlašovací obrazovka prostřednictvím Wayland není podporována"),
-        ("Reboot required", "Je třeba restartovat"),
+        ("Warning", "Upozornění"),
+        ("Login screen using Wayland is not supported", "Přihlašovací obrazovka pomocí systému Wayland není podporována"),
+        ("Reboot required", "Je vyžadován restart"),
         ("Unsupported display server", "Nepodporovaný zobrazovací server"),
-        ("x11 expected", "očekávány x11"),
-        ("Port", ""),
+        ("x11 expected", "očekávaný x11"),
+        ("Port", "Port"),
         ("Settings", "Nastavení"),
         ("Username", "Uživatelské jméno"),
         ("Invalid port", "Neplatné číslo portu"),
-        ("Closed manually by the peer", "Ručně ukončeno protějškem"),
-        ("Enable remote configuration modification", "Umožnit upravování nastavení vzdáleného"),
-        ("Run without install", "Spustit bez instalování"),
-        ("Connect via relay", ""),
-        ("Always connect via relay", "Vždy se spojovat prostřednictvím brány pro předávání (relay)"),
+        ("Closed manually by the peer", "Ručně ukončeno protistranou"),
+        ("Enable remote configuration modification", "Povolit vzdálenou úpravu konfigurace"),
+        ("Run without install", "Spustit bez instalace"),
+        ("Connect via relay", "Připojení přes předávací server"),
+        ("Always connect via relay", "Vždy se připojovat prostřednictvím předávacího serveru"),
         ("whitelist_tip", "Přístup je umožněn pouze z IP adres, nacházejících se na seznamu povolených"),
         ("Login", "Přihlásit se"),
-        ("Verify", ""),
-        ("Remember me", ""),
-        ("Trust this device", ""),
-        ("Verification code", ""),
-        ("verification_tip", ""),
+        ("Verify", "Ověřit"),
+        ("Remember me", "Zapamatovat si"),
+        ("Trust this device", "Důvěřovat tomuto zařízení"),
+        ("Verification code", "Ověřovací kód"),
+        ("verification_tip", "Na registrovanou e-mailovou adresu byl zaslán ověřovací kód, zadejte jej a pokračujte v přihlašování."),
         ("Logout", "Odhlásit se"),
         ("Tags", "Štítky"),
-        ("Search ID", "Hledat identifikátor"),
-        ("whitelist_sep", "Odělováno čárkou, středníkem, mezerou nebo koncem řádku"),
-        ("Add ID", "Přidat identifikátor"),
+        ("Search ID", "Hledat ID"),
+        ("whitelist_sep", "Oddělené čárkou, středníkem, mezerami, nebo novým řádkem."),
+        ("Add ID", "Přidat ID"),
         ("Add Tag", "Přidat štítek"),
         ("Unselect all tags", "Zrušit výběr všech štítků"),
         ("Network error", "Chyba sítě"),
         ("Username missed", "Chybí uživatelské jméno"),
         ("Password missed", "Chybí heslo"),
         ("Wrong credentials", "Nesprávné přihlašovací údaje"),
+        ("The verification code is incorrect or has expired", "Ověřovací kód je nesprávný, nebo jeho platnost vypršela"),
         ("Edit Tag", "Upravit štítek"),
-        ("Unremember Password", "Přestat si heslo pamatovat"),
+        ("Unremember Password", "Přestat si pamatovat heslo"),
         ("Favorites", "Oblíbené"),
         ("Add to Favorites", "Přidat do oblíbených"),
         ("Remove from Favorites", "Odebrat z oblíbených"),
         ("Empty", "Prázdné"),
         ("Invalid folder name", "Neplatný název složky"),
         ("Socks5 Proxy", "Socks5 proxy"),
-        ("Hostname", "Název stroje"),
+        ("Hostname", "Název hostitele"),
         ("Discovered", "Objeveno"),
         ("install_daemon_tip", "Pokud má být spouštěno při startu systému, je třeba nainstalovat systémovou službu."),
-        ("Remote ID", "Identif. protějšku"),
+        ("Remote ID", "Vzdálené ID"),
         ("Paste", "Vložit"),
-        ("Paste here?", "Vložit sem?"),
-        ("Are you sure to close the connection?", "Opravdu chcete spojení ukončit?"),
-        ("Download new version", "Stáhnout si novou verzi"),
+        ("Paste here?", "Vložit zde?"),
+        ("Are you sure to close the connection?", "Opravdu chcete spojení uzavřít?"),
+        ("Download new version", "Stáhnout novou verzi"),
         ("Touch mode", "Režim dotyku"),
         ("Mouse mode", "Režim myši"),
         ("One-Finger Tap", "Klepnutí jedním prstem"),
@@ -266,10 +267,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
         ("Pinch to Zoom", "Přiblížíte roztažením dvěma prsty"),
         ("Canvas Zoom", "Přiblížení zobrazení"),
         ("Reset canvas", "Vrátit měřtko zobrazení na výchozí"),
-        ("No permission of file transfer", "Žádné oprávnění přenosu souboru"),
+        ("No permission of file transfer", "Žádné oprávnění k přenosu souborů"),
         ("Note", "Poznámka"),
         ("Connection", "Připojení"),
-        ("Share Screen", "Nasdílet obrazovku"),
+        ("Share Screen", "Sdílet obrazovku"),
         ("Chat", "Chat"),
         ("Total", "Celkem"),
         ("items", "Položek"),
@@ -287,230 +288,260 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
         ("android_new_connection_tip", "Obdržen nový požadavek na řízení zařízení, který chce ovládat vaše stávající zařízení."),
         ("android_service_will_start_tip", "Zapnutí „Zachytávání obsahu obrazovky“ automaticky spustí službu, což umožní ostatním zařízením žádat o připojení k vašemu zařízení."),
         ("android_stop_service_tip", "Zastavení služby automaticky ukončí veškerá navázaná spojení."),
-        ("android_version_audio_tip", "Vámi nyní používaná verze systému Android nepodporuje zachytávání zvuku – přejděte na Android 10 nebo novější."),
-        ("android_start_service_tip", ""),
-        ("android_permission_may_not_change_tip", ""),
-        ("Account", ""),
+        ("android_version_audio_tip", "Vámi nyní používaná verze systému Android nepodporuje zachytávání zvuku – přejděte na Android 10, nebo novější."),
+        ("android_start_service_tip", "Klepnutím na možnost [Spustit službu], nebo povolením oprávnění [Snímání obrazovky] spustíte službu sdílení obrazovky."),
+        ("android_permission_may_not_change_tip", "Oprávnění pro navázaná připojení lze změnit až po opětovném připojení."),
+        ("Account", "Účet"),
         ("Overwrite", "Přepsat"),
-        ("This file exists, skip or overwrite this file?", "Tento soubor existuje – přeskočit ho nebo přepsat?"),
+        ("This file exists, skip or overwrite this file?", "Tento soubor existuje, přeskočit, nebo přepsat tento soubor?"),
         ("Quit", "Ukončit"),
         ("doc_mac_permission", "https://rustdesk.com/docs/en/manual/mac/#enable-permissions"),
         ("Help", "Nápověda"),
         ("Failed", "Nepodařilo se"),
-        ("Succeeded", "Uspěl"),
-        ("Someone turns on privacy mode, exit", "Někdo zapne režim soukromí, ukončete ho"),
+        ("Succeeded", "Úspěšný"),
+        ("Someone turns on privacy mode, exit", "Někdo zapne režim ochrany soukromí, ukončete ho"),
         ("Unsupported", "Nepodporováno"),
-        ("Peer denied", "Peer popřel"),
+        ("Peer denied", "Protistana odmítnula"),
         ("Please install plugins", "Nainstalujte si prosím pluginy"),
-        ("Peer exit", "Peer exit"),
+        ("Peer exit", "Ukončení protistrany"),
         ("Failed to turn off", "Nepodařilo se vypnout"),
         ("Turned off", "Vypnutý"),
-        ("In privacy mode", "v režimu soukromí"),
-        ("Out privacy mode", "mimo režim soukromí"),
-        ("Language", ""),
-        ("Keep RustDesk background service", ""),
-        ("Ignore Battery Optimizations", ""),
-        ("android_open_battery_optimizations_tip", ""),
-        ("Start on Boot", ""),
-        ("Start the screen sharing service on boot, requires special permissions", ""),
-        ("Connection not allowed", ""),
-        ("Legacy mode", ""),
-        ("Map mode", ""),
-        ("Translate mode", ""),
-        ("Use permanent password", ""),
-        ("Use both passwords", ""),
-        ("Set permanent password", ""),
-        ("Enable Remote Restart", ""),
-        ("Allow remote restart", ""),
-        ("Restart Remote Device", ""),
-        ("Are you sure you want to restart", ""),
-        ("Restarting Remote Device", ""),
-        ("remote_restarting_tip", ""),
-        ("Copied", ""),
-        ("Exit Fullscreen", "Ukončete celou obrazovku"),
+        ("In privacy mode", "v režimu ochrany soukromí"),
+        ("Out privacy mode", "mimo režim ochrany soukromí"),
+        ("Language", "Jazyk"),
+        ("Keep RustDesk background service", "Zachovat službu RustDesk na pozadí"),
+        ("Ignore Battery Optimizations", "Ignorovat optimalizaci baterie"),
+        ("android_open_battery_optimizations_tip", "Pokud chcete tuto funkci zakázat, přejděte na další stránku nastavení aplikace RustDesk, najděte a zadejte [Baterie], zrušte zaškrtnutí [Neomezeno]."),
+        ("Start on Boot", "Spustit při startu systému"),
+        ("Start the screen sharing service on boot, requires special permissions", "Spuštění služby sdílení obrazovky při spuštění systému, vyžaduje zvláštní oprávnění"),
+        ("Connection not allowed", "Připojení není povoleno"),
+        ("Legacy mode", "Režim Legacy"),
+        ("Map mode", "Režim mapování"),
+        ("Translate mode", "Režim překladu"),
+        ("Use permanent password", "Použít trvalé heslo"),
+        ("Use both passwords", "Použít obě hesla"),
+        ("Set permanent password", "Nastavit trvalé heslo"),
+        ("Enable Remote Restart", "Povolit vzdálené restartování"),
+        ("Allow remote restart", "Povolit vzdálený restart"),
+        ("Restart Remote Device", "Restartovat vzdálené zařízení"),
+        ("Are you sure you want to restart", "Jste si jisti, že chcete restartovat"),
+        ("Restarting Remote Device", "Restartování vzdáleného zařízení"),
+        ("remote_restarting_tip", "Vzdálené zařízení se restartuje, zavřete prosím toto okno a po chvíli se znovu připojte pomocí trvalého hesla."),
+        ("Copied", "Zkopírováno"),
+        ("Exit Fullscreen", "Ukončit celou obrazovku"),
         ("Fullscreen", "Celá obrazovka"),
         ("Mobile Actions", "Mobilní akce"),
-        ("Select Monitor", "Vyberte možnost Monitor"),
+        ("Select Monitor", "Vybrat monitor"),
         ("Control Actions", "Ovládací akce"),
         ("Display Settings", "Nastavení obrazovky"),
         ("Ratio", "Poměr"),
         ("Image Quality", "Kvalita obrazu"),
         ("Scroll Style", "Štýl posúvania"),
-        ("Show Toolbar", ""),
-        ("Hide Toolbar", ""),
+        ("Show Toolbar", "Zobrazit panel nástrojů"),
+        ("Hide Toolbar", "Skrýt panel nástrojů"),
         ("Direct Connection", "Přímé spojení"),
-        ("Relay Connection", "Připojení relé"),
+        ("Relay Connection", "Připojení předávací server"),
         ("Secure Connection", "Zabezpečené připojení"),
         ("Insecure Connection", "Nezabezpečené připojení"),
-        ("Scale original", "Měřítko původní"),
-        ("Scale adaptive", "Měřítko adaptivní"),
-        ("General", ""),
-        ("Security", ""),
-        ("Theme", ""),
-        ("Dark Theme", ""),
-        ("Light Theme", ""),
-        ("Dark", ""),
-        ("Light", ""),
-        ("Follow System", ""),
-        ("Enable hardware codec", ""),
-        ("Unlock Security Settings", ""),
-        ("Enable Audio", ""),
-        ("Unlock Network Settings", ""),
-        ("Server", ""),
-        ("Direct IP Access", ""),
-        ("Proxy", ""),
-        ("Apply", ""),
-        ("Disconnect all devices?", ""),
-        ("Clear", ""),
-        ("Audio Input Device", ""),
-        ("Use IP Whitelisting", ""),
-        ("Network", ""),
-        ("Enable RDP", ""),
-        ("Pin Toolbar", ""),
-        ("Unpin Toolbar", ""),
-        ("Recording", ""),
-        ("Directory", ""),
-        ("Automatically record incoming sessions", ""),
-        ("Change", ""),
-        ("Start session recording", ""),
-        ("Stop session recording", ""),
-        ("Enable Recording Session", ""),
-        ("Allow recording session", ""),
-        ("Enable LAN Discovery", ""),
-        ("Deny LAN Discovery", ""),
-        ("Write a message", ""),
-        ("Prompt", ""),
-        ("Please wait for confirmation of UAC...", ""),
-        ("elevated_foreground_window_tip", ""),
-        ("Disconnected", ""),
-        ("Other", ""),
-        ("Confirm before closing multiple tabs", ""),
-        ("Keyboard Settings", ""),
-        ("Full Access", ""),
-        ("Screen Share", ""),
-        ("Wayland requires Ubuntu 21.04 or higher version.", "Wayland vyžaduje Ubuntu 21.04 nebo vyšší verzi."),
-        ("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "Wayland vyžaduje vyšší verzi linuxové distribuce. Zkuste prosím X11 desktop nebo změňte OS."),
-        ("JumpLink", "View"),
-        ("Please Select the screen to be shared(Operate on the peer side).", "Vyberte prosím obrazovku, kterou chcete sdílet (Ovládejte na straně protějšku)."),
-        ("Show RustDesk", ""),
-        ("This PC", ""),
-        ("or", ""),
-        ("Continue with", ""),
-        ("Elevate", ""),
-        ("Zoom cursor", ""),
-        ("Accept sessions via password", ""),
-        ("Accept sessions via click", ""),
-        ("Accept sessions via both", ""),
-        ("Please wait for the remote side to accept your session request...", ""),
-        ("One-time Password", ""),
-        ("Use one-time password", ""),
-        ("One-time password length", ""),
-        ("Request access to your device", ""),
-        ("Hide connection management window", ""),
-        ("hide_cm_tip", ""),
-        ("wayland_experiment_tip", ""),
-        ("Right click to select tabs", ""),
-        ("Skipped", ""),
-        ("Add to Address Book", ""),
-        ("Group", ""),
-        ("Search", ""),
-        ("Closed manually by web console", ""),
-        ("Local keyboard type", ""),
-        ("Select local keyboard type", ""),
-        ("software_render_tip", ""),
-        ("Always use software rendering", ""),
-        ("config_input", ""),
-        ("config_microphone", ""),
-        ("request_elevation_tip", ""),
-        ("Wait", ""),
-        ("Elevation Error", ""),
-        ("Ask the remote user for authentication", ""),
-        ("Choose this if the remote account is administrator", ""),
-        ("Transmit the username and password of administrator", ""),
-        ("still_click_uac_tip", ""),
-        ("Request Elevation", ""),
-        ("wait_accept_uac_tip", ""),
-        ("Elevate successfully", ""),
-        ("uppercase", ""),
-        ("lowercase", ""),
-        ("digit", ""),
-        ("special character", ""),
-        ("length>=8", ""),
-        ("Weak", ""),
-        ("Medium", ""),
-        ("Strong", ""),
-        ("Switch Sides", ""),
-        ("Please confirm if you want to share your desktop?", ""),
-        ("Display", ""),
-        ("Default View Style", ""),
-        ("Default Scroll Style", ""),
-        ("Default Image Quality", ""),
-        ("Default Codec", ""),
-        ("Bitrate", ""),
-        ("FPS", ""),
-        ("Auto", ""),
-        ("Other Default Options", ""),
-        ("Voice call", ""),
-        ("Text chat", ""),
-        ("Stop voice call", ""),
-        ("relay_hint_tip", ""),
-        ("Reconnect", ""),
-        ("Codec", ""),
-        ("Resolution", ""),
-        ("No transfers in progress", ""),
-        ("Set one-time password length", ""),
-        ("idd_driver_tip", ""),
-        ("confirm_idd_driver_tip", ""),
-        ("RDP Settings", ""),
-        ("Sort by", ""),
-        ("New Connection", ""),
-        ("Restore", ""),
-        ("Minimize", ""),
-        ("Maximize", ""),
-        ("Your Device", ""),
-        ("empty_recent_tip", ""),
-        ("empty_favorite_tip", ""),
-        ("empty_lan_tip", ""),
-        ("empty_address_book_tip", ""),
-        ("eg: admin", ""),
-        ("Empty Username", ""),
-        ("Empty Password", ""),
-        ("Me", ""),
-        ("identical_file_tip", ""),
-        ("show_monitors_tip", ""),
-        ("View Mode", ""),
-        ("login_linux_tip", ""),
-        ("verify_rustdesk_password_tip", ""),
-        ("remember_account_tip", ""),
-        ("os_account_desk_tip", ""),
-        ("OS Account", ""),
-        ("another_user_login_title_tip", ""),
-        ("another_user_login_text_tip", ""),
-        ("xorg_not_found_title_tip", ""),
-        ("xorg_not_found_text_tip", ""),
-        ("no_desktop_title_tip", ""),
-        ("no_desktop_text_tip", ""),
-        ("No need to elevate", ""),
-        ("System Sound", ""),
-        ("Default", ""),
-        ("New RDP", ""),
-        ("Fingerprint", ""),
-        ("Copy Fingerprint", ""),
-        ("no fingerprints", ""),
-        ("Select a peer", ""),
-        ("Select peers", ""),
-        ("Plugins", ""),
-        ("Uninstall", ""),
-        ("Update", ""),
-        ("Enable", ""),
-        ("Disable", ""),
-        ("Options", ""),
-        ("resolution_original_tip", ""),
-        ("resolution_fit_local_tip", ""),
-        ("resolution_custom_tip", ""),
-        ("Collapse toolbar", ""),
-        ("Accept and Elevate", ""),
-        ("accept_and_elevate_btn_tooltip", ""),
-        ("clipboard_wait_response_timeout_tip", ""),
+        ("Scale original", "Originální měřítko"),
+        ("Scale adaptive", "Adaptivní měřítko"),
+        ("General", "Obecné"),
+        ("Security", "Zabezpečení"),
+        ("Theme", "Motiv"),
+        ("Dark Theme", "Tmavý motiv"),
+        ("Light Theme", "Světlý motiv"),
+        ("Dark", "Tmavý"),
+        ("Light", "Světlý"),
+        ("Follow System", "Podle systému"),
+        ("Enable hardware codec", "Povolit hardwarový kodek"),
+        ("Unlock Security Settings", "Odemknout nastavení zabezpečení"),
+        ("Enable Audio", "Povolit zvuk"),
+        ("Unlock Network Settings", "Odemknout nastavení sítě"),
+        ("Server", "Server"),
+        ("Direct IP Access", "Přímý IP přístup"),
+        ("Proxy", "Proxy"),
+        ("Apply", "Použít"),
+        ("Disconnect all devices?", "Odpojit všechna zařízení?"),
+        ("Clear", "Smazat"),
+        ("Audio Input Device", "Vstupní zvukové zařízení"),
+        ("Use IP Whitelisting", "Použít bílou listinu IP"),
+        ("Network", "Síť"),
+        ("Enable RDP", "Povolit protokol RDP"),
+        ("Pin Toolbar", "Připnout panel nástrojů"),
+        ("Unpin Toolbar", "Odepnout panel nástrojů"),
+        ("Recording", "Nahrávání"),
+        ("Directory", "Adresář"),
+        ("Automatically record incoming sessions", "Automaticky nahrávat příchozí relace"),
+        ("Change", "Změnit"),
+        ("Start session recording", "Spustit záznam relace"),
+        ("Stop session recording", "Zastavit záznam relace"),
+        ("Enable Recording Session", "Povolit nahrávání relace"),
+        ("Allow recording session", "Povolit nahrávání relace"),
+        ("Enable LAN Discovery", "Povolit zjišťování sítě LAN"),
+        ("Deny LAN Discovery", "Zakázat zjišťování sítě LAN"),
+        ("Write a message", "Napsat zprávu"),
+        ("Prompt", "Výzva"),
+        ("Please wait for confirmation of UAC...", "Počkejte prosím na potvrzení UAC..."),
+        ("elevated_foreground_window_tip", "Aktuální okno vzdálené plochy vyžaduje vyšší oprávnění, takže dočasně nemůže používat myš a klávesnici. Můžete vzdáleného uživatele požádat, aby aktuální okno minimalizoval, nebo kliknout na tlačítko pro zvýšení v okně správy připojení. Chcete-li se tomuto problému vyhnout, doporučujeme nainstalovat software na vzdálené zařízení."),
+        ("Disconnected", "Odpojeno"),
+        ("Other", "Jiné"),
+        ("Confirm before closing multiple tabs", "Potvrdit před zavřením více karet"),
+        ("Keyboard Settings", "Nastavení klávesnice"),
+        ("Full Access", "Úplný přístup"),
+        ("Screen Share", "Sdílení obrazovky"),
+        ("Wayland requires Ubuntu 21.04 or higher version.", "Wayland vyžaduje Ubuntu 21.04, nebo vyšší verzi."),
+        ("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "Wayland vyžaduje vyšší verzi linuxové distribuce. Zkuste prosím X11 desktop, nebo změňte OS."),
+        ("JumpLink", "JumpLink"),
+        ("Please Select the screen to be shared(Operate on the peer side).", "Vyberte prosím obrazovku, kterou chcete sdílet (Ovládejte na straně protistrany)."),
+        ("Show RustDesk", "Zobrazit RustDesk"),
+        ("This PC", "Tento počítač"),
+        ("or", "nebo"),
+        ("Continue with", "Pokračovat s"),
+        ("Elevate", "Zvýšit"),
+        ("Zoom cursor", "Kurzor přiblížení"),
+        ("Accept sessions via password", "Přijímat relace pomocí hesla"),
+        ("Accept sessions via click", "Přijímat relace kliknutím"),
+        ("Accept sessions via both", "Přijímat relace prostřednictvím obou"),
+        ("Please wait for the remote side to accept your session request...", "Počkejte prosím, až vzdálená strana přijme váš požadavek na relaci..."),
+        ("One-time Password", "Jednorázové heslo"),
+        ("Use one-time password", "Použít jednorázové heslo"),
+        ("One-time password length", "Délka jednorázového hesla"),
+        ("Request access to your device", "Žádost o přístup k vašemu zařízení"),
+        ("Hide connection management window", "Skrýt okno správy připojení"),
+        ("hide_cm_tip", "Povolit skrývání pouze v případě, že přijímáte relace pomocí hesla a používáte trvalé heslo."),
+        ("wayland_experiment_tip", "Podpora Waylandu je v experimentální fázi, pokud potřebujete bezobslužný přístup, použijte prosím X11."),
+        ("Right click to select tabs", "Výběr karet kliknutím pravým tlačítkem myši"),
+        ("Skipped", "Vynecháno"),
+        ("Add to Address Book", "Přidat do adresáře"),
+        ("Group", "Skupina"),
+        ("Search", "Vyhledávání"),
+        ("Closed manually by web console", "Uzavřeno ručně pomocí webové konzole"),
+        ("Local keyboard type", "Typ místní klávesnice"),
+        ("Select local keyboard type", "Výběr typu místní klávesnice"),
+        ("software_render_tip", "Pokud používáte grafickou kartu Nvidia v systému Linux a vzdálené okno se po připojení ihned zavře, může vám pomoci přepnutí na open-source ovladač Nouveau a volba softwarového vykreslování. Je nutný restart softwaru."),
+        ("Always use software rendering", "Vždy použít softwarové vykreslování"),
+        ("config_input", "Chcete-li ovládat vzdálenou plochu pomocí klávesnice, musíte udělit oprávnění RustDesk \"Sledování vstupu\"."),
+        ("config_microphone", "Abyste mohli mluvit na dálku, musíte udělit oprávnění RustDesk \"Nahrávat zvuk\"."),
+        ("request_elevation_tip", "Můžete také požádat o zvýšení, pokud je někdo na vzdálené straně."),
+        ("Wait", "Počkejte"),
+        ("Elevation Error", "Chyba navýšení"),
+        ("Ask the remote user for authentication", "Požádat vzdáleného uživatele o ověření"),
+        ("Choose this if the remote account is administrator", "Tuto možnost vyberte, pokud je vzdálený účet správce"),
+        ("Transmit the username and password of administrator", "Přenos uživatelského jména a hesla správce"),
+        ("still_click_uac_tip", "Stále vyžaduje, aby vzdálený uživatel kliknul na OK v okně UAC spuštěného RustDesku."),
+        ("Request Elevation", "Žádost o navýšení"),
+        ("wait_accept_uac_tip", "Počkejte, až vzdálený uživatel přijme dialogové okno UAC."),
+        ("Elevate successfully", "Úspěšné navýšení"),
+        ("uppercase", "velká písmena"),
+        ("lowercase", "malá písmena"),
+        ("digit", "číslice"),
+        ("special character", "speciální znak"),
+        ("length>=8", "délka>=8"),
+        ("Weak", "Slabé"),
+        ("Medium", "Střední"),
+        ("Strong", "Silné"),
+        ("Switch Sides", "Přepínání stran"),
+        ("Please confirm if you want to share your desktop?", "Potvrďte prosím, zda chcete sdílet svou plochu?"),
+        ("Display", "Obrazovka"),
+        ("Default View Style", "Výchozí styl zobrazení"),
+        ("Default Scroll Style", "Výchozí styl rolování"),
+        ("Default Image Quality", "Výchozí kvalita obrazu"),
+        ("Default Codec", "Výchozí kodek"),
+        ("Bitrate", "Datový tok"),
+        ("FPS", "FPS"),
+        ("Auto", "Auto"),
+        ("Other Default Options", "Ostatní výchozí možnosti"),
+        ("Voice call", "Hlasové volání"),
+        ("Text chat", "Textový chat"),
+        ("Stop voice call", "Zastavit hlasové volání"),
+        ("relay_hint_tip", "Přímé připojení nemusí být možné, můžete se zkusit připojit přes předávací server. Pokud navíc chcete při prvním pokusu použít předávací server, můžete k ID přidat příponu \"/r\", nebo v kartě posledních relací vybrat možnost \"Vždy se připojovat přes bránu\", pokud existuje."),
+        ("Reconnect", "Znovu připojit"),
+        ("Codec", "Kodek"),
+        ("Resolution", "Rozlišení"),
+        ("No transfers in progress", "Žádné probíhající přenosy"),
+        ("Set one-time password length", "Nastavení délky jednorázového hesla"),
+        ("install_cert_tip", "Instalace certifikátu RustDesk"),
+        ("confirm_install_cert_tip", "Jedná se o testovací certifikát RustDesk, kterému lze důvěřovat. Certifikát bude v případě potřeby použit k důvěryhodnosti a instalaci ovladačů RustDesk."),
+        ("RDP Settings", "Nastavení RDP"),
+        ("Sort by", "Seřadit podle"),
+        ("New Connection", "Nové připojení"),
+        ("Restore", "Obnovit"),
+        ("Minimize", "Minimalizovat"),
+        ("Maximize", "Maximalizovat"),
+        ("Your Device", "Vaše zařízení"),
+        ("empty_recent_tip", "Ups, žádná nedávná relace!\nČas naplánovat nové."),
+        ("empty_favorite_tip", "Ještě nemáte oblíbené protistrany?\nNajděte někoho, s kým se můžete spojit, a přidejte si ho do oblíbených!"),
+        ("empty_lan_tip", "Ale ne, vypadá, že jsme ještě neobjevili žádné protistrany."),
+        ("empty_address_book_tip", "Ach bože, zdá se, že ve vašem adresáři nejsou v současné době uvedeni žádní kolegové."),
+        ("eg: admin", "např. admin"),
+        ("Empty Username", "Prázdné uživatelské jméno"),
+        ("Empty Password", "Prázdné heslo"),
+        ("Me", "Já"),
+        ("identical_file_tip", "Tento soubor je totožný se souborem partnera."),
+        ("show_monitors_tip", "Zobrazit monitory na panelu nástrojů"),
+        ("View Mode", "Režim zobrazení"),
+        ("login_linux_tip", "Chcete-li povolit relaci plochy X, musíte se přihlásit ke vzdálenému účtu systému Linux."),
+        ("verify_rustdesk_password_tip", "Ověření hesla RustDesk"),
+        ("remember_account_tip", "Zapamatovat si tento účet"),
+        ("os_account_desk_tip", "Tento účet se používá k přihlášení do vzdáleného operačního systému a k povolení relace plochy v režimu headless."),
+        ("OS Account", "Účet operačního systému"),
+        ("another_user_login_title_tip", "Další uživatel je již přihlášen"),
+        ("another_user_login_text_tip", "Odpojit"),
+        ("xorg_not_found_title_tip", "Xorg nebyl nalezen"),
+        ("xorg_not_found_text_tip", "Prosím, nainstalujte Xorg"),
+        ("no_desktop_title_tip", "Není k dispozici žádná plocha"),
+        ("no_desktop_text_tip", "Nainstalujte si prosím prostředí GNOME"),
+        ("No need to elevate", "Není třeba navýšení"),
+        ("System Sound", "Systémový zvuk"),
+        ("Default", "Výchozí"),
+        ("New RDP", "Nové RDP"),
+        ("Fingerprint", "Otisk"),
+        ("Copy Fingerprint", "Kopírovat otisk"),
+        ("no fingerprints", "žádný otisk"),
+        ("Select a peer", "Výběr protistrany"),
+        ("Select peers", "Vybrat protistrany"),
+        ("Plugins", "Pluginy"),
+        ("Uninstall", "Odinstalovat"),
+        ("Update", "Aktualizovat"),
+        ("Enable", "Povolit"),
+        ("Disable", "Zakázat"),
+        ("Options", "Možnosti"),
+        ("resolution_original_tip", "Původní rozlišení"),
+        ("resolution_fit_local_tip", "Přizpůsobit místní rozlišení"),
+        ("resolution_custom_tip", "Vlastní rozlišení"),
+        ("Collapse toolbar", "Sbalit panel nástrojů"),
+        ("Accept and Elevate", "Přijmout navýšení"),
+        ("accept_and_elevate_btn_tooltip", "Přijměte připojení a zvyšte oprávnění UAC."),
+        ("clipboard_wait_response_timeout_tip", "Vypršel čas čekání odpovědi na kopii."),
+        ("Incoming connection", "Příchozí připojení"),
+        ("Outgoing connection", "Odchozí připojení"),
+        ("Exit", "Ukončit"),
+        ("Open", "Otevřít"),
+        ("logout_tip", "Opravdu se chcete odhlásit?"),
+        ("Service", "Služba"),
+        ("Start", "Spustit"),
+        ("Stop", "Zastavit"),
+        ("exceed_max_devices", "Dosáhli jste maximálního počtu spravovaných zařízení."),
+        ("Sync with recent sessions", "Synchronizace s posledními relacemi"),
+        ("Sort tags", "Seřadit štítky"),
+        ("Open connection in new tab", "Otevřít připojení na nové kartě"),
+        ("Move tab to new window", "Přesunout kartu do nového okna"),
+        ("Can not be empty", "Nemůže být prázdné"),
+        ("Already exists", "Již existuje"),
+        ("Change Password", "Změnit heslo"),
+        ("Refresh Password", "Obnovit heslo"),
+        ("ID", "ID"),
+        ("Grid View", "Mřížka"),
+        ("List View", "Seznam"),
+        ("Select", "Vybrat"),
+        ("Toggle Tags", "Přepnout štítky"),
+        ("pull_ab_failed_tip", "Nepodařilo se obnovit adresář"),
+        ("push_ab_failed_tip", "Nepodařilo se synchronizovat adresář se serverem"),
+        ("synced_peer_readded_tip", "Zařízení, která byla přítomna v posledních relacích, budou synchronizována zpět do adresáře."),
+        ("Change Color", ""),
+        ("Primary Color", ""),
+        ("HSV Color", ""),
+        ("Installation Successful!", ""),
+        ("Installation failed!", ""),
     ].iter().cloned().collect();
 }
diff --git a/src/lang/da.rs b/src/lang/da.rs
index aaa5acf0e..999875df2 100644
--- a/src/lang/da.rs
+++ b/src/lang/da.rs
@@ -53,7 +53,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
         ("Audio Input", "Lydinput"),
         ("Enhancements", "Forbedringer"),
         ("Hardware Codec", "Hardware-codec"),
-        ("Adaptive Bitrate", "Adaptiv Bitrate"),
+        ("Adaptive bitrate", "Adaptiv bitrate"),
         ("ID Server", "ID Server"),
         ("Relay Server", "Relay Server"),
         ("API Server", "API Server"),
@@ -233,6 +233,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
         ("Username missed", "Glemt brugernavn"),
         ("Password missed", "Glemt kodeord"),
         ("Wrong credentials", "Forkerte registreringsdata"),
+        ("The verification code is incorrect or has expired", ""),
         ("Edit Tag", "Rediger nøgleord"),
         ("Unremember Password", "Glem adgangskoden"),
         ("Favorites", "Favoritter"),
@@ -459,8 +460,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
         ("Resolution", "Opløsning"),
         ("No transfers in progress", "Ingen overførsler i gang"),
         ("Set one-time password length", "Sæt engangsadgangskode længde"),
-        ("idd_driver_tip", ""),
-        ("confirm_idd_driver_tip", ""),
+        ("install_cert_tip", ""),
+        ("confirm_install_cert_tip", ""),
         ("RDP Settings", "RDP indstillinger"),
         ("Sort by", "Sortér efter"),
         ("New Connection", "Ny forbindelse"),
@@ -512,5 +513,35 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
         ("Accept and Elevate", ""),
         ("accept_and_elevate_btn_tooltip", ""),
         ("clipboard_wait_response_timeout_tip", ""),
+        ("Incoming connection", ""),
+        ("Outgoing connection", ""),
+        ("Exit", ""),
+        ("Open", ""),
+        ("logout_tip", ""),
+        ("Service", ""),
+        ("Start", ""),
+        ("Stop", ""),
+        ("exceed_max_devices", ""),
+        ("Sync with recent sessions", ""),
+        ("Sort tags", ""),
+        ("Open connection in new tab", ""),
+        ("Move tab to new window", ""),
+        ("Can not be empty", ""),
+        ("Already exists", ""),
+        ("Change Password", ""),
+        ("Refresh Password", ""),
+        ("ID", ""),
+        ("Grid View", ""),
+        ("List View", ""),
+        ("Select", ""),
+        ("Toggle Tags", ""),
+        ("pull_ab_failed_tip", ""),
+        ("push_ab_failed_tip", ""),
+        ("synced_peer_readded_tip", ""),
+        ("Change Color", ""),
+        ("Primary Color", ""),
+        ("HSV Color", ""),
+        ("Installation Successful!", ""),
+        ("Installation failed!", ""),
     ].iter().cloned().collect();
 }
diff --git a/src/lang/de.rs b/src/lang/de.rs
index e3852af7d..154805fbc 100644
--- a/src/lang/de.rs
+++ b/src/lang/de.rs
@@ -53,7 +53,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
         ("Audio Input", "Audioeingang"),
         ("Enhancements", "Verbesserungen"),
         ("Hardware Codec", "Hardware-Codec"),
-        ("Adaptive Bitrate", "Bitrate automatisch anpassen"),
+        ("Adaptive bitrate", "Bitrate automatisch anpassen"),
         ("ID Server", "ID-Server"),
         ("Relay Server", "Relay-Server"),
         ("API Server", "API-Server"),
@@ -223,17 +223,18 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
         ("Verification code", "Verifizierungscode"),
         ("verification_tip", "Ein Verifizierungscode wurde an die registrierte E-Mail-Adresse gesendet. Geben Sie den Verifizierungscode ein, um sich erneut anzumelden."),
         ("Logout", "Abmelden"),
-        ("Tags", "Schlagworte"),
+        ("Tags", "Tags"),
         ("Search ID", "ID suchen"),
         ("whitelist_sep", "Getrennt durch Komma, Semikolon, Leerzeichen oder Zeilenumbruch"),
         ("Add ID", "ID hinzufügen"),
-        ("Add Tag", "Stichwort hinzufügen"),
-        ("Unselect all tags", "Alle Stichworte abwählen"),
+        ("Add Tag", "Tag hinzufügen"),
+        ("Unselect all tags", "Alle Tags abwählen"),
         ("Network error", "Netzwerkfehler"),
         ("Username missed", "Benutzername fehlt"),
         ("Password missed", "Passwort fehlt"),
         ("Wrong credentials", "Falsche Anmeldedaten"),
-        ("Edit Tag", "Schlagwort bearbeiten"),
+        ("The verification code is incorrect or has expired", "Der Verifizierungscode ist falsch oder abgelaufen"),
+        ("Edit Tag", "Tag bearbeiten"),
         ("Unremember Password", "Gespeichertes Passwort löschen"),
         ("Favorites", "Favoriten"),
         ("Add to Favorites", "Zu Favoriten hinzufügen"),
@@ -409,7 +410,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
         ("Hide connection management window", "Fenster zur Verwaltung der Verbindung verstecken"),
         ("hide_cm_tip", "Dies ist nur möglich, wenn der Zugriff über ein permanentes Passwort erfolgt."),
         ("wayland_experiment_tip", "Die Unterstützung von Wayland ist nur experimentell. Bitte nutzen Sie X11, wenn Sie einen unbeaufsichtigten Zugriff benötigen."),
-        ("Right click to select tabs", "Register mit rechtem Mausklick auswählen"),
+        ("Right click to select tabs", "Tabs mit rechtem Mausklick auswählen"),
         ("Skipped", "Übersprungen"),
         ("Add to Address Book", "Zum Adressbuch hinzufügen"),
         ("Group", "Gruppe"),
@@ -453,14 +454,14 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
         ("Voice call", "Sprachanruf"),
         ("Text chat", "Text-Chat"),
         ("Stop voice call", "Sprachanruf beenden"),
-        ("relay_hint_tip", "Wenn eine direkte Verbindung nicht möglich ist, können Sie versuchen, eine Verbindung über einen Relay-Server herzustellen. \nWenn Sie eine Relay-Verbindung beim ersten Versuch herstellen möchten, können Sie das Suffix \"/r\" an die ID anhängen oder die Option \"Immer über Relay-Server verbinden\" auf der Gegenstelle auswählen."),
+        ("relay_hint_tip", "Wenn eine direkte Verbindung nicht möglich ist, können Sie versuchen, eine Verbindung über einen Relay-Server herzustellen.\nWenn Sie eine Relay-Verbindung beim ersten Versuch herstellen möchten, können Sie das Suffix \"/r\" an die ID anhängen oder die Option \"Immer über Relay-Server verbinden\" in der Liste der letzten Sitzungen auswählen, sofern diese vorhanden ist."),
         ("Reconnect", "Erneut verbinden"),
         ("Codec", "Codec"),
         ("Resolution", "Auflösung"),
         ("No transfers in progress", "Keine Übertragungen im Gange"),
         ("Set one-time password length", "Länge des Einmalpassworts festlegen"),
-        ("idd_driver_tip", "Installieren Sie den virtuellen Anzeigetreiber, der verwendet wird, wenn Sie keine physischen Anzeigen haben."),
-        ("confirm_idd_driver_tip", "Die Option zur Installation des virtuellen Anzeigetreibers ist aktiviert. Beachten Sie, dass ein Testzertifikat installiert wird, um dem virtuellen Anzeigetreiber zu vertrauen. Dieses Testzertifikat wird nur verwendet, um RustDesk-Treibern zu vertrauen."),
+        ("install_cert_tip", "RustDesk-Zertifikat installieren"),
+        ("confirm_install_cert_tip", "Dies ist ein RustDesk-Testzertifikat, dem vertraut werden kann. Das Zertifikat wird verwendet, um RustDesk-Treibern bei Bedarf zu vertrauen und diese zu installieren."),
         ("RDP Settings", "RDP-Einstellungen"),
         ("Sort by", "Sortieren nach"),
         ("New Connection", "Neue Verbindung"),
@@ -505,12 +506,42 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
         ("Enable", "Aktivieren"),
         ("Disable", "Deaktivieren"),
         ("Options", "Einstellungen"),
-        ("resolution_original_tip", "Originalauflösung"),
+        ("resolution_original_tip", "Originale Auflösung"),
         ("resolution_fit_local_tip", "Lokale Auflösung anpassen"),
         ("resolution_custom_tip", "Benutzerdefinierte Auflösung"),
         ("Collapse toolbar", "Symbolleiste einklappen"),
         ("Accept and Elevate", "Akzeptieren und Rechte erhöhen"),
         ("accept_and_elevate_btn_tooltip", "Akzeptieren Sie die Verbindung und erhöhen Sie die UAC-Berechtigungen."),
         ("clipboard_wait_response_timeout_tip", "Zeitüberschreitung beim Warten auf die Antwort der Kopie."),
+        ("Incoming connection", "Eingehende Verbindung"),
+        ("Outgoing connection", "Ausgehende Verbindung"),
+        ("Exit", "Beenden"),
+        ("Open", "Öffnen"),
+        ("logout_tip", "Sind Sie sicher, dass Sie sich abmelden wollen?"),
+        ("Service", "Vermittlungsdienst"),
+        ("Start", "Start"),
+        ("Stop", "Stopp"),
+        ("exceed_max_devices", "Sie haben die maximale Anzahl der verwalteten Geräte erreicht."),
+        ("Sync with recent sessions", "Synchronisierung mit den letzten Sitzungen"),
+        ("Sort tags", "Tags sortieren"),
+        ("Open connection in new tab", "Verbindung in neuem Tab öffnen"),
+        ("Move tab to new window", "Tab in neues Fenster verschieben"),
+        ("Can not be empty", "Darf nicht leer sein"),
+        ("Already exists", "Existiert bereits"),
+        ("Change Password", "Passwort ändern"),
+        ("Refresh Password", "Passwort aktualisieren"),
+        ("ID", "ID"),
+        ("Grid View", "Rasteransicht"),
+        ("List View", "Listenansicht"),
+        ("Select", "Auswählen"),
+        ("Toggle Tags", "Tags umschalten"),
+        ("pull_ab_failed_tip", "Aktualisierung des Adressbuchs fehlgeschlagen"),
+        ("push_ab_failed_tip", "Synchronisierung des Adressbuchs mit dem Server fehlgeschlagen"),
+        ("synced_peer_readded_tip", "Die Geräte, die in den letzten Sitzungen vorhanden waren, werden erneut zum Adressbuch hinzugefügt."),
+        ("Change Color", "Farbe ändern"),
+        ("Primary Color", "Primärfarbe"),
+        ("HSV Color", "HSV-Farbe"),
+        ("Installation Successful!", ""),
+        ("Installation failed!", ""),
     ].iter().cloned().collect();
 }
diff --git a/src/lang/el.rs b/src/lang/el.rs
index 6eacba007..4e7a83017 100644
--- a/src/lang/el.rs
+++ b/src/lang/el.rs
@@ -53,7 +53,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
         ("Audio Input", "Είσοδος ήχου"),
         ("Enhancements", "Βελτιώσεις"),
         ("Hardware Codec", "Κωδικοποιητής υλικού"),
-        ("Adaptive Bitrate", "Adaptive Bitrate"),
+        ("Adaptive bitrate", "Adaptive bitrate"),
         ("ID Server", "Διακομιστής ID"),
         ("Relay Server", "Διακομιστής αναμετάδοσης"),
         ("API Server", "Διακομιστής API"),
@@ -233,6 +233,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
         ("Username missed", "Δεν συμπληρώσατε το όνομα χρήστη"),
         ("Password missed", "Δεν συμπληρώσατε τον κωδικό πρόσβασης"),
         ("Wrong credentials", "Λάθος διαπιστευτήρια"),
+        ("The verification code is incorrect or has expired", ""),
         ("Edit Tag", "Επεξεργασία ετικέτας"),
         ("Unremember Password", "Διαγραφή απομνημονευμένου κωδικού"),
         ("Favorites", "Αγαπημένα"),
@@ -459,8 +460,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
         ("Resolution", "Ανάλυση"),
         ("No transfers in progress", "Δεν υπάρχει μεταφορά σε εξέλιξη"),
         ("Set one-time password length", "Μέγεθος κωδικού μιας χρήσης"),
-        ("idd_driver_tip", "Εγκαταστήστε το πρόγραμμα οδήγησης εικονικής οθόνης που χρησιμοποιείται όταν δεν έχετε φυσικές οθόνες."),
-        ("confirm_idd_driver_tip", "Είναι ενεργοποιημένη η επιλογή εγκατάστασης του προγράμματος οδήγησης εικονικής οθόνης. Λάβετε υπόψη ότι θα εγκατασταθεί ένα δοκιμαστικό πιστοποιητικό για το πρόγραμμα οδήγησης εικονικής οθόνης. Αυτό το πιστοποιητικό θα χρησιμοποιηθεί μόνο για την πιστοποίηση των προγραμμάτων οδήγησης του Rustdesk."),
+        ("install_cert_tip", ""),
+        ("confirm_install_cert_tip", ""),
         ("RDP Settings", "Ρυθμίσεις RDP"),
         ("Sort by", "Ταξινόμηση κατά"),
         ("New Connection", "Νέα σύνδεση"),
@@ -512,5 +513,35 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
         ("Accept and Elevate", ""),
         ("accept_and_elevate_btn_tooltip", ""),
         ("clipboard_wait_response_timeout_tip", ""),
+        ("Incoming connection", ""),
+        ("Outgoing connection", ""),
+        ("Exit", ""),
+        ("Open", ""),
+        ("logout_tip", ""),
+        ("Service", ""),
+        ("Start", ""),
+        ("Stop", ""),
+        ("exceed_max_devices", ""),
+        ("Sync with recent sessions", ""),
+        ("Sort tags", ""),
+        ("Open connection in new tab", ""),
+        ("Move tab to new window", ""),
+        ("Can not be empty", ""),
+        ("Already exists", ""),
+        ("Change Password", ""),
+        ("Refresh Password", ""),
+        ("ID", ""),
+        ("Grid View", ""),
+        ("List View", ""),
+        ("Select", ""),
+        ("Toggle Tags", ""),
+        ("pull_ab_failed_tip", ""),
+        ("push_ab_failed_tip", ""),
+        ("synced_peer_readded_tip", ""),
+        ("Change Color", ""),
+        ("Primary Color", ""),
+        ("HSV Color", ""),
+        ("Installation Successful!", ""),
+        ("Installation failed!", ""),
     ].iter().cloned().collect();
 }
diff --git a/src/lang/en.rs b/src/lang/en.rs
index 9d759ada4..c949575c2 100644
--- a/src/lang/en.rs
+++ b/src/lang/en.rs
@@ -12,6 +12,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
         ("not_close_tcp_tip", "Don't close this window while you are using the tunnel"),
         ("setup_server_tip", "For faster connection, please set up your own server"),
         ("Auto Login", "Auto Login (Only valid if you set \"Lock after session end\")"),
+        ("Always connect via relay", "Always Connect via Relay"),
         ("whitelist_tip", "Only whitelisted IP can access me"),
         ("whitelist_sep", "Separated by comma, semicolon, spaces or new line"),
         ("Wrong credentials", "Wrong username or password"),
@@ -44,10 +45,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
         ("wait_accept_uac_tip", "Please wait for the remote user to accept the UAC dialog."),
         ("still_click_uac_tip", "Still requires the remote user to click OK on the UAC window of running RustDesk."),
         ("config_microphone", "In order to speak remotely, you need to grant RustDesk \"Record Audio\" permissions."),
-        ("relay_hint_tip", "It may not be possible to connect directly, you can try to connect via relay. \nIn addition, if you want to use relay on your first try, you can add the \"/r\" suffix to the ID, or select the option \"Always connect via relay\" in the peer card."),
+        ("relay_hint_tip", "It may not be possible to connect directly; you can try connecting via relay. Additionally, if you want to use a relay on your first attempt, you can add the \"/r\" suffix to the ID or select the option \"Always connect via relay\" in the card of recent sessions if it exists."),
         ("No transfers in progress", ""),
-        ("idd_driver_tip", "Install virtual display driver which is used when you have no physical displays."),
-        ("confirm_idd_driver_tip", "The option to install the virtual display driver is checked. Note that a test certificate will be installed to trust the virtual display driver. This test certificate will only be used to trust Rustdesk drivers."),
+        ("install_cert_tip", "Install RustDesk certificate"),
+        ("confirm_install_cert_tip", "This is a RustDesk testing certificate, which can be trusted. The certificate will be used to trust and install RustDesk drivers when required."),
         ("empty_recent_tip", "Oops, no recent sessions!\nTime to plan a new one."),
         ("empty_favorite_tip", "No favorite peers yet?\nLet's find someone to connect with and add it to your favorites!"),
         ("empty_lan_tip", "Oh no, it looks like we haven't discovered any peers yet."),
@@ -71,5 +72,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
         ("resolution_custom_tip", "Custom resolution"),
         ("accept_and_elevate_btn_tooltip", "Accept the connection and elevate UAC permissions."),
         ("clipboard_wait_response_timeout_tip", "Timed out waiting for copy response."),
+        ("logout_tip", "Are you sure you want to log out?"),
+        ("exceed_max_devices", "You have reached the maximum number of managed devices."),
+        ("pull_ab_failed_tip", "Failed to refresh address book"),
+        ("push_ab_failed_tip", "Failed to sync address book to server"),
+        ("synced_peer_readded_tip", "The devices that were present in the recent sessions will be synchronized back to the address book."),
         ].iter().cloned().collect();
 }
diff --git a/src/lang/eo.rs b/src/lang/eo.rs
index e232cb582..7cb5a1b2a 100644
--- a/src/lang/eo.rs
+++ b/src/lang/eo.rs
@@ -53,7 +53,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
         ("Audio Input", "Aŭdia enigo"),
         ("Enhancements", ""),
         ("Hardware Codec", ""),
-        ("Adaptive Bitrate", ""),
+        ("Adaptive bitrate", ""),
         ("ID Server", "Servilo de identigiloj"),
         ("Relay Server", "Relajsa servilo"),
         ("API Server", "Servilo de API"),
@@ -233,6 +233,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
         ("Username missed", "Uzantnomo forgesita"),
         ("Password missed", "Pasvorto forgesita"),
         ("Wrong credentials", "Identigilo aŭ pasvorto erara"),
+        ("The verification code is incorrect or has expired", ""),
         ("Edit Tag", "Redakti etikedo"),
         ("Unremember Password", "Forgesi pasvorton"),
         ("Favorites", "Favorataj"),
@@ -459,8 +460,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
         ("Resolution", ""),
         ("No transfers in progress", ""),
         ("Set one-time password length", ""),
-        ("idd_driver_tip", ""),
-        ("confirm_idd_driver_tip", ""),
+        ("install_cert_tip", ""),
+        ("confirm_install_cert_tip", ""),
         ("RDP Settings", ""),
         ("Sort by", ""),
         ("New Connection", ""),
@@ -512,5 +513,35 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
         ("Accept and Elevate", ""),
         ("accept_and_elevate_btn_tooltip", ""),
         ("clipboard_wait_response_timeout_tip", ""),
+        ("Incoming connection", ""),
+        ("Outgoing connection", ""),
+        ("Exit", ""),
+        ("Open", ""),
+        ("logout_tip", ""),
+        ("Service", ""),
+        ("Start", ""),
+        ("Stop", ""),
+        ("exceed_max_devices", ""),
+        ("Sync with recent sessions", ""),
+        ("Sort tags", ""),
+        ("Open connection in new tab", ""),
+        ("Move tab to new window", ""),
+        ("Can not be empty", ""),
+        ("Already exists", ""),
+        ("Change Password", ""),
+        ("Refresh Password", ""),
+        ("ID", ""),
+        ("Grid View", ""),
+        ("List View", ""),
+        ("Select", ""),
+        ("Toggle Tags", ""),
+        ("pull_ab_failed_tip", ""),
+        ("push_ab_failed_tip", ""),
+        ("synced_peer_readded_tip", ""),
+        ("Change Color", ""),
+        ("Primary Color", ""),
+        ("HSV Color", ""),
+        ("Installation Successful!", ""),
+        ("Installation failed!", ""),
     ].iter().cloned().collect();
 }
diff --git a/src/lang/es.rs b/src/lang/es.rs
index 28d8832f0..b1fc57954 100644
--- a/src/lang/es.rs
+++ b/src/lang/es.rs
@@ -17,7 +17,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
         ("Transfer File", "Transferir archivo"),
         ("Connect", "Conectar"),
         ("Recent Sessions", "Sesiones recientes"),
-        ("Address Book", "Libreta de direcciones"),
+        ("Address Book", "Directorio"),
         ("Confirmation", "Confirmación"),
         ("TCP Tunneling", "Túnel TCP"),
         ("Remove", "Quitar"),
@@ -37,23 +37,23 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
         ("Clipboard is empty", "El portapapeles está vacío"),
         ("Stop service", "Detener servicio"),
         ("Change ID", "Cambiar ID"),
-        ("Your new ID", ""),
-        ("length %min% to %max%", ""),
-        ("starts with a letter", ""),
-        ("allowed characters", ""),
+        ("Your new ID", "Tu nueva ID"),
+        ("length %min% to %max%", "de %min% a %max% de longitud"),
+        ("starts with a letter", "comenzar con una letra"),
+        ("allowed characters", "Caracteres permitidos"),
         ("id_change_tip", "Solo puedes usar caracteres a-z, A-Z, 0-9 e _ (guion bajo). El primer carácter debe ser a-z o A-Z. La longitud debe estar entre 6 y 16 caracteres."),
         ("Website", "Sitio web"),
         ("About", "Acerca de"),
         ("Slogan_tip", "¡Hecho con corazón en este mundo caótico!"),
         ("Privacy Statement", "Declaración de privacidad"),
         ("Mute", "Silenciar"),
-        ("Build Date", ""),
+        ("Build Date", "Fecha de compilación"),
         ("Version", ""),
-        ("Home", ""),
+        ("Home", "Inicio"),
         ("Audio Input", "Entrada de audio"),
         ("Enhancements", "Mejoras"),
         ("Hardware Codec", "Códec de hardware"),
-        ("Adaptive Bitrate", "Tasa de bits adaptativa"),
+        ("Adaptive bitrate", "Tasa de bits adaptativa"),
         ("ID Server", "Servidor de IDs"),
         ("Relay Server", "Servidor Relay"),
         ("API Server", "Servidor API"),
@@ -208,7 +208,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
         ("x11 expected", "x11 necesario"),
         ("Port", "Puerto"),
         ("Settings", "Ajustes"),
-        ("Username", " Nombre de usuario"),
+        ("Username", "Nombre de usuario"),
         ("Invalid port", "Puerto incorrecto"),
         ("Closed manually by the peer", "Cerrado manualmente por el par"),
         ("Enable remote configuration modification", "Habilitar modificación remota de configuración"),
@@ -233,6 +233,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
         ("Username missed", "Olvidó su nombre de usuario"),
         ("Password missed", "Olvidó su contraseña"),
         ("Wrong credentials", "Credenciales incorrectas"),
+        ("The verification code is incorrect or has expired", ""),
         ("Edit Tag", "Editar tag"),
         ("Unremember Password", "Olvidar contraseña"),
         ("Favorites", "Favoritos"),
@@ -411,7 +412,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
         ("wayland_experiment_tip", "El soporte para Wayland está en fase experimental, por favor, use X11 si necesita acceso desatendido."),
         ("Right click to select tabs", "Clic derecho para seleccionar pestañas"),
         ("Skipped", "Omitido"),
-        ("Add to Address Book", "Añadir a la libreta de direcciones"),
+        ("Add to Address Book", "Añadir al directorio"),
         ("Group", "Grupo"),
         ("Search", "Búsqueda"),
         ("Closed manually by web console", "Cerrado manualmente por la consola web"),
@@ -459,8 +460,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
         ("Resolution", "Resolución"),
         ("No transfers in progress", "No hay transferencias en curso"),
         ("Set one-time password length", "Establecer contraseña de un solo uso"),
-        ("idd_driver_tip", "Instalar controlador virtual de pantalla a usar cuando no hay pantalla física."),
-        ("confirm_idd_driver_tip", "La opción de instalar el controlador de pantalla virtual está marcada. Hay que tener en cuenta que se instalará un certificado de prueba para confirar en el controlador de pantalla. Este certificado solo se usará para confiar en controladores Rustdesk."),
+        ("install_cert_tip", ""),
+        ("confirm_install_cert_tip", ""),
         ("RDP Settings", "Ajustes RDP"),
         ("Sort by", "Ordenar por"),
         ("New Connection", "Nueva conexión"),
@@ -471,7 +472,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
         ("empty_recent_tip", "¡Vaya, no hay conexiones recientes!\nEs hora de planificar una nueva."),
         ("empty_favorite_tip", "¿Sin pares favoritos aún?\nEncontremos uno al que conectarte y ¡añádelo a tus favoritos!"),
         ("empty_lan_tip", "Oh no, parece que aún no has descubierto ningún par."),
-        ("empty_address_book_tip", "Parece que actualmente no hay pares en tu libreta de direcciones."),
+        ("empty_address_book_tip", "Parece que actualmente no hay pares en tu directorio."),
         ("eg: admin", "ej.: admin"),
         ("Empty Username", "Nombre de usuario vacío"),
         ("Empty Password", "Contraseña vacía"),
@@ -511,6 +512,36 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
         ("Collapse toolbar", "Contraer barra de herramientas"),
         ("Accept and Elevate", "Aceptar y Elevar"),
         ("accept_and_elevate_btn_tooltip", "Aceptar la conexión y elevar permisos UAC."),
-        ("clipboard_wait_response_timeout_tip", ""),
+        ("clipboard_wait_response_timeout_tip", "Tiempo de espera para copia agotado."),
+        ("Incoming connection", "Conexión entrante"),
+        ("Outgoing connection", "Conexión saliente"),
+        ("Exit", "Salir"),
+        ("Open", "Abrir"),
+        ("logout_tip", "¿Seguro que deseas cerrar sesión?"),
+        ("Service", "Servicio"),
+        ("Start", "Iniciar"),
+        ("Stop", "Detener"),
+        ("exceed_max_devices", "Has alcanzado el máximo número de dispositivos administrados."),
+        ("Sync with recent sessions", "Sincronizar con sesiones recientes"),
+        ("Sort tags", "Ordenar etiquetas"),
+        ("Open connection in new tab", "Abrir conexión en nueva pestaña"),
+        ("Move tab to new window", "Mover pestaña a nueva ventana"),
+        ("Can not be empty", "No puede estar vacío"),
+        ("Already exists", "Ya existe"),
+        ("Change Password", "Cambiar contraseña"),
+        ("Refresh Password", "Refrescar contraseña"),
+        ("ID", ""),
+        ("Grid View", "Vista Cuadrícula"),
+        ("List View", "Vista Lista"),
+        ("Select", "Seleccionar"),
+        ("Toggle Tags", "Alternar Etiquetas"),
+        ("pull_ab_failed_tip", "No se ha podido refrescar el directorio"),
+        ("push_ab_failed_tip", "No se ha podido sincronizar el directorio con el servidor"),
+        ("synced_peer_readded_tip", "Los dispositivos presentes en sesiones recientes se sincronizarán con el directorio."),
+        ("Change Color", "Cambiar Color"),
+        ("Primary Color", "Color Primario"),
+        ("HSV Color", "Color HSV"),
+        ("Installation Successful!", ""),
+        ("Installation failed!", ""),
     ].iter().cloned().collect();
 }
diff --git a/src/lang/fa.rs b/src/lang/fa.rs
index 10b49ee98..1284ec985 100644
--- a/src/lang/fa.rs
+++ b/src/lang/fa.rs
@@ -53,7 +53,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
         ("Audio Input", "ورودی صدا"),
         ("Enhancements", "بهبودها"),
         ("Hardware Codec", "کدک سخت افزاری"),
-        ("Adaptive Bitrate", "سازگار Bitrate"),
+        ("Adaptive bitrate", "سازگار Bitrate"),
         ("ID Server", "شناسه سرور"),
         ("Relay Server", "Relay سرور"),
         ("API Server", "API سرور"),
@@ -80,7 +80,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
         ("Connection in progress. Please wait.", "در حال اتصال. لطفا متظر بمانید"),
         ("Please try 1 minute later", "لطفا بعد از 1 دقیقه مجددا تلاش کنید"),
         ("Login Error", "ورود ناموفق بود"),
-        ("Successful", "ورود با موفقیت انجام شد"),
+        ("Successful", "با موفقیت انجام شد"),
         ("Connected, waiting for image...", "...ارتباط برقرار شد. انتظار برای دریافت تصاویر"),
         ("Name", "نام"),
         ("Type", "نوع فایل"),
@@ -144,7 +144,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
         ("Failed to connect via relay server", "انجام نشد Relay اتصال از طریق سرور"),
         ("Failed to make direct connection to remote desktop", "اتصال مستقیم به دسکتاپ راه دور انجام نشد"),
         ("Set Password", "تنظیم رمزعبور"),
-        ("OS Password", "رمز عیور سیستم عامل"),
+        ("OS Password", "رمز عبور سیستم عامل"),
         ("install_tip", "لطفا برنامه را نصب کنید UAC و جلوگیری از خطای RustDesk برای راحتی در استفاده از نرم افزار"),
         ("Click to upgrade", "برای ارتقا کلیک کنید"),
         ("Click to download", "برای دانلود کلیک کنید"),
@@ -233,6 +233,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
         ("Username missed", "نام کاربری وجود ندارد"),
         ("Password missed", "رمزعبور وجود ندارد"),
         ("Wrong credentials", "اعتبارنامه نادرست است"),
+        ("The verification code is incorrect or has expired", "کد تأیید نادرست است یا منقضی شده است"),
         ("Edit Tag", "ویرایش برچسب"),
         ("Unremember Password", "رمز عبور ذخیره نشود"),
         ("Favorites", "اتصالات دلخواه"),
@@ -459,8 +460,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
         ("Resolution", "وضوح"),
         ("No transfers in progress", "هیچ انتقالی در حال انجام نیست"),
         ("Set one-time password length", "طول رمز یکبار مصرف را تعیین کنید"),
-        ("idd_driver_tip", "درایور صفحه نمایش مجازی را نصب کنید این برای زمانیست که هیچ نمایشگر فیزیکی ندارید."),
-        ("confirm_idd_driver_tip", "استفاده خواهد شد Rustdesk گزینه نصب درایور نمایش مجازی تیک خورده است. توجه داشته باشید که یک گواهی آزمایشی برای اعتماد به درایور نمایش مجازی نصب خواهد شد. این گواهی آزمایشی فقط برای اعتماد به درایورهای."),
+        ("install_cert_tip", "RustDesk نصب گواهی"),
+        ("confirm_install_cert_tip", "استفاده خواهد شد RustDesk است و شما می توانید به این گواهی اعتماد کنید. این گواهی برای اعتماد و نصب درایورهای RustDesk این گواهینامه یک گواهی تست"),
         ("RDP Settings", "RDP تنظیمات"),
         ("Sort by", "مرتب سازی بر اساس"),
         ("New Connection", "اتصال جدید"),
@@ -511,6 +512,36 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
         ("Collapse toolbar", "جمع کردن نوار ابزار"),
         ("Accept and Elevate", "بپذیرید و افزایش دهید"),
         ("accept_and_elevate_btn_tooltip", "را افزایش دهید UAC اتصال را بپذیرید و مجوزهای."),
-        ("clipboard_wait_response_timeout_tip", ""),
+        ("clipboard_wait_response_timeout_tip", "زمان انتظار برای مشخص شدن وضعیت کپی تمام شد."),
+        ("Incoming connection", "اتصال ورودی"),
+        ("Outgoing connection", "اتصال خروجی"),
+        ("Exit", "خروج"),
+        ("Open", "باز کردن"),
+        ("logout_tip", "آیا برای خارج شدن مطمئن هستید؟"),
+        ("Service", ""),
+        ("Start", ""),
+        ("Stop", ""),
+        ("exceed_max_devices", ""),
+        ("Sync with recent sessions", ""),
+        ("Sort tags", ""),
+        ("Open connection in new tab", ""),
+        ("Move tab to new window", ""),
+        ("Can not be empty", ""),
+        ("Already exists", ""),
+        ("Change Password", ""),
+        ("Refresh Password", ""),
+        ("ID", ""),
+        ("Grid View", ""),
+        ("List View", ""),
+        ("Select", ""),
+        ("Toggle Tags", ""),
+        ("pull_ab_failed_tip", ""),
+        ("push_ab_failed_tip", ""),
+        ("synced_peer_readded_tip", ""),
+        ("Change Color", ""),
+        ("Primary Color", ""),
+        ("HSV Color", ""),
+        ("Installation Successful!", ""),
+        ("Installation failed!", ""),
     ].iter().cloned().collect();
 }
diff --git a/src/lang/fr.rs b/src/lang/fr.rs
index e782e8dc0..fea4f7ffa 100644
--- a/src/lang/fr.rs
+++ b/src/lang/fr.rs
@@ -53,7 +53,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
         ("Audio Input", "Entrée audio"),
         ("Enhancements", "Améliorations"),
         ("Hardware Codec", "Transcodage matériel"),
-        ("Adaptive Bitrate", "Débit adaptatif"),
+        ("Adaptive bitrate", "Débit adaptatif"),
         ("ID Server", "Serveur ID"),
         ("Relay Server", "Serveur relais"),
         ("API Server", "Serveur API"),
@@ -75,7 +75,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
         ("Do you want to enter again?", "Voulez-vous participer à nouveau ?"),
         ("Connection Error", "Erreur de connexion"),
         ("Error", "Erreur"),
-        ("Reset by the peer", "La connexion a été fermée par la machine distante"),
+        ("Reset by the peer", "La connexion a été fermée par l'appareil distant"),
         ("Connecting...", "Connexion..."),
         ("Connection in progress. Please wait.", "Connexion en cours. Veuillez patienter."),
         ("Please try 1 minute later", "Réessayez dans une minute"),
@@ -92,8 +92,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
         ("Refresh File", "Rafraîchir le contenu"),
         ("Local", "Local"),
         ("Remote", "Distant"),
-        ("Remote Computer", "Ordinateur distant"),
-        ("Local Computer", "Ordinateur local"),
+        ("Remote Computer", "Appareil distant"),
+        ("Local Computer", "Appareil local"),
         ("Confirm Delete", "Confirmer la suppression"),
         ("Delete", "Supprimer"),
         ("Properties", "Propriétés"),
@@ -129,9 +129,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
         ("Show remote cursor", "Afficher le curseur distant"),
         ("Show quality monitor", "Afficher le moniteur de qualité"),
         ("Disable clipboard", "Désactiver le presse-papier"),
-        ("Lock after session end", "Verrouiller l'ordinateur distant après la déconnexion"),
+        ("Lock after session end", "Verrouiller l'appareil distant après la déconnexion"),
         ("Insert", "Envoyer"),
-        ("Insert Lock", "Verrouiller l'ordinateur distant"),
+        ("Insert Lock", "Verrouiller l'appareil distant"),
         ("Refresh", "Rafraîchir l'écran"),
         ("ID does not exist", "L'ID n'existe pas"),
         ("Failed to connect to rendezvous server", "Échec de la connexion au serveur rendezvous"),
@@ -188,7 +188,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
         ("Relayed and encrypted connection", "Connexion relais chiffrée"),
         ("Direct and unencrypted connection", "Connexion directe non chiffrée"),
         ("Relayed and unencrypted connection", "Connexion relais non chiffrée"),
-        ("Enter Remote ID", "Entrer l'ID de l'appareil à distance"),
+        ("Enter Remote ID", "Entrer l'ID de l'appareil distant"),
         ("Enter your password", "Entrer votre mot de passe"),
         ("Logging in...", "En cours de connexion ..."),
         ("Enable RDP session sharing", "Activer le partage de session RDP"),
@@ -210,7 +210,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
         ("Settings", "Paramètres"),
         ("Username", " Nom d'utilisateur"),
         ("Invalid port", "Port invalide"),
-        ("Closed manually by the peer", "Fermé manuellement par la machine distante"),
+        ("Closed manually by the peer", "Fermé manuellement par l'appareil distant"),
         ("Enable remote configuration modification", "Autoriser la modification de la configuration à distance"),
         ("Run without install", "Exécuter sans installer"),
         ("Connect via relay", "Connexion via relais"),
@@ -223,17 +223,18 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
         ("Verification code", "Code de vérification"),
         ("verification_tip", "Un nouvel appareil a été détecté et un code de vérification a été envoyé à l'adresse e-mail enregistrée, entrez le code de vérification pour continuer la connexion."),
         ("Logout", "Déconnexion"),
-        ("Tags", "Étiqueter"),
+        ("Tags", "Étiquettes"),
         ("Search ID", "Rechercher un ID"),
         ("whitelist_sep", "Vous pouvez utiliser une virgule, un point-virgule, un espace ou une nouvelle ligne comme séparateur"),
         ("Add ID", "Ajouter un ID"),
-        ("Add Tag", "Ajouter une balise"),
-        ("Unselect all tags", "Désélectionner toutes les balises"),
+        ("Add Tag", "Ajout étiquette(s)"),
+        ("Unselect all tags", "Désélectionner toutes les étiquettes"),
         ("Network error", "Erreur réseau"),
         ("Username missed", "Nom d'utilisateur manquant"),
         ("Password missed", "Mot de passe manquant"),
         ("Wrong credentials", "Identifiant ou mot de passe erroné"),
-        ("Edit Tag", "Modifier la balise"),
+        ("The verification code is incorrect or has expired", "Le code de vérification est incorrect ou a expiré"),
+        ("Edit Tag", "Gestion étiquettes"),
         ("Unremember Password", "Oublier le Mot de passe"),
         ("Favorites", "Favoris"),
         ("Add to Favorites", "Ajouter aux Favoris"),
@@ -244,7 +245,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
         ("Hostname", "Nom d'hôte"),
         ("Discovered", "Découvert"),
         ("install_daemon_tip", "Pour une exécution au démarrage du système, vous devez installer le service système."),
-        ("Remote ID", "ID de l'appareil à distance"),
+        ("Remote ID", "ID de l'appareil distant"),
         ("Paste", "Coller"),
         ("Paste here?", "Coller ici?"),
         ("Are you sure to close the connection?", "Êtes-vous sûr de fermer la connexion?"),
@@ -273,7 +274,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
         ("Chat", "Discuter"),
         ("Total", "Total"),
         ("items", "éléments"),
-        ("Selected", "Sélectionné"),
+        ("Selected", "Sélectionné(s)"),
         ("Screen Capture", "Capture d'écran"),
         ("Input Control", "Contrôle de saisie"),
         ("Audio Capture", "Capture audio"),
@@ -300,9 +301,9 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
         ("Succeeded", "Succès"),
         ("Someone turns on privacy mode, exit", "Quelqu'un active le mode de confidentialité, quittez"),
         ("Unsupported", "Non pris en charge"),
-        ("Peer denied", "Machine distante refusée"),
+        ("Peer denied", "Appareil distant refusé"),
         ("Please install plugins", "Veuillez installer les plugins"),
-        ("Peer exit", ""),
+        ("Peer exit", "Appareil distant déconnecté"),
         ("Failed to turn off", "Échec de la désactivation"),
         ("Turned off", "Désactivé"),
         ("In privacy mode", "en mode privé"),
@@ -336,8 +337,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
         ("Ratio", "Rapport"),
         ("Image Quality", "Qualité d'image"),
         ("Scroll Style", "Style de défilement"),
-        ("Show Toolbar", ""),
-        ("Hide Toolbar", ""),
+        ("Show Toolbar", "Afficher la barre d'outils"),
+        ("Hide Toolbar", "Masquer la barre d'outils"),
         ("Direct Connection", "Connexion directe"),
         ("Relay Connection", "Connexion relais"),
         ("Secure Connection", "Connexion sécurisée"),
@@ -366,8 +367,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
         ("Use IP Whitelisting", "Utiliser une liste blanche d'IP"),
         ("Network", "Réseau"),
         ("Enable RDP", "Activer connection RDP"),
-        ("Pin Toolbar", ""),
-        ("Unpin Toolbar", ""),
+        ("Pin Toolbar", "Épingler la barre d'outil"),
+        ("Unpin Toolbar", "Détacher la barre d'outil"),
         ("Recording", "Enregistrement"),
         ("Directory", "Répertoire"),
         ("Automatically record incoming sessions", "Enregistrement automatique des sessions entrantes"),
@@ -381,7 +382,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
         ("Write a message", "Ecrire un message"),
         ("Prompt", ""),
         ("Please wait for confirmation of UAC...", "Veuillez attendre la confirmation de l'UAC..."),
-        ("elevated_foreground_window_tip", "La fenêtre actuelle que la machine distante nécessite des privilèges plus élevés pour fonctionner, elle ne peut donc pas être atteinte par la souris et le clavier. Vous pouvez demander à l'utilisateur distant de réduire la fenêtre actuelle ou de cliquer sur le bouton d'élévation dans la fenêtre de gestion des connexions. Pour éviter ce problème, il est recommandé d'installer le logiciel sur l'appareil distant."),
+        ("elevated_foreground_window_tip", "La fenêtre actuelle de l'appareil distant nécessite des privilèges plus élevés pour fonctionner, elle ne peut donc pas être atteinte par la souris et le clavier. Vous pouvez demander à l'utilisateur distant de réduire la fenêtre actuelle ou de cliquer sur le bouton d'élévation dans la fenêtre de gestion des connexions. Pour éviter ce problème, il est recommandé d'installer le logiciel sur l'appareil distant."),
         ("Disconnected", "Déconnecté"),
         ("Other", "Divers"),
         ("Confirm before closing multiple tabs", "Confirmer avant de fermer plusieurs onglets"),
@@ -391,7 +392,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
         ("Wayland requires Ubuntu 21.04 or higher version.", "Wayland nécessite Ubuntu 21.04 ou une version supérieure."),
         ("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "Wayland nécessite une version supérieure de la distribution Linux. Veuillez essayer le bureau X11 ou changer votre système d'exploitation."),
         ("JumpLink", "Afficher"),
-        ("Please Select the screen to be shared(Operate on the peer side).", "Veuillez sélectionner l'écran à partager (côté machine distante)."),
+        ("Please Select the screen to be shared(Operate on the peer side).", "Veuillez sélectionner l'écran à partager (côté appareil distant)."),
         ("Show RustDesk", "Afficher RustDesk"),
         ("This PC", "Ce PC"),
         ("or", "ou"),
@@ -451,16 +452,16 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
         ("Auto", "Auto"),
         ("Other Default Options", "Autres options par défaut"),
         ("Voice call", "Appel voix"),
-        ("Text chat", "Conversation textuelfle"),
+        ("Text chat", "Conversation textuelle"),
         ("Stop voice call", "Stopper l'appel voix"),
-        ("relay_hint_tip", "Il se peut qu'il ne doit pas possible de se connecter directement, vous pouvez essayer de vous connecter via un relais. \nEn outre, si vous souhaitez utiliser directement le relais, vous pouvez ajouter le suffixe \"/r\" à l'ID ou sélectionner l'option \"Toujours se connecter via le relais\" dans la fiche pair."),
+        ("relay_hint_tip", "Il se peut qu'il ne doit pas possible de se connecter directement, vous pouvez essayer de vous connecter via un relais. \nEn outre, si vous souhaitez utiliser directement le relais, vous pouvez ajouter le suffixe \"/r\" à l'ID ou sélectionner l'option \"Toujours se connecter via le relais\" dans la fiche appareils distants."),
         ("Reconnect", "Se reconnecter"),
         ("Codec", "Codec"),
         ("Resolution", "Résolution"),
         ("No transfers in progress", "Pas de transfert en cours"),
         ("Set one-time password length", "Définir la longueur du mot de passe à usage unique"),
-        ("idd_driver_tip", "Installez le pilote d'affichage virtuel pour être utilisé lorsque vous n'avez pas d'affichage physique."),
-        ("confirm_idd_driver_tip", "L'option d'installation du pilote d'affichage virtuel est cochée. Notez qu'un certificat de test sera installé pour faire confiance au pilote d'affichage virtuel. Ce certificat de test ne sera utilisé que pour faire confiance aux pilotes Rustdesk."),
+        ("install_cert_tip", "Installer le certificat RustDesk"),
+        ("confirm_install_cert_tip", "Il s'agit d'un certificat RustDesk, auquel on peut faire confiance. Le certificat sera utilisé pour approuver et installer les pilotes RustDesk si nécessaire."),
         ("RDP Settings", "Configuration RDP"),
         ("Sort by", "Trier par"),
         ("New Connection", "Nouvelle connexion"),
@@ -469,14 +470,14 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
         ("Maximize", "Maximiser"),
         ("Your Device", "Votre appareil"),
         ("empty_recent_tip", "Oups, pas de sessions récentes!\nIl est temps d'en prévoir une nouvelle."),
-        ("empty_favorite_tip", "Vous n'avez pas encore de pairs favoris?\nTrouvons quelqu'un avec qui vous connecter et ajoutez-le à vos favoris!"),
-        ("empty_lan_tip", "Oh non, il semble que nous n'ayons pas encore de pairs découverts."),
-        ("empty_address_book_tip", "Ouh là là! il semble qu'il n'y ait actuellement aucun pair répertorié dans votre carnet d'adresses."),
+        ("empty_favorite_tip", "Vous n'avez pas encore d'appareils distants favorits?\nTrouvons quelqu'un avec qui vous connecter et ajoutez-les à vos favoris!"),
+        ("empty_lan_tip", "Oh non, il semble que nous n'ayons pas encore d'appareils réseau local découverts."),
+        ("empty_address_book_tip", "Ouh là là! il semble qu'il n'y ait actuellement aucun appareil distant répertorié dans votre carnet d'adresses."),
         ("eg: admin", "ex: admin"),
         ("Empty Username", "Nom d'utilisation non spécifié"),
         ("Empty Password", "Mot de passe non spécifié"),
         ("Me", "Moi"),
-        ("identical_file_tip", "Ce fichier est identique à celui du pair."),
+        ("identical_file_tip", "Ce fichier est identique à celui de l'appareil distant."),
         ("show_monitors_tip", "Afficher les moniteurs dans la barre d'outils"),
         ("View Mode", "Mode vue"),
         ("login_linux_tip", "Se connecter au compte Linux distant"),
@@ -497,20 +498,50 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
         ("Fingerprint", "Empreinte digitale"),
         ("Copy Fingerprint", "Copier empreinte digitale"),
         ("no fingerprints", "Pas d'empreintes digitales"),
-        ("Select a peer", "Sélectionnez la machine distante"),
-        ("Select peers", "Sélectionnez des machines distantes"),
+        ("Select a peer", "Sélectionnez l'appareil distant"),
+        ("Select peers", "Sélectionnez des appareils distants"),
         ("Plugins", "Plugins"),
         ("Uninstall", "Désinstaller"),
         ("Update", "Mise à jour"),
         ("Enable", "Activé"),
         ("Disable", "Desactivé"),
         ("Options", "Options"),
-        ("resolution_original_tip", ""),
-        ("resolution_fit_local_tip", ""),
-        ("resolution_custom_tip", ""),
-        ("Collapse toolbar", ""),
-        ("Accept and Elevate", ""),
-        ("accept_and_elevate_btn_tooltip", ""),
-        ("clipboard_wait_response_timeout_tip", ""),
+        ("resolution_original_tip", "Résolution d'origine"),
+        ("resolution_fit_local_tip", "Adapter la résolution local"),
+        ("resolution_custom_tip", "Résolution personnalisée"),
+        ("Collapse toolbar", "Réduire la barre d'outils"),
+        ("Accept and Elevate", "Accepter et autoriser l'augmentation des privilèges"),
+        ("accept_and_elevate_btn_tooltip", "Accepter la connexion l'augmentation des privilèges UAC."),
+        ("clipboard_wait_response_timeout_tip", "Expiration du délai d'attente presse-papiers."),
+        ("Incoming connection", "Connexion entrante"),
+        ("Outgoing connection", "Connexion sortante"),
+        ("Exit", "Quitter"),
+        ("Open", "Ouvrir"),
+        ("logout_tip", "Êtes-vous sûr de vouloir vous déconnecter?"),
+        ("Service", "Service"),
+        ("Start", "Lancer"),
+        ("Stop", "Stopper"),
+        ("exceed_max_devices", "Vous avez atteint le nombre maximal d'appareils gérés."),
+        ("Sync with recent sessions", "Synchroniser avec les sessions récentes"),
+        ("Sort tags", "Trier les étiquettes"),
+        ("Open connection in new tab", "Ouvrir la connexion dans un nouvel onglet"),
+        ("Move tab to new window", "Déplacer l'onglet vers une nouvelle fenêtre"),
+        ("Can not be empty", "Ne peux pas être vide"),
+        ("Already exists", "Existe déjà"),
+        ("Change Password", "Changer le mot de passe"),
+        ("Refresh Password", "Actualiser le mot de passe"),
+        ("ID", "ID"),
+        ("Grid View", "Vue Grille"),
+        ("List View", "Vue Liste"),
+        ("Select", "Sélectionner"),
+        ("Toggle Tags", "Basculer vers les étiquettes"),
+        ("pull_ab_failed_tip", "Impossible d'actualiser le carnet d'adresses"),
+        ("push_ab_failed_tip", "Échec de la synchronisation du carnet d'adresses"),
+        ("synced_peer_readded_tip", "Les appareils qui étaient présents dans les sessions récentes seront synchronisés avec le carnet d'adresses."),
+        ("Change Color", "Modifier la couleur"),
+        ("Primary Color", "Couleur primaire"),
+        ("HSV Color", "Couleur TSL"),
+        ("Installation Successful!", ""),
+        ("Installation failed!", ""),
     ].iter().cloned().collect();
 }
diff --git a/src/lang/hu.rs b/src/lang/hu.rs
index d9b95eeb0..a9089de35 100644
--- a/src/lang/hu.rs
+++ b/src/lang/hu.rs
@@ -53,7 +53,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
         ("Audio Input", "Hangátvitel"),
         ("Enhancements", "Fejlesztések"),
         ("Hardware Codec", "Hardware kodek"),
-        ("Adaptive Bitrate", "Adaptív bitráta"),
+        ("Adaptive bitrate", "Adaptív bitráta"),
         ("ID Server", "Szerver azonosító/domain"),
         ("Relay Server", "Kiszolgáló szerver"),
         ("API Server", "API szerver"),
@@ -233,6 +233,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
         ("Username missed", "Üres felhasználónév"),
         ("Password missed", "Üres jelszó"),
         ("Wrong credentials", "Hibás felhasználónév vagy jelszó"),
+        ("The verification code is incorrect or has expired", ""),
         ("Edit Tag", "Címke szerkesztése"),
         ("Unremember Password", "A jelszó megjegyzésének törlése"),
         ("Favorites", "Kedvencek"),
@@ -459,8 +460,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
         ("Resolution", ""),
         ("No transfers in progress", ""),
         ("Set one-time password length", ""),
-        ("idd_driver_tip", ""),
-        ("confirm_idd_driver_tip", ""),
+        ("install_cert_tip", ""),
+        ("confirm_install_cert_tip", ""),
         ("RDP Settings", ""),
         ("Sort by", ""),
         ("New Connection", ""),
@@ -512,5 +513,35 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
         ("Accept and Elevate", ""),
         ("accept_and_elevate_btn_tooltip", ""),
         ("clipboard_wait_response_timeout_tip", ""),
+        ("Incoming connection", ""),
+        ("Outgoing connection", ""),
+        ("Exit", ""),
+        ("Open", ""),
+        ("logout_tip", ""),
+        ("Service", ""),
+        ("Start", ""),
+        ("Stop", ""),
+        ("exceed_max_devices", ""),
+        ("Sync with recent sessions", ""),
+        ("Sort tags", ""),
+        ("Open connection in new tab", ""),
+        ("Move tab to new window", ""),
+        ("Can not be empty", ""),
+        ("Already exists", ""),
+        ("Change Password", ""),
+        ("Refresh Password", ""),
+        ("ID", ""),
+        ("Grid View", ""),
+        ("List View", ""),
+        ("Select", ""),
+        ("Toggle Tags", ""),
+        ("pull_ab_failed_tip", ""),
+        ("push_ab_failed_tip", ""),
+        ("synced_peer_readded_tip", ""),
+        ("Change Color", ""),
+        ("Primary Color", ""),
+        ("HSV Color", ""),
+        ("Installation Successful!", ""),
+        ("Installation failed!", ""),
     ].iter().cloned().collect();
 }
diff --git a/src/lang/id.rs b/src/lang/id.rs
index 27cdcf1f1..27676a420 100644
--- a/src/lang/id.rs
+++ b/src/lang/id.rs
@@ -4,7 +4,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
         ("Status", "Status"),
         ("Your Desktop", "Desktop Anda"),
         ("desk_tip", "Desktop Anda dapat diakses dengan ID dan kata sandi ini."),
-        ("Password", "Password"),
+        ("Password", "Kata sandi"),
         ("Ready", "Siap"),
         ("Established", "Didirikan"),
         ("connecting_status", "Menghubungkan ke jaringan RustDesk..."),
@@ -15,48 +15,48 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
         ("not_ready_status", "Belum siap. Silakan periksa koneksi Anda"),
         ("Control Remote Desktop", "Kontrol Remote Desktop"),
         ("Transfer File", "File Transfer"),
-        ("Connect", "Terhubung"),
+        ("Connect", "Hubungkan"),
         ("Recent Sessions", "Sesi Terkini"),
         ("Address Book", "Buku Alamat"),
         ("Confirmation", "Konfirmasi"),
         ("TCP Tunneling", "TCP Tunneling"),
         ("Remove", "Hapus"),
-        ("Refresh random password", "Segarkan kata sandi acak"),
-        ("Set your own password", "Tetapkan kata sandi Anda sendiri"),
+        ("Refresh random password", "Perbarui kata sandi acak"),
+        ("Set your own password", "Tetapkan kata sandi Anda"),
         ("Enable Keyboard/Mouse", "Aktifkan Keyboard/Mouse"),
         ("Enable Clipboard", "Aktifkan Papan Klip"),
         ("Enable File Transfer", "Aktifkan Transfer File"),
         ("Enable TCP Tunneling", "Aktifkan TCP Tunneling"),
-        ("IP Whitelisting", "Daftar Putih IP"),
+        ("IP Whitelisting", "Daftar IP yang diizinkan"),
         ("ID/Relay Server", "ID/Relay Server"),
         ("Import Server Config", "Impor Konfigurasi Server"),
-        ("Export Server Config", "Ekspor Konfigutasi Server"),
+        ("Export Server Config", "Ekspor Konfigurasi Server"),
         ("Import server configuration successfully", "Impor konfigurasi server berhasil"),
         ("Export server configuration successfully", "Ekspor konfigurasi server berhasil"),
         ("Invalid server configuration", "Konfigurasi server tidak valid"),
         ("Clipboard is empty", "Papan klip kosong"),
         ("Stop service", "Hentikan Layanan"),
         ("Change ID", "Ubah ID"),
-        ("Your new ID", ""),
-        ("length %min% to %max%", ""),
-        ("starts with a letter", ""),
-        ("allowed characters", ""),
+        ("Your new ID", "ID baru anda"),
+        ("length %min% to %max%", "panjang %min% s/d %max%"),
+        ("starts with a letter", "Dimulai dengan huruf"),
+        ("allowed characters", "Karakter yang dapat digunakan"),
         ("id_change_tip", "Hanya karakter a-z, A-Z, 0-9 dan _ (underscore) yang diperbolehkan. Huruf pertama harus a-z, A-Z. Panjang antara 6 dan 16."),
-        ("Website", "Website"),
+        ("Website", "Situs Web"),
         ("About", "Tentang"),
-        ("Slogan_tip", ""),
+        ("Slogan_tip", "Dibuat dengan penuh kasih sayang dalam dunia yang penuh kekacauan ini"),
         ("Privacy Statement", "Pernyataan Privasi"),
         ("Mute", "Bisukan"),
-        ("Build Date", ""),
-        ("Version", ""),
+        ("Build Date", "Tanggal Build"),
+        ("Version", "Versi"),
         ("Home", ""),
-        ("Audio Input", "Masukkan Audio"),
+        ("Audio Input", "Input Audio"),
         ("Enhancements", "Peningkatan"),
         ("Hardware Codec", "Codec Perangkat Keras"),
-        ("Adaptive Bitrate", "Kecepatan Bitrate Adaptif"),
+        ("Adaptive bitrate", "Kecepatan Bitrate Adaptif"),
         ("ID Server", "Server ID"),
         ("Relay Server", "Server Relay"),
-        ("API Server", "API Server"),
+        ("API Server", "Server API"),
         ("invalid_http", "harus dimulai dengan http:// atau https://"),
         ("Invalid IP", "IP tidak valid"),
         ("Invalid format", "Format tidak valid"),
@@ -66,16 +66,16 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
         ("Cancel", "Batal"),
         ("Skip", "Lanjutkan"),
         ("Close", "Tutup"),
-        ("Retry", "Ulangi"),
+        ("Retry", "Coba lagi"),
         ("OK", "Oke"),
         ("Password Required", "Kata sandi dibutuhkan"),
         ("Please enter your password", "Silahkan masukkan kata sandi anda"),
-        ("Remember password", "Ingat Password"),
+        ("Remember password", "Ingat kata sandi"),
         ("Wrong Password", "Kata sandi Salah"),
         ("Do you want to enter again?", "Apakah anda ingin masuk lagi?"),
         ("Connection Error", "Kesalahan koneksi"),
         ("Error", "Kesalahan"),
-        ("Reset by the peer", "Setel ulang oleh rekan"),
+        ("Reset by the peer", "Direset oleh rekan"),
         ("Connecting...", "Menghubungkan..."),
         ("Connection in progress. Please wait.", "Koneksi sedang berlangsung. Mohon tunggu."),
         ("Please try 1 minute later", "Silahkan coba 1 menit lagi"),
@@ -114,19 +114,19 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
         ("Speed", "Kecepatan"),
         ("Custom Image Quality", "Sesuaikan Kualitas Gambar"),
         ("Privacy mode", "Mode Privasi"),
-        ("Block user input", "Blokir masukan pengguna"),
-        ("Unblock user input", "Jangan blokir masukan pengguna"),
+        ("Block user input", "Blokir input pengguna"),
+        ("Unblock user input", "Jangan blokir input pengguna"),
         ("Adjust Window", "Sesuaikan Jendela"),
         ("Original", "Asli"),
         ("Shrink", "Susutkan"),
         ("Stretch", "Regangkan"),
-        ("Scrollbar", "Scroll bar"),
-        ("ScrollAuto", "Gulir Otomatis"),
+        ("Scrollbar", "Scrollbar"),
+        ("ScrollAuto", "Scroll Otomatis"),
         ("Good image quality", "Kualitas Gambar Baik"),
         ("Balanced", "Seimbang"),
         ("Optimize reaction time", "Optimalkan waktu reaksi"),
         ("Custom", "Kustom"),
-        ("Show remote cursor", "Tampilkan remote kursor"),
+        ("Show remote cursor", "Tampilkan kursor remote"),
         ("Show quality monitor", "Tampilkan kualitas monitor"),
         ("Disable clipboard", "Matikan papan klip"),
         ("Lock after session end", "Kunci setelah sesi berakhir"),
@@ -143,36 +143,36 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
         ("Failed to connect via rendezvous server", "Gagal terkoneksi via rendezvous server"),
         ("Failed to connect via relay server", "Gagal terkoneksi via relay server"),
         ("Failed to make direct connection to remote desktop", "Gagal membuat koneksi langsung ke desktop jarak jauh"),
-        ("Set Password", "Tetapkan Password"),
+        ("Set Password", "Tetapkan kata sandi"),
         ("OS Password", "Kata Sandi OS"),
         ("install_tip", "Karena UAC, RustDesk tidak dapat bekerja dengan baik sebagai sisi remote dalam beberapa kasus. Untuk menghindari UAC, silakan klik tombol di bawah ini untuk menginstal RustDesk ke sistem."),
         ("Click to upgrade", "Klik untuk upgrade"),
-        ("Click to download", "Kli untuk download"),
-        ("Click to update", "Klik untuk update"),
+        ("Click to download", "Klik untuk unduh"),
+        ("Click to update", "Klik untuk memperbarui"),
         ("Configure", "Konfigurasi"),
         ("config_acc", "Untuk mengontrol Desktop Anda dari jarak jauh, Anda perlu memberikan izin \"Aksesibilitas\" RustDesk."),
         ("config_screen", "Untuk mengakses Desktop Anda dari jarak jauh, Anda perlu memberikan izin \"Perekaman Layar\" RustDesk."),
         ("Installing ...", "Menginstall"),
         ("Install", "Instal"),
         ("Installation", "Instalasi"),
-        ("Installation Path", "Jalur Instalasi"),
+        ("Installation Path", "Direktori Instalasi"),
         ("Create start menu shortcuts", "Buat pintasan start menu"),
         ("Create desktop icon", "Buat icon desktop"),
         ("agreement_tip", "Dengan memulai instalasi, Anda menerima perjanjian lisensi."),
         ("Accept and Install", "Terima dan Install"),
-        ("End-user license agreement", "Perjanjian lisensi pengguna akhir"),
-        ("Generating ...", "Menghasilkan..."),
+        ("End-user license agreement", "Perjanjian lisensi pengguna"),
+        ("Generating ...", "Memproses..."),
         ("Your installation is lower version.", "Instalasi Anda adalah versi yang lebih rendah."),
         ("not_close_tcp_tip", "Jangan tutup jendela ini saat menggunakan tunnel"),
-        ("Listening ...", "Mendengarkan..."),
-        ("Remote Host", "Remote Host"),
-        ("Remote Port", "Remote Port"),
+        ("Listening ...", "Menghubungkan..."),
+        ("Remote Host", "Host Remote"),
+        ("Remote Port", "Port Remote"),
         ("Action", "Aksi"),
         ("Add", "Tambah"),
         ("Local Port", "Port Lokal"),
         ("Local Address", "Alamat lokal"),
         ("Change Local Port", "Ubah Port Lokal"),
-        ("setup_server_tip", "Untuk koneksi yang lebih cepat, silakan atur server Anda sendiri"),
+        ("setup_server_tip", "Sudah siap, Untuk mendapatkan koneksi yang lebih baik, disarankan untuk menginstal di server anda sendiri"),
         ("Too short, at least 6 characters.", "Terlalu pendek, setidaknya 6 karekter."),
         ("The confirmation is not identical.", "Konfirmasi tidak identik."),
         ("Permissions", "Izin"),
@@ -182,46 +182,46 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
         ("Allow using keyboard and mouse", "Izinkan menggunakan keyboard dan mouse"),
         ("Allow using clipboard", "Izinkan menggunakan papan klip"),
         ("Allow hearing sound", "Izinkan mendengarkan suara"),
-        ("Allow file copy and paste", "Izinkan penyalinan dan tempel file"),
-        ("Connected", "Terkoneksi"),
+        ("Allow file copy and paste", "Izinkan salin dan tempel file"),
+        ("Connected", "Terhubung"),
         ("Direct and encrypted connection", "Koneksi langsung dan terenkripsi"),
-        ("Relayed and encrypted connection", "Koneksi relai dan terenkripsi"),
-        ("Direct and unencrypted connection", "Koneksi langsung dan tidak terenkripsi"),
-        ("Relayed and unencrypted connection", "Koneksi relai dan tidak terenkripsi"),
-        ("Enter Remote ID", "Masukkan Remote ID"),
-        ("Enter your password", "Masukkan password anda"),
+        ("Relayed and encrypted connection", "Koneksi relay dan terenkripsi"),
+        ("Direct and unencrypted connection", "Koneksi langsung dan tanpa enkripsi"),
+        ("Relayed and unencrypted connection", "Koneksi relay dan tanpa enkripsi"),
+        ("Enter Remote ID", "Masukkan ID Remote"),
+        ("Enter your password", "Masukkan kata sandi anda"),
         ("Logging in...", "Masuk..."),
         ("Enable RDP session sharing", "Aktifkan berbagi sesi RDP"),
-        ("Auto Login", "Auto Login (Hanya valid jika Anda menyetel \"Kunci setelah sesi berakhir\")"),
+        ("Auto Login", "Login Otomatis (Hanya berlaku jika Anda mengatur \"Kunci setelah sesi berakhir\")"),
         ("Enable Direct IP Access", "Aktifkan Akses IP Langsung"),
         ("Rename", "Ubah nama"),
         ("Space", "Spasi"),
         ("Create Desktop Shortcut", "Buat Pintasan Desktop"),
-        ("Change Path", "Ubah Jalur"),
+        ("Change Path", "Ubah Direktori"),
         ("Create Folder", "Buat Folder"),
         ("Please enter the folder name", "Silahkan masukkan nama folder"),
-        ("Fix it", "Memperbaiki"),
+        ("Fix it", "Perbaiki"),
         ("Warning", "Peringatan"),
         ("Login screen using Wayland is not supported", "Layar masuk menggunakan Wayland tidak didukung"),
         ("Reboot required", "Diperlukan boot ulang"),
         ("Unsupported display server", "Server tampilan tidak didukung "),
-        ("x11 expected", "x11 diharapkan"),
+        ("x11 expected", "Diperlukan x11"),
         ("Port", "Port"),
         ("Settings", "Pengaturan"),
-        ("Username", "Username"),
+        ("Username", "Nama pengguna"),
         ("Invalid port", "Kesalahan port"),
-        ("Closed manually by the peer", "Ditutup secara manual oleh peer"),
-        ("Enable remote configuration modification", "Aktifkan modifikasi konfigurasi jarak jauh"),
+        ("Closed manually by the peer", "Ditutup secara manual oleh rekan"),
+        ("Enable remote configuration modification", "Aktifkan modifikasi konfigurasi remotE"),
         ("Run without install", "Jalankan tanpa menginstal"),
-        ("Connect via relay", ""),
-        ("Always connect via relay", "Selalu terhubung melalui relai"),
-        ("whitelist_tip", "Hanya whitelisted IP yang dapat mengakses saya"),
+        ("Connect via relay", "Sambungkan via relay"),
+        ("Always connect via relay", "Selalu terhubung melalui relay"),
+        ("whitelist_tip", "Hanya IP yang diizikan dapat mengakses"),
         ("Login", "Masuk"),
-        ("Verify", ""),
-        ("Remember me", ""),
-        ("Trust this device", ""),
-        ("Verification code", ""),
-        ("verification_tip", ""),
+        ("Verify", "Verifikasi"),
+        ("Remember me", "Ingatkan saya"),
+        ("Trust this device", "Izinkan perangkat ini"),
+        ("Verification code", "Kode verifikasi"),
+        ("verification_tip", "Kode verifikasi sudah dikirim ke email yang terdaftar, masukkan kode verifikasi untuk melanjutkan."),
         ("Logout", "Keluar"),
         ("Tags", "Tag"),
         ("Search ID", "Cari ID"),
@@ -230,30 +230,31 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
         ("Add Tag", "Tambah Tag"),
         ("Unselect all tags", "Batalkan pilihan semua tag"),
         ("Network error", "Kesalahan Jaringan"),
-        ("Username missed", "Username tidak sesuai"),
+        ("Username missed", "Nama pengguna tidak sesuai"),
         ("Password missed", "Kata sandi tidak sesuai"),
-        ("Wrong credentials", "Username atau password salah"),
+        ("Wrong credentials", "Nama pengguna atau kata sandi salah"),
+        ("The verification code is incorrect or has expired", "Kode verifikasi salah atau sudah kadaluarsa"),
         ("Edit Tag", "Ubah Tag"),
-        ("Unremember Password", "Lupa Kata Sandi"),
+        ("Unremember Password", "Lupakan Kata Sandi"),
         ("Favorites", "Favorit"),
         ("Add to Favorites", "Tambah ke Favorit"),
         ("Remove from Favorites", "Hapus dari favorit"),
         ("Empty", "Kosong"),
         ("Invalid folder name", "Nama folder tidak valid"),
-        ("Socks5 Proxy", "Socks5 Proxy"),
+        ("Socks5 Proxy", "Proxy Socks5"),
         ("Hostname", "Hostname"),
         ("Discovered", "Telah ditemukan"),
         ("install_daemon_tip", "Untuk memulai saat boot, Anda perlu menginstal system service."),
-        ("Remote ID", "Remote ID"),
+        ("Remote ID", "ID Remote"),
         ("Paste", "Tempel"),
         ("Paste here?", "Tempel disini?"),
         ("Are you sure to close the connection?", "Apakah anda yakin akan menutup koneksi?"),
-        ("Download new version", "Untuk versi baru"),
-        ("Touch mode", "Mode Sentuh"),
+        ("Download new version", "Unduh versi baru"),
+        ("Touch mode", "Mode Layar Sentuh"),
         ("Mouse mode", "Mode Mouse"),
         ("One-Finger Tap", "Ketuk Satu Jari"),
         ("Left Mouse", "Mouse Kiri"),
-        ("One-Long Tap", "Ketuk Satu Panjang"),
+        ("One-Long Tap", "Ketuk Tahan"),
         ("Two-Finger Tap", "Ketuk Dua Jari"),
         ("Right Mouse", "Mouse Kanan"),
         ("One-Finger Move", "Gerakan Satu Jari"),
@@ -274,25 +275,25 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
         ("Total", "Total"),
         ("items", "item"),
         ("Selected", "Dipilih"),
-        ("Screen Capture", "Rekam Layar"),
-        ("Input Control", "kontrol input"),
+        ("Screen Capture", "Tangkapan Layar"),
+        ("Input Control", "Kontrol input"),
         ("Audio Capture", "Rekam Suara"),
         ("File Connection", "Koneksi File"),
-        ("Screen Connection", "koneksi layar"),
-        ("Do you accept?", "Apakah diperbolehkan?"),
+        ("Screen Connection", "Koneksi layar"),
+        ("Do you accept?", "Apakah anda setuju?"),
         ("Open System Setting", "Buka Pengaturan Sistem"),
-        ("How to get Android input permission?", ""),
+        ("How to get Android input permission?", "Bagaimana cara mendapatkan izin input dari Android?"),
         ("android_input_permission_tip1", "Agar perangkat jarak jauh dapat mengontrol perangkat Android Anda melalui mouse atau sentuhan, Anda harus mengizinkan RustDesk untuk menggunakan layanan \"Aksesibilitas\"."),
         ("android_input_permission_tip2", "Silakan buka halaman pengaturan sistem berikutnya, temukan dan masuk ke [Layanan Terinstal], aktifkan layanan [Input RustDesk]."),
-        ("android_new_connection_tip", "Permintaan kontrol baru telah diterima, yang ingin mengontrol perangkat Anda saat ini."),
-        ("android_service_will_start_tip", "Mengaktifkan \"Tangkapan Layar\" akan memulai layanan secara otomatis, memungkinkan perangkat lain untuk meminta sambungan ke perangkat Anda."),
-        ("android_stop_service_tip", "Menutup layanan akan secara otomatis menutup semua koneksi yang dibuat."),
+        ("android_new_connection_tip", "Permintaan akses remote telah diterima"),
+        ("android_service_will_start_tip", "Mengaktifkan \"Tangkapan Layar\" akan memulai secara otomatis, memungkinkan perangkat lain untuk meminta koneksi ke perangkat Anda."),
+        ("android_stop_service_tip", "Menutup layanan secara otomatis akan menutup semua koneksi yang dibuat."),
         ("android_version_audio_tip", "Versi Android saat ini tidak mendukung pengambilan audio, harap tingkatkan ke Android 10 atau lebih tinggi."),
-        ("android_start_service_tip", ""),
-        ("android_permission_may_not_change_tip", ""),
+        ("android_start_service_tip", "Tap [Mulai Layanan] atau aktifkan izin [Tangkapan Layar] untuk memulai berbagi layar."),
+        ("android_permission_may_not_change_tip", "Izin untuk koneksi yang sudah terhubung mungkin tidak dapat diubah secara instan hingga terhubung kembali"),
         ("Account", "Akun"),
-        ("Overwrite", "Timpa"),
-        ("This file exists, skip or overwrite this file?", "File ini sudah ada, lewati atau timpa file ini?"),
+        ("Overwrite", "Ganti"),
+        ("This file exists, skip or overwrite this file?", "File ini sudah ada, lewati atau ganti file ini?"),
         ("Quit", "Keluar"),
         ("doc_mac_permission", "https://rustdesk.com/docs/en/manual/mac/#enable-permissions"),
         ("Help", "Bantuan"),
@@ -300,32 +301,32 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
         ("Succeeded", "Berhasil"),
         ("Someone turns on privacy mode, exit", "Seseorang mengaktifkan mode privasi, keluar"),
         ("Unsupported", "Tidak didukung"),
-        ("Peer denied", "Rekan ditolak"),
+        ("Peer denied", "Rekan menolak"),
         ("Please install plugins", "Silakan instal plugin"),
-        ("Peer exit", "keluar rekan"),
+        ("Peer exit", "Rekan keluar"),
         ("Failed to turn off", "Gagal mematikan"),
-        ("Turned off", "Matikan"),
+        ("Turned off", "Dimatikan"),
         ("In privacy mode", "Dalam mode privasi"),
         ("Out privacy mode", "Keluar dari mode privasi"),
         ("Language", "Bahasa"),
-        ("Keep RustDesk background service", "Pertahankan RustDesk berjalan pada background service"),
+        ("Keep RustDesk background service", "Pertahankan RustDesk berjalan pada service background"),
         ("Ignore Battery Optimizations", "Abaikan Pengoptimalan Baterai"),
-        ("android_open_battery_optimizations_tip", ""),
-        ("Start on Boot", ""),
-        ("Start the screen sharing service on boot, requires special permissions", ""),
-        ("Connection not allowed", "Koneksi tidak dijinkan"),
-        ("Legacy mode", "Mode lama"),
+        ("android_open_battery_optimizations_tip", "Jika anda ingin menonaktifkan fitur ini, buka halam pengaturan, cari dan pilih [Baterai], Uncheck [Tidak dibatasi]"),
+        ("Start on Boot", "Mulai saat dihidupkan"),
+        ("Start the screen sharing service on boot, requires special permissions", "Mulai layanan berbagi layar saat sistem dinyalakan, memerlukan izin khusus."),
+        ("Connection not allowed", "Koneksi tidak dizinkan"),
+        ("Legacy mode", "Mode lawas"),
         ("Map mode", "Mode peta"),
         ("Translate mode", "Mode terjemahan"),
         ("Use permanent password", "Gunakan kata sandi permanaen"),
-        ("Use both passwords", "Gunakan kedua kata sandi "),
+        ("Use both passwords", "Gunakan kedua kata sandi"),
         ("Set permanent password", "Setel kata sandi permanen"),
-        ("Enable Remote Restart", "Aktifkan Restart Jarak Jauh"),
-        ("Allow remote restart", "Ijinkan Restart Jarak Jauh"),
-        ("Restart Remote Device", "Restart Perangkat Jarak Jauh"),
-        ("Are you sure you want to restart", "Apakah Anda yakin untuk memulai ulang"),
-        ("Restarting Remote Device", "Memulai Ulang Perangkat Jarak Jauh"),
-        ("remote_restarting_tip", ""),
+        ("Enable Remote Restart", "Aktifkan Restart Secara Remote"),
+        ("Allow remote restart", "Ijinkan Restart Secara Remote"),
+        ("Restart Remote Device", "Restart Perangkat Secara Remote"),
+        ("Are you sure you want to restart", "Apakah Anda yakin ingin merestart"),
+        ("Restarting Remote Device", "Merestart Perangkat Remote"),
+        ("remote_restarting_tip", "Perangkat remote sedang merestart, harap tutup pesan ini dan sambungkan kembali dengan kata sandi permanen setelah beberapa saat."),
         ("Copied", "Disalin"),
         ("Exit Fullscreen", "Keluar dari Layar Penuh"),
         ("Fullscreen", "Layar penuh"),
@@ -333,11 +334,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
         ("Select Monitor", "Pilih Monitor"),
         ("Control Actions", "Tindakan Kontrol"),
         ("Display Settings", "Pengaturan tampilan"),
-        ("Ratio", "Perbandingan"),
+        ("Ratio", "Rasio"),
         ("Image Quality", "Kualitas gambar"),
-        ("Scroll Style", "Gaya Gulir"),
-        ("Show Toolbar", ""),
-        ("Hide Toolbar", ""),
+        ("Scroll Style", "Gaya Scroll"),
+        ("Show Toolbar", "Tampilkan Toolbar"),
+        ("Hide Toolbar", "Sembunyikan Toolbar"),
         ("Direct Connection", "Koneksi langsung"),
         ("Relay Connection", "Koneksi Relay"),
         ("Secure Connection", "Koneksi aman"),
@@ -347,31 +348,31 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
         ("General", "Umum"),
         ("Security", "Keamanan"),
         ("Theme", "Tema"),
-        ("Dark Theme", "Tema gelap"),
-        ("Light Theme", ""),
+        ("Dark Theme", "Tema Gelap"),
+        ("Light Theme", "Tema Terang"),
         ("Dark", "Gelap"),
         ("Light", "Terang"),
-        ("Follow System", "Ikuti sistem"),
+        ("Follow System", "Ikuti Sistem"),
         ("Enable hardware codec", "Aktifkan codec perangkat keras"),
-        ("Unlock Security Settings", "Buka Kunci Pengaturan Keamanan"),
+        ("Unlock Security Settings", "Buka Keamanan Pengaturan"),
         ("Enable Audio", "Aktifkan Audio"),
-        ("Unlock Network Settings", "Buka Kunci Pengaturan Jaringan"),
+        ("Unlock Network Settings", "Buka Keamanan Pengaturan Jaringan"),
         ("Server", "Server"),
-        ("Direct IP Access", "Direct IP Access"),
+        ("Direct IP Access", "Akses IP Langsung"),
         ("Proxy", "Proxy"),
         ("Apply", "Terapkan"),
         ("Disconnect all devices?", "Putuskan sambungan semua perangkat?"),
-        ("Clear", ""),
-        ("Audio Input Device", ""),
-        ("Use IP Whitelisting", "Gunakan Daftar Putih IP"),
+        ("Clear", "Bersihkan"),
+        ("Audio Input Device", "Input Perangkat Audio"),
+        ("Use IP Whitelisting", "Gunakan daftar IP yang diizinkan"),
         ("Network", "Jaringan"),
         ("Enable RDP", "Aktifkan RDP"),
-        ("Pin Toolbar", ""),
-        ("Unpin Toolbar", ""),
-        ("Recording", "Rekaman"),
+        ("Pin Toolbar", "Sematkan Toolbar"),
+        ("Unpin Toolbar", "Batal sematkan Toolbar"),
+        ("Recording", "Sedang Merekam"),
         ("Directory", "Direktori"),
         ("Automatically record incoming sessions", "Secara otomatis merekam sesi masuk"),
-        ("Change", "Mengubah"),
+        ("Change", "Ubah"),
         ("Start session recording", "Mulai sesi perekaman"),
         ("Stop session recording", "Hentikan sesi perekaman"),
         ("Enable Recording Session", "Aktifkan Sesi Perekaman"),
@@ -380,8 +381,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
         ("Deny LAN Discovery", "Tolak Penemuan LAN"),
         ("Write a message", "Menulis pesan"),
         ("Prompt", ""),
-        ("Please wait for confirmation of UAC...", ""),
-        ("elevated_foreground_window_tip", ""),
+        ("Please wait for confirmation of UAC...", "Harap tunggu konfirmasi UAC"),
+        ("elevated_foreground_window_tip", "Jendela remote desktop ini memerlukan hak akses khusus, jadi anda tidak bisa menggunakan mouse dan keyboard untuk sementara. Anda bisa meminta pihak pengguna yang diremote untuk menyembunyikan jendela ini atau klik tombol elevasi di jendela pengaturan koneksi. Untuk menghindari masalah ini, direkomendasikan untuk menginstall aplikasi secara permanen"),
         ("Disconnected", "Terputus"),
         ("Other", "Lainnya"),
         ("Confirm before closing multiple tabs", "Konfirmasi sebelum menutup banyak tab"),
@@ -390,127 +391,157 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
         ("Screen Share", "Berbagi Layar"),
         ("Wayland requires Ubuntu 21.04 or higher version.", "Wayland membutuhkan Ubuntu 21.04 atau versi yang lebih tinggi."),
         ("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "Wayland membutuhkan versi distro linux yang lebih tinggi. Silakan coba desktop X11 atau ubah OS Anda."),
-        ("JumpLink", "View"),
-        ("Please Select the screen to be shared(Operate on the peer side).", "Silakan Pilih layar yang akan dibagikan (Operasi di sisi rekan)."),
+        ("JumpLink", "Tautan Cepat"),
+        ("Please Select the screen to be shared(Operate on the peer side).", "Silakan Pilih layar yang akan dibagikan kepada rekan anda."),
         ("Show RustDesk", "Tampilkan RustDesk"),
         ("This PC", "PC ini"),
         ("or", "atau"),
         ("Continue with", "Lanjutkan dengan"),
-        ("Elevate", ""),
-        ("Zoom cursor", ""),
+        ("Elevate", "Elevasi"),
+        ("Zoom cursor", "Perbersar Kursor"),
         ("Accept sessions via password", "Izinkan sesi dengan kata sandi"),
         ("Accept sessions via click", "Izinkan sesi dengan klik"),
         ("Accept sessions via both", "Izinkan sesi dengan keduanya"),
-        ("Please wait for the remote side to accept your session request...", "Harap tunggu sisi jarak jauh untuk menerima permintaan sesi Anda..."),
+        ("Please wait for the remote side to accept your session request...", "Harap tunggu pihak pengguna remote untuk menerima permintaan sesi..."),
         ("One-time Password", "Kata sandi satu kali"),
         ("Use one-time password", "Gunakan kata sandi satu kali"),
-        ("One-time password length", ""),
-        ("Request access to your device", ""),
-        ("Hide connection management window", ""),
-        ("hide_cm_tip", ""),
-        ("wayland_experiment_tip", ""),
-        ("Right click to select tabs", ""),
-        ("Skipped", ""),
-        ("Add to Address Book", ""),
-        ("Group", ""),
+        ("One-time password length", "Panjang kata sandi satu kali pakai"),
+        ("Request access to your device", "Permintaan akses ke perangkat ini"),
+        ("Hide connection management window", "Sembunyikan jendela pengaturan koneksi"),
+        ("hide_cm_tip", "Izinkan untuk menyembunyikan hanya jika menerima sesi melalui kata sandi dan menggunakan kata sandi permanen"),
+        ("wayland_experiment_tip", "Dukungan Wayland masih dalam tahap percobaan, harap gunakan X11 jika Anda memerlukan akses tanpa pengawasan"),
+        ("Right click to select tabs", "Klik kanan untuk memilih tab"),
+        ("Skipped", "Dilewati"),
+        ("Add to Address Book", "Tambahkan ke Buku Alamat"),
+        ("Group", "Grup"),
         ("Search", "Pencarian"),
-        ("Closed manually by web console", ""),
-        ("Local keyboard type", ""),
-        ("Select local keyboard type", ""),
-        ("software_render_tip", ""),
-        ("Always use software rendering", ""),
-        ("config_input", ""),
-        ("config_microphone", ""),
-        ("request_elevation_tip", ""),
-        ("Wait", ""),
-        ("Elevation Error", ""),
-        ("Ask the remote user for authentication", ""),
-        ("Choose this if the remote account is administrator", ""),
-        ("Transmit the username and password of administrator", ""),
-        ("still_click_uac_tip", ""),
-        ("Request Elevation", ""),
-        ("wait_accept_uac_tip", ""),
-        ("Elevate successfully", ""),
-        ("uppercase", ""),
-        ("lowercase", ""),
-        ("digit", ""),
-        ("special character", ""),
-        ("length>=8", ""),
-        ("Weak", ""),
-        ("Medium", ""),
-        ("Strong", ""),
-        ("Switch Sides", ""),
-        ("Please confirm if you want to share your desktop?", ""),
-        ("Display", ""),
-        ("Default View Style", ""),
-        ("Default Scroll Style", ""),
-        ("Default Image Quality", ""),
-        ("Default Codec", ""),
-        ("Bitrate", ""),
-        ("FPS", ""),
-        ("Auto", ""),
-        ("Other Default Options", ""),
-        ("Voice call", ""),
-        ("Text chat", ""),
-        ("Stop voice call", ""),
-        ("relay_hint_tip", ""),
-        ("Reconnect", ""),
-        ("Codec", ""),
-        ("Resolution", ""),
-        ("No transfers in progress", ""),
-        ("Set one-time password length", ""),
-        ("idd_driver_tip", ""),
-        ("confirm_idd_driver_tip", ""),
-        ("RDP Settings", ""),
-        ("Sort by", ""),
-        ("New Connection", ""),
-        ("Restore", ""),
-        ("Minimize", ""),
-        ("Maximize", ""),
-        ("Your Device", ""),
-        ("empty_recent_tip", ""),
-        ("empty_favorite_tip", ""),
-        ("empty_lan_tip", ""),
-        ("empty_address_book_tip", ""),
+        ("Closed manually by web console", "Ditutup secara manual dari konsol web."),
+        ("Local keyboard type", "Tipe papan ketik"),
+        ("Select local keyboard type", "Pilih tipe papan ketik"),
+        ("software_render_tip", "Jika anda menggunakan kartu grafis Nvidia pada sistem linux dan jendela windows ditutup secara instan setelah terhung, silahkan ubah ke driver open-source Nouveau, dibutukan untuk merestart aplikasi"),
+        ("Always use software rendering", "Selalu gunakan software rendering"),
+        ("config_input", "Untuk menggunakan input keyboard remote, anda perlu memberikan izin \"Pemantauan Input\" pada RustDesk"),
+        ("config_microphone", "Untuk berbicara secara remote, anda perlu memberikan izin \"Rekam Audio\" pada RustDesk"),
+        ("request_elevation_tip", "Anda juga bisa meminta izin elevasi jika ada pihak pengguna remote"),
+        ("Wait", "Tunggu"),
+        ("Elevation Error", "Kesalahan Elevasi"),
+        ("Ask the remote user for authentication", "Minta pihak pengguna remote untuk otentikasi"),
+        ("Choose this if the remote account is administrator", "Pilih ini jika akun adalah \"administrator\""),
+        ("Transmit the username and password of administrator", "Transmisikan nama pengguna dan kata sandi administrator"),
+        ("still_click_uac_tip", "Masih memerlukan persetujuan pihak pengguna remote untuk mengklik OK pada jendela UAC RustDesk yang sedang berjalan"),
+        ("Request Elevation", "Permintaan Elevasi"),
+        ("wait_accept_uac_tip", "Harap tunggu pihak pengguna remote menerima jendela UAC."),
+        ("Elevate successfully", "Elevasi berhasil"),
+        ("uppercase", "Huruf besar"),
+        ("lowercase", "Huruf kecil"),
+        ("digit", "angka"),
+        ("special character", "Karakter spesial"),
+        ("length>=8", "panjang>=8"),
+        ("Weak", "Lemah"),
+        ("Medium", "Sedang"),
+        ("Strong", "Kuat"),
+        ("Switch Sides", "Ganti Posisi"),
+        ("Please confirm if you want to share your desktop?", "Harap konfirmasi apakah Anda ingin berbagi layar?"),
+        ("Display", "Tampilan"),
+        ("Default View Style", "Gaya Tampilan Default"),
+        ("Default Scroll Style", "Gaya Scroll Default"),
+        ("Default Image Quality", "Kualitas Gambar Default"),
+        ("Default Codec", "Codec default"),
+        ("Bitrate", "Bitrate"),
+        ("FPS", "FPS"),
+        ("Auto", "Otomatis"),
+        ("Other Default Options", "Opsi Default Lainnya"),
+        ("Voice call", "Panggilan suara"),
+        ("Text chat", "Obrolan Teks"),
+        ("Stop voice call", "Hentikan panggilan suara"),
+        ("relay_hint_tip", "Tidak memungkinkan untuk terhubung secara langsung; anda bisa mencoba terhubung via relay. Selain itu, jika ingin menggunakan relay pada percobaan pertama, silahkan tambah akhiran \"/r\" pada ID atau pilih \"Selalu terhubung via relay\" di pilihan sesi terbaru."),
+        ("Reconnect", "Menyambungkan ulang"),
+        ("Codec", "Codec"),
+        ("Resolution", "Resolusi"),
+        ("No transfers in progress", "Tidak ada transfer data yang sedang berlangsung"),
+        ("Set one-time password length", "Atur panjang kata sandi satu kali pakai"),
+        ("install_cert_tip", "Install sertifikat RustDesk"),
+        ("confirm_install_cert_tip", "Ini adalah sertifikat pengujian RustDesk, yang dapat dipercaya. Sertifikat ini akan digunakan untuk menginstal driver RustDesk saat diperlukan"),
+        ("RDP Settings", "Pengaturan RDP"),
+        ("Sort by", "Urutkan berdasarkan"),
+        ("New Connection", "Koneksi baru"),
+        ("Restore", "Mengembalikan"),
+        ("Minimize", "Meminimalkan"),
+        ("Maximize", "Memaksimalkan"),
+        ("Your Device", "Perangkat anda"),
+        ("empty_recent_tip", "Tidak ada sesi terbaru!"),
+        ("empty_favorite_tip", "Belum ada rekan favorit?\nTemukan seseorang untuk terhubung dan tambahkan ke favorit!"),
+        ("empty_lan_tip", "Sepertinya kami belum menemukan rekan"),
+        ("empty_address_book_tip", "Tampaknya saat ini tidak ada rekan yang terdaftar dalam buku alamat Anda"),
         ("eg: admin", ""),
-        ("Empty Username", ""),
-        ("Empty Password", ""),
-        ("Me", ""),
-        ("identical_file_tip", ""),
-        ("show_monitors_tip", ""),
-        ("View Mode", ""),
-        ("login_linux_tip", ""),
-        ("verify_rustdesk_password_tip", ""),
-        ("remember_account_tip", ""),
-        ("os_account_desk_tip", ""),
-        ("OS Account", ""),
-        ("another_user_login_title_tip", ""),
-        ("another_user_login_text_tip", ""),
-        ("xorg_not_found_title_tip", ""),
-        ("xorg_not_found_text_tip", ""),
-        ("no_desktop_title_tip", ""),
-        ("no_desktop_text_tip", ""),
-        ("No need to elevate", ""),
-        ("System Sound", ""),
-        ("Default", ""),
-        ("New RDP", ""),
+        ("Empty Username", "Nama pengguna kosong"),
+        ("Empty Password", "Kata sandi kosong"),
+        ("Me", "Saya"),
+        ("identical_file_tip", "Data ini identik dengan milik rekan"),
+        ("show_monitors_tip", "Tampilkan monitor di toolbar"),
+        ("View Mode", "Mode Tampilan"),
+        ("login_linux_tip", "Anda harus masuk ke akun remote linux untuk mengaktifkan sesi X desktop"),
+        ("verify_rustdesk_password_tip", "Verifikasi Kata Sandi RustDesk"),
+        ("remember_account_tip", "Ingat akun ini"),
+        ("os_account_desk_tip", "Akun ini digunakan untuk masuk ke sistem operasi remote dan mengaktifkan sesi desktop dalam mode tanpa tampilan (headless)"),
+        ("OS Account", "Akun OS"),
+        ("another_user_login_title_tip", "Akun ini sedang digunakan"),
+        ("another_user_login_text_tip", "Putuskan koneksi diperangkat lain"),
+        ("xorg_not_found_title_tip", "Xorg tidak ditemukan"),
+        ("xorg_not_found_text_tip", "Silahkan install Xorg"),
+        ("no_desktop_title_tip", "Desktop tidak tersedia"),
+        ("no_desktop_text_tip", "Silahkan install GNOME Desktop"),
+        ("No need to elevate", "Tidak perlu elevasi"),
+        ("System Sound", "Suara Sistem"),
+        ("Default", "Default"),
+        ("New RDP", "RDP Baru"),
         ("Fingerprint", ""),
         ("Copy Fingerprint", ""),
         ("no fingerprints", ""),
-        ("Select a peer", ""),
-        ("Select peers", ""),
-        ("Plugins", ""),
-        ("Uninstall", ""),
-        ("Update", ""),
-        ("Enable", ""),
-        ("Disable", ""),
-        ("Options", ""),
-        ("resolution_original_tip", ""),
-        ("resolution_fit_local_tip", ""),
-        ("resolution_custom_tip", ""),
+        ("Select a peer", "Pilih rekan"),
+        ("Select peers", "Pilih rekan-rekan"),
+        ("Plugins", "Plugin"),
+        ("Uninstall", "Hapus instalasi"),
+        ("Update", "Perbarui"),
+        ("Enable", "Aktifkan"),
+        ("Disable", "Nonaktifkan"),
+        ("Options", "Opsi"),
+        ("resolution_original_tip", "Resolusi original"),
+        ("resolution_fit_local_tip", "Sesuaikan resolusi lokal"),
+        ("resolution_custom_tip", "Resolusi kustom"),
         ("Collapse toolbar", ""),
-        ("Accept and Elevate", ""),
-        ("accept_and_elevate_btn_tooltip", ""),
-        ("clipboard_wait_response_timeout_tip", ""),
+        ("Accept and Elevate", "Terima dan Elevasi"),
+        ("accept_and_elevate_btn_tooltip", "Terima koneksi dan elevasi izin UAC"),
+        ("clipboard_wait_response_timeout_tip", "Batas waktu habis saat menunggu respons salinan"),
+        ("Incoming connection", "Koneksi akan masuk"),
+        ("Outgoing connection", "Koneksi akan keluar"),
+        ("Exit", "Keluar"),
+        ("Open", "Buka"),
+        ("logout_tip", "Apakah Anda yakin ingin keluar?"),
+        ("Service", "Service"),
+        ("Start", "Mulai"),
+        ("Stop", "Berhenti"),
+        ("exceed_max_devices", "Anda telah mencapai jumlah maksimal perangkat yang dikelola"),
+        ("Sync with recent sessions", "Sinkronkan dengan sesi terbaru"),
+        ("Sort tags", "Urutkan tag"),
+        ("Open connection in new tab", "Buka koneksi di tab baru"),
+        ("Move tab to new window", "Pindahkan tab ke jendela baru"),
+        ("Can not be empty", "Tidak boleh kosong"),
+        ("Already exists", "Sudah ada"),
+        ("Change Password", "Ganti kata sandi"),
+        ("Refresh Password", "Perbarui Kata Sandi"),
+        ("ID", "ID"),
+        ("Grid View", "Tampilan Kotak"),
+        ("List View", "Tampilan Daftar"),
+        ("Select", "Pilih"),
+        ("Toggle Tags", "Toggle Tag"),
+        ("pull_ab_failed_tip", "Gagal memuat ulang buku alamat"),
+        ("push_ab_failed_tip", "Gagal menyinkronkan buku alamat ke server"),
+        ("synced_peer_readded_tip", "Perangkat yang terdaftar dalam sesi-sesi terbaru akan di-sinkronkan kembali ke buku alamat."),
+        ("Change Color", "Ganti warna"),
+        ("Primary Color", "Warna utama"),
+        ("HSV Color", "Warna HSV"),
+        ("Installation Successful!", ""),
+        ("Installation failed!", ""),
     ].iter().cloned().collect();
 }
diff --git a/src/lang/it.rs b/src/lang/it.rs
index edc2eb9a8..07234b3f4 100644
--- a/src/lang/it.rs
+++ b/src/lang/it.rs
@@ -53,7 +53,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
         ("Audio Input", "Ingresso audio"),
         ("Enhancements", "Miglioramenti"),
         ("Hardware Codec", "Codec hardware"),
-        ("Adaptive Bitrate", "Bitrate adattivo"),
+        ("Adaptive bitrate", "Bitrate adattivo"),
         ("ID Server", "ID server"),
         ("Relay Server", "Server relay"),
         ("API Server", "Server API"),
@@ -223,17 +223,18 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
         ("Verification code", "Codice di verifica"),
         ("verification_tip", "È stato inviato un codice di verifica all'indirizzo email registrato, per accedere inserisci il codice di verifica."),
         ("Logout", "Esci"),
-        ("Tags", "Tag"),
+        ("Tags", "Etichette"),
         ("Search ID", "Cerca ID"),
         ("whitelist_sep", "Separati da virgola, punto e virgola, spazio o a capo"),
         ("Add ID", "Aggiungi ID"),
-        ("Add Tag", "Aggiungi tag"),
-        ("Unselect all tags", "Deseleziona tutti i tag"),
+        ("Add Tag", "Aggiungi etichetta"),
+        ("Unselect all tags", "Deseleziona tutte le etichette"),
         ("Network error", "Errore di rete"),
         ("Username missed", "Nome utente mancante"),
         ("Password missed", "Password mancante"),
         ("Wrong credentials", "Credenziali errate"),
-        ("Edit Tag", "Modifica tag"),
+        ("The verification code is incorrect or has expired", "Il codice di verifica non è corretto o è scaduto"),
+        ("Edit Tag", "Modifica etichetta"),
         ("Unremember Password", "Dimentica password"),
         ("Favorites", "Preferiti"),
         ("Add to Favorites", "Aggiungi ai preferiti"),
@@ -453,14 +454,14 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
         ("Voice call", "Chiamata vocale"),
         ("Text chat", "Chat testuale"),
         ("Stop voice call", "Interrompi chiamata vocale"),
-        ("relay_hint_tip", "Se non è possibile connettersi direttamente, puoi provare a farlo tramite relay.\nInoltre, se si vuoi usare il relay al primo tentativo, è possibile aggiungere all'ID il suffisso '/r\' o selezionare nella scheda peer l'opzione 'Collegati sempre tramite relay'."),
+        ("relay_hint_tip", "Se non è possibile connettersi direttamente, puoi provare a farlo tramite relay.\nInoltre, se si vuoi usare il relay al primo tentativo, è possibile aggiungere all'ID il suffisso '/r\' o selezionare nella scheda se esiste l'opzione 'Collegati sempre tramite relay'."),
         ("Reconnect", "Riconnetti"),
         ("Codec", "Codec"),
         ("Resolution", "Risoluzione"),
         ("No transfers in progress", "Nessun trasferimento in corso"),
         ("Set one-time password length", "Imposta lunghezza password monouso"),
-        ("idd_driver_tip", "Installa driver schermo virtuale che verrà usato quando non sono disponibili schermi fisici."),
-        ("confirm_idd_driver_tip", "È stata selezionata l'opzione per installare il driver schermo virtuale.\nNota che verrà installato un certificato di test per l'attendibilità del driver dello schermo virtuale.\nQuesto certificato di test verrà utilizzato solo per l'attendibilità dei driver di RustDesk."),
+        ("install_cert_tip", "Installa certificato RustDesk"),
+        ("confirm_install_cert_tip", "Questo è un certificato di test RustDesk, che può essere considerato attendibile.\nIl certificato verrà usato per certificarsi ed installare i driver RustDesk quando richiesto."),
         ("RDP Settings", "Impostazioni RDP"),
         ("Sort by", "Ordina per"),
         ("New Connection", "Nuova connessione"),
@@ -511,6 +512,36 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
         ("Collapse toolbar", "Comprimi barra strumenti"),
         ("Accept and Elevate", "Accetta ed eleva"),
         ("accept_and_elevate_btn_tooltip", "Accetta la connessione ed eleva le autorizzazioni UAC."),
-        ("clipboard_wait_response_timeout_tip", ""),
+        ("clipboard_wait_response_timeout_tip", "Timeout attesa risposta della copia."),
+        ("Incoming connection", "Connessioni in entrata"),
+        ("Outgoing connection", "Connessioni in uscita"),
+        ("Exit", "Esci da RustDesk"),
+        ("Open", "Apri RustDesk"),
+        ("logout_tip", "Sei sicuro di voler uscire?"),
+        ("Service", "Servizio"),
+        ("Start", "Avvia"),
+        ("Stop", "Ferma"),
+        ("exceed_max_devices", "Hai raggiunto il numero massimo di dispositivi gestibili."),
+        ("Sync with recent sessions", "Sincronizza con le sessioni recenti"),
+        ("Sort tags", "Ordina etichette"),
+        ("Open connection in new tab", "Apri connessione in una nuova scheda"),
+        ("Move tab to new window", "Sposta scheda nella finestra successiva"),
+        ("Can not be empty", "Non può essere vuoto"),
+        ("Already exists", "Esiste già"),
+        ("Change Password", "Modifica password"),
+        ("Refresh Password", "Aggiorna password"),
+        ("ID", "ID"),
+        ("Grid View", "Vista griglia"),
+        ("List View", "Vista elenco"),
+        ("Select", "Seleziona"),
+        ("Toggle Tags", "Attiva/disattiva tag"),
+        ("pull_ab_failed_tip", "Impossibile aggiornare la rubrica"),
+        ("push_ab_failed_tip", "Impossibile sincronizzare la rubrica con il server"),
+        ("synced_peer_readded_tip", "I dispositivi presenti nelle sessioni recenti saranno sincronizzati di nuovo nella rubrica."),
+        ("Change Color", "Modifica colore"),
+        ("Primary Color", "Colore primario"),
+        ("HSV Color", "Colore HSV"),
+        ("Installation Successful!", "Installazione completata"),
+        ("Installation failed!", "Installazione fallita"),
     ].iter().cloned().collect();
 }
diff --git a/src/lang/ja.rs b/src/lang/ja.rs
index 5def2cfca..e6ef75815 100644
--- a/src/lang/ja.rs
+++ b/src/lang/ja.rs
@@ -53,7 +53,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
         ("Audio Input", "音声入力デバイス"),
         ("Enhancements", "追加機能"),
         ("Hardware Codec", "ハードウェア コーデック"),
-        ("Adaptive Bitrate", "アダプティブビットレート"),
+        ("Adaptive bitrate", "アダプティブビットレート"),
         ("ID Server", "認証サーバー"),
         ("Relay Server", "中継サーバー"),
         ("API Server", "APIサーバー"),
@@ -233,6 +233,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
         ("Username missed", "ユーザー名がありません"),
         ("Password missed", "パスワードがありません"),
         ("Wrong credentials", "資格情報が間違っています"),
+        ("The verification code is incorrect or has expired", ""),
         ("Edit Tag", "タグを編集"),
         ("Unremember Password", "パスワードの記憶を解除"),
         ("Favorites", "お気に入り"),
@@ -459,8 +460,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
         ("Resolution", ""),
         ("No transfers in progress", ""),
         ("Set one-time password length", ""),
-        ("idd_driver_tip", ""),
-        ("confirm_idd_driver_tip", ""),
+        ("install_cert_tip", ""),
+        ("confirm_install_cert_tip", ""),
         ("RDP Settings", ""),
         ("Sort by", ""),
         ("New Connection", ""),
@@ -512,5 +513,35 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
         ("Accept and Elevate", ""),
         ("accept_and_elevate_btn_tooltip", ""),
         ("clipboard_wait_response_timeout_tip", ""),
+        ("Incoming connection", ""),
+        ("Outgoing connection", ""),
+        ("Exit", ""),
+        ("Open", ""),
+        ("logout_tip", ""),
+        ("Service", ""),
+        ("Start", ""),
+        ("Stop", ""),
+        ("exceed_max_devices", ""),
+        ("Sync with recent sessions", ""),
+        ("Sort tags", ""),
+        ("Open connection in new tab", ""),
+        ("Move tab to new window", ""),
+        ("Can not be empty", ""),
+        ("Already exists", ""),
+        ("Change Password", ""),
+        ("Refresh Password", ""),
+        ("ID", ""),
+        ("Grid View", ""),
+        ("List View", ""),
+        ("Select", ""),
+        ("Toggle Tags", ""),
+        ("pull_ab_failed_tip", ""),
+        ("push_ab_failed_tip", ""),
+        ("synced_peer_readded_tip", ""),
+        ("Change Color", ""),
+        ("Primary Color", ""),
+        ("HSV Color", ""),
+        ("Installation Successful!", ""),
+        ("Installation failed!", ""),
     ].iter().cloned().collect();
 }
diff --git a/src/lang/ko.rs b/src/lang/ko.rs
index 90040b964..6f26674c5 100644
--- a/src/lang/ko.rs
+++ b/src/lang/ko.rs
@@ -53,7 +53,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
         ("Audio Input", "오디오 입력"),
         ("Enhancements", ""),
         ("Hardware Codec", "하드웨어 코덱"),
-        ("Adaptive Bitrate", "가변 비트레이트"),
+        ("Adaptive bitrate", "가변 비트레이트"),
         ("ID Server", "ID 서버"),
         ("Relay Server", "Relay 서버"),
         ("API Server", "API 서버"),
@@ -233,6 +233,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
         ("Username missed", "사용자명 누락"),
         ("Password missed", "비밀번호 누락"),
         ("Wrong credentials", "틀린 인증 정보"),
+        ("The verification code is incorrect or has expired", ""),
         ("Edit Tag", "태그 수정"),
         ("Unremember Password", "패스워드 기억하지 않기"),
         ("Favorites", "즐겨찾기"),
@@ -459,8 +460,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
         ("Resolution", ""),
         ("No transfers in progress", ""),
         ("Set one-time password length", ""),
-        ("idd_driver_tip", ""),
-        ("confirm_idd_driver_tip", ""),
+        ("install_cert_tip", ""),
+        ("confirm_install_cert_tip", ""),
         ("RDP Settings", ""),
         ("Sort by", ""),
         ("New Connection", ""),
@@ -512,5 +513,35 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
         ("Accept and Elevate", ""),
         ("accept_and_elevate_btn_tooltip", ""),
         ("clipboard_wait_response_timeout_tip", ""),
+        ("Incoming connection", ""),
+        ("Outgoing connection", ""),
+        ("Exit", ""),
+        ("Open", ""),
+        ("logout_tip", ""),
+        ("Service", ""),
+        ("Start", ""),
+        ("Stop", ""),
+        ("exceed_max_devices", ""),
+        ("Sync with recent sessions", ""),
+        ("Sort tags", ""),
+        ("Open connection in new tab", ""),
+        ("Move tab to new window", ""),
+        ("Can not be empty", ""),
+        ("Already exists", ""),
+        ("Change Password", ""),
+        ("Refresh Password", ""),
+        ("ID", ""),
+        ("Grid View", ""),
+        ("List View", ""),
+        ("Select", ""),
+        ("Toggle Tags", ""),
+        ("pull_ab_failed_tip", ""),
+        ("push_ab_failed_tip", ""),
+        ("synced_peer_readded_tip", ""),
+        ("Change Color", ""),
+        ("Primary Color", ""),
+        ("HSV Color", ""),
+        ("Installation Successful!", ""),
+        ("Installation failed!", ""),
     ].iter().cloned().collect();
 }
diff --git a/src/lang/kz.rs b/src/lang/kz.rs
index 6bb1c7a95..b68caad51 100644
--- a/src/lang/kz.rs
+++ b/src/lang/kz.rs
@@ -53,7 +53,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
         ("Audio Input", "Аудио Еңгізу"),
         ("Enhancements", "Жақсартулар"),
         ("Hardware Codec", "Hardware Codec"),
-        ("Adaptive Bitrate", "Adaptive Bitrate"),
+        ("Adaptive bitrate", "Adaptive bitrate"),
         ("ID Server", "ID Сербері"),
         ("Relay Server", "Relay Сербері"),
         ("API Server", "API Сербері"),
@@ -233,6 +233,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
         ("Username missed", "Қолданушы аты бос"),
         ("Password missed", "Құпия сөз бос"),
         ("Wrong credentials", "Бұрыс тіркелгі деректер"),
+        ("The verification code is incorrect or has expired", ""),
         ("Edit Tag", "Тақты Өндеу"),
         ("Unremember Password", "Құпия сөзді Ұмыту"),
         ("Favorites", "Таңдаулылар"),
@@ -459,8 +460,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
         ("Resolution", ""),
         ("No transfers in progress", ""),
         ("Set one-time password length", ""),
-        ("idd_driver_tip", ""),
-        ("confirm_idd_driver_tip", ""),
+        ("install_cert_tip", ""),
+        ("confirm_install_cert_tip", ""),
         ("RDP Settings", ""),
         ("Sort by", ""),
         ("New Connection", ""),
@@ -512,5 +513,35 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
         ("Accept and Elevate", ""),
         ("accept_and_elevate_btn_tooltip", ""),
         ("clipboard_wait_response_timeout_tip", ""),
+        ("Incoming connection", ""),
+        ("Outgoing connection", ""),
+        ("Exit", ""),
+        ("Open", ""),
+        ("logout_tip", ""),
+        ("Service", ""),
+        ("Start", ""),
+        ("Stop", ""),
+        ("exceed_max_devices", ""),
+        ("Sync with recent sessions", ""),
+        ("Sort tags", ""),
+        ("Open connection in new tab", ""),
+        ("Move tab to new window", ""),
+        ("Can not be empty", ""),
+        ("Already exists", ""),
+        ("Change Password", ""),
+        ("Refresh Password", ""),
+        ("ID", ""),
+        ("Grid View", ""),
+        ("List View", ""),
+        ("Select", ""),
+        ("Toggle Tags", ""),
+        ("pull_ab_failed_tip", ""),
+        ("push_ab_failed_tip", ""),
+        ("synced_peer_readded_tip", ""),
+        ("Change Color", ""),
+        ("Primary Color", ""),
+        ("HSV Color", ""),
+        ("Installation Successful!", ""),
+        ("Installation failed!", ""),
     ].iter().cloned().collect();
 }
diff --git a/src/lang/lt.rs b/src/lang/lt.rs
index d2402c7f2..41caa606f 100644
--- a/src/lang/lt.rs
+++ b/src/lang/lt.rs
@@ -53,7 +53,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
         ("Audio Input", "Garso įvestis"),
         ("Enhancements", "Patobulinimai"),
         ("Hardware Codec", "Aparatinės įrangos paspartinimas"),
-        ("Adaptive Bitrate", "Adaptyvusis pralaidumas"),
+        ("Adaptive bitrate", "Adaptyvusis pralaidumas"),
         ("ID Server", "ID serveris"),
         ("Relay Server", "Perdavimo serveris"),
         ("API Server", "API serveris"),
@@ -233,6 +233,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
         ("Username missed", "Prarastas vartotojo vardas"),
         ("Password missed", "Slaptažodis praleistas"),
         ("Wrong credentials", "Klaidingi kredencialai"),
+        ("The verification code is incorrect or has expired", ""),
         ("Edit Tag", "Redaguoti žymą"),
         ("Unremember Password", "Nebeprisiminti slaptažodžio"),
         ("Favorites", "Parankiniai"),
@@ -459,8 +460,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
         ("Resolution", "Rezoliucija"),
         ("No transfers in progress", "Nevyksta jokių perdavimų"),
         ("Set one-time password length", "Nustatyti vienkartinio slaptažodžio ilgį"),
-        ("idd_driver_tip", "Įdiekite virtualaus ekrano tvarkyklę (naudojama, kai nėra fizinių ekranų)"),
-        ("confirm_idd_driver_tip", "Įjungta virtualaus ekrano tvarkyklės diegimo funkcija. Atminkite, kad bus įdiegtas bandomasis sertifikatas, kad būtų galima pasitikėti tvarkykle. Šis sertifikatas bus naudojamas tik pasitikėjimui Rustdesk tvarkyklėmis patikrinti."),
+        ("install_cert_tip", ""),
+        ("confirm_install_cert_tip", ""),
         ("RDP Settings", "RDP nustatymai"),
         ("Sort by", "Rūšiuoti pagal"),
         ("New Connection", "Naujas ryšys"),
@@ -512,5 +513,35 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
         ("Accept and Elevate", ""),
         ("accept_and_elevate_btn_tooltip", ""),
         ("clipboard_wait_response_timeout_tip", ""),
+        ("Incoming connection", ""),
+        ("Outgoing connection", ""),
+        ("Exit", ""),
+        ("Open", ""),
+        ("logout_tip", ""),
+        ("Service", ""),
+        ("Start", ""),
+        ("Stop", ""),
+        ("exceed_max_devices", ""),
+        ("Sync with recent sessions", ""),
+        ("Sort tags", ""),
+        ("Open connection in new tab", ""),
+        ("Move tab to new window", ""),
+        ("Can not be empty", ""),
+        ("Already exists", ""),
+        ("Change Password", ""),
+        ("Refresh Password", ""),
+        ("ID", ""),
+        ("Grid View", ""),
+        ("List View", ""),
+        ("Select", ""),
+        ("Toggle Tags", ""),
+        ("pull_ab_failed_tip", ""),
+        ("push_ab_failed_tip", ""),
+        ("synced_peer_readded_tip", ""),
+        ("Change Color", ""),
+        ("Primary Color", ""),
+        ("HSV Color", ""),
+        ("Installation Successful!", ""),
+        ("Installation failed!", ""),
     ].iter().cloned().collect();
 }
diff --git a/src/lang/nl.rs b/src/lang/nl.rs
index 2e0de4906..6010a4132 100644
--- a/src/lang/nl.rs
+++ b/src/lang/nl.rs
@@ -53,7 +53,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
         ("Audio Input", "Audio Ingang"),
         ("Enhancements", "Verbeteringen"),
         ("Hardware Codec", "Hardware Codec"),
-        ("Adaptive Bitrate", "Aangepaste Bitsnelheid"),
+        ("Adaptive bitrate", "Aangepaste Bitsnelheid"),
         ("ID Server", "Server ID"),
         ("Relay Server", "Relay Server"),
         ("API Server", "API Server"),
@@ -233,6 +233,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
         ("Username missed", "Gebruikersnaam gemist"),
         ("Password missed", "Wachtwoord vergeten"),
         ("Wrong credentials", "Verkeerde inloggegevens"),
+        ("The verification code is incorrect or has expired", ""),
         ("Edit Tag", "Label Bewerken"),
         ("Unremember Password", "Wachtwoord vergeten"),
         ("Favorites", "Favorieten"),
@@ -459,8 +460,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
         ("Resolution", "Resolutie"),
         ("No transfers in progress", "Geen overdrachten in uitvoering"),
         ("Set one-time password length", "Stel de lengte van het eenmalige wachtwoord in"),
-        ("idd_driver_tip", "Installeer het virtuele beeldschermstuurprogramma dat wordt gebruikt wanneer u geen fysieke beeldschermen hebt."),
-        ("confirm_idd_driver_tip", "De optie om het virtuele displaystuurprogramma te installeren is ingeschakeld. Er wordt een testcertificaat geplaatst om het virtuele displaystuurprogramma te vertrouwen. Dit testcertificaat wordt alleen gebruikt om RustDesk-stuurprogramma's te vertrouwen."),
+        ("install_cert_tip", ""),
+        ("confirm_install_cert_tip", ""),
         ("RDP Settings", "RDP Instellingen"),
         ("Sort by", "Sorteren op"),
         ("New Connection", "Nieuwe Verbinding"),
@@ -511,6 +512,36 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
         ("Collapse toolbar", "Werkbalk samenvouwen"),
         ("Accept and Elevate", "Accepteren en Verheffen"),
         ("accept_and_elevate_btn_tooltip", "Accepteer de verbinding en verhoog de UAC-machtigingen."),
-        ("clipboard_wait_response_timeout_tip", ""),
+        ("clipboard_wait_response_timeout_tip", "Time-out in afwachting van kopieer-antwoord."),
+        ("Incoming connection", "Inkomende verbinding"),
+        ("Outgoing connection", "Uitgaande verbinding"),
+        ("Exit", "Verlaten"),
+        ("Open", "Open"),
+        ("logout_tip", "Weet je zeker dat je je wilt afmelden?"),
+        ("Service", "Service"),
+        ("Start", "Start"),
+        ("Stop", "Stop"),
+        ("exceed_max_devices", "Het maximum aantal gecontroleerde apparaten is bereikt."),
+        ("Sync with recent sessions", "Recente sessies synchroniseren"),
+        ("Sort tags", "Labels sorteren"),
+        ("Open connection in new tab", "Verbinding openen in een nieuw tabblad"),
+        ("Move tab to new window", "Tabblad verplaatsen naar nieuw venster"),
+        ("Can not be empty", "Mag niet leeg zijn"),
+        ("Already exists", "Bestaat reeds"),
+        ("Change Password", "Wijzig Wachtwoord"),
+        ("Refresh Password", "Wachtwoord Vernieuwen"),
+        ("ID", "ID"),
+        ("Grid View", "Rasterweergave"),
+        ("List View", "Lijstweergave"),
+        ("Select", "Selecteer"),
+        ("Toggle Tags", "Schakel Tags"),
+        ("pull_ab_failed_tip", "Adresboek kan niet worden bijgewerkt"),
+        ("push_ab_failed_tip", "Synchronisatie van adresboek mislukt"),
+        ("synced_peer_readded_tip", "Apparaten die aanwezig waren in recente sessies worden gesynchroniseerd met het adresboek."),
+        ("Change Color", "Kleur Aanpassen"),
+        ("Primary Color", "Hoofdkleur"),
+        ("HSV Color", "HSV Kleur"),
+        ("Installation Successful!", ""),
+        ("Installation failed!", ""),
     ].iter().cloned().collect();
 }
diff --git a/src/lang/pl.rs b/src/lang/pl.rs
index dfa6834ca..fe0693595 100644
--- a/src/lang/pl.rs
+++ b/src/lang/pl.rs
@@ -53,7 +53,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
         ("Audio Input", "Wejście audio"),
         ("Enhancements", "Ulepszenia"),
         ("Hardware Codec", "Kodek sprzętowy"),
-        ("Adaptive Bitrate", "Adaptacyjny bitrate"),
+        ("Adaptive bitrate", "Adaptacyjny bitrate"),
         ("ID Server", "Serwer ID"),
         ("Relay Server", "Serwer pośredniczący"),
         ("API Server", "Serwer API"),
@@ -233,6 +233,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
         ("Username missed", "Nieprawidłowe nazwa użytkownika"),
         ("Password missed", "Nieprawidłowe hasło"),
         ("Wrong credentials", "Błędne dane uwierzytelniające"),
+        ("The verification code is incorrect or has expired", "Kod weryfikacyjny jest niepoprawny lub wygasł"),
         ("Edit Tag", "Edytuj tag"),
         ("Unremember Password", "Zapomnij hasło"),
         ("Favorites", "Ulubione"),
@@ -459,8 +460,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
         ("Resolution", "Rozdzielczość"),
         ("No transfers in progress", "Brak transferów w toku"),
         ("Set one-time password length", "Ustaw długość jednorazowego hasła"),
-        ("idd_driver_tip", "Zainstaluj sterownik wirtualnego wyświetlacza, który jest używany, gdy nie masz fizycznych monitorów."),
-        ("confirm_idd_driver_tip", "Opcja instalacji sterownika wirtualnego wyświetlacza jest zaznaczona. Pamiętaj, że zostanie zainstalowany testowy certyfikat, aby zaufać wirtualnemu sterownikowi. Ten certyfikat będzie używany tylko do weryfikacji sterowników RustDesk."),
+        ("install_cert_tip", "Instalacja certyfikatu RustDesk"),
+        ("confirm_install_cert_tip", "To jest certyfikat testowy RustDesk, któremu można zaufać. Certyfikat jest używany do zaufania i instalowania sterowników RustDesk w razie potrzeby."),
         ("RDP Settings", "Ustawienia RDP"),
         ("Sort by", "Sortuj wg"),
         ("New Connection", "Nowe połączenie"),
@@ -505,12 +506,42 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
         ("Enable", "Włącz"),
         ("Disable", "Wyłącz"),
         ("Options", "Opcje"),
-        ("resolution_original_tip", ""),
-        ("resolution_fit_local_tip", ""),
-        ("resolution_custom_tip", ""),
+        ("resolution_original_tip", "Oryginalna rozdzielczość"),
+        ("resolution_fit_local_tip", "Dostosuj rozdzielczość lokalną"),
+        ("resolution_custom_tip", "Rozdzielczość niestandardowa"),
         ("Collapse toolbar", "Zwiń pasek narzędzi"),
         ("Accept and Elevate", "Akceptuj i Podnieś uprawnienia"),
-        ("accept_and_elevate_btn_tooltip", ""),
-        ("clipboard_wait_response_timeout_tip", ""),
+        ("accept_and_elevate_btn_tooltip", "Zaakceptuj połączenie i podnieś uprawnienia UAC"),
+        ("clipboard_wait_response_timeout_tip", "Upłynął limit czasu oczekiwania na schowek."),
+        ("Incoming connection", "Połączenie przychodzące"),
+        ("Outgoing connection", "Połączenie wychodzące"),
+        ("Exit", "Wyjście"),
+        ("Open", "Otwórz"),
+        ("logout_tip", "Na pewno chcesz się wylogować?"),
+        ("Service", "Usługa"),
+        ("Start", "Uruchom"),
+        ("Stop", "Zatrzymaj"),
+        ("exceed_max_devices", "Przekroczona maks. liczba urządzeń"),
+        ("Sync with recent sessions", "Synchronizacja z ostatnimi sesjami"),
+        ("Sort tags", "Znaczniki sortowania"),
+        ("Open connection in new tab", "Otwórz połączenie w nowej zakładce"),
+        ("Move tab to new window", "Przenieś zakładkę do nowego okna"),
+        ("Can not be empty", "Nie może być puste"),
+        ("Already exists", "Już istnieje"),
+        ("Change Password", ""),
+        ("Refresh Password", ""),
+        ("ID", ""),
+        ("Grid View", ""),
+        ("List View", ""),
+        ("Select", ""),
+        ("Toggle Tags", ""),
+        ("pull_ab_failed_tip", ""),
+        ("push_ab_failed_tip", ""),
+        ("synced_peer_readded_tip", ""),
+        ("Change Color", ""),
+        ("Primary Color", ""),
+        ("HSV Color", ""),
+        ("Installation Successful!", ""),
+        ("Installation failed!", ""),
     ].iter().cloned().collect();
 }
diff --git a/src/lang/pt_PT.rs b/src/lang/pt_PT.rs
index 743154d5f..d0ade1088 100644
--- a/src/lang/pt_PT.rs
+++ b/src/lang/pt_PT.rs
@@ -53,7 +53,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
         ("Audio Input", "Entrada de Áudio"),
         ("Enhancements", "Melhorias"),
         ("Hardware Codec", ""),
-        ("Adaptive Bitrate", ""),
+        ("Adaptive bitrate", ""),
         ("ID Server", "Servidor de ID"),
         ("Relay Server", "Servidor de Relay"),
         ("API Server", "Servidor da API"),
@@ -233,6 +233,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
         ("Username missed", "Nome de utilizador em falta"),
         ("Password missed", "Palavra-chave em falta"),
         ("Wrong credentials", "Nome de utilizador ou palavra-chave incorrectos"),
+        ("The verification code is incorrect or has expired", ""),
         ("Edit Tag", "Editar Tag"),
         ("Unremember Password", "Esquecer Palavra-chave"),
         ("Favorites", "Favoritos"),
@@ -459,8 +460,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
         ("Resolution", ""),
         ("No transfers in progress", ""),
         ("Set one-time password length", ""),
-        ("idd_driver_tip", ""),
-        ("confirm_idd_driver_tip", ""),
+        ("install_cert_tip", ""),
+        ("confirm_install_cert_tip", ""),
         ("RDP Settings", ""),
         ("Sort by", ""),
         ("New Connection", ""),
@@ -512,5 +513,35 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
         ("Accept and Elevate", ""),
         ("accept_and_elevate_btn_tooltip", ""),
         ("clipboard_wait_response_timeout_tip", ""),
+        ("Incoming connection", ""),
+        ("Outgoing connection", ""),
+        ("Exit", ""),
+        ("Open", ""),
+        ("logout_tip", ""),
+        ("Service", ""),
+        ("Start", ""),
+        ("Stop", ""),
+        ("exceed_max_devices", ""),
+        ("Sync with recent sessions", ""),
+        ("Sort tags", ""),
+        ("Open connection in new tab", ""),
+        ("Move tab to new window", ""),
+        ("Can not be empty", ""),
+        ("Already exists", ""),
+        ("Change Password", ""),
+        ("Refresh Password", ""),
+        ("ID", ""),
+        ("Grid View", ""),
+        ("List View", ""),
+        ("Select", ""),
+        ("Toggle Tags", ""),
+        ("pull_ab_failed_tip", ""),
+        ("push_ab_failed_tip", ""),
+        ("synced_peer_readded_tip", ""),
+        ("Change Color", ""),
+        ("Primary Color", ""),
+        ("HSV Color", ""),
+        ("Installation Successful!", ""),
+        ("Installation failed!", ""),
     ].iter().cloned().collect();
 }
diff --git a/src/lang/ptbr.rs b/src/lang/ptbr.rs
index 3bcc10e6c..6dfc8f2a2 100644
--- a/src/lang/ptbr.rs
+++ b/src/lang/ptbr.rs
@@ -53,7 +53,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
         ("Audio Input", "Entrada de Áudio"),
         ("Enhancements", "Melhorias"),
         ("Hardware Codec", "Codec de hardware"),
-        ("Adaptive Bitrate", "Taxa de bits adaptável"),
+        ("Adaptive bitrate", "Taxa de bits adaptável"),
         ("ID Server", "Servidor de ID"),
         ("Relay Server", "Servidor de Relay"),
         ("API Server", "Servidor da API"),
@@ -233,6 +233,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
         ("Username missed", "Nome de usuário requerido"),
         ("Password missed", "Senha requerida"),
         ("Wrong credentials", "Nome de usuário ou senha incorretos"),
+        ("The verification code is incorrect or has expired", ""),
         ("Edit Tag", "Editar Tag"),
         ("Unremember Password", "Esquecer Senha"),
         ("Favorites", "Favoritos"),
@@ -459,8 +460,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
         ("Resolution", "Resolução"),
         ("No transfers in progress", "Nenhuma transferência em andamento"),
         ("Set one-time password length", "Definir comprimento de senha descartável"),
-        ("idd_driver_tip", "Instale o driver de exibição virtual que é usado quando você não possui displays físicos."),
-        ("confirm_idd_driver_tip", "A opção para instalar o driver de exibição virtual está marcada. Observe que um certificado de teste será instalado para confiar no driver de vídeo virtual. Este certificado de teste será usado apenas para confiar nos drivers Rustdesk."),
+        ("install_cert_tip", ""),
+        ("confirm_install_cert_tip", ""),
         ("RDP Settings", "Configurações RDP"),
         ("Sort by", "Ordenar por"),
         ("New Connection", "Nova Conexão"),
@@ -512,5 +513,35 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
         ("Accept and Elevate", ""),
         ("accept_and_elevate_btn_tooltip", ""),
         ("clipboard_wait_response_timeout_tip", ""),
+        ("Incoming connection", ""),
+        ("Outgoing connection", ""),
+        ("Exit", ""),
+        ("Open", ""),
+        ("logout_tip", ""),
+        ("Service", ""),
+        ("Start", ""),
+        ("Stop", ""),
+        ("exceed_max_devices", ""),
+        ("Sync with recent sessions", ""),
+        ("Sort tags", ""),
+        ("Open connection in new tab", ""),
+        ("Move tab to new window", ""),
+        ("Can not be empty", ""),
+        ("Already exists", ""),
+        ("Change Password", ""),
+        ("Refresh Password", ""),
+        ("ID", ""),
+        ("Grid View", ""),
+        ("List View", ""),
+        ("Select", ""),
+        ("Toggle Tags", ""),
+        ("pull_ab_failed_tip", ""),
+        ("push_ab_failed_tip", ""),
+        ("synced_peer_readded_tip", ""),
+        ("Change Color", ""),
+        ("Primary Color", ""),
+        ("HSV Color", ""),
+        ("Installation Successful!", ""),
+        ("Installation failed!", ""),
     ].iter().cloned().collect();
 }
diff --git a/src/lang/ro.rs b/src/lang/ro.rs
index b75d5950d..01e0402a9 100644
--- a/src/lang/ro.rs
+++ b/src/lang/ro.rs
@@ -4,29 +4,29 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
         ("Status", "Stare"),
         ("Your Desktop", "Desktopul tău"),
         ("desk_tip", "Desktopul tău poate fi accesat folosind ID-ul și parola de mai jos."),
-        ("Password", "Parola"),
+        ("Password", "Parolă"),
         ("Ready", "Pregătit"),
         ("Established", "Stabilit"),
         ("connecting_status", "În curs de conectare la rețeaua RustDesk..."),
-        ("Enable Service", "Activează serviciu"),
-        ("Start Service", "Pornește serviciu"),
-        ("Service is running", "Serviciul este în curs de executare..."),
+        ("Enable Service", "Activează serviciul"),
+        ("Start Service", "Pornește serviciul"),
+        ("Service is running", "Serviciul rulează..."),
         ("Service is not running", "Serviciul nu funcționează"),
         ("not_ready_status", "Nepregătit. Verifică conexiunea la rețea."),
-        ("Control Remote Desktop", "Controlează desktop-ul la distanță"),
-        ("Transfer File", "Transferă fișier"),
+        ("Control Remote Desktop", "Controlează desktopul la distanță"),
+        ("Transfer File", "Transferă fișiere"),
         ("Connect", "Conectează-te"),
         ("Recent Sessions", "Sesiuni recente"),
         ("Address Book", "Agendă"),
         ("Confirmation", "Confirmare"),
         ("TCP Tunneling", "Tunel TCP"),
         ("Remove", "Elimină"),
-        ("Refresh random password", "Actualizează parolă aleatorie"),
+        ("Refresh random password", "Actualizează parola aleatorie"),
         ("Set your own password", "Setează propria parolă"),
         ("Enable Keyboard/Mouse", "Activează control tastatură/mouse"),
         ("Enable Clipboard", "Activează clipboard"),
-        ("Enable File Transfer", "Activează transfer fișiere"),
-        ("Enable TCP Tunneling", "Activează tunel TCP"),
+        ("Enable File Transfer", "Activează transferul de fișiere"),
+        ("Enable TCP Tunneling", "Activează tunelul TCP"),
         ("IP Whitelisting", "Listă de IP-uri autorizate"),
         ("ID/Relay Server", "Server de ID/retransmisie"),
         ("Import Server Config", "Importă configurație server"),
@@ -35,25 +35,25 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
         ("Export server configuration successfully", "Configurație server exportată cu succes"),
         ("Invalid server configuration", "Configurație server nevalidă"),
         ("Clipboard is empty", "Clipboard gol"),
-        ("Stop service", "Oprește serviciu"),
+        ("Stop service", "Oprește serviciul"),
         ("Change ID", "Schimbă ID"),
-        ("Your new ID", ""),
-        ("length %min% to %max%", ""),
-        ("starts with a letter", ""),
-        ("allowed characters", ""),
+        ("Your new ID", "Noul tău ID"),
+        ("length %min% to %max%", "lungime între %min% și %max%"),
+        ("starts with a letter", "începe cu o literă"),
+        ("allowed characters", "caractere permise"),
         ("id_change_tip", "Pot fi utilizate doar caractere a-z, A-Z, 0-9, _ (bară jos). Primul caracter trebuie să fie a-z, A-Z. Lungimea trebuie să fie între 6 și 16 caractere."),
         ("Website", "Site web"),
         ("About", "Despre"),
-        ("Slogan_tip", ""),
-        ("Privacy Statement", ""),
-        ("Mute", "Fără sunet"),
-        ("Build Date", ""),
-        ("Version", ""),
-        ("Home", ""),
-        ("Audio Input", "Intrare audio"),
+        ("Slogan_tip", "Făcut din inimă în lumea aceasta haotică!"),
+        ("Privacy Statement", "Politică de confidențialitate"),
+        ("Mute", "Dezactivează sunet"),
+        ("Build Date", "Dată build"),
+        ("Version", "Versiune"),
+        ("Home", "Acasă"),
+        ("Audio Input", "Intrări audio"),
         ("Enhancements", "Îmbunătățiri"),
         ("Hardware Codec", "Codec hardware"),
-        ("Adaptive Bitrate", "Rată de biți adaptabilă"),
+        ("Adaptive bitrate", "Rată de biți adaptabilă"),
         ("ID Server", "Server de ID"),
         ("Relay Server", "Server de retransmisie"),
         ("API Server", "Server API"),
@@ -76,7 +76,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
         ("Connection Error", "Eroare de conexiune"),
         ("Error", "Eroare"),
         ("Reset by the peer", "Conexiunea a fost închisă de dispozitivul pereche"),
-        ("Connecting...", "Conectare..."),
+        ("Connecting...", "Se conectează..."),
         ("Connection in progress. Please wait.", "Conectare în curs. Te rugăm așteaptă."),
         ("Please try 1 minute later", "Reîncearcă într-un minut"),
         ("Login Error", "Eroare de autentificare"),
@@ -105,7 +105,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
         ("Are you sure you want to delete this file?", "Sigur vrei să ștergi acest fișier?"),
         ("Are you sure you want to delete this empty directory?", "Sigur vrei să ștergi acest director gol?"),
         ("Are you sure you want to delete the file of this directory?", "Sigur vrei să ștergi fișierul din acest director?"),
-        ("Do this for all conflicts", "Aplică pentru toate conflictele"),
+        ("Do this for all conflicts", "Aplică la toate conflictele"),
         ("This is irreversible!", "Această acțiune este ireversibilă!"),
         ("Deleting", "În curs de ștergere..."),
         ("files", "fișier"),
@@ -114,8 +114,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
         ("Speed", "Viteză"),
         ("Custom Image Quality", "Setează calitatea imaginii"),
         ("Privacy mode", "Mod privat"),
-        ("Block user input", "Blochează utilizator"),
-        ("Unblock user input", "Deblochează utilizator"),
+        ("Block user input", "Blochează intervenție utilizator"),
+        ("Unblock user input", "Deblochează intervenție utilizator"),
         ("Adjust Window", "Ajustează fereastra"),
         ("Original", "Dimensiune originală"),
         ("Shrink", "Micșorează"),
@@ -124,10 +124,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
         ("ScrollAuto", "Derulare automată"),
         ("Good image quality", "Calitate bună a imaginii"),
         ("Balanced", "Calitate normală a imaginii"),
-        ("Optimize reaction time", "Optimizează timpul de reacție"),
+        ("Optimize reaction time", "Timp de reacție optimizat"),
         ("Custom", "Personalizat"),
         ("Show remote cursor", "Afișează cursor la distanță"),
-        ("Show quality monitor", "Afișează indicator de calitate"),
+        ("Show quality monitor", "Afișează detalii despre conexiune"),
         ("Disable clipboard", "Dezactivează clipboard"),
         ("Lock after session end", "Blochează după deconectare"),
         ("Insert", "Introdu"),
@@ -137,22 +137,22 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
         ("Failed to connect to rendezvous server", "Conectare la server rendezvous eșuată"),
         ("Please try later", "Încearcă mai târziu"),
         ("Remote desktop is offline", "Desktopul la distanță este offline"),
-        ("Key mismatch", "Nepotrivire chei"),
+        ("Key mismatch", "Nepotrivire cheie"),
         ("Timeout", "Conexiune expirată"),
         ("Failed to connect to relay server", "Conectare la server de retransmisie eșuată"),
         ("Failed to connect via rendezvous server", "Conectare prin intermediul serverului rendezvous eșuată"),
         ("Failed to connect via relay server", "Conectare prin intermediul serverului de retransmisie eșuată"),
         ("Failed to make direct connection to remote desktop", "Imposibil de stabilit o conexiune directă cu desktopul la distanță"),
         ("Set Password", "Setează parola"),
-        ("OS Password", "Parolă OS"),
-        ("install_tip", "Din cauza restricțiilor UAC, e posibil ca RustDesk să nu funcționeze corespunzător. Pentru a evita acest lucru, fă clic pe butonul de mai jos pentru a instala RustDesk."),
-        ("Click to upgrade", "Fă clic pentru a face upgrade"),
-        ("Click to download", "Fă clic pentru a descărca"),
-        ("Click to update", "Fă clic pentru a actualiza"),
+        ("OS Password", "Parolă sistem"),
+        ("install_tip", "Din cauza restricțiilor CCU, este posibil ca RustDesk să nu funcționeze corespunzător. Pentru a evita acest lucru, dă clic pe butonul de mai jos pentru a instala RustDesk."),
+        ("Click to upgrade", "Dă clic pentru a face upgrade"),
+        ("Click to download", "Dă clic pentru a descărca"),
+        ("Click to update", "Dă clic pentru a actualiza"),
         ("Configure", "Configurează"),
         ("config_acc", "Pentru a controla desktopul la distanță, trebuie să permiți RustDesk acces la setările de Accesibilitate."),
         ("config_screen", "Pentru a controla desktopul la distanță, trebuie să permiți RustDesk acces la setările de Înregistrare ecran."),
-        ("Installing ...", "Instalare în curs..."),
+        ("Installing ...", "Se instalează..."),
         ("Install", "Instalează"),
         ("Installation", "Instalare"),
         ("Installation Path", "Cale de instalare"),
@@ -171,18 +171,18 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
         ("Add", "Adaugă"),
         ("Local Port", "Port local"),
         ("Local Address", "Adresă locală"),
-        ("Change Local Port", "Schimbă port local"),
+        ("Change Local Port", "Modifică port local"),
         ("setup_server_tip", "Pentru o conexiune mai rapidă, îți poți configura propriul server."),
-        ("Too short, at least 6 characters.", "Prea scurt; trebuie cel puțin 6 caractere."),
+        ("Too short, at least 6 characters.", "Prea scurtă; trebuie cel puțin 6 caractere."),
         ("The confirmation is not identical.", "Cele două intrări nu corespund."),
         ("Permissions", "Permisiuni"),
         ("Accept", "Acceptă"),
         ("Dismiss", "Respinge"),
         ("Disconnect", "Deconectează-te"),
-        ("Allow using keyboard and mouse", "Permite utilizarea tastaturii și mouse-ului"),
+        ("Allow using keyboard and mouse", "Permite utilizarea tastaturii și a mouse-ului"),
         ("Allow using clipboard", "Permite utilizarea clipboardului"),
         ("Allow hearing sound", "Permite auzirea sunetului"),
-        ("Allow file copy and paste", "Permite copierea/lipirea fișierelor"),
+        ("Allow file copy and paste", "Permite copierea și lipirea fișierelor"),
         ("Connected", "Conectat"),
         ("Direct and encrypted connection", "Conexiune directă criptată"),
         ("Relayed and encrypted connection", "Conexiune retransmisă criptată"),
@@ -192,7 +192,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
         ("Enter your password", "Introdu parola"),
         ("Logging in...", "Se conectează..."),
         ("Enable RDP session sharing", "Activează partajarea sesiunii RDP"),
-        ("Auto Login", "Conectare automată (valid doar dacă funcția de Blocare după deconectare este activă)"),
+        ("Auto Login", "Conectare automată (validă doar dacă opțiunea Blocare după deconectare este selectată)"),
         ("Enable Direct IP Access", "Activează accesul direct cu IP"),
         ("Rename", "Redenumește"),
         ("Space", "Spațiu"),
@@ -205,25 +205,25 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
         ("Login screen using Wayland is not supported", "Ecranele de conectare care folosesc Wayland nu sunt acceptate"),
         ("Reboot required", "Repornire necesară"),
         ("Unsupported display server", "Tipul de server de afișaj nu este acceptat"),
-        ("x11 expected", "E necesar X11"),
+        ("x11 expected", "Este necesar X11"),
         ("Port", "Port"),
         ("Settings", "Setări"),
-        ("Username", " Nume de utilizator"),
+        ("Username", " Nume utilizator"),
         ("Invalid port", "Port nevalid"),
-        ("Closed manually by the peer", "Închis manual de dispozitivul pereche"),
+        ("Closed manually by the peer", "Conexiune închisă manual de dispozitivul pereche"),
         ("Enable remote configuration modification", "Activează modificarea configurației de la distanță"),
-        ("Run without install", "Rulează fără instalare"),
-        ("Connect via relay", ""),
-        ("Always connect via relay", "Se conectează mereu prin retransmisie"),
+        ("Run without install", "Rulează fără a instala"),
+        ("Connect via relay", "Conectează-te prin retransmisie"),
+        ("Always connect via relay", "Conectează-te mereu prin retransmisie"),
         ("whitelist_tip", "Doar adresele IP autorizate pot accesa acest dispozitiv"),
-        ("Login", "Conectare"),
-        ("Verify", ""),
-        ("Remember me", ""),
-        ("Trust this device", ""),
-        ("Verification code", ""),
+        ("Login", "Conectează-te"),
+        ("Verify", "Verificare"),
+        ("Remember me", "Reține-mă"),
+        ("Trust this device", "Acest dispozitiv este de încredere"),
+        ("Verification code", "Cod de verificare"),
         ("verification_tip", ""),
-        ("Logout", "Deconectare"),
-        ("Tags", "Etichetare"),
+        ("Logout", "Deconectează-te"),
+        ("Tags", "Etichete"),
         ("Search ID", "Caută după ID"),
         ("whitelist_sep", "Poți folosi ca separator virgula, punctul și virgula, spațiul sau linia nouă"),
         ("Add ID", "Adaugă ID"),
@@ -233,6 +233,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
         ("Username missed", "Lipsește numele de utilizator"),
         ("Password missed", "Lipsește parola"),
         ("Wrong credentials", "Nume sau parolă greșită"),
+        ("The verification code is incorrect or has expired", ""),
         ("Edit Tag", "Modifică etichetă"),
         ("Unremember Password", "Uită parola"),
         ("Favorites", "Favorite"),
@@ -263,32 +264,32 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
         ("Mouse Wheel", "Rotiță mouse"),
         ("Two-Finger Move", "Mișcă cu două degete"),
         ("Canvas Move", "Mută ecran"),
-        ("Pinch to Zoom", "Apropie degetele pentru zoom"),
-        ("Canvas Zoom", "Zoom ecran"),
+        ("Pinch to Zoom", "Apropie degetele pentru a mări"),
+        ("Canvas Zoom", "Mărire ecran"),
         ("Reset canvas", "Reinițializează ecranul"),
         ("No permission of file transfer", "Nicio permisiune pentru transferul de fișiere"),
         ("Note", "Reține"),
         ("Connection", "Conexiune"),
         ("Share Screen", "Partajează ecran"),
-        ("Chat", "Discută"),
+        ("Chat", "Mesaje"),
         ("Total", "Total"),
         ("items", "elemente"),
         ("Selected", "Selectat"),
-        ("Screen Capture", "Captură ecran"),
+        ("Screen Capture", "Capturare ecran"),
         ("Input Control", "Control intrări"),
-        ("Audio Capture", "Captură audio"),
+        ("Audio Capture", "Capturare audio"),
         ("File Connection", "Conexiune fișier"),
         ("Screen Connection", "Conexiune ecran"),
         ("Do you accept?", "Accepți?"),
         ("Open System Setting", "Deschide setări sistem"),
         ("How to get Android input permission?", "Cum autorizez dispozitive de intrare pe Android?"),
-        ("android_input_permission_tip1", "Pentru ca un dispozitiv la distanță să poată controla un dispozitiv Android folosind mouse-ul sau suportul tactil, trebuie să permiți RustDesk să utilize serviciul Accesibilitate."),
-        ("android_input_permission_tip2", "Accesează următoarea pagină din Setări, caută și deschide [Aplicații instalate] și activează serviciul [RustDesk Input]."),
-        ("android_new_connection_tip", "Ai primit o nouă solicitare de control pentru dispozitivul actual."),
-        ("android_service_will_start_tip", "Activarea setării Captură ecran va porni automat serviciul, permițând altor dispozitive să solicite conectarea la dispozitivul tău."),
+        ("android_input_permission_tip1", "Pentru ca un dispozitiv la distanță să poată controla un dispozitiv Android folosind mouse-ul sau suportul tactil, trebuie să permiți RustDesk să utilize serviciul „Accesibilitate”."),
+        ("android_input_permission_tip2", "Accesează următoarea pagină din Setări, deschide [Aplicații instalate] și pornește serviciul [RustDesk Input]."),
+        ("android_new_connection_tip", "Ai primit o nouă solicitare de controlare a dispozitivului actual."),
+        ("android_service_will_start_tip", "Activarea setării de capturare a ecranului va porni automat serviciul, permițând altor dispozitive să solicite conectarea la dispozitivul tău."),
         ("android_stop_service_tip", "Închiderea serviciului va închide automat toate conexiunile stabilite."),
         ("android_version_audio_tip", "Versiunea actuală de Android nu suportă captura audio. Fă upgrade la Android 10 sau la o versiune superioară."),
-        ("android_start_service_tip", ""),
+        ("android_start_service_tip", "Apasă [Pornește serviciu] sau DESCHIDE [Capturare ecran] pentru a porni serviciul de partajare a ecranului."),
         ("android_permission_may_not_change_tip", ""),
         ("Account", "Cont"),
         ("Overwrite", "Suprascrie"),
@@ -311,14 +312,14 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
         ("Keep RustDesk background service", "Rulează serviciul RustDesk în fundal"),
         ("Ignore Battery Optimizations", "Ignoră optimizările de baterie"),
         ("android_open_battery_optimizations_tip", "Pentru dezactivarea acestei funcții, accesează setările aplicației RustDesk, deschide secțiunea [Baterie] și deselectează [Fără restricții]."),
-        ("Start on Boot", ""),
-        ("Start the screen sharing service on boot, requires special permissions", ""),
+        ("Start on Boot", "Pornește la boot"),
+        ("Start the screen sharing service on boot, requires special permissions", "Pornește serviciul de partajare a ecranului la boot; necesită permisiuni speciale"),
         ("Connection not allowed", "Conexiune neautoriztă"),
         ("Legacy mode", "Mod legacy"),
         ("Map mode", "Mod hartă"),
         ("Translate mode", "Mod traducere"),
         ("Use permanent password", "Folosește parola permanentă"),
-        ("Use both passwords", "Folosește parola unică și cea permanentă"),
+        ("Use both passwords", "Folosește ambele programe"),
         ("Set permanent password", "Setează parola permanentă"),
         ("Enable Remote Restart", "Activează repornirea la distanță"),
         ("Allow remote restart", "Permite repornirea la distanță"),
@@ -329,33 +330,33 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
         ("Copied", "Copiat"),
         ("Exit Fullscreen", "Ieși din modul ecran complet"),
         ("Fullscreen", "Ecran complet"),
-        ("Mobile Actions", "Acțiuni mobile"),
+        ("Mobile Actions", "Bară de navigare"),
         ("Select Monitor", "Selectează monitor"),
         ("Control Actions", "Acțiuni de control"),
         ("Display Settings", "Setări afișaj"),
         ("Ratio", "Raport"),
         ("Image Quality", "Calitate imagine"),
         ("Scroll Style", "Stil de derulare"),
-        ("Show Toolbar", ""),
-        ("Hide Toolbar", ""),
+        ("Show Toolbar", "Arată bară de instrumente"),
+        ("Hide Toolbar", "Ascunde bară de instrumente"),
         ("Direct Connection", "Conexiune directă"),
         ("Relay Connection", "Conexiune prin retransmisie"),
         ("Secure Connection", "Conexiune securizată"),
         ("Insecure Connection", "Conexiune nesecurizată"),
-        ("Scale original", "Scală originală"),
-        ("Scale adaptive", "Scală adaptivă"),
+        ("Scale original", "Dimensiune originală"),
+        ("Scale adaptive", "Scalare automată"),
         ("General", "General"),
         ("Security", "Securitate"),
         ("Theme", "Temă"),
         ("Dark Theme", "Temă întunecată"),
-        ("Light Theme", ""),
-        ("Dark", "Întunecat"),
-        ("Light", "Luminos"),
-        ("Follow System", "Urmărește sistem"),
+        ("Light Theme", "Temă luminoasă"),
+        ("Dark", "Întunecată"),
+        ("Light", "Luminoasă"),
+        ("Follow System", "Temă sistem"),
         ("Enable hardware codec", "Activează codec hardware"),
-        ("Unlock Security Settings", "Deblochează setări de securitate"),
+        ("Unlock Security Settings", "Deblochează setările de securitate"),
         ("Enable Audio", "Activează audio"),
-        ("Unlock Network Settings", "Deblochează setări de rețea"),
+        ("Unlock Network Settings", "Deblochează setările de rețea"),
         ("Server", "Server"),
         ("Direct IP Access", "Acces direct IP"),
         ("Proxy", "Proxy"),
@@ -366,26 +367,26 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
         ("Use IP Whitelisting", "Folosește lista de IP-uri autorizate"),
         ("Network", "Rețea"),
         ("Enable RDP", "Activează RDP"),
-        ("Pin Toolbar", ""),
-        ("Unpin Toolbar", ""),
+        ("Pin Toolbar", "Fixează bara de instrumente"),
+        ("Unpin Toolbar", "Detașează bara de instrumente"),
         ("Recording", "Înregistrare"),
         ("Directory", "Director"),
         ("Automatically record incoming sessions", "Înregistrează automat sesiunile viitoare"),
         ("Change", "Modifică"),
-        ("Start session recording", "Începe înregistrare"),
-        ("Stop session recording", "Oprește înregistrare"),
+        ("Start session recording", "Începe înregistrarea"),
+        ("Stop session recording", "Oprește înregistrarea"),
         ("Enable Recording Session", "Activează înregistrarea sesiunii"),
         ("Allow recording session", "Permite înregistrarea sesiunii"),
-        ("Enable LAN Discovery", "Activează descoperire LAN"),
-        ("Deny LAN Discovery", "Interzice descoperire LAN"),
+        ("Enable LAN Discovery", "Activează descoperirea LAN"),
+        ("Deny LAN Discovery", "Interzice descoperirea LAN"),
         ("Write a message", "Scrie un mesaj"),
-        ("Prompt", "Solicită"),
-        ("Please wait for confirmation of UAC...", "Așteaptă confirmarea UAC..."),
+        ("Prompt", "Prompt"),
+        ("Please wait for confirmation of UAC...", "Așteaptă confirmarea CCU..."),
         ("elevated_foreground_window_tip", "Fereastra actuală a dispozitivului la distanță necesită privilegii sporite pentru a funcționa, astfel că mouse-ul și tastatura nu pot fi folosite. Poți cere utilizatorului la distanță să minimizeze fereastra actuală sau să facă clic pe butonul de sporire a privilegiilor din fereastra de gestionare a conexiunilor. Pentru a evita această problemă, recomandăm instalarea software-ului pe dispozitivul la distanță."),
         ("Disconnected", "Deconectat"),
         ("Other", "Altele"),
         ("Confirm before closing multiple tabs", "Confirmă înainte de a închide mai multe file"),
-        ("Keyboard Settings", "Configurare tastatură"),
+        ("Keyboard Settings", "Setări tastatură"),
         ("Full Access", "Acces total"),
         ("Screen Share", "Partajare ecran"),
         ("Wayland requires Ubuntu 21.04 or higher version.", "Wayland necesită Ubuntu 21.04 sau o versiune superioară."),
@@ -396,121 +397,151 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
         ("This PC", "Acest PC"),
         ("or", "sau"),
         ("Continue with", "Continuă cu"),
-        ("Elevate", "Sporește"),
+        ("Elevate", "Sporește privilegii"),
         ("Zoom cursor", "Cursor lupă"),
-        ("Accept sessions via password", "Acceptă sesiunile folosind parola"),
-        ("Accept sessions via click", "Acceptă sesiunile cu un clic de confirmare"),
-        ("Accept sessions via both", "Acceptă sesiunile folosind ambele moduri"),
+        ("Accept sessions via password", "Acceptă începerea sesiunii folosind parola"),
+        ("Accept sessions via click", "Acceptă începerea sesiunii dând clic"),
+        ("Accept sessions via both", "Acceptă începerea sesiunii folosind ambele moduri"),
         ("Please wait for the remote side to accept your session request...", "Așteaptă ca solicitarea ta de conectare la distanță să fie acceptată..."),
         ("One-time Password", "Parolă unică"),
         ("Use one-time password", "Folosește parola unică"),
         ("One-time password length", "Lungimea parolei unice"),
-        ("Request access to your device", "Solicită acces la dispozitivul tău"),
+        ("Request access to your device", "Solicitare de acces la dispozitivul tău"),
         ("Hide connection management window", "Ascunde fereastra de gestionare a conexiunilor"),
         ("hide_cm_tip", "Permite ascunderea ferestrei de gestionare doar dacă accepți începerea sesiunilor folosind parola permanentă"),
-        ("wayland_experiment_tip", ""),
-        ("Right click to select tabs", ""),
-        ("Skipped", ""),
-        ("Add to Address Book", ""),
-        ("Group", ""),
-        ("Search", ""),
-        ("Closed manually by web console", ""),
-        ("Local keyboard type", ""),
-        ("Select local keyboard type", ""),
-        ("software_render_tip", ""),
-        ("Always use software rendering", ""),
-        ("config_input", ""),
-        ("config_microphone", ""),
-        ("request_elevation_tip", ""),
-        ("Wait", ""),
-        ("Elevation Error", ""),
-        ("Ask the remote user for authentication", ""),
-        ("Choose this if the remote account is administrator", ""),
-        ("Transmit the username and password of administrator", ""),
-        ("still_click_uac_tip", ""),
-        ("Request Elevation", ""),
-        ("wait_accept_uac_tip", ""),
-        ("Elevate successfully", ""),
-        ("uppercase", ""),
-        ("lowercase", ""),
-        ("digit", ""),
-        ("special character", ""),
-        ("length>=8", ""),
-        ("Weak", ""),
-        ("Medium", ""),
-        ("Strong", ""),
-        ("Switch Sides", ""),
-        ("Please confirm if you want to share your desktop?", ""),
-        ("Display", ""),
-        ("Default View Style", ""),
-        ("Default Scroll Style", ""),
-        ("Default Image Quality", ""),
-        ("Default Codec", ""),
-        ("Bitrate", ""),
-        ("FPS", ""),
-        ("Auto", ""),
-        ("Other Default Options", ""),
-        ("Voice call", ""),
-        ("Text chat", ""),
-        ("Stop voice call", ""),
-        ("relay_hint_tip", ""),
-        ("Reconnect", ""),
-        ("Codec", ""),
-        ("Resolution", ""),
-        ("No transfers in progress", ""),
-        ("Set one-time password length", ""),
-        ("idd_driver_tip", ""),
-        ("confirm_idd_driver_tip", ""),
-        ("RDP Settings", ""),
-        ("Sort by", ""),
-        ("New Connection", ""),
-        ("Restore", ""),
-        ("Minimize", ""),
-        ("Maximize", ""),
-        ("Your Device", ""),
-        ("empty_recent_tip", ""),
-        ("empty_favorite_tip", ""),
-        ("empty_lan_tip", ""),
-        ("empty_address_book_tip", ""),
-        ("eg: admin", ""),
-        ("Empty Username", ""),
-        ("Empty Password", ""),
-        ("Me", ""),
-        ("identical_file_tip", ""),
-        ("show_monitors_tip", ""),
-        ("View Mode", ""),
-        ("login_linux_tip", ""),
-        ("verify_rustdesk_password_tip", ""),
-        ("remember_account_tip", ""),
-        ("os_account_desk_tip", ""),
-        ("OS Account", ""),
-        ("another_user_login_title_tip", ""),
-        ("another_user_login_text_tip", ""),
-        ("xorg_not_found_title_tip", ""),
-        ("xorg_not_found_text_tip", ""),
-        ("no_desktop_title_tip", ""),
-        ("no_desktop_text_tip", ""),
-        ("No need to elevate", ""),
-        ("System Sound", ""),
-        ("Default", ""),
-        ("New RDP", ""),
-        ("Fingerprint", ""),
-        ("Copy Fingerprint", ""),
-        ("no fingerprints", ""),
-        ("Select a peer", ""),
-        ("Select peers", ""),
-        ("Plugins", ""),
-        ("Uninstall", ""),
-        ("Update", ""),
-        ("Enable", ""),
-        ("Disable", ""),
-        ("Options", ""),
-        ("resolution_original_tip", ""),
-        ("resolution_fit_local_tip", ""),
-        ("resolution_custom_tip", ""),
-        ("Collapse toolbar", ""),
-        ("Accept and Elevate", ""),
-        ("accept_and_elevate_btn_tooltip", ""),
-        ("clipboard_wait_response_timeout_tip", ""),
+        ("wayland_experiment_tip", "Wayland este acceptat doar într-o formă experimentală. Folosește X11 dacă nu ai nevoie de acces supravegheat."),
+        ("Right click to select tabs", "Dă clic dreapta pentru a selecta file"),
+        ("Skipped", "Ignorat"),
+        ("Add to Address Book", "Adaugă la agendă"),
+        ("Group", "Grup"),
+        ("Search", "Caută"),
+        ("Closed manually by web console", "Conexiune închisă manual de consola web"),
+        ("Local keyboard type", "Tastatură locală"),
+        ("Select local keyboard type", "Selectează tastatura locală"),
+        ("software_render_tip", "Dacă ai o placă video Nvidia și folosești Linux, iar fereastra cu conexiunea la distanță se închide imediat după conectare, îți sugerăm să instalezi driverul gratuit Nouveau și să folosești randarea de software. Este necesară repornirea."),
+        ("Always use software rendering", "Utilizează mereu randarea de software"),
+        ("config_input", "Pentru a controla desktopul la distanță folosind tastatura, trebuie să acorzi RustDesk permisiunea Monitorizare intrare"),
+        ("config_microphone", "Pentru a desfășura un apel vocal, este nevoie să acorzi RustDesk permisiunea Înregistrare audio."),
+        ("request_elevation_tip", "Poți solicita sporirea privilegiilor și dacă este cineva la desktopul la distanță."),
+        ("Wait", "În curs..."),
+        ("Elevation Error", "Eroare la sporirea privilegiilor"),
+        ("Ask the remote user for authentication", "Solicită utilizatorului de la distanță să se autentifice"),
+        ("Choose this if the remote account is administrator", "Alege asta dacă contul la distanță este un cont de administrator"),
+        ("Transmit the username and password of administrator", "Transmite numele de utilizator și parola administratorului"),
+        ("still_click_uac_tip", "Este necesar ca utilizatorul la distanță să confirme în fereastra CCU din RustDesk care rulează."),
+        ("Request Elevation", "Solicită sporirea privilegiilor"),
+        ("wait_accept_uac_tip", "Așteaptă ca utilizatorul la distanță să accepte dialogul CCU."),
+        ("Elevate successfully", "Sporirea privilegiilor realizată cu succes"),
+        ("uppercase", "majuscule"),
+        ("lowercase", "minuscule"),
+        ("digit", "cifre"),
+        ("special character", "caractere speciale"),
+        ("length>=8", "lungime>=8"),
+        ("Weak", "Slabă"),
+        ("Medium", "Medie"),
+        ("Strong", "Puternică"),
+        ("Switch Sides", "Inversează controlul"),
+        ("Please confirm if you want to share your desktop?", "Confirmi că dorești să îți partajezi desktopul?"),
+        ("Display", "Afișare"),
+        ("Default View Style", "Stilul implicit de vizualizare"),
+        ("Default Scroll Style", "Stilul implicit de derulare"),
+        ("Default Image Quality", "Calitatea implicită a imaginii"),
+        ("Default Codec", "Codec implicit"),
+        ("Bitrate", "Rată de biți"),
+        ("FPS", "CPS"),
+        ("Auto", "Auto"),
+        ("Other Default Options", "Alte opțiuni implicite"),
+        ("Voice call", "Apel vocal"),
+        ("Text chat", "Conversație text"),
+        ("Stop voice call", "Încheie apel vocal"),
+        ("relay_hint_tip", "Este posibil să nu te poți conecta direct; poți încerca să te conectezi prin retransmisie. De asemenea, dacă dorești să te conectezi direct prin retransmisie, poți adăuga sufixul „/r” la ID sau să bifezi opțiunea Conectează-te mereu prin retransmisie."),
+        ("Reconnect", "Reconectează-te"),
+        ("Codec", "Codec"),
+        ("Resolution", "Rezoluție"),
+        ("No transfers in progress", "Niciun transfer nu este în desfășurare"),
+        ("Set one-time password length", "Definește lungimea parolei unice"),
+        ("install_cert_tip", "Instalează certificatul RustDesk"),
+        ("confirm_install_cert_tip", "Acesta este un certificat de testare RustDesk și este de încredere. Certificatul va fi utilizat pentru a acorda încredere și instala drivere RustDesk atunci când este necesar."),
+        ("RDP Settings", "Setări RDP"),
+        ("Sort by", "Sortează după"),
+        ("New Connection", "Conexiune nouă"),
+        ("Restore", "Restaurează"),
+        ("Minimize", "Minimizează"),
+        ("Maximize", "Maximizează"),
+        ("Your Device", "Dispozitivul tău"),
+        ("empty_recent_tip", "Hopa! Nu există nicio sesiune recentă.\nPoate ar trebui să plănuiești una chiar acum!"),
+        ("empty_favorite_tip", "Încă nu ai niciun dispozitiv pereche favorit?\nHai să-ți găsim pe cineva cu care să te conectezi, iar apoi poți adăuga dispozitivul la Favorite!"),
+        ("empty_lan_tip", "Of! S-ar părea că încă nu am descoperit niciun dispozitiv."),
+        ("empty_address_book_tip", "Măi să fie! Se pare că deocamdată nu figurează niciun dispozitiv în agenda ta."),
+        ("eg: admin", "ex: admin"),
+        ("Empty Username", "Nume utilizator nespecificat"),
+        ("Empty Password", "Parolă nespecificată"),
+        ("Me", "Eu"),
+        ("identical_file_tip", "Acest fișier este identic cu cel al dispozitivului pereche."),
+        ("show_monitors_tip", "Afișează monitoare în bara de instrumente"),
+        ("View Mode", "Mod vizualizare"),
+        ("login_linux_tip", "Este necesar să te conectezi la contul de Linux de la distanță pentru a începe o sesiune cu un desktop care folosește X11"),
+        ("verify_rustdesk_password_tip", "Verifică parola RustDesk"),
+        ("remember_account_tip", "Reține contul"),
+        ("os_account_desk_tip", "Acest cont este utilizat pentru conectarea la sistemul de operare la distanță și începerea sesiunii cu desktopul în modul fără afișaj."),
+        ("OS Account", "Cont OS"),
+        ("another_user_login_title_tip", "Un alt utilizator este deja conectat"),
+        ("another_user_login_text_tip", "Deconectare"),
+        ("xorg_not_found_title_tip", "Xorg nu a fost găsit"),
+        ("xorg_not_found_text_tip", "Instalează Xorg"),
+        ("no_desktop_title_tip", "Nu este disponibil niciun mediu desktop"),
+        ("no_desktop_text_tip", "Instalează mediul desktop GNOME"),
+        ("No need to elevate", "Nu sunt necesare permisiuni de administrator"),
+        ("System Sound", "Sunet sistem"),
+        ("Default", "Implicit"),
+        ("New RDP", "RDP nou"),
+        ("Fingerprint", "Amprentă digitală"),
+        ("Copy Fingerprint", "Copiază amprenta digitală"),
+        ("no fingerprints", "Nicio amprentă digitală"),
+        ("Select a peer", "Selectează un dispozitiv pereche"),
+        ("Select peers", "Selectează dispozitive pereche"),
+        ("Plugins", "Pluginuri"),
+        ("Uninstall", "Dezinstalează"),
+        ("Update", "Actualizează"),
+        ("Enable", "Activează"),
+        ("Disable", "Dezactivează"),
+        ("Options", "Opțiuni"),
+        ("resolution_original_tip", "Rezoluție originală"),
+        ("resolution_fit_local_tip", "Adaptează la rezoluția locală"),
+        ("resolution_custom_tip", "Rezoluție personalizată"),
+        ("Collapse toolbar", "Restrânge bara de instrumente"),
+        ("Accept and Elevate", "Acceptă și sporește privilegii"),
+        ("accept_and_elevate_btn_tooltip", "Acceptă conectarea și sporește privilegiile CCU"),
+        ("clipboard_wait_response_timeout_tip", "Procesul a expirat așteptând un răspuns la copiere"),
+        ("Incoming connection", "Conexiune de intrare"),
+        ("Outgoing connection", "Conexiune de ieșire"),
+        ("Exit", "Ieși"),
+        ("Open", "Deschide"),
+        ("logout_tip", "Sigur vrei să te deconectezi?"),
+        ("Service", ""),
+        ("Start", ""),
+        ("Stop", ""),
+        ("exceed_max_devices", ""),
+        ("Sync with recent sessions", ""),
+        ("Sort tags", ""),
+        ("Open connection in new tab", ""),
+        ("Move tab to new window", ""),
+        ("Can not be empty", ""),
+        ("Already exists", ""),
+        ("Change Password", ""),
+        ("Refresh Password", ""),
+        ("ID", ""),
+        ("Grid View", ""),
+        ("List View", ""),
+        ("Select", ""),
+        ("Toggle Tags", ""),
+        ("pull_ab_failed_tip", ""),
+        ("push_ab_failed_tip", ""),
+        ("synced_peer_readded_tip", ""),
+        ("Change Color", ""),
+        ("Primary Color", ""),
+        ("HSV Color", ""),
+        ("Installation Successful!", ""),
+        ("Installation failed!", ""),
     ].iter().cloned().collect();
 }
diff --git a/src/lang/ru.rs b/src/lang/ru.rs
index 98d206131..2f86351cb 100644
--- a/src/lang/ru.rs
+++ b/src/lang/ru.rs
@@ -28,7 +28,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
         ("Enable File Transfer", "Включить передачу файлов"),
         ("Enable TCP Tunneling", "Включить туннелирование TCP"),
         ("IP Whitelisting", "Список разрешённых IP-адресов"),
-        ("ID/Relay Server", "ID/Сервер ретрансляции"),
+        ("ID/Relay Server", "ID/Ретранслятор"),
         ("Import Server Config", "Импортировать конфигурацию сервера"),
         ("Export Server Config", "Экспортировать конфигурацию сервера"),
         ("Import server configuration successfully", "Конфигурация сервера успешно импортирована"),
@@ -53,10 +53,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
         ("Audio Input", "Аудиовход"),
         ("Enhancements", "Улучшения"),
         ("Hardware Codec", "Аппаратный кодек"),
-        ("Adaptive Bitrate", "Адаптивная скорость потока"),
-        ("ID Server", "ID-сервер"),
-        ("Relay Server", "Сервер ретрансляции"),
-        ("API Server", "API-сервер"),
+        ("Adaptive bitrate", "Адаптивная скорость потока"),
+        ("ID Server", "Сервер ID"),
+        ("Relay Server", "Ретранслятор"),
+        ("API Server", "Сервер API"),
         ("invalid_http", "Адрес должен начинаться с http:// или https://"),
         ("Invalid IP", "Неправильный IP-адрес"),
         ("Invalid format", "Неправильный формат"),
@@ -139,13 +139,13 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
         ("Remote desktop is offline", "Удалённый рабочий стол не в сети"),
         ("Key mismatch", "Несоответствие ключей"),
         ("Timeout", "Истекло время ожидания"),
-        ("Failed to connect to relay server", "Невозможно подключиться к серверу ретрансляции"),
+        ("Failed to connect to relay server", "Невозможно подключиться к ретранслятору"),
         ("Failed to connect via rendezvous server", "Невозможно подключиться через промежуточный сервер"),
-        ("Failed to connect via relay server", "Невозможно подключиться через сервер ретрансляции"),
+        ("Failed to connect via relay server", "Невозможно подключиться через ретранслятор"),
         ("Failed to make direct connection to remote desktop", "Невозможно установить прямое подключение к удалённому рабочему столу"),
         ("Set Password", "Установить пароль"),
         ("OS Password", "Пароль ОС"),
-        ("install_tip", "В некоторых случаях из-за UAC RustDesk может работать неправильно на удалённом узле. Чтобы избежать UAC, нажмите кнопку ниже, чтобы установить RustDesk в системе."),
+        ("install_tip", "В некоторых случаях из-за UAC RustDesk может работать неправильно на удалённом узле. Чтобы избежать UAC, нажмите кнопку ниже для установки RustDesk в системе."),
         ("Click to upgrade", "Нажмите для проверки обновлений"),
         ("Click to download", "Нажмите для скачивания"),
         ("Click to update", "Нажмите для обновления"),
@@ -225,7 +225,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
         ("Logout", "Выйти"),
         ("Tags", "Метки"),
         ("Search ID", "Поиск по ID"),
-        ("whitelist_sep", "Раздельно запятой, точкой с запятой, пробелом или новой строкой"),
+        ("whitelist_sep", "Разделение запятой, точкой с запятой, пробелом или новой строкой"),
         ("Add ID", "Добавить ID"),
         ("Add Tag", "Добавить ключевое слово"),
         ("Unselect all tags", "Отменить выбор всех меток"),
@@ -233,6 +233,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
         ("Username missed", "Имя пользователя отсутствует"),
         ("Password missed", "Забыли пароль"),
         ("Wrong credentials", "Неправильные учётные данные"),
+        ("The verification code is incorrect or has expired", "Проверочный код неправильный или устарел"),
         ("Edit Tag", "Изменить метку"),
         ("Unremember Password", "Не сохранять пароль"),
         ("Favorites", "Избранное"),
@@ -298,7 +299,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
         ("Help", "Помощь"),
         ("Failed", "Не выполнено"),
         ("Succeeded", "Выполнено"),
-        ("Someone turns on privacy mode, exit", "Кто-то включает режим конфиденциальности, выход"),
+        ("Someone turns on privacy mode, exit", "Кто-то включил режим конфиденциальности, выход"),
         ("Unsupported", "Не поддерживается"),
         ("Peer denied", "Отклонено удалённым узлом"),
         ("Please install plugins", "Установите плагины"),
@@ -399,8 +400,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
         ("Elevate", "Повысить"),
         ("Zoom cursor", "Масштабировать курсор"),
         ("Accept sessions via password", "Принимать сеансы по паролю"),
-        ("Accept sessions via click", "Принимать сеансы по нажатию"),
-        ("Accept sessions via both", "Принимать сеансы по паролю+нажатию"),
+        ("Accept sessions via click", "Принимать сеансы нажатем кнопки"),
+        ("Accept sessions via both", "Принимать сеансы по паролю и нажатию кнопки"),
         ("Please wait for the remote side to accept your session request...", "Подождите, пока удалённая сторона примет ваш запрос на сеанс..."),
         ("One-time Password", "Одноразовый пароль"),
         ("Use one-time password", "Использовать одноразовый пароль"),
@@ -453,14 +454,14 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
         ("Voice call", "Голосовой вызов"),
         ("Text chat", "Текстовый чат"),
         ("Stop voice call", "Завершить голосовой вызов"),
-        ("relay_hint_tip", "Прямое подключение может оказаться невозможным. В этом случае можно попытаться подключиться через сервер ретрансляции. \nКроме того, если вы хотите сразу использовать сервер ретрансляции, можно добавить к ID суффикс \"/r\" или включить \"Всегда подключаться через ретранслятор\" в настройках удалённого узла."),
+        ("relay_hint_tip", "Прямое подключение может оказаться невозможным. В этом случае можно попытаться подключиться через ретранслятор. \nКроме того, если вы хотите сразу использовать ретранслятор, можно добавить к ID суффикс \"/r\" или включить \"Всегда подключаться через ретранслятор\" в настройках удалённого узла."),
         ("Reconnect", "Переподключить"),
         ("Codec", "Кодек"),
         ("Resolution", "Разрешение"),
         ("No transfers in progress", "Передача не осуществляется"),
         ("Set one-time password length", "Установить длину одноразового пароля"),
-        ("idd_driver_tip", "Установить драйвер виртуального дисплея (используется при отсутствии физических дисплеев)"),
-        ("confirm_idd_driver_tip", "Включена функция установки драйвера виртуального дисплея. Обратите внимание, что для доверия к драйверу будет установлен тестовый сертификат. Этот сертификат будет использоваться только для подтверждения доверия драйверам Rustdesk."),
+        ("install_cert_tip", "Установить сертификат RustDesk"),
+        ("confirm_install_cert_tip", "Это тестовый сертификат RustDesk, которому можно доверять. Он будет использоваться только по необходимости для установки драйверов RustDesk."),
         ("RDP Settings", "Настройки RDP"),
         ("Sort by", "Сортировка"),
         ("New Connection", "Новое подключение"),
@@ -511,6 +512,36 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
         ("Collapse toolbar", "Свернуть панель инструментов"),
         ("Accept and Elevate", "Принять и повысить"),
         ("accept_and_elevate_btn_tooltip", "Разрешить подключение и повысить права UAC."),
-        ("clipboard_wait_response_timeout_tip", "Время ожидания копирования буфера обмена, истекло"),
+        ("clipboard_wait_response_timeout_tip", "Время ожидания копирования буфера обмена истекло"),
+        ("Incoming connection", "Входящее подключение"),
+        ("Outgoing connection", "Исходящее подключение"),
+        ("Exit", "Выход"),
+        ("Open", "Открыть"),
+        ("logout_tip", "Вы действительно хотите выйти?"),
+        ("Service", "Сервис"),
+        ("Start", "Запустить"),
+        ("Stop", "Остановить"),
+        ("exceed_max_devices", "Достигнуто максимальное количество управляемых устройств."),
+        ("Sync with recent sessions", "Синхронизация последних сеансов"),
+        ("Sort tags", "Сортировка меток"),
+        ("Open connection in new tab", "Открыть подключение в новой вкладке"),
+        ("Move tab to new window", "Переместить вкладку в отдельное окно"),
+        ("Can not be empty", "Не может быть пустым"),
+        ("Already exists", "Уже существует"),
+        ("Change Password", "Изменить пароль"),
+        ("Refresh Password", "Обновить пароль"),
+        ("ID", "ID"),
+        ("Grid View", "Сетка"),
+        ("List View", "Список"),
+        ("Select", "Выбор"),
+        ("Toggle Tags", "Переключить метки"),
+        ("pull_ab_failed_tip", "Невозможно обновить адресную книгу"),
+        ("push_ab_failed_tip", "Невозможно синхронизировать адресную книгу с сервером"),
+        ("synced_peer_readded_tip", "Устройства, присутствовавшие в последних сеансах, будут синхронизированы с адресной книгой."),
+        ("Change Color", "Изменить цвет"),
+        ("Primary Color", "Основной цвет"),
+        ("HSV Color", "Цвет HSV"),
+        ("Installation Successful!", "Установка выполнена успешно!"),
+        ("Installation failed!", "Установка не выполнена!"),
     ].iter().cloned().collect();
 }
diff --git a/src/lang/sk.rs b/src/lang/sk.rs
index c99330096..615dd08f4 100644
--- a/src/lang/sk.rs
+++ b/src/lang/sk.rs
@@ -53,7 +53,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
         ("Audio Input", "Zvukový vstup"),
         ("Enhancements", ""),
         ("Hardware Codec", ""),
-        ("Adaptive Bitrate", ""),
+        ("Adaptive bitrate", ""),
         ("ID Server", "ID server"),
         ("Relay Server", "Prepojovací server"),
         ("API Server", "API server"),
@@ -233,6 +233,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
         ("Username missed", "Chýba užívateľské meno"),
         ("Password missed", "Chýba heslo"),
         ("Wrong credentials", "Nesprávne prihlasovacie údaje"),
+        ("The verification code is incorrect or has expired", ""),
         ("Edit Tag", "Upraviť štítok"),
         ("Unremember Password", "Zabudnúť heslo"),
         ("Favorites", "Obľúbené"),
@@ -459,8 +460,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
         ("Resolution", ""),
         ("No transfers in progress", ""),
         ("Set one-time password length", ""),
-        ("idd_driver_tip", ""),
-        ("confirm_idd_driver_tip", ""),
+        ("install_cert_tip", ""),
+        ("confirm_install_cert_tip", ""),
         ("RDP Settings", ""),
         ("Sort by", ""),
         ("New Connection", ""),
@@ -512,5 +513,35 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
         ("Accept and Elevate", ""),
         ("accept_and_elevate_btn_tooltip", ""),
         ("clipboard_wait_response_timeout_tip", ""),
+        ("Incoming connection", ""),
+        ("Outgoing connection", ""),
+        ("Exit", ""),
+        ("Open", ""),
+        ("logout_tip", ""),
+        ("Service", ""),
+        ("Start", ""),
+        ("Stop", ""),
+        ("exceed_max_devices", ""),
+        ("Sync with recent sessions", ""),
+        ("Sort tags", ""),
+        ("Open connection in new tab", ""),
+        ("Move tab to new window", ""),
+        ("Can not be empty", ""),
+        ("Already exists", ""),
+        ("Change Password", ""),
+        ("Refresh Password", ""),
+        ("ID", ""),
+        ("Grid View", ""),
+        ("List View", ""),
+        ("Select", ""),
+        ("Toggle Tags", ""),
+        ("pull_ab_failed_tip", ""),
+        ("push_ab_failed_tip", ""),
+        ("synced_peer_readded_tip", ""),
+        ("Change Color", ""),
+        ("Primary Color", ""),
+        ("HSV Color", ""),
+        ("Installation Successful!", ""),
+        ("Installation failed!", ""),
     ].iter().cloned().collect();
 }
diff --git a/src/lang/sl.rs b/src/lang/sl.rs
index 99e3f2805..a05b5f4c0 100755
--- a/src/lang/sl.rs
+++ b/src/lang/sl.rs
@@ -53,7 +53,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
         ("Audio Input", "Avdio vhod"),
         ("Enhancements", "Izboljšave"),
         ("Hardware Codec", "Strojni kodek"),
-        ("Adaptive Bitrate", "Prilagodljiva bitna hitrost"),
+        ("Adaptive bitrate", "Prilagodljiva bitna hitrost"),
         ("ID Server", "ID strežnik"),
         ("Relay Server", "Posredniški strežnik"),
         ("API Server", "API strežnik"),
@@ -233,6 +233,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
         ("Username missed", "Up. ime izpuščeno"),
         ("Password missed", "Geslo izpuščeno"),
         ("Wrong credentials", "Napačne poverilnice"),
+        ("The verification code is incorrect or has expired", ""),
         ("Edit Tag", "Uredi oznako"),
         ("Unremember Password", "Pozabi geslo"),
         ("Favorites", "Priljubljene"),
@@ -459,8 +460,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
         ("Resolution", ""),
         ("No transfers in progress", ""),
         ("Set one-time password length", ""),
-        ("idd_driver_tip", ""),
-        ("confirm_idd_driver_tip", ""),
+        ("install_cert_tip", ""),
+        ("confirm_install_cert_tip", ""),
         ("RDP Settings", ""),
         ("Sort by", ""),
         ("New Connection", ""),
@@ -512,5 +513,35 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
         ("Accept and Elevate", ""),
         ("accept_and_elevate_btn_tooltip", ""),
         ("clipboard_wait_response_timeout_tip", ""),
+        ("Incoming connection", ""),
+        ("Outgoing connection", ""),
+        ("Exit", ""),
+        ("Open", ""),
+        ("logout_tip", ""),
+        ("Service", ""),
+        ("Start", ""),
+        ("Stop", ""),
+        ("exceed_max_devices", ""),
+        ("Sync with recent sessions", ""),
+        ("Sort tags", ""),
+        ("Open connection in new tab", ""),
+        ("Move tab to new window", ""),
+        ("Can not be empty", ""),
+        ("Already exists", ""),
+        ("Change Password", ""),
+        ("Refresh Password", ""),
+        ("ID", ""),
+        ("Grid View", ""),
+        ("List View", ""),
+        ("Select", ""),
+        ("Toggle Tags", ""),
+        ("pull_ab_failed_tip", ""),
+        ("push_ab_failed_tip", ""),
+        ("synced_peer_readded_tip", ""),
+        ("Change Color", ""),
+        ("Primary Color", ""),
+        ("HSV Color", ""),
+        ("Installation Successful!", ""),
+        ("Installation failed!", ""),
     ].iter().cloned().collect();
 }
diff --git a/src/lang/sq.rs b/src/lang/sq.rs
index 04735b4b8..cfc2fda74 100644
--- a/src/lang/sq.rs
+++ b/src/lang/sq.rs
@@ -53,7 +53,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
         ("Audio Input", "Inputi zërit"),
         ("Enhancements", "Përmirësimet"),
         ("Hardware Codec", "Kodeku Harduerik"),
-        ("Adaptive Bitrate", "Shpejtësia adaptive e biteve"),
+        ("Adaptive bitrate", "Shpejtësia adaptive e biteve"),
         ("ID Server", "ID e serverit"),
         ("Relay Server", "Serveri rele"),
         ("API Server", "Serveri API"),
@@ -233,6 +233,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
         ("Username missed", "Mungon përdorusesi"),
         ("Password missed", "Mungon fjalëkalimi"),
         ("Wrong credentials", "Kredinciale të gabuara"),
+        ("The verification code is incorrect or has expired", ""),
         ("Edit Tag", "Edito tagun"),
         ("Unremember Password", "Fjalëkalim jo i kujtueshëm"),
         ("Favorites", "Te preferuarat"),
@@ -459,8 +460,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
         ("Resolution", ""),
         ("No transfers in progress", ""),
         ("Set one-time password length", ""),
-        ("idd_driver_tip", ""),
-        ("confirm_idd_driver_tip", ""),
+        ("install_cert_tip", ""),
+        ("confirm_install_cert_tip", ""),
         ("RDP Settings", ""),
         ("Sort by", ""),
         ("New Connection", ""),
@@ -512,5 +513,35 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
         ("Accept and Elevate", ""),
         ("accept_and_elevate_btn_tooltip", ""),
         ("clipboard_wait_response_timeout_tip", ""),
+        ("Incoming connection", ""),
+        ("Outgoing connection", ""),
+        ("Exit", ""),
+        ("Open", ""),
+        ("logout_tip", ""),
+        ("Service", ""),
+        ("Start", ""),
+        ("Stop", ""),
+        ("exceed_max_devices", ""),
+        ("Sync with recent sessions", ""),
+        ("Sort tags", ""),
+        ("Open connection in new tab", ""),
+        ("Move tab to new window", ""),
+        ("Can not be empty", ""),
+        ("Already exists", ""),
+        ("Change Password", ""),
+        ("Refresh Password", ""),
+        ("ID", ""),
+        ("Grid View", ""),
+        ("List View", ""),
+        ("Select", ""),
+        ("Toggle Tags", ""),
+        ("pull_ab_failed_tip", ""),
+        ("push_ab_failed_tip", ""),
+        ("synced_peer_readded_tip", ""),
+        ("Change Color", ""),
+        ("Primary Color", ""),
+        ("HSV Color", ""),
+        ("Installation Successful!", ""),
+        ("Installation failed!", ""),
     ].iter().cloned().collect();
 }
diff --git a/src/lang/sr.rs b/src/lang/sr.rs
index 9a9e251de..85bb6f488 100644
--- a/src/lang/sr.rs
+++ b/src/lang/sr.rs
@@ -53,7 +53,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
         ("Audio Input", "Audio ulaz"),
         ("Enhancements", "Proširenja"),
         ("Hardware Codec", "Hardverski kodek"),
-        ("Adaptive Bitrate", "Prilagodljiva gustina podataka"),
+        ("Adaptive bitrate", "Prilagodljiva gustina podataka"),
         ("ID Server", "ID server"),
         ("Relay Server", "Posredni server"),
         ("API Server", "API server"),
@@ -233,6 +233,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
         ("Username missed", "Korisničko ime promašeno"),
         ("Password missed", "Lozinka promašena"),
         ("Wrong credentials", "Pogrešno korisničko ime ili lozinka"),
+        ("The verification code is incorrect or has expired", ""),
         ("Edit Tag", "Izmeni oznaku"),
         ("Unremember Password", "Zaboravi lozinku"),
         ("Favorites", "Favoriti"),
@@ -459,8 +460,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
         ("Resolution", ""),
         ("No transfers in progress", ""),
         ("Set one-time password length", ""),
-        ("idd_driver_tip", ""),
-        ("confirm_idd_driver_tip", ""),
+        ("install_cert_tip", ""),
+        ("confirm_install_cert_tip", ""),
         ("RDP Settings", ""),
         ("Sort by", ""),
         ("New Connection", ""),
@@ -512,5 +513,35 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
         ("Accept and Elevate", ""),
         ("accept_and_elevate_btn_tooltip", ""),
         ("clipboard_wait_response_timeout_tip", ""),
+        ("Incoming connection", ""),
+        ("Outgoing connection", ""),
+        ("Exit", ""),
+        ("Open", ""),
+        ("logout_tip", ""),
+        ("Service", ""),
+        ("Start", ""),
+        ("Stop", ""),
+        ("exceed_max_devices", ""),
+        ("Sync with recent sessions", ""),
+        ("Sort tags", ""),
+        ("Open connection in new tab", ""),
+        ("Move tab to new window", ""),
+        ("Can not be empty", ""),
+        ("Already exists", ""),
+        ("Change Password", ""),
+        ("Refresh Password", ""),
+        ("ID", ""),
+        ("Grid View", ""),
+        ("List View", ""),
+        ("Select", ""),
+        ("Toggle Tags", ""),
+        ("pull_ab_failed_tip", ""),
+        ("push_ab_failed_tip", ""),
+        ("synced_peer_readded_tip", ""),
+        ("Change Color", ""),
+        ("Primary Color", ""),
+        ("HSV Color", ""),
+        ("Installation Successful!", ""),
+        ("Installation failed!", ""),
     ].iter().cloned().collect();
 }
diff --git a/src/lang/sv.rs b/src/lang/sv.rs
index 73eb138bd..90f543617 100644
--- a/src/lang/sv.rs
+++ b/src/lang/sv.rs
@@ -53,7 +53,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
         ("Audio Input", "Ljud input"),
         ("Enhancements", "Förbättringar"),
         ("Hardware Codec", "Hårdvarucodec"),
-        ("Adaptive Bitrate", "Adaptiv Bitrate"),
+        ("Adaptive bitrate", "Adaptiv Bitrate"),
         ("ID Server", "ID server"),
         ("Relay Server", "Relay Server"),
         ("API Server", "API Server"),
@@ -233,6 +233,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
         ("Username missed", "Användarnamn saknas"),
         ("Password missed", "Lösenord saknas"),
         ("Wrong credentials", "Fel användarnamn eller lösenord"),
+        ("The verification code is incorrect or has expired", ""),
         ("Edit Tag", "Ändra Tagg"),
         ("Unremember Password", "Glöm lösenord"),
         ("Favorites", "Favoriter"),
@@ -459,8 +460,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
         ("Resolution", ""),
         ("No transfers in progress", ""),
         ("Set one-time password length", ""),
-        ("idd_driver_tip", ""),
-        ("confirm_idd_driver_tip", ""),
+        ("install_cert_tip", ""),
+        ("confirm_install_cert_tip", ""),
         ("RDP Settings", ""),
         ("Sort by", ""),
         ("New Connection", ""),
@@ -512,5 +513,35 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
         ("Accept and Elevate", ""),
         ("accept_and_elevate_btn_tooltip", ""),
         ("clipboard_wait_response_timeout_tip", ""),
+        ("Incoming connection", ""),
+        ("Outgoing connection", ""),
+        ("Exit", ""),
+        ("Open", ""),
+        ("logout_tip", ""),
+        ("Service", ""),
+        ("Start", ""),
+        ("Stop", ""),
+        ("exceed_max_devices", ""),
+        ("Sync with recent sessions", ""),
+        ("Sort tags", ""),
+        ("Open connection in new tab", ""),
+        ("Move tab to new window", ""),
+        ("Can not be empty", ""),
+        ("Already exists", ""),
+        ("Change Password", ""),
+        ("Refresh Password", ""),
+        ("ID", ""),
+        ("Grid View", ""),
+        ("List View", ""),
+        ("Select", ""),
+        ("Toggle Tags", ""),
+        ("pull_ab_failed_tip", ""),
+        ("push_ab_failed_tip", ""),
+        ("synced_peer_readded_tip", ""),
+        ("Change Color", ""),
+        ("Primary Color", ""),
+        ("HSV Color", ""),
+        ("Installation Successful!", ""),
+        ("Installation failed!", ""),
     ].iter().cloned().collect();
 }
diff --git a/src/lang/template.rs b/src/lang/template.rs
index e5fa47fd2..7f58b4452 100644
--- a/src/lang/template.rs
+++ b/src/lang/template.rs
@@ -53,7 +53,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
         ("Audio Input", ""),
         ("Enhancements", ""),
         ("Hardware Codec", ""),
-        ("Adaptive Bitrate", ""),
+        ("Adaptive bitrate", ""),
         ("ID Server", ""),
         ("Relay Server", ""),
         ("API Server", ""),
@@ -233,6 +233,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
         ("Username missed", ""),
         ("Password missed", ""),
         ("Wrong credentials", ""),
+        ("The verification code is incorrect or has expired", ""),
         ("Edit Tag", ""),
         ("Unremember Password", ""),
         ("Favorites", ""),
@@ -459,8 +460,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
         ("Resolution", ""),
         ("No transfers in progress", ""),
         ("Set one-time password length", ""),
-        ("idd_driver_tip", ""),
-        ("confirm_idd_driver_tip", ""),
+        ("install_cert_tip", ""),
+        ("confirm_install_cert_tip", ""),
         ("RDP Settings", ""),
         ("Sort by", ""),
         ("New Connection", ""),
@@ -512,5 +513,35 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
         ("Accept and Elevate", ""),
         ("accept_and_elevate_btn_tooltip", ""),
         ("clipboard_wait_response_timeout_tip", ""),
+        ("Incoming connection", ""),
+        ("Outgoing connection", ""),
+        ("Exit", ""),
+        ("Open", ""),
+        ("logout_tip", ""),
+        ("Service", ""),
+        ("Start", ""),
+        ("Stop", ""),
+        ("exceed_max_devices", ""),
+        ("Sync with recent sessions", ""),
+        ("Sort tags", ""),
+        ("Open connection in new tab", ""),
+        ("Move tab to new window", ""),
+        ("Can not be empty", ""),
+        ("Already exists", ""),
+        ("Change Password", ""),
+        ("Refresh Password", ""),
+        ("ID", ""),
+        ("Grid View", ""),
+        ("List View", ""),
+        ("Select", ""),
+        ("Toggle Tags", ""),
+        ("pull_ab_failed_tip", ""),
+        ("push_ab_failed_tip", ""),
+        ("synced_peer_readded_tip", ""),
+        ("Change Color", ""),
+        ("Primary Color", ""),
+        ("HSV Color", ""),
+        ("Installation Successful!", ""),
+        ("Installation failed!", ""),
     ].iter().cloned().collect();
 }
diff --git a/src/lang/th.rs b/src/lang/th.rs
index 78aa94c6f..94251c5b2 100644
--- a/src/lang/th.rs
+++ b/src/lang/th.rs
@@ -53,7 +53,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
         ("Audio Input", "ออดิโออินพุท"),
         ("Enhancements", "การปรับปรุง"),
         ("Hardware Codec", "ฮาร์ดแวร์ codec"),
-        ("Adaptive Bitrate", "บิทเรทผันแปร"),
+        ("Adaptive bitrate", "บิทเรทผันแปร"),
         ("ID Server", "เซิร์ฟเวอร์ ID"),
         ("Relay Server", "เซิร์ฟเวอร์ Relay"),
         ("API Server", "เซิร์ฟเวอร์ API"),
@@ -233,6 +233,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
         ("Username missed", "ไม่พบข้อมูลผู้ใช้งาน"),
         ("Password missed", "ไม่พบรหัสผ่าน"),
         ("Wrong credentials", "ข้อมูลสำหรับเข้าสู่ระบบไม่ถูกต้อง"),
+        ("The verification code is incorrect or has expired", ""),
         ("Edit Tag", "แก้ไขแท็ก"),
         ("Unremember Password", "ยกเลิกการจดจำรหัสผ่าน"),
         ("Favorites", "รายการโปรด"),
@@ -459,8 +460,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
         ("Resolution", ""),
         ("No transfers in progress", ""),
         ("Set one-time password length", ""),
-        ("idd_driver_tip", ""),
-        ("confirm_idd_driver_tip", ""),
+        ("install_cert_tip", ""),
+        ("confirm_install_cert_tip", ""),
         ("RDP Settings", ""),
         ("Sort by", ""),
         ("New Connection", ""),
@@ -512,5 +513,35 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
         ("Accept and Elevate", ""),
         ("accept_and_elevate_btn_tooltip", ""),
         ("clipboard_wait_response_timeout_tip", ""),
+        ("Incoming connection", ""),
+        ("Outgoing connection", ""),
+        ("Exit", ""),
+        ("Open", ""),
+        ("logout_tip", ""),
+        ("Service", ""),
+        ("Start", ""),
+        ("Stop", ""),
+        ("exceed_max_devices", ""),
+        ("Sync with recent sessions", ""),
+        ("Sort tags", ""),
+        ("Open connection in new tab", ""),
+        ("Move tab to new window", ""),
+        ("Can not be empty", ""),
+        ("Already exists", ""),
+        ("Change Password", ""),
+        ("Refresh Password", ""),
+        ("ID", ""),
+        ("Grid View", ""),
+        ("List View", ""),
+        ("Select", ""),
+        ("Toggle Tags", ""),
+        ("pull_ab_failed_tip", ""),
+        ("push_ab_failed_tip", ""),
+        ("synced_peer_readded_tip", ""),
+        ("Change Color", ""),
+        ("Primary Color", ""),
+        ("HSV Color", ""),
+        ("Installation Successful!", ""),
+        ("Installation failed!", ""),
     ].iter().cloned().collect();
 }
diff --git a/src/lang/tr.rs b/src/lang/tr.rs
index bc9cd694f..57a337b1a 100644
--- a/src/lang/tr.rs
+++ b/src/lang/tr.rs
@@ -53,7 +53,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
         ("Audio Input", "Ses Girişi"),
         ("Enhancements", "Geliştirmeler"),
         ("Hardware Codec", "Donanımsal Codec"),
-        ("Adaptive Bitrate", "Uyarlanabilir Bit Hızı"),
+        ("Adaptive bitrate", "Uyarlanabilir Bit Hızı"),
         ("ID Server", "ID Sunucu"),
         ("Relay Server", "Relay Sunucu"),
         ("API Server", "API Sunucu"),
@@ -233,6 +233,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
         ("Username missed", "Kullanıcı adı boş"),
         ("Password missed", "Şifre boş"),
         ("Wrong credentials", "Yanlış kimlik bilgileri"),
+        ("The verification code is incorrect or has expired", ""),
         ("Edit Tag", "Etiketi düzenle"),
         ("Unremember Password", "Şifreyi Unut"),
         ("Favorites", "Favoriler"),
@@ -407,110 +408,140 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
         ("One-time password length", "Tek seferlik şifre uzunluğu"),
         ("Request access to your device", "Cihazınıza erişim talep edin"),
         ("Hide connection management window", "Bağlantı yönetimi penceresini gizle"),
-        ("hide_cm_tip", ""),
-        ("wayland_experiment_tip", ""),
-        ("Right click to select tabs", ""),
-        ("Skipped", ""),
-        ("Add to Address Book", ""),
-        ("Group", ""),
-        ("Search", ""),
-        ("Closed manually by web console", ""),
-        ("Local keyboard type", ""),
-        ("Select local keyboard type", ""),
-        ("software_render_tip", ""),
-        ("Always use software rendering", ""),
-        ("config_input", ""),
-        ("config_microphone", ""),
-        ("request_elevation_tip", ""),
-        ("Wait", ""),
-        ("Elevation Error", ""),
-        ("Ask the remote user for authentication", ""),
-        ("Choose this if the remote account is administrator", ""),
-        ("Transmit the username and password of administrator", ""),
-        ("still_click_uac_tip", ""),
-        ("Request Elevation", ""),
-        ("wait_accept_uac_tip", ""),
-        ("Elevate successfully", ""),
-        ("uppercase", ""),
-        ("lowercase", ""),
-        ("digit", ""),
-        ("special character", ""),
-        ("length>=8", ""),
-        ("Weak", ""),
-        ("Medium", ""),
-        ("Strong", ""),
-        ("Switch Sides", ""),
-        ("Please confirm if you want to share your desktop?", ""),
-        ("Display", ""),
-        ("Default View Style", ""),
-        ("Default Scroll Style", ""),
-        ("Default Image Quality", ""),
-        ("Default Codec", ""),
-        ("Bitrate", ""),
-        ("FPS", ""),
-        ("Auto", ""),
-        ("Other Default Options", ""),
-        ("Voice call", ""),
-        ("Text chat", ""),
-        ("Stop voice call", ""),
-        ("relay_hint_tip", ""),
-        ("Reconnect", ""),
-        ("Codec", ""),
-        ("Resolution", ""),
-        ("No transfers in progress", ""),
-        ("Set one-time password length", ""),
-        ("idd_driver_tip", ""),
-        ("confirm_idd_driver_tip", ""),
-        ("RDP Settings", ""),
-        ("Sort by", ""),
-        ("New Connection", ""),
-        ("Restore", ""),
-        ("Minimize", ""),
-        ("Maximize", ""),
-        ("Your Device", ""),
-        ("empty_recent_tip", ""),
-        ("empty_favorite_tip", ""),
-        ("empty_lan_tip", ""),
-        ("empty_address_book_tip", ""),
-        ("eg: admin", ""),
-        ("Empty Username", ""),
-        ("Empty Password", ""),
-        ("Me", ""),
-        ("identical_file_tip", ""),
-        ("show_monitors_tip", ""),
-        ("View Mode", ""),
-        ("login_linux_tip", ""),
-        ("verify_rustdesk_password_tip", ""),
-        ("remember_account_tip", ""),
-        ("os_account_desk_tip", ""),
-        ("OS Account", ""),
-        ("another_user_login_title_tip", ""),
-        ("another_user_login_text_tip", ""),
-        ("xorg_not_found_title_tip", ""),
-        ("xorg_not_found_text_tip", ""),
-        ("no_desktop_title_tip", ""),
-        ("no_desktop_text_tip", ""),
-        ("No need to elevate", ""),
-        ("System Sound", ""),
-        ("Default", ""),
-        ("New RDP", ""),
-        ("Fingerprint", ""),
-        ("Copy Fingerprint", ""),
-        ("no fingerprints", ""),
-        ("Select a peer", ""),
-        ("Select peers", ""),
-        ("Plugins", ""),
-        ("Uninstall", ""),
-        ("Update", ""),
-        ("Enable", ""),
-        ("Disable", ""),
-        ("Options", ""),
-        ("resolution_original_tip", ""),
-        ("resolution_fit_local_tip", ""),
-        ("resolution_custom_tip", ""),
-        ("Collapse toolbar", ""),
-        ("Accept and Elevate", ""),
-        ("accept_and_elevate_btn_tooltip", ""),
-        ("clipboard_wait_response_timeout_tip", ""),
+        ("hide_cm_tip", "Oturumları yalnızca parola ile kabul edebilir ve kalıcı parola kullanıyorsanız gizlemeye izin verin"),
+        ("wayland_experiment_tip", "Wayland desteği deneysel aşamada olduğundan, gerektiğinde X11'i kullanmanız önerilir"),
+        ("Right click to select tabs", "Sekmeleri seçmek için sağ tıklayın"),
+        ("Skipped", "Atlandı"),
+        ("Add to Address Book", "Adres Defterine Ekle"),
+        ("Group", "Grup"),
+        ("Search", "Ara"),
+        ("Closed manually by web console", "Web konsoluyla manuel olarak kapatıldı"),
+        ("Local keyboard type", "Yerel klavye türü"),
+        ("Select local keyboard type", "Yerel klavye türünü seçin"),
+        ("software_render_tip", "Linux altında Nvidia grafik kartı kullanıyorsanız ve uzak pencere bağlandıktan hemen sonra kapanıyorsa, açık kaynaklı Nouveau sürücüsüne geçmeyi ve yazılım renderleme seçeneğini seçmeyi deneyin. Yazılımı yeniden başlatmanız gerekebilir."),
+        ("Always use software rendering", "Her zaman yazılım renderleme kullan"),
+        ("config_input", "Uzaktaki masaüstünü klavye ile kontrol etmek için RustDesk'e \"Giriş İzleme\" izinleri vermelisiniz."),
+        ("config_microphone", "Uzaktan konuşmak için RustDesk'e \"Ses Kaydı\" izinleri vermelisiniz."),
+        ("request_elevation_tip", "Ayrıca, uzak tarafta biri varsa yükseltme isteğinde bulunabilirsiniz."),
+        ("Wait", "Bekle"),
+        ("Elevation Error", "Yükseltme Hatası"),
+        ("Ask the remote user for authentication", "Uzaktaki kullanıcıdan kimlik doğrulamasını isteyin"),
+        ("Choose this if the remote account is administrator", "Uzak hesap yönetici ise bunu seçin"),
+        ("Transmit the username and password of administrator", "Yönetici kullanıcı adı ve parolasını iletim yapın"),
+        ("still_click_uac_tip", "Uzaktaki kullanıcının çalışan RustDesk'in UAC penceresinde hala Tamam'ı tıklaması gerekmektedir."),
+        ("Request Elevation", "Yükseltme İsteği"),
+        ("wait_accept_uac_tip", "Lütfen uzaktaki kullanıcının UAC iletişim kutusunu kabul etmesini bekleyin."),
+        ("Elevate successfully", "Başarıyla yükseltildi"),
+        ("uppercase", "büyük harf"),
+        ("lowercase", "küçük harf"),
+        ("digit", "rakam"),
+        ("special character", "özel karakter"),
+        ("length>=8", "uzunluk>=8"),
+        ("Weak", "Zayıf"),
+        ("Medium", "Orta"),
+        ("Strong", "Güçlü"),
+        ("Switch Sides", "Tarafları Değiştir"),
+        ("Please confirm if you want to share your desktop?", "Masaüstünüzü paylaşmak isteyip istemediğinizi onaylayın?"),
+        ("Display", "Görüntüle"),
+        ("Default View Style", "Varsayılan Görünüm Stili"),
+        ("Default Scroll Style", "Varsayılan Kaydırma Stili"),
+        ("Default Image Quality", "Varsayılan Görüntü Kalitesi"),
+        ("Default Codec", "Varsayılan Kodlayıcı"),
+        ("Bitrate", "Bit Hızı"),
+        ("FPS", "FPS"),
+        ("Auto", "Otomatik"),
+        ("Other Default Options", "Diğer Varsayılan Seçenekler"),
+        ("Voice call", "Sesli görüşme"),
+        ("Text chat", "Metin sohbeti"),
+        ("Stop voice call", "Sesli görüşmeyi durdur"),
+        ("relay_hint_tip", "Doğrudan bağlanmak mümkün olmayabilir; röle aracılığıyla bağlanmayı deneyebilirsiniz. Ayrıca, ilk denemenizde bir röle kullanmak istiyorsanız, ID'nin sonuna \"/r\" ekleyebilir veya son oturum kartındaki \"Her Zaman Röle Üzerinden Bağlan\" seçeneğini seçebilirsiniz."),
+        ("Reconnect", "Yeniden Bağlan"),
+        ("Codec", "Kodlayıcı"),
+        ("Resolution", "Çözünürlük"),
+        ("No transfers in progress", "Devam eden aktarımlar yok"),
+        ("Set one-time password length", "Bir seferlik parola uzunluğunu ayarla"),
+        ("install_cert_tip", "RustDesk sertifikasını yükleyin"),
+        ("confirm_install_cert_tip", "Bu, güvenilir olabilecek bir RustDesk test sertifikasıdır. Sertifika, gerekli olduğunda RustDesk sürücülerini güvenilir ve yüklemek üzere kullanacaktır."),
+        ("RDP Settings", "RDP Ayarları"),
+        ("Sort by", "Sırala"),
+        ("New Connection", "Yeni Bağlantı"),
+        ("Restore", "Geri Yükle"),
+        ("Minimize", "Simge Durumuna Küçült"),
+        ("Maximize", "Büyüt"),
+        ("Your Device", "Cihazınız"),
+        ("empty_recent_tip", "Üzgünüz, henüz son oturum yok!\nYeni bir plan yapma zamanı."),
+        ("empty_favorite_tip", "Henüz favori cihazınız yok mu?\nBağlanacak ve favorilere eklemek için birini bulalım!"),
+        ("empty_lan_tip", "Hayır, henüz hiçbir cihaz bulamadık gibi görünüyor."),
+        ("empty_address_book_tip", "Üzgünüm, şu anda adres defterinizde kayıtlı cihaz yok gibi görünüyor."),
+        ("eg: admin", "örn: admin"),
+        ("Empty Username", "Boş Kullanıcı Adı"),
+        ("Empty Password", "Boş Parola"),
+        ("Me", "Ben"),
+        ("identical_file_tip", "Bu dosya, cihazın dosyası ile aynıdır."),
+        ("show_monitors_tip", "Monitörleri araç çubuğunda göster"),
+        ("View Mode", "Görünüm Modu"),
+        ("login_linux_tip", "X masaüstü oturumu başlatmak için uzaktaki Linux hesabına giriş yapmanız gerekiyor"),
+        ("verify_rustdesk_password_tip", "RustDesk parolasını doğrulayın"),
+        ("remember_account_tip", "Bu hesabı hatırla"),
+        ("os_account_desk_tip", "Bu hesap, uzaktaki işletim sistemine giriş yapmak ve başsız masaüstü oturumunu etkinleştirmek için kullanılır."),
+        ("OS Account", "İşletim Sistemi Hesabı"),
+        ("another_user_login_title_tip", "Başka bir kullanıcı zaten oturum açtı"),
+        ("another_user_login_text_tip", "Bağlantıyı Kapat"),
+        ("xorg_not_found_title_tip", "Xorg bulunamadı"),
+        ("xorg_not_found_text_tip", "Lütfen Xorg'u yükleyin"),
+        ("no_desktop_title_tip", "Masaüstü mevcut değil"),
+        ("no_desktop_text_tip", "Lütfen GNOME masaüstünü yükleyin"),
+        ("No need to elevate", "Yükseltmeye gerek yok"),
+        ("System Sound", "Sistem Ses"),
+        ("Default", "Varsayılan"),
+        ("New RDP", "Yeni RDP"),
+        ("Fingerprint", "Parmak İzi"),
+        ("Copy Fingerprint", "Parmak İzini Kopyala"),
+        ("no fingerprints", "parmak izi yok"),
+        ("Select a peer", "Bir cihaz seçin"),
+        ("Select peers", "Cihazları seçin"),
+        ("Plugins", "Eklentiler"),
+        ("Uninstall", "Kaldır"),
+        ("Update", "Güncelle"),
+        ("Enable", "Etkinleştir"),
+        ("Disable", "Devre Dışı Bırak"),
+        ("Options", "Seçenekler"),
+        ("resolution_original_tip", "Orijinal çözünürlük"),
+        ("resolution_fit_local_tip", "Yerel çözünürlüğe sığdır"),
+        ("resolution_custom_tip", "Özel çözünürlük"),
+        ("Collapse toolbar", "Araç çubuğunu daralt"),
+        ("Accept and Elevate", "Kabul et ve yükselt"),
+        ("accept_and_elevate_btn_tooltip", "Bağlantıyı kabul et ve UAC izinlerini yükselt."),
+        ("clipboard_wait_response_timeout_tip", "Kopyalama yanıtı için zaman aşımına uğradı."),
+        ("Incoming connection", "Gelen bağlantı"),
+        ("Outgoing connection", "Giden bağlantı"),
+        ("Exit", "Çıkış"),
+        ("Open", "Aç"),
+        ("logout_tip", "Çıkış yapmak istediğinizden emin misiniz?"),
+        ("Service", "Hizmet"),
+        ("Start", "Başlat"),
+        ("Stop", "Durdur"),
+        ("exceed_max_devices", "Yönetilen cihazların maksimum sayısına ulaştınız."),
+        ("Sync with recent sessions", "Son oturumlarla senkronize et"),
+        ("Sort tags", "Etiketleri sırala"),
+        ("Open connection in new tab", "Bağlantıyı yeni sekmede aç"),
+        ("Move tab to new window", "Sekmeyi yeni pencereye taşı"),
+        ("Can not be empty", "Boş olamaz"),
+        ("Already exists", "Zaten var"),
+        ("Change Password", "Parolayı Değiştir"),
+        ("Refresh Password", "Parolayı Yenile"),
+        ("ID", "Kimlik"),
+        ("Grid View", "Izgara Görünümü"),
+        ("List View", "Liste Görünümü"),
+        ("Select", "Seç"),
+        ("Toggle Tags", "Etiketleri Değiştir"),
+        ("pull_ab_failed_tip", "Adres defterini yenileyemedi"),
+        ("push_ab_failed_tip", "Adres defterini sunucuya senkronize edemedi"),
+        ("synced_peer_readded_tip", "Son oturumlar listesinde bulunan cihazlar adres defterine geri senkronize edilecektir."),
+        ("Change Color", "Rengi Değiştir"),
+        ("Primary Color", "Birincil Renk"),
+        ("HSV Color", "HSV Rengi"),
+        ("Installation Successful!", ""),
+        ("Installation failed!", ""),
     ].iter().cloned().collect();
 }
diff --git a/src/lang/tw.rs b/src/lang/tw.rs
index 5f5136134..49d1c08cf 100644
--- a/src/lang/tw.rs
+++ b/src/lang/tw.rs
@@ -28,7 +28,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
         ("Enable File Transfer", "啟用檔案傳輸"),
         ("Enable TCP Tunneling", "啟用 TCP 通道"),
         ("IP Whitelisting", "IP 白名單"),
-        ("ID/Relay Server", "ID / 轉送伺服器"),
+        ("ID/Relay Server", "ID / 中繼伺服器"),
         ("Import Server Config", "匯入伺服器設定"),
         ("Export Server Config", "匯出伺服器設定"),
         ("Import server configuration successfully", "匯入伺服器設定成功"),
@@ -40,27 +40,27 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
         ("Your new ID", "您的新 ID"),
         ("length %min% to %max%", "長度在 %min% 與 %max% 之間"),
         ("starts with a letter", "以字母開頭"),
-        ("allowed characters", "使用允許的字元"),
+        ("allowed characters", "允許的字元"),
         ("id_change_tip", "僅能使用以下字元:a-z、A-Z、0-9、_ (底線)。首字元必須為 a-z 或 A-Z。長度介於 6 到 16 之間。"),
         ("Website", "網站"),
         ("About", "關於"),
-        ("Slogan_tip", ""),
+        ("Slogan_tip", "在這個混沌的世界中用心製作!"),
         ("Privacy Statement", "隱私權聲明"),
         ("Mute", "靜音"),
-        ("Build Date", "構建日期"),
+        ("Build Date", "建構日期"),
         ("Version", "版本"),
         ("Home", "首頁"),
         ("Audio Input", "音訊輸入"),
         ("Enhancements", "增強功能"),
         ("Hardware Codec", "硬體編解碼器"),
-        ("Adaptive Bitrate", "自適應位元速率"),
+        ("Adaptive bitrate", "自適應位元速率"),
         ("ID Server", "ID 伺服器"),
-        ("Relay Server", "轉送伺服器"),
+        ("Relay Server", "中繼伺服器"),
         ("API Server", "API 伺服器"),
         ("invalid_http", "開頭必須為 http:// 或 https://"),
         ("Invalid IP", "IP 無效"),
         ("Invalid format", "格式無效"),
-        ("server_not_support", "伺服器暫不支持"),
+        ("server_not_support", "伺服器尚未支援"),
         ("Not available", "無法使用"),
         ("Too frequent", "修改過於頻繁,請稍後再試。"),
         ("Cancel", "取消"),
@@ -90,10 +90,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
         ("Receive", "接收"),
         ("Send", "傳送"),
         ("Refresh File", "重新整理檔案"),
-        ("Local", "本地"),
+        ("Local", "本機"),
         ("Remote", "遠端"),
         ("Remote Computer", "遠端電腦"),
-        ("Local Computer", "本地電腦"),
+        ("Local Computer", "本機電腦"),
         ("Confirm Delete", "確認刪除"),
         ("Delete", "刪除"),
         ("Properties", "屬性"),
@@ -139,39 +139,39 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
         ("Remote desktop is offline", "遠端桌面已離線"),
         ("Key mismatch", "金鑰不符"),
         ("Timeout", "逾時"),
-        ("Failed to connect to relay server", "無法連線到轉送伺服器"),
+        ("Failed to connect to relay server", "無法連線到中繼伺服器"),
         ("Failed to connect via rendezvous server", "無法透過 rendezvous 伺服器連線"),
-        ("Failed to connect via relay server", "無法透過轉送伺服器連線"),
+        ("Failed to connect via relay server", "無法透過中繼伺服器連線"),
         ("Failed to make direct connection to remote desktop", "無法直接連線到遠端桌面"),
         ("Set Password", "設定密碼"),
         ("OS Password", "作業系統密碼"),
-        ("install_tip", "UAC 會導致 RustDesk 在某些情況下無法正常以遠端電腦執行。若要避開 UAC,請點擊下方按鈕將 RustDesk 安裝到系統中。"),
-        ("Click to upgrade", "點擊以升級"),
-        ("Click to download", "點擊以下載"),
-        ("Click to update", "點擊以更新"),
+        ("install_tip", "UAC 會導致 RustDesk 在某些情況下無法正常作為遠端端點運作。若要避開 UAC,請點選下方按鈕將 RustDesk 安裝到系統中。"),
+        ("Click to upgrade", "點選以升級"),
+        ("Click to download", "點選以下載"),
+        ("Click to update", "點選以更新"),
         ("Configure", "設定"),
-        ("config_acc", "您需要授予 RustDesk「協助工具」權限才能存取遠端電腦。"),
-        ("config_screen", "您需要授予 RustDesk「畫面錄製」權限才能存取遠端電腦。"),
+        ("config_acc", "為了遠端控制您的桌面,您需要授予 RustDesk「協助工具」權限。"),
+        ("config_screen", "為了遠端存取您的桌面,您需要授予 RustDesk「螢幕錄製」權限。"),
         ("Installing ...", "正在安裝 ..."),
         ("Install", "安裝"),
         ("Installation", "安裝"),
         ("Installation Path", "安裝路徑"),
         ("Create start menu shortcuts", "新增開始功能表捷徑"),
         ("Create desktop icon", "新增桌面捷徑"),
-        ("agreement_tip", "開始安裝即表示接受許可協議"),
+        ("agreement_tip", "開始安裝即表示您接受授權條款。"),
         ("Accept and Install", "接受並安裝"),
-        ("End-user license agreement", "使用者授權合約"),
+        ("End-user license agreement", "終端使用者授權合約"),
         ("Generating ...", "正在產生 ..."),
         ("Your installation is lower version.", "您安裝的版本過舊。"),
-        ("not_close_tcp_tip", "使用通道時請不要關閉此視窗"),
+        ("not_close_tcp_tip", "在使用通道時請不要關閉此視窗"),
         ("Listening ...", "正在等待通道連線 ..."),
         ("Remote Host", "遠端主機"),
-        ("Remote Port", "遠端連線端口"),
+        ("Remote Port", "遠端連接埠"),
         ("Action", "操作"),
         ("Add", "新增"),
-        ("Local Port", "本機連線端口"),
+        ("Local Port", "本機連接埠"),
         ("Local Address", "本機地址"),
-        ("Change Local Port", "修改本機連線端口"),
+        ("Change Local Port", "修改本機連接埠"),
         ("setup_server_tip", "若您需要更快的連線速度,可以選擇自行建立伺服器"),
         ("Too short, at least 6 characters.", "過短,至少需要 6 個字元。"),
         ("The confirmation is not identical.", "兩次輸入不相符"),
@@ -185,15 +185,15 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
         ("Allow file copy and paste", "允許檔案複製和貼上"),
         ("Connected", "已連線"),
         ("Direct and encrypted connection", "加密直接連線"),
-        ("Relayed and encrypted connection", "加密轉送連線"),
-        ("Direct and unencrypted connection", "未加密直接連線"),
-        ("Relayed and unencrypted connection", "未加密轉送連線"),
+        ("Relayed and encrypted connection", "加密中繼連線"),
+        ("Direct and unencrypted connection", "直接且未加密的連線"),
+        ("Relayed and unencrypted connection", "中繼且未加密的連線"),
         ("Enter Remote ID", "輸入遠端 ID"),
         ("Enter your password", "輸入您的密碼"),
         ("Logging in...", "正在登入 ..."),
         ("Enable RDP session sharing", "啟用 RDP 工作階段共享"),
-        ("Auto Login", "自動登入 (鎖定將在設定關閉後套用)"),
-        ("Enable Direct IP Access", "允許 IP 直接存取"),
+        ("Auto Login", "自動登入 (只在您設定「工作階段結束後鎖定」時有效)"),
+        ("Enable Direct IP Access", "啟用 IP 直接存取"),
         ("Rename", "重新命名"),
         ("Space", "空白"),
         ("Create Desktop Shortcut", "新增桌面捷徑"),
@@ -204,49 +204,50 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
         ("Warning", "警告"),
         ("Login screen using Wayland is not supported", "不支援使用 Wayland 的登入畫面"),
         ("Reboot required", "需要重新啟動"),
-        ("Unsupported display server", "不支援顯示伺服器"),
-        ("x11 expected", "預期 x11"),
-        ("Port", "端口"),
+        ("Unsupported display server", "不支援的顯示伺服器"),
+        ("x11 expected", "預期為 x11"),
+        ("Port", "連接埠"),
         ("Settings", "設定"),
         ("Username", "使用者名稱"),
-        ("Invalid port", "連線端口無效"),
-        ("Closed manually by the peer", "遠端使用者關閉了工作階段"),
+        ("Invalid port", "連接埠無效"),
+        ("Closed manually by the peer", "對方關閉了工作階段"),
         ("Enable remote configuration modification", "允許遠端使用者更改設定"),
         ("Run without install", "跳過安裝直接執行"),
         ("Connect via relay", "中繼連線"),
-        ("Always connect via relay", "一律透過轉送連線"),
-        ("whitelist_tip", "只有白名單中的 IP 可以存取"),
+        ("Always connect via relay", "一律透過中繼連線"),
+        ("whitelist_tip", "只有白名單上的 IP 可以存取"),
         ("Login", "登入"),
         ("Verify", "驗證"),
         ("Remember me", "記住我"),
         ("Trust this device", "信任此裝置"),
         ("Verification code", "驗證碼"),
-        ("verification_tip", "已向註冊電子信箱發送了登入驗證碼,請輸入驗證碼以繼續登入"),
+        ("verification_tip", "驗證碼已發送到註冊的電子郵件地址,請輸入驗證碼以繼續登入。"),
         ("Logout", "登出"),
         ("Tags", "標籤"),
         ("Search ID", "搜尋 ID"),
-        ("whitelist_sep", "使用逗號、分號、空白,或是換行來分隔"),
+        ("whitelist_sep", "使用逗號、分號、空格,或是換行來分隔"),
         ("Add ID", "新增 ID"),
         ("Add Tag", "新增標籤"),
         ("Unselect all tags", "取消選取所有標籤"),
         ("Network error", "網路錯誤"),
         ("Username missed", "缺少使用者名稱"),
         ("Password missed", "缺少密碼"),
-        ("Wrong credentials", "提供的登入資訊有誤"),
+        ("Wrong credentials", "登入資訊錯誤"),
+        ("The verification code is incorrect or has expired", "驗證碼錯誤或已過期"),
         ("Edit Tag", "編輯標籤"),
-        ("Unremember Password", "忘掉密碼"),
+        ("Unremember Password", "忘記密碼"),
         ("Favorites", "我的最愛"),
-        ("Add to Favorites", "新增到我的最愛"),
-        ("Remove from Favorites", "從我的最愛中刪除"),
+        ("Add to Favorites", "加入我的最愛"),
+        ("Remove from Favorites", "從我的最愛中移除"),
         ("Empty", "空空如也"),
         ("Invalid folder name", "資料夾名稱無效"),
-        ("Socks5 Proxy", "Socks5 代理"),
+        ("Socks5 Proxy", "Socks5 代理伺服器"),
         ("Hostname", "主機名稱"),
         ("Discovered", "已探索"),
-        ("install_daemon_tip", "為了能夠開機時自動啟動,請先安裝系統服務。"),
+        ("install_daemon_tip", "若要在開機時啟動,您需要安裝系統服務。"),
         ("Remote ID", "遠端 ID"),
         ("Paste", "貼上"),
-        ("Paste here?", "貼上到這裡?"),
+        ("Paste here?", "在此貼上?"),
         ("Are you sure to close the connection?", "您確定要關閉連線嗎?"),
         ("Download new version", "下載新版本"),
         ("Touch mode", "觸控模式"),
@@ -257,8 +258,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
         ("Two-Finger Tap", "雙指輕觸"),
         ("Right Mouse", "滑鼠右鍵"),
         ("One-Finger Move", "單指移動"),
-        ("Double Tap & Move", "雙擊並移動"),
-        ("Mouse Drag", "滑鼠選中拖動"),
+        ("Double Tap & Move", "點兩下並移動"),
+        ("Mouse Drag", "滑鼠拖曳"),
         ("Three-Finger vertically", "三指垂直滑動"),
         ("Mouse Wheel", "滑鼠滾輪"),
         ("Two-Finger Move", "雙指移動"),
@@ -269,8 +270,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
         ("No permission of file transfer", "沒有檔案傳輸權限"),
         ("Note", "備註"),
         ("Connection", "連線"),
-        ("Share Screen", "共享螢幕畫面"),
-        ("Chat", "聊天訊息"),
+        ("Share Screen", "螢幕分享"),
+        ("Chat", "聊天"),
         ("Total", "總計"),
         ("items", "個項目"),
         ("Selected", "已選擇"),
@@ -281,39 +282,39 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
         ("Screen Connection", "畫面連線"),
         ("Do you accept?", "是否接受?"),
         ("Open System Setting", "開啟系統設定"),
-        ("How to get Android input permission?", "如何獲取 Android 的輸入權限?"),
-        ("android_input_permission_tip1", "取得輸入權限後可以讓遠端裝置透過滑鼠控制此 Android 裝置"),
-        ("android_input_permission_tip2", "請在接下來的系統設定頁面中,找到並進入「已安裝的服務」頁面,並將「RustDesk Input」服務開啟"),
-        ("android_new_connection_tip", "收到新的連線控制請求,對方想要控制您目前的裝置"),
-        ("android_service_will_start_tip", "開啟畫面錄製權限將自動開啟服務,允許其他裝置向此裝置請求建立連線。"),
+        ("How to get Android input permission?", "如何取得 Android 的輸入權限?"),
+        ("android_input_permission_tip1", "為了讓遠端裝置能夠透過滑鼠或觸控控制您的 Android 裝置,您需要允許 RustDesk 使用「輔助功能」服務。"),
+        ("android_input_permission_tip2", "請前往下一個系統設定頁面,找到並進入「已安裝的服務」,開啟「RustDesk Input」服務。"),
+        ("android_new_connection_tip", "收到新的控制請求,對方想要控制您目前的裝置。"),
+        ("android_service_will_start_tip", "開啟「畫面錄製」將自動啟動服務,允許其他裝置向您的裝置請求連線。"),
         ("android_stop_service_tip", "關閉服務將自動關閉所有已建立的連線。"),
-        ("android_version_audio_tip", "目前的 Android 版本不支援音訊錄製,請升級到 Android 10 或以上版本。"),
-        ("android_start_service_tip", "點擊「啟動服務」或啟用「螢幕錄製」權限,以啟動螢幕共享服務。"),
-        ("android_permission_may_not_change_tip", "對於已經建立的連線,權限可能不會立即發生改變,除非重新建立連線。"),
+        ("android_version_audio_tip", "目前的 Android 版本不支援音訊錄製,請升級至 Android 10 或更新的版本。"),
+        ("android_start_service_tip", "點選「啟動服務」或啟用「畫面錄製」權限以啟動螢幕分享服務。"),
+        ("android_permission_may_not_change_tip", "已建立連線的權限可能不會立即改變,除非重新連線。"),
         ("Account", "帳號"),
         ("Overwrite", "取代"),
         ("This file exists, skip or overwrite this file?", "此檔案/資料夾已存在,要略過或是取代此檔案嗎?"),
         ("Quit", "退出"),
         ("doc_mac_permission", "https://rustdesk.com/docs/zh-tw/manual/mac/#啟用權限"),
-        ("Help", "幫助"),
+        ("Help", "說明"),
         ("Failed", "失敗"),
         ("Succeeded", "成功"),
-        ("Someone turns on privacy mode, exit", "其他使用者開啟隱私模式,退出"),
+        ("Someone turns on privacy mode, exit", "有人開啟了隱私模式,退出"),
         ("Unsupported", "不支援"),
-        ("Peer denied", "被控端拒絕"),
-        ("Please install plugins", "請安裝插件"),
-        ("Peer exit", "被控端退出"),
-        ("Failed to turn off", "退出失敗"),
-        ("Turned off", "退出"),
+        ("Peer denied", "對方拒絕"),
+        ("Please install plugins", "請安裝外掛程式"),
+        ("Peer exit", "對方退出"),
+        ("Failed to turn off", "關閉失敗"),
+        ("Turned off", "已關閉"),
         ("In privacy mode", "開啟隱私模式"),
         ("Out privacy mode", "退出隱私模式"),
         ("Language", "語言"),
         ("Keep RustDesk background service", "保持 RustDesk 後台服務"),
         ("Ignore Battery Optimizations", "忽略電池最佳化"),
-        ("android_open_battery_optimizations_tip", "如需關閉此功能,請在接下來的 RustDesk 應用設定頁面中,找到並進入 [電源] 頁面,取消勾選 [不受限制]"),
-        ("Start on Boot", "開機自動啟動"),
-        ("Start the screen sharing service on boot, requires special permissions", "開機自動啟動螢幕共享服務,此功能需要一些特殊權限。"),
-        ("Connection not allowed", "對方不允許連線"),
+        ("android_open_battery_optimizations_tip", "如果您想要停用此功能,請前往下一個 RustDesk 應用程式設定頁面,找到並進入「電池」,取消勾選「不受限制」"),
+        ("Start on Boot", "開機時啟動"),
+        ("Start the screen sharing service on boot, requires special permissions", "開機時啟動螢幕分享服務,需要特殊權限。"),
+        ("Connection not allowed", "不允許連線"),
         ("Legacy mode", "傳統模式"),
         ("Map mode", "1:1 傳輸模式"),
         ("Translate mode", "翻譯模式"),
@@ -322,10 +323,10 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
         ("Set permanent password", "設定固定密碼"),
         ("Enable Remote Restart", "啟用遠端重新啟動"),
         ("Allow remote restart", "允許遠端重新啟動"),
-        ("Restart Remote Device", "重新啟動遠端電腦"),
-        ("Are you sure you want to restart", "確定要重新啟動"),
+        ("Restart Remote Device", "重新啟動遠端裝置"),
+        ("Are you sure you want to restart", "確定要重新啟動嗎?"),
         ("Restarting Remote Device", "正在重新啟動遠端裝置"),
-        ("remote_restarting_tip", "遠端裝置正在重新啟動,請關閉當前提示框,並在一段時間後使用永久密碼重新連線"),
+        ("remote_restarting_tip", "遠端裝置正在重新啟動,請關閉此對話框,並在一段時間後使用永久密碼重新連線"),
         ("Copied", "已複製"),
         ("Exit Fullscreen", "退出全螢幕"),
         ("Fullscreen", "全螢幕"),
@@ -336,15 +337,15 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
         ("Ratio", "比例"),
         ("Image Quality", "畫質"),
         ("Scroll Style", "滾動樣式"),
-        ("Show Toolbar", ""),
-        ("Hide Toolbar", ""),
+        ("Show Toolbar", "顯示工具列"),
+        ("Hide Toolbar", "隱藏工具列"),
         ("Direct Connection", "直接連線"),
         ("Relay Connection", "中繼連線"),
         ("Secure Connection", "安全連線"),
         ("Insecure Connection", "非安全連線"),
         ("Scale original", "原始尺寸"),
         ("Scale adaptive", "適應視窗"),
-        ("General", "通用"),
+        ("General", "一般"),
         ("Security", "安全"),
         ("Theme", "主題"),
         ("Dark Theme", "黑暗主題"),
@@ -352,22 +353,22 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
         ("Dark", "黑暗"),
         ("Light", "明亮"),
         ("Follow System", "跟隨系統"),
-        ("Enable hardware codec", "使用硬體編解碼器"),
+        ("Enable hardware codec", "啟用硬體編解碼器"),
         ("Unlock Security Settings", "解鎖安全設定"),
-        ("Enable Audio", "允許傳輸音訊"),
+        ("Enable Audio", "啟用音訊"),
         ("Unlock Network Settings", "解鎖網路設定"),
         ("Server", "伺服器"),
         ("Direct IP Access", "IP 直接連線"),
-        ("Proxy", "代理"),
-        ("Apply", "應用"),
+        ("Proxy", "代理伺服器"),
+        ("Apply", "套用"),
         ("Disconnect all devices?", "中斷所有遠端連線?"),
         ("Clear", "清空"),
         ("Audio Input Device", "音訊輸入裝置"),
         ("Use IP Whitelisting", "只允許白名單上的 IP 進行連線"),
         ("Network", "網路"),
-        ("Enable RDP", "允許 RDP 訪問"),
-        ("Pin Toolbar", ""),
-        ("Unpin Toolbar", ""),
+        ("Enable RDP", "允許 RDP 存取"),
+        ("Pin Toolbar", "釘選工具列"),
+        ("Unpin Toolbar", "取消釘選工具列"),
         ("Recording", "錄製"),
         ("Directory", "路徑"),
         ("Automatically record incoming sessions", "自動錄製連入的工作階段"),
@@ -381,17 +382,17 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
         ("Write a message", "輸入聊天訊息"),
         ("Prompt", "提示"),
         ("Please wait for confirmation of UAC...", "請等待對方確認 UAC ..."),
-        ("elevated_foreground_window_tip", "目前的遠端桌面視窗需要更高的權限才能繼續操作,暫時無法使用滑鼠、鍵盤,可以請求對方最小化目前視窗,或者在連線管理視窗點擊提升權限。為了避免這個問題,建議在遠端裝置上安裝本軟體。"),
+        ("elevated_foreground_window_tip", "目前的遠端桌面視窗需要更高的權限才能繼續操作,暫時無法使用滑鼠、鍵盤,可以請求對方最小化目前視窗,或者在連線管理視窗點選提升權限。為了避免這個問題,建議在遠端裝置上安裝本軟體。"),
         ("Disconnected", "斷開連線"),
         ("Other", "其他"),
         ("Confirm before closing multiple tabs", "關閉多個分頁前詢問我"),
         ("Keyboard Settings", "鍵盤設定"),
-        ("Full Access", "完全訪問"),
+        ("Full Access", "完全存取"),
         ("Screen Share", "僅分享螢幕畫面"),
-        ("Wayland requires Ubuntu 21.04 or higher version.", "Wayland 需要 Ubuntu 21.04 或更高版本。"),
-        ("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "Wayland 需要更高版本的 Linux 發行版。請嘗試使用 X11 桌面或更改您的作業系統。"),
+        ("Wayland requires Ubuntu 21.04 or higher version.", "Wayland 需要 Ubuntu 21.04 或更新的版本。"),
+        ("Wayland requires higher version of linux distro. Please try X11 desktop or change your OS.", "Wayland 需要更新的版的 Linux 發行版。請嘗試使用 X11 桌面或更改您的作業系統。"),
         ("JumpLink", "查看"),
-        ("Please Select the screen to be shared(Operate on the peer side).", "請選擇要分享的螢幕畫面(在對端操作)。"),
+        ("Please Select the screen to be shared(Operate on the peer side).", "請選擇要分享的螢幕畫面(在對方的裝置上操作)。"),
         ("Show RustDesk", "顯示 RustDesk"),
         ("This PC", "此電腦"),
         ("or", "或"),
@@ -399,25 +400,25 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
         ("Elevate", "提升權限"),
         ("Zoom cursor", "縮放游標"),
         ("Accept sessions via password", "只允許透過輸入密碼進行連線"),
-        ("Accept sessions via click", "只允許透過點擊接受進行連線"),
-        ("Accept sessions via both", "允許輸入密碼或點擊接受進行連線"),
+        ("Accept sessions via click", "只允許透過點選接受進行連線"),
+        ("Accept sessions via both", "允許輸入密碼或點選接受進行連線"),
         ("Please wait for the remote side to accept your session request...", "請等待對方接受您的連線請求 ..."),
         ("One-time Password", "一次性密碼"),
         ("Use one-time password", "使用一次性密碼"),
         ("One-time password length", "一次性密碼長度"),
-        ("Request access to your device", "請求訪問您的裝置"),
+        ("Request access to your device", "請求存取您的裝置"),
         ("Hide connection management window", "隱藏連線管理視窗"),
         ("hide_cm_tip", "在只允許密碼連線並且只用固定密碼的情況下才允許隱藏"),
-        ("wayland_experiment_tip", "Wayland 支援處於實驗階段,如果您需要使用無人值守訪問,請使用 X11。"),
+        ("wayland_experiment_tip", "Wayland 支援處於實驗階段,如果您需要使用無人值守存取,請使用 X11。"),
         ("Right click to select tabs", "右鍵選擇分頁"),
-        ("Skipped", "已略過"),
+        ("Skipped", "已跳過"),
         ("Add to Address Book", "新增到通訊錄"),
         ("Group", "群組"),
         ("Search", "搜尋"),
         ("Closed manually by web console", "被 Web 控制台手動關閉"),
-        ("Local keyboard type", "本地鍵盤類型"),
-        ("Select local keyboard type", "請選擇本地鍵盤類型"),
-        ("software_render_tip", "如果您使用 NVIDIA 顯示卡,並且遠端視窗在建立連線後會立刻關閉,那麼請安裝 nouveau 顯示卡驅動程式並且選擇使用軟體渲染可能會有幫助。重新啟動軟體後生效。"),
+        ("Local keyboard type", "本機鍵盤類型"),
+        ("Select local keyboard type", "請選擇本機鍵盤類型"),
+        ("software_render_tip", "如果您使用 Nvidia 顯示卡,並且遠端視窗在建立連線後會立刻關閉,那麼請安裝 nouveau 顯示卡驅動程式並且選擇使用軟體渲染可能會有幫助。重新啟動軟體後生效。"),
         ("Always use software rendering", "使用軟體渲染"),
         ("config_input", "為了能夠透過鍵盤控制遠端桌面,請給予 RustDesk \"輸入監控\" 權限。"),
         ("config_microphone", "為了支援透過麥克風進行音訊傳輸,請給予 RustDesk \"錄音\"權限。"),
@@ -427,7 +428,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
         ("Ask the remote user for authentication", "請求遠端使用者進行身分驗證"),
         ("Choose this if the remote account is administrator", "當遠端使用者帳戶是管理員時,請選擇此選項"),
         ("Transmit the username and password of administrator", "發送管理員的使用者名稱和密碼"),
-        ("still_click_uac_tip", "依然需要遠端使用者在 UAC 視窗點擊確認。"),
+        ("still_click_uac_tip", "依然需要遠端使用者在 UAC 視窗點選確認。"),
         ("Request Elevation", "請求權限提升"),
         ("wait_accept_uac_tip", "請等待遠端使用者確認 UAC 對話框。"),
         ("Elevate successfully", "權限提升成功"),
@@ -453,14 +454,14 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
         ("Voice call", "語音通話"),
         ("Text chat", "文字聊天"),
         ("Stop voice call", "停止語音通話"),
-        ("relay_hint_tip", "可能無法直接連線,可以嘗試中繼連線。\n另外,如果想要直接使用中繼連線,可以在 ID 後面新增/r,或者在卡片選項裡選擇強制走中繼連線。"),
+        ("relay_hint_tip", "可能無法直接連線,可以嘗試中繼連線。\n另外,如果想要直接使用中繼連線,可以在 ID 後面新增/r,如果近期工作階段裏存在該分頁,也可以在分頁選項裡選擇強制走中繼連線。"),
         ("Reconnect", "重新連線"),
         ("Codec", "編解碼器"),
         ("Resolution", "解析度"),
         ("No transfers in progress", "沒有正在進行的傳輸"),
         ("Set one-time password length", "設定一次性密碼長度"),
-        ("idd_driver_tip", "安裝虛擬顯示器驅動程式,以便在沒有連接顯示器的情況下啟動虛擬顯示器進行控制。"),
-        ("confirm_idd_driver_tip", "安裝虛擬顯示器驅動程式的選項已勾選。請注意,測試證書將被安裝以信任虛擬顯示器驅動。測試證書僅會用於信任 RustDesk 的驅動程式。"),
+        ("install_cert_tip", ""),
+        ("confirm_install_cert_tip", ""),
         ("RDP Settings", "RDP 設定"),
         ("Sort by", "排序方式"),
         ("New Connection", "新連線"),
@@ -468,15 +469,15 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
         ("Minimize", "最小化"),
         ("Maximize", "最大化"),
         ("Your Device", "您的裝置"),
-        ("empty_recent_tip", "空空如也"),
+        ("empty_recent_tip", "哎呀,沒有近期的工作階段!\n是時候安排點新工作了。"),
         ("empty_favorite_tip", "空空如也"),
-        ("empty_lan_tip", "空空如也"),
-        ("empty_address_book_tip", "空空如也"),
+        ("empty_lan_tip", "喔不,看來我們目前找不到任何夥伴。"),
+        ("empty_address_book_tip", "老天,看來您的通訊錄中沒有任何夥伴。"),
         ("eg: admin", "例如:admin"),
         ("Empty Username", "空使用者帳號"),
         ("Empty Password", "空密碼"),
         ("Me", "我"),
-        ("identical_file_tip", "此檔案與對方的檔案一致"),
+        ("identical_file_tip", "此檔案與對方的檔案一致。"),
         ("show_monitors_tip", "在工具列中顯示顯示器"),
         ("View Mode", "瀏覽模式"),
         ("login_linux_tip", "需要登入到遠端 Linux 使用者帳戶才能啟用 X 介面"),
@@ -486,7 +487,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
         ("OS Account", "作業系統使用者帳戶"),
         ("another_user_login_title_tip", "另一個使用者已經登入"),
         ("another_user_login_text_tip", "斷開連線"),
-        ("xorg_not_found_title_tip", "未找到 Xorg"),
+        ("xorg_not_found_title_tip", "找不到 Xorg"),
         ("xorg_not_found_text_tip", "請安裝 Xorg"),
         ("no_desktop_title_tip", "沒有可用的桌面"),
         ("no_desktop_text_tip", "請安裝 GNOME 桌面"),
@@ -497,20 +498,50 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
         ("Fingerprint", "指紋"),
         ("Copy Fingerprint", "複製指紋"),
         ("no fingerprints", "沒有指紋"),
-        ("Select a peer", ""),
-        ("Select peers", ""),
-        ("Plugins", ""),
-        ("Uninstall", ""),
-        ("Update", ""),
-        ("Enable", ""),
-        ("Disable", ""),
-        ("Options", ""),
-        ("resolution_original_tip", ""),
-        ("resolution_fit_local_tip", ""),
-        ("resolution_custom_tip", ""),
-        ("Collapse toolbar", ""),
-        ("Accept and Elevate", ""),
-        ("accept_and_elevate_btn_tooltip", ""),
-        ("clipboard_wait_response_timeout_tip", ""),
+        ("Select a peer", "選擇夥伴"),
+        ("Select peers", "選擇夥伴"),
+        ("Plugins", "外掛程式"),
+        ("Uninstall", "解除安裝"),
+        ("Update", "更新"),
+        ("Enable", "啟用"),
+        ("Disable", "停用"),
+        ("Options", "選項"),
+        ("resolution_original_tip", "原始解析度"),
+        ("resolution_fit_local_tip", "調整成本機解析度"),
+        ("resolution_custom_tip", "自動解析度"),
+        ("Collapse toolbar", "收回工具列"),
+        ("Accept and Elevate", "接受並提升"),
+        ("accept_and_elevate_btn_tooltip", "接受連線並提升 UAC 權限。"),
+        ("clipboard_wait_response_timeout_tip", "等待複製回應逾時。"),
+        ("Incoming connection", "接收的連線"),
+        ("Outgoing connection", "發起的連線"),
+        ("Exit", "退出"),
+        ("Open", "開啟"),
+        ("logout_tip", "確定要登出嗎?"),
+        ("Service", "服務"),
+        ("Start", "啟動"),
+        ("Stop", "停止"),
+        ("exceed_max_devices", "超過最大裝置數量"),
+        ("Sync with recent sessions", "與近期工作階段同步"),
+        ("Sort tags", "排序標籤"),
+        ("Open connection in new tab", "在新分頁開啟連線"),
+        ("Move tab to new window", "移動標籤到新視窗"),
+        ("Can not be empty", "不能為空"),
+        ("Already exists", "已經存在"),
+        ("Change Password", "更改密碼"),
+        ("Refresh Password", "重新整理密碼"),
+        ("ID", "ID"),
+        ("Grid View", "網格檢視"),
+        ("List View", "清單檢視"),
+        ("Select", "選擇"),
+        ("Toggle Tags", "切換標籤"),
+        ("pull_ab_failed_tip", "通訊錄更新失敗"),
+        ("push_ab_failed_tip", "成功同步通訊錄至伺服器"),
+        ("synced_peer_readded_tip", "最近會話中存在的設備將會被重新同步到通訊錄。"),
+        ("Change Color", "更改顏色"),
+        ("Primary Color", "基本色"),
+        ("HSV Color", "HSV 色"),
+        ("Installation Successful!", ""),
+        ("Installation failed!", ""),
     ].iter().cloned().collect();
 }
diff --git a/src/lang/ua.rs b/src/lang/ua.rs
index 135b3b357..b398afab5 100644
--- a/src/lang/ua.rs
+++ b/src/lang/ua.rs
@@ -53,11 +53,11 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
         ("Audio Input", "Аудіовхід"),
         ("Enhancements", "Покращення"),
         ("Hardware Codec", "Апаратний кодек"),
-        ("Adaptive Bitrate", "Адаптивна швидкість потоку"),
+        ("Adaptive bitrate", "Адаптивна швидкість потоку"),
         ("ID Server", "ID-сервер"),
         ("Relay Server", "Сервер ретрансляції"),
         ("API Server", "API-сервер"),
-        ("invalid_http", "Повиннна починатися з http:// або https://"),
+        ("invalid_http", "Повинна починатися з http:// або https://"),
         ("Invalid IP", "Неправильна IP-адреса"),
         ("Invalid format", "Неправильний формат"),
         ("server_not_support", "Наразі не підтримується сервером"),
@@ -121,7 +121,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
         ("Shrink", "Зменшити"),
         ("Stretch", "Розтягнути"),
         ("Scrollbar", "Смуга прокрутки"),
-        ("ScrollAuto", "Прокрутка Авто"),
+        ("ScrollAuto", "Автоматична прокрутка"),
         ("Good image quality", "Хороша якість зображення"),
         ("Balanced", "Збалансована"),
         ("Optimize reaction time", "Оптимізувати час реакції"),
@@ -178,7 +178,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
         ("Permissions", "Дозволи"),
         ("Accept", "Прийняти"),
         ("Dismiss", "Відхилити"),
-        ("Disconnect", "Відключити"),
+        ("Disconnect", "Відʼєднати"),
         ("Allow using keyboard and mouse", "Дозволити використання клавіатури та миші"),
         ("Allow using clipboard", "Дозволити використання буфера обміну"),
         ("Allow hearing sound", "Дозволити передачу звуку"),
@@ -233,6 +233,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
         ("Username missed", "Імʼя користувача відсутнє"),
         ("Password missed", "Забули пароль"),
         ("Wrong credentials", "Неправильні дані"),
+        ("The verification code is incorrect or has expired", "Код підтвердження некоректний або протермінований"),
         ("Edit Tag", "Редагувати тег"),
         ("Unremember Password", "Не зберігати пароль"),
         ("Favorites", "Вибране"),
@@ -288,7 +289,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
         ("android_service_will_start_tip", "Увімкнення захоплення екрана автоматично запускає службу, дозволяючи іншим пристроям запитувати підключення до вашого пристрою."),
         ("android_stop_service_tip", "Зупинка служби автоматично завершить всі встановлені зʼєднання."),
         ("android_version_audio_tip", "Поточна версія Android не підтримує захоплення звуку, оновіть її до Android 10 або вище."),
-        ("android_start_service_tip", "Натисніть [Запустити службу] або увімкніть дозвіл на [Захоплення екрана], шоб запустити службу спільного доступу до екрана."),
+        ("android_start_service_tip", "Натисніть [Запустити службу] або увімкніть дозвіл на [Захоплення екрана], щоб запустити службу спільного доступу до екрана."),
         ("android_permission_may_not_change_tip", "Дозволи для встановлених зʼєднань можуть не змінитися миттєво аж до перепідключення."),
         ("Account", "Акаунт"),
         ("Overwrite", "Перезаписати"),
@@ -302,7 +303,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
         ("Unsupported", "Не підтримується"),
         ("Peer denied", "Відхилено віддаленим пристроєм"),
         ("Please install plugins", "Будь ласка, встановіть плагіни"),
-        ("Peer exit", "Відключення віддаленого пристрою"),
+        ("Peer exit", "Вийти з віддаленого пристрою"),
         ("Failed to turn off", "Не вдалося вимкнути"),
         ("Turned off", "Вимкнений"),
         ("In privacy mode", "У режимі конфіденційності"),
@@ -325,19 +326,19 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
         ("Restart Remote Device", "Перезапустити віддалений пристрій"),
         ("Are you sure you want to restart", "Ви впевнені, що хочете виконати перезапуск?"),
         ("Restarting Remote Device", "Перезавантаження віддаленого пристрою"),
-        ("remote_restarting_tip", "Віддалений пристрій перезапускається. Будь ласка, закрийте це повідомлення та через деякий час перепідключіться, використовуючи постійний пароль."),
+        ("remote_restarting_tip", "Віддалений пристрій перезапускається. Будь ласка, закрийте це повідомлення та через деякий час перепідʼєднайтесь, використовуючи постійний пароль."),
         ("Copied", "Скопійовано"),
         ("Exit Fullscreen", "Вийти з повноекранного режиму"),
         ("Fullscreen", "Повноекранний"),
         ("Mobile Actions", "Мобільні дії"),
         ("Select Monitor", "Виберіть монітор"),
         ("Control Actions", "Дії для керування"),
-        ("Display Settings", "Налаштування відображення"),
+        ("Display Settings", "Налаштування дисплею"),
         ("Ratio", "Співвідношення"),
         ("Image Quality", "Якість зображення"),
         ("Scroll Style", "Стиль прокрутки"),
-        ("Show Toolbar", ""),
-        ("Hide Toolbar", ""),
+        ("Show Toolbar", "Показати панель інструментів"),
+        ("Hide Toolbar", "Приховати панель інструментів"),
         ("Direct Connection", "Пряме підключення"),
         ("Relay Connection", "Релейне підключення"),
         ("Secure Connection", "Безпечне підключення"),
@@ -360,14 +361,14 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
         ("Direct IP Access", "Прямий IP доступ"),
         ("Proxy", "Проксі"),
         ("Apply", "Застосувати"),
-        ("Disconnect all devices?", "Відключити всі прилади?"),
+        ("Disconnect all devices?", "Відʼєднати всі прилади?"),
         ("Clear", "Очистити"),
         ("Audio Input Device", "Пристрій введення звуку"),
         ("Use IP Whitelisting", "Використовувати білий список IP"),
         ("Network", "Мережа"),
         ("Enable RDP", "Увімкнути RDP"),
-        ("Pin Toolbar", ""),
-        ("Unpin Toolbar", ""),
+        ("Pin Toolbar", "Закріпити панель інструментів"),
+        ("Unpin Toolbar", "Відкріпити панель інструментів"),
         ("Recording", "Запис"),
         ("Directory", "Директорія"),
         ("Automatically record incoming sessions", "Автоматично записувати вхідні сеанси"),
@@ -382,7 +383,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
         ("Prompt", "Підказка"),
         ("Please wait for confirmation of UAC...", "Будь ласка, зачекайте підтвердження UAC..."),
         ("elevated_foreground_window_tip", "Поточне вікно віддаленої стільниці потребує розширених прав для роботи, тому наразі неможливо використати мишу та клавіатуру. Ви можете запропонувати віддаленому користувачу згорнути поточне вікно чи натиснути кнопку розширення прав у вікні керування підключеннями. Для уникнення цієї проблеми, рекомендується встановити програму на віддаленому пристрої"),
-        ("Disconnected", "Відключено"),
+        ("Disconnected", "Відʼєднано"),
         ("Other", "Інше"),
         ("Confirm before closing multiple tabs", "Підтверджувати перед закриттям кількох вкладок"),
         ("Keyboard Settings", "Налаштування клавіатури"),
@@ -440,7 +441,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
         ("Medium", "Середній"),
         ("Strong", "Сильний"),
         ("Switch Sides", "Поміняти місцями"),
-        ("Please confirm if you want to share your desktop?", "Будь ласка, пітвердіть дозвіл на спільне використання стільниці"),
+        ("Please confirm if you want to share your desktop?", "Будь ласка, підтвердіть дозвіл на спільне використання стільниці"),
         ("Display", "Екран"),
         ("Default View Style", "Типовий стиль перегляду"),
         ("Default Scroll Style", "Типовий стиль гортання"),
@@ -450,17 +451,17 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
         ("FPS", "FPS"),
         ("Auto", "Авто"),
         ("Other Default Options", "Інші типові параметри"),
-        ("Voice call", "Голосовий дзвінок"),
+        ("Voice call", "Голосовий виклик"),
         ("Text chat", "Текстовий чат"),
-        ("Stop voice call", "Покласти слухавку"),
-        ("relay_hint_tip", "Якщо відсутня можливості підключитись напряму, ви можете спробувати підключення по реле. \nТакож, якщо ви хочете відразу використовувати реле, можна додати суфікс \"/r\" до ID, або ж вибрати опцію \"Завжди підключатися через реле\" в картці вузла."),
+        ("Stop voice call", "Завершити голосовий виклик"),
+        ("relay_hint_tip", "Якщо відсутня можливості підключитись напряму, ви можете спробувати підключення по реле. \nТакож, якщо ви хочете відразу використовувати реле, можна додати суфікс \"/r\" до ID, або ж вибрати опцію \"Завжди підключатися через реле\" в картці нещодавніх сеансів."),
         ("Reconnect", "Перепідключитися"),
         ("Codec", "Кодек"),
         ("Resolution", "Роздільна здатність"),
         ("No transfers in progress", "Наразі нічого не пересилається"),
         ("Set one-time password length", "Вказати довжину одноразового пароля"),
-        ("idd_driver_tip", "Встановити драйвер віртуального дисплея, який використовується в разі відсутності фізичних екранів."),
-        ("confirm_idd_driver_tip", "Позначено встановлення драйвера віртуального дисплея. Зважайте, що буде встановлено тестовий сертифікат для засвідчення драйвера віртуального дисплея. Цей тестовий сертифікат буде використовуватись лише для засвідчення драйверів Rustdesk."),
+        ("install_cert_tip", "Додати сертифікат Rustdesk"),
+        ("confirm_install_cert_tip", "Це сертифікат тестування Rustdesk, якому можна довіряти. За потреби сертифікат буде використано для погодження та встановлення драйверів Rustdesk."),
         ("RDP Settings", "Налаштування RDP"),
         ("Sort by", "Сортувати за"),
         ("New Connection", "Нове підключення"),
@@ -468,8 +469,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
         ("Minimize", "Згорнути"),
         ("Maximize", "Розгорнути"),
         ("Your Device", "Вам пристрій"),
-        ("empty_recent_tip", "Овва, відсутні нещодавні сеанси!\nСаме час запланувати новий."),
-        ("empty_favorite_tip", "Поки немає улюблених вузлів?\nДавайте знайдемо нове підключення та додамо його до улюблених!"),
+        ("empty_recent_tip", "Овва, відсутні нещодавні сеанси!\nСаме час запланувати нове підключення."),
+        ("empty_favorite_tip", "Досі немає улюблених вузлів?\nДавайте організуємо нове підключення та додамо його до улюблених!"),
         ("empty_lan_tip", "О ні, схоже ми поки не виявили жодного віддаленого пристрою"),
         ("empty_address_book_tip", "Ой лишенько, схоже до вашої адресної книги немає жодного віддаленого пристрою"),
         ("eg: admin", "напр. admin"),
@@ -508,9 +509,39 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
         ("resolution_original_tip", "Початкова роздільна здатність"),
         ("resolution_fit_local_tip", "Припасувати поточну роздільну здатність"),
         ("resolution_custom_tip", "Користувацька роздільна здатність"),
-        ("Collapse toolbar", ""),
-        ("Accept and Elevate", ""),
-        ("accept_and_elevate_btn_tooltip", ""),
-        ("clipboard_wait_response_timeout_tip", ""),
+        ("Collapse toolbar", "Згорнути панель інструментів"),
+        ("Accept and Elevate", "Погодитись та розширити права"),
+        ("accept_and_elevate_btn_tooltip", "Погодити підключення та розширити дозволи UAC."),
+        ("clipboard_wait_response_timeout_tip", "Вийшов час очікування копіювання."),
+        ("Incoming connection", "Вхідне підключення"),
+        ("Outgoing connection", "Вихідне підключення"),
+        ("Exit", "Вийти"),
+        ("Open", "Відкрити"),
+        ("logout_tip", "Ви впевнені, що хочете вилогуватися?"),
+        ("Service", ""),
+        ("Start", ""),
+        ("Stop", ""),
+        ("exceed_max_devices", ""),
+        ("Sync with recent sessions", ""),
+        ("Sort tags", ""),
+        ("Open connection in new tab", ""),
+        ("Move tab to new window", ""),
+        ("Can not be empty", ""),
+        ("Already exists", ""),
+        ("Change Password", ""),
+        ("Refresh Password", ""),
+        ("ID", ""),
+        ("Grid View", ""),
+        ("List View", ""),
+        ("Select", ""),
+        ("Toggle Tags", ""),
+        ("pull_ab_failed_tip", ""),
+        ("push_ab_failed_tip", ""),
+        ("synced_peer_readded_tip", ""),
+        ("Change Color", ""),
+        ("Primary Color", ""),
+        ("HSV Color", ""),
+        ("Installation Successful!", ""),
+        ("Installation failed!", ""),
     ].iter().cloned().collect();
 }
diff --git a/src/lang/vn.rs b/src/lang/vn.rs
index 05689843b..a6a4478e7 100644
--- a/src/lang/vn.rs
+++ b/src/lang/vn.rs
@@ -53,7 +53,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
         ("Audio Input", "Đầu vào âm thanh"),
         ("Enhancements", "Các tiện ích"),
         ("Hardware Codec", "Codec phần cứng"),
-        ("Adaptive Bitrate", "Bitrate thích ứng"),
+        ("Adaptive bitrate", "Bitrate thích ứng"),
         ("ID Server", "Máy chủ ID"),
         ("Relay Server", "Máy chủ Chuyển tiếp"),
         ("API Server", "Máy chủ API"),
@@ -233,6 +233,7 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
         ("Username missed", "Mất tên người dùng"),
         ("Password missed", "Mất mật khẩu"),
         ("Wrong credentials", "Chứng danh bị sai"),
+        ("The verification code is incorrect or has expired", ""),
         ("Edit Tag", "Chỉnh sửa Tag"),
         ("Unremember Password", "Quên mật khẩu"),
         ("Favorites", "Ưa thích"),
@@ -459,8 +460,8 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
         ("Resolution", "Độ phân giải"),
         ("No transfers in progress", "Không có tệp tin nào đang được truyền"),
         ("Set one-time password length", "Thiết lập độ dài mật khẩu một lần"),
-        ("idd_driver_tip", "Cài đặt driver màn hình ảo để sử dụng khi bạn không có màn hình vật lý."),
-        ("confirm_idd_driver_tip", "Tùy chọn cài đặt driver màn hình ảo đã được bật. Lưu ý rằng một chứng nhận thử sẽ được cài đặt để tin cậy driver màn hình ảo. Chứng nhận thử này sẽ chỉ được dùng để tin cậy những driver của RustDesk."),
+        ("install_cert_tip", ""),
+        ("confirm_install_cert_tip", ""),
         ("RDP Settings", "Cài đặt RDP"),
         ("Sort by", "Sắp xếp theo"),
         ("New Connection", "Kết nối mới"),
@@ -512,5 +513,35 @@ pub static ref T: std::collections::HashMap<&'static str, &'static str> =
         ("Accept and Elevate", "Chấp nhận và Cấp Quyền"),
         ("accept_and_elevate_btn_tooltip", "Chấp nhận kết nối và cấp các quyền UAC."),
         ("clipboard_wait_response_timeout_tip", ""),
+        ("Incoming connection", ""),
+        ("Outgoing connection", ""),
+        ("Exit", ""),
+        ("Open", ""),
+        ("logout_tip", ""),
+        ("Service", ""),
+        ("Start", ""),
+        ("Stop", ""),
+        ("exceed_max_devices", ""),
+        ("Sync with recent sessions", ""),
+        ("Sort tags", ""),
+        ("Open connection in new tab", ""),
+        ("Move tab to new window", ""),
+        ("Can not be empty", ""),
+        ("Already exists", ""),
+        ("Change Password", ""),
+        ("Refresh Password", ""),
+        ("ID", ""),
+        ("Grid View", ""),
+        ("List View", ""),
+        ("Select", ""),
+        ("Toggle Tags", ""),
+        ("pull_ab_failed_tip", ""),
+        ("push_ab_failed_tip", ""),
+        ("synced_peer_readded_tip", ""),
+        ("Change Color", ""),
+        ("Primary Color", ""),
+        ("HSV Color", ""),
+        ("Installation Successful!", ""),
+        ("Installation failed!", ""),
     ].iter().cloned().collect();
 }
diff --git a/src/lib.rs b/src/lib.rs
index 50ac4351e..631766a2d 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -41,7 +41,6 @@ pub mod cli;
 #[cfg(not(any(target_os = "android", target_os = "ios", feature = "cli")))]
 pub mod core_main;
 mod lang;
-#[cfg(windows)]
 mod license;
 #[cfg(not(any(target_os = "android", target_os = "ios")))]
 mod port_forward;
diff --git a/src/license.rs b/src/license.rs
index 1b8742533..029611345 100644
--- a/src/license.rs
+++ b/src/license.rs
@@ -31,7 +31,9 @@ fn get_license_from_string_(s: &str) -> ResultType<License> {
 }
 
 pub fn get_license_from_string(s: &str) -> ResultType<License> {
-    let s = if s.to_lowercase().ends_with(".exe") {
+    let s = if s.to_lowercase().ends_with(".exe.exe") {
+        &s[0..s.len() - 8]
+    } else if s.to_lowercase().ends_with(".exe") {
         &s[0..s.len() - 4]
     } else {
         s
@@ -53,20 +55,23 @@ pub fn get_license_from_string(s: &str) -> ResultType<License> {
         let strs: Vec<&str> = stripped.split(",").collect();
         let mut host = "";
         let mut key = "";
+        let mut api = "";
         let strs_iter = strs.iter();
         for el in strs_iter {
             if el.starts_with("host=") {
                 host = &el[5..el.len()];
             }
-
             if el.starts_with("key=") {
                 key = &el[4..el.len()];
             }
+            if el.starts_with("api=") {
+                api = &el[4..el.len()];
+            }
         }
         return Ok(License {
             host: host.to_owned(),
             key: key.to_owned(),
-            api: "".to_owned(),
+            api: api.to_owned(),
         });
     } else {
         let strs = if s.contains("-licensed-") {
@@ -110,12 +115,14 @@ mod test {
         );
         // key in these tests is "foobar.,2" base64 encoded
         assert_eq!(
-            get_license_from_string("rustdesk-host=server.example.net,key=Zm9vYmFyLiwyCg==.exe")
-                .unwrap(),
+            get_license_from_string(
+                "rustdesk-host=server.example.net,api=abc,key=Zm9vYmFyLiwyCg==.exe"
+            )
+            .unwrap(),
             License {
                 host: "server.example.net".to_owned(),
                 key: "Zm9vYmFyLiwyCg==".to_owned(),
-                api: "".to_owned(),
+                api: "abc".to_owned(),
             }
         );
         assert_eq!(
diff --git a/src/naming.rs b/src/naming.rs
index 7a8d0cecc..1b70af923 100644
--- a/src/naming.rs
+++ b/src/naming.rs
@@ -12,15 +12,14 @@ fn main() {
     let args: Vec<_> = std::env::args().skip(1).collect();
     let api = args.get(2).cloned().unwrap_or_default();
     if args.len() >= 2 {
-        println!(
-            "rustdesk-licensed-{}.exe",
-            gen_name(&License {
-                key: args[0].clone(),
-                host: args[1].clone(),
-                api,
-            })
-            .unwrap()
-        );
+        match gen_name(&License {
+            key: args[0].clone(),
+            host: args[1].clone(),
+            api,
+        }) {
+            Ok(name) => println!("rustdesk-licensed-{}.exe", name),
+            Err(e) => println!("{:?}", e),
+        }
     }
     if args.len() == 1 {
         println!("{:?}", get_license_from_string(&args[0]));
diff --git a/src/platform/delegate.rs b/src/platform/delegate.rs
index e4d9cc396..d2a073888 100644
--- a/src/platform/delegate.rs
+++ b/src/platform/delegate.rs
@@ -64,11 +64,10 @@ impl AppHandler for Rc<Host> {
 
 // https://github.com/xi-editor/druid/blob/master/druid-shell/src/platform/mac/application.rs
 pub unsafe fn set_delegate(handler: Option<Box<dyn AppHandler>>) {
-    let decl = ClassDecl::new("AppDelegate", class!(NSObject));
-    if decl.is_none() {
+    let Some(mut decl) = ClassDecl::new("AppDelegate", class!(NSObject)) else {
+        log::error!("Failed to new AppDelegate");
         return;
-    }
-    let mut decl = decl.unwrap();
+    };
     decl.add_ivar::<*mut c_void>(APP_HANDLER_IVAR);
 
     decl.add_method(
@@ -116,7 +115,10 @@ pub unsafe fn set_delegate(handler: Option<Box<dyn AppHandler>>) {
     let handler_ptr = Box::into_raw(Box::new(state));
     (*delegate).set_ivar(APP_HANDLER_IVAR, handler_ptr as *mut c_void);
     // Set the url scheme handler
-    let cls = Class::get("NSAppleEventManager").unwrap();
+    let Some(cls) = Class::get("NSAppleEventManager") else {
+        log::error!("Failed to get NSAppleEventManager");
+        return;
+    };
     let manager: *mut Object = msg_send![cls, sharedAppleEventManager];
     let _: () = msg_send![manager,
                               setEventHandler: delegate
@@ -199,10 +201,10 @@ fn service_should_handle_reopen(
     _sel: Sel,
     _sender: id,
     _has_visible_windows: BOOL,
-  ) -> BOOL {
+) -> BOOL {
     log::debug!("Invoking the main rustdesk process");
-    std::thread::spawn(move || crate::handle_url_scheme("".to_string())); 
-    // Prevent default logic. 
+    std::thread::spawn(move || crate::handle_url_scheme("".to_string()));
+    // Prevent default logic.
     NO
 }
 
diff --git a/src/platform/linux.rs b/src/platform/linux.rs
index bbe415c71..944e24c15 100644
--- a/src/platform/linux.rs
+++ b/src/platform/linux.rs
@@ -1,5 +1,8 @@
 use super::{CursorData, ResultType};
 use desktop::Desktop;
+#[cfg(all(feature = "linux_headless"))]
+#[cfg(not(any(feature = "flatpak", feature = "appimage")))]
+use hbb_common::config::CONFIG_OPTION_ALLOW_LINUX_HEADLESS;
 pub use hbb_common::platform::linux::*;
 use hbb_common::{
     allow_err, bail,
@@ -69,6 +72,19 @@ pub struct xcb_xfixes_get_cursor_image {
     pub pixels: *const c_long,
 }
 
+#[inline]
+#[cfg(feature = "linux_headless")]
+#[cfg(not(any(feature = "flatpak", feature = "appimage")))]
+pub fn is_headless_allowed() -> bool {
+    Config::get_option(CONFIG_OPTION_ALLOW_LINUX_HEADLESS) == "Y"
+}
+
+#[inline]
+pub fn is_login_screen_wayland() -> bool {
+    let values = get_values_of_seat0_with_gdm_wayland(&[0, 2]);
+    is_gdm_user(&values[1]) && get_display_server_of_session(&values[0]) == DISPLAY_SERVER_WAYLAND
+}
+
 #[inline]
 fn sleep_millis(millis: u64) {
     std::thread::sleep(Duration::from_millis(millis));
@@ -429,13 +445,21 @@ fn get_cm() -> bool {
 }
 
 pub fn is_login_wayland() -> bool {
-    if let Ok(contents) = std::fs::read_to_string("/etc/gdm3/custom.conf") {
-        contents.contains("#WaylandEnable=false") || contents.contains("WaylandEnable=true")
-    } else if let Ok(contents) = std::fs::read_to_string("/etc/gdm/custom.conf") {
-        contents.contains("#WaylandEnable=false") || contents.contains("WaylandEnable=true")
-    } else {
-        false
+    let files = ["/etc/gdm3/custom.conf", "/etc/gdm/custom.conf"];
+    match (
+        Regex::new(r"# *WaylandEnable *= *false"),
+        Regex::new(r"WaylandEnable *= *true"),
+    ) {
+        (Ok(pat1), Ok(pat2)) => {
+            for file in files {
+                if let Ok(contents) = std::fs::read_to_string(file) {
+                    return pat1.is_match(&contents) || pat2.is_match(&contents);
+                }
+            }
+        }
+        _ => {}
     }
+    false
 }
 
 #[inline]
@@ -669,7 +693,8 @@ pub fn check_super_user_permission() -> ResultType<bool> {
     }
     // https://github.com/rustdesk/rustdesk/issues/2756
     if let Ok(status) = Command::new("pkexec").arg(arg).status() {
-        Ok(status.code() != Some(126))
+        // https://github.com/rustdesk/rustdesk/issues/5205#issuecomment-1658059657s
+        Ok(status.code() != Some(126) && status.code() != Some(127))
     } else {
         Ok(true)
     }
@@ -728,7 +753,9 @@ pub fn get_double_click_time() -> u32 {
     // g_object_get (settings, "gtk-double-click-time", &double_click_time, NULL);
     unsafe {
         let mut double_click_time = 0u32;
-        let property = std::ffi::CString::new("gtk-double-click-time").unwrap();
+        let Ok(property) = std::ffi::CString::new("gtk-double-click-time") else {
+            return 0;
+        };
         let settings = gtk_settings_get_default();
         g_object_get(
             settings,
@@ -801,7 +828,10 @@ pub fn resolutions(name: &str) -> Vec<Resolution> {
                     if let Some(resolutions) = caps.name("resolutions") {
                         let resolution_pat =
                             r"\s*(?P<width>\d+)x(?P<height>\d+)\s+(?P<rates>(\d+\.\d+\D*)+)\s*\n";
-                        let resolution_re = Regex::new(&format!(r"{}", resolution_pat)).unwrap();
+                        let Ok(resolution_re) = Regex::new(&format!(r"{}", resolution_pat)) else {
+                            log::error!("Regex new failed");
+                            return vec![];
+                        };
                         for resolution_caps in resolution_re.captures_iter(resolutions.as_str()) {
                             if let Some((width, height)) =
                                 get_width_height_from_captures(&resolution_caps)
@@ -1043,8 +1073,8 @@ mod desktop {
         }
 
         pub fn refresh(&mut self) {
-            if !self.sid.is_empty() && is_active(&self.sid) {
-                return;
+            if !self.sid.is_empty() && is_active_and_seat0(&self.sid) {
+                     return;
             }
 
             let seat0_values = get_values_of_seat0(&[0, 1, 2]);
diff --git a/src/platform/linux_desktop_manager.rs b/src/platform/linux_desktop_manager.rs
index fe60964cc..b7caa527c 100644
--- a/src/platform/linux_desktop_manager.rs
+++ b/src/platform/linux_desktop_manager.rs
@@ -109,6 +109,10 @@ pub fn try_start_desktop(_username: &str, _passsword: &str) -> String {
             // No need to verify password here.
             return "".to_owned();
         }
+        if !username.is_empty() {
+            // Another user is logged in. No need to start a new xsession.
+            return "".to_owned();
+        }
 
         if let Some(msg) = detect_headless() {
             return msg.to_owned();
diff --git a/src/platform/macos.mm b/src/platform/macos.mm
index d40a56014..4a19973fc 100644
--- a/src/platform/macos.mm
+++ b/src/platform/macos.mm
@@ -4,6 +4,27 @@
 #include <Security/Authorization.h>
 #include <Security/AuthorizationTags.h>
 
+extern "C" bool CanUseNewApiForScreenCaptureCheck() {
+    #ifdef NO_InputMonitoringAuthStatus
+    return false;
+    #else
+    NSOperatingSystemVersion version = [[NSProcessInfo processInfo] operatingSystemVersion];
+    return version.majorVersion >= 11;
+    #endif
+}
+
+extern "C" bool IsCanScreenRecording(bool prompt) {
+    #ifdef NO_InputMonitoringAuthStatus
+    return false;
+    #else
+    bool res = CGPreflightScreenCaptureAccess();
+    if (!res && prompt) {
+        CGRequestScreenCaptureAccess();
+    }
+    return res;
+    #endif
+}
+
 
 // https://github.com/codebytere/node-mac-permissions/blob/main/permissions.mm
 
diff --git a/src/platform/macos.rs b/src/platform/macos.rs
index 3f480f839..78e52faf4 100644
--- a/src/platform/macos.rs
+++ b/src/platform/macos.rs
@@ -34,6 +34,8 @@ extern "C" {
     static kAXTrustedCheckOptionPrompt: CFStringRef;
     fn AXIsProcessTrustedWithOptions(options: CFDictionaryRef) -> BOOL;
     fn InputMonitoringAuthStatus(_: BOOL) -> BOOL;
+    fn IsCanScreenRecording(_: BOOL) -> BOOL;
+    fn CanUseNewApiForScreenCaptureCheck() -> BOOL;
     fn MacCheckAdminAuthorization() -> BOOL;
     fn MacGetModeNum(display: u32, numModes: *mut u32) -> BOOL;
     fn MacGetModes(
@@ -71,6 +73,14 @@ pub fn is_can_input_monitoring(prompt: bool) -> bool {
 // https://stackoverflow.com/questions/56597221/detecting-screen-recording-settings-on-macos-catalina/
 // remove just one app from all the permissions: tccutil reset All com.carriez.rustdesk
 pub fn is_can_screen_recording(prompt: bool) -> bool {
+    // we got some report that we show no permission even after set it, so we try to use new api for screen recording check
+    // the new api is only available on macOS >= 10.15, but on stackoverflow, some people said it works on >= 10.16 (crash on 10.15),
+    // but also some said it has bug on 10.16, so we just use it on 11.0.
+    unsafe {
+        if CanUseNewApiForScreenCaptureCheck() == YES {
+            return IsCanScreenRecording(if prompt { YES } else { NO }) == YES;
+        }
+    }
     let mut can_record_screen: bool = false;
     unsafe {
         let our_pid: i32 = std::process::id() as _;
@@ -122,6 +132,10 @@ pub fn is_can_screen_recording(prompt: bool) -> bool {
     can_record_screen
 }
 
+pub fn install_service() -> bool {
+    is_installed_daemon(false)
+}
+
 pub fn is_installed_daemon(prompt: bool) -> bool {
     let daemon = format!("{}_service.plist", crate::get_full_name());
     let agent = format!("{}_server.plist", crate::get_full_name());
@@ -136,14 +150,26 @@ pub fn is_installed_daemon(prompt: bool) -> bool {
         return true;
     }
 
-    let install_script = PRIVILEGES_SCRIPTS_DIR.get_file("install.scpt").unwrap();
-    let install_script_body = install_script.contents_utf8().unwrap();
+    let Some(install_script) = PRIVILEGES_SCRIPTS_DIR.get_file("install.scpt") else {
+        return false;
+    };
+    let Some(install_script_body) = install_script.contents_utf8() else {
+        return false;
+    };
 
-    let daemon_plist = PRIVILEGES_SCRIPTS_DIR.get_file(&daemon).unwrap();
-    let daemon_plist_body = daemon_plist.contents_utf8().unwrap();
+    let Some(daemon_plist) = PRIVILEGES_SCRIPTS_DIR.get_file(&daemon) else {
+        return false;
+    };
+    let Some(daemon_plist_body) = daemon_plist.contents_utf8() else {
+        return false;
+    };
 
-    let agent_plist = PRIVILEGES_SCRIPTS_DIR.get_file(&agent).unwrap();
-    let agent_plist_body = agent_plist.contents_utf8().unwrap();
+    let Some(agent_plist) = PRIVILEGES_SCRIPTS_DIR.get_file(&agent) else {
+        return false;
+    };
+    let Some(agent_plist_body) = agent_plist.contents_utf8() else {
+        return false;
+    };
 
     std::thread::spawn(move || {
         match std::process::Command::new("osascript")
@@ -188,8 +214,12 @@ pub fn uninstall_service(show_new_window: bool) -> bool {
         return false;
     }
 
-    let script_file = PRIVILEGES_SCRIPTS_DIR.get_file("uninstall.scpt").unwrap();
-    let script_body = script_file.contents_utf8().unwrap();
+    let Some(script_file) = PRIVILEGES_SCRIPTS_DIR.get_file("uninstall.scpt") else {
+        return false;
+    };
+    let Some(script_body) = script_file.contents_utf8() else {
+        return false;
+    };
 
     std::thread::spawn(move || {
         match std::process::Command::new("osascript")
@@ -210,10 +240,12 @@ pub fn uninstall_service(show_new_window: bool) -> bool {
                     uninstalled
                 );
                 if uninstalled {
+                    if !show_new_window {
+                        let _ = crate::ipc::close_all_instances();
+                        // leave ipc a little time
+                        std::thread::sleep(std::time::Duration::from_millis(300));
+                    }
                     crate::ipc::set_option("stop-service", "Y");
-                    let _ = crate::ipc::close_all_instances();
-                    // leave ipc a little time
-                    std::thread::sleep(std::time::Duration::from_millis(300));
                     std::process::Command::new("launchctl")
                         .args(&["remove", &format!("{}_server", crate::get_full_name())])
                         .status()
@@ -227,11 +259,6 @@ pub fn uninstall_service(show_new_window: bool) -> bool {
                             ))
                             .spawn()
                             .ok();
-                    } else {
-                        std::process::Command::new("pkill")
-                            .arg(crate::get_app_name())
-                            .status()
-                            .ok();
                     }
                     quit_gui();
                 }
diff --git a/src/platform/mod.rs b/src/platform/mod.rs
index 60ef1806b..e962ef9d5 100644
--- a/src/platform/mod.rs
+++ b/src/platform/mod.rs
@@ -48,7 +48,7 @@ pub fn breakdown_callback() {
 pub fn change_resolution(name: &str, width: usize, height: usize) -> ResultType<()> {
     let cur_resolution = current_resolution(name)?;
     // For MacOS
-    // to-do: Make sure the following comparison works. 
+    // to-do: Make sure the following comparison works.
     // For Linux
     // Just run "xrandr", dpi may not be taken into consideration.
     // For Windows
diff --git a/src/platform/windows.cc b/src/platform/windows.cc
index c4286ebdd..4ffa6eeeb 100644
--- a/src/platform/windows.cc
+++ b/src/platform/windows.cc
@@ -628,4 +628,8 @@ extern "C"
         return bSystem;
     }
 
+    void alloc_console_and_redirect() {
+        AllocConsole();
+        freopen("CONOUT$", "w", stdout);
+    }
 } // end of extern "C"
\ No newline at end of file
diff --git a/src/platform/windows.rs b/src/platform/windows.rs
index df783b781..feadffb2c 100644
--- a/src/platform/windows.rs
+++ b/src/platform/windows.rs
@@ -6,7 +6,9 @@ use crate::{
     privacy_win_mag::{self, WIN_MAG_INJECTED_PROCESS_EXE},
 };
 use hbb_common::{
-    allow_err, bail,
+    allow_err,
+    anyhow::anyhow,
+    bail,
     config::{self, Config},
     log,
     message_proto::Resolution,
@@ -455,6 +457,7 @@ extern "C" {
     fn win_stop_system_key_propagate(v: BOOL);
     fn is_win_down() -> BOOL;
     fn is_local_system() -> BOOL;
+    fn alloc_console_and_redirect();
 }
 
 extern "system" {
@@ -847,10 +850,9 @@ pub fn check_update_broker_process() -> ResultType<()> {
     let origin_process_exe = privacy_win_mag::ORIGIN_PROCESS_EXE;
 
     let exe_file = std::env::current_exe()?;
-    if exe_file.parent().is_none() {
+    let Some(cur_dir) = exe_file.parent() else {
         bail!("Cannot get parent of current exe file");
-    }
-    let cur_dir = exe_file.parent().unwrap();
+    };
     let cur_exe = cur_dir.join(process_exe);
 
     if !std::path::Path::new(&cur_exe).exists() {
@@ -901,60 +903,31 @@ fn get_install_info_with_subkey(subkey: String) -> (String, String, String, Stri
     (subkey, path, start_menu, exe)
 }
 
-pub fn copy_raw_cmd(src_raw: &str, _raw: &str, _path: &str) -> String {
+pub fn copy_raw_cmd(src_raw: &str, _raw: &str, _path: &str) -> ResultType<String> {
     let main_raw = format!(
         "XCOPY \"{}\" \"{}\" /Y /E /H /C /I /K /R /Z",
         PathBuf::from(src_raw)
             .parent()
-            .unwrap()
+            .ok_or(anyhow!("Can't get parent directory of {src_raw}"))?
             .to_string_lossy()
             .to_string(),
         _path
     );
-    return main_raw;
+    return Ok(main_raw);
 }
 
-pub fn copy_exe_cmd(src_exe: &str, exe: &str, path: &str) -> String {
-    let main_exe = copy_raw_cmd(src_exe, exe, path);
-    format!(
+pub fn copy_exe_cmd(src_exe: &str, exe: &str, path: &str) -> ResultType<String> {
+    let main_exe = copy_raw_cmd(src_exe, exe, path)?;
+    Ok(format!(
         "
         {main_exe}
         copy /Y \"{ORIGIN_PROCESS_EXE}\" \"{path}\\{broker_exe}\"
         ",
         ORIGIN_PROCESS_EXE = privacy_win_mag::ORIGIN_PROCESS_EXE,
         broker_exe = privacy_win_mag::INJECTED_PROCESS_EXE,
-    )
+    ))
 }
 
-/* // update_me has bad compatibility, so disable it.
-pub fn update_me() -> ResultType<()> {
-    let (_, path, _, exe) = get_install_info();
-    let src_exe = std::env::current_exe()?.to_str().unwrap_or("").to_owned();
-    let cmds = format!(
-        "
-        chcp 65001
-        sc stop {app_name}
-        taskkill /F /IM {broker_exe}
-        taskkill /F /IM {app_name}.exe /FI \"PID ne {cur_pid}\"
-        {copy_exe}
-        sc start {app_name}
-        {lic}
-    ",
-        copy_exe = copy_exe_cmd(&src_exe, &exe, &path),
-        broker_exe = WIN_MAG_INJECTED_PROCESS_EXE,
-        app_name = crate::get_app_name(),
-        lic = register_licence(),
-        cur_pid = get_current_pid(),
-    );
-    run_cmds(cmds, false, "update")?;
-    run_after_run_cmds(false);
-    std::process::Command::new(&exe)
-        .args(&["--remove", &src_exe])
-        .spawn()?;
-    Ok(())
-}
-*/
-
 fn get_after_install(exe: &str) -> String {
     let app_name = crate::get_app_name();
     let ext = app_name.to_lowercase();
@@ -981,6 +954,7 @@ fn get_after_install(exe: &str) -> String {
     reg add HKEY_CLASSES_ROOT\\{ext}\\shell\\open /f
     reg add HKEY_CLASSES_ROOT\\{ext}\\shell\\open\\command /f
     reg add HKEY_CLASSES_ROOT\\{ext}\\shell\\open\\command /f /ve /t REG_SZ /d \"\\\"{exe}\\\" \\\"%%1\\\"\"
+    netsh advfirewall firewall add rule name=\"{app_name} Service\" dir=out action=allow program=\"{exe}\" enable=yes
     netsh advfirewall firewall add rule name=\"{app_name} Service\" dir=in action=allow program=\"{exe}\" enable=yes
     {create_service}
     reg add HKEY_LOCAL_MACHINE\\Software\\Microsoft\\Windows\\CurrentVersion\\Policies\\System /f /v SoftwareSASGeneration /t REG_DWORD /d 1
@@ -1092,6 +1066,13 @@ if exist \"{tmp_path}\\{app_name} Tray.lnk\" del /f /q \"{tmp_path}\\{app_name}
         "".to_owned()
     };
 
+    // potential bug here: if run_cmd cancelled, but config file is changed.
+    if let Some(lic) = get_license() {
+        Config::set_option("key".into(), lic.key);
+        Config::set_option("custom-rendezvous-server".into(), lic.host);
+        Config::set_option("api-server".into(), lic.api);
+    }
+
     let cmds = format!(
         "
 {uninstall_str}
@@ -1112,7 +1093,6 @@ reg add {subkey} /f /v VersionBuild /t REG_DWORD /d {version_build}
 reg add {subkey} /f /v UninstallString /t REG_SZ /d \"\\\"{exe}\\\" --uninstall\"
 reg add {subkey} /f /v EstimatedSize /t REG_DWORD /d {size}
 reg add {subkey} /f /v WindowsInstaller /t REG_DWORD /d 0
-{lic}
 cscript \"{mk_shortcut}\"
 cscript \"{uninstall_shortcut}\"
 cscript \"{tray_shortcut}\"
@@ -1127,7 +1107,6 @@ copy /Y \"{tmp_path}\\Uninstall {app_name}.lnk\" \"{path}\\\"
     ",
         version=crate::VERSION,
         build_date=crate::BUILD_DATE,
-        lic=register_licence(),
         after_install=get_after_install(&exe),
         sleep=if debug {
             "timeout 300"
@@ -1139,7 +1118,7 @@ copy /Y \"{tmp_path}\\Uninstall {app_name}.lnk\" \"{path}\\\"
         } else {
             &dels
         },
-        copy_exe = copy_exe_cmd(&src_exe, &exe, &path),
+        copy_exe = copy_exe_cmd(&src_exe, &exe, &path)?,
         import_config = get_import_config(&exe),
     );
     run_cmds(cmds, debug, "install")?;
@@ -1221,7 +1200,7 @@ fn write_cmds(cmds: String, ext: &str, tip: &str) -> ResultType<std::path::PathB
     tmp.push(format!("{}_{}.{}", crate::get_app_name(), tip, ext));
     let mut file = std::fs::File::create(&tmp)?;
     if ext == "bat" {
-        let tmp2 = get_undone_file(&tmp);
+        let tmp2 = get_undone_file(&tmp)?;
         std::fs::File::create(&tmp2).ok();
         cmds = format!(
             "
@@ -1252,18 +1231,20 @@ fn to_le(v: &mut [u16]) -> &[u8] {
     unsafe { v.align_to().1 }
 }
 
-fn get_undone_file(tmp: &PathBuf) -> PathBuf {
+fn get_undone_file(tmp: &PathBuf) -> ResultType<PathBuf> {
     let mut tmp1 = tmp.clone();
     tmp1.set_file_name(format!(
         "{}.undone",
-        tmp.file_name().unwrap().to_string_lossy()
+        tmp.file_name()
+            .ok_or(anyhow!("Failed to get filename of {:?}", tmp))?
+            .to_string_lossy()
     ));
-    tmp1
+    Ok(tmp1)
 }
 
 fn run_cmds(cmds: String, show: bool, tip: &str) -> ResultType<()> {
     let tmp = write_cmds(cmds, "bat", tip)?;
-    let tmp2 = get_undone_file(&tmp);
+    let tmp2 = get_undone_file(&tmp)?;
     let tmp_fn = tmp.to_str().unwrap_or("");
     let res = runas::Command::new("cmd")
         .args(&["/C", &tmp_fn])
@@ -1346,12 +1327,11 @@ fn get_reg_of(subkey: &str, name: &str) -> String {
     "".to_owned()
 }
 
-fn get_license_from_exe_name() -> ResultType<License> {
+pub fn get_license_from_exe_name() -> ResultType<License> {
     let mut exe = std::env::current_exe()?.to_str().unwrap_or("").to_owned();
     // if defined portable appname entry, replace original executable name with it.
     if let Ok(portable_exe) = std::env::var(PORTABLE_APPNAME_RUNTIME_ENV_KEY) {
         exe = portable_exe;
-        log::debug!("update portable executable name to {}", exe);
     }
     get_license_from_string(&exe)
 }
@@ -1361,42 +1341,9 @@ pub fn is_win_server() -> bool {
     unsafe { is_windows_server() > 0 }
 }
 
-pub fn get_license() -> Option<License> {
-    let mut lic: License = Default::default();
-    if let Ok(tmp) = get_license_from_exe_name() {
-        lic = tmp;
-    } else {
-        lic.key = get_reg("Key");
-        lic.host = get_reg("Host");
-        lic.api = get_reg("Api");
-    }
-    if lic.key.is_empty() || lic.host.is_empty() {
-        return None;
-    }
-    Some(lic)
-}
-
 pub fn bootstrap() {
-    if let Some(lic) = get_license() {
-        *config::PROD_RENDEZVOUS_SERVER.write().unwrap() = lic.host.clone();
-    }
-}
-
-fn register_licence() -> String {
-    let (subkey, _, _, _) = get_install_info();
     if let Ok(lic) = get_license_from_exe_name() {
-        format!(
-            "
-        reg add {subkey} /f /v Key /t REG_SZ /d \"{key}\"
-        reg add {subkey} /f /v Host /t REG_SZ /d \"{host}\"
-        reg add {subkey} /f /v Api /t REG_SZ /d \"{api}\"
-    ",
-            key = &lic.key,
-            host = &lic.host,
-            api = &lic.api,
-        )
-    } else {
-        "".to_owned()
+        *config::EXE_RENDEZVOUS_SERVER.write().unwrap() = lic.host.clone();
     }
 }
 
@@ -1601,18 +1548,8 @@ pub fn elevate_or_run_as_system(is_setup: bool, is_elevate: bool, is_run_as_syst
     }
 }
 
-// https://github.com/mgostIH/process_list/blob/master/src/windows/mod.rs
-#[repr(transparent)]
-pub(self) struct RAIIHandle(pub HANDLE);
-
-impl Drop for RAIIHandle {
-    fn drop(&mut self) {
-        // This never gives problem except when running under a debugger.
-        unsafe { CloseHandle(self.0) };
-    }
-}
-
 pub fn is_elevated(process_id: Option<DWORD>) -> ResultType<bool> {
+    use hbb_common::platform::windows::RAIIHandle;
     unsafe {
         let handle: HANDLE = match process_id {
             Some(process_id) => OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, process_id),
@@ -1923,18 +1860,22 @@ pub fn uninstall_cert() -> ResultType<()> {
 mod cert {
     use hbb_common::{allow_err, bail, log, ResultType};
     use std::{path::Path, str::from_utf8};
-    use winapi::shared::{
-        minwindef::{BYTE, DWORD, TRUE},
-        ntdef::NULL,
-    };
-    use winapi::um::{
-        errhandlingapi::GetLastError,
-        wincrypt::{
-            CertCloseStore, CertEnumCertificatesInStore, CertNameToStrA, CertOpenSystemStoreA,
-            CryptHashCertificate, ALG_ID, CALG_SHA1, CERT_ID_SHA1_HASH, CERT_X500_NAME_STR,
-            PCCERT_CONTEXT,
+    use winapi::{
+        shared::{
+            minwindef::{BYTE, DWORD, FALSE, TRUE},
+            ntdef::NULL,
+        },
+        um::{
+            errhandlingapi::GetLastError,
+            wincrypt::{
+                CertAddEncodedCertificateToStore, CertCloseStore, CertDeleteCertificateFromStore,
+                CertEnumCertificatesInStore, CertNameToStrA, CertOpenSystemStoreW,
+                CryptHashCertificate, ALG_ID, CALG_SHA1, CERT_ID_SHA1_HASH,
+                CERT_STORE_ADD_REPLACE_EXISTING, CERT_X500_NAME_STR, PCCERT_CONTEXT,
+                X509_ASN_ENCODING,
+            },
+            winreg::HKEY_LOCAL_MACHINE,
         },
-        winreg::HKEY_LOCAL_MACHINE,
     };
     use winreg::{
         enums::{KEY_WRITE, REG_BINARY},
@@ -1946,6 +1887,8 @@ mod cert {
     const THUMBPRINT_ALG: ALG_ID = CALG_SHA1;
     const THUMBPRINT_LEN: DWORD = 20;
 
+    const CERT_ISSUER_1: &str = "CN=\"WDKTestCert admin,133225435702113567\"\0";
+
     #[inline]
     unsafe fn compute_thumbprint(pb_encoded: *const BYTE, cb_encoded: DWORD) -> (Vec<u8>, String) {
         let mut size = THUMBPRINT_LEN;
@@ -2000,6 +1943,12 @@ mod cert {
 
     pub fn install_cert<P: AsRef<Path>>(path: P) -> ResultType<()> {
         let mut cert_bytes = std::fs::read(path)?;
+        install_cert_reg(&mut cert_bytes)?;
+        install_cert_add_cert_store(&mut cert_bytes)?;
+        Ok(())
+    }
+
+    fn install_cert_reg(cert_bytes: &mut [u8]) -> ResultType<()> {
         unsafe {
             let thumbprint = compute_thumbprint(cert_bytes.as_mut_ptr(), cert_bytes.len() as _);
             log::debug!("Thumbprint of cert {}", &thumbprint.1);
@@ -2008,25 +1957,56 @@ mod cert {
             let (cert_key, _) = reg_cert_key.create_subkey(&thumbprint.1)?;
             let data = winreg::RegValue {
                 vtype: REG_BINARY,
-                bytes: create_cert_blob(thumbprint.0, cert_bytes),
+                bytes: create_cert_blob(thumbprint.0, cert_bytes.to_vec()),
             };
             cert_key.set_raw_value("Blob", &data)?;
         }
         Ok(())
     }
 
+    fn install_cert_add_cert_store(cert_bytes: &mut [u8]) -> ResultType<()> {
+        unsafe {
+            let store_handle = CertOpenSystemStoreW(0 as _, "ROOT\0".as_ptr() as _);
+            if store_handle.is_null() {
+                bail!("Error opening certificate store: {}", GetLastError());
+            }
+            let mut cert_ctx: PCCERT_CONTEXT = std::ptr::null_mut();
+            if FALSE
+                == CertAddEncodedCertificateToStore(
+                    store_handle,
+                    X509_ASN_ENCODING,
+                    cert_bytes.as_mut_ptr(),
+                    cert_bytes.len() as _,
+                    CERT_STORE_ADD_REPLACE_EXISTING,
+                    &mut cert_ctx as _,
+                )
+            {
+                log::error!(
+                    "Failed to call CertAddEncodedCertificateToStore: {}",
+                    GetLastError()
+                );
+            } else {
+                log::info!("Add cert to store successfully");
+            }
+
+            CertCloseStore(store_handle, 0);
+        }
+        Ok(())
+    }
+
     fn get_thumbprints_to_rm() -> ResultType<Vec<String>> {
-        let issuers_to_rm = ["CN=\"WDKTestCert admin,133225435702113567\""];
+        let issuers_to_rm = [CERT_ISSUER_1];
 
         let mut thumbprints = Vec::new();
         let mut buf = [0u8; 1024];
 
         unsafe {
-            let store_handle = CertOpenSystemStoreA(0 as _, "ROOT\0".as_ptr() as _);
+            let store_handle = CertOpenSystemStoreW(0 as _, "ROOT\0".as_ptr() as _);
             if store_handle.is_null() {
                 bail!("Error opening certificate store: {}", GetLastError());
             }
 
+            let mut vec_ctx = Vec::new();
             let mut cert_ctx: PCCERT_CONTEXT = CertEnumCertificatesInStore(store_handle, NULL as _);
             while !cert_ctx.is_null() {
                 // https://stackoverflow.com/a/66432736
@@ -2038,9 +2018,11 @@ mod cert {
                     buf.len() as _,
                 );
                 if cb_size != 1 {
+                    let mut add_ctx = false;
                     if let Ok(issuer) = from_utf8(&buf[..cb_size as _]) {
                         for iss in issuers_to_rm.iter() {
-                            if issuer.contains(iss) {
+                            if issuer == *iss {
+                                add_ctx = true;
                                 let (_, thumbprint) = compute_thumbprint(
                                     (*cert_ctx).pbCertEncoded,
                                     (*cert_ctx).cbCertEncoded,
@@ -2051,9 +2033,15 @@ mod cert {
                             }
                         }
                     }
+                    if add_ctx {
+                        vec_ctx.push(cert_ctx);
+                    }
                 }
                 cert_ctx = CertEnumCertificatesInStore(store_handle, cert_ctx);
             }
+            for ctx in vec_ctx {
+                CertDeleteCertificateFromStore(ctx);
+            }
             CertCloseStore(store_handle, 0);
         }
 
@@ -2063,6 +2051,7 @@ mod cert {
     pub fn uninstall_cert() -> ResultType<()> {
         let thumbprints = get_thumbprints_to_rm()?;
         let reg_cert_key = unsafe { open_reg_cert_store()? };
+        log::info!("Found {} certs to remove", thumbprints.len());
         for thumbprint in thumbprints.iter() {
             allow_err!(reg_cert_key.delete_subkey(thumbprint));
         }
@@ -2161,9 +2150,11 @@ pub fn uninstall_service(show_new_window: bool) -> bool {
     sc stop {app_name}
     sc delete {app_name}
     if exist \"%PROGRAMDATA%\\Microsoft\\Windows\\Start Menu\\Programs\\Startup\\{app_name} Tray.lnk\" del /f /q \"%PROGRAMDATA%\\Microsoft\\Windows\\Start Menu\\Programs\\Startup\\{app_name} Tray.lnk\"
+    taskkill /F /IM {broker_exe}
     taskkill /F /IM {app_name}.exe{filter}
     ",
         app_name = crate::get_app_name(),
+        broker_exe = WIN_MAG_INJECTED_PROCESS_EXE,
     );
     if let Err(err) = run_cmds(cmds, false, "uninstall") {
         Config::set_option("stop-service".into(), "".into());
@@ -2273,6 +2264,15 @@ fn run_after_run_cmds(silent: bool) {
     std::thread::sleep(std::time::Duration::from_millis(300));
 }
 
+#[inline]
+pub fn try_kill_broker() {
+    allow_err!(std::process::Command::new("cmd")
+        .arg("/c")
+        .arg(&format!("taskkill /F /IM {}", WIN_MAG_INJECTED_PROCESS_EXE))
+        .creation_flags(winapi::um::winbase::CREATE_NO_WINDOW)
+        .spawn());
+}
+
 #[cfg(test)]
 mod tests {
     use super::*;
@@ -2297,3 +2297,62 @@ mod tests {
         assert_eq!(chr, None)
     }
 }
+
+pub fn message_box(text: &str) {
+    let mut text = text.to_owned();
+    let nodialog = std::env::var("NO_DIALOG").unwrap_or_default() == "Y";
+    if !text.ends_with("!") || nodialog {
+        use arboard::Clipboard as ClipboardContext;
+        match ClipboardContext::new() {
+            Ok(mut ctx) => {
+                ctx.set_text(&text).ok();
+                if !nodialog {
+                    text = format!("{}\n\nAbove text has been copied to clipboard", &text);
+                }
+            }
+            _ => {}
+        }
+    }
+    if nodialog {
+        if std::env::var("PRINT_OUT").unwrap_or_default() == "Y" {
+            println!("{text}");
+        }
+        if let Ok(x) = std::env::var("WRITE_TO_FILE") {
+            if !x.is_empty() {
+                allow_err!(std::fs::write(x, text));
+            }
+        }
+        return;
+    }
+    let text = text
+        .encode_utf16()
+        .chain(std::iter::once(0))
+        .collect::<Vec<u16>>();
+    let caption = "RustDesk Output"
+        .encode_utf16()
+        .chain(std::iter::once(0))
+        .collect::<Vec<u16>>();
+    unsafe { MessageBoxW(std::ptr::null_mut(), text.as_ptr(), caption.as_ptr(), MB_OK) };
+}
+
+pub fn alloc_console() {
+    unsafe {
+        alloc_console_and_redirect();
+    }
+}
+
+fn get_license() -> Option<License> {
+    let mut lic: License = Default::default();
+    if let Ok(tmp) = get_license_from_exe_name() {
+        lic = tmp;
+    } else {
+        // for back compatibility from migrating from <= 1.2.1 to 1.2.2
+        lic.key = get_reg("Key");
+        lic.host = get_reg("Host");
+        lic.api = get_reg("Api");
+    }
+    if lic.key.is_empty() || lic.host.is_empty() {
+        return None;
+    }
+    Some(lic)
+}
diff --git a/src/plugin/callback_msg.rs b/src/plugin/callback_msg.rs
index 7ea0f7ae5..e634595e8 100644
--- a/src/plugin/callback_msg.rs
+++ b/src/plugin/callback_msg.rs
@@ -368,6 +368,7 @@ fn push_event_to_ui(channel: u16, peer: &str, content: &str) {
     m.insert("peer", &peer);
     m.insert("content", &content);
     let event = serde_json::to_string(&m).unwrap_or("".to_string());
+    // Send to main and cm
     for (k, v) in MSG_TO_UI_FLUTTER_CHANNELS.iter() {
         if channel & k != 0 {
             let _res = flutter::push_global_event(v as _, event.to_string());
diff --git a/src/plugin/native_handlers/session.rs b/src/plugin/native_handlers/session.rs
index f8c507a5b..fda07cd17 100644
--- a/src/plugin/native_handlers/session.rs
+++ b/src/plugin/native_handlers/session.rs
@@ -131,6 +131,8 @@ impl PluginNativeSessionHandler {
             let mut m = HashMap::new();
             m.insert("name", MSG_TO_UI_TYPE_SESSION_CREATED);
             m.insert("session_id", &session_id);
+            // todo: APP_TYPE_DESKTOP_REMOTE is not used anymore.
+            // crate::flutter::APP_TYPE_DESKTOP_REMOTE + window id, is used for multi-window support.
             crate::flutter::push_global_event(
                 crate::flutter::APP_TYPE_DESKTOP_REMOTE,
                 serde_json::to_string(&m).unwrap_or("".to_string()),
diff --git a/src/plugin/native_handlers/ui.rs b/src/plugin/native_handlers/ui.rs
index d4ee91299..aec7facd8 100644
--- a/src/plugin/native_handlers/ui.rs
+++ b/src/plugin/native_handlers/ui.rs
@@ -2,10 +2,7 @@ use std::{collections::HashMap, ffi::c_void, os::raw::c_int};
 
 use serde_json::json;
 
-use crate::{
-    define_method_prefix,
-    flutter::{APP_TYPE_MAIN},
-};
+use crate::{define_method_prefix, flutter::APP_TYPE_MAIN};
 
 use super::PluginNativeHandler;
 
@@ -26,7 +23,8 @@ pub struct PluginNativeUIHandler;
 /// ```
 /// [Safety]
 /// Please make sure the callback u provided is VALID, or memory or calling issues may occur to cause the program crash!
-pub type OnUIReturnCallback = extern "C" fn(return_code: c_int, data: *const c_void, data_len: u64, user_data: *const c_void);
+pub type OnUIReturnCallback =
+    extern "C" fn(return_code: c_int, data: *const c_void, data_len: u64, user_data: *const c_void);
 
 impl PluginNativeHandler for PluginNativeUIHandler {
     define_method_prefix!("ui_");
@@ -41,9 +39,7 @@ impl PluginNativeHandler for PluginNativeUIHandler {
                 if let Some(cb) = data.get("cb") {
                     if let Some(cb) = cb.as_u64() {
                         let user_data = match data.get("user_data") {
-                            Some(user_data) => {
-                                user_data.as_u64().unwrap_or(0)
-                            },
+                            Some(user_data) => user_data.as_u64().unwrap_or(0),
                             None => 0,
                         };
                         self.select_peers_async(cb, user_data);
@@ -68,9 +64,7 @@ impl PluginNativeHandler for PluginNativeUIHandler {
                 if let Some(on_tap_cb) = data.get("on_tap_cb") {
                     if let Some(on_tap_cb) = on_tap_cb.as_u64() {
                         let user_data = match data.get("user_data") {
-                            Some(user_data) => {
-                                user_data.as_u64().unwrap_or(0)
-                            },
+                            Some(user_data) => user_data.as_u64().unwrap_or(0),
                             None => 0,
                         };
                         self.register_ui_entry(title, on_tap_cb, user_data);
@@ -109,10 +103,10 @@ impl PluginNativeUIHandler {
     ///     "user_data": 0 // An opaque pointer value passed to the callback.
     /// }
     /// ```
-    /// 
-    /// [Arguments] 
+    ///
+    /// [Arguments]
     /// @param cb: the function address with type [OnUIReturnCallback].
-    /// @param user_data: the function will be called with this value. 
+    /// @param user_data: the function will be called with this value.
     fn select_peers_async(&self, cb: u64, user_data: u64) {
         let mut param = HashMap::new();
         param.insert("name", json!("native_ui"));
diff --git a/src/plugin/plugins.rs b/src/plugin/plugins.rs
index d4ffc4539..b40ee4116 100644
--- a/src/plugin/plugins.rs
+++ b/src/plugin/plugins.rs
@@ -628,13 +628,7 @@ fn reload_ui(desc: &Desc, sync_to: Option<&str>) {
                     // The first element is the "client" or "host".
                     // The second element is the "main", "remote", "cm", "file transfer", "port forward".
                     if v.len() >= 2 {
-                        let available_channels = vec![
-                            flutter::APP_TYPE_MAIN,
-                            flutter::APP_TYPE_DESKTOP_REMOTE,
-                            flutter::APP_TYPE_CM,
-                            flutter::APP_TYPE_DESKTOP_FILE_TRANSFER,
-                            flutter::APP_TYPE_DESKTOP_PORT_FORWARD,
-                        ];
+                        let available_channels = flutter::get_global_event_channels();
                         if available_channels.contains(&v[1]) {
                             let _res = flutter::push_global_event(v[1], make_event(&ui));
                         }
diff --git a/src/port_forward.rs b/src/port_forward.rs
index 62ba7b41c..6a087abe2 100644
--- a/src/port_forward.rs
+++ b/src/port_forward.rs
@@ -81,7 +81,7 @@ pub async fn listen(
                        });
                     }
                     Err(err) => {
-                        interface.msgbox("error", "Error", &err.to_string(), "");
+                        interface.on_establish_connection_error(err.to_string());
                     }
                     _ => {}
                 }
@@ -112,43 +112,6 @@ async fn connect_and_login(
     key: &str,
     token: &str,
     is_rdp: bool,
-) -> ResultType<Option<Stream>> {
-    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<Data>,
-    interface: impl Interface,
-    forward: &mut Framed<TcpStream, BytesCodec>,
-    key: &str,
-    token: &str,
-    is_rdp: bool,
 ) -> ResultType<Option<Stream>> {
     let conn_type = if is_rdp {
         ConnType::RDP
@@ -157,6 +120,7 @@ async fn connect_and_login_2(
     };
     let (mut stream, direct, _pk) =
         Client::start(id, key, token, conn_type, interface.clone()).await?;
+    interface.update_direct(Some(direct));
     let mut interface = interface;
     let mut buffer = Vec::new();
     let mut received = false;
@@ -167,7 +131,10 @@ async fn connect_and_login_2(
                     bail!("Timeout");
                 }
                 Ok(Some(Ok(bytes))) => {
-                    received = true;
+                    if !received {
+                        received = true;
+                        interface.update_received(true);
+                    }
                     let msg_in = Message::parse_from_bytes(&bytes)?;
                     match msg_in.union {
                         Some(message::Union::Hash(hash)) => {
@@ -191,8 +158,6 @@ async fn connect_and_login_2(
                     }
                 }
                 Ok(Some(Err(err))) => {
-                    log::error!("Connection closed: {}", err);
-                    interface.set_force_relay(direct, received);
                     bail!("Connection closed: {}", err);
                 }
                 _ => {
diff --git a/src/privacy_win_mag.rs b/src/privacy_win_mag.rs
index fe0ee4f69..05c2daf05 100644
--- a/src/privacy_win_mag.rs
+++ b/src/privacy_win_mag.rs
@@ -21,7 +21,7 @@ use winapi::{
         libloaderapi::{GetModuleHandleA, GetModuleHandleExA, GetProcAddress},
         memoryapi::{VirtualAllocEx, WriteProcessMemory},
         processthreadsapi::{
-            CreateProcessAsUserW, GetCurrentThreadId, QueueUserAPC, ResumeThread,
+            CreateProcessAsUserW, GetCurrentThreadId, QueueUserAPC, ResumeThread, TerminateProcess,
             PROCESS_INFORMATION, STARTUPINFOW,
         },
         winbase::{WTSGetActiveConsoleSessionId, CREATE_SUSPENDED, DETACHED_PROCESS},
@@ -46,7 +46,6 @@ pub const GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS: u32 = 4;
 const WM_USER_EXIT_HOOK: u32 = WM_USER + 1;
 
 lazy_static::lazy_static! {
-    static ref DLL_FOUND: Mutex<bool> = Mutex::new(false);
     static ref CONN_ID: Mutex<i32> = Mutex::new(0);
     static ref CUR_HOOK_THREAD_ID: Mutex<DWORD> = Mutex::new(0);
     static ref WND_HANDLERS: Mutex<WindowHandlers> = Mutex::new(WindowHandlers{hthread: 0, hprocess: 0});
@@ -59,17 +58,28 @@ struct WindowHandlers {
 
 impl Drop for WindowHandlers {
     fn drop(&mut self) {
+        self.reset();
+    }
+}
+
+impl WindowHandlers {
+    fn reset(&mut self) {
         unsafe {
+            if self.hprocess != 0 {
+                let _res = TerminateProcess(self.hprocess as _, 0);
+                CloseHandle(self.hprocess as _);
+            }
+            self.hprocess = 0;
             if self.hthread != 0 {
                 CloseHandle(self.hthread as _);
             }
             self.hthread = 0;
-            if self.hprocess != 0 {
-                CloseHandle(self.hprocess as _);
-            }
-            self.hprocess = 0;
         }
     }
+
+    fn is_default(&self) -> bool {
+        self.hthread == 0 && self.hprocess == 0
+    }
 }
 
 pub fn turn_on_privacy(conn_id: i32) -> ResultType<bool> {
@@ -85,7 +95,7 @@ pub fn turn_on_privacy(conn_id: i32) -> ResultType<bool> {
         );
     }
 
-    if !*DLL_FOUND.lock().unwrap() {
+    if WND_HANDLERS.lock().unwrap().is_default() {
         log::info!("turn_on_privacy, dll not found when started, try start");
         start()?;
         std::thread::sleep(std::time::Duration::from_millis(1_000));
@@ -143,10 +153,10 @@ pub fn start() -> ResultType<()> {
     }
 
     let exe_file = std::env::current_exe()?;
-    if exe_file.parent().is_none() {
+    let Some(cur_dir) = exe_file
+    .parent() else {
         bail!("Cannot get parent of current exe file");
-    }
-    let cur_dir = exe_file.parent().unwrap();
+    };
 
     let dll_file = cur_dir.join("WindowInjection.dll");
     if !dll_file.exists() {
@@ -156,8 +166,6 @@ pub fn start() -> ResultType<()> {
         );
     }
 
-    *DLL_FOUND.lock().unwrap() = true;
-
     let hwnd = wait_find_privacy_hwnd(1_000)?;
     if !hwnd.is_null() {
         log::info!("Privacy window is ready");
@@ -257,6 +265,11 @@ pub fn start() -> ResultType<()> {
     Ok(())
 }
 
+#[inline]
+pub fn stop() {
+    WND_HANDLERS.lock().unwrap().reset();
+}
+
 unsafe fn inject_dll<'a>(hproc: HANDLE, hthread: HANDLE, dll_file: &'a str) -> ResultType<()> {
     let dll_file_utf16: Vec<u16> = dll_file.encode_utf16().chain(Some(0).into_iter()).collect();
 
@@ -331,6 +344,7 @@ async fn set_privacy_mode_state(
 }
 
 pub(super) mod privacy_hook {
+
     use super::*;
     use std::sync::mpsc::{channel, Sender};
 
@@ -413,7 +427,7 @@ pub(super) mod privacy_hook {
                     }
                     Err(e) => {
                         // Fatal error
-                        tx.send(format!("Unexpected err when hook {}", e)).unwrap();
+                        allow_err!(tx.send(format!("Unexpected err when hook {}", e)));
                         return;
                     }
                 }
diff --git a/src/rendezvous_mediator.rs b/src/rendezvous_mediator.rs
index 8b43d86a5..924c0c709 100644
--- a/src/rendezvous_mediator.rs
+++ b/src/rendezvous_mediator.rs
@@ -52,6 +52,7 @@ impl RendezvousMediator {
     }
 
     pub async fn start_all() {
+        crate::hbbs_http::sync::start();
         let mut nat_tested = false;
         check_zombie();
         let server = new_server();
@@ -72,6 +73,7 @@ impl RendezvousMediator {
                 allow_err!(super::lan::start_listening());
             });
         }
+        // It is ok to run xdesktop manager when the headless function is not allowed.
         #[cfg(all(target_os = "linux", feature = "linux_headless"))]
         #[cfg(not(any(feature = "flatpak", feature = "appimage")))]
         crate::platform::linux_desktop_manager::start_xdesktop();
@@ -88,7 +90,9 @@ impl RendezvousMediator {
                 for host in servers.clone() {
                     let server = server.clone();
                     futs.push(tokio::spawn(async move {
-                        allow_err!(Self::start(server, host).await);
+                        if let Err(err) = Self::start(server, host).await {
+                            log::error!("rendezvous mediator error: {err}");
+                        }
                         // SHOULD_EXIT here is to ensure once one exits, the others also exit.
                         SHOULD_EXIT.store(true, Ordering::SeqCst);
                     }));
@@ -183,16 +187,18 @@ impl RendezvousMediator {
                                     }
                                     Some(rendezvous_message::Union::RegisterPkResponse(rpr)) => {
                                         update_latency();
-                                        match rpr.result.enum_value_or_default() {
-                                            register_pk_response::Result::OK => {
+                                        match rpr.result.enum_value() {
+                                            Ok(register_pk_response::Result::OK) => {
                                                 Config::set_key_confirmed(true);
                                                 Config::set_host_key_confirmed(&rz.host_prefix, true);
                                                 *SOLVING_PK_MISMATCH.lock().unwrap() = "".to_owned();
                                             }
-                                            register_pk_response::Result::UUID_MISMATCH => {
+                                            Ok(register_pk_response::Result::UUID_MISMATCH) => {
                                                 allow_err!(rz.handle_uuid_mismatch(&mut socket).await);
                                             }
-                                            _ => {}
+                                            _ => {
+                                                log::error!("unknown RegisterPkResponse");
+                                            }
                                         }
                                     }
                                     Some(rendezvous_message::Union::PunchHole(ph)) => {
@@ -373,7 +379,7 @@ impl RendezvousMediator {
 
     async fn handle_punch_hole(&self, ph: PunchHole, server: ServerPtr) -> ResultType<()> {
         let relay_server = self.get_relay_server(ph.relay_server);
-        if ph.nat_type.enum_value_or_default() == NatType::SYMMETRIC
+        if ph.nat_type.enum_value() == Ok(NatType::SYMMETRIC)
             || Config::get_nat_type() == NatType::SYMMETRIC as i32
         {
             let uuid = Uuid::new_v4().to_string();
@@ -503,7 +509,8 @@ async fn direct_server(server: ServerPtr) {
     let mut listener = None;
     let mut port = 0;
     loop {
-        let disabled = Config::get_option("direct-server").is_empty();
+        let disabled = Config::get_option("direct-server").is_empty()
+            || !Config::get_option("stop-service").is_empty();
         if !disabled && listener.is_none() {
             port = get_direct_port();
             match hbb_common::tcp::listen_any(port as _).await {
@@ -511,7 +518,7 @@ async fn direct_server(server: ServerPtr) {
                     listener = Some(l);
                     log::info!(
                         "Direct server listening on: {:?}",
-                        listener.as_ref().unwrap().local_addr()
+                        listener.as_ref().map(|l| l.local_addr())
                     );
                 }
                 Err(err) => {
diff --git a/src/server.rs b/src/server.rs
index 545f81e3c..c296e60be 100644
--- a/src/server.rs
+++ b/src/server.rs
@@ -72,6 +72,7 @@ lazy_static::lazy_static! {
     // for all initiative connections.
     //
     // [Note]
+    // ugly
     // Now we use this [`CLIENT_SERVER`] to do following operations:
     // - record local audio, and send to remote
     pub static ref CLIENT_SERVER: ServerPtr = new();
@@ -90,7 +91,7 @@ pub fn new() -> ServerPtr {
     let mut server = Server {
         connections: HashMap::new(),
         services: HashMap::new(),
-        id_count: 0,
+        id_count: hbb_common::rand::random::<i32>() % 1000 + 1000, // ensure positive
     };
     server.add_service(Box::new(audio_service::new()));
     server.add_service(Box::new(video_service::new()));
@@ -128,11 +129,7 @@ pub async fn create_tcp_connection(
     secure: bool,
 ) -> ResultType<()> {
     let mut stream = stream;
-    let id = {
-        let mut w = server.write().unwrap();
-        w.id_count += 1;
-        w.id_count
-    };
+    let id = server.write().unwrap().get_new_id();
     let (sk, pk) = Config::get_key_pair();
     if secure && pk.len() == sign::PUBLICKEYBYTES && sk.len() == sign::SECRETKEYBYTES {
         let mut sk_ = [0u8; sign::SECRETKEYBYTES];
@@ -305,9 +302,8 @@ impl Server {
 
     // get a new unique id
     pub fn get_new_id(&mut self) -> i32 {
-        let new_id = self.id_count;
         self.id_count += 1;
-        new_id
+        self.id_count
     }
 }
 
@@ -367,13 +363,9 @@ pub async fn start_server(is_server: bool) {
         log::info!("XAUTHORITY={:?}", std::env::var("XAUTHORITY"));
     }
     #[cfg(feature = "hwcodec")]
-    {
-        use std::sync::Once;
-        static ONCE: Once = Once::new();
-        ONCE.call_once(|| {
-            scrap::hwcodec::check_config_process();
-        })
-    }
+    scrap::hwcodec::check_config_process();
+    #[cfg(windows)]
+    hbb_common::platform::windows::start_cpu_performance_monitor();
 
     if is_server {
         crate::common::set_server_running(true);
@@ -383,16 +375,15 @@ pub async fn start_server(is_server: bool) {
                 std::process::exit(-1);
             }
         });
-        #[cfg(windows)]
-        crate::platform::windows::bootstrap();
         input_service::fix_key_down_timeout_loop();
-        crate::hbbs_http::sync::start();
         #[cfg(target_os = "linux")]
         if crate::platform::current_is_wayland() {
             allow_err!(input_service::setup_uinput(0, 1920, 0, 1080).await);
         }
         #[cfg(any(target_os = "macos", target_os = "linux"))]
         tokio::spawn(async { sync_and_watch_config_dir().await });
+        #[cfg(target_os = "windows")]
+        crate::platform::try_kill_broker();
         crate::RendezvousMediator::start_all().await;
     } else {
         match crate::ipc::connect(1000, "").await {
diff --git a/src/server/audio_service.rs b/src/server/audio_service.rs
index 5a2184fe4..ac3cd6b95 100644
--- a/src/server/audio_service.rs
+++ b/src/server/audio_service.rs
@@ -13,10 +13,10 @@
 // https://github.com/krruzic/pulsectl
 
 use super::*;
-use magnum_opus::{Application::*, Channels::*, Encoder};
-use std::sync::atomic::{AtomicBool, Ordering};
 #[cfg(not(any(target_os = "linux", target_os = "android")))]
 use hbb_common::anyhow::anyhow;
+use magnum_opus::{Application::*, Channels::*, Encoder};
+use std::sync::atomic::{AtomicBool, Ordering};
 
 pub const NAME: &'static str = "audio";
 pub const AUDIO_DATA_SIZE_U8: usize = 960 * 4; // 10ms in 48000 stereo
@@ -206,13 +206,10 @@ mod cpal_impl {
                 }
             }
         }
-        if device.is_none() {
-            device = Some(
-                HOST.default_input_device()
-                    .with_context(|| "Failed to get default input device for loopback")?,
-            );
-        }
-        let device = device.unwrap();
+        let device = device.unwrap_or(
+            HOST.default_input_device()
+                .with_context(|| "Failed to get default input device for loopback")?,
+        );
         log::info!("Input device: {}", device.name().unwrap_or("".to_owned()));
         let format = device
             .default_input_config()
diff --git a/src/server/connection.rs b/src/server/connection.rs
index f747e47c1..c9f07eb78 100644
--- a/src/server/connection.rs
+++ b/src/server/connection.rs
@@ -39,7 +39,7 @@ use hbb_common::{
     tokio_util::codec::{BytesCodec, Framed},
 };
 #[cfg(any(target_os = "android", target_os = "ios"))]
-use scrap::android::call_main_service_mouse_input;
+use scrap::android::call_main_service_pointer_input;
 use serde_json::{json, value::Value};
 use sha2::{Digest, Sha256};
 #[cfg(not(any(target_os = "android", target_os = "ios")))]
@@ -115,6 +115,8 @@ enum MessageInput {
     Mouse((MouseEvent, i32)),
     #[cfg(not(any(target_os = "android", target_os = "ios")))]
     Key((KeyEvent, bool)),
+    #[cfg(not(any(target_os = "android", target_os = "ios")))]
+    Pointer((PointerDeviceEvent, i32)),
     BlockOn,
     BlockOff,
     #[cfg(all(feature = "flutter", feature = "plugin_framework"))]
@@ -133,6 +135,14 @@ struct Session {
     random_password: String,
 }
 
+#[cfg(not(any(target_os = "android", target_os = "ios")))]
+struct StartCmIpcPara {
+    rx_to_cm: mpsc::UnboundedReceiver<ipc::Data>,
+    tx_from_cm: mpsc::UnboundedSender<ipc::Data>,
+    rx_desktop_ready: mpsc::Receiver<()>,
+    tx_cm_stream_ready: mpsc::Sender<()>,
+}
+
 pub struct Connection {
     inner: ConnInner,
     stream: super::Stream,
@@ -153,6 +163,7 @@ pub struct Connection {
     restart: bool,
     recording: bool,
     last_test_delay: i64,
+    network_delay: Option<u32>,
     lock_after_session_end: bool,
     show_remote_cursor: bool,
     // by peer
@@ -172,12 +183,12 @@ pub struct Connection {
     tx_input: std_mpsc::Sender<MessageInput>,
     // handle input messages
     video_ack_required: bool,
-    peer_info: (String, String),
     server_audit_conn: String,
     server_audit_file: String,
     lr: LoginRequest,
     last_recv_time: Arc<Mutex<Instant>>,
     chat_unanswered: bool,
+    file_transferred: bool,
     #[cfg(windows)]
     portable: PortableState,
     from_switch: bool,
@@ -188,10 +199,11 @@ pub struct Connection {
     pressed_modifiers: HashSet<rdev::Key>,
     #[cfg(all(target_os = "linux", feature = "linux_headless"))]
     #[cfg(not(any(feature = "flatpak", feature = "appimage")))]
-    rx_cm_stream_ready: mpsc::Receiver<()>,
-    #[cfg(all(target_os = "linux", feature = "linux_headless"))]
-    #[cfg(not(any(feature = "flatpak", feature = "appimage")))]
-    tx_desktop_ready: mpsc::Sender<()>,
+    linux_headless_handle: LinuxHeadlessHandle,
+    closed: bool,
+    delay_response_instant: Instant,
+    #[cfg(not(any(target_os = "android", target_os = "ios")))]
+    start_cm_ipc_para: Option<StartCmIpcPara>,
 }
 
 impl ConnInner {
@@ -261,6 +273,10 @@ impl Connection {
         let (tx_cm_stream_ready, _rx_cm_stream_ready) = mpsc::channel(1);
         #[cfg(not(any(target_os = "android", target_os = "ios")))]
         let (_tx_desktop_ready, rx_desktop_ready) = mpsc::channel(1);
+        #[cfg(all(target_os = "linux", feature = "linux_headless"))]
+        #[cfg(not(any(feature = "flatpak", feature = "appimage")))]
+        let linux_headless_handle =
+            LinuxHeadlessHandle::new(_rx_cm_stream_ready, _tx_desktop_ready);
 
         #[cfg(not(any(target_os = "android", target_os = "ios")))]
         let tx_cloned = tx.clone();
@@ -289,6 +305,7 @@ impl Connection {
             restart: Connection::permission("enable-remote-restart"),
             recording: Connection::permission("enable-record-session"),
             last_test_delay: 0,
+            network_delay: None,
             lock_after_session_end: false,
             show_remote_cursor: false,
             ip: "".to_owned(),
@@ -299,12 +316,12 @@ impl Connection {
             disable_keyboard: false,
             tx_input,
             video_ack_required: false,
-            peer_info: Default::default(),
             server_audit_conn: "".to_owned(),
             server_audit_file: "".to_owned(),
             lr: Default::default(),
             last_recv_time: Arc::new(Mutex::new(Instant::now())),
             chat_unanswered: false,
+            file_transferred: false,
             #[cfg(windows)]
             portable: Default::default(),
             from_switch: false,
@@ -316,24 +333,24 @@ impl Connection {
             pressed_modifiers: Default::default(),
             #[cfg(all(target_os = "linux", feature = "linux_headless"))]
             #[cfg(not(any(feature = "flatpak", feature = "appimage")))]
-            rx_cm_stream_ready: _rx_cm_stream_ready,
-            #[cfg(all(target_os = "linux", feature = "linux_headless"))]
-            #[cfg(not(any(feature = "flatpak", feature = "appimage")))]
-            tx_desktop_ready: _tx_desktop_ready,
+            linux_headless_handle,
+            closed: false,
+            delay_response_instant: Instant::now(),
+            #[cfg(not(any(target_os = "android", target_os = "ios")))]
+            start_cm_ipc_para: Some(StartCmIpcPara {
+                rx_to_cm,
+                tx_from_cm,
+                rx_desktop_ready,
+                tx_cm_stream_ready,
+            }),
         };
+        let addr = hbb_common::try_into_v4(addr);
         if !conn.on_open(addr).await {
+            conn.closed = true;
             // sleep to ensure msg got received.
             sleep(1.).await;
             return;
         }
-        #[cfg(not(any(target_os = "android", target_os = "ios")))]
-        tokio::spawn(async move {
-            if let Err(err) =
-                start_ipc(rx_to_cm, tx_from_cm, rx_desktop_ready, tx_cm_stream_ready).await
-            {
-                log::error!("ipc to connection manager exit: {}", err);
-            }
-        });
         #[cfg(target_os = "android")]
         start_channel(rx_to_cm, tx_from_cm);
         if !conn.keyboard {
@@ -384,6 +401,7 @@ impl Connection {
                         }
                         ipc::Data::Close => {
                             conn.chat_unanswered = false; // seen
+                            conn.file_transferred = false; //seen
                             conn.send_close_reason_no_retry("").await;
                             conn.on_close("connection manager", true).await;
                             break;
@@ -521,9 +539,17 @@ impl Connection {
                 },
                 _ = conn.file_timer.tick() => {
                     if !conn.read_jobs.is_empty() {
-                        if let Err(err) = fs::handle_read_jobs(&mut conn.read_jobs, &mut conn.stream).await {
-                            conn.on_close(&err.to_string(), false).await;
-                            break;
+                        conn.send_to_cm(ipc::Data::FileTransferLog(fs::serialize_transfer_jobs(&conn.read_jobs)));
+                        match fs::handle_read_jobs(&mut conn.read_jobs, &mut conn.stream).await {
+                            Ok(log) => {
+                                if !log.is_empty() {
+                                    conn.send_to_cm(ipc::Data::FileTransferLog(log));
+                                }
+                            }
+                            Err(err) =>  {
+                                conn.on_close(&err.to_string(), false).await;
+                                break;
+                            }
                         }
                     } else {
                         conn.file_timer = time::interval_at(Instant::now() + SEC30, SEC30);
@@ -563,7 +589,7 @@ impl Connection {
                             match &m.union {
                                 Some(misc::Union::StopService(_)) => {
                                     conn.send_close_reason_no_retry("").await;
-                                    conn.on_close("stop service", true).await;
+                                    conn.on_close("stop service", false).await;
                                     break;
                                 }
                                 _ => {},
@@ -586,18 +612,19 @@ impl Connection {
                         break;
                     }
                     let time = get_time();
+                    let mut qos = video_service::VIDEO_QOS.lock().unwrap();
                     if time > 0 && conn.last_test_delay == 0 {
                         conn.last_test_delay = time;
                         let mut msg_out = Message::new();
-                        let qos = video_service::VIDEO_QOS.lock().unwrap();
                         msg_out.set_test_delay(TestDelay{
                             time,
-                            last_delay:qos.current_delay,
-                            target_bitrate:qos.target_bitrate,
+                            last_delay:conn.network_delay.unwrap_or_default(),
+                            target_bitrate: qos.bitrate(),
                             ..Default::default()
                         });
                         conn.inner.send(msg_out.into());
                     }
+                    qos.user_delay_response_elapsed(conn.inner.id(), conn.delay_response_instant.elapsed().as_millis());
                 }
             }
         }
@@ -617,7 +644,6 @@ impl Connection {
         );
         video_service::notify_video_frame_fetched(id, None);
         scrap::codec::Encoder::update(id, scrap::codec::EncodingUpdate::Remove);
-        video_service::VIDEO_QOS.lock().unwrap().reset();
         if conn.authorized {
             password::update_temporary_password();
         }
@@ -634,6 +660,7 @@ impl Connection {
             #[cfg(not(any(target_os = "android", target_os = "ios")))]
             try_stop_record_cursor_pos();
         }
+        conn.on_close("End", true).await;
         log::info!("#{} connection loop exited", id);
     }
 
@@ -655,15 +682,18 @@ impl Connection {
                     }
                     MessageInput::Key((mut msg, press)) => {
                         // todo: press and down have similar meanings.
-                        if press && msg.mode.unwrap() == KeyboardMode::Legacy {
+                        if press && msg.mode.enum_value() == Ok(KeyboardMode::Legacy) {
                             msg.down = true;
                         }
                         handle_key(&msg);
-                        if press && msg.mode.unwrap() == KeyboardMode::Legacy {
+                        if press && msg.mode.enum_value() == Ok(KeyboardMode::Legacy) {
                             msg.down = false;
                             handle_key(&msg);
                         }
                     }
+                    MessageInput::Pointer((msg, id)) => {
+                        handle_pointer(&msg, id);
+                    }
                     MessageInput::BlockOn => {
                         if crate::platform::block_input(true) {
                             block_input_mode = true;
@@ -867,6 +897,7 @@ impl Connection {
         v["id"] = json!(Config::get_id());
         v["uuid"] = json!(crate::encode64(hbb_common::get_uuid()));
         v["conn_id"] = json!(self.inner.id);
+        v["session_id"] = json!(self.lr.session_id);
         tokio::spawn(async move {
             allow_err!(Self::post_audit_async(url, v).await);
         });
@@ -942,13 +973,14 @@ impl Connection {
         } else {
             0
         };
-        self.post_conn_audit(json!({"peer": self.peer_info, "type": conn_type}));
+        self.post_conn_audit(
+            json!({"peer": ((&self.lr.my_id, &self.lr.my_name)), "type": conn_type}),
+        );
         #[allow(unused_mut)]
         let mut username = crate::platform::get_active_username();
         let mut res = LoginResponse::new();
         let mut pi = PeerInfo {
             username: username.clone(),
-            conn_id: self.inner.id,
             version: VERSION.to_owned(),
             ..Default::default()
         };
@@ -971,8 +1003,10 @@ impl Connection {
             }
             #[cfg(feature = "linux_headless")]
             #[cfg(not(any(feature = "flatpak", feature = "appimage")))]
-            if linux_desktop_manager::is_headless() {
-                platform_additions.insert("headless".into(), json!(true));
+            if crate::platform::is_headless_allowed() {
+                if linux_desktop_manager::is_headless() {
+                    platform_additions.insert("headless".into(), json!(true));
+                }
             }
             if !platform_additions.is_empty() {
                 pi.platform_additions =
@@ -995,10 +1029,15 @@ impl Connection {
             if dtype != crate::platform::linux::DISPLAY_SERVER_X11
                 && dtype != crate::platform::linux::DISPLAY_SERVER_WAYLAND
             {
-                res.set_error(format!(
-                    "Unsupported display server type \"{}\", x11 or wayland expected",
-                    dtype
-                ));
+                let msg = if crate::platform::linux::is_login_screen_wayland() {
+                    crate::client::LOGIN_SCREEN_WAYLAND.to_owned()
+                } else {
+                    format!(
+                        "Unsupported display server type \"{}\", x11 or wayland expected",
+                        dtype
+                    )
+                };
+                res.set_error(msg);
                 let mut msg_out = Message::new();
                 msg_out.set_login_response(res);
                 self.send(msg_out).await;
@@ -1032,13 +1071,15 @@ impl Connection {
             ..Default::default()
         })
         .into();
+        // `try_reset_current_display` is needed because `get_displays` may change the current display,
+        // which may cause the mismatch of current display and the current display name.
         #[cfg(not(any(target_os = "android", target_os = "ios")))]
         video_service::try_reset_current_display();
         #[cfg(not(any(target_os = "android", target_os = "ios")))]
         {
             pi.resolutions = Some(SupportedResolutions {
-                resolutions: video_service::get_current_display_name()
-                    .map(|name| crate::platform::resolutions(&name))
+                resolutions: video_service::get_current_display()
+                    .map(|(_, _, d)| crate::platform::resolutions(&d.name()))
                     .unwrap_or(vec![]),
                 ..Default::default()
             })
@@ -1121,7 +1162,6 @@ impl Connection {
     }
 
     fn try_start_cm(&mut self, peer_id: String, name: String, authorized: bool) {
-        self.peer_info = (peer_id.clone(), name.clone());
         self.send_to_cm(ipc::Data::Login {
             id: self.inner.id(),
             is_file_transfer: self.file_transfer.is_some(),
@@ -1175,6 +1215,14 @@ impl Connection {
         self.tx_input.send(MessageInput::Mouse((msg, conn_id))).ok();
     }
 
+    #[inline]
+    #[cfg(not(any(target_os = "android", target_os = "ios")))]
+    fn input_pointer(&self, msg: PointerDeviceEvent, conn_id: i32) {
+        self.tx_input
+            .send(MessageInput::Pointer((msg, conn_id)))
+            .ok();
+    }
+
     #[inline]
     #[cfg(not(any(target_os = "android", target_os = "ios")))]
     fn input_key(&self, msg: KeyEvent, press: bool) {
@@ -1228,6 +1276,7 @@ impl Connection {
             .lock()
             .unwrap()
             .retain(|_, s| s.last_recv_time.lock().unwrap().elapsed() < SESSION_TIMEOUT);
+        // last_recv_time is a mutex variable shared with connection, can be updated lively.
         if let Some(session) = session {
             if session.name == self.lr.my_name
                 && session.session_id == self.lr.session_id
@@ -1287,6 +1336,24 @@ impl Connection {
         self.video_ack_required = lr.video_ack_required;
     }
 
+    #[cfg(not(any(target_os = "android", target_os = "ios")))]
+    fn try_start_cm_ipc(&mut self) {
+        if let Some(p) = self.start_cm_ipc_para.take() {
+            tokio::spawn(async move {
+                if let Err(err) = start_ipc(
+                    p.rx_to_cm,
+                    p.tx_from_cm,
+                    p.rx_desktop_ready,
+                    p.tx_cm_stream_ready,
+                )
+                .await
+                {
+                    log::error!("ipc to connection manager exit: {}", err);
+                }
+            });
+        }
+    }
+
     async fn on_message(&mut self, msg: Message) -> bool {
         if let Some(message::Union::LoginRequest(lr)) = msg.union {
             self.handle_login_request_without_validation(&lr).await;
@@ -1350,28 +1417,25 @@ impl Connection {
                 }
             }
 
+            #[cfg(not(any(target_os = "android", target_os = "ios")))]
+            self.try_start_cm_ipc();
+
+            #[cfg(any(
+                feature = "flatpak",
+                feature = "appimage",
+                not(all(target_os = "linux", feature = "linux_headless"))
+            ))]
+            let err_msg = "".to_owned();
             #[cfg(all(target_os = "linux", feature = "linux_headless"))]
             #[cfg(not(any(feature = "flatpak", feature = "appimage")))]
-            let desktop_err = match lr.os_login.as_ref() {
-                Some(os_login) => {
-                    linux_desktop_manager::try_start_desktop(&os_login.username, &os_login.password)
-                }
-                None => linux_desktop_manager::try_start_desktop("", ""),
-            };
-            #[cfg(all(target_os = "linux", feature = "linux_headless"))]
-            #[cfg(not(any(feature = "flatpak", feature = "appimage")))]
-            let is_headless = linux_desktop_manager::is_headless();
-            #[cfg(all(target_os = "linux", feature = "linux_headless"))]
-            #[cfg(not(any(feature = "flatpak", feature = "appimage")))]
-            let wait_ipc_timeout = 10_000;
+            let err_msg = self
+                .linux_headless_handle
+                .try_start_desktop(lr.os_login.as_ref());
 
             // If err is LOGIN_MSG_DESKTOP_SESSION_NOT_READY, just keep this msg and go on checking password.
-            #[cfg(all(target_os = "linux", feature = "linux_headless"))]
-            #[cfg(not(any(feature = "flatpak", feature = "appimage")))]
-            if !desktop_err.is_empty()
-                && desktop_err != crate::client::LOGIN_MSG_DESKTOP_SESSION_NOT_READY
+            if !err_msg.is_empty() && err_msg != crate::client::LOGIN_MSG_DESKTOP_SESSION_NOT_READY
             {
-                self.send_login_error(desktop_err).await;
+                self.send_login_error(err_msg).await;
                 return true;
             }
 
@@ -1399,34 +1463,20 @@ impl Connection {
                 self.send_login_error("Connection not allowed").await;
                 return false;
             } else if self.is_recent_session() {
-                #[cfg(all(target_os = "linux", feature = "linux_headless"))]
-                #[cfg(not(any(feature = "flatpak", feature = "appimage")))]
-                if desktop_err.is_empty() {
-                    #[cfg(target_os = "linux")]
-                    if is_headless {
-                        self.tx_desktop_ready.send(()).await.ok();
-                        let _res = timeout(wait_ipc_timeout, self.rx_cm_stream_ready.recv()).await;
-                    }
-                    self.try_start_cm(lr.my_id, lr.my_name, true);
+                if err_msg.is_empty() {
+                    #[cfg(all(target_os = "linux", feature = "linux_headless"))]
+                    #[cfg(not(any(feature = "flatpak", feature = "appimage")))]
+                    self.linux_headless_handle.wait_desktop_cm_ready().await;
+                    self.try_start_cm(lr.my_id.clone(), lr.my_name.clone(), true);
                     self.send_logon_response().await;
                     if self.port_forward_socket.is_some() {
                         return false;
                     }
                 } else {
-                    self.send_login_error(desktop_err).await;
-                }
-                #[cfg(not(all(target_os = "linux", feature = "linux_headless")))]
-                {
-                    self.try_start_cm(lr.my_id, lr.my_name, true);
-                    self.send_logon_response().await;
-                    if self.port_forward_socket.is_some() {
-                        return false;
-                    }
+                    self.send_login_error(err_msg).await;
                 }
             } else if lr.password.is_empty() {
-                #[cfg(all(target_os = "linux", feature = "linux_headless"))]
-                #[cfg(not(any(feature = "flatpak", feature = "appimage")))]
-                if desktop_err.is_empty() {
+                if err_msg.is_empty() {
                     self.try_start_cm(lr.my_id, lr.my_name, false);
                 } else {
                     self.send_login_error(
@@ -1434,8 +1484,6 @@ impl Connection {
                     )
                     .await;
                 }
-                #[cfg(not(all(target_os = "linux", feature = "linux_headless")))]
-                self.try_start_cm(lr.my_id, lr.my_name, false);
             } else {
                 let mut failure = LOGIN_FAILURES
                     .lock()
@@ -1448,17 +1496,21 @@ impl Connection {
                     self.send_login_error("Too many wrong password attempts")
                         .await;
                     Self::post_alarm_audit(
-                        AlarmAuditType::ManyWrongPassword,
+                        AlarmAuditType::ExceedThirtyAttempts,
                         json!({
                                     "ip":self.ip,
+                                    "id":lr.my_id.clone(),
+                                    "name": lr.my_name.clone(),
                         }),
                     );
                 } else if time == failure.0 && failure.1 > 6 {
                     self.send_login_error("Please try 1 minute later").await;
                     Self::post_alarm_audit(
-                        AlarmAuditType::FrequentAttempt,
+                        AlarmAuditType::SixAttemptsWithinOneMinute,
                         json!({
                                     "ip":self.ip,
+                                    "id":lr.my_id.clone(),
+                                    "name": lr.my_name.clone(),
                         }),
                     );
                 } else if !self.validate_password() {
@@ -1474,9 +1526,7 @@ impl Connection {
                         .lock()
                         .unwrap()
                         .insert(self.ip.clone(), failure);
-                    #[cfg(all(target_os = "linux", feature = "linux_headless"))]
-                    #[cfg(not(any(feature = "flatpak", feature = "appimage")))]
-                    if desktop_err.is_empty() {
+                    if err_msg.is_empty() {
                         self.send_login_error(crate::client::LOGIN_MSG_PASSWORD_WRONG)
                             .await;
                         self.try_start_cm(lr.my_id, lr.my_name, false);
@@ -1486,40 +1536,21 @@ impl Connection {
                         )
                         .await;
                     }
-                    #[cfg(not(all(target_os = "linux", feature = "linux_headless")))]
-                    {
-                        self.send_login_error(crate::client::LOGIN_MSG_PASSWORD_WRONG)
-                            .await;
-                        self.try_start_cm(lr.my_id, lr.my_name, false);
-                    }
                 } else {
                     if failure.0 != 0 {
                         LOGIN_FAILURES.lock().unwrap().remove(&self.ip);
                     }
-                    #[cfg(all(target_os = "linux", feature = "linux_headless"))]
-                    #[cfg(not(any(feature = "flatpak", feature = "appimage")))]
-                    if desktop_err.is_empty() {
-                        #[cfg(target_os = "linux")]
-                        if is_headless {
-                            self.tx_desktop_ready.send(()).await.ok();
-                            let _res =
-                                timeout(wait_ipc_timeout, self.rx_cm_stream_ready.recv()).await;
-                        }
+                    if err_msg.is_empty() {
+                        #[cfg(all(target_os = "linux", feature = "linux_headless"))]
+                        #[cfg(not(any(feature = "flatpak", feature = "appimage")))]
+                        self.linux_headless_handle.wait_desktop_cm_ready().await;
                         self.send_logon_response().await;
                         self.try_start_cm(lr.my_id, lr.my_name, true);
                         if self.port_forward_socket.is_some() {
                             return false;
                         }
                     } else {
-                        self.send_login_error(desktop_err).await;
-                    }
-                    #[cfg(not(all(target_os = "linux", feature = "linux_headless")))]
-                    {
-                        self.send_logon_response().await;
-                        self.try_start_cm(lr.my_id, lr.my_name, true);
-                        if self.port_forward_socket.is_some() {
-                            return false;
-                        }
+                        self.send_login_error(err_msg).await;
                     }
                 }
             }
@@ -1534,7 +1565,9 @@ impl Connection {
                 video_service::VIDEO_QOS
                     .lock()
                     .unwrap()
-                    .update_network_delay(new_delay);
+                    .user_network_delay(self.inner.id(), new_delay);
+                self.network_delay = Some(new_delay);
+                self.delay_response_instant = Instant::now();
             }
         } else if let Some(message::Union::SwitchSidesResponse(_s)) = msg.union {
             #[cfg(feature = "flutter")]
@@ -1551,6 +1584,8 @@ impl Connection {
                             self.from_switch = true;
                             self.try_start_cm(lr.my_id.clone(), lr.my_name.clone(), true);
                             self.send_logon_response().await;
+                            #[cfg(not(any(target_os = "android", target_os = "ios")))]
+                            self.try_start_cm_ipc();
                         }
                     }
                 }
@@ -1559,8 +1594,8 @@ impl Connection {
             match msg.union {
                 Some(message::Union::MouseEvent(me)) => {
                     #[cfg(any(target_os = "android", target_os = "ios"))]
-                    if let Err(e) = call_main_service_mouse_input(me.mask, me.x, me.y) {
-                        log::debug!("call_main_service_mouse_input fail:{}", e);
+                    if let Err(e) = call_main_service_pointer_input("mouse", me.mask, me.x, me.y) {
+                        log::debug!("call_main_service_pointer_input fail:{}", e);
                     }
                     #[cfg(not(any(target_os = "android", target_os = "ios")))]
                     if self.peer_keyboard_enabled() {
@@ -1572,6 +1607,41 @@ impl Connection {
                         self.input_mouse(me, self.inner.id());
                     }
                 }
+                Some(message::Union::PointerDeviceEvent(pde)) => {
+                    #[cfg(any(target_os = "android", target_os = "ios"))]
+                    if let Err(e) = match pde.union {
+                        Some(pointer_device_event::Union::TouchEvent(touch)) => match touch.union {
+                            Some(touch_event::Union::PanStart(pan_start)) => {
+                                call_main_service_pointer_input(
+                                    "touch",
+                                    4,
+                                    pan_start.x,
+                                    pan_start.y,
+                                )
+                            }
+                            Some(touch_event::Union::PanUpdate(pan_update)) => {
+                                call_main_service_pointer_input(
+                                    "touch",
+                                    5,
+                                    pan_update.x,
+                                    pan_update.y,
+                                )
+                            }
+                            Some(touch_event::Union::PanEnd(pan_end)) => {
+                                call_main_service_pointer_input("touch", 6, pan_end.x, pan_end.y)
+                            }
+                            _ => Ok(()),
+                        },
+                        _ => Ok(()),
+                    } {
+                        log::debug!("call_main_service_pointer_input fail:{}", e);
+                    }
+                    #[cfg(not(any(target_os = "android", target_os = "ios")))]
+                    if self.peer_keyboard_enabled() {
+                        MOUSE_MOVE_TIME.store(get_time(), Ordering::SeqCst);
+                        self.input_pointer(pde, self.inner.id());
+                    }
+                }
                 #[cfg(any(target_os = "android", target_os = "ios"))]
                 Some(message::Union::KeyEvent(..)) => {}
                 #[cfg(not(any(target_os = "android", target_os = "ios")))]
@@ -1589,11 +1659,11 @@ impl Connection {
                             me.press
                         };
 
-                        let key = match me.mode.enum_value_or_default() {
-                            KeyboardMode::Map => {
+                        let key = match me.mode.enum_value() {
+                            Ok(KeyboardMode::Map) => {
                                 Some(crate::keyboard::keycode_to_rdev_key(me.chr()))
                             }
-                            KeyboardMode::Translate => {
+                            Ok(KeyboardMode::Translate) => {
                                 if let Some(key_event::Union::Chr(code)) = me.union {
                                     Some(crate::keyboard::keycode_to_rdev_key(code & 0x0000FFFF))
                                 } else {
@@ -1658,6 +1728,7 @@ impl Connection {
                                 }
                             }
                             Some(file_action::Union::Send(s)) => {
+                                // server to client
                                 let id = s.id;
                                 let od = can_enable_overwrite_detection(get_version_number(
                                     &self.lr.version,
@@ -1675,10 +1746,12 @@ impl Connection {
                                     Err(err) => {
                                         self.send(fs::new_error(id, err, 0)).await;
                                     }
-                                    Ok(job) => {
+                                    Ok(mut job) => {
                                         self.send(fs::new_dir(id, path, job.files().to_vec()))
                                             .await;
                                         let mut files = job.files().to_owned();
+                                        job.is_remote = true;
+                                        job.conn_id = self.inner.id();
                                         self.read_jobs.push(job);
                                         self.file_timer = time::interval(MILLI1);
                                         self.post_file_audit(
@@ -1692,8 +1765,10 @@ impl Connection {
                                         );
                                     }
                                 }
+                                self.file_transferred = true;
                             }
                             Some(file_action::Union::Receive(r)) => {
+                                // client to server
                                 // note: 1.1.10 introduced identical file detection, which breaks original logic of send/recv files
                                 // whenever got send/recv request, check peer version to ensure old version of rustdesk
                                 let od = can_enable_overwrite_detection(get_version_number(
@@ -1710,6 +1785,8 @@ impl Connection {
                                         .map(|f| (f.name, f.modified_time))
                                         .collect(),
                                     overwrite_detection: od,
+                                    total_size: r.total_size,
+                                    conn_id: self.inner.id(),
                                 });
                                 self.post_file_audit(
                                     FileAuditType::RemoteReceive,
@@ -1721,6 +1798,7 @@ impl Connection {
                                         .collect(),
                                     json!({}),
                                 );
+                                self.file_transferred = true;
                             }
                             Some(file_action::Union::RemoveDir(d)) => {
                                 self.send_fs(ipc::FS::RemoveDir {
@@ -1744,6 +1822,11 @@ impl Connection {
                             }
                             Some(file_action::Union::Cancel(c)) => {
                                 self.send_fs(ipc::FS::CancelWrite { id: c.id });
+                                if let Some(job) = fs::get_job_immutable(c.id, &self.read_jobs) {
+                                    self.send_to_cm(ipc::Data::FileTransferLog(
+                                        fs::serialize_transfer_job(job, false, true, ""),
+                                    ));
+                                }
                                 fs::remove_job(c.id, &mut self.read_jobs);
                             }
                             Some(file_action::Union::SendConfirm(r)) => {
@@ -1832,42 +1915,17 @@ impl Connection {
                             }
                         }
                     }
+                    #[cfg(windows)]
                     Some(misc::Union::ElevationRequest(r)) => match r.union {
                         Some(elevation_request::Union::Direct(_)) => {
-                            #[cfg(windows)]
-                            {
-                                let mut err = "No need to elevate".to_string();
-                                if !crate::platform::is_installed() && !portable_client::running() {
-                                    err = portable_client::start_portable_service(
-                                        portable_client::StartPara::Direct,
-                                    )
-                                    .err()
-                                    .map_or("".to_string(), |e| e.to_string());
-                                }
-                                let mut misc = Misc::new();
-                                misc.set_elevation_response(err);
-                                let mut msg = Message::new();
-                                msg.set_misc(misc);
-                                self.send(msg).await;
-                            }
+                            self.handle_elevation_request(portable_client::StartPara::Direct)
+                                .await;
                         }
-                        Some(elevation_request::Union::Logon(_r)) => {
-                            #[cfg(windows)]
-                            {
-                                let mut err = "No need to elevate".to_string();
-                                if !crate::platform::is_installed() && !portable_client::running() {
-                                    err = portable_client::start_portable_service(
-                                        portable_client::StartPara::Logon(_r.username, _r.password),
-                                    )
-                                    .err()
-                                    .map_or("".to_string(), |e| e.to_string());
-                                }
-                                let mut misc = Misc::new();
-                                misc.set_elevation_response(err);
-                                let mut msg = Message::new();
-                                msg.set_misc(misc);
-                                self.send(msg).await;
-                            }
+                        Some(elevation_request::Union::Logon(r)) => {
+                            self.handle_elevation_request(portable_client::StartPara::Logon(
+                                r.username, r.password,
+                            ))
+                            .await;
                         }
                         _ => {}
                     },
@@ -1876,11 +1934,9 @@ impl Connection {
                             // Drop the audio sender previously.
                             drop(std::mem::replace(&mut self.audio_sender, None));
                             self.audio_sender = Some(start_audio_thread());
-                            allow_err!(self
-                                .audio_sender
+                            self.audio_sender
                                 .as_ref()
-                                .unwrap()
-                                .send(MediaData::AudioFormat(format)));
+                                .map(|a| allow_err!(a.send(MediaData::AudioFormat(format))));
                         }
                     }
                     #[cfg(feature = "flutter")]
@@ -1906,6 +1962,18 @@ impl Connection {
                             crate::plugin::handle_client_event(&p.id, &self.lr.my_id, &p.content);
                         self.send(msg).await;
                     }
+                    Some(misc::Union::FullSpeedFps(fps)) => video_service::VIDEO_QOS
+                        .lock()
+                        .unwrap()
+                        .user_full_speed_fps(self.inner.id(), fps),
+                    Some(misc::Union::AutoAdjustFps(fps)) => video_service::VIDEO_QOS
+                        .lock()
+                        .unwrap()
+                        .user_auto_adjust_fps(self.inner.id(), fps),
+                    Some(misc::Union::ClientRecordStatus(status)) => video_service::VIDEO_QOS
+                        .lock()
+                        .unwrap()
+                        .user_record(self.inner.id(), status),
                     _ => {}
                 },
                 Some(message::Union::AudioFrame(frame)) => {
@@ -1940,10 +2008,26 @@ impl Connection {
         true
     }
 
+    #[cfg(windows)]
+    async fn handle_elevation_request(&mut self, para: portable_client::StartPara) {
+        let mut err = "No need to elevate".to_string();
+        if !crate::platform::is_installed() && !portable_client::running() {
+            err = portable_client::start_portable_service(para)
+                .err()
+                .map_or("".to_string(), |e| e.to_string());
+        }
+        let mut misc = Misc::new();
+        misc.set_elevation_response(err);
+        let mut msg = Message::new();
+        msg.set_misc(misc);
+        self.send(msg).await;
+    }
+
     #[cfg(not(any(target_os = "android", target_os = "ios")))]
     fn change_resolution(&mut self, r: &Resolution) {
         if self.keyboard {
-            if let Ok(name) = video_service::get_current_display_name() {
+            if let Ok((_, _, display)) = video_service::get_current_display() {
+                let name = display.name();
                 #[cfg(all(windows, feature = "virtual_display_driver"))]
                 if let Some(_ok) =
                     crate::virtual_display_manager::change_resolution_if_is_virtual_display(
@@ -1954,6 +2038,11 @@ impl Connection {
                 {
                     return;
                 }
+                video_service::set_last_changed_resolution(
+                    &name,
+                    (display.width() as _, display.height() as _),
+                    (r.width, r.height),
+                );
                 if let Err(e) =
                     crate::platform::change_resolution(&name, r.width as _, r.height as _)
                 {
@@ -2020,14 +2109,14 @@ impl Connection {
                 video_service::VIDEO_QOS
                     .lock()
                     .unwrap()
-                    .update_image_quality(image_quality);
+                    .user_image_quality(self.inner.id(), image_quality);
             }
         }
         if o.custom_fps > 0 {
             video_service::VIDEO_QOS
                 .lock()
                 .unwrap()
-                .update_user_fps(o.custom_fps as _);
+                .user_custom_fps(self.inner.id(), o.custom_fps as _);
         }
         if let Some(q) = o.supported_decoding.clone().take() {
             scrap::codec::Encoder::update(self.inner.id(), scrap::codec::EncodingUpdate::New(q));
@@ -2179,13 +2268,17 @@ impl Connection {
     }
 
     async fn on_close(&mut self, reason: &str, lock: bool) {
+        if self.closed {
+            return;
+        }
+        self.closed = true;
         log::info!("#{} Connection closed: {}", self.inner.id(), reason);
         if lock && self.lock_after_session_end && self.keyboard {
             #[cfg(not(any(target_os = "android", target_os = "ios")))]
             lock_screen().await;
         }
         #[cfg(not(any(target_os = "android", target_os = "ios")))]
-        let data = if self.chat_unanswered {
+        let data = if self.chat_unanswered || self.file_transferred {
             ipc::Data::Disconnected
         } else {
             ipc::Data::Close
@@ -2301,6 +2394,8 @@ async fn start_ipc(
     mut _rx_desktop_ready: mpsc::Receiver<()>,
     tx_stream_ready: mpsc::Sender<()>,
 ) -> ResultType<()> {
+    use hbb_common::anyhow::anyhow;
+
     loop {
         if !crate::platform::is_prelogin() {
             break;
@@ -2312,23 +2407,17 @@ async fn start_ipc(
         stream = Some(s);
     } else {
         let mut args = vec!["--cm"];
-        if password::hide_cm() {
+        if crate::hbbs_http::sync::is_pro() && password::hide_cm() {
             args.push("--hide");
-        };
-
+        }
+        #[allow(unused_mut)]
         #[cfg(target_os = "linux")]
-        #[cfg(not(feature = "linux_headless"))]
-        let user = None;
-        #[cfg(all(target_os = "linux", feature = "linux_headless"))]
-        #[cfg(any(feature = "flatpak", feature = "appimage"))]
-        let user = None;
-        #[cfg(all(target_os = "linux", feature = "linux_headless"))]
-        #[cfg(not(any(feature = "flatpak", feature = "appimage")))]
         let mut user = None;
+
         // Cm run as user, wait until desktop session is ready.
         #[cfg(all(target_os = "linux", feature = "linux_headless"))]
         #[cfg(not(any(feature = "flatpak", feature = "appimage")))]
-        if linux_desktop_manager::is_headless() {
+        if crate::platform::is_headless_allowed() && linux_desktop_manager::is_headless() {
             let mut username = linux_desktop_manager::get_username();
             loop {
                 if !username.is_empty() {
@@ -2394,7 +2483,7 @@ async fn start_ipc(
     }
 
     let _res = tx_stream_ready.send(()).await;
-    let mut stream = stream.unwrap();
+    let mut stream = stream.ok_or(anyhow!("none stream"))?;
     loop {
         tokio::select! {
             res = stream.next() => {
@@ -2491,8 +2580,8 @@ mod privacy_mode {
 
 pub enum AlarmAuditType {
     IpWhitelist = 0,
-    ManyWrongPassword = 1,
-    FrequentAttempt = 2,
+    ExceedThirtyAttempts = 1,
+    SixAttemptsWithinOneMinute = 2,
 }
 
 pub enum FileAuditType {
@@ -2527,6 +2616,52 @@ impl Drop for Connection {
     }
 }
 
+#[cfg(all(target_os = "linux", feature = "linux_headless"))]
+#[cfg(not(any(feature = "flatpak", feature = "appimage")))]
+struct LinuxHeadlessHandle {
+    pub is_headless_allowed: bool,
+    pub is_headless: bool,
+    pub wait_ipc_timeout: u64,
+    pub rx_cm_stream_ready: mpsc::Receiver<()>,
+    pub tx_desktop_ready: mpsc::Sender<()>,
+}
+
+#[cfg(all(target_os = "linux", feature = "linux_headless"))]
+#[cfg(not(any(feature = "flatpak", feature = "appimage")))]
+impl LinuxHeadlessHandle {
+    pub fn new(rx_cm_stream_ready: mpsc::Receiver<()>, tx_desktop_ready: mpsc::Sender<()>) -> Self {
+        let is_headless_allowed = crate::platform::is_headless_allowed();
+        let is_headless = is_headless_allowed && linux_desktop_manager::is_headless();
+        Self {
+            is_headless_allowed,
+            is_headless,
+            wait_ipc_timeout: 10_000,
+            rx_cm_stream_ready,
+            tx_desktop_ready,
+        }
+    }
+
+    pub fn try_start_desktop(&mut self, os_login: Option<&OSLogin>) -> String {
+        if self.is_headless_allowed {
+            match os_login {
+                Some(os_login) => {
+                    linux_desktop_manager::try_start_desktop(&os_login.username, &os_login.password)
+                }
+                None => linux_desktop_manager::try_start_desktop("", ""),
+            }
+        } else {
+            "".to_string()
+        }
+    }
+
+    pub async fn wait_desktop_cm_ready(&mut self) {
+        if self.is_headless {
+            self.tx_desktop_ready.send(()).await.ok();
+            let _res = timeout(self.wait_ipc_timeout, self.rx_cm_stream_ready.recv()).await;
+        }
+    }
+}
+
 mod raii {
     use super::*;
     pub struct ConnectionID(i32);
@@ -2550,6 +2685,14 @@ mod raii {
             if active_conns_lock.is_empty() {
                 video_service::try_plug_out_virtual_display();
             }
+            #[cfg(all(windows))]
+            if active_conns_lock.is_empty() {
+                crate::privacy_win_mag::stop();
+            }
+            video_service::VIDEO_QOS
+                .lock()
+                .unwrap()
+                .on_connection_close(self.0);
         }
     }
 }
diff --git a/src/server/input_service.rs b/src/server/input_service.rs
index e71d166f5..b721149e9 100644
--- a/src/server/input_service.rs
+++ b/src/server/input_service.rs
@@ -1,13 +1,17 @@
 use super::*;
-use crate::input::*;
 #[cfg(target_os = "macos")]
 use crate::common::is_server;
 #[cfg(target_os = "linux")]
 use crate::common::IS_X11;
+use crate::input::*;
 #[cfg(target_os = "macos")]
 use dispatch::Queue;
 use enigo::{Enigo, Key, KeyboardControllable, MouseButton, MouseControllable};
-use hbb_common::{get_time, protobuf::EnumOrUnknown};
+use hbb_common::{
+    get_time,
+    message_proto::{pointer_device_event::Union::TouchEvent, touch_event::Union::ScaleUpdate},
+    protobuf::EnumOrUnknown,
+};
 use rdev::{self, EventType, Key as RdevKey, KeyCode, RawKey};
 #[cfg(target_os = "macos")]
 use rdev::{CGEventSourceStateID, CGEventTapLocation, VirtualInput};
@@ -523,6 +527,21 @@ pub fn handle_mouse(evt: &MouseEvent, conn: i32) {
     handle_mouse_(evt, conn);
 }
 
+// to-do: merge handle_mouse and handle_pointer
+pub fn handle_pointer(evt: &PointerDeviceEvent, conn: i32) {
+    #[cfg(target_os = "macos")]
+    if !is_server() {
+        // having GUI, run main GUI thread, otherwise crash
+        let evt = evt.clone();
+        QUEUE.exec_async(move || handle_pointer_(&evt, conn));
+        return;
+    }
+    #[cfg(windows)]
+    crate::portable_service::client::handle_pointer(evt, conn);
+    #[cfg(not(windows))]
+    handle_pointer_(evt, conn);
+}
+
 pub fn fix_key_down_timeout_loop() {
     std::thread::spawn(move || loop {
         std::thread::sleep(std::time::Duration::from_millis(10_000));
@@ -688,7 +707,10 @@ fn get_last_input_cursor_pos() -> (i32, i32) {
     (lock.x, lock.y)
 }
 
+// check if mouse is moved by the controlled side user to make controlled side has higher mouse priority than remote.
 fn active_mouse_(conn: i32) -> bool {
+    true
+    /* this method is buggy (not working on macOS, making fast moving mouse event discarded here) and added latency (this is blocking way, must do in async way), so we disable it for now
     // out of time protection
     if LATEST_SYS_CURSOR_POS.lock().unwrap().0.elapsed() > MOUSE_MOVE_PROTECTION_TIMEOUT {
         return true;
@@ -741,6 +763,28 @@ fn active_mouse_(conn: i32) -> bool {
         }
         None => true,
     }
+    */
+}
+
+pub fn handle_pointer_(evt: &PointerDeviceEvent, conn: i32) {
+    if !active_mouse_(conn) {
+        return;
+    }
+
+    if EXITING.load(Ordering::SeqCst) {
+        return;
+    }
+
+    match &evt.union {
+        Some(TouchEvent(evt)) => match &evt.union {
+            Some(ScaleUpdate(_scale_evt)) => {
+                #[cfg(target_os = "windows")]
+                handle_scale(_scale_evt.scale);
+            }
+            _ => {}
+        },
+        _ => {}
+    }
 }
 
 pub fn handle_mouse_(evt: &MouseEvent, conn: i32) {
@@ -759,7 +803,7 @@ pub fn handle_mouse_(evt: &MouseEvent, conn: i32) {
     let mut en = ENIGO.lock().unwrap();
     #[cfg(not(target_os = "macos"))]
     let mut to_release = Vec::new();
-    if evt_type == 1 {
+    if evt_type == MOUSE_TYPE_DOWN {
         fix_modifiers(&evt.modifiers[..], &mut en, 0);
         #[cfg(target_os = "macos")]
         en.reset_flag();
@@ -885,6 +929,18 @@ pub fn handle_mouse_(evt: &MouseEvent, conn: i32) {
     }
 }
 
+#[cfg(target_os = "windows")]
+fn handle_scale(scale: i32) {
+    let mut en = ENIGO.lock().unwrap();
+    if scale == 0 {
+        en.key_up(Key::Control);
+    } else {
+        if en.key_down(Key::Control).is_ok() {
+            en.mouse_scroll_y(scale);
+        }
+    }
+}
+
 pub fn is_enter(evt: &KeyEvent) -> bool {
     if let Some(key_event::Union::ControlKey(ck)) = evt.union {
         if ck.value() == ControlKey::Return.value() || ck.value() == ControlKey::NumpadEnter.value()
@@ -1445,11 +1501,11 @@ pub fn handle_key_(evt: &KeyEvent) {
         _ => {}
     };
 
-    match evt.mode.unwrap() {
-        KeyboardMode::Map => {
+    match evt.mode.enum_value() {
+        Ok(KeyboardMode::Map) => {
             map_keyboard_mode(evt);
         }
-        KeyboardMode::Translate => {
+        Ok(KeyboardMode::Translate) => {
             translate_keyboard_mode(evt);
         }
         _ => {
diff --git a/src/server/portable_service.rs b/src/server/portable_service.rs
index b794e9823..155f2acb4 100644
--- a/src/server/portable_service.rs
+++ b/src/server/portable_service.rs
@@ -222,6 +222,8 @@ mod utils {
 
 // functions called in separate SYSTEM user process.
 pub mod server {
+    use hbb_common::message_proto::PointerDeviceEvent;
+
     use super::*;
 
     lazy_static::lazy_static! {
@@ -229,7 +231,13 @@ pub mod server {
     }
 
     pub fn run_portable_service() {
-        let shmem = Arc::new(SharedMemory::open_existing(SHMEM_NAME).unwrap());
+        let shmem = match SharedMemory::open_existing(SHMEM_NAME) {
+            Ok(shmem) => Arc::new(shmem),
+            Err(e) => {
+                log::error!("Failed to open existing shared memory: {:?}", e);
+                return;
+            }
+        };
         let shmem1 = shmem.clone();
         let shmem2 = shmem.clone();
         let mut threads = vec![];
@@ -247,7 +255,7 @@ pub mod server {
         }));
         let record_pos_handle = crate::input_service::try_start_record_cursor_pos();
         for th in threads.drain(..) {
-            th.join().unwrap();
+            th.join().ok();
             log::info!("thread joined");
         }
 
@@ -317,7 +325,11 @@ pub mod server {
                 }
                 if c.is_none() {
                     *crate::video_service::CURRENT_DISPLAY.lock().unwrap() = current_display;
-                    let (_, _current, display) = get_current_display().unwrap();
+                    let Ok((_, _current, display)) = get_current_display() else {
+                        log::error!("Failed to get current display");
+                        *EXIT.lock().unwrap() = true;
+                        return;
+                    };
                     display_width = display.width();
                     display_height = display.height();
                     match Capturer::new(display, use_yuv) {
@@ -378,8 +390,8 @@ pub mod server {
                         continue;
                     }
                 }
-                match c.as_mut().unwrap().frame(spf) {
-                    Ok(f) => {
+                match c.as_mut().map(|f| f.frame(spf)) {
+                    Some(Ok(f)) => {
                         utils::set_frame_info(
                             &shmem,
                             FrameInfo {
@@ -394,7 +406,7 @@ pub mod server {
                         first_frame_captured = true;
                         dxgi_failed_times = 0;
                     }
-                    Err(e) => {
+                    Some(Err(e)) => {
                         if e.kind() != std::io::ErrorKind::WouldBlock {
                             // DXGI_ERROR_INVALID_CALL after each success on Microsoft GPU driver
                             // log::error!("capture frame failed:{:?}", e);
@@ -404,7 +416,8 @@ pub mod server {
                                 std::thread::sleep(spf);
                                 continue;
                             }
-                            if !c.as_ref().unwrap().is_gdi() {
+                            if c.as_ref().map(|c| c.is_gdi()) == Some(false) {
+                                // nog gdi
                                 dxgi_failed_times += 1;
                             }
                             if dxgi_failed_times > MAX_DXGI_FAIL_TIME {
@@ -416,6 +429,9 @@ pub mod server {
                             shmem.write(ADDR_CAPTURE_WOULDBLOCK, &utils::i32_to_vec(TRUE));
                         }
                     }
+                    _ => {
+                        println!("unreachable!");
+                    }
                 }
             }
         }
@@ -466,6 +482,11 @@ pub mod server {
                                             crate::input_service::handle_mouse_(&evt, conn);
                                         }
                                     }
+                                    Pointer((v, conn)) => {
+                                        if let Ok(evt) = PointerDeviceEvent::parse_from_bytes(&v) {
+                                            crate::input_service::handle_pointer_(&evt, conn);
+                                        }
+                                    }
                                     Key(v) => {
                                         if let Ok(evt) = KeyEvent::parse_from_bytes(&v) {
                                             crate::input_service::handle_key_(&evt);
@@ -499,7 +520,7 @@ pub mod server {
 
 // functions called in main process.
 pub mod client {
-    use hbb_common::anyhow::Context;
+    use hbb_common::{anyhow::Context, message_proto::PointerDeviceEvent};
 
     use super::*;
 
@@ -864,6 +885,14 @@ pub mod client {
         ))))
     }
 
+    fn handle_pointer_(evt: &PointerDeviceEvent, conn: i32) -> ResultType<()> {
+        let mut v = vec![];
+        evt.write_to_vec(&mut v)?;
+        ipc_send(Data::DataPortableService(DataPortableService::Pointer((
+            v, conn,
+        ))))
+    }
+
     fn handle_key_(evt: &KeyEvent) -> ResultType<()> {
         let mut v = vec![];
         evt.write_to_vec(&mut v)?;
@@ -910,6 +939,15 @@ pub mod client {
         }
     }
 
+    pub fn handle_pointer(evt: &PointerDeviceEvent, conn: i32) {
+        if RUNNING.lock().unwrap().clone() {
+            crate::input_service::update_latest_input_cursor_time(conn);
+            handle_pointer_(evt, conn).ok();
+        } else {
+            crate::input_service::handle_pointer_(evt, conn);
+        }
+    }
+
     pub fn handle_key(evt: &KeyEvent) {
         if RUNNING.lock().unwrap().clone() {
             handle_key_(evt).ok();
diff --git a/src/server/video_qos.rs b/src/server/video_qos.rs
index d53053691..e9eb9dc79 100644
--- a/src/server/video_qos.rs
+++ b/src/server/video_qos.rs
@@ -1,8 +1,9 @@
 use super::*;
+use scrap::codec::Quality;
 use std::time::Duration;
-pub const FPS: u8 = 30;
-pub const MIN_FPS: u8 = 1;
-pub const MAX_FPS: u8 = 120;
+pub const FPS: u32 = 30;
+pub const MIN_FPS: u32 = 1;
+pub const MAX_FPS: u32 = 120;
 trait Percent {
     fn as_percent(&self) -> u32;
 }
@@ -18,22 +19,34 @@ impl Percent for ImageQuality {
     }
 }
 
-pub struct VideoQoS {
-    width: u32,
-    height: u32,
-    user_image_quality: u32,
-    current_image_quality: u32,
-    enable_abr: bool,
-    pub current_delay: u32,
-    pub fps: u8, // abr
-    pub user_fps: u8,
-    pub target_bitrate: u32, // abr
-    updated: bool,
+#[derive(Default, Debug, Copy, Clone)]
+struct Delay {
     state: DelayState,
-    debounce_count: u32,
+    staging_state: DelayState,
+    delay: u32,
+    counter: u32,
+    slower_than_old_state: Option<bool>,
 }
 
-#[derive(PartialEq, Debug)]
+#[derive(Default, Debug, Copy, Clone)]
+struct UserData {
+    full_speed_fps: Option<u32>,
+    auto_adjust_fps: Option<u32>,
+    custom_fps: Option<u32>,
+    quality: Option<(i64, Quality)>, // (time, quality)
+    delay: Option<Delay>,
+    response_delayed: bool,
+    record: bool,
+}
+
+pub struct VideoQoS {
+    fps: u32,
+    quality: Quality,
+    users: HashMap<i32, UserData>,
+    bitrate_store: u32,
+}
+
+#[derive(PartialEq, Debug, Clone, Copy)]
 enum DelayState {
     Normal = 0,
     LowDelay = 200,
@@ -41,6 +54,12 @@ enum DelayState {
     Broken = 1000,
 }
 
+impl Default for DelayState {
+    fn default() -> Self {
+        DelayState::Normal
+    }
+}
+
 impl DelayState {
     fn from_delay(delay: u32) -> Self {
         if delay > DelayState::Broken as u32 {
@@ -59,187 +78,329 @@ impl Default for VideoQoS {
     fn default() -> Self {
         VideoQoS {
             fps: FPS,
-            user_fps: FPS,
-            user_image_quality: ImageQuality::Balanced.as_percent(),
-            current_image_quality: ImageQuality::Balanced.as_percent(),
-            enable_abr: false,
-            width: 0,
-            height: 0,
-            current_delay: 0,
-            target_bitrate: 0,
-            updated: false,
-            state: DelayState::Normal,
-            debounce_count: 0,
+            quality: Default::default(),
+            users: Default::default(),
+            bitrate_store: 0,
         }
     }
 }
 
+#[derive(Debug, PartialEq, Eq)]
+pub enum RefreshType {
+    SetImageQuality,
+}
+
 impl VideoQoS {
-    pub fn set_size(&mut self, width: u32, height: u32) {
-        if width == 0 || height == 0 {
-            return;
-        }
-        self.width = width;
-        self.height = height;
+    pub fn spf(&self) -> Duration {
+        Duration::from_secs_f32(1. / (self.fps() as f32))
     }
 
-    pub fn spf(&mut self) -> Duration {
-        if self.fps < MIN_FPS || self.fps > MAX_FPS {
-            self.fps = self.base_fps();
+    pub fn fps(&self) -> u32 {
+        if self.fps >= MIN_FPS && self.fps <= MAX_FPS {
+            self.fps
+        } else {
+            FPS
         }
-        Duration::from_secs_f32(1. / (self.fps as f32))
     }
 
-    fn base_fps(&self) -> u8 {
-        if self.user_fps >= MIN_FPS && self.user_fps <= MAX_FPS {
-            return self.user_fps;
-        }
-        return FPS;
+    pub fn store_bitrate(&mut self, bitrate: u32) {
+        self.bitrate_store = bitrate;
     }
 
-    // update_network_delay periodically
-    // decrease the bitrate when the delay gets bigger
-    pub fn update_network_delay(&mut self, delay: u32) {
-        if self.current_delay.eq(&0) {
-            self.current_delay = delay;
+    pub fn bitrate(&self) -> u32 {
+        self.bitrate_store
+    }
+
+    pub fn quality(&self) -> Quality {
+        self.quality
+    }
+
+    pub fn record(&self) -> bool {
+        self.users.iter().any(|u| u.1.record)
+    }
+
+    pub fn abr_enabled() -> bool {
+        "N" != Config::get_option("enable-abr")
+    }
+
+    pub fn refresh(&mut self, typ: Option<RefreshType>) {
+        // fps
+        let user_fps = |u: &UserData| {
+            // full_speed_fps
+            let mut fps = u.full_speed_fps.unwrap_or_default() * 9 / 10;
+            // auto adjust fps
+            if let Some(auto_adjust_fps) = u.auto_adjust_fps {
+                if fps == 0 || auto_adjust_fps < fps {
+                    fps = auto_adjust_fps;
+                }
+            }
+            // custom_fps
+            if let Some(custom_fps) = u.custom_fps {
+                if fps == 0 || custom_fps < fps {
+                    fps = custom_fps;
+                }
+            }
+            // delay
+            if let Some(delay) = u.delay {
+                fps = match delay.state {
+                    DelayState::Normal => fps,
+                    DelayState::LowDelay => fps * 3 / 4,
+                    DelayState::HighDelay => fps / 2,
+                    DelayState::Broken => fps / 4,
+                }
+            }
+            // delay response
+            if u.response_delayed {
+                if fps > MIN_FPS + 2 {
+                    fps = MIN_FPS + 2;
+                }
+            }
+            return fps;
+        };
+        let mut fps = self
+            .users
+            .iter()
+            .map(|(_, u)| user_fps(u))
+            .filter(|u| *u >= MIN_FPS)
+            .min()
+            .unwrap_or(FPS);
+        if fps > MAX_FPS {
+            fps = MAX_FPS;
+        }
+        self.fps = fps;
+
+        // quality
+        // latest image quality
+        let latest_quality = self
+            .users
+            .iter()
+            .map(|(_, u)| u.quality)
+            .filter(|q| *q != None)
+            .max_by(|a, b| a.unwrap_or_default().0.cmp(&b.unwrap_or_default().0))
+            .unwrap_or_default()
+            .unwrap_or_default()
+            .1;
+        let mut quality = latest_quality;
+
+        // network delay
+        if Self::abr_enabled() && typ != Some(RefreshType::SetImageQuality) {
+            // max delay
+            let delay = self
+                .users
+                .iter()
+                .map(|u| u.1.delay)
+                .filter(|d| d.is_some())
+                .max_by(|a, b| {
+                    (a.unwrap_or_default().state as u32).cmp(&(b.unwrap_or_default().state as u32))
+                });
+            let delay = delay.unwrap_or_default().unwrap_or_default().state;
+            if delay != DelayState::Normal {
+                match self.quality {
+                    Quality::Best => {
+                        quality = if delay == DelayState::Broken {
+                            Quality::Low
+                        } else {
+                            Quality::Balanced
+                        };
+                    }
+                    Quality::Balanced => {
+                        quality = Quality::Low;
+                    }
+                    Quality::Low => {
+                        quality = Quality::Low;
+                    }
+                    Quality::Custom(b) => match delay {
+                        DelayState::LowDelay => {
+                            quality =
+                                Quality::Custom(if b >= 150 { 100 } else { std::cmp::min(50, b) });
+                        }
+                        DelayState::HighDelay => {
+                            quality =
+                                Quality::Custom(if b >= 100 { 50 } else { std::cmp::min(25, b) });
+                        }
+                        DelayState::Broken => {
+                            quality =
+                                Quality::Custom(if b >= 50 { 25 } else { std::cmp::min(10, b) });
+                        }
+                        DelayState::Normal => {}
+                    },
+                }
+            } else {
+                match self.quality {
+                    Quality::Low => {
+                        if latest_quality == Quality::Best {
+                            quality = Quality::Balanced;
+                        }
+                    }
+                    Quality::Custom(current_b) => {
+                        if let Quality::Custom(latest_b) = latest_quality {
+                            if current_b < latest_b / 2 {
+                                quality = Quality::Custom(latest_b / 2);
+                            }
+                        }
+                    }
+                    _ => {}
+                }
+            }
+        }
+        self.quality = quality;
+    }
+
+    pub fn user_custom_fps(&mut self, id: i32, fps: u32) {
+        if fps < MIN_FPS {
             return;
         }
-
-        self.current_delay = delay / 2 + self.current_delay / 2;
-        log::trace!(
-            "VideoQoS update_network_delay:{}, {}, state:{:?}",
-            self.current_delay,
-            delay,
-            self.state,
-        );
-
-        // ABR
-        if !self.enable_abr {
-            return;
-        }
-        let current_state = DelayState::from_delay(self.current_delay);
-        if current_state != self.state && self.debounce_count > 5 {
-            log::debug!(
-                "VideoQoS state changed:{:?} -> {:?}",
-                self.state,
-                current_state
+        if let Some(user) = self.users.get_mut(&id) {
+            user.custom_fps = Some(fps);
+        } else {
+            self.users.insert(
+                id,
+                UserData {
+                    custom_fps: Some(fps),
+                    ..Default::default()
+                },
             );
-            self.state = current_state;
-            self.debounce_count = 0;
-            self.refresh_quality();
+        }
+        self.refresh(None);
+    }
+
+    pub fn user_full_speed_fps(&mut self, id: i32, full_speed_fps: u32) {
+        if let Some(user) = self.users.get_mut(&id) {
+            user.full_speed_fps = Some(full_speed_fps);
         } else {
-            self.debounce_count += 1;
+            self.users.insert(
+                id,
+                UserData {
+                    full_speed_fps: Some(full_speed_fps),
+                    ..Default::default()
+                },
+            );
         }
+        self.refresh(None);
     }
 
-    fn refresh_quality(&mut self) {
-        match self.state {
-            DelayState::Normal => {
-                self.fps = self.base_fps();
-                self.current_image_quality = self.user_image_quality;
-            }
-            DelayState::LowDelay => {
-                self.fps = self.base_fps();
-                self.current_image_quality = std::cmp::min(self.user_image_quality, 50);
-            }
-            DelayState::HighDelay => {
-                self.fps = self.base_fps() / 2;
-                self.current_image_quality = std::cmp::min(self.user_image_quality, 25);
-            }
-            DelayState::Broken => {
-                self.fps = self.base_fps() / 4;
-                self.current_image_quality = 10;
-            }
-        }
-        let _ = self.generate_bitrate().ok();
-        self.updated = true;
-    }
-
-    // handle image_quality change from peer
-    pub fn update_image_quality(&mut self, image_quality: i32) {
-        if image_quality == ImageQuality::Low.value()
-            || image_quality == ImageQuality::Balanced.value()
-            || image_quality == ImageQuality::Best.value()
-        {
-            // not custom
-            self.user_fps = FPS;
-            self.fps = FPS;
-        }
-        let image_quality = Self::convert_quality(image_quality) as _;
-        if self.current_image_quality != image_quality {
-            self.current_image_quality = image_quality;
-            let _ = self.generate_bitrate().ok();
-            self.updated = true;
-        }
-
-        self.user_image_quality = self.current_image_quality;
-    }
-
-    pub fn update_user_fps(&mut self, fps: u8) {
-        if fps >= MIN_FPS && fps <= MAX_FPS {
-            if self.user_fps != fps {
-                self.user_fps = fps;
-                self.fps = fps;
-                self.updated = true;
-            }
-        }
-    }
-
-    pub fn generate_bitrate(&mut self) -> ResultType<u32> {
-        // https://www.nvidia.com/en-us/geforce/guides/broadcasting-guide/
-        if self.width == 0 || self.height == 0 {
-            bail!("Fail to generate_bitrate, width or height is not set");
-        }
-        if self.current_image_quality == 0 {
-            self.current_image_quality = ImageQuality::Balanced.as_percent();
-        }
-
-        let base_bitrate = ((self.width * self.height) / 800) as u32;
-
-        #[cfg(target_os = "android")]
-        {
-            // fix when android screen shrinks
-            let fix = scrap::Display::fix_quality() as u32;
-            log::debug!("Android screen, fix quality:{}", fix);
-            let base_bitrate = base_bitrate * fix;
-            self.target_bitrate = base_bitrate * self.current_image_quality / 100;
-            Ok(self.target_bitrate)
-        }
-        #[cfg(not(target_os = "android"))]
-        {
-            self.target_bitrate = base_bitrate * self.current_image_quality / 100;
-            Ok(self.target_bitrate)
-        }
-    }
-
-    pub fn check_if_updated(&mut self) -> bool {
-        if self.updated {
-            self.updated = false;
-            return true;
-        }
-        return false;
-    }
-
-    pub fn reset(&mut self) {
-        self.fps = FPS;
-        self.user_fps = FPS;
-        self.updated = true;
-    }
-
-    pub fn check_abr_config(&mut self) -> bool {
-        self.enable_abr = "N" != Config::get_option("enable-abr");
-        self.enable_abr
-    }
-
-    pub fn convert_quality(q: i32) -> i32 {
-        if q == ImageQuality::Balanced.value() {
-            100 * 2 / 3
-        } else if q == ImageQuality::Low.value() {
-            100 / 2
-        } else if q == ImageQuality::Best.value() {
-            100
+    pub fn user_auto_adjust_fps(&mut self, id: i32, fps: u32) {
+        if let Some(user) = self.users.get_mut(&id) {
+            user.auto_adjust_fps = Some(fps);
         } else {
-            (q >> 8 & 0xFF) * 2
+            self.users.insert(
+                id,
+                UserData {
+                    auto_adjust_fps: Some(fps),
+                    ..Default::default()
+                },
+            );
         }
+        self.refresh(None);
+    }
+
+    pub fn user_image_quality(&mut self, id: i32, image_quality: i32) {
+        // https://github.com/rustdesk/rustdesk/blob/d716e2b40c38737f1aa3f16de0dec67394a6ac68/src/server/video_service.rs#L493
+        let convert_quality = |q: i32| {
+            if q == ImageQuality::Balanced.value() {
+                Quality::Balanced
+            } else if q == ImageQuality::Low.value() {
+                Quality::Low
+            } else if q == ImageQuality::Best.value() {
+                Quality::Best
+            } else {
+                let mut b = (q >> 8 & 0xFFF) * 2;
+                b = std::cmp::max(b, 20);
+                b = std::cmp::min(b, 8000);
+                Quality::Custom(b as u32)
+            }
+        };
+
+        let quality = Some((hbb_common::get_time(), convert_quality(image_quality)));
+        if let Some(user) = self.users.get_mut(&id) {
+            user.quality = quality;
+        } else {
+            self.users.insert(
+                id,
+                UserData {
+                    quality,
+                    ..Default::default()
+                },
+            );
+        }
+        self.refresh(Some(RefreshType::SetImageQuality));
+    }
+
+    pub fn user_network_delay(&mut self, id: i32, delay: u32) {
+        let state = DelayState::from_delay(delay);
+        let debounce = 3;
+        if let Some(user) = self.users.get_mut(&id) {
+            if let Some(d) = &mut user.delay {
+                d.delay = (delay + d.delay) / 2;
+                let new_state = DelayState::from_delay(d.delay);
+                let slower_than_old_state = new_state as i32 - d.staging_state as i32;
+                let slower_than_old_state = if slower_than_old_state > 0 {
+                    Some(true)
+                } else if slower_than_old_state < 0 {
+                    Some(false)
+                } else {
+                    None
+                };
+                if d.slower_than_old_state == slower_than_old_state {
+                    let old_counter = d.counter;
+                    d.counter += delay / 1000 + 1;
+                    if old_counter < debounce && d.counter >= debounce {
+                        d.counter = 0;
+                        d.state = d.staging_state;
+                        d.staging_state = new_state;
+                    }
+                    if d.counter % debounce == 0 {
+                        self.refresh(None);
+                    }
+                } else {
+                    d.counter = 0;
+                    d.staging_state = new_state;
+                    d.slower_than_old_state = slower_than_old_state;
+                }
+            } else {
+                user.delay = Some(Delay {
+                    state: DelayState::Normal,
+                    staging_state: state,
+                    delay,
+                    counter: 0,
+                    slower_than_old_state: None,
+                });
+            }
+        } else {
+            self.users.insert(
+                id,
+                UserData {
+                    delay: Some(Delay {
+                        state: DelayState::Normal,
+                        staging_state: state,
+                        delay,
+                        counter: 0,
+                        slower_than_old_state: None,
+                    }),
+                    ..Default::default()
+                },
+            );
+        }
+    }
+
+    pub fn user_delay_response_elapsed(&mut self, id: i32, elapsed: u128) {
+        if let Some(user) = self.users.get_mut(&id) {
+            let old = user.response_delayed;
+            user.response_delayed = elapsed > 3000;
+            if old != user.response_delayed {
+                self.refresh(None);
+            }
+        }
+    }
+
+    pub fn user_record(&mut self, id: i32, v: bool) {
+        if let Some(user) = self.users.get_mut(&id) {
+            user.record = v;
+        }
+    }
+
+    pub fn on_connection_close(&mut self, id: i32) {
+        self.users.remove(&id);
+        self.refresh(None);
     }
 }
diff --git a/src/server/video_service.rs b/src/server/video_service.rs
index ada41e266..e8f6f30c2 100644
--- a/src/server/video_service.rs
+++ b/src/server/video_service.rs
@@ -36,7 +36,7 @@ use hbb_common::{
 use scrap::Capturer;
 use scrap::{
     aom::AomEncoderConfig,
-    codec::{Encoder, EncoderCfg, HwEncoderConfig},
+    codec::{Encoder, EncoderCfg, HwEncoderConfig, Quality},
     record::{Recorder, RecorderContext},
     vpxcodec::{VpxEncoderConfig, VpxVideoCodecId},
     CodecName, Display, TraitCapturer,
@@ -52,6 +52,11 @@ use std::{
 
 pub const NAME: &'static str = "video";
 
+struct ChangedResolution {
+    original: (i32, i32),
+    changed: (i32, i32),
+}
+
 lazy_static::lazy_static! {
     pub static ref CURRENT_DISPLAY: Arc<Mutex<usize>> = Arc::new(Mutex::new(usize::MAX));
     static ref LAST_ACTIVE: Arc<Mutex<Instant>> = Arc::new(Mutex::new(Instant::now()));
@@ -66,58 +71,29 @@ lazy_static::lazy_static! {
     pub static ref IS_UAC_RUNNING: Arc<Mutex<bool>> = Default::default();
     pub static ref IS_FOREGROUND_WINDOW_ELEVATED: Arc<Mutex<bool>> = Default::default();
     pub static ref LAST_SYNC_DISPLAYS: Arc<RwLock<Vec<DisplayInfo>>> = Default::default();
-    static ref ORIGINAL_RESOLUTIONS: Arc<RwLock<HashMap<String, (i32, i32)>>> = Default::default();
+    static ref CHANGED_RESOLUTIONS: Arc<RwLock<HashMap<String, ChangedResolution>>> = Default::default();
 }
 
-// Not virtual display
 #[inline]
-fn set_original_resolution_(display_name: &str, wh: (i32, i32)) -> (i32, i32) {
-    let mut original_resolutions = ORIGINAL_RESOLUTIONS.write().unwrap();
-    match original_resolutions.get(display_name) {
-        Some(r) => r.clone(),
+pub fn set_last_changed_resolution(display_name: &str, original: (i32, i32), changed: (i32, i32)) {
+    let mut lock = CHANGED_RESOLUTIONS.write().unwrap();
+    match lock.get_mut(display_name) {
+        Some(res) => res.changed = changed,
         None => {
-            original_resolutions.insert(display_name.to_owned(), wh.clone());
-            wh
+            lock.insert(
+                display_name.to_owned(),
+                ChangedResolution { original, changed },
+            );
         }
     }
 }
 
-// Not virtual display
-#[inline]
-fn get_original_resolution_(display_name: &str) -> Option<(i32, i32)> {
-    ORIGINAL_RESOLUTIONS
-        .read()
-        .unwrap()
-        .get(display_name)
-        .map(|r| r.clone())
-}
-
-// Not virtual display
-#[inline]
-fn get_or_set_original_resolution_(display_name: &str, wh: (i32, i32)) -> (i32, i32) {
-    let r = get_original_resolution_(display_name);
-    if let Some(r) = r {
-        return r;
-    }
-    set_original_resolution_(display_name, wh)
-}
-
-// Not virtual display
-#[inline]
-fn update_get_original_resolution_(display_name: &str, w: usize, h: usize) -> Resolution {
-    let wh = get_or_set_original_resolution_(display_name, (w as _, h as _));
-    Resolution {
-        width: wh.0,
-        height: wh.1,
-        ..Default::default()
-    }
-}
-
 #[inline]
 #[cfg(not(any(target_os = "android", target_os = "ios")))]
 pub fn reset_resolutions() {
-    for (name, (w, h)) in ORIGINAL_RESOLUTIONS.read().unwrap().iter() {
-        if let Err(e) = crate::platform::change_resolution(name, *w as _, *h as _) {
+    for (name, res) in CHANGED_RESOLUTIONS.read().unwrap().iter() {
+        let (w, h) = res.original;
+        if let Err(e) = crate::platform::change_resolution(name, w as _, h as _) {
             log::error!(
                 "Failed to reset resolution of display '{}' to ({},{}): {}",
                 name,
@@ -144,7 +120,7 @@ pub fn capture_cursor_embedded() -> bool {
 
 #[inline]
 pub fn notify_video_frame_fetched(conn_id: i32, frame_tm: Option<Instant>) {
-    FRAME_FETCHED_NOTIFIER.0.send((conn_id, frame_tm)).unwrap()
+    FRAME_FETCHED_NOTIFIER.0.send((conn_id, frame_tm)).ok();
 }
 
 #[inline]
@@ -461,30 +437,19 @@ fn get_capturer(use_yuv: bool, portable_service_running: bool) -> ResultType<Cap
 fn check_displays_new() -> Option<Vec<Display>> {
     let displays = try_get_displays().ok()?;
     let last_sync_displays = &*LAST_SYNC_DISPLAYS.read().unwrap();
-
     if displays.len() != last_sync_displays.len() {
+        // No need to check if the resolutions are changed by third process.
         Some(displays)
     } else {
-        for i in 0..displays.len() {
-            if displays[i].height() != (last_sync_displays[i].height as usize) {
-                return Some(displays);
-            }
-            if displays[i].width() != (last_sync_displays[i].width as usize) {
-                return Some(displays);
-            }
-            if displays[i].origin() != (last_sync_displays[i].x, last_sync_displays[i].y) {
-                return Some(displays);
-            }
-        }
         None
     }
 }
 
 fn check_get_displays_changed_msg() -> Option<Message> {
     let displays = check_displays_new()?;
+    // Display to DisplayInfo
     let (current, displays) = get_displays_2(&displays);
     let mut pi = PeerInfo {
-        conn_id: crate::SYNC_PEER_INFO_DISPLAYS,
         ..Default::default()
     };
     pi.displays = displays.clone();
@@ -504,7 +469,7 @@ fn run(sp: GenericService) -> ResultType<()> {
     #[cfg(not(any(target_os = "android", target_os = "ios")))]
     let _wake_lock = get_wake_lock();
 
-    // ensure_inited() is needed because release_resource() may be called.
+    // ensure_inited() is needed because clear() may be called.
     #[cfg(target_os = "linux")]
     super::wayland::ensure_inited()?;
     #[cfg(windows)]
@@ -515,40 +480,17 @@ fn run(sp: GenericService) -> ResultType<()> {
     let mut c = get_capturer(true, last_portable_service_running)?;
 
     let mut video_qos = VIDEO_QOS.lock().unwrap();
-    video_qos.set_size(c.width as _, c.height as _);
-    let mut spf = video_qos.spf();
-    let bitrate = video_qos.generate_bitrate()?;
-    let abr = video_qos.check_abr_config();
+    video_qos.refresh(None);
+    let mut spf;
+    let mut quality = video_qos.quality();
+    let abr = VideoQoS::abr_enabled();
+    log::info!("init quality={:?}, abr enabled:{}", quality, abr);
+    let codec_name = Encoder::negotiated_codec();
+    let recorder = get_recorder(c.width, c.height, &codec_name);
+    let last_recording =
+        (recorder.lock().unwrap().is_some() || video_qos.record()) && codec_name != CodecName::AV1;
     drop(video_qos);
-    log::info!("init bitrate={}, abr enabled:{}", bitrate, abr);
-
-    let encoder_cfg = match Encoder::negotiated_codec() {
-        scrap::CodecName::H264(name) | scrap::CodecName::H265(name) => {
-            EncoderCfg::HW(HwEncoderConfig {
-                name,
-                width: c.width,
-                height: c.height,
-                bitrate: bitrate as _,
-            })
-        }
-        name @ (scrap::CodecName::VP8 | scrap::CodecName::VP9) => {
-            EncoderCfg::VPX(VpxEncoderConfig {
-                width: c.width as _,
-                height: c.height as _,
-                bitrate,
-                codec: if name == scrap::CodecName::VP8 {
-                    VpxVideoCodecId::VP8
-                } else {
-                    VpxVideoCodecId::VP9
-                },
-            })
-        }
-        scrap::CodecName::AV1 => EncoderCfg::AOM(AomEncoderConfig {
-            width: c.width as _,
-            height: c.height as _,
-            bitrate: bitrate as _,
-        }),
-    };
+    let encoder_cfg = get_encoder_config(&c, quality, last_recording);
 
     let mut encoder;
     match Encoder::new(encoder_cfg) {
@@ -556,11 +498,14 @@ fn run(sp: GenericService) -> ResultType<()> {
         Err(err) => bail!("Failed to create encoder: {}", err),
     }
     c.set_use_yuv(encoder.use_yuv());
+    VIDEO_QOS.lock().unwrap().store_bitrate(encoder.bitrate());
 
     if *SWITCH.lock().unwrap() {
         log::debug!("Broadcasting display switch");
         let mut misc = Misc::new();
-        let display_name = get_current_display_name().unwrap_or_default();
+        let display_name = get_current_display()
+            .map(|(_, _, d)| d.name())
+            .unwrap_or_default();
         let original_resolution = get_original_resolution(&display_name, c.width, c.height);
         misc.set_switch_display(SwitchDisplay {
             display: c.current as _,
@@ -596,8 +541,6 @@ fn run(sp: GenericService) -> ResultType<()> {
     let mut try_gdi = 1;
     #[cfg(windows)]
     log::info!("gdi: {}", c.is_gdi());
-    let codec_name = Encoder::negotiated_codec();
-    let recorder = get_recorder(c.width, c.height, &codec_name);
     #[cfg(windows)]
     start_uac_elevation_check();
 
@@ -609,14 +552,17 @@ fn run(sp: GenericService) -> ResultType<()> {
         check_uac_switch(c.privacy_mode_id, c._capturer_privacy_mode_id)?;
 
         let mut video_qos = VIDEO_QOS.lock().unwrap();
-        if video_qos.check_if_updated() && video_qos.target_bitrate > 0 {
-            log::debug!(
-                "qos is updated, target_bitrate:{}, fps:{}",
-                video_qos.target_bitrate,
-                video_qos.fps
-            );
-            allow_err!(encoder.set_bitrate(video_qos.target_bitrate));
-            spf = video_qos.spf();
+        spf = video_qos.spf();
+        if quality != video_qos.quality() {
+            log::debug!("quality: {:?} -> {:?}", quality, video_qos.quality());
+            quality = video_qos.quality();
+            allow_err!(encoder.set_quality(quality));
+            video_qos.store_bitrate(encoder.bitrate());
+        }
+        let recording = (recorder.lock().unwrap().is_some() || video_qos.record())
+            && codec_name != CodecName::AV1;
+        if recording != last_recording {
+            bail!("SWITCH");
         }
         drop(video_qos);
 
@@ -624,6 +570,8 @@ fn run(sp: GenericService) -> ResultType<()> {
             bail!("SWITCH");
         }
         if c.current != *CURRENT_DISPLAY.lock().unwrap() {
+            #[cfg(target_os = "linux")]
+            super::wayland::clear();
             *SWITCH.lock().unwrap() = true;
             bail!("SWITCH");
         }
@@ -658,6 +606,8 @@ fn run(sp: GenericService) -> ResultType<()> {
             if let Some(msg_out) = check_get_displays_changed_msg() {
                 sp.send(msg_out);
                 log::info!("Displays changed");
+                #[cfg(target_os = "linux")]
+                super::wayland::clear();
                 *SWITCH.lock().unwrap() = true;
                 bail!("SWITCH");
             }
@@ -726,7 +676,7 @@ fn run(sp: GenericService) -> ResultType<()> {
                             // Do not reset the capturer for now, as it will cause the prompt to show every few minutes.
                             // https://github.com/rustdesk/rustdesk/issues/4276
                             //
-                            // super::wayland::release_resource();
+                            // super::wayland::clear();
                             // bail!("Wayland capturer none 100 times, try restart capture");
                         }
                     }
@@ -735,6 +685,8 @@ fn run(sp: GenericService) -> ResultType<()> {
             Err(err) => {
                 if check_display_changed(c.ndisplay, c.current, c.width, c.height) {
                     log::info!("Displays changed");
+                    #[cfg(target_os = "linux")]
+                    super::wayland::clear();
                     *SWITCH.lock().unwrap() = true;
                     bail!("SWITCH");
                 }
@@ -779,13 +731,46 @@ fn run(sp: GenericService) -> ResultType<()> {
     }
 
     #[cfg(target_os = "linux")]
-    if !scrap::is_x11() {
-        super::wayland::release_resource();
-    }
+    super::wayland::clear();
 
     Ok(())
 }
 
+fn get_encoder_config(c: &CapturerInfo, quality: Quality, recording: bool) -> EncoderCfg {
+    // https://www.wowza.com/community/t/the-correct-keyframe-interval-in-obs-studio/95162
+    let keyframe_interval = if recording { Some(240) } else { None };
+    match Encoder::negotiated_codec() {
+        scrap::CodecName::H264(name) | scrap::CodecName::H265(name) => {
+            EncoderCfg::HW(HwEncoderConfig {
+                name,
+                width: c.width,
+                height: c.height,
+                quality,
+                keyframe_interval,
+            })
+        }
+        name @ (scrap::CodecName::VP8 | scrap::CodecName::VP9) => {
+            EncoderCfg::VPX(VpxEncoderConfig {
+                width: c.width as _,
+                height: c.height as _,
+                quality,
+                codec: if name == scrap::CodecName::VP8 {
+                    VpxVideoCodecId::VP8
+                } else {
+                    VpxVideoCodecId::VP9
+                },
+                keyframe_interval,
+            })
+        }
+        scrap::CodecName::AV1 => EncoderCfg::AOM(AomEncoderConfig {
+            width: c.width as _,
+            height: c.height as _,
+            quality,
+            keyframe_interval,
+        }),
+    }
+}
+
 fn get_recorder(
     width: usize,
     height: usize,
@@ -878,7 +863,24 @@ fn get_original_resolution(display_name: &str, w: usize, h: usize) -> MessageFie
             ..Default::default()
         }
     } else {
-        update_get_original_resolution_(&display_name, w, h)
+        let mut changed_resolutions = CHANGED_RESOLUTIONS.write().unwrap();
+        let (width, height) = match changed_resolutions.get(display_name) {
+            Some(res) => {
+                if res.changed.0 != w as i32 || res.changed.1 != h as i32 {
+                    // If the resolution is changed by third process, remove the record in changed_resolutions.
+                    changed_resolutions.remove(display_name);
+                    (w as _, h as _)
+                } else {
+                    res.original
+                }
+            }
+            None => (w as _, h as _),
+        };
+        Resolution {
+            width,
+            height,
+            ..Default::default()
+        }
     })
     .into()
 }
@@ -995,8 +997,18 @@ fn no_displays(displays: &Vec<Display>) -> bool {
     } else if display_len == 1 {
         let display = &displays[0];
         let dummy_display_side_max_size = 800;
-        display.width() <= dummy_display_side_max_size
-            && display.height() <= dummy_display_side_max_size
+        if display.width() > dummy_display_side_max_size
+            || display.height() > dummy_display_side_max_size
+        {
+            return false;
+        }
+        let any_real = crate::platform::resolutions(&display.name())
+            .iter()
+            .any(|r| {
+                (r.height as usize) > dummy_display_side_max_size
+                    || (r.width as usize) > dummy_display_side_max_size
+            });
+        !any_real
     } else {
         false
     }
@@ -1004,16 +1016,16 @@ fn no_displays(displays: &Vec<Display>) -> bool {
 
 #[cfg(all(windows, feature = "virtual_display_driver"))]
 fn try_get_displays() -> ResultType<Vec<Display>> {
-    let mut displays = Display::all()?;
-    if no_displays(&displays) {
-        log::debug!("no displays, create virtual display");
-        if let Err(e) = virtual_display_manager::plug_in_headless() {
-            log::error!("plug in headless failed {}", e);
-        } else {
-            displays = Display::all()?;
-        }
-    }
-    Ok(displays)
+    // let mut displays = Display::all()?;
+    // if no_displays(&displays) {
+    //     log::debug!("no displays, create virtual display");
+    //     if let Err(e) = virtual_display_manager::plug_in_headless() {
+    //         log::error!("plug in headless failed {}", e);
+    //     } else {
+    //         displays = Display::all()?;
+    //     }
+    // }
+    Ok( Display::all()?)
 }
 
 pub(super) fn get_current_display_2(mut all: Vec<Display>) -> ResultType<(usize, usize, Display)> {
@@ -1040,13 +1052,6 @@ pub fn get_current_display() -> ResultType<(usize, usize, Display)> {
     get_current_display_2(try_get_displays()?)
 }
 
-// `try_reset_current_display` is needed because `get_displays` may change the current display,
-// which may cause the mismatch of current display and the current display name.
-#[inline]
-pub fn get_current_display_name() -> ResultType<String> {
-    Ok(get_current_display_2(try_get_displays()?)?.2.name())
-}
-
 #[cfg(windows)]
 fn start_uac_elevation_check() {
     static START: Once = Once::new();
diff --git a/src/server/wayland.rs b/src/server/wayland.rs
index 10b93afce..efed83936 100644
--- a/src/server/wayland.rs
+++ b/src/server/wayland.rs
@@ -186,21 +186,6 @@ pub(super) async fn check_init() -> ResultType<()> {
     Ok(())
 }
 
-pub fn clear() {
-    if scrap::is_x11() {
-        return;
-    }
-
-    let mut lock = CAP_DISPLAY_INFO.write().unwrap();
-    if *lock != 0 {
-        unsafe {
-            let cap_display_info = Box::from_raw(*lock as *mut CapDisplayInfo);
-            let _ = Box::from_raw(cap_display_info.capturer.0);
-        }
-        *lock = 0;
-    }
-}
-
 pub(super) async fn get_displays() -> ResultType<(usize, Vec<DisplayInfo>)> {
     check_init().await?;
     let addr = *CAP_DISPLAY_INFO.read().unwrap();
@@ -230,8 +215,7 @@ pub(super) fn get_primary() -> ResultType<usize> {
     }
 }
 
-#[allow(dead_code)]
-pub(super) fn release_resource() {
+pub fn clear() {
     if scrap::is_x11() {
         return;
     }
diff --git a/src/tray.rs b/src/tray.rs
index ae5afe1e7..d44a29d09 100644
--- a/src/tray.rs
+++ b/src/tray.rs
@@ -61,7 +61,7 @@ pub fn make_tray() -> hbb_common::ResultType<()> {
     let mut docker_hiden = false;
 
     let open_func = move || {
-        #[cfg(not(feature = "flutter"))]
+        if cfg!(not(feature = "flutter"))
         {
             crate::run_me::<&str>(vec![]).ok();
             return;
@@ -101,11 +101,15 @@ pub fn make_tray() -> hbb_common::ResultType<()> {
 
         if let Ok(event) = menu_channel.try_recv() {
             if event.id == quit_i.id() {
+                /* failed in windows, seems no permission to check system process
                 if !crate::check_process("--server", false) {
                     *control_flow = ControlFlow::Exit;
                     return;
                 }
-                crate::platform::uninstall_service(false);
+                */
+                if !crate::platform::uninstall_service(false) {
+                    *control_flow = ControlFlow::Exit;
+                }
             } else if event.id == open_i.id() {
                 open_func();
             }
diff --git a/src/ui.rs b/src/ui.rs
index baa7fef4c..27793b31f 100644
--- a/src/ui.rs
+++ b/src/ui.rs
@@ -94,8 +94,7 @@ pub fn start(args: &mut [String]) {
         args[1] = id;
     }
     if args.is_empty() {
-        let children: Children = Default::default();
-        std::thread::spawn(move || check_zombie(children));
+        std::thread::spawn(move || check_zombie());
         crate::common::check_software_update();
         frame.event_handler(UI {});
         frame.sciter_handler(UIHostHandler {});
@@ -124,8 +123,16 @@ pub fn start(args: &mut [String]) {
             crate::platform::windows::enable_lowlevel_keyboard(hw as _);
         }
         let mut iter = args.iter();
-        let cmd = iter.next().unwrap().clone();
-        let id = iter.next().unwrap().clone();
+        let Some(cmd) = iter.next() else {
+            log::error!("Failed to get cmd arg");
+            return;
+        };
+        let cmd = cmd.to_owned();
+        let Some(id) = iter.next() else {
+            log::error!("Failed to get id arg");
+            return;
+        };
+        let id = id.to_owned();
         let pass = iter.next().unwrap_or(&"".to_owned()).clone();
         let args: Vec<String> = iter.map(|x| x.clone()).collect();
         frame.set_title(&id);
@@ -259,7 +266,8 @@ impl UI {
     }
 
     fn get_options(&self) -> Value {
-        let hashmap: HashMap<String, String> = serde_json::from_str(&get_options()).unwrap();
+        let hashmap: HashMap<String, String> =
+            serde_json::from_str(&get_options()).unwrap_or_default();
         let mut m = Value::map();
         for (k, v) in hashmap {
             m.set_item(k, v);
@@ -403,7 +411,7 @@ impl UI {
 
     fn get_recent_sessions(&mut self) -> Value {
         // to-do: limit number of recent sessions, and remove old peer file
-        let peers: Vec<Value> = PeerConfig::peers()
+        let peers: Vec<Value> = PeerConfig::peers(None)
             .drain(..)
             .map(|p| Self::get_peer_value(p.0, p.2))
             .collect();
@@ -552,7 +560,7 @@ impl UI {
     }
 
     fn is_ok_change_id(&self) -> bool {
-        machine_uid::get().is_ok()
+        hbb_common::machine_uid::get().is_ok()
     }
 
     fn get_async_job_status(&self) -> String {
@@ -587,8 +595,8 @@ impl UI {
         handle_relay_id(id)
     }
 
-    fn get_hostname(&self) -> String {
-        get_hostname()
+    fn get_login_device_info(&self) -> String {
+        get_login_device_info_json()
     }
 }
 
@@ -674,7 +682,7 @@ impl sciter::EventHandler for UI {
         fn get_langs();
         fn default_video_save_directory();
         fn handle_relay_id(String);
-        fn get_hostname();
+        fn get_login_device_info();
     }
 }
 
@@ -684,28 +692,6 @@ impl sciter::host::HostHandler for UIHostHandler {
     }
 }
 
-pub fn check_zombie(children: Children) {
-    let mut deads = Vec::new();
-    loop {
-        let mut lock = children.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));
-    }
-}
-
 #[cfg(not(target_os = "linux"))]
 fn get_sound_inputs() -> Vec<String> {
     let mut out = Vec::new();
@@ -739,50 +725,6 @@ pub fn value_crash_workaround(values: &[Value]) -> Arc<Vec<Value>> {
     persist
 }
 
-#[inline]
-pub fn new_remote(id: String, remote_type: String, force_relay: bool) {
-    let mut lock = CHILDREN.lock().unwrap();
-    let mut args = vec![format!("--{}", remote_type), id.clone()];
-    if force_relay {
-        args.push("".to_string()); // password
-        args.push("--relay".to_string());
-    }
-    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);
-        }
-    }
-}
-
-#[inline]
-pub fn recent_sessions_updated() -> bool {
-    let mut children = CHILDREN.lock().unwrap();
-    if children.0 {
-        children.0 = false;
-        true
-    } else {
-        false
-    }
-}
-
 pub fn get_icon() -> String {
     // 128x128
     #[cfg(target_os = "macos")]
diff --git a/src/ui/cm.rs b/src/ui/cm.rs
index a574b5e88..b827b76b1 100644
--- a/src/ui/cm.rs
+++ b/src/ui/cm.rs
@@ -59,13 +59,11 @@ impl InvokeUiCM for SciterHandler {
     fn update_voice_call_state(&self, client: &crate::ui_cm_interface::Client) {
         self.call(
             "updateVoiceCallState",
-            &make_args!(
-                client.id,
-                client.in_voice_call,
-                client.incoming_voice_call
-            ),
+            &make_args!(client.id, client.in_voice_call, client.incoming_voice_call),
         );
     }
+
+    fn file_transfer_log(&self, _log: String) {}
 }
 
 impl SciterHandler {
diff --git a/src/ui/header.tis b/src/ui/header.tis
index 2d79db8fa..2adc37027 100644
--- a/src/ui/header.tis
+++ b/src/ui/header.tis
@@ -297,6 +297,7 @@ class Header: Reactor.Component {
     event click $(span#recording) (_, me) {
         recording = !recording;
         header.update();
+        handler.record_status(recording);
         if (recording)
             handler.refresh_video();
         else
@@ -424,10 +425,22 @@ class Header: Reactor.Component {
 function handle_custom_image_quality() {
     var tmp = handler.get_custom_image_quality();
     var bitrate = (tmp[0] || 50);
-    msgbox("custom", "Custom Image Quality", "<div .form> \
-          <div><input type=\"hslider\" style=\"width: 50%\" name=\"bitrate\" max=\"100\" min=\"10\" value=\"" + bitrate + "\"/ buddy=\"bitrate-buddy\"><b #bitrate-buddy>x</b>% Bitrate</div> \
+    var extendedBitrate = bitrate > 100;
+    var maxRate = extendedBitrate ? 2000 : 100;
+    msgbox("custom-image-quality", "Custom Image Quality", "<div .form> \
+          <div><input #bitrate-slider type=\"hslider\" style=\"width: 50%\" name=\"bitrate\" max=\"" + maxRate + "\" min=\"10\" value=\"" + bitrate + "\"/ buddy=\"bitrate-buddy\"><b #bitrate-buddy>x</b>% Bitrate <button|checkbox #extended-slider .custom-event " + (extendedBitrate ? "checked" : "") + ">More</button></div> \
       </div>", "", function(res=null) {
         if (!res) return;
+        if (res.id === "extended-slider") {
+            var slider = res.parent.$(#bitrate-slider)
+            slider.slider.max = res.checked ? 2000 : 100;
+            if (slider.value > slider.slider.max) {
+                slider.value = slider.slider.max;
+            }
+            var buddy = res.parent.$(#bitrate-buddy);
+            buddy.value = slider.value;
+            return;
+        }
         if (!res.bitrate) return;
         handler.save_custom_image_quality(res.bitrate);
         toggleMenuState();
diff --git a/src/ui/index.tis b/src/ui/index.tis
index ecbf3ceac..9d09dd7a7 100644
--- a/src/ui/index.tis
+++ b/src/ui/index.tis
@@ -215,7 +215,7 @@ class Enhancements: Reactor.Component {
         return <li>{translate('Enhancements')}
             <menu #enhancements-menu>
                 {has_hwcodec ? <li #enable-hwcodec><span>{svg_checkmark}</span>{translate("Hardware Codec")} (beta)</li> : ""}
-                <li #enable-abr><span>{svg_checkmark}</span>{translate("Adaptive Bitrate")} (beta)</li>
+                <li #enable-abr><span>{svg_checkmark}</span>{translate("Adaptive bitrate")} (beta)</li>
                 <li #screen-recording>{translate("Recording")}</li>
             </menu>
         </li>;
@@ -365,7 +365,7 @@ class MyIdMenu: Reactor.Component {
         msgbox("custom-nocancel-nook-hasclose", translate("About") + " " + name, "<div style='line-height: 2em'> \
             <div>Version: " + handler.get_version() + " \
             <div>Fingerprint: " + handler.get_fingerprint() + " \
-            <div .link .custom-event url='https://rustdesk.com/privacy'>" + translate("Privacy Statement") + "</div> \
+            <div .link .custom-event url='https://rustdesk.com/privacy.html'>" + translate("Privacy Statement") + "</div> \
             <div .link .custom-event url='https://rustdesk.com'>" + translate("Website") + "</div> \
 	    <div style='background: #2c8cff; color: white; padding: 1em; margin-top: 1em;'>Copyright &copy; 2023 Purslane Ltd.\
             <br />" + handler.get_license() + " \
@@ -659,7 +659,7 @@ class UpdateMe: Reactor.Component {
     }
 
     event click $(#install-me) {
-        handler.open_url("https://rustdesk.com");
+        handler.open_url("https://rustdesk.com/download");
         return;
         if (!is_win) {
             handler.open_url("https://rustdesk.com");
@@ -1323,18 +1323,5 @@ function getHttpHeaders() {
 }
 
 function getDeviceInfo() {
-    var os;
-    if (is_win) {
-        os = 'windows';
-    } else if (is_linux) {
-        os = 'linux';
-    } else if (is_osx) {
-        os = 'macos';
-    }
-
-    return {
-        os: os,
-        type: 'client',
-        name: handler.get_hostname()
-    };
+    return JSON.parse(handler.get_login_device_info());
 }
diff --git a/src/ui/remote.rs b/src/ui/remote.rs
index 4c2af77bd..4ec0d5a5c 100644
--- a/src/ui/remote.rs
+++ b/src/ui/remote.rs
@@ -377,7 +377,7 @@ impl sciter::EventHandler for SciterSession {
                 let source = Element::from(source);
                 use sciter::dom::ELEMENT_AREAS;
                 let flags = ELEMENT_AREAS::CONTENT_BOX as u32 | ELEMENT_AREAS::SELF_RELATIVE as u32;
-                let rc = source.get_location(flags).unwrap();
+                let rc = source.get_location(flags).unwrap_or_default();
                 log::debug!(
                     "[video] start video thread on <{}> which is about {:?} pixels",
                     source,
@@ -451,6 +451,7 @@ impl sciter::EventHandler for SciterSession {
         fn save_custom_image_quality(i32);
         fn refresh_video();
         fn record_screen(bool, i32, i32);
+        fn record_status(bool);
         fn get_toggle_option(String);
         fn is_privacy_mode_supported();
         fn toggle_option(String);
diff --git a/src/ui_cm_interface.rs b/src/ui_cm_interface.rs
index a8778ac1a..02bf5721b 100644
--- a/src/ui_cm_interface.rs
+++ b/src/ui_cm_interface.rs
@@ -69,7 +69,6 @@ struct IpcTaskRunner<T: InvokeUiCM> {
     rx: mpsc::UnboundedReceiver<Data>,
     close: bool,
     running: bool,
-    authorized: bool,
     conn_id: i32,
     #[cfg(any(target_os = "windows", target_os = "linux"))]
     file_transfer_enabled: bool,
@@ -101,6 +100,8 @@ pub trait InvokeUiCM: Send + Clone + 'static + Sized {
     fn show_elevation(&self, show: bool);
 
     fn update_voice_call_state(&self, client: &Client);
+
+    fn file_transfer_log(&self, log: String);
 }
 
 impl<T: InvokeUiCM> Deref for ConnectionManager<T> {
@@ -163,6 +164,16 @@ impl<T: InvokeUiCM> ConnectionManager<T> {
         self.ui_handler.add_connection(&client);
     }
 
+    #[inline]
+    fn is_authorized(&self, id: i32) -> bool {
+        CLIENTS
+            .read()
+            .unwrap()
+            .get(&id)
+            .map(|c| c.authorized)
+            .unwrap_or(false)
+    }
+
     fn remove_connection(&self, id: i32, close: bool) {
         if close {
             CLIENTS.write().unwrap().remove(&id);
@@ -318,12 +329,15 @@ impl<T: InvokeUiCM> IpcTaskRunner<T> {
         // for tmp use, without real conn id
         let mut write_jobs: Vec<fs::TransferJob> = Vec::new();
 
+        #[cfg(any(target_os = "windows", target_os = "linux"))]
+        let is_authorized = self.cm.is_authorized(self.conn_id);
+
         #[cfg(any(target_os = "windows", target_os = "linux"))]
         let rx_clip1;
         let mut rx_clip;
         let _tx_clip;
         #[cfg(any(target_os = "windows", target_os = "linux"))]
-        if self.conn_id > 0 && self.authorized {
+        if self.conn_id > 0 && is_authorized {
             rx_clip1 = clipboard::get_rx_cliprdr_server(self.conn_id);
             rx_clip = rx_clip1.lock().await;
         } else {
@@ -347,6 +361,7 @@ impl<T: InvokeUiCM> IpcTaskRunner<T> {
                 );
             }
         }
+        let (tx_log, mut rx_log) = mpsc::unbounded_channel::<String>();
 
         self.running = false;
         loop {
@@ -362,7 +377,6 @@ impl<T: InvokeUiCM> IpcTaskRunner<T> {
                                 Data::Login{id, is_file_transfer, port_forward, peer_id, name, authorized, keyboard, clipboard, audio, file, file_transfer_enabled: _file_transfer_enabled, restart, recording, from_switch} => {
                                     log::debug!("conn_id: {}", id);
                                     self.cm.add_connection(id, is_file_transfer, port_forward, peer_id, name, authorized, keyboard, clipboard, audio, file, restart, recording, from_switch,self.tx.clone());
-                                    self.authorized = authorized;
                                     self.conn_id = id;
                                     #[cfg(windows)]
                                     {
@@ -394,11 +408,16 @@ impl<T: InvokeUiCM> IpcTaskRunner<T> {
                                     if let ipc::FS::WriteBlock { id, file_num, data: _, compressed } = fs {
                                         if let Ok(bytes) = self.stream.next_raw().await {
                                             fs = ipc::FS::WriteBlock{id, file_num, data:bytes.into(), compressed};
-                                            handle_fs(fs, &mut write_jobs, &self.tx).await;
+                                            handle_fs(fs, &mut write_jobs, &self.tx, Some(&tx_log)).await;
                                         }
                                     } else {
-                                        handle_fs(fs, &mut write_jobs, &self.tx).await;
+                                        handle_fs(fs, &mut write_jobs, &self.tx, Some(&tx_log)).await;
                                     }
+                                    let log = fs::serialize_transfer_jobs(&write_jobs);
+                                    self.cm.ui_handler.file_transfer_log(log);
+                                }
+                                Data::FileTransferLog(log) => {
+                                    self.cm.ui_handler.file_transfer_log(log);
                                 }
                                 #[cfg(not(any(target_os = "android", target_os = "ios")))]
                                 Data::ClipboardFile(_clip) => {
@@ -414,6 +433,10 @@ impl<T: InvokeUiCM> IpcTaskRunner<T> {
                                         if stop {
                                             ContextSend::set_is_stopped();
                                         } else {
+                                            if !is_authorized {
+                                                log::debug!("Clipboard message from client peer, but not authorized");
+                                                continue;
+                                            }
                                             let conn_id = self.conn_id;
                                             let _ = ContextSend::proc(|context| -> ResultType<()> {
                                                 context.server_clip_file(conn_id, _clip)
@@ -456,16 +479,24 @@ impl<T: InvokeUiCM> IpcTaskRunner<T> {
                     }
                 }
                 Some(data) = self.rx.recv() => {
-                    if let Data::SwitchPermission{name: _name, enabled: _enabled} = &data {
-                        #[cfg(windows)]
-                        if _name == "file" {
-                            self.file_transfer_enabled = *_enabled;
-                        }
-                    }
                     if self.stream.send(&data).await.is_err() {
                         break;
                     }
-                }
+                    match &data {
+                        Data::SwitchPermission{name: _name, enabled: _enabled} => {
+                            #[cfg(windows)]
+                            if _name == "file" {
+                                self.file_transfer_enabled = *_enabled;
+                            }
+                        }
+                        Data::Authorize => {
+                            self.running = true;
+                            break;
+                        }
+                        _ => {
+                        }
+                    }
+                },
                 clip_file = rx_clip.recv() => match clip_file {
                     Some(_clip) => {
                         #[cfg(any(target_os = "windows", target_os ="linux"))]
@@ -489,6 +520,9 @@ impl<T: InvokeUiCM> IpcTaskRunner<T> {
                         //
                     }
                 },
+                Some(job_log) = rx_log.recv() => {
+                    self.cm.ui_handler.file_transfer_log(job_log);
+                }
             }
         }
     }
@@ -503,7 +537,6 @@ impl<T: InvokeUiCM> IpcTaskRunner<T> {
             rx,
             close: true,
             running: true,
-            authorized: false,
             conn_id: 0,
             #[cfg(any(target_os = "windows", target_os = "linux"))]
             file_transfer_enabled: false,
@@ -535,7 +568,6 @@ pub async fn start_ipc<T: InvokeUiCM>(cm: ConnectionManager<T>) {
                 e
             );
         }
-        allow_err!(crate::privacy_win_mag::start());
     });
 
     #[cfg(target_os = "windows")]
@@ -614,7 +646,7 @@ pub async fn start_listen<T: InvokeUiCM>(
                 cm.new_message(current_id, text);
             }
             Some(Data::FS(fs)) => {
-                handle_fs(fs, &mut write_jobs, &tx).await;
+                handle_fs(fs, &mut write_jobs, &tx, None).await;
             }
             Some(Data::Close) => {
                 break;
@@ -629,7 +661,14 @@ pub async fn start_listen<T: InvokeUiCM>(
 }
 
 #[cfg(not(any(target_os = "ios")))]
-async fn handle_fs(fs: ipc::FS, write_jobs: &mut Vec<fs::TransferJob>, tx: &UnboundedSender<Data>) {
+async fn handle_fs(
+    fs: ipc::FS,
+    write_jobs: &mut Vec<fs::TransferJob>,
+    tx: &UnboundedSender<Data>,
+    tx_log: Option<&UnboundedSender<String>>,
+) {
+    use hbb_common::fs::serialize_transfer_job;
+
     match fs {
         ipc::FS::ReadDir {
             dir,
@@ -656,10 +695,12 @@ async fn handle_fs(fs: ipc::FS, write_jobs: &mut Vec<fs::TransferJob>, tx: &Unbo
             file_num,
             mut files,
             overwrite_detection,
+            total_size,
+            conn_id,
         } => {
             // cm has no show_hidden context
             // dummy remote, show_hidden, is_remote
-            write_jobs.push(fs::TransferJob::new_write(
+            let mut job = fs::TransferJob::new_write(
                 id,
                 "".to_string(),
                 path,
@@ -675,11 +716,17 @@ async fn handle_fs(fs: ipc::FS, write_jobs: &mut Vec<fs::TransferJob>, tx: &Unbo
                     })
                     .collect(),
                 overwrite_detection,
-            ));
+            );
+            job.total_size = total_size;
+            job.conn_id = conn_id;
+            write_jobs.push(job);
         }
         ipc::FS::CancelWrite { id } => {
             if let Some(job) = fs::get_job(id, write_jobs) {
                 job.remove_download_file();
+                tx_log.map(|tx: &UnboundedSender<String>| {
+                    tx.send(serialize_transfer_job(job, false, true, ""))
+                });
                 fs::remove_job(id, write_jobs);
             }
         }
@@ -687,11 +734,13 @@ async fn handle_fs(fs: ipc::FS, write_jobs: &mut Vec<fs::TransferJob>, tx: &Unbo
             if let Some(job) = fs::get_job(id, write_jobs) {
                 job.modify_time();
                 send_raw(fs::new_done(id, file_num), tx);
+                tx_log.map(|tx| tx.send(serialize_transfer_job(job, true, false, "")));
                 fs::remove_job(id, write_jobs);
             }
         }
         ipc::FS::WriteError { id, file_num, err } => {
             if let Some(job) = fs::get_job(id, write_jobs) {
+                tx_log.map(|tx| tx.send(serialize_transfer_job(job, false, false, &err)));
                 send_raw(fs::new_error(job.id(), err, file_num), tx);
                 fs::remove_job(job.id(), write_jobs);
             }
diff --git a/src/ui_interface.rs b/src/ui_interface.rs
index 2232f3c42..ed2b4f4fc 100644
--- a/src/ui_interface.rs
+++ b/src/ui_interface.rs
@@ -44,6 +44,13 @@ pub struct UiStatus {
     pub id: String,
 }
 
+#[derive(Debug, Clone, Serialize)]
+pub struct LoginDeviceInfo {
+    pub os: String,
+    pub r#type: String,
+    pub name: String,
+}
+
 lazy_static::lazy_static! {
     static ref UI_STATUS : Arc<Mutex<UiStatus>> = Arc::new(Mutex::new(UiStatus{
         status_num: 0,
@@ -63,6 +70,7 @@ lazy_static::lazy_static! {
     static ref OPTION_SYNCED: Arc<Mutex<bool>> = Default::default();
     static ref OPTIONS : Arc<Mutex<HashMap<String, String>>> = Arc::new(Mutex::new(Config::get_options()));
     pub static ref SENDER : Mutex<mpsc::UnboundedSender<ipc::Data>> = Mutex::new(check_connect_status(true));
+    static ref CHILDREN : Children = Default::default();
 }
 
 const INIT_ASYNC_JOB_STATUS: &str = " ";
@@ -118,7 +126,7 @@ pub fn show_run_without_install() -> bool {
 #[inline]
 pub fn get_license() -> String {
     #[cfg(windows)]
-    if let Some(lic) = crate::platform::windows::get_license() {
+    if let Ok(lic) = crate::platform::windows::get_license_from_exe_name() {
         #[cfg(feature = "flutter")]
         return format!("Key: {}\nHost: {}\nApi: {}", lic.key, lic.host, lic.api);
         // default license format is html formed (sciter)
@@ -160,14 +168,14 @@ pub fn set_local_option(key: String, value: String) {
 
 #[cfg(any(target_os = "android", target_os = "ios", feature = "flutter"))]
 #[inline]
-pub fn get_local_flutter_config(key: String) -> String {
-    LocalConfig::get_flutter_config(&key)
+pub fn get_local_flutter_option(key: String) -> String {
+    LocalConfig::get_flutter_option(&key)
 }
 
 #[cfg(any(target_os = "android", target_os = "ios", feature = "flutter"))]
 #[inline]
-pub fn set_local_flutter_config(key: String, value: String) {
-    LocalConfig::set_flutter_config(key, value);
+pub fn set_local_flutter_option(key: String, value: String) {
+    LocalConfig::set_flutter_option(key, value);
 }
 
 #[cfg(feature = "flutter")]
@@ -200,6 +208,25 @@ pub fn get_peer_option(id: String, name: String) -> String {
     c.options.get(&name).unwrap_or(&"".to_owned()).to_owned()
 }
 
+#[inline]
+#[cfg(feature = "flutter")]
+pub fn get_peer_flutter_option(id: String, name: String) -> String {
+    let c = PeerConfig::load(&id);
+    c.ui_flutter.get(&name).unwrap_or(&"".to_owned()).to_owned()
+}
+
+#[inline]
+#[cfg(feature = "flutter")]
+pub fn set_peer_flutter_option(id: String, name: String, value: String) {
+    let mut c = PeerConfig::load(&id);
+    if value.is_empty() {
+        c.ui_flutter.remove(&name);
+    } else {
+        c.ui_flutter.insert(name, value);
+    }
+    c.store(&id);
+}
+
 #[inline]
 pub fn set_peer_option(id: String, name: String, value: String) {
     let mut c = PeerConfig::load(&id);
@@ -233,7 +260,7 @@ pub fn get_options() -> String {
     for (k, v) in options.iter() {
         m.insert(k.into(), v.to_owned().into());
     }
-    serde_json::to_string(&m).unwrap()
+    serde_json::to_string(&m).unwrap_or_default()
 }
 
 #[inline]
@@ -567,7 +594,7 @@ pub fn current_is_wayland() -> bool {
 
 #[inline]
 pub fn get_new_version() -> String {
-    hbb_common::get_version_from_url(&*SOFTWARE_UPDATE_URL.lock().unwrap())
+    (*SOFTWARE_UPDATE_URL.lock().unwrap().rsplit('/').next().unwrap_or("")).to_string()
 }
 
 #[inline]
@@ -598,6 +625,7 @@ pub fn discover() {
 
 #[cfg(feature = "flutter")]
 pub fn peer_to_map(id: String, p: PeerConfig) -> HashMap<&'static str, String> {
+    use hbb_common::sodiumoxide::base64;
     HashMap::<&str, String>::from_iter([
         ("id", id),
         ("username", p.info.username.clone()),
@@ -607,9 +635,18 @@ pub fn peer_to_map(id: String, p: PeerConfig) -> HashMap<&'static str, String> {
             "alias",
             p.options.get("alias").unwrap_or(&"".to_owned()).to_owned(),
         ),
+        (
+            "hash",
+            base64::encode(p.password, base64::Variant::Original),
+        ),
     ])
 }
 
+#[cfg(feature = "flutter")]
+pub fn peer_exists(id: &str) -> bool {
+    PeerConfig::exists(id)
+}
+
 #[inline]
 pub fn get_lan_peers() -> Vec<HashMap<&'static str, String>> {
     config::LanPeers::load()
@@ -815,11 +852,11 @@ pub fn check_super_user_permission() -> bool {
     return true;
 }
 
-#[allow(dead_code)]
-pub fn check_zombie(children: Children) {
+#[cfg(not(any(target_os = "android", target_os = "ios", feature = "flutter")))]
+pub fn check_zombie() {
     let mut deads = Vec::new();
     loop {
-        let mut lock = children.lock().unwrap();
+        let mut lock = CHILDREN.lock().unwrap();
         let mut n = 0;
         for (id, c) in lock.1.iter_mut() {
             if let Ok(Some(_)) = c.try_wait() {
@@ -838,6 +875,51 @@ pub fn check_zombie(children: Children) {
     }
 }
 
+#[inline]
+#[cfg(not(any(target_os = "android", target_os = "ios", feature = "flutter")))]
+pub fn recent_sessions_updated() -> bool {
+    let mut children = CHILDREN.lock().unwrap();
+    if children.0 {
+        children.0 = false;
+        true
+    } else {
+        false
+    }
+}
+
+#[cfg(not(any(target_os = "android", target_os = "ios", feature = "flutter")))]
+pub fn new_remote(id: String, remote_type: String, force_relay: bool) {
+    let mut lock = CHILDREN.lock().unwrap();
+    let mut args = vec![format!("--{}", remote_type), id.clone()];
+    if force_relay {
+        args.push("".to_string()); // password
+        args.push("--relay".to_string());
+    }
+    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);
+        }
+    }
+}
+
 // Make sure `SENDER` is inited here.
 #[inline]
 #[cfg(not(any(target_os = "android", target_os = "ios")))]
@@ -855,7 +937,7 @@ fn check_connect_status(reconnect: bool) -> mpsc::UnboundedSender<ipc::Data> {
 
 #[cfg(feature = "flutter")]
 pub fn account_auth(op: String, id: String, uuid: String, remember_me: bool) {
-    account::OidcSession::account_auth(op, id, uuid, remember_me);
+    account::OidcSession::account_auth(get_api_server(), op, id, uuid, remember_me);
 }
 
 #[cfg(feature = "flutter")]
@@ -891,8 +973,19 @@ pub fn get_fingerprint() -> String {
     return ipc::get_fingerprint();
 }
 
-pub fn get_hostname() -> String {
-    crate::common::hostname()
+#[inline]
+pub fn get_login_device_info() -> LoginDeviceInfo {
+    LoginDeviceInfo {
+        // std::env::consts::OS is better than whoami::platform() here.
+        os: std::env::consts::OS.to_owned(),
+        r#type: "client".to_owned(),
+        name: crate::common::hostname(),
+    }
+}
+
+#[inline]
+pub fn get_login_device_info_json() -> String {
+    serde_json::to_string(&get_login_device_info()).unwrap_or("{}".to_string())
 }
 
 // notice: avoiding create ipc connection repeatedly,
@@ -1027,8 +1120,10 @@ const UNKNOWN_ERROR: &'static str = "Unknown error";
 
 #[inline]
 #[tokio::main(flavor = "current_thread")]
-pub async fn change_id_shared(id: String, old_id: String) {
-    *ASYNC_JOB_STATUS.lock().unwrap() = change_id_shared_(id, old_id).await.to_owned();
+pub async fn change_id_shared(id: String, old_id: String) -> String {
+    let res = change_id_shared_(id, old_id).await.to_owned();
+    *ASYNC_JOB_STATUS.lock().unwrap() = res.clone();
+    res
 }
 
 pub async fn change_id_shared_(id: String, old_id: String) -> &'static str {
@@ -1037,7 +1132,12 @@ pub async fn change_id_shared_(id: String, old_id: String) -> &'static str {
     }
 
     #[cfg(not(any(target_os = "android", target_os = "ios")))]
-    let uuid = Bytes::from(machine_uid::get().unwrap_or("".to_owned()).as_bytes().to_vec());
+    let uuid = Bytes::from(
+        hbb_common::machine_uid::get()
+            .unwrap_or("".to_owned())
+            .as_bytes()
+            .to_vec(),
+    );
     #[cfg(any(target_os = "android", target_os = "ios"))]
     let uuid = Bytes::from(hbb_common::get_uuid());
 
@@ -1105,23 +1205,23 @@ async fn check_id(
             {
                 match msg_in.union {
                     Some(rendezvous_message::Union::RegisterPkResponse(rpr)) => {
-                        match rpr.result.enum_value_or_default() {
-                            register_pk_response::Result::OK => {
+                        match rpr.result.enum_value() {
+                            Ok(register_pk_response::Result::OK) => {
                                 ok = true;
                             }
-                            register_pk_response::Result::ID_EXISTS => {
+                            Ok(register_pk_response::Result::ID_EXISTS) => {
                                 return "Not available";
                             }
-                            register_pk_response::Result::TOO_FREQUENT => {
+                            Ok(register_pk_response::Result::TOO_FREQUENT) => {
                                 return "Too frequent";
                             }
-                            register_pk_response::Result::NOT_SUPPORT => {
+                            Ok(register_pk_response::Result::NOT_SUPPORT) => {
                                 return "server_not_support";
                             }
-                            register_pk_response::Result::SERVER_ERROR => {
+                            Ok(register_pk_response::Result::SERVER_ERROR) => {
                                 return "Server error";
                             }
-                            register_pk_response::Result::INVALID_ID_FORMAT => {
+                            Ok(register_pk_response::Result::INVALID_ID_FORMAT) => {
                                 return INVALID_FORMAT;
                             }
                             _ => {}
diff --git a/src/ui_session_interface.rs b/src/ui_session_interface.rs
index e821b71dd..a8304b5d0 100644
--- a/src/ui_session_interface.rs
+++ b/src/ui_session_interface.rs
@@ -1,3 +1,4 @@
+use crate::input::{MOUSE_BUTTON_LEFT, MOUSE_TYPE_DOWN, MOUSE_TYPE_UP};
 #[cfg(not(any(target_os = "android", target_os = "ios")))]
 use std::{collections::HashMap, sync::atomic::AtomicBool};
 use std::{
@@ -7,7 +8,7 @@ use std::{
         atomic::{AtomicUsize, Ordering},
         Arc, Mutex, RwLock,
     },
-    time::{Duration, SystemTime},
+    time::SystemTime,
 };
 
 use async_trait::async_trait;
@@ -34,8 +35,8 @@ use hbb_common::{
 use crate::client::io_loop::Remote;
 use crate::client::{
     check_if_retry, handle_hash, handle_login_error, handle_login_from_ui, handle_test_delay,
-    input_os_password, load_config, send_mouse, start_video_audio_threads, FileManager, Key,
-    LoginConfigHandler, QualityStatus, KEY_MAP,
+    input_os_password, load_config, send_mouse, send_pointer_device_event,
+    start_video_audio_threads, FileManager, Key, LoginConfigHandler, QualityStatus, KEY_MAP,
 };
 #[cfg(not(any(target_os = "android", target_os = "ios")))]
 use crate::common::GrabState;
@@ -49,8 +50,8 @@ const CHANGE_RESOLUTION_VALID_TIMEOUT_SECS: u64 = 15;
 
 #[derive(Clone, Default)]
 pub struct Session<T: InvokeUiSession> {
-    pub session_id: SessionID,
-    pub id: String, // peer id
+    pub session_id: SessionID, // different from the one in LoginConfigHandler, used for flutter UI message pass
+    pub id: String,            // peer id
     pub password: String,
     pub args: Vec<String>,
     pub lc: Arc<RwLock<LoginConfigHandler>>,
@@ -146,12 +147,6 @@ impl<T: InvokeUiSession> Session<T> {
         self.lc.read().unwrap().conn_type.eq(&ConnType::RDP)
     }
 
-    pub fn set_connection_info(&mut self, direct: bool, received: bool) {
-        let mut lc = self.lc.write().unwrap();
-        lc.direct = Some(direct);
-        lc.received = received;
-    }
-
     pub fn get_view_style(&self) -> String {
         self.lc.read().unwrap().view_style.clone()
     }
@@ -188,12 +183,12 @@ impl<T: InvokeUiSession> Session<T> {
         self.lc.write().unwrap().save_scroll_style(value);
     }
 
-    pub fn save_flutter_config(&mut self, k: String, v: String) {
+    pub fn save_flutter_option(&mut self, k: String, v: String) {
         self.lc.write().unwrap().save_ui_flutter(k, v);
     }
 
-    pub fn get_flutter_config(&self, k: String) -> String {
-        self.lc.write().unwrap().get_ui_flutter(&k)
+    pub fn get_flutter_option(&self, k: String) -> String {
+        self.lc.read().unwrap().get_ui_flutter(&k)
     }
 
     pub fn toggle_option(&mut self, name: String) {
@@ -231,6 +226,14 @@ impl<T: InvokeUiSession> Session<T> {
         self.send(Data::RecordScreen(start, w, h, self.id.clone()));
     }
 
+    pub fn record_status(&self, status: bool) {
+        let mut misc = Misc::new();
+        misc.set_client_record_status(status);
+        let mut msg = Message::new();
+        msg.set_misc(misc);
+        self.send(Data::Message(msg));
+    }
+
     pub fn save_custom_image_quality(&mut self, custom_image_quality: i32) {
         let msg = self
             .lc
@@ -312,9 +315,7 @@ impl<T: InvokeUiSession> Session<T> {
     }
 
     pub fn get_audit_server(&self, typ: String) -> String {
-        if self.lc.read().unwrap().conn_id <= 0
-            || LocalConfig::get_option("access_token").is_empty()
-        {
+        if LocalConfig::get_option("access_token").is_empty() {
             return "".to_owned();
         }
         crate::get_audit_server(
@@ -327,9 +328,9 @@ impl<T: InvokeUiSession> Session<T> {
     pub fn send_note(&self, note: String) {
         let url = self.get_audit_server("conn".to_string());
         let id = self.id.clone();
-        let conn_id = self.lc.read().unwrap().conn_id;
+        let session_id = self.lc.read().unwrap().session_id;
         std::thread::spawn(move || {
-            send_note(url, id, conn_id, note);
+            send_note(url, id, session_id, note);
         });
     }
 
@@ -419,7 +420,7 @@ impl<T: InvokeUiSession> Session<T> {
 
     pub fn get_path_sep(&self, is_remote: bool) -> &'static str {
         let p = self.get_platform(is_remote);
-        if &p == "Windows" {
+        if &p == crate::PLATFORM_WINDOWS {
             return "\\";
         } else {
             return "/";
@@ -697,6 +698,61 @@ impl<T: InvokeUiSession> Session<T> {
         self.send_key_event(&key_event);
     }
 
+    pub fn send_touch_scale(&self, scale: i32, alt: bool, ctrl: bool, shift: bool, command: bool) {
+        let scale_evt = TouchScaleUpdate {
+            scale,
+            ..Default::default()
+        };
+        let mut touch_evt = TouchEvent::new();
+        touch_evt.set_scale_update(scale_evt);
+        let mut evt = PointerDeviceEvent::new();
+        evt.set_touch_event(touch_evt);
+        send_pointer_device_event(evt, alt, ctrl, shift, command, self);
+    }
+
+    pub fn send_touch_pan_event(
+        &self,
+        event: &str,
+        x: i32,
+        y: i32,
+        alt: bool,
+        ctrl: bool,
+        shift: bool,
+        command: bool,
+    ) {
+        let mut touch_evt = TouchEvent::new();
+        match event {
+            "pan_start" => {
+                touch_evt.set_pan_start(TouchPanStart {
+                    x,
+                    y,
+                    ..Default::default()
+                });
+            }
+            "pan_update" => {
+                touch_evt.set_pan_update(TouchPanUpdate {
+                    x,
+                    y,
+                    ..Default::default()
+                });
+            }
+            "pan_end" => {
+                touch_evt.set_pan_end(TouchPanEnd {
+                    x,
+                    y,
+                    ..Default::default()
+                });
+            }
+            _ => {
+                log::warn!("unknown touch pan event: {}", event);
+                return;
+            }
+        };
+        let mut evt = PointerDeviceEvent::new();
+        evt.set_touch_event(touch_evt);
+        send_pointer_device_event(evt, alt, ctrl, shift, command, self);
+    }
+
     pub fn send_mouse(
         &self,
         mask: i32,
@@ -727,8 +783,20 @@ impl<T: InvokeUiSession> Session<T> {
         if cfg!(target_os = "macos") {
             let buttons = mask >> 3;
             let evt_type = mask & 0x7;
-            if buttons == 1 && evt_type == 1 && ctrl && self.peer_platform() != "Mac OS" {
-                self.send_mouse((1 << 3 | 2) as _, x, y, alt, ctrl, shift, command);
+            if buttons == MOUSE_BUTTON_LEFT
+                && evt_type == MOUSE_TYPE_DOWN
+                && ctrl
+                && self.peer_platform() != "Mac OS"
+            {
+                self.send_mouse(
+                    (MOUSE_BUTTON_LEFT << 3 | MOUSE_TYPE_UP) as _,
+                    x,
+                    y,
+                    alt,
+                    ctrl,
+                    shift,
+                    command,
+                );
             }
         }
     }
@@ -888,34 +956,49 @@ impl<T: InvokeUiSession> Session<T> {
         }
     }
 
-    pub fn handle_peer_switch_display(&self, display: &SwitchDisplay) {
-        self.ui_handler.switch_display(display);
-
-        if self.last_change_display.lock().unwrap().is_the_same_record(
-            display.display,
-            display.width,
-            display.height,
-        ) {
-            let custom_resolution = if display.width != display.original_resolution.width
-                || display.height != display.original_resolution.height
-            {
-                Some((display.width, display.height))
-            } else {
-                None
-            };
+    fn set_custom_resolution(&self, display: &SwitchDisplay) {
+        if display.width == display.original_resolution.width
+            && display.height == display.original_resolution.height
+        {
             self.lc
                 .write()
                 .unwrap()
-                .set_custom_resolution(display.display, custom_resolution);
+                .set_custom_resolution(display.display, None);
+        } else {
+            let last_change_display = self.last_change_display.lock().unwrap();
+            if last_change_display.display == display.display {
+                let wh = if last_change_display.is_the_same_record(
+                    display.display,
+                    display.width,
+                    display.height,
+                ) {
+                    Some((display.width, display.height))
+                } else {
+                    // display origin is changed, or some other events.
+                    None
+                };
+                self.lc
+                    .write()
+                    .unwrap()
+                    .set_custom_resolution(display.display, wh);
+            }
         }
     }
 
+    #[inline]
+    pub fn handle_peer_switch_display(&self, display: &SwitchDisplay) {
+        self.ui_handler.switch_display(display);
+        self.set_custom_resolution(display);
+    }
+
+    #[inline]
     pub fn change_resolution(&self, display: i32, width: i32, height: i32) {
         *self.last_change_display.lock().unwrap() =
             ChangeDisplayRecord::new(display, width, height);
         self.do_change_resolution(width, height);
     }
 
+    #[inline]
     fn try_change_init_resolution(&self, display: i32) {
         if let Some((w, h)) = self.lc.read().unwrap().get_custom_resolution(display) {
             self.do_change_resolution(w, h);
@@ -934,45 +1017,15 @@ impl<T: InvokeUiSession> Session<T> {
         self.send(Data::Message(msg));
     }
 
+    #[inline]
     pub fn request_voice_call(&self) {
         self.send(Data::NewVoiceCall);
     }
 
+    #[inline]
     pub fn close_voice_call(&self) {
         self.send(Data::CloseVoiceCall);
     }
-
-    pub fn show_relay_hint(
-        &mut self,
-        last_recv_time: tokio::time::Instant,
-        msgtype: &str,
-        title: &str,
-        text: &str,
-    ) -> bool {
-        let duration = Duration::from_secs(3);
-        let counter_interval = 3;
-        let lock = self.lc.read().unwrap();
-        let success_time = lock.success_time;
-        let direct = lock.direct.unwrap_or(false);
-        let received = lock.received;
-        drop(lock);
-        if let Some(success_time) = success_time {
-            if direct && last_recv_time.duration_since(success_time) < duration {
-                let retry_for_relay = direct && !received;
-                let retry = check_if_retry(msgtype, title, text, retry_for_relay);
-                if retry && !retry_for_relay {
-                    self.lc.write().unwrap().direct_error_counter += 1;
-                    if self.lc.read().unwrap().direct_error_counter % counter_interval == 0 {
-                        #[cfg(feature = "flutter")]
-                        return true;
-                    }
-                }
-            } else {
-                self.lc.write().unwrap().direct_error_counter = 0;
-            }
-        }
-        false
-    }
 }
 
 pub trait InvokeUiSession: Send + Sync + Clone + 'static + Sized + Default {
@@ -1060,9 +1113,9 @@ impl<T: InvokeUiSession> Interface for Session<T> {
     }
 
     fn msgbox(&self, msgtype: &str, title: &str, text: &str, link: &str) {
-        let direct = self.lc.read().unwrap().direct.unwrap_or_default();
+        let direct = self.lc.read().unwrap().direct;
         let received = self.lc.read().unwrap().received;
-        let retry_for_relay = direct && !received;
+        let retry_for_relay = direct == Some(true) && !received;
         let retry = check_if_retry(msgtype, title, text, retry_for_relay);
         self.ui_handler.msgbox(msgtype, title, text, link, retry);
     }
@@ -1119,7 +1172,6 @@ impl<T: InvokeUiSession> Interface for Session<T> {
                 "Connected, waiting for image...",
                 "",
             );
-            self.lc.write().unwrap().success_time = Some(tokio::time::Instant::now());
         }
         self.on_connected(self.lc.read().unwrap().conn_type);
         #[cfg(windows)]
@@ -1201,6 +1253,12 @@ impl<T: InvokeUiSession> Session<T> {
 
 #[tokio::main(flavor = "current_thread")]
 pub async fn io_loop<T: InvokeUiSession>(handler: Session<T>) {
+    // It is ok to call this function multiple times.
+    #[cfg(target_os = "windows")]
+    if !handler.is_file_transfer() && !handler.is_port_forward() {
+        clipboard::ContextSend::enable(true);
+    }
+
     #[cfg(any(target_os = "android", target_os = "ios"))]
     let (sender, receiver) = mpsc::unbounded_channel::<Data>();
     #[cfg(not(any(target_os = "android", target_os = "ios")))]
@@ -1347,7 +1405,7 @@ async fn start_one_port_forward<T: InvokeUiSession>(
 }
 
 #[tokio::main(flavor = "current_thread")]
-async fn send_note(url: String, id: String, conn_id: i32, note: String) {
-    let body = serde_json::json!({ "id": id, "conn_id": conn_id, "note": note });
+async fn send_note(url: String, id: String, sid: u64, note: String) {
+    let body = serde_json::json!({ "id": id, "session_id": sid, "note": note });
     allow_err!(crate::post_request(url, body.to_string(), "").await);
 }