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
52 changes: 49 additions & 3 deletions src/view.rs
Original file line number Diff line number Diff line change
Expand Up @@ -296,9 +296,7 @@ pub fn is_animated(state: &HouseholdState, ui: &UiState) -> bool {
let advancing = group
.and_then(|group| group.playback)
.is_some_and(|playback| playback.is_advancing());
// The playback menu shows the time left on the sleep timer.
let counting_down = matches!(ui.mode, Mode::PlaybackMenu { .. })
&& group.is_some_and(|group| group.sleep_timer.is_some());
let counting_down = group.is_some_and(|group| group.sleep_timer.is_some());
showing_error || advancing || counting_down
}

Expand Down Expand Up @@ -754,6 +752,54 @@ mod tests {
assert_eq!(slider_start("Office Move"), group);
}

#[test]
fn sleep_timer_shows_beside_the_controls_when_there_is_room() {
use ratatui::{Terminal, backend::TestBackend};

let controls_row = |state: &HouseholdState, width| {
let mut ui = UiState::default();
ui.sync(state);
let mut terminal = Terminal::new(TestBackend::new(width, 30)).unwrap();
let (mut artwork, _) = Artwork::new(ratatui_image::picker::Picker::halfblocks(), None);
terminal
.draw(|f| {
render_ui(
f,
state,
&mut ui,
&mut artwork,
&UiConfig::default(),
&Theme::TERMINAL,
);
})
.unwrap();
let buffer = terminal.backend().buffer();
(0..buffer.area.height)
.map(|y| {
(0..buffer.area.width)
.map(|x| buffer[(x, y)].symbol())
.collect::<String>()
})
.find(|row| row.contains("shuffle"))
.unwrap()
};
let without = household();
let mut with = household();
with.groups[0].sleep_timer =
Some(std::time::Instant::now() + Duration::from_secs(20 * 60 + 30));

let row = controls_row(&with, 80);
let timer = row.find("🌙").expect(&row);
assert!(row[timer..].contains("20:2"), "{row}");
// The controls don't move to make room for it.
let plain = controls_row(&without, 80);
assert_eq!(row[..timer].trim_end(), plain.trim_end(), "{row}");

// There is no room for it beside the controls.
let narrow = controls_row(&with, 44);
assert!(!narrow.contains("🌙"), "{narrow}");
}

#[test]
fn panel_scrolls_as_little_as_possible() {
let mut highlight = PanelHighlight::new(0);
Expand Down
41 changes: 41 additions & 0 deletions src/view/now_playing.rs
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,11 @@ pub(super) fn render_now_playing(
// icons), and the progress bar with the slider.
let controls = controls_line(group, symbols, theme);
let volume_width = volume_width(&controls);
let controls_width = u16::try_from(controls.width()).unwrap_or(u16::MAX);
frame.render_widget(Paragraph::new(controls).centered(), controls_area);
if let Some(end) = group.sleep_timer {
render_sleep_timer(end, symbols, theme, frame, controls_area, controls_width);
}
let slider_area = render_volume(
group,
symbols,
Expand Down Expand Up @@ -450,6 +454,43 @@ pub(super) fn volume_text(volume: Option<u8>) -> String {
volume.map_or_else(|| " --".to_owned(), |v| format!("{v:>3}"))
}

/// The time left on a sleep timer running out at `end`, to the right of the controls, which are
/// `controls_width` wide in the middle of `area`, if there is room for it.
fn render_sleep_timer(
end: Instant,
symbols: &Symbols,
theme: &Theme,
frame: &mut Frame,
area: Rect,
controls_width: u16,
) {
// As far from the controls as they are from each other
const GAP: u16 = 3;
let timer = Line::styled(
format!("{} {}", symbols.sleep_timer, sleep_timer_text(end)),
Style::default().fg(theme.on),
);
let width = u16::try_from(timer.width()).unwrap_or(u16::MAX);
// Where `Paragraph::centered` puts the controls
let x = area.x + area.width.saturating_sub(controls_width) / 2 + controls_width + GAP;
let timer_area = Rect { x, width, ..area }.intersection(area);
if timer_area.width == width {
frame.render_widget(timer, timer_area);
}
}

/// The time left on a sleep timer running out at `end`: hours, minutes and seconds, or minutes
/// and seconds if under an hour.
pub(super) fn sleep_timer_text(end: Instant) -> String {
let secs = end.saturating_duration_since(Instant::now()).as_secs();
let (hours, minutes, seconds) = (secs / 3600, secs / 60 % 60, secs % 60);
if hours > 0 {
format!("{hours}:{minutes:02}:{seconds:02}")
} else {
format!("{minutes}:{seconds:02}")
}
}

fn format_duration(secs: u32) -> String {
let minutes = secs / 60;
let seconds = secs % 60;
Expand Down
20 changes: 2 additions & 18 deletions src/view/popup.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
//! The popups shown in the middle of the screen.

use std::time::{Duration, Instant};

use ratatui::{
Frame,
layout::{Constraint, Layout, Rect},
Expand All @@ -16,7 +14,7 @@ use crate::symbols::Symbols;
use crate::theme::{self, THEMES, Theme};

use super::input::{Action, supports};
use super::now_playing::{volume_slider, volume_text};
use super::now_playing::{sleep_timer_text, volume_slider, volume_text};
use super::panel::speaker_name;
use super::{PlaybackItem, SLEEP_TIMER_MINUTES};

Expand Down Expand Up @@ -169,10 +167,7 @@ fn sleep_timer_line(group: &GroupState, symbols: &Symbols, theme: &Theme) -> Lin
.saturating_sub(Span::raw(symbols.sleep_timer).width());
let value = match group.sleep_timer {
Some(end) => Span::styled(
format!(
"{} left",
time_left(end.saturating_duration_since(Instant::now()))
),
format!("{} left", sleep_timer_text(end)),
Style::default().fg(theme.name),
),
None => Span::styled("off", Style::default().fg(theme.secondary)),
Expand All @@ -185,17 +180,6 @@ fn sleep_timer_line(group: &GroupState, symbols: &Symbols, theme: &Theme) -> Lin
])
}

/// `time` as hours, minutes and seconds, or minutes and seconds if under an hour.
fn time_left(time: Duration) -> String {
let secs = time.as_secs();
let (hours, minutes, seconds) = (secs / 3600, secs / 60 % 60, secs % 60);
if hours > 0 {
format!("{hours}:{minutes:02}:{seconds:02}")
} else {
format!("{minutes}:{seconds:02}")
}
}

/// A popup listing what the sleep timer can be set to, with the choice at `highlighted`
/// highlighted.
pub(super) fn render_sleep_timer_picker(theme: &Theme, highlighted: usize, frame: &mut Frame) {
Expand Down
Loading