Added playback history window

This commit is contained in:
Ultradesu
2026-07-27 23:37:46 +01:00
parent a7f41ff205
commit 28189bae95
14 changed files with 281 additions and 5 deletions
+5 -1
View File
@@ -43,6 +43,7 @@ pub enum Action {
RemoveFromQueue,
ClearQueue,
OpenConnectedDevices,
OpenListenHistory,
GoToRelease,
AddToPlaylist,
NewPlaylist,
@@ -102,7 +103,8 @@ impl Action {
| Action::ToggleShuffle
| Action::CycleRepeat
| Action::ToggleVisualizer
| Action::OpenConnectedDevices => Category::Playback,
| Action::OpenConnectedDevices
| Action::OpenListenHistory => Category::Playback,
Action::QueueAddNext
| Action::QueueAddLast
| Action::DownloadSelected
@@ -152,6 +154,7 @@ impl Action {
Action::CycleRepeat => Some(":repeat [off|one|all]"),
Action::ClearQueue => Some(":clear"),
Action::OpenConnectedDevices => None,
Action::OpenListenHistory => None,
Action::ToggleHelp => Some(":help"),
Action::OpenSearch => Some("/text"),
_ => None,
@@ -194,6 +197,7 @@ impl Action {
Action::RemoveFromQueue => "Queue: remove selected".into(),
Action::ClearQueue => "Queue: clear".into(),
Action::OpenConnectedDevices => "Connected devices…".into(),
Action::OpenListenHistory => "Listening history…".into(),
Action::GoToRelease => "Open the track's release".into(),
Action::AddToPlaylist => "Add track to a playlist…".into(),
Action::NewPlaylist => "Create a playlist".into(),
+1
View File
@@ -56,6 +56,7 @@ pub enum AppEvent {
LocalContentIdsLoaded(Result<Vec<String>, String>),
/// Counts and storage footprint of the local library/database.
LocalLibraryStatsLoaded(Result<crate::library::LocalLibraryStats, String>),
ListenHistoryLoaded(Result<Vec<crate::library::ListenHistoryEntry>, String>),
/// One content id became available locally while the UI is open.
LocalContentAvailable {
content_id: String,
+22 -1
View File
@@ -1332,6 +1332,18 @@ fn perform_effect(state: &mut AppState, runtime: &mut Runtime, effect: Effect) {
}
});
}
Effect::LoadListenHistory => {
let library = Arc::clone(&runtime.library);
let devices = Arc::clone(&runtime.devices);
let tx = runtime.event_tx.clone();
tokio::task::spawn_blocking(move || {
let result = library
.listen_history(500)
.map_err(|err| format!("{err:#}"));
let _ = tx.send(AppEvent::ListenHistoryLoaded(result));
let _ = tx.send(AppEvent::DeviceSyncStatus(devices.status()));
});
}
Effect::ToggleLikes {
track_ids,
fed_tracks,
@@ -1685,7 +1697,10 @@ fn perform_control_playback_effect(state: &mut AppState, runtime: &mut Runtime,
state.player.volume = volume.min(100);
save_app_settings(state);
}
Effect::SetOptions | Effect::RemoveQueueIndices { .. } | Effect::PlaybackQueueChanged => {}
Effect::SetOptions
| Effect::RemoveQueueIndices { .. }
| Effect::PlaybackQueueChanged
| Effect::LoadListenHistory => {}
_ => {}
}
record_control_playback_state(state, runtime, seek);
@@ -2831,6 +2846,12 @@ fn handle_playback_command(
fn handle_app_event(state: &mut AppState, runtime: &mut Runtime, event: AppEvent) {
match event {
AppEvent::StatusMessage(message) => state.status_message = Some(message),
AppEvent::ListenHistoryLoaded(result) => {
state.listen_history = Some(match result {
Ok(entries) => state::Loadable::Ready(entries),
Err(err) => state::Loadable::Failed(err),
});
}
AppEvent::FederationStatus(status) => {
state.federation.status = Some(status);
}
+19
View File
@@ -196,9 +196,28 @@ pub fn handle_key(state: &mut AppState, runtime: &mut Runtime, key: KeyEvent) {
Popup::ConnectedDevices { cursor } => {
handle_connected_devices(state, runtime, cursor, key);
}
Popup::ListenHistory { cursor } => handle_listen_history(state, cursor, key),
}
}
fn handle_listen_history(state: &mut AppState, cursor: usize, key: KeyEvent) {
let len = match state.listen_history.as_ref() {
Some(crate::app::state::Loadable::Ready(entries)) => entries.len(),
_ => 0,
};
let cursor = match key.code {
KeyCode::Esc | KeyCode::Char('q') => return,
KeyCode::Up | KeyCode::Char('k') => cursor.saturating_sub(1),
KeyCode::Down | KeyCode::Char('j') => (cursor + 1).min(len.saturating_sub(1)),
KeyCode::PageUp => cursor.saturating_sub(10),
KeyCode::PageDown => (cursor + 10).min(len.saturating_sub(1)),
KeyCode::Home | KeyCode::Char('g') => 0,
KeyCode::End | KeyCode::Char('G') => len.saturating_sub(1),
_ => cursor,
};
state.popup = Some(Popup::ListenHistory { cursor });
}
fn handle_federation_status_details(
state: &mut AppState,
mut parent: FederationStatusPopupState,
+3
View File
@@ -732,6 +732,8 @@ pub enum Popup {
ConfirmDeviceLeave,
/// Connected playback devices and their current role/status.
ConnectedDevices { cursor: usize },
/// Qualified listening history from every trusted device.
ListenHistory { cursor: usize },
/// Full federation, transport and device status details.
FederationStatusDetails {
focus: StatusDetailFocus,
@@ -1280,6 +1282,7 @@ pub struct AppState {
pub likes_loaded: bool,
pub local_content_ids_loaded: bool,
pub local_library_stats: Option<Loadable<crate::library::LocalLibraryStats>>,
pub listen_history: Option<Loadable<Vec<crate::library::ListenHistoryEntry>>>,
pub logs: LogsTab,
pub queue_tab: QueueTab,
pub federation: FederationTab,
+7
View File
@@ -82,6 +82,8 @@ pub enum Effect {
OpenVisualizerEditor {
path: std::path::PathBuf,
},
/// Load qualified listening history without blocking the UI thread.
LoadListenHistory,
}
pub fn update(state: &mut AppState, action: Action) -> Option<Effect> {
@@ -114,6 +116,11 @@ pub fn update(state: &mut AppState, action: Action) -> Option<Effect> {
state.popup = Some(super::state::Popup::ConnectedDevices { cursor: 0 });
None
}
Action::OpenListenHistory => {
state.popup = Some(super::state::Popup::ListenHistory { cursor: 0 });
state.listen_history = Some(Loadable::Loading);
Some(Effect::LoadListenHistory)
}
Action::NextTab => {
switch_tab(state, state.active_tab.next());
None
+17
View File
@@ -16,6 +16,23 @@ fn with_artists(n: usize) -> AppState {
state
}
#[test]
fn listening_history_popup_requests_a_background_load() {
let mut state = AppState::default();
assert_eq!(
update(&mut state, Action::OpenListenHistory),
Some(Effect::LoadListenHistory)
);
assert!(matches!(
state.popup,
Some(crate::app::state::Popup::ListenHistory { cursor: 0 })
));
assert!(matches!(
state.listen_history,
Some(crate::app::state::Loadable::Loading)
));
}
fn test_track(id: i64) -> TrackItem {
TrackItem {
id,