diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 911818ca..dfde5bbc 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -3,7 +3,7 @@ on: push: branches: [ master ] pull_request: - branches: [ master ] + branches: [ master, dev ] env: CARGO_TERM_COLOR: always @@ -241,4 +241,4 @@ jobs: ~/.cargo/git key: ${{ runner.os }}-audit-${{ hashFiles('**/Cargo.lock') }} - run: cargo install cargo-audit - - run: cargo audit \ No newline at end of file + - run: cargo audit diff --git a/.github/workflows/nix.yml b/.github/workflows/nix.yml index 41f105b7..c33d4ed4 100644 --- a/.github/workflows/nix.yml +++ b/.github/workflows/nix.yml @@ -3,7 +3,7 @@ on: pull_request: branches: [ master ] push: - branches: [ master ] + branches: [ master, dev ] jobs: build: diff --git a/.gitignore b/.gitignore index 35e8b11d..e7ea9203 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,5 @@ nmrs/*.tar.gz.* RELEASE_NOTES.md __pycache__/ *.pyc +vendor/ + diff --git a/Cargo.lock b/Cargo.lock index 4caa1c13..1b6c1a2f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,56 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "anstream" +version = "0.6.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" + +[[package]] +name = "anstyle-parse" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys 0.61.0", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys 0.61.0", +] + [[package]] name = "anyhow" version = "1.0.100" @@ -227,6 +277,52 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" +[[package]] +name = "clap" +version = "4.5.53" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9e340e012a1bf4935f5282ed1436d1489548e8f72308207ea5df0e23d2d03f8" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.5.53" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d76b5d13eaa18c901fd2f7fca939fefe3a0727a953561fefdf3b2922b8569d00" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.5.49" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a0b5487afeab2deb2ff4e03a807ad1a03ac532ff5a2cee5d86884440c7f7671" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1d728cc89cf3aee9ff92b05e62b19ee65a02b5702cff7d5a377e32c6ae29d8d" + +[[package]] +name = "colorchoice" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" + [[package]] name = "concurrent-queue" version = "2.5.0" @@ -740,6 +836,12 @@ dependencies = [ "hashbrown", ] +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + [[package]] name = "js-sys" version = "0.3.81" @@ -845,6 +947,7 @@ name = "nmrs-ui" version = "0.3.0" dependencies = [ "anyhow", + "clap", "dirs", "fs2", "glib", @@ -859,6 +962,12 @@ version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + [[package]] name = "option-ext" version = "0.2.0" @@ -1151,6 +1260,12 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + [[package]] name = "syn" version = "2.0.106" @@ -1354,6 +1469,12 @@ version = "1.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f63a545481291138910575129486daeaf8ac54aee4387fe7906919f7830c7d9d" +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + [[package]] name = "uuid" version = "1.18.1" diff --git a/nmrs-core/src/connection.rs b/nmrs-core/src/connection.rs index f8a223b4..2233f65a 100644 --- a/nmrs-core/src/connection.rs +++ b/nmrs-core/src/connection.rs @@ -123,32 +123,13 @@ pub(crate) async fn connect(conn: &Connection, ssid: &str, creds: WifiSecurity) if use_saved_connection { if let Some(active) = current_ssid(conn).await { - eprintln!("Disconnecting from {active}."); + eprintln!("Disconnecting from {active}"); if let Ok(conns) = nm.active_connections().await { for conn_path in conns { - eprintln!("Deactivating connection: {conn_path}"); let _ = nm.deactivate_connection(conn_path).await; } } - - for i in 0..retries::DISCONNECT_MAX_RETRIES { - let d = NMDeviceProxy::builder(conn) - .path(wifi_device.clone())? - .build() - .await?; - let state = DeviceState::from(d.state().await?); - eprintln!("Loop {i}: Device state = {state:?}"); - - if state == DeviceState::Disconnected || state == DeviceState::Unavailable { - eprintln!("Device disconnected"); - break; - } - - Delay::new(timeouts::disconnect_poll_interval()).await; - } - - Delay::new(timeouts::disconnect_final_delay()).await; - eprintln!("Disconnect complete"); + disconnect_wifi_device(conn, &wifi_device).await? } let conn_path = saved_conn_path.unwrap(); @@ -253,29 +234,10 @@ pub(crate) async fn connect(conn: &Connection, ssid: &str, creds: WifiSecurity) eprintln!("Disconnecting from {active}."); if let Ok(conns) = nm.active_connections().await { for conn_path in conns { - eprintln!("Deactivating connection: {conn_path}"); let _ = nm.deactivate_connection(conn_path).await; } } - - for i in 0..retries::DISCONNECT_MAX_RETRIES { - let d = NMDeviceProxy::builder(conn) - .path(wifi_device.clone())? - .build() - .await?; - let state = DeviceState::from(d.state().await?); - eprintln!("Loop {i}: Device state = {state:?}"); - - if state == DeviceState::Disconnected || state == DeviceState::Unavailable { - eprintln!("Device disconnected"); - break; - } - - Delay::new(timeouts::disconnect_poll_interval()).await; - } - - Delay::new(timeouts::disconnect_final_delay()).await; - eprintln!("Disconnect complete"); + disconnect_wifi_device(conn, &wifi_device).await?; } match nm @@ -580,3 +542,41 @@ pub(crate) async fn forget(conn: &Connection, ssid: &str) -> zbus::Result<()> { ))) } } + +pub(crate) async fn disconnect_wifi_device( + conn: &Connection, + dev_path: &OwnedObjectPath, +) -> Result<()> { + let dev = NMDeviceProxy::builder(conn) + .path(dev_path.clone())? + .build() + .await?; + + let raw: zbus::proxy::Proxy = zbus::proxy::Builder::new(conn) + .destination("org.freedesktop.NetworkManager")? + .path(dev_path.clone())? + .interface("org.freedesktop.NetworkManager.Device")? + .build() + .await?; + + let _ = raw.call_method("Disconnect", &()).await; + + for _ in 0..retries::DISCONNECT_MAX_RETRIES { + Delay::new(timeouts::disconnect_poll_interval()).await; + match dev.state().await { + Ok(s) if s == device_state::DISCONNECTED || s == device_state::UNAVAILABLE => { + break; + } + Ok(_) => continue, + Err(e) => return Err(e), + } + } + + Delay::new(timeouts::disconnect_final_delay()).await; + + match dev.state().await { + Ok(s) if s == device_state::DISCONNECTED || s == device_state::UNAVAILABLE => Ok(()), + Ok(s) => Err(zbus::Error::Failure(format!("device stuck in state {s}"))), + Err(e) => Err(e), + } +} diff --git a/nmrs-ui/Cargo.toml b/nmrs-ui/Cargo.toml index a71abc56..9d92fee0 100644 --- a/nmrs-ui/Cargo.toml +++ b/nmrs-ui/Cargo.toml @@ -10,4 +10,5 @@ gtk = { version = "0.10.1", package = "gtk4" } glib = "0.21.3" dirs = "6.0.0" fs2 = "0.4.3" -anyhow = "1.0.100" \ No newline at end of file +anyhow = "1.0.100" +clap = { version = "4.5.53", features = ["derive"] } diff --git a/nmrs-ui/build.rs b/nmrs-ui/build.rs new file mode 100644 index 00000000..f67398d3 --- /dev/null +++ b/nmrs-ui/build.rs @@ -0,0 +1,19 @@ +use std::process::Command; + +fn main() { + let output = Command::new("git") + .args(["rev-parse", "--short", "HEAD"]) + .output(); + + let hash = match output { + Ok(output) if output.status.success() => { + String::from_utf8_lossy(&output.stdout).trim().to_string() + } + _ => { + println!("cargo:warning=Unable to determine git hash, using 'unknown'"); + String::from("unknown") + } + }; + + println!("cargo:rustc-env=GIT_HASH={}", hash); +} diff --git a/nmrs-ui/src/lib.rs b/nmrs-ui/src/lib.rs index 28982e44..ed6c5d4c 100644 --- a/nmrs-ui/src/lib.rs +++ b/nmrs-ui/src/lib.rs @@ -1,8 +1,10 @@ pub mod file_lock; +pub mod objects; pub mod style; pub mod theme_config; pub mod ui; +use clap::{ArgAction, Parser}; use gtk::Application; use gtk::prelude::*; @@ -10,7 +12,27 @@ use crate::file_lock::acquire_app_lock; use crate::style::load_css; use crate::ui::build_ui; +#[derive(Parser, Debug)] +#[command(name = "nmrs")] +#[command(disable_version_flag = true)] +#[command(version)] +struct Args { + #[arg(short = 'V', long = "version", action = ArgAction::SetTrue)] + version: bool, +} + pub fn run() -> anyhow::Result<()> { + let args = Args::parse(); + + if let Args { version: true } = args { + println!( + "nmrs {}-beta ({})", + env!("CARGO_PKG_VERSION"), + env!("GIT_HASH") + ); + return Ok(()); + } + let app = Application::builder() .application_id("org.netrs.ui") .build(); diff --git a/nmrs-ui/src/objects/mod.rs b/nmrs-ui/src/objects/mod.rs new file mode 100644 index 00000000..9757ba5f --- /dev/null +++ b/nmrs-ui/src/objects/mod.rs @@ -0,0 +1 @@ +pub mod theme; diff --git a/nmrs-ui/src/objects/theme.rs b/nmrs-ui/src/objects/theme.rs new file mode 100644 index 00000000..a3be5740 --- /dev/null +++ b/nmrs-ui/src/objects/theme.rs @@ -0,0 +1,38 @@ +use gtk::glib; +use gtk::subclass::prelude::ObjectSubclassIsExt; + +mod imp { + use super::*; + use crate::objects::theme::glib::subclass::prelude::ObjectImpl; + use crate::objects::theme::glib::subclass::prelude::ObjectSubclass; + use std::cell::RefCell; + + #[derive(Default)] + pub struct Theme { + pub label: RefCell, + } + + #[glib::object_subclass] + impl ObjectSubclass for Theme { + const NAME: &'static str = "ThemeObject"; + type Type = super::Theme; + } + + impl ObjectImpl for Theme {} +} + +glib::wrapper! { + pub struct Theme(ObjectSubclass); +} + +impl Theme { + pub fn new(label: &str) -> Self { + let obj: Self = glib::Object::new(); + obj.imp().label.replace(label.to_string()); + obj + } + + pub fn label(&self) -> String { + self.imp().label.borrow().clone() + } +} diff --git a/nmrs-ui/src/style.css b/nmrs-ui/src/style.css index 1372a022..d06fe8ac 100644 --- a/nmrs-ui/src/style.css +++ b/nmrs-ui/src/style.css @@ -1,4 +1,4 @@ -/* Light theme colors */ +/* Themes */ window.light-theme { --bg-primary: #ffffff; --bg-secondary: #f5f5f5; @@ -13,8 +13,6 @@ window.light-theme { --warning-color: #d97706; --error-color: #dc2626; } - -/* Dark theme colors (default) */ window, window.dark-theme { --bg-primary: #121212; @@ -31,17 +29,20 @@ window.dark-theme { --error-color: #ef4444; } +/* Window */ window { background-color: var(--bg-primary); color: var(--text-primary); } +/* Header */ headerbar { background: var(--bg-secondary); color: var(--text-primary); border-bottom: 1px solid var(--border-color); } +/* Switch */ switch { background-color: var(--bg-tertiary); } @@ -49,11 +50,13 @@ switch:checked { background-color: var(--accent-color); } +/* Wi-Fi label */ .wifi-label { font-weight: 600; color: var(--text-primary); } +/* List */ list { background: var(--bg-primary); border: none; @@ -68,12 +71,14 @@ list > row:selected { color: var(--bg-primary); } +/* Entry fields */ .pw-entry { background-color: transparent; color: var(--text-primary); border-color: transparent; } +/* Network selection */ .network-selection { padding: 6px 10px; margin: 2px 0; @@ -105,17 +110,20 @@ list > row:selected { opacity: 0.9; } +/* Network quality labels */ label.network-good { color: var(--success-color); } label.network-okay { color: var(--warning-color); } label.network-poor { color: var(--error-color); } +/* Network page */ .network-page { background: var(--bg-primary); padding: 16px 20px; - border: none; color: var(--text-primary); + border: none; } +/* Back button */ .back-button { background: none; border: none; @@ -126,6 +134,7 @@ label.network-poor { color: var(--error-color); } } .back-button:hover { color: var(--text-primary); } +/* Network details */ .network-icon { color: var(--text-primary); } @@ -135,11 +144,9 @@ label.network-poor { color: var(--error-color); } color: var(--text-primary); margin-bottom: 4px; } - .network-arrow { color: var(--text-primary); } - .network-info { margin-top: 14px; padding-left: 6px; @@ -151,6 +158,7 @@ label.network-poor { color: var(--error-color); } font-weight: 500; } +/* Section headers */ .section-header { font-weight: 600; font-size: 13px; @@ -162,12 +170,7 @@ label.network-poor { color: var(--error-color); } letter-spacing: 0.5px; } -.advanced-section, -.basic-section { - padding-left: 0; - margin-left: 0; -} - +/* Info keys and values */ .basic-key, .info-label { font-weight: 600; @@ -176,7 +179,6 @@ label.network-poor { color: var(--error-color); } margin-bottom: 2px; text-decoration: underline; } - .basic-value, .info-value { font-size: 14px; @@ -185,6 +187,7 @@ label.network-poor { color: var(--error-color); } margin-bottom: 8px; } +/* Divider */ .divider { margin-top: 10px; margin-bottom: 10px; @@ -192,51 +195,93 @@ label.network-poor { color: var(--error-color); } border-bottom: 1px solid var(--border-color); } -.wifi-secure { - color: var(--text-primary); -} - -.wifi-open { - color: var(--text-primary); -} +/* Wi-Fi secure/open */ +.wifi-secure { color: var(--text-primary); } +.wifi-open { color: var(--text-primary); } +/* Loading spinner */ .loading-spinner { margin-top: 12px; margin-bottom: 12px; opacity: 0.6; } +/* Forget button */ .forget-button { - font-size: 0.85em; - opacity: 0.7; - padding: 2px 6px; - border-radius: 6px; -} -.forget-button:hover { - opacity: 1; + font-size: 0.85em; + opacity: 0.7; + padding: 2px 6px; + border-radius: 6px; } +.forget-button:hover { opacity: 1; } +/* Refresh button */ .refresh-btn { - background: none; - border: none; - color: var(--text-tertiary); - font-weight: 500; - font-size: 13px; - padding: 4px 0; + background: none; + border: none; + color: var(--text-tertiary); + font-weight: 500; + font-size: 13px; + padding: 4px 0; } - .refresh-btn:hover { color: var(--text-primary); } +/* Theme toggle */ .theme-toggle-btn { - background: none; - border: none; - color: var(--text-tertiary); - padding: 4px 8px; - opacity: 0.7; - transition: opacity 150ms ease, color 150ms ease; + background: none; + border: none; + color: var(--text-tertiary); + padding: 4px 8px; + opacity: 0.7; + transition: opacity 150ms ease, color 150ms ease; } - .theme-toggle-btn:hover { - opacity: 1; - color: var(--text-primary); + opacity: 1; + color: var(--text-primary); } + +/* Dropdown */ +.dropdown { + background: var(--bg-secondary); + border: none; + color: var(--text-primary); + padding: 6px 12px; + font-size: 13px; +} +.dropdown button { + background: var(--bg-secondary); + border: none; + color: var(--text-primary); + padding: 6px 12px; + font-size: 13px; +} +.dropdown button label { + color: var(--text-primary); +} +.dropdown button:hover { + background: var(--bg-tertiary); + border-radius: 0; +} + +/* Popover */ +popover.background, +popover contents { + background: var(--bg-secondary); + border-radius: 0; +} +popover row { + background: var(--bg-secondary); + color: var(--text-primary); + border-radius: 0; +} +popover row:hover { + background: var(--bg-tertiary); +} +popover row:selected { + background: var(--accent-color); + color: var(--bg-primary); +} +popover row label { + color: var(--text-primary); +} + diff --git a/nmrs-ui/src/theme_config.rs b/nmrs-ui/src/theme_config.rs index d1d6e1d0..3f73735c 100644 --- a/nmrs-ui/src/theme_config.rs +++ b/nmrs-ui/src/theme_config.rs @@ -10,15 +10,17 @@ fn get_config_path() -> Option { })? } -pub fn save_theme(is_light: bool) { +/// Save the selected theme *name* (e.g. "nord", "gruvbox", "dracula") +pub fn save_theme(name: &str) { if let Some(path) = get_config_path() { - let _ = fs::write(path, if is_light { "light" } else { "dark" }); + let _ = fs::write(path, name); } } -pub fn load_theme() -> bool { +/// Load the previously selected theme. +/// Returns Some("nord") or None if missing. +pub fn load_theme() -> Option { get_config_path() .and_then(|path| fs::read_to_string(path).ok()) - .map(|content| content.trim() == "light") - .unwrap_or(false) // Default to dark theme + .map(|s| s.trim().to_string()) } diff --git a/nmrs-ui/src/themes/catppuccin.css b/nmrs-ui/src/themes/catppuccin.css new file mode 100644 index 00000000..0a4962a3 --- /dev/null +++ b/nmrs-ui/src/themes/catppuccin.css @@ -0,0 +1,267 @@ +/* Light theme */ +window.light-theme { + --bg-primary: #f1f5f9; + --bg-secondary: #e2e8f0; + --bg-tertiary: #cbd5e1; + --text-primary: #0f172a; + --text-secondary: #1e293b; + --text-tertiary: #475569; + --border-color: #cbd5e1; + --border-color-hover: #94a3b8; + --accent-color: #6366f1; + --success-color: #10b981; + --warning-color: #f59e0b; + --error-color: #ef4444; +} + +/* Dark theme */ +window, +window.dark-theme { + --bg-primary: #0f172a; + --bg-secondary: #1e293b; + --bg-tertiary: #334155; + --text-primary: #e2e8f0; + --text-secondary: #cbd5e1; + --text-tertiary: #94a3b8; + --border-color: #334155; + --border-color-hover: #475569; + --accent-color: #6366f1; + --success-color: #10b981; + --warning-color: #f59e0b; + --error-color: #ef4444; +} + +window { + background-color: var(--bg-primary); + color: var(--text-primary); +} + +headerbar { + background: var(--bg-secondary); + color: var(--text-primary); + border-bottom: 1px solid var(--border-color); +} + +switch { + background-color: var(--bg-tertiary); +} +switch:checked { + background-color: var(--accent-color); +} + +.wifi-label { + font-weight: 600; + color: var(--text-primary); +} + +list { + background: var(--bg-primary); + border: none; +} +list > row { + background: transparent; + border: none; + padding: 0; +} +list > row:selected { + background: var(--accent-color); + color: var(--bg-primary); +} + +.pw-entry { + background-color: transparent; + color: var(--text-primary); + border-color: transparent; +} + +.network-selection { + padding: 6px 10px; + margin: 2px 0; + background: var(--bg-secondary); + border: 1px solid var(--border-color); +} +.network-selection:hover { + background: var(--bg-tertiary); + border-color: var(--border-color-hover); + transition: background 150ms ease, border-color 150ms ease; +} +.network-selection.connected { + background: color-mix(in srgb, var(--success-color) 15%, transparent); + border-color: color-mix(in srgb, var(--success-color) 30%, transparent); +} +.network-selection.connected:hover { + background: color-mix(in srgb, var(--success-color) 20%, transparent); + border-color: color-mix(in srgb, var(--success-color) 40%, transparent); +} +.network-selection label { + font-size: 14px; + color: var(--text-primary); +} +.connected-label { + font-size: 12px; + color: var(--success-color); + font-style: italic; + margin-left: 8px; + opacity: 0.9; +} + +label.network-good { color: var(--success-color); } +label.network-okay { color: var(--warning-color); } +label.network-poor { color: var(--error-color); } + +.network-page { + background: var(--bg-primary); + padding: 16px 20px; + color: var(--text-primary); + border: none; +} + +.back-button { + background: none; + border: none; + color: var(--text-tertiary); + font-weight: 500; + font-size: 13px; + padding: 4px 0; +} +.back-button:hover { color: var(--text-primary); } + +.network-icon { + color: var(--text-primary); +} +.network-title { + font-size: 18px; + font-weight: 600; + color: var(--text-primary); + margin-bottom: 4px; +} +.network-arrow { + color: var(--text-primary); +} +.network-info { + margin-top: 14px; + padding-left: 6px; +} +.info-row { padding: 2px 0; } +.info-value { + color: var(--text-primary); + font-size: 14px; + font-weight: 500; +} + +.section-header { + font-weight: 600; + font-size: 13px; + text-transform: uppercase; + border-bottom: 1px solid var(--border-color); + padding-bottom: 4px; + margin-bottom: 6px; + color: var(--text-secondary); + letter-spacing: 0.5px; +} + +.basic-key, +.info-label { + font-weight: 600; + font-size: 13px; + color: var(--text-tertiary); + margin-bottom: 2px; + text-decoration: underline; +} +.basic-value, +.info-value { + font-size: 14px; + color: var(--text-primary); + font-weight: 500; + margin-bottom: 8px; +} + +.divider { + margin-top: 10px; + margin-bottom: 10px; + opacity: 0.25; + border-bottom: 1px solid var(--border-color); +} + +.wifi-secure { color: var(--text-primary); } +.wifi-open { color: var(--text-primary); } + +.loading-spinner { + margin-top: 12px; + margin-bottom: 12px; + opacity: 0.6; +} + +.forget-button { + font-size: 0.85em; + opacity: 0.7; + padding: 2px 6px; + border-radius: 6px; +} +.forget-button:hover { opacity: 1; } + +.refresh-btn { + background: none; + border: none; + color: var(--text-tertiary); + font-weight: 500; + font-size: 13px; + padding: 4px 0; +} +.refresh-btn:hover { color: var(--text-primary); } + +.theme-toggle-btn { + background: none; + border: none; + color: var(--text-tertiary); + padding: 4px 8px; + opacity: 0.7; + transition: opacity 150ms ease, color 150ms ease; +} +.theme-toggle-btn:hover { + opacity: 1; + color: var(--text-primary); +} + +.dropdown { + background: var(--bg-secondary); + border: none; + color: var(--text-primary); + padding: 6px 12px; + font-size: 13px; +} +.dropdown button { + background: var(--bg-secondary); + border: none; + color: var(--text-primary); + padding: 6px 12px; + font-size: 13px; +} +.dropdown button label { + color: var(--text-primary); +} +.dropdown button:hover { + background: var(--bg-tertiary); + border-radius: 0; +} + +popover.background, +popover contents { + background: var(--bg-secondary); + border-radius: 0; +} +popover row { + background: var(--bg-secondary); + color: var(--text-primary); + border-radius: 0; +} +popover row:hover { + background: var(--bg-tertiary); +} +popover row:selected { + background: var(--accent-color); + color: var(--bg-primary); +} +popover row label { + color: var(--text-primary); +} diff --git a/nmrs-ui/src/themes/dracula.css b/nmrs-ui/src/themes/dracula.css new file mode 100644 index 00000000..0dd7999d --- /dev/null +++ b/nmrs-ui/src/themes/dracula.css @@ -0,0 +1,267 @@ +/* Light theme */ +window.light-theme { + --bg-primary: #f8f8f2; + --bg-secondary: #e9e9e4; + --bg-tertiary: #dcdcd7; + --text-primary: #282a36; + --text-secondary: #44475a; + --text-tertiary: #6d7080; + --border-color: #dcdcd7; + --border-color-hover: #c9c9c4; + --accent-color: #6272a4; + --success-color: #50fa7b; + --warning-color: #f1fa8c; + --error-color: #ff5555; +} + +/* Dark theme */ +window, +window.dark-theme { + --bg-primary: #282a36; + --bg-secondary: #3a3c4e; + --bg-tertiary: #44475a; + --text-primary: #f8f8f2; + --text-secondary: #e5e5e1; + --text-tertiary: #cfcfcb; + --border-color: #44475a; + --border-color-hover: #5a5d70; + --accent-color: #6272a4; + --success-color: #50fa7b; + --warning-color: #f1fa8c; + --error-color: #ff5555; +} + +window { + background-color: var(--bg-primary); + color: var(--text-primary); +} + +headerbar { + background: var(--bg-secondary); + color: var(--text-primary); + border-bottom: 1px solid var(--border-color); +} + +switch { + background-color: var(--bg-tertiary); +} +switch:checked { + background-color: var(--accent-color); +} + +.wifi-label { + font-weight: 600; + color: var(--text-primary); +} + +list { + background: var(--bg-primary); + border: none; +} +list > row { + background: transparent; + border: none; + padding: 0; +} +list > row:selected { + background: var(--accent-color); + color: var(--bg-primary); +} + +.pw-entry { + background-color: transparent; + color: var(--text-primary); + border-color: transparent; +} + +.network-selection { + padding: 6px 10px; + margin: 2px 0; + background: var(--bg-secondary); + border: 1px solid var(--border-color); +} +.network-selection:hover { + background: var(--bg-tertiary); + border-color: var(--border-color-hover); + transition: background 150ms ease, border-color 150ms ease; +} +.network-selection.connected { + background: color-mix(in srgb, var(--success-color) 15%, transparent); + border-color: color-mix(in srgb, var(--success-color) 30%, transparent); +} +.network-selection.connected:hover { + background: color-mix(in srgb, var(--success-color) 20%, transparent); + border-color: color-mix(in srgb, var(--success-color) 40%, transparent); +} +.network-selection label { + font-size: 14px; + color: var(--text-primary); +} +.connected-label { + font-size: 12px; + color: var(--success-color); + font-style: italic; + margin-left: 8px; + opacity: 0.9; +} + +label.network-good { color: var(--success-color); } +label.network-okay { color: var(--warning-color); } +label.network-poor { color: var(--error-color); } + +.network-page { + background: var(--bg-primary); + padding: 16px 20px; + color: var(--text-primary); + border: none; +} + +.back-button { + background: none; + border: none; + color: var(--text-tertiary); + font-weight: 500; + font-size: 13px; + padding: 4px 0; +} +.back-button:hover { color: var(--text-primary); } + +.network-icon { + color: var(--text-primary); +} +.network-title { + font-size: 18px; + font-weight: 600; + color: var(--text-primary); + margin-bottom: 4px; +} +.network-arrow { + color: var(--text-primary); +} +.network-info { + margin-top: 14px; + padding-left: 6px; +} +.info-row { padding: 2px 0; } +.info-value { + color: var(--text-primary); + font-size: 14px; + font-weight: 500; +} + +.section-header { + font-weight: 600; + font-size: 13px; + text-transform: uppercase; + border-bottom: 1px solid var(--border-color); + padding-bottom: 4px; + margin-bottom: 6px; + color: var(--text-secondary); + letter-spacing: 0.5px; +} + +.basic-key, +.info-label { + font-weight: 600; + font-size: 13px; + color: var(--text-tertiary); + margin-bottom: 2px; + text-decoration: underline; +} +.basic-value, +.info-value { + font-size: 14px; + color: var(--text-primary); + font-weight: 500; + margin-bottom: 8px; +} + +.divider { + margin-top: 10px; + margin-bottom: 10px; + opacity: 0.25; + border-bottom: 1px solid var(--border-color); +} + +.wifi-secure { color: var(--text-primary); } +.wifi-open { color: var(--text-primary); } + +.loading-spinner { + margin-top: 12px; + margin-bottom: 12px; + opacity: 0.6; +} + +.forget-button { + font-size: 0.85em; + opacity: 0.7; + padding: 2px 6px; + border-radius: 6px; +} +.forget-button:hover { opacity: 1; } + +.refresh-btn { + background: none; + border: none; + color: var(--text-tertiary); + font-weight: 500; + font-size: 13px; + padding: 4px 0; +} +.refresh-btn:hover { color: var(--text-primary); } + +.theme-toggle-btn { + background: none; + border: none; + color: var(--text-tertiary); + padding: 4px 8px; + opacity: 0.7; + transition: opacity 150ms ease, color 150ms ease; +} +.theme-toggle-btn:hover { + opacity: 1; + color: var(--text-primary); +} + +.dropdown { + background: var(--bg-secondary); + border: none; + color: var(--text-primary); + padding: 6px 12px; + font-size: 13px; +} +.dropdown button { + background: var(--bg-secondary); + border: none; + color: var(--text-primary); + padding: 6px 12px; + font-size: 13px; +} +.dropdown button label { + color: var(--text-primary); +} +.dropdown button:hover { + background: var(--bg-tertiary); + border-radius: 0; +} + +popover.background, +popover contents { + background: var(--bg-secondary); + border-radius: 0; +} +popover row { + background: var(--bg-secondary); + color: var(--text-primary); + border-radius: 0; +} +popover row:hover { + background: var(--bg-tertiary); +} +popover row:selected { + background: var(--accent-color); + color: var(--bg-primary); +} +popover row label { + color: var(--text-primary); +} diff --git a/nmrs-ui/src/themes/gruvbox.css b/nmrs-ui/src/themes/gruvbox.css new file mode 100644 index 00000000..2511e851 --- /dev/null +++ b/nmrs-ui/src/themes/gruvbox.css @@ -0,0 +1,267 @@ +/* Light theme */ +window.light-theme { + --bg-primary: #fbf1c7; + --bg-secondary: #f2e5bc; + --bg-tertiary: #ebdbb2; + --text-primary: #3c3836; + --text-secondary: #504945; + --text-tertiary: #7c6f64; + --border-color: #d5c4a1; + --border-color-hover: #bdae93; + --accent-color: #d79921; + --success-color: #98971a; + --warning-color: #d65d0e; + --error-color: #cc241d; +} + +/* Dark theme */ +window, +window.dark-theme { + --bg-primary: #282828; + --bg-secondary: #3c3836; + --bg-tertiary: #504945; + --text-primary: #ebdbb2; + --text-secondary: #d5c4a1; + --text-tertiary: #bdae93; + --border-color: #504945; + --border-color-hover: #665c54; + --accent-color: #d79921; + --success-color: #98971a; + --warning-color: #d65d0e; + --error-color: #cc241d; +} + +window { + background-color: var(--bg-primary); + color: var(--text-primary); +} + +headerbar { + background: var(--bg-secondary); + color: var(--text-primary); + border-bottom: 1px solid var(--border-color); +} + +switch { + background-color: var(--bg-tertiary); +} +switch:checked { + background-color: var(--accent-color); +} + +.wifi-label { + font-weight: 600; + color: var(--text-primary); +} + +list { + background: var(--bg-primary); + border: none; +} +list > row { + background: transparent; + border: none; + padding: 0; +} +list > row:selected { + background: var(--accent-color); + color: var(--bg-primary); +} + +.pw-entry { + background-color: transparent; + color: var(--text-primary); + border-color: transparent; +} + +.network-selection { + padding: 6px 10px; + margin: 2px 0; + background: var(--bg-secondary); + border: 1px solid var(--border-color); +} +.network-selection:hover { + background: var(--bg-tertiary); + border-color: var(--border-color-hover); + transition: background 150ms ease, border-color 150ms ease; +} +.network-selection.connected { + background: color-mix(in srgb, var(--success-color) 15%, transparent); + border-color: color-mix(in srgb, var(--success-color) 30%, transparent); +} +.network-selection.connected:hover { + background: color-mix(in srgb, var(--success-color) 20%, transparent); + border-color: color-mix(in srgb, var(--success-color) 40%, transparent); +} +.network-selection label { + font-size: 14px; + color: var(--text-primary); +} +.connected-label { + font-size: 12px; + color: var(--success-color); + font-style: italic; + margin-left: 8px; + opacity: 0.9; +} + +label.network-good { color: var(--success-color); } +label.network-okay { color: var(--warning-color); } +label.network-poor { color: var(--error-color); } + +.network-page { + background: var(--bg-primary); + padding: 16px 20px; + color: var(--text-primary); + border: none; +} + +.back-button { + background: none; + border: none; + color: var(--text-tertiary); + font-weight: 500; + font-size: 13px; + padding: 4px 0; +} +.back-button:hover { color: var(--text-primary); } + +.network-icon { + color: var(--text-primary); +} +.network-title { + font-size: 18px; + font-weight: 600; + color: var(--text-primary); + margin-bottom: 4px; +} +.network-arrow { + color: var(--text-primary); +} +.network-info { + margin-top: 14px; + padding-left: 6px; +} +.info-row { padding: 2px 0; } +.info-value { + color: var(--text-primary); + font-size: 14px; + font-weight: 500; +} + +.section-header { + font-weight: 600; + font-size: 13px; + text-transform: uppercase; + border-bottom: 1px solid var(--border-color); + padding-bottom: 4px; + margin-bottom: 6px; + color: var(--text-secondary); + letter-spacing: 0.5px; +} + +.basic-key, +.info-label { + font-weight: 600; + font-size: 13px; + color: var(--text-tertiary); + margin-bottom: 2px; + text-decoration: underline; +} +.basic-value, +.info-value { + font-size: 14px; + color: var(--text-primary); + font-weight: 500; + margin-bottom: 8px; +} + +.divider { + margin-top: 10px; + margin-bottom: 10px; + opacity: 0.25; + border-bottom: 1px solid var(--border-color); +} + +.wifi-secure { color: var(--text-primary); } +.wifi-open { color: var(--text-primary); } + +.loading-spinner { + margin-top: 12px; + margin-bottom: 12px; + opacity: 0.6; +} + +.forget-button { + font-size: 0.85em; + opacity: 0.7; + padding: 2px 6px; + border-radius: 6px; +} +.forget-button:hover { opacity: 1; } + +.refresh-btn { + background: none; + border: none; + color: var(--text-tertiary); + font-weight: 500; + font-size: 13px; + padding: 4px 0; +} +.refresh-btn:hover { color: var(--text-primary); } + +.theme-toggle-btn { + background: none; + border: none; + color: var(--text-tertiary); + padding: 4px 8px; + opacity: 0.7; + transition: opacity 150ms ease, color 150ms ease; +} +.theme-toggle-btn:hover { + opacity: 1; + color: var(--text-primary); +} + +.dropdown { + background: var(--bg-secondary); + border: none; + color: var(--text-primary); + padding: 6px 12px; + font-size: 13px; +} +.dropdown button { + background: var(--bg-secondary); + border: none; + color: var(--text-primary); + padding: 6px 12px; + font-size: 13px; +} +.dropdown button label { + color: var(--text-primary); +} +.dropdown button:hover { + background: var(--bg-tertiary); + border-radius: 0; +} + +popover.background, +popover contents { + background: var(--bg-secondary); + border-radius: 0; +} +popover row { + background: var(--bg-secondary); + color: var(--text-primary); + border-radius: 0; +} +popover row:hover { + background: var(--bg-tertiary); +} +popover row:selected { + background: var(--accent-color); + color: var(--bg-primary); +} +popover row label { + color: var(--text-primary); +} diff --git a/nmrs-ui/src/themes/nord.css b/nmrs-ui/src/themes/nord.css new file mode 100644 index 00000000..c5d81adb --- /dev/null +++ b/nmrs-ui/src/themes/nord.css @@ -0,0 +1,267 @@ +/* Light theme */ +window.light-theme { + --bg-primary: #eceff4; + --bg-secondary: #e5e9f0; + --bg-tertiary: #d8dee9; + --text-primary: #2e3440; + --text-secondary: #4c566a; + --text-tertiary: #6c7a96; + --border-color: #d8dee9; + --border-color-hover: #c2c8d3; + --accent-color: #5e81ac; + --success-color: #a3be8c; + --warning-color: #ebcb8b; + --error-color: #bf616a; +} + +/* Dark theme */ +window, +window.dark-theme { + --bg-primary: #2e3440; + --bg-secondary: #3b4252; + --bg-tertiary: #434c5e; + --text-primary: #eceff4; + --text-secondary: #e5e9f0; + --text-tertiary: #d8dee9; + --border-color: #434c5e; + --border-color-hover: #4c556a; + --accent-color: #81a1c1; + --success-color: #a3be8c; + --warning-color: #ebcb8b; + --error-color: #bf616a; +} + +window { + background-color: var(--bg-primary); + color: var(--text-primary); +} + +headerbar { + background: var(--bg-secondary); + color: var(--text-primary); + border-bottom: 1px solid var(--border-color); +} + +switch { + background-color: var(--bg-tertiary); +} +switch:checked { + background-color: var(--accent-color); +} + +.wifi-label { + font-weight: 600; + color: var(--text-primary); +} + +list { + background: var(--bg-primary); + border: none; +} +list > row { + background: transparent; + border: none; + padding: 0; +} +list > row:selected { + background: var(--accent-color); + color: var(--bg-primary); +} + +.pw-entry { + background-color: transparent; + color: var(--text-primary); + border-color: transparent; +} + +.network-selection { + padding: 6px 10px; + margin: 2px 0; + background: var(--bg-secondary); + border: 1px solid var(--border-color); +} +.network-selection:hover { + background: var(--bg-tertiary); + border-color: var(--border-color-hover); + transition: background 150ms ease, border-color 150ms ease; +} +.network-selection.connected { + background: color-mix(in srgb, var(--success-color) 15%, transparent); + border-color: color-mix(in srgb, var(--success-color) 30%, transparent); +} +.network-selection.connected:hover { + background: color-mix(in srgb, var(--success-color) 20%, transparent); + border-color: color-mix(in srgb, var(--success-color) 40%, transparent); +} +.network-selection label { + font-size: 14px; + color: var(--text-primary); +} +.connected-label { + font-size: 12px; + color: var(--success-color); + font-style: italic; + margin-left: 8px; + opacity: 0.9; +} + +label.network-good { color: var(--success-color); } +label.network-okay { color: var(--warning-color); } +label.network-poor { color: var(--error-color); } + +.network-page { + background: var(--bg-primary); + padding: 16px 20px; + color: var(--text-primary); + border: none; +} + +.back-button { + background: none; + border: none; + color: var(--text-tertiary); + font-weight: 500; + font-size: 13px; + padding: 4px 0; +} +.back-button:hover { color: var(--text-primary); } + +.network-icon { + color: var(--text-primary); +} +.network-title { + font-size: 18px; + font-weight: 600; + color: var(--text-primary); + margin-bottom: 4px; +} +.network-arrow { + color: var(--text-primary); +} +.network-info { + margin-top: 14px; + padding-left: 6px; +} +.info-row { padding: 2px 0; } +.info-value { + color: var(--text-primary); + font-size: 14px; + font-weight: 500; +} + +.section-header { + font-weight: 600; + font-size: 13px; + text-transform: uppercase; + border-bottom: 1px solid var(--border-color); + padding-bottom: 4px; + margin-bottom: 6px; + color: var(--text-secondary); + letter-spacing: 0.5px; +} + +.basic-key, +.info-label { + font-weight: 600; + font-size: 13px; + color: var(--text-tertiary); + margin-bottom: 2px; + text-decoration: underline; +} +.basic-value, +.info-value { + font-size: 14px; + color: var(--text-primary); + font-weight: 500; + margin-bottom: 8px; +} + +.divider { + margin-top: 10px; + margin-bottom: 10px; + opacity: 0.25; + border-bottom: 1px solid var(--border-color); +} + +.wifi-secure { color: var(--text-primary); } +.wifi-open { color: var(--text-primary); } + +.loading-spinner { + margin-top: 12px; + margin-bottom: 12px; + opacity: 0.6; +} + +.forget-button { + font-size: 0.85em; + opacity: 0.7; + padding: 2px 6px; + border-radius: 6px; +} +.forget-button:hover { opacity: 1; } + +.refresh-btn { + background: none; + border: none; + color: var(--text-tertiary); + font-weight: 500; + font-size: 13px; + padding: 4px 0; +} +.refresh-btn:hover { color: var(--text-primary); } + +.theme-toggle-btn { + background: none; + border: none; + color: var(--text-tertiary); + padding: 4px 8px; + opacity: 0.7; + transition: opacity 150ms ease, color 150ms ease; +} +.theme-toggle-btn:hover { + opacity: 1; + color: var(--text-primary); +} + +.dropdown { + background: var(--bg-secondary); + border: none; + color: var(--text-primary); + padding: 6px 12px; + font-size: 13px; +} +.dropdown button { + background: var(--bg-secondary); + border: none; + color: var(--text-primary); + padding: 6px 12px; + font-size: 13px; +} +.dropdown button label { + color: var(--text-primary); +} +.dropdown button:hover { + background: var(--bg-tertiary); + border-radius: 0; +} + +popover.background, +popover contents { + background: var(--bg-secondary); + border-radius: 0; +} +popover row { + background: var(--bg-secondary); + color: var(--text-primary); + border-radius: 0; +} +popover row:hover { + background: var(--bg-tertiary); +} +popover row:selected { + background: var(--accent-color); + color: var(--bg-primary); +} +popover row label { + color: var(--text-primary); +} diff --git a/nmrs-ui/src/themes/tokyo.css b/nmrs-ui/src/themes/tokyo.css new file mode 100644 index 00000000..93316b6e --- /dev/null +++ b/nmrs-ui/src/themes/tokyo.css @@ -0,0 +1,267 @@ +/* Light theme */ +window.light-theme { + --bg-primary: #dfe1e8; + --bg-secondary: #cfd1d9; + --bg-tertiary: #bfc2cc; + --text-primary: #1a1b26; + --text-secondary: #414868; + --text-tertiary: #565f89; + --border-color: #bfc2cc; + --border-color-hover: #a9acb8; + --accent-color: #7aa2f7; + --success-color: #9ece6a; + --warning-color: #e0af68; + --error-color: #f7768e; +} + +/* Dark theme */ +window, +window.dark-theme { + --bg-primary: #1a1b26; + --bg-secondary: #24283b; + --bg-tertiary: #2f3549; + --text-primary: #c0caf5; + --text-secondary: #a9b1d6; + --text-tertiary: #737aa2; + --border-color: #2f3549; + --border-color-hover: #414868; + --accent-color: #7aa2f7; + --success-color: #9ece6a; + --warning-color: #e0af68; + --error-color: #f7768e; +} + +window { + background-color: var(--bg-primary); + color: var(--text-primary); +} + +headerbar { + background: var(--bg-secondary); + color: var(--text-primary); + border-bottom: 1px solid var(--border-color); +} + +switch { + background-color: var(--bg-tertiary); +} +switch:checked { + background-color: var(--accent-color); +} + +.wifi-label { + font-weight: 600; + color: var(--text-primary); +} + +list { + background: var(--bg-primary); + border: none; +} +list > row { + background: transparent; + border: none; + padding: 0; +} +list > row:selected { + background: var(--accent-color); + color: var(--bg-primary); +} + +.pw-entry { + background-color: transparent; + color: var(--text-primary); + border-color: transparent; +} + +.network-selection { + padding: 6px 10px; + margin: 2px 0; + background: var(--bg-secondary); + border: 1px solid var(--border-color); +} +.network-selection:hover { + background: var(--bg-tertiary); + border-color: var(--border-color-hover); + transition: background 150ms ease, border-color 150ms ease; +} +.network-selection.connected { + background: color-mix(in srgb, var(--success-color) 15%, transparent); + border-color: color-mix(in srgb, var(--success-color) 30%, transparent); +} +.network-selection.connected:hover { + background: color-mix(in srgb, var(--success-color) 20%, transparent); + border-color: color-mix(in srgb, var(--success-color) 40%, transparent); +} +.network-selection label { + font-size: 14px; + color: var(--text-primary); +} +.connected-label { + font-size: 12px; + color: var(--success-color); + font-style: italic; + margin-left: 8px; + opacity: 0.9; +} + +label.network-good { color: var(--success-color); } +label.network-okay { color: var(--warning-color); } +label.network-poor { color: var(--error-color); } + +.network-page { + background: var(--bg-primary); + padding: 16px 20px; + color: var(--text-primary); + border: none; +} + +.back-button { + background: none; + border: none; + color: var(--text-tertiary); + font-weight: 500; + font-size: 13px; + padding: 4px 0; +} +.back-button:hover { color: var(--text-primary); } + +.network-icon { + color: var(--text-primary); +} +.network-title { + font-size: 18px; + font-weight: 600; + color: var(--text-primary); + margin-bottom: 4px; +} +.network-arrow { + color: var(--text-primary); +} +.network-info { + margin-top: 14px; + padding-left: 6px; +} +.info-row { padding: 2px 0; } +.info-value { + color: var(--text-primary); + font-size: 14px; + font-weight: 500; +} + +.section-header { + font-weight: 600; + font-size: 13px; + text-transform: uppercase; + border-bottom: 1px solid var(--border-color); + padding-bottom: 4px; + margin-bottom: 6px; + color: var(--text-secondary); + letter-spacing: 0.5px; +} + +.basic-key, +.info-label { + font-weight: 600; + font-size: 13px; + color: var(--text-tertiary); + margin-bottom: 2px; + text-decoration: underline; +} +.basic-value, +.info-value { + font-size: 14px; + color: var(--text-primary); + font-weight: 500; + margin-bottom: 8px; +} + +.divider { + margin-top: 10px; + margin-bottom: 10px; + opacity: 0.25; + border-bottom: 1px solid var(--border-color); +} + +.wifi-secure { color: var(--text-primary); } +.wifi-open { color: var(--text-primary); } + +.loading-spinner { + margin-top: 12px; + margin-bottom: 12px; + opacity: 0.6; +} + +.forget-button { + font-size: 0.85em; + opacity: 0.7; + padding: 2px 6px; + border-radius: 6px; +} +.forget-button:hover { opacity: 1; } + +.refresh-btn { + background: none; + border: none; + color: var(--text-tertiary); + font-weight: 500; + font-size: 13px; + padding: 4px 0; +} +.refresh-btn:hover { color: var(--text-primary); } + +.theme-toggle-btn { + background: none; + border: none; + color: var(--text-tertiary); + padding: 4px 8px; + opacity: 0.7; + transition: opacity 150ms ease, color 150ms ease; +} +.theme-toggle-btn:hover { + opacity: 1; + color: var(--text-primary); +} + +.dropdown { + background: var(--bg-secondary); + border: none; + color: var(--text-primary); + padding: 6px 12px; + font-size: 13px; +} +.dropdown button { + background: var(--bg-secondary); + border: none; + color: var(--text-primary); + padding: 6px 12px; + font-size: 13px; +} +.dropdown button label { + color: var(--text-primary); +} +.dropdown button:hover { + background: var(--bg-tertiary); + border-radius: 0; +} + +popover.background, +popover contents { + background: var(--bg-secondary); + border-radius: 0; +} +popover row { + background: var(--bg-secondary); + color: var(--text-primary); + border-radius: 0; +} +popover row:hover { + background: var(--bg-tertiary); +} +popover row:selected { + background: var(--accent-color); + color: var(--bg-primary); +} +popover row label { + color: var(--text-primary); +} diff --git a/nmrs-ui/src/ui/header.rs b/nmrs-ui/src/ui/header.rs index f2c98680..2f7e3d33 100644 --- a/nmrs-ui/src/ui/header.rs +++ b/nmrs-ui/src/ui/header.rs @@ -1,6 +1,7 @@ use glib::clone; +use gtk::STYLE_PROVIDER_PRIORITY_USER; use gtk::prelude::*; -use gtk::{Box as GtkBox, HeaderBar, Label, ListBox, Orientation, Switch}; +use gtk::{Box as GtkBox, HeaderBar, Label, ListBox, Orientation, Switch, glib}; use std::cell::Cell; use std::collections::HashSet; use std::rc::Rc; @@ -8,6 +9,40 @@ use std::rc::Rc; use crate::ui::networks; use crate::ui::networks::NetworksContext; +pub struct ThemeDef { + pub key: &'static str, + pub name: &'static str, + pub css: &'static str, +} + +pub static THEMES: &[ThemeDef] = &[ + ThemeDef { + key: "gruvbox", + name: "Gruvbox", + css: include_str!("../themes/gruvbox.css"), + }, + ThemeDef { + key: "nord", + name: "Nord", + css: include_str!("../themes/nord.css"), + }, + ThemeDef { + key: "dracula", + name: "Dracula", + css: include_str!("../themes/dracula.css"), + }, + ThemeDef { + key: "catppuccin", + name: "Catppuccin", + css: include_str!("../themes/catppuccin.css"), + }, + ThemeDef { + key: "tokyo", + name: "Tokyo Night", + css: include_str!("../themes/tokyo.css"), + }, +]; + pub fn build_header( ctx: Rc, list_container: &GtkBox, @@ -24,7 +59,46 @@ pub fn build_header( wifi_label.set_halign(gtk::Align::Start); wifi_label.add_css_class("wifi-label"); + let names: Vec<&str> = THEMES.iter().map(|t| t.name).collect(); + let dropdown = gtk::DropDown::from_strings(&names); + + if let Some(saved) = crate::theme_config::load_theme() + && let Some(idx) = THEMES.iter().position(|t| t.key == saved.as_str()) + { + dropdown.set_selected(idx as u32); + } + + dropdown.set_valign(gtk::Align::Center); + dropdown.add_css_class("dropdown"); + + let window_weak = window.downgrade(); + + dropdown.connect_selected_notify(move |dd| { + let idx = dd.selected() as usize; + if idx >= THEMES.len() { + return; + } + + let theme = &THEMES[idx]; + + if let Some(window) = window_weak.upgrade() { + let provider = gtk::CssProvider::new(); + provider.load_from_data(theme.css); + + let display = gtk::prelude::RootExt::display(&window); + + gtk::style_context_add_provider_for_display( + &display, + &provider, + STYLE_PROVIDER_PRIORITY_USER, + ); + + crate::theme_config::save_theme(theme.key); + } + }); + wifi_box.append(&wifi_label); + wifi_box.append(&dropdown); header.pack_start(&wifi_box); let refresh_btn = gtk::Button::from_icon_name("view-refresh-symbolic"); @@ -71,12 +145,12 @@ pub fn build_header( window.remove_css_class("light-theme"); window.add_css_class("dark-theme"); btn.set_icon_name("weather-clear-symbolic"); - crate::theme_config::save_theme(false); + crate::theme_config::save_theme("light"); } else { window.remove_css_class("dark-theme"); window.add_css_class("light-theme"); btn.set_icon_name("weather-clear-night-symbolic"); - crate::theme_config::save_theme(true); + crate::theme_config::save_theme("dark"); } } }); diff --git a/nmrs-ui/src/ui/mod.rs b/nmrs-ui/src/ui/mod.rs index c6219761..6906bab0 100644 --- a/nmrs-ui/src/ui/mod.rs +++ b/nmrs-ui/src/ui/mod.rs @@ -5,12 +5,14 @@ pub mod networks; use gtk::prelude::*; use gtk::{ - Application, ApplicationWindow, Box as GtkBox, Label, Orientation, ScrolledWindow, Spinner, - Stack, + Application, ApplicationWindow, Box as GtkBox, Label, Orientation, + STYLE_PROVIDER_PRIORITY_USER, ScrolledWindow, Spinner, Stack, }; use std::cell::Cell; use std::rc::Rc; +use crate::ui::header::THEMES; + type Callback = Rc; type CallbackCell = Rc>>; @@ -19,10 +21,19 @@ pub fn build_ui(app: &Application) { win.set_title(Some("")); win.set_default_size(400, 600); - let is_light = crate::theme_config::load_theme(); - if is_light { - win.add_css_class("light-theme"); - } else { + if let Some(key) = crate::theme_config::load_theme() + && let Some(theme) = THEMES.iter().find(|t| t.key == key.as_str()) + { + let provider = gtk::CssProvider::new(); + provider.load_from_data(theme.css); + + let display = gtk::prelude::RootExt::display(&win); + gtk::style_context_add_provider_for_display( + &display, + &provider, + STYLE_PROVIDER_PRIORITY_USER, + ); + win.add_css_class("dark-theme"); } diff --git a/package.nix b/package.nix index 41b7c05f..2d11e41b 100644 --- a/package.nix +++ b/package.nix @@ -19,7 +19,7 @@ rustPlatform.buildRustPackage { src = ./.; - cargoHash = "sha256-VXGB/asGXxs371wyiTakQlsCTxe3LQfkSdfFBdFc0Qc="; + cargoHash = "sha256-bd0Y/8NF0BmCqkpvyMsdUqeToobfKk3lHdTDICp+3LI="; nativeBuildInputs = [ pkg-config