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'
|
||||
args: ''
|
||||
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'
|
||||
args: "--config '{\"bundle\":{\"createUpdaterArtifacts\":false}}'"
|
||||
assetPattern: '[name]_[arch]_ALTERNATE[ext]'
|
||||
variant: 'alternate'
|
||||
- platform: 'ubuntu-22.04-arm'
|
||||
args: ''
|
||||
assetPattern: '[name]_[arch][ext]'
|
||||
variant: 'default'
|
||||
- platform: 'windows-11-arm'
|
||||
args: '--target x86_64-pc-windows-msvc'
|
||||
arch: 'arm64'
|
||||
@@ -81,16 +74,16 @@ jobs:
|
||||
uses: swatinem/rust-cache@v2
|
||||
with:
|
||||
workspaces: './src-tauri -> target'
|
||||
key: ${{ runner.os }}-${{ runner.arch }}${{ matrix.variant && format('-{0}', matrix.variant) || '' }}
|
||||
key: ${{ matrix.platform }}
|
||||
|
||||
- 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: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf
|
||||
|
||||
- name: install dependencies (linux legacy only)
|
||||
if: matrix.variant == 'legacy'
|
||||
- name: install dependencies (llinux only)
|
||||
if: matrix.platform == 'ubuntu-22.04'
|
||||
run: |
|
||||
sudo apt-get update
|
||||
|
||||
@@ -133,7 +126,7 @@ jobs:
|
||||
cargo install trusted-signing-cli
|
||||
|
||||
- name: Override linuxdeploy (linux legacy only)
|
||||
if: matrix.variant == 'legacy'
|
||||
if: matrix.platform == 'ubuntu-22.04'
|
||||
run: |
|
||||
TAURI_TOOLKIT_PATH="${XDG_CACHE_HOME:-$HOME/.cache}/tauri"
|
||||
mkdir -p "$TAURI_TOOLKIT_PATH"
|
||||
@@ -192,7 +185,7 @@ jobs:
|
||||
with:
|
||||
tagName: 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 }}
|
||||
releaseDraft: true
|
||||
prerelease: false
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
<img src=".github/rclone-mobile.png" alt="Rclone Mobile" width="100%">
|
||||
|
||||
<br />
|
||||
<br />
|
||||
|
||||
<h1 align="center">
|
||||
<a href="https://rcloneui.com">
|
||||
<img src="./public/banner.png" alt="Rclone UI" width="100%">
|
||||
@@ -50,28 +55,50 @@
|
||||
|
||||
<br />
|
||||
|
||||
## Features
|
||||
### 🖥️ Cross platform running on Windows, Linux, macOS (x64, Arm and Apple Silicon)
|
||||
### ✅ Works with your existing rclone installation (or can install rclone for you) and keeps it updated (with your approval)
|
||||
### 🔗 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
|
||||
<a href="https://get.rcloneui.com/showcase">
|
||||
<img src=".github/rclone-video.png" alt="The GUI for Rclone">
|
||||
</a>
|
||||
|
||||
<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)
|
||||
### 📊 Job Logs (visualize all background operations)
|
||||
### 🚀 Start on boot (autostart) and Mount on start (automount)
|
||||
### 🔒 Set a password for the Settings panel
|
||||
### 👁️ Hide commands or Remotes from the tray menu
|
||||
### ⚙️ Import/Export Configs (encrypted with pass or command)
|
||||
### ✍️ Signed by Microsoft and Notarized on macOS
|
||||
#### Docker Compose
|
||||
```yaml
|
||||
services:
|
||||
rclone:
|
||||
image: rclone/rclone
|
||||
container_name: rclone
|
||||
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
|
||||
- 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`**
|
||||
- Chocolatey **`choco install rclone-ui`**
|
||||
- WinGet **`winget install --id=RcloneUI.RcloneUI -e`**
|
||||
- NPM **`npx rclone-ui`**
|
||||
|
||||
## Downloads
|
||||
- **Windows** (**[Arm](https://get.rcloneui.com/win-arm)**, **[x64](https://get.rcloneui.com/win)**)
|
||||
@@ -100,7 +128,15 @@
|
||||
</a>
|
||||
|
||||
## 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 />
|
||||
|
||||
|
||||
@@ -50,28 +50,49 @@
|
||||
|
||||
<br />
|
||||
|
||||
## 功能
|
||||
### 🖥️ 跨平台:Windows、Linux、macOS(x64、Arm、Apple Silicon)
|
||||
### ✅ 可配合你现有的 rclone 安装使用(也可为你安装),并在你确认后保持 up-to-date
|
||||
### 🔗 添加 / 编辑 / 移除 Remotes
|
||||
### ⚡️ Sync、Copy、Move、Delete、Purge、Serve、Mount 等
|
||||
### ⭐️ 为每个 Remote、每种操作(Mount、Sync、Copy 等)分别设置默认参数(flags),节省时间
|
||||
### 🎛️ 按需为每次操作自定义参数(flags)
|
||||
### 📚 内置每个参数的说明,无需再 Google 查询!
|
||||
### 📂 文件浏览器
|
||||
### 📡 代理
|
||||
<a href="https://get.rcloneui.com/showcase">
|
||||
<img src=".github/rclone-video.png" alt="The GUI for Rclone">
|
||||
</a>
|
||||
|
||||
<img src=".github/rclone-1.png" alt="Rclone UI">
|
||||
## Docker/Homelab/服务器使用
|
||||
使用 **最简单的方式** 管理远程 **`rclone`** 实例,控制你的服务器、Homelab 或家人的电脑。
|
||||
|
||||
### ⏰ 计划任务(cron)
|
||||
### 📊 任务日志(可视化所有后台操作)
|
||||
### 🚀 开机自启(autostart)与启动自动挂载(automount)
|
||||
### 🔒 为设置面板设置密码
|
||||
### 👁️ 可在托盘菜单中隐藏命令或 Remotes
|
||||
### ⚙️ 导入 / 导出配置(可用 pass 或 command 加密)
|
||||
### ✍️ 由 Microsoft 签名,macOS 已完成公证(Notarized)
|
||||
#### Docker Compose
|
||||
```yaml
|
||||
services:
|
||||
rclone:
|
||||
image: rclone/rclone
|
||||
container_name: rclone
|
||||
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)**
|
||||
@@ -79,6 +100,7 @@
|
||||
- Scoop **`scoop bucket add extras`** & **`scoop install rclone-ui`**
|
||||
- Chocolatey **`choco install rclone-ui`**
|
||||
- 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)**)
|
||||
@@ -100,7 +122,15 @@
|
||||
</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 />
|
||||
|
||||
|
||||
@@ -50,28 +50,49 @@
|
||||
|
||||
<br />
|
||||
|
||||
## Features
|
||||
### 🖥️ Cross-platform: Windows, Linux, macOS (x64, Arm und Apple Silicon)
|
||||
### ✅ Funktioniert mit deiner bestehenden rclone-Installation (oder kann rclone für dich installieren) und hält sie auf Wunsch aktuell
|
||||
### 🔗 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
|
||||
<a href="https://get.rcloneui.com/showcase">
|
||||
<img src=".github/rclone-video.png" alt="The GUI for Rclone">
|
||||
</a>
|
||||
|
||||
<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)
|
||||
### 📊 Job Logs (alle Hintergrund-Operationen visualisieren)
|
||||
### 🚀 Start on boot (Autostart) & Mount on start (Automount)
|
||||
### 🔒 Passwort für das Settings Panel setzen
|
||||
### 👁️ Commands oder Remotes im Tray Menu ausblenden
|
||||
### ⚙️ Import/Export Configs (encrypted with <tt>pass</tt> oder <tt>command</tt>)
|
||||
### ✍️ Signed by Microsoft & Notarized on macOS
|
||||
#### Docker Compose
|
||||
```yaml
|
||||
services:
|
||||
rclone:
|
||||
image: rclone/rclone
|
||||
container_name: rclone
|
||||
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
|
||||
- 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`**
|
||||
- Chocolatey **`choco install rclone-ui`**
|
||||
- WinGet **`winget install --id=RcloneUI.RcloneUI -e`**
|
||||
- NPM **`npx rclone-ui`**
|
||||
|
||||
## Downloads
|
||||
- **Windows** (**[Arm](https://get.rcloneui.com/win-arm)**, **[x64](https://get.rcloneui.com/win)**)
|
||||
@@ -100,7 +122,15 @@
|
||||
</a>
|
||||
|
||||
## 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 />
|
||||
|
||||
|
||||
@@ -50,28 +50,49 @@
|
||||
|
||||
<br />
|
||||
|
||||
## 機能
|
||||
### 🖥️ Cross-platform:Windows・Linux・macOS(x64 / Arm / Apple Silicon)
|
||||
### ✅ 既存の rclone 環境でそのまま動作(必要ならインストールも可能)。アップデートはあなたの承認のもとで適用
|
||||
### 🔗 Remote の追加/編集/削除
|
||||
### ⚡️ Sync・Copy・Move・Delete・Purge・Serve・Mount など
|
||||
### ⭐️ 操作ごと(Mount/Sync/Copy など)に Remote 単位で default flags を設定して時短
|
||||
### 🎛️ 実行時にケースバイケースで flags(パラメータ)をカスタマイズ
|
||||
### 📚 各 flag のドキュメントを内蔵。ググる手間はもう不要!
|
||||
### 📂 File Browser
|
||||
### 📡 Proxy
|
||||
<a href="https://get.rcloneui.com/showcase">
|
||||
<img src=".github/rclone-video.png" alt="The GUI for Rclone">
|
||||
</a>
|
||||
|
||||
<img src=".github/rclone-1.png" alt="Rclone UI">
|
||||
## Docker/Homelab/サーバー利用
|
||||
**リモート **`rclone`** インスタンスを管理する最も簡単な方法**で、サーバー、Homelab、家族の PC を管理しましょう。
|
||||
|
||||
### ⏰ スケジュール実行(cron)
|
||||
### 📊 Job Logs(すべてのバックグラウンド処理を可視化)
|
||||
### 🚀 OS 起動時に自動起動(autostart)・自動マウント(automount)
|
||||
### 🔒 Settings パネルにパスワードを設定
|
||||
### 👁️ トレイメニューからコマンドや Remote を非表示に
|
||||
### ⚙️ 設定のインポート/エクスポート(`pass` または `command` で暗号化)
|
||||
### ✍️ Windows は Microsoft 署名済み、macOS は公証(Notarized)済み
|
||||
#### Docker Compose
|
||||
```yaml
|
||||
services:
|
||||
rclone:
|
||||
image: rclone/rclone
|
||||
container_name: rclone
|
||||
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)**
|
||||
@@ -79,6 +100,7 @@
|
||||
- Scoop **`scoop bucket add extras`** と **`scoop install rclone-ui`**
|
||||
- Chocolatey **`choco install rclone-ui`**
|
||||
- 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)**)
|
||||
@@ -100,7 +122,15 @@
|
||||
</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 />
|
||||
|
||||
|
||||
@@ -50,28 +50,49 @@
|
||||
|
||||
<br />
|
||||
|
||||
## Features
|
||||
### 🖥️ Runs on all decks — Windows, Linux, macOS (x64, Arm, an’ Apple Silicon)
|
||||
### ✅ Sails with yer existing rclone (or we’ll fetch it fer ye) an’ keeps it shipshape (with yer aye-aye)
|
||||
### 🔗 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
|
||||
<a href="https://get.rcloneui.com/showcase">
|
||||
<img src=".github/rclone-video.png" alt="The GUI for Rclone">
|
||||
</a>
|
||||
|
||||
<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)
|
||||
### 📊 Ship’s Log fer Jobs (spy all background maneuvers)
|
||||
### 🚀 Weigh anchor on boot (autostart) an’ auto-mount the drives
|
||||
### 🔒 Guard the Captain’s Quarters (Settings) with a secret word
|
||||
### 👁️ Keep commands or Remotes hidden from the crow’s-nest tray
|
||||
### ⚙️ Import/Export Maps (configs) — encrypt ’em with pass or command
|
||||
### ✍️ Signed by the Crown o’ Microsoft an’ Notarized on macOS—legit as a Letter o’ Marque
|
||||
#### Docker Compose
|
||||
```yaml
|
||||
services:
|
||||
rclone:
|
||||
image: rclone/rclone
|
||||
container_name: rclone
|
||||
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
|
||||
- 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`**
|
||||
- Chocolatey **`choco install rclone-ui`**
|
||||
- WinGet **`winget install --id=RcloneUI.RcloneUI -e`**
|
||||
- NPM **`npx rclone-ui`**
|
||||
|
||||
## Downloads (hoist the booty!)
|
||||
- **Windows** (**[Arm](https://get.rcloneui.com/win-arm)**, **[x64](https://get.rcloneui.com/win)**)
|
||||
@@ -99,8 +121,16 @@
|
||||
</picture>
|
||||
</a>
|
||||
|
||||
## 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 🎉🏴☠️
|
||||
## Contribootin'
|
||||
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 />
|
||||
|
||||
|
||||
@@ -50,28 +50,49 @@
|
||||
|
||||
<br />
|
||||
|
||||
## Funkcje
|
||||
### 🖥️ Cross platform, działa na Windows, Linux, macOS (x64, ARM i Apple Silicon)
|
||||
### ✅ 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
|
||||
### 🔗 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
|
||||
<a href="https://get.rcloneui.com/showcase">
|
||||
<img src=".github/rclone-video.png" alt="The GUI for Rclone">
|
||||
</a>
|
||||
|
||||
<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)
|
||||
### 📊 Dzienniki zadań — wizualizacja wszystkich operacji w tle
|
||||
### 🚀 Uruchamianie przy starcie systemu (autostart) i automatyczne montowanie (automount)
|
||||
### 🔒 Ustawienie hasła do panelu ustawień
|
||||
### 👁️ Ukrywanie poleceń lub zdalnych zasobów (Remotes) w menu zasobnika
|
||||
### ⚙️ Import/Eksport konfiguracji (szyfrowany hasłem lub poleceniem)
|
||||
### ✍️ Podpisane cyfrowo przez Microsoft i notaryzowane na macOS
|
||||
#### Docker Compose
|
||||
```yaml
|
||||
services:
|
||||
rclone:
|
||||
image: rclone/rclone
|
||||
container_name: rclone
|
||||
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
|
||||
- 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`**
|
||||
- Chocolatey **`choco install rclone-ui`**
|
||||
- WinGet **`winget install --id=RcloneUI.RcloneUI -e`**
|
||||
- NPM **`npx rclone-ui`**
|
||||
|
||||
## Link do pobrania
|
||||
- **Windows** (**[Arm](https://get.rcloneui.com/win-arm)**, **[x64](https://get.rcloneui.com/win)**)
|
||||
@@ -100,7 +122,15 @@
|
||||
</a>
|
||||
|
||||
## 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 />
|
||||
|
||||
|
||||
@@ -50,28 +50,49 @@
|
||||
|
||||
<br />
|
||||
|
||||
## Features
|
||||
### 🖥️ Toate Platformele: Windows, Linux, macOS (x64, Arm și Apple Silicon)
|
||||
### ✅ Funcționează cu instalarea ta existentă de rclone (sau îl poate instala pentru tine) și îl menține up-to-date (cu aprobarea ta)
|
||||
### 🔗 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
|
||||
<a href="https://get.rcloneui.com/showcase">
|
||||
<img src=".github/rclone-video.png" alt="The GUI for Rclone">
|
||||
</a>
|
||||
|
||||
<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)
|
||||
### 📊 Job Logs (vizualizează toate operațiunile în background)
|
||||
### 🚀 Start on boot (autostart) și Mount on start (automount)
|
||||
### 🔒 Setează o parolă pentru panoul de Settings
|
||||
### 👁️ Ascunde comenzile sau Remotes din meniul tray
|
||||
### ⚙️ Import/Export Configs (criptate cu `pass` sau `command`)
|
||||
### ✍️ Signed by Microsoft și Notarized pe macOS
|
||||
#### Docker Compose
|
||||
```yaml
|
||||
services:
|
||||
rclone:
|
||||
image: rclone/rclone
|
||||
container_name: rclone
|
||||
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
|
||||
- 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`**
|
||||
- Chocolatey **`choco install rclone-ui`**
|
||||
- WinGet **`winget install --id=RcloneUI.RcloneUI -e`**
|
||||
- NPM **`npx rclone-ui`**
|
||||
|
||||
## Descarcă
|
||||
- **Windows** (**[Arm](https://get.rcloneui.com/win-arm)**, **[x64](https://get.rcloneui.com/win)**)
|
||||
@@ -100,7 +122,15 @@
|
||||
</a>
|
||||
|
||||
## 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 />
|
||||
|
||||
|
||||
@@ -50,28 +50,49 @@
|
||||
|
||||
<br />
|
||||
|
||||
## Funciones
|
||||
### 🖥️ Multiplataforma: Windows, Linux, macOS (x64, Arm y Apple Silicon)
|
||||
### ✅ Funciona con tu instalación existente de rclone (o puede instalar rclone por ti) y lo mantiene actualizado (con tu aprobación)
|
||||
### 🔗 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
|
||||
<a href="https://get.rcloneui.com/showcase">
|
||||
<img src=".github/rclone-video.png" alt="The GUI for Rclone">
|
||||
</a>
|
||||
|
||||
<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)
|
||||
### 📊 Registros de trabajos (visualiza todas las operaciones en segundo plano)
|
||||
### 🚀 Inicio con el sistema (autostart) y montaje al iniciar (automount)
|
||||
### 🔒 Protege el panel de Configuración con contraseña
|
||||
### 👁️ Oculta comandos o Remotes del menú de la bandeja del sistema (tray)
|
||||
### ⚙️ Import/Export de configuraciones (cifradas con pass o command)
|
||||
### ✍️ Firmado por Microsoft y notarizado en macOS
|
||||
#### Docker Compose
|
||||
```yaml
|
||||
services:
|
||||
rclone:
|
||||
image: rclone/rclone
|
||||
container_name: rclone
|
||||
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
|
||||
- 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`**
|
||||
- Chocolatey **`choco install rclone-ui`**
|
||||
- WinGet **`winget install --id=RcloneUI.RcloneUI -e`**
|
||||
- NPM **`npx rclone-ui`**
|
||||
|
||||
## Descargas
|
||||
- **Windows** (**[Arm](https://get.rcloneui.com/win-arm)**, **[x64](https://get.rcloneui.com/win)**)
|
||||
@@ -100,7 +122,15 @@
|
||||
</a>
|
||||
|
||||
## 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 />
|
||||
|
||||
|
||||
@@ -14,10 +14,12 @@
|
||||
"complexity": {
|
||||
"all": true,
|
||||
"noUselessTypeConstraint": "off",
|
||||
"useSimplifiedLogicExpression": "off"
|
||||
"useSimplifiedLogicExpression": "off",
|
||||
"noExcessiveCognitiveComplexity": "off"
|
||||
},
|
||||
"correctness": {
|
||||
"all": true
|
||||
"all": true,
|
||||
"useImportExtensions": "off"
|
||||
},
|
||||
"performance": {
|
||||
"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>
|
||||
<html lang="en" class="bg-neutral-900 dark">
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<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>
|
||||
|
||||
<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'
|
||||
|
||||
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) {
|
||||
if (bytes < 1024) {
|
||||
return `${bytes} B`
|
||||
}
|
||||
|
||||
if (bytes < 1024 * 1024) {
|
||||
return `${(bytes / 1024).toFixed(2)} KB`
|
||||
return `${Number.parseFloat((bytes / 1024).toFixed(2))} KB`
|
||||
}
|
||||
|
||||
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) {
|
||||
@@ -30,15 +35,26 @@ export function replaceSmartQuotes(value: string) {
|
||||
|
||||
export function getRemoteName(path?: string) {
|
||||
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]
|
||||
}
|
||||
|
||||
export function buildReadablePath(path: string, type: 'short' | 'long' = 'long') {
|
||||
console.log('[buildReadablePath] path', path)
|
||||
console.log('[buildReadablePath] sep()', sep())
|
||||
|
||||
if (!path) {
|
||||
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') {
|
||||
return lastSegment
|
||||
@@ -47,6 +63,34 @@ export function buildReadablePath(path: string, type: 'short' | 'long' = 'long')
|
||||
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) {
|
||||
console.log('[getConfigParentFolder] path', path)
|
||||
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')
|
||||
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 { fetch } from '@tauri-apps/plugin-http'
|
||||
import { platform } from '@tauri-apps/plugin-os'
|
||||
import { usePersistedStore } from './store'
|
||||
import { usePersistedStore } from '../store/persisted'
|
||||
|
||||
export async function validateLicense(licenseKey: string) {
|
||||
console.log('[validateLicense]')
|
||||
@@ -29,7 +29,7 @@ export async function validateLicense(licenseKey: string) {
|
||||
platform: platform(),
|
||||
}),
|
||||
})
|
||||
.then((r) => r.json())
|
||||
.then((r) => r.json() as Promise<{ error: string; valid: boolean }>)
|
||||
.catch((e) => {
|
||||
console.error('[validateLicense] failed to validate license')
|
||||
console.error(JSON.stringify(e))
|
||||
@@ -76,7 +76,7 @@ export async function revokeMachineLicense(licenseKey: string) {
|
||||
id,
|
||||
}),
|
||||
})
|
||||
.then((r) => r.json())
|
||||
.then((r) => r.json() as Promise<{ error: string; revoked: boolean }>)
|
||||
.catch((e) => {
|
||||
console.error('[revokeMachineLicense] failed to revoke license, fetch failed')
|
||||
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 {
|
||||
isPermissionGranted,
|
||||
requestPermission,
|
||||
@@ -17,5 +18,10 @@ export default async function notify({ title, body }: { title: string; body: str
|
||||
title,
|
||||
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 { 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 createRCDClient from 'rclone-sdk'
|
||||
import { useHostStore } from '../../store/host'
|
||||
import type { FlagValue } from '../../types/rclone'
|
||||
import { getConfigParentFolder } from '../format'
|
||||
import { usePersistedStore } from '../store'
|
||||
import rclone from './client'
|
||||
import { DOUBLE_BACKSLASH_REGEX } from './constants'
|
||||
|
||||
export async function getDefaultPaths() {
|
||||
console.log('[getDefaultPaths]')
|
||||
|
||||
const r = await fetch('http://localhost:5572/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 }
|
||||
const defaultPaths = await rclone('/config/paths')
|
||||
|
||||
console.log('[getDefaultPaths] json', JSON.stringify(defaultPaths, null, 2))
|
||||
|
||||
@@ -33,6 +27,7 @@ export async function getDefaultPaths() {
|
||||
|
||||
export async function getSystemConfigPath() {
|
||||
console.log('[getSystemConfigPath] running system rclone')
|
||||
|
||||
const instance = Command.create('rclone-system', [
|
||||
'rcd',
|
||||
'--rc-no-auth',
|
||||
@@ -53,13 +48,20 @@ export async function getSystemConfigPath() {
|
||||
await new Promise((resolve) => setTimeout(resolve, 200))
|
||||
|
||||
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')
|
||||
}
|
||||
|
||||
return defaultPaths.config
|
||||
return configPath.replace(DOUBLE_BACKSLASH_REGEX, '\\')
|
||||
} catch (error) {
|
||||
console.error('[getSystemConfigPath] error', 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)
|
||||
|
||||
return options
|
||||
@@ -247,7 +249,7 @@ export function shouldUpdateRclone(versionData: { yours: string; latest: string
|
||||
console.log('[shouldUpdateRclone] current version:', currentVersion)
|
||||
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')
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -1,83 +1,177 @@
|
||||
export const RCLONE_CONFIG_DEFAULTS = {
|
||||
'MultiThreadCutoff': '64M',
|
||||
'MultiThreadStreams': 8,
|
||||
'MultiThreadChunkSize': '8M',
|
||||
'UseListR': true,
|
||||
'UseServerModTime': true,
|
||||
'BufferSize': '32M',
|
||||
'Transfers': 8,
|
||||
'Checkers': 16,
|
||||
}
|
||||
|
||||
export const RCLONE_VFS_DEFAULTS = {
|
||||
'ChunkSize': '4M',
|
||||
'ChunkStreams': 16,
|
||||
}
|
||||
copy: {
|
||||
'multi_thread_cutoff': '64M',
|
||||
'multi_thread_streams': 8,
|
||||
'multi_thread_chunk_size': '8M',
|
||||
},
|
||||
config: {
|
||||
'fast_list': true,
|
||||
'use_server_modtime': true,
|
||||
'buffer_size': '32M',
|
||||
'transfers': 8,
|
||||
'checkers': 16,
|
||||
},
|
||||
vfs: {
|
||||
'chunk_size': '4M',
|
||||
'chunk_streams': 16,
|
||||
},
|
||||
} as const
|
||||
|
||||
export const RCLONE_CONF_REGEX = /[\/\\]rclone\.conf$/
|
||||
export const DOUBLE_BACKSLASH_REGEX = /\\\\/g
|
||||
|
||||
export const SUPPORTED_BACKENDS = [
|
||||
'azureblob',
|
||||
'azurefiles',
|
||||
export const SERVE_TYPES = ['dlna', 'ftp', 'sftp', 'http', 'nfs', 'restic', 's3', 'webdav'] as const
|
||||
|
||||
export const SUPPORTS_CLEANUP = [
|
||||
's3',
|
||||
'b2',
|
||||
'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',
|
||||
'quatrix',
|
||||
'sugarsync',
|
||||
'hidrive',
|
||||
'drive',
|
||||
'internetarchive',
|
||||
'jottacloud',
|
||||
'mailru',
|
||||
'mega',
|
||||
'onedrive',
|
||||
'oos',
|
||||
'pcloud',
|
||||
'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',
|
||||
'putio',
|
||||
'sharefile',
|
||||
'protondrive',
|
||||
'quatrix',
|
||||
'seafile',
|
||||
'sugarsync',
|
||||
'storj',
|
||||
'webdav',
|
||||
'yandex',
|
||||
'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 { fetch } from '@tauri-apps/plugin-http'
|
||||
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 { useHostStore } from '../../store/host'
|
||||
import { useStore } from '../../store/memory'
|
||||
import { usePersistedStore } from '../../store/persisted'
|
||||
import { getConfigParentFolder } from '../format'
|
||||
import { usePersistedStore, useStore } from '../store'
|
||||
import { openSmallWindow } from '../window'
|
||||
import { ensureEncryptedConfigEnv } from './cli'
|
||||
import {
|
||||
createConfigFile,
|
||||
getConfigPath,
|
||||
@@ -25,7 +28,7 @@ import {
|
||||
export async function initRclone(args: string[]) {
|
||||
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)
|
||||
let internal = await isInternalRcloneInstalled()
|
||||
console.log('[initRclone] internal rclone installed:', internal)
|
||||
@@ -33,8 +36,7 @@ export async function initRclone(args: string[]) {
|
||||
// rclone not available, let's download it
|
||||
if (!system && !internal) {
|
||||
console.log('[initRclone] no rclone installation found, provisioning...')
|
||||
useStore.setState({ startupDisplayed: true })
|
||||
useStore.setState({ startupStatus: 'initializing' })
|
||||
useStore.setState({ startupDisplayed: true, startupStatus: 'initializing' })
|
||||
await openSmallWindow({
|
||||
name: 'Startup',
|
||||
url: '/startup',
|
||||
@@ -46,6 +48,7 @@ export async function initRclone(args: string[]) {
|
||||
useStore.setState({ startupStatus: 'fatal' })
|
||||
return
|
||||
}
|
||||
|
||||
console.log('[initRclone] provision succeeded')
|
||||
useStore.setState({ startupStatus: 'initialized' })
|
||||
|
||||
@@ -92,7 +95,7 @@ export async function initRclone(args: string[]) {
|
||||
console.log('[initRclone] user skipping version:', skipping)
|
||||
if (skipping) {
|
||||
console.log('[initRclone] saving skipped version:', rcloneVersion!.yours)
|
||||
usePersistedStore.setState({ lastSkippedVersion: rcloneVersion!.yours })
|
||||
useHostStore.setState({ lastSkippedVersion: rcloneVersion!.yours })
|
||||
}
|
||||
} else {
|
||||
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)
|
||||
useStore.setState({ startupStatus: 'error' })
|
||||
}
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000))
|
||||
}
|
||||
|
||||
const persistedState = usePersistedStore.getState()
|
||||
let configFiles = persistedState.configFiles || []
|
||||
const hostState = useHostStore.getState()
|
||||
let configFiles = hostState.configFiles || []
|
||||
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)
|
||||
|
||||
if (system) {
|
||||
@@ -135,18 +140,24 @@ export async function initRclone(args: string[]) {
|
||||
console.log('[initRclone] created system config file')
|
||||
}
|
||||
|
||||
const existingDefaultConfig = configFiles.find((config) => config.id === 'default')
|
||||
configFiles = configFiles.filter((config) => config.id !== 'default')
|
||||
console.log('[initRclone] filtered config files, remaining count:', configFiles.length)
|
||||
configFiles.unshift({
|
||||
|
||||
const defaultConfig = existingDefaultConfig
|
||||
? existingDefaultConfig
|
||||
: {
|
||||
id: 'default',
|
||||
label: 'Default config',
|
||||
sync: undefined,
|
||||
isEncrypted: false,
|
||||
pass: undefined,
|
||||
passCommand: undefined,
|
||||
})
|
||||
}
|
||||
|
||||
configFiles.unshift(defaultConfig)
|
||||
console.log('[initRclone] added default config to list')
|
||||
usePersistedStore.setState({ configFiles })
|
||||
useHostStore.setState({ configFiles })
|
||||
|
||||
if (!activeConfigFile) {
|
||||
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)
|
||||
usePersistedStore.setState({ activeConfigFile })
|
||||
useHostStore.setState({ activeConfigFile })
|
||||
}
|
||||
|
||||
if (internal && activeConfigFile.id === 'default') {
|
||||
@@ -196,12 +207,12 @@ export async function initRclone(args: string[]) {
|
||||
await getConfigPath({ id: 'default', validate: true })
|
||||
)
|
||||
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
|
||||
console.log('[initRclone] password configured:', !!password)
|
||||
const passwordConfigured = activeConfigFile.pass || activeConfigFile.passCommand || null
|
||||
console.log('[initRclone] password configured:', !!passwordConfigured)
|
||||
try {
|
||||
console.log('[initRclone] reading config file', configPath)
|
||||
const configContent = await readTextFile(configPath)
|
||||
@@ -211,22 +222,10 @@ export async function initRclone(args: string[]) {
|
||||
|
||||
if (isEncrypted) {
|
||||
console.log('[initRclone] config file is encrypted')
|
||||
if (password) {
|
||||
console.log('[initRclone] using existing password')
|
||||
if (passwordConfigured) {
|
||||
console.log('[initRclone] using existing password configuration')
|
||||
} else {
|
||||
console.log('[initRclone] prompting user for password')
|
||||
password = await promptForConfigPassword(activeConfigFile.label)
|
||||
console.log('[initRclone] password received:', !!password)
|
||||
|
||||
if (!password) {
|
||||
console.error('[initRclone] no password provided, exiting')
|
||||
await message('Password is required for encrypted configurations.', {
|
||||
title: 'Password Required',
|
||||
kind: 'error',
|
||||
okLabel: 'OK',
|
||||
})
|
||||
await exit(0)
|
||||
return
|
||||
console.log('[initRclone] no stored password configured')
|
||||
}
|
||||
|
||||
if (!activeConfigFile.isEncrypted) {
|
||||
@@ -235,7 +234,7 @@ export async function initRclone(args: string[]) {
|
||||
const updatedConfigFiles = configFiles.map((config) =>
|
||||
config.id === activeConfigFile!.id ? updatedConfigFile : config
|
||||
)
|
||||
usePersistedStore.setState({
|
||||
useHostStore.setState({
|
||||
configFiles: updatedConfigFiles,
|
||||
activeConfigFile: updatedConfigFile,
|
||||
})
|
||||
@@ -244,14 +243,13 @@ export async function initRclone(args: string[]) {
|
||||
// Update activeConfigFile reference for the rest of the function
|
||||
activeConfigFile = updatedConfigFile
|
||||
}
|
||||
}
|
||||
} else if (activeConfigFile.isEncrypted) {
|
||||
console.log('[initRclone] config file is not encrypted, clearing encryption flag')
|
||||
const updatedConfigFile = { ...activeConfigFile, isEncrypted: false }
|
||||
const updatedConfigFiles = configFiles.map((config) =>
|
||||
config.id === activeConfigFile!.id ? updatedConfigFile : config
|
||||
)
|
||||
usePersistedStore.setState({
|
||||
useHostStore.setState({
|
||||
configFiles: updatedConfigFiles,
|
||||
activeConfigFile: updatedConfigFile,
|
||||
})
|
||||
@@ -279,11 +277,11 @@ export async function initRclone(args: string[]) {
|
||||
env: {},
|
||||
}
|
||||
|
||||
if (persistedState.proxy) {
|
||||
console.log('[initRclone] proxy configured:', persistedState.proxy.url)
|
||||
if (hostState.proxy) {
|
||||
console.log('[initRclone] proxy configured:', hostState.proxy.url)
|
||||
try {
|
||||
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')
|
||||
} catch (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')
|
||||
extraParams.env.http_proxy = persistedState.proxy.url
|
||||
extraParams.env.https_proxy = persistedState.proxy.url
|
||||
extraParams.env.HTTP_PROXY = persistedState.proxy.url
|
||||
extraParams.env.HTTPS_PROXY = persistedState.proxy.url
|
||||
extraParams.env.no_proxy = persistedState.proxy.ignoredHosts.join(',')
|
||||
extraParams.env.NO_PROXY = persistedState.proxy.ignoredHosts.join(',')
|
||||
extraParams.env.http_proxy = hostState.proxy.url
|
||||
extraParams.env.https_proxy = hostState.proxy.url
|
||||
extraParams.env.HTTP_PROXY = hostState.proxy.url
|
||||
extraParams.env.HTTPS_PROXY = hostState.proxy.url
|
||||
extraParams.env.no_proxy = hostState.proxy.ignoredHosts.join(',')
|
||||
extraParams.env.NO_PROXY = hostState.proxy.ignoredHosts.join(',')
|
||||
console.log(
|
||||
'[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') {
|
||||
console.log('[initRclone] setting custom config path:', configFolderPath)
|
||||
extraParams.env.RCLONE_CONFIG_DIR = configFolderPath
|
||||
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)
|
||||
|
||||
if (system) {
|
||||
@@ -576,30 +600,3 @@ export async function provisionRclone() {
|
||||
|
||||
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 { revealItemInDir } from '@tauri-apps/plugin-opener'
|
||||
import { platform } from '@tauri-apps/plugin-os'
|
||||
import { Command } from '@tauri-apps/plugin-shell'
|
||||
|
||||
export async function 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,152 +1,171 @@
|
||||
import * as Sentry from '@sentry/browser'
|
||||
import { Menu } from '@tauri-apps/api/menu'
|
||||
import { MenuItem } from '@tauri-apps/api/menu'
|
||||
import { invoke } from '@tauri-apps/api/core'
|
||||
import { Menu, MenuItem, PredefinedMenuItem } from '@tauri-apps/api/menu'
|
||||
import { resolveResource } from '@tauri-apps/api/path'
|
||||
import type { TrayIconEvent } from '@tauri-apps/api/tray'
|
||||
import { TrayIcon } from '@tauri-apps/api/tray'
|
||||
import { getAllWindows, getCurrentWindow } from '@tauri-apps/api/window'
|
||||
import { ask } from '@tauri-apps/plugin-dialog'
|
||||
import { TrayIcon, type TrayIconEvent } from '@tauri-apps/api/tray'
|
||||
import { getCurrentWindow } from '@tauri-apps/api/window'
|
||||
import { ask, message } from '@tauri-apps/plugin-dialog'
|
||||
import { openUrl } from '@tauri-apps/plugin-opener'
|
||||
import { platform } from '@tauri-apps/plugin-os'
|
||||
import { buildMenu } from './menu'
|
||||
import { usePersistedStore } from './store'
|
||||
import { resetMainWindow } from './window'
|
||||
import { exit } from '@tauri-apps/plugin-process'
|
||||
import { usePersistedStore } from '../store/persisted'
|
||||
import { openSmallWindow, openWindow } from './window'
|
||||
|
||||
export async function triggerTrayRebuild() {
|
||||
return getAllWindows().then((windows) => {
|
||||
windows.find((w) => w.label === 'main')?.emit('rebuild-tray')
|
||||
async function buildMenu() {
|
||||
console.log('[buildMenu]')
|
||||
|
||||
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() {
|
||||
return await TrayIcon.getById('main-tray')
|
||||
await PredefinedMenuItem.new({
|
||||
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() {
|
||||
console.log('[resolveTrayIconForTheme]')
|
||||
|
||||
const existingTheme = usePersistedStore.getState().themeV2
|
||||
const existingTheme = usePersistedStore.getState().appearance
|
||||
console.log('[resolveTrayIconForTheme] existingTheme', existingTheme)
|
||||
|
||||
if (platform() !== 'macos' && 'tray' in existingTheme && existingTheme.tray) {
|
||||
const pickedPath =
|
||||
existingTheme.tray === 'dark'
|
||||
? 'icons/favicon/icon.png'
|
||||
: 'icons/favicon/icon-light.png'
|
||||
|
||||
return await resolveResource(pickedPath)
|
||||
if (existingTheme.tray === 'system') {
|
||||
if (platform() === 'macos') {
|
||||
return await resolveResource('icons/favicon/icon.png')
|
||||
}
|
||||
|
||||
let theme: 'light' | 'dark' = 'dark'
|
||||
|
||||
if (['windows', 'macos'].includes(platform())) {
|
||||
try {
|
||||
const currentWindow = getCurrentWindow()
|
||||
const windowTheme = await currentWindow.theme()
|
||||
console.log('[resolveTrayIconForTheme] windowTheme', windowTheme)
|
||||
|
||||
theme = windowTheme === 'dark' ? 'dark' : 'light'
|
||||
} catch {}
|
||||
} 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) {
|
||||
theme = 'dark'
|
||||
} else {
|
||||
theme = 'light'
|
||||
}
|
||||
}
|
||||
|
||||
console.log('[resolveTrayIconForTheme] theme', theme)
|
||||
|
||||
usePersistedStore.setState({ themeV2: { tray: theme } })
|
||||
|
||||
const pickedPath = theme === 'dark' ? 'icons/favicon/icon.png' : 'icons/favicon/icon-light.png'
|
||||
console.log('[resolveTrayIconForTheme] pickedPath', pickedPath)
|
||||
const pickedPath =
|
||||
windowTheme === 'dark' ? 'icons/favicon/icon.png' : 'icons/favicon/icon-light.png'
|
||||
|
||||
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')
|
||||
}
|
||||
},
|
||||
} catch {
|
||||
await message('Failed to get window theme', {
|
||||
title: 'Error',
|
||||
kind: 'error',
|
||||
})
|
||||
|
||||
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
|
||||
await new Promise((resolve) => setTimeout(resolve, 5000))
|
||||
await exit()
|
||||
}
|
||||
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 pickedPath =
|
||||
existingTheme.tray === 'dark' ? 'icons/favicon/icon-light.png' : 'icons/favicon/icon.png'
|
||||
|
||||
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')
|
||||
return await resolveResource(pickedPath)
|
||||
}
|
||||
|
||||
export async function initTray() {
|
||||
@@ -155,17 +174,16 @@ export async function initTray() {
|
||||
try {
|
||||
console.log('[initTray]')
|
||||
|
||||
const initialIcon = await resolveResource('icons/favicon/icon.png')
|
||||
await TrayIcon.new({
|
||||
id: 'main-tray',
|
||||
icon: initialIcon,
|
||||
id: 'rclone-menu5',
|
||||
menu: await buildMenu(),
|
||||
icon: await resolveTrayIconForTheme(),
|
||||
tooltip: 'Rclone',
|
||||
menuOnLeftClick: true,
|
||||
iconAsTemplate: true,
|
||||
action: async (event: TrayIconEvent) => {
|
||||
if (event.type === 'Click') {
|
||||
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 {
|
||||
LogicalSize,
|
||||
PhysicalSize,
|
||||
availableMonitors,
|
||||
currentMonitor,
|
||||
getAllWindows,
|
||||
} from '@tauri-apps/api/window'
|
||||
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({
|
||||
name,
|
||||
@@ -28,53 +10,8 @@ export async function openFullWindow({
|
||||
url: string
|
||||
}) {
|
||||
console.log('[openFullWindow] ', name, url)
|
||||
|
||||
const w = new WebviewWindow(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
|
||||
await invoke('open_full_window', { name, url })
|
||||
return WebviewWindow.getByLabel(name)
|
||||
}
|
||||
|
||||
export async function openWindow({
|
||||
@@ -89,44 +26,8 @@ export async function openWindow({
|
||||
height?: number
|
||||
}) {
|
||||
console.log('[openWindow] ', name, url)
|
||||
|
||||
const isFirstWindow = useStore.getState().firstWindow
|
||||
|
||||
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
|
||||
await invoke('open_window', { name, url, width, height })
|
||||
return WebviewWindow.getByLabel(name)
|
||||
}
|
||||
|
||||
export async function openSmallWindow({
|
||||
@@ -137,51 +38,14 @@ export async function openSmallWindow({
|
||||
url: string
|
||||
}) {
|
||||
console.log('[openSmallWindow] ', name, url)
|
||||
|
||||
const isFirstWindow = useStore.getState().firstWindow
|
||||
|
||||
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
|
||||
await invoke('open_small_window', { name, url })
|
||||
return WebviewWindow.getByLabel(name)
|
||||
}
|
||||
|
||||
export async function lockWindows(ids?: string[]) {
|
||||
const windows = await getAllWindows()
|
||||
const lockedWindows = ids ? windows.filter((w) => ids.includes(w.label)) : windows
|
||||
await Promise.all(lockedWindows.map((w) => w.setClosable(false)))
|
||||
await invoke('lock_windows', { ids: ids ?? null })
|
||||
}
|
||||
|
||||
export async function unlockWindows(ids?: string[]) {
|
||||
const windows = await getAllWindows()
|
||||
const unlockedWindows = ids ? windows.filter((w) => ids.includes(w.label)) : windows
|
||||
await Promise.all(unlockedWindows.map((w) => w.setClosable(true)))
|
||||
await invoke('unlock_windows', { ids: ids ?? null })
|
||||
}
|
||||
|
||||
@@ -2,34 +2,43 @@ import * as Sentry from '@sentry/browser'
|
||||
import { getVersion as getUiVersion } from '@tauri-apps/api/app'
|
||||
import { invoke } from '@tauri-apps/api/core'
|
||||
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 { debug, error, info, trace, warn } from '@tauri-apps/plugin-log'
|
||||
import { platform } from '@tauri-apps/plugin-os'
|
||||
import { exit, relaunch } from '@tauri-apps/plugin-process'
|
||||
import type { Child } from '@tauri-apps/plugin-shell'
|
||||
import { check } from '@tauri-apps/plugin-updater'
|
||||
import { CronExpressionParser } from 'cron-parser'
|
||||
import { defaultOptions } from 'tauri-plugin-sentry-api'
|
||||
import { getDeepLinkUrl, handleDeepLinkUrl } from './lib/deep'
|
||||
import { isDirectoryEmpty } from './lib/fs'
|
||||
import { LOCAL_HOST_ID, getHostInfo } from './lib/hosts'
|
||||
import { validateLicense } from './lib/license'
|
||||
import notify from './lib/notify'
|
||||
import queryClient from './lib/query'
|
||||
import {
|
||||
getRemote,
|
||||
listJobs,
|
||||
listRemotes,
|
||||
mountRemote,
|
||||
listTransfers,
|
||||
startBisync,
|
||||
startCopy,
|
||||
startDelete,
|
||||
startMount,
|
||||
startMove,
|
||||
startPurge,
|
||||
startSync,
|
||||
} from './lib/rclone/api'
|
||||
import rcloneClient from './lib/rclone/client'
|
||||
import { compareVersions } from './lib/rclone/common'
|
||||
import { SUPPORTED_BACKENDS } from './lib/rclone/constants'
|
||||
import { initRclone } from './lib/rclone/init'
|
||||
import { usePersistedStore, useStore } from './lib/store'
|
||||
import { initTray, showDefaultTray, showLoadingTray } from './lib/tray'
|
||||
import { initTray } from './lib/tray'
|
||||
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 {
|
||||
Sentry.init({
|
||||
@@ -76,32 +85,99 @@ async function waitForHydration() {
|
||||
console.log('[waitForHydration] store hydrated')
|
||||
}
|
||||
|
||||
async function validateInstance() {
|
||||
console.log('[validateInstance]')
|
||||
async function initializeHostStore() {
|
||||
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') {
|
||||
await message(
|
||||
'You are not connected to the internet. Please check your connection and try again.',
|
||||
console.log('[initializeHostStore] initialized for', hostId)
|
||||
}
|
||||
|
||||
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',
|
||||
kind: 'error',
|
||||
okLabel: 'Exit',
|
||||
title: 'Host Not Reachable',
|
||||
kind: 'warning',
|
||||
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
|
||||
if (!licenseKey) {
|
||||
console.log('[validateInstance] no license key, skipping license validation')
|
||||
usePersistedStore.setState({ licenseValid: false })
|
||||
return
|
||||
}
|
||||
|
||||
if (!navigator.onLine) {
|
||||
console.log('[validateInstance] not online, skipping license validation')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
await validateLicense(licenseKey)
|
||||
} catch (e) {
|
||||
console.log('[validateInstance] error validating license, marking as invalid')
|
||||
usePersistedStore.setState({ licenseValid: false })
|
||||
|
||||
if (e instanceof Error) {
|
||||
@@ -110,6 +186,7 @@ async function validateInstance() {
|
||||
kind: 'error',
|
||||
okLabel: 'OK',
|
||||
})
|
||||
console.log('[validateInstance] error message displayed, returning')
|
||||
return
|
||||
}
|
||||
|
||||
@@ -118,6 +195,9 @@ async function validateInstance() {
|
||||
kind: 'error',
|
||||
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')
|
||||
|
||||
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',
|
||||
'kind': 'info',
|
||||
@@ -158,7 +238,7 @@ async function checkAlreadyRunning() {
|
||||
}
|
||||
const result = await invoke('stop_rclone_processes')
|
||||
console.log('[checkAlreadyRunning] stop_rclone_processes', result)
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000))
|
||||
await new Promise((resolve) => setTimeout(resolve, 700))
|
||||
} else {
|
||||
console.log('[checkAlreadyRunning] exiting')
|
||||
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() {
|
||||
console.log('[startRclone]')
|
||||
|
||||
try {
|
||||
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)
|
||||
}
|
||||
await registerRcloneWindowListeners()
|
||||
|
||||
let rclone: Awaited<ReturnType<typeof initRclone>> | null = null
|
||||
|
||||
try {
|
||||
const sessionPassword = Math.random().toString(36).substring(2, 15)
|
||||
useStore.setState({ rcloneAuth: sessionPassword })
|
||||
useStore.setState({ rcloneAuthHeader: 'Basic ' + btoa(`admin:${sessionPassword}`) })
|
||||
|
||||
rclone = await initRclone([
|
||||
'rcd',
|
||||
// ...(platform() === 'macos'
|
||||
@@ -225,15 +415,9 @@ async function startRclone() {
|
||||
|
||||
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) => {
|
||||
console.log('close', event)
|
||||
currentRcloneChild = null
|
||||
|
||||
if (platform() === 'windows') {
|
||||
return await exit(0)
|
||||
@@ -262,105 +446,49 @@ async function startRclone() {
|
||||
|
||||
console.log('[startRclone] starting rclone')
|
||||
const childProcess = await command.spawn()
|
||||
currentRcloneChild = childProcess
|
||||
console.log('[startRclone] running rclone')
|
||||
|
||||
getCurrentWindow().listen('close-app', async (e) => {
|
||||
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`
|
||||
await new Promise((resolve) => setTimeout(resolve, 500))
|
||||
}
|
||||
|
||||
async function startupMounts() {
|
||||
const remoteConfigList = usePersistedStore.getState().remoteConfigList
|
||||
const remotes = useStore.getState().remotes
|
||||
const remoteConfigList = useHostStore.getState().remoteConfigs
|
||||
|
||||
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) {
|
||||
const remoteConfig = remoteConfigList[remote]
|
||||
if (!remoteConfig) continue
|
||||
if (remoteConfig.mountOnStart && remoteConfig.defaultMountPoint) {
|
||||
if (remoteConfig.mountOnStart?.enabled && remoteConfig.mountOnStart?.mountPoint) {
|
||||
try {
|
||||
const isEmpty = await isDirectoryEmpty(remoteConfig.defaultMountPoint)
|
||||
const isEmpty = await isDirectoryEmpty(remoteConfig.mountOnStart.mountPoint)
|
||||
if (!isEmpty) {
|
||||
continue
|
||||
}
|
||||
|
||||
await mountRemote({
|
||||
remotePath: `${remote}:${remoteConfig?.defaultRemotePath || ''}`,
|
||||
mountPoint: remoteConfig.defaultMountPoint,
|
||||
mountOptions: remoteConfig?.mountDefaults,
|
||||
vfsOptions: remoteConfig?.vfsDefaults,
|
||||
const {
|
||||
mountPoint,
|
||||
remotePath,
|
||||
mountOptions,
|
||||
vfsOptions,
|
||||
filterOptions,
|
||||
configOptions,
|
||||
} = remoteConfig.mountOnStart
|
||||
|
||||
await startMount({
|
||||
source: `${remote}:${remotePath}`,
|
||||
destination: mountPoint,
|
||||
options: {
|
||||
mount: mountOptions,
|
||||
vfs: vfsOptions,
|
||||
filter: filterOptions,
|
||||
config: configOptions,
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Error mounting remote:', error)
|
||||
@@ -376,46 +504,61 @@ async function startupMounts() {
|
||||
}
|
||||
|
||||
async function showStartup() {
|
||||
console.log('[showStartup] showing startup')
|
||||
const hideStartup = usePersistedStore.getState().hideStartup
|
||||
if (hideStartup) {
|
||||
console.log('[showStartup] startup hidden, returning')
|
||||
return
|
||||
}
|
||||
|
||||
const startupDisplayed = useStore.getState().startupDisplayed
|
||||
if (!startupDisplayed) {
|
||||
if (startupDisplayed) {
|
||||
console.log('[showStartup] startup already displayed, returning')
|
||||
return
|
||||
}
|
||||
|
||||
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
|
||||
let hasScheduledTasks = false
|
||||
async function resumeTasks() {
|
||||
console.log('resuming tasks')
|
||||
console.log('[resumeTasks] resuming tasks')
|
||||
|
||||
if (hasScheduledTasks) {
|
||||
console.log('[resumeTasks] tasks already resumed')
|
||||
return
|
||||
}
|
||||
|
||||
const scheduledTasks = usePersistedStore.getState().scheduledTasks
|
||||
const activeConfigId = usePersistedStore.getState().activeConfigFile?.id
|
||||
const scheduledTasks = useHostStore.getState().scheduledTasks
|
||||
const activeConfigId = useHostStore.getState().activeConfigFile?.id
|
||||
|
||||
if (!activeConfigId) {
|
||||
console.log('[resumeTasks] no active config id')
|
||||
return
|
||||
}
|
||||
|
||||
hasScheduledTasks = true
|
||||
|
||||
console.log('[resumeTasks] resuming tasks for config', activeConfigId)
|
||||
|
||||
for (const task of scheduledTasks) {
|
||||
if (task.isRunning) {
|
||||
usePersistedStore.getState().updateScheduledTask(task.id, {
|
||||
useHostStore.getState().updateScheduledTask(task.id, {
|
||||
isRunning: false,
|
||||
currentRunId: undefined,
|
||||
error: 'Task closed prematurely',
|
||||
lastRunError: 'Task closed prematurely',
|
||||
})
|
||||
continue
|
||||
}
|
||||
@@ -430,15 +573,15 @@ async function resumeTasks() {
|
||||
const difference = nextRun.getTime() - Date.now()
|
||||
|
||||
if (difference <= MAX_INT_MS && difference > 0) {
|
||||
setTimeout(() => {
|
||||
setTimeout(async () => {
|
||||
console.log('running task', task)
|
||||
notify({
|
||||
title: 'Task Started',
|
||||
body: `Task ${task.type} (${task.id}) started`,
|
||||
body: `Task ${task.operation} (${task.id}) started`,
|
||||
})
|
||||
handleTask(task)
|
||||
await handleTask(task)
|
||||
}, difference)
|
||||
console.log('scheduled task', task.type, task.id, nextRun)
|
||||
console.log('scheduled task', task.operation, task.id, nextRun)
|
||||
}
|
||||
} catch (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) {
|
||||
const currentTask = usePersistedStore.getState().scheduledTasks.find((t) => t.id === task.id)
|
||||
const currentTask = useHostStore.getState().scheduledTasks.find((t) => t.id === task.id)
|
||||
|
||||
if (!currentTask) {
|
||||
return
|
||||
@@ -462,15 +605,15 @@ async function handleTask(task: ScheduledTask) {
|
||||
|
||||
const freshRunId = crypto.randomUUID()
|
||||
|
||||
usePersistedStore.getState().updateScheduledTask(task.id, {
|
||||
useHostStore.getState().updateScheduledTask(task.id, {
|
||||
isRunning: true,
|
||||
currentRunId: freshRunId,
|
||||
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()
|
||||
.scheduledTasks.find((t) => t.id === task.id)?.currentRunId
|
||||
|
||||
@@ -479,71 +622,69 @@ async function handleTask(task: ScheduledTask) {
|
||||
}
|
||||
|
||||
try {
|
||||
const {
|
||||
srcFs,
|
||||
dstFs,
|
||||
_config,
|
||||
_filter,
|
||||
createEmptySrcDirs,
|
||||
deleteEmptyDstDirs,
|
||||
fs,
|
||||
rmDirs,
|
||||
remote,
|
||||
} = task.args
|
||||
|
||||
switch (task.type) {
|
||||
case 'delete':
|
||||
await startDelete({
|
||||
fs,
|
||||
rmDirs,
|
||||
_filter,
|
||||
_config,
|
||||
})
|
||||
break
|
||||
case 'copy':
|
||||
switch (task.operation) {
|
||||
case 'copy': {
|
||||
const { sources, options, destination } = task.args
|
||||
await startCopy({
|
||||
srcFs,
|
||||
dstFs,
|
||||
_config,
|
||||
_filter,
|
||||
sources,
|
||||
destination,
|
||||
options,
|
||||
})
|
||||
break
|
||||
case 'move':
|
||||
}
|
||||
case 'move': {
|
||||
const { sources, options, destination } = task.args
|
||||
await startMove({
|
||||
srcFs,
|
||||
dstFs,
|
||||
createEmptySrcDirs,
|
||||
deleteEmptyDstDirs,
|
||||
_config,
|
||||
_filter,
|
||||
sources,
|
||||
destination,
|
||||
options,
|
||||
})
|
||||
break
|
||||
case 'sync':
|
||||
}
|
||||
case 'sync': {
|
||||
const { source, destination, options } = task.args
|
||||
await startSync({
|
||||
srcFs,
|
||||
dstFs,
|
||||
_config,
|
||||
_filter,
|
||||
source,
|
||||
destination,
|
||||
options,
|
||||
})
|
||||
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({
|
||||
fs,
|
||||
remote,
|
||||
_filter,
|
||||
_config,
|
||||
sources,
|
||||
options,
|
||||
})
|
||||
break
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
} catch (err) {
|
||||
Sentry.captureException(err)
|
||||
console.error('Failed to start task:', err)
|
||||
usePersistedStore.getState().updateScheduledTask(task.id, {
|
||||
useHostStore.getState().updateScheduledTask(task.id, {
|
||||
isRunning: false,
|
||||
currentRunId: undefined,
|
||||
error: err instanceof Error ? err.message : 'Unknown error',
|
||||
lastRunError: err instanceof Error ? err.message : 'Unknown error',
|
||||
})
|
||||
} finally {
|
||||
}
|
||||
@@ -560,7 +701,10 @@ async function checkVersion() {
|
||||
)
|
||||
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')
|
||||
|
||||
const { minimumVersion, okVersion } = metaJsonData
|
||||
@@ -666,35 +810,78 @@ async function checkVersion() {
|
||||
}
|
||||
}
|
||||
|
||||
async function checkTraySupport() {
|
||||
console.log('[checkTraySupport] platform', platform())
|
||||
async function checkRclone() {
|
||||
let currentHost = usePersistedStore.getState().currentHost
|
||||
|
||||
let traySupported = false
|
||||
|
||||
try {
|
||||
traySupported = await invoke<boolean>('is_tray_supported')
|
||||
} catch (error) {
|
||||
console.error('[checkTraySupport] error', error)
|
||||
Sentry.captureException(error)
|
||||
if (!currentHost) {
|
||||
currentHost = {
|
||||
id: 'local',
|
||||
name: 'Local Machine',
|
||||
url: 'http://localhost:5572',
|
||||
os: 'linux',
|
||||
cliVersion: 'unknown',
|
||||
}
|
||||
}
|
||||
|
||||
if (!traySupported) {
|
||||
console.log('[checkTraySupport] tray not supported')
|
||||
let hostInfo = await getHostInfo({
|
||||
url: currentHost.url,
|
||||
authUser: currentHost.authUser,
|
||||
authPassword: currentHost.authPassword,
|
||||
})
|
||||
|
||||
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?',
|
||||
if (!hostInfo) {
|
||||
await message(
|
||||
'Failed to get host info, is it online and reachable?\n\nSwitching to local host.',
|
||||
{
|
||||
title: 'Tray Not Supported',
|
||||
title: 'Host Not Reachable',
|
||||
kind: 'error',
|
||||
okLabel: 'Continue',
|
||||
cancelLabel: 'Exit Now',
|
||||
}
|
||||
)
|
||||
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) {
|
||||
await Promise.race([new Promise((resolve) => setTimeout(resolve, 100_000)), exit(0)])
|
||||
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) => {
|
||||
@@ -702,25 +889,52 @@ getCurrentWindow().listen('tauri://close-requested', async (e) => {
|
||||
await getCurrentWindow().destroy()
|
||||
})
|
||||
|
||||
getCurrentWindow().listen('rebuild-tray', async (e) => {
|
||||
console.log('(main) window rebuild-tray requested')
|
||||
// maybe place this inside handleDeepLink?
|
||||
onOpenUrl((urls) => {
|
||||
console.log('deep links while running', urls)
|
||||
const receivedUrl = urls[0]
|
||||
|
||||
// wait for store to be updated
|
||||
await new Promise((resolve) => setTimeout(resolve, 250))
|
||||
const deepLinkUrl = getDeepLinkUrl(receivedUrl)
|
||||
|
||||
await showDefaultTray()
|
||||
console.log('deep link url', deepLinkUrl)
|
||||
|
||||
handleDeepLinkUrl(deepLinkUrl)
|
||||
|
||||
useStore.setState({ startupDisplayed: true, startupStatus: 'initializing' })
|
||||
})
|
||||
|
||||
checkTraySupport()
|
||||
.then(() => initTray())
|
||||
.then(() => showLoadingTray())
|
||||
.then(() => waitForHydration())
|
||||
async function handleDeepLink() {
|
||||
console.log('[handleDeepLink] getting current deep links')
|
||||
const urls = await getCurrent()
|
||||
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(() => validateInstance())
|
||||
.then(() => checkAlreadyRunning())
|
||||
.then(() => startRclone())
|
||||
.then(() => checkRclone())
|
||||
.then(() => handleDeepLink())
|
||||
.then(() => showStartup())
|
||||
.then(() => startupMounts())
|
||||
.then(() => resumeTasks())
|
||||
.then(() => showDefaultTray())
|
||||
.then(() => initTray())
|
||||
.catch(console.error)
|
||||
|
||||
@@ -1,20 +1,24 @@
|
||||
{
|
||||
"name": "rclone-ui",
|
||||
"version": "2.8.1",
|
||||
"version": "3.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "rclone-ui",
|
||||
"version": "2.8.1",
|
||||
"version": "3.0.0",
|
||||
"dependencies": {
|
||||
"@formkit/auto-animate": "^0.9.0",
|
||||
"@heroui/react": "^2.7.11",
|
||||
"@mux/mux-player-react": "^3.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/plugin-autostart": "^2.5.1",
|
||||
"@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-fs": "^2.4.4",
|
||||
"@tauri-apps/plugin-http": "^2.5.4",
|
||||
@@ -27,14 +31,16 @@
|
||||
"@tauri-apps/plugin-store": "^2.4.1",
|
||||
"@tauri-apps/plugin-updater": "^2.9.0",
|
||||
"@tauri-apps/plugin-window-state": "^2.4.1",
|
||||
"canvas-confetti": "^1.9.4",
|
||||
"cron-parser": "^5.4.0",
|
||||
"cronstrue": "^3.9.0",
|
||||
"date-fns": "^4.1.0",
|
||||
"framer-motion": "^11.18.2",
|
||||
"fuse.js": "^7.1.0",
|
||||
"lucide-react": "^0.552.0",
|
||||
"p-retry": "^7.1.0",
|
||||
"rclone-sdk": "^1.72.0",
|
||||
"react": "^18.3.1",
|
||||
"react-compiler-runtime": "^19.1.0-rc.3",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-router-dom": "^6.28.1",
|
||||
"tauri-plugin-sentry-api": "^0.5.0",
|
||||
@@ -46,6 +52,8 @@
|
||||
"@biomejs/biome": "^1.9.4",
|
||||
"@iarna/toml": "^2.2.5",
|
||||
"@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/react": "^18.3.1",
|
||||
"@types/react-dom": "^18.3.1",
|
||||
@@ -89,7 +97,7 @@
|
||||
"version": "7.27.1",
|
||||
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz",
|
||||
"integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-validator-identifier": "^7.27.1",
|
||||
@@ -241,7 +249,7 @@
|
||||
"version": "7.27.1",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz",
|
||||
"integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"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"
|
||||
}
|
||||
},
|
||||
"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": {
|
||||
"version": "1.21.0",
|
||||
"resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.21.0.tgz",
|
||||
@@ -5265,6 +5336,59 @@
|
||||
"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": {
|
||||
"version": "3.11.3",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/react-virtual/-/react-virtual-3.11.3.tgz",
|
||||
@@ -5537,6 +5661,15 @@
|
||||
"@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": {
|
||||
"version": "2.4.2",
|
||||
"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"
|
||||
}
|
||||
},
|
||||
"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": {
|
||||
"version": "7.20.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
|
||||
@@ -5686,6 +5826,13 @@
|
||||
"@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": {
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
|
||||
@@ -5762,6 +5909,28 @@
|
||||
"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": {
|
||||
"version": "6.1.0",
|
||||
"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",
|
||||
"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": {
|
||||
"version": "10.4.21",
|
||||
"resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.21.tgz",
|
||||
@@ -5959,6 +6136,16 @@
|
||||
],
|
||||
"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": {
|
||||
"version": "1.1.11",
|
||||
"resolved": "https://registry.npmjs.org/castable-video/-/castable-video-1.1.11.tgz",
|
||||
@@ -5977,6 +6164,14 @@
|
||||
"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": {
|
||||
"version": "3.6.0",
|
||||
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
|
||||
@@ -6065,6 +6260,14 @@
|
||||
"integrity": "sha512-zW190nQTIoXcGCaU08DvVNFTmQhUpnJfVuAKfWqUQkflXKpaDdpaYoM0iluLS9lgJNHyBF58KKA2FBEwkD7wog==",
|
||||
"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": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz",
|
||||
@@ -6157,7 +6360,7 @@
|
||||
"version": "4.4.1",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz",
|
||||
"integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ms": "^2.1.3"
|
||||
@@ -6186,6 +6389,16 @@
|
||||
"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": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz",
|
||||
@@ -6265,6 +6478,14 @@
|
||||
"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": {
|
||||
"version": "3.3.2",
|
||||
"resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz",
|
||||
@@ -6395,6 +6616,15 @@
|
||||
"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": {
|
||||
"version": "1.0.0-beta.2",
|
||||
"resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
|
||||
@@ -6452,6 +6682,35 @@
|
||||
"integrity": "sha512-CSpT2aXsv71HST8C5ETeVo+6YybqCpHBiYrCRQSn3U5QUZuLTSsvtq/bj+zuvjLVADeKxoebzo16OkH8m1+65Q==",
|
||||
"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": {
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmjs.org/input-otp/-/input-otp-1.4.1.tgz",
|
||||
@@ -6578,11 +6837,36 @@
|
||||
"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": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
||||
"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": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
|
||||
@@ -6596,6 +6880,14 @@
|
||||
"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": {
|
||||
"version": "2.2.3",
|
||||
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
|
||||
@@ -6740,7 +7032,7 @@
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/mux-embed": {
|
||||
@@ -6817,6 +7109,34 @@
|
||||
"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": {
|
||||
"version": "7.1.0",
|
||||
"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",
|
||||
"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": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
|
||||
@@ -6927,6 +7266,17 @@
|
||||
"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": {
|
||||
"version": "8.5.6",
|
||||
"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": {
|
||||
"version": "18.3.1",
|
||||
"resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
|
||||
@@ -7106,15 +7525,6 @@
|
||||
"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": {
|
||||
"version": "18.3.1",
|
||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
|
||||
@@ -7209,6 +7619,17 @@
|
||||
"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": {
|
||||
"version": "1.22.10",
|
||||
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz",
|
||||
@@ -7505,6 +7926,20 @@
|
||||
"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": {
|
||||
"version": "1.0.0",
|
||||
"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"
|
||||
}
|
||||
},
|
||||
"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": {
|
||||
"version": "2.5.4",
|
||||
"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",
|
||||
"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": {
|
||||
"version": "5.9.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
@@ -7767,6 +8227,14 @@
|
||||
"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": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/use-broadcast-ts/-/use-broadcast-ts-2.0.1.tgz",
|
||||
@@ -8068,6 +8536,25 @@
|
||||
"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": {
|
||||
"version": "5.0.8",
|
||||
"resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.8.tgz",
|
||||
|
||||
@@ -1,8 +1,19 @@
|
||||
{
|
||||
"name": "rclone-ui",
|
||||
"private": true,
|
||||
"version": "2.8.1",
|
||||
"version": "3.0.0",
|
||||
"description": "The GUI for rclone & s3",
|
||||
"author": {
|
||||
"name": "Rclone UI",
|
||||
"url": "https://github.com/rclone-ui/rclone-ui"
|
||||
},
|
||||
"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": {
|
||||
"dev": "node scripts/buildExternal.js && vite",
|
||||
"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:linux:arm": "tauri build --target aarch64-unknown-linux-gnu",
|
||||
"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": {
|
||||
"@formkit/auto-animate": "^0.9.0",
|
||||
"@heroui/react": "^2.7.11",
|
||||
"@mux/mux-player-react": "^3.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/plugin-autostart": "^2.5.1",
|
||||
"@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-fs": "^2.4.4",
|
||||
"@tauri-apps/plugin-http": "^2.5.4",
|
||||
@@ -39,14 +55,16 @@
|
||||
"@tauri-apps/plugin-store": "^2.4.1",
|
||||
"@tauri-apps/plugin-updater": "^2.9.0",
|
||||
"@tauri-apps/plugin-window-state": "^2.4.1",
|
||||
"canvas-confetti": "^1.9.4",
|
||||
"cron-parser": "^5.4.0",
|
||||
"cronstrue": "^3.9.0",
|
||||
"date-fns": "^4.1.0",
|
||||
"framer-motion": "^11.18.2",
|
||||
"fuse.js": "^7.1.0",
|
||||
"lucide-react": "^0.552.0",
|
||||
"p-retry": "^7.1.0",
|
||||
"rclone-sdk": "^1.72.0",
|
||||
"react": "^18.3.1",
|
||||
"react-compiler-runtime": "^19.1.0-rc.3",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-router-dom": "^6.28.1",
|
||||
"tauri-plugin-sentry-api": "^0.5.0",
|
||||
@@ -58,6 +76,8 @@
|
||||
"@biomejs/biome": "^1.9.4",
|
||||
"@iarna/toml": "^2.2.5",
|
||||
"@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/react": "^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 {
|
||||
// Only show navigation spinner for non-initial loads
|
||||
if (!isInitialLoad) {
|
||||
@@ -303,17 +303,22 @@ It's not pretty, but at the same time Adobe's .PSD implementation exists.
|
||||
}
|
||||
|
||||
let newUrl = url
|
||||
if (pass) {
|
||||
newUrl = url.replace('localhost:5572', 'admin:' + pass + '@localhost:5572')
|
||||
} else {
|
||||
console.log('no pass')
|
||||
}
|
||||
|
||||
console.log('newUrl', newUrl)
|
||||
|
||||
const fetch = window.__TAURI__.http.fetch
|
||||
// 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();
|
||||
|
||||
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);
|
||||
|
||||
// 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
|
||||
setupEventHandlers();
|
||||
@@ -413,7 +422,10 @@ It's not pretty, but at the same time Adobe's .PSD implementation exists.
|
||||
currentUrl = currentUrl.split('/');
|
||||
currentUrl.pop();
|
||||
const fullUrl = currentUrl.join('/') + '/';
|
||||
fetchAndInjectContent(fullUrl);
|
||||
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const auth = urlParams.get('auth');
|
||||
fetchAndInjectContent(fullUrl, auth);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -457,11 +469,22 @@ It's not pretty, but at the same time Adobe's .PSD implementation exists.
|
||||
toggleNavSpinner(true);
|
||||
|
||||
console.log('downloading', downloadUrl);
|
||||
// Fetch the file contents
|
||||
const response = await http.fetch(downloadUrl, {
|
||||
|
||||
const options = {
|
||||
method: 'GET',
|
||||
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();
|
||||
|
||||
@@ -531,7 +554,9 @@ It's not pretty, but at the same time Adobe's .PSD implementation exists.
|
||||
event.preventDefault();
|
||||
|
||||
// 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) {
|
||||
@@ -557,10 +582,10 @@ It's not pretty, but at the same time Adobe's .PSD implementation exists.
|
||||
// Initial setup
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const url = urlParams.get('url');
|
||||
const pass = urlParams.get('pass');
|
||||
const auth = urlParams.get('auth');
|
||||
if (url) {
|
||||
// Pass true to indicate this is the initial load
|
||||
fetchAndInjectContent(url, pass, true);
|
||||
fetchAndInjectContent(url, auth, true);
|
||||
} else {
|
||||
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
|
||||
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) {
|
||||
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(() => {})))
|
||||
})(),
|
||||
// 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
|
||||
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]]
|
||||
name = "app"
|
||||
version = "2.8.1"
|
||||
version = "3.0.0"
|
||||
dependencies = [
|
||||
"cocoa",
|
||||
"fix-path-env",
|
||||
"log",
|
||||
"machine-uid",
|
||||
"objc",
|
||||
"reqwest 0.11.27",
|
||||
"sentry",
|
||||
"serde",
|
||||
@@ -264,8 +265,10 @@ dependencies = [
|
||||
"tauri-build",
|
||||
"tauri-plugin-autostart",
|
||||
"tauri-plugin-clipboard-manager",
|
||||
"tauri-plugin-deep-link",
|
||||
"tauri-plugin-dialog",
|
||||
"tauri-plugin-fs",
|
||||
"tauri-plugin-global-shortcut",
|
||||
"tauri-plugin-http",
|
||||
"tauri-plugin-log",
|
||||
"tauri-plugin-notification",
|
||||
@@ -278,6 +281,7 @@ dependencies = [
|
||||
"tauri-plugin-single-instance",
|
||||
"tauri-plugin-store",
|
||||
"tauri-plugin-updater",
|
||||
"tinyfiledialogs",
|
||||
"x11rb",
|
||||
"zbus",
|
||||
"zip 0.6.6",
|
||||
@@ -433,7 +437,7 @@ checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.108",
|
||||
"syn 2.0.110",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -468,7 +472,7 @@ checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.108",
|
||||
"syn 2.0.110",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -643,7 +647,7 @@ dependencies = [
|
||||
"proc-macro-crate 3.4.0",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.108",
|
||||
"syn 2.0.110",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -831,9 +835,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "cc"
|
||||
version = "1.2.44"
|
||||
version = "1.2.45"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "37521ac7aabe3d13122dc382493e20c9416f299d2ccd5b3a5340a2570cdeb0f3"
|
||||
checksum = "35900b6c8d709fb1d854671ae27aeaa9eec2f8b01b364e1619a40da3e6fe2afe"
|
||||
dependencies = [
|
||||
"find-msvc-tools",
|
||||
"jobserver",
|
||||
@@ -965,6 +969,26 @@ dependencies = [
|
||||
"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]]
|
||||
name = "constant_time_eq"
|
||||
version = "0.1.5"
|
||||
@@ -1153,7 +1177,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331"
|
||||
dependencies = [
|
||||
"quote",
|
||||
"syn 2.0.108",
|
||||
"syn 2.0.110",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1163,7 +1187,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "32a2785755761f3ddc1492979ce1e48d2c00d09311c39e4466429188f3dd6501"
|
||||
dependencies = [
|
||||
"quote",
|
||||
"syn 2.0.108",
|
||||
"syn 2.0.110",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1187,7 +1211,7 @@ dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"strsim",
|
||||
"syn 2.0.108",
|
||||
"syn 2.0.110",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1198,7 +1222,7 @@ checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81"
|
||||
dependencies = [
|
||||
"darling_core",
|
||||
"quote",
|
||||
"syn 2.0.108",
|
||||
"syn 2.0.110",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1245,7 +1269,7 @@ checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.108",
|
||||
"syn 2.0.110",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1258,7 +1282,7 @@ dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"rustc_version",
|
||||
"syn 2.0.108",
|
||||
"syn 2.0.110",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1278,7 +1302,7 @@ checksum = "bda628edc44c4bb645fbe0f758797143e4e07926f7ebf4e9bdfbd3d2ce621df3"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.108",
|
||||
"syn 2.0.110",
|
||||
"unicode-xid",
|
||||
]
|
||||
|
||||
@@ -1360,7 +1384,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.108",
|
||||
"syn 2.0.110",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1392,7 +1416,16 @@ checksum = "788160fb30de9cdd857af31c6a2675904b16ece8fc2737b2c7127ba368c9d0f4"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"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]]
|
||||
@@ -1505,7 +1538,7 @@ checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.108",
|
||||
"syn 2.0.110",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1526,9 +1559,9 @@ checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
|
||||
|
||||
[[package]]
|
||||
name = "erased-serde"
|
||||
version = "0.4.8"
|
||||
version = "0.4.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "259d404d09818dec19332e31d94558aeb442fea04c817006456c24b5460bbd4b"
|
||||
checksum = "89e8918065695684b2b0702da20382d5ae6065cf3327bc2d6436bd49a71ce9f3"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"serde_core",
|
||||
@@ -1595,7 +1628,7 @@ checksum = "a0aca10fb742cb43f9e7bb8467c91aa9bcb8e3ffbc6a6f7389bb93ffc920577d"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.108",
|
||||
"syn 2.0.110",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1721,7 +1754,7 @@ checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.108",
|
||||
"syn 2.0.110",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1815,7 +1848,7 @@ checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.108",
|
||||
"syn 2.0.110",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2085,7 +2118,7 @@ dependencies = [
|
||||
"proc-macro-error",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.108",
|
||||
"syn 2.0.110",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2104,6 +2137,24 @@ version = "0.3.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
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]]
|
||||
name = "gobject-sys"
|
||||
version = "0.18.0"
|
||||
@@ -2175,7 +2226,7 @@ dependencies = [
|
||||
"proc-macro-error",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.108",
|
||||
"syn 2.0.110",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2236,6 +2287,12 @@ dependencies = [
|
||||
"ahash",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.14.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.16.0"
|
||||
@@ -2704,9 +2761,9 @@ checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130"
|
||||
|
||||
[[package]]
|
||||
name = "iri-string"
|
||||
version = "0.7.8"
|
||||
version = "0.7.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dbc5ebe9c3a1a7a5127f920a418f7585e9e758e911d0466ed004f393b0e380b2"
|
||||
checksum = "4f867b9d1d896b67beb18518eda36fdb77a32ea590de864f1325b294a6d14397"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
"serde",
|
||||
@@ -3074,7 +3131,7 @@ checksum = "88a9689d8d44bf9964484516275f5cd4c9b59457a6940c1d5d0ecbb94510a36b"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.108",
|
||||
"syn 2.0.110",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3378,7 +3435,7 @@ checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.108",
|
||||
"syn 2.0.110",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3409,7 +3466,7 @@ dependencies = [
|
||||
"proc-macro-crate 3.4.0",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.108",
|
||||
"syn 2.0.110",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3745,9 +3802,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "openssl"
|
||||
version = "0.10.74"
|
||||
version = "0.10.75"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "24ad14dd45412269e1a30f52ad8f0664f0f4f4a89ee8fe28c3b3527021ebb654"
|
||||
checksum = "08838db121398ad17ab8531ce9de97b244589089e290a384c900cb9ff7434328"
|
||||
dependencies = [
|
||||
"bitflags 2.10.0",
|
||||
"cfg-if",
|
||||
@@ -3766,7 +3823,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.108",
|
||||
"syn 2.0.110",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3777,9 +3834,9 @@ checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e"
|
||||
|
||||
[[package]]
|
||||
name = "openssl-sys"
|
||||
version = "0.9.110"
|
||||
version = "0.9.111"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0a9f0075ba3c21b09f8e8b2026584b1d18d49388648f2fbbf3c97ea8deced8e2"
|
||||
checksum = "82cab2d520aa75e3c58898289429321eb788c3106963d0dc886ec7a5f4adc321"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"libc",
|
||||
@@ -3793,6 +3850,16 @@ version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
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]]
|
||||
name = "ordered-stream"
|
||||
version = "0.2.0"
|
||||
@@ -3967,7 +4034,7 @@ dependencies = [
|
||||
"pest_meta",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.108",
|
||||
"syn 2.0.110",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4094,7 +4161,7 @@ dependencies = [
|
||||
"phf_shared 0.11.3",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.108",
|
||||
"syn 2.0.110",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4446,9 +4513,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.41"
|
||||
version = "1.0.42"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ce25767e7b499d1b604768e7cde645d14cc8584231ea6b295e9c9eb22c02e1d1"
|
||||
checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
@@ -4638,7 +4705,7 @@ checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.108",
|
||||
"syn 2.0.110",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4709,7 +4776,7 @@ dependencies = [
|
||||
"once_cell",
|
||||
"percent-encoding",
|
||||
"pin-project-lite",
|
||||
"rustls-pemfile 1.0.4",
|
||||
"rustls-pemfile",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_urlencoded",
|
||||
@@ -4843,6 +4910,16 @@ dependencies = [
|
||||
"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]]
|
||||
name = "rust_decimal"
|
||||
version = "1.39.0"
|
||||
@@ -4908,9 +4985,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rustls"
|
||||
version = "0.23.34"
|
||||
version = "0.23.35"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6a9586e9ee2b4f8fab52a0048ca7334d7024eef48e2cb9407e3497bb7cab7fa7"
|
||||
checksum = "533f54bc6a7d4f647e46ad909549eda97bf5afc1585190ef692b4286b198bd8f"
|
||||
dependencies = [
|
||||
"once_cell",
|
||||
"ring",
|
||||
@@ -4929,15 +5006,6 @@ dependencies = [
|
||||
"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]]
|
||||
name = "rustls-pki-types"
|
||||
version = "1.13.0"
|
||||
@@ -5018,9 +5086,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "schemars"
|
||||
version = "1.0.5"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1317c3bf3e7df961da95b0a56a172a02abead31276215a0497241a7624b487ce"
|
||||
checksum = "9558e172d4e8533736ba97870c4b2cd63f84b382a3d6eb063da41b91cce17289"
|
||||
dependencies = [
|
||||
"dyn-clone",
|
||||
"ref-cast",
|
||||
@@ -5037,7 +5105,7 @@ dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"serde_derive_internals",
|
||||
"syn 2.0.108",
|
||||
"syn 2.0.110",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -5069,7 +5137,7 @@ checksum = "1783eabc414609e28a5ba76aee5ddd52199f7107a0b24c2e9746a1ecc34a683d"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.108",
|
||||
"syn 2.0.110",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -5300,7 +5368,7 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.108",
|
||||
"syn 2.0.110",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -5311,7 +5379,7 @@ checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.108",
|
||||
"syn 2.0.110",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -5335,7 +5403,7 @@ checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.108",
|
||||
"syn 2.0.110",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -5380,7 +5448,7 @@ dependencies = [
|
||||
"indexmap 1.9.3",
|
||||
"indexmap 2.12.0",
|
||||
"schemars 0.9.0",
|
||||
"schemars 1.0.5",
|
||||
"schemars 1.1.0",
|
||||
"serde_core",
|
||||
"serde_json",
|
||||
"serde_with_macros",
|
||||
@@ -5396,7 +5464,7 @@ dependencies = [
|
||||
"darling",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.108",
|
||||
"syn 2.0.110",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -5418,7 +5486,7 @@ checksum = "772ee033c0916d670af7860b6e1ef7d658a4629a6d0b4c8c3e67f09b3765b75d"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.108",
|
||||
"syn 2.0.110",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -5544,7 +5612,7 @@ checksum = "0eb01866308440fc64d6c44d9e86c5cc17adfe33c4d6eed55da9145044d0ffc1"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.108",
|
||||
"syn 2.0.110",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -5685,7 +5753,7 @@ dependencies = [
|
||||
"heck 0.5.0",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.108",
|
||||
"syn 2.0.110",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -5718,9 +5786,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.108"
|
||||
version = "2.0.110"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "da58917d35242480a05c2897064da0a80589a2a0476c9a3f2fdc83b53502e917"
|
||||
checksum = "a99801b5bd34ede4cf3fc688c5919368fea4e4814a4664359503e6015b280aea"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -5750,7 +5818,7 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.108",
|
||||
"syn 2.0.110",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -5834,8 +5902,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "tao"
|
||||
version = "0.34.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f3a753bdc39c07b192151523a3f77cd0394aa75413802c883a0f6f6a0e5ee2e7"
|
||||
source = "git+https://github.com/tauri-apps/tao?branch=fix-windows-white-flash#cf28a8e8f205bf5cd7a41c4d7fc54ed9247c5f93"
|
||||
dependencies = [
|
||||
"bitflags 2.10.0",
|
||||
"block2 0.6.2",
|
||||
@@ -5862,7 +5929,7 @@ dependencies = [
|
||||
"parking_lot",
|
||||
"raw-window-handle",
|
||||
"scopeguard",
|
||||
"tao-macros",
|
||||
"tao-macros 0.1.3 (git+https://github.com/tauri-apps/tao?branch=fix-windows-white-flash)",
|
||||
"unicode-segmentation",
|
||||
"url",
|
||||
"windows",
|
||||
@@ -5879,7 +5946,17 @@ checksum = "f4e16beb8b2ac17db28eab8bca40e62dbfbb34c0fcdc6d9826b11b7b5d047dfd"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"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]]
|
||||
@@ -5997,7 +6074,7 @@ dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sha2",
|
||||
"syn 2.0.108",
|
||||
"syn 2.0.110",
|
||||
"tauri-utils",
|
||||
"thiserror 2.0.17",
|
||||
"time",
|
||||
@@ -6015,7 +6092,7 @@ dependencies = [
|
||||
"heck 0.5.0",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.108",
|
||||
"syn 2.0.110",
|
||||
"tauri-codegen",
|
||||
"tauri-utils",
|
||||
]
|
||||
@@ -6066,6 +6143,27 @@ dependencies = [
|
||||
"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]]
|
||||
name = "tauri-plugin-dialog"
|
||||
version = "2.4.2"
|
||||
@@ -6106,6 +6204,21 @@ dependencies = [
|
||||
"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]]
|
||||
name = "tauri-plugin-http"
|
||||
version = "2.5.4"
|
||||
@@ -6283,6 +6396,7 @@ dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tauri",
|
||||
"tauri-plugin-deep-link",
|
||||
"thiserror 2.0.17",
|
||||
"tracing",
|
||||
"windows-sys 0.60.2",
|
||||
@@ -6500,7 +6614,7 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.108",
|
||||
"syn 2.0.110",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -6511,7 +6625,7 @@ checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.108",
|
||||
"syn 2.0.110",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -6561,6 +6675,25 @@ dependencies = [
|
||||
"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]]
|
||||
name = "tinystr"
|
||||
version = "0.8.2"
|
||||
@@ -6612,7 +6745,7 @@ checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.108",
|
||||
"syn 2.0.110",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -6809,7 +6942,7 @@ checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.108",
|
||||
"syn 2.0.110",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -6985,16 +7118,15 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
|
||||
|
||||
[[package]]
|
||||
name = "ureq"
|
||||
version = "3.1.2"
|
||||
version = "3.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "99ba1025f18a4a3fc3e9b48c868e9beb4f24f4b4b1a325bada26bd4119f46537"
|
||||
checksum = "d39cb1dbab692d82a977c0392ffac19e188bd9186a9f32806f0aaa859d75585a"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"der",
|
||||
"log",
|
||||
"native-tls",
|
||||
"percent-encoding",
|
||||
"rustls-pemfile 2.2.0",
|
||||
"rustls-pki-types",
|
||||
"ureq-proto",
|
||||
"utf-8",
|
||||
@@ -7211,7 +7343,7 @@ dependencies = [
|
||||
"bumpalo",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.108",
|
||||
"syn 2.0.110",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
|
||||
@@ -7414,7 +7546,7 @@ checksum = "1d228f15bba3b9d56dde8bddbee66fa24545bd17b48d5128ccf4a8742b18e431"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.108",
|
||||
"syn 2.0.110",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -7430,9 +7562,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "weezl"
|
||||
version = "0.1.10"
|
||||
version = "0.1.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a751b3277700db47d3e574514de2eced5e54dc8a5436a3bf7a0b248b2cee16f3"
|
||||
checksum = "009936b22a61d342859b5f0ea64681cbb35a358ab548e2a44a8cf0dac2d980b8"
|
||||
|
||||
[[package]]
|
||||
name = "win7-notifications"
|
||||
@@ -7557,7 +7689,7 @@ checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.108",
|
||||
"syn 2.0.110",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -7568,7 +7700,7 @@ checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.108",
|
||||
"syn 2.0.110",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -8077,7 +8209,7 @@ dependencies = [
|
||||
"raw-window-handle",
|
||||
"sha2",
|
||||
"soup3",
|
||||
"tao-macros",
|
||||
"tao-macros 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"thiserror 2.0.17",
|
||||
"url",
|
||||
"webkit2gtk",
|
||||
@@ -8146,6 +8278,12 @@ dependencies = [
|
||||
"rustix 1.1.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "xkeysym"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b9cc00251562a284751c9973bace760d86c0276c471b4be569fe6b068ee97a56"
|
||||
|
||||
[[package]]
|
||||
name = "yoke"
|
||||
version = "0.8.1"
|
||||
@@ -8165,7 +8303,7 @@ checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.108",
|
||||
"syn 2.0.110",
|
||||
"synstructure",
|
||||
]
|
||||
|
||||
@@ -8213,7 +8351,7 @@ dependencies = [
|
||||
"proc-macro-crate 3.4.0",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.108",
|
||||
"syn 2.0.110",
|
||||
"zbus_names",
|
||||
"zvariant",
|
||||
"zvariant_utils",
|
||||
@@ -8248,7 +8386,7 @@ checksum = "88d2b8d9c68ad2b9e4340d7832716a4d21a22a1154777ad56ea55c51a9cf3831"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.108",
|
||||
"syn 2.0.110",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -8268,7 +8406,7 @@ checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.108",
|
||||
"syn 2.0.110",
|
||||
"synstructure",
|
||||
]
|
||||
|
||||
@@ -8308,7 +8446,7 @@ checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.108",
|
||||
"syn 2.0.110",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -8411,7 +8549,7 @@ dependencies = [
|
||||
"proc-macro-crate 3.4.0",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.108",
|
||||
"syn 2.0.110",
|
||||
"zvariant_utils",
|
||||
]
|
||||
|
||||
@@ -8424,6 +8562,6 @@ dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"serde",
|
||||
"syn 2.0.108",
|
||||
"syn 2.0.110",
|
||||
"winnow 0.7.13",
|
||||
]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "app"
|
||||
version = "2.8.1"
|
||||
version = "3.0.0"
|
||||
description = "A Tauri App"
|
||||
authors = ["you"]
|
||||
license = ""
|
||||
@@ -33,22 +33,29 @@ tauri-plugin-store = "2.4.1"
|
||||
tauri-plugin-process = "2.3.1"
|
||||
tauri-plugin-notification = { version = "2.3.3", features = [ "windows7-compat" ] }
|
||||
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" }
|
||||
zip = "0.6.6"
|
||||
machine-uid = "0.5.4"
|
||||
tauri-plugin-sentry = "0.5.0"
|
||||
sentry = "0.42"
|
||||
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-clipboard-manager = "2.3.2"
|
||||
tauri-plugin-prevent-default = "3"
|
||||
reqwest = { version = "0.11", features = ["json"] }
|
||||
sysinfo = "0.37"
|
||||
tinyfiledialogs = "3.9.1"
|
||||
tauri-plugin-deep-link = "2"
|
||||
|
||||
[target.'cfg(target_os = "macos")'.dependencies]
|
||||
cocoa = "0.26"
|
||||
objc = "0.2"
|
||||
|
||||
[target.'cfg(target_os = "linux")'.dependencies]
|
||||
zbus = { version = "5" }
|
||||
x11rb = "0.13"
|
||||
|
||||
[patch.crates-io]
|
||||
tao = { git = "https://github.com/tauri-apps/tao", branch = "fix-windows-white-flash" }
|
||||
|
||||
@@ -11,13 +11,16 @@
|
||||
"Purge",
|
||||
"Delete",
|
||||
"Settings",
|
||||
"Jobs",
|
||||
"Cron",
|
||||
"Transfers",
|
||||
"Schedules",
|
||||
"Templates",
|
||||
"Serve",
|
||||
"Bisync",
|
||||
"Download",
|
||||
"Browse",
|
||||
"Startup",
|
||||
"Toolbar",
|
||||
"Onboarding",
|
||||
"Test"
|
||||
],
|
||||
"permissions": [
|
||||
@@ -247,7 +250,6 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
"opener:allow-open-url",
|
||||
{
|
||||
"identifier": "opener:allow-open-path",
|
||||
@@ -266,57 +268,21 @@
|
||||
]
|
||||
},
|
||||
"opener:allow-default-urls",
|
||||
|
||||
"updater:allow-download",
|
||||
"updater:allow-download-and-install",
|
||||
"updater:allow-check",
|
||||
"updater:allow-install",
|
||||
|
||||
"autostart:allow-enable",
|
||||
"autostart:allow-disable",
|
||||
"autostart:allow-is-enabled",
|
||||
|
||||
{
|
||||
"identifier": "http:default",
|
||||
"allow": [
|
||||
{
|
||||
"url": "http://localhost:5572/**"
|
||||
"url": "http://*:*"
|
||||
},
|
||||
{
|
||||
"url": "https://downloads.rclone.org/**"
|
||||
},
|
||||
{
|
||||
"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/**"
|
||||
"url": "https://*:*"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -326,6 +292,8 @@
|
||||
"os:default",
|
||||
"sentry:allow-event",
|
||||
"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>✅ Proxy support</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>✅ 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>
|
||||
@@ -114,10 +114,10 @@
|
||||
</screenshots>
|
||||
|
||||
<releases>
|
||||
<release version="2.8.1" date="2025-11-11">
|
||||
<url type="details">https://github.com/rclone-ui/rclone-ui/releases/tag/v2.8.1</url>
|
||||
<release version="3.0.0" date="2025-12-06">
|
||||
<url type="details">https://github.com/rclone-ui/rclone-ui/releases/tag/v3.0.0</url>
|
||||
<description>
|
||||
<p>2.8.1 Release</p>
|
||||
<p>3.0.0 Release</p>
|
||||
<ul>
|
||||
<li>Improved Linux stability and overall performance.</li>
|
||||
</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 |