Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion gui/src/bindings/HelpStrings.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.

export type HelpStrings = { arrow_key_template: string, key_alternative_template: string, next_page: string, previous_page: string, playlist_start: string, playlist_end: string, current_album: string, current_artist_albums: string, current_context: string, volume_up: string, volume_down: string, next_track: string, previous_track: string, force_previous_track: string, seek_backward: string, seek_forward: string, toggle_shuffle: string, copy_song_url: string, copy_album_url: string, cycle_repeat: string, move_left: string, move_down: string, move_up: string, move_right: string, list_top: string, list_middle: string, list_bottom: string, search: string, filter_help: string, search_playlist_picker: string, filter_settings: string, toggle_playback: string, active_mode: string, audio_analysis: string, visualizer_style: string, lyrics_view: string, lyrics_scroll: string, lyrics_follow: string, lyrics_timing: string, miniplayer: string, cover_art: string, back: string, source_device: string, settings_open: string, settings_save: string, settings_button: string, hover_mode: string, save_track: string, add_selected_to_playlist: string, add_playing_to_playlist: string, quick_add_playing: string, sidebar_narrower: string, sidebar_wider: string, panels_shorter: string, panels_taller: string, layout_reset: string, remove_playlist_track: string, search_playlist: string, clear_playlist_filter: string, play_selection: string, recommendations: string, artist_tracks: string, search_submit: string, cursor_left: string, cursor_right: string, input_clear: string, input_delete_before: string, input_delete_after: string, input_delete_word: string, input_start: string, input_end: string, input_escape: string, album_delete: string, artist_unfollow: string, show_remove: string, playlist_delete: string, playlist_mirror: string, radio_remove: string, follow_artist_playlist: string, album_save: string, playlist_random: string, episode_sort: string, show_follow: string, show_unfollow: string, queue_add: string, queue_show: string, queue_remove: string, queue_move: string, queue_play: string, toggle_saved: string, radio_favorite: string, listening_recap: string, stats_open: string, stats_period: string, stats_play: string, playlist_sync_open: string, playlist_sync_run: string, playlist_sync_remove: string, sort_open: string, party_open: string, dj_open: string, dj_auto_queue: string, dj_vibe: string, dj_fresh: string, dj_model: string, pane_detach: string, pane_close: string, pane_focus_left: string, pane_focus_down: string, pane_focus_up: string, pane_focus_right: string, pane_move_left: string, pane_move_down: string, pane_move_up: string, pane_move_right: string, pane_rotate: string, pane_grow: string, pane_shrink: string, pane_reset: string, context_pagination: string, context_general: string, context_general_playlist_search: string, context_help: string, context_playlist_picker: string, context_settings: string, context_lyrics: string, context_selected_block: string, context_add_track: string, context_playbar: string, context_global: string, context_layout: string, context_playlist_tracks: string, context_filtered_playlist_tracks: string, context_library_artists: string, context_search_input: string, context_library_albums: string, context_artist_unfollow: string, context_library_podcasts: string, context_playlist: string, context_radio: string, context_search_results: string, context_save_album: string, context_selected_playlist: string, context_selected_show: string, context_hovered_track: string, context_queue: string, context_stats: string, context_playlist_sync: string, context_sortable_lists: string, context_ai_dj: string, library_sidebar: string, title: string, column_description: string, column_event: string, column_context: string, no_matches_template: string, filter_prompt: string, filter_edit_hint: string, matches_template: string, filter_count_hint_template: string, filter_hint: string, needs_spotify: string, not_for_source_template: string, only_for_source_template: string, unavailable_description_template: string, };
export type HelpStrings = { arrow_key_template: string, key_alternative_template: string, next_page: string, previous_page: string, playlist_start: string, playlist_end: string, current_album: string, current_artist_albums: string, current_context: string, volume_up: string, volume_down: string, next_track: string, previous_track: string, force_previous_track: string, seek_backward: string, seek_forward: string, toggle_shuffle: string, copy_song_url: string, copy_album_url: string, cycle_repeat: string, move_left: string, move_down: string, move_up: string, move_right: string, list_top: string, list_middle: string, list_bottom: string, search: string, filter_help: string, search_playlist_picker: string, filter_settings: string, toggle_playback: string, active_mode: string, audio_analysis: string, visualizer_style: string, lyrics_view: string, lyrics_scroll: string, lyrics_follow: string, lyrics_timing: string, lyrics_seek: string, miniplayer: string, cover_art: string, back: string, source_device: string, settings_open: string, settings_save: string, settings_button: string, hover_mode: string, save_track: string, add_selected_to_playlist: string, add_playing_to_playlist: string, quick_add_playing: string, sidebar_narrower: string, sidebar_wider: string, panels_shorter: string, panels_taller: string, layout_reset: string, remove_playlist_track: string, search_playlist: string, clear_playlist_filter: string, play_selection: string, recommendations: string, artist_tracks: string, search_submit: string, cursor_left: string, cursor_right: string, input_clear: string, input_delete_before: string, input_delete_after: string, input_delete_word: string, input_start: string, input_end: string, input_escape: string, album_delete: string, artist_unfollow: string, show_remove: string, playlist_delete: string, playlist_mirror: string, radio_remove: string, follow_artist_playlist: string, album_save: string, playlist_random: string, episode_sort: string, show_follow: string, show_unfollow: string, queue_add: string, queue_show: string, queue_remove: string, queue_move: string, queue_play: string, toggle_saved: string, radio_favorite: string, listening_recap: string, stats_open: string, stats_period: string, stats_play: string, playlist_sync_open: string, playlist_sync_run: string, playlist_sync_remove: string, sort_open: string, party_open: string, dj_open: string, dj_auto_queue: string, dj_vibe: string, dj_fresh: string, dj_model: string, pane_detach: string, pane_close: string, pane_focus_left: string, pane_focus_down: string, pane_focus_up: string, pane_focus_right: string, pane_move_left: string, pane_move_down: string, pane_move_up: string, pane_move_right: string, pane_rotate: string, pane_grow: string, pane_shrink: string, pane_reset: string, context_pagination: string, context_general: string, context_general_playlist_search: string, context_help: string, context_playlist_picker: string, context_settings: string, context_lyrics: string, context_selected_block: string, context_add_track: string, context_playbar: string, context_global: string, context_layout: string, context_playlist_tracks: string, context_filtered_playlist_tracks: string, context_library_artists: string, context_search_input: string, context_library_albums: string, context_artist_unfollow: string, context_library_podcasts: string, context_playlist: string, context_radio: string, context_search_results: string, context_save_album: string, context_selected_playlist: string, context_selected_show: string, context_hovered_track: string, context_queue: string, context_stats: string, context_playlist_sync: string, context_sortable_lists: string, context_ai_dj: string, library_sidebar: string, title: string, column_description: string, column_event: string, column_context: string, no_matches_template: string, filter_prompt: string, filter_edit_hint: string, matches_template: string, filter_count_hint_template: string, filter_hint: string, needs_spotify: string, not_for_source_template: string, only_for_source_template: string, unavailable_description_template: string, };
9 changes: 6 additions & 3 deletions gui/src/bindings/strings.json
Original file line number Diff line number Diff line change
Expand Up @@ -601,6 +601,7 @@
"listening_recap": "Generate listening recap card (selected period on Stats, 30 days elsewhere)",
"lyrics_follow": "Resume following the current lyric line",
"lyrics_scroll": "Scroll lyrics (pauses auto-follow)",
"lyrics_seek": "Seek to selected lyric line",
"lyrics_timing": "Nudge lyric timing earlier/later",
"lyrics_view": "Go to lyrics view",
"matches_template": "matches for '{filter}'",
Expand Down Expand Up @@ -1036,7 +1037,7 @@
"local_files_empty": "Local Files (no folders — set behavior.local_music_path)",
"local_source_short": "Local",
"log_location": "Log located in {path}",
"lyrics_browsing": "browsing · ↑/↓ scroll · Esc follow",
"lyrics_browsing": "browsing · ↑/↓ scroll · Esc follow · Enter seek",
"lyrics_credit": "lyrics provided by LRCLIB",
"lyrics_from": "from LRCLIB",
"lyrics_loading": "Fetching lyrics…",
Expand Down Expand Up @@ -2710,6 +2711,7 @@
"listening_recap": "生成收听回顾卡片(统计页用所选时间段,其他界面用 30 天)",
"lyrics_follow": "恢复跟随当前歌词",
"lyrics_scroll": "滚动歌词(暂停自动跟随)",
"lyrics_seek": "跳转到所选歌词行",
"lyrics_timing": "微调歌词时间:提前或延后",
"lyrics_view": "打开歌词界面",
"matches_template": "“{filter}”的匹配项",
Expand Down Expand Up @@ -3145,7 +3147,7 @@
"local_files_empty": "本地文件(没有文件夹,请设置 behavior.local_music_path)",
"local_source_short": "本地",
"log_location": "日志位于 {path}",
"lyrics_browsing": "浏览中 · ↑/↓ 滚动 · Esc 跟随",
"lyrics_browsing": "浏览中 · ↑/↓ 滚动 · Esc 跟随 · Enter 跳转",
"lyrics_credit": "歌词由 LRCLIB 提供",
"lyrics_from": "来自 LRCLIB",
"lyrics_loading": "正在获取歌词…",
Expand Down Expand Up @@ -4819,6 +4821,7 @@
"listening_recap": "產生聆聽回顧卡片(統計頁使用所選期間,其他畫面使用 30 天)",
"lyrics_follow": "恢復跟隨目前歌詞",
"lyrics_scroll": "捲動歌詞(暫停自動跟隨)",
"lyrics_seek": "跳轉到所選歌詞行",
"lyrics_timing": "微調歌詞時間:提前或延後",
"lyrics_view": "開啟歌詞畫面",
"matches_template": "「{filter}」的符合項目",
Expand Down Expand Up @@ -5254,7 +5257,7 @@
"local_files_empty": "本機檔案(無資料夾,請設定 behavior.local_music_path)",
"local_source_short": "本機",
"log_location": "日誌位於 {path}",
"lyrics_browsing": "瀏覽中 · ↑/↓ 捲動 · Esc 跟隨",
"lyrics_browsing": "瀏覽中 · ↑/↓ 捲動 · Esc 跟隨 · Enter 跳轉",
"lyrics_credit": "歌詞由 LRCLIB 提供",
"lyrics_from": "來自 LRCLIB",
"lyrics_loading": "正在取得歌詞…",
Expand Down
7 changes: 7 additions & 0 deletions src/core/app/construction.rs
Original file line number Diff line number Diff line change
Expand Up @@ -310,6 +310,13 @@ impl App {
self
}

/// This app with song progress seeded, for tests.
#[cfg(all(test, feature = "tui"))]
pub(crate) fn with_song_progress(mut self, progress_ms: u128) -> App {
self.song_progress_ms = progress_ms;
self
}

/// Mark the native backend parked with no player, for tests.
#[cfg(all(test, feature = "streaming"))]
pub(crate) fn seed_native_parked(&mut self) {
Expand Down
4 changes: 4 additions & 0 deletions src/core/i18n/help.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ pub struct HelpStrings {
pub lyrics_scroll: &'static str,
pub lyrics_follow: &'static str,
pub lyrics_timing: &'static str,
pub lyrics_seek: &'static str,
pub miniplayer: &'static str,
pub cover_art: &'static str,
pub back: &'static str,
Expand Down Expand Up @@ -212,6 +213,7 @@ pub const EN: HelpStrings = HelpStrings {
lyrics_scroll: "Scroll lyrics (pauses auto-follow)",
lyrics_follow: "Resume following the current lyric line",
lyrics_timing: "Nudge lyric timing earlier/later",
lyrics_seek: "Seek to selected lyric line",
miniplayer: "Toggle miniplayer view",
cover_art: "Go to cover art view",
back: "Go back or exit when nowhere left to back to",
Expand Down Expand Up @@ -381,6 +383,7 @@ pub const ZH_HANS: HelpStrings = HelpStrings {
lyrics_scroll: "滚动歌词(暂停自动跟随)",
lyrics_follow: "恢复跟随当前歌词",
lyrics_timing: "微调歌词时间:提前或延后",
lyrics_seek: "跳转到所选歌词行",
miniplayer: "切换迷你播放器",
cover_art: "打开封面界面",
back: "返回;无处可返回时退出",
Expand Down Expand Up @@ -548,6 +551,7 @@ pub const ZH_HANT: HelpStrings = HelpStrings {
lyrics_scroll: "捲動歌詞(暫停自動跟隨)",
lyrics_follow: "恢復跟隨目前歌詞",
lyrics_timing: "微調歌詞時間:提前或延後",
lyrics_seek: "跳轉到所選歌詞行",
miniplayer: "切換迷你播放器",
cover_art: "開啟封面畫面",
back: "返回;無處可返回時離開",
Expand Down
6 changes: 3 additions & 3 deletions src/core/i18n/screens.rs
Original file line number Diff line number Diff line change
Expand Up @@ -317,7 +317,7 @@ pub const EN: ScreenStrings = ScreenStrings {
no_devices: "No devices found: Make sure a device is active",
timing_estimated: "timing estimated",
lyrics_title: " Lyrics ",
lyrics_browsing: "browsing · ↑/↓ scroll · Esc follow",
lyrics_browsing: "browsing · ↑/↓ scroll · Esc follow · Enter seek",
lyrics_loading: "Fetching lyrics…",
lyrics_from: "from LRCLIB",
lyrics_missing: "No lyrics for this track",
Expand Down Expand Up @@ -582,7 +582,7 @@ pub const ZH_HANS: ScreenStrings = ScreenStrings {
no_devices: "未找到设备:请确保有设备处于活动状态",
timing_estimated: "时间为估算值",
lyrics_title: " 歌词 ",
lyrics_browsing: "浏览中 · ↑/↓ 滚动 · Esc 跟随",
lyrics_browsing: "浏览中 · ↑/↓ 滚动 · Esc 跟随 · Enter 跳转",
lyrics_loading: "正在获取歌词…",
lyrics_from: "来自 LRCLIB",
lyrics_missing: "此歌曲暂无歌词",
Expand Down Expand Up @@ -849,7 +849,7 @@ pub const ZH_HANT: ScreenStrings = ScreenStrings {
no_devices: "找不到裝置:請確認有裝置處於作用中",
timing_estimated: "時間為估計值",
lyrics_title: " 歌詞 ",
lyrics_browsing: "瀏覽中 · ↑/↓ 捲動 · Esc 跟隨",
lyrics_browsing: "瀏覽中 · ↑/↓ 捲動 · Esc 跟隨 · Enter 跳轉",
lyrics_loading: "正在取得歌詞…",
lyrics_from: "來自 LRCLIB",
lyrics_missing: "此歌曲尚無歌詞",
Expand Down
132 changes: 132 additions & 0 deletions src/tui/handlers/lyrics_view.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ pub fn handler(key: Key, app: &mut App) {
k if k == app.user_config.keys.previous_page => scroll_by(app, -5),
k if k == app.user_config.keys.jump_to_start => jump_to(app, 0),
k if k == app.user_config.keys.jump_to_end => jump_to(app, usize::MAX),
k if k == app.user_config.keys.submit || k == Key::Enter => seek_to_focused(app),
Comment thread
LargeModGames marked this conversation as resolved.
Resolved
_ => {}
}
}
Expand Down Expand Up @@ -67,3 +68,134 @@ fn jump_to(app: &mut App, index: usize) {
app.view.lyrics_view.manual_index = Some(index.min(lyrics.len() - 1));
app.view.lyrics_view.last_manual_input = Some(Instant::now());
}

fn seek_to_focused(app: &mut App) {
if app.lyrics_status() != LyricsStatus::Found || !app.lyrics_synced() {
return;
}
let Some(lyrics) = app.lyrics() else {
return;
};
if lyrics.is_empty() {
return;
}
let target_index = app
.view
.lyrics_view
.manual_index
.unwrap_or_else(|| active_lyric_index(lyrics, app.lyric_progress_ms()))
.min(lyrics.len() - 1);

let target_time_ms = lyrics[target_index].0;
let seek_ms = (target_time_ms as i128 - i128::from(app.view.lyrics_view.timing_offset_ms)).max(0);
let target_u32 = u32::try_from(seek_ms).unwrap_or(u32::MAX);

app.apply(crate::core::action::Action::SeekTo(target_u32));
app.view.lyrics_view.manual_index = None;
}

#[cfg(test)]
mod tests {
use super::*;

fn playing_context() -> rspotify::model::CurrentPlaybackContext {
use rspotify::model::{
context::Actions, CurrentlyPlayingType, Device, DeviceType, PlayableItem, RepeatState,
};
rspotify::model::CurrentPlaybackContext {
device: Device {
id: Some("dev".to_string()),
is_active: true,
is_private_session: false,
is_restricted: false,
name: "Test Device".to_string(),
_type: DeviceType::Computer,
volume_percent: Some(50),
},
repeat_state: RepeatState::Off,
shuffle_state: false,
context: None,
timestamp: chrono::Utc::now(),
progress: Some(chrono::Duration::milliseconds(0)),
is_playing: true,
item: Some(PlayableItem::Track(crate::core::test_helpers::full_track(
"4uLU6hMCjMI75M1A2tKUQC",
"Test Song",
))),
currently_playing_type: CurrentlyPlayingType::Track,
actions: Actions::default(),
}
}

fn app_with_sample_lyrics() -> App {
let mut app = App::default_connected().with_playback(playing_context());
app.set_lyrics(
LyricsStatus::Found,
Some(vec![
(0, "Line 0".to_string()),
(5_000, "Line 1".to_string()),
(10_000, "Line 2".to_string()),
]),
true,
);
app
}

#[test]
fn enter_seeks_to_browsed_lyric_and_resumes_follow() {
let mut app = app_with_sample_lyrics();
app.view.lyrics_view.timing_offset_ms = 500;
// Browse to Line 2 (at 10_000 ms)
scroll_by(&mut app, 2);
assert_eq!(app.view.lyrics_view.manual_index, Some(2));

handler(Key::Enter, &mut app);

// 10_000 ms browsed lyric minus 500 ms timing offset
assert_eq!(app.song_progress_ms, 9_500);
// Manual browsing should be cleared (auto-follow resumes)
assert_eq!(app.view.lyrics_view.manual_index, None);
}

#[test]
fn enter_without_browsing_seeks_to_active_lyric() {
let mut app = app_with_sample_lyrics().with_song_progress(6_000);
assert_eq!(app.view.lyrics_view.manual_index, None);

handler(Key::Enter, &mut app);

// Active line at 6_000 ms is Line 1 (5_000 ms)
assert_eq!(app.song_progress_ms, 5_000);
assert_eq!(app.view.lyrics_view.manual_index, None);
}

#[test]
fn enter_does_nothing_when_lyrics_unsynced() {
let mut app = App::default()
.with_playback(playing_context())
.with_song_progress(1_000);
app.set_lyrics(
LyricsStatus::Found,
Some(vec![(0, "Unsynced text".to_string())]),
false,
);
app.view.lyrics_view.manual_index = Some(0);

handler(Key::Enter, &mut app);

assert_eq!(app.song_progress_ms, 1_000);
assert_eq!(app.view.lyrics_view.manual_index, Some(0));
}

#[test]
fn enter_does_nothing_when_lyrics_not_found() {
let mut app = App::default()
.with_playback(playing_context())
.with_song_progress(1_000);
app.set_lyrics(LyricsStatus::NotFound, None, false);

handler(Key::Enter, &mut app);

assert_eq!(app.song_progress_ms, 1_000);
}
}
Loading
Loading