Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 55e3f04e2c | |||
| 1c2c942e4b | |||
| 3e7707e883 | |||
| b59d1aed65 | |||
| f1412b4f8c | |||
| ffe7cd0090 | |||
| 907a734be3 | |||
| 135700ce02 | |||
| ea72368841 |
23
CLAUDE.md
23
CLAUDE.md
@@ -14,6 +14,29 @@ A high-performance Rust-based TUI player for playing music and video files. Buil
|
|||||||
|
|
||||||
## Architecture
|
## Architecture
|
||||||
|
|
||||||
|
### State Management
|
||||||
|
|
||||||
|
**CRITICAL:** Player state must be derived from MPV, not maintained separately.
|
||||||
|
|
||||||
|
**Single Source of Truth:** MPV properties via IPC
|
||||||
|
- `idle-active` (bool) - No file loaded or file ended
|
||||||
|
- `pause` (bool) - Playback is paused
|
||||||
|
|
||||||
|
**Derive PlayerState:**
|
||||||
|
```rust
|
||||||
|
if player.is_idle → PlayerState::Stopped
|
||||||
|
if !player.is_idle && player.is_paused → PlayerState::Paused
|
||||||
|
if !player.is_idle && !player.is_paused → PlayerState::Playing
|
||||||
|
```
|
||||||
|
|
||||||
|
**Rationale:**
|
||||||
|
- Eliminates state synchronization bugs
|
||||||
|
- MPV is always the authoritative source
|
||||||
|
- No need to update state in multiple places
|
||||||
|
- Simpler auto-play logic
|
||||||
|
|
||||||
|
**Anti-pattern:** DO NOT maintain `state.player_state` that can desync from MPV
|
||||||
|
|
||||||
### Cache-Only Operation
|
### Cache-Only Operation
|
||||||
|
|
||||||
**CRITICAL:** Left panel shows ONLY cached data. Never browse filesystem directly during operation.
|
**CRITICAL:** Left panel shows ONLY cached data. Never browse filesystem directly during operation.
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "cm-player"
|
name = "cm-player"
|
||||||
version = "0.1.14"
|
version = "0.1.22"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
|||||||
307
src/main.rs
307
src/main.rs
@@ -96,6 +96,8 @@ fn action_play_selection(state: &mut AppState, player: &mut player::Player) -> R
|
|||||||
state.play_selection();
|
state.play_selection();
|
||||||
if let Some(ref path) = state.current_file {
|
if let Some(ref path) = state.current_file {
|
||||||
player.play(path)?;
|
player.play(path)?;
|
||||||
|
// Explicitly resume playback in case MPV was paused
|
||||||
|
player.resume()?;
|
||||||
state.player_state = PlayerState::Playing;
|
state.player_state = PlayerState::Playing;
|
||||||
player.update_metadata();
|
player.update_metadata();
|
||||||
tracing::info!("Playing: {:?} (playlist: {} tracks)", path, state.playlist.len());
|
tracing::info!("Playing: {:?} (playlist: {} tracks)", path, state.playlist.len());
|
||||||
@@ -126,6 +128,7 @@ fn action_toggle_play_pause(state: &mut AppState, player: &mut player::Player) -
|
|||||||
state.player_state = PlayerState::Playing;
|
state.player_state = PlayerState::Playing;
|
||||||
if let Some(ref path) = state.current_file {
|
if let Some(ref path) = state.current_file {
|
||||||
player.play(path)?;
|
player.play(path)?;
|
||||||
|
player.resume()?;
|
||||||
player.update_metadata();
|
player.update_metadata();
|
||||||
tracing::info!("Restarting playback: {:?}", path);
|
tracing::info!("Restarting playback: {:?}", path);
|
||||||
}
|
}
|
||||||
@@ -144,6 +147,108 @@ fn action_stop(state: &mut AppState, player: &mut player::Player) -> Result<()>
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn action_remove_from_playlist(state: &mut AppState, player: &mut player::Player) -> Result<()> {
|
||||||
|
let was_playing_removed = state.playlist_index == state.selected_playlist_index;
|
||||||
|
state.remove_selected_playlist_item();
|
||||||
|
|
||||||
|
if state.playlist.is_empty() {
|
||||||
|
state.player_state = PlayerState::Stopped;
|
||||||
|
state.current_file = None;
|
||||||
|
player.stop()?;
|
||||||
|
} else if was_playing_removed && state.player_state == PlayerState::Playing {
|
||||||
|
state.current_file = Some(state.playlist[state.playlist_index].clone());
|
||||||
|
if let Some(ref path) = state.current_file {
|
||||||
|
player.play(path)?;
|
||||||
|
// Explicitly resume playback in case MPV was paused
|
||||||
|
player.resume()?;
|
||||||
|
player.update_metadata();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn action_play_from_playlist(state: &mut AppState, player: &mut player::Player, preserve_pause: bool) -> Result<()> {
|
||||||
|
state.playlist_index = state.selected_playlist_index;
|
||||||
|
state.current_file = Some(state.playlist[state.playlist_index].clone());
|
||||||
|
|
||||||
|
if preserve_pause {
|
||||||
|
match state.player_state {
|
||||||
|
PlayerState::Playing => {
|
||||||
|
if let Some(ref path) = state.current_file {
|
||||||
|
player.play(path)?;
|
||||||
|
player.resume()?;
|
||||||
|
player.update_metadata();
|
||||||
|
tracing::info!("Jumped to track: {:?}", path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
PlayerState::Paused => {
|
||||||
|
if let Some(ref path) = state.current_file {
|
||||||
|
player.play(path)?;
|
||||||
|
player.update_metadata();
|
||||||
|
player.pause()?;
|
||||||
|
tracing::info!("Jumped to track (paused): {:?}", path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
PlayerState::Stopped => {
|
||||||
|
state.player_state = PlayerState::Playing;
|
||||||
|
if let Some(ref path) = state.current_file {
|
||||||
|
player.play(path)?;
|
||||||
|
player.resume()?;
|
||||||
|
player.update_metadata();
|
||||||
|
tracing::info!("Started playing track: {:?}", path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
state.player_state = PlayerState::Playing;
|
||||||
|
if let Some(ref path) = state.current_file {
|
||||||
|
player.play(path)?;
|
||||||
|
// Explicitly resume playback in case MPV was paused
|
||||||
|
player.resume()?;
|
||||||
|
player.update_metadata();
|
||||||
|
tracing::info!("Playing from playlist: {:?}", path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_context_menu_action(menu_type: state::ContextMenuType, selected: usize, state: &mut AppState, player: &mut player::Player) -> Result<()> {
|
||||||
|
match menu_type {
|
||||||
|
state::ContextMenuType::FilePanel => {
|
||||||
|
match selected {
|
||||||
|
0 => action_play_selection(state, player)?,
|
||||||
|
1 => state.add_to_playlist(),
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
state::ContextMenuType::Playlist => {
|
||||||
|
match selected {
|
||||||
|
0 => action_remove_from_playlist(state, player)?,
|
||||||
|
1 => {
|
||||||
|
state.shuffle_playlist();
|
||||||
|
tracing::info!("Playlist randomised from context menu");
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
state::ContextMenuType::TitleBar => {
|
||||||
|
match selected {
|
||||||
|
0 => action_stop(state, player)?,
|
||||||
|
1 => {
|
||||||
|
state.cycle_play_mode();
|
||||||
|
tracing::info!("Play mode: {:?}", state.play_mode);
|
||||||
|
}
|
||||||
|
2 => {
|
||||||
|
state.show_refresh_confirm = true;
|
||||||
|
tracing::info!("Refresh requested from context menu");
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
async fn run_app<B: ratatui::backend::Backend>(
|
async fn run_app<B: ratatui::backend::Backend>(
|
||||||
terminal: &mut Terminal<B>,
|
terminal: &mut Terminal<B>,
|
||||||
state: &mut AppState,
|
state: &mut AppState,
|
||||||
@@ -183,8 +288,10 @@ async fn run_app<B: ratatui::backend::Backend>(
|
|||||||
let new_position = player.get_position().unwrap_or(0.0);
|
let new_position = player.get_position().unwrap_or(0.0);
|
||||||
let new_duration = player.get_duration().unwrap_or(0.0);
|
let new_duration = player.get_duration().unwrap_or(0.0);
|
||||||
|
|
||||||
// Only mark as changed if position moved by at least 0.5 seconds
|
// Only update if displayed value (rounded to seconds) changed
|
||||||
if (new_position - last_position).abs() >= 0.5 {
|
let old_display_secs = last_position as u32;
|
||||||
|
let new_display_secs = new_position as u32;
|
||||||
|
if new_display_secs != old_display_secs {
|
||||||
state.current_position = new_position;
|
state.current_position = new_position;
|
||||||
last_position = new_position;
|
last_position = new_position;
|
||||||
state_changed = true;
|
state_changed = true;
|
||||||
@@ -197,7 +304,8 @@ async fn run_app<B: ratatui::backend::Backend>(
|
|||||||
|
|
||||||
// Check if track ended and play next (but only if track was actually loaded AND played)
|
// Check if track ended and play next (but only if track was actually loaded AND played)
|
||||||
// Require position > 0.5 to ensure track actually started playing (not just loaded)
|
// Require position > 0.5 to ensure track actually started playing (not just loaded)
|
||||||
if player.is_idle() && state.player_state == PlayerState::Playing && state.current_duration > 0.0 && state.current_position > 0.5 {
|
// Also check !is_paused to avoid triggering during pause/unpause transitions
|
||||||
|
if player.is_idle() && !player.is_paused() && state.player_state == PlayerState::Playing && state.current_duration > 0.0 && state.current_position > 0.5 {
|
||||||
state.play_next();
|
state.play_next();
|
||||||
// play_next() handles the play mode and may stop if in Normal mode at end
|
// play_next() handles the play mode and may stop if in Normal mode at end
|
||||||
if state.player_state == PlayerState::Playing {
|
if state.player_state == PlayerState::Playing {
|
||||||
@@ -208,6 +316,7 @@ async fn run_app<B: ratatui::backend::Backend>(
|
|||||||
last_position = 0.0;
|
last_position = 0.0;
|
||||||
|
|
||||||
player.play(path)?;
|
player.play(path)?;
|
||||||
|
player.resume()?;
|
||||||
}
|
}
|
||||||
// Update metadata immediately when track changes
|
// Update metadata immediately when track changes
|
||||||
player.update_metadata();
|
player.update_metadata();
|
||||||
@@ -348,7 +457,7 @@ async fn handle_key_event<B: ratatui::backend::Backend>(terminal: &mut Terminal<
|
|||||||
let max_items = match menu.menu_type {
|
let max_items = match menu.menu_type {
|
||||||
ContextMenuType::FilePanel => 2,
|
ContextMenuType::FilePanel => 2,
|
||||||
ContextMenuType::Playlist => 2,
|
ContextMenuType::Playlist => 2,
|
||||||
ContextMenuType::TitleBar => 3,
|
ContextMenuType::TitleBar => 4,
|
||||||
};
|
};
|
||||||
if menu.selected_index < max_items - 1 {
|
if menu.selected_index < max_items - 1 {
|
||||||
menu.selected_index += 1;
|
menu.selected_index += 1;
|
||||||
@@ -359,64 +468,7 @@ async fn handle_key_event<B: ratatui::backend::Backend>(terminal: &mut Terminal<
|
|||||||
let menu_type = menu.menu_type;
|
let menu_type = menu.menu_type;
|
||||||
let selected = menu.selected_index;
|
let selected = menu.selected_index;
|
||||||
state.context_menu = None;
|
state.context_menu = None;
|
||||||
|
handle_context_menu_action(menu_type, selected, state, player)?;
|
||||||
match menu_type {
|
|
||||||
ContextMenuType::FilePanel => {
|
|
||||||
match selected {
|
|
||||||
0 => action_play_selection(state, player)?,
|
|
||||||
1 => state.add_to_playlist(),
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ContextMenuType::Playlist => {
|
|
||||||
match selected {
|
|
||||||
0 => {
|
|
||||||
// Remove
|
|
||||||
let was_playing_removed = state.playlist_index == state.selected_playlist_index;
|
|
||||||
state.remove_selected_playlist_item();
|
|
||||||
|
|
||||||
// Handle edge cases after removal
|
|
||||||
if state.playlist.is_empty() {
|
|
||||||
state.player_state = PlayerState::Stopped;
|
|
||||||
state.current_file = None;
|
|
||||||
player.stop()?;
|
|
||||||
} else if was_playing_removed && state.player_state == PlayerState::Playing {
|
|
||||||
// Removed currently playing track, start new one at same index
|
|
||||||
state.current_file = Some(state.playlist[state.playlist_index].clone());
|
|
||||||
if let Some(ref path) = state.current_file {
|
|
||||||
player.play(path)?;
|
|
||||||
player.update_metadata();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
1 => {
|
|
||||||
// Randomise
|
|
||||||
state.shuffle_playlist();
|
|
||||||
tracing::info!("Playlist randomised from context menu");
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ContextMenuType::TitleBar => {
|
|
||||||
match selected {
|
|
||||||
0 => {
|
|
||||||
// Stop
|
|
||||||
action_stop(state, player)?;
|
|
||||||
}
|
|
||||||
1 => {
|
|
||||||
// Toggle Loop
|
|
||||||
state.cycle_play_mode();
|
|
||||||
tracing::info!("Play mode: {:?}", state.play_mode);
|
|
||||||
}
|
|
||||||
2 => {
|
|
||||||
// Refresh
|
|
||||||
state.show_refresh_confirm = true;
|
|
||||||
tracing::info!("Refresh requested from context menu");
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
KeyCode::Esc => {
|
KeyCode::Esc => {
|
||||||
state.context_menu = None;
|
state.context_menu = None;
|
||||||
@@ -478,6 +530,7 @@ async fn handle_key_event<B: ratatui::backend::Backend>(terminal: &mut Terminal<
|
|||||||
// Keep playing
|
// Keep playing
|
||||||
if let Some(ref path) = state.current_file {
|
if let Some(ref path) = state.current_file {
|
||||||
player.play(path)?;
|
player.play(path)?;
|
||||||
|
player.resume()?;
|
||||||
player.update_metadata(); // Update metadata immediately
|
player.update_metadata(); // Update metadata immediately
|
||||||
tracing::info!("Next track: {:?}", path);
|
tracing::info!("Next track: {:?}", path);
|
||||||
}
|
}
|
||||||
@@ -516,6 +569,7 @@ async fn handle_key_event<B: ratatui::backend::Backend>(terminal: &mut Terminal<
|
|||||||
// Keep playing
|
// Keep playing
|
||||||
if let Some(ref path) = state.current_file {
|
if let Some(ref path) = state.current_file {
|
||||||
player.play(path)?;
|
player.play(path)?;
|
||||||
|
player.resume()?;
|
||||||
player.update_metadata(); // Update metadata immediately
|
player.update_metadata(); // Update metadata immediately
|
||||||
tracing::info!("Previous track: {:?}", path);
|
tracing::info!("Previous track: {:?}", path);
|
||||||
}
|
}
|
||||||
@@ -573,12 +627,12 @@ async fn handle_key_event<B: ratatui::backend::Backend>(terminal: &mut Terminal<
|
|||||||
state.move_selection_down();
|
state.move_selection_down();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
(KeyCode::Char('h'), _) => {
|
(KeyCode::Char('h'), _) | (KeyCode::Left, _) => {
|
||||||
if !state.focus_playlist {
|
if !state.focus_playlist {
|
||||||
state.collapse_selected();
|
state.collapse_selected();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
(KeyCode::Char('l'), _) => {
|
(KeyCode::Char('l'), _) | (KeyCode::Right, _) => {
|
||||||
if !state.focus_playlist {
|
if !state.focus_playlist {
|
||||||
state.expand_selected();
|
state.expand_selected();
|
||||||
}
|
}
|
||||||
@@ -602,39 +656,13 @@ async fn handle_key_event<B: ratatui::backend::Backend>(terminal: &mut Terminal<
|
|||||||
}
|
}
|
||||||
(KeyCode::Char('d'), _) => {
|
(KeyCode::Char('d'), _) => {
|
||||||
if state.focus_playlist {
|
if state.focus_playlist {
|
||||||
// Remove selected track from playlist
|
action_remove_from_playlist(state, player)?;
|
||||||
state.remove_selected_playlist_item();
|
|
||||||
// If removed currently playing track, handle it
|
|
||||||
if state.playlist.is_empty() {
|
|
||||||
state.player_state = PlayerState::Stopped;
|
|
||||||
state.current_file = None;
|
|
||||||
player.stop()?;
|
|
||||||
} else if state.playlist_index == state.selected_playlist_index {
|
|
||||||
// Removed currently playing track, play next one
|
|
||||||
if state.playlist_index < state.playlist.len() {
|
|
||||||
state.current_file = Some(state.playlist[state.playlist_index].clone());
|
|
||||||
if state.player_state == PlayerState::Playing {
|
|
||||||
if let Some(ref path) = state.current_file {
|
|
||||||
player.play(path)?;
|
|
||||||
player.update_metadata();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
(KeyCode::Enter, _) => {
|
(KeyCode::Enter, _) => {
|
||||||
if state.focus_playlist {
|
if state.focus_playlist {
|
||||||
// Play selected track from playlist
|
|
||||||
if state.selected_playlist_index < state.playlist.len() {
|
if state.selected_playlist_index < state.playlist.len() {
|
||||||
state.playlist_index = state.selected_playlist_index;
|
action_play_from_playlist(state, player, false)?;
|
||||||
state.current_file = Some(state.playlist[state.playlist_index].clone());
|
|
||||||
state.player_state = PlayerState::Playing;
|
|
||||||
if let Some(ref path) = state.current_file {
|
|
||||||
player.play(path)?;
|
|
||||||
player.update_metadata();
|
|
||||||
tracing::info!("Playing from playlist: {:?}", path);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
action_play_selection(state, player)?;
|
action_play_selection(state, player)?;
|
||||||
@@ -700,7 +728,7 @@ fn handle_mouse_event(state: &mut AppState, mouse: MouseEvent, title_bar_area: r
|
|||||||
let items = match menu.menu_type {
|
let items = match menu.menu_type {
|
||||||
ContextMenuType::FilePanel => 2,
|
ContextMenuType::FilePanel => 2,
|
||||||
ContextMenuType::Playlist => 2,
|
ContextMenuType::Playlist => 2,
|
||||||
ContextMenuType::TitleBar => 3,
|
ContextMenuType::TitleBar => 4,
|
||||||
};
|
};
|
||||||
let popup_width = 13;
|
let popup_width = 13;
|
||||||
let popup_height = items as u16 + 2; // +2 for borders
|
let popup_height = items as u16 + 2; // +2 for borders
|
||||||
@@ -756,63 +784,7 @@ fn handle_mouse_event(state: &mut AppState, mouse: MouseEvent, title_bar_area: r
|
|||||||
let menu_type = menu.menu_type;
|
let menu_type = menu.menu_type;
|
||||||
let selected = relative_y;
|
let selected = relative_y;
|
||||||
state.context_menu = None;
|
state.context_menu = None;
|
||||||
|
handle_context_menu_action(menu_type, selected, state, player)?;
|
||||||
match menu_type {
|
|
||||||
ContextMenuType::FilePanel => {
|
|
||||||
match selected {
|
|
||||||
0 => action_play_selection(state, player)?,
|
|
||||||
1 => state.add_to_playlist(),
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ContextMenuType::Playlist => {
|
|
||||||
match selected {
|
|
||||||
0 => {
|
|
||||||
// Remove
|
|
||||||
let was_playing_removed = state.playlist_index == state.selected_playlist_index;
|
|
||||||
state.remove_selected_playlist_item();
|
|
||||||
|
|
||||||
// Handle edge cases after removal
|
|
||||||
if state.playlist.is_empty() {
|
|
||||||
state.player_state = PlayerState::Stopped;
|
|
||||||
state.current_file = None;
|
|
||||||
player.stop()?;
|
|
||||||
} else if was_playing_removed && state.player_state == PlayerState::Playing {
|
|
||||||
state.current_file = Some(state.playlist[state.playlist_index].clone());
|
|
||||||
if let Some(ref path) = state.current_file {
|
|
||||||
player.play(path)?;
|
|
||||||
player.update_metadata();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
1 => {
|
|
||||||
// Randomise
|
|
||||||
state.shuffle_playlist();
|
|
||||||
tracing::info!("Playlist randomised from context menu (mouse)");
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ContextMenuType::TitleBar => {
|
|
||||||
match selected {
|
|
||||||
0 => {
|
|
||||||
// Stop
|
|
||||||
action_stop(state, player)?;
|
|
||||||
}
|
|
||||||
1 => {
|
|
||||||
// Toggle Loop
|
|
||||||
state.cycle_play_mode();
|
|
||||||
tracing::info!("Play mode: {:?} (mouse)", state.play_mode);
|
|
||||||
}
|
|
||||||
2 => {
|
|
||||||
// Refresh
|
|
||||||
state.show_refresh_confirm = true;
|
|
||||||
tracing::info!("Refresh requested from context menu (mouse)");
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return Ok(());
|
return Ok(());
|
||||||
} else {
|
} else {
|
||||||
@@ -992,38 +964,9 @@ fn handle_mouse_event(state: &mut AppState, mouse: MouseEvent, title_bar_area: r
|
|||||||
};
|
};
|
||||||
|
|
||||||
if is_double_click {
|
if is_double_click {
|
||||||
// Double click = play the track
|
// Double click = play the track (preserve pause state)
|
||||||
state.playlist_index = actual_track;
|
state.selected_playlist_index = actual_track;
|
||||||
state.current_file = Some(state.playlist[state.playlist_index].clone());
|
action_play_from_playlist(state, player, true)?;
|
||||||
|
|
||||||
match state.player_state {
|
|
||||||
PlayerState::Playing => {
|
|
||||||
// Keep playing
|
|
||||||
if let Some(ref path) = state.current_file {
|
|
||||||
player.play(path)?;
|
|
||||||
player.update_metadata();
|
|
||||||
tracing::info!("Jumped to track: {:?}", path);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
PlayerState::Paused => {
|
|
||||||
// Load but stay paused
|
|
||||||
if let Some(ref path) = state.current_file {
|
|
||||||
player.play(path)?;
|
|
||||||
player.update_metadata();
|
|
||||||
player.pause()?;
|
|
||||||
tracing::info!("Jumped to track (paused): {:?}", path);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
PlayerState::Stopped => {
|
|
||||||
// Start playing from clicked track
|
|
||||||
state.player_state = PlayerState::Playing;
|
|
||||||
if let Some(ref path) = state.current_file {
|
|
||||||
player.play(path)?;
|
|
||||||
player.update_metadata();
|
|
||||||
tracing::info!("Started playing track: {:?}", path);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Reset click tracking after action
|
// Reset click tracking after action
|
||||||
state.last_click_time = None;
|
state.last_click_time = None;
|
||||||
state.last_click_index = None;
|
state.last_click_index = None;
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ pub struct Player {
|
|||||||
pub audio_codec: Option<String>,
|
pub audio_codec: Option<String>,
|
||||||
pub audio_bitrate: Option<f64>,
|
pub audio_bitrate: Option<f64>,
|
||||||
pub sample_rate: Option<i64>,
|
pub sample_rate: Option<i64>,
|
||||||
|
pub cache_duration: Option<f64>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Player {
|
impl Player {
|
||||||
@@ -62,6 +63,7 @@ impl Player {
|
|||||||
audio_codec: None,
|
audio_codec: None,
|
||||||
audio_bitrate: None,
|
audio_bitrate: None,
|
||||||
sample_rate: None,
|
sample_rate: None,
|
||||||
|
cache_duration: None,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -119,6 +121,7 @@ impl Player {
|
|||||||
self.audio_codec = None;
|
self.audio_codec = None;
|
||||||
self.audio_bitrate = None;
|
self.audio_bitrate = None;
|
||||||
self.sample_rate = None;
|
self.sample_rate = None;
|
||||||
|
self.cache_duration = None;
|
||||||
|
|
||||||
// Wait for socket to be created and mpv to be ready
|
// Wait for socket to be created and mpv to be ready
|
||||||
std::thread::sleep(Duration::from_millis(800));
|
std::thread::sleep(Duration::from_millis(800));
|
||||||
@@ -303,6 +306,13 @@ impl Player {
|
|||||||
if let Some(val) = self.get_property("audio-params/samplerate") {
|
if let Some(val) = self.get_property("audio-params/samplerate") {
|
||||||
self.sample_rate = val.as_i64();
|
self.sample_rate = val.as_i64();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update cache duration (how many seconds are buffered ahead)
|
||||||
|
if let Some(val) = self.get_property("demuxer-cache-duration") {
|
||||||
|
self.cache_duration = val.as_f64();
|
||||||
|
} else {
|
||||||
|
self.cache_duration = None;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_position(&self) -> Option<f64> {
|
pub fn get_position(&self) -> Option<f64> {
|
||||||
@@ -317,6 +327,10 @@ impl Player {
|
|||||||
self.is_idle
|
self.is_idle
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn is_paused(&self) -> bool {
|
||||||
|
self.is_paused
|
||||||
|
}
|
||||||
|
|
||||||
pub fn is_process_alive(&mut self) -> bool {
|
pub fn is_process_alive(&mut self) -> bool {
|
||||||
// Check if mpv process is still running
|
// Check if mpv process is still running
|
||||||
match self.process.try_wait() {
|
match self.process.try_wait() {
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ pub fn render(frame: &mut Frame, state: &mut AppState, player: &Player) -> (Rect
|
|||||||
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)])
|
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)])
|
||||||
.split(main_chunks[1]);
|
.split(main_chunks[1]);
|
||||||
|
|
||||||
render_title_bar(frame, state, main_chunks[0]);
|
render_title_bar(frame, state, player, main_chunks[0]);
|
||||||
render_file_panel(frame, state, content_chunks[0]);
|
render_file_panel(frame, state, content_chunks[0]);
|
||||||
render_right_panel(frame, state, content_chunks[1]);
|
render_right_panel(frame, state, content_chunks[1]);
|
||||||
render_status_bar(frame, state, player, main_chunks[2]);
|
render_status_bar(frame, state, player, main_chunks[2]);
|
||||||
@@ -349,7 +349,7 @@ fn render_right_panel(frame: &mut Frame, state: &mut AppState, area: Rect) {
|
|||||||
frame.render_stateful_widget(playlist_widget, area, &mut playlist_state);
|
frame.render_stateful_widget(playlist_widget, area, &mut playlist_state);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_title_bar(frame: &mut Frame, state: &AppState, area: Rect) {
|
fn render_title_bar(frame: &mut Frame, state: &AppState, _player: &Player, area: Rect) {
|
||||||
let background_color = match state.player_state {
|
let background_color = match state.player_state {
|
||||||
PlayerState::Playing => Theme::success(), // Green for playing
|
PlayerState::Playing => Theme::success(), // Green for playing
|
||||||
PlayerState::Paused => Theme::highlight(), // Blue for paused
|
PlayerState::Paused => Theme::highlight(), // Blue for paused
|
||||||
@@ -521,7 +521,7 @@ fn render_status_bar(frame: &mut Frame, state: &AppState, player: &Player, area:
|
|||||||
left_parts.push(title.clone());
|
left_parts.push(title.clone());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Right side: Bitrate | Codec | Sample rate
|
// Right side: Bitrate | Codec | Sample rate | Cache
|
||||||
if let Some(bitrate) = player.audio_bitrate {
|
if let Some(bitrate) = player.audio_bitrate {
|
||||||
right_parts.push(format!("{:.0} kbps", bitrate));
|
right_parts.push(format!("{:.0} kbps", bitrate));
|
||||||
}
|
}
|
||||||
@@ -534,6 +534,12 @@ fn render_status_bar(frame: &mut Frame, state: &AppState, player: &Player, area:
|
|||||||
right_parts.push(format!("{} Hz", samplerate));
|
right_parts.push(format!("{} Hz", samplerate));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let Some(cache_dur) = player.cache_duration {
|
||||||
|
if cache_dur > 0.0 {
|
||||||
|
right_parts.push(format!("{:.1}s", cache_dur));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Create layout for left and right sections
|
// Create layout for left and right sections
|
||||||
let chunks = Layout::default()
|
let chunks = Layout::default()
|
||||||
.direction(Direction::Horizontal)
|
.direction(Direction::Horizontal)
|
||||||
|
|||||||
Reference in New Issue
Block a user