summaryrefslogtreecommitdiff
path: root/raveos-gnome-theme/theme-data/extensions/installed/dash-to-dock@micxgx.gmail.com/locations.js
diff options
context:
space:
mode:
authorAlexanderCurl <alexc@alexc.hu>2026-04-18 17:46:06 +0100
committerAlexanderCurl <alexc@alexc.hu>2026-04-18 17:46:06 +0100
commit70ca3fef77ee8bdc6e3ac28589a6fa08c024cc69 (patch)
treeab1123d4067c1b086dd6faa7ee4ea643236b565a /raveos-gnome-theme/theme-data/extensions/installed/dash-to-dock@micxgx.gmail.com/locations.js
parent5d94c0a7d44a2255b81815a52a7056a94a39842d (diff)
downloadRaveOS-PKGBUILD-70ca3fef77ee8bdc6e3ac28589a6fa08c024cc69.tar.gz
RaveOS-PKGBUILD-70ca3fef77ee8bdc6e3ac28589a6fa08c024cc69.zip
Replaced file structures for themes in the correct format
Diffstat (limited to 'raveos-gnome-theme/theme-data/extensions/installed/dash-to-dock@micxgx.gmail.com/locations.js')
-rw-r--r--raveos-gnome-theme/theme-data/extensions/installed/dash-to-dock@micxgx.gmail.com/locations.js1535
1 files changed, 1535 insertions, 0 deletions
diff --git a/raveos-gnome-theme/theme-data/extensions/installed/dash-to-dock@micxgx.gmail.com/locations.js b/raveos-gnome-theme/theme-data/extensions/installed/dash-to-dock@micxgx.gmail.com/locations.js
new file mode 100644
index 0000000..534466c
--- /dev/null
+++ b/raveos-gnome-theme/theme-data/extensions/installed/dash-to-dock@micxgx.gmail.com/locations.js
@@ -0,0 +1,1535 @@
+// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
+
+import {
+ Gio,
+ GioUnix,
+ GLib,
+ GObject,
+ Shell,
+ St,
+} from './dependencies/gi.js';
+
+import {ShellMountOperation} from './dependencies/shell/ui.js';
+
+import {
+ Docking,
+ Utils,
+} from './imports.js';
+
+import {Extension} from './dependencies/shell/extensions/extension.js';
+
+// Use __ () and N__() for the extension gettext domain, and reuse
+// the shell domain with the default _() and N_()
+const {gettext: __} = Extension;
+
+const {signals: Signals} = imports;
+
+const FALLBACK_REMOVABLE_MEDIA_ICON = 'drive-removable-media';
+const FALLBACK_TRASH_ICON = 'user-trash';
+const FILE_MANAGER_DESKTOP_APP_ID = 'org.gnome.Nautilus.desktop';
+const ATTRIBUTE_METADATA_CUSTOM_ICON = 'metadata::custom-icon';
+const TRASH_URI = 'trash://';
+const UPDATE_TRASH_DELAY = 1000;
+const LAUNCH_HANDLER_MAX_WAIT = 200;
+
+const NautilusFileOperations2Interface = '<node>\
+ <interface name="org.gnome.Nautilus.FileOperations2">\
+ <method name="EmptyTrash">\
+ <arg type="b" name="ask_confirmation" direction="in"/>\
+ <arg type="a{sv}" name="platform_data" direction="in"/>\
+ </method>\
+ </interface>\
+</node>';
+
+const NautilusFileOperations2ProxyInterface =
+ Gio.DBusProxy.makeProxyWrapper(NautilusFileOperations2Interface);
+
+const Labels = Object.freeze({
+ LOCATION_WINDOWS: Symbol('location-windows'),
+ WINDOWS_CHANGED: Symbol('windows-changed'),
+});
+
+const GJS_SUPPORTS_FILE_IFACE_PROMISES = imports.system.version >= 17101;
+
+if (GJS_SUPPORTS_FILE_IFACE_PROMISES) {
+ Gio._promisify(Gio.File.prototype, 'query_info_async');
+ Gio._promisify(Gio.File.prototype, 'query_default_handler_async');
+}
+
+
+/**
+ *
+ */
+function makeNautilusFileOperationsProxy() {
+ const proxy = new NautilusFileOperations2ProxyInterface(
+ Gio.DBus.session,
+ 'org.gnome.Nautilus',
+ '/org/gnome/Nautilus/FileOperations2', (_p, error) => {
+ if (error)
+ logError(error, 'Error connecting to Nautilus');
+ }
+ );
+
+ proxy.platformData = params => {
+ const defaultParams = {
+ parentHandle: '',
+ timestamp: global.get_current_time(),
+ windowPosition: 'center',
+ };
+ const {parentHandle, timestamp, windowPosition} = {
+ ...defaultParams,
+ ...params,
+ };
+
+ return {
+ 'parent-handle': new GLib.Variant('s', parentHandle),
+ 'timestamp': new GLib.Variant('u', timestamp),
+ 'window-position': new GLib.Variant('s', windowPosition),
+ };
+ };
+
+ return proxy;
+}
+
+export const LocationAppInfo = GObject.registerClass({
+ Implements: [Gio.AppInfo],
+ Properties: {
+ 'location': GObject.ParamSpec.object(
+ 'location', 'location', 'location',
+ GObject.ParamFlags.READWRITE,
+ Gio.File.$gtype),
+ 'name': GObject.ParamSpec.string(
+ 'name', 'name', 'name',
+ GObject.ParamFlags.READWRITE,
+ null),
+ 'icon': GObject.ParamSpec.object(
+ 'icon', 'icon', 'icon',
+ GObject.ParamFlags.READWRITE,
+ Gio.Icon.$gtype),
+ 'cancellable': GObject.ParamSpec.object(
+ 'cancellable', 'cancellable', 'cancellable',
+ GObject.ParamFlags.READWRITE,
+ Gio.Cancellable.$gtype),
+ },
+}, class LocationAppInfo extends GioUnix.DesktopAppInfo {
+ static get GJS_BINARY_PATH() {
+ if (!this._gjsBinaryPath)
+ this._gjsBinaryPath = GLib.find_program_in_path('gjs');
+
+ return this._gjsBinaryPath;
+ }
+
+ list_actions() {
+ return [];
+ }
+
+ get_action_name() {
+ return null;
+ }
+
+ get_boolean() {
+ return false;
+ }
+
+ vfunc_dup() {
+ return new LocationAppInfo({
+ location: this.location,
+ name: this.name,
+ icon: this.icon,
+ cancellable: this.cancellable,
+ });
+ }
+
+ vfunc_equal(other) {
+ if (this.location)
+ return this.location.equal(other?.location);
+
+ return this.name === other.name &&
+ (this.icon ? this.icon.equal(other?.icon) : !other?.icon);
+ }
+
+ vfunc_get_id() {
+ return 'location:%s'.format(this.location?.get_uri());
+ }
+
+ vfunc_get_name() {
+ return this.name;
+ }
+
+ vfunc_get_description() {
+ return null;
+ }
+
+ vfunc_get_executable() {
+ return null;
+ }
+
+ vfunc_get_icon() {
+ return this.icon;
+ }
+
+ vfunc_launch(files, context) {
+ if (files?.length) {
+ throw new GLib.Error(Gio.IOErrorEnum,
+ Gio.IOErrorEnum.NOT_SUPPORTED, 'Launching with files not supported');
+ }
+
+ return this.getHandlerApp().launch_uris([this.location.get_uri()], context);
+ }
+
+ vfunc_supports_uris() {
+ return false;
+ }
+
+ vfunc_supports_files() {
+ return false;
+ }
+
+ vfunc_launch_uris(uris, context) {
+ return this.launch(uris, context);
+ }
+
+ vfunc_should_show() {
+ return true;
+ }
+
+ vfunc_set_as_default_for_type() {
+ throw new GLib.Error(Gio.IOErrorEnum,
+ Gio.IOErrorEnum.NOT_SUPPORTED, 'Not supported');
+ }
+
+ vfunc_set_as_default_for_extension() {
+ throw new GLib.Error(Gio.IOErrorEnum,
+ Gio.IOErrorEnum.NOT_SUPPORTED, 'Not supported');
+ }
+
+ vfunc_add_supports_type() {
+ throw new GLib.Error(Gio.IOErrorEnum,
+ Gio.IOErrorEnum.NOT_SUPPORTED, 'Not supported');
+ }
+
+ vfunc_can_remove_supports_type() {
+ return false;
+ }
+
+ vfunc_remove_supports_type() {
+ return false;
+ }
+
+ vfunc_can_delete() {
+ return false;
+ }
+
+ vfunc_do_delete() {
+ return false;
+ }
+
+ vfunc_get_commandline() {
+ try {
+ return this.getHandlerApp().get_commandline();
+ } catch {
+ return this._getFallbackCommandLine();
+ }
+ }
+
+ vfunc_get_display_name() {
+ return this.name;
+ }
+
+ vfunc_set_as_last_used_for_type() {
+ throw new GLib.Error(Gio.IOErrorEnum,
+ Gio.IOErrorEnum.NOT_SUPPORTED, 'Not supported');
+ }
+
+ vfunc_get_supported_types() {
+ return [];
+ }
+
+ _getFallbackCommandLine() {
+ return `gio open ${this.location?.get_uri()}`;
+ }
+
+ async _queryLocationIcons(params) {
+ const icons = {standard: null, custom: null};
+ if (!this.location)
+ return icons;
+
+ const cancellable = params.cancellable ?? this.cancellable;
+ const iconsQuery = [];
+ if (params?.standard)
+ iconsQuery.push(Gio.FILE_ATTRIBUTE_STANDARD_ICON);
+
+ if (params?.custom)
+ iconsQuery.push(ATTRIBUTE_METADATA_CUSTOM_ICON);
+
+ if (!iconsQuery.length)
+ throw new Error('Invalid Query Location Icons parameters');
+
+ let info;
+ try {
+ if (!GJS_SUPPORTS_FILE_IFACE_PROMISES) {
+ Gio._promisify(this.location.constructor.prototype,
+ 'query_info_async', 'query_info_finish');
+ }
+ info = await this.location.query_info_async(
+ iconsQuery.join(','),
+ Gio.FileQueryInfoFlags.NONE,
+ GLib.PRIORITY_LOW, cancellable);
+ if (info.has_attribute(Gio.FILE_ATTRIBUTE_STANDARD_ICON))
+ icons.standard = info.get_icon();
+ } catch (e) {
+ if (e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.NOT_FOUND) ||
+ e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.NOT_MOUNTED))
+ return icons;
+ throw e;
+ }
+
+ const customIcon = info.get_attribute_string(ATTRIBUTE_METADATA_CUSTOM_ICON);
+ if (customIcon) {
+ const customIconFile = GLib.uri_parse_scheme(customIcon)
+ ? Gio.File.new_for_uri(customIcon) : Gio.File.new_for_path(customIcon);
+ const iconFileInfo = await customIconFile.query_info_async(
+ Gio.FILE_ATTRIBUTE_STANDARD_TYPE,
+ Gio.FileQueryInfoFlags.NONE,
+ GLib.PRIORITY_LOW, cancellable);
+
+ if (iconFileInfo.get_file_type() === Gio.FileType.REGULAR)
+ icons.custom = Gio.FileIcon.new(customIconFile);
+ }
+
+ return icons;
+ }
+
+ async _updateLocationIcon(params = {standard: true, custom: true}) {
+ const cancellable = new Utils.CancellableChild(this.cancellable);
+
+ try {
+ this._updateIconCancellable?.cancel();
+ this._updateIconCancellable = cancellable;
+
+ const icons = await this._queryLocationIcons({cancellable, ...params});
+ const icon = icons.custom ?? icons.standard;
+
+ if (icon && !icon.equal(this.icon))
+ this.icon = icon;
+ } catch (e) {
+ if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED))
+ logError(e, 'Impossible to update icon for %s'.format(this.get_id()));
+ } finally {
+ cancellable.cancel();
+ if (this._updateIconCancellable === cancellable)
+ delete this._updateIconCancellable;
+ }
+ }
+
+ async _getHandlerAppAsync(cancellable) {
+ if (!this.location)
+ return null;
+
+ try {
+ if (!GJS_SUPPORTS_FILE_IFACE_PROMISES) {
+ Gio._promisify(this.location.constructor.prototype,
+ 'query_default_handler_async',
+ 'query_default_handler_finish');
+ }
+
+ return await this.location.query_default_handler_async(
+ GLib.PRIORITY_DEFAULT, cancellable);
+ } catch (e) {
+ if (e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.NOT_MOUNTED))
+ return getFileManagerApp()?.appInfo;
+
+ if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED)) {
+ logError(e, 'Impossible to find an URI handler for %s'.format(
+ this.get_id()));
+ }
+
+ throw e;
+ }
+ }
+
+ _getHandlerAppFromWorker(cancellable) {
+ const locationsWorker = GLib.build_filenamev([
+ Docking.DockManager.extension.path,
+ 'locationsWorker.js',
+ ]);
+ const locationsWorkerArgs = [LocationAppInfo.GJS_BINARY_PATH, '-m',
+ locationsWorker, 'handler', this.location.get_uri(),
+ '--timeout', `${LAUNCH_HANDLER_MAX_WAIT}`];
+ const subProcess = Gio.Subprocess.new(locationsWorkerArgs,
+ Gio.SubprocessFlags.STDOUT_PIPE | Gio.SubprocessFlags.STDERR_PIPE);
+
+ try {
+ const [, stdOut, stdErr] = subProcess.communicate(null, cancellable);
+ subProcess.wait(cancellable);
+ const errorCode = subProcess.get_exit_status();
+ const textDecoder = new TextDecoder();
+
+ if (errorCode) {
+ const errorLines = textDecoder.decode(stdErr.toArray()).split('\n');
+ const error = new GLib.Error(Gio.IOErrorEnum,
+ errorCode === GLib.MAXUINT8 ? 0 : errorCode, errorLines[0]);
+ error.stack = `${errorLines.slice(3).join('\n')}${error.stack}`;
+ throw error;
+ }
+
+ const desktopId = textDecoder.decode(stdOut.toArray()).trim();
+ const handlerApp = Shell.AppSystem.get_default().lookup_app(desktopId)?.appInfo;
+ return handlerApp;
+ } finally {
+ subProcess.force_exit();
+ }
+ }
+
+ getHandlerApp() {
+ if (this._handlerApp)
+ return this._handlerApp;
+
+ if (!this.location)
+ return null;
+
+ const cancellable = new Utils.CancellableChild(this.cancellable);
+
+ try {
+ if (LocationAppInfo.GJS_BINARY_PATH)
+ this._handlerApp = this._getHandlerAppFromWorker(cancellable);
+ else
+ this._handlerApp = this.location.query_default_handler(cancellable);
+
+ if (!this._handlerApp) {
+ throw new GLib.Error(Gio.IOErrorEnum,
+ Gio.IOErrorEnum.NOT_FOUND, `Handler for ${this.location} not found`);
+ }
+ } catch (e) {
+ if (e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.NOT_MOUNTED))
+ return getFileManagerApp()?.appInfo;
+
+ if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED)) {
+ logError(e, 'Impossible to find an URI handler for %s'.format(
+ this.get_id()));
+ }
+
+ throw e;
+ }
+
+ return this._handlerApp;
+ }
+
+ destroy() {
+ this.location = null;
+ this.icon = null;
+ this.name = null;
+ this._handlerApp = null;
+ this.cancellable?.cancel();
+ }
+});
+
+const RemovableAction = Object.freeze({
+ MOUNT: 'mount',
+ UNMOUNT: 'unmount',
+ EJECT: 'eject',
+});
+
+const MountableVolumeAppInfo = GObject.registerClass({
+ Implements: [Gio.AppInfo],
+ Properties: {
+ 'volume': GObject.ParamSpec.object(
+ 'volume', 'volume', 'volume',
+ GObject.ParamFlags.READWRITE | GObject.ParamFlags.CONSTRUCT_ONLY,
+ Gio.Volume.$gtype),
+ 'mount': GObject.ParamSpec.object(
+ 'mount', 'mount', 'mount',
+ GObject.ParamFlags.READWRITE,
+ Gio.Mount.$gtype),
+ 'busy': GObject.ParamSpec.boolean(
+ 'busy', 'busy', 'busy',
+ GObject.ParamFlags.READWRITE,
+ false),
+ },
+},
+class MountableVolumeAppInfo extends LocationAppInfo {
+ _init(volume, cancellable = null) {
+ super._init({
+ volume,
+ cancellable,
+ });
+
+ this._signalsHandler = new Utils.GlobalSignalsHandler();
+
+ const updateAndMonitor = () => {
+ this._update();
+ this._monitorChanges();
+ };
+ updateAndMonitor();
+ this._mountChanged = this.connect('notify::mount', updateAndMonitor);
+
+ if (!this.mount && this.volume.get_identifier('class') === 'network') {
+ // For some devices the mount point isn't advertised promptly
+ // even if it's already existing, and there's no signaling about
+ this._lazyUpdater = GLib.timeout_add_seconds(GLib.PRIORITY_DEFAULT, 2, () => {
+ this._update();
+ delete this._lazyUpdater;
+ return GLib.SOURCE_REMOVE;
+ });
+ }
+ }
+
+ get busy() {
+ return !!this._currentAction;
+ }
+
+ get currentAction() {
+ return this._currentAction;
+ }
+
+ destroy() {
+ if (this._lazyUpdater) {
+ GLib.source_remove(this._lazyUpdater);
+ delete this._lazyUpdater;
+ }
+ this.disconnect(this._mountChanged);
+ this.mount = null;
+ this._signalsHandler.destroy();
+
+ super.destroy();
+ }
+
+ vfunc_dup() {
+ return new MountableVolumeAppInfo({
+ volume: this.volume,
+ cancellable: this.cancellable,
+ });
+ }
+
+ vfunc_get_id() {
+ const uuid = this.mount?.get_uuid() ?? this.volume.get_uuid();
+ return uuid ? 'mountable-volume:%s'.format(uuid) : super.vfunc_get_id();
+ }
+
+ vfunc_equal(other) {
+ if (this.volume === other?.volume && this.mount === other?.mount)
+ return true;
+
+ return this.get_id() === other?.get_id();
+ }
+
+ list_actions() {
+ const actions = [];
+ const {mount} = this;
+
+ if (mount) {
+ if (this.mount.can_unmount())
+ actions.push(RemovableAction.UNMOUNT);
+ if (this.mount.can_eject())
+ actions.push(RemovableAction.EJECT);
+
+ return actions;
+ }
+
+ if (this.volume.can_mount())
+ actions.push(RemovableAction.MOUNT);
+ if (this.volume.can_eject())
+ actions.push(RemovableAction.EJECT);
+
+ return actions;
+ }
+
+ get_action_name(action) {
+ switch (action) {
+ case RemovableAction.MOUNT:
+ return __('Mount');
+ case RemovableAction.UNMOUNT:
+ return __('Unmount');
+ case RemovableAction.EJECT:
+ return __('Eject');
+ default:
+ return null;
+ }
+ }
+
+ vfunc_launch(files, context) {
+ if (this.mount || files?.length)
+ return super.vfunc_launch(files, context);
+
+ this.mountAndLaunch(files, context);
+ return true;
+ }
+
+ _update() {
+ this.mount = this.volume.get_mount();
+
+ const removable = this.mount ?? this.volume;
+ this.name = removable.get_name();
+ this.icon = removable.get_icon();
+
+ this.location = this.mount?.get_default_location() ??
+ this.volume.get_activation_root();
+
+ this._updateLocationIcon({custom: true});
+ }
+
+ _monitorChanges() {
+ this._signalsHandler.destroy();
+
+ const removable = this.mount ?? this.volume;
+ this._signalsHandler.add(removable, 'changed', () => this._update());
+
+ if (this.mount) {
+ this._signalsHandler.add(this.mount, 'pre-unmount', () => this._update());
+ this._signalsHandler.add(this.mount, 'unmounted', () => this._update());
+ }
+ }
+
+ async mountAndLaunch(files, context) {
+ if (this.mount)
+ return super.vfunc_launch(files, context);
+
+ try {
+ await this.launchAction(RemovableAction.MOUNT);
+ if (!this.mount) {
+ throw new Error('No mounted location to open for %s'.format(
+ this.get_id()));
+ }
+
+ return super.vfunc_launch(files, context);
+ } catch (e) {
+ logError(e, 'Mount and launch %s'.format(this.get_id()));
+ return false;
+ }
+ }
+
+ _notifyActionError(action, message) {
+ switch (action) {
+ case RemovableAction.MOUNT:
+ global.notify_error(__('Failed to mount “%s”'.format(
+ this.get_name())), message);
+ break;
+
+ case RemovableAction.UNMOUNT:
+ global.notify_error(__('Failed to unmount “%s”'.format(
+ this.get_name())), message);
+ break;
+
+ case RemovableAction.EJECT:
+ global.notify_error(__('Failed to eject “%s”'.format(
+ this.get_name())), message);
+ break;
+ }
+ }
+
+ async launchAction(action) {
+ if (!this.list_actions().includes(action))
+ throw new Error('Action %s is not supported by %s', action, this);
+
+ switch (this._currentAction) {
+ case RemovableAction.MOUNT:
+ this._notifyActionError(action,
+ __('Mount operation already in progress'));
+ break;
+
+ case RemovableAction.UNMOUNT:
+ this._notifyActionError(action,
+ __('Unmount operation already in progress'));
+ break;
+
+ case RemovableAction.EJECT:
+ this._notifyActionError(action,
+ __('Eject operation already in progress'));
+ break;
+
+ default:
+ if (this._currentAction) {
+ throw new Error('Another action %s is being performed in %s'.format(
+ this._currentAction, this));
+ }
+ }
+
+ this._currentAction = action;
+ this.notify('busy');
+ const removable = this.mount ?? this.volume;
+ const operation = new ShellMountOperation.ShellMountOperation(removable);
+ try {
+ switch (action) {
+ case RemovableAction.MOUNT:
+ await this.volume.mount(Gio.MountMountFlags.NONE, operation.mountOp,
+ this.cancellable);
+ return true;
+
+ case RemovableAction.UNMOUNT:
+ await this.mount.unmount_with_operation(Gio.MountUnmountFlags.FORCE,
+ operation.mountOp, this.cancellable);
+ return true;
+
+ case RemovableAction.EJECT:
+ await removable.eject_with_operation(Gio.MountUnmountFlags.FORCE,
+ operation.mountOp, this.cancellable);
+ return true;
+
+ default:
+ logError(new Error(), 'No action %s on removable %s'.format(action,
+ removable.get_name()));
+ return false;
+ }
+ } catch (e) {
+ if (action === RemovableAction.MOUNT &&
+ e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.ALREADY_MOUNTED))
+ return true;
+ else if (action === RemovableAction.UNMOUNT &&
+ e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.NOT_MOUNTED))
+ return true;
+
+ if (e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.FAILED))
+ this._notifyActionError(action, e.message);
+
+ if (action === RemovableAction.MOUNT && this._isEncryptedMountError(e)) {
+ delete this._currentAction;
+ operation.close();
+ return this.launchAction(action);
+ }
+
+ if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED)) {
+ logError(e, 'Impossible to %s removable %s'.format(action,
+ removable.get_name()));
+ }
+
+ return false;
+ } finally {
+ delete this._currentAction;
+ this.notify('busy');
+ this._update();
+ operation.close();
+ }
+ }
+
+ _isEncryptedMountError(error) {
+ // FIXME: we will always get G_IO_ERROR_FAILED from the gvfs udisks
+ // backend, see https://bugs.freedesktop.org/show_bug.cgi?id=51271
+
+ if (!error.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.FAILED))
+ return false;
+
+ // cryptsetup
+ if (error.message.includes('No key available with this passphrase'))
+ return true;
+
+ // udisks (no password)
+ if (error.message.includes('No key available to unlock device'))
+ return true;
+
+ // libblockdev wrong password opening LUKS device
+ if (error.message.includes('Failed to activate device: Incorrect passphrase'))
+ return true;
+
+ // cryptsetup returns EINVAL in many cases, including wrong TCRYPT password/parameters
+ if (error.message.includes('Failed to load device\'s parameters: Invalid argument') ||
+ error.message.includes(`Failed to load device's parameters: ${GLib.strerror(22 /* EINVAL */)}`))
+ return true;
+
+ // cryptsetup returns EPERM when the TCRYPT header can't be decrypted
+ // with the provided password/parameters.
+ if (error.message.includes('Failed to load device\'s parameters: Operation not permitted') ||
+ error.message.includes(`Failed to load device's parameters: ${GLib.strerror(1 /* EPERM */)}`))
+ return true;
+
+ return false;
+ }
+});
+
+const TrashAppInfo = GObject.registerClass({
+ Implements: [Gio.AppInfo],
+ Properties: {
+ 'empty': GObject.ParamSpec.boolean(
+ 'empty', 'empty', 'empty',
+ GObject.ParamFlags.READWRITE,
+ true),
+ },
+},
+class TrashAppInfo extends LocationAppInfo {
+ static initPromises(file) {
+ if (TrashAppInfo._promisified)
+ return;
+
+ const trashProto = file.constructor.prototype;
+ Gio._promisify(Gio.FileEnumerator.prototype, 'close_async', 'close_finish');
+ Gio._promisify(Gio.FileEnumerator.prototype, 'next_files_async', 'next_files_finish');
+ Gio._promisify(trashProto, 'enumerate_children_async', 'enumerate_children_finish');
+ Gio._promisify(trashProto, 'query_info_async', 'query_info_finish');
+ TrashAppInfo._promisified = true;
+ }
+
+ _init(cancellable = null) {
+ super._init({
+ location: Gio.file_new_for_uri(TRASH_URI),
+ name: __('Trash'),
+ icon: Gio.ThemedIcon.new(FALLBACK_TRASH_ICON),
+ cancellable,
+ });
+ TrashAppInfo.initPromises(this.location);
+
+ try {
+ this._monitor = this.location.monitor_directory(0, this.cancellable);
+ this._schedUpdateId = 0;
+ this._monitorChangedId = this._monitor.connect('changed', () =>
+ this._onTrashChange());
+ } catch (e) {
+ if (e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED))
+ return;
+ logError(e, 'Impossible to monitor trash');
+ }
+ this._updateTrash();
+
+ this.connect('notify::empty', () => this._updateLocationIcon());
+ this.notify('empty');
+ }
+
+ destroy() {
+ if (this._schedUpdateId) {
+ GLib.source_remove(this._schedUpdateId);
+ this._schedUpdateId = 0;
+ }
+ this._updateTrashCancellable?.cancel();
+ this._monitor?.disconnect(this._monitorChangedId);
+ this._monitor = null;
+
+ super.destroy();
+ }
+
+ list_actions() {
+ return this.empty ? [] : ['empty-trash'];
+ }
+
+ get_action_name(action) {
+ switch (action) {
+ case 'empty-trash':
+ return __('Empty Trash');
+ default:
+ return null;
+ }
+ }
+
+ _onTrashChange() {
+ if (this._schedUpdateId) {
+ GLib.source_remove(this._schedUpdateId);
+ this._schedUpdateId = 0;
+ }
+
+ if (this._monitor.is_cancelled())
+ return;
+
+ this._schedUpdateId = GLib.timeout_add(GLib.PRIORITY_LOW,
+ UPDATE_TRASH_DELAY, () => {
+ this._schedUpdateId = 0;
+ this._updateTrash();
+ return GLib.SOURCE_REMOVE;
+ });
+ }
+
+ async _updateTrash() {
+ const priority = GLib.PRIORITY_LOW;
+ this._updateTrashCancellable?.cancel();
+ const cancellable = new Utils.CancellableChild(this.cancellable);
+ this._updateTrashCancellable = cancellable;
+
+ try {
+ const trashInfo = await this.location.query_info_async(
+ Gio.FILE_ATTRIBUTE_TRASH_ITEM_COUNT,
+ Gio.FileQueryInfoFlags.NONE,
+ priority, cancellable);
+ this.empty = !trashInfo.get_attribute_uint32(
+ Gio.FILE_ATTRIBUTE_TRASH_ITEM_COUNT);
+ return;
+ } catch (e) {
+ if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED))
+ logError(e, 'Impossible to get trash children from infos');
+ } finally {
+ cancellable.cancel();
+ if (this._updateIconCancellable === cancellable)
+ delete this._updateTrashCancellable;
+ }
+
+ try {
+ const childrenEnumerator = await this.location.enumerate_children_async(
+ Gio.FILE_ATTRIBUTE_STANDARD_TYPE, Gio.FileQueryInfoFlags.NONE,
+ priority, cancellable);
+ const children = await childrenEnumerator.next_files_async(1,
+ priority, cancellable);
+ this.empty = !children.length;
+
+ await childrenEnumerator.close_async(priority, null);
+ } catch (e) {
+ if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED))
+ logError(e, 'Impossible to enumerate trash children');
+ } finally {
+ cancellable.cancel();
+ if (this._updateIconCancellable === cancellable)
+ delete this._updateTrashCancellable;
+ }
+ }
+
+ launchAction(action, timestamp) {
+ if (!this.list_actions().includes(action))
+ throw new Error('Action %s is not supported by %s', action, this);
+
+ const nautilus = makeNautilusFileOperationsProxy();
+ const askConfirmation = true;
+ nautilus.EmptyTrashRemote(askConfirmation,
+ nautilus.platformData({timestamp}), (_p, error) => {
+ if (error)
+ logError(error, 'Empty trash failed');
+ }, this.cancellable);
+ }
+});
+
+/**
+ * @param shellApp
+ */
+function wrapWindowsBackedApp(shellApp) {
+ if (shellApp._dtdData)
+ throw new Error('%s has been already wrapped'.format(shellApp));
+
+ shellApp._dtdData = {
+ windows: [],
+ state: undefined,
+ startingWorkspace: 0,
+ isFocused: false,
+ proxyProperties: [],
+ sources: new Set(),
+ signalConnections: new Utils.GlobalSignalsHandler(),
+ methodInjections: new Utils.InjectionsHandler(),
+ propertyInjections: new Utils.PropertyInjectionsHandler(),
+ addProxyProperties(parent, proxyProperties) {
+ Object.entries(proxyProperties).forEach(([p, o]) => {
+ const publicProp = o.public ? p : `_${p}`;
+ const get = o.getter && o.value instanceof Function
+ ? () => this[p]() : () => this[p];
+ Object.defineProperty(parent, publicProp, Object.assign({
+ get,
+ set: v => (this[p] = v),
+ configurable: true,
+ enumerable: !!o.enumerable,
+ }, o.readOnly ? {set: undefined} : {}));
+ if (o.value)
+ this[p] = o.value;
+ this.proxyProperties.push(publicProp);
+ });
+ },
+ destroy() {
+ this.windows = [];
+ this.proxyProperties = [];
+ this.sources.forEach(s => GLib.source_remove(s));
+ this.sources.clear();
+ this.signalConnections.destroy();
+ this.methodInjections.destroy();
+ this.propertyInjections.destroy();
+ },
+ };
+
+ shellApp._dtdData.addProxyProperties(shellApp, {
+ windows: {},
+ state: {},
+ startingWorkspace: {},
+ isFocused: {public: true},
+ signalConnections: {readOnly: true},
+ sources: {readOnly: true},
+ checkFocused: {},
+ setDtdData: {},
+ });
+
+ shellApp._setDtdData = function (data, params = {}) {
+ for (const [name, value] of Object.entries(data)) {
+ if (params.readOnly && name in this._dtdData)
+ throw new Error('Property %s is already defined'.format(name));
+ const defaultParams = {public: true, readOnly: true};
+ this._dtdData.addProxyProperties(this, {
+ [name]: {...defaultParams, ...params, value},
+ });
+ }
+ };
+
+ const m = (...args) => shellApp._dtdData.methodInjections.add(shellApp, ...args);
+ const p = (...args) => shellApp._dtdData.propertyInjections.add(shellApp, ...args);
+
+ // mi is Method injector, pi is Property injector
+ shellApp._setDtdData({mi: m, pi: p}, {public: false});
+
+ m('get_state', () => shellApp._state ?? shellApp._getStateByWindows());
+ p('state', {get: () => shellApp.get_state()});
+
+ m('get_windows', () => shellApp._windows);
+ m('get_n_windows', () => shellApp._windows.length);
+ m('get_pids', () => shellApp._windows.reduce((pids, w) => {
+ if (w.get_pid() > 0 && !pids.includes(w.get_pid()))
+ pids.push(w.get_pid());
+ return pids;
+ }, []));
+ m('is_on_workspace', (_om, workspace) => shellApp._windows.some(w =>
+ w.get_workspace() === workspace) ||
+ (shellApp.state === Shell.AppState.STARTING &&
+ [-1, workspace.index()].includes(shellApp._startingWorkspace)));
+ m('request_quit', () => shellApp._windows.filter(w =>
+ w.can_close()).forEach(w => w.delete(global.get_current_time())));
+
+ shellApp._setDtdData({
+ _getStateByWindows() {
+ return this.get_n_windows() ? Shell.AppState.RUNNING : Shell.AppState.STOPPED;
+ },
+
+ _updateWindows() {
+ throw new GObject.NotImplementedError(`_updateWindows in ${this.constructor.name}`);
+ },
+
+ _notifyStateChanged() {
+ Shell.AppSystem.get_default().emit('app-state-changed', this);
+ this.notify('state');
+ },
+
+ _setState(state) {
+ const oldState = this.state;
+ this._state = state;
+
+ if (this.state !== oldState)
+ this._notifyStateChanged();
+ },
+
+ _setWindows(windows) {
+ const oldState = this.state;
+ const oldWindows = this._windows.slice();
+ const result = {windowsChanged: false, stateChanged: false};
+ this._state = undefined;
+
+ if (windows.length !== oldWindows.length ||
+ windows.some((win, index) => win !== oldWindows[index])) {
+ this._windows = windows.filter(w => !w.is_override_redirect());
+ this.emit('windows-changed');
+ result.windowsChanged = true;
+ }
+
+ if (this.state !== oldState) {
+ this._notifyStateChanged();
+ this._checkFocused();
+ result.stateChanged = true;
+ }
+
+ return result;
+ },
+ }, {readOnly: false});
+
+ shellApp._sources.add(GLib.idle_add(GLib.DEFAULT_PRIORITY, () => {
+ shellApp._updateWindows();
+ shellApp._sources.delete(GLib.main_current_source().source_id);
+ return GLib.SOURCE_REMOVE;
+ }));
+
+ const windowTracker = Shell.WindowTracker.get_default();
+ shellApp._checkFocused = function () {
+ if (this._windows.some(w => w.has_focus())) {
+ this.isFocused = true;
+ windowTracker.notify('focus-app');
+ } else if (this.isFocused) {
+ this.isFocused = false;
+ windowTracker.notify('focus-app');
+ }
+ };
+
+ shellApp._checkFocused();
+ shellApp._signalConnections.add(global.display, 'notify::focus-window', () =>
+ shellApp._checkFocused());
+
+ // Re-implements shell_app_activate_window for generic activation and alt-tab support
+ m('activate_window', function (_om, window, timestamp) {
+ /* eslint-disable no-invalid-this */
+ if (!window)
+ [window] = this.get_windows();
+ else if (!this._windows.includes(window))
+ return;
+
+ const currentWorkspace = global.workspace_manager.get_active_workspace();
+ const workspace = window.get_workspace();
+ const sameWorkspaceWindows = this.get_windows().filter(w =>
+ w.get_workspace() === workspace);
+ sameWorkspaceWindows.forEach(w => w.raise());
+
+ if (workspace !== currentWorkspace)
+ workspace.activate_with_focus(window, timestamp);
+ else
+ window.activate(timestamp);
+ /* eslint-enable no-invalid-this */
+ });
+
+ // Re-implements shell_app_activate_full for generic activation and dash support
+ m('activate_full', function (_om, workspace, timestamp) {
+ /* eslint-disable no-invalid-this */
+ if (!timestamp)
+ timestamp = global.get_current_time();
+
+ switch (this.state) {
+ case Shell.AppState.STOPPED:
+ try {
+ this._startingWorkspace = workspace;
+ this._setState(Shell.AppState.STARTING);
+ this.launch(timestamp, workspace, Shell.AppLaunchGpu.APP_PREF);
+ } catch (e) {
+ logError(e);
+ this._setState(Shell.AppState.STOPPED);
+ global.notify_error(_('Failed to launch “%s”'.format(
+ this.get_name())), e.message);
+ }
+ break;
+ case Shell.AppState.RUNNING:
+ this.activate_window(null, timestamp);
+ break;
+ }
+ /* eslint-enable no-invalid-this */
+ });
+
+ m('activate', () => shellApp.activate_full(-1, 0));
+
+ m('compare', (_om, other) => Utils.shellAppCompare(shellApp, other));
+
+ const {destroy: defaultDestroy} = shellApp;
+ shellApp.destroy = function () {
+ /* eslint-disable no-invalid-this */
+ this._dtdData.proxyProperties.forEach(prop => delete this[prop]);
+ this._dtdData.destroy();
+ this._dtdData = undefined;
+ this.appInfo.destroy?.();
+ this.destroy = defaultDestroy;
+ defaultDestroy?.call(this);
+ /* eslint-enable no-invalid-this */
+ };
+
+ return shellApp;
+}
+
+/**
+ * We can't inherit from Shell.App as it's a final type, so let's patch it
+ *
+ * @param params
+ */
+function makeLocationApp(params) {
+ if (!(params?.appInfo instanceof LocationAppInfo))
+ throw new TypeError('Invalid location');
+
+ const {fallbackIconName} = params;
+ delete params.fallbackIconName;
+
+ const shellApp = new Shell.App(params);
+ wrapWindowsBackedApp(shellApp);
+
+ shellApp._setDtdData({
+ location: () => shellApp.appInfo.location,
+ isTrash: shellApp.appInfo instanceof TrashAppInfo,
+ }, {getter: true, enumerable: true});
+
+ shellApp._mi('toString', defaultToString =>
+ '[LocationApp "%s" - %s]'.format(shellApp.get_id(),
+ defaultToString.call(shellApp)));
+
+ shellApp._mi('launch', (_om, timestamp, workspace, _gpuPref) =>
+ shellApp.appInfo.launch([],
+ global.create_app_launch_context(timestamp, workspace)));
+
+ shellApp._mi('launch_action', (_om, actionName, ...args) =>
+ shellApp.appInfo.launchAction(actionName, ...args));
+
+ shellApp._mi('create_icon_texture', (_om, iconSize) => new St.Icon({
+ iconSize,
+ gicon: shellApp.icon,
+ fallbackIconName,
+ }));
+
+ // FIXME: We need to add a new API to Nautilus to open new windows
+ shellApp._mi('can_open_new_window', () => {
+ try {
+ if (!shellApp.get_n_windows())
+ return true;
+
+ const handlerApp = shellApp.appInfo.getHandlerApp();
+
+ if (handlerApp.has_key('SingleMainWindow'))
+ return !handlerApp.get_boolean('SingleMainWindow');
+
+ if (handlerApp.has_key('X-GNOME-SingleWindow'))
+ return !handlerApp.get_boolean('X-GNOME-SingleWindow');
+
+ if (handlerApp.get_commandline()?.split(' ').includes('--new-window'))
+ return true;
+
+ const [window] = shellApp.get_windows();
+ if (window && window.get_gtk_window_object_path())
+ return window.get_gtk_application_id() === null;
+
+ return true;
+ } catch {
+ return false;
+ }
+ });
+
+ shellApp._mi('open_new_window', function (_om, workspace) {
+ /* eslint-disable no-invalid-this */
+ const context = global.create_app_launch_context(0, workspace);
+ if (!this.get_n_windows()) {
+ this.appInfo.launch([], context);
+ return;
+ }
+ const appId = this.appInfo.get_id();
+ Gio.AppInfo.create_from_commandline(this.appInfo.get_commandline(),
+ this.appInfo.get_id(), appId
+ ? Gio.AppInfoCreateFlags.SUPPORTS_STARTUP_NOTIFICATION
+ : Gio.AppInfoCreateFlags.NONE).launch_uris(
+ [this.appInfo.location.get_uri()], context);
+ /* eslint-enable no-invalid-this */
+ });
+
+ if (shellApp.appInfo instanceof MountableVolumeAppInfo) {
+ shellApp._mi('get_busy', function (parentGetBusy) {
+ /* eslint-disable no-invalid-this */
+ if (this.appInfo.busy)
+ return true;
+ return parentGetBusy.call(this);
+ /* eslint-enable no-invalid-this */
+ });
+ shellApp._pi('busy', {get: () => shellApp.get_busy()});
+ shellApp._signalConnections.add(shellApp.appInfo, 'notify::busy', _ =>
+ shellApp.notify('busy'));
+ }
+
+ shellApp._mi('get_windows', function () {
+ /* eslint-disable no-invalid-this */
+ if (this._needsResort)
+ this._sortWindows();
+ return this._windows;
+ /* eslint-enable no-invalid-this */
+ });
+
+ const {fm1Client} = Docking.DockManager.getDefault();
+ shellApp._setDtdData({
+ _needsResort: true,
+
+ _windowsOrderChanged() {
+ this._needsResort = true;
+ this.emit('windows-changed');
+ },
+
+ _sortWindows() {
+ this._windows.sort(Utils.shellWindowsCompare);
+ this._needsResort = false;
+ },
+
+ _updateWindows() {
+ const windows = fm1Client.getWindows(this.location?.get_uri()).sort(
+ Utils.shellWindowsCompare);
+ const {windowsChanged} = this._setWindows(windows);
+
+ if (!windowsChanged)
+ return;
+
+ this._signalConnections.removeWithLabel(Labels.LOCATION_WINDOWS);
+ windows.forEach(w =>
+ this._signalConnections.addWithLabel(Labels.LOCATION_WINDOWS, w,
+ 'notify::user-time', () => {
+ if (w !== this._windows[0])
+ this._windowsOrderChanged();
+ }));
+ },
+ }, {readOnly: false});
+
+ shellApp._signalConnections.add(fm1Client, 'windows-changed', () =>
+ shellApp._updateWindows());
+ shellApp._signalConnections.add(shellApp.appInfo, 'notify::icon', () =>
+ shellApp.notify('icon'));
+ shellApp._signalConnections.add(global.workspaceManager,
+ 'workspace-switched', () => shellApp._windowsOrderChanged());
+
+ return shellApp;
+}
+
+/**
+ *
+ */
+function getFileManagerApp() {
+ return Shell.AppSystem.get_default().lookup_app(FILE_MANAGER_DESKTOP_APP_ID);
+}
+
+/**
+ *
+ */
+export function wrapFileManagerApp() {
+ const fileManagerApp = getFileManagerApp();
+ if (!fileManagerApp)
+ return null;
+
+ if (fileManagerApp._dtdData)
+ return fileManagerApp;
+
+ const originalGetWindows = fileManagerApp.get_windows;
+ wrapWindowsBackedApp(fileManagerApp);
+
+ const {removables, trash} = Docking.DockManager.getDefault();
+ fileManagerApp._signalConnections.addWithLabel(Labels.WINDOWS_CHANGED,
+ fileManagerApp, 'windows-changed', () => {
+ fileManagerApp.stop_emission_by_name('windows-changed');
+ // Let's wait for the location app to take control before of us
+ const id = GLib.timeout_add(GLib.PRIORITY_DEFAULT, 50, () => {
+ fileManagerApp._sources.delete(id);
+ fileManagerApp._updateWindows();
+ return GLib.SOURCE_REMOVE;
+ });
+ fileManagerApp._sources.add(id);
+ });
+
+ fileManagerApp._signalConnections.add(global.workspaceManager,
+ 'workspace-switched', () => {
+ fileManagerApp._signalConnections.blockWithLabel(Labels.WINDOWS_CHANGED);
+ fileManagerApp.emit('windows-changed');
+ fileManagerApp._signalConnections.unblockWithLabel(Labels.WINDOWS_CHANGED);
+ });
+
+ if (removables) {
+ fileManagerApp._signalConnections.add(removables, 'changed', () =>
+ fileManagerApp._updateWindows());
+ fileManagerApp._signalConnections.add(removables, 'windows-changed', () =>
+ fileManagerApp._updateWindows());
+ }
+
+ if (trash?.getApp()) {
+ fileManagerApp._signalConnections.add(trash.getApp(), 'windows-changed', () =>
+ fileManagerApp._updateWindows());
+ }
+
+ fileManagerApp._updateWindows = function () {
+ const locationWindows = [];
+ getRunningApps().forEach(a => locationWindows.push(...a.get_windows()));
+ const windows = originalGetWindows.call(this).filter(w =>
+ !locationWindows.includes(w));
+
+ this._signalConnections.blockWithLabel(Labels.WINDOWS_CHANGED);
+ this._setWindows(windows);
+ this._signalConnections.unblockWithLabel(Labels.WINDOWS_CHANGED);
+ };
+
+ fileManagerApp._mi('toString', defaultToString =>
+ '[FileManagerApp - %s]'.format(defaultToString.call(fileManagerApp)));
+
+ return fileManagerApp;
+}
+
+/**
+ *
+ */
+export function unWrapFileManagerApp() {
+ const fileManagerApp = getFileManagerApp();
+ if (!fileManagerApp || !fileManagerApp._dtdData)
+ return;
+
+ fileManagerApp.destroy();
+}
+
+/**
+ * This class maintains a Shell.App representing the Trash and keeps it
+ * up-to-date as the trash fills and is emptied over time.
+ */
+export class Trash {
+ destroy() {
+ this._trashApp?.destroy();
+ }
+
+ _ensureApp() {
+ if (this._trashApp)
+ return;
+
+ this._trashApp = makeLocationApp({
+ appInfo: new TrashAppInfo(new Gio.Cancellable()),
+ fallbackIconName: FALLBACK_TRASH_ICON,
+ });
+ }
+
+ getApp() {
+ this._ensureApp();
+ return this._trashApp;
+ }
+}
+
+/**
+ * This class maintains Shell.App representations for removable devices
+ * plugged into the system, and keeps the list of Apps up-to-date as
+ * devices come and go and are mounted and unmounted.
+ */
+export class Removables {
+ static initVolumePromises(object) {
+ // TODO: This can be simplified using actual interface type when we
+ // can depend on gjs 1.72
+ if (!(object instanceof Gio.Volume) || object.constructor.prototype._d2dPromisified)
+ return;
+
+ Gio._promisify(object.constructor.prototype, 'mount', 'mount_finish');
+ Gio._promisify(object.constructor.prototype, 'eject_with_operation',
+ 'eject_with_operation_finish');
+ object.constructor.prototype._d2dPromisified = true;
+ }
+
+ static initMountPromises(object) {
+ // TODO: This can be simplified using actual interface type when we
+ // can depend on gjs 1.72
+ if (!(object instanceof Gio.Mount) || object.constructor.prototype._d2dPromisified)
+ return;
+
+ Gio._promisify(object.constructor.prototype, 'eject_with_operation',
+ 'eject_with_operation_finish');
+ Gio._promisify(object.constructor.prototype, 'unmount_with_operation',
+ 'unmount_with_operation_finish');
+ object.constructor.prototype._d2dPromisified = true;
+ }
+
+ constructor() {
+ this._signalsHandler = new Utils.GlobalSignalsHandler();
+
+ this._monitor = Gio.VolumeMonitor.get();
+ this._cancellable = new Gio.Cancellable();
+
+ this._monitor.get_mounts().forEach(m => Removables.initMountPromises(m));
+ this._updateVolumes();
+
+ this._signalsHandler.add([
+ this._monitor,
+ 'volume-added',
+ (_, volume) => this._onVolumeAdded(volume),
+ ], [
+ this._monitor,
+ 'volume-removed',
+ (_, volume) => this._onVolumeRemoved(volume),
+ ], [
+ this._monitor,
+ 'mount-added',
+ (_, mount) => this._onMountAdded(mount),
+ ], [
+ Docking.DockManager.settings,
+ 'changed::show-mounts-only-mounted',
+ () => this._updateVolumes(),
+ ], [
+ Docking.DockManager.settings,
+ 'changed::show-mounts-network',
+ () => this._updateVolumes(),
+ ]);
+ }
+
+ destroy() {
+ this._volumeApps.forEach(a => a.destroy());
+ this._volumeApps = [];
+ this._cancellable.cancel();
+ this._cancellable = null;
+ this._signalsHandler.destroy();
+ this._monitor = null;
+ }
+
+ _updateVolumes() {
+ this._volumeApps?.forEach(a => a.destroy());
+ this._volumeApps = [];
+ this.emit('changed');
+
+ this._monitor.get_volumes().forEach(v => this._onVolumeAdded(v));
+ }
+
+ _onVolumeAdded(volume) {
+ Removables.initVolumePromises(volume);
+
+ if (!Docking.DockManager.settings.showMountsNetwork &&
+ volume.get_identifier('class') === 'network')
+ return;
+
+
+ const mount = volume.get_mount();
+ if (mount) {
+ if (mount.is_shadowed())
+ return;
+ if (!mount.can_eject() && !mount.can_unmount())
+ return;
+ } else {
+ if (Docking.DockManager.settings.showMountsOnlyMounted)
+ return;
+ if (!volume.can_mount() && !volume.can_eject())
+ return;
+ }
+
+ const appInfo = new MountableVolumeAppInfo(volume,
+ new Utils.CancellableChild(this._cancellable));
+ const volumeApp = makeLocationApp({
+ appInfo,
+ fallbackIconName: FALLBACK_REMOVABLE_MEDIA_ICON,
+ });
+
+ volumeApp._signalConnections.add(volumeApp, 'windows-changed',
+ () => this.emit('windows-changed', volumeApp));
+
+ if (Docking.DockManager.settings.showMountsOnlyMounted) {
+ volumeApp._signalConnections.add(appInfo, 'notify::mount',
+ () => !appInfo.mount && this._onVolumeRemoved(appInfo.volume));
+ }
+
+ this._volumeApps.push(volumeApp);
+ this.emit('changed');
+ }
+
+ _onVolumeRemoved(volume) {
+ const volumeIndex = this._volumeApps.findIndex(({appInfo}) =>
+ appInfo.volume === volume);
+ if (volumeIndex !== -1) {
+ const [volumeApp] = this._volumeApps.splice(volumeIndex, 1);
+ // We don't care about cancelling the ongoing operations from now on.
+ volumeApp.appInfo.cancellable = null;
+ volumeApp.destroy();
+ this.emit('changed');
+ }
+ }
+
+ _onMountAdded(mount) {
+ Removables.initMountPromises(mount);
+
+ if (!Docking.DockManager.settings.showMountsOnlyMounted)
+ return;
+
+ if (!this._volumeApps.find(({appInfo}) => appInfo.mount === mount)) {
+ // In some Gio.Mount implementations the volume may be set after
+ // mount is emitted, so we could just ignore it as we'll get it
+ // later via volume-added
+ const volume = mount.get_volume();
+ if (volume)
+ this._onVolumeAdded(volume);
+ }
+ }
+
+ getApps() {
+ return this._volumeApps;
+ }
+}
+Signals.addSignalMethods(Removables.prototype);
+
+/**
+ *
+ */
+function getApps() {
+ const dockManager = Docking.DockManager.getDefault();
+ const locationApps = [];
+
+ if (dockManager.removables)
+ locationApps.push(...dockManager.removables.getApps());
+
+ if (dockManager.trash)
+ locationApps.push(dockManager.trash.getApp());
+
+ return locationApps;
+}
+
+/**
+ *
+ */
+export function getRunningApps() {
+ return getApps().filter(a => a.state === Shell.AppState.RUNNING);
+}
+
+/**
+ *
+ */
+export function getStartingApps() {
+ return getApps().filter(a => a.state === Shell.AppState.STARTING);
+}