Replaces the Persona x Mocha rice. NieR's menu language is kept whole — plain rectangles, hairline rules, letter-spaced upright capitals, a square marker on the current row, corner ticks round panels, and selection shown by inverting the row — but the colours come from ~/.wallpapers/space-1.jpg rather than from the game's beige: the grey-green void as the ground, the suit's sage grey as text, the planet's white as the one accent, and the hull's red indicator lights as the alarm. The Persona geometry is switched off rather than ripped out: Theme.skew and Theme.cut are 0, so P5Panel, Skew and Geom collapse to rectangles. Overshoot easing, italics and black weights are gone shell-wide. New, borrowed from caelestia's screen border: bar/Frame.qml draws the bar, the rail and a thin left/bottom band as one hull on the bottom layer, with the desktop as a rounded opening cut out of it. Frame also owns all four exclusive zones, so the bar and rail are placed from the screen edge. Hyprland moves to 1px borders lit from the planet white into grey, a soft wide shadow and gaps measured from the opening. Alacritty, fish and dunst get matching YoRHa palettes. Neovim, GTK/Qt and btop are still Mocha. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
229 lines
8.1 KiB
QML
229 lines
8.1 KiB
QML
import QtQuick
|
|
import QtQuick.Shapes
|
|
import QtQuick.Window
|
|
import Quickshell
|
|
import "root:/config"
|
|
import "root:/components"
|
|
import "root:/services"
|
|
import "root:/popouts"
|
|
|
|
// Now-playing module. Collapses to nothing when no player is around.
|
|
BarPill {
|
|
id: root
|
|
|
|
required property var screenRef
|
|
|
|
accent: Theme.primary
|
|
padding: Theme.padS
|
|
visible: Media.hasPlayer || width > 1
|
|
|
|
// Track position, drawn by the pill in its own rule band.
|
|
progress: Media.hasTimeline ? Media.progress : -1
|
|
|
|
implicitWidth: Media.hasPlayer
|
|
? layout.implicitWidth + contentPad * 2
|
|
: 0
|
|
|
|
Behavior on implicitWidth {
|
|
NumberAnimation { duration: Theme.durSlow; easing.type: Easing.OutExpo }
|
|
}
|
|
|
|
clip: true
|
|
|
|
onClicked: Media.playPause()
|
|
onRightClicked: Media.next()
|
|
onMiddleClicked: Media.raise()
|
|
onScrolled: delta => delta > 0 ? Media.next() : Media.previous()
|
|
|
|
Row {
|
|
id: layout
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
anchors.left: parent.left
|
|
spacing: Theme.padS
|
|
|
|
// Album art, chopped into the house shape.
|
|
Item {
|
|
id: artFrame
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
width: 24
|
|
height: 24
|
|
|
|
Shape {
|
|
id: artMask
|
|
anchors.fill: parent
|
|
visible: false
|
|
layer.enabled: true
|
|
layer.smooth: true
|
|
preferredRendererType: Shape.CurveRenderer
|
|
ShapePath {
|
|
fillColor: "white"
|
|
strokeColor: "transparent"
|
|
PathPolyline {
|
|
path: Geom.chamfer(Geom.parallelogram(24, 24, 0.18), 5, [1, 3])
|
|
}
|
|
}
|
|
}
|
|
|
|
Image {
|
|
id: art
|
|
anchors.fill: parent
|
|
source: Media.artUrl
|
|
fillMode: Image.PreserveAspectCrop
|
|
cache: true
|
|
asynchronous: true
|
|
visible: status === Image.Ready
|
|
layer.enabled: true
|
|
layer.smooth: true
|
|
layer.effect: MaskedArt { maskItem: artMask }
|
|
}
|
|
|
|
// Fallback: the source app's glyph.
|
|
Icon {
|
|
anchors.centerIn: parent
|
|
visible: art.status !== Image.Ready
|
|
text: Media.sourceIcon
|
|
font.pixelSize: Theme.fsLarge
|
|
color: Theme.primary
|
|
}
|
|
}
|
|
|
|
// Title and artist size to the text, up to a cap.
|
|
//
|
|
// These used to be a flat 130px wide each, which meant a short title
|
|
// still reserved 130px and left a hand-sized hole between the artist and
|
|
// the equaliser — the pill looked broken rather than compact. They take
|
|
// what they need now and only start scrolling past the cap.
|
|
Column {
|
|
id: meta
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
spacing: 0
|
|
|
|
readonly property real cap: 150
|
|
|
|
Text {
|
|
width: Math.min(meta.cap, implicitWidth)
|
|
height: 14
|
|
text: Media.title || Media.identity
|
|
color: Theme.text
|
|
elide: Text.ElideRight
|
|
font.pixelSize: Theme.fsSmall
|
|
font.family: Theme.fontDisplay
|
|
font.weight: Font.DemiBold
|
|
renderType: Text.NativeRendering
|
|
}
|
|
|
|
Text {
|
|
width: Math.min(meta.cap, implicitWidth)
|
|
height: 12
|
|
text: Media.artist || Media.album || Media.identity
|
|
color: Theme.muted
|
|
elide: Text.ElideRight
|
|
font.pixelSize: Theme.fsMicro
|
|
font.family: Theme.fontMono
|
|
renderType: Text.NativeRendering
|
|
}
|
|
}
|
|
|
|
// Compact play-state equaliser. It moves only while audio is playing
|
|
// and freezes into three quiet bars when paused.
|
|
//
|
|
// Sampled from a timer rather than tweened by an infinite
|
|
// NumberAnimation. DP-1 runs at 240 Hz, and a frame-synced animation
|
|
// here repainted the entire bar 240 times a second: this pill sits
|
|
// inside ancestors that use layer.enabled together with
|
|
// Shape.CurveRenderer, and the album art carries a MultiEffect pass, so
|
|
// every one of those frames re-rendered several layer textures. Measured
|
|
// on its own, this one animation cost about two thirds of a CPU core
|
|
// whenever anything was playing.
|
|
//
|
|
// The timer samples a sine, it does not step through arbitrary levels: a
|
|
// low tick rate only looks like the original if consecutive ticks stay
|
|
// near each other. Each bar keeps the eased breathing motion and the
|
|
// period it had before — the curve is just read 25 times a second
|
|
// instead of 240.
|
|
Row {
|
|
id: eq
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
spacing: 2
|
|
|
|
// Sampling interval — the one knob trading smoothness against cost.
|
|
// Frames are the whole expense: a full-bar repaint runs about 0.3% of
|
|
// a core (the bar spans 2544x40 on a 240 Hz output), so cost scales
|
|
// linearly with this rate. 16 ms holds 60 Hz, which is visually
|
|
// indistinguishable from the original frame-synced tween at a quarter
|
|
// of its ~75%.
|
|
readonly property int tickMs: 16
|
|
property int phase: 0
|
|
|
|
// Reproduces the original per-bar tween exactly, sampled instead of
|
|
// frame-synced. Each bar eased between two heights with its own pair
|
|
// of durations, so the three differed in both swing and period — the
|
|
// middle bar moved barely a pixel while the outer two ran in
|
|
// opposition. Giving all three the full swing, as a single symmetric
|
|
// sine would, is three times the motion and reads far busier than
|
|
// this meter is supposed to.
|
|
function level(i: int): real {
|
|
const lo = 4 + (i * 5) % 13; // old first NumberAnimation `to`
|
|
const hi = 14 - (i * 4) % 9; // old second NumberAnimation `to`
|
|
const d1 = 320 + i * 90; // ...and their durations
|
|
const d2 = 280 + i * 70;
|
|
|
|
const t = (eq.phase * eq.tickMs) % (d1 + d2);
|
|
// Easing.InOutSine, which is what both halves used.
|
|
const ease = x => (1 - Math.cos(Math.PI * x)) / 2;
|
|
|
|
return t < d1
|
|
? hi + (lo - hi) * ease(t / d1)
|
|
: lo + (hi - lo) * ease((t - d1) / d2);
|
|
}
|
|
|
|
Timer {
|
|
interval: eq.tickMs
|
|
running: Media.playing && eq.visible && (eq.Window.window?.visible ?? true)
|
|
repeat: true
|
|
onTriggered: eq.phase++
|
|
// Restart the cycle from its trough, so playback always begins
|
|
// from a settled meter rather than mid-swing.
|
|
onRunningChanged: if (!running) eq.phase = 0
|
|
}
|
|
|
|
Repeater {
|
|
model: 3
|
|
|
|
Rectangle {
|
|
required property int index
|
|
width: 3
|
|
height: Media.playing ? eq.level(index) : 6
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
color: Media.playing ? Theme.primary : Theme.muted
|
|
|
|
Behavior on color {
|
|
ColorAnimation { duration: Theme.durBase }
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
onVisibleChanged: if (visible) titleGlitch.restart()
|
|
|
|
Connections {
|
|
target: Media
|
|
function onTrackChanged() { titleGlitch.restart(); }
|
|
}
|
|
|
|
SequentialAnimation {
|
|
id: titleGlitch
|
|
NumberAnimation {
|
|
target: artFrame; property: "rotation"
|
|
from: -8; to: 0; duration: Theme.durSlow; easing.type: Easing.OutCubic
|
|
}
|
|
}
|
|
|
|
MediaPopout {
|
|
id: popout
|
|
anchorItem: root
|
|
triggerHovered: root.hovered && Media.hasPlayer
|
|
}
|
|
}
|