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
3 changes: 3 additions & 0 deletions docs/config.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ All configuration files should be placed inside the application's configuration
| `default_device` | the default device to connect to on startup if no playing device found | `spotify-player` |
| `play_icon` | the icon to indicate playing state of a Spotify item | `▶` |
| `pause_icon` | the icon to indicate pause state of a Spotify item | `▌▌` |
| `liked_icon` | the icon to indicate the liked state of a song | `♥` |
| `playback_position` | the position of the playback window | `Top` |
| `playback_window_width` | the width of the playback window | `6` |
| `cover_img_width` | the width of the cover image (`image` feature only) | `5` |
| `cover_img_length` | the length of the cover image (`image` feature only) | `9` |
Expand Down Expand Up @@ -61,6 +63,7 @@ The default `app.toml` can be found in the example [`app.toml`](../examples/app.

- An example of event that triggers a playback update is the one happening when the current track ends.
- `copy_command` is represented by a struct with two fields `command` and `args`. For example, `copy_command = { command = "xclip", args = ["-sel", "c"] }`. The copy command should read input from **standard input**.
- `playback_position` can only be either `Top` or `Bottom`.

#### Media control

Expand Down
2 changes: 2 additions & 0 deletions examples/app.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ enable_media_control = false
default_device = "spotify-player"
play_icon = "▶"
pause_icon = "▌▌"
liked_icon = "♥"
playback_position = "Top"
cover_img_length = 9
cover_img_width = 5
playback_window_width = 6
Expand Down
2 changes: 1 addition & 1 deletion spotify_player/src/command.rs
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ pub fn construct_track_actions(track: &Track, data: &DataReadGuard) -> Vec<Track
];

// check if the track is a liked track
if data.user_data.saved_tracks.iter().any(|t| t.id == track.id) {
if data.user_data.is_liked_track(track) {
actions.push(TrackAction::DeleteFromLikedTracks);
} else {
actions.push(TrackAction::AddToLikedTracks);
Expand Down
13 changes: 13 additions & 0 deletions spotify_player/src/config/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,11 @@ pub struct AppConfig {
// icon configs
pub play_icon: String,
pub pause_icon: String,
pub liked_icon: String,

// layout configs
pub playback_position: Position,

#[cfg(feature = "image")]
pub cover_img_length: usize,
#[cfg(feature = "image")]
Expand All @@ -58,6 +61,13 @@ pub struct AppConfig {
pub device: DeviceConfig,
}

#[derive(Debug, Deserialize, Clone)]
pub enum Position {
Top,
Bottom,
}
config_parser_impl!(Position);

#[derive(Debug, Deserialize, ConfigParse, Clone)]
pub struct Command {
pub command: String,
Expand Down Expand Up @@ -111,6 +121,9 @@ impl Default for AppConfig {

pause_icon: "▌▌".to_string(),
play_icon: "▶".to_string(),
liked_icon: "♥".to_string(),

playback_position: Position::Top,

#[cfg(feature = "image")]
cover_img_length: 9,
Expand Down
5 changes: 5 additions & 0 deletions spotify_player/src/state/data.rs
Original file line number Diff line number Diff line change
Expand Up @@ -77,4 +77,9 @@ impl UserData {
.collect(),
}
}

/// checks if a track is a liked track
pub fn is_liked_track(&self, track: &Track) -> bool {
self.saved_tracks.iter().any(|t| t.id == track.id)
}
}
48 changes: 32 additions & 16 deletions spotify_player/src/ui/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,30 @@ fn render_application(
Ok(())
}

fn split_rect_for_playback_window(rect: Rect, state: &SharedState) -> (Rect, Rect) {
// +2 for top/bot borders
let playback_width = (state.app_config.playback_window_width + 2) as u16;

match state.app_config.playback_position {
config::Position::Top => {
let chunks = Layout::default()
.direction(Direction::Vertical)
.constraints([Constraint::Length(playback_width), Constraint::Min(0)].as_ref())
.split(rect);

(chunks[0], chunks[1])
}
config::Position::Bottom => {
let chunks = Layout::default()
.direction(Direction::Vertical)
.constraints([Constraint::Min(0), Constraint::Length(playback_width)].as_ref())
.split(rect);

(chunks[1], chunks[0])
}
}
}

/// renders the application's main layout
fn render_main_layout(
is_active: bool,
Expand All @@ -90,25 +114,17 @@ fn render_main_layout(
ui: &mut UIStateGuard,
rect: Rect,
) -> Result<()> {
let chunks = Layout::default()
.direction(Direction::Vertical)
.constraints(
[
Constraint::Length((state.app_config.playback_window_width + 2) as u16),
Constraint::Min(0),
]
.as_ref(),
) // +2 for top/bot borders
.split(rect);
playback::render_playback_window(frame, state, ui, chunks[0])?;
let (playback_rect, main_rect) = split_rect_for_playback_window(rect, state);

playback::render_playback_window(frame, state, ui, playback_rect)?;

let page_type = ui.current_page().page_type();
match page_type {
PageType::Library => page::render_library_page(is_active, frame, state, ui, chunks[1]),
PageType::Search => page::render_search_page(is_active, frame, state, ui, chunks[1]),
PageType::Context => page::render_context_page(is_active, frame, state, ui, chunks[1]),
PageType::Browse => page::render_browse_page(is_active, frame, state, ui, chunks[1]),
PageType::Library => page::render_library_page(is_active, frame, state, ui, main_rect),
PageType::Search => page::render_search_page(is_active, frame, state, ui, main_rect),
PageType::Context => page::render_context_page(is_active, frame, state, ui, main_rect),
PageType::Browse => page::render_browse_page(is_active, frame, state, ui, main_rect),
#[cfg(feature = "lyric-finder")]
PageType::Lyric => page::render_lyric_page(is_active, frame, state, ui, chunks[1]),
PageType::Lyric => page::render_lyric_page(is_active, frame, state, ui, main_rect),
}
}
27 changes: 21 additions & 6 deletions spotify_player/src/ui/page.rs
Original file line number Diff line number Diff line change
Expand Up @@ -213,7 +213,8 @@ pub fn render_context_page(
Some(id) => id,
};

match state.data.read().caches.context.peek(&id.uri()) {
let data = state.data.read();
match data.caches.context.peek(&id.uri()) {
Some(context) => {
frame.render_widget(block, rect);

Expand All @@ -239,6 +240,7 @@ pub fn render_context_page(
frame,
state,
ui,
&data,
chunks[1],
(top_tracks, albums, related_artists),
)?;
Expand All @@ -251,6 +253,7 @@ pub fn render_context_page(
state,
ui.search_filtered_items(tracks),
ui,
&data,
)?;
}
Context::Album { tracks, .. } => {
Expand All @@ -261,6 +264,7 @@ pub fn render_context_page(
state,
ui.search_filtered_items(tracks),
ui,
&data,
)?;
}
Context::Tracks { tracks, .. } => {
Expand All @@ -271,6 +275,7 @@ pub fn render_context_page(
state,
ui.search_filtered_items(tracks),
ui,
&data,
)?;
}
}
Expand Down Expand Up @@ -518,13 +523,14 @@ fn render_artist_context_page_windows(
frame: &mut Frame,
state: &SharedState,
ui: &mut UIStateGuard,
data: &DataReadGuard,
rect: Rect,
data: (&[Track], &[Album], &[Artist]),
artist_data: (&[Track], &[Album], &[Artist]),
) -> Result<()> {
let (tracks, albums, artists) = (
ui.search_filtered_items(data.0),
ui.search_filtered_items(data.1),
ui.search_filtered_items(data.2),
ui.search_filtered_items(artist_data.0),
ui.search_filtered_items(artist_data.1),
ui.search_filtered_items(artist_data.2),
);

let focus_state = match ui.current_page() {
Expand All @@ -550,6 +556,7 @@ fn render_artist_context_page_windows(
state,
tracks,
ui,
data,
)?;

chunks[1]
Expand Down Expand Up @@ -618,6 +625,7 @@ pub fn render_track_table_window(
state: &SharedState,
tracks: Vec<&Track>,
ui: &mut UIStateGuard,
data: &DataReadGuard,
) -> Result<()> {
// get the current playing track's URI to decorate such track (if exists) in the track table
let mut playing_track_uri = "".to_string();
Expand Down Expand Up @@ -646,6 +654,11 @@ pub fn render_track_table_window(
((id + 1).to_string(), Style::default())
};
Row::new(vec![
Cell::from(if data.user_data.is_liked_track(t) {
&state.app_config.liked_icon
} else {
""
}),
Cell::from(id),
Cell::from(crate::utils::truncate_string(t.name.clone(), item_max_len)),
Cell::from(crate::utils::truncate_string(
Expand All @@ -662,6 +675,7 @@ pub fn render_track_table_window(
let track_table = Table::new(rows)
.header(
Row::new(vec![
Cell::from(""),
Cell::from("#"),
Cell::from("Title"),
Cell::from("Artists"),
Expand All @@ -672,11 +686,12 @@ pub fn render_track_table_window(
)
.block(Block::default())
.widths(&[
Constraint::Length(2),
Constraint::Length(5),
Constraint::Percentage(25),
Constraint::Percentage(25),
Constraint::Percentage(30),
Constraint::Percentage(15),
Constraint::Percentage(20),
])
.highlight_style(ui.theme.selection_style(is_active));

Expand Down
32 changes: 6 additions & 26 deletions spotify_player/src/ui/popup.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,34 +43,14 @@ pub fn render_popup(
(chunks[0], true)
}
PopupState::CommandHelp { .. } => {
let chunks = Layout::default()
.direction(Direction::Vertical)
.constraints(
[
Constraint::Length((state.app_config.playback_window_width + 2) as u16),
Constraint::Min(0),
]
.as_ref(),
)
.split(rect);

render_commands_help_popup(frame, state, ui, chunks[1]);
(chunks[0], false)
let (playback_rect, main_rect) = split_rect_for_playback_window(rect, state);
render_commands_help_popup(frame, state, ui, main_rect);
(playback_rect, false)
}
PopupState::Queue { .. } => {
let chunks = Layout::default()
.direction(Direction::Vertical)
.constraints(
[
Constraint::Length((state.app_config.playback_window_width + 2) as u16),
Constraint::Min(0),
]
.as_ref(),
)
.split(rect);

render_queue_popup(frame, state, ui, chunks[1]);
(chunks[0], false)
let (playback_rect, main_rect) = split_rect_for_playback_window(rect, state);
render_queue_popup(frame, state, ui, main_rect);
(playback_rect, false)
}
PopupState::ActionList(item, _) => {
let rect = render_list_popup(
Expand Down