Skip to content

Commit c0f064a

Browse files
authored
Add various UI improvements (#132)
Resolves #124. Resolves #128 ## Changes - supported displaying the track's liked status - allowed to configure the playback window's position
1 parent 004f9eb commit c0f064a

File tree

8 files changed

+83
-49
lines changed

8 files changed

+83
-49
lines changed

docs/config.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@ All configuration files should be placed inside the application's configuration
3333
| `default_device` | the default device to connect to on startup if no playing device found | `spotify-player` |
3434
| `play_icon` | the icon to indicate playing state of a Spotify item | `` |
3535
| `pause_icon` | the icon to indicate pause state of a Spotify item | `▌▌` |
36+
| `liked_icon` | the icon to indicate the liked state of a song | `` |
37+
| `playback_position` | the position of the playback window | `Top` |
3638
| `playback_window_width` | the width of the playback window | `6` |
3739
| `cover_img_width` | the width of the cover image (`image` feature only) | `5` |
3840
| `cover_img_length` | the length of the cover image (`image` feature only) | `9` |
@@ -61,6 +63,7 @@ The default `app.toml` can be found in the example [`app.toml`](../examples/app.
6163

6264
- An example of event that triggers a playback update is the one happening when the current track ends.
6365
- `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**.
66+
- `playback_position` can only be either `Top` or `Bottom`.
6467

6568
#### Media control
6669

examples/app.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ enable_media_control = false
1212
default_device = "spotify-player"
1313
play_icon = ""
1414
pause_icon = "▌▌"
15+
liked_icon = ""
16+
playback_position = "Top"
1517
cover_img_length = 9
1618
cover_img_width = 5
1719
playback_window_width = 6

spotify_player/src/command.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,7 @@ pub fn construct_track_actions(track: &Track, data: &DataReadGuard) -> Vec<Track
123123
];
124124

125125
// check if the track is a liked track
126-
if data.user_data.saved_tracks.iter().any(|t| t.id == track.id) {
126+
if data.user_data.is_liked_track(track) {
127127
actions.push(TrackAction::DeleteFromLikedTracks);
128128
} else {
129129
actions.push(TrackAction::AddToLikedTracks);

spotify_player/src/config/mod.rs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,11 @@ pub struct AppConfig {
4242
// icon configs
4343
pub play_icon: String,
4444
pub pause_icon: String,
45+
pub liked_icon: String,
4546

4647
// layout configs
48+
pub playback_position: Position,
49+
4750
#[cfg(feature = "image")]
4851
pub cover_img_length: usize,
4952
#[cfg(feature = "image")]
@@ -58,6 +61,13 @@ pub struct AppConfig {
5861
pub device: DeviceConfig,
5962
}
6063

64+
#[derive(Debug, Deserialize, Clone)]
65+
pub enum Position {
66+
Top,
67+
Bottom,
68+
}
69+
config_parser_impl!(Position);
70+
6171
#[derive(Debug, Deserialize, ConfigParse, Clone)]
6272
pub struct Command {
6373
pub command: String,
@@ -111,6 +121,9 @@ impl Default for AppConfig {
111121

112122
pause_icon: "▌▌".to_string(),
113123
play_icon: "▶".to_string(),
124+
liked_icon: "♥".to_string(),
125+
126+
playback_position: Position::Top,
114127

115128
#[cfg(feature = "image")]
116129
cover_img_length: 9,

spotify_player/src/state/data.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,4 +77,9 @@ impl UserData {
7777
.collect(),
7878
}
7979
}
80+
81+
/// checks if a track is a liked track
82+
pub fn is_liked_track(&self, track: &Track) -> bool {
83+
self.saved_tracks.iter().any(|t| t.id == track.id)
84+
}
8085
}

spotify_player/src/ui/mod.rs

Lines changed: 32 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,30 @@ fn render_application(
8282
Ok(())
8383
}
8484

85+
fn split_rect_for_playback_window(rect: Rect, state: &SharedState) -> (Rect, Rect) {
86+
// +2 for top/bot borders
87+
let playback_width = (state.app_config.playback_window_width + 2) as u16;
88+
89+
match state.app_config.playback_position {
90+
config::Position::Top => {
91+
let chunks = Layout::default()
92+
.direction(Direction::Vertical)
93+
.constraints([Constraint::Length(playback_width), Constraint::Min(0)].as_ref())
94+
.split(rect);
95+
96+
(chunks[0], chunks[1])
97+
}
98+
config::Position::Bottom => {
99+
let chunks = Layout::default()
100+
.direction(Direction::Vertical)
101+
.constraints([Constraint::Min(0), Constraint::Length(playback_width)].as_ref())
102+
.split(rect);
103+
104+
(chunks[1], chunks[0])
105+
}
106+
}
107+
}
108+
85109
/// renders the application's main layout
86110
fn render_main_layout(
87111
is_active: bool,
@@ -90,25 +114,17 @@ fn render_main_layout(
90114
ui: &mut UIStateGuard,
91115
rect: Rect,
92116
) -> Result<()> {
93-
let chunks = Layout::default()
94-
.direction(Direction::Vertical)
95-
.constraints(
96-
[
97-
Constraint::Length((state.app_config.playback_window_width + 2) as u16),
98-
Constraint::Min(0),
99-
]
100-
.as_ref(),
101-
) // +2 for top/bot borders
102-
.split(rect);
103-
playback::render_playback_window(frame, state, ui, chunks[0])?;
117+
let (playback_rect, main_rect) = split_rect_for_playback_window(rect, state);
118+
119+
playback::render_playback_window(frame, state, ui, playback_rect)?;
104120

105121
let page_type = ui.current_page().page_type();
106122
match page_type {
107-
PageType::Library => page::render_library_page(is_active, frame, state, ui, chunks[1]),
108-
PageType::Search => page::render_search_page(is_active, frame, state, ui, chunks[1]),
109-
PageType::Context => page::render_context_page(is_active, frame, state, ui, chunks[1]),
110-
PageType::Browse => page::render_browse_page(is_active, frame, state, ui, chunks[1]),
123+
PageType::Library => page::render_library_page(is_active, frame, state, ui, main_rect),
124+
PageType::Search => page::render_search_page(is_active, frame, state, ui, main_rect),
125+
PageType::Context => page::render_context_page(is_active, frame, state, ui, main_rect),
126+
PageType::Browse => page::render_browse_page(is_active, frame, state, ui, main_rect),
111127
#[cfg(feature = "lyric-finder")]
112-
PageType::Lyric => page::render_lyric_page(is_active, frame, state, ui, chunks[1]),
128+
PageType::Lyric => page::render_lyric_page(is_active, frame, state, ui, main_rect),
113129
}
114130
}

spotify_player/src/ui/page.rs

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -213,7 +213,8 @@ pub fn render_context_page(
213213
Some(id) => id,
214214
};
215215

216-
match state.data.read().caches.context.peek(&id.uri()) {
216+
let data = state.data.read();
217+
match data.caches.context.peek(&id.uri()) {
217218
Some(context) => {
218219
frame.render_widget(block, rect);
219220

@@ -239,6 +240,7 @@ pub fn render_context_page(
239240
frame,
240241
state,
241242
ui,
243+
&data,
242244
chunks[1],
243245
(top_tracks, albums, related_artists),
244246
)?;
@@ -251,6 +253,7 @@ pub fn render_context_page(
251253
state,
252254
ui.search_filtered_items(tracks),
253255
ui,
256+
&data,
254257
)?;
255258
}
256259
Context::Album { tracks, .. } => {
@@ -261,6 +264,7 @@ pub fn render_context_page(
261264
state,
262265
ui.search_filtered_items(tracks),
263266
ui,
267+
&data,
264268
)?;
265269
}
266270
Context::Tracks { tracks, .. } => {
@@ -271,6 +275,7 @@ pub fn render_context_page(
271275
state,
272276
ui.search_filtered_items(tracks),
273277
ui,
278+
&data,
274279
)?;
275280
}
276281
}
@@ -518,13 +523,14 @@ fn render_artist_context_page_windows(
518523
frame: &mut Frame,
519524
state: &SharedState,
520525
ui: &mut UIStateGuard,
526+
data: &DataReadGuard,
521527
rect: Rect,
522-
data: (&[Track], &[Album], &[Artist]),
528+
artist_data: (&[Track], &[Album], &[Artist]),
523529
) -> Result<()> {
524530
let (tracks, albums, artists) = (
525-
ui.search_filtered_items(data.0),
526-
ui.search_filtered_items(data.1),
527-
ui.search_filtered_items(data.2),
531+
ui.search_filtered_items(artist_data.0),
532+
ui.search_filtered_items(artist_data.1),
533+
ui.search_filtered_items(artist_data.2),
528534
);
529535

530536
let focus_state = match ui.current_page() {
@@ -550,6 +556,7 @@ fn render_artist_context_page_windows(
550556
state,
551557
tracks,
552558
ui,
559+
data,
553560
)?;
554561

555562
chunks[1]
@@ -618,6 +625,7 @@ pub fn render_track_table_window(
618625
state: &SharedState,
619626
tracks: Vec<&Track>,
620627
ui: &mut UIStateGuard,
628+
data: &DataReadGuard,
621629
) -> Result<()> {
622630
// get the current playing track's URI to decorate such track (if exists) in the track table
623631
let mut playing_track_uri = "".to_string();
@@ -646,6 +654,11 @@ pub fn render_track_table_window(
646654
((id + 1).to_string(), Style::default())
647655
};
648656
Row::new(vec![
657+
Cell::from(if data.user_data.is_liked_track(t) {
658+
&state.app_config.liked_icon
659+
} else {
660+
""
661+
}),
649662
Cell::from(id),
650663
Cell::from(crate::utils::truncate_string(t.name.clone(), item_max_len)),
651664
Cell::from(crate::utils::truncate_string(
@@ -662,6 +675,7 @@ pub fn render_track_table_window(
662675
let track_table = Table::new(rows)
663676
.header(
664677
Row::new(vec![
678+
Cell::from(""),
665679
Cell::from("#"),
666680
Cell::from("Title"),
667681
Cell::from("Artists"),
@@ -672,11 +686,12 @@ pub fn render_track_table_window(
672686
)
673687
.block(Block::default())
674688
.widths(&[
689+
Constraint::Length(2),
675690
Constraint::Length(5),
676691
Constraint::Percentage(25),
677692
Constraint::Percentage(25),
678693
Constraint::Percentage(30),
679-
Constraint::Percentage(15),
694+
Constraint::Percentage(20),
680695
])
681696
.highlight_style(ui.theme.selection_style(is_active));
682697

spotify_player/src/ui/popup.rs

Lines changed: 6 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -43,34 +43,14 @@ pub fn render_popup(
4343
(chunks[0], true)
4444
}
4545
PopupState::CommandHelp { .. } => {
46-
let chunks = Layout::default()
47-
.direction(Direction::Vertical)
48-
.constraints(
49-
[
50-
Constraint::Length((state.app_config.playback_window_width + 2) as u16),
51-
Constraint::Min(0),
52-
]
53-
.as_ref(),
54-
)
55-
.split(rect);
56-
57-
render_commands_help_popup(frame, state, ui, chunks[1]);
58-
(chunks[0], false)
46+
let (playback_rect, main_rect) = split_rect_for_playback_window(rect, state);
47+
render_commands_help_popup(frame, state, ui, main_rect);
48+
(playback_rect, false)
5949
}
6050
PopupState::Queue { .. } => {
61-
let chunks = Layout::default()
62-
.direction(Direction::Vertical)
63-
.constraints(
64-
[
65-
Constraint::Length((state.app_config.playback_window_width + 2) as u16),
66-
Constraint::Min(0),
67-
]
68-
.as_ref(),
69-
)
70-
.split(rect);
71-
72-
render_queue_popup(frame, state, ui, chunks[1]);
73-
(chunks[0], false)
51+
let (playback_rect, main_rect) = split_rect_for_playback_window(rect, state);
52+
render_queue_popup(frame, state, ui, main_rect);
53+
(playback_rect, false)
7454
}
7555
PopupState::ActionList(item, _) => {
7656
let rect = render_list_popup(

0 commit comments

Comments
 (0)