feat: redesign bookmarks and settings surfaces

Make the disconnected app flow feel more deliberate with a real bookmark management page, categorized settings navigation, and proper device dropdowns. This moves the UI closer to the Apple-style voice-first layout while leaving unrelated in-progress audio work untouched.
This commit is contained in:
ReTeamSpeak
2026-05-13 19:35:45 +09:00
parent 83346c337f
commit 73168ef965
4 changed files with 1052 additions and 414 deletions
+127 -72
View File
@@ -8,27 +8,27 @@ use std::collections::HashSet;
use std::sync::Arc; use std::sync::Arc;
use tokio::sync::{mpsc, Mutex}; use tokio::sync::{mpsc, Mutex};
use tsclientlib::sync::{SyncConnection, SyncConnectionHandle};
use tsclientlib::{ClientId, Connection, DisconnectOptions, Identity, MessageTarget};
use tsclientlib::events::{Event, PropertyId}; use tsclientlib::events::{Event, PropertyId};
use tsclientlib::prelude::*; use tsclientlib::prelude::*;
use tsclientlib::sync::{SyncConnection, SyncConnectionHandle};
use tsclientlib::{ClientId, Connection, DisconnectOptions, Identity, MessageTarget};
mod theme; mod audio;
mod icons; mod icons;
mod identity; mod identity;
mod noise_cancel;
mod persistence; mod persistence;
mod runtime; mod runtime;
mod theme;
mod types; mod types;
mod view; mod view;
mod audio;
mod noise_cancel;
use crate::persistence::{load_bookmarks, load_settings, persist_bookmarks, persist_settings};
use crate::runtime::{run_connection, start_transmission, stop_transmission}; use crate::runtime::{run_connection, start_transmission, stop_transmission};
use crate::types::{ use crate::types::{
AppSettings, BookmarkInfo, ChannelEntry, ChatMessage, ClientEntry, LocalClientAudio, Message, AppSettings, BookmarkInfo, ChannelEntry, ChatMessage, ClientEntry, LocalClientAudio, Message,
Page, TsEvent, Page, SettingsSection, TsEvent,
}; };
use crate::persistence::{load_bookmarks, load_settings, persist_bookmarks, persist_settings};
fn main() -> iced::Result { fn main() -> iced::Result {
iced::application("ReTeamSpeak", App::update, App::view) iced::application("ReTeamSpeak", App::update, App::view)
@@ -84,6 +84,7 @@ struct App {
afk: bool, afk: bool,
selected_output_device: String, selected_output_device: String,
selected_input_device: String, selected_input_device: String,
settings_section: SettingsSection,
} }
impl App { impl App {
@@ -147,6 +148,7 @@ impl App {
afk: false, afk: false,
selected_output_device, selected_output_device,
selected_input_device, selected_input_device,
settings_section: SettingsSection::Audio,
}; };
(app, Task::none()) (app, Task::none())
@@ -297,11 +299,9 @@ impl App {
async move { async move {
let (event_tx, event_rx) = mpsc::channel(100); let (event_tx, event_rx) = mpsc::channel(100);
*event_rx_store.lock().await = Some(event_rx); *event_rx_store.lock().await = Some(event_rx);
let mut builder = Connection::build(format!( let mut builder =
"{}:{}", Connection::build(format!("{}:{}", bookmark.address, bookmark.port))
bookmark.address, bookmark.port .name(nickname);
))
.name(nickname);
if let Some(pwd) = password { if let Some(pwd) = password {
builder = builder.password(pwd); builder = builder.password(pwd);
@@ -388,7 +388,11 @@ impl App {
let selected_device = self.selected_output_device.clone(); let selected_device = self.selected_output_device.clone();
tokio::spawn(async move { tokio::spawn(async move {
let mut playback = audio.lock().await; let mut playback = audio.lock().await;
let device = if selected_device.is_empty() { None } else { Some(selected_device.as_str()) }; let device = if selected_device.is_empty() {
None
} else {
Some(selected_device.as_str())
};
if let Err(e) = playback.start(device) { if let Err(e) = playback.start(device) {
tracing::warn!("Audio playback failed to start: {e}"); tracing::warn!("Audio playback failed to start: {e}");
} }
@@ -442,10 +446,13 @@ impl App {
Task::none() Task::none()
} }
Message::ToggleLocalClientMute(client_id) => { Message::ToggleLocalClientMute(client_id) => {
let entry = self.local_client_audio.entry(client_id).or_insert(LocalClientAudio { let entry = self
muted: false, .local_client_audio
volume: 1.0, .entry(client_id)
}); .or_insert(LocalClientAudio {
muted: false,
volume: 1.0,
});
entry.muted = !entry.muted; entry.muted = !entry.muted;
let audio = self.audio.clone(); let audio = self.audio.clone();
let muted = entry.muted; let muted = entry.muted;
@@ -455,10 +462,13 @@ impl App {
Task::none() Task::none()
} }
Message::IncreaseClientVolume(client_id) => { Message::IncreaseClientVolume(client_id) => {
let entry = self.local_client_audio.entry(client_id).or_insert(LocalClientAudio { let entry = self
muted: false, .local_client_audio
volume: 1.0, .entry(client_id)
}); .or_insert(LocalClientAudio {
muted: false,
volume: 1.0,
});
entry.volume = (entry.volume + 0.1).min(2.0); entry.volume = (entry.volume + 0.1).min(2.0);
let audio = self.audio.clone(); let audio = self.audio.clone();
let volume = entry.volume; let volume = entry.volume;
@@ -468,10 +478,13 @@ impl App {
Task::none() Task::none()
} }
Message::DecreaseClientVolume(client_id) => { Message::DecreaseClientVolume(client_id) => {
let entry = self.local_client_audio.entry(client_id).or_insert(LocalClientAudio { let entry = self
muted: false, .local_client_audio
volume: 1.0, .entry(client_id)
}); .or_insert(LocalClientAudio {
muted: false,
volume: 1.0,
});
entry.volume = (entry.volume - 0.1).max(0.0); entry.volume = (entry.volume - 0.1).max(0.0);
let audio = self.audio.clone(); let audio = self.audio.clone();
let volume = entry.volume; let volume = entry.volume;
@@ -519,9 +532,16 @@ impl App {
) )
} }
Message::PttPressed => { Message::PttPressed => {
if !self.ptt_active && self.connected && self.talk_mode == audio::TalkMode::PushToTalk { if !self.ptt_active
&& self.connected
&& self.talk_mode == audio::TalkMode::PushToTalk
{
self.ptt_active = true; self.ptt_active = true;
let device = if self.selected_input_device.is_empty() { None } else { Some(self.selected_input_device.clone()) }; let device = if self.selected_input_device.is_empty() {
None
} else {
Some(self.selected_input_device.clone())
};
start_transmission( start_transmission(
self.mic.clone(), self.mic.clone(),
self.handle.clone(), self.handle.clone(),
@@ -551,9 +571,16 @@ impl App {
Task::none() Task::none()
} }
Message::StartContinuous => { Message::StartContinuous => {
if !self.continuous_active && self.connected && self.talk_mode == audio::TalkMode::Continuous { if !self.continuous_active
&& self.connected
&& self.talk_mode == audio::TalkMode::Continuous
{
self.continuous_active = true; self.continuous_active = true;
let device = if self.selected_input_device.is_empty() { None } else { Some(self.selected_input_device.clone()) }; let device = if self.selected_input_device.is_empty() {
None
} else {
Some(self.selected_input_device.clone())
};
start_transmission( start_transmission(
self.mic.clone(), self.mic.clone(),
self.handle.clone(), self.handle.clone(),
@@ -643,8 +670,7 @@ impl App {
.with_connection(move |con| -> Result<(), String> { .with_connection(move |con| -> Result<(), String> {
let state = con.get_state().map_err(|e| e.to_string())?; let state = con.get_state().map_err(|e| e.to_string())?;
if state.clients.contains_key(&state.own_client) { if state.clients.contains_key(&state.own_client) {
let cmd = state.client_update() let cmd = state.client_update().set_input_muted(muted);
.set_input_muted(muted);
cmd.send(con).map_err(|e| e.to_string())?; cmd.send(con).map_err(|e| e.to_string())?;
} }
Ok(()) Ok(())
@@ -667,8 +693,7 @@ impl App {
.with_connection(move |con| -> Result<(), String> { .with_connection(move |con| -> Result<(), String> {
let state = con.get_state().map_err(|e| e.to_string())?; let state = con.get_state().map_err(|e| e.to_string())?;
if state.clients.contains_key(&state.own_client) { if state.clients.contains_key(&state.own_client) {
let cmd = state.client_update() let cmd = state.client_update().set_output_muted(muted);
.set_output_muted(muted);
cmd.send(con).map_err(|e| e.to_string())?; cmd.send(con).map_err(|e| e.to_string())?;
} }
Ok(()) Ok(())
@@ -691,7 +716,8 @@ impl App {
.with_connection(move |con| -> Result<(), String> { .with_connection(move |con| -> Result<(), String> {
let state = con.get_state().map_err(|e| e.to_string())?; let state = con.get_state().map_err(|e| e.to_string())?;
if state.clients.contains_key(&state.own_client) { if state.clients.contains_key(&state.own_client) {
let cmd = state.client_update() let cmd = state
.client_update()
.set_output_hardware_enabled(!muted); .set_output_hardware_enabled(!muted);
cmd.send(con).map_err(|e| e.to_string())?; cmd.send(con).map_err(|e| e.to_string())?;
} }
@@ -716,11 +742,13 @@ impl App {
let state = con.get_state().map_err(|e| e.to_string())?; let state = con.get_state().map_err(|e| e.to_string())?;
if state.clients.contains_key(&state.own_client) { if state.clients.contains_key(&state.own_client) {
let cmd = if afk { let cmd = if afk {
state.client_update() state
.client_update()
.set_input_muted(true) .set_input_muted(true)
.set_away(Some("AFK")) .set_away(Some("AFK"))
} else { } else {
state.client_update() state
.client_update()
.set_input_muted(false) .set_input_muted(false)
.set_away(Some("")) .set_away(Some(""))
}; };
@@ -789,6 +817,10 @@ impl App {
self.save_settings(); self.save_settings();
Task::none() Task::none()
} }
Message::SelectSettingsSection(section) => {
self.settings_section = section;
Task::none()
}
Message::SetAppearance(mode) => { Message::SetAppearance(mode) => {
self.appearance_mode = mode; self.appearance_mode = mode;
self.save_settings(); self.save_settings();
@@ -829,9 +861,7 @@ impl App {
_ => {} _ => {}
}, },
Event::Message { Event::Message {
invoker, invoker, message, ..
message,
..
} => { } => {
self.messages.push(ChatMessage { self.messages.push(ChatMessage {
invoker_name: invoker.name.clone(), invoker_name: invoker.name.clone(),
@@ -856,25 +886,35 @@ impl App {
state.server.version.clone(), state.server.version.clone(),
state.server.max_clients, state.server.max_clients,
state.own_client, state.own_client,
state.channels.values().map(|ch| ChannelEntry { state
id: ch.id, .channels
name: ch.name.clone(), .values()
parent: ch.parent, .map(|ch| ChannelEntry {
order: ch.order, id: ch.id,
}).collect::<Vec<_>>(), name: ch.name.clone(),
state.clients.values().map(|c| ClientEntry { parent: ch.parent,
id: c.id, order: ch.order,
name: c.name.clone(), })
channel: c.channel, .collect::<Vec<_>>(),
input_muted: c.input_muted, state
output_muted: c.output_muted, .clients
}).collect::<Vec<_>>(), .values()
.map(|c| ClientEntry {
id: c.id,
name: c.name.clone(),
channel: c.channel,
input_muted: c.input_muted,
output_muted: c.output_muted,
})
.collect::<Vec<_>>(),
)); ));
} }
None None
}) })
.await; .await;
if let Ok(Some((name, platform, version, max_clients, own_id, channels, clients))) = result { if let Ok(Some((name, platform, version, max_clients, own_id, channels, clients))) =
result
{
self.server_name = name; self.server_name = name;
self.server_platform = platform; self.server_platform = platform;
self.server_version = version; self.server_version = version;
@@ -896,13 +936,19 @@ impl App {
let result = handle let result = handle
.with_connection(|con| { .with_connection(|con| {
if let Ok(state) = con.get_state() { if let Ok(state) = con.get_state() {
return Some(state.clients.values().map(|c| ClientEntry { return Some(
id: c.id, state
name: c.name.clone(), .clients
channel: c.channel, .values()
input_muted: c.input_muted, .map(|c| ClientEntry {
output_muted: c.output_muted, id: c.id,
}).collect::<Vec<_>>()); name: c.name.clone(),
channel: c.channel,
input_muted: c.input_muted,
output_muted: c.output_muted,
})
.collect::<Vec<_>>(),
);
} }
None None
}) })
@@ -923,12 +969,18 @@ impl App {
let result = handle let result = handle
.with_connection(|con| { .with_connection(|con| {
if let Ok(state) = con.get_state() { if let Ok(state) = con.get_state() {
return Some(state.channels.values().map(|ch| ChannelEntry { return Some(
id: ch.id, state
name: ch.name.clone(), .channels
parent: ch.parent, .values()
order: ch.order, .map(|ch| ChannelEntry {
}).collect::<Vec<_>>()); id: ch.id,
name: ch.name.clone(),
parent: ch.parent,
order: ch.order,
})
.collect::<Vec<_>>(),
);
} }
None None
}) })
@@ -975,7 +1027,8 @@ impl App {
}; };
match event { match event {
Some(event) => { Some(event) => {
let is_disconnect = matches!(event, TsEvent::Disconnected | TsEvent::Error(_)); let is_disconnect =
matches!(event, TsEvent::Disconnected | TsEvent::Error(_));
if sender.send(Message::TsEvent(event)).await.is_err() { if sender.send(Message::TsEvent(event)).await.is_err() {
break; break;
} }
@@ -1028,10 +1081,12 @@ impl App {
let sidebar = self.view_sidebar(); let sidebar = self.view_sidebar();
let content = self.view_content(); let content = self.view_content();
row![sidebar, container(content).style(theme::main_panel_container)] row![
.width(Length::Fill) sidebar,
.height(Length::Fill) container(content).style(theme::main_panel_container)
.into() ]
.width(Length::Fill)
.height(Length::Fill)
.into()
} }
} }
+136 -33
View File
@@ -16,24 +16,104 @@ impl AppearanceMode {
} }
} }
const LIGHT_BG: Color = Color { r: 245.0 / 255.0, g: 245.0 / 255.0, b: 247.0 / 255.0, a: 1.0 }; const LIGHT_BG: Color = Color {
const LIGHT_SURFACE: Color = Color { r: 1.0, g: 1.0, b: 1.0, a: 1.0 }; r: 245.0 / 255.0,
const LIGHT_ELEVATED: Color = Color { r: 251.0 / 255.0, g: 251.0 / 255.0, b: 253.0 / 255.0, a: 1.0 }; g: 245.0 / 255.0,
const LIGHT_TEXT: Color = Color { r: 29.0 / 255.0, g: 29.0 / 255.0, b: 31.0 / 255.0, a: 1.0 }; b: 247.0 / 255.0,
const LIGHT_TEXT_SECONDARY: Color = Color { r: 110.0 / 255.0, g: 110.0 / 255.0, b: 115.0 / 255.0, a: 1.0 }; a: 1.0,
const LIGHT_BORDER: Color = Color { r: 210.0 / 255.0, g: 210.0 / 255.0, b: 215.0 / 255.0, a: 1.0 }; };
const LIGHT_ACCENT: Color = Color { r: 0.0, g: 122.0 / 255.0, b: 1.0, a: 1.0 }; const LIGHT_SURFACE: Color = Color {
r: 1.0,
g: 1.0,
b: 1.0,
a: 1.0,
};
const LIGHT_ELEVATED: Color = Color {
r: 251.0 / 255.0,
g: 251.0 / 255.0,
b: 253.0 / 255.0,
a: 1.0,
};
const LIGHT_TEXT: Color = Color {
r: 29.0 / 255.0,
g: 29.0 / 255.0,
b: 31.0 / 255.0,
a: 1.0,
};
const LIGHT_TEXT_SECONDARY: Color = Color {
r: 110.0 / 255.0,
g: 110.0 / 255.0,
b: 115.0 / 255.0,
a: 1.0,
};
const LIGHT_BORDER: Color = Color {
r: 210.0 / 255.0,
g: 210.0 / 255.0,
b: 215.0 / 255.0,
a: 1.0,
};
const LIGHT_ACCENT: Color = Color {
r: 0.0,
g: 122.0 / 255.0,
b: 1.0,
a: 1.0,
};
const DARK_BG: Color = Color { r: 0.0, g: 0.0, b: 0.0, a: 1.0 }; const DARK_BG: Color = Color {
const DARK_SURFACE: Color = Color { r: 28.0 / 255.0, g: 28.0 / 255.0, b: 30.0 / 255.0, a: 1.0 }; r: 11.0 / 255.0,
const DARK_ELEVATED: Color = Color { r: 44.0 / 255.0, g: 44.0 / 255.0, b: 46.0 / 255.0, a: 1.0 }; g: 11.0 / 255.0,
const DARK_TEXT: Color = Color { r: 245.0 / 255.0, g: 245.0 / 255.0, b: 247.0 / 255.0, a: 1.0 }; b: 13.0 / 255.0,
const DARK_TEXT_SECONDARY: Color = Color { r: 174.0 / 255.0, g: 174.0 / 255.0, b: 178.0 / 255.0, a: 1.0 }; a: 1.0,
const DARK_BORDER: Color = Color { r: 56.0 / 255.0, g: 56.0 / 255.0, b: 58.0 / 255.0, a: 1.0 }; };
const DARK_ACCENT: Color = Color { r: 10.0 / 255.0, g: 132.0 / 255.0, b: 1.0, a: 1.0 }; const DARK_SURFACE: Color = Color {
r: 28.0 / 255.0,
g: 28.0 / 255.0,
b: 31.0 / 255.0,
a: 1.0,
};
const DARK_ELEVATED: Color = Color {
r: 22.0 / 255.0,
g: 22.0 / 255.0,
b: 24.0 / 255.0,
a: 1.0,
};
const DARK_TEXT: Color = Color {
r: 245.0 / 255.0,
g: 245.0 / 255.0,
b: 247.0 / 255.0,
a: 1.0,
};
const DARK_TEXT_SECONDARY: Color = Color {
r: 161.0 / 255.0,
g: 161.0 / 255.0,
b: 170.0 / 255.0,
a: 1.0,
};
const DARK_BORDER: Color = Color {
r: 1.0,
g: 1.0,
b: 1.0,
a: 0.08,
};
const DARK_ACCENT: Color = Color {
r: 10.0 / 255.0,
g: 132.0 / 255.0,
b: 1.0,
a: 1.0,
};
const SUCCESS: Color = Color { r: 52.0 / 255.0, g: 199.0 / 255.0, b: 89.0 / 255.0, a: 1.0 }; const SUCCESS: Color = Color {
const DANGER: Color = Color { r: 1.0, g: 69.0 / 255.0, b: 58.0 / 255.0, a: 1.0 }; r: 52.0 / 255.0,
g: 199.0 / 255.0,
b: 89.0 / 255.0,
a: 1.0,
};
const DANGER: Color = Color {
r: 1.0,
g: 69.0 / 255.0,
b: 58.0 / 255.0,
a: 1.0,
};
pub fn light_theme() -> Theme { pub fn light_theme() -> Theme {
Theme::custom( Theme::custom(
@@ -66,15 +146,27 @@ fn is_dark(theme: &Theme) -> bool {
} }
fn surface(theme: &Theme) -> Color { fn surface(theme: &Theme) -> Color {
if is_dark(theme) { DARK_SURFACE } else { LIGHT_SURFACE } if is_dark(theme) {
DARK_SURFACE
} else {
LIGHT_SURFACE
}
} }
fn elevated(theme: &Theme) -> Color { fn elevated(theme: &Theme) -> Color {
if is_dark(theme) { DARK_ELEVATED } else { LIGHT_ELEVATED } if is_dark(theme) {
DARK_ELEVATED
} else {
LIGHT_ELEVATED
}
} }
fn text_secondary(theme: &Theme) -> Color { fn text_secondary(theme: &Theme) -> Color {
if is_dark(theme) { DARK_TEXT_SECONDARY } else { LIGHT_TEXT_SECONDARY } if is_dark(theme) {
DARK_TEXT_SECONDARY
} else {
LIGHT_TEXT_SECONDARY
}
} }
pub fn icon_color(theme: &Theme) -> Color { pub fn icon_color(theme: &Theme) -> Color {
@@ -86,7 +178,11 @@ pub fn nav_icon_color(theme: &Theme) -> Color {
} }
fn border(theme: &Theme) -> Color { fn border(theme: &Theme) -> Color {
if is_dark(theme) { DARK_BORDER } else { LIGHT_BORDER } if is_dark(theme) {
DARK_BORDER
} else {
LIGHT_BORDER
}
} }
fn accent(theme: &Theme) -> Color { fn accent(theme: &Theme) -> Color {
@@ -95,12 +191,15 @@ fn accent(theme: &Theme) -> Color {
fn accent_hover(theme: &Theme) -> Color { fn accent_hover(theme: &Theme) -> Color {
let primary = accent(theme); let primary = accent(theme);
Color { a: primary.a, ..Color::from_rgba( Color {
(primary.r + 0.05).min(1.0), a: primary.a,
(primary.g + 0.05).min(1.0), ..Color::from_rgba(
(primary.b + 0.05).min(1.0), (primary.r + 0.05).min(1.0),
1.0, (primary.g + 0.05).min(1.0),
) } (primary.b + 0.05).min(1.0),
1.0,
)
}
} }
pub fn sidebar_container(theme: &Theme) -> container::Style { pub fn sidebar_container(theme: &Theme) -> container::Style {
@@ -108,7 +207,7 @@ pub fn sidebar_container(theme: &Theme) -> container::Style {
background: Some(Background::Color(elevated(theme))), background: Some(Background::Color(elevated(theme))),
border: Border { border: Border {
color: border(theme), color: border(theme),
width: 0.0, width: if is_dark(theme) { 1.0 } else { 0.0 },
radius: 0.0.into(), radius: 0.0.into(),
}, },
..container::Style::default() ..container::Style::default()
@@ -213,7 +312,11 @@ pub fn input_style(theme: &Theme, status: text_input::Status) -> text_input::Sty
background: Background::Color(elevated(theme)), background: Background::Color(elevated(theme)),
border: Border { border: Border {
color: base, color: base,
width: if matches!(status, text_input::Status::Focused) { 1.5 } else { 1.0 }, width: if matches!(status, text_input::Status::Focused) {
1.5
} else {
1.0
},
radius: 12.0.into(), radius: 12.0.into(),
}, },
icon: text_secondary(theme), icon: text_secondary(theme),
@@ -237,7 +340,7 @@ pub fn primary_button(theme: &Theme, status: button::Status) -> button::Style {
border: Border { border: Border {
color: Color::TRANSPARENT, color: Color::TRANSPARENT,
width: 0.0, width: 0.0,
radius: 12.0.into(), radius: 999.0.into(),
}, },
shadow: Shadow { shadow: Shadow {
color: Color::from_rgba(0.0, 0.0, 0.0, if is_dark(theme) { 0.20 } else { 0.10 }), color: Color::from_rgba(0.0, 0.0, 0.0, if is_dark(theme) { 0.20 } else { 0.10 }),
@@ -260,7 +363,7 @@ pub fn secondary_button(theme: &Theme, status: button::Status) -> button::Style
border: Border { border: Border {
color: border(theme), color: border(theme),
width: 1.0, width: 1.0,
radius: 12.0.into(), radius: 999.0.into(),
}, },
shadow: Shadow::default(), shadow: Shadow::default(),
} }
@@ -273,7 +376,7 @@ pub fn danger_button(_theme: &Theme, _status: button::Status) -> button::Style {
border: Border { border: Border {
color: Color::TRANSPARENT, color: Color::TRANSPARENT,
width: 0.0, width: 0.0,
radius: 12.0.into(), radius: 999.0.into(),
}, },
shadow: Shadow::default(), shadow: Shadow::default(),
} }
@@ -286,7 +389,7 @@ pub fn nav_button_active(theme: &Theme, _status: button::Status) -> button::Styl
border: Border { border: Border {
color: Color::TRANSPARENT, color: Color::TRANSPARENT,
width: 0.0, width: 0.0,
radius: 12.0.into(), radius: 999.0.into(),
}, },
shadow: Shadow::default(), shadow: Shadow::default(),
} }
@@ -309,7 +412,7 @@ pub fn nav_button_inactive(theme: &Theme, status: button::Status) -> button::Sty
border: Border { border: Border {
color: Color::TRANSPARENT, color: Color::TRANSPARENT,
width: 0.0, width: 0.0,
radius: 12.0.into(), radius: 999.0.into(),
}, },
shadow: Shadow::default(), shadow: Shadow::default(),
} }
+46 -1
View File
@@ -2,8 +2,8 @@ use serde::{Deserialize, Serialize};
use tsclientlib::events::Event; use tsclientlib::events::Event;
use tsclientlib::{ChannelId, ClientId, Identity}; use tsclientlib::{ChannelId, ClientId, Identity};
use crate::{audio, noise_cancel};
use crate::theme; use crate::theme;
use crate::{audio, noise_cancel};
#[derive(Debug, Clone, Copy, PartialEq, Eq)] #[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum Page { pub(crate) enum Page {
@@ -13,6 +13,50 @@ pub(crate) enum Page {
Settings, Settings,
} }
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum SettingsSection {
General,
Audio,
Capture,
Playback,
Hotkeys,
Notifications,
Appearance,
Chat,
Identities,
Advanced,
}
impl SettingsSection {
pub(crate) const ALL: [Self; 10] = [
Self::General,
Self::Audio,
Self::Capture,
Self::Playback,
Self::Hotkeys,
Self::Notifications,
Self::Appearance,
Self::Chat,
Self::Identities,
Self::Advanced,
];
pub(crate) fn label(self) -> &'static str {
match self {
Self::General => "General",
Self::Audio => "Audio",
Self::Capture => "Capture",
Self::Playback => "Playback",
Self::Hotkeys => "Hotkeys",
Self::Notifications => "Notifications",
Self::Appearance => "Appearance",
Self::Chat => "Chat",
Self::Identities => "Identities",
Self::Advanced => "Advanced",
}
}
}
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
#[allow(clippy::enum_variant_names)] #[allow(clippy::enum_variant_names)]
pub(crate) enum Message { pub(crate) enum Message {
@@ -58,6 +102,7 @@ pub(crate) enum Message {
IdentityImported(Result<Identity, String>), IdentityImported(Result<Identity, String>),
SetOutputDevice(String), SetOutputDevice(String),
SetInputDevice(String), SetInputDevice(String),
SelectSettingsSection(SettingsSection),
SetAppearance(theme::AppearanceMode), SetAppearance(theme::AppearanceMode),
Noop, Noop,
} }
+743 -308
View File
File diff suppressed because it is too large Load Diff