mirror of
https://github.com/zhom/donutbrowser.git
synced 2026-06-28 09:29:58 +02:00
Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| bbc12bcc03 | |||
| 6d437f30e1 | |||
| 4b0ab6b732 | |||
| a802895491 | |||
| a57f90899b | |||
| 3ebc714b23 | |||
| 1acd4781b5 | |||
| a5b9afafcb | |||
| 0c8dd5ace5 | |||
| e8c3188657 | |||
| e6f0b2b9e9 | |||
| 394406e134 |
@@ -3,4 +3,4 @@ description:
|
|||||||
globs:
|
globs:
|
||||||
alwaysApply: true
|
alwaysApply: true
|
||||||
---
|
---
|
||||||
After your changes, instead of running specific tests or linting specific files, run "pnpm format && pnpm lint && pnpm test"
|
After your changes, instead of running specific tests or linting specific files, run "pnpm format && pnpm lint && pnpm test". It means that you first format the code, then lint it, then test it, so that no part is broken after your changes.
|
||||||
@@ -69,19 +69,18 @@ jobs:
|
|||||||
target: "aarch64-unknown-linux-gnu"
|
target: "aarch64-unknown-linux-gnu"
|
||||||
pkg_target: "latest-linux-arm64"
|
pkg_target: "latest-linux-arm64"
|
||||||
nodecar_script: "build:linux-arm64"
|
nodecar_script: "build:linux-arm64"
|
||||||
# Future platforms can be added here:
|
- platform: "windows-11"
|
||||||
# - platform: "windows-latest"
|
args: "--target x86_64-pc-windows-msvc"
|
||||||
# args: "--target x86_64-pc-windows-msvc"
|
arch: "x86_64"
|
||||||
# arch: "x86_64"
|
target: "x86_64-pc-windows-msvc"
|
||||||
# target: "x86_64-pc-windows-msvc"
|
pkg_target: "latest-win-x64"
|
||||||
# pkg_target: "latest-win-x64"
|
nodecar_script: "build:win-x64"
|
||||||
# nodecar_script: "build:win-x64"
|
- platform: "windows-11-arm"
|
||||||
# - platform: "windows-latest"
|
args: "--target aarch64-pc-windows-msvc"
|
||||||
# args: "--target aarch64-pc-windows-msvc"
|
arch: "aarch64"
|
||||||
# arch: "aarch64"
|
target: "aarch64-pc-windows-msvc"
|
||||||
# target: "aarch64-pc-windows-msvc"
|
pkg_target: "latest-win-arm64"
|
||||||
# pkg_target: "latest-win-arm64"
|
nodecar_script: "build:win-arm64"
|
||||||
# nodecar_script: "build:win-arm64"
|
|
||||||
|
|
||||||
runs-on: ${{ matrix.platform }}
|
runs-on: ${{ matrix.platform }}
|
||||||
steps:
|
steps:
|
||||||
|
|||||||
@@ -68,6 +68,18 @@ jobs:
|
|||||||
target: "aarch64-unknown-linux-gnu"
|
target: "aarch64-unknown-linux-gnu"
|
||||||
pkg_target: "latest-linux-arm64"
|
pkg_target: "latest-linux-arm64"
|
||||||
nodecar_script: "build:linux-arm64"
|
nodecar_script: "build:linux-arm64"
|
||||||
|
- platform: "windows-11"
|
||||||
|
args: "--target x86_64-pc-windows-msvc"
|
||||||
|
arch: "x86_64"
|
||||||
|
target: "x86_64-pc-windows-msvc"
|
||||||
|
pkg_target: "latest-win-x64"
|
||||||
|
nodecar_script: "build:win-x64"
|
||||||
|
- platform: "windows-11-arm"
|
||||||
|
args: "--target aarch64-pc-windows-msvc"
|
||||||
|
arch: "aarch64"
|
||||||
|
target: "aarch64-pc-windows-msvc"
|
||||||
|
pkg_target: "latest-win-arm64"
|
||||||
|
nodecar_script: "build:win-arm64"
|
||||||
|
|
||||||
runs-on: ${{ matrix.platform }}
|
runs-on: ${{ matrix.platform }}
|
||||||
steps:
|
steps:
|
||||||
|
|||||||
Vendored
+2
@@ -15,6 +15,7 @@
|
|||||||
"donutbrowser",
|
"donutbrowser",
|
||||||
"dpkg",
|
"dpkg",
|
||||||
"dtolnay",
|
"dtolnay",
|
||||||
|
"dyld",
|
||||||
"elif",
|
"elif",
|
||||||
"esbuild",
|
"esbuild",
|
||||||
"eslintcache",
|
"eslintcache",
|
||||||
@@ -66,6 +67,7 @@
|
|||||||
"unlisten",
|
"unlisten",
|
||||||
"unrs",
|
"unrs",
|
||||||
"vercel",
|
"vercel",
|
||||||
|
"winreg",
|
||||||
"wiremock",
|
"wiremock",
|
||||||
"xattr",
|
"xattr",
|
||||||
"zhom"
|
"zhom"
|
||||||
|
|||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
# Instructions for AI Agents
|
# Instructions for AI Agents
|
||||||
|
|
||||||
- If you want to run tests, only ever run them as "pnpm format && pnpm lint && pnpm test".
|
- After your changes, instead of running specific tests or linting specific files, run "pnpm format && pnpm lint && pnpm test". It means that you first format the code, then lint it, then test it, so that no part is broken after your changes.
|
||||||
- Don't leave comments that don't add value
|
- Don't leave comments that don't add value
|
||||||
- Do not duplicate code unless you have a very good reason to do so. It is important that the same logic is not duplicated multiple times
|
- Do not duplicate code unless you have a very good reason to do so. It is important that the same logic is not duplicated multiple times
|
||||||
|
|||||||
+4
-2
@@ -2,7 +2,7 @@
|
|||||||
"name": "donutbrowser",
|
"name": "donutbrowser",
|
||||||
"private": true,
|
"private": true,
|
||||||
"license": "AGPL-3.0",
|
"license": "AGPL-3.0",
|
||||||
"version": "0.3.2",
|
"version": "0.3.3",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "next dev --turbopack",
|
"dev": "next dev --turbopack",
|
||||||
@@ -38,6 +38,7 @@
|
|||||||
"@tauri-apps/plugin-dialog": "^2.2.2",
|
"@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",
|
||||||
|
"ahooks": "^3.8.5",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"cmdk": "^1.1.1",
|
"cmdk": "^1.1.1",
|
||||||
@@ -47,7 +48,8 @@
|
|||||||
"react-dom": "^19.1.0",
|
"react-dom": "^19.1.0",
|
||||||
"react-icons": "^5.5.0",
|
"react-icons": "^5.5.0",
|
||||||
"sonner": "^2.0.5",
|
"sonner": "^2.0.5",
|
||||||
"tailwind-merge": "^3.3.0"
|
"tailwind-merge": "^3.3.0",
|
||||||
|
"tauri-plugin-macos-permissions-api": "^2.3.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@biomejs/biome": "1.9.4",
|
"@biomejs/biome": "1.9.4",
|
||||||
|
|||||||
Generated
+159
-84
@@ -53,6 +53,9 @@ importers:
|
|||||||
'@tauri-apps/plugin-opener':
|
'@tauri-apps/plugin-opener':
|
||||||
specifier: ^2.2.7
|
specifier: ^2.2.7
|
||||||
version: 2.2.7
|
version: 2.2.7
|
||||||
|
ahooks:
|
||||||
|
specifier: ^3.8.5
|
||||||
|
version: 3.8.5(react@19.1.0)
|
||||||
class-variance-authority:
|
class-variance-authority:
|
||||||
specifier: ^0.7.1
|
specifier: ^0.7.1
|
||||||
version: 0.7.1
|
version: 0.7.1
|
||||||
@@ -83,6 +86,9 @@ importers:
|
|||||||
tailwind-merge:
|
tailwind-merge:
|
||||||
specifier: ^3.3.0
|
specifier: ^3.3.0
|
||||||
version: 3.3.0
|
version: 3.3.0
|
||||||
|
tauri-plugin-macos-permissions-api:
|
||||||
|
specifier: ^2.3.0
|
||||||
|
version: 2.3.0
|
||||||
devDependencies:
|
devDependencies:
|
||||||
'@biomejs/biome':
|
'@biomejs/biome':
|
||||||
specifier: 1.9.4
|
specifier: 1.9.4
|
||||||
@@ -255,6 +261,10 @@ packages:
|
|||||||
peerDependencies:
|
peerDependencies:
|
||||||
'@babel/core': ^7.0.0-0
|
'@babel/core': ^7.0.0-0
|
||||||
|
|
||||||
|
'@babel/runtime@7.27.6':
|
||||||
|
resolution: {integrity: sha512-vbavdySgbTTrmFE+EsiqUTzlOr5bzlnJtUv9PynGCAKvfQqjIXbvFdumPM/GxMDfyuGMJaJAU6TO4zc1Jf1i8Q==}
|
||||||
|
engines: {node: '>=6.9.0'}
|
||||||
|
|
||||||
'@babel/template@7.27.2':
|
'@babel/template@7.27.2':
|
||||||
resolution: {integrity: sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==}
|
resolution: {integrity: sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==}
|
||||||
engines: {node: '>=6.9.0'}
|
engines: {node: '>=6.9.0'}
|
||||||
@@ -1168,103 +1178,103 @@ packages:
|
|||||||
'@rolldown/pluginutils@1.0.0-beta.9':
|
'@rolldown/pluginutils@1.0.0-beta.9':
|
||||||
resolution: {integrity: sha512-e9MeMtVWo186sgvFFJOPGy7/d2j2mZhLJIdVW0C/xDluuOvymEATqz6zKsP0ZmXGzQtqlyjz5sC1sYQUoJG98w==}
|
resolution: {integrity: sha512-e9MeMtVWo186sgvFFJOPGy7/d2j2mZhLJIdVW0C/xDluuOvymEATqz6zKsP0ZmXGzQtqlyjz5sC1sYQUoJG98w==}
|
||||||
|
|
||||||
'@rollup/rollup-android-arm-eabi@4.41.1':
|
'@rollup/rollup-android-arm-eabi@4.42.0':
|
||||||
resolution: {integrity: sha512-NELNvyEWZ6R9QMkiytB4/L4zSEaBC03KIXEghptLGLZWJ6VPrL63ooZQCOnlx36aQPGhzuOMwDerC1Eb2VmrLw==}
|
resolution: {integrity: sha512-gldmAyS9hpj+H6LpRNlcjQWbuKUtb94lodB9uCz71Jm+7BxK1VIOo7y62tZZwxhA7j1ylv/yQz080L5WkS+LoQ==}
|
||||||
cpu: [arm]
|
cpu: [arm]
|
||||||
os: [android]
|
os: [android]
|
||||||
|
|
||||||
'@rollup/rollup-android-arm64@4.41.1':
|
'@rollup/rollup-android-arm64@4.42.0':
|
||||||
resolution: {integrity: sha512-DXdQe1BJ6TK47ukAoZLehRHhfKnKg9BjnQYUu9gzhI8Mwa1d2fzxA1aw2JixHVl403bwp1+/o/NhhHtxWJBgEA==}
|
resolution: {integrity: sha512-bpRipfTgmGFdCZDFLRvIkSNO1/3RGS74aWkJJTFJBH7h3MRV4UijkaEUeOMbi9wxtxYmtAbVcnMtHTPBhLEkaw==}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [android]
|
os: [android]
|
||||||
|
|
||||||
'@rollup/rollup-darwin-arm64@4.41.1':
|
'@rollup/rollup-darwin-arm64@4.42.0':
|
||||||
resolution: {integrity: sha512-5afxvwszzdulsU2w8JKWwY8/sJOLPzf0e1bFuvcW5h9zsEg+RQAojdW0ux2zyYAz7R8HvvzKCjLNJhVq965U7w==}
|
resolution: {integrity: sha512-JxHtA081izPBVCHLKnl6GEA0w3920mlJPLh89NojpU2GsBSB6ypu4erFg/Wx1qbpUbepn0jY4dVWMGZM8gplgA==}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [darwin]
|
os: [darwin]
|
||||||
|
|
||||||
'@rollup/rollup-darwin-x64@4.41.1':
|
'@rollup/rollup-darwin-x64@4.42.0':
|
||||||
resolution: {integrity: sha512-egpJACny8QOdHNNMZKf8xY0Is6gIMz+tuqXlusxquWu3F833DcMwmGM7WlvCO9sB3OsPjdC4U0wHw5FabzCGZg==}
|
resolution: {integrity: sha512-rv5UZaWVIJTDMyQ3dCEK+m0SAn6G7H3PRc2AZmExvbDvtaDc+qXkei0knQWcI3+c9tEs7iL/4I4pTQoPbNL2SA==}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [darwin]
|
os: [darwin]
|
||||||
|
|
||||||
'@rollup/rollup-freebsd-arm64@4.41.1':
|
'@rollup/rollup-freebsd-arm64@4.42.0':
|
||||||
resolution: {integrity: sha512-DBVMZH5vbjgRk3r0OzgjS38z+atlupJ7xfKIDJdZZL6sM6wjfDNo64aowcLPKIx7LMQi8vybB56uh1Ftck/Atg==}
|
resolution: {integrity: sha512-fJcN4uSGPWdpVmvLuMtALUFwCHgb2XiQjuECkHT3lWLZhSQ3MBQ9pq+WoWeJq2PrNxr9rPM1Qx+IjyGj8/c6zQ==}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [freebsd]
|
os: [freebsd]
|
||||||
|
|
||||||
'@rollup/rollup-freebsd-x64@4.41.1':
|
'@rollup/rollup-freebsd-x64@4.42.0':
|
||||||
resolution: {integrity: sha512-3FkydeohozEskBxNWEIbPfOE0aqQgB6ttTkJ159uWOFn42VLyfAiyD9UK5mhu+ItWzft60DycIN1Xdgiy8o/SA==}
|
resolution: {integrity: sha512-CziHfyzpp8hJpCVE/ZdTizw58gr+m7Y2Xq5VOuCSrZR++th2xWAz4Nqk52MoIIrV3JHtVBhbBsJcAxs6NammOQ==}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [freebsd]
|
os: [freebsd]
|
||||||
|
|
||||||
'@rollup/rollup-linux-arm-gnueabihf@4.41.1':
|
'@rollup/rollup-linux-arm-gnueabihf@4.42.0':
|
||||||
resolution: {integrity: sha512-wC53ZNDgt0pqx5xCAgNunkTzFE8GTgdZ9EwYGVcg+jEjJdZGtq9xPjDnFgfFozQI/Xm1mh+D9YlYtl+ueswNEg==}
|
resolution: {integrity: sha512-UsQD5fyLWm2Fe5CDM7VPYAo+UC7+2Px4Y+N3AcPh/LdZu23YcuGPegQly++XEVaC8XUTFVPscl5y5Cl1twEI4A==}
|
||||||
cpu: [arm]
|
cpu: [arm]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
|
||||||
'@rollup/rollup-linux-arm-musleabihf@4.41.1':
|
'@rollup/rollup-linux-arm-musleabihf@4.42.0':
|
||||||
resolution: {integrity: sha512-jwKCca1gbZkZLhLRtsrka5N8sFAaxrGz/7wRJ8Wwvq3jug7toO21vWlViihG85ei7uJTpzbXZRcORotE+xyrLA==}
|
resolution: {integrity: sha512-/i8NIrlgc/+4n1lnoWl1zgH7Uo0XK5xK3EDqVTf38KvyYgCU/Rm04+o1VvvzJZnVS5/cWSd07owkzcVasgfIkQ==}
|
||||||
cpu: [arm]
|
cpu: [arm]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
|
||||||
'@rollup/rollup-linux-arm64-gnu@4.41.1':
|
'@rollup/rollup-linux-arm64-gnu@4.42.0':
|
||||||
resolution: {integrity: sha512-g0UBcNknsmmNQ8V2d/zD2P7WWfJKU0F1nu0k5pW4rvdb+BIqMm8ToluW/eeRmxCared5dD76lS04uL4UaNgpNA==}
|
resolution: {integrity: sha512-eoujJFOvoIBjZEi9hJnXAbWg+Vo1Ov8n/0IKZZcPZ7JhBzxh2A+2NFyeMZIRkY9iwBvSjloKgcvnjTbGKHE44Q==}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
|
||||||
'@rollup/rollup-linux-arm64-musl@4.41.1':
|
'@rollup/rollup-linux-arm64-musl@4.42.0':
|
||||||
resolution: {integrity: sha512-XZpeGB5TKEZWzIrj7sXr+BEaSgo/ma/kCgrZgL0oo5qdB1JlTzIYQKel/RmhT6vMAvOdM2teYlAaOGJpJ9lahg==}
|
resolution: {integrity: sha512-/3NrcOWFSR7RQUQIuZQChLND36aTU9IYE4j+TB40VU78S+RA0IiqHR30oSh6P1S9f9/wVOenHQnacs/Byb824g==}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
|
||||||
'@rollup/rollup-linux-loongarch64-gnu@4.41.1':
|
'@rollup/rollup-linux-loongarch64-gnu@4.42.0':
|
||||||
resolution: {integrity: sha512-bkCfDJ4qzWfFRCNt5RVV4DOw6KEgFTUZi2r2RuYhGWC8WhCA8lCAJhDeAmrM/fdiAH54m0mA0Vk2FGRPyzI+tw==}
|
resolution: {integrity: sha512-O8AplvIeavK5ABmZlKBq9/STdZlnQo7Sle0LLhVA7QT+CiGpNVe197/t8Aph9bhJqbDVGCHpY2i7QyfEDDStDg==}
|
||||||
cpu: [loong64]
|
cpu: [loong64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
|
||||||
'@rollup/rollup-linux-powerpc64le-gnu@4.41.1':
|
'@rollup/rollup-linux-powerpc64le-gnu@4.42.0':
|
||||||
resolution: {integrity: sha512-3mr3Xm+gvMX+/8EKogIZSIEF0WUu0HL9di+YWlJpO8CQBnoLAEL/roTCxuLncEdgcfJcvA4UMOf+2dnjl4Ut1A==}
|
resolution: {integrity: sha512-6Qb66tbKVN7VyQrekhEzbHRxXXFFD8QKiFAwX5v9Xt6FiJ3BnCVBuyBxa2fkFGqxOCSGGYNejxd8ht+q5SnmtA==}
|
||||||
cpu: [ppc64]
|
cpu: [ppc64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
|
||||||
'@rollup/rollup-linux-riscv64-gnu@4.41.1':
|
'@rollup/rollup-linux-riscv64-gnu@4.42.0':
|
||||||
resolution: {integrity: sha512-3rwCIh6MQ1LGrvKJitQjZFuQnT2wxfU+ivhNBzmxXTXPllewOF7JR1s2vMX/tWtUYFgphygxjqMl76q4aMotGw==}
|
resolution: {integrity: sha512-KQETDSEBamQFvg/d8jajtRwLNBlGc3aKpaGiP/LvEbnmVUKlFta1vqJqTrvPtsYsfbE/DLg5CC9zyXRX3fnBiA==}
|
||||||
cpu: [riscv64]
|
cpu: [riscv64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
|
||||||
'@rollup/rollup-linux-riscv64-musl@4.41.1':
|
'@rollup/rollup-linux-riscv64-musl@4.42.0':
|
||||||
resolution: {integrity: sha512-LdIUOb3gvfmpkgFZuccNa2uYiqtgZAz3PTzjuM5bH3nvuy9ty6RGc/Q0+HDFrHrizJGVpjnTZ1yS5TNNjFlklw==}
|
resolution: {integrity: sha512-qMvnyjcU37sCo/tuC+JqeDKSuukGAd+pVlRl/oyDbkvPJ3awk6G6ua7tyum02O3lI+fio+eM5wsVd66X0jQtxw==}
|
||||||
cpu: [riscv64]
|
cpu: [riscv64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
|
||||||
'@rollup/rollup-linux-s390x-gnu@4.41.1':
|
'@rollup/rollup-linux-s390x-gnu@4.42.0':
|
||||||
resolution: {integrity: sha512-oIE6M8WC9ma6xYqjvPhzZYk6NbobIURvP/lEbh7FWplcMO6gn7MM2yHKA1eC/GvYwzNKK/1LYgqzdkZ8YFxR8g==}
|
resolution: {integrity: sha512-I2Y1ZUgTgU2RLddUHXTIgyrdOwljjkmcZ/VilvaEumtS3Fkuhbw4p4hgHc39Ypwvo2o7sBFNl2MquNvGCa55Iw==}
|
||||||
cpu: [s390x]
|
cpu: [s390x]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
|
||||||
'@rollup/rollup-linux-x64-gnu@4.41.1':
|
'@rollup/rollup-linux-x64-gnu@4.42.0':
|
||||||
resolution: {integrity: sha512-cWBOvayNvA+SyeQMp79BHPK8ws6sHSsYnK5zDcsC3Hsxr1dgTABKjMnMslPq1DvZIp6uO7kIWhiGwaTdR4Og9A==}
|
resolution: {integrity: sha512-Gfm6cV6mj3hCUY8TqWa63DB8Mx3NADoFwiJrMpoZ1uESbK8FQV3LXkhfry+8bOniq9pqY1OdsjFWNsSbfjPugw==}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
|
||||||
'@rollup/rollup-linux-x64-musl@4.41.1':
|
'@rollup/rollup-linux-x64-musl@4.42.0':
|
||||||
resolution: {integrity: sha512-y5CbN44M+pUCdGDlZFzGGBSKCA4A/J2ZH4edTYSSxFg7ce1Xt3GtydbVKWLlzL+INfFIZAEg1ZV6hh9+QQf9YQ==}
|
resolution: {integrity: sha512-g86PF8YZ9GRqkdi0VoGlcDUb4rYtQKyTD1IVtxxN4Hpe7YqLBShA7oHMKU6oKTCi3uxwW4VkIGnOaH/El8de3w==}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
|
||||||
'@rollup/rollup-win32-arm64-msvc@4.41.1':
|
'@rollup/rollup-win32-arm64-msvc@4.42.0':
|
||||||
resolution: {integrity: sha512-lZkCxIrjlJlMt1dLO/FbpZbzt6J/A8p4DnqzSa4PWqPEUUUnzXLeki/iyPLfV0BmHItlYgHUqJe+3KiyydmiNQ==}
|
resolution: {integrity: sha512-+axkdyDGSp6hjyzQ5m1pgcvQScfHnMCcsXkx8pTgy/6qBmWVhtRVlgxjWwDp67wEXXUr0x+vD6tp5W4x6V7u1A==}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [win32]
|
os: [win32]
|
||||||
|
|
||||||
'@rollup/rollup-win32-ia32-msvc@4.41.1':
|
'@rollup/rollup-win32-ia32-msvc@4.42.0':
|
||||||
resolution: {integrity: sha512-+psFT9+pIh2iuGsxFYYa/LhS5MFKmuivRsx9iPJWNSGbh2XVEjk90fmpUEjCnILPEPJnikAU6SFDiEUyOv90Pg==}
|
resolution: {integrity: sha512-F+5J9pelstXKwRSDq92J0TEBXn2nfUrQGg+HK1+Tk7VOL09e0gBqUHugZv7SW4MGrYj41oNCUe3IKCDGVlis2g==}
|
||||||
cpu: [ia32]
|
cpu: [ia32]
|
||||||
os: [win32]
|
os: [win32]
|
||||||
|
|
||||||
'@rollup/rollup-win32-x64-msvc@4.41.1':
|
'@rollup/rollup-win32-x64-msvc@4.42.0':
|
||||||
resolution: {integrity: sha512-Wq2zpapRYLfi4aKxf2Xff0tN+7slj2d4R87WEzqw7ZLsVvO5zwYCIuEGSZYiK41+GlwUo1HiR+GdkLEJnCKTCw==}
|
resolution: {integrity: sha512-LpHiJRwkaVz/LqjHjK8LCi8osq7elmpwujwbXKNW88bM8eeGxavJIKKjkjpMHAh/2xfnrt1ZSnhTv41WYUHYmA==}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [win32]
|
os: [win32]
|
||||||
|
|
||||||
@@ -1690,6 +1700,12 @@ packages:
|
|||||||
resolution: {integrity: sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==}
|
resolution: {integrity: sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==}
|
||||||
engines: {node: '>= 14'}
|
engines: {node: '>= 14'}
|
||||||
|
|
||||||
|
ahooks@3.8.5:
|
||||||
|
resolution: {integrity: sha512-Y+MLoJpBXVdjsnnBjE5rOSPkQ4DK+8i5aPDzLJdIOsCpo/fiAeXcBY1Y7oWgtOK0TpOz0gFa/XcyO1UGdoqLcw==}
|
||||||
|
engines: {node: '>=8.0.0'}
|
||||||
|
peerDependencies:
|
||||||
|
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
|
||||||
|
|
||||||
ajv@6.12.6:
|
ajv@6.12.6:
|
||||||
resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==}
|
resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==}
|
||||||
|
|
||||||
@@ -1940,6 +1956,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==}
|
resolution: {integrity: sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
|
|
||||||
|
dayjs@1.11.13:
|
||||||
|
resolution: {integrity: sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==}
|
||||||
|
|
||||||
debug@3.2.7:
|
debug@3.2.7:
|
||||||
resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==}
|
resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -2409,6 +2428,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==}
|
resolution: {integrity: sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
|
|
||||||
|
intersection-observer@0.12.2:
|
||||||
|
resolution: {integrity: sha512-7m1vEcPCxXYI8HqnL8CKI6siDyD+eIWSwgB3DZA+ZTogxk9I4CDnj4wilt9x/+/QbHI4YG5YZNmC6458/e9Ktg==}
|
||||||
|
|
||||||
into-stream@6.0.0:
|
into-stream@6.0.0:
|
||||||
resolution: {integrity: sha512-XHbaOAvP+uFKUFsOgoNPRjLkwB+I22JFPFe5OjTkQ0nwgj6+pSjb4NmB6VMxaPshLiOf+zcpOCBQuLwC1KHhZA==}
|
resolution: {integrity: sha512-XHbaOAvP+uFKUFsOgoNPRjLkwB+I22JFPFe5OjTkQ0nwgj6+pSjb4NmB6VMxaPshLiOf+zcpOCBQuLwC1KHhZA==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
@@ -2556,6 +2578,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==}
|
resolution: {integrity: sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
js-cookie@3.0.5:
|
||||||
|
resolution: {integrity: sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==}
|
||||||
|
engines: {node: '>=14'}
|
||||||
|
|
||||||
js-tokens@4.0.0:
|
js-tokens@4.0.0:
|
||||||
resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
|
resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
|
||||||
|
|
||||||
@@ -2694,6 +2720,9 @@ packages:
|
|||||||
lodash.merge@4.6.2:
|
lodash.merge@4.6.2:
|
||||||
resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==}
|
resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==}
|
||||||
|
|
||||||
|
lodash@4.17.21:
|
||||||
|
resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==}
|
||||||
|
|
||||||
log-update@6.1.0:
|
log-update@6.1.0:
|
||||||
resolution: {integrity: sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==}
|
resolution: {integrity: sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
@@ -2985,6 +3014,9 @@ packages:
|
|||||||
peerDependencies:
|
peerDependencies:
|
||||||
react: ^19.1.0
|
react: ^19.1.0
|
||||||
|
|
||||||
|
react-fast-compare@3.2.2:
|
||||||
|
resolution: {integrity: sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ==}
|
||||||
|
|
||||||
react-icons@5.5.0:
|
react-icons@5.5.0:
|
||||||
resolution: {integrity: sha512-MEFcXdkP3dLo8uumGI5xN3lDFNsRtrjbOEKDLD7yv76v4wpnEq2Lt2qeHaQOr34I/wPN3s3+N08WkQ+CW37Xiw==}
|
resolution: {integrity: sha512-MEFcXdkP3dLo8uumGI5xN3lDFNsRtrjbOEKDLD7yv76v4wpnEq2Lt2qeHaQOr34I/wPN3s3+N08WkQ+CW37Xiw==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -3054,6 +3086,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==}
|
resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
|
|
||||||
|
resize-observer-polyfill@1.5.1:
|
||||||
|
resolution: {integrity: sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==}
|
||||||
|
|
||||||
resolve-from@4.0.0:
|
resolve-from@4.0.0:
|
||||||
resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==}
|
resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==}
|
||||||
engines: {node: '>=4'}
|
engines: {node: '>=4'}
|
||||||
@@ -3081,8 +3116,8 @@ packages:
|
|||||||
rfdc@1.4.1:
|
rfdc@1.4.1:
|
||||||
resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==}
|
resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==}
|
||||||
|
|
||||||
rollup@4.41.1:
|
rollup@4.42.0:
|
||||||
resolution: {integrity: sha512-cPmwD3FnFv8rKMBc1MxWCwVQFxwf1JEmSX3iQXrRVVG15zerAIXRjMFVWnd5Q5QvgKF7Aj+5ykXFhUl+QGnyOw==}
|
resolution: {integrity: sha512-LW+Vse3BJPyGJGAJt1j8pWDKPd73QM8cRXYK1IxOBgL2AGLu7Xd2YOW0M2sLUBCkF5MshXXtMApyEAEzMVMsnw==}
|
||||||
engines: {node: '>=18.0.0', npm: '>=8.0.0'}
|
engines: {node: '>=18.0.0', npm: '>=8.0.0'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
@@ -3110,6 +3145,10 @@ packages:
|
|||||||
scheduler@0.26.0:
|
scheduler@0.26.0:
|
||||||
resolution: {integrity: sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==}
|
resolution: {integrity: sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==}
|
||||||
|
|
||||||
|
screenfull@5.2.0:
|
||||||
|
resolution: {integrity: sha512-9BakfsO2aUQN2K9Fdbj87RJIEZ82Q9IGim7FqM5OsebfoFC6ZHXgDq/KvniuLTPdeM8wY2o6Dj3WQ7KeQCj3cA==}
|
||||||
|
engines: {node: '>=0.10.0'}
|
||||||
|
|
||||||
semver@6.3.1:
|
semver@6.3.1:
|
||||||
resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==}
|
resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
@@ -3330,6 +3369,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==}
|
resolution: {integrity: sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
|
tauri-plugin-macos-permissions-api@2.3.0:
|
||||||
|
resolution: {integrity: sha512-pZp0jmDySysBqrGueknd1a7Rr4XEO9aXpMv9TNrT2PDHP0MSH20njieOagsFYJ5MCVb8A+wcaK0cIkjUC2dOww==}
|
||||||
|
|
||||||
tinyglobby@0.2.14:
|
tinyglobby@0.2.14:
|
||||||
resolution: {integrity: sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==}
|
resolution: {integrity: sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==}
|
||||||
engines: {node: '>=12.0.0'}
|
engines: {node: '>=12.0.0'}
|
||||||
@@ -3674,6 +3716,8 @@ snapshots:
|
|||||||
'@babel/core': 7.27.4
|
'@babel/core': 7.27.4
|
||||||
'@babel/helper-plugin-utils': 7.27.1
|
'@babel/helper-plugin-utils': 7.27.1
|
||||||
|
|
||||||
|
'@babel/runtime@7.27.6': {}
|
||||||
|
|
||||||
'@babel/template@7.27.2':
|
'@babel/template@7.27.2':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/code-frame': 7.27.1
|
'@babel/code-frame': 7.27.1
|
||||||
@@ -4464,64 +4508,64 @@ snapshots:
|
|||||||
|
|
||||||
'@rolldown/pluginutils@1.0.0-beta.9': {}
|
'@rolldown/pluginutils@1.0.0-beta.9': {}
|
||||||
|
|
||||||
'@rollup/rollup-android-arm-eabi@4.41.1':
|
'@rollup/rollup-android-arm-eabi@4.42.0':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@rollup/rollup-android-arm64@4.41.1':
|
'@rollup/rollup-android-arm64@4.42.0':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@rollup/rollup-darwin-arm64@4.41.1':
|
'@rollup/rollup-darwin-arm64@4.42.0':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@rollup/rollup-darwin-x64@4.41.1':
|
'@rollup/rollup-darwin-x64@4.42.0':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@rollup/rollup-freebsd-arm64@4.41.1':
|
'@rollup/rollup-freebsd-arm64@4.42.0':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@rollup/rollup-freebsd-x64@4.41.1':
|
'@rollup/rollup-freebsd-x64@4.42.0':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@rollup/rollup-linux-arm-gnueabihf@4.41.1':
|
'@rollup/rollup-linux-arm-gnueabihf@4.42.0':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@rollup/rollup-linux-arm-musleabihf@4.41.1':
|
'@rollup/rollup-linux-arm-musleabihf@4.42.0':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@rollup/rollup-linux-arm64-gnu@4.41.1':
|
'@rollup/rollup-linux-arm64-gnu@4.42.0':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@rollup/rollup-linux-arm64-musl@4.41.1':
|
'@rollup/rollup-linux-arm64-musl@4.42.0':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@rollup/rollup-linux-loongarch64-gnu@4.41.1':
|
'@rollup/rollup-linux-loongarch64-gnu@4.42.0':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@rollup/rollup-linux-powerpc64le-gnu@4.41.1':
|
'@rollup/rollup-linux-powerpc64le-gnu@4.42.0':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@rollup/rollup-linux-riscv64-gnu@4.41.1':
|
'@rollup/rollup-linux-riscv64-gnu@4.42.0':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@rollup/rollup-linux-riscv64-musl@4.41.1':
|
'@rollup/rollup-linux-riscv64-musl@4.42.0':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@rollup/rollup-linux-s390x-gnu@4.41.1':
|
'@rollup/rollup-linux-s390x-gnu@4.42.0':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@rollup/rollup-linux-x64-gnu@4.41.1':
|
'@rollup/rollup-linux-x64-gnu@4.42.0':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@rollup/rollup-linux-x64-musl@4.41.1':
|
'@rollup/rollup-linux-x64-musl@4.42.0':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@rollup/rollup-win32-arm64-msvc@4.41.1':
|
'@rollup/rollup-win32-arm64-msvc@4.42.0':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@rollup/rollup-win32-ia32-msvc@4.41.1':
|
'@rollup/rollup-win32-ia32-msvc@4.42.0':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@rollup/rollup-win32-x64-msvc@4.41.1':
|
'@rollup/rollup-win32-x64-msvc@4.42.0':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@rtsao/scc@1.1.0': {}
|
'@rtsao/scc@1.1.0': {}
|
||||||
@@ -4936,6 +4980,19 @@ snapshots:
|
|||||||
|
|
||||||
agent-base@7.1.3: {}
|
agent-base@7.1.3: {}
|
||||||
|
|
||||||
|
ahooks@3.8.5(react@19.1.0):
|
||||||
|
dependencies:
|
||||||
|
'@babel/runtime': 7.27.6
|
||||||
|
dayjs: 1.11.13
|
||||||
|
intersection-observer: 0.12.2
|
||||||
|
js-cookie: 3.0.5
|
||||||
|
lodash: 4.17.21
|
||||||
|
react: 19.1.0
|
||||||
|
react-fast-compare: 3.2.2
|
||||||
|
resize-observer-polyfill: 1.5.1
|
||||||
|
screenfull: 5.2.0
|
||||||
|
tslib: 2.8.1
|
||||||
|
|
||||||
ajv@6.12.6:
|
ajv@6.12.6:
|
||||||
dependencies:
|
dependencies:
|
||||||
fast-deep-equal: 3.1.3
|
fast-deep-equal: 3.1.3
|
||||||
@@ -5231,6 +5288,8 @@ snapshots:
|
|||||||
es-errors: 1.3.0
|
es-errors: 1.3.0
|
||||||
is-data-view: 1.0.2
|
is-data-view: 1.0.2
|
||||||
|
|
||||||
|
dayjs@1.11.13: {}
|
||||||
|
|
||||||
debug@3.2.7:
|
debug@3.2.7:
|
||||||
dependencies:
|
dependencies:
|
||||||
ms: 2.1.3
|
ms: 2.1.3
|
||||||
@@ -5840,6 +5899,8 @@ snapshots:
|
|||||||
hasown: 2.0.2
|
hasown: 2.0.2
|
||||||
side-channel: 1.1.0
|
side-channel: 1.1.0
|
||||||
|
|
||||||
|
intersection-observer@0.12.2: {}
|
||||||
|
|
||||||
into-stream@6.0.0:
|
into-stream@6.0.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
from2: 2.3.0
|
from2: 2.3.0
|
||||||
@@ -5993,6 +6054,8 @@ snapshots:
|
|||||||
|
|
||||||
jiti@2.4.2: {}
|
jiti@2.4.2: {}
|
||||||
|
|
||||||
|
js-cookie@3.0.5: {}
|
||||||
|
|
||||||
js-tokens@4.0.0: {}
|
js-tokens@4.0.0: {}
|
||||||
|
|
||||||
js-yaml@4.1.0:
|
js-yaml@4.1.0:
|
||||||
@@ -6120,6 +6183,8 @@ snapshots:
|
|||||||
|
|
||||||
lodash.merge@4.6.2: {}
|
lodash.merge@4.6.2: {}
|
||||||
|
|
||||||
|
lodash@4.17.21: {}
|
||||||
|
|
||||||
log-update@6.1.0:
|
log-update@6.1.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
ansi-escapes: 7.0.0
|
ansi-escapes: 7.0.0
|
||||||
@@ -6414,6 +6479,8 @@ snapshots:
|
|||||||
react: 19.1.0
|
react: 19.1.0
|
||||||
scheduler: 0.26.0
|
scheduler: 0.26.0
|
||||||
|
|
||||||
|
react-fast-compare@3.2.2: {}
|
||||||
|
|
||||||
react-icons@5.5.0(react@19.1.0):
|
react-icons@5.5.0(react@19.1.0):
|
||||||
dependencies:
|
dependencies:
|
||||||
react: 19.1.0
|
react: 19.1.0
|
||||||
@@ -6493,6 +6560,8 @@ snapshots:
|
|||||||
|
|
||||||
require-directory@2.1.1: {}
|
require-directory@2.1.1: {}
|
||||||
|
|
||||||
|
resize-observer-polyfill@1.5.1: {}
|
||||||
|
|
||||||
resolve-from@4.0.0: {}
|
resolve-from@4.0.0: {}
|
||||||
|
|
||||||
resolve-pkg-maps@1.0.0: {}
|
resolve-pkg-maps@1.0.0: {}
|
||||||
@@ -6518,30 +6587,30 @@ snapshots:
|
|||||||
|
|
||||||
rfdc@1.4.1: {}
|
rfdc@1.4.1: {}
|
||||||
|
|
||||||
rollup@4.41.1:
|
rollup@4.42.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/estree': 1.0.7
|
'@types/estree': 1.0.7
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@rollup/rollup-android-arm-eabi': 4.41.1
|
'@rollup/rollup-android-arm-eabi': 4.42.0
|
||||||
'@rollup/rollup-android-arm64': 4.41.1
|
'@rollup/rollup-android-arm64': 4.42.0
|
||||||
'@rollup/rollup-darwin-arm64': 4.41.1
|
'@rollup/rollup-darwin-arm64': 4.42.0
|
||||||
'@rollup/rollup-darwin-x64': 4.41.1
|
'@rollup/rollup-darwin-x64': 4.42.0
|
||||||
'@rollup/rollup-freebsd-arm64': 4.41.1
|
'@rollup/rollup-freebsd-arm64': 4.42.0
|
||||||
'@rollup/rollup-freebsd-x64': 4.41.1
|
'@rollup/rollup-freebsd-x64': 4.42.0
|
||||||
'@rollup/rollup-linux-arm-gnueabihf': 4.41.1
|
'@rollup/rollup-linux-arm-gnueabihf': 4.42.0
|
||||||
'@rollup/rollup-linux-arm-musleabihf': 4.41.1
|
'@rollup/rollup-linux-arm-musleabihf': 4.42.0
|
||||||
'@rollup/rollup-linux-arm64-gnu': 4.41.1
|
'@rollup/rollup-linux-arm64-gnu': 4.42.0
|
||||||
'@rollup/rollup-linux-arm64-musl': 4.41.1
|
'@rollup/rollup-linux-arm64-musl': 4.42.0
|
||||||
'@rollup/rollup-linux-loongarch64-gnu': 4.41.1
|
'@rollup/rollup-linux-loongarch64-gnu': 4.42.0
|
||||||
'@rollup/rollup-linux-powerpc64le-gnu': 4.41.1
|
'@rollup/rollup-linux-powerpc64le-gnu': 4.42.0
|
||||||
'@rollup/rollup-linux-riscv64-gnu': 4.41.1
|
'@rollup/rollup-linux-riscv64-gnu': 4.42.0
|
||||||
'@rollup/rollup-linux-riscv64-musl': 4.41.1
|
'@rollup/rollup-linux-riscv64-musl': 4.42.0
|
||||||
'@rollup/rollup-linux-s390x-gnu': 4.41.1
|
'@rollup/rollup-linux-s390x-gnu': 4.42.0
|
||||||
'@rollup/rollup-linux-x64-gnu': 4.41.1
|
'@rollup/rollup-linux-x64-gnu': 4.42.0
|
||||||
'@rollup/rollup-linux-x64-musl': 4.41.1
|
'@rollup/rollup-linux-x64-musl': 4.42.0
|
||||||
'@rollup/rollup-win32-arm64-msvc': 4.41.1
|
'@rollup/rollup-win32-arm64-msvc': 4.42.0
|
||||||
'@rollup/rollup-win32-ia32-msvc': 4.41.1
|
'@rollup/rollup-win32-ia32-msvc': 4.42.0
|
||||||
'@rollup/rollup-win32-x64-msvc': 4.41.1
|
'@rollup/rollup-win32-x64-msvc': 4.42.0
|
||||||
fsevents: 2.3.3
|
fsevents: 2.3.3
|
||||||
|
|
||||||
run-parallel@1.2.0:
|
run-parallel@1.2.0:
|
||||||
@@ -6573,6 +6642,8 @@ snapshots:
|
|||||||
|
|
||||||
scheduler@0.26.0: {}
|
scheduler@0.26.0: {}
|
||||||
|
|
||||||
|
screenfull@5.2.0: {}
|
||||||
|
|
||||||
semver@6.3.1: {}
|
semver@6.3.1: {}
|
||||||
|
|
||||||
semver@7.7.2: {}
|
semver@7.7.2: {}
|
||||||
@@ -6861,6 +6932,10 @@ snapshots:
|
|||||||
mkdirp: 3.0.1
|
mkdirp: 3.0.1
|
||||||
yallist: 5.0.0
|
yallist: 5.0.0
|
||||||
|
|
||||||
|
tauri-plugin-macos-permissions-api@2.3.0:
|
||||||
|
dependencies:
|
||||||
|
'@tauri-apps/api': 2.5.0
|
||||||
|
|
||||||
tinyglobby@0.2.14:
|
tinyglobby@0.2.14:
|
||||||
dependencies:
|
dependencies:
|
||||||
fdir: 6.4.5(picomatch@4.0.2)
|
fdir: 6.4.5(picomatch@4.0.2)
|
||||||
@@ -7036,7 +7111,7 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
esbuild: 0.25.5
|
esbuild: 0.25.5
|
||||||
postcss: 8.5.4
|
postcss: 8.5.4
|
||||||
rollup: 4.41.1
|
rollup: 4.42.0
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@types/node': 22.15.30
|
'@types/node': 22.15.30
|
||||||
fsevents: 2.3.3
|
fsevents: 2.3.3
|
||||||
|
|||||||
Generated
+55
-27
@@ -405,9 +405,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bumpalo"
|
name = "bumpalo"
|
||||||
version = "3.18.0"
|
version = "3.18.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c1b094a32014c3d1f3944e4808e0e7c70e97dae0660886a8eb6dbc52d745badc"
|
checksum = "793db76d6187cd04dff33004d8e6c9cc4e05cd330500379d2394209271b4aeee"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bytemuck"
|
name = "bytemuck"
|
||||||
@@ -518,9 +518,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cc"
|
name = "cc"
|
||||||
version = "1.2.25"
|
version = "1.2.26"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d0fc897dc1e865cc67c0e05a836d9d3f1df3cbe442aa4a9473b18e12624a4951"
|
checksum = "956a5e21988b87f372569b66183b78babf23ebc2e744b733e4350a752c4dafac"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"jobserver",
|
"jobserver",
|
||||||
"libc",
|
"libc",
|
||||||
@@ -993,7 +993,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "donutbrowser"
|
name = "donutbrowser"
|
||||||
version = "0.3.2"
|
version = "0.3.3"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-trait",
|
"async-trait",
|
||||||
"base64 0.22.1",
|
"base64 0.22.1",
|
||||||
@@ -1012,11 +1012,14 @@ dependencies = [
|
|||||||
"tauri-plugin-deep-link",
|
"tauri-plugin-deep-link",
|
||||||
"tauri-plugin-dialog",
|
"tauri-plugin-dialog",
|
||||||
"tauri-plugin-fs",
|
"tauri-plugin-fs",
|
||||||
|
"tauri-plugin-macos-permissions",
|
||||||
"tauri-plugin-opener",
|
"tauri-plugin-opener",
|
||||||
"tauri-plugin-shell",
|
"tauri-plugin-shell",
|
||||||
"tempfile",
|
"tempfile",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tokio-test",
|
"tokio-test",
|
||||||
|
"windows",
|
||||||
|
"winreg",
|
||||||
"wiremock",
|
"wiremock",
|
||||||
"zip",
|
"zip",
|
||||||
]
|
]
|
||||||
@@ -1187,9 +1190,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "flate2"
|
name = "flate2"
|
||||||
version = "1.1.1"
|
version = "1.1.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7ced92e76e966ca2fd84c8f7aa01a4aea65b0eb6648d72f7c8f3e2764a67fece"
|
checksum = "4a3d7db9596fecd151c5f638c0ee5d5bd487b6e0ea232e5dc96d5250f6f94b1d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"crc32fast",
|
"crc32fast",
|
||||||
"libz-rs-sys",
|
"libz-rs-sys",
|
||||||
@@ -1826,9 +1829,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hyper-rustls"
|
name = "hyper-rustls"
|
||||||
version = "0.27.6"
|
version = "0.27.7"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "03a01595e11bdcec50946522c32dde3fc6914743000a68b93000965f2f02406d"
|
checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"http",
|
"http",
|
||||||
"hyper",
|
"hyper",
|
||||||
@@ -2284,9 +2287,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "liblzma-sys"
|
name = "liblzma-sys"
|
||||||
version = "0.4.3"
|
version = "0.4.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "5839bad90c3cc2e0b8c4ed8296b80e86040240f81d46b9c0e9bc8dd51ddd3af1"
|
checksum = "01b9596486f6d60c3bbe644c0e1be1aa6ccc472ad630fe8927b456973d7cb736"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cc",
|
"cc",
|
||||||
"libc",
|
"libc",
|
||||||
@@ -2305,9 +2308,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "libz-rs-sys"
|
name = "libz-rs-sys"
|
||||||
version = "0.5.0"
|
version = "0.5.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6489ca9bd760fe9642d7644e827b0c9add07df89857b0416ee15c1cc1a3b8c5a"
|
checksum = "172a788537a2221661b480fee8dc5f96c580eb34fa88764d3205dc356c7e4221"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"zlib-rs",
|
"zlib-rs",
|
||||||
]
|
]
|
||||||
@@ -2346,6 +2349,16 @@ version = "0.1.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4"
|
checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "macos-accessibility-client"
|
||||||
|
version = "0.0.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "edf7710fbff50c24124331760978fb9086d6de6288dcdb38b25a97f8b1bdebbb"
|
||||||
|
dependencies = [
|
||||||
|
"core-foundation 0.9.4",
|
||||||
|
"core-foundation-sys",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "markup5ever"
|
name = "markup5ever"
|
||||||
version = "0.11.0"
|
version = "0.11.0"
|
||||||
@@ -3791,9 +3804,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde_spanned"
|
name = "serde_spanned"
|
||||||
version = "0.6.8"
|
version = "0.6.9"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1"
|
checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
@@ -3948,9 +3961,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "smallvec"
|
name = "smallvec"
|
||||||
version = "1.15.0"
|
version = "1.15.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8917285742e9f3e1683f0a9c4e6b57960b7314d0b08d30d1ecd426713ee2eee9"
|
checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "socket2"
|
name = "socket2"
|
||||||
@@ -4407,6 +4420,21 @@ dependencies = [
|
|||||||
"url",
|
"url",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tauri-plugin-macos-permissions"
|
||||||
|
version = "2.3.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5607e0707d37d7b20e287cf0ce396d1efebe7b833b8e9cbd2ea4257091d9c604"
|
||||||
|
dependencies = [
|
||||||
|
"macos-accessibility-client",
|
||||||
|
"objc2 0.6.1",
|
||||||
|
"objc2-foundation 0.3.1",
|
||||||
|
"serde",
|
||||||
|
"tauri",
|
||||||
|
"tauri-plugin",
|
||||||
|
"thiserror 2.0.12",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tauri-plugin-opener"
|
name = "tauri-plugin-opener"
|
||||||
version = "2.2.7"
|
version = "2.2.7"
|
||||||
@@ -4769,9 +4797,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "toml_datetime"
|
name = "toml_datetime"
|
||||||
version = "0.6.9"
|
version = "0.6.11"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "3da5db5a963e24bc68be8b17b6fa82814bb22ee8660f192bb182771d498f09a3"
|
checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
@@ -4814,9 +4842,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "toml_write"
|
name = "toml_write"
|
||||||
version = "0.1.1"
|
version = "0.1.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "bfb942dfe1d8e29a7ee7fcbde5bd2b9a25fb89aa70caea2eba3bee836ff41076"
|
checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tower"
|
name = "tower"
|
||||||
@@ -4876,9 +4904,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tracing-attributes"
|
name = "tracing-attributes"
|
||||||
version = "0.1.28"
|
version = "0.1.29"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d"
|
checksum = "1b1ffbcf9c6f6b99d386e7444eb608ba646ae452a36b39737deb9663b610f662"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
@@ -4887,9 +4915,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tracing-core"
|
name = "tracing-core"
|
||||||
version = "0.1.33"
|
version = "0.1.34"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c"
|
checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"once_cell",
|
"once_cell",
|
||||||
]
|
]
|
||||||
@@ -5974,9 +6002,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zlib-rs"
|
name = "zlib-rs"
|
||||||
version = "0.5.0"
|
version = "0.5.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "868b928d7949e09af2f6086dfc1e01936064cc7a819253bce650d4e2a2d63ba8"
|
checksum = "626bd9fa9734751fc50d6060752170984d7053f5a39061f524cda68023d4db8a"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zopfli"
|
name = "zopfli"
|
||||||
|
|||||||
+16
-1
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "donutbrowser"
|
name = "donutbrowser"
|
||||||
version = "0.3.2"
|
version = "0.3.3"
|
||||||
description = "Simple Yet Powerful Browser Orchestrator"
|
description = "Simple Yet Powerful Browser Orchestrator"
|
||||||
authors = ["zhom@github"]
|
authors = ["zhom@github"]
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
@@ -27,6 +27,7 @@ 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"
|
tauri-plugin-dialog = "2"
|
||||||
|
tauri-plugin-macos-permissions = "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"] }
|
||||||
@@ -42,6 +43,20 @@ core-foundation="0.10"
|
|||||||
objc2 = "0.6.1"
|
objc2 = "0.6.1"
|
||||||
objc2-app-kit = { version = "0.3.1", features = ["NSWindow"] }
|
objc2-app-kit = { version = "0.3.1", features = ["NSWindow"] }
|
||||||
|
|
||||||
|
[target.'cfg(target_os = "windows")'.dependencies]
|
||||||
|
winreg = "0.55"
|
||||||
|
windows = { version = "0.61", features = [
|
||||||
|
"Win32_Foundation",
|
||||||
|
"Win32_System_ProcessStatus",
|
||||||
|
"Win32_System_Threading",
|
||||||
|
"Win32_System_Diagnostics_Debug",
|
||||||
|
"Win32_System_SystemInformation",
|
||||||
|
"Win32_Security",
|
||||||
|
"Win32_Storage_FileSystem",
|
||||||
|
"Win32_System_Registry",
|
||||||
|
"Win32_UI_Shell",
|
||||||
|
] }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
tempfile = "3.13.0"
|
tempfile = "3.13.0"
|
||||||
tokio-test = "0.4.4"
|
tokio-test = "0.4.4"
|
||||||
|
|||||||
+6
-26
@@ -2,47 +2,27 @@
|
|||||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
<plist version="1.0">
|
<plist version="1.0">
|
||||||
<dict>
|
<dict>
|
||||||
|
<key>NSCameraUsageDescription</key>
|
||||||
|
<string>Donut Browser needs camera access to enable camera functionality in web browsers. Each website will still ask for your permission individually.</string>
|
||||||
|
<key>NSMicrophoneUsageDescription</key>
|
||||||
|
<string>Donut Browser needs microphone access to enable microphone functionality in web browsers. Each website will still ask for your permission individually.</string>
|
||||||
<key>CFBundleDisplayName</key>
|
<key>CFBundleDisplayName</key>
|
||||||
<string>Donut Browser</string>
|
<string>Donut Browser</string>
|
||||||
<key>CFBundleName</key>
|
<key>CFBundleName</key>
|
||||||
<string>Donut Browser</string>
|
<string>Donut Browser</string>
|
||||||
<key>CFBundleIdentifier</key>
|
<key>CFBundleIdentifier</key>
|
||||||
<string>com.donutbrowser</string>
|
<string>com.donutbrowser</string>
|
||||||
|
<key>CFBundleURLName</key>
|
||||||
|
<string>com.donutbrowser</string>
|
||||||
<key>CFBundleExecutable</key>
|
<key>CFBundleExecutable</key>
|
||||||
<string>donutbrowser</string>
|
<string>donutbrowser</string>
|
||||||
<key>CFBundleVersion</key>
|
<key>CFBundleVersion</key>
|
||||||
<string>1</string>
|
<string>1</string>
|
||||||
<key>CFBundleShortVersionString</key>
|
|
||||||
<string>0.3.2</string>
|
|
||||||
<key>CFBundlePackageType</key>
|
|
||||||
<string>APPL</string>
|
|
||||||
<key>CFBundleIconFile</key>
|
<key>CFBundleIconFile</key>
|
||||||
<string>icon.icns</string>
|
<string>icon.icns</string>
|
||||||
<key>CFBundleSignature</key>
|
|
||||||
<string>????</string>
|
|
||||||
<key>CFBundleIconFile</key>
|
|
||||||
<string>icon.icns</string>
|
|
||||||
<key>CFBundleURLTypes</key>
|
|
||||||
<array>
|
|
||||||
<dict>
|
|
||||||
<key>CFBundleURLName</key>
|
|
||||||
<string>Web Browser</string>
|
|
||||||
<key>CFBundleURLSchemes</key>
|
|
||||||
<array>
|
|
||||||
<string>http</string>
|
|
||||||
<string>https</string>
|
|
||||||
</array>
|
|
||||||
<key>CFBundleURLIconFile</key>
|
|
||||||
<string>icon.icns</string>
|
|
||||||
<key>LSHandlerRank</key>
|
|
||||||
<string>Owner</string>
|
|
||||||
</dict>
|
|
||||||
</array>
|
|
||||||
<key>LSApplicationCategoryType</key>
|
<key>LSApplicationCategoryType</key>
|
||||||
<string>public.app-category.productivity</string>
|
<string>public.app-category.productivity</string>
|
||||||
<key>NSHumanReadableCopyright</key>
|
<key>NSHumanReadableCopyright</key>
|
||||||
<string>Copyright © 2025 Donut Browser</string>
|
<string>Copyright © 2025 Donut Browser</string>
|
||||||
<key>LSMinimumSystemVersion</key>
|
|
||||||
<string>10.13</string>
|
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
@@ -20,6 +20,11 @@
|
|||||||
"shell:allow-stdin-write",
|
"shell:allow-stdin-write",
|
||||||
"deep-link:default",
|
"deep-link:default",
|
||||||
"dialog:default",
|
"dialog:default",
|
||||||
"dialog:allow-open"
|
"dialog:allow-open",
|
||||||
|
"macos-permissions:default",
|
||||||
|
"macos-permissions:allow-request-microphone-permission",
|
||||||
|
"macos-permissions:allow-request-camera-permission",
|
||||||
|
"macos-permissions:allow-check-microphone-permission",
|
||||||
|
"macos-permissions:allow-check-camera-permission"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,5 +12,21 @@
|
|||||||
<true/>
|
<true/>
|
||||||
<key>com.apple.security.files.downloads.read-write</key>
|
<key>com.apple.security.files.downloads.read-write</key>
|
||||||
<true/>
|
<true/>
|
||||||
|
<key>com.apple.security.device.camera</key>
|
||||||
|
<true/>
|
||||||
|
<key>com.apple.security.device.audio-output</key>
|
||||||
|
<true/>
|
||||||
|
<key>com.apple.security.device.microphone</key>
|
||||||
|
<true/>
|
||||||
|
<key>com.apple.security.device.audio-input</key>
|
||||||
|
<true/>
|
||||||
|
<key>com.apple.security.cs.allow-jit</key>
|
||||||
|
<true/>
|
||||||
|
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
|
||||||
|
<true/>
|
||||||
|
<key>com.apple.security.cs.allow-dyld-environment-variables</key>
|
||||||
|
<true/>
|
||||||
|
<key>com.apple.security.cs.disable-library-validation</key>
|
||||||
|
<true/>
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
@@ -398,6 +398,30 @@ impl AppAutoUpdater {
|
|||||||
Err("DMG extraction is only supported on macOS".into())
|
Err("DMG extraction is only supported on macOS".into())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
"msi" => {
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
{
|
||||||
|
// For MSI files on Windows, we need to run the installer
|
||||||
|
// MSI files can't be extracted like archives, they need to be executed
|
||||||
|
// Return the path to the MSI file itself for installation
|
||||||
|
Ok(archive_path.to_path_buf())
|
||||||
|
}
|
||||||
|
#[cfg(not(target_os = "windows"))]
|
||||||
|
{
|
||||||
|
Err("MSI installation is only supported on Windows".into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"exe" => {
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
{
|
||||||
|
// For exe installers on Windows, return the path for execution
|
||||||
|
Ok(archive_path.to_path_buf())
|
||||||
|
}
|
||||||
|
#[cfg(not(target_os = "windows"))]
|
||||||
|
{
|
||||||
|
Err("EXE installation is only supported on Windows".into())
|
||||||
|
}
|
||||||
|
}
|
||||||
"zip" => extractor.extract_zip(archive_path, dest_dir).await,
|
"zip" => extractor.extract_zip(archive_path, dest_dir).await,
|
||||||
_ => Err(format!("Unsupported archive format: {extension}").into()),
|
_ => Err(format!("Unsupported archive format: {extension}").into()),
|
||||||
}
|
}
|
||||||
@@ -406,71 +430,282 @@ impl AppAutoUpdater {
|
|||||||
/// Install the update by replacing the current app
|
/// Install the update by replacing the current app
|
||||||
async fn install_update(
|
async fn install_update(
|
||||||
&self,
|
&self,
|
||||||
new_app_path: &Path,
|
installer_path: &Path,
|
||||||
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||||
// Get the current application bundle path
|
#[cfg(target_os = "macos")]
|
||||||
let current_app_path = self.get_current_app_path()?;
|
{
|
||||||
|
// Get the current application bundle path
|
||||||
|
let current_app_path = self.get_current_app_path()?;
|
||||||
|
|
||||||
// Create a backup of the current app
|
// Create a backup of the current app
|
||||||
let backup_path = current_app_path.with_extension("app.backup");
|
let backup_path = current_app_path.with_extension("app.backup");
|
||||||
if backup_path.exists() {
|
if backup_path.exists() {
|
||||||
fs::remove_dir_all(&backup_path)?;
|
fs::remove_dir_all(&backup_path)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Move current app to backup
|
||||||
|
fs::rename(¤t_app_path, &backup_path)?;
|
||||||
|
|
||||||
|
// Move new app to current location
|
||||||
|
fs::rename(installer_path, ¤t_app_path)?;
|
||||||
|
|
||||||
|
// Remove quarantine attributes from the new app
|
||||||
|
let _ = Command::new("xattr")
|
||||||
|
.args([
|
||||||
|
"-dr",
|
||||||
|
"com.apple.quarantine",
|
||||||
|
current_app_path.to_str().unwrap(),
|
||||||
|
])
|
||||||
|
.output();
|
||||||
|
|
||||||
|
let _ = Command::new("xattr")
|
||||||
|
.args(["-cr", current_app_path.to_str().unwrap()])
|
||||||
|
.output();
|
||||||
|
|
||||||
|
// Clean up backup after successful installation
|
||||||
|
let _ = fs::remove_dir_all(&backup_path);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
// Move current app to backup
|
#[cfg(target_os = "windows")]
|
||||||
fs::rename(¤t_app_path, &backup_path)?;
|
{
|
||||||
|
let extension = installer_path
|
||||||
|
.extension()
|
||||||
|
.and_then(|ext| ext.to_str())
|
||||||
|
.unwrap_or("");
|
||||||
|
|
||||||
// Move new app to current location
|
println!("Installing Windows update with extension: {extension}");
|
||||||
fs::rename(new_app_path, ¤t_app_path)?;
|
|
||||||
|
|
||||||
// Remove quarantine attributes from the new app
|
match extension {
|
||||||
let _ = Command::new("xattr")
|
"msi" => {
|
||||||
.args([
|
// Install MSI silently with enhanced error handling
|
||||||
"-dr",
|
println!("Running MSI installer: {}", installer_path.display());
|
||||||
"com.apple.quarantine",
|
|
||||||
current_app_path.to_str().unwrap(),
|
|
||||||
])
|
|
||||||
.output();
|
|
||||||
|
|
||||||
let _ = Command::new("xattr")
|
let mut cmd = Command::new("msiexec");
|
||||||
.args(["-cr", current_app_path.to_str().unwrap()])
|
cmd.args([
|
||||||
.output();
|
"/i",
|
||||||
|
installer_path.to_str().unwrap(),
|
||||||
|
"/quiet",
|
||||||
|
"/norestart",
|
||||||
|
"REBOOT=ReallySuppress",
|
||||||
|
"/l*v", // Enable verbose logging
|
||||||
|
&format!("{}.log", installer_path.to_str().unwrap()),
|
||||||
|
]);
|
||||||
|
|
||||||
// Clean up backup after successful installation
|
let output = cmd.output()?;
|
||||||
let _ = fs::remove_dir_all(&backup_path);
|
|
||||||
|
|
||||||
Ok(())
|
if !output.status.success() {
|
||||||
|
let error_msg = String::from_utf8_lossy(&output.stderr);
|
||||||
|
let exit_code = output.status.code().unwrap_or(-1);
|
||||||
|
|
||||||
|
// Try to read the log file for more details
|
||||||
|
let log_path = format!("{}.log", installer_path.to_str().unwrap());
|
||||||
|
let log_content = fs::read_to_string(&log_path).unwrap_or_default();
|
||||||
|
|
||||||
|
println!("MSI installation failed with exit code: {exit_code}");
|
||||||
|
println!("Error output: {error_msg}");
|
||||||
|
if !log_content.is_empty() {
|
||||||
|
println!(
|
||||||
|
"Log file content (last 500 chars): {}",
|
||||||
|
&log_content
|
||||||
|
.chars()
|
||||||
|
.rev()
|
||||||
|
.take(500)
|
||||||
|
.collect::<String>()
|
||||||
|
.chars()
|
||||||
|
.rev()
|
||||||
|
.collect::<String>()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Err(
|
||||||
|
format!("MSI installation failed (exit code {exit_code}): {error_msg}").into(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
println!("MSI installation completed successfully");
|
||||||
|
}
|
||||||
|
"exe" => {
|
||||||
|
// Run exe installer silently with multiple fallback options
|
||||||
|
println!("Running EXE installer: {}", installer_path.display());
|
||||||
|
|
||||||
|
// Try NSIS silent flag first (most common for Tauri)
|
||||||
|
let mut success = false;
|
||||||
|
let mut last_error = String::new();
|
||||||
|
|
||||||
|
// NSIS installer flags (used by Tauri)
|
||||||
|
let nsis_args = vec![
|
||||||
|
vec!["/S"], // Standard NSIS silent flag
|
||||||
|
vec!["/VERYSILENT", "/SUPPRESSMSGBOXES", "/NORESTART"], // Inno Setup flags
|
||||||
|
vec!["/quiet"], // Generic quiet flag
|
||||||
|
vec!["/silent"], // Alternative silent flag
|
||||||
|
];
|
||||||
|
|
||||||
|
for args in nsis_args {
|
||||||
|
println!("Trying installer with args: {:?}", args);
|
||||||
|
let output = Command::new(installer_path).args(&args).output();
|
||||||
|
|
||||||
|
match output {
|
||||||
|
Ok(output) if output.status.success() => {
|
||||||
|
println!(
|
||||||
|
"EXE installation completed successfully with args: {:?}",
|
||||||
|
args
|
||||||
|
);
|
||||||
|
success = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
Ok(output) => {
|
||||||
|
let error_msg = String::from_utf8_lossy(&output.stderr);
|
||||||
|
last_error = format!(
|
||||||
|
"Exit code {}: {}",
|
||||||
|
output.status.code().unwrap_or(-1),
|
||||||
|
error_msg
|
||||||
|
);
|
||||||
|
println!("Installer failed with args {:?}: {}", args, last_error);
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
last_error = format!("Failed to execute installer: {e}");
|
||||||
|
println!(
|
||||||
|
"Failed to execute installer with args {:?}: {}",
|
||||||
|
args, last_error
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !success {
|
||||||
|
return Err(
|
||||||
|
format!(
|
||||||
|
"EXE installation failed after trying multiple methods. Last error: {last_error}"
|
||||||
|
)
|
||||||
|
.into(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"zip" => {
|
||||||
|
// Handle ZIP files by extracting and replacing the current executable
|
||||||
|
println!("Handling ZIP update: {}", installer_path.display());
|
||||||
|
|
||||||
|
let temp_extract_dir = installer_path.parent().unwrap().join("extracted");
|
||||||
|
fs::create_dir_all(&temp_extract_dir)?;
|
||||||
|
|
||||||
|
// Extract ZIP file
|
||||||
|
let extractor = crate::extraction::Extractor::new();
|
||||||
|
let extracted_path = extractor
|
||||||
|
.extract_zip(installer_path, &temp_extract_dir)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
// Find the executable in the extracted files
|
||||||
|
let current_exe = self.get_current_app_path()?;
|
||||||
|
let current_exe_name = current_exe.file_name().unwrap();
|
||||||
|
|
||||||
|
// Look for the new executable
|
||||||
|
let new_exe_path =
|
||||||
|
if extracted_path.is_file() && extracted_path.file_name() == Some(current_exe_name) {
|
||||||
|
extracted_path
|
||||||
|
} else {
|
||||||
|
// Search in extracted directory
|
||||||
|
let mut found_exe = None;
|
||||||
|
if let Ok(entries) = fs::read_dir(&extracted_path) {
|
||||||
|
for entry in entries.flatten() {
|
||||||
|
let path = entry.path();
|
||||||
|
if path.file_name() == Some(current_exe_name) {
|
||||||
|
found_exe = Some(path);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
found_exe.ok_or("Could not find executable in ZIP file")?
|
||||||
|
};
|
||||||
|
|
||||||
|
// Create backup of current executable
|
||||||
|
let backup_path = current_exe.with_extension("exe.backup");
|
||||||
|
if backup_path.exists() {
|
||||||
|
fs::remove_file(&backup_path)?;
|
||||||
|
}
|
||||||
|
fs::copy(¤t_exe, &backup_path)?;
|
||||||
|
|
||||||
|
// Replace current executable
|
||||||
|
fs::copy(&new_exe_path, ¤t_exe)?;
|
||||||
|
|
||||||
|
// Clean up
|
||||||
|
let _ = fs::remove_dir_all(&temp_extract_dir);
|
||||||
|
|
||||||
|
println!("ZIP update completed successfully");
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
return Err(format!("Unsupported installer format: {extension}").into());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
{
|
||||||
|
// For Linux, we would handle different package formats here
|
||||||
|
// This implementation would depend on the specific package type
|
||||||
|
Err("Linux auto-update installation not yet implemented".into())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(any(target_os = "macos", target_os = "windows", target_os = "linux")))]
|
||||||
|
{
|
||||||
|
Err("Auto-update installation not supported on this platform".into())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the current application bundle path
|
/// Get the current application bundle path
|
||||||
fn get_current_app_path(&self) -> Result<PathBuf, Box<dyn std::error::Error + Send + Sync>> {
|
fn get_current_app_path(&self) -> Result<PathBuf, Box<dyn std::error::Error + Send + Sync>> {
|
||||||
// Get the current executable path
|
#[cfg(target_os = "macos")]
|
||||||
let exe_path = std::env::current_exe()?;
|
{
|
||||||
|
// Get the current executable path
|
||||||
|
let exe_path = std::env::current_exe()?;
|
||||||
|
|
||||||
// Navigate up to find the .app bundle
|
// Navigate up to find the .app bundle
|
||||||
let mut current = exe_path.as_path();
|
let mut current = exe_path.as_path();
|
||||||
while let Some(parent) = current.parent() {
|
while let Some(parent) = current.parent() {
|
||||||
if parent.extension().is_some_and(|ext| ext == "app") {
|
if parent.extension().is_some_and(|ext| ext == "app") {
|
||||||
return Ok(parent.to_path_buf());
|
return Ok(parent.to_path_buf());
|
||||||
|
}
|
||||||
|
current = parent;
|
||||||
}
|
}
|
||||||
current = parent;
|
|
||||||
|
Err("Could not find application bundle".into())
|
||||||
}
|
}
|
||||||
|
|
||||||
Err("Could not find application bundle".into())
|
#[cfg(target_os = "windows")]
|
||||||
|
{
|
||||||
|
// On Windows, just return the current executable path
|
||||||
|
std::env::current_exe().map_err(|e| e.into())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
{
|
||||||
|
// On Linux, return the current executable path
|
||||||
|
std::env::current_exe().map_err(|e| e.into())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(any(target_os = "macos", target_os = "windows", target_os = "linux")))]
|
||||||
|
{
|
||||||
|
Err("Platform not supported".into())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Restart the application
|
/// Restart the application
|
||||||
async fn restart_application(&self) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
async fn restart_application(&self) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||||
let app_path = self.get_current_app_path()?;
|
#[cfg(target_os = "macos")]
|
||||||
let current_pid = std::process::id();
|
{
|
||||||
|
let app_path = self.get_current_app_path()?;
|
||||||
|
let current_pid = std::process::id();
|
||||||
|
|
||||||
// Create a temporary restart script
|
// Create a temporary restart script
|
||||||
let temp_dir = std::env::temp_dir();
|
let temp_dir = std::env::temp_dir();
|
||||||
let script_path = temp_dir.join("donut_restart.sh");
|
let script_path = temp_dir.join("donut_restart.sh");
|
||||||
|
|
||||||
// Create the restart script content
|
// Create the restart script content
|
||||||
let script_content = format!(
|
let script_content = format!(
|
||||||
r#"#!/bin/bash
|
r#"#!/bin/bash
|
||||||
# Wait for the current process to exit
|
# Wait for the current process to exit
|
||||||
while kill -0 {} 2>/dev/null; do
|
while kill -0 {} 2>/dev/null; do
|
||||||
sleep 0.5
|
sleep 0.5
|
||||||
@@ -485,37 +720,146 @@ open "{}"
|
|||||||
# Clean up this script
|
# Clean up this script
|
||||||
rm "{}"
|
rm "{}"
|
||||||
"#,
|
"#,
|
||||||
current_pid,
|
current_pid,
|
||||||
app_path.to_str().unwrap(),
|
app_path.to_str().unwrap(),
|
||||||
script_path.to_str().unwrap()
|
script_path.to_str().unwrap()
|
||||||
);
|
);
|
||||||
|
|
||||||
// Write the script to file
|
// Write the script to file
|
||||||
fs::write(&script_path, script_content)?;
|
fs::write(&script_path, script_content)?;
|
||||||
|
|
||||||
// Make the script executable
|
// Make the script executable
|
||||||
let _ = Command::new("chmod")
|
let _ = Command::new("chmod")
|
||||||
.args(["+x", script_path.to_str().unwrap()])
|
.args(["+x", script_path.to_str().unwrap()])
|
||||||
.output();
|
.output();
|
||||||
|
|
||||||
// Execute the restart script in the background
|
// Execute the restart script in the background
|
||||||
let mut cmd = Command::new("bash");
|
let mut cmd = Command::new("bash");
|
||||||
cmd.arg(script_path.to_str().unwrap());
|
cmd.arg(script_path.to_str().unwrap());
|
||||||
|
|
||||||
// Detach the process completely
|
// Detach the process completely
|
||||||
#[cfg(unix)]
|
|
||||||
{
|
|
||||||
use std::os::unix::process::CommandExt;
|
use std::os::unix::process::CommandExt;
|
||||||
cmd.process_group(0);
|
cmd.process_group(0);
|
||||||
|
|
||||||
|
let _child = cmd.spawn()?;
|
||||||
|
|
||||||
|
// Give the script a moment to start
|
||||||
|
tokio::time::sleep(tokio::time::Duration::from_millis(500)).await;
|
||||||
|
|
||||||
|
// Exit the current process
|
||||||
|
std::process::exit(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
let _child = cmd.spawn()?;
|
#[cfg(target_os = "windows")]
|
||||||
|
{
|
||||||
|
let app_path = self.get_current_app_path()?;
|
||||||
|
let current_pid = std::process::id();
|
||||||
|
|
||||||
// Give the script a moment to start
|
// Create a temporary restart batch script
|
||||||
tokio::time::sleep(tokio::time::Duration::from_millis(500)).await;
|
let temp_dir = std::env::temp_dir();
|
||||||
|
let script_path = temp_dir.join("donut_restart.bat");
|
||||||
|
|
||||||
// Exit the current process
|
// Create the restart script content
|
||||||
std::process::exit(0);
|
let script_content = format!(
|
||||||
|
r#"@echo off
|
||||||
|
rem Wait for the current process to exit
|
||||||
|
:wait_loop
|
||||||
|
tasklist /fi "PID eq {}" >nul 2>&1
|
||||||
|
if %errorlevel% equ 0 (
|
||||||
|
timeout /t 1 /nobreak >nul
|
||||||
|
goto wait_loop
|
||||||
|
)
|
||||||
|
|
||||||
|
rem Wait a bit more to ensure clean exit
|
||||||
|
timeout /t 2 /nobreak >nul
|
||||||
|
|
||||||
|
rem Start the new application
|
||||||
|
start "" "{}"
|
||||||
|
|
||||||
|
rem Clean up this script
|
||||||
|
del "%~f0"
|
||||||
|
"#,
|
||||||
|
current_pid,
|
||||||
|
app_path.to_str().unwrap()
|
||||||
|
);
|
||||||
|
|
||||||
|
// Write the script to file
|
||||||
|
fs::write(&script_path, script_content)?;
|
||||||
|
|
||||||
|
// Execute the restart script in the background
|
||||||
|
let mut cmd = Command::new("cmd");
|
||||||
|
cmd.args(["/C", script_path.to_str().unwrap()]);
|
||||||
|
|
||||||
|
// Start the process detached
|
||||||
|
let _child = cmd.spawn()?;
|
||||||
|
|
||||||
|
// Give the script a moment to start
|
||||||
|
tokio::time::sleep(tokio::time::Duration::from_millis(500)).await;
|
||||||
|
|
||||||
|
// Exit the current process
|
||||||
|
std::process::exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
{
|
||||||
|
let app_path = self.get_current_app_path()?;
|
||||||
|
let current_pid = std::process::id();
|
||||||
|
|
||||||
|
// Create a temporary restart script
|
||||||
|
let temp_dir = std::env::temp_dir();
|
||||||
|
let script_path = temp_dir.join("donut_restart.sh");
|
||||||
|
|
||||||
|
// Create the restart script content
|
||||||
|
let script_content = format!(
|
||||||
|
r#"#!/bin/bash
|
||||||
|
# Wait for the current process to exit
|
||||||
|
while kill -0 {} 2>/dev/null; do
|
||||||
|
sleep 0.5
|
||||||
|
done
|
||||||
|
|
||||||
|
# Wait a bit more to ensure clean exit
|
||||||
|
sleep 1
|
||||||
|
|
||||||
|
# Start the new application
|
||||||
|
"{}" &
|
||||||
|
|
||||||
|
# Clean up this script
|
||||||
|
rm "{}"
|
||||||
|
"#,
|
||||||
|
current_pid,
|
||||||
|
app_path.to_str().unwrap(),
|
||||||
|
script_path.to_str().unwrap()
|
||||||
|
);
|
||||||
|
|
||||||
|
// Write the script to file
|
||||||
|
fs::write(&script_path, script_content)?;
|
||||||
|
|
||||||
|
// Make the script executable
|
||||||
|
let _ = Command::new("chmod")
|
||||||
|
.args(["+x", script_path.to_str().unwrap()])
|
||||||
|
.output();
|
||||||
|
|
||||||
|
// Execute the restart script in the background
|
||||||
|
let mut cmd = Command::new("bash");
|
||||||
|
cmd.arg(script_path.to_str().unwrap());
|
||||||
|
|
||||||
|
// Detach the process completely
|
||||||
|
use std::os::unix::process::CommandExt;
|
||||||
|
cmd.process_group(0);
|
||||||
|
|
||||||
|
let _child = cmd.spawn()?;
|
||||||
|
|
||||||
|
// Give the script a moment to start
|
||||||
|
tokio::time::sleep(tokio::time::Duration::from_millis(500)).await;
|
||||||
|
|
||||||
|
// Exit the current process
|
||||||
|
std::process::exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(any(target_os = "macos", target_os = "windows", target_os = "linux")))]
|
||||||
|
{
|
||||||
|
Err("Application restart not supported on this platform".into())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+10
-24
@@ -216,17 +216,13 @@ mod linux {
|
|||||||
install_dir: &Path,
|
install_dir: &Path,
|
||||||
browser_type: &BrowserType,
|
browser_type: &BrowserType,
|
||||||
) -> Result<PathBuf, Box<dyn std::error::Error>> {
|
) -> Result<PathBuf, Box<dyn std::error::Error>> {
|
||||||
// Expected structure: install_dir/<browser>/<binary>
|
|
||||||
let browser_subdir = install_dir.join(browser_type.as_str());
|
|
||||||
|
|
||||||
let possible_executables = match browser_type {
|
let possible_executables = match browser_type {
|
||||||
BrowserType::Chromium => vec![
|
BrowserType::Chromium => vec![install_dir.join("chromium"), install_dir.join("chrome")],
|
||||||
browser_subdir.join("chromium"),
|
|
||||||
browser_subdir.join("chrome"),
|
|
||||||
],
|
|
||||||
BrowserType::Brave => vec![
|
BrowserType::Brave => vec![
|
||||||
browser_subdir.join("brave"),
|
install_dir.join("brave"),
|
||||||
browser_subdir.join("brave-browser"),
|
install_dir.join("brave-browser"),
|
||||||
|
install_dir.join("brave-browser-nightly"),
|
||||||
|
install_dir.join("brave-browser-beta"),
|
||||||
],
|
],
|
||||||
_ => vec![],
|
_ => vec![],
|
||||||
};
|
};
|
||||||
@@ -292,23 +288,13 @@ mod linux {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn is_chromium_version_downloaded(install_dir: &Path, browser_type: &BrowserType) -> bool {
|
pub fn is_chromium_version_downloaded(install_dir: &Path, browser_type: &BrowserType) -> bool {
|
||||||
// Expected structure: install_dir/<browser>/<binary>
|
|
||||||
let browser_subdir = install_dir.join(browser_type.as_str());
|
|
||||||
|
|
||||||
if !browser_subdir.exists() || !browser_subdir.is_dir() {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
let possible_executables = match browser_type {
|
let possible_executables = match browser_type {
|
||||||
BrowserType::Chromium => vec![
|
BrowserType::Chromium => vec![install_dir.join("chromium"), install_dir.join("chrome")],
|
||||||
browser_subdir.join("chromium"),
|
|
||||||
browser_subdir.join("chrome"),
|
|
||||||
],
|
|
||||||
BrowserType::Brave => vec![
|
BrowserType::Brave => vec![
|
||||||
browser_subdir.join("brave"),
|
install_dir.join("brave"),
|
||||||
browser_subdir.join("brave-browser"),
|
install_dir.join("brave-browser"),
|
||||||
browser_subdir.join("brave-browser-nightly"),
|
install_dir.join("brave-browser-nightly"),
|
||||||
browser_subdir.join("brave-browser-beta"),
|
install_dir.join("brave-browser-beta"),
|
||||||
],
|
],
|
||||||
_ => vec![],
|
_ => vec![],
|
||||||
};
|
};
|
||||||
|
|||||||
+226
-39
@@ -3,7 +3,6 @@ use directories::BaseDirs;
|
|||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::fs::{self, create_dir_all};
|
use std::fs::{self, create_dir_all};
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use std::process::Command;
|
|
||||||
use std::time::{SystemTime, UNIX_EPOCH};
|
use std::time::{SystemTime, UNIX_EPOCH};
|
||||||
use sysinfo::{Pid, System};
|
use sysinfo::{Pid, System};
|
||||||
use tauri::Emitter;
|
use tauri::Emitter;
|
||||||
@@ -35,6 +34,7 @@ pub struct BrowserProfile {
|
|||||||
mod macos {
|
mod macos {
|
||||||
use super::*;
|
use super::*;
|
||||||
use std::ffi::OsString;
|
use std::ffi::OsString;
|
||||||
|
use std::process::Command;
|
||||||
|
|
||||||
pub fn is_tor_or_mullvad_browser(exe_name: &str, cmd: &[OsString], browser_type: &str) -> bool {
|
pub fn is_tor_or_mullvad_browser(exe_name: &str, cmd: &[OsString], browser_type: &str) -> bool {
|
||||||
match browser_type {
|
match browser_type {
|
||||||
@@ -482,14 +482,42 @@ end try
|
|||||||
mod windows {
|
mod windows {
|
||||||
use super::*;
|
use super::*;
|
||||||
use std::ffi::OsString;
|
use std::ffi::OsString;
|
||||||
|
use std::process::Command;
|
||||||
|
|
||||||
pub fn is_tor_or_mullvad_browser(
|
pub fn is_tor_or_mullvad_browser(exe_name: &str, cmd: &[OsString], browser_type: &str) -> bool {
|
||||||
_exe_name: &str,
|
let exe_lower = exe_name.to_lowercase();
|
||||||
_cmd: &[OsString],
|
|
||||||
_browser_type: &str,
|
// Check for Firefox-based browsers first by executable name
|
||||||
) -> bool {
|
let is_firefox_family = exe_lower.contains("firefox") || exe_lower.contains(".exe");
|
||||||
// Windows implementation would go here
|
|
||||||
false
|
if !is_firefox_family {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check command arguments for profile paths and browser-specific indicators
|
||||||
|
let cmd_line = cmd
|
||||||
|
.iter()
|
||||||
|
.map(|s| s.to_string_lossy().to_lowercase())
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join(" ");
|
||||||
|
|
||||||
|
match browser_type {
|
||||||
|
"tor-browser" => {
|
||||||
|
// Check for TOR browser specific paths and arguments
|
||||||
|
cmd_line.contains("tor")
|
||||||
|
|| cmd_line.contains("browser\\torbrowser")
|
||||||
|
|| cmd_line.contains("tor-browser")
|
||||||
|
|| cmd_line.contains("profile") && (cmd_line.contains("tor") || cmd_line.contains("tbb"))
|
||||||
|
}
|
||||||
|
"mullvad-browser" => {
|
||||||
|
// Check for Mullvad browser specific paths and arguments
|
||||||
|
cmd_line.contains("mullvad")
|
||||||
|
|| cmd_line.contains("browser\\mullvadbrowser")
|
||||||
|
|| cmd_line.contains("mullvad-browser")
|
||||||
|
|| cmd_line.contains("profile") && cmd_line.contains("mullvad")
|
||||||
|
}
|
||||||
|
_ => false,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn launch_browser_process(
|
pub async fn launch_browser_process(
|
||||||
@@ -500,7 +528,48 @@ mod windows {
|
|||||||
"Launching browser on Windows: {:?} with args: {:?}",
|
"Launching browser on Windows: {:?} with args: {:?}",
|
||||||
executable_path, args
|
executable_path, args
|
||||||
);
|
);
|
||||||
Ok(Command::new(executable_path).args(args).spawn()?)
|
|
||||||
|
// Check if the executable exists
|
||||||
|
if !executable_path.exists() {
|
||||||
|
return Err(format!("Browser executable not found: {:?}", executable_path).into());
|
||||||
|
}
|
||||||
|
|
||||||
|
// On Windows, set up the command with proper working directory
|
||||||
|
let mut cmd = Command::new(executable_path);
|
||||||
|
cmd.args(args);
|
||||||
|
|
||||||
|
// Set working directory to the executable's directory for better compatibility
|
||||||
|
if let Some(parent_dir) = executable_path.parent() {
|
||||||
|
cmd.current_dir(parent_dir);
|
||||||
|
}
|
||||||
|
|
||||||
|
// For Windows 7 compatibility, set some environment variables
|
||||||
|
cmd.env(
|
||||||
|
"PROCESSOR_ARCHITECTURE",
|
||||||
|
std::env::var("PROCESSOR_ARCHITECTURE").unwrap_or_else(|_| "x86".to_string()),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Ensure proper PATH for DLL loading
|
||||||
|
if let Some(exe_dir) = executable_path.parent() {
|
||||||
|
let mut path_var = std::env::var("PATH").unwrap_or_default();
|
||||||
|
if !path_var.is_empty() {
|
||||||
|
path_var = format!("{};{}", exe_dir.display(), path_var);
|
||||||
|
} else {
|
||||||
|
path_var = exe_dir.display().to_string();
|
||||||
|
}
|
||||||
|
cmd.env("PATH", path_var);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Launch the process
|
||||||
|
let child = cmd
|
||||||
|
.spawn()
|
||||||
|
.map_err(|e| format!("Failed to launch browser process: {}", e))?;
|
||||||
|
|
||||||
|
println!(
|
||||||
|
"Successfully launched browser process with PID: {}",
|
||||||
|
child.id()
|
||||||
|
);
|
||||||
|
Ok(child)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn open_url_in_existing_browser_firefox_like(
|
pub async fn open_url_in_existing_browser_firefox_like(
|
||||||
@@ -514,14 +583,88 @@ mod windows {
|
|||||||
.get_executable_path(browser_dir)
|
.get_executable_path(browser_dir)
|
||||||
.map_err(|e| format!("Failed to get executable path: {}", e))?;
|
.map_err(|e| format!("Failed to get executable path: {}", e))?;
|
||||||
|
|
||||||
let output = Command::new(executable_path)
|
// For Windows, try using the -requestPending approach for Firefox
|
||||||
.args(["-profile", &profile.profile_path, "-new-tab", url])
|
let mut cmd = Command::new(executable_path);
|
||||||
.output()?;
|
cmd.args([
|
||||||
|
"-profile",
|
||||||
|
&profile.profile_path,
|
||||||
|
"-requestPending",
|
||||||
|
"-new-tab",
|
||||||
|
url,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Set working directory
|
||||||
|
if let Some(parent_dir) = browser_dir
|
||||||
|
.parent()
|
||||||
|
.or_else(|| browser_dir.ancestors().nth(1))
|
||||||
|
{
|
||||||
|
cmd.current_dir(parent_dir);
|
||||||
|
}
|
||||||
|
|
||||||
|
let output = cmd.output()?;
|
||||||
|
|
||||||
|
if !output.status.success() {
|
||||||
|
// Fallback: try without -requestPending
|
||||||
|
let executable_path = browser
|
||||||
|
.get_executable_path(browser_dir)
|
||||||
|
.map_err(|e| format!("Failed to get executable path: {}", e))?;
|
||||||
|
let mut fallback_cmd = Command::new(executable_path);
|
||||||
|
fallback_cmd.args(["-profile", &profile.profile_path, "-new-tab", url]);
|
||||||
|
|
||||||
|
if let Some(parent_dir) = browser_dir
|
||||||
|
.parent()
|
||||||
|
.or_else(|| browser_dir.ancestors().nth(1))
|
||||||
|
{
|
||||||
|
fallback_cmd.current_dir(parent_dir);
|
||||||
|
}
|
||||||
|
|
||||||
|
let fallback_output = fallback_cmd.output()?;
|
||||||
|
|
||||||
|
if !fallback_output.status.success() {
|
||||||
|
return Err(
|
||||||
|
format!(
|
||||||
|
"Failed to open URL in existing browser: {}",
|
||||||
|
String::from_utf8_lossy(&fallback_output.stderr)
|
||||||
|
)
|
||||||
|
.into(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn open_url_in_existing_browser_tor_mullvad(
|
||||||
|
profile: &BrowserProfile,
|
||||||
|
url: &str,
|
||||||
|
browser_type: BrowserType,
|
||||||
|
browser_dir: &Path,
|
||||||
|
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||||
|
// On Windows, TOR and Mullvad browsers can sometimes accept URLs via command line
|
||||||
|
// even with -no-remote, by launching a new instance that hands off to existing one
|
||||||
|
let browser = create_browser(browser_type);
|
||||||
|
let executable_path = browser
|
||||||
|
.get_executable_path(browser_dir)
|
||||||
|
.map_err(|e| format!("Failed to get executable path: {}", e))?;
|
||||||
|
|
||||||
|
let mut cmd = Command::new(executable_path);
|
||||||
|
cmd.args(["-profile", &profile.profile_path, url]);
|
||||||
|
|
||||||
|
// Set working directory
|
||||||
|
if let Some(parent_dir) = browser_dir
|
||||||
|
.parent()
|
||||||
|
.or_else(|| browser_dir.ancestors().nth(1))
|
||||||
|
{
|
||||||
|
cmd.current_dir(parent_dir);
|
||||||
|
}
|
||||||
|
|
||||||
|
let output = cmd.output()?;
|
||||||
|
|
||||||
if !output.status.success() {
|
if !output.status.success() {
|
||||||
return Err(
|
return Err(
|
||||||
format!(
|
format!(
|
||||||
"Failed to open URL in existing browser: {}",
|
"Failed to open URL in existing {}: {}. Note: TOR and Mullvad browsers may require manual URL opening for security reasons.",
|
||||||
|
browser_type.as_str(),
|
||||||
String::from_utf8_lossy(&output.stderr)
|
String::from_utf8_lossy(&output.stderr)
|
||||||
)
|
)
|
||||||
.into(),
|
.into(),
|
||||||
@@ -531,38 +674,57 @@ mod windows {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn open_url_in_existing_browser_tor_mullvad(
|
|
||||||
_profile: &BrowserProfile,
|
|
||||||
_url: &str,
|
|
||||||
_browser_type: BrowserType,
|
|
||||||
_browser_dir: &Path,
|
|
||||||
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
|
||||||
Err("Opening URLs in existing Firefox-based browsers is not supported on Windows when using -no-remote".into())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn open_url_in_existing_browser_chromium(
|
pub async fn open_url_in_existing_browser_chromium(
|
||||||
profile: &BrowserProfile,
|
profile: &BrowserProfile,
|
||||||
url: &str,
|
url: &str,
|
||||||
browser_type: BrowserType,
|
browser_type: BrowserType,
|
||||||
browser_dir: &Path,
|
browser_dir: &Path,
|
||||||
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||||
let browser = create_browser(browser_type);
|
let browser = create_browser(browser_type.clone());
|
||||||
let executable_path = browser
|
let executable_path = browser
|
||||||
.get_executable_path(browser_dir)
|
.get_executable_path(browser_dir)
|
||||||
.map_err(|e| format!("Failed to get executable path: {}", e))?;
|
.map_err(|e| format!("Failed to get executable path: {}", e))?;
|
||||||
|
|
||||||
let output = Command::new(executable_path)
|
let mut cmd = Command::new(executable_path);
|
||||||
.args([&format!("--user-data-dir={}", profile.profile_path), url])
|
cmd.args([
|
||||||
.output()?;
|
&format!("--user-data-dir={}", profile.profile_path),
|
||||||
|
"--new-window",
|
||||||
|
url,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Set working directory
|
||||||
|
if let Some(parent_dir) = browser_dir
|
||||||
|
.parent()
|
||||||
|
.or_else(|| browser_dir.ancestors().nth(1))
|
||||||
|
{
|
||||||
|
cmd.current_dir(parent_dir);
|
||||||
|
}
|
||||||
|
|
||||||
|
let output = cmd.output()?;
|
||||||
|
|
||||||
if !output.status.success() {
|
if !output.status.success() {
|
||||||
return Err(
|
// Try fallback without --new-window
|
||||||
format!(
|
let mut fallback_cmd = Command::new(executable_path);
|
||||||
"Failed to open URL in existing Chromium-based browser: {}",
|
fallback_cmd.args([&format!("--user-data-dir={}", profile.profile_path), url]);
|
||||||
String::from_utf8_lossy(&output.stderr)
|
|
||||||
)
|
if let Some(parent_dir) = browser_dir
|
||||||
.into(),
|
.parent()
|
||||||
);
|
.or_else(|| browser_dir.ancestors().nth(1))
|
||||||
|
{
|
||||||
|
fallback_cmd.current_dir(parent_dir);
|
||||||
|
}
|
||||||
|
|
||||||
|
let fallback_output = fallback_cmd.output()?;
|
||||||
|
|
||||||
|
if !fallback_output.status.success() {
|
||||||
|
return Err(
|
||||||
|
format!(
|
||||||
|
"Failed to open URL in existing Chromium-based browser: {}",
|
||||||
|
String::from_utf8_lossy(&fallback_output.stderr)
|
||||||
|
)
|
||||||
|
.into(),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -571,17 +733,41 @@ mod windows {
|
|||||||
pub async fn kill_browser_process_impl(
|
pub async fn kill_browser_process_impl(
|
||||||
pid: u32,
|
pid: u32,
|
||||||
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||||
|
// First try using sysinfo (cross-platform approach)
|
||||||
let system = System::new_all();
|
let system = System::new_all();
|
||||||
if let Some(process) = system.process(Pid::from(pid as usize)) {
|
if let Some(process) = system.process(Pid::from(pid as usize)) {
|
||||||
if !process.kill() {
|
if process.kill() {
|
||||||
return Err(format!("Failed to kill process {}", pid).into());
|
println!("Successfully killed browser process with PID: {pid}");
|
||||||
|
return Ok(());
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
return Err(format!("Process {} not found", pid).into());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
println!("Successfully killed browser process with PID: {pid}");
|
// Fallback to Windows-specific process termination
|
||||||
Ok(())
|
use std::process::Command;
|
||||||
|
|
||||||
|
// Try taskkill command as fallback
|
||||||
|
let output = Command::new("taskkill")
|
||||||
|
.args(["/F", "/PID", &pid.to_string()])
|
||||||
|
.output();
|
||||||
|
|
||||||
|
match output {
|
||||||
|
Ok(result) => {
|
||||||
|
if result.status.success() {
|
||||||
|
println!("Successfully killed browser process with PID: {pid} using taskkill");
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err(
|
||||||
|
format!(
|
||||||
|
"Failed to kill process {} with taskkill: {}",
|
||||||
|
pid,
|
||||||
|
String::from_utf8_lossy(&result.stderr)
|
||||||
|
)
|
||||||
|
.into(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => Err(format!("Failed to execute taskkill for process {}: {}", pid, e).into()),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -589,6 +775,7 @@ mod windows {
|
|||||||
mod linux {
|
mod linux {
|
||||||
use super::*;
|
use super::*;
|
||||||
use std::ffi::OsString;
|
use std::ffi::OsString;
|
||||||
|
use std::process::Command;
|
||||||
|
|
||||||
pub fn is_tor_or_mullvad_browser(
|
pub fn is_tor_or_mullvad_browser(
|
||||||
_exe_name: &str,
|
_exe_name: &str,
|
||||||
|
|||||||
@@ -65,13 +65,283 @@ mod macos {
|
|||||||
|
|
||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
mod windows {
|
mod windows {
|
||||||
|
use std::path::Path;
|
||||||
|
use winreg::enums::*;
|
||||||
|
use winreg::RegKey;
|
||||||
|
|
||||||
|
const APP_NAME: &str = "DonutBrowser";
|
||||||
|
const APP_EXECUTABLE: &str = "DonutBrowser.exe";
|
||||||
|
const PROG_ID: &str = "DonutBrowser.HTML";
|
||||||
|
|
||||||
pub fn is_default_browser() -> Result<bool, String> {
|
pub fn is_default_browser() -> Result<bool, String> {
|
||||||
// Windows implementation would go here
|
let schemes = ["http", "https"];
|
||||||
Err("Windows support not implemented yet".to_string())
|
|
||||||
|
for scheme in schemes {
|
||||||
|
// Check if our browser is set as the default handler for this scheme
|
||||||
|
if !is_default_for_scheme(scheme)? {
|
||||||
|
return Ok(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_as_default_browser() -> Result<(), String> {
|
pub fn set_as_default_browser() -> Result<(), String> {
|
||||||
Err("Windows support not implemented yet".to_string())
|
// Get the current executable path
|
||||||
|
let exe_path = std::env::current_exe()
|
||||||
|
.map_err(|e| format!("Failed to get current executable path: {}", e))?;
|
||||||
|
|
||||||
|
let exe_path_str = exe_path
|
||||||
|
.to_str()
|
||||||
|
.ok_or("Failed to convert executable path to string")?;
|
||||||
|
|
||||||
|
// Verify the executable exists
|
||||||
|
if !Path::new(exe_path_str).exists() {
|
||||||
|
return Err(format!("Executable not found at: {}", exe_path_str));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register the application
|
||||||
|
register_application(exe_path_str)?;
|
||||||
|
|
||||||
|
// Set as default for HTTP and HTTPS
|
||||||
|
set_default_for_scheme("http")?;
|
||||||
|
set_default_for_scheme("https")?;
|
||||||
|
|
||||||
|
// Register file associations for HTML files
|
||||||
|
register_html_file_association(exe_path_str)?;
|
||||||
|
|
||||||
|
// Notify the system of changes
|
||||||
|
notify_system_of_changes();
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_default_for_scheme(scheme: &str) -> Result<bool, String> {
|
||||||
|
let hkcu = RegKey::predef(HKEY_CURRENT_USER);
|
||||||
|
|
||||||
|
// Check Software\Microsoft\Windows\Shell\Associations\UrlAssociations\{scheme}\UserChoice
|
||||||
|
let path = format!(
|
||||||
|
"Software\\Microsoft\\Windows\\Shell\\Associations\\UrlAssociations\\{}\\UserChoice",
|
||||||
|
scheme
|
||||||
|
);
|
||||||
|
|
||||||
|
match hkcu.open_subkey(&path) {
|
||||||
|
Ok(key) => match key.get_value::<String, _>("ProgId") {
|
||||||
|
Ok(prog_id) => Ok(prog_id == PROG_ID),
|
||||||
|
Err(_) => Ok(false),
|
||||||
|
},
|
||||||
|
Err(_) => Ok(false),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn register_application(exe_path: &str) -> Result<(), String> {
|
||||||
|
let hkcu = RegKey::predef(HKEY_CURRENT_USER);
|
||||||
|
|
||||||
|
// Register in Software\RegisteredApplications
|
||||||
|
let (registered_apps, _) = hkcu
|
||||||
|
.create_subkey("Software\\RegisteredApplications")
|
||||||
|
.map_err(|e| format!("Failed to create RegisteredApplications key: {}", e))?;
|
||||||
|
|
||||||
|
registered_apps
|
||||||
|
.set_value(APP_NAME, &format!("Software\\{}", APP_NAME))
|
||||||
|
.map_err(|e| format!("Failed to set registered application: {}", e))?;
|
||||||
|
|
||||||
|
// Create application key
|
||||||
|
let (app_key, _) = hkcu
|
||||||
|
.create_subkey(&format!("Software\\{}", APP_NAME))
|
||||||
|
.map_err(|e| format!("Failed to create application key: {}", e))?;
|
||||||
|
|
||||||
|
// Set application properties
|
||||||
|
app_key
|
||||||
|
.set_value("ApplicationName", &APP_NAME)
|
||||||
|
.map_err(|e| format!("Failed to set ApplicationName: {}", e))?;
|
||||||
|
|
||||||
|
app_key
|
||||||
|
.set_value(
|
||||||
|
"ApplicationDescription",
|
||||||
|
&"Donut Browser - Simple Yet Powerful Browser Orchestrator",
|
||||||
|
)
|
||||||
|
.map_err(|e| format!("Failed to set ApplicationDescription: {}", e))?;
|
||||||
|
|
||||||
|
app_key
|
||||||
|
.set_value("ApplicationIcon", &format!("{},0", exe_path))
|
||||||
|
.map_err(|e| format!("Failed to set ApplicationIcon: {}", e))?;
|
||||||
|
|
||||||
|
// Create Capabilities key
|
||||||
|
let (capabilities, _) = app_key
|
||||||
|
.create_subkey("Capabilities")
|
||||||
|
.map_err(|e| format!("Failed to create Capabilities key: {}", e))?;
|
||||||
|
|
||||||
|
capabilities
|
||||||
|
.set_value(
|
||||||
|
"ApplicationDescription",
|
||||||
|
&"Donut Browser - Simple Yet Powerful Browser Orchestrator",
|
||||||
|
)
|
||||||
|
.map_err(|e| format!("Failed to set Capabilities description: {}", e))?;
|
||||||
|
|
||||||
|
// Set URL associations
|
||||||
|
let (url_assoc, _) = capabilities
|
||||||
|
.create_subkey("URLAssociations")
|
||||||
|
.map_err(|e| format!("Failed to create URLAssociations key: {}", e))?;
|
||||||
|
|
||||||
|
url_assoc
|
||||||
|
.set_value("http", &PROG_ID)
|
||||||
|
.map_err(|e| format!("Failed to set http association: {}", e))?;
|
||||||
|
|
||||||
|
url_assoc
|
||||||
|
.set_value("https", &PROG_ID)
|
||||||
|
.map_err(|e| format!("Failed to set https association: {}", e))?;
|
||||||
|
|
||||||
|
// Set file associations
|
||||||
|
let (file_assoc, _) = capabilities
|
||||||
|
.create_subkey("FileAssociations")
|
||||||
|
.map_err(|e| format!("Failed to create FileAssociations key: {}", e))?;
|
||||||
|
|
||||||
|
file_assoc
|
||||||
|
.set_value(".html", &PROG_ID)
|
||||||
|
.map_err(|e| format!("Failed to set .html association: {}", e))?;
|
||||||
|
|
||||||
|
file_assoc
|
||||||
|
.set_value(".htm", &PROG_ID)
|
||||||
|
.map_err(|e| format!("Failed to set .htm association: {}", e))?;
|
||||||
|
|
||||||
|
// Register the ProgID
|
||||||
|
register_prog_id(exe_path)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn register_prog_id(exe_path: &str) -> Result<(), String> {
|
||||||
|
let hkcu = RegKey::predef(HKEY_CURRENT_USER);
|
||||||
|
|
||||||
|
// Create ProgID key
|
||||||
|
let (prog_id_key, _) = hkcu
|
||||||
|
.create_subkey(&format!("Software\\Classes\\{}", PROG_ID))
|
||||||
|
.map_err(|e| format!("Failed to create ProgID key: {}", e))?;
|
||||||
|
|
||||||
|
prog_id_key
|
||||||
|
.set_value("", &"Donut Browser Document")
|
||||||
|
.map_err(|e| format!("Failed to set ProgID default value: {}", e))?;
|
||||||
|
|
||||||
|
prog_id_key
|
||||||
|
.set_value("FriendlyTypeName", &"Donut Browser Document")
|
||||||
|
.map_err(|e| format!("Failed to set FriendlyTypeName: {}", e))?;
|
||||||
|
|
||||||
|
// Create DefaultIcon key
|
||||||
|
let (icon_key, _) = prog_id_key
|
||||||
|
.create_subkey("DefaultIcon")
|
||||||
|
.map_err(|e| format!("Failed to create DefaultIcon key: {}", e))?;
|
||||||
|
|
||||||
|
icon_key
|
||||||
|
.set_value("", &format!("{},0", exe_path))
|
||||||
|
.map_err(|e| format!("Failed to set default icon: {}", e))?;
|
||||||
|
|
||||||
|
// Create shell\open\command key
|
||||||
|
let (command_key, _) = prog_id_key
|
||||||
|
.create_subkey("shell\\open\\command")
|
||||||
|
.map_err(|e| format!("Failed to create command key: {}", e))?;
|
||||||
|
|
||||||
|
command_key
|
||||||
|
.set_value("", &format!("\"{}\" \"%1\"", exe_path))
|
||||||
|
.map_err(|e| format!("Failed to set command: {}", e))?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_default_for_scheme(scheme: &str) -> Result<(), String> {
|
||||||
|
let hkcu = RegKey::predef(HKEY_CURRENT_USER);
|
||||||
|
|
||||||
|
// Set in Software\Microsoft\Windows\CurrentVersion\Explorer\FileExts\.html\UserChoice
|
||||||
|
// Note: On Windows 10+, this might require elevated permissions or user interaction
|
||||||
|
// through the Settings app due to security restrictions
|
||||||
|
|
||||||
|
// Try to set the association in the user's choice
|
||||||
|
let user_choice_path = format!(
|
||||||
|
"Software\\Microsoft\\Windows\\Shell\\Associations\\UrlAssociations\\{}\\UserChoice",
|
||||||
|
scheme
|
||||||
|
);
|
||||||
|
|
||||||
|
// Note: Setting UserChoice directly may not work on Windows 10+ due to hash verification
|
||||||
|
// The user may need to manually set the default browser through Windows Settings
|
||||||
|
match hkcu.create_subkey(&user_choice_path) {
|
||||||
|
Ok((user_choice, _)) => {
|
||||||
|
// Attempt to set the ProgId
|
||||||
|
if let Err(_) = user_choice.set_value("ProgId", &PROG_ID) {
|
||||||
|
// If we can't set UserChoice, that's expected on newer Windows versions
|
||||||
|
// The registration is still valuable for the "Open with" menu
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(_) => {
|
||||||
|
// Expected on newer Windows versions - user must set manually
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn register_html_file_association(exe_path: &str) -> Result<(), String> {
|
||||||
|
let hkcu = RegKey::predef(HKEY_CURRENT_USER);
|
||||||
|
|
||||||
|
// Register .html and .htm file associations
|
||||||
|
for ext in &[".html", ".htm"] {
|
||||||
|
let ext_path = format!("Software\\Classes\\{}", ext);
|
||||||
|
|
||||||
|
match hkcu.create_subkey(&ext_path) {
|
||||||
|
Ok((ext_key, _)) => {
|
||||||
|
// Set the default value to our ProgID
|
||||||
|
let _ = ext_key.set_value("", &PROG_ID);
|
||||||
|
}
|
||||||
|
Err(_) => {
|
||||||
|
// Continue if we can't set the file association
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn notify_system_of_changes() {
|
||||||
|
// Use Windows API to notify the system of association changes
|
||||||
|
// This helps refresh the system's understanding of the changes
|
||||||
|
unsafe {
|
||||||
|
use std::ffi::c_void;
|
||||||
|
|
||||||
|
// Declare the Windows API functions
|
||||||
|
type UINT = u32;
|
||||||
|
type DWORD = u32;
|
||||||
|
type LPARAM = isize;
|
||||||
|
type WPARAM = usize;
|
||||||
|
|
||||||
|
const HWND_BROADCAST: *mut c_void = 0xffff as *mut c_void;
|
||||||
|
const WM_SETTINGCHANGE: UINT = 0x001A;
|
||||||
|
const SMTO_ABORTIFHUNG: UINT = 0x0002;
|
||||||
|
|
||||||
|
// Link to user32.dll functions
|
||||||
|
extern "system" {
|
||||||
|
fn SendMessageTimeoutA(
|
||||||
|
hWnd: *mut c_void,
|
||||||
|
Msg: UINT,
|
||||||
|
wParam: WPARAM,
|
||||||
|
lParam: LPARAM,
|
||||||
|
fuFlags: UINT,
|
||||||
|
uTimeout: UINT,
|
||||||
|
lpdwResult: *mut DWORD,
|
||||||
|
) -> isize;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut result: DWORD = 0;
|
||||||
|
|
||||||
|
// Notify about file associations change
|
||||||
|
SendMessageTimeoutA(
|
||||||
|
HWND_BROADCAST,
|
||||||
|
WM_SETTINGCHANGE,
|
||||||
|
0,
|
||||||
|
"Software\\Classes\0".as_ptr() as LPARAM,
|
||||||
|
SMTO_ABORTIFHUNG,
|
||||||
|
1000,
|
||||||
|
&mut result,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+270
-41
@@ -453,8 +453,13 @@ impl Extractor {
|
|||||||
zip_path: &Path,
|
zip_path: &Path,
|
||||||
dest_dir: &Path,
|
dest_dir: &Path,
|
||||||
) -> Result<PathBuf, Box<dyn std::error::Error + Send + Sync>> {
|
) -> Result<PathBuf, Box<dyn std::error::Error + Send + Sync>> {
|
||||||
// Use PowerShell's Expand-Archive on Windows
|
println!("Extracting ZIP archive on Windows: {}", zip_path.display());
|
||||||
let output = Command::new("powershell")
|
|
||||||
|
// Create destination directory if it doesn't exist
|
||||||
|
fs::create_dir_all(dest_dir)?;
|
||||||
|
|
||||||
|
// First try PowerShell's Expand-Archive (Windows 10+)
|
||||||
|
let powershell_result = Command::new("powershell")
|
||||||
.args([
|
.args([
|
||||||
"-Command",
|
"-Command",
|
||||||
&format!(
|
&format!(
|
||||||
@@ -463,21 +468,81 @@ impl Extractor {
|
|||||||
dest_dir.display()
|
dest_dir.display()
|
||||||
),
|
),
|
||||||
])
|
])
|
||||||
.output()?;
|
.output();
|
||||||
|
|
||||||
if !output.status.success() {
|
match powershell_result {
|
||||||
return Err(
|
Ok(output) if output.status.success() => {
|
||||||
format!(
|
println!("Successfully extracted using PowerShell");
|
||||||
"Failed to extract zip with PowerShell: {}",
|
}
|
||||||
|
Ok(output) => {
|
||||||
|
println!(
|
||||||
|
"PowerShell extraction failed: {}, trying Rust zip crate fallback",
|
||||||
String::from_utf8_lossy(&output.stderr)
|
String::from_utf8_lossy(&output.stderr)
|
||||||
)
|
);
|
||||||
.into(),
|
// Fallback to Rust zip crate for Windows 7 compatibility
|
||||||
);
|
return self.extract_zip_with_rust_crate(zip_path, dest_dir).await;
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
println!("PowerShell not available: {}, using Rust zip crate", e);
|
||||||
|
// Fallback to Rust zip crate for Windows 7 compatibility
|
||||||
|
return self.extract_zip_with_rust_crate(zip_path, dest_dir).await;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
self.find_extracted_executable(dest_dir).await
|
self.find_extracted_executable(dest_dir).await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
async fn extract_zip_with_rust_crate(
|
||||||
|
&self,
|
||||||
|
zip_path: &Path,
|
||||||
|
dest_dir: &Path,
|
||||||
|
) -> Result<PathBuf, Box<dyn std::error::Error + Send + Sync>> {
|
||||||
|
println!("Using Rust zip crate for extraction (Windows 7+ compatibility)");
|
||||||
|
|
||||||
|
let file = fs::File::open(zip_path)?;
|
||||||
|
let mut archive = zip::ZipArchive::new(file)?;
|
||||||
|
|
||||||
|
for i in 0..archive.len() {
|
||||||
|
let mut file = archive.by_index(i)?;
|
||||||
|
let outpath = match file.enclosed_name() {
|
||||||
|
Some(path) => dest_dir.join(path),
|
||||||
|
None => continue,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handle directory creation
|
||||||
|
if file.name().ends_with('/') {
|
||||||
|
fs::create_dir_all(&outpath)?;
|
||||||
|
} else {
|
||||||
|
// Create parent directories
|
||||||
|
if let Some(p) = outpath.parent() {
|
||||||
|
if !p.exists() {
|
||||||
|
fs::create_dir_all(p)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract file
|
||||||
|
let mut outfile = fs::File::create(&outpath)?;
|
||||||
|
std::io::copy(&mut file, &mut outfile)?;
|
||||||
|
|
||||||
|
// On Windows, verify executable files
|
||||||
|
if outpath
|
||||||
|
.extension()
|
||||||
|
.is_some_and(|ext| ext.to_string_lossy().to_lowercase() == "exe")
|
||||||
|
{
|
||||||
|
if let Ok(metadata) = fs::metadata(&outpath) {
|
||||||
|
if metadata.len() > 0 {
|
||||||
|
println!("Extracted executable: {}", outpath.display());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
println!("ZIP extraction completed. Searching for executable...");
|
||||||
|
self.find_extracted_executable(dest_dir).await
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(not(target_os = "windows"))]
|
#[cfg(not(target_os = "windows"))]
|
||||||
async fn extract_zip_unix(
|
async fn extract_zip_unix(
|
||||||
&self,
|
&self,
|
||||||
@@ -514,24 +579,60 @@ impl Extractor {
|
|||||||
) -> Result<PathBuf, Box<dyn std::error::Error + Send + Sync>> {
|
) -> Result<PathBuf, Box<dyn std::error::Error + Send + Sync>> {
|
||||||
create_dir_all(dest_dir)?;
|
create_dir_all(dest_dir)?;
|
||||||
|
|
||||||
// Use tar command for more reliable extraction
|
#[cfg(target_os = "windows")]
|
||||||
let output = Command::new("tar")
|
{
|
||||||
.args([
|
// On Windows, try multiple extraction methods for better compatibility
|
||||||
"-xf",
|
// First try using tar if available (Windows 10+)
|
||||||
tar_path.to_str().unwrap(),
|
let tar_result = Command::new("tar")
|
||||||
"-C",
|
.args([
|
||||||
dest_dir.to_str().unwrap(),
|
"-xf",
|
||||||
])
|
tar_path.to_str().unwrap(),
|
||||||
.output()?;
|
"-C",
|
||||||
|
dest_dir.to_str().unwrap(),
|
||||||
|
])
|
||||||
|
.output();
|
||||||
|
|
||||||
if !output.status.success() {
|
match tar_result {
|
||||||
return Err(
|
Ok(output) if output.status.success() => {
|
||||||
format!(
|
println!("Successfully extracted tar.xz using tar command");
|
||||||
"Failed to extract tar.xz: {}",
|
}
|
||||||
String::from_utf8_lossy(&output.stderr)
|
Ok(output) => {
|
||||||
)
|
println!(
|
||||||
.into(),
|
"tar command failed: {}, trying 7-Zip fallback",
|
||||||
);
|
String::from_utf8_lossy(&output.stderr)
|
||||||
|
);
|
||||||
|
// Try 7-Zip as fallback
|
||||||
|
return self.extract_with_7zip(tar_path, dest_dir).await;
|
||||||
|
}
|
||||||
|
Err(_) => {
|
||||||
|
println!("tar command not available, trying 7-Zip");
|
||||||
|
// Try 7-Zip as fallback
|
||||||
|
return self.extract_with_7zip(tar_path, dest_dir).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(target_os = "windows"))]
|
||||||
|
{
|
||||||
|
// Use tar command for Unix-like systems
|
||||||
|
let output = Command::new("tar")
|
||||||
|
.args([
|
||||||
|
"-xf",
|
||||||
|
tar_path.to_str().unwrap(),
|
||||||
|
"-C",
|
||||||
|
dest_dir.to_str().unwrap(),
|
||||||
|
])
|
||||||
|
.output()?;
|
||||||
|
|
||||||
|
if !output.status.success() {
|
||||||
|
return Err(
|
||||||
|
format!(
|
||||||
|
"Failed to extract tar.xz: {}",
|
||||||
|
String::from_utf8_lossy(&output.stderr)
|
||||||
|
)
|
||||||
|
.into(),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find the extracted executable and set proper permissions
|
// Find the extracted executable and set proper permissions
|
||||||
@@ -545,6 +646,44 @@ impl Extractor {
|
|||||||
Ok(executable_path)
|
Ok(executable_path)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
async fn extract_with_7zip(
|
||||||
|
&self,
|
||||||
|
archive_path: &Path,
|
||||||
|
dest_dir: &Path,
|
||||||
|
) -> Result<PathBuf, Box<dyn std::error::Error + Send + Sync>> {
|
||||||
|
// Try to use 7-Zip for extraction (common on Windows)
|
||||||
|
let seven_zip_paths = [
|
||||||
|
"7z", // If 7z is in PATH
|
||||||
|
"C:\\Program Files\\7-Zip\\7z.exe",
|
||||||
|
"C:\\Program Files (x86)\\7-Zip\\7z.exe",
|
||||||
|
];
|
||||||
|
|
||||||
|
for seven_zip_path in &seven_zip_paths {
|
||||||
|
let result = Command::new(seven_zip_path)
|
||||||
|
.args([
|
||||||
|
"x", // Extract with full paths
|
||||||
|
archive_path.to_str().unwrap(),
|
||||||
|
&format!("-o{}", dest_dir.display()), // Output directory
|
||||||
|
"-y", // Yes to all
|
||||||
|
])
|
||||||
|
.output();
|
||||||
|
|
||||||
|
match result {
|
||||||
|
Ok(output) if output.status.success() => {
|
||||||
|
println!("Successfully extracted using 7-Zip: {}", seven_zip_path);
|
||||||
|
return self.find_extracted_executable(dest_dir).await;
|
||||||
|
}
|
||||||
|
Ok(_) => continue,
|
||||||
|
Err(_) => continue,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Err(
|
||||||
|
"No suitable extraction tool found. Please install 7-Zip or ensure tar is available.".into(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn extract_tar_bz2(
|
pub async fn extract_tar_bz2(
|
||||||
&self,
|
&self,
|
||||||
tar_path: &Path,
|
tar_path: &Path,
|
||||||
@@ -827,40 +966,130 @@ impl Extractor {
|
|||||||
&self,
|
&self,
|
||||||
dest_dir: &Path,
|
dest_dir: &Path,
|
||||||
) -> Result<PathBuf, Box<dyn std::error::Error + Send + Sync>> {
|
) -> Result<PathBuf, Box<dyn std::error::Error + Send + Sync>> {
|
||||||
|
println!(
|
||||||
|
"Searching for Windows executable in: {}",
|
||||||
|
dest_dir.display()
|
||||||
|
);
|
||||||
|
|
||||||
// Look for .exe files, preferring main browser executables
|
// Look for .exe files, preferring main browser executables
|
||||||
let exe_names = [
|
let priority_exe_names = [
|
||||||
"chrome.exe",
|
|
||||||
"firefox.exe",
|
"firefox.exe",
|
||||||
|
"chrome.exe",
|
||||||
|
"chromium.exe",
|
||||||
"zen.exe",
|
"zen.exe",
|
||||||
"brave.exe",
|
"brave.exe",
|
||||||
|
"tor-browser.exe",
|
||||||
"tor.exe",
|
"tor.exe",
|
||||||
|
"mullvad-browser.exe",
|
||||||
];
|
];
|
||||||
|
|
||||||
for exe_name in &exe_names {
|
// First try priority executable names
|
||||||
|
for exe_name in &priority_exe_names {
|
||||||
let exe_path = dest_dir.join(exe_name);
|
let exe_path = dest_dir.join(exe_name);
|
||||||
if exe_path.exists() {
|
if exe_path.exists() {
|
||||||
|
println!("Found priority executable: {}", exe_path.display());
|
||||||
return Ok(exe_path);
|
return Ok(exe_path);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// If no specific executable found, look for any .exe file
|
// Recursively search for executables with depth limit
|
||||||
if let Ok(entries) = fs::read_dir(dest_dir) {
|
match self.find_windows_executable_recursive(dest_dir, 0, 3).await {
|
||||||
for entry in entries.flatten() {
|
Ok(exe_path) => {
|
||||||
let path = entry.path();
|
println!(
|
||||||
if path.extension().is_some_and(|ext| ext == "exe") {
|
"Found executable via recursive search: {}",
|
||||||
return Ok(path);
|
exe_path.display()
|
||||||
|
);
|
||||||
|
Ok(exe_path)
|
||||||
|
}
|
||||||
|
Err(_) => {
|
||||||
|
// List directory contents for debugging
|
||||||
|
if let Ok(entries) = fs::read_dir(dest_dir) {
|
||||||
|
println!("Directory contents:");
|
||||||
|
for entry in entries.flatten() {
|
||||||
|
let path = entry.path();
|
||||||
|
let metadata = if path.is_dir() { "dir" } else { "file" };
|
||||||
|
println!(" - {} ({})", path.display(), metadata);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check subdirectories
|
Err("No executable found after extraction".into())
|
||||||
if path.is_dir() {
|
}
|
||||||
if let Ok(sub_result) = self.find_windows_executable(&path).await {
|
}
|
||||||
return Ok(sub_result);
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
async fn find_windows_executable_recursive(
|
||||||
|
&self,
|
||||||
|
dir: &Path,
|
||||||
|
depth: usize,
|
||||||
|
max_depth: usize,
|
||||||
|
) -> Result<PathBuf, Box<dyn std::error::Error + Send + Sync>> {
|
||||||
|
if depth > max_depth {
|
||||||
|
return Err("Maximum search depth reached".into());
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Ok(entries) = fs::read_dir(dir) {
|
||||||
|
let mut dirs_to_search = Vec::new();
|
||||||
|
|
||||||
|
// First pass: look for .exe files in current directory
|
||||||
|
for entry in entries.flatten() {
|
||||||
|
let path = entry.path();
|
||||||
|
|
||||||
|
if path.is_file()
|
||||||
|
&& path
|
||||||
|
.extension()
|
||||||
|
.is_some_and(|ext| ext.to_string_lossy().to_lowercase() == "exe")
|
||||||
|
{
|
||||||
|
let file_name = path
|
||||||
|
.file_name()
|
||||||
|
.and_then(|n| n.to_str())
|
||||||
|
.unwrap_or("")
|
||||||
|
.to_lowercase();
|
||||||
|
|
||||||
|
// Check if it's a browser executable
|
||||||
|
if file_name.contains("firefox")
|
||||||
|
|| file_name.contains("chrome")
|
||||||
|
|| file_name.contains("chromium")
|
||||||
|
|| file_name.contains("zen")
|
||||||
|
|| file_name.contains("brave")
|
||||||
|
|| file_name.contains("tor")
|
||||||
|
|| file_name.contains("mullvad")
|
||||||
|
|| file_name.contains("browser")
|
||||||
|
{
|
||||||
|
return Ok(path);
|
||||||
|
}
|
||||||
|
} else if path.is_dir() {
|
||||||
|
// Collect directories for later search
|
||||||
|
dirs_to_search.push(path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Second pass: search subdirectories
|
||||||
|
for subdir in dirs_to_search {
|
||||||
|
if let Ok(result) = self
|
||||||
|
.find_windows_executable_recursive(&subdir, depth + 1, max_depth)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
return Ok(result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Third pass: if no browser-specific executable found, return any .exe
|
||||||
|
if let Ok(entries) = fs::read_dir(dir) {
|
||||||
|
for entry in entries.flatten() {
|
||||||
|
let path = entry.path();
|
||||||
|
if path.is_file()
|
||||||
|
&& path
|
||||||
|
.extension()
|
||||||
|
.is_some_and(|ext| ext.to_string_lossy().to_lowercase() == "exe")
|
||||||
|
{
|
||||||
|
return Ok(path);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Err("No executable found after extraction".into())
|
Err("No executable found".into())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(target_os = "linux")]
|
#[cfg(target_os = "linux")]
|
||||||
|
|||||||
@@ -172,6 +172,7 @@ pub fn run() {
|
|||||||
.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())
|
.plugin(tauri_plugin_dialog::init())
|
||||||
|
.plugin(tauri_plugin_macos_permissions::init())
|
||||||
.setup(|app| {
|
.setup(|app| {
|
||||||
// Create the main window programmatically
|
// Create the main window programmatically
|
||||||
#[allow(unused_variables)]
|
#[allow(unused_variables)]
|
||||||
|
|||||||
@@ -55,6 +55,9 @@ impl ProfileImporter {
|
|||||||
// Detect Zen Browser profiles
|
// Detect Zen Browser profiles
|
||||||
detected_profiles.extend(self.detect_zen_browser_profiles()?);
|
detected_profiles.extend(self.detect_zen_browser_profiles()?);
|
||||||
|
|
||||||
|
// Detect TOR Browser profiles
|
||||||
|
detected_profiles.extend(self.detect_tor_browser_profiles()?);
|
||||||
|
|
||||||
// Remove duplicates based on path
|
// Remove duplicates based on path
|
||||||
let mut seen_paths = HashSet::new();
|
let mut seen_paths = HashSet::new();
|
||||||
let unique_profiles: Vec<DetectedProfile> = detected_profiles
|
let unique_profiles: Vec<DetectedProfile> = detected_profiles
|
||||||
@@ -80,10 +83,19 @@ impl ProfileImporter {
|
|||||||
|
|
||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
{
|
{
|
||||||
|
// Primary location in AppData\Roaming
|
||||||
if let Some(app_data) = self.base_dirs.data_dir() {
|
if let Some(app_data) = self.base_dirs.data_dir() {
|
||||||
let firefox_dir = app_data.join("Mozilla/Firefox/Profiles");
|
let firefox_dir = app_data.join("Mozilla/Firefox/Profiles");
|
||||||
profiles.extend(self.scan_firefox_profiles_dir(&firefox_dir, "firefox")?);
|
profiles.extend(self.scan_firefox_profiles_dir(&firefox_dir, "firefox")?);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Also check AppData\Local for portable installations
|
||||||
|
if let Some(local_app_data) = self.base_dirs.data_local_dir() {
|
||||||
|
let firefox_local_dir = local_app_data.join("Mozilla/Firefox/Profiles");
|
||||||
|
if firefox_local_dir.exists() {
|
||||||
|
profiles.extend(self.scan_firefox_profiles_dir(&firefox_local_dir, "firefox")?);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(target_os = "linux")]
|
#[cfg(target_os = "linux")]
|
||||||
@@ -251,10 +263,19 @@ impl ProfileImporter {
|
|||||||
|
|
||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
{
|
{
|
||||||
|
// Primary location in AppData\Roaming
|
||||||
if let Some(app_data) = self.base_dirs.data_dir() {
|
if let Some(app_data) = self.base_dirs.data_dir() {
|
||||||
let mullvad_dir = app_data.join("MullvadBrowser/Profiles");
|
let mullvad_dir = app_data.join("MullvadBrowser/Profiles");
|
||||||
profiles.extend(self.scan_firefox_profiles_dir(&mullvad_dir, "mullvad-browser")?);
|
profiles.extend(self.scan_firefox_profiles_dir(&mullvad_dir, "mullvad-browser")?);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Also check common installation locations
|
||||||
|
if let Some(local_app_data) = self.base_dirs.data_local_dir() {
|
||||||
|
let mullvad_local_dir = local_app_data.join("MullvadBrowser/Profiles");
|
||||||
|
if mullvad_local_dir.exists() {
|
||||||
|
profiles.extend(self.scan_firefox_profiles_dir(&mullvad_local_dir, "mullvad-browser")?);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(target_os = "linux")]
|
#[cfg(target_os = "linux")]
|
||||||
@@ -298,6 +319,108 @@ impl ProfileImporter {
|
|||||||
Ok(profiles)
|
Ok(profiles)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Detect TOR Browser profiles
|
||||||
|
fn detect_tor_browser_profiles(
|
||||||
|
&self,
|
||||||
|
) -> Result<Vec<DetectedProfile>, Box<dyn std::error::Error>> {
|
||||||
|
let mut profiles = Vec::new();
|
||||||
|
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
{
|
||||||
|
// TOR Browser on macOS is typically in Applications
|
||||||
|
let tor_dir = self
|
||||||
|
.base_dirs
|
||||||
|
.home_dir()
|
||||||
|
.join("Library/Application Support/TorBrowser-Data/Browser/profile.default");
|
||||||
|
|
||||||
|
if tor_dir.exists() {
|
||||||
|
profiles.push(DetectedProfile {
|
||||||
|
browser: "tor-browser".to_string(),
|
||||||
|
name: "TOR Browser - Default Profile".to_string(),
|
||||||
|
path: tor_dir.to_string_lossy().to_string(),
|
||||||
|
description: "Default TOR Browser profile".to_string(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
{
|
||||||
|
// Check common TOR Browser installation locations on Windows
|
||||||
|
let possible_paths = [
|
||||||
|
// Default installation in user directory
|
||||||
|
(
|
||||||
|
"Desktop",
|
||||||
|
"Desktop/Tor Browser/Browser/TorBrowser/Data/Browser/profile.default",
|
||||||
|
),
|
||||||
|
// AppData locations
|
||||||
|
(
|
||||||
|
"AppData/Roaming",
|
||||||
|
"TorBrowser/Browser/TorBrowser/Data/Browser/profile.default",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"AppData/Local",
|
||||||
|
"TorBrowser/Browser/TorBrowser/Data/Browser/profile.default",
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
let home_dir = self.base_dirs.home_dir();
|
||||||
|
|
||||||
|
for (location_name, relative_path) in &possible_paths {
|
||||||
|
let tor_dir = home_dir.join(relative_path);
|
||||||
|
if tor_dir.exists() {
|
||||||
|
profiles.push(DetectedProfile {
|
||||||
|
browser: "tor-browser".to_string(),
|
||||||
|
name: format!("TOR Browser - {} Profile", location_name),
|
||||||
|
path: tor_dir.to_string_lossy().to_string(),
|
||||||
|
description: format!("TOR Browser profile from {}", location_name),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Also check AppData directories if available
|
||||||
|
if let Some(app_data) = self.base_dirs.data_dir() {
|
||||||
|
let tor_app_data =
|
||||||
|
app_data.join("TorBrowser/Browser/TorBrowser/Data/Browser/profile.default");
|
||||||
|
if tor_app_data.exists() {
|
||||||
|
profiles.push(DetectedProfile {
|
||||||
|
browser: "tor-browser".to_string(),
|
||||||
|
name: "TOR Browser - AppData Profile".to_string(),
|
||||||
|
path: tor_app_data.to_string_lossy().to_string(),
|
||||||
|
description: "TOR Browser profile from AppData".to_string(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
{
|
||||||
|
// Common TOR Browser locations on Linux
|
||||||
|
let possible_paths = [
|
||||||
|
".local/share/torbrowser/tbb/x86_64/tor-browser_en-US/Browser/TorBrowser/Data/Browser/profile.default",
|
||||||
|
"tor-browser_en-US/Browser/TorBrowser/Data/Browser/profile.default",
|
||||||
|
".tor-browser/Browser/TorBrowser/Data/Browser/profile.default",
|
||||||
|
"Downloads/tor-browser_en-US/Browser/TorBrowser/Data/Browser/profile.default",
|
||||||
|
];
|
||||||
|
|
||||||
|
let home_dir = self.base_dirs.home_dir();
|
||||||
|
|
||||||
|
for relative_path in &possible_paths {
|
||||||
|
let tor_dir = home_dir.join(relative_path);
|
||||||
|
if tor_dir.exists() {
|
||||||
|
profiles.push(DetectedProfile {
|
||||||
|
browser: "tor-browser".to_string(),
|
||||||
|
name: "TOR Browser - Default Profile".to_string(),
|
||||||
|
path: tor_dir.to_string_lossy().to_string(),
|
||||||
|
description: "TOR Browser profile".to_string(),
|
||||||
|
});
|
||||||
|
break; // Only add the first one found to avoid duplicates
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(profiles)
|
||||||
|
}
|
||||||
|
|
||||||
/// Scan Firefox-style profiles directory
|
/// Scan Firefox-style profiles directory
|
||||||
fn scan_firefox_profiles_dir(
|
fn scan_firefox_profiles_dir(
|
||||||
&self,
|
&self,
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://schema.tauri.app/config/2",
|
"$schema": "https://schema.tauri.app/config/2",
|
||||||
"productName": "Donut Browser",
|
"productName": "Donut Browser",
|
||||||
"version": "0.3.2",
|
"version": "0.3.3",
|
||||||
"identifier": "com.donutbrowser",
|
"identifier": "com.donutbrowser",
|
||||||
"build": {
|
"build": {
|
||||||
"beforeDevCommand": "pnpm dev",
|
"beforeDevCommand": "pnpm dev",
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
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 { ImportProfileDialog } from "@/components/import-profile-dialog";
|
||||||
|
import { PermissionDialog } from "@/components/permission-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";
|
||||||
@@ -21,6 +22,8 @@ import {
|
|||||||
TooltipTrigger,
|
TooltipTrigger,
|
||||||
} from "@/components/ui/tooltip";
|
} from "@/components/ui/tooltip";
|
||||||
import { useAppUpdateNotifications } from "@/hooks/use-app-update-notifications";
|
import { useAppUpdateNotifications } from "@/hooks/use-app-update-notifications";
|
||||||
|
import { usePermissions } from "@/hooks/use-permissions";
|
||||||
|
import type { PermissionType } from "@/hooks/use-permissions";
|
||||||
import { useUpdateNotifications } from "@/hooks/use-update-notifications";
|
import { useUpdateNotifications } from "@/hooks/use-update-notifications";
|
||||||
import { showErrorToast } from "@/lib/toast-utils";
|
import { showErrorToast } from "@/lib/toast-utils";
|
||||||
import type { BrowserProfile, ProxySettings } from "@/types";
|
import type { BrowserProfile, ProxySettings } from "@/types";
|
||||||
@@ -58,6 +61,11 @@ export default function Home() {
|
|||||||
const [currentProfileForVersionChange, setCurrentProfileForVersionChange] =
|
const [currentProfileForVersionChange, setCurrentProfileForVersionChange] =
|
||||||
useState<BrowserProfile | null>(null);
|
useState<BrowserProfile | null>(null);
|
||||||
const [hasCheckedStartupPrompt, setHasCheckedStartupPrompt] = useState(false);
|
const [hasCheckedStartupPrompt, setHasCheckedStartupPrompt] = useState(false);
|
||||||
|
const [permissionDialogOpen, setPermissionDialogOpen] = useState(false);
|
||||||
|
const [currentPermissionType, setCurrentPermissionType] =
|
||||||
|
useState<PermissionType>("microphone");
|
||||||
|
const { isMicrophoneAccessGranted, isCameraAccessGranted, isInitialized } =
|
||||||
|
usePermissions();
|
||||||
|
|
||||||
// Simple profiles loader without updates check (for use as callback)
|
// Simple profiles loader without updates check (for use as callback)
|
||||||
const loadProfiles = useCallback(async () => {
|
const loadProfiles = useCallback(async () => {
|
||||||
@@ -119,6 +127,13 @@ export default function Home() {
|
|||||||
};
|
};
|
||||||
}, [loadProfilesWithUpdateCheck, checkForUpdates]);
|
}, [loadProfilesWithUpdateCheck, checkForUpdates]);
|
||||||
|
|
||||||
|
// Check permissions when they are initialized
|
||||||
|
useEffect(() => {
|
||||||
|
if (isInitialized) {
|
||||||
|
void checkAllPermissions();
|
||||||
|
}
|
||||||
|
}, [isInitialized]);
|
||||||
|
|
||||||
const checkStartupPrompt = async () => {
|
const checkStartupPrompt = async () => {
|
||||||
// Only check once during app startup to prevent reopening after dismissing notifications
|
// Only check once during app startup to prevent reopening after dismissing notifications
|
||||||
if (hasCheckedStartupPrompt) return;
|
if (hasCheckedStartupPrompt) return;
|
||||||
@@ -137,6 +152,42 @@ export default function Home() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const checkAllPermissions = async () => {
|
||||||
|
try {
|
||||||
|
// Wait for permissions to be initialized before checking
|
||||||
|
if (!isInitialized) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if any permissions are not granted - prioritize missing permissions
|
||||||
|
if (!isMicrophoneAccessGranted) {
|
||||||
|
setCurrentPermissionType("microphone");
|
||||||
|
setPermissionDialogOpen(true);
|
||||||
|
} else if (!isCameraAccessGranted) {
|
||||||
|
setCurrentPermissionType("camera");
|
||||||
|
setPermissionDialogOpen(true);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to check permissions:", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const checkNextPermission = () => {
|
||||||
|
try {
|
||||||
|
if (!isMicrophoneAccessGranted) {
|
||||||
|
setCurrentPermissionType("microphone");
|
||||||
|
setPermissionDialogOpen(true);
|
||||||
|
} else if (!isCameraAccessGranted) {
|
||||||
|
setCurrentPermissionType("camera");
|
||||||
|
setPermissionDialogOpen(true);
|
||||||
|
} else {
|
||||||
|
setPermissionDialogOpen(false);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to check next permission:", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const checkStartupUrls = async () => {
|
const checkStartupUrls = async () => {
|
||||||
try {
|
try {
|
||||||
const hasStartupUrl = await invoke<boolean>(
|
const hasStartupUrl = await invoke<boolean>(
|
||||||
@@ -533,6 +584,15 @@ export default function Home() {
|
|||||||
runningProfiles={runningProfiles}
|
runningProfiles={runningProfiles}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
|
<PermissionDialog
|
||||||
|
isOpen={permissionDialogOpen}
|
||||||
|
onClose={() => {
|
||||||
|
setPermissionDialogOpen(false);
|
||||||
|
}}
|
||||||
|
permissionType={currentPermissionType}
|
||||||
|
onPermissionGranted={checkNextPermission}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,182 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { LoadingButton } from "@/components/loading-button";
|
||||||
|
import { Badge } from "@/components/ui/badge";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
} from "@/components/ui/dialog";
|
||||||
|
import { usePermissions } from "@/hooks/use-permissions";
|
||||||
|
import type { PermissionType } from "@/hooks/use-permissions";
|
||||||
|
import { showErrorToast, showSuccessToast } from "@/lib/toast-utils";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { BsCamera, BsMic } from "react-icons/bs";
|
||||||
|
|
||||||
|
interface PermissionDialogProps {
|
||||||
|
isOpen: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
permissionType: PermissionType;
|
||||||
|
onPermissionGranted?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function PermissionDialog({
|
||||||
|
isOpen,
|
||||||
|
onClose,
|
||||||
|
permissionType,
|
||||||
|
onPermissionGranted,
|
||||||
|
}: PermissionDialogProps) {
|
||||||
|
const [isRequesting, setIsRequesting] = useState(false);
|
||||||
|
const [isMacOS, setIsMacOS] = useState(false);
|
||||||
|
const {
|
||||||
|
requestPermission,
|
||||||
|
isMicrophoneAccessGranted,
|
||||||
|
isCameraAccessGranted,
|
||||||
|
} = usePermissions();
|
||||||
|
|
||||||
|
// Check if we're on macOS and close dialog if not
|
||||||
|
useEffect(() => {
|
||||||
|
const userAgent = navigator.userAgent;
|
||||||
|
const isMac = userAgent.includes("Mac");
|
||||||
|
setIsMacOS(isMac);
|
||||||
|
|
||||||
|
// If not macOS, close the dialog as permissions aren't needed
|
||||||
|
if (!isMac) {
|
||||||
|
onClose();
|
||||||
|
}
|
||||||
|
}, [onClose]);
|
||||||
|
|
||||||
|
// Get current permission status
|
||||||
|
const isCurrentPermissionGranted =
|
||||||
|
permissionType === "microphone"
|
||||||
|
? isMicrophoneAccessGranted
|
||||||
|
: isCameraAccessGranted;
|
||||||
|
|
||||||
|
// Auto-close dialog when permission is granted
|
||||||
|
useEffect(() => {
|
||||||
|
if (isCurrentPermissionGranted && isOpen) {
|
||||||
|
onPermissionGranted?.();
|
||||||
|
}
|
||||||
|
}, [isCurrentPermissionGranted, isOpen, onPermissionGranted]);
|
||||||
|
|
||||||
|
const getPermissionIcon = (type: PermissionType) => {
|
||||||
|
switch (type) {
|
||||||
|
case "microphone":
|
||||||
|
return <BsMic className="w-8 h-8" />;
|
||||||
|
case "camera":
|
||||||
|
return <BsCamera className="w-8 h-8" />;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getPermissionTitle = (type: PermissionType) => {
|
||||||
|
switch (type) {
|
||||||
|
case "microphone":
|
||||||
|
return "Microphone Access Required";
|
||||||
|
case "camera":
|
||||||
|
return "Camera Access Required";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getPermissionDescription = (type: PermissionType) => {
|
||||||
|
switch (type) {
|
||||||
|
case "microphone":
|
||||||
|
return "Donut Browser needs access to your microphone to enable microphone functionality in web browsers. Each website that wants to use your microphone will still ask for your permission individually.";
|
||||||
|
case "camera":
|
||||||
|
return "Donut Browser needs access to your camera to enable camera functionality in web browsers. Each website that wants to use your camera will still ask for your permission individually.";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getStatusBadge = (isGranted: boolean) => {
|
||||||
|
if (isGranted) {
|
||||||
|
return (
|
||||||
|
<Badge variant="default" className="text-green-800 bg-green-100">
|
||||||
|
Granted
|
||||||
|
</Badge>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return <Badge variant="secondary">Not Granted</Badge>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRequestPermission = async () => {
|
||||||
|
setIsRequesting(true);
|
||||||
|
try {
|
||||||
|
await requestPermission(permissionType);
|
||||||
|
showSuccessToast(
|
||||||
|
`${getPermissionTitle(permissionType).replace(
|
||||||
|
" Required",
|
||||||
|
"",
|
||||||
|
)} permission requested`,
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to request permission:", error);
|
||||||
|
showErrorToast("Failed to request permission");
|
||||||
|
} finally {
|
||||||
|
setIsRequesting(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Don't render if not macOS
|
||||||
|
if (!isMacOS) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={isOpen} onOpenChange={onClose}>
|
||||||
|
<DialogContent className="max-w-md">
|
||||||
|
<DialogHeader className="text-center">
|
||||||
|
<div className="flex justify-center items-center mx-auto mb-4 w-16 h-16 bg-blue-100 rounded-full dark:bg-blue-900">
|
||||||
|
{getPermissionIcon(permissionType)}
|
||||||
|
</div>
|
||||||
|
<DialogTitle className="text-xl">
|
||||||
|
{getPermissionTitle(permissionType)}
|
||||||
|
</DialogTitle>
|
||||||
|
<DialogDescription className="text-base">
|
||||||
|
{getPermissionDescription(permissionType)}
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<div className="space-y-4">
|
||||||
|
{isCurrentPermissionGranted && (
|
||||||
|
<div className="p-3 bg-green-50 rounded-lg dark:bg-green-900/20">
|
||||||
|
<p className="text-sm text-green-800 dark:text-green-200">
|
||||||
|
✅ Permission granted! Browsers launched from Donut Browser can
|
||||||
|
now access your {permissionType}.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!isCurrentPermissionGranted && (
|
||||||
|
<div className="p-3 bg-amber-50 rounded-lg dark:bg-amber-900/20">
|
||||||
|
<p className="text-sm text-amber-800 dark:text-amber-200">
|
||||||
|
⚠️ Permission not granted. Click the button below to request
|
||||||
|
access to your {permissionType}.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DialogFooter className="gap-2">
|
||||||
|
<Button variant="outline" onClick={onClose}>
|
||||||
|
{isCurrentPermissionGranted ? "Done" : "Cancel"}
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
{!isCurrentPermissionGranted && (
|
||||||
|
<LoadingButton
|
||||||
|
isLoading={isRequesting}
|
||||||
|
onClick={() => {
|
||||||
|
handleRequestPermission().catch(console.error);
|
||||||
|
}}
|
||||||
|
className="min-w-24"
|
||||||
|
>
|
||||||
|
Grant Access
|
||||||
|
</LoadingButton>
|
||||||
|
)}
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -19,10 +19,13 @@ import {
|
|||||||
SelectTrigger,
|
SelectTrigger,
|
||||||
SelectValue,
|
SelectValue,
|
||||||
} from "@/components/ui/select";
|
} from "@/components/ui/select";
|
||||||
|
import { usePermissions } from "@/hooks/use-permissions";
|
||||||
|
import type { PermissionType } from "@/hooks/use-permissions";
|
||||||
import { showSuccessToast } from "@/lib/toast-utils";
|
import { showSuccessToast } from "@/lib/toast-utils";
|
||||||
import { invoke } from "@tauri-apps/api/core";
|
import { invoke } from "@tauri-apps/api/core";
|
||||||
import { useTheme } from "next-themes";
|
import { useTheme } from "next-themes";
|
||||||
import { useEffect, useState } from "react";
|
import { useCallback, useEffect, useState } from "react";
|
||||||
|
import { BsCamera, BsMic } from "react-icons/bs";
|
||||||
|
|
||||||
interface AppSettings {
|
interface AppSettings {
|
||||||
set_as_default_browser: boolean;
|
set_as_default_browser: boolean;
|
||||||
@@ -32,6 +35,12 @@ interface AppSettings {
|
|||||||
auto_delete_unused_binaries: boolean;
|
auto_delete_unused_binaries: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface PermissionInfo {
|
||||||
|
permission_type: PermissionType;
|
||||||
|
isGranted: boolean;
|
||||||
|
description: string;
|
||||||
|
}
|
||||||
|
|
||||||
interface SettingsDialogProps {
|
interface SettingsDialogProps {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
@@ -57,18 +66,46 @@ export function SettingsDialog({ isOpen, onClose }: SettingsDialogProps) {
|
|||||||
const [isSaving, setIsSaving] = useState(false);
|
const [isSaving, setIsSaving] = useState(false);
|
||||||
const [isSettingDefault, setIsSettingDefault] = useState(false);
|
const [isSettingDefault, setIsSettingDefault] = useState(false);
|
||||||
const [isClearingCache, setIsClearingCache] = useState(false);
|
const [isClearingCache, setIsClearingCache] = useState(false);
|
||||||
|
const [permissions, setPermissions] = useState<PermissionInfo[]>([]);
|
||||||
|
const [isLoadingPermissions, setIsLoadingPermissions] = useState(false);
|
||||||
|
const [requestingPermission, setRequestingPermission] =
|
||||||
|
useState<PermissionType | null>(null);
|
||||||
|
const [isMacOS, setIsMacOS] = useState(false);
|
||||||
|
|
||||||
const { setTheme } = useTheme();
|
const { setTheme } = useTheme();
|
||||||
|
const {
|
||||||
|
requestPermission,
|
||||||
|
isMicrophoneAccessGranted,
|
||||||
|
isCameraAccessGranted,
|
||||||
|
} = usePermissions();
|
||||||
|
|
||||||
|
const getPermissionDescription = useCallback((type: PermissionType) => {
|
||||||
|
switch (type) {
|
||||||
|
case "microphone":
|
||||||
|
return "Access to microphone for browser applications";
|
||||||
|
case "camera":
|
||||||
|
return "Access to camera for browser applications";
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isOpen) {
|
if (isOpen) {
|
||||||
void loadSettings();
|
loadSettings().catch(console.error);
|
||||||
void checkDefaultBrowserStatus();
|
checkDefaultBrowserStatus().catch(console.error);
|
||||||
|
|
||||||
|
// Check if we're on macOS
|
||||||
|
const userAgent = navigator.userAgent;
|
||||||
|
const isMac = userAgent.includes("Mac");
|
||||||
|
setIsMacOS(isMac);
|
||||||
|
|
||||||
|
if (isMac) {
|
||||||
|
loadPermissions().catch(console.error);
|
||||||
|
}
|
||||||
|
|
||||||
// Set up interval to check default browser status
|
// Set up interval to check default browser status
|
||||||
const intervalId = setInterval(() => {
|
const intervalId = setInterval(() => {
|
||||||
void checkDefaultBrowserStatus();
|
checkDefaultBrowserStatus().catch(console.error);
|
||||||
}, 500); // Check every 2 seconds
|
}, 500); // Check every 500ms
|
||||||
|
|
||||||
// Cleanup interval on component unmount or dialog close
|
// Cleanup interval on component unmount or dialog close
|
||||||
return () => {
|
return () => {
|
||||||
@@ -77,6 +114,32 @@ export function SettingsDialog({ isOpen, onClose }: SettingsDialogProps) {
|
|||||||
}
|
}
|
||||||
}, [isOpen]);
|
}, [isOpen]);
|
||||||
|
|
||||||
|
// Update permissions when the permission states change
|
||||||
|
useEffect(() => {
|
||||||
|
if (isMacOS) {
|
||||||
|
const permissionList: PermissionInfo[] = [
|
||||||
|
{
|
||||||
|
permission_type: "microphone",
|
||||||
|
isGranted: isMicrophoneAccessGranted,
|
||||||
|
description: getPermissionDescription("microphone"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
permission_type: "camera",
|
||||||
|
isGranted: isCameraAccessGranted,
|
||||||
|
description: getPermissionDescription("camera"),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
setPermissions(permissionList);
|
||||||
|
} else {
|
||||||
|
setPermissions([]);
|
||||||
|
}
|
||||||
|
}, [
|
||||||
|
isMacOS,
|
||||||
|
isMicrophoneAccessGranted,
|
||||||
|
isCameraAccessGranted,
|
||||||
|
getPermissionDescription,
|
||||||
|
]);
|
||||||
|
|
||||||
const loadSettings = async () => {
|
const loadSettings = async () => {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
try {
|
try {
|
||||||
@@ -90,6 +153,36 @@ export function SettingsDialog({ isOpen, onClose }: SettingsDialogProps) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const loadPermissions = async () => {
|
||||||
|
setIsLoadingPermissions(true);
|
||||||
|
try {
|
||||||
|
if (!isMacOS) {
|
||||||
|
// On non-macOS platforms, don't show permissions
|
||||||
|
setPermissions([]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const permissionList: PermissionInfo[] = [
|
||||||
|
{
|
||||||
|
permission_type: "microphone",
|
||||||
|
isGranted: isMicrophoneAccessGranted,
|
||||||
|
description: getPermissionDescription("microphone"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
permission_type: "camera",
|
||||||
|
isGranted: isCameraAccessGranted,
|
||||||
|
description: getPermissionDescription("camera"),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
setPermissions(permissionList);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to load permissions:", error);
|
||||||
|
} finally {
|
||||||
|
setIsLoadingPermissions(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const checkDefaultBrowserStatus = async () => {
|
const checkDefaultBrowserStatus = async () => {
|
||||||
try {
|
try {
|
||||||
const isDefault = await invoke<boolean>("is_default_browser");
|
const isDefault = await invoke<boolean>("is_default_browser");
|
||||||
@@ -127,6 +220,49 @@ export function SettingsDialog({ isOpen, onClose }: SettingsDialogProps) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleRequestPermission = async (permissionType: PermissionType) => {
|
||||||
|
setRequestingPermission(permissionType);
|
||||||
|
try {
|
||||||
|
await requestPermission(permissionType);
|
||||||
|
showSuccessToast(
|
||||||
|
`${getPermissionDisplayName(permissionType)} access requested`,
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to request permission:", error);
|
||||||
|
} finally {
|
||||||
|
setRequestingPermission(null);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getPermissionIcon = (type: PermissionType) => {
|
||||||
|
switch (type) {
|
||||||
|
case "microphone":
|
||||||
|
return <BsMic className="h-4 w-4" />;
|
||||||
|
case "camera":
|
||||||
|
return <BsCamera className="h-4 w-4" />;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getPermissionDisplayName = (type: PermissionType) => {
|
||||||
|
switch (type) {
|
||||||
|
case "microphone":
|
||||||
|
return "Microphone";
|
||||||
|
case "camera":
|
||||||
|
return "Camera";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getStatusBadge = (isGranted: boolean) => {
|
||||||
|
if (isGranted) {
|
||||||
|
return (
|
||||||
|
<Badge variant="default" className="bg-green-100 text-green-800">
|
||||||
|
Granted
|
||||||
|
</Badge>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return <Badge variant="secondary">Not Granted</Badge>;
|
||||||
|
};
|
||||||
|
|
||||||
const handleSave = async () => {
|
const handleSave = async () => {
|
||||||
setIsSaving(true);
|
setIsSaving(true);
|
||||||
try {
|
try {
|
||||||
@@ -204,7 +340,7 @@ export function SettingsDialog({ isOpen, onClose }: SettingsDialogProps) {
|
|||||||
<LoadingButton
|
<LoadingButton
|
||||||
isLoading={isSettingDefault}
|
isLoading={isSettingDefault}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
void handleSetDefaultBrowser();
|
handleSetDefaultBrowser().catch(console.error);
|
||||||
}}
|
}}
|
||||||
disabled={isDefaultBrowser}
|
disabled={isDefaultBrowser}
|
||||||
variant={isDefaultBrowser ? "outline" : "default"}
|
variant={isDefaultBrowser ? "outline" : "default"}
|
||||||
@@ -284,6 +420,69 @@ export function SettingsDialog({ isOpen, onClose }: SettingsDialogProps) {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Permissions Section - Only show on macOS */}
|
||||||
|
{isMacOS && (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<Label className="text-base font-medium">
|
||||||
|
System Permissions
|
||||||
|
</Label>
|
||||||
|
|
||||||
|
{isLoadingPermissions ? (
|
||||||
|
<div className="text-sm text-muted-foreground">
|
||||||
|
Loading permissions...
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="space-y-3">
|
||||||
|
{permissions.map((permission) => (
|
||||||
|
<div
|
||||||
|
key={permission.permission_type}
|
||||||
|
className="flex items-center justify-between p-3 border rounded-lg"
|
||||||
|
>
|
||||||
|
<div className="flex items-center space-x-3">
|
||||||
|
{getPermissionIcon(permission.permission_type)}
|
||||||
|
<div>
|
||||||
|
<div className="text-sm font-medium">
|
||||||
|
{getPermissionDisplayName(
|
||||||
|
permission.permission_type,
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="text-xs text-muted-foreground">
|
||||||
|
{permission.description}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
{getStatusBadge(permission.isGranted)}
|
||||||
|
{!permission.isGranted && (
|
||||||
|
<LoadingButton
|
||||||
|
size="sm"
|
||||||
|
isLoading={
|
||||||
|
requestingPermission ===
|
||||||
|
permission.permission_type
|
||||||
|
}
|
||||||
|
onClick={() => {
|
||||||
|
handleRequestPermission(
|
||||||
|
permission.permission_type,
|
||||||
|
).catch(console.error);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Grant
|
||||||
|
</LoadingButton>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
These permissions allow browsers launched from Donut Browser to
|
||||||
|
access system resources. Each website will still ask for your
|
||||||
|
permission individually.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Advanced Section */}
|
{/* Advanced Section */}
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<Label className="text-base font-medium">Advanced</Label>
|
<Label className="text-base font-medium">Advanced</Label>
|
||||||
@@ -291,7 +490,7 @@ export function SettingsDialog({ isOpen, onClose }: SettingsDialogProps) {
|
|||||||
<LoadingButton
|
<LoadingButton
|
||||||
isLoading={isClearingCache}
|
isLoading={isClearingCache}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
void handleClearCache();
|
handleClearCache().catch(console.error);
|
||||||
}}
|
}}
|
||||||
variant="outline"
|
variant="outline"
|
||||||
className="w-full"
|
className="w-full"
|
||||||
@@ -314,7 +513,7 @@ export function SettingsDialog({ isOpen, onClose }: SettingsDialogProps) {
|
|||||||
<LoadingButton
|
<LoadingButton
|
||||||
isLoading={isSaving}
|
isLoading={isSaving}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
void handleSave();
|
handleSave().catch(console.error);
|
||||||
}}
|
}}
|
||||||
disabled={isLoading || !hasChanges}
|
disabled={isLoading || !hasChanges}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -0,0 +1,174 @@
|
|||||||
|
import { useCallback, useEffect, useRef, useState } from "react";
|
||||||
|
|
||||||
|
// Platform-specific imports
|
||||||
|
let macOSPermissions:
|
||||||
|
| typeof import("tauri-plugin-macos-permissions-api")
|
||||||
|
| null = null;
|
||||||
|
|
||||||
|
// Dynamically import macOS permissions only when needed
|
||||||
|
const loadMacOSPermissions = async () => {
|
||||||
|
if (macOSPermissions) return macOSPermissions;
|
||||||
|
|
||||||
|
try {
|
||||||
|
macOSPermissions = await import("tauri-plugin-macos-permissions-api");
|
||||||
|
return macOSPermissions;
|
||||||
|
} catch (error) {
|
||||||
|
console.warn("Failed to load macOS permissions API:", error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export type PermissionType = "microphone" | "camera";
|
||||||
|
|
||||||
|
export interface UsePermissionsReturn {
|
||||||
|
requestPermission: (type: PermissionType) => Promise<void>;
|
||||||
|
isMicrophoneAccessGranted: boolean;
|
||||||
|
isCameraAccessGranted: boolean;
|
||||||
|
isInitialized: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function usePermissions(): UsePermissionsReturn {
|
||||||
|
const [isMicrophoneAccessGranted, setIsMicrophoneAccessGranted] =
|
||||||
|
useState(false);
|
||||||
|
const [isCameraAccessGranted, setIsCameraAccessGranted] = useState(false);
|
||||||
|
const [currentPlatform, setCurrentPlatform] = useState<string | null>(null);
|
||||||
|
const [isInitialized, setIsInitialized] = useState(false);
|
||||||
|
const intervalRef = useRef<NodeJS.Timeout | null>(null);
|
||||||
|
|
||||||
|
// Check permissions status
|
||||||
|
const checkPermissions = useCallback(async () => {
|
||||||
|
if (!currentPlatform) return;
|
||||||
|
|
||||||
|
if (currentPlatform !== "macos") {
|
||||||
|
// Windows/Linux - assume permissions are granted
|
||||||
|
setIsMicrophoneAccessGranted(true);
|
||||||
|
setIsCameraAccessGranted(true);
|
||||||
|
setIsInitialized(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// macOS - use the permissions API
|
||||||
|
try {
|
||||||
|
const permissions = await loadMacOSPermissions();
|
||||||
|
if (permissions) {
|
||||||
|
const [micGranted, camGranted] = await Promise.all([
|
||||||
|
permissions.checkMicrophonePermission(),
|
||||||
|
permissions.checkCameraPermission(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
setIsMicrophoneAccessGranted(micGranted);
|
||||||
|
setIsCameraAccessGranted(camGranted);
|
||||||
|
setIsInitialized(true);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to check permissions on macOS:", error);
|
||||||
|
setIsInitialized(true);
|
||||||
|
}
|
||||||
|
}, [currentPlatform]);
|
||||||
|
|
||||||
|
// Request permission
|
||||||
|
const requestPermission = useCallback(
|
||||||
|
async (type: PermissionType): Promise<void> => {
|
||||||
|
if (!currentPlatform || currentPlatform !== "macos") return;
|
||||||
|
|
||||||
|
// macOS - use the permissions API
|
||||||
|
try {
|
||||||
|
const permissions = await loadMacOSPermissions();
|
||||||
|
if (!permissions) return;
|
||||||
|
|
||||||
|
if (type === "microphone") {
|
||||||
|
await permissions.requestMicrophonePermission();
|
||||||
|
|
||||||
|
// Poll for permission status change
|
||||||
|
const pollMicPermission = async () => {
|
||||||
|
const granted = await permissions.checkMicrophonePermission();
|
||||||
|
setIsMicrophoneAccessGranted(granted);
|
||||||
|
|
||||||
|
if (!granted) {
|
||||||
|
setTimeout(() => {
|
||||||
|
void pollMicPermission();
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
await pollMicPermission();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type === "camera") {
|
||||||
|
await permissions.requestCameraPermission();
|
||||||
|
|
||||||
|
// Poll for permission status change
|
||||||
|
const pollCamPermission = async () => {
|
||||||
|
const granted = await permissions.checkCameraPermission();
|
||||||
|
setIsCameraAccessGranted(granted);
|
||||||
|
|
||||||
|
if (!granted) {
|
||||||
|
setTimeout(() => {
|
||||||
|
void pollCamPermission();
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
await pollCamPermission();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Failed to request ${type} permission on macOS:`, error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[currentPlatform],
|
||||||
|
);
|
||||||
|
|
||||||
|
// Initialize platform detection and start interval checking
|
||||||
|
useEffect(() => {
|
||||||
|
const initializePlatform = async () => {
|
||||||
|
try {
|
||||||
|
// Detect platform - on macOS we need permissions, on others we don't
|
||||||
|
const userAgent = navigator.userAgent;
|
||||||
|
let platformName = "unknown";
|
||||||
|
|
||||||
|
if (userAgent.includes("Mac")) {
|
||||||
|
platformName = "macos";
|
||||||
|
} else if (userAgent.includes("Win")) {
|
||||||
|
platformName = "windows";
|
||||||
|
} else if (userAgent.includes("Linux")) {
|
||||||
|
platformName = "linux";
|
||||||
|
}
|
||||||
|
|
||||||
|
setCurrentPlatform(platformName);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to detect platform:", error);
|
||||||
|
// Fallback - assume non-macOS
|
||||||
|
setCurrentPlatform("unknown");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
initializePlatform().catch(console.error);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Set up interval checking when platform is determined
|
||||||
|
useEffect(() => {
|
||||||
|
if (!currentPlatform) return;
|
||||||
|
|
||||||
|
// Initial check
|
||||||
|
void checkPermissions();
|
||||||
|
|
||||||
|
// Set up 500ms interval for checking permissions
|
||||||
|
intervalRef.current = setInterval(() => {
|
||||||
|
void checkPermissions();
|
||||||
|
}, 500);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (intervalRef.current) {
|
||||||
|
clearInterval(intervalRef.current);
|
||||||
|
intervalRef.current = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [currentPlatform, checkPermissions]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
requestPermission,
|
||||||
|
isMicrophoneAccessGranted,
|
||||||
|
isCameraAccessGranted,
|
||||||
|
isInitialized,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -40,3 +40,17 @@ export interface AppVersionInfo {
|
|||||||
version: string;
|
version: string;
|
||||||
is_nightly: boolean;
|
is_nightly: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type PermissionType = "microphone" | "camera" | "location";
|
||||||
|
|
||||||
|
export type PermissionStatus =
|
||||||
|
| "granted"
|
||||||
|
| "denied"
|
||||||
|
| "not_determined"
|
||||||
|
| "restricted";
|
||||||
|
|
||||||
|
export interface PermissionInfo {
|
||||||
|
permission_type: PermissionType;
|
||||||
|
status: PermissionStatus;
|
||||||
|
description: string;
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user