more oled features and system freeze fixes

This commit is contained in:
2026-08-06 19:58:50 +02:00
parent 40af1dad26
commit faa3a7257f
9 changed files with 382 additions and 58 deletions
+169 -9
View File
@@ -1,6 +1,7 @@
import GLib from 'gi://GLib';
import Gio from 'gi://Gio';
import Clutter from 'gi://Clutter';
import Graphene from 'gi://Graphene';
import St from 'gi://St';
import * as Main from 'resource:///org/gnome/shell/ui/main.js';
@@ -89,6 +90,9 @@ export class LiveWallpaperManager {
this._noFrameWatchdogId = 0;
this._imageStrategyIndex = 0;
this._frameErrorCount = 0;
this._transitionOverlay = null;
this._fadeInPending = false;
}
get isAvailable() {
@@ -113,6 +117,7 @@ export class LiveWallpaperManager {
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_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_PAUSE_ON_BATTERY}`, () => this._updatePauseState()),
this._settings.connect(`changed::${SettingsKey.LIVE_WALLPAPER_PAUSE_WHEN_FULLSCREEN}`, () => this._updatePauseState())
@@ -134,6 +139,7 @@ export class LiveWallpaperManager {
}
this._stop();
this._clearTransition();
}
async _loadGstreamer() {
@@ -157,12 +163,27 @@ export class LiveWallpaperManager {
if (shouldRun)
this._active ? this._restart() : this._start();
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() {
const transitionsOn = this._settings.get_boolean(SettingsKey.TRANSITION_ENABLED);
const outgoingActor = transitionsOn ? this._actor : null;
if (outgoingActor)
this._actor = null;
this._stop();
this._start();
if (outgoingActor)
this._playTransition(outgoingActor);
}
/**
@@ -233,11 +254,15 @@ export class LiveWallpaperManager {
try {
this._actor = new Clutter.Actor({
content_gravity: Clutter.ContentGravity.RESIZE_ASPECT,
reactive: false,
pivot_point: new Graphene.Point({x: 0.5, y: 0.5}),
});
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();
@@ -312,9 +337,17 @@ export class LiveWallpaperManager {
try {
this._updateFrame(mapInfo.data, width, height);
this._frameCount++;
if (this._frameCount === 1)
if (this._frameCount === 1) {
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`);
} finally {
buffer.unmap(mapInfo);
@@ -346,6 +379,7 @@ export class LiveWallpaperManager {
this._image = strategy.create(width, height);
this._videoWidth = width;
this._videoHeight = height;
this._layoutActor();
}
strategy.upload(this._image, this._Cogl, data, width, height);
this._actor.set_content(this._image);
@@ -414,11 +448,61 @@ export class LiveWallpaperManager {
Gst.SeekType.SET, position, Gst.SeekType.NONE, -1);
}
/**
* 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() {
if (!this._actor)
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_size(global.stage.width, global.stage.height);
this._actor.set_size(stageWidth, stageHeight);
}
_connectPowerWatches() {
@@ -502,7 +586,15 @@ export class LiveWallpaperManager {
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();
if (this._noFrameWatchdogId) {
@@ -536,13 +628,81 @@ export class LiveWallpaperManager {
this._appsink = null;
}
this._actor?.destroy();
this._actor = null;
if (animate && this._actor && this._settings.get_boolean(SettingsKey.TRANSITION_ENABLED)) {
this._playTransition(this._actor);
this._actor = null;
} else {
this._actor?.destroy();
this._actor = null;
}
this._image = null;
this._videoWidth = 0;
this._videoHeight = 0;
this._fadeInPending = false;
this._setActive(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;
}
}