feat(locallibrary): Settings > Local Library — folder management (Slice A)
Port the Tauri folder-management surface to a new Settings > Local Library sub-section (index 4); the browse-view gear routes here and pre-selects it. UI: - LocalLibrarySettings.slint: folder list (icon, display name, disabled/ unavailable badges, multi-select checkboxes) + toolbar (scan-all/add/edit/ remove icon buttons) + filter + Maintenance (cleanup missing) + two-step Danger Zone clear. ADR-008: plain bordered icon buttons + square badges. - LibFolderEditModal.slint: alias, enabled, network override + fs-type, change-path, accessibility status, last-scanned, scan-this-folder. Mounted at the AppShell root (overlays the window, ADR-009 by mount order). - SettingsView gains the section-4 sub-nav + panel; app.slint re-exports the new globals so the Rust layer can populate them. Controller (local_library_settings.rs): load/add/remove/edit/save/change-path, cleanup-missing (stats outside the DB lock), two-step clear, filter, select. All DB access via qbz-library through library_db::with_db on spawn_blocking; native dir picker + confirms via rfd. Selection preserved across reloads; network folders get an async 6s-timeout accessibility check (exists() fallback on timeout, mirroring Tauri). Core (qbz-library): network_fs_label() for fs-type parity; separator-safe delete_tracks_in_folder_prefixed() + remove_folder_with_tracks() (fixes the prefix-collision bug without changing the legacy fn). Scan engine lands in Slice B. cc
This commit is contained in:
11 files changed
+1527
-6
No files matched your search
@@ -1168,6 +1168,31 @@ impl LibraryDatabase {
|
||||
Ok(count)
|
||||
}
|
||||
|
||||
/// Delete all tracks under a folder, matching a path prefix terminated by
|
||||
/// the separator so a sibling like `/music/jazz2` is NOT removed when
|
||||
/// deleting `/music/jazz`. Use this for folder removal — the older
|
||||
/// `delete_tracks_in_folder` (kept for backward behavior compatibility with
|
||||
/// the Tauri command) has a prefix-collision bug (`{}%`, no separator).
|
||||
pub fn delete_tracks_in_folder_prefixed(&self, folder: &str) -> Result<usize, LibraryError> {
|
||||
let pattern = format!("{}/%", folder.trim_end_matches('/'));
|
||||
let count = self
|
||||
.conn
|
||||
.execute(
|
||||
"DELETE FROM local_tracks WHERE file_path LIKE ?",
|
||||
params![pattern],
|
||||
)
|
||||
.map_err(|e| LibraryError::Database(e.to_string()))?;
|
||||
Ok(count)
|
||||
}
|
||||
|
||||
/// Remove a folder and its indexed tracks (separator-safe cascade). Mirrors
|
||||
/// the Tauri remove-folder command order: drop the folder row, then the
|
||||
/// tracks under it. Returns the number of tracks removed.
|
||||
pub fn remove_folder_with_tracks(&self, path: &str) -> Result<usize, LibraryError> {
|
||||
self.remove_folder(path)?;
|
||||
self.delete_tracks_in_folder_prefixed(path)
|
||||
}
|
||||
|
||||
/// Clear all LOCAL library tracks (preserves Qobuz downloads)
|
||||
pub fn clear_all_tracks(&self) -> Result<(), LibraryError> {
|
||||
self.conn
|
||||
|
||||
@@ -48,7 +48,7 @@ pub use database::{
|
||||
pub use errors::LibraryError;
|
||||
pub use metadata::MetadataExtractor;
|
||||
pub use models::*;
|
||||
pub use mount_info::is_network_path;
|
||||
pub use mount_info::{is_network_path, network_fs_label};
|
||||
pub use scanner::{LibraryScanner, ScanResult};
|
||||
pub use thumbnails::{
|
||||
clear_thumbnails, generate_thumbnail, generate_thumbnail_from_bytes, get_cache_size,
|
||||
|
||||
@@ -81,6 +81,63 @@ pub fn is_network_path(_path: &Path) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
/// Return the normalized network-filesystem label (`cifs` / `nfs` / `sshfs` /
|
||||
/// `rclone` / `webdav` / `glusterfs` / `ceph` / `other`) for `path` when it
|
||||
/// lives on a network-backed filesystem, else `None`. Mirrors the fs-type
|
||||
/// classification the Tauri side persisted via `crate::network::is_network_path`,
|
||||
/// so the Slint folder-settings modal can show + store the same auto-detected
|
||||
/// type. (`is_network_path` returns only the bool; this adds the label.)
|
||||
#[cfg(target_os = "linux")]
|
||||
pub fn network_fs_label(path: &Path) -> Option<String> {
|
||||
let mounts = read_mounts();
|
||||
if mounts.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let target = path
|
||||
.canonicalize()
|
||||
.unwrap_or_else(|_| path.to_path_buf());
|
||||
let target_str = target.to_string_lossy();
|
||||
|
||||
let mut best: Option<(&str, usize)> = None;
|
||||
for (mount_point, fs_type) in &mounts {
|
||||
if target_str.starts_with(mount_point) {
|
||||
let len = mount_point.len();
|
||||
if best.map(|(_, l)| l < len).unwrap_or(true) {
|
||||
best = Some((fs_type.as_str(), len));
|
||||
}
|
||||
}
|
||||
}
|
||||
let fs_type = best.map(|(t, _)| t)?;
|
||||
if !is_network_fs(fs_type) {
|
||||
return None;
|
||||
}
|
||||
Some(normalize_network_label(fs_type))
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
pub fn network_fs_label(_path: &Path) -> Option<String> {
|
||||
None
|
||||
}
|
||||
|
||||
/// Collapse the raw /proc/mounts fs type to the label set the folder-settings
|
||||
/// modal exposes. Unknown network types fall through to `other`.
|
||||
#[cfg(target_os = "linux")]
|
||||
fn normalize_network_label(fs_type: &str) -> String {
|
||||
let lower = fs_type.to_lowercase();
|
||||
let base = lower.strip_prefix("fuse.").unwrap_or(&lower);
|
||||
match base {
|
||||
"nfs" | "nfs4" => "nfs",
|
||||
"cifs" | "smb" | "smbfs" | "smb3" => "cifs",
|
||||
"sshfs" => "sshfs",
|
||||
"rclone" | "rclonefs" => "rclone",
|
||||
"davfs" | "webdav" => "webdav",
|
||||
"glusterfs" => "glusterfs",
|
||||
"ceph" => "ceph",
|
||||
_ => "other",
|
||||
}
|
||||
.to_string()
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
fn read_mounts() -> Vec<(String, String)> {
|
||||
// Inside Flatpak the sandbox's own /proc/mounts reflects the
|
||||
|
||||
Reference in new issue
Block a user