diff --git a/quick-settings@celiopy/README.md b/quick-settings@celiopy/README.md new file mode 100644 index 00000000000..5a58c7ef1c6 --- /dev/null +++ b/quick-settings@celiopy/README.md @@ -0,0 +1,58 @@ +# Quick Settings (Cinnamon Applet) + +**Quick Settings** is a modern and fully redesigned rework of the original \[cinnamon-user-applet]. +It provides an elegant and customizable way to access your essential desktop settings directly from the Cinnamon panel. + +![Screenshot](./screenshot.png) + +--- + +## ✨ Features + +* **Quick toggles** for common preferences: + + * Dark mode + * Night light + * Prevent sleep (inhibit suspend) + * …and more +* **User avatar** with optional username and host display +* **Session controls** (logout, lock, shutdown, restart, etc.) +* **Highly customizable** via applet preferences +* Define **separate themes** for light and dark mode + +--- + +## ⚙️ Preferences + +All aspects of the applet can be tuned in its preferences dialog: + +* Choose which toggles to display +* Enable/disable user information +* Configure session buttons +* Set light and dark mode themes + +--- + +## 🚧 Status + +This applet is still in active development. +Some **bugs are known and currently being fixed**, so please expect improvements over time. + +--- + +## 📦 Installation + +1. Clone this repository into your Cinnamon applets directory: + + ```bash + git clone https://github.com/celiopy/cinnamon-user-applet + cp -r cinnamon-user-applet/files/user@celiopy ~/.local/share/cinnamon/applets/ + ``` +2. Enable it from **Cinnamon Settings → Applets**. +3. Configure it to your liking through the **Preferences** panel. + +--- + +## 🙌 Acknowledgements + +This project is based on and inspired by the original \[cinnamon-user-applet], but rebuilt and modernized with new design principles. \ No newline at end of file diff --git a/quick-settings@celiopy/files/quick-settings@celiopy/applet.js b/quick-settings@celiopy/files/quick-settings@celiopy/applet.js new file mode 100644 index 00000000000..1cbdec3eeb2 --- /dev/null +++ b/quick-settings@celiopy/files/quick-settings@celiopy/applet.js @@ -0,0 +1,876 @@ +const Applet = imports.ui.applet; +const { St, GLib, Gio, AccountsService, Clutter } = imports.gi; +const PopupMenu = imports.ui.popupMenu; +const Settings = imports.ui.settings; +const UserWidget = imports.ui.userWidget; +const Main = imports.ui.main; +const Tooltips = imports.ui.tooltips; +const GnomeSession = imports.misc.gnomeSession; +const ScreenSaver = imports.misc.screenSaver; +const Gettext = imports.gettext; +const Signals = imports.signals; // Add this line + +const UUID = 'quick-settings@celiopy'; + +const DIALOG_ICON_SIZE = 32; +const INHIBIT_IDLE_FLAG = 8; +const INHIBIT_SLEEP_FLAG = 4; + +// l10n/translation support +Gettext.bindtextdomain(UUID, GLib.get_home_dir() + "/.local/share/locale"); + +function _(str) { + return Gettext.dgettext(UUID, str); +} + +class ThemeManager { + constructor(schemas, settings) { + this._schemas = schemas; + this._settings = settings; + + this._themeDirs = [ + '/usr/share/themes', + GLib.get_home_dir() + '/.themes', + GLib.get_home_dir() + '/.local/share/themes', + ]; + this._monitors = []; + + // Initialize themes + this._lightTheme = settings.getValue("light-theme"); + this._darkTheme = settings.getValue("dark-theme"); + + // Use portal color-scheme as the single source of truth + this._darkMode = this._getDarkModeFromPortal(); + + // Bind to settings changes + this._bindings = []; + this._bindSettings(); + + // Monitor theme directory changes + this._themeDirs.forEach(dir => this._monitorDir(dir)); + + this._populateThemeOptions(); + } + + _getDarkModeFromPortal() { + const colorScheme = this._schemas.portal.get_string("color-scheme"); + return colorScheme === "prefer-dark"; + } + + _bindSettings() { + // Listen to portal color-scheme changes (our single source of truth) + this._bindings.push({ + obj: this._schemas.portal, + id: this._schemas.portal.connect("changed::color-scheme", () => { + this._darkMode = this._getDarkModeFromPortal(); + this._applyCurrentTheme(); + + // Emit a signal so the applet can update its UI + this.emit('dark-mode-changed', this._darkMode); + }) + }); + + // Bind light-theme setting + this._bindings.push({ + obj: this._settings, + id: this._settings.connect("changed::light-theme", () => { + this._lightTheme = this._settings.getValue("light-theme"); + this._applyCurrentTheme(); + }) + }); + + // Bind dark-theme setting + this._bindings.push({ + obj: this._settings, + id: this._settings.connect("changed::dark-theme", () => { + this._darkTheme = this._settings.getValue("dark-theme"); + this._applyCurrentTheme(); + }) + }); + } + + _applyCurrentTheme() { + let theme = this._darkMode ? this._darkTheme : this._lightTheme; + + // Apply the theme to all schemas + this._schemas.gtk.set_string("gtk-theme", theme); + this._schemas.cinnamon.set_string("gtk-theme", theme); + } + + _monitorDir(dirPath) { + try { + let file = Gio.file_new_for_path(dirPath); + let monitor = file.monitor_directory(Gio.FileMonitorFlags.NONE, null); + monitor.connect('changed', () => { + this._populateThemeOptions(); + }); + this._monitors.push(monitor); + } catch(e) { + global.logError(`Failed to monitor directory ${dirPath}: ${e}`); + } + } + + isDarkMode() { + return this._darkMode; + } + + setDarkMode(dark) { + // Update the portal setting (our single source of truth) + let colorScheme = dark ? "prefer-dark" : "default"; + this._schemas.portal.set_string("color-scheme", colorScheme); + + // Note: The actual _darkMode update will happen in the + // changed::color-scheme signal handler + } + + _populateThemeOptions() { + let themes = {}; + + const readThemesFromDir = (dir) => { + try { + let file = Gio.file_new_for_path(dir); + let enumerator = file.enumerate_children('standard::name', Gio.FileQueryInfoFlags.NONE, null); + + let info; + while ((info = enumerator.next_file(null))) { + let themeName = info.get_name(); + if (info.get_file_type() === Gio.FileType.DIRECTORY) { + themes[themeName] = themeName; + } + } + } catch (e) { + global.log(`Error reading themes from ${dir}: ${e.message}`); + } + }; + + this._themeDirs.forEach(dir => readThemesFromDir(dir)); + + this._settings.setOptions("light-theme", themes); + this._settings.setOptions("dark-theme", themes); + } + + // Clean up method to disconnect all signals + destroy() { + // Disconnect all setting bindings + this._bindings.forEach(binding => { + try { + binding.obj.disconnect(binding.id); + } catch (e) { + global.logError(`Error disconnecting binding: ${e}`); + } + }); + this._bindings = []; + + // Stop all directory monitors + this._monitors.forEach(monitor => { + try { + monitor.cancel(); + } catch (e) { + global.logError(`Error canceling monitor: ${e}`); + } + }); + this._monitors = []; + } +} + +// Add signal support to ThemeManager +Signals.addSignalMethods(ThemeManager.prototype); + +const BASE_BUTTON_DEFAULT_PARAMS = Object.freeze({ + name: '', + description: '', + styleClass: 'settings-toggle-box', + reactive: true, + activatable: true, + withMenu: false, +}); + +class BaseButton { + constructor(applet, params) { + this.applet = applet; + params = Object.assign({}, BASE_BUTTON_DEFAULT_PARAMS, params); + + this._active = false; + + this.actor = new St.BoxLayout({ + style_class: params.styleClass, + reactive: params.reactive, + vertical: true, + x_align: Clutter.ActorAlign.CENTER, + y_align: Clutter.ActorAlign.CENTER, + x_expand: false, + y_expand: false + }); + + this._container = new St.BoxLayout({ + style_class: 'settings-toggle-button', + x_align: Clutter.ActorAlign.CENTER, + y_align: Clutter.ActorAlign.CENTER, + x_expand: false, + y_expand: false + }); + + this.actor._delegate = this; + this.name = params.name; + this.description = params.description; + + this.label = null; + this.icon = null; + + if (params.reactive) { + if (params.activatable || params.withMenu) { + this.actor.connect('button-press-event', (actor, event) => this._onButtonPressEvent(actor, event)); + this.actor.connect('key-press-event', (actor, event) => this._onKeyPressEvent(actor, event)); + } + } + } + + set active(value) { + this._active = value; + // Adiciona ou remove a classe CSS + if (value) this._container.add_style_class_name('active'); + else this._container.remove_style_class_name('active'); + } + + get active() { + return this._active; + } + + _onButtonPressEvent(actor, event) { + const button = event.get_button(); + + if (button === Clutter.BUTTON_PRIMARY) { + if (this.onLeftClick) this.onLeftClick(this._active); + return Clutter.EVENT_STOP; + } + else if (button === Clutter.BUTTON_MIDDLE) { + if (this.onMiddleClick) this.onMiddleClick(this._active); + return Clutter.EVENT_STOP; + } + else if (button === Clutter.BUTTON_SECONDARY) { + if (this.populateMenu && this.applet) this.applet.toggleContextMenu(this); + return Clutter.EVENT_STOP; + } + + return Clutter.EVENT_PROPAGATE; + } + + _onKeyPressEvent(actor, event) { + const symbol = event.get_key_symbol(); + if ((symbol === Clutter.KEY_space || symbol === Clutter.KEY_Return || symbol === Clutter.KEY_KP_Enter) && this.onLeftClick) { + this.onLeftClick(this._active); + return Clutter.EVENT_STOP; + } + return Clutter.EVENT_PROPAGATE; + } + + addIcon(iconSize, iconName='', gicon=null, symbolic=false) { + if (this.icon) return; + + const params = { icon_size: iconSize }; + if (iconName) params.icon_name = iconName; + else if (gicon) params.gicon = gicon; + params.icon_type = symbolic ? St.IconType.SYMBOLIC : St.IconType.FULLCOLOR; + + this.icon = new St.Icon(params); + + this._container.add_actor(this.icon); + this.actor.add_actor(this._container); + + this.buttonActor = this.icon; + } + + addLabel(label='', styleClass=null) { + if (this.label) return; + this.label = new St.Label({ + text: label, + y_expand: true, + x_align: Clutter.ActorAlign.CENTER, + }); + if (styleClass) this.label.set_style_class_name(styleClass); + this.actor.add_actor(this.label); + } + + destroy(actorDestroySignal=false) { + if (this.label) this.label.destroy(); + if (this.icon) this.icon.destroy(); + if (!actorDestroySignal) this.actor.destroy(); + + delete this.actor._delegate; + delete this.actor; + delete this.label; + delete this.icon; + } +} + +class ToggleButton extends BaseButton { + constructor(applet, iconName, labelText) { + super(applet, { name: labelText, styleClass: 'settings-toggle-box' }); + this.addIcon(24, iconName, null, true); + this.addLabel(labelText, 'settings-toggle-label'); + this.onLeftClick = null; + this.onMiddleClick = null; + this.active = false; // estado inicial + } +} + +class CinnamonUserApplet extends Applet.TextIconApplet { + constructor(orientation, panel_height, instance_id) { + super(orientation, panel_height, instance_id); + this.setAllowedLayout(Applet.AllowedLayout.BOTH); + + this._signals = []; + this.sessionCookie = null; + + this.set_applet_icon_symbolic_name('user-menu-symbolic'); + + // Mapa de modos + this._powerModes = { + "power-saver": { next: "balanced", label: _("Power Saver"), icon: "power-profile-power-saver" }, + "balanced": { next: "performance", label: _("Balanced"), icon: "power-profile-balanced" }, + "performance": { next: "power-saver", label: _("Performance"), icon: "power-profile-performance" } + }; + + // Inicializa schemas, bindings, UI e toggles + this._initSchemas(); + this.themeManager = new ThemeManager(this._schemas, this.settings); + this._initUI(orientation); + this._initPowerProfiles(); + + // Métodos iniciais + this._onUserChanged(); + this._setKeybinding(); + + } + + // === Inicializa schemas e bindings === + _initSchemas() { + // Schemas do applet + this.settings = new Settings.AppletSettings(this, UUID, this.instance_id); + this.settings.bind("keyOpen", "keyOpen", () => this._setKeybinding()); + this.settings.bind("display-name", "display_name", () => this._updateLabels()); + + // Schemas do sistema + this._schemas = { + color: new Gio.Settings({ schema_id: "org.cinnamon.settings-daemon.plugins.color" }), + interface: new Gio.Settings({ schema_id: "org.cinnamon.desktop.interface" }), + gtk: new Gio.Settings({ schema_id: "org.gnome.desktop.interface" }), + cinnamon: new Gio.Settings({ schema_id: "org.cinnamon.desktop.interface" }), + portal: new Gio.Settings({ schema_id: "org.x.apps.portal" }), + screensaver: new Gio.Settings({ schema_id: "org.cinnamon.desktop.screensaver" }) + }; + } + + _initPowerProfiles() { + this._powerProxy = new Gio.DBusProxy.new_for_bus_sync( + Gio.BusType.SYSTEM, + Gio.DBusProxyFlags.NONE, + null, + "net.hadess.PowerProfiles", + "/net/hadess/PowerProfiles", + "net.hadess.PowerProfiles", + null + ); + + // Atualiza inicialmente + this._updatePowerModeIcon(); + + // Escuta mudanças + this._signals.push({ + obj: this._powerProxy, + id: this._powerProxy.connect("g-properties-changed", () => { + this._updatePowerModeIcon(); + }) + }); + } + + // === Inicializa UI do menu e painel === + _initUI(orientation) { + // Sessão + this.sessionProxy = null; + this._session = new GnomeSession.SessionManager((proxy, error) => { + if (error) { + global.logError("Error initializing session proxy: " + error.message); + return; + } + this.sessionProxy = proxy; + global.log("Session proxy initialized successfully"); + }); + this._screenSaverProxy = new ScreenSaver.ScreenSaverProxy(); + + // Menu + this.menuManager = new PopupMenu.PopupMenuManager(this); + this.menu = new Applet.AppletPopupMenu(this, orientation); + this.menuManager.addMenu(this.menu); + + // Seções do menu + this.prefsSection = new PopupMenu.PopupMenuSection(); + this.interfaceSection = new PopupMenu.PopupMenuSection(); + this.sessionSection = new PopupMenu.PopupMenuSection(); + + this.menu.addMenuItem(this.prefsSection); + this.menu.addMenuItem(this.interfaceSection); + this.menu.addMenuItem(new PopupMenu.PopupSeparatorMenuItem()); + this.menu.addMenuItem(this.sessionSection); + + // Grid de toggles (3 colunas) + this.prefsGrid = new St.Widget({ + layout_manager: new Clutter.GridLayout(), + style_class: "prefs-grid-container", + x_expand: true, + x_align: Clutter.ActorAlign.FILL + }); + let gridLayout = this.prefsGrid.layout_manager; + gridLayout.set_column_spacing(16); + gridLayout.set_row_spacing(24); + gridLayout.set_column_homogeneous(true); + this.prefsSection.actor.add_child(this.prefsGrid); + + this.maxTogglesPerRow = 3; + this.currentRow = 0; + this.currentColumn = 0; + + // Sessão: container horizontal + this.sessionContainer = new St.BoxLayout({ + style_class: "session-container", + vertical: false, + x_expand: true + }); + this.sessionSection.actor.add_child(this.sessionContainer); + + // User info + this._user = AccountsService.UserManager.get_default().get_user(GLib.get_user_name()); + this._signals.push({ + obj: this._user, + id: this._user.connect('notify::is-loaded', this._onUserChanged.bind(this)) + }); + + this._signals.push({ + obj: this._user, + id: this._user.connect('changed', this._onUserChanged.bind(this)) + }); + + let userBox = new St.BoxLayout({ style_class: 'user-box', vertical: false, y_expand: false }); + // Avatar dentro de botão + this._userButton = new St.Button({ + style_class: "user-avatar-button", + reactive: true, + can_focus: true, + track_hover: true + }); + + // Coloca o avatar dentro do botão + this._userIcon = new UserWidget.Avatar(this._user, { iconSize: DIALOG_ICON_SIZE }); + this._userIcon.style = `icon-size: ${DIALOG_ICON_SIZE}px;`; + this._userButton.set_child(this._userIcon); + + // Adiciona o botão no userBox + userBox.add(this._userButton); + + // Clique no avatar abre "Detalhes da conta" + this._userButton.connect('clicked', () => { + GLib.spawn_command_line_async("cinnamon-settings user"); + }); + + this._labelBox = new St.BoxLayout({ style_class: 'label-box', vertical: true, y_align: Clutter.ActorAlign.CENTER }); + this.userLabel = new St.Label({ style_class: 'user-label' }); + this.hostLabel = new St.Label({ style_class: 'host-label' }); + this._labelBox.add(this.userLabel); + this._labelBox.add(this.hostLabel); + userBox.add(this._labelBox); + + // Adiciona user info e spacer + this.sessionContainer.add_child(userBox); + this.sessionContainer.add_child(new St.BoxLayout({ x_expand: true })); // Spacer + + // Session buttons + this.sessionButtonsBox = new St.BoxLayout({ style_class: 'session-buttons-box', vertical: false, x_align: Clutter.ActorAlign.END, y_align: Clutter.ActorAlign.CENTER }); + this.sessionContainer.add_child(this.sessionButtonsBox); + + this._initSessionButtons(); + this._initToggles(); + this._initTextScaling(); + } + + // === Inicializa session buttons === + _initSessionButtons() { + const addBtn = (iconName, tooltip, callback) => { + let btn = new St.Button({ style_class: "system-button", reactive: true, can_focus: true, track_hover: true }); + let icon = new St.Icon({ icon_name: iconName, icon_type: St.IconType.SYMBOLIC, style_class: "system-status-icon" }); + btn.set_child(icon); + new Tooltips.Tooltip(btn, tooltip); + btn.connect("clicked", () => { + callback(); + this.menu.toggle(); + }); + this.sessionButtonsBox.add_child(btn); + }; + + // System Settings + addBtn("applications-system", _("Settings"), () => { + GLib.spawn_command_line_async("cinnamon-settings"); + }); + + // Lock + addBtn("system-lock-screen", _("Lock Screen"), () => { + let screensaver_file = Gio.file_new_for_path("/usr/bin/cinnamon-screensaver-command"); + if (screensaver_file.query_exists(null)) { + let ask = this._schemas.screensaver.get_boolean("ask-for-away-message"); + GLib.spawn_command_line_async(ask ? "cinnamon-screensaver-lock-dialog" : "cinnamon-screensaver-command --lock"); + } else { + this._screenSaverProxy.LockRemote(); + } + }); + + // Logout + addBtn("system-log-out", _("Log Out"), () => this._session.LogoutRemote(0)); + + // Shutdown + addBtn("system-shutdown", _("Shut Down"), () => this._session.ShutdownRemote()); + } + + // === Inicializa toggles do applet === + _initToggles() { + // Dark Mode + this.darkModeToggle = new ToggleButton(this, "weather-clear-night-symbolic", _("Dark mode")); + this.darkModeToggle.active = this.themeManager.isDarkMode(); + this.darkModeToggle.onLeftClick = () => { + // This will now update the portal setting, which will trigger the signal + this.themeManager.setDarkMode(!this.themeManager.isDarkMode()); + }; + this.darkModeToggle.onMiddleClick = () => { + GLib.spawn_command_line_async("cinnamon-settings themes"); + this._openMenu(); + } + this._addToggleToGrid(this.darkModeToggle.actor); + + this.themeManager.connect('dark-mode-changed', (manager, darkMode) => { + this.darkModeToggle.active = darkMode; + }); + + // Night Light + this.nightLightToggle = new ToggleButton(this, "night-light-symbolic", _("Night Light")); + this.nightLightToggle.active = this._schemas.color.get_boolean("night-light-enabled"); + this.nightLightToggle.onLeftClick = () => { + this.nightLightToggle.active = !this.nightLightToggle.active; + this._schemas.color.set_boolean("night-light-enabled", this.nightLightToggle.active); + }; + this.nightLightToggle.onMiddleClick = () => { + GLib.spawn_command_line_async("cinnamon-settings nightlight"); + this._openMenu(); + }; + this._addToggleToGrid(this.nightLightToggle.actor); + + // Conecta para sincronizar mudanças externas + this._signals.push({ + obj: this._schemas.color, + id: this._schemas.color.connect("changed::night-light-enabled", (settings, key) => { + this.nightLightToggle.active = settings.get_boolean(key); + }) + }); + + // Prevent Sleep toggle + this.preventSleepToggle = new ToggleButton(this, "preferences-desktop-screensaver-symbolic", _("Prevent Sleep")); + this.preventSleepToggle.active = false; // estado inicial + this.preventSleepToggle.onLeftClick = () => { + this.preventSleepToggle.active = !this.preventSleepToggle.active; + this._togglePreventSleep(this.preventSleepToggle.active); + } + this.preventSleepToggle.onMiddleClick = () => { + GLib.spawn_command_line_async("cinnamon-settings power"); + this._openMenu() + } + this._addToggleToGrid(this.preventSleepToggle.actor); + + // Power Mode toggle + this.powerModeToggle = new ToggleButton(this, "power-profile-balanced", _("Power Mode")); + this.powerModeToggle.onLeftClick = () => this._togglePowerMode(); + this.powerModeToggle.onMiddleClick = () => GLib.spawn_command_line_async("cinnamon-settings power"); + this._addToggleToGrid(this.powerModeToggle.actor); + + // Inicializa estado ao iniciar (atualiza ícone e label conforme powerprofilesctl) + this._updatePowerModeIcon(); + } + + // === Adiciona toggle ao grid === + _addToggleToGrid(toggleActor) { + let row = Math.floor(this.prefsGrid.get_children().length / this.maxTogglesPerRow); + let col = this.prefsGrid.get_children().length % this.maxTogglesPerRow; + this.prefsGrid.layout_manager.attach(toggleActor, col, row, 1, 1); + } + + // === Inicializa slider de text scaling === + _initTextScaling() { + const FACTORS = [0.9, 1.0, 1.1, 1.2, 1.3]; + const MAX_INDEX = FACTORS.length - 1; + + let currentFactor = this._schemas.interface.get_double("text-scaling-factor"); + let idx = FACTORS.indexOf(currentFactor); + if (idx < 0) idx = 1; + + // Container do slider + let fakeSlider = new St.BoxLayout({ + style_class: "fake-slider", + vertical: false, + x_expand: true, + y_align: Clutter.ActorAlign.CENTER, + x_align: Clutter.ActorAlign.FILL + }); + + // Track em grid + let track = new St.Widget({ + style_class: "fake-slider-track", + x_expand: true, + y_expand: true, + layout_manager: new Clutter.GridLayout() + }); + let grid = track.layout_manager; + grid.set_row_homogeneous(true); + grid.set_column_homogeneous(true); + + // Fill do slider + let fill = new St.BoxLayout({ style_class: "fake-slider-fill" }); + + // Atualiza fill conforme índice + const updateFakeSlider = (idx) => { + track.remove_all_children(); + + let weight = (idx + 1); + + // Fill ocupa "weight" colunas + track.add_child(fill); + grid.attach(fill, 0, 0, weight, 1); + + // Spacer ocupa o resto + if (weight < FACTORS.length) { + let spacer = new St.BoxLayout({ x_expand: true }); + track.add_child(spacer); + grid.attach(spacer, weight, 0, FACTORS.length - weight, 1); + } + + // === Marcador fixo no 1.0 === + let markerIndex = FACTORS.indexOf(1.0); + if (markerIndex >= 0) { + let marker = new St.BoxLayout({ + style_class: "fake-slider-marker", + x_expand: false, + y_expand: false, + x_align: Clutter.ActorAlign.END + }); + track.add_child(marker); + grid.attach(marker, markerIndex, 0, 1, 1); + } + }; + + fakeSlider.add_child(track); + + // Função para alterar escala + const setScale = (newIdx) => { + newIdx = Math.max(0, Math.min(MAX_INDEX, newIdx)); + idx = newIdx; + let factor = FACTORS[idx]; + this._schemas.interface.set_double("text-scaling-factor", factor); + updateFakeSlider(idx); + }; + + // Escuta mudanças externas do text-scaling-factor + this._signals.push({ + obj: this._schemas.interface, + id: this._schemas.interface.connect("changed::text-scaling-factor", () => { + let f = this._schemas.interface.get_double("text-scaling-factor"); + let i = FACTORS.indexOf(f); + if (i >= 0) { + idx = i; + updateFakeSlider(idx); + } + }) + }); + + // Botões de menos/mais + let minusBtn = new St.Button({ style_class: "system-button", reactive: true, can_focus: true, track_hover: true }); + minusBtn.set_child(new St.Icon({ icon_name: 'format-text-size-decrease-symbolic', icon_type: St.IconType.SYMBOLIC, style_class: "system-status-icon" })); + minusBtn.connect("clicked", () => setScale(idx - 1)); + + let plusBtn = new St.Button({ style_class: "system-button icon-large", reactive: true, can_focus: true, track_hover: true }); + plusBtn.set_child(new St.Icon({ icon_name: 'format-text-size-increase-symbolic', icon_type: St.IconType.SYMBOLIC, style_class: "system-status-icon" })); + plusBtn.connect("clicked", () => setScale(idx + 1)); + + // Container final + let scalingContainer = new St.BoxLayout({ style_class: "scaling-container", vertical: false, x_expand: true }); + scalingContainer.add_child(minusBtn); + scalingContainer.add_child(fakeSlider); + scalingContainer.add_child(plusBtn); + + this.interfaceSection.actor.add_child(scalingContainer); + + updateFakeSlider(idx); + } + + // === Toggle Prevent Sleep === + _togglePreventSleep(active) { + if (active) { + // Activate prevent sleep + this.sessionProxy.InhibitRemote( + "inhibit@cinnamon.org", + 0, + "prevent system sleep and suspension", + INHIBIT_SLEEP_FLAG, + (cookie) => { + this.sessionCookie = cookie; + global.log("Prevent sleep activated, cookie: " + cookie); + this.preventSleepToggle.button.checked = true; + } + ); + } else if (this.sessionCookie) { + // Deactivate prevent sleep + this.sessionProxy.UninhibitRemote(this.sessionCookie, () => { + global.log("Prevent sleep deactivated"); + this.sessionCookie = null; + if (this.preventSleepToggle) { + this.preventSleepToggle.button.checked = false; + } + }); + } else { + // No cookie to uninhibit, just update UI + this.preventSleepToggle.button.checked = false; + } + } + + // === Keybinding === + _setKeybinding() { + if (this.keybindingId) { + Main.keybindingManager.removeHotKey("user-applet-open-" + this.instance_id); + } + Main.keybindingManager.addHotKey("user-applet-open-" + this.instance_id, this.keyOpen, () => this._openMenu()); + } + + _openMenu() { this.menu.toggle(); } + + // === Atualiza labels e avatar === + _updateLabels() { + this.set_applet_label(""); + + if (this.display_name) { + // === UserBox === + this.userLabel.set_text(this._user.get_real_name()); + this.hostLabel.set_text(`${GLib.get_user_name()}@${GLib.get_host_name()}`); + this._labelBox.show(); + } else { + // === UserBox === + this.userLabel.set_text(""); + this.hostLabel.set_text(""); + this._labelBox.hide(); + } + } + + _onUserChanged() { + if (this._user && this._user.is_loaded) { + this.set_applet_tooltip(this._user.get_real_name()); + + if (this.display_name) { + let hostname = GLib.get_host_name(); + this.hostLabel.set_text(`${GLib.get_user_name()}@${hostname}`); + this.userLabel.set_text(this._user.get_real_name()); + } + + this._userIcon.update(); + this._updateLabels(); + } + } + + // Alterna entre os modos + // Toggle + _togglePowerMode() { + this._getCurrentPowerMode((current) => { + let mode = this._powerModes[current] || this._powerModes["balanced"]; + + // Muda o modo de energia + GLib.spawn_command_line_async(`powerprofilesctl set ${mode.next}`); + this._updatePowerModeIcon(); + }); + } + + // Atualiza ícone externo + _updatePowerModeIcon() { + this._getCurrentPowerMode((current) => { + let mode = this._powerModes[current] || this._powerModes["balanced"]; + + this.powerModeToggle.active = (current === "performance"); + + // Atualiza ícone e label + this.powerModeToggle.icon.icon_name = mode?.icon; + this.powerModeToggle.label.set_text(mode?.label); + + // Atualiza estado ativo do toggle conforme performance + }); + } + + // Obtém modo atual do powerprofilesctl + _getCurrentPowerMode(callback) { + try { + let proc = new Gio.Subprocess({ + argv: ["powerprofilesctl", "get"], + flags: Gio.SubprocessFlags.STDOUT_PIPE | Gio.SubprocessFlags.STDERR_PIPE + }); + + proc.init(null); + proc.communicate_utf8_async(null, null, (proc, res) => { + try { + let [, stdout, stderr] = proc.communicate_utf8_finish(res); + if (stdout && stdout.trim().length > 0) { + callback(stdout.trim()); + } else { + callback("balanced"); // fallback + } + } catch (e) { + global.logError("Erro ao processar power mode: " + e); + callback("balanced"); + } + }); + } catch(e) { + global.logError("Erro ao iniciar subprocesso: " + e); + callback("balanced"); + } + } + + on_applet_clicked() { this._openMenu(); } + + on_applet_removed_from_panel() { + // Clean up theme manager + if (this.themeManager) { + this.themeManager.destroy(); + this.themeManager = null; + } + + // Clean up inhibit cookie + if (this.sessionCookie !== null && this.sessionProxy) { + try { + this.sessionProxy.UninhibitRemote(this.sessionCookie); + global.log("Cleaned up prevent sleep cookie: " + this.sessionCookie); + } catch(e) { + global.logError("Erro ao limpar inhibit cookie: " + e); + } + } + + // Desconectar todos os sinais registrados + this._signals.forEach(sig => { + try { sig.obj.disconnect(sig.id); } catch(e) {} + }); + this._signals = null; + + // Remover keybinding + Main.keybindingManager.removeHotKey("user-applet-open-" + this.instance_id); + + // Finalizar settings + this.settings.finalize(); + + // Liberar proxies + this._powerProxy = null; + this.sessionProxy = null; + this._screenSaverProxy = null; + } +} + +function main(metadata, orientation, panel_height, instance_id) { + return new CinnamonUserApplet(orientation, panel_height, instance_id); +} diff --git a/quick-settings@celiopy/files/quick-settings@celiopy/icon.png b/quick-settings@celiopy/files/quick-settings@celiopy/icon.png new file mode 100644 index 00000000000..f2db0adcf12 Binary files /dev/null and b/quick-settings@celiopy/files/quick-settings@celiopy/icon.png differ diff --git a/quick-settings@celiopy/files/quick-settings@celiopy/icons/format-text-size-decrease-symbolic.svg b/quick-settings@celiopy/files/quick-settings@celiopy/icons/format-text-size-decrease-symbolic.svg new file mode 100644 index 00000000000..3bce77be1c2 --- /dev/null +++ b/quick-settings@celiopy/files/quick-settings@celiopy/icons/format-text-size-decrease-symbolic.svg @@ -0,0 +1,43 @@ + + + + + + diff --git a/quick-settings@celiopy/files/quick-settings@celiopy/icons/format-text-size-increase-symbolic.svg b/quick-settings@celiopy/files/quick-settings@celiopy/icons/format-text-size-increase-symbolic.svg new file mode 100644 index 00000000000..1d06800b097 --- /dev/null +++ b/quick-settings@celiopy/files/quick-settings@celiopy/icons/format-text-size-increase-symbolic.svg @@ -0,0 +1,43 @@ + + + + + + diff --git a/quick-settings@celiopy/files/quick-settings@celiopy/icons/user-menu-symbolic.svg b/quick-settings@celiopy/files/quick-settings@celiopy/icons/user-menu-symbolic.svg new file mode 100644 index 00000000000..39b9f4a1344 --- /dev/null +++ b/quick-settings@celiopy/files/quick-settings@celiopy/icons/user-menu-symbolic.svg @@ -0,0 +1,19 @@ + + + + + + diff --git a/quick-settings@celiopy/files/quick-settings@celiopy/metadata.json b/quick-settings@celiopy/files/quick-settings@celiopy/metadata.json new file mode 100644 index 00000000000..8d311e43add --- /dev/null +++ b/quick-settings@celiopy/files/quick-settings@celiopy/metadata.json @@ -0,0 +1,6 @@ +{ + "description": "An applet to access your account details, switch users and quickly logout or power off the computer", + "uuid": "quick-settings@celiopy", + "name": "Quick Settings", + "max-instances": 1 +} diff --git a/quick-settings@celiopy/files/quick-settings@celiopy/po/pt_BR.po b/quick-settings@celiopy/files/quick-settings@celiopy/po/pt_BR.po new file mode 100644 index 00000000000..123be4b2590 --- /dev/null +++ b/quick-settings@celiopy/files/quick-settings@celiopy/po/pt_BR.po @@ -0,0 +1,95 @@ +# QUICK SETTINGS +# This file is put in the public domain. +# celiopy, 2025 +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: quick-settings@celiopy 1.0\n" +"Report-Msgid-Bugs-To: https://github.com/linuxmint/cinnamon-spices-applets/" +"issues\n" +"POT-Creation-Date: 2025-09-15 11:15-0300\n" +"PO-Revision-Date: \n" +"Last-Translator: \n" +"Language-Team: \n" +"Language: pt_BR \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" + +#. applet.js:330 +msgid "Power Saver" +msgstr "Economia de energia" + +#. applet.js:331 +msgid "Balanced" +msgstr "Equilibrado" + +#. applet.js:332 +msgid "Performance" +msgstr "Desempenho" + +#. settings-schema.json->section1->description +#. applet.js:511 +msgid "Settings" +msgstr "Configurações" + +#. applet.js:516 +msgid "Lock Screen" +msgstr "Bloquear tela" + +#. applet.js:527 +msgid "Log Out" +msgstr "Encerrar sessão" + +#. applet.js:530 +msgid "Shut Down" +msgstr "Desligar" + +#. applet.js:536 +msgid "Dark mode" +msgstr "Modo escuro" + +#. applet.js:553 +msgid "Night Light" +msgstr "Luz noturna" + +#. applet.js:574 +msgid "Prevent Sleep" +msgstr "Impedir suspensão" + +#. applet.js:587 +msgid "Power Mode" +msgstr "Modo de energia" + +#. metadata.json->name +msgid "Quick Settings" +msgstr "Configurações Rápidas" + +#. settings-schema.json->display-name->description +msgid "Display user name and host" +msgstr "Mostrar nome de usuário e host" + +#. settings-schema.json->section2->description +msgid "Theme" +msgstr "Tema" + +#. settings-schema.json->light-theme->description +msgid "Select a light theme" +msgstr "Selecione o tema claro" + +#. settings-schema.json->dark-theme->description +msgid "Select a dark theme" +msgstr "Selecione o tema escuro" + +#. settings-schema.json->section3->description +msgid "Keyboard shortcuts" +msgstr "Atalhos do teclado" + +#. settings-schema.json->keyOpen->description +msgid "Show menu" +msgstr "Mostrar o menu" + +#. settings-schema.json->keyOpen->tooltip +msgid "Set keybinding(s) to show menu." +msgstr "Defina o(s) atalho(s) para mostrar o menu." diff --git a/quick-settings@celiopy/files/quick-settings@celiopy/po/quick-settings@celiopy.pot b/quick-settings@celiopy/files/quick-settings@celiopy/po/quick-settings@celiopy.pot new file mode 100644 index 00000000000..2534390583e --- /dev/null +++ b/quick-settings@celiopy/files/quick-settings@celiopy/po/quick-settings@celiopy.pot @@ -0,0 +1,99 @@ +# QUICK SETTINGS +# This file is put in the public domain. +# celiopy, 2025 +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: quick-settings@celiopy 1.0\n" +"Report-Msgid-Bugs-To: https://github.com/linuxmint/cinnamon-spices-applets/" +"issues\n" +"POT-Creation-Date: 2025-09-15 11:15-0300\n" +"PO-Revision-Date: \n" +"Last-Translator: \n" +"Language-Team: \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" + +#. applet.js:330 +msgid "Power Saver" +msgstr "" + +#. applet.js:331 +msgid "Balanced" +msgstr "" + +#. applet.js:332 +msgid "Performance" +msgstr "" + +#. settings-schema.json->section1->description +#. applet.js:511 +msgid "Settings" +msgstr "" + +#. applet.js:516 +msgid "Lock Screen" +msgstr "" + +#. applet.js:527 +msgid "Log Out" +msgstr "" + +#. applet.js:530 +msgid "Shut Down" +msgstr "" + +#. applet.js:536 +msgid "Dark mode" +msgstr "" + +#. applet.js:553 +msgid "Night Light" +msgstr "" + +#. applet.js:574 +msgid "Prevent Sleep" +msgstr "" + +#. applet.js:587 +msgid "Power Mode" +msgstr "" + +#. metadata.json->name +msgid "Quick Settings" +msgstr "" + +#. settings-schema.json->display-name->description +msgid "Display user name and host" +msgstr "" + +#. settings-schema.json->section2->description +msgid "Theme" +msgstr "" + +#. settings-schema.json->light-theme->options +msgid "Materia-light-compact" +msgstr "" + +#. settings-schema.json->light-theme->description +msgid "Select a light theme" +msgstr "" + +#. settings-schema.json->dark-theme->description +msgid "Select a dark theme" +msgstr "" + +#. settings-schema.json->section3->description +msgid "Keyboard shortcuts" +msgstr "" + +#. settings-schema.json->keyOpen->description +msgid "Show menu" +msgstr "" + +#. settings-schema.json->keyOpen->tooltip +msgid "Set keybinding(s) to show menu." +msgstr "" diff --git a/quick-settings@celiopy/files/quick-settings@celiopy/settings-schema.json b/quick-settings@celiopy/files/quick-settings@celiopy/settings-schema.json new file mode 100644 index 00000000000..95fa3719e6a --- /dev/null +++ b/quick-settings@celiopy/files/quick-settings@celiopy/settings-schema.json @@ -0,0 +1,48 @@ +{ + "section1": { + "type": "section", + "description": "Settings" + }, + "display-name": { + "type": "switch", + "default": false, + "description": "Display user name and host" + }, + "section2": { + "type": "section", + "description": "Theme" + }, + "light-theme": { + "type": "combobox", + "default": "Adwaita", + "options": { + "Adwaita": "Adwaita", + "Arc": "Arc", + "Numix": "Numix", + "Materia": "Materia", + "Materia-light-compact": "Materia Light Compact" + }, + "description": "Select a light theme" + }, + "dark-theme": { + "type": "combobox", + "default": "Adwaita-dark", + "options": { + "Adwaita-dark": "Adwaita Dark", + "Arc-Dark": "Arc Dark", + "Numix-Dark": "Numix Dark", + "Materia-dark-compact": "Materia Dark Compact" + }, + "description": "Select a dark theme" + }, + "section3": { + "type": "section", + "description": "Keyboard shortcuts" + }, + "keyOpen": { + "type": "keybinding", + "description": "Show menu", + "default": "a", + "tooltip": "Set keybinding(s) to show menu." + } +} diff --git a/quick-settings@celiopy/files/quick-settings@celiopy/stylesheet.css b/quick-settings@celiopy/files/quick-settings@celiopy/stylesheet.css new file mode 100644 index 00000000000..b044c20f8cb --- /dev/null +++ b/quick-settings@celiopy/files/quick-settings@celiopy/stylesheet.css @@ -0,0 +1,107 @@ +/* Session section layout */ +.session-container { + padding: 0.45em; /* 6pt para em */ + spacing: 0.45em; /* 2pt para em */ +} + +.session-spacer { + min-width: 0; + min-height: 0; +} + +.session-buttons-box { + spacing: 0.3em; /* 4pt para em */ +} + +/* User box styles */ +.user-box { + spacing: 0.45em; /* 4pt para em */ +} + +.user-icon { + border-radius: 50%; /* Mais semântico do que 999px */ + border: 0; +} + +.user-label { + font-size: 1em; /* 9pt para em */ + font-weight: bold; +} + +.host-label { + font-size: 0.8em; /* 8pt para em */ + color: rgb(150, 150, 150); +} + +/* Session button styles */ +.system-button { + border-radius: 7.5em; /* 100pt para em */ + padding: 0.75em; /* 6pt para em */ + background-color: rgba(160, 160, 160, 0.08); + transition: background-color 150ms ease; +} + +.system-button:hover { + background-color: rgba(160, 160, 160, 0.18); +} + +.system-button:active { + background-color: rgba(160, 160, 160, 0.28); +} + +.system-button .system-status-icon { + icon-size: 0.9em; /* 10pt para em */ +} + +/* Grid Layout */ +.prefs-grid-container { + padding: 1.1em 0.45em; /* 10pt e 0.375pt para em */ +} + +.settings-toggle-box { + spacing: 0.5em; /* 5pt para em */ +} + +.settings-toggle-button { + background-color: rgba(160, 160, 160, 0.1); + padding: 1em; /* 10pt para em */ + border-radius: 1em; /* 10pt para em */ +} + +.settings-toggle-button.active { + background-color: #1c71d8; +} + +.settings-toggle-icon { + icon-size: 1.5em; /* 20px já em px */ +} + +.settings-toggle-label { + font-size: 0.85em; /* 9pt para em */ + max-width: 80px; /* 80px para em */ +} + +/* Scaling slider */ +.scaling-container { + spacing: 0.35em; /* 5pt para em */ + margin: 0.15em 0.45em; /* 2pt e 6pt para em */ +} + +/* CSS */ +.fake-slider-track { + background-color: rgba(100, 100, 100, 0.3); + border-radius: 0.45em; /* 5pt para em */ + height: 0.45em; /* 5pt para em */ +} + +.fake-slider-fill { + background-color: #5a9bd5; + border-radius: 0.45em; /* 5pt para em */ + height: 0.45em; /* 5pt para em */ +} + +.fake-slider-marker { + background-color: #ddffff; + width: 0.075em; /* 1pt para em */ + border-radius: 0.075em; /* 1pt para em */ +} \ No newline at end of file diff --git a/quick-settings@celiopy/info.json b/quick-settings@celiopy/info.json new file mode 100644 index 00000000000..ed7a1e9e769 --- /dev/null +++ b/quick-settings@celiopy/info.json @@ -0,0 +1 @@ +{ "author": "celiopy" } diff --git a/quick-settings@celiopy/screenshot.png b/quick-settings@celiopy/screenshot.png new file mode 100644 index 00000000000..c597f0f6990 Binary files /dev/null and b/quick-settings@celiopy/screenshot.png differ