feat: add new pages for music, video, and editor; implement data table with user navigation
This commit is contained in:
parent
63941e41c3
commit
c1769e0d1a
21 changed files with 507 additions and 22 deletions
|
@ -1,19 +1,32 @@
|
||||||
"use client";
|
"use client";
|
||||||
import React, { createContext, useContext, useState, useEffect } from 'react';
|
import React, { createContext, useContext, useState, useEffect } from 'react';
|
||||||
import { core } from '@tauri-apps/api';
|
|
||||||
import { User, ChatInstance } from '../types';
|
import { User, ChatInstance } from '../types';
|
||||||
|
|
||||||
interface ChatContextType {
|
interface ChatContextType {
|
||||||
line: any;
|
line: any;
|
||||||
user: User;
|
user: User;
|
||||||
instance: ChatInstance | null;
|
instance: ChatInstance | null;
|
||||||
sendActivity: (activity: any) => void;
|
sendActivity: (activity: any) => Promise<void>;
|
||||||
selectedVoice: any;
|
selectedVoice: any;
|
||||||
setVoice: (voice: any) => void;
|
setVoice: (voice: any) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ChatContext = createContext<ChatContextType | undefined>(undefined);
|
const ChatContext = createContext<ChatContextType | undefined>(undefined);
|
||||||
|
|
||||||
|
// Unified API caller that works in both environments
|
||||||
|
async function apiFetch(endpoint: string, options?: RequestInit) {
|
||||||
|
const baseUrl = 'http://localhost:4242/'; //typeof window !== 'undefined' ? window.location.origin : '';
|
||||||
|
|
||||||
|
if (typeof window !== 'undefined' && '__TAURI__' in window) {
|
||||||
|
// Tauri environment - use HTTP module for better security
|
||||||
|
const { http } = await import('@tauri-apps/api');
|
||||||
|
return http.fetch(`${baseUrl}${endpoint}`, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Web environment - standard fetch
|
||||||
|
return fetch(`${baseUrl}${endpoint}`, options);
|
||||||
|
}
|
||||||
|
|
||||||
export function ChatProvider({ children }: { children: React.ReactNode }) {
|
export function ChatProvider({ children }: { children: React.ReactNode }) {
|
||||||
const [line, setLine] = useState<any>(null);
|
const [line, setLine] = useState<any>(null);
|
||||||
const [instance, setInstance] = useState<ChatInstance | null>(null);
|
const [instance, setInstance] = useState<ChatInstance | null>(null);
|
||||||
|
@ -26,16 +39,25 @@ export function ChatProvider({ children }: { children: React.ReactNode }) {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const initializeChat = async () => {
|
const initializeChat = async () => {
|
||||||
try {
|
try {
|
||||||
const botId = window.location.pathname.split('/')[1] || 'default';
|
const botId = 'doula'; // window.location.pathname.split('/')[1] || 'default';
|
||||||
const instanceData = await core.invoke('get_chat_instance', { botId });
|
|
||||||
setInstance(instanceData as ChatInstance);
|
|
||||||
|
|
||||||
// Initialize DirectLine or other chat service
|
// Get instance from REST API
|
||||||
const directLine = {
|
const response = await apiFetch(`/instances/${botId}`);
|
||||||
|
if (!response.ok) throw new Error('Failed to get chat instance');
|
||||||
|
const instanceData = await response.json();
|
||||||
|
setInstance(instanceData);
|
||||||
|
|
||||||
|
// Initialize chat service
|
||||||
|
const chatService = {
|
||||||
activity$: { subscribe: () => {} },
|
activity$: { subscribe: () => {} },
|
||||||
postActivity: () => ({ subscribe: () => {} })
|
postActivity: (activity: any) => ({
|
||||||
|
subscribe: (observer: any) => {
|
||||||
|
// Handle real-time updates if needed
|
||||||
|
return { unsubscribe: () => {} };
|
||||||
|
}
|
||||||
|
})
|
||||||
};
|
};
|
||||||
setLine(directLine);
|
setLine(chatService);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to initialize chat:', error);
|
console.error('Failed to initialize chat:', error);
|
||||||
}
|
}
|
||||||
|
@ -46,14 +68,25 @@ export function ChatProvider({ children }: { children: React.ReactNode }) {
|
||||||
|
|
||||||
const sendActivity = async (activity: any) => {
|
const sendActivity = async (activity: any) => {
|
||||||
try {
|
try {
|
||||||
await core.invoke('send_chat_activity', {
|
const fullActivity = {
|
||||||
activity: {
|
|
||||||
...activity,
|
...activity,
|
||||||
from: user,
|
from: user,
|
||||||
timestamp: new Date().toISOString()
|
timestamp: new Date().toISOString()
|
||||||
}
|
};
|
||||||
|
|
||||||
|
// Send activity via REST API
|
||||||
|
const response = await apiFetch('/activities', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify(fullActivity)
|
||||||
});
|
});
|
||||||
line?.postActivity(activity).subscribe();
|
|
||||||
|
if (!response.ok) throw new Error('Failed to send activity');
|
||||||
|
|
||||||
|
// Notify local chat service
|
||||||
|
line?.postActivity(fullActivity).subscribe();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to send activity:', error);
|
console.error('Failed to send activity:', error);
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,12 +4,15 @@ import { usePathname, useRouter } from 'next/navigation';
|
||||||
import { Button } from '../src/components/ui/button';
|
import { Button } from '../src/components/ui/button';
|
||||||
|
|
||||||
const examples = [
|
const examples = [
|
||||||
{ name: "Home", href: "/authentication" },
|
{ name: "Home", href: "/auth" },
|
||||||
{ name: "Dashboard", href: "/dashboard" },
|
{ name: "Dashboard", href: "/dashboard" },
|
||||||
{ name: "Chat", href: "/chat" },
|
{ name: "Chat", href: "/chat" },
|
||||||
{ name: "Mail", href: "/mail" },
|
{ name: "Mail", href: "/mail" },
|
||||||
{ name: "Drive", href: "/drive" },
|
{ name: "Tree", href: "/tree" },
|
||||||
{ name: "Tasks", href: "/tasks" },
|
{ name: "Editor", href: "/editor" },
|
||||||
|
{ name: "Tables", href: "/table" },
|
||||||
|
{ name: "Video", href: "/video" },
|
||||||
|
{ name: "Music", href: "/music" },
|
||||||
{ name: "Templates", href: "/templates" },
|
{ name: "Templates", href: "/templates" },
|
||||||
{ name: "Settings", href: "/sync" },
|
{ name: "Settings", href: "/sync" },
|
||||||
];
|
];
|
||||||
|
|
0
app/editor/page.tsx
Normal file
0
app/editor/page.tsx
Normal file
0
app/music/page.tsx
Normal file
0
app/music/page.tsx
Normal file
3
app/tree/components/editors/tabular/README.md
Normal file
3
app/tree/components/editors/tabular/README.md
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
|
||||||
|
Lotus 123.
|
||||||
|
https://docs.sheetjs.com/docs/demos/desktop/tauri
|
|
@ -4,6 +4,8 @@ import { FileTree } from './components/FileTree';
|
||||||
import { FileBrowser } from './components/FileBrowser';
|
import { FileBrowser } from './components/FileBrowser';
|
||||||
import { FileOperations } from './components/FileOperations';
|
import { FileOperations } from './components/FileOperations';
|
||||||
|
|
||||||
|
// TODO: XTREE like keyword.
|
||||||
|
|
||||||
export default function DriveScreen() {
|
export default function DriveScreen() {
|
||||||
const [currentPath, setCurrentPath] = useState('');
|
const [currentPath, setCurrentPath] = useState('');
|
||||||
const [refreshKey, setRefreshKey] = useState(0);
|
const [refreshKey, setRefreshKey] = useState(0);
|
0
app/video/page.tsx
Normal file
0
app/video/page.tsx
Normal file
444
src-rust/src/local-sync.rs
Normal file
444
src-rust/src/local-sync.rs
Normal file
|
@ -0,0 +1,444 @@
|
||||||
|
use dioxus::prelude::*;
|
||||||
|
use dioxus_desktop::{use_window, LogicalSize};
|
||||||
|
use std::env;
|
||||||
|
use std::fs::{File, OpenOptions, create_dir_all};
|
||||||
|
use std::io::{BufRead, BufReader, Write};
|
||||||
|
use std::path::Path;
|
||||||
|
use std::process::{Command as ProcCommand, Child, Stdio};
|
||||||
|
use std::sync::{Arc, Mutex};
|
||||||
|
use std::thread;
|
||||||
|
use std::time::{Duration, Instant};
|
||||||
|
use notify_rust::Notification;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use serde_json::Value;
|
||||||
|
|
||||||
|
// App state
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
struct AppState {
|
||||||
|
name: String,
|
||||||
|
access_key: String,
|
||||||
|
secret_key: String,
|
||||||
|
status_text: String,
|
||||||
|
sync_processes: Arc<Mutex<Vec<Child>>>,
|
||||||
|
sync_active: Arc<Mutex<bool>>,
|
||||||
|
sync_statuses: Arc<Mutex<Vec<SyncStatus>>>,
|
||||||
|
show_config_dialog: bool,
|
||||||
|
show_about_dialog: bool,
|
||||||
|
current_screen: Screen,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
enum Screen {
|
||||||
|
Main,
|
||||||
|
Status,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
enum Message {
|
||||||
|
NameChanged(String),
|
||||||
|
AccessKeyChanged(String),
|
||||||
|
SecretKeyChanged(String),
|
||||||
|
SaveConfig,
|
||||||
|
StartSync,
|
||||||
|
StopSync,
|
||||||
|
UpdateStatus(Vec<SyncStatus>),
|
||||||
|
ShowConfigDialog(bool),
|
||||||
|
ShowAboutDialog(bool),
|
||||||
|
ShowStatusScreen,
|
||||||
|
BackToMain,
|
||||||
|
None,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
dioxus_desktop::launch(app);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn app(cx: Scope) -> Element {
|
||||||
|
let window = use_window();
|
||||||
|
window.set_inner_size(LogicalSize::new(800, 600));
|
||||||
|
|
||||||
|
let state = use_ref(cx, || AppState {
|
||||||
|
name: String::new(),
|
||||||
|
access_key: String::new(),
|
||||||
|
secret_key: String::new(),
|
||||||
|
status_text: "Enter credentials to set up sync".to_string(),
|
||||||
|
sync_processes: Arc::new(Mutex::new(Vec::new())),
|
||||||
|
sync_active: Arc::new(Mutex::new(false)),
|
||||||
|
sync_statuses: Arc::new(Mutex::new(Vec::new())),
|
||||||
|
show_config_dialog: false,
|
||||||
|
show_about_dialog: false,
|
||||||
|
current_screen: Screen::Main,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Monitor sync status
|
||||||
|
use_future( async move {
|
||||||
|
let state = state.clone();
|
||||||
|
async move {
|
||||||
|
let mut last_check = Instant::now();
|
||||||
|
let check_interval = Duration::from_secs(5);
|
||||||
|
|
||||||
|
loop {
|
||||||
|
tokio::time::sleep(Duration::from_secs(1)).await;
|
||||||
|
|
||||||
|
if !*state.read().sync_active.lock().unwrap() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if last_check.elapsed() < check_interval {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
last_check = Instant::now();
|
||||||
|
|
||||||
|
match read_rclone_configs() {
|
||||||
|
Ok(configs) => {
|
||||||
|
let mut new_statuses = Vec::new();
|
||||||
|
for config in configs {
|
||||||
|
match get_rclone_status(&config.name) {
|
||||||
|
Ok(status) => new_statuses.push(status),
|
||||||
|
Err(e) => eprintln!("Failed to get status: {}", e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*state.write().sync_statuses.lock().unwrap() = new_statuses.clone();
|
||||||
|
state.write().status_text = format!("Syncing {} repositories...", new_statuses.len());
|
||||||
|
}
|
||||||
|
Err(e) => eprintln!("Failed to read configs: {}", e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
cx.render(rsx! {
|
||||||
|
div {
|
||||||
|
class: "app",
|
||||||
|
// Main menu bar
|
||||||
|
div {
|
||||||
|
class: "menu-bar",
|
||||||
|
button {
|
||||||
|
onclick: move |_| state.write().show_config_dialog = true,
|
||||||
|
"Add Sync Configuration"
|
||||||
|
}
|
||||||
|
button {
|
||||||
|
onclick: move |_| state.write().show_about_dialog = true,
|
||||||
|
"About"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Main content
|
||||||
|
{match state.read().current_screen {
|
||||||
|
Screen::Main => rsx! {
|
||||||
|
div {
|
||||||
|
class: "main-screen",
|
||||||
|
h1 { "General Bots" }
|
||||||
|
p { "{state.read().status_text}" }
|
||||||
|
button {
|
||||||
|
onclick: move |_| start_sync(&state),
|
||||||
|
"Start Sync"
|
||||||
|
}
|
||||||
|
button {
|
||||||
|
onclick: move |_| stop_sync(&state),
|
||||||
|
"Stop Sync"
|
||||||
|
}
|
||||||
|
button {
|
||||||
|
onclick: move |_| state.write().current_screen = Screen::Status,
|
||||||
|
"Show Status"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Screen::Status => rsx! {
|
||||||
|
div {
|
||||||
|
class: "status-screen",
|
||||||
|
h1 { "Sync Status" }
|
||||||
|
div {
|
||||||
|
class: "status-list",
|
||||||
|
for status in state.read().sync_statuses.lock().unwrap().iter() {
|
||||||
|
div {
|
||||||
|
class: "status-item",
|
||||||
|
h2 { "{status.name}" }
|
||||||
|
p { "Status: {status.status}" }
|
||||||
|
p { "Transferred: {status.transferred}" }
|
||||||
|
p { "Bytes: {status.bytes}" }
|
||||||
|
p { "Errors: {status.errors}" }
|
||||||
|
p { "Last Updated: {status.last_updated}" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
button {
|
||||||
|
onclick: move |_| state.write().current_screen = Screen::Main,
|
||||||
|
"Back"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
|
||||||
|
// Config dialog
|
||||||
|
if state.read().show_config_dialog {
|
||||||
|
div {
|
||||||
|
class: "dialog",
|
||||||
|
h2 { "Add Sync Configuration" }
|
||||||
|
input {
|
||||||
|
value: "{state.read().name}",
|
||||||
|
oninput: move |e| state.write().name = e.value.clone(),
|
||||||
|
placeholder: "Enter sync name",
|
||||||
|
}
|
||||||
|
input {
|
||||||
|
value: "{state.read().access_key}",
|
||||||
|
oninput: move |e| state.write().access_key = e.value.clone(),
|
||||||
|
placeholder: "Enter access key",
|
||||||
|
}
|
||||||
|
input {
|
||||||
|
value: "{state.read().secret_key}",
|
||||||
|
oninput: move |e| state.write().secret_key = e.value.clone(),
|
||||||
|
placeholder: "Enter secret key",
|
||||||
|
}
|
||||||
|
button {
|
||||||
|
onclick: move |_| {
|
||||||
|
save_config(&state);
|
||||||
|
state.write().show_config_dialog = false;
|
||||||
|
},
|
||||||
|
"Save"
|
||||||
|
}
|
||||||
|
button {
|
||||||
|
onclick: move |_| state.write().show_config_dialog = false,
|
||||||
|
"Cancel"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// About dialog
|
||||||
|
if state.read().show_about_dialog {
|
||||||
|
div {
|
||||||
|
class: "dialog",
|
||||||
|
h2 { "About General Bots" }
|
||||||
|
p { "Version: 1.0.0" }
|
||||||
|
p { "A professional-grade sync tool for OneDrive/Dropbox-like functionality." }
|
||||||
|
button {
|
||||||
|
onclick: move |_| state.write().show_about_dialog = false,
|
||||||
|
"Close"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save sync configuration
|
||||||
|
fn save_config(state: &UseRef<AppState>) {
|
||||||
|
if state.read().name.is_empty() || state.read().access_key.is_empty() || state.read().secret_key.is_empty() {
|
||||||
|
state.write_with(|state| state.status_text = "All fields are required!".to_string());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let new_config = RcloneConfig {
|
||||||
|
name: state.read().name.clone(),
|
||||||
|
remote_path: format!("s3://{}", state.read().name),
|
||||||
|
local_path: Path::new(&env::var("HOME").unwrap()).join("General Bots").join(&state.read().name).to_string_lossy().to_string(),
|
||||||
|
access_key: state.read().access_key.clone(),
|
||||||
|
secret_key: state.read().secret_key.clone(),
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Err(e) = save_rclone_config(&new_config) {
|
||||||
|
state.write_with(|state| state.status_text = format!("Failed to save config: {}", e));
|
||||||
|
} else {
|
||||||
|
state.write_with(|state| state.status_text = "New sync saved!".to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start sync process
|
||||||
|
fn start_sync(state: &UseRef<AppState>) {
|
||||||
|
let mut processes = state.write_with(|state| state.sync_processes.lock().unwrap());
|
||||||
|
processes.clear();
|
||||||
|
|
||||||
|
match read_rclone_configs() {
|
||||||
|
Ok(configs) => {
|
||||||
|
for config in configs {
|
||||||
|
match run_sync(&config) {
|
||||||
|
Ok(child) => processes.push(child),
|
||||||
|
Err(e) => eprintln!("Failed to start sync: {}", e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
state.write_with(|state| *state.sync_active.lock().unwrap() = true);
|
||||||
|
state.write_with(|state| state.status_text = format!("Syncing with {} configurations.", processes.len()));
|
||||||
|
}
|
||||||
|
Err(e) => state.write_with(|state| state.status_text = format!("Failed to read configurations: {}", e)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop sync process
|
||||||
|
fn stop_sync(state: &UseRef<AppState>) {
|
||||||
|
let mut processes = state.write_with(|state| state.sync_processes.lock().unwrap());
|
||||||
|
for child in processes.iter_mut() {
|
||||||
|
let _ = child.kill();
|
||||||
|
}
|
||||||
|
processes.clear();
|
||||||
|
state.write_with(|state| *state.sync_active.lock().unwrap() = false);
|
||||||
|
state.write_with(|state| state.status_text = "Sync stopped.".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Utility functions (rclone, notifications, etc.)
|
||||||
|
fn save_rclone_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))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_rclone_configs() -> Result<Vec<RcloneConfig>, 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");
|
||||||
|
|
||||||
|
if !config_path.exists() {
|
||||||
|
return Ok(Vec::new());
|
||||||
|
}
|
||||||
|
|
||||||
|
let file = File::open(&config_path).map_err(|e| format!("Failed to open config file: {}", e))?;
|
||||||
|
let reader = BufReader::new(file);
|
||||||
|
let mut configs = Vec::new();
|
||||||
|
let mut current_config: Option<RcloneConfig> = None;
|
||||||
|
|
||||||
|
for line in reader.lines() {
|
||||||
|
let line = line.map_err(|e| format!("Failed to read line: {}", e))?;
|
||||||
|
if line.is_empty() || line.starts_with('#') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if line.starts_with('[') && line.ends_with(']') {
|
||||||
|
if let Some(config) = current_config.take() {
|
||||||
|
configs.push(config);
|
||||||
|
}
|
||||||
|
let name = line[1..line.len()-1].to_string();
|
||||||
|
current_config = Some(RcloneConfig {
|
||||||
|
name: name.clone(),
|
||||||
|
remote_path: format!("s3://{}", name),
|
||||||
|
local_path: Path::new(&home_dir).join("General Bots").join(&name).to_string_lossy().to_string(),
|
||||||
|
access_key: String::new(),
|
||||||
|
secret_key: String::new(),
|
||||||
|
});
|
||||||
|
} else if let Some(ref mut config) = current_config {
|
||||||
|
if let Some(pos) = line.find('=') {
|
||||||
|
let key = line[..pos].trim().to_string();
|
||||||
|
let value = line[pos+1..].trim().to_string();
|
||||||
|
match key.as_str() {
|
||||||
|
"access_key_id" => config.access_key = value,
|
||||||
|
"secret_access_key" => config.secret_key = value,
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(config) = current_config {
|
||||||
|
configs.push(config);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(configs)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run_sync(config: &RcloneConfig) -> Result<Child, std::io::Error> {
|
||||||
|
let local_path = Path::new(&config.local_path);
|
||||||
|
if !local_path.exists() {
|
||||||
|
create_dir_all(local_path)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
ProcCommand::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()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_rclone_status(remote_name: &str) -> Result<SyncStatus, String> {
|
||||||
|
let output = ProcCommand::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 parsed: Result<Value, _> = serde_json::from_str(&json);
|
||||||
|
match parsed {
|
||||||
|
Ok(value) => {
|
||||||
|
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.to_string(),
|
||||||
|
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(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
Err(e) => Err(format!("Failed to parse rclone status: {}", e)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -5,7 +5,7 @@
|
||||||
"identifier": "online.generalbots",
|
"identifier": "online.generalbots",
|
||||||
"build": {
|
"build": {
|
||||||
"beforeDevCommand": "npm run dev",
|
"beforeDevCommand": "npm run dev",
|
||||||
"devUrl": "http://localhost:1420",
|
"devUrl": "http://localhost:3001",
|
||||||
"beforeBuildCommand": "npm run build",
|
"beforeBuildCommand": "npm run build",
|
||||||
"frontendDist": "../out"
|
"frontendDist": "../out"
|
||||||
},
|
},
|
||||||
|
|
Loading…
Add table
Reference in a new issue