[gnome] Update extensions

This commit is contained in:
2026-06-10 11:03:09 -04:00
parent 20fc1e4e75
commit 6c973853c6
394 changed files with 12513 additions and 1340 deletions

View File

@ -1,171 +1,272 @@
import Adw from "gi://Adw";
import Gio from "gi://Gio";
import GioUnix from "gi://GioUnix";
import Gtk from "gi://Gtk";
import { ExtensionPreferences } from "resource:///org/gnome/Shell/Extensions/js/extensions/prefs.js";
const LOG_LEVELS = ["debug", "info", "warn", "error"];
function isValidRegex(pattern) {
try {
new RegExp(pattern);
return true;
}
catch {
return false;
}
}
const LOG_LEVELS = [
"debug",
"info",
"warn",
"error",
];
export default class JunkNotificationCleanerPreferences extends ExtensionPreferences {
async fillPreferencesWindow(window) {
const settings = this.getSettings();
model;
getPreferencesWidget() {
this.model = new PreferencesModel(this.getSettings());
const page = new Adw.PreferencesPage();
page.set_title("Settings");
page.set_icon_name("preferences-system-symbolic");
window.add(page);
const generalGroup = new Adw.PreferencesGroup();
generalGroup.set_title("General Settings");
page.add(generalGroup);
const focusRow = new Adw.ActionRow({
page.add(this.buildGeneralGroup());
page.add(this.buildLoggingGroup());
page.add(this.buildExcludedAppsGroup());
return page;
}
buildGeneralGroup() {
const group = new Adw.PreferencesGroup();
group.set_title("General Settings");
group.add(this.buildSwitchRow({
key: "delete-on-focus",
title: "Delete on Focus",
subtitle: "Delete notifications when an application window is focused.",
});
const focusSwitch = new Gtk.Switch({
active: settings.get_boolean("delete-on-focus"),
valign: Gtk.Align.CENTER,
});
settings.bind("delete-on-focus", focusSwitch, "active", Gio.SettingsBindFlags.DEFAULT);
focusRow.add_suffix(focusSwitch);
generalGroup.add(focusRow);
const closeRow = new Adw.ActionRow({
}));
group.add(this.buildSwitchRow({
key: "delete-on-close",
title: "Delete on Close",
subtitle: "Delete notifications when an application window is closed.",
}));
return group;
}
buildSwitchRow(opts) {
const row = new Adw.ActionRow({
title: opts.title,
subtitle: opts.subtitle,
});
const closeSwitch = new Gtk.Switch({
active: settings.get_boolean("delete-on-close"),
valign: Gtk.Align.CENTER,
});
settings.bind("delete-on-close", closeSwitch, "active", Gio.SettingsBindFlags.DEFAULT);
closeRow.add_suffix(closeSwitch);
generalGroup.add(closeRow);
const debugGroup = new Adw.PreferencesGroup();
debugGroup.set_title("Logging");
page.add(debugGroup);
const logLevelRow = new Adw.ActionRow({
const toggle = new Gtk.Switch({ valign: Gtk.Align.CENTER });
this.model.bindDeleteTrigger(opts.key, toggle, "active");
row.add_suffix(toggle);
return row;
}
buildLoggingGroup() {
const group = new Adw.PreferencesGroup();
group.set_title("Logging");
const row = new Adw.ActionRow({
title: "Log Level",
subtitle: "Set the logging level for troubleshooting notification matching.",
});
const logLevelDropdown = new Gtk.DropDown({
model: Gtk.StringList.new(LOG_LEVELS),
const dropdown = new Gtk.DropDown({
model: Gtk.StringList.new([...LOG_LEVELS]),
valign: Gtk.Align.CENTER,
});
let currentLogLevel = settings.get_string("log-level") || "info";
const currentIndex = LOG_LEVELS.indexOf(currentLogLevel);
if (currentIndex !== -1) {
logLevelDropdown.set_selected(currentIndex);
}
logLevelDropdown.connect("notify::selected", () => {
const selectedIndex = logLevelDropdown.get_selected();
settings.set_string("log-level", LOG_LEVELS[selectedIndex]);
this.bindLogLevelDropdown(dropdown);
row.add_suffix(dropdown);
group.add(row);
return group;
}
bindLogLevelDropdown(dropdown) {
const sync = () => {
dropdown.set_selected(this.model.getLogLevelIndex());
};
sync();
const changedId = this.model.onLogLevelChanged(sync);
dropdown.connect("notify::selected", (dd) => {
this.model.setLogLevelIndex(dd.get_selected());
});
logLevelRow.add_suffix(logLevelDropdown);
debugGroup.add(logLevelRow);
const excludedGroup = new Adw.PreferencesGroup();
excludedGroup.set_title("Excluded WM Classes");
excludedGroup.set_description([
"Window Manager Classes whose notifications will not be automatically deleted.",
"Will be matched against the wm_class property of the window, supports ECMAScript regular expressions.",
].join("\n"));
page.add(excludedGroup);
const excludedBox = new Gtk.Box({
orientation: Gtk.Orientation.VERTICAL,
margin_top: 8,
margin_bottom: 8,
margin_start: 8,
margin_end: 8,
spacing: 8,
dropdown.connect("destroy", () => {
this.model.disconnect(changedId);
});
const excludedApps = settings.get_strv("excluded-apps");
}
buildExcludedAppsGroup() {
const group = new Adw.PreferencesGroup();
group.set_title("Excluded Applications");
group.set_description("Applications whose notifications will not be automatically deleted.");
const listBox = this.buildExcludedAppsList();
const addButton = new Gtk.Button({
icon_name: "list-add-symbolic",
tooltip_text: "Add application",
css_classes: ["flat"],
valign: Gtk.Align.CENTER,
});
addButton.connect("clicked", (btn) => {
const parent = btn.get_root();
this.openAppSelector(parent, (appId) => {
this.model.addExcludedApp(appId);
});
});
group.set_header_suffix(addButton);
group.add(listBox);
return group;
}
buildExcludedAppsList() {
const listBox = new Gtk.ListBox({
selection_mode: Gtk.SelectionMode.NONE,
css_classes: ["boxed-list"],
});
excludedBox.append(listBox);
for (const app of excludedApps) {
this.addExcludedAppRow(app, listBox, settings);
}
const addBox = new Gtk.Box({
orientation: Gtk.Orientation.HORIZONTAL,
spacing: 8,
margin_top: 8,
listBox.set_placeholder(new Gtk.Label({
label: "No excluded applications.",
css_classes: ["dim-label"],
margin_top: 12,
margin_bottom: 12,
}));
this.rebuildExcludedAppsList(listBox);
const handlerId = this.model.onExcludedAppsChanged(() => {
this.rebuildExcludedAppsList(listBox);
});
const entry = new Gtk.Entry({
placeholder_text: "Enter WM Class regex (e.g. .*firefox.*)",
hexpand: true,
listBox.connect("destroy", () => {
this.model.disconnect(handlerId);
});
const errorLabel = new Gtk.Label({
label: "Invalid regular expression",
css_classes: ["error"],
xalign: 0,
visible: false,
});
const addButton = new Gtk.Button({
label: "Add",
css_classes: ["suggested-action"],
});
entry.connect("changed", () => {
entry.remove_css_class("error");
errorLabel.set_visible(false);
});
addButton.connect("clicked", () => {
const text = entry.get_text().trim();
if (!text)
return;
if (!isValidRegex(text)) {
entry.add_css_class("error");
errorLabel.set_visible(true);
}
else {
entry.remove_css_class("error");
errorLabel.set_visible(false);
const currentApps = settings.get_strv("excluded-apps");
if (currentApps.includes(text))
return;
settings.set_strv("excluded-apps", [...currentApps, text]);
this.addExcludedAppRow(text, listBox, settings);
entry.set_text("");
}
});
addBox.append(entry);
addBox.append(addButton);
excludedBox.append(addBox);
excludedBox.append(errorLabel);
excludedGroup.add(excludedBox);
return listBox;
}
addExcludedAppRow(app, listBox, settings) {
const row = new Gtk.ListBoxRow();
const box = new Gtk.Box({
orientation: Gtk.Orientation.HORIZONTAL,
spacing: 8,
margin_top: 8,
margin_bottom: 8,
margin_start: 8,
margin_end: 8,
});
const label = new Gtk.Label({
label: app,
hexpand: true,
xalign: 0,
});
rebuildExcludedAppsList(listBox) {
listBox.remove_all();
for (const appId of this.model.getExcludedApps()) {
listBox.append(this.buildExcludedAppRow(appId));
}
}
buildExcludedAppRow(appId) {
const app = tryLoadDesktopApp(appId);
const row = buildAppRow(app, appId);
if (!app)
row.set_subtitle("Uninstalled");
const removeButton = new Gtk.Button({
icon_name: "user-trash-symbolic",
tooltip_text: "Remove",
css_classes: ["flat"],
valign: Gtk.Align.CENTER,
});
removeButton.connect("clicked", () => {
const currentApps = settings.get_strv("excluded-apps");
const newApps = currentApps.filter((a) => a !== app);
settings.set_strv("excluded-apps", newApps);
listBox.remove(row);
this.model.removeExcludedApp(appId);
});
box.append(label);
box.append(removeButton);
row.set_child(box);
listBox.append(row);
row.add_suffix(removeButton);
return row;
}
openAppSelector(parent, onSelected) {
const window = new Adw.Window({
title: "Add Excluded Application",
modal: true,
default_width: 420,
default_height: 520,
});
if (parent)
window.set_transient_for(parent);
const search = new Gtk.SearchEntry({
placeholder_text: "Search applications",
hexpand: true,
});
const headerBar = new Adw.HeaderBar();
headerBar.set_title_widget(search);
const appList = buildSelectableAppList(this.getSelectableApps(), (appId) => {
onSelected(appId);
window.close();
});
wireSearchFilter(appList, search);
addCloseOnEscape(window);
const toolbarView = new Adw.ToolbarView();
toolbarView.add_top_bar(headerBar);
toolbarView.set_content(new Gtk.ScrolledWindow({ child: appList }));
window.set_content(toolbarView);
window.present();
search.grab_focus();
}
getSelectableApps() {
const excluded = new Set(this.model.getExcludedApps());
return Gio.AppInfo.get_all()
.filter((a) => a.should_show() && a.get_id() && !excluded.has(getAppId(a)))
.sort((a, b) => a.get_name().localeCompare(b.get_name()));
}
}
function buildSelectableAppList(apps, onActivated) {
const appList = new Gtk.ListBox({
selection_mode: Gtk.SelectionMode.NONE,
css_classes: ["boxed-list"],
margin_top: 8,
margin_bottom: 8,
margin_start: 8,
margin_end: 8,
});
for (const app of apps) {
const appId = getAppId(app);
const row = buildAppRow(app, appId, { activatable: true });
appList.append(row);
row.connect("activated", () => {
onActivated(appId);
});
}
return appList;
}
// DesktopAppInfo.new is typed as non-null but returns null when the
// .desktop file is missing (e.g. the user uninstalled the app).
function tryLoadDesktopApp(appId) {
return GioUnix.DesktopAppInfo.new(`${appId}.desktop`);
}
function buildAppRow(app, appId, extra = {}) {
const title = app?.get_name() ?? appId;
const row = new Adw.ActionRow({ ...extra, title, subtitle: appId });
const icon = app?.get_icon();
if (icon) {
const image = Gtk.Image.new_from_gicon(icon);
image.set_pixel_size(32);
row.add_prefix(image);
}
return row;
}
function wireSearchFilter(appList, search) {
appList.set_filter_func((row) => {
const query = search.get_text().toLowerCase().trim();
if (query === "")
return true;
const actionRow = row;
const title = actionRow.get_title().toLowerCase();
const subtitle = actionRow.get_subtitle()?.toLowerCase() ?? "";
return title.includes(query) || subtitle.includes(query);
});
search.connect("search-changed", () => {
appList.invalidate_filter();
});
}
function addCloseOnEscape(window) {
const controller = new Gtk.ShortcutController();
controller.add_shortcut(new Gtk.Shortcut({
trigger: Gtk.ShortcutTrigger.parse_string("Escape"),
action: Gtk.ShortcutAction.parse_string("action(window.close)"),
}));
window.add_controller(controller);
}
function getAppId(app) {
return (app.get_id() ?? "").replace(/\.desktop$/, "");
}
class PreferencesModel {
settings;
constructor(settings) {
this.settings = settings;
}
bindDeleteTrigger(key, target, property) {
this.settings.bind(key, target, property, Gio.SettingsBindFlags.DEFAULT);
}
getExcludedApps() {
return this.settings.get_strv("excluded-apps");
}
addExcludedApp(appId) {
const current = this.getExcludedApps();
if (current.includes(appId))
return;
this.settings.set_strv("excluded-apps", [...current, appId]);
}
removeExcludedApp(appId) {
this.settings.set_strv("excluded-apps", this.getExcludedApps().filter((id) => id !== appId));
}
onExcludedAppsChanged(handler) {
return this.settings.connect("changed::excluded-apps", handler);
}
getLogLevelIndex() {
return this.settings.get_enum("log-level");
}
setLogLevelIndex(idx) {
this.settings.set_enum("log-level", idx);
}
onLogLevelChanged(handler) {
return this.settings.connect("changed::log-level", handler);
}
disconnect(handlerId) {
this.settings.disconnect(handlerId);
}
}