da7P^
zNUiAiow3{-oT#^bzkh7zVSz_ISA01XKL-=uH(Y|4X-r+)@YQHE3o}LKsr;oIZV-
zZ|#No&R}EXYwhu`>cXqGEkS-^m1LXfe_sH@d!|A$76=7_LX>x#_hHZSFlfC8F=Tyj
zPVmZ83SN{egnIIAYkhrP$r&Rj5(VADPASg9?+4^(YVFBLdw+aq)H?g=zkk{X>>hNW
zLDBe_`IkJ9f+E`ByzDV5Z7Vw>uD&40ETZTKAM!)pJG&XKh&E!aF633h`%Qhfooi5f?HL+0
zA~i$eEqUyba9lBu!sz6R4v@|b{zrK#n}xV0}kqG
zCwEW3{rK^ti?Q)!Dmb78J3{mIZ-b1swgt6VVYpm+o_8+dNa$P>Tr^$5zA0-0{dSj@
zhlhvn5#TE9XjOu>c{;>+owfJFT_01bIgiP8b?>bjrYLsVgM_yi!FiW|Qm0no2020e
zx7VP@vP+UFmH{w0-_9@ljkY$%QUtkqd+*;Qk~-LHs6D6Vt=ZSM+0mC}uU`u?9mFNX
zOY-uzhnV+H7rF(#ls|fHD+79x_ccnU9<)ixCgs{1k&9RXl`9?Eh6uqq`!B`snV1DDsFq=`uc;qRwXt62wE^w|Z{Q{hioF9sN%#e}LWhV~tj^rgPqx!GwT
z;YNW!$GfQOvW6Orcx0_cTj?PE;lO(VcX?U&mFxupJH%{wGx?+L@scp$VjCUwbZ8!5
zQ*tGk&flEeYlj%*2OpmD>QDIlzxu}*tak>y!W2IXw$-YpAB-fTZ4VL8_WyCSop1pfe;ddXA0>x_2!eu4}S9(go-b82X
zn=d5}5O2C*ZfknubsXdO-_Lx%$(}g_
zqP`|@jGZ3PyYM!)vtxrVE6f=Q3NIRY4m>8SIQk18rm*W~siNmO7}P
zg~2DGDj5yOX>8+T|2SQJOH5PwXRXswRkfWJTt+u#ElzYZAvg^9vZcqzjNQ*#X0hYeM<)f)kKiLLp{i@5ml=kEt>+doe>~0qHSDy(I|~}3&1pO5
zppW`5BFz_`#W51as;JYd?m(SYi%^(q9aC+6z7n?OlxRo^uD{)Q-*~hFa{PL9hEut^
zy1R)7TWK;(^!mE{va%c;9QD*)LIAI|a2)a$hY3RjfT;!Xe+eV``)J~QN?_|0LQl|M
z0eT5%|G(FxsEjN2u7U5i8h=+d|B;qMo)7G%=|6d?gh2qe5c!3${Se8dtPD0noU+%x
z^M${Ro*bX+FJ$Vh>ssop&aW3c}&TfrW)CGwA2p}eqG9c
zQ0dA3cZ!n`u+bYSkrp(ef!+14&lp$f+0xzNqJ;tT(=qS+yE^C#=wN%j){vFoU|WVm
gw9Nn8XK{-HJOAUyreJbf_(}OqO?{2`YSy9u2dgdV@Bjb+
delta 990
zcmV<410npyaDoUSiBL{Q4GJ0x0000DNk~Le0000Z0000Z2nGNE03O-1S&<<$e*>3E
zL_t(|0i{^GY8ycmofQJxRa6*^?IJQs?|?tRf&Bpgfdx^ofKywAQpXVY4p_Yc`T#+c
zIsv;kNN)7#ltxIPN6uN_73
zoC-{*(<$uk?ly3HZn8^rGMSW5PEJNiiaC=)0OW2%Ae0NN|CUBE5zJsPsBlBGos?uB
zr#Rgt!P-JVzSVRjV7s^$U3!oa-1Xk&t|ZHxYX=YnF8iYfn`X+C9|sa!$3t7TIk;+HbCqftva2#Rasi>py!Al1
zs-UgQYZIXNU=d9Ih`4zO$MO8nq!8+1o$X-8VAo^;19P&*?8}?1e2oL>hLY$sHIqZ{
zjk2{CPj)cExQu*$RI@^de{Z=)_YyPKb*xvU3|rk=iy4v=4tX*!HAn>_QsKt&LAdCL
zqTgmLlDk6ds$f_Mp?H%5L@3m|j_px$kY649hun21g?Amy1{FD!-{B42i^HsY2=}y+
z7@s4<)hM6&s7*CwfzC;NV7ljBl3IJW*WT
zK+e$!SsN=7o6#A%BJ-+%^sN0fanLea+FI)l-2f|SfnAVa5>}D@tkz?jlEar4bOl>M
zZ^``*?M}7g2H<%SIf5)1zG2_}(2Z+E$Z0JKU
z=Sdq|#1DvSoozDDoe%|
zfL^iutL923%4s*Ji;$|P1ccAMNiWREt9ZGw)^lb8l&^8nN2OeZEk!L_(HVPEcuN`x
zd~T7Aw>G9uH2h11j5(XliU_hG8*qIXkH^n0+Lwl&8B~l6x05=6|KjcSyyyrM@c;k-
M07*qoM6N<$f)3x*{r~^~
diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs
index 6568cdb..0c8c703 100644
--- a/src-tauri/src/lib.rs
+++ b/src-tauri/src/lib.rs
@@ -250,21 +250,96 @@ fn get_uid() -> String {
#[tauri::command]
fn get_system_theme() -> String {
- #[cfg(not(target_os = "linux"))]
+ #[cfg(target_os = "macos")]
{
- match dark_light::detect() {
- dark_light::Mode::Dark => "dark".to_string(),
- dark_light::Mode::Light => "light".to_string(),
- dark_light::Mode::Default => "unknown".to_string(),
+ use std::process::Command;
+ if let Ok(output) = Command::new("defaults")
+ .args(&["read", "-g", "AppleInterfaceStyle"])
+ .output()
+ {
+ if output.status.success() {
+ let out = String::from_utf8_lossy(&output.stdout).to_ascii_lowercase();
+ if out.contains("dark") {
+ return "dark".to_string();
+ }
+ }
}
+ return "light".to_string();
+ }
+
+ #[cfg(target_os = "windows")]
+ {
+ use std::process::Command;
+ if let Ok(output) = Command::new("reg").args(&[
+ "query",
+ r"HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize",
+ "/v",
+ "AppsUseLightTheme",
+ ]).output() {
+ if output.status.success() {
+ let out = String::from_utf8_lossy(&output.stdout).to_ascii_lowercase();
+ if out.contains("0x0") || out.contains("0x00000000") {
+ return "dark".to_string();
+ }
+ if out.contains("0x1") || out.contains("0x00000001") {
+ return "light".to_string();
+ }
+ }
+ }
+ return "light".to_string();
}
#[cfg(target_os = "linux")]
{
- "unknown".to_string()
+ use std::process::Command;
+
+ // GNOME color-scheme (prefer-dark)
+ if let Ok(output) = Command::new("gsettings")
+ .args(&["get", "org.gnome.desktop.interface", "color-scheme"])
+ .output()
+ {
+ if output.status.success() {
+ let out = String::from_utf8_lossy(&output.stdout).to_ascii_lowercase();
+ if out.contains("dark") {
+ return "dark".to_string();
+ }
+ }
+ }
+
+ // GNOME gtk-theme name contains "dark"
+ if let Ok(output) = Command::new("gsettings")
+ .args(&["get", "org.gnome.desktop.interface", "gtk-theme"])
+ .output()
+ {
+ if output.status.success() {
+ let out = String::from_utf8_lossy(&output.stdout).to_ascii_lowercase();
+ if out.contains("dark") {
+ return "dark".to_string();
+ }
+ }
+ }
+
+ // KDE color scheme via kreadconfig5
+ if let Ok(output) = Command::new("kreadconfig5")
+ .args(&["--group", "General", "--key", "ColorScheme"])
+ .output()
+ {
+ if output.status.success() {
+ let out = String::from_utf8_lossy(&output.stdout).to_ascii_lowercase();
+ if out.contains("dark") {
+ return "dark".to_string();
+ }
+ }
+ }
+
+ return "light".to_string();
+ }
+
+ #[cfg(not(any(target_os = "macos", target_os = "windows", target_os = "linux")))]
+ {
+ "dark".to_string()
}
}
-
#[tauri::command]
fn is_rclone_running(port: Option) -> bool {
@@ -469,103 +544,52 @@ async fn prompt_text(
if is_sensitive {
// Try zenity password dialog first
- match std::process::Command::new("zenity")
+ if let Ok(output) = std::process::Command::new("zenity")
.args(&["--password", "--title", &title, "--text", &message])
.output()
{
- Ok(output) => {
- if output.status.success() {
- let result = String::from_utf8_lossy(&output.stdout).trim().to_string();
+ if output.status.success() {
+ let result = String::from_utf8_lossy(&output.stdout).trim().to_string();
+ if !result.is_empty() {
return Ok(Some(result));
- } else {
- // Check if user cancelled (exit code 1) or other error
- let exit_code = output.status.code().unwrap_or(-1);
- if exit_code == 1 {
- // User clicked Cancel
- return Ok(None);
- }
- // For other errors, fall through to try kdialog
- let stderr = String::from_utf8_lossy(&output.stderr);
- eprintln!("zenity password dialog failed (exit {}): {}", exit_code, stderr);
}
}
- Err(e) => {
- // Command not found or couldn't execute, try kdialog
- eprintln!("zenity not available: {}", e);
- }
}
// Fallback to kdialog password
- match std::process::Command::new("kdialog")
+ if let Ok(output) = std::process::Command::new("kdialog")
.args(&["--password", &message, "--title", &title])
.output()
{
- Ok(output) => {
- if output.status.success() {
- let result = String::from_utf8_lossy(&output.stdout).trim().to_string();
+ if output.status.success() {
+ let result = String::from_utf8_lossy(&output.stdout).trim().to_string();
+ if !result.is_empty() {
return Ok(Some(result));
- } else {
- let exit_code = output.status.code().unwrap_or(-1);
- if exit_code == 1 {
- // User clicked Cancel
- return Ok(None);
- }
- let stderr = String::from_utf8_lossy(&output.stderr);
- eprintln!("kdialog password dialog failed (exit {}): {}", exit_code, stderr);
}
}
- Err(e) => {
- eprintln!("kdialog not available: {}", e);
- }
}
return Err("No suitable password dialog found. Please install zenity or kdialog.".to_string());
} else {
// Try zenity text entry first
- match std::process::Command::new("zenity")
+ if let Ok(output) = std::process::Command::new("zenity")
.args(&["--entry", "--title", &title, "--text", &message, "--entry-text", &default_value])
.output()
{
- Ok(output) => {
- if output.status.success() {
- let result = String::from_utf8_lossy(&output.stdout).trim().to_string();
- return Ok(Some(result));
- } else {
- let exit_code = output.status.code().unwrap_or(-1);
- if exit_code == 1 {
- // User clicked Cancel
- return Ok(None);
- }
- let stderr = String::from_utf8_lossy(&output.stderr);
- eprintln!("zenity entry dialog failed (exit {}): {}", exit_code, stderr);
- }
- }
- Err(e) => {
- eprintln!("zenity not available: {}", e);
+ if output.status.success() {
+ let result = String::from_utf8_lossy(&output.stdout).trim().to_string();
+ return Ok(Some(result));
}
}
// Fallback to kdialog input box
- match std::process::Command::new("kdialog")
+ if let Ok(output) = std::process::Command::new("kdialog")
.args(&["--inputbox", &message, &default_value, "--title", &title])
.output()
{
- Ok(output) => {
- if output.status.success() {
- let result = String::from_utf8_lossy(&output.stdout).trim().to_string();
- return Ok(Some(result));
- } else {
- let exit_code = output.status.code().unwrap_or(-1);
- if exit_code == 1 {
- // User clicked Cancel
- return Ok(None);
- }
- let stderr = String::from_utf8_lossy(&output.stderr);
- eprintln!("kdialog inputbox failed (exit {}): {}", exit_code, stderr);
- }
- }
- Err(e) => {
- eprintln!("kdialog not available: {}", e);
+ if output.status.success() {
+ let result = String::from_utf8_lossy(&output.stdout).trim().to_string();
+ return Ok(Some(result));
}
}
diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json
index 3ee9f88..7084891 100644
--- a/src-tauri/tauri.conf.json
+++ b/src-tauri/tauri.conf.json
@@ -2,7 +2,7 @@
"$schema": "https://schema.tauri.app/config/2",
"productName": "Rclone UI",
"mainBinaryName": "Rclone UI",
- "version": "2.7.19",
+ "version": "2.7.20",
"identifier": "com.rclone.ui",
"build": {
"frontendDist": "../dist",
diff --git a/src/pages/Settings.tsx b/src/pages/Settings.tsx
index 365bdc2..d0f03e0 100644
--- a/src/pages/Settings.tsx
+++ b/src/pages/Settings.tsx
@@ -49,7 +49,7 @@ import {
MedalIcon,
PencilIcon,
PlusIcon,
- SatelliteDishIcon,
+ SatelliteDish,
ServerIcon,
Trash2Icon,
} from 'lucide-react'
@@ -213,7 +213,7 @@ function Settings() {
key="proxy"
title={
-
+
Proxy
}
@@ -292,7 +292,11 @@ function GeneralSection() {
const [isWorkingUpdate, setIsWorkingUpdate] = useState(false)
const [update, setUpdate] = useState(null)
- const [showServe, setShowServe] = useState(false)
+ const [rcloneVersion, setRcloneVersion] = useState<{ yours: string; latest: string } | null>(
+ null
+ )
+
+ const showServe = rcloneVersion?.yours && compareVersions(rcloneVersion.yours, '1.70.0') === 1
async function updateCallback() {
if (!update) {
@@ -393,28 +397,19 @@ function GeneralSection() {
await getCurrentWindow().emit('relaunch-app')
}
- function updateDisabledActions({
- name,
- value,
- }: { name: (typeof disabledActions)[number]; value: boolean }) {
- if (value) {
- setDisabledActions(disabledActions?.filter((action) => action !== name) || [])
- } else {
- setDisabledActions([...(disabledActions || []), name])
- }
- triggerTrayRebuild()
- }
-
useEffect(() => {
// needed since the first value from the persisted store is undefined
setPasswordInput(settingsPass || '')
}, [settingsPass])
+ // biome-ignore lint/correctness/useExhaustiveDependencies:
+ useEffect(() => {
+ triggerTrayRebuild()
+ }, [disabledActions])
+
useEffect(() => {
getRcloneVersion().then((version) => {
- if (version?.yours && compareVersions(version.yours, '1.70.0') === 1) {
- setShowServe(true)
- }
+ setRcloneVersion(version)
})
}, [])
@@ -530,66 +525,123 @@ function GeneralSection() {
- updateDisabledActions({ name: 'tray-mount', value })
- }
+ onValueChange={(value) => {
+ if (value) {
+ setDisabledActions(
+ disabledActions?.filter((action) => action !== 'tray-mount') ||
+ []
+ )
+ } else {
+ setDisabledActions([...(disabledActions || []), 'tray-mount'])
+ }
+ }}
>
Show Mount option
- updateDisabledActions({ name: 'tray-sync', value })
- }
+ onValueChange={(value) => {
+ if (value) {
+ setDisabledActions(
+ disabledActions?.filter((action) => action !== 'tray-sync') ||
+ []
+ )
+ } else {
+ setDisabledActions([...(disabledActions || []), 'tray-sync'])
+ }
+ }}
>
Show Sync option
- updateDisabledActions({ name: 'tray-copy', value })
- }
+ onValueChange={(value) => {
+ if (value) {
+ setDisabledActions(
+ disabledActions?.filter((action) => action !== 'tray-copy') ||
+ []
+ )
+ } else {
+ setDisabledActions([...(disabledActions || []), 'tray-copy'])
+ }
+ }}
>
Show Copy option
- updateDisabledActions({ name: 'tray-move', value })
- }
+ onValueChange={(value) => {
+ if (value) {
+ setDisabledActions(
+ disabledActions?.filter((action) => action !== 'tray-move') ||
+ []
+ )
+ } else {
+ setDisabledActions([...(disabledActions || []), 'tray-move'])
+ }
+ }}
>
Show Move option
- updateDisabledActions({ name: 'tray-download', value })
- }
+ onValueChange={(value) => {
+ if (value) {
+ setDisabledActions(
+ disabledActions?.filter(
+ (action) => action !== 'tray-download'
+ ) || []
+ )
+ } else {
+ setDisabledActions([...(disabledActions || []), 'tray-download'])
+ }
+ }}
>
Show Download option
- updateDisabledActions({ name: 'tray-serve', value })
- }
+ onValueChange={(value) => {
+ if (value) {
+ setDisabledActions(
+ disabledActions?.filter((action) => action !== 'tray-serve') ||
+ []
+ )
+ } else {
+ setDisabledActions([...(disabledActions || []), 'tray-serve'])
+ }
+ }}
isDisabled={!showServe}
>
Show Serve option
- updateDisabledActions({ name: 'tray-purge', value })
- }
+ onValueChange={(value) => {
+ if (value) {
+ setDisabledActions(
+ disabledActions?.filter((action) => action !== 'tray-purge') ||
+ []
+ )
+ } else {
+ setDisabledActions([...(disabledActions || []), 'tray-purge'])
+ }
+ }}
>
Show Purge option
- updateDisabledActions({ name: 'tray-delete', value })
- }
+ onValueChange={(value) => {
+ if (value) {
+ setDisabledActions(
+ disabledActions?.filter((action) => action !== 'tray-delete') ||
+ []
+ )
+ } else {
+ setDisabledActions([...(disabledActions || []), 'tray-delete'])
+ }
+ }}
>
Show Delete option