From the 2026-06-15 on-box hardware test (all 6 prior fixes confirmed; these are the new findings, baked into the fork source rather than shipped as pacman-hook patches, per the Phase-2 "we own the shell" approach): - F-W1 (Weather.qml): fetch at startup. Stock only called reload() on a config CHANGE, so a saved location blanked after reboot and an empty/auto location never populated on a fresh boot. Add Component.onCompleted: reload() and switch the hourly Timer to reload() (re-detects auto/IP too). - F-W3 (Weather.qml): 7-day forecast showed "undefined°" in °F mode — only maxTempC/minTempC were stored. Store rounded maxTempF/minTempF (and round °C). - F-W2 (Weather.qml): empty/auto-IP location never populated — the in-shell ipinfo.io fetch didn't set loc (provider-specific; the city path works via the same Requests.get). Repair attempt: defensive JSON parse + error callback + fallback to geojs.io. Compiled Requests module not inspectable, so this is best-effort; a saved city still works regardless. - F-D1a (modules/dashboard/*.qml): square the 25 card radii bound to the compiled Tokens.rounding.* Material defaults (which ignore shell.json rounding.scale=0) so the dashboard matches the square redesign. - F-D1b (Glyphs.qml): remap the weather glyphs to the Material-weather family (every codepoint verified present in JetBrainsMono Nerd Font Mono); the old Font-Awesome codepoints were absent (rendered "?") and thunderstorm / snowing_heavy were unmapped. - F-T1 (NsBar.qml): gate the system-tray pill on SystemTray.items count so it only shows when an app registers a tray icon (was opening an empty panel). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
327 lines
8.9 KiB
QML
327 lines
8.9 KiB
QML
pragma ComponentBehavior: Bound
|
|
|
|
import QtQuick
|
|
import QtQuick.Effects
|
|
import QtQuick.Layouts
|
|
import Caelestia.Config
|
|
import Caelestia.Services
|
|
import qs.components
|
|
import qs.components.containers
|
|
import qs.components.controls
|
|
import qs.components.effects
|
|
import qs.services
|
|
|
|
Item {
|
|
id: root
|
|
|
|
// Funny binding hack to make lyrics update
|
|
readonly property var _: {
|
|
const p = Players.active;
|
|
if (p)
|
|
Lyrics.setTrack(p.trackArtist, p.trackTitle, p.trackAlbum, p.length);
|
|
else
|
|
Lyrics.clearTrack();
|
|
}
|
|
|
|
readonly property real fadeAmount: 0.1
|
|
property bool flag
|
|
property list<string> lyricList: Lyrics.lyrics
|
|
|
|
layer.enabled: true
|
|
layer.effect: Mask {
|
|
maskSource: mask
|
|
|
|
Rectangle {
|
|
id: mask
|
|
|
|
layer.enabled: true
|
|
visible: false
|
|
implicitWidth: root.width
|
|
implicitHeight: root.height
|
|
|
|
gradient: Gradient {
|
|
orientation: Gradient.Vertical
|
|
|
|
GradientStop {
|
|
color: Qt.alpha("black", 0)
|
|
position: 0
|
|
}
|
|
GradientStop {
|
|
color: Qt.alpha("black", 1)
|
|
position: root.fadeAmount
|
|
}
|
|
GradientStop {
|
|
color: Qt.alpha("black", 1)
|
|
position: 1 - root.fadeAmount
|
|
}
|
|
GradientStop {
|
|
color: Qt.alpha("black", 0)
|
|
position: 1
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
state: {
|
|
flag; // For some reason it doesn't update sometimes, so use this to force an update
|
|
if (Lyrics.hasLyrics)
|
|
return "hasLyrics";
|
|
if (Lyrics.loading)
|
|
return "loading";
|
|
return "noLyrics";
|
|
}
|
|
|
|
states: [
|
|
State {
|
|
name: "loading"
|
|
|
|
PropertyChanges {
|
|
loadingIndicator.opacity: 1
|
|
lyrics.opacity: 0
|
|
noLyrics.opacity: 0
|
|
}
|
|
},
|
|
State {
|
|
name: "hasLyrics"
|
|
|
|
PropertyChanges {
|
|
loadingIndicator.opacity: 0
|
|
lyrics.opacity: 1
|
|
noLyrics.opacity: 0
|
|
}
|
|
},
|
|
State {
|
|
name: "noLyrics"
|
|
|
|
PropertyChanges {
|
|
loadingIndicator.opacity: 0
|
|
lyrics.opacity: 0
|
|
noLyrics.opacity: 1
|
|
}
|
|
}
|
|
]
|
|
|
|
transitions: [
|
|
Transition {
|
|
from: "loading"
|
|
|
|
SequentialAnimation {
|
|
Anim {
|
|
target: loadingIndicator
|
|
property: "opacity"
|
|
type: Anim.DefaultEffects
|
|
}
|
|
Anim {
|
|
targets: [lyrics, noLyrics]
|
|
property: "opacity"
|
|
type: Anim.SlowEffects
|
|
}
|
|
}
|
|
},
|
|
Transition {
|
|
from: "hasLyrics"
|
|
|
|
SequentialAnimation {
|
|
Anim {
|
|
target: lyrics
|
|
property: "opacity"
|
|
type: Anim.DefaultEffects
|
|
}
|
|
Anim {
|
|
targets: [loadingIndicator, noLyrics]
|
|
property: "opacity"
|
|
type: Anim.SlowEffects
|
|
}
|
|
}
|
|
},
|
|
Transition {
|
|
from: "noLyrics"
|
|
|
|
SequentialAnimation {
|
|
Anim {
|
|
target: noLyrics
|
|
property: "opacity"
|
|
type: Anim.DefaultEffects
|
|
}
|
|
Anim {
|
|
targets: [loadingIndicator, lyrics]
|
|
property: "opacity"
|
|
type: Anim.SlowEffects
|
|
}
|
|
}
|
|
}
|
|
]
|
|
|
|
Connections {
|
|
function onHasLyricsChanged() {
|
|
root.flag = !root.flag;
|
|
}
|
|
|
|
target: Lyrics
|
|
}
|
|
|
|
Loader {
|
|
id: loadingIndicator
|
|
|
|
anchors.centerIn: parent
|
|
asynchronous: true
|
|
active: opacity > 0
|
|
opacity: 0
|
|
|
|
sourceComponent: ColumnLayout {
|
|
spacing: Tokens.spacing.large
|
|
|
|
StyledRect {
|
|
Layout.alignment: Qt.AlignHCenter
|
|
implicitWidth: shape.implicitSize + Tokens.padding.medium * 2
|
|
implicitHeight: shape.implicitSize + Tokens.padding.medium * 2
|
|
color: Colours.palette.m3primaryContainer
|
|
radius: 0 // nosignal: square dashboard cards to match the redesign (F-D1)
|
|
|
|
LoadingIndicator {
|
|
id: shape
|
|
|
|
anchors.centerIn: parent
|
|
implicitSize: Math.round(Tokens.sizes.dashboard.mediaSectionWidth / 5)
|
|
containsIcon: true // This removes the pentagon, which is not centered
|
|
}
|
|
}
|
|
|
|
StyledText {
|
|
text: qsTr("Loading lyrics...")
|
|
color: Colours.palette.m3onSurfaceVariant
|
|
font: Tokens.font.title.medium
|
|
}
|
|
}
|
|
|
|
Behavior on opacity {
|
|
Anim {
|
|
type: Anim.DefaultEffects
|
|
}
|
|
}
|
|
}
|
|
|
|
Loader {
|
|
id: noLyrics
|
|
|
|
anchors.centerIn: parent
|
|
asynchronous: true
|
|
active: opacity > 0
|
|
opacity: 0
|
|
|
|
sourceComponent: ColumnLayout {
|
|
spacing: Tokens.spacing.small
|
|
|
|
NsIcon {
|
|
Layout.alignment: Qt.AlignHCenter
|
|
icon: "sentiment_sad"
|
|
fontStyle: Tokens.font.icon.builders.large.scale(2).build()
|
|
color: Colours.palette.m3outline
|
|
}
|
|
|
|
StyledText {
|
|
text: qsTr("No lyrics found")
|
|
color: Colours.palette.m3outline
|
|
font: Tokens.font.title.medium
|
|
}
|
|
}
|
|
}
|
|
|
|
StyledListView {
|
|
id: lyrics
|
|
|
|
anchors.fill: parent
|
|
anchors.topMargin: parent.height * root.fadeAmount / 2
|
|
anchors.bottomMargin: parent.height * root.fadeAmount / 2
|
|
|
|
displayMarginBeginning: anchors.topMargin
|
|
displayMarginEnd: anchors.bottomMargin
|
|
|
|
model: root.lyricList
|
|
Component.onCompleted: {
|
|
currentIndex = Qt.binding(() => {
|
|
model; // Force update when lyrics change
|
|
return Lyrics.indexForTime(Players.active?.position ?? 0);
|
|
});
|
|
positionViewAtIndex(currentIndex, ListView.Center);
|
|
}
|
|
onModelChanged: Qt.callLater(() => positionViewAtIndex(currentIndex, ListView.Center))
|
|
|
|
highlightRangeMode: ListView.ApplyRange
|
|
highlightMoveDuration: Tokens.anim.durations.large
|
|
highlightMoveVelocity: -1
|
|
preferredHighlightBegin: (height - (currentItem?.implicitHeight ?? 0)) / 2
|
|
preferredHighlightEnd: (height + (currentItem?.implicitHeight ?? 0)) / 2
|
|
|
|
spacing: Tokens.spacing.small
|
|
opacity: 0
|
|
|
|
delegate: StyledText {
|
|
id: lyric
|
|
|
|
required property string modelData
|
|
required property int index
|
|
property real effectScale: ListView.isCurrentItem ? 1 : 0
|
|
|
|
anchors.left: lyrics.contentItem.left
|
|
anchors.right: lyrics.contentItem.right
|
|
|
|
text: modelData || ". . ."
|
|
color: ListView.isCurrentItem ? Colours.palette.m3primary : mouse.containsMouse ? Colours.palette.m3onSurface : Colours.palette.m3outline
|
|
font: Tokens.font.body.medium
|
|
wrapMode: Text.WrapAtWordBoundaryOrAnywhere
|
|
|
|
layer.enabled: effectScale > 0
|
|
layer.effect: MultiEffect {
|
|
shadowEnabled: true
|
|
shadowColor: Colours.palette.m3primary
|
|
shadowOpacity: 0.5 * lyric.effectScale
|
|
shadowBlur: 0.6 * lyric.effectScale
|
|
blur: 0.4 * lyric.effectScale
|
|
}
|
|
|
|
Behavior on effectScale {
|
|
Anim {
|
|
type: Anim.SlowEffects
|
|
}
|
|
}
|
|
|
|
MouseArea {
|
|
id: mouse
|
|
|
|
anchors.fill: parent
|
|
cursorShape: Qt.PointingHandCursor
|
|
hoverEnabled: true
|
|
onClicked: {
|
|
const p = Players.active;
|
|
if (p)
|
|
p.position = Lyrics.timeForIndex(lyric.index);
|
|
}
|
|
}
|
|
}
|
|
|
|
Behavior on opacity {
|
|
Anim {
|
|
type: Anim.SlowEffects
|
|
}
|
|
}
|
|
}
|
|
|
|
Behavior on lyricList {
|
|
SequentialAnimation {
|
|
Anim {
|
|
target: lyrics
|
|
property: "opacity"
|
|
to: 0
|
|
type: Anim.DefaultEffects
|
|
}
|
|
PropertyAction {}
|
|
Anim {
|
|
target: lyrics
|
|
property: "opacity"
|
|
to: 1
|
|
type: Anim.SlowEffects
|
|
}
|
|
}
|
|
}
|
|
}
|