mirror of
https://github.com/zhom/donutbrowser.git
synced 2026-08-05 10:48:41 +02:00
Compare commits
10
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
beea23307b | ||
|
|
19b66d006d | ||
|
|
c7a36f6cd0 | ||
|
|
7404cb3ff8 | ||
|
|
ee91445fe1 | ||
|
|
77d53c7f32 | ||
|
|
a21f22a916 | ||
|
|
93eae1d77f | ||
|
|
82615c24bd | ||
|
|
0769106a51 |
@@ -5,7 +5,7 @@ updates:
|
|||||||
directory: "/"
|
directory: "/"
|
||||||
schedule:
|
schedule:
|
||||||
interval: "weekly"
|
interval: "weekly"
|
||||||
day: "monday"
|
day: "saturday"
|
||||||
time: "09:00"
|
time: "09:00"
|
||||||
allow:
|
allow:
|
||||||
- dependency-type: "all"
|
- dependency-type: "all"
|
||||||
@@ -25,7 +25,7 @@ updates:
|
|||||||
directory: "/nodecar"
|
directory: "/nodecar"
|
||||||
schedule:
|
schedule:
|
||||||
interval: "weekly"
|
interval: "weekly"
|
||||||
day: "monday"
|
day: "saturday"
|
||||||
time: "09:00"
|
time: "09:00"
|
||||||
allow:
|
allow:
|
||||||
- dependency-type: "all"
|
- dependency-type: "all"
|
||||||
@@ -42,7 +42,7 @@ updates:
|
|||||||
directory: "/src-tauri"
|
directory: "/src-tauri"
|
||||||
schedule:
|
schedule:
|
||||||
interval: "weekly"
|
interval: "weekly"
|
||||||
day: "monday"
|
day: "saturday"
|
||||||
time: "09:00"
|
time: "09:00"
|
||||||
allow:
|
allow:
|
||||||
- dependency-type: "all"
|
- dependency-type: "all"
|
||||||
@@ -59,7 +59,7 @@ updates:
|
|||||||
directory: "/"
|
directory: "/"
|
||||||
schedule:
|
schedule:
|
||||||
interval: "weekly"
|
interval: "weekly"
|
||||||
day: "monday"
|
day: "saturday"
|
||||||
time: "09:00"
|
time: "09:00"
|
||||||
groups:
|
groups:
|
||||||
github-actions:
|
github-actions:
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ jobs:
|
|||||||
target: "x86_64-unknown-linux-gnu"
|
target: "x86_64-unknown-linux-gnu"
|
||||||
pkg_target: "latest-linux-x64"
|
pkg_target: "latest-linux-x64"
|
||||||
nodecar_script: "build:linux-x64"
|
nodecar_script: "build:linux-x64"
|
||||||
- platform: "ubuntu-22.04"
|
- platform: "ubuntu-22.04-arm"
|
||||||
args: "--target aarch64-unknown-linux-gnu"
|
args: "--target aarch64-unknown-linux-gnu"
|
||||||
arch: "aarch64"
|
arch: "aarch64"
|
||||||
target: "aarch64-unknown-linux-gnu"
|
target: "aarch64-unknown-linux-gnu"
|
||||||
@@ -101,14 +101,10 @@ jobs:
|
|||||||
targets: ${{ matrix.target }}
|
targets: ${{ matrix.target }}
|
||||||
|
|
||||||
- name: Install dependencies (Ubuntu only)
|
- name: Install dependencies (Ubuntu only)
|
||||||
if: matrix.platform == 'ubuntu-22.04'
|
if: matrix.platform == 'ubuntu-22.04' || matrix.platform == 'ubuntu-22.04-arm'
|
||||||
run: |
|
run: |
|
||||||
sudo apt-get update
|
sudo apt-get update
|
||||||
sudo apt-get install -y libwebkit2gtk-4.1-dev libgtk-3-dev libayatana-appindicator3-dev librsvg2-dev
|
sudo apt-get install -y libwebkit2gtk-4.1-dev libgtk-3-dev libayatana-appindicator3-dev librsvg2-dev pkg-config xdg-utils
|
||||||
# Install cross-compilation tools for ARM64
|
|
||||||
if [[ "${{ matrix.arch }}" == "aarch64" ]]; then
|
|
||||||
sudo apt-get install -y gcc-aarch64-linux-gnu
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Rust cache
|
- name: Rust cache
|
||||||
uses: swatinem/rust-cache@v2
|
uses: swatinem/rust-cache@v2
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ jobs:
|
|||||||
target: "x86_64-unknown-linux-gnu"
|
target: "x86_64-unknown-linux-gnu"
|
||||||
pkg_target: "latest-linux-x64"
|
pkg_target: "latest-linux-x64"
|
||||||
nodecar_script: "build:linux-x64"
|
nodecar_script: "build:linux-x64"
|
||||||
- platform: "ubuntu-22.04"
|
- platform: "ubuntu-22.04-arm"
|
||||||
args: "--target aarch64-unknown-linux-gnu"
|
args: "--target aarch64-unknown-linux-gnu"
|
||||||
arch: "aarch64"
|
arch: "aarch64"
|
||||||
target: "aarch64-unknown-linux-gnu"
|
target: "aarch64-unknown-linux-gnu"
|
||||||
@@ -87,14 +87,10 @@ jobs:
|
|||||||
targets: ${{ matrix.target }}
|
targets: ${{ matrix.target }}
|
||||||
|
|
||||||
- name: Install dependencies (Ubuntu only)
|
- name: Install dependencies (Ubuntu only)
|
||||||
if: matrix.platform == 'ubuntu-22.04'
|
if: matrix.platform == 'ubuntu-22.04' || matrix.platform == 'ubuntu-22.04-arm'
|
||||||
run: |
|
run: |
|
||||||
sudo apt-get update
|
sudo apt-get update
|
||||||
sudo apt-get install -y libwebkit2gtk-4.1-dev libgtk-3-dev libayatana-appindicator3-dev librsvg2-dev
|
sudo apt-get install -y libwebkit2gtk-4.1-dev libgtk-3-dev libayatana-appindicator3-dev librsvg2-dev pkg-config xdg-utils
|
||||||
# Install cross-compilation tools for ARM64
|
|
||||||
if [[ "${{ matrix.arch }}" == "aarch64" ]]; then
|
|
||||||
sudo apt-get install -y gcc-aarch64-linux-gnu
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Rust cache
|
- name: Rust cache
|
||||||
uses: swatinem/rust-cache@v2
|
uses: swatinem/rust-cache@v2
|
||||||
|
|||||||
Vendored
+8
@@ -1,5 +1,6 @@
|
|||||||
{
|
{
|
||||||
"cSpell.words": [
|
"cSpell.words": [
|
||||||
|
"appimage",
|
||||||
"appindicator",
|
"appindicator",
|
||||||
"applescript",
|
"applescript",
|
||||||
"autoconfig",
|
"autoconfig",
|
||||||
@@ -12,6 +13,7 @@
|
|||||||
"codegen",
|
"codegen",
|
||||||
"devedition",
|
"devedition",
|
||||||
"donutbrowser",
|
"donutbrowser",
|
||||||
|
"dpkg",
|
||||||
"dtolnay",
|
"dtolnay",
|
||||||
"elif",
|
"elif",
|
||||||
"esbuild",
|
"esbuild",
|
||||||
@@ -23,7 +25,12 @@
|
|||||||
"idletime",
|
"idletime",
|
||||||
"KHTML",
|
"KHTML",
|
||||||
"launchservices",
|
"launchservices",
|
||||||
|
"libatk",
|
||||||
"libayatana",
|
"libayatana",
|
||||||
|
"libcairo",
|
||||||
|
"libgdk",
|
||||||
|
"libglib",
|
||||||
|
"libpango",
|
||||||
"librsvg",
|
"librsvg",
|
||||||
"libwebkit",
|
"libwebkit",
|
||||||
"mountpoint",
|
"mountpoint",
|
||||||
@@ -34,6 +41,7 @@
|
|||||||
"ntlm",
|
"ntlm",
|
||||||
"objc",
|
"objc",
|
||||||
"osascript",
|
"osascript",
|
||||||
|
"pixbuf",
|
||||||
"plasmohq",
|
"plasmohq",
|
||||||
"propertylist",
|
"propertylist",
|
||||||
"reqwest",
|
"reqwest",
|
||||||
|
|||||||
@@ -35,6 +35,7 @@
|
|||||||
"@radix-ui/react-tooltip": "^1.2.7",
|
"@radix-ui/react-tooltip": "^1.2.7",
|
||||||
"@tanstack/react-table": "^8.21.3",
|
"@tanstack/react-table": "^8.21.3",
|
||||||
"@tauri-apps/api": "^2.5.0",
|
"@tauri-apps/api": "^2.5.0",
|
||||||
|
"@tauri-apps/plugin-dialog": "^2.2.2",
|
||||||
"@tauri-apps/plugin-fs": "~2.3.0",
|
"@tauri-apps/plugin-fs": "~2.3.0",
|
||||||
"@tauri-apps/plugin-opener": "^2.2.7",
|
"@tauri-apps/plugin-opener": "^2.2.7",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
|
|||||||
Generated
+10
@@ -44,6 +44,9 @@ importers:
|
|||||||
'@tauri-apps/api':
|
'@tauri-apps/api':
|
||||||
specifier: ^2.5.0
|
specifier: ^2.5.0
|
||||||
version: 2.5.0
|
version: 2.5.0
|
||||||
|
'@tauri-apps/plugin-dialog':
|
||||||
|
specifier: ^2.2.2
|
||||||
|
version: 2.2.2
|
||||||
'@tauri-apps/plugin-fs':
|
'@tauri-apps/plugin-fs':
|
||||||
specifier: ~2.3.0
|
specifier: ~2.3.0
|
||||||
version: 2.3.0
|
version: 2.3.0
|
||||||
@@ -1450,6 +1453,9 @@ packages:
|
|||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
'@tauri-apps/plugin-dialog@2.2.2':
|
||||||
|
resolution: {integrity: sha512-Pm9qnXQq8ZVhAMFSEPwxvh+nWb2mk7LASVlNEHYaksHvcz8P6+ElR5U5dNL9Ofrm+uwhh1/gYKWswK8JJJAh6A==}
|
||||||
|
|
||||||
'@tauri-apps/plugin-fs@2.3.0':
|
'@tauri-apps/plugin-fs@2.3.0':
|
||||||
resolution: {integrity: sha512-G9gEyYVUaaxhdRJBgQTTLmzAe0vtHYxYyN1oTQzU3zwvb8T+tVLcAqCdFMWHq0qGeGbmynI5whvYpcXo5LvZ1w==}
|
resolution: {integrity: sha512-G9gEyYVUaaxhdRJBgQTTLmzAe0vtHYxYyN1oTQzU3zwvb8T+tVLcAqCdFMWHq0qGeGbmynI5whvYpcXo5LvZ1w==}
|
||||||
|
|
||||||
@@ -4657,6 +4663,10 @@ snapshots:
|
|||||||
'@tauri-apps/cli-win32-ia32-msvc': 2.5.0
|
'@tauri-apps/cli-win32-ia32-msvc': 2.5.0
|
||||||
'@tauri-apps/cli-win32-x64-msvc': 2.5.0
|
'@tauri-apps/cli-win32-x64-msvc': 2.5.0
|
||||||
|
|
||||||
|
'@tauri-apps/plugin-dialog@2.2.2':
|
||||||
|
dependencies:
|
||||||
|
'@tauri-apps/api': 2.5.0
|
||||||
|
|
||||||
'@tauri-apps/plugin-fs@2.3.0':
|
'@tauri-apps/plugin-fs@2.3.0':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@tauri-apps/api': 2.5.0
|
'@tauri-apps/api': 2.5.0
|
||||||
|
|||||||
Generated
+108
-2
@@ -82,6 +82,24 @@ dependencies = [
|
|||||||
"derive_arbitrary",
|
"derive_arbitrary",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ashpd"
|
||||||
|
version = "0.11.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6cbdf310d77fd3aaee6ea2093db7011dc2d35d2eb3481e5607f1f8d942ed99df"
|
||||||
|
dependencies = [
|
||||||
|
"enumflags2",
|
||||||
|
"futures-channel",
|
||||||
|
"futures-util",
|
||||||
|
"rand 0.9.1",
|
||||||
|
"raw-window-handle",
|
||||||
|
"serde",
|
||||||
|
"serde_repr",
|
||||||
|
"tokio",
|
||||||
|
"url",
|
||||||
|
"zbus",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "assert-json-diff"
|
name = "assert-json-diff"
|
||||||
version = "2.0.2"
|
version = "2.0.2"
|
||||||
@@ -908,6 +926,18 @@ version = "0.2.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b"
|
checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "dispatch2"
|
||||||
|
version = "0.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1a0d569e003ff27784e0e14e4a594048698e0c0f0b66cabcb51511be55a7caa0"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags 2.9.1",
|
||||||
|
"block2 0.6.1",
|
||||||
|
"libc",
|
||||||
|
"objc2 0.6.1",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "dispatch2"
|
name = "dispatch2"
|
||||||
version = "0.3.0"
|
version = "0.3.0"
|
||||||
@@ -980,6 +1010,7 @@ dependencies = [
|
|||||||
"tauri",
|
"tauri",
|
||||||
"tauri-build",
|
"tauri-build",
|
||||||
"tauri-plugin-deep-link",
|
"tauri-plugin-deep-link",
|
||||||
|
"tauri-plugin-dialog",
|
||||||
"tauri-plugin-fs",
|
"tauri-plugin-fs",
|
||||||
"tauri-plugin-opener",
|
"tauri-plugin-opener",
|
||||||
"tauri-plugin-shell",
|
"tauri-plugin-shell",
|
||||||
@@ -2599,7 +2630,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "1c10c2894a6fed806ade6027bcd50662746363a9589d3ec9d9bef30a4e4bc166"
|
checksum = "1c10c2894a6fed806ade6027bcd50662746363a9589d3ec9d9bef30a4e4bc166"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 2.9.1",
|
"bitflags 2.9.1",
|
||||||
"dispatch2",
|
"dispatch2 0.3.0",
|
||||||
"objc2 0.6.1",
|
"objc2 0.6.1",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -2610,7 +2641,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "989c6c68c13021b5c2d6b71456ebb0f9dc78d752e86a98da7c716f4f9470f5a4"
|
checksum = "989c6c68c13021b5c2d6b71456ebb0f9dc78d752e86a98da7c716f4f9470f5a4"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 2.9.1",
|
"bitflags 2.9.1",
|
||||||
"dispatch2",
|
"dispatch2 0.3.0",
|
||||||
"objc2 0.6.1",
|
"objc2 0.6.1",
|
||||||
"objc2-core-foundation",
|
"objc2-core-foundation",
|
||||||
"objc2-io-surface",
|
"objc2-io-surface",
|
||||||
@@ -3282,6 +3313,16 @@ dependencies = [
|
|||||||
"rand_core 0.6.4",
|
"rand_core 0.6.4",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rand"
|
||||||
|
version = "0.9.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9fbfd9d094a40bf3ae768db9361049ace4c0e04a4fd6b359518bd7b73a73dd97"
|
||||||
|
dependencies = [
|
||||||
|
"rand_chacha 0.9.0",
|
||||||
|
"rand_core 0.9.3",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rand_chacha"
|
name = "rand_chacha"
|
||||||
version = "0.2.2"
|
version = "0.2.2"
|
||||||
@@ -3302,6 +3343,16 @@ dependencies = [
|
|||||||
"rand_core 0.6.4",
|
"rand_core 0.6.4",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rand_chacha"
|
||||||
|
version = "0.9.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb"
|
||||||
|
dependencies = [
|
||||||
|
"ppv-lite86",
|
||||||
|
"rand_core 0.9.3",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rand_core"
|
name = "rand_core"
|
||||||
version = "0.5.1"
|
version = "0.5.1"
|
||||||
@@ -3320,6 +3371,15 @@ dependencies = [
|
|||||||
"getrandom 0.2.16",
|
"getrandom 0.2.16",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rand_core"
|
||||||
|
version = "0.9.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38"
|
||||||
|
dependencies = [
|
||||||
|
"getrandom 0.3.3",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rand_hc"
|
name = "rand_hc"
|
||||||
version = "0.2.0"
|
version = "0.2.0"
|
||||||
@@ -3438,6 +3498,31 @@ dependencies = [
|
|||||||
"web-sys",
|
"web-sys",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rfd"
|
||||||
|
version = "0.15.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "80c844748fdc82aae252ee4594a89b6e7ebef1063de7951545564cbc4e57075d"
|
||||||
|
dependencies = [
|
||||||
|
"ashpd",
|
||||||
|
"block2 0.6.1",
|
||||||
|
"dispatch2 0.2.0",
|
||||||
|
"glib-sys",
|
||||||
|
"gobject-sys",
|
||||||
|
"gtk-sys",
|
||||||
|
"js-sys",
|
||||||
|
"log",
|
||||||
|
"objc2 0.6.1",
|
||||||
|
"objc2-app-kit",
|
||||||
|
"objc2-core-foundation",
|
||||||
|
"objc2-foundation 0.3.1",
|
||||||
|
"raw-window-handle",
|
||||||
|
"wasm-bindgen",
|
||||||
|
"wasm-bindgen-futures",
|
||||||
|
"web-sys",
|
||||||
|
"windows-sys 0.59.0",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ring"
|
name = "ring"
|
||||||
version = "0.17.14"
|
version = "0.17.14"
|
||||||
@@ -4282,6 +4367,24 @@ dependencies = [
|
|||||||
"windows-result",
|
"windows-result",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tauri-plugin-dialog"
|
||||||
|
version = "2.2.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a33318fe222fc2a612961de8b0419e2982767f213f54a4d3a21b0d7b85c41df8"
|
||||||
|
dependencies = [
|
||||||
|
"log",
|
||||||
|
"raw-window-handle",
|
||||||
|
"rfd",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"tauri",
|
||||||
|
"tauri-plugin",
|
||||||
|
"tauri-plugin-fs",
|
||||||
|
"thiserror 2.0.12",
|
||||||
|
"url",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tauri-plugin-fs"
|
name = "tauri-plugin-fs"
|
||||||
version = "2.3.0"
|
version = "2.3.0"
|
||||||
@@ -4580,6 +4683,7 @@ dependencies = [
|
|||||||
"signal-hook-registry",
|
"signal-hook-registry",
|
||||||
"socket2",
|
"socket2",
|
||||||
"tokio-macros",
|
"tokio-macros",
|
||||||
|
"tracing",
|
||||||
"windows-sys 0.52.0",
|
"windows-sys 0.52.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -5795,6 +5899,7 @@ dependencies = [
|
|||||||
"ordered-stream",
|
"ordered-stream",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_repr",
|
"serde_repr",
|
||||||
|
"tokio",
|
||||||
"tracing",
|
"tracing",
|
||||||
"uds_windows",
|
"uds_windows",
|
||||||
"windows-sys 0.59.0",
|
"windows-sys 0.59.0",
|
||||||
@@ -6006,6 +6111,7 @@ dependencies = [
|
|||||||
"endi",
|
"endi",
|
||||||
"enumflags2",
|
"enumflags2",
|
||||||
"serde",
|
"serde",
|
||||||
|
"url",
|
||||||
"winnow 0.7.10",
|
"winnow 0.7.10",
|
||||||
"zvariant_derive",
|
"zvariant_derive",
|
||||||
"zvariant_utils",
|
"zvariant_utils",
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ tauri-plugin-opener = "2"
|
|||||||
tauri-plugin-fs = "2"
|
tauri-plugin-fs = "2"
|
||||||
tauri-plugin-shell = "2"
|
tauri-plugin-shell = "2"
|
||||||
tauri-plugin-deep-link = "2"
|
tauri-plugin-deep-link = "2"
|
||||||
|
tauri-plugin-dialog = "2"
|
||||||
directories = "6"
|
directories = "6"
|
||||||
reqwest = { version = "0.12", features = ["json", "stream"] }
|
reqwest = { version = "0.12", features = ["json", "stream"] }
|
||||||
tokio = { version = "1", features = ["full"] }
|
tokio = { version = "1", features = ["full"] }
|
||||||
|
|||||||
@@ -18,6 +18,8 @@
|
|||||||
"shell:allow-open",
|
"shell:allow-open",
|
||||||
"shell:allow-spawn",
|
"shell:allow-spawn",
|
||||||
"shell:allow-stdin-write",
|
"shell:allow-stdin-write",
|
||||||
"deep-link:default"
|
"deep-link:default",
|
||||||
|
"dialog:default",
|
||||||
|
"dialog:allow-open"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ mod default_browser;
|
|||||||
mod download;
|
mod download;
|
||||||
mod downloaded_browsers;
|
mod downloaded_browsers;
|
||||||
mod extraction;
|
mod extraction;
|
||||||
|
mod profile_importer;
|
||||||
mod proxy_manager;
|
mod proxy_manager;
|
||||||
mod settings_manager;
|
mod settings_manager;
|
||||||
mod theme_detector;
|
mod theme_detector;
|
||||||
@@ -55,6 +56,8 @@ use app_auto_updater::{
|
|||||||
check_for_app_updates, check_for_app_updates_manual, download_and_install_app_update,
|
check_for_app_updates, check_for_app_updates_manual, download_and_install_app_update,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use profile_importer::{detect_existing_profiles, import_browser_profile};
|
||||||
|
|
||||||
use theme_detector::get_system_theme;
|
use theme_detector::get_system_theme;
|
||||||
|
|
||||||
// Trait to extend WebviewWindow with transparent titlebar functionality
|
// Trait to extend WebviewWindow with transparent titlebar functionality
|
||||||
@@ -168,6 +171,7 @@ pub fn run() {
|
|||||||
.plugin(tauri_plugin_opener::init())
|
.plugin(tauri_plugin_opener::init())
|
||||||
.plugin(tauri_plugin_shell::init())
|
.plugin(tauri_plugin_shell::init())
|
||||||
.plugin(tauri_plugin_deep_link::init())
|
.plugin(tauri_plugin_deep_link::init())
|
||||||
|
.plugin(tauri_plugin_dialog::init())
|
||||||
.setup(|app| {
|
.setup(|app| {
|
||||||
// Create the main window programmatically
|
// Create the main window programmatically
|
||||||
#[allow(unused_variables)]
|
#[allow(unused_variables)]
|
||||||
@@ -309,6 +313,8 @@ pub fn run() {
|
|||||||
check_for_app_updates_manual,
|
check_for_app_updates_manual,
|
||||||
download_and_install_app_update,
|
download_and_install_app_update,
|
||||||
get_system_theme,
|
get_system_theme,
|
||||||
|
detect_existing_profiles,
|
||||||
|
import_browser_profile,
|
||||||
])
|
])
|
||||||
.run(tauri::generate_context!())
|
.run(tauri::generate_context!())
|
||||||
.expect("error while running tauri application");
|
.expect("error while running tauri application");
|
||||||
|
|||||||
@@ -0,0 +1,661 @@
|
|||||||
|
use directories::BaseDirs;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::collections::HashSet;
|
||||||
|
use std::fs::{self, create_dir_all};
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
|
use crate::browser::BrowserType;
|
||||||
|
use crate::browser_runner::BrowserRunner;
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||||
|
pub struct DetectedProfile {
|
||||||
|
pub browser: String,
|
||||||
|
pub name: String,
|
||||||
|
pub path: String,
|
||||||
|
pub description: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct ProfileImporter {
|
||||||
|
base_dirs: BaseDirs,
|
||||||
|
browser_runner: BrowserRunner,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ProfileImporter {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
base_dirs: BaseDirs::new().expect("Failed to get base directories"),
|
||||||
|
browser_runner: BrowserRunner::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Detect existing browser profiles on the system
|
||||||
|
pub fn detect_existing_profiles(
|
||||||
|
&self,
|
||||||
|
) -> Result<Vec<DetectedProfile>, Box<dyn std::error::Error>> {
|
||||||
|
let mut detected_profiles = Vec::new();
|
||||||
|
|
||||||
|
// Detect Firefox profiles
|
||||||
|
detected_profiles.extend(self.detect_firefox_profiles()?);
|
||||||
|
|
||||||
|
// Detect Chrome profiles
|
||||||
|
detected_profiles.extend(self.detect_chrome_profiles()?);
|
||||||
|
|
||||||
|
// Detect Brave profiles
|
||||||
|
detected_profiles.extend(self.detect_brave_profiles()?);
|
||||||
|
|
||||||
|
// Detect Firefox Developer Edition profiles
|
||||||
|
detected_profiles.extend(self.detect_firefox_developer_profiles()?);
|
||||||
|
|
||||||
|
// Detect Chromium profiles
|
||||||
|
detected_profiles.extend(self.detect_chromium_profiles()?);
|
||||||
|
|
||||||
|
// Detect Mullvad Browser profiles
|
||||||
|
detected_profiles.extend(self.detect_mullvad_browser_profiles()?);
|
||||||
|
|
||||||
|
// Detect Zen Browser profiles
|
||||||
|
detected_profiles.extend(self.detect_zen_browser_profiles()?);
|
||||||
|
|
||||||
|
// Remove duplicates based on path
|
||||||
|
let mut seen_paths = HashSet::new();
|
||||||
|
let unique_profiles: Vec<DetectedProfile> = detected_profiles
|
||||||
|
.into_iter()
|
||||||
|
.filter(|profile| seen_paths.insert(profile.path.clone()))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
Ok(unique_profiles)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Detect Firefox profiles
|
||||||
|
fn detect_firefox_profiles(&self) -> Result<Vec<DetectedProfile>, Box<dyn std::error::Error>> {
|
||||||
|
let mut profiles = Vec::new();
|
||||||
|
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
{
|
||||||
|
let firefox_dir = self
|
||||||
|
.base_dirs
|
||||||
|
.home_dir()
|
||||||
|
.join("Library/Application Support/Firefox/Profiles");
|
||||||
|
profiles.extend(self.scan_firefox_profiles_dir(&firefox_dir, "firefox")?);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
{
|
||||||
|
if let Some(app_data) = self.base_dirs.data_dir() {
|
||||||
|
let firefox_dir = app_data.join("Mozilla/Firefox/Profiles");
|
||||||
|
profiles.extend(self.scan_firefox_profiles_dir(&firefox_dir, "firefox")?);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
{
|
||||||
|
let firefox_dir = self.base_dirs.home_dir().join(".mozilla/firefox");
|
||||||
|
profiles.extend(self.scan_firefox_profiles_dir(&firefox_dir, "firefox")?);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(profiles)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Detect Firefox Developer Edition profiles
|
||||||
|
fn detect_firefox_developer_profiles(
|
||||||
|
&self,
|
||||||
|
) -> Result<Vec<DetectedProfile>, Box<dyn std::error::Error>> {
|
||||||
|
let mut profiles = Vec::new();
|
||||||
|
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
{
|
||||||
|
// Firefox Developer Edition on macOS uses separate profile directories
|
||||||
|
let firefox_dev_alt_dir = self
|
||||||
|
.base_dirs
|
||||||
|
.home_dir()
|
||||||
|
.join("Library/Application Support/Firefox Developer Edition/Profiles");
|
||||||
|
|
||||||
|
// Only scan the dedicated dev edition directory if it exists, otherwise skip to avoid duplicates
|
||||||
|
if firefox_dev_alt_dir.exists() {
|
||||||
|
profiles.extend(self.scan_firefox_profiles_dir(&firefox_dev_alt_dir, "firefox-developer")?);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
{
|
||||||
|
if let Some(app_data) = self.base_dirs.data_dir() {
|
||||||
|
// Firefox Developer Edition on Windows typically uses separate directories
|
||||||
|
let firefox_dev_dir = app_data.join("Mozilla/Firefox Developer Edition/Profiles");
|
||||||
|
if firefox_dev_dir.exists() {
|
||||||
|
profiles.extend(self.scan_firefox_profiles_dir(&firefox_dev_dir, "firefox-developer")?);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
{
|
||||||
|
// Firefox Developer Edition on Linux uses separate directories
|
||||||
|
let firefox_dev_dir = self
|
||||||
|
.base_dirs
|
||||||
|
.home_dir()
|
||||||
|
.join(".mozilla/firefox-dev-edition");
|
||||||
|
if firefox_dev_dir.exists() {
|
||||||
|
profiles.extend(self.scan_firefox_profiles_dir(&firefox_dev_dir, "firefox-developer")?);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(profiles)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Detect Chrome profiles
|
||||||
|
fn detect_chrome_profiles(&self) -> Result<Vec<DetectedProfile>, Box<dyn std::error::Error>> {
|
||||||
|
let mut profiles = Vec::new();
|
||||||
|
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
{
|
||||||
|
let chrome_dir = self
|
||||||
|
.base_dirs
|
||||||
|
.home_dir()
|
||||||
|
.join("Library/Application Support/Google/Chrome");
|
||||||
|
profiles.extend(self.scan_chrome_profiles_dir(&chrome_dir, "chromium")?);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
{
|
||||||
|
if let Some(local_app_data) = self.base_dirs.data_local_dir() {
|
||||||
|
let chrome_dir = local_app_data.join("Google/Chrome/User Data");
|
||||||
|
profiles.extend(self.scan_chrome_profiles_dir(&chrome_dir, "chromium")?);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
{
|
||||||
|
let chrome_dir = self.base_dirs.home_dir().join(".config/google-chrome");
|
||||||
|
profiles.extend(self.scan_chrome_profiles_dir(&chrome_dir, "chromium")?);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(profiles)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Detect Chromium profiles
|
||||||
|
fn detect_chromium_profiles(&self) -> Result<Vec<DetectedProfile>, Box<dyn std::error::Error>> {
|
||||||
|
let mut profiles = Vec::new();
|
||||||
|
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
{
|
||||||
|
let chromium_dir = self
|
||||||
|
.base_dirs
|
||||||
|
.home_dir()
|
||||||
|
.join("Library/Application Support/Chromium");
|
||||||
|
profiles.extend(self.scan_chrome_profiles_dir(&chromium_dir, "chromium")?);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
{
|
||||||
|
if let Some(local_app_data) = self.base_dirs.data_local_dir() {
|
||||||
|
let chromium_dir = local_app_data.join("Chromium/User Data");
|
||||||
|
profiles.extend(self.scan_chrome_profiles_dir(&chromium_dir, "chromium")?);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
{
|
||||||
|
let chromium_dir = self.base_dirs.home_dir().join(".config/chromium");
|
||||||
|
profiles.extend(self.scan_chrome_profiles_dir(&chromium_dir, "chromium")?);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(profiles)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Detect Brave profiles
|
||||||
|
fn detect_brave_profiles(&self) -> Result<Vec<DetectedProfile>, Box<dyn std::error::Error>> {
|
||||||
|
let mut profiles = Vec::new();
|
||||||
|
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
{
|
||||||
|
let brave_dir = self
|
||||||
|
.base_dirs
|
||||||
|
.home_dir()
|
||||||
|
.join("Library/Application Support/BraveSoftware/Brave-Browser");
|
||||||
|
profiles.extend(self.scan_chrome_profiles_dir(&brave_dir, "brave")?);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
{
|
||||||
|
if let Some(local_app_data) = self.base_dirs.data_local_dir() {
|
||||||
|
let brave_dir = local_app_data.join("BraveSoftware/Brave-Browser/User Data");
|
||||||
|
profiles.extend(self.scan_chrome_profiles_dir(&brave_dir, "brave")?);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
{
|
||||||
|
let brave_dir = self
|
||||||
|
.base_dirs
|
||||||
|
.home_dir()
|
||||||
|
.join(".config/BraveSoftware/Brave-Browser");
|
||||||
|
profiles.extend(self.scan_chrome_profiles_dir(&brave_dir, "brave")?);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(profiles)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Detect Mullvad Browser profiles
|
||||||
|
fn detect_mullvad_browser_profiles(
|
||||||
|
&self,
|
||||||
|
) -> Result<Vec<DetectedProfile>, Box<dyn std::error::Error>> {
|
||||||
|
let mut profiles = Vec::new();
|
||||||
|
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
{
|
||||||
|
let mullvad_dir = self
|
||||||
|
.base_dirs
|
||||||
|
.home_dir()
|
||||||
|
.join("Library/Application Support/MullvadBrowser/Profiles");
|
||||||
|
profiles.extend(self.scan_firefox_profiles_dir(&mullvad_dir, "mullvad-browser")?);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
{
|
||||||
|
if let Some(app_data) = self.base_dirs.data_dir() {
|
||||||
|
let mullvad_dir = app_data.join("MullvadBrowser/Profiles");
|
||||||
|
profiles.extend(self.scan_firefox_profiles_dir(&mullvad_dir, "mullvad-browser")?);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
{
|
||||||
|
let mullvad_dir = self.base_dirs.home_dir().join(".mullvad-browser");
|
||||||
|
profiles.extend(self.scan_firefox_profiles_dir(&mullvad_dir, "mullvad-browser")?);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(profiles)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Detect Zen Browser profiles
|
||||||
|
fn detect_zen_browser_profiles(
|
||||||
|
&self,
|
||||||
|
) -> Result<Vec<DetectedProfile>, Box<dyn std::error::Error>> {
|
||||||
|
let mut profiles = Vec::new();
|
||||||
|
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
{
|
||||||
|
let zen_dir = self
|
||||||
|
.base_dirs
|
||||||
|
.home_dir()
|
||||||
|
.join("Library/Application Support/Zen/Profiles");
|
||||||
|
profiles.extend(self.scan_firefox_profiles_dir(&zen_dir, "zen")?);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
{
|
||||||
|
if let Some(app_data) = self.base_dirs.data_dir() {
|
||||||
|
let zen_dir = app_data.join("Zen/Profiles");
|
||||||
|
profiles.extend(self.scan_firefox_profiles_dir(&zen_dir, "zen")?);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
{
|
||||||
|
let zen_dir = self.base_dirs.home_dir().join(".zen");
|
||||||
|
profiles.extend(self.scan_firefox_profiles_dir(&zen_dir, "zen")?);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(profiles)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Scan Firefox-style profiles directory
|
||||||
|
fn scan_firefox_profiles_dir(
|
||||||
|
&self,
|
||||||
|
profiles_dir: &Path,
|
||||||
|
browser_type: &str,
|
||||||
|
) -> Result<Vec<DetectedProfile>, Box<dyn std::error::Error>> {
|
||||||
|
let mut profiles = Vec::new();
|
||||||
|
|
||||||
|
if !profiles_dir.exists() {
|
||||||
|
return Ok(profiles);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read profiles.ini file if it exists
|
||||||
|
let profiles_ini = profiles_dir
|
||||||
|
.parent()
|
||||||
|
.unwrap_or(profiles_dir)
|
||||||
|
.join("profiles.ini");
|
||||||
|
if profiles_ini.exists() {
|
||||||
|
if let Ok(content) = fs::read_to_string(&profiles_ini) {
|
||||||
|
profiles.extend(self.parse_firefox_profiles_ini(&content, profiles_dir, browser_type)?);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Also scan directory for any profile folders not in profiles.ini
|
||||||
|
if let Ok(entries) = fs::read_dir(profiles_dir) {
|
||||||
|
for entry in entries.flatten() {
|
||||||
|
let path = entry.path();
|
||||||
|
if path.is_dir() {
|
||||||
|
let prefs_file = path.join("prefs.js");
|
||||||
|
if prefs_file.exists() {
|
||||||
|
let profile_name = path
|
||||||
|
.file_name()
|
||||||
|
.and_then(|n| n.to_str())
|
||||||
|
.unwrap_or("Unknown Profile");
|
||||||
|
|
||||||
|
// Check if this profile was already found in profiles.ini
|
||||||
|
let already_added = profiles.iter().any(|p| p.path == path.to_string_lossy());
|
||||||
|
if !already_added {
|
||||||
|
profiles.push(DetectedProfile {
|
||||||
|
browser: browser_type.to_string(),
|
||||||
|
name: format!(
|
||||||
|
"{} Profile - {}",
|
||||||
|
self.get_browser_display_name(browser_type),
|
||||||
|
profile_name
|
||||||
|
),
|
||||||
|
path: path.to_string_lossy().to_string(),
|
||||||
|
description: format!("Profile folder: {profile_name}"),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(profiles)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parse Firefox profiles.ini file
|
||||||
|
fn parse_firefox_profiles_ini(
|
||||||
|
&self,
|
||||||
|
content: &str,
|
||||||
|
profiles_dir: &Path,
|
||||||
|
browser_type: &str,
|
||||||
|
) -> Result<Vec<DetectedProfile>, Box<dyn std::error::Error>> {
|
||||||
|
let mut profiles = Vec::new();
|
||||||
|
let mut current_section = String::new();
|
||||||
|
let mut profile_name = String::new();
|
||||||
|
let mut profile_path = String::new();
|
||||||
|
let mut is_relative = true;
|
||||||
|
|
||||||
|
for line in content.lines() {
|
||||||
|
let line = line.trim();
|
||||||
|
|
||||||
|
if line.starts_with('[') && line.ends_with(']') {
|
||||||
|
// Save previous profile if complete
|
||||||
|
if !current_section.is_empty()
|
||||||
|
&& current_section.starts_with("Profile")
|
||||||
|
&& !profile_path.is_empty()
|
||||||
|
{
|
||||||
|
let full_path = if is_relative {
|
||||||
|
profiles_dir.join(&profile_path)
|
||||||
|
} else {
|
||||||
|
PathBuf::from(&profile_path)
|
||||||
|
};
|
||||||
|
|
||||||
|
if full_path.exists() {
|
||||||
|
let display_name = if profile_name.is_empty() {
|
||||||
|
format!("{} Profile", self.get_browser_display_name(browser_type))
|
||||||
|
} else {
|
||||||
|
format!(
|
||||||
|
"{} - {}",
|
||||||
|
self.get_browser_display_name(browser_type),
|
||||||
|
profile_name
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
profiles.push(DetectedProfile {
|
||||||
|
browser: browser_type.to_string(),
|
||||||
|
name: display_name,
|
||||||
|
path: full_path.to_string_lossy().to_string(),
|
||||||
|
description: format!("Profile: {profile_name}"),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start new section
|
||||||
|
current_section = line[1..line.len() - 1].to_string();
|
||||||
|
profile_name.clear();
|
||||||
|
profile_path.clear();
|
||||||
|
is_relative = true;
|
||||||
|
} else if line.contains('=') {
|
||||||
|
let parts: Vec<&str> = line.splitn(2, '=').collect();
|
||||||
|
if parts.len() == 2 {
|
||||||
|
let key = parts[0].trim();
|
||||||
|
let value = parts[1].trim();
|
||||||
|
|
||||||
|
match key {
|
||||||
|
"Name" => profile_name = value.to_string(),
|
||||||
|
"Path" => profile_path = value.to_string(),
|
||||||
|
"IsRelative" => is_relative = value == "1",
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle last profile
|
||||||
|
if !current_section.is_empty()
|
||||||
|
&& current_section.starts_with("Profile")
|
||||||
|
&& !profile_path.is_empty()
|
||||||
|
{
|
||||||
|
let full_path = if is_relative {
|
||||||
|
profiles_dir.join(&profile_path)
|
||||||
|
} else {
|
||||||
|
PathBuf::from(&profile_path)
|
||||||
|
};
|
||||||
|
|
||||||
|
if full_path.exists() {
|
||||||
|
let display_name = if profile_name.is_empty() {
|
||||||
|
format!("{} Profile", self.get_browser_display_name(browser_type))
|
||||||
|
} else {
|
||||||
|
format!(
|
||||||
|
"{} - {}",
|
||||||
|
self.get_browser_display_name(browser_type),
|
||||||
|
profile_name
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
profiles.push(DetectedProfile {
|
||||||
|
browser: browser_type.to_string(),
|
||||||
|
name: display_name,
|
||||||
|
path: full_path.to_string_lossy().to_string(),
|
||||||
|
description: format!("Profile: {profile_name}"),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(profiles)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Scan Chrome-style profiles directory
|
||||||
|
fn scan_chrome_profiles_dir(
|
||||||
|
&self,
|
||||||
|
browser_dir: &Path,
|
||||||
|
browser_type: &str,
|
||||||
|
) -> Result<Vec<DetectedProfile>, Box<dyn std::error::Error>> {
|
||||||
|
let mut profiles = Vec::new();
|
||||||
|
|
||||||
|
if !browser_dir.exists() {
|
||||||
|
return Ok(profiles);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for Default profile
|
||||||
|
let default_profile = browser_dir.join("Default");
|
||||||
|
if default_profile.exists() && default_profile.join("Preferences").exists() {
|
||||||
|
profiles.push(DetectedProfile {
|
||||||
|
browser: browser_type.to_string(),
|
||||||
|
name: format!(
|
||||||
|
"{} - Default Profile",
|
||||||
|
self.get_browser_display_name(browser_type)
|
||||||
|
),
|
||||||
|
path: default_profile.to_string_lossy().to_string(),
|
||||||
|
description: "Default profile".to_string(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for Profile X directories
|
||||||
|
if let Ok(entries) = fs::read_dir(browser_dir) {
|
||||||
|
for entry in entries.flatten() {
|
||||||
|
let path = entry.path();
|
||||||
|
if path.is_dir() {
|
||||||
|
let dir_name = path.file_name().and_then(|n| n.to_str()).unwrap_or("");
|
||||||
|
|
||||||
|
if dir_name.starts_with("Profile ") && path.join("Preferences").exists() {
|
||||||
|
let profile_number = &dir_name[8..]; // Remove "Profile " prefix
|
||||||
|
profiles.push(DetectedProfile {
|
||||||
|
browser: browser_type.to_string(),
|
||||||
|
name: format!(
|
||||||
|
"{} - Profile {}",
|
||||||
|
self.get_browser_display_name(browser_type),
|
||||||
|
profile_number
|
||||||
|
),
|
||||||
|
path: path.to_string_lossy().to_string(),
|
||||||
|
description: format!("Profile {profile_number}"),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(profiles)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get browser display name
|
||||||
|
fn get_browser_display_name(&self, browser_type: &str) -> &str {
|
||||||
|
match browser_type {
|
||||||
|
"firefox" => "Firefox",
|
||||||
|
"firefox-developer" => "Firefox Developer",
|
||||||
|
"chromium" => "Chrome/Chromium",
|
||||||
|
"brave" => "Brave",
|
||||||
|
"mullvad-browser" => "Mullvad Browser",
|
||||||
|
"zen" => "Zen Browser",
|
||||||
|
"tor-browser" => "Tor Browser",
|
||||||
|
_ => "Unknown Browser",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Import a profile from an existing browser profile
|
||||||
|
pub fn import_profile(
|
||||||
|
&self,
|
||||||
|
source_path: &str,
|
||||||
|
browser_type: &str,
|
||||||
|
new_profile_name: &str,
|
||||||
|
) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
// Validate that source path exists
|
||||||
|
let source_path = Path::new(source_path);
|
||||||
|
if !source_path.exists() {
|
||||||
|
return Err("Source profile path does not exist".into());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate browser type
|
||||||
|
let _browser_type = BrowserType::from_str(browser_type)
|
||||||
|
.map_err(|_| format!("Invalid browser type: {browser_type}"))?;
|
||||||
|
|
||||||
|
// Check if a profile with this name already exists
|
||||||
|
let existing_profiles = self.browser_runner.list_profiles()?;
|
||||||
|
if existing_profiles
|
||||||
|
.iter()
|
||||||
|
.any(|p| p.name.to_lowercase() == new_profile_name.to_lowercase())
|
||||||
|
{
|
||||||
|
return Err(format!("Profile with name '{new_profile_name}' already exists").into());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the new profile directory
|
||||||
|
let snake_case_name = new_profile_name.to_lowercase().replace(' ', "_");
|
||||||
|
let profiles_dir = self.browser_runner.get_profiles_dir();
|
||||||
|
let new_profile_path = profiles_dir.join(&snake_case_name);
|
||||||
|
|
||||||
|
create_dir_all(&new_profile_path)?;
|
||||||
|
|
||||||
|
// Copy all files from source to destination
|
||||||
|
Self::copy_directory_recursive(source_path, &new_profile_path)?;
|
||||||
|
|
||||||
|
// Create the profile metadata without overwriting the imported data
|
||||||
|
// We need to find a suitable version for this browser type
|
||||||
|
let available_versions = self.get_default_version_for_browser(browser_type)?;
|
||||||
|
|
||||||
|
let profile = crate::browser_runner::BrowserProfile {
|
||||||
|
name: new_profile_name.to_string(),
|
||||||
|
browser: browser_type.to_string(),
|
||||||
|
version: available_versions,
|
||||||
|
profile_path: new_profile_path.to_string_lossy().to_string(),
|
||||||
|
proxy: None,
|
||||||
|
process_id: None,
|
||||||
|
last_launch: None,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Save the profile metadata
|
||||||
|
self.browser_runner.save_profile(&profile)?;
|
||||||
|
|
||||||
|
println!(
|
||||||
|
"Successfully imported profile '{}' from '{}'",
|
||||||
|
new_profile_name,
|
||||||
|
source_path.display()
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get a default version for a browser type
|
||||||
|
fn get_default_version_for_browser(
|
||||||
|
&self,
|
||||||
|
browser_type: &str,
|
||||||
|
) -> Result<String, Box<dyn std::error::Error>> {
|
||||||
|
// Try to get a downloaded version first, fallback to a reasonable default
|
||||||
|
let registry =
|
||||||
|
crate::downloaded_browsers::DownloadedBrowsersRegistry::load().unwrap_or_default();
|
||||||
|
let downloaded_versions = registry.get_downloaded_versions(browser_type);
|
||||||
|
|
||||||
|
if let Some(version) = downloaded_versions.first() {
|
||||||
|
return Ok(version.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
// If no downloaded versions, return a sensible default
|
||||||
|
match browser_type {
|
||||||
|
"firefox" => Ok("latest".to_string()),
|
||||||
|
"firefox-developer" => Ok("latest".to_string()),
|
||||||
|
"chromium" => Ok("latest".to_string()),
|
||||||
|
"brave" => Ok("latest".to_string()),
|
||||||
|
"zen" => Ok("latest".to_string()),
|
||||||
|
"mullvad-browser" => Ok("13.5.16".to_string()), // Mullvad Browser common version
|
||||||
|
"tor-browser" => Ok("latest".to_string()),
|
||||||
|
_ => Ok("latest".to_string()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Recursively copy directory contents
|
||||||
|
fn copy_directory_recursive(
|
||||||
|
source: &Path,
|
||||||
|
destination: &Path,
|
||||||
|
) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
if !destination.exists() {
|
||||||
|
create_dir_all(destination)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
for entry in fs::read_dir(source)? {
|
||||||
|
let entry = entry?;
|
||||||
|
let source_path = entry.path();
|
||||||
|
let dest_path = destination.join(entry.file_name());
|
||||||
|
|
||||||
|
if source_path.is_dir() {
|
||||||
|
Self::copy_directory_recursive(&source_path, &dest_path)?;
|
||||||
|
} else {
|
||||||
|
fs::copy(&source_path, &dest_path)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tauri commands
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn detect_existing_profiles() -> Result<Vec<DetectedProfile>, String> {
|
||||||
|
let importer = ProfileImporter::new();
|
||||||
|
importer
|
||||||
|
.detect_existing_profiles()
|
||||||
|
.map_err(|e| format!("Failed to detect existing profiles: {e}"))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn import_browser_profile(
|
||||||
|
source_path: String,
|
||||||
|
browser_type: String,
|
||||||
|
new_profile_name: String,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
let importer = ProfileImporter::new();
|
||||||
|
importer
|
||||||
|
.import_profile(&source_path, &browser_type, &new_profile_name)
|
||||||
|
.map_err(|e| format!("Failed to import profile: {e}"))
|
||||||
|
}
|
||||||
+40
-9
@@ -2,12 +2,19 @@
|
|||||||
|
|
||||||
import { ChangeVersionDialog } from "@/components/change-version-dialog";
|
import { ChangeVersionDialog } from "@/components/change-version-dialog";
|
||||||
import { CreateProfileDialog } from "@/components/create-profile-dialog";
|
import { CreateProfileDialog } from "@/components/create-profile-dialog";
|
||||||
|
import { ImportProfileDialog } from "@/components/import-profile-dialog";
|
||||||
import { ProfilesDataTable } from "@/components/profile-data-table";
|
import { ProfilesDataTable } from "@/components/profile-data-table";
|
||||||
import { ProfileSelectorDialog } from "@/components/profile-selector-dialog";
|
import { ProfileSelectorDialog } from "@/components/profile-selector-dialog";
|
||||||
import { ProxySettingsDialog } from "@/components/proxy-settings-dialog";
|
import { ProxySettingsDialog } from "@/components/proxy-settings-dialog";
|
||||||
import { SettingsDialog } from "@/components/settings-dialog";
|
import { SettingsDialog } from "@/components/settings-dialog";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
|
import {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
} from "@/components/ui/dropdown-menu";
|
||||||
import {
|
import {
|
||||||
Tooltip,
|
Tooltip,
|
||||||
TooltipContent,
|
TooltipContent,
|
||||||
@@ -20,7 +27,8 @@ import type { BrowserProfile, ProxySettings } from "@/types";
|
|||||||
import { invoke } from "@tauri-apps/api/core";
|
import { invoke } from "@tauri-apps/api/core";
|
||||||
import { listen } from "@tauri-apps/api/event";
|
import { listen } from "@tauri-apps/api/event";
|
||||||
import { useCallback, useEffect, useRef, useState } from "react";
|
import { useCallback, useEffect, useRef, useState } from "react";
|
||||||
import { GoGear, GoPlus } from "react-icons/go";
|
import { FaDownload } from "react-icons/fa";
|
||||||
|
import { GoGear, GoKebabHorizontal, GoPlus } from "react-icons/go";
|
||||||
|
|
||||||
type BrowserTypeString =
|
type BrowserTypeString =
|
||||||
| "mullvad-browser"
|
| "mullvad-browser"
|
||||||
@@ -43,6 +51,7 @@ export default function Home() {
|
|||||||
const [createProfileDialogOpen, setCreateProfileDialogOpen] = useState(false);
|
const [createProfileDialogOpen, setCreateProfileDialogOpen] = useState(false);
|
||||||
const [changeVersionDialogOpen, setChangeVersionDialogOpen] = useState(false);
|
const [changeVersionDialogOpen, setChangeVersionDialogOpen] = useState(false);
|
||||||
const [settingsDialogOpen, setSettingsDialogOpen] = useState(false);
|
const [settingsDialogOpen, setSettingsDialogOpen] = useState(false);
|
||||||
|
const [importProfileDialogOpen, setImportProfileDialogOpen] = useState(false);
|
||||||
const [pendingUrls, setPendingUrls] = useState<PendingUrl[]>([]);
|
const [pendingUrls, setPendingUrls] = useState<PendingUrl[]>([]);
|
||||||
const [currentProfileForProxy, setCurrentProfileForProxy] =
|
const [currentProfileForProxy, setCurrentProfileForProxy] =
|
||||||
useState<BrowserProfile | null>(null);
|
useState<BrowserProfile | null>(null);
|
||||||
@@ -407,21 +416,35 @@ export default function Home() {
|
|||||||
<div className="flex justify-between items-center">
|
<div className="flex justify-between items-center">
|
||||||
<CardTitle>Profiles</CardTitle>
|
<CardTitle>Profiles</CardTitle>
|
||||||
<div className="flex gap-2 items-center">
|
<div className="flex gap-2 items-center">
|
||||||
<Tooltip>
|
<DropdownMenu>
|
||||||
<TooltipTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
|
className="flex gap-2 items-center"
|
||||||
|
>
|
||||||
|
<GoKebabHorizontal className="w-4 h-4" />
|
||||||
|
</Button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent align="end">
|
||||||
|
<DropdownMenuItem
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setSettingsDialogOpen(true);
|
setSettingsDialogOpen(true);
|
||||||
}}
|
}}
|
||||||
className="flex gap-2 items-center"
|
|
||||||
>
|
>
|
||||||
<GoGear className="w-4 h-4" />
|
<GoGear className="mr-2 w-4 h-4" />
|
||||||
</Button>
|
Settings
|
||||||
</TooltipTrigger>
|
</DropdownMenuItem>
|
||||||
<TooltipContent>Settings</TooltipContent>
|
<DropdownMenuItem
|
||||||
</Tooltip>
|
onClick={() => {
|
||||||
|
setImportProfileDialogOpen(true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FaDownload className="mr-2 w-4 h-4" />
|
||||||
|
Import Profile
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
<Button
|
<Button
|
||||||
@@ -489,6 +512,14 @@ export default function Home() {
|
|||||||
onVersionChanged={() => void loadProfiles()}
|
onVersionChanged={() => void loadProfiles()}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<ImportProfileDialog
|
||||||
|
isOpen={importProfileDialogOpen}
|
||||||
|
onClose={() => {
|
||||||
|
setImportProfileDialogOpen(false);
|
||||||
|
}}
|
||||||
|
onImportComplete={() => void loadProfiles()}
|
||||||
|
/>
|
||||||
|
|
||||||
{pendingUrls.map((pendingUrl) => (
|
{pendingUrls.map((pendingUrl) => (
|
||||||
<ProfileSelectorDialog
|
<ProfileSelectorDialog
|
||||||
key={pendingUrl.id}
|
key={pendingUrl.id}
|
||||||
|
|||||||
@@ -0,0 +1,482 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { LoadingButton } from "@/components/loading-button";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
} from "@/components/ui/dialog";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { Label } from "@/components/ui/label";
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from "@/components/ui/select";
|
||||||
|
import { useBrowserSupport } from "@/hooks/use-browser-support";
|
||||||
|
import { getBrowserDisplayName, getBrowserIcon } from "@/lib/browser-utils";
|
||||||
|
import type { DetectedProfile } from "@/types";
|
||||||
|
import { invoke } from "@tauri-apps/api/core";
|
||||||
|
import { open } from "@tauri-apps/plugin-dialog";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { FaFolder } from "react-icons/fa";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
|
||||||
|
interface ImportProfileDialogProps {
|
||||||
|
isOpen: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
onImportComplete?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ImportProfileDialog({
|
||||||
|
isOpen,
|
||||||
|
onClose,
|
||||||
|
onImportComplete,
|
||||||
|
}: ImportProfileDialogProps) {
|
||||||
|
const [detectedProfiles, setDetectedProfiles] = useState<DetectedProfile[]>(
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const [isImporting, setIsImporting] = useState(false);
|
||||||
|
const [importMode, setImportMode] = useState<"auto-detect" | "manual">(
|
||||||
|
"auto-detect",
|
||||||
|
);
|
||||||
|
|
||||||
|
// Auto-detect state
|
||||||
|
const [selectedDetectedProfile, setSelectedDetectedProfile] = useState<
|
||||||
|
string | null
|
||||||
|
>(null);
|
||||||
|
const [autoDetectProfileName, setAutoDetectProfileName] = useState("");
|
||||||
|
|
||||||
|
// Manual import state
|
||||||
|
const [manualBrowserType, setManualBrowserType] = useState<string | null>(
|
||||||
|
null,
|
||||||
|
);
|
||||||
|
const [manualProfilePath, setManualProfilePath] = useState("");
|
||||||
|
const [manualProfileName, setManualProfileName] = useState("");
|
||||||
|
|
||||||
|
const { supportedBrowsers, isLoading: isLoadingSupport } =
|
||||||
|
useBrowserSupport();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isOpen) {
|
||||||
|
void loadDetectedProfiles();
|
||||||
|
}
|
||||||
|
}, [isOpen]);
|
||||||
|
|
||||||
|
const loadDetectedProfiles = async () => {
|
||||||
|
setIsLoading(true);
|
||||||
|
try {
|
||||||
|
const profiles = await invoke<DetectedProfile[]>(
|
||||||
|
"detect_existing_profiles",
|
||||||
|
);
|
||||||
|
setDetectedProfiles(profiles);
|
||||||
|
|
||||||
|
// Auto-switch to manual mode if no profiles detected
|
||||||
|
if (profiles.length === 0) {
|
||||||
|
setImportMode("manual");
|
||||||
|
} else {
|
||||||
|
// Auto-select first profile if available
|
||||||
|
setSelectedDetectedProfile(profiles[0].path);
|
||||||
|
|
||||||
|
// Generate default name from the detected profile
|
||||||
|
const profile = profiles[0];
|
||||||
|
const browserName = getBrowserDisplayName(profile.browser);
|
||||||
|
const defaultName = `Imported ${browserName} Profile`;
|
||||||
|
setAutoDetectProfileName(defaultName);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to detect existing profiles:", error);
|
||||||
|
toast.error("Failed to detect existing browser profiles");
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleBrowseFolder = async () => {
|
||||||
|
try {
|
||||||
|
const selected = await open({
|
||||||
|
directory: true,
|
||||||
|
multiple: false,
|
||||||
|
title: "Select Browser Profile Folder",
|
||||||
|
});
|
||||||
|
|
||||||
|
if (selected && typeof selected === "string") {
|
||||||
|
setManualProfilePath(selected);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to open folder dialog:", error);
|
||||||
|
toast.error("Failed to open folder dialog");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAutoDetectImport = async () => {
|
||||||
|
if (!selectedDetectedProfile || !autoDetectProfileName.trim()) {
|
||||||
|
toast.error("Please select a profile and provide a name");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const profile = detectedProfiles.find(
|
||||||
|
(p) => p.path === selectedDetectedProfile,
|
||||||
|
);
|
||||||
|
if (!profile) {
|
||||||
|
toast.error("Selected profile not found");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsImporting(true);
|
||||||
|
try {
|
||||||
|
await invoke("import_browser_profile", {
|
||||||
|
sourcePath: profile.path,
|
||||||
|
browserType: profile.browser,
|
||||||
|
newProfileName: autoDetectProfileName.trim(),
|
||||||
|
});
|
||||||
|
|
||||||
|
toast.success(
|
||||||
|
`Successfully imported profile "${autoDetectProfileName.trim()}"`,
|
||||||
|
);
|
||||||
|
if (onImportComplete) {
|
||||||
|
onImportComplete();
|
||||||
|
}
|
||||||
|
onClose();
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to import profile:", error);
|
||||||
|
const errorMessage =
|
||||||
|
error instanceof Error ? error.message : String(error);
|
||||||
|
toast.error(`Failed to import profile: ${errorMessage}`);
|
||||||
|
} finally {
|
||||||
|
setIsImporting(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleManualImport = async () => {
|
||||||
|
if (
|
||||||
|
!manualBrowserType ||
|
||||||
|
!manualProfilePath.trim() ||
|
||||||
|
!manualProfileName.trim()
|
||||||
|
) {
|
||||||
|
toast.error("Please fill in all fields");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsImporting(true);
|
||||||
|
try {
|
||||||
|
await invoke("import_browser_profile", {
|
||||||
|
sourcePath: manualProfilePath.trim(),
|
||||||
|
browserType: manualBrowserType,
|
||||||
|
newProfileName: manualProfileName.trim(),
|
||||||
|
});
|
||||||
|
|
||||||
|
toast.success(
|
||||||
|
`Successfully imported profile "${manualProfileName.trim()}"`,
|
||||||
|
);
|
||||||
|
if (onImportComplete) {
|
||||||
|
onImportComplete();
|
||||||
|
}
|
||||||
|
onClose();
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to import profile:", error);
|
||||||
|
const errorMessage =
|
||||||
|
error instanceof Error ? error.message : String(error);
|
||||||
|
toast.error(`Failed to import profile: ${errorMessage}`);
|
||||||
|
} finally {
|
||||||
|
setIsImporting(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
setSelectedDetectedProfile(null);
|
||||||
|
setAutoDetectProfileName("");
|
||||||
|
setManualBrowserType(null);
|
||||||
|
setManualProfilePath("");
|
||||||
|
setManualProfileName("");
|
||||||
|
// Only reset to auto-detect if there are profiles available
|
||||||
|
if (detectedProfiles.length > 0) {
|
||||||
|
setImportMode("auto-detect");
|
||||||
|
} else {
|
||||||
|
setImportMode("manual");
|
||||||
|
}
|
||||||
|
onClose();
|
||||||
|
};
|
||||||
|
|
||||||
|
// Update auto-detect profile name when selection changes
|
||||||
|
useEffect(() => {
|
||||||
|
if (selectedDetectedProfile) {
|
||||||
|
const profile = detectedProfiles.find(
|
||||||
|
(p) => p.path === selectedDetectedProfile,
|
||||||
|
);
|
||||||
|
if (profile) {
|
||||||
|
const browserName = getBrowserDisplayName(profile.browser);
|
||||||
|
const defaultName = `Imported ${browserName} Profile`;
|
||||||
|
setAutoDetectProfileName(defaultName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [selectedDetectedProfile, detectedProfiles]);
|
||||||
|
|
||||||
|
const selectedProfile = detectedProfiles.find(
|
||||||
|
(p) => p.path === selectedDetectedProfile,
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={isOpen} onOpenChange={onClose}>
|
||||||
|
<DialogContent className="max-w-2xl max-h-[80vh] my-8 flex flex-col">
|
||||||
|
<DialogHeader className="flex-shrink-0">
|
||||||
|
<DialogTitle>Import Browser Profile</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<div className="overflow-y-auto flex-1 space-y-6 min-h-0">
|
||||||
|
{/* Mode Selection */}
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Button
|
||||||
|
variant={importMode === "auto-detect" ? "default" : "outline"}
|
||||||
|
onClick={() => {
|
||||||
|
setImportMode("auto-detect");
|
||||||
|
}}
|
||||||
|
className="flex-1"
|
||||||
|
disabled={isLoading}
|
||||||
|
>
|
||||||
|
Auto-Detect
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant={importMode === "manual" ? "default" : "outline"}
|
||||||
|
onClick={() => {
|
||||||
|
setImportMode("manual");
|
||||||
|
}}
|
||||||
|
className="flex-1"
|
||||||
|
disabled={isLoading}
|
||||||
|
>
|
||||||
|
Manual Import
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Auto-Detect Mode */}
|
||||||
|
{importMode === "auto-detect" && (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<h3 className="text-lg font-medium">Detected Browser Profiles</h3>
|
||||||
|
|
||||||
|
{isLoading ? (
|
||||||
|
<div className="py-8 text-center">
|
||||||
|
<p className="text-muted-foreground">
|
||||||
|
Scanning for browser profiles...
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
) : detectedProfiles.length === 0 ? (
|
||||||
|
<div className="py-8 text-center">
|
||||||
|
<p className="text-muted-foreground">
|
||||||
|
No browser profiles found on your system.
|
||||||
|
</p>
|
||||||
|
<p className="mt-2 text-sm text-muted-foreground">
|
||||||
|
Try the manual import option if you have profiles in custom
|
||||||
|
locations.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<Label htmlFor="detected-profile-select" className="mb-2">
|
||||||
|
Select Profile:
|
||||||
|
</Label>
|
||||||
|
<Select
|
||||||
|
value={selectedDetectedProfile ?? undefined}
|
||||||
|
onValueChange={(value) => {
|
||||||
|
setSelectedDetectedProfile(value);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<SelectTrigger id="detected-profile-select">
|
||||||
|
<SelectValue placeholder="Choose a detected profile" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{detectedProfiles.map((profile) => {
|
||||||
|
const IconComponent = getBrowserIcon(profile.browser);
|
||||||
|
return (
|
||||||
|
<SelectItem key={profile.path} value={profile.path}>
|
||||||
|
<div className="flex gap-2 items-center">
|
||||||
|
{IconComponent && (
|
||||||
|
<IconComponent className="w-4 h-4" />
|
||||||
|
)}
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<span className="font-medium">
|
||||||
|
{profile.name}
|
||||||
|
</span>
|
||||||
|
<span className="text-xs text-muted-foreground">
|
||||||
|
{profile.description}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</SelectItem>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{selectedProfile && (
|
||||||
|
<div className="p-3 rounded-lg bg-muted">
|
||||||
|
<p className="text-sm">
|
||||||
|
<span className="font-medium">Path:</span>{" "}
|
||||||
|
{selectedProfile.path}
|
||||||
|
</p>
|
||||||
|
<p className="text-sm">
|
||||||
|
<span className="font-medium">Browser:</span>{" "}
|
||||||
|
{getBrowserDisplayName(selectedProfile.browser)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Label htmlFor="auto-profile-name" className="mb-2">
|
||||||
|
New Profile Name:
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
id="auto-profile-name"
|
||||||
|
value={autoDetectProfileName}
|
||||||
|
onChange={(e) => {
|
||||||
|
setAutoDetectProfileName(e.target.value);
|
||||||
|
}}
|
||||||
|
placeholder="Enter a name for the imported profile"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Manual Import Mode */}
|
||||||
|
{importMode === "manual" && (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<h3 className="text-lg font-medium">Manual Profile Import</h3>
|
||||||
|
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<Label htmlFor="manual-browser-select" className="mb-2">
|
||||||
|
Browser Type:
|
||||||
|
</Label>
|
||||||
|
<Select
|
||||||
|
value={manualBrowserType ?? undefined}
|
||||||
|
onValueChange={(value) => {
|
||||||
|
setManualBrowserType(value);
|
||||||
|
}}
|
||||||
|
disabled={isLoadingSupport}
|
||||||
|
>
|
||||||
|
<SelectTrigger id="manual-browser-select">
|
||||||
|
<SelectValue
|
||||||
|
placeholder={
|
||||||
|
isLoadingSupport
|
||||||
|
? "Loading browsers..."
|
||||||
|
: "Select browser type"
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{supportedBrowsers.map((browser) => {
|
||||||
|
const IconComponent = getBrowserIcon(browser);
|
||||||
|
return (
|
||||||
|
<SelectItem key={browser} value={browser}>
|
||||||
|
<div className="flex gap-2 items-center">
|
||||||
|
{IconComponent && (
|
||||||
|
<IconComponent className="w-4 h-4" />
|
||||||
|
)}
|
||||||
|
<span>{getBrowserDisplayName(browser)}</span>
|
||||||
|
</div>
|
||||||
|
</SelectItem>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Label htmlFor="manual-profile-path" className="mb-2">
|
||||||
|
Profile Folder Path:
|
||||||
|
</Label>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Input
|
||||||
|
id="manual-profile-path"
|
||||||
|
value={manualProfilePath}
|
||||||
|
onChange={(e) => {
|
||||||
|
setManualProfilePath(e.target.value);
|
||||||
|
}}
|
||||||
|
placeholder="Enter the full path to the profile folder"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="icon"
|
||||||
|
onClick={() => void handleBrowseFolder()}
|
||||||
|
title="Browse for folder"
|
||||||
|
>
|
||||||
|
<FaFolder className="w-4 h-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<p className="mt-2 text-xs text-muted-foreground">
|
||||||
|
Example paths:
|
||||||
|
<br />
|
||||||
|
macOS: ~/Library/Application
|
||||||
|
Support/Firefox/Profiles/xxx.default
|
||||||
|
<br />
|
||||||
|
Windows: %APPDATA%\Mozilla\Firefox\Profiles\xxx.default
|
||||||
|
<br />
|
||||||
|
Linux: ~/.mozilla/firefox/xxx.default
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Label htmlFor="manual-profile-name" className="mb-2">
|
||||||
|
New Profile Name:
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
id="manual-profile-name"
|
||||||
|
value={manualProfileName}
|
||||||
|
onChange={(e) => {
|
||||||
|
setManualProfileName(e.target.value);
|
||||||
|
}}
|
||||||
|
placeholder="Enter a name for the imported profile"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DialogFooter className="flex-shrink-0">
|
||||||
|
<Button variant="outline" onClick={handleClose}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
{importMode === "auto-detect" ? (
|
||||||
|
<LoadingButton
|
||||||
|
isLoading={isImporting}
|
||||||
|
onClick={() => {
|
||||||
|
void handleAutoDetectImport();
|
||||||
|
}}
|
||||||
|
disabled={
|
||||||
|
!selectedDetectedProfile ||
|
||||||
|
!autoDetectProfileName.trim() ||
|
||||||
|
isLoading
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Import Detected Profile
|
||||||
|
</LoadingButton>
|
||||||
|
) : (
|
||||||
|
<LoadingButton
|
||||||
|
isLoading={isImporting}
|
||||||
|
onClick={() => {
|
||||||
|
void handleManualImport();
|
||||||
|
}}
|
||||||
|
disabled={
|
||||||
|
!manualBrowserType ||
|
||||||
|
!manualProfilePath.trim() ||
|
||||||
|
!manualProfileName.trim()
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Import Manual Profile
|
||||||
|
</LoadingButton>
|
||||||
|
)}
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -20,6 +20,13 @@ export interface BrowserProfile {
|
|||||||
last_launch?: number;
|
last_launch?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface DetectedProfile {
|
||||||
|
browser: string;
|
||||||
|
name: string;
|
||||||
|
path: string;
|
||||||
|
description: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface AppUpdateInfo {
|
export interface AppUpdateInfo {
|
||||||
current_version: string;
|
current_version: string;
|
||||||
new_version: string;
|
new_version: string;
|
||||||
|
|||||||
Reference in New Issue
Block a user