Cloud folder organization, file uploads, appearance themes, title bar rework, faster autosync

This commit is contained in:
2026-07-20 23:58:53 -04:00
parent 46e2de2af9
commit 62f2f8483f
9 changed files with 808 additions and 97 deletions
+162 -10
View File
@@ -89,7 +89,7 @@ fn update_settings(
workspace_root: Option<String>,
server_url: Option<String>,
autosave_seconds: Option<u32>,
sync_minutes: Option<u32>,
sync_seconds: Option<u32>,
) -> Result<Settings, String> {
let mut settings = load_settings(&app, &store)?;
if let Some(root) = workspace_root {
@@ -104,8 +104,8 @@ fn update_settings(
if let Some(seconds) = autosave_seconds {
settings.autosave_seconds = seconds;
}
if let Some(minutes) = sync_minutes {
settings.sync_minutes = minutes;
if let Some(seconds) = sync_seconds {
settings.sync_seconds = seconds;
}
save_settings(&store, &settings)?;
Ok(settings)
@@ -745,6 +745,71 @@ fn cloud_list_folders(
sync::list_folders(&server_url, &token)
}
#[tauri::command]
fn cloud_create_folder(
app: AppHandle,
store: State<'_, Store>,
name: String,
parent_id: Option<String>,
) -> Result<sync::CloudFolder, String> {
let (server_url, token) = cloud_credentials(&app, &store)?;
sync::create_folder(&server_url, &token, &name, parent_id.as_deref())
}
#[tauri::command]
fn cloud_rename_folder(
app: AppHandle,
store: State<'_, Store>,
folder_id: String,
name: String,
) -> Result<sync::CloudFolder, String> {
let (server_url, token) = cloud_credentials(&app, &store)?;
sync::rename_folder(&server_url, &token, &folder_id, &name)
}
#[tauri::command]
fn cloud_move_folder(
app: AppHandle,
store: State<'_, Store>,
folder_id: String,
parent_id: Option<String>,
) -> Result<sync::CloudFolder, String> {
let (server_url, token) = cloud_credentials(&app, &store)?;
sync::move_folder(&server_url, &token, &folder_id, parent_id.as_deref())
}
#[tauri::command]
fn cloud_delete_folder(
app: AppHandle,
store: State<'_, Store>,
folder_id: String,
) -> Result<(), String> {
let (server_url, token) = cloud_credentials(&app, &store)?;
sync::delete_folder(&server_url, &token, &folder_id)
}
#[tauri::command]
fn cloud_move_project(
app: AppHandle,
store: State<'_, Store>,
cloud_project_id: String,
folder_id: Option<String>,
) -> Result<ProjectSummary, String> {
let (server_url, token) = cloud_credentials(&app, &store)?;
sync::move_cloud_project(&server_url, &token, &cloud_project_id, folder_id.as_deref())
}
#[tauri::command]
fn cloud_move_document(
app: AppHandle,
store: State<'_, Store>,
document_id: String,
folder_id: Option<String>,
) -> Result<sync::CloudDocument, String> {
let (server_url, token) = cloud_credentials(&app, &store)?;
sync::move_document(&server_url, &token, &document_id, folder_id.as_deref())
}
#[tauri::command]
fn cloud_list_documents(
app: AppHandle,
@@ -793,6 +858,73 @@ fn cloud_delete_file(app: AppHandle, store: State<'_, Store>, file_id: String) -
sync::delete_account_file(&server_url, &token, &file_id)
}
fn guess_mime_type(name: &str) -> &'static str {
let extension = std::path::Path::new(name)
.extension()
.and_then(|value| value.to_str())
.unwrap_or("")
.to_lowercase();
match extension.as_str() {
"png" => "image/png",
"jpg" | "jpeg" => "image/jpeg",
"gif" => "image/gif",
"svg" => "image/svg+xml",
"webp" => "image/webp",
"ttf" => "font/ttf",
"otf" => "font/otf",
"ttc" | "otc" => "font/collection",
"pdf" => "application/pdf",
_ => "application/octet-stream",
}
}
#[tauri::command]
fn cloud_upload_file(
app: AppHandle,
store: State<'_, Store>,
path: String,
folder_id: Option<String>,
) -> Result<sync::CloudFile, String> {
let (server_url, token) = cloud_credentials(&app, &store)?;
let name = std::path::Path::new(&path)
.file_name()
.map(|value| value.to_string_lossy().to_string())
.ok_or_else(|| format!("'{}' has no file name", path))?;
let data = std::fs::read(&path).map_err(|e| e.to_string())?;
let mime_type = guess_mime_type(&name);
sync::upload_account_file(
&server_url,
&token,
&name,
mime_type,
&data,
folder_id.as_deref(),
)
}
#[tauri::command]
fn cloud_rename_file(
app: AppHandle,
store: State<'_, Store>,
file_id: String,
name: String,
) -> Result<sync::CloudFile, String> {
let (server_url, token) = cloud_credentials(&app, &store)?;
sync::rename_account_file(&server_url, &token, &file_id, name.trim())
}
#[tauri::command]
fn cloud_move_file(
app: AppHandle,
store: State<'_, Store>,
file_id: String,
folder_id: Option<String>,
) -> Result<sync::CloudFile, String> {
let (server_url, token) = cloud_credentials(&app, &store)?;
sync::move_account_file(&server_url, &token, &file_id, folder_id.as_deref())
}
#[tauri::command]
fn cloud_delete_document(
app: AppHandle,
@@ -905,7 +1037,7 @@ fn cloud_create_document(
let full = workspace_path(&app, &store, &path)?;
let content = std::fs::read_to_string(&full).map_err(|e| e.to_string())?;
let document = sync::create_document(&server_url, &token, title.trim(), &content)?;
let document = sync::create_document(&server_url, &token, title.trim(), &content, None)?;
store.save_document_link(
&path,
@@ -995,13 +1127,23 @@ fn cloud_document_link(
}
#[tauri::command]
fn cloud_create_project(app: AppHandle, store: State<'_, Store>, name: String) -> Result<ProjectSummary, String> {
fn cloud_create_project(
app: AppHandle,
store: State<'_, Store>,
name: String,
folder_id: Option<String>,
) -> Result<ProjectSummary, String> {
let (server_url, token) = cloud_credentials(&app, &store)?;
sync::create_cloud_project(&server_url, &token, name.trim())
sync::create_cloud_project(&server_url, &token, name.trim(), folder_id.as_deref())
}
#[tauri::command]
fn cloud_new_document(app: AppHandle, store: State<'_, Store>, title: String) -> Result<sync::DocumentContent, String> {
fn cloud_new_document(
app: AppHandle,
store: State<'_, Store>,
title: String,
folder_id: Option<String>,
) -> Result<sync::DocumentContent, String> {
let title = title.trim();
if title.is_empty() {
return Err("Document name cannot be empty".to_string());
@@ -1009,7 +1151,7 @@ fn cloud_new_document(app: AppHandle, store: State<'_, Store>, title: String) ->
let (server_url, token) = cloud_credentials(&app, &store)?;
let content = format!("= {}\n\nStart writing here.\n", title);
sync::create_document(&server_url, &token, title, &content)
sync::create_document(&server_url, &token, title, &content, folder_id.as_deref())
}
#[tauri::command]
@@ -1024,9 +1166,10 @@ fn cloud_clone_project(
store: State<'_, Store>,
cloud_project_id: String,
project_name: String,
parent: String,
) -> Result<SyncReport, String> {
let (server_url, token) = cloud_credentials(&app, &store)?;
let project = project_name.trim().to_string();
let project = join_path(&parent, project_name.trim());
let dir = workspace_path(&app, &store, &project)?;
if dir.exists() {
return Err(format!("A project named '{}' already exists", project_name));
@@ -1054,7 +1197,7 @@ fn cloud_link_project(
let cloud_project_id = match cloud_project_id {
Some(id) if !id.trim().is_empty() => id,
_ => sync::create_cloud_project(&server_url, &token, &project)?.id,
_ => sync::create_cloud_project(&server_url, &token, &project, None)?.id,
};
meta.cloud_project_id = Some(cloud_project_id);
@@ -1196,11 +1339,20 @@ pub fn run() {
cloud_account,
cloud_list_projects,
cloud_list_folders,
cloud_create_folder,
cloud_rename_folder,
cloud_move_folder,
cloud_delete_folder,
cloud_move_project,
cloud_move_document,
cloud_list_documents,
cloud_list_shared,
cloud_list_files,
cloud_download_file,
cloud_delete_file,
cloud_upload_file,
cloud_rename_file,
cloud_move_file,
cloud_download_document,
cloud_delete_document,
cloud_sync_document,
+148 -2
View File
@@ -181,6 +181,7 @@ pub struct ProjectSummary {
pub id: String,
pub name: String,
pub entrypoint: String,
pub folder_id: Option<String>,
pub role: String,
pub updated_at: String,
}
@@ -199,11 +200,12 @@ pub fn create_cloud_project(
server_url: &str,
token: &str,
name: &str,
folder_id: Option<&str>,
) -> Result<ProjectSummary, String> {
agent()
.post(&endpoint(server_url, "/projects"))
.set("Authorization", &format!("Bearer {}", token))
.send_json(ureq::json!({ "name": name }))
.send_json(ureq::json!({ "name": name, "folder_id": folder_id }))
.map_err(describe)?
.into_json::<ProjectSummary>()
.map_err(|e| e.to_string())
@@ -222,6 +224,21 @@ pub fn delete_cloud_project(
Ok(())
}
pub fn move_cloud_project(
server_url: &str,
token: &str,
cloud_project_id: &str,
folder_id: Option<&str>,
) -> Result<ProjectSummary, String> {
agent()
.patch(&endpoint(server_url, &format!("/projects/{}", cloud_project_id)))
.set("Authorization", &format!("Bearer {}", token))
.send_json(ureq::json!({ "folder_id": folder_id }))
.map_err(describe)?
.into_json::<ProjectSummary>()
.map_err(|e| e.to_string())
}
#[derive(Deserialize)]
pub struct ManifestEntry {
pub path: String,
@@ -750,6 +767,66 @@ pub fn list_folders(server_url: &str, token: &str) -> Result<Vec<CloudFolder>, S
.map_err(|e| e.to_string())
}
pub fn create_folder(
server_url: &str,
token: &str,
name: &str,
parent_id: Option<&str>,
) -> Result<CloudFolder, String> {
agent()
.post(&endpoint(server_url, "/folders"))
.set("Authorization", &format!("Bearer {}", token))
.send_json(ureq::json!({
"name": name,
"parent_id": parent_id,
}))
.map_err(describe)?
.into_json::<CloudFolder>()
.map_err(|e| e.to_string())
}
pub fn rename_folder(
server_url: &str,
token: &str,
folder_id: &str,
name: &str,
) -> Result<CloudFolder, String> {
agent()
.patch(&endpoint(server_url, &format!("/folders/{}", folder_id)))
.set("Authorization", &format!("Bearer {}", token))
.send_json(ureq::json!({ "name": name }))
.map_err(describe)?
.into_json::<CloudFolder>()
.map_err(|e| e.to_string())
}
pub fn move_folder(
server_url: &str,
token: &str,
folder_id: &str,
parent_id: Option<&str>,
) -> Result<CloudFolder, String> {
agent()
.patch(&endpoint(
server_url,
&format!("/folders/{}/move", folder_id),
))
.set("Authorization", &format!("Bearer {}", token))
.send_json(ureq::json!({ "parent_id": parent_id }))
.map_err(describe)?
.into_json::<CloudFolder>()
.map_err(|e| e.to_string())
}
pub fn delete_folder(server_url: &str, token: &str, folder_id: &str) -> Result<(), String> {
agent()
.delete(&endpoint(server_url, &format!("/folders/{}", folder_id)))
.set("Authorization", &format!("Bearer {}", token))
.call()
.map_err(describe)?;
Ok(())
}
pub fn list_documents(
server_url: &str,
token: &str,
@@ -808,6 +885,7 @@ pub fn create_document(
token: &str,
title: &str,
content: &str,
folder_id: Option<&str>,
) -> Result<DocumentContent, String> {
agent()
.post(&endpoint(server_url, "/documents"))
@@ -815,13 +893,28 @@ pub fn create_document(
.send_json(ureq::json!({
"title": title,
"content": content,
"folder_id": null,
"folder_id": folder_id,
}))
.map_err(describe)?
.into_json::<DocumentContent>()
.map_err(|e| e.to_string())
}
pub fn move_document(
server_url: &str,
token: &str,
document_id: &str,
folder_id: Option<&str>,
) -> Result<CloudDocument, String> {
agent()
.patch(&endpoint(server_url, &format!("/documents/{}", document_id)))
.set("Authorization", &format!("Bearer {}", token))
.send_json(ureq::json!({ "folder_id": folder_id }))
.map_err(describe)?
.into_json::<CloudDocument>()
.map_err(|e| e.to_string())
}
pub fn sync_document(
server_url: &str,
token: &str,
@@ -1060,6 +1153,29 @@ pub fn list_account_files(
.map_err(|e| e.to_string())
}
pub fn upload_account_file(
server_url: &str,
token: &str,
name: &str,
mime_type: &str,
data: &[u8],
folder_id: Option<&str>,
) -> Result<CloudFile, String> {
agent()
.post(&endpoint(server_url, "/files"))
.set("Authorization", &format!("Bearer {}", token))
.send_json(ureq::json!({
"name": name,
"mime_type": mime_type,
"encoding": "base64",
"content": BASE64.encode(data),
"folder_id": folder_id,
}))
.map_err(describe)?
.into_json::<CloudFile>()
.map_err(|e| e.to_string())
}
pub fn pull_account_file(
server_url: &str,
token: &str,
@@ -1082,3 +1198,33 @@ pub fn delete_account_file(server_url: &str, token: &str, file_id: &str) -> Resu
.map_err(describe)?;
Ok(())
}
pub fn rename_account_file(
server_url: &str,
token: &str,
file_id: &str,
name: &str,
) -> Result<CloudFile, String> {
agent()
.patch(&endpoint(server_url, &format!("/files/{}", file_id)))
.set("Authorization", &format!("Bearer {}", token))
.send_json(ureq::json!({ "name": name }))
.map_err(describe)?
.into_json::<CloudFile>()
.map_err(|e| e.to_string())
}
pub fn move_account_file(
server_url: &str,
token: &str,
file_id: &str,
folder_id: Option<&str>,
) -> Result<CloudFile, String> {
agent()
.patch(&endpoint(server_url, &format!("/files/{}/move", file_id)))
.set("Authorization", &format!("Bearer {}", token))
.send_json(ureq::json!({ "folder_id": folder_id }))
.map_err(describe)?
.into_json::<CloudFile>()
.map_err(|e| e.to_string())
}
+2 -2
View File
@@ -68,7 +68,7 @@ pub struct Settings {
#[serde(default)]
pub autosave_seconds: u32,
#[serde(default)]
pub sync_minutes: u32,
pub sync_seconds: u32,
}
impl Settings {
@@ -84,7 +84,7 @@ impl Settings {
account_email: None,
account_username: None,
autosave_seconds: 5,
sync_minutes: 0,
sync_seconds: 0,
}
}
}