first commit
This commit is contained in:
8
README.md
Normal file
8
README.md
Normal file
@@ -0,0 +1,8 @@
|
||||
# Quickshell Dots
|
||||
|
||||
**Warning:** I used AI to create the theme styling and pass many variables to `config.qml`.
|
||||
Additionally, I used AI to fix several bugs, which suggests the code may not be very readable and could be broken in some aspects.
|
||||
I am currently identifying and cleaning up bugs and bad code as I find them. (im not qml expert)
|
||||
|
||||
|
||||
Just git clone this repo into .config/quickshell and it will work.
|
||||
179
bar/Bar.qml
Normal file
179
bar/Bar.qml
Normal file
@@ -0,0 +1,179 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Wayland
|
||||
|
||||
import "../config" as Cfg
|
||||
|
||||
PanelWindow {
|
||||
id: barWindow
|
||||
|
||||
required property var modelData
|
||||
property bool isTop
|
||||
|
||||
readonly property var cfg: Cfg.Config
|
||||
|
||||
color: "transparent"
|
||||
anchors {
|
||||
top: isTop
|
||||
bottom: !isTop
|
||||
left: true
|
||||
right: true
|
||||
}
|
||||
implicitHeight: cfg.barHeight
|
||||
|
||||
WlrLayershell.layer: WlrLayer.Top
|
||||
WlrLayershell.namespace: "main-shell-bar"
|
||||
WlrLayershell.exclusiveZone: cfg.barHeight
|
||||
|
||||
Item {
|
||||
id: animatedWrapper
|
||||
anchors.fill: parent
|
||||
|
||||
opacity: 0
|
||||
scale: 0.95
|
||||
y: barWindow.isTop ? -20 : 20
|
||||
|
||||
Component.onCompleted: barEntrance.start()
|
||||
|
||||
ParallelAnimation {
|
||||
id: barEntrance
|
||||
|
||||
NumberAnimation {
|
||||
target: animatedWrapper
|
||||
property: "opacity"
|
||||
from: 0; to: 1
|
||||
duration: 400
|
||||
easing.type: Easing.OutCubic
|
||||
}
|
||||
|
||||
NumberAnimation {
|
||||
target: animatedWrapper
|
||||
property: "scale"
|
||||
from: 0.95; to: 1.0
|
||||
duration: 500
|
||||
easing.type: Easing.OutBack
|
||||
}
|
||||
|
||||
NumberAnimation {
|
||||
target: animatedWrapper
|
||||
property: "y"
|
||||
to: 0
|
||||
duration: 600
|
||||
easing.type: Easing.OutExpo
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
id: barContent
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: cfg.margin
|
||||
anchors.rightMargin: cfg.margin
|
||||
|
||||
function bindModule(item, side) {
|
||||
item.height = Qt.binding(() => barContent.height - 12)
|
||||
|
||||
const props = {
|
||||
"panelRadius": cfg.radius / 2,
|
||||
"borderWidth": cfg.panelBorderWidth,
|
||||
"isTop": barWindow.isTop,
|
||||
"targetScreen": barWindow.modelData,
|
||||
"barHeight": cfg.barHeight,
|
||||
"barMargin": cfg.margin,
|
||||
"barSide": side,
|
||||
"menuWindow": barWindow
|
||||
}
|
||||
for (let prop in props) {
|
||||
if (prop in item) item[prop] = props[prop]
|
||||
}
|
||||
}
|
||||
|
||||
readonly property real sideMaxWidth: Math.max(0,
|
||||
(width - centerRow.implicitWidth - 2 * cfg.panelSpacing) / 2)
|
||||
|
||||
Item {
|
||||
id: leftContainer
|
||||
anchors.left: parent.left
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
height: parent.height
|
||||
width: Math.max(0, Math.min(leftRow.implicitWidth, barContent.sideMaxWidth))
|
||||
clip: true
|
||||
|
||||
Row {
|
||||
id: leftRow
|
||||
anchors.left: parent.left
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: cfg.moduleSpacing
|
||||
transformOrigin: Item.Left
|
||||
scale: leftContainer.width / Math.max(leftContainer.width, implicitWidth)
|
||||
|
||||
Repeater {
|
||||
model: cfg.leftModules
|
||||
Loader {
|
||||
required property string modelData
|
||||
source: Qt.resolvedUrl("modules/" + modelData + ".qml")
|
||||
onLoaded: barContent.bindModule(item, "left")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
id: rightContainer
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
height: parent.height
|
||||
width: Math.max(0, Math.min(rightRow.implicitWidth, barContent.sideMaxWidth))
|
||||
clip: true
|
||||
|
||||
Row {
|
||||
id: rightRow
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: cfg.moduleSpacing
|
||||
transformOrigin: Item.Right
|
||||
scale: rightContainer.width / Math.max(rightContainer.width, implicitWidth)
|
||||
|
||||
Repeater {
|
||||
model: cfg.rightModules
|
||||
Loader {
|
||||
required property string modelData
|
||||
source: Qt.resolvedUrl("modules/" + modelData + ".qml")
|
||||
onLoaded: barContent.bindModule(item, "right")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
id: centerContainer
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
height: parent.height
|
||||
width: Math.max(0, Math.min(
|
||||
centerRow.implicitWidth,
|
||||
barContent.width
|
||||
- 2 * (Math.max(leftContainer.width, rightContainer.width)
|
||||
+ cfg.panelSpacing)
|
||||
))
|
||||
x: (barContent.width - width) / 2
|
||||
clip: true
|
||||
|
||||
Row {
|
||||
id: centerRow
|
||||
anchors.centerIn: parent
|
||||
spacing: cfg.moduleSpacing
|
||||
transformOrigin: Item.Center
|
||||
scale: centerContainer.width / Math.max(centerContainer.width, implicitWidth)
|
||||
|
||||
Repeater {
|
||||
model: cfg.centerModules
|
||||
Loader {
|
||||
required property string modelData
|
||||
source: Qt.resolvedUrl("modules/" + modelData + ".qml")
|
||||
onLoaded: barContent.bindModule(item, "center")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
91
bar/modules/ArchLogo.qml
Normal file
91
bar/modules/ArchLogo.qml
Normal file
@@ -0,0 +1,91 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
|
||||
import "../../config" as Cfg
|
||||
|
||||
Rectangle {
|
||||
id: root
|
||||
|
||||
property real panelRadius: Cfg.Config.radius / 2
|
||||
property int borderWidth: Cfg.Config.panelBorderWidth
|
||||
|
||||
property bool isTop: true
|
||||
property var targetScreen: null
|
||||
property int barHeight: Cfg.Config.barHeight
|
||||
property int barMargin: Cfg.Config.margin
|
||||
|
||||
readonly property var t: Cfg.Config.theme
|
||||
|
||||
property var launcher: null
|
||||
|
||||
readonly property bool launcherActive: !!launcher && launcher.isOpen
|
||||
readonly property bool isHovered: hoverHandler.hovered
|
||||
|
||||
property real spinTarget: 0
|
||||
|
||||
onTargetScreenChanged: Qt.callLater(() => {
|
||||
launcher = (targetScreen && targetScreen.launcher) ? targetScreen.launcher : null
|
||||
})
|
||||
|
||||
onLauncherActiveChanged: {
|
||||
spinTarget += launcherActive ? 360 : -360
|
||||
if (launcherActive && !clickAnim.running)
|
||||
clickAnim.restart()
|
||||
}
|
||||
|
||||
width: Cfg.Config.logoIconSize * 2 + 4
|
||||
height: parent.height
|
||||
|
||||
color: isHovered ? t.archLogoBgHover : t.archLogoBg
|
||||
radius: panelRadius
|
||||
|
||||
border.color: launcherActive ? t.archLogoBorderActive : t.archLogoBorder
|
||||
border.width: borderWidth
|
||||
|
||||
Behavior on color { ColorAnimation { duration: 250 } }
|
||||
Behavior on border.color { ColorAnimation { duration: 250 } }
|
||||
|
||||
HoverHandler { id: hoverHandler }
|
||||
|
||||
TapHandler {
|
||||
onTapped: {
|
||||
clickAnim.restart()
|
||||
if (root.targetScreen && root.targetScreen.launcher) {
|
||||
root.targetScreen.launcher.toggle()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SequentialAnimation {
|
||||
id: clickAnim
|
||||
NumberAnimation { target: logoText; property: "scale"; from: 1.0; to: 0.7; duration: 60; easing.type: Easing.OutQuad }
|
||||
NumberAnimation { target: logoText; property: "scale"; from: 0.7; to: 1.2; duration: 150; easing.type: Easing.OutBack }
|
||||
NumberAnimation { target: logoText; property: "scale"; to: 1.0; duration: 100 }
|
||||
}
|
||||
|
||||
Text {
|
||||
id: logoText
|
||||
anchors.centerIn: parent
|
||||
text: Cfg.Config.logoIcon
|
||||
|
||||
color: launcherActive ? t.archLogoIconActive : t.archLogoIcon
|
||||
font.pixelSize: Cfg.Config.logoIconSize
|
||||
|
||||
Behavior on color { ColorAnimation { duration: 250 } }
|
||||
|
||||
scale: clickAnim.running ? logoText.scale : 1.0
|
||||
Behavior on scale {
|
||||
enabled: !clickAnim.running
|
||||
NumberAnimation { duration: 250; easing.type: Easing.OutBack }
|
||||
}
|
||||
|
||||
rotation: root.spinTarget
|
||||
Behavior on rotation {
|
||||
NumberAnimation {
|
||||
duration: 600
|
||||
easing.type: Easing.InOutBack
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
331
bar/modules/Clock.qml
Normal file
331
bar/modules/Clock.qml
Normal file
@@ -0,0 +1,331 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import Quickshell.Wayland
|
||||
|
||||
import "../../config" as Cfg
|
||||
|
||||
Rectangle {
|
||||
id: root
|
||||
|
||||
property real panelRadius: Cfg.Config.radius / 2
|
||||
property int borderWidth: Cfg.Config.panelBorderWidth
|
||||
|
||||
property bool isTop: true
|
||||
property var targetScreen: null
|
||||
property int barHeight: Cfg.Config.barHeight
|
||||
property int barMargin: Cfg.Config.margin
|
||||
|
||||
readonly property var t: Cfg.Config.theme
|
||||
|
||||
readonly property int tzOffsetHours: -timeManager.now.getTimezoneOffset() / 60
|
||||
|
||||
width: clockLayout.implicitWidth + 24
|
||||
height: Cfg.Config.moduleHeight
|
||||
radius: panelRadius
|
||||
|
||||
color: hoverHandler.hovered ? t.clockBgHover : t.clockBg
|
||||
border.color: hoverHandler.hovered ? t.clockBorderHover : t.clockBorder
|
||||
border.width: borderWidth
|
||||
|
||||
anchors.right: parent.right
|
||||
|
||||
Behavior on width { NumberAnimation { duration: 250; easing.type: Easing.OutCubic } }
|
||||
Behavior on color { ColorAnimation { duration: 150 } }
|
||||
Behavior on border.color { ColorAnimation { duration: 150 } }
|
||||
|
||||
HoverHandler {
|
||||
id: hoverHandler
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
}
|
||||
|
||||
TapHandler {
|
||||
onTapped: calendarPopup.toggle()
|
||||
}
|
||||
|
||||
Item {
|
||||
id: timeManager
|
||||
property date now: new Date()
|
||||
Timer {
|
||||
interval: 1000; running: true; repeat: true
|
||||
onTriggered: timeManager.now = new Date()
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
id: clockLayout
|
||||
anchors.centerIn: parent
|
||||
spacing: 0
|
||||
|
||||
Text {
|
||||
text: ""
|
||||
color: root.t.clockIcon
|
||||
font.pixelSize: 14
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
Layout.rightMargin: 8
|
||||
}
|
||||
|
||||
Text {
|
||||
text: timeManager.now.toLocaleTimeString(Qt.locale(), "HH:mm")
|
||||
color: root.t.clockTime
|
||||
font.pixelSize: 13
|
||||
font.bold: true
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
}
|
||||
|
||||
Text {
|
||||
id: secondsText
|
||||
text: ":" + timeManager.now.toLocaleTimeString(Qt.locale(), "ss")
|
||||
color: root.t.clockSeconds
|
||||
font.pixelSize: 13
|
||||
font.bold: true
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
|
||||
opacity: (hoverHandler.hovered || calendarPopup.popupOpen) ? 1.0 : 0.0
|
||||
visible: opacity > 0
|
||||
Layout.preferredWidth: (hoverHandler.hovered || calendarPopup.popupOpen) ? implicitWidth : 0
|
||||
clip: true
|
||||
|
||||
Behavior on opacity { NumberAnimation { duration: 200 } }
|
||||
Behavior on Layout.preferredWidth { NumberAnimation { duration: 200; easing.type: Easing.OutCubic } }
|
||||
}
|
||||
}
|
||||
|
||||
PanelWindow {
|
||||
id: calendarPopup
|
||||
screen: root.targetScreen
|
||||
visible: false
|
||||
color: "transparent"
|
||||
|
||||
property bool popupOpen: false
|
||||
|
||||
function open() { hideTimer.stop(); visible = true; popupOpen = true }
|
||||
function close() { popupOpen = false; hideTimer.restart() }
|
||||
function toggle() { popupOpen ? close() : open() }
|
||||
|
||||
Timer {
|
||||
id: hideTimer
|
||||
interval: Cfg.Config.popupAnimDuration + 70
|
||||
onTriggered: calendarPopup.visible = false
|
||||
}
|
||||
|
||||
anchors { top: true; bottom: true; left: true; right: true }
|
||||
WlrLayershell.layer: WlrLayer.Top
|
||||
WlrLayershell.namespace: "main-shell-calendar"
|
||||
WlrLayershell.exclusiveZone: -1
|
||||
mask: popupOpen ? null : _noInput
|
||||
Region { id: _noInput }
|
||||
|
||||
TapHandler { onTapped: calendarPopup.close() }
|
||||
|
||||
Item {
|
||||
id: calClipContainer
|
||||
readonly property int cardW: 284
|
||||
readonly property int screenPad: root.barMargin + 10
|
||||
|
||||
x: {
|
||||
let centerX = root.mapToItem(null, 0, 0).x + root.width / 2
|
||||
let desiredX = centerX - cardW / 2
|
||||
return Math.max(screenPad, Math.min(desiredX, parent.width - cardW - screenPad))
|
||||
}
|
||||
width: cardW
|
||||
|
||||
anchors.top: root.isTop ? parent.top : undefined
|
||||
anchors.bottom: root.isTop ? undefined : parent.bottom
|
||||
anchors.topMargin: root.isTop ? (root.barHeight + 10) : 0
|
||||
anchors.bottomMargin: root.isTop ? 0 : (root.barHeight + 10)
|
||||
height: parent.height - root.barHeight - 10
|
||||
|
||||
clip: true
|
||||
|
||||
Rectangle {
|
||||
id: card
|
||||
width: parent.width
|
||||
height: cardCol.implicitHeight + 24
|
||||
|
||||
anchors.top: root.isTop ? parent.top : undefined
|
||||
anchors.bottom: root.isTop ? undefined : parent.bottom
|
||||
|
||||
transform: Translate {
|
||||
y: calendarPopup.popupOpen ? 0 : (root.isTop ? -card.height : card.height)
|
||||
|
||||
Behavior on y {
|
||||
NumberAnimation {
|
||||
duration: Cfg.Config.popupAnimDuration;
|
||||
easing.type: Easing.OutExpo
|
||||
}
|
||||
enabled: calendarPopup.visible
|
||||
}
|
||||
}
|
||||
|
||||
opacity: calendarPopup.popupOpen ? 1.0 : 0.0
|
||||
Behavior on opacity { NumberAnimation { duration: 200 } }
|
||||
|
||||
color: root.t.calCardBg
|
||||
radius: Cfg.Config.popupRadius
|
||||
border.color: root.t.calCardBorder
|
||||
border.width: Cfg.Config.popupBorderWidth
|
||||
layer.enabled: true
|
||||
|
||||
MouseArea { anchors.fill: parent; propagateComposedEvents: false }
|
||||
|
||||
ColumnLayout {
|
||||
id: cardCol
|
||||
anchors { left: parent.left; right: parent.right; top: parent.top }
|
||||
anchors.margins: 16
|
||||
anchors.topMargin: 18
|
||||
spacing: 14
|
||||
|
||||
ColumnLayout {
|
||||
spacing: 2
|
||||
Layout.fillWidth: true
|
||||
|
||||
Text {
|
||||
text: timeManager.now.toLocaleDateString(Qt.locale(), "dddd")
|
||||
color: root.t.clockPopupDim
|
||||
font.pixelSize: 12
|
||||
font.capitalization: Font.AllUppercase
|
||||
Layout.fillWidth: true
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
}
|
||||
Text {
|
||||
text: timeManager.now.toLocaleDateString(Qt.locale(), "d MMMM yyyy")
|
||||
color: root.t.clockPopupHeader
|
||||
font.pixelSize: 22
|
||||
font.bold: true
|
||||
Layout.fillWidth: true
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle { Layout.fillWidth: true; height: 1; color: root.t.calSeparator }
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
|
||||
Text {
|
||||
text: "‹"; color: root.t.clockPopupDim; font.pixelSize: 18; font.bold: true
|
||||
MouseArea {
|
||||
anchors.fill: parent; cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
cal.displayMonth -= 1
|
||||
if (cal.displayMonth < 0) { cal.displayMonth = 11; cal.displayYear -= 1 }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Text {
|
||||
text: new Date(cal.displayYear, cal.displayMonth, 1).toLocaleDateString(Qt.locale(), "MMMM yyyy")
|
||||
color: root.t.clockPopupText
|
||||
font.pixelSize: 13; font.bold: true
|
||||
Layout.fillWidth: true; horizontalAlignment: Text.AlignHCenter
|
||||
}
|
||||
|
||||
Text {
|
||||
text: "›"; color: root.t.clockPopupDim; font.pixelSize: 18; font.bold: true
|
||||
MouseArea {
|
||||
anchors.fill: parent; cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
cal.displayMonth += 1
|
||||
if (cal.displayMonth > 11) { cal.displayMonth = 0; cal.displayYear += 1 }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
id: cal
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: gridCol.implicitHeight
|
||||
property int displayYear: timeManager.now.getFullYear()
|
||||
property int displayMonth: timeManager.now.getMonth()
|
||||
|
||||
Connections {
|
||||
target: calendarPopup
|
||||
function onPopupOpenChanged() {
|
||||
if (calendarPopup.popupOpen) {
|
||||
cal.displayYear = timeManager.now.getFullYear()
|
||||
cal.displayMonth = timeManager.now.getMonth()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
id: gridCol
|
||||
anchors.fill: parent
|
||||
spacing: 2
|
||||
|
||||
Row {
|
||||
spacing: 0
|
||||
Repeater {
|
||||
model: ["Mo","Tu","We","Th","Fr","Sa","Su"]
|
||||
Text {
|
||||
width: (card.width - 32) / 7
|
||||
text: modelData; color: root.t.clockPopupDim
|
||||
font.pixelSize: 11; horizontalAlignment: Text.AlignHCenter
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: 6
|
||||
Row {
|
||||
id: weekRow
|
||||
property int weekIndex: index
|
||||
Repeater {
|
||||
model: 7
|
||||
delegate: Item {
|
||||
width: (card.width - 32) / 7; height: 26
|
||||
readonly property int dayNum: (weekRow.weekIndex * 7 + index) - ((new Date(cal.displayYear, cal.displayMonth, 1).getDay() + 6) % 7) + 1
|
||||
readonly property bool inMonth: dayNum >= 1 && dayNum <= new Date(cal.displayYear, cal.displayMonth + 1, 0).getDate()
|
||||
readonly property bool isToday: inMonth && dayNum === timeManager.now.getDate() && cal.displayYear === timeManager.now.getFullYear() && cal.displayMonth === timeManager.now.getMonth()
|
||||
|
||||
Rectangle {
|
||||
anchors.centerIn: parent; width: 22; height: 22; radius: 11
|
||||
color: isToday ? root.t.accent : "transparent"
|
||||
visible: inMonth
|
||||
}
|
||||
|
||||
Text {
|
||||
anchors.fill: parent
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
text: inMonth ? dayNum : ""
|
||||
color: isToday ? root.t.todayText : (index >= 5 ? root.t.clockPopupDim : root.t.clockPopupText)
|
||||
font.pixelSize: 12; font.bold: isToday
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle { Layout.fillWidth: true; height: 1; color: root.t.calSeparator }
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true; spacing: 12
|
||||
Text { text: ""; color: root.t.clockIcon; font.pixelSize: 16 }
|
||||
ColumnLayout {
|
||||
spacing: 0; Layout.fillWidth: true
|
||||
Text {
|
||||
text: timeManager.now.toLocaleTimeString(Qt.locale(), "t") || "UTC"
|
||||
color: root.t.clockPopupText; font.pixelSize: 12; font.bold: true
|
||||
}
|
||||
Text {
|
||||
text: "UTC " + (root.tzOffsetHours >= 0 ? "+" : "") + root.tzOffsetHours
|
||||
color: root.t.clockPopupDim; font.pixelSize: 10
|
||||
}
|
||||
}
|
||||
Text {
|
||||
text: timeManager.now.toLocaleTimeString(Qt.locale(), "HH:mm:ss")
|
||||
color: root.t.clockTime; font.pixelSize: 20; font.bold: true; font.family: "monospace"
|
||||
}
|
||||
}
|
||||
|
||||
Item { height: 4 }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
357
bar/modules/Notifications.qml
Normal file
357
bar/modules/Notifications.qml
Normal file
@@ -0,0 +1,357 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import QtQuick.Controls
|
||||
import Quickshell
|
||||
import Quickshell.Wayland
|
||||
import Quickshell.Services.Notifications
|
||||
|
||||
import "../../config" as Cfg
|
||||
import "../../notifications" as Notif
|
||||
|
||||
Rectangle {
|
||||
id: root
|
||||
|
||||
property real panelRadius: Cfg.Config.radius / 2
|
||||
property int borderWidth: Cfg.Config.panelBorderWidth
|
||||
property bool isTop: true
|
||||
property var targetScreen: null
|
||||
property int barHeight: Cfg.Config.barHeight
|
||||
property int barMargin: Cfg.Config.margin
|
||||
readonly property var t: Cfg.Config.theme
|
||||
|
||||
property int unreadCount: 0
|
||||
property bool historyOpen: historyPopup.popupOpen
|
||||
|
||||
width: 35
|
||||
height: 40
|
||||
radius: panelRadius
|
||||
|
||||
color: hoverHandler.hovered ? t.notifBgHover : t.notifBg
|
||||
border.color: (hoverHandler.hovered || historyOpen) ? t.notifBorderActive : t.notifBorder
|
||||
border.width: borderWidth
|
||||
|
||||
Behavior on color { ColorAnimation { duration: 150 } }
|
||||
Behavior on border.color { ColorAnimation { duration: 150 } }
|
||||
Behavior on width { NumberAnimation { duration: 200; easing.type: Easing.OutCubic } }
|
||||
|
||||
HoverHandler { id: hoverHandler; cursorShape: Qt.PointingHandCursor }
|
||||
TapHandler {
|
||||
onTapped: {
|
||||
if (historyPopup.popupOpen) {
|
||||
historyPopup.close()
|
||||
} else {
|
||||
root.unreadCount = 0
|
||||
historyPopup.open()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
anchors.centerIn: parent
|
||||
width: Cfg.Config.notifIconSize + 10
|
||||
height: Cfg.Config.notifIconSize + 10
|
||||
|
||||
Text {
|
||||
id: bellIcon
|
||||
anchors.centerIn: parent
|
||||
text: ""
|
||||
color: root.unreadCount > 0 ? t.notifIconActive : t.notifIcon
|
||||
font.pixelSize: Cfg.Config.notifIconSize
|
||||
Behavior on color { ColorAnimation { duration: 150 } }
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
visible: root.unreadCount > 0
|
||||
anchors.top: parent.top
|
||||
anchors.right: parent.right
|
||||
width: Math.max(14, badgeLabel.implicitWidth + 6)
|
||||
height: 14
|
||||
radius: 7
|
||||
color: t.notifBadgeBg
|
||||
|
||||
Behavior on width { NumberAnimation { duration: 150; easing.type: Easing.OutCubic } }
|
||||
|
||||
Text {
|
||||
id: badgeLabel
|
||||
anchors.centerIn: parent
|
||||
text: root.unreadCount > 99 ? "99+" : root.unreadCount.toString()
|
||||
color: t.notifBadgeText
|
||||
font.pixelSize: 8
|
||||
font.bold: true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ListModel { id: notifHistory }
|
||||
ListModel { id: activeToasts }
|
||||
|
||||
function removeToast(toastId) {
|
||||
for (var i = 0; i < activeToasts.count; i++) {
|
||||
if (activeToasts.get(i).toastId === toastId) {
|
||||
activeToasts.remove(i)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function urgencyColor(urgency) {
|
||||
if (urgency === NotificationUrgency.Critical) return t.notifUrgencyCritical
|
||||
if (urgency === NotificationUrgency.Normal) return t.notifUrgencyNormal
|
||||
return t.notifUrgencyLow
|
||||
}
|
||||
|
||||
function urgencyIcon(urgency) {
|
||||
if (urgency === NotificationUrgency.Critical) return ""
|
||||
if (urgency === NotificationUrgency.Normal) return ""
|
||||
return ""
|
||||
}
|
||||
|
||||
NotificationServer {
|
||||
id: notifServer
|
||||
keepOnReload: false
|
||||
actionIconsSupported: true
|
||||
bodyMarkupSupported: true
|
||||
bodySupported: true
|
||||
persistenceSupported: true
|
||||
imageSupported: true
|
||||
|
||||
onNotification: function(notif) {
|
||||
notif.tracked = true
|
||||
var timeStr = new Date().toLocaleTimeString(Qt.locale(), "HH:mm")
|
||||
var toastId = notif.id + "_" + Date.now()
|
||||
|
||||
notifHistory.insert(0, {
|
||||
nid: notif.id,
|
||||
appName: notif.appName || "Unknown",
|
||||
summary: notif.summary || "",
|
||||
body: notif.body || "",
|
||||
urgency: notif.urgency,
|
||||
timeStr: timeStr
|
||||
})
|
||||
while (notifHistory.count > Cfg.Config.notificationHistorySize)
|
||||
notifHistory.remove(notifHistory.count - 1)
|
||||
|
||||
var cfgTimeout = notif.urgency === NotificationUrgency.Critical
|
||||
? Cfg.Config.notifTimeoutCritical
|
||||
: (notif.urgency === NotificationUrgency.Low
|
||||
? Cfg.Config.notifTimeoutLow
|
||||
: Cfg.Config.notifTimeoutNormal)
|
||||
|
||||
var finalTimeout = (notif.expireTimeout > 0) ? notif.expireTimeout : cfgTimeout
|
||||
|
||||
var toastEntry = {
|
||||
toastId: toastId,
|
||||
nid: notif.id,
|
||||
appName: notif.appName || "Unknown",
|
||||
summary: notif.summary || "",
|
||||
body: notif.body || "",
|
||||
urgency: notif.urgency,
|
||||
timeStr: timeStr,
|
||||
timeout: finalTimeout
|
||||
}
|
||||
if (toastLayer.atBottom) activeToasts.append(toastEntry)
|
||||
else activeToasts.insert(0, toastEntry)
|
||||
|
||||
if (!root.historyOpen) root.unreadCount++
|
||||
}
|
||||
}
|
||||
|
||||
PanelWindow {
|
||||
id: toastLayer
|
||||
screen: root.targetScreen
|
||||
visible: activeToasts.count > 0
|
||||
color: "transparent"
|
||||
|
||||
readonly property string pos: Cfg.Config.notificationPosition
|
||||
readonly property bool atRight: pos === "topright" || pos === "bottomright"
|
||||
readonly property bool atBottom: pos === "bottomleft" || pos === "bottomright"
|
||||
|
||||
anchors { top: true; bottom: true; right: atRight; left: !atRight }
|
||||
implicitWidth: 350 + root.barMargin + 4
|
||||
|
||||
WlrLayershell.layer: WlrLayer.Overlay
|
||||
WlrLayershell.namespace: "main-shell-notifications-toasts"
|
||||
WlrLayershell.exclusiveZone: -1
|
||||
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
|
||||
|
||||
mask: Region { item: toastCol }
|
||||
|
||||
readonly property bool shareEdge: (root.isTop && !atBottom) || (!root.isTop && atBottom)
|
||||
readonly property int edgeGap: shareEdge ? root.barMargin + root.barHeight + 10 : root.barMargin + 10
|
||||
|
||||
Column {
|
||||
id: toastCol
|
||||
x: !toastLayer.atRight ? 4 : 4
|
||||
y: !toastLayer.atBottom ? toastLayer.edgeGap : parent.height - toastLayer.edgeGap - toastCol.height
|
||||
spacing: 8
|
||||
width: 350
|
||||
|
||||
Repeater {
|
||||
model: activeToasts
|
||||
Notif.NotificationCard {
|
||||
width: toastCol.width
|
||||
toastId: model.toastId
|
||||
appName: model.appName
|
||||
summary: model.summary
|
||||
body: model.body
|
||||
urgency: model.urgency
|
||||
timeStr: model.timeStr
|
||||
timeout: model.timeout
|
||||
t: root.t
|
||||
urgencyColor: root.urgencyColor
|
||||
urgencyIcon: root.urgencyIcon
|
||||
onDismissed: (id) => root.removeToast(id)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
PanelWindow {
|
||||
id: historyPopup
|
||||
screen: root.targetScreen
|
||||
visible: false
|
||||
color: "transparent"
|
||||
|
||||
property bool popupOpen: false
|
||||
|
||||
function open() { hideTimer.stop(); visible = true; popupOpen = true }
|
||||
function close() { popupOpen = false; hideTimer.restart() }
|
||||
|
||||
Timer {
|
||||
id: hideTimer
|
||||
interval: Cfg.Config.popupAnimDuration + 70
|
||||
onTriggered: historyPopup.visible = false
|
||||
}
|
||||
|
||||
anchors { top: true; bottom: true; left: true; right: true }
|
||||
WlrLayershell.layer: WlrLayer.Top
|
||||
WlrLayershell.namespace: "main-shell-notifications-history"
|
||||
WlrLayershell.exclusiveZone: -1
|
||||
mask: popupOpen ? null : _noInput
|
||||
Region { id: _noInput }
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
onClicked: historyPopup.close()
|
||||
}
|
||||
|
||||
Item {
|
||||
id: histClipContainer
|
||||
readonly property int cardW: 340
|
||||
readonly property int screenPad: root.barMargin + 10
|
||||
|
||||
x: {
|
||||
var cx = root.mapToItem(null, 0, 0).x + root.width / 2
|
||||
return Math.max(screenPad, Math.min(cx - cardW / 2, parent.width - cardW - screenPad))
|
||||
}
|
||||
width: cardW
|
||||
|
||||
anchors.top: root.isTop ? parent.top : undefined
|
||||
anchors.bottom: root.isTop ? undefined : parent.bottom
|
||||
anchors.topMargin: root.isTop ? (root.barHeight + 10) : 0
|
||||
anchors.bottomMargin: root.isTop ? 0 : (root.barHeight + 10)
|
||||
height: parent.height - root.barHeight - 10
|
||||
|
||||
clip: true
|
||||
|
||||
Rectangle {
|
||||
id: historyCard
|
||||
width: parent.width
|
||||
height: Math.min(histHeader.implicitHeight + 56 + (notifHistory.count > 0 ? notifHistory.count * 76 : 90), 520)
|
||||
|
||||
anchors.top: root.isTop ? parent.top : undefined
|
||||
anchors.bottom: root.isTop ? undefined : parent.bottom
|
||||
|
||||
transform: Translate {
|
||||
y: historyPopup.popupOpen ? 0 : (root.isTop ? -historyCard.height : historyCard.height)
|
||||
|
||||
Behavior on y {
|
||||
NumberAnimation {
|
||||
duration: Cfg.Config.popupAnimDuration;
|
||||
easing.type: Easing.OutExpo
|
||||
}
|
||||
enabled: historyPopup.visible
|
||||
}
|
||||
}
|
||||
|
||||
opacity: historyPopup.popupOpen ? 1.0 : 0.0
|
||||
Behavior on opacity { NumberAnimation { duration: 200 } }
|
||||
|
||||
color: t.notifHistoryBg
|
||||
radius: Cfg.Config.popupRadius
|
||||
border.color: t.notifHistoryBorder
|
||||
border.width: Cfg.Config.popupBorderWidth
|
||||
layer.enabled: true
|
||||
|
||||
MouseArea { anchors.fill: parent; propagateComposedEvents: false }
|
||||
|
||||
RowLayout {
|
||||
id: histHeader
|
||||
anchors { left: parent.left; right: parent.right; top: parent.top }
|
||||
anchors.topMargin: 20; anchors.leftMargin: 20; anchors.rightMargin: 20
|
||||
spacing: 10
|
||||
|
||||
Text { text: ""; color: t.notifToastAppName; font.pixelSize: 18; Layout.alignment: Qt.AlignVCenter }
|
||||
Text {
|
||||
text: "Notifications"; color: t.notifToastAppName; font.pixelSize: 15; font.bold: true
|
||||
Layout.fillWidth: true; Layout.alignment: Qt.AlignVCenter
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
visible: notifHistory.count > 0
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
width: clearText.implicitWidth + 16
|
||||
height: 24
|
||||
radius: 12
|
||||
color: clearHover.hovered ? t.notifHistoryHover : "transparent"
|
||||
Behavior on color { ColorAnimation { duration: 150 } }
|
||||
|
||||
Text { id: clearText; anchors.centerIn: parent; text: "Clear all"; color: t.notifToastDim; font.pixelSize: 11 }
|
||||
HoverHandler { id: clearHover; cursorShape: Qt.PointingHandCursor }
|
||||
TapHandler { onTapped: { notifHistory.clear(); root.unreadCount = 0 } }
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: histDivider
|
||||
anchors { left: parent.left; right: parent.right; top: histHeader.bottom }
|
||||
anchors.topMargin: 12; height: 1; color: t.notifSeparator
|
||||
}
|
||||
|
||||
Flickable {
|
||||
id: histFlick
|
||||
anchors { left: parent.left; right: parent.right; top: histDivider.bottom; bottom: parent.bottom }
|
||||
anchors.leftMargin: 8; anchors.rightMargin: 8
|
||||
anchors.topMargin: 6; anchors.bottomMargin: 8; clip: true
|
||||
contentHeight: histCol.implicitHeight
|
||||
flickableDirection: Flickable.VerticalFlick
|
||||
ScrollBar.vertical: ScrollBar { policy: ScrollBar.AsNeeded }
|
||||
|
||||
Column {
|
||||
id: histCol
|
||||
width: histFlick.width; spacing: 2; topPadding: 4; bottomPadding: 4; leftPadding: 8; rightPadding: 8
|
||||
|
||||
Item {
|
||||
width: histCol.width - 16; height: 90; visible: notifHistory.count === 0
|
||||
ColumnLayout {
|
||||
anchors.centerIn: parent; spacing: 8
|
||||
Text { text: ""; color: t.notifHistoryEmpty; font.pixelSize: 30; Layout.alignment: Qt.AlignHCenter }
|
||||
Text { text: "No notifications"; color: t.notifHistoryEmpty; font.pixelSize: 12; Layout.alignment: Qt.AlignHCenter }
|
||||
}
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: notifHistory
|
||||
Notif.HistoryCard {
|
||||
width: histCol.width - 16; x: 8; itemIndex: index
|
||||
appName: model.appName; summary: model.summary; body: model.body; urgency: model.urgency; timeStr: model.timeStr
|
||||
t: root.t; urgencyColor: root.urgencyColor; urgencyIcon: root.urgencyIcon
|
||||
onDismissed: (idx) => notifHistory.remove(idx)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
181
bar/modules/Stats.qml
Normal file
181
bar/modules/Stats.qml
Normal file
@@ -0,0 +1,181 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
|
||||
import "../../config" as Cfg
|
||||
|
||||
Rectangle {
|
||||
id: root
|
||||
|
||||
property real panelRadius: Cfg.Config.radius / 2
|
||||
property int borderWidth: Cfg.Config.panelBorderWidth
|
||||
property int barHeight: Cfg.Config.barHeight
|
||||
property int barMargin: Cfg.Config.margin
|
||||
readonly property var t: Cfg.Config.theme
|
||||
|
||||
width: statsLayout.implicitWidth + 20
|
||||
height: Cfg.Config.moduleHeight
|
||||
color: t.statsBg
|
||||
radius: panelRadius
|
||||
border.color: t.statsBorder
|
||||
border.width: borderWidth
|
||||
|
||||
Behavior on width {
|
||||
NumberAnimation { duration: 250; easing.type: Easing.OutCubic }
|
||||
}
|
||||
|
||||
HoverHandler { cursorShape: Qt.PointingHandCursor }
|
||||
|
||||
property real cpuVal: 0
|
||||
property real memVal: 0
|
||||
property real diskVal: 0
|
||||
|
||||
Timer {
|
||||
interval: 2000
|
||||
running: true
|
||||
repeat: true
|
||||
triggeredOnStart: true
|
||||
onTriggered: {
|
||||
cpuProc.running = true
|
||||
memProc.running = true
|
||||
diskProc.running = true
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: cpuProc
|
||||
command: ["sh", "-c", "top -bn1 | awk '/^%Cpu/ {print 100-$8}'"]
|
||||
stdout: SplitParser {
|
||||
onRead: data => {
|
||||
let val = parseFloat(data.trim())
|
||||
root.cpuVal = isNaN(val) ? 0 : val
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: memProc
|
||||
command: ["sh", "-c", "free | awk '/Mem:/ {print ($2-$7)/$2 * 100}'"]
|
||||
stdout: SplitParser {
|
||||
onRead: data => {
|
||||
let val = parseFloat(data.trim())
|
||||
root.memVal = isNaN(val) ? 0 : val
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: diskProc
|
||||
command: ["sh", "-c", "df / --output=pcent | tail -1 | tr -dc '0-9'"]
|
||||
stdout: SplitParser {
|
||||
onRead: data => {
|
||||
let val = parseFloat(data.trim())
|
||||
root.diskVal = isNaN(val) ? 0 : val
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
component StatRing: Item {
|
||||
id: ring
|
||||
|
||||
property real value: 0.0
|
||||
property string label: ""
|
||||
property color ringColor: Cfg.Config.theme.accent
|
||||
property color trackColor: Cfg.Config.theme.statsTrackColor
|
||||
property color labelColor: Cfg.Config.theme.textMain
|
||||
|
||||
Behavior on value {
|
||||
NumberAnimation {
|
||||
duration: 800
|
||||
easing.type: Easing.OutCubic
|
||||
}
|
||||
}
|
||||
|
||||
readonly property real fraction: Math.max(0, Math.min(value, 100)) / 100
|
||||
|
||||
width: Cfg.Config.statsRingSize
|
||||
height: Cfg.Config.statsRingSize
|
||||
|
||||
onFractionChanged: arc.requestPaint()
|
||||
onRingColorChanged: arc.requestPaint()
|
||||
|
||||
Canvas {
|
||||
id: arc
|
||||
anchors.fill: parent
|
||||
antialiasing: true
|
||||
|
||||
onPaint: {
|
||||
var ctx = getContext("2d")
|
||||
ctx.reset()
|
||||
ctx.clearRect(0, 0, width, height)
|
||||
|
||||
var cx = width / 2
|
||||
var cy = height / 2
|
||||
var lw = 2.8
|
||||
var r = Math.min(cx, cy) - lw / 2 - 0.5
|
||||
|
||||
ctx.beginPath()
|
||||
ctx.arc(cx, cy, r, 0, Math.PI * 2)
|
||||
ctx.strokeStyle = ring.trackColor
|
||||
ctx.lineWidth = lw
|
||||
ctx.stroke()
|
||||
|
||||
if (ring.fraction > 0) {
|
||||
var start = -Math.PI / 2
|
||||
var end = start + (ring.fraction * Math.PI * 2)
|
||||
ctx.beginPath()
|
||||
ctx.arc(cx, cy, r, start, end)
|
||||
ctx.strokeStyle = ring.ringColor
|
||||
ctx.lineWidth = lw
|
||||
ctx.lineCap = "round"
|
||||
ctx.stroke()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
HoverHandler { id: hov }
|
||||
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
text: Cfg.Config.statsShowPercentByDefault
|
||||
? (hov.hovered ? ring.label : Math.round(ring.value) + "%")
|
||||
: (hov.hovered ? Math.round(ring.value) + "%" : ring.label)
|
||||
color: ring.labelColor
|
||||
font.pixelSize: Cfg.Config.statsRingFontSize
|
||||
font.bold: true
|
||||
font.family: "monospace"
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
id: statsLayout
|
||||
anchors.centerIn: parent
|
||||
spacing: 10
|
||||
|
||||
StatRing {
|
||||
value: root.cpuVal
|
||||
label: "cpu"
|
||||
ringColor: root.t.statsCpuColor
|
||||
trackColor: root.t.statsTrackColor
|
||||
labelColor: root.t.statsText
|
||||
}
|
||||
|
||||
StatRing {
|
||||
value: root.memVal
|
||||
label: "ram"
|
||||
ringColor: root.t.statsMemColor
|
||||
trackColor: root.t.statsTrackColor
|
||||
labelColor: root.t.statsText
|
||||
}
|
||||
|
||||
StatRing {
|
||||
value: root.diskVal
|
||||
label: "disk"
|
||||
ringColor: root.t.statsDiskColor
|
||||
trackColor: root.t.statsTrackColor
|
||||
labelColor: root.t.statsText
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
161
bar/modules/Tray.qml
Normal file
161
bar/modules/Tray.qml
Normal file
@@ -0,0 +1,161 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import Quickshell.Wayland
|
||||
import Quickshell.Services.SystemTray
|
||||
import Quickshell.Widgets
|
||||
|
||||
import "../../config" as Cfg
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property real panelRadius: Cfg.Config.radius / 2
|
||||
property int borderWidth: Cfg.Config.panelBorderWidth
|
||||
|
||||
property bool isTop: true
|
||||
property var targetScreen: null
|
||||
property string barSide: "right"
|
||||
|
||||
readonly property var t: Cfg.Config.theme
|
||||
|
||||
property bool expanded: false
|
||||
property bool manuallyExpanded: false
|
||||
property var menuWindow: null
|
||||
|
||||
implicitWidth: bgRect.width
|
||||
implicitHeight: bgRect.height
|
||||
|
||||
Rectangle {
|
||||
id: bgRect
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
height: Cfg.Config.trayHeight
|
||||
width: mainRow.width
|
||||
radius: root.panelRadius
|
||||
|
||||
color: bgHover.hovered ? root.t.trayBgHover : root.t.trayBg
|
||||
border.color: bgHover.hovered ? root.t.trayBorderHover : root.t.trayBorder
|
||||
border.width: root.borderWidth
|
||||
|
||||
Behavior on color { ColorAnimation { duration: 150 } }
|
||||
Behavior on border.color { ColorAnimation { duration: 150 } }
|
||||
|
||||
HoverHandler {
|
||||
id: bgHover
|
||||
onHoveredChanged: {
|
||||
if (!root.manuallyExpanded) {
|
||||
root.expanded = hovered;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
id: mainRow
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
leftPadding: root.expanded && root.barSide === "right" ? 8 : 0
|
||||
rightPadding: root.expanded && root.barSide === "left" ? 8 : 0
|
||||
|
||||
Behavior on leftPadding { NumberAnimation { duration: 200; easing.type: Easing.OutCubic } }
|
||||
Behavior on rightPadding { NumberAnimation { duration: 200; easing.type: Easing.OutCubic } }
|
||||
|
||||
layoutDirection: root.barSide === "left" ? Qt.LeftToRight : Qt.RightToLeft
|
||||
spacing: root.expanded ? 4 : 0
|
||||
|
||||
Behavior on spacing { NumberAnimation { duration: 180; easing.type: Easing.OutCubic } }
|
||||
|
||||
Item {
|
||||
id: toggleArea
|
||||
width: Cfg.Config.trayHeight
|
||||
height: Cfg.Config.trayHeight
|
||||
z: 10
|
||||
|
||||
TapHandler {
|
||||
onTapped: {
|
||||
root.manuallyExpanded = !root.manuallyExpanded;
|
||||
root.expanded = root.manuallyExpanded;
|
||||
}
|
||||
}
|
||||
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
text: root.expanded
|
||||
? (root.barSide === "left" ? "" : "")
|
||||
: (root.barSide === "left" ? "" : "")
|
||||
color: root.t.trayIcon
|
||||
font.pixelSize: Cfg.Config.trayHeight * 0.55
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
id: iconsRow
|
||||
spacing: 6
|
||||
clip: true
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
width: root.expanded ? implicitWidth : 0
|
||||
opacity: root.expanded ? 1 : 0
|
||||
|
||||
Behavior on width { NumberAnimation { duration: 200; easing.type: Easing.OutCubic } }
|
||||
Behavior on opacity { NumberAnimation { duration: 150 } }
|
||||
|
||||
Repeater {
|
||||
model: SystemTray.items
|
||||
|
||||
delegate: MouseArea {
|
||||
id: iconDelegate
|
||||
required property SystemTrayItem modelData
|
||||
property alias item: iconDelegate.modelData
|
||||
|
||||
implicitWidth: Cfg.Config.trayIconSize
|
||||
implicitHeight: Cfg.Config.trayIconSize
|
||||
width: Cfg.Config.trayIconSize
|
||||
height: Cfg.Config.trayIconSize
|
||||
|
||||
acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
|
||||
hoverEnabled: true
|
||||
|
||||
onClicked: function(mouse) {
|
||||
if (mouse.button === Qt.LeftButton) {
|
||||
item.activate()
|
||||
} else if (mouse.button === Qt.MiddleButton) {
|
||||
item.secondaryActivate()
|
||||
} else if (mouse.button === Qt.RightButton) {
|
||||
menuAnchor.open()
|
||||
}
|
||||
}
|
||||
|
||||
onWheel: function(wheel) {
|
||||
wheel.accepted = true
|
||||
item.scroll(wheel.angleDelta.y / 120, false)
|
||||
}
|
||||
|
||||
IconImage {
|
||||
anchors.fill: parent
|
||||
source: item.icon
|
||||
}
|
||||
|
||||
QsMenuAnchor {
|
||||
id: menuAnchor
|
||||
menu: item.menu
|
||||
anchor.window: root.menuWindow
|
||||
anchor.adjustment: PopupAdjustment.Flip
|
||||
anchor.onAnchoring: {
|
||||
if (!root.menuWindow) return
|
||||
const global = iconDelegate.mapToGlobal(0, iconDelegate.height)
|
||||
const local = root.menuWindow.contentItem.mapFromGlobal(
|
||||
global.x, global.y
|
||||
)
|
||||
menuAnchor.anchor.rect = Qt.rect(
|
||||
local.x, local.y,
|
||||
iconDelegate.width, 0
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
530
bar/modules/Volume.qml
Normal file
530
bar/modules/Volume.qml
Normal file
@@ -0,0 +1,530 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import Quickshell.Wayland
|
||||
import Quickshell.Services.Pipewire
|
||||
|
||||
import "../../config" as Cfg
|
||||
|
||||
Rectangle {
|
||||
id: root
|
||||
|
||||
property real panelRadius: Cfg.Config.radius / 2
|
||||
property int borderWidth: Cfg.Config.panelBorderWidth
|
||||
property bool isTop: true
|
||||
property var targetScreen: null
|
||||
property int barHeight: Cfg.Config.barHeight
|
||||
property int barMargin: Cfg.Config.margin
|
||||
|
||||
readonly property var t: Cfg.Config.theme
|
||||
|
||||
property int outputMaxVolume: Cfg.Config.volumeOutputMax
|
||||
property int inputMaxVolume: Cfg.Config.volumeInputMax
|
||||
|
||||
readonly property var sink: Pipewire.defaultAudioSink
|
||||
readonly property var source: Pipewire.defaultAudioSource
|
||||
|
||||
PwObjectTracker {
|
||||
objects: root.sinkNodes.concat(root.sourceNodes)
|
||||
}
|
||||
|
||||
readonly property var sinkNodes: {
|
||||
if (!Pipewire.nodes) return []
|
||||
return Pipewire.nodes.values.filter(n => !n.isStream && n.isSink)
|
||||
}
|
||||
readonly property var sourceNodes: {
|
||||
if (!Pipewire.nodes) return []
|
||||
return Pipewire.nodes.values.filter(n => !n.isStream && !n.isSink)
|
||||
}
|
||||
|
||||
property int _outVol: 0
|
||||
property int _inVol: 0
|
||||
|
||||
readonly property int outputVolume: {
|
||||
if (sink && sink.ready) { _outVol = Math.round(sink.audio.volume * 100); return _outVol }
|
||||
return _outVol
|
||||
}
|
||||
readonly property int inputVolume: {
|
||||
if (source && source.ready) { _inVol = Math.round(source.audio.volume * 100); return _inVol }
|
||||
return _inVol
|
||||
}
|
||||
|
||||
readonly property bool outputMuted: sink && sink.ready ? sink.audio.muted : false
|
||||
readonly property bool inputMuted: source && source.ready ? source.audio.muted : false
|
||||
|
||||
readonly property string outputDeviceName: sink ? (sink.description || sink.nickName || sink.name) : "No device"
|
||||
readonly property string inputDeviceName: source ? (source.description || source.nickName || source.name) : "No device"
|
||||
|
||||
function setOutputVol(pct) { if (sink && sink.ready) sink.audio.volume = pct / 100 }
|
||||
function setInputVol(pct) { if (source && source.ready) source.audio.volume = pct / 100 }
|
||||
function toggleOutputMute() { if (sink && sink.ready) sink.audio.muted = !sink.audio.muted }
|
||||
function toggleInputMute() { if (source && source.ready) source.audio.muted = !source.audio.muted }
|
||||
|
||||
Process {
|
||||
id: defaultSetter
|
||||
running: false
|
||||
property string nodeId: "0"
|
||||
command: ["wpctl", "set-default", nodeId]
|
||||
}
|
||||
function setDefaultDevice(node) {
|
||||
defaultSetter.nodeId = node.id.toString()
|
||||
defaultSetter.running = true
|
||||
}
|
||||
|
||||
readonly property string iconSpeakerHigh: ""
|
||||
readonly property string iconSpeakerMed: ""
|
||||
readonly property string iconSpeakerLow: ""
|
||||
readonly property string iconSpeakerMuted: ""
|
||||
readonly property string iconMic: ""
|
||||
readonly property string iconMicMuted: ""
|
||||
|
||||
function speakerGlyph(muted, vol) {
|
||||
if (muted || vol === 0) return iconSpeakerMuted
|
||||
if (vol > 60) return iconSpeakerHigh
|
||||
if (vol > 25) return iconSpeakerMed
|
||||
return iconSpeakerLow
|
||||
}
|
||||
|
||||
width: chipLayout.implicitWidth + 15
|
||||
height: Cfg.Config.moduleHeight
|
||||
radius: panelRadius
|
||||
|
||||
color: hoverHandler.hovered ? t.volBgHover : t.volBg
|
||||
border.color: (hoverHandler.hovered || volumePopup.popupOpen) ? t.volBorderHover : t.volBorder
|
||||
border.width: borderWidth
|
||||
|
||||
Behavior on color { ColorAnimation { duration: 150 } }
|
||||
Behavior on border.color { ColorAnimation { duration: 150 } }
|
||||
|
||||
HoverHandler { id: hoverHandler; cursorShape: Qt.PointingHandCursor }
|
||||
TapHandler { onTapped: volumePopup.toggle() }
|
||||
|
||||
RowLayout {
|
||||
id: chipLayout
|
||||
anchors.centerIn: parent
|
||||
spacing: 5
|
||||
|
||||
Text {
|
||||
text: root.speakerGlyph(root.outputMuted, root.outputVolume)
|
||||
color: root.outputMuted ? root.t.volIconMuted : root.t.volIcon
|
||||
font.pixelSize: Cfg.Config.volumeIconSize
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
}
|
||||
}
|
||||
|
||||
PanelWindow {
|
||||
id: volumePopup
|
||||
screen: root.targetScreen
|
||||
visible: false
|
||||
color: "transparent"
|
||||
|
||||
property bool popupOpen: false
|
||||
|
||||
function open() { hideTimer.stop(); visible = true; popupOpen = true }
|
||||
function close() { popupOpen = false; hideTimer.restart() }
|
||||
function toggle() { popupOpen ? close() : open() }
|
||||
|
||||
Timer {
|
||||
id: hideTimer
|
||||
interval: Cfg.Config.popupAnimDuration + 70
|
||||
onTriggered: volumePopup.visible = false
|
||||
}
|
||||
|
||||
anchors { top: true; bottom: true; left: true; right: true }
|
||||
WlrLayershell.layer: WlrLayer.Top
|
||||
WlrLayershell.namespace: "main-shell-volume"
|
||||
WlrLayershell.exclusiveZone: -1
|
||||
mask: popupOpen ? null : _noInput
|
||||
Region { id: _noInput }
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
onClicked: volumePopup.close()
|
||||
}
|
||||
|
||||
Item {
|
||||
id: volClipContainer
|
||||
readonly property int cardW: Cfg.Config.volumePopupWidth
|
||||
readonly property int screenPad: root.barMargin + 10
|
||||
|
||||
x: {
|
||||
let cx = root.mapToItem(null, 0, 0).x + root.width / 2
|
||||
return Math.max(screenPad, Math.min(cx - cardW / 2, parent.width - cardW - screenPad))
|
||||
}
|
||||
width: cardW
|
||||
|
||||
anchors.top: root.isTop ? parent.top : undefined
|
||||
anchors.bottom: root.isTop ? undefined : parent.bottom
|
||||
anchors.topMargin: root.isTop ? (root.barHeight + 10) : 0
|
||||
anchors.bottomMargin: root.isTop ? 0 : (root.barHeight + 10)
|
||||
height: parent.height - root.barHeight - 10
|
||||
|
||||
clip: true
|
||||
|
||||
Rectangle {
|
||||
id: volCard
|
||||
width: parent.width
|
||||
height: popupCol.implicitHeight + 32
|
||||
|
||||
anchors.top: root.isTop ? parent.top : undefined
|
||||
anchors.bottom: root.isTop ? undefined : parent.bottom
|
||||
|
||||
transform: Translate {
|
||||
y: volumePopup.popupOpen ? 0 : (root.isTop ? -volCard.height : volCard.height)
|
||||
|
||||
Behavior on y {
|
||||
NumberAnimation {
|
||||
duration: Cfg.Config.popupAnimDuration;
|
||||
easing.type: Easing.OutExpo
|
||||
}
|
||||
enabled: volumePopup.visible
|
||||
}
|
||||
}
|
||||
|
||||
opacity: volumePopup.popupOpen ? 1.0 : 0.0
|
||||
Behavior on opacity { NumberAnimation { duration: 200 } }
|
||||
|
||||
radius: Cfg.Config.popupRadius
|
||||
color: root.t.volPopupBg
|
||||
border.color: root.t.volPopupBorder
|
||||
border.width: Cfg.Config.popupBorderWidth
|
||||
layer.enabled: true
|
||||
|
||||
MouseArea { anchors.fill: parent; propagateComposedEvents: false }
|
||||
|
||||
ColumnLayout {
|
||||
id: popupCol
|
||||
anchors { left: parent.left; right: parent.right; top: parent.top }
|
||||
anchors.margins: 16
|
||||
anchors.topMargin: 18
|
||||
spacing: 14
|
||||
|
||||
VolumeSection {
|
||||
id: outputSection
|
||||
Layout.fillWidth: true
|
||||
label: "Output"
|
||||
icon: root.speakerGlyph(root.outputMuted, root.outputVolume)
|
||||
iconColor: root.t.accent
|
||||
volume: root.outputVolume
|
||||
maxVolume: root.outputMaxVolume
|
||||
muted: root.outputMuted
|
||||
accentColor: root.t.volHandle
|
||||
dimColor: root.t.textDim
|
||||
textColor: root.t.textMain
|
||||
trackColor: root.t.volTrack
|
||||
deviceName: root.outputDeviceName
|
||||
nodes: root.sinkNodes
|
||||
currentNode: root.sink
|
||||
onSliderMoved: vol => root.setOutputVol(vol)
|
||||
onMuteToggled: root.toggleOutputMute()
|
||||
onDeviceSelected: node => root.setDefaultDevice(node)
|
||||
}
|
||||
|
||||
Rectangle { Layout.fillWidth: true; height: 1; color: root.t.separator }
|
||||
|
||||
VolumeSection {
|
||||
id: inputSection
|
||||
Layout.fillWidth: true
|
||||
label: "Input"
|
||||
icon: root.inputMuted ? root.iconMicMuted : root.iconMic
|
||||
iconColor: root.t.statusInfo
|
||||
volume: root.inputVolume
|
||||
maxVolume: root.inputMaxVolume
|
||||
muted: root.inputMuted
|
||||
accentColor: root.t.statusInfo
|
||||
dimColor: root.t.textDim
|
||||
textColor: root.t.textMain
|
||||
trackColor: root.t.volTrack
|
||||
deviceName: root.inputDeviceName
|
||||
nodes: root.sourceNodes
|
||||
currentNode: root.source
|
||||
onSliderMoved: vol => root.setInputVol(vol)
|
||||
onMuteToggled: root.toggleInputMute()
|
||||
onDeviceSelected: node => root.setDefaultDevice(node)
|
||||
}
|
||||
|
||||
Rectangle { Layout.fillWidth: true; height: 1; color: root.t.separator }
|
||||
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: 10
|
||||
Text {
|
||||
text: "Volume Limits"
|
||||
color: root.t.textDim
|
||||
font.pixelSize: 10
|
||||
font.capitalization: Font.AllUppercase
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
LimitRow {
|
||||
Layout.fillWidth: true
|
||||
icon: root.iconSpeakerHigh
|
||||
iconColor: root.t.accent
|
||||
labelText: "Output max"
|
||||
value: root.outputMaxVolume
|
||||
minValue: 10
|
||||
maxValue: 300
|
||||
textColor: root.t.textMain
|
||||
dimColor: root.t.textDim
|
||||
borderColor: root.t.volPopupBorder
|
||||
onDecrement: root.outputMaxVolume = Math.max(10, root.outputMaxVolume - 10)
|
||||
onIncrement: root.outputMaxVolume = Math.min(300, root.outputMaxVolume + 10)
|
||||
}
|
||||
LimitRow {
|
||||
Layout.fillWidth: true
|
||||
icon: root.iconMic
|
||||
iconColor: root.t.statusInfo
|
||||
labelText: "Input max"
|
||||
value: root.inputMaxVolume
|
||||
minValue: 10
|
||||
maxValue: 300
|
||||
textColor: root.t.textMain
|
||||
dimColor: root.t.textDim
|
||||
borderColor: root.t.volPopupBorder
|
||||
onDecrement: root.inputMaxVolume = Math.max(10, root.inputMaxVolume - 10)
|
||||
onIncrement: root.inputMaxVolume = Math.min(300, root.inputMaxVolume + 10)
|
||||
}
|
||||
}
|
||||
Item { height: 2 }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
component VolumeSection: ColumnLayout {
|
||||
id: vs
|
||||
spacing: 8
|
||||
required property string label
|
||||
required property string icon
|
||||
required property color iconColor
|
||||
required property int volume
|
||||
required property int maxVolume
|
||||
required property bool muted
|
||||
required property color accentColor
|
||||
required property color dimColor
|
||||
required property color textColor
|
||||
required property color trackColor
|
||||
required property string deviceName
|
||||
required property var nodes
|
||||
required property var currentNode
|
||||
|
||||
signal sliderMoved(int vol)
|
||||
signal muteToggled()
|
||||
signal deviceSelected(var node)
|
||||
|
||||
property bool pickerOpen: false
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: 6
|
||||
Text {
|
||||
text: vs.icon
|
||||
color: vs.muted ? vs.dimColor : vs.iconColor
|
||||
font.pixelSize: Cfg.Config.volumeIconSize
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
Layout.preferredWidth: Cfg.Config.volumeIconSize + 4
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
}
|
||||
Text {
|
||||
text: vs.label
|
||||
color: vs.textColor
|
||||
font.pixelSize: 13
|
||||
font.bold: true
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
Text {
|
||||
text: vs.muted ? "muted" : vs.volume + "%"
|
||||
color: vs.muted ? vs.dimColor : vs.textColor
|
||||
font.pixelSize: 12
|
||||
font.bold: !vs.muted
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
Layout.preferredWidth: 44
|
||||
}
|
||||
Rectangle {
|
||||
width: 22; height: 22; radius: 6
|
||||
color: muteHover.hovered ? (vs.muted ? "#33ffffff" : "#22ffffff") : (vs.muted ? root.t.volMuteBg : "transparent")
|
||||
border.color: vs.muted ? vs.accentColor : vs.dimColor
|
||||
border.width: 1
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
text: vs.muted ? "" : ""
|
||||
color: vs.muted ? vs.accentColor : vs.dimColor
|
||||
font.pixelSize: 11
|
||||
}
|
||||
HoverHandler { id: muteHover; cursorShape: Qt.PointingHandCursor }
|
||||
TapHandler { onTapped: vs.muteToggled() }
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
id: sliderItem
|
||||
Layout.fillWidth: true
|
||||
height: 22
|
||||
Rectangle {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
width: parent.width; height: Cfg.Config.volumeSliderHeight; radius: 2
|
||||
color: vs.trackColor
|
||||
Rectangle {
|
||||
width: Math.min(parent.width, parent.width * (vs.volume / Math.max(1, vs.maxVolume)))
|
||||
height: parent.height; radius: parent.radius
|
||||
color: vs.muted ? vs.dimColor : vs.accentColor
|
||||
}
|
||||
}
|
||||
Rectangle {
|
||||
readonly property real ratio: vs.volume / Math.max(1, vs.maxVolume)
|
||||
x: Math.min(sliderItem.width - width, Math.max(0, sliderItem.width * ratio - width / 2))
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
width: Cfg.Config.volumeHandleSize; height: Cfg.Config.volumeHandleSize; radius: width / 2
|
||||
color: vs.muted ? vs.dimColor : vs.accentColor
|
||||
border.color: root.t.volHandleBorder
|
||||
border.width: 1
|
||||
}
|
||||
MouseArea {
|
||||
anchors.fill: parent; cursorShape: Qt.PointingHandCursor
|
||||
onPressed: pos => sliderItem.seek(pos.x)
|
||||
onPositionChanged: pos => { if (pressed) sliderItem.seek(pos.x) }
|
||||
}
|
||||
function seek(x) {
|
||||
let ratio = Math.max(0.0, Math.min(1.0, x / sliderItem.width))
|
||||
vs.sliderMoved(Math.round(ratio * vs.maxVolume))
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: 6
|
||||
Text { text: ""; color: vs.dimColor; font.pixelSize: 11 }
|
||||
Text {
|
||||
text: vs.deviceName
|
||||
color: vs.textColor
|
||||
font.pixelSize: 11
|
||||
elide: Text.ElideRight
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
Rectangle {
|
||||
width: 20; height: 20; radius: 5
|
||||
color: chevHover.hovered ? "#22ffffff" : "transparent"
|
||||
border.color: vs.pickerOpen ? vs.accentColor : root.t.volPopupBorder
|
||||
border.width: 1
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
text: vs.pickerOpen ? "" : ""
|
||||
color: vs.pickerOpen ? vs.accentColor : vs.dimColor
|
||||
font.pixelSize: 10
|
||||
}
|
||||
HoverHandler { id: chevHover; cursorShape: Qt.PointingHandCursor }
|
||||
TapHandler { onTapped: vs.pickerOpen = !vs.pickerOpen }
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: vs.pickerOpen ? Math.min(deviceFlickable.contentHeight, 120) : 0
|
||||
opacity: vs.pickerOpen ? 1.0 : 0.0
|
||||
clip: true
|
||||
|
||||
Behavior on Layout.preferredHeight { NumberAnimation { duration: 150; easing.type: Easing.OutCubic } }
|
||||
Behavior on opacity { NumberAnimation { duration: 150; easing.type: Easing.OutCubic } }
|
||||
|
||||
Flickable {
|
||||
id: deviceFlickable
|
||||
anchors.fill: parent
|
||||
contentWidth: width
|
||||
contentHeight: deviceList.implicitHeight
|
||||
clip: true
|
||||
boundsBehavior: Flickable.StopAtBounds
|
||||
|
||||
ColumnLayout {
|
||||
id: deviceList
|
||||
width: deviceFlickable.width
|
||||
spacing: 2
|
||||
Repeater {
|
||||
model: vs.nodes
|
||||
delegate: Rectangle {
|
||||
required property var modelData
|
||||
readonly property bool isActive: vs.currentNode !== null && modelData.id === vs.currentNode.id
|
||||
Layout.fillWidth: true; height: 28; radius: 7
|
||||
color: isActive ? Qt.alpha(vs.accentColor, 0.18) : (rowHover.hovered ? root.t.volPickerHover : "transparent")
|
||||
border.color: isActive ? Qt.alpha(vs.accentColor, 0.45) : "transparent"
|
||||
border.width: 1
|
||||
RowLayout {
|
||||
anchors { fill: parent; leftMargin: 8; rightMargin: 8 }
|
||||
spacing: 8
|
||||
Rectangle {
|
||||
width: 6; height: 6; radius: 3
|
||||
color: isActive ? vs.accentColor : "transparent"
|
||||
border.color: isActive ? "transparent" : vs.dimColor
|
||||
border.width: 1
|
||||
}
|
||||
Text {
|
||||
text: modelData.description || modelData.nickName || modelData.name
|
||||
color: isActive ? vs.accentColor : vs.textColor
|
||||
font.pixelSize: 11; font.bold: isActive
|
||||
elide: Text.ElideRight; Layout.fillWidth: true
|
||||
}
|
||||
}
|
||||
HoverHandler { id: rowHover; cursorShape: Qt.PointingHandCursor }
|
||||
TapHandler { onTapped: { vs.deviceSelected(modelData); vs.pickerOpen = false } }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
component LimitRow: RowLayout {
|
||||
id: lr
|
||||
spacing: 8
|
||||
required property string icon
|
||||
required property color iconColor
|
||||
required property string labelText
|
||||
required property int value
|
||||
required property int minValue
|
||||
required property int maxValue
|
||||
required property color textColor
|
||||
required property color dimColor
|
||||
required property color borderColor
|
||||
|
||||
signal decrement()
|
||||
signal increment()
|
||||
|
||||
Text { text: lr.icon; color: lr.iconColor; font.pixelSize: 13 }
|
||||
Text {
|
||||
text: lr.labelText
|
||||
color: lr.textColor
|
||||
font.pixelSize: 11
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
Rectangle {
|
||||
width: 22; height: 22; radius: 5
|
||||
color: decHover.hovered ? "#22ffffff" : "transparent"
|
||||
border.color: lr.borderColor; border.width: 1
|
||||
Text {
|
||||
anchors.centerIn: parent; text: "−"
|
||||
color: lr.value <= lr.minValue ? lr.dimColor : lr.textColor
|
||||
font.pixelSize: 14
|
||||
}
|
||||
HoverHandler { id: decHover; cursorShape: Qt.PointingHandCursor }
|
||||
TapHandler { onTapped: { if (lr.value > lr.minValue) lr.decrement() } }
|
||||
}
|
||||
Text {
|
||||
text: lr.value + "%"
|
||||
color: lr.textColor
|
||||
font.pixelSize: 12; font.bold: true
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
Layout.preferredWidth: 44
|
||||
}
|
||||
Rectangle {
|
||||
width: 22; height: 22; radius: 5
|
||||
color: incHover.hovered ? "#22ffffff" : "transparent"
|
||||
border.color: lr.borderColor; border.width: 1
|
||||
Text {
|
||||
anchors.centerIn: parent; text: "+"
|
||||
color: lr.value >= lr.maxValue ? lr.dimColor : lr.textColor
|
||||
font.pixelSize: 14
|
||||
}
|
||||
HoverHandler { id: incHover; cursorShape: Qt.PointingHandCursor }
|
||||
TapHandler { onTapped: { if (lr.value < lr.maxValue) lr.increment() } }
|
||||
}
|
||||
}
|
||||
}
|
||||
62
bar/modules/Workspace.qml
Normal file
62
bar/modules/Workspace.qml
Normal file
@@ -0,0 +1,62 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Quickshell.Hyprland
|
||||
|
||||
import "../../config" as Cfg
|
||||
|
||||
Rectangle {
|
||||
id: root
|
||||
|
||||
property real panelRadius: Cfg.Config.radius / 2
|
||||
property int borderWidth: Cfg.Config.panelBorderWidth
|
||||
|
||||
readonly property var t: Cfg.Config.theme
|
||||
|
||||
width: workspaceLayout.implicitWidth + 24
|
||||
height: Cfg.Config.moduleHeight
|
||||
color: t.wsPanelBg
|
||||
radius: panelRadius
|
||||
border.color: t.wsPanelBorder
|
||||
border.width: borderWidth
|
||||
|
||||
RowLayout {
|
||||
id: workspaceLayout
|
||||
anchors.centerIn: parent
|
||||
spacing: 6
|
||||
|
||||
Repeater {
|
||||
model: Hyprland.workspaces
|
||||
|
||||
Rectangle {
|
||||
Layout.preferredWidth: modelData.active ? 34 : 28
|
||||
Layout.preferredHeight: 22
|
||||
radius: 11
|
||||
|
||||
color: modelData.active
|
||||
? root.t.wsActiveBg
|
||||
: (wsHover.containsMouse ? root.t.wsHoverBg : root.t.wsInactiveBg)
|
||||
|
||||
Behavior on color { ColorAnimation { duration: 200 } }
|
||||
Behavior on Layout.preferredWidth { NumberAnimation { duration: 200 } }
|
||||
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
text: modelData.name
|
||||
font.pixelSize: 11
|
||||
font.bold: true
|
||||
color: modelData.active
|
||||
? root.t.wsActiveText
|
||||
: (wsHover.containsMouse ? root.t.wsHoverText : root.t.wsInactiveText)
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: wsHover
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: modelData.activate()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
87
config/config.qml
Normal file
87
config/config.qml
Normal file
@@ -0,0 +1,87 @@
|
||||
pragma Singleton
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import "themes"
|
||||
|
||||
QtObject {
|
||||
id: root
|
||||
|
||||
readonly property var theme: Everforest
|
||||
|
||||
// ────────────────────────────────────────────────────────────────────────
|
||||
// Widgets
|
||||
// ────────────────────────────────────────────────────────────────────────
|
||||
property bool enableBgDate: true
|
||||
|
||||
// ────────────────────────────────────────────────────────────────────────
|
||||
// BAR
|
||||
// ────────────────────────────────────────────────────────────────────────
|
||||
|
||||
// ── Position & Dimensions ─────────────────────────────────────────────────
|
||||
property string barPosition: "bottom"
|
||||
property int barHeight: 37
|
||||
property int margin: 20
|
||||
property int radius: 26
|
||||
|
||||
// ── Module Lists ──────────────────────────────────────────────────────────
|
||||
property var leftModules: ["ArchLogo", "Stats"]
|
||||
property var centerModules: ["Workspace"]
|
||||
property var rightModules: ["Tray", "Notifications", "Volume", "Clock"]
|
||||
|
||||
// ── Arch Logo ─────────────────────────────────────────────────────────────
|
||||
property int logoIconSize: 22 // icon font size; container width scales with it
|
||||
property string logoIcon: "" // Nerd Font glyph for the logo
|
||||
|
||||
// ── Panel Styling ─────────────────────────────────────────────────────────
|
||||
property int panelBorderWidth: 1
|
||||
property int moduleSpacing: 2 // space between modules within a panel
|
||||
property int panelSpacing: 12 // gap between left / center / right panels
|
||||
property int moduleHeight: 32 // default chip height for bar modules
|
||||
|
||||
// ── Popup Styling ─────────────────────────────────────────────────────────
|
||||
property int popupRadius: 18
|
||||
property int popupBorderWidth: 1
|
||||
property int popupAnimDuration: 500
|
||||
|
||||
// ── Tray Styling ─────────────────────────────────────────────────────────
|
||||
property int trayHeight: 26
|
||||
property int trayIconSize: 20
|
||||
|
||||
// ── Stats Styling ─────────────────────────────────────────────────────────
|
||||
property int statsRingSize: 25
|
||||
property int statsRingFontSize: 10
|
||||
property bool statsShowPercentByDefault: true
|
||||
|
||||
// ── Volume Module ─────────────────────────────────────────────────────────
|
||||
property int volumeIconSize: 20
|
||||
property int volumeSliderHeight: 4
|
||||
property int volumeHandleSize: 20
|
||||
property int volumePopupWidth: 280
|
||||
property int volumeOutputMax: 100
|
||||
property int volumeInputMax: 200
|
||||
|
||||
// ────────────────────────────────────────────────────────────────────────
|
||||
// FRAME
|
||||
// ────────────────────────────────────────────────────────────────────────
|
||||
property int frameBorderWidth: 1
|
||||
|
||||
// ────────────────────────────────────────────────────────────────────────
|
||||
// NOTIFICATIONS
|
||||
// ────────────────────────────────────────────────────────────────────────
|
||||
|
||||
property string notificationPosition: "topright"
|
||||
property int notificationHistorySize: 50
|
||||
|
||||
// ── Toast timeouts (milliseconds) ─────────────────────────────────────────
|
||||
property int notifTimeoutLow: 3500
|
||||
property int notifTimeoutNormal: 5000
|
||||
property int notifTimeoutCritical: 0
|
||||
|
||||
property int notifIconSize: 18
|
||||
|
||||
// ────────────────────────────────────────────────────────────────────────
|
||||
// LAUNCHER
|
||||
// ────────────────────────────────────────────────────────────────────────
|
||||
property string launcherScript: Quickshell.env("HOME") + "/.config/quickshell/launcher/launcher.sh"
|
||||
property string launcherCacheDir: "/tmp/qs_launcher"
|
||||
}
|
||||
3
config/qmldir
Normal file
3
config/qmldir
Normal file
@@ -0,0 +1,3 @@
|
||||
module config
|
||||
|
||||
singleton Config 1.0 config.qml
|
||||
159
config/themes/everforest.qml
Normal file
159
config/themes/everforest.qml
Normal file
@@ -0,0 +1,159 @@
|
||||
pragma Singleton
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
|
||||
QtObject {
|
||||
id: theme
|
||||
|
||||
// ── Wallpaper ──────────────────────────────────────────────────────────────
|
||||
readonly property string wallpaper: Quickshell.env("HOME") + "/.config/quickshell/wallpaper.jpg"
|
||||
|
||||
// ── Base Backgrounds ───────────────────────────────────────────────────────
|
||||
readonly property color bg: "#aa0a1410"
|
||||
readonly property color bgPanel: "transparent"
|
||||
readonly property color bgPopup: bg
|
||||
readonly property color bgFrame: bg
|
||||
|
||||
// ── Separators ────────────────────────────────────────────────────────────
|
||||
readonly property color separator: "#33ffffff"
|
||||
|
||||
// ── Text ──────────────────────────────────────────────────────────────────
|
||||
readonly property color textMain: "#ffdce3de"
|
||||
readonly property color textDim: "#d97a9480"
|
||||
readonly property color todayText: "#ffffffff"
|
||||
|
||||
// ── Accent & Borders ──────────────────────────────────────────────────────
|
||||
readonly property color accent: "#ff4a8c5c"
|
||||
readonly property color border: "transparent"
|
||||
readonly property color borderPopup: "#ff2d4032"
|
||||
readonly property color borderToday: accent
|
||||
|
||||
// ── ArchLogo Module ────────────────────────────────────────────────────────
|
||||
readonly property color archLogoBg: bgPanel
|
||||
readonly property color archLogoBgHover: "#22ffffff"
|
||||
readonly property color archLogoHover: accent
|
||||
readonly property color archLogoBorder: border
|
||||
readonly property color archLogoBorderActive: accent
|
||||
readonly property color archLogoIcon: textMain
|
||||
readonly property color archLogoIconActive: accent
|
||||
|
||||
// ── Stats Module ───────────────────────────────────────────────────────────
|
||||
readonly property color statsBg: bgPanel
|
||||
readonly property color statsBorder: border
|
||||
readonly property color statsText: textMain
|
||||
readonly property color statsRingColor: "#ff83c092"
|
||||
readonly property color statsCpuColor: statsRingColor
|
||||
readonly property color statsMemColor: statsRingColor
|
||||
readonly property color statsDiskColor: statsRingColor
|
||||
readonly property color statsGpuColor: statsRingColor
|
||||
readonly property color statsTrackColor: "#28ffffff"
|
||||
|
||||
// ── Volume Module ──────────────────────────────────────────────────────────
|
||||
readonly property color volBg: bgPanel
|
||||
readonly property color volBgHover: "#22ffffff"
|
||||
readonly property color volBorder: border
|
||||
readonly property color volBorderHover: accent
|
||||
readonly property color volPopupBg: bgPopup
|
||||
readonly property color volPopupBorder: borderPopup
|
||||
readonly property color volIcon: textMain
|
||||
readonly property color volIconMuted: textDim
|
||||
readonly property color volTrack: "#22ffffff"
|
||||
readonly property color volHandle: accent
|
||||
readonly property color volHandleBorder: "#33ffffff"
|
||||
readonly property color volMuteBg: "#22ffffff"
|
||||
readonly property color volPickerHover: "#15ffffff"
|
||||
|
||||
// ── Clock Module ───────────────────────────────────────────────────────────
|
||||
readonly property color clockBg: bgPanel
|
||||
readonly property color clockBgHover: "#22ffffff"
|
||||
readonly property color clockBorder: border
|
||||
readonly property color clockBorderHover: accent
|
||||
readonly property color clockTime: textMain
|
||||
readonly property color clockSeconds: textMain
|
||||
readonly property color clockIcon: textMain
|
||||
readonly property color clockSeparator: separator
|
||||
readonly property color clockText: textMain
|
||||
readonly property color clockShadow: "#22000000"
|
||||
|
||||
// ── Calendar Popup ─────────────────────────────────────────────────────────
|
||||
readonly property color calCardBg: bgPopup
|
||||
readonly property color calCardBorder: borderPopup
|
||||
readonly property color calSeparator: separator
|
||||
readonly property color calShadow: "#44000000"
|
||||
readonly property color clockPopupHeader: accent
|
||||
readonly property color clockPopupText: textMain
|
||||
readonly property color clockPopupDim: textDim
|
||||
|
||||
// ── Workspace Module ───────────────────────────────────────────────────────
|
||||
readonly property color wsPanelBg: bgPanel
|
||||
readonly property color wsPanelBorder: "transparent"
|
||||
readonly property color wsActiveBg: accent
|
||||
readonly property color wsInactiveBg: "#22ffffff"
|
||||
readonly property color wsHoverBg: borderPopup
|
||||
readonly property color wsActiveText: "#ff0e1410"
|
||||
readonly property color wsInactiveText: textMain
|
||||
readonly property color wsHoverText: wsActiveText
|
||||
|
||||
// ── Tray Module ────────────────────────────────────────────────────────────
|
||||
readonly property color trayBg: clockBg
|
||||
readonly property color trayBgHover: clockBgHover
|
||||
readonly property color trayBorder: clockBorder
|
||||
readonly property color trayBorderHover: clockBorderHover
|
||||
readonly property color trayIcon: clockIcon
|
||||
|
||||
// ── Launcher ───────────────────────────────────────────────────────────────
|
||||
readonly property color launcherBg: bgPopup
|
||||
readonly property color launcherBorder: "#22ffffff"
|
||||
readonly property color launcherInput: "#330b1210"
|
||||
readonly property color launcherInputBorderFocus: accent
|
||||
readonly property color launcherPill: "#4d111a14"
|
||||
readonly property color launcherText: textMain
|
||||
readonly property color launcherDim: textDim
|
||||
readonly property color launcherAccent: accent
|
||||
readonly property color launcherHover: "#4d2d4032"
|
||||
readonly property color launcherSelected: launcherHover
|
||||
readonly property color launcherModeActiveText: todayText
|
||||
readonly property color launcherModeActiveBg: accent
|
||||
|
||||
// ── Status Colors ──────────────────────────────────────────────────────────
|
||||
readonly property color statusOk: "#ffa7c080"
|
||||
readonly property color statusWarn: "#ffdbbc7f"
|
||||
readonly property color statusErr: "#ffe67e80"
|
||||
readonly property color statusInfo: "#ff7fbbb3"
|
||||
|
||||
// ── Notifications Module ───────────────────────────────────────────────────
|
||||
readonly property color notifBg: bgPanel
|
||||
readonly property color notifBgHover: "#22ffffff"
|
||||
readonly property color notifBorder: border
|
||||
readonly property color notifBorderActive: accent
|
||||
readonly property color notifIcon: textMain
|
||||
readonly property color notifIconActive: accent
|
||||
readonly property color notifBadgeBg: statusErr
|
||||
readonly property color notifBadgeText: "#ff0e1410"
|
||||
readonly property color notifSeparator: separator
|
||||
|
||||
// ── Toast cards ────────────────────────────────────────────────────────────
|
||||
readonly property color notifToastBg: bgPopup
|
||||
readonly property color notifToastBgHover: borderPopup
|
||||
readonly property color notifToastBorder: borderPopup
|
||||
readonly property color notifToastShadow: "#55000000"
|
||||
readonly property color notifToastText: textMain
|
||||
readonly property color notifToastDim: textDim
|
||||
readonly property color notifToastAppName: accent
|
||||
|
||||
// ── Urgency colours ────────────────────────────────────────────────────────
|
||||
readonly property color notifUrgencyCritical: statusErr
|
||||
readonly property color notifUrgencyNormal: statusInfo
|
||||
readonly property color notifUrgencyLow: textDim
|
||||
|
||||
// ── Progress bar ───────────────────────────────────────────────────────────
|
||||
readonly property color notifProgressBg: "#22ffffff"
|
||||
readonly property color notifProgressFill: accent
|
||||
|
||||
// ── History popup ──────────────────────────────────────────────────────────
|
||||
readonly property color notifHistoryBg: bgPopup
|
||||
readonly property color notifHistoryBorder: borderPopup
|
||||
readonly property color notifHistoryShadow: "#55000000"
|
||||
readonly property color notifHistoryHover: "#22ffffff"
|
||||
readonly property color notifHistoryEmpty: textDim
|
||||
}
|
||||
3
config/themes/qmldir
Normal file
3
config/themes/qmldir
Normal file
@@ -0,0 +1,3 @@
|
||||
module themes
|
||||
|
||||
singleton Everforest 1.0 everforest.qml
|
||||
393
launcher/Launcher.qml
Normal file
393
launcher/Launcher.qml
Normal file
@@ -0,0 +1,393 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import QtQuick.Controls
|
||||
import Quickshell
|
||||
import Quickshell.Wayland
|
||||
import Quickshell.Io
|
||||
import Quickshell.Hyprland
|
||||
|
||||
import "../config" as Cfg
|
||||
|
||||
PanelWindow {
|
||||
id: root
|
||||
|
||||
property bool isTop: true
|
||||
property int barHeight: Cfg.Config.barHeight
|
||||
property int barMargin: Cfg.Config.margin
|
||||
|
||||
readonly property var t: Cfg.Config.theme
|
||||
|
||||
readonly property int launcherGap: 12
|
||||
|
||||
GlobalShortcut {
|
||||
id: launcherShortcut
|
||||
name: "toggle"
|
||||
onPressed: root.toggle()
|
||||
}
|
||||
|
||||
property bool _isOpen: false
|
||||
readonly property bool isOpen: _isOpen
|
||||
property string mode: "drun"
|
||||
property bool _keyboardNav: false
|
||||
property var allItems: []
|
||||
property bool loading: false
|
||||
property bool proxyMode: false
|
||||
|
||||
readonly property var filteredItems: {
|
||||
const q = searchInput.text.toLowerCase()
|
||||
if (!q) return allItems.slice(0, 80)
|
||||
return allItems.filter(i => i.name.toLowerCase().includes(q))
|
||||
}
|
||||
|
||||
function toggle() { _isOpen ? close() : open() }
|
||||
|
||||
function open() {
|
||||
closeTimer.stop()
|
||||
visible = true
|
||||
_isOpen = true
|
||||
searchInput.text = ""
|
||||
resultsList.currentIndex = -1
|
||||
loadItems()
|
||||
Qt.callLater(() => searchInput.forceActiveFocus())
|
||||
}
|
||||
|
||||
function close() {
|
||||
_isOpen = false
|
||||
closeTimer.restart()
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: root
|
||||
function onActiveChanged() {
|
||||
if (!root.active && root._isOpen) root.close()
|
||||
}
|
||||
}
|
||||
|
||||
function loadItems() {
|
||||
loading = true
|
||||
readCache()
|
||||
updateProc.command = [Cfg.Config.launcherScript, "--update-only"]
|
||||
updateProc.running = true
|
||||
}
|
||||
|
||||
function readCache() {
|
||||
const path = Cfg.Config.launcherCacheDir + "/" + (mode === "drun" ? "drun.txt" : "run.txt")
|
||||
dataProc.rawOutput = ""
|
||||
dataProc.command = ["bash", "-c", "cat " + path]
|
||||
dataProc.running = true
|
||||
}
|
||||
|
||||
function launch(item) {
|
||||
if (!item || !item.exec) return
|
||||
|
||||
const cleanExec = item.exec.replace(/%[fFuUik]/g, "").trim()
|
||||
const finalExec = root.proxyMode ? "proxychains4 " + cleanExec : cleanExec
|
||||
const logPath = "/tmp/qml_launch.log"
|
||||
|
||||
launchProc.command = ["sh", "-c", finalExec + " > " + logPath + " 2>&1 &"]
|
||||
launchProc.running = true
|
||||
|
||||
console.log("Launching: " + finalExec)
|
||||
if (typeof close === "function") close()
|
||||
}
|
||||
|
||||
onModeChanged: if (_isOpen) loadItems()
|
||||
|
||||
visible: false
|
||||
color: "transparent"
|
||||
|
||||
anchors { top: true; bottom: true; left: true; right: true }
|
||||
WlrLayershell.layer: WlrLayer.Overlay
|
||||
WlrLayershell.namespace: "main-shell-launcher"
|
||||
WlrLayershell.exclusiveZone: -1
|
||||
WlrLayershell.keyboardFocus: _isOpen ? WlrKeyboardFocus.OnDemand : WlrKeyboardFocus.None
|
||||
mask: Region { item: clipContainer }
|
||||
|
||||
Timer {
|
||||
id: closeTimer
|
||||
interval: Cfg.Config.popupAnimDuration + 70
|
||||
onTriggered: {
|
||||
root.visible = false
|
||||
root.mode = "drun"
|
||||
root.allItems = []
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: updateProc
|
||||
onRunningChanged: if (!running) root.readCache()
|
||||
}
|
||||
|
||||
Process {
|
||||
id: dataProc
|
||||
property string rawOutput: ""
|
||||
stdout: SplitParser { onRead: data => dataProc.rawOutput += data + "\n" }
|
||||
onRunningChanged: {
|
||||
if (!running && rawOutput) {
|
||||
const lines = rawOutput.trim().split("\n")
|
||||
const items = []
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const p = lines[i].split("\t")
|
||||
if (p.length >= 3) items.push({ name: p[0], exec: p[1], id: p[2], icon: p[3] || "" })
|
||||
}
|
||||
root.allItems = items
|
||||
root.loading = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Process { id: launchProc }
|
||||
|
||||
Item {
|
||||
id: clipContainer
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
width: 460
|
||||
|
||||
anchors.top: root.isTop ? parent.top : undefined
|
||||
anchors.bottom: root.isTop ? undefined : parent.bottom
|
||||
anchors.topMargin: root.isTop ? (root.barHeight + root.launcherGap) : 0
|
||||
anchors.bottomMargin: root.isTop ? 0 : (root.barHeight + root.launcherGap)
|
||||
height: parent.height - root.barHeight - root.launcherGap
|
||||
|
||||
clip: true
|
||||
|
||||
Rectangle {
|
||||
id: bubble
|
||||
width: parent.width
|
||||
property int targetHeight: Math.min(layout.implicitHeight + 28, 540)
|
||||
height: targetHeight
|
||||
|
||||
anchors.top: root.isTop ? parent.top : undefined
|
||||
anchors.bottom: root.isTop ? undefined : parent.bottom
|
||||
|
||||
transform: Translate {
|
||||
y: root._isOpen ? 0 : (root.isTop ? -bubble.height : bubble.height)
|
||||
|
||||
Behavior on y {
|
||||
NumberAnimation {
|
||||
duration: Cfg.Config.popupAnimDuration;
|
||||
easing.type: Easing.OutExpo
|
||||
}
|
||||
enabled: root.visible
|
||||
}
|
||||
}
|
||||
|
||||
opacity: root._isOpen ? 1 : 0
|
||||
Behavior on opacity { NumberAnimation { duration: 200 } }
|
||||
|
||||
radius: Cfg.Config.popupRadius
|
||||
color: t.launcherBg
|
||||
border.color: t.launcherBorder
|
||||
border.width: Cfg.Config.popupBorderWidth
|
||||
|
||||
focus: root._isOpen
|
||||
Keys.onEscapePressed: root.close()
|
||||
|
||||
ColumnLayout {
|
||||
id: layout
|
||||
anchors.fill: parent
|
||||
anchors.margins: 14
|
||||
spacing: 10
|
||||
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
height: 44
|
||||
radius: 10
|
||||
color: t.launcherInput
|
||||
border.color: searchInput.activeFocus ? t.launcherInputBorderFocus : t.launcherBorder
|
||||
|
||||
Behavior on color { NumberAnimation { duration: 180 } }
|
||||
|
||||
RowLayout {
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: 12
|
||||
anchors.rightMargin: 8
|
||||
spacing: 10
|
||||
|
||||
TextInput {
|
||||
id: searchInput
|
||||
Layout.fillWidth: true
|
||||
color: t.launcherText
|
||||
font.pixelSize: 15
|
||||
verticalAlignment: TextInput.AlignVCenter
|
||||
selectionColor: t.launcherAccent
|
||||
|
||||
onTextChanged: resultsList.currentIndex = -1
|
||||
|
||||
Keys.onReturnPressed: {
|
||||
if (root.filteredItems.length > 0) {
|
||||
let idx = resultsList.currentIndex;
|
||||
if (idx === -1) idx = 0;
|
||||
root.launch(root.filteredItems[idx]);
|
||||
}
|
||||
}
|
||||
|
||||
Keys.onDownPressed: {
|
||||
root._keyboardNav = true
|
||||
if (resultsList.currentIndex < 0) {
|
||||
resultsList.currentIndex = 0;
|
||||
} else {
|
||||
resultsList.currentIndex = Math.min(resultsList.currentIndex + 1, resultsList.count - 1)
|
||||
}
|
||||
resultsList.positionViewAtIndex(resultsList.currentIndex, ListView.Contain)
|
||||
}
|
||||
|
||||
Keys.onUpPressed: {
|
||||
root._keyboardNav = true
|
||||
if (resultsList.currentIndex < 0) {
|
||||
resultsList.currentIndex = resultsList.count - 1;
|
||||
} else {
|
||||
resultsList.currentIndex = Math.max(resultsList.currentIndex - 1, 0)
|
||||
}
|
||||
resultsList.positionViewAtIndex(resultsList.currentIndex, ListView.Contain)
|
||||
}
|
||||
|
||||
Keys.onTabPressed: root.mode = root.mode === "drun" ? "run" : "drun"
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
height: 28
|
||||
width: modeRow.implicitWidth + 10
|
||||
radius: 8
|
||||
color: t.launcherPill
|
||||
border.color: t.launcherBorder
|
||||
|
||||
RowLayout {
|
||||
id: modeRow
|
||||
anchors.centerIn: parent
|
||||
spacing: 2
|
||||
Repeater {
|
||||
model: ["drun", "run"]
|
||||
delegate: Rectangle {
|
||||
height: 22
|
||||
width: label.implicitWidth + 14
|
||||
radius: 6
|
||||
color: root.mode === modelData ? t.launcherModeActiveBg : "transparent"
|
||||
Text {
|
||||
id: label
|
||||
anchors.centerIn: parent
|
||||
text: modelData
|
||||
font.pixelSize: 10
|
||||
font.bold: root.mode === modelData
|
||||
color: root.mode === modelData ? t.launcherModeActiveText : t.launcherDim
|
||||
}
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
onClicked: root.mode = modelData
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: proxyBtn
|
||||
height: 28
|
||||
width: 110
|
||||
radius: 8
|
||||
color: root.proxyMode ? Qt.rgba(1, 0.35, 0.35, 0.22) : t.launcherPill
|
||||
border.color: root.proxyMode ? Qt.rgba(1, 0.35, 0.35, 0.65) : t.launcherBorder
|
||||
border.width: 1
|
||||
Behavior on color { ColorAnimation { duration: 150 } }
|
||||
Behavior on border.color { ColorAnimation { duration: 150 } }
|
||||
|
||||
RowLayout {
|
||||
id: proxyBtnRow
|
||||
anchors.centerIn: parent
|
||||
spacing: 5
|
||||
Text {
|
||||
text: ""
|
||||
font.pixelSize: 11
|
||||
color: root.proxyMode ? Qt.rgba(1, 0.45, 0.45, 1) : t.launcherDim
|
||||
Behavior on color { ColorAnimation { duration: 150 } }
|
||||
}
|
||||
Text {
|
||||
text: "proxychains4"
|
||||
font.pixelSize: 10
|
||||
font.bold: root.proxyMode
|
||||
color: root.proxyMode ? Qt.rgba(1, 0.45, 0.45, 1) : t.launcherDim
|
||||
Behavior on color { ColorAnimation { duration: 150 } }
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: root.proxyMode = !root.proxyMode
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ListView {
|
||||
id: resultsList
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: Math.min(contentHeight, 400)
|
||||
clip: true
|
||||
model: root.filteredItems
|
||||
currentIndex: -1
|
||||
spacing: 2
|
||||
|
||||
onModelChanged: currentIndex = -1
|
||||
ScrollBar.vertical: ScrollBar { policy: ScrollBar.AsNeeded }
|
||||
|
||||
delegate: Rectangle {
|
||||
width: resultsList.width
|
||||
height: 38
|
||||
radius: 8
|
||||
property bool hovered: false
|
||||
|
||||
color: hovered ? t.launcherHover
|
||||
: (resultsList.currentIndex === index && index !== -1
|
||||
? t.launcherSelected : "transparent")
|
||||
|
||||
RowLayout {
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: 12
|
||||
anchors.rightMargin: 12
|
||||
spacing: 12
|
||||
|
||||
Item {
|
||||
width: 22; height: 22
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
|
||||
Image {
|
||||
id: appIcon
|
||||
anchors.fill: parent
|
||||
source: modelData.icon ? "image://icon/" + modelData.icon : ""
|
||||
fillMode: Image.PreserveAspectFit
|
||||
smooth: true
|
||||
visible: status === Image.Ready
|
||||
}
|
||||
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
visible: !appIcon.visible
|
||||
text: root.mode === "drun" ? "" : ""
|
||||
color: resultsList.currentIndex === index && index !== -1 ? t.launcherAccent : t.launcherDim
|
||||
font.pixelSize: 16
|
||||
}
|
||||
}
|
||||
|
||||
Text {
|
||||
text: modelData.name
|
||||
color: t.launcherText
|
||||
font.pixelSize: 14
|
||||
Layout.fillWidth: true
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
onEntered: parent.hovered = true
|
||||
onExited: parent.hovered = false
|
||||
onClicked: root.launch(modelData)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
105
launcher/launcher.sh
Executable file
105
launcher/launcher.sh
Executable file
@@ -0,0 +1,105 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
CACHE_DIR="/tmp/qs_launcher"
|
||||
|
||||
DRUN_CACHE="$CACHE_DIR/drun.txt"
|
||||
RUN_CACHE="$CACHE_DIR/run.txt"
|
||||
|
||||
mkdir -p "$CACHE_DIR"
|
||||
|
||||
|
||||
get_desktop_files() {
|
||||
IFS=':' read -ra DIRS <<< \
|
||||
"${XDG_DATA_DIRS:-/usr/local/share:/usr/share}"
|
||||
|
||||
for dir in "${DIRS[@]}"; do
|
||||
APP_DIR="$dir/applications"
|
||||
|
||||
[ -d "$APP_DIR" ] || continue
|
||||
|
||||
find "$APP_DIR" \
|
||||
-maxdepth 1 \
|
||||
-name "*.desktop" \
|
||||
2>/dev/null
|
||||
done | while read -r f; do
|
||||
|
||||
name=$(
|
||||
grep -m1 '^Name=' "$f" \
|
||||
| cut -d= -f2-
|
||||
)
|
||||
|
||||
exec=$(
|
||||
grep -m1 '^Exec=' "$f" \
|
||||
| cut -d= -f2- \
|
||||
| sed 's/%[fFuUik]//g' \
|
||||
| xargs
|
||||
)
|
||||
|
||||
type=$(
|
||||
grep -m1 '^Type=' "$f" \
|
||||
| cut -d= -f2-
|
||||
)
|
||||
|
||||
nodisplay=$(
|
||||
grep -m1 '^NoDisplay=' "$f" \
|
||||
| cut -d= -f2- \
|
||||
| tr '[:upper:]' '[:lower:]'
|
||||
)
|
||||
|
||||
icon=$(
|
||||
grep -m1 '^Icon=' "$f" \
|
||||
| cut -d= -f2-
|
||||
)
|
||||
|
||||
[[ "$type" == "Application" ]] || continue
|
||||
[[ "$nodisplay" != "true" ]] || continue
|
||||
[[ -n "$name" ]] || continue
|
||||
|
||||
printf "%s\t%s\t%s\t%s\n" \
|
||||
"$name" \
|
||||
"$exec" \
|
||||
"$(basename "$f" .desktop)" \
|
||||
"$icon"
|
||||
done
|
||||
}
|
||||
|
||||
|
||||
get_binaries() {
|
||||
compgen -c \
|
||||
| sort -u \
|
||||
| head -600 \
|
||||
| awk '{
|
||||
print $1 "\t" $1 "\t" $1
|
||||
}'
|
||||
}
|
||||
|
||||
|
||||
build_drun() {
|
||||
get_desktop_files \
|
||||
| sort -f -t$'\t' -k1,1 \
|
||||
| awk -F'\t' '!seen[$1]++'
|
||||
}
|
||||
|
||||
build_run() {
|
||||
{
|
||||
get_desktop_files
|
||||
get_binaries
|
||||
} \
|
||||
| sort -f -t$'\t' -k1,1 \
|
||||
| awk -F'\t' '!seen[$1]++'
|
||||
}
|
||||
|
||||
|
||||
update_cache() {
|
||||
build_drun > "$DRUN_CACHE.tmp"
|
||||
build_run > "$RUN_CACHE.tmp"
|
||||
|
||||
mv "$DRUN_CACHE.tmp" "$DRUN_CACHE"
|
||||
mv "$RUN_CACHE.tmp" "$RUN_CACHE"
|
||||
}
|
||||
|
||||
|
||||
if [ "$1" == "--update-only" ]; then
|
||||
update_cache
|
||||
exit 0
|
||||
fi
|
||||
83
notifications/HistoryCard.qml
Normal file
83
notifications/HistoryCard.qml
Normal file
@@ -0,0 +1,83 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
|
||||
Rectangle {
|
||||
id: root
|
||||
|
||||
property int itemIndex: 0
|
||||
property string appName: ""
|
||||
property string summary: ""
|
||||
property string body: ""
|
||||
property int urgency: 0
|
||||
property string timeStr: ""
|
||||
|
||||
property var t
|
||||
property var urgencyColor: function(u) { return "#ffffff" }
|
||||
property var urgencyIcon: function(u) { return "" }
|
||||
|
||||
signal dismissed(int itemIndex)
|
||||
|
||||
height: histItemContent.implicitHeight + 24
|
||||
radius: 12
|
||||
clip: true
|
||||
color: histItemHover.hovered ? root.t.notifHistoryHover : "transparent"
|
||||
Behavior on color { ColorAnimation { duration: 150; easing.type: Easing.OutCubic } }
|
||||
|
||||
HoverHandler { id: histItemHover }
|
||||
|
||||
SequentialAnimation {
|
||||
id: dismissAnim
|
||||
ParallelAnimation {
|
||||
NumberAnimation { target: root; property: "opacity"; to: 0; duration: 150; easing.type: Easing.OutCubic }
|
||||
NumberAnimation { target: root; property: "scale"; to: 0.95; duration: 150; easing.type: Easing.OutCubic }
|
||||
}
|
||||
NumberAnimation { target: root; property: "height"; to: 0; duration: 150; easing.type: Easing.InOutQuad }
|
||||
ScriptAction { script: root.dismissed(root.itemIndex) }
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: 3
|
||||
height: Math.max(12, root.height - 24)
|
||||
radius: 1.5
|
||||
anchors { left: parent.left; leftMargin: 10; verticalCenter: parent.verticalCenter }
|
||||
color: root.urgencyColor(root.urgency)
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors { right: parent.right; top: parent.top; margins: 8 }
|
||||
width: 22; height: 22; radius: 11
|
||||
color: histCloseHover.hovered ? root.t.notifHistoryHover : "transparent"
|
||||
opacity: histItemHover.hovered ? 1 : 0
|
||||
Behavior on opacity { NumberAnimation { duration: 150 } }
|
||||
Behavior on color { ColorAnimation { duration: 150 } }
|
||||
|
||||
Text { anchors.centerIn: parent; text: "✕"; color: root.t.notifToastText; font.pixelSize: 10 }
|
||||
HoverHandler { id: histCloseHover; cursorShape: Qt.PointingHandCursor }
|
||||
TapHandler { onTapped: dismissAnim.start() }
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
id: histItemContent
|
||||
anchors { left: parent.left; right: parent.right; top: parent.top; leftMargin: 24; rightMargin: 36; topMargin: 12 }
|
||||
spacing: 4
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true; spacing: 5
|
||||
Text { text: root.urgencyIcon(root.urgency); color: root.urgencyColor(root.urgency); font.pixelSize: 11; Layout.alignment: Qt.AlignVCenter }
|
||||
Text {
|
||||
text: root.appName; color: root.t.notifToastAppName; font.pixelSize: 11; font.bold: true;
|
||||
font.capitalization: Font.AllUppercase; Layout.fillWidth: true; elide: Text.ElideRight; verticalAlignment: Text.AlignVCenter
|
||||
}
|
||||
Text { text: root.timeStr; color: root.t.notifToastDim; font.pixelSize: 11; verticalAlignment: Text.AlignVCenter }
|
||||
}
|
||||
|
||||
Text {
|
||||
text: root.summary; color: root.t.notifToastText; font.pixelSize: 13; font.bold: true;
|
||||
Layout.fillWidth: true; elide: Text.ElideRight; textFormat: Text.PlainText; visible: root.summary !== ""
|
||||
}
|
||||
Text {
|
||||
text: root.body; color: root.t.notifToastDim; font.pixelSize: 12; Layout.fillWidth: true;
|
||||
wrapMode: Text.WordWrap; maximumLineCount: 2; elide: Text.ElideRight; textFormat: Text.PlainText; visible: root.body !== ""; lineHeight: 1.15
|
||||
}
|
||||
}
|
||||
}
|
||||
191
notifications/NotificationCard.qml
Normal file
191
notifications/NotificationCard.qml
Normal file
@@ -0,0 +1,191 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Quickshell.Services.Notifications
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property string toastId: ""
|
||||
property string appName: ""
|
||||
property string summary: ""
|
||||
property string body: ""
|
||||
property int urgency: NotificationUrgency.Normal
|
||||
property string timeStr: ""
|
||||
property int timeout: 5000
|
||||
|
||||
property var t
|
||||
property var urgencyColor: function(u) { return "#ffffff" }
|
||||
property var urgencyIcon: function(u) { return "" }
|
||||
|
||||
signal dismissed(string toastId)
|
||||
|
||||
width: parent ? parent.width : 350
|
||||
height: toastCard.height
|
||||
|
||||
property real progressVal: 1.0
|
||||
|
||||
Timer {
|
||||
interval: root.timeout
|
||||
running: root.timeout > 0
|
||||
repeat: false
|
||||
onTriggered: root.dismissed(root.toastId)
|
||||
}
|
||||
|
||||
NumberAnimation on progressVal {
|
||||
from: 1.0; to: 0.0
|
||||
duration: root.timeout
|
||||
running: root.urgency === NotificationUrgency.Low && root.timeout > 0
|
||||
}
|
||||
|
||||
opacity: 0
|
||||
scale: 0.90
|
||||
Component.onCompleted: {
|
||||
entranceOp.start()
|
||||
entranceSc.start()
|
||||
}
|
||||
NumberAnimation {
|
||||
id: entranceOp; target: root
|
||||
property: "opacity"; from: 0; to: 1
|
||||
duration: 240; easing.type: Easing.OutCubic
|
||||
}
|
||||
NumberAnimation {
|
||||
id: entranceSc; target: root
|
||||
property: "scale"; from: 0.90; to: 1.0
|
||||
duration: 240; easing.type: Easing.OutBack
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: toastCard
|
||||
width: parent.width
|
||||
height: toastContent.implicitHeight + (root.urgency === NotificationUrgency.Low ? 30 : 24)
|
||||
radius: 16
|
||||
|
||||
color: cardHover.hovered ? root.t.notifToastBgHover : root.t.notifToastBg
|
||||
border.color: root.urgency === NotificationUrgency.Critical
|
||||
? root.t.notifUrgencyCritical
|
||||
: (cardHover.hovered ? root.t.notifBorderActive : root.t.notifToastBorder)
|
||||
border.width: 1
|
||||
layer.enabled: true
|
||||
|
||||
Behavior on color { ColorAnimation { duration: 150; easing.type: Easing.OutCubic } }
|
||||
Behavior on border.color { ColorAnimation { duration: 150 } }
|
||||
|
||||
HoverHandler { id: cardHover }
|
||||
TapHandler { onTapped: root.dismissed(root.toastId) }
|
||||
|
||||
Rectangle {
|
||||
width: 4
|
||||
height: Math.max(16, toastCard.height - 24)
|
||||
radius: 2
|
||||
anchors {
|
||||
left: parent.left
|
||||
leftMargin: 10
|
||||
verticalCenter: parent.verticalCenter
|
||||
}
|
||||
color: root.urgencyColor(root.urgency)
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
id: toastContent
|
||||
anchors {
|
||||
left: parent.left
|
||||
right: parent.right
|
||||
top: parent.top
|
||||
leftMargin: 24
|
||||
rightMargin: 12
|
||||
topMargin: 12
|
||||
}
|
||||
spacing: 4
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: 6
|
||||
|
||||
Text {
|
||||
text: root.urgencyIcon(root.urgency)
|
||||
color: root.urgencyColor(root.urgency)
|
||||
font.pixelSize: 12
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
}
|
||||
Text {
|
||||
text: root.appName
|
||||
color: root.t.notifToastAppName
|
||||
font.pixelSize: 11
|
||||
font.bold: true
|
||||
font.capitalization: Font.AllUppercase
|
||||
Layout.fillWidth: true
|
||||
elide: Text.ElideRight
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
}
|
||||
Text {
|
||||
text: root.timeStr
|
||||
color: root.t.notifToastDim
|
||||
font.pixelSize: 10
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
width: 20; height: 20; radius: 10
|
||||
color: closeHover.hovered ? root.t.notifHistoryHover : "transparent"
|
||||
Behavior on color { ColorAnimation { duration: 100 } }
|
||||
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
text: "✕"
|
||||
color: closeHover.hovered ? root.t.notifToastText : root.t.notifToastDim
|
||||
font.pixelSize: 10
|
||||
}
|
||||
HoverHandler { id: closeHover; cursorShape: Qt.PointingHandCursor }
|
||||
TapHandler { onTapped: root.dismissed(root.toastId) }
|
||||
}
|
||||
}
|
||||
|
||||
Text {
|
||||
text: root.summary
|
||||
color: root.t.notifToastText
|
||||
font.pixelSize: 14
|
||||
font.bold: true
|
||||
Layout.fillWidth: true
|
||||
elide: Text.ElideRight
|
||||
textFormat: Text.PlainText
|
||||
visible: root.summary !== ""
|
||||
}
|
||||
|
||||
Text {
|
||||
text: root.body
|
||||
color: root.t.notifToastDim
|
||||
font.pixelSize: 12
|
||||
Layout.fillWidth: true
|
||||
wrapMode: Text.WordWrap
|
||||
maximumLineCount: 3
|
||||
elide: Text.ElideRight
|
||||
textFormat: Text.PlainText
|
||||
visible: root.body !== ""
|
||||
Layout.bottomMargin: root.urgency === NotificationUrgency.Low ? 6 : 0
|
||||
lineHeight: 1.15
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
visible: root.urgency === NotificationUrgency.Low && root.timeout > 0
|
||||
anchors {
|
||||
left: parent.left; right: parent.right; bottom: parent.bottom
|
||||
margins: 10; bottomMargin: 10
|
||||
}
|
||||
height: 4
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
radius: 2
|
||||
color: root.t.notifProgressBg
|
||||
}
|
||||
Rectangle {
|
||||
width: parent.width * root.progressVal
|
||||
height: parent.height
|
||||
radius: 2
|
||||
color: root.t.notifProgressFill
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
247
shell.qml
Normal file
247
shell.qml
Normal file
@@ -0,0 +1,247 @@
|
||||
//@ pragma UseQApplication
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Wayland
|
||||
import Quickshell.Io
|
||||
|
||||
import "bar"
|
||||
import "launcher"
|
||||
import "widgets/bgDate"
|
||||
import "config" as Cfg
|
||||
|
||||
ShellRoot {
|
||||
id: root
|
||||
|
||||
readonly property bool isTop: Cfg.Config.barPosition === "top"
|
||||
|
||||
property color frameBackground: Cfg.Config.theme.bgFrame
|
||||
property color frameBorderColor: Cfg.Config.theme.borderPopup
|
||||
|
||||
// ── Spacing Gap: Top / Bottom ────────────────────────────────────────
|
||||
Variants {
|
||||
model: Quickshell.screens
|
||||
PanelWindow {
|
||||
required property var modelData
|
||||
screen: modelData
|
||||
anchors {
|
||||
top: !root.isTop
|
||||
bottom: root.isTop
|
||||
left: true
|
||||
right: true
|
||||
}
|
||||
implicitHeight: Cfg.Config.margin
|
||||
color: "transparent"
|
||||
WlrLayershell.layer: WlrLayer.Top
|
||||
WlrLayershell.exclusiveZone: Cfg.Config.margin
|
||||
mask: Region {}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Spacing Gap: Left ────────────────────────────────────────────────
|
||||
Variants {
|
||||
model: Quickshell.screens
|
||||
PanelWindow {
|
||||
required property var modelData
|
||||
screen: modelData
|
||||
anchors { left: true; top: true; bottom: true }
|
||||
implicitWidth: Cfg.Config.margin
|
||||
color: "transparent"
|
||||
WlrLayershell.layer: WlrLayer.Top
|
||||
WlrLayershell.exclusiveZone: Cfg.Config.margin
|
||||
mask: Region {}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Spacing Gap: Right ───────────────────────────────────────────────
|
||||
Variants {
|
||||
model: Quickshell.screens
|
||||
PanelWindow {
|
||||
required property var modelData
|
||||
screen: modelData
|
||||
anchors { right: true; top: true; bottom: true }
|
||||
implicitWidth: Cfg.Config.margin
|
||||
color: "transparent"
|
||||
WlrLayershell.layer: WlrLayer.Top
|
||||
WlrLayershell.exclusiveZone: Cfg.Config.margin
|
||||
mask: Region {}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Wallpaper ────────────────────────────────────────────────────────
|
||||
Variants {
|
||||
model: Quickshell.screens
|
||||
PanelWindow {
|
||||
required property var modelData
|
||||
screen: modelData
|
||||
color: "transparent"
|
||||
anchors { top: true; bottom: true; left: true; right: true }
|
||||
WlrLayershell.layer: WlrLayer.Background
|
||||
WlrLayershell.namespace: "main-shell-wallpaper"
|
||||
WlrLayershell.exclusiveZone: -1
|
||||
mask: Region {}
|
||||
|
||||
Image {
|
||||
anchors.fill: parent
|
||||
source: {
|
||||
var p = Cfg.Config.theme.wallpaper
|
||||
return p.startsWith("file://") ? p : "file://" + p
|
||||
}
|
||||
fillMode: Image.PreserveAspectCrop
|
||||
asynchronous: true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Center Clock (bgDate Widget) ─────────────────────────────────────
|
||||
Variants {
|
||||
model: Cfg.Config.enableBgDate ? Quickshell.screens : []
|
||||
BgDate {
|
||||
modelData: modelData
|
||||
}
|
||||
}
|
||||
|
||||
// ── Frame ────────────────────────────────────────────────────────────
|
||||
Variants {
|
||||
model: Quickshell.screens
|
||||
PanelWindow {
|
||||
required property var modelData
|
||||
screen: modelData
|
||||
color: "transparent"
|
||||
anchors { top: true; bottom: true; left: true; right: true }
|
||||
WlrLayershell.layer: WlrLayer.Top
|
||||
WlrLayershell.namespace: "main-shell-frame"
|
||||
WlrLayershell.exclusiveZone: -1
|
||||
mask: Region {}
|
||||
|
||||
Canvas {
|
||||
id: frameCanvas
|
||||
anchors.fill: parent
|
||||
|
||||
// Animation Properties
|
||||
opacity: 0
|
||||
scale: 0.98
|
||||
|
||||
Component.onCompleted: startupAnimation.start()
|
||||
|
||||
ParallelAnimation {
|
||||
id: startupAnimation
|
||||
NumberAnimation {
|
||||
target: frameCanvas; property: "opacity"
|
||||
from: 0; to: 1; duration: 500
|
||||
easing.type: Easing.OutCubic
|
||||
}
|
||||
NumberAnimation {
|
||||
target: frameCanvas; property: "scale"
|
||||
from: 0.98; to: 1.0; duration: 600
|
||||
easing.type: Easing.OutBack
|
||||
}
|
||||
}
|
||||
|
||||
onPaint: {
|
||||
var ctx = getContext("2d")
|
||||
ctx.clearRect(0, 0, width, height)
|
||||
|
||||
var mT = root.isTop ? Cfg.Config.barHeight : Cfg.Config.margin
|
||||
var mB = root.isTop ? Cfg.Config.margin : Cfg.Config.barHeight
|
||||
var r = Cfg.Config.radius
|
||||
var cw = width - Cfg.Config.margin * 2
|
||||
var ch = height - mT - mB
|
||||
var x = Cfg.Config.margin
|
||||
var y = mT
|
||||
|
||||
ctx.fillStyle = root.frameBackground
|
||||
|
||||
ctx.fillRect(0, 0, width, mT)
|
||||
ctx.fillRect(0, height - mB, width, mB)
|
||||
ctx.fillRect(0, mT, x, ch)
|
||||
ctx.fillRect(x + cw, mT, width - (x + cw), ch)
|
||||
|
||||
ctx.beginPath()
|
||||
ctx.moveTo(x, y)
|
||||
ctx.lineTo(x + r, y)
|
||||
ctx.arc(x + r, y + r, r, -Math.PI / 2, Math.PI, true)
|
||||
ctx.closePath()
|
||||
ctx.fill()
|
||||
|
||||
ctx.beginPath()
|
||||
ctx.moveTo(x + cw, y)
|
||||
ctx.lineTo(x + cw - r, y)
|
||||
ctx.arc(x + cw - r, y + r, r, -Math.PI / 2, 0, false)
|
||||
ctx.closePath()
|
||||
ctx.fill()
|
||||
|
||||
ctx.beginPath()
|
||||
ctx.moveTo(x + cw, y + ch)
|
||||
ctx.lineTo(x + cw, y + ch - r)
|
||||
ctx.arc(x + cw - r, y + ch - r, r, 0, Math.PI / 2, false)
|
||||
ctx.closePath()
|
||||
ctx.fill()
|
||||
|
||||
ctx.beginPath()
|
||||
ctx.moveTo(x, y + ch)
|
||||
ctx.lineTo(x + r, y + ch)
|
||||
ctx.arc(x + r, y + ch - r, r, Math.PI / 2, Math.PI, false)
|
||||
ctx.closePath()
|
||||
ctx.fill()
|
||||
|
||||
ctx.lineWidth = Cfg.Config.frameBorderWidth
|
||||
ctx.strokeStyle = root.frameBorderColor
|
||||
|
||||
ctx.beginPath()
|
||||
ctx.moveTo(x + r, y)
|
||||
ctx.lineTo(x + cw - r, y)
|
||||
ctx.arcTo(x + cw, y, x + cw, y + r, r)
|
||||
ctx.stroke()
|
||||
|
||||
ctx.beginPath()
|
||||
ctx.moveTo(x + cw, y + r)
|
||||
ctx.lineTo(x + cw, y + ch - r)
|
||||
ctx.arcTo(x + cw, y + ch, x + cw - r, y + ch, r)
|
||||
ctx.stroke()
|
||||
|
||||
ctx.beginPath()
|
||||
ctx.moveTo(x + cw - r, y + ch)
|
||||
ctx.lineTo(x + r, y + ch)
|
||||
ctx.arcTo(x, y + ch, x, y + ch - r, r)
|
||||
ctx.stroke()
|
||||
|
||||
ctx.beginPath()
|
||||
ctx.moveTo(x, y + ch - r)
|
||||
ctx.lineTo(x, y + r)
|
||||
ctx.arcTo(x, y, x + r, y, r)
|
||||
ctx.stroke()
|
||||
}
|
||||
|
||||
onWidthChanged: requestPaint()
|
||||
onHeightChanged: requestPaint()
|
||||
|
||||
Connections {
|
||||
target: root
|
||||
function onFrameBackgroundChanged() { frameCanvas.requestPaint() }
|
||||
function onFrameBorderColorChanged() { frameCanvas.requestPaint() }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Launcher ─────────────────────────────────────────────────────────
|
||||
Variants {
|
||||
model: Quickshell.screens
|
||||
Launcher {
|
||||
required property var modelData
|
||||
screen: modelData
|
||||
isTop: root.isTop
|
||||
Component.onCompleted: modelData.launcher = this
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// ── Bar ──────────────────────────────────────────────────────────────
|
||||
Variants {
|
||||
model: Quickshell.screens
|
||||
Bar {
|
||||
screen: modelData
|
||||
isTop: root.isTop
|
||||
}
|
||||
}
|
||||
}
|
||||
BIN
wallpaper.jpg
Normal file
BIN
wallpaper.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.2 MiB |
34
widgets/bgDate/BgDate.qml
Normal file
34
widgets/bgDate/BgDate.qml
Normal file
@@ -0,0 +1,34 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Wayland
|
||||
import "../../config" as Cfg
|
||||
|
||||
PanelWindow {
|
||||
required property var modelData
|
||||
screen: modelData
|
||||
color: "transparent"
|
||||
|
||||
anchors { top: true; bottom: true; left: true; right: true }
|
||||
|
||||
WlrLayershell.layer: WlrLayer.Background
|
||||
WlrLayershell.exclusiveZone: -1
|
||||
mask: Region {}
|
||||
|
||||
Text {
|
||||
id: clockText
|
||||
anchors.centerIn: parent
|
||||
color: Cfg.Config.theme.clockText
|
||||
styleColor: Cfg.Config.theme.clockShadow
|
||||
font.pixelSize: 100
|
||||
font.weight: Font.Thin
|
||||
style: Text.Outline
|
||||
text: new Date().toLocaleTimeString(Qt.locale(), "HH:mm")
|
||||
}
|
||||
|
||||
Timer {
|
||||
interval: 1000
|
||||
running: true
|
||||
repeat: true
|
||||
onTriggered: clockText.text = new Date().toLocaleTimeString(Qt.locale(), "HH:mm")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user