nosignal-shell/modules/controlcenter/appearance/AppearancePane.qml

2104 lines
98 KiB
QML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

pragma ComponentBehavior: Bound
import ".."
import "../../launcher/services"
import qs.components
import qs.components.controls
import qs.components.effects
import qs.components.containers
import qs.components.images
import qs.services
import qs.config
import qs.utils
import Caelestia.Models
import Quickshell
import Quickshell.Widgets
import QtQuick
import QtQuick.Layouts
RowLayout {
id: root
required property Session session
// Appearance settings
property real animDurationsScale: Config.appearance.anim.durations.scale ?? 1
property string fontFamilyMaterial: Config.appearance.font.family.material ?? "Material Symbols Rounded"
property string fontFamilyMono: Config.appearance.font.family.mono ?? "CaskaydiaCove NF"
property string fontFamilySans: Config.appearance.font.family.sans ?? "Rubik"
property real fontSizeScale: Config.appearance.font.size.scale ?? 1
property real paddingScale: Config.appearance.padding.scale ?? 1
property real roundingScale: Config.appearance.rounding.scale ?? 1
property real spacingScale: Config.appearance.spacing.scale ?? 1
property bool transparencyEnabled: Config.appearance.transparency.enabled ?? false
property real transparencyBase: Config.appearance.transparency.base ?? 0.85
property real transparencyLayers: Config.appearance.transparency.layers ?? 0.4
property real borderRounding: Config.border.rounding ?? 1
property real borderThickness: Config.border.thickness ?? 1
// Background settings
property bool desktopClockEnabled: Config.background.desktopClock.enabled ?? false
property bool backgroundEnabled: Config.background.enabled ?? true
property bool visualiserEnabled: Config.background.visualiser.enabled ?? false
property bool visualiserAutoHide: Config.background.visualiser.autoHide ?? true
property real visualiserRounding: Config.background.visualiser.rounding ?? 1
property real visualiserSpacing: Config.background.visualiser.spacing ?? 1
anchors.fill: parent
spacing: 0
function saveConfig() {
// Update animations
Config.appearance.anim.durations.scale = root.animDurationsScale;
// Update fonts
Config.appearance.font.family.material = root.fontFamilyMaterial;
Config.appearance.font.family.mono = root.fontFamilyMono;
Config.appearance.font.family.sans = root.fontFamilySans;
Config.appearance.font.size.scale = root.fontSizeScale;
// Update scales
Config.appearance.padding.scale = root.paddingScale;
Config.appearance.rounding.scale = root.roundingScale;
Config.appearance.spacing.scale = root.spacingScale;
// Update transparency
Config.appearance.transparency.enabled = root.transparencyEnabled;
Config.appearance.transparency.base = root.transparencyBase;
Config.appearance.transparency.layers = root.transparencyLayers;
// Update desktop clock
Config.background.desktopClock.enabled = root.desktopClockEnabled;
// Update background enabled
Config.background.enabled = root.backgroundEnabled;
// Update visualiser
Config.background.visualiser.enabled = root.visualiserEnabled;
Config.background.visualiser.autoHide = root.visualiserAutoHide;
Config.background.visualiser.rounding = root.visualiserRounding;
Config.background.visualiser.spacing = root.visualiserSpacing;
// Update border
Config.border.rounding = root.borderRounding;
Config.border.thickness = root.borderThickness;
}
Item {
Layout.preferredWidth: Math.floor(parent.width * 0.4)
Layout.minimumWidth: 420
Layout.fillHeight: true
ClippingRectangle {
id: leftAppearanceClippingRect
anchors.fill: parent
anchors.margins: Appearance.padding.normal
anchors.leftMargin: 0
anchors.rightMargin: Appearance.padding.normal / 2
radius: leftAppearanceBorder.innerRadius
color: "transparent"
Loader {
id: leftAppearanceLoader
anchors.fill: parent
anchors.margins: Appearance.padding.large + Appearance.padding.normal
anchors.leftMargin: Appearance.padding.large
anchors.rightMargin: Appearance.padding.large + Appearance.padding.normal / 2
asynchronous: true
sourceComponent: appearanceLeftContentComponent
property var rootPane: root
}
}
InnerBorder {
id: leftAppearanceBorder
leftThickness: 0
rightThickness: Appearance.padding.normal / 2
}
Component {
id: appearanceLeftContentComponent
StyledFlickable {
id: sidebarFlickable
readonly property var rootPane: leftAppearanceLoader.rootPane
flickableDirection: Flickable.VerticalFlick
contentHeight: sidebarLayout.height
function collapseAllSections(exceptSection) {
if (exceptSection !== themeModeSection)
themeModeSection.expanded = false;
if (exceptSection !== colorVariantSection)
colorVariantSection.expanded = false;
if (exceptSection !== colorSchemeSection)
colorSchemeSection.expanded = false;
if (exceptSection !== animationsSection)
animationsSection.expanded = false;
if (exceptSection !== fontsSection)
fontsSection.expanded = false;
if (exceptSection !== scalesSection)
scalesSection.expanded = false;
if (exceptSection !== transparencySection)
transparencySection.expanded = false;
if (exceptSection !== borderSection)
borderSection.expanded = false;
if (exceptSection !== backgroundSection)
backgroundSection.expanded = false;
}
StyledScrollBar.vertical: StyledScrollBar {
flickable: sidebarFlickable
}
ColumnLayout {
id: sidebarLayout
anchors.left: parent.left
anchors.right: parent.right
spacing: Appearance.spacing.small
RowLayout {
spacing: Appearance.spacing.smaller
StyledText {
text: qsTr("Settings")
font.pointSize: Appearance.font.size.large
font.weight: 500
}
Item {
Layout.fillWidth: true
}
}
CollapsibleSection {
id: themeModeSection
title: qsTr("Theme mode")
description: qsTr("Light or dark theme")
onToggleRequested: {
sidebarFlickable.collapseAllSections(themeModeSection);
}
SwitchRow {
label: qsTr("Dark mode")
checked: !Colours.currentLight
onToggled: checked => {
Colours.setMode(checked ? "dark" : "light");
}
}
}
CollapsibleSection {
id: colorVariantSection
title: qsTr("Color variant")
description: qsTr("Material theme variant")
onToggleRequested: {
sidebarFlickable.collapseAllSections(colorVariantSection);
}
StyledListView {
Layout.fillWidth: true
implicitHeight: colorVariantSection.expanded ? Math.min(400, M3Variants.list.length * 60) : 0
model: M3Variants.list
spacing: Appearance.spacing.small / 2
clip: true
StyledScrollBar.vertical: StyledScrollBar {
flickable: parent
}
delegate: StyledRect {
required property var modelData
width: parent ? parent.width : 0
color: Qt.alpha(Colours.tPalette.m3surfaceContainer, modelData.variant === Schemes.currentVariant ? Colours.tPalette.m3surfaceContainer.a : 0)
radius: Appearance.rounding.normal
border.width: modelData.variant === Schemes.currentVariant ? 1 : 0
border.color: Colours.palette.m3primary
StateLayer {
function onClicked(): void {
const variant = modelData.variant;
// Optimistic update - set immediately
Schemes.currentVariant = variant;
// Execute the command
Quickshell.execDetached(["caelestia", "scheme", "set", "-v", variant]);
// Reload after a delay to confirm
Qt.callLater(() => {
reloadTimer.restart();
});
}
}
Timer {
id: reloadTimer
interval: 300
onTriggered: {
Schemes.reload();
}
}
RowLayout {
id: variantRow
anchors.left: parent.left
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
anchors.margins: Appearance.padding.normal
spacing: Appearance.spacing.normal
MaterialIcon {
text: modelData.icon
font.pointSize: Appearance.font.size.large
fill: modelData.variant === Schemes.currentVariant ? 1 : 0
}
StyledText {
Layout.fillWidth: true
text: modelData.name
font.weight: modelData.variant === Schemes.currentVariant ? 500 : 400
}
MaterialIcon {
visible: modelData.variant === Schemes.currentVariant
text: "check"
color: Colours.palette.m3primary
font.pointSize: Appearance.font.size.large
}
}
implicitHeight: variantRow.implicitHeight + Appearance.padding.normal * 2
}
}
}
CollapsibleSection {
id: colorSchemeSection
title: qsTr("Color scheme")
description: qsTr("Available color schemes")
onToggleRequested: {
sidebarFlickable.collapseAllSections(colorSchemeSection);
}
StyledListView {
Layout.fillWidth: true
implicitHeight: colorSchemeSection.expanded ? Math.min(400, Schemes.list.length * 80) : 0
model: Schemes.list
spacing: Appearance.spacing.small / 2
clip: true
StyledScrollBar.vertical: StyledScrollBar {
flickable: parent
}
delegate: StyledRect {
required property var modelData
width: parent ? parent.width : 0
readonly property string schemeKey: `${modelData.name} ${modelData.flavour}`
readonly property bool isCurrent: schemeKey === Schemes.currentScheme
color: Qt.alpha(Colours.tPalette.m3surfaceContainer, isCurrent ? Colours.tPalette.m3surfaceContainer.a : 0)
radius: Appearance.rounding.normal
border.width: isCurrent ? 1 : 0
border.color: Colours.palette.m3primary
StateLayer {
function onClicked(): void {
const name = modelData.name;
const flavour = modelData.flavour;
const schemeKey = `${name} ${flavour}`;
// Optimistic update - set immediately
Schemes.currentScheme = schemeKey;
// Execute the command
Quickshell.execDetached(["caelestia", "scheme", "set", "-n", name, "-f", flavour]);
// Reload after a delay to confirm
Qt.callLater(() => {
reloadTimer.restart();
});
}
}
Timer {
id: reloadTimer
interval: 300
onTriggered: {
Schemes.reload();
}
}
RowLayout {
id: schemeRow
anchors.fill: parent
anchors.margins: Appearance.padding.normal
spacing: Appearance.spacing.normal
StyledRect {
id: preview
Layout.alignment: Qt.AlignVCenter
border.width: 1
border.color: Qt.alpha(`#${modelData.colours?.outline}`, 0.5)
color: `#${modelData.colours?.surface}`
radius: Appearance.rounding.full
implicitWidth: iconPlaceholder.implicitWidth
implicitHeight: iconPlaceholder.implicitWidth
MaterialIcon {
id: iconPlaceholder
visible: false
text: "circle"
font.pointSize: Appearance.font.size.large
}
Item {
anchors.top: parent.top
anchors.bottom: parent.bottom
anchors.right: parent.right
implicitWidth: parent.implicitWidth / 2
clip: true
StyledRect {
anchors.top: parent.top
anchors.bottom: parent.bottom
anchors.right: parent.right
implicitWidth: preview.implicitWidth
color: `#${modelData.colours?.primary}`
radius: Appearance.rounding.full
}
}
}
Column {
Layout.fillWidth: true
spacing: 0
StyledText {
text: modelData.flavour ?? ""
font.pointSize: Appearance.font.size.normal
}
StyledText {
text: modelData.name ?? ""
font.pointSize: Appearance.font.size.small
color: Colours.palette.m3outline
elide: Text.ElideRight
anchors.left: parent.left
anchors.right: parent.right
}
}
Loader {
active: isCurrent
asynchronous: true
sourceComponent: MaterialIcon {
text: "check"
color: Colours.palette.m3onSurfaceVariant
font.pointSize: Appearance.font.size.large
}
}
}
implicitHeight: schemeRow.implicitHeight + Appearance.padding.normal * 2
}
}
}
CollapsibleSection {
id: animationsSection
title: qsTr("Animations")
onToggleRequested: {
sidebarFlickable.collapseAllSections(animationsSection);
}
SectionContainer {
contentSpacing: Appearance.spacing.normal
ColumnLayout {
Layout.fillWidth: true
spacing: Appearance.spacing.small
RowLayout {
Layout.fillWidth: true
spacing: Appearance.spacing.normal
StyledText {
text: qsTr("Animation duration scale")
font.pointSize: Appearance.font.size.normal
}
Item {
Layout.fillWidth: true
}
StyledRect {
Layout.preferredWidth: 70
implicitHeight: animDurationsInput.implicitHeight + Appearance.padding.small * 2
color: animDurationsInputHover.containsMouse || animDurationsInput.activeFocus
? Colours.layer(Colours.palette.m3surfaceContainer, 3)
: Colours.layer(Colours.palette.m3surfaceContainer, 2)
radius: Appearance.rounding.small
border.width: 1
border.color: animDurationsInput.activeFocus
? Colours.palette.m3primary
: Qt.alpha(Colours.palette.m3outline, 0.3)
Behavior on color { CAnim {} }
Behavior on border.color { CAnim {} }
MouseArea {
id: animDurationsInputHover
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.IBeamCursor
acceptedButtons: Qt.NoButton
}
StyledTextField {
id: animDurationsInput
anchors.centerIn: parent
width: parent.width - Appearance.padding.normal
horizontalAlignment: TextInput.AlignHCenter
validator: DoubleValidator { bottom: 0.1; top: 5.0 }
Component.onCompleted: {
text = (rootPane.animDurationsScale).toFixed(1);
}
onTextChanged: {
if (activeFocus) {
const val = parseFloat(text);
if (!isNaN(val) && val >= 0.1 && val <= 5.0) {
rootPane.animDurationsScale = val;
rootPane.saveConfig();
}
}
}
onEditingFinished: {
const val = parseFloat(text);
if (isNaN(val) || val < 0.1 || val > 5.0) {
text = (rootPane.animDurationsScale).toFixed(1);
}
}
}
}
StyledText {
text: "×"
color: Colours.palette.m3outline
font.pointSize: Appearance.font.size.normal
}
}
StyledSlider {
id: animDurationsSlider
Layout.fillWidth: true
implicitHeight: Appearance.padding.normal * 3
from: 0.1
to: 5.0
value: rootPane.animDurationsScale
onMoved: {
rootPane.animDurationsScale = animDurationsSlider.value;
if (!animDurationsInput.activeFocus) {
animDurationsInput.text = (animDurationsSlider.value).toFixed(1);
}
rootPane.saveConfig();
}
}
}
}
}
CollapsibleSection {
id: fontsSection
title: qsTr("Fonts")
onToggleRequested: {
sidebarFlickable.collapseAllSections(fontsSection);
}
StyledText {
Layout.topMargin: Appearance.spacing.normal
text: qsTr("Material font family")
font.pointSize: Appearance.font.size.larger
font.weight: 500
}
StyledListView {
Layout.fillWidth: true
implicitHeight: fontsSection.expanded ? Math.min(300, Qt.fontFamilies().length * 50) : 0
model: Qt.fontFamilies()
spacing: Appearance.spacing.small / 2
clip: true
StyledScrollBar.vertical: StyledScrollBar {
flickable: parent
}
delegate: StyledRect {
required property string modelData
width: parent ? parent.width : 0
readonly property bool isCurrent: modelData === rootPane.fontFamilyMaterial
color: Qt.alpha(Colours.tPalette.m3surfaceContainer, isCurrent ? Colours.tPalette.m3surfaceContainer.a : 0)
radius: Appearance.rounding.normal
border.width: isCurrent ? 1 : 0
border.color: Colours.palette.m3primary
StateLayer {
function onClicked(): void {
rootPane.fontFamilyMaterial = modelData;
rootPane.saveConfig();
}
}
RowLayout {
id: fontFamilyMaterialRow
anchors.left: parent.left
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
anchors.margins: Appearance.padding.normal
spacing: Appearance.spacing.normal
StyledText {
text: modelData
font.pointSize: Appearance.font.size.normal
}
Item {
Layout.fillWidth: true
}
Loader {
active: isCurrent
asynchronous: true
sourceComponent: MaterialIcon {
text: "check"
color: Colours.palette.m3onSurfaceVariant
font.pointSize: Appearance.font.size.large
}
}
}
implicitHeight: fontFamilyMaterialRow.implicitHeight + Appearance.padding.normal * 2
}
}
StyledText {
Layout.topMargin: Appearance.spacing.normal
text: qsTr("Monospace font family")
font.pointSize: Appearance.font.size.larger
font.weight: 500
}
StyledListView {
Layout.fillWidth: true
implicitHeight: fontsSection.expanded ? Math.min(300, Qt.fontFamilies().length * 50) : 0
model: Qt.fontFamilies()
spacing: Appearance.spacing.small / 2
clip: true
StyledScrollBar.vertical: StyledScrollBar {
flickable: parent
}
delegate: StyledRect {
required property string modelData
width: parent ? parent.width : 0
readonly property bool isCurrent: modelData === rootPane.fontFamilyMono
color: Qt.alpha(Colours.tPalette.m3surfaceContainer, isCurrent ? Colours.tPalette.m3surfaceContainer.a : 0)
radius: Appearance.rounding.normal
border.width: isCurrent ? 1 : 0
border.color: Colours.palette.m3primary
StateLayer {
function onClicked(): void {
rootPane.fontFamilyMono = modelData;
rootPane.saveConfig();
}
}
RowLayout {
id: fontFamilyMonoRow
anchors.left: parent.left
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
anchors.margins: Appearance.padding.normal
spacing: Appearance.spacing.normal
StyledText {
text: modelData
font.pointSize: Appearance.font.size.normal
}
Item {
Layout.fillWidth: true
}
Loader {
active: isCurrent
asynchronous: true
sourceComponent: MaterialIcon {
text: "check"
color: Colours.palette.m3onSurfaceVariant
font.pointSize: Appearance.font.size.large
}
}
}
implicitHeight: fontFamilyMonoRow.implicitHeight + Appearance.padding.normal * 2
}
}
StyledText {
Layout.topMargin: Appearance.spacing.normal
text: qsTr("Sans-serif font family")
font.pointSize: Appearance.font.size.larger
font.weight: 500
}
StyledListView {
Layout.fillWidth: true
implicitHeight: fontsSection.expanded ? Math.min(300, Qt.fontFamilies().length * 50) : 0
model: Qt.fontFamilies()
spacing: Appearance.spacing.small / 2
clip: true
StyledScrollBar.vertical: StyledScrollBar {
flickable: parent
}
delegate: StyledRect {
required property string modelData
width: parent ? parent.width : 0
readonly property bool isCurrent: modelData === rootPane.fontFamilySans
color: Qt.alpha(Colours.tPalette.m3surfaceContainer, isCurrent ? Colours.tPalette.m3surfaceContainer.a : 0)
radius: Appearance.rounding.normal
border.width: isCurrent ? 1 : 0
border.color: Colours.palette.m3primary
StateLayer {
function onClicked(): void {
rootPane.fontFamilySans = modelData;
rootPane.saveConfig();
}
}
RowLayout {
id: fontFamilySansRow
anchors.left: parent.left
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
anchors.margins: Appearance.padding.normal
spacing: Appearance.spacing.normal
StyledText {
text: modelData
font.pointSize: Appearance.font.size.normal
}
Item {
Layout.fillWidth: true
}
Loader {
active: isCurrent
asynchronous: true
sourceComponent: MaterialIcon {
text: "check"
color: Colours.palette.m3onSurfaceVariant
font.pointSize: Appearance.font.size.large
}
}
}
implicitHeight: fontFamilySansRow.implicitHeight + Appearance.padding.normal * 2
}
}
SectionContainer {
contentSpacing: Appearance.spacing.normal
ColumnLayout {
Layout.fillWidth: true
spacing: Appearance.spacing.small
RowLayout {
Layout.fillWidth: true
spacing: Appearance.spacing.normal
StyledText {
text: qsTr("Font size scale")
font.pointSize: Appearance.font.size.normal
}
Item {
Layout.fillWidth: true
}
StyledRect {
Layout.preferredWidth: 70
implicitHeight: fontSizeInput.implicitHeight + Appearance.padding.small * 2
color: fontSizeInputHover.containsMouse || fontSizeInput.activeFocus
? Colours.layer(Colours.palette.m3surfaceContainer, 3)
: Colours.layer(Colours.palette.m3surfaceContainer, 2)
radius: Appearance.rounding.small
border.width: 1
border.color: fontSizeInput.activeFocus
? Colours.palette.m3primary
: Qt.alpha(Colours.palette.m3outline, 0.3)
Behavior on color { CAnim {} }
Behavior on border.color { CAnim {} }
MouseArea {
id: fontSizeInputHover
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.IBeamCursor
acceptedButtons: Qt.NoButton
}
StyledTextField {
id: fontSizeInput
anchors.centerIn: parent
width: parent.width - Appearance.padding.normal
horizontalAlignment: TextInput.AlignHCenter
validator: DoubleValidator { bottom: 0.1; top: 5.0 }
Component.onCompleted: {
text = (rootPane.fontSizeScale).toFixed(1);
}
onTextChanged: {
if (activeFocus) {
const val = parseFloat(text);
if (!isNaN(val) && val >= 0.1 && val <= 5.0) {
rootPane.fontSizeScale = val;
rootPane.saveConfig();
}
}
}
onEditingFinished: {
const val = parseFloat(text);
if (isNaN(val) || val < 0.1 || val > 5.0) {
text = (rootPane.fontSizeScale).toFixed(1);
}
}
}
}
StyledText {
text: "×"
color: Colours.palette.m3outline
font.pointSize: Appearance.font.size.normal
}
}
StyledSlider {
id: fontSizeSlider
Layout.fillWidth: true
implicitHeight: Appearance.padding.normal * 3
from: 0.1
to: 5.0
value: rootPane.fontSizeScale
onMoved: {
rootPane.fontSizeScale = fontSizeSlider.value;
if (!fontSizeInput.activeFocus) {
fontSizeInput.text = (fontSizeSlider.value).toFixed(1);
}
rootPane.saveConfig();
}
}
}
}
}
CollapsibleSection {
id: scalesSection
title: qsTr("Scales")
onToggleRequested: {
sidebarFlickable.collapseAllSections(scalesSection);
}
SectionContainer {
contentSpacing: Appearance.spacing.normal
ColumnLayout {
Layout.fillWidth: true
spacing: Appearance.spacing.small
RowLayout {
Layout.fillWidth: true
spacing: Appearance.spacing.normal
StyledText {
text: qsTr("Padding scale")
font.pointSize: Appearance.font.size.normal
}
Item {
Layout.fillWidth: true
}
StyledRect {
Layout.preferredWidth: 70
implicitHeight: paddingInput.implicitHeight + Appearance.padding.small * 2
color: paddingInputHover.containsMouse || paddingInput.activeFocus
? Colours.layer(Colours.palette.m3surfaceContainer, 3)
: Colours.layer(Colours.palette.m3surfaceContainer, 2)
radius: Appearance.rounding.small
border.width: 1
border.color: paddingInput.activeFocus
? Colours.palette.m3primary
: Qt.alpha(Colours.palette.m3outline, 0.3)
Behavior on color { CAnim {} }
Behavior on border.color { CAnim {} }
MouseArea {
id: paddingInputHover
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.IBeamCursor
acceptedButtons: Qt.NoButton
}
StyledTextField {
id: paddingInput
anchors.centerIn: parent
width: parent.width - Appearance.padding.normal
horizontalAlignment: TextInput.AlignHCenter
validator: DoubleValidator { bottom: 0.1; top: 5.0 }
Component.onCompleted: {
text = (rootPane.paddingScale).toFixed(1);
}
onTextChanged: {
if (activeFocus) {
const val = parseFloat(text);
if (!isNaN(val) && val >= 0.1 && val <= 5.0) {
rootPane.paddingScale = val;
rootPane.saveConfig();
}
}
}
onEditingFinished: {
const val = parseFloat(text);
if (isNaN(val) || val < 0.1 || val > 5.0) {
text = (rootPane.paddingScale).toFixed(1);
}
}
}
}
StyledText {
text: "×"
color: Colours.palette.m3outline
font.pointSize: Appearance.font.size.normal
}
}
StyledSlider {
id: paddingSlider
Layout.fillWidth: true
implicitHeight: Appearance.padding.normal * 3
from: 0.1
to: 5.0
value: rootPane.paddingScale
onMoved: {
rootPane.paddingScale = paddingSlider.value;
if (!paddingInput.activeFocus) {
paddingInput.text = (paddingSlider.value).toFixed(1);
}
rootPane.saveConfig();
}
}
}
}
SectionContainer {
contentSpacing: Appearance.spacing.normal
ColumnLayout {
Layout.fillWidth: true
spacing: Appearance.spacing.small
RowLayout {
Layout.fillWidth: true
spacing: Appearance.spacing.normal
StyledText {
text: qsTr("Rounding scale")
font.pointSize: Appearance.font.size.normal
}
Item {
Layout.fillWidth: true
}
StyledRect {
Layout.preferredWidth: 70
implicitHeight: roundingInput.implicitHeight + Appearance.padding.small * 2
color: roundingInputHover.containsMouse || roundingInput.activeFocus
? Colours.layer(Colours.palette.m3surfaceContainer, 3)
: Colours.layer(Colours.palette.m3surfaceContainer, 2)
radius: Appearance.rounding.small
border.width: 1
border.color: roundingInput.activeFocus
? Colours.palette.m3primary
: Qt.alpha(Colours.palette.m3outline, 0.3)
Behavior on color { CAnim {} }
Behavior on border.color { CAnim {} }
MouseArea {
id: roundingInputHover
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.IBeamCursor
acceptedButtons: Qt.NoButton
}
StyledTextField {
id: roundingInput
anchors.centerIn: parent
width: parent.width - Appearance.padding.normal
horizontalAlignment: TextInput.AlignHCenter
validator: DoubleValidator { bottom: 0.1; top: 5.0 }
Component.onCompleted: {
text = (rootPane.roundingScale).toFixed(1);
}
onTextChanged: {
if (activeFocus) {
const val = parseFloat(text);
if (!isNaN(val) && val >= 0.1 && val <= 5.0) {
rootPane.roundingScale = val;
rootPane.saveConfig();
}
}
}
onEditingFinished: {
const val = parseFloat(text);
if (isNaN(val) || val < 0.1 || val > 5.0) {
text = (rootPane.roundingScale).toFixed(1);
}
}
}
}
StyledText {
text: "×"
color: Colours.palette.m3outline
font.pointSize: Appearance.font.size.normal
}
}
StyledSlider {
id: roundingSlider
Layout.fillWidth: true
implicitHeight: Appearance.padding.normal * 3
from: 0.1
to: 5.0
value: rootPane.roundingScale
onMoved: {
rootPane.roundingScale = roundingSlider.value;
if (!roundingInput.activeFocus) {
roundingInput.text = (roundingSlider.value).toFixed(1);
}
rootPane.saveConfig();
}
}
}
}
SectionContainer {
contentSpacing: Appearance.spacing.normal
ColumnLayout {
Layout.fillWidth: true
spacing: Appearance.spacing.small
RowLayout {
Layout.fillWidth: true
spacing: Appearance.spacing.normal
StyledText {
text: qsTr("Spacing scale")
font.pointSize: Appearance.font.size.normal
}
Item {
Layout.fillWidth: true
}
StyledRect {
Layout.preferredWidth: 70
implicitHeight: spacingInput.implicitHeight + Appearance.padding.small * 2
color: spacingInputHover.containsMouse || spacingInput.activeFocus
? Colours.layer(Colours.palette.m3surfaceContainer, 3)
: Colours.layer(Colours.palette.m3surfaceContainer, 2)
radius: Appearance.rounding.small
border.width: 1
border.color: spacingInput.activeFocus
? Colours.palette.m3primary
: Qt.alpha(Colours.palette.m3outline, 0.3)
Behavior on color { CAnim {} }
Behavior on border.color { CAnim {} }
MouseArea {
id: spacingInputHover
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.IBeamCursor
acceptedButtons: Qt.NoButton
}
StyledTextField {
id: spacingInput
anchors.centerIn: parent
width: parent.width - Appearance.padding.normal
horizontalAlignment: TextInput.AlignHCenter
validator: DoubleValidator { bottom: 0.1; top: 5.0 }
Component.onCompleted: {
text = (rootPane.spacingScale).toFixed(1);
}
onTextChanged: {
if (activeFocus) {
const val = parseFloat(text);
if (!isNaN(val) && val >= 0.1 && val <= 5.0) {
rootPane.spacingScale = val;
rootPane.saveConfig();
}
}
}
onEditingFinished: {
const val = parseFloat(text);
if (isNaN(val) || val < 0.1 || val > 5.0) {
text = (rootPane.spacingScale).toFixed(1);
}
}
}
}
StyledText {
text: "×"
color: Colours.palette.m3outline
font.pointSize: Appearance.font.size.normal
}
}
StyledSlider {
id: spacingSlider
Layout.fillWidth: true
implicitHeight: Appearance.padding.normal * 3
from: 0.1
to: 5.0
value: rootPane.spacingScale
onMoved: {
rootPane.spacingScale = spacingSlider.value;
if (!spacingInput.activeFocus) {
spacingInput.text = (spacingSlider.value).toFixed(1);
}
rootPane.saveConfig();
}
}
}
}
}
CollapsibleSection {
id: transparencySection
title: qsTr("Transparency")
onToggleRequested: {
sidebarFlickable.collapseAllSections(transparencySection);
}
SwitchRow {
label: qsTr("Transparency enabled")
checked: rootPane.transparencyEnabled
onToggled: checked => {
rootPane.transparencyEnabled = checked;
rootPane.saveConfig();
}
}
SectionContainer {
contentSpacing: Appearance.spacing.normal
ColumnLayout {
Layout.fillWidth: true
spacing: Appearance.spacing.small
RowLayout {
Layout.fillWidth: true
spacing: Appearance.spacing.normal
StyledText {
text: qsTr("Transparency base")
font.pointSize: Appearance.font.size.normal
}
Item {
Layout.fillWidth: true
}
StyledRect {
Layout.preferredWidth: 70
implicitHeight: transparencyBaseInput.implicitHeight + Appearance.padding.small * 2
color: transparencyBaseInputHover.containsMouse || transparencyBaseInput.activeFocus
? Colours.layer(Colours.palette.m3surfaceContainer, 3)
: Colours.layer(Colours.palette.m3surfaceContainer, 2)
radius: Appearance.rounding.small
border.width: 1
border.color: transparencyBaseInput.activeFocus
? Colours.palette.m3primary
: Qt.alpha(Colours.palette.m3outline, 0.3)
Behavior on color { CAnim {} }
Behavior on border.color { CAnim {} }
MouseArea {
id: transparencyBaseInputHover
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.IBeamCursor
acceptedButtons: Qt.NoButton
}
StyledTextField {
id: transparencyBaseInput
anchors.centerIn: parent
width: parent.width - Appearance.padding.normal
horizontalAlignment: TextInput.AlignHCenter
validator: IntValidator { bottom: 0; top: 100 }
Component.onCompleted: {
text = Math.round(rootPane.transparencyBase * 100).toString();
}
onTextChanged: {
if (activeFocus) {
const val = parseInt(text);
if (!isNaN(val) && val >= 0 && val <= 100) {
rootPane.transparencyBase = val / 100;
rootPane.saveConfig();
}
}
}
onEditingFinished: {
const val = parseInt(text);
if (isNaN(val) || val < 0 || val > 100) {
text = Math.round(rootPane.transparencyBase * 100).toString();
}
}
}
}
StyledText {
text: "%"
color: Colours.palette.m3outline
font.pointSize: Appearance.font.size.normal
}
}
StyledSlider {
id: baseSlider
Layout.fillWidth: true
implicitHeight: Appearance.padding.normal * 3
from: 0
to: 100
value: rootPane.transparencyBase * 100
onMoved: {
rootPane.transparencyBase = baseSlider.value / 100;
if (!transparencyBaseInput.activeFocus) {
transparencyBaseInput.text = Math.round(baseSlider.value).toString();
}
rootPane.saveConfig();
}
}
}
}
SectionContainer {
contentSpacing: Appearance.spacing.normal
ColumnLayout {
Layout.fillWidth: true
spacing: Appearance.spacing.small
RowLayout {
Layout.fillWidth: true
spacing: Appearance.spacing.normal
StyledText {
text: qsTr("Transparency layers")
font.pointSize: Appearance.font.size.normal
}
Item {
Layout.fillWidth: true
}
StyledRect {
Layout.preferredWidth: 70
implicitHeight: transparencyLayersInput.implicitHeight + Appearance.padding.small * 2
color: transparencyLayersInputHover.containsMouse || transparencyLayersInput.activeFocus
? Colours.layer(Colours.palette.m3surfaceContainer, 3)
: Colours.layer(Colours.palette.m3surfaceContainer, 2)
radius: Appearance.rounding.small
border.width: 1
border.color: transparencyLayersInput.activeFocus
? Colours.palette.m3primary
: Qt.alpha(Colours.palette.m3outline, 0.3)
Behavior on color { CAnim {} }
Behavior on border.color { CAnim {} }
MouseArea {
id: transparencyLayersInputHover
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.IBeamCursor
acceptedButtons: Qt.NoButton
}
StyledTextField {
id: transparencyLayersInput
anchors.centerIn: parent
width: parent.width - Appearance.padding.normal
horizontalAlignment: TextInput.AlignHCenter
validator: IntValidator { bottom: 0; top: 100 }
Component.onCompleted: {
text = Math.round(rootPane.transparencyLayers * 100).toString();
}
onTextChanged: {
if (activeFocus) {
const val = parseInt(text);
if (!isNaN(val) && val >= 0 && val <= 100) {
rootPane.transparencyLayers = val / 100;
rootPane.saveConfig();
}
}
}
onEditingFinished: {
const val = parseInt(text);
if (isNaN(val) || val < 0 || val > 100) {
text = Math.round(rootPane.transparencyLayers * 100).toString();
}
}
}
}
StyledText {
text: "%"
color: Colours.palette.m3outline
font.pointSize: Appearance.font.size.normal
}
}
StyledSlider {
id: layersSlider
Layout.fillWidth: true
implicitHeight: Appearance.padding.normal * 3
from: 0
to: 100
value: rootPane.transparencyLayers * 100
onMoved: {
rootPane.transparencyLayers = layersSlider.value / 100;
if (!transparencyLayersInput.activeFocus) {
transparencyLayersInput.text = Math.round(layersSlider.value).toString();
}
rootPane.saveConfig();
}
}
}
}
}
CollapsibleSection {
id: borderSection
title: qsTr("Border")
onToggleRequested: {
sidebarFlickable.collapseAllSections(borderSection);
}
SectionContainer {
contentSpacing: Appearance.spacing.normal
ColumnLayout {
Layout.fillWidth: true
spacing: Appearance.spacing.small
RowLayout {
Layout.fillWidth: true
spacing: Appearance.spacing.normal
StyledText {
text: qsTr("Border rounding")
font.pointSize: Appearance.font.size.normal
}
Item {
Layout.fillWidth: true
}
StyledRect {
Layout.preferredWidth: 70
implicitHeight: borderRoundingInput.implicitHeight + Appearance.padding.small * 2
color: borderRoundingInputHover.containsMouse || borderRoundingInput.activeFocus
? Colours.layer(Colours.palette.m3surfaceContainer, 3)
: Colours.layer(Colours.palette.m3surfaceContainer, 2)
radius: Appearance.rounding.small
border.width: 1
border.color: borderRoundingInput.activeFocus
? Colours.palette.m3primary
: Qt.alpha(Colours.palette.m3outline, 0.3)
Behavior on color { CAnim {} }
Behavior on border.color { CAnim {} }
MouseArea {
id: borderRoundingInputHover
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.IBeamCursor
acceptedButtons: Qt.NoButton
}
StyledTextField {
id: borderRoundingInput
anchors.centerIn: parent
width: parent.width - Appearance.padding.normal
horizontalAlignment: TextInput.AlignHCenter
validator: DoubleValidator { bottom: 0.1; top: 100 }
Component.onCompleted: {
text = (rootPane.borderRounding).toFixed(1);
}
onTextChanged: {
if (activeFocus) {
const val = parseFloat(text);
if (!isNaN(val) && val >= 0.1 && val <= 100) {
rootPane.borderRounding = val;
rootPane.saveConfig();
}
}
}
onEditingFinished: {
const val = parseFloat(text);
if (isNaN(val) || val < 0.1 || val > 100) {
text = (rootPane.borderRounding).toFixed(1);
}
}
}
}
}
StyledSlider {
id: borderRoundingSlider
Layout.fillWidth: true
implicitHeight: Appearance.padding.normal * 3
from: 0.1
to: 100
value: rootPane.borderRounding
onMoved: {
rootPane.borderRounding = borderRoundingSlider.value;
if (!borderRoundingInput.activeFocus) {
borderRoundingInput.text = (borderRoundingSlider.value).toFixed(1);
}
rootPane.saveConfig();
}
}
}
}
SectionContainer {
contentSpacing: Appearance.spacing.normal
ColumnLayout {
Layout.fillWidth: true
spacing: Appearance.spacing.small
RowLayout {
Layout.fillWidth: true
spacing: Appearance.spacing.normal
StyledText {
text: qsTr("Border thickness")
font.pointSize: Appearance.font.size.normal
}
Item {
Layout.fillWidth: true
}
StyledRect {
Layout.preferredWidth: 70
implicitHeight: borderThicknessInput.implicitHeight + Appearance.padding.small * 2
color: borderThicknessInputHover.containsMouse || borderThicknessInput.activeFocus
? Colours.layer(Colours.palette.m3surfaceContainer, 3)
: Colours.layer(Colours.palette.m3surfaceContainer, 2)
radius: Appearance.rounding.small
border.width: 1
border.color: borderThicknessInput.activeFocus
? Colours.palette.m3primary
: Qt.alpha(Colours.palette.m3outline, 0.3)
Behavior on color { CAnim {} }
Behavior on border.color { CAnim {} }
MouseArea {
id: borderThicknessInputHover
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.IBeamCursor
acceptedButtons: Qt.NoButton
}
StyledTextField {
id: borderThicknessInput
anchors.centerIn: parent
width: parent.width - Appearance.padding.normal
horizontalAlignment: TextInput.AlignHCenter
validator: DoubleValidator { bottom: 0.1; top: 100 }
Component.onCompleted: {
text = (rootPane.borderThickness).toFixed(1);
}
onTextChanged: {
if (activeFocus) {
const val = parseFloat(text);
if (!isNaN(val) && val >= 0.1 && val <= 100) {
rootPane.borderThickness = val;
rootPane.saveConfig();
}
}
}
onEditingFinished: {
const val = parseFloat(text);
if (isNaN(val) || val < 0.1 || val > 100) {
text = (rootPane.borderThickness).toFixed(1);
}
}
}
}
}
StyledSlider {
id: borderThicknessSlider
Layout.fillWidth: true
implicitHeight: Appearance.padding.normal * 3
from: 0.1
to: 100
value: rootPane.borderThickness
onMoved: {
rootPane.borderThickness = borderThicknessSlider.value;
if (!borderThicknessInput.activeFocus) {
borderThicknessInput.text = (borderThicknessSlider.value).toFixed(1);
}
rootPane.saveConfig();
}
}
}
}
}
CollapsibleSection {
id: backgroundSection
title: qsTr("Background")
onToggleRequested: {
sidebarFlickable.collapseAllSections(backgroundSection);
}
SwitchRow {
label: qsTr("Desktop clock")
checked: rootPane.desktopClockEnabled
onToggled: checked => {
rootPane.desktopClockEnabled = checked;
rootPane.saveConfig();
}
}
SwitchRow {
label: qsTr("Background enabled")
checked: rootPane.backgroundEnabled
onToggled: checked => {
rootPane.backgroundEnabled = checked;
rootPane.saveConfig();
}
}
StyledText {
Layout.topMargin: Appearance.spacing.normal
text: qsTr("Visualiser")
font.pointSize: Appearance.font.size.larger
font.weight: 500
}
SwitchRow {
label: qsTr("Visualiser enabled")
checked: rootPane.visualiserEnabled
onToggled: checked => {
rootPane.visualiserEnabled = checked;
rootPane.saveConfig();
}
}
SwitchRow {
label: qsTr("Visualiser auto hide")
checked: rootPane.visualiserAutoHide
onToggled: checked => {
rootPane.visualiserAutoHide = checked;
rootPane.saveConfig();
}
}
SectionContainer {
contentSpacing: Appearance.spacing.normal
ColumnLayout {
Layout.fillWidth: true
spacing: Appearance.spacing.small
RowLayout {
Layout.fillWidth: true
spacing: Appearance.spacing.normal
StyledText {
text: qsTr("Visualiser rounding")
font.pointSize: Appearance.font.size.normal
}
Item {
Layout.fillWidth: true
}
StyledRect {
Layout.preferredWidth: 70
implicitHeight: visualiserRoundingInput.implicitHeight + Appearance.padding.small * 2
color: visualiserRoundingInputHover.containsMouse || visualiserRoundingInput.activeFocus
? Colours.layer(Colours.palette.m3surfaceContainer, 3)
: Colours.layer(Colours.palette.m3surfaceContainer, 2)
radius: Appearance.rounding.small
border.width: 1
border.color: visualiserRoundingInput.activeFocus
? Colours.palette.m3primary
: Qt.alpha(Colours.palette.m3outline, 0.3)
Behavior on color { CAnim {} }
Behavior on border.color { CAnim {} }
MouseArea {
id: visualiserRoundingInputHover
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.IBeamCursor
acceptedButtons: Qt.NoButton
}
StyledTextField {
id: visualiserRoundingInput
anchors.centerIn: parent
width: parent.width - Appearance.padding.normal
horizontalAlignment: TextInput.AlignHCenter
validator: IntValidator { bottom: 0; top: 10 }
Component.onCompleted: {
text = Math.round(rootPane.visualiserRounding).toString();
}
onTextChanged: {
if (activeFocus) {
const val = parseInt(text);
if (!isNaN(val) && val >= 0 && val <= 10) {
rootPane.visualiserRounding = val;
rootPane.saveConfig();
}
}
}
onEditingFinished: {
const val = parseInt(text);
if (isNaN(val) || val < 0 || val > 10) {
text = Math.round(rootPane.visualiserRounding).toString();
}
}
}
}
}
StyledSlider {
id: visualiserRoundingSlider
Layout.fillWidth: true
implicitHeight: Appearance.padding.normal * 3
from: 0
to: 10
stepSize: 1
value: rootPane.visualiserRounding
onMoved: {
rootPane.visualiserRounding = Math.round(visualiserRoundingSlider.value);
if (!visualiserRoundingInput.activeFocus) {
visualiserRoundingInput.text = Math.round(visualiserRoundingSlider.value).toString();
}
rootPane.saveConfig();
}
}
}
}
SectionContainer {
contentSpacing: Appearance.spacing.normal
ColumnLayout {
Layout.fillWidth: true
spacing: Appearance.spacing.small
RowLayout {
Layout.fillWidth: true
spacing: Appearance.spacing.normal
StyledText {
text: qsTr("Visualiser spacing")
font.pointSize: Appearance.font.size.normal
}
Item {
Layout.fillWidth: true
}
StyledRect {
Layout.preferredWidth: 70
implicitHeight: visualiserSpacingInput.implicitHeight + Appearance.padding.small * 2
color: visualiserSpacingInputHover.containsMouse || visualiserSpacingInput.activeFocus
? Colours.layer(Colours.palette.m3surfaceContainer, 3)
: Colours.layer(Colours.palette.m3surfaceContainer, 2)
radius: Appearance.rounding.small
border.width: 1
border.color: visualiserSpacingInput.activeFocus
? Colours.palette.m3primary
: Qt.alpha(Colours.palette.m3outline, 0.3)
Behavior on color { CAnim {} }
Behavior on border.color { CAnim {} }
MouseArea {
id: visualiserSpacingInputHover
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.IBeamCursor
acceptedButtons: Qt.NoButton
}
StyledTextField {
id: visualiserSpacingInput
anchors.centerIn: parent
width: parent.width - Appearance.padding.normal
horizontalAlignment: TextInput.AlignHCenter
validator: DoubleValidator { bottom: 0; top: 2 }
Component.onCompleted: {
text = (rootPane.visualiserSpacing).toFixed(1);
}
onTextChanged: {
if (activeFocus) {
const val = parseFloat(text);
if (!isNaN(val) && val >= 0 && val <= 2) {
rootPane.visualiserSpacing = val;
rootPane.saveConfig();
}
}
}
onEditingFinished: {
const val = parseFloat(text);
if (isNaN(val) || val < 0 || val > 2) {
text = (rootPane.visualiserSpacing).toFixed(1);
}
}
}
}
}
StyledSlider {
id: visualiserSpacingSlider
Layout.fillWidth: true
implicitHeight: Appearance.padding.normal * 3
from: 0
to: 2
value: rootPane.visualiserSpacing
onMoved: {
rootPane.visualiserSpacing = visualiserSpacingSlider.value;
if (!visualiserSpacingInput.activeFocus) {
visualiserSpacingInput.text = (visualiserSpacingSlider.value).toFixed(1);
}
rootPane.saveConfig();
}
}
}
}
}
}
}
}
}
Item {
Layout.fillWidth: true
Layout.fillHeight: true
ClippingRectangle {
id: rightAppearanceClippingRect
anchors.fill: parent
anchors.margins: Appearance.padding.normal
anchors.leftMargin: 0
anchors.rightMargin: Appearance.padding.normal / 2
radius: rightAppearanceBorder.innerRadius
color: "transparent"
Loader {
id: rightAppearanceLoader
anchors.fill: parent
anchors.topMargin: Appearance.padding.large * 2
anchors.bottomMargin: Appearance.padding.large * 2
anchors.leftMargin: 0
anchors.rightMargin: 0
asynchronous: true
sourceComponent: appearanceRightContentComponent
property var rootPane: root
}
}
InnerBorder {
id: rightAppearanceBorder
leftThickness: Appearance.padding.normal / 2
}
Component {
id: appearanceRightContentComponent
StyledFlickable {
id: rightAppearanceFlickable
flickableDirection: Flickable.VerticalFlick
contentHeight: contentLayout.height
StyledScrollBar.vertical: StyledScrollBar {
flickable: rightAppearanceFlickable
}
ColumnLayout {
id: contentLayout
anchors.left: parent.left
anchors.right: parent.right
anchors.leftMargin: Appearance.padding.large * 2
anchors.rightMargin: Appearance.padding.large * 2
spacing: Appearance.spacing.normal
MaterialIcon {
Layout.alignment: Qt.AlignHCenter
text: "palette"
font.pointSize: Appearance.font.size.extraLarge * 3
font.bold: true
}
StyledText {
Layout.alignment: Qt.AlignHCenter
text: qsTr("Appearance Settings")
font.pointSize: Appearance.font.size.large
font.bold: true
}
StyledText {
Layout.topMargin: Appearance.spacing.large
Layout.alignment: Qt.AlignHCenter
text: qsTr("Wallpaper")
font.pointSize: Appearance.font.size.extraLarge
font.weight: 600
}
StyledText {
Layout.alignment: Qt.AlignHCenter
text: qsTr("Select a wallpaper")
font.pointSize: Appearance.font.size.normal
color: Colours.palette.m3onSurfaceVariant
}
GridLayout {
Layout.fillWidth: true
Layout.topMargin: Appearance.spacing.large
Layout.alignment: Qt.AlignHCenter
columns: Math.max(2, Math.floor(parent.width / 200))
rowSpacing: Appearance.spacing.normal
columnSpacing: Appearance.spacing.normal
// Center the grid content
Layout.maximumWidth: {
const cols = columns;
const itemWidth = 200;
const spacing = columnSpacing;
return cols * itemWidth + (cols - 1) * spacing;
}
Repeater {
model: Wallpapers.list
delegate: Item {
required property var modelData
Layout.preferredWidth: 200
Layout.preferredHeight: 140
Layout.minimumWidth: 200
Layout.minimumHeight: 140
Layout.maximumWidth: 200
Layout.maximumHeight: 140
readonly property bool isCurrent: modelData.path === Wallpapers.actualCurrent
StateLayer {
radius: Appearance.rounding.normal
function onClicked(): void {
Wallpapers.setWallpaper(modelData.path);
}
}
StyledClippingRect {
id: image
anchors.fill: parent
color: Colours.tPalette.m3surfaceContainer
radius: Appearance.rounding.normal
CachingImage {
id: cachingImage
path: modelData.path
anchors.fill: parent
cache: true
visible: opacity > 0
// Show when ready
opacity: status === Image.Ready ? 1 : 0
Behavior on opacity {
NumberAnimation {
duration: 1000
easing.type: Easing.OutQuad
}
}
}
// Fallback image for when caching fails
Image {
id: fallbackImage
anchors.fill: parent
source: fallbackTimer.triggered && cachingImage.status !== Image.Ready ? modelData.path : ""
asynchronous: true
fillMode: Image.PreserveAspectCrop
cache: true
visible: opacity > 0
opacity: status === Image.Ready && cachingImage.status !== Image.Ready ? 1 : 0
Behavior on opacity {
NumberAnimation {
duration: 1000
easing.type: Easing.OutQuad
}
}
}
// Timer to trigger fallback only if caching hasn't loaded
Timer {
id: fallbackTimer
property bool triggered: false
interval: 800
running: cachingImage.status === Image.Loading || cachingImage.status === Image.Null
onTriggered: triggered = true
}
}
// Border overlay that doesn't affect image size
Rectangle {
anchors.fill: parent
color: "transparent"
radius: Appearance.rounding.normal
border.width: isCurrent ? 2 : 0
border.color: Colours.palette.m3primary
Behavior on border.width {
NumberAnimation {
duration: 150
easing.type: Easing.OutQuad
}
}
MaterialIcon {
anchors.right: parent.right
anchors.top: parent.top
anchors.margins: Appearance.padding.small
visible: isCurrent
text: "check_circle"
color: Colours.palette.m3primary
font.pointSize: Appearance.font.size.large
}
// Gradient overlay for filename with rounded bottom corners
Rectangle {
id: filenameOverlay
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: parent.bottom
implicitHeight: filenameText.implicitHeight + Appearance.padding.normal * 2
gradient: Gradient {
GradientStop {
position: 0.0
color: Qt.rgba(0, 0, 0, 0)
}
GradientStop {
position: 0.3
color: Qt.rgba(0, 0, 0, 0.3)
}
GradientStop {
position: 0.7
color: Qt.rgba(0, 0, 0, 0.75)
}
GradientStop {
position: 1.0
color: Qt.rgba(0, 0, 0, 0.85)
}
}
}
opacity: 0
Behavior on opacity {
NumberAnimation {
duration: 1000
easing.type: Easing.OutCubic
}
}
Component.onCompleted: {
opacity = 1;
}
}
StyledText {
id: filenameText
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: parent.bottom
anchors.leftMargin: Appearance.padding.normal
anchors.rightMargin: Appearance.padding.normal
anchors.bottomMargin: Appearance.padding.normal
readonly property string fileName: {
const path = modelData.relativePath || "";
const parts = path.split("/");
return parts.length > 0 ? parts[parts.length - 1] : path;
}
text: fileName
font.pointSize: Appearance.font.size.smaller
font.weight: 500
color: isCurrent ? Colours.palette.m3primary : "#FFFFFF"
elide: Text.ElideMiddle
maximumLineCount: 1
opacity: 0
Behavior on opacity {
NumberAnimation {
duration: 1000
easing.type: Easing.OutCubic
}
}
Component.onCompleted: {
opacity = 1;
}
}
}
}
}
}
}
}
}
}