refactor: make audio mandatory and split iced app modules
@@ -19,5 +19,5 @@ serde_json = "1"
|
||||
tracing = "0.1"
|
||||
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
||||
chrono = { version = "0.4", features = ["serde"] }
|
||||
tsclientlib = { git = "https://github.com/ReSpeak/tsclientlib.git", branch = "master", default-features = false, features = ["default-tls"] }
|
||||
tsclientlib = { git = "https://github.com/ReSpeak/tsclientlib.git", branch = "master", default-features = false, features = ["default-tls", "audio"] }
|
||||
tsproto-packets = { git = "https://github.com/ReSpeak/tsclientlib.git", branch = "master" }
|
||||
|
||||
@@ -10,7 +10,7 @@ name = "re-teamspeak"
|
||||
path = "src/main.rs"
|
||||
|
||||
[dependencies]
|
||||
iced = { version = "0.13", features = ["tokio", "debug"] }
|
||||
iced = { version = "0.13", features = ["tokio", "debug", "svg"] }
|
||||
tokio = { workspace = true }
|
||||
futures = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
@@ -18,16 +18,9 @@ serde_json = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
tracing-subscriber = { workspace = true }
|
||||
chrono = { workspace = true }
|
||||
cpal = { version = "0.15", optional = true }
|
||||
audiopus = { version = "0.3.0-rc.0", optional = true }
|
||||
nnnoiseless = { version = "0.5", optional = true }
|
||||
sonora = { version = "0.1", optional = true }
|
||||
rusqlite = { version = "0.31", optional = true }
|
||||
|
||||
cpal = { version = "0.15" }
|
||||
audiopus = { version = "0.3.0-rc.0" }
|
||||
nnnoiseless = { version = "0.5" }
|
||||
sonora = { version = "0.1" }
|
||||
tsclientlib = { workspace = true }
|
||||
tsproto-packets = { workspace = true }
|
||||
|
||||
[features]
|
||||
default = []
|
||||
audio = ["dep:cpal", "dep:audiopus", "dep:nnnoiseless", "dep:sonora"]
|
||||
rusqlite = ["dep:rusqlite"]
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M17.5933 3.32241C18.6939 3.45014 19.5 4.399 19.5 5.50699V21L12 17.25L4.5 21V5.50699C4.5 4.399 5.30608 3.45014 6.40668 3.32241C8.24156 3.10947 10.108 3 12 3C13.892 3 15.7584 3.10947 17.5933 3.32241Z" stroke="#0F172A" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 396 B |
@@ -0,0 +1,3 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M19.5 8.25L12 15.75L4.5 8.25" stroke="#0F172A" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 227 B |
@@ -0,0 +1,3 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M8.25 4.5L15.75 12L8.25 19.5" stroke="#0F172A" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 227 B |
@@ -0,0 +1,4 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M9.59356 3.94014C9.68397 3.39768 10.1533 3.00009 10.7033 3.00009H13.2972C13.8472 3.00009 14.3165 3.39768 14.4069 3.94014L14.6204 5.22119C14.6828 5.59523 14.9327 5.9068 15.2645 6.09045C15.3387 6.13151 15.412 6.17393 15.4844 6.21766C15.8095 6.41393 16.2048 6.47495 16.5604 6.34175L17.7772 5.88587C18.2922 5.69293 18.8712 5.9006 19.1462 6.37687L20.4432 8.6233C20.7181 9.09957 20.6085 9.70482 20.1839 10.0544L19.1795 10.8812C18.887 11.122 18.742 11.4938 18.7491 11.8726C18.7498 11.915 18.7502 11.9575 18.7502 12.0001C18.7502 12.0427 18.7498 12.0852 18.7491 12.1275C18.742 12.5064 18.887 12.8782 19.1795 13.119L20.1839 13.9458C20.6085 14.2953 20.7181 14.9006 20.4432 15.3769L19.1462 17.6233C18.8712 18.0996 18.2922 18.3072 17.7772 18.1143L16.5604 17.6584C16.2048 17.5252 15.8095 17.5862 15.4844 17.7825C15.412 17.8263 15.3387 17.8687 15.2645 17.9097C14.9327 18.0934 14.6828 18.4049 14.6204 18.779L14.4069 20.06C14.3165 20.6025 13.8472 21.0001 13.2972 21.0001H10.7033C10.1533 21.0001 9.68397 20.6025 9.59356 20.06L9.38005 18.779C9.31771 18.4049 9.06774 18.0934 8.73597 17.9097C8.66179 17.8687 8.58847 17.8263 8.51604 17.7825C8.19101 17.5863 7.79568 17.5252 7.44011 17.6584L6.22325 18.1143C5.70826 18.3072 5.12926 18.0996 4.85429 17.6233L3.55731 15.3769C3.28234 14.9006 3.39199 14.2954 3.81657 13.9458L4.82092 13.119C5.11343 12.8782 5.25843 12.5064 5.25141 12.1276C5.25063 12.0852 5.25023 12.0427 5.25023 12.0001C5.25023 11.9575 5.25063 11.915 5.25141 11.8726C5.25843 11.4938 5.11343 11.122 4.82092 10.8812L3.81657 10.0544C3.39199 9.70484 3.28234 9.09958 3.55731 8.62332L4.85429 6.37688C5.12926 5.90061 5.70825 5.69295 6.22325 5.88588L7.4401 6.34176C7.79566 6.47496 8.19099 6.41394 8.51603 6.21767C8.58846 6.17393 8.66179 6.13151 8.73597 6.09045C9.06774 5.9068 9.31771 5.59523 9.38005 5.22119L9.59356 3.94014Z" stroke="#0F172A" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M15 12C15 13.6569 13.6569 15 12 15C10.3431 15 9 13.6569 9 12C9 10.3432 10.3431 9.00001 12 9.00001C13.6569 9.00001 15 10.3432 15 12Z" stroke="#0F172A" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.2 KiB |
@@ -0,0 +1,3 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M2.25 12L11.2045 3.04549C11.6438 2.60615 12.3562 2.60615 12.7955 3.04549L21.75 12M4.5 9.75V19.875C4.5 20.4963 5.00368 21 5.625 21H9.75V16.125C9.75 15.5037 10.2537 15 10.875 15H13.125C13.7463 15 14.25 15.5037 14.25 16.125V21H18.375C18.9963 21 19.5 20.4963 19.5 19.875V9.75M8.25 21H16.5" stroke="#0F172A" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 483 B |
@@ -0,0 +1,3 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M12 18.75C15.3137 18.75 18 16.0637 18 12.75V11.25M12 18.75C8.68629 18.75 6 16.0637 6 12.75V11.25M12 18.75V22.5M8.25 22.5H15.75M12 15.75C10.3431 15.75 9 14.4069 9 12.75V4.5C9 2.84315 10.3431 1.5 12 1.5C13.6569 1.5 15 2.84315 15 4.5V12.75C15 14.4069 13.6569 15.75 12 15.75Z" stroke="#0F172A" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 470 B |
@@ -0,0 +1,3 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M19.114 5.63597C22.6287 9.15069 22.6287 14.8492 19.114 18.3639M16.4626 8.28771C18.5129 10.338 18.5129 13.6621 16.4626 15.7123M6.75 8.24999L11.4697 3.53032C11.9421 3.05784 12.75 3.39247 12.75 4.06065V19.9393C12.75 20.6075 11.9421 20.9421 11.4697 20.4697L6.75 15.75H4.50905C3.62971 15.75 2.8059 15.2435 2.57237 14.3957C2.36224 13.6329 2.25 12.8296 2.25 12C2.25 11.1704 2.36224 10.367 2.57237 9.60423C2.8059 8.75646 3.62971 8.24999 4.50905 8.24999H6.75Z" stroke="#0F172A" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 649 B |
@@ -0,0 +1,3 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M17.25 9.74999L19.5 12M19.5 12L21.75 14.25M19.5 12L21.75 9.74999M19.5 12L17.25 14.25M6.75 8.24999L11.4697 3.53032C11.9421 3.05784 12.75 3.39247 12.75 4.06065V19.9393C12.75 20.6075 11.9421 20.9421 11.4697 20.4697L6.75 15.75H4.50905C3.62971 15.75 2.8059 15.2435 2.57237 14.3957C2.36224 13.6329 2.25 12.8296 2.25 12C2.25 11.1704 2.36224 10.367 2.57237 9.60423C2.8059 8.75646 3.62971 8.24999 4.50905 8.24999H6.75Z" stroke="#0F172A" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 608 B |
@@ -0,0 +1,3 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M17.9815 18.7248C16.6121 16.9175 14.4424 15.75 12 15.75C9.55761 15.75 7.38789 16.9175 6.01846 18.7248M17.9815 18.7248C19.8335 17.0763 21 14.6744 21 12C21 7.02944 16.9706 3 12 3C7.02944 3 3 7.02944 3 12C3 14.6744 4.1665 17.0763 6.01846 18.7248M17.9815 18.7248C16.3915 20.1401 14.2962 21 12 21C9.70383 21 7.60851 20.1401 6.01846 18.7248M15 9.75C15 11.4069 13.6569 12.75 12 12.75C10.3431 12.75 9 11.4069 9 9.75C9 8.09315 10.3431 6.75 12 6.75C13.6569 6.75 15 8.09315 15 9.75Z" stroke="#0F172A" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 670 B |
@@ -0,0 +1,3 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M22 10.5H16M13.75 6.375C13.75 8.23896 12.239 9.75 10.375 9.75C8.51104 9.75 7 8.23896 7 6.375C7 4.51104 8.51104 3 10.375 3C12.239 3 13.75 4.51104 13.75 6.375ZM4.00092 19.2343C4.00031 19.198 4 19.1615 4 19.125C4 15.6042 6.85418 12.75 10.375 12.75C13.8958 12.75 16.75 15.6042 16.75 19.125V19.1276C16.75 19.1632 16.7497 19.1988 16.7491 19.2343C14.8874 20.3552 12.7065 21 10.375 21C8.04353 21 5.86264 20.3552 4.00092 19.2343Z" stroke="#0F172A" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 619 B |
@@ -0,0 +1,4 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M15.75 6C15.75 8.07107 14.071 9.75 12 9.75C9.9289 9.75 8.24996 8.07107 8.24996 6C8.24996 3.92893 9.9289 2.25 12 2.25C14.071 2.25 15.75 3.92893 15.75 6Z" stroke="#0F172A" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M4.5011 20.1182C4.5714 16.0369 7.90184 12.75 12 12.75C16.0982 12.75 19.4287 16.0371 19.4988 20.1185C17.216 21.166 14.6764 21.75 12.0003 21.75C9.32396 21.75 6.78406 21.1659 4.5011 20.1182Z" stroke="#0F172A" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 633 B |
@@ -1,7 +1,10 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use cpal::traits::{DeviceTrait, HostTrait, StreamTrait};
|
||||
use std::collections::HashMap;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use tsclientlib::audio::AudioHandler;
|
||||
use tsclientlib::ClientId;
|
||||
use tsproto_packets::packets::{AudioData, CodecType, InAudioBuf, OutAudio};
|
||||
use tsproto_packets::packets::{AudioData, InAudioBuf};
|
||||
|
||||
const SAMPLE_RATE: u32 = 48000;
|
||||
const CHANNELS: u16 = 2;
|
||||
@@ -12,7 +15,8 @@ type PacketSender = std::sync::mpsc::Sender<InAudioBuf>;
|
||||
|
||||
struct PlaybackState {
|
||||
sender: PacketSender,
|
||||
output_device: String,
|
||||
volumes: Arc<Mutex<HashMap<ClientId, f32>>>,
|
||||
muted_clients: Arc<Mutex<HashMap<ClientId, bool>>>,
|
||||
_stream: cpal::Stream,
|
||||
}
|
||||
|
||||
@@ -65,6 +69,10 @@ impl AudioPlayback {
|
||||
};
|
||||
|
||||
let (tx, rx) = std::sync::mpsc::channel::<InAudioBuf>();
|
||||
let volumes = Arc::new(Mutex::new(HashMap::<ClientId, f32>::new()));
|
||||
let muted_clients = Arc::new(Mutex::new(HashMap::<ClientId, bool>::new()));
|
||||
let callback_volumes = Arc::clone(&volumes);
|
||||
let callback_muted = Arc::clone(&muted_clients);
|
||||
let mut handler = AudioHandler::<ClientId>::new();
|
||||
|
||||
let stream = device
|
||||
@@ -82,6 +90,19 @@ impl AudioPlayback {
|
||||
for sample in data.iter_mut() {
|
||||
*sample = 0.0;
|
||||
}
|
||||
let volumes = callback_volumes.lock().ok().map(|volumes| volumes.clone());
|
||||
let muted_clients = callback_muted.lock().ok().map(|muted| muted.clone());
|
||||
for (id, queue) in handler.get_mut_queues().iter_mut() {
|
||||
let volume = volumes
|
||||
.as_ref()
|
||||
.and_then(|volumes| volumes.get(id).copied())
|
||||
.unwrap_or(1.0);
|
||||
let muted = muted_clients
|
||||
.as_ref()
|
||||
.and_then(|muted| muted.get(id).copied())
|
||||
.unwrap_or(false);
|
||||
queue.volume = if muted { 0.0 } else { volume };
|
||||
}
|
||||
handler.fill_buffer(data);
|
||||
},
|
||||
|err| tracing::error!("Audio output error: {err}"),
|
||||
@@ -95,7 +116,8 @@ impl AudioPlayback {
|
||||
|
||||
self.state = Some(PlaybackState {
|
||||
sender: tx,
|
||||
output_device: device_name_str,
|
||||
volumes,
|
||||
muted_clients,
|
||||
_stream: stream,
|
||||
});
|
||||
|
||||
@@ -114,21 +136,25 @@ impl AudioPlayback {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn output_device(&self) -> &str {
|
||||
self.state
|
||||
.as_ref()
|
||||
.map(|s| s.output_device.as_str())
|
||||
.unwrap_or("")
|
||||
pub fn set_client_volume(&self, client: ClientId, volume: f32) {
|
||||
if let Some(ref state) = self.state {
|
||||
if let Ok(mut volumes) = state.volumes.lock() {
|
||||
volumes.insert(client, volume.clamp(0.0, 2.0));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_active(&self) -> bool {
|
||||
self.state.is_some()
|
||||
pub fn set_client_muted(&self, client: ClientId, muted: bool) {
|
||||
if let Some(ref state) = self.state {
|
||||
if let Ok(mut muted_clients) = state.muted_clients.lock() {
|
||||
muted_clients.insert(client, muted);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct CaptureState {
|
||||
_stream: cpal::Stream,
|
||||
input_device: String,
|
||||
}
|
||||
|
||||
unsafe impl Send for CaptureState {}
|
||||
@@ -186,7 +212,6 @@ impl Microphone {
|
||||
|
||||
self.state = Some(CaptureState {
|
||||
_stream: stream,
|
||||
input_device: device_name_str,
|
||||
});
|
||||
|
||||
tracing::info!("Microphone started");
|
||||
@@ -198,16 +223,6 @@ impl Microphone {
|
||||
tracing::info!("Microphone stopped");
|
||||
}
|
||||
|
||||
pub fn input_device(&self) -> &str {
|
||||
self.state
|
||||
.as_ref()
|
||||
.map(|s| s.input_device.as_str())
|
||||
.unwrap_or("")
|
||||
}
|
||||
|
||||
pub fn is_active(&self) -> bool {
|
||||
self.state.is_some()
|
||||
}
|
||||
}
|
||||
|
||||
pub struct OpusEncoderState {
|
||||
@@ -256,17 +271,9 @@ impl OpusEncoderState {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn packet_id(&self) -> u16 {
|
||||
self.packet_id
|
||||
}
|
||||
|
||||
pub fn reset(&mut self) {
|
||||
self.sample_buf.clear();
|
||||
self.packet_id = 0;
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum TalkMode {
|
||||
PushToTalk,
|
||||
Continuous,
|
||||
@@ -339,21 +346,7 @@ impl VoiceActivation {
|
||||
self.state
|
||||
}
|
||||
|
||||
pub fn state(&self) -> VadState {
|
||||
self.state
|
||||
}
|
||||
|
||||
pub fn is_speaking(&self) -> bool {
|
||||
matches!(self.state, VadState::Speaking | VadState::Hangover)
|
||||
}
|
||||
|
||||
pub fn set_threshold(&mut self, threshold: f32) {
|
||||
self.threshold = threshold;
|
||||
}
|
||||
|
||||
pub fn reset(&mut self) {
|
||||
self.state = VadState::Silent;
|
||||
self.hangover_counter = 0;
|
||||
self.energy_history.clear();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
use iced::widget::svg::{self, Handle};
|
||||
use iced::widget::{svg as svg_widget, Svg};
|
||||
use iced::{Color, Length, Theme};
|
||||
use std::sync::LazyLock;
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum Icon {
|
||||
Home,
|
||||
Bookmark,
|
||||
UserCircle,
|
||||
User,
|
||||
UserMinus,
|
||||
Cog,
|
||||
ChevronRight,
|
||||
ChevronDown,
|
||||
Microphone,
|
||||
SpeakerWave,
|
||||
SpeakerXMark,
|
||||
}
|
||||
|
||||
static HOME: LazyLock<Handle> = LazyLock::new(|| Handle::from_memory(include_bytes!("../assets/icons/home.svg")));
|
||||
static BOOKMARK: LazyLock<Handle> = LazyLock::new(|| Handle::from_memory(include_bytes!("../assets/icons/bookmark.svg")));
|
||||
static USER_CIRCLE: LazyLock<Handle> = LazyLock::new(|| Handle::from_memory(include_bytes!("../assets/icons/user-circle.svg")));
|
||||
static USER: LazyLock<Handle> = LazyLock::new(|| Handle::from_memory(include_bytes!("../assets/icons/user.svg")));
|
||||
static USER_MINUS: LazyLock<Handle> = LazyLock::new(|| Handle::from_memory(include_bytes!("../assets/icons/user-minus.svg")));
|
||||
static COG: LazyLock<Handle> = LazyLock::new(|| Handle::from_memory(include_bytes!("../assets/icons/cog-6-tooth.svg")));
|
||||
static CHEVRON_RIGHT: LazyLock<Handle> = LazyLock::new(|| Handle::from_memory(include_bytes!("../assets/icons/chevron-right.svg")));
|
||||
static CHEVRON_DOWN: LazyLock<Handle> = LazyLock::new(|| Handle::from_memory(include_bytes!("../assets/icons/chevron-down.svg")));
|
||||
static MICROPHONE: LazyLock<Handle> = LazyLock::new(|| Handle::from_memory(include_bytes!("../assets/icons/microphone.svg")));
|
||||
static SPEAKER_WAVE: LazyLock<Handle> = LazyLock::new(|| Handle::from_memory(include_bytes!("../assets/icons/speaker-wave.svg")));
|
||||
static SPEAKER_X_MARK: LazyLock<Handle> = LazyLock::new(|| Handle::from_memory(include_bytes!("../assets/icons/speaker-x-mark.svg")));
|
||||
|
||||
fn handle(icon: Icon) -> Handle {
|
||||
match icon {
|
||||
Icon::Home => HOME.clone(),
|
||||
Icon::Bookmark => BOOKMARK.clone(),
|
||||
Icon::UserCircle => USER_CIRCLE.clone(),
|
||||
Icon::User => USER.clone(),
|
||||
Icon::UserMinus => USER_MINUS.clone(),
|
||||
Icon::Cog => COG.clone(),
|
||||
Icon::ChevronRight => CHEVRON_RIGHT.clone(),
|
||||
Icon::ChevronDown => CHEVRON_DOWN.clone(),
|
||||
Icon::Microphone => MICROPHONE.clone(),
|
||||
Icon::SpeakerWave => SPEAKER_WAVE.clone(),
|
||||
Icon::SpeakerXMark => SPEAKER_X_MARK.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn view<'a>(icon: Icon, size: f32, color: Color) -> Svg<'a, Theme> {
|
||||
svg_widget(handle(icon))
|
||||
.width(Length::Fixed(size))
|
||||
.height(Length::Fixed(size))
|
||||
.style(move |_theme: &Theme, _status| svg::Style { color: Some(color) })
|
||||
}
|
||||
@@ -1,45 +1,23 @@
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
|
||||
use tsclientlib::Identity;
|
||||
|
||||
#[cfg(feature = "rusqlite")]
|
||||
use rusqlite::Connection;
|
||||
pub fn import_identity_from_string(raw: &str) -> Result<Identity, String> {
|
||||
let trimmed = raw.trim();
|
||||
if trimmed.is_empty() {
|
||||
return Err("Identity string is empty".to_string());
|
||||
}
|
||||
|
||||
#[cfg(feature = "rusqlite")]
|
||||
pub fn import_identity_from_ts3(path: &str) -> Result<Identity, String> {
|
||||
let conn = Connection::open(path)
|
||||
.map_err(|e| format!("Failed to open TS3 settings database: {e}"))?;
|
||||
|
||||
let identity_str: String = conn
|
||||
.query_row(
|
||||
"SELECT value FROM properties WHERE key = 'identity_secret_key'",
|
||||
[],
|
||||
|row| row.get(0),
|
||||
)
|
||||
.map_err(|e| format!("Failed to query identity from database: {e}"))?;
|
||||
|
||||
tracing::info!("Found identity string: {}", &identity_str[..identity_str.len().min(50)]);
|
||||
|
||||
Identity::new_from_ts_str(&identity_str)
|
||||
Identity::new_from_ts_str(trimmed)
|
||||
.or_else(|_| Identity::new_from_str(trimmed))
|
||||
.map_err(|e| format!("Failed to parse identity: {e}"))
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "rusqlite"))]
|
||||
pub fn import_identity_from_ts3(_path: &str) -> Result<Identity, String> {
|
||||
Err("rusqlite feature not enabled".to_string())
|
||||
}
|
||||
pub fn import_identity_from_file(path: &str) -> Result<Identity, String> {
|
||||
let path_ref = Path::new(path);
|
||||
let raw = fs::read_to_string(path_ref)
|
||||
.map_err(|e| format!("Failed to read identity file '{}': {e}", path_ref.display()))?;
|
||||
|
||||
pub fn find_ts3_config_dir() -> Option<String> {
|
||||
let home = std::env::var("HOME").ok()?;
|
||||
|
||||
let paths = [
|
||||
format!("{}/.ts3client/settings.db", home),
|
||||
format!("{}/.config/teamspeak3/settings.db", home),
|
||||
];
|
||||
|
||||
for path in &paths {
|
||||
if std::path::Path::new(path).exists() {
|
||||
return Some(path.clone());
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
import_identity_from_string(&raw)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,10 @@
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sonora::config::{NoiseSuppression, NoiseSuppressionLevel};
|
||||
use sonora::{AudioProcessing, Config, StreamConfig};
|
||||
|
||||
const SONORA_FRAME_SIZE: usize = 480;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum NoiseCancelMethod {
|
||||
None,
|
||||
Nnnoiseless,
|
||||
@@ -21,23 +27,57 @@ impl NoiseCancelMethod {
|
||||
|
||||
pub struct NoiseReducer {
|
||||
method: NoiseCancelMethod,
|
||||
#[cfg(feature = "nnnoiseless")]
|
||||
nnnoiseless: Option<nnnoiseless::DenoiseState<'static>>,
|
||||
#[cfg(feature = "sonora")]
|
||||
sonora: Option<sonora::NoiseSuppression>,
|
||||
#[cfg(feature = "nnnoiseless")]
|
||||
nnnoiseless: Option<Box<nnnoiseless::DenoiseState<'static>>>,
|
||||
sonora: Option<SonoraNoiseReducer>,
|
||||
residual_buf: Vec<f32>,
|
||||
}
|
||||
|
||||
struct SonoraNoiseReducer {
|
||||
processor: AudioProcessing,
|
||||
input: Vec<f32>,
|
||||
output: Vec<f32>,
|
||||
}
|
||||
|
||||
impl SonoraNoiseReducer {
|
||||
fn new() -> Self {
|
||||
let stream = StreamConfig::new(48_000, 1);
|
||||
let config = Config {
|
||||
noise_suppression: Some(NoiseSuppression {
|
||||
level: NoiseSuppressionLevel::Moderate,
|
||||
..NoiseSuppression::default()
|
||||
}),
|
||||
..Config::default()
|
||||
};
|
||||
|
||||
Self {
|
||||
processor: AudioProcessing::builder()
|
||||
.config(config)
|
||||
.capture_config(stream)
|
||||
.render_config(stream)
|
||||
.build(),
|
||||
input: vec![0.0; SONORA_FRAME_SIZE],
|
||||
output: vec![0.0; SONORA_FRAME_SIZE],
|
||||
}
|
||||
}
|
||||
|
||||
fn process(&mut self, frame: &mut [f32]) {
|
||||
self.input.copy_from_slice(frame);
|
||||
let src = [&self.input[..]];
|
||||
let mut dest = [&mut self.output[..]];
|
||||
if let Err(error) = self.processor.process_capture_f32(&src, &mut dest) {
|
||||
tracing::debug!("Sonora noise suppression error: {error}");
|
||||
return;
|
||||
}
|
||||
frame.copy_from_slice(&self.output);
|
||||
}
|
||||
}
|
||||
|
||||
impl NoiseReducer {
|
||||
pub fn new(method: NoiseCancelMethod) -> Self {
|
||||
let mut this = Self {
|
||||
method,
|
||||
#[cfg(feature = "nnnoiseless")]
|
||||
nnnoiseless: None,
|
||||
#[cfg(feature = "sonora")]
|
||||
sonora: None,
|
||||
#[cfg(feature = "nnnoiseless")]
|
||||
residual_buf: Vec::new(),
|
||||
};
|
||||
this.init_method();
|
||||
@@ -45,26 +85,15 @@ impl NoiseReducer {
|
||||
}
|
||||
|
||||
fn init_method(&mut self) {
|
||||
#[cfg(feature = "nnnoiseless")]
|
||||
{
|
||||
self.nnnoiseless = match self.method {
|
||||
NoiseCancelMethod::Nnnoiseless => Some(nnnoiseless::DenoiseState::new()),
|
||||
_ => None,
|
||||
};
|
||||
}
|
||||
#[cfg(feature = "sonora")]
|
||||
{
|
||||
self.sonora = match self.method {
|
||||
NoiseCancelMethod::Sonora => {
|
||||
Some(sonora::NoiseSuppression::new(48000, 480).expect("Failed to create Sonora NS"))
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
}
|
||||
#[cfg(feature = "nnnoiseless")]
|
||||
{
|
||||
self.residual_buf.clear();
|
||||
}
|
||||
self.nnnoiseless = match self.method {
|
||||
NoiseCancelMethod::Nnnoiseless => Some(nnnoiseless::DenoiseState::new()),
|
||||
_ => None,
|
||||
};
|
||||
self.sonora = match self.method {
|
||||
NoiseCancelMethod::Sonora => Some(SonoraNoiseReducer::new()),
|
||||
_ => None,
|
||||
};
|
||||
self.residual_buf.clear();
|
||||
}
|
||||
|
||||
pub fn set_method(&mut self, method: NoiseCancelMethod) {
|
||||
@@ -72,25 +101,14 @@ impl NoiseReducer {
|
||||
self.init_method();
|
||||
}
|
||||
|
||||
pub fn method(&self) -> NoiseCancelMethod {
|
||||
self.method
|
||||
}
|
||||
|
||||
pub fn process(&mut self, samples: &mut [f32]) {
|
||||
pub fn process(&mut self, _samples: &mut [f32]) {
|
||||
match self.method {
|
||||
NoiseCancelMethod::None => {}
|
||||
#[cfg(feature = "nnnoiseless")]
|
||||
NoiseCancelMethod::Nnnoiseless => self.process_nnnoiseless(samples),
|
||||
#[cfg(not(feature = "nnnoiseless"))]
|
||||
NoiseCancelMethod::Nnnoiseless => {}
|
||||
#[cfg(feature = "sonora")]
|
||||
NoiseCancelMethod::Sonora => self.process_sonora(samples),
|
||||
#[cfg(not(feature = "sonora"))]
|
||||
NoiseCancelMethod::Sonora => {}
|
||||
NoiseCancelMethod::Nnnoiseless => self.process_nnnoiseless(_samples),
|
||||
NoiseCancelMethod::Sonora => self.process_sonora(_samples),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "nnnoiseless")]
|
||||
fn process_nnnoiseless(&mut self, samples: &mut [f32]) {
|
||||
let denoise = match &mut self.nnnoiseless {
|
||||
Some(d) => d,
|
||||
@@ -118,21 +136,20 @@ impl NoiseReducer {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "sonora")]
|
||||
fn process_sonora(&mut self, samples: &mut [f32]) {
|
||||
let ns = match &mut self.sonora {
|
||||
let reducer = match &mut self.sonora {
|
||||
Some(s) => s,
|
||||
None => return,
|
||||
};
|
||||
|
||||
// Sonora processes 10ms frames (480 samples at 48kHz)
|
||||
let frame_size = 480;
|
||||
let frame_size = SONORA_FRAME_SIZE;
|
||||
let mut offset = 0;
|
||||
|
||||
while offset + frame_size <= samples.len() {
|
||||
let frame = &mut samples[offset..offset + frame_size];
|
||||
ns.process(frame);
|
||||
reducer.process(frame);
|
||||
offset += frame_size;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use crate::types::{AppSettings, BookmarkInfo};
|
||||
|
||||
pub(crate) fn load_bookmarks() -> Vec<BookmarkInfo> {
|
||||
let Some(path) = bookmarks_config_path() else {
|
||||
return default_bookmarks();
|
||||
};
|
||||
|
||||
let Ok(raw) = fs::read_to_string(path) else {
|
||||
return default_bookmarks();
|
||||
};
|
||||
|
||||
match serde_json::from_str::<Vec<BookmarkInfo>>(&raw) {
|
||||
Ok(bookmarks) if !bookmarks.is_empty() => bookmarks,
|
||||
_ => default_bookmarks(),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn persist_bookmarks(bookmarks: &[BookmarkInfo]) {
|
||||
let Some(path) = bookmarks_config_path() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let Some(parent) = path.parent() else {
|
||||
return;
|
||||
};
|
||||
|
||||
if fs::create_dir_all(parent).is_err() {
|
||||
return;
|
||||
}
|
||||
|
||||
let Ok(raw) = serde_json::to_string_pretty(bookmarks) else {
|
||||
return;
|
||||
};
|
||||
|
||||
if let Err(error) = fs::write(path, raw) {
|
||||
tracing::warn!("Failed to persist bookmarks: {error}");
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn load_settings() -> AppSettings {
|
||||
let Some(path) = settings_config_path() else {
|
||||
return AppSettings::default();
|
||||
};
|
||||
|
||||
let Ok(raw) = fs::read_to_string(path) else {
|
||||
return AppSettings::default();
|
||||
};
|
||||
|
||||
serde_json::from_str::<AppSettings>(&raw).unwrap_or_default()
|
||||
}
|
||||
|
||||
pub(crate) fn persist_settings(settings: &AppSettings) {
|
||||
let Some(path) = settings_config_path() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let Some(parent) = path.parent() else {
|
||||
return;
|
||||
};
|
||||
|
||||
if fs::create_dir_all(parent).is_err() {
|
||||
return;
|
||||
}
|
||||
|
||||
let Ok(raw) = serde_json::to_string_pretty(settings) else {
|
||||
return;
|
||||
};
|
||||
|
||||
if let Err(error) = fs::write(path, raw) {
|
||||
tracing::warn!("Failed to persist settings: {error}");
|
||||
}
|
||||
}
|
||||
|
||||
fn default_bookmarks() -> Vec<BookmarkInfo> {
|
||||
vec![
|
||||
BookmarkInfo {
|
||||
name: "Local Server".to_string(),
|
||||
address: "127.0.0.1".to_string(),
|
||||
port: 9987,
|
||||
nickname: Some("User".to_string()),
|
||||
favorite: true,
|
||||
last_used_at: None,
|
||||
},
|
||||
BookmarkInfo {
|
||||
name: "KR TeamSpeak".to_string(),
|
||||
address: "kr.teamspeak.app".to_string(),
|
||||
port: 9987,
|
||||
nickname: None,
|
||||
favorite: true,
|
||||
last_used_at: None,
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
fn config_dir() -> Option<PathBuf> {
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
std::env::var_os("APPDATA").map(|base| PathBuf::from(base).join("re-teamspeak"))
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
{
|
||||
if let Some(base) = std::env::var_os("XDG_CONFIG_HOME") {
|
||||
return Some(PathBuf::from(base).join("re-teamspeak"));
|
||||
}
|
||||
std::env::var_os("HOME")
|
||||
.map(|home| PathBuf::from(home).join(".config").join("re-teamspeak"))
|
||||
}
|
||||
}
|
||||
|
||||
fn bookmarks_config_path() -> Option<PathBuf> {
|
||||
config_dir().map(|path| path.join("bookmarks.json"))
|
||||
}
|
||||
|
||||
fn settings_config_path() -> Option<PathBuf> {
|
||||
config_dir().map(|path| path.join("settings.json"))
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
use iced::futures::StreamExt;
|
||||
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::{mpsc, Mutex};
|
||||
|
||||
use tsclientlib::sync::{SyncConnection, SyncConnectionHandle, SyncStreamItem};
|
||||
use tsclientlib::AudioEvent;
|
||||
use tsproto_packets::packets::{AudioData, CodecType, OutAudio};
|
||||
|
||||
use crate::audio;
|
||||
use crate::noise_cancel;
|
||||
use crate::types::TsEvent;
|
||||
|
||||
pub async fn run_connection(
|
||||
con: SyncConnection,
|
||||
event_tx: mpsc::Sender<TsEvent>,
|
||||
audio: Arc<Mutex<audio::AudioPlayback>>,
|
||||
) {
|
||||
let mut stream = con;
|
||||
while let Some(item) = stream.next().await {
|
||||
let ts_event = match item {
|
||||
Ok(SyncStreamItem::BookEvents(events)) => TsEvent::BookEvents(events),
|
||||
Ok(SyncStreamItem::MessageEvent(msg)) => TsEvent::MessageEvent(msg),
|
||||
Ok(SyncStreamItem::AudioChange(change)) => match change {
|
||||
AudioEvent::CanSendAudio(can) => TsEvent::AudioChange(can, true),
|
||||
AudioEvent::CanReceiveAudio(can) => TsEvent::AudioChange(false, can),
|
||||
},
|
||||
Ok(SyncStreamItem::IdentityLevelIncreasing(level)) => {
|
||||
TsEvent::IdentityLevelIncreasing(level)
|
||||
}
|
||||
Ok(SyncStreamItem::IdentityLevelIncreased) => TsEvent::IdentityLevelIncreased,
|
||||
Ok(SyncStreamItem::DisconnectedTemporarily(_)) => TsEvent::DisconnectedTemporarily,
|
||||
Ok(SyncStreamItem::NetworkStatsUpdated) => continue,
|
||||
Ok(SyncStreamItem::Audio(audio_buf)) => {
|
||||
let playback = audio.lock().await;
|
||||
playback.send_packet(audio_buf);
|
||||
continue;
|
||||
}
|
||||
Err(e) => TsEvent::Error(e.to_string()),
|
||||
};
|
||||
if event_tx.send(ts_event).await.is_err() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
let _ = event_tx.send(TsEvent::Disconnected).await;
|
||||
}
|
||||
|
||||
pub fn start_transmission(
|
||||
mic: Arc<Mutex<audio::Microphone>>,
|
||||
handle: Arc<Mutex<Option<SyncConnectionHandle>>>,
|
||||
device_name: Option<String>,
|
||||
noise_reducer: Arc<Mutex<noise_cancel::NoiseReducer>>,
|
||||
) {
|
||||
let (sample_tx, sample_rx) = std::sync::mpsc::channel();
|
||||
|
||||
tokio::spawn(async move {
|
||||
{
|
||||
let mut mic_guard = mic.lock().await;
|
||||
if mic_guard.start(device_name.as_deref(), sample_tx).is_err() {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
let mut enc = match audio::OpusEncoderState::new() {
|
||||
Ok(e) => e,
|
||||
Err(_) => return,
|
||||
};
|
||||
|
||||
let (audio_tx, mut audio_rx) = tokio::sync::mpsc::channel::<Vec<u8>>(100);
|
||||
|
||||
let send_handle = handle.clone();
|
||||
tokio::spawn(async move {
|
||||
while let Some(data) = audio_rx.recv().await {
|
||||
let mut guard = send_handle.lock().await;
|
||||
if let Some(ref mut h) = *guard {
|
||||
let _ = h
|
||||
.with_connection(move |con| {
|
||||
let packet = OutAudio::new(&AudioData::C2S {
|
||||
id: 0,
|
||||
codec: CodecType::OpusVoice,
|
||||
data: &data,
|
||||
});
|
||||
let _ = con.send_audio(packet);
|
||||
Ok::<(), ()>(())
|
||||
})
|
||||
.await;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
loop {
|
||||
match sample_rx.recv() {
|
||||
Ok(samples) => {
|
||||
let mut nr = noise_reducer.lock().await;
|
||||
let mut samples_copy = samples.clone();
|
||||
nr.process(&mut samples_copy);
|
||||
enc.encode_and_send(&samples_copy, &audio_tx);
|
||||
}
|
||||
Err(_) => break,
|
||||
}
|
||||
}
|
||||
|
||||
let mut mic_guard = mic.lock().await;
|
||||
mic_guard.stop();
|
||||
});
|
||||
}
|
||||
|
||||
pub fn stop_transmission(
|
||||
mic: Arc<Mutex<audio::Microphone>>,
|
||||
handle: Arc<Mutex<Option<SyncConnectionHandle>>>,
|
||||
) {
|
||||
tokio::spawn(async move {
|
||||
{
|
||||
let mut mic_guard = mic.lock().await;
|
||||
mic_guard.stop();
|
||||
}
|
||||
let mut guard = handle.lock().await;
|
||||
if let Some(ref mut h) = *guard {
|
||||
let _ = h
|
||||
.with_connection(move |con| {
|
||||
let packet = OutAudio::new(&AudioData::C2S {
|
||||
id: 0,
|
||||
codec: CodecType::OpusVoice,
|
||||
data: &[],
|
||||
});
|
||||
let _ = con.send_audio(packet);
|
||||
Ok::<(), ()>(())
|
||||
})
|
||||
.await;
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -1,39 +1,113 @@
|
||||
use iced::widget::{button, container, text_input};
|
||||
use iced::{Background, Border, Color, Shadow, Theme};
|
||||
|
||||
// Apple-inspired dark palette
|
||||
pub const BG_PRIMARY: Color = Color::from_rgb(0.11, 0.11, 0.118); // #1C1C1E
|
||||
pub const BG_SECONDARY: Color = Color::from_rgb(0.17, 0.17, 0.18); // #2C2C2E
|
||||
pub const BG_TERTIARY: Color = Color::from_rgb(0.22, 0.22, 0.235); // #38383A
|
||||
pub const BG_ELEVATED: Color = Color::from_rgb(0.29, 0.29, 0.305); // #48484A
|
||||
pub const TEXT_PRIMARY: Color = Color::from_rgb(0.95, 0.95, 0.97); // #F2F2F7
|
||||
pub const TEXT_SECONDARY: Color = Color::from_rgb(0.60, 0.60, 0.63); // #98989D
|
||||
pub const TEXT_TERTIARY: Color = Color::from_rgb(0.42, 0.42, 0.44); // #6B6B70
|
||||
pub const ACCENT: Color = Color::from_rgb(0.29, 0.56, 1.0); // #4A90D9
|
||||
pub const ACCENT_HOVER: Color = Color::from_rgb(0.36, 0.63, 1.0); // #5CA0FF
|
||||
pub const SUCCESS: Color = Color::from_rgb(0.29, 0.85, 0.56); // #4AD98F
|
||||
pub const DANGER: Color = Color::from_rgb(1.0, 0.27, 0.23); // #FF453A
|
||||
pub const SEPARATOR: Color = Color::from_rgb(0.33, 0.33, 0.35); // #545458
|
||||
pub const SIDEBAR_BG: Color = Color::from_rgb(0.09, 0.09, 0.098); // #171719
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
|
||||
pub enum AppearanceMode {
|
||||
Light,
|
||||
Dark,
|
||||
}
|
||||
|
||||
pub fn dark_theme() -> Theme {
|
||||
impl AppearanceMode {
|
||||
pub fn label(self) -> &'static str {
|
||||
match self {
|
||||
Self::Light => "Light",
|
||||
Self::Dark => "Dark",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 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(
|
||||
"ReTeamSpeak".to_string(),
|
||||
"ReTeamSpeak Light".to_string(),
|
||||
iced::theme::Palette {
|
||||
background: BG_PRIMARY,
|
||||
text: TEXT_PRIMARY,
|
||||
primary: ACCENT,
|
||||
background: LIGHT_BG,
|
||||
text: LIGHT_TEXT,
|
||||
primary: LIGHT_ACCENT,
|
||||
success: SUCCESS,
|
||||
danger: DANGER,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
pub fn sidebar_container(_theme: &Theme) -> container::Style {
|
||||
pub fn dark_theme() -> Theme {
|
||||
Theme::custom(
|
||||
"ReTeamSpeak Dark".to_string(),
|
||||
iced::theme::Palette {
|
||||
background: DARK_BG,
|
||||
text: DARK_TEXT,
|
||||
primary: DARK_ACCENT,
|
||||
success: SUCCESS,
|
||||
danger: DANGER,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
fn is_dark(theme: &Theme) -> bool {
|
||||
theme.palette().background.r < 0.2
|
||||
}
|
||||
|
||||
fn surface(theme: &Theme) -> Color {
|
||||
if is_dark(theme) { DARK_SURFACE } else { LIGHT_SURFACE }
|
||||
}
|
||||
|
||||
fn elevated(theme: &Theme) -> Color {
|
||||
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 }
|
||||
}
|
||||
|
||||
pub fn icon_color(theme: &Theme) -> Color {
|
||||
text_secondary(theme)
|
||||
}
|
||||
|
||||
pub fn nav_icon_color(theme: &Theme) -> Color {
|
||||
text_secondary(theme)
|
||||
}
|
||||
|
||||
fn border(theme: &Theme) -> Color {
|
||||
if is_dark(theme) { DARK_BORDER } else { LIGHT_BORDER }
|
||||
}
|
||||
|
||||
fn accent(theme: &Theme) -> Color {
|
||||
theme.palette().primary
|
||||
}
|
||||
|
||||
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,
|
||||
) }
|
||||
}
|
||||
|
||||
pub fn sidebar_container(theme: &Theme) -> container::Style {
|
||||
container::Style {
|
||||
background: Some(Background::Color(SIDEBAR_BG)),
|
||||
background: Some(Background::Color(elevated(theme))),
|
||||
border: Border {
|
||||
color: SEPARATOR,
|
||||
color: border(theme),
|
||||
width: 0.0,
|
||||
radius: 0.0.into(),
|
||||
},
|
||||
@@ -41,360 +115,238 @@ pub fn sidebar_container(_theme: &Theme) -> container::Style {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn card_container(_theme: &Theme) -> container::Style {
|
||||
pub fn main_panel_container(theme: &Theme) -> container::Style {
|
||||
container::Style {
|
||||
background: Some(Background::Color(BG_SECONDARY)),
|
||||
background: Some(Background::Color(theme.palette().background)),
|
||||
..container::Style::default()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn sidebar_section_container(theme: &Theme) -> container::Style {
|
||||
container::Style {
|
||||
background: Some(Background::Color(surface(theme))),
|
||||
border: Border {
|
||||
color: border(theme),
|
||||
width: 1.0,
|
||||
radius: 16.0.into(),
|
||||
},
|
||||
shadow: Shadow {
|
||||
color: Color::from_rgba(0.0, 0.0, 0.0, if is_dark(theme) { 0.18 } else { 0.08 }),
|
||||
offset: iced::Vector::new(0.0, 4.0),
|
||||
blur_radius: 16.0,
|
||||
},
|
||||
..container::Style::default()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn hero_container(theme: &Theme) -> container::Style {
|
||||
container::Style {
|
||||
background: Some(Background::Color(surface(theme))),
|
||||
border: Border {
|
||||
color: border(theme),
|
||||
width: 1.0,
|
||||
radius: 20.0.into(),
|
||||
},
|
||||
shadow: Shadow {
|
||||
color: Color::from_rgba(0.0, 0.0, 0.0, if is_dark(theme) { 0.24 } else { 0.10 }),
|
||||
offset: iced::Vector::new(0.0, 8.0),
|
||||
blur_radius: 24.0,
|
||||
},
|
||||
..container::Style::default()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn card_container(theme: &Theme) -> container::Style {
|
||||
container::Style {
|
||||
background: Some(Background::Color(surface(theme))),
|
||||
border: Border {
|
||||
color: border(theme),
|
||||
width: 1.0,
|
||||
radius: 16.0.into(),
|
||||
},
|
||||
shadow: Shadow {
|
||||
color: Color::from_rgba(0.0, 0.0, 0.0, if is_dark(theme) { 0.16 } else { 0.07 }),
|
||||
offset: iced::Vector::new(0.0, 4.0),
|
||||
blur_radius: 16.0,
|
||||
},
|
||||
..container::Style::default()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn voice_capsule_container(theme: &Theme) -> container::Style {
|
||||
container::Style {
|
||||
background: Some(Background::Color(elevated(theme))),
|
||||
border: Border {
|
||||
color: border(theme),
|
||||
width: 1.0,
|
||||
radius: 18.0.into(),
|
||||
},
|
||||
shadow: Shadow {
|
||||
color: Color::from_rgba(0.0, 0.0, 0.0, if is_dark(theme) { 0.20 } else { 0.08 }),
|
||||
offset: iced::Vector::new(0.0, 6.0),
|
||||
blur_radius: 18.0,
|
||||
},
|
||||
..container::Style::default()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn elevated_container(theme: &Theme) -> container::Style {
|
||||
container::Style {
|
||||
background: Some(Background::Color(elevated(theme))),
|
||||
border: Border {
|
||||
color: border(theme),
|
||||
width: 1.0,
|
||||
radius: 12.0.into(),
|
||||
},
|
||||
..container::Style::default()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn input_style(theme: &Theme, status: text_input::Status) -> text_input::Style {
|
||||
let base = match status {
|
||||
text_input::Status::Focused => accent(theme),
|
||||
text_input::Status::Hovered => text_secondary(theme),
|
||||
_ => border(theme),
|
||||
};
|
||||
|
||||
text_input::Style {
|
||||
background: Background::Color(elevated(theme)),
|
||||
border: Border {
|
||||
color: base,
|
||||
width: if matches!(status, text_input::Status::Focused) { 1.5 } else { 1.0 },
|
||||
radius: 12.0.into(),
|
||||
},
|
||||
icon: text_secondary(theme),
|
||||
placeholder: text_secondary(theme),
|
||||
value: theme.palette().text,
|
||||
selection: accent(theme),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn primary_button(theme: &Theme, status: button::Status) -> button::Style {
|
||||
let background = match status {
|
||||
button::Status::Hovered => accent_hover(theme),
|
||||
button::Status::Pressed => accent_hover(theme),
|
||||
button::Status::Disabled => elevated(theme),
|
||||
_ => accent(theme),
|
||||
};
|
||||
|
||||
button::Style {
|
||||
background: Some(Background::Color(background)),
|
||||
text_color: Color::WHITE,
|
||||
border: Border {
|
||||
color: Color::TRANSPARENT,
|
||||
width: 0.0,
|
||||
radius: 12.0.into(),
|
||||
},
|
||||
shadow: Shadow {
|
||||
color: Color::from_rgba(0.0, 0.0, 0.0, 0.3),
|
||||
offset: iced::Vector::new(0.0, 2.0),
|
||||
blur_radius: 8.0,
|
||||
color: Color::from_rgba(0.0, 0.0, 0.0, if is_dark(theme) { 0.20 } else { 0.10 }),
|
||||
offset: iced::Vector::new(0.0, 3.0),
|
||||
blur_radius: 10.0,
|
||||
},
|
||||
..container::Style::default()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn elevated_container(_theme: &Theme) -> container::Style {
|
||||
container::Style {
|
||||
background: Some(Background::Color(BG_TERTIARY)),
|
||||
pub fn secondary_button(theme: &Theme, status: button::Status) -> button::Style {
|
||||
let background = match status {
|
||||
button::Status::Hovered => elevated(theme),
|
||||
button::Status::Pressed => elevated(theme),
|
||||
_ => surface(theme),
|
||||
};
|
||||
|
||||
button::Style {
|
||||
background: Some(Background::Color(background)),
|
||||
text_color: theme.palette().text,
|
||||
border: Border {
|
||||
color: border(theme),
|
||||
width: 1.0,
|
||||
radius: 12.0.into(),
|
||||
},
|
||||
shadow: Shadow::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn danger_button(_theme: &Theme, _status: button::Status) -> button::Style {
|
||||
button::Style {
|
||||
background: Some(Background::Color(DANGER)),
|
||||
text_color: Color::WHITE,
|
||||
border: Border {
|
||||
color: Color::TRANSPARENT,
|
||||
width: 0.0,
|
||||
radius: 10.0.into(),
|
||||
radius: 12.0.into(),
|
||||
},
|
||||
..container::Style::default()
|
||||
shadow: Shadow::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn input_style(_theme: &Theme, status: text_input::Status) -> text_input::Style {
|
||||
match status {
|
||||
text_input::Status::Active => text_input::Style {
|
||||
background: Background::Color(BG_TERTIARY),
|
||||
border: Border {
|
||||
color: SEPARATOR,
|
||||
width: 1.0,
|
||||
radius: 8.0.into(),
|
||||
},
|
||||
icon: TEXT_SECONDARY,
|
||||
placeholder: TEXT_TERTIARY,
|
||||
value: TEXT_PRIMARY,
|
||||
selection: ACCENT,
|
||||
},
|
||||
text_input::Status::Focused => text_input::Style {
|
||||
background: Background::Color(BG_TERTIARY),
|
||||
border: Border {
|
||||
color: ACCENT,
|
||||
width: 1.5,
|
||||
radius: 8.0.into(),
|
||||
},
|
||||
icon: TEXT_SECONDARY,
|
||||
placeholder: TEXT_TERTIARY,
|
||||
value: TEXT_PRIMARY,
|
||||
selection: ACCENT,
|
||||
},
|
||||
text_input::Status::Hovered => text_input::Style {
|
||||
background: Background::Color(BG_TERTIARY),
|
||||
border: Border {
|
||||
color: TEXT_TERTIARY,
|
||||
width: 1.0,
|
||||
radius: 8.0.into(),
|
||||
},
|
||||
icon: TEXT_SECONDARY,
|
||||
placeholder: TEXT_TERTIARY,
|
||||
value: TEXT_PRIMARY,
|
||||
selection: ACCENT,
|
||||
},
|
||||
_ => text_input::Style {
|
||||
background: Background::Color(BG_TERTIARY),
|
||||
border: Border {
|
||||
color: SEPARATOR,
|
||||
width: 1.0,
|
||||
radius: 8.0.into(),
|
||||
},
|
||||
icon: TEXT_SECONDARY,
|
||||
placeholder: TEXT_TERTIARY,
|
||||
value: TEXT_PRIMARY,
|
||||
selection: ACCENT,
|
||||
pub fn nav_button_active(theme: &Theme, _status: button::Status) -> button::Style {
|
||||
button::Style {
|
||||
background: Some(Background::Color(accent(theme))),
|
||||
text_color: Color::WHITE,
|
||||
border: Border {
|
||||
color: Color::TRANSPARENT,
|
||||
width: 0.0,
|
||||
radius: 12.0.into(),
|
||||
},
|
||||
shadow: Shadow::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn primary_button(_theme: &Theme, status: button::Status) -> button::Style {
|
||||
match status {
|
||||
button::Status::Active => button::Style {
|
||||
background: Some(Background::Color(ACCENT)),
|
||||
text_color: Color::WHITE,
|
||||
border: Border {
|
||||
color: Color::TRANSPARENT,
|
||||
width: 0.0,
|
||||
radius: 8.0.into(),
|
||||
},
|
||||
shadow: Shadow {
|
||||
color: Color::from_rgba(0.29, 0.56, 1.0, 0.3),
|
||||
offset: iced::Vector::new(0.0, 1.0),
|
||||
blur_radius: 4.0,
|
||||
},
|
||||
pub fn nav_button_inactive(theme: &Theme, status: button::Status) -> button::Style {
|
||||
let background = if matches!(status, button::Status::Hovered) {
|
||||
surface(theme)
|
||||
} else {
|
||||
Color::TRANSPARENT
|
||||
};
|
||||
|
||||
button::Style {
|
||||
background: Some(Background::Color(background)),
|
||||
text_color: if matches!(status, button::Status::Hovered) {
|
||||
theme.palette().text
|
||||
} else {
|
||||
text_secondary(theme)
|
||||
},
|
||||
button::Status::Hovered => button::Style {
|
||||
background: Some(Background::Color(ACCENT_HOVER)),
|
||||
text_color: Color::WHITE,
|
||||
border: Border {
|
||||
color: Color::TRANSPARENT,
|
||||
width: 0.0,
|
||||
radius: 8.0.into(),
|
||||
},
|
||||
shadow: Shadow {
|
||||
color: Color::from_rgba(0.29, 0.56, 1.0, 0.4),
|
||||
offset: iced::Vector::new(0.0, 2.0),
|
||||
blur_radius: 6.0,
|
||||
},
|
||||
},
|
||||
button::Status::Pressed => button::Style {
|
||||
background: Some(Background::Color(Color::from_rgb(0.22, 0.44, 0.80))),
|
||||
text_color: Color::WHITE,
|
||||
border: Border {
|
||||
color: Color::TRANSPARENT,
|
||||
width: 0.0,
|
||||
radius: 8.0.into(),
|
||||
},
|
||||
shadow: Shadow::default(),
|
||||
},
|
||||
button::Status::Disabled => button::Style {
|
||||
background: Some(Background::Color(BG_ELEVATED)),
|
||||
text_color: TEXT_TERTIARY,
|
||||
border: Border {
|
||||
color: Color::TRANSPARENT,
|
||||
width: 0.0,
|
||||
radius: 8.0.into(),
|
||||
},
|
||||
shadow: Shadow::default(),
|
||||
border: Border {
|
||||
color: Color::TRANSPARENT,
|
||||
width: 0.0,
|
||||
radius: 12.0.into(),
|
||||
},
|
||||
shadow: Shadow::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn secondary_button(_theme: &Theme, status: button::Status) -> button::Style {
|
||||
match status {
|
||||
button::Status::Active => button::Style {
|
||||
background: Some(Background::Color(BG_TERTIARY)),
|
||||
text_color: TEXT_PRIMARY,
|
||||
border: Border {
|
||||
color: SEPARATOR,
|
||||
width: 1.0,
|
||||
radius: 8.0.into(),
|
||||
},
|
||||
shadow: Shadow::default(),
|
||||
pub fn bookmark_button_selected(theme: &Theme, _status: button::Status) -> button::Style {
|
||||
button::Style {
|
||||
background: Some(Background::Color(elevated(theme))),
|
||||
text_color: theme.palette().text,
|
||||
border: Border {
|
||||
color: accent(theme),
|
||||
width: 1.0,
|
||||
radius: 14.0.into(),
|
||||
},
|
||||
button::Status::Hovered => button::Style {
|
||||
background: Some(Background::Color(BG_ELEVATED)),
|
||||
text_color: TEXT_PRIMARY,
|
||||
border: Border {
|
||||
color: TEXT_TERTIARY,
|
||||
width: 1.0,
|
||||
radius: 8.0.into(),
|
||||
},
|
||||
shadow: Shadow::default(),
|
||||
},
|
||||
button::Status::Pressed => button::Style {
|
||||
background: Some(Background::Color(BG_SECONDARY)),
|
||||
text_color: TEXT_PRIMARY,
|
||||
border: Border {
|
||||
color: SEPARATOR,
|
||||
width: 1.0,
|
||||
radius: 8.0.into(),
|
||||
},
|
||||
shadow: Shadow::default(),
|
||||
},
|
||||
_ => button::Style::default(),
|
||||
shadow: Shadow::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn danger_button(_theme: &Theme, status: button::Status) -> button::Style {
|
||||
match status {
|
||||
button::Status::Active => button::Style {
|
||||
background: Some(Background::Color(DANGER)),
|
||||
text_color: Color::WHITE,
|
||||
border: Border {
|
||||
color: Color::TRANSPARENT,
|
||||
width: 0.0,
|
||||
radius: 8.0.into(),
|
||||
},
|
||||
shadow: Shadow::default(),
|
||||
pub fn bookmark_button(theme: &Theme, status: button::Status) -> button::Style {
|
||||
let background = if matches!(status, button::Status::Hovered) {
|
||||
elevated(theme)
|
||||
} else {
|
||||
Color::TRANSPARENT
|
||||
};
|
||||
|
||||
button::Style {
|
||||
background: Some(Background::Color(background)),
|
||||
text_color: if matches!(status, button::Status::Hovered) {
|
||||
theme.palette().text
|
||||
} else {
|
||||
text_secondary(theme)
|
||||
},
|
||||
button::Status::Hovered => button::Style {
|
||||
background: Some(Background::Color(Color::from_rgb(1.0, 0.35, 0.30))),
|
||||
text_color: Color::WHITE,
|
||||
border: Border {
|
||||
color: Color::TRANSPARENT,
|
||||
width: 0.0,
|
||||
radius: 8.0.into(),
|
||||
},
|
||||
shadow: Shadow::default(),
|
||||
},
|
||||
_ => button::Style {
|
||||
background: Some(Background::Color(DANGER)),
|
||||
text_color: Color::WHITE,
|
||||
border: Border {
|
||||
color: Color::TRANSPARENT,
|
||||
width: 0.0,
|
||||
radius: 8.0.into(),
|
||||
},
|
||||
shadow: Shadow::default(),
|
||||
border: Border {
|
||||
color: Color::TRANSPARENT,
|
||||
width: 0.0,
|
||||
radius: 14.0.into(),
|
||||
},
|
||||
shadow: Shadow::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn nav_button_active(_theme: &Theme, status: button::Status) -> button::Style {
|
||||
match status {
|
||||
button::Status::Active => button::Style {
|
||||
background: Some(Background::Color(BG_TERTIARY)),
|
||||
text_color: TEXT_PRIMARY,
|
||||
border: Border {
|
||||
color: Color::TRANSPARENT,
|
||||
width: 0.0,
|
||||
radius: 8.0.into(),
|
||||
},
|
||||
shadow: Shadow::default(),
|
||||
},
|
||||
button::Status::Hovered => button::Style {
|
||||
background: Some(Background::Color(BG_ELEVATED)),
|
||||
text_color: TEXT_PRIMARY,
|
||||
border: Border {
|
||||
color: Color::TRANSPARENT,
|
||||
width: 0.0,
|
||||
radius: 8.0.into(),
|
||||
},
|
||||
shadow: Shadow::default(),
|
||||
},
|
||||
_ => button::Style {
|
||||
background: Some(Background::Color(BG_TERTIARY)),
|
||||
text_color: TEXT_PRIMARY,
|
||||
border: Border {
|
||||
color: Color::TRANSPARENT,
|
||||
width: 0.0,
|
||||
radius: 8.0.into(),
|
||||
},
|
||||
shadow: Shadow::default(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn nav_button_inactive(_theme: &Theme, status: button::Status) -> button::Style {
|
||||
match status {
|
||||
button::Status::Active => button::Style {
|
||||
background: Some(Background::Color(Color::TRANSPARENT)),
|
||||
text_color: TEXT_SECONDARY,
|
||||
border: Border {
|
||||
color: Color::TRANSPARENT,
|
||||
width: 0.0,
|
||||
radius: 8.0.into(),
|
||||
},
|
||||
shadow: Shadow::default(),
|
||||
},
|
||||
button::Status::Hovered => button::Style {
|
||||
background: Some(Background::Color(BG_SECONDARY)),
|
||||
text_color: TEXT_PRIMARY,
|
||||
border: Border {
|
||||
color: Color::TRANSPARENT,
|
||||
width: 0.0,
|
||||
radius: 8.0.into(),
|
||||
},
|
||||
shadow: Shadow::default(),
|
||||
},
|
||||
_ => button::Style {
|
||||
background: Some(Background::Color(Color::TRANSPARENT)),
|
||||
text_color: TEXT_SECONDARY,
|
||||
border: Border {
|
||||
color: Color::TRANSPARENT,
|
||||
width: 0.0,
|
||||
radius: 8.0.into(),
|
||||
},
|
||||
shadow: Shadow::default(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn bookmark_button_selected(_theme: &Theme, status: button::Status) -> button::Style {
|
||||
match status {
|
||||
button::Status::Active => button::Style {
|
||||
background: Some(Background::Color(BG_TERTIARY)),
|
||||
text_color: TEXT_PRIMARY,
|
||||
border: Border {
|
||||
color: ACCENT,
|
||||
width: 1.0,
|
||||
radius: 10.0.into(),
|
||||
},
|
||||
shadow: Shadow::default(),
|
||||
},
|
||||
button::Status::Hovered => button::Style {
|
||||
background: Some(Background::Color(BG_ELEVATED)),
|
||||
text_color: TEXT_PRIMARY,
|
||||
border: Border {
|
||||
color: ACCENT,
|
||||
width: 1.0,
|
||||
radius: 10.0.into(),
|
||||
},
|
||||
shadow: Shadow::default(),
|
||||
},
|
||||
_ => button::Style {
|
||||
background: Some(Background::Color(BG_TERTIARY)),
|
||||
text_color: TEXT_PRIMARY,
|
||||
border: Border {
|
||||
color: ACCENT,
|
||||
width: 1.0,
|
||||
radius: 10.0.into(),
|
||||
},
|
||||
shadow: Shadow::default(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn bookmark_button(_theme: &Theme, status: button::Status) -> button::Style {
|
||||
match status {
|
||||
button::Status::Active => button::Style {
|
||||
background: Some(Background::Color(Color::TRANSPARENT)),
|
||||
text_color: TEXT_SECONDARY,
|
||||
border: Border {
|
||||
color: Color::TRANSPARENT,
|
||||
width: 0.0,
|
||||
radius: 10.0.into(),
|
||||
},
|
||||
shadow: Shadow::default(),
|
||||
},
|
||||
button::Status::Hovered => button::Style {
|
||||
background: Some(Background::Color(BG_SECONDARY)),
|
||||
text_color: TEXT_PRIMARY,
|
||||
border: Border {
|
||||
color: Color::TRANSPARENT,
|
||||
width: 0.0,
|
||||
radius: 10.0.into(),
|
||||
},
|
||||
shadow: Shadow::default(),
|
||||
},
|
||||
_ => button::Style {
|
||||
background: Some(Background::Color(Color::TRANSPARENT)),
|
||||
text_color: TEXT_SECONDARY,
|
||||
border: Border {
|
||||
color: Color::TRANSPARENT,
|
||||
width: 0.0,
|
||||
radius: 10.0.into(),
|
||||
},
|
||||
shadow: Shadow::default(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
pub fn separator_line<'a>() -> iced::widget::Rule<'a, Theme> {
|
||||
iced::widget::horizontal_rule(1).style(|_theme: &Theme| {
|
||||
iced::widget::rule::Style {
|
||||
color: SEPARATOR,
|
||||
width: 1,
|
||||
radius: 0.0.into(),
|
||||
fill_mode: iced::widget::rule::FillMode::Full,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -0,0 +1,150 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tsclientlib::events::Event;
|
||||
use tsclientlib::{ChannelId, ClientId, Identity};
|
||||
|
||||
use crate::{audio, noise_cancel};
|
||||
use crate::theme;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub(crate) enum Page {
|
||||
Home,
|
||||
Bookmarks,
|
||||
Identities,
|
||||
Settings,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
#[allow(clippy::enum_variant_names)]
|
||||
pub(crate) enum Message {
|
||||
PageChanged(Page),
|
||||
SelectBookmark(Option<usize>),
|
||||
AddBookmark,
|
||||
CancelAddBookmark,
|
||||
DeleteBookmark(usize),
|
||||
BookmarkNameChanged(String),
|
||||
BookmarkAddressChanged(String),
|
||||
BookmarkPortChanged(String),
|
||||
NicknameChanged(String),
|
||||
PasswordChanged(String),
|
||||
Connect,
|
||||
JoinChannel(ChannelId),
|
||||
ToggleChannelCollapsed(ChannelId),
|
||||
Disconnect,
|
||||
TsEvent(TsEvent),
|
||||
ChannelSearchChanged(String),
|
||||
ToggleLocalClientMute(ClientId),
|
||||
IncreaseClientVolume(ClientId),
|
||||
DecreaseClientVolume(ClientId),
|
||||
MessageInputChanged(String),
|
||||
SendChannelMessage,
|
||||
PttPressed,
|
||||
PttReleased,
|
||||
ToggleTalkMode,
|
||||
SetNoiseCancelMethod(noise_cancel::NoiseCancelMethod),
|
||||
StartContinuous,
|
||||
StopContinuous,
|
||||
MicSamples(Vec<f32>),
|
||||
ToggleMicMute,
|
||||
ToggleSpeakerMute,
|
||||
ToggleHeadsetMute,
|
||||
ToggleAfk,
|
||||
IdentityStringChanged(String),
|
||||
IdentityFilePathChanged(String),
|
||||
ImportIdentityFromString,
|
||||
ImportIdentityFromFile,
|
||||
IdentityImported(Result<Identity, String>),
|
||||
SetOutputDevice(String),
|
||||
SetInputDevice(String),
|
||||
SetAppearance(theme::AppearanceMode),
|
||||
Noop,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) enum TsEvent {
|
||||
Connected,
|
||||
BookEvents(Vec<Event>),
|
||||
MessageEvent(tsclientlib::InMessage),
|
||||
AudioChange(bool, bool),
|
||||
IdentityLevelIncreasing(u8),
|
||||
IdentityLevelIncreased,
|
||||
DisconnectedTemporarily,
|
||||
Disconnected,
|
||||
Error(String),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(default)]
|
||||
pub(crate) struct BookmarkInfo {
|
||||
pub(crate) name: String,
|
||||
pub(crate) address: String,
|
||||
pub(crate) port: u16,
|
||||
pub(crate) nickname: Option<String>,
|
||||
pub(crate) favorite: bool,
|
||||
pub(crate) last_used_at: Option<i64>,
|
||||
}
|
||||
|
||||
impl Default for BookmarkInfo {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
name: String::new(),
|
||||
address: String::new(),
|
||||
port: 9987,
|
||||
nickname: None,
|
||||
favorite: false,
|
||||
last_used_at: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct ChannelEntry {
|
||||
pub(crate) id: ChannelId,
|
||||
pub(crate) name: String,
|
||||
#[allow(dead_code)]
|
||||
pub(crate) parent: ChannelId,
|
||||
pub(crate) order: ChannelId,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct ClientEntry {
|
||||
pub(crate) id: ClientId,
|
||||
pub(crate) name: String,
|
||||
pub(crate) channel: ChannelId,
|
||||
pub(crate) input_muted: bool,
|
||||
#[allow(dead_code)]
|
||||
pub(crate) output_muted: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct ChatMessage {
|
||||
pub(crate) invoker_name: String,
|
||||
pub(crate) message: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct LocalClientAudio {
|
||||
pub(crate) muted: bool,
|
||||
pub(crate) volume: f32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(default)]
|
||||
pub(crate) struct AppSettings {
|
||||
pub(crate) appearance_mode: theme::AppearanceMode,
|
||||
pub(crate) selected_output_device: String,
|
||||
pub(crate) selected_input_device: String,
|
||||
pub(crate) talk_mode: audio::TalkMode,
|
||||
pub(crate) noise_cancel_method: noise_cancel::NoiseCancelMethod,
|
||||
}
|
||||
|
||||
impl Default for AppSettings {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
appearance_mode: theme::AppearanceMode::Dark,
|
||||
selected_output_device: String::new(),
|
||||
selected_input_device: String::new(),
|
||||
talk_mode: audio::TalkMode::PushToTalk,
|
||||
noise_cancel_method: noise_cancel::NoiseCancelMethod::None,
|
||||
}
|
||||
}
|
||||
}
|
||||