Added Shift+J to jump to release

This commit is contained in:
Ultradesu
2026-06-10 17:01:40 +01:00
parent 00a558570c
commit 5be633d168
5 changed files with 157 additions and 0 deletions
+122
View File
@@ -187,6 +187,14 @@ pub fn update(state: &mut AppState, action: Action) -> Option<Effect> {
}
Action::QueueAddNext => queue_add(state, true),
Action::QueueAddLast => queue_add(state, false),
Action::GoToRelease => {
let track = selected_track(state).or_else(|| state.player.current.clone());
match track {
Some(track) => open_release_for_track(state, &track),
None => state.status_message = Some("no track selected".into()),
}
None
}
Action::ClearQueue => {
let had_tracks = !state.player.queue.is_empty();
state.player.queue.clear();
@@ -288,6 +296,40 @@ fn queue_add(state: &mut AppState, next: bool) -> Option<Effect> {
None
}
/// Shift-J: open the release the track belongs to, with the cursor on that
/// track. If the release view is still loading, the focus is applied when
/// it arrives (`pending_release_focus`).
fn open_release_for_track(state: &mut AppState, track: &TrackItem) {
let release_id = track.release_id;
let cursor = match state.release_views.get(&release_id) {
Some(Loadable::Ready(detail)) => detail
.tracks
.iter()
.position(|t| t.id == track.id)
.unwrap_or(0),
_ => {
state.pending_release_focus = Some((release_id, track.id));
0
}
};
let origin = state.active_tab;
state.active_tab = Tab::Global;
match state.global.stack.last_mut() {
Some(GlobalView::Release { id, cursor: current }) if *id == release_id => {
*current = cursor;
}
_ => state.global.stack.push(GlobalView::Release {
id: release_id,
cursor,
}),
}
// Jumps from another tab return there on Esc; jumps within Global
// unwind the navigation stack as usual.
if origin != Tab::Global {
state.jump_origin = Some((origin, state.global.stack.len() - 1));
}
}
/// Insert tracks after the playing one (`next`) or at the end. Keeps the
/// gapless prefetch index pointing at the same track if items shift.
pub fn enqueue_tracks(state: &mut AppState, tracks: Vec<TrackItem>, next: bool) {
@@ -860,6 +902,16 @@ fn go_back(state: &mut AppState) {
state.playlists.opened = None;
}
Tab::Global => {
// Esc on a view opened by Shift-J from another tab goes back to
// that tab, not down the Global stack.
if let Some((origin, depth)) = state.jump_origin {
if state.global.stack.len() == depth + 1 {
state.global.stack.pop();
state.jump_origin = None;
state.active_tab = origin;
return;
}
}
if let Some(popped) = state.global.stack.pop() {
if matches!(popped, GlobalView::Search { .. }) {
state.search = SearchState::default();
@@ -873,6 +925,8 @@ fn go_back(state: &mut AppState) {
fn switch_tab(state: &mut AppState, tab: Tab) {
state.active_tab = tab;
state.help_visible = false;
// Manually leaving a view cancels any pending Shift-J return path.
state.jump_origin = None;
}
fn reset_tab(state: &mut AppState, tab: Tab) {
@@ -1261,6 +1315,74 @@ mod tests {
assert!(state.player.original_order.is_none());
}
#[test]
fn shift_j_opens_release_from_queue() {
use crate::api::models::{ReleaseDetail, TrackItem};
let track = |id: i64, release_id: i64| TrackItem {
id,
title: format!("t{id}"),
track_number: None,
duration_seconds: 1.0,
artists: vec![],
featured_artists: vec![],
release_id,
release_title: "r".into(),
release_year: None,
cover_url: None,
stream_url: format!("/s/{id}"),
audio_format: None,
audio_bitrate: None,
audio_sample_rate: None,
file_size_bytes: None,
lastfm_playcount: None,
};
let mut state = AppState {
active_tab: Tab::Queue,
..AppState::default()
};
state.player.queue = vec![track(1, 7), track(2, 7)];
state.queue_tab.cursor = 1;
// Release not loaded yet → jump queued as pending focus.
update(&mut state, Action::GoToRelease);
assert_eq!(state.active_tab, Tab::Global);
assert_eq!(
state.global.stack.last(),
Some(&GlobalView::Release { id: 7, cursor: 0 })
);
assert_eq!(state.pending_release_focus, Some((7, 2)));
// Esc returns to the origin tab, not to the Global grid.
update(&mut state, Action::Back);
assert_eq!(state.active_tab, Tab::Queue);
assert!(state.global.stack.is_empty());
assert!(state.jump_origin.is_none());
// With the release cached, the cursor lands on the track directly.
state.global.stack.clear();
state.pending_release_focus = None;
state.release_views.insert(
7,
Loadable::Ready(ReleaseDetail {
id: 7,
title: "r".into(),
release_type: "album".into(),
year: None,
cover_url: None,
artists: vec![],
tracks: vec![track(1, 7), track(2, 7)],
uploaders: vec![],
}),
);
state.active_tab = Tab::Queue;
update(&mut state, Action::GoToRelease);
assert_eq!(
state.global.stack.last(),
Some(&GlobalView::Release { id: 7, cursor: 1 })
);
assert!(state.pending_release_focus.is_none());
}
#[test]
fn view_toggle() {
let mut state = AppState::default();