1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2025-12-05 21:15:38 -05:00
Files
DankMaterialShell/shell-working.qml
2025-07-10 11:28:27 -04:00

2246 lines
96 KiB
QML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//@ pragma UseQApplication
import QtQuick
import QtQuick.Controls
import Qt5Compat.GraphicalEffects
import Quickshell
import Quickshell.Widgets
import Quickshell.Wayland
import Quickshell.Io
import Quickshell.Services.SystemTray
import Quickshell.Services.Notifications
import Quickshell.Services.Mpris
import "Services"
ShellRoot {
id: root
property bool calendarVisible: false
property bool showTrayMenu: false
property real trayMenuX: 0
property real trayMenuY: 0
property var currentTrayMenu: null
property var currentTrayItem: null
property string osLogo: ""
property string osName: ""
property bool notificationHistoryVisible: false
property var activeNotification: null
property bool showNotificationPopup: false
property bool mediaPlayerVisible: false
property MprisPlayer activePlayer: MprisController.activePlayer
property bool hasActiveMedia: MprisController.isPlaying && (activePlayer?.trackTitle || activePlayer?.trackArtist)
// Weather data
property var weather: ({
available: false,
temp: 0,
tempF: 0,
city: "",
wCode: "113",
humidity: 0,
wind: "",
sunrise: "06:00",
sunset: "18:00",
uv: 0,
pressure: 0
})
// Weather configuration
property bool useFahrenheit: true // Default to Fahrenheit
// Material 3 theme system
QtObject {
id: theme
property color primary: "#D0BCFF"
property color primaryText: "#381E72"
property color primaryContainer: "#4F378B"
property color secondary: "#CCC2DC"
property color surface: "#10121E"
property color surfaceText: "#E6E0E9"
property color surfaceVariant: "#49454F"
property color surfaceVariantText: "#CAC4D0"
property color surfaceTint: "#D0BCFF"
property color background: "#10121E"
property color backgroundText: "#E6E0E9"
property color outline: "#938F99"
property color surfaceContainer: "#1D1B20"
property color surfaceContainerHigh: "#2B2930"
property color archBlue: "#1793D1"
property color success: "#4CAF50"
property color warning: "#FF9800"
property color info: "#2196F3"
property color error: "#F2B8B5"
property int shortDuration: 150
property int mediumDuration: 300
property int longDuration: 500
property int extraLongDuration: 1000
property int standardEasing: Easing.OutCubic
property int emphasizedEasing: Easing.OutQuart
property real cornerRadius: 12
property real cornerRadiusSmall: 8
property real cornerRadiusLarge: 16
property real cornerRadiusXLarge: 24
property real spacingXS: 4
property real spacingS: 8
property real spacingM: 12
property real spacingL: 16
property real spacingXL: 24
property real fontSizeSmall: 12
property real fontSizeMedium: 14
property real fontSizeLarge: 16
property real fontSizeXLarge: 20
property real barHeight: 48
property real iconSize: 24
property real iconSizeSmall: 16
property real iconSizeLarge: 32
property real opacityDisabled: 0.38
property real opacityMedium: 0.60
property real opacityHigh: 0.87
property real opacityFull: 1.0
property string iconFont: "Material Symbols Rounded"
property string iconFontFilled: "Material Symbols Rounded"
property int iconFontWeight: Font.Normal
property int iconFontFilledWeight: Font.Medium
}
// Weather icon mapping (based on wttr.in weather codes)
property var weatherIcons: ({
"113": "clear_day",
"116": "partly_cloudy_day",
"119": "cloud",
"122": "cloud",
"143": "foggy",
"176": "rainy",
"179": "rainy",
"182": "rainy",
"185": "rainy",
"200": "thunderstorm",
"227": "cloudy_snowing",
"230": "snowing_heavy",
"248": "foggy",
"260": "foggy",
"263": "rainy",
"266": "rainy",
"281": "rainy",
"284": "rainy",
"293": "rainy",
"296": "rainy",
"299": "rainy",
"302": "weather_hail",
"305": "rainy",
"308": "weather_hail",
"311": "rainy",
"314": "rainy",
"317": "rainy",
"320": "cloudy_snowing",
"323": "cloudy_snowing",
"326": "cloudy_snowing",
"329": "snowing_heavy",
"332": "snowing_heavy",
"335": "snowing",
"338": "snowing_heavy",
"350": "rainy",
"353": "rainy",
"356": "rainy",
"359": "weather_hail",
"362": "rainy",
"365": "rainy",
"368": "cloudy_snowing",
"371": "snowing",
"374": "rainy",
"377": "rainy",
"386": "thunderstorm",
"389": "thunderstorm",
"392": "thunderstorm",
"395": "snowing"
})
// Top bar
PanelWindow {
id: topBar
anchors {
top: true
left: true
right: true
}
implicitHeight: theme.barHeight
color: "transparent"
Rectangle {
anchors.fill: parent
color: Qt.rgba(theme.surfaceContainer.r, theme.surfaceContainer.g, theme.surfaceContainer.b, 0.95)
Rectangle {
anchors.fill: parent
color: "transparent"
border.color: Qt.rgba(theme.outline.r, theme.outline.g, theme.outline.b, 0.12)
border.width: 1
}
Rectangle {
anchors.fill: parent
color: Qt.rgba(theme.surfaceTint.r, theme.surfaceTint.g, theme.surfaceTint.b, 0.08)
SequentialAnimation on opacity {
running: true
loops: Animation.Infinite
NumberAnimation {
to: 0.12
duration: theme.extraLongDuration
easing.type: theme.standardEasing
}
NumberAnimation {
to: 0.06
duration: theme.extraLongDuration
easing.type: theme.standardEasing
}
}
}
}
Item {
anchors.fill: parent
anchors.leftMargin: theme.spacingL
anchors.rightMargin: theme.spacingL
Row {
id: leftSection
height: parent.height
spacing: theme.spacingL
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
Rectangle {
id: archLauncher
width: Math.max(120, launcherRow.implicitWidth + theme.spacingM * 2)
height: 32
radius: theme.cornerRadius
color: launcherArea.containsMouse ? Qt.rgba(theme.surfaceText.r, theme.surfaceText.g, theme.surfaceText.b, 0.12) : Qt.rgba(theme.surfaceText.r, theme.surfaceText.g, theme.surfaceText.b, 0.08)
anchors.verticalCenter: parent.verticalCenter
Row {
id: launcherRow
anchors.centerIn: parent
spacing: theme.spacingS
Text {
anchors.verticalCenter: parent.verticalCenter
text: root.osLogo || "apps" // Use OS logo if detected, fallback to apps icon
font.family: root.osLogo ? "NerdFont" : theme.iconFont
font.pixelSize: root.osLogo ? theme.iconSize - 2 : theme.iconSize - 2
font.weight: theme.iconFontWeight
color: theme.surfaceText
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
}
Text {
anchors.verticalCenter: parent.verticalCenter
text: "Applications"
font.pixelSize: theme.fontSizeMedium
font.weight: Font.Medium
color: theme.surfaceText
}
}
MouseArea {
id: launcherArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
appLauncher.toggle()
}
}
Behavior on color {
ColorAnimation {
duration: theme.shortDuration
easing.type: theme.standardEasing
}
}
}
Rectangle {
id: workspaceSwitcher
width: Math.max(120, workspaceRow.implicitWidth + theme.spacingL * 2)
height: 32
radius: theme.cornerRadiusLarge
color: Qt.rgba(theme.surfaceContainerHigh.r, theme.surfaceContainerHigh.g, theme.surfaceContainerHigh.b, 0.8)
anchors.verticalCenter: parent.verticalCenter
property int currentWorkspace: 1
property var workspaceList: []
Process {
id: workspaceQuery
command: ["niri", "msg", "workspaces"]
running: true
stdout: SplitParser {
splitMarker: "\n"
onRead: (data) => {
if (data.trim()) {
workspaceSwitcher.parseWorkspaceOutput(data.trim())
}
}
}
}
function parseWorkspaceOutput(data) {
const lines = data.split('\n')
let currentOutputName = ""
let focusedOutput = ""
let focusedWorkspace = 1
let outputWorkspaces = {}
for (const line of lines) {
if (line.startsWith('Output "')) {
const outputMatch = line.match(/Output "(.+)"/)
if (outputMatch) {
currentOutputName = outputMatch[1]
outputWorkspaces[currentOutputName] = []
}
continue
}
if (line.trim() && line.match(/^\s*\*?\s*(\d+)$/)) {
const wsMatch = line.match(/^\s*(\*?)\s*(\d+)$/)
if (wsMatch) {
const isActive = wsMatch[1] === '*'
const wsNum = parseInt(wsMatch[2])
if (currentOutputName && outputWorkspaces[currentOutputName]) {
outputWorkspaces[currentOutputName].push(wsNum)
}
if (isActive) {
focusedOutput = currentOutputName
focusedWorkspace = wsNum
}
}
}
}
currentWorkspace = focusedWorkspace
if (focusedOutput && outputWorkspaces[focusedOutput]) {
workspaceList = outputWorkspaces[focusedOutput]
} else {
workspaceList = [1, 2]
}
}
Timer {
interval: 500
running: true
repeat: true
onTriggered: {
workspaceQuery.running = true
}
}
Row {
id: workspaceRow
anchors.centerIn: parent
spacing: theme.spacingS
Repeater {
model: workspaceSwitcher.workspaceList
Rectangle {
property bool isActive: modelData === workspaceSwitcher.currentWorkspace
property bool isHovered: mouseArea.containsMouse
width: isActive ? theme.spacingXL + theme.spacingS : theme.spacingL
height: theme.spacingS
radius: height / 2
color: isActive ? theme.primary :
isHovered ? Qt.rgba(theme.primary.r, theme.primary.g, theme.primary.b, 0.5) :
Qt.rgba(theme.surfaceText.r, theme.surfaceText.g, theme.surfaceText.b, 0.3)
Behavior on width {
NumberAnimation {
duration: theme.mediumDuration
easing.type: theme.emphasizedEasing
}
}
Behavior on color {
ColorAnimation {
duration: theme.mediumDuration
easing.type: theme.emphasizedEasing
}
}
MouseArea {
id: mouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
switchProcess.command = ["niri", "msg", "action", "focus-workspace", modelData.toString()]
switchProcess.running = true
workspaceSwitcher.currentWorkspace = modelData
Qt.callLater(() => {
workspaceQuery.running = true
})
}
}
}
}
}
Process {
id: switchProcess
running: false
}
}
}
Rectangle {
id: clockContainer
width: Math.min(root.hasActiveMedia ? 500 : (root.weather.available ? 280 : 200), parent.width - theme.spacingL * 2)
height: root.hasActiveMedia ? 80 : 32
radius: theme.cornerRadius
color: clockMouseArea.containsMouse && root.hasActiveMedia ?
Qt.rgba(theme.primary.r, theme.primary.g, theme.primary.b, 0.12) :
Qt.rgba(theme.surfaceText.r, theme.surfaceText.g, theme.surfaceText.b, 0.08)
anchors.centerIn: parent
Behavior on color {
ColorAnimation {
duration: theme.shortDuration
easing.type: theme.standardEasing
}
}
property date currentDate: new Date()
// Media player content (when active)
Column {
visible: root.hasActiveMedia
anchors.centerIn: parent
width: parent.width - theme.spacingM * 2
spacing: theme.spacingXS
Row {
width: parent.width
spacing: theme.spacingS
Rectangle {
width: 48
height: 48
radius: theme.cornerRadiusSmall
color: Qt.rgba(theme.surfaceVariant.r, theme.surfaceVariant.g, theme.surfaceVariant.b, 0.3)
Item {
anchors.fill: parent
clip: true
Image {
anchors.fill: parent
source: root.activePlayer?.trackArtUrl || ""
fillMode: Image.PreserveAspectCrop
smooth: true
}
Rectangle {
anchors.fill: parent
visible: parent.children[0].status !== Image.Ready
color: "transparent"
Text {
anchors.centerIn: parent
text: "music_note"
font.family: theme.iconFont
font.pixelSize: theme.iconSize
color: theme.surfaceVariantText
}
}
}
}
Column {
width: parent.width - 48 - theme.spacingS - 120
spacing: 2
anchors.verticalCenter: parent.verticalCenter
Text {
text: root.activePlayer?.trackTitle || "Unknown Track"
font.pixelSize: theme.fontSizeMedium
color: theme.surfaceText
font.weight: Font.Medium
width: parent.width
elide: Text.ElideRight
}
Text {
text: root.activePlayer?.trackArtist || "Unknown Artist"
font.pixelSize: theme.fontSizeSmall
color: Qt.rgba(theme.surfaceText.r, theme.surfaceText.g, theme.surfaceText.b, 0.7)
width: parent.width
elide: Text.ElideRight
}
}
Row {
anchors.verticalCenter: parent.verticalCenter
spacing: theme.spacingS
Rectangle {
width: 28
height: 28
radius: 14
color: prevBtnArea.containsMouse ? Qt.rgba(theme.surfaceVariant.r, theme.surfaceVariant.g, theme.surfaceVariant.b, 0.12) : "transparent"
Text {
anchors.centerIn: parent
text: "skip_previous"
font.family: theme.iconFont
font.pixelSize: 16
color: theme.surfaceText
}
MouseArea {
id: prevBtnArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: root.activePlayer?.previous()
}
}
Rectangle {
width: 28
height: 28
radius: 14
color: theme.primary
Text {
anchors.centerIn: parent
text: root.activePlayer?.playbackState === MprisPlaybackState.Playing ? "pause" : "play_arrow"
font.family: theme.iconFont
font.pixelSize: 16
color: theme.background
}
MouseArea {
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: root.activePlayer?.togglePlaying()
}
}
Rectangle {
width: 28
height: 28
radius: 14
color: nextBtnArea.containsMouse ? Qt.rgba(theme.surfaceVariant.r, theme.surfaceVariant.g, theme.surfaceVariant.b, 0.12) : "transparent"
Text {
anchors.centerIn: parent
text: "skip_next"
font.family: theme.iconFont
font.pixelSize: 16
color: theme.surfaceText
}
MouseArea {
id: nextBtnArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: root.activePlayer?.next()
}
}
}
}
Rectangle {
width: parent.width
height: 4
radius: 2
color: Qt.rgba(theme.surfaceVariant.r, theme.surfaceVariant.g, theme.surfaceVariant.b, 0.3)
Rectangle {
id: progressFill
width: parent.width * (root.activePlayer?.position / Math.max(root.activePlayer?.length || 1, 1))
height: parent.height
radius: parent.radius
color: theme.primary
Behavior on width {
NumberAnimation {
duration: 200
easing.type: Easing.OutQuad
}
}
}
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: (mouse) => {
if (root.activePlayer && root.activePlayer.length > 0) {
const newPosition = (mouse.x / width) * root.activePlayer.length
root.activePlayer.setPosition(newPosition)
}
}
}
}
}
// Normal clock/weather content (when no media)
Row {
anchors.centerIn: parent
spacing: theme.spacingM
visible: !root.hasActiveMedia
// Weather info (when available)
Row {
spacing: theme.spacingXS
visible: root.weather.available
anchors.verticalCenter: parent.verticalCenter
Text {
text: root.weatherIcons[root.weather.wCode] || "clear_day"
font.family: theme.iconFont
font.pixelSize: theme.iconSize - 2
color: theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
Text {
text: (root.useFahrenheit ? root.weather.tempF : root.weather.temp) + "°" + (root.useFahrenheit ? "F" : "C")
font.pixelSize: theme.fontSizeMedium
color: theme.surfaceText
font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter
}
}
// Separator when weather is available
Text {
text: "•"
font.pixelSize: theme.fontSizeMedium
color: Qt.rgba(theme.surfaceText.r, theme.surfaceText.g, theme.surfaceText.b, 0.5)
anchors.verticalCenter: parent.verticalCenter
visible: root.weather.available
}
// Time and date
Row {
spacing: theme.spacingS
anchors.verticalCenter: parent.verticalCenter
Text {
text: Qt.formatTime(clockContainer.currentDate, "h:mm AP")
font.pixelSize: theme.fontSizeMedium
color: theme.surfaceText
font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter
}
Text {
text: "•"
font.pixelSize: theme.fontSizeMedium
color: Qt.rgba(theme.surfaceText.r, theme.surfaceText.g, theme.surfaceText.b, 0.5)
anchors.verticalCenter: parent.verticalCenter
}
Text {
text: Qt.formatDate(clockContainer.currentDate, "ddd d")
font.pixelSize: theme.fontSizeMedium
color: theme.surfaceText
font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter
}
}
}
Timer {
interval: 1000
running: true
repeat: true
onTriggered: {
clockContainer.currentDate = new Date()
}
}
MouseArea {
id: clockMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: !root.hasActiveMedia ? Qt.PointingHandCursor : Qt.ArrowCursor
enabled: !root.hasActiveMedia
onClicked: {
root.calendarVisible = !root.calendarVisible
}
}
}
Row {
id: rightSection
height: parent.height
spacing: theme.spacingXS
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
Rectangle {
width: Math.max(40, systemTrayRow.implicitWidth + theme.spacingS * 2)
height: 32
radius: theme.cornerRadius
color: Qt.rgba(theme.secondary.r, theme.secondary.g, theme.secondary.b, 0.08)
anchors.verticalCenter: parent.verticalCenter
visible: systemTrayRow.children.length > 0
Row {
id: systemTrayRow
anchors.centerIn: parent
spacing: theme.spacingXS
Repeater {
model: SystemTray.items
delegate: Rectangle {
width: 24
height: 24
radius: theme.cornerRadiusSmall
color: trayItemArea.containsMouse ? Qt.rgba(theme.primary.r, theme.primary.g, theme.primary.b, 0.12) : "transparent"
property var trayItem: modelData
Image {
anchors.centerIn: parent
width: 18
height: 18
source: {
let icon = trayItem?.icon || "";
if (!icon) return "";
if (icon.includes("?path=")) {
const [name, path] = icon.split("?path=");
const fileName = name.substring(name.lastIndexOf("/") + 1);
return `file://${path}/${fileName}`;
}
return icon;
}
asynchronous: true
smooth: true
fillMode: Image.PreserveAspectFit
}
MouseArea {
id: trayItemArea
anchors.fill: parent
acceptedButtons: Qt.LeftButton | Qt.RightButton
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: (mouse) => {
if (!trayItem) return;
if (mouse.button === Qt.LeftButton) {
if (!trayItem.onlyMenu) {
trayItem.activate()
}
} else if (mouse.button === Qt.RightButton) {
if (trayItem.hasMenu) {
console.log("Right-click detected, showing menu for:", trayItem.title || "Unknown")
customTrayMenu.showMenu(mouse.x, mouse.y)
} else {
console.log("No menu available for:", trayItem.title || "Unknown")
}
}
}
}
// Custom Material 3 styled menu
QtObject {
id: customTrayMenu
property bool menuVisible: false
function showMenu(x, y) {
root.currentTrayMenu = customTrayMenu
root.currentTrayItem = trayItem
// Simple positioning: right side of screen, below the panel
root.trayMenuX = rightSection.x + rightSection.width - 180 - theme.spacingL
root.trayMenuY = theme.barHeight + theme.spacingS
console.log("Showing menu at:", root.trayMenuX, root.trayMenuY)
menuVisible = true
root.showTrayMenu = true
}
function hideMenu() {
menuVisible = false
root.showTrayMenu = false
root.currentTrayMenu = null
root.currentTrayItem = null
}
}
Behavior on color {
ColorAnimation {
duration: theme.shortDuration
easing.type: theme.standardEasing
}
}
}
}
}
}
// Clipboard History Button
Rectangle {
width: 40
height: 32
radius: theme.cornerRadius
color: clipboardArea.containsMouse ? Qt.rgba(theme.primary.r, theme.primary.g, theme.primary.b, 0.12) : Qt.rgba(theme.secondary.r, theme.secondary.g, theme.secondary.b, 0.08)
anchors.verticalCenter: parent.verticalCenter
Text {
anchors.centerIn: parent
text: "content_paste" // Material icon for clipboard
font.family: theme.iconFont
font.pixelSize: theme.iconSize - 6
font.weight: theme.iconFontWeight
color: theme.surfaceText
}
MouseArea {
id: clipboardArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
clipboardHistoryPopup.toggle()
}
}
Behavior on color {
ColorAnimation {
duration: theme.shortDuration
easing.type: theme.standardEasing
}
}
}
// Color Picker Button
Rectangle {
width: 40
height: 32
radius: theme.cornerRadius
color: colorPickerArea.containsMouse ? Qt.rgba(theme.primary.r, theme.primary.g, theme.primary.b, 0.12) : Qt.rgba(theme.secondary.r, theme.secondary.g, theme.secondary.b, 0.08)
anchors.verticalCenter: parent.verticalCenter
Text {
anchors.centerIn: parent
text: "colorize" // Material icon for color picker
font.family: theme.iconFont
font.pixelSize: theme.iconSize - 6
font.weight: theme.iconFontWeight
color: theme.surfaceText
}
MouseArea {
id: colorPickerArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
colorPickerProcess.running = true
}
}
Behavior on color {
ColorAnimation {
duration: theme.shortDuration
easing.type: theme.standardEasing
}
}
}
// Notification Center Button
Rectangle {
width: 40
height: 32
radius: theme.cornerRadius
color: notificationArea.containsMouse || root.notificationHistoryVisible ?
Qt.rgba(theme.primary.r, theme.primary.g, theme.primary.b, 0.16) :
Qt.rgba(theme.secondary.r, theme.secondary.g, theme.secondary.b, 0.08)
anchors.verticalCenter: parent.verticalCenter
property bool hasUnread: notificationHistory.count > 0
Text {
anchors.centerIn: parent
text: "notifications" // Material icon for notifications
font.family: theme.iconFont
font.pixelSize: theme.iconSize - 6
font.weight: theme.iconFontWeight
color: notificationArea.containsMouse || root.notificationHistoryVisible ?
theme.primary : theme.surfaceText
}
// Notification dot indicator
Rectangle {
width: 8
height: 8
radius: 4
color: theme.error
anchors.right: parent.right
anchors.top: parent.top
anchors.rightMargin: 6
anchors.topMargin: 6
visible: parent.hasUnread
}
MouseArea {
id: notificationArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
root.notificationHistoryVisible = !root.notificationHistoryVisible
}
}
Behavior on color {
ColorAnimation {
duration: theme.shortDuration
easing.type: theme.standardEasing
}
}
}
}
}
}
PanelWindow {
id: calendarPopup
visible: root.calendarVisible
implicitWidth: 320
implicitHeight: 400
WlrLayershell.layer: WlrLayershell.Overlay
WlrLayershell.exclusiveZone: -1
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
color: "transparent"
anchors {
top: true
left: true
right: true
bottom: true
}
property date displayDate: new Date()
property date selectedDate: new Date()
Rectangle {
width: 400
height: root.weather.available ? 480 : 400
x: (parent.width - width) / 2
y: theme.barHeight + theme.spacingS
color: theme.surfaceContainer
radius: theme.cornerRadiusLarge
border.color: Qt.rgba(theme.outline.r, theme.outline.g, theme.outline.b, 0.12)
border.width: 1
opacity: root.calendarVisible ? 1.0 : 0.0
scale: root.calendarVisible ? 1.0 : 0.85
Behavior on opacity {
NumberAnimation {
duration: theme.mediumDuration
easing.type: theme.emphasizedEasing
}
}
Behavior on scale {
NumberAnimation {
duration: theme.mediumDuration
easing.type: theme.emphasizedEasing
}
}
Column {
anchors.fill: parent
anchors.margins: theme.spacingL
spacing: theme.spacingM
// Weather header (when available)
Rectangle {
visible: root.weather.available
width: parent.width
height: 80
radius: theme.cornerRadius
color: Qt.rgba(theme.primary.r, theme.primary.g, theme.primary.b, 0.08)
border.color: Qt.rgba(theme.primary.r, theme.primary.g, theme.primary.b, 0.2)
border.width: 1
Row {
anchors.centerIn: parent
spacing: theme.spacingL
// Weather icon and temp
Column {
spacing: 2
anchors.verticalCenter: parent.verticalCenter
Text {
text: root.weatherIcons[root.weather.wCode] || "clear_day"
font.family: theme.iconFont
font.pixelSize: theme.iconSize + 4
color: theme.primary
anchors.horizontalCenter: parent.horizontalCenter
}
Text {
text: (root.useFahrenheit ? root.weather.tempF : root.weather.temp) + "°" + (root.useFahrenheit ? "F" : "C")
font.pixelSize: theme.fontSizeLarge
color: theme.surfaceText
font.weight: Font.Bold
anchors.horizontalCenter: parent.horizontalCenter
MouseArea {
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: root.useFahrenheit = !root.useFahrenheit
}
}
Text {
text: root.weather.city
font.pixelSize: theme.fontSizeSmall
color: Qt.rgba(theme.surfaceText.r, theme.surfaceText.g, theme.surfaceText.b, 0.7)
anchors.horizontalCenter: parent.horizontalCenter
}
}
// Weather details grid
Grid {
columns: 2
spacing: theme.spacingS
anchors.verticalCenter: parent.verticalCenter
Row {
spacing: theme.spacingXS
Text {
text: "humidity_low"
font.family: theme.iconFont
font.pixelSize: theme.fontSizeSmall
color: theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
Text {
text: root.weather.humidity + "%"
font.pixelSize: theme.fontSizeSmall
color: theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
}
Row {
spacing: theme.spacingXS
Text {
text: "air"
font.family: theme.iconFont
font.pixelSize: theme.fontSizeSmall
color: theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
Text {
text: root.weather.wind
font.pixelSize: theme.fontSizeSmall
color: theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
}
Row {
spacing: theme.spacingXS
Text {
text: "wb_twilight"
font.family: theme.iconFont
font.pixelSize: theme.fontSizeSmall
color: theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
Text {
text: root.weather.sunrise
font.pixelSize: theme.fontSizeSmall
color: theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
}
Row {
spacing: theme.spacingXS
Text {
text: "bedtime"
font.family: theme.iconFont
font.pixelSize: theme.fontSizeSmall
color: theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
Text {
text: root.weather.sunset
font.pixelSize: theme.fontSizeSmall
color: theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
}
}
}
}
Row {
width: parent.width
height: 40
Rectangle {
width: 40
height: 40
radius: theme.cornerRadius
color: prevMonthArea.containsMouse ? Qt.rgba(theme.primary.r, theme.primary.g, theme.primary.b, 0.12) : "transparent"
Text {
anchors.centerIn: parent
text: "chevron_left"
font.family: theme.iconFont
font.pixelSize: theme.iconSize
color: theme.primary
font.weight: theme.iconFontWeight
}
MouseArea {
id: prevMonthArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
let newDate = new Date(calendarPopup.displayDate)
newDate.setMonth(newDate.getMonth() - 1)
calendarPopup.displayDate = newDate
}
}
}
Text {
width: parent.width - 80
height: 40
text: Qt.formatDate(calendarPopup.displayDate, "MMMM yyyy")
font.pixelSize: theme.fontSizeLarge
color: theme.surfaceText
font.weight: Font.Medium
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
}
Rectangle {
width: 40
height: 40
radius: theme.cornerRadius
color: nextMonthArea.containsMouse ? Qt.rgba(theme.primary.r, theme.primary.g, theme.primary.b, 0.12) : "transparent"
Text {
anchors.centerIn: parent
text: "chevron_right"
font.family: theme.iconFont
font.pixelSize: theme.iconSize
color: theme.primary
font.weight: theme.iconFontWeight
}
MouseArea {
id: nextMonthArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
let newDate = new Date(calendarPopup.displayDate)
newDate.setMonth(newDate.getMonth() + 1)
calendarPopup.displayDate = newDate
}
}
}
}
Row {
width: parent.width
height: 32
Repeater {
model: ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]
Rectangle {
width: parent.width / 7
height: 32
color: "transparent"
Text {
anchors.centerIn: parent
text: modelData
font.pixelSize: theme.fontSizeSmall
color: Qt.rgba(theme.surfaceText.r, theme.surfaceText.g, theme.surfaceText.b, 0.6)
font.weight: Font.Medium
}
}
}
}
Grid {
width: parent.width
height: root.weather.available ? parent.height - 200 : parent.height - 120
columns: 7
rows: 6
property date firstDay: {
let date = new Date(calendarPopup.displayDate.getFullYear(), calendarPopup.displayDate.getMonth(), 1)
let dayOfWeek = date.getDay()
date.setDate(date.getDate() - dayOfWeek)
return date
}
Repeater {
model: 42
Rectangle {
width: parent.width / 7
height: parent.height / 6
property date dayDate: {
let date = new Date(parent.firstDay)
date.setDate(date.getDate() + index)
return date
}
property bool isCurrentMonth: dayDate.getMonth() === calendarPopup.displayDate.getMonth()
property bool isToday: dayDate.toDateString() === new Date().toDateString()
property bool isSelected: dayDate.toDateString() === calendarPopup.selectedDate.toDateString()
color: isSelected ? theme.primary :
isToday ? Qt.rgba(theme.primary.r, theme.primary.g, theme.primary.b, 0.12) :
dayArea.containsMouse ? Qt.rgba(theme.primary.r, theme.primary.g, theme.primary.b, 0.08) : "transparent"
radius: theme.cornerRadiusSmall
Text {
anchors.centerIn: parent
text: dayDate.getDate()
font.pixelSize: theme.fontSizeMedium
color: isSelected ? theme.surface :
isToday ? theme.primary :
isCurrentMonth ? theme.surfaceText :
Qt.rgba(theme.surfaceText.r, theme.surfaceText.g, theme.surfaceText.b, 0.4)
font.weight: isToday || isSelected ? Font.Medium : Font.Normal
}
MouseArea {
id: dayArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
calendarPopup.selectedDate = dayDate
}
}
}
}
}
}
}
MouseArea {
anchors.fill: parent
z: -1
onClicked: {
root.calendarVisible = false
}
}
}
// Custom Material 3 System Tray Menu
PanelWindow {
id: trayMenuPopup
visible: root.showTrayMenu
WlrLayershell.layer: WlrLayershell.Overlay
WlrLayershell.exclusiveZone: -1
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
color: "transparent"
anchors {
top: true
left: true
right: true
bottom: true
}
Rectangle {
id: menuContainer
x: root.trayMenuX
y: root.trayMenuY
width: 180
height: Math.max(60, menuList.contentHeight + theme.spacingS * 2)
color: theme.surfaceContainer
radius: theme.cornerRadiusLarge
border.color: Qt.rgba(theme.outline.r, theme.outline.g, theme.outline.b, 0.12)
border.width: 1
// Material 3 drop shadow
Rectangle {
anchors.fill: parent
anchors.topMargin: 4
anchors.leftMargin: 2
anchors.rightMargin: -2
anchors.bottomMargin: -4
radius: parent.radius
color: Qt.rgba(0, 0, 0, 0.15)
z: parent.z - 1
}
// Material 3 animations
opacity: root.showTrayMenu ? 1.0 : 0.0
scale: root.showTrayMenu ? 1.0 : 0.85
Behavior on opacity {
NumberAnimation {
duration: theme.mediumDuration
easing.type: theme.emphasizedEasing
}
}
Behavior on scale {
NumberAnimation {
duration: theme.mediumDuration
easing.type: theme.emphasizedEasing
}
}
Item {
anchors.fill: parent
anchors.margins: theme.spacingS
QsMenuOpener {
id: menuOpener
menu: root.currentTrayItem?.menu
}
// Custom menu styling using ListView
ListView {
id: menuList
anchors.fill: parent
spacing: 1
model: ScriptModel {
values: menuOpener.children ? [...menuOpener.children.values].filter(item => {
// Filter out empty items and separators
return item && item.text && item.text.trim().length > 0 && !item.isSeparator
}) : []
}
delegate: Rectangle {
width: ListView.view.width
height: modelData.isSeparator ? 5 : 28
radius: modelData.isSeparator ? 0 : theme.cornerRadiusSmall
color: modelData.isSeparator ? "transparent" :
(menuItemArea.containsMouse ? Qt.rgba(theme.primary.r, theme.primary.g, theme.primary.b, 0.12) : "transparent")
// Separator line
Rectangle {
visible: modelData.isSeparator
anchors.centerIn: parent
width: parent.width - theme.spacingS * 2
height: 1
color: Qt.rgba(theme.outline.r, theme.outline.g, theme.outline.b, 0.2)
}
// Menu item content
Row {
visible: !modelData.isSeparator
anchors.left: parent.left
anchors.leftMargin: theme.spacingS
anchors.verticalCenter: parent.verticalCenter
spacing: theme.spacingXS
Text {
text: modelData.text || ""
font.pixelSize: theme.fontSizeSmall
color: theme.surfaceText
font.weight: Font.Normal
}
}
MouseArea {
id: menuItemArea
anchors.fill: parent
hoverEnabled: true
cursorShape: modelData.isSeparator ? Qt.ArrowCursor : Qt.PointingHandCursor
enabled: !modelData.isSeparator
onClicked: {
if (modelData.triggered) {
modelData.triggered()
}
root.showTrayMenu = false
}
}
Behavior on color {
ColorAnimation {
duration: theme.shortDuration
easing.type: theme.standardEasing
}
}
}
}
}
}
// Click outside to close
MouseArea {
anchors.fill: parent
z: -1
onClicked: {
root.showTrayMenu = false
}
}
}
// Notification Popup (using PanelWindow like reference)
PanelWindow {
id: notificationPopup
visible: root.showNotificationPopup && root.activeNotification
WlrLayershell.layer: WlrLayershell.Overlay
WlrLayershell.exclusiveZone: -1
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
color: "transparent"
anchors {
top: true
right: true
bottom: true
}
implicitWidth: 400
Rectangle {
id: popupContainer
width: 380
height: 100
anchors.top: parent.top
anchors.right: parent.right
anchors.topMargin: theme.barHeight + 16
anchors.rightMargin: 16
color: theme.surfaceContainer
radius: theme.cornerRadiusLarge
border.color: Qt.rgba(theme.outline.r, theme.outline.g, theme.outline.b, 0.2)
border.width: 1
opacity: root.showNotificationPopup ? 1.0 : 0.0
Behavior on opacity {
NumberAnimation { duration: 200; easing.type: Easing.OutQuad }
}
MouseArea {
anchors.fill: parent
onClicked: hideNotificationPopup()
}
// Close button with cursor pointer
Text {
anchors.right: parent.right
anchors.top: parent.top
anchors.margins: 8
text: "×"
font.pixelSize: 16
color: theme.surfaceText
MouseArea {
anchors.fill: parent
anchors.margins: -4
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: hideNotificationPopup()
}
}
// Content layout
Row {
anchors.fill: parent
anchors.margins: 12
anchors.rightMargin: 32
spacing: 12
// Notification icon using reference pattern
Rectangle {
width: 40
height: 40
radius: 8
color: Qt.rgba(theme.primary.r, theme.primary.g, theme.primary.b, 0.1)
anchors.verticalCenter: parent.verticalCenter
// Fallback material icon when no app icon
Loader {
active: !root.activeNotification || root.activeNotification.appIcon === ""
anchors.fill: parent
sourceComponent: Text {
anchors.centerIn: parent
text: "notifications"
font.family: theme.iconFont
font.pixelSize: 20
color: theme.primary
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
}
}
// App icon when no notification image
Loader {
active: root.activeNotification && root.activeNotification.appIcon !== "" && (root.activeNotification.image === "" || !root.activeNotification.image)
anchors.centerIn: parent
sourceComponent: IconImage {
width: 32
height: 32
asynchronous: true
source: {
if (!root.activeNotification) return ""
let iconPath = root.activeNotification.appIcon
// Skip file:// URLs as they're usually screenshots/images, not icons
if (iconPath && iconPath.startsWith("file://")) return ""
return iconPath ? Quickshell.iconPath(iconPath, "image-missing") : ""
}
}
}
// Notification image with rounded corners
Loader {
active: root.activeNotification && root.activeNotification.image !== ""
anchors.fill: parent
sourceComponent: Item {
anchors.fill: parent
Image {
id: notifImage
anchors.fill: parent
source: root.activeNotification ? root.activeNotification.image : ""
fillMode: Image.PreserveAspectCrop
cache: false
antialiasing: true
asynchronous: true
layer.enabled: true
layer.effect: OpacityMask {
maskSource: Rectangle {
width: notifImage.width
height: notifImage.height
radius: 8
}
}
}
// Small app icon overlay when showing notification image
Loader {
active: root.activeNotification && root.activeNotification.appIcon !== ""
anchors.bottom: parent.bottom
anchors.right: parent.right
anchors.margins: 2
sourceComponent: IconImage {
width: 16
height: 16
asynchronous: true
source: root.activeNotification ? Quickshell.iconPath(root.activeNotification.appIcon, "image-missing") : ""
}
}
}
}
}
// Text content
Column {
width: parent.width - 52
anchors.verticalCenter: parent.verticalCenter
spacing: 4
Text {
text: root.activeNotification ? (root.activeNotification.summary || "") : ""
font.pixelSize: 14
color: theme.surfaceText
font.weight: Font.Medium
width: parent.width
elide: Text.ElideRight
visible: text.length > 0
}
Text {
text: root.activeNotification ? (root.activeNotification.body || "") : ""
font.pixelSize: 12
color: Qt.rgba(theme.surfaceText.r, theme.surfaceText.g, theme.surfaceText.b, 0.7)
width: parent.width
wrapMode: Text.WordWrap
maximumLineCount: 2
elide: Text.ElideRight
visible: text.length > 0
}
}
}
}
}
// Auto-hide notification popup timer
Timer {
id: notificationTimer
interval: 5000 // 5 seconds
repeat: false
onTriggered: hideNotificationPopup()
}
// Timer for clearing active notification after animation
Timer {
id: clearNotificationTimer
interval: theme.mediumDuration + 50
repeat: false
onTriggered: root.activeNotification = null
}
function showNotificationPopup(notification) {
root.activeNotification = notification
root.showNotificationPopup = true
notificationTimer.restart()
}
function hideNotificationPopup() {
root.showNotificationPopup = false
notificationTimer.stop()
clearNotificationTimer.restart()
}
// Notification History Panel
PanelWindow {
id: notificationHistoryPopup
visible: root.notificationHistoryVisible
implicitWidth: 400
implicitHeight: 500
WlrLayershell.layer: WlrLayershell.Overlay
WlrLayershell.exclusiveZone: -1
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
color: "transparent"
anchors {
top: true
left: true
right: true
bottom: true
}
Rectangle {
width: 400
height: 500
x: parent.width - width - theme.spacingL
y: theme.barHeight + theme.spacingS
color: theme.surfaceContainer
radius: theme.cornerRadiusLarge
border.color: Qt.rgba(theme.outline.r, theme.outline.g, theme.outline.b, 0.12)
border.width: 1
opacity: root.notificationHistoryVisible ? 1.0 : 0.0
scale: root.notificationHistoryVisible ? 1.0 : 0.85
Behavior on opacity {
NumberAnimation {
duration: theme.mediumDuration
easing.type: theme.emphasizedEasing
}
}
Behavior on scale {
NumberAnimation {
duration: theme.mediumDuration
easing.type: theme.emphasizedEasing
}
}
Column {
anchors.fill: parent
anchors.margins: theme.spacingL
spacing: theme.spacingM
// Header
Column {
width: parent.width
spacing: theme.spacingM
Row {
width: parent.width
height: 32
Text {
text: "Notifications"
font.pixelSize: theme.fontSizeLarge
color: theme.surfaceText
font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter
}
Item { width: parent.width - 200; height: 1 }
}
Rectangle {
width: parent.width
height: 36
radius: theme.cornerRadius
color: clearArea.containsMouse ? Qt.rgba(theme.error.r, theme.error.g, theme.error.b, 0.16) : Qt.rgba(theme.error.r, theme.error.g, theme.error.b, 0.12)
border.color: Qt.rgba(theme.error.r, theme.error.g, theme.error.b, 0.5)
border.width: 1
visible: notificationHistory.count > 0
Row {
anchors.centerIn: parent
spacing: theme.spacingS
Text {
text: "delete_sweep"
font.family: theme.iconFont
font.pixelSize: theme.iconSizeSmall + 2
color: theme.error
font.weight: theme.iconFontWeight
anchors.verticalCenter: parent.verticalCenter
}
Text {
text: "Clear All Notifications"
font.pixelSize: theme.fontSizeMedium
color: theme.error
font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter
}
}
MouseArea {
id: clearArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
notificationHistory.clear()
}
}
Behavior on color {
ColorAnimation {
duration: theme.shortDuration
easing.type: theme.standardEasing
}
}
Behavior on border.color {
ColorAnimation {
duration: theme.shortDuration
easing.type: theme.standardEasing
}
}
}
}
// Notification List
ScrollView {
width: parent.width
height: parent.height - 120
clip: true
ListView {
id: notificationListView
model: notificationHistory
spacing: theme.spacingS
delegate: Rectangle {
width: notificationListView.width
height: 80
radius: theme.cornerRadius
color: notifArea.containsMouse ? Qt.rgba(theme.primary.r, theme.primary.g, theme.primary.b, 0.08) : Qt.rgba(theme.surfaceVariant.r, theme.surfaceVariant.g, theme.surfaceVariant.b, 0.08)
Row {
anchors.fill: parent
anchors.margins: theme.spacingM
spacing: theme.spacingM
// Notification icon using reference pattern
Rectangle {
width: 32
height: 32
radius: theme.cornerRadius
color: Qt.rgba(theme.primary.r, theme.primary.g, theme.primary.b, 0.12)
anchors.verticalCenter: parent.verticalCenter
// Fallback material icon when no app icon
Loader {
active: !model.appIcon || model.appIcon === ""
anchors.fill: parent
sourceComponent: Text {
anchors.centerIn: parent
text: model.appName ? model.appName.charAt(0).toUpperCase() : "notifications"
font.family: model.appName ? "Roboto" : theme.iconFont
font.pixelSize: model.appName ? theme.fontSizeMedium : 16
color: theme.primary
font.weight: Font.Medium
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
}
}
// App icon when no notification image
Loader {
active: model.appIcon && model.appIcon !== "" && (!model.image || model.image === "")
anchors.centerIn: parent
sourceComponent: IconImage {
width: 28
height: 28
asynchronous: true
source: {
if (!model.appIcon) return ""
// Skip file:// URLs as they're usually screenshots/images, not icons
if (model.appIcon.startsWith("file://")) return ""
return Quickshell.iconPath(model.appIcon, "image-missing")
}
}
}
// Notification image with rounded corners
Loader {
active: model.image && model.image !== ""
anchors.fill: parent
sourceComponent: Item {
anchors.fill: parent
Image {
id: historyNotifImage
anchors.fill: parent
source: model.image || ""
fillMode: Image.PreserveAspectCrop
cache: false
antialiasing: true
asynchronous: true
layer.enabled: true
layer.effect: OpacityMask {
maskSource: Rectangle {
width: historyNotifImage.width
height: historyNotifImage.height
radius: theme.cornerRadius
}
}
}
// Small app icon overlay when showing notification image
Loader {
active: model.appIcon && model.appIcon !== ""
anchors.bottom: parent.bottom
anchors.right: parent.right
anchors.margins: 2
sourceComponent: IconImage {
width: 12
height: 12
asynchronous: true
source: model.appIcon ? Quickshell.iconPath(model.appIcon, "image-missing") : ""
}
}
}
}
}
// Content
Column {
anchors.verticalCenter: parent.verticalCenter
width: parent.width - 80
spacing: theme.spacingXS
Text {
text: model.appName || "App"
font.pixelSize: theme.fontSizeSmall
color: theme.primary
font.weight: Font.Medium
}
Text {
text: model.summary || ""
font.pixelSize: theme.fontSizeMedium
color: theme.surfaceText
font.weight: Font.Medium
width: parent.width
elide: Text.ElideRight
visible: text.length > 0
}
Text {
text: model.body || ""
font.pixelSize: theme.fontSizeSmall
color: Qt.rgba(theme.surfaceText.r, theme.surfaceText.g, theme.surfaceText.b, 0.7)
width: parent.width
wrapMode: Text.WordWrap
maximumLineCount: 2
elide: Text.ElideRight
visible: text.length > 0
}
}
}
MouseArea {
id: notifArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
notificationHistory.remove(index)
}
}
Behavior on color {
ColorAnimation {
duration: theme.shortDuration
easing.type: theme.standardEasing
}
}
}
}
// Empty state - properly centered
Rectangle {
anchors.fill: parent
visible: notificationHistory.count === 0
color: "transparent"
Column {
anchors.centerIn: parent
spacing: theme.spacingM
width: parent.width * 0.8
Text {
anchors.horizontalCenter: parent.horizontalCenter
text: "notifications_none"
font.family: theme.iconFont
font.pixelSize: theme.iconSizeLarge + 16
color: Qt.rgba(theme.surfaceText.r, theme.surfaceText.g, theme.surfaceText.b, 0.3)
font.weight: theme.iconFontWeight
}
Text {
anchors.horizontalCenter: parent.horizontalCenter
text: "No notifications"
font.pixelSize: theme.fontSizeLarge
color: Qt.rgba(theme.surfaceText.r, theme.surfaceText.g, theme.surfaceText.b, 0.6)
font.weight: Font.Medium
horizontalAlignment: Text.AlignHCenter
}
Text {
anchors.horizontalCenter: parent.horizontalCenter
text: "Notifications will appear here"
font.pixelSize: theme.fontSizeMedium
color: Qt.rgba(theme.surfaceText.r, theme.surfaceText.g, theme.surfaceText.b, 0.4)
horizontalAlignment: Text.AlignHCenter
wrapMode: Text.WordWrap
width: parent.width
}
}
}
}
}
}
// Click outside to close
MouseArea {
anchors.fill: parent
z: -1
onClicked: {
root.notificationHistoryVisible = false
}
}
}
AppLauncher {
id: appLauncher
theme: root.theme
}
ClipboardHistory {
id: clipboardHistoryPopup
theme: root.theme
}
// OS Detection
Process {
id: osDetector
command: ["lsb_release", "-i", "-s"]
running: true
stdout: SplitParser {
splitMarker: "\n"
onRead: (data) => {
if (data.trim()) {
let osId = data.trim().toLowerCase()
console.log("Detected OS:", osId)
// Set OS-specific Nerd Font icons and names
if (osId.includes("arch")) {
root.osLogo = "\uf303" // Arch Linux Nerd Font icon
root.osName = "Arch Linux"
console.log("Set Arch logo:", root.osLogo)
} else if (osId.includes("ubuntu")) {
root.osLogo = "\uf31b" // Ubuntu Nerd Font icon
root.osName = "Ubuntu"
} else if (osId.includes("fedora")) {
root.osLogo = "\uf30a" // Fedora Nerd Font icon
root.osName = "Fedora"
} else if (osId.includes("debian")) {
root.osLogo = "\uf306" // Debian Nerd Font icon
root.osName = "Debian"
} else if (osId.includes("opensuse")) {
root.osLogo = "\uef6d" // openSUSE Nerd Font icon
root.osName = "openSUSE"
} else if (osId.includes("manjaro")) {
root.osLogo = "\uf312" // Manjaro Nerd Font icon
root.osName = "Manjaro"
} else {
root.osLogo = "\uf033" // Generic Linux Nerd Font icon
root.osName = "Linux"
}
}
}
}
onExited: (exitCode) => {
if (exitCode !== 0) {
// Fallback: try checking /etc/os-release
osDetectorFallback.running = true
}
}
}
// Fallback OS detection
Process {
id: osDetectorFallback
command: ["sh", "-c", "grep '^ID=' /etc/os-release | cut -d'=' -f2 | tr -d '\"'"]
running: false
stdout: SplitParser {
splitMarker: "\n"
onRead: (data) => {
if (data.trim()) {
let osId = data.trim().toLowerCase()
console.log("Detected OS (fallback):", osId)
if (osId.includes("arch")) {
root.osLogo = "\uf303"
root.osName = "Arch Linux"
} else if (osId.includes("ubuntu")) {
root.osLogo = "\uf31b"
root.osName = "Ubuntu"
} else if (osId.includes("fedora")) {
root.osLogo = "\uf30a"
root.osName = "Fedora"
} else if (osId.includes("debian")) {
root.osLogo = "\uf306"
root.osName = "Debian"
} else if (osId.includes("opensuse")) {
root.osLogo = "\uef6d"
root.osName = "openSUSE"
} else if (osId.includes("manjaro")) {
root.osLogo = "\uf312"
root.osName = "Manjaro"
} else {
root.osLogo = "\uf033"
root.osName = "Linux"
}
}
}
}
onExited: (exitCode) => {
if (exitCode !== 0) {
// Ultimate fallback - use generic apps icon (empty logo means fallback to "apps")
root.osLogo = ""
root.osName = "Linux"
console.log("OS detection failed, using generic icon")
}
}
}
// Color Picker Process
Process {
id: colorPickerProcess
command: ["hyprpicker", "-a"]
running: false
onExited: (exitCode) => {
if (exitCode !== 0) {
console.warn("Color picker failed. Make sure hyprpicker is installed: yay -S hyprpicker")
}
}
}
// Notification Server
NotificationServer {
id: notificationServer
actionsSupported: true
bodyMarkupSupported: true
imageSupported: true
keepOnReload: false
persistenceSupported: true
onNotification: (notification) => {
if (!notification || !notification.id) return
// Filter empty notifications
if (!notification.appName && !notification.summary && !notification.body) {
return
}
console.log("New notification from:", notification.appName || "Unknown", "Summary:", notification.summary || "No summary")
// Create notification object with correct properties
var notifObj = {
"id": notification.id,
"appName": notification.appName || "App",
"summary": notification.summary || "",
"body": notification.body || "",
"timestamp": new Date(),
"appIcon": notification.appIcon || notification.icon || "",
"icon": notification.icon || "",
"image": notification.image || ""
}
// Add to history (prepend to show newest first)
notificationHistory.insert(0, notifObj)
// Keep only last 50 notifications
while (notificationHistory.count > 50) {
notificationHistory.remove(notificationHistory.count - 1)
}
// Show popup notification
root.activeNotification = notifObj
root.showNotificationPopup = true
notificationTimer.restart()
}
}
// Notification History Model
ListModel {
id: notificationHistory
}
// Weather Service
Process {
id: weatherFetcher
command: ["bash", "-c", "curl -s 'wttr.in/?format=j1' | jq '{current: .current_condition[0], location: .nearest_area[0], astronomy: .weather[0].astronomy[0]}'"]
running: false
stdout: StdioCollector {
onStreamFinished: {
if (text.trim() && text.trim().startsWith("{")) {
try {
let parsedData = JSON.parse(text.trim())
if (parsedData.current && parsedData.location) {
root.weather = {
available: true,
temp: parseInt(parsedData.current.temp_C || 0),
tempF: parseInt(parsedData.current.temp_F || 0),
city: parsedData.location.areaName[0]?.value || "Unknown",
wCode: parsedData.current.weatherCode || "113",
humidity: parseInt(parsedData.current.humidity || 0),
wind: (parsedData.current.windspeedKmph || 0) + " km/h",
sunrise: parsedData.astronomy?.sunrise || "06:00",
sunset: parsedData.astronomy?.sunset || "18:00",
uv: parseInt(parsedData.current.uvIndex || 0),
pressure: parseInt(parsedData.current.pressure || 0)
}
console.log("Weather updated:", root.weather.city, root.weather.temp + "°C")
}
} catch (e) {
console.warn("Failed to parse weather data:", e.message)
root.weather.available = false
}
} else {
console.warn("No valid weather data received")
root.weather.available = false
}
}
}
onExited: (exitCode) => {
if (exitCode !== 0) {
console.warn("Weather fetch failed with exit code:", exitCode)
root.weather.available = false
}
}
}
// Weather fetch timer (every 10 minutes)
Timer {
interval: 600000 // 10 minutes
running: true
repeat: true
triggeredOnStart: true
onTriggered: {
weatherFetcher.running = true
}
}
Timer {
running: root.activePlayer?.playbackState === MprisPlaybackState.Playing
interval: 1000
repeat: true
onTriggered: {
if (root.activePlayer) {
root.activePlayer.positionChanged()
}
}
}
Component.onCompleted: {
console.log("DankMaterialDark shell loaded successfully!")
}
}