From 342ab875287f10cf5eef13dd765b19bb0c30ec75 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Markus=20G=C3=B6llnitz?= Date: Mon, 13 May 2024 17:21:18 +0200 Subject: [PATCH 1/2] status: Add new flashlight quick settings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Devices can have dedicated torches, or flash LED that can function as flashlights. It is a common feature, at least amongst mobile operating systems, to control it from their shells. Devices can have an arbitrary amount of flashlights, most commonly, for different colours or colour temperatures or beam angles, and they can have any brightness between 0 and their maximum brightness. Signed-off-by: Markus Göllnitz --- data/gnome-shell-icons.gresource.xml | 1 + .../scalable/actions/flashlight-symbolic.svg | 2 + js/js-resources.gresource.xml | 1 + js/ui/panel.js | 3 + js/ui/status/flashlight.js | 142 ++++++++++++++++++ po/POTFILES.in | 1 + 6 files changed, 150 insertions(+) create mode 100644 data/icons/scalable/actions/flashlight-symbolic.svg create mode 100644 js/ui/status/flashlight.js diff --git a/data/gnome-shell-icons.gresource.xml b/data/gnome-shell-icons.gresource.xml index f28a37d2b..e25908c4b 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 000000000..2976cf107 --- /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 3c096d7bc..8a4b0da63 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 6e9f12211..5e4cd2596 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 000000000..49959780d --- /dev/null +++ b/js/ui/status/flashlight.js @@ -0,0 +1,142 @@ +// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*- +/* exported Indicator */ + +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 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._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.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(); + } +}); + +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 7df17181f..6ea72605a 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 -- GitLab From 8dd67bcf45fa459df529c1bf31c4f1b3af8668c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Markus=20G=C3=B6llnitz?= Date: Sun, 30 Mar 2025 10:25:15 +0200 Subject: [PATCH 2/2] status/flashlight: Automatically turn off flashlight after set period of time MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit To protect the device from physical damage and battery drainage, it is useful to not let the user accidentally keep the flashlight on indefinitely. Signed-off-by: Markus Göllnitz --- js/ui/status/flashlight.js | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/js/ui/status/flashlight.js b/js/ui/status/flashlight.js index 49959780d..9d1972b7e 100644 --- a/js/ui/status/flashlight.js +++ b/js/ui/status/flashlight.js @@ -1,6 +1,7 @@ // -*- 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'; @@ -9,6 +10,8 @@ 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( @@ -57,6 +60,8 @@ class FlashlightToggle extends QuickMenuToggle { iconName: 'flashlight-symbolic', }); + this._autoFlashlightOffTimeoutId = 0; + this._torches = []; this._currentTorch = null; @@ -112,6 +117,8 @@ class FlashlightToggle extends QuickMenuToggle { this._brightnessSlider.value = brightness / maxBrightness; } + this._updateFlashlightTimeout(anyFlashlightOn); + this.checked = anyFlashlightOn; this.menuEnabled = this._torches.length > 0; this.visible = this._torches.length > 0; @@ -130,6 +137,23 @@ class FlashlightToggle extends QuickMenuToggle { })); 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( -- GitLab