diff --git a/Makefile b/Makefile
index 827c2ed..efa0595 100644
--- a/Makefile
+++ b/Makefile
@@ -13,9 +13,10 @@ schemas:
build: schemas
install: build
- mkdir -p $(INSTALL_DIR)/lib $(INSTALL_DIR)/schemas
+ mkdir -p $(INSTALL_DIR)/lib $(INSTALL_DIR)/schemas $(INSTALL_DIR)/icons
cp extension.js prefs.js metadata.json stylesheet.css $(INSTALL_DIR)/
cp lib/*.js $(INSTALL_DIR)/lib/
+ cp icons/*.svg $(INSTALL_DIR)/icons/
cp $(SCHEMA_DIR)/*.xml $(SCHEMA_DIR)/gschemas.compiled $(INSTALL_DIR)/schemas/
@echo "Installed to $(INSTALL_DIR)"
@echo "Reload GNOME Shell (Alt+F2, r, Enter on X11; log out/in on Wayland), then run:"
@@ -25,7 +26,7 @@ uninstall:
rm -rf $(INSTALL_DIR)
pack: schemas
- gnome-extensions pack --force --extra-source=lib -o dist .
+ gnome-extensions pack --force --extra-source=lib --extra-source=icons -o dist .
clean:
rm -f $(SCHEMA_DIR)/gschemas.compiled
diff --git a/README.md b/README.md
index e956ac1..7150192 100644
--- a/README.md
+++ b/README.md
@@ -81,8 +81,8 @@ gnome-extensions prefs benthicbloom@quinta0.github.io
logging, and the folders scanned for wallpapers.
- **Rotation** — enable/disable, interval, shuffle vs. sequential order,
and crossfade transition settings.
-- **Live Wallpaper** — enable/disable, video file, mute, playback speed,
- and power-saving pause behavior.
+- **Live Wallpaper** — enable/disable, video file, source folders to pick
+ a video/GIF from, mute, playback speed, and power-saving pause behavior.
- **OLED Protection** — master switch plus independent controls for
pixel shifting, idle dimming, and forced periodic rotation.
@@ -99,6 +99,7 @@ lib/rotationManager.js Timer-driven wallpaper rotation + crossfade overlay
lib/liveWallpaper.js GStreamer video playback rendered onto a Clutter actor
lib/oledProtection.js Pixel shifting, idle dimming, forced rotation
lib/indicator.js Top-bar quick-access menu
+icons/ Bundled symbolic icons for the preferences window
schemas/ GSettings schema
```
diff --git a/icons/bb-about-symbolic.svg b/icons/bb-about-symbolic.svg
new file mode 100644
index 0000000..b08af0c
--- /dev/null
+++ b/icons/bb-about-symbolic.svg
@@ -0,0 +1,5 @@
+
diff --git a/icons/bb-general-symbolic.svg b/icons/bb-general-symbolic.svg
new file mode 100644
index 0000000..2b157e9
--- /dev/null
+++ b/icons/bb-general-symbolic.svg
@@ -0,0 +1,10 @@
+
diff --git a/icons/bb-live-wallpaper-symbolic.svg b/icons/bb-live-wallpaper-symbolic.svg
new file mode 100644
index 0000000..3343d06
--- /dev/null
+++ b/icons/bb-live-wallpaper-symbolic.svg
@@ -0,0 +1,4 @@
+
diff --git a/icons/bb-oled-symbolic.svg b/icons/bb-oled-symbolic.svg
new file mode 100644
index 0000000..5184bcb
--- /dev/null
+++ b/icons/bb-oled-symbolic.svg
@@ -0,0 +1,5 @@
+
diff --git a/icons/bb-rotation-symbolic.svg b/icons/bb-rotation-symbolic.svg
new file mode 100644
index 0000000..4897dcd
--- /dev/null
+++ b/icons/bb-rotation-symbolic.svg
@@ -0,0 +1,6 @@
+
diff --git a/lib/settingsKeys.js b/lib/settingsKeys.js
index d6eb665..ed7c823 100644
--- a/lib/settingsKeys.js
+++ b/lib/settingsKeys.js
@@ -13,6 +13,7 @@ export const SettingsKey = Object.freeze({
LIVE_WALLPAPER_ENABLED: 'live-wallpaper-enabled',
LIVE_WALLPAPER_PATH: 'live-wallpaper-path',
+ LIVE_WALLPAPER_FOLDERS: 'live-wallpaper-folders',
LIVE_WALLPAPER_MUTED: 'live-wallpaper-muted',
LIVE_WALLPAPER_PLAYBACK_RATE: 'live-wallpaper-playback-rate',
LIVE_WALLPAPER_PAUSE_ON_BATTERY: 'live-wallpaper-pause-on-battery',
@@ -32,3 +33,7 @@ export const SettingsKey = Object.freeze({
export const IMAGE_EXTENSIONS = Object.freeze([
'.jpg', '.jpeg', '.png', '.webp', '.bmp', '.tiff', '.tif', '.gif',
]);
+
+export const LIVE_WALLPAPER_EXTENSIONS = Object.freeze([
+ '.gif', '.mp4', '.webm', '.mkv', '.mov', '.avi', '.m4v', '.ogv',
+]);
diff --git a/lib/wallpaperSource.js b/lib/wallpaperSource.js
index 7695685..8562405 100644
--- a/lib/wallpaperSource.js
+++ b/lib/wallpaperSource.js
@@ -1,19 +1,19 @@
import Gio from 'gi://Gio';
import GLib from 'gi://GLib';
-import {IMAGE_EXTENSIONS} from './settingsKeys.js';
+import {IMAGE_EXTENSIONS, LIVE_WALLPAPER_EXTENSIONS} from './settingsKeys.js';
Gio._promisify(Gio.File.prototype, 'enumerate_children_async', 'enumerate_children_finish');
Gio._promisify(Gio.FileEnumerator.prototype, 'next_files_async', 'next_files_finish');
Gio._promisify(Gio.FileEnumerator.prototype, 'close_async', 'close_finish');
-function hasImageExtension(name) {
+function hasExtension(name, extensions) {
const lower = name.toLowerCase();
- return IMAGE_EXTENSIONS.some(ext => lower.endsWith(ext));
+ return extensions.some(ext => lower.endsWith(ext));
}
-/** Non-recursively lists image files directly inside a single folder. */
-export async function listImagesInFolder(folderPath) {
+/** Non-recursively lists files with one of `extensions` directly inside a single folder. */
+async function listFilesInFolder(folderPath, extensions) {
const results = [];
const dir = folderPath.startsWith('file://')
? Gio.File.new_for_uri(folderPath)
@@ -38,7 +38,7 @@ export async function listImagesInFolder(folderPath) {
for (const info of infos) {
if (info.get_file_type() !== Gio.FileType.REGULAR)
continue;
- if (!hasImageExtension(info.get_name()))
+ if (!hasExtension(info.get_name(), extensions))
continue;
results.push(enumerator.get_child(info).get_path());
}
@@ -53,10 +53,10 @@ export async function listImagesInFolder(folderPath) {
return results;
}
-/** Merges and de-duplicates images found across several folders. */
-export async function listImagesInFolders(folderPaths) {
+/** Merges and de-duplicates files with one of `extensions` found across several folders. */
+async function listFilesInFolders(folderPaths, extensions) {
const lists = await Promise.all(
- folderPaths.map(folder => listImagesInFolder(folder).catch(() => [])));
+ folderPaths.map(folder => listFilesInFolder(folder, extensions).catch(() => [])));
const seen = new Set();
const merged = [];
@@ -72,3 +72,18 @@ export async function listImagesInFolders(folderPaths) {
merged.sort();
return merged;
}
+
+/** Non-recursively lists image files directly inside a single folder. */
+export async function listImagesInFolder(folderPath) {
+ return listFilesInFolder(folderPath, IMAGE_EXTENSIONS);
+}
+
+/** Merges and de-duplicates images found across several folders. */
+export async function listImagesInFolders(folderPaths) {
+ return listFilesInFolders(folderPaths, IMAGE_EXTENSIONS);
+}
+
+/** Merges and de-duplicates live-wallpaper-capable videos/GIFs found across several folders. */
+export async function listMediaFilesInFolders(folderPaths) {
+ return listFilesInFolders(folderPaths, LIVE_WALLPAPER_EXTENSIONS);
+}
diff --git a/prefs.js b/prefs.js
index 1051370..df9576c 100644
--- a/prefs.js
+++ b/prefs.js
@@ -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);
diff --git a/schemas/org.gnome.shell.extensions.benthicbloom.gschema.xml b/schemas/org.gnome.shell.extensions.benthicbloom.gschema.xml
index 8291cf2..c2f51a0 100644
--- a/schemas/org.gnome.shell.extensions.benthicbloom.gschema.xml
+++ b/schemas/org.gnome.shell.extensions.benthicbloom.gschema.xml
@@ -79,6 +79,12 @@
Path to the video or animated GIF file used as the live wallpaper.
+
+ []
+ Live wallpaper source folders
+ List of folder paths scanned for videos and animated GIFs to pick a live wallpaper from.
+
+
true
Mute live wallpaper audio