/**
* @file Defines BlockerToggle, BlockerIndicator, and BlockerIndicator.
* @author Pedro Sader Azevedo <email@pesader.dev>
* @copyright Pedro Sader Azevedo 2025
* @license GPL-3.0
*/
'use strict';
import GObject from 'gi://GObject';
import Gio from 'gi://Gio';
import * as Main from 'resource:///org/gnome/shell/ui/main.js';
import {Extension, gettext as _} from 'resource:///org/gnome/shell/extensions/extension.js';
import {QuickToggle, SystemIndicator} from 'resource:///org/gnome/shell/ui/quickSettings.js';
import {BlockerIcons} from './modules/icons.js';
import {BlockerNotifier} from './modules/notifier.js';
import {BlockerRunner} from './modules/runner.js';
import {BlockerState, State} from './modules/state.js';
/**
* Blocker's toggle.
*
* @class
* @param {Gio.Settings} settings - The extension's settings.
*/
const BlockerToggle = GObject.registerClass(
class BlockerToggle extends QuickToggle {
constructor(settings) {
super({
title: 'Blocker',
toggleMode: false,
});
/** @type {Gio.Settings} */
this._settings = settings;
this._settings.bind(
'blocker-enabled',
this,
'checked',
Gio.SettingsBindFlags.DEFAULT
);
}
});
/**
* Blocker's indicator.
*
* @class
* @param {Gio.Settings} settings - The extension's settings.
* @param {BlockerState} state - Blocker's state.
* @param {BlockerIcons} icons - Blocker's icons.
* @param {BlockerNotifier} notifier - notification sender.
* @param {BlockerRunner} runner - command runner.
*/
const BlockerIndicator = GObject.registerClass(
class BlockerIndicator extends SystemIndicator {
constructor(settings, state, icons, notifier, runner) {
super();
/** @type {BlockerIcons} */
this._icons = icons;
/** @type {BlockerNotifier} */
this._notifier = notifier;
/** @type {BlockerRunner} */
this._runner = runner;
/** @type {BlockerIndicator} */
this._indicator = this._addIndicator();
/** @type {BlockerToggle} */
this._toggle = new BlockerToggle(settings);
this._clickedHandlerId = this._toggle.connect('clicked', () => this._onClicked());
this._notifyCheckedHandlerId = this._toggle.connect('notify::checked', () => this._onChecked());
// Set initial Blocker state
/** @type {BlockerState} */
this._state = state;
this._notifyStateHandlerId = this._state.connect('notify::state', () => this._onStateChanged());
this._state.state = this._toggle.checked ? State.ENABLED : State.DISABLED;
/** @type {Gio.NetworkMonitor} */
this._netman = Gio.network_monitor_get_default();
this._networkChangedHandlerId = this._netman.connect('network-changed', (_monitor, _networkAvailable) => this._onNetworkChanged());
this._onNetworkChanged(); // Check initial network state
/** @type {Gio.Icon} */
const icon = this._icons.select(this._toggle.checked);
this._indicator.gicon = icon;
this._toggle.gicon = icon;
this.quickSettingsItems.push(this._toggle);
}
/**
* Responds to network changes.
*
* @returns {void}
* @access protected
*/
_onNetworkChanged() {
if (!this._netman.network_available) {
this._toggle.set_reactive(false);
this._toggle.subtitle = _('Network unavailable');
// NOTE: It is possible that a network drop doesn't make
// Blocker's enablement fail, so it doesn't go back to
// its DISABLED state. Because of that, we need to check
// its state at this point.
//
} else if (this._state.state === State.ENABLING) {
this._toggle.subtitle = _('Enabling in progress');
this._indicator.gicon = this._icons.acquiring;
this._toggle.gicon = this._icons.acquiring;
} else {
this._toggle.set_reactive(true);
this._toggle.subtitle = null;
}
}
/**
* Responds to checking/unchecking the extension toggle.
*
* @returns {void}
* @access protected
*/
_onChecked() {
this._notifier.notifyStatus(this._toggle.checked);
/** @type {Gio.Icon} */
const icon = this._icons.select(this._toggle.checked);
this._indicator.gicon = icon;
this._toggle.gicon = icon;
}
/**
* Responds to clicks in the extension toggle.
*
* @returns {void}
* @access protected
*/
async _onClicked() {
// Save initial state
/** @type {State} */
const initialState = this._state.state;
// Set an intermediary state while commands are running
this._state.state = this._state.nextState();
// Toggle hblock
/** @type {boolean} */
const success = await this._hblockToggle();
if (success)
// Proceed to next state
this._state.state = this._state.nextState();
else
// Restore initial state
this._state.state = initialState;
}
/**
* Responds to changes in Blocker's state.
*
* @returns {void}
* @access protected
*/
_onStateChanged() {
if (this._state.isIntermediary()) {
// While commands are running, change the icons
this._indicator.visible = true;
this._indicator.gicon = this._icons.acquiring;
this._toggle.gicon = this._icons.acquiring;
this._toggle.set_reactive(false);
// Add an explanatory subtitle to the toggle
switch (this._state.state) {
case State.ENABLING:
this._toggle.subtitle = _('Enabling in progress');
break;
case State.DISABLING:
this._toggle.subtitle = _('Disabling in progress');
break;
}
} else {
switch (this._state.state) {
case State.DISABLED:
this._indicator.visible = false;
this._toggle.checked = false;
this._indicator.gicon = this._icons.disabled;
this._toggle.gicon = this._icons.disabled;
break;
case State.ENABLED:
this._indicator.visible = true;
this._toggle.checked = true;
this._indicator.gicon = this._icons.enabled;
this._toggle.gicon = this._icons.enabled;
break;
}
// Remove subtitles and make toggle reactive again
this._toggle.subtitle = null;
this._toggle.set_reactive(true);
}
}
/**
* Toggles hBlock.
*
* @returns {boolean} true if hBlock was toggled successfully, false otherwise.
* @access protected
*/
async _hblockToggle() {
/** @type {boolean} */
let success;
if (this._toggle.checked)
success = await this._runner.hblockDisable();
else
success = await this._runner.hblockEnable();
return success;
}
/**
* Destroys the object.
*
* @returns {void}
*/
destroy() {
if (this._toggle) {
this._toggle.disconnect(this._clickedHandlerId);
this._clickedHandlerId = null;
this._toggle.disconnect(this._notifyCheckedHandlerId);
this._notifyCheckedHandlerId = null;
this._toggle.destroy();
this._toggle = null;
}
if (this._indicator) {
this._indicator.destroy();
this._indicator = null;
}
if (this._runner) {
this._runner.destroy();
this._runner = null;
}
if (this._notifier) {
this._notifier.destroy();
this._notifier = null;
}
if (this._icons) {
this._icons.destroy();
this._icons = null;
}
if (this._state) {
this._state.disconnect(this._notifyStateHandlerId);
this._notifyStateHandlerId = null;
this._state.destroy();
this._state = null;
}
if (this._netman) {
this._netman.disconnect(this._networkChangedHandlerId);
this._networkChangedHandlerId = null;
// There's no need to destroy() this object
this._netman = null;
}
}
});
/**
* Blocker's extension.
*
* @class
*/
export default class BlockerExtension extends Extension {
/**
* Enable the extension.
*
* @returns {void}
*/
enable() {
this._icons = new BlockerIcons(this.path);
this._notifier = new BlockerNotifier(this._icons);
this._runner = new BlockerRunner(this._notifier);
// Check if hBlock is installed
if (this._runner.hblockAvailable()) {
this._state = new BlockerState();
this._indicator = new BlockerIndicator(this.getSettings(), this._state, this._icons, this._notifier, this._runner);
Main.panel.statusArea.quickSettings.addExternalIndicator(this._indicator);
}
}
/**
* Disable the extension.
*
* @returns {void}
*/
disable() {
this.destroy();
}
/**
* Destroys the object.
*
* @returns {void}
*/
destroy() {
if (this._indicator) {
this._indicator.destroy();
this._indicator = null;
}
if (this._state) {
this._state.destroy();
this._state = null;
}
if (this._runner) {
this._runner.destroy();
this._runner = null;
}
if (this._notifier) {
this._notifier.destroy();
this._notifier = null;
}
if (this._icons) {
this._icons.destroy();
this._icons = null;
}
}
}