Files
benthicbloom/prefs.js
T
Claude f7c05c2c76 Fix false-positive "GStreamer Not Found" warning in Preferences
The Preferences window's availability probe (added to surface missing
GStreamer deps in-UI) reused loadGstreamerModules(), which also checks
for Cogl. Cogl is Mutter's private library — its typelib is only
reachable from inside the actual gnome-shell process (which gets a
private GI search path), never from the separate, plain-GTK4
Preferences process. So the check always failed there with "Requiring
Cogl ... not found" even when live wallpapers were working correctly
in the real shell process, as confirmed by a user report where the
extension's own log showed live wallpaper playback starting cleanly
while Preferences simultaneously claimed GStreamer was missing.

Split into loadGstreamerModules() (Gst+GstApp+Cogl, used only by
liveWallpaper.js in the shell process) and a new lighter
checkGstreamerBaseAvailable() (Gst+GstApp only) for prefs.js, with a
note in the UI that a clean check there isn't a full guarantee since
the private half can't be verified from that process.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019RDqbdjsiisSU7CbQke4g8
2026-08-04 00:36:48 +00:00

357 lines
16 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import Adw from 'gi://Adw';
import Gtk from 'gi://Gtk';
import Gio from 'gi://Gio';
import Gdk from 'gi://Gdk';
import {ExtensionPreferences, gettext as _} from 'resource:///org/gnome/Shell/Extensions/js/extensions/prefs.js';
import {SettingsKey} from './lib/settingsKeys.js';
import {checkGstreamerBaseAvailable, GSTREAMER_INSTALL_HINT} from './lib/gstreamerAvailability.js';
export default class BenthicBloomPreferences extends ExtensionPreferences {
async fillPreferencesWindow(window) {
const settings = this.getSettings();
const gstreamerError = await checkGstreamerBaseAvailable().then(() => null, e => e.message ?? String(e));
window.set_default_size(640, 720);
window.add(this._buildGeneralPage(settings));
window.add(this._buildRotationPage(settings));
window.add(this._buildLiveWallpaperPage(settings, gstreamerError));
window.add(this._buildOledPage(settings));
window.add(this._buildAboutPage());
}
_switchRow(settings, key, title, subtitle) {
const row = new Adw.SwitchRow({title, subtitle});
settings.bind(key, row, 'active', Gio.SettingsBindFlags.DEFAULT);
return row;
}
_spinRow({title, subtitle, lower, upper, step, page, digits = 0}, getValue, setValue) {
const row = new Adw.SpinRow({
title,
subtitle,
digits,
adjustment: new Gtk.Adjustment({lower, upper, step_increment: step, page_increment: page ?? step}),
});
row.value = getValue();
row.connect('notify::value', () => setValue(row.value));
return row;
}
// --- General page ------------------------------------------------
_buildGeneralPage(settings) {
const page = new Adw.PreferencesPage({title: _('General'), icon_name: 'preferences-system-symbolic'});
const behaviorGroup = new Adw.PreferencesGroup({title: _('Behavior')});
page.add(behaviorGroup);
behaviorGroup.add(this._switchRow(
settings, SettingsKey.SHOW_INDICATOR,
_('Show Panel Indicator'), _('Display a quick-access icon in the top bar')));
behaviorGroup.add(this._switchRow(
settings, SettingsKey.APPLY_TO_LOCK_SCREEN,
_('Apply to Lock Screen'), _('Also use the current wallpaper as the lock screen background')));
behaviorGroup.add(this._switchRow(
settings, SettingsKey.DEBUG_LOGGING,
_('Debug Logging'), _('Print verbose diagnostics to the system log (journalctl -f)')));
const foldersGroup = new Adw.PreferencesGroup({
title: _('Wallpaper Folders'),
description: _('Images found directly inside these folders are used for rotation'),
});
page.add(foldersGroup);
this._folderList = new Gtk.ListBox({
selection_mode: Gtk.SelectionMode.NONE,
css_classes: ['boxed-list'],
});
foldersGroup.add(this._folderList);
this._refreshFolderList(settings);
const addButton = new Gtk.Button({
label: _('Add Folder…'),
halign: Gtk.Align.START,
margin_top: 6,
css_classes: ['flat'],
});
addButton.connect('clicked', () => this._pickFolder(settings));
foldersGroup.add(addButton);
return page;
}
_refreshFolderList(settings) {
let child = this._folderList.get_first_child();
while (child) {
const next = child.get_next_sibling();
this._folderList.remove(child);
child = next;
}
const folders = settings.get_strv(SettingsKey.WALLPAPER_FOLDERS);
if (folders.length === 0) {
this._folderList.append(new Adw.ActionRow({title: _('No folders added yet')}));
return;
}
for (const folder of folders) {
const row = new Adw.ActionRow({title: folder});
const removeButton = new Gtk.Button({
icon_name: 'user-trash-symbolic',
valign: Gtk.Align.CENTER,
css_classes: ['flat'],
});
removeButton.connect('clicked', () => {
const current = settings.get_strv(SettingsKey.WALLPAPER_FOLDERS);
settings.set_strv(SettingsKey.WALLPAPER_FOLDERS, current.filter(f => f !== folder));
this._refreshFolderList(settings);
});
row.add_suffix(removeButton);
this._folderList.append(row);
}
}
_pickFolder(settings) {
const dialog = new Gtk.FileDialog({title: _('Select Wallpaper Folder')});
dialog.select_folder(this._folderList.get_root(), null, (source, result) => {
try {
const folder = dialog.select_folder_finish(result);
const path = folder.get_path();
if (!path)
return;
const current = settings.get_strv(SettingsKey.WALLPAPER_FOLDERS);
if (!current.includes(path)) {
settings.set_strv(SettingsKey.WALLPAPER_FOLDERS, [...current, path]);
this._refreshFolderList(settings);
}
} catch (e) {
// Dialog was dismissed; nothing to do.
}
});
}
// --- Rotation page -------------------------------------------------
_buildRotationPage(settings) {
const page = new Adw.PreferencesPage({title: _('Rotation'), icon_name: 'media-playlist-shuffle-symbolic'});
const group = new Adw.PreferencesGroup({title: _('Automatic Rotation')});
page.add(group);
group.add(this._switchRow(
settings, SettingsKey.ROTATION_ENABLED,
_('Enable Rotation'), _('Automatically change the wallpaper on a timer')));
group.add(this._spinRow(
{title: _('Interval'), subtitle: _('Minutes between wallpaper changes'), lower: 1, upper: 1440, step: 1, page: 10},
() => settings.get_uint(SettingsKey.ROTATION_INTERVAL_SECONDS) / 60,
value => settings.set_uint(SettingsKey.ROTATION_INTERVAL_SECONDS, Math.round(value) * 60)));
const modeRow = new Adw.ComboRow({
title: _('Order'),
model: new Gtk.StringList({strings: [_('Shuffle'), _('Sequential')]}),
});
modeRow.selected = settings.get_string(SettingsKey.ROTATION_MODE) === 'sequential' ? 1 : 0;
modeRow.connect('notify::selected', () => {
settings.set_string(SettingsKey.ROTATION_MODE, modeRow.selected === 1 ? 'sequential' : 'shuffle');
});
group.add(modeRow);
const transitionGroup = new Adw.PreferencesGroup({title: _('Transitions')});
page.add(transitionGroup);
transitionGroup.add(this._switchRow(
settings, SettingsKey.TRANSITION_ENABLED,
_('Crossfade'), _('Smoothly fade between wallpapers instead of switching instantly')));
transitionGroup.add(this._spinRow(
{title: _('Fade Duration'), subtitle: _('Milliseconds'), lower: 200, upper: 5000, step: 100, page: 500},
() => settings.get_uint(SettingsKey.TRANSITION_DURATION_MS),
value => settings.set_uint(SettingsKey.TRANSITION_DURATION_MS, Math.round(value))));
return page;
}
// --- Live wallpaper page --------------------------------------------
_buildLiveWallpaperPage(settings, gstreamerError) {
const page = new Adw.PreferencesPage({title: _('Live Wallpaper'), icon_name: 'video-x-generic-symbolic'});
if (gstreamerError) {
const warningGroup = new Adw.PreferencesGroup();
const warningRow = new Adw.ActionRow({
title: _('GStreamer Not Found'),
subtitle: `${_('Live wallpapers will stay disabled until this is fixed:')} ${gstreamerError}\n\n${GSTREAMER_INSTALL_HINT}`,
css_classes: ['warning'],
});
warningRow.subtitle_lines = 0;
warningGroup.add(warningRow);
page.add(warningGroup);
}
const group = new Adw.PreferencesGroup({
title: _('Video Wallpaper'),
description: _(
'Play a looping video or animated GIF as your desktop background instead of a static image. ' +
'Requires GStreamer (with its "good" and "base" plugin sets, which provide GIF decoding) ' +
'to be installed on your system. Note: this page can only detect GStreamer being ' +
'completely missing — the rendering path it also needs is private to gnome-shell and can’t ' +
'be checked from here, so the absence of a warning below isn’t a full guarantee.'),
});
page.add(group);
group.add(this._switchRow(
settings, SettingsKey.LIVE_WALLPAPER_ENABLED,
_('Enable Live Wallpaper'), _('Overrides the static wallpaper while active')));
const fileRow = new Adw.ActionRow({
title: _('Video / GIF File'),
subtitle: settings.get_string(SettingsKey.LIVE_WALLPAPER_PATH) || _('None selected'),
});
const chooseButton = new Gtk.Button({label: _('Choose…'), valign: Gtk.Align.CENTER, css_classes: ['flat']});
chooseButton.connect('clicked', () => {
const dialog = new Gtk.FileDialog({title: _('Select Wallpaper Video or GIF')});
const mediaFilter = new Gtk.FileFilter({name: _('Videos and animated GIFs')});
mediaFilter.add_mime_type('video/*');
mediaFilter.add_mime_type('image/gif');
mediaFilter.add_pattern('*.gif');
const allFilter = new Gtk.FileFilter({name: _('All files')});
allFilter.add_pattern('*');
const filterList = new Gio.ListStore({item_type: Gtk.FileFilter});
filterList.append(mediaFilter);
filterList.append(allFilter);
dialog.filters = filterList;
dialog.default_filter = mediaFilter;
dialog.open(fileRow.get_root(), null, (source, result) => {
try {
const file = dialog.open_finish(result);
const path = file.get_path();
settings.set_string(SettingsKey.LIVE_WALLPAPER_PATH, path);
fileRow.subtitle = path;
} catch (e) {
// Dialog was dismissed; nothing to do.
}
});
});
fileRow.add_suffix(chooseButton);
group.add(fileRow);
const muteRow = this._switchRow(
settings, SettingsKey.LIVE_WALLPAPER_MUTED,
_('Mute Audio'), _('Play video wallpapers without sound'));
group.add(muteRow);
const rateRow = this._spinRow(
{
title: _('Playback Speed'),
subtitle: _('Multiplier, e.g. 0.5 for half speed, 2.0 for double speed'),
lower: 0.1, upper: 4.0, step: 0.1, page: 0.5, digits: 1,
},
() => settings.get_double(SettingsKey.LIVE_WALLPAPER_PLAYBACK_RATE),
value => settings.set_double(SettingsKey.LIVE_WALLPAPER_PLAYBACK_RATE, value));
group.add(rateRow);
const powerGroup = new Adw.PreferencesGroup({title: _('Power Saving')});
page.add(powerGroup);
powerGroup.add(this._switchRow(
settings, SettingsKey.LIVE_WALLPAPER_PAUSE_ON_BATTERY,
_('Pause on Battery'), _('Stop video playback while running on battery power')));
powerGroup.add(this._switchRow(
settings, SettingsKey.LIVE_WALLPAPER_PAUSE_WHEN_FULLSCREEN,
_('Pause When Fullscreen'), _('Stop video playback while a window is fullscreen')));
// The enable switch itself stays usable so the setting can be
// prepared ahead of time, but everything that only matters once
// GStreamer is actually driving playback is greyed out.
if (gstreamerError) {
fileRow.sensitive = false;
muteRow.sensitive = false;
rateRow.sensitive = false;
powerGroup.sensitive = false;
}
return page;
}
// --- OLED protection page --------------------------------------------
_buildOledPage(settings) {
const page = new Adw.PreferencesPage({title: _('OLED Protection'), icon_name: 'weather-clear-night-symbolic'});
const group = new Adw.PreferencesGroup({
title: _('Burn-in Protection'),
description: _('Reduces the risk of permanent image retention on OLED displays'),
});
page.add(group);
group.add(this._switchRow(
settings, SettingsKey.OLED_PROTECTION_ENABLED,
_('Enable OLED Protection'), _('Master switch for all burn-in protection features')));
const shiftGroup = new Adw.PreferencesGroup({title: _('Pixel Shifting')});
page.add(shiftGroup);
shiftGroup.add(this._switchRow(
settings, SettingsKey.OLED_PIXEL_SHIFT_ENABLED,
_('Enable Pixel Shifting'), _('Periodically nudge the background by a few pixels')));
shiftGroup.add(this._spinRow(
{title: _('Shift Interval'), subtitle: _('Seconds between each shift step'), lower: 10, upper: 600, step: 5, page: 30},
() => settings.get_uint(SettingsKey.OLED_PIXEL_SHIFT_INTERVAL_SECONDS),
value => settings.set_uint(SettingsKey.OLED_PIXEL_SHIFT_INTERVAL_SECONDS, Math.round(value))));
shiftGroup.add(this._spinRow(
{title: _('Shift Amount'), subtitle: _('Pixels'), lower: 1, upper: 10, step: 1, page: 1},
() => settings.get_uint(SettingsKey.OLED_PIXEL_SHIFT_AMOUNT_PX),
value => settings.set_uint(SettingsKey.OLED_PIXEL_SHIFT_AMOUNT_PX, Math.round(value))));
const dimGroup = new Adw.PreferencesGroup({title: _('Idle Dimming')});
page.add(dimGroup);
dimGroup.add(this._switchRow(
settings, SettingsKey.OLED_DIM_ON_IDLE_ENABLED,
_('Dim When Idle'), _('Lower brightness after a period of inactivity')));
dimGroup.add(this._spinRow(
{title: _('Idle Delay'), subtitle: _('Seconds of inactivity before dimming'), lower: 10, upper: 3600, step: 10, page: 60},
() => settings.get_uint(SettingsKey.OLED_DIM_IDLE_DELAY_SECONDS),
value => settings.set_uint(SettingsKey.OLED_DIM_IDLE_DELAY_SECONDS, Math.round(value))));
dimGroup.add(this._spinRow(
{
title: _('Dimmed Brightness'), subtitle: _('0.0 = black, 0.9 = barely dimmed'),
lower: 0.0, upper: 0.9, step: 0.05, page: 0.1, digits: 2,
},
() => settings.get_double(SettingsKey.OLED_DIM_BRIGHTNESS),
value => settings.set_double(SettingsKey.OLED_DIM_BRIGHTNESS, value)));
const forceGroup = new Adw.PreferencesGroup({title: _('Forced Rotation')});
page.add(forceGroup);
forceGroup.add(this._switchRow(
settings, SettingsKey.OLED_FORCE_ROTATION_ENABLED,
_('Force Periodic Change'),
_('Change the wallpaper even if automatic rotation is off, to avoid prolonged static images')));
forceGroup.add(this._spinRow(
{title: _('Maximum Static Duration'), subtitle: _('Hours before a change is forced'), lower: 1, upper: 48, step: 1, page: 4},
() => settings.get_uint(SettingsKey.OLED_MAX_STATIC_DURATION_SECONDS) / 3600,
value => settings.set_uint(SettingsKey.OLED_MAX_STATIC_DURATION_SECONDS, Math.round(value) * 3600)));
return page;
}
// --- About page -----------------------------------------------------
_buildAboutPage() {
const page = new Adw.PreferencesPage({title: _('About'), icon_name: 'help-about-symbolic'});
const group = new Adw.PreferencesGroup();
page.add(group);
group.add(new Adw.ActionRow({title: this.metadata.name, subtitle: this.metadata.description}));
group.add(new Adw.ActionRow({
title: _('Version'),
subtitle: this.metadata['version-name'] ?? String(this.metadata.version ?? ''),
}));
if (this.metadata.url) {
const linkRow = new Adw.ActionRow({title: _('Source Code'), subtitle: this.metadata.url, activatable: true});
linkRow.connect('activated', () => Gtk.show_uri(null, this.metadata.url, Gdk.CURRENT_TIME));
group.add(linkRow);
}
return page;
}
}