331 lines
11 KiB
QML
331 lines
11 KiB
QML
import QtQuick
|
|
import Quickshell
|
|
import Quickshell.Wayland
|
|
import Quickshell.Io
|
|
import qs.Commons
|
|
import qs.Commons as Commons
|
|
|
|
// Keep startup and theme covers alive across background service handoffs.
|
|
Item {
|
|
id: root
|
|
|
|
property var host: null
|
|
property bool cover: true
|
|
readonly property bool sessionConsumed: String(sessionMarker.text() || "").trim() === (Quickshell.env("OMARCHY_SESSION_ID") || Quickshell.env("HYPRLAND_INSTANCE_SIGNATURE"))
|
|
property bool startupSettled: sessionConsumed
|
|
property bool startupPending: !sessionConsumed
|
|
property bool startupMediaReady: false
|
|
property real startupOpacity: startupPending ? 1 : 0
|
|
readonly property bool desktopReady: !host || (!host.pluginReloading && (!host.pluginRegistry || !host.pluginRegistry.scanning) && (!("bar" in host) || !!host.bar))
|
|
property bool checked: false
|
|
property string themeToken: ""
|
|
property string transitionToken: ""
|
|
readonly property string startupBackgroundPath: Quickshell.env("OMARCHY_STARTUP_BACKGROUND")
|
|
property string themeBackground: sessionConsumed && !Util.isVideoPath(startupBackgroundPath) ? startupBackgroundPath : ""
|
|
property var themeNativeSize: null
|
|
property string themeColors: ""
|
|
property string themeShell: ""
|
|
property real themeOpacity: 1
|
|
readonly property var backgroundService: host && host.services ? host.firstPartyServiceFor("omarchy.background") : null
|
|
readonly property bool backgroundActive: !!backgroundService && !backgroundService.suspended
|
|
readonly property bool backgroundReady: backgroundActive && backgroundService.ready !== false
|
|
|
|
function restoreStartupCursor() {
|
|
Quickshell.execDetached(["hyprctl", "eval", "if omarchy_startup_cursor_restore then omarchy_startup_cursor_restore() end"])
|
|
}
|
|
|
|
function finishStartup() {
|
|
if (!cover || !startupSettled) return
|
|
var registry = host ? host.pluginRegistry : null
|
|
var backgroundId = registry ? registry.resolveEnabledId("omarchy.background") : ""
|
|
var disabled = registry && registry.installedPlugins[backgroundId] && !registry.isEnabled(backgroundId)
|
|
if (!backgroundReady && !disabled) return
|
|
cover = false
|
|
if (!themeToken) themeBackground = ""
|
|
startupMediaReady = true
|
|
revealStartup()
|
|
}
|
|
|
|
function revealStartup() {
|
|
if (!startupPending || startupFade.running || !startupMediaReady || !desktopReady) return
|
|
if (!startupCovers.instances.length) return
|
|
for (var panel of startupCovers.instances)
|
|
if (panel.readyFrames < 2) return
|
|
startupFade.start()
|
|
}
|
|
|
|
onDesktopReadyChanged: revealStartup()
|
|
onBackgroundReadyChanged: finishStartup()
|
|
Connections {
|
|
target: root.host && root.host.pluginRegistry ? root.host.pluginRegistry : null
|
|
function onPluginsChanged() { root.finishStartup() }
|
|
}
|
|
|
|
function prepareTheme(fromPath, token, colors, shell) {
|
|
framePoll.stop()
|
|
themeFade.stop()
|
|
themeToken = token
|
|
transitionToken = token
|
|
if (themeBackground !== fromPath) {
|
|
themeNativeSize = backgroundService && backgroundService.nativeSizes ? backgroundService.nativeSizes[backgroundService.displayedBackground] : null
|
|
}
|
|
themeBackground = fromPath
|
|
themeColors = colors
|
|
themeShell = shell
|
|
themeOpacity = 1
|
|
themeFallback.restart()
|
|
}
|
|
|
|
function revealTheme() {
|
|
if (!themeToken && !cover) return
|
|
framePoll.stop()
|
|
themeFallback.stop()
|
|
if (themeToken) {
|
|
Commons.Color.loadColors(Util.decodeBase64(themeColors))
|
|
Commons.Color.loadShell(Util.decodeBase64(themeShell))
|
|
Style.scheduleRefresh()
|
|
}
|
|
cover = false
|
|
themeToken = ""
|
|
themeColors = ""
|
|
themeShell = ""
|
|
if (startupPending) {
|
|
// The login reveal starts on moving video, without showing its final still.
|
|
themeBackground = ""
|
|
startupMediaReady = true
|
|
revealStartup()
|
|
} else {
|
|
themeFade.restart()
|
|
}
|
|
}
|
|
|
|
function finishTheme(token) {
|
|
if (token === themeToken) revealTheme()
|
|
}
|
|
|
|
function themeStatus(token) {
|
|
return token === transitionToken && (themeToken || themeFade.running || startupFade.running) ? "pending" : "ready"
|
|
}
|
|
|
|
function themeCoverStatus(token) {
|
|
if (token !== themeToken) return "superseded"
|
|
if (!themeBackground) return "ready"
|
|
for (var panel of covers.instances) {
|
|
if (panel.coverFailed) return "error"
|
|
if (!panel.coverReady) return "loading"
|
|
}
|
|
return covers.instances.length > 0 ? "ready" : "loading"
|
|
}
|
|
|
|
function cancelTheme() {
|
|
framePoll.stop()
|
|
themeFallback.stop()
|
|
themeFade.stop()
|
|
themeToken = ""
|
|
transitionToken = ""
|
|
themeBackground = ""
|
|
themeColors = ""
|
|
themeShell = ""
|
|
}
|
|
|
|
NumberAnimation {
|
|
id: startupFade
|
|
target: root
|
|
property: "startupOpacity"
|
|
to: 0
|
|
duration: Style.duration(420)
|
|
easing.type: Easing.OutCubic
|
|
onStarted: root.restoreStartupCursor()
|
|
onFinished: root.startupPending = false
|
|
}
|
|
|
|
// A missing wallpaper or failed plugin must not leave applications hidden.
|
|
Timer {
|
|
interval: 10000
|
|
running: root.startupPending
|
|
onTriggered: {
|
|
root.cover = false
|
|
if (!root.themeToken) root.themeBackground = ""
|
|
root.startupMediaReady = true
|
|
startupFade.start()
|
|
}
|
|
}
|
|
|
|
NumberAnimation {
|
|
id: themeFade
|
|
target: root
|
|
property: "themeOpacity"
|
|
to: 0
|
|
duration: Style.duration(420)
|
|
easing.type: Easing.OutCubic
|
|
onFinished: root.themeBackground = ""
|
|
}
|
|
|
|
Timer {
|
|
id: themeFallback
|
|
interval: 10000
|
|
onTriggered: root.revealTheme()
|
|
}
|
|
|
|
// A renderer left over from another intro can first fade from its still.
|
|
// Keep that preparation hidden until the actual video is fully revealed.
|
|
Timer {
|
|
id: framePoll
|
|
interval: 16
|
|
repeat: true
|
|
onTriggered: if (!frameStatus.running) frameStatus.running = true
|
|
}
|
|
|
|
Process {
|
|
id: frameStatus
|
|
command: ["owe", "render-status"]
|
|
property string token: ""
|
|
onStarted: token = root.themeToken
|
|
stdout: StdioCollector { id: frameStatusOut }
|
|
onExited: function(exitCode) {
|
|
if (exitCode !== 0 || token !== root.themeToken || (!token && !root.cover)) return
|
|
try {
|
|
var status = JSON.parse(frameStatusOut.text)
|
|
if (status.kind === "video" && status.ready && !status.has_transition && status.time_pos > 0)
|
|
root.revealTheme()
|
|
} catch (e) {}
|
|
}
|
|
}
|
|
|
|
// A shell restart shares this compositor session; a new login does not.
|
|
FileView {
|
|
id: sessionMarker
|
|
path: Quickshell.env("HOME") + "/.local/state/omarchy/background-intro.session-id"
|
|
blockLoading: true
|
|
watchChanges: false
|
|
printErrors: false
|
|
}
|
|
|
|
Process {
|
|
id: startupBackground
|
|
command: ["readlink", "-f", Quickshell.env("HOME") + "/.local/state/omarchy/current/background"]
|
|
stdout: StdioCollector { id: startupBackgroundOut }
|
|
onExited: function(exitCode) {
|
|
var path = String(startupBackgroundOut.text || "").trim()
|
|
if (exitCode === 0 && root.cover && !root.startupPending && !root.themeToken && !Util.isVideoPath(path))
|
|
root.themeBackground = path
|
|
}
|
|
}
|
|
|
|
Component.onCompleted: {
|
|
checked = true
|
|
if (sessionConsumed) restoreStartupCursor()
|
|
if (!startupBackgroundPath && !startupPending) startupBackground.running = true
|
|
introProc.running = true
|
|
}
|
|
|
|
Component.onDestruction: if (startupPending) restoreStartupCursor()
|
|
|
|
onBackgroundActiveChanged: {
|
|
if (!backgroundActive && checked) {
|
|
if (cover || themeToken) {
|
|
framePoll.start()
|
|
if (!frameStatus.running) frameStatus.running = true
|
|
}
|
|
}
|
|
}
|
|
|
|
// Cover the bar as well as the wallpaper on a new login. Shell restarts
|
|
// retain the current desktop and never map this startup cover.
|
|
Variants {
|
|
id: startupCovers
|
|
model: Quickshell.screens
|
|
|
|
PanelWindow {
|
|
required property var modelData
|
|
property int readyFrames: 0
|
|
screen: modelData
|
|
visible: root.startupPending
|
|
color: "transparent"
|
|
mask: Region {}
|
|
anchors { top: true; bottom: true; left: true; right: true }
|
|
exclusionMode: ExclusionMode.Ignore
|
|
WlrLayershell.layer: WlrLayer.Overlay
|
|
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
|
|
WlrLayershell.namespace: "omarchy-background"
|
|
|
|
Rectangle {
|
|
anchors.fill: parent
|
|
color: "black"
|
|
opacity: root.startupOpacity
|
|
}
|
|
|
|
FrameAnimation {
|
|
running: root.startupPending && root.desktopReady && readyFrames < 2
|
|
onTriggered: {
|
|
readyFrames += 1
|
|
if (readyFrames === 2) root.revealStartup()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Variants {
|
|
id: covers
|
|
model: Quickshell.screens
|
|
|
|
PanelWindow {
|
|
id: panel
|
|
required property var modelData
|
|
property int coverFrames: 0
|
|
readonly property bool coverReady: outgoingFrame.status === Image.Ready && coverFrames >= 2
|
|
readonly property bool coverFailed: outgoingFrame.status === Image.Error
|
|
screen: modelData
|
|
visible: (!root.startupPending && root.cover) || root.themeBackground !== ""
|
|
// Keep the window transparent throughout the fade. Paint the startup
|
|
// color inside it so changing the window format cannot flash black.
|
|
color: "transparent"
|
|
mask: Region {}
|
|
anchors { top: true; bottom: true; left: true; right: true }
|
|
exclusionMode: ExclusionMode.Ignore
|
|
WlrLayershell.layer: WlrLayer.Bottom
|
|
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
|
|
WlrLayershell.namespace: "omarchy-background"
|
|
|
|
Rectangle {
|
|
anchors.fill: parent
|
|
visible: root.cover
|
|
color: Commons.Color.background
|
|
}
|
|
|
|
Image {
|
|
id: outgoingFrame
|
|
anchors.fill: parent
|
|
source: root.themeBackground ? Util.fileUrl(root.themeBackground) : ""
|
|
sourceSize: {
|
|
var w = Math.ceil((parent.width || modelData.width) * modelData.devicePixelRatio)
|
|
var h = Math.ceil((parent.height || modelData.height) * modelData.devicePixelRatio)
|
|
var native = root.themeNativeSize
|
|
return native && (native.width < w || native.height < h) ? Qt.size(native.width, native.height) : Qt.size(w, h)
|
|
}
|
|
fillMode: Image.PreserveAspectCrop
|
|
opacity: root.themeOpacity
|
|
// Start decoding while the remaining theme configs render.
|
|
asynchronous: true
|
|
onStatusChanged: panel.coverFrames = 0
|
|
}
|
|
|
|
// Give the ready image a frame to reach the compositor before OWE
|
|
// releases the shell's wallpaper underneath this cover.
|
|
FrameAnimation {
|
|
running: outgoingFrame.status === Image.Ready && panel.coverFrames < 2
|
|
onTriggered: panel.coverFrames += 1
|
|
}
|
|
}
|
|
}
|
|
|
|
Process {
|
|
id: introProc
|
|
command: ["omarchy-theme-bg-boot-intro"]
|
|
onExited: {
|
|
root.startupSettled = true
|
|
root.finishStartup()
|
|
}
|
|
}
|
|
}
|