Add settings category navigation
This commit is contained in:
+195
-43
@@ -30,6 +30,55 @@ pub enum Screen {
|
||||
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)
|
||||
/// messages from peers' for colouring.
|
||||
#[derive(Debug, Clone)]
|
||||
@@ -161,6 +210,7 @@ pub enum AppMessage {
|
||||
EventOccurred(Event),
|
||||
NavigateToSettings,
|
||||
NavigateBack,
|
||||
SelectSettingsCategory(SettingsCategory),
|
||||
ToggleNotifications(bool),
|
||||
ToggleEchoCancellation(bool),
|
||||
CustomSoundPathChanged(Sound, String),
|
||||
@@ -276,6 +326,7 @@ pub struct AppState {
|
||||
ever_connected: HashSet<EndpointId>,
|
||||
controller: Arc<CoreController>,
|
||||
current_screen: Screen,
|
||||
settings_category: SettingsCategory,
|
||||
/// Whether we're currently sharing our own screen (confirmed by the core).
|
||||
self_sharing: bool,
|
||||
/// Whether the `pixelpass` binary is available, gating the Share controls.
|
||||
@@ -401,6 +452,7 @@ impl Default for AppState {
|
||||
ever_connected: HashSet::new(),
|
||||
controller,
|
||||
current_screen: Screen::Home,
|
||||
settings_category: SettingsCategory::Audio,
|
||||
self_sharing: false,
|
||||
pixelpass_available,
|
||||
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);
|
||||
state.config.save();
|
||||
}
|
||||
AppMessage::SelectSettingsCategory(category) => {
|
||||
state.settings_category = category;
|
||||
}
|
||||
AppMessage::ToggleNotifications(enabled) => {
|
||||
state.config.notifications_enabled = enabled;
|
||||
state.config.save();
|
||||
@@ -2118,9 +2173,8 @@ fn view(state: &AppState) -> Element<'_, AppMessage> {
|
||||
|
||||
// Presence + Friends moved to the home screen (see `friends_panel`).
|
||||
|
||||
let settings_content = scrollable(
|
||||
column![
|
||||
// --- Audio Devices ---
|
||||
let settings_body: Element<'_, AppMessage> = match state.settings_category {
|
||||
SettingsCategory::Audio => column![
|
||||
section_header("Audio Devices"),
|
||||
row![
|
||||
column![
|
||||
@@ -2149,8 +2203,6 @@ fn view(state: &AppState) -> Element<'_, AppMessage> {
|
||||
].spacing(8).width(iced::Length::Fill),
|
||||
].spacing(20).align_y(iced::alignment::Vertical::Top).width(iced::Length::Fill),
|
||||
vertical_space(section_gap),
|
||||
|
||||
// --- Microphone ---
|
||||
section_header("Microphone"),
|
||||
column![
|
||||
mic_meter,
|
||||
@@ -2161,9 +2213,11 @@ fn view(state: &AppState) -> Element<'_, AppMessage> {
|
||||
.on_toggle(AppMessage::ToggleEchoCancellation),
|
||||
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),
|
||||
vertical_space(section_gap),
|
||||
|
||||
// --- Recording ---
|
||||
]
|
||||
.spacing(10)
|
||||
.width(iced::Length::Fill)
|
||||
.into(),
|
||||
SettingsCategory::Recording => column![
|
||||
section_header("Recording"),
|
||||
column![
|
||||
mode_radio(RecordingMode::Mixed, "Mixed (single file)"),
|
||||
@@ -2172,22 +2226,21 @@ fn view(state: &AppState) -> Element<'_, AppMessage> {
|
||||
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),
|
||||
].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),
|
||||
|
||||
// --- Network & Privacy ---
|
||||
section_header("Network & Privacy"),
|
||||
column![
|
||||
pick_list(
|
||||
&NetworkMode::ALL[..],
|
||||
Some(state.config.network_mode),
|
||||
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("Identity"),
|
||||
identity_section,
|
||||
]
|
||||
.spacing(10)
|
||||
.width(iced::Length::Fill)
|
||||
.into(),
|
||||
SettingsCategory::Appearance => column![
|
||||
section_header("Room Layout"),
|
||||
column![
|
||||
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),
|
||||
].spacing(8).width(iced::Length::Fill),
|
||||
vertical_space(section_gap),
|
||||
|
||||
// --- Theme ---
|
||||
section_header("Theme"),
|
||||
theme_section,
|
||||
vertical_space(section_gap),
|
||||
|
||||
// --- Avatar ---
|
||||
section_header("Avatar"),
|
||||
avatar_section,
|
||||
vertical_space(section_gap),
|
||||
|
||||
// --- Identity ---
|
||||
section_header("Identity"),
|
||||
identity_section,
|
||||
vertical_space(section_gap),
|
||||
|
||||
// (Presence + Friends now live on the home screen.)
|
||||
|
||||
// --- Notifications & Sounds ---
|
||||
]
|
||||
.spacing(10)
|
||||
.width(iced::Length::Fill)
|
||||
.into(),
|
||||
SettingsCategory::Network => column![
|
||||
section_header("Network & Privacy"),
|
||||
column![
|
||||
pick_list(
|
||||
&NetworkMode::ALL[..],
|
||||
Some(state.config.network_mode),
|
||||
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),
|
||||
]
|
||||
.spacing(10)
|
||||
.width(iced::Length::Fill)
|
||||
.into(),
|
||||
SettingsCategory::Notifications => column![
|
||||
section_header("Notifications & Sounds"),
|
||||
column![
|
||||
checkbox(state.config.notifications_enabled)
|
||||
@@ -2244,9 +2300,92 @@ fn view(state: &AppState) -> Element<'_, AppMessage> {
|
||||
]
|
||||
.spacing(10)
|
||||
.width(iced::Length::Fill)
|
||||
)
|
||||
.width(iced::Length::Fill)
|
||||
.height(iced::Length::Fill);
|
||||
.into(),
|
||||
};
|
||||
|
||||
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
|
||||
// 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(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};
|
||||
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user