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";
|
||||
import React, { createContext, useContext, useState, useEffect } from 'react';
|
||||
import { core } from '@tauri-apps/api';
|
||||
import { User, ChatInstance } from '../types';
|
||||
|
||||
interface ChatContextType {
|
||||
line: any;
|
||||
user: User;
|
||||
instance: ChatInstance | null;
|
||||
sendActivity: (activity: any) => void;
|
||||
sendActivity: (activity: any) => Promise<void>;
|
||||
selectedVoice: any;
|
||||
setVoice: (voice: any) => void;
|
||||
}
|
||||
|
||||
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 }) {
|
||||
const [line, setLine] = useState<any>(null);
|
||||
const [instance, setInstance] = useState<ChatInstance | null>(null);
|
||||
|
@ -26,16 +39,25 @@ export function ChatProvider({ children }: { children: React.ReactNode }) {
|
|||
useEffect(() => {
|
||||
const initializeChat = async () => {
|
||||
try {
|
||||
const botId = window.location.pathname.split('/')[1] || 'default';
|
||||
const instanceData = await core.invoke('get_chat_instance', { botId });
|
||||
setInstance(instanceData as ChatInstance);
|
||||
const botId = 'doula'; // window.location.pathname.split('/')[1] || 'default';
|
||||
|
||||
// Initialize DirectLine or other chat service
|
||||
const directLine = {
|
||||
// Get instance from REST API
|
||||
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: () => {} },
|
||||
postActivity: () => ({ subscribe: () => {} })
|
||||
postActivity: (activity: any) => ({
|
||||
subscribe: (observer: any) => {
|
||||
// Handle real-time updates if needed
|
||||
return { unsubscribe: () => {} };
|
||||
}
|
||||
})
|
||||
};
|
||||
setLine(directLine);
|
||||
setLine(chatService);
|
||||
} catch (error) {
|
||||
console.error('Failed to initialize chat:', error);
|
||||
}
|
||||
|
@ -46,14 +68,25 @@ export function ChatProvider({ children }: { children: React.ReactNode }) {
|
|||
|
||||
const sendActivity = async (activity: any) => {
|
||||
try {
|
||||
await core.invoke('send_chat_activity', {
|
||||
activity: {
|
||||
...activity,
|
||||
from: user,
|
||||
timestamp: new Date().toISOString()
|
||||
}
|
||||
const fullActivity = {
|
||||
...activity,
|
||||
from: user,
|
||||
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) {
|
||||
console.error('Failed to send activity:', error);
|
||||
}
|
||||
|
|
|
@ -4,12 +4,15 @@ import { usePathname, useRouter } from 'next/navigation';
|
|||
import { Button } from '../src/components/ui/button';
|
||||
|
||||
const examples = [
|
||||
{ name: "Home", href: "/authentication" },
|
||||
{ name: "Home", href: "/auth" },
|
||||
{ name: "Dashboard", href: "/dashboard" },
|
||||
{ name: "Chat", href: "/chat" },
|
||||
{ name: "Mail", href: "/mail" },
|
||||
{ name: "Drive", href: "/drive" },
|
||||
{ name: "Tasks", href: "/tasks" },
|
||||
{ name: "Tree", href: "/tree" },
|
||||
{ name: "Editor", href: "/editor" },
|
||||
{ name: "Tables", href: "/table" },
|
||||
{ name: "Video", href: "/video" },
|
||||
{ name: "Music", href: "/music" },
|
||||
{ name: "Templates", href: "/templates" },
|
||||
{ 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 { FileOperations } from './components/FileOperations';
|
||||
|
||||
// TODO: XTREE like keyword.
|
||||
|
||||
export default function DriveScreen() {
|
||||
const [currentPath, setCurrentPath] = useState('');
|
||||
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",
|
||||
"build": {
|
||||
"beforeDevCommand": "npm run dev",
|
||||
"devUrl": "http://localhost:1420",
|
||||
"devUrl": "http://localhost:3001",
|
||||
"beforeBuildCommand": "npm run build",
|
||||
"frontendDist": "../out"
|
||||
},
|
||||
|
|
Loading…
Add table
Reference in a new issue