fix(slint): network-folder visibility keyed on connectivity, playback lock on missing files
Root cause of the empty Albums tab: transposed positional booleans from
the Plex-port rewrite (a45840d1) left exclude_network_folders=true and
include_qobuz_downloads=false unconditionally on every browse query since
2026-06-07. The owner-approved model replaces it: network-folder content
hides ONLY under hard offline (link confirmed down - no link means no LAN
mounts either); a logged-out session or induced offline with the link up
shows everything (the Qobuz verdict says nothing about NFS/SMB). The
show-network-folders toggle is retired from Settings (DB column stays,
Tauri-compatible).
Playback lock instead of hiding: local tracks get an existence check at
the play seam and in the advance walk (FileMissing verdict, 'is the drive
mounted?' toasts, never on the audio thread - D-state caveat documented).
Scan-cleanup guard: tracks under a network folder whose mount is
unreachable are never treated as missing (a refresh pressed before
remounting used to hard-delete the folder's whole index). mount_info: nfs4
and smb3-style version suffixes now classify as network (its red test is
green) and the longest-mount match respects path boundaries.
cc
This commit is contained in:
8 files changed
+317
-130
No files matched your search
@@ -459,6 +459,42 @@ fn plex_cache_db_path() -> Option<std::path::PathBuf> {
|
||||
dirs::data_dir().map(|d| d.join("qbz").join("plex_cache.db"))
|
||||
}
|
||||
|
||||
// NETWORK-FOLDER VISIBILITY (owner verdict 2026-06-10, refined same day):
|
||||
// hiding network-folder content is keyed on RAW CONNECTIVITY, never on the
|
||||
// offline MODE. A logged-out session or induced offline with the link up
|
||||
// says nothing about LAN mounts — content stays visible there (offline mode
|
||||
// exists precisely to use the local library). Only a CONFIRMED-down link
|
||||
// (hard offline: no default route / probes dead) hides network folders, the
|
||||
// one state where LAN mounts are gone too. An unmounted-while-online path is
|
||||
// handled at PLAYBACK time instead (existence guard + friendly toast in
|
||||
// playback.rs), not by hiding library content.
|
||||
//
|
||||
// Known approximation, accepted with the model: an ISP outage with a live
|
||||
// LAN reads as hard offline and hides NAS content; per-mount accessibility
|
||||
// checks in every browse query would be the exact-but-costly alternative.
|
||||
|
||||
/// True only under HARD offline (connectivity confirmed down). See the
|
||||
/// NETWORK-FOLDER VISIBILITY note above.
|
||||
pub(crate) fn exclude_network_folders_now() -> bool {
|
||||
crate::offline_mode::engine().status().connectivity
|
||||
== qbz_app::offline_mode::Connectivity::Down
|
||||
}
|
||||
|
||||
/// Reset the four browse-tab models so each tab re-fetches on its next visit
|
||||
/// (the `ensure_*_loaded` guards key on an empty model). Used after a scan,
|
||||
/// after the danger-zone clear, and on offline-mode flips (where the
|
||||
/// connectivity-keyed network-folder gate may change the browse SET — see
|
||||
/// the NETWORK-FOLDER VISIBILITY note above).
|
||||
pub fn reset_browse_models(window: &AppWindow) {
|
||||
let s = window.global::<LocalLibraryState>();
|
||||
let empty_albums = ModelRc::new(VecModel::from(Vec::<AlbumCardItem>::new()));
|
||||
let empty_tracks = ModelRc::new(VecModel::from(Vec::<TrackItem>::new()));
|
||||
s.set_albums(empty_albums.clone());
|
||||
s.set_folders(empty_albums);
|
||||
s.set_tracks(empty_tracks);
|
||||
s.set_artists(ModelRc::new(VecModel::from(Vec::<LocalArtistItem>::new())));
|
||||
}
|
||||
|
||||
/// Upper bound for the full-load page. The Albums tab loads the entire set in
|
||||
/// one shot (search/sort/filter/group are all derived client-side over the
|
||||
/// cached set in `derive_albums`), so we request a single large page rather
|
||||
@@ -485,6 +521,11 @@ fn spawn_albums_load(
|
||||
handle.spawn(async move {
|
||||
let loaded: Option<(Vec<qbz_library::LocalAlbum>, Vec<crate::album_map::AlbumCard>)> =
|
||||
tokio::task::spawn_blocking(move || {
|
||||
// include_qobuz_downloads: true (offline copies belong in the
|
||||
// grid — the toolbar's "Offline" source filter selects them).
|
||||
// exclude_network_folders: connectivity-keyed — see the
|
||||
// NETWORK-FOLDER VISIBILITY note.
|
||||
let exclude_network = exclude_network_folders_now();
|
||||
crate::library_db::with_db(|db| {
|
||||
let page = db.get_albums_metadata_page(
|
||||
0,
|
||||
@@ -492,8 +533,8 @@ fn spawn_albums_load(
|
||||
None,
|
||||
"artist",
|
||||
"asc",
|
||||
false,
|
||||
true,
|
||||
exclude_network,
|
||||
plex_path.as_deref(),
|
||||
)?;
|
||||
let albums = page.albums;
|
||||
@@ -581,6 +622,10 @@ pub fn seed_counts(weak: slint::Weak<AppWindow>, handle: tokio::runtime::Handle)
|
||||
let plex_enabled = crate::plex_settings::get().enabled;
|
||||
handle.spawn(async move {
|
||||
let counts: Option<(usize, usize, usize, usize)> = tokio::task::spawn_blocking(move || {
|
||||
// Same include-qobuz / network flags as the Albums tab loader, so
|
||||
// the badge always matches the grid (network content always in —
|
||||
// see the NETWORK-FOLDER VISIBILITY note).
|
||||
let exclude_network = exclude_network_folders_now();
|
||||
crate::library_db::with_db(|db| {
|
||||
// Total under the same filter the Albums tab uses, incl. the
|
||||
// Plex union when enabled, so the badge matches the grid.
|
||||
@@ -591,8 +636,8 @@ pub fn seed_counts(weak: slint::Weak<AppWindow>, handle: tokio::runtime::Handle)
|
||||
None,
|
||||
"artist",
|
||||
"asc",
|
||||
false,
|
||||
true,
|
||||
exclude_network,
|
||||
plex_path.as_deref(),
|
||||
)
|
||||
.map(|p| p.total as usize)
|
||||
@@ -759,8 +804,11 @@ fn fetch_tracks_page(
|
||||
offset: u64,
|
||||
plex: bool,
|
||||
) -> Option<(Vec<qbz_library::LocalTrack>, bool)> {
|
||||
// exclude_network_folders: connectivity-keyed — see the NETWORK-FOLDER
|
||||
// VISIBILITY note.
|
||||
let exclude_network = exclude_network_folders_now();
|
||||
let mut rows = crate::library_db::with_db(|db| {
|
||||
db.search_with_filter_page(query.trim(), offset, TRACKS_PAGE, true, false)
|
||||
db.search_with_filter_page(query.trim(), offset, TRACKS_PAGE, true, exclude_network)
|
||||
})?;
|
||||
let has_more = rows.len() as u64 == TRACKS_PAGE;
|
||||
if plex && offset == 0 {
|
||||
@@ -1401,6 +1449,10 @@ fn spawn_plex_quality_hydration(
|
||||
// The album aggregate (MAX over tracks) is a pure runtime query; the
|
||||
// refreshed Plex track quality flows into it automatically.
|
||||
let album = plex_path.as_deref().and_then(|p| {
|
||||
// Same flags as the Albums tab loader so the looked-up
|
||||
// aggregate comes from the identical set (network content
|
||||
// always in — see the NETWORK-FOLDER VISIBILITY note).
|
||||
let exclude_network = exclude_network_folders_now();
|
||||
crate::library_db::with_db(|db| {
|
||||
let page = db.get_albums_metadata_page(
|
||||
0,
|
||||
@@ -1408,8 +1460,8 @@ fn spawn_plex_quality_hydration(
|
||||
None,
|
||||
"artist",
|
||||
"asc",
|
||||
false,
|
||||
true,
|
||||
exclude_network,
|
||||
Some(p),
|
||||
)?;
|
||||
Ok(page.albums.into_iter().find(|a| a.id == gk))
|
||||
@@ -1631,11 +1683,15 @@ pub fn current_album_version_tracks(window: &AppWindow) -> Vec<qbz_library::Loca
|
||||
// the Tracks table). Reuses AlbumCollectionView; covers load per card.
|
||||
|
||||
fn fetch_folder_albums() -> Vec<crate::album_map::AlbumCard> {
|
||||
crate::library_db::with_db(|db| {
|
||||
// exclude_network_folders: connectivity-keyed — an unmounted-but-online
|
||||
// folder stays visible (the index is the source); only hard offline
|
||||
// hides it (see the NETWORK-FOLDER VISIBILITY note).
|
||||
let exclude_network = exclude_network_folders_now();
|
||||
crate::library_db::with_db(move |db| {
|
||||
db.get_albums_with_full_filter(
|
||||
/* include_hidden */ false,
|
||||
/* include_qobuz_downloads */ true,
|
||||
/* exclude_network_folders */ false,
|
||||
exclude_network,
|
||||
)
|
||||
})
|
||||
.unwrap_or_default()
|
||||
@@ -2792,8 +2848,13 @@ pub fn ensure_artists_loaded(
|
||||
let plex_path = plex_cache_db_path();
|
||||
handle.spawn(async move {
|
||||
let items = tokio::task::spawn_blocking(move || {
|
||||
let artists = crate::library_db::with_db(|db| db.get_artists_with_filter(true, false))
|
||||
.unwrap_or_default();
|
||||
// Same network flag as every browse tab: connectivity-keyed
|
||||
// — see the NETWORK-FOLDER VISIBILITY note.
|
||||
let exclude_network = exclude_network_folders_now();
|
||||
let artists = crate::library_db::with_db(|db| {
|
||||
db.get_artists_with_filter(true, exclude_network)
|
||||
})
|
||||
.unwrap_or_default();
|
||||
// Album cache for the right pane + album_count. With Plex ON,
|
||||
// use the Plex-aware ATTACH/union query so the artist-detail grid
|
||||
// and counts include Plex albums (1:1 with the Albums tab set);
|
||||
@@ -2806,8 +2867,8 @@ pub fn ensure_artists_loaded(
|
||||
None,
|
||||
"artist",
|
||||
"asc",
|
||||
false,
|
||||
true,
|
||||
exclude_network,
|
||||
plex_path.as_deref(),
|
||||
)
|
||||
.map(|p| p.albums)
|
||||
@@ -2815,7 +2876,7 @@ pub fn ensure_artists_loaded(
|
||||
.unwrap_or_default()
|
||||
} else {
|
||||
crate::library_db::with_db(|db| {
|
||||
db.get_albums_with_full_filter(false, true, false)
|
||||
db.get_albums_with_full_filter(false, true, exclude_network)
|
||||
})
|
||||
.unwrap_or_default()
|
||||
};
|
||||
|
||||
@@ -515,10 +515,31 @@ pub fn cleanup_missing(weak: Weak<AppWindow>, handle: tokio::runtime::Handle) {
|
||||
handle.spawn(async move {
|
||||
let result = tokio::task::spawn_blocking(|| {
|
||||
let paths = crate::library_db::with_db(|db| db.get_all_track_paths())?;
|
||||
// Same guard as the scan's cleanup phase: a network folder whose
|
||||
// mount is DOWN right now stats as missing for every file — that
|
||||
// is "unreachable", not "deleted". Skip those subtrees so a
|
||||
// maintenance click while a share is unmounted can't wipe its
|
||||
// index.
|
||||
let skip: Vec<String> =
|
||||
crate::library_db::with_db(|db| db.get_folders_with_metadata())
|
||||
.unwrap_or_default()
|
||||
.into_iter()
|
||||
.filter(|f| f.is_network && std::fs::read_dir(&f.path).is_err())
|
||||
.map(|f| {
|
||||
if f.path.ends_with('/') {
|
||||
f.path
|
||||
} else {
|
||||
format!("{}/", f.path)
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
let checked = paths.len();
|
||||
let missing: Vec<i64> = paths
|
||||
.into_iter()
|
||||
.filter(|(_, p)| !std::path::Path::new(p).exists())
|
||||
.filter(|(_, p)| {
|
||||
!skip.iter().any(|pre| p.starts_with(pre.as_str()))
|
||||
&& !std::path::Path::new(p).exists()
|
||||
})
|
||||
.map(|(id, _)| id)
|
||||
.collect();
|
||||
let mut removed = 0usize;
|
||||
@@ -602,13 +623,7 @@ pub fn clear_library(weak: Weak<AppWindow>, handle: tokio::runtime::Handle) {
|
||||
let _ = weak.upgrade_in_event_loop(|w| {
|
||||
w.global::<LibraryFoldersState>().set_clearing_library(false);
|
||||
// Reset the browse models so the tabs re-fetch on next visit.
|
||||
let s = w.global::<crate::LocalLibraryState>();
|
||||
let empty_albums = ModelRc::new(VecModel::from(Vec::<crate::AlbumCardItem>::new()));
|
||||
let empty_tracks = ModelRc::new(VecModel::from(Vec::<crate::TrackItem>::new()));
|
||||
s.set_albums(empty_albums.clone());
|
||||
s.set_folders(empty_albums);
|
||||
s.set_tracks(empty_tracks);
|
||||
s.set_artists(ModelRc::new(VecModel::from(Vec::<crate::LocalArtistItem>::new())));
|
||||
crate::local_library::reset_browse_models(&w);
|
||||
});
|
||||
if ok {
|
||||
crate::toast::success_weak(&weak, "Library database cleared");
|
||||
@@ -752,13 +767,7 @@ fn run_scan(weak: Weak<AppWindow>, handle: tokio::runtime::Handle, ids: Option<V
|
||||
// Post-scan: refresh the folder list (last_scan labels) + reset the
|
||||
// browse models so the tabs re-fetch the new index on next visit.
|
||||
let _ = weak.upgrade_in_event_loop(|w| {
|
||||
let s = w.global::<crate::LocalLibraryState>();
|
||||
let empty_albums = ModelRc::new(VecModel::from(Vec::<crate::AlbumCardItem>::new()));
|
||||
let empty_tracks = ModelRc::new(VecModel::from(Vec::<crate::TrackItem>::new()));
|
||||
s.set_albums(empty_albums.clone());
|
||||
s.set_folders(empty_albums);
|
||||
s.set_tracks(empty_tracks);
|
||||
s.set_artists(ModelRc::new(VecModel::from(Vec::<crate::LocalArtistItem>::new())));
|
||||
crate::local_library::reset_browse_models(&w);
|
||||
});
|
||||
load_folders(weak, h);
|
||||
});
|
||||
|
||||
@@ -415,15 +415,20 @@ pub fn fetch_local_and_plex(artist_name: &str) -> Vec<Candidate> {
|
||||
// local rows carry `a.artwork_path`, with the same cover.jpg/folder.jpg
|
||||
// on-disk fallback the grid uses so a DB row missing artwork_path still
|
||||
// resolves a cover.
|
||||
crate::library_db::with_db(|db| {
|
||||
// Same flags as the LocalLibrary Albums tab (include offline copies;
|
||||
// network content connectivity-keyed — see local_library.rs's
|
||||
// NETWORK-FOLDER VISIBILITY note) so the builder sees the identical
|
||||
// candidate set.
|
||||
let exclude_network = crate::local_library::exclude_network_folders_now();
|
||||
crate::library_db::with_db(move |db| {
|
||||
let page = db.get_albums_metadata_page(
|
||||
0,
|
||||
1_000_000,
|
||||
None,
|
||||
"artist",
|
||||
"asc",
|
||||
false,
|
||||
true,
|
||||
exclude_network,
|
||||
plex_path.as_deref(),
|
||||
)?;
|
||||
let out: Vec<Candidate> = page
|
||||
|
||||
@@ -221,18 +221,60 @@ enum OfflinePlayability {
|
||||
/// The track IS offline-cached but the D4 subscription grace window has
|
||||
/// elapsed — gets its own honest message.
|
||||
GraceExpired,
|
||||
/// A LOCAL user file whose indexed path is not on disk right now —
|
||||
/// typically an unmounted network drive. Checked online AND offline
|
||||
/// (library content is never hidden, so playback is where this must
|
||||
/// surface) — gets the "is the drive mounted?" message.
|
||||
FileMissing,
|
||||
}
|
||||
|
||||
/// Cheap existence guard for a LOCAL queue track's underlying file: resolve
|
||||
/// the indexed path (ephemeral store, or one indexed library-DB read) and
|
||||
/// stat it with `Path::exists()`. Unresolvable id/path → `true` (don't
|
||||
/// invent a skip; the play path has its own not-found handling).
|
||||
///
|
||||
/// D-STATE CAVEAT: `exists()` on an UNMOUNTED path returns false instantly
|
||||
/// (the path simply isn't there) — that is the case this guards. But a stat
|
||||
/// on a DEAD-yet-still-MOUNTED NFS/CIFS share can block in uninterruptible
|
||||
/// sleep (D state). This is therefore only ever called from the async layer
|
||||
/// (the advance walk and the play fast-fail run on the tokio runtime;
|
||||
/// `play_local_file_audible` checks inside `spawn_blocking`) and NEVER from
|
||||
/// the audio callback thread — a worst-case hang stalls an advance, not the
|
||||
/// audio pipeline. Do NOT add mount probing here; the hot path stays one
|
||||
/// stat per advance.
|
||||
fn local_track_file_exists(track: &QueueTrack) -> bool {
|
||||
let path = if crate::ephemeral::is_ephemeral_id(track.id as i64) {
|
||||
crate::ephemeral::get_track(track.id as i64).map(|row| row.file_path)
|
||||
} else {
|
||||
crate::library_db::with_db(|db| db.get_track(track.id as i64))
|
||||
.flatten()
|
||||
.map(|row| row.file_path)
|
||||
};
|
||||
match path {
|
||||
Some(p) => std::path::Path::new(&p).exists(),
|
||||
None => true,
|
||||
}
|
||||
}
|
||||
|
||||
/// Decide whether `track` can play under the CURRENT offline status.
|
||||
/// Online → always playable (the normal path pays one status read).
|
||||
/// Local / ephemeral user files → existence-checked regardless of
|
||||
/// online/offline (the library never hides network-folder content — see
|
||||
/// local_library.rs's NETWORK-FOLDER VISIBILITY note — so an unmounted
|
||||
/// drive is caught here, at playback time).
|
||||
/// Online → otherwise always playable (the normal path pays one status read).
|
||||
/// Offline:
|
||||
/// - local / ephemeral → playable (file presence is the player's own
|
||||
/// failure path)
|
||||
/// - plex → induced offline only (a LAN Plex server may be reachable;
|
||||
/// under real offline it is not — Tauri parity)
|
||||
/// - qobuz (incl. "qobuz_download" copies, which keep the real Qobuz id)
|
||||
/// → offline-cached AND within the D4 subscription grace window
|
||||
fn offline_playability(track: &QueueTrack) -> OfflinePlayability {
|
||||
if matches!(track.source.as_deref(), Some("local") | Some("ephemeral")) {
|
||||
return if local_track_file_exists(track) {
|
||||
OfflinePlayability::Playable
|
||||
} else {
|
||||
OfflinePlayability::FileMissing
|
||||
};
|
||||
}
|
||||
let status = crate::offline_mode::engine().status();
|
||||
if !status.is_offline() {
|
||||
return OfflinePlayability::Playable;
|
||||
@@ -241,7 +283,7 @@ fn offline_playability(track: &QueueTrack) -> OfflinePlayability {
|
||||
return OfflinePlayability::Playable;
|
||||
}
|
||||
match track.source.as_deref() {
|
||||
Some("local") | Some("ephemeral") => OfflinePlayability::Playable,
|
||||
// ("local" / "ephemeral" never reach here — handled above.)
|
||||
Some("plex") => {
|
||||
if status.mode == qbz_app::offline_mode::OfflineMode::InducedOffline {
|
||||
OfflinePlayability::Playable
|
||||
@@ -266,12 +308,14 @@ fn offline_track_playable(track: &QueueTrack) -> bool {
|
||||
offline_playability(track) == OfflinePlayability::Playable
|
||||
}
|
||||
|
||||
/// Move the queue cursor forward/backward to the next offline-playable
|
||||
/// track. Online this returns the immediate neighbor on the first
|
||||
/// iteration. Offline it skips unavailable tracks, bounded at
|
||||
/// [`MAX_OFFLINE_SKIPS`] consecutive (Tauri #467 parity); on exhaustion
|
||||
/// (bound hit, or queue edge after at least one skip) playback stops and
|
||||
/// ONE toast reports it.
|
||||
/// Move the queue cursor forward/backward to the next playable track.
|
||||
/// Online this returns the immediate neighbor on the first iteration unless
|
||||
/// that neighbor is a LOCAL file whose path is gone (unmounted drive) — the
|
||||
/// only possible online skip. Offline it also skips unavailable tracks.
|
||||
/// Bounded at [`MAX_OFFLINE_SKIPS`] consecutive (Tauri #467 parity); on
|
||||
/// exhaustion (bound hit, or queue edge after at least one skip) playback
|
||||
/// stops and ONE toast reports it — worded for the drive when every skip was
|
||||
/// a missing local file, for offline otherwise.
|
||||
///
|
||||
/// The gapless-prefetched target never passes through here: a gapless
|
||||
/// hand-off happens inside the audio engine and surfaces to the poll loop
|
||||
@@ -283,6 +327,17 @@ async fn advance_to_playable(
|
||||
forward: bool,
|
||||
) -> Option<QueueTrack> {
|
||||
let mut skips = 0usize;
|
||||
let mut missing_files = 0usize;
|
||||
// One message for the whole walk: when every skipped track was a local
|
||||
// file that isn't on disk, point at the drive; any other mix keeps the
|
||||
// offline wording (online, FileMissing is the only possible skip).
|
||||
let walk_toast = |skips: usize, missing_files: usize| {
|
||||
if missing_files == skips {
|
||||
"Files not available — is the drive mounted?"
|
||||
} else {
|
||||
"No tracks available offline"
|
||||
}
|
||||
};
|
||||
loop {
|
||||
let step = if forward {
|
||||
runtime.core().next_track().await
|
||||
@@ -295,27 +350,29 @@ async fn advance_to_playable(
|
||||
if skips > 0 {
|
||||
crate::toast::show_weak(
|
||||
weak,
|
||||
"No tracks available offline",
|
||||
walk_toast(skips, missing_files),
|
||||
crate::ToastKind::Warning,
|
||||
);
|
||||
}
|
||||
return None;
|
||||
};
|
||||
if offline_track_playable(&track) {
|
||||
return Some(track);
|
||||
match offline_playability(&track) {
|
||||
OfflinePlayability::Playable => return Some(track),
|
||||
OfflinePlayability::FileMissing => missing_files += 1,
|
||||
_ => {}
|
||||
}
|
||||
skips += 1;
|
||||
log::info!(
|
||||
"[qbz-slint] offline: skipping unavailable track {} ({skips}/{MAX_OFFLINE_SKIPS})",
|
||||
"[qbz-slint] advance: skipping unavailable track {} ({skips}/{MAX_OFFLINE_SKIPS})",
|
||||
track.id
|
||||
);
|
||||
if skips >= MAX_OFFLINE_SKIPS {
|
||||
if let Err(e) = runtime.core().stop() {
|
||||
log::warn!("[qbz-slint] offline: stop after skip bound failed: {e}");
|
||||
log::warn!("[qbz-slint] advance: stop after skip bound failed: {e}");
|
||||
}
|
||||
crate::toast::show_weak(
|
||||
weak,
|
||||
"No tracks available offline",
|
||||
walk_toast(skips, missing_files),
|
||||
crate::ToastKind::Warning,
|
||||
);
|
||||
return None;
|
||||
@@ -359,6 +416,17 @@ async fn play_audible(runtime: &Runtime, weak: &slint::Weak<AppWindow>, track_id
|
||||
);
|
||||
return;
|
||||
}
|
||||
OfflinePlayability::FileMissing => {
|
||||
log::info!(
|
||||
"[qbz-slint] local play: refused track {track_id} (file missing — unmounted drive?)"
|
||||
);
|
||||
crate::toast::show_weak(
|
||||
weak,
|
||||
"File not available — is the drive mounted?",
|
||||
crate::ToastKind::Warning,
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -482,13 +550,30 @@ async fn play_local_file_audible(
|
||||
clear_loading(weak, row_id);
|
||||
return;
|
||||
};
|
||||
// PLAYBACK LOCK (owner verdict 2026-06-10): the library never hides
|
||||
// network-folder content, so an unmounted drive surfaces HERE — one cheap
|
||||
// `Path::exists()` stat before the read, with friendly feedback instead
|
||||
// of a silent log-only failure. Runs inside spawn_blocking, never on the
|
||||
// audio callback thread: an unmounted path returns false instantly; only
|
||||
// a dead-but-still-mounted NFS/CIFS share could block, and then it blocks
|
||||
// a pool thread, not audio (see `local_track_file_exists`).
|
||||
let read_path = path.clone();
|
||||
let bytes = tokio::task::spawn_blocking(move || std::fs::read(&read_path))
|
||||
.await
|
||||
.ok()
|
||||
.and_then(Result::ok);
|
||||
let bytes = tokio::task::spawn_blocking(move || {
|
||||
if !std::path::Path::new(&read_path).exists() {
|
||||
return None;
|
||||
}
|
||||
std::fs::read(&read_path).ok()
|
||||
})
|
||||
.await
|
||||
.ok()
|
||||
.flatten();
|
||||
let Some(bytes) = bytes else {
|
||||
log::error!("[qbz-slint] local play: failed to read {path}");
|
||||
log::error!("[qbz-slint] local play: file not available at {path}");
|
||||
crate::toast::show_weak(
|
||||
weak,
|
||||
"File not available — is the drive mounted?",
|
||||
crate::ToastKind::Warning,
|
||||
);
|
||||
clear_loading(weak, row_id);
|
||||
return;
|
||||
};
|
||||
|
||||
@@ -754,19 +754,12 @@ pub async fn handle_bool(
|
||||
value: bool,
|
||||
) {
|
||||
let key = key.as_str();
|
||||
// Offline-MODE toggles persist through the shared engine's per-user
|
||||
// The offline-MODE toggle persists through the shared engine's per-user
|
||||
// store, not the audio/playback stores — routed apart from the Apply
|
||||
// machinery below.
|
||||
match key {
|
||||
"offline-mode-enabled" => {
|
||||
set_offline_mode(ctx, runtime, weak, value).await;
|
||||
return;
|
||||
}
|
||||
"offline-show-network-folders" => {
|
||||
set_offline_network_folders(weak, value);
|
||||
return;
|
||||
}
|
||||
_ => {}
|
||||
if key == "offline-mode-enabled" {
|
||||
set_offline_mode(ctx, runtime, weak, value).await;
|
||||
return;
|
||||
}
|
||||
// Cross-setting cascades — force dependent settings off and persist
|
||||
// those forced changes. `cascaded` flags whether a full snapshot
|
||||
@@ -944,25 +937,6 @@ async fn set_offline_mode(
|
||||
}
|
||||
}
|
||||
|
||||
/// Settings > Offline — the Show Network Folder Content policy toggle
|
||||
/// (visible only while induced offline is on). Pure persistence; the
|
||||
/// gating slice consumes the flag.
|
||||
fn set_offline_network_folders(weak: slint::Weak<AppWindow>, value: bool) {
|
||||
let engine = crate::offline_mode::engine();
|
||||
if let Err(e) = engine.set_show_network_folders(value) {
|
||||
log::error!("[qbz-slint] show-network-folders toggle failed: {e}");
|
||||
// Revert the optimistic toggle to the persisted state.
|
||||
let actual = engine
|
||||
.settings()
|
||||
.map(|s| s.show_network_folders_in_manual_offline)
|
||||
.unwrap_or(!value);
|
||||
let _ = weak.upgrade_in_event_loop(move |w| {
|
||||
w.global::<SettingsState>()
|
||||
.set_offline_show_network_folders(actual);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// Handle a slider change: persist it and reload the player settings.
|
||||
/// Currently only the Initial Buffer Size slider exists.
|
||||
pub fn handle_slider(
|
||||
|
||||
@@ -119,19 +119,6 @@ export component OfflineSettings inherits VerticalLayout {
|
||||
}
|
||||
}
|
||||
}
|
||||
// The network-folders policy only applies while induced offline is on,
|
||||
// so the row only mounts then (Tauri parity; ADR-010 conditional mount).
|
||||
if SettingsState.offline-mode-enabled: SettingRow {
|
||||
label: @tr("Show Network Folder Content");
|
||||
description: @tr("Display content from network-mounted folders (NAS, Samba, etc.) while in offline mode.");
|
||||
QbzToggle {
|
||||
checked: SettingsState.offline-show-network-folders;
|
||||
toggled(v) => {
|
||||
SettingsState.offline-show-network-folders = v;
|
||||
root.settings-bool("offline-show-network-folders", v);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle { height: 12px; }
|
||||
Divider { }
|
||||
|
||||
Reference in new issue
Block a user