Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7724da73b8 |
+144
-234
@@ -31,51 +31,21 @@ pub enum Screen {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
pub enum SettingsCategory {
|
enum HomeLayoutMode {
|
||||||
Audio,
|
FocusedEmpty,
|
||||||
Recording,
|
ThreeColumn,
|
||||||
Profile,
|
Stacked,
|
||||||
Appearance,
|
|
||||||
Network,
|
|
||||||
Notifications,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SettingsCategory {
|
fn home_layout_mode(width: f32, has_recents: bool, has_friends: bool) -> HomeLayoutMode {
|
||||||
const ALL: [SettingsCategory; 6] = [
|
if width < 900.0 {
|
||||||
SettingsCategory::Audio,
|
HomeLayoutMode::Stacked
|
||||||
SettingsCategory::Recording,
|
} else if !has_recents && !has_friends {
|
||||||
SettingsCategory::Profile,
|
HomeLayoutMode::FocusedEmpty
|
||||||
SettingsCategory::Appearance,
|
} else if width >= 1280.0 {
|
||||||
SettingsCategory::Network,
|
HomeLayoutMode::ThreeColumn
|
||||||
SettingsCategory::Notifications,
|
} else {
|
||||||
];
|
HomeLayoutMode::Stacked
|
||||||
|
|
||||||
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())
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -210,7 +180,6 @@ 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),
|
||||||
@@ -326,7 +295,6 @@ 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.
|
||||||
@@ -452,7 +420,6 @@ 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,
|
||||||
@@ -987,9 +954,6 @@ 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();
|
||||||
@@ -1390,7 +1354,7 @@ fn connect_card(state: &AppState) -> Element<'_, AppMessage> {
|
|||||||
selection: color_blue,
|
selection: color_blue,
|
||||||
};
|
};
|
||||||
|
|
||||||
let logo = text("PEERSPEAK").size(36).color(color_blue);
|
let logo = text("PEERSPEAK").size(38).color(color_blue);
|
||||||
let subtitle = text("NAT-traversing full-mesh voice chat").size(16).color(color_subtext);
|
let subtitle = text("NAT-traversing full-mesh voice chat").size(16).color(color_subtext);
|
||||||
|
|
||||||
let nickname_input = column![
|
let nickname_input = column![
|
||||||
@@ -1454,8 +1418,8 @@ fn connect_card(state: &AppState) -> Element<'_, AppMessage> {
|
|||||||
.align_x(iced::alignment::Horizontal::Center),
|
.align_x(iced::alignment::Horizontal::Center),
|
||||||
)
|
)
|
||||||
.style(c_style(color_mantle, color_surface, 12.0))
|
.style(c_style(color_mantle, color_surface, 12.0))
|
||||||
.padding(30)
|
.padding(32)
|
||||||
.width(380)
|
.width(420)
|
||||||
.into()
|
.into()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1499,18 +1463,20 @@ fn recents_card(state: &AppState) -> Element<'_, AppMessage> {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let empty = state.config.recents.is_empty();
|
||||||
|
let content: Element<'_, AppMessage> = if empty {
|
||||||
|
column![
|
||||||
|
text("RECENT ROOMS").size(14).color(color_subtext),
|
||||||
|
text("No recent rooms yet.").size(12).color(color_subtext),
|
||||||
|
]
|
||||||
|
.spacing(4)
|
||||||
|
.into()
|
||||||
|
} else {
|
||||||
let now = std::time::SystemTime::now()
|
let now = std::time::SystemTime::now()
|
||||||
.duration_since(std::time::UNIX_EPOCH)
|
.duration_since(std::time::UNIX_EPOCH)
|
||||||
.map(|d| d.as_secs())
|
.map(|d| d.as_secs())
|
||||||
.unwrap_or(0);
|
.unwrap_or(0);
|
||||||
let mut rows = column![].spacing(6).width(iced::Length::Fill);
|
let mut rows = column![].spacing(6).width(iced::Length::Fill);
|
||||||
if state.config.recents.is_empty() {
|
|
||||||
rows = rows.push(
|
|
||||||
text("No recent rooms yet — they'll appear here after you join one.")
|
|
||||||
.size(12)
|
|
||||||
.color(color_subtext),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
for r in &state.config.recents {
|
for r in &state.config.recents {
|
||||||
let label = {
|
let label = {
|
||||||
let n = crate::sanitize::sanitize_name(&r.name);
|
let n = crate::sanitize::sanitize_name(&r.name);
|
||||||
@@ -1542,7 +1508,6 @@ fn recents_card(state: &AppState) -> Element<'_, AppMessage> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
container(
|
|
||||||
column![
|
column![
|
||||||
text("RECENT ROOMS").size(18).color(color_text),
|
text("RECENT ROOMS").size(18).color(color_text),
|
||||||
text("Rooms you've been in — click to hop back. Best-effort: only works while someone's still there.")
|
text("Rooms you've been in — click to hop back. Best-effort: only works while someone's still there.")
|
||||||
@@ -1551,11 +1516,14 @@ fn recents_card(state: &AppState) -> Element<'_, AppMessage> {
|
|||||||
vertical_space(10.0),
|
vertical_space(10.0),
|
||||||
rows,
|
rows,
|
||||||
]
|
]
|
||||||
.spacing(6),
|
.spacing(6)
|
||||||
)
|
.into()
|
||||||
.style(c_style(color_mantle, color_surface, 12.0))
|
};
|
||||||
.padding(24)
|
|
||||||
.width(380)
|
container(content)
|
||||||
|
.style(c_style(if empty { color_crust } else { color_mantle }, color_surface, 8.0))
|
||||||
|
.padding(if empty { 16 } else { 24 })
|
||||||
|
.width(if empty { 340 } else { 380 })
|
||||||
.into()
|
.into()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1571,6 +1539,7 @@ fn friends_panel(state: &AppState) -> Element<'_, AppMessage> {
|
|||||||
let color_red = pal.red;
|
let color_red = pal.red;
|
||||||
let color_maroon = pal.maroon;
|
let color_maroon = pal.maroon;
|
||||||
let color_green = pal.green;
|
let color_green = pal.green;
|
||||||
|
let has_friends = !state.friends.list().is_empty();
|
||||||
|
|
||||||
let c_style = move |bg: Color, b_color: Color, radius: f32| {
|
let c_style = move |bg: Color, b_color: Color, radius: f32| {
|
||||||
move |_theme: &Theme| container::Style {
|
move |_theme: &Theme| container::Style {
|
||||||
@@ -1609,9 +1578,9 @@ fn friends_panel(state: &AppState) -> Element<'_, AppMessage> {
|
|||||||
|
|
||||||
// The live friends list: status dot, inline rename, short id, remove.
|
// The live friends list: status dot, inline rename, short id, remove.
|
||||||
let mut friend_rows = column![].spacing(6).width(iced::Length::Fill);
|
let mut friend_rows = column![].spacing(6).width(iced::Length::Fill);
|
||||||
if state.friends.list().is_empty() {
|
if !has_friends {
|
||||||
friend_rows = friend_rows.push(
|
friend_rows = friend_rows.push(
|
||||||
text("No friends yet — add one by their node ID below.")
|
text("No friends yet.")
|
||||||
.size(12)
|
.size(12)
|
||||||
.color(color_subtext),
|
.color(color_subtext),
|
||||||
);
|
);
|
||||||
@@ -1714,28 +1683,34 @@ fn friends_panel(state: &AppState) -> Element<'_, AppMessage> {
|
|||||||
]
|
]
|
||||||
.spacing(4)
|
.spacing(4)
|
||||||
.width(iced::Length::Fill);
|
.width(iced::Length::Fill);
|
||||||
|
let intro: Element<'_, AppMessage> = if has_friends {
|
||||||
|
text("Who's online — click Join to hop into a friend's room.")
|
||||||
|
.size(11)
|
||||||
|
.color(color_subtext)
|
||||||
|
.into()
|
||||||
|
} else {
|
||||||
|
column![].into()
|
||||||
|
};
|
||||||
|
|
||||||
container(
|
container(
|
||||||
column![
|
column![
|
||||||
text("FRIENDS").size(18).color(color_text),
|
text("FRIENDS").size(if has_friends { 18 } else { 14 }).color(color_text),
|
||||||
text("Who's online — click Join to hop into a friend's room.")
|
intro,
|
||||||
.size(11)
|
vertical_space(if has_friends { 10.0 } else { 4.0 }),
|
||||||
.color(color_subtext),
|
|
||||||
vertical_space(10.0),
|
|
||||||
readonly_warning,
|
readonly_warning,
|
||||||
friend_rows,
|
friend_rows,
|
||||||
vertical_space(12.0),
|
vertical_space(if has_friends { 12.0 } else { 8.0 }),
|
||||||
text("Add a friend").size(13).color(color_subtext),
|
text("Add a friend").size(13).color(color_subtext),
|
||||||
add_form,
|
add_form,
|
||||||
vertical_space(14.0),
|
vertical_space(if has_friends { 14.0 } else { 10.0 }),
|
||||||
text("Your presence").size(13).color(color_subtext),
|
text("Your presence").size(13).color(color_subtext),
|
||||||
presence_picker,
|
presence_picker,
|
||||||
]
|
]
|
||||||
.spacing(6),
|
.spacing(6),
|
||||||
)
|
)
|
||||||
.style(c_style(color_mantle, color_surface, 12.0))
|
.style(c_style(color_mantle, color_surface, 12.0))
|
||||||
.padding(24)
|
.padding(if has_friends { 24 } else { 18 })
|
||||||
.width(460)
|
.width(if has_friends { 460 } else { 360 })
|
||||||
.into()
|
.into()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1805,8 +1780,9 @@ fn view(state: &AppState) -> Element<'_, AppMessage> {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let top_bar = row![
|
let layout_button: Element<'_, AppMessage> = if state.current_screen == Screen::Home {
|
||||||
horizontal_space(),
|
iced::widget::Space::new().width(0.0).height(0.0).into()
|
||||||
|
} else {
|
||||||
tooltip(
|
tooltip(
|
||||||
button(
|
button(
|
||||||
Canvas::new(LayoutIcon { fg: color_text })
|
Canvas::new(LayoutIcon { fg: color_text })
|
||||||
@@ -1821,7 +1797,13 @@ fn view(state: &AppState) -> Element<'_, AppMessage> {
|
|||||||
.style(c_style(color_crust, color_surface, 6.0)),
|
.style(c_style(color_crust, color_surface, 6.0)),
|
||||||
iced::widget::tooltip::Position::Bottom,
|
iced::widget::tooltip::Position::Bottom,
|
||||||
)
|
)
|
||||||
.gap(8),
|
.gap(8)
|
||||||
|
.into()
|
||||||
|
};
|
||||||
|
|
||||||
|
let top_bar = row![
|
||||||
|
horizontal_space(),
|
||||||
|
layout_button,
|
||||||
button(
|
button(
|
||||||
row![
|
row![
|
||||||
icon(IconKind::Settings, 15.0, color_text),
|
icon(IconKind::Settings, 15.0, color_text),
|
||||||
@@ -2173,8 +2155,9 @@ 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_body: Element<'_, AppMessage> = match state.settings_category {
|
let settings_content = scrollable(
|
||||||
SettingsCategory::Audio => column![
|
column![
|
||||||
|
// --- Audio Devices ---
|
||||||
section_header("Audio Devices"),
|
section_header("Audio Devices"),
|
||||||
row![
|
row![
|
||||||
column![
|
column![
|
||||||
@@ -2203,6 +2186,8 @@ 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,
|
||||||
@@ -2213,11 +2198,9 @@ 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)
|
|
||||||
.width(iced::Length::Fill)
|
// --- Recording ---
|
||||||
.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)"),
|
||||||
@@ -2226,38 +2209,9 @@ 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"),
|
|
||||||
identity_section,
|
// --- Network & Privacy ---
|
||||||
]
|
|
||||||
.spacing(10)
|
|
||||||
.width(iced::Length::Fill)
|
|
||||||
.into(),
|
|
||||||
SettingsCategory::Appearance => column![
|
|
||||||
section_header("Room Layout"),
|
|
||||||
column![
|
|
||||||
row![
|
|
||||||
layout_choice(RoomLayout::ThreeColumn, "3-Column"),
|
|
||||||
layout_choice(RoomLayout::BottomDock, "Bottom Dock"),
|
|
||||||
layout_choice(RoomLayout::Drawer, "Drawer"),
|
|
||||||
].spacing(16),
|
|
||||||
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),
|
|
||||||
section_header("Theme"),
|
|
||||||
theme_section,
|
|
||||||
]
|
|
||||||
.spacing(10)
|
|
||||||
.width(iced::Length::Fill)
|
|
||||||
.into(),
|
|
||||||
SettingsCategory::Network => column![
|
|
||||||
section_header("Network & Privacy"),
|
section_header("Network & Privacy"),
|
||||||
column![
|
column![
|
||||||
pick_list(
|
pick_list(
|
||||||
@@ -2268,11 +2222,38 @@ fn view(state: &AppState) -> Element<'_, AppMessage> {
|
|||||||
text(network_mode_hint(state.config.network_mode)).size(11).color(color_subtext),
|
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),
|
text("Takes effect on your next room join.").size(11).color(color_subtext),
|
||||||
].spacing(4).width(iced::Length::Fill),
|
].spacing(4).width(iced::Length::Fill),
|
||||||
]
|
vertical_space(section_gap),
|
||||||
.spacing(10)
|
|
||||||
.width(iced::Length::Fill)
|
// --- Room Layout ---
|
||||||
.into(),
|
section_header("Room Layout"),
|
||||||
SettingsCategory::Notifications => column![
|
column![
|
||||||
|
row![
|
||||||
|
layout_choice(RoomLayout::ThreeColumn, "3-Column"),
|
||||||
|
layout_choice(RoomLayout::BottomDock, "Bottom Dock"),
|
||||||
|
layout_choice(RoomLayout::Drawer, "Drawer"),
|
||||||
|
].spacing(16),
|
||||||
|
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 ---
|
||||||
section_header("Notifications & Sounds"),
|
section_header("Notifications & Sounds"),
|
||||||
column![
|
column![
|
||||||
checkbox(state.config.notifications_enabled)
|
checkbox(state.config.notifications_enabled)
|
||||||
@@ -2300,92 +2281,9 @@ fn view(state: &AppState) -> Element<'_, AppMessage> {
|
|||||||
]
|
]
|
||||||
.spacing(10)
|
.spacing(10)
|
||||||
.width(iced::Length::Fill)
|
.width(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)
|
.width(iced::Length::Fill)
|
||||||
.height(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
|
||||||
@@ -2445,28 +2343,43 @@ fn view(state: &AppState) -> Element<'_, AppMessage> {
|
|||||||
|
|
||||||
if state.current_screen == Screen::Home {
|
if state.current_screen == Screen::Home {
|
||||||
// --- HOME SCREEN ---
|
// --- HOME SCREEN ---
|
||||||
// Two cards: Connect (left) + the live Friends list (right). They sit
|
// Keep Create/Join dominant on a fresh install. Once Recents or Friends
|
||||||
// side-by-side when the window is wide enough, and stack vertically on a
|
// has real content, the wider three-card layout returns.
|
||||||
// narrow window so the Friends card never gets crushed — below ~860px the
|
let has_recents = !state.config.recents.is_empty();
|
||||||
// fixed-width Connect card would otherwise squeeze it until its node-ID
|
let has_friends = !state.friends.list().is_empty();
|
||||||
// field and remove button clip away. `responsive` measures the available
|
|
||||||
// width each layout pass and picks the orientation accordingly.
|
|
||||||
// Three cards: Recents | Connect | Friends, side-by-side when there's room.
|
|
||||||
// Three 380–460px cards need ~1280px to fit in a row, so below that the
|
|
||||||
// `responsive` measure stacks them in a column (Connect first — the primary
|
|
||||||
// action) rather than letting the row clip. Recents always shows (empty-
|
|
||||||
// state hint when no history) for parity with the Friends card.
|
|
||||||
let body = responsive(move |size| {
|
let body = responsive(move |size| {
|
||||||
let cards: Element<AppMessage> = if size.width < 1280.0 {
|
let cards: Element<AppMessage> =
|
||||||
column![connect_card(state), recents_card(state), friends_panel(state)]
|
match home_layout_mode(size.width, has_recents, has_friends) {
|
||||||
.spacing(20)
|
HomeLayoutMode::FocusedEmpty => row![
|
||||||
.align_x(iced::alignment::Horizontal::Center)
|
connect_card(state),
|
||||||
.into()
|
column![friends_panel(state), recents_card(state)]
|
||||||
} else {
|
.spacing(16)
|
||||||
row![recents_card(state), connect_card(state), friends_panel(state)]
|
.width(iced::Length::Fixed(360.0)),
|
||||||
|
]
|
||||||
|
.spacing(22)
|
||||||
|
.align_y(iced::alignment::Vertical::Top)
|
||||||
|
.into(),
|
||||||
|
HomeLayoutMode::ThreeColumn => row![
|
||||||
|
recents_card(state),
|
||||||
|
connect_card(state),
|
||||||
|
friends_panel(state),
|
||||||
|
]
|
||||||
.spacing(20)
|
.spacing(20)
|
||||||
.align_y(iced::alignment::Vertical::Top)
|
.align_y(iced::alignment::Vertical::Top)
|
||||||
.into()
|
.into(),
|
||||||
|
HomeLayoutMode::Stacked => {
|
||||||
|
let mut stack = column![connect_card(state)]
|
||||||
|
.spacing(20)
|
||||||
|
.align_x(iced::alignment::Horizontal::Center);
|
||||||
|
if has_recents {
|
||||||
|
stack = stack.push(recents_card(state));
|
||||||
|
}
|
||||||
|
stack = stack.push(friends_panel(state));
|
||||||
|
if !has_recents {
|
||||||
|
stack = stack.push(recents_card(state));
|
||||||
|
}
|
||||||
|
stack.into()
|
||||||
|
}
|
||||||
};
|
};
|
||||||
scrollable(container(cards).center_x(iced::Length::Fill))
|
scrollable(container(cards).center_x(iced::Length::Fill))
|
||||||
.width(iced::Length::Fill)
|
.width(iced::Length::Fill)
|
||||||
@@ -4229,15 +4142,12 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn settings_categories_are_stable_and_grouped_for_navigation() {
|
fn home_layout_prioritizes_connect_on_empty_home() {
|
||||||
use super::SettingsCategory;
|
use super::{home_layout_mode, HomeLayoutMode};
|
||||||
let labels: Vec<_> = SettingsCategory::ALL.iter().map(|c| c.label()).collect();
|
assert_eq!(home_layout_mode(1280.0, false, false), HomeLayoutMode::FocusedEmpty);
|
||||||
assert_eq!(
|
assert_eq!(home_layout_mode(760.0, false, false), HomeLayoutMode::Stacked);
|
||||||
labels,
|
assert_eq!(home_layout_mode(1280.0, true, false), HomeLayoutMode::ThreeColumn);
|
||||||
vec!["Audio", "Recording", "Profile", "Appearance", "Network", "Notifications"]
|
assert_eq!(home_layout_mode(1100.0, true, true), HomeLayoutMode::Stacked);
|
||||||
);
|
|
||||||
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};
|
||||||
|
|||||||
Reference in New Issue
Block a user