save progress

This commit is contained in:
kossLAN 2025-06-17 10:23:00 -04:00
parent f0673a66a2
commit c45c04e9ac
Signed by: kossLAN
SSH key fingerprint: SHA256:bdV0x+wdQHGJ6LgmstH3KV8OpWY+OOFmJcPcB0wQPV8
15 changed files with 621 additions and 418 deletions

View file

@ -4,15 +4,14 @@ The idea is to eventually be minimal but also use material 3 design language, lo
## TODO List
- [x] Custom Popup Window Surface for smooth anims on top bar
- [ ] Lockscreen
- [ ] Port visuals to more material 3 aethestic
- [ ] Screenshot tool
- [ ] Lockscreen (WIP)
- [ ] Port visuals to more material 3 aethestic (WIP)
- [x] Screenshot tool (WIP - kinda scuffed, but is functional)
- [ ] Recording/Clip widget with gpuscreenrecorder
- [ ] Session Manager
- [ ] Battery Profile Popup
- [ ] REDO Volume OSD
- [ ] REDO Volume OSD (WIP)
- [ ] REDO Launcher (wallpaper picker, calculator, commands, etc...)
- [ ] Integrate wallpaper picker into launcher
- [ ] Music Player Popup V4 lol
### Optionals

View file

@ -2,7 +2,8 @@ import QtQuick
import QtQuick.Layouts
import Quickshell
import Quickshell.Widgets
import "battery"
import "power"
import "volume"
import "systray" as SysTray
import "popups" as Popup
import "mpris" as Mpris
@ -99,6 +100,15 @@ PanelWindow {
Layout.fillHeight: true
}
VolumeIndicator {
id: volumeIndicator
popup: root.popup
Layout.preferredWidth: this.height
Layout.fillHeight: true
Layout.topMargin: 2
Layout.bottomMargin: 2
}
BatteryIndicator {
id: batteryIndicator
popup: root.popup

View file

@ -1,123 +0,0 @@
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Qt5Compat.GraphicalEffects
import Quickshell.Widgets
import Quickshell.Services.UPower
import "../.."
Item {
id: root
implicitWidth: height + 8 // for margin
visible: UPower.displayDevice.isLaptopBattery
required property var popup
MouseArea {
id: batteryButton
hoverEnabled: true
anchors.fill: parent
onClicked: {
if (root.popup.content == powerMenu) {
root.popup.hide();
return;
}
root.popup.set(this, powerMenu);
root.popup.show();
}
}
Item {
id: powerMenu
visible: false
implicitWidth: 250
implicitHeight: 80
MouseArea {
anchors.fill: parent
onClicked: {
console.log("why this work");
powerMenu.implicitWidth = 300;
}
}
RowLayout {
anchors.fill: parent
// placeholder for now
// Text {
// text
// }
ComboBox {
model: ["Power Save", "Balanced", "Performance"]
currentIndex: PowerProfiles.profile
onCurrentIndexChanged: PowerProfiles.profile = currentIndex
}
}
}
Rectangle {
id: highlight
color: batteryButton.containsMouse ? ShellSettings.colors["primary"] : "transparent"
// radius: width / 2
radius: 10
anchors {
fill: parent
// topMargin: 2
// bottomMargin: 2
}
Behavior on color {
ColorAnimation {
duration: 100
}
}
}
Item {
implicitWidth: parent.height
implicitHeight: parent.height
anchors.centerIn: parent
layer.enabled: true
layer.effect: OpacityMask {
source: Rectangle {
width: root.width
height: root.height
color: "white"
}
maskSource: IconImage {
implicitSize: root.width
source: "root:resources/battery/battery.svg"
}
}
Rectangle {
id: batteryBackground
color: Qt.color(ShellSettings.colors["surface"]).lighter(4)
opacity: 0.75
anchors {
fill: parent
margins: 2
}
}
Rectangle {
id: batteryPercentage
width: (parent.width - 4) * UPower.displayDevice.percentage
color: ShellSettings.colors["inverse_surface"]
anchors {
left: batteryBackground.left
top: batteryBackground.top
bottom: batteryBackground.bottom
}
}
}
}

View file

@ -1,14 +1,16 @@
import Quickshell.Widgets
import QtQuick
import "../../mpris" as Mpris
import "../../widgets" as Widgets
import "../.."
WrapperRectangle {
Widgets.MaterialButton {
id: root
color: button.containsMouse ? ShellSettings.colors["primary"] : "transparent"
radius: 6
leftMargin: 5
rightMargin: 5
implicitWidth: mediaInfo.contentWidth + 8
implicitHeight: parent.height
// onClicked: {
// popup.visible = !popup.visible;
// }
required property var bar
property var player: Mpris.Controller.trackedPlayer
@ -16,48 +18,25 @@ WrapperRectangle {
Text {
id: mediaInfo
text: root.player?.trackTitle ?? ""
color: button.containsMouse ? ShellSettings.colors["inverse_primary"] : ShellSettings.colors["inverse_surface"]
color: root.containsMouse ? ShellSettings.colors["inverse_primary"] : ShellSettings.colors["inverse_surface"]
elide: Text.ElideRight
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
font.pointSize: 11
anchors.verticalCenter: parent.verticalCenter
anchors.centerIn: parent
MouseArea {
id: button
anchors.fill: parent
hoverEnabled: true
onClicked: {
popup.visible = !popup.visible;
Behavior on color {
ColorAnimation {
duration: 200
}
}
}
}
WidgetWindow {
id: popup
visible: false
parentWindow: root.bar
// anchor.window: root.bar
}
// Item {
// id: menu
// WidgetWindow {
// id: popup
// visible: false
// implicitWidth: 250
// implicitHeight: 80
// parentWindow: root.bar
//
// // anchor.window: root.bar
// }
Behavior on color {
ColorAnimation {
duration: 100
}
}
}
Behavior on color {
ColorAnimation {
duration: 100
}
}
}

View file

@ -0,0 +1,102 @@
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Qt5Compat.GraphicalEffects
import Quickshell.Widgets
import Quickshell.Services.UPower
import "../../widgets" as Widgets
import "../.."
// todo: redo the tray icon handling
Item {
id: root
implicitWidth: height + 8 // for margin
visible: UPower.displayDevice.isLaptopBattery
required property var popup
Widgets.MaterialButton {
id: batteryButton
hoverEnabled: true
onClicked: {
if (root.popup.content == powerMenu) {
root.popup.hide();
return;
}
root.popup.set(this, powerMenu);
root.popup.show();
}
anchors {
fill: parent
margins: 1
}
Item {
implicitWidth: parent.height
implicitHeight: parent.height
anchors.centerIn: parent
layer.enabled: true
layer.effect: OpacityMask {
source: Rectangle {
width: root.width
height: root.height
color: "white"
}
maskSource: IconImage {
implicitSize: root.width
source: "root:resources/battery/battery.svg"
}
}
Rectangle {
id: batteryBackground
color: Qt.color(ShellSettings.colors["surface"]).lighter(4)
opacity: 0.75
anchors {
fill: parent
margins: 2
}
}
Rectangle {
id: batteryPercentage
width: (parent.width - 4) * UPower.displayDevice.percentage
color: ShellSettings.colors["inverse_surface"]
anchors {
left: batteryBackground.left
top: batteryBackground.top
bottom: batteryBackground.bottom
}
}
}
}
Item {
id: powerMenu
visible: false
implicitWidth: 250
implicitHeight: 80
RowLayout {
anchors.fill: parent
// ComboBox {
// model: ScriptModel {
// values: ["Power Save", "Balanced", "Performance"]
// }
//
// currentIndex: PowerProfiles.profile
// onCurrentIndexChanged: {
// PowerProfiles.profile = this.currentIndex;
// console.log(PowerProfile.toString(PowerProfiles.profile));
// }
// }
}
}
}

View file

@ -5,11 +5,11 @@ import QtQuick.Layouts
import Quickshell
import Quickshell.Widgets
import Quickshell.Services.SystemTray
import "../.."
import "../../widgets" as Widgets
RowLayout {
id: root
spacing: 10
spacing: 5
visible: SystemTray.items.values.length > 0
required property var popup
@ -23,10 +23,9 @@ RowLayout {
Layout.fillHeight: true
required property SystemTrayItem modelData
MouseArea {
Widgets.MaterialButton {
id: trayButton
hoverEnabled: true
anchors.fill: parent
onClicked: {
menuOpener.menu = trayField.modelData.menu;
@ -38,6 +37,25 @@ RowLayout {
root.popup.set(this, trayMenu);
root.popup.show();
}
anchors {
fill: parent
margins: 2
}
IconImage {
id: trayIcon
anchors.fill: parent
source: {
// console.log(trayField.modelData.id);
switch (trayField.modelData.id) {
case "obs":
return "image://icon/obs-tray";
default:
return trayField.modelData.icon;
}
}
}
}
QsMenuOpener {
@ -73,45 +91,6 @@ RowLayout {
}
}
}
Rectangle {
id: trayContainer
color: trayButton.containsMouse ? ShellSettings.colors["primary"] : "transparent"
radius: width / 2
implicitHeight: parent.height
implicitWidth: parent.height
anchors {
fill: parent
margins: 1
}
IconImage {
id: trayIcon
source: {
// console.log(trayField.modelData.id);
switch (trayField.modelData.id) {
case "obs":
return "image://icon/obs-tray";
default:
return trayField.modelData.icon;
}
}
anchors {
fill: parent
margins: 1
}
}
Behavior on color {
ColorAnimation {
duration: 100
}
}
}
}
}
}

View file

@ -0,0 +1,70 @@
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Layouts
import Quickshell.Widgets
import Quickshell.Services.Pipewire
import "../../widgets/" as Widgets
import "../.."
ColumnLayout {
id: root
Loader {
id: sinkLoader
active: sink
property PwNode sink: Pipewire.defaultAudioSink
sourceComponent: WrapperItem {
PwNodeLinkTracker {
id: linkTracker
node: sinkLoader.sink
}
ColumnLayout {
Repeater {
model: linkTracker.linkGroups
delegate: Loader {
id: nodeLoader
active: modelData.source !== null
Layout.preferredWidth: 350
Layout.preferredHeight: 45
required property PwLinkGroup modelData
sourceComponent: VolumeCard {
id: nodeCard
node: nodeLoader.modelData.source
text: node.properties["media.name"] ?? ""
// if icon-name is undefined, just gonna fallback on the application name
icon: IconImage {
source: {
if (nodeCard.node.properties["application.icon-name"] !== undefined)
return `image://icon/${nodeCard.node.properties["application.icon-name"]}`;
let applicationName = nodeCard.node.properties["application.name"];
return `image://icon/${applicationName?.toLowerCase() ?? "image-missing"}`;
}
}
button: Widgets.FontIconButton {
hoverEnabled: false
iconName: nodeCard.node.audio.muted ? "volume_off" : "volume_up"
checked: !nodeCard.node.audio.muted
inactiveColor: ShellSettings.colors["surface_container_highest"]
onClicked: {
nodeCard.node.audio.muted = !nodeCard.node.audio.muted;
}
}
anchors.fill: parent
}
}
}
}
}
}
}

View file

@ -0,0 +1,64 @@
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Layouts
import Quickshell.Services.Pipewire
import "../../widgets/" as Widgets
import "../.."
ColumnLayout {
id: root
// headphones
// don't load until the node is not null
Loader {
id: sinkLoader
active: sink !== null
Layout.preferredWidth: 350
Layout.preferredHeight: 45
property PwNode sink: Pipewire.defaultAudioSink
sourceComponent: VolumeCard {
id: sinkCard
node: sinkLoader.sink
button: Widgets.FontIconButton {
hoverEnabled: false
iconName: sinkCard.node.audio.muted ? "volume_off" : "volume_up"
checked: !sinkCard.node.audio.muted
inactiveColor: ShellSettings.colors["surface_container_highest"]
onClicked: {
sinkCard.node.audio.muted = !sinkCard.node.audio.muted;
}
}
anchors.fill: parent
}
}
// microphone, same as above
Loader {
id: sourceLoader
active: source !== null
Layout.preferredWidth: 350
Layout.preferredHeight: 45
property PwNode source: Pipewire.defaultAudioSource
sourceComponent: VolumeCard {
id: sourceCard
node: sourceLoader.source
button: Widgets.FontIconButton {
hoverEnabled: false
iconName: sourceCard.node.audio.muted ? "mic_off" : "mic"
checked: !sourceCard.node.audio.muted
inactiveColor: ShellSettings.colors["surface_container_highest"]
onClicked: {
sourceCard.node.audio.muted = !sourceCard.node.audio.muted;
}
}
anchors.fill: parent
}
}
}

50
bar/volume/VolumeCard.qml Normal file
View file

@ -0,0 +1,50 @@
import QtQuick
import QtQuick.Layouts
import Quickshell.Widgets
import Quickshell.Services.Pipewire
import "../../widgets/" as Widgets
import "../.."
WrapperRectangle {
id: root
color: ShellSettings.colors["surface_container"]
radius: width / 2
margin: 6
required property PwNode node
property string text
property Component button
property Component icon
PwObjectTracker {
id: tracker
objects: [root.node]
}
RowLayout {
Widgets.MaterialSlider {
value: root.node.audio.volume ?? 0
text: root.text
icon: root.icon
onValueChanged: {
// only allow changes when the node is ready other wise you will combust
if (!root.node.ready)
return;
root.node.audio.volume = value;
}
Layout.fillWidth: true
Layout.fillHeight: true
}
Loader {
id: buttonLoader
sourceComponent: root.button
Layout.preferredWidth: this.height
Layout.fillHeight: true
}
}
}

View file

@ -0,0 +1,76 @@
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Layouts
import Qt5Compat.GraphicalEffects
import Quickshell.Widgets
import Quickshell.Services.Pipewire
import "../../widgets/" as Widgets
import "../.."
WrapperItem {
id: root
visible: false
ColumnLayout {
spacing: 5
// Toolbar
Rectangle {
id: toolbar
color: ShellSettings.colors["surface_container_highest"]
radius: 10
layer.enabled: true
layer.effect: OpacityMask {
maskSource: Rectangle {
width: toolbar.width
height: toolbar.height
radius: toolbar.radius
color: "black"
}
}
Layout.fillWidth: true
Layout.preferredHeight: 35
RowLayout {
spacing: 0
anchors.fill: parent
Widgets.FontIconButton {
hoverEnabled: false
iconName: "headphones"
radius: 0
checked: page.currentIndex === 0
onClicked: page.currentIndex = 0
Layout.fillWidth: true
Layout.fillHeight: true
}
Widgets.FontIconButton {
hoverEnabled: false
iconName: "tune"
radius: 0
checked: page.currentIndex === 1
onClicked: page.currentIndex = 1
Layout.fillWidth: true
Layout.fillHeight: true
}
}
}
StackLayout {
id: page
currentIndex: 0
Layout.fillWidth: true
Layout.preferredHeight: currentItem ? currentItem.implicitHeight : 0
readonly property Item currentItem: children[currentIndex]
DeviceMixer {}
ApplicationMixer {}
}
}
}

View file

@ -0,0 +1,27 @@
import QtQuick
import "../../widgets/" as Widgets
Item {
id: root
required property var popup
Widgets.FontIconButton {
id: button
iconName: "volume_up"
anchors.fill: parent
onClicked: {
if (root.popup.content == volumeMenu) {
root.popup.hide();
return;
}
root.popup.set(this, volumeMenu);
root.popup.show();
}
}
VolumeControl {
id: volumeMenu
}
}

View file

@ -0,0 +1,48 @@
import QtQuick
import ".."
MaterialButton {
id: root
property real implicitSize
property string iconName: ""
property string activeIconColor: ShellSettings.colors["inverse_primary"]
property string inactiveIconColor: ShellSettings.colors["inverse_surface"]
implicitWidth: this.implicitSize
implicitHeight: this.implicitSize
Text {
id: textIcon
text: root.iconName
renderType: Text.NativeRendering
textFormat: Text.PlainText
color: root.containsMouse || root.checked ? root.activeIconColor : root.inactiveIconColor
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
anchors.fill: parent
font {
family: "Material Symbols Outlined"
pointSize: Math.max(parent.height * 0.60, 11)
variableAxes: {
"FILL": fill
}
}
property real fill: !root.containsMouse && !root.checked ? 0 : 1
Behavior on fill {
NumberAnimation {
duration: 200
}
}
Behavior on color {
ColorAnimation {
duration: 200
}
}
}
}

View file

@ -1,3 +1,24 @@
import QtQuick
import ".."
Rectangle {}
MouseArea {
id: root
hoverEnabled: true
property real radius: width / 2
property bool checked: false
property var activeColor: ShellSettings.colors["primary"]
property var inactiveColor: "transparent"
Rectangle {
color: root.containsMouse || root.checked ? root.activeColor : root.inactiveColor
radius: root.radius
anchors.fill: parent
Behavior on color {
ColorAnimation {
duration: 200
}
}
}
}

105
widgets/MaterialSlider.qml Normal file
View file

@ -0,0 +1,105 @@
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Controls
import Qt5Compat.GraphicalEffects
import ".."
Slider {
id: root
value: 0.5
from: 0.0
to: 1.0
property string text
property Component icon
background: Rectangle {
id: background
implicitWidth: parent.width
implicitHeight: parent.height
width: root.availableWidth
height: implicitHeight
x: root.leftPadding
y: root.topPadding + root.availableHeight / 2 - height / 2
z: 0
color: ShellSettings.colors["surface_container_highest"]
radius: height / 2
layer.enabled: true
layer.effect: OpacityMask {
maskSource: Rectangle {
width: background.implicitWidth
height: background.implicitHeight
radius: background.radius
color: "black"
}
}
Rectangle {
id: visualPos
width: root.visualPosition * (root.availableWidth - root.height) + (root.height / 2)
height: parent.height
color: ShellSettings.colors["primary"]
}
Text {
id: sliderText
text: root.text
visible: text !== ""
color: ShellSettings.colors["inverse_primary"]
horizontalAlignment: Text.AlignLeft
verticalAlignment: Text.AlignVCenter
elide: Text.ElideRight
font {
pointSize: Math.max(handle.implicitHeight * 0.35, 11)
}
anchors {
top: parent.top
bottom: parent.bottom
left: {
let visualWidth = (root.visualPosition * root.availableWidth);
if ((visualWidth / root.availableWidth) < 0.5)
return visualPos.right;
else
return parent.left;
}
right: {
let visualWidth = (root.visualPosition * root.availableWidth);
if ((visualWidth / root.availableWidth) > 0.5)
return visualPos.right;
else
return parent.right;
}
leftMargin: 20
rightMargin: 20
}
}
}
handle: Rectangle {
id: handle
color: ShellSettings.colors["primary"]
implicitWidth: root.height
implicitHeight: root.height
radius: width / 2
x: root.leftPadding + root.visualPosition * (root.availableWidth - width)
y: root.topPadding + root.availableHeight / 2 - height / 2
// icon maybe
Loader {
active: root.icon !== undefined
sourceComponent: root.icon
anchors {
fill: parent
margins: 2
}
}
}
}

View file

@ -1,204 +0,0 @@
import QtQuick
import Qt5Compat.GraphicalEffects
import Quickshell
import Quickshell.Widgets
import ".."
Item {
id: root
property string icon
property bool enabled: true
property bool highlight: true
property bool clickable: false
property bool useMask: true
property bool useVariableFill: false
property real size: 18
property real outerSize: size
property color hoverColor: ShellSettings.colors["inverse_primary"]
property color iconColor: ShellSettings.colors["primary"]
property real buttonRadius: 6
property real padding: 4
property real fillValue: 0.0
property real fillTarget: 0.0
property real fillHover: 1.0
property real fillNormal: 0.0
property int fillDuration: 300
signal clicked(var mouse)
implicitWidth: outerSize + (padding * 2)
implicitHeight: outerSize + (padding * 2)
Rectangle {
id: backgroundRect
anchors.fill: parent
radius: root.buttonRadius
color: mouseArea.containsMouse && root.highlight ? root.hoverColor : "transparent"
visible: root.highlight
Behavior on color {
ColorAnimation {
duration: 200
easing.type: Easing.InOutCubic
}
}
}
NumberAnimation {
id: fillAnimation
target: root
property: "fillValue"
duration: root.fillDuration
easing.type: Easing.OutCubic
to: root.fillTarget
}
Text {
id: variableFillIcon
visible: root.useVariableFill
anchors.centerIn: parent
font.family: "Material Symbols Outlined"
renderType: Text.NativeRendering
textFormat: Text.PlainText
font.pointSize: root.size * 0.8
text: root.icon
color: root.enabled ?? root.iconColor
opacity: root.enabled ? 1.0 : 0.5
font.variableAxes: {
"FILL": root.fillValue.toFixed(1)
}
Behavior on color {
ColorAnimation {
duration: 200
easing.type: Easing.InOutCubic
}
}
}
Item {
id: maskedIcon
visible: root.useMask && !root.useVariableFill
anchors.centerIn: parent
width: root.size
height: root.size
smooth: true
layer.enabled: true
layer.effect: OpacityMask {
source: Rectangle {
width: root.size
height: root.size
color: "white"
}
maskSource: IconImage {
mipmap: true
implicitSize: root.size
source: Quickshell.iconPath(root.icon)
opacity: root.enabled ? 1.0 : 0.5
smooth: true
}
}
transform: Translate {
x: mouseArea.containsMouse ? -1 : 0
y: mouseArea.containsMouse ? -2 : 0
Behavior on x {
NumberAnimation {
duration: 450
easing.type: Easing.OutElastic
easing.amplitude: 1.0
easing.period: 0.3
}
}
Behavior on y {
NumberAnimation {
duration: 450
easing.type: Easing.OutElastic
easing.amplitude: 1.0
easing.period: 0.3
}
}
}
Rectangle {
anchors.fill: parent
color: root.enabled ?? root.iconColor
Behavior on color {
ColorAnimation {
duration: 200
easing.type: Easing.InOutCubic
}
}
}
}
IconImage {
id: unmaskedIcon
visible: !root.useMask && !root.useVariableFill
anchors.centerIn: parent
source: Quickshell.iconPath(root.icon)
implicitSize: root.size
opacity: root.enabled ? 1.0 : 0.5
transform: Translate {
x: mouseArea.containsMouse ? -1 : 0
y: mouseArea.containsMouse ? -2 : 0
Behavior on x {
NumberAnimation {
duration: 350
easing.type: Easing.OutElastic
easing.amplitude: 1.0
easing.period: 0.6
}
}
Behavior on y {
NumberAnimation {
duration: 350
easing.type: Easing.OutElastic
easing.amplitude: 1.0
easing.period: 0.6
}
}
}
Behavior on opacity {
NumberAnimation {
duration: 200
easing.type: Easing.InOutCubic
}
}
}
MouseArea {
id: mouseArea
anchors.fill: parent
cursorShape: root.clickable ? Qt.PointingHandCursor : ""
enabled: root.clickable && root.enabled
hoverEnabled: true
acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
onClicked: function (mouse) {
root.clicked(mouse);
}
onContainsMouseChanged: {
if (root.useVariableFill) {
root.fillTarget = containsMouse ? root.fillHover : root.fillNormal;
fillAnimation.restart();
}
}
}
Component.onCompleted: {
if (useVariableFill) {
fillValue = fillNormal;
}
}
}