Add dedicated Updates view with check-now and update-all
This commit is contained in:
@@ -13,4 +13,5 @@ pub mod permission_dialog;
|
||||
pub mod preferences;
|
||||
pub mod security_report;
|
||||
pub mod update_dialog;
|
||||
pub mod updates_view;
|
||||
pub mod widgets;
|
||||
|
||||
325
src/ui/updates_view.rs
Normal file
325
src/ui/updates_view.rs
Normal file
@@ -0,0 +1,325 @@
|
||||
use adw::prelude::*;
|
||||
use std::path::Path;
|
||||
use std::rc::Rc;
|
||||
|
||||
use gtk::gio;
|
||||
|
||||
use crate::config::APP_ID;
|
||||
use crate::core::database::Database;
|
||||
use crate::core::updater;
|
||||
use crate::i18n::i18n;
|
||||
use crate::ui::update_dialog;
|
||||
use crate::ui::widgets;
|
||||
|
||||
/// Build the Updates top-level view.
|
||||
/// Returns a ToolbarView that can be added to the ViewStack.
|
||||
pub fn build_updates_view(db: &Rc<Database>) -> adw::ToolbarView {
|
||||
let toast_overlay = adw::ToastOverlay::new();
|
||||
|
||||
let header = adw::HeaderBar::new();
|
||||
let title = adw::WindowTitle::builder()
|
||||
.title(&i18n("Updates"))
|
||||
.build();
|
||||
header.set_title_widget(Some(&title));
|
||||
|
||||
// Check Now button
|
||||
let check_btn = gtk::Button::builder()
|
||||
.icon_name("view-refresh-symbolic")
|
||||
.tooltip_text(&i18n("Check for updates"))
|
||||
.build();
|
||||
header.pack_end(&check_btn);
|
||||
|
||||
// Update All button (only visible when updates exist)
|
||||
let update_all_btn = gtk::Button::builder()
|
||||
.label(&i18n("Update All"))
|
||||
.css_classes(["suggested-action", "pill"])
|
||||
.visible(false)
|
||||
.build();
|
||||
header.pack_start(&update_all_btn);
|
||||
|
||||
// Content stack: empty state vs checking vs update list
|
||||
let stack = gtk::Stack::new();
|
||||
|
||||
// Empty state - all up to date
|
||||
let empty_page = adw::StatusPage::builder()
|
||||
.icon_name("emblem-ok-symbolic")
|
||||
.title(&i18n("Everything is Up to Date"))
|
||||
.description(&i18n("All your AppImages are running the latest version"))
|
||||
.build();
|
||||
stack.add_named(&empty_page, Some("empty"));
|
||||
|
||||
// Checking state
|
||||
let checking_page = adw::StatusPage::builder()
|
||||
.icon_name("emblem-synchronizing-symbolic")
|
||||
.title(&i18n("Checking for Updates"))
|
||||
.description(&i18n("Looking for new versions of your AppImages..."))
|
||||
.build();
|
||||
let spinner = gtk::Spinner::builder()
|
||||
.spinning(true)
|
||||
.width_request(32)
|
||||
.height_request(32)
|
||||
.halign(gtk::Align::Center)
|
||||
.build();
|
||||
checking_page.set_child(Some(&spinner));
|
||||
stack.add_named(&checking_page, Some("checking"));
|
||||
|
||||
// Update list
|
||||
let list_box = gtk::ListBox::builder()
|
||||
.selection_mode(gtk::SelectionMode::None)
|
||||
.css_classes(["boxed-list"])
|
||||
.margin_start(18)
|
||||
.margin_end(18)
|
||||
.margin_top(12)
|
||||
.margin_bottom(24)
|
||||
.build();
|
||||
|
||||
let last_checked_label = gtk::Label::builder()
|
||||
.css_classes(["dim-label", "caption"])
|
||||
.halign(gtk::Align::Start)
|
||||
.margin_start(18)
|
||||
.margin_top(6)
|
||||
.build();
|
||||
|
||||
let updates_content = gtk::Box::builder()
|
||||
.orientation(gtk::Orientation::Vertical)
|
||||
.spacing(6)
|
||||
.build();
|
||||
updates_content.append(&last_checked_label);
|
||||
|
||||
let clamp = adw::Clamp::builder()
|
||||
.maximum_size(800)
|
||||
.tightening_threshold(600)
|
||||
.child(&list_box)
|
||||
.build();
|
||||
updates_content.append(&clamp);
|
||||
|
||||
let scrolled = gtk::ScrolledWindow::builder()
|
||||
.child(&updates_content)
|
||||
.vexpand(true)
|
||||
.build();
|
||||
stack.add_named(&scrolled, Some("updates"));
|
||||
|
||||
toast_overlay.set_child(Some(&stack));
|
||||
|
||||
// Shared state for refresh
|
||||
let state = Rc::new(UpdatesState {
|
||||
db: db.clone(),
|
||||
list_box: list_box.clone(),
|
||||
stack: stack.clone(),
|
||||
update_all_btn: update_all_btn.clone(),
|
||||
title: title.clone(),
|
||||
last_checked_label: last_checked_label.clone(),
|
||||
toast_overlay: toast_overlay.clone(),
|
||||
});
|
||||
|
||||
// Initial population
|
||||
populate_update_list(&state);
|
||||
|
||||
// Check Now handler
|
||||
{
|
||||
let state_ref = state.clone();
|
||||
check_btn.connect_clicked(move |btn| {
|
||||
btn.set_sensitive(false);
|
||||
state_ref.stack.set_visible_child_name("checking");
|
||||
|
||||
let state_c = state_ref.clone();
|
||||
let btn_c = btn.clone();
|
||||
|
||||
glib::spawn_future_local(async move {
|
||||
let db_bg = Database::open().ok();
|
||||
let _result = gio::spawn_blocking(move || {
|
||||
if let Some(ref db) = db_bg {
|
||||
update_dialog::batch_check_updates(db);
|
||||
}
|
||||
}).await;
|
||||
|
||||
btn_c.set_sensitive(true);
|
||||
|
||||
// Update last-checked timestamp
|
||||
let settings = gio::Settings::new(APP_ID);
|
||||
let now = chrono::Local::now().format("%Y-%m-%d %H:%M:%S").to_string();
|
||||
settings.set_string("last-update-check", &now).ok();
|
||||
|
||||
// Reload from main DB
|
||||
let fresh_db = state_c.db.clone();
|
||||
// Re-read records from the shared db so UI picks up changes from the bg thread
|
||||
drop(fresh_db);
|
||||
populate_update_list(&state_c);
|
||||
state_c.toast_overlay.add_toast(adw::Toast::new(&i18n("Update check complete")));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Update All handler
|
||||
{
|
||||
let state_ref = state.clone();
|
||||
update_all_btn.connect_clicked(move |btn| {
|
||||
btn.set_sensitive(false);
|
||||
let state_c = state_ref.clone();
|
||||
let btn_c = btn.clone();
|
||||
|
||||
glib::spawn_future_local(async move {
|
||||
let db_bg = Database::open().ok();
|
||||
let result = gio::spawn_blocking(move || {
|
||||
let mut updated = 0u32;
|
||||
if let Some(ref db) = db_bg {
|
||||
let updatable = db.get_appimages_with_updates().unwrap_or_default();
|
||||
for rec in &updatable {
|
||||
let path = Path::new(&rec.path);
|
||||
if path.exists() {
|
||||
if updater::perform_update(
|
||||
path,
|
||||
rec.update_url.as_deref(),
|
||||
true,
|
||||
None,
|
||||
).is_ok() {
|
||||
db.clear_update_available(rec.id).ok();
|
||||
updated += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
updated
|
||||
}).await;
|
||||
|
||||
btn_c.set_sensitive(true);
|
||||
let count = result.unwrap_or(0);
|
||||
if count > 0 {
|
||||
state_c.toast_overlay.add_toast(
|
||||
adw::Toast::new(&format!("{} apps updated", count)),
|
||||
);
|
||||
}
|
||||
populate_update_list(&state_c);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
let toolbar_view = adw::ToolbarView::new();
|
||||
toolbar_view.add_top_bar(&header);
|
||||
toolbar_view.set_content(Some(&toast_overlay));
|
||||
|
||||
toolbar_view
|
||||
}
|
||||
|
||||
struct UpdatesState {
|
||||
db: Rc<Database>,
|
||||
list_box: gtk::ListBox,
|
||||
stack: gtk::Stack,
|
||||
update_all_btn: gtk::Button,
|
||||
title: adw::WindowTitle,
|
||||
last_checked_label: gtk::Label,
|
||||
toast_overlay: adw::ToastOverlay,
|
||||
}
|
||||
|
||||
fn populate_update_list(state: &Rc<UpdatesState>) {
|
||||
// Clear existing rows
|
||||
while let Some(row) = state.list_box.row_at_index(0) {
|
||||
state.list_box.remove(&row);
|
||||
}
|
||||
|
||||
// Re-read from database to pick up changes from background threads
|
||||
let db_fresh = Database::open().ok();
|
||||
let updatable = db_fresh
|
||||
.as_ref()
|
||||
.and_then(|db| db.get_appimages_with_updates().ok())
|
||||
.unwrap_or_default();
|
||||
|
||||
// Update last checked label
|
||||
let settings = gio::Settings::new(APP_ID);
|
||||
let last_check = settings.string("last-update-check");
|
||||
if last_check.is_empty() || last_check.as_str() == "never" {
|
||||
state.last_checked_label.set_label(&i18n("Never checked"));
|
||||
} else {
|
||||
state.last_checked_label.set_label(
|
||||
&format!("Last checked: {}", widgets::relative_time(&last_check)),
|
||||
);
|
||||
}
|
||||
|
||||
if updatable.is_empty() {
|
||||
state.stack.set_visible_child_name("empty");
|
||||
state.update_all_btn.set_visible(false);
|
||||
state.title.set_subtitle(&i18n("All up to date"));
|
||||
return;
|
||||
}
|
||||
|
||||
state.stack.set_visible_child_name("updates");
|
||||
state.update_all_btn.set_visible(true);
|
||||
state.title.set_subtitle(&format!("{} updates available", updatable.len()));
|
||||
|
||||
for record in &updatable {
|
||||
let name = record.app_name.as_deref().unwrap_or(&record.filename);
|
||||
|
||||
let row = adw::ActionRow::builder()
|
||||
.title(name)
|
||||
.activatable(false)
|
||||
.build();
|
||||
|
||||
// Show version info: current -> latest
|
||||
let current = record.app_version.as_deref().unwrap_or("unknown");
|
||||
let latest = record.latest_version.as_deref().unwrap_or("unknown");
|
||||
row.set_subtitle(&format!("{} -> {}", current, latest));
|
||||
|
||||
// App icon
|
||||
let icon = widgets::app_icon(record.icon_path.as_deref(), name, 32);
|
||||
row.add_prefix(&icon);
|
||||
|
||||
// Individual update button
|
||||
let update_btn = gtk::Button::builder()
|
||||
.icon_name("software-update-available-symbolic")
|
||||
.valign(gtk::Align::Center)
|
||||
.tooltip_text(&i18n("Update this app"))
|
||||
.css_classes(["flat"])
|
||||
.build();
|
||||
|
||||
let app_id = record.id;
|
||||
let update_url = record.update_url.clone();
|
||||
let app_path = record.path.clone();
|
||||
let state_ref = state.clone();
|
||||
update_btn.connect_clicked(move |btn| {
|
||||
btn.set_sensitive(false);
|
||||
let state_c = state_ref.clone();
|
||||
let path = app_path.clone();
|
||||
let url = update_url.clone();
|
||||
let btn_c = btn.clone();
|
||||
let aid = app_id;
|
||||
|
||||
glib::spawn_future_local(async move {
|
||||
let db_bg = Database::open().ok();
|
||||
let result = gio::spawn_blocking(move || {
|
||||
let p = Path::new(&path);
|
||||
if p.exists() {
|
||||
let ok = updater::perform_update(
|
||||
p,
|
||||
url.as_deref(),
|
||||
true,
|
||||
None,
|
||||
).is_ok();
|
||||
if ok {
|
||||
if let Some(ref db) = db_bg {
|
||||
db.clear_update_available(aid).ok();
|
||||
}
|
||||
}
|
||||
ok
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}).await;
|
||||
|
||||
btn_c.set_sensitive(true);
|
||||
if result.unwrap_or(false) {
|
||||
state_c.toast_overlay.add_toast(
|
||||
adw::Toast::new(&i18n("Update complete")),
|
||||
);
|
||||
} else {
|
||||
state_c.toast_overlay.add_toast(
|
||||
adw::Toast::new(&i18n("Update failed")),
|
||||
);
|
||||
}
|
||||
populate_update_list(&state_c);
|
||||
});
|
||||
});
|
||||
|
||||
row.add_suffix(&update_btn);
|
||||
state.list_box.append(&row);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user