From 924ac2dc6619d6310dd272ed73aaccef73d73ad4 Mon Sep 17 00:00:00 2001 From: Ignacio Butler Date: Fri, 3 Apr 2026 17:17:49 +0200 Subject: [PATCH 1/2] Documented recent changes in settings dialog, bumped version to 1.1.0, and fixed broken tests. --- .gitignore | 1 + CHANGELOG.md | 11 +++++++++ README.md | 2 +- __mocks__/GObject.js | 10 +++++++++ jest.config.js | 1 + metadata.json | 3 ++- package.json | 2 +- prefs.js | 36 +++++++++++++++++++++++------- tests/unit/test_launcherService.js | 1 + 9 files changed, 56 insertions(+), 11 deletions(-) create mode 100644 CHANGELOG.md create mode 100644 __mocks__/GObject.js diff --git a/.gitignore b/.gitignore index 3c3629e..918863b 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ node_modules +*.zip diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..58cfbbf --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,11 @@ +# Changelog + +## [1.1.0] - 2026-04-03 + +### Added +- Added support for GNOME Shell 49. +- Added a directory picker in the settings dialog for easier folder selection. + +### Changed +- Replaced the manual text entry for the monitored directory with a folder selection button in the extension preferences. +- Made the `prefs.js` UI more user-friendly by using `Gtk.FileDialog`. diff --git a/README.md b/README.md index 2c31193..ce2d3e5 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,7 @@ This GNOME Shell extension automatically integrates AppImage applications into t ## Configuration -Access the extension preferences through GNOME Tweaks to change the monitored directory. +Access the extension preferences through GNOME Extensions or Settings to change the monitored directory. You can now use a directory picker to select the folder instead of manually typing the path. ## Development diff --git a/__mocks__/GObject.js b/__mocks__/GObject.js new file mode 100644 index 0000000..1950183 --- /dev/null +++ b/__mocks__/GObject.js @@ -0,0 +1,10 @@ +'use strict'; + +const GObject = { + Object: class { + _init() {} + }, + registerClass: jest.fn((options, cls) => cls), +}; + +export default GObject; diff --git a/jest.config.js b/jest.config.js index d922f95..56f3cdf 100644 --- a/jest.config.js +++ b/jest.config.js @@ -7,5 +7,6 @@ module.exports = { moduleNameMapper: { '^gi://Gio$': '/__mocks__/Gio.js', '^gi://GLib$': '/__mocks__/GLib.js', + '^gi://GObject$': '/__mocks__/GObject.js', }, }; diff --git a/metadata.json b/metadata.json index 1226b3c..4aba08c 100644 --- a/metadata.json +++ b/metadata.json @@ -7,7 +7,8 @@ "45", "46", "47", - "48" + "48", + "49" ], "url": "https://github.com/ignaci0/appimage-manager", "settings-schema": "org.gnome.shell.extensions.appimage-manager", diff --git a/package.json b/package.json index f3d4128..0ade3e8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "appimg-manager", - "version": "1.0.1", + "version": "1.1.0", "description": "AppImage Manager Gnome Extension", "main": "extension.js", "scripts": { diff --git a/prefs.js b/prefs.js index 02356d0..73c5519 100644 --- a/prefs.js +++ b/prefs.js @@ -1,5 +1,6 @@ import Adw from 'gi://Adw'; import Gtk from 'gi://Gtk'; +import Gio from 'gi://Gio'; import {ExtensionPreferences} from 'resource:///org/gnome/Shell/Extensions/js/extensions/prefs.js'; @@ -18,18 +19,37 @@ export default class AppImageManagerPreferences extends ExtensionPreferences { }); group.add(monitoredDirectoryRow); - const monitoredDirectoryEntry = new Gtk.Entry({ - text: settings.get_string('monitored-directory'), - hexpand: true, + const monitoredDirectoryButton = new Gtk.Button({ + label: 'Select Monitored Directory', }); - monitoredDirectoryRow.add_suffix(monitoredDirectoryEntry); - monitoredDirectoryRow.activatable_widget = monitoredDirectoryEntry; - - monitoredDirectoryEntry.connect('changed', () => { - settings.set_string('monitored-directory', monitoredDirectoryEntry.get_text()); + const currentMonitoredDirectory = settings.get_string('monitored-directory'); + if (currentMonitoredDirectory) { + monitoredDirectoryButton.set_label(currentMonitoredDirectory); + } + + monitoredDirectoryButton.connect('clicked', () => { + const dialog = new Gtk.FileDialog({ + title: 'Select Monitored Directory', + }); + + dialog.select_folder(window, null, (dialog, res) => { + try { + const folder = dialog.select_folder_finish(res); + if (folder) { + const path = folder.get_path(); + settings.set_string('monitored-directory', path); + monitoredDirectoryButton.set_label(path); + } + } catch (e) { + logError(e, 'Failed to select folder'); + } + }); }); + monitoredDirectoryRow.add_suffix(monitoredDirectoryButton); + monitoredDirectoryRow.activatable_widget = monitoredDirectoryButton; + window.add(page); } } \ No newline at end of file diff --git a/tests/unit/test_launcherService.js b/tests/unit/test_launcherService.js index 4ce984e..05723e0 100644 --- a/tests/unit/test_launcherService.js +++ b/tests/unit/test_launcherService.js @@ -25,6 +25,7 @@ describe('LauncherService', () => { const mockFile = { query_exists: jest.fn(() => true), delete: jest.fn(), + get_path: jest.fn(() => '/home/user/.local/share/applications'), }; Gio.File.new_for_path.mockReturnValue(mockFile); From 70a765afe2e4447d03cbbb2ccf63347ded97af84 Mon Sep 17 00:00:00 2001 From: Pappmann Date: Fri, 1 May 2026 18:28:49 +0200 Subject: [PATCH 2/2] Fix enable when monitored directory is missing --- extension.js | 38 +++++++++++++++++++++++-------------- src/appImageManager.js | 43 +++++++++++++++++++++++++++++++++--------- src/fileMonitor.js | 7 ++++++- 3 files changed, 64 insertions(+), 24 deletions(-) diff --git a/extension.js b/extension.js index d7fa1f5..1a62e7c 100644 --- a/extension.js +++ b/extension.js @@ -23,37 +23,47 @@ export default class AppImageManagerExtension extends Extension { let monitoredDirectory = this._settingsManager.getMonitoredDirectory(); - await this._appImageManager.rescan(monitoredDirectory); + try { + await this._appImageManager.rescan(monitoredDirectory); + } catch (e) { + logError(`Rescan failed: ${e?.message ?? e}`); + } - this._fileMonitor.startMonitoring( + const started = this._fileMonitor.startMonitoring( monitoredDirectory, (filePath) => { - this._appImageManager.addAppImage(filePath); + this._appImageManager.addAppImage(filePath).catch(err => logError(err)); }, (filePath) => { - this._appImageManager.removeAppImage(filePath); + this._appImageManager.removeAppImage(filePath).catch(err => logError(err)); } ); + if (!started) + logError(`Failed to monitor directory: ${monitoredDirectory}`); } disable() { log(`Disabling ${this.metadata.name} extension`); - this._fileMonitor.stopMonitoring(); + this._fileMonitor?.stopMonitoring(); let monitoredDirectory = this._settingsManager.getMonitoredDirectory(); let dir = Gio.File.new_for_path(monitoredDirectory); if (dir.query_exists(null)) { - let enumerator = dir.enumerate_children('standard::name,standard::type', Gio.FileQueryInfoFlags.NONE, null); - let fileInfo; - while ((fileInfo = enumerator.next_file(null)) !== null) { - let child = dir.get_child(fileInfo.get_name()); - if (fileInfo.get_file_type() === Gio.FileType.REGULAR && this._appImageManager.isAppImage(child.get_path())) { - let fileName = GLib.path_get_basename(child.get_path()); - let appImageName = fileName.replace(/\.AppImage$/, ''); - this._launcherService.deleteLauncher(appImageName); + try { + let enumerator = dir.enumerate_children('standard::name,standard::type', Gio.FileQueryInfoFlags.NONE, null); + let fileInfo; + while ((fileInfo = enumerator.next_file(null)) !== null) { + let child = dir.get_child(fileInfo.get_name()); + if (fileInfo.get_file_type() === Gio.FileType.REGULAR && this._appImageManager.isAppImage(child.get_path())) { + let fileName = GLib.path_get_basename(child.get_path()); + let appImageName = fileName.replace(/\.AppImage$/, ''); + this._launcherService.deleteLauncher(appImageName); + } } + enumerator.close(null); + } catch (e) { + logError(`Failed to cleanup launchers: ${e?.message ?? e}`); } - enumerator.close(null); } this._launcherService = null; diff --git a/src/appImageManager.js b/src/appImageManager.js index 8683605..cac1caf 100644 --- a/src/appImageManager.js +++ b/src/appImageManager.js @@ -330,16 +330,41 @@ export class AppImageManager { async rescan(directory) { const directoryFile = Gio.File.new_for_path(directory); - const enumerator = directoryFile.enumerate_children('standard::name', Gio.FileQueryInfoFlags.NONE, null); - let fileInfo; + if (!directoryFile.query_exists(null)) { + try { + directoryFile.make_directory_with_parents(null); + log(`Created monitored directory: ${directory}`); + } catch (e) { + logError(`Failed to create monitored directory ${directory}: ${e.message}`); + return; + } + } + + let enumerator; + try { + enumerator = directoryFile.enumerate_children('standard::name', Gio.FileQueryInfoFlags.NONE, null); + } catch (e) { + logError(`Failed to enumerate directory ${directory}: ${e.message}`); + return; + } + const filesInDirectory = new Set(); - while ((fileInfo = enumerator.next_file(null)) !== null) { - const fileName = fileInfo.get_name(); - const filePath = GLib.build_pathv('/', [directory, fileName]); - filesInDirectory.add(filePath); - if (this.isAppImage(filePath)) { - await this.addAppImage(filePath); + try { + let fileInfo; + while ((fileInfo = enumerator.next_file(null)) !== null) { + const fileName = fileInfo.get_name(); + const filePath = GLib.build_pathv('/', [directory, fileName]); + filesInDirectory.add(filePath); + if (this.isAppImage(filePath)) { + await this.addAppImage(filePath); + } + } + } finally { + try { + enumerator.close(null); + } catch { + // ignore } } @@ -350,4 +375,4 @@ export class AppImageManager { } } } -} \ No newline at end of file +} diff --git a/src/fileMonitor.js b/src/fileMonitor.js index b97855b..0bfc505 100644 --- a/src/fileMonitor.js +++ b/src/fileMonitor.js @@ -28,7 +28,12 @@ export class FileMonitor { this._onFileAdded = onFileAdded; this._onFileRemoved = onFileRemoved; - this._monitor = this._directory.monitor_directory(Gio.FileMonitorFlags.NONE, null); + try { + this._monitor = this._directory.monitor_directory(Gio.FileMonitorFlags.NONE, null); + } catch (e) { + logError(`Failed to monitor directory ${directoryPath}: ${e.message}`); + return false; + } this._monitor.connect('changed', (monitor, file, otherFile, eventType) => { log(`File monitor event: ${eventType}`); switch (eventType) {