diff --git a/data/gnome-shell-icons.gresource.xml b/data/gnome-shell-icons.gresource.xml index f28a37d2b09d90dffd923f49de7a90d37f256f8a..e25908c4bd84f17bb9c064a9da37292633cd43ed 100644 --- a/data/gnome-shell-icons.gresource.xml +++ b/data/gnome-shell-icons.gresource.xml @@ -6,6 +6,7 @@ scalable/actions/carousel-arrow-previous-symbolic.svg scalable/actions/cog-wheel-symbolic.svg scalable/actions/dark-mode-symbolic.svg + scalable/actions/flashlight-symbolic.svg scalable/actions/group-collapse-symbolic.svg scalable/actions/notification-expand-symbolic.svg scalable/actions/ornament-check-symbolic.svg diff --git a/data/icons/scalable/actions/flashlight-symbolic.svg b/data/icons/scalable/actions/flashlight-symbolic.svg new file mode 100644 index 0000000000000000000000000000000000000000..2976cf107266c147d65d492f1c6e009eaec72c1c --- /dev/null +++ b/data/icons/scalable/actions/flashlight-symbolic.svg @@ -0,0 +1,2 @@ + + diff --git a/js/js-resources.gresource.xml b/js/js-resources.gresource.xml index 3c096d7bc912887b3029010d4d56f8b088358b1b..8a4b0da63cd89357b13aefd89b161ca4f9c52346 100644 --- a/js/js-resources.gresource.xml +++ b/js/js-resources.gresource.xml @@ -148,6 +148,7 @@ ui/status/darkMode.js ui/status/doNotDisturb.js ui/status/dwellClick.js + ui/status/flashlight.js ui/status/keyboard.js ui/status/location.js ui/status/network.js diff --git a/js/ui/panel.js b/js/ui/panel.js index 6e9f12211f69e38ae9addad6ba87c76414f457a5..5e4cd2596b61f7b0407ca157ced599a2e2c31274 100644 --- a/js/ui/panel.js +++ b/js/ui/panel.js @@ -34,6 +34,7 @@ import * as ThunderboltStatus from './status/thunderbolt.js'; import * as AutoRotateStatus from './status/autoRotate.js'; import * as BackgroundAppsStatus from './status/backgroundApps.js'; import * as DoNotDisturbStatus from './status/doNotDisturb.js'; +import * as FlashlightStatus from './status/flashlight.js'; import {DateMenuButton} from './dateMenu.js'; import {ATIndicator} from './status/accessibility.js'; @@ -548,6 +549,7 @@ class QuickSettings extends PanelMenu.Button { this._doNotDisturb = new DoNotDisturbStatus.Indicator(); this._unsafeMode = new UnsafeModeIndicator(); this._backgroundApps = new BackgroundAppsStatus.Indicator(); + this._flashlight = new FlashlightStatus.Indicator(); // add privacy-related indicators before any external indicators let pos = 0; @@ -600,6 +602,7 @@ class QuickSettings extends PanelMenu.Button { this._addItemsBefore(this._rfkill.quickSettingsItems, sibling); this._addItemsBefore(this._autoRotate.quickSettingsItems, sibling); this._addItemsBefore(this._doNotDisturb.quickSettingsItems, sibling); + this._addItemsBefore(this._flashlight.quickSettingsItems, sibling); this._addItemsBefore(this._unsafeMode.quickSettingsItems, sibling); // append background apps diff --git a/js/ui/status/flashlight.js b/js/ui/status/flashlight.js new file mode 100644 index 0000000000000000000000000000000000000000..9d1972b7eaa23c0756fc63130e8a4e0ce069bf54 --- /dev/null +++ b/js/ui/status/flashlight.js @@ -0,0 +1,166 @@ +// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*- +/* exported Indicator */ + +import GLib from 'gi://GLib'; +import GObject from 'gi://GObject'; +import GUdev from 'gi://GUdev'; + +import * as PopupMenu from '../popupMenu.js'; +import {Slider} from '../slider.js'; +import {QuickMenuToggle, SystemIndicator} from '../quickSettings.js'; +import * as LoginManager from '../../misc/loginManager.js'; + +const _AUTO_TURN_OFF_FLASHLIGHT_TIMEOUT = 5 * 60; + +const SliderItem = GObject.registerClass({ + Properties: { + 'value': GObject.ParamSpec.double( + 'value', '', '', + GObject.ParamFlags.READWRITE, + 0, 1, 0), + }, +}, class SliderItem extends PopupMenu.PopupBaseMenuItem { + constructor() { + super({ + activate: false, + }); + + this._slider = new Slider(0); + + this._sliderChangedId = this._slider.connect('notify::value', + () => this.notify('value')); + this._slider.accessible_name = _('Flashlight Brightness'); + + this.add_child(this._slider); + } + + get value() { + return this._slider.value; + } + + set value(value) { + value = Math.max(Math.min(value, 1), 0); + + if (this.value === value) + return; + + this._slider.block_signal_handler(this._sliderChangedId); + this._slider.value = value; + this._slider.unblock_signal_handler(this._sliderChangedId); + + this.notify('value'); + } +}); + +const FlashlightToggle = GObject.registerClass( +class FlashlightToggle extends QuickMenuToggle { + _init() { + super._init({ + title: _('Flashlight'), + iconName: 'flashlight-symbolic', + }); + + this._autoFlashlightOffTimeoutId = 0; + + this._torches = []; + this._currentTorch = null; + + this._loginSession = null; + this._getLoginSession().catch(logError); + + this.connect('clicked', () => { + this._brightnessSlider.value = this.checked ? 0.0 : 1.0; + }); + + this._brightnessSlider = new SliderItem(); + + this._brightnessSlider.connect('notify::value', () => this._syncTorchState().catch(logError)); + + this.menu.addMenuItem(this._brightnessSlider); + + this._udevClient = GUdev.Client.new(['leds']); + + this._udevClient.connect('uevent', () => this._syncTorchList()); + this._syncTorchList(); + } + + async _getLoginSession() { + this._loginSession = await LoginManager.getLoginManager().getCurrentSessionProxy(); + } + + _syncTorchList() { + this._torches = []; + this._currentTorch = null; + + try { + const udevEnumerator = GUdev.Enumerator.new(this._udevClient); + + udevEnumerator.add_match_subsystem('leds'); + udevEnumerator.add_match_name('*:torch'); + udevEnumerator.add_match_name('*:flash'); + + this._torches = udevEnumerator.execute(); + if (this._torches.length > 0) + this._currentTorch = this._torches[0]; + } catch (error) { + logError(error); + } + + let anyFlashlightOn = this._torches.some(torch => { + return torch.get_sysfs_attr_as_int('brightness') > 0; + }); + + if (this._currentTorch !== null) { + const maxBrightness = this._currentTorch.get_sysfs_attr_as_int('max_brightness'); + const brightness = this._currentTorch.get_sysfs_attr_as_int('brightness'); + if (Math.floor(this._brightnessSlider.value * maxBrightness) !== brightness) + this._brightnessSlider.value = brightness / maxBrightness; + } + + this._updateFlashlightTimeout(anyFlashlightOn); + + this.checked = anyFlashlightOn; + this.menuEnabled = this._torches.length > 0; + this.visible = this._torches.length > 0; + } + + async _syncTorchState() { + await Promise.all(this._torches.map(torch => { + const deviceName = torch.get_name(); + const maxBrightness = torch.get_sysfs_attr_as_int('max_brightness'); + let newBrightness = 0; + + if (torch === this._currentTorch) + newBrightness = Math.floor(this._brightnessSlider.value * maxBrightness); + + return this._loginSession?.SetBrightnessAsync('leds', deviceName, newBrightness); + })); + this._syncTorchList(); + } + + _updateFlashlightTimeout(anyFlashlightOn) { + if (this._autoFlashlightOffTimeoutId !== 0) { + GLib.source_remove(this._autoFlashlightOffTimeoutId); + this._autoFlashlightOffTimeoutId = 0; + } + + if (!anyFlashlightOn) + return; + + this._autoFlashlightOffTimeoutId = GLib.timeout_add_seconds(GLib.PRIORITY_DEFAULT, _AUTO_TURN_OFF_FLASHLIGHT_TIMEOUT, () => { + this._brightnessSlider.value = 0; + + this._autoFlashlightOffTimeoutId = 0; + return GLib.SOURCE_REMOVE; + }); + } +}); + +export const Indicator = GObject.registerClass( +class Indicator extends SystemIndicator { + _init() { + super._init(); + + this.quickSettingsItems.push(new FlashlightToggle()); + } +}); diff --git a/po/POTFILES.in b/po/POTFILES.in index 7df17181f4e3ce4bf13a26532c24d47abcf05a20..6ea72605a9d28cf4303ecc96b8a6cb7de4cb5bb5 100644 --- a/po/POTFILES.in +++ b/po/POTFILES.in @@ -69,6 +69,7 @@ js/ui/status/bluetooth.js js/ui/status/brightness.js js/ui/status/darkMode.js js/ui/status/dwellClick.js +js/ui/status/flashlight.js js/ui/status/keyboard.js js/ui/status/location.js js/ui/status/network.js