diff --git a/modules/nexus/PageCompRegistry.qml b/modules/nexus/PageCompRegistry.qml index 76d2ab5a..8c48110b 100644 --- a/modules/nexus/PageCompRegistry.qml +++ b/modules/nexus/PageCompRegistry.qml @@ -132,6 +132,15 @@ QtObject { LanguageAndRegion {} } } + }, + + // About + Component { + StackPage { + Component { + AboutPage {} + } + } } ] diff --git a/modules/nexus/common/AnimatedLogo.qml b/modules/nexus/common/AnimatedLogo.qml new file mode 100644 index 00000000..6d2aaa00 --- /dev/null +++ b/modules/nexus/common/AnimatedLogo.qml @@ -0,0 +1,513 @@ +pragma ComponentBehavior: Bound + +import QtQuick +import QtQuick.Effects +import QtQuick.Shapes +import qs.components +import qs.services + +Item { + id: root + + property real blurAmount: skipIntroAnimation ? 0.0 : 1.0 + property bool skipIntroAnimation: false + + property real star1Opacity: skipIntroAnimation ? 1.0 : 0.0 + property real star2Opacity: skipIntroAnimation ? 1.0 : 0.0 + property real star3Opacity: skipIntroAnimation ? 1.0 : 0.0 + + property real star1Scale: skipIntroAnimation ? 1.0 : 0.0 + property real star2Scale: skipIntroAnimation ? 1.0 : 0.0 + property real star3Scale: skipIntroAnimation ? 1.0 : 0.0 + + readonly property alias topShape: topShape + readonly property alias bottomShape: bottomShape + readonly property alias star1: star1 + readonly property alias star2: star2 + readonly property alias star3: star3 + + signal animationCompleted + + implicitWidth: 128 + implicitHeight: 90.38 + + Item { + id: logo + + readonly property real designWidth: 128 + readonly property real designHeight: 90.38 + + property color topColour: Colours.palette.m3primary + property color bottomColour: Colours.palette.m3onSurface + + implicitWidth: designWidth + implicitHeight: designHeight + + transformOrigin: Item.Center + scale: root.skipIntroAnimation ? 1.0 : 0.0 + opacity: root.skipIntroAnimation ? 1.0 : 0.0 + rotation: 0.0 + + layer.enabled: root.blurAmount > 0 + layer.effect: MultiEffect { + blurEnabled: true + blur: root.blurAmount + blurMax: 60 + } + + Component.onCompleted: { + root.star1.opacity = Qt.binding(() => root.star1Opacity); + root.star1.scale = Qt.binding(() => root.star1Scale); + root.star2.opacity = Qt.binding(() => root.star2Opacity); + root.star2.scale = Qt.binding(() => root.star2Scale); + root.star3.opacity = Qt.binding(() => root.star3Opacity); + root.star3.scale = Qt.binding(() => root.star3Scale); + } + + Behavior on topColour { + CAnim {} + } + + Behavior on bottomColour { + CAnim {} + } + + Shape { + id: topShape + + anchors.centerIn: parent + width: logo.designWidth + height: logo.designHeight + scale: Math.min(logo.width / width, logo.height / height) + transformOrigin: Item.Center + preferredRendererType: Shape.CurveRenderer + + ShapePath { + fillColor: logo.topColour + strokeColor: "transparent" + + PathSvg { + path: "m42.56,42.96c-7.76,1.6-16.36,4.22-22.44,6.22-.49.16-.88-.44-.53-.82,5.37-5.85,9.66-13.3,9.66-13.3,8.66-14.67,22.97-23.51,39.85-21.14,6.47.91,12.33,3.38,17.26,6.98.99.72,1.14,2.14.31,3.04-.4.44-.95.67-1.51.67-.34,0-.69-.09-1-.26-3.21-1.84-6.82-2.69-10.71-3.24-13.1-1.84-25.41,4.75-31.06,15.83-.94,1.84-.61,3.81.45,5.21.22.3.07.72-.29.8Z" + } + } + } + + Shape { + id: bottomShape + + anchors.fill: parent + preferredRendererType: Shape.CurveRenderer + + ShapePath { + fillColor: logo.bottomColour + strokeColor: "transparent" + + PathSvg { + path: "m103.02,51.8c-.65.11-1.26-.37-1.28-1.03-.06-1.96.15-3.89-.2-5.78-.28-1.48-1.66-2.5-3.16-2.34h-.05c-6.53.73-24.63,3.1-48,9.32-6.89,1.83-9.83,10-5.67,15.79,4.62,6.44,11.84,10.93,20.41,12.13,11.82,1.66,22.99-3.36,29.21-12.65.54-.81,1.54-1.17,2.47-.86.91.3,1.47,1.15,1.47,2.04,0,.33-.08.66-.24.98-7.23,14.21-22.91,22.95-39.59,20.6-7.84-1.1-14.8-4.5-20.28-9.43,0,0,0,0-.02-.01-7.28-5.14-14.7-9.99-27.24-11.98-18.82-2.98-9.53-8.75.46-13.78,7.36-3.13,25.17-7.9,36.24-10.73.16-.03.31-.06.47-.1,1.52-.4,3.2-.83,5.02-1.29,1.06-.26,1.93-.48,2.58-.64.09-.02.18-.04.26-.06.31-.08.56-.14.73-.18.03,0,.06-.01.08-.02.03,0,.05-.01.07-.02.02,0,.04,0,.06-.01.01,0,.03,0,.04-.01,0,0,.02,0,.03,0,.01,0,.02,0,.02,0,10.62-2.58,24.63-5.62,37.74-7.34,1.02-.13,2.03-.26,3.03-.37,7.49-.87,14.58-1.26,20.42-.81,25.43,1.95-4.71,16.77-15.12,18.61Z" + } + } + } + + Shape { + id: star1 + + anchors.fill: parent + preferredRendererType: Shape.CurveRenderer + opacity: 0.0 + + ShapePath { + fillColor: logo.topColour + strokeColor: "transparent" + + PathSvg { + path: "m98.12.06c-.29,2.08-1.72,8.42-8.36,9.19-.09,0-.09.13,0,.14,6.64.78,8.07,7.11,8.36,9.19.01.08.13.08.14,0,.29-2.08,1.72-8.42,8.36-9.19.09,0,.09-.13,0-.14-6.64-.78-8.07-7.11-8.36-9.19-.01-.08-.13-.08-.14,0Z" + } + } + } + + Shape { + id: star2 + + anchors.fill: parent + preferredRendererType: Shape.CurveRenderer + opacity: 0.0 + + ShapePath { + fillColor: logo.topColour + strokeColor: "transparent" + + PathSvg { + path: "m113.36,15.5c-.22,1.29-1.08,4.35-4.38,4.87-.08.01-.08.13,0,.14,3.3.52,4.17,3.58,4.38,4.87.01.08.13.08.14,0,.22-1.29,1.08-4.35,4.38-4.87.08-.01.08-.13,0-.14-3.3-.52-4.17-3.58-4.38-4.87-.01-.08-.13-.08-.14,0Z" + } + } + } + + Shape { + id: star3 + + anchors.fill: parent + preferredRendererType: Shape.CurveRenderer + opacity: 0.0 + + ShapePath { + fillColor: logo.topColour + strokeColor: "transparent" + + PathSvg { + path: "m112.69,65.22c-.19,1.01-.86,3.15-3.2,3.57-.08.01-.08.13,0,.14,2.34.42,3.01,2.56,3.2,3.57.01.08.13.08.14,0,.19-1.01.86-3.15,3.2-3.57.08-.01.08-.13,0-.14-2.34-.42-3.01-2.56-3.2-3.57-.01-.08-.13-.08-.14,0Z" + } + } + } + } + + SequentialAnimation { + running: !root.skipIntroAnimation + onFinished: root.animationCompleted() + + ParallelAnimation { + SequentialAnimation { + NumberAnimation { + target: logo + property: "rotation" + from: 0 + to: 750 + duration: 1000 + easing.type: Easing.OutCubic + } + + NumberAnimation { + target: logo + property: "rotation" + from: 750 + to: 710 + duration: 300 + easing.type: Easing.InOutQuad + } + + NumberAnimation { + target: logo + property: "rotation" + from: 710 + to: 725 + duration: 350 + easing.type: Easing.InOutQuad + } + + NumberAnimation { + target: logo + property: "rotation" + from: 725 + to: 720 + duration: 250 + easing.type: Easing.OutQuad + } + + ScriptAction { + script: logo.rotation = 0 + } + } + + SequentialAnimation { + NumberAnimation { + target: logo + property: "scale" + from: 0.0 + to: 1.08 + duration: 1000 + easing.type: Easing.OutCubic + } + + NumberAnimation { + target: logo + property: "scale" + from: 1.08 + to: 0.96 + duration: 200 + easing.type: Easing.InOutQuad + } + + NumberAnimation { + target: logo + property: "scale" + from: 0.96 + to: 1.0 + duration: 250 + easing.type: Easing.OutBack + easing.overshoot: 1.05 + } + } + + NumberAnimation { + target: logo + property: "opacity" + from: 0.0 + to: 1.0 + duration: 600 + easing.type: Easing.InOutQuad + } + + NumberAnimation { + target: root + property: "blurAmount" + from: 1.0 + to: 0.0 + duration: 900 + easing.type: Easing.OutCubic + } + + SequentialAnimation { + PauseAnimation { + duration: 1100 + } + + ParallelAnimation { + NumberAnimation { + target: root + property: "star1Opacity" + from: 0.0 + to: 1.0 + duration: 700 + easing.type: Easing.InOutQuad + } + + SequentialAnimation { + NumberAnimation { + target: root + property: "star1Scale" + from: 0.0 + to: 1.08 + duration: 500 + easing.type: Easing.OutQuad + } + + NumberAnimation { + target: root + property: "star1Scale" + from: 1.08 + to: 1.0 + duration: 400 + easing.type: Easing.InOutQuad + } + } + } + } + + SequentialAnimation { + PauseAnimation { + duration: 1250 + } + + ParallelAnimation { + NumberAnimation { + target: root + property: "star2Opacity" + from: 0.0 + to: 1.0 + duration: 700 + easing.type: Easing.InOutQuad + } + + SequentialAnimation { + NumberAnimation { + target: root + property: "star2Scale" + from: 0.0 + to: 1.08 + duration: 500 + easing.type: Easing.OutQuad + } + + NumberAnimation { + target: root + property: "star2Scale" + from: 1.08 + to: 1.0 + duration: 400 + easing.type: Easing.InOutQuad + } + } + } + } + + SequentialAnimation { + PauseAnimation { + duration: 1400 + } + + ParallelAnimation { + NumberAnimation { + target: root + property: "star3Opacity" + from: 0.0 + to: 1.0 + duration: 700 + easing.type: Easing.InOutQuad + } + + SequentialAnimation { + NumberAnimation { + target: root + property: "star3Scale" + from: 0.0 + to: 1.08 + duration: 500 + easing.type: Easing.OutQuad + } + + NumberAnimation { + target: root + property: "star3Scale" + from: 1.08 + to: 1.0 + duration: 400 + easing.type: Easing.InOutQuad + } + } + } + } + } + } + + SequentialAnimation { + running: true + loops: Animation.Infinite + + PauseAnimation { + duration: 2500 + } + + ParallelAnimation { + SequentialAnimation { + loops: Animation.Infinite + + NumberAnimation { + target: root.star1 + property: "y" + from: root.star1.y + to: root.star1.y - 5 + duration: 2500 + easing.type: Easing.InOutQuad + } + + NumberAnimation { + target: root.star1 + property: "y" + from: root.star1.y - 5 + to: root.star1.y + duration: 2500 + easing.type: Easing.InOutQuad + } + } + + SequentialAnimation { + loops: Animation.Infinite + + NumberAnimation { + target: root.star2 + property: "y" + from: root.star2.y + to: root.star2.y + 5 + duration: 3000 + easing.type: Easing.InOutQuad + } + + NumberAnimation { + target: root.star2 + property: "y" + from: root.star2.y + 5 + to: root.star2.y + duration: 3000 + easing.type: Easing.InOutQuad + } + } + + SequentialAnimation { + loops: Animation.Infinite + + NumberAnimation { + target: root.star3 + property: "y" + from: root.star3.y + to: root.star3.y - 5 + duration: 2800 + easing.type: Easing.InOutQuad + } + + NumberAnimation { + target: root.star3 + property: "y" + from: root.star3.y - 5 + to: root.star3.y + duration: 2800 + easing.type: Easing.InOutQuad + } + } + + SequentialAnimation { + loops: Animation.Infinite + + NumberAnimation { + target: root.star1 + property: "scale" + from: 1.0 + to: 1.08 + duration: 2500 + easing.type: Easing.InOutQuad + } + + NumberAnimation { + target: root.star1 + property: "scale" + from: 1.08 + to: 1.0 + duration: 2500 + easing.type: Easing.InOutQuad + } + } + + SequentialAnimation { + loops: Animation.Infinite + + NumberAnimation { + target: root.star2 + property: "scale" + from: 1.0 + to: 1.12 + duration: 3000 + easing.type: Easing.InOutQuad + } + + NumberAnimation { + target: root.star2 + property: "scale" + from: 1.12 + to: 1.0 + duration: 3000 + easing.type: Easing.InOutQuad + } + } + + SequentialAnimation { + loops: Animation.Infinite + + NumberAnimation { + target: root.star3 + property: "scale" + from: 1.0 + to: 1.08 + duration: 2800 + easing.type: Easing.InOutQuad + } + + NumberAnimation { + target: root.star3 + property: "scale" + from: 1.08 + to: 1.0 + duration: 2800 + easing.type: Easing.InOutQuad + } + } + } + } +} diff --git a/modules/nexus/common/InfoRow.qml b/modules/nexus/common/InfoRow.qml new file mode 100644 index 00000000..f06094f8 --- /dev/null +++ b/modules/nexus/common/InfoRow.qml @@ -0,0 +1,59 @@ +import QtQuick +import QtQuick.Layouts +import Caelestia.Config +import qs.components +import qs.services +import qs.modules.nexus.common + +ConnectedRect { + id: root + + property alias label: label.text + property string subtext + property alias value: value.text + + Layout.fillWidth: true + implicitHeight: rowLayout.implicitHeight + rowLayout.anchors.margins * 2 + + RowLayout { + id: rowLayout + + anchors.fill: parent + anchors.margins: Tokens.padding.medium + anchors.leftMargin: Tokens.padding.largeIncreased + anchors.rightMargin: Tokens.padding.largeIncreased + spacing: Tokens.spacing.medium + + ColumnLayout { + Layout.fillWidth: true + spacing: 0 + + StyledText { + id: label + + Layout.fillWidth: true + font: Tokens.font.body.small + elide: Text.ElideRight + } + + StyledText { + Layout.fillWidth: true + visible: root.subtext + text: root.subtext + color: Colours.palette.m3outline + font: Tokens.font.label.small + elide: Text.ElideRight + } + } + + StyledText { + id: value + + Layout.maximumWidth: root.width / 2 + horizontalAlignment: Text.AlignRight + color: Colours.palette.m3onSurfaceVariant + font: Tokens.font.body.small + elide: Text.ElideRight + } + } +} diff --git a/modules/nexus/pages/AboutPage.qml b/modules/nexus/pages/AboutPage.qml new file mode 100644 index 00000000..c3c7b5d5 --- /dev/null +++ b/modules/nexus/pages/AboutPage.qml @@ -0,0 +1,157 @@ +import QtQuick +import QtQuick.Layouts +import Quickshell.Io +import Caelestia +import Caelestia.Config +import qs.components +import qs.services +import qs.utils +import qs.modules.nexus.common + +PageBase { + id: root + + // Plugin support is not wired up yet; always 0 for now + readonly property int pluginCount: 0 + + property string quickshellVersion + property string cliVersion + + title: qsTr("About") + + ColumnLayout { + anchors.horizontalCenter: parent.horizontalCenter + anchors.top: parent.top + width: root.cappedWidth + spacing: Tokens.spacing.extraSmall / 2 + + // e.g. "Quickshell 0.3.0 (revision ...)" + Process { + running: true + command: ["quickshell", "--version"] + stdout: StdioCollector { + onStreamFinished: root.quickshellVersion = text.trim().split(" ")[1] ?? "" + } + } + + // Parsed from the caelestia CLI's package listing; the sh wrapper avoids a + // warning when the (optional) CLI isn't installed + Process { + running: true + command: ["sh", "-c", "caelestia --version 2>/dev/null"] + stdout: StdioCollector { + onStreamFinished: { + const m = text.match(/caelestia-cli\S*\s+(\d+(?:\.\d+)*)/); + root.cliVersion = m ? m[1] : ""; + } + } + } + + // Hero + ConnectedRect { + Layout.fillWidth: true + first: true + last: true + implicitHeight: hero.implicitHeight + Tokens.padding.extraLarge * 2 + + ColumnLayout { + id: hero + + anchors.centerIn: parent + width: parent.width - Tokens.padding.largeIncreased * 2 + spacing: Tokens.spacing.small + + AnimatedLogo { + Layout.alignment: Qt.AlignHCenter + Layout.preferredWidth: implicitWidth + Layout.preferredHeight: implicitHeight + } + + StyledText { + Layout.alignment: Qt.AlignHCenter + Layout.topMargin: Tokens.spacing.small + text: "Caelestia" + font: Tokens.font.headline.builders.large.width(110).build() + } + + StyledText { + Layout.alignment: Qt.AlignHCenter + text: CUtils.version ? `v${CUtils.version}` : "…" + color: Colours.palette.m3onSurfaceVariant + font: Tokens.font.body.medium + } + } + } + + // System + SectionHeader { + text: qsTr("System") + } + + InfoRow { + first: true + label: qsTr("Hostname") + value: SysInfo.hostname + } + + InfoRow { + label: qsTr("Device") + value: SysInfo.device + } + + InfoRow { + label: qsTr("Distro") + value: SysInfo.osPrettyName || SysInfo.osName + } + + InfoRow { + label: qsTr("Kernel") + value: SysInfo.kernel + } + + InfoRow { + last: true + label: qsTr("Firmware") + value: SysInfo.firmware + } + + // Software + SectionHeader { + text: qsTr("Software") + } + + InfoRow { + first: true + label: qsTr("Shell") + value: CUtils.version || "…" + } + + InfoRow { + label: qsTr("CLI") + value: root.cliVersion || "…" + } + + InfoRow { + label: qsTr("Quickshell") + value: root.quickshellVersion || "…" + } + + InfoRow { + last: true + label: qsTr("Qt") + value: CUtils.qtVersion || "…" + } + + // Plugins + SectionHeader { + text: qsTr("Plugins") + } + + InfoRow { + first: true + last: true + label: qsTr("Loaded plugins") + value: root.pluginCount.toString() + } + } +} diff --git a/plugin/src/Caelestia/CMakeLists.txt b/plugin/src/Caelestia/CMakeLists.txt index 0ae917bd..ea2ee748 100644 --- a/plugin/src/Caelestia/CMakeLists.txt +++ b/plugin/src/Caelestia/CMakeLists.txt @@ -15,6 +15,9 @@ qml_module(caelestia PkgConfig::Qalculate ) +# Bake the shell version in so CUtils can expose it without spawning a process +target_compile_definitions(caelestia PRIVATE CAELESTIA_VERSION="${VERSION}") + add_subdirectory(Components) add_subdirectory(Config) add_subdirectory(Internal) diff --git a/plugin/src/Caelestia/cutils.cpp b/plugin/src/Caelestia/cutils.cpp index 9c2426c2..b6080f6e 100644 --- a/plugin/src/Caelestia/cutils.cpp +++ b/plugin/src/Caelestia/cutils.cpp @@ -142,4 +142,16 @@ qreal CUtils::clamp(qreal value, qreal min, qreal max) { return qBound(min, value, max); } +#ifndef CAELESTIA_VERSION +#define CAELESTIA_VERSION "" +#endif + +QString CUtils::version() const { + return QStringLiteral(CAELESTIA_VERSION); +} + +QString CUtils::qtVersion() const { + return QStringLiteral(QT_VERSION_STR); +} + } // namespace caelestia diff --git a/plugin/src/Caelestia/cutils.hpp b/plugin/src/Caelestia/cutils.hpp index 29ae3ca5..7fb4163a 100644 --- a/plugin/src/Caelestia/cutils.hpp +++ b/plugin/src/Caelestia/cutils.hpp @@ -11,6 +11,9 @@ class CUtils : public QObject { QML_ELEMENT QML_SINGLETON + Q_PROPERTY(QString version READ version CONSTANT) + Q_PROPERTY(QString qtVersion READ qtVersion CONSTANT) + public: // clang-format off Q_INVOKABLE void saveItem(QQuickItem* target, const QUrl& path); @@ -26,6 +29,9 @@ public: Q_INVOKABLE static QString toLocalFile(const QUrl& url); Q_INVOKABLE static qreal clamp(qreal value, qreal min, qreal max); + + [[nodiscard]] QString version() const; + [[nodiscard]] QString qtVersion() const; }; } // namespace caelestia diff --git a/utils/SysInfo.qml b/utils/SysInfo.qml index c715b8d2..c18ea560 100644 --- a/utils/SysInfo.qml +++ b/utils/SysInfo.qml @@ -21,6 +21,28 @@ Singleton { readonly property string wm: Quickshell.env("XDG_CURRENT_DESKTOP") || Quickshell.env("XDG_SESSION_DESKTOP") readonly property string shell: Quickshell.env("SHELL").split("/").pop() + property string kernel + property string hostname + property string firmware + + // DMI vendor/model, combined into a single human-readable device name + property string boardVendor + property string boardName + readonly property string device: { + if (!boardName) + return boardVendor; + if (!boardVendor || boardName.toLowerCase().startsWith(boardVendor.toLowerCase())) + return boardName; + return `${boardVendor} ${boardName}`; + } + + // Strips the placeholder strings OEMs commonly leave in DMI fields + function sanitiseDmi(s: string): string { + const t = s.trim(); + const junk = ["to be filled by o.e.m.", "system product name", "system manufacturer", "system version", "default string", "o.e.m.", "not specified", "not applicable", "unknown", "none", ""]; + return junk.includes(t.toLowerCase()) ? "" : t; + } + FileView { id: osRelease @@ -57,6 +79,34 @@ Singleton { target: GlobalConfig.general } + FileView { + path: "/proc/sys/kernel/osrelease" + onLoaded: root.kernel = text().trim() + } + + FileView { + path: "/proc/sys/kernel/hostname" + onLoaded: root.hostname = text().trim() + } + + FileView { + path: "/sys/class/dmi/id/sys_vendor" + printErrors: false + onLoaded: root.boardVendor = root.sanitiseDmi(text()) + } + + FileView { + path: "/sys/class/dmi/id/product_name" + printErrors: false + onLoaded: root.boardName = root.sanitiseDmi(text()) + } + + FileView { + path: "/sys/class/dmi/id/bios_version" + printErrors: false + onLoaded: root.firmware = root.sanitiseDmi(text()) + } + Timer { running: true repeat: true