Compare commits
10
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
603b9f9ded | ||
|
|
27f102d594 | ||
|
|
e39d8973d5 | ||
|
|
9876f98317 | ||
|
|
3795e9316c | ||
|
|
f8adc2594f | ||
|
|
faa3a7257f | ||
|
|
40af1dad26 | ||
|
|
d7278ee56d | ||
|
|
5761d37145 |
@@ -0,0 +1,99 @@
|
|||||||
|
# For most projects, this workflow file will not need changing; you simply need
|
||||||
|
# to commit it to your repository.
|
||||||
|
#
|
||||||
|
# You may wish to alter this file to override the set of languages analyzed,
|
||||||
|
# or to provide custom queries or build logic.
|
||||||
|
#
|
||||||
|
# ******** NOTE ********
|
||||||
|
# We have attempted to detect the languages in your repository. Please check
|
||||||
|
# the `language` matrix defined below to confirm you have the correct set of
|
||||||
|
# supported CodeQL languages.
|
||||||
|
#
|
||||||
|
name: "CodeQL Advanced"
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [ "main" ]
|
||||||
|
pull_request:
|
||||||
|
branches: [ "main" ]
|
||||||
|
schedule:
|
||||||
|
- cron: '15 7 * * 2'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
analyze:
|
||||||
|
name: Analyze (${{ matrix.language }})
|
||||||
|
# Runner size impacts CodeQL analysis time. To learn more, please see:
|
||||||
|
# - https://gh.io/recommended-hardware-resources-for-running-codeql
|
||||||
|
# - https://gh.io/supported-runners-and-hardware-resources
|
||||||
|
# - https://gh.io/using-larger-runners (GitHub.com only)
|
||||||
|
# Consider using larger runners or machines with greater resources for possible analysis time improvements.
|
||||||
|
runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }}
|
||||||
|
permissions:
|
||||||
|
# required for all workflows
|
||||||
|
security-events: write
|
||||||
|
|
||||||
|
# required to fetch internal or private CodeQL packs
|
||||||
|
packages: read
|
||||||
|
|
||||||
|
# only required for workflows in private repositories
|
||||||
|
actions: read
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
- language: javascript-typescript
|
||||||
|
build-mode: none
|
||||||
|
# CodeQL supports the following values keywords for 'language': 'actions', 'c-cpp', 'csharp', 'go', 'java-kotlin', 'javascript-typescript', 'python', 'ruby', 'rust', 'swift'
|
||||||
|
# Use `c-cpp` to analyze code written in C, C++ or both
|
||||||
|
# Use 'java-kotlin' to analyze code written in Java, Kotlin or both
|
||||||
|
# Use 'javascript-typescript' to analyze code written in JavaScript, TypeScript or both
|
||||||
|
# To learn more about changing the languages that are analyzed or customizing the build mode for your analysis,
|
||||||
|
# see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/customizing-your-advanced-setup-for-code-scanning.
|
||||||
|
# If you are analyzing a compiled language, you can modify the 'build-mode' for that language to customize how
|
||||||
|
# your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v7
|
||||||
|
|
||||||
|
# Add any setup steps before running the `github/codeql-action/init` action.
|
||||||
|
# This includes steps like installing compilers or runtimes (`actions/setup-node`
|
||||||
|
# or others). This is typically only required for manual builds.
|
||||||
|
# - name: Setup runtime (example)
|
||||||
|
# uses: actions/setup-example@v1
|
||||||
|
|
||||||
|
# Initializes the CodeQL tools for scanning.
|
||||||
|
- name: Initialize CodeQL
|
||||||
|
uses: github/codeql-action/init@v4
|
||||||
|
with:
|
||||||
|
languages: ${{ matrix.language }}
|
||||||
|
build-mode: ${{ matrix.build-mode }}
|
||||||
|
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||||
|
# By default, queries listed here will override any specified in a config file.
|
||||||
|
# Prefix the list here with "+" to use these queries and those in the config file.
|
||||||
|
|
||||||
|
# For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
|
||||||
|
# queries: security-extended,security-and-quality
|
||||||
|
|
||||||
|
# If the analyze step fails for one of the languages you are analyzing with
|
||||||
|
# "We were unable to automatically build your code", modify the matrix above
|
||||||
|
# to set the build mode to "manual" for that language. Then modify this step
|
||||||
|
# to build your code.
|
||||||
|
# ℹ️ Command-line programs to run using the OS shell.
|
||||||
|
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
|
||||||
|
- name: Run manual build steps
|
||||||
|
if: matrix.build-mode == 'manual'
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
echo 'If you are using a "manual" build mode for one or more of the' \
|
||||||
|
'languages you are analyzing, replace this with the commands to build' \
|
||||||
|
'your code, for example:'
|
||||||
|
echo ' make bootstrap'
|
||||||
|
echo ' make release'
|
||||||
|
exit 1
|
||||||
|
|
||||||
|
- name: Perform CodeQL Analysis
|
||||||
|
uses: github/codeql-action/analyze@v4
|
||||||
|
with:
|
||||||
|
category: "/language:${{matrix.language}}"
|
||||||
@@ -1,45 +1,53 @@
|
|||||||
# BenthicBloom
|
# BenthicBloom
|
||||||
|
|
||||||
A GNOME Shell 50 extension that automatically rotates your wallpaper,
|
|
||||||
supports looping video (live) wallpapers, and includes built-in
|
https://github.com/user-attachments/assets/cf95d723-7c0d-44b1-9b8f-01e03f88850b
|
||||||
protection against OLED burn-in.
|
|
||||||
|
|
||||||
|
A GNOME Shell 50 extension that rotates your wallpaper, supports looping
|
||||||
|
video (live) wallpapers, and protects OLED displays from burn-in.
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
- **Auto rotation** — pick one or more folders and BenthicBloom cycles
|
- **Auto rotation**: pick one or more folders and BenthicBloom cycles
|
||||||
through the images inside them on a timer, in sequential or shuffled
|
through the images inside them on a timer, sequential or shuffled, with
|
||||||
order, with an optional crossfade between changes. The chosen wallpaper
|
an optional crossfade. Can also apply to the lock screen.
|
||||||
can also be applied to the lock screen.
|
- **Live wallpapers**: play a looping video or animated GIF as your
|
||||||
- **Live wallpapers** — play a looping video *or animated GIF* as your
|
|
||||||
desktop background. Frames are decoded with GStreamer and rendered
|
desktop background. Frames are decoded with GStreamer and rendered
|
||||||
directly by GNOME Shell's own Clutter stage (no external, incompatible
|
directly on GNOME Shell's own Clutter stage. Playback can pause on
|
||||||
Clutter build involved). Playback can automatically pause on battery
|
battery power or while a window is fullscreen. Auto rotation suspends
|
||||||
power or while a window is fullscreen. Automatic rotation is
|
while a live wallpaper is on screen and resumes after.
|
||||||
automatically suspended while a live wallpaper is actually on screen
|
- **OLED burn-in protection**, three independent toggles:
|
||||||
(and resumes afterward), since changing the static wallpaper would
|
|
||||||
otherwise repaint over the video.
|
|
||||||
- **OLED burn-in protection** — three independent, individually toggled
|
|
||||||
techniques:
|
|
||||||
- *Pixel shifting*: nudges the background a few pixels on a slow drift
|
- *Pixel shifting*: nudges the background a few pixels on a slow drift
|
||||||
cycle so the same subpixels aren't lit continuously.
|
cycle so the same subpixels aren't lit continuously.
|
||||||
- *Idle dimming*: fades the background to a configurable lower
|
- *Idle dimming*: fades the background after the system has been idle
|
||||||
brightness after the system has been idle for a while, and restores
|
for a while, restores it when you're back.
|
||||||
it the moment you're back.
|
- *Forced rotation*: guarantees a wallpaper change after a maximum
|
||||||
- *Forced rotation*: guarantees the wallpaper changes after a maximum
|
static duration, even if auto rotation is off.
|
||||||
static duration, even if automatic rotation is otherwise switched
|
|
||||||
off.
|
|
||||||
- A top-bar indicator for quick access to rotation, live wallpaper, and
|
- A top-bar indicator for quick access to rotation, live wallpaper, and
|
||||||
OLED protection toggles, plus a "Next Wallpaper" action, and a full
|
OLED protection toggles, a "Next Wallpaper" action, and a full
|
||||||
libadwaita preferences window.
|
libadwaita preferences window.
|
||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
|
|
||||||
- GNOME Shell 50.
|
- GNOME Shell 50.
|
||||||
- For live wallpapers: GStreamer with its `good` and `base` plugin sets
|
- For live wallpapers: GStreamer's `base` and `good` plugin sets (the
|
||||||
(e.g. `gstreamer1.0-plugins-good` and `gstreamer1.0-plugins-base`, or
|
`good` set also provides GIF decoding), with GObject-Introspection data.
|
||||||
your distribution's equivalent) — the `good` set is also what provides
|
Without them the live wallpaper toggle stays disabled; everything else
|
||||||
animated GIF decoding. If these aren't installed, the live wallpaper
|
works normally.
|
||||||
toggle stays disabled and BenthicBloom's other features work normally.
|
|
||||||
|
```sh
|
||||||
|
# Arch
|
||||||
|
sudo pacman -S gst-plugins-base gst-plugins-good gst-plugins-bad gst-plugins-ugly gst-libav
|
||||||
|
|
||||||
|
# Debian/Ubuntu
|
||||||
|
sudo apt install gstreamer1.0-plugins-base gstreamer1.0-plugins-good gir1.2-gst-plugins-base-1.0
|
||||||
|
|
||||||
|
# Fedora
|
||||||
|
sudo dnf install gstreamer1-plugins-base gstreamer1-plugins-good gobject-introspection
|
||||||
|
```
|
||||||
|
|
||||||
|
Restart GNOME Shell after installing (log out and back in on Wayland).
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
@@ -51,9 +59,8 @@ cd benthicbloom
|
|||||||
make install
|
make install
|
||||||
```
|
```
|
||||||
|
|
||||||
Then reload GNOME Shell — press <kbd>Alt</kbd>+<kbd>F2</kbd>, type `r`,
|
Reload GNOME Shell (<kbd>Alt</kbd>+<kbd>F2</kbd>, type `r`, <kbd>Enter</kbd>
|
||||||
press <kbd>Enter</kbd> on X11, or log out and back in on Wayland — and
|
on X11; log out and back in on Wayland), then enable the extension:
|
||||||
enable the extension:
|
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
gnome-extensions enable benthicbloom@quinta0.github.io
|
gnome-extensions enable benthicbloom@quinta0.github.io
|
||||||
@@ -70,55 +77,60 @@ installable via `gnome-extensions install <file>` or the Extensions app.
|
|||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
|
|
||||||
Open preferences from the panel indicator's "Wallpaper Settings…" entry,
|
Open preferences from the panel indicator's "Wallpaper Settings..." entry,
|
||||||
or run:
|
or run:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
gnome-extensions prefs benthicbloom@quinta0.github.io
|
gnome-extensions prefs benthicbloom@quinta0.github.io
|
||||||
```
|
```
|
||||||
|
|
||||||
- **General** — panel indicator visibility, lock screen syncing, debug
|
- **General**: panel indicator visibility, lock screen syncing, debug
|
||||||
logging, and the folders scanned for wallpapers.
|
logging, and the folders scanned for wallpapers.
|
||||||
- **Rotation** — enable/disable, interval, shuffle vs. sequential order,
|
- **Rotation**: enable/disable, interval, shuffle vs. sequential order,
|
||||||
and crossfade transition settings.
|
crossfade settings.
|
||||||
- **Live Wallpaper** — enable/disable, video file, source folders to pick
|
- **Live Wallpaper**: enable/disable, video file, source folders, mute,
|
||||||
a video/GIF from, mute, playback speed, and power-saving pause behavior.
|
playback speed, power-saving pause behavior.
|
||||||
- **OLED Protection** — master switch plus independent controls for
|
- **OLED Protection**: master switch plus independent controls for pixel
|
||||||
pixel shifting, idle dimming, and forced periodic rotation.
|
shifting, idle dimming, and forced periodic rotation.
|
||||||
|
|
||||||
## Architecture
|
## Architecture
|
||||||
|
|
||||||
```
|
```
|
||||||
extension.js Entry point: wires up the three managers + indicator
|
extension.js Entry point: wires up the three managers + indicator
|
||||||
prefs.js libadwaita preferences window
|
prefs.js libadwaita preferences window
|
||||||
lib/settingsKeys.js GSettings key name constants
|
lib/settingsKeys.js GSettings key name constants
|
||||||
lib/logger.js Small logging wrapper gated by the debug-logging setting
|
lib/logger.js Small logging wrapper gated by the debug-logging setting
|
||||||
lib/wallpaperSource.js Async folder scanning for image files
|
lib/wallpaperSource.js Async folder scanning for image files
|
||||||
lib/shuffleBag.js No-immediate-repeat random ordering for shuffle mode
|
lib/shuffleBag.js No-immediate-repeat random ordering for shuffle mode
|
||||||
lib/rotationManager.js Timer-driven wallpaper rotation + crossfade overlay
|
lib/rotationManager.js Timer-driven wallpaper rotation + crossfade overlay
|
||||||
lib/liveWallpaper.js GStreamer video playback rendered onto a Clutter actor
|
lib/gstreamerAvailability.js Non-blocking GStreamer init/registry check + availability probe
|
||||||
lib/oledProtection.js Pixel shifting, idle dimming, forced rotation
|
lib/liveWallpaper.js GStreamer video playback rendered onto a Clutter actor
|
||||||
lib/indicator.js Top-bar quick-access menu
|
lib/oledProtection.js Pixel shifting, idle dimming, forced rotation
|
||||||
icons/ Bundled symbolic icons for the preferences window
|
lib/indicator.js Top-bar quick-access menu
|
||||||
schemas/ GSettings schema
|
icons/ Bundled symbolic icons for the preferences window
|
||||||
|
schemas/ GSettings schema
|
||||||
```
|
```
|
||||||
|
|
||||||
## Known limitations
|
## Known limitations
|
||||||
|
|
||||||
- The live wallpaper currently renders across the full stage as a single
|
- The live wallpaper renders across the full stage as a single layer, so
|
||||||
layer, so on multi-monitor setups the video spans across all monitors
|
on multi-monitor setups the video spans all monitors as one canvas
|
||||||
as one canvas rather than being tiled per-monitor.
|
instead of being tiled per-monitor.
|
||||||
- Pixel shifting and idle dimming rely on GNOME Shell's private
|
- Pixel shifting and idle dimming rely on GNOME Shell's private
|
||||||
`Main.layoutManager._backgroundGroup` and `Meta.IdleMonitor` APIs, which
|
`Main.layoutManager._backgroundGroup` and `Meta.IdleMonitor` APIs, not
|
||||||
are not part of the stable extension API and could change in future
|
part of the stable extension API, and could change in future shell
|
||||||
shell versions.
|
versions.
|
||||||
|
- GStreamer's registry scan is skipped (trusting the existing cache)
|
||||||
|
unless no cache exists yet, to avoid blocking gnome-shell's main thread,
|
||||||
|
and therefore the whole screen, for several seconds right after login.
|
||||||
|
A background subprocess refreshes the cache afterward so newly
|
||||||
|
installed plugins still become visible eventually.
|
||||||
|
|
||||||
## Testing
|
## Testing
|
||||||
|
|
||||||
This was developed and validated (JSON, GSettings schema compilation,
|
This was developed and validated (JSON, GSettings schema compilation, and
|
||||||
and JavaScript syntax) without a live GNOME Shell 50 session available in
|
JavaScript syntax) without a live GNOME Shell 50 session available in the
|
||||||
the development environment. Before relying on it, test in a nested
|
development environment. Before relying on it, test in a nested session:
|
||||||
session:
|
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
dbus-run-session -- gnome-shell --nested --wayland
|
dbus-run-session -- gnome-shell --nested --wayland
|
||||||
@@ -129,4 +141,4 @@ you hit.
|
|||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
GPL-3.0-or-later — see [LICENSE](LICENSE).
|
GPL-3.0-or-later, see [LICENSE](LICENSE).
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
|
||||||
<g fill="none" stroke="#2e3436" stroke-width="1.3" stroke-linecap="round">
|
<g fill="none" stroke="#2e3436" stroke-width="1.6" stroke-linecap="round">
|
||||||
<line x1="3.5" y1="2" x2="3.5" y2="14"/>
|
<line x1="3.5" y1="2" x2="3.5" y2="14"/>
|
||||||
<line x1="8" y1="2" x2="8" y2="14"/>
|
<line x1="8" y1="2" x2="8" y2="14"/>
|
||||||
<line x1="12.5" y1="2" x2="12.5" y2="14"/>
|
<line x1="12.5" y1="2" x2="12.5" y2="14"/>
|
||||||
</g>
|
</g>
|
||||||
<circle cx="3.5" cy="5.5" r="1.7" fill="#2e3436"/>
|
<circle cx="3.5" cy="5.3" r="2" fill="#2e3436"/>
|
||||||
<circle cx="8" cy="10.5" r="1.7" fill="#2e3436"/>
|
<circle cx="8" cy="10.7" r="2" fill="#2e3436"/>
|
||||||
<circle cx="12.5" cy="7" r="1.7" fill="#2e3436"/>
|
<circle cx="12.5" cy="7" r="2" fill="#2e3436"/>
|
||||||
</svg>
|
</svg>
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 465 B After Width: | Height: | Size: 459 B |
@@ -1,4 +1,4 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
|
||||||
<rect x="1.5" y="3" width="13" height="10" rx="1.8" fill="none" stroke="#2e3436" stroke-width="1.3"/>
|
<rect x="1.3" y="2.8" width="13.4" height="10.4" rx="2" fill="none" stroke="#2e3436" stroke-width="1.5"/>
|
||||||
<path d="M 6.6 5.8 L 6.6 10.2 L 10.4 8 Z" fill="#2e3436"/>
|
<path d="M 6.2 5.3 L 6.2 10.7 L 10.7 8 Z" fill="#2e3436"/>
|
||||||
</svg>
|
</svg>
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 256 B After Width: | Height: | Size: 260 B |
@@ -1,6 +1,4 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
|
||||||
<path d="M 3.11 6.22 A 5.2 5.2 0 0 1 13.12 7.10" fill="none" stroke="#2e3436" stroke-width="1.4" stroke-linecap="round"/>
|
<path d="M 12.8 5.6 A 5.3 5.3 0 1 1 11.4 2.9" fill="none" stroke="#2e3436" stroke-width="1.6" stroke-linecap="round"/>
|
||||||
<path d="M 12.89 9.78 A 5.2 5.2 0 0 1 2.88 8.90" fill="none" stroke="#2e3436" stroke-width="1.4" stroke-linecap="round"/>
|
<path d="M 13.9,2.6 13.5,6.4 10.2,4.5 Z" fill="#2e3436"/>
|
||||||
<path d="M 13.52,9.36 11.38,7.00 14.73,6.41 Z" fill="#2e3436"/>
|
|
||||||
<path d="M 2.48,6.64 4.62,9.00 1.27,9.59 Z" fill="#2e3436"/>
|
|
||||||
</svg>
|
</svg>
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 468 B After Width: | Height: | Size: 272 B |
@@ -1,3 +1,67 @@
|
|||||||
|
import Gio from 'gi://Gio';
|
||||||
|
import GLib from 'gi://GLib';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* True if a GStreamer plugin registry cache already exists for this user.
|
||||||
|
* A plain local directory listing — fast, unlike the scan below.
|
||||||
|
*/
|
||||||
|
function hasGstRegistryCache() {
|
||||||
|
const cacheDir = Gio.File.new_for_path(
|
||||||
|
GLib.build_filenamev([GLib.get_user_cache_dir(), 'gstreamer-1.0']));
|
||||||
|
try {
|
||||||
|
const enumerator = cacheDir.enumerate_children('standard::name', Gio.FileQueryInfoFlags.NONE, null);
|
||||||
|
const hasEntry = enumerator.next_file(null) !== null;
|
||||||
|
enumerator.close(null);
|
||||||
|
return hasEntry;
|
||||||
|
} catch (e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gst.init() has no async variant. If its plugin registry cache is stale
|
||||||
|
* (e.g. right after a system update touched GStreamer packages), it
|
||||||
|
* synchronously rescans every plugin — spawning gst-plugin-scanner per
|
||||||
|
* plugin, and probing hardware for codec plugins — which can block
|
||||||
|
* gnome-shell's single main thread (it *is* the compositor) for many
|
||||||
|
* seconds, exactly the conditions right after login. That froze the entire
|
||||||
|
* screen the same way the blocking UPower proxy lookup did.
|
||||||
|
*
|
||||||
|
* GST_REGISTRY_UPDATE=no makes Gst.init() trust the existing cache
|
||||||
|
* unconditionally instead of validating it, so it returns immediately. Only
|
||||||
|
* set when a cache already exists — an extension-lifetime-first-ever
|
||||||
|
* GStreamer run on this machine still needs one real scan to have anything
|
||||||
|
* to trust.
|
||||||
|
*/
|
||||||
|
function keepGstInitFast() {
|
||||||
|
if (hasGstRegistryCache())
|
||||||
|
GLib.setenv('GST_REGISTRY_UPDATE', 'no', true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Refreshes the on-disk registry cache in a throwaway subprocess, so a
|
||||||
|
* stale cache (skipped above) doesn't stay stale forever and newly
|
||||||
|
* installed plugins eventually become visible. Runs fully out-of-process:
|
||||||
|
* even if a hardware probe inside it hangs, gnome-shell is unaffected.
|
||||||
|
*/
|
||||||
|
function refreshGstRegistryInBackground() {
|
||||||
|
try {
|
||||||
|
const launcher = new Gio.SubprocessLauncher(
|
||||||
|
Gio.SubprocessFlags.STDOUT_SILENCE | Gio.SubprocessFlags.STDERR_SILENCE);
|
||||||
|
launcher.setenv('GST_REGISTRY_UPDATE', 'yes', true);
|
||||||
|
const proc = launcher.spawnv(['gst-inspect-1.0', '--exists', 'playbin']);
|
||||||
|
proc.wait_check_async(null, (source, result) => {
|
||||||
|
try {
|
||||||
|
source.wait_check_finish(result);
|
||||||
|
} catch (e) {
|
||||||
|
// Best-effort refresh; a missing binary or nonzero exit just leaves the cache as-is.
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
// Best-effort refresh; a missing gst-inspect-1.0 just leaves the cache as-is.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Loads the GObject-Introspection bindings the live wallpaper feature needs
|
* Loads the GObject-Introspection bindings the live wallpaper feature needs
|
||||||
* (Gst, GstApp for the appsink signals, Cogl for uploading decoded frames)
|
* (Gst, GstApp for the appsink signals, Cogl for uploading decoded frames)
|
||||||
@@ -13,6 +77,8 @@
|
|||||||
* checkGstreamerBaseAvailable() there instead.
|
* checkGstreamerBaseAvailable() there instead.
|
||||||
*/
|
*/
|
||||||
export async function loadGstreamerModules() {
|
export async function loadGstreamerModules() {
|
||||||
|
keepGstInitFast();
|
||||||
|
|
||||||
const [{default: Gst}, , {default: Cogl}] = await Promise.all([
|
const [{default: Gst}, , {default: Cogl}] = await Promise.all([
|
||||||
import('gi://Gst?version=1.0'),
|
import('gi://Gst?version=1.0'),
|
||||||
import('gi://GstApp?version=1.0'),
|
import('gi://GstApp?version=1.0'),
|
||||||
@@ -22,6 +88,8 @@ export async function loadGstreamerModules() {
|
|||||||
if (!Gst.is_initialized())
|
if (!Gst.is_initialized())
|
||||||
Gst.init(null);
|
Gst.init(null);
|
||||||
|
|
||||||
|
refreshGstRegistryInBackground();
|
||||||
|
|
||||||
return {Gst, Cogl};
|
return {Gst, Cogl};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -33,6 +101,8 @@ export async function loadGstreamerModules() {
|
|||||||
* failure here is a genuine, actionable problem, unlike a Cogl probe.
|
* failure here is a genuine, actionable problem, unlike a Cogl probe.
|
||||||
*/
|
*/
|
||||||
export async function checkGstreamerBaseAvailable() {
|
export async function checkGstreamerBaseAvailable() {
|
||||||
|
keepGstInitFast();
|
||||||
|
|
||||||
const [{default: Gst}] = await Promise.all([
|
const [{default: Gst}] = await Promise.all([
|
||||||
import('gi://Gst?version=1.0'),
|
import('gi://Gst?version=1.0'),
|
||||||
import('gi://GstApp?version=1.0'),
|
import('gi://GstApp?version=1.0'),
|
||||||
@@ -40,6 +110,8 @@ export async function checkGstreamerBaseAvailable() {
|
|||||||
|
|
||||||
if (!Gst.is_initialized())
|
if (!Gst.is_initialized())
|
||||||
Gst.init(null);
|
Gst.init(null);
|
||||||
|
|
||||||
|
refreshGstRegistryInBackground();
|
||||||
}
|
}
|
||||||
|
|
||||||
export const GSTREAMER_INSTALL_HINT =
|
export const GSTREAMER_INSTALL_HINT =
|
||||||
|
|||||||
+248
-13
@@ -1,6 +1,7 @@
|
|||||||
import GLib from 'gi://GLib';
|
import GLib from 'gi://GLib';
|
||||||
import Gio from 'gi://Gio';
|
import Gio from 'gi://Gio';
|
||||||
import Clutter from 'gi://Clutter';
|
import Clutter from 'gi://Clutter';
|
||||||
|
import Graphene from 'gi://Graphene';
|
||||||
import St from 'gi://St';
|
import St from 'gi://St';
|
||||||
import * as Main from 'resource:///org/gnome/shell/ui/main.js';
|
import * as Main from 'resource:///org/gnome/shell/ui/main.js';
|
||||||
|
|
||||||
@@ -77,6 +78,9 @@ export class LiveWallpaperManager {
|
|||||||
|
|
||||||
this._settingsSignals = [];
|
this._settingsSignals = [];
|
||||||
this._busWatchId = 0;
|
this._busWatchId = 0;
|
||||||
|
this._eosSeekTimeoutId = 0;
|
||||||
|
this._lastEosSeekTimeMs = 0;
|
||||||
|
this._pendingInitialRateSeek = false;
|
||||||
this._monitorsChangedId = 0;
|
this._monitorsChangedId = 0;
|
||||||
this._fullscreenChangedId = 0;
|
this._fullscreenChangedId = 0;
|
||||||
this._upowerProxy = null;
|
this._upowerProxy = null;
|
||||||
@@ -89,6 +93,9 @@ export class LiveWallpaperManager {
|
|||||||
this._noFrameWatchdogId = 0;
|
this._noFrameWatchdogId = 0;
|
||||||
this._imageStrategyIndex = 0;
|
this._imageStrategyIndex = 0;
|
||||||
this._frameErrorCount = 0;
|
this._frameErrorCount = 0;
|
||||||
|
|
||||||
|
this._transitionOverlay = null;
|
||||||
|
this._fadeInPending = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
get isAvailable() {
|
get isAvailable() {
|
||||||
@@ -113,6 +120,7 @@ export class LiveWallpaperManager {
|
|||||||
this._settings.connect(`changed::${SettingsKey.LIVE_WALLPAPER_ENABLED}`, () => this._sync()),
|
this._settings.connect(`changed::${SettingsKey.LIVE_WALLPAPER_ENABLED}`, () => this._sync()),
|
||||||
this._settings.connect(`changed::${SettingsKey.LIVE_WALLPAPER_PATH}`, () => this._sync()),
|
this._settings.connect(`changed::${SettingsKey.LIVE_WALLPAPER_PATH}`, () => this._sync()),
|
||||||
this._settings.connect(`changed::${SettingsKey.LIVE_WALLPAPER_MUTED}`, () => this._applyMute()),
|
this._settings.connect(`changed::${SettingsKey.LIVE_WALLPAPER_MUTED}`, () => this._applyMute()),
|
||||||
|
this._settings.connect(`changed::${SettingsKey.LIVE_WALLPAPER_DISPLAY_MODE}`, () => this._applyDisplayMode()),
|
||||||
this._settings.connect(`changed::${SettingsKey.LIVE_WALLPAPER_PLAYBACK_RATE}`, () => this._applyPlaybackRate()),
|
this._settings.connect(`changed::${SettingsKey.LIVE_WALLPAPER_PLAYBACK_RATE}`, () => this._applyPlaybackRate()),
|
||||||
this._settings.connect(`changed::${SettingsKey.LIVE_WALLPAPER_PAUSE_ON_BATTERY}`, () => this._updatePauseState()),
|
this._settings.connect(`changed::${SettingsKey.LIVE_WALLPAPER_PAUSE_ON_BATTERY}`, () => this._updatePauseState()),
|
||||||
this._settings.connect(`changed::${SettingsKey.LIVE_WALLPAPER_PAUSE_WHEN_FULLSCREEN}`, () => this._updatePauseState())
|
this._settings.connect(`changed::${SettingsKey.LIVE_WALLPAPER_PAUSE_WHEN_FULLSCREEN}`, () => this._updatePauseState())
|
||||||
@@ -134,6 +142,7 @@ export class LiveWallpaperManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this._stop();
|
this._stop();
|
||||||
|
this._clearTransition();
|
||||||
}
|
}
|
||||||
|
|
||||||
async _loadGstreamer() {
|
async _loadGstreamer() {
|
||||||
@@ -157,12 +166,27 @@ export class LiveWallpaperManager {
|
|||||||
if (shouldRun)
|
if (shouldRun)
|
||||||
this._active ? this._restart() : this._start();
|
this._active ? this._restart() : this._start();
|
||||||
else if (this._active)
|
else if (this._active)
|
||||||
this._stop();
|
this._stop({animate: true});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Switching to a different file while already active. If transitions
|
||||||
|
* are on, the outgoing actor (already showing the old file's last
|
||||||
|
* frame) is detached before _stop() would destroy it, so it can be
|
||||||
|
* kept on screen and eased away by _playTransition() after _start()
|
||||||
|
* has the new one playing underneath — a crossfade between the two.
|
||||||
|
*/
|
||||||
_restart() {
|
_restart() {
|
||||||
|
const transitionsOn = this._settings.get_boolean(SettingsKey.TRANSITION_ENABLED);
|
||||||
|
const outgoingActor = transitionsOn ? this._actor : null;
|
||||||
|
if (outgoingActor)
|
||||||
|
this._actor = null;
|
||||||
|
|
||||||
this._stop();
|
this._stop();
|
||||||
this._start();
|
this._start();
|
||||||
|
|
||||||
|
if (outgoingActor)
|
||||||
|
this._playTransition(outgoingActor);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -233,11 +257,15 @@ export class LiveWallpaperManager {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
this._actor = new Clutter.Actor({
|
this._actor = new Clutter.Actor({
|
||||||
content_gravity: Clutter.ContentGravity.RESIZE_ASPECT,
|
|
||||||
reactive: false,
|
reactive: false,
|
||||||
|
pivot_point: new Graphene.Point({x: 0.5, y: 0.5}),
|
||||||
});
|
});
|
||||||
Main.layoutManager._backgroundGroup.add_child(this._actor);
|
Main.layoutManager._backgroundGroup.add_child(this._actor);
|
||||||
this._layoutActor();
|
this._applyDisplayMode();
|
||||||
|
|
||||||
|
this._fadeInPending = this._settings.get_boolean(SettingsKey.TRANSITION_ENABLED);
|
||||||
|
if (this._fadeInPending)
|
||||||
|
this._actor.opacity = 0;
|
||||||
|
|
||||||
const sinkBin = this._buildSinkBin();
|
const sinkBin = this._buildSinkBin();
|
||||||
|
|
||||||
@@ -252,6 +280,17 @@ export class LiveWallpaperManager {
|
|||||||
this._applyMute();
|
this._applyMute();
|
||||||
const stateChangeResult = this._playbin.set_state(Gst.State.PLAYING);
|
const stateChangeResult = this._playbin.set_state(Gst.State.PLAYING);
|
||||||
this._logger.debug(`playbin.set_state(PLAYING) returned ${stateChangeResult}`);
|
this._logger.debug(`playbin.set_state(PLAYING) returned ${stateChangeResult}`);
|
||||||
|
// Seeking here immediately would race the pipeline's preroll —
|
||||||
|
// GStreamer can only seek once a state change has actually
|
||||||
|
// completed, so an early seek is silently dropped and playback
|
||||||
|
// sticks at native 1.0x. Instead, defer to the first ASYNC_DONE
|
||||||
|
// bus message (handled in _onBusMessage), which fires once the
|
||||||
|
// pipeline is actually ready to seek.
|
||||||
|
const initialRate = this._settings.get_double(SettingsKey.LIVE_WALLPAPER_PLAYBACK_RATE);
|
||||||
|
this._pendingInitialRateSeek = initialRate !== 1.0;
|
||||||
|
this._logger.debug(
|
||||||
|
`Live wallpaper: configured playback rate ${initialRate}, ` +
|
||||||
|
`deferring initial seek to ASYNC_DONE: ${this._pendingInitialRateSeek}`);
|
||||||
|
|
||||||
this._frameCount = 0;
|
this._frameCount = 0;
|
||||||
this._pollCount = 0;
|
this._pollCount = 0;
|
||||||
@@ -312,9 +351,17 @@ export class LiveWallpaperManager {
|
|||||||
try {
|
try {
|
||||||
this._updateFrame(mapInfo.data, width, height);
|
this._updateFrame(mapInfo.data, width, height);
|
||||||
this._frameCount++;
|
this._frameCount++;
|
||||||
if (this._frameCount === 1)
|
if (this._frameCount === 1) {
|
||||||
this._logger.debug(`Live wallpaper: first frame received (${width}x${height})`);
|
this._logger.debug(`Live wallpaper: first frame received (${width}x${height})`);
|
||||||
else if (this._frameCount % 120 === 0)
|
if (this._fadeInPending) {
|
||||||
|
this._fadeInPending = false;
|
||||||
|
this._actor.ease({
|
||||||
|
opacity: 255,
|
||||||
|
duration: this._settings.get_uint(SettingsKey.TRANSITION_DURATION_MS),
|
||||||
|
mode: Clutter.AnimationMode.EASE_OUT_QUAD,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else if (this._frameCount % 120 === 0)
|
||||||
this._logger.debug(`Live wallpaper: ${this._frameCount} frames rendered so far`);
|
this._logger.debug(`Live wallpaper: ${this._frameCount} frames rendered so far`);
|
||||||
} finally {
|
} finally {
|
||||||
buffer.unmap(mapInfo);
|
buffer.unmap(mapInfo);
|
||||||
@@ -346,6 +393,7 @@ export class LiveWallpaperManager {
|
|||||||
this._image = strategy.create(width, height);
|
this._image = strategy.create(width, height);
|
||||||
this._videoWidth = width;
|
this._videoWidth = width;
|
||||||
this._videoHeight = height;
|
this._videoHeight = height;
|
||||||
|
this._layoutActor();
|
||||||
}
|
}
|
||||||
strategy.upload(this._image, this._Cogl, data, width, height);
|
strategy.upload(this._image, this._Cogl, data, width, height);
|
||||||
this._actor.set_content(this._image);
|
this._actor.set_content(this._image);
|
||||||
@@ -366,8 +414,15 @@ export class LiveWallpaperManager {
|
|||||||
const Gst = this._Gst;
|
const Gst = this._Gst;
|
||||||
switch (message.type) {
|
switch (message.type) {
|
||||||
case Gst.MessageType.EOS:
|
case Gst.MessageType.EOS:
|
||||||
this._playbin.seek_simple(
|
this._loopPlayback();
|
||||||
Gst.Format.TIME, Gst.SeekFlags.FLUSH | Gst.SeekFlags.KEY_UNIT, 0);
|
break;
|
||||||
|
case Gst.MessageType.ASYNC_DONE:
|
||||||
|
this._logger.debug(
|
||||||
|
`Live wallpaper: ASYNC_DONE received (pending initial rate seek: ${this._pendingInitialRateSeek})`);
|
||||||
|
if (this._pendingInitialRateSeek) {
|
||||||
|
this._pendingInitialRateSeek = false;
|
||||||
|
this._seekToLoopStart();
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
case Gst.MessageType.ERROR: {
|
case Gst.MessageType.ERROR: {
|
||||||
const [error, debug] = message.parse_error();
|
const [error, debug] = message.parse_error();
|
||||||
@@ -389,6 +444,50 @@ export class LiveWallpaperManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Restarts playback at EOS. `seek()` with GST_SEEK_FLAG_FLUSH blocks
|
||||||
|
* the calling thread until the pipeline flushes — and since this whole
|
||||||
|
* extension runs on gnome-shell's own main loop, that blocks the entire
|
||||||
|
* desktop for as long as it takes. At rate 1.0 that's one blocking call
|
||||||
|
* per full playthrough, tolerable; at higher rates a short clip loops
|
||||||
|
* (and EOS fires) many times a second, turning it into a sustained
|
||||||
|
* back-to-back stall that reads as a hard freeze. Throttling to at most
|
||||||
|
* one restart per EOS_SEEK_MIN_INTERVAL_MS gives the main loop room to
|
||||||
|
* breathe between them.
|
||||||
|
*/
|
||||||
|
_loopPlayback() {
|
||||||
|
const nowMs = GLib.get_monotonic_time() / 1000;
|
||||||
|
const elapsedMs = nowMs - this._lastEosSeekTimeMs;
|
||||||
|
const minIntervalMs = 150;
|
||||||
|
|
||||||
|
if (elapsedMs >= minIntervalMs) {
|
||||||
|
this._seekToLoopStart();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this._eosSeekTimeoutId)
|
||||||
|
return;
|
||||||
|
this._eosSeekTimeoutId = GLib.timeout_add(GLib.PRIORITY_DEFAULT, minIntervalMs - elapsedMs, () => {
|
||||||
|
this._eosSeekTimeoutId = 0;
|
||||||
|
this._seekToLoopStart();
|
||||||
|
return GLib.SOURCE_REMOVE;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
_seekToLoopStart() {
|
||||||
|
if (!this._playbin)
|
||||||
|
return;
|
||||||
|
|
||||||
|
this._lastEosSeekTimeMs = GLib.get_monotonic_time() / 1000;
|
||||||
|
const Gst = this._Gst;
|
||||||
|
const rate = this._settings.get_double(SettingsKey.LIVE_WALLPAPER_PLAYBACK_RATE);
|
||||||
|
const ok = this._playbin.seek(
|
||||||
|
rate, Gst.Format.TIME, Gst.SeekFlags.FLUSH | Gst.SeekFlags.KEY_UNIT,
|
||||||
|
Gst.SeekType.SET, 0, Gst.SeekType.NONE, -1);
|
||||||
|
this._logger.debug(
|
||||||
|
`Live wallpaper: loop-restart seek to rate ${rate} ${ok ? 'succeeded' : 'FAILED (seek() returned false)'}`);
|
||||||
|
}
|
||||||
|
|
||||||
_stateName(state) {
|
_stateName(state) {
|
||||||
const Gst = this._Gst;
|
const Gst = this._Gst;
|
||||||
return Object.keys(Gst.State).find(name => Gst.State[name] === state) ?? String(state);
|
return Object.keys(Gst.State).find(name => Gst.State[name] === state) ?? String(state);
|
||||||
@@ -406,19 +505,74 @@ export class LiveWallpaperManager {
|
|||||||
const Gst = this._Gst;
|
const Gst = this._Gst;
|
||||||
const rate = this._settings.get_double(SettingsKey.LIVE_WALLPAPER_PLAYBACK_RATE);
|
const rate = this._settings.get_double(SettingsKey.LIVE_WALLPAPER_PLAYBACK_RATE);
|
||||||
const [ok, position] = this._playbin.query_position(Gst.Format.TIME);
|
const [ok, position] = this._playbin.query_position(Gst.Format.TIME);
|
||||||
if (!ok)
|
if (!ok) {
|
||||||
|
this._logger.debug('Live wallpaper: applyPlaybackRate aborted, query_position() failed');
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
this._playbin.seek(
|
const seekOk = this._playbin.seek(
|
||||||
rate, Gst.Format.TIME, Gst.SeekFlags.FLUSH | Gst.SeekFlags.ACCURATE,
|
rate, Gst.Format.TIME, Gst.SeekFlags.FLUSH | Gst.SeekFlags.ACCURATE,
|
||||||
Gst.SeekType.SET, position, Gst.SeekType.NONE, -1);
|
Gst.SeekType.SET, position, Gst.SeekType.NONE, -1);
|
||||||
|
this._logger.debug(
|
||||||
|
`Live wallpaper: applyPlaybackRate to ${rate} at position ${position} ` +
|
||||||
|
`${seekOk ? 'succeeded' : 'FAILED (seek() returned false)'}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clutter's ContentGravity enum covers fit/stretch/center natively, but
|
||||||
|
* has no "cover" (scale-and-crop) value, so "fill" mode is done by hand:
|
||||||
|
* the actor is overscanned to the smallest size that fully covers the
|
||||||
|
* stage while preserving the video's aspect ratio, centered, and then
|
||||||
|
* clipped back down to the stage bounds.
|
||||||
|
*/
|
||||||
|
_applyDisplayMode() {
|
||||||
|
if (!this._actor)
|
||||||
|
return;
|
||||||
|
|
||||||
|
const mode = this._settings.get_string(SettingsKey.LIVE_WALLPAPER_DISPLAY_MODE);
|
||||||
|
switch (mode) {
|
||||||
|
case 'stretch':
|
||||||
|
this._actor.content_gravity = Clutter.ContentGravity.RESIZE_FILL;
|
||||||
|
break;
|
||||||
|
case 'center':
|
||||||
|
this._actor.content_gravity = Clutter.ContentGravity.CENTER;
|
||||||
|
break;
|
||||||
|
case 'fill':
|
||||||
|
this._actor.content_gravity = Clutter.ContentGravity.RESIZE_FILL;
|
||||||
|
break;
|
||||||
|
case 'fit':
|
||||||
|
default:
|
||||||
|
this._actor.content_gravity = Clutter.ContentGravity.RESIZE_ASPECT;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
this._layoutActor();
|
||||||
}
|
}
|
||||||
|
|
||||||
_layoutActor() {
|
_layoutActor() {
|
||||||
if (!this._actor)
|
if (!this._actor)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
const stageWidth = global.stage.width;
|
||||||
|
const stageHeight = global.stage.height;
|
||||||
|
const mode = this._settings.get_string(SettingsKey.LIVE_WALLPAPER_DISPLAY_MODE);
|
||||||
|
|
||||||
|
if (mode === 'fill' && this._videoWidth > 0 && this._videoHeight > 0) {
|
||||||
|
const scale = Math.max(stageWidth / this._videoWidth, stageHeight / this._videoHeight);
|
||||||
|
const scaledWidth = this._videoWidth * scale;
|
||||||
|
const scaledHeight = this._videoHeight * scale;
|
||||||
|
const offsetX = (stageWidth - scaledWidth) / 2;
|
||||||
|
const offsetY = (stageHeight - scaledHeight) / 2;
|
||||||
|
|
||||||
|
this._actor.set_position(offsetX, offsetY);
|
||||||
|
this._actor.set_size(scaledWidth, scaledHeight);
|
||||||
|
this._actor.set_clip(-offsetX, -offsetY, stageWidth, stageHeight);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this._actor.remove_clip();
|
||||||
this._actor.set_position(0, 0);
|
this._actor.set_position(0, 0);
|
||||||
this._actor.set_size(global.stage.width, global.stage.height);
|
this._actor.set_size(stageWidth, stageHeight);
|
||||||
}
|
}
|
||||||
|
|
||||||
_connectPowerWatches() {
|
_connectPowerWatches() {
|
||||||
@@ -502,7 +656,15 @@ export class LiveWallpaperManager {
|
|||||||
this._playbin.set_state(paused ? this._Gst.State.PAUSED : this._Gst.State.PLAYING);
|
this._playbin.set_state(paused ? this._Gst.State.PAUSED : this._Gst.State.PLAYING);
|
||||||
}
|
}
|
||||||
|
|
||||||
_stop() {
|
/**
|
||||||
|
* `animate: true` is for a user-visible stop (toggling the setting off,
|
||||||
|
* or the path being cleared) — the last frame stays on screen and eases
|
||||||
|
* away per the shared Transitions setting, revealing the static
|
||||||
|
* wallpaper beneath. Left off for restarts (which hand the actor to
|
||||||
|
* _playTransition() themselves, see _restart()), errors, and
|
||||||
|
* disable(), where an immediate teardown is what's wanted.
|
||||||
|
*/
|
||||||
|
_stop({animate = false} = {}) {
|
||||||
this._disconnectPowerWatches();
|
this._disconnectPowerWatches();
|
||||||
|
|
||||||
if (this._noFrameWatchdogId) {
|
if (this._noFrameWatchdogId) {
|
||||||
@@ -515,6 +677,11 @@ export class LiveWallpaperManager {
|
|||||||
this._pollTimeoutId = 0;
|
this._pollTimeoutId = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this._eosSeekTimeoutId) {
|
||||||
|
GLib.source_remove(this._eosSeekTimeoutId);
|
||||||
|
this._eosSeekTimeoutId = 0;
|
||||||
|
}
|
||||||
|
|
||||||
if (this._playbin) {
|
if (this._playbin) {
|
||||||
const Gst = this._Gst;
|
const Gst = this._Gst;
|
||||||
const bus = this._playbin.get_bus();
|
const bus = this._playbin.get_bus();
|
||||||
@@ -536,13 +703,81 @@ export class LiveWallpaperManager {
|
|||||||
this._appsink = null;
|
this._appsink = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
this._actor?.destroy();
|
if (animate && this._actor && this._settings.get_boolean(SettingsKey.TRANSITION_ENABLED)) {
|
||||||
this._actor = null;
|
this._playTransition(this._actor);
|
||||||
|
this._actor = null;
|
||||||
|
} else {
|
||||||
|
this._actor?.destroy();
|
||||||
|
this._actor = null;
|
||||||
|
}
|
||||||
this._image = null;
|
this._image = null;
|
||||||
this._videoWidth = 0;
|
this._videoWidth = 0;
|
||||||
this._videoHeight = 0;
|
this._videoHeight = 0;
|
||||||
|
this._fadeInPending = false;
|
||||||
|
|
||||||
this._setActive(false);
|
this._setActive(false);
|
||||||
this._paused = false;
|
this._paused = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Eases `overlay` — an actor that was just detached from active duty,
|
||||||
|
* still showing its last frame — off screen per the Transitions
|
||||||
|
* setting (fade/slide/zoom/random, same styles and duration as static
|
||||||
|
* rotation), then destroys it. Raising it above everything else first
|
||||||
|
* means this works whether it's revealing a freshly started live
|
||||||
|
* wallpaper actor underneath (switch) or the static desktop background
|
||||||
|
* (stop) — both are already in place by the time this runs.
|
||||||
|
*/
|
||||||
|
_playTransition(overlay) {
|
||||||
|
this._transitionOverlay?.destroy();
|
||||||
|
this._transitionOverlay = overlay;
|
||||||
|
|
||||||
|
Main.layoutManager._backgroundGroup.set_child_above_sibling(overlay, null);
|
||||||
|
|
||||||
|
const durationMs = this._settings.get_uint(SettingsKey.TRANSITION_DURATION_MS);
|
||||||
|
let style = this._settings.get_string(SettingsKey.TRANSITION_STYLE);
|
||||||
|
if (style === 'random')
|
||||||
|
style = ['fade', 'slide', 'zoom'][Math.floor(Math.random() * 3)];
|
||||||
|
|
||||||
|
const onComplete = () => {
|
||||||
|
if (this._transitionOverlay === overlay)
|
||||||
|
this._transitionOverlay = null;
|
||||||
|
overlay.destroy();
|
||||||
|
};
|
||||||
|
|
||||||
|
switch (style) {
|
||||||
|
case 'slide':
|
||||||
|
overlay.ease({
|
||||||
|
x: overlay.x - overlay.width,
|
||||||
|
duration: durationMs,
|
||||||
|
mode: Clutter.AnimationMode.EASE_IN_OUT_QUAD,
|
||||||
|
onComplete,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case 'zoom':
|
||||||
|
overlay.ease({
|
||||||
|
scale_x: 1.15,
|
||||||
|
scale_y: 1.15,
|
||||||
|
opacity: 0,
|
||||||
|
duration: durationMs,
|
||||||
|
mode: Clutter.AnimationMode.EASE_IN_QUAD,
|
||||||
|
onComplete,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case 'fade':
|
||||||
|
default:
|
||||||
|
overlay.ease({
|
||||||
|
opacity: 0,
|
||||||
|
duration: durationMs,
|
||||||
|
mode: Clutter.AnimationMode.EASE_OUT_QUAD,
|
||||||
|
onComplete,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_clearTransition() {
|
||||||
|
this._transitionOverlay?.destroy();
|
||||||
|
this._transitionOverlay = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import GLib from 'gi://GLib';
|
import GLib from 'gi://GLib';
|
||||||
import Meta from 'gi://Meta';
|
import Meta from 'gi://Meta';
|
||||||
import Clutter from 'gi://Clutter';
|
import Clutter from 'gi://Clutter';
|
||||||
|
import Graphene from 'gi://Graphene';
|
||||||
import * as Main from 'resource:///org/gnome/shell/ui/main.js';
|
import * as Main from 'resource:///org/gnome/shell/ui/main.js';
|
||||||
|
|
||||||
import {SettingsKey} from './settingsKeys.js';
|
import {SettingsKey} from './settingsKeys.js';
|
||||||
@@ -13,6 +14,11 @@ const SHIFT_PATTERN = [
|
|||||||
|
|
||||||
const FORCE_ROTATION_CHECK_SECONDS = 60;
|
const FORCE_ROTATION_CHECK_SECONDS = 60;
|
||||||
|
|
||||||
|
// Extra margin, in pixels, added on top of the configured shift amount when
|
||||||
|
// overscanning background actors — rounding/timing slack so a step never
|
||||||
|
// exposes a sliver of empty stage at the trailing edge.
|
||||||
|
const OVERSCAN_SAFETY_PX = 2;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Reduces OLED burn-in risk through three independent techniques:
|
* Reduces OLED burn-in risk through three independent techniques:
|
||||||
* - pixel shifting: nudges the background actors a few px on a slow cycle
|
* - pixel shifting: nudges the background actors a few px on a slow cycle
|
||||||
@@ -105,6 +111,7 @@ export class OledProtectionManager {
|
|||||||
const [dx, dy] = SHIFT_PATTERN[this._shiftStep];
|
const [dx, dy] = SHIFT_PATTERN[this._shiftStep];
|
||||||
|
|
||||||
for (const actor of this._backgroundActors()) {
|
for (const actor of this._backgroundActors()) {
|
||||||
|
this._ensureOverscan(actor, amount);
|
||||||
actor.ease({
|
actor.ease({
|
||||||
translation_x: dx * amount,
|
translation_x: dx * amount,
|
||||||
translation_y: dy * amount,
|
translation_y: dy * amount,
|
||||||
@@ -114,12 +121,36 @@ export class OledProtectionManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Background actors (the real desktop background, and our own live
|
||||||
|
* wallpaper actor in most display modes) are sized to exactly cover
|
||||||
|
* the stage with no slack, so nudging one by a few pixels for burn-in
|
||||||
|
* protection would drag its edge inward and expose bare stage on the
|
||||||
|
* side it moved away from. Scaling the actor up slightly around its
|
||||||
|
* center — by just enough margin to cover the configured shift amount
|
||||||
|
* — keeps every edge overscanned past the stage bounds no matter which
|
||||||
|
* step of the shift pattern is active.
|
||||||
|
*/
|
||||||
|
_ensureOverscan(actor, amount) {
|
||||||
|
const width = actor.width;
|
||||||
|
const height = actor.height;
|
||||||
|
if (width <= 0 || height <= 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
const margin = amount + OVERSCAN_SAFETY_PX;
|
||||||
|
actor.pivot_point = new Graphene.Point({x: 0.5, y: 0.5});
|
||||||
|
actor.scale_x = 1 + (2 * margin) / width;
|
||||||
|
actor.scale_y = 1 + (2 * margin) / height;
|
||||||
|
}
|
||||||
|
|
||||||
_resetShift() {
|
_resetShift() {
|
||||||
this._shiftStep = 0;
|
this._shiftStep = 0;
|
||||||
for (const actor of this._backgroundActors()) {
|
for (const actor of this._backgroundActors()) {
|
||||||
actor.ease({
|
actor.ease({
|
||||||
translation_x: 0,
|
translation_x: 0,
|
||||||
translation_y: 0,
|
translation_y: 0,
|
||||||
|
scale_x: 1,
|
||||||
|
scale_y: 1,
|
||||||
duration: 500,
|
duration: 500,
|
||||||
mode: Clutter.AnimationMode.EASE_OUT_QUAD,
|
mode: Clutter.AnimationMode.EASE_OUT_QUAD,
|
||||||
});
|
});
|
||||||
|
|||||||
+48
-16
@@ -2,6 +2,7 @@ import GLib from 'gi://GLib';
|
|||||||
import Gio from 'gi://Gio';
|
import Gio from 'gi://Gio';
|
||||||
import St from 'gi://St';
|
import St from 'gi://St';
|
||||||
import Clutter from 'gi://Clutter';
|
import Clutter from 'gi://Clutter';
|
||||||
|
import Graphene from 'gi://Graphene';
|
||||||
import * as Main from 'resource:///org/gnome/shell/ui/main.js';
|
import * as Main from 'resource:///org/gnome/shell/ui/main.js';
|
||||||
|
|
||||||
import {SettingsKey} from './settingsKeys.js';
|
import {SettingsKey} from './settingsKeys.js';
|
||||||
@@ -40,6 +41,7 @@ export class RotationManager {
|
|||||||
enable() {
|
enable() {
|
||||||
this._settingsSignals.push(
|
this._settingsSignals.push(
|
||||||
this._settings.connect(`changed::${SettingsKey.WALLPAPER_FOLDERS}`, () => this._reloadImages()),
|
this._settings.connect(`changed::${SettingsKey.WALLPAPER_FOLDERS}`, () => this._reloadImages()),
|
||||||
|
this._settings.connect(`changed::${SettingsKey.WALLPAPER_RESCAN_REQUEST}`, () => this._reloadImages()),
|
||||||
this._settings.connect(`changed::${SettingsKey.ROTATION_ENABLED}`, () => this._restartTimer()),
|
this._settings.connect(`changed::${SettingsKey.ROTATION_ENABLED}`, () => this._restartTimer()),
|
||||||
this._settings.connect(`changed::${SettingsKey.ROTATION_INTERVAL_SECONDS}`, () => this._restartTimer()),
|
this._settings.connect(`changed::${SettingsKey.ROTATION_INTERVAL_SECONDS}`, () => this._restartTimer()),
|
||||||
this._settings.connect(`changed::${SettingsKey.ROTATION_MODE}`, () => this._onModeChanged())
|
this._settings.connect(`changed::${SettingsKey.ROTATION_MODE}`, () => this._onModeChanged())
|
||||||
@@ -185,7 +187,7 @@ export class RotationManager {
|
|||||||
const uri = GLib.filename_to_uri(path, null);
|
const uri = GLib.filename_to_uri(path, null);
|
||||||
|
|
||||||
if (this._settings.get_boolean(SettingsKey.TRANSITION_ENABLED) && previousPath)
|
if (this._settings.get_boolean(SettingsKey.TRANSITION_ENABLED) && previousPath)
|
||||||
this._playCrossfade(previousPath);
|
this._playTransition(previousPath);
|
||||||
|
|
||||||
this._backgroundSettings.set_string('picture-uri', uri);
|
this._backgroundSettings.set_string('picture-uri', uri);
|
||||||
if (this._backgroundSettings.settings_schema.has_key('picture-uri-dark'))
|
if (this._backgroundSettings.settings_schema.has_key('picture-uri-dark'))
|
||||||
@@ -200,15 +202,19 @@ export class RotationManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The real background actor doesn't crossfade on its own, so we paint the
|
* The real background actor doesn't animate changes on its own, so we
|
||||||
* *old* image full-screen on a throwaway overlay right as the new image
|
* paint the *old* image full-screen on a throwaway overlay right as the
|
||||||
* is set underneath, then fade the overlay out to reveal it.
|
* new image is set underneath, then animate the overlay away to reveal
|
||||||
|
* it. Which animation runs is picked per monitor by style.
|
||||||
*/
|
*/
|
||||||
_playCrossfade(previousPath) {
|
_playTransition(previousPath) {
|
||||||
this._clearTransitionOverlays();
|
this._clearTransitionOverlays();
|
||||||
|
|
||||||
const durationMs = this._settings.get_uint(SettingsKey.TRANSITION_DURATION_MS);
|
const durationMs = this._settings.get_uint(SettingsKey.TRANSITION_DURATION_MS);
|
||||||
const uri = GLib.filename_to_uri(previousPath, null).replace(/"/g, '%22');
|
const uri = GLib.filename_to_uri(previousPath, null).replace(/"/g, '%22');
|
||||||
|
let style = this._settings.get_string(SettingsKey.TRANSITION_STYLE);
|
||||||
|
if (style === 'random')
|
||||||
|
style = ['fade', 'slide', 'zoom'][Math.floor(Math.random() * 3)];
|
||||||
|
|
||||||
for (const monitor of Main.layoutManager.monitors) {
|
for (const monitor of Main.layoutManager.monitors) {
|
||||||
const overlay = new St.Widget({
|
const overlay = new St.Widget({
|
||||||
@@ -217,23 +223,49 @@ export class RotationManager {
|
|||||||
y: monitor.y,
|
y: monitor.y,
|
||||||
width: monitor.width,
|
width: monitor.width,
|
||||||
height: monitor.height,
|
height: monitor.height,
|
||||||
|
pivot_point: new Graphene.Point({x: 0.5, y: 0.5}),
|
||||||
style: `background-image: url("${uri}"); background-size: cover; background-position: center;`,
|
style: `background-image: url("${uri}"); background-size: cover; background-position: center;`,
|
||||||
opacity: 255,
|
opacity: 255,
|
||||||
});
|
});
|
||||||
Main.layoutManager._backgroundGroup.add_child(overlay);
|
Main.layoutManager._backgroundGroup.add_child(overlay);
|
||||||
this._transitionOverlays.push(overlay);
|
this._transitionOverlays.push(overlay);
|
||||||
|
|
||||||
overlay.ease({
|
const onComplete = () => {
|
||||||
opacity: 0,
|
overlay.destroy();
|
||||||
duration: durationMs,
|
const idx = this._transitionOverlays.indexOf(overlay);
|
||||||
mode: Clutter.AnimationMode.EASE_OUT_QUAD,
|
if (idx >= 0)
|
||||||
onComplete: () => {
|
this._transitionOverlays.splice(idx, 1);
|
||||||
overlay.destroy();
|
};
|
||||||
const idx = this._transitionOverlays.indexOf(overlay);
|
|
||||||
if (idx >= 0)
|
switch (style) {
|
||||||
this._transitionOverlays.splice(idx, 1);
|
case 'slide':
|
||||||
},
|
overlay.ease({
|
||||||
});
|
x: monitor.x - monitor.width,
|
||||||
|
duration: durationMs,
|
||||||
|
mode: Clutter.AnimationMode.EASE_IN_OUT_QUAD,
|
||||||
|
onComplete,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case 'zoom':
|
||||||
|
overlay.ease({
|
||||||
|
scale_x: 1.15,
|
||||||
|
scale_y: 1.15,
|
||||||
|
opacity: 0,
|
||||||
|
duration: durationMs,
|
||||||
|
mode: Clutter.AnimationMode.EASE_IN_QUAD,
|
||||||
|
onComplete,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case 'fade':
|
||||||
|
default:
|
||||||
|
overlay.ease({
|
||||||
|
opacity: 0,
|
||||||
|
duration: durationMs,
|
||||||
|
mode: Clutter.AnimationMode.EASE_OUT_QUAD,
|
||||||
|
onComplete,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ export const SettingsKey = Object.freeze({
|
|||||||
SHOW_INDICATOR: 'show-indicator',
|
SHOW_INDICATOR: 'show-indicator',
|
||||||
DEBUG_LOGGING: 'debug-logging',
|
DEBUG_LOGGING: 'debug-logging',
|
||||||
WALLPAPER_FOLDERS: 'wallpaper-folders',
|
WALLPAPER_FOLDERS: 'wallpaper-folders',
|
||||||
|
WALLPAPER_RESCAN_REQUEST: 'wallpaper-rescan-request',
|
||||||
APPLY_TO_LOCK_SCREEN: 'apply-to-lock-screen',
|
APPLY_TO_LOCK_SCREEN: 'apply-to-lock-screen',
|
||||||
|
|
||||||
ROTATION_ENABLED: 'rotation-enabled',
|
ROTATION_ENABLED: 'rotation-enabled',
|
||||||
@@ -10,10 +11,12 @@ export const SettingsKey = Object.freeze({
|
|||||||
CURRENT_INDEX: 'current-index',
|
CURRENT_INDEX: 'current-index',
|
||||||
TRANSITION_ENABLED: 'transition-enabled',
|
TRANSITION_ENABLED: 'transition-enabled',
|
||||||
TRANSITION_DURATION_MS: 'transition-duration-ms',
|
TRANSITION_DURATION_MS: 'transition-duration-ms',
|
||||||
|
TRANSITION_STYLE: 'transition-style',
|
||||||
|
|
||||||
LIVE_WALLPAPER_ENABLED: 'live-wallpaper-enabled',
|
LIVE_WALLPAPER_ENABLED: 'live-wallpaper-enabled',
|
||||||
LIVE_WALLPAPER_PATH: 'live-wallpaper-path',
|
LIVE_WALLPAPER_PATH: 'live-wallpaper-path',
|
||||||
LIVE_WALLPAPER_FOLDERS: 'live-wallpaper-folders',
|
LIVE_WALLPAPER_FOLDERS: 'live-wallpaper-folders',
|
||||||
|
LIVE_WALLPAPER_DISPLAY_MODE: 'live-wallpaper-display-mode',
|
||||||
LIVE_WALLPAPER_MUTED: 'live-wallpaper-muted',
|
LIVE_WALLPAPER_MUTED: 'live-wallpaper-muted',
|
||||||
LIVE_WALLPAPER_PLAYBACK_RATE: 'live-wallpaper-playback-rate',
|
LIVE_WALLPAPER_PLAYBACK_RATE: 'live-wallpaper-playback-rate',
|
||||||
LIVE_WALLPAPER_PAUSE_ON_BATTERY: 'live-wallpaper-pause-on-battery',
|
LIVE_WALLPAPER_PAUSE_ON_BATTERY: 'live-wallpaper-pause-on-battery',
|
||||||
|
|||||||
+1
-1
@@ -7,5 +7,5 @@
|
|||||||
"settings-schema": "org.gnome.shell.extensions.benthicbloom",
|
"settings-schema": "org.gnome.shell.extensions.benthicbloom",
|
||||||
"gettext-domain": "benthicbloom",
|
"gettext-domain": "benthicbloom",
|
||||||
"version": 1,
|
"version": 1,
|
||||||
"version-name": "1.0.0"
|
"version-name": "1.0.1"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,25 @@ import {SettingsKey} from './lib/settingsKeys.js';
|
|||||||
import {checkGstreamerBaseAvailable, GSTREAMER_INSTALL_HINT} from './lib/gstreamerAvailability.js';
|
import {checkGstreamerBaseAvailable, GSTREAMER_INSTALL_HINT} from './lib/gstreamerAvailability.js';
|
||||||
import {listMediaFilesInFolders} from './lib/wallpaperSource.js';
|
import {listMediaFilesInFolders} from './lib/wallpaperSource.js';
|
||||||
|
|
||||||
|
const BACKGROUND_SCHEMA = 'org.gnome.desktop.background';
|
||||||
|
|
||||||
|
// GNOME's own picture-options values, exposed here so wallpaper fill mode
|
||||||
|
// can be controlled without inventing a parallel setting of our own.
|
||||||
|
// Built lazily (not at module top level) because gettext can only be
|
||||||
|
// called once the extension is registered, which hasn't happened yet
|
||||||
|
// while this module is still being evaluated on import.
|
||||||
|
function pictureOptions() {
|
||||||
|
return [
|
||||||
|
{value: 'zoom', label: _('Fill (Crop to Zoom)')},
|
||||||
|
{value: 'scaled', label: _('Fit (No Cropping)')},
|
||||||
|
{value: 'stretched', label: _('Stretch')},
|
||||||
|
{value: 'centered', label: _('Centered')},
|
||||||
|
{value: 'wallpaper', label: _('Tiled')},
|
||||||
|
{value: 'spanned', label: _('Spanned Across Monitors')},
|
||||||
|
{value: 'none', label: _('None (Background Color Only)')},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
export default class BenthicBloomPreferences extends ExtensionPreferences {
|
export default class BenthicBloomPreferences extends ExtensionPreferences {
|
||||||
async fillPreferencesWindow(window) {
|
async fillPreferencesWindow(window) {
|
||||||
const settings = this.getSettings();
|
const settings = this.getSettings();
|
||||||
@@ -19,8 +38,10 @@ export default class BenthicBloomPreferences extends ExtensionPreferences {
|
|||||||
// blank/broken-image placeholder in the page switcher.
|
// blank/broken-image placeholder in the page switcher.
|
||||||
Gtk.IconTheme.get_for_display(window.get_display()).add_search_path(`${this.path}/icons`);
|
Gtk.IconTheme.get_for_display(window.get_display()).add_search_path(`${this.path}/icons`);
|
||||||
|
|
||||||
|
const backgroundSettings = new Gio.Settings({schema_id: BACKGROUND_SCHEMA});
|
||||||
|
|
||||||
window.set_default_size(640, 720);
|
window.set_default_size(640, 720);
|
||||||
window.add(this._buildGeneralPage(settings));
|
window.add(this._buildGeneralPage(settings, backgroundSettings));
|
||||||
window.add(this._buildRotationPage(settings));
|
window.add(this._buildRotationPage(settings));
|
||||||
window.add(this._buildLiveWallpaperPage(settings, gstreamerError));
|
window.add(this._buildLiveWallpaperPage(settings, gstreamerError));
|
||||||
window.add(this._buildOledPage(settings));
|
window.add(this._buildOledPage(settings));
|
||||||
@@ -47,7 +68,7 @@ export default class BenthicBloomPreferences extends ExtensionPreferences {
|
|||||||
|
|
||||||
// --- General page ------------------------------------------------
|
// --- General page ------------------------------------------------
|
||||||
|
|
||||||
_buildGeneralPage(settings) {
|
_buildGeneralPage(settings, backgroundSettings) {
|
||||||
const page = new Adw.PreferencesPage({title: _('General'), icon_name: 'bb-general-symbolic'});
|
const page = new Adw.PreferencesPage({title: _('General'), icon_name: 'bb-general-symbolic'});
|
||||||
|
|
||||||
const behaviorGroup = new Adw.PreferencesGroup({title: _('Behavior')});
|
const behaviorGroup = new Adw.PreferencesGroup({title: _('Behavior')});
|
||||||
@@ -62,6 +83,23 @@ export default class BenthicBloomPreferences extends ExtensionPreferences {
|
|||||||
settings, SettingsKey.DEBUG_LOGGING,
|
settings, SettingsKey.DEBUG_LOGGING,
|
||||||
_('Debug Logging'), _('Print verbose diagnostics to the system log (journalctl -f)')));
|
_('Debug Logging'), _('Print verbose diagnostics to the system log (journalctl -f)')));
|
||||||
|
|
||||||
|
const displayGroup = new Adw.PreferencesGroup({
|
||||||
|
title: _('Display'),
|
||||||
|
description: _('How the wallpaper image is fitted to your screen, e.g. cropped to fill it'),
|
||||||
|
});
|
||||||
|
page.add(displayGroup);
|
||||||
|
const pictureOptionsList = pictureOptions();
|
||||||
|
const fillModeRow = new Adw.ComboRow({
|
||||||
|
title: _('Fill Mode'),
|
||||||
|
model: new Gtk.StringList({strings: pictureOptionsList.map(o => o.label)}),
|
||||||
|
});
|
||||||
|
const currentOption = backgroundSettings.get_string('picture-options');
|
||||||
|
fillModeRow.selected = Math.max(0, pictureOptionsList.findIndex(o => o.value === currentOption));
|
||||||
|
fillModeRow.connect('notify::selected', () => {
|
||||||
|
backgroundSettings.set_string('picture-options', pictureOptionsList[fillModeRow.selected].value);
|
||||||
|
});
|
||||||
|
displayGroup.add(fillModeRow);
|
||||||
|
|
||||||
const foldersGroup = new Adw.PreferencesGroup({
|
const foldersGroup = new Adw.PreferencesGroup({
|
||||||
title: _('Wallpaper Folders'),
|
title: _('Wallpaper Folders'),
|
||||||
description: _('Images found directly inside these folders are used for rotation'),
|
description: _('Images found directly inside these folders are used for rotation'),
|
||||||
@@ -71,15 +109,18 @@ export default class BenthicBloomPreferences extends ExtensionPreferences {
|
|||||||
const folderList = this._buildFolderListBox(settings, SettingsKey.WALLPAPER_FOLDERS, _('No folders added yet'));
|
const folderList = this._buildFolderListBox(settings, SettingsKey.WALLPAPER_FOLDERS, _('No folders added yet'));
|
||||||
foldersGroup.add(folderList);
|
foldersGroup.add(folderList);
|
||||||
|
|
||||||
const addButton = new Gtk.Button({
|
const buttonRow = new Gtk.Box({orientation: Gtk.Orientation.HORIZONTAL, spacing: 6, margin_top: 6});
|
||||||
label: _('Add Folder…'),
|
foldersGroup.add(buttonRow);
|
||||||
halign: Gtk.Align.START,
|
|
||||||
margin_top: 6,
|
const addButton = new Gtk.Button({label: _('Add Folder…'), css_classes: ['flat']});
|
||||||
css_classes: ['flat'],
|
|
||||||
});
|
|
||||||
addButton.connect('clicked', () => this._pickFolderForList(
|
addButton.connect('clicked', () => this._pickFolderForList(
|
||||||
settings, SettingsKey.WALLPAPER_FOLDERS, folderList, _('No folders added yet')));
|
settings, SettingsKey.WALLPAPER_FOLDERS, folderList, _('No folders added yet')));
|
||||||
foldersGroup.add(addButton);
|
buttonRow.append(addButton);
|
||||||
|
|
||||||
|
const refreshButton = new Gtk.Button({label: _('Refresh'), css_classes: ['flat']});
|
||||||
|
refreshButton.connect('clicked', () => settings.set_uint(
|
||||||
|
SettingsKey.WALLPAPER_RESCAN_REQUEST, settings.get_uint(SettingsKey.WALLPAPER_RESCAN_REQUEST) + 1));
|
||||||
|
buttonRow.append(refreshButton);
|
||||||
|
|
||||||
return page;
|
return page;
|
||||||
}
|
}
|
||||||
@@ -174,7 +215,17 @@ export default class BenthicBloomPreferences extends ExtensionPreferences {
|
|||||||
page.add(transitionGroup);
|
page.add(transitionGroup);
|
||||||
transitionGroup.add(this._switchRow(
|
transitionGroup.add(this._switchRow(
|
||||||
settings, SettingsKey.TRANSITION_ENABLED,
|
settings, SettingsKey.TRANSITION_ENABLED,
|
||||||
_('Crossfade'), _('Smoothly fade between wallpapers instead of switching instantly')));
|
_('Enable Transition'), _('Play an animation between wallpapers instead of switching instantly')));
|
||||||
|
|
||||||
|
const styles = ['fade', 'slide', 'zoom', 'random'];
|
||||||
|
const styleLabels = [_('Fade'), _('Slide'), _('Zoom'), _('Random')];
|
||||||
|
const styleRow = new Adw.ComboRow({title: _('Style'), model: new Gtk.StringList({strings: styleLabels})});
|
||||||
|
styleRow.selected = Math.max(0, styles.indexOf(settings.get_string(SettingsKey.TRANSITION_STYLE)));
|
||||||
|
styleRow.connect('notify::selected', () => {
|
||||||
|
settings.set_string(SettingsKey.TRANSITION_STYLE, styles[styleRow.selected]);
|
||||||
|
});
|
||||||
|
transitionGroup.add(styleRow);
|
||||||
|
|
||||||
transitionGroup.add(this._spinRow(
|
transitionGroup.add(this._spinRow(
|
||||||
{title: _('Fade Duration'), subtitle: _('Milliseconds'), lower: 200, upper: 5000, step: 100, page: 500},
|
{title: _('Fade Duration'), subtitle: _('Milliseconds'), lower: 200, upper: 5000, step: 100, page: 500},
|
||||||
() => settings.get_uint(SettingsKey.TRANSITION_DURATION_MS),
|
() => settings.get_uint(SettingsKey.TRANSITION_DURATION_MS),
|
||||||
@@ -251,6 +302,19 @@ export default class BenthicBloomPreferences extends ExtensionPreferences {
|
|||||||
fileRow.add_suffix(chooseButton);
|
fileRow.add_suffix(chooseButton);
|
||||||
group.add(fileRow);
|
group.add(fileRow);
|
||||||
|
|
||||||
|
const liveDisplayModes = ['fit', 'fill', 'stretch', 'center'];
|
||||||
|
const liveDisplayLabels = [_('Fit (Letterboxed)'), _('Fill (Crop to Cover)'), _('Stretch'), _('Center')];
|
||||||
|
const displayModeRow = new Adw.ComboRow({
|
||||||
|
title: _('Display Mode'),
|
||||||
|
model: new Gtk.StringList({strings: liveDisplayLabels}),
|
||||||
|
});
|
||||||
|
displayModeRow.selected = Math.max(
|
||||||
|
0, liveDisplayModes.indexOf(settings.get_string(SettingsKey.LIVE_WALLPAPER_DISPLAY_MODE)));
|
||||||
|
displayModeRow.connect('notify::selected', () => {
|
||||||
|
settings.set_string(SettingsKey.LIVE_WALLPAPER_DISPLAY_MODE, liveDisplayModes[displayModeRow.selected]);
|
||||||
|
});
|
||||||
|
group.add(displayModeRow);
|
||||||
|
|
||||||
const muteRow = this._switchRow(
|
const muteRow = this._switchRow(
|
||||||
settings, SettingsKey.LIVE_WALLPAPER_MUTED,
|
settings, SettingsKey.LIVE_WALLPAPER_MUTED,
|
||||||
_('Mute Audio'), _('Play video wallpapers without sound'));
|
_('Mute Audio'), _('Play video wallpapers without sound'));
|
||||||
@@ -283,15 +347,17 @@ export default class BenthicBloomPreferences extends ExtensionPreferences {
|
|||||||
settings, SettingsKey.LIVE_WALLPAPER_FOLDERS, _('No folders added yet'), refreshMedia);
|
settings, SettingsKey.LIVE_WALLPAPER_FOLDERS, _('No folders added yet'), refreshMedia);
|
||||||
foldersGroup.add(liveFolderList);
|
foldersGroup.add(liveFolderList);
|
||||||
|
|
||||||
const addFolderButton = new Gtk.Button({
|
const liveButtonRow = new Gtk.Box({orientation: Gtk.Orientation.HORIZONTAL, spacing: 6, margin_top: 6});
|
||||||
label: _('Add Folder…'),
|
foldersGroup.add(liveButtonRow);
|
||||||
halign: Gtk.Align.START,
|
|
||||||
margin_top: 6,
|
const addFolderButton = new Gtk.Button({label: _('Add Folder…'), css_classes: ['flat']});
|
||||||
css_classes: ['flat'],
|
|
||||||
});
|
|
||||||
addFolderButton.connect('clicked', () => this._pickFolderForList(
|
addFolderButton.connect('clicked', () => this._pickFolderForList(
|
||||||
settings, SettingsKey.LIVE_WALLPAPER_FOLDERS, liveFolderList, _('No folders added yet'), refreshMedia));
|
settings, SettingsKey.LIVE_WALLPAPER_FOLDERS, liveFolderList, _('No folders added yet'), refreshMedia));
|
||||||
foldersGroup.add(addFolderButton);
|
liveButtonRow.append(addFolderButton);
|
||||||
|
|
||||||
|
const refreshMediaButton = new Gtk.Button({label: _('Refresh'), css_classes: ['flat']});
|
||||||
|
refreshMediaButton.connect('clicked', refreshMedia);
|
||||||
|
liveButtonRow.append(refreshMediaButton);
|
||||||
|
|
||||||
refreshMedia();
|
refreshMedia();
|
||||||
|
|
||||||
@@ -309,6 +375,7 @@ export default class BenthicBloomPreferences extends ExtensionPreferences {
|
|||||||
// GStreamer is actually driving playback is greyed out.
|
// GStreamer is actually driving playback is greyed out.
|
||||||
if (gstreamerError) {
|
if (gstreamerError) {
|
||||||
fileRow.sensitive = false;
|
fileRow.sensitive = false;
|
||||||
|
displayModeRow.sensitive = false;
|
||||||
muteRow.sensitive = false;
|
muteRow.sensitive = false;
|
||||||
rateRow.sensitive = false;
|
rateRow.sensitive = false;
|
||||||
foldersGroup.sensitive = false;
|
foldersGroup.sensitive = false;
|
||||||
@@ -394,9 +461,12 @@ export default class BenthicBloomPreferences extends ExtensionPreferences {
|
|||||||
settings, SettingsKey.OLED_PIXEL_SHIFT_ENABLED,
|
settings, SettingsKey.OLED_PIXEL_SHIFT_ENABLED,
|
||||||
_('Enable Pixel Shifting'), _('Periodically nudge the background by a few pixels')));
|
_('Enable Pixel Shifting'), _('Periodically nudge the background by a few pixels')));
|
||||||
shiftGroup.add(this._spinRow(
|
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),
|
title: _('Shift Interval'), subtitle: _('Minutes between each shift step'),
|
||||||
value => settings.set_uint(SettingsKey.OLED_PIXEL_SHIFT_INTERVAL_SECONDS, Math.round(value))));
|
lower: 0.5, upper: 10, step: 0.5, page: 1, digits: 1,
|
||||||
|
},
|
||||||
|
() => settings.get_uint(SettingsKey.OLED_PIXEL_SHIFT_INTERVAL_SECONDS) / 60,
|
||||||
|
value => settings.set_uint(SettingsKey.OLED_PIXEL_SHIFT_INTERVAL_SECONDS, Math.round(value * 60))));
|
||||||
shiftGroup.add(this._spinRow(
|
shiftGroup.add(this._spinRow(
|
||||||
{title: _('Shift Amount'), subtitle: _('Pixels'), lower: 1, upper: 10, step: 1, page: 1},
|
{title: _('Shift Amount'), subtitle: _('Pixels'), lower: 1, upper: 10, step: 1, page: 1},
|
||||||
() => settings.get_uint(SettingsKey.OLED_PIXEL_SHIFT_AMOUNT_PX),
|
() => settings.get_uint(SettingsKey.OLED_PIXEL_SHIFT_AMOUNT_PX),
|
||||||
@@ -408,9 +478,9 @@ export default class BenthicBloomPreferences extends ExtensionPreferences {
|
|||||||
settings, SettingsKey.OLED_DIM_ON_IDLE_ENABLED,
|
settings, SettingsKey.OLED_DIM_ON_IDLE_ENABLED,
|
||||||
_('Dim When Idle'), _('Lower brightness after a period of inactivity')));
|
_('Dim When Idle'), _('Lower brightness after a period of inactivity')));
|
||||||
dimGroup.add(this._spinRow(
|
dimGroup.add(this._spinRow(
|
||||||
{title: _('Idle Delay'), subtitle: _('Seconds of inactivity before dimming'), lower: 10, upper: 3600, step: 10, page: 60},
|
{title: _('Idle Delay'), subtitle: _('Minutes of inactivity before dimming'), lower: 1, upper: 60, step: 1, page: 5},
|
||||||
() => settings.get_uint(SettingsKey.OLED_DIM_IDLE_DELAY_SECONDS),
|
() => settings.get_uint(SettingsKey.OLED_DIM_IDLE_DELAY_SECONDS) / 60,
|
||||||
value => settings.set_uint(SettingsKey.OLED_DIM_IDLE_DELAY_SECONDS, Math.round(value))));
|
value => settings.set_uint(SettingsKey.OLED_DIM_IDLE_DELAY_SECONDS, Math.round(value * 60))));
|
||||||
dimGroup.add(this._spinRow(
|
dimGroup.add(this._spinRow(
|
||||||
{
|
{
|
||||||
title: _('Dimmed Brightness'), subtitle: _('0.0 = black, 0.9 = barely dimmed'),
|
title: _('Dimmed Brightness'), subtitle: _('0.0 = black, 0.9 = barely dimmed'),
|
||||||
|
|||||||
@@ -21,6 +21,12 @@
|
|||||||
<description>List of folder paths scanned for wallpaper images.</description>
|
<description>List of folder paths scanned for wallpaper images.</description>
|
||||||
</key>
|
</key>
|
||||||
|
|
||||||
|
<key name="wallpaper-rescan-request" type="u">
|
||||||
|
<default>0</default>
|
||||||
|
<summary>Wallpaper folder rescan request</summary>
|
||||||
|
<description>Internal counter bumped by the preferences UI to ask the running extension to rescan wallpaper folders for new files.</description>
|
||||||
|
</key>
|
||||||
|
|
||||||
<key name="apply-to-lock-screen" type="b">
|
<key name="apply-to-lock-screen" type="b">
|
||||||
<default>true</default>
|
<default>true</default>
|
||||||
<summary>Apply wallpaper to lock screen</summary>
|
<summary>Apply wallpaper to lock screen</summary>
|
||||||
@@ -67,6 +73,18 @@
|
|||||||
<description>Duration, in milliseconds, of the crossfade animation between wallpapers.</description>
|
<description>Duration, in milliseconds, of the crossfade animation between wallpapers.</description>
|
||||||
</key>
|
</key>
|
||||||
|
|
||||||
|
<key name="transition-style" type="s">
|
||||||
|
<choices>
|
||||||
|
<choice value="fade"/>
|
||||||
|
<choice value="slide"/>
|
||||||
|
<choice value="zoom"/>
|
||||||
|
<choice value="random"/>
|
||||||
|
</choices>
|
||||||
|
<default>"fade"</default>
|
||||||
|
<summary>Transition style</summary>
|
||||||
|
<description>Visual effect used when the wallpaper changes.</description>
|
||||||
|
</key>
|
||||||
|
|
||||||
<key name="live-wallpaper-enabled" type="b">
|
<key name="live-wallpaper-enabled" type="b">
|
||||||
<default>false</default>
|
<default>false</default>
|
||||||
<summary>Enable live wallpaper</summary>
|
<summary>Enable live wallpaper</summary>
|
||||||
@@ -85,6 +103,18 @@
|
|||||||
<description>List of folder paths scanned for videos and animated GIFs to pick a live wallpaper from.</description>
|
<description>List of folder paths scanned for videos and animated GIFs to pick a live wallpaper from.</description>
|
||||||
</key>
|
</key>
|
||||||
|
|
||||||
|
<key name="live-wallpaper-display-mode" type="s">
|
||||||
|
<choices>
|
||||||
|
<choice value="fit"/>
|
||||||
|
<choice value="fill"/>
|
||||||
|
<choice value="stretch"/>
|
||||||
|
<choice value="center"/>
|
||||||
|
</choices>
|
||||||
|
<default>"fit"</default>
|
||||||
|
<summary>Live wallpaper display mode</summary>
|
||||||
|
<description>How the video/GIF is fitted to the screen: fit (letterboxed), fill (cropped to cover), stretch (distorted to fill) or center (original size).</description>
|
||||||
|
</key>
|
||||||
|
|
||||||
<key name="live-wallpaper-muted" type="b">
|
<key name="live-wallpaper-muted" type="b">
|
||||||
<default>true</default>
|
<default>true</default>
|
||||||
<summary>Mute live wallpaper audio</summary>
|
<summary>Mute live wallpaper audio</summary>
|
||||||
|
|||||||
Reference in New Issue
Block a user