feat: Apple-style dark UI, bookmark CRUD, join channel, settings
UI Redesign: - Custom dark theme with Apple-inspired palette (SIDEBAR_BG, BG_PRIMARY, etc.) - Rounded cards with subtle shadows and depth - Styled inputs with focus/hover states - Styled buttons (primary, secondary, danger, nav, bookmark) - Proper visual hierarchy with generous whitespace - Sidebar with dark background, cards with elevated surfaces Features: - Bookmark CRUD: add new server, delete existing - Join channel: click channel in list to move to it - Settings page with audio device info placeholder - ServerQuery: configurable command input - Chat messages with invoker name display - Channel list shows current channel highlighted - Client list with mute indicators - Error display with danger styling - Identity level progress indicator Architecture: - Custom theme module (theme.rs) with all styles - Bookmark editing form in sidebar - Channel click handler sends client_move command - 69 tests passing, clippy clean
This commit is contained in:
+413
-241
@@ -1,7 +1,7 @@
|
||||
use iced::futures::sink::SinkExt;
|
||||
use iced::futures::StreamExt;
|
||||
use iced::widget::{button, column, container, horizontal_rule, horizontal_space, row, scrollable, text, text_input, vertical_space};
|
||||
use iced::{Element, Length, Subscription, Task};
|
||||
use iced::widget::{button, column, container, horizontal_space, row, scrollable, text, text_input, vertical_space};
|
||||
use iced::{Element, Length, Subscription, Task, Theme};
|
||||
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::{mpsc, Mutex};
|
||||
@@ -11,11 +11,14 @@ use tsclientlib::{ChannelId, ClientId, Connection, DisconnectOptions, MessageTar
|
||||
use tsclientlib::events::{Event, PropertyId};
|
||||
use tsclientlib::prelude::*;
|
||||
|
||||
mod theme;
|
||||
|
||||
#[cfg(feature = "audio")]
|
||||
mod audio;
|
||||
|
||||
fn main() -> iced::Result {
|
||||
iced::application("ReTeamSpeak", App::update, App::view)
|
||||
.theme(App::theme)
|
||||
.subscription(App::subscription)
|
||||
.run_with(App::new)
|
||||
}
|
||||
@@ -32,15 +35,22 @@ enum Page {
|
||||
enum Message {
|
||||
PageChanged(Page),
|
||||
SelectBookmark(Option<usize>),
|
||||
AddBookmark,
|
||||
DeleteBookmark(usize),
|
||||
BookmarkNameChanged(String),
|
||||
BookmarkAddressChanged(String),
|
||||
BookmarkPortChanged(String),
|
||||
NicknameChanged(String),
|
||||
PasswordChanged(String),
|
||||
Connect,
|
||||
JoinChannel(ChannelId),
|
||||
Disconnect,
|
||||
TsEvent(TsEvent),
|
||||
MessageInputChanged(String),
|
||||
SendChannelMessage,
|
||||
QueryAddressChanged(String),
|
||||
QueryPortChanged(String),
|
||||
QueryCommandChanged(String),
|
||||
RunServerQuery,
|
||||
QueryResponse(String),
|
||||
QueryError(String),
|
||||
@@ -97,6 +107,10 @@ struct App {
|
||||
page: Page,
|
||||
bookmarks: Vec<BookmarkInfo>,
|
||||
selected_bookmark: Option<usize>,
|
||||
editing_bookmark: bool,
|
||||
bm_name_input: String,
|
||||
bm_address_input: String,
|
||||
bm_port_input: String,
|
||||
nickname: String,
|
||||
password: String,
|
||||
connected: bool,
|
||||
@@ -113,6 +127,7 @@ struct App {
|
||||
message_input: String,
|
||||
query_address: String,
|
||||
query_port: u16,
|
||||
query_command: String,
|
||||
query_loading: bool,
|
||||
query_response: Option<String>,
|
||||
query_error: Option<String>,
|
||||
@@ -142,6 +157,10 @@ impl App {
|
||||
},
|
||||
],
|
||||
selected_bookmark: None,
|
||||
editing_bookmark: false,
|
||||
bm_name_input: String::new(),
|
||||
bm_address_input: String::new(),
|
||||
bm_port_input: "9987".to_string(),
|
||||
nickname: String::new(),
|
||||
password: String::new(),
|
||||
connected: false,
|
||||
@@ -158,6 +177,7 @@ impl App {
|
||||
message_input: String::new(),
|
||||
query_address: String::new(),
|
||||
query_port: 10011,
|
||||
query_command: "help".to_string(),
|
||||
query_loading: false,
|
||||
query_response: None,
|
||||
query_error: None,
|
||||
@@ -171,6 +191,10 @@ impl App {
|
||||
(app, Task::none())
|
||||
}
|
||||
|
||||
fn theme(&self) -> Theme {
|
||||
theme::dark_theme()
|
||||
}
|
||||
|
||||
fn update(&mut self, message: Message) -> Task<Message> {
|
||||
match message {
|
||||
Message::PageChanged(page) => {
|
||||
@@ -179,15 +203,44 @@ impl App {
|
||||
}
|
||||
Message::SelectBookmark(idx) => {
|
||||
self.selected_bookmark = idx;
|
||||
self.editing_bookmark = false;
|
||||
if let Some(i) = idx {
|
||||
if let Some(b) = self.bookmarks.get(i) {
|
||||
if let Some(nick) = &b.nickname {
|
||||
self.nickname = nick.clone();
|
||||
}
|
||||
self.nickname = b.nickname.clone().unwrap_or_default();
|
||||
}
|
||||
}
|
||||
Task::none()
|
||||
}
|
||||
Message::AddBookmark => {
|
||||
self.editing_bookmark = true;
|
||||
self.bm_name_input.clear();
|
||||
self.bm_address_input.clear();
|
||||
self.bm_port_input = "9987".to_string();
|
||||
Task::none()
|
||||
}
|
||||
Message::DeleteBookmark(idx) => {
|
||||
if idx < self.bookmarks.len() {
|
||||
self.bookmarks.remove(idx);
|
||||
if self.selected_bookmark == Some(idx) {
|
||||
self.selected_bookmark = None;
|
||||
} else if self.selected_bookmark.is_some_and(|s| s > idx) {
|
||||
self.selected_bookmark = self.selected_bookmark.map(|s| s - 1);
|
||||
}
|
||||
}
|
||||
Task::none()
|
||||
}
|
||||
Message::BookmarkNameChanged(n) => {
|
||||
self.bm_name_input = n;
|
||||
Task::none()
|
||||
}
|
||||
Message::BookmarkAddressChanged(a) => {
|
||||
self.bm_address_input = a;
|
||||
Task::none()
|
||||
}
|
||||
Message::BookmarkPortChanged(p) => {
|
||||
self.bm_port_input = p;
|
||||
Task::none()
|
||||
}
|
||||
Message::NicknameChanged(n) => {
|
||||
self.nickname = n;
|
||||
Task::none()
|
||||
@@ -197,6 +250,20 @@ impl App {
|
||||
Task::none()
|
||||
}
|
||||
Message::Connect => {
|
||||
if self.editing_bookmark {
|
||||
if !self.bm_name_input.is_empty() && !self.bm_address_input.is_empty() {
|
||||
self.bookmarks.push(BookmarkInfo {
|
||||
name: self.bm_name_input.clone(),
|
||||
address: self.bm_address_input.clone(),
|
||||
port: self.bm_port_input.parse().unwrap_or(9987),
|
||||
nickname: None,
|
||||
});
|
||||
self.selected_bookmark = Some(self.bookmarks.len() - 1);
|
||||
self.editing_bookmark = false;
|
||||
}
|
||||
return Task::none();
|
||||
}
|
||||
|
||||
let bookmark = match self.selected_bookmark.and_then(|i| self.bookmarks.get(i)) {
|
||||
Some(b) => b.clone(),
|
||||
None => return Task::none(),
|
||||
@@ -258,6 +325,27 @@ impl App {
|
||||
},
|
||||
)
|
||||
}
|
||||
Message::JoinChannel(channel_id) => {
|
||||
let h = self.handle.clone();
|
||||
Task::perform(
|
||||
async move {
|
||||
let mut guard = h.lock().await;
|
||||
if let Some(ref mut handle) = *guard {
|
||||
let _ = handle
|
||||
.with_connection(move |con| -> Result<(), String> {
|
||||
let state = con.get_state().map_err(|e| e.to_string())?;
|
||||
if let Some(client) = state.clients.get(&state.own_client) {
|
||||
let cmd = client.client_move(channel_id);
|
||||
cmd.send(con).map_err(|e| e.to_string())?;
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
.await;
|
||||
}
|
||||
},
|
||||
|_| Message::Noop,
|
||||
)
|
||||
}
|
||||
Message::Disconnect => {
|
||||
let h = self.handle.clone();
|
||||
let rx = self.event_rx.clone();
|
||||
@@ -357,17 +445,17 @@ impl App {
|
||||
async move {
|
||||
let mut guard = h.lock().await;
|
||||
if let Some(ref mut handle) = *guard {
|
||||
let result = handle
|
||||
.with_connection(move |con| -> Result<(), String> {
|
||||
let state = con.get_state().map_err(|e| e.to_string())?;
|
||||
let cmd = state.send_message(MessageTarget::Channel, &msg);
|
||||
cmd.send(con).map_err(|e| e.to_string())?;
|
||||
Ok(())
|
||||
})
|
||||
.await;
|
||||
if let Err(e) = result {
|
||||
return Some(e.to_string());
|
||||
}
|
||||
let result = handle
|
||||
.with_connection(move |con| -> Result<(), String> {
|
||||
let state = con.get_state().map_err(|e| e.to_string())?;
|
||||
let cmd = state.send_message(MessageTarget::Channel, &msg);
|
||||
cmd.send(con).map_err(|e| e.to_string())?;
|
||||
Ok(())
|
||||
})
|
||||
.await;
|
||||
if let Err(e) = result {
|
||||
return Some(e.to_string());
|
||||
}
|
||||
}
|
||||
None
|
||||
},
|
||||
@@ -388,6 +476,10 @@ impl App {
|
||||
self.query_port = port.parse().unwrap_or(10011);
|
||||
Task::none()
|
||||
}
|
||||
Message::QueryCommandChanged(cmd) => {
|
||||
self.query_command = cmd;
|
||||
Task::none()
|
||||
}
|
||||
Message::RunServerQuery => {
|
||||
if self.query_address.is_empty() {
|
||||
self.query_error = Some("Address required".to_string());
|
||||
@@ -397,6 +489,7 @@ impl App {
|
||||
self.query_error = None;
|
||||
self.query_response = None;
|
||||
let addr = format!("{}:{}", self.query_address, self.query_port);
|
||||
let cmd = self.query_command.clone();
|
||||
Task::perform(
|
||||
async move {
|
||||
use tscore::QueryClient;
|
||||
@@ -404,7 +497,7 @@ impl App {
|
||||
.await
|
||||
.map_err(|e| format!("Connect: {e}"))?;
|
||||
let resp = client
|
||||
.execute("help")
|
||||
.execute(&cmd)
|
||||
.await
|
||||
.map_err(|e| format!("Query: {e}"))?;
|
||||
Ok::<String, String>(resp.raw)
|
||||
@@ -431,16 +524,10 @@ impl App {
|
||||
|
||||
fn handle_book_event(&mut self, event: &Event) {
|
||||
match event {
|
||||
Event::PropertyAdded { id, invoker: _, .. } => match id {
|
||||
PropertyId::Server => {
|
||||
self.refresh_server_state_from_handle();
|
||||
}
|
||||
PropertyId::Client(_cid) => {
|
||||
self.refresh_clients_from_handle();
|
||||
}
|
||||
PropertyId::Channel(_chid) => {
|
||||
self.refresh_channels_from_handle();
|
||||
}
|
||||
Event::PropertyAdded { id, .. } => match id {
|
||||
PropertyId::Server => self.refresh_server_state_from_handle(),
|
||||
PropertyId::Client(_) => self.refresh_clients_from_handle(),
|
||||
PropertyId::Channel(_) => self.refresh_channels_from_handle(),
|
||||
_ => {}
|
||||
},
|
||||
Event::PropertyChanged { id, invoker, .. } => match id {
|
||||
@@ -451,19 +538,9 @@ impl App {
|
||||
}
|
||||
}
|
||||
}
|
||||
PropertyId::ClientChannel(cid) => {
|
||||
if let Some(c) = self.clients.iter_mut().find(|c| c.id == *cid) {
|
||||
if let Some(inv) = invoker {
|
||||
c.name = inv.name.clone();
|
||||
}
|
||||
}
|
||||
}
|
||||
PropertyId::ClientInputMuted(_cid) => {
|
||||
self.refresh_clients_from_handle();
|
||||
}
|
||||
PropertyId::ChannelName(_chid) => {
|
||||
self.refresh_channels_from_handle();
|
||||
}
|
||||
PropertyId::ClientChannel(_) => self.refresh_clients_from_handle(),
|
||||
PropertyId::ClientInputMuted(_) => self.refresh_clients_from_handle(),
|
||||
PropertyId::ChannelName(_) => self.refresh_channels_from_handle(),
|
||||
_ => {}
|
||||
},
|
||||
Event::PropertyRemoved { id, .. } => match id {
|
||||
@@ -503,35 +580,25 @@ 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;
|
||||
@@ -553,19 +620,13 @@ 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
|
||||
})
|
||||
@@ -586,18 +647,12 @@ 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
|
||||
})
|
||||
@@ -628,10 +683,7 @@ 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;
|
||||
}
|
||||
@@ -662,53 +714,111 @@ impl App {
|
||||
nav_button("Query", Page::ServerQuery, self.page),
|
||||
nav_button("Settings", Page::Settings, self.page),
|
||||
]
|
||||
.spacing(4)
|
||||
.padding(8);
|
||||
.spacing(2)
|
||||
.padding(12);
|
||||
|
||||
let content = match self.page {
|
||||
Page::ServerList => self.view_server_list(),
|
||||
Page::ServerQuery => container(text("ServerQuery panel")).padding(8).into(),
|
||||
Page::Settings => container(text("Settings panel")).padding(8).into(),
|
||||
Page::ServerQuery => container(text("").size(12)).padding(8).into(),
|
||||
Page::Settings => container(text("").size(12)).padding(8).into(),
|
||||
};
|
||||
|
||||
column![nav, horizontal_rule(1), content]
|
||||
.width(260)
|
||||
.height(Length::Fill)
|
||||
.into()
|
||||
container(
|
||||
column![nav, theme::separator_line(), content]
|
||||
.width(280)
|
||||
.height(Length::Fill),
|
||||
)
|
||||
.style(theme::sidebar_container)
|
||||
.width(280)
|
||||
.height(Length::Fill)
|
||||
.into()
|
||||
}
|
||||
|
||||
fn view_server_list(&self) -> Element<'_, Message> {
|
||||
let mut list = column![].spacing(2).padding(8);
|
||||
let mut list = column![].spacing(4).padding(12);
|
||||
|
||||
for (i, bookmark) in self.bookmarks.iter().enumerate() {
|
||||
let is_selected = self.selected_bookmark == Some(i);
|
||||
|
||||
let btn = button(
|
||||
column![
|
||||
text(&bookmark.name).size(14),
|
||||
text(format!("{}:{}", bookmark.address, bookmark.port)).size(11),
|
||||
row![
|
||||
column![
|
||||
text(&bookmark.name).size(13),
|
||||
text(format!("{}:{}", bookmark.address, bookmark.port))
|
||||
.size(11)
|
||||
.style(text::secondary),
|
||||
]
|
||||
.spacing(2)
|
||||
.width(Length::Fill),
|
||||
button(text("x").size(10))
|
||||
.padding(4)
|
||||
.style(theme::danger_button)
|
||||
.on_press(Message::DeleteBookmark(i)),
|
||||
]
|
||||
.spacing(2),
|
||||
.align_y(iced::Alignment::Center)
|
||||
.spacing(8),
|
||||
)
|
||||
.width(Length::Fill)
|
||||
.on_press(Message::SelectBookmark(Some(i)))
|
||||
.padding(8)
|
||||
.padding([10, 12])
|
||||
.style(if is_selected {
|
||||
button::primary
|
||||
theme::bookmark_button_selected
|
||||
} else {
|
||||
button::secondary
|
||||
theme::bookmark_button
|
||||
});
|
||||
|
||||
list = list.push(btn);
|
||||
}
|
||||
|
||||
if self.editing_bookmark {
|
||||
let form = container(
|
||||
column![
|
||||
text_input("Server name", &self.bm_name_input)
|
||||
.on_input(Message::BookmarkNameChanged)
|
||||
.style(theme::input_style),
|
||||
text_input("Address", &self.bm_address_input)
|
||||
.on_input(Message::BookmarkAddressChanged)
|
||||
.style(theme::input_style),
|
||||
text_input("Port", &self.bm_port_input)
|
||||
.on_input(Message::BookmarkPortChanged)
|
||||
.style(theme::input_style),
|
||||
row![
|
||||
button(text("Save").size(12))
|
||||
.padding([6, 16])
|
||||
.style(theme::primary_button)
|
||||
.on_press(Message::Connect),
|
||||
button(text("Cancel").size(12))
|
||||
.padding([6, 16])
|
||||
.style(theme::secondary_button)
|
||||
.on_press(Message::AddBookmark),
|
||||
]
|
||||
.spacing(8),
|
||||
]
|
||||
.spacing(8)
|
||||
.padding(12),
|
||||
)
|
||||
.style(theme::card_container)
|
||||
.padding(8);
|
||||
|
||||
list = list.push(form);
|
||||
}
|
||||
|
||||
column![
|
||||
text("Server Bookmarks").size(16),
|
||||
container(
|
||||
row![
|
||||
text("Servers").size(14),
|
||||
horizontal_space(),
|
||||
button(text("+").size(16))
|
||||
.padding([2, 8])
|
||||
.style(theme::secondary_button)
|
||||
.on_press(Message::AddBookmark),
|
||||
]
|
||||
.align_y(iced::Alignment::Center),
|
||||
)
|
||||
.padding([0, 12]),
|
||||
vertical_space().height(8),
|
||||
scrollable(list).height(Length::Fill),
|
||||
]
|
||||
.spacing(4)
|
||||
.padding(8)
|
||||
.into()
|
||||
}
|
||||
|
||||
@@ -724,135 +834,180 @@ impl App {
|
||||
if let Some(idx) = self.selected_bookmark {
|
||||
if let Some(bookmark) = self.bookmarks.get(idx) {
|
||||
let mut connect_form = column![
|
||||
text(format!("Connect to {}", bookmark.name)).size(20),
|
||||
vertical_space().height(12),
|
||||
row![
|
||||
text("Nickname:").width(80),
|
||||
text_input("Enter nickname", &self.nickname)
|
||||
.width(Length::Fill)
|
||||
.on_input(Message::NicknameChanged),
|
||||
]
|
||||
.spacing(8)
|
||||
.align_y(iced::Alignment::Center),
|
||||
text(bookmark.name.clone()).size(20),
|
||||
text(format!("{}:{}", bookmark.address, bookmark.port))
|
||||
.size(12)
|
||||
.style(text::secondary),
|
||||
vertical_space().height(16),
|
||||
text_input("Nickname", &self.nickname)
|
||||
.on_input(Message::NicknameChanged)
|
||||
.style(theme::input_style),
|
||||
vertical_space().height(8),
|
||||
row![
|
||||
text("Password:").width(80),
|
||||
text_input("Optional", &self.password)
|
||||
.width(Length::Fill)
|
||||
.secure(true)
|
||||
.on_input(Message::PasswordChanged),
|
||||
]
|
||||
.spacing(8)
|
||||
.align_y(iced::Alignment::Center),
|
||||
vertical_space().height(12),
|
||||
text_input("Password", &self.password)
|
||||
.secure(true)
|
||||
.on_input(Message::PasswordChanged)
|
||||
.style(theme::input_style),
|
||||
vertical_space().height(16),
|
||||
row![
|
||||
horizontal_space(),
|
||||
if self.connected {
|
||||
button(text("Disconnect").size(14))
|
||||
.padding([8, 24])
|
||||
button(text("Disconnect").size(13))
|
||||
.padding([10, 28])
|
||||
.style(theme::danger_button)
|
||||
.on_press(Message::Disconnect)
|
||||
} else {
|
||||
button(text("Connect").size(14))
|
||||
.padding([8, 24])
|
||||
button(text("Connect").size(13))
|
||||
.padding([10, 28])
|
||||
.style(theme::primary_button)
|
||||
.on_press(Message::Connect)
|
||||
},
|
||||
],
|
||||
]
|
||||
.spacing(4)
|
||||
.padding(16);
|
||||
.padding(20);
|
||||
|
||||
if let Some(err) = &self.error {
|
||||
connect_form = connect_form.push(text(format!("Error: {err}")).size(12));
|
||||
connect_form = connect_form.push(
|
||||
container(text(err.clone()).size(12).style(text::danger))
|
||||
.padding(10)
|
||||
.style(theme::elevated_container),
|
||||
);
|
||||
}
|
||||
if self.identity_level > 0 {
|
||||
connect_form = connect_form.push(
|
||||
text(format!(
|
||||
"Computing identity level {}... this may take a while",
|
||||
self.identity_level
|
||||
))
|
||||
.size(12),
|
||||
text(format!("Computing identity level {}...", self.identity_level))
|
||||
.size(12)
|
||||
.style(text::secondary),
|
||||
);
|
||||
}
|
||||
|
||||
if self.connected {
|
||||
let server_info = column![
|
||||
text(&self.server_name).size(18),
|
||||
text(format!("{} / {}", self.server_platform, self.server_version)).size(12),
|
||||
text(format!("max clients: {}", self.server_max_clients)).size(12),
|
||||
]
|
||||
.spacing(4);
|
||||
let server_header = container(
|
||||
column![
|
||||
text(&self.server_name).size(18),
|
||||
text(format!("{} / {} — {}/{} online",
|
||||
self.server_platform, self.server_version,
|
||||
self.clients.len(), self.server_max_clients))
|
||||
.size(12)
|
||||
.style(text::secondary),
|
||||
]
|
||||
.spacing(4),
|
||||
)
|
||||
.padding(16)
|
||||
.style(theme::card_container);
|
||||
|
||||
let mut channel_entries: Vec<(ChannelId, String)> = self
|
||||
.channels
|
||||
.iter()
|
||||
.map(|ch| (ch.id, ch.name.clone()))
|
||||
.collect();
|
||||
// Sort by order (use existing order from channels)
|
||||
let order_map: std::collections::HashMap<ChannelId, u64> =
|
||||
self.channels.iter().map(|ch| (ch.id, ch.order.0)).collect();
|
||||
channel_entries.sort_by_key(|(id, _)| order_map.get(id).copied().unwrap_or(0));
|
||||
|
||||
let mut channels = column![text("Channels").size(14)].spacing(2);
|
||||
for (ch_id, ch_name) in &channel_entries {
|
||||
let client_count = self.clients.iter().filter(|c| c.channel == *ch_id).count();
|
||||
channels = channels.push(
|
||||
let mut channel_list = column![].spacing(2);
|
||||
let mut sorted: Vec<&ChannelEntry> = self.channels.iter().collect();
|
||||
sorted.sort_by_key(|ch| ch.order.0);
|
||||
for ch in &sorted {
|
||||
let client_count = self.clients.iter().filter(|c| c.channel == ch.id).count();
|
||||
let is_current = self.own_client_id.is_some_and(|oid| {
|
||||
self.clients.iter().any(|c| c.id == oid && c.channel == ch.id)
|
||||
});
|
||||
let ch_row = button(
|
||||
row![
|
||||
text(ch_name.clone()).size(12).width(Length::Fill),
|
||||
text(format!("{}", client_count)).size(11),
|
||||
text(&ch.name).size(12).width(Length::Fill),
|
||||
text(format!("{}", client_count)).size(11).style(text::secondary),
|
||||
]
|
||||
.spacing(8),
|
||||
.spacing(8)
|
||||
.padding([4, 0]),
|
||||
)
|
||||
.width(Length::Fill)
|
||||
.padding([6, 10])
|
||||
.style(if is_current {
|
||||
theme::bookmark_button_selected
|
||||
} else {
|
||||
theme::bookmark_button
|
||||
})
|
||||
.on_press(Message::JoinChannel(ch.id));
|
||||
channel_list = channel_list.push(ch_row);
|
||||
}
|
||||
|
||||
let channels_panel = container(
|
||||
column![
|
||||
text("Channels").size(13),
|
||||
vertical_space().height(8),
|
||||
scrollable(channel_list).height(Length::Fill),
|
||||
]
|
||||
.spacing(4)
|
||||
.padding(12),
|
||||
)
|
||||
.style(theme::card_container)
|
||||
.height(Length::Fill);
|
||||
|
||||
let mut client_list = column![].spacing(2);
|
||||
for c in &self.clients {
|
||||
let mute = if c.input_muted { " 🔇" } else { "" };
|
||||
client_list = client_list.push(
|
||||
text(format!("{}{}", c.name, mute)).size(12),
|
||||
);
|
||||
}
|
||||
|
||||
let mut clients = column![text("Clients").size(14)].spacing(2);
|
||||
for c in &self.clients {
|
||||
let mute_indicator = if c.input_muted { " [M]" } else { "" };
|
||||
clients =
|
||||
clients.push(text(format!("{}{}", c.name, mute_indicator)).size(12));
|
||||
}
|
||||
let clients_panel = container(
|
||||
column![
|
||||
text("Clients").size(13),
|
||||
vertical_space().height(8),
|
||||
scrollable(client_list).height(Length::Fill),
|
||||
]
|
||||
.spacing(4)
|
||||
.padding(12),
|
||||
)
|
||||
.style(theme::card_container)
|
||||
.height(Length::Fill);
|
||||
|
||||
let mut messages_col = column![].spacing(4);
|
||||
let mut messages_col = column![].spacing(6);
|
||||
for msg in &self.messages {
|
||||
messages_col = messages_col.push(
|
||||
row![
|
||||
text(format!("{}:", msg.invoker_name)).size(12),
|
||||
text(&msg.message).size(12),
|
||||
column![
|
||||
text(&msg.invoker_name).size(11).style(text::secondary),
|
||||
text(&msg.message).size(13),
|
||||
]
|
||||
.spacing(4),
|
||||
.spacing(2),
|
||||
);
|
||||
}
|
||||
|
||||
let message_input = row![
|
||||
text_input("Type a message...", &self.message_input)
|
||||
.width(Length::Fill)
|
||||
.on_input(Message::MessageInputChanged)
|
||||
.on_submit(Message::SendChannelMessage),
|
||||
button(text("Send").size(12))
|
||||
.padding([8, 16])
|
||||
.on_press(Message::SendChannelMessage),
|
||||
]
|
||||
.spacing(8);
|
||||
let messages_panel = container(
|
||||
column![
|
||||
text("Chat").size(13),
|
||||
vertical_space().height(8),
|
||||
scrollable(messages_col).height(Length::Fill),
|
||||
vertical_space().height(8),
|
||||
row![
|
||||
text_input("Type a message...", &self.message_input)
|
||||
.width(Length::Fill)
|
||||
.on_input(Message::MessageInputChanged)
|
||||
.on_submit(Message::SendChannelMessage)
|
||||
.style(theme::input_style),
|
||||
button(text("Send").size(12))
|
||||
.padding([8, 16])
|
||||
.style(theme::primary_button)
|
||||
.on_press(Message::SendChannelMessage),
|
||||
]
|
||||
.spacing(8),
|
||||
]
|
||||
.spacing(4)
|
||||
.padding(12),
|
||||
)
|
||||
.style(theme::card_container)
|
||||
.height(Length::Fill);
|
||||
|
||||
column![
|
||||
connect_form,
|
||||
horizontal_rule(1),
|
||||
server_info,
|
||||
vertical_space().height(12),
|
||||
row![
|
||||
scrollable(channels).height(200),
|
||||
scrollable(clients).height(200),
|
||||
]
|
||||
.spacing(16),
|
||||
horizontal_rule(1),
|
||||
text("Messages").size(14),
|
||||
scrollable(messages_col).height(120),
|
||||
message_input,
|
||||
server_header,
|
||||
vertical_space().height(12),
|
||||
row![channels_panel, clients_panel].spacing(12).height(250),
|
||||
vertical_space().height(12),
|
||||
messages_panel,
|
||||
]
|
||||
.spacing(8)
|
||||
.padding(16)
|
||||
.spacing(0)
|
||||
.padding(20)
|
||||
.into()
|
||||
} else {
|
||||
connect_form.into()
|
||||
container(connect_form)
|
||||
.center_x(Length::Fill)
|
||||
.center_y(Length::Fill)
|
||||
.into()
|
||||
}
|
||||
} else {
|
||||
self.view_welcome()
|
||||
@@ -865,46 +1020,49 @@ impl App {
|
||||
fn view_query_content(&self) -> Element<'_, Message> {
|
||||
let mut content = column![
|
||||
text("ServerQuery").size(20),
|
||||
vertical_space().height(12),
|
||||
row![
|
||||
text("Address:").width(80),
|
||||
text_input("e.g. teamspeak.example.com", &self.query_address)
|
||||
.width(Length::Fill)
|
||||
.on_input(Message::QueryAddressChanged),
|
||||
]
|
||||
.spacing(8)
|
||||
.align_y(iced::Alignment::Center),
|
||||
vertical_space().height(16),
|
||||
text_input("Address", &self.query_address)
|
||||
.on_input(Message::QueryAddressChanged)
|
||||
.style(theme::input_style),
|
||||
vertical_space().height(8),
|
||||
row![
|
||||
text("Port:").width(80),
|
||||
text_input("10011", &self.query_port.to_string())
|
||||
.width(Length::Fill)
|
||||
.on_input(Message::QueryPortChanged),
|
||||
text_input("Port", &self.query_port.to_string())
|
||||
.on_input(Message::QueryPortChanged)
|
||||
.style(theme::input_style),
|
||||
text_input("Command", &self.query_command)
|
||||
.on_input(Message::QueryCommandChanged)
|
||||
.style(theme::input_style),
|
||||
]
|
||||
.spacing(8)
|
||||
.align_y(iced::Alignment::Center),
|
||||
.spacing(8),
|
||||
vertical_space().height(12),
|
||||
row![
|
||||
horizontal_space(),
|
||||
button(text(if self.query_loading { "Loading..." } else { "Query" }).size(14))
|
||||
.padding([8, 24])
|
||||
button(text(if self.query_loading { "Running..." } else { "Execute" }).size(13))
|
||||
.padding([10, 24])
|
||||
.style(theme::primary_button)
|
||||
.on_press_maybe(if self.query_loading { None } else { Some(Message::RunServerQuery) }),
|
||||
],
|
||||
]
|
||||
.spacing(4)
|
||||
.padding(16);
|
||||
.padding(20);
|
||||
|
||||
if let Some(resp) = &self.query_response {
|
||||
content = content.push(
|
||||
column![
|
||||
text("Response:").size(14),
|
||||
scrollable(text(resp).size(12)).height(300),
|
||||
]
|
||||
.spacing(4),
|
||||
container(
|
||||
column![
|
||||
text("Response").size(13),
|
||||
vertical_space().height(8),
|
||||
scrollable(text(resp).size(12)).height(Length::Fill),
|
||||
]
|
||||
.padding(12),
|
||||
)
|
||||
.style(theme::card_container),
|
||||
);
|
||||
}
|
||||
if let Some(err) = &self.query_error {
|
||||
content = content.push(text(format!("Error: {err}")).size(12));
|
||||
content = content.push(
|
||||
text(err.clone()).size(12).style(text::danger),
|
||||
);
|
||||
}
|
||||
|
||||
content.into()
|
||||
@@ -913,11 +1071,27 @@ impl App {
|
||||
fn view_settings_content(&self) -> Element<'_, Message> {
|
||||
container(
|
||||
column![
|
||||
text("Settings").size(24),
|
||||
vertical_space().height(12),
|
||||
text("Settings will be available in a future update.").size(14),
|
||||
text("Settings").size(20),
|
||||
vertical_space().height(16),
|
||||
container(
|
||||
column![
|
||||
text("Audio Output Device").size(13),
|
||||
text("System Default").size(12).style(text::secondary),
|
||||
vertical_space().height(12),
|
||||
text("Audio Input Device").size(13),
|
||||
text("System Default").size(12).style(text::secondary),
|
||||
vertical_space().height(12),
|
||||
text("Push-to-Talk Key").size(13),
|
||||
text("Not configured").size(12).style(text::secondary),
|
||||
]
|
||||
.spacing(4)
|
||||
.padding(16),
|
||||
)
|
||||
.style(theme::card_container)
|
||||
.width(400),
|
||||
]
|
||||
.align_x(iced::Alignment::Center),
|
||||
.spacing(8)
|
||||
.padding(20),
|
||||
)
|
||||
.center_x(Length::Fill)
|
||||
.center_y(Length::Fill)
|
||||
@@ -927,9 +1101,11 @@ impl App {
|
||||
fn view_welcome(&self) -> Element<'_, Message> {
|
||||
container(
|
||||
column![
|
||||
text("Welcome to ReTeamSpeak").size(24),
|
||||
vertical_space().height(12),
|
||||
text("Select a server bookmark from the sidebar to connect.").size(14),
|
||||
text("ReTeamSpeak").size(28),
|
||||
vertical_space().height(8),
|
||||
text("Select a server from the sidebar to get started.")
|
||||
.size(14)
|
||||
.style(text::secondary),
|
||||
]
|
||||
.align_x(iced::Alignment::Center),
|
||||
)
|
||||
@@ -950,9 +1126,7 @@ async fn run_connection(con: SyncConnection, event_tx: mpsc::Sender<TsEvent>, au
|
||||
tsclientlib::AudioEvent::CanSendAudio(can) => TsEvent::AudioChange(can, true),
|
||||
tsclientlib::AudioEvent::CanReceiveAudio(can) => TsEvent::AudioChange(false, can),
|
||||
},
|
||||
Ok(SyncStreamItem::IdentityLevelIncreasing(level)) => {
|
||||
TsEvent::IdentityLevelIncreasing(level)
|
||||
}
|
||||
Ok(SyncStreamItem::IdentityLevelIncreasing(level)) => TsEvent::IdentityLevelIncreasing(level),
|
||||
Ok(SyncStreamItem::IdentityLevelIncreased) => TsEvent::IdentityLevelIncreased,
|
||||
Ok(SyncStreamItem::DisconnectedTemporarily(_)) => TsEvent::DisconnectedTemporarily,
|
||||
Ok(SyncStreamItem::NetworkStatsUpdated) => continue,
|
||||
@@ -981,9 +1155,7 @@ async fn run_connection(con: SyncConnection, event_tx: mpsc::Sender<TsEvent>) {
|
||||
tsclientlib::AudioEvent::CanSendAudio(can) => TsEvent::AudioChange(can, true),
|
||||
tsclientlib::AudioEvent::CanReceiveAudio(can) => TsEvent::AudioChange(false, can),
|
||||
},
|
||||
Ok(SyncStreamItem::IdentityLevelIncreasing(level)) => {
|
||||
TsEvent::IdentityLevelIncreasing(level)
|
||||
}
|
||||
Ok(SyncStreamItem::IdentityLevelIncreasing(level)) => TsEvent::IdentityLevelIncreasing(level),
|
||||
Ok(SyncStreamItem::IdentityLevelIncreased) => TsEvent::IdentityLevelIncreased,
|
||||
Ok(SyncStreamItem::DisconnectedTemporarily(_)) => TsEvent::DisconnectedTemporarily,
|
||||
Ok(SyncStreamItem::NetworkStatsUpdated) => continue,
|
||||
@@ -999,12 +1171,12 @@ async fn run_connection(con: SyncConnection, event_tx: mpsc::Sender<TsEvent>) {
|
||||
|
||||
fn nav_button(label: &str, page: Page, current: Page) -> Element<'static, Message> {
|
||||
button(text(label.to_string()).size(12))
|
||||
.padding([6, 12])
|
||||
.padding([8, 16])
|
||||
.on_press(Message::PageChanged(page))
|
||||
.style(if current == page {
|
||||
button::primary
|
||||
theme::nav_button_active
|
||||
} else {
|
||||
button::secondary
|
||||
theme::nav_button_inactive
|
||||
})
|
||||
.into()
|
||||
}
|
||||
|
||||
@@ -0,0 +1,400 @@
|
||||
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
|
||||
|
||||
pub fn dark_theme() -> Theme {
|
||||
Theme::custom(
|
||||
"ReTeamSpeak".to_string(),
|
||||
iced::theme::Palette {
|
||||
background: BG_PRIMARY,
|
||||
text: TEXT_PRIMARY,
|
||||
primary: ACCENT,
|
||||
success: SUCCESS,
|
||||
danger: DANGER,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
pub fn sidebar_container(_theme: &Theme) -> container::Style {
|
||||
container::Style {
|
||||
background: Some(Background::Color(SIDEBAR_BG)),
|
||||
border: Border {
|
||||
color: SEPARATOR,
|
||||
width: 0.0,
|
||||
radius: 0.0.into(),
|
||||
},
|
||||
..container::Style::default()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn card_container(_theme: &Theme) -> container::Style {
|
||||
container::Style {
|
||||
background: Some(Background::Color(BG_SECONDARY)),
|
||||
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,
|
||||
},
|
||||
..container::Style::default()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn elevated_container(_theme: &Theme) -> container::Style {
|
||||
container::Style {
|
||||
background: Some(Background::Color(BG_TERTIARY)),
|
||||
border: Border {
|
||||
color: Color::TRANSPARENT,
|
||||
width: 0.0,
|
||||
radius: 10.0.into(),
|
||||
},
|
||||
..container::Style::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 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,
|
||||
},
|
||||
},
|
||||
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(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
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(),
|
||||
},
|
||||
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(),
|
||||
}
|
||||
}
|
||||
|
||||
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(),
|
||||
},
|
||||
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(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
}
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user