Show cloud files and sync state in the cloud workspace
This commit is contained in:
@@ -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
@@ -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])?;
|
||||
|
||||
@@ -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,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 });
|
||||
|
||||
Reference in New Issue
Block a user