add rclone sync functionality and enhance UI styles
This commit is contained in:
parent
e9df98b0ff
commit
bb3cd0120c
6 changed files with 424 additions and 42 deletions
|
@ -10,6 +10,9 @@
|
||||||
"tauri": "tauri"
|
"tauri": "tauri"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"autoprefixer": "^10.4.17",
|
||||||
|
"postcss": "^8.4.35",
|
||||||
|
"tailwindcss": "^3.4.1",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
"@tauri-apps/api": "^2",
|
"@tauri-apps/api": "^2",
|
||||||
|
|
18
src-tauri/Cargo.lock
generated
18
src-tauri/Cargo.lock
generated
|
@ -493,8 +493,10 @@ checksum = "1a7964611d71df112cb1730f2ee67324fcf4d0fc6606acbbe9bfe06df124637c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"android-tzdata",
|
"android-tzdata",
|
||||||
"iana-time-zone",
|
"iana-time-zone",
|
||||||
|
"js-sys",
|
||||||
"num-traits",
|
"num-traits",
|
||||||
"serde",
|
"serde",
|
||||||
|
"wasm-bindgen",
|
||||||
"windows-link",
|
"windows-link",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -2048,11 +2050,13 @@ dependencies = [
|
||||||
name = "my-tauri-app"
|
name = "my-tauri-app"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"chrono",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"tauri",
|
"tauri",
|
||||||
"tauri-build",
|
"tauri-build",
|
||||||
"tauri-plugin-opener",
|
"tauri-plugin-opener",
|
||||||
|
"tokio",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -3879,11 +3883,25 @@ dependencies = [
|
||||||
"bytes",
|
"bytes",
|
||||||
"libc",
|
"libc",
|
||||||
"mio",
|
"mio",
|
||||||
|
"parking_lot",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
|
"signal-hook-registry",
|
||||||
"socket2",
|
"socket2",
|
||||||
|
"tokio-macros",
|
||||||
"windows-sys 0.52.0",
|
"windows-sys 0.52.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tokio-macros"
|
||||||
|
version = "2.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.100",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tokio-util"
|
name = "tokio-util"
|
||||||
version = "0.7.14"
|
version = "0.7.14"
|
||||||
|
|
|
@ -22,6 +22,8 @@ tauri = { version = "2", features = [] }
|
||||||
tauri-plugin-opener = "2"
|
tauri-plugin-opener = "2"
|
||||||
serde = { version = "1", features = ["derive"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
serde_json = "1"
|
serde_json = "1"
|
||||||
|
tokio = { version = "1.0", features = ["full"] }
|
||||||
|
chrono = "0.4"
|
||||||
|
|
||||||
[profile.release]
|
[profile.release]
|
||||||
lto = true # Enables Link-Time Optimization
|
lto = true # Enables Link-Time Optimization
|
||||||
|
|
|
@ -1,3 +1,153 @@
|
||||||
|
// Prevents additional console window on Windows in release, DO NOT REMOVE!!
|
||||||
|
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
||||||
|
|
||||||
|
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use tauri::{Manager, Window};
|
||||||
|
use std::sync::Mutex;
|
||||||
|
use std::process::{Command, Stdio};
|
||||||
|
use std::path::Path;
|
||||||
|
use std::fs::{File, OpenOptions, create_dir_all};
|
||||||
|
use std::io::Write;
|
||||||
|
use std::env;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
struct RcloneConfig {
|
||||||
|
name: String,
|
||||||
|
remote_path: String,
|
||||||
|
local_path: String,
|
||||||
|
access_key: String,
|
||||||
|
secret_key: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
struct SyncStatus {
|
||||||
|
name: String,
|
||||||
|
status: String,
|
||||||
|
transferred: String,
|
||||||
|
bytes: String,
|
||||||
|
errors: usize,
|
||||||
|
last_updated: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct AppState {
|
||||||
|
sync_processes: Mutex<Vec<std::process::Child>>,
|
||||||
|
sync_active: Mutex<bool>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
fn save_config(config: RcloneConfig) -> Result<(), String> {
|
||||||
|
let home_dir = env::var("HOME").map_err(|_| "HOME environment variable not set".to_string())?;
|
||||||
|
let config_path = Path::new(&home_dir).join(".config/rclone/rclone.conf");
|
||||||
|
|
||||||
|
let mut file = OpenOptions::new()
|
||||||
|
.create(true)
|
||||||
|
.append(true)
|
||||||
|
.open(&config_path)
|
||||||
|
.map_err(|e| format!("Failed to open config file: {}", e))?;
|
||||||
|
|
||||||
|
writeln!(file, "[{}]", config.name)
|
||||||
|
.and_then(|_| writeln!(file, "type = s3"))
|
||||||
|
.and_then(|_| writeln!(file, "provider = Other"))
|
||||||
|
.and_then(|_| writeln!(file, "access_key_id = {}", config.access_key))
|
||||||
|
.and_then(|_| writeln!(file, "secret_access_key = {}", config.secret_key))
|
||||||
|
.and_then(|_| writeln!(file, "endpoint = https://drive-api.pragmatismo.com.br"))
|
||||||
|
.and_then(|_| writeln!(file, "acl = private"))
|
||||||
|
.map_err(|e| format!("Failed to write config: {}", e))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
fn start_sync(config: RcloneConfig, state: tauri::State<AppState>) -> Result<(), String> {
|
||||||
|
let local_path = Path::new(&config.local_path);
|
||||||
|
if !local_path.exists() {
|
||||||
|
create_dir_all(local_path).map_err(|e| format!("Failed to create local path: {}", e))?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let child = Command::new("rclone")
|
||||||
|
.arg("sync")
|
||||||
|
.arg(&config.remote_path)
|
||||||
|
.arg(&config.local_path)
|
||||||
|
.arg("--no-check-certificate")
|
||||||
|
.arg("--verbose")
|
||||||
|
.arg("--rc")
|
||||||
|
.stdout(Stdio::null())
|
||||||
|
.stderr(Stdio::null())
|
||||||
|
.spawn()
|
||||||
|
.map_err(|e| format!("Failed to start rclone: {}", e))?;
|
||||||
|
|
||||||
|
state.sync_processes.lock().unwrap().push(child);
|
||||||
|
*state.sync_active.lock().unwrap() = true;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
fn stop_sync(state: tauri::State<AppState>) -> Result<(), String> {
|
||||||
|
let mut processes = state.sync_processes.lock().unwrap();
|
||||||
|
for child in processes.iter_mut() {
|
||||||
|
child.kill().map_err(|e| format!("Failed to kill process: {}", e))?;
|
||||||
|
}
|
||||||
|
processes.clear();
|
||||||
|
*state.sync_active.lock().unwrap() = false;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
fn get_status(remote_name: String) -> Result<SyncStatus, String> {
|
||||||
|
let output = Command::new("rclone")
|
||||||
|
.arg("rc")
|
||||||
|
.arg("core/stats")
|
||||||
|
.arg("--json")
|
||||||
|
.output()
|
||||||
|
.map_err(|e| format!("Failed to execute rclone rc: {}", e))?;
|
||||||
|
|
||||||
|
if !output.status.success() {
|
||||||
|
return Err(format!("rclone rc failed: {}", String::from_utf8_lossy(&output.stderr)));
|
||||||
|
}
|
||||||
|
|
||||||
|
let json = String::from_utf8_lossy(&output.stdout);
|
||||||
|
let value: serde_json::Value = serde_json::from_str(&json)
|
||||||
|
.map_err(|e| format!("Failed to parse rclone status: {}", e))?;
|
||||||
|
|
||||||
|
let transferred = value.get("bytes").and_then(|v| v.as_u64()).unwrap_or(0);
|
||||||
|
let errors = value.get("errors").and_then(|v| v.as_u64()).unwrap_or(0);
|
||||||
|
let speed = value.get("speed").and_then(|v| v.as_f64()).unwrap_or(0.0);
|
||||||
|
|
||||||
|
let status = if errors > 0 {
|
||||||
|
"Error occurred".to_string()
|
||||||
|
} else if speed > 0.0 {
|
||||||
|
"Transferring".to_string()
|
||||||
|
} else if transferred > 0 {
|
||||||
|
"Completed".to_string()
|
||||||
|
} else {
|
||||||
|
"Initializing".to_string()
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(SyncStatus {
|
||||||
|
name: remote_name,
|
||||||
|
status,
|
||||||
|
transferred: format_bytes(transferred),
|
||||||
|
bytes: format!("{}/s", format_bytes(speed as u64)),
|
||||||
|
errors: errors as usize,
|
||||||
|
last_updated: chrono::Local::now().format("%H:%M:%S").to_string(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn format_bytes(bytes: u64) -> String {
|
||||||
|
const KB: u64 = 1024;
|
||||||
|
const MB: u64 = KB * 1024;
|
||||||
|
const GB: u64 = MB * 1024;
|
||||||
|
|
||||||
|
if bytes >= GB {
|
||||||
|
format!("{:.2} GB", bytes as f64 / GB as f64)
|
||||||
|
} else if bytes >= MB {
|
||||||
|
format!("{:.2} MB", bytes as f64 / MB as f64)
|
||||||
|
} else if bytes >= KB {
|
||||||
|
format!("{:.2} KB", bytes as f64 / KB as f64)
|
||||||
|
} else {
|
||||||
|
format!("{} B", bytes)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Learn more about Tauri commands at https://tauri.app/develop/calling-rust/
|
// Learn more about Tauri commands at https://tauri.app/develop/calling-rust/
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
fn greet(name: &str) -> String {
|
fn greet(name: &str) -> String {
|
||||||
|
@ -7,8 +157,16 @@ fn greet(name: &str) -> String {
|
||||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||||
pub fn run() {
|
pub fn run() {
|
||||||
tauri::Builder::default()
|
tauri::Builder::default()
|
||||||
.plugin(tauri_plugin_opener::init())
|
.manage(AppState {
|
||||||
.invoke_handler(tauri::generate_handler![greet])
|
sync_processes: Mutex::new(Vec::new()),
|
||||||
|
sync_active: Mutex::new(false),
|
||||||
|
})
|
||||||
|
.plugin(tauri_plugin_opener::init())
|
||||||
|
.invoke_handler(tauri::generate_handler![
|
||||||
|
save_config,
|
||||||
|
start_sync,
|
||||||
|
stop_sync,
|
||||||
|
get_status])
|
||||||
.run(tauri::generate_context!())
|
.run(tauri::generate_context!())
|
||||||
.expect("error while running tauri application");
|
.expect("error while running tauri application");
|
||||||
}
|
}
|
||||||
|
|
49
src/App.css
49
src/App.css
|
@ -1,3 +1,51 @@
|
||||||
|
.app {
|
||||||
|
padding: 20px;
|
||||||
|
max-width: 800px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-bar {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-screen, .status-screen {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 15px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog {
|
||||||
|
position: fixed;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
background-color: white;
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
|
||||||
|
z-index: 100;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 15px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-item {
|
||||||
|
background-color: #f0f0f0;
|
||||||
|
padding: 15px;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Existing styles below */
|
||||||
.logo.vite:hover {
|
.logo.vite:hover {
|
||||||
filter: drop-shadow(0 0 2em #747bff);
|
filter: drop-shadow(0 0 2em #747bff);
|
||||||
}
|
}
|
||||||
|
@ -5,6 +53,7 @@
|
||||||
.logo.react:hover {
|
.logo.react:hover {
|
||||||
filter: drop-shadow(0 0 2em #61dafb);
|
filter: drop-shadow(0 0 2em #61dafb);
|
||||||
}
|
}
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
font-family: Inter, Avenir, Helvetica, Arial, sans-serif;
|
font-family: Inter, Avenir, Helvetica, Arial, sans-serif;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
|
|
230
src/App.tsx
230
src/App.tsx
|
@ -1,50 +1,202 @@
|
||||||
import { useState } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import reactLogo from "./assets/react.svg";
|
|
||||||
import { invoke } from "@tauri-apps/api/core";
|
import { invoke } from "@tauri-apps/api/core";
|
||||||
import "./App.css";
|
import "./App.css";
|
||||||
|
|
||||||
function App() {
|
interface RcloneConfig {
|
||||||
const [greetMsg, setGreetMsg] = useState("");
|
name: string;
|
||||||
const [name, setName] = useState("");
|
remote_path: string;
|
||||||
|
local_path: string;
|
||||||
|
access_key: string;
|
||||||
|
secret_key: string;
|
||||||
|
}
|
||||||
|
|
||||||
async function greet() {
|
interface SyncStatus {
|
||||||
// Learn more about Tauri commands at https://tauri.app/develop/calling-rust/
|
name: string;
|
||||||
setGreetMsg(await invoke("greet", { name }));
|
status: string;
|
||||||
}
|
transferred: string;
|
||||||
|
bytes: string;
|
||||||
|
errors: number;
|
||||||
|
last_updated: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
type Screen = "Main" | "Status";
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
const [state, setState] = useState({
|
||||||
|
name: "",
|
||||||
|
access_key: "",
|
||||||
|
secret_key: "",
|
||||||
|
status_text: "Enter credentials to set up sync",
|
||||||
|
sync_statuses: [] as SyncStatus[],
|
||||||
|
show_config_dialog: false,
|
||||||
|
show_about_dialog: false,
|
||||||
|
current_screen: "Main" as Screen,
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (state.current_screen === "Status") {
|
||||||
|
const interval = setInterval(() => {
|
||||||
|
// In a real app, you would fetch actual configs here
|
||||||
|
// This is just a mock implementation
|
||||||
|
invoke<SyncStatus>("get_status", { remoteName: "example" })
|
||||||
|
.then((status) => {
|
||||||
|
setState(prev => ({
|
||||||
|
...prev,
|
||||||
|
sync_statuses: [status]
|
||||||
|
}));
|
||||||
|
})
|
||||||
|
.catch(console.error);
|
||||||
|
}, 5000);
|
||||||
|
return () => clearInterval(interval);
|
||||||
|
}
|
||||||
|
}, [state.current_screen]);
|
||||||
|
|
||||||
|
const saveConfig = async () => {
|
||||||
|
if (!state.name || !state.access_key || !state.secret_key) {
|
||||||
|
setState(prev => ({ ...prev, status_text: "All fields are required!" }));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const config: RcloneConfig = {
|
||||||
|
name: state.name,
|
||||||
|
remote_path: `s3://${state.name}`,
|
||||||
|
local_path: `${window.__TAURI__.path.homeDir}/General Bots/${state.name}`,
|
||||||
|
access_key: state.access_key,
|
||||||
|
secret_key: state.secret_key,
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
await invoke("save_config", { config });
|
||||||
|
setState(prev => ({
|
||||||
|
...prev,
|
||||||
|
status_text: "New sync saved!",
|
||||||
|
show_config_dialog: false
|
||||||
|
}));
|
||||||
|
} catch (e) {
|
||||||
|
setState(prev => ({
|
||||||
|
...prev,
|
||||||
|
status_text: `Failed to save config: ${e}`
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const startSync = async () => {
|
||||||
|
// In a real app, you would fetch actual configs here
|
||||||
|
const config: RcloneConfig = {
|
||||||
|
name: state.name || "example",
|
||||||
|
remote_path: `s3://${state.name || "example"}`,
|
||||||
|
local_path: `${window.__TAURI__.path.homeDir}/General Bots/${state.name || "example"}`,
|
||||||
|
access_key: state.access_key || "dummy",
|
||||||
|
secret_key: state.secret_key || "dummy",
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
await invoke("start_sync", { config });
|
||||||
|
setState(prev => ({
|
||||||
|
...prev,
|
||||||
|
status_text: "Sync started!"
|
||||||
|
}));
|
||||||
|
} catch (e) {
|
||||||
|
setState(prev => ({
|
||||||
|
...prev,
|
||||||
|
status_text: `Failed to start sync: ${e}`
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const stopSync = async () => {
|
||||||
|
try {
|
||||||
|
await invoke("stop_sync");
|
||||||
|
setState(prev => ({
|
||||||
|
...prev,
|
||||||
|
status_text: "Sync stopped."
|
||||||
|
}));
|
||||||
|
} catch (e) {
|
||||||
|
setState(prev => ({
|
||||||
|
...prev,
|
||||||
|
status_text: `Failed to stop sync: ${e}`
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main className="container">
|
<div className="app">
|
||||||
<h1>Welcome to Tauri + React</h1>
|
<div className="menu-bar">
|
||||||
|
<button onClick={() => setState(prev => ({ ...prev, show_config_dialog: true }))}>
|
||||||
<div className="row">
|
Add Sync Configuration
|
||||||
<a href="https://vitejs.dev" target="_blank">
|
</button>
|
||||||
<img src="/vite.svg" className="logo vite" alt="Vite logo" />
|
<button onClick={() => setState(prev => ({ ...prev, show_about_dialog: true }))}>
|
||||||
</a>
|
About
|
||||||
<a href="https://tauri.app" target="_blank">
|
</button>
|
||||||
<img src="/tauri.svg" className="logo tauri" alt="Tauri logo" />
|
|
||||||
</a>
|
|
||||||
<a href="https://reactjs.org" target="_blank">
|
|
||||||
<img src={reactLogo} className="logo react" alt="React logo" />
|
|
||||||
</a>
|
|
||||||
</div>
|
</div>
|
||||||
<p>Click on the Tauri, Vite, and React logos to learn more.</p>
|
|
||||||
|
|
||||||
<form
|
{state.current_screen === "Main" ? (
|
||||||
className="row"
|
<div className="main-screen">
|
||||||
onSubmit={(e) => {
|
<h1>General Bots</h1>
|
||||||
e.preventDefault();
|
<p>{state.status_text}</p>
|
||||||
greet();
|
<button onClick={startSync}>Start Sync</button>
|
||||||
}}
|
<button onClick={stopSync}>Stop Sync</button>
|
||||||
>
|
<button onClick={() => setState(prev => ({ ...prev, current_screen: "Status" }))}>
|
||||||
<input
|
Show Status
|
||||||
id="greet-input"
|
</button>
|
||||||
onChange={(e) => setName(e.currentTarget.value)}
|
</div>
|
||||||
placeholder="Enter a name..."
|
) : (
|
||||||
/>
|
<div className="status-screen">
|
||||||
<button type="submit">Greet</button>
|
<h1>Sync Status</h1>
|
||||||
</form>
|
<div className="status-list">
|
||||||
<p>{greetMsg}</p>
|
{state.sync_statuses.map((status, index) => (
|
||||||
</main>
|
<div key={index} className="status-item">
|
||||||
|
<h2>{status.name}</h2>
|
||||||
|
<p>Status: {status.status}</p>
|
||||||
|
<p>Transferred: {status.transferred}</p>
|
||||||
|
<p>Bytes: {status.bytes}</p>
|
||||||
|
<p>Errors: {status.errors}</p>
|
||||||
|
<p>Last Updated: {status.last_updated}</p>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<button onClick={() => setState(prev => ({ ...prev, current_screen: "Main" }))}>
|
||||||
|
Back
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{state.show_config_dialog && (
|
||||||
|
<div className="dialog">
|
||||||
|
<h2>Add Sync Configuration</h2>
|
||||||
|
<input
|
||||||
|
value={state.name}
|
||||||
|
onChange={(e) => setState(prev => ({ ...prev, name: e.target.value }))}
|
||||||
|
placeholder="Enter sync name"
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
value={state.access_key}
|
||||||
|
onChange={(e) => setState(prev => ({ ...prev, access_key: e.target.value }))}
|
||||||
|
placeholder="Enter access key"
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
value={state.secret_key}
|
||||||
|
onChange={(e) => setState(prev => ({ ...prev, secret_key: e.target.value }))}
|
||||||
|
placeholder="Enter secret key"
|
||||||
|
/>
|
||||||
|
<button onClick={saveConfig}>Save</button>
|
||||||
|
<button onClick={() => setState(prev => ({ ...prev, show_config_dialog: false }))}>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{state.show_about_dialog && (
|
||||||
|
<div className="dialog">
|
||||||
|
<h2>About General Bots</h2>
|
||||||
|
<p>Version: 1.0.0</p>
|
||||||
|
<p>A professional-grade sync tool for OneDrive/Dropbox-like functionality.</p>
|
||||||
|
<button onClick={() => setState(prev => ({ ...prev, show_about_dialog: false }))}>
|
||||||
|
Close
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Add table
Reference in a new issue