Add live wallpaper source folders and bundle preferences page icons

Live Wallpaper page gains a "Wallpaper Folders" list (mirroring the
static rotation one) plus an "Available Media" list of the videos/GIFs
found inside them to pick from, backed by a new live-wallpaper-folders
GSettings key and a generalized wallpaperSource.js extension scan.

The preferences page-switcher icons (General, Rotation, Live Wallpaper,
OLED Protection, About) were showing as broken-image placeholders since
they depended on exact symbolic icon names being present in the user's
system icon theme. Bundle a small set of custom SVG icons under icons/
and register them via Gtk.IconTheme.add_search_path so they always
resolve regardless of the system theme.
This commit is contained in:
Claude
2026-08-04 02:08:14 +00:00
parent bbd21c9e5a
commit f246d6a4fe
11 changed files with 199 additions and 40 deletions
+128 -27
View File
@@ -7,12 +7,18 @@ import {ExtensionPreferences, gettext as _} from 'resource:///org/gnome/Shell/Ex
import {SettingsKey} from './lib/settingsKeys.js';
import {checkGstreamerBaseAvailable, GSTREAMER_INSTALL_HINT} from './lib/gstreamerAvailability.js';
import {listMediaFilesInFolders} from './lib/wallpaperSource.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));
// Bundled rather than relying on the system icon theme having these
// exact names — a missing symbolic icon otherwise renders as a
// blank/broken-image placeholder in the page switcher.
Gtk.IconTheme.get_for_display(window.get_display()).add_search_path(`${this.path}/icons`);
window.set_default_size(640, 720);
window.add(this._buildGeneralPage(settings));
window.add(this._buildRotationPage(settings));
@@ -42,7 +48,7 @@ export default class BenthicBloomPreferences extends ExtensionPreferences {
// --- General page ------------------------------------------------
_buildGeneralPage(settings) {
const page = new Adw.PreferencesPage({title: _('General'), icon_name: 'preferences-system-symbolic'});
const page = new Adw.PreferencesPage({title: _('General'), icon_name: 'bb-general-symbolic'});
const behaviorGroup = new Adw.PreferencesGroup({title: _('Behavior')});
page.add(behaviorGroup);
@@ -62,12 +68,8 @@ export default class BenthicBloomPreferences extends ExtensionPreferences {
});
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 folderList = this._buildFolderListBox(settings, SettingsKey.WALLPAPER_FOLDERS, _('No folders added yet'));
foldersGroup.add(folderList);
const addButton = new Gtk.Button({
label: _('Add Folder…'),
@@ -75,23 +77,32 @@ export default class BenthicBloomPreferences extends ExtensionPreferences {
margin_top: 6,
css_classes: ['flat'],
});
addButton.connect('clicked', () => this._pickFolder(settings));
addButton.connect('clicked', () => this._pickFolderForList(
settings, SettingsKey.WALLPAPER_FOLDERS, folderList, _('No folders added yet')));
foldersGroup.add(addButton);
return page;
}
_refreshFolderList(settings) {
let child = this._folderList.get_first_child();
// --- Shared folder-list widgets (used by the General and Live Wallpaper pages) --
_buildFolderListBox(settings, key, emptyText, onChange) {
const listBox = new Gtk.ListBox({selection_mode: Gtk.SelectionMode.NONE, css_classes: ['boxed-list']});
this._refreshFolderListBox(listBox, settings, key, emptyText, onChange);
return listBox;
}
_refreshFolderListBox(listBox, settings, key, emptyText, onChange) {
let child = listBox.get_first_child();
while (child) {
const next = child.get_next_sibling();
this._folderList.remove(child);
listBox.remove(child);
child = next;
}
const folders = settings.get_strv(SettingsKey.WALLPAPER_FOLDERS);
const folders = settings.get_strv(key);
if (folders.length === 0) {
this._folderList.append(new Adw.ActionRow({title: _('No folders added yet')}));
listBox.append(new Adw.ActionRow({title: emptyText}));
return;
}
@@ -103,27 +114,29 @@ export default class BenthicBloomPreferences extends ExtensionPreferences {
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);
const current = settings.get_strv(key);
settings.set_strv(key, current.filter(f => f !== folder));
this._refreshFolderListBox(listBox, settings, key, emptyText, onChange);
onChange?.();
});
row.add_suffix(removeButton);
this._folderList.append(row);
listBox.append(row);
}
}
_pickFolder(settings) {
const dialog = new Gtk.FileDialog({title: _('Select Wallpaper Folder')});
dialog.select_folder(this._folderList.get_root(), null, (source, result) => {
_pickFolderForList(settings, key, listBox, emptyText, onChange) {
const dialog = new Gtk.FileDialog({title: _('Select Folder')});
dialog.select_folder(listBox.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);
const current = settings.get_strv(key);
if (!current.includes(path)) {
settings.set_strv(SettingsKey.WALLPAPER_FOLDERS, [...current, path]);
this._refreshFolderList(settings);
settings.set_strv(key, [...current, path]);
this._refreshFolderListBox(listBox, settings, key, emptyText, onChange);
onChange?.();
}
} catch (e) {
// Dialog was dismissed; nothing to do.
@@ -134,7 +147,7 @@ export default class BenthicBloomPreferences extends ExtensionPreferences {
// --- Rotation page -------------------------------------------------
_buildRotationPage(settings) {
const page = new Adw.PreferencesPage({title: _('Rotation'), icon_name: 'media-playlist-shuffle-symbolic'});
const page = new Adw.PreferencesPage({title: _('Rotation'), icon_name: 'bb-rotation-symbolic'});
const group = new Adw.PreferencesGroup({title: _('Automatic Rotation')});
page.add(group);
@@ -173,7 +186,7 @@ export default class BenthicBloomPreferences extends ExtensionPreferences {
// --- Live wallpaper page --------------------------------------------
_buildLiveWallpaperPage(settings, gstreamerError) {
const page = new Adw.PreferencesPage({title: _('Live Wallpaper'), icon_name: 'video-x-generic-symbolic'});
const page = new Adw.PreferencesPage({title: _('Live Wallpaper'), icon_name: 'bb-live-wallpaper-symbolic'});
if (gstreamerError) {
const warningGroup = new Adw.PreferencesGroup();
@@ -229,6 +242,7 @@ export default class BenthicBloomPreferences extends ExtensionPreferences {
const path = file.get_path();
settings.set_string(SettingsKey.LIVE_WALLPAPER_PATH, path);
fileRow.subtitle = path;
this._refreshLiveMediaList(settings, fileRow);
} catch (e) {
// Dialog was dismissed; nothing to do.
}
@@ -251,6 +265,36 @@ export default class BenthicBloomPreferences extends ExtensionPreferences {
value => settings.set_double(SettingsKey.LIVE_WALLPAPER_PLAYBACK_RATE, value));
group.add(rateRow);
const foldersGroup = new Adw.PreferencesGroup({
title: _('Wallpaper Folders'),
description: _('Videos and animated GIFs found directly inside these folders can be picked below'),
});
page.add(foldersGroup);
const mediaGroup = new Adw.PreferencesGroup({title: _('Available Media')});
page.add(mediaGroup);
this._liveMediaList = new Gtk.ListBox({selection_mode: Gtk.SelectionMode.NONE, css_classes: ['boxed-list']});
mediaGroup.add(this._liveMediaList);
this._liveMediaGeneration = 0;
const refreshMedia = () => this._refreshLiveMediaList(settings, fileRow);
const liveFolderList = this._buildFolderListBox(
settings, SettingsKey.LIVE_WALLPAPER_FOLDERS, _('No folders added yet'), refreshMedia);
foldersGroup.add(liveFolderList);
const addFolderButton = new Gtk.Button({
label: _('Add Folder…'),
halign: Gtk.Align.START,
margin_top: 6,
css_classes: ['flat'],
});
addFolderButton.connect('clicked', () => this._pickFolderForList(
settings, SettingsKey.LIVE_WALLPAPER_FOLDERS, liveFolderList, _('No folders added yet'), refreshMedia));
foldersGroup.add(addFolderButton);
refreshMedia();
const powerGroup = new Adw.PreferencesGroup({title: _('Power Saving')});
page.add(powerGroup);
powerGroup.add(this._switchRow(
@@ -267,16 +311,73 @@ export default class BenthicBloomPreferences extends ExtensionPreferences {
fileRow.sensitive = false;
muteRow.sensitive = false;
rateRow.sensitive = false;
foldersGroup.sensitive = false;
mediaGroup.sensitive = false;
powerGroup.sensitive = false;
}
return page;
}
/** Re-scans the configured live wallpaper folders and repopulates the "Available Media" list. */
_refreshLiveMediaList(settings, fileRow) {
const listBox = this._liveMediaList;
const generation = ++this._liveMediaGeneration;
let child = listBox.get_first_child();
while (child) {
const next = child.get_next_sibling();
listBox.remove(child);
child = next;
}
const folders = settings.get_strv(SettingsKey.LIVE_WALLPAPER_FOLDERS);
if (folders.length === 0) {
listBox.append(new Adw.ActionRow({title: _('Add a folder above to browse its videos and GIFs')}));
return;
}
listBox.append(new Adw.ActionRow({title: _('Scanning…')}));
listMediaFilesInFolders(folders).then(paths => {
if (generation !== this._liveMediaGeneration)
return; // A folder changed again before this scan finished; a newer one is in flight.
let c = listBox.get_first_child();
while (c) {
const next = c.get_next_sibling();
listBox.remove(c);
c = next;
}
if (paths.length === 0) {
listBox.append(new Adw.ActionRow({title: _('No videos or GIFs found in those folders')}));
return;
}
const currentPath = settings.get_string(SettingsKey.LIVE_WALLPAPER_PATH);
for (const path of paths) {
const row = new Adw.ActionRow({
title: Gio.File.new_for_path(path).get_basename(),
subtitle: path,
activatable: true,
});
if (path === currentPath)
row.add_suffix(new Gtk.Image({icon_name: 'object-select-symbolic'}));
row.connect('activated', () => {
settings.set_string(SettingsKey.LIVE_WALLPAPER_PATH, path);
fileRow.subtitle = path;
this._refreshLiveMediaList(settings, fileRow);
});
listBox.append(row);
}
}).catch(() => {});
}
// --- OLED protection page --------------------------------------------
_buildOledPage(settings) {
const page = new Adw.PreferencesPage({title: _('OLED Protection'), icon_name: 'weather-clear-night-symbolic'});
const page = new Adw.PreferencesPage({title: _('OLED Protection'), icon_name: 'bb-oled-symbolic'});
const group = new Adw.PreferencesGroup({
title: _('Burn-in Protection'),
@@ -335,7 +436,7 @@ export default class BenthicBloomPreferences extends ExtensionPreferences {
// --- About page -----------------------------------------------------
_buildAboutPage() {
const page = new Adw.PreferencesPage({title: _('About'), icon_name: 'help-about-symbolic'});
const page = new Adw.PreferencesPage({title: _('About'), icon_name: 'bb-about-symbolic'});
const group = new Adw.PreferencesGroup();
page.add(group);