1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2026-04-12 16:52:10 -04:00

meta: integrate wallpaper, FileBrowser, StateLayer

- A lot of this is implements patterns implemented by soramannew's
  caelestia-shell
This commit is contained in:
bbedward
2025-07-23 23:20:11 -04:00
parent a0735db7a4
commit ee2cbd708d
33 changed files with 1494 additions and 915 deletions

View File

@@ -2,7 +2,7 @@ import QtQuick
import qs.Common
import qs.Widgets
Rectangle {
StyledRect {
id: root
property string iconName: ""
@@ -18,7 +18,7 @@ Rectangle {
width: buttonSize
height: buttonSize
radius: circular ? buttonSize / 2 : Theme.cornerRadius
color: mouseArea.containsMouse ? hoverColor : backgroundColor
color: backgroundColor
DankIcon {
anchors.centerIn: parent
@@ -27,21 +27,10 @@ Rectangle {
color: root.iconColor
}
MouseArea {
id: mouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
StateLayer {
stateColor: Theme.primary
cornerRadius: root.radius
onClicked: root.clicked()
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}

View File

@@ -1,18 +1,48 @@
import QtQuick
import qs.Common
Text {
StyledText {
id: icon
property alias name: icon.text
property alias size: icon.font.pixelSize
property alias color: icon.color
property bool filled: false
property real fill: filled ? 1 : 0
property int grade: Theme.isLightMode ? 0 : -25
property int weight: filled ? 500 : 400
font.family: "Material Symbols Rounded"
font.pixelSize: Theme.iconSize
font.weight: filled ? Font.Medium : Font.Normal
font.pixelSize: Appearance.fontSize.normal
font.weight: weight
color: Theme.surfaceText
verticalAlignment: Text.AlignVCenter
horizontalAlignment: Text.AlignHCenter
// Material Icons variable font axes support
font.variableAxes: ({
"FILL": fill.toFixed(1),
"GRAD": grade,
"opsz": 24,
"wght": weight
})
// Smooth transitions for variable axes
Behavior on fill {
NumberAnimation {
duration: Appearance.anim.durations.quick
easing.type: Easing.BezierSpline
easing.bezierCurve: Appearance.anim.curves.standard
}
}
Behavior on weight {
NumberAnimation {
duration: Appearance.anim.durations.quick
easing.type: Easing.BezierSpline
easing.bezierCurve: Appearance.anim.curves.standard
}
}
}

View File

@@ -25,7 +25,7 @@ Item {
spacing: Theme.spacingM
// Value display
Text {
StyledText {
text: slider.value + slider.unit
font.pixelSize: Theme.fontSizeMedium
color: slider.enabled ? Theme.surfaceText : Theme.surfaceVariantText
@@ -49,7 +49,7 @@ Item {
}
// Slider track
Rectangle {
StyledRect {
id: sliderTrack
property int leftIconWidth: slider.leftIcon.length > 0 ? Theme.iconSize : 0
@@ -62,7 +62,7 @@ Item {
anchors.verticalCenter: parent.verticalCenter
// Fill
Rectangle {
StyledRect {
id: sliderFill
width: parent.width * ((slider.value - slider.minimum) / (slider.maximum - slider.minimum))
@@ -72,8 +72,8 @@ Item {
Behavior on width {
NumberAnimation {
duration: 150
easing.type: Easing.OutCubic
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
@@ -81,7 +81,7 @@ Item {
}
// Draggable handle
Rectangle {
StyledRect {
id: sliderHandle
width: 18
@@ -95,7 +95,7 @@ Item {
scale: sliderMouseArea.containsMouse || sliderMouseArea.pressed ? 1.2 : 1
// Handle glow effect when active
Rectangle {
StyledRect {
anchors.centerIn: parent
width: parent.width + 4
height: parent.height + 4
@@ -104,19 +104,12 @@ Item {
border.color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.3)
border.width: 2
visible: sliderMouseArea.containsMouse && slider.enabled
Behavior on opacity {
NumberAnimation {
duration: 150
}
}
}
Behavior on scale {
NumberAnimation {
duration: 150
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}

View File

@@ -1,38 +0,0 @@
import QtQuick
import QtQuick.Effects
import Quickshell
import Quickshell.Io
import Quickshell.Widgets
import qs.Common
IconImage {
id: root
property string colorOverride: ""
property real brightnessOverride: 0.5
property real contrastOverride: 1
smooth: true
asynchronous: true
layer.enabled: colorOverride !== ""
Process {
running: true
command: ["sh", "-c", ". /etc/os-release && echo $LOGO"]
stdout: StdioCollector {
onStreamFinished: () => {
root.source = Quickshell.iconPath(this.text.trim());
}
}
}
layer.effect: MultiEffect {
colorization: 1
colorizationColor: colorOverride
brightness: brightnessOverride
contrast: contrastOverride
}
}

View File

@@ -1,5 +1,6 @@
import QtQuick
import qs.Common
import qs.Widgets
Item {
id: toggle
@@ -16,13 +17,25 @@ Item {
width: text ? parent.width : 48
height: text ? 60 : 24
Rectangle {
StyledRect {
id: background
anchors.fill: parent
radius: toggle.text ? Theme.cornerRadius : 0
color: toggle.text ? (toggleArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08)) : "transparent"
color: toggle.text ? Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08) : "transparent"
visible: toggle.text
StateLayer {
visible: toggle.text
stateColor: Theme.primary
cornerRadius: parent.radius
onClicked: {
toggle.checked = !toggle.checked;
toggle.clicked();
toggle.toggled(toggle.checked);
}
}
}
Row {
@@ -40,16 +53,15 @@ Item {
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingXS
Text {
StyledText {
text: toggle.text
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
font.pixelSize: Appearance.fontSize.normal
font.weight: Font.Medium
}
Text {
StyledText {
text: toggle.description
font.pixelSize: Theme.fontSizeSmall
font.pixelSize: Appearance.fontSize.small
color: Theme.surfaceVariantText
wrapMode: Text.WordWrap
width: Math.min(implicitWidth, toggle.width - 120)
@@ -60,7 +72,7 @@ Item {
}
Rectangle {
StyledRect {
id: toggleTrack
width: toggle.text ? 48 : parent.width
@@ -72,7 +84,7 @@ Item {
color: toggle.checked ? Theme.primary : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.3)
opacity: toggle.toggling ? 0.6 : 1
Rectangle {
StyledRect {
id: toggleHandle
width: 20
@@ -82,7 +94,7 @@ Item {
anchors.verticalCenter: parent.verticalCenter
x: toggle.checked ? parent.width - width - 2 : 2
Rectangle {
StyledRect {
anchors.centerIn: parent
width: parent.width + 2
height: parent.height + 2
@@ -95,28 +107,26 @@ Item {
Behavior on x {
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.emphasizedEasing
duration: Appearance.anim.durations.normal
easing.type: Easing.BezierSpline
easing.bezierCurve: Appearance.anim.curves.emphasizedDecel
}
}
}
}
MouseArea {
id: toggleArea
anchors.fill: toggle.text ? toggle : toggleTrack
hoverEnabled: true
cursorShape: toggle.enabled ? Qt.PointingHandCursor : Qt.ArrowCursor
enabled: toggle.enabled
onClicked: {
toggle.checked = !toggle.checked;
toggle.clicked();
toggle.toggled(toggle.checked);
StateLayer {
disabled: !toggle.enabled
stateColor: Theme.primary
cornerRadius: parent.radius
onClicked: {
toggle.checked = !toggle.checked;
toggle.clicked();
toggle.toggled(toggle.checked);
}
}
}
}

281
Widgets/FileBrowser.qml Normal file
View File

@@ -0,0 +1,281 @@
import QtQuick
import QtQuick.Controls
import QtCore
import Qt.labs.folderlistmodel
import Quickshell.Io
import qs.Common
import qs.Widgets
import qs.Modals
DankModal {
id: fileBrowser
signal fileSelected(string path)
property string homeDir: StandardPaths.writableLocation(StandardPaths.HomeLocation)
property string currentPath: ""
property var fileExtensions: ["*.*"]
property string browserTitle: "Select File"
property string browserIcon: "folder_open"
property string browserType: "generic" // "wallpaper" or "profile" for last path memory
FolderListModel {
id: folderModel
showDirsFirst: true
showDotAndDotDot: false
showHidden: false
nameFilters: fileExtensions
showFiles: true
showDirs: true
folder: currentPath ? "file://" + currentPath : "file://" + homeDir
}
function getLastPath() {
var lastPath = "";
if (browserType === "wallpaper") {
lastPath = Prefs.wallpaperLastPath;
} else if (browserType === "profile") {
lastPath = Prefs.profileLastPath;
}
// Check if last path exists, otherwise use home
if (lastPath && lastPath !== "") {
// TODO: Could add directory existence check here
return lastPath;
}
return homeDir;
}
function saveLastPath(path) {
if (browserType === "wallpaper") {
Prefs.wallpaperLastPath = path;
} else if (browserType === "profile") {
Prefs.profileLastPath = path;
}
Prefs.saveSettings();
}
Component.onCompleted: {
currentPath = getLastPath();
}
width: 800
height: 600
keyboardFocus: "ondemand"
enableShadow: true
visible: false
onBackgroundClicked: visible = false
onVisibleChanged: {
if (visible) {
// Use last path or home directory when opening
var startPath = getLastPath();
console.log("Opening file browser, setting path to:", startPath);
currentPath = startPath;
}
}
onCurrentPathChanged: {
console.log("Current path changed to:", currentPath);
console.log("Model count:", folderModel.count);
// Log first few files to debug
for (var i = 0; i < Math.min(3, folderModel.count); i++) {
console.log("File", i, ":", folderModel.get(i, "fileName"));
}
}
function navigateUp() {
var path = currentPath;
// Don't go above home directory
if (path === homeDir) {
console.log("Already at home directory, can't go up");
return;
}
var lastSlash = path.lastIndexOf('/');
if (lastSlash > 0) {
var newPath = path.substring(0, lastSlash);
// Make sure we don't go above home (check if newPath starts with homeDir)
if (newPath.startsWith(homeDir)) {
console.log("Navigating up from", path, "to", newPath);
currentPath = newPath;
saveLastPath(newPath);
} else {
console.log("Would go above home directory, stopping at", homeDir);
currentPath = homeDir;
saveLastPath(homeDir);
}
}
}
function navigateTo(path) {
currentPath = path;
saveLastPath(path); // Save the path when navigating
}
content: Component {
Column {
anchors.fill: parent
anchors.margins: Theme.spacingM
spacing: Theme.spacingS
// Header
Row {
width: parent.width
spacing: Theme.spacingM
DankIcon {
name: browserIcon
size: Theme.iconSizeLarge
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: browserTitle
font.pixelSize: Theme.fontSizeXLarge
color: Theme.surfaceText
font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter
}
Item {
width: parent.width - 200
height: 1
}
// Close button
DankActionButton {
iconName: "close"
iconSize: Theme.iconSizeSmall
iconColor: Theme.surfaceText
onClicked: fileBrowser.visible = false
}
}
// Current path display and navigation
Row {
width: parent.width
spacing: Theme.spacingS
StyledRect {
width: 32
height: 32
radius: Theme.cornerRadius
color: mouseArea.containsMouse && currentPath !== homeDir ? Theme.surfaceVariant : "transparent"
opacity: currentPath !== homeDir ? 1.0 : 0.0
DankIcon {
anchors.centerIn: parent
name: "arrow_back"
size: Theme.iconSizeSmall
color: Theme.surfaceText
}
MouseArea {
id: mouseArea
anchors.fill: parent
hoverEnabled: currentPath !== homeDir
cursorShape: currentPath !== homeDir ? Qt.PointingHandCursor : Qt.ArrowCursor
enabled: currentPath !== homeDir
onClicked: navigateUp()
}
}
StyledText {
text: "Current folder: " + fileBrowser.currentPath
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
width: parent.width - 40 - Theme.spacingS
elide: Text.ElideMiddle
anchors.verticalCenter: parent.verticalCenter
maximumLineCount: 1
wrapMode: Text.NoWrap
}
}
// File grid
ScrollView {
width: parent.width
height: parent.height - 80
clip: true
GridView {
id: fileGrid
cellWidth: 150
cellHeight: 130
model: folderModel
delegate: StyledRect {
width: 140
height: 120
radius: Theme.cornerRadius
color: mouseArea.containsMouse ? Theme.surfaceVariant : "transparent"
border.color: Theme.outline
border.width: mouseArea.containsMouse ? 1 : 0
Column {
anchors.centerIn: parent
spacing: Theme.spacingXS
// Image preview or folder icon
Item {
width: 80
height: 60
anchors.horizontalCenter: parent.horizontalCenter
Image {
anchors.fill: parent
source: !model.fileIsDir ? model.fileURL : ""
fillMode: Image.PreserveAspectCrop
visible: !model.fileIsDir
asynchronous: true
}
DankIcon {
anchors.centerIn: parent
name: model.fileIsDir ? "folder" : "description"
size: Theme.iconSizeLarge
color: Theme.primary
visible: model.fileIsDir
}
}
// File name
StyledText {
text: model.fileName || ""
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
width: 120
elide: Text.ElideMiddle
horizontalAlignment: Text.AlignHCenter
anchors.horizontalCenter: parent.horizontalCenter
maximumLineCount: 2
wrapMode: Text.WordWrap
}
}
MouseArea {
id: mouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (model.fileIsDir) {
navigateTo(model.filePath);
} else {
fileSelected(model.filePath);
}
}
}
}
}
}
}
}
}

View File

@@ -1,56 +0,0 @@
import QtQuick
import qs.Common
Item {
id: root
required property Item target
property int direction: Anims.direction.fadeOnly
function show() { _apply(true) }
function hide() { _apply(false) }
function _apply(showing) {
const off = Anims.slidePx
let fromX = 0
let toX = 0
switch(direction) {
case Anims.direction.fromLeft: fromX = -off; toX = 0; break
case Anims.direction.fromRight: fromX = off; toX = 0; break
default: fromX = 0; toX = 0;
}
if (showing) {
target.x = fromX
target.opacity = 0
target.visible = true
animX.from = fromX; animX.to = toX
animO.from = 0; animO.to = 1
} else {
animX.from = target.x; animX.to = (direction === Anims.direction.fromLeft ? -off :
direction === Anims.direction.fromRight ? off : 0)
animO.from = target.opacity; animO.to = 0
}
seq.restart()
}
SequentialAnimation {
id: seq
ParallelAnimation {
NumberAnimation {
id: animX
target: root.target
property: "x"
duration: Anims.durMed
easing.type: Easing.OutCubic
}
NumberAnimation {
id: animO
target: root.target
property: "opacity"
duration: Anims.durShort
}
}
ScriptAction { script: if (root.target.opacity === 0) root.target.visible = false }
}
}

98
Widgets/StateLayer.qml Normal file
View File

@@ -0,0 +1,98 @@
import QtQuick
import qs.Common
import qs.Widgets
MouseArea {
id: root
property bool disabled: false
property color stateColor: Theme.surfaceText
property real cornerRadius: parent?.radius ?? Appearance.rounding.normal
anchors.fill: parent
cursorShape: disabled ? undefined : Qt.PointingHandCursor
hoverEnabled: true
onPressed: event => {
if (disabled) return;
rippleAnimation.x = event.x;
rippleAnimation.y = event.y;
const dist = (ox, oy) => ox * ox + oy * oy;
rippleAnimation.radius = Math.sqrt(Math.max(
dist(event.x, event.y),
dist(event.x, height - event.y),
dist(width - event.x, event.y),
dist(width - event.x, height - event.y)
));
rippleAnimation.restart();
}
Rectangle {
id: hoverLayer
anchors.fill: parent
radius: root.cornerRadius
color: Qt.rgba(root.stateColor.r, root.stateColor.g, root.stateColor.b,
root.disabled ? 0 :
root.pressed ? 0.12 :
root.containsMouse ? 0.08 : 0)
Rectangle {
id: ripple
radius: width / 2
color: root.stateColor
opacity: 0
transform: Translate {
x: -ripple.width / 2
y: -ripple.height / 2
}
}
// Clip ripple to container bounds
clip: true
}
SequentialAnimation {
id: rippleAnimation
property real x
property real y
property real radius
PropertyAction {
target: ripple
property: "x"
value: rippleAnimation.x
}
PropertyAction {
target: ripple
property: "y"
value: rippleAnimation.y
}
PropertyAction {
target: ripple
property: "opacity"
value: 0.12
}
NumberAnimation {
target: ripple
properties: "width,height"
from: 0
to: rippleAnimation.radius * 2
duration: Appearance.anim.durations.normal
easing.type: Easing.BezierSpline
easing.bezierCurve: Appearance.anim.curves.standardDecel
}
NumberAnimation {
target: ripple
property: "opacity"
to: 0
duration: Appearance.anim.durations.normal
easing.type: Easing.BezierSpline
easing.bezierCurve: Appearance.anim.curves.standard
}
}
}

37
Widgets/StyledRect.qml Normal file
View File

@@ -0,0 +1,37 @@
import QtQuick
import qs.Common
Rectangle {
id: root
color: "transparent"
radius: Appearance.rounding.normal
Behavior on color {
ColorAnimation {
duration: Appearance.anim.durations.normal
easing.type: Easing.BezierSpline
easing.bezierCurve: Appearance.anim.curves.standard
}
}
Behavior on radius {
NumberAnimation {
duration: Appearance.anim.durations.normal
easing.type: Easing.BezierSpline
easing.bezierCurve: Appearance.anim.curves.standard
}
}
Behavior on opacity {
NumberAnimation {
duration: Appearance.anim.durations.normal
easing.type: Easing.BezierSpline
easing.bezierCurve: Appearance.anim.curves.standard
}
}
}

35
Widgets/StyledText.qml Normal file
View File

@@ -0,0 +1,35 @@
import QtQuick
import qs.Common
Text {
id: root
color: Theme.surfaceText
font.pixelSize: Appearance.fontSize.normal
wrapMode: Text.WordWrap
elide: Text.ElideRight
verticalAlignment: Text.AlignVCenter
// Font rendering improvements for crisp text
renderType: Text.NativeRendering
textFormat: Text.PlainText
antialiasing: true
Behavior on color {
ColorAnimation {
duration: Appearance.anim.durations.normal
easing.type: Easing.BezierSpline
easing.bezierCurve: Appearance.anim.curves.standard
}
}
Behavior on opacity {
NumberAnimation {
duration: Appearance.anim.durations.normal
easing.type: Easing.BezierSpline
easing.bezierCurve: Appearance.anim.curves.standard
}
}
}