mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-04-12 08:42:13 -04:00
feat(lockscreen): enable use of videos as screensaver in the lock screen (#1819)
* feat(lockscreen): enable use of videos as screensaver in the lock screen * reducing debug logs * feature becomes available only when QtMultimedia is available
This commit is contained in:
committed by
GitHub
parent
5d09acca4c
commit
bd6ad53875
@@ -497,6 +497,9 @@ Singleton {
|
|||||||
property string lockScreenActiveMonitor: "all"
|
property string lockScreenActiveMonitor: "all"
|
||||||
property string lockScreenInactiveColor: "#000000"
|
property string lockScreenInactiveColor: "#000000"
|
||||||
property int lockScreenNotificationMode: 0
|
property int lockScreenNotificationMode: 0
|
||||||
|
property bool lockScreenVideoEnabled: false
|
||||||
|
property string lockScreenVideoPath: ""
|
||||||
|
property bool lockScreenVideoCycling: false
|
||||||
property bool hideBrightnessSlider: false
|
property bool hideBrightnessSlider: false
|
||||||
|
|
||||||
property int notificationTimeoutLow: 5000
|
property int notificationTimeoutLow: 5000
|
||||||
|
|||||||
@@ -320,6 +320,9 @@ var SPEC = {
|
|||||||
lockScreenActiveMonitor: { def: "all" },
|
lockScreenActiveMonitor: { def: "all" },
|
||||||
lockScreenInactiveColor: { def: "#000000" },
|
lockScreenInactiveColor: { def: "#000000" },
|
||||||
lockScreenNotificationMode: { def: 0 },
|
lockScreenNotificationMode: { def: 0 },
|
||||||
|
lockScreenVideoEnabled: { def: false },
|
||||||
|
lockScreenVideoPath: { def: "" },
|
||||||
|
lockScreenVideoCycling: { def: false },
|
||||||
hideBrightnessSlider: { def: false },
|
hideBrightnessSlider: { def: false },
|
||||||
|
|
||||||
notificationTimeoutLow: { def: 5000 },
|
notificationTimeoutLow: { def: 5000 },
|
||||||
|
|||||||
@@ -2,8 +2,9 @@ pragma ComponentBehavior: Bound
|
|||||||
|
|
||||||
import QtQuick
|
import QtQuick
|
||||||
import Quickshell.Wayland
|
import Quickshell.Wayland
|
||||||
|
import qs.Common
|
||||||
|
|
||||||
Rectangle {
|
FocusScope {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
required property WlSessionLock lock
|
required property WlSessionLock lock
|
||||||
@@ -14,7 +15,17 @@ Rectangle {
|
|||||||
signal passwordChanged(string newPassword)
|
signal passwordChanged(string newPassword)
|
||||||
signal unlockRequested
|
signal unlockRequested
|
||||||
|
|
||||||
color: "transparent"
|
Keys.onPressed: event => {
|
||||||
|
if (videoScreensaver.active && videoScreensaver.inputEnabled) {
|
||||||
|
videoScreensaver.dismiss();
|
||||||
|
event.accepted = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
anchors.fill: parent
|
||||||
|
color: "transparent"
|
||||||
|
}
|
||||||
|
|
||||||
LockScreenContent {
|
LockScreenContent {
|
||||||
id: lockContent
|
id: lockContent
|
||||||
@@ -23,17 +34,38 @@ Rectangle {
|
|||||||
demoMode: false
|
demoMode: false
|
||||||
passwordBuffer: root.sharedPasswordBuffer
|
passwordBuffer: root.sharedPasswordBuffer
|
||||||
screenName: root.screenName
|
screenName: root.screenName
|
||||||
|
enabled: !videoScreensaver.active
|
||||||
|
focus: !videoScreensaver.active
|
||||||
|
opacity: videoScreensaver.active ? 0 : 1
|
||||||
onUnlockRequested: root.unlockRequested()
|
onUnlockRequested: root.unlockRequested()
|
||||||
onPasswordBufferChanged: {
|
onPasswordBufferChanged: {
|
||||||
if (root.sharedPasswordBuffer !== passwordBuffer) {
|
if (root.sharedPasswordBuffer !== passwordBuffer) {
|
||||||
root.passwordChanged(passwordBuffer);
|
root.passwordChanged(passwordBuffer);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Behavior on opacity {
|
||||||
|
NumberAnimation {
|
||||||
|
duration: 200
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
VideoScreensaver {
|
||||||
|
id: videoScreensaver
|
||||||
|
anchors.fill: parent
|
||||||
|
screenName: root.screenName
|
||||||
|
}
|
||||||
|
|
||||||
|
Component.onCompleted: forceActiveFocus()
|
||||||
|
|
||||||
onIsLockedChanged: {
|
onIsLockedChanged: {
|
||||||
if (isLocked) {
|
if (isLocked) {
|
||||||
|
forceActiveFocus();
|
||||||
lockContent.resetLockState();
|
lockContent.resetLockState();
|
||||||
|
if (SettingsData.lockScreenVideoEnabled) {
|
||||||
|
videoScreensaver.start();
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
lockContent.unlocking = false;
|
lockContent.unlocking = false;
|
||||||
|
|||||||
200
quickshell/Modules/Lock/VideoScreensaver.qml
Normal file
200
quickshell/Modules/Lock/VideoScreensaver.qml
Normal file
@@ -0,0 +1,200 @@
|
|||||||
|
pragma ComponentBehavior: Bound
|
||||||
|
|
||||||
|
import QtQuick
|
||||||
|
import Quickshell.Io
|
||||||
|
import qs.Common
|
||||||
|
import qs.Services
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
required property string screenName
|
||||||
|
property bool active: false
|
||||||
|
property string videoSource: ""
|
||||||
|
property bool inputEnabled: false
|
||||||
|
property point lastMousePos: Qt.point(-1, -1)
|
||||||
|
property bool mouseInitialized: false
|
||||||
|
property var videoPlayer: null
|
||||||
|
|
||||||
|
signal dismissed
|
||||||
|
|
||||||
|
visible: active
|
||||||
|
z: 1000
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: background
|
||||||
|
anchors.fill: parent
|
||||||
|
color: "black"
|
||||||
|
visible: root.active
|
||||||
|
}
|
||||||
|
|
||||||
|
Timer {
|
||||||
|
id: inputEnableTimer
|
||||||
|
interval: 500
|
||||||
|
onTriggered: root.inputEnabled = true
|
||||||
|
}
|
||||||
|
|
||||||
|
Process {
|
||||||
|
id: videoPicker
|
||||||
|
property string result: ""
|
||||||
|
property string folder: ""
|
||||||
|
|
||||||
|
command: ["sh", "-c", "find '" + folder + "' -maxdepth 1 -type f \\( " + "-iname '*.mp4' -o -iname '*.mkv' -o -iname '*.webm' -o " + "-iname '*.mov' -o -iname '*.avi' -o -iname '*.m4v' " + "\\) 2>/dev/null | shuf -n1"]
|
||||||
|
|
||||||
|
stdout: SplitParser {
|
||||||
|
onRead: data => {
|
||||||
|
const path = data.trim();
|
||||||
|
if (path) {
|
||||||
|
videoPicker.result = path;
|
||||||
|
root.videoSource = "file://" + path;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onExited: exitCode => {
|
||||||
|
if (exitCode !== 0 || !videoPicker.result) {
|
||||||
|
console.warn("VideoScreensaver: no video found in folder");
|
||||||
|
ToastService.showError(I18n.tr("Video Screensaver"), I18n.tr("No video found in folder"));
|
||||||
|
root.dismiss();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Process {
|
||||||
|
id: fileChecker
|
||||||
|
command: ["test", "-d", SettingsData.lockScreenVideoPath]
|
||||||
|
|
||||||
|
onExited: exitCode => {
|
||||||
|
const isDir = exitCode === 0;
|
||||||
|
const videoPath = SettingsData.lockScreenVideoPath;
|
||||||
|
|
||||||
|
if (isDir) {
|
||||||
|
videoPicker.folder = videoPath;
|
||||||
|
videoPicker.running = true;
|
||||||
|
} else if (SettingsData.lockScreenVideoCycling) {
|
||||||
|
const parentFolder = videoPath.substring(0, videoPath.lastIndexOf('/'));
|
||||||
|
videoPicker.folder = parentFolder;
|
||||||
|
videoPicker.running = true;
|
||||||
|
} else {
|
||||||
|
root.videoSource = "file://" + videoPath;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function createVideoPlayer() {
|
||||||
|
if (videoPlayer)
|
||||||
|
return true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
videoPlayer = Qt.createQmlObject(`
|
||||||
|
import QtQuick
|
||||||
|
import QtMultimedia
|
||||||
|
Video {
|
||||||
|
anchors.fill: parent
|
||||||
|
fillMode: VideoOutput.PreserveAspectCrop
|
||||||
|
loops: MediaPlayer.Infinite
|
||||||
|
volume: 0
|
||||||
|
}
|
||||||
|
`, background, "VideoScreensaver.VideoPlayer");
|
||||||
|
|
||||||
|
videoPlayer.errorOccurred.connect((error, errorString) => {
|
||||||
|
console.warn("VideoScreensaver: playback error:", errorString);
|
||||||
|
ToastService.showError(I18n.tr("Video Screensaver"), I18n.tr("Playback error: ") + errorString);
|
||||||
|
root.dismiss();
|
||||||
|
});
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} catch (e) {
|
||||||
|
console.warn("VideoScreensaver: Failed to create video player:", e);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function destroyVideoPlayer() {
|
||||||
|
if (videoPlayer) {
|
||||||
|
videoPlayer.stop();
|
||||||
|
videoPlayer.destroy();
|
||||||
|
videoPlayer = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function start() {
|
||||||
|
if (!SettingsData.lockScreenVideoEnabled || !SettingsData.lockScreenVideoPath)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (!MultimediaService.available) {
|
||||||
|
ToastService.showError(I18n.tr("Video Screensaver"), I18n.tr("QtMultimedia is not available"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!createVideoPlayer())
|
||||||
|
return;
|
||||||
|
|
||||||
|
videoPicker.result = "";
|
||||||
|
videoPicker.folder = "";
|
||||||
|
inputEnabled = false;
|
||||||
|
mouseInitialized = false;
|
||||||
|
lastMousePos = Qt.point(-1, -1);
|
||||||
|
active = true;
|
||||||
|
inputEnableTimer.start();
|
||||||
|
fileChecker.running = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function dismiss() {
|
||||||
|
if (!active)
|
||||||
|
return;
|
||||||
|
destroyVideoPlayer();
|
||||||
|
inputEnabled = false;
|
||||||
|
active = false;
|
||||||
|
videoSource = "";
|
||||||
|
dismissed();
|
||||||
|
}
|
||||||
|
|
||||||
|
onVideoSourceChanged: {
|
||||||
|
if (videoSource && active && videoPlayer) {
|
||||||
|
videoPlayer.source = videoSource;
|
||||||
|
videoPlayer.play();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: mouseArea
|
||||||
|
anchors.fill: parent
|
||||||
|
enabled: root.active && root.inputEnabled
|
||||||
|
hoverEnabled: true
|
||||||
|
propagateComposedEvents: false
|
||||||
|
|
||||||
|
onPositionChanged: mouse => {
|
||||||
|
if (!root.mouseInitialized) {
|
||||||
|
root.lastMousePos = Qt.point(mouse.x, mouse.y);
|
||||||
|
root.mouseInitialized = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var dx = Math.abs(mouse.x - root.lastMousePos.x);
|
||||||
|
var dy = Math.abs(mouse.y - root.lastMousePos.y);
|
||||||
|
if (dx > 5 || dy > 5) {
|
||||||
|
root.dismiss();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
onClicked: root.dismiss()
|
||||||
|
onPressed: root.dismiss()
|
||||||
|
onWheel: root.dismiss()
|
||||||
|
}
|
||||||
|
|
||||||
|
Connections {
|
||||||
|
target: IdleService
|
||||||
|
|
||||||
|
function onLockRequested() {
|
||||||
|
if (SettingsData.lockScreenVideoEnabled && !root.active) {
|
||||||
|
root.start();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onFadeToLockRequested() {
|
||||||
|
if (SettingsData.lockScreenVideoEnabled && !root.active) {
|
||||||
|
IdleService.cancelFadeToLock();
|
||||||
|
root.start();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import QtQuick
|
import QtQuick
|
||||||
import Quickshell
|
import Quickshell
|
||||||
import qs.Common
|
import qs.Common
|
||||||
|
import qs.Modals.FileBrowser
|
||||||
import qs.Services
|
import qs.Services
|
||||||
import qs.Widgets
|
import qs.Widgets
|
||||||
import qs.Modules.Settings.Widgets
|
import qs.Modules.Settings.Widgets
|
||||||
@@ -8,6 +9,16 @@ import qs.Modules.Settings.Widgets
|
|||||||
Item {
|
Item {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
|
FileBrowserModal {
|
||||||
|
id: videoBrowserModal
|
||||||
|
browserTitle: I18n.tr("Select Video or Folder")
|
||||||
|
browserIcon: "movie"
|
||||||
|
browserType: "video"
|
||||||
|
showHiddenFiles: false
|
||||||
|
fileExtensions: ["*.mp4", "*.mkv", "*.webm", "*.mov", "*.avi", "*.m4v"]
|
||||||
|
onFileSelected: path => SettingsData.set("lockScreenVideoPath", path)
|
||||||
|
}
|
||||||
|
|
||||||
DankFlickable {
|
DankFlickable {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
clip: true
|
clip: true
|
||||||
@@ -168,6 +179,87 @@ Item {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SettingsCard {
|
||||||
|
width: parent.width
|
||||||
|
iconName: "movie"
|
||||||
|
title: I18n.tr("Video Screensaver")
|
||||||
|
settingKey: "videoScreensaver"
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
visible: !MultimediaService.available
|
||||||
|
text: I18n.tr("QtMultimedia is not available - video screensaver requires qt multimedia services")
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: Theme.warning
|
||||||
|
width: parent.width
|
||||||
|
wrapMode: Text.WordWrap
|
||||||
|
}
|
||||||
|
|
||||||
|
SettingsToggleRow {
|
||||||
|
settingKey: "lockScreenVideoEnabled"
|
||||||
|
tags: ["lock", "screen", "video", "screensaver", "animation", "movie"]
|
||||||
|
text: I18n.tr("Enable Video Screensaver")
|
||||||
|
description: I18n.tr("Play a video when the screen locks.")
|
||||||
|
enabled: MultimediaService.available
|
||||||
|
checked: SettingsData.lockScreenVideoEnabled
|
||||||
|
onToggled: checked => SettingsData.set("lockScreenVideoEnabled", checked)
|
||||||
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
|
width: parent.width
|
||||||
|
spacing: Theme.spacingXS
|
||||||
|
visible: SettingsData.lockScreenVideoEnabled && MultimediaService.available
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: I18n.tr("Video Path")
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: Theme.surfaceVariantText
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: I18n.tr("Path to a video file or folder containing videos")
|
||||||
|
font.pixelSize: Theme.fontSizeXSmall
|
||||||
|
color: Theme.outlineVariant
|
||||||
|
wrapMode: Text.WordWrap
|
||||||
|
width: parent.width
|
||||||
|
}
|
||||||
|
|
||||||
|
Row {
|
||||||
|
width: parent.width
|
||||||
|
spacing: Theme.spacingS
|
||||||
|
|
||||||
|
DankTextField {
|
||||||
|
id: videoPathField
|
||||||
|
width: parent.width - browseVideoButton.width - Theme.spacingS
|
||||||
|
placeholderText: I18n.tr("/path/to/videos")
|
||||||
|
text: SettingsData.lockScreenVideoPath
|
||||||
|
backgroundColor: Theme.surfaceContainerHighest
|
||||||
|
onTextChanged: {
|
||||||
|
if (text !== SettingsData.lockScreenVideoPath) {
|
||||||
|
SettingsData.set("lockScreenVideoPath", text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DankButton {
|
||||||
|
id: browseVideoButton
|
||||||
|
text: I18n.tr("Browse")
|
||||||
|
onClicked: videoBrowserModal.open()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SettingsToggleRow {
|
||||||
|
settingKey: "lockScreenVideoCycling"
|
||||||
|
tags: ["lock", "screen", "video", "screensaver", "cycling", "random", "shuffle"]
|
||||||
|
text: I18n.tr("Automatic Cycling")
|
||||||
|
description: I18n.tr("Pick a different random video each time from the same folder")
|
||||||
|
visible: SettingsData.lockScreenVideoEnabled && MultimediaService.available
|
||||||
|
enabled: MultimediaService.available
|
||||||
|
checked: SettingsData.lockScreenVideoCycling
|
||||||
|
onToggled: checked => SettingsData.set("lockScreenVideoCycling", checked)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
SettingsCard {
|
SettingsCard {
|
||||||
width: parent.width
|
width: parent.width
|
||||||
iconName: "monitor"
|
iconName: "monitor"
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import Quickshell
|
|||||||
import Quickshell.Io
|
import Quickshell.Io
|
||||||
import Quickshell.Services.Pipewire
|
import Quickshell.Services.Pipewire
|
||||||
import qs.Common
|
import qs.Common
|
||||||
|
import qs.Services
|
||||||
|
|
||||||
Singleton {
|
Singleton {
|
||||||
id: root
|
id: root
|
||||||
@@ -14,7 +15,7 @@ Singleton {
|
|||||||
readonly property PwNode sink: Pipewire.defaultAudioSink
|
readonly property PwNode sink: Pipewire.defaultAudioSink
|
||||||
readonly property PwNode source: Pipewire.defaultAudioSource
|
readonly property PwNode source: Pipewire.defaultAudioSource
|
||||||
|
|
||||||
property bool soundsAvailable: false
|
readonly property bool soundsAvailable: MultimediaService.available
|
||||||
property bool gsettingsAvailable: false
|
property bool gsettingsAvailable: false
|
||||||
property var availableSoundThemes: []
|
property var availableSoundThemes: []
|
||||||
property string currentSoundTheme: ""
|
property string currentSoundTheme: ""
|
||||||
@@ -312,24 +313,6 @@ EOFCONFIG
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function detectSoundsAvailability() {
|
|
||||||
try {
|
|
||||||
const testObj = Qt.createQmlObject(`
|
|
||||||
import QtQuick
|
|
||||||
import QtMultimedia
|
|
||||||
Item {}
|
|
||||||
`, root, "AudioService.TestComponent");
|
|
||||||
if (testObj) {
|
|
||||||
testObj.destroy();
|
|
||||||
}
|
|
||||||
soundsAvailable = true;
|
|
||||||
return true;
|
|
||||||
} catch (e) {
|
|
||||||
soundsAvailable = false;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function checkGsettings() {
|
function checkGsettings() {
|
||||||
Proc.runCommand("checkGsettings", ["sh", "-c", "gsettings get org.gnome.desktop.sound theme-name 2>/dev/null"], (output, exitCode) => {
|
Proc.runCommand("checkGsettings", ["sh", "-c", "gsettings get org.gnome.desktop.sound theme-name 2>/dev/null"], (output, exitCode) => {
|
||||||
gsettingsAvailable = (exitCode === 0);
|
gsettingsAvailable = (exitCode === 0);
|
||||||
@@ -1028,10 +1011,7 @@ EOFCONFIG
|
|||||||
}
|
}
|
||||||
|
|
||||||
Component.onCompleted: {
|
Component.onCompleted: {
|
||||||
if (!detectSoundsAvailability()) {
|
if (soundsAvailable) {
|
||||||
console.warn("AudioService: QtMultimedia not available - sound effects disabled");
|
|
||||||
} else {
|
|
||||||
console.info("AudioService: Sound effects enabled");
|
|
||||||
checkGsettings();
|
checkGsettings();
|
||||||
Qt.callLater(createSoundPlayers);
|
Qt.callLater(createSoundPlayers);
|
||||||
}
|
}
|
||||||
|
|||||||
35
quickshell/Services/MultimediaService.qml
Normal file
35
quickshell/Services/MultimediaService.qml
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
pragma Singleton
|
||||||
|
pragma ComponentBehavior: Bound
|
||||||
|
|
||||||
|
import QtQuick
|
||||||
|
import Quickshell
|
||||||
|
|
||||||
|
Singleton {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property bool available: false
|
||||||
|
|
||||||
|
function detectAvailability() {
|
||||||
|
try {
|
||||||
|
const testObj = Qt.createQmlObject(`
|
||||||
|
import QtQuick
|
||||||
|
import QtMultimedia
|
||||||
|
Item {}
|
||||||
|
`, root, "MultimediaService.TestComponent");
|
||||||
|
if (testObj) {
|
||||||
|
testObj.destroy();
|
||||||
|
}
|
||||||
|
available = true;
|
||||||
|
return true;
|
||||||
|
} catch (e) {
|
||||||
|
available = false;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Component.onCompleted: {
|
||||||
|
if (!detectAvailability()) {
|
||||||
|
console.warn("MultimediaService: QtMultimedia not available");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user