Skip to content

Commit 2968bdb

Browse files
author
Ross
committed
Add repeat and shuffle mode
1 parent b86d727 commit 2968bdb

5 files changed

Lines changed: 106 additions & 30 deletions

File tree

.github/workflows/build.yaml

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@ jobs:
8787
- name: Install Rust toolchain
8888
uses: dtolnay/rust-toolchain@stable
8989
with:
90+
toolchain: stable
9091
targets: ${{ matrix.target }}
9192

9293
- name: Add Dependency
@@ -122,6 +123,6 @@ jobs:
122123
uses: softprops/action-gh-release@v2
123124
with:
124125
files: ${{ env.TARBALL_NAME }}
125-
generate_release_notes: true
126-
env:
127-
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
126+
# generate_release_notes: true
127+
# env:
128+
# GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

src/app.rs

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,17 @@ pub enum VolumeDirection {
6868
Down,
6969
}
7070

71+
#[derive(Debug, Clone, PartialEq, Eq)]
72+
pub enum RepeatMode {
73+
None,
74+
One,
75+
All,
76+
}
77+
#[derive(Debug, Clone, PartialEq, Eq)]
78+
pub enum ShuffleMode {
79+
Off,
80+
On,
81+
}
7182
pub struct TabSelection<T> {
7283
pub index: usize,
7384
pub state: ListState,
@@ -182,6 +193,11 @@ pub struct App {
182193
pub widget_notification: Option<(String, std::time::Instant)>,
183194
pub w_notification_duration: std::time::Duration,
184195
pub last_search_keystroke: Option<std::time::Instant>,
196+
// Shuffle and repeat
197+
pub on_repeat: RepeatMode,
198+
pub shuffle_mode: ShuffleMode,
199+
pub shuffle_order: Vec<usize>,
200+
pub shuffle_position: usize,
185201
// TabSelection
186202
pub queue_tab: TabSelection<Track>,
187203
pub tracks_tab: TabSelection<Track>,
@@ -197,10 +213,6 @@ pub struct App {
197213
pub search_query: String,
198214
pub search_engine: SearchEngine,
199215
pub is_searching: bool,
200-
// player status
201-
pub on_repeat: bool,
202-
// Need to work on the logic to allow shuffling
203-
pub _on_shuffle: bool,
204216
pub cover_art_protocol: Option<StatefulProtocol>,
205217
}
206218

@@ -301,8 +313,10 @@ impl App {
301313
search_query: String::new(),
302314
search_engine,
303315
is_searching: false,
304-
on_repeat: false,
305-
_on_shuffle: false,
316+
on_repeat: RepeatMode::None,
317+
shuffle_mode: ShuffleMode::Off,
318+
shuffle_order: Vec::new(),
319+
shuffle_position: 0,
306320
cover_art_protocol: None,
307321
};
308322

src/app/playback.rs

Lines changed: 68 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
use anyhow::Result;
22
use futures::future;
33
use mpris_server::{Metadata, Property};
4+
use rand::seq::SliceRandom;
45

56
use crate::{
6-
app::{ActiveSection, ActiveTab, AppError, Track, VolumeDirection},
7+
app::{ActiveSection, ActiveTab, AppError, RepeatMode, ShuffleMode, Track, VolumeDirection},
78
mpris_handler::track_to_metadata,
89
};
910

@@ -59,14 +60,61 @@ impl App {
5960
self.subsonic_client.scrobble(&track, false).await?;
6061
Ok(())
6162
}
63+
pub fn enable_shuffle(&mut self) {
64+
if self.queue_tab.data.is_empty() {
65+
return;
66+
}
67+
let mut rng = rand::thread_rng();
68+
let mut indices: Vec<usize> = (0..self.queue_tab.len()).collect();
69+
indices.shuffle(&mut rng);
70+
// Put current_track as first track
71+
if let Some(pos) = indices.iter().position(|&i| i == self.playing_index) {
72+
indices.swap(0, pos)
73+
};
74+
self.shuffle_order = indices;
75+
self.shuffle_position = 0;
76+
self.shuffle_mode = ShuffleMode::On;
77+
}
78+
pub fn disable_shuffle(&mut self) {
79+
self.shuffle_mode = ShuffleMode::Off;
80+
self.shuffle_order.clear();
81+
}
82+
pub fn toggle_shuffle(&mut self) {
83+
match self.shuffle_mode {
84+
ShuffleMode::Off => self.enable_shuffle(),
85+
ShuffleMode::On => self.disable_shuffle(),
86+
}
87+
}
88+
pub fn toggle_repeat(&mut self) {
89+
self.on_repeat = match self.on_repeat {
90+
RepeatMode::None => RepeatMode::One,
91+
RepeatMode::One => RepeatMode::All,
92+
RepeatMode::All => RepeatMode::None,
93+
}
94+
}
6295
pub async fn play_next(&mut self) -> Result<(), AppError> {
6396
if self.current_track.is_none() {
6497
return Err(AppError::NoTrackLoaded);
6598
}
99+
if self.shuffle_mode == ShuffleMode::On && !self.shuffle_order.is_empty() {
100+
self.shuffle_position = (self.shuffle_position + 1) % self.shuffle_order.len();
101+
let idx = self.shuffle_order[self.shuffle_position];
102+
self.play_from_queue(idx).await?;
103+
return Ok(());
104+
}
66105
if self.queue_tab.data.is_empty() {
67106
return Err(AppError::EmptyQueue);
68107
} else if self.playing_index < self.queue_tab.data.len() - 1 {
69-
self.play_from_queue(self.playing_index + 1).await?;
108+
match self.on_repeat {
109+
RepeatMode::One => {
110+
self.play_selected(self.playing_index).await?;
111+
}
112+
RepeatMode::None => {
113+
self.playing_index += 1;
114+
self.play_from_queue(self.playing_index).await?;
115+
}
116+
RepeatMode::All => {}
117+
}
70118
} else {
71119
self.player.lock().await.stop()?;
72120
self.is_playing = false;
@@ -156,15 +204,24 @@ impl App {
156204
self.subsonic_client
157205
.scrobble(self.current_track.as_ref().unwrap(), true)
158206
.await?;
159-
if self.on_repeat {
160-
self.play_selected(self.playing_index).await?;
161-
} else if self.playing_index < self.queue_tab.data.len().saturating_sub(1) {
162-
self.play_next().await?;
163-
} else {
164-
self.is_playing = false;
165-
self.current_track = None;
166-
self.metadata = Metadata::default();
167-
self.sync_mpris().await;
207+
match self.on_repeat {
208+
RepeatMode::One => {
209+
self.play_selected(self.playing_index).await?;
210+
}
211+
RepeatMode::All => {
212+
let next = (self.playing_index + 1) % self.queue_tab.len();
213+
self.play_from_queue(next).await?;
214+
}
215+
RepeatMode::None => {
216+
if self.playing_index < self.queue_tab.data.len().saturating_sub(1) {
217+
self.play_next().await?;
218+
} else {
219+
self.is_playing = false;
220+
self.current_track = None;
221+
self.metadata = Metadata::default();
222+
self.sync_mpris().await;
223+
}
224+
}
168225
}
169226
Ok(())
170227
}

src/main.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,8 +73,12 @@ async fn run_app<B: Backend>(terminal: &mut Terminal<B>, mut app: App) -> Result
7373
KeyCode::Char('j') | KeyCode::Down => app.next_item_in_tab(),
7474
KeyCode::Enter => app.play_selected(app.find_selected()).await?,
7575
KeyCode::Left => app.seek_backward().await?,
76+
KeyCode::Char('r') if key.modifiers.contains(KeyModifiers::SHIFT) => {
77+
app.refresh_library().await?
78+
}
79+
KeyCode::Char('r') => app.toggle_repeat(),
80+
KeyCode::Char('S') => app.toggle_shuffle(),
7681
KeyCode::Right => app.seek_forward().await?,
77-
KeyCode::Char('r') => app.refresh_library().await?,
7882
KeyCode::Char('a') => app._add_to_queue().await?,
7983
KeyCode::Char('+') => app.adjust_volume(app::VolumeDirection::Up).await?,
8084
KeyCode::Char('-') => app.adjust_volume(app::VolumeDirection::Down).await?,

src/ui.rs

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
use crate::{
2-
app::{ActiveSection, ActiveTab, App, InputMode, Track},
2+
app::{ActiveSection, ActiveTab, App, InputMode, RepeatMode, ShuffleMode, Track},
33
theme::ResolvedTheme,
44
};
55
use ratatui::{
@@ -198,15 +198,15 @@ fn draw_track_info(f: &mut Frame, app: &App, track: &Track, area: Rect, theme: &
198198
} else {
199199
Color::LightYellow
200200
};
201-
let repeat_indicator = if app.on_repeat {
202-
Span::styled("repeat: on", Style::default().fg(theme.accent))
203-
} else {
204-
Span::styled("repeat: off", Style::default().fg(theme.muted_color))
201+
let repeat_indicator = match app.on_repeat {
202+
RepeatMode::None => Span::styled("repeat: off", Style::default().fg(theme.accent)),
203+
RepeatMode::One =>
204+
Span::styled("repeat: one", Style::default().fg(theme.muted_color)),
205+
RepeatMode::All => Span::styled("repeat: all", Style::default().fg(theme.muted_color))
205206
};
206-
let shuffle_indicator = if app._on_shuffle {
207-
Span::styled("shuffle: on", Style::default().fg(theme.accent))
208-
} else {
209-
Span::styled("shuffle: off", Style::default().fg(theme.muted_color))
207+
let shuffle_indicator = match app.shuffle_mode {
208+
ShuffleMode::On => Span::styled("shuffle: on", Style::default().fg(theme.accent)),
209+
ShuffleMode::Off =>Span::styled("shuffle: off", Style::default().fg(theme.muted_color))
210210
};
211211
let info_lines = vec![
212212
Line::from(vec![

0 commit comments

Comments
 (0)