v3
can’t innovate my ass Signed-off-by: FTCHD <144691102+FTCHD@users.noreply.github.com>
|
After Width: | Height: | Size: 85 KiB |
|
After Width: | Height: | Size: 887 KiB |
@@ -24,19 +24,12 @@ jobs:
|
|||||||
- platform: 'ubuntu-22.04'
|
- platform: 'ubuntu-22.04'
|
||||||
args: ''
|
args: ''
|
||||||
assetPattern: '[name]_[arch][ext]'
|
assetPattern: '[name]_[arch][ext]'
|
||||||
variant: 'default'
|
|
||||||
- platform: 'ubuntu-22.04'
|
|
||||||
args: "--config '{\"bundle\":{\"createUpdaterArtifacts\":false}}'"
|
|
||||||
assetPattern: '[name]_[arch]_LEGACY[ext]'
|
|
||||||
variant: 'legacy'
|
|
||||||
- platform: 'ubuntu-24.04'
|
- platform: 'ubuntu-24.04'
|
||||||
args: "--config '{\"bundle\":{\"createUpdaterArtifacts\":false}}'"
|
args: "--config '{\"bundle\":{\"createUpdaterArtifacts\":false}}'"
|
||||||
assetPattern: '[name]_[arch]_ALTERNATE[ext]'
|
assetPattern: '[name]_[arch]_ALTERNATE[ext]'
|
||||||
variant: 'alternate'
|
|
||||||
- platform: 'ubuntu-22.04-arm'
|
- platform: 'ubuntu-22.04-arm'
|
||||||
args: ''
|
args: ''
|
||||||
assetPattern: '[name]_[arch][ext]'
|
assetPattern: '[name]_[arch][ext]'
|
||||||
variant: 'default'
|
|
||||||
- platform: 'windows-11-arm'
|
- platform: 'windows-11-arm'
|
||||||
args: '--target x86_64-pc-windows-msvc'
|
args: '--target x86_64-pc-windows-msvc'
|
||||||
arch: 'arm64'
|
arch: 'arm64'
|
||||||
@@ -81,16 +74,16 @@ jobs:
|
|||||||
uses: swatinem/rust-cache@v2
|
uses: swatinem/rust-cache@v2
|
||||||
with:
|
with:
|
||||||
workspaces: './src-tauri -> target'
|
workspaces: './src-tauri -> target'
|
||||||
key: ${{ runner.os }}-${{ runner.arch }}${{ matrix.variant && format('-{0}', matrix.variant) || '' }}
|
key: ${{ matrix.platform }}
|
||||||
|
|
||||||
- name: install dependencies (linux only)
|
- name: install dependencies (linux only)
|
||||||
if: matrix.variant == 'default' || matrix.variant == 'alternate'
|
if: matrix.platform == 'ubuntu-22.04-arm' || matrix.platform == 'ubuntu-24.04'
|
||||||
run: |
|
run: |
|
||||||
sudo apt-get update
|
sudo apt-get update
|
||||||
sudo apt-get install -y libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf
|
sudo apt-get install -y libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf
|
||||||
|
|
||||||
- name: install dependencies (linux legacy only)
|
- name: install dependencies (llinux only)
|
||||||
if: matrix.variant == 'legacy'
|
if: matrix.platform == 'ubuntu-22.04'
|
||||||
run: |
|
run: |
|
||||||
sudo apt-get update
|
sudo apt-get update
|
||||||
|
|
||||||
@@ -133,7 +126,7 @@ jobs:
|
|||||||
cargo install trusted-signing-cli
|
cargo install trusted-signing-cli
|
||||||
|
|
||||||
- name: Override linuxdeploy (linux legacy only)
|
- name: Override linuxdeploy (linux legacy only)
|
||||||
if: matrix.variant == 'legacy'
|
if: matrix.platform == 'ubuntu-22.04'
|
||||||
run: |
|
run: |
|
||||||
TAURI_TOOLKIT_PATH="${XDG_CACHE_HOME:-$HOME/.cache}/tauri"
|
TAURI_TOOLKIT_PATH="${XDG_CACHE_HOME:-$HOME/.cache}/tauri"
|
||||||
mkdir -p "$TAURI_TOOLKIT_PATH"
|
mkdir -p "$TAURI_TOOLKIT_PATH"
|
||||||
@@ -192,7 +185,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
tagName: v__VERSION__
|
tagName: v__VERSION__
|
||||||
releaseName: 'v__VERSION__'
|
releaseName: 'v__VERSION__'
|
||||||
releaseBody: "<img width=\"1645\" height=\"626\" alt=\"Rclone UI\" src=\"https://github.com/user-attachments/assets/539c0b90-fee1-487e-879a-6a9168a44508\" />\n\n# Rclone UI v__VERSION__\n- Bug squashing & improvements.\n- **See the assets to download this version and install.**\n\n# Downloads\nBelow you can download the main releases. \n\n**If you already have Rclone UI installed, you can update simply by opening the Settings menu.**\n\n| | Linux | Mac | Windows |\n|--------------|---------|--------|-----|\n| 64 bit | [deb](https://github.com/rclone-ui/rclone-ui/releases/download/v__VERSION__/Rclone.UI_amd64.deb) [AppImage](https://github.com/rclone-ui/rclone-ui/releases/download/v__VERSION__/Rclone.UI_amd64.AppImage) [RPM](https://github.com/rclone-ui/rclone-ui/releases/download/v__VERSION__/Rclone.UI_x86_64.rpm) | [Intel](https://github.com/rclone-ui/rclone-ui/releases/download/v__VERSION__/Rclone.UI_x64.dmg) | [x64](https://github.com/rclone-ui/rclone-ui/releases/download/v__VERSION__/Rclone.UI_x64.exe) |\n| ARM | [deb](https://github.com/rclone-ui/rclone-ui/releases/download/v__VERSION__/Rclone.UI_arm64.deb) [AppImage](https://github.com/rclone-ui/rclone-ui/releases/download/v__VERSION__/Rclone.UI_aarch64.AppImage) [RPM](https://github.com/rclone-ui/rclone-ui/releases/download/v__VERSION__/Rclone.UI_aarch64.rpm) | [Apple Silicon](https://github.com/rclone-ui/rclone-ui/releases/download/v__VERSION__/Rclone.UI_aarch64.dmg) | [Arm64](https://github.com/rclone-ui/rclone-ui/releases/download/v__VERSION__/Rclone.UI_arm64.exe) |\n\n\n# Linux (LEGACY)\nIf you have an older system or distro and the main build fails to run, you can try the ones below.\n\n| | deb | AppImage | RPM |\n|--------------|---------|--------|-----|\n| 64 bit | [Download](https://github.com/rclone-ui/rclone-ui/releases/download/v__VERSION__/Rclone.UI_amd64_LEGACY.deb) | [Download](https://github.com/rclone-ui/rclone-ui/releases/download/v__VERSION__/Rclone.UI_amd64_LEGACY.AppImage) | [Download](https://github.com/rclone-ui/rclone-ui/releases/download/v__VERSION__/Rclone.UI_x86_64_LEGACY.rpm) |\n\n\n# Linux (ALTERNATE)\nMainly for testing or if you're running a cutting edge distro.\n\n| | deb | AppImage | RPM |\n|--------------|---------|--------|-----|\n| 64 bit | [Download](https://github.com/rclone-ui/rclone-ui/releases/download/v__VERSION__/Rclone.UI_amd64_ALTERNATE.deb) | [Download](https://github.com/rclone-ui/rclone-ui/releases/download/v__VERSION__/Rclone.UI_amd64_ALTERNATE.AppImage) | [Download](https://github.com/rclone-ui/rclone-ui/releases/download/v__VERSION__/Rclone.UI_x86_64_ALTERNATE.rpm) |"
|
releaseBody: "<img width=\"1645\" height=\"626\" alt=\"Rclone UI\" src=\"https://github.com/user-attachments/assets/539c0b90-fee1-487e-879a-6a9168a44508\" />\n\n# Rclone UI v__VERSION__\n- Bug squashing & improvements.\n- **See the assets to download this version and install.**\n\n# Downloads\nBelow you can download the main releases. \n\n**If you already have Rclone UI installed, you can update simply by opening the Settings menu.**\n\n| | Linux | Mac | Windows |\n|--------------|---------|--------|-----|\n| 64 bit | [deb](https://github.com/rclone-ui/rclone-ui/releases/download/v__VERSION__/Rclone.UI_amd64.deb) [AppImage](https://github.com/rclone-ui/rclone-ui/releases/download/v__VERSION__/Rclone.UI_amd64.AppImage) [RPM](https://github.com/rclone-ui/rclone-ui/releases/download/v__VERSION__/Rclone.UI_x86_64.rpm) | [Intel](https://github.com/rclone-ui/rclone-ui/releases/download/v__VERSION__/Rclone.UI_x64.dmg) | [x64](https://github.com/rclone-ui/rclone-ui/releases/download/v__VERSION__/Rclone.UI_x64.exe) |\n| ARM | [deb](https://github.com/rclone-ui/rclone-ui/releases/download/v__VERSION__/Rclone.UI_arm64.deb) [AppImage](https://github.com/rclone-ui/rclone-ui/releases/download/v__VERSION__/Rclone.UI_aarch64.AppImage) [RPM](https://github.com/rclone-ui/rclone-ui/releases/download/v__VERSION__/Rclone.UI_aarch64.rpm) | [Apple Silicon](https://github.com/rclone-ui/rclone-ui/releases/download/v__VERSION__/Rclone.UI_aarch64.dmg) | [Arm64](https://github.com/rclone-ui/rclone-ui/releases/download/v__VERSION__/Rclone.UI_arm64.exe) |\n\n\n# Linux (ALTERNATE)\nMainly for testing or if you're running a cutting edge distro.\n\n| | deb | AppImage | RPM |\n|--------------|---------|--------|-----|\n| 64 bit | [Download](https://github.com/rclone-ui/rclone-ui/releases/download/v__VERSION__/Rclone.UI_amd64_ALTERNATE.deb) | [Download](https://github.com/rclone-ui/rclone-ui/releases/download/v__VERSION__/Rclone.UI_amd64_ALTERNATE.AppImage) | [Download](https://github.com/rclone-ui/rclone-ui/releases/download/v__VERSION__/Rclone.UI_x86_64_ALTERNATE.rpm) |"
|
||||||
assetNamePattern: ${{ matrix.assetPattern }}
|
assetNamePattern: ${{ matrix.assetPattern }}
|
||||||
releaseDraft: true
|
releaseDraft: true
|
||||||
prerelease: false
|
prerelease: false
|
||||||
|
|||||||
@@ -1,3 +1,8 @@
|
|||||||
|
<img src=".github/rclone-mobile.png" alt="Rclone Mobile" width="100%">
|
||||||
|
|
||||||
|
<br />
|
||||||
|
<br />
|
||||||
|
|
||||||
<h1 align="center">
|
<h1 align="center">
|
||||||
<a href="https://rcloneui.com">
|
<a href="https://rcloneui.com">
|
||||||
<img src="./public/banner.png" alt="Rclone UI" width="100%">
|
<img src="./public/banner.png" alt="Rclone UI" width="100%">
|
||||||
@@ -50,28 +55,50 @@
|
|||||||
|
|
||||||
<br />
|
<br />
|
||||||
|
|
||||||
## Features
|
<a href="https://get.rcloneui.com/showcase">
|
||||||
### 🖥️ Cross platform running on Windows, Linux, macOS (x64, Arm and Apple Silicon)
|
<img src=".github/rclone-video.png" alt="The GUI for Rclone">
|
||||||
### ✅ Works with your existing rclone installation (or can install rclone for you) and keeps it updated (with your approval)
|
</a>
|
||||||
### 🔗 Add/Edit/Remove Remotes
|
|
||||||
### ⚡️ Sync, BiSync, Bisync, Copy, Move, Delete, Purge, Serve, Mount, etc
|
|
||||||
### ⭐️ Save time by setting default flags for each Remote separately, for each operation (Mount, Sync, Copy, etc)
|
|
||||||
### 🎛️ Customize the flags (parameters) when running an operation, on a case by case basis
|
|
||||||
### 📚 Embedded documentation for each flag, no need to google what everything does!
|
|
||||||
### 📂 File Browser
|
|
||||||
### 📡 Proxy
|
|
||||||
|
|
||||||
<img src=".github/rclone-1.png" alt="Rclone UI">
|
## Docker/Homelab/Server Usage
|
||||||
|
Control your server, homelab, or mom's PC with **the easiest solution to manage remote **`rclone`** instances.**
|
||||||
|
|
||||||
### ⏰ Scheduled Tasks (cron)
|
#### Docker Compose
|
||||||
### 📊 Job Logs (visualize all background operations)
|
```yaml
|
||||||
### 🚀 Start on boot (autostart) and Mount on start (automount)
|
services:
|
||||||
### 🔒 Set a password for the Settings panel
|
rclone:
|
||||||
### 👁️ Hide commands or Remotes from the tray menu
|
image: rclone/rclone
|
||||||
### ⚙️ Import/Export Configs (encrypted with pass or command)
|
container_name: rclone
|
||||||
### ✍️ Signed by Microsoft and Notarized on macOS
|
command: rcd --rc-addr=0.0.0.0:5572 --rc-no-auth
|
||||||
|
ports:
|
||||||
|
- 5572:5572
|
||||||
|
volumes:
|
||||||
|
- ./config:/config/rclone
|
||||||
|
- /path/to/data:/data
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Docker CLI
|
||||||
|
```bash
|
||||||
|
docker run -d \
|
||||||
|
--name rclone \
|
||||||
|
-p 5572:5572 \
|
||||||
|
-v ./config:/config/rclone \
|
||||||
|
-v /path/to/data:/data \
|
||||||
|
rclone/rclone rcd --rc-addr=0.0.0.0:5572 --rc-no-auth
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Without Docker
|
||||||
|
Just start the `rcd` daemon directly:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
rclone rcd --rc-addr=0.0.0.0:5572 --rc-no-auth
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Notes
|
||||||
|
- After starting up **`rclone`** using your preferred method, simply open Rclone UI and navigate to Settings > Hosts.
|
||||||
|
- Make sure to allow traffic to port **`5572`** in your firewall and/or reverse proxy (nginx/caddy/traefik).
|
||||||
|
- Rclone UI can connect to any RCD port, so you can customize the default **`5572`** port.
|
||||||
|
- Use **`--rc-user`** and **`--rc-pass`** instead of **`--rc-no-auth`** in production.
|
||||||
|
|
||||||
<img src=".github/rclone-2.png" alt="Rclone UI">
|
|
||||||
|
|
||||||
## Package Managers
|
## Package Managers
|
||||||
- Flathub **`flatpak install com.rcloneui.RcloneUI`** or **[from the store](https://flathub.org/en/apps/com.rcloneui.RcloneUI)**
|
- Flathub **`flatpak install com.rcloneui.RcloneUI`** or **[from the store](https://flathub.org/en/apps/com.rcloneui.RcloneUI)**
|
||||||
@@ -79,6 +106,7 @@
|
|||||||
- Scoop **`scoop bucket add extras`** & **`scoop install rclone-ui`**
|
- Scoop **`scoop bucket add extras`** & **`scoop install rclone-ui`**
|
||||||
- Chocolatey **`choco install rclone-ui`**
|
- Chocolatey **`choco install rclone-ui`**
|
||||||
- WinGet **`winget install --id=RcloneUI.RcloneUI -e`**
|
- WinGet **`winget install --id=RcloneUI.RcloneUI -e`**
|
||||||
|
- NPM **`npx rclone-ui`**
|
||||||
|
|
||||||
## Downloads
|
## Downloads
|
||||||
- **Windows** (**[Arm](https://get.rcloneui.com/win-arm)**, **[x64](https://get.rcloneui.com/win)**)
|
- **Windows** (**[Arm](https://get.rcloneui.com/win-arm)**, **[x64](https://get.rcloneui.com/win)**)
|
||||||
@@ -100,7 +128,15 @@
|
|||||||
</a>
|
</a>
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
We welcome your contributions! Please open an [issue](https://github.com/FTCHD/rclone-ui/issues/new?template=Blank+issue) or a [PR](https://github.com/FTCHD/rclone-ui/compare), and let's make this better together 🎉🎉
|
Welcome, anon. We’ve been expecting you.
|
||||||
|
|
||||||
|
Here are some good problems to tackle:
|
||||||
|
- Fix an open [**Issue**](https://github.com/rclone-ui/rclone-ui/issues)
|
||||||
|
- Upgrade repository to Vite 7 & React 19
|
||||||
|
- Introduce React Compiler
|
||||||
|
- Move Cron logic to Rust
|
||||||
|
|
||||||
|
🎁 **Merged PRs receive a Lifetime License!**
|
||||||
|
|
||||||
<br />
|
<br />
|
||||||
|
|
||||||
|
|||||||
@@ -50,28 +50,49 @@
|
|||||||
|
|
||||||
<br />
|
<br />
|
||||||
|
|
||||||
## 功能
|
<a href="https://get.rcloneui.com/showcase">
|
||||||
### 🖥️ 跨平台:Windows、Linux、macOS(x64、Arm、Apple Silicon)
|
<img src=".github/rclone-video.png" alt="The GUI for Rclone">
|
||||||
### ✅ 可配合你现有的 rclone 安装使用(也可为你安装),并在你确认后保持 up-to-date
|
</a>
|
||||||
### 🔗 添加 / 编辑 / 移除 Remotes
|
|
||||||
### ⚡️ Sync、Copy、Move、Delete、Purge、Serve、Mount 等
|
|
||||||
### ⭐️ 为每个 Remote、每种操作(Mount、Sync、Copy 等)分别设置默认参数(flags),节省时间
|
|
||||||
### 🎛️ 按需为每次操作自定义参数(flags)
|
|
||||||
### 📚 内置每个参数的说明,无需再 Google 查询!
|
|
||||||
### 📂 文件浏览器
|
|
||||||
### 📡 代理
|
|
||||||
|
|
||||||
<img src=".github/rclone-1.png" alt="Rclone UI">
|
## Docker/Homelab/服务器使用
|
||||||
|
使用 **最简单的方式** 管理远程 **`rclone`** 实例,控制你的服务器、Homelab 或家人的电脑。
|
||||||
|
|
||||||
### ⏰ 计划任务(cron)
|
#### Docker Compose
|
||||||
### 📊 任务日志(可视化所有后台操作)
|
```yaml
|
||||||
### 🚀 开机自启(autostart)与启动自动挂载(automount)
|
services:
|
||||||
### 🔒 为设置面板设置密码
|
rclone:
|
||||||
### 👁️ 可在托盘菜单中隐藏命令或 Remotes
|
image: rclone/rclone
|
||||||
### ⚙️ 导入 / 导出配置(可用 pass 或 command 加密)
|
container_name: rclone
|
||||||
### ✍️ 由 Microsoft 签名,macOS 已完成公证(Notarized)
|
command: rcd --rc-addr=0.0.0.0:5572 --rc-no-auth
|
||||||
|
ports:
|
||||||
|
- 5572:5572
|
||||||
|
volumes:
|
||||||
|
- ./config:/config/rclone
|
||||||
|
- /path/to/data:/data
|
||||||
|
```
|
||||||
|
|
||||||
<img src=".github/rclone-2.png" alt="Rclone UI">
|
#### Docker CLI
|
||||||
|
```bash
|
||||||
|
docker run -d \
|
||||||
|
--name rclone \
|
||||||
|
-p 5572:5572 \
|
||||||
|
-v ./config:/config/rclone \
|
||||||
|
-v /path/to/data:/data \
|
||||||
|
rclone/rclone rcd --rc-addr=0.0.0.0:5572 --rc-no-auth
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 不使用 Docker
|
||||||
|
直接启动 `rcd` 守护进程:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
rclone rcd --rc-addr=0.0.0.0:5572 --rc-no-auth
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 注意事项
|
||||||
|
- 使用你喜欢的方式启动 **`rclone`** 后,打开 Rclone UI 并导航到 Settings > Hosts。
|
||||||
|
- 确保在防火墙和/或反向代理(nginx/caddy/traefik)中允许端口 **`5572`** 的流量。
|
||||||
|
- Rclone UI 可以连接到任意 RCD 端口,你可以自定义默认的 **`5572`** 端口。
|
||||||
|
- 在生产环境中使用 **`--rc-user`** 和 **`--rc-pass`** 而不是 **`--rc-no-auth`**。
|
||||||
|
|
||||||
## 包管理器
|
## 包管理器
|
||||||
- Flathub **`flatpak install com.rcloneui.RcloneUI`** 或 **[从商店获取](https://flathub.org/en/apps/com.rcloneui.RcloneUI)**
|
- Flathub **`flatpak install com.rcloneui.RcloneUI`** 或 **[从商店获取](https://flathub.org/en/apps/com.rcloneui.RcloneUI)**
|
||||||
@@ -79,6 +100,7 @@
|
|||||||
- Scoop **`scoop bucket add extras`** & **`scoop install rclone-ui`**
|
- Scoop **`scoop bucket add extras`** & **`scoop install rclone-ui`**
|
||||||
- Chocolatey **`choco install rclone-ui`**
|
- Chocolatey **`choco install rclone-ui`**
|
||||||
- WinGet **`winget install --id=RcloneUI.RcloneUI -e`**
|
- WinGet **`winget install --id=RcloneUI.RcloneUI -e`**
|
||||||
|
- NPM **`npx rclone-ui`**
|
||||||
|
|
||||||
## 下载
|
## 下载
|
||||||
- **Windows**(**[Arm](https://get.rcloneui.com/win-arm)**、**[x64](https://get.rcloneui.com/win)**)
|
- **Windows**(**[Arm](https://get.rcloneui.com/win-arm)**、**[x64](https://get.rcloneui.com/win)**)
|
||||||
@@ -100,7 +122,15 @@
|
|||||||
</a>
|
</a>
|
||||||
|
|
||||||
## 参与贡献
|
## 参与贡献
|
||||||
欢迎你的贡献!请提交一个 [issue](https://github.com/FTCHD/rclone-ui/issues/new?template=Blank+issue) 或 [PR](https://github.com/FTCHD/rclone-ui/compare),让我们一起把它做得更好 🎉🎉
|
欢迎,anon。我们一直在等你。
|
||||||
|
|
||||||
|
以下是一些值得解决的问题:
|
||||||
|
- 修复一个 [**Issue**](https://github.com/rclone-ui/rclone-ui/issues)
|
||||||
|
- 将仓库升级到 Vite 7 和 React 19
|
||||||
|
- 引入 React Compiler
|
||||||
|
- 将 Cron 逻辑迁移到 Rust
|
||||||
|
|
||||||
|
🎁 **合并的 PR 可获终身许可证!**
|
||||||
|
|
||||||
<br />
|
<br />
|
||||||
|
|
||||||
|
|||||||
@@ -50,28 +50,49 @@
|
|||||||
|
|
||||||
<br />
|
<br />
|
||||||
|
|
||||||
## Features
|
<a href="https://get.rcloneui.com/showcase">
|
||||||
### 🖥️ Cross-platform: Windows, Linux, macOS (x64, Arm und Apple Silicon)
|
<img src=".github/rclone-video.png" alt="The GUI for Rclone">
|
||||||
### ✅ Funktioniert mit deiner bestehenden rclone-Installation (oder kann rclone für dich installieren) und hält sie auf Wunsch aktuell
|
</a>
|
||||||
### 🔗 Remotes hinzufügen/bearbeiten/entfernen
|
|
||||||
### ⚡️ Sync, Bisync, Copy, Move, Delete, Purge, Serve, Mount, etc.
|
|
||||||
### ⭐️ Zeit sparen durch Default Flags pro Remote und pro Operation (Mount, Sync, Copy, etc.)
|
|
||||||
### 🎛️ Flags (Parameter) bei Bedarf pro Run/Case anpassen
|
|
||||||
### 📚 Eingebettete Doku zu jedem Flag – kein Googeln nötig!
|
|
||||||
### 📂 File Browser
|
|
||||||
### 📡 Proxy
|
|
||||||
|
|
||||||
<img src=".github/rclone-1.png" alt="Rclone UI">
|
## Docker/Homelab/Server-Nutzung
|
||||||
|
Steuere deinen Server, dein Homelab oder den PC deiner Mutter mit **der einfachsten Lösung, Remote-**`rclone`**-Instanzen zu verwalten.**
|
||||||
|
|
||||||
### ⏰ Scheduled Tasks (cron)
|
#### Docker Compose
|
||||||
### 📊 Job Logs (alle Hintergrund-Operationen visualisieren)
|
```yaml
|
||||||
### 🚀 Start on boot (Autostart) & Mount on start (Automount)
|
services:
|
||||||
### 🔒 Passwort für das Settings Panel setzen
|
rclone:
|
||||||
### 👁️ Commands oder Remotes im Tray Menu ausblenden
|
image: rclone/rclone
|
||||||
### ⚙️ Import/Export Configs (encrypted with <tt>pass</tt> oder <tt>command</tt>)
|
container_name: rclone
|
||||||
### ✍️ Signed by Microsoft & Notarized on macOS
|
command: rcd --rc-addr=0.0.0.0:5572 --rc-no-auth
|
||||||
|
ports:
|
||||||
|
- 5572:5572
|
||||||
|
volumes:
|
||||||
|
- ./config:/config/rclone
|
||||||
|
- /path/to/data:/data
|
||||||
|
```
|
||||||
|
|
||||||
<img src=".github/rclone-2.png" alt="Rclone UI">
|
#### Docker CLI
|
||||||
|
```bash
|
||||||
|
docker run -d \
|
||||||
|
--name rclone \
|
||||||
|
-p 5572:5572 \
|
||||||
|
-v ./config:/config/rclone \
|
||||||
|
-v /path/to/data:/data \
|
||||||
|
rclone/rclone rcd --rc-addr=0.0.0.0:5572 --rc-no-auth
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Ohne Docker
|
||||||
|
Starte den `rcd`-Daemon einfach direkt:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
rclone rcd --rc-addr=0.0.0.0:5572 --rc-no-auth
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Hinweise
|
||||||
|
- Nachdem du **`rclone`** mit deiner bevorzugten Methode gestartet hast, öffne Rclone UI und navigiere zu Settings > Hosts.
|
||||||
|
- Stelle sicher, dass der Datenverkehr auf Port **`5572`** in deiner Firewall und/oder deinem Reverse Proxy (nginx/caddy/traefik) erlaubt ist.
|
||||||
|
- Rclone UI kann sich mit jedem RCD-Port verbinden, sodass du den Standard-Port **`5572`** anpassen kannst.
|
||||||
|
- Verwende **`--rc-user`** und **`--rc-pass`** anstelle von **`--rc-no-auth`** in der Produktion.
|
||||||
|
|
||||||
## Package Managers
|
## Package Managers
|
||||||
- Flathub **`flatpak install com.rcloneui.RcloneUI`** oder **[aus dem Store](https://flathub.org/en/apps/com.rcloneui.RcloneUI)**
|
- Flathub **`flatpak install com.rcloneui.RcloneUI`** oder **[aus dem Store](https://flathub.org/en/apps/com.rcloneui.RcloneUI)**
|
||||||
@@ -79,6 +100,7 @@
|
|||||||
- Scoop **`scoop bucket add extras`** & **`scoop install rclone-ui`**
|
- Scoop **`scoop bucket add extras`** & **`scoop install rclone-ui`**
|
||||||
- Chocolatey **`choco install rclone-ui`**
|
- Chocolatey **`choco install rclone-ui`**
|
||||||
- WinGet **`winget install --id=RcloneUI.RcloneUI -e`**
|
- WinGet **`winget install --id=RcloneUI.RcloneUI -e`**
|
||||||
|
- NPM **`npx rclone-ui`**
|
||||||
|
|
||||||
## Downloads
|
## Downloads
|
||||||
- **Windows** (**[Arm](https://get.rcloneui.com/win-arm)**, **[x64](https://get.rcloneui.com/win)**)
|
- **Windows** (**[Arm](https://get.rcloneui.com/win-arm)**, **[x64](https://get.rcloneui.com/win)**)
|
||||||
@@ -100,7 +122,15 @@
|
|||||||
</a>
|
</a>
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
Wir freuen uns über Beiträge! Öffne einfach ein [issue](https://github.com/FTCHD/rclone-ui/issues/new?template=Blank+issue) oder einen [PR](https://github.com/FTCHD/rclone-ui/compare) – lass uns das gemeinsam besser machen 🎉🎉
|
Willkommen, anon. Wir haben dich bereits erwartet.
|
||||||
|
|
||||||
|
Hier ein paar spannende Aufgaben:
|
||||||
|
- Behebe ein offenes [**Issue**](https://github.com/rclone-ui/rclone-ui/issues)
|
||||||
|
- Hebe das Repository auf Vite 7 & React 19 an
|
||||||
|
- Führe den React Compiler ein
|
||||||
|
- Verlege die Cron-Logik nach Rust
|
||||||
|
|
||||||
|
🎁 **Für gemergte PRs gibt's eine Lifetime-Lizenz!**
|
||||||
|
|
||||||
<br />
|
<br />
|
||||||
|
|
||||||
|
|||||||
@@ -50,28 +50,49 @@
|
|||||||
|
|
||||||
<br />
|
<br />
|
||||||
|
|
||||||
## 機能
|
<a href="https://get.rcloneui.com/showcase">
|
||||||
### 🖥️ Cross-platform:Windows・Linux・macOS(x64 / Arm / Apple Silicon)
|
<img src=".github/rclone-video.png" alt="The GUI for Rclone">
|
||||||
### ✅ 既存の rclone 環境でそのまま動作(必要ならインストールも可能)。アップデートはあなたの承認のもとで適用
|
</a>
|
||||||
### 🔗 Remote の追加/編集/削除
|
|
||||||
### ⚡️ Sync・Copy・Move・Delete・Purge・Serve・Mount など
|
|
||||||
### ⭐️ 操作ごと(Mount/Sync/Copy など)に Remote 単位で default flags を設定して時短
|
|
||||||
### 🎛️ 実行時にケースバイケースで flags(パラメータ)をカスタマイズ
|
|
||||||
### 📚 各 flag のドキュメントを内蔵。ググる手間はもう不要!
|
|
||||||
### 📂 File Browser
|
|
||||||
### 📡 Proxy
|
|
||||||
|
|
||||||
<img src=".github/rclone-1.png" alt="Rclone UI">
|
## Docker/Homelab/サーバー利用
|
||||||
|
**リモート **`rclone`** インスタンスを管理する最も簡単な方法**で、サーバー、Homelab、家族の PC を管理しましょう。
|
||||||
|
|
||||||
### ⏰ スケジュール実行(cron)
|
#### Docker Compose
|
||||||
### 📊 Job Logs(すべてのバックグラウンド処理を可視化)
|
```yaml
|
||||||
### 🚀 OS 起動時に自動起動(autostart)・自動マウント(automount)
|
services:
|
||||||
### 🔒 Settings パネルにパスワードを設定
|
rclone:
|
||||||
### 👁️ トレイメニューからコマンドや Remote を非表示に
|
image: rclone/rclone
|
||||||
### ⚙️ 設定のインポート/エクスポート(`pass` または `command` で暗号化)
|
container_name: rclone
|
||||||
### ✍️ Windows は Microsoft 署名済み、macOS は公証(Notarized)済み
|
command: rcd --rc-addr=0.0.0.0:5572 --rc-no-auth
|
||||||
|
ports:
|
||||||
|
- 5572:5572
|
||||||
|
volumes:
|
||||||
|
- ./config:/config/rclone
|
||||||
|
- /path/to/data:/data
|
||||||
|
```
|
||||||
|
|
||||||
<img src=".github/rclone-2.png" alt="Rclone UI">
|
#### Docker CLI
|
||||||
|
```bash
|
||||||
|
docker run -d \
|
||||||
|
--name rclone \
|
||||||
|
-p 5572:5572 \
|
||||||
|
-v ./config:/config/rclone \
|
||||||
|
-v /path/to/data:/data \
|
||||||
|
rclone/rclone rcd --rc-addr=0.0.0.0:5572 --rc-no-auth
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Docker なしで使う
|
||||||
|
`rcd` デーモンを直接起動してください:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
rclone rcd --rc-addr=0.0.0.0:5572 --rc-no-auth
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 注意事項
|
||||||
|
- お好みの方法で **`rclone`** を起動したら、Rclone UI を開き Settings > Hosts に移動します。
|
||||||
|
- ファイアウォールやリバースプロキシ(nginx/caddy/traefik)でポート **`5572`** へのトラフィックを許可してください。
|
||||||
|
- Rclone UI は任意の RCD ポートに接続できるので、デフォルトの **`5572`** ポートをカスタマイズできます。
|
||||||
|
- 本番環境では **`--rc-no-auth`** の代わりに **`--rc-user`** と **`--rc-pass`** を使用してください。
|
||||||
|
|
||||||
## パッケージマネージャー
|
## パッケージマネージャー
|
||||||
- Flathub **`flatpak install com.rcloneui.RcloneUI`** または **[ストアから](https://flathub.org/en/apps/com.rcloneui.RcloneUI)**
|
- Flathub **`flatpak install com.rcloneui.RcloneUI`** または **[ストアから](https://flathub.org/en/apps/com.rcloneui.RcloneUI)**
|
||||||
@@ -79,6 +100,7 @@
|
|||||||
- Scoop **`scoop bucket add extras`** と **`scoop install rclone-ui`**
|
- Scoop **`scoop bucket add extras`** と **`scoop install rclone-ui`**
|
||||||
- Chocolatey **`choco install rclone-ui`**
|
- Chocolatey **`choco install rclone-ui`**
|
||||||
- WinGet **`winget install --id=RcloneUI.RcloneUI -e`**
|
- WinGet **`winget install --id=RcloneUI.RcloneUI -e`**
|
||||||
|
- NPM **`npx rclone-ui`**
|
||||||
|
|
||||||
## ダウンロード
|
## ダウンロード
|
||||||
- **Windows**(**[Arm](https://get.rcloneui.com/win-arm)**、**[x64](https://get.rcloneui.com/win)**)
|
- **Windows**(**[Arm](https://get.rcloneui.com/win-arm)**、**[x64](https://get.rcloneui.com/win)**)
|
||||||
@@ -100,7 +122,15 @@
|
|||||||
</a>
|
</a>
|
||||||
|
|
||||||
## コントリビューション
|
## コントリビューション
|
||||||
貢献は大歓迎です!気軽に [issue](https://github.com/FTCHD/rclone-ui/issues/new?template=Blank+issue) を作成するか [PR](https://github.com/FTCHD/rclone-ui/compare) を送ってください。いっしょにもっと良いプロジェクトにしていきましょう 🎉🎉
|
ようこそ、anon。お待ちしていました。
|
||||||
|
|
||||||
|
取り組むと良い課題はこちらです:
|
||||||
|
- オープンな [**Issue**](https://github.com/rclone-ui/rclone-ui/issues) を修正する
|
||||||
|
- リポジトリを Vite 7 と React 19 にアップグレードする
|
||||||
|
- React Compiler を導入する
|
||||||
|
- Cron ロジックを Rust に移行する
|
||||||
|
|
||||||
|
🎁 **マージされた PR にはライフタイムライセンスを贈呈!**
|
||||||
|
|
||||||
<br />
|
<br />
|
||||||
|
|
||||||
|
|||||||
@@ -50,28 +50,49 @@
|
|||||||
|
|
||||||
<br />
|
<br />
|
||||||
|
|
||||||
## Features
|
<a href="https://get.rcloneui.com/showcase">
|
||||||
### 🖥️ Runs on all decks — Windows, Linux, macOS (x64, Arm, an’ Apple Silicon)
|
<img src=".github/rclone-video.png" alt="The GUI for Rclone">
|
||||||
### ✅ Sails with yer existing rclone (or we’ll fetch it fer ye) an’ keeps it shipshape (with yer aye-aye)
|
</a>
|
||||||
### 🔗 Add/Edit/Remove (Scuttle) Remotes
|
|
||||||
### ⚡️ Sync, Bisync, Copy, Move, Delete, Purge, Serve, Mount—pillagin’ made easy
|
|
||||||
### ⭐️ Save precious time by settin’ default flags per Remote, per operation (Mount, Sync, Copy, an’ the like)
|
|
||||||
### 🎛️ Tweak the flags (parrrrrameters) fer each voyage as ye see fit
|
|
||||||
### 📚 Flag lore be embedded—no need t’ consult the oracle o’ Google!
|
|
||||||
### 📂 File Hold—er, Browser—fer rummagin’ through yer treasure
|
|
||||||
### 📡 Proxy—send yer packets through the fog
|
|
||||||
|
|
||||||
<img src=".github/rclone-1.png" alt="Rclone UI">
|
## Docker/Homelab/Server, Ye Scallywag
|
||||||
|
Command yer server, homelab, or yer mum's vessel with **the simplest way t' manage remote **`rclone`** instances, arrr!**
|
||||||
|
|
||||||
### ⏰ Scheduled Raids (cron)
|
#### Docker Compose
|
||||||
### 📊 Ship’s Log fer Jobs (spy all background maneuvers)
|
```yaml
|
||||||
### 🚀 Weigh anchor on boot (autostart) an’ auto-mount the drives
|
services:
|
||||||
### 🔒 Guard the Captain’s Quarters (Settings) with a secret word
|
rclone:
|
||||||
### 👁️ Keep commands or Remotes hidden from the crow’s-nest tray
|
image: rclone/rclone
|
||||||
### ⚙️ Import/Export Maps (configs) — encrypt ’em with pass or command
|
container_name: rclone
|
||||||
### ✍️ Signed by the Crown o’ Microsoft an’ Notarized on macOS—legit as a Letter o’ Marque
|
command: rcd --rc-addr=0.0.0.0:5572 --rc-no-auth
|
||||||
|
ports:
|
||||||
|
- 5572:5572
|
||||||
|
volumes:
|
||||||
|
- ./config:/config/rclone
|
||||||
|
- /path/to/data:/data
|
||||||
|
```
|
||||||
|
|
||||||
<img src=".github/rclone-2.png" alt="Rclone UI">
|
#### Docker CLI
|
||||||
|
```bash
|
||||||
|
docker run -d \
|
||||||
|
--name rclone \
|
||||||
|
-p 5572:5572 \
|
||||||
|
-v ./config:/config/rclone \
|
||||||
|
-v /path/to/data:/data \
|
||||||
|
rclone/rclone rcd --rc-addr=0.0.0.0:5572 --rc-no-auth
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Without Docker, Ye Landlubber
|
||||||
|
Just fire up th' `rcd` daemon directly:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
rclone rcd --rc-addr=0.0.0.0:5572 --rc-no-auth
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Notes from the Captain
|
||||||
|
- After hoisting **`rclone`** using yer preferred method, open Rclone UI an' sail to Settings > Hosts.
|
||||||
|
- Make certain yer firewall an' reverse proxy (nginx/caddy/traefik) be allowin' traffic on port **`5572`**.
|
||||||
|
- Rclone UI can dock at any RCD port, so ye can customize the default **`5572`** port as ye please.
|
||||||
|
- Use **`--rc-user`** an' **`--rc-pass`** instead o' **`--rc-no-auth`** in production waters, matey.
|
||||||
|
|
||||||
## Package Mangarrs
|
## Package Mangarrs
|
||||||
- Flathub **`flatpak install com.rcloneui.RcloneUI`** or **[from th' store](https://flathub.org/en/apps/com.rcloneui.RcloneUI)**
|
- Flathub **`flatpak install com.rcloneui.RcloneUI`** or **[from th' store](https://flathub.org/en/apps/com.rcloneui.RcloneUI)**
|
||||||
@@ -79,6 +100,7 @@
|
|||||||
- Scoop **`scoop bucket add extras`** & **`scoop install rclone-ui`**
|
- Scoop **`scoop bucket add extras`** & **`scoop install rclone-ui`**
|
||||||
- Chocolatey **`choco install rclone-ui`**
|
- Chocolatey **`choco install rclone-ui`**
|
||||||
- WinGet **`winget install --id=RcloneUI.RcloneUI -e`**
|
- WinGet **`winget install --id=RcloneUI.RcloneUI -e`**
|
||||||
|
- NPM **`npx rclone-ui`**
|
||||||
|
|
||||||
## Downloads (hoist the booty!)
|
## Downloads (hoist the booty!)
|
||||||
- **Windows** (**[Arm](https://get.rcloneui.com/win-arm)**, **[x64](https://get.rcloneui.com/win)**)
|
- **Windows** (**[Arm](https://get.rcloneui.com/win-arm)**, **[x64](https://get.rcloneui.com/win)**)
|
||||||
@@ -99,8 +121,16 @@
|
|||||||
</picture>
|
</picture>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
## Contribootin’
|
## Contribootin'
|
||||||
We welcome yer code booty! Hoist an [issue](https://github.com/FTCHD/rclone-ui/issues/new?template=Blank+issue) or fire a [PR broadside](https://github.com/FTCHD/rclone-ui/compare), an’ let’s make this vessel swifter together 🎉🏴☠️
|
Ahoy, matey. We been awaitin' yer arrival.
|
||||||
|
|
||||||
|
Some fine plunder t' tackle:
|
||||||
|
- Patch an open [**Issue**](https://github.com/rclone-ui/rclone-ui/issues)
|
||||||
|
- Hoist th' repo t' Vite 7 & React 19
|
||||||
|
- Bring aboard th' React Compiler
|
||||||
|
- Shift th' Cron magic over t' Rust
|
||||||
|
|
||||||
|
🎁 **Merged PRs earn ye a Lifetime Letter o' Marque!**
|
||||||
|
|
||||||
<br />
|
<br />
|
||||||
|
|
||||||
|
|||||||
@@ -50,28 +50,49 @@
|
|||||||
|
|
||||||
<br />
|
<br />
|
||||||
|
|
||||||
## Funkcje
|
<a href="https://get.rcloneui.com/showcase">
|
||||||
### 🖥️ Cross platform, działa na Windows, Linux, macOS (x64, ARM i Apple Silicon)
|
<img src=".github/rclone-video.png" alt="The GUI for Rclone">
|
||||||
### ✅ Działa z istniejącą kopią rclone (lub może ją dla ciebie zainstalować). Za Twoją zgodą może równie aktualizować ją do najnowszej wersji
|
</a>
|
||||||
### 🔗 Dodawanie / edycja / usuwanie zdalnych zasobów (Remotes)
|
|
||||||
### ⚡️ Wspiera wszystkie operacje: Sync, Bisync, Copy, Move, Delete, Purge, Serve, Mount, itd.
|
|
||||||
### ⭐️ Oszczędzaj czas ustawiając domyślne flagi osobno dla każdego Remote’a i dla każdej operacji (Mount, Sync, Copy itd.)
|
|
||||||
### 🎛️ Dostosuj flagi (parametry) podczas uruchamiania operacji – indywidualnie, w zależności od przypadku
|
|
||||||
### 📚 Wbudowana dokumentacja dla każdej flagi — nie musisz już googlować, co oznacza!
|
|
||||||
### 📂 Przeglądarka plików
|
|
||||||
### 📡 Proxy
|
|
||||||
|
|
||||||
<img src=".github/rclone-1.png" alt="Rclone UI">
|
## Docker/Homelab/Użycie na serwerze
|
||||||
|
Kontroluj swój serwer, homelab lub komputer mamy za pomocą **najprostszego rozwiązania do zarządzania zdalnymi instancjami **`rclone`**.**
|
||||||
|
|
||||||
### ⏰ Zaplanowane zadania (cron)
|
#### Docker Compose
|
||||||
### 📊 Dzienniki zadań — wizualizacja wszystkich operacji w tle
|
```yaml
|
||||||
### 🚀 Uruchamianie przy starcie systemu (autostart) i automatyczne montowanie (automount)
|
services:
|
||||||
### 🔒 Ustawienie hasła do panelu ustawień
|
rclone:
|
||||||
### 👁️ Ukrywanie poleceń lub zdalnych zasobów (Remotes) w menu zasobnika
|
image: rclone/rclone
|
||||||
### ⚙️ Import/Eksport konfiguracji (szyfrowany hasłem lub poleceniem)
|
container_name: rclone
|
||||||
### ✍️ Podpisane cyfrowo przez Microsoft i notaryzowane na macOS
|
command: rcd --rc-addr=0.0.0.0:5572 --rc-no-auth
|
||||||
|
ports:
|
||||||
|
- 5572:5572
|
||||||
|
volumes:
|
||||||
|
- ./config:/config/rclone
|
||||||
|
- /path/to/data:/data
|
||||||
|
```
|
||||||
|
|
||||||
<img src=".github/rclone-2.png" alt="Rclone UI">
|
#### Docker CLI
|
||||||
|
```bash
|
||||||
|
docker run -d \
|
||||||
|
--name rclone \
|
||||||
|
-p 5572:5572 \
|
||||||
|
-v ./config:/config/rclone \
|
||||||
|
-v /path/to/data:/data \
|
||||||
|
rclone/rclone rcd --rc-addr=0.0.0.0:5572 --rc-no-auth
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Bez Dockera
|
||||||
|
Po prostu uruchom demon `rcd` bezpośrednio:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
rclone rcd --rc-addr=0.0.0.0:5572 --rc-no-auth
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Uwagi
|
||||||
|
- Po uruchomieniu **`rclone`** wybraną metodą, otwórz Rclone UI i przejdź do Settings > Hosts.
|
||||||
|
- Upewnij się, że zezwalasz na ruch na porcie **`5572`** w swoim firewallu i/lub reverse proxy (nginx/caddy/traefik).
|
||||||
|
- Rclone UI może łączyć się z dowolnym portem RCD, więc możesz dostosować domyślny port **`5572`**.
|
||||||
|
- W środowisku produkcyjnym używaj **`--rc-user`** i **`--rc-pass`** zamiast **`--rc-no-auth`**.
|
||||||
|
|
||||||
## Package Managers
|
## Package Managers
|
||||||
- Flathub **`flatpak install com.rcloneui.RcloneUI`** lub **[ze sklepu](https://flathub.org/en/apps/com.rcloneui.RcloneUI)**
|
- Flathub **`flatpak install com.rcloneui.RcloneUI`** lub **[ze sklepu](https://flathub.org/en/apps/com.rcloneui.RcloneUI)**
|
||||||
@@ -79,6 +100,7 @@
|
|||||||
- Scoop **`scoop bucket add extras`** & **`scoop install rclone-ui`**
|
- Scoop **`scoop bucket add extras`** & **`scoop install rclone-ui`**
|
||||||
- Chocolatey **`choco install rclone-ui`**
|
- Chocolatey **`choco install rclone-ui`**
|
||||||
- WinGet **`winget install --id=RcloneUI.RcloneUI -e`**
|
- WinGet **`winget install --id=RcloneUI.RcloneUI -e`**
|
||||||
|
- NPM **`npx rclone-ui`**
|
||||||
|
|
||||||
## Link do pobrania
|
## Link do pobrania
|
||||||
- **Windows** (**[Arm](https://get.rcloneui.com/win-arm)**, **[x64](https://get.rcloneui.com/win)**)
|
- **Windows** (**[Arm](https://get.rcloneui.com/win-arm)**, **[x64](https://get.rcloneui.com/win)**)
|
||||||
@@ -100,7 +122,15 @@
|
|||||||
</a>
|
</a>
|
||||||
|
|
||||||
## Chcesz pomóc?
|
## Chcesz pomóc?
|
||||||
Zapraszamy do współpracy! Otwórz [zgłoszenie](https://github.com/FTCHD/rclone-ui/issues/new?template=Blank+issue) lub [PR](https://github.com/FTCHD/rclone-ui/compare), i wspólnie ulepszmy Rclone 🎉🎉
|
Witaj, anon. Spodziewaliśmy się Ciebie.
|
||||||
|
|
||||||
|
Oto kilka ciekawych wyzwań:
|
||||||
|
- Napraw otwarty [**Issue**](https://github.com/rclone-ui/rclone-ui/issues)
|
||||||
|
- Zaktualizuj repozytorium do Vite 7 i React 19
|
||||||
|
- Wprowadź React Compiler
|
||||||
|
- Przenieś logikę Cron do Rust
|
||||||
|
|
||||||
|
🎁 **Zmergowane PR-y otrzymują licencję dożywotnią!**
|
||||||
|
|
||||||
<br />
|
<br />
|
||||||
|
|
||||||
|
|||||||
@@ -50,28 +50,49 @@
|
|||||||
|
|
||||||
<br />
|
<br />
|
||||||
|
|
||||||
## Features
|
<a href="https://get.rcloneui.com/showcase">
|
||||||
### 🖥️ Toate Platformele: Windows, Linux, macOS (x64, Arm și Apple Silicon)
|
<img src=".github/rclone-video.png" alt="The GUI for Rclone">
|
||||||
### ✅ Funcționează cu instalarea ta existentă de rclone (sau îl poate instala pentru tine) și îl menține up-to-date (cu aprobarea ta)
|
</a>
|
||||||
### 🔗 Adaugă/Editează/Șterge Remotes
|
|
||||||
### ⚡️ Sync, Bisync, Copy, Move, Delete, Purge, Serve, Mount etc.
|
|
||||||
### ⭐️ Economisește timp setând default flags pentru fiecare Remote, separat pentru fiecare operațiune (Mount, Sync, Copy etc.)
|
|
||||||
### 🎛️ Personalizează flags când rulezi o operațiune, de la caz la caz
|
|
||||||
### 📚 Documentație integrată pentru fiecare flag — nu mai trebuie să dai search pe Google ca să afli ce face!
|
|
||||||
### 📂 File Browser
|
|
||||||
### 📡 Proxy
|
|
||||||
|
|
||||||
<img src=".github/rclone-1.png" alt="Rclone UI">
|
## Docker/Homelab/Server
|
||||||
|
Controlează serverul, homelab-ul sau PC-ul mamei tale cu **cea mai simplă soluție pentru a gestiona instanțe **`rclone`** la distanță.**
|
||||||
|
|
||||||
### ⏰ Sarcini programate (cron)
|
#### Docker Compose
|
||||||
### 📊 Job Logs (vizualizează toate operațiunile în background)
|
```yaml
|
||||||
### 🚀 Start on boot (autostart) și Mount on start (automount)
|
services:
|
||||||
### 🔒 Setează o parolă pentru panoul de Settings
|
rclone:
|
||||||
### 👁️ Ascunde comenzile sau Remotes din meniul tray
|
image: rclone/rclone
|
||||||
### ⚙️ Import/Export Configs (criptate cu `pass` sau `command`)
|
container_name: rclone
|
||||||
### ✍️ Signed by Microsoft și Notarized pe macOS
|
command: rcd --rc-addr=0.0.0.0:5572 --rc-no-auth
|
||||||
|
ports:
|
||||||
|
- 5572:5572
|
||||||
|
volumes:
|
||||||
|
- ./config:/config/rclone
|
||||||
|
- /path/to/data:/data
|
||||||
|
```
|
||||||
|
|
||||||
<img src=".github/rclone-2.png" alt="Rclone UI">
|
#### Docker CLI
|
||||||
|
```bash
|
||||||
|
docker run -d \
|
||||||
|
--name rclone \
|
||||||
|
-p 5572:5572 \
|
||||||
|
-v ./config:/config/rclone \
|
||||||
|
-v /path/to/data:/data \
|
||||||
|
rclone/rclone rcd --rc-addr=0.0.0.0:5572 --rc-no-auth
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Fără Docker
|
||||||
|
Pornește daemon-ul `rcd` direct:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
rclone rcd --rc-addr=0.0.0.0:5572 --rc-no-auth
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Note
|
||||||
|
- După pornirea **`rclone`** folosind metoda preferată, deschide Rclone UI și navighează la Settings > Hosts.
|
||||||
|
- Asigură-te că permiți traficul pe portul **`5572`** în firewall-ul tău și/sau reverse proxy (nginx/caddy/traefik).
|
||||||
|
- Rclone UI se poate conecta la orice port RCD, astfel încât poți personaliza portul implicit **`5572`**.
|
||||||
|
- Folosește **`--rc-user`** și **`--rc-pass`** în loc de **`--rc-no-auth`** în producție.
|
||||||
|
|
||||||
## Package Managers
|
## Package Managers
|
||||||
- Flathub **`flatpak install com.rcloneui.RcloneUI`** sau **[din magazin](https://flathub.org/en/apps/com.rcloneui.RcloneUI)**
|
- Flathub **`flatpak install com.rcloneui.RcloneUI`** sau **[din magazin](https://flathub.org/en/apps/com.rcloneui.RcloneUI)**
|
||||||
@@ -79,6 +100,7 @@
|
|||||||
- Scoop **`scoop bucket add extras`** & **`scoop install rclone-ui`**
|
- Scoop **`scoop bucket add extras`** & **`scoop install rclone-ui`**
|
||||||
- Chocolatey **`choco install rclone-ui`**
|
- Chocolatey **`choco install rclone-ui`**
|
||||||
- WinGet **`winget install --id=RcloneUI.RcloneUI -e`**
|
- WinGet **`winget install --id=RcloneUI.RcloneUI -e`**
|
||||||
|
- NPM **`npx rclone-ui`**
|
||||||
|
|
||||||
## Descarcă
|
## Descarcă
|
||||||
- **Windows** (**[Arm](https://get.rcloneui.com/win-arm)**, **[x64](https://get.rcloneui.com/win)**)
|
- **Windows** (**[Arm](https://get.rcloneui.com/win-arm)**, **[x64](https://get.rcloneui.com/win)**)
|
||||||
@@ -100,7 +122,15 @@
|
|||||||
</a>
|
</a>
|
||||||
|
|
||||||
## Contribuții
|
## Contribuții
|
||||||
Apreciem contribuțiile tale! Deschide un [issue](https://github.com/FTCHD/rclone-ui/issues/new?template=Blank+issue) sau un [PR](https://github.com/FTCHD/rclone-ui/compare) și hai să facem Rclone UI și mai bun împreună 🎉🎉
|
Bine ai venit, anon. Te-așteptam.
|
||||||
|
|
||||||
|
Iată câteva probleme bune de abordat:
|
||||||
|
- Rezolvă un [**Issue**](https://github.com/rclone-ui/rclone-ui/issues) deschis
|
||||||
|
- Actualizează repository-ul la Vite 7 & React 19
|
||||||
|
- Introdu React Compiler
|
||||||
|
- Mută logica Cron în Rust
|
||||||
|
|
||||||
|
🎁 **PR-urile acceptate primesc o licență gratis!**
|
||||||
|
|
||||||
<br />
|
<br />
|
||||||
|
|
||||||
|
|||||||
@@ -50,28 +50,49 @@
|
|||||||
|
|
||||||
<br />
|
<br />
|
||||||
|
|
||||||
## Funciones
|
<a href="https://get.rcloneui.com/showcase">
|
||||||
### 🖥️ Multiplataforma: Windows, Linux, macOS (x64, Arm y Apple Silicon)
|
<img src=".github/rclone-video.png" alt="The GUI for Rclone">
|
||||||
### ✅ Funciona con tu instalación existente de rclone (o puede instalar rclone por ti) y lo mantiene actualizado (con tu aprobación)
|
</a>
|
||||||
### 🔗 Añadir/Editar/Eliminar Remotes
|
|
||||||
### ⚡️ Sync, Bisync, Copy, Move, Delete, Purge, Serve, Mount, etc.
|
|
||||||
### ⭐️ Ahorra tiempo definiendo flags predeterminados por Remote y por operación (Mount, Sync, Copy, etc.)
|
|
||||||
### 🎛️ Personaliza los flags (parámetros) al ejecutar una operación, caso por caso
|
|
||||||
### 📚 Documentación integrada para cada flag; ¡sin tener que googlear qué hace cada cosa!
|
|
||||||
### 📂 Explorador de archivos
|
|
||||||
### 📡 Proxy
|
|
||||||
|
|
||||||
<img src=".github/rclone-1.png" alt="Rclone UI">
|
## Docker/Homelab/Servidor
|
||||||
|
Controla tu servidor, homelab o el PC de tu madre con **la solución más fácil para gestionar instancias remotas de **`rclone`**.**
|
||||||
|
|
||||||
### ⏰ Tareas programadas (cron)
|
#### Docker Compose
|
||||||
### 📊 Registros de trabajos (visualiza todas las operaciones en segundo plano)
|
```yaml
|
||||||
### 🚀 Inicio con el sistema (autostart) y montaje al iniciar (automount)
|
services:
|
||||||
### 🔒 Protege el panel de Configuración con contraseña
|
rclone:
|
||||||
### 👁️ Oculta comandos o Remotes del menú de la bandeja del sistema (tray)
|
image: rclone/rclone
|
||||||
### ⚙️ Import/Export de configuraciones (cifradas con pass o command)
|
container_name: rclone
|
||||||
### ✍️ Firmado por Microsoft y notarizado en macOS
|
command: rcd --rc-addr=0.0.0.0:5572 --rc-no-auth
|
||||||
|
ports:
|
||||||
|
- 5572:5572
|
||||||
|
volumes:
|
||||||
|
- ./config:/config/rclone
|
||||||
|
- /path/to/data:/data
|
||||||
|
```
|
||||||
|
|
||||||
<img src=".github/rclone-2.png" alt="Rclone UI">
|
#### Docker CLI
|
||||||
|
```bash
|
||||||
|
docker run -d \
|
||||||
|
--name rclone \
|
||||||
|
-p 5572:5572 \
|
||||||
|
-v ./config:/config/rclone \
|
||||||
|
-v /path/to/data:/data \
|
||||||
|
rclone/rclone rcd --rc-addr=0.0.0.0:5572 --rc-no-auth
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Sin Docker
|
||||||
|
Simplemente inicia el daemon `rcd` directamente:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
rclone rcd --rc-addr=0.0.0.0:5572 --rc-no-auth
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Notas
|
||||||
|
- Después de iniciar **`rclone`** usando tu método preferido, abre Rclone UI y navega a Settings > Hosts.
|
||||||
|
- Asegúrate de permitir el tráfico en el puerto **`5572`** en tu firewall y/o proxy inverso (nginx/caddy/traefik).
|
||||||
|
- Rclone UI puede conectarse a cualquier puerto RCD, por lo que puedes personalizar el puerto predeterminado **`5572`**.
|
||||||
|
- Usa **`--rc-user`** y **`--rc-pass`** en lugar de **`--rc-no-auth`** en producción.
|
||||||
|
|
||||||
## Package Managers
|
## Package Managers
|
||||||
- Flathub **`flatpak install com.rcloneui.RcloneUI`** o **[desde la tienda](https://flathub.org/en/apps/com.rcloneui.RcloneUI)**
|
- Flathub **`flatpak install com.rcloneui.RcloneUI`** o **[desde la tienda](https://flathub.org/en/apps/com.rcloneui.RcloneUI)**
|
||||||
@@ -79,6 +100,7 @@
|
|||||||
- Scoop **`scoop bucket add extras`** & **`scoop install rclone-ui`**
|
- Scoop **`scoop bucket add extras`** & **`scoop install rclone-ui`**
|
||||||
- Chocolatey **`choco install rclone-ui`**
|
- Chocolatey **`choco install rclone-ui`**
|
||||||
- WinGet **`winget install --id=RcloneUI.RcloneUI -e`**
|
- WinGet **`winget install --id=RcloneUI.RcloneUI -e`**
|
||||||
|
- NPM **`npx rclone-ui`**
|
||||||
|
|
||||||
## Descargas
|
## Descargas
|
||||||
- **Windows** (**[Arm](https://get.rcloneui.com/win-arm)**, **[x64](https://get.rcloneui.com/win)**)
|
- **Windows** (**[Arm](https://get.rcloneui.com/win-arm)**, **[x64](https://get.rcloneui.com/win)**)
|
||||||
@@ -100,7 +122,15 @@
|
|||||||
</a>
|
</a>
|
||||||
|
|
||||||
## Contribuciones
|
## Contribuciones
|
||||||
¡Tus contribuciones son bienvenidas! Abre un [issue](https://github.com/FTCHD/rclone-ui/issues/new?template=Blank+issue) o un [PR](https://github.com/FTCHD/rclone-ui/compare) y mejoremos esto juntos 🎉🎉
|
Bienvenido, anon. Te estábamos esperando.
|
||||||
|
|
||||||
|
Aquí tienes algunos retos interesantes:
|
||||||
|
- Arregla un [**Issue**](https://github.com/rclone-ui/rclone-ui/issues) abierto
|
||||||
|
- Actualiza el repositorio a Vite 7 y React 19
|
||||||
|
- Introduce React Compiler
|
||||||
|
- Mueve la lógica de Cron a Rust
|
||||||
|
|
||||||
|
🎁 **¡Los PR fusionados reciben una licencia de por vida!**
|
||||||
|
|
||||||
<br />
|
<br />
|
||||||
|
|
||||||
|
|||||||
@@ -14,10 +14,12 @@
|
|||||||
"complexity": {
|
"complexity": {
|
||||||
"all": true,
|
"all": true,
|
||||||
"noUselessTypeConstraint": "off",
|
"noUselessTypeConstraint": "off",
|
||||||
"useSimplifiedLogicExpression": "off"
|
"useSimplifiedLogicExpression": "off",
|
||||||
|
"noExcessiveCognitiveComplexity": "off"
|
||||||
},
|
},
|
||||||
"correctness": {
|
"correctness": {
|
||||||
"all": true
|
"all": true,
|
||||||
|
"useImportExtensions": "off"
|
||||||
},
|
},
|
||||||
"performance": {
|
"performance": {
|
||||||
"all": true
|
"all": true
|
||||||
|
|||||||
@@ -0,0 +1,42 @@
|
|||||||
|
<h1 align="center">
|
||||||
|
<a href="https://rcloneui.com">
|
||||||
|
<img src="https://raw.githubusercontent.com/rclone-ui/rclone-ui/main/public/banner.png" alt="Rclone UI" width="100%">
|
||||||
|
</a>
|
||||||
|
<br>
|
||||||
|
<a href="https://rcloneui.com">
|
||||||
|
The cross-platform GUI for Rclone
|
||||||
|
</a>
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
Tnstalls and launches [**Rclone UI**](https://rcloneui.com).
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npx rclone-ui
|
||||||
|
```
|
||||||
|
|
||||||
|
That's it!
|
||||||
|
|
||||||
|
If Rclone UI is already installed, it opens the app. If not, it downloads the latest release from GitHub and installs it.
|
||||||
|
|
||||||
|
## Options
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npx rclone-ui # Open Rclone UI (installs if not found)
|
||||||
|
npx rclone-ui --install # Reinstall (or update)
|
||||||
|
npx rclone-ui --version # Show version
|
||||||
|
npx rclone-ui --help # Show help
|
||||||
|
```
|
||||||
|
|
||||||
|
## How it works
|
||||||
|
|
||||||
|
- **macOS**: Mounts the `.dmg` and copies the app to `/Applications`
|
||||||
|
- **Windows**: Runs the NSIS installer in silent mode
|
||||||
|
- **Linux**: Installs the AppImage to `~/.local/bin`
|
||||||
|
|
||||||
|
## Links
|
||||||
|
|
||||||
|
- [Website](https://rcloneui.com)
|
||||||
|
- [GitHub](https://github.com/rclone-ui/rclone-ui)
|
||||||
|
- [Releases](https://github.com/rclone-ui/rclone-ui/releases)
|
||||||
@@ -0,0 +1,124 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
import readline from 'node:readline'
|
||||||
|
import { isInstalled } from '../lib/detect.js'
|
||||||
|
import { download, install } from '../lib/install.js'
|
||||||
|
import { openApp } from '../lib/open.js'
|
||||||
|
import { getLatestVersion } from '../lib/platform.js'
|
||||||
|
|
||||||
|
function waitForEnter(prompt) {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const rl = readline.createInterface({
|
||||||
|
input: process.stdin,
|
||||||
|
output: process.stdout,
|
||||||
|
})
|
||||||
|
|
||||||
|
rl.question(prompt, () => {
|
||||||
|
rl.close()
|
||||||
|
resolve()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearLine() {
|
||||||
|
process.stdout.write('\r\x1b[K')
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatBytes(bytes) {
|
||||||
|
if (bytes < 1024) return `${bytes} B`
|
||||||
|
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`
|
||||||
|
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const args = process.argv.slice(2)
|
||||||
|
|
||||||
|
// Handle --version
|
||||||
|
if (args.includes('--version') || args.includes('-v')) {
|
||||||
|
const version = await getLatestVersion()
|
||||||
|
console.log(`rclone-ui v${version} (latest)`)
|
||||||
|
process.exit(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle --help
|
||||||
|
if (args.includes('--help') || args.includes('-h')) {
|
||||||
|
console.log(`
|
||||||
|
rclone-ui - The GUI for rclone
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
npx rclone-ui Open Rclone UI (installs if not found)
|
||||||
|
npx rclone-ui --install Reinstall (or update)
|
||||||
|
npx rclone-ui --version Show version
|
||||||
|
npx rclone-ui --help Show this help
|
||||||
|
|
||||||
|
More info: https://rcloneui.com
|
||||||
|
`)
|
||||||
|
process.exit(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
const forceInstall = args.includes('--install')
|
||||||
|
|
||||||
|
// Check if already installed
|
||||||
|
if (isInstalled() && !forceInstall) {
|
||||||
|
console.log('Opening Rclone UI...')
|
||||||
|
try {
|
||||||
|
await openApp()
|
||||||
|
process.exit(0)
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to open Rclone UI:', err.message)
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Not installed - prompt for installation
|
||||||
|
console.log('')
|
||||||
|
await waitForEnter(
|
||||||
|
'Rclone UI is not installed. Press Enter to install the latest release from GitHub...'
|
||||||
|
)
|
||||||
|
console.log('')
|
||||||
|
|
||||||
|
// Download
|
||||||
|
process.stdout.write('Downloading...')
|
||||||
|
let installerPath
|
||||||
|
try {
|
||||||
|
installerPath = await download((downloaded, total) => {
|
||||||
|
clearLine()
|
||||||
|
const percent = Math.round((downloaded / total) * 100)
|
||||||
|
const downloadedStr = formatBytes(downloaded)
|
||||||
|
const totalStr = formatBytes(total)
|
||||||
|
process.stdout.write(`Downloading... ${percent}% (${downloadedStr} / ${totalStr})`)
|
||||||
|
})
|
||||||
|
clearLine()
|
||||||
|
console.log('Downloading... Done!')
|
||||||
|
} catch (err) {
|
||||||
|
clearLine()
|
||||||
|
console.error('Download failed:', err.message)
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Install
|
||||||
|
console.log('Installing...')
|
||||||
|
try {
|
||||||
|
await install(installerPath)
|
||||||
|
console.log('Installing... Done!')
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Installation failed:', err.message)
|
||||||
|
console.error('You may need to run with elevated permissions (sudo on macOS/Linux).')
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('')
|
||||||
|
await waitForEnter('Installed! Press Enter to run Rclone UI...')
|
||||||
|
|
||||||
|
try {
|
||||||
|
await openApp()
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to open Rclone UI:', err.message)
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch((err) => {
|
||||||
|
console.error('Error:', err.message)
|
||||||
|
process.exit(1)
|
||||||
|
})
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
import fs from "node:fs";
|
||||||
|
import path from "node:path";
|
||||||
|
import os from "node:os";
|
||||||
|
import { getPlatform } from "./platform.js";
|
||||||
|
|
||||||
|
export function getInstallPath() {
|
||||||
|
const { isMac, isWindows, isLinux } = getPlatform();
|
||||||
|
|
||||||
|
if (isMac) {
|
||||||
|
return "/Applications/Rclone UI.app";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isWindows) {
|
||||||
|
// NSIS installs to %LOCALAPPDATA%\Rclone UI
|
||||||
|
const localAppData = process.env.LOCALAPPDATA || path.join(os.homedir(), "AppData", "Local");
|
||||||
|
return path.join(localAppData, "Rclone UI", "Rclone UI.exe");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isLinux) {
|
||||||
|
// AppImage installed to ~/.local/bin
|
||||||
|
return path.join(os.homedir(), ".local", "bin", "rclone-ui");
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isInstalled() {
|
||||||
|
const installPath = getInstallPath();
|
||||||
|
if (!installPath) return false;
|
||||||
|
|
||||||
|
try {
|
||||||
|
return fs.existsSync(installPath);
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,163 @@
|
|||||||
|
import { execSync, spawn } from 'node:child_process'
|
||||||
|
import fs from 'node:fs'
|
||||||
|
import https from 'node:https'
|
||||||
|
import os from 'node:os'
|
||||||
|
import path from 'node:path'
|
||||||
|
import { getDownloadUrl, getPlatform } from './platform.js'
|
||||||
|
|
||||||
|
function followRedirects(url, callback) {
|
||||||
|
https.get(url, (response) => {
|
||||||
|
if (response.statusCode >= 300 && response.statusCode < 400 && response.headers.location) {
|
||||||
|
followRedirects(response.headers.location, callback)
|
||||||
|
} else {
|
||||||
|
callback(response)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function download(onProgress) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const { url, filename } = getDownloadUrl()
|
||||||
|
const tempDir = os.tmpdir()
|
||||||
|
const filePath = path.join(tempDir, filename)
|
||||||
|
|
||||||
|
followRedirects(url, (response) => {
|
||||||
|
if (response.statusCode !== 200) {
|
||||||
|
reject(new Error(`Failed to download: ${response.statusCode}`))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const totalSize = Number.parseInt(response.headers['content-length'], 10)
|
||||||
|
let downloadedSize = 0
|
||||||
|
|
||||||
|
const file = fs.createWriteStream(filePath)
|
||||||
|
|
||||||
|
response.on('data', (chunk) => {
|
||||||
|
downloadedSize += chunk.length
|
||||||
|
if (onProgress && totalSize) {
|
||||||
|
onProgress(downloadedSize, totalSize)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
response.pipe(file)
|
||||||
|
|
||||||
|
file.on('finish', () => {
|
||||||
|
file.close()
|
||||||
|
resolve(filePath)
|
||||||
|
})
|
||||||
|
|
||||||
|
file.on('error', (err) => {
|
||||||
|
fs.unlink(filePath, () => {})
|
||||||
|
reject(err)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function install(installerPath) {
|
||||||
|
const { isMac, isWindows, isLinux } = getPlatform()
|
||||||
|
const { type } = getDownloadUrl()
|
||||||
|
|
||||||
|
if (isMac && type === 'dmg') {
|
||||||
|
return installMacDmg(installerPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isWindows && type === 'exe') {
|
||||||
|
return installWindowsExe(installerPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isLinux && type === 'appimage') {
|
||||||
|
return installLinuxAppImage(installerPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(`Unsupported installer type: ${type}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function installMacDmg(dmgPath) {
|
||||||
|
// Mount the DMG
|
||||||
|
const mountOutput = execSync(`hdiutil attach -nobrowse -readonly "${dmgPath}"`, {
|
||||||
|
encoding: 'utf-8',
|
||||||
|
})
|
||||||
|
|
||||||
|
// Parse mount point from output
|
||||||
|
const mountMatch = mountOutput.match(/\/Volumes\/[^\n]+/)
|
||||||
|
if (!mountMatch) {
|
||||||
|
throw new Error('Failed to mount DMG')
|
||||||
|
}
|
||||||
|
const mountPoint = mountMatch[0].trim()
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Find the .app in the mounted volume
|
||||||
|
const apps = fs.readdirSync(mountPoint).filter((f) => f.endsWith('.app'))
|
||||||
|
if (apps.length === 0) {
|
||||||
|
throw new Error('No .app found in DMG')
|
||||||
|
}
|
||||||
|
|
||||||
|
const appName = apps[0]
|
||||||
|
const sourcePath = path.join(mountPoint, appName)
|
||||||
|
const destPath = path.join('/Applications', appName)
|
||||||
|
|
||||||
|
// Remove existing installation if present
|
||||||
|
if (fs.existsSync(destPath)) {
|
||||||
|
fs.rmSync(destPath, { recursive: true, force: true })
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy to Applications
|
||||||
|
execSync(`cp -R "${sourcePath}" "/Applications/"`, { encoding: 'utf-8' })
|
||||||
|
} finally {
|
||||||
|
// Unmount the DMG
|
||||||
|
try {
|
||||||
|
execSync(`hdiutil detach "${mountPoint}" -quiet`, { encoding: 'utf-8' })
|
||||||
|
} catch {
|
||||||
|
// Ignore unmount errors
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function installWindowsExe(exePath) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
// Run NSIS installer with /S for silent mode
|
||||||
|
const child = spawn(exePath, ['/S'], {
|
||||||
|
stdio: 'ignore',
|
||||||
|
shell: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
child.on('error', reject)
|
||||||
|
child.on('close', (code) => {
|
||||||
|
if (code === 0) {
|
||||||
|
resolve()
|
||||||
|
} else {
|
||||||
|
reject(new Error(`Installer exited with code ${code}`))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async function installLinuxAppImage(appImagePath) {
|
||||||
|
const homeDir = os.homedir()
|
||||||
|
const binDir = path.join(homeDir, '.local', 'bin')
|
||||||
|
const destPath = path.join(binDir, 'rclone-ui')
|
||||||
|
|
||||||
|
// Ensure ~/.local/bin exists
|
||||||
|
fs.mkdirSync(binDir, { recursive: true })
|
||||||
|
|
||||||
|
// Copy AppImage to destination
|
||||||
|
fs.copyFileSync(appImagePath, destPath)
|
||||||
|
|
||||||
|
// Make executable
|
||||||
|
fs.chmodSync(destPath, 0o755)
|
||||||
|
|
||||||
|
// Create .desktop file for application menu
|
||||||
|
const desktopDir = path.join(homeDir, '.local', 'share', 'applications')
|
||||||
|
fs.mkdirSync(desktopDir, { recursive: true })
|
||||||
|
|
||||||
|
const desktopEntry = `[Desktop Entry]
|
||||||
|
Name=Rclone UI
|
||||||
|
Exec=${destPath}
|
||||||
|
Type=Application
|
||||||
|
Categories=Utility;
|
||||||
|
Comment=The GUI for rclone
|
||||||
|
`
|
||||||
|
|
||||||
|
fs.writeFileSync(path.join(desktopDir, 'rclone-ui.desktop'), desktopEntry)
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
import { spawn } from "node:child_process";
|
||||||
|
import { getPlatform } from "./platform.js";
|
||||||
|
import { getInstallPath } from "./detect.js";
|
||||||
|
|
||||||
|
export function openApp() {
|
||||||
|
const { isMac, isWindows, isLinux } = getPlatform();
|
||||||
|
const installPath = getInstallPath();
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
let child;
|
||||||
|
|
||||||
|
if (isMac) {
|
||||||
|
child = spawn("open", ["-a", "Rclone UI"], {
|
||||||
|
detached: true,
|
||||||
|
stdio: "ignore",
|
||||||
|
});
|
||||||
|
} else if (isWindows) {
|
||||||
|
child = spawn(installPath, [], {
|
||||||
|
detached: true,
|
||||||
|
stdio: "ignore",
|
||||||
|
shell: true,
|
||||||
|
});
|
||||||
|
} else if (isLinux) {
|
||||||
|
child = spawn(installPath, [], {
|
||||||
|
detached: true,
|
||||||
|
stdio: "ignore",
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
reject(new Error("Unsupported platform"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
child.unref();
|
||||||
|
child.on("error", reject);
|
||||||
|
|
||||||
|
// Give it a moment to spawn, then resolve
|
||||||
|
setTimeout(() => resolve(), 500);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
import https from 'node:https'
|
||||||
|
import os from 'node:os'
|
||||||
|
|
||||||
|
const GITHUB_REPO = 'rclone-ui/rclone-ui'
|
||||||
|
|
||||||
|
export function getLatestVersion() {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const options = {
|
||||||
|
hostname: 'api.github.com',
|
||||||
|
path: `/repos/${GITHUB_REPO}/releases/latest`,
|
||||||
|
headers: { 'User-Agent': 'rclone-ui-npm' },
|
||||||
|
}
|
||||||
|
|
||||||
|
https
|
||||||
|
.get(options, (res) => {
|
||||||
|
let data = ''
|
||||||
|
res.on('data', (chunk) => (data += chunk))
|
||||||
|
res.on('end', () => {
|
||||||
|
try {
|
||||||
|
const release = JSON.parse(data)
|
||||||
|
const version = release.tag_name?.replace(/^v/, '') || 'unknown'
|
||||||
|
resolve(version)
|
||||||
|
} catch {
|
||||||
|
resolve('unknown')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.on('error', () => resolve('unknown'))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getPlatform() {
|
||||||
|
const platform = os.platform()
|
||||||
|
const arch = os.arch()
|
||||||
|
|
||||||
|
return {
|
||||||
|
os: platform,
|
||||||
|
arch: arch,
|
||||||
|
isWindows: platform === 'win32',
|
||||||
|
isMac: platform === 'darwin',
|
||||||
|
isLinux: platform === 'linux',
|
||||||
|
isArm: arch === 'arm64',
|
||||||
|
isX64: arch === 'x64',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getDownloadUrl(version = 'latest') {
|
||||||
|
const { os: platform, arch } = getPlatform()
|
||||||
|
const tag = version === 'latest' ? 'latest/download' : `download/v${version}`
|
||||||
|
const base = `https://github.com/${GITHUB_REPO}/releases/${tag}`
|
||||||
|
|
||||||
|
// Map platform + arch to release filename
|
||||||
|
if (platform === 'darwin') {
|
||||||
|
// macOS
|
||||||
|
const archSuffix = arch === 'arm64' ? 'aarch64' : 'x64'
|
||||||
|
return {
|
||||||
|
url: `${base}/Rclone.UI_${archSuffix}.dmg`,
|
||||||
|
filename: `Rclone.UI_${archSuffix}.dmg`,
|
||||||
|
type: 'dmg',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (platform === 'win32') {
|
||||||
|
// Windows
|
||||||
|
const archSuffix = arch === 'arm64' ? 'arm64' : 'x64'
|
||||||
|
return {
|
||||||
|
url: `${base}/Rclone.UI_${archSuffix}.exe`,
|
||||||
|
filename: `Rclone.UI_${archSuffix}.exe`,
|
||||||
|
type: 'exe',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (platform === 'linux') {
|
||||||
|
// Linux - prefer AppImage for portability
|
||||||
|
const archSuffix = arch === 'arm64' ? 'aarch64' : 'amd64'
|
||||||
|
return {
|
||||||
|
url: `${base}/Rclone.UI_${archSuffix}.AppImage`,
|
||||||
|
filename: `Rclone.UI_${archSuffix}.AppImage`,
|
||||||
|
type: 'appimage',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(`Unsupported platform: ${platform} ${arch}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getAppName() {
|
||||||
|
const { isMac } = getPlatform()
|
||||||
|
return isMac ? 'Rclone UI.app' : 'Rclone UI'
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"name": "rclone-ui",
|
||||||
|
"version": "3.0.1",
|
||||||
|
"description": "Installs and launches Rclone UI",
|
||||||
|
"bin": {
|
||||||
|
"rclone-ui": "bin/rclone-ui.js"
|
||||||
|
},
|
||||||
|
"type": "module",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/rclone-ui/rclone-ui"
|
||||||
|
},
|
||||||
|
"homepage": "https://rcloneui.com",
|
||||||
|
"keywords": ["rclone", "gui", "drive", "s3", "cloud", "storage", "sync", "backup"],
|
||||||
|
"license": "MIT",
|
||||||
|
"author": {
|
||||||
|
"name": "Rclone UI",
|
||||||
|
"url": "https://github.com/rclone-ui/rclone-ui"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,10 +1,26 @@
|
|||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="en" class="bg-neutral-900 dark">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>Rclone UI</title>
|
<title>Rclone UI</title>
|
||||||
|
<script>
|
||||||
|
document.documentElement.classList.toggle(
|
||||||
|
"dark",
|
||||||
|
localStorage.theme === "dark" ||
|
||||||
|
(!("theme" in localStorage) && window.matchMedia("(prefers-color-scheme: dark)").matches),
|
||||||
|
);
|
||||||
|
</script>
|
||||||
|
<script>
|
||||||
|
if (
|
||||||
|
window.location.pathname.startsWith('/toolbar') ||
|
||||||
|
window.location.pathname.startsWith('/startup') ||
|
||||||
|
window.location.pathname.startsWith('/onboarding')
|
||||||
|
) {
|
||||||
|
document.documentElement.classList.add('!bg-transparent')
|
||||||
|
}
|
||||||
|
</script>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body class="select-none">
|
<body class="select-none">
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import { openWindow } from './window'
|
||||||
|
|
||||||
|
export function getDeepLinkUrl(url: string) {
|
||||||
|
let cleanedUrl = url.replace('rclone:', '')
|
||||||
|
while (cleanedUrl.startsWith('/')) {
|
||||||
|
cleanedUrl = cleanedUrl.slice(1)
|
||||||
|
}
|
||||||
|
return cleanedUrl
|
||||||
|
}
|
||||||
|
|
||||||
|
export function handleDeepLinkUrl(url: string) {
|
||||||
|
console.log('deep link url', url)
|
||||||
|
|
||||||
|
const domain = url.split('/')[0]
|
||||||
|
|
||||||
|
if (domain === 'add-template') {
|
||||||
|
return openWindow({ name: 'Templates', url: '/templates?action=add' })
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,127 @@
|
|||||||
|
import type { FlagValue } from '../types/rclone'
|
||||||
|
import { SERVE_TYPES } from './rclone/constants'
|
||||||
|
|
||||||
|
export const FLAG_CATEGORIES = [
|
||||||
|
'copy',
|
||||||
|
'sync',
|
||||||
|
'config',
|
||||||
|
'vfs',
|
||||||
|
'filter',
|
||||||
|
'mount',
|
||||||
|
...SERVE_TYPES.map((type) => `serve.${type}` as const),
|
||||||
|
] as const
|
||||||
|
|
||||||
|
function returnFlag(
|
||||||
|
category: (typeof FLAG_CATEGORIES)[number],
|
||||||
|
flag: string
|
||||||
|
): { category: (typeof FLAG_CATEGORIES)[number]; flag: string } {
|
||||||
|
return {
|
||||||
|
category,
|
||||||
|
flag,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getFlagCategory(
|
||||||
|
flag: string,
|
||||||
|
flags: Record<string, { Name: string; Groups?: string }[]>
|
||||||
|
) {
|
||||||
|
console.log('[getFlagCategory] flag', flag)
|
||||||
|
const normalizedFlag = (flag.startsWith('--') ? flag.slice(2) : flag).replace(/-/g, '_')
|
||||||
|
|
||||||
|
console.log('[getFlagCategory] normalized flag', normalizedFlag)
|
||||||
|
let foundFlag = null
|
||||||
|
|
||||||
|
foundFlag = flags.main.find((f) => f.Name === normalizedFlag)
|
||||||
|
|
||||||
|
if (foundFlag) {
|
||||||
|
if (foundFlag.Groups?.includes('Copy')) {
|
||||||
|
return returnFlag('copy', normalizedFlag)
|
||||||
|
}
|
||||||
|
if (foundFlag.Groups?.includes('Sync')) {
|
||||||
|
return returnFlag('sync', normalizedFlag)
|
||||||
|
}
|
||||||
|
return returnFlag('config', normalizedFlag)
|
||||||
|
}
|
||||||
|
|
||||||
|
foundFlag = flags.vfs.find((f) => f.Name === normalizedFlag)
|
||||||
|
if (foundFlag) {
|
||||||
|
return returnFlag('vfs', normalizedFlag)
|
||||||
|
}
|
||||||
|
|
||||||
|
foundFlag = flags.filter.find((f) => f.Name === normalizedFlag)
|
||||||
|
if (foundFlag) {
|
||||||
|
return returnFlag('filter', normalizedFlag)
|
||||||
|
}
|
||||||
|
|
||||||
|
foundFlag = flags.mount.find((f) => f.Name === normalizedFlag)
|
||||||
|
if (foundFlag) {
|
||||||
|
return returnFlag('mount', normalizedFlag)
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const serveType of SERVE_TYPES) {
|
||||||
|
foundFlag = flags[serveType].find((f) => f.Name === normalizedFlag)
|
||||||
|
if (foundFlag) {
|
||||||
|
return returnFlag(`serve.${serveType}`, normalizedFlag)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
export function sortByName(flag1: { Name: string }, flag2: { Name: string }) {
|
||||||
|
return flag1.Name.localeCompare(flag2.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getOptionsSubtitle = (count: number) =>
|
||||||
|
count > 0 ? `${count} option${count !== 1 ? 's' : ''} set` : undefined
|
||||||
|
|
||||||
|
export const getJsonKeyCount = (json: string) => {
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(json) as Record<string, unknown>
|
||||||
|
return Object.keys(parsed).length
|
||||||
|
} catch {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function groupByCategory(
|
||||||
|
flags: Record<string, FlagValue>,
|
||||||
|
allFlags: Record<string, { Name: string; Groups?: string }[]>
|
||||||
|
) {
|
||||||
|
const collectedFlags = {
|
||||||
|
mount: {} as Record<string, FlagValue>,
|
||||||
|
config: {} as Record<string, FlagValue>,
|
||||||
|
vfs: {} as Record<string, FlagValue>,
|
||||||
|
filter: {} as Record<string, FlagValue>,
|
||||||
|
copy: {} as Record<string, FlagValue>,
|
||||||
|
sync: {} as Record<string, FlagValue>,
|
||||||
|
serve: {
|
||||||
|
...SERVE_TYPES.reduce(
|
||||||
|
(acc, type) => {
|
||||||
|
acc[type] = {}
|
||||||
|
return acc
|
||||||
|
},
|
||||||
|
{} as Record<(typeof SERVE_TYPES)[number], Record<string, FlagValue>>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [k, v] of Object.entries(flags)) {
|
||||||
|
const category = getFlagCategory(k, allFlags)
|
||||||
|
if (!category) continue
|
||||||
|
if (category.category.startsWith('serve.')) {
|
||||||
|
const serveType = category.category.slice(5) as (typeof SERVE_TYPES)[number]
|
||||||
|
collectedFlags.serve[serveType] = {
|
||||||
|
...collectedFlags.serve[serveType],
|
||||||
|
[k]: v,
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
collectedFlags[category.category as keyof Omit<typeof collectedFlags, 'serve'>] = {
|
||||||
|
...collectedFlags[category.category as keyof Omit<typeof collectedFlags, 'serve'>],
|
||||||
|
[k]: v,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return collectedFlags
|
||||||
|
}
|
||||||
@@ -1,19 +1,24 @@
|
|||||||
import { sep } from '@tauri-apps/api/path'
|
import { sep } from '@tauri-apps/api/path'
|
||||||
|
|
||||||
|
const RE_WINDOWS_DRIVE = /^[a-zA-Z]:[/\\]/
|
||||||
|
const RE_WINDOWS_DRIVE_WITH_SLASH = /^([a-zA-Z]:)\/?/
|
||||||
|
const RE_LOCAL_WINDOWS_PATH = /^:local:([a-zA-Z]:\/?.*)$/
|
||||||
|
const RE_PATH_SEPARATOR = /[/\\]/
|
||||||
|
|
||||||
export function formatBytes(bytes: number) {
|
export function formatBytes(bytes: number) {
|
||||||
if (bytes < 1024) {
|
if (bytes < 1024) {
|
||||||
return `${bytes} B`
|
return `${bytes} B`
|
||||||
}
|
}
|
||||||
|
|
||||||
if (bytes < 1024 * 1024) {
|
if (bytes < 1024 * 1024) {
|
||||||
return `${(bytes / 1024).toFixed(2)} KB`
|
return `${Number.parseFloat((bytes / 1024).toFixed(2))} KB`
|
||||||
}
|
}
|
||||||
|
|
||||||
if (bytes < 1024 * 1024 * 1024) {
|
if (bytes < 1024 * 1024 * 1024) {
|
||||||
return `${(bytes / 1024 / 1024).toFixed(2)} MB`
|
return `${Number.parseFloat((bytes / 1024 / 1024).toFixed(2))} MB`
|
||||||
}
|
}
|
||||||
|
|
||||||
return `${(bytes / 1024 / 1024 / 1024).toFixed(2)} GB`
|
return `${Number.parseFloat((bytes / 1024 / 1024 / 1024).toFixed(2))} GB`
|
||||||
}
|
}
|
||||||
|
|
||||||
export function replaceSmartQuotes(value: string) {
|
export function replaceSmartQuotes(value: string) {
|
||||||
@@ -30,15 +35,26 @@ export function replaceSmartQuotes(value: string) {
|
|||||||
|
|
||||||
export function getRemoteName(path?: string) {
|
export function getRemoteName(path?: string) {
|
||||||
if (!path?.includes(':')) return null // Return null for local paths
|
if (!path?.includes(':')) return null // Return null for local paths
|
||||||
|
if (path.startsWith(':local:')) {
|
||||||
|
return ':local'
|
||||||
|
}
|
||||||
|
// Windows drive letter check (C:\, D:/, etc.)
|
||||||
|
if (RE_WINDOWS_DRIVE.test(path)) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
return path.split(':')[0]
|
return path.split(':')[0]
|
||||||
}
|
}
|
||||||
|
|
||||||
export function buildReadablePath(path: string, type: 'short' | 'long' = 'long') {
|
export function buildReadablePath(path: string, type: 'short' | 'long' = 'long') {
|
||||||
|
console.log('[buildReadablePath] path', path)
|
||||||
|
console.log('[buildReadablePath] sep()', sep())
|
||||||
|
|
||||||
if (!path) {
|
if (!path) {
|
||||||
return ''
|
return ''
|
||||||
}
|
}
|
||||||
|
|
||||||
const lastSegment = path.split(sep()).slice(-1).join('')
|
const lastSegment = path.split(RE_PATH_SEPARATOR).filter(Boolean).pop()
|
||||||
|
console.log('[buildReadablePath] lastSegment', lastSegment)
|
||||||
|
|
||||||
if (type === 'short') {
|
if (type === 'short') {
|
||||||
return lastSegment
|
return lastSegment
|
||||||
@@ -47,6 +63,34 @@ export function buildReadablePath(path: string, type: 'short' | 'long' = 'long')
|
|||||||
return `${path.split(':')[0]}:/.../${lastSegment}`
|
return `${path.split(':')[0]}:/.../${lastSegment}`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function buildReadablePathMultiple(
|
||||||
|
paths: string[],
|
||||||
|
type: 'short' | 'long',
|
||||||
|
truncate: boolean = false
|
||||||
|
) {
|
||||||
|
console.log('[buildReadablePathMultiple] paths', paths)
|
||||||
|
|
||||||
|
if (paths.length < 2) {
|
||||||
|
return buildReadablePath(paths[0], type)
|
||||||
|
}
|
||||||
|
|
||||||
|
const readablePath = `${buildReadablePath(paths[0], type)} + ${paths.length - 1} more`
|
||||||
|
|
||||||
|
if (truncate) {
|
||||||
|
let [start, end] = readablePath.split(' + ')
|
||||||
|
|
||||||
|
if (start.length <= 30) {
|
||||||
|
return readablePath
|
||||||
|
}
|
||||||
|
|
||||||
|
start = start.slice(0, 47) + '...'
|
||||||
|
|
||||||
|
return `${start} + ${end}`
|
||||||
|
}
|
||||||
|
|
||||||
|
return readablePath
|
||||||
|
}
|
||||||
|
|
||||||
export function getConfigParentFolder(path: string) {
|
export function getConfigParentFolder(path: string) {
|
||||||
console.log('[getConfigParentFolder] path', path)
|
console.log('[getConfigParentFolder] path', path)
|
||||||
if (path.endsWith('\\rclone.conf')) {
|
if (path.endsWith('\\rclone.conf')) {
|
||||||
@@ -62,3 +106,77 @@ export function getConfigParentFolder(path: string) {
|
|||||||
console.log('[getConfigParentFolder] path does not end with \\rclone.conf or /rclone.conf')
|
console.log('[getConfigParentFolder] path does not end with \\rclone.conf or /rclone.conf')
|
||||||
return path
|
return path
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getFsInfo(fs: string, sep = '/') {
|
||||||
|
let normalizedFs = fs.replace(/\\/g, '/')
|
||||||
|
|
||||||
|
const localWindowsMatch = normalizedFs.match(RE_LOCAL_WINDOWS_PATH)
|
||||||
|
if (localWindowsMatch) {
|
||||||
|
normalizedFs = localWindowsMatch[1]
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('[getFsInfo] ', normalizedFs)
|
||||||
|
|
||||||
|
const fsRemote = getRemoteName(normalizedFs)
|
||||||
|
console.log('[getFsInfo] ', normalizedFs, 'fsRemote', fsRemote)
|
||||||
|
|
||||||
|
let root: string
|
||||||
|
let path: string
|
||||||
|
|
||||||
|
if (fsRemote) {
|
||||||
|
root = `${fsRemote}:`
|
||||||
|
path = normalizedFs.replace(fsRemote, '').split(':')[1]
|
||||||
|
} else {
|
||||||
|
const windowsDriveMatch = normalizedFs.match(RE_WINDOWS_DRIVE_WITH_SLASH)
|
||||||
|
if (windowsDriveMatch) {
|
||||||
|
root = `:local:${windowsDriveMatch[1]}/`
|
||||||
|
path = normalizedFs.slice(windowsDriveMatch[0].length)
|
||||||
|
} else {
|
||||||
|
root = ':local:'
|
||||||
|
path = normalizedFs
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('[getFsInfo] ', normalizedFs, 'root', root)
|
||||||
|
console.log('[getFsInfo] ', normalizedFs, 'path', path)
|
||||||
|
|
||||||
|
let type = 'file'
|
||||||
|
|
||||||
|
if (normalizedFs.endsWith('/') || normalizedFs.endsWith('\\')) {
|
||||||
|
type = 'folder'
|
||||||
|
}
|
||||||
|
|
||||||
|
while (path.startsWith('/')) {
|
||||||
|
path = path.slice(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
while (path.endsWith('/') || path.endsWith('\\')) {
|
||||||
|
path = path.slice(0, -1)
|
||||||
|
}
|
||||||
|
|
||||||
|
const dirPath = `${path}${sep}`
|
||||||
|
|
||||||
|
const name = path.split('/').pop()!
|
||||||
|
|
||||||
|
console.log('[getFsInfo] ', normalizedFs, 'name', name)
|
||||||
|
console.log('[getFsInfo] ', normalizedFs, 'fullFilePath', `${root}${path}`)
|
||||||
|
console.log('[getFsInfo] ', normalizedFs, 'fullDirPath', `${root}${dirPath}`)
|
||||||
|
console.log('[getFsInfo] ', normalizedFs, 'type', type)
|
||||||
|
console.log('[getFsInfo] ', normalizedFs, 'remoteName', fsRemote || ':local')
|
||||||
|
console.log('[getFsInfo] ', normalizedFs, 'isRemote', !!fsRemote)
|
||||||
|
console.log('[getFsInfo] ', normalizedFs, 'root', root || '/')
|
||||||
|
console.log('[getFsInfo] ', normalizedFs, 'filePath', path)
|
||||||
|
console.log('[getFsInfo] ', normalizedFs, 'dirPath', dirPath)
|
||||||
|
|
||||||
|
return {
|
||||||
|
isRemote: !!fsRemote,
|
||||||
|
root,
|
||||||
|
filePath: path,
|
||||||
|
dirPath,
|
||||||
|
fullFilePath: `${root}${path}`,
|
||||||
|
fullDirPath: `${root}${dirPath}`,
|
||||||
|
name,
|
||||||
|
type,
|
||||||
|
remoteName: fsRemote || ':local',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,81 @@
|
|||||||
|
import { useQuery } from '@tanstack/react-query'
|
||||||
|
import { sortByName } from './flags'
|
||||||
|
import rclone from './rclone/client'
|
||||||
|
import { SERVE_TYPES } from './rclone/constants'
|
||||||
|
|
||||||
|
export function useFlags() {
|
||||||
|
const allFlagsQuery = useQuery({
|
||||||
|
queryKey: ['options', 'all'],
|
||||||
|
queryFn: async () => await rclone('/options/info'),
|
||||||
|
})
|
||||||
|
|
||||||
|
const globalFlagsQuery = useQuery({
|
||||||
|
queryKey: ['options', 'global'],
|
||||||
|
queryFn: async () => await rclone('/options/get'),
|
||||||
|
})
|
||||||
|
|
||||||
|
const globalFlags = globalFlagsQuery.data
|
||||||
|
const allFlags = allFlagsQuery.data
|
||||||
|
|
||||||
|
const filterFlags = allFlags?.filter
|
||||||
|
.filter((flag) => !flag.Groups?.includes('Metadata'))
|
||||||
|
.sort(sortByName)
|
||||||
|
|
||||||
|
const configFlags = allFlags?.main
|
||||||
|
.filter(
|
||||||
|
(flag) =>
|
||||||
|
flag.Groups?.includes('Performance') ||
|
||||||
|
flag.Groups?.includes('Listing') ||
|
||||||
|
flag.Groups?.includes('Networking') ||
|
||||||
|
flag.Groups?.includes('Check') ||
|
||||||
|
flag.Name === 'use_server_modtime'
|
||||||
|
)
|
||||||
|
.sort(sortByName)
|
||||||
|
|
||||||
|
const mountFlags = allFlags?.mount
|
||||||
|
.filter((flag) => !flag.Groups?.includes('Metadata'))
|
||||||
|
.sort(sortByName)
|
||||||
|
|
||||||
|
const vfsFlags = allFlags?.vfs
|
||||||
|
.filter((flag) => !flag.Groups?.includes('Metadata'))
|
||||||
|
.sort(sortByName)
|
||||||
|
|
||||||
|
const copyFlags = allFlags?.main
|
||||||
|
.filter((flag) => flag.Groups?.includes('Copy'))
|
||||||
|
.sort(sortByName)
|
||||||
|
|
||||||
|
const syncFlags = allFlags?.main
|
||||||
|
.filter((flag) => flag.Groups?.includes('Copy') || flag.Groups?.includes('Sync'))
|
||||||
|
.sort(sortByName)
|
||||||
|
|
||||||
|
const serveFlags = SERVE_TYPES.reduce(
|
||||||
|
(acc, type) => {
|
||||||
|
acc[type] =
|
||||||
|
allFlags?.[type] ||
|
||||||
|
[]
|
||||||
|
.map((flag: any) => ({
|
||||||
|
...flag,
|
||||||
|
FieldName: flag.Name,
|
||||||
|
DefaultStr:
|
||||||
|
flag.Name === 'addr'
|
||||||
|
? flag.DefaultStr.replace('[', '').replace(']', '')
|
||||||
|
: flag.DefaultStr,
|
||||||
|
}))
|
||||||
|
.sort((a: any, b: any) => a.Name.localeCompare(b.Name))
|
||||||
|
return acc
|
||||||
|
},
|
||||||
|
{} as Record<(typeof SERVE_TYPES)[number], any[]>
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
allFlags,
|
||||||
|
globalFlags,
|
||||||
|
filterFlags,
|
||||||
|
configFlags,
|
||||||
|
mountFlags,
|
||||||
|
vfsFlags,
|
||||||
|
copyFlags,
|
||||||
|
syncFlags,
|
||||||
|
serveFlags,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
import { fetch as tauriFetch } from '@tauri-apps/plugin-http'
|
||||||
|
import pRetry from 'p-retry'
|
||||||
|
import createRCDClient from 'rclone-sdk'
|
||||||
|
|
||||||
|
export interface Host {
|
||||||
|
id: 'local' | string
|
||||||
|
name: string
|
||||||
|
cliVersion: string
|
||||||
|
os: 'windows' | 'macos' | 'linux'
|
||||||
|
url: string
|
||||||
|
authUser?: string
|
||||||
|
authPassword?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const LOCAL_HOST_ID = 'local' as const
|
||||||
|
|
||||||
|
export const LABEL_FOR_OS = {
|
||||||
|
windows: 'Windows',
|
||||||
|
macos: 'macOS',
|
||||||
|
linux: 'Linux',
|
||||||
|
} as const
|
||||||
|
|
||||||
|
export async function getHostInfo({
|
||||||
|
url,
|
||||||
|
authUser,
|
||||||
|
authPassword,
|
||||||
|
}: {
|
||||||
|
url: string
|
||||||
|
authUser?: string
|
||||||
|
authPassword?: string
|
||||||
|
}) {
|
||||||
|
try {
|
||||||
|
const parsedUrl = new URL(url)
|
||||||
|
if (!parsedUrl.hostname) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
let authHeader = ''
|
||||||
|
if (authUser && authPassword) {
|
||||||
|
authHeader = `Basic ${btoa(`${authUser}:${authPassword}`)}`
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('[getHostInfo] authHeader', authHeader)
|
||||||
|
|
||||||
|
const rcloneClient = createRCDClient({
|
||||||
|
baseUrl: url,
|
||||||
|
headers: authHeader
|
||||||
|
? {
|
||||||
|
'Authorization': authHeader,
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
|
fetch: (request: Request) => tauriFetch(request),
|
||||||
|
})
|
||||||
|
|
||||||
|
console.log('[getHostInfo] rcloneClient', rcloneClient)
|
||||||
|
|
||||||
|
const infoResponse = await pRetry(
|
||||||
|
() =>
|
||||||
|
rcloneClient.POST('/core/version').then((res) => {
|
||||||
|
if (!res || !res.data || res.error) {
|
||||||
|
throw new Error('Failed to get rclone version')
|
||||||
|
}
|
||||||
|
return res.data
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
retries: 3,
|
||||||
|
factor: 2,
|
||||||
|
minTimeout: 1000,
|
||||||
|
maxTimeout: 10000,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
console.log('[getHostInfo] infoResponse', infoResponse)
|
||||||
|
|
||||||
|
const { os, version } = infoResponse
|
||||||
|
|
||||||
|
const cleanedVersion = version.replace(/^v/, '')
|
||||||
|
const parsedOs = os === 'windows' ? 'windows' : os === 'darwin' ? 'macos' : 'linux'
|
||||||
|
|
||||||
|
return {
|
||||||
|
cliVersion: cleanedVersion,
|
||||||
|
os: parsedOs,
|
||||||
|
} as const
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import { invoke } from '@tauri-apps/api/core'
|
import { invoke } from '@tauri-apps/api/core'
|
||||||
import { fetch } from '@tauri-apps/plugin-http'
|
import { fetch } from '@tauri-apps/plugin-http'
|
||||||
import { platform } from '@tauri-apps/plugin-os'
|
import { platform } from '@tauri-apps/plugin-os'
|
||||||
import { usePersistedStore } from './store'
|
import { usePersistedStore } from '../store/persisted'
|
||||||
|
|
||||||
export async function validateLicense(licenseKey: string) {
|
export async function validateLicense(licenseKey: string) {
|
||||||
console.log('[validateLicense]')
|
console.log('[validateLicense]')
|
||||||
@@ -29,7 +29,7 @@ export async function validateLicense(licenseKey: string) {
|
|||||||
platform: platform(),
|
platform: platform(),
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
.then((r) => r.json())
|
.then((r) => r.json() as Promise<{ error: string; valid: boolean }>)
|
||||||
.catch((e) => {
|
.catch((e) => {
|
||||||
console.error('[validateLicense] failed to validate license')
|
console.error('[validateLicense] failed to validate license')
|
||||||
console.error(JSON.stringify(e))
|
console.error(JSON.stringify(e))
|
||||||
@@ -76,7 +76,7 @@ export async function revokeMachineLicense(licenseKey: string) {
|
|||||||
id,
|
id,
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
.then((r) => r.json())
|
.then((r) => r.json() as Promise<{ error: string; revoked: boolean }>)
|
||||||
.catch((e) => {
|
.catch((e) => {
|
||||||
console.error('[revokeMachineLicense] failed to revoke license, fetch failed')
|
console.error('[revokeMachineLicense] failed to revoke license, fetch failed')
|
||||||
console.error(JSON.stringify(e))
|
console.error(JSON.stringify(e))
|
||||||
|
|||||||
@@ -1,707 +0,0 @@
|
|||||||
import * as Sentry from '@sentry/browser'
|
|
||||||
import { Menu, MenuItem, PredefinedMenuItem, Submenu } from '@tauri-apps/api/menu'
|
|
||||||
import { resolveResource, sep } from '@tauri-apps/api/path'
|
|
||||||
import { getCurrentWindow } from '@tauri-apps/api/window'
|
|
||||||
import { ask, message, open } from '@tauri-apps/plugin-dialog'
|
|
||||||
import { exists, mkdir, remove } from '@tauri-apps/plugin-fs'
|
|
||||||
import { openPath, openUrl } from '@tauri-apps/plugin-opener'
|
|
||||||
import { platform } from '@tauri-apps/plugin-os'
|
|
||||||
import { isDirectoryEmpty } from './fs'
|
|
||||||
import notify from './notify'
|
|
||||||
import {
|
|
||||||
cleanupRemote,
|
|
||||||
deleteRemote,
|
|
||||||
getRemote,
|
|
||||||
listMounts,
|
|
||||||
listServes,
|
|
||||||
mountRemote,
|
|
||||||
stopServe,
|
|
||||||
unmountRemote,
|
|
||||||
} from './rclone/api'
|
|
||||||
import { compareVersions, getRcloneVersion } from './rclone/common'
|
|
||||||
import { dialogGetMountPlugin, needsMountPlugin } from './rclone/mount'
|
|
||||||
import { usePersistedStore, useStore } from './store'
|
|
||||||
import { showDefaultTray, showLoadingTray } from './tray'
|
|
||||||
import { lockWindows, openFullWindow, openWindow, unlockWindows } from './window'
|
|
||||||
|
|
||||||
const SUPPORTS_CLEANUP = [
|
|
||||||
's3',
|
|
||||||
'b2',
|
|
||||||
'box',
|
|
||||||
'drive',
|
|
||||||
'onedrive',
|
|
||||||
'internetarchive',
|
|
||||||
'jottacloud',
|
|
||||||
'mailru',
|
|
||||||
'mega',
|
|
||||||
'oos',
|
|
||||||
'pcloud',
|
|
||||||
'pikpak',
|
|
||||||
'putio',
|
|
||||||
'qingstor',
|
|
||||||
'protondrive',
|
|
||||||
'seafile',
|
|
||||||
'yandex',
|
|
||||||
]
|
|
||||||
|
|
||||||
async function parseRemotes(remotes: string[]) {
|
|
||||||
console.log('[parseRemotes] remotes', remotes)
|
|
||||||
|
|
||||||
const persistedStoreState = usePersistedStore.getState()
|
|
||||||
|
|
||||||
console.log('[parseRemotes] listing mounts')
|
|
||||||
const currentMounts = await listMounts()
|
|
||||||
console.log('[parseRemotes] currentMounts', currentMounts)
|
|
||||||
const currentServes = await listServes()
|
|
||||||
console.log('[parseRemotes] currentServes', currentServes)
|
|
||||||
|
|
||||||
const parsedRemotes: Record<
|
|
||||||
string,
|
|
||||||
{ icon: string; items: (MenuItem | Submenu | PredefinedMenuItem)[] }
|
|
||||||
> = {}
|
|
||||||
|
|
||||||
for (const remote of remotes) {
|
|
||||||
console.log('[parseRemotes] remote', remote)
|
|
||||||
|
|
||||||
const remoteConfig = persistedStoreState.remoteConfigList?.[remote]
|
|
||||||
if (remoteConfig?.disabledActions?.includes('tray')) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
const remoteInfo = await getRemote(remote).catch(null)
|
|
||||||
console.log('[parseRemotes] remoteInfo', remoteInfo?.provider, remoteInfo?.type)
|
|
||||||
|
|
||||||
const submenuItems: (MenuItem | Submenu | PredefinedMenuItem)[] = []
|
|
||||||
|
|
||||||
if (!remoteConfig?.disabledActions?.includes('tray-mount')) {
|
|
||||||
const mountMenuItem = await MenuItem.new({
|
|
||||||
id: `mount-${remote}`,
|
|
||||||
text: 'Quick Mount',
|
|
||||||
action: async () => {
|
|
||||||
await showLoadingTray()
|
|
||||||
|
|
||||||
try {
|
|
||||||
const needsPlugin = await needsMountPlugin()
|
|
||||||
if (needsPlugin) {
|
|
||||||
console.log('Mount plugin not installed')
|
|
||||||
await dialogGetMountPlugin()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
console.log('Mount plugin installed')
|
|
||||||
|
|
||||||
await lockWindows()
|
|
||||||
|
|
||||||
console.log('remoteConfig', remoteConfig)
|
|
||||||
|
|
||||||
let selectedPath = remoteConfig?.defaultMountPoint || null
|
|
||||||
|
|
||||||
if (!selectedPath) {
|
|
||||||
await getCurrentWindow().setFocus()
|
|
||||||
selectedPath = await open({
|
|
||||||
title: `Select a directory to mount "${remote}"`,
|
|
||||||
multiple: false,
|
|
||||||
directory: true,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!selectedPath) {
|
|
||||||
// await resetMainWindow()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('selectedPath', selectedPath)
|
|
||||||
|
|
||||||
let directoryExists: boolean | undefined
|
|
||||||
|
|
||||||
try {
|
|
||||||
directoryExists = await exists(selectedPath)
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error checking if directory exists:', error)
|
|
||||||
}
|
|
||||||
console.log('directoryExists', directoryExists)
|
|
||||||
|
|
||||||
const isPlatformWindows = platform() === 'windows'
|
|
||||||
|
|
||||||
if (directoryExists) {
|
|
||||||
const isEmpty = await isDirectoryEmpty(selectedPath)
|
|
||||||
if (!isEmpty) {
|
|
||||||
// await resetMainWindow()
|
|
||||||
|
|
||||||
await message(
|
|
||||||
'The selected directory must be empty to mount a remote.',
|
|
||||||
{
|
|
||||||
title: 'Mount Error',
|
|
||||||
kind: 'error',
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isPlatformWindows) {
|
|
||||||
await remove(selectedPath)
|
|
||||||
}
|
|
||||||
} else if (!isPlatformWindows) {
|
|
||||||
try {
|
|
||||||
await mkdir(selectedPath)
|
|
||||||
} catch (error) {
|
|
||||||
Sentry.captureException(error)
|
|
||||||
console.error('Error creating directory:', error)
|
|
||||||
await message(
|
|
||||||
'Failed to create mount directory. Try creating it manually first.',
|
|
||||||
{
|
|
||||||
title: 'Mount Error',
|
|
||||||
kind: 'error',
|
|
||||||
}
|
|
||||||
)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mount the remote
|
|
||||||
await mountRemote({
|
|
||||||
remotePath: `${remote}:${remoteConfig?.defaultRemotePath || ''}`,
|
|
||||||
mountPoint: selectedPath,
|
|
||||||
mountOptions: remoteConfig?.mountDefaults,
|
|
||||||
vfsOptions: remoteConfig?.vfsDefaults,
|
|
||||||
})
|
|
||||||
|
|
||||||
await notify({
|
|
||||||
title: 'Mounted',
|
|
||||||
body: `Successfully mounted ${remote} to ${selectedPath}`,
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!remoteConfig?.defaultMountPoint) {
|
|
||||||
const answer = await ask(
|
|
||||||
`Mount successful! Do you want to set ${selectedPath} as the default mount point for ${remote}? You can always change it later in Remote settings.`,
|
|
||||||
{
|
|
||||||
title: 'Set Default?',
|
|
||||||
okLabel: 'Set',
|
|
||||||
cancelLabel: 'Cancel',
|
|
||||||
}
|
|
||||||
)
|
|
||||||
if (answer) {
|
|
||||||
usePersistedStore.setState((state) => ({
|
|
||||||
remoteConfigList: {
|
|
||||||
...state.remoteConfigList,
|
|
||||||
[remote]: {
|
|
||||||
...state.remoteConfigList[remote],
|
|
||||||
defaultMountPoint: selectedPath,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
// await resetMainWindow()
|
|
||||||
Sentry.captureException(error)
|
|
||||||
console.error('Mount operation failed:', error)
|
|
||||||
await message(`Failed to mount ${remote}: ${error}`, {
|
|
||||||
title: 'Mount Error',
|
|
||||||
})
|
|
||||||
} finally {
|
|
||||||
await unlockWindows()
|
|
||||||
await showDefaultTray()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
submenuItems.push(mountMenuItem)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!remoteConfig?.disabledActions?.includes('tray-browse')) {
|
|
||||||
const browseMenuItem = await MenuItem.new({
|
|
||||||
id: `browse-${remote}`,
|
|
||||||
text: 'Browse',
|
|
||||||
action: async () => {
|
|
||||||
// await openBrowser(`http://localhost:5572/[${remote}:]/`)
|
|
||||||
try {
|
|
||||||
await openFullWindow({
|
|
||||||
name: 'Browse',
|
|
||||||
url:
|
|
||||||
'browse.html?url=' +
|
|
||||||
encodeURIComponent(`http://localhost:5572/[${remote}:]/`),
|
|
||||||
})
|
|
||||||
} catch (error) {
|
|
||||||
Sentry.captureException(error)
|
|
||||||
await message('Could not open browse window. Please try again.', {
|
|
||||||
title: 'Error',
|
|
||||||
kind: 'error',
|
|
||||||
okLabel: 'OK',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
},
|
|
||||||
})
|
|
||||||
submenuItems.push(browseMenuItem)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!remoteConfig?.disabledActions?.includes('tray-cleanup')) {
|
|
||||||
if (remoteInfo?.provider && SUPPORTS_CLEANUP.includes(remoteInfo.provider)) {
|
|
||||||
const cleanupMenuItem = await MenuItem.new({
|
|
||||||
id: `cleanup-${remote}`,
|
|
||||||
text: 'Cleanup',
|
|
||||||
action: async () => {
|
|
||||||
try {
|
|
||||||
const confirmed = await ask(`Start cleanup on ${remote}?`, {
|
|
||||||
title: `Cleaning up ${remote}`,
|
|
||||||
kind: 'warning',
|
|
||||||
okLabel: 'Start',
|
|
||||||
cancelLabel: 'Cancel',
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!confirmed) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
await cleanupRemote(remote)
|
|
||||||
await message(`Cleanup started for ${remote}`, {
|
|
||||||
title: 'Cleanup Started',
|
|
||||||
okLabel: 'OK',
|
|
||||||
})
|
|
||||||
} catch (error) {
|
|
||||||
await message(`Failed to start cleanup job, ${error}`, {
|
|
||||||
title: 'Cleanup Error',
|
|
||||||
kind: 'error',
|
|
||||||
okLabel: 'OK',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
},
|
|
||||||
})
|
|
||||||
submenuItems.push(cleanupMenuItem)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!remoteConfig?.disabledActions?.includes('tray-remove')) {
|
|
||||||
const removeMenuItem = await MenuItem.new({
|
|
||||||
id: `remove-${remote}`,
|
|
||||||
text: 'Remove',
|
|
||||||
action: async () => {
|
|
||||||
await showLoadingTray()
|
|
||||||
const answer = await ask(
|
|
||||||
`Are you sure you want to remove ${remote}? This action cannot be reverted.`,
|
|
||||||
{ title: `Removing ${remote}`, kind: 'warning' }
|
|
||||||
)
|
|
||||||
|
|
||||||
if (!answer) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
await deleteRemote(remote)
|
|
||||||
await showDefaultTray()
|
|
||||||
},
|
|
||||||
})
|
|
||||||
submenuItems.push(removeMenuItem)
|
|
||||||
}
|
|
||||||
|
|
||||||
const currentRemoteMounts = currentMounts.filter(
|
|
||||||
(mount) => mount.Fs.split(':')[0] === remote
|
|
||||||
)
|
|
||||||
|
|
||||||
console.log('[parseRemotes] currentRemoteMounts', currentRemoteMounts)
|
|
||||||
|
|
||||||
for (const currentMount of currentRemoteMounts) {
|
|
||||||
console.log(
|
|
||||||
'[parseRemotes] Adding Unmount (' +
|
|
||||||
currentMount.MountPoint.split(sep()).pop() +
|
|
||||||
') for ',
|
|
||||||
remote
|
|
||||||
)
|
|
||||||
const unmountMenuItem = await MenuItem.new({
|
|
||||||
id: `unmount-${remote}-${currentMount.MountPoint}`,
|
|
||||||
text: 'Unmount (' + currentMount.MountPoint.split(sep()).pop() + ')',
|
|
||||||
action: async () => {
|
|
||||||
try {
|
|
||||||
await showLoadingTray()
|
|
||||||
await unmountRemote({ mountPoint: currentMount.MountPoint })
|
|
||||||
await message(
|
|
||||||
`Successfully unmounted ${remote} from ${currentMount.MountPoint.split('/').pop()}`,
|
|
||||||
{
|
|
||||||
title: 'Success',
|
|
||||||
}
|
|
||||||
)
|
|
||||||
} catch (error) {
|
|
||||||
Sentry.captureException(error)
|
|
||||||
console.error('Unmount operation failed:', error)
|
|
||||||
await message(`Failed to unmount ${remote}: ${error}`, {
|
|
||||||
kind: 'error',
|
|
||||||
title: 'Unmount Error',
|
|
||||||
})
|
|
||||||
} finally {
|
|
||||||
await showDefaultTray()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
})
|
|
||||||
submenuItems.push(unmountMenuItem)
|
|
||||||
|
|
||||||
console.log(
|
|
||||||
'[parseRemotes] Adding Open (' +
|
|
||||||
currentMount.MountPoint.split('/').pop() +
|
|
||||||
') for ',
|
|
||||||
remote
|
|
||||||
)
|
|
||||||
const showLocationItem = await MenuItem.new({
|
|
||||||
id: `open-${remote}-${currentMount.MountPoint}`,
|
|
||||||
|
|
||||||
text: 'Open (' + currentMount.MountPoint.split('/').pop() + ')',
|
|
||||||
action: async () => {
|
|
||||||
console.log(
|
|
||||||
'[parseRemotes] Opening (' + currentMount.MountPoint.split('/').pop() + ')'
|
|
||||||
)
|
|
||||||
try {
|
|
||||||
await openPath(currentMount.MountPoint)
|
|
||||||
} catch (error) {
|
|
||||||
Sentry.captureException(error)
|
|
||||||
console.error('Error opening path:', error)
|
|
||||||
await message(`Failed to open ${currentMount.MountPoint} (${error})`, {
|
|
||||||
title: 'Open Error',
|
|
||||||
kind: 'error',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
},
|
|
||||||
})
|
|
||||||
submenuItems.push(showLocationItem)
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const currentServe of currentServes) {
|
|
||||||
// console.log('[parseRemotes] Adding Stop Serve (' + currentServe.Addr.split(':')[0] + ') for ', remote)
|
|
||||||
const stopServeMenuItem = await MenuItem.new({
|
|
||||||
id: `stop-serve-${currentServe.id}`,
|
|
||||||
text: 'Stop Serve (' + currentServe.id.split('-')[0]?.toUpperCase() + ')',
|
|
||||||
action: async () => {
|
|
||||||
try {
|
|
||||||
await showLoadingTray()
|
|
||||||
await stopServe(currentServe.id)
|
|
||||||
|
|
||||||
await message(`Successfully stopped serve ${currentServe.id}`, {
|
|
||||||
title: 'Success',
|
|
||||||
})
|
|
||||||
} catch (error) {
|
|
||||||
Sentry.captureException(error)
|
|
||||||
console.error('Stop serve operation failed:', error)
|
|
||||||
await message(`Failed to stop serve ${currentServe.id}: ${error}`, {
|
|
||||||
kind: 'error',
|
|
||||||
title: 'Stop Serve Error',
|
|
||||||
})
|
|
||||||
} finally {
|
|
||||||
await showDefaultTray()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
})
|
|
||||||
submenuItems.push(stopServeMenuItem)
|
|
||||||
}
|
|
||||||
|
|
||||||
let iconPath = await resolveResource('icons/favicon/icon.png')
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (remoteInfo?.provider && !remoteInfo.type) {
|
|
||||||
console.log('[parseRemotes] resolving provider icon', remoteInfo.provider)
|
|
||||||
iconPath = await resolveResource(`icons/small/providers/${remoteInfo.provider}.png`)
|
|
||||||
} else if (remoteInfo?.type) {
|
|
||||||
console.log('[parseRemotes] resolving backend icon', remoteInfo.type)
|
|
||||||
iconPath = await resolveResource(`icons/small/backends/${remoteInfo?.type}.png`)
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
Sentry.captureException(new Error(`Error resolving icon path for ${remote}: ${error}`))
|
|
||||||
Sentry.captureException(error)
|
|
||||||
console.error('Error resolving icon path for', remote, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('[parseRemotes] iconPath', iconPath)
|
|
||||||
|
|
||||||
parsedRemotes[remote] = {
|
|
||||||
icon: iconPath,
|
|
||||||
items: submenuItems,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return parsedRemotes
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function buildMenu() {
|
|
||||||
console.log('[buildMenu]')
|
|
||||||
|
|
||||||
const storeState = useStore.getState()
|
|
||||||
|
|
||||||
const persistedStoreState = usePersistedStore.getState()
|
|
||||||
|
|
||||||
const remotes = storeState.remotes
|
|
||||||
|
|
||||||
const menuItems: (MenuItem | Submenu | PredefinedMenuItem)[] = []
|
|
||||||
|
|
||||||
if (remotes.length === 0) {
|
|
||||||
const noRemotesMenuItem = await MenuItem.new({
|
|
||||||
id: 'no-remotes',
|
|
||||||
text: 'Add Remotes in Settings!',
|
|
||||||
enabled: false,
|
|
||||||
})
|
|
||||||
menuItems.push(noRemotesMenuItem)
|
|
||||||
} else if (remotes.length > 5) {
|
|
||||||
const parsedRemotes = await parseRemotes(remotes)
|
|
||||||
const submenuItems: (MenuItem | Submenu | PredefinedMenuItem)[] = []
|
|
||||||
|
|
||||||
for (const r in parsedRemotes) {
|
|
||||||
const sub = await Submenu.new({
|
|
||||||
items: parsedRemotes[r].items,
|
|
||||||
icon: parsedRemotes[r].icon,
|
|
||||||
text: r,
|
|
||||||
})
|
|
||||||
submenuItems.push(sub)
|
|
||||||
}
|
|
||||||
|
|
||||||
const sub = await Submenu.new({
|
|
||||||
items: submenuItems,
|
|
||||||
text: 'Remotes',
|
|
||||||
})
|
|
||||||
|
|
||||||
menuItems.push(sub)
|
|
||||||
} else {
|
|
||||||
const parsedRemotes = await parseRemotes(remotes)
|
|
||||||
|
|
||||||
for (const r in parsedRemotes) {
|
|
||||||
const sub = await Submenu.new({
|
|
||||||
items: parsedRemotes[r].items,
|
|
||||||
icon: parsedRemotes[r].icon,
|
|
||||||
text: r,
|
|
||||||
})
|
|
||||||
menuItems.push(sub)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await PredefinedMenuItem.new({
|
|
||||||
item: 'Separator',
|
|
||||||
}).then((item) => {
|
|
||||||
menuItems.push(item)
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!persistedStoreState.disabledActions?.includes('tray-mount')) {
|
|
||||||
const mountToMenuItem = await MenuItem.new({
|
|
||||||
id: 'mount',
|
|
||||||
text: 'Mount',
|
|
||||||
action: async () => {
|
|
||||||
await openWindow({
|
|
||||||
name: 'Mount',
|
|
||||||
url: '/mount',
|
|
||||||
})
|
|
||||||
},
|
|
||||||
})
|
|
||||||
menuItems.push(mountToMenuItem)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!persistedStoreState.disabledActions?.includes('tray-copy')) {
|
|
||||||
const copyMenuItem = await MenuItem.new({
|
|
||||||
id: 'copy',
|
|
||||||
text: 'Copy',
|
|
||||||
action: async () => {
|
|
||||||
await openWindow({
|
|
||||||
name: 'Copy',
|
|
||||||
url: '/copy',
|
|
||||||
})
|
|
||||||
},
|
|
||||||
})
|
|
||||||
menuItems.push(copyMenuItem)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!persistedStoreState.disabledActions?.includes('tray-download')) {
|
|
||||||
const downloadMenuItem = await MenuItem.new({
|
|
||||||
id: 'download',
|
|
||||||
text: 'Download',
|
|
||||||
action: async () => {
|
|
||||||
await openWindow({
|
|
||||||
name: 'Download',
|
|
||||||
url: '/download',
|
|
||||||
})
|
|
||||||
},
|
|
||||||
})
|
|
||||||
menuItems.push(downloadMenuItem)
|
|
||||||
}
|
|
||||||
|
|
||||||
const allowsAdditional =
|
|
||||||
!persistedStoreState.disabledActions?.includes('tray-sync') ||
|
|
||||||
!persistedStoreState.disabledActions?.includes('tray-move') ||
|
|
||||||
!persistedStoreState.disabledActions?.includes('tray-bisync') ||
|
|
||||||
!persistedStoreState.disabledActions?.includes('tray-serve') ||
|
|
||||||
!persistedStoreState.disabledActions?.includes('tray-purge') ||
|
|
||||||
!persistedStoreState.disabledActions?.includes('tray-delete')
|
|
||||||
|
|
||||||
if (allowsAdditional) {
|
|
||||||
const commandsSubmenuItems: (MenuItem | Submenu | PredefinedMenuItem)[] = []
|
|
||||||
|
|
||||||
if (!persistedStoreState.disabledActions?.includes('tray-sync')) {
|
|
||||||
const syncMenuItem = await MenuItem.new({
|
|
||||||
id: 'sync',
|
|
||||||
text: 'Sync',
|
|
||||||
action: async () => {
|
|
||||||
await openWindow({
|
|
||||||
name: 'Sync',
|
|
||||||
url: '/sync',
|
|
||||||
})
|
|
||||||
},
|
|
||||||
})
|
|
||||||
commandsSubmenuItems.push(syncMenuItem)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!persistedStoreState.disabledActions?.includes('tray-move')) {
|
|
||||||
const moveMenuItem = await MenuItem.new({
|
|
||||||
id: 'move',
|
|
||||||
text: 'Move',
|
|
||||||
action: async () => {
|
|
||||||
await openWindow({
|
|
||||||
name: 'Move',
|
|
||||||
url: '/move',
|
|
||||||
})
|
|
||||||
},
|
|
||||||
})
|
|
||||||
commandsSubmenuItems.push(moveMenuItem)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!persistedStoreState.disabledActions?.includes('tray-bisync')) {
|
|
||||||
const bisyncMenuItem = await MenuItem.new({
|
|
||||||
id: 'bisync',
|
|
||||||
text: 'Bisync',
|
|
||||||
action: async () => {
|
|
||||||
await openWindow({
|
|
||||||
name: 'Bisync',
|
|
||||||
url: '/bisync',
|
|
||||||
})
|
|
||||||
},
|
|
||||||
})
|
|
||||||
commandsSubmenuItems.push(bisyncMenuItem)
|
|
||||||
}
|
|
||||||
|
|
||||||
const rcloneVersion = await getRcloneVersion()
|
|
||||||
|
|
||||||
if (
|
|
||||||
rcloneVersion?.yours &&
|
|
||||||
compareVersions(rcloneVersion.yours, '1.70.0') === 1 &&
|
|
||||||
!persistedStoreState.disabledActions?.includes('tray-serve')
|
|
||||||
) {
|
|
||||||
const serveToMenuItem = await MenuItem.new({
|
|
||||||
id: 'serve',
|
|
||||||
text: 'Serve',
|
|
||||||
action: async () => {
|
|
||||||
await openWindow({
|
|
||||||
name: 'Serve',
|
|
||||||
url: '/serve',
|
|
||||||
})
|
|
||||||
},
|
|
||||||
})
|
|
||||||
commandsSubmenuItems.push(serveToMenuItem)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!persistedStoreState.disabledActions?.includes('tray-purge')) {
|
|
||||||
const purgeMenuItem = await MenuItem.new({
|
|
||||||
id: 'purge',
|
|
||||||
text: 'Purge',
|
|
||||||
action: async () => {
|
|
||||||
await openWindow({
|
|
||||||
name: 'Purge',
|
|
||||||
url: '/purge',
|
|
||||||
})
|
|
||||||
},
|
|
||||||
})
|
|
||||||
commandsSubmenuItems.push(purgeMenuItem)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!persistedStoreState.disabledActions?.includes('tray-delete')) {
|
|
||||||
const deleteMenuItem = await MenuItem.new({
|
|
||||||
id: 'delete',
|
|
||||||
text: 'Delete',
|
|
||||||
action: async () => {
|
|
||||||
await openWindow({
|
|
||||||
name: 'Delete',
|
|
||||||
url: '/delete',
|
|
||||||
})
|
|
||||||
},
|
|
||||||
})
|
|
||||||
commandsSubmenuItems.push(deleteMenuItem)
|
|
||||||
}
|
|
||||||
|
|
||||||
const commandsSubmenu = await Submenu.new({
|
|
||||||
items: commandsSubmenuItems,
|
|
||||||
text: 'Commands',
|
|
||||||
})
|
|
||||||
menuItems.push(commandsSubmenu)
|
|
||||||
}
|
|
||||||
|
|
||||||
await PredefinedMenuItem.new({
|
|
||||||
item: 'Separator',
|
|
||||||
}).then((item) => {
|
|
||||||
menuItems.push(item)
|
|
||||||
})
|
|
||||||
|
|
||||||
const jobsMenuItem = await MenuItem.new({
|
|
||||||
id: 'jobs',
|
|
||||||
text: 'Jobs',
|
|
||||||
action: async () => {
|
|
||||||
await openWindow({
|
|
||||||
name: 'Jobs',
|
|
||||||
url: '/jobs',
|
|
||||||
})
|
|
||||||
},
|
|
||||||
})
|
|
||||||
menuItems.push(jobsMenuItem)
|
|
||||||
|
|
||||||
const cronMenuItem = await MenuItem.new({
|
|
||||||
id: 'cron',
|
|
||||||
text: 'Cron',
|
|
||||||
action: async () => {
|
|
||||||
await openWindow({
|
|
||||||
name: 'Cron',
|
|
||||||
url: '/cron',
|
|
||||||
})
|
|
||||||
},
|
|
||||||
})
|
|
||||||
menuItems.push(cronMenuItem)
|
|
||||||
|
|
||||||
await PredefinedMenuItem.new({
|
|
||||||
item: 'Separator',
|
|
||||||
}).then((item) => {
|
|
||||||
menuItems.push(item)
|
|
||||||
})
|
|
||||||
|
|
||||||
const settingsItem = await MenuItem.new({
|
|
||||||
id: 'settings',
|
|
||||||
text: 'Settings',
|
|
||||||
action: async () => {
|
|
||||||
await openWindow({
|
|
||||||
name: 'Settings',
|
|
||||||
url: '/settings',
|
|
||||||
})
|
|
||||||
},
|
|
||||||
})
|
|
||||||
menuItems.push(settingsItem)
|
|
||||||
|
|
||||||
const issuesItem = await MenuItem.new({
|
|
||||||
id: 'issues',
|
|
||||||
text: 'Issues?',
|
|
||||||
action: async () => {
|
|
||||||
const confirmed = await ask(
|
|
||||||
'Please open an issue on Github and we will get it sorted in less than 48 hours, no matter if you have a license or not.',
|
|
||||||
{
|
|
||||||
title: 'Sorry ):',
|
|
||||||
kind: 'info',
|
|
||||||
okLabel: 'Open Github',
|
|
||||||
cancelLabel: 'Cancel',
|
|
||||||
}
|
|
||||||
)
|
|
||||||
if (confirmed) {
|
|
||||||
await openUrl('https://github.com/rclone-ui/rclone-ui/issues')
|
|
||||||
}
|
|
||||||
},
|
|
||||||
})
|
|
||||||
menuItems.push(issuesItem)
|
|
||||||
|
|
||||||
const quitItem = await MenuItem.new({
|
|
||||||
id: 'quit',
|
|
||||||
text: 'Quit',
|
|
||||||
action: async () => {
|
|
||||||
await getCurrentWindow().emit('close-app')
|
|
||||||
},
|
|
||||||
})
|
|
||||||
menuItems.push(quitItem)
|
|
||||||
|
|
||||||
return await Menu.new({
|
|
||||||
id: 'main-menu',
|
|
||||||
items: menuItems,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { message } from '@tauri-apps/plugin-dialog'
|
||||||
import {
|
import {
|
||||||
isPermissionGranted,
|
isPermissionGranted,
|
||||||
requestPermission,
|
requestPermission,
|
||||||
@@ -17,5 +18,10 @@ export default async function notify({ title, body }: { title: string; body: str
|
|||||||
title,
|
title,
|
||||||
body,
|
body,
|
||||||
})
|
})
|
||||||
|
} else {
|
||||||
|
await message(body, {
|
||||||
|
title,
|
||||||
|
kind: 'info',
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
'use no memo'
|
||||||
|
import { persistQueryClient } from '@tanstack/query-persist-client-core'
|
||||||
|
import { createSyncStoragePersister } from '@tanstack/query-sync-storage-persister'
|
||||||
|
import { QueryClient } from '@tanstack/react-query'
|
||||||
|
|
||||||
|
const queryClient = new QueryClient({
|
||||||
|
defaultOptions: {
|
||||||
|
queries: {
|
||||||
|
// staleTime: 60_000,
|
||||||
|
// gcTime: 3_600_000,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const persister = createSyncStoragePersister({
|
||||||
|
storage: window.localStorage,
|
||||||
|
key: 'rclone-ui-persisted-query-cache',
|
||||||
|
throttleTime: 1000,
|
||||||
|
})
|
||||||
|
|
||||||
|
persistQueryClient({
|
||||||
|
queryClient,
|
||||||
|
persister,
|
||||||
|
maxAge: 1000 * 60 * 60 * 24 * 30, // 30 days
|
||||||
|
})
|
||||||
|
|
||||||
|
export default queryClient
|
||||||
@@ -0,0 +1,382 @@
|
|||||||
|
import * as Sentry from '@sentry/browser'
|
||||||
|
import { invoke } from '@tauri-apps/api/core'
|
||||||
|
import { sep } from '@tauri-apps/api/path'
|
||||||
|
import { getAllWindows } from '@tauri-apps/api/window'
|
||||||
|
import { message } from '@tauri-apps/plugin-dialog'
|
||||||
|
import { Command } from '@tauri-apps/plugin-shell'
|
||||||
|
import { useHostStore } from '../../store/host'
|
||||||
|
import type { ConfigFile } from '../../types/config'
|
||||||
|
import { getConfigParentFolder } from '../format'
|
||||||
|
import { getConfigPath, isInternalRcloneInstalled, isSystemRcloneInstalled } from './common'
|
||||||
|
|
||||||
|
export interface RcloneCliCommandContext {
|
||||||
|
command: Command<string>
|
||||||
|
activeConfig: ConfigFile
|
||||||
|
configPath: string
|
||||||
|
configDirectory: string
|
||||||
|
env: Record<string, string>
|
||||||
|
flavour: 'system' | 'internal'
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function promptForConfigPassword(message: string) {
|
||||||
|
console.log('[promptForConfigPassword] message:', message)
|
||||||
|
try {
|
||||||
|
const result = await invoke<string | null>('tiny_prompt_text', {
|
||||||
|
title: 'Rclone UI',
|
||||||
|
message: message.replace(/"/g, '“'),
|
||||||
|
default: null,
|
||||||
|
sensitive: true,
|
||||||
|
})
|
||||||
|
console.log('[promptForConfigPassword] received result type:', typeof result)
|
||||||
|
|
||||||
|
if (typeof result === 'string') {
|
||||||
|
console.log('[promptForConfigPassword] password received, length:', result.length)
|
||||||
|
return result.trim()
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('[promptForConfigPassword] no password received (cancelled or empty)')
|
||||||
|
return null
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[promptForConfigPassword] Error prompting for password:', error)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function validateConfigAccess(
|
||||||
|
commandName: 'rclone-system' | 'rclone-internal',
|
||||||
|
env: Record<string, string>
|
||||||
|
): Promise<{
|
||||||
|
success: boolean
|
||||||
|
code?: number | null
|
||||||
|
stderr?: string
|
||||||
|
error?: Error
|
||||||
|
}> {
|
||||||
|
console.log('[validateConfigAccess] command:', commandName)
|
||||||
|
try {
|
||||||
|
const command = Command.create(commandName, ['config', 'dump'], { env })
|
||||||
|
const result = await command.execute()
|
||||||
|
console.log('[validateConfigAccess] exit code:', result.code)
|
||||||
|
|
||||||
|
if (result.code === 0) {
|
||||||
|
return { success: true }
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
code: result.code ?? null,
|
||||||
|
stderr: result.stderr,
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[validateConfigAccess] execution error:', error)
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: error instanceof Error ? error : new Error(String(error)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function ensureEncryptedConfigEnv(
|
||||||
|
activeConfig: ConfigFile,
|
||||||
|
env: Record<string, string>,
|
||||||
|
autoPromptForPassword: boolean,
|
||||||
|
commandName: 'rclone-system' | 'rclone-internal',
|
||||||
|
promptMessage: string
|
||||||
|
) {
|
||||||
|
console.log('[ensureEncryptedConfigEnv] ensuring encrypted config env for:', activeConfig.id)
|
||||||
|
|
||||||
|
env.RCLONE_ASK_PASSWORD = 'false'
|
||||||
|
|
||||||
|
if (activeConfig.passCommand) {
|
||||||
|
console.log('[ensureEncryptedConfigEnv] using passCommand for decryption')
|
||||||
|
const validationEnv = {
|
||||||
|
...env,
|
||||||
|
RCLONE_CONFIG_PASS_COMMAND: activeConfig.passCommand,
|
||||||
|
}
|
||||||
|
|
||||||
|
const validation = await validateConfigAccess(commandName, validationEnv)
|
||||||
|
if (validation.success) {
|
||||||
|
console.log('[ensureEncryptedConfigEnv] passCommand validation succeeded')
|
||||||
|
env.RCLONE_CONFIG_PASS_COMMAND = activeConfig.passCommand
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
console.error('[ensureEncryptedConfigEnv] passCommand validation failed', validation.code)
|
||||||
|
await message(
|
||||||
|
'Failed to decrypt the configuration using the configured password command. Please verify the command output.',
|
||||||
|
{
|
||||||
|
title: 'Password Command Failed',
|
||||||
|
kind: 'error',
|
||||||
|
okLabel: 'OK',
|
||||||
|
}
|
||||||
|
)
|
||||||
|
throw new Error('Failed to decrypt configuration using pass command.')
|
||||||
|
}
|
||||||
|
|
||||||
|
let password = activeConfig.pass ?? undefined
|
||||||
|
let passwordSource: 'stored' | 'prompted' | null = password ? 'stored' : null
|
||||||
|
|
||||||
|
const hostStore = useHostStore.getState()
|
||||||
|
const updateConfigFile = hostStore.updateConfigFile
|
||||||
|
const activeConfigId = activeConfig.id
|
||||||
|
|
||||||
|
if (!password && !autoPromptForPassword) {
|
||||||
|
throw new Error('Password required to access encrypted configuration.')
|
||||||
|
}
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
if (!password) {
|
||||||
|
if (!autoPromptForPassword) {
|
||||||
|
throw new Error('Password required to access encrypted configuration.')
|
||||||
|
}
|
||||||
|
|
||||||
|
const promptedPassword = await promptForConfigPassword(promptMessage)
|
||||||
|
console.log('[ensureEncryptedConfigEnv] password received:', !!promptedPassword)
|
||||||
|
|
||||||
|
if (!promptedPassword) {
|
||||||
|
console.log('[ensureEncryptedConfigEnv] password prompt cancelled by user')
|
||||||
|
throw new Error('Password prompt cancelled by user.')
|
||||||
|
}
|
||||||
|
|
||||||
|
password = promptedPassword
|
||||||
|
passwordSource = 'prompted'
|
||||||
|
}
|
||||||
|
|
||||||
|
const validationEnv = {
|
||||||
|
...env,
|
||||||
|
RCLONE_CONFIG_PASS: password,
|
||||||
|
}
|
||||||
|
|
||||||
|
const validation = await validateConfigAccess(commandName, validationEnv)
|
||||||
|
if (validation.success) {
|
||||||
|
console.log('[ensureEncryptedConfigEnv] password validation succeeded')
|
||||||
|
env.RCLONE_CONFIG_PASS = password
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
console.error('[ensureEncryptedConfigEnv] password validation failed', validation.code)
|
||||||
|
|
||||||
|
if (passwordSource === 'stored') {
|
||||||
|
console.log('[ensureEncryptedConfigEnv] clearing invalid stored password')
|
||||||
|
if (activeConfigId && updateConfigFile) {
|
||||||
|
try {
|
||||||
|
updateConfigFile(activeConfigId, { pass: undefined })
|
||||||
|
console.log('[ensureEncryptedConfigEnv] stored password cleared')
|
||||||
|
} catch (error) {
|
||||||
|
Sentry.captureException(error)
|
||||||
|
console.error(
|
||||||
|
'[ensureEncryptedConfigEnv] failed to clear stored password',
|
||||||
|
error
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await message(
|
||||||
|
'The saved password for this configuration appears to be incorrect. You will be prompted for a new password.',
|
||||||
|
{
|
||||||
|
title: 'Invalid Password',
|
||||||
|
kind: 'error',
|
||||||
|
okLabel: 'OK',
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
password = undefined
|
||||||
|
passwordSource = null
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!autoPromptForPassword) {
|
||||||
|
throw new Error('Password required to access encrypted configuration.')
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await message('Incorrect password. Please try again.', {
|
||||||
|
title: 'Invalid Password',
|
||||||
|
kind: 'error',
|
||||||
|
buttons: {
|
||||||
|
ok: 'Try Again',
|
||||||
|
cancel: 'Cancel',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
if (response !== 'Try Again') {
|
||||||
|
console.log('[ensureEncryptedConfigEnv] user cancelled password retry')
|
||||||
|
throw new Error('Password prompt cancelled by user.')
|
||||||
|
}
|
||||||
|
|
||||||
|
password = undefined
|
||||||
|
passwordSource = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createRcloneCliCommand(
|
||||||
|
args: string[],
|
||||||
|
additionalEnv?: Record<string, string>,
|
||||||
|
autoPromptForPassword = true
|
||||||
|
): Promise<RcloneCliCommandContext> {
|
||||||
|
console.log('[createRcloneCliCommand] creating rclone CLI command with args:', args)
|
||||||
|
const env: Record<string, string> = {}
|
||||||
|
const hostStore = useHostStore.getState()
|
||||||
|
const activeConfig = hostStore.activeConfigFile
|
||||||
|
|
||||||
|
if (!activeConfig || !activeConfig.id) {
|
||||||
|
throw new Error('No active configuration selected.')
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('[createRcloneCliCommand] active config:', activeConfig)
|
||||||
|
|
||||||
|
let configPath: string
|
||||||
|
try {
|
||||||
|
configPath = await getConfigPath({ id: activeConfig.id, validate: true })
|
||||||
|
} catch (error) {
|
||||||
|
Sentry.captureException(error)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('[createRcloneCliCommand] config path:', configPath)
|
||||||
|
|
||||||
|
const configDirectory = getConfigParentFolder(configPath)
|
||||||
|
console.log('[createRcloneCliCommand] config directory:', configDirectory)
|
||||||
|
|
||||||
|
const proxy = hostStore.proxy
|
||||||
|
console.log('[createRcloneCliCommand] proxy:', proxy)
|
||||||
|
if (proxy?.url) {
|
||||||
|
env.http_proxy = proxy.url
|
||||||
|
env.https_proxy = proxy.url
|
||||||
|
env.HTTP_PROXY = proxy.url
|
||||||
|
env.HTTPS_PROXY = proxy.url
|
||||||
|
env.no_proxy = proxy.ignoredHosts.join(',')
|
||||||
|
env.NO_PROXY = proxy.ignoredHosts.join(',')
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('[createRcloneCliCommand] checking for system rclone installation')
|
||||||
|
const hasSystem = await isSystemRcloneInstalled()
|
||||||
|
console.log('[createRcloneCliCommand] checking for internal rclone installation')
|
||||||
|
const hasInternal = await isInternalRcloneInstalled()
|
||||||
|
|
||||||
|
console.log('[createRcloneCliCommand] has system:', hasSystem)
|
||||||
|
console.log('[createRcloneCliCommand] has internal:', hasInternal)
|
||||||
|
|
||||||
|
if (!hasSystem && !hasInternal) {
|
||||||
|
console.log('[createRcloneCliCommand] no rclone installation found')
|
||||||
|
const error = new Error('Unable to locate an rclone installation.')
|
||||||
|
Sentry.captureException(error)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
|
||||||
|
const flavour = hasSystem ? 'system' : 'internal'
|
||||||
|
const commandName = flavour === 'system' ? 'rclone-system' : 'rclone-internal'
|
||||||
|
|
||||||
|
if (!hasSystem || activeConfig.id !== 'default') {
|
||||||
|
console.log('[createRcloneCliCommand] setting config directory and path')
|
||||||
|
env.RCLONE_CONFIG_DIR = configDirectory
|
||||||
|
env.RCLONE_CONFIG = configPath.endsWith('rclone.conf')
|
||||||
|
? configPath
|
||||||
|
: `${configDirectory}${sep()}rclone.conf`
|
||||||
|
}
|
||||||
|
|
||||||
|
if (activeConfig.isEncrypted) {
|
||||||
|
console.log('[createRcloneCliCommand] ensuring encrypted configuration access')
|
||||||
|
await ensureEncryptedConfigEnv(
|
||||||
|
activeConfig,
|
||||||
|
env,
|
||||||
|
autoPromptForPassword,
|
||||||
|
commandName,
|
||||||
|
`Please enter the current password for "${activeConfig.label}"`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (additionalEnv) {
|
||||||
|
console.log('[createRcloneCliCommand] setting additional environment')
|
||||||
|
Object.assign(env, additionalEnv)
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
'[createRcloneCliCommand] creating command, name:',
|
||||||
|
commandName,
|
||||||
|
'args:',
|
||||||
|
args,
|
||||||
|
'env:',
|
||||||
|
env
|
||||||
|
)
|
||||||
|
|
||||||
|
const command = Command.create(commandName, args, { env })
|
||||||
|
|
||||||
|
console.log('[createRcloneCliCommand] command created')
|
||||||
|
|
||||||
|
return {
|
||||||
|
command,
|
||||||
|
activeConfig,
|
||||||
|
configPath,
|
||||||
|
configDirectory,
|
||||||
|
env,
|
||||||
|
flavour,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function runRcloneCli(args: string[], input: string[] = []) {
|
||||||
|
const { command } = await createRcloneCliCommand(args, undefined, true)
|
||||||
|
|
||||||
|
let stdout = ''
|
||||||
|
let stderr = ''
|
||||||
|
|
||||||
|
console.log('[runRcloneCli] running command', 'args:', args, 'input:', input)
|
||||||
|
|
||||||
|
return await new Promise<void>((resolve, reject) => {
|
||||||
|
command.stdout.on('data', (line) => {
|
||||||
|
console.log('[runRcloneCli] stdout:', line)
|
||||||
|
stdout += line
|
||||||
|
})
|
||||||
|
command.stderr.on('data', (line) => {
|
||||||
|
console.log('[runRcloneCli] stderr:', line)
|
||||||
|
stderr += line
|
||||||
|
})
|
||||||
|
command.addListener('error', (event) => {
|
||||||
|
console.log('[runRcloneCli] error:', event)
|
||||||
|
const error = typeof event === 'string' ? new Error(event) : event
|
||||||
|
Sentry.captureException(error)
|
||||||
|
reject(error instanceof Error ? error : new Error('Unknown rclone CLI error.'))
|
||||||
|
})
|
||||||
|
command.addListener('close', (event) => {
|
||||||
|
console.log('[runRcloneCli] close:', event)
|
||||||
|
|
||||||
|
if (event.code === 0) {
|
||||||
|
resolve()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const error = new Error(
|
||||||
|
`rclone command failed (code ${event.code ?? 'unknown'}): ${stderr || stdout}`
|
||||||
|
)
|
||||||
|
Sentry.captureException(error)
|
||||||
|
reject(error)
|
||||||
|
})
|
||||||
|
|
||||||
|
command
|
||||||
|
.spawn()
|
||||||
|
.then(async (child) => {
|
||||||
|
console.log('[runRcloneCli] child:', child)
|
||||||
|
for (const line of input) {
|
||||||
|
console.log('[runRcloneCli] writing input:', line)
|
||||||
|
await child.write(`${line}\n`)
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 100))
|
||||||
|
console.log('[runRcloneCli] input written')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.log('[runRcloneCli] error:', error)
|
||||||
|
Sentry.captureException(error)
|
||||||
|
reject(error)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function restartActiveRclone() {
|
||||||
|
try {
|
||||||
|
;(await getAllWindows())
|
||||||
|
.filter((window) => window.label === 'main')[0]
|
||||||
|
.emit('restart-rclone')
|
||||||
|
// await getCurrentWindow().emit('restart-rclone')
|
||||||
|
} catch (error) {
|
||||||
|
Sentry.captureException(error)
|
||||||
|
console.error('[restartActiveRclone] failed to emit restart event', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,110 @@
|
|||||||
|
import { fetch as tauriFetch } from '@tauri-apps/plugin-http'
|
||||||
|
import pRetry from 'p-retry'
|
||||||
|
import createRCDClient, {
|
||||||
|
type OpenApiMethodResponse,
|
||||||
|
type OpenApiClient,
|
||||||
|
type OpenApiClientPathsWithMethod,
|
||||||
|
type OpenApiMaybeOptionalInit,
|
||||||
|
type OpenApiRequiredKeysOf,
|
||||||
|
type RCDClient,
|
||||||
|
} from 'rclone-sdk'
|
||||||
|
import { usePersistedStore } from '../../store/persisted'
|
||||||
|
|
||||||
|
// const client = createRCDClient({
|
||||||
|
// baseUrl: 'http://localhost:5572',
|
||||||
|
// fetch: (request: Request) => tauriFetch(request),
|
||||||
|
// })
|
||||||
|
|
||||||
|
let client: RCDClient | null = null
|
||||||
|
|
||||||
|
function getClient() {
|
||||||
|
if (!client) {
|
||||||
|
const currentHost = usePersistedStore.getState().currentHost
|
||||||
|
if (!currentHost) {
|
||||||
|
console.error('[rclone] No current host')
|
||||||
|
throw new Error('No current host')
|
||||||
|
}
|
||||||
|
|
||||||
|
let authHeader = ''
|
||||||
|
if (currentHost.authUser && currentHost.authPassword) {
|
||||||
|
authHeader = `Basic ${btoa(`${currentHost.authUser}:${currentHost.authPassword}`)}`
|
||||||
|
}
|
||||||
|
|
||||||
|
client = createRCDClient({
|
||||||
|
baseUrl: currentHost.url,
|
||||||
|
headers: authHeader
|
||||||
|
? {
|
||||||
|
'Authorization': authHeader,
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
|
fetch: (request: Request) => tauriFetch(request),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return client
|
||||||
|
}
|
||||||
|
|
||||||
|
export function clearClient() {
|
||||||
|
client = null
|
||||||
|
}
|
||||||
|
|
||||||
|
type ClientPaths<T> = T extends OpenApiClient<infer P, any> ? P : never
|
||||||
|
type Paths = ClientPaths<RCDClient>
|
||||||
|
type InitParam<Init> = OpenApiRequiredKeysOf<Init> extends never
|
||||||
|
? [(Init & { [key: string]: unknown })?]
|
||||||
|
: [Init & { [key: string]: unknown }]
|
||||||
|
|
||||||
|
export default async function rclone<
|
||||||
|
Path extends OpenApiClientPathsWithMethod<RCDClient, 'post'>,
|
||||||
|
Init extends OpenApiMaybeOptionalInit<Paths[Path], 'post'> = OpenApiMaybeOptionalInit<
|
||||||
|
Paths[Path],
|
||||||
|
'post'
|
||||||
|
>,
|
||||||
|
>(
|
||||||
|
path: Path,
|
||||||
|
...init: InitParam<Init>
|
||||||
|
): Promise<OpenApiMethodResponse<RCDClient, 'post', Path, Init>> {
|
||||||
|
console.log('[rclone] STARTING', path)
|
||||||
|
|
||||||
|
const client = await pRetry(() => getClient(), {
|
||||||
|
'maxTimeout': 500,
|
||||||
|
}) //! for some reason this still fails sometimes
|
||||||
|
|
||||||
|
if (!client) {
|
||||||
|
throw new Error('Failed to get client after retries')
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('[rclone] init', init)
|
||||||
|
const result = await client.POST(
|
||||||
|
path,
|
||||||
|
...(init as InitParam<OpenApiMaybeOptionalInit<Paths[Path], 'post'>>)
|
||||||
|
)
|
||||||
|
|
||||||
|
if (result?.error) {
|
||||||
|
console.log('[rclone] result?.error', result.error)
|
||||||
|
const message =
|
||||||
|
typeof result.error === 'string' ? result.error : JSON.stringify(result.error)
|
||||||
|
|
||||||
|
throw new Error(message)
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = result.data as { error?: unknown } | undefined
|
||||||
|
if (data?.error) {
|
||||||
|
console.log('[rclone] data?.error', data.error)
|
||||||
|
const message = typeof data.error === 'string' ? data.error : JSON.stringify(data.error)
|
||||||
|
|
||||||
|
throw new Error(message)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!result.response.ok) {
|
||||||
|
console.log(
|
||||||
|
'[rclone] !result.response.ok',
|
||||||
|
result.response.status,
|
||||||
|
result.response.statusText
|
||||||
|
)
|
||||||
|
throw new Error(`${result.response.status} ${result.response.statusText}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('[rclone] ENDING', path)
|
||||||
|
|
||||||
|
return result.data as OpenApiMethodResponse<typeof client, 'post', Path, Init>
|
||||||
|
}
|
||||||
@@ -1,24 +1,18 @@
|
|||||||
import { appLocalDataDir, sep } from '@tauri-apps/api/path'
|
import { appLocalDataDir, sep } from '@tauri-apps/api/path'
|
||||||
import { exists, mkdir, writeTextFile } from '@tauri-apps/plugin-fs'
|
import { exists, mkdir, writeTextFile } from '@tauri-apps/plugin-fs'
|
||||||
import { fetch } from '@tauri-apps/plugin-http'
|
import { fetch as tauriFetch } from '@tauri-apps/plugin-http'
|
||||||
import { Command } from '@tauri-apps/plugin-shell'
|
import { Command } from '@tauri-apps/plugin-shell'
|
||||||
|
import createRCDClient from 'rclone-sdk'
|
||||||
|
import { useHostStore } from '../../store/host'
|
||||||
|
import type { FlagValue } from '../../types/rclone'
|
||||||
import { getConfigParentFolder } from '../format'
|
import { getConfigParentFolder } from '../format'
|
||||||
import { usePersistedStore } from '../store'
|
import rclone from './client'
|
||||||
import { DOUBLE_BACKSLASH_REGEX } from './constants'
|
import { DOUBLE_BACKSLASH_REGEX } from './constants'
|
||||||
|
|
||||||
export async function getDefaultPaths() {
|
export async function getDefaultPaths() {
|
||||||
console.log('[getDefaultPaths]')
|
console.log('[getDefaultPaths]')
|
||||||
|
|
||||||
const r = await fetch('http://localhost:5572/config/paths', {
|
const defaultPaths = await rclone('/config/paths')
|
||||||
method: 'POST',
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!r.ok) {
|
|
||||||
console.error('[getDefaultPaths] failed to make request to config/paths')
|
|
||||||
throw new Error('Failed to make request to config/paths')
|
|
||||||
}
|
|
||||||
|
|
||||||
const defaultPaths = (await r.json()) as { cache: string; config: string; temp: string }
|
|
||||||
|
|
||||||
console.log('[getDefaultPaths] json', JSON.stringify(defaultPaths, null, 2))
|
console.log('[getDefaultPaths] json', JSON.stringify(defaultPaths, null, 2))
|
||||||
|
|
||||||
@@ -33,6 +27,7 @@ export async function getDefaultPaths() {
|
|||||||
|
|
||||||
export async function getSystemConfigPath() {
|
export async function getSystemConfigPath() {
|
||||||
console.log('[getSystemConfigPath] running system rclone')
|
console.log('[getSystemConfigPath] running system rclone')
|
||||||
|
|
||||||
const instance = Command.create('rclone-system', [
|
const instance = Command.create('rclone-system', [
|
||||||
'rcd',
|
'rcd',
|
||||||
'--rc-no-auth',
|
'--rc-no-auth',
|
||||||
@@ -53,13 +48,20 @@ export async function getSystemConfigPath() {
|
|||||||
await new Promise((resolve) => setTimeout(resolve, 200))
|
await new Promise((resolve) => setTimeout(resolve, 200))
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const defaultPaths = await getDefaultPaths()
|
// no host store at this point
|
||||||
|
const client = createRCDClient({
|
||||||
|
baseUrl: 'http://localhost:5572',
|
||||||
|
fetch: (request: Request) => tauriFetch(request),
|
||||||
|
})
|
||||||
|
|
||||||
if (typeof defaultPaths?.config === 'undefined') {
|
const defaultPaths = await client.POST('/config/paths', {})
|
||||||
|
|
||||||
|
const configPath = defaultPaths.data?.config
|
||||||
|
if (!configPath) {
|
||||||
throw new Error('Failed to fetch config path')
|
throw new Error('Failed to fetch config path')
|
||||||
}
|
}
|
||||||
|
|
||||||
return defaultPaths.config
|
return configPath.replace(DOUBLE_BACKSLASH_REGEX, '\\')
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[getSystemConfigPath] error', error)
|
console.error('[getSystemConfigPath] error', error)
|
||||||
if (error instanceof Error) {
|
if (error instanceof Error) {
|
||||||
@@ -166,7 +168,7 @@ export async function isInternalRcloneInstalled() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function parseRcloneOptions(options: Record<string, string | number | boolean | string[]>) {
|
export function parseRcloneOptions(options: Record<string, FlagValue>) {
|
||||||
console.log('[parseRcloneOptions]', options)
|
console.log('[parseRcloneOptions]', options)
|
||||||
|
|
||||||
return options
|
return options
|
||||||
@@ -247,7 +249,7 @@ export function shouldUpdateRclone(versionData: { yours: string; latest: string
|
|||||||
console.log('[shouldUpdateRclone] current version:', currentVersion)
|
console.log('[shouldUpdateRclone] current version:', currentVersion)
|
||||||
console.log('[shouldUpdateRclone] latest version:', latestVersion)
|
console.log('[shouldUpdateRclone] latest version:', latestVersion)
|
||||||
|
|
||||||
if (usePersistedStore.getState().lastSkippedVersion === currentVersion) {
|
if (useHostStore.getState().lastSkippedVersion === currentVersion) {
|
||||||
console.log('[shouldUpdateRclone] current version is in the lastSkippedVersion')
|
console.log('[shouldUpdateRclone] current version is in the lastSkippedVersion')
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,83 +1,177 @@
|
|||||||
export const RCLONE_CONFIG_DEFAULTS = {
|
export const RCLONE_CONFIG_DEFAULTS = {
|
||||||
'MultiThreadCutoff': '64M',
|
copy: {
|
||||||
'MultiThreadStreams': 8,
|
'multi_thread_cutoff': '64M',
|
||||||
'MultiThreadChunkSize': '8M',
|
'multi_thread_streams': 8,
|
||||||
'UseListR': true,
|
'multi_thread_chunk_size': '8M',
|
||||||
'UseServerModTime': true,
|
},
|
||||||
'BufferSize': '32M',
|
config: {
|
||||||
'Transfers': 8,
|
'fast_list': true,
|
||||||
'Checkers': 16,
|
'use_server_modtime': true,
|
||||||
}
|
'buffer_size': '32M',
|
||||||
|
'transfers': 8,
|
||||||
export const RCLONE_VFS_DEFAULTS = {
|
'checkers': 16,
|
||||||
'ChunkSize': '4M',
|
},
|
||||||
'ChunkStreams': 16,
|
vfs: {
|
||||||
}
|
'chunk_size': '4M',
|
||||||
|
'chunk_streams': 16,
|
||||||
|
},
|
||||||
|
} as const
|
||||||
|
|
||||||
export const RCLONE_CONF_REGEX = /[\/\\]rclone\.conf$/
|
export const RCLONE_CONF_REGEX = /[\/\\]rclone\.conf$/
|
||||||
export const DOUBLE_BACKSLASH_REGEX = /\\\\/g
|
export const DOUBLE_BACKSLASH_REGEX = /\\\\/g
|
||||||
|
|
||||||
export const SUPPORTED_BACKENDS = [
|
export const SERVE_TYPES = ['dlna', 'ftp', 'sftp', 'http', 'nfs', 'restic', 's3', 'webdav'] as const
|
||||||
'azureblob',
|
|
||||||
'azurefiles',
|
export const SUPPORTS_CLEANUP = [
|
||||||
|
's3',
|
||||||
'b2',
|
'b2',
|
||||||
'box',
|
'box',
|
||||||
'drive',
|
|
||||||
'dropbox',
|
|
||||||
'sftp',
|
|
||||||
's3',
|
|
||||||
'ftp',
|
|
||||||
'gcs',
|
|
||||||
'protondrive',
|
|
||||||
'webdav',
|
|
||||||
'onedrive',
|
|
||||||
'http',
|
|
||||||
'storj',
|
|
||||||
'gphotos',
|
|
||||||
'netstorage',
|
|
||||||
'oos',
|
|
||||||
'swift',
|
|
||||||
'compress',
|
|
||||||
'union',
|
|
||||||
'hasher',
|
|
||||||
'combine',
|
|
||||||
'chunker',
|
|
||||||
'memory',
|
|
||||||
'crypt',
|
|
||||||
'alias',
|
|
||||||
'cache',
|
|
||||||
'local',
|
|
||||||
'hdfs',
|
|
||||||
'cloudinary',
|
|
||||||
'doi',
|
|
||||||
'filelu',
|
|
||||||
'filescom',
|
|
||||||
'imagekit',
|
|
||||||
'internetarchive',
|
|
||||||
'koofr',
|
|
||||||
'linkbox',
|
|
||||||
'mega',
|
|
||||||
'opendrive',
|
|
||||||
'pixeldrain',
|
|
||||||
'seafile',
|
|
||||||
'sia',
|
|
||||||
'smb',
|
|
||||||
'ulozto',
|
|
||||||
'gofile',
|
|
||||||
'qingstor',
|
|
||||||
'filefabric',
|
'filefabric',
|
||||||
'quatrix',
|
'drive',
|
||||||
'sugarsync',
|
'internetarchive',
|
||||||
'hidrive',
|
|
||||||
'jottacloud',
|
'jottacloud',
|
||||||
'mailru',
|
'mailru',
|
||||||
|
'mega',
|
||||||
|
'onedrive',
|
||||||
|
'oos',
|
||||||
'pcloud',
|
'pcloud',
|
||||||
'pikpak',
|
'pikpak',
|
||||||
|
'putio',
|
||||||
|
'protondrive',
|
||||||
|
'qingstor',
|
||||||
|
'seafile',
|
||||||
|
'yandex',
|
||||||
|
] as const
|
||||||
|
|
||||||
|
export const SUPPORTS_PURGE = [
|
||||||
|
'netstorage',
|
||||||
|
'box',
|
||||||
|
'sharefile',
|
||||||
|
'dropbox',
|
||||||
|
'filefabric',
|
||||||
|
'filescom',
|
||||||
|
'gofile',
|
||||||
|
'gcs',
|
||||||
|
'drive',
|
||||||
|
'hdfs',
|
||||||
|
'hifile',
|
||||||
|
'iclouddrive',
|
||||||
|
'imagekit',
|
||||||
|
'jottacloud',
|
||||||
|
'koofr',
|
||||||
|
'mailru',
|
||||||
|
'mega',
|
||||||
|
'azureblob',
|
||||||
|
'onedrive',
|
||||||
|
'opendrive',
|
||||||
|
'swift',
|
||||||
|
'pikpak',
|
||||||
|
'pcloud',
|
||||||
|
'pixeldrain',
|
||||||
'premiumizeme',
|
'premiumizeme',
|
||||||
'putio',
|
'putio',
|
||||||
'sharefile',
|
'protondrive',
|
||||||
|
'quatrix',
|
||||||
|
'seafile',
|
||||||
|
'sugarsync',
|
||||||
|
'storj',
|
||||||
|
'webdav',
|
||||||
'yandex',
|
'yandex',
|
||||||
'zoho',
|
'zoho',
|
||||||
]
|
] as const
|
||||||
|
|
||||||
export const SERVE_TYPES = ['dlna', 'ftp', 'sftp', 'http', 'nfs', 'restic', 's3', 'webdav'] as const
|
export const SUPPORTS_ABOUT = [
|
||||||
|
'box',
|
||||||
|
'dropbox',
|
||||||
|
'gofile',
|
||||||
|
'drive',
|
||||||
|
'hdfs',
|
||||||
|
'internetarchive',
|
||||||
|
'jottacloud',
|
||||||
|
'koofr',
|
||||||
|
'mailru',
|
||||||
|
'mega',
|
||||||
|
'azurefiles',
|
||||||
|
'onedrive',
|
||||||
|
'opendrive',
|
||||||
|
'swift',
|
||||||
|
'pcloud',
|
||||||
|
'pikpak',
|
||||||
|
'pixeldrain',
|
||||||
|
'premiumizeme',
|
||||||
|
'putio',
|
||||||
|
'protondrive',
|
||||||
|
'quatrix',
|
||||||
|
'seafile',
|
||||||
|
'sftp',
|
||||||
|
'webdav',
|
||||||
|
'yandex',
|
||||||
|
'zoho',
|
||||||
|
'local',
|
||||||
|
] as const
|
||||||
|
|
||||||
|
// export const SUPPORTED_OPERATIONS = [
|
||||||
|
// {
|
||||||
|
// id: 'uncategorized',
|
||||||
|
// name: 'Uncategorized',
|
||||||
|
// icon: <FileIcon />,
|
||||||
|
// titleColor: 'text-foreground',
|
||||||
|
// indicatorColor: 'text-foreground-500',
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// id: 'copy',
|
||||||
|
// name: 'Copy',
|
||||||
|
// icon: <CopyIcon />,
|
||||||
|
// titleColor: 'text-primary-400',
|
||||||
|
// indicatorColor: 'text-primary-300',
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// id: 'move',
|
||||||
|
// name: 'Move',
|
||||||
|
// icon: <MoveIcon />,
|
||||||
|
// titleColor: 'text-primary-400',
|
||||||
|
// indicatorColor: 'text-primary-300',
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// id: 'delete',
|
||||||
|
// name: 'Delete',
|
||||||
|
// icon: <TrashIcon />,
|
||||||
|
// titleColor: 'text-danger-400',
|
||||||
|
// indicatorColor: 'text-danger-300',
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// id: 'sync',
|
||||||
|
// name: 'Sync',
|
||||||
|
// icon: <ArrowRightLeftIcon />,
|
||||||
|
// titleColor: 'text-success-300',
|
||||||
|
// indicatorColor: 'text-success-300',
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// id: 'bisync',
|
||||||
|
// name: 'Bisync',
|
||||||
|
// icon: <ArrowRightLeftIcon />,
|
||||||
|
// titleColor: 'text-primary-400',
|
||||||
|
// indicatorColor: 'text-primary-300',
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// id: 'mount',
|
||||||
|
// name: 'Mount',
|
||||||
|
// icon: <HardDriveIcon />,
|
||||||
|
// titleColor: 'text-secondary-500',
|
||||||
|
// indicatorColor: 'text-secondary-400',
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// id: 'purge',
|
||||||
|
// name: 'Purge',
|
||||||
|
// icon: <Trash2Icon />,
|
||||||
|
// titleColor: 'text-warning-300',
|
||||||
|
// indicatorColor: 'text-warning-300',
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// id: 'serve',
|
||||||
|
// name: 'Serve',
|
||||||
|
// icon: <ServerIcon />,
|
||||||
|
// titleColor: 'text-cyan-500',
|
||||||
|
// indicatorColor: 'text-cyan-300',
|
||||||
|
// },
|
||||||
|
// ] as const
|
||||||
|
|||||||
@@ -7,11 +7,14 @@ import { copyFile, exists, mkdir, readTextFile, remove } from '@tauri-apps/plugi
|
|||||||
import { writeFile } from '@tauri-apps/plugin-fs'
|
import { writeFile } from '@tauri-apps/plugin-fs'
|
||||||
import { fetch } from '@tauri-apps/plugin-http'
|
import { fetch } from '@tauri-apps/plugin-http'
|
||||||
import { platform } from '@tauri-apps/plugin-os'
|
import { platform } from '@tauri-apps/plugin-os'
|
||||||
import { exit } from '@tauri-apps/plugin-process'
|
import { exit, relaunch } from '@tauri-apps/plugin-process'
|
||||||
import { Command } from '@tauri-apps/plugin-shell'
|
import { Command } from '@tauri-apps/plugin-shell'
|
||||||
|
import { useHostStore } from '../../store/host'
|
||||||
|
import { useStore } from '../../store/memory'
|
||||||
|
import { usePersistedStore } from '../../store/persisted'
|
||||||
import { getConfigParentFolder } from '../format'
|
import { getConfigParentFolder } from '../format'
|
||||||
import { usePersistedStore, useStore } from '../store'
|
|
||||||
import { openSmallWindow } from '../window'
|
import { openSmallWindow } from '../window'
|
||||||
|
import { ensureEncryptedConfigEnv } from './cli'
|
||||||
import {
|
import {
|
||||||
createConfigFile,
|
createConfigFile,
|
||||||
getConfigPath,
|
getConfigPath,
|
||||||
@@ -25,7 +28,7 @@ import {
|
|||||||
export async function initRclone(args: string[]) {
|
export async function initRclone(args: string[]) {
|
||||||
console.log('[initRclone] starting with args:', args)
|
console.log('[initRclone] starting with args:', args)
|
||||||
|
|
||||||
const system = await isSystemRcloneInstalled()
|
const system = !(await invoke<boolean>('is_flathub')) && (await isSystemRcloneInstalled())
|
||||||
console.log('[initRclone] system rclone installed:', system)
|
console.log('[initRclone] system rclone installed:', system)
|
||||||
let internal = await isInternalRcloneInstalled()
|
let internal = await isInternalRcloneInstalled()
|
||||||
console.log('[initRclone] internal rclone installed:', internal)
|
console.log('[initRclone] internal rclone installed:', internal)
|
||||||
@@ -33,8 +36,7 @@ export async function initRclone(args: string[]) {
|
|||||||
// rclone not available, let's download it
|
// rclone not available, let's download it
|
||||||
if (!system && !internal) {
|
if (!system && !internal) {
|
||||||
console.log('[initRclone] no rclone installation found, provisioning...')
|
console.log('[initRclone] no rclone installation found, provisioning...')
|
||||||
useStore.setState({ startupDisplayed: true })
|
useStore.setState({ startupDisplayed: true, startupStatus: 'initializing' })
|
||||||
useStore.setState({ startupStatus: 'initializing' })
|
|
||||||
await openSmallWindow({
|
await openSmallWindow({
|
||||||
name: 'Startup',
|
name: 'Startup',
|
||||||
url: '/startup',
|
url: '/startup',
|
||||||
@@ -46,6 +48,7 @@ export async function initRclone(args: string[]) {
|
|||||||
useStore.setState({ startupStatus: 'fatal' })
|
useStore.setState({ startupStatus: 'fatal' })
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('[initRclone] provision succeeded')
|
console.log('[initRclone] provision succeeded')
|
||||||
useStore.setState({ startupStatus: 'initialized' })
|
useStore.setState({ startupStatus: 'initialized' })
|
||||||
|
|
||||||
@@ -92,7 +95,7 @@ export async function initRclone(args: string[]) {
|
|||||||
console.log('[initRclone] user skipping version:', skipping)
|
console.log('[initRclone] user skipping version:', skipping)
|
||||||
if (skipping) {
|
if (skipping) {
|
||||||
console.log('[initRclone] saving skipped version:', rcloneVersion!.yours)
|
console.log('[initRclone] saving skipped version:', rcloneVersion!.yours)
|
||||||
usePersistedStore.setState({ lastSkippedVersion: rcloneVersion!.yours })
|
useHostStore.setState({ lastSkippedVersion: rcloneVersion!.yours })
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
console.log('[initRclone] system rclone updated successfully')
|
console.log('[initRclone] system rclone updated successfully')
|
||||||
@@ -119,12 +122,14 @@ export async function initRclone(args: string[]) {
|
|||||||
console.error('[initRclone] failed to update rclone', error)
|
console.error('[initRclone] failed to update rclone', error)
|
||||||
useStore.setState({ startupStatus: 'error' })
|
useStore.setState({ startupStatus: 'error' })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 1000))
|
||||||
}
|
}
|
||||||
|
|
||||||
const persistedState = usePersistedStore.getState()
|
const hostState = useHostStore.getState()
|
||||||
let configFiles = persistedState.configFiles || []
|
let configFiles = hostState.configFiles || []
|
||||||
console.log('[initRclone] loaded config files count:', configFiles.length)
|
console.log('[initRclone] loaded config files count:', configFiles.length)
|
||||||
let activeConfigFile = persistedState.activeConfigFile
|
let activeConfigFile = hostState.activeConfigFile
|
||||||
console.log('[initRclone] active config file:', activeConfigFile?.id)
|
console.log('[initRclone] active config file:', activeConfigFile?.id)
|
||||||
|
|
||||||
if (system) {
|
if (system) {
|
||||||
@@ -135,18 +140,24 @@ export async function initRclone(args: string[]) {
|
|||||||
console.log('[initRclone] created system config file')
|
console.log('[initRclone] created system config file')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const existingDefaultConfig = configFiles.find((config) => config.id === 'default')
|
||||||
configFiles = configFiles.filter((config) => config.id !== 'default')
|
configFiles = configFiles.filter((config) => config.id !== 'default')
|
||||||
console.log('[initRclone] filtered config files, remaining count:', configFiles.length)
|
console.log('[initRclone] filtered config files, remaining count:', configFiles.length)
|
||||||
configFiles.unshift({
|
|
||||||
id: 'default',
|
const defaultConfig = existingDefaultConfig
|
||||||
label: 'Default config',
|
? existingDefaultConfig
|
||||||
sync: undefined,
|
: {
|
||||||
isEncrypted: false,
|
id: 'default',
|
||||||
pass: undefined,
|
label: 'Default config',
|
||||||
passCommand: undefined,
|
sync: undefined,
|
||||||
})
|
isEncrypted: false,
|
||||||
|
pass: undefined,
|
||||||
|
passCommand: undefined,
|
||||||
|
}
|
||||||
|
|
||||||
|
configFiles.unshift(defaultConfig)
|
||||||
console.log('[initRclone] added default config to list')
|
console.log('[initRclone] added default config to list')
|
||||||
usePersistedStore.setState({ configFiles })
|
useHostStore.setState({ configFiles })
|
||||||
|
|
||||||
if (!activeConfigFile) {
|
if (!activeConfigFile) {
|
||||||
console.log('[initRclone] no active config file, setting default')
|
console.log('[initRclone] no active config file, setting default')
|
||||||
@@ -157,7 +168,7 @@ export async function initRclone(args: string[]) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
console.log('[initRclone] set active config file to:', activeConfigFile.id)
|
console.log('[initRclone] set active config file to:', activeConfigFile.id)
|
||||||
usePersistedStore.setState({ activeConfigFile })
|
useHostStore.setState({ activeConfigFile })
|
||||||
}
|
}
|
||||||
|
|
||||||
if (internal && activeConfigFile.id === 'default') {
|
if (internal && activeConfigFile.id === 'default') {
|
||||||
@@ -196,12 +207,12 @@ export async function initRclone(args: string[]) {
|
|||||||
await getConfigPath({ id: 'default', validate: true })
|
await getConfigPath({ id: 'default', validate: true })
|
||||||
)
|
)
|
||||||
console.log('[initRclone] switched to default config')
|
console.log('[initRclone] switched to default config')
|
||||||
usePersistedStore.setState({ activeConfigFile: configFiles[0] })
|
useHostStore.setState({ activeConfigFile: configFiles[0] })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let password: string | null = activeConfigFile.pass || activeConfigFile.passCommand || null
|
const passwordConfigured = activeConfigFile.pass || activeConfigFile.passCommand || null
|
||||||
console.log('[initRclone] password configured:', !!password)
|
console.log('[initRclone] password configured:', !!passwordConfigured)
|
||||||
try {
|
try {
|
||||||
console.log('[initRclone] reading config file', configPath)
|
console.log('[initRclone] reading config file', configPath)
|
||||||
const configContent = await readTextFile(configPath)
|
const configContent = await readTextFile(configPath)
|
||||||
@@ -211,39 +222,26 @@ export async function initRclone(args: string[]) {
|
|||||||
|
|
||||||
if (isEncrypted) {
|
if (isEncrypted) {
|
||||||
console.log('[initRclone] config file is encrypted')
|
console.log('[initRclone] config file is encrypted')
|
||||||
if (password) {
|
if (passwordConfigured) {
|
||||||
console.log('[initRclone] using existing password')
|
console.log('[initRclone] using existing password configuration')
|
||||||
} else {
|
} else {
|
||||||
console.log('[initRclone] prompting user for password')
|
console.log('[initRclone] no stored password configured')
|
||||||
password = await promptForConfigPassword(activeConfigFile.label)
|
}
|
||||||
console.log('[initRclone] password received:', !!password)
|
|
||||||
|
|
||||||
if (!password) {
|
if (!activeConfigFile.isEncrypted) {
|
||||||
console.error('[initRclone] no password provided, exiting')
|
console.log('[initRclone] updating config file encryption flag')
|
||||||
await message('Password is required for encrypted configurations.', {
|
const updatedConfigFile = { ...activeConfigFile, isEncrypted: true }
|
||||||
title: 'Password Required',
|
const updatedConfigFiles = configFiles.map((config) =>
|
||||||
kind: 'error',
|
config.id === activeConfigFile!.id ? updatedConfigFile : config
|
||||||
okLabel: 'OK',
|
)
|
||||||
})
|
useHostStore.setState({
|
||||||
await exit(0)
|
configFiles: updatedConfigFiles,
|
||||||
return
|
activeConfigFile: updatedConfigFile,
|
||||||
}
|
})
|
||||||
|
console.log('[initRclone] saved updated encryption flag')
|
||||||
|
|
||||||
if (!activeConfigFile.isEncrypted) {
|
// Update activeConfigFile reference for the rest of the function
|
||||||
console.log('[initRclone] updating config file encryption flag')
|
activeConfigFile = updatedConfigFile
|
||||||
const updatedConfigFile = { ...activeConfigFile, isEncrypted: true }
|
|
||||||
const updatedConfigFiles = configFiles.map((config) =>
|
|
||||||
config.id === activeConfigFile!.id ? updatedConfigFile : config
|
|
||||||
)
|
|
||||||
usePersistedStore.setState({
|
|
||||||
configFiles: updatedConfigFiles,
|
|
||||||
activeConfigFile: updatedConfigFile,
|
|
||||||
})
|
|
||||||
console.log('[initRclone] saved updated encryption flag')
|
|
||||||
|
|
||||||
// Update activeConfigFile reference for the rest of the function
|
|
||||||
activeConfigFile = updatedConfigFile
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} else if (activeConfigFile.isEncrypted) {
|
} else if (activeConfigFile.isEncrypted) {
|
||||||
console.log('[initRclone] config file is not encrypted, clearing encryption flag')
|
console.log('[initRclone] config file is not encrypted, clearing encryption flag')
|
||||||
@@ -251,7 +249,7 @@ export async function initRclone(args: string[]) {
|
|||||||
const updatedConfigFiles = configFiles.map((config) =>
|
const updatedConfigFiles = configFiles.map((config) =>
|
||||||
config.id === activeConfigFile!.id ? updatedConfigFile : config
|
config.id === activeConfigFile!.id ? updatedConfigFile : config
|
||||||
)
|
)
|
||||||
usePersistedStore.setState({
|
useHostStore.setState({
|
||||||
configFiles: updatedConfigFiles,
|
configFiles: updatedConfigFiles,
|
||||||
activeConfigFile: updatedConfigFile,
|
activeConfigFile: updatedConfigFile,
|
||||||
})
|
})
|
||||||
@@ -279,11 +277,11 @@ export async function initRclone(args: string[]) {
|
|||||||
env: {},
|
env: {},
|
||||||
}
|
}
|
||||||
|
|
||||||
if (persistedState.proxy) {
|
if (hostState.proxy) {
|
||||||
console.log('[initRclone] proxy configured:', persistedState.proxy.url)
|
console.log('[initRclone] proxy configured:', hostState.proxy.url)
|
||||||
try {
|
try {
|
||||||
console.log('[initRclone] testing proxy connection')
|
console.log('[initRclone] testing proxy connection')
|
||||||
await invoke<string>('test_proxy_connection', { proxy_url: persistedState.proxy.url })
|
await invoke<string>('test_proxy_connection', { proxy_url: hostState.proxy.url })
|
||||||
console.log('[initRclone] proxy connection successful')
|
console.log('[initRclone] proxy connection successful')
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[initRclone] proxy connection failed:', error)
|
console.error('[initRclone] proxy connection failed:', error)
|
||||||
@@ -305,36 +303,62 @@ export async function initRclone(args: string[]) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
console.log('[initRclone] setting proxy environment variables')
|
console.log('[initRclone] setting proxy environment variables')
|
||||||
extraParams.env.http_proxy = persistedState.proxy.url
|
extraParams.env.http_proxy = hostState.proxy.url
|
||||||
extraParams.env.https_proxy = persistedState.proxy.url
|
extraParams.env.https_proxy = hostState.proxy.url
|
||||||
extraParams.env.HTTP_PROXY = persistedState.proxy.url
|
extraParams.env.HTTP_PROXY = hostState.proxy.url
|
||||||
extraParams.env.HTTPS_PROXY = persistedState.proxy.url
|
extraParams.env.HTTPS_PROXY = hostState.proxy.url
|
||||||
extraParams.env.no_proxy = persistedState.proxy.ignoredHosts.join(',')
|
extraParams.env.no_proxy = hostState.proxy.ignoredHosts.join(',')
|
||||||
extraParams.env.NO_PROXY = persistedState.proxy.ignoredHosts.join(',')
|
extraParams.env.NO_PROXY = hostState.proxy.ignoredHosts.join(',')
|
||||||
console.log(
|
console.log(
|
||||||
'[initRclone] proxy env vars set, ignored hosts:',
|
'[initRclone] proxy env vars set, ignored hosts:',
|
||||||
persistedState.proxy.ignoredHosts.length
|
hostState.proxy.ignoredHosts.length
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (activeConfigFile.isEncrypted) {
|
|
||||||
console.log('[initRclone] setting encryption environment variables')
|
|
||||||
extraParams.env.RCLONE_ASK_PASSWORD = 'false'
|
|
||||||
if (activeConfigFile.passCommand) {
|
|
||||||
console.log('[initRclone] using passCommand for decryption')
|
|
||||||
extraParams.env.RCLONE_CONFIG_PASS_COMMAND = activeConfigFile.passCommand
|
|
||||||
} else {
|
|
||||||
console.log('[initRclone] using password for decryption')
|
|
||||||
extraParams.env.RCLONE_CONFIG_PASS = activeConfigFile.pass || password!
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (internal || activeConfigFile.id !== 'default') {
|
if (internal || activeConfigFile.id !== 'default') {
|
||||||
console.log('[initRclone] setting custom config path:', configFolderPath)
|
console.log('[initRclone] setting custom config path:', configFolderPath)
|
||||||
extraParams.env.RCLONE_CONFIG_DIR = configFolderPath
|
extraParams.env.RCLONE_CONFIG_DIR = configFolderPath
|
||||||
extraParams.env.RCLONE_CONFIG = `${configFolderPath}${sep()}rclone.conf`
|
extraParams.env.RCLONE_CONFIG = `${configFolderPath}${sep()}rclone.conf`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const commandName = system ? 'rclone-system' : internal ? 'rclone-internal' : null
|
||||||
|
|
||||||
|
if (activeConfigFile.isEncrypted && commandName) {
|
||||||
|
console.log('[initRclone] ensuring encrypted configuration access')
|
||||||
|
try {
|
||||||
|
await ensureEncryptedConfigEnv(
|
||||||
|
activeConfigFile,
|
||||||
|
extraParams.env,
|
||||||
|
true,
|
||||||
|
commandName,
|
||||||
|
`Please enter the current password for "${activeConfigFile.label}"`
|
||||||
|
)
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof Error && error.message === 'Password prompt cancelled by user.') {
|
||||||
|
console.error('[initRclone] password prompt cancelled by user')
|
||||||
|
const response = await message(
|
||||||
|
'Password is required for encrypted configurations.',
|
||||||
|
{
|
||||||
|
title: 'Password Required',
|
||||||
|
kind: 'error',
|
||||||
|
buttons: {
|
||||||
|
cancel: 'Close',
|
||||||
|
ok: 'Try Again',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
console.log('[initRclone] message response:', response)
|
||||||
|
if (response === 'Try Again') {
|
||||||
|
await relaunch()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
await exit(0)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
console.log('[initRclone] extraParams', extraParams)
|
console.log('[initRclone] extraParams', extraParams)
|
||||||
|
|
||||||
if (system) {
|
if (system) {
|
||||||
@@ -576,30 +600,3 @@ export async function provisionRclone() {
|
|||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Prompts the user for a password for an encrypted configuration
|
|
||||||
* @param configLabel - The label of the configuration file
|
|
||||||
* @returns Promise<string | null> - The password entered by the user, or null if cancelled
|
|
||||||
*/
|
|
||||||
async function promptForConfigPassword(configLabel: string): Promise<string | null> {
|
|
||||||
console.log('[promptForConfigPassword] prompting for config:', configLabel)
|
|
||||||
try {
|
|
||||||
const result = await invoke<string | null>('prompt_password', {
|
|
||||||
title: 'Rclone UI',
|
|
||||||
message: `Please enter the password for the encrypted configuration "${configLabel}".`,
|
|
||||||
})
|
|
||||||
console.log('[promptForConfigPassword] received result type:', typeof result)
|
|
||||||
|
|
||||||
if (typeof result === 'string') {
|
|
||||||
console.log('[promptForConfigPassword] password received, length:', result.length)
|
|
||||||
return result.trim()
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('[promptForConfigPassword] no password received (cancelled or empty)')
|
|
||||||
return null
|
|
||||||
} catch (error) {
|
|
||||||
console.error('[promptForConfigPassword] Error prompting for password:', error)
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import { exists, writeFile } from '@tauri-apps/plugin-fs'
|
|||||||
import { fetch } from '@tauri-apps/plugin-http'
|
import { fetch } from '@tauri-apps/plugin-http'
|
||||||
import { revealItemInDir } from '@tauri-apps/plugin-opener'
|
import { revealItemInDir } from '@tauri-apps/plugin-opener'
|
||||||
import { platform } from '@tauri-apps/plugin-os'
|
import { platform } from '@tauri-apps/plugin-os'
|
||||||
import { Command } from '@tauri-apps/plugin-shell'
|
|
||||||
|
|
||||||
export async function needsMountPlugin() {
|
export async function needsMountPlugin() {
|
||||||
console.log('[needsMountPlugin]')
|
console.log('[needsMountPlugin]')
|
||||||
@@ -74,36 +73,3 @@ export async function dialogGetMountPlugin() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function unmount(mountPoint: string, force = false) {
|
|
||||||
console.log('[unmount]', mountPoint, force)
|
|
||||||
|
|
||||||
const command = Command.create('umount', [force ? '-f' : '', mountPoint])
|
|
||||||
|
|
||||||
const output = await command.execute()
|
|
||||||
|
|
||||||
if (output.code !== 0) {
|
|
||||||
if (output.stderr.toLowerCase().includes('busy')) {
|
|
||||||
const answer = await ask('This resource is busy, do you wish to force unmount?', {
|
|
||||||
title: 'Could not unmount',
|
|
||||||
kind: 'warning',
|
|
||||||
okLabel: 'Force Unmount',
|
|
||||||
cancelLabel: 'Cancel',
|
|
||||||
})
|
|
||||||
if (answer) {
|
|
||||||
return await unmount(mountPoint, true)
|
|
||||||
}
|
|
||||||
throw new Error(output.stderr)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (output.stderr.toLowerCase().includes('not currently mounted')) {
|
|
||||||
console.error('[unmount] not currently mounted')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
console.error('[unmount] failed to unmount', output.stderr)
|
|
||||||
throw new Error(output.stderr)
|
|
||||||
}
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -0,0 +1,59 @@
|
|||||||
|
export const OVERRIDES = {
|
||||||
|
alias: {
|
||||||
|
Description: 'ALIAS',
|
||||||
|
},
|
||||||
|
crypt: {
|
||||||
|
Description: 'CRYPT',
|
||||||
|
},
|
||||||
|
union: {
|
||||||
|
Description: 'UNION',
|
||||||
|
},
|
||||||
|
memory: {
|
||||||
|
Description: 'MEMORY',
|
||||||
|
},
|
||||||
|
archive: {
|
||||||
|
Description: 'ARCHIVE',
|
||||||
|
},
|
||||||
|
compress: {
|
||||||
|
Description: 'COMPRESS',
|
||||||
|
},
|
||||||
|
hasher: {
|
||||||
|
Description: 'HASHER',
|
||||||
|
},
|
||||||
|
chunker: {
|
||||||
|
Description: 'CHUNKER',
|
||||||
|
},
|
||||||
|
cache: {
|
||||||
|
Description: 'CACHE',
|
||||||
|
},
|
||||||
|
combine: {
|
||||||
|
Description: 'COMBINE',
|
||||||
|
},
|
||||||
|
'google cloud storage': {
|
||||||
|
Description: 'Google Cloud Storage',
|
||||||
|
},
|
||||||
|
'hdfs': {
|
||||||
|
Description: 'Hadoop Distributed File System',
|
||||||
|
},
|
||||||
|
'pixeldrain': {
|
||||||
|
Description: 'Pixeldrain',
|
||||||
|
},
|
||||||
|
'swift': {
|
||||||
|
Description: 'Openstack Swift',
|
||||||
|
},
|
||||||
|
'azureblob': {
|
||||||
|
Description: 'Azure Blob Storage',
|
||||||
|
},
|
||||||
|
'azurefiles': {
|
||||||
|
Description: 'Azure Files',
|
||||||
|
},
|
||||||
|
'filelu': {
|
||||||
|
Description: 'FileLu',
|
||||||
|
},
|
||||||
|
's3': {
|
||||||
|
Description: 'Amazon S3 & Compatible Storage',
|
||||||
|
},
|
||||||
|
'seafile': {
|
||||||
|
Description: 'Seafile',
|
||||||
|
},
|
||||||
|
}
|
||||||
@@ -1,283 +0,0 @@
|
|||||||
import { LazyStore } from '@tauri-apps/plugin-store'
|
|
||||||
import { shared } from 'use-broadcast-ts'
|
|
||||||
import { create } from 'zustand'
|
|
||||||
import { type StateStorage, createJSONStorage, persist } from 'zustand/middleware'
|
|
||||||
import type { ConfigFile } from '../types/config'
|
|
||||||
import type { ScheduledTask } from '../types/task'
|
|
||||||
import type { Template } from '../types/template'
|
|
||||||
import type { SERVE_TYPES } from './rclone/constants'
|
|
||||||
|
|
||||||
// const { LazyStore } = window.__TAURI__.store
|
|
||||||
const store = new LazyStore('store.json')
|
|
||||||
|
|
||||||
export interface RemoteConfig {
|
|
||||||
disabledActions?: ('tray' | 'tray-mount' | 'tray-browse' | 'tray-remove' | 'tray-cleanup')[]
|
|
||||||
|
|
||||||
defaultRemotePath?: string
|
|
||||||
defaultMountPoint?: string
|
|
||||||
mountOnStart?: boolean
|
|
||||||
|
|
||||||
mountDefaults?: Record<string, any>
|
|
||||||
vfsDefaults?: Record<string, any>
|
|
||||||
filterDefaults?: Record<string, any>
|
|
||||||
copyDefaults?: Record<string, any>
|
|
||||||
moveDefaults?: Record<string, any>
|
|
||||||
syncDefaults?: Record<string, any>
|
|
||||||
configDefaults?: Record<string, any>
|
|
||||||
serveDefaults?: Record<(typeof SERVE_TYPES)[number], Record<string, any>>
|
|
||||||
bisyncDefaults?: Record<string, any>
|
|
||||||
remoteDefaults?: Record<string, any>
|
|
||||||
}
|
|
||||||
|
|
||||||
interface State {
|
|
||||||
firstWindow: boolean
|
|
||||||
|
|
||||||
rcloneLoaded: boolean
|
|
||||||
rcloneAuth: string
|
|
||||||
rcloneAuthHeader: string
|
|
||||||
|
|
||||||
remotes: string[]
|
|
||||||
setRemotes: (remotes: string[]) => void
|
|
||||||
addRemote: (remote: string) => void
|
|
||||||
removeRemote: (remote: string) => void
|
|
||||||
|
|
||||||
startupStatus:
|
|
||||||
| null
|
|
||||||
| 'initializing'
|
|
||||||
| 'initialized'
|
|
||||||
| 'updating'
|
|
||||||
| 'updated'
|
|
||||||
| 'error'
|
|
||||||
| 'fatal'
|
|
||||||
|
|
||||||
startupDisplayed: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
type SupportedAction =
|
|
||||||
| 'tray-mount'
|
|
||||||
| 'tray-sync'
|
|
||||||
| 'tray-copy'
|
|
||||||
| 'tray-serve'
|
|
||||||
| 'tray-move'
|
|
||||||
| 'tray-bisync'
|
|
||||||
| 'tray-delete'
|
|
||||||
| 'tray-purge'
|
|
||||||
| 'tray-download'
|
|
||||||
|
|
||||||
interface PersistedState {
|
|
||||||
remoteConfigList: Record<string, RemoteConfig>
|
|
||||||
setRemoteConfig: (remote: string, config: RemoteConfig) => void
|
|
||||||
mergeRemoteConfig: (remote: string, config: RemoteConfig) => void
|
|
||||||
|
|
||||||
disabledActions: SupportedAction[]
|
|
||||||
|
|
||||||
setDisabledActions: (actions: SupportedAction[]) => void
|
|
||||||
|
|
||||||
proxy:
|
|
||||||
| {
|
|
||||||
url: string
|
|
||||||
ignoredHosts: string[]
|
|
||||||
}
|
|
||||||
| undefined
|
|
||||||
|
|
||||||
favoritePaths: { remote: string; path: string; added: number }[]
|
|
||||||
|
|
||||||
settingsPass: string | undefined
|
|
||||||
setSettingsPass: (pass: string | undefined) => void
|
|
||||||
|
|
||||||
licenseKey: string | undefined
|
|
||||||
setLicenseKey: (key: string | undefined) => void
|
|
||||||
licenseValid: boolean
|
|
||||||
setLicenseValid: (valid: boolean) => void
|
|
||||||
|
|
||||||
startOnBoot: boolean
|
|
||||||
setStartOnBoot: (startOnBoot: boolean) => void
|
|
||||||
|
|
||||||
scheduledTasks: ScheduledTask[]
|
|
||||||
addScheduledTask: (
|
|
||||||
task: Omit<
|
|
||||||
ScheduledTask,
|
|
||||||
'id' | 'isRunning' | 'currentRunId' | 'lastRun' | 'configId' | 'isEnabled'
|
|
||||||
>
|
|
||||||
) => void
|
|
||||||
removeScheduledTask: (id: string) => void
|
|
||||||
updateScheduledTask: (id: string, task: Partial<ScheduledTask>) => void
|
|
||||||
|
|
||||||
templates: Template[]
|
|
||||||
|
|
||||||
configFiles: ConfigFile[]
|
|
||||||
addConfigFile: (configFile: ConfigFile) => void
|
|
||||||
removeConfigFile: (id: string) => void
|
|
||||||
activeConfigFile: ConfigFile | null
|
|
||||||
setActiveConfigFile: (configFile: string) => void
|
|
||||||
updateConfigFile: (id: string, configFile: Partial<ConfigFile>) => void
|
|
||||||
|
|
||||||
lastSkippedVersion: string | undefined
|
|
||||||
|
|
||||||
hideStartup: boolean
|
|
||||||
|
|
||||||
themeV2: {
|
|
||||||
tray: 'light' | 'dark' | undefined
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const getStorage = (store: LazyStore): StateStorage => ({
|
|
||||||
getItem: async (name: string): Promise<string | null> => {
|
|
||||||
console.log('getItem', { name })
|
|
||||||
return (await store.get(name)) || null
|
|
||||||
},
|
|
||||||
setItem: async (name: string, value: string): Promise<void> => {
|
|
||||||
console.log('setItem', { name, value })
|
|
||||||
await store.set(name, value)
|
|
||||||
await store.save()
|
|
||||||
},
|
|
||||||
removeItem: async (name: string): Promise<void> => {
|
|
||||||
console.log('removeItem', { name })
|
|
||||||
await store.delete(name)
|
|
||||||
await store.save()
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
export const useStore = create<State>()(
|
|
||||||
shared(
|
|
||||||
(set) => ({
|
|
||||||
firstWindow: true,
|
|
||||||
|
|
||||||
rcloneLoaded: false,
|
|
||||||
rcloneAuth: '',
|
|
||||||
rcloneAuthHeader: '',
|
|
||||||
|
|
||||||
remotes: [],
|
|
||||||
setRemotes: (remotes: string[]) => set((_) => ({ remotes })),
|
|
||||||
addRemote: (remote: string) =>
|
|
||||||
set((state) => ({ remotes: [...state.remotes, remote] })),
|
|
||||||
removeRemote: (remote: string) =>
|
|
||||||
set((state) => ({ remotes: state.remotes.filter((r) => r !== remote) })),
|
|
||||||
|
|
||||||
startupStatus: null,
|
|
||||||
startupDisplayed: false,
|
|
||||||
}),
|
|
||||||
{ name: 'shared-store' }
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
export const usePersistedStore = create<PersistedState>()(
|
|
||||||
persist(
|
|
||||||
(set) => ({
|
|
||||||
remoteConfigList: {},
|
|
||||||
setRemoteConfig: (remote: string, config: Record<string, any>) =>
|
|
||||||
set((state) => ({
|
|
||||||
remoteConfigList: { ...state.remoteConfigList, [remote]: config },
|
|
||||||
})),
|
|
||||||
mergeRemoteConfig: (remote: string, config: Record<string, any>) =>
|
|
||||||
set((state) => ({
|
|
||||||
remoteConfigList: {
|
|
||||||
...state.remoteConfigList,
|
|
||||||
[remote]: { ...state.remoteConfigList[remote], ...config },
|
|
||||||
},
|
|
||||||
})),
|
|
||||||
|
|
||||||
disabledActions: [],
|
|
||||||
setDisabledActions: (actions: SupportedAction[]) =>
|
|
||||||
set((_) => ({ disabledActions: actions })),
|
|
||||||
|
|
||||||
favoritePaths: [],
|
|
||||||
|
|
||||||
proxy: undefined,
|
|
||||||
|
|
||||||
settingsPass: undefined,
|
|
||||||
setSettingsPass: (pass: string | undefined) => set((_) => ({ settingsPass: pass })),
|
|
||||||
|
|
||||||
licenseKey: undefined,
|
|
||||||
setLicenseKey: (key: string | undefined) => set((_) => ({ licenseKey: key })),
|
|
||||||
licenseValid: false,
|
|
||||||
setLicenseValid: (valid: boolean) => set((_) => ({ licenseValid: valid })),
|
|
||||||
|
|
||||||
startOnBoot: false,
|
|
||||||
setStartOnBoot: (startOnBoot: boolean) => set((_) => ({ startOnBoot })),
|
|
||||||
|
|
||||||
scheduledTasks: [],
|
|
||||||
addScheduledTask: (
|
|
||||||
task: Omit<
|
|
||||||
ScheduledTask,
|
|
||||||
'id' | 'isRunning' | 'currentRunId' | 'lastRun' | 'configId' | 'isEnabled'
|
|
||||||
>
|
|
||||||
) => {
|
|
||||||
const state = usePersistedStore.getState()
|
|
||||||
const configId = state.activeConfigFile?.id
|
|
||||||
|
|
||||||
if (!configId) {
|
|
||||||
console.error('No active config file for scheduled task')
|
|
||||||
throw new Error('No active config file')
|
|
||||||
}
|
|
||||||
|
|
||||||
set((state) => ({
|
|
||||||
scheduledTasks: [
|
|
||||||
...state.scheduledTasks,
|
|
||||||
{
|
|
||||||
...task,
|
|
||||||
id: crypto.randomUUID(),
|
|
||||||
isRunning: false,
|
|
||||||
isEnabled: true,
|
|
||||||
configId,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}))
|
|
||||||
},
|
|
||||||
removeScheduledTask: (id: string) =>
|
|
||||||
set((state) => ({
|
|
||||||
scheduledTasks: state.scheduledTasks.filter((t) => t.id !== id),
|
|
||||||
})),
|
|
||||||
updateScheduledTask: (id: string, task: Partial<ScheduledTask>) =>
|
|
||||||
set((state) => ({
|
|
||||||
scheduledTasks: state.scheduledTasks.map((t) =>
|
|
||||||
t.id === id ? { ...t, ...task } : t
|
|
||||||
),
|
|
||||||
})),
|
|
||||||
|
|
||||||
templates: [],
|
|
||||||
|
|
||||||
configFiles: [],
|
|
||||||
addConfigFile: (configFile: ConfigFile) =>
|
|
||||||
set((state) => ({
|
|
||||||
configFiles: [...state.configFiles, configFile],
|
|
||||||
})),
|
|
||||||
removeConfigFile: (id: string) =>
|
|
||||||
set((state) => ({
|
|
||||||
configFiles: state.configFiles.filter((f) => f.id !== id),
|
|
||||||
})),
|
|
||||||
activeConfigFile: null,
|
|
||||||
setActiveConfigFile: (id: string) =>
|
|
||||||
set((state) => ({
|
|
||||||
activeConfigFile: state.configFiles.find((f) => f.id === id) || null,
|
|
||||||
})),
|
|
||||||
updateConfigFile: (id: string, configFile: Partial<ConfigFile>) =>
|
|
||||||
set((state) => ({
|
|
||||||
configFiles: state.configFiles.map((f) =>
|
|
||||||
f.id === id ? { ...f, ...configFile } : f
|
|
||||||
),
|
|
||||||
})),
|
|
||||||
|
|
||||||
lastSkippedVersion: undefined,
|
|
||||||
|
|
||||||
hideStartup: false,
|
|
||||||
|
|
||||||
themeV2: {
|
|
||||||
tray: undefined,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
{
|
|
||||||
name: 'store',
|
|
||||||
storage: createJSONStorage(() => getStorage(store)),
|
|
||||||
version: 1,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
// useStore.persist.onFinishHydration(() => {
|
|
||||||
// console.log('onFinishHydration')
|
|
||||||
// })
|
|
||||||
|
|
||||||
store.onKeyChange('store', async (_) => {
|
|
||||||
await usePersistedStore.persist.rehydrate()
|
|
||||||
})
|
|
||||||
@@ -1,171 +1,189 @@
|
|||||||
import * as Sentry from '@sentry/browser'
|
import * as Sentry from '@sentry/browser'
|
||||||
import { Menu } from '@tauri-apps/api/menu'
|
import { invoke } from '@tauri-apps/api/core'
|
||||||
import { MenuItem } from '@tauri-apps/api/menu'
|
import { Menu, MenuItem, PredefinedMenuItem } from '@tauri-apps/api/menu'
|
||||||
import { resolveResource } from '@tauri-apps/api/path'
|
import { resolveResource } from '@tauri-apps/api/path'
|
||||||
import type { TrayIconEvent } from '@tauri-apps/api/tray'
|
import { TrayIcon, type TrayIconEvent } from '@tauri-apps/api/tray'
|
||||||
import { TrayIcon } from '@tauri-apps/api/tray'
|
import { getCurrentWindow } from '@tauri-apps/api/window'
|
||||||
import { getAllWindows, getCurrentWindow } from '@tauri-apps/api/window'
|
import { ask, message } from '@tauri-apps/plugin-dialog'
|
||||||
import { ask } from '@tauri-apps/plugin-dialog'
|
import { openUrl } from '@tauri-apps/plugin-opener'
|
||||||
import { platform } from '@tauri-apps/plugin-os'
|
import { platform } from '@tauri-apps/plugin-os'
|
||||||
import { buildMenu } from './menu'
|
import { exit } from '@tauri-apps/plugin-process'
|
||||||
import { usePersistedStore } from './store'
|
import { usePersistedStore } from '../store/persisted'
|
||||||
import { resetMainWindow } from './window'
|
import { openSmallWindow, openWindow } from './window'
|
||||||
|
|
||||||
export async function triggerTrayRebuild() {
|
async function buildMenu() {
|
||||||
return getAllWindows().then((windows) => {
|
console.log('[buildMenu]')
|
||||||
windows.find((w) => w.label === 'main')?.emit('rebuild-tray')
|
|
||||||
|
const menuItems: (MenuItem | PredefinedMenuItem)[] = []
|
||||||
|
|
||||||
|
const onboardingMenuItem = await MenuItem.new({
|
||||||
|
id: 'onboarding',
|
||||||
|
text: 'Onboarding',
|
||||||
|
action: async () => {
|
||||||
|
await openSmallWindow({
|
||||||
|
name: 'Onboarding',
|
||||||
|
url: '/onboarding',
|
||||||
|
})
|
||||||
|
},
|
||||||
})
|
})
|
||||||
}
|
menuItems.push(onboardingMenuItem)
|
||||||
|
|
||||||
let interval: NodeJS.Timeout | null = null
|
const openMenuItem = await MenuItem.new({
|
||||||
|
id: 'open',
|
||||||
|
text: 'Open',
|
||||||
|
action: async () => {
|
||||||
|
await invoke('show_toolbar')
|
||||||
|
},
|
||||||
|
})
|
||||||
|
menuItems.push(openMenuItem)
|
||||||
|
|
||||||
async function getTray() {
|
await PredefinedMenuItem.new({
|
||||||
return await TrayIcon.getById('main-tray')
|
item: 'Separator',
|
||||||
|
}).then((item) => {
|
||||||
|
menuItems.push(item)
|
||||||
|
})
|
||||||
|
|
||||||
|
const transfersMenuItem = await MenuItem.new({
|
||||||
|
id: 'transfers',
|
||||||
|
text: 'Transfers',
|
||||||
|
action: async () => {
|
||||||
|
await openWindow({
|
||||||
|
name: 'Transfers',
|
||||||
|
url: '/transfers',
|
||||||
|
})
|
||||||
|
},
|
||||||
|
})
|
||||||
|
menuItems.push(transfersMenuItem)
|
||||||
|
|
||||||
|
const schedulesMenuItem = await MenuItem.new({
|
||||||
|
id: 'schedules',
|
||||||
|
text: 'Schedules',
|
||||||
|
action: async () => {
|
||||||
|
await openWindow({
|
||||||
|
name: 'Schedules',
|
||||||
|
url: '/schedules',
|
||||||
|
})
|
||||||
|
},
|
||||||
|
})
|
||||||
|
menuItems.push(schedulesMenuItem)
|
||||||
|
|
||||||
|
const templatesMenuItem = await MenuItem.new({
|
||||||
|
id: 'templates',
|
||||||
|
text: 'Templates',
|
||||||
|
action: async () => {
|
||||||
|
await openWindow({
|
||||||
|
name: 'Templates',
|
||||||
|
url: '/templates',
|
||||||
|
})
|
||||||
|
},
|
||||||
|
})
|
||||||
|
menuItems.push(templatesMenuItem)
|
||||||
|
|
||||||
|
await PredefinedMenuItem.new({
|
||||||
|
item: 'Separator',
|
||||||
|
}).then((item) => {
|
||||||
|
menuItems.push(item)
|
||||||
|
})
|
||||||
|
|
||||||
|
const settingsItem = await MenuItem.new({
|
||||||
|
id: 'settings',
|
||||||
|
text: 'Settings',
|
||||||
|
action: async () => {
|
||||||
|
await openWindow({
|
||||||
|
name: 'Settings',
|
||||||
|
url: '/settings',
|
||||||
|
})
|
||||||
|
},
|
||||||
|
})
|
||||||
|
menuItems.push(settingsItem)
|
||||||
|
|
||||||
|
const issuesItem = await MenuItem.new({
|
||||||
|
id: 'issues',
|
||||||
|
text: 'Issues?',
|
||||||
|
action: async () => {
|
||||||
|
const confirmed = await ask(
|
||||||
|
'Please open an issue on Github and we will get it sorted in less than 48 hours, no matter if you have a license or not.',
|
||||||
|
{
|
||||||
|
title: 'Sorry ):',
|
||||||
|
kind: 'info',
|
||||||
|
okLabel: 'Open Github',
|
||||||
|
cancelLabel: 'Cancel',
|
||||||
|
}
|
||||||
|
)
|
||||||
|
if (confirmed) {
|
||||||
|
await openUrl('https://github.com/rclone-ui/rclone-ui/issues')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
menuItems.push(issuesItem)
|
||||||
|
|
||||||
|
const quitItem = await MenuItem.new({
|
||||||
|
id: 'quit',
|
||||||
|
text: 'Quit',
|
||||||
|
action: async () => {
|
||||||
|
await getCurrentWindow().emit('close-app')
|
||||||
|
},
|
||||||
|
})
|
||||||
|
menuItems.push(quitItem)
|
||||||
|
|
||||||
|
return await Menu.new({
|
||||||
|
id: 'main-menu',
|
||||||
|
items: menuItems,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async function resolveTrayIconForTheme() {
|
async function resolveTrayIconForTheme() {
|
||||||
console.log('[resolveTrayIconForTheme]')
|
console.log('[resolveTrayIconForTheme]')
|
||||||
|
|
||||||
const existingTheme = usePersistedStore.getState().themeV2
|
const existingTheme = usePersistedStore.getState().appearance
|
||||||
console.log('[resolveTrayIconForTheme] existingTheme', existingTheme)
|
console.log('[resolveTrayIconForTheme] existingTheme', existingTheme)
|
||||||
|
|
||||||
if (platform() !== 'macos' && 'tray' in existingTheme && existingTheme.tray) {
|
if (existingTheme.tray === 'system') {
|
||||||
const pickedPath =
|
if (platform() === 'macos') {
|
||||||
existingTheme.tray === 'dark'
|
return await resolveResource('icons/favicon/icon.png')
|
||||||
? 'icons/favicon/icon.png'
|
}
|
||||||
: 'icons/favicon/icon-light.png'
|
|
||||||
|
|
||||||
return await resolveResource(pickedPath)
|
|
||||||
}
|
|
||||||
|
|
||||||
let theme: 'light' | 'dark' = 'dark'
|
|
||||||
|
|
||||||
if (['windows', 'macos'].includes(platform())) {
|
|
||||||
try {
|
try {
|
||||||
const currentWindow = getCurrentWindow()
|
const currentWindow = getCurrentWindow()
|
||||||
const windowTheme = await currentWindow.theme()
|
const windowTheme = await currentWindow.theme()
|
||||||
console.log('[resolveTrayIconForTheme] windowTheme', windowTheme)
|
console.log('[resolveTrayIconForTheme] windowTheme', windowTheme)
|
||||||
|
|
||||||
theme = windowTheme === 'dark' ? 'dark' : 'light'
|
const pickedPath =
|
||||||
} catch {}
|
windowTheme === 'dark' ? 'icons/favicon/icon.png' : 'icons/favicon/icon-light.png'
|
||||||
} else {
|
|
||||||
const answer = await ask(
|
|
||||||
'Based on your tray/menubar, how do you want the icon to look?\n\nIf your menubar has a dark background select light, otherwise select dark.',
|
|
||||||
{
|
|
||||||
title: 'Greetings, Linux User',
|
|
||||||
okLabel: 'Dark',
|
|
||||||
cancelLabel: 'Light',
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
if (answer) {
|
return await resolveResource(pickedPath)
|
||||||
theme = 'dark'
|
} catch {
|
||||||
} else {
|
await message('Failed to get window theme', {
|
||||||
theme = 'light'
|
title: 'Error',
|
||||||
|
kind: 'error',
|
||||||
|
})
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 5000))
|
||||||
|
await exit()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('[resolveTrayIconForTheme] theme', theme)
|
const pickedPath =
|
||||||
|
existingTheme.tray === 'dark' ? 'icons/favicon/icon-light.png' : 'icons/favicon/icon.png'
|
||||||
usePersistedStore.setState({ themeV2: { tray: theme } })
|
|
||||||
|
|
||||||
const pickedPath = theme === 'dark' ? 'icons/favicon/icon.png' : 'icons/favicon/icon-light.png'
|
|
||||||
console.log('[resolveTrayIconForTheme] pickedPath', pickedPath)
|
|
||||||
|
|
||||||
return await resolveResource(pickedPath)
|
return await resolveResource(pickedPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function showLoadingTray() {
|
|
||||||
console.log('[showLoadingTray]')
|
|
||||||
|
|
||||||
if (platform() === 'linux') {
|
|
||||||
console.log('[showLoadingTray] platform is linux, skipping')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const tray = await getTray()
|
|
||||||
if (!tray) {
|
|
||||||
console.error('[showLoadingTray] tray not found')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const globeIconPath = await resolveResource('icons/favicon/frame_00_delay-0.1s.png')
|
|
||||||
|
|
||||||
const quitItem = await MenuItem.new({
|
|
||||||
id: 'quit-loading',
|
|
||||||
text: 'Quit',
|
|
||||||
action: async () => {
|
|
||||||
const answer = await ask('An operation is in progress, are you sure you want to exit?')
|
|
||||||
if (answer) {
|
|
||||||
await getCurrentWindow().emit('close-app')
|
|
||||||
}
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
const loadingMenu = await Menu.new({
|
|
||||||
id: 'loading-menu',
|
|
||||||
items: [quitItem],
|
|
||||||
})
|
|
||||||
|
|
||||||
await tray.setMenu(loadingMenu)
|
|
||||||
await tray.setIcon(globeIconPath)
|
|
||||||
await tray.setTooltip('Loading...')
|
|
||||||
|
|
||||||
let currentIcon = 1
|
|
||||||
|
|
||||||
interval = setInterval(async () => {
|
|
||||||
if (currentIcon > 17) {
|
|
||||||
currentIcon = 1
|
|
||||||
}
|
|
||||||
const globeIconPath = await resolveResource(
|
|
||||||
`icons/favicon/frame_${currentIcon < 10 ? '0' : ''}${currentIcon}_delay-0.1s.png`
|
|
||||||
)
|
|
||||||
await tray?.setIcon(globeIconPath)
|
|
||||||
currentIcon += 1
|
|
||||||
}, 200)
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function showDefaultTray() {
|
|
||||||
console.log('[showDefaultTray]')
|
|
||||||
|
|
||||||
const tray = await getTray()
|
|
||||||
if (!tray) {
|
|
||||||
console.error('[showDefaultTray] tray not found')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (interval) {
|
|
||||||
clearInterval(interval)
|
|
||||||
interval = null
|
|
||||||
}
|
|
||||||
|
|
||||||
const newMenu = await buildMenu()
|
|
||||||
await tray.setMenu(newMenu)
|
|
||||||
|
|
||||||
const iconPath = await resolveTrayIconForTheme()
|
|
||||||
await tray.setIcon(iconPath)
|
|
||||||
await tray.setTooltip('Rclone')
|
|
||||||
|
|
||||||
console.log('[showDefaultTray] tray menu rebuilt')
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function initTray() {
|
export async function initTray() {
|
||||||
console.log('[initTray] platform', platform())
|
console.log('[initTray] platform', platform())
|
||||||
|
|
||||||
try {
|
try {
|
||||||
console.log('[initTray]')
|
console.log('[initTray]')
|
||||||
|
|
||||||
const initialIcon = await resolveResource('icons/favicon/icon.png')
|
|
||||||
await TrayIcon.new({
|
await TrayIcon.new({
|
||||||
id: 'main-tray',
|
id: 'rclone-menu5',
|
||||||
icon: initialIcon,
|
menu: await buildMenu(),
|
||||||
|
icon: await resolveTrayIconForTheme(),
|
||||||
tooltip: 'Rclone',
|
tooltip: 'Rclone',
|
||||||
menuOnLeftClick: true,
|
menuOnLeftClick: true,
|
||||||
|
iconAsTemplate: true,
|
||||||
action: async (event: TrayIconEvent) => {
|
action: async (event: TrayIconEvent) => {
|
||||||
if (event.type === 'Click') {
|
if (event.type === 'Click') {
|
||||||
console.log('[onTrayAction] tray clicked:', event)
|
console.log('[onTrayAction] tray clicked:', event)
|
||||||
|
|
||||||
await resetMainWindow()
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,24 +1,6 @@
|
|||||||
|
import { invoke } from '@tauri-apps/api/core'
|
||||||
import { WebviewWindow } from '@tauri-apps/api/webviewWindow'
|
import { WebviewWindow } from '@tauri-apps/api/webviewWindow'
|
||||||
import {
|
|
||||||
LogicalSize,
|
|
||||||
PhysicalSize,
|
|
||||||
availableMonitors,
|
|
||||||
currentMonitor,
|
|
||||||
getAllWindows,
|
|
||||||
} from '@tauri-apps/api/window'
|
|
||||||
import { platform } from '@tauri-apps/plugin-os'
|
import { platform } from '@tauri-apps/plugin-os'
|
||||||
import { useStore } from './store'
|
|
||||||
import { showDefaultTray, showLoadingTray } from './tray'
|
|
||||||
|
|
||||||
export async function resetMainWindow() {
|
|
||||||
const window = await getAllWindows().then((w) => w.find((w) => w.label === 'main'))
|
|
||||||
if (!window) return
|
|
||||||
|
|
||||||
await window.setSize(new PhysicalSize(0, 0))
|
|
||||||
await window.center()
|
|
||||||
await window.hide()
|
|
||||||
await window.setAlwaysOnTop(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function openFullWindow({
|
export async function openFullWindow({
|
||||||
name,
|
name,
|
||||||
@@ -28,53 +10,8 @@ export async function openFullWindow({
|
|||||||
url: string
|
url: string
|
||||||
}) {
|
}) {
|
||||||
console.log('[openFullWindow] ', name, url)
|
console.log('[openFullWindow] ', name, url)
|
||||||
|
await invoke('open_full_window', { name, url })
|
||||||
const w = new WebviewWindow(name, {
|
return WebviewWindow.getByLabel(name)
|
||||||
height: 0,
|
|
||||||
width: 0,
|
|
||||||
visibleOnAllWorkspaces: false,
|
|
||||||
resizable: true,
|
|
||||||
visible: false,
|
|
||||||
focus: true,
|
|
||||||
title: name,
|
|
||||||
decorations: true,
|
|
||||||
url: url,
|
|
||||||
theme: 'dark',
|
|
||||||
// @ts-expect-error
|
|
||||||
backgroundThrottling: 'disabled',
|
|
||||||
})
|
|
||||||
|
|
||||||
let monitor = await currentMonitor()
|
|
||||||
|
|
||||||
if (!monitor) {
|
|
||||||
monitor = (await availableMonitors())[0]
|
|
||||||
}
|
|
||||||
|
|
||||||
const size = monitor?.size
|
|
||||||
|
|
||||||
if (!size) {
|
|
||||||
console.error('[openFullWindow] no monitor found')
|
|
||||||
throw new Error('No monitor found')
|
|
||||||
}
|
|
||||||
|
|
||||||
if (platform() === 'windows') {
|
|
||||||
// subtract from the height to correct for the taskbar
|
|
||||||
size.height -= 100
|
|
||||||
}
|
|
||||||
|
|
||||||
await w.hide()
|
|
||||||
await w.setSize(size)
|
|
||||||
await w.center()
|
|
||||||
await w.setZoom(1)
|
|
||||||
await w.show()
|
|
||||||
await w.setFocus()
|
|
||||||
|
|
||||||
if (platform() === 'linux') {
|
|
||||||
await w.setResizable(false)
|
|
||||||
await w.setResizable(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
return w
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function openWindow({
|
export async function openWindow({
|
||||||
@@ -89,44 +26,8 @@ export async function openWindow({
|
|||||||
height?: number
|
height?: number
|
||||||
}) {
|
}) {
|
||||||
console.log('[openWindow] ', name, url)
|
console.log('[openWindow] ', name, url)
|
||||||
|
await invoke('open_window', { name, url, width, height })
|
||||||
const isFirstWindow = useStore.getState().firstWindow
|
return WebviewWindow.getByLabel(name)
|
||||||
|
|
||||||
const w = new WebviewWindow(name, {
|
|
||||||
height: 0,
|
|
||||||
width: 0,
|
|
||||||
resizable: false,
|
|
||||||
visibleOnAllWorkspaces: false,
|
|
||||||
visible: false,
|
|
||||||
focus: true,
|
|
||||||
title: name,
|
|
||||||
decorations: false,
|
|
||||||
url: url,
|
|
||||||
theme: 'dark',
|
|
||||||
// @ts-expect-error
|
|
||||||
backgroundThrottling: 'disabled',
|
|
||||||
})
|
|
||||||
|
|
||||||
await showLoadingTray()
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, isFirstWindow ? 900 : 150))
|
|
||||||
await showDefaultTray()
|
|
||||||
|
|
||||||
// await w.hide()
|
|
||||||
await w.setSize(new LogicalSize(width, height))
|
|
||||||
await w.center()
|
|
||||||
await w.setDecorations(true)
|
|
||||||
await w.setZoom(1)
|
|
||||||
await w.show()
|
|
||||||
await w.setFocus()
|
|
||||||
|
|
||||||
useStore.setState({ firstWindow: false })
|
|
||||||
|
|
||||||
if (platform() === 'linux') {
|
|
||||||
await w.setResizable(false)
|
|
||||||
await w.setResizable(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
return w
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function openSmallWindow({
|
export async function openSmallWindow({
|
||||||
@@ -137,51 +38,14 @@ export async function openSmallWindow({
|
|||||||
url: string
|
url: string
|
||||||
}) {
|
}) {
|
||||||
console.log('[openSmallWindow] ', name, url)
|
console.log('[openSmallWindow] ', name, url)
|
||||||
|
await invoke('open_small_window', { name, url })
|
||||||
const isFirstWindow = useStore.getState().firstWindow
|
return WebviewWindow.getByLabel(name)
|
||||||
|
|
||||||
const w = new WebviewWindow(name, {
|
|
||||||
height: 0,
|
|
||||||
width: 0,
|
|
||||||
resizable: false,
|
|
||||||
visibleOnAllWorkspaces: false,
|
|
||||||
visible: false,
|
|
||||||
focus: true,
|
|
||||||
title: name,
|
|
||||||
decorations: false,
|
|
||||||
url: url,
|
|
||||||
theme: 'dark',
|
|
||||||
closable: false,
|
|
||||||
// @ts-expect-error
|
|
||||||
backgroundThrottling: 'disabled',
|
|
||||||
})
|
|
||||||
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, isFirstWindow ? 900 : 150))
|
|
||||||
|
|
||||||
await w.setSize(new LogicalSize(800, 500))
|
|
||||||
await w.center()
|
|
||||||
await w.setZoom(1)
|
|
||||||
await w.show()
|
|
||||||
await w.setFocus()
|
|
||||||
|
|
||||||
useStore.setState({ firstWindow: false })
|
|
||||||
|
|
||||||
if (platform() === 'linux') {
|
|
||||||
await w.setResizable(false)
|
|
||||||
await w.setResizable(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
return w
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function lockWindows(ids?: string[]) {
|
export async function lockWindows(ids?: string[]) {
|
||||||
const windows = await getAllWindows()
|
await invoke('lock_windows', { ids: ids ?? null })
|
||||||
const lockedWindows = ids ? windows.filter((w) => ids.includes(w.label)) : windows
|
|
||||||
await Promise.all(lockedWindows.map((w) => w.setClosable(false)))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function unlockWindows(ids?: string[]) {
|
export async function unlockWindows(ids?: string[]) {
|
||||||
const windows = await getAllWindows()
|
await invoke('unlock_windows', { ids: ids ?? null })
|
||||||
const unlockedWindows = ids ? windows.filter((w) => ids.includes(w.label)) : windows
|
|
||||||
await Promise.all(unlockedWindows.map((w) => w.setClosable(true)))
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,34 +2,43 @@ import * as Sentry from '@sentry/browser'
|
|||||||
import { getVersion as getUiVersion } from '@tauri-apps/api/app'
|
import { getVersion as getUiVersion } from '@tauri-apps/api/app'
|
||||||
import { invoke } from '@tauri-apps/api/core'
|
import { invoke } from '@tauri-apps/api/core'
|
||||||
import { getCurrentWindow } from '@tauri-apps/api/window'
|
import { getCurrentWindow } from '@tauri-apps/api/window'
|
||||||
|
import { getCurrent, onOpenUrl } from '@tauri-apps/plugin-deep-link'
|
||||||
import { ask, message } from '@tauri-apps/plugin-dialog'
|
import { ask, message } from '@tauri-apps/plugin-dialog'
|
||||||
import { debug, error, info, trace, warn } from '@tauri-apps/plugin-log'
|
import { debug, error, info, trace, warn } from '@tauri-apps/plugin-log'
|
||||||
import { platform } from '@tauri-apps/plugin-os'
|
import { platform } from '@tauri-apps/plugin-os'
|
||||||
import { exit, relaunch } from '@tauri-apps/plugin-process'
|
import { exit, relaunch } from '@tauri-apps/plugin-process'
|
||||||
|
import type { Child } from '@tauri-apps/plugin-shell'
|
||||||
import { check } from '@tauri-apps/plugin-updater'
|
import { check } from '@tauri-apps/plugin-updater'
|
||||||
import { CronExpressionParser } from 'cron-parser'
|
import { CronExpressionParser } from 'cron-parser'
|
||||||
import { defaultOptions } from 'tauri-plugin-sentry-api'
|
import { defaultOptions } from 'tauri-plugin-sentry-api'
|
||||||
|
import { getDeepLinkUrl, handleDeepLinkUrl } from './lib/deep'
|
||||||
import { isDirectoryEmpty } from './lib/fs'
|
import { isDirectoryEmpty } from './lib/fs'
|
||||||
|
import { LOCAL_HOST_ID, getHostInfo } from './lib/hosts'
|
||||||
import { validateLicense } from './lib/license'
|
import { validateLicense } from './lib/license'
|
||||||
import notify from './lib/notify'
|
import notify from './lib/notify'
|
||||||
|
import queryClient from './lib/query'
|
||||||
import {
|
import {
|
||||||
getRemote,
|
listTransfers,
|
||||||
listJobs,
|
startBisync,
|
||||||
listRemotes,
|
|
||||||
mountRemote,
|
|
||||||
startCopy,
|
startCopy,
|
||||||
startDelete,
|
startDelete,
|
||||||
|
startMount,
|
||||||
startMove,
|
startMove,
|
||||||
startPurge,
|
startPurge,
|
||||||
startSync,
|
startSync,
|
||||||
} from './lib/rclone/api'
|
} from './lib/rclone/api'
|
||||||
|
import rcloneClient from './lib/rclone/client'
|
||||||
import { compareVersions } from './lib/rclone/common'
|
import { compareVersions } from './lib/rclone/common'
|
||||||
import { SUPPORTED_BACKENDS } from './lib/rclone/constants'
|
|
||||||
import { initRclone } from './lib/rclone/init'
|
import { initRclone } from './lib/rclone/init'
|
||||||
import { usePersistedStore, useStore } from './lib/store'
|
import { initTray } from './lib/tray'
|
||||||
import { initTray, showDefaultTray, showLoadingTray } from './lib/tray'
|
|
||||||
import { openSmallWindow } from './lib/window'
|
import { openSmallWindow } from './lib/window'
|
||||||
import type { ScheduledTask } from './types/task'
|
import { initHostStore, useHostStore } from './store/host'
|
||||||
|
import { useStore } from './store/memory'
|
||||||
|
import { usePersistedStore } from './store/persisted'
|
||||||
|
import type { ScheduledTask } from './types/cron'
|
||||||
|
|
||||||
|
let currentRcloneChild: Child | null = null
|
||||||
|
let rcloneListenersRegistered = false
|
||||||
|
|
||||||
try {
|
try {
|
||||||
Sentry.init({
|
Sentry.init({
|
||||||
@@ -76,32 +85,99 @@ async function waitForHydration() {
|
|||||||
console.log('[waitForHydration] store hydrated')
|
console.log('[waitForHydration] store hydrated')
|
||||||
}
|
}
|
||||||
|
|
||||||
async function validateInstance() {
|
async function initializeHostStore() {
|
||||||
console.log('[validateInstance]')
|
console.log('[initializeHostStore] initializing')
|
||||||
|
const currentHost = usePersistedStore.getState().currentHost
|
||||||
|
// Default to 'local' if fresh install/no host selected
|
||||||
|
const hostId = currentHost?.id || 'local'
|
||||||
|
|
||||||
const isOnline = navigator.onLine
|
await initHostStore(hostId)
|
||||||
|
|
||||||
if (!isOnline && platform() !== 'linux') {
|
console.log('[initializeHostStore] initialized for', hostId)
|
||||||
await message(
|
}
|
||||||
'You are not connected to the internet. Please check your connection and try again.',
|
|
||||||
|
async function checkHostReachability(): Promise<void> {
|
||||||
|
console.log('[checkHostReachability] checking host reachability')
|
||||||
|
|
||||||
|
const currentHost = usePersistedStore.getState().currentHost
|
||||||
|
|
||||||
|
// If no host selected or local host, skip check (local rclone hasn't started yet)
|
||||||
|
if (!currentHost || currentHost.id === LOCAL_HOST_ID) {
|
||||||
|
console.log('[checkHostReachability] local or no host, skipping')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('[checkHostReachability] checking remote host:', currentHost.name)
|
||||||
|
|
||||||
|
const checkReachability = async (): Promise<boolean> => {
|
||||||
|
try {
|
||||||
|
const hostInfo = await getHostInfo({
|
||||||
|
url: currentHost.url,
|
||||||
|
authUser: currentHost.authUser,
|
||||||
|
authPassword: currentHost.authPassword,
|
||||||
|
})
|
||||||
|
return hostInfo !== null
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[checkHostReachability] error:', err)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let isReachable = await checkReachability()
|
||||||
|
|
||||||
|
while (!isReachable) {
|
||||||
|
console.log('[checkHostReachability] host not reachable')
|
||||||
|
|
||||||
|
const answer = await ask(
|
||||||
|
`The selected host "${currentHost.name}" is not reachable.\n\nURL: ${currentHost.url}\n\nWould you like to retry, switch to local host, or exit?`,
|
||||||
{
|
{
|
||||||
title: 'Error',
|
title: 'Host Not Reachable',
|
||||||
kind: 'error',
|
kind: 'warning',
|
||||||
okLabel: 'Exit',
|
okLabel: 'Retry',
|
||||||
|
cancelLabel: 'Use Local',
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
return await exit(0)
|
|
||||||
|
if (answer) {
|
||||||
|
// User chose to retry
|
||||||
|
console.log('[checkHostReachability] retrying connection')
|
||||||
|
isReachable = await checkReachability()
|
||||||
|
} else {
|
||||||
|
// User chose to use local host
|
||||||
|
console.log('[checkHostReachability] switching to local host')
|
||||||
|
const hosts = usePersistedStore.getState().hosts
|
||||||
|
const localHost = hosts.find((h) => h.id === LOCAL_HOST_ID)
|
||||||
|
if (localHost) {
|
||||||
|
usePersistedStore.setState({ currentHost: localHost })
|
||||||
|
}
|
||||||
|
// Re-initialize host store for local
|
||||||
|
await initHostStore(LOCAL_HOST_ID)
|
||||||
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log('[checkHostReachability] host is reachable')
|
||||||
|
}
|
||||||
|
|
||||||
|
async function validateInstance() {
|
||||||
|
console.log('[validateInstance] validating license')
|
||||||
|
|
||||||
const licenseKey = usePersistedStore.getState().licenseKey
|
const licenseKey = usePersistedStore.getState().licenseKey
|
||||||
if (!licenseKey) {
|
if (!licenseKey) {
|
||||||
|
console.log('[validateInstance] no license key, skipping license validation')
|
||||||
usePersistedStore.setState({ licenseValid: false })
|
usePersistedStore.setState({ licenseValid: false })
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!navigator.onLine) {
|
||||||
|
console.log('[validateInstance] not online, skipping license validation')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await validateLicense(licenseKey)
|
await validateLicense(licenseKey)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
console.log('[validateInstance] error validating license, marking as invalid')
|
||||||
usePersistedStore.setState({ licenseValid: false })
|
usePersistedStore.setState({ licenseValid: false })
|
||||||
|
|
||||||
if (e instanceof Error) {
|
if (e instanceof Error) {
|
||||||
@@ -110,6 +186,7 @@ async function validateInstance() {
|
|||||||
kind: 'error',
|
kind: 'error',
|
||||||
okLabel: 'OK',
|
okLabel: 'OK',
|
||||||
})
|
})
|
||||||
|
console.log('[validateInstance] error message displayed, returning')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -118,6 +195,9 @@ async function validateInstance() {
|
|||||||
kind: 'error',
|
kind: 'error',
|
||||||
okLabel: 'OK',
|
okLabel: 'OK',
|
||||||
})
|
})
|
||||||
|
console.log('[validateInstance] default error message displayed, returning')
|
||||||
|
} finally {
|
||||||
|
console.log('[validateInstance] license validation complete')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -148,7 +228,7 @@ async function checkAlreadyRunning() {
|
|||||||
console.log('[checkAlreadyRunning] windows, showing message')
|
console.log('[checkAlreadyRunning] windows, showing message')
|
||||||
|
|
||||||
await message(
|
await message(
|
||||||
"If you're on Windows, you might notice a few powershell/terminal windows opening and closing.\n\nThis is normal and expected, imagine we are playing whack-a-mole with the rclone process to close it.",
|
"If you're on Windows, you might notice a few powershell/terminal dialogs open and close.\n\nThis is normal and expected, imagine we are playing whack-a-mole with the rclone process to close it.",
|
||||||
{
|
{
|
||||||
'title': 'Trigger Warning',
|
'title': 'Trigger Warning',
|
||||||
'kind': 'info',
|
'kind': 'info',
|
||||||
@@ -158,7 +238,7 @@ async function checkAlreadyRunning() {
|
|||||||
}
|
}
|
||||||
const result = await invoke('stop_rclone_processes')
|
const result = await invoke('stop_rclone_processes')
|
||||||
console.log('[checkAlreadyRunning] stop_rclone_processes', result)
|
console.log('[checkAlreadyRunning] stop_rclone_processes', result)
|
||||||
await new Promise((resolve) => setTimeout(resolve, 1000))
|
await new Promise((resolve) => setTimeout(resolve, 700))
|
||||||
} else {
|
} else {
|
||||||
console.log('[checkAlreadyRunning] exiting')
|
console.log('[checkAlreadyRunning] exiting')
|
||||||
await exit(0)
|
await exit(0)
|
||||||
@@ -170,27 +250,137 @@ async function checkAlreadyRunning() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function registerRcloneWindowListeners() {
|
||||||
|
if (rcloneListenersRegistered) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const window = getCurrentWindow()
|
||||||
|
|
||||||
|
await window.listen('close-app', async () => {
|
||||||
|
console.log('[registerRcloneWindowListeners] close-app requested')
|
||||||
|
|
||||||
|
const transfers = await queryClient.ensureQueryData({
|
||||||
|
queryKey: ['transfers', 'list', 'all'],
|
||||||
|
queryFn: async () => await listTransfers(),
|
||||||
|
staleTime: 10_000, // 10 seconds
|
||||||
|
gcTime: 60_000, // 1 minute
|
||||||
|
})
|
||||||
|
|
||||||
|
if (transfers?.active && transfers.active.length > 0) {
|
||||||
|
const answer = await ask('All active transfers will be stopped.', {
|
||||||
|
title: 'Exit',
|
||||||
|
kind: 'info',
|
||||||
|
okLabel: 'Quit',
|
||||||
|
cancelLabel: 'Cancel',
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!answer) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const child = currentRcloneChild
|
||||||
|
|
||||||
|
if (child) {
|
||||||
|
try {
|
||||||
|
await child.kill()
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[close-app] failed to kill rclone child', error)
|
||||||
|
Sentry.captureException(error)
|
||||||
|
}
|
||||||
|
currentRcloneChild = null
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 1000))
|
||||||
|
}
|
||||||
|
|
||||||
|
await exit(0)
|
||||||
|
})
|
||||||
|
console.log('[registerRcloneWindowListeners] close-app listener registered')
|
||||||
|
|
||||||
|
await window.listen('relaunch-app', async () => {
|
||||||
|
console.log('[registerRcloneWindowListeners] relaunch-app requested')
|
||||||
|
|
||||||
|
const transfers = await queryClient.ensureQueryData({
|
||||||
|
queryKey: ['transfers', 'list', 'all'],
|
||||||
|
queryFn: async () => await listTransfers(),
|
||||||
|
staleTime: 10_000, // 10 seconds
|
||||||
|
gcTime: 60_000, // 1 minute
|
||||||
|
})
|
||||||
|
|
||||||
|
if (transfers?.active && transfers.active.length > 0) {
|
||||||
|
const answer = await ask('All active transfers will be stopped.', {
|
||||||
|
title: 'Exit',
|
||||||
|
kind: 'info',
|
||||||
|
okLabel: 'Relaunch',
|
||||||
|
cancelLabel: 'Cancel',
|
||||||
|
})
|
||||||
|
if (!answer) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const child = currentRcloneChild
|
||||||
|
|
||||||
|
if (child) {
|
||||||
|
try {
|
||||||
|
await child.kill()
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[relaunch-app] failed to kill rclone child', error)
|
||||||
|
Sentry.captureException(error)
|
||||||
|
}
|
||||||
|
currentRcloneChild = null
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 1000))
|
||||||
|
}
|
||||||
|
|
||||||
|
await relaunch()
|
||||||
|
})
|
||||||
|
console.log('[registerRcloneWindowListeners] relaunch-app listener registered')
|
||||||
|
|
||||||
|
await window.listen('restart-rclone', async () => {
|
||||||
|
console.log('[registerRcloneWindowListeners] restart-rclone requested')
|
||||||
|
|
||||||
|
if (useStore.getState().isRestartingRclone) {
|
||||||
|
console.log('[restart-rclone] restart already in progress, ignoring request')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
useStore.setState({ isRestartingRclone: true })
|
||||||
|
|
||||||
|
try {
|
||||||
|
const child = currentRcloneChild
|
||||||
|
|
||||||
|
if (child) {
|
||||||
|
try {
|
||||||
|
await child.kill()
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[restart-rclone] failed to exit rclone process', error)
|
||||||
|
Sentry.captureException(error)
|
||||||
|
}
|
||||||
|
currentRcloneChild = null
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 1000))
|
||||||
|
}
|
||||||
|
|
||||||
|
await startRclone()
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[restart-rclone] failed to restart rclone', error)
|
||||||
|
Sentry.captureException(error)
|
||||||
|
} finally {
|
||||||
|
useStore.setState({ isRestartingRclone: false })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
console.log('[registerRcloneWindowListeners] restart-rclone listener registered')
|
||||||
|
|
||||||
|
rcloneListenersRegistered = true
|
||||||
|
}
|
||||||
|
|
||||||
async function startRclone() {
|
async function startRclone() {
|
||||||
console.log('[startRclone]')
|
console.log('[startRclone]')
|
||||||
|
|
||||||
try {
|
await registerRcloneWindowListeners()
|
||||||
const remotes = await listRemotes()
|
|
||||||
console.log('[startRclone] rclone rcd already running')
|
|
||||||
useStore.setState({ rcloneLoaded: true })
|
|
||||||
useStore.setState({ remotes: remotes })
|
|
||||||
return
|
|
||||||
} catch (error) {
|
|
||||||
console.error('[startRclone] error', error)
|
|
||||||
Sentry.captureException(error)
|
|
||||||
}
|
|
||||||
|
|
||||||
let rclone: Awaited<ReturnType<typeof initRclone>> | null = null
|
let rclone: Awaited<ReturnType<typeof initRclone>> | null = null
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const sessionPassword = Math.random().toString(36).substring(2, 15)
|
|
||||||
useStore.setState({ rcloneAuth: sessionPassword })
|
|
||||||
useStore.setState({ rcloneAuthHeader: 'Basic ' + btoa(`admin:${sessionPassword}`) })
|
|
||||||
|
|
||||||
rclone = await initRclone([
|
rclone = await initRclone([
|
||||||
'rcd',
|
'rcd',
|
||||||
// ...(platform() === 'macos'
|
// ...(platform() === 'macos'
|
||||||
@@ -225,15 +415,9 @@ async function startRclone() {
|
|||||||
|
|
||||||
const command = rcloneCommandFn!
|
const command = rcloneCommandFn!
|
||||||
|
|
||||||
// command.stdout.on('data', (line) => {
|
|
||||||
// console.log('stdout ' + line)
|
|
||||||
// })
|
|
||||||
// command.stderr.on('data', (line) => {
|
|
||||||
// console.log('stderr ' + line)
|
|
||||||
// })
|
|
||||||
|
|
||||||
command.addListener('close', async (event) => {
|
command.addListener('close', async (event) => {
|
||||||
console.log('close', event)
|
console.log('close', event)
|
||||||
|
currentRcloneChild = null
|
||||||
|
|
||||||
if (platform() === 'windows') {
|
if (platform() === 'windows') {
|
||||||
return await exit(0)
|
return await exit(0)
|
||||||
@@ -262,105 +446,49 @@ async function startRclone() {
|
|||||||
|
|
||||||
console.log('[startRclone] starting rclone')
|
console.log('[startRclone] starting rclone')
|
||||||
const childProcess = await command.spawn()
|
const childProcess = await command.spawn()
|
||||||
|
currentRcloneChild = childProcess
|
||||||
console.log('[startRclone] running rclone')
|
console.log('[startRclone] running rclone')
|
||||||
|
|
||||||
getCurrentWindow().listen('close-app', async (e) => {
|
await new Promise((resolve) => setTimeout(resolve, 500))
|
||||||
console.log('[startRclone] (main) window close-app requested')
|
|
||||||
|
|
||||||
await showLoadingTray()
|
|
||||||
|
|
||||||
const jobs = await listJobs().catch(() => ({ active: [] }))
|
|
||||||
|
|
||||||
if (jobs.active.length > 0) {
|
|
||||||
const answer = await ask('All active jobs will be stopped.', {
|
|
||||||
title: 'Exit',
|
|
||||||
kind: 'info',
|
|
||||||
okLabel: 'Quit',
|
|
||||||
cancelLabel: 'Cancel',
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!answer) {
|
|
||||||
await showDefaultTray()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await childProcess.kill()
|
|
||||||
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, 1000))
|
|
||||||
|
|
||||||
await exit(0)
|
|
||||||
})
|
|
||||||
console.log('[startRclone] set listener for close-app')
|
|
||||||
|
|
||||||
getCurrentWindow().listen('relaunch-app', async (e) => {
|
|
||||||
console.log('[startRclone] (main) window relaunch-app requested')
|
|
||||||
await showLoadingTray()
|
|
||||||
|
|
||||||
const jobs = await listJobs().catch(() => ({ active: [] }))
|
|
||||||
|
|
||||||
if (jobs.active.length > 0) {
|
|
||||||
const answer = await ask('All active jobs will be stopped.', {
|
|
||||||
title: 'Exit',
|
|
||||||
kind: 'info',
|
|
||||||
okLabel: 'Relaunch',
|
|
||||||
cancelLabel: 'Cancel',
|
|
||||||
})
|
|
||||||
if (!answer) {
|
|
||||||
await showDefaultTray()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await childProcess.kill()
|
|
||||||
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, 1000))
|
|
||||||
|
|
||||||
await relaunch()
|
|
||||||
})
|
|
||||||
console.log('[startRclone] set listener for relaunch-app')
|
|
||||||
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, 200))
|
|
||||||
|
|
||||||
useStore.setState({ rcloneLoaded: true })
|
|
||||||
|
|
||||||
console.log('[startRclone] listing remotes')
|
|
||||||
const remotes = await listRemotes()
|
|
||||||
console.log('[startRclone] got remotes')
|
|
||||||
|
|
||||||
const remotesInfo = await Promise.all(
|
|
||||||
remotes.map(async (remote) => await getRemote(remote).catch(() => null))
|
|
||||||
)
|
|
||||||
const supportedRemotes = remotes.filter(
|
|
||||||
(_, index) => remotesInfo[index] && SUPPORTED_BACKENDS.includes(remotesInfo[index]!.type)
|
|
||||||
)
|
|
||||||
|
|
||||||
console.log(`[startRclone] skipped ${remotes.length - supportedRemotes.length} remotes`)
|
|
||||||
|
|
||||||
useStore.setState({ remotes: supportedRemotes })
|
|
||||||
|
|
||||||
// console.log('childProcess', JSON.stringify(childProcess)) // prints `pid`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function startupMounts() {
|
async function startupMounts() {
|
||||||
const remoteConfigList = usePersistedStore.getState().remoteConfigList
|
const remoteConfigList = useHostStore.getState().remoteConfigs
|
||||||
const remotes = useStore.getState().remotes
|
|
||||||
|
const remotes = await queryClient.ensureQueryData({
|
||||||
|
queryKey: ['remotes', 'list', 'all'],
|
||||||
|
queryFn: async () => await rcloneClient('/config/listremotes').then((r) => r?.remotes),
|
||||||
|
staleTime: 1000 * 60,
|
||||||
|
})
|
||||||
|
|
||||||
for (const remote in remotes) {
|
for (const remote in remotes) {
|
||||||
const remoteConfig = remoteConfigList[remote]
|
const remoteConfig = remoteConfigList[remote]
|
||||||
if (!remoteConfig) continue
|
if (!remoteConfig) continue
|
||||||
if (remoteConfig.mountOnStart && remoteConfig.defaultMountPoint) {
|
if (remoteConfig.mountOnStart?.enabled && remoteConfig.mountOnStart?.mountPoint) {
|
||||||
try {
|
try {
|
||||||
const isEmpty = await isDirectoryEmpty(remoteConfig.defaultMountPoint)
|
const isEmpty = await isDirectoryEmpty(remoteConfig.mountOnStart.mountPoint)
|
||||||
if (!isEmpty) {
|
if (!isEmpty) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
await mountRemote({
|
const {
|
||||||
remotePath: `${remote}:${remoteConfig?.defaultRemotePath || ''}`,
|
mountPoint,
|
||||||
mountPoint: remoteConfig.defaultMountPoint,
|
remotePath,
|
||||||
mountOptions: remoteConfig?.mountDefaults,
|
mountOptions,
|
||||||
vfsOptions: remoteConfig?.vfsDefaults,
|
vfsOptions,
|
||||||
|
filterOptions,
|
||||||
|
configOptions,
|
||||||
|
} = remoteConfig.mountOnStart
|
||||||
|
|
||||||
|
await startMount({
|
||||||
|
source: `${remote}:${remotePath}`,
|
||||||
|
destination: mountPoint,
|
||||||
|
options: {
|
||||||
|
mount: mountOptions,
|
||||||
|
vfs: vfsOptions,
|
||||||
|
filter: filterOptions,
|
||||||
|
config: configOptions,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error mounting remote:', error)
|
console.error('Error mounting remote:', error)
|
||||||
@@ -376,46 +504,61 @@ async function startupMounts() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function showStartup() {
|
async function showStartup() {
|
||||||
|
console.log('[showStartup] showing startup')
|
||||||
const hideStartup = usePersistedStore.getState().hideStartup
|
const hideStartup = usePersistedStore.getState().hideStartup
|
||||||
if (hideStartup) {
|
if (hideStartup) {
|
||||||
|
console.log('[showStartup] startup hidden, returning')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const startupDisplayed = useStore.getState().startupDisplayed
|
const startupDisplayed = useStore.getState().startupDisplayed
|
||||||
if (!startupDisplayed) {
|
if (startupDisplayed) {
|
||||||
useStore.setState({ startupDisplayed: true, startupStatus: 'initialized' })
|
console.log('[showStartup] startup already displayed, returning')
|
||||||
await openSmallWindow({
|
return
|
||||||
name: 'Startup',
|
|
||||||
url: '/startup',
|
|
||||||
})
|
|
||||||
if (!['windows', 'macos'].includes(platform())) {
|
|
||||||
usePersistedStore.setState({ hideStartup: true })
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log('[showStartup] startup not displayed, setting displayed and status')
|
||||||
|
useStore.setState({ startupDisplayed: true, startupStatus: 'initialized' })
|
||||||
|
console.log('[showStartup] store updated with startup displayed and status set')
|
||||||
|
await openSmallWindow({
|
||||||
|
name: 'Startup',
|
||||||
|
url: '/startup',
|
||||||
|
})
|
||||||
|
console.log('[showStartup] startup window opened')
|
||||||
|
if (!['windows', 'macos'].includes(platform())) {
|
||||||
|
usePersistedStore.setState({ hideStartup: true })
|
||||||
|
}
|
||||||
|
console.log('[showStartup] startup hidden')
|
||||||
}
|
}
|
||||||
|
|
||||||
const MAX_INT_MS = 2_147_483_647
|
const MAX_INT_MS = 2_147_483_647
|
||||||
let hasScheduledTasks = false
|
let hasScheduledTasks = false
|
||||||
async function resumeTasks() {
|
async function resumeTasks() {
|
||||||
console.log('resuming tasks')
|
console.log('[resumeTasks] resuming tasks')
|
||||||
|
|
||||||
if (hasScheduledTasks) {
|
if (hasScheduledTasks) {
|
||||||
|
console.log('[resumeTasks] tasks already resumed')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const scheduledTasks = usePersistedStore.getState().scheduledTasks
|
const scheduledTasks = useHostStore.getState().scheduledTasks
|
||||||
const activeConfigId = usePersistedStore.getState().activeConfigFile?.id
|
const activeConfigId = useHostStore.getState().activeConfigFile?.id
|
||||||
|
|
||||||
if (!activeConfigId) {
|
if (!activeConfigId) {
|
||||||
|
console.log('[resumeTasks] no active config id')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
hasScheduledTasks = true
|
||||||
|
|
||||||
|
console.log('[resumeTasks] resuming tasks for config', activeConfigId)
|
||||||
|
|
||||||
for (const task of scheduledTasks) {
|
for (const task of scheduledTasks) {
|
||||||
if (task.isRunning) {
|
if (task.isRunning) {
|
||||||
usePersistedStore.getState().updateScheduledTask(task.id, {
|
useHostStore.getState().updateScheduledTask(task.id, {
|
||||||
isRunning: false,
|
isRunning: false,
|
||||||
currentRunId: undefined,
|
currentRunId: undefined,
|
||||||
error: 'Task closed prematurely',
|
lastRunError: 'Task closed prematurely',
|
||||||
})
|
})
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -430,15 +573,15 @@ async function resumeTasks() {
|
|||||||
const difference = nextRun.getTime() - Date.now()
|
const difference = nextRun.getTime() - Date.now()
|
||||||
|
|
||||||
if (difference <= MAX_INT_MS && difference > 0) {
|
if (difference <= MAX_INT_MS && difference > 0) {
|
||||||
setTimeout(() => {
|
setTimeout(async () => {
|
||||||
console.log('running task', task)
|
console.log('running task', task)
|
||||||
notify({
|
notify({
|
||||||
title: 'Task Started',
|
title: 'Task Started',
|
||||||
body: `Task ${task.type} (${task.id}) started`,
|
body: `Task ${task.operation} (${task.id}) started`,
|
||||||
})
|
})
|
||||||
handleTask(task)
|
await handleTask(task)
|
||||||
}, difference)
|
}, difference)
|
||||||
console.log('scheduled task', task.type, task.id, nextRun)
|
console.log('scheduled task', task.operation, task.id, nextRun)
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error scheduling task:', error)
|
console.error('Error scheduling task:', error)
|
||||||
@@ -446,11 +589,11 @@ async function resumeTasks() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
hasScheduledTasks = true
|
console.log('[resumeTasks] tasks resumed')
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleTask(task: ScheduledTask) {
|
async function handleTask(task: ScheduledTask) {
|
||||||
const currentTask = usePersistedStore.getState().scheduledTasks.find((t) => t.id === task.id)
|
const currentTask = useHostStore.getState().scheduledTasks.find((t) => t.id === task.id)
|
||||||
|
|
||||||
if (!currentTask) {
|
if (!currentTask) {
|
||||||
return
|
return
|
||||||
@@ -462,15 +605,15 @@ async function handleTask(task: ScheduledTask) {
|
|||||||
|
|
||||||
const freshRunId = crypto.randomUUID()
|
const freshRunId = crypto.randomUUID()
|
||||||
|
|
||||||
usePersistedStore.getState().updateScheduledTask(task.id, {
|
useHostStore.getState().updateScheduledTask(task.id, {
|
||||||
isRunning: true,
|
isRunning: true,
|
||||||
currentRunId: freshRunId,
|
currentRunId: freshRunId,
|
||||||
lastRun: new Date().toISOString(),
|
lastRun: new Date().toISOString(),
|
||||||
})
|
})
|
||||||
|
|
||||||
console.log('running task', task.type, task.id)
|
console.log('running task', task.operation, task.id)
|
||||||
|
|
||||||
const currentRunId = usePersistedStore
|
const currentRunId = useHostStore
|
||||||
.getState()
|
.getState()
|
||||||
.scheduledTasks.find((t) => t.id === task.id)?.currentRunId
|
.scheduledTasks.find((t) => t.id === task.id)?.currentRunId
|
||||||
|
|
||||||
@@ -479,71 +622,69 @@ async function handleTask(task: ScheduledTask) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const {
|
switch (task.operation) {
|
||||||
srcFs,
|
case 'copy': {
|
||||||
dstFs,
|
const { sources, options, destination } = task.args
|
||||||
_config,
|
|
||||||
_filter,
|
|
||||||
createEmptySrcDirs,
|
|
||||||
deleteEmptyDstDirs,
|
|
||||||
fs,
|
|
||||||
rmDirs,
|
|
||||||
remote,
|
|
||||||
} = task.args
|
|
||||||
|
|
||||||
switch (task.type) {
|
|
||||||
case 'delete':
|
|
||||||
await startDelete({
|
|
||||||
fs,
|
|
||||||
rmDirs,
|
|
||||||
_filter,
|
|
||||||
_config,
|
|
||||||
})
|
|
||||||
break
|
|
||||||
case 'copy':
|
|
||||||
await startCopy({
|
await startCopy({
|
||||||
srcFs,
|
sources,
|
||||||
dstFs,
|
destination,
|
||||||
_config,
|
options,
|
||||||
_filter,
|
|
||||||
})
|
})
|
||||||
break
|
break
|
||||||
case 'move':
|
}
|
||||||
|
case 'move': {
|
||||||
|
const { sources, options, destination } = task.args
|
||||||
await startMove({
|
await startMove({
|
||||||
srcFs,
|
sources,
|
||||||
dstFs,
|
destination,
|
||||||
createEmptySrcDirs,
|
options,
|
||||||
deleteEmptyDstDirs,
|
|
||||||
_config,
|
|
||||||
_filter,
|
|
||||||
})
|
})
|
||||||
break
|
break
|
||||||
case 'sync':
|
}
|
||||||
|
case 'sync': {
|
||||||
|
const { source, destination, options } = task.args
|
||||||
await startSync({
|
await startSync({
|
||||||
srcFs,
|
source,
|
||||||
dstFs,
|
destination,
|
||||||
_config,
|
options,
|
||||||
_filter,
|
|
||||||
})
|
})
|
||||||
break
|
break
|
||||||
case 'purge':
|
}
|
||||||
|
case 'bisync': {
|
||||||
|
const { source, destination, options } = task.args
|
||||||
|
await startBisync({
|
||||||
|
source,
|
||||||
|
destination,
|
||||||
|
options,
|
||||||
|
})
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'delete': {
|
||||||
|
const { sources, options } = task.args
|
||||||
|
await startDelete({
|
||||||
|
sources,
|
||||||
|
options,
|
||||||
|
})
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'purge': {
|
||||||
|
const { sources, options } = task.args
|
||||||
await startPurge({
|
await startPurge({
|
||||||
fs,
|
sources,
|
||||||
remote,
|
options,
|
||||||
_filter,
|
|
||||||
_config,
|
|
||||||
})
|
})
|
||||||
break
|
break
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
Sentry.captureException(err)
|
Sentry.captureException(err)
|
||||||
console.error('Failed to start task:', err)
|
console.error('Failed to start task:', err)
|
||||||
usePersistedStore.getState().updateScheduledTask(task.id, {
|
useHostStore.getState().updateScheduledTask(task.id, {
|
||||||
isRunning: false,
|
isRunning: false,
|
||||||
currentRunId: undefined,
|
currentRunId: undefined,
|
||||||
error: err instanceof Error ? err.message : 'Unknown error',
|
lastRunError: err instanceof Error ? err.message : 'Unknown error',
|
||||||
})
|
})
|
||||||
} finally {
|
} finally {
|
||||||
}
|
}
|
||||||
@@ -560,7 +701,10 @@ async function checkVersion() {
|
|||||||
)
|
)
|
||||||
console.log('[checkVersion] meta.json fetched')
|
console.log('[checkVersion] meta.json fetched')
|
||||||
|
|
||||||
const metaJsonData = await metaJson.json()
|
const metaJsonData = (await metaJson.json()) as {
|
||||||
|
minimumVersion: string
|
||||||
|
okVersion: string
|
||||||
|
}
|
||||||
console.log('[checkVersion] meta.json parsed')
|
console.log('[checkVersion] meta.json parsed')
|
||||||
|
|
||||||
const { minimumVersion, okVersion } = metaJsonData
|
const { minimumVersion, okVersion } = metaJsonData
|
||||||
@@ -666,35 +810,78 @@ async function checkVersion() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function checkTraySupport() {
|
async function checkRclone() {
|
||||||
console.log('[checkTraySupport] platform', platform())
|
let currentHost = usePersistedStore.getState().currentHost
|
||||||
|
|
||||||
let traySupported = false
|
if (!currentHost) {
|
||||||
|
currentHost = {
|
||||||
try {
|
id: 'local',
|
||||||
traySupported = await invoke<boolean>('is_tray_supported')
|
name: 'Local Machine',
|
||||||
} catch (error) {
|
url: 'http://localhost:5572',
|
||||||
console.error('[checkTraySupport] error', error)
|
os: 'linux',
|
||||||
Sentry.captureException(error)
|
cliVersion: 'unknown',
|
||||||
}
|
|
||||||
|
|
||||||
if (!traySupported) {
|
|
||||||
console.log('[checkTraySupport] tray not supported')
|
|
||||||
|
|
||||||
const confirmed = await ask(
|
|
||||||
'Your desktop environment does not appear to have a tray/menubar.\n\nRclone UI requires a tray to run. Do you still wish to continue?',
|
|
||||||
{
|
|
||||||
title: 'Tray Not Supported',
|
|
||||||
kind: 'error',
|
|
||||||
okLabel: 'Continue',
|
|
||||||
cancelLabel: 'Exit Now',
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
if (!confirmed) {
|
|
||||||
await Promise.race([new Promise((resolve) => setTimeout(resolve, 100_000)), exit(0)])
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let hostInfo = await getHostInfo({
|
||||||
|
url: currentHost.url,
|
||||||
|
authUser: currentHost.authUser,
|
||||||
|
authPassword: currentHost.authPassword,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!hostInfo) {
|
||||||
|
await message(
|
||||||
|
'Failed to get host info, is it online and reachable?\n\nSwitching to local host.',
|
||||||
|
{
|
||||||
|
title: 'Host Not Reachable',
|
||||||
|
kind: 'error',
|
||||||
|
}
|
||||||
|
)
|
||||||
|
currentHost = {
|
||||||
|
id: 'local',
|
||||||
|
name: 'Local Machine',
|
||||||
|
url: 'http://localhost:5572',
|
||||||
|
os: 'linux',
|
||||||
|
cliVersion: 'unknown',
|
||||||
|
}
|
||||||
|
|
||||||
|
hostInfo = await getHostInfo({
|
||||||
|
url: currentHost.url,
|
||||||
|
authUser: currentHost.authUser,
|
||||||
|
authPassword: currentHost.authPassword,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!hostInfo) {
|
||||||
|
const confirmed = await ask(
|
||||||
|
'Local host is not reachable, please try again or file an issue on Github if the problem persists.',
|
||||||
|
{
|
||||||
|
title: 'Local Host Not Reachable',
|
||||||
|
kind: 'error',
|
||||||
|
okLabel: 'Exit',
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
if (confirmed) {
|
||||||
|
return await exit(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 60_000))
|
||||||
|
return await exit(0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
currentHost = {
|
||||||
|
...currentHost,
|
||||||
|
os: hostInfo.os,
|
||||||
|
cliVersion: hostInfo.cliVersion,
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('[checkRclone] setting currentHost', currentHost)
|
||||||
|
|
||||||
|
usePersistedStore.setState({ currentHost })
|
||||||
|
usePersistedStore.setState((prev) => ({
|
||||||
|
hosts: [...prev.hosts.filter((h) => h.id !== currentHost!.id), currentHost],
|
||||||
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
getCurrentWindow().listen('tauri://close-requested', async (e) => {
|
getCurrentWindow().listen('tauri://close-requested', async (e) => {
|
||||||
@@ -702,25 +889,52 @@ getCurrentWindow().listen('tauri://close-requested', async (e) => {
|
|||||||
await getCurrentWindow().destroy()
|
await getCurrentWindow().destroy()
|
||||||
})
|
})
|
||||||
|
|
||||||
getCurrentWindow().listen('rebuild-tray', async (e) => {
|
// maybe place this inside handleDeepLink?
|
||||||
console.log('(main) window rebuild-tray requested')
|
onOpenUrl((urls) => {
|
||||||
|
console.log('deep links while running', urls)
|
||||||
|
const receivedUrl = urls[0]
|
||||||
|
|
||||||
// wait for store to be updated
|
const deepLinkUrl = getDeepLinkUrl(receivedUrl)
|
||||||
await new Promise((resolve) => setTimeout(resolve, 250))
|
|
||||||
|
|
||||||
await showDefaultTray()
|
console.log('deep link url', deepLinkUrl)
|
||||||
|
|
||||||
|
handleDeepLinkUrl(deepLinkUrl)
|
||||||
|
|
||||||
|
useStore.setState({ startupDisplayed: true, startupStatus: 'initializing' })
|
||||||
})
|
})
|
||||||
|
|
||||||
checkTraySupport()
|
async function handleDeepLink() {
|
||||||
.then(() => initTray())
|
console.log('[handleDeepLink] getting current deep links')
|
||||||
.then(() => showLoadingTray())
|
const urls = await getCurrent()
|
||||||
.then(() => waitForHydration())
|
if (!urls || urls.length === 0) {
|
||||||
|
console.log('[handleDeepLink] no deep links found')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('[handleDeepLink] getting deep link url')
|
||||||
|
const deepLinkUrl = getDeepLinkUrl(urls[0])
|
||||||
|
console.log('[handleDeepLink] deep link url', deepLinkUrl)
|
||||||
|
|
||||||
|
console.log('[handleDeepLink] handling deep link url')
|
||||||
|
handleDeepLinkUrl(deepLinkUrl)
|
||||||
|
console.log('[handleDeepLink] deep link url handled')
|
||||||
|
|
||||||
|
console.log('[handleDeepLink] setting startup displayed and status')
|
||||||
|
useStore.setState({ startupDisplayed: true, startupStatus: 'initializing' })
|
||||||
|
console.log('[handleDeepLink] startup displayed and status set')
|
||||||
|
}
|
||||||
|
|
||||||
|
waitForHydration()
|
||||||
|
.then(() => initializeHostStore())
|
||||||
|
.then(() => checkHostReachability())
|
||||||
.then(() => checkVersion())
|
.then(() => checkVersion())
|
||||||
.then(() => validateInstance())
|
.then(() => validateInstance())
|
||||||
.then(() => checkAlreadyRunning())
|
.then(() => checkAlreadyRunning())
|
||||||
.then(() => startRclone())
|
.then(() => startRclone())
|
||||||
|
.then(() => checkRclone())
|
||||||
|
.then(() => handleDeepLink())
|
||||||
.then(() => showStartup())
|
.then(() => showStartup())
|
||||||
.then(() => startupMounts())
|
.then(() => startupMounts())
|
||||||
.then(() => resumeTasks())
|
.then(() => resumeTasks())
|
||||||
.then(() => showDefaultTray())
|
.then(() => initTray())
|
||||||
.catch(console.error)
|
.catch(console.error)
|
||||||
|
|||||||
@@ -1,20 +1,24 @@
|
|||||||
{
|
{
|
||||||
"name": "rclone-ui",
|
"name": "rclone-ui",
|
||||||
"version": "2.8.1",
|
"version": "3.0.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "rclone-ui",
|
"name": "rclone-ui",
|
||||||
"version": "2.8.1",
|
"version": "3.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@formkit/auto-animate": "^0.9.0",
|
"@formkit/auto-animate": "^0.9.0",
|
||||||
"@heroui/react": "^2.7.11",
|
"@heroui/react": "^2.7.11",
|
||||||
"@mux/mux-player-react": "^3.8.0",
|
"@mux/mux-player-react": "^3.8.0",
|
||||||
"@sentry/browser": "^10.8.0",
|
"@sentry/browser": "^10.8.0",
|
||||||
|
"@tanstack/query-persist-client-core": "^5.91.6",
|
||||||
|
"@tanstack/query-sync-storage-persister": "^5.90.9",
|
||||||
|
"@tanstack/react-query": "^5.90.7",
|
||||||
"@tauri-apps/api": "~2.9.0",
|
"@tauri-apps/api": "~2.9.0",
|
||||||
"@tauri-apps/plugin-autostart": "^2.5.1",
|
"@tauri-apps/plugin-autostart": "^2.5.1",
|
||||||
"@tauri-apps/plugin-clipboard-manager": "^2.3.2",
|
"@tauri-apps/plugin-clipboard-manager": "^2.3.2",
|
||||||
|
"@tauri-apps/plugin-deep-link": "^2.4.5",
|
||||||
"@tauri-apps/plugin-dialog": "^2.4.2",
|
"@tauri-apps/plugin-dialog": "^2.4.2",
|
||||||
"@tauri-apps/plugin-fs": "^2.4.4",
|
"@tauri-apps/plugin-fs": "^2.4.4",
|
||||||
"@tauri-apps/plugin-http": "^2.5.4",
|
"@tauri-apps/plugin-http": "^2.5.4",
|
||||||
@@ -27,14 +31,16 @@
|
|||||||
"@tauri-apps/plugin-store": "^2.4.1",
|
"@tauri-apps/plugin-store": "^2.4.1",
|
||||||
"@tauri-apps/plugin-updater": "^2.9.0",
|
"@tauri-apps/plugin-updater": "^2.9.0",
|
||||||
"@tauri-apps/plugin-window-state": "^2.4.1",
|
"@tauri-apps/plugin-window-state": "^2.4.1",
|
||||||
|
"canvas-confetti": "^1.9.4",
|
||||||
"cron-parser": "^5.4.0",
|
"cron-parser": "^5.4.0",
|
||||||
"cronstrue": "^3.9.0",
|
"cronstrue": "^3.9.0",
|
||||||
"date-fns": "^4.1.0",
|
"date-fns": "^4.1.0",
|
||||||
"framer-motion": "^11.18.2",
|
"framer-motion": "^11.18.2",
|
||||||
|
"fuse.js": "^7.1.0",
|
||||||
"lucide-react": "^0.552.0",
|
"lucide-react": "^0.552.0",
|
||||||
"p-retry": "^7.1.0",
|
"p-retry": "^7.1.0",
|
||||||
|
"rclone-sdk": "^1.72.0",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-compiler-runtime": "^19.1.0-rc.3",
|
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
"react-router-dom": "^6.28.1",
|
"react-router-dom": "^6.28.1",
|
||||||
"tauri-plugin-sentry-api": "^0.5.0",
|
"tauri-plugin-sentry-api": "^0.5.0",
|
||||||
@@ -46,6 +52,8 @@
|
|||||||
"@biomejs/biome": "^1.9.4",
|
"@biomejs/biome": "^1.9.4",
|
||||||
"@iarna/toml": "^2.2.5",
|
"@iarna/toml": "^2.2.5",
|
||||||
"@tauri-apps/cli": "^2.9.2",
|
"@tauri-apps/cli": "^2.9.2",
|
||||||
|
"@total-typescript/ts-reset": "^0.6.1",
|
||||||
|
"@types/canvas-confetti": "^1.9.0",
|
||||||
"@types/node": "^22.10.7",
|
"@types/node": "^22.10.7",
|
||||||
"@types/react": "^18.3.1",
|
"@types/react": "^18.3.1",
|
||||||
"@types/react-dom": "^18.3.1",
|
"@types/react-dom": "^18.3.1",
|
||||||
@@ -89,7 +97,7 @@
|
|||||||
"version": "7.27.1",
|
"version": "7.27.1",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz",
|
||||||
"integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==",
|
"integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==",
|
||||||
"dev": true,
|
"devOptional": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/helper-validator-identifier": "^7.27.1",
|
"@babel/helper-validator-identifier": "^7.27.1",
|
||||||
@@ -241,7 +249,7 @@
|
|||||||
"version": "7.27.1",
|
"version": "7.27.1",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz",
|
||||||
"integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==",
|
"integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==",
|
||||||
"dev": true,
|
"devOptional": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6.9.0"
|
"node": ">=6.9.0"
|
||||||
@@ -4886,6 +4894,69 @@
|
|||||||
"react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
|
"react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@redocly/ajv": {
|
||||||
|
"version": "8.11.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@redocly/ajv/-/ajv-8.11.4.tgz",
|
||||||
|
"integrity": "sha512-77MhyFgZ1zGMwtCpqsk532SJEc3IJmSOXKTCeWoMTAvPnQOkuOgxEip1n5pG5YX1IzCTJ4kCvPKr8xYyzWFdhg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"peer": true,
|
||||||
|
"dependencies": {
|
||||||
|
"fast-deep-equal": "^3.1.1",
|
||||||
|
"json-schema-traverse": "^1.0.0",
|
||||||
|
"require-from-string": "^2.0.2",
|
||||||
|
"uri-js-replace": "^1.0.1"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/epoberezkin"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@redocly/config": {
|
||||||
|
"version": "0.22.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@redocly/config/-/config-0.22.2.tgz",
|
||||||
|
"integrity": "sha512-roRDai8/zr2S9YfmzUfNhKjOF0NdcOIqF7bhf4MVC5UxpjIysDjyudvlAiVbpPHp3eDRWbdzUgtkK1a7YiDNyQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"peer": true
|
||||||
|
},
|
||||||
|
"node_modules/@redocly/openapi-core": {
|
||||||
|
"version": "1.34.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@redocly/openapi-core/-/openapi-core-1.34.5.tgz",
|
||||||
|
"integrity": "sha512-0EbE8LRbkogtcCXU7liAyC00n9uNG9hJ+eMyHFdUsy9lB/WGqnEBgwjA9q2cyzAVcdTkQqTBBU1XePNnN3OijA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"peer": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@redocly/ajv": "^8.11.2",
|
||||||
|
"@redocly/config": "^0.22.0",
|
||||||
|
"colorette": "^1.2.0",
|
||||||
|
"https-proxy-agent": "^7.0.5",
|
||||||
|
"js-levenshtein": "^1.1.6",
|
||||||
|
"js-yaml": "^4.1.0",
|
||||||
|
"minimatch": "^5.0.1",
|
||||||
|
"pluralize": "^8.0.0",
|
||||||
|
"yaml-ast-parser": "0.0.43"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18.17.0",
|
||||||
|
"npm": ">=9.5.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@redocly/openapi-core/node_modules/minimatch": {
|
||||||
|
"version": "5.1.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz",
|
||||||
|
"integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==",
|
||||||
|
"license": "ISC",
|
||||||
|
"optional": true,
|
||||||
|
"peer": true,
|
||||||
|
"dependencies": {
|
||||||
|
"brace-expansion": "^2.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@remix-run/router": {
|
"node_modules/@remix-run/router": {
|
||||||
"version": "1.21.0",
|
"version": "1.21.0",
|
||||||
"resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.21.0.tgz",
|
"resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.21.0.tgz",
|
||||||
@@ -5265,6 +5336,59 @@
|
|||||||
"tslib": "^2.8.0"
|
"tslib": "^2.8.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@tanstack/query-core": {
|
||||||
|
"version": "5.90.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.90.7.tgz",
|
||||||
|
"integrity": "sha512-6PN65csiuTNfBMXqQUxQhCNdtm1rV+9kC9YwWAIKcaxAauq3Wu7p18j3gQY3YIBJU70jT/wzCCZ2uqto/vQgiQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/tannerlinsley"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tanstack/query-persist-client-core": {
|
||||||
|
"version": "5.91.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tanstack/query-persist-client-core/-/query-persist-client-core-5.91.6.tgz",
|
||||||
|
"integrity": "sha512-VDnWKhXn4YD8fosh35oSEkwqvsO9vveOyswp4bL/L8SZuQukPFRjyNIq4xGHO/olQneudLt57MdA4mYLAp5P5g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@tanstack/query-core": "5.90.7"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/tannerlinsley"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tanstack/query-sync-storage-persister": {
|
||||||
|
"version": "5.90.9",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tanstack/query-sync-storage-persister/-/query-sync-storage-persister-5.90.9.tgz",
|
||||||
|
"integrity": "sha512-8x/1w8G2mWAI5gpnJqOM5vPxAsx3SXFrk1bsStr8wpaXFtAJWk+K5dMwmYB5ee7pVkeUvBhNo4+UPOMvG/GgwQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@tanstack/query-core": "5.90.7",
|
||||||
|
"@tanstack/query-persist-client-core": "5.91.6"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/tannerlinsley"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tanstack/react-query": {
|
||||||
|
"version": "5.90.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.90.7.tgz",
|
||||||
|
"integrity": "sha512-wAHc/cgKzW7LZNFloThyHnV/AX9gTg3w5yAv0gvQHPZoCnepwqCMtzbuPbb2UvfvO32XZ46e8bPOYbfZhzVnnQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@tanstack/query-core": "5.90.7"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/tannerlinsley"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^18 || ^19"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@tanstack/react-virtual": {
|
"node_modules/@tanstack/react-virtual": {
|
||||||
"version": "3.11.3",
|
"version": "3.11.3",
|
||||||
"resolved": "https://registry.npmjs.org/@tanstack/react-virtual/-/react-virtual-3.11.3.tgz",
|
"resolved": "https://registry.npmjs.org/@tanstack/react-virtual/-/react-virtual-3.11.3.tgz",
|
||||||
@@ -5537,6 +5661,15 @@
|
|||||||
"@tauri-apps/api": "^2.8.0"
|
"@tauri-apps/api": "^2.8.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@tauri-apps/plugin-deep-link": {
|
||||||
|
"version": "2.4.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-deep-link/-/plugin-deep-link-2.4.5.tgz",
|
||||||
|
"integrity": "sha512-Zf2RTj1D9IQQ45/jqW8XTKvql24HqlPjcpv0mV/O2jHQkNe11HOTZBVj6IK37qs+MWV7xZzcmazx/QVZnhAwaQ==",
|
||||||
|
"license": "MIT OR Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@tauri-apps/api": "^2.8.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@tauri-apps/plugin-dialog": {
|
"node_modules/@tauri-apps/plugin-dialog": {
|
||||||
"version": "2.4.2",
|
"version": "2.4.2",
|
||||||
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-dialog/-/plugin-dialog-2.4.2.tgz",
|
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-dialog/-/plugin-dialog-2.4.2.tgz",
|
||||||
@@ -5645,6 +5778,13 @@
|
|||||||
"@tauri-apps/api": "^2.8.0"
|
"@tauri-apps/api": "^2.8.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@total-typescript/ts-reset": {
|
||||||
|
"version": "0.6.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@total-typescript/ts-reset/-/ts-reset-0.6.1.tgz",
|
||||||
|
"integrity": "sha512-cka47fVSo6lfQDIATYqb/vO1nvFfbPw7uWLayIXIhGETj0wcOOlrlkobOMDNQOFr9QOafegUPq13V2+6vtD7yg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@types/babel__core": {
|
"node_modules/@types/babel__core": {
|
||||||
"version": "7.20.5",
|
"version": "7.20.5",
|
||||||
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
|
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
|
||||||
@@ -5686,6 +5826,13 @@
|
|||||||
"@babel/types": "^7.20.7"
|
"@babel/types": "^7.20.7"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/canvas-confetti": {
|
||||||
|
"version": "1.9.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/canvas-confetti/-/canvas-confetti-1.9.0.tgz",
|
||||||
|
"integrity": "sha512-aBGj/dULrimR1XDZLtG9JwxX1b4HPRF6CX9Yfwh3NvstZEm1ZL7RBnel4keCPSqs1ANRu1u2Aoz9R+VmtjYuTg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@types/estree": {
|
"node_modules/@types/estree": {
|
||||||
"version": "1.0.8",
|
"version": "1.0.8",
|
||||||
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
|
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
|
||||||
@@ -5762,6 +5909,28 @@
|
|||||||
"node": ">=0.4.0"
|
"node": ">=0.4.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/agent-base": {
|
||||||
|
"version": "7.1.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz",
|
||||||
|
"integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"peer": true,
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 14"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/ansi-colors": {
|
||||||
|
"version": "4.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz",
|
||||||
|
"integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"peer": true,
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/ansi-regex": {
|
"node_modules/ansi-regex": {
|
||||||
"version": "6.1.0",
|
"version": "6.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz",
|
||||||
@@ -5806,6 +5975,14 @@
|
|||||||
"resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz",
|
||||||
"integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg=="
|
"integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg=="
|
||||||
},
|
},
|
||||||
|
"node_modules/argparse": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
|
||||||
|
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
|
||||||
|
"license": "Python-2.0",
|
||||||
|
"optional": true,
|
||||||
|
"peer": true
|
||||||
|
},
|
||||||
"node_modules/autoprefixer": {
|
"node_modules/autoprefixer": {
|
||||||
"version": "10.4.21",
|
"version": "10.4.21",
|
||||||
"resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.21.tgz",
|
"resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.21.tgz",
|
||||||
@@ -5959,6 +6136,16 @@
|
|||||||
],
|
],
|
||||||
"license": "CC-BY-4.0"
|
"license": "CC-BY-4.0"
|
||||||
},
|
},
|
||||||
|
"node_modules/canvas-confetti": {
|
||||||
|
"version": "1.9.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/canvas-confetti/-/canvas-confetti-1.9.4.tgz",
|
||||||
|
"integrity": "sha512-yxQbJkAVrFXWNbTUjPqjF7G+g6pDotOUHGbkZq2NELZUMDpiJ85rIEazVb8GTaAptNW2miJAXbs1BtioA251Pw==",
|
||||||
|
"license": "ISC",
|
||||||
|
"funding": {
|
||||||
|
"type": "donate",
|
||||||
|
"url": "https://www.paypal.me/kirilvatev"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/castable-video": {
|
"node_modules/castable-video": {
|
||||||
"version": "1.1.11",
|
"version": "1.1.11",
|
||||||
"resolved": "https://registry.npmjs.org/castable-video/-/castable-video-1.1.11.tgz",
|
"resolved": "https://registry.npmjs.org/castable-video/-/castable-video-1.1.11.tgz",
|
||||||
@@ -5977,6 +6164,14 @@
|
|||||||
"react": ">=17.0.0"
|
"react": ">=17.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/change-case": {
|
||||||
|
"version": "5.4.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/change-case/-/change-case-5.4.4.tgz",
|
||||||
|
"integrity": "sha512-HRQyTk2/YPEkt9TnUPbOpr64Uw3KOicFWPVBb+xiHvd6eBx/qPr9xqfBFDT8P2vWsvvz4jbEkfDe71W3VyNu2w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"peer": true
|
||||||
|
},
|
||||||
"node_modules/chokidar": {
|
"node_modules/chokidar": {
|
||||||
"version": "3.6.0",
|
"version": "3.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
|
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
|
||||||
@@ -6065,6 +6260,14 @@
|
|||||||
"integrity": "sha512-zW190nQTIoXcGCaU08DvVNFTmQhUpnJfVuAKfWqUQkflXKpaDdpaYoM0iluLS9lgJNHyBF58KKA2FBEwkD7wog==",
|
"integrity": "sha512-zW190nQTIoXcGCaU08DvVNFTmQhUpnJfVuAKfWqUQkflXKpaDdpaYoM0iluLS9lgJNHyBF58KKA2FBEwkD7wog==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/colorette": {
|
||||||
|
"version": "1.4.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/colorette/-/colorette-1.4.0.tgz",
|
||||||
|
"integrity": "sha512-Y2oEozpomLn7Q3HFP7dpww7AtMJplbM9lGZP6RDfHqmbeRjiwRg4n6VM6j4KLmRke85uWEI7JqF17f3pqdRA0g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"peer": true
|
||||||
|
},
|
||||||
"node_modules/commander": {
|
"node_modules/commander": {
|
||||||
"version": "4.1.1",
|
"version": "4.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz",
|
||||||
@@ -6157,7 +6360,7 @@
|
|||||||
"version": "4.4.1",
|
"version": "4.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz",
|
||||||
"integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==",
|
"integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==",
|
||||||
"dev": true,
|
"devOptional": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"ms": "^2.1.3"
|
"ms": "^2.1.3"
|
||||||
@@ -6186,6 +6389,16 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/dequal": {
|
||||||
|
"version": "2.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz",
|
||||||
|
"integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/didyoumean": {
|
"node_modules/didyoumean": {
|
||||||
"version": "1.2.2",
|
"version": "1.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz",
|
||||||
@@ -6265,6 +6478,14 @@
|
|||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/fast-deep-equal": {
|
||||||
|
"version": "3.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
|
||||||
|
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"peer": true
|
||||||
|
},
|
||||||
"node_modules/fast-glob": {
|
"node_modules/fast-glob": {
|
||||||
"version": "3.3.2",
|
"version": "3.3.2",
|
||||||
"resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz",
|
"resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz",
|
||||||
@@ -6395,6 +6616,15 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/fuse.js": {
|
||||||
|
"version": "7.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/fuse.js/-/fuse.js-7.1.0.tgz",
|
||||||
|
"integrity": "sha512-trLf4SzuuUxfusZADLINj+dE8clK1frKdmqiJNb1Es75fmI5oY6X2mxLVUciLLjxqw/xr72Dhy+lER6dGd02FQ==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/gensync": {
|
"node_modules/gensync": {
|
||||||
"version": "1.0.0-beta.2",
|
"version": "1.0.0-beta.2",
|
||||||
"resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
|
"resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
|
||||||
@@ -6452,6 +6682,35 @@
|
|||||||
"integrity": "sha512-CSpT2aXsv71HST8C5ETeVo+6YybqCpHBiYrCRQSn3U5QUZuLTSsvtq/bj+zuvjLVADeKxoebzo16OkH8m1+65Q==",
|
"integrity": "sha512-CSpT2aXsv71HST8C5ETeVo+6YybqCpHBiYrCRQSn3U5QUZuLTSsvtq/bj+zuvjLVADeKxoebzo16OkH8m1+65Q==",
|
||||||
"license": "Apache-2.0"
|
"license": "Apache-2.0"
|
||||||
},
|
},
|
||||||
|
"node_modules/https-proxy-agent": {
|
||||||
|
"version": "7.0.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz",
|
||||||
|
"integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"peer": true,
|
||||||
|
"dependencies": {
|
||||||
|
"agent-base": "^7.1.2",
|
||||||
|
"debug": "4"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 14"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/index-to-position": {
|
||||||
|
"version": "1.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/index-to-position/-/index-to-position-1.2.0.tgz",
|
||||||
|
"integrity": "sha512-Yg7+ztRkqslMAS2iFaU+Oa4KTSidr63OsFGlOrJoW981kIYO3CGCS3wA95P1mUi/IVSJkn0D479KTJpVpvFNuw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"peer": true,
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/input-otp": {
|
"node_modules/input-otp": {
|
||||||
"version": "1.4.1",
|
"version": "1.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/input-otp/-/input-otp-1.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/input-otp/-/input-otp-1.4.1.tgz",
|
||||||
@@ -6578,11 +6837,36 @@
|
|||||||
"jiti": "bin/jiti.js"
|
"jiti": "bin/jiti.js"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/js-levenshtein": {
|
||||||
|
"version": "1.1.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/js-levenshtein/-/js-levenshtein-1.1.6.tgz",
|
||||||
|
"integrity": "sha512-X2BB11YZtrRqY4EnQcLX5Rh373zbK4alC1FW7D7MBhL2gtcC17cTnr6DmfHZeS0s2rTHjUTMMHfG7gO8SSdw+g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"peer": true,
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/js-tokens": {
|
"node_modules/js-tokens": {
|
||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
||||||
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="
|
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="
|
||||||
},
|
},
|
||||||
|
"node_modules/js-yaml": {
|
||||||
|
"version": "4.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
|
||||||
|
"integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"peer": true,
|
||||||
|
"dependencies": {
|
||||||
|
"argparse": "^2.0.1"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"js-yaml": "bin/js-yaml.js"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/jsesc": {
|
"node_modules/jsesc": {
|
||||||
"version": "3.1.0",
|
"version": "3.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
|
||||||
@@ -6596,6 +6880,14 @@
|
|||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/json-schema-traverse": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"peer": true
|
||||||
|
},
|
||||||
"node_modules/json5": {
|
"node_modules/json5": {
|
||||||
"version": "2.2.3",
|
"version": "2.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
|
||||||
@@ -6740,7 +7032,7 @@
|
|||||||
"version": "2.1.3",
|
"version": "2.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||||
"dev": true,
|
"devOptional": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/mux-embed": {
|
"node_modules/mux-embed": {
|
||||||
@@ -6817,6 +7109,34 @@
|
|||||||
"node": ">= 6"
|
"node": ">= 6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/openapi-fetch": {
|
||||||
|
"version": "0.15.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/openapi-fetch/-/openapi-fetch-0.15.0.tgz",
|
||||||
|
"integrity": "sha512-OjQUdi61WO4HYhr9+byCPMj0+bgste/LtSBEcV6FzDdONTs7x0fWn8/ndoYwzqCsKWIxEZwo4FN/TG1c1rI8IQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"openapi-typescript-helpers": "^0.0.15"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/openapi-react-query": {
|
||||||
|
"version": "0.5.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/openapi-react-query/-/openapi-react-query-0.5.1.tgz",
|
||||||
|
"integrity": "sha512-BzUDxICV9v8584pBiGM9jgCqSBBp/mP91SJ68Pu7PqeppqfWxazi3vVaMup5Acjxv3cN9GbV6xuVHfbGsJfexw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"openapi-typescript-helpers": "^0.0.15"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tanstack/react-query": "^5.80.0",
|
||||||
|
"openapi-fetch": "^0.15.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/openapi-typescript-helpers": {
|
||||||
|
"version": "0.0.15",
|
||||||
|
"resolved": "https://registry.npmjs.org/openapi-typescript-helpers/-/openapi-typescript-helpers-0.0.15.tgz",
|
||||||
|
"integrity": "sha512-opyTPaunsklCBpTK8JGef6mfPhLSnyy5a0IN9vKtx3+4aExf+KxEqYwIy3hqkedXIB97u357uLMJsOnm3GVjsw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/p-retry": {
|
"node_modules/p-retry": {
|
||||||
"version": "7.1.0",
|
"version": "7.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/p-retry/-/p-retry-7.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/p-retry/-/p-retry-7.1.0.tgz",
|
||||||
@@ -6837,6 +7157,25 @@
|
|||||||
"resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz",
|
||||||
"integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw=="
|
"integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw=="
|
||||||
},
|
},
|
||||||
|
"node_modules/parse-json": {
|
||||||
|
"version": "8.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/parse-json/-/parse-json-8.3.0.tgz",
|
||||||
|
"integrity": "sha512-ybiGyvspI+fAoRQbIPRddCcSTV9/LsJbf0e/S85VLowVGzRmokfneg2kwVW/KU5rOXrPSbF1qAKPMgNTqqROQQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"peer": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/code-frame": "^7.26.2",
|
||||||
|
"index-to-position": "^1.1.0",
|
||||||
|
"type-fest": "^4.39.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/path-key": {
|
"node_modules/path-key": {
|
||||||
"version": "3.1.1",
|
"version": "3.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
|
||||||
@@ -6927,6 +7266,17 @@
|
|||||||
"ce-la-react": "^0.3.0"
|
"ce-la-react": "^0.3.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/pluralize": {
|
||||||
|
"version": "8.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/pluralize/-/pluralize-8.0.0.tgz",
|
||||||
|
"integrity": "sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"peer": true,
|
||||||
|
"engines": {
|
||||||
|
"node": ">=4"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/postcss": {
|
"node_modules/postcss": {
|
||||||
"version": "8.5.6",
|
"version": "8.5.6",
|
||||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
|
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
|
||||||
@@ -7094,6 +7444,75 @@
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"node_modules/rclone-openapi": {
|
||||||
|
"version": "1.0.23",
|
||||||
|
"resolved": "https://registry.npmjs.org/rclone-openapi/-/rclone-openapi-1.0.23.tgz",
|
||||||
|
"integrity": "sha512-lO3ONGdjigjcuxCC2uGht7gmypQmBcgRiuZORDqTAuPwZEupQvGSyQ6Vt25z3LaeeN9ww2i62ajFnxP04NIE2w==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/rclone-sdk": {
|
||||||
|
"version": "1.72.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/rclone-sdk/-/rclone-sdk-1.72.0.tgz",
|
||||||
|
"integrity": "sha512-IQOzLJJHwn+mSoB8jzxLR3xmro13+A+DlNrdvPhXut1nsumxixC2Bm9povX7g6ig5TVfvLQNNNYVLiZ+22Veuw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"openapi-fetch": "^0.15.0",
|
||||||
|
"openapi-react-query": "^0.5.1",
|
||||||
|
"rclone-openapi": "^1.0.23",
|
||||||
|
"swr-openapi": "^5.4.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/rclone-sdk/node_modules/openapi-typescript": {
|
||||||
|
"version": "7.10.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/openapi-typescript/-/openapi-typescript-7.10.0.tgz",
|
||||||
|
"integrity": "sha512-vc+vAFU8plDiz7XYI7h/aBwW5NI+onJ2ydUd+Y51+h8O6vcMEp1+3MHIUB5cGclp08iHpCIotljhk51Nf+7+Yg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"peer": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@redocly/openapi-core": "^1.34.5",
|
||||||
|
"ansi-colors": "^4.1.3",
|
||||||
|
"change-case": "^5.4.4",
|
||||||
|
"parse-json": "^8.3.0",
|
||||||
|
"supports-color": "^10.2.2",
|
||||||
|
"yargs-parser": "^21.1.1"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"openapi-typescript": "bin/cli.js"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"typescript": "^5.x"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/rclone-sdk/node_modules/swr-openapi": {
|
||||||
|
"version": "5.4.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/swr-openapi/-/swr-openapi-5.4.2.tgz",
|
||||||
|
"integrity": "sha512-MciLRP1EK/SrC8BYEGkTc6B61imZwS7OfDG+8oFwtQ0jkwH+X+HXGzs6Q7xe/z8YebZ4WK8U7kwBjg7WKU1G5A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"openapi-typescript-helpers": "^0.0.15",
|
||||||
|
"type-fest": "^4.41.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "buymeacoffee",
|
||||||
|
"url": "https://buymeacoffee.com/htunnicliff"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"openapi-fetch": "0.15.0",
|
||||||
|
"openapi-typescript": "7.10.0",
|
||||||
|
"react": "18 || 19",
|
||||||
|
"swr": "2",
|
||||||
|
"typescript": "^5.x"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"openapi-typescript": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/react": {
|
"node_modules/react": {
|
||||||
"version": "18.3.1",
|
"version": "18.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
|
||||||
@@ -7106,15 +7525,6 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/react-compiler-runtime": {
|
|
||||||
"version": "19.1.0-rc.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/react-compiler-runtime/-/react-compiler-runtime-19.1.0-rc.3.tgz",
|
|
||||||
"integrity": "sha512-Cssogys2XZu6SqxRdX2xd8cQAf57BBvFbLEBlIa77161lninbKUn/EqbecCe7W3eqDQfg3rIoOwzExzgCh7h/g==",
|
|
||||||
"license": "MIT",
|
|
||||||
"peerDependencies": {
|
|
||||||
"react": "^17.0.0 || ^18.0.0 || ^19.0.0 || ^0.0.0-experimental"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/react-dom": {
|
"node_modules/react-dom": {
|
||||||
"version": "18.3.1",
|
"version": "18.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
|
||||||
@@ -7209,6 +7619,17 @@
|
|||||||
"node": ">=8.10.0"
|
"node": ">=8.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/require-from-string": {
|
||||||
|
"version": "2.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
|
||||||
|
"integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"peer": true,
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/resolve": {
|
"node_modules/resolve": {
|
||||||
"version": "1.22.10",
|
"version": "1.22.10",
|
||||||
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz",
|
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz",
|
||||||
@@ -7505,6 +7926,20 @@
|
|||||||
"node": ">=16 || 14 >=14.17"
|
"node": ">=16 || 14 >=14.17"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/supports-color": {
|
||||||
|
"version": "10.2.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-10.2.2.tgz",
|
||||||
|
"integrity": "sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"peer": true,
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/chalk/supports-color?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/supports-preserve-symlinks-flag": {
|
"node_modules/supports-preserve-symlinks-flag": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
|
||||||
@@ -7516,6 +7951,20 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/swr": {
|
||||||
|
"version": "2.3.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/swr/-/swr-2.3.6.tgz",
|
||||||
|
"integrity": "sha512-wfHRmHWk/isGNMwlLGlZX5Gzz/uTgo0o2IRuTMcf4CPuPFJZlq0rDaKUx+ozB5nBOReNV1kiOyzMfj+MBMikLw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
|
"dependencies": {
|
||||||
|
"dequal": "^2.0.3",
|
||||||
|
"use-sync-external-store": "^1.4.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/tailwind-merge": {
|
"node_modules/tailwind-merge": {
|
||||||
"version": "2.5.4",
|
"version": "2.5.4",
|
||||||
"resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-2.5.4.tgz",
|
"resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-2.5.4.tgz",
|
||||||
@@ -7716,11 +8165,22 @@
|
|||||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
||||||
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="
|
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="
|
||||||
},
|
},
|
||||||
|
"node_modules/type-fest": {
|
||||||
|
"version": "4.41.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz",
|
||||||
|
"integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==",
|
||||||
|
"license": "(MIT OR CC0-1.0)",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=16"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/typescript": {
|
"node_modules/typescript": {
|
||||||
"version": "5.9.3",
|
"version": "5.9.3",
|
||||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
||||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||||
"dev": true,
|
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"bin": {
|
"bin": {
|
||||||
"tsc": "bin/tsc",
|
"tsc": "bin/tsc",
|
||||||
@@ -7767,6 +8227,14 @@
|
|||||||
"browserslist": ">= 4.21.0"
|
"browserslist": ">= 4.21.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/uri-js-replace": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/uri-js-replace/-/uri-js-replace-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-W+C9NWNLFOoBI2QWDp4UT9pv65r2w5Cx+3sTYFvtMdDBxkKt1syCqsUdSFAChbEe1uK5TfS04wt/nGwmaeIQ0g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"peer": true
|
||||||
|
},
|
||||||
"node_modules/use-broadcast-ts": {
|
"node_modules/use-broadcast-ts": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/use-broadcast-ts/-/use-broadcast-ts-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/use-broadcast-ts/-/use-broadcast-ts-2.0.1.tgz",
|
||||||
@@ -8068,6 +8536,25 @@
|
|||||||
"node": ">= 14.6"
|
"node": ">= 14.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/yaml-ast-parser": {
|
||||||
|
"version": "0.0.43",
|
||||||
|
"resolved": "https://registry.npmjs.org/yaml-ast-parser/-/yaml-ast-parser-0.0.43.tgz",
|
||||||
|
"integrity": "sha512-2PTINUwsRqSd+s8XxKaJWQlUuEMHJQyEuh2edBbW8KNJz0SJPwUSD2zRWqezFEdN7IzAgeuYHFUCF7o8zRdZ0A==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"optional": true,
|
||||||
|
"peer": true
|
||||||
|
},
|
||||||
|
"node_modules/yargs-parser": {
|
||||||
|
"version": "21.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz",
|
||||||
|
"integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==",
|
||||||
|
"license": "ISC",
|
||||||
|
"optional": true,
|
||||||
|
"peer": true,
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/zustand": {
|
"node_modules/zustand": {
|
||||||
"version": "5.0.8",
|
"version": "5.0.8",
|
||||||
"resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.8.tgz",
|
"resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.8.tgz",
|
||||||
|
|||||||
@@ -1,8 +1,19 @@
|
|||||||
{
|
{
|
||||||
"name": "rclone-ui",
|
"name": "rclone-ui",
|
||||||
"private": true,
|
"version": "3.0.0",
|
||||||
"version": "2.8.1",
|
"description": "The GUI for rclone & s3",
|
||||||
|
"author": {
|
||||||
|
"name": "Rclone UI",
|
||||||
|
"url": "https://github.com/rclone-ui/rclone-ui"
|
||||||
|
},
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/rclone-ui/rclone-ui"
|
||||||
|
},
|
||||||
|
"homepage": "https://rcloneui.com",
|
||||||
|
"keywords": ["rclone", "gui", "drive", "s3", "cloud", "storage", "sync", "backup"],
|
||||||
|
"license": "Apache-2.0",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "node scripts/buildExternal.js && vite",
|
"dev": "node scripts/buildExternal.js && vite",
|
||||||
"build": "node scripts/buildExternal.js && tsc && vite build",
|
"build": "node scripts/buildExternal.js && tsc && vite build",
|
||||||
@@ -17,16 +28,21 @@
|
|||||||
"build:windows:arm": "tauri build --runner cargo-xwin --target aarch64-pc-windows-msvc",
|
"build:windows:arm": "tauri build --runner cargo-xwin --target aarch64-pc-windows-msvc",
|
||||||
"build:linux:arm": "tauri build --target aarch64-unknown-linux-gnu",
|
"build:linux:arm": "tauri build --target aarch64-unknown-linux-gnu",
|
||||||
"build:wasm": "tauri build --target wasm32-unknown-unknown",
|
"build:wasm": "tauri build --target wasm32-unknown-unknown",
|
||||||
"sources:cargo": "node scripts/flatpak-cargo-generator.js"
|
"sources:cargo": "node scripts/flatpak-cargo-generator.js",
|
||||||
|
"pub": "npm publish ./cli"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@formkit/auto-animate": "^0.9.0",
|
"@formkit/auto-animate": "^0.9.0",
|
||||||
"@heroui/react": "^2.7.11",
|
"@heroui/react": "^2.7.11",
|
||||||
"@mux/mux-player-react": "^3.8.0",
|
"@mux/mux-player-react": "^3.8.0",
|
||||||
"@sentry/browser": "^10.8.0",
|
"@sentry/browser": "^10.8.0",
|
||||||
|
"@tanstack/query-persist-client-core": "^5.91.6",
|
||||||
|
"@tanstack/query-sync-storage-persister": "^5.90.9",
|
||||||
|
"@tanstack/react-query": "^5.90.7",
|
||||||
"@tauri-apps/api": "~2.9.0",
|
"@tauri-apps/api": "~2.9.0",
|
||||||
"@tauri-apps/plugin-autostart": "^2.5.1",
|
"@tauri-apps/plugin-autostart": "^2.5.1",
|
||||||
"@tauri-apps/plugin-clipboard-manager": "^2.3.2",
|
"@tauri-apps/plugin-clipboard-manager": "^2.3.2",
|
||||||
|
"@tauri-apps/plugin-deep-link": "^2.4.5",
|
||||||
"@tauri-apps/plugin-dialog": "^2.4.2",
|
"@tauri-apps/plugin-dialog": "^2.4.2",
|
||||||
"@tauri-apps/plugin-fs": "^2.4.4",
|
"@tauri-apps/plugin-fs": "^2.4.4",
|
||||||
"@tauri-apps/plugin-http": "^2.5.4",
|
"@tauri-apps/plugin-http": "^2.5.4",
|
||||||
@@ -39,14 +55,16 @@
|
|||||||
"@tauri-apps/plugin-store": "^2.4.1",
|
"@tauri-apps/plugin-store": "^2.4.1",
|
||||||
"@tauri-apps/plugin-updater": "^2.9.0",
|
"@tauri-apps/plugin-updater": "^2.9.0",
|
||||||
"@tauri-apps/plugin-window-state": "^2.4.1",
|
"@tauri-apps/plugin-window-state": "^2.4.1",
|
||||||
|
"canvas-confetti": "^1.9.4",
|
||||||
"cron-parser": "^5.4.0",
|
"cron-parser": "^5.4.0",
|
||||||
"cronstrue": "^3.9.0",
|
"cronstrue": "^3.9.0",
|
||||||
"date-fns": "^4.1.0",
|
"date-fns": "^4.1.0",
|
||||||
"framer-motion": "^11.18.2",
|
"framer-motion": "^11.18.2",
|
||||||
|
"fuse.js": "^7.1.0",
|
||||||
"lucide-react": "^0.552.0",
|
"lucide-react": "^0.552.0",
|
||||||
"p-retry": "^7.1.0",
|
"p-retry": "^7.1.0",
|
||||||
|
"rclone-sdk": "^1.72.0",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-compiler-runtime": "^19.1.0-rc.3",
|
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
"react-router-dom": "^6.28.1",
|
"react-router-dom": "^6.28.1",
|
||||||
"tauri-plugin-sentry-api": "^0.5.0",
|
"tauri-plugin-sentry-api": "^0.5.0",
|
||||||
@@ -58,6 +76,8 @@
|
|||||||
"@biomejs/biome": "^1.9.4",
|
"@biomejs/biome": "^1.9.4",
|
||||||
"@iarna/toml": "^2.2.5",
|
"@iarna/toml": "^2.2.5",
|
||||||
"@tauri-apps/cli": "^2.9.2",
|
"@tauri-apps/cli": "^2.9.2",
|
||||||
|
"@total-typescript/ts-reset": "^0.6.1",
|
||||||
|
"@types/canvas-confetti": "^1.9.0",
|
||||||
"@types/node": "^22.10.7",
|
"@types/node": "^22.10.7",
|
||||||
"@types/react": "^18.3.1",
|
"@types/react": "^18.3.1",
|
||||||
"@types/react-dom": "^18.3.1",
|
"@types/react-dom": "^18.3.1",
|
||||||
|
|||||||
@@ -295,7 +295,7 @@ It's not pretty, but at the same time Adobe's .PSD implementation exists.
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fetchAndInjectContent(url, pass, isInitialLoad = false) {
|
async function fetchAndInjectContent(url, auth, isInitialLoad = false) {
|
||||||
try {
|
try {
|
||||||
// Only show navigation spinner for non-initial loads
|
// Only show navigation spinner for non-initial loads
|
||||||
if (!isInitialLoad) {
|
if (!isInitialLoad) {
|
||||||
@@ -303,17 +303,22 @@ It's not pretty, but at the same time Adobe's .PSD implementation exists.
|
|||||||
}
|
}
|
||||||
|
|
||||||
let newUrl = url
|
let newUrl = url
|
||||||
if (pass) {
|
|
||||||
newUrl = url.replace('localhost:5572', 'admin:' + pass + '@localhost:5572')
|
|
||||||
} else {
|
|
||||||
console.log('no pass')
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('newUrl', newUrl)
|
console.log('newUrl', newUrl)
|
||||||
|
|
||||||
const fetch = window.__TAURI__.http.fetch
|
const fetch = window.__TAURI__.http.fetch
|
||||||
// Fetch the webpage content
|
// Fetch the webpage content
|
||||||
const response = await fetch(newUrl);
|
const options = {
|
||||||
|
method: 'GET',
|
||||||
|
};
|
||||||
|
|
||||||
|
if (auth) {
|
||||||
|
options.headers = {
|
||||||
|
'Authorization': 'Basic ' + auth
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await fetch(newUrl, options);
|
||||||
const html = await response.text();
|
const html = await response.text();
|
||||||
|
|
||||||
console.log('fetched html for', newUrl)
|
console.log('fetched html for', newUrl)
|
||||||
@@ -373,7 +378,11 @@ It's not pretty, but at the same time Adobe's .PSD implementation exists.
|
|||||||
document.body.appendChild(navSpinner);
|
document.body.appendChild(navSpinner);
|
||||||
|
|
||||||
// Update the URL in the address bar without reloading
|
// Update the URL in the address bar without reloading
|
||||||
window.history.pushState({}, '', `?url=${encodeURIComponent(url)}` + (pass ? `&pass=${encodeURIComponent(pass)}` : ''));
|
const params = new URLSearchParams();
|
||||||
|
params.set('url', url);
|
||||||
|
if (auth) params.set('auth', auth);
|
||||||
|
|
||||||
|
window.history.pushState({}, '', `?${params.toString()}`);
|
||||||
|
|
||||||
// Re-add our event handlers
|
// Re-add our event handlers
|
||||||
setupEventHandlers();
|
setupEventHandlers();
|
||||||
@@ -413,7 +422,10 @@ It's not pretty, but at the same time Adobe's .PSD implementation exists.
|
|||||||
currentUrl = currentUrl.split('/');
|
currentUrl = currentUrl.split('/');
|
||||||
currentUrl.pop();
|
currentUrl.pop();
|
||||||
const fullUrl = currentUrl.join('/') + '/';
|
const fullUrl = currentUrl.join('/') + '/';
|
||||||
fetchAndInjectContent(fullUrl);
|
|
||||||
|
const urlParams = new URLSearchParams(window.location.search);
|
||||||
|
const auth = urlParams.get('auth');
|
||||||
|
fetchAndInjectContent(fullUrl, auth);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -457,11 +469,22 @@ It's not pretty, but at the same time Adobe's .PSD implementation exists.
|
|||||||
toggleNavSpinner(true);
|
toggleNavSpinner(true);
|
||||||
|
|
||||||
console.log('downloading', downloadUrl);
|
console.log('downloading', downloadUrl);
|
||||||
// Fetch the file contents
|
|
||||||
const response = await http.fetch(downloadUrl, {
|
const options = {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
responseType: 2 // ResponseType.Binary
|
responseType: 2 // ResponseType.Binary
|
||||||
});
|
};
|
||||||
|
|
||||||
|
const urlParams = new URLSearchParams(window.location.search);
|
||||||
|
const auth = urlParams.get('auth');
|
||||||
|
if (auth) {
|
||||||
|
options.headers = {
|
||||||
|
'Authorization': 'Basic ' + auth
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch the file contents
|
||||||
|
const response = await http.fetch(downloadUrl, options);
|
||||||
|
|
||||||
const data = await response.arrayBuffer();
|
const data = await response.arrayBuffer();
|
||||||
|
|
||||||
@@ -531,7 +554,9 @@ It's not pretty, but at the same time Adobe's .PSD implementation exists.
|
|||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
||||||
// Fetch and inject the new content
|
// Fetch and inject the new content
|
||||||
fetchAndInjectContent(fullUrl);
|
const urlParams = new URLSearchParams(window.location.search);
|
||||||
|
const auth = urlParams.get('auth');
|
||||||
|
fetchAndInjectContent(fullUrl, auth);
|
||||||
}
|
}
|
||||||
|
|
||||||
function isInViewport(element) {
|
function isInViewport(element) {
|
||||||
@@ -557,10 +582,10 @@ It's not pretty, but at the same time Adobe's .PSD implementation exists.
|
|||||||
// Initial setup
|
// Initial setup
|
||||||
const urlParams = new URLSearchParams(window.location.search);
|
const urlParams = new URLSearchParams(window.location.search);
|
||||||
const url = urlParams.get('url');
|
const url = urlParams.get('url');
|
||||||
const pass = urlParams.get('pass');
|
const auth = urlParams.get('auth');
|
||||||
if (url) {
|
if (url) {
|
||||||
// Pass true to indicate this is the initial load
|
// Pass true to indicate this is the initial load
|
||||||
fetchAndInjectContent(url, pass, true);
|
fetchAndInjectContent(url, auth, true);
|
||||||
} else {
|
} else {
|
||||||
document.body.innerHTML = '<div>Error: No URL provided.</div>';
|
document.body.innerHTML = '<div>Error: No URL provided.</div>';
|
||||||
}
|
}
|
||||||
@@ -570,9 +595,11 @@ It's not pretty, but at the same time Adobe's .PSD implementation exists.
|
|||||||
|
|
||||||
// Handle browser back/forward buttons
|
// Handle browser back/forward buttons
|
||||||
window.addEventListener('popstate', () => {
|
window.addEventListener('popstate', () => {
|
||||||
const newUrl = new URLSearchParams(window.location.search).get('url');
|
const params = new URLSearchParams(window.location.search);
|
||||||
|
const newUrl = params.get('url');
|
||||||
|
const auth = params.get('auth');
|
||||||
if (newUrl) {
|
if (newUrl) {
|
||||||
fetchAndInjectContent(newUrl, pass);
|
fetchAndInjectContent(newUrl, auth);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 63 KiB |
|
After Width: | Height: | Size: 32 KiB |
|
After Width: | Height: | Size: 955 KiB |
|
After Width: | Height: | Size: 124 KiB |
|
After Width: | Height: | Size: 35 KiB |
|
After Width: | Height: | Size: 89 KiB |
|
After Width: | Height: | Size: 89 KiB |
|
After Width: | Height: | Size: 836 KiB |
|
After Width: | Height: | Size: 32 KiB |
|
After Width: | Height: | Size: 31 KiB |
|
After Width: | Height: | Size: 26 KiB |
|
After Width: | Height: | Size: 29 KiB |
|
After Width: | Height: | Size: 9.5 KiB |
|
After Width: | Height: | Size: 11 KiB |
@@ -0,0 +1 @@
|
|||||||
|
import '@total-typescript/ts-reset'
|
||||||
@@ -47,6 +47,14 @@ async function buildExternal() {
|
|||||||
})
|
})
|
||||||
await Promise.all(jsFiles.map((file) => unlink(file).catch(() => {})))
|
await Promise.all(jsFiles.map((file) => unlink(file).catch(() => {})))
|
||||||
})(),
|
})(),
|
||||||
|
// Remove store/*.js files - preserve .ts files
|
||||||
|
(async () => {
|
||||||
|
const jsFiles = await glob('**/*.js', {
|
||||||
|
cwd: join(projectRoot, 'store'),
|
||||||
|
absolute: true,
|
||||||
|
})
|
||||||
|
await Promise.all(jsFiles.map((file) => unlink(file).catch(() => {})))
|
||||||
|
})(),
|
||||||
// Remove main.js
|
// Remove main.js
|
||||||
unlink(join(projectRoot, 'main.js')).catch(() => {}), // Ignore if file doesn't exist
|
unlink(join(projectRoot, 'main.js')).catch(() => {}), // Ignore if file doesn't exist
|
||||||
])
|
])
|
||||||
|
|||||||
@@ -0,0 +1,204 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
import { readFileSync } from 'node:fs'
|
||||||
|
import { resolve } from 'node:path'
|
||||||
|
import process from 'node:process'
|
||||||
|
|
||||||
|
function printUsage() {
|
||||||
|
console.log(`Usage: node scripts/rc-request.mjs [options] <path>
|
||||||
|
|
||||||
|
Options:
|
||||||
|
-X, --method <method> HTTP method to use (default: POST)
|
||||||
|
-b, --base <url> Base URL (default: http://localhost:5572)
|
||||||
|
-q, --query <key=value> Query parameter to append (repeatable)
|
||||||
|
-d, --data <json> JSON payload string
|
||||||
|
--data-file <file> Path to file containing JSON payload
|
||||||
|
-h, --help Show this help message
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
node scripts/rc-request.mjs /rc/noop
|
||||||
|
node scripts/rc-request.mjs -q _async=true /job/list
|
||||||
|
node scripts/rc-request.mjs -d '{"fs":"rct:","remote":""}' /operations/list`)
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseArgs(argv) {
|
||||||
|
const args = {
|
||||||
|
baseUrl: process.env.RCLONE_RC_BASE_URL || 'http://localhost:5572',
|
||||||
|
method: 'POST',
|
||||||
|
path: null,
|
||||||
|
query: [],
|
||||||
|
data: null,
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 0; i < argv.length; i += 1) {
|
||||||
|
const value = argv[i]
|
||||||
|
switch (value) {
|
||||||
|
case '-h':
|
||||||
|
case '--help': {
|
||||||
|
printUsage()
|
||||||
|
process.exit(0)
|
||||||
|
}
|
||||||
|
case '-X':
|
||||||
|
case '--method': {
|
||||||
|
const next = argv[++i]
|
||||||
|
if (!next) {
|
||||||
|
throw new Error('Expected HTTP method after --method')
|
||||||
|
}
|
||||||
|
args.method = next.toUpperCase()
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case '-b':
|
||||||
|
case '--base': {
|
||||||
|
const next = argv[++i]
|
||||||
|
if (!next) {
|
||||||
|
throw new Error('Expected URL after --base')
|
||||||
|
}
|
||||||
|
args.baseUrl = next
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case '-q':
|
||||||
|
case '--query': {
|
||||||
|
const next = argv[++i]
|
||||||
|
if (!next) {
|
||||||
|
throw new Error('Expected key=value after --query')
|
||||||
|
}
|
||||||
|
args.query.push(next)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case '-d':
|
||||||
|
case '--data': {
|
||||||
|
const next = argv[++i]
|
||||||
|
if (next === undefined) {
|
||||||
|
throw new Error('Expected JSON string after --data')
|
||||||
|
}
|
||||||
|
args.data = next
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case '--data-file': {
|
||||||
|
const next = argv[++i]
|
||||||
|
if (!next) {
|
||||||
|
throw new Error('Expected file path after --data-file')
|
||||||
|
}
|
||||||
|
args.data = readFileSync(resolve(next), 'utf8')
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
if (args.path) {
|
||||||
|
throw new Error(`Unexpected argument: ${value}`)
|
||||||
|
}
|
||||||
|
args.path = value
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!args.path) {
|
||||||
|
throw new Error('Missing path argument (e.g. /rc/noop)')
|
||||||
|
}
|
||||||
|
|
||||||
|
return args
|
||||||
|
}
|
||||||
|
|
||||||
|
function ensurePath(path) {
|
||||||
|
if (!path.startsWith('/')) {
|
||||||
|
return `/${path}`
|
||||||
|
}
|
||||||
|
return path
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildUrl(baseUrl, path, queryEntries) {
|
||||||
|
const normalizedBase = baseUrl.endsWith('/') ? baseUrl : `${baseUrl}/`
|
||||||
|
const url = new URL(ensurePath(path), normalizedBase)
|
||||||
|
|
||||||
|
for (const entry of queryEntries) {
|
||||||
|
const [key, ...valueParts] = entry.split('=')
|
||||||
|
if (!key) {
|
||||||
|
throw new Error(`Invalid query entry "${entry}", expected key=value`)
|
||||||
|
}
|
||||||
|
const value = valueParts.length > 0 ? valueParts.join('=') : ''
|
||||||
|
url.searchParams.append(key, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
return url
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseJsonMaybe(input) {
|
||||||
|
if (input === null || input === undefined) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
const trimmed = input.trim()
|
||||||
|
if (!trimmed) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return JSON.parse(trimmed)
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(`Failed to parse JSON payload: ${error?.message || error}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
let args
|
||||||
|
try {
|
||||||
|
args = parseArgs(process.argv.slice(2))
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`[rc-request] ${error.message}`)
|
||||||
|
printUsage()
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
const url = buildUrl(args.baseUrl, args.path, args.query)
|
||||||
|
const payload = parseJsonMaybe(args.data)
|
||||||
|
const headers = new Headers()
|
||||||
|
|
||||||
|
const requestInit = {
|
||||||
|
method: args.method,
|
||||||
|
headers,
|
||||||
|
}
|
||||||
|
|
||||||
|
if (payload !== null) {
|
||||||
|
headers.set('Content-Type', 'application/json')
|
||||||
|
requestInit.body = JSON.stringify(payload)
|
||||||
|
}
|
||||||
|
|
||||||
|
const startedAt = Date.now()
|
||||||
|
let response
|
||||||
|
try {
|
||||||
|
response = await fetch(url, requestInit)
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`[rc-request] Request failed: ${error?.message || error}`)
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
const durationMs = Date.now() - startedAt
|
||||||
|
const text = await response.text()
|
||||||
|
|
||||||
|
let parsedBody = null
|
||||||
|
if (text) {
|
||||||
|
try {
|
||||||
|
parsedBody = JSON.parse(text)
|
||||||
|
} catch {
|
||||||
|
parsedBody = text
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = {
|
||||||
|
url: url.toString(),
|
||||||
|
method: args.method,
|
||||||
|
status: response.status,
|
||||||
|
statusText: response.statusText,
|
||||||
|
durationMs,
|
||||||
|
headers: Object.fromEntries(response.headers.entries()),
|
||||||
|
body: parsedBody,
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(JSON.stringify(result, null, 2))
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
process.exitCode = 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch((error) => {
|
||||||
|
console.error(`[rc-request] Unexpected error: ${error?.message || error}`)
|
||||||
|
process.exit(1)
|
||||||
|
})
|
||||||
@@ -249,12 +249,13 @@ checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "app"
|
name = "app"
|
||||||
version = "2.8.1"
|
version = "3.0.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cocoa",
|
"cocoa",
|
||||||
"fix-path-env",
|
"fix-path-env",
|
||||||
"log",
|
"log",
|
||||||
"machine-uid",
|
"machine-uid",
|
||||||
|
"objc",
|
||||||
"reqwest 0.11.27",
|
"reqwest 0.11.27",
|
||||||
"sentry",
|
"sentry",
|
||||||
"serde",
|
"serde",
|
||||||
@@ -264,8 +265,10 @@ dependencies = [
|
|||||||
"tauri-build",
|
"tauri-build",
|
||||||
"tauri-plugin-autostart",
|
"tauri-plugin-autostart",
|
||||||
"tauri-plugin-clipboard-manager",
|
"tauri-plugin-clipboard-manager",
|
||||||
|
"tauri-plugin-deep-link",
|
||||||
"tauri-plugin-dialog",
|
"tauri-plugin-dialog",
|
||||||
"tauri-plugin-fs",
|
"tauri-plugin-fs",
|
||||||
|
"tauri-plugin-global-shortcut",
|
||||||
"tauri-plugin-http",
|
"tauri-plugin-http",
|
||||||
"tauri-plugin-log",
|
"tauri-plugin-log",
|
||||||
"tauri-plugin-notification",
|
"tauri-plugin-notification",
|
||||||
@@ -278,6 +281,7 @@ dependencies = [
|
|||||||
"tauri-plugin-single-instance",
|
"tauri-plugin-single-instance",
|
||||||
"tauri-plugin-store",
|
"tauri-plugin-store",
|
||||||
"tauri-plugin-updater",
|
"tauri-plugin-updater",
|
||||||
|
"tinyfiledialogs",
|
||||||
"x11rb",
|
"x11rb",
|
||||||
"zbus",
|
"zbus",
|
||||||
"zip 0.6.6",
|
"zip 0.6.6",
|
||||||
@@ -433,7 +437,7 @@ checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.108",
|
"syn 2.0.110",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -468,7 +472,7 @@ checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.108",
|
"syn 2.0.110",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -643,7 +647,7 @@ dependencies = [
|
|||||||
"proc-macro-crate 3.4.0",
|
"proc-macro-crate 3.4.0",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.108",
|
"syn 2.0.110",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -831,9 +835,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cc"
|
name = "cc"
|
||||||
version = "1.2.44"
|
version = "1.2.45"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "37521ac7aabe3d13122dc382493e20c9416f299d2ccd5b3a5340a2570cdeb0f3"
|
checksum = "35900b6c8d709fb1d854671ae27aeaa9eec2f8b01b364e1619a40da3e6fe2afe"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"find-msvc-tools",
|
"find-msvc-tools",
|
||||||
"jobserver",
|
"jobserver",
|
||||||
@@ -965,6 +969,26 @@ dependencies = [
|
|||||||
"crossbeam-utils",
|
"crossbeam-utils",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "const-random"
|
||||||
|
version = "0.1.18"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "87e00182fe74b066627d63b85fd550ac2998d4b0bd86bfed477a0ae4c7c71359"
|
||||||
|
dependencies = [
|
||||||
|
"const-random-macro",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "const-random-macro"
|
||||||
|
version = "0.1.16"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e"
|
||||||
|
dependencies = [
|
||||||
|
"getrandom 0.2.16",
|
||||||
|
"once_cell",
|
||||||
|
"tiny-keccak",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "constant_time_eq"
|
name = "constant_time_eq"
|
||||||
version = "0.1.5"
|
version = "0.1.5"
|
||||||
@@ -1153,7 +1177,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331"
|
checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.108",
|
"syn 2.0.110",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1163,7 +1187,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "32a2785755761f3ddc1492979ce1e48d2c00d09311c39e4466429188f3dd6501"
|
checksum = "32a2785755761f3ddc1492979ce1e48d2c00d09311c39e4466429188f3dd6501"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.108",
|
"syn 2.0.110",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1187,7 +1211,7 @@ dependencies = [
|
|||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"strsim",
|
"strsim",
|
||||||
"syn 2.0.108",
|
"syn 2.0.110",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1198,7 +1222,7 @@ checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"darling_core",
|
"darling_core",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.108",
|
"syn 2.0.110",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1245,7 +1269,7 @@ checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.108",
|
"syn 2.0.110",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1258,7 +1282,7 @@ dependencies = [
|
|||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"rustc_version",
|
"rustc_version",
|
||||||
"syn 2.0.108",
|
"syn 2.0.110",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1278,7 +1302,7 @@ checksum = "bda628edc44c4bb645fbe0f758797143e4e07926f7ebf4e9bdfbd3d2ce621df3"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.108",
|
"syn 2.0.110",
|
||||||
"unicode-xid",
|
"unicode-xid",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -1360,7 +1384,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.108",
|
"syn 2.0.110",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1392,7 +1416,16 @@ checksum = "788160fb30de9cdd857af31c6a2675904b16ece8fc2737b2c7127ba368c9d0f4"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.108",
|
"syn 2.0.110",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "dlv-list"
|
||||||
|
version = "0.5.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "442039f5147480ba31067cb00ada1adae6892028e40e45fc5de7b7df6dcc1b5f"
|
||||||
|
dependencies = [
|
||||||
|
"const-random",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1505,7 +1538,7 @@ checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.108",
|
"syn 2.0.110",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1526,9 +1559,9 @@ checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "erased-serde"
|
name = "erased-serde"
|
||||||
version = "0.4.8"
|
version = "0.4.9"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "259d404d09818dec19332e31d94558aeb442fea04c817006456c24b5460bbd4b"
|
checksum = "89e8918065695684b2b0702da20382d5ae6065cf3327bc2d6436bd49a71ce9f3"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"serde",
|
"serde",
|
||||||
"serde_core",
|
"serde_core",
|
||||||
@@ -1595,7 +1628,7 @@ checksum = "a0aca10fb742cb43f9e7bb8467c91aa9bcb8e3ffbc6a6f7389bb93ffc920577d"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.108",
|
"syn 2.0.110",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1721,7 +1754,7 @@ checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.108",
|
"syn 2.0.110",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1815,7 +1848,7 @@ checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.108",
|
"syn 2.0.110",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -2085,7 +2118,7 @@ dependencies = [
|
|||||||
"proc-macro-error",
|
"proc-macro-error",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.108",
|
"syn 2.0.110",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -2104,6 +2137,24 @@ version = "0.3.3"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280"
|
checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "global-hotkey"
|
||||||
|
version = "0.7.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b9247516746aa8e53411a0db9b62b0e24efbcf6a76e0ba73e5a91b512ddabed7"
|
||||||
|
dependencies = [
|
||||||
|
"crossbeam-channel",
|
||||||
|
"keyboard-types",
|
||||||
|
"objc2 0.6.3",
|
||||||
|
"objc2-app-kit",
|
||||||
|
"once_cell",
|
||||||
|
"serde",
|
||||||
|
"thiserror 2.0.17",
|
||||||
|
"windows-sys 0.59.0",
|
||||||
|
"x11rb",
|
||||||
|
"xkeysym",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "gobject-sys"
|
name = "gobject-sys"
|
||||||
version = "0.18.0"
|
version = "0.18.0"
|
||||||
@@ -2175,7 +2226,7 @@ dependencies = [
|
|||||||
"proc-macro-error",
|
"proc-macro-error",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.108",
|
"syn 2.0.110",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -2236,6 +2287,12 @@ dependencies = [
|
|||||||
"ahash",
|
"ahash",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hashbrown"
|
||||||
|
version = "0.14.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hashbrown"
|
name = "hashbrown"
|
||||||
version = "0.16.0"
|
version = "0.16.0"
|
||||||
@@ -2704,9 +2761,9 @@ checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "iri-string"
|
name = "iri-string"
|
||||||
version = "0.7.8"
|
version = "0.7.9"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "dbc5ebe9c3a1a7a5127f920a418f7585e9e758e911d0466ed004f393b0e380b2"
|
checksum = "4f867b9d1d896b67beb18518eda36fdb77a32ea590de864f1325b294a6d14397"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"memchr",
|
"memchr",
|
||||||
"serde",
|
"serde",
|
||||||
@@ -3074,7 +3131,7 @@ checksum = "88a9689d8d44bf9964484516275f5cd4c9b59457a6940c1d5d0ecbb94510a36b"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.108",
|
"syn 2.0.110",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -3378,7 +3435,7 @@ checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.108",
|
"syn 2.0.110",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -3409,7 +3466,7 @@ dependencies = [
|
|||||||
"proc-macro-crate 3.4.0",
|
"proc-macro-crate 3.4.0",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.108",
|
"syn 2.0.110",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -3745,9 +3802,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "openssl"
|
name = "openssl"
|
||||||
version = "0.10.74"
|
version = "0.10.75"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "24ad14dd45412269e1a30f52ad8f0664f0f4f4a89ee8fe28c3b3527021ebb654"
|
checksum = "08838db121398ad17ab8531ce9de97b244589089e290a384c900cb9ff7434328"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 2.10.0",
|
"bitflags 2.10.0",
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
@@ -3766,7 +3823,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.108",
|
"syn 2.0.110",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -3777,9 +3834,9 @@ checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "openssl-sys"
|
name = "openssl-sys"
|
||||||
version = "0.9.110"
|
version = "0.9.111"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0a9f0075ba3c21b09f8e8b2026584b1d18d49388648f2fbbf3c97ea8deced8e2"
|
checksum = "82cab2d520aa75e3c58898289429321eb788c3106963d0dc886ec7a5f4adc321"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cc",
|
"cc",
|
||||||
"libc",
|
"libc",
|
||||||
@@ -3793,6 +3850,16 @@ version = "0.2.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
|
checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ordered-multimap"
|
||||||
|
version = "0.7.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "49203cdcae0030493bad186b28da2fa25645fa276a51b6fec8010d281e02ef79"
|
||||||
|
dependencies = [
|
||||||
|
"dlv-list",
|
||||||
|
"hashbrown 0.14.5",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ordered-stream"
|
name = "ordered-stream"
|
||||||
version = "0.2.0"
|
version = "0.2.0"
|
||||||
@@ -3967,7 +4034,7 @@ dependencies = [
|
|||||||
"pest_meta",
|
"pest_meta",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.108",
|
"syn 2.0.110",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -4094,7 +4161,7 @@ dependencies = [
|
|||||||
"phf_shared 0.11.3",
|
"phf_shared 0.11.3",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.108",
|
"syn 2.0.110",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -4446,9 +4513,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "quote"
|
name = "quote"
|
||||||
version = "1.0.41"
|
version = "1.0.42"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ce25767e7b499d1b604768e7cde645d14cc8584231ea6b295e9c9eb22c02e1d1"
|
checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
]
|
]
|
||||||
@@ -4638,7 +4705,7 @@ checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.108",
|
"syn 2.0.110",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -4709,7 +4776,7 @@ dependencies = [
|
|||||||
"once_cell",
|
"once_cell",
|
||||||
"percent-encoding",
|
"percent-encoding",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
"rustls-pemfile 1.0.4",
|
"rustls-pemfile",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"serde_urlencoded",
|
"serde_urlencoded",
|
||||||
@@ -4843,6 +4910,16 @@ dependencies = [
|
|||||||
"syn 1.0.109",
|
"syn 1.0.109",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rust-ini"
|
||||||
|
version = "0.21.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "796e8d2b6696392a43bea58116b667fb4c29727dc5abd27d6acf338bb4f688c7"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"ordered-multimap",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rust_decimal"
|
name = "rust_decimal"
|
||||||
version = "1.39.0"
|
version = "1.39.0"
|
||||||
@@ -4908,9 +4985,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustls"
|
name = "rustls"
|
||||||
version = "0.23.34"
|
version = "0.23.35"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6a9586e9ee2b4f8fab52a0048ca7334d7024eef48e2cb9407e3497bb7cab7fa7"
|
checksum = "533f54bc6a7d4f647e46ad909549eda97bf5afc1585190ef692b4286b198bd8f"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"ring",
|
"ring",
|
||||||
@@ -4929,15 +5006,6 @@ dependencies = [
|
|||||||
"base64 0.21.7",
|
"base64 0.21.7",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "rustls-pemfile"
|
|
||||||
version = "2.2.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50"
|
|
||||||
dependencies = [
|
|
||||||
"rustls-pki-types",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustls-pki-types"
|
name = "rustls-pki-types"
|
||||||
version = "1.13.0"
|
version = "1.13.0"
|
||||||
@@ -5018,9 +5086,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "schemars"
|
name = "schemars"
|
||||||
version = "1.0.5"
|
version = "1.1.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1317c3bf3e7df961da95b0a56a172a02abead31276215a0497241a7624b487ce"
|
checksum = "9558e172d4e8533736ba97870c4b2cd63f84b382a3d6eb063da41b91cce17289"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"dyn-clone",
|
"dyn-clone",
|
||||||
"ref-cast",
|
"ref-cast",
|
||||||
@@ -5037,7 +5105,7 @@ dependencies = [
|
|||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"serde_derive_internals",
|
"serde_derive_internals",
|
||||||
"syn 2.0.108",
|
"syn 2.0.110",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -5069,7 +5137,7 @@ checksum = "1783eabc414609e28a5ba76aee5ddd52199f7107a0b24c2e9746a1ecc34a683d"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.108",
|
"syn 2.0.110",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -5300,7 +5368,7 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.108",
|
"syn 2.0.110",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -5311,7 +5379,7 @@ checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.108",
|
"syn 2.0.110",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -5335,7 +5403,7 @@ checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.108",
|
"syn 2.0.110",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -5380,7 +5448,7 @@ dependencies = [
|
|||||||
"indexmap 1.9.3",
|
"indexmap 1.9.3",
|
||||||
"indexmap 2.12.0",
|
"indexmap 2.12.0",
|
||||||
"schemars 0.9.0",
|
"schemars 0.9.0",
|
||||||
"schemars 1.0.5",
|
"schemars 1.1.0",
|
||||||
"serde_core",
|
"serde_core",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"serde_with_macros",
|
"serde_with_macros",
|
||||||
@@ -5396,7 +5464,7 @@ dependencies = [
|
|||||||
"darling",
|
"darling",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.108",
|
"syn 2.0.110",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -5418,7 +5486,7 @@ checksum = "772ee033c0916d670af7860b6e1ef7d658a4629a6d0b4c8c3e67f09b3765b75d"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.108",
|
"syn 2.0.110",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -5544,7 +5612,7 @@ checksum = "0eb01866308440fc64d6c44d9e86c5cc17adfe33c4d6eed55da9145044d0ffc1"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.108",
|
"syn 2.0.110",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -5685,7 +5753,7 @@ dependencies = [
|
|||||||
"heck 0.5.0",
|
"heck 0.5.0",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.108",
|
"syn 2.0.110",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -5718,9 +5786,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "syn"
|
name = "syn"
|
||||||
version = "2.0.108"
|
version = "2.0.110"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "da58917d35242480a05c2897064da0a80589a2a0476c9a3f2fdc83b53502e917"
|
checksum = "a99801b5bd34ede4cf3fc688c5919368fea4e4814a4664359503e6015b280aea"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
@@ -5750,7 +5818,7 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.108",
|
"syn 2.0.110",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -5834,8 +5902,7 @@ dependencies = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "tao"
|
name = "tao"
|
||||||
version = "0.34.5"
|
version = "0.34.5"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "git+https://github.com/tauri-apps/tao?branch=fix-windows-white-flash#cf28a8e8f205bf5cd7a41c4d7fc54ed9247c5f93"
|
||||||
checksum = "f3a753bdc39c07b192151523a3f77cd0394aa75413802c883a0f6f6a0e5ee2e7"
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 2.10.0",
|
"bitflags 2.10.0",
|
||||||
"block2 0.6.2",
|
"block2 0.6.2",
|
||||||
@@ -5862,7 +5929,7 @@ dependencies = [
|
|||||||
"parking_lot",
|
"parking_lot",
|
||||||
"raw-window-handle",
|
"raw-window-handle",
|
||||||
"scopeguard",
|
"scopeguard",
|
||||||
"tao-macros",
|
"tao-macros 0.1.3 (git+https://github.com/tauri-apps/tao?branch=fix-windows-white-flash)",
|
||||||
"unicode-segmentation",
|
"unicode-segmentation",
|
||||||
"url",
|
"url",
|
||||||
"windows",
|
"windows",
|
||||||
@@ -5879,7 +5946,17 @@ checksum = "f4e16beb8b2ac17db28eab8bca40e62dbfbb34c0fcdc6d9826b11b7b5d047dfd"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.108",
|
"syn 2.0.110",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tao-macros"
|
||||||
|
version = "0.1.3"
|
||||||
|
source = "git+https://github.com/tauri-apps/tao?branch=fix-windows-white-flash#cf28a8e8f205bf5cd7a41c4d7fc54ed9247c5f93"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.110",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -5997,7 +6074,7 @@ dependencies = [
|
|||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"sha2",
|
"sha2",
|
||||||
"syn 2.0.108",
|
"syn 2.0.110",
|
||||||
"tauri-utils",
|
"tauri-utils",
|
||||||
"thiserror 2.0.17",
|
"thiserror 2.0.17",
|
||||||
"time",
|
"time",
|
||||||
@@ -6015,7 +6092,7 @@ dependencies = [
|
|||||||
"heck 0.5.0",
|
"heck 0.5.0",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.108",
|
"syn 2.0.110",
|
||||||
"tauri-codegen",
|
"tauri-codegen",
|
||||||
"tauri-utils",
|
"tauri-utils",
|
||||||
]
|
]
|
||||||
@@ -6066,6 +6143,27 @@ dependencies = [
|
|||||||
"thiserror 2.0.17",
|
"thiserror 2.0.17",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tauri-plugin-deep-link"
|
||||||
|
version = "2.4.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6e82759f7c7d51de3cbde51c04b3f2332de52436ed84541182cd8944b04e9e73"
|
||||||
|
dependencies = [
|
||||||
|
"dunce",
|
||||||
|
"plist",
|
||||||
|
"rust-ini",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"tauri",
|
||||||
|
"tauri-plugin",
|
||||||
|
"tauri-utils",
|
||||||
|
"thiserror 2.0.17",
|
||||||
|
"tracing",
|
||||||
|
"url",
|
||||||
|
"windows-registry 0.5.3",
|
||||||
|
"windows-result 0.3.4",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tauri-plugin-dialog"
|
name = "tauri-plugin-dialog"
|
||||||
version = "2.4.2"
|
version = "2.4.2"
|
||||||
@@ -6106,6 +6204,21 @@ dependencies = [
|
|||||||
"url",
|
"url",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tauri-plugin-global-shortcut"
|
||||||
|
version = "2.3.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "424af23c7e88d05e4a1a6fc2c7be077912f8c76bd7900fd50aa2b7cbf5a2c405"
|
||||||
|
dependencies = [
|
||||||
|
"global-hotkey",
|
||||||
|
"log",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"tauri",
|
||||||
|
"tauri-plugin",
|
||||||
|
"thiserror 2.0.17",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tauri-plugin-http"
|
name = "tauri-plugin-http"
|
||||||
version = "2.5.4"
|
version = "2.5.4"
|
||||||
@@ -6283,6 +6396,7 @@ dependencies = [
|
|||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"tauri",
|
"tauri",
|
||||||
|
"tauri-plugin-deep-link",
|
||||||
"thiserror 2.0.17",
|
"thiserror 2.0.17",
|
||||||
"tracing",
|
"tracing",
|
||||||
"windows-sys 0.60.2",
|
"windows-sys 0.60.2",
|
||||||
@@ -6500,7 +6614,7 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.108",
|
"syn 2.0.110",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -6511,7 +6625,7 @@ checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.108",
|
"syn 2.0.110",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -6561,6 +6675,25 @@ dependencies = [
|
|||||||
"time-core",
|
"time-core",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tiny-keccak"
|
||||||
|
version = "2.0.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237"
|
||||||
|
dependencies = [
|
||||||
|
"crunchy",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tinyfiledialogs"
|
||||||
|
version = "3.9.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e25fa0bc43a6566e2cc6d7ac96df3fa5a57beba34445bead1b368ba8fe9ca568"
|
||||||
|
dependencies = [
|
||||||
|
"cc",
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tinystr"
|
name = "tinystr"
|
||||||
version = "0.8.2"
|
version = "0.8.2"
|
||||||
@@ -6612,7 +6745,7 @@ checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.108",
|
"syn 2.0.110",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -6809,7 +6942,7 @@ checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.108",
|
"syn 2.0.110",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -6985,16 +7118,15 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ureq"
|
name = "ureq"
|
||||||
version = "3.1.2"
|
version = "3.1.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "99ba1025f18a4a3fc3e9b48c868e9beb4f24f4b4b1a325bada26bd4119f46537"
|
checksum = "d39cb1dbab692d82a977c0392ffac19e188bd9186a9f32806f0aaa859d75585a"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64 0.22.1",
|
"base64 0.22.1",
|
||||||
"der",
|
"der",
|
||||||
"log",
|
"log",
|
||||||
"native-tls",
|
"native-tls",
|
||||||
"percent-encoding",
|
"percent-encoding",
|
||||||
"rustls-pemfile 2.2.0",
|
|
||||||
"rustls-pki-types",
|
"rustls-pki-types",
|
||||||
"ureq-proto",
|
"ureq-proto",
|
||||||
"utf-8",
|
"utf-8",
|
||||||
@@ -7211,7 +7343,7 @@ dependencies = [
|
|||||||
"bumpalo",
|
"bumpalo",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.108",
|
"syn 2.0.110",
|
||||||
"wasm-bindgen-shared",
|
"wasm-bindgen-shared",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -7414,7 +7546,7 @@ checksum = "1d228f15bba3b9d56dde8bddbee66fa24545bd17b48d5128ccf4a8742b18e431"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.108",
|
"syn 2.0.110",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -7430,9 +7562,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "weezl"
|
name = "weezl"
|
||||||
version = "0.1.10"
|
version = "0.1.11"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a751b3277700db47d3e574514de2eced5e54dc8a5436a3bf7a0b248b2cee16f3"
|
checksum = "009936b22a61d342859b5f0ea64681cbb35a358ab548e2a44a8cf0dac2d980b8"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "win7-notifications"
|
name = "win7-notifications"
|
||||||
@@ -7557,7 +7689,7 @@ checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.108",
|
"syn 2.0.110",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -7568,7 +7700,7 @@ checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.108",
|
"syn 2.0.110",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -8077,7 +8209,7 @@ dependencies = [
|
|||||||
"raw-window-handle",
|
"raw-window-handle",
|
||||||
"sha2",
|
"sha2",
|
||||||
"soup3",
|
"soup3",
|
||||||
"tao-macros",
|
"tao-macros 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||||
"thiserror 2.0.17",
|
"thiserror 2.0.17",
|
||||||
"url",
|
"url",
|
||||||
"webkit2gtk",
|
"webkit2gtk",
|
||||||
@@ -8146,6 +8278,12 @@ dependencies = [
|
|||||||
"rustix 1.1.2",
|
"rustix 1.1.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "xkeysym"
|
||||||
|
version = "0.2.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b9cc00251562a284751c9973bace760d86c0276c471b4be569fe6b068ee97a56"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "yoke"
|
name = "yoke"
|
||||||
version = "0.8.1"
|
version = "0.8.1"
|
||||||
@@ -8165,7 +8303,7 @@ checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.108",
|
"syn 2.0.110",
|
||||||
"synstructure",
|
"synstructure",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -8213,7 +8351,7 @@ dependencies = [
|
|||||||
"proc-macro-crate 3.4.0",
|
"proc-macro-crate 3.4.0",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.108",
|
"syn 2.0.110",
|
||||||
"zbus_names",
|
"zbus_names",
|
||||||
"zvariant",
|
"zvariant",
|
||||||
"zvariant_utils",
|
"zvariant_utils",
|
||||||
@@ -8248,7 +8386,7 @@ checksum = "88d2b8d9c68ad2b9e4340d7832716a4d21a22a1154777ad56ea55c51a9cf3831"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.108",
|
"syn 2.0.110",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -8268,7 +8406,7 @@ checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.108",
|
"syn 2.0.110",
|
||||||
"synstructure",
|
"synstructure",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -8308,7 +8446,7 @@ checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.108",
|
"syn 2.0.110",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -8411,7 +8549,7 @@ dependencies = [
|
|||||||
"proc-macro-crate 3.4.0",
|
"proc-macro-crate 3.4.0",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.108",
|
"syn 2.0.110",
|
||||||
"zvariant_utils",
|
"zvariant_utils",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -8424,6 +8562,6 @@ dependencies = [
|
|||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"serde",
|
"serde",
|
||||||
"syn 2.0.108",
|
"syn 2.0.110",
|
||||||
"winnow 0.7.13",
|
"winnow 0.7.13",
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "app"
|
name = "app"
|
||||||
version = "2.8.1"
|
version = "3.0.0"
|
||||||
description = "A Tauri App"
|
description = "A Tauri App"
|
||||||
authors = ["you"]
|
authors = ["you"]
|
||||||
license = ""
|
license = ""
|
||||||
@@ -33,22 +33,29 @@ tauri-plugin-store = "2.4.1"
|
|||||||
tauri-plugin-process = "2.3.1"
|
tauri-plugin-process = "2.3.1"
|
||||||
tauri-plugin-notification = { version = "2.3.3", features = [ "windows7-compat" ] }
|
tauri-plugin-notification = { version = "2.3.3", features = [ "windows7-compat" ] }
|
||||||
tauri-plugin-os = "2.3.2"
|
tauri-plugin-os = "2.3.2"
|
||||||
|
tauri-plugin-global-shortcut = "2.3.1"
|
||||||
fix-path-env = { git = "https://github.com/tauri-apps/fix-path-env-rs" }
|
fix-path-env = { git = "https://github.com/tauri-apps/fix-path-env-rs" }
|
||||||
zip = "0.6.6"
|
zip = "0.6.6"
|
||||||
machine-uid = "0.5.4"
|
machine-uid = "0.5.4"
|
||||||
tauri-plugin-sentry = "0.5.0"
|
tauri-plugin-sentry = "0.5.0"
|
||||||
sentry = "0.42"
|
sentry = "0.42"
|
||||||
tauri-plugin-autostart = "2.5.1"
|
tauri-plugin-autostart = "2.5.1"
|
||||||
tauri-plugin-single-instance = "2.3.6"
|
tauri-plugin-single-instance = { version = "2.3.6", features = ["deep-link"] }
|
||||||
tauri-plugin-updater = "2.9.0"
|
tauri-plugin-updater = "2.9.0"
|
||||||
tauri-plugin-clipboard-manager = "2.3.2"
|
tauri-plugin-clipboard-manager = "2.3.2"
|
||||||
tauri-plugin-prevent-default = "3"
|
tauri-plugin-prevent-default = "3"
|
||||||
reqwest = { version = "0.11", features = ["json"] }
|
reqwest = { version = "0.11", features = ["json"] }
|
||||||
sysinfo = "0.37"
|
sysinfo = "0.37"
|
||||||
|
tinyfiledialogs = "3.9.1"
|
||||||
|
tauri-plugin-deep-link = "2"
|
||||||
|
|
||||||
[target.'cfg(target_os = "macos")'.dependencies]
|
[target.'cfg(target_os = "macos")'.dependencies]
|
||||||
cocoa = "0.26"
|
cocoa = "0.26"
|
||||||
|
objc = "0.2"
|
||||||
|
|
||||||
[target.'cfg(target_os = "linux")'.dependencies]
|
[target.'cfg(target_os = "linux")'.dependencies]
|
||||||
zbus = { version = "5" }
|
zbus = { version = "5" }
|
||||||
x11rb = "0.13"
|
x11rb = "0.13"
|
||||||
|
|
||||||
|
[patch.crates-io]
|
||||||
|
tao = { git = "https://github.com/tauri-apps/tao", branch = "fix-windows-white-flash" }
|
||||||
|
|||||||
@@ -11,13 +11,16 @@
|
|||||||
"Purge",
|
"Purge",
|
||||||
"Delete",
|
"Delete",
|
||||||
"Settings",
|
"Settings",
|
||||||
"Jobs",
|
"Transfers",
|
||||||
"Cron",
|
"Schedules",
|
||||||
|
"Templates",
|
||||||
"Serve",
|
"Serve",
|
||||||
"Bisync",
|
"Bisync",
|
||||||
"Download",
|
"Download",
|
||||||
"Browse",
|
"Browse",
|
||||||
"Startup",
|
"Startup",
|
||||||
|
"Toolbar",
|
||||||
|
"Onboarding",
|
||||||
"Test"
|
"Test"
|
||||||
],
|
],
|
||||||
"permissions": [
|
"permissions": [
|
||||||
@@ -247,7 +250,6 @@
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|
||||||
"opener:allow-open-url",
|
"opener:allow-open-url",
|
||||||
{
|
{
|
||||||
"identifier": "opener:allow-open-path",
|
"identifier": "opener:allow-open-path",
|
||||||
@@ -266,57 +268,21 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"opener:allow-default-urls",
|
"opener:allow-default-urls",
|
||||||
|
|
||||||
"updater:allow-download",
|
"updater:allow-download",
|
||||||
"updater:allow-download-and-install",
|
"updater:allow-download-and-install",
|
||||||
"updater:allow-check",
|
"updater:allow-check",
|
||||||
"updater:allow-install",
|
"updater:allow-install",
|
||||||
|
|
||||||
"autostart:allow-enable",
|
"autostart:allow-enable",
|
||||||
"autostart:allow-disable",
|
"autostart:allow-disable",
|
||||||
"autostart:allow-is-enabled",
|
"autostart:allow-is-enabled",
|
||||||
|
|
||||||
{
|
{
|
||||||
"identifier": "http:default",
|
"identifier": "http:default",
|
||||||
"allow": [
|
"allow": [
|
||||||
{
|
{
|
||||||
"url": "http://localhost:5572/**"
|
"url": "http://*:*"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"url": "https://downloads.rclone.org/**"
|
"url": "https://*:*"
|
||||||
},
|
|
||||||
{
|
|
||||||
"url": "https://github.com/**"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url": "https://raw.githubusercontent.com/**"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url": "https://rcloneui.com/**"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url": "https://stripe.com/**"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url": "https://buy.stripe.com/**"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url": "https://checkout.stripe.com/**"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url": "https://js.stripe.com/**"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url": "https://donate.stripe.com/**"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url": "https://payments.stripe.com/**"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url": "https://checkout.link.com/**"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"url": "https://**.sentry.io/**"
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@@ -326,6 +292,8 @@
|
|||||||
"os:default",
|
"os:default",
|
||||||
"sentry:allow-event",
|
"sentry:allow-event",
|
||||||
"sentry:allow-breadcrumb",
|
"sentry:allow-breadcrumb",
|
||||||
"sentry:allow-envelope"
|
"sentry:allow-envelope",
|
||||||
|
"deep-link:default",
|
||||||
|
"deep-link:allow-get-current"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,184 @@
|
|||||||
|
use tauri::{AppHandle, Manager, PhysicalSize, Size, WebviewWindow, WebviewWindowBuilder};
|
||||||
|
use tauri_plugin_global_shortcut::{GlobalShortcutExt, Shortcut, ShortcutState};
|
||||||
|
|
||||||
|
use super::window::make_transparent;
|
||||||
|
|
||||||
|
pub const DEFAULT_TOOLBAR_SHORTCUT: &str = "CmdOrCtrl+Shift+/";
|
||||||
|
const TOOLBAR_WINDOW_LABEL: &str = "Toolbar";
|
||||||
|
|
||||||
|
// Fixed physical size for the toolbar window on Windows (in physical pixels)
|
||||||
|
// This avoids DPI scaling issues on multi-monitor setups with different scale factors
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
const TOOLBAR_PHYSICAL_WIDTH: u32 = 702;
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
const TOOLBAR_PHYSICAL_HEIGHT: u32 = 460;
|
||||||
|
|
||||||
|
fn create_toolbar_window(app_handle: &AppHandle) -> Result<WebviewWindow, tauri::Error> {
|
||||||
|
let monitor = match app_handle.primary_monitor()? {
|
||||||
|
Some(monitor) => monitor,
|
||||||
|
None => app_handle
|
||||||
|
.available_monitors()?
|
||||||
|
.into_iter()
|
||||||
|
.next()
|
||||||
|
.ok_or_else(|| {
|
||||||
|
std::io::Error::new(std::io::ErrorKind::NotFound, "No monitors available")
|
||||||
|
})?,
|
||||||
|
};
|
||||||
|
|
||||||
|
let physical_size = monitor.size();
|
||||||
|
let physical_position = monitor.position();
|
||||||
|
|
||||||
|
// On Windows, use physical pixels directly to avoid DPI scaling issues
|
||||||
|
// across multiple monitors with different scale factors
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
let builder = {
|
||||||
|
// Calculate position in physical pixels, centered on the primary monitor
|
||||||
|
let pos_x = physical_position.x + ((physical_size.width - TOOLBAR_PHYSICAL_WIDTH) / 2) as i32;
|
||||||
|
let pos_y = physical_position.y + (physical_size.height / 4) as i32;
|
||||||
|
|
||||||
|
WebviewWindowBuilder::new(
|
||||||
|
app_handle,
|
||||||
|
TOOLBAR_WINDOW_LABEL,
|
||||||
|
tauri::WebviewUrl::App("/toolbar".into()),
|
||||||
|
)
|
||||||
|
.title(TOOLBAR_WINDOW_LABEL)
|
||||||
|
// Use initial size (will be corrected to physical pixels after creation)
|
||||||
|
.inner_size(TOOLBAR_PHYSICAL_WIDTH as f64, TOOLBAR_PHYSICAL_HEIGHT as f64)
|
||||||
|
.position(pos_x as f64, pos_y as f64)
|
||||||
|
.resizable(false)
|
||||||
|
.decorations(false)
|
||||||
|
.shadow(false)
|
||||||
|
.focused(false)
|
||||||
|
.visible(false)
|
||||||
|
.visible_on_all_workspaces(true)
|
||||||
|
.zoom_hotkeys_enabled(false)
|
||||||
|
};
|
||||||
|
|
||||||
|
// On macOS and Linux, use logical coordinates (full screen size for click-through)
|
||||||
|
#[cfg(not(target_os = "windows"))]
|
||||||
|
let builder = {
|
||||||
|
let scale_factor = monitor.scale_factor();
|
||||||
|
let logical_size = physical_size.to_logical::<f64>(scale_factor);
|
||||||
|
let logical_position = physical_position.to_logical::<f64>(scale_factor);
|
||||||
|
|
||||||
|
let pos_x = logical_position.x;
|
||||||
|
let pos_y = logical_position.y + (logical_size.height / 4.0);
|
||||||
|
|
||||||
|
let mut b = WebviewWindowBuilder::new(
|
||||||
|
app_handle,
|
||||||
|
TOOLBAR_WINDOW_LABEL,
|
||||||
|
tauri::WebviewUrl::App("/toolbar".into()),
|
||||||
|
)
|
||||||
|
.title(TOOLBAR_WINDOW_LABEL)
|
||||||
|
.inner_size(logical_size.width, logical_size.height)
|
||||||
|
.position(pos_x, pos_y)
|
||||||
|
.resizable(false)
|
||||||
|
.decorations(false)
|
||||||
|
.shadow(false)
|
||||||
|
.focused(false)
|
||||||
|
.visible(false)
|
||||||
|
.visible_on_all_workspaces(true)
|
||||||
|
.zoom_hotkeys_enabled(false);
|
||||||
|
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
{
|
||||||
|
b = b.transparent(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
b
|
||||||
|
};
|
||||||
|
|
||||||
|
let window = builder.build()?;
|
||||||
|
|
||||||
|
// On Windows, explicitly set the size in physical pixels to ensure
|
||||||
|
// consistent sizing regardless of which monitor's DPI context is used
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
{
|
||||||
|
let physical_size = PhysicalSize::new(TOOLBAR_PHYSICAL_WIDTH, TOOLBAR_PHYSICAL_HEIGHT);
|
||||||
|
window.set_size(Size::Physical(physical_size))?;
|
||||||
|
}
|
||||||
|
|
||||||
|
window.set_zoom(1.0)?;
|
||||||
|
window.hide()?;
|
||||||
|
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
if let Err(err) = make_transparent(&window) {
|
||||||
|
log::warn!("failed to make toolbar window transparent: {}", err);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(window)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn ensure_toolbar_window(app_handle: &AppHandle) -> Result<(), tauri::Error> {
|
||||||
|
if app_handle
|
||||||
|
.get_webview_window(TOOLBAR_WINDOW_LABEL)
|
||||||
|
.is_none()
|
||||||
|
{
|
||||||
|
create_toolbar_window(app_handle)?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn show_toolbar_window(app_handle: &AppHandle) -> Result<(), tauri::Error> {
|
||||||
|
ensure_toolbar_window(app_handle)?;
|
||||||
|
|
||||||
|
if let Some(window) = app_handle.get_webview_window(TOOLBAR_WINDOW_LABEL) {
|
||||||
|
window.show()?;
|
||||||
|
window.unminimize()?;
|
||||||
|
window.set_focus()?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn open_toolbar(app_handle: &AppHandle) -> Result<(), tauri::Error> {
|
||||||
|
ensure_toolbar_window(app_handle)?;
|
||||||
|
|
||||||
|
if let Some(window) = app_handle.get_webview_window(TOOLBAR_WINDOW_LABEL) {
|
||||||
|
if window.is_visible()? {
|
||||||
|
window.hide()?;
|
||||||
|
} else {
|
||||||
|
window.show()?;
|
||||||
|
window.unminimize()?;
|
||||||
|
window.set_focus()?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_shortcut(shortcut: &str) -> Result<Shortcut, String> {
|
||||||
|
shortcut.parse::<Shortcut>().map_err(|e| e.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn register_toolbar_shortcut(app_handle: &AppHandle, shortcut: &str) -> Result<(), String> {
|
||||||
|
let parsed = parse_shortcut(shortcut)?;
|
||||||
|
app_handle
|
||||||
|
.global_shortcut()
|
||||||
|
.on_shortcut(parsed, |app, _shortcut, event| {
|
||||||
|
if matches!(event.state(), ShortcutState::Pressed) {
|
||||||
|
if let Err(err) = open_toolbar(app) {
|
||||||
|
log::error!("failed to open toolbar via shortcut: {}", err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.map_err(|e| e.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_toolbar_shortcut(app_handle: &AppHandle, shortcut: Option<&str>) -> Result<(), String> {
|
||||||
|
let shortcut = shortcut
|
||||||
|
.map(|s| s.trim())
|
||||||
|
.filter(|s| !s.is_empty())
|
||||||
|
.map(|s| s.to_string());
|
||||||
|
|
||||||
|
app_handle
|
||||||
|
.global_shortcut()
|
||||||
|
.unregister_all()
|
||||||
|
.map_err(|e| e.to_string())?;
|
||||||
|
|
||||||
|
if let Some(shortcut) = shortcut {
|
||||||
|
register_toolbar_shortcut(app_handle, shortcut.as_str())?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
@@ -0,0 +1,250 @@
|
|||||||
|
use tauri::{AppHandle, Manager, WebviewUrl, WebviewWindow, WebviewWindowBuilder};
|
||||||
|
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
pub fn make_transparent(window: &WebviewWindow) -> Result<(), tauri::Error> {
|
||||||
|
use cocoa::{
|
||||||
|
appkit::NSColor,
|
||||||
|
base::{id, nil},
|
||||||
|
foundation::NSString,
|
||||||
|
};
|
||||||
|
use objc::{class, msg_send, runtime::Object, sel, sel_impl};
|
||||||
|
|
||||||
|
window.with_webview(|webview| unsafe {
|
||||||
|
let webview_obj = webview.inner() as *mut Object;
|
||||||
|
let no_value: id = msg_send![class!(NSNumber), numberWithBool:0];
|
||||||
|
let key = NSString::alloc(nil).init_str("drawsBackground");
|
||||||
|
let _: id = msg_send![webview_obj, setValue:no_value forKey:key];
|
||||||
|
let _: () = msg_send![key, release];
|
||||||
|
let ns_window: id = msg_send![webview_obj, window];
|
||||||
|
let bg_color = NSColor::colorWithSRGBRed_green_blue_alpha_(nil, 0.0, 0.0, 0.0, 0.0);
|
||||||
|
let _: id = msg_send![ns_window, setBackgroundColor: bg_color];
|
||||||
|
// let _: () = msg_send![ns_window, setIgnoresMouseEvents:true];
|
||||||
|
})?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(target_os = "macos"))]
|
||||||
|
pub fn make_transparent(_window: &WebviewWindow) -> Result<(), tauri::Error> {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn open_full_window(
|
||||||
|
app_handle: AppHandle,
|
||||||
|
name: String,
|
||||||
|
url: String,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
if let Some(existing) = app_handle.get_webview_window(&name) {
|
||||||
|
existing.set_focus().map_err(|e| e.to_string())?;
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let os = std::env::consts::OS;
|
||||||
|
|
||||||
|
let primary_monitor = app_handle
|
||||||
|
.primary_monitor()
|
||||||
|
.map_err(|e| e.to_string())?
|
||||||
|
.or_else(|| app_handle.available_monitors().ok()?.into_iter().next());
|
||||||
|
|
||||||
|
let monitor = primary_monitor.ok_or("No monitor found")?;
|
||||||
|
let size = monitor.size();
|
||||||
|
|
||||||
|
let width = size.width as f64;
|
||||||
|
let mut height = size.height as f64;
|
||||||
|
|
||||||
|
if os == "windows" {
|
||||||
|
height -= 100.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
let window = WebviewWindowBuilder::new(&app_handle, &name, WebviewUrl::App(url.into()))
|
||||||
|
.title(&name)
|
||||||
|
.inner_size(width, height)
|
||||||
|
.resizable(true)
|
||||||
|
.visible(false)
|
||||||
|
.focused(false)
|
||||||
|
.decorations(true)
|
||||||
|
// .additional_browser_args("--disable-pinch")
|
||||||
|
.zoom_hotkeys_enabled(false)
|
||||||
|
.build()
|
||||||
|
.map_err(|e| e.to_string())?;
|
||||||
|
|
||||||
|
std::thread::sleep(std::time::Duration::from_millis(750));
|
||||||
|
|
||||||
|
window.center().map_err(|e| e.to_string())?;
|
||||||
|
window.set_zoom(1.0).map_err(|e| e.to_string())?;
|
||||||
|
window.show().map_err(|e| e.to_string())?;
|
||||||
|
window.set_focus().map_err(|e| e.to_string())?;
|
||||||
|
|
||||||
|
if os != "windows" && os != "macos" {
|
||||||
|
window.set_resizable(false).map_err(|e| e.to_string())?;
|
||||||
|
window.set_resizable(true).map_err(|e| e.to_string())?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn open_window(
|
||||||
|
app_handle: AppHandle,
|
||||||
|
name: String,
|
||||||
|
url: String,
|
||||||
|
width: Option<f64>,
|
||||||
|
height: Option<f64>,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
if let Some(existing) = app_handle.get_webview_window(&name) {
|
||||||
|
existing.set_focus().map_err(|e| e.to_string())?;
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let os = std::env::consts::OS;
|
||||||
|
|
||||||
|
let default_height = if os == "windows" { 755.0 } else { 725.0 };
|
||||||
|
let width = width.unwrap_or(840.0);
|
||||||
|
let height = height.unwrap_or(default_height);
|
||||||
|
|
||||||
|
let mut builder = WebviewWindowBuilder::new(&app_handle, &name, WebviewUrl::App(url.into()))
|
||||||
|
.title(&name)
|
||||||
|
.inner_size(width, height)
|
||||||
|
.resizable(false)
|
||||||
|
.visible(false)
|
||||||
|
.focused(false)
|
||||||
|
.decorations(true)
|
||||||
|
.zoom_hotkeys_enabled(false);
|
||||||
|
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
{
|
||||||
|
builder = builder.title_bar_style(tauri::TitleBarStyle::Overlay);
|
||||||
|
}
|
||||||
|
|
||||||
|
let window = builder.build().map_err(|e| e.to_string())?;
|
||||||
|
|
||||||
|
std::thread::sleep(std::time::Duration::from_millis(750));
|
||||||
|
|
||||||
|
// window.center().map_err(|e| e.to_string())?;
|
||||||
|
let monitor = window
|
||||||
|
.current_monitor()
|
||||||
|
.map_err(|e| e.to_string())?
|
||||||
|
.or_else(|| app_handle.primary_monitor().ok().flatten())
|
||||||
|
.ok_or("Monitor not found")?;
|
||||||
|
|
||||||
|
let screen_size = monitor.size();
|
||||||
|
let monitor_pos = monitor.position();
|
||||||
|
let window_size = window.outer_size().map_err(|e| e.to_string())?;
|
||||||
|
|
||||||
|
let x = monitor_pos.x + (screen_size.width as i32 - window_size.width as i32) / 2;
|
||||||
|
let y = monitor_pos.y + (screen_size.height as i32 - window_size.height as i32) / 2;
|
||||||
|
|
||||||
|
let count = app_handle.webview_windows().len() as i32;
|
||||||
|
let offset = count * 20;
|
||||||
|
let sign = if count % 2 != 0 { -1 } else { 1 };
|
||||||
|
let final_offset = offset * sign;
|
||||||
|
|
||||||
|
window
|
||||||
|
.set_position(tauri::Position::Physical(tauri::PhysicalPosition {
|
||||||
|
x: x + final_offset,
|
||||||
|
y: y + final_offset,
|
||||||
|
}))
|
||||||
|
.map_err(|e| e.to_string())?;
|
||||||
|
|
||||||
|
window.set_zoom(1.0).map_err(|e| e.to_string())?;
|
||||||
|
window.show().map_err(|e| e.to_string())?;
|
||||||
|
window.set_focus().map_err(|e| e.to_string())?;
|
||||||
|
|
||||||
|
if os != "windows" && os != "macos" {
|
||||||
|
window.set_resizable(true).map_err(|e| e.to_string())?;
|
||||||
|
window.set_resizable(false).map_err(|e| e.to_string())?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn open_small_window(
|
||||||
|
app_handle: AppHandle,
|
||||||
|
name: String,
|
||||||
|
url: String,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
if let Some(existing) = app_handle.get_webview_window(&name) {
|
||||||
|
existing.set_focus().map_err(|e| e.to_string())?;
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let os = std::env::consts::OS;
|
||||||
|
|
||||||
|
let mut builder = WebviewWindowBuilder::new(&app_handle, &name, WebviewUrl::App(url.into()))
|
||||||
|
.title(&name)
|
||||||
|
.inner_size(800.0, 500.0)
|
||||||
|
.resizable(false)
|
||||||
|
.visible(false)
|
||||||
|
.focused(false)
|
||||||
|
.decorations(false)
|
||||||
|
.closable(false)
|
||||||
|
.shadow(false)
|
||||||
|
.zoom_hotkeys_enabled(false);
|
||||||
|
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
{
|
||||||
|
builder = builder.transparent(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
let window = builder.build().map_err(|e| e.to_string())?;
|
||||||
|
|
||||||
|
window.center().map_err(|e| e.to_string())?;
|
||||||
|
window.set_zoom(1.0).map_err(|e| e.to_string())?;
|
||||||
|
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
if let Err(err) = make_transparent(&window) {
|
||||||
|
log::warn!("failed to make small window transparent: {}", err);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::thread::sleep(std::time::Duration::from_millis(750));
|
||||||
|
|
||||||
|
window.set_decorations(false).map_err(|e| e.to_string())?;
|
||||||
|
window.show().map_err(|e| e.to_string())?;
|
||||||
|
window.set_focus().map_err(|e| e.to_string())?;
|
||||||
|
window.set_always_on_top(true).map_err(|e| e.to_string())?;
|
||||||
|
|
||||||
|
if os != "windows" && os != "macos" {
|
||||||
|
window.set_resizable(true).map_err(|e| e.to_string())?;
|
||||||
|
window.set_resizable(false).map_err(|e| e.to_string())?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn lock_windows(app_handle: AppHandle, ids: Option<Vec<String>>) -> Result<(), String> {
|
||||||
|
let windows = app_handle.webview_windows();
|
||||||
|
|
||||||
|
for (label, window) in windows.iter() {
|
||||||
|
let should_lock = match &ids {
|
||||||
|
Some(id_list) => id_list.contains(label),
|
||||||
|
None => true,
|
||||||
|
};
|
||||||
|
|
||||||
|
if should_lock {
|
||||||
|
window.set_closable(false).map_err(|e| e.to_string())?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn unlock_windows(app_handle: AppHandle, ids: Option<Vec<String>>) -> Result<(), String> {
|
||||||
|
let windows = app_handle.webview_windows();
|
||||||
|
|
||||||
|
for (label, window) in windows.iter() {
|
||||||
|
let should_unlock = match &ids {
|
||||||
|
Some(id_list) => id_list.contains(label),
|
||||||
|
None => true,
|
||||||
|
};
|
||||||
|
|
||||||
|
if should_unlock {
|
||||||
|
window.set_closable(true).map_err(|e| e.to_string())?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
@@ -41,7 +41,7 @@
|
|||||||
<li>✅ File Browser 2.0</li>
|
<li>✅ File Browser 2.0</li>
|
||||||
<li>✅ Proxy support</li>
|
<li>✅ Proxy support</li>
|
||||||
<li>✅ Sync, Download, Copy, Mount, Serve, Move, and more</li>
|
<li>✅ Sync, Download, Copy, Mount, Serve, Move, and more</li>
|
||||||
<li>✅ Jobs (see all background operations)</li>
|
<li>✅ Transfers (see all background operations)</li>
|
||||||
<li>✅ Set default flags for each remote separately, based on the operation (mount, copy, etc)</li>
|
<li>✅ Set default flags for each remote separately, based on the operation (mount, copy, etc)</li>
|
||||||
<li>✅ Customize the flags when running an operation on a case by case basis (on top of defaults)</li>
|
<li>✅ Customize the flags when running an operation on a case by case basis (on top of defaults)</li>
|
||||||
<li>✅ Set a password for the Settings panel</li>
|
<li>✅ Set a password for the Settings panel</li>
|
||||||
@@ -114,10 +114,10 @@
|
|||||||
</screenshots>
|
</screenshots>
|
||||||
|
|
||||||
<releases>
|
<releases>
|
||||||
<release version="2.8.1" date="2025-11-11">
|
<release version="3.0.0" date="2025-12-06">
|
||||||
<url type="details">https://github.com/rclone-ui/rclone-ui/releases/tag/v2.8.1</url>
|
<url type="details">https://github.com/rclone-ui/rclone-ui/releases/tag/v3.0.0</url>
|
||||||
<description>
|
<description>
|
||||||
<p>2.8.1 Release</p>
|
<p>3.0.0 Release</p>
|
||||||
<ul>
|
<ul>
|
||||||
<li>Improved Linux stability and overall performance.</li>
|
<li>Improved Linux stability and overall performance.</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 5.3 KiB |
|
Before Width: | Height: | Size: 5.3 KiB |
|
Before Width: | Height: | Size: 3.2 KiB |
|
Before Width: | Height: | Size: 2.3 KiB |
|
Before Width: | Height: | Size: 431 B |
|
Before Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 1.0 KiB |
|
Before Width: | Height: | Size: 531 B |
|
Before Width: | Height: | Size: 656 B |
|
Before Width: | Height: | Size: 721 B |
|
Before Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 2.8 KiB |
|
Before Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 818 B |
|
Before Width: | Height: | Size: 4.1 KiB |
|
Before Width: | Height: | Size: 1.0 KiB |
|
Before Width: | Height: | Size: 5.1 KiB |
|
Before Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 2.6 KiB |
|
Before Width: | Height: | Size: 2.3 KiB |
|
Before Width: | Height: | Size: 946 B |
|
Before Width: | Height: | Size: 3.1 KiB |
|
Before Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 3.6 KiB |
|
Before Width: | Height: | Size: 848 B |
|
Before Width: | Height: | Size: 677 B |
|
Before Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 5.2 KiB |
|
Before Width: | Height: | Size: 665 B |