Files
rclone-ui/src-tauri/src/lib.rs
T
2026-05-12 19:28:58 +03:00

981 lines
32 KiB
Rust

use machine_uid;
use sentry;
use std::fs::{self, File};
use std::path::Path;
use sysinfo::System;
use tauri::{AppHandle, Emitter, Manager};
use tauri_plugin_sentry;
use tinyfiledialogs as tfd;
use zip::ZipArchive;
#[path = "../common/shortcut.rs"]
mod shortcut;
#[path = "../common/window.rs"]
mod window;
use shortcut::{
ensure_toolbar_window, set_toolbar_shortcut, show_toolbar_window, DEFAULT_TOOLBAR_SHORTCUT,
};
use window::{lock_windows, open_full_window, open_small_window, open_window, unlock_windows};
#[tauri::command]
fn update_toolbar_shortcut(app_handle: AppHandle, shortcut: Option<String>) -> Result<(), String> {
set_toolbar_shortcut(&app_handle, shortcut.as_deref())
}
#[tauri::command]
fn show_toolbar(app_handle: AppHandle) -> Result<(), String> {
show_toolbar_window(&app_handle).map_err(|e| e.to_string())
}
#[tauri::command]
fn is_flatpak() -> bool {
std::path::Path::new("/.flatpak-info").exists() || std::env::var_os("FLATPAK_ID").is_some()
}
#[tauri::command]
fn is_linux_mint() -> bool {
#[cfg(not(target_os = "linux"))]
{
false
}
#[cfg(target_os = "linux")]
{
let paths: &[&str] = if is_flatpak() {
&["/run/host/os-release", "/etc/os-release", "/usr/lib/os-release"]
} else {
&["/etc/os-release", "/usr/lib/os-release"]
};
for path in paths {
if let Ok(contents) = std::fs::read_to_string(path) {
return contents.lines().any(|line| {
let line = line.trim();
line == "ID=linuxmint" || line == "ID=\"linuxmint\""
});
}
}
false
}
}
#[tauri::command]
fn has_flatpak_permissions() -> bool {
// Native app: no Flatpak permission needed.
if !is_flatpak() {
return true;
}
let Ok(contents) = std::fs::read_to_string("/.flatpak-info") else {
return false;
};
let mut in_context = false;
for line in contents.lines() {
let line = line.trim();
if line.is_empty() || line.starts_with('#') {
continue;
}
if line.starts_with('[') && line.ends_with(']') {
in_context = line == "[Context]";
continue;
}
if !in_context {
continue;
}
let Some((key, value)) = line.split_once('=') else {
continue;
};
if key.trim() != "filesystems" {
continue;
}
for raw in value.split(';') {
let item = raw.trim();
if item.is_empty() {
continue;
}
// Explicit negative override, e.g. !host
if item == "!host" || item.starts_with("!host:") {
return false;
}
// Writable host access
if item == "host" || item == "host:rw" || item == "host:create" {
return true;
}
// Read-only host access is not enough for full rclone filesystem usage
if item == "host:ro" {
return false;
}
}
}
false
}
#[tauri::command]
fn unzip_file(zip_path: &str, output_folder: &str) -> Result<(), String> {
// Open the zip file
let file = File::open(zip_path).map_err(|e| e.to_string())?;
// Create output directory if it doesn't exist
fs::create_dir_all(output_folder).map_err(|e| e.to_string())?;
// Create ZIP archive reader
let mut archive = ZipArchive::new(file).map_err(|e| e.to_string())?;
// Extract everything
for i in 0..archive.len() {
let mut file = archive.by_index(i).map_err(|e| e.to_string())?;
let outpath = Path::new(output_folder).join(file.name());
if file.name().ends_with('/') || file.name().ends_with('\\') {
fs::create_dir_all(&outpath).map_err(|e| e.to_string())?;
} else {
if let Some(p) = outpath.parent() {
fs::create_dir_all(p).map_err(|e| e.to_string())?;
}
let mut outfile = File::create(&outpath).map_err(|e| e.to_string())?;
std::io::copy(&mut file, &mut outfile).map_err(|e| e.to_string())?;
}
// Get and set permissions (Unix only)
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
if let Some(mode) = file.unix_mode() {
fs::set_permissions(&outpath, fs::Permissions::from_mode(mode))
.map_err(|e| e.to_string())?;
}
}
}
Ok(())
}
#[tauri::command]
async fn stop_pid(pid: u32, timeout_ms: Option<u64>) -> Result<(), String> {
let timeout = timeout_ms.unwrap_or(5000);
#[cfg(any(
target_os = "macos",
target_os = "linux",
target_os = "freebsd",
target_os = "openbsd",
target_os = "netbsd"
))]
{
use std::time::{Duration, Instant};
let pid_str = pid.to_string();
// Try graceful termination first
let _ = std::process::Command::new("kill")
.args(&["-TERM", &pid_str])
.status();
let deadline = Instant::now() + Duration::from_millis(timeout);
while Instant::now() < deadline {
// Check if process still exists: kill -0 <pid>
let alive = std::process::Command::new("kill")
.args(&["-0", &pid_str])
.status()
.map(|s| s.success())
.unwrap_or(false);
if !alive {
return Ok(());
}
std::thread::sleep(Duration::from_millis(100));
}
// Force kill
let _ = std::process::Command::new("kill")
.args(&["-KILL", &pid_str])
.status();
// Final check (best effort)
let alive = std::process::Command::new("kill")
.args(&["-0", &pid_str])
.status()
.map(|s| s.success())
.unwrap_or(false);
if alive {
return Err("Failed to terminate process".to_string());
}
return Ok(());
}
#[cfg(target_os = "windows")]
{
use std::time::{Duration, Instant};
let pid_str = pid.to_string();
let _ = std::process::Command::new("taskkill")
.args(&["/PID", &pid_str, "/F", "/T"])
.status();
let output = std::process::Command::new("tasklist")
.args(&["/FI", &format!("PID eq {}", pid), "/FO", "CSV", "/NH"])
.output()
.map_err(|e| e.to_string())?;
let stdout = String::from_utf8_lossy(&output.stdout).to_string();
if !stdout.trim().is_empty()
&& stdout.contains(&pid_str)
&& !stdout.contains("No tasks are running")
{
return Err("Failed to terminate process".to_string());
}
return Ok(());
}
#[cfg(not(any(
target_os = "macos",
target_os = "linux",
target_os = "freebsd",
target_os = "openbsd",
target_os = "netbsd",
target_os = "windows"
)))]
{
Err("Unsupported platform".to_string())
}
}
#[tauri::command]
fn get_arch() -> String {
let arch = std::env::consts::ARCH;
match arch {
"aarch64" => "arm64".to_string(),
"x86_64" => "amd64".to_string(),
"i386" => "386".to_string(),
_ => "unknown".to_string(),
}
}
#[tauri::command]
fn get_uid() -> String {
return machine_uid::get().unwrap();
}
#[tauri::command]
fn is_rclone_running(port: Option<u16>) -> bool {
if let Some(port) = port {
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr, TcpStream};
use std::time::Duration;
let timeout = Duration::from_millis(200);
let addrs = [
SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), port),
SocketAddr::new(IpAddr::V6(Ipv6Addr::LOCALHOST), port),
];
for addr in addrs.iter() {
if let Ok(stream) = TcpStream::connect_timeout(addr, timeout) {
drop(stream);
return true;
}
}
return false;
}
let system = System::new_all();
for (_pid, process) in system.processes() {
let name = process.name();
let lower = name.to_ascii_lowercase();
if lower == "rclone" || lower == "rclone.exe" {
return true;
}
}
false
}
#[tauri::command]
async fn stop_rclone_processes(timeout_ms: Option<u64>) -> Result<u32, String> {
let timeout = timeout_ms.unwrap_or(5000);
let system = System::new_all();
// Collect PIDs first to avoid holding references across await points
let mut pids: Vec<u32> = Vec::new();
for (pid, process) in system.processes() {
let name_lower = process.name().to_ascii_lowercase();
if name_lower == "rclone" || name_lower == "rclone.exe" {
pids.push(pid.as_u32());
}
}
let mut stopped: u32 = 0;
for pid in pids {
match stop_pid(pid, Some(timeout)).await {
Ok(()) => stopped += 1,
Err(_e) => {}
}
}
Ok(stopped)
}
async fn prompt_password(title: String, message: String) -> Result<Option<String>, String> {
prompt_text(title, message, None, Some(true)).await
}
async fn prompt_text(
title: String,
message: String,
default: Option<String>,
sensitive: Option<bool>,
) -> Result<Option<String>, String> {
#[cfg(target_os = "macos")]
{
use std::process::Command;
let default_value = default.unwrap_or_default();
let is_sensitive = sensitive.unwrap_or(false);
let script = if is_sensitive {
format!(
r#"display dialog "{}" with title "{}" default answer "{}" with hidden answer"#,
message.replace("\"", "\\\""),
title.replace("\"", "\\\""),
default_value.replace("\"", "\\\""),
)
} else {
format!(
r#"display dialog "{}" with title "{}" default answer "{}""#,
message.replace("\"", "\\\""),
title.replace("\"", "\\\""),
default_value.replace("\"", "\\\""),
)
};
let output = Command::new("osascript")
.arg("-e")
.arg(&script)
.output()
.map_err(|e| e.to_string())?;
let pid = std::process::id();
let _ = Command::new("osascript")
.arg("-e")
.arg(format!(
"tell application \"System Events\" to set frontmost of (first process whose unix id is {}) to true",
pid
))
.output();
if output.status.success() {
let result = String::from_utf8_lossy(&output.stdout);
// Parse AppleScript result: "text returned:VALUE, button returned:OK"
if let Some(text_part) = result.split("text returned:").nth(1) {
if let Some(value) = text_part.split(", button returned:").next() {
return Ok(Some(value.trim().to_string()));
}
}
}
return Ok(None);
}
#[cfg(target_os = "windows")]
{
use std::process::Command;
let default_value = default.unwrap_or_default();
let is_sensitive = sensitive.unwrap_or(false);
// Use PowerShell to create a simple text input dialog
let ps_default = default_value.replace('\'', "''");
let ps_title = title.replace('\'', "''");
let ps_message = message.replace('\'', "''");
let ps_password_flag = if is_sensitive { "$true" } else { "$false" };
let powershell_script = format!(
r#"
Add-Type -AssemblyName System.Windows.Forms
Add-Type -AssemblyName System.Drawing
$form = New-Object System.Windows.Forms.Form
$form.Text = '{title}'
$form.Size = New-Object System.Drawing.Size(350, 180)
$form.StartPosition = 'CenterScreen'
$form.FormBorderStyle = 'FixedDialog'
$form.MaximizeBox = $false
$form.MinimizeBox = $false
$form.TopMost = $true
$label = New-Object System.Windows.Forms.Label
$label.Location = New-Object System.Drawing.Point(10, 15)
$label.Size = New-Object System.Drawing.Size(320, 40)
$label.Text = '{message}'
$form.Controls.Add($label)
$textBox = New-Object System.Windows.Forms.TextBox
$textBox.Location = New-Object System.Drawing.Point(10, 60)
$textBox.Size = New-Object System.Drawing.Size(320, 20)
$textBox.Text = '{default}'
$textBox.UseSystemPasswordChar = {password}
$form.Controls.Add($textBox)
$okButton = New-Object System.Windows.Forms.Button
$okButton.Location = New-Object System.Drawing.Point(175, 100)
$okButton.Size = New-Object System.Drawing.Size(75, 23)
$okButton.Text = 'OK'
$okButton.DialogResult = [System.Windows.Forms.DialogResult]::OK
$form.AcceptButton = $okButton
$form.Controls.Add($okButton)
$cancelButton = New-Object System.Windows.Forms.Button
$cancelButton.Location = New-Object System.Drawing.Point(255, 100)
$cancelButton.Size = New-Object System.Drawing.Size(75, 23)
$cancelButton.Text = 'Cancel'
$cancelButton.DialogResult = [System.Windows.Forms.DialogResult]::Cancel
$form.CancelButton = $cancelButton
$form.Controls.Add($cancelButton)
$form.Add_Shown({{$textBox.Select()}})
$result = $form.ShowDialog()
if ($result -eq [System.Windows.Forms.DialogResult]::OK) {{
$textBox.Text
}}
"#,
title = ps_title,
message = ps_message,
default = ps_default,
password = ps_password_flag
);
let output = Command::new("powershell")
.args(&[
"-NoProfile",
"-ExecutionPolicy",
"Bypass",
"-Command",
&powershell_script,
])
.output()
.map_err(|e| e.to_string())?;
if output.status.success() {
let result = String::from_utf8_lossy(&output.stdout).trim().to_string();
if !result.is_empty() || !default_value.is_empty() {
return Ok(Some(result));
}
}
return Ok(None);
}
#[cfg(not(any(target_os = "macos", target_os = "windows")))]
{
Err("Text input not supported on this platform".to_string())
}
}
async fn tiny_prompt_text(
title: String,
message: String,
default: Option<String>,
sensitive: Option<bool>,
) -> Result<Option<String>, String> {
let is_sensitive = sensitive.unwrap_or(false);
let default_value = default.unwrap_or_default();
let title_clone = title.clone();
let message_clone = message.clone();
let result = tauri::async_runtime::spawn_blocking(move || {
if is_sensitive {
tfd::password_box(&title_clone, &message_clone)
} else {
tfd::input_box(&title_clone, &message_clone, &default_value)
}
})
.await
.map_err(|error| error.to_string())?;
Ok(result)
}
#[tauri::command]
async fn prompt(
title: String,
message: String,
default: Option<String>,
sensitive: Option<bool>,
) -> Result<Option<String>, String> {
#[cfg(any(target_os = "macos", target_os = "windows"))]
{
prompt_text(title, message, default, sensitive).await
}
#[cfg(not(any(target_os = "macos", target_os = "windows")))]
{
tiny_prompt_text(title, message, default, sensitive).await
}
}
#[tauri::command]
async fn start_cloudflared_tunnel(app: tauri::AppHandle) -> Result<(u32, String), String> {
use std::io::{BufRead, BufReader};
use std::process::{Command as SysCommand, Stdio};
use std::sync::{Arc, Mutex};
use std::thread;
use std::time::Duration;
// Get the binary path
let app_local_data_dir = app
.path()
.app_local_data_dir()
.map_err(|e| format!("Failed to get app local data directory: {}", e))?;
#[cfg(target_os = "windows")]
let binary_name = "cloudflared.exe";
#[cfg(not(target_os = "windows"))]
let binary_name = "cloudflared";
let cloudflared_path = app_local_data_dir.join(binary_name);
if !cloudflared_path.exists() {
return Err("Cloudflared binary not found".to_string());
}
// Start cloudflared tunnel
let mut child = SysCommand::new(&cloudflared_path)
.args(&["tunnel", "--url", "http://localhost:5572"])
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.map_err(|e| format!("Failed to start cloudflared: {}", e))?;
let pid = child.id();
let tunnel_url = Arc::new(Mutex::new(String::new()));
let tunnel_url_clone = Arc::clone(&tunnel_url);
// Read stdout to extract tunnel URL
if let Some(stderr) = child.stderr.take() {
thread::spawn(move || {
let reader = BufReader::new(stderr);
for line in reader.lines().flatten() {
if line.contains("trycloudflare.com") {
// Extract the URL from the line
if let Some(start) = line.find("https://") {
if let Some(end) = line[start..].find(char::is_whitespace) {
let url = &line[start..start + end];
let mut tunnel_url = tunnel_url_clone.lock().unwrap();
*tunnel_url = url.to_string();
} else {
let url = &line[start..];
let mut tunnel_url = tunnel_url_clone.lock().unwrap();
*tunnel_url = url.to_string();
}
}
}
}
});
}
// Wait for tunnel URL (max 15 seconds)
for _ in 0..150 {
thread::sleep(Duration::from_millis(100));
let url = tunnel_url.lock().unwrap();
if !url.is_empty() {
return Ok((pid, url.clone()));
}
}
// If we didn't get a URL, kill the process and return error
let _ = stop_pid(pid, Some(2000)).await;
Err("Failed to get tunnel URL from cloudflared".to_string())
}
#[tauri::command]
async fn stop_cloudflared_tunnel(pid: u32) -> Result<(), String> {
use std::time::Duration;
// Cloudflared takes ~5s to gracefully shut down, so give it enough time
match stop_pid(pid, Some(6000)).await {
Ok(()) => Ok(()),
Err(e) => {
// Wait a bit for the process to fully terminate
std::thread::sleep(Duration::from_millis(200));
// Even if we get an error, the process might have stopped
// Check one more time if the process is actually gone
#[cfg(any(target_os = "macos", target_os = "linux"))]
{
let alive = std::process::Command::new("kill")
.args(&["-0", &pid.to_string()])
.status()
.map(|s| s.success())
.unwrap_or(false);
if !alive {
// Process is gone, consider it a success
return Ok(());
}
}
#[cfg(target_os = "windows")]
{
let output = std::process::Command::new("tasklist")
.args(&["/FI", &format!("PID eq {}", pid), "/FO", "CSV", "/NH"])
.output();
if let Ok(output) = output {
let stdout = String::from_utf8_lossy(&output.stdout).to_string();
if stdout.trim().is_empty()
|| stdout.contains("No tasks are running")
|| !stdout.contains(&pid.to_string())
{
// Process is gone, consider it a success
return Ok(());
}
}
}
// As a last resort, check if a process with this PID is still a cloudflared process
let system = System::new_all();
let mut cloudflared_still_running = false;
for (p, process) in system.processes() {
if p.as_u32() == pid {
let name = process.name().to_string_lossy().to_lowercase();
if name.contains("cloudflared") {
cloudflared_still_running = true;
}
break;
}
}
if !cloudflared_still_running {
// PID either gone or reused by another process; treat as successfully stopped
return Ok(());
}
Err(e)
}
}
}
#[tauri::command]
fn extract_tgz(tgz_path: &str, output_folder: &str) -> Result<(), String> {
use flate2::read::GzDecoder;
use std::fs::File;
use tar::Archive;
let file = File::open(tgz_path).map_err(|e| e.to_string())?;
let tar = GzDecoder::new(file);
let mut archive = Archive::new(tar);
fs::create_dir_all(output_folder).map_err(|e| e.to_string())?;
archive.set_preserve_permissions(true);
archive.unpack(output_folder).map_err(|e| e.to_string())?;
Ok(())
}
#[tauri::command]
async fn test_proxy_connection(proxy_url: String) -> Result<String, String> {
use std::time::Duration;
// Validate
let proxy_url = proxy_url.trim();
if proxy_url.is_empty() {
return Err("Proxy URL cannot be empty".to_string());
}
// Build client with proxy
let proxy = reqwest::Proxy::all(proxy_url).map_err(|e| format!("Invalid proxy URL: {}", e))?;
let client = reqwest::Client::builder()
.proxy(proxy)
.timeout(Duration::from_secs(10))
.build()
.map_err(|e| format!("Failed to create HTTP client: {}", e))?;
// Multiple fallback endpoints
let candidates = [
"https://httpbin.org/ip",
"https://www.cloudflare.com/cdn-cgi/trace",
"https://ifconfig.me/ip",
"https://1.1.1.1/cdn-cgi/trace",
];
let mut last_error: Option<String> = None;
for url in candidates.iter() {
match client.get(*url).send().await {
Ok(resp) => {
if resp.status().is_success() {
match resp.text().await {
Ok(body) => {
return Ok(format!(
"Connected via proxy. Endpoint: {}. Snippet: {}",
url,
body.chars().take(200).collect::<String>()
))
}
Err(e) => {
last_error =
Some(format!("Failed to read response from {}: {}", url, e));
continue;
}
}
} else {
last_error = Some(format!("{} responded with status {}", url, resp.status()));
continue;
}
}
Err(e) => {
last_error = Some(format!("Request to {} failed: {}", url, e));
continue;
}
}
}
Err(last_error.unwrap_or_else(|| "All proxy tests failed".to_string()))
}
#[tauri::command]
async fn update_system_rclone() -> Result<i32, String> {
#[cfg(target_os = "macos")]
{
use std::process::Command as SysCommand;
fn quote_posix(value: &str) -> String {
let escaped = value.replace("'", "'\\''");
format!("'{}'", escaped)
}
let mut cmdline =
String::from("PATH=/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:$PATH; ");
cmdline.push_str(&quote_posix("rclone"));
cmdline.push(' ');
cmdline.push_str(&quote_posix("selfupdate"));
// Escape for embedding inside an AppleScript string literal
let applescript_cmd = cmdline.replace('\\', "\\\\").replace('"', "\\\"");
let prompt = "Rclone UI needs permission to run rclone selfupdate.";
let script = format!(
"do shell script \"{}\" with administrator privileges with prompt \"{}\"",
applescript_cmd,
prompt.replace('"', "\\\"")
);
let status = SysCommand::new("osascript")
.arg("-e")
.arg(script)
.status()
.map_err(|e| e.to_string())?;
return Ok(status.code().unwrap_or(0));
}
#[cfg(target_os = "linux")]
{
use std::process::Command as SysCommand;
let path_env =
"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/snap/bin";
// Try PolicyKit first (graphical auth prompt on most desktops)
let mut pkexec_args: Vec<String> = Vec::new();
pkexec_args.push("--description".to_string());
pkexec_args.push("Rclone UI needs to run rclone selfupdate".to_string());
pkexec_args.push("env".to_string());
pkexec_args.push(path_env.to_string());
pkexec_args.push("rclone".to_string());
pkexec_args.push("selfupdate".to_string());
match SysCommand::new("pkexec").args(&pkexec_args).status() {
Ok(status) => return Ok(status.code().unwrap_or(0)),
Err(_e) => {
// Fallback to sudo with custom prompt (works if the user has NOPASSWD or cached credentials)
let mut sudo_env = std::collections::HashMap::new();
sudo_env.insert("SUDO_PROMPT", "Rclone UI needs permission to run rclone selfupdate. Please enter your password: ");
let mut sudo_args: Vec<String> = Vec::new();
sudo_args.push("-n".to_string());
sudo_args.push("env".to_string());
sudo_args.push(path_env.to_string());
sudo_args.push("rclone".to_string());
sudo_args.push("selfupdate".to_string());
let status = SysCommand::new("sudo")
.envs(&sudo_env)
.args(&sudo_args)
.status()
.map_err(|e| e.to_string())?;
return Ok(status.code().unwrap_or(0));
}
}
}
#[cfg(target_os = "windows")]
{
use std::process::Command as SysCommand;
fn quote_ps(value: &str) -> String {
// PowerShell single-quote escaping: ' -> ''
format!("'{}'", value.replace('\'', "''"))
}
let file_path = quote_ps("rclone");
let arg_list = String::from("@('selfupdate')");
let ps_script = format!(
"$p = Start-Process -Verb RunAs -WindowStyle Hidden -PassThru -FilePath {file} -ArgumentList {args}; \n\
$p.WaitForExit();\n\
exit $p.ExitCode",
file = file_path,
args = arg_list
);
let status = SysCommand::new("powershell")
.args([
"-NoProfile",
"-ExecutionPolicy",
"Bypass",
"-Command",
&ps_script,
])
.status()
.map_err(|e| e.to_string())?;
return Ok(status.code().unwrap_or(0));
}
#[cfg(not(any(target_os = "macos", target_os = "linux", target_os = "windows")))]
{
Err("Unsupported platform".to_string())
}
}
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
let client = sentry::init((
"https://7c7c55918ff850112780d2b2b29121a6@o4508503751983104.ingest.de.sentry.io/4508739164110928",
sentry::ClientOptions {
release: sentry::release_name!(),
..Default::default()
},
));
let _guard = tauri_plugin_sentry::minidump::init(&client);
let mut builder = tauri::Builder::default();
if !is_flatpak() {
builder = builder
.plugin(tauri_plugin_single_instance::init(|app, _argv, _cwd| {
let _ = shortcut::show_toolbar_window(app);
}));
}
let mut app = builder
.plugin(tauri_plugin_deep_link::init())
.plugin(tauri_plugin_sentry::init_with_no_injection(&client))
.plugin(tauri_plugin_clipboard_manager::init())
.plugin(tauri_plugin_updater::Builder::new().build())
.plugin(tauri_plugin_os::init())
.plugin(tauri_plugin_notification::init())
.plugin(tauri_plugin_process::init())
.plugin(tauri_plugin_autostart::init(
tauri_plugin_autostart::MacosLauncher::LaunchAgent,
Some(vec![]),
))
.plugin(tauri_plugin_store::Builder::new().build())
.plugin(tauri_plugin_http::init())
.plugin(tauri_plugin_fs::init())
.plugin(tauri_plugin_dialog::init())
.plugin(tauri_plugin_log::Builder::new().build())
.plugin(tauri_plugin_shell::init())
.plugin(tauri_plugin_opener::init())
.plugin(tauri_plugin_prevent_default::debug())
.plugin(tauri_plugin_global_shortcut::Builder::new().build())
.invoke_handler(tauri::generate_handler![
unzip_file,
get_arch,
get_uid,
is_rclone_running,
stop_rclone_processes,
prompt,
stop_pid,
update_toolbar_shortcut,
show_toolbar,
update_system_rclone,
test_proxy_connection,
is_flatpak,
is_linux_mint,
has_flatpak_permissions,
open_full_window,
open_window,
open_small_window,
lock_windows,
unlock_windows,
start_cloudflared_tunnel,
stop_cloudflared_tunnel,
extract_tgz
])
.setup(|app| {
#[cfg(target_os = "linux")]
{
// Flatpak/Flathub sandbox typically cannot write to system desktop/mime locations.
// Deep-link registration is best-effort; never fail app startup.
if is_flatpak() {
log::info!("skipping deep-link registration in Flatpak/Flathub");
} else {
use tauri_plugin_deep_link::DeepLinkExt;
if let Err(err) = app.deep_link().register_all() {
log::warn!("deep-link registration failed (continuing): {}", err);
}
}
let cache_dir = app.path().cache_dir()?;
let package_info = app.package_info();
let app_name = package_info.name.as_str();
let app_cache = cache_dir.join(app_name);
if app_cache.exists() {
let _ = fs::remove_dir_all(&app_cache);
}
}
#[cfg(windows)]
{
use tauri_plugin_deep_link::DeepLinkExt;
if let Err(err) = app.deep_link().register_all() {
log::warn!("deep-link registration failed (continuing): {}", err);
}
}
if let Err(err) = ensure_toolbar_window(&app.handle()) {
log::warn!("failed to prepare toolbar window: {}", err);
}
if let Err(err) = set_toolbar_shortcut(&app.handle(), Some(DEFAULT_TOOLBAR_SHORTCUT)) {
log::error!("failed to update default toolbar shortcut: {}", err);
}
Ok(())
})
.build(tauri::generate_context!())
.expect("error while running tauri application");
#[cfg(target_os = "macos")]
app.set_activation_policy(tauri::ActivationPolicy::Accessory);
app.run(|_app, _event| {})
}