diff --git a/docs/dev-workflow.md b/docs/dev-workflow.md index e70ceaa..723848a 100644 --- a/docs/dev-workflow.md +++ b/docs/dev-workflow.md @@ -116,11 +116,14 @@ HTML-макеты → PNG: **headless Edge** (`--headless --screenshot`), он домена в конфиг; переключение галочек персистится - [x] URL-бар привязан к свойству (был захардкожен), метка чекбокса — домен -### E2 — страница настроек (следующий шаг) +### E2 — страница настроек — ГОТОВО (базовая версия) -- [ ] Переключение вид/настройки по кнопке cog (settings-clicked) -- [ ] Редактор списка браузеров: имя/путь/флаги (LineEdit), вкл/выкл -- [ ] Список запомненных доменов с удалением +- [x] Переключение вид/настройки по кнопке cog; Esc в настройках = назад; + env `BP_VIEW=settings` для скриншотов +- [x] Список браузеров: иконка + имя + путь (elide), удаление корзиной +- [x] Кнопка «Автодетект»: мерж найденных по пути, без дублей +- [x] Запомненные домены (domain → browser) с удалением +- [ ] Редактирование браузера (имя/путь/флаги через LineEdit) — следующий шаг - [ ] Чтение флагов из .lnk ярлыков (крейс `lnk`) — по желанию - [ ] Добавить логотипы Edge/Zen в logos/ (пересоздать набор из alrrr/browser-logos) diff --git a/icons/arrow_left.svg b/icons/arrow_left.svg new file mode 100644 index 0000000..982a498 --- /dev/null +++ b/icons/arrow_left.svg @@ -0,0 +1,4 @@ + + + + diff --git a/icons/trash.svg b/icons/trash.svg new file mode 100644 index 0000000..8a15eaf --- /dev/null +++ b/icons/trash.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/main.rs b/src/main.rs index 8a25f32..391e260 100644 --- a/src/main.rs +++ b/src/main.rs @@ -38,6 +38,24 @@ fn launch(entry: &BrowserEntry, url: &str) -> std::io::Result<()> { cmd.arg(url).spawn().map(|_| ()) } +fn refresh_models( + browser_model: &slint::VecModel, + remembered_model: &slint::VecModel, + cfg: &Config, +) { + let rows: Vec = cfg.browsers.iter().map(to_ui_config).collect(); + browser_model.set_vec(rows); + let rows: Vec = cfg + .remembered + .iter() + .map(|(d, b)| RememberedEntry { + domain: d.clone().into(), + browser: b.clone().into(), + }) + .collect(); + remembered_model.set_vec(rows); +} + fn init(cfg: Config, url: Option) -> State { let cfg = Rc::new(RefCell::new(cfg)); @@ -48,6 +66,7 @@ fn init(cfg: Config, url: Option) -> State { .map(to_ui_config) .collect(); let browser_model = Rc::new(slint::VecModel::::from(ui_browsers)); + let remembered_model = Rc::new(slint::VecModel::::from(vec![])); let main_window = MainWindow::new().unwrap(); @@ -55,6 +74,12 @@ fn init(cfg: Config, url: Option) -> State { main_window.global::().set_is_dark(false); } + refresh_models(&browser_model, &remembered_model, &cfg.borrow()); + + if std::env::var("BP_VIEW").as_deref() == Ok("settings") { + main_window.set_settings_open(true); + } + main_window.set_current_url( url.clone().unwrap_or_else(|| "ku6epxboctuk.github.io".into()).into(), ); @@ -176,17 +201,90 @@ fn init(cfg: Config, url: Option) -> State { } }); + main_window.on_settings_clicked({ + let main_window = main_window.clone_strong(); + let browser_model = browser_model.clone(); + let remembered_model = remembered_model.clone(); + let cfg = cfg.clone(); + move || { + refresh_models(&browser_model, &remembered_model, &cfg.borrow()); + main_window.set_settings_open(true); + } + }); + + main_window.on_settings_closed({ + let main_window = main_window.clone_strong(); + move || { + main_window.set_settings_open(false); + } + }); + + main_window.on_remove_browser({ + let browser_model = browser_model.clone(); + let remembered_model = remembered_model.clone(); + let cfg = cfg.clone(); + move |name| { + let name = name.to_string(); + cfg.borrow_mut().browsers.retain(|b| b.name != name); + config::save(&cfg.borrow()); + refresh_models(&browser_model, &remembered_model, &cfg.borrow()); + println!("Removed browser: {name}"); + } + }); + + main_window.on_remove_remembered({ + let browser_model = browser_model.clone(); + let remembered_model = remembered_model.clone(); + let cfg = cfg.clone(); + move |domain| { + let domain = domain.to_string(); + cfg.borrow_mut().remembered.remove(&domain); + config::save(&cfg.borrow()); + refresh_models(&browser_model, &remembered_model, &cfg.borrow()); + println!("Removed remembered domain: {domain}"); + } + }); + + main_window.on_redetect_browsers({ + let browser_model = browser_model.clone(); + let remembered_model = remembered_model.clone(); + let cfg = cfg.clone(); + move || { + let detected = config::detect_browsers(); + let mut cfg_ref = cfg.borrow_mut(); + for entry in detected { + if !cfg_ref + .browsers + .iter() + .any(|b| b.path.eq_ignore_ascii_case(&entry.path)) + { + cfg_ref.browsers.push(entry); + } + } + config::save(&cfg_ref); + drop(cfg_ref); + refresh_models(&browser_model, &remembered_model, &cfg.borrow()); + println!("Re-detected browsers"); + } + }); + main_window.on_esc_pressed({ let main_window = main_window.clone_strong(); move || { - let _ = main_window.window().hide(); + if main_window.get_settings_open() { + main_window.set_settings_open(false); + } else { + let _ = main_window.window().hide(); + } } }); main_window.set_browser_model(browser_model.clone().into()); + main_window.set_remembered_list(remembered_model.clone().into()); State { main_window, browser_model, + remembered_model, config: cfg, } } @@ -194,6 +292,7 @@ fn init(cfg: Config, url: Option) -> State { pub struct State { pub main_window: MainWindow, pub browser_model: Rc>, + pub remembered_model: Rc>, pub config: Rc>, } diff --git a/ui/main.slint b/ui/main.slint index ca4a0af..5df4fde 100644 --- a/ui/main.slint +++ b/ui/main.slint @@ -2,7 +2,8 @@ import { Button, CheckBox, ListView } from "std-widgets.slint"; import { BrowserItem } from "./browser_item.slint"; import { Header } from "./header.slint"; import { Footer } from "./footer.slint"; -import { BrowserConfig } from "./types.slint"; +import { SettingsView } from "./settings.slint"; +import { BrowserConfig, RememberedEntry } from "./types.slint"; import { Theme } from "theme.slint"; export { Theme } @@ -62,11 +63,17 @@ export component MainWindow inherits Window { in-out property remember-choice: false; in-out property always-ask: true; in property selected-index: 0; + in property <[RememberedEntry]> remembered-list: []; + in-out property settings-open: false; callback launch-browser(BrowserConfig); callback toggle-remember(bool); callback toggle-always-ask(bool); callback settings-clicked(); + callback settings-closed(); + callback remove-browser(string); + callback remove-remembered(string); + callback redetect-browsers(); callback open-clicked(); callback request-drag(); callback esc-pressed(); @@ -106,7 +113,24 @@ export component MainWindow inherits Window { border-width: 1px; border-color: Theme.border-main; - VerticalLayout { + if root.settings-open: SettingsView { + browsers: root.browser-model; + remembered: root.remembered-list; + back => { + root.settings-closed(); + } + remove-browser(name) => { + root.remove-browser(name); + } + remove-remembered(domain) => { + root.remove-remembered(domain); + } + redetect => { + root.redetect-browsers(); + } + } + + if !root.settings-open: VerticalLayout { Header { url: current-url; diff --git a/ui/settings.slint b/ui/settings.slint new file mode 100644 index 0000000..1c28437 --- /dev/null +++ b/ui/settings.slint @@ -0,0 +1,235 @@ +import { ListView } from "std-widgets.slint"; +import { Theme } from "./theme.slint"; +import { BrowserConfig, RememberedEntry } from "./types.slint"; + +export component SettingsView inherits Rectangle { + in property <[BrowserConfig]> browsers; + in property <[RememberedEntry]> remembered; + callback back(); + callback remove-browser(string); + callback remove-remembered(string); + callback redetect(); + + background: transparent; + + VerticalLayout { + padding: 16px; + spacing: 12px; + + HorizontalLayout { + spacing: 8px; + + Rectangle { + width: 32px; + border-radius: 8px; + background: back-touch.has-hover ? Theme.surface-hover : transparent; + + back-touch := TouchArea { + mouse-cursor: pointer; + clicked => { + root.back(); + } + } + + Image { + x: (parent.width - self.width) / 2; + y: (parent.height - self.height) / 2; + width: 20px; + height: 20px; + source: @image-url("../icons/arrow_left.svg"); + colorize: back-touch.has-hover ? Theme.text-main : Theme.text-muted; + } + } + + VerticalLayout { + alignment: center; + + Text { + text: "Настройки"; + color: Theme.text-main; + font-size: 18px; + font-weight: 600; + vertical-alignment: center; + } + } + } + + HorizontalLayout { + spacing: 8px; + + Text { + text: "Браузеры"; + color: Theme.text-muted; + font-size: 13px; + vertical-alignment: center; + } + + Rectangle {} + + Rectangle { + width: 120px; + height: 26px; + border-radius: 8px; + background: redetect-touch.has-hover ? Theme.surface-hover : Theme.surface-elevated; + border-width: 1px; + border-color: Theme.border-main; + + redetect-touch := TouchArea { + mouse-cursor: pointer; + clicked => { + root.redetect(); + } + } + + Text { + width: 100%; + height: 100%; + text: "Автодетект"; + color: redetect-touch.has-hover ? Theme.text-main : Theme.text-control; + font-size: 12px; + horizontal-alignment: center; + vertical-alignment: center; + } + } + } + + Rectangle { + vertical-stretch: 3; + border-radius: 8px; + background: Theme.surface-elevated; + clip: true; + + ListView { + for b in root.browsers: Rectangle { + height: 52px; + background: transparent; + + HorizontalLayout { + padding-left: 12px; + padding-right: 8px; + spacing: 10px; + + Image { + width: 22px; + y: (parent.height - self.height) / 2; + source: b.icon; + } + + VerticalLayout { + horizontal-stretch: 1; + alignment: center; + + Text { + text: b.name; + color: Theme.text-main; + font-size: 14px; + } + + Text { + text: b.path; + color: Theme.text-hint; + font-size: 11px; + font-family: "Consolas"; + overflow: elide; + } + } + + Rectangle { + width: 28px; + border-radius: 6px; + background: del-touch.has-hover ? #ef444420 : transparent; + + del-touch := TouchArea { + mouse-cursor: pointer; + clicked => { + root.remove-browser(b.name); + } + } + + Image { + x: (parent.width - self.width) / 2; + y: (parent.height - self.height) / 2; + width: 15px; + height: 15px; + source: @image-url("../icons/trash.svg"); + colorize: del-touch.has-hover ? #ef4444 : Theme.text-muted; + } + } + } + } + } + } + + Text { + text: "Запомненные домены"; + color: Theme.text-muted; + font-size: 13px; + } + + Rectangle { + vertical-stretch: 2; + border-radius: 8px; + background: Theme.surface-elevated; + clip: true; + + ListView { + for r in root.remembered: Rectangle { + height: 40px; + background: transparent; + + HorizontalLayout { + padding-left: 12px; + padding-right: 8px; + spacing: 10px; + + VerticalLayout { + horizontal-stretch: 1; + alignment: center; + + HorizontalLayout { + spacing: 6px; + + Text { + text: r.domain; + color: Theme.text-main; + font-size: 13px; + font-family: "Consolas"; + vertical-alignment: center; + } + + Text { + text: "→ " + r.browser; + color: Theme.text-hint; + font-size: 12px; + vertical-alignment: center; + } + } + } + + Rectangle { + width: 28px; + border-radius: 6px; + background: del2-touch.has-hover ? #ef444420 : transparent; + + del2-touch := TouchArea { + mouse-cursor: pointer; + clicked => { + root.remove-remembered(r.domain); + } + } + + Image { + x: (parent.width - self.width) / 2; + y: (parent.height - self.height) / 2; + width: 15px; + height: 15px; + source: @image-url("../icons/trash.svg"); + colorize: del2-touch.has-hover ? #ef4444 : Theme.text-muted; + } + } + } + } + } + } + } +} diff --git a/ui/types.slint b/ui/types.slint index 46ce35f..113615a 100644 --- a/ui/types.slint +++ b/ui/types.slint @@ -5,3 +5,8 @@ export struct BrowserConfig { flags: string, is-default: bool, } + +export struct RememberedEntry { + domain: string, + browser: string, +}