mirror of
https://github.com/narrrl/proton-drive-linux.git
synced 2026-11-04 16:56:59 +01:00
feat(core,fuse,gui): rebuild Trash and shared pages on one file list
The list and grid view from My files moves into widgets/file_list.rs as FileList, so Trash, Shared with me and Shared by me share its columns, selection, context menus, keyboard handling and list/grid switch instead of each drawing its own rows. Trash shows where each item came from and when it was deleted. Proton does not expose the trash time through the SDK yet, so the daemon records when it first sees a node in the trash (migration V35, trash_seen) and reports that; the date becomes exact once proton-sdk-rs exposes Node::trash_time. Restoring offers a Show action, and opening a trashed item explains that it needs to be restored first. Shared with me splits files and saved links into two views. Files show who shared them, with a warning pill for unverified senders, the access they give and when they were shared. Pending invitations sit in a banner that opens a dialog to accept or decline each one. Several items can be left at once. Saved links get their own list with open, copy and remove actions. Shared by me gains a filter (all, links, people, expired links) and columns for location, people and link state, with a copy button for live links. Several items can stop being shared at once, the page refreshes after the share dialog closes, and a reload rebinds only the rows that changed so the selection survives. All catalogs are updated and complete.
This commit is contained in:
1 parent
2339b17d97
commit
127b71efd0
28 files changed
+9501
-6673
No files matched your search
@@ -905,7 +905,7 @@ pub struct ShareEntry {
|
||||
}
|
||||
|
||||
/// A node's public link (in [`Response::Share`] / [`Response::PublicLink`]).
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
|
||||
pub struct PublicLinkInfo {
|
||||
/// Public-link id — the handle for [`Request::RemovePublicLink`].
|
||||
pub id: String,
|
||||
@@ -918,6 +918,9 @@ pub struct PublicLinkInfo {
|
||||
pub expires: Option<i64>,
|
||||
/// Whether a custom password additionally protects the link.
|
||||
pub has_password: bool,
|
||||
/// When the link was made, epoch seconds; 0 when unknown.
|
||||
#[serde(default)]
|
||||
pub created: i64,
|
||||
}
|
||||
|
||||
/// One invitation addressed to me (in [`Response::Invitations`]).
|
||||
@@ -950,7 +953,7 @@ pub struct BookmarkInfo {
|
||||
/// the share's state so the "Shared" view renders in one pass: how many people
|
||||
/// have access, how many invitations are still pending, and the public link if
|
||||
/// the node has one.
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
|
||||
pub struct SharedItem {
|
||||
/// Node uid in `volume~link` form — the handle for opening the node's share
|
||||
/// (via its mount path) or its details.
|
||||
@@ -1138,6 +1141,16 @@ pub struct DirEntry {
|
||||
/// warning worth showing.
|
||||
#[serde(default)]
|
||||
pub shared_by_unverified: bool,
|
||||
/// When a [`Request::ListTrash`] entry went to the trash, epoch seconds; 0
|
||||
/// when unknown, and for anything not in the trash. The API does not report
|
||||
/// it to this client, so the daemon notes when it first saw the node there.
|
||||
#[serde(default)]
|
||||
pub trashed_at: i64,
|
||||
/// The folder a [`Request::ListTrash`] entry goes back to on restore, as a
|
||||
/// mountpoint-relative path (`""` for the root). `None` when that folder is
|
||||
/// not known, and for anything not in the trash.
|
||||
#[serde(default)]
|
||||
pub trashed_from: Option<String>,
|
||||
}
|
||||
|
||||
/// One hit in a [`Request::Search`] result. Like [`DirEntry`] but carries the
|
||||
|
||||
@@ -9,7 +9,7 @@ use super::Db;
|
||||
use crate::Result;
|
||||
|
||||
/// Current schema version. Bump on every forward migration added below.
|
||||
pub(super) const SCHEMA_VERSION: i64 = 34;
|
||||
pub(super) const SCHEMA_VERSION: i64 = 35;
|
||||
|
||||
impl Db {
|
||||
pub(super) fn migrate(&self) -> Result<()> {
|
||||
@@ -316,6 +316,22 @@ impl Db {
|
||||
tx.execute_batch(MIGRATION_V34)?;
|
||||
}
|
||||
}
|
||||
if current < 35 {
|
||||
// Same guards as V26-V34.
|
||||
let has_column: bool = tx.query_row(
|
||||
"SELECT COUNT(*) FROM pragma_table_info('trash') WHERE name = 'trashed_at'",
|
||||
[],
|
||||
|row| row.get::<_, i64>(0),
|
||||
)? > 0;
|
||||
let has_trash: bool = tx.query_row(
|
||||
"SELECT COUNT(*) FROM sqlite_master WHERE type = 'table' AND name = 'trash'",
|
||||
[],
|
||||
|row| row.get::<_, i64>(0),
|
||||
)? > 0;
|
||||
if has_trash && !has_column {
|
||||
tx.execute_batch(MIGRATION_V35)?;
|
||||
}
|
||||
}
|
||||
tx.execute(
|
||||
"INSERT INTO sync_state (key, value) VALUES ('schema_version', ?1)
|
||||
ON CONFLICT(key) DO UPDATE SET value = excluded.value",
|
||||
@@ -1033,3 +1049,13 @@ UPDATE photos SET resolved_at = NULL;
|
||||
const MIGRATION_V34: &str = "
|
||||
ALTER TABLE photos ADD COLUMN similar_hash INTEGER;
|
||||
";
|
||||
|
||||
/// Schema v35: when each trashed node went to the trash.
|
||||
///
|
||||
/// The API does not tell this client, so `trashed_at` is when the daemon first
|
||||
/// saw the node in the trash, in epoch seconds. `NULL` when it cannot say: rows
|
||||
/// from before this version, and nodes found by a refresh that did not follow a
|
||||
/// complete one, which may have been in the trash for a long time already.
|
||||
const MIGRATION_V35: &str = "
|
||||
ALTER TABLE trash ADD COLUMN trashed_at INTEGER;
|
||||
";
|
||||
@@ -583,6 +583,7 @@ fn trash_replace_lists_folders_first() {
|
||||
size: 10,
|
||||
mtime: 1,
|
||||
parent_uid: Some("t2".into()),
|
||||
trashed_at: Some(100),
|
||||
},
|
||||
StoredTrash {
|
||||
uid: "t2".into(),
|
||||
@@ -591,6 +592,7 @@ fn trash_replace_lists_folders_first() {
|
||||
size: 0,
|
||||
mtime: 2,
|
||||
parent_uid: None,
|
||||
trashed_at: None,
|
||||
},
|
||||
])
|
||||
.unwrap();
|
||||
@@ -613,6 +615,15 @@ fn trash_replace_lists_folders_first() {
|
||||
]
|
||||
);
|
||||
|
||||
// The first-seen times round-trip, unknown ones included.
|
||||
let mut seen: Vec<_> = db.trash_seen().unwrap().into_iter().collect();
|
||||
seen.sort();
|
||||
assert_eq!(
|
||||
seen,
|
||||
[("t1".to_string(), Some(100)), ("t2".to_string(), None)]
|
||||
);
|
||||
assert_eq!(items[1].trashed_at, Some(100));
|
||||
|
||||
// A replace is a replace: emptying the trash on the server empties it here.
|
||||
db.trash_replace(&[]).unwrap();
|
||||
assert!(db.trash_list().unwrap().is_empty());
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
//! Trashed nodes, persisted so the trash view opens without a round-trip.
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
use rusqlite::params;
|
||||
|
||||
use super::Db;
|
||||
@@ -15,6 +17,9 @@ pub struct StoredTrash {
|
||||
/// row written before schema v29, and for a node whose parent the server
|
||||
/// did not report.
|
||||
pub parent_uid: Option<String>,
|
||||
/// When the daemon first saw the node in the trash, epoch seconds. `None`
|
||||
/// when it cannot say (see schema v35).
|
||||
pub trashed_at: Option<i64>,
|
||||
}
|
||||
|
||||
impl Db {
|
||||
@@ -24,8 +29,8 @@ impl Db {
|
||||
tx.execute("DELETE FROM trash", [])?;
|
||||
{
|
||||
let mut stmt = tx.prepare(
|
||||
"INSERT INTO trash (uid, name, is_dir, size, mtime, parent_uid) \
|
||||
VALUES (?1, ?2, ?3, ?4, ?5, ?6)",
|
||||
"INSERT INTO trash (uid, name, is_dir, size, mtime, parent_uid, trashed_at) \
|
||||
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)",
|
||||
)?;
|
||||
for item in items {
|
||||
stmt.execute(params![
|
||||
@@ -34,7 +39,8 @@ impl Db {
|
||||
item.is_dir as i64,
|
||||
item.size,
|
||||
item.mtime,
|
||||
item.parent_uid
|
||||
item.parent_uid,
|
||||
item.trashed_at
|
||||
])?;
|
||||
}
|
||||
}
|
||||
@@ -47,7 +53,7 @@ impl Db {
|
||||
pub fn trash_list(&self) -> Result<Vec<StoredTrash>> {
|
||||
let conn = self.read();
|
||||
let mut stmt = conn.prepare(
|
||||
"SELECT uid, name, is_dir, size, mtime, parent_uid FROM trash
|
||||
"SELECT uid, name, is_dir, size, mtime, parent_uid, trashed_at FROM trash
|
||||
ORDER BY is_dir DESC, name COLLATE NOCASE",
|
||||
)?;
|
||||
let rows = stmt.query_map([], |r| {
|
||||
@@ -58,6 +64,7 @@ impl Db {
|
||||
size: r.get(3)?,
|
||||
mtime: r.get(4)?,
|
||||
parent_uid: r.get(5)?,
|
||||
trashed_at: r.get(6)?,
|
||||
})
|
||||
})?;
|
||||
let mut items = Vec::new();
|
||||
@@ -81,5 +88,19 @@ impl Db {
|
||||
Ok(pairs)
|
||||
}
|
||||
|
||||
/// When each node now in the trash was first seen there, keyed by uid, so
|
||||
/// a refresh can carry the times over into the listing it writes.
|
||||
pub fn trash_seen(&self) -> Result<HashMap<String, Option<i64>>> {
|
||||
let conn = self.read();
|
||||
let mut stmt = conn.prepare("SELECT uid, trashed_at FROM trash")?;
|
||||
let rows = stmt.query_map([], |r| Ok((r.get(0)?, r.get(1)?)))?;
|
||||
let mut seen = HashMap::new();
|
||||
for row in rows {
|
||||
let (uid, at) = row?;
|
||||
seen.insert(uid, at);
|
||||
}
|
||||
Ok(seen)
|
||||
}
|
||||
|
||||
// ---- device sync (devices.md) -----------------------------------------
|
||||
}
|
||||
Reference in new issue
Block a user