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 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
@@ -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(),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user