Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
92c9d585b8 |
+195
-43
@@ -30,6 +30,55 @@ pub enum Screen {
|
|||||||
Settings,
|
Settings,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum SettingsCategory {
|
||||||
|
Audio,
|
||||||
|
Recording,
|
||||||
|
Profile,
|
||||||
|
Appearance,
|
||||||
|
Network,
|
||||||
|
Notifications,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SettingsCategory {
|
||||||
|
const ALL: [SettingsCategory; 6] = [
|
||||||
|
SettingsCategory::Audio,
|
||||||
|
SettingsCategory::Recording,
|
||||||
|
SettingsCategory::Profile,
|
||||||
|
SettingsCategory::Appearance,
|
||||||
|
SettingsCategory::Network,
|
||||||
|
SettingsCategory::Notifications,
|
||||||
|
];
|
||||||
|
|
||||||
|
fn label(self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
SettingsCategory::Audio => "Audio",
|
||||||
|
SettingsCategory::Recording => "Recording",
|
||||||
|
SettingsCategory::Profile => "Profile",
|
||||||
|
SettingsCategory::Appearance => "Appearance",
|
||||||
|
SettingsCategory::Network => "Network",
|
||||||
|
SettingsCategory::Notifications => "Notifications",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn hint(self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
SettingsCategory::Audio => "Devices, mic gate, echo",
|
||||||
|
SettingsCategory::Recording => "Mixed and stem capture",
|
||||||
|
SettingsCategory::Profile => "Avatar and identity",
|
||||||
|
SettingsCategory::Appearance => "Layout and theme",
|
||||||
|
SettingsCategory::Network => "Relay and privacy mode",
|
||||||
|
SettingsCategory::Notifications => "Chimes and sounds",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for SettingsCategory {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
f.write_str(self.label())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// One rendered room-chat line. `mine` distinguishes our own (locally echoed)
|
/// One rendered room-chat line. `mine` distinguishes our own (locally echoed)
|
||||||
/// messages from peers' for colouring.
|
/// messages from peers' for colouring.
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
@@ -161,6 +210,7 @@ pub enum AppMessage {
|
|||||||
EventOccurred(Event),
|
EventOccurred(Event),
|
||||||
NavigateToSettings,
|
NavigateToSettings,
|
||||||
NavigateBack,
|
NavigateBack,
|
||||||
|
SelectSettingsCategory(SettingsCategory),
|
||||||
ToggleNotifications(bool),
|
ToggleNotifications(bool),
|
||||||
ToggleEchoCancellation(bool),
|
ToggleEchoCancellation(bool),
|
||||||
CustomSoundPathChanged(Sound, String),
|
CustomSoundPathChanged(Sound, String),
|
||||||
@@ -276,6 +326,7 @@ pub struct AppState {
|
|||||||
ever_connected: HashSet<EndpointId>,
|
ever_connected: HashSet<EndpointId>,
|
||||||
controller: Arc<CoreController>,
|
controller: Arc<CoreController>,
|
||||||
current_screen: Screen,
|
current_screen: Screen,
|
||||||
|
settings_category: SettingsCategory,
|
||||||
/// Whether we're currently sharing our own screen (confirmed by the core).
|
/// Whether we're currently sharing our own screen (confirmed by the core).
|
||||||
self_sharing: bool,
|
self_sharing: bool,
|
||||||
/// Whether the `pixelpass` binary is available, gating the Share controls.
|
/// Whether the `pixelpass` binary is available, gating the Share controls.
|
||||||
@@ -401,6 +452,7 @@ impl Default for AppState {
|
|||||||
ever_connected: HashSet::new(),
|
ever_connected: HashSet::new(),
|
||||||
controller,
|
controller,
|
||||||
current_screen: Screen::Home,
|
current_screen: Screen::Home,
|
||||||
|
settings_category: SettingsCategory::Audio,
|
||||||
self_sharing: false,
|
self_sharing: false,
|
||||||
pixelpass_available,
|
pixelpass_available,
|
||||||
self_node_id: None,
|
self_node_id: None,
|
||||||
@@ -935,6 +987,9 @@ fn update(state: &mut AppState, message: AppMessage) -> Task<AppMessage> {
|
|||||||
crate::recents::remove_recent(&mut state.config.recents, &ticket);
|
crate::recents::remove_recent(&mut state.config.recents, &ticket);
|
||||||
state.config.save();
|
state.config.save();
|
||||||
}
|
}
|
||||||
|
AppMessage::SelectSettingsCategory(category) => {
|
||||||
|
state.settings_category = category;
|
||||||
|
}
|
||||||
AppMessage::ToggleNotifications(enabled) => {
|
AppMessage::ToggleNotifications(enabled) => {
|
||||||
state.config.notifications_enabled = enabled;
|
state.config.notifications_enabled = enabled;
|
||||||
state.config.save();
|
state.config.save();
|
||||||
@@ -2118,9 +2173,8 @@ fn view(state: &AppState) -> Element<'_, AppMessage> {
|
|||||||
|
|
||||||
// Presence + Friends moved to the home screen (see `friends_panel`).
|
// Presence + Friends moved to the home screen (see `friends_panel`).
|
||||||
|
|
||||||
let settings_content = scrollable(
|
let settings_body: Element<'_, AppMessage> = match state.settings_category {
|
||||||
column![
|
SettingsCategory::Audio => column![
|
||||||
// --- Audio Devices ---
|
|
||||||
section_header("Audio Devices"),
|
section_header("Audio Devices"),
|
||||||
row![
|
row![
|
||||||
column![
|
column![
|
||||||
@@ -2149,8 +2203,6 @@ fn view(state: &AppState) -> Element<'_, AppMessage> {
|
|||||||
].spacing(8).width(iced::Length::Fill),
|
].spacing(8).width(iced::Length::Fill),
|
||||||
].spacing(20).align_y(iced::alignment::Vertical::Top).width(iced::Length::Fill),
|
].spacing(20).align_y(iced::alignment::Vertical::Top).width(iced::Length::Fill),
|
||||||
vertical_space(section_gap),
|
vertical_space(section_gap),
|
||||||
|
|
||||||
// --- Microphone ---
|
|
||||||
section_header("Microphone"),
|
section_header("Microphone"),
|
||||||
column![
|
column![
|
||||||
mic_meter,
|
mic_meter,
|
||||||
@@ -2161,9 +2213,11 @@ fn view(state: &AppState) -> Element<'_, AppMessage> {
|
|||||||
.on_toggle(AppMessage::ToggleEchoCancellation),
|
.on_toggle(AppMessage::ToggleEchoCancellation),
|
||||||
text("Cancels speaker echo + suppresses noise (PipeWire). Takes effect on your next room join.").size(11).color(color_subtext),
|
text("Cancels speaker echo + suppresses noise (PipeWire). Takes effect on your next room join.").size(11).color(color_subtext),
|
||||||
].spacing(8).width(iced::Length::Fill),
|
].spacing(8).width(iced::Length::Fill),
|
||||||
vertical_space(section_gap),
|
]
|
||||||
|
.spacing(10)
|
||||||
// --- Recording ---
|
.width(iced::Length::Fill)
|
||||||
|
.into(),
|
||||||
|
SettingsCategory::Recording => column![
|
||||||
section_header("Recording"),
|
section_header("Recording"),
|
||||||
column![
|
column![
|
||||||
mode_radio(RecordingMode::Mixed, "Mixed (single file)"),
|
mode_radio(RecordingMode::Mixed, "Mixed (single file)"),
|
||||||
@@ -2172,22 +2226,21 @@ fn view(state: &AppState) -> Element<'_, AppMessage> {
|
|||||||
vertical_space(2.0),
|
vertical_space(2.0),
|
||||||
text("Hover an option for what it does. Saved to ~/peerspeak-recordings/ — Multitrack/Both as a timestamped folder of tracks, Mixed as a single file. Applies to your next recording.").size(11).color(color_subtext),
|
text("Hover an option for what it does. Saved to ~/peerspeak-recordings/ — Multitrack/Both as a timestamped folder of tracks, Mixed as a single file. Applies to your next recording.").size(11).color(color_subtext),
|
||||||
].spacing(8).width(iced::Length::Fill),
|
].spacing(8).width(iced::Length::Fill),
|
||||||
|
]
|
||||||
|
.spacing(10)
|
||||||
|
.width(iced::Length::Fill)
|
||||||
|
.into(),
|
||||||
|
SettingsCategory::Profile => column![
|
||||||
|
section_header("Avatar"),
|
||||||
|
avatar_section,
|
||||||
vertical_space(section_gap),
|
vertical_space(section_gap),
|
||||||
|
section_header("Identity"),
|
||||||
// --- Network & Privacy ---
|
identity_section,
|
||||||
section_header("Network & Privacy"),
|
]
|
||||||
column![
|
.spacing(10)
|
||||||
pick_list(
|
.width(iced::Length::Fill)
|
||||||
&NetworkMode::ALL[..],
|
.into(),
|
||||||
Some(state.config.network_mode),
|
SettingsCategory::Appearance => column![
|
||||||
AppMessage::NetworkModeSelected,
|
|
||||||
).width(iced::Length::Fill),
|
|
||||||
text(network_mode_hint(state.config.network_mode)).size(11).color(color_subtext),
|
|
||||||
text("Takes effect on your next room join.").size(11).color(color_subtext),
|
|
||||||
].spacing(4).width(iced::Length::Fill),
|
|
||||||
vertical_space(section_gap),
|
|
||||||
|
|
||||||
// --- Room Layout ---
|
|
||||||
section_header("Room Layout"),
|
section_header("Room Layout"),
|
||||||
column![
|
column![
|
||||||
row![
|
row![
|
||||||
@@ -2198,25 +2251,28 @@ fn view(state: &AppState) -> Element<'_, AppMessage> {
|
|||||||
text("How the in-call room is arranged. Applies live.").size(11).color(color_subtext),
|
text("How the in-call room is arranged. Applies live.").size(11).color(color_subtext),
|
||||||
].spacing(8).width(iced::Length::Fill),
|
].spacing(8).width(iced::Length::Fill),
|
||||||
vertical_space(section_gap),
|
vertical_space(section_gap),
|
||||||
|
|
||||||
// --- Theme ---
|
|
||||||
section_header("Theme"),
|
section_header("Theme"),
|
||||||
theme_section,
|
theme_section,
|
||||||
vertical_space(section_gap),
|
]
|
||||||
|
.spacing(10)
|
||||||
// --- Avatar ---
|
.width(iced::Length::Fill)
|
||||||
section_header("Avatar"),
|
.into(),
|
||||||
avatar_section,
|
SettingsCategory::Network => column![
|
||||||
vertical_space(section_gap),
|
section_header("Network & Privacy"),
|
||||||
|
column![
|
||||||
// --- Identity ---
|
pick_list(
|
||||||
section_header("Identity"),
|
&NetworkMode::ALL[..],
|
||||||
identity_section,
|
Some(state.config.network_mode),
|
||||||
vertical_space(section_gap),
|
AppMessage::NetworkModeSelected,
|
||||||
|
).width(iced::Length::Fill),
|
||||||
// (Presence + Friends now live on the home screen.)
|
text(network_mode_hint(state.config.network_mode)).size(11).color(color_subtext),
|
||||||
|
text("Takes effect on your next room join.").size(11).color(color_subtext),
|
||||||
// --- Notifications & Sounds ---
|
].spacing(4).width(iced::Length::Fill),
|
||||||
|
]
|
||||||
|
.spacing(10)
|
||||||
|
.width(iced::Length::Fill)
|
||||||
|
.into(),
|
||||||
|
SettingsCategory::Notifications => column![
|
||||||
section_header("Notifications & Sounds"),
|
section_header("Notifications & Sounds"),
|
||||||
column![
|
column![
|
||||||
checkbox(state.config.notifications_enabled)
|
checkbox(state.config.notifications_enabled)
|
||||||
@@ -2244,9 +2300,92 @@ fn view(state: &AppState) -> Element<'_, AppMessage> {
|
|||||||
]
|
]
|
||||||
.spacing(10)
|
.spacing(10)
|
||||||
.width(iced::Length::Fill)
|
.width(iced::Length::Fill)
|
||||||
)
|
.into(),
|
||||||
.width(iced::Length::Fill)
|
};
|
||||||
.height(iced::Length::Fill);
|
|
||||||
|
let category_button = |category: SettingsCategory| -> Element<'_, AppMessage> {
|
||||||
|
let selected = state.settings_category == category;
|
||||||
|
let label_color = if selected { color_blue } else { color_text };
|
||||||
|
let border_color = if selected { color_blue } else { Color::TRANSPARENT };
|
||||||
|
let bg = if selected { color_surface } else { Color::TRANSPARENT };
|
||||||
|
button(
|
||||||
|
container(
|
||||||
|
column![
|
||||||
|
text(category.label()).size(14).color(label_color),
|
||||||
|
text(category.hint()).size(11).color(color_subtext),
|
||||||
|
]
|
||||||
|
.spacing(2)
|
||||||
|
.width(iced::Length::Fill),
|
||||||
|
)
|
||||||
|
.width(iced::Length::Fill),
|
||||||
|
)
|
||||||
|
.on_press(AppMessage::SelectSettingsCategory(category))
|
||||||
|
.style(move |_theme: &Theme, status: button::Status| {
|
||||||
|
let active_bg = match status {
|
||||||
|
button::Status::Hovered if selected => color_surface,
|
||||||
|
button::Status::Hovered => color_crust,
|
||||||
|
_ => bg,
|
||||||
|
};
|
||||||
|
button::Style {
|
||||||
|
background: Some(Background::Color(active_bg)),
|
||||||
|
text_color: label_color,
|
||||||
|
border: Border {
|
||||||
|
color: border_color,
|
||||||
|
width: if selected { 1.0 } else { 0.0 },
|
||||||
|
radius: 8.0.into(),
|
||||||
|
},
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.padding(10)
|
||||||
|
.width(iced::Length::Fill)
|
||||||
|
.into()
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut settings_nav = column![
|
||||||
|
text("SETTINGS").size(11).color(color_subtext),
|
||||||
|
]
|
||||||
|
.spacing(8)
|
||||||
|
.width(iced::Length::Fill);
|
||||||
|
for category in SettingsCategory::ALL {
|
||||||
|
settings_nav = settings_nav.push(category_button(category));
|
||||||
|
}
|
||||||
|
let settings_nav = container(settings_nav)
|
||||||
|
.padding(12)
|
||||||
|
.width(iced::Length::Fixed(220.0))
|
||||||
|
.height(iced::Length::Fill)
|
||||||
|
.style(c_style(color_crust, color_surface, 8.0));
|
||||||
|
|
||||||
|
let settings_content: Element<'_, AppMessage> = if state.window_size.width < 820.0 {
|
||||||
|
scrollable(
|
||||||
|
column![
|
||||||
|
text("Category").size(12).color(color_subtext),
|
||||||
|
pick_list(
|
||||||
|
&SettingsCategory::ALL[..],
|
||||||
|
Some(state.settings_category),
|
||||||
|
AppMessage::SelectSettingsCategory,
|
||||||
|
).width(iced::Length::Fill),
|
||||||
|
vertical_space(10.0),
|
||||||
|
settings_body,
|
||||||
|
]
|
||||||
|
.spacing(8)
|
||||||
|
.width(iced::Length::Fill),
|
||||||
|
)
|
||||||
|
.width(iced::Length::Fill)
|
||||||
|
.height(iced::Length::Fill)
|
||||||
|
.into()
|
||||||
|
} else {
|
||||||
|
row![
|
||||||
|
settings_nav,
|
||||||
|
scrollable(settings_body)
|
||||||
|
.width(iced::Length::Fill)
|
||||||
|
.height(iced::Length::Fill),
|
||||||
|
]
|
||||||
|
.spacing(16)
|
||||||
|
.width(iced::Length::Fill)
|
||||||
|
.height(iced::Length::Fill)
|
||||||
|
.into()
|
||||||
|
};
|
||||||
|
|
||||||
// Sticky header bar: stays fixed above the scrollable content so the Back
|
// Sticky header bar: stays fixed above the scrollable content so the Back
|
||||||
// button is always reachable. The "Settings" title is centered by flanking
|
// button is always reachable. The "Settings" title is centered by flanking
|
||||||
@@ -4088,6 +4227,19 @@ mod tests {
|
|||||||
assert_eq!(format_duration(3661), "1:01:01");
|
assert_eq!(format_duration(3661), "1:01:01");
|
||||||
assert_eq!(format_duration(3725), "1:02:05");
|
assert_eq!(format_duration(3725), "1:02:05");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn settings_categories_are_stable_and_grouped_for_navigation() {
|
||||||
|
use super::SettingsCategory;
|
||||||
|
let labels: Vec<_> = SettingsCategory::ALL.iter().map(|c| c.label()).collect();
|
||||||
|
assert_eq!(
|
||||||
|
labels,
|
||||||
|
vec!["Audio", "Recording", "Profile", "Appearance", "Network", "Notifications"]
|
||||||
|
);
|
||||||
|
assert_eq!(SettingsCategory::Audio.hint(), "Devices, mic gate, echo");
|
||||||
|
assert_eq!(SettingsCategory::Profile.hint(), "Avatar and identity");
|
||||||
|
}
|
||||||
|
|
||||||
use super::{clamp_chat_height, clamp_participants_width, CHAT_MIN_H, PARTICIPANTS_MIN_W};
|
use super::{clamp_chat_height, clamp_participants_width, CHAT_MIN_H, PARTICIPANTS_MIN_W};
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
Reference in New Issue
Block a user