From d36f55b3f66b28abbc87dad52995d01b4ac4bb17 Mon Sep 17 00:00:00 2001 From: Ultradesu Date: Mon, 27 Jul 2026 14:13:56 +0100 Subject: [PATCH] Reworked MODE switch --- Cargo.toml | 2 +- src/app/action.rs | 9 +- src/app/mod.rs | 40 +++++++-- src/app/popup.rs | 35 ++------ src/app/update.rs | 88 ++++++++++++++---- src/app/update_tests.rs | 159 +++++++++++++++++++++++++++++++++ src/config/default_keymap.toml | 4 + src/config/keymap.rs | 15 ++++ src/config/settings.rs | 29 ++---- src/ui/global.rs | 6 +- src/ui/mod.rs | 5 +- src/ui/playlists.rs | 21 ++++- src/ui/popup.rs | 16 +--- 13 files changed, 333 insertions(+), 96 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 07c633f..d721084 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "furumi_tui" -version = "0.1.6" +version = "0.1.7" edition = "2024" rust-version = "1.97" description = "A federated P2P player for personal music libraries" diff --git a/src/app/action.rs b/src/app/action.rs index e681b96..2e8d8c6 100644 --- a/src/app/action.rs +++ b/src/app/action.rs @@ -48,6 +48,7 @@ pub enum Action { NewPlaylist, ToggleHelp, ToggleViewMode, + CycleSourceMode, OpenLibraryFilters, OpenCommandLine, OpenSearch, @@ -128,9 +129,10 @@ impl Action { | Action::GoToTab(_) | Action::GoToRelease | Action::ToggleViewMode => Category::Navigation, - Action::EditSelected | Action::DeleteSelected | Action::OpenLibraryFilters => { - Category::Library - } + Action::EditSelected + | Action::DeleteSelected + | Action::CycleSourceMode + | Action::OpenLibraryFilters => Category::Library, Action::OpenSearch | Action::OpenCommandLine => Category::Search, Action::ToggleHelp | Action::Quit => Category::System, } @@ -197,6 +199,7 @@ impl Action { Action::NewPlaylist => "Create a playlist".into(), Action::ToggleHelp => "Show / hide keybindings".into(), Action::ToggleViewMode => "Toggle tiles / table view".into(), + Action::CycleSourceMode => "Cycle source mode: Local / My / Global".into(), Action::OpenLibraryFilters => "Library filters…".into(), Action::OpenCommandLine => "Command line (:help for commands)".into(), Action::OpenSearch => "Search artists, releases, tracks".into(), diff --git a/src/app/mod.rs b/src/app/mod.rs index 70cd745..3990192 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -425,14 +425,21 @@ fn apply_playback_state_to_ui( wire: &crate::devices::PlaybackStateWire, library: Option<&Library>, ) { - state.player.queue = wire + let queue: Vec<_> = wire .queue .iter() .map(|track| playback_track_to_ui(track, library)) .collect(); - state.player.queue_pos = wire - .queue_pos - .min(state.player.queue.len().saturating_sub(1)); + let queue_pos = queue + .iter() + .take(wire.queue_pos) + .filter(|track| update::track_allowed_by_source_mode(state, track)) + .count(); + state.player.queue = queue + .into_iter() + .filter(|track| update::track_allowed_by_source_mode(state, track)) + .collect(); + state.player.queue_pos = queue_pos.min(state.player.queue.len().saturating_sub(1)); state.player.playing = wire.playing && !state.player.queue.is_empty(); state.player.paused = wire.paused; state.device_playback.local_idle_since_ms = if state.player.playing && !state.player.paused { @@ -1291,6 +1298,24 @@ fn perform_effect(state: &mut AppState, runtime: &mut Runtime, effect: Effect) { } Effect::SetOptions => {} Effect::PlaybackQueueChanged => {} + Effect::SourceModeChanged => { + runtime.library_network_refresh_at = None; + if let Ok(mut cursors) = runtime.library_network_cursors.lock() { + cursors.clear(); + } + if let Ok(mut done) = runtime.library_network_done.lock() { + done.clear(); + } + if let Ok(mut attempted) = runtime.library_network_art_attempted.lock() { + attempted.clear(); + } + save_app_settings(state); + reset_artist_pagination(state); + refresh_artists(state, runtime); + if let Some(effect) = update::apply_library_filter_change(state) { + perform_effect(state, runtime, effect); + } + } Effect::EnqueueRelease { id, next } => { let library = Arc::clone(&runtime.library); let tx = runtime.event_tx.clone(); @@ -3410,10 +3435,13 @@ fn handle_app_event(state: &mut AppState, runtime: &mut Runtime, event: AppEvent }); } AppEvent::EnqueueTracks { tracks, next } => { - let count = tracks.len(); + let previous_len = state.player.queue.len(); update::enqueue_tracks(state, tracks, next); + let count = state.player.queue.len().saturating_sub(previous_len); record_control_playback_state(state, runtime, false); - state.status_message = Some(if next { + state.status_message = Some(if count == 0 { + "no tracks available in the current source mode".to_string() + } else if next { format!("{count} tracks queued next") } else { format!("{count} tracks queued") diff --git a/src/app/popup.rs b/src/app/popup.rs index 812aabb..865bc11 100644 --- a/src/app/popup.rs +++ b/src/app/popup.rs @@ -404,45 +404,26 @@ fn handle_connected_devices( fn handle_library_filters( state: &mut AppState, runtime: &mut Runtime, - cursor: usize, + _cursor: usize, key: KeyEvent, ) { - let max_cursor = crate::config::settings::LibrarySourceMode::ALL.len(); - let cursor = cursor.min(max_cursor); + let cursor = 0; match key.code { KeyCode::Esc | KeyCode::Char('q') => {} KeyCode::Up | KeyCode::Char('k') => { - state.popup = Some(Popup::LibraryFilters { - cursor: cursor.saturating_sub(1), - }); + state.popup = Some(Popup::LibraryFilters { cursor: 0 }); } KeyCode::Down | KeyCode::Char('j') => { - state.popup = Some(Popup::LibraryFilters { - cursor: (cursor + 1).min(max_cursor), - }); + state.popup = Some(Popup::LibraryFilters { cursor: 0 }); } KeyCode::Enter | KeyCode::Char(' ') => { - if cursor == 0 { - state.global.filters.hide_featured_only = !state.global.filters.hide_featured_only; - } else if let Some(mode) = - crate::config::settings::LibrarySourceMode::ALL.get(cursor - 1) - { - state.global.filters.source_mode = *mode; - } - runtime.library_network_refresh_at = None; - if let Ok(mut cursors) = runtime.library_network_cursors.lock() { - cursors.clear(); - } - if let Ok(mut done) = runtime.library_network_done.lock() { - done.clear(); - } - if let Ok(mut attempted) = runtime.library_network_art_attempted.lock() { - attempted.clear(); - } + state.global.filters.hide_featured_only = !state.global.filters.hide_featured_only; super::save_app_settings(state); super::reset_artist_pagination(state); super::refresh_artists(state, runtime); - super::update::apply_library_filter_change(state); + if let Some(effect) = super::update::apply_library_filter_change(state) { + super::perform_effect(state, runtime, effect); + } } _ => state.popup = Some(Popup::LibraryFilters { cursor }), } diff --git a/src/app/update.rs b/src/app/update.rs index 4e13f59..ee39cb8 100644 --- a/src/app/update.rs +++ b/src/app/update.rs @@ -48,6 +48,8 @@ pub enum Effect { }, /// Queue/options changed without a direct audio engine action. PlaybackQueueChanged, + /// Persist and apply a Local / My / Global source-mode change. + SourceModeChanged, /// Persist the federation settings and start/stop the node. FedApplySettings, /// Force an immediate library publish into the DHT. @@ -239,6 +241,18 @@ pub fn update(state: &mut AppState, action: Action) -> Option { } None } + Action::CycleSourceMode => { + if matches!(state.active_tab, Tab::Global | Tab::Playlists | Tab::Queue) { + state.global.filters.source_mode = state.global.filters.source_mode.next(); + state.status_message = Some(format!( + "source mode: {}", + state.global.filters.source_mode.label() + )); + Some(Effect::SourceModeChanged) + } else { + None + } + } Action::OpenLibraryFilters => { if state.active_tab == Tab::Global { state.popup = Some(super::state::Popup::LibraryFilters { cursor: 0 }); @@ -920,7 +934,7 @@ fn current_track_list_context(state: &AppState) -> Option<(TrackSelectionScope, } } -fn current_track_list(state: &AppState) -> Option<(TrackSelectionScope, usize, &[TrackItem])> { +fn current_track_list(state: &AppState) -> Option<(TrackSelectionScope, usize, Vec<&TrackItem>)> { match state.active_tab { Tab::Global => match state.global.stack.last()? { GlobalView::Artist { id, cursor } => match state.artist_views.get(id)? { @@ -931,23 +945,25 @@ fn current_track_list(state: &AppState) -> Option<(TrackSelectionScope, usize, & Some(( TrackSelectionScope::ArtistTop(*id), *cursor, - &detail.top_tracks, + detail.top_tracks.iter().collect(), )) } else { let featured = cursor.checked_sub(tracks + releases)?; (featured < detail.featured_tracks.len()).then_some(( TrackSelectionScope::ArtistFeatured(*id), featured, - &detail.featured_tracks, + detail.featured_tracks.iter().collect(), )) } } _ => None, }, GlobalView::Release { id, cursor } => match state.release_views.get(id)? { - Loadable::Ready(detail) => { - Some((TrackSelectionScope::Release(*id), *cursor, &detail.tracks)) - } + Loadable::Ready(detail) => Some(( + TrackSelectionScope::Release(*id), + *cursor, + detail.tracks.iter().collect(), + )), _ => None, }, _ => None, @@ -963,7 +979,7 @@ fn current_track_list(state: &AppState) -> Option<(TrackSelectionScope, usize, & Tab::Queue => Some(( TrackSelectionScope::Queue, state.queue_tab.cursor, - &state.player.queue, + state.player.queue.iter().collect(), )), Tab::Federation | Tab::Logs => None, } @@ -987,7 +1003,7 @@ pub fn selected_tracks(state: &AppState) -> Vec { .unwrap_or_else(|| vec![cursor.min(tracks.len().saturating_sub(1))]); indices .into_iter() - .filter_map(|index| tracks.get(index).cloned()) + .filter_map(|index| tracks.get(index).map(|track| (*track).clone())) .collect() } @@ -1175,21 +1191,32 @@ pub fn selected_track(state: &AppState) -> Option { let opened = state.playlists.opened.as_ref()?; playlist_tracks(state, opened.id)? .get(opened.cursor) - .cloned() + .map(|track| (*track).clone()) } Tab::Queue => state.player.queue.get(state.queue_tab.cursor).cloned(), Tab::Federation | Tab::Logs => None, } } -/// Tracks backing an opened playlist, if loaded. -pub fn playlist_tracks(state: &AppState, id: i64) -> Option<&Vec> { +/// Visible tracks backing an opened playlist. Local mode excludes pending +/// federation entries so they cannot be selected or copied into playback. +pub fn playlist_tracks(state: &AppState, id: i64) -> Option> { match state.playlist_views.get(&id)? { - Loadable::Ready(detail) => Some(&detail.tracks), + Loadable::Ready(detail) => Some( + detail + .tracks + .iter() + .filter(|track| track_allowed_by_source_mode(state, track)) + .collect(), + ), _ => None, } } +pub(crate) fn track_allowed_by_source_mode(state: &AppState, track: &TrackItem) -> bool { + state.global.filters.source_mode.includes_network() || !track.is_fed_pending() +} + /// A *release* under the cursor (artist-view tile/row or a search release). fn selected_release_id(state: &AppState) -> Option { if state.active_tab != Tab::Global { @@ -1334,6 +1361,10 @@ pub(crate) fn track_artist_refs(track: &TrackItem) -> Vec, next: bool) { + let tracks: Vec<_> = tracks + .into_iter() + .filter(|track| track_allowed_by_source_mode(state, track)) + .collect(); let player = &mut state.player; if tracks.is_empty() { return; @@ -1790,7 +1821,7 @@ fn set_view_cursor(state: &mut AppState, value: usize) { /// Items in the playlists tab's current view (list or opened playlist). fn playlists_view_len(state: &AppState) -> usize { match &state.playlists.opened { - Some(opened) => playlist_tracks(state, opened.id).map_or(0, Vec::len), + Some(opened) => playlist_tracks(state, opened.id).map_or(0, |tracks| tracks.len()), None => match &state.playlists.list { Some(Loadable::Ready(list)) => list.len(), _ => 0, @@ -1832,7 +1863,7 @@ fn current_view_len(state: &AppState) -> usize { } } -pub(crate) fn apply_library_filter_change(state: &mut AppState) { +pub(crate) fn apply_library_filter_change(state: &mut AppState) -> Option { state.track_selection.clear(); let len = current_view_len(state); if state.active_tab == Tab::Global { @@ -1848,11 +1879,32 @@ pub(crate) fn apply_library_filter_change(state: &mut AppState) { }; *cursor = (*cursor).min(len.saturating_sub(1)); } + } else if state.active_tab == Tab::Playlists + && let Some(opened) = &mut state.playlists.opened + { + opened.cursor = opened.cursor.min(len.saturating_sub(1)); } if state.global.filters.source_mode.includes_network() { - return; + return None; } + let remote_indices: Vec<_> = state + .player + .queue + .iter() + .enumerate() + .filter_map(|(index, track)| track.is_fed_pending().then_some(index)) + .collect(); + let queue_effect = if remote_indices.is_empty() { + None + } else { + let outcome = remove_queue_indices(state, &remote_indices); + Some(Effect::RemoveQueueIndices { + indices: remote_indices, + restart_paused: outcome.restart_paused, + stop: outcome.stop, + }) + }; let message = match state.global.stack.last() { Some(GlobalView::Artist { id, .. }) => match state.artist_views.get(id) { Some(Loadable::Ready(detail)) @@ -1885,6 +1937,7 @@ pub(crate) fn apply_library_filter_change(state: &mut AppState) { if let Some(message) = message { state.status_message = Some(message); } + queue_effect } fn jump_selection(state: &mut AppState, first: bool) { @@ -1954,7 +2007,10 @@ fn jump_selection(state: &mut AppState, first: bool) { fn select_playlist(state: &mut AppState) -> Option { match state.playlists.opened { Some(opened) => { - let tracks = playlist_tracks(state, opened.id)?.clone(); + let tracks: Vec<_> = playlist_tracks(state, opened.id)? + .into_iter() + .cloned() + .collect(); if tracks.is_empty() { return None; } diff --git a/src/app/update_tests.rs b/src/app/update_tests.rs index 9d00a14..3955cda 100644 --- a/src/app/update_tests.rs +++ b/src/app/update_tests.rs @@ -41,6 +41,23 @@ fn test_track(id: i64) -> TrackItem { } } +fn pending_fed_track(id: i64) -> TrackItem { + crate::federation::pending_track(&crate::federation::FedTrack { + item_id: format!("fed-{id}"), + owner: "peer".into(), + own: false, + title: format!("remote-{id}"), + artist_names: vec!["remote artist".into()], + featured_artist_names: vec![], + year: None, + duration_seconds: Some(1), + content_id: Some(format!("b3:{id:064x}")), + release_title: Some("remote release".into()), + track_number: None, + disc_number: None, + }) +} + #[test] fn quit_needs_double_press() { let mut state = AppState::default(); @@ -115,6 +132,36 @@ fn library_filters_popup_opens_on_library_screens() { assert!(state.popup.is_none()); } +#[test] +fn source_mode_cycles_on_library_playlists_and_queue_tabs() { + use crate::config::settings::LibrarySourceMode; + + let mut state = AppState::default(); + assert_eq!( + update(&mut state, Action::CycleSourceMode), + Some(Effect::SourceModeChanged) + ); + assert_eq!(state.global.filters.source_mode, LibrarySourceMode::My); + + state.active_tab = Tab::Playlists; + assert_eq!( + update(&mut state, Action::CycleSourceMode), + Some(Effect::SourceModeChanged) + ); + assert_eq!(state.global.filters.source_mode, LibrarySourceMode::Global); + + state.active_tab = Tab::Queue; + assert_eq!( + update(&mut state, Action::CycleSourceMode), + Some(Effect::SourceModeChanged) + ); + assert_eq!(state.global.filters.source_mode, LibrarySourceMode::Local); + + state.active_tab = Tab::Federation; + assert_eq!(update(&mut state, Action::CycleSourceMode), None); + assert_eq!(state.global.filters.source_mode, LibrarySourceMode::Local); +} + #[test] fn back_closes_help_first() { let mut state = AppState::default(); @@ -387,6 +434,118 @@ fn queue_tab_select_and_clear() { assert!(!state.player.playing); } +#[test] +fn local_mode_hides_pending_federation_tracks_from_playlists_and_playback() { + let mut state = AppState { + active_tab: Tab::Playlists, + ..AppState::default() + }; + state.playlists.opened = Some(OpenedPlaylist { id: 7, cursor: 1 }); + state.playlist_views.insert( + 7, + Loadable::Ready(crate::library::models::PlaylistDetail { + id: 7, + title: "mixed".into(), + description: None, + tracks: vec![test_track(1), pending_fed_track(2), test_track(3)], + }), + ); + + assert_eq!( + playlist_tracks(&state, 7) + .unwrap() + .iter() + .map(|track| track.id) + .collect::>(), + vec![1, 3] + ); + assert_eq!( + update(&mut state, Action::Select), + Some(Effect::PlayCurrent) + ); + assert_eq!( + state + .player + .queue + .iter() + .map(|track| track.id) + .collect::>(), + vec![1, 3] + ); + assert_eq!(state.player.queue_pos, 1); +} + +#[test] +fn network_modes_show_pending_federation_playlist_tracks() { + let mut state = AppState::default(); + state.global.filters.source_mode = crate::config::settings::LibrarySourceMode::My; + state.playlist_views.insert( + 7, + Loadable::Ready(crate::library::models::PlaylistDetail { + id: 7, + title: "mixed".into(), + description: None, + tracks: vec![test_track(1), pending_fed_track(2)], + }), + ); + + assert_eq!(playlist_tracks(&state, 7).unwrap().len(), 2); +} + +#[test] +fn local_mode_rejects_async_federation_queue_additions() { + let mut state = AppState::default(); + + enqueue_tracks( + &mut state, + vec![test_track(1), pending_fed_track(2), test_track(3)], + false, + ); + + assert_eq!( + state + .player + .queue + .iter() + .map(|track| track.id) + .collect::>(), + vec![1, 3] + ); +} + +#[test] +fn switching_to_local_mode_removes_pending_federation_queue_tracks() { + let mut state = AppState::default(); + state.global.filters.source_mode = crate::config::settings::LibrarySourceMode::My; + state.player.queue = vec![test_track(1), pending_fed_track(2), test_track(3)]; + state.player.queue_pos = 1; + state.player.current = Some(state.player.queue[1].clone()); + state.player.playing = true; + state.global.filters.source_mode = crate::config::settings::LibrarySourceMode::Local; + + let effect = apply_library_filter_change(&mut state); + + assert!(matches!( + effect, + Some(Effect::RemoveQueueIndices { + indices, + restart_paused: Some(false), + stop: false, + }) if indices == vec![1] + )); + assert_eq!( + state + .player + .queue + .iter() + .map(|track| track.id) + .collect::>(), + vec![1, 3] + ); + assert_eq!(state.player.queue_pos, 1); + assert_eq!(state.player.current.as_ref().map(|track| track.id), Some(3)); +} + #[test] fn current_track_info_uses_now_playing_track() { let mut state = AppState { diff --git a/src/config/default_keymap.toml b/src/config/default_keymap.toml index 77b189b..8582670 100644 --- a/src/config/default_keymap.toml +++ b/src/config/default_keymap.toml @@ -225,6 +225,10 @@ command = "DeleteSelected" key_sequence = "v" command = "ToggleViewMode" +[[keymaps]] +key_sequence = "m" +command = "CycleSourceMode" + [[keymaps]] key_sequence = "f" command = "OpenLibraryFilters" diff --git a/src/config/keymap.rs b/src/config/keymap.rs index 9b2869c..21eb8de 100644 --- a/src/config/keymap.rs +++ b/src/config/keymap.rs @@ -426,6 +426,21 @@ mod tests { ); } + #[test] + fn default_source_mode_key_resolves_on_content_tabs() { + let mut km = keymap_from(DEFAULT_KEYMAP); + for context in [ + KeyContext::Library, + KeyContext::Playlists, + KeyContext::Queue, + ] { + assert_eq!( + km.resolve(key!(m), context), + KeyResolution::Action(Action::CycleSourceMode) + ); + } + } + #[test] fn default_shift_n_is_unbound() { let mut km = keymap_from(DEFAULT_KEYMAP); diff --git a/src/config/settings.rs b/src/config/settings.rs index 3eb5ebc..274da0a 100644 --- a/src/config/settings.rs +++ b/src/config/settings.rs @@ -11,12 +11,6 @@ pub enum LibrarySourceMode { } impl LibrarySourceMode { - pub const ALL: [LibrarySourceMode; 3] = [ - LibrarySourceMode::Local, - LibrarySourceMode::My, - LibrarySourceMode::Global, - ]; - pub fn label(self) -> &'static str { match self { LibrarySourceMode::Local => "Local", @@ -25,14 +19,6 @@ impl LibrarySourceMode { } } - pub fn description(self) -> &'static str { - match self { - LibrarySourceMode::Local => "only this device", - LibrarySourceMode::My => "this device + connected devices", - LibrarySourceMode::Global => "my devices + known federation peers", - } - } - pub fn includes_network(self) -> bool { !matches!(self, LibrarySourceMode::Local) } @@ -40,6 +26,14 @@ impl LibrarySourceMode { pub fn includes_global_peers(self) -> bool { matches!(self, LibrarySourceMode::Global) } + + pub fn next(self) -> Self { + match self { + LibrarySourceMode::Local => LibrarySourceMode::My, + LibrarySourceMode::My => LibrarySourceMode::Global, + LibrarySourceMode::Global => LibrarySourceMode::Local, + } + } } #[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)] @@ -50,12 +44,6 @@ pub struct LibraryFilters { pub source_mode: LibrarySourceMode, } -impl LibraryFilters { - pub fn is_active(&self) -> bool { - self.hide_featured_only || self.source_mode != LibrarySourceMode::Local - } -} - #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct AppSettings { #[serde(default = "default_volume")] @@ -140,7 +128,6 @@ hide_featured_only = true assert_eq!(settings.volume, 100); assert!(settings.library.hide_featured_only); - assert!(settings.library.is_active()); assert_eq!(settings.library.source_mode, LibrarySourceMode::Local); } } diff --git a/src/ui/global.rs b/src/ui/global.rs index 4b4ed65..d884436 100644 --- a/src/ui/global.rs +++ b/src/ui/global.rs @@ -383,15 +383,15 @@ fn draw_grid(frame: &mut Frame, area: Rect, state: &AppState) { let global = &state.global; let title = if global.total > 0 { format!( - " Library — {} artists · {} ", + " Library — {} artists · Mode: {} ", global.total, global.filters.source_mode.label() ) } else { - format!(" Library · {} ", global.filters.source_mode.label()) + format!(" Library · Mode: {} ", global.filters.source_mode.label()) }; let mut title_spans = vec![Span::styled(title, theme::tab_active_for(state))]; - if global.filters.is_active() { + if global.filters.hide_featured_only { title_spans.push(Span::raw(" ")); title_spans.push(Span::styled(" FILTERED ", theme::tab_active_for(state))); } diff --git a/src/ui/mod.rs b/src/ui/mod.rs index 72e9e0e..bfb08c4 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -247,8 +247,9 @@ fn draw_queue(frame: &mut Frame, area: Rect, state: &AppState) { let player = &state.player; let block = Block::bordered() .title(format!( - " Queue — {} tracks · enter: play · d: remove · shift-v: select · :clear ", - player.queue.len() + " Queue — {} tracks · Mode: {} · enter: play · d: remove · shift-v: select · :clear ", + player.queue.len(), + state.global.filters.source_mode.label() )) .title_style(theme::header_for(state)) .border_style(theme::border_for(state)); diff --git a/src/ui/playlists.rs b/src/ui/playlists.rs index 40ee748..f903425 100644 --- a/src/ui/playlists.rs +++ b/src/ui/playlists.rs @@ -38,7 +38,15 @@ fn centered_line(frame: &mut Frame, area: Rect, line: Line) { } fn draw_list(frame: &mut Frame, area: Rect, state: &AppState) { - let inner = bordered(frame, area, state, " Playlists ".to_string()); + let inner = bordered( + frame, + area, + state, + format!( + " Playlists · Mode: {} ", + state.global.filters.source_mode.label() + ), + ); let selected = state.playlists.selected; let list = match &state.playlists.list { @@ -97,8 +105,15 @@ fn draw_list(frame: &mut Frame, area: Rect, state: &AppState) { fn draw_opened(frame: &mut Frame, area: Rect, state: &AppState, id: i64, cursor: usize) { let loadable = state.playlist_views.get(&id); let title = match loadable { - Some(Loadable::Ready(detail)) => format!(" Playlists ▸ {} ", detail.title), - _ => " Playlists ▸ … ".to_string(), + Some(Loadable::Ready(detail)) => format!( + " Playlists ▸ {} · Mode: {} ", + detail.title, + state.global.filters.source_mode.label() + ), + _ => format!( + " Playlists ▸ … · Mode: {} ", + state.global.filters.source_mode.label() + ), }; let inner = bordered(frame, area, state, title); diff --git a/src/ui/popup.rs b/src/ui/popup.rs index acd5eb7..aba1eef 100644 --- a/src/ui/popup.rs +++ b/src/ui/popup.rs @@ -598,7 +598,7 @@ fn clip_cells(text: &str, max_width: usize) -> String { } fn draw_library_filters(frame: &mut Frame, state: &AppState, cursor: usize) { - let area = centered(frame.area(), 54, 9); + let area = centered(frame.area(), 44, 6); let block = Block::bordered() .title(" Library filters ") .title_style(theme::header_for(state)) @@ -608,7 +608,7 @@ fn draw_library_filters(frame: &mut Frame, state: &AppState, cursor: usize) { frame.render_widget(block, area); let [list_area, _, footer] = Layout::vertical([ - Constraint::Length(4), + Constraint::Length(1), Constraint::Length(1), Constraint::Length(1), ]) @@ -624,18 +624,6 @@ fn draw_library_filters(frame: &mut Frame, state: &AppState, cursor: usize) { Span::styled(format!("{checked} "), theme::accent_for(state)), Span::raw("Hide featured only"), ])); - for mode in crate::config::settings::LibrarySourceMode::ALL { - let marker = if state.global.filters.source_mode == mode { - "(*)" - } else { - "( )" - }; - rows.push(Line::from(vec![ - Span::styled(format!("{marker} "), theme::accent_for(state)), - Span::raw(mode.label()), - Span::styled(format!(" {}", mode.description()), theme::dim()), - ])); - } for (index, line) in rows.into_iter().enumerate() { let row = Rect { y: list_area.y + index as u16,