*
* Copyright 2026 Alexander Vanhee
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* SPDX-License-Identifier: GPL-3.0-or-later
*/
[GtkTemplate (ui = "/io/github/kolunmi/Bazaar/bz-updates-card.ui")]
public class Bz.UpdatesCard : Adw.Bin {
private Bz.StateInfo? _state;
public Bz.StateInfo? state {
get { return _state; }
set {
_state = value;
if (_state != null)
_state.notify["available-updates"].connect ((s, p) => repopulate ());
repopulate ();
}
}
[GtkChild]
private unowned Adw.ExpanderRow expander_row;
private GenericArray<Gtk.Widget> app_rows = new GenericArray<Gtk.Widget> ();
private Gtk.Widget? runtimes_row = null;
private GenericArray<Bz.Entry> runtime_entries = new GenericArray<Bz.Entry> ();
private bool has_closed = false;
public signal void update (ListModel entries);
[GtkCallback]
private string format_update_count (ListModel? updates) {
if (updates == null)
return "";
uint n_updates = updates.get_n_items ();
return ngettext ("%u Available Update", "%u Available Updates", n_updates).printf (n_updates);
}
[GtkCallback]
private void update_all_cb (Gtk.Button button) {
var updates = _state?.available_updates;
if (updates == null || updates.get_n_items () == 0)
return;
var entries = new GenericArray<Bz.Entry> ();
uint n_items = updates.get_n_items ();
for (uint i = 0; i < n_items; i++) {
var entry = (updates.get_item (i) as Bz.UpdatePermissionInfo)?.entry;
if (entry != null)
entries.add (entry);
}
emit_update (entries);
}
[GtkCallback]
private void on_expander_expanded_cb (Object row, ParamSpec pspec) {
if (!expander_row.expanded)
has_closed = true;
}
private void emit_update (GenericArray<Bz.Entry> entries) {
var store = new ListStore (typeof (Bz.Entry));
foreach (var entry in entries)
store.append (entry);
update (store);
}
private void repopulate () {
foreach (var row in app_rows)
expander_row.remove (row);
if (runtimes_row != null)
expander_row.remove (runtimes_row);
app_rows = new GenericArray<Gtk.Widget> ();
runtime_entries = new GenericArray<Bz.Entry> ();
runtimes_row = null;
var updates = _state?.available_updates;
if (updates == null)
return;
uint n_items = updates.get_n_items ();
for (uint i = 0; i < n_items; i++) {
var info = updates.get_item (i) as Bz.UpdatePermissionInfo;
var entry = info?.entry;
if (entry == null)
continue;
if (entry.is_of_kinds (Bz.EntryKind.APPLICATION)) {
var row = build_app_row (info);
expander_row.add_row (row);
app_rows.add (row);
} else if (entry.is_of_kinds (Bz.EntryKind.RUNTIME) ||
entry.is_of_kinds (Bz.EntryKind.ADDON)) {
runtime_entries.add (entry);
}
}
runtimes_row = build_runtimes_row ();
expander_row.add_row (runtimes_row);
if (!has_closed)
expander_row.expanded = n_items <= 3;
}
private Gtk.Widget build_app_row (Bz.UpdatePermissionInfo info) {
var entry = info.entry;
var delta = info.additional_permissions;
var row = new Adw.ActionRow () {
title = entry.title,
};
var icon = new Gtk.Image () {
pixel_size = 48,
valign = Gtk.Align.CENTER,
margin_top = 6,
margin_bottom = 6,
};
if (entry.icon_paintable != null)
icon.paintable = entry.icon_paintable;
else
icon.icon_name = "application-x-executable";
icon.add_css_class ("icon-dropshadow");
row.add_prefix (icon);
ListModel? history = entry.version_history;
string? installed_ver = entry.installed_version;
bool has_history = history != null && history.get_n_items () > 0;
if (has_history && installed_ver != null) {
string? new_ver = ((Bz.Release) history.get_item (0)).version;
if (new_ver != null)
row.subtitle = installed_ver != new_ver
? "%s → %s".printf (installed_ver, new_ver)
: new_ver;
}
if (delta != null && !delta.is_empty ()) {
var permissions_icon = new Gtk.Image.from_icon_name ("permissions-warning-symbolic") {
valign = Gtk.Align.CENTER,
tooltip_text = _("This update requests new permissions"),
};
permissions_icon.add_css_class ("warning");
row.add_suffix (permissions_icon);
}
var history_button = new Gtk.Button.from_icon_name ("view-list-bullet-symbolic") {
valign = Gtk.Align.CENTER,
tooltip_text = _("Version History"),
visible = has_history,
};
history_button.add_css_class ("flat");
history_button.clicked.connect (() => {
var dialog = Bz.ReleasesList.dialog_new (entry.version_history, null);
((Adw.Dialog) dialog).present (this.get_root ());
});
row.add_suffix (history_button);
var update_button = new Gtk.Button.with_label (_("Update")) {
valign = Gtk.Align.CENTER,
};
update_button.clicked.connect (() => {
var entries = new GenericArray<Bz.Entry> ();
entries.add (info.entry);
emit_update (entries);
});
row.add_suffix (update_button);
return row;
}
private Gtk.Widget build_runtimes_row () {
uint n_items = runtime_entries.length;
var row = new Adw.ActionRow () {
visible = n_items > 0,
};
if (n_items > 0)
row.title = ngettext ("%u Runtime Update", "%u Runtime Updates", n_items).printf (n_items);
var update_button = new Gtk.Button.with_label (_("Update")) {
valign = Gtk.Align.CENTER,
};
update_button.clicked.connect (() => {
if (runtime_entries.length == 0)
return;
emit_update (runtime_entries);
});
row.add_suffix (update_button);
return row;
}
}