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,
+}