1704 lines
58 KiB
JavaScript
1704 lines
58 KiB
JavaScript
import GObject from 'gi://GObject';
|
||
import St from 'gi://St';
|
||
import Gio from 'gi://Gio';
|
||
import Clutter from 'gi://Clutter';
|
||
import Shell from 'gi://Shell';
|
||
import GLib from 'gi://GLib';
|
||
import { Extension, gettext as _ } from 'resource:///org/gnome/shell/extensions/extension.js';
|
||
import * as PanelMenu from 'resource:///org/gnome/shell/ui/panelMenu.js';
|
||
import * as PopupMenu from 'resource:///org/gnome/shell/ui/popupMenu.js';
|
||
import * as Main from 'resource:///org/gnome/shell/ui/main.js';
|
||
|
||
const AppPinner = GObject.registerClass(
|
||
class AppPinner extends PanelMenu.Button {
|
||
|
||
// 1. Costruttore e inizializzazione
|
||
_init(settings) {
|
||
super._init(0.0, _('App Pinner'));
|
||
this._settings = settings;
|
||
this._destroyed = false;
|
||
this._pendingApps = new Set();
|
||
this._settingsHandler = [];
|
||
this._longPressTimeoutId = null;
|
||
|
||
this._windowTracker = Shell.WindowTracker.get_default();
|
||
this._windowTracker.connect('tracked-windows-changed', this._updateRunningIndicators.bind(this));
|
||
|
||
this._settingsHandler.push(
|
||
this._settings.connect('changed::show-in-panel', () => this._updateVisibility()),
|
||
this._settings.connect('changed::sort-alphabetically', () => this._refreshUI()),
|
||
this._settings.connect('changed::show-pin-icon', () => this._updatePinIconVisibility())
|
||
);
|
||
|
||
this._appSystem = Shell.AppSystem.get_default();
|
||
this._runningTracker = new Set();
|
||
this._appStateChangedId = this._appSystem.connect('app-state-changed',
|
||
() => this._updateRunningIndicators());
|
||
|
||
// Contenitore principale
|
||
this._mainContainer = new St.BoxLayout({
|
||
style_class: 'panel-button',
|
||
vertical: false,
|
||
x_align: Clutter.ActorAlign.CENTER,
|
||
y_align: Clutter.ActorAlign.CENTER,
|
||
reactive: true,
|
||
track_hover: true
|
||
});
|
||
this.add_child(this._mainContainer);
|
||
|
||
// Icona del menu
|
||
this._menuIcon = new St.Icon({
|
||
icon_name: 'view-pin-symbolic',
|
||
style_class: 'system-status-icon app-pinner-menu-icon',
|
||
icon_size: 15
|
||
});
|
||
this._mainContainer.add_child(this._menuIcon);
|
||
|
||
// Contenitore icone pinnate
|
||
this._pinnedIconsBox = new St.BoxLayout({
|
||
style_class: 'app-pinner-icons',
|
||
vertical: false,
|
||
x_align: Clutter.ActorAlign.CENTER,
|
||
y_align: Clutter.ActorAlign.CENTER,
|
||
x_expand: true,
|
||
y_expand: true
|
||
});
|
||
this._mainContainer.add_child(this._pinnedIconsBox);
|
||
|
||
this._updateIconsSpacing();
|
||
|
||
// Connessioni impostazioni
|
||
this._settingsHandler.push(
|
||
this._settings.connect('changed::icon-size', () => this._refreshUI()),
|
||
this._settings.connect('changed::spacing', () => {
|
||
this._updateIconsSpacing();
|
||
this._pinnedIconsBox.queue_relayout();
|
||
}),
|
||
this._settings.connect('changed::enable-labels', () => this._refreshUI()),
|
||
this._settings.connect('changed::pinned-apps', () => this._refreshUI()),
|
||
this._settings.connect('changed::indicator-color', () => {
|
||
this._updateIndicatorColor();
|
||
this._updateRunningIndicators();
|
||
})
|
||
);
|
||
|
||
|
||
this.menu.actor.connect('button-press-event', (actor, event) => {
|
||
const target = event.get_source();
|
||
|
||
if (!this.menu.actor.contains(target)) {
|
||
this.menu.close();
|
||
}
|
||
});
|
||
|
||
this._buildMenu();
|
||
this._refreshUI();
|
||
|
||
this._logindProxy = new Gio.DBusProxy({
|
||
g_connection: Gio.DBus.system,
|
||
g_interface_name: 'org.freedesktop.login1.Manager',
|
||
g_object_path: '/org/freedesktop/login1',
|
||
g_name: 'org.freedesktop.login1'
|
||
});
|
||
|
||
this._logindProxy.init_async(GLib.PRIORITY_DEFAULT, null, (proxy, res) => {
|
||
try {
|
||
this._logindProxy.init_finish(res);
|
||
this._logindId = this._logindProxy.connectSignal('PrepareForSleep',
|
||
this._handleSleepSignal.bind(this));
|
||
} catch (e) {
|
||
console.error('Errore inizializzazione proxy login1:', e.message);
|
||
}
|
||
});
|
||
|
||
this._appStateChangedId = this._appSystem.connect('app-state-changed', () => {
|
||
this._addTimeout(100, () => {
|
||
this._updateRunningIndicators();
|
||
return GLib.SOURCE_REMOVE;
|
||
});
|
||
});
|
||
|
||
this._settingsHandler.push(
|
||
this._settings.connect('changed::custom-links', () => this._refreshUI())
|
||
);
|
||
|
||
this._timeoutIds = new Set();
|
||
|
||
}
|
||
|
||
_updateVisibility() {
|
||
const showInPanel = this._settings.get_boolean('show-in-panel');
|
||
this._pinnedIconsBox.visible = showInPanel;
|
||
this._updatePinIconVisibility();
|
||
this._pinnedIconsBox.queue_relayout();
|
||
this.queue_relayout();
|
||
}
|
||
|
||
_updatePinIconVisibility() {
|
||
const showPinIcon = this._settings.get_boolean('show-pin-icon');
|
||
const hasPinnedApps = this._settings.get_strv('pinned-apps').length > 0;
|
||
const noPinnedApps = !hasPinnedApps;
|
||
|
||
const iconSize = noPinnedApps ? 18 : 18;
|
||
this._menuIcon.icon_size = iconSize;
|
||
|
||
this._menuIcon.opacity = (noPinnedApps || showPinIcon) ? 255 : 0;
|
||
this._menuIcon.reactive = (noPinnedApps || showPinIcon);
|
||
this._menuIcon.set_width(showPinIcon ? -1 : iconSize);
|
||
|
||
this.queue_relayout();
|
||
}
|
||
|
||
_addTimeout(interval, callback) {
|
||
const sourceId = GLib.timeout_add(GLib.PRIORITY_DEFAULT, interval, () => {
|
||
const result = callback();
|
||
this._timeoutIds.delete(sourceId);
|
||
return result;
|
||
});
|
||
this._timeoutIds.add(sourceId);
|
||
return sourceId;
|
||
}
|
||
|
||
// 2. Metodi lifecycle e gestione stato
|
||
destroy() {
|
||
if (this._destroyed) return;
|
||
this._destroyed = true;
|
||
|
||
if (this._logindId && this._logindProxy) {
|
||
this._logindProxy.disconnect(this._logindId);
|
||
this._logindProxy = null;
|
||
}
|
||
|
||
this._settingsHandler?.forEach(h => this._settings.disconnect(h));
|
||
this._settingsHandler = null;
|
||
|
||
this._pinnedIconsBox?.destroy();
|
||
this._searchInput?.destroy();
|
||
|
||
if (this._appStateChangedId) {
|
||
this._appSystem.disconnect(this._appStateChangedId);
|
||
this._appStateChangedId = null;
|
||
}
|
||
|
||
if (this._timeoutIds) {
|
||
this._timeoutIds.forEach(id => GLib.Source.remove(id));
|
||
this._timeoutIds.clear();
|
||
}
|
||
|
||
if (this._longPressTimeoutId !== null) {
|
||
GLib.Source.remove(this._longPressTimeoutId);
|
||
this._timeoutIds.delete(this._longPressTimeoutId);
|
||
this._longPressTimeoutId = null;
|
||
}
|
||
|
||
super.destroy();
|
||
}
|
||
|
||
// 3. Costruzione UI principale
|
||
_buildMenu() {
|
||
this.menu.removeAll();
|
||
|
||
// Barra di ricerca
|
||
const searchEntry = new PopupMenu.PopupBaseMenuItem();
|
||
const searchBox = new St.BoxLayout({
|
||
vertical: false,
|
||
style_class: 'search-box',
|
||
x_expand: true,
|
||
x_align: Clutter.ActorAlign.FILL
|
||
});
|
||
|
||
this._searchInput = new St.Entry({
|
||
hint_text: _('Search applications...'),
|
||
can_focus: true,
|
||
x_expand: true
|
||
});
|
||
|
||
searchBox.add_child(this._searchInput);
|
||
searchEntry.actor.add_child(searchBox);
|
||
this.menu.addMenuItem(searchEntry);
|
||
|
||
// Sezione risultati
|
||
this._resultsSection = new PopupMenu.PopupMenuSection();
|
||
this.menu.addMenuItem(this._resultsSection);
|
||
|
||
// Sezione pinnati
|
||
this.menu.addMenuItem(new PopupMenu.PopupSeparatorMenuItem());
|
||
this._pinnedSection = new PopupMenu.PopupMenuSection();
|
||
this.menu.addMenuItem(this._pinnedSection);
|
||
|
||
this._searchInput.clutter_text.connect('text-changed', () => this._updateSearch());
|
||
|
||
// Sezione aggiungi link
|
||
this.menu.addMenuItem(new PopupMenu.PopupSeparatorMenuItem());
|
||
const linkSection = new PopupMenu.PopupMenuSection();
|
||
this.menu.addMenuItem(linkSection);
|
||
|
||
const linkBox = new St.BoxLayout({
|
||
vertical: false,
|
||
style_class: 'app-pinner-link-box',
|
||
x_expand: true,
|
||
margin_top: 6,
|
||
margin_bottom: 6
|
||
});
|
||
this._linkInput = new St.Entry({
|
||
hint_text: _('Enter URL...'),
|
||
can_focus: true,
|
||
style_class: 'app-pinner-link-input'
|
||
});
|
||
|
||
const addLinkBtn = new St.Button({
|
||
style_class: 'app-pinner-add-link-btn',
|
||
label: _('Add'),
|
||
can_focus: true,
|
||
x_align: Clutter.ActorAlign.END
|
||
});
|
||
|
||
addLinkBtn.connect('clicked', () => {
|
||
const url = this._linkInput.get_text().trim();
|
||
if (this._isValidUrl(url)) {
|
||
this._pinLink(url);
|
||
this._linkInput.set_text('');
|
||
this.menu.close();
|
||
}
|
||
});
|
||
|
||
linkBox.add_child(this._linkInput);
|
||
linkBox.add_child(addLinkBtn);
|
||
linkSection.actor.add_child(linkBox);
|
||
|
||
this._mainContainer.reactive = true;
|
||
this._mainContainer.connect('button-press-event', (actor, event) => {
|
||
if (event.get_button() === Clutter.BUTTON_PRIMARY) {
|
||
this.menu.toggle();
|
||
return Clutter.EVENT_STOP;
|
||
}
|
||
return Clutter.EVENT_PROPAGATE;
|
||
});
|
||
|
||
this.menu.actor.connect('button-press-event', (actor, event) => {
|
||
const target = event.get_source();
|
||
if (!this.menu.actor.contains(target)) {
|
||
this.menu.close();
|
||
}
|
||
});
|
||
}
|
||
|
||
_addPinnedIcon(appId) {
|
||
|
||
if (appId.startsWith('link://')) {
|
||
const url = appId.replace('link://', '');
|
||
return this._addLinkIcon(url);
|
||
}
|
||
|
||
|
||
const app = Gio.DesktopAppInfo.new(appId + '.desktop') ||
|
||
Gio.DesktopAppInfo.new(appId);
|
||
|
||
if (!app) {
|
||
const altPaths = [
|
||
GLib.build_filenamev([GLib.get_home_dir(), '.local', 'share', 'applications', `${appId}.desktop`]),
|
||
`/var/lib/flatpak/exports/share/applications/${appId}.desktop`,
|
||
`${GLib.get_home_dir()}/.local/share/flatpak/exports/share/applications/${appId}.desktop`
|
||
];
|
||
|
||
for (const path of altPaths) {
|
||
if (GLib.file_test(path, GLib.FileTest.EXISTS)) {
|
||
const altApp = Gio.DesktopAppInfo.new_from_filename(path);
|
||
if (altApp) {
|
||
app = altApp;
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
|
||
if (!app) {
|
||
console.error(`[CRITICAL] App not found in any location: ${appId}`);
|
||
return;
|
||
}
|
||
}
|
||
|
||
const showLabels = this._settings.get_boolean('enable-labels');
|
||
|
||
const iconBox = new St.BoxLayout({
|
||
vertical: true,
|
||
style_class: 'app-pinner-icon-box',
|
||
y_expand: true
|
||
});
|
||
|
||
const iconContainer = new St.Bin({
|
||
style_class: 'app-pinner-icon-container',
|
||
x_align: Clutter.ActorAlign.CENTER,
|
||
y_align: Clutter.ActorAlign.CENTER,
|
||
});
|
||
|
||
|
||
const iconButton = new St.Button({
|
||
child: new St.Icon({
|
||
gicon: app.get_icon(),
|
||
icon_size: this._settings.get_int('icon-size')
|
||
}),
|
||
style_class: 'app-pinner-icon',
|
||
reactive: true,
|
||
hover: true,
|
||
track_hover: true
|
||
});
|
||
|
||
const runningIndicator = new St.Widget({
|
||
style_class: 'app-pinner-running-indicator',
|
||
visible: false,
|
||
reactive: false,
|
||
x_align: Clutter.ActorAlign.END,
|
||
y_align: Clutter.ActorAlign.START,
|
||
x_expand: false,
|
||
y_expand: false,
|
||
translation_x: +9,
|
||
translation_y: -9
|
||
});
|
||
|
||
this._updateIndicatorColor(runningIndicator);
|
||
|
||
this._settings.connect('changed::indicator-color',
|
||
() => this._updateIndicatorColor(runningIndicator));
|
||
|
||
|
||
iconContainer.add_child(iconButton);
|
||
iconContainer.add_child(runningIndicator);
|
||
|
||
iconBox.runningIndicator = runningIndicator;
|
||
|
||
let pressStartTime = 0;
|
||
this._longPressTimeoutId = null;
|
||
let isLongPress = false;
|
||
|
||
iconButton.connect('button-press-event', (actor, event) => {
|
||
|
||
if (this._longPressTimeoutId !== null) {
|
||
GLib.Source.remove(this._longPressTimeoutId);
|
||
this._timeoutIds.delete(this._longPressTimeoutId);
|
||
this._longPressTimeoutId = null;
|
||
}
|
||
|
||
pressStartTime = Date.now();
|
||
isLongPress = false;
|
||
|
||
this._longPressTimeoutId = this._addTimeout(500, () => {
|
||
isLongPress = true;
|
||
this._animateAndMoveToEnd(appId, iconBox);
|
||
this._longPressTimeoutId = null;
|
||
return GLib.SOURCE_REMOVE;
|
||
});
|
||
|
||
actor.ease({
|
||
scale_x: 0.8,
|
||
scale_y: 0.8,
|
||
duration: 200
|
||
});
|
||
return Clutter.EVENT_PROPAGATE;
|
||
});
|
||
|
||
iconButton.connect('button-release-event', (actor, event) => {
|
||
if (this._longPressTimeoutId !== null) {
|
||
GLib.Source.remove(this._longPressTimeoutId);
|
||
this._timeoutIds.delete(this._longPressTimeoutId);
|
||
this._longPressTimeoutId = null;
|
||
}
|
||
|
||
actor.ease({
|
||
scale_x: 1.0,
|
||
scale_y: 1.0,
|
||
duration: 200
|
||
});
|
||
return Clutter.EVENT_PROPAGATE;
|
||
});
|
||
|
||
iconButton.connect('clicked', () => {
|
||
if (!isLongPress) {
|
||
this._launchApp(appId);
|
||
}
|
||
isLongPress = false;
|
||
});
|
||
|
||
iconBox.add_child(iconContainer);
|
||
|
||
if (showLabels) {
|
||
iconBox.add_child(new St.Label({
|
||
text: app.get_name(),
|
||
style_class: 'app-pinner-label'
|
||
}));
|
||
}
|
||
|
||
this._pinnedIconsBox.add_child(iconBox);
|
||
iconBox.appId = appId;
|
||
|
||
this._updateRunningIndicator(appId, runningIndicator);
|
||
|
||
return iconBox;
|
||
}
|
||
|
||
_addLinkIcon(url) {
|
||
const appId = `link://${url}`;
|
||
const iconSize = this._settings.get_int('icon-size');
|
||
const showLabels = this._settings.get_boolean('enable-labels');
|
||
|
||
const iconBox = new St.BoxLayout({
|
||
vertical: true,
|
||
style_class: 'app-pinner-icon-box'
|
||
});
|
||
|
||
const iconButton = new St.Button({
|
||
child: new St.Icon({
|
||
icon_name: 'emblem-web-symbolic',
|
||
icon_size: iconSize,
|
||
style_class: 'app-pinner-link-icon'
|
||
}),
|
||
style_class: 'app-pinner-icon',
|
||
reactive: true,
|
||
hover: true,
|
||
track_hover: true
|
||
});
|
||
|
||
iconButton.connect('notify::hover', () => {
|
||
iconButton.style = iconButton.hover
|
||
? 'background: rgba(255,255,255,0.1); border-radius: 6px;'
|
||
: '';
|
||
});
|
||
|
||
let pressStartTime = 0;
|
||
this._longPressTimeoutId = null;
|
||
let isLongPress = false;
|
||
|
||
iconButton.connect('button-press-event', (actor, event) => {
|
||
|
||
pressStartTime = Date.now();
|
||
isLongPress = false;
|
||
|
||
this._longPressTimeoutId = this._addTimeout(500, () => {
|
||
isLongPress = true;
|
||
this._animateAndMoveToEnd(appId, iconBox);
|
||
return GLib.SOURCE_REMOVE;
|
||
});
|
||
|
||
actor.ease({
|
||
scale_x: 0.8,
|
||
scale_y: 0.8,
|
||
duration: 200
|
||
});
|
||
|
||
return Clutter.EVENT_PROPAGATE;
|
||
});
|
||
|
||
iconButton.connect('button-release-event', (actor, event) => {
|
||
if (this._longPressTimeoutId !== null) {
|
||
GLib.Source.remove(this._longPressTimeoutId);
|
||
this._timeoutIds.delete(this._longPressTimeoutId);
|
||
this._longPressTimeoutId = null;
|
||
}
|
||
|
||
actor.ease({
|
||
scale_x: 1.0,
|
||
scale_y: 1.0,
|
||
duration: 200
|
||
});
|
||
|
||
return Clutter.EVENT_PROPAGATE;
|
||
});
|
||
|
||
iconButton.connect('clicked', () => {
|
||
if (!isLongPress) {
|
||
Gio.AppInfo.launch_default_for_uri(url, null);
|
||
}
|
||
isLongPress = false;
|
||
});
|
||
|
||
const container = new St.Bin();
|
||
container.add_child(iconButton);
|
||
iconBox.add_child(container);
|
||
|
||
if (showLabels) {
|
||
const label = new St.Label({
|
||
text: this._shortenUrl(url),
|
||
style_class: 'app-pinner-label',
|
||
x_align: Clutter.ActorAlign.CENTER
|
||
});
|
||
iconBox.add_child(label);
|
||
}
|
||
|
||
iconBox.appId = appId;
|
||
this._pinnedIconsBox.add_child(iconBox);
|
||
|
||
return iconBox;
|
||
}
|
||
|
||
_checkVisibility() {
|
||
if (this._destroyed) return;
|
||
this._updateVisibility();
|
||
this._addTimeout(2000, () => {
|
||
this._checkVisibility();
|
||
return GLib.SOURCE_REMOVE;
|
||
});
|
||
}
|
||
|
||
// 4. Gestione interazioni utente
|
||
_launchApp(appId) {
|
||
|
||
if (appId.startsWith('link://')) {
|
||
const url = appId.replace('link://', '');
|
||
Gio.AppInfo.launch_default_for_uri(url, null);
|
||
return;
|
||
}
|
||
try {
|
||
const appSystem = Shell.AppSystem.get_default();
|
||
let app = appSystem.lookup_app(`${appId}.desktop`) || appSystem.lookup_app(appId);
|
||
|
||
if (!app) {
|
||
const desktopApp = Gio.DesktopAppInfo.new(`${appId}.desktop`);
|
||
if (desktopApp) {
|
||
app = new Shell.App({ desktop_app_info: desktopApp });
|
||
}
|
||
if (!app) {
|
||
console.error(`[ERROR] Application ${appId} not found`);
|
||
return;
|
||
}
|
||
}
|
||
|
||
this._pendingApps.add(appId);
|
||
|
||
app.activate();
|
||
|
||
this._forceImmediateUpdate(appId);
|
||
|
||
this._addTimeout(1500, () => {
|
||
this._pendingApps.delete(appId);
|
||
this._updateRunningIndicators();
|
||
this._addTimeout(500, () => {
|
||
this._updateRunningIndicators();
|
||
return GLib.SOURCE_REMOVE;
|
||
});
|
||
return GLib.SOURCE_REMOVE;
|
||
});
|
||
|
||
} catch (e) {
|
||
this._pendingApps.delete(appId);
|
||
}
|
||
|
||
this._pendingApps.add(appId);
|
||
this._updateRunningIndicators();
|
||
}
|
||
|
||
_handleSleepSignal(sender, signalName, params) {
|
||
const [isSleeping] = params.deepUnpack();
|
||
if (!isSleeping) {
|
||
this._addTimeout(1000, () => {
|
||
this._updateVisibility();
|
||
this._forceFullRefresh();
|
||
return GLib.SOURCE_REMOVE;
|
||
});
|
||
}
|
||
}
|
||
|
||
// 5. Gestione stato applicazioni
|
||
_updateRunningIndicators() {
|
||
const children = this._pinnedIconsBox.get_children();
|
||
|
||
children.forEach(iconBox => {
|
||
const appId = iconBox.appId;
|
||
if (!appId || appId.startsWith('link://')) return;
|
||
this._updateRunningIndicator(appId, iconBox.runningIndicator);
|
||
});
|
||
}
|
||
|
||
_updateRunningIndicator(appId, indicator) {
|
||
let isRunning = false;
|
||
|
||
if (this._pendingApps.has(appId)) {
|
||
isRunning = true;
|
||
} else {
|
||
const app = this._findAppById(appId);
|
||
if (app) {
|
||
const state = app.get_state();
|
||
const hasWindows = app.get_windows().length > 0;
|
||
const isInAppSystem = this._appSystem.get_running().some(a => a.get_id() === app.get_id());
|
||
|
||
isRunning = state === Shell.AppState.RUNNING || hasWindows || isInAppSystem;
|
||
|
||
}
|
||
}
|
||
|
||
if (indicator.visible !== isRunning) {
|
||
indicator.visible = isRunning;
|
||
|
||
if (isRunning) {
|
||
indicator.ease({
|
||
scale_x: 1.2,
|
||
scale_y: 1.2,
|
||
opacity: 200,
|
||
duration: 200,
|
||
mode: Clutter.AnimationMode.EASE_OUT_QUAD,
|
||
onComplete: () => {
|
||
indicator.ease({
|
||
scale_x: 1.0,
|
||
scale_y: 1.0,
|
||
opacity: 255,
|
||
duration: 300
|
||
});
|
||
}
|
||
});
|
||
} else {
|
||
indicator.set_scale(1.0, 1.0);
|
||
indicator.opacity = 255;
|
||
}
|
||
}
|
||
|
||
if (!isRunning && this._pendingApps.has(appId)) {
|
||
this._pendingApps.delete(appId);
|
||
}
|
||
}
|
||
|
||
// 6. Gestione impostazioni e aggiornamenti UI
|
||
_refreshUI() {
|
||
if (this._destroyed) return;
|
||
|
||
const schema = this._settings.settings_schema;
|
||
const key = schema.get_key('icon-size');
|
||
const range = key.get_range();
|
||
const [minSize, maxIconSize] = range.deep_unpack();
|
||
|
||
let pinnedApps = this._settings.get_strv('pinned-apps');
|
||
|
||
if (this._settings.get_boolean('sort-alphabetically') && pinnedApps.length > 0) {
|
||
const sortedApps = [...pinnedApps].sort((a, b) => {
|
||
const nameA = this._getAppOrLinkName(a).toLowerCase();
|
||
const nameB = this._getAppOrLinkName(b).toLowerCase();
|
||
return nameA.localeCompare(nameB);
|
||
});
|
||
|
||
if (!arraysEqual(pinnedApps, sortedApps)) {
|
||
const handler = this._settings.connect('changed::pinned-apps', () => { });
|
||
this._settings.set_strv('pinned-apps', sortedApps);
|
||
this._settings.disconnect(handler);
|
||
return;
|
||
}
|
||
}
|
||
|
||
const spacing = this._settings.get_int('spacing');
|
||
const totalItems = pinnedApps.length;
|
||
|
||
const containerWidth = (maxIconSize * totalItems) + (spacing * Math.max(0, totalItems - 1));
|
||
|
||
this._mainContainer.set_style(`
|
||
min-width: ${containerWidth}px;
|
||
padding: 0 ${spacing}px;
|
||
`);
|
||
|
||
this._pinnedIconsBox.destroy_all_children();
|
||
|
||
pinnedApps.forEach(appId => {
|
||
const app = Gio.DesktopAppInfo.new(`${appId}.desktop`) || Gio.DesktopAppInfo.new(appId);
|
||
if (app) {
|
||
this._addPinnedIcon(appId);
|
||
} else if (appId.startsWith('link://')) {
|
||
this._addPinnedIcon(appId);
|
||
}
|
||
});
|
||
|
||
this._pinnedSection.box.destroy_all_children();
|
||
pinnedApps.forEach(appId => this._addMenuPinnedItem(appId));
|
||
|
||
this._updatePinIconVisibility();
|
||
}
|
||
|
||
_updateIconsSpacing() {
|
||
const spacing = Math.min(20, Math.max(0, this._settings.get_int('spacing')));
|
||
this._pinnedIconsBox.set_style(`spacing: ${spacing}px;`);
|
||
}
|
||
|
||
// 7. Gestione elementi pinnati
|
||
_pinApp(app) {
|
||
|
||
const current = this._settings.get_strv('pinned-apps');
|
||
if (current.length >= 10) {
|
||
this._showMaxItemsError();
|
||
return;
|
||
}
|
||
|
||
const rawAppId = app.get_id();
|
||
const appId = rawAppId.replace(/\.desktop$/i, '');
|
||
|
||
if (!current.includes(appId)) {
|
||
const updated = [...current, appId];
|
||
this._settings.set_strv('pinned-apps', updated);
|
||
}
|
||
}
|
||
|
||
_unpinApp(appId) {
|
||
if (appId.startsWith('link://')) {
|
||
const url = appId.replace('link://', '');
|
||
const links = this._settings.get_strv('custom-links')
|
||
.filter(l => l !== url);
|
||
this._settings.set_strv('custom-links', links);
|
||
}
|
||
|
||
const startupApps = this._settings.get_strv('startup-apps')
|
||
.filter(id => id !== appId);
|
||
this._settings.set_strv('startup-apps', startupApps);
|
||
|
||
const pinnedApps = this._settings.get_strv('pinned-apps')
|
||
.filter(id => id !== appId);
|
||
this._settings.set_strv('pinned-apps', pinnedApps);
|
||
}
|
||
|
||
_isPinned(appId) {
|
||
const cleanAppId = this._sanitizeAppId(appId);
|
||
return this._settings.get_strv('pinned-apps').includes(cleanAppId);
|
||
}
|
||
|
||
// 8. Funzioni helper e utilità
|
||
_isValidUrl(url) {
|
||
try {
|
||
new URL(url);
|
||
return true;
|
||
} catch {
|
||
return url.startsWith('http') || url.startsWith('ftp');
|
||
}
|
||
}
|
||
|
||
_sortApps(a, b, query) {
|
||
const aName = a.get_name().toLowerCase();
|
||
const bName = b.get_name().toLowerCase();
|
||
|
||
if (aName === query) return -1;
|
||
if (bName === query) return 1;
|
||
|
||
const aStart = aName.startsWith(query);
|
||
const bStart = bName.startsWith(query);
|
||
if (aStart !== bStart) return aStart ? -1 : 1;
|
||
|
||
return aName.length - bName.length;
|
||
}
|
||
|
||
_handleWakeupEvent() {
|
||
this._addTimeout(2000, () => {
|
||
this._updateRunningIndicators();
|
||
this._refreshUI();
|
||
return GLib.SOURCE_REMOVE;
|
||
});
|
||
}
|
||
|
||
_pinLink(url) {
|
||
|
||
const pinnedApps = this._settings.get_strv('pinned-apps');
|
||
if (pinnedApps.length >= 10) {
|
||
this._showMaxItemsError();
|
||
return;
|
||
}
|
||
|
||
if (!url) return;
|
||
if (!url.includes('://')) url = `http://${url}`;
|
||
|
||
const links = this._settings.get_strv('custom-links');
|
||
|
||
if (!links.includes(url)) {
|
||
const newLinks = [...links, url];
|
||
const newPinnedApps = [...pinnedApps, `link://${url}`];
|
||
|
||
this._settings.set_strv('custom-links', newLinks);
|
||
this._settings.set_strv('pinned-apps', newPinnedApps);
|
||
this._refreshUI();
|
||
}
|
||
}
|
||
|
||
async _updateSearch() {
|
||
this._resultsSection.box.destroy_all_children();
|
||
const query = this._searchInput.get_text().trim().toLowerCase();
|
||
if (!query) return;
|
||
|
||
const iconSize = this._settings.get_int('icon-size');
|
||
const appSys = Shell.AppSystem.get_default();
|
||
|
||
const results = appSys.get_installed()
|
||
.filter(app => {
|
||
const appId = this._sanitizeAppId(app.get_id());
|
||
return !this._isPinned(appId) &&
|
||
this._matchQuery(query, app.get_name().toLowerCase());
|
||
})
|
||
.sort((a, b) => this._sortApps(a, b, query))
|
||
.slice(0, 10);
|
||
|
||
if (results.length === 0) {
|
||
this._resultsSection.box.add_child(
|
||
new PopupMenu.PopupMenuItem(_('No results found'))
|
||
);
|
||
return;
|
||
}
|
||
|
||
results.forEach(app => {
|
||
const item = new PopupMenu.PopupMenuItem(app.get_name());
|
||
const icon = new St.Icon({
|
||
gicon: app.get_icon(),
|
||
icon_size: iconSize
|
||
});
|
||
item.insert_child_at_index(icon, 0);
|
||
|
||
if (this._settings.get_strv('pinned-apps').length >= 10) {
|
||
item.setSensitive(false);
|
||
item.label.text = _("Max items reached - unpin something first");
|
||
}
|
||
|
||
item.connect('activate', () => {
|
||
this._pinApp(app);
|
||
|
||
this._searchInput.set_text('');
|
||
this._updateSearch();
|
||
|
||
this.menu.close();
|
||
|
||
});
|
||
|
||
this._resultsSection.box.add_child(item);
|
||
});
|
||
|
||
this.menu.actor.show_all();
|
||
this.menu.actor.queue_redraw();
|
||
|
||
}
|
||
|
||
_sanitizeAppId(appId) {
|
||
const sanitized = appId
|
||
.replace(/\.desktop$/i, '')
|
||
.replace(/^application:\/\//i, '');
|
||
return sanitized;
|
||
}
|
||
|
||
_matchQuery(query, appName) {
|
||
const keywords = appName.split(/[\s-]/);
|
||
return appName.includes(query) ||
|
||
keywords.some(k => k.startsWith(query)) ||
|
||
this._fuzzyMatch(query, appName);
|
||
}
|
||
|
||
_fuzzyMatch(pattern, str) {
|
||
let patternIndex = 0;
|
||
for (const char of str.toLowerCase()) {
|
||
if (char === pattern[patternIndex]) {
|
||
if (++patternIndex === pattern.length) return true;
|
||
}
|
||
}
|
||
return false;
|
||
}
|
||
|
||
_getIndicatorForApp(appId) {
|
||
const iconBox = this._pinnedIconsBox.get_children().find(b => b.appId === appId);
|
||
return iconBox ? iconBox.runningIndicator : null;
|
||
}
|
||
|
||
_forceImmediateUpdate(appId) {
|
||
const indicator = this._getIndicatorForApp(appId);
|
||
if (indicator) {
|
||
indicator.visible = true;
|
||
indicator.opacity = 0;
|
||
indicator.set_scale(0.5, 0.5);
|
||
indicator.ease({
|
||
opacity: 255,
|
||
scale_x: 1,
|
||
scale_y: 1,
|
||
duration: 300,
|
||
mode: Clutter.AnimationMode.EASE_OUT_QUAD
|
||
});
|
||
}
|
||
this._updateRunningIndicators();
|
||
}
|
||
|
||
_shortenUrl(url) {
|
||
try {
|
||
const parsed = new URL(url);
|
||
return parsed.hostname.replace(/^www\./i, '');
|
||
} catch {
|
||
return url.substring(0, 15) + '...';
|
||
}
|
||
}
|
||
|
||
_forceFullRefresh() {
|
||
this._runningTracker = new Set(
|
||
this._appSystem.get_running().map(app => this._sanitizeAppId(app.get_id()))
|
||
);
|
||
|
||
this._pinnedIconsBox.get_children().forEach(iconBox => {
|
||
this._updateRunningIndicator(iconBox.appId, iconBox.runningIndicator);
|
||
});
|
||
|
||
this._addTimeout(1000, () => {
|
||
this._appSystem = Shell.AppSystem.get_default();
|
||
this._updateRunningIndicators();
|
||
return GLib.SOURCE_REMOVE;
|
||
});
|
||
}
|
||
|
||
_findAppById(appId) {
|
||
let app = this._appSystem.lookup_app(appId) ||
|
||
this._appSystem.lookup_app(`${appId}.desktop`);
|
||
|
||
if (!app) {
|
||
const gioApp = Gio.DesktopAppInfo.new(`${appId}.desktop`) ||
|
||
Gio.DesktopAppInfo.new(appId);
|
||
|
||
if (gioApp) {
|
||
app = this._appSystem.lookup_app(gioApp.get_id());
|
||
}
|
||
}
|
||
|
||
return app;
|
||
}
|
||
|
||
_refreshUIWithEffect() {
|
||
this._pinnedIconsBox.get_children().forEach((child, index) => {
|
||
child.ease({
|
||
opacity: 0,
|
||
scale_x: 0.5,
|
||
scale_y: 0.5,
|
||
duration: 300,
|
||
delay: index * 30,
|
||
onComplete: () => child.destroy()
|
||
});
|
||
});
|
||
|
||
this._addTimeout(300, () => {
|
||
this._settings.get_strv('pinned-apps').forEach(appId =>
|
||
this._addPinnedIcon(appId)
|
||
);
|
||
return GLib.SOURCE_REMOVE;
|
||
});
|
||
}
|
||
|
||
_animateAndMoveToEnd(appId, iconBox) {
|
||
const currentApps = this._settings.get_strv('pinned-apps');
|
||
const currentIndex = currentApps.indexOf(appId);
|
||
|
||
if (currentIndex === currentApps.length - 1) {
|
||
iconBox.ease({
|
||
scale_x: 1.2,
|
||
scale_y: 1.2,
|
||
duration: 200,
|
||
mode: Clutter.AnimationMode.EASE_OUT_QUAD,
|
||
onComplete: () => {
|
||
iconBox.ease({
|
||
scale_x: 1.0,
|
||
scale_y: 1.0,
|
||
duration: 300,
|
||
mode: Clutter.AnimationMode.EASE_OUT_BOUNCE
|
||
});
|
||
}
|
||
});
|
||
return;
|
||
}
|
||
|
||
iconBox.ease({
|
||
scale_x: 1.2,
|
||
scale_y: 1.2,
|
||
opacity: 0,
|
||
duration: 300,
|
||
mode: Clutter.AnimationMode.EASE_OUT_QUAD,
|
||
onComplete: () => {
|
||
const updatedApps = this._settings.get_strv('pinned-apps');
|
||
const newIndex = updatedApps.indexOf(appId);
|
||
|
||
if (newIndex === updatedApps.length - 1) return;
|
||
|
||
const newOrder = [
|
||
...updatedApps.slice(0, newIndex),
|
||
...updatedApps.slice(newIndex + 1),
|
||
updatedApps[newIndex]
|
||
];
|
||
|
||
this._settings.set_strv('pinned-apps', newOrder);
|
||
}
|
||
});
|
||
}
|
||
|
||
_updateIndicatorColor(indicator) {
|
||
const color = this._settings.get_string('indicator-color');
|
||
indicator.set_style(`background-color: ${color};`);
|
||
}
|
||
|
||
_getUrlName(url) {
|
||
try {
|
||
if (!url.match(/^[a-zA-Z]+:\/\//)) {
|
||
url = 'http://' + url;
|
||
}
|
||
|
||
const parsed = new URL(url);
|
||
let hostname = parsed.hostname;
|
||
|
||
hostname = hostname.replace(/^www\./i, '');
|
||
|
||
const specialCases = {
|
||
'youtube.com': 'YouTube',
|
||
'google.com': 'Google',
|
||
'github.com': 'GitHub',
|
||
};
|
||
|
||
return specialCases[hostname.toLowerCase()] ||
|
||
hostname.split('.')[0].charAt(0).toUpperCase() +
|
||
hostname.split('.')[0].slice(1);
|
||
} catch (e) {
|
||
return url.replace(/^https?:\/\//, '')
|
||
.replace(/^www\./i, '')
|
||
.split('/')[0];
|
||
}
|
||
}
|
||
|
||
_getAppOrLinkName(appId) {
|
||
if (appId.startsWith('link://')) {
|
||
return this._getUrlName(appId.replace('link://', ''));
|
||
} else {
|
||
const app = Gio.DesktopAppInfo.new(`${appId}.desktop`) || Gio.DesktopAppInfo.new(appId);
|
||
return app ? app.get_name() : appId;
|
||
}
|
||
}
|
||
|
||
|
||
_addMenuPinnedItem(appId) {
|
||
if (appId.startsWith('link://')) {
|
||
const url = appId.replace('link://', '');
|
||
const item = new PopupMenu.PopupMenuItem(url);
|
||
|
||
const icon = new St.Icon({
|
||
icon_name: 'emblem-web-symbolic',
|
||
icon_size: this._settings.get_int('icon-size'),
|
||
style_class: 'app-pinner-link-icon'
|
||
});
|
||
item.insert_child_at_index(icon, 0);
|
||
|
||
const spacer = new St.BoxLayout({ x_expand: true });
|
||
item.actor.add_child(spacer);
|
||
|
||
const removeBtn = new St.Button({
|
||
child: new St.Label({ text: '×' }),
|
||
style_class: 'app-pinner-remove-btn'
|
||
});
|
||
|
||
item.connect('activate', () => {
|
||
Gio.AppInfo.launch_default_for_uri(url, null);
|
||
this.menu.close();
|
||
});
|
||
|
||
removeBtn.connect('clicked', () => {
|
||
this._unpinApp(`link://${url}`);
|
||
});
|
||
|
||
item.add_child(removeBtn);
|
||
this._pinnedSection.box.add_child(item);
|
||
} else {
|
||
const app = Gio.DesktopAppInfo.new(`${appId}.desktop`);
|
||
if (!app) {
|
||
console.error(`Applicazione non trovata nel menu: ${appId}`);
|
||
return;
|
||
}
|
||
|
||
const item = new PopupMenu.PopupMenuItem(app.get_name());
|
||
const icon = new St.Icon({
|
||
gicon: app.get_icon(),
|
||
icon_size: this._settings.get_int('icon-size')
|
||
});
|
||
item.insert_child_at_index(icon, 0);
|
||
|
||
const spacer = new St.BoxLayout({ x_expand: true });
|
||
item.actor.add_child(spacer);
|
||
|
||
const removeBtn = new St.Button({
|
||
child: new St.Label({
|
||
text: '×',
|
||
style_class: 'app-pinner-remove-label'
|
||
}),
|
||
style_class: 'app-pinner-remove-btn'
|
||
});
|
||
|
||
removeBtn.connect('button-press-event', (actor, event) => {
|
||
this._unpinApp(appId);
|
||
this.menu.close();
|
||
this.menu.open();
|
||
return Clutter.EVENT_STOP;
|
||
});
|
||
|
||
item.connect('activate', () => {
|
||
this._launchApp(appId);
|
||
this.menu.close();
|
||
});
|
||
|
||
item.add_child(removeBtn);
|
||
this._pinnedSection.box.add_child(item);
|
||
}
|
||
}
|
||
});
|
||
|
||
function arraysEqual(a, b) {
|
||
return a.length === b.length && a.every((v, i) => v === b[i]);
|
||
}
|
||
|
||
export default class AppPinnerExtension extends Extension {
|
||
|
||
constructor(metadata) {
|
||
super(metadata);
|
||
this._timeoutIds = new Set();
|
||
this._settingsHandler = [];
|
||
}
|
||
|
||
_addTimeout(interval, callback) {
|
||
const sourceId = GLib.timeout_add(GLib.PRIORITY_DEFAULT, interval, () => {
|
||
const result = callback();
|
||
this._timeoutIds.delete(sourceId);
|
||
return result;
|
||
});
|
||
this._timeoutIds.add(sourceId);
|
||
return sourceId;
|
||
}
|
||
|
||
_dbusImpl = null;
|
||
|
||
// 1. Lifecycle dell'estensione
|
||
enable() {
|
||
|
||
this._settingsHandler = [];
|
||
|
||
this._settings = this.getSettings();
|
||
|
||
this._validateSettings();
|
||
|
||
this._settingsHandler.push(
|
||
this._settings.connect('changed::show-in-panel', () => {
|
||
if (this._indicator) {
|
||
this._indicator._updateVisibility();
|
||
}
|
||
}),
|
||
this._settings.connect('changed::show-pin-icon', () => {
|
||
if (this._indicator) {
|
||
this._indicator._updatePinIconVisibility();
|
||
}
|
||
})
|
||
);
|
||
|
||
this._sessionConnection = Gio.DBus.session;
|
||
this._sessionWatcher = this._sessionConnection.watch_name(
|
||
'org.gnome.Shell',
|
||
Gio.BusNameWatcherFlags.NONE,
|
||
() => { },
|
||
() => this.disable()
|
||
);
|
||
|
||
const dbusInterface = `
|
||
<node>
|
||
<interface name="org.gnome.Shell.Extensions.AppPinner">
|
||
<method name="LaunchPosition">
|
||
<arg type="u" name="position" direction="in"/>
|
||
</method>
|
||
</interface>
|
||
</node>`;
|
||
|
||
if (!this._dbusImpl) {
|
||
try {
|
||
this._dbusImpl = Gio.DBusExportedObject.wrapJSObject(dbusInterface, this);
|
||
this._dbusImpl.export(Gio.DBus.session, '/org/gnome/Shell/Extensions/AppPinner');
|
||
} catch (e) {
|
||
console.error('Errore registrazione D-Bus:', e.message);
|
||
}
|
||
}
|
||
|
||
if (!this._settings.settings_schema.get_key('show-in-panel')) {
|
||
this._settings.set_boolean('show-in-panel', true);
|
||
}
|
||
|
||
this._positionHandler = this._settings.connect(
|
||
'changed::position-in-panel',
|
||
() => this._safeRecreateIndicator()
|
||
);
|
||
|
||
this._keybindings = [];
|
||
for (let i = 1; i <= 10; i++) {
|
||
const shortcut = this._settings.get_string(`shortcut-${i}`);
|
||
if (shortcut) {
|
||
this._addKeybinding(i);
|
||
}
|
||
}
|
||
|
||
this._shortcutHandlers = [];
|
||
for (let i = 1; i <= 10; i++) {
|
||
const handlerId = this._settings.connect(`changed::shortcut-${i}`, () => this._updateKeybinding(i));
|
||
this._shortcutHandlers.push(handlerId);
|
||
}
|
||
|
||
this._safeRecreateIndicator();
|
||
|
||
this._settings.connect('changed::startup-apps', () => this._syncAutostart());
|
||
this._syncAutostart();
|
||
|
||
this._startupChangedId = this._settings.connect(
|
||
'changed::startup-apps',
|
||
() => {
|
||
this._addTimeout(500, () => {
|
||
console.log("[DEBUG] Startup apps changed!");
|
||
this._syncAutostart();
|
||
return GLib.SOURCE_REMOVE;
|
||
});
|
||
}
|
||
);
|
||
|
||
this._settings.connect('changed::pinned-apps', () => {
|
||
this._cleanOrphanedStartupApps();
|
||
this._syncAutostart();
|
||
});
|
||
|
||
this._settings.connect('changed::startup-apps', () => this._syncAutostart());
|
||
|
||
this._cleanOrphanedStartupApps();
|
||
this._syncAutostart();
|
||
|
||
this._settings.connect('changed::pinned-apps', () => {
|
||
this._cleanOrphanedStartupApps();
|
||
this._syncAutostart();
|
||
});
|
||
|
||
this._addTimeout(2000, () => {
|
||
this._syncAutostart();
|
||
return GLib.SOURCE_REMOVE;
|
||
});
|
||
|
||
this._indicator._checkVisibility();
|
||
|
||
}
|
||
|
||
disable() {
|
||
|
||
if (this._sessionWatcherId) {
|
||
this._sessionConnection.unwatch_name(this._sessionWatcherId);
|
||
this._sessionWatcherId = null;
|
||
}
|
||
|
||
if (this._dbusImpl) {
|
||
try {
|
||
this._dbusImpl.unexport();
|
||
} catch (e) {
|
||
console.error('DBus unexport error:', e);
|
||
}
|
||
this._dbusImpl = null;
|
||
}
|
||
|
||
|
||
for (let i = 1; i <= 10; i++) {
|
||
this._removeKeybinding(i);
|
||
}
|
||
|
||
const mediaKeysSettings = new Gio.Settings({
|
||
schema_id: 'org.gnome.settings-daemon.plugins.media-keys'
|
||
});
|
||
|
||
const currentPaths = mediaKeysSettings.get_strv('custom-keybindings');
|
||
const newPaths = currentPaths.filter(p => !p.includes('app-pinner-'));
|
||
mediaKeysSettings.set_strv('custom-keybindings', newPaths);
|
||
|
||
if (this._positionHandler) {
|
||
this._settings.disconnect(this._positionHandler);
|
||
this._positionHandler = null;
|
||
}
|
||
|
||
this._keybindings.forEach(key => Main.wm.removeKeybinding(key));
|
||
this._keybindings = [];
|
||
|
||
if (this._indicator) {
|
||
const parent = this._indicator.get_parent();
|
||
if (parent) {
|
||
parent.remove_child(this._indicator);
|
||
}
|
||
this._indicator.destroy();
|
||
this._indicator = null;
|
||
}
|
||
|
||
if (this._timeoutIds) {
|
||
this._timeoutIds.forEach(id => GLib.Source.remove(id));
|
||
this._timeoutIds.clear();
|
||
}
|
||
|
||
this._settings.disconnect(this._shortcutHandler);
|
||
|
||
const autostartDir = GLib.build_filenamev([GLib.get_user_config_dir(), 'autostart']);
|
||
const dir = Gio.File.new_for_path(autostartDir);
|
||
|
||
const enumerator = dir.enumerate_children('standard::name', Gio.FileQueryInfoFlags.NONE, null);
|
||
let fileInfo;
|
||
while ((fileInfo = enumerator.next_file(null)) !== null) {
|
||
const name = fileInfo.get_name();
|
||
if (name.startsWith('app-pinner-')) {
|
||
GLib.unlink(`${autostartDir}/${name}`);
|
||
}
|
||
}
|
||
|
||
if (this._startupChangedId) {
|
||
this._settings.disconnect(this._startupChangedId);
|
||
}
|
||
|
||
try {
|
||
const enumerator = dir.enumerate_children('standard::name', Gio.FileQueryInfoFlags.NONE, null);
|
||
let fileInfo;
|
||
while ((fileInfo = enumerator.next_file(null)) !== null) {
|
||
const name = fileInfo.get_name();
|
||
if (name.startsWith('app-pinner-')) {
|
||
GLib.unlink(`${autostartDir}/${name}`);
|
||
}
|
||
}
|
||
} catch (e) {
|
||
console.error('Errore pulizia autostart:', e);
|
||
}
|
||
|
||
if (this._settingsHandler) {
|
||
this._settingsHandler.forEach(handler => {
|
||
if (handler) {
|
||
this._settings.disconnect(handler);
|
||
}
|
||
});
|
||
this._settingsHandler = null;
|
||
}
|
||
|
||
if (this._indicator && this._indicator._checkVisibility) {
|
||
GLib.Source.remove(this._indicator._checkVisibilityTimeout);
|
||
}
|
||
|
||
this._settings = null
|
||
}
|
||
|
||
_updateKeybinding(position) {
|
||
const shortcut = this._settings.get_string(`shortcut-${position}`);
|
||
if (shortcut) {
|
||
this._addKeybinding(position);
|
||
} else {
|
||
this._removeKeybinding(position);
|
||
}
|
||
}
|
||
|
||
// 2. Gestione D-Bus e keybindings
|
||
_addKeybinding(position) {
|
||
const key = `shortcut-${position}`;
|
||
const shortcut = this._settings.get_string(key);
|
||
|
||
if (!shortcut) return;
|
||
|
||
const customPath = `/org/gnome/settings-daemon/plugins/media-keys/custom-keybindings/app-pinner-${position}/`;
|
||
|
||
const customSettings = new Gio.Settings({
|
||
schema_id: 'org.gnome.settings-daemon.plugins.media-keys.custom-keybinding',
|
||
path: customPath
|
||
});
|
||
|
||
customSettings.set_string('name', `App Pinner Position ${position}`);
|
||
customSettings.set_string('command', `dbus-send --session --type=method_call --dest=org.gnome.Shell /org/gnome/Shell/Extensions/AppPinner org.gnome.Shell.Extensions.AppPinner.LaunchPosition uint32:${position}`);
|
||
customSettings.set_string('binding', shortcut);
|
||
|
||
const mediaKeysSettings = new Gio.Settings({
|
||
schema_id: 'org.gnome.settings-daemon.plugins.media-keys'
|
||
});
|
||
|
||
const currentPaths = mediaKeysSettings.get_strv('custom-keybindings');
|
||
if (!currentPaths.includes(customPath)) {
|
||
mediaKeysSettings.set_strv('custom-keybindings', [...currentPaths, customPath]);
|
||
}
|
||
}
|
||
|
||
LaunchPosition(position) {
|
||
this._launchAppByPosition(position);
|
||
}
|
||
|
||
_launchAppByPosition(position) {
|
||
|
||
const apps = this._settings.get_strv('pinned-apps');
|
||
|
||
if (apps.length === 0) return;
|
||
|
||
const index = position - 1;
|
||
if (index < 0 || index >= apps.length) return;
|
||
|
||
const appId = apps[index];
|
||
|
||
if (appId.startsWith('link://')) {
|
||
if (!this._indicator) return;
|
||
|
||
this._indicator._launchApp(appId);
|
||
return;
|
||
}
|
||
|
||
const appSys = Shell.AppSystem.get_default();
|
||
let app = appSys.lookup_app(`${appId}.desktop`) || appSys.lookup_app(appId);
|
||
|
||
if (!app) {
|
||
const gioApp = Gio.DesktopAppInfo.new(`${appId}.desktop`) || Gio.DesktopAppInfo.new(appId);
|
||
|
||
if (!gioApp) {
|
||
console.error(`[ERROR] App ${appId} non trovata in nessun formato`);
|
||
return;
|
||
}
|
||
} else {
|
||
}
|
||
|
||
this._indicator._launchApp(appId);
|
||
}
|
||
|
||
// 3. Gestione autostart
|
||
_syncAutostart() {
|
||
const autostartDir = GLib.build_filenamev([GLib.get_user_config_dir(), 'autostart']);
|
||
const startupApps = this._settings.get_strv('startup-apps');
|
||
|
||
if (!GLib.file_test(autostartDir, GLib.FileTest.IS_DIR)) {
|
||
GLib.mkdir_with_parents(autostartDir, 0o755);
|
||
}
|
||
|
||
const validFiles = new Map();
|
||
|
||
startupApps.forEach(entry => {
|
||
if (entry.startsWith('link://')) {
|
||
const url = entry.replace('link://', '');
|
||
const sanitized = this._sanitizeForFilename(url);
|
||
const fileName = `app-pinner-link-${sanitized}.desktop`;
|
||
validFiles.set(fileName, true);
|
||
|
||
const content = [
|
||
'[Desktop Entry]',
|
||
'Type=Application',
|
||
`Name=Link ${url}`,
|
||
`Exec=xdg-open "${url}"`,
|
||
'Icon=emblem-web-symbolic',
|
||
'X-GNOME-Autostart-enabled=true',
|
||
'X-GNOME-Autostart-Delay=2',
|
||
'NoDisplay=false',
|
||
''
|
||
].join('\n');
|
||
|
||
try {
|
||
GLib.file_set_contents(`${autostartDir}/${fileName}`, content);
|
||
GLib.chmod(`${autostartDir}/${fileName}`, 0o644);
|
||
} catch (e) {
|
||
console.error(`Errore scrittura link ${url}:`, e.message);
|
||
}
|
||
} else {
|
||
const appInfo = this._getAppInfo(entry);
|
||
if (!appInfo) {
|
||
console.error(`[ERROR] App non trovata: ${entry}`);
|
||
return;
|
||
}
|
||
|
||
const desktopId = appInfo.get_id() || `${entry}.desktop`;
|
||
const sanitizedId = this._sanitizeForFilename(desktopId);
|
||
const fileName = `app-pinner-${sanitizedId}.desktop`;
|
||
validFiles.set(fileName, true);
|
||
|
||
const execCommand = appInfo.get_string('Exec');
|
||
if (!execCommand) {
|
||
console.error(`[ERROR] Comando non trovato per ${entry}`);
|
||
return;
|
||
}
|
||
|
||
const desktopContent = [
|
||
'[Desktop Entry]',
|
||
'Type=Application',
|
||
`Name=${appInfo.get_name()} (App Pinner)`,
|
||
`Exec=${execCommand}`,
|
||
'X-GNOME-Autostart-enabled=true',
|
||
'X-GNOME-Autostart-Delay=2',
|
||
'Icon=' + (appInfo.get_icon() || ''),
|
||
'Comment=Avviato automaticamente da App Pinner',
|
||
'NoDisplay=false',
|
||
''
|
||
].join('\n');
|
||
|
||
try {
|
||
GLib.file_set_contents(`${autostartDir}/${fileName}`, desktopContent);
|
||
GLib.chmod(`${autostartDir}/${fileName}`, 0o644);
|
||
} catch (e) {
|
||
console.error(`Errore scrittura app ${entry}:`, e.message);
|
||
}
|
||
}
|
||
});
|
||
|
||
const dir = Gio.File.new_for_path(autostartDir);
|
||
try {
|
||
const enumerator = dir.enumerate_children('standard::name', Gio.FileQueryInfoFlags.NONE, null);
|
||
let fileInfo;
|
||
while ((fileInfo = enumerator.next_file(null))) {
|
||
const fileName = fileInfo.get_name();
|
||
if (fileName.startsWith('app-pinner-') && !validFiles.has(fileName)) {
|
||
GLib.unlink(`${autostartDir}/${fileName}`);
|
||
}
|
||
}
|
||
} catch (e) {
|
||
console.error('Errore durante la pulizia dei file:', e.message);
|
||
}
|
||
}
|
||
|
||
// 4. Funzioni helper e validazione
|
||
_validateSettings() {
|
||
const currentPos = this._settings.get_string('position-in-panel');
|
||
if (!['left', 'right'].includes(currentPos)) {
|
||
this._settings.set_string('position-in-panel', 'right');
|
||
}
|
||
}
|
||
|
||
_getAppInfo(appId) {
|
||
console.log(`[DEBUG] Getting app info for: ${appId}`);
|
||
|
||
const appSys = Shell.AppSystem.get_default();
|
||
const allApps = appSys.get_installed();
|
||
const foundApp = allApps.find(app =>
|
||
app.get_id().toLowerCase() === appId.toLowerCase()
|
||
);
|
||
|
||
if (foundApp) {
|
||
return Gio.DesktopAppInfo.new(foundApp.get_id());
|
||
}
|
||
|
||
const flatpakPaths = [
|
||
'/var/lib/flatpak/exports/share/applications/',
|
||
`${GLib.get_home_dir()}/.local/share/flatpak/exports/share/applications/`
|
||
];
|
||
|
||
for (const path of flatpakPaths) {
|
||
const fullPath = `${path}${appId}.desktop`;
|
||
if (GLib.file_test(fullPath, GLib.FileTest.EXISTS)) {
|
||
return Gio.DesktopAppInfo.new_from_filename(fullPath);
|
||
}
|
||
}
|
||
|
||
const dataDirs = GLib.get_system_data_dirs();
|
||
for (const dataDir of dataDirs) {
|
||
const appPath = `${dataDir}/applications/${appId}.desktop`;
|
||
if (GLib.file_test(appPath, GLib.FileTest.EXISTS)) {
|
||
return Gio.DesktopAppInfo.new_from_filename(appPath);
|
||
}
|
||
}
|
||
|
||
return null;
|
||
}
|
||
|
||
_sanitizeForFilename(appId) {
|
||
return appId
|
||
.replace(/\.desktop$/i, '')
|
||
.replace(/[^a-zA-Z0-9-]/g, '_')
|
||
.replace(/_+/g, '_')
|
||
.substring(0, 50);
|
||
}
|
||
|
||
_cleanOrphanedStartupApps() {
|
||
const pinnedApps = this._settings.get_strv('pinned-apps');
|
||
const customLinks = this._settings.get_strv('custom-links');
|
||
const startupApps = this._settings.get_strv('startup-apps');
|
||
|
||
const validEntries = new Set([
|
||
...pinnedApps,
|
||
...customLinks.map(url => `link://${url}`)
|
||
]);
|
||
|
||
const cleaned = startupApps.filter(appId =>
|
||
validEntries.has(appId) ||
|
||
customLinks.includes(appId.replace('link://', ''))
|
||
);
|
||
|
||
if (!arraysEqual(cleaned, startupApps)) {
|
||
this._settings.set_strv('startup-apps', cleaned);
|
||
}
|
||
}
|
||
|
||
_removeKeybinding(position) {
|
||
const customPath = `/org/gnome/settings-daemon/plugins/media-keys/custom-keybindings/app-pinner-${position}/`;
|
||
|
||
const mediaKeysSettings = new Gio.Settings({
|
||
schema_id: 'org.gnome.settings-daemon.plugins.media-keys'
|
||
});
|
||
|
||
const currentPaths = mediaKeysSettings.get_strv('custom-keybindings');
|
||
if (currentPaths.includes(customPath)) {
|
||
const newPaths = currentPaths.filter(p => p !== customPath);
|
||
mediaKeysSettings.set_strv('custom-keybindings', newPaths);
|
||
|
||
const customSettings = new Gio.Settings({
|
||
schema_id: 'org.gnome.settings-daemon.plugins.media-keys.custom-keybinding',
|
||
path: customPath
|
||
});
|
||
customSettings.reset('name');
|
||
customSettings.reset('command');
|
||
customSettings.reset('binding');
|
||
}
|
||
}
|
||
|
||
_showMaxItemsError() {
|
||
Main.notifyError(
|
||
_("Maximum items reached"),
|
||
_("You can pin up to 10 items maximum")
|
||
);
|
||
}
|
||
|
||
_validateAccelerator(accelerator) {
|
||
if (!accelerator) return false;
|
||
|
||
try {
|
||
const [key, mods] = Clutter.accelerator_parse(accelerator);
|
||
return key !== 0;
|
||
} catch (e) {
|
||
return false;
|
||
}
|
||
}
|
||
|
||
_safeRecreateIndicator() {
|
||
const iconSize = this._settings.get_int('icon-size');
|
||
const spacing = this._settings.get_int('spacing');
|
||
const labels = this._settings.get_boolean('enable-labels');
|
||
|
||
this._recreateIndicator();
|
||
|
||
this._settings.set_int('icon-size', iconSize);
|
||
this._settings.set_int('spacing', spacing);
|
||
this._settings.set_boolean('enable-labels', labels);
|
||
}
|
||
|
||
_recreateIndicator() {
|
||
if (this._indicator) {
|
||
this._indicator.destroy();
|
||
this._indicator = null;
|
||
}
|
||
|
||
this._indicator = new AppPinner(this._settings);
|
||
const position = this._settings.get_string('position-in-panel');
|
||
|
||
if (this._indicator.get_parent()) {
|
||
this._indicator.get_parent().remove_child(this._indicator);
|
||
}
|
||
|
||
switch (position) {
|
||
case 'left':
|
||
Main.panel._leftBox.insert_child_at_index(this._indicator, 0);
|
||
break;
|
||
|
||
case 'center':
|
||
if (Main.panel._centerBox) {
|
||
Main.panel._centerBox.add_child(this._indicator);
|
||
} else {
|
||
Main.panel._centerBox = new St.BoxLayout();
|
||
Main.panel._centerBox.x_align = Clutter.ActorAlign.CENTER;
|
||
Main.panel.insert_child_at_index(Main.panel._centerBox, 1);
|
||
Main.panel._centerBox.add_child(this._indicator);
|
||
}
|
||
break;
|
||
|
||
case 'right':
|
||
const dateMenu = Main.panel.statusArea?.dateMenu;
|
||
let targetIndex = 0;
|
||
if (dateMenu?.actor) {
|
||
const children = Main.panel._rightBox.get_children();
|
||
const dateMenuIndex = children.indexOf(dateMenu.actor);
|
||
targetIndex = dateMenuIndex !== -1 ? dateMenuIndex + 1 : 0;
|
||
}
|
||
Main.panel._rightBox.insert_child_at_index(this._indicator, targetIndex);
|
||
break;
|
||
}
|
||
|
||
this._indicator.set_style_class_name(
|
||
`app-pinner-position-${position}`
|
||
);
|
||
|
||
Main.panel.queue_relayout();
|
||
Main.panel.menuManager.addMenu(this._indicator.menu);
|
||
}
|
||
} |