Skip to content

Commit 5f67e02

Browse files
committed
🐛 fix: fix: browser detection, mode switch persistence, UX polish
Browser detection: - Add multi-path probe: /usr/bin/brave, brave-browser, brave-browser-stable - Normalize browser_id to "brave" (match BigLinux convention) - Fix xdg-settings mapping: brave-browser.desktop → "brave" Mode switch (App ↔ Browser): - Preserve browser field on App mode toggle (no __viewer__ override) - Update browser_row subtitle on mode change - Rewrite same .desktop file on mode switch (no duplicate entries) - Call update-desktop-database after install/remove Version: - APP_VERSION reads from Cargo.toml via env!("CARGO_PKG_VERSION") UI/UX: - Globe icon → "Detect" text button with auto-detect on URL change (800ms debounce) - "+" icon → "Add" text button with suggested-action styling - Template icon → "Templates" text button with suggested-action styling - App mode icon: big-webapps colorful SVG (not grey symbolic) - System accent color (blue) on action buttons via suggested-action CSS - Dialog: 3 PreferencesGroup cards (Website, Appearance, Behavior) Translations (pt-BR): - "Modo do App" → "Modo App" - Add "Adicionar", Detect "Detectar" Viewer: - Chrome 131 User-Agent string (fix Spotify WebKitGTK block) Fixes: - RefCell panic: Rc<RefCell<Option<SourceId>>> for debounce timer - URL normalization: prepend https:// if missing scheme - Icon resolution: hostname fallback + Google Favicon API - Cookie persistence: ephemeral → default data manager - Desktop entry: sanitize fields, proper WM class derivation
1 parent 4d93140 commit 5f67e02

9 files changed

Lines changed: 196 additions & 68 deletions

File tree

biglinux-webapps/locale/pt-BR.po

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ msgstr "Categoria"
9898

9999
#: /home/talesam/Servidor/GitHub/biglinux-webapps/crates/webapps-manager/src/webapp_dialog.rs:171
100100
msgid "App Mode"
101-
msgstr "Modo do App"
101+
msgstr "Modo App"
102102

103103
#: /home/talesam/Servidor/GitHub/biglinux-webapps/crates/webapps-manager/src/webapp_dialog.rs:172
104104
msgid "Opens as a native window without browser interface"
@@ -352,3 +352,11 @@ msgstr "Download Concluído"
352352
#: /home/talesam/Servidor/GitHub/biglinux-webapps/crates/webapps-viewer/src/window.rs:406
353353
msgid "Save File"
354354
msgstr "Salvar Arquivo"
355+
356+
#: /home/talesam/Servidor/GitHub/biglinux-webapps/crates/webapps-manager/src/window.rs:58
357+
msgid "Add"
358+
msgstr "Adicionar"
359+
360+
#: /home/talesam/Servidor/GitHub/biglinux-webapps/crates/webapps-manager/src/webapp_dialog.rs:112
361+
msgid "Detect"
362+
msgstr "Detectar"
Binary file not shown.

crates/webapps-core/src/config.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
use std::path::PathBuf;
22

3-
pub const APP_VERSION: &str = "4.0.0";
3+
pub const APP_VERSION: &str = env!("CARGO_PKG_VERSION");
44
pub const APP_ID: &str = "br.com.biglinux.webapps";
55

66
/// Config dir: ~/.config/biglinux-webapps/

crates/webapps-core/src/desktop.rs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,7 @@ pub fn install_desktop_entry(webapp: &WebApp) -> Result<()> {
123123
let content = generate_desktop_entry(webapp);
124124
fs::write(&path, content)?;
125125
log::info!("Installed desktop entry: {}", path.display());
126+
refresh_desktop_database();
126127
Ok(())
127128
}
128129

@@ -132,10 +133,29 @@ pub fn remove_desktop_entry(webapp: &WebApp) -> Result<()> {
132133
if path.exists() {
133134
fs::remove_file(&path)?;
134135
log::info!("Removed desktop entry: {}", path.display());
136+
refresh_desktop_database();
135137
}
136138
Ok(())
137139
}
138140

141+
/// Remove a desktop file by filename directly
142+
pub fn remove_desktop_file(filename: &str) -> Result<()> {
143+
let path = config::applications_dir().join(filename);
144+
if path.exists() {
145+
fs::remove_file(&path)?;
146+
log::info!("Removed old desktop entry: {}", path.display());
147+
}
148+
Ok(())
149+
}
150+
151+
/// Notify desktop environment of .desktop changes
152+
fn refresh_desktop_database() {
153+
let apps_dir = config::applications_dir();
154+
let _ = std::process::Command::new("update-desktop-database")
155+
.arg(&apps_dir)
156+
.spawn();
157+
}
158+
139159
/// Strip chars that could break desktop file Exec or shell parsing
140160
fn sanitize_desktop_field(s: &str) -> String {
141161
s.chars()

crates/webapps-manager/src/favicon.rs

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,26 @@ pub fn fetch_site_info(url: &str) -> Result<SiteInfo> {
3838
let html_text = resp.text()?;
3939
let doc = Html::parse_document(&html_text);
4040

41-
let title = extract_title(&doc).unwrap_or_default();
41+
let mut title = extract_title(&doc).unwrap_or_default();
42+
43+
// fallback: if title is empty/generic (SPA stub), derive from hostname
44+
let generic_titles = ["ok", "loading", "redirect", "please wait", ""];
45+
if generic_titles
46+
.iter()
47+
.any(|g| title.trim().to_lowercase() == *g)
48+
|| title.len() < 3
49+
{
50+
if let Some(host) = parsed.host_str() {
51+
// strip www. prefix, capitalize first letter
52+
let clean = host.strip_prefix("www.").unwrap_or(host);
53+
let mut chars = clean.chars();
54+
title = match chars.next() {
55+
Some(c) => c.to_uppercase().to_string() + chars.as_str(),
56+
None => clean.to_string(),
57+
};
58+
}
59+
}
60+
4261
let icon_urls = extract_icon_urls(&doc, &url);
4362

4463
// download icons to cache
@@ -67,6 +86,17 @@ pub fn fetch_site_info(url: &str) -> Result<SiteInfo> {
6786
}
6887
}
6988

89+
// last resort: Google favicon service
90+
if icon_paths.is_empty() {
91+
if let Some(host) = parsed.host_str() {
92+
let google_url =
93+
format!("https://www.google.com/s2/favicons?domain={host}&sz=128");
94+
if let Ok(path) = download_icon(&client, &google_url, &cache, 100) {
95+
icon_paths.push(path);
96+
}
97+
}
98+
}
99+
70100
Ok(SiteInfo { title, icon_paths })
71101
}
72102

@@ -135,6 +165,20 @@ fn download_icon(
135165
anyhow::bail!("HTTP {}", resp.status());
136166
}
137167

168+
// reject non-image content types (e.g. HTML redirect pages)
169+
if let Some(ct) = resp.headers().get(reqwest::header::CONTENT_TYPE) {
170+
if let Ok(ct_str) = ct.to_str() {
171+
let ct_lower = ct_str.to_lowercase();
172+
if !ct_lower.starts_with("image/")
173+
&& !ct_lower.contains("svg")
174+
&& !ct_lower.contains("icon")
175+
&& !ct_lower.contains("octet-stream")
176+
{
177+
anyhow::bail!("Not an image: {ct_str}");
178+
}
179+
}
180+
}
181+
138182
// check content-length before downloading
139183
if let Some(cl) = resp.content_length() {
140184
if cl as usize > MAX_ICON_BYTES {

crates/webapps-manager/src/service.rs

Lines changed: 44 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -42,10 +42,18 @@ pub fn save_webapps(collection: &WebAppCollection) -> Result<()> {
4242
}
4343

4444
pub fn create_webapp(webapp: &WebApp) -> Result<()> {
45+
let mut app = webapp.clone();
46+
// ensure app_file is populated → needed for update/remove by file
47+
if app.app_file.is_empty() {
48+
app.app_file = format!(
49+
"biglinux-webapp-{}.desktop",
50+
desktop::desktop_file_id(&app.app_url)
51+
);
52+
}
4553
let mut col = load_webapps();
46-
col.add(webapp.clone());
54+
col.add(app.clone());
4755
save_webapps(&col)?;
48-
desktop::install_desktop_entry(webapp)?;
56+
desktop::install_desktop_entry(&app)?;
4957
Ok(())
5058
}
5159

@@ -122,32 +130,46 @@ pub fn profile_shared(webapp: &WebApp) -> bool {
122130
// -- browser detection --
123131

124132
pub fn detect_browsers() -> BrowserCollection {
125-
let known_browsers = [
126-
("firefox", "/usr/bin/firefox"),
133+
// (browser_id, [candidate_paths]) — first existing path wins
134+
let known_browsers: &[(&str, &[&str])] = &[
135+
("firefox", &["/usr/bin/firefox"]),
127136
(
128137
"firefox-developer-edition",
129-
"/usr/bin/firefox-developer-edition",
138+
&["/usr/bin/firefox-developer-edition"],
139+
),
140+
("librewolf", &["/usr/bin/librewolf"]),
141+
("google-chrome-stable", &["/usr/bin/google-chrome-stable"]),
142+
("google-chrome-beta", &["/usr/bin/google-chrome-beta"]),
143+
("google-chrome-unstable", &["/usr/bin/google-chrome-unstable"]),
144+
("chromium", &["/usr/bin/chromium"]),
145+
(
146+
"brave",
147+
&[
148+
"/usr/bin/brave",
149+
"/usr/bin/brave-browser",
150+
"/usr/bin/brave-browser-stable",
151+
],
152+
),
153+
(
154+
"brave-beta",
155+
&["/usr/bin/brave-browser-beta", "/usr/bin/brave-beta"],
156+
),
157+
(
158+
"brave-nightly",
159+
&["/usr/bin/brave-browser-nightly", "/usr/bin/brave-nightly"],
130160
),
131-
("librewolf", "/usr/bin/librewolf"),
132-
("google-chrome-stable", "/usr/bin/google-chrome-stable"),
133-
("google-chrome-beta", "/usr/bin/google-chrome-beta"),
134-
("google-chrome-unstable", "/usr/bin/google-chrome-unstable"),
135-
("chromium", "/usr/bin/chromium"),
136-
("brave-browser", "/usr/bin/brave-browser-stable"),
137-
("brave-browser-beta", "/usr/bin/brave-browser-beta"),
138-
("brave-browser-nightly", "/usr/bin/brave-browser-nightly"),
139-
("microsoft-edge-stable", "/usr/bin/microsoft-edge-stable"),
140-
("microsoft-edge-beta", "/usr/bin/microsoft-edge-beta"),
141-
("vivaldi-stable", "/usr/bin/vivaldi-stable"),
142-
("vivaldi-beta", "/usr/bin/vivaldi-beta"),
143-
("vivaldi-snapshot", "/usr/bin/vivaldi-snapshot"),
144-
("ungoogled-chromium", "/usr/bin/ungoogled-chromium"),
161+
("microsoft-edge-stable", &["/usr/bin/microsoft-edge-stable"]),
162+
("microsoft-edge-beta", &["/usr/bin/microsoft-edge-beta"]),
163+
("vivaldi-stable", &["/usr/bin/vivaldi-stable"]),
164+
("vivaldi-beta", &["/usr/bin/vivaldi-beta"]),
165+
("vivaldi-snapshot", &["/usr/bin/vivaldi-snapshot"]),
166+
("ungoogled-chromium", &["/usr/bin/ungoogled-chromium"]),
145167
];
146168

147169
let mut browsers: Vec<Browser> = Vec::new();
148170

149-
for (id, path) in &known_browsers {
150-
if Path::new(path).exists() {
171+
for (id, paths) in known_browsers {
172+
if paths.iter().any(|p| Path::new(p).exists()) {
151173
browsers.push(Browser {
152174
browser_id: id.to_string(),
153175
is_default: false,
@@ -215,9 +237,7 @@ fn match_desktop_to_browser(desktop: &str) -> Option<String> {
215237
("google-chrome-beta", "google-chrome-beta"),
216238
("google-chrome-unstable", "google-chrome-unstable"),
217239
("chromium", "chromium"),
218-
("brave-browser-stable", "brave-browser"),
219-
("brave-browser-beta", "brave-browser-beta"),
220-
("brave-browser-nightly", "brave-browser-nightly"),
240+
("brave", "brave"),
221241
("microsoft-edge-stable", "microsoft-edge-stable"),
222242
("microsoft-edge-beta", "microsoft-edge-beta"),
223243
("vivaldi-stable", "vivaldi-stable"),

crates/webapps-manager/src/webapp_dialog.rs

Lines changed: 73 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -61,9 +61,9 @@ pub fn show(
6161
let header = adw::HeaderBar::new();
6262
// placeholder for template button — will be wired after widgets exist
6363
let tmpl_btn = if is_new {
64-
let btn = gtk::Button::from_icon_name("view-grid-symbolic");
65-
btn.set_tooltip_text(Some(&gettext("Templates")));
66-
btn.update_property(&[gtk::accessible::Property::Label(&gettext("Templates"))]);
64+
let btn = gtk::Button::with_label(&gettext("Templates"));
65+
btn.set_tooltip_text(Some(&gettext("Choose from templates")));
66+
btn.add_css_class("suggested-action");
6767
header.pack_start(&btn);
6868
Some(btn)
6969
} else {
@@ -109,16 +109,9 @@ pub fn show(
109109
.title(gettext("URL"))
110110
.text(&webapp_cell.borrow().app_url)
111111
.build();
112-
let detect_img = gtk::Image::from_icon_name("emblem-web-symbolic");
113-
detect_img.set_pixel_size(24);
114-
let detect_btn = gtk::Button::new();
115-
detect_btn.set_child(Some(&detect_img));
112+
let detect_btn = gtk::Button::with_label(&gettext("Detect"));
116113
detect_btn.set_tooltip_text(Some(&gettext("Detect name and icon from website")));
117-
detect_btn.update_property(&[gtk::accessible::Property::Label(&gettext(
118-
"Detect name and icon from website",
119-
))]);
120114
detect_btn.set_valign(gtk::Align::Center);
121-
detect_btn.add_css_class("flat");
122115
url_row.add_suffix(&detect_btn);
123116
group_website.add(&url_row);
124117

@@ -288,11 +281,33 @@ pub fn show(
288281
});
289282
}
290283

291-
// URL changed
284+
// URL changed → update model + auto-detect with debounce
285+
let debounce_handle: Rc<RefCell<Option<glib::SourceId>>> = Rc::new(RefCell::new(None));
292286
{
293287
let wc = webapp_cell.clone();
288+
let db_handle = debounce_handle.clone();
289+
let detect_btn_ref = detect_btn.clone();
294290
url_row.connect_changed(move |row| {
295291
wc.borrow_mut().app_url = row.text().to_string();
292+
// cancel previous debounce
293+
if let Some(id) = db_handle.borrow_mut().take() {
294+
id.remove();
295+
}
296+
// schedule auto-detect after 800ms idle
297+
let btn = detect_btn_ref.clone();
298+
let handle = db_handle.clone();
299+
let text = row.text().to_string();
300+
let source = glib::timeout_add_local_once(
301+
std::time::Duration::from_millis(800),
302+
move || {
303+
handle.borrow_mut().take();
304+
// only trigger if URL looks valid (has a dot)
305+
if text.contains('.') && text.len() > 3 {
306+
btn.emit_clicked();
307+
}
308+
},
309+
);
310+
*db_handle.borrow_mut() = Some(source);
296311
});
297312
}
298313

@@ -320,12 +335,30 @@ pub fn show(
320335
let wc = webapp_cell.clone();
321336
let br = browser_row.clone();
322337
let pr = profile_row.clone();
338+
let brs = browsers.clone();
339+
let brow = browser_row.clone();
323340
mode_switch.connect_state_set(move |_, active| {
324-
wc.borrow_mut().app_mode = if active {
325-
AppMode::App
341+
let mut app = wc.borrow_mut();
342+
if active {
343+
app.app_mode = AppMode::App;
344+
// keep browser field unchanged → restored on switch back
326345
} else {
327-
AppMode::Browser
328-
};
346+
app.app_mode = AppMode::Browser;
347+
// if browser was __viewer__ (legacy data), pick default
348+
if app.browser == "__viewer__" || app.browser.is_empty() {
349+
if let Some(def) = brs.borrow().default_browser() {
350+
app.browser = def.browser_id.clone();
351+
}
352+
}
353+
// update browser row subtitle
354+
let name = brs
355+
.borrow()
356+
.get_by_id(&app.browser)
357+
.map(|b| b.display_name().to_string())
358+
.unwrap_or_else(|| app.browser.clone());
359+
brow.set_subtitle(&name);
360+
}
361+
drop(app);
329362
br.set_visible(!active);
330363
pr.set_visible(!active);
331364
glib::Propagation::Proceed
@@ -518,35 +551,47 @@ pub fn show(
518551
let wc = webapp_cell.clone();
519552
let w = win.clone();
520553
save_btn.connect_clicked(move |_| {
521-
let app = wc.borrow().clone();
554+
let mut app = wc.borrow().clone();
522555

523556
// validate
524557
if app.app_name.trim().is_empty() || app.app_url.trim().is_empty() {
525558
return;
526559
}
527560

528-
// validate URL format
529-
let url_str = app.app_url.trim();
530-
let test_url = if !url_str.starts_with("http://")
561+
// normalize URL: prepend https:// if missing scheme
562+
let url_str = app.app_url.trim().to_string();
563+
if !url_str.starts_with("http://")
531564
&& !url_str.starts_with("https://")
532565
&& !url_str.starts_with("file://")
533566
{
534-
format!("https://{url_str}")
567+
app.app_url = format!("https://{url_str}");
535568
} else {
536-
url_str.to_string()
537-
};
538-
if url::Url::parse(&test_url).is_err() {
569+
app.app_url = url_str;
570+
}
571+
572+
// validate URL format
573+
if url::Url::parse(&app.app_url).is_err() {
539574
return;
540575
}
541576

542-
let ok = if is_new {
543-
service::create_webapp(&app).is_ok()
577+
let result = if is_new {
578+
service::create_webapp(&app)
544579
} else {
545-
service::update_webapp(&app).is_ok()
580+
service::update_webapp(&app)
546581
};
582+
583+
match &result {
584+
Ok(()) => log::info!(
585+
"Saved webapp '{}' mode={:?}",
586+
app.app_name,
587+
app.app_mode
588+
),
589+
Err(e) => log::error!("Save webapp failed: {e}"),
590+
}
591+
547592
w.close();
548593
on_done(DialogResult {
549-
saved: ok,
594+
saved: result.is_ok(),
550595
webapp: app,
551596
});
552597
});

0 commit comments

Comments
 (0)