refactor: fix hardcoded values, remove println, remove doubles
This commit is contained in:
+9
-7
@@ -1,10 +1,12 @@
|
|||||||
# Ревью кода bropicker (2026-08-26)
|
# Ревью кода bropicker (2026-08-26) — ВЫПОЛНЕНО
|
||||||
|
|
||||||
> Скоуп: src/main.rs (401 строка), src/config.rs (~210), src/winit.rs (45),
|
> Скоуп: src/main.rs (401 строка), src/config.rs (~210), src/winit.rs (45),
|
||||||
> ui/*.slint. Плюс вывод clippy.
|
> ui/*.slint. Плюс вывод clippy.
|
||||||
>
|
>
|
||||||
> Вердикт: для MVP код здоровый, критических дыр нет. Но есть 2 мусорных
|
> Итог рефакторинга: main.rs 401→340 строк (и стал тонким по смыслу),
|
||||||
> артефакта правок, крупное дублирование и файл-бог. Ниже по приоритетам.
|
> появился launcher.rs, 12 юнит-тестов, tools/check.ps1 (fmt+clippy -D warnings
|
||||||
|
> +test) зелёный, дублирование запуска устранено, индекс из UI вместо поиска
|
||||||
|
> по имени, все P0-P2 закрыты. UI проверен скриншотом — идентичен.
|
||||||
|
|
||||||
## P0 — мусор и артефакты правок (чинить сразу)
|
## P0 — мусор и артефакты правок (чинить сразу)
|
||||||
|
|
||||||
@@ -74,9 +76,9 @@
|
|||||||
|
|
||||||
## Порядок исполнения (одна сессия)
|
## Порядок исполнения (одна сессия)
|
||||||
|
|
||||||
1. P0 целиком + прогон clippy --fix (P2 первая строка)
|
1. [x] P0 целиком + прогон clippy --fix (P2 первая строка)
|
||||||
2. P1: launcher.rs + launch_and_remember + From-конвертации + индекс из UI
|
2. [x] P1: launcher.rs + launch_and_remember + From-конвертации + индекс из UI
|
||||||
3. Юнит-тесты чистых функций + tools/check.ps1
|
3. [x] Юнит-тесты чистых функций + tools/check.ps1
|
||||||
4. P3 косметика по остаточному принципу
|
4. [x] P3 косметика: импорты, Theme.accent/danger токены
|
||||||
|
|
||||||
Каждый пункт — зелёный `tools\check.ps1` перед коммитом.
|
Каждый пункт — зелёный `tools\check.ps1` перед коммитом.
|
||||||
|
|||||||
+257
-201
@@ -1,202 +1,258 @@
|
|||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
pub fn log(msg: &str) {
|
pub fn log(msg: &str) {
|
||||||
use std::io::Write;
|
use std::io::Write;
|
||||||
let Ok(appdata) = std::env::var("APPDATA") else {
|
let Ok(appdata) = std::env::var("APPDATA") else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
let dir = Path::new(&appdata).join("bropicker");
|
let dir = Path::new(&appdata).join("bropicker");
|
||||||
if std::fs::create_dir_all(&dir).is_err() {
|
if std::fs::create_dir_all(&dir).is_err() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let path = dir.join("bropicker.log");
|
let path = dir.join("bropicker.log");
|
||||||
let ts = std::time::SystemTime::now()
|
let ts = std::time::SystemTime::now()
|
||||||
.duration_since(std::time::UNIX_EPOCH)
|
.duration_since(std::time::UNIX_EPOCH)
|
||||||
.map(|d| d.as_secs())
|
.map(|d| d.as_secs())
|
||||||
.unwrap_or(0);
|
.unwrap_or(0);
|
||||||
if let Ok(mut f) = std::fs::OpenOptions::new().create(true).append(true).open(&path) {
|
if let Ok(mut f) = std::fs::OpenOptions::new()
|
||||||
let _ = writeln!(f, "[{ts}] {msg}");
|
.create(true)
|
||||||
}
|
.append(true)
|
||||||
}
|
.open(&path)
|
||||||
|
{
|
||||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
let _ = writeln!(f, "[{ts}] [bp] {msg}");
|
||||||
pub struct BrowserEntry {
|
}
|
||||||
pub name: String,
|
}
|
||||||
pub path: String,
|
|
||||||
#[serde(default)]
|
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||||
pub flags: String,
|
pub struct BrowserEntry {
|
||||||
#[serde(default)]
|
pub name: String,
|
||||||
pub icon: String,
|
pub path: String,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub emoji: String,
|
pub flags: String,
|
||||||
}
|
#[serde(default)]
|
||||||
|
pub icon: String,
|
||||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
#[serde(default)]
|
||||||
pub struct Settings {
|
pub emoji: String,
|
||||||
#[serde(default = "default_true")]
|
}
|
||||||
pub remember_choice: bool,
|
|
||||||
#[serde(default = "default_true")]
|
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||||
pub always_ask: bool,
|
pub struct Settings {
|
||||||
}
|
#[serde(default = "default_true")]
|
||||||
|
pub remember_choice: bool,
|
||||||
fn default_true() -> bool {
|
#[serde(default = "default_true")]
|
||||||
true
|
pub always_ask: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for Settings {
|
fn default_true() -> bool {
|
||||||
fn default() -> Self {
|
true
|
||||||
Self {
|
}
|
||||||
remember_choice: true,
|
|
||||||
always_ask: true,
|
impl Default for Settings {
|
||||||
}
|
fn default() -> Self {
|
||||||
}
|
Self {
|
||||||
}
|
remember_choice: true,
|
||||||
|
always_ask: true,
|
||||||
#[derive(Serialize, Deserialize, Clone, Debug, Default)]
|
}
|
||||||
pub struct Config {
|
}
|
||||||
#[serde(default)]
|
}
|
||||||
pub browsers: Vec<BrowserEntry>,
|
|
||||||
#[serde(default)]
|
#[derive(Serialize, Deserialize, Clone, Debug, Default)]
|
||||||
pub remembered: HashMap<String, String>,
|
pub struct Config {
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub settings: Settings,
|
pub browsers: Vec<BrowserEntry>,
|
||||||
}
|
#[serde(default)]
|
||||||
|
pub remembered: HashMap<String, String>,
|
||||||
pub fn config_path() -> PathBuf {
|
#[serde(default)]
|
||||||
if let Ok(p) = std::env::var("BP_CONFIG") {
|
pub settings: Settings,
|
||||||
return PathBuf::from(p);
|
}
|
||||||
}
|
|
||||||
std::env::var("APPDATA")
|
pub fn config_path() -> PathBuf {
|
||||||
.map(|d| PathBuf::from(d).join("bropicker").join("config.toml"))
|
if let Ok(p) = std::env::var("BP_CONFIG") {
|
||||||
.unwrap_or_else(|_| PathBuf::from("config.toml"))
|
return PathBuf::from(p);
|
||||||
}
|
}
|
||||||
|
std::env::var("APPDATA")
|
||||||
pub fn load() -> Config {
|
.map(|d| PathBuf::from(d).join("bropicker").join("config.toml"))
|
||||||
let path = config_path();
|
.unwrap_or_else(|_| PathBuf::from("config.toml"))
|
||||||
match std::fs::read_to_string(&path) {
|
}
|
||||||
Ok(s) => toml::from_str(&s).unwrap_or_else(|e| {
|
|
||||||
crate::config::log(&format!("[bp] config parse error ({path:?}): {e}"));
|
pub fn load() -> Config {
|
||||||
Config::default()
|
let path = config_path();
|
||||||
}),
|
match std::fs::read_to_string(&path) {
|
||||||
Err(_) => Config::default(),
|
Ok(s) => toml::from_str(&s).unwrap_or_else(|e| {
|
||||||
}
|
crate::config::log(&format!("[bp] config parse error ({path:?}): {e}"));
|
||||||
}
|
Config::default()
|
||||||
|
}),
|
||||||
pub fn save(cfg: &Config) {
|
Err(_) => Config::default(),
|
||||||
let path = config_path();
|
}
|
||||||
if let Some(dir) = path.parent() {
|
}
|
||||||
let _ = std::fs::create_dir_all(dir);
|
|
||||||
}
|
pub fn save(cfg: &Config) {
|
||||||
match toml::to_string_pretty(cfg) {
|
let path = config_path();
|
||||||
Ok(s) => {
|
if let Some(dir) = path.parent() {
|
||||||
if let Err(e) = std::fs::write(&path, s) {
|
let _ = std::fs::create_dir_all(dir);
|
||||||
log(&format!("[bp] config save failed ({path:?}): {e}"));
|
}
|
||||||
}
|
match toml::to_string_pretty(cfg) {
|
||||||
}
|
Ok(s) => {
|
||||||
Err(e) => log(&format!("[bp] config serialize failed: {e}")),
|
if let Err(e) = std::fs::write(&path, s) {
|
||||||
}
|
log(&format!("[bp] config save failed ({path:?}): {e}"));
|
||||||
}
|
}
|
||||||
|
}
|
||||||
pub fn ensure_browsers(cfg: &mut Config) {
|
Err(e) => log(&format!("[bp] config serialize failed: {e}")),
|
||||||
if cfg.browsers.is_empty() {
|
}
|
||||||
cfg.browsers = detect_browsers();
|
}
|
||||||
log(&format!("[bp] autodetected {} browsers", cfg.browsers.len()));
|
|
||||||
if !cfg.browsers.is_empty() {
|
pub fn ensure_browsers(cfg: &mut Config) {
|
||||||
save(cfg);
|
if cfg.browsers.is_empty() {
|
||||||
}
|
cfg.browsers = detect_browsers();
|
||||||
}
|
log(&format!("autodetected {} browsers", cfg.browsers.len()));
|
||||||
}
|
if !cfg.browsers.is_empty() {
|
||||||
|
save(cfg);
|
||||||
pub fn domain_of(url: &str) -> String {
|
}
|
||||||
let rest = url.split("://").nth(1).unwrap_or(url);
|
}
|
||||||
let host = rest.split(['/', '?', '#']).next().unwrap_or("");
|
}
|
||||||
let host = host.split('@').next().unwrap_or(host);
|
|
||||||
let host = host.split(':').next().unwrap_or(host);
|
pub fn domain_of(url: &str) -> String {
|
||||||
host.trim_start_matches("www.").to_lowercase()
|
let rest = url.split("://").nth(1).unwrap_or(url);
|
||||||
}
|
let host = rest.split(['/', '?', '#']).next().unwrap_or("");
|
||||||
|
let host = host.split('@').next().unwrap_or(host);
|
||||||
fn extract_exe(cmd: &str) -> Option<String> {
|
let host = host.split(':').next().unwrap_or(host);
|
||||||
let t = cmd.trim();
|
host.trim_start_matches("www.").to_lowercase()
|
||||||
if t.starts_with('"') {
|
}
|
||||||
t.split('"').nth(1).map(String::from)
|
|
||||||
} else {
|
fn extract_exe(cmd: &str) -> Option<String> {
|
||||||
t.split_whitespace().next().map(String::from)
|
let t = cmd.trim();
|
||||||
}
|
if t.starts_with('"') {
|
||||||
}
|
t.split('"').nth(1).map(String::from)
|
||||||
|
} else {
|
||||||
pub fn detect_browsers() -> Vec<BrowserEntry> {
|
t.split_whitespace().next().map(String::from)
|
||||||
use winreg::enums::{HKEY_CURRENT_USER, HKEY_LOCAL_MACHINE};
|
}
|
||||||
use winreg::RegKey;
|
}
|
||||||
|
|
||||||
let mut out: Vec<BrowserEntry> = Vec::new();
|
pub fn detect_browsers() -> Vec<BrowserEntry> {
|
||||||
|
use winreg::RegKey;
|
||||||
for hive in [HKEY_LOCAL_MACHINE, HKEY_CURRENT_USER] {
|
use winreg::enums::{HKEY_CURRENT_USER, HKEY_LOCAL_MACHINE};
|
||||||
let Ok(clients) =
|
|
||||||
RegKey::predef(hive).open_subkey("SOFTWARE\\Clients\\StartMenuInternet")
|
let mut out: Vec<BrowserEntry> = Vec::new();
|
||||||
else {
|
|
||||||
continue;
|
for hive in [HKEY_LOCAL_MACHINE, HKEY_CURRENT_USER] {
|
||||||
};
|
let Ok(clients) = RegKey::predef(hive).open_subkey("SOFTWARE\\Clients\\StartMenuInternet")
|
||||||
for key_name in clients.enum_keys().flatten() {
|
else {
|
||||||
let Ok(client) = clients.open_subkey(&key_name) else {
|
continue;
|
||||||
continue;
|
};
|
||||||
};
|
for key_name in clients.enum_keys().flatten() {
|
||||||
let friendly: String = client
|
let Ok(client) = clients.open_subkey(&key_name) else {
|
||||||
.get_value("")
|
continue;
|
||||||
.unwrap_or_else(|_| key_name.clone());
|
};
|
||||||
let command: String = client
|
let friendly: String = client.get_value("").unwrap_or_else(|_| key_name.clone());
|
||||||
.open_subkey("shell\\open\\command")
|
let command: String = client
|
||||||
.and_then(|k| k.get_value(""))
|
.open_subkey("shell\\open\\command")
|
||||||
.unwrap_or_default();
|
.and_then(|k| k.get_value(""))
|
||||||
let Some(path) = extract_exe(&command) else {
|
.unwrap_or_default();
|
||||||
continue;
|
let Some(path) = extract_exe(&command) else {
|
||||||
};
|
continue;
|
||||||
if !Path::new(&path).exists() {
|
};
|
||||||
continue;
|
if !Path::new(&path).exists() {
|
||||||
}
|
continue;
|
||||||
if out
|
}
|
||||||
.iter()
|
if out
|
||||||
.any(|b: &BrowserEntry| b.path.eq_ignore_ascii_case(&path))
|
.iter()
|
||||||
{
|
.any(|b: &BrowserEntry| b.path.eq_ignore_ascii_case(&path))
|
||||||
continue;
|
{
|
||||||
}
|
continue;
|
||||||
out.push(BrowserEntry {
|
}
|
||||||
name: friendly,
|
out.push(BrowserEntry {
|
||||||
path,
|
name: friendly,
|
||||||
flags: String::new(),
|
path,
|
||||||
icon: String::new(),
|
flags: String::new(),
|
||||||
emoji: String::new(),
|
icon: String::new(),
|
||||||
});
|
emoji: String::new(),
|
||||||
}
|
});
|
||||||
}
|
}
|
||||||
|
}
|
||||||
out
|
|
||||||
}
|
out
|
||||||
|
}
|
||||||
pub fn icon_for(name: &str) -> &'static str {
|
|
||||||
if is_known_browser(name) {
|
pub fn icon_for(name: &str) -> &'static str {
|
||||||
KNOWN_ICONS
|
if is_known_browser(name) {
|
||||||
.iter()
|
KNOWN_ICONS
|
||||||
.find(|(kw, _)| name.to_lowercase().contains(kw))
|
.iter()
|
||||||
.map(|(_, path)| *path)
|
.find(|(kw, _)| name.to_lowercase().contains(kw))
|
||||||
.unwrap_or("icons/globe.svg")
|
.map(|(_, path)| *path)
|
||||||
} else {
|
.unwrap_or("icons/globe.svg")
|
||||||
"icons/globe.svg"
|
} else {
|
||||||
}
|
"icons/globe.svg"
|
||||||
}
|
}
|
||||||
|
}
|
||||||
const KNOWN_ICONS: &[(&str, &str)] = &[
|
|
||||||
("firefox", "logos/firefox_48x48.png"),
|
const KNOWN_ICONS: &[(&str, &str)] = &[
|
||||||
("chrome", "logos/chrome_48x48.png"),
|
("firefox", "logos/firefox_48x48.png"),
|
||||||
("opera", "logos/opera_48x48.png"),
|
("chrome", "logos/chrome_48x48.png"),
|
||||||
("brave", "logos/brave_48x48.png"),
|
("opera", "logos/opera_48x48.png"),
|
||||||
("яндекс", "logos/yandex_48x48.png"),
|
("brave", "logos/brave_48x48.png"),
|
||||||
("yandex", "logos/yandex_48x48.png"),
|
("яндекс", "logos/yandex_48x48.png"),
|
||||||
];
|
("yandex", "logos/yandex_48x48.png"),
|
||||||
|
];
|
||||||
pub fn is_known_browser(name: &str) -> bool {
|
|
||||||
let n = name.to_lowercase();
|
pub fn is_known_browser(name: &str) -> bool {
|
||||||
KNOWN_ICONS.iter().any(|(kw, _)| n.contains(kw))
|
let n = name.to_lowercase();
|
||||||
|
KNOWN_ICONS.iter().any(|(kw, _)| n.contains(kw))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::{domain_of, extract_exe, is_known_browser};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn domain_basic() {
|
||||||
|
assert_eq!(domain_of("https://example.com/path?q=1"), "example.com");
|
||||||
|
assert_eq!(domain_of("http://example.com"), "example.com");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn domain_strips_www_and_port() {
|
||||||
|
assert_eq!(domain_of("https://www.example.com:8443/a"), "example.com");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn domain_without_scheme() {
|
||||||
|
assert_eq!(domain_of("example.com/x"), "example.com");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn domain_lowercases() {
|
||||||
|
assert_eq!(domain_of("https://EXAMPLE.com"), "example.com");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn extract_exe_quoted_with_args() {
|
||||||
|
assert_eq!(
|
||||||
|
extract_exe(r#""C:\Program Files\app.exe" --flag"#),
|
||||||
|
Some(r"C:\Program Files\app.exe".to_string())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn extract_exe_unquoted() {
|
||||||
|
assert_eq!(
|
||||||
|
extract_exe(r"C:\tools\app.exe --other"),
|
||||||
|
Some(r"C:\tools\app.exe".to_string())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn extract_exe_empty() {
|
||||||
|
assert_eq!(extract_exe(" "), None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn known_browsers_match_case_insensitive() {
|
||||||
|
assert!(is_known_browser("Mozilla Firefox"));
|
||||||
|
assert!(is_known_browser("google chrome"));
|
||||||
|
assert!(is_known_browser("Яндекс Браузер"));
|
||||||
|
assert!(!is_known_browser("Zen Browser"));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,96 @@
|
|||||||
|
use std::cell::RefCell;
|
||||||
|
use std::rc::Rc;
|
||||||
|
|
||||||
|
use crate::config::{self, BrowserEntry, Config};
|
||||||
|
|
||||||
|
pub fn split_flags(flags: &str) -> Vec<String> {
|
||||||
|
let mut out = Vec::new();
|
||||||
|
let mut cur = String::new();
|
||||||
|
let mut in_quotes = false;
|
||||||
|
for c in flags.chars() {
|
||||||
|
match c {
|
||||||
|
'"' => in_quotes = !in_quotes,
|
||||||
|
' ' if !in_quotes => {
|
||||||
|
if !cur.is_empty() {
|
||||||
|
out.push(std::mem::take(&mut cur));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => cur.push(c),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !cur.is_empty() {
|
||||||
|
out.push(cur);
|
||||||
|
}
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn launch(entry: &BrowserEntry, url: &str) -> std::io::Result<()> {
|
||||||
|
let mut cmd = std::process::Command::new(&entry.path);
|
||||||
|
if !entry.flags.is_empty() {
|
||||||
|
cmd.args(split_flags(&entry.flags));
|
||||||
|
}
|
||||||
|
cmd.arg(url).spawn().map(|_| ())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Launch `entry` with `url` and, when `remember` is set, store the
|
||||||
|
/// domain→browser pair in the config. Returns `false` on spawn failure
|
||||||
|
/// (the error is logged).
|
||||||
|
pub fn launch_and_remember(
|
||||||
|
cfg: &Rc<RefCell<Config>>,
|
||||||
|
entry: &BrowserEntry,
|
||||||
|
url: &str,
|
||||||
|
remember: bool,
|
||||||
|
) -> bool {
|
||||||
|
config::log(&format!(
|
||||||
|
"[bp] launching {:?} {:?} {}",
|
||||||
|
entry.path, entry.flags, url
|
||||||
|
));
|
||||||
|
match launch(entry, url) {
|
||||||
|
Ok(_) => {
|
||||||
|
if remember {
|
||||||
|
let domain = config::domain_of(url);
|
||||||
|
if !domain.is_empty() {
|
||||||
|
cfg.borrow_mut()
|
||||||
|
.remembered
|
||||||
|
.insert(domain, entry.name.clone());
|
||||||
|
config::save(&cfg.borrow());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
true
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
config::log(&format!("[bp] launch failed: {e}"));
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::split_flags;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn empty_flags() {
|
||||||
|
assert!(split_flags("").is_empty());
|
||||||
|
assert!(split_flags(" ").is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn simple_flags() {
|
||||||
|
assert_eq!(split_flags("-P Work"), vec!["-P", "Work"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn quoted_spaces_stay_one_arg() {
|
||||||
|
assert_eq!(
|
||||||
|
split_flags(r#"--profile-directory="Profile 1""#),
|
||||||
|
vec!["--profile-directory=Profile 1"]
|
||||||
|
);
|
||||||
|
assert_eq!(split_flags(r#"-P "My Work""#), vec!["-P", "My Work"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn multiple_spaces_collapse() {
|
||||||
|
assert_eq!(split_flags(" a b "), vec!["a", "b"]);
|
||||||
|
}
|
||||||
|
}
|
||||||
+113
-188
@@ -1,4 +1,7 @@
|
|||||||
#![cfg_attr(all(not(debug_assertions), target_os = "windows"), windows_subsystem = "windows")]
|
#![cfg_attr(
|
||||||
|
all(not(debug_assertions), target_os = "windows"),
|
||||||
|
windows_subsystem = "windows"
|
||||||
|
)]
|
||||||
|
|
||||||
use std::cell::RefCell;
|
use std::cell::RefCell;
|
||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
@@ -7,43 +10,38 @@ use i_slint_backend_winit::WinitWindowAccessor;
|
|||||||
use slint::Model;
|
use slint::Model;
|
||||||
|
|
||||||
mod config;
|
mod config;
|
||||||
|
mod launcher;
|
||||||
mod winit;
|
mod winit;
|
||||||
|
|
||||||
use config::{BrowserEntry, Config};
|
use config::{BrowserEntry, Config};
|
||||||
|
|
||||||
slint::include_modules!();
|
slint::include_modules!();
|
||||||
|
|
||||||
fn split_flags(flags: &str) -> Vec<String> {
|
const DEFAULT_URL: &str = "ku6epxboctuk.github.io";
|
||||||
let mut out = Vec::new();
|
const ENV_THEME: &str = "BP_THEME";
|
||||||
let mut cur = String::new();
|
const ENV_VIEW: &str = "BP_VIEW";
|
||||||
let mut in_quotes = false;
|
const VIEW_SETTINGS: &str = "settings";
|
||||||
for c in flags.chars() {
|
|
||||||
match c {
|
impl From<&BrowserConfig> for BrowserEntry {
|
||||||
'"' => in_quotes = !in_quotes,
|
fn from(b: &BrowserConfig) -> Self {
|
||||||
' ' if !in_quotes => {
|
Self {
|
||||||
if !cur.is_empty() {
|
name: b.name.to_string(),
|
||||||
out.push(std::mem::take(&mut cur));
|
path: b.path.to_string(),
|
||||||
}
|
flags: b.flags.to_string(),
|
||||||
}
|
icon: String::new(),
|
||||||
_ => cur.push(c),
|
emoji: b.emoji.to_string(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if !cur.is_empty() {
|
|
||||||
out.push(cur);
|
|
||||||
}
|
|
||||||
out
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn load_icon(rel: &str) -> slint::Image {
|
fn load_icon(rel: &str) -> slint::Image {
|
||||||
if let Ok(exe) = std::env::current_exe() {
|
let from_exe = std::env::current_exe()
|
||||||
if let Some(dir) = exe.parent() {
|
.ok()
|
||||||
let p = dir.join(rel);
|
.and_then(|exe| exe.parent().map(|dir| dir.join(rel)));
|
||||||
if p.exists() {
|
let path = from_exe
|
||||||
return slint::Image::load_from_path(&p).unwrap_or_default();
|
.filter(|p| p.exists())
|
||||||
}
|
.unwrap_or_else(|| std::path::PathBuf::from(rel));
|
||||||
}
|
slint::Image::load_from_path(&path).unwrap_or_default()
|
||||||
}
|
|
||||||
slint::Image::load_from_path(std::path::Path::new(rel)).unwrap_or_default()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn to_ui_config(entry: &BrowserEntry) -> BrowserConfig {
|
fn to_ui_config(entry: &BrowserEntry) -> BrowserConfig {
|
||||||
@@ -65,14 +63,6 @@ fn to_ui_config(entry: &BrowserEntry) -> BrowserConfig {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn launch(entry: &BrowserEntry, url: &str) -> std::io::Result<()> {
|
|
||||||
let mut cmd = std::process::Command::new(&entry.path);
|
|
||||||
if !entry.flags.is_empty() {
|
|
||||||
cmd.args(split_flags(&entry.flags));
|
|
||||||
}
|
|
||||||
cmd.arg(url).spawn().map(|_| ())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn refresh_models(
|
fn refresh_models(
|
||||||
browser_model: &slint::VecModel<BrowserConfig>,
|
browser_model: &slint::VecModel<BrowserConfig>,
|
||||||
remembered_model: &slint::VecModel<RememberedEntry>,
|
remembered_model: &slint::VecModel<RememberedEntry>,
|
||||||
@@ -91,32 +81,63 @@ fn refresh_models(
|
|||||||
remembered_model.set_vec(rows);
|
remembered_model.set_vec(rows);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn init(cfg: Config, url: Option<String>) -> State {
|
fn launch_index(
|
||||||
let cfg = Rc::new(RefCell::new(cfg));
|
main_window: &MainWindow,
|
||||||
|
browser_model: &slint::VecModel<BrowserConfig>,
|
||||||
|
cfg: &Rc<RefCell<Config>>,
|
||||||
|
index: usize,
|
||||||
|
) {
|
||||||
|
let Some(ui_entry) = browser_model.row_data(index) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
main_window.set_selected_index(index as i32);
|
||||||
|
|
||||||
let ui_browsers: Vec<BrowserConfig> = cfg
|
let entry = BrowserEntry::from(&ui_entry);
|
||||||
.borrow()
|
let url = main_window.get_current_url().to_string();
|
||||||
.browsers
|
let remember = main_window.get_remember_choice();
|
||||||
.iter()
|
|
||||||
.map(to_ui_config)
|
if launcher::launch_and_remember(cfg, &entry, &url, remember) {
|
||||||
.collect();
|
let _ = main_window.window().hide();
|
||||||
let browser_model = Rc::new(slint::VecModel::<BrowserConfig>::from(ui_browsers));
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn open_config_in_editor() {
|
||||||
|
let path = config::config_path();
|
||||||
|
if !path.exists() {
|
||||||
|
let mut c = config::load();
|
||||||
|
config::ensure_browsers(&mut c);
|
||||||
|
}
|
||||||
|
let editor = std::process::Command::new("subl")
|
||||||
|
.arg(&path)
|
||||||
|
.spawn()
|
||||||
|
.or_else(|_| std::process::Command::new("notepad").arg(&path).spawn());
|
||||||
|
if let Err(e) = editor {
|
||||||
|
config::log(&format!("cannot open editor: {e}"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn init(cfg: Config, url: Option<String>) -> (MainWindow, SettingsWindow) {
|
||||||
|
let cfg = Rc::new(RefCell::new(cfg));
|
||||||
|
let browser_model = Rc::new(slint::VecModel::<BrowserConfig>::from(vec![]));
|
||||||
let remembered_model = Rc::new(slint::VecModel::<RememberedEntry>::from(vec![]));
|
let remembered_model = Rc::new(slint::VecModel::<RememberedEntry>::from(vec![]));
|
||||||
|
|
||||||
let main_window = MainWindow::new().unwrap();
|
let main_window = MainWindow::new().unwrap();
|
||||||
|
let settings_window = SettingsWindow::new().unwrap();
|
||||||
|
|
||||||
if std::env::var("BP_THEME").as_deref() == Ok("light") {
|
if std::env::var(ENV_THEME).as_deref() == Ok("light") {
|
||||||
main_window.global::<Theme>().set_is_dark(false);
|
main_window.global::<Theme>().set_is_dark(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
refresh_models(&browser_model, &remembered_model, &cfg.borrow());
|
refresh_models(&browser_model, &remembered_model, &cfg.borrow());
|
||||||
|
|
||||||
let settings_window = SettingsWindow::new().unwrap();
|
|
||||||
settings_window.set_browsers(browser_model.clone().into());
|
settings_window.set_browsers(browser_model.clone().into());
|
||||||
settings_window.set_remembered(remembered_model.clone().into());
|
settings_window.set_remembered(remembered_model.clone().into());
|
||||||
|
main_window.set_browser_model(browser_model.clone().into());
|
||||||
|
|
||||||
main_window.set_current_url(
|
main_window.set_current_url(
|
||||||
url.clone().unwrap_or_else(|| "ku6epxboctuk.github.io".into()).into(),
|
url.clone()
|
||||||
|
.unwrap_or_else(|| DEFAULT_URL.to_string())
|
||||||
|
.into(),
|
||||||
);
|
);
|
||||||
if let Some(u) = &url {
|
if let Some(u) = &url {
|
||||||
main_window.set_current_domain(config::domain_of(u).into());
|
main_window.set_current_domain(config::domain_of(u).into());
|
||||||
@@ -128,46 +149,8 @@ fn init(cfg: Config, url: Option<String>) -> State {
|
|||||||
let main_window = main_window.clone_strong();
|
let main_window = main_window.clone_strong();
|
||||||
let browser_model = browser_model.clone();
|
let browser_model = browser_model.clone();
|
||||||
let cfg = cfg.clone();
|
let cfg = cfg.clone();
|
||||||
move |browser: BrowserConfig| {
|
move |index: i32| {
|
||||||
let index = (0..browser_model.row_count())
|
launch_index(&main_window, &browser_model, &cfg, index.max(0) as usize);
|
||||||
.find(|&i| {
|
|
||||||
browser_model
|
|
||||||
.row_data(i)
|
|
||||||
.is_some_and(|b| b.name == browser.name)
|
|
||||||
})
|
|
||||||
.unwrap_or(0);
|
|
||||||
main_window.set_selected_index(index as i32);
|
|
||||||
|
|
||||||
let entry = browser_model.row_data(index).expect("row exists");
|
|
||||||
let url = main_window.get_current_url().to_string();
|
|
||||||
|
|
||||||
let entry_ref = BrowserEntry {
|
|
||||||
name: entry.name.to_string(),
|
|
||||||
path: entry.path.to_string(),
|
|
||||||
flags: entry.flags.to_string(),
|
|
||||||
icon: String::new(),
|
|
||||||
emoji: String::new(),
|
|
||||||
};
|
|
||||||
let url = main_window.get_current_url().to_string();
|
|
||||||
|
|
||||||
config::log(&format!(
|
|
||||||
"[bp] launching [{}]: {:?} {:?} {}",
|
|
||||||
index, entry_ref.path, entry_ref.flags, url
|
|
||||||
));
|
|
||||||
|
|
||||||
match launch(&entry_ref, &url) {
|
|
||||||
Ok(_) => {
|
|
||||||
let domain = config::domain_of(&url);
|
|
||||||
if main_window.get_remember_choice() && !domain.is_empty() {
|
|
||||||
cfg.borrow_mut()
|
|
||||||
.remembered
|
|
||||||
.insert(domain, entry_ref.name.clone());
|
|
||||||
config::save(&cfg.borrow());
|
|
||||||
}
|
|
||||||
let _ = main_window.window().hide();
|
|
||||||
}
|
|
||||||
Err(e) => config::log(&format!("[bp] launch failed: {e}")),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -177,36 +160,7 @@ fn init(cfg: Config, url: Option<String>) -> State {
|
|||||||
let cfg = cfg.clone();
|
let cfg = cfg.clone();
|
||||||
move || {
|
move || {
|
||||||
let index = main_window.get_selected_index().max(0) as usize;
|
let index = main_window.get_selected_index().max(0) as usize;
|
||||||
let Some(ui_entry) = browser_model.row_data(index) else {
|
launch_index(&main_window, &browser_model, &cfg, index);
|
||||||
return;
|
|
||||||
};
|
|
||||||
let entry = BrowserEntry {
|
|
||||||
name: ui_entry.name.to_string(),
|
|
||||||
path: ui_entry.path.to_string(),
|
|
||||||
flags: ui_entry.flags.to_string(),
|
|
||||||
icon: String::new(),
|
|
||||||
emoji: String::new(),
|
|
||||||
};
|
|
||||||
let url = main_window.get_current_url().to_string();
|
|
||||||
|
|
||||||
config::log(&format!(
|
|
||||||
"[bp] launching [{}]: {:?} {:?} {}",
|
|
||||||
index, entry.path, entry.flags, url
|
|
||||||
));
|
|
||||||
|
|
||||||
match launch(&entry, &url) {
|
|
||||||
Ok(_) => {
|
|
||||||
let domain = config::domain_of(&url);
|
|
||||||
if main_window.get_remember_choice() && !domain.is_empty() {
|
|
||||||
cfg.borrow_mut()
|
|
||||||
.remembered
|
|
||||||
.insert(domain, entry.name.clone());
|
|
||||||
config::save(&cfg.borrow());
|
|
||||||
}
|
|
||||||
let _ = main_window.window().hide();
|
|
||||||
}
|
|
||||||
Err(e) => config::log(&format!("[bp] launch failed: {e}")),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -227,7 +181,7 @@ fn init(cfg: Config, url: Option<String>) -> State {
|
|||||||
});
|
});
|
||||||
|
|
||||||
main_window.on_settings_clicked(move || {
|
main_window.on_settings_clicked(move || {
|
||||||
println!("Settings clicked");
|
open_config_in_editor();
|
||||||
});
|
});
|
||||||
|
|
||||||
main_window.on_request_drag({
|
main_window.on_request_drag({
|
||||||
@@ -239,20 +193,10 @@ fn init(cfg: Config, url: Option<String>) -> State {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
main_window.on_settings_clicked({
|
main_window.on_esc_pressed({
|
||||||
|
let main_window = main_window.clone_strong();
|
||||||
move || {
|
move || {
|
||||||
let path = config::config_path();
|
let _ = main_window.window().hide();
|
||||||
if !path.exists() {
|
|
||||||
let mut c = config::load();
|
|
||||||
config::ensure_browsers(&mut c);
|
|
||||||
}
|
|
||||||
let editor = std::process::Command::new("subl")
|
|
||||||
.arg(&path)
|
|
||||||
.spawn()
|
|
||||||
.or_else(|_| std::process::Command::new("notepad").arg(&path).spawn());
|
|
||||||
if let Err(e) = editor {
|
|
||||||
config::log(&format!("[bp] cannot open editor: {e}"));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -265,7 +209,6 @@ fn init(cfg: Config, url: Option<String>) -> State {
|
|||||||
cfg.borrow_mut().browsers.retain(|b| b.name != name);
|
cfg.borrow_mut().browsers.retain(|b| b.name != name);
|
||||||
config::save(&cfg.borrow());
|
config::save(&cfg.borrow());
|
||||||
refresh_models(&browser_model, &remembered_model, &cfg.borrow());
|
refresh_models(&browser_model, &remembered_model, &cfg.borrow());
|
||||||
println!("Removed browser: {name}");
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -278,7 +221,6 @@ fn init(cfg: Config, url: Option<String>) -> State {
|
|||||||
cfg.borrow_mut().remembered.remove(&domain);
|
cfg.borrow_mut().remembered.remove(&domain);
|
||||||
config::save(&cfg.borrow());
|
config::save(&cfg.borrow());
|
||||||
refresh_models(&browser_model, &remembered_model, &cfg.borrow());
|
refresh_models(&browser_model, &remembered_model, &cfg.borrow());
|
||||||
println!("Removed remembered domain: {domain}");
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -301,7 +243,6 @@ fn init(cfg: Config, url: Option<String>) -> State {
|
|||||||
config::save(&cfg_ref);
|
config::save(&cfg_ref);
|
||||||
drop(cfg_ref);
|
drop(cfg_ref);
|
||||||
refresh_models(&browser_model, &remembered_model, &cfg.borrow());
|
refresh_models(&browser_model, &remembered_model, &cfg.borrow());
|
||||||
println!("Re-detected browsers");
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -314,13 +255,6 @@ fn init(cfg: Config, url: Option<String>) -> State {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
main_window.on_esc_pressed({
|
|
||||||
let main_window = main_window.clone_strong();
|
|
||||||
move || {
|
|
||||||
let _ = main_window.window().hide();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
settings_window.window().on_close_requested({
|
settings_window.window().on_close_requested({
|
||||||
let main_window = main_window.clone_strong();
|
let main_window = main_window.clone_strong();
|
||||||
move || {
|
move || {
|
||||||
@@ -329,22 +263,39 @@ fn init(cfg: Config, url: Option<String>) -> State {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
main_window.set_browser_model(browser_model.clone().into());
|
(main_window, settings_window)
|
||||||
State {
|
|
||||||
main_window,
|
|
||||||
settings_window,
|
|
||||||
browser_model,
|
|
||||||
remembered_model,
|
|
||||||
config: cfg,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct State {
|
fn show_and_run(
|
||||||
pub main_window: MainWindow,
|
main_window: MainWindow,
|
||||||
pub settings_window: SettingsWindow,
|
settings_window: SettingsWindow,
|
||||||
pub browser_model: Rc<slint::VecModel<BrowserConfig>>,
|
start: std::time::Instant,
|
||||||
pub remembered_model: Rc<slint::VecModel<RememberedEntry>>,
|
) {
|
||||||
pub config: Rc<RefCell<Config>>,
|
let settings_only = std::env::var(ENV_VIEW).as_deref() == Ok(VIEW_SETTINGS);
|
||||||
|
|
||||||
|
if settings_only {
|
||||||
|
settings_window.show().unwrap();
|
||||||
|
let weak = settings_window.as_weak();
|
||||||
|
let _ = slint::invoke_from_event_loop(move || {
|
||||||
|
if let Some(sw) = weak.upgrade() {
|
||||||
|
winit::center_window(sw.window());
|
||||||
|
sw.invoke_restore_focus();
|
||||||
|
}
|
||||||
|
config::log(&format!("cold start -> event loop: {:?}", start.elapsed()));
|
||||||
|
});
|
||||||
|
settings_window.run().unwrap();
|
||||||
|
} else {
|
||||||
|
main_window.show().unwrap();
|
||||||
|
let weak = main_window.as_weak();
|
||||||
|
let _ = slint::invoke_from_event_loop(move || {
|
||||||
|
if let Some(mw) = weak.upgrade() {
|
||||||
|
winit::center_window(mw.window());
|
||||||
|
mw.invoke_restore_focus();
|
||||||
|
}
|
||||||
|
config::log(&format!("cold start -> event loop: {:?}", start.elapsed()));
|
||||||
|
});
|
||||||
|
main_window.run().unwrap();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn main() {
|
pub fn main() {
|
||||||
@@ -359,43 +310,17 @@ pub fn main() {
|
|||||||
let domain = config::domain_of(url);
|
let domain = config::domain_of(url);
|
||||||
if let Some(name) = cfg.remembered.get(&domain).cloned() {
|
if let Some(name) = cfg.remembered.get(&domain).cloned() {
|
||||||
if cfg.settings.always_ask {
|
if cfg.settings.always_ask {
|
||||||
config::log(&format!("[bp] '{domain}' remembered -> {name}, but always_ask is on"));
|
config::log(&format!(
|
||||||
|
"'{domain}' remembered -> {name}, but always_ask is on"
|
||||||
|
));
|
||||||
} else if let Some(entry) = cfg.browsers.iter().find(|b| b.name == name) {
|
} else if let Some(entry) = cfg.browsers.iter().find(|b| b.name == name) {
|
||||||
config::log(&format!("[bp] '{domain}' remembered -> launching {name}"));
|
config::log(&format!("'{domain}' remembered -> launching {name}"));
|
||||||
let _ = launch(entry, url);
|
let _ = launcher::launch(entry, url);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let state = init(cfg, url);
|
let (main_window, settings_window) = init(cfg, url);
|
||||||
|
show_and_run(main_window, settings_window, start);
|
||||||
if std::env::var("BP_VIEW").as_deref() == Ok("settings") {
|
|
||||||
state.settings_window.show().unwrap();
|
|
||||||
let weak = state.settings_window.as_weak();
|
|
||||||
let _ = slint::invoke_from_event_loop(move || {
|
|
||||||
if let Some(sw) = weak.upgrade() {
|
|
||||||
winit::center_window(sw.window());
|
|
||||||
sw.invoke_restore_focus();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
state.settings_window.run().unwrap();
|
|
||||||
} else {
|
|
||||||
let main_window = state.main_window.clone_strong();
|
|
||||||
main_window.show().unwrap();
|
|
||||||
|
|
||||||
let weak = main_window.as_weak();
|
|
||||||
let _ = slint::invoke_from_event_loop(move || {
|
|
||||||
if let Some(mw) = weak.upgrade() {
|
|
||||||
winit::center_window(mw.window());
|
|
||||||
mw.invoke_restore_focus();
|
|
||||||
}
|
|
||||||
config::log(&format!(
|
|
||||||
"[bp] cold start -> event loop: {:?}",
|
|
||||||
start.elapsed()
|
|
||||||
));
|
|
||||||
});
|
|
||||||
|
|
||||||
state.main_window.run().unwrap();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-4
@@ -6,10 +6,9 @@ use i_slint_backend_winit::winit::window::Window;
|
|||||||
pub fn center_window(window: &slint::Window) {
|
pub fn center_window(window: &slint::Window) {
|
||||||
if window.has_winit_window() {
|
if window.has_winit_window() {
|
||||||
window.with_winit_window(|window: &Window| {
|
window.with_winit_window(|window: &Window| {
|
||||||
match window.current_monitor() {
|
if let Some(monitor) = window.current_monitor() {
|
||||||
Some(monitor) => set_centered(window, &monitor),
|
set_centered(window, &monitor);
|
||||||
None => (),
|
}
|
||||||
};
|
|
||||||
|
|
||||||
None as Option<()>
|
None as Option<()>
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
$ErrorActionPreference = "Stop"
|
||||||
|
|
||||||
|
cargo fmt
|
||||||
|
if ($LASTEXITCODE -ne 0) { exit 1 }
|
||||||
|
|
||||||
|
cargo clippy --all-targets -- -D warnings
|
||||||
|
if ($LASTEXITCODE -ne 0) { exit 1 }
|
||||||
|
|
||||||
|
cargo test
|
||||||
|
if ($LASTEXITCODE -ne 0) { exit 1 }
|
||||||
|
|
||||||
|
Write-Host "check OK"
|
||||||
@@ -1,13 +1,14 @@
|
|||||||
import { Theme } from "./theme.slint";
|
import { Theme } from "./theme.slint";
|
||||||
|
|
||||||
export component BrowserItem inherits Rectangle {
|
export component BrowserItem inherits Rectangle {
|
||||||
|
in property <int> index;
|
||||||
in property <image> browser-icon;
|
in property <image> browser-icon;
|
||||||
in property <string> emoji;
|
in property <string> emoji;
|
||||||
in property <bool> icon-mask;
|
in property <bool> icon-mask;
|
||||||
in property <string> browser-name;
|
in property <string> browser-name;
|
||||||
in property <bool> is-default;
|
in property <bool> is-default;
|
||||||
in property <bool> selected;
|
in property <bool> selected;
|
||||||
callback clicked;
|
callback clicked(int);
|
||||||
|
|
||||||
height: 84px;
|
height: 84px;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
@@ -20,12 +21,12 @@ export component BrowserItem inherits Rectangle {
|
|||||||
: root.selected ? Theme.surface-elevated
|
: root.selected ? Theme.surface-elevated
|
||||||
: Theme.surface-item;
|
: Theme.surface-item;
|
||||||
border-width: root.selected ? 2px : 1px;
|
border-width: root.selected ? 2px : 1px;
|
||||||
border-color: root.selected ? #3b82f680 : transparent;
|
border-color: root.selected ? Theme.accent-ring : transparent;
|
||||||
|
|
||||||
touch := TouchArea {
|
touch := TouchArea {
|
||||||
mouse-cursor: pointer;
|
mouse-cursor: pointer;
|
||||||
clicked => {
|
clicked => {
|
||||||
root.clicked();
|
root.clicked(root.index);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -84,7 +85,7 @@ export component BrowserItem inherits Rectangle {
|
|||||||
if root.is-default: Rectangle {
|
if root.is-default: Rectangle {
|
||||||
height: 22px;
|
height: 22px;
|
||||||
border-radius: 11px;
|
border-radius: 11px;
|
||||||
background: #3b82f620;
|
background: Theme.accent-soft;
|
||||||
|
|
||||||
HorizontalLayout {
|
HorizontalLayout {
|
||||||
padding-left: 10px;
|
padding-left: 10px;
|
||||||
@@ -109,7 +110,7 @@ export component BrowserItem inherits Rectangle {
|
|||||||
width: 16px;
|
width: 16px;
|
||||||
height: 16px;
|
height: 16px;
|
||||||
source: @image-url("../icons/check.svg");
|
source: @image-url("../icons/check.svg");
|
||||||
colorize: #3b82f6;
|
colorize: Theme.accent;
|
||||||
visible: root.selected;
|
visible: root.selected;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+7
-6
@@ -1,4 +1,4 @@
|
|||||||
import { Button, CheckBox, ListView } from "std-widgets.slint";
|
import { ListView } from "std-widgets.slint";
|
||||||
import { BrowserItem } from "./browser_item.slint";
|
import { BrowserItem } from "./browser_item.slint";
|
||||||
import { Header } from "./header.slint";
|
import { Header } from "./header.slint";
|
||||||
import { Footer } from "./footer.slint";
|
import { Footer } from "./footer.slint";
|
||||||
@@ -32,9 +32,9 @@ component CheckRow inherits Rectangle {
|
|||||||
Rectangle {
|
Rectangle {
|
||||||
width: 20px;
|
width: 20px;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
background: root.checked ? #3b82f6 : transparent;
|
background: root.checked ? Theme.accent : transparent;
|
||||||
border-width: 2px;
|
border-width: 2px;
|
||||||
border-color: root.checked ? #3b82f6 : Theme.border-main;
|
border-color: root.checked ? Theme.accent : Theme.border-main;
|
||||||
|
|
||||||
Image {
|
Image {
|
||||||
x: (parent.width - self.width) / 2;
|
x: (parent.width - self.width) / 2;
|
||||||
@@ -65,7 +65,7 @@ export component MainWindow inherits Window {
|
|||||||
in-out property <bool> always-ask: true;
|
in-out property <bool> always-ask: true;
|
||||||
in property <int> selected-index: 0;
|
in property <int> selected-index: 0;
|
||||||
|
|
||||||
callback launch-browser(BrowserConfig);
|
callback launch-browser(int);
|
||||||
callback toggle-remember(bool);
|
callback toggle-remember(bool);
|
||||||
callback toggle-always-ask(bool);
|
callback toggle-always-ask(bool);
|
||||||
callback settings-clicked();
|
callback settings-clicked();
|
||||||
@@ -128,14 +128,15 @@ export component MainWindow inherits Window {
|
|||||||
vertical-stretch: 1;
|
vertical-stretch: 1;
|
||||||
|
|
||||||
for browser[i] in root.browser-model: BrowserItem {
|
for browser[i] in root.browser-model: BrowserItem {
|
||||||
|
index: i;
|
||||||
browser-icon: browser.icon;
|
browser-icon: browser.icon;
|
||||||
emoji: browser.emoji;
|
emoji: browser.emoji;
|
||||||
icon-mask: browser.icon-mask;
|
icon-mask: browser.icon-mask;
|
||||||
browser-name: browser.name;
|
browser-name: browser.name;
|
||||||
is-default: browser.is-default;
|
is-default: browser.is-default;
|
||||||
selected: i == root.selected-index;
|
selected: i == root.selected-index;
|
||||||
clicked => {
|
clicked(idx) => {
|
||||||
root.launch-browser(browser);
|
root.launch-browser(idx);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+4
-4
@@ -137,7 +137,7 @@ export component SettingsWindow inherits Window {
|
|||||||
Rectangle {
|
Rectangle {
|
||||||
width: 30px;
|
width: 30px;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
background: del-touch.has-hover ? #ef444420 : transparent;
|
background: del-touch.has-hover ? Theme.danger-soft : transparent;
|
||||||
|
|
||||||
del-touch := TouchArea {
|
del-touch := TouchArea {
|
||||||
mouse-cursor: pointer;
|
mouse-cursor: pointer;
|
||||||
@@ -152,7 +152,7 @@ export component SettingsWindow inherits Window {
|
|||||||
width: 16px;
|
width: 16px;
|
||||||
height: 16px;
|
height: 16px;
|
||||||
source: @image-url("../icons/trash.svg");
|
source: @image-url("../icons/trash.svg");
|
||||||
colorize: del-touch.has-hover ? #ef4444 : Theme.text-muted;
|
colorize: del-touch.has-hover ? Theme.danger : Theme.text-muted;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -209,7 +209,7 @@ export component SettingsWindow inherits Window {
|
|||||||
Rectangle {
|
Rectangle {
|
||||||
width: 30px;
|
width: 30px;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
background: del2-touch.has-hover ? #ef444420 : transparent;
|
background: del2-touch.has-hover ? Theme.danger-soft : transparent;
|
||||||
|
|
||||||
del2-touch := TouchArea {
|
del2-touch := TouchArea {
|
||||||
mouse-cursor: pointer;
|
mouse-cursor: pointer;
|
||||||
@@ -224,7 +224,7 @@ export component SettingsWindow inherits Window {
|
|||||||
width: 16px;
|
width: 16px;
|
||||||
height: 16px;
|
height: 16px;
|
||||||
source: @image-url("../icons/trash.svg");
|
source: @image-url("../icons/trash.svg");
|
||||||
colorize: del2-touch.has-hover ? #ef4444 : Theme.text-muted;
|
colorize: del2-touch.has-hover ? Theme.danger : Theme.text-muted;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,6 +21,12 @@ export global Theme {
|
|||||||
|
|
||||||
out property <brush> text-hint: is-dark ? #475569 : #475569;
|
out property <brush> text-hint: is-dark ? #475569 : #475569;
|
||||||
|
|
||||||
|
out property <color> accent: #3b82f6;
|
||||||
|
out property <color> accent-ring: #3b82f680;
|
||||||
|
out property <color> accent-soft: #3b82f620;
|
||||||
|
out property <color> danger: #ef4444;
|
||||||
|
out property <color> danger-soft: #ef444420;
|
||||||
|
|
||||||
out property <brush> shadow-color: is-dark ? #3b82f640 : #3b82f640;
|
out property <brush> shadow-color: is-dark ? #3b82f640 : #3b82f640;
|
||||||
|
|
||||||
out property <length> shadow-primary-x: 0px;
|
out property <length> shadow-primary-x: 0px;
|
||||||
|
|||||||
Reference in New Issue
Block a user