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:
+127
-72
@@ -8,27 +8,27 @@ use std::collections::HashSet;
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::{mpsc, Mutex};
|
||||
|
||||
use tsclientlib::sync::{SyncConnection, SyncConnectionHandle};
|
||||
use tsclientlib::{ClientId, Connection, DisconnectOptions, Identity, MessageTarget};
|
||||
use tsclientlib::events::{Event, PropertyId};
|
||||
use tsclientlib::prelude::*;
|
||||
use tsclientlib::sync::{SyncConnection, SyncConnectionHandle};
|
||||
use tsclientlib::{ClientId, Connection, DisconnectOptions, Identity, MessageTarget};
|
||||
|
||||
mod theme;
|
||||
mod audio;
|
||||
mod icons;
|
||||
mod identity;
|
||||
mod noise_cancel;
|
||||
mod persistence;
|
||||
mod runtime;
|
||||
mod theme;
|
||||
mod types;
|
||||
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::types::{
|
||||
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 {
|
||||
iced::application("ReTeamSpeak", App::update, App::view)
|
||||
@@ -84,6 +84,7 @@ struct App {
|
||||
afk: bool,
|
||||
selected_output_device: String,
|
||||
selected_input_device: String,
|
||||
settings_section: SettingsSection,
|
||||
}
|
||||
|
||||
impl App {
|
||||
@@ -147,6 +148,7 @@ impl App {
|
||||
afk: false,
|
||||
selected_output_device,
|
||||
selected_input_device,
|
||||
settings_section: SettingsSection::Audio,
|
||||
};
|
||||
|
||||
(app, Task::none())
|
||||
@@ -297,11 +299,9 @@ impl App {
|
||||
async move {
|
||||
let (event_tx, event_rx) = mpsc::channel(100);
|
||||
*event_rx_store.lock().await = Some(event_rx);
|
||||
let mut builder = Connection::build(format!(
|
||||
"{}:{}",
|
||||
bookmark.address, bookmark.port
|
||||
))
|
||||
.name(nickname);
|
||||
let mut builder =
|
||||
Connection::build(format!("{}:{}", bookmark.address, bookmark.port))
|
||||
.name(nickname);
|
||||
|
||||
if let Some(pwd) = password {
|
||||
builder = builder.password(pwd);
|
||||
@@ -388,7 +388,11 @@ impl App {
|
||||
let selected_device = self.selected_output_device.clone();
|
||||
tokio::spawn(async move {
|
||||
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) {
|
||||
tracing::warn!("Audio playback failed to start: {e}");
|
||||
}
|
||||
@@ -442,10 +446,13 @@ impl App {
|
||||
Task::none()
|
||||
}
|
||||
Message::ToggleLocalClientMute(client_id) => {
|
||||
let entry = self.local_client_audio.entry(client_id).or_insert(LocalClientAudio {
|
||||
muted: false,
|
||||
volume: 1.0,
|
||||
});
|
||||
let entry = self
|
||||
.local_client_audio
|
||||
.entry(client_id)
|
||||
.or_insert(LocalClientAudio {
|
||||
muted: false,
|
||||
volume: 1.0,
|
||||
});
|
||||
entry.muted = !entry.muted;
|
||||
let audio = self.audio.clone();
|
||||
let muted = entry.muted;
|
||||
@@ -455,10 +462,13 @@ impl App {
|
||||
Task::none()
|
||||
}
|
||||
Message::IncreaseClientVolume(client_id) => {
|
||||
let entry = self.local_client_audio.entry(client_id).or_insert(LocalClientAudio {
|
||||
muted: false,
|
||||
volume: 1.0,
|
||||
});
|
||||
let entry = self
|
||||
.local_client_audio
|
||||
.entry(client_id)
|
||||
.or_insert(LocalClientAudio {
|
||||
muted: false,
|
||||
volume: 1.0,
|
||||
});
|
||||
entry.volume = (entry.volume + 0.1).min(2.0);
|
||||
let audio = self.audio.clone();
|
||||
let volume = entry.volume;
|
||||
@@ -468,10 +478,13 @@ impl App {
|
||||
Task::none()
|
||||
}
|
||||
Message::DecreaseClientVolume(client_id) => {
|
||||
let entry = self.local_client_audio.entry(client_id).or_insert(LocalClientAudio {
|
||||
muted: false,
|
||||
volume: 1.0,
|
||||
});
|
||||
let entry = self
|
||||
.local_client_audio
|
||||
.entry(client_id)
|
||||
.or_insert(LocalClientAudio {
|
||||
muted: false,
|
||||
volume: 1.0,
|
||||
});
|
||||
entry.volume = (entry.volume - 0.1).max(0.0);
|
||||
let audio = self.audio.clone();
|
||||
let volume = entry.volume;
|
||||
@@ -519,9 +532,16 @@ impl App {
|
||||
)
|
||||
}
|
||||
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;
|
||||
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(
|
||||
self.mic.clone(),
|
||||
self.handle.clone(),
|
||||
@@ -551,9 +571,16 @@ impl App {
|
||||
Task::none()
|
||||
}
|
||||
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;
|
||||
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(
|
||||
self.mic.clone(),
|
||||
self.handle.clone(),
|
||||
@@ -643,8 +670,7 @@ impl App {
|
||||
.with_connection(move |con| -> Result<(), String> {
|
||||
let state = con.get_state().map_err(|e| e.to_string())?;
|
||||
if state.clients.contains_key(&state.own_client) {
|
||||
let cmd = state.client_update()
|
||||
.set_input_muted(muted);
|
||||
let cmd = state.client_update().set_input_muted(muted);
|
||||
cmd.send(con).map_err(|e| e.to_string())?;
|
||||
}
|
||||
Ok(())
|
||||
@@ -667,8 +693,7 @@ impl App {
|
||||
.with_connection(move |con| -> Result<(), String> {
|
||||
let state = con.get_state().map_err(|e| e.to_string())?;
|
||||
if state.clients.contains_key(&state.own_client) {
|
||||
let cmd = state.client_update()
|
||||
.set_output_muted(muted);
|
||||
let cmd = state.client_update().set_output_muted(muted);
|
||||
cmd.send(con).map_err(|e| e.to_string())?;
|
||||
}
|
||||
Ok(())
|
||||
@@ -691,7 +716,8 @@ impl App {
|
||||
.with_connection(move |con| -> Result<(), String> {
|
||||
let state = con.get_state().map_err(|e| e.to_string())?;
|
||||
if state.clients.contains_key(&state.own_client) {
|
||||
let cmd = state.client_update()
|
||||
let cmd = state
|
||||
.client_update()
|
||||
.set_output_hardware_enabled(!muted);
|
||||
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())?;
|
||||
if state.clients.contains_key(&state.own_client) {
|
||||
let cmd = if afk {
|
||||
state.client_update()
|
||||
state
|
||||
.client_update()
|
||||
.set_input_muted(true)
|
||||
.set_away(Some("AFK"))
|
||||
} else {
|
||||
state.client_update()
|
||||
state
|
||||
.client_update()
|
||||
.set_input_muted(false)
|
||||
.set_away(Some(""))
|
||||
};
|
||||
@@ -789,6 +817,10 @@ impl App {
|
||||
self.save_settings();
|
||||
Task::none()
|
||||
}
|
||||
Message::SelectSettingsSection(section) => {
|
||||
self.settings_section = section;
|
||||
Task::none()
|
||||
}
|
||||
Message::SetAppearance(mode) => {
|
||||
self.appearance_mode = mode;
|
||||
self.save_settings();
|
||||
@@ -829,9 +861,7 @@ impl App {
|
||||
_ => {}
|
||||
},
|
||||
Event::Message {
|
||||
invoker,
|
||||
message,
|
||||
..
|
||||
invoker, message, ..
|
||||
} => {
|
||||
self.messages.push(ChatMessage {
|
||||
invoker_name: invoker.name.clone(),
|
||||
@@ -856,25 +886,35 @@ impl App {
|
||||
state.server.version.clone(),
|
||||
state.server.max_clients,
|
||||
state.own_client,
|
||||
state.channels.values().map(|ch| ChannelEntry {
|
||||
id: ch.id,
|
||||
name: ch.name.clone(),
|
||||
parent: ch.parent,
|
||||
order: ch.order,
|
||||
}).collect::<Vec<_>>(),
|
||||
state.clients.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<_>>(),
|
||||
state
|
||||
.channels
|
||||
.values()
|
||||
.map(|ch| ChannelEntry {
|
||||
id: ch.id,
|
||||
name: ch.name.clone(),
|
||||
parent: ch.parent,
|
||||
order: ch.order,
|
||||
})
|
||||
.collect::<Vec<_>>(),
|
||||
state
|
||||
.clients
|
||||
.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
|
||||
})
|
||||
.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_platform = platform;
|
||||
self.server_version = version;
|
||||
@@ -896,13 +936,19 @@ impl App {
|
||||
let result = handle
|
||||
.with_connection(|con| {
|
||||
if let Ok(state) = con.get_state() {
|
||||
return Some(state.clients.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<_>>());
|
||||
return Some(
|
||||
state
|
||||
.clients
|
||||
.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
|
||||
})
|
||||
@@ -923,12 +969,18 @@ impl App {
|
||||
let result = handle
|
||||
.with_connection(|con| {
|
||||
if let Ok(state) = con.get_state() {
|
||||
return Some(state.channels.values().map(|ch| ChannelEntry {
|
||||
id: ch.id,
|
||||
name: ch.name.clone(),
|
||||
parent: ch.parent,
|
||||
order: ch.order,
|
||||
}).collect::<Vec<_>>());
|
||||
return Some(
|
||||
state
|
||||
.channels
|
||||
.values()
|
||||
.map(|ch| ChannelEntry {
|
||||
id: ch.id,
|
||||
name: ch.name.clone(),
|
||||
parent: ch.parent,
|
||||
order: ch.order,
|
||||
})
|
||||
.collect::<Vec<_>>(),
|
||||
);
|
||||
}
|
||||
None
|
||||
})
|
||||
@@ -975,7 +1027,8 @@ impl App {
|
||||
};
|
||||
match 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() {
|
||||
break;
|
||||
}
|
||||
@@ -1028,10 +1081,12 @@ impl App {
|
||||
let sidebar = self.view_sidebar();
|
||||
let content = self.view_content();
|
||||
|
||||
row![sidebar, container(content).style(theme::main_panel_container)]
|
||||
.width(Length::Fill)
|
||||
.height(Length::Fill)
|
||||
.into()
|
||||
row![
|
||||
sidebar,
|
||||
container(content).style(theme::main_panel_container)
|
||||
]
|
||||
.width(Length::Fill)
|
||||
.height(Length::Fill)
|
||||
.into()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+136
-33
@@ -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_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 LIGHT_BG: Color = Color {
|
||||
r: 245.0 / 255.0,
|
||||
g: 245.0 / 255.0,
|
||||
b: 247.0 / 255.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_SURFACE: Color = Color { r: 28.0 / 255.0, g: 28.0 / 255.0, b: 30.0 / 255.0, a: 1.0 };
|
||||
const DARK_ELEVATED: Color = Color { r: 44.0 / 255.0, g: 44.0 / 255.0, b: 46.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: 174.0 / 255.0, g: 174.0 / 255.0, b: 178.0 / 255.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_BG: Color = Color {
|
||||
r: 11.0 / 255.0,
|
||||
g: 11.0 / 255.0,
|
||||
b: 13.0 / 255.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 DANGER: Color = Color { r: 1.0, g: 69.0 / 255.0, b: 58.0 / 255.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 DANGER: Color = Color {
|
||||
r: 1.0,
|
||||
g: 69.0 / 255.0,
|
||||
b: 58.0 / 255.0,
|
||||
a: 1.0,
|
||||
};
|
||||
|
||||
pub fn light_theme() -> Theme {
|
||||
Theme::custom(
|
||||
@@ -66,15 +146,27 @@ fn is_dark(theme: &Theme) -> bool {
|
||||
}
|
||||
|
||||
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 {
|
||||
if is_dark(theme) { DARK_ELEVATED } else { LIGHT_ELEVATED }
|
||||
if is_dark(theme) {
|
||||
DARK_ELEVATED
|
||||
} else {
|
||||
LIGHT_ELEVATED
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
@@ -86,7 +178,11 @@ pub fn nav_icon_color(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 {
|
||||
@@ -95,12 +191,15 @@ fn accent(theme: &Theme) -> Color {
|
||||
|
||||
fn accent_hover(theme: &Theme) -> Color {
|
||||
let primary = accent(theme);
|
||||
Color { a: primary.a, ..Color::from_rgba(
|
||||
(primary.r + 0.05).min(1.0),
|
||||
(primary.g + 0.05).min(1.0),
|
||||
(primary.b + 0.05).min(1.0),
|
||||
1.0,
|
||||
) }
|
||||
Color {
|
||||
a: primary.a,
|
||||
..Color::from_rgba(
|
||||
(primary.r + 0.05).min(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 {
|
||||
@@ -108,7 +207,7 @@ pub fn sidebar_container(theme: &Theme) -> container::Style {
|
||||
background: Some(Background::Color(elevated(theme))),
|
||||
border: Border {
|
||||
color: border(theme),
|
||||
width: 0.0,
|
||||
width: if is_dark(theme) { 1.0 } else { 0.0 },
|
||||
radius: 0.0.into(),
|
||||
},
|
||||
..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)),
|
||||
border: Border {
|
||||
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(),
|
||||
},
|
||||
icon: text_secondary(theme),
|
||||
@@ -237,7 +340,7 @@ pub fn primary_button(theme: &Theme, status: button::Status) -> button::Style {
|
||||
border: Border {
|
||||
color: Color::TRANSPARENT,
|
||||
width: 0.0,
|
||||
radius: 12.0.into(),
|
||||
radius: 999.0.into(),
|
||||
},
|
||||
shadow: Shadow {
|
||||
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 {
|
||||
color: border(theme),
|
||||
width: 1.0,
|
||||
radius: 12.0.into(),
|
||||
radius: 999.0.into(),
|
||||
},
|
||||
shadow: Shadow::default(),
|
||||
}
|
||||
@@ -273,7 +376,7 @@ pub fn danger_button(_theme: &Theme, _status: button::Status) -> button::Style {
|
||||
border: Border {
|
||||
color: Color::TRANSPARENT,
|
||||
width: 0.0,
|
||||
radius: 12.0.into(),
|
||||
radius: 999.0.into(),
|
||||
},
|
||||
shadow: Shadow::default(),
|
||||
}
|
||||
@@ -286,7 +389,7 @@ pub fn nav_button_active(theme: &Theme, _status: button::Status) -> button::Styl
|
||||
border: Border {
|
||||
color: Color::TRANSPARENT,
|
||||
width: 0.0,
|
||||
radius: 12.0.into(),
|
||||
radius: 999.0.into(),
|
||||
},
|
||||
shadow: Shadow::default(),
|
||||
}
|
||||
@@ -309,7 +412,7 @@ pub fn nav_button_inactive(theme: &Theme, status: button::Status) -> button::Sty
|
||||
border: Border {
|
||||
color: Color::TRANSPARENT,
|
||||
width: 0.0,
|
||||
radius: 12.0.into(),
|
||||
radius: 999.0.into(),
|
||||
},
|
||||
shadow: Shadow::default(),
|
||||
}
|
||||
|
||||
@@ -2,8 +2,8 @@ use serde::{Deserialize, Serialize};
|
||||
use tsclientlib::events::Event;
|
||||
use tsclientlib::{ChannelId, ClientId, Identity};
|
||||
|
||||
use crate::{audio, noise_cancel};
|
||||
use crate::theme;
|
||||
use crate::{audio, noise_cancel};
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub(crate) enum Page {
|
||||
@@ -13,6 +13,50 @@ pub(crate) enum Page {
|
||||
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)]
|
||||
#[allow(clippy::enum_variant_names)]
|
||||
pub(crate) enum Message {
|
||||
@@ -58,6 +102,7 @@ pub(crate) enum Message {
|
||||
IdentityImported(Result<Identity, String>),
|
||||
SetOutputDevice(String),
|
||||
SetInputDevice(String),
|
||||
SelectSettingsSection(SettingsSection),
|
||||
SetAppearance(theme::AppearanceMode),
|
||||
Noop,
|
||||
}
|
||||
|
||||
+743
-308
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user