Compare commits
3
Commits
0aaf6be529
...
c0c1969332
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c0c1969332 | ||
|
|
d155091eed | ||
|
|
b553a94875 |
+232
-76
@@ -237,15 +237,15 @@ pub enum DividerKind {
|
|||||||
/// Horizontal divider between the main row and the Chat dock (resizes the
|
/// Horizontal divider between the main row and the Chat dock (resizes the
|
||||||
/// Chat dock height).
|
/// Chat dock height).
|
||||||
Chat,
|
Chat,
|
||||||
/// Horizontal divider between Chat and the standalone Playlist card in the
|
|
||||||
/// 3-column layout (resizes the Playlist card height).
|
|
||||||
ThreeColPlaylist,
|
|
||||||
/// Vertical divider between Chat and Controls in the 3-column layout (resizes
|
/// Vertical divider between Chat and Controls in the 3-column layout (resizes
|
||||||
/// the Controls panel width).
|
/// the Controls panel width).
|
||||||
Controls,
|
Controls,
|
||||||
/// Vertical divider on the left edge of the Chat drawer (resizes the drawer
|
/// Vertical divider on the left edge of the Chat drawer (resizes the drawer
|
||||||
/// width) in the drawer layout.
|
/// width) in the drawer layout.
|
||||||
ChatDrawer,
|
ChatDrawer,
|
||||||
|
/// Vertical divider on the left edge of the Playlist drawer (resizes the
|
||||||
|
/// drawer width).
|
||||||
|
PlaylistDrawer,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
@@ -264,11 +264,6 @@ const CHAT_MIN_H: f32 = 110.0;
|
|||||||
/// Minimum height reserved above the Chat dock (header + main row) when resizing
|
/// Minimum height reserved above the Chat dock (header + main row) when resizing
|
||||||
/// the dock (px).
|
/// the dock (px).
|
||||||
const ABOVE_CHAT_MIN_H: f32 = 300.0;
|
const ABOVE_CHAT_MIN_H: f32 = 300.0;
|
||||||
/// Minimum height of the standalone Playlist card in the 3-column layout (px).
|
|
||||||
const THREECOL_PLAYLIST_MIN_H: f32 = 150.0;
|
|
||||||
/// Minimum height reserved for the Chat above the Playlist card in the 3-column
|
|
||||||
/// layout when resizing the card (px).
|
|
||||||
const THREECOL_CHAT_MIN_H: f32 = 160.0;
|
|
||||||
/// Thickness of a draggable divider (px).
|
/// Thickness of a draggable divider (px).
|
||||||
const DIVIDER_THICKNESS: f32 = 8.0;
|
const DIVIDER_THICKNESS: f32 = 8.0;
|
||||||
/// Upper bound for waiting on orderly core shutdown before letting the window exit.
|
/// Upper bound for waiting on orderly core shutdown before letting the window exit.
|
||||||
@@ -290,13 +285,6 @@ fn clamp_chat_height(height: f32, window_h: f32) -> f32 {
|
|||||||
height.clamp(CHAT_MIN_H, max)
|
height.clamp(CHAT_MIN_H, max)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Clamp the 3-column Playlist card height so neither it nor the Chat above it
|
|
||||||
/// drops below its minimum, given the current window height.
|
|
||||||
fn clamp_threecol_playlist_height(height: f32, window_h: f32) -> f32 {
|
|
||||||
let max = (window_h - THREECOL_CHAT_MIN_H).max(THREECOL_PLAYLIST_MIN_H);
|
|
||||||
height.clamp(THREECOL_PLAYLIST_MIN_H, max)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Minimum width of the Chat column / drawer (px).
|
/// Minimum width of the Chat column / drawer (px).
|
||||||
const CHAT_MIN_W: f32 = 200.0;
|
const CHAT_MIN_W: f32 = 200.0;
|
||||||
|
|
||||||
@@ -315,6 +303,13 @@ fn clamp_chat_drawer_width(width: f32, window_w: f32) -> f32 {
|
|||||||
width.clamp(CHAT_MIN_W, max)
|
width.clamp(CHAT_MIN_W, max)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Clamp the Playlist drawer width so neither it nor the room body drops below
|
||||||
|
/// its minimum, given the current window width.
|
||||||
|
fn clamp_playlist_drawer_width(width: f32, window_w: f32) -> f32 {
|
||||||
|
let max = (window_w - PARTICIPANTS_MIN_W - CONTROLS_MIN_W).max(CHAT_MIN_W);
|
||||||
|
width.clamp(CHAT_MIN_W, max)
|
||||||
|
}
|
||||||
|
|
||||||
#[allow(clippy::large_enum_variant)]
|
#[allow(clippy::large_enum_variant)]
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub enum AppMessage {
|
pub enum AppMessage {
|
||||||
@@ -444,6 +439,10 @@ pub enum AppMessage {
|
|||||||
MusicSetVolume(f32),
|
MusicSetVolume(f32),
|
||||||
/// Set the tuned-in source's music playback volume (and persist it).
|
/// Set the tuned-in source's music playback volume (and persist it).
|
||||||
MusicSetSourceVolume(f32),
|
MusicSetSourceVolume(f32),
|
||||||
|
/// Show / hide the room now-playing player bar (persisted).
|
||||||
|
TogglePlayerBar,
|
||||||
|
/// Open / close the full playlist drawer.
|
||||||
|
TogglePlaylistDrawer,
|
||||||
/// Remove the playlist track at this index.
|
/// Remove the playlist track at this index.
|
||||||
MusicRemove(usize),
|
MusicRemove(usize),
|
||||||
/// Move a personal playlist track one slot up/down.
|
/// Move a personal playlist track one slot up/down.
|
||||||
@@ -754,6 +753,8 @@ pub struct AppState {
|
|||||||
clock_skew_warning: Option<ClockSkewBanner>,
|
clock_skew_warning: Option<ClockSkewBanner>,
|
||||||
/// Whether the Chat drawer is open (drawer layout only).
|
/// Whether the Chat drawer is open (drawer layout only).
|
||||||
drawer_chat_open: bool,
|
drawer_chat_open: bool,
|
||||||
|
/// Whether the Playlist drawer is open beside the room body.
|
||||||
|
playlist_drawer_open: bool,
|
||||||
/// Raw mic level (normalized RMS, `0.0..=1.0`) for the settings meter.
|
/// Raw mic level (normalized RMS, `0.0..=1.0`) for the settings meter.
|
||||||
mic_level: f32,
|
mic_level: f32,
|
||||||
/// Whether the standalone (off-call) mic test stream is running.
|
/// Whether the standalone (off-call) mic test stream is running.
|
||||||
@@ -871,10 +872,10 @@ impl Default for AppState {
|
|||||||
config.participants_width =
|
config.participants_width =
|
||||||
clamp_participants_width(config.participants_width, ww);
|
clamp_participants_width(config.participants_width, ww);
|
||||||
config.chat_height = clamp_chat_height(config.chat_height, wh);
|
config.chat_height = clamp_chat_height(config.chat_height, wh);
|
||||||
config.threecol_playlist_height =
|
|
||||||
clamp_threecol_playlist_height(config.threecol_playlist_height, wh);
|
|
||||||
config.controls_width = clamp_controls_width(config.controls_width, ww);
|
config.controls_width = clamp_controls_width(config.controls_width, ww);
|
||||||
config.chat_drawer_width = clamp_chat_drawer_width(config.chat_drawer_width, ww);
|
config.chat_drawer_width = clamp_chat_drawer_width(config.chat_drawer_width, ww);
|
||||||
|
config.playlist_drawer_width =
|
||||||
|
clamp_playlist_drawer_width(config.playlist_drawer_width, ww);
|
||||||
notify::set_enabled(config.notifications_enabled);
|
notify::set_enabled(config.notifications_enabled);
|
||||||
for sound in Sound::ALL {
|
for sound in Sound::ALL {
|
||||||
notify::set_sound_enabled(sound, config.sound_enabled(sound));
|
notify::set_sound_enabled(sound, config.sound_enabled(sound));
|
||||||
@@ -1003,6 +1004,7 @@ impl Default for AppState {
|
|||||||
share_app_audio_supported: true,
|
share_app_audio_supported: true,
|
||||||
clock_skew_warning: None,
|
clock_skew_warning: None,
|
||||||
drawer_chat_open: false,
|
drawer_chat_open: false,
|
||||||
|
playlist_drawer_open: false,
|
||||||
mic_level: 0.0,
|
mic_level: 0.0,
|
||||||
mic_test_active: false,
|
mic_test_active: false,
|
||||||
connecting: HashSet::new(),
|
connecting: HashSet::new(),
|
||||||
@@ -2056,14 +2058,6 @@ fn update(state: &mut AppState, message: AppMessage) -> Task<AppMessage> {
|
|||||||
state.window_size.height,
|
state.window_size.height,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
DividerKind::ThreeColPlaylist => {
|
|
||||||
// The Playlist card sits at the bottom of the middle column; dragging the
|
|
||||||
// divider down (positive delta) gives Chat more room and shrinks the card.
|
|
||||||
state.config.threecol_playlist_height = clamp_threecol_playlist_height(
|
|
||||||
state.config.threecol_playlist_height - delta,
|
|
||||||
state.window_size.height,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
DividerKind::Controls => {
|
DividerKind::Controls => {
|
||||||
// Controls sits on the right; dragging the divider right (positive
|
// Controls sits on the right; dragging the divider right (positive
|
||||||
// delta) gives Chat more room and shrinks Controls.
|
// delta) gives Chat more room and shrinks Controls.
|
||||||
@@ -2080,6 +2074,14 @@ fn update(state: &mut AppState, message: AppMessage) -> Task<AppMessage> {
|
|||||||
state.window_size.width,
|
state.window_size.width,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
DividerKind::PlaylistDrawer => {
|
||||||
|
// The drawer sits on the right; dragging its left-edge divider
|
||||||
|
// left (negative delta) widens the drawer.
|
||||||
|
state.config.playlist_drawer_width = clamp_playlist_drawer_width(
|
||||||
|
state.config.playlist_drawer_width - delta,
|
||||||
|
state.window_size.width,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
AppMessage::OpenLayoutPicker => {
|
AppMessage::OpenLayoutPicker => {
|
||||||
@@ -2350,6 +2352,16 @@ fn update(state: &mut AppState, message: AppMessage) -> Task<AppMessage> {
|
|||||||
AppMessage::ToggleDrawerChat => {
|
AppMessage::ToggleDrawerChat => {
|
||||||
state.drawer_chat_open = !state.drawer_chat_open;
|
state.drawer_chat_open = !state.drawer_chat_open;
|
||||||
}
|
}
|
||||||
|
AppMessage::TogglePlayerBar => {
|
||||||
|
state.config.show_player_bar = !state.config.show_player_bar;
|
||||||
|
if !state.config.show_player_bar {
|
||||||
|
state.playlist_drawer_open = false;
|
||||||
|
}
|
||||||
|
state.config.save();
|
||||||
|
}
|
||||||
|
AppMessage::TogglePlaylistDrawer => {
|
||||||
|
state.playlist_drawer_open = !state.playlist_drawer_open;
|
||||||
|
}
|
||||||
AppMessage::ChatSubmit => {
|
AppMessage::ChatSubmit => {
|
||||||
let text = sanitize_chat(&state.chat_input);
|
let text = sanitize_chat(&state.chat_input);
|
||||||
if !text.is_empty() {
|
if !text.is_empty() {
|
||||||
@@ -2783,12 +2795,12 @@ fn update(state: &mut AppState, message: AppMessage) -> Task<AppMessage> {
|
|||||||
clamp_participants_width(state.config.participants_width, size.width);
|
clamp_participants_width(state.config.participants_width, size.width);
|
||||||
state.config.chat_height =
|
state.config.chat_height =
|
||||||
clamp_chat_height(state.config.chat_height, size.height);
|
clamp_chat_height(state.config.chat_height, size.height);
|
||||||
state.config.threecol_playlist_height =
|
|
||||||
clamp_threecol_playlist_height(state.config.threecol_playlist_height, size.height);
|
|
||||||
state.config.controls_width =
|
state.config.controls_width =
|
||||||
clamp_controls_width(state.config.controls_width, size.width);
|
clamp_controls_width(state.config.controls_width, size.width);
|
||||||
state.config.chat_drawer_width =
|
state.config.chat_drawer_width =
|
||||||
clamp_chat_drawer_width(state.config.chat_drawer_width, size.width);
|
clamp_chat_drawer_width(state.config.chat_drawer_width, size.width);
|
||||||
|
state.config.playlist_drawer_width =
|
||||||
|
clamp_playlist_drawer_width(state.config.playlist_drawer_width, size.width);
|
||||||
}
|
}
|
||||||
AppMessage::EventOccurred(Event::Window(iced::window::Event::Moved(position))) => {
|
AppMessage::EventOccurred(Event::Window(iced::window::Event::Moved(position))) => {
|
||||||
// Remember the position in-memory; written to disk once on close.
|
// Remember the position in-memory; written to disk once on close.
|
||||||
@@ -3121,6 +3133,15 @@ fn can_broadcast_music(state: &AppState) -> bool {
|
|||||||
state.music_broadcasting && state.music_listening_to.is_none()
|
state.music_broadcasting && state.music_listening_to.is_none()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Text shown on the now-playing player bar. Pure for testing.
|
||||||
|
fn now_playing_label(listening_to: Option<&str>, current_track: Option<&str>) -> String {
|
||||||
|
match (listening_to, current_track) {
|
||||||
|
(Some(peer), _) => format!("Tuned in to {peer}"),
|
||||||
|
(None, Some(track)) => track.to_string(),
|
||||||
|
(None, None) => "Nothing playing".to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn stop_music_broadcast(state: &mut AppState) {
|
fn stop_music_broadcast(state: &mut AppState) {
|
||||||
let old_current = state.music_broadcast_id.take();
|
let old_current = state.music_broadcast_id.take();
|
||||||
let old_next = state.music_broadcast_next.take().map(|(_, id, _)| id);
|
let old_next = state.music_broadcast_next.take().map(|(_, id, _)| id);
|
||||||
@@ -4015,7 +4036,7 @@ fn view(state: &AppState) -> Element<'_, AppMessage> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// The Hotkeys info button is always available (hotkeys are app-wide). The
|
// The Hotkeys info button is always available (hotkeys are app-wide). The
|
||||||
// room-layout button is hidden on the Home screen, leaving only it + Settings.
|
// room-only player button is hidden off-room; the layout button is hidden on Home.
|
||||||
let info_button = tooltip(
|
let info_button = tooltip(
|
||||||
button(icon(IconKind::Info, 18.0, color_text))
|
button(icon(IconKind::Info, 18.0, color_text))
|
||||||
.on_press(AppMessage::OpenHotkeyInfo)
|
.on_press(AppMessage::OpenHotkeyInfo)
|
||||||
@@ -4028,6 +4049,27 @@ fn view(state: &AppState) -> Element<'_, AppMessage> {
|
|||||||
)
|
)
|
||||||
.gap(8);
|
.gap(8);
|
||||||
|
|
||||||
|
let player_bar_button: Element<'_, AppMessage> =
|
||||||
|
if state.current_screen == Screen::Room {
|
||||||
|
let active = state.config.show_player_bar;
|
||||||
|
let bg = if active { color_blue } else { color_surface };
|
||||||
|
let fg = if active { color_crust } else { color_text };
|
||||||
|
tooltip(
|
||||||
|
button(text("♪").size(15))
|
||||||
|
.on_press(AppMessage::TogglePlayerBar)
|
||||||
|
.style(b_style(bg, color_blue, fg, 6.0))
|
||||||
|
.padding(8),
|
||||||
|
container(text("Player bar").size(11).color(color_text))
|
||||||
|
.padding(8)
|
||||||
|
.style(c_style(color_crust, color_surface, 6.0)),
|
||||||
|
iced::widget::tooltip::Position::Bottom,
|
||||||
|
)
|
||||||
|
.gap(8)
|
||||||
|
.into()
|
||||||
|
} else {
|
||||||
|
iced::widget::Space::new().width(0.0).height(0.0).into()
|
||||||
|
};
|
||||||
|
|
||||||
let layout_button: Element<'_, AppMessage> = if state.current_screen == Screen::Home {
|
let layout_button: Element<'_, AppMessage> = if state.current_screen == Screen::Home {
|
||||||
iced::widget::Space::new().width(0.0).height(0.0).into()
|
iced::widget::Space::new().width(0.0).height(0.0).into()
|
||||||
} else {
|
} else {
|
||||||
@@ -4051,6 +4093,7 @@ fn view(state: &AppState) -> Element<'_, AppMessage> {
|
|||||||
|
|
||||||
let top_bar = row![
|
let top_bar = row![
|
||||||
horizontal_space(),
|
horizontal_space(),
|
||||||
|
player_bar_button,
|
||||||
info_button,
|
info_button,
|
||||||
layout_button,
|
layout_button,
|
||||||
button(
|
button(
|
||||||
@@ -5524,7 +5567,7 @@ fn view(state: &AppState) -> Element<'_, AppMessage> {
|
|||||||
.unwrap_or_else(|| "--:--".to_string());
|
.unwrap_or_else(|| "--:--".to_string());
|
||||||
column![
|
column![
|
||||||
row![
|
row![
|
||||||
button(text("⏮").size(13))
|
button(text("|◀").size(13))
|
||||||
.on_press(AppMessage::MusicPrev)
|
.on_press(AppMessage::MusicPrev)
|
||||||
.style(b_style(color_surface, color_blue, color_text, 6.0))
|
.style(b_style(color_surface, color_blue, color_text, 6.0))
|
||||||
.padding(7),
|
.padding(7),
|
||||||
@@ -5532,7 +5575,7 @@ fn view(state: &AppState) -> Element<'_, AppMessage> {
|
|||||||
.on_press(AppMessage::MusicPlayPause)
|
.on_press(AppMessage::MusicPlayPause)
|
||||||
.style(b_style(color_blue, color_lavender, color_crust, 6.0))
|
.style(b_style(color_blue, color_lavender, color_crust, 6.0))
|
||||||
.padding(7),
|
.padding(7),
|
||||||
button(text("⏭").size(13))
|
button(text("▶|").size(13))
|
||||||
.on_press(AppMessage::MusicNext)
|
.on_press(AppMessage::MusicNext)
|
||||||
.style(b_style(color_surface, color_blue, color_text, 6.0))
|
.style(b_style(color_surface, color_blue, color_text, 6.0))
|
||||||
.padding(7),
|
.padding(7),
|
||||||
@@ -5664,37 +5707,83 @@ fn view(state: &AppState) -> Element<'_, AppMessage> {
|
|||||||
.width(iced::Length::Fill)
|
.width(iced::Length::Fill)
|
||||||
.into()
|
.into()
|
||||||
};
|
};
|
||||||
// W22: in the 3-column layout the Playlist gets its own card stacked under Chat
|
let music_bar_status = status_snapshot(&state.music_status);
|
||||||
// (built below in the ThreeColumn body arm); in every other layout it stays in
|
let music_bar_playing = music_bar_status.playing_id.is_some();
|
||||||
// the Controls panel. `music_panel` is consumed by exactly one of these.
|
let music_bar_play_label = if music_bar_playing && music_bar_status.paused {
|
||||||
let three_col = matches!(state.config.room_layout, RoomLayout::ThreeColumn);
|
"▶"
|
||||||
let (ctrl_music, playlist_card): (Element<'_, AppMessage>, Option<Element<'_, AppMessage>>) =
|
} else if music_bar_playing {
|
||||||
if three_col {
|
"⏸"
|
||||||
let card = container(
|
} else {
|
||||||
column![
|
"▶"
|
||||||
text("Playlist").size(18).color(color_blue),
|
};
|
||||||
vertical_space(8.0),
|
let listening_peer_name = state
|
||||||
scrollable(music_panel)
|
.music_listening_to
|
||||||
.width(iced::Length::Fill)
|
.and_then(|peer| state.peers.get(&peer).map(|p| p.name.as_str()));
|
||||||
.height(iced::Length::Fill),
|
let current_track_name = state
|
||||||
|
.music_current
|
||||||
|
.and_then(|i| state.music_playlist.get(i).map(|track| track.name.as_str()));
|
||||||
|
let elapsed = format_clip_time(music_bar_status.position);
|
||||||
|
let duration = music_bar_status
|
||||||
|
.total
|
||||||
|
.map(format_clip_time)
|
||||||
|
.unwrap_or_else(|| "--:--".to_string());
|
||||||
|
let player_bar = container(
|
||||||
|
row![
|
||||||
|
container(
|
||||||
|
row![
|
||||||
|
text("♪").size(18).color(color_blue),
|
||||||
|
text(now_playing_label(listening_peer_name, current_track_name))
|
||||||
|
.size(13)
|
||||||
|
.color(color_text),
|
||||||
]
|
]
|
||||||
|
.spacing(8)
|
||||||
|
.align_y(iced::alignment::Vertical::Center)
|
||||||
|
)
|
||||||
|
.width(iced::Length::Fill),
|
||||||
|
row![
|
||||||
|
button(text("|◀").size(13))
|
||||||
|
.on_press(AppMessage::MusicPrev)
|
||||||
|
.style(b_style(color_surface, color_blue, color_text, 6.0))
|
||||||
|
.padding(7),
|
||||||
|
button(text(music_bar_play_label).size(13))
|
||||||
|
.on_press(AppMessage::MusicPlayPause)
|
||||||
|
.style(b_style(color_blue, color_lavender, color_crust, 6.0))
|
||||||
|
.padding(7),
|
||||||
|
button(text("▶|").size(13))
|
||||||
|
.on_press(AppMessage::MusicNext)
|
||||||
|
.style(b_style(color_surface, color_blue, color_text, 6.0))
|
||||||
|
.padding(7),
|
||||||
|
]
|
||||||
|
.spacing(8)
|
||||||
|
.align_y(iced::alignment::Vertical::Center),
|
||||||
|
container(
|
||||||
|
row![
|
||||||
|
text(format!("{elapsed} / {duration}"))
|
||||||
|
.size(11)
|
||||||
|
.color(color_subtext),
|
||||||
|
button(text(if state.playlist_drawer_open { "⤡" } else { "⤢" }).size(13))
|
||||||
|
.on_press(AppMessage::TogglePlaylistDrawer)
|
||||||
|
.style(b_style(
|
||||||
|
if state.playlist_drawer_open { color_blue } else { color_surface },
|
||||||
|
color_blue,
|
||||||
|
if state.playlist_drawer_open { color_crust } else { color_text },
|
||||||
|
6.0,
|
||||||
|
))
|
||||||
|
.padding(7),
|
||||||
|
]
|
||||||
|
.spacing(8)
|
||||||
|
.align_y(iced::alignment::Vertical::Center)
|
||||||
|
)
|
||||||
|
.width(iced::Length::Fill)
|
||||||
|
.align_x(iced::alignment::Horizontal::Right),
|
||||||
|
]
|
||||||
|
.spacing(8)
|
||||||
|
.align_y(iced::alignment::Vertical::Center)
|
||||||
)
|
)
|
||||||
.style(c_style(color_mantle, color_surface, 8.0))
|
.style(c_style(color_mantle, color_surface, 8.0))
|
||||||
.padding(12)
|
.padding(10)
|
||||||
.width(iced::Length::Fill)
|
.width(iced::Length::Fill)
|
||||||
.height(iced::Length::Fill);
|
.height(iced::Length::Fixed(56.0));
|
||||||
(column![].into(), Some(card.into()))
|
|
||||||
} else {
|
|
||||||
(
|
|
||||||
column![
|
|
||||||
vertical_space(20.0),
|
|
||||||
text("Playlist").size(18).color(color_blue),
|
|
||||||
music_panel,
|
|
||||||
]
|
|
||||||
.into(),
|
|
||||||
None,
|
|
||||||
)
|
|
||||||
};
|
|
||||||
let ctrl_buttons = column![
|
let ctrl_buttons = column![
|
||||||
button(btn_content(mute_kind, mute_text, mute_fg))
|
button(btn_content(mute_kind, mute_text, mute_fg))
|
||||||
.on_press(AppMessage::ToggleMutePressed)
|
.on_press(AppMessage::ToggleMutePressed)
|
||||||
@@ -5798,7 +5887,6 @@ fn view(state: &AppState) -> Element<'_, AppMessage> {
|
|||||||
.padding(14)
|
.padding(14)
|
||||||
.width(iced::Length::Fill)
|
.width(iced::Length::Fill)
|
||||||
},
|
},
|
||||||
ctrl_music,
|
|
||||||
];
|
];
|
||||||
|
|
||||||
// Leave is the exit control, so it's pinned below the scrolling controls
|
// Leave is the exit control, so it's pinned below the scrolling controls
|
||||||
@@ -6112,13 +6200,35 @@ fn view(state: &AppState) -> Element<'_, AppMessage> {
|
|||||||
.height(iced::Length::Fixed(DIVIDER_THICKNESS))
|
.height(iced::Length::Fixed(DIVIDER_THICKNESS))
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let playlist_drawer = container(
|
||||||
|
column![
|
||||||
|
text("Playlist").size(18).color(color_blue),
|
||||||
|
vertical_space(8.0),
|
||||||
|
scrollable(music_panel)
|
||||||
|
.width(iced::Length::Fill)
|
||||||
|
.height(iced::Length::Fill),
|
||||||
|
]
|
||||||
|
.spacing(0)
|
||||||
|
)
|
||||||
|
.style(c_style(color_mantle, color_surface, 8.0))
|
||||||
|
.padding(12)
|
||||||
|
.width(iced::Length::Fixed(state.config.playlist_drawer_width))
|
||||||
|
.height(iced::Length::Fill);
|
||||||
|
|
||||||
|
let drawer_open = state.playlist_drawer_open && state.config.show_player_bar;
|
||||||
|
let body_w = if drawer_open {
|
||||||
|
state.window_size.width - state.config.playlist_drawer_width - DIVIDER_THICKNESS
|
||||||
|
} else {
|
||||||
|
state.window_size.width
|
||||||
|
};
|
||||||
|
|
||||||
// Assemble the body per the chosen room layout. `chat_inner` is moved into
|
// Assemble the body per the chosen room layout. `chat_inner` is moved into
|
||||||
// exactly one arm (allowed across mutually-exclusive match arms).
|
// exactly one arm (allowed across mutually-exclusive match arms).
|
||||||
let pw = state.config.participants_width;
|
let pw = state.config.participants_width;
|
||||||
let body: Element<'_, AppMessage> = match state.config.room_layout {
|
let body: Element<'_, AppMessage> = match state.config.room_layout {
|
||||||
RoomLayout::BottomDock => {
|
RoomLayout::BottomDock => {
|
||||||
// Cap Participants so the Fill Controls panel keeps its minimum.
|
// Cap Participants so the Fill Controls panel keeps its minimum.
|
||||||
let avail = state.window_size.width - 30.0;
|
let avail = body_w - 30.0;
|
||||||
let pwb = pw.min((avail - CONTROLS_MIN_W - DIVIDER_THICKNESS).max(PARTICIPANTS_MIN_W));
|
let pwb = pw.min((avail - CONTROLS_MIN_W - DIVIDER_THICKNESS).max(PARTICIPANTS_MIN_W));
|
||||||
let main = row![
|
let main = row![
|
||||||
peers_panel.width(iced::Length::Fixed(pwb)),
|
peers_panel.width(iced::Length::Fixed(pwb)),
|
||||||
@@ -6135,7 +6245,7 @@ fn view(state: &AppState) -> Element<'_, AppMessage> {
|
|||||||
column![main, hdiv(DividerKind::Chat), chat].into()
|
column![main, hdiv(DividerKind::Chat), chat].into()
|
||||||
}
|
}
|
||||||
RoomLayout::ThreeColumn => {
|
RoomLayout::ThreeColumn => {
|
||||||
let avail = state.window_size.width - 30.0; // outer padding
|
let avail = body_w - 30.0; // outer padding
|
||||||
let pw3 = pw.min(
|
let pw3 = pw.min(
|
||||||
(avail - state.config.controls_width - CHAT_MIN_W - 2.0 * DIVIDER_THICKNESS)
|
(avail - state.config.controls_width - CHAT_MIN_W - 2.0 * DIVIDER_THICKNESS)
|
||||||
.max(PARTICIPANTS_MIN_W),
|
.max(PARTICIPANTS_MIN_W),
|
||||||
@@ -6145,19 +6255,10 @@ fn view(state: &AppState) -> Element<'_, AppMessage> {
|
|||||||
.padding(12)
|
.padding(12)
|
||||||
.width(iced::Length::Fill)
|
.width(iced::Length::Fill)
|
||||||
.height(iced::Length::Fill);
|
.height(iced::Length::Fill);
|
||||||
// The Playlist card was built above iff this is the 3-column layout.
|
|
||||||
let card = playlist_card.expect("playlist_card is Some for ThreeColumn");
|
|
||||||
let middle = column![
|
|
||||||
chat,
|
|
||||||
hdiv(DividerKind::ThreeColPlaylist),
|
|
||||||
container(card).height(iced::Length::Fixed(state.config.threecol_playlist_height)),
|
|
||||||
]
|
|
||||||
.width(iced::Length::Fill)
|
|
||||||
.height(iced::Length::Fill);
|
|
||||||
row![
|
row![
|
||||||
peers_panel.width(iced::Length::Fixed(pw3)),
|
peers_panel.width(iced::Length::Fixed(pw3)),
|
||||||
vdiv(DividerKind::Panels),
|
vdiv(DividerKind::Panels),
|
||||||
middle,
|
chat,
|
||||||
vdiv(DividerKind::Controls),
|
vdiv(DividerKind::Controls),
|
||||||
control_panel.width(iced::Length::Fixed(state.config.controls_width)),
|
control_panel.width(iced::Length::Fixed(state.config.controls_width)),
|
||||||
]
|
]
|
||||||
@@ -6166,7 +6267,7 @@ fn view(state: &AppState) -> Element<'_, AppMessage> {
|
|||||||
.into()
|
.into()
|
||||||
}
|
}
|
||||||
RoomLayout::Drawer => {
|
RoomLayout::Drawer => {
|
||||||
let avail = state.window_size.width - 30.0;
|
let avail = body_w - 30.0;
|
||||||
if state.drawer_chat_open {
|
if state.drawer_chat_open {
|
||||||
// Participants + Chat drawer are both fixed; cap Participants so
|
// Participants + Chat drawer are both fixed; cap Participants so
|
||||||
// the Fill Controls panel between them keeps its minimum.
|
// the Fill Controls panel between them keeps its minimum.
|
||||||
@@ -6238,9 +6339,30 @@ fn view(state: &AppState) -> Element<'_, AppMessage> {
|
|||||||
iced::widget::Space::new().width(0.0).height(0.0).into()
|
iced::widget::Space::new().width(0.0).height(0.0).into()
|
||||||
};
|
};
|
||||||
|
|
||||||
let room = container(
|
let main_area = column![
|
||||||
column![top_bar, header_container, clock_skew_banner, vertical_space(12.0), body]
|
header_container,
|
||||||
)
|
clock_skew_banner,
|
||||||
|
vertical_space(12.0),
|
||||||
|
body,
|
||||||
|
];
|
||||||
|
let main_with_bar: Element<'_, AppMessage> = if state.config.show_player_bar {
|
||||||
|
column![main_area.height(iced::Length::Fill), player_bar].into()
|
||||||
|
} else {
|
||||||
|
main_area.into()
|
||||||
|
};
|
||||||
|
let inner: Element<'_, AppMessage> = if drawer_open {
|
||||||
|
row![
|
||||||
|
column![main_with_bar].width(iced::Length::Fill),
|
||||||
|
vdiv(DividerKind::PlaylistDrawer),
|
||||||
|
playlist_drawer,
|
||||||
|
]
|
||||||
|
.height(iced::Length::Fill)
|
||||||
|
.into()
|
||||||
|
} else {
|
||||||
|
main_with_bar
|
||||||
|
};
|
||||||
|
|
||||||
|
let room = container(column![top_bar, inner])
|
||||||
.padding(15)
|
.padding(15)
|
||||||
.width(iced::Length::Fill)
|
.width(iced::Length::Fill)
|
||||||
.height(iced::Length::Fill)
|
.height(iced::Length::Fill)
|
||||||
@@ -7653,8 +7775,8 @@ impl Program<AppMessage> for Icon {
|
|||||||
mod tests {
|
mod tests {
|
||||||
use super::{
|
use super::{
|
||||||
attachment_default_name, clear_expired_clock_skew_warning, format_clock_skew_duration,
|
attachment_default_name, clear_expired_clock_skew_warning, format_clock_skew_duration,
|
||||||
format_duration, format_relative_ago, initial_window_position, reconnect_attempt_chime, reconnected_chime,
|
format_duration, format_relative_ago, initial_window_position, now_playing_label,
|
||||||
set_peer_gate_config, set_peer_volume_config, show_clock_skew_warning, update, AppConfig,
|
reconnect_attempt_chime, reconnected_chime, set_peer_gate_config, set_peer_volume_config, show_clock_skew_warning, update, AppConfig,
|
||||||
AppMessage, AppState, AttachmentCache, AttachmentState, ChatEntry, ClockSkewBanner,
|
AppMessage, AppState, AttachmentCache, AttachmentState, ChatEntry, ClockSkewBanner,
|
||||||
GateMeter, METER_MAX, UiEvent, CLOCK_SKEW_WARNING_VISIBLE_SECS,
|
GateMeter, METER_MAX, UiEvent, CLOCK_SKEW_WARNING_VISIBLE_SECS,
|
||||||
};
|
};
|
||||||
@@ -8330,6 +8452,40 @@ mod tests {
|
|||||||
assert!(d.is_finite() && d >= CHAT_MIN_W);
|
assert!(d.is_finite() && d >= CHAT_MIN_W);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn playlist_drawer_width_clamps() {
|
||||||
|
use super::{clamp_playlist_drawer_width, CHAT_MIN_W, CONTROLS_MIN_W, PARTICIPANTS_MIN_W};
|
||||||
|
let window_w = 1000.0;
|
||||||
|
let max = window_w - PARTICIPANTS_MIN_W - CONTROLS_MIN_W;
|
||||||
|
// Mid-range passes through.
|
||||||
|
assert_eq!(clamp_playlist_drawer_width(320.0, window_w), 320.0);
|
||||||
|
// Below minimum snaps up.
|
||||||
|
assert_eq!(clamp_playlist_drawer_width(10.0, window_w), CHAT_MIN_W);
|
||||||
|
// Above maximum leaves the rest of the room at its reserved width.
|
||||||
|
assert_eq!(clamp_playlist_drawer_width(800.0, window_w), max);
|
||||||
|
// Tiny window stays finite and at/above the minimum (no inverted range).
|
||||||
|
let d = clamp_playlist_drawer_width(400.0, 100.0);
|
||||||
|
assert!(d.is_finite() && d >= CHAT_MIN_W);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn now_playing_label_prefers_tuned_peer() {
|
||||||
|
assert_eq!(
|
||||||
|
now_playing_label(Some("Alice"), Some("local.flac")),
|
||||||
|
"Tuned in to Alice"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn now_playing_label_uses_current_track_when_local() {
|
||||||
|
assert_eq!(now_playing_label(None, Some("local.flac")), "local.flac");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn now_playing_label_handles_empty_player() {
|
||||||
|
assert_eq!(now_playing_label(None, None), "Nothing playing");
|
||||||
|
}
|
||||||
|
|
||||||
use crate::notify::Sound;
|
use crate::notify::Sound;
|
||||||
use iroh::EndpointId;
|
use iroh::EndpointId;
|
||||||
use std::collections::HashSet;
|
use std::collections::HashSet;
|
||||||
|
|||||||
@@ -138,6 +138,10 @@ fn default_chat_drawer_width() -> f32 {
|
|||||||
320.0
|
320.0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn default_playlist_drawer_width() -> f32 {
|
||||||
|
320.0
|
||||||
|
}
|
||||||
|
|
||||||
fn default_window_width() -> f32 {
|
fn default_window_width() -> f32 {
|
||||||
900.0
|
900.0
|
||||||
}
|
}
|
||||||
@@ -174,6 +178,9 @@ pub struct AppConfig {
|
|||||||
/// W22 music: opt-in shared listening broadcast toggle. Local preference.
|
/// W22 music: opt-in shared listening broadcast toggle. Local preference.
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub music_broadcast: bool,
|
pub music_broadcast: bool,
|
||||||
|
/// Show the slim now-playing player bar in the room screen.
|
||||||
|
#[serde(default = "default_true")]
|
||||||
|
pub show_player_bar: bool,
|
||||||
/// When true, `clip_volume` governs every clip. When false, each clip keeps
|
/// When true, `clip_volume` governs every clip. When false, each clip keeps
|
||||||
/// its own (in-memory) level and the universal slider is inactive.
|
/// its own (in-memory) level and the universal slider is inactive.
|
||||||
#[serde(default = "default_true")]
|
#[serde(default = "default_true")]
|
||||||
@@ -207,6 +214,9 @@ pub struct AppConfig {
|
|||||||
/// Chat drawer width for the drawer layout (px).
|
/// Chat drawer width for the drawer layout (px).
|
||||||
#[serde(default = "default_chat_drawer_width")]
|
#[serde(default = "default_chat_drawer_width")]
|
||||||
pub chat_drawer_width: f32,
|
pub chat_drawer_width: f32,
|
||||||
|
/// Playlist drawer width for the room-screen right-edge music panel (px).
|
||||||
|
#[serde(default = "default_playlist_drawer_width")]
|
||||||
|
pub playlist_drawer_width: f32,
|
||||||
/// Chosen arrangement of the in-call room screen.
|
/// Chosen arrangement of the in-call room screen.
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub room_layout: RoomLayout,
|
pub room_layout: RoomLayout,
|
||||||
@@ -348,6 +358,7 @@ impl Default for AppConfig {
|
|||||||
music_playlist: Vec::new(),
|
music_playlist: Vec::new(),
|
||||||
music_volume: 1.0,
|
music_volume: 1.0,
|
||||||
music_broadcast: false,
|
music_broadcast: false,
|
||||||
|
show_player_bar: true,
|
||||||
clip_volume_universal: true,
|
clip_volume_universal: true,
|
||||||
network_mode: NetworkMode::default(),
|
network_mode: NetworkMode::default(),
|
||||||
presence_mode: crate::presence::PresenceMode::default(),
|
presence_mode: crate::presence::PresenceMode::default(),
|
||||||
@@ -358,6 +369,7 @@ impl Default for AppConfig {
|
|||||||
threecol_playlist_height: default_threecol_playlist_height(),
|
threecol_playlist_height: default_threecol_playlist_height(),
|
||||||
controls_width: default_controls_width(),
|
controls_width: default_controls_width(),
|
||||||
chat_drawer_width: default_chat_drawer_width(),
|
chat_drawer_width: default_chat_drawer_width(),
|
||||||
|
playlist_drawer_width: default_playlist_drawer_width(),
|
||||||
room_layout: RoomLayout::default(),
|
room_layout: RoomLayout::default(),
|
||||||
theme: AppTheme::default(),
|
theme: AppTheme::default(),
|
||||||
avatar: crate::avatar::Avatar::default(),
|
avatar: crate::avatar::Avatar::default(),
|
||||||
@@ -517,6 +529,8 @@ mod tests {
|
|||||||
assert_eq!(deserialized.room_layout, RoomLayout::BottomDock);
|
assert_eq!(deserialized.room_layout, RoomLayout::BottomDock);
|
||||||
assert_eq!(deserialized.controls_width, 280.0);
|
assert_eq!(deserialized.controls_width, 280.0);
|
||||||
assert_eq!(deserialized.chat_drawer_width, 320.0);
|
assert_eq!(deserialized.chat_drawer_width, 320.0);
|
||||||
|
assert_eq!(deserialized.playlist_drawer_width, 320.0);
|
||||||
|
assert!(deserialized.show_player_bar);
|
||||||
assert!(deserialized.custom_sound_self_join.is_none());
|
assert!(deserialized.custom_sound_self_join.is_none());
|
||||||
assert!(deserialized.custom_sound_peer_join.is_none());
|
assert!(deserialized.custom_sound_peer_join.is_none());
|
||||||
assert!(deserialized.custom_sound_peer_leave.is_none());
|
assert!(deserialized.custom_sound_peer_leave.is_none());
|
||||||
@@ -701,6 +715,7 @@ mod tests {
|
|||||||
assert!(def.music_playlist.is_empty());
|
assert!(def.music_playlist.is_empty());
|
||||||
assert_eq!(def.music_volume, 1.0);
|
assert_eq!(def.music_volume, 1.0);
|
||||||
assert!(!def.music_broadcast);
|
assert!(!def.music_broadcast);
|
||||||
|
assert!(def.show_player_bar);
|
||||||
assert!(def.clip_volume_universal);
|
assert!(def.clip_volume_universal);
|
||||||
|
|
||||||
// Missing in JSON → unity (serde default).
|
// Missing in JSON → unity (serde default).
|
||||||
@@ -712,6 +727,7 @@ mod tests {
|
|||||||
assert!(cfg_missing.music_playlist.is_empty());
|
assert!(cfg_missing.music_playlist.is_empty());
|
||||||
assert_eq!(cfg_missing.music_volume, 1.0);
|
assert_eq!(cfg_missing.music_volume, 1.0);
|
||||||
assert!(!cfg_missing.music_broadcast);
|
assert!(!cfg_missing.music_broadcast);
|
||||||
|
assert!(cfg_missing.show_player_bar);
|
||||||
// Configs predating the toggle default to universal mode.
|
// Configs predating the toggle default to universal mode.
|
||||||
assert!(cfg_missing.clip_volume_universal);
|
assert!(cfg_missing.clip_volume_universal);
|
||||||
|
|
||||||
@@ -723,6 +739,7 @@ mod tests {
|
|||||||
music_playlist: vec!["/tmp/song.ogg".to_string()],
|
music_playlist: vec!["/tmp/song.ogg".to_string()],
|
||||||
music_volume: 0.6,
|
music_volume: 0.6,
|
||||||
music_broadcast: true,
|
music_broadcast: true,
|
||||||
|
show_player_bar: false,
|
||||||
clip_volume_universal: false,
|
clip_volume_universal: false,
|
||||||
..AppConfig::default()
|
..AppConfig::default()
|
||||||
};
|
};
|
||||||
@@ -734,6 +751,7 @@ mod tests {
|
|||||||
assert_eq!(round_tripped.music_playlist, vec!["/tmp/song.ogg".to_string()]);
|
assert_eq!(round_tripped.music_playlist, vec!["/tmp/song.ogg".to_string()]);
|
||||||
assert_eq!(round_tripped.music_volume, 0.6);
|
assert_eq!(round_tripped.music_volume, 0.6);
|
||||||
assert!(round_tripped.music_broadcast);
|
assert!(round_tripped.music_broadcast);
|
||||||
|
assert!(!round_tripped.show_player_bar);
|
||||||
assert!(!round_tripped.clip_volume_universal);
|
assert!(!round_tripped.clip_volume_universal);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user