Show cloud files and sync state in the cloud workspace

This commit is contained in:
2026-07-18 18:04:30 -04:00
parent 2383091937
commit 470d8c3d25
8 changed files with 633 additions and 149 deletions
+1
View File
@@ -14,6 +14,7 @@
"core:window:allow-start-dragging",
"core:window:allow-close",
"opener:default",
"opener:allow-reveal-item-in-dir",
"dialog:default",
"dialog:allow-open",
"dialog:allow-save"
+57 -6
View File
@@ -16,6 +16,7 @@ pub struct DocumentLink {
pub base_hash: String,
pub role: String,
pub base_content: String,
pub synced_at: Option<String>,
}
const SCHEMA: [&str; 5] = [
@@ -41,7 +42,8 @@ const SCHEMA: [&str; 5] = [
document_id TEXT NOT NULL,
base_hash TEXT NOT NULL,
role TEXT NOT NULL,
base_content TEXT
base_content TEXT,
synced_at TEXT
)",
"CREATE TABLE IF NOT EXISTS thumbnails (
path TEXT PRIMARY KEY,
@@ -51,6 +53,9 @@ const SCHEMA: [&str; 5] = [
)",
];
const MIGRATIONS: [&str; 1] =
["ALTER TABLE document_links ADD COLUMN synced_at TEXT"];
impl Store {
pub fn open(app: &AppHandle) -> Result<Self, String> {
let dir = app
@@ -73,6 +78,10 @@ impl Store {
connection.execute(statement, []).map_err(|e| e.to_string())?;
}
for statement in MIGRATIONS {
let _ = connection.execute(statement, []);
}
Ok(Store {
connection: Mutex::new(connection),
})
@@ -283,14 +292,23 @@ impl Store {
) -> Result<(), String> {
self.with(|connection| {
connection.execute(
"INSERT INTO document_links (path, document_id, base_hash, role, base_content)
VALUES (?1, ?2, ?3, ?4, ?5)
"INSERT INTO document_links
(path, document_id, base_hash, role, base_content, synced_at)
VALUES (?1, ?2, ?3, ?4, ?5, ?6)
ON CONFLICT(path) DO UPDATE SET
document_id = excluded.document_id,
base_hash = excluded.base_hash,
role = excluded.role,
base_content = excluded.base_content",
params![path, document_id, base_hash, role, base_content],
base_content = excluded.base_content,
synced_at = excluded.synced_at",
params![
path,
document_id,
base_hash,
role,
base_content,
chrono::Utc::now().to_rfc3339()
],
)?;
Ok(())
})
@@ -300,7 +318,7 @@ impl Store {
self.with(|connection| {
connection
.query_row(
"SELECT document_id, base_hash, role, base_content
"SELECT document_id, base_hash, role, base_content, synced_at
FROM document_links WHERE path = ?1",
params![path],
|row| {
@@ -309,6 +327,7 @@ impl Store {
base_hash: row.get(1)?,
role: row.get(2)?,
base_content: row.get::<_, Option<String>>(3)?.unwrap_or_default(),
synced_at: row.get(4)?,
})
},
)
@@ -316,6 +335,38 @@ impl Store {
})
}
pub fn all_space_links(&self) -> Result<Vec<(String, String, Option<String>)>, String> {
self.with(|connection| {
let mut statement = connection.prepare(
"SELECT path, space_id, last_synced_at FROM projects
WHERE space_id IS NOT NULL",
)?;
let rows = statement.query_map([], |row| {
Ok((
row.get::<_, String>(0)?,
row.get::<_, String>(1)?,
row.get::<_, Option<String>>(2)?,
))
})?;
rows.collect::<rusqlite::Result<Vec<_>>>()
})
}
pub fn all_document_links(&self) -> Result<Vec<(String, String, Option<String>)>, String> {
self.with(|connection| {
let mut statement = connection
.prepare("SELECT path, document_id, synced_at FROM document_links")?;
let rows = statement.query_map([], |row| {
Ok((
row.get::<_, String>(0)?,
row.get::<_, String>(1)?,
row.get::<_, Option<String>>(2)?,
))
})?;
rows.collect::<rusqlite::Result<Vec<_>>>()
})
}
pub fn forget_document_link(&self, path: &str) -> Result<(), String> {
self.with(|connection| {
connection.execute("DELETE FROM document_links WHERE path = ?1", params![path])?;
+49
View File
@@ -848,3 +848,52 @@ pub fn push_document(
Err(other) => Err(describe(other)),
}
}
#[derive(Deserialize, Serialize, Clone)]
pub struct CloudFile {
pub id: String,
pub name: String,
pub mime_type: String,
pub folder_id: Option<String>,
pub created_at: String,
}
#[derive(Deserialize)]
pub struct CloudFileContent {
pub name: String,
pub content: String,
}
pub fn list_account_files(
server_url: &str,
token: &str,
folder_id: Option<&str>,
) -> Result<Vec<CloudFile>, String> {
let mut request = agent()
.get(&endpoint(server_url, "/files"))
.set("Authorization", &format!("Bearer {}", token));
if let Some(folder) = folder_id {
request = request.query("folder_id", folder);
}
request
.call()
.map_err(describe)?
.into_json::<Vec<CloudFile>>()
.map_err(|e| e.to_string())
}
pub fn pull_account_file(
server_url: &str,
token: &str,
file_id: &str,
) -> Result<CloudFileContent, String> {
agent()
.get(&endpoint(server_url, &format!("/files/{}", file_id)))
.set("Authorization", &format!("Bearer {}", token))
.call()
.map_err(describe)?
.into_json::<CloudFileContent>()
.map_err(|e| e.to_string())
}
+27 -2
View File
@@ -27,6 +27,26 @@ fn modified_seconds(path: &Path) -> i64 {
.unwrap_or(0)
}
fn newest_change_seconds(dir: &Path) -> i64 {
let mut newest = 0;
for entry in walkdir::WalkDir::new(dir).into_iter().filter_map(|e| e.ok()) {
if !entry.file_type().is_file() {
continue;
}
let seconds = entry
.metadata()
.ok()
.and_then(|meta| meta.modified().ok())
.and_then(|time| time.duration_since(UNIX_EPOCH).ok())
.map(|elapsed| elapsed.as_secs() as i64)
.unwrap_or(0);
if seconds > newest {
newest = seconds;
}
}
newest
}
fn mime_for(name: &str) -> &'static str {
let lower = name.to_lowercase();
if lower.ends_with(".png") {
@@ -115,12 +135,17 @@ pub fn thumbnail(app: &AppHandle, store: &Store, path: &str) -> Result<Thumbnail
.map(|n| n.to_string_lossy().to_string())
.unwrap_or_default();
let is_project = full.is_dir();
let image = is_image(&name);
if !image && !name.to_lowercase().ends_with(".typ") {
if !image && !is_project && !name.to_lowercase().ends_with(".typ") {
return Err("No preview available".to_string());
}
let modified = modified_seconds(&full);
let modified = if is_project {
newest_change_seconds(&full)
} else {
modified_seconds(&full)
};
if let Some((kind, data)) = store.thumbnail(path, modified)? {
return Ok(Thumbnail { kind, data });