add crypto

This commit is contained in:
zack 2025-07-22 20:21:21 -04:00
parent 90cbe489f6
commit af6a3bce3e
Signed by: zoey
GPG key ID: 81FB9FECDD6A33E2
120 changed files with 24616 additions and 462 deletions

View file

@ -0,0 +1,125 @@
// Calendar.qml
import QtQuick
import QtQuick.Layouts
import QtQuick.Controls
import "root:/Data" as Data
// Calendar widget with navigation
Rectangle {
id: calendarRoot
property var shell
radius: 20
color: Qt.lighter(Data.ThemeManager.bgColor, 1.2)
readonly property date currentDate: new Date()
property int month: currentDate.getMonth()
property int year: currentDate.getFullYear()
readonly property int currentDay: currentDate.getDate()
ColumnLayout {
anchors.fill: parent
anchors.margins: 12
spacing: 12
// Month/Year header
RowLayout {
Layout.fillWidth: true
spacing: 8
// Reusable navigation button
component NavButton: AbstractButton {
property alias buttonText: buttonLabel.text
implicitWidth: 30
implicitHeight: 30
background: Rectangle {
radius: 15
color: parent.down ? Qt.darker(Data.ThemeManager.accentColor, 1.2) :
parent.hovered ? Qt.lighter(Data.ThemeManager.highlightBg, 1.1) : Data.ThemeManager.highlightBg
}
Text {
id: buttonLabel
anchors.centerIn: parent
color: Data.ThemeManager.fgColor
font.pixelSize: 16
font.bold: true
}
}
// Current month and year display
Text {
text: Qt.locale("en_US").monthName(calendarRoot.month) + " " + calendarRoot.year
color: Data.ThemeManager.accentColor
font.bold: true
Layout.fillWidth: true
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
font.pixelSize: 18
}
}
// Weekday headers (Monday-Sunday)
Grid {
columns: 7
rowSpacing: 4
columnSpacing: 0
Layout.leftMargin: 2
Layout.fillWidth: true
Repeater {
model: ["M", "T", "W", "T", "F", "S", "S"]
delegate: Text {
text: modelData
color: Data.ThemeManager.fgColor
font.bold: true
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
width: parent.width / 7
font.pixelSize: 14
}
}
}
// Calendar grid
MonthGrid {
id: monthGrid
month: calendarRoot.month
year: calendarRoot.year
Layout.fillWidth: true
Layout.fillHeight: true
spacing: 4
leftPadding: 0
rightPadding: 0
locale: Qt.locale("en_US")
implicitHeight: 400
delegate: Rectangle {
width: 30
height: 30
radius: 15
readonly property bool isCurrentMonth: model.month === calendarRoot.month
readonly property bool isToday: model.day === calendarRoot.currentDay &&
model.month === calendarRoot.currentDate.getMonth() &&
calendarRoot.year === calendarRoot.currentDate.getFullYear() &&
isCurrentMonth
// Dynamic styling: today = accent color, current month = normal, other months = dimmed
color: isToday ? Data.ThemeManager.accentColor :
isCurrentMonth ? Data.ThemeManager.bgColor : Qt.darker(Data.ThemeManager.bgColor, 1.4)
Text {
text: model.day
anchors.centerIn: parent
color: isToday ? Data.ThemeManager.bgColor :
isCurrentMonth ? Data.ThemeManager.fgColor : Qt.darker(Data.ThemeManager.fgColor, 1.5)
font.bold: isToday
font.pixelSize: 14
font.family: "Roboto"
}
}
}
}
}

View file

@ -0,0 +1,121 @@
import QtQuick
import QtQuick.Controls
import "root:/Data" as Data
// Calendar popup with animations
Popup {
id: calendarPopup
property bool hovered: false
property bool clickMode: false // Persistent mode - stays open until clicked again
property var shell
property int targetX: 0
readonly property int targetY: Screen.height - height
width: 280
height: 280
modal: false
focus: true
closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside
padding: 15
// Animation state properties
property bool _visible: false
property real animX: targetX - 20
property real animOpacity: 0
x: animX
y: targetY
opacity: animOpacity
visible: _visible
// Smooth slide-in animation
Behavior on animX {
NumberAnimation { duration: 200; easing.type: Easing.InOutQuad }
}
Behavior on animOpacity {
NumberAnimation { duration: 200; easing.type: Easing.InOutQuad }
}
// Hover mode: show/hide based on mouse state
onHoveredChanged: {
if (!clickMode) {
if (hovered) {
_visible = true
animX = targetX
animOpacity = 1
} else {
animX = targetX - 20
animOpacity = 0
}
}
}
// Click mode: persistent visibility toggle
onClickModeChanged: {
if (clickMode) {
_visible = true
animX = targetX
animOpacity = 1
} else {
animX = targetX - 20
animOpacity = 0
}
}
// Hide when animation completes
onAnimOpacityChanged: {
if (animOpacity === 0 && !hovered && !clickMode) {
_visible = false
}
}
function setHovered(state) {
hovered = state
}
function setClickMode(state) {
clickMode = state
}
// Hover detection
MouseArea {
id: hoverArea
anchors.fill: parent
hoverEnabled: true
propagateComposedEvents: true
anchors.margins: 10 // Larger area to reduce flicker
onEntered: {
if (!clickMode) {
setHovered(true)
}
}
onExited: {
if (!clickMode) {
// Delayed exit check to prevent hover flicker
Qt.callLater(() => {
if (!hoverArea.containsMouse) {
setHovered(false)
}
})
}
}
}
// Lazy-loaded calendar content
Loader {
anchors.fill: parent
active: calendarPopup._visible
source: active ? "Calendar.qml" : ""
onLoaded: {
if (item) {
item.shell = calendarPopup.shell
}
}
}
background: Rectangle {
color: Data.ThemeManager.bgColor
topRightRadius: 20
}
}

View file

@ -0,0 +1,64 @@
import QtQuick
import QtQuick.Controls
import Quickshell
import "root:/Data" as Data
import "root:/Core" as Core
// Clock with border integration
Item {
id: clockRoot
width: clockBackground.width
height: clockBackground.height
Rectangle {
id: clockBackground
width: clockText.implicitWidth + 24
height: 32
color: Data.ThemeManager.bgColor
// Rounded corner for border integration
topRightRadius: height / 2
Text {
id: clockText
anchors.centerIn: parent
font.family: "Roboto"
font.pixelSize: 14
font.bold: true
color: Data.ThemeManager.accentColor
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
text: Qt.formatTime(new Date(), "HH:mm")
}
}
// Update every minute
Timer {
interval: 60000
running: true
repeat: true
onTriggered: clockText.text = Qt.formatTime(new Date(), "HH:mm")
}
// Border integration corner pieces
Core.Corners {
id: topLeftCorner
position: "topleft"
size: 1.3
fillColor: Data.ThemeManager.bgColor
offsetX: -39
offsetY: -26
z: 0 // Same z-level as clock background
}
Core.Corners {
id: topLeftCorner2
position: "topleft"
size: 1.3
fillColor: Data.ThemeManager.bgColor
offsetX: 20
offsetY: 6
z: 0 // Same z-level as clock background
}
}

View file

@ -0,0 +1,136 @@
import QtQuick
import "root:/Data" as Data
import "root:/Core" as Core
// Main control panel coordinator - handles recording and system actions
Item {
id: controlPanelContainer
required property var shell
property bool isRecording: false
property int currentTab: 0 // 0=main, 1=calendar, 2=clipboard, 3=notifications, 4=wallpapers, 5=music, 6=settings
property var tabIcons: ["widgets", "calendar_month", "content_paste", "notifications", "wallpaper", "music_note", "settings"]
property bool isShown: false
property var recordingProcess: null
signal recordingRequested()
signal stopRecordingRequested()
signal systemActionRequested(string action)
signal performanceActionRequested(string action)
// Screen recording
onRecordingRequested: {
var currentDate = new Date()
var hours = String(currentDate.getHours()).padStart(2, '0')
var minutes = String(currentDate.getMinutes()).padStart(2, '0')
var day = String(currentDate.getDate()).padStart(2, '0')
var month = String(currentDate.getMonth() + 1).padStart(2, '0')
var year = currentDate.getFullYear()
var filename = hours + "-" + minutes + "-" + day + "-" + month + "-" + year + ".mp4"
var outputPath = Data.Settings.videoPath + filename
var command = "gpu-screen-recorder -w portal -f 60 -a default_output -o " + outputPath
var qmlString = 'import Quickshell.Io; Process { command: ["sh", "-c", "' + command + '"]; running: true }'
try {
recordingProcess = Qt.createQmlObject(qmlString, controlPanelContainer)
isRecording = true
} catch (e) {
console.error("Failed to start recording:", e)
}
}
// Stop recording with cleanup
onStopRecordingRequested: {
if (recordingProcess && isRecording) {
var stopQmlString = 'import Quickshell.Io; Process { command: ["sh", "-c", "pkill -SIGINT -f \'gpu-screen-recorder.*portal\'"]; running: true; onExited: function() { destroy() } }'
try {
var stopProcess = Qt.createQmlObject(stopQmlString, controlPanelContainer)
var cleanupTimer = Qt.createQmlObject('import QtQuick; Timer { interval: 3000; running: true; repeat: false }', controlPanelContainer)
cleanupTimer.triggered.connect(function() {
if (recordingProcess) {
recordingProcess.running = false
recordingProcess.destroy()
recordingProcess = null
}
var forceKillQml = 'import Quickshell.Io; Process { command: ["sh", "-c", "pkill -9 -f \'gpu-screen-recorder.*portal\' 2>/dev/null || true"]; running: true; onExited: function() { destroy() } }'
var forceKillProcess = Qt.createQmlObject(forceKillQml, controlPanelContainer)
cleanupTimer.destroy()
})
} catch (e) {
console.error("Failed to stop recording:", e)
}
}
isRecording = false
}
// System action routing
onSystemActionRequested: function(action) {
switch(action) {
case "lock":
Core.ProcessManager.lock()
break
case "reboot":
Core.ProcessManager.reboot()
break
case "shutdown":
Core.ProcessManager.shutdown()
break
}
}
onPerformanceActionRequested: function(action) {
console.log("Performance action requested:", action)
}
// Control panel window component
ControlPanelWindow {
id: controlPanelWindow
// Pass through properties
shell: controlPanelContainer.shell
isRecording: controlPanelContainer.isRecording
currentTab: controlPanelContainer.currentTab
tabIcons: controlPanelContainer.tabIcons
isShown: controlPanelContainer.isShown
// Bind state changes back to parent
onCurrentTabChanged: controlPanelContainer.currentTab = currentTab
onIsShownChanged: controlPanelContainer.isShown = isShown
// Forward signals
onRecordingRequested: controlPanelContainer.recordingRequested()
onStopRecordingRequested: controlPanelContainer.stopRecordingRequested()
onSystemActionRequested: function(action) { controlPanelContainer.systemActionRequested(action) }
onPerformanceActionRequested: function(action) { controlPanelContainer.performanceActionRequested(action) }
}
// Clean up processes on destruction
Component.onDestruction: {
if (recordingProcess) {
try {
if (recordingProcess.running) {
recordingProcess.terminate()
}
recordingProcess.destroy()
} catch (e) {
console.warn("Error cleaning up recording process:", e)
}
recordingProcess = null
}
// Force kill any remaining gpu-screen-recorder processes
var forceCleanupCmd = 'import Quickshell.Io; Process { command: ["sh", "-c", "pkill -9 -f gpu-screen-recorder 2>/dev/null || true"]; running: true; onExited: function() { destroy() } }'
try {
Qt.createQmlObject(forceCleanupCmd, controlPanelContainer)
} catch (e) {
console.warn("Error in force cleanup:", e)
}
}
}

View file

@ -0,0 +1,106 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Qt5Compat.GraphicalEffects
import "root:/Data" as Data
import "./components/navigation" as Navigation
// Panel content with tab layout - now clean and organized!
Item {
id: contentRoot
// Properties passed from parent
required property var shell
required property bool isRecording
property int currentTab: 0
property var tabIcons: []
required property var triggerMouseArea
// Signals to forward to parent
signal recordingRequested()
signal stopRecordingRequested()
signal systemActionRequested(string action)
signal performanceActionRequested(string action)
// Hover detection for auto-hide
property bool isHovered: {
const mouseStates = {
triggerHovered: triggerMouseArea.containsMouse,
backgroundHovered: backgroundMouseArea.containsMouse,
tabSidebarHovered: tabNavigation.containsMouse,
tabContainerHovered: tabContainer.isHovered,
tabContentActive: currentTab !== 0, // Non-main tabs stay open
tabNavigationActive: tabNavigation.containsMouse
}
return Object.values(mouseStates).some(state => state)
}
// Expose text input focus state for keyboard management
property bool textInputFocused: tabContainer.textInputFocused
// Panel background with bottom-only rounded corners
Rectangle {
anchors.fill: parent
color: Data.ThemeManager.bgColor
topLeftRadius: 0
topRightRadius: 0
bottomLeftRadius: 20
bottomRightRadius: 20
z: -10 // Far behind everything to avoid layering conflicts
}
// Main content container with tab layout
Rectangle {
id: mainContainer
anchors.fill: parent
anchors.margins: 9
color: "transparent"
radius: 12
MouseArea {
id: backgroundMouseArea
anchors.fill: parent
hoverEnabled: true
propagateComposedEvents: true
property alias containsMouse: backgroundMouseArea.containsMouse
}
// Left sidebar with tab navigation
Navigation.TabNavigation {
id: tabNavigation
width: 40
height: parent.height
anchors.left: parent.left
anchors.leftMargin: 9
anchors.top: parent.top
anchors.topMargin: 18
currentTab: contentRoot.currentTab
tabIcons: contentRoot.tabIcons
onCurrentTabChanged: contentRoot.currentTab = currentTab
}
// Main tab content area with sliding animation
Navigation.TabContainer {
id: tabContainer
width: parent.width - tabNavigation.width - 45
height: parent.height - 36
anchors.left: tabNavigation.right
anchors.leftMargin: 9
anchors.top: parent.top
anchors.topMargin: 18
shell: contentRoot.shell
isRecording: contentRoot.isRecording
triggerMouseArea: contentRoot.triggerMouseArea
currentTab: contentRoot.currentTab
onRecordingRequested: contentRoot.recordingRequested()
onStopRecordingRequested: contentRoot.stopRecordingRequested()
onSystemActionRequested: function(action) { contentRoot.systemActionRequested(action) }
onPerformanceActionRequested: function(action) { contentRoot.performanceActionRequested(action) }
}
}
}

View file

@ -0,0 +1,171 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Qt5Compat.GraphicalEffects
import Quickshell
import Quickshell.Wayland
import "root:/Data" as Data
import "root:/Core" as Core
// Control panel window and trigger
PanelWindow {
id: controlPanelWindow
// Properties passed from parent ControlPanel
required property var shell
required property bool isRecording
property int currentTab: 0
property var tabIcons: []
property bool isShown: false
// Signals to forward to parent
signal recordingRequested()
signal stopRecordingRequested()
signal systemActionRequested(string action)
signal performanceActionRequested(string action)
screen: Quickshell.primaryScreen || Quickshell.screens[0]
anchors.top: true
anchors.left: true
anchors.right: true
margins.bottom: 0
margins.left: (screen ? screen.width / 2 - 400 : 0) // Centered
margins.right: (screen ? screen.width / 2 - 400 : 0)
implicitWidth: 640
implicitHeight: isShown ? 400 : 8 // Expand/collapse animation
Behavior on implicitHeight {
NumberAnimation {
duration: 200
easing.type: Easing.OutCubic
}
}
exclusiveZone: (panelContent && panelContent.textInputFocused) ? -1 : 0
color: "transparent"
visible: true
WlrLayershell.namespace: "quickshell-controlpanel"
WlrLayershell.keyboardFocus: (panelContent && panelContent.textInputFocused) ? WlrKeyboardFocus.Exclusive : WlrKeyboardFocus.OnDemand
// Hover trigger area at screen top
MouseArea {
id: triggerMouseArea
anchors.top: parent.top
anchors.horizontalCenter: parent.horizontalCenter
width: 600
height: 8
hoverEnabled: true
onContainsMouseChanged: {
if (containsMouse) {
show()
}
}
}
// Main panel content
ControlPanelContent {
id: panelContent
width: 600
height: 380
anchors.top: parent.top
anchors.topMargin: 8 // Trigger area space
anchors.horizontalCenter: parent.horizontalCenter
visible: isShown
opacity: isShown ? 1.0 : 0.0
Behavior on opacity {
NumberAnimation {
duration: 150
easing.type: Easing.OutCubic
}
}
// Pass through properties
shell: controlPanelWindow.shell
isRecording: controlPanelWindow.isRecording
currentTab: controlPanelWindow.currentTab
tabIcons: controlPanelWindow.tabIcons
triggerMouseArea: triggerMouseArea
// Bind state changes
onCurrentTabChanged: controlPanelWindow.currentTab = currentTab
// Forward signals
onRecordingRequested: controlPanelWindow.recordingRequested()
onStopRecordingRequested: controlPanelWindow.stopRecordingRequested()
onSystemActionRequested: function(action) { controlPanelWindow.systemActionRequested(action) }
onPerformanceActionRequested: function(action) { controlPanelWindow.performanceActionRequested(action) }
// Hover state management
onIsHoveredChanged: {
if (isHovered) {
hideTimer.stop()
} else {
hideTimer.restart()
}
}
}
// Border integration corners (positioned to match panel edges)
Core.Corners {
id: controlPanelLeftCorner
position: "bottomright"
size: 1.3
fillColor: Data.ThemeManager.bgColor
offsetX: -661
offsetY: -313
visible: isShown
z: 1 // Higher z-index to render above shadow effects
// Disable implicit animations to prevent corner sliding
Behavior on x { enabled: false }
Behavior on y { enabled: false }
}
Core.Corners {
id: controlPanelRightCorner
position: "bottomleft"
size: 1.3
fillColor: Data.ThemeManager.bgColor
offsetX: 661
offsetY: -313
visible: isShown
z: 1 // Higher z-index to render above shadow effects
Behavior on x { enabled: false }
Behavior on y { enabled: false }
}
// Auto-hide timer
Timer {
id: hideTimer
interval: 400
repeat: false
onTriggered: hide()
}
function show() {
if (isShown) return
isShown = true
hideTimer.stop()
}
function hide() {
if (!isShown) return
// Only hide if on main tab and nothing is being hovered
if (currentTab === 0 && !panelContent.isHovered && !triggerMouseArea.containsMouse) {
isShown = false
}
// For non-main tabs, only hide if explicitly not hovered and no trigger hover
else if (currentTab !== 0 && !panelContent.isHovered && !triggerMouseArea.containsMouse) {
// Add delay for non-main tabs to prevent accidental hiding
Qt.callLater(function() {
if (!panelContent.isHovered && !triggerMouseArea.containsMouse) {
isShown = false
}
})
}
}
}

View file

@ -0,0 +1,107 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import "root:/Data" as Data
import "." as Controls
// Dual-section control panel
Row {
id: root
spacing: 16
visible: true
height: 80
required property bool isRecording
required property var shell
signal performanceActionRequested(string action)
signal systemActionRequested(string action)
signal mouseChanged(bool containsMouse)
// Combined hover state from both sections
property bool containsMouse: performanceSection.containsMouse || systemSection.containsMouse
onContainsMouseChanged: mouseChanged(containsMouse)
// Performance controls section (left half)
Rectangle {
id: performanceSection
width: (parent.width - parent.spacing) / 2
height: parent.height
radius: 20
color: Qt.darker(Data.ThemeManager.bgColor, 1.15)
visible: true
// Hover tracking with coordination between background and content
property bool containsMouse: performanceMouseArea.containsMouse || performanceControls.containsMouse
MouseArea {
id: performanceMouseArea
anchors.fill: parent
hoverEnabled: true
propagateComposedEvents: true
onContainsMouseChanged: {
if (containsMouse) {
performanceSection.containsMouse = true
} else if (!performanceControls.containsMouse) {
performanceSection.containsMouse = false
}
}
}
Controls.PerformanceControls {
id: performanceControls
anchors.fill: parent
anchors.margins: 12
shell: root.shell
onPerformanceActionRequested: function(action) { root.performanceActionRequested(action) }
onMouseChanged: function(containsMouse) {
if (containsMouse) {
performanceSection.containsMouse = true
} else if (!performanceMouseArea.containsMouse) {
performanceSection.containsMouse = false
}
}
}
}
// System controls section (right half)
Rectangle {
id: systemSection
width: (parent.width - parent.spacing) / 2
height: parent.height
radius: 20
color: Qt.darker(Data.ThemeManager.bgColor, 1.15)
visible: true
// Hover tracking with coordination between background and content
property bool containsMouse: systemMouseArea.containsMouse || systemControls.containsMouse
MouseArea {
id: systemMouseArea
anchors.fill: parent
hoverEnabled: true
propagateComposedEvents: true
onContainsMouseChanged: {
if (containsMouse) {
systemSection.containsMouse = true
} else if (!systemControls.containsMouse) {
systemSection.containsMouse = false
}
}
}
Controls.SystemControls {
id: systemControls
anchors.fill: parent
anchors.margins: 12
shell: root.shell
onSystemActionRequested: function(action) { root.systemActionRequested(action) }
onMouseChanged: function(containsMouse) {
if (containsMouse) {
systemSection.containsMouse = true
} else if (!systemMouseArea.containsMouse) {
systemSection.containsMouse = false
}
}
}
}
}

View file

@ -0,0 +1,127 @@
import QtQuick
import QtQuick.Controls
import Quickshell.Services.UPower
// Power profile controls
Column {
id: root
required property var shell
spacing: 8
signal performanceActionRequested(string action)
signal mouseChanged(bool containsMouse)
readonly property bool containsMouse: performanceButton.containsMouse ||
balancedButton.containsMouse ||
powerSaverButton.containsMouse
// Safe UPower service access with fallback checks
readonly property bool upowerReady: typeof PowerProfiles !== 'undefined' && PowerProfiles
readonly property int currentProfile: upowerReady ? PowerProfiles.profile : 0
onContainsMouseChanged: root.mouseChanged(containsMouse)
opacity: visible ? 1 : 0
Behavior on opacity {
NumberAnimation {
duration: 300
easing.type: Easing.OutCubic
}
}
Row {
spacing: 8
width: parent.width
// Performance mode button
SystemButton {
id: performanceButton
width: (parent.width - parent.spacing * 2) / 3
height: 52
shell: root.shell
iconText: "speed"
isActive: root.upowerReady && (typeof PowerProfile !== 'undefined') ?
root.currentProfile === PowerProfile.Performance : false
onClicked: {
if (root.upowerReady && typeof PowerProfile !== 'undefined') {
PowerProfiles.profile = PowerProfile.Performance
root.performanceActionRequested("performance")
} else {
console.warn("PowerProfiles not available")
}
}
onMouseChanged: function(containsMouse) {
if (!containsMouse && !root.containsMouse) {
root.mouseChanged(false)
}
}
}
// Balanced mode button
SystemButton {
id: balancedButton
width: (parent.width - parent.spacing * 2) / 3
height: 52
shell: root.shell
iconText: "balance"
isActive: root.upowerReady && (typeof PowerProfile !== 'undefined') ?
root.currentProfile === PowerProfile.Balanced : false
onClicked: {
if (root.upowerReady && typeof PowerProfile !== 'undefined') {
PowerProfiles.profile = PowerProfile.Balanced
root.performanceActionRequested("balanced")
} else {
console.warn("PowerProfiles not available")
}
}
onMouseChanged: function(containsMouse) {
if (!containsMouse && !root.containsMouse) {
root.mouseChanged(false)
}
}
}
// Power saver mode button
SystemButton {
id: powerSaverButton
width: (parent.width - parent.spacing * 2) / 3
height: 52
shell: root.shell
iconText: "battery_saver"
isActive: root.upowerReady && (typeof PowerProfile !== 'undefined') ?
root.currentProfile === PowerProfile.PowerSaver : false
onClicked: {
if (root.upowerReady && typeof PowerProfile !== 'undefined') {
PowerProfiles.profile = PowerProfile.PowerSaver
root.performanceActionRequested("powersaver")
} else {
console.warn("PowerProfiles not available")
}
}
onMouseChanged: function(containsMouse) {
if (!containsMouse && !root.containsMouse) {
root.mouseChanged(false)
}
}
}
}
// Ensure UPower service initialization
Component.onCompleted: {
Qt.callLater(function() {
if (!root.upowerReady) {
console.warn("UPower service not ready - performance controls may not work correctly")
}
})
}
}

View file

@ -0,0 +1,122 @@
import QtQuick
import QtQuick.Controls
import "root:/Data" as Data
// System button
Rectangle {
id: root
required property var shell
required property string iconText
property string labelText: ""
property bool isActive: false
radius: 20
// Dynamic color based on active and hover states
color: {
if (isActive) {
return mouseArea.containsMouse ?
Qt.lighter(Data.ThemeManager.accentColor, 1.1) :
Qt.rgba(Data.ThemeManager.accentColor.r, Data.ThemeManager.accentColor.g, Data.ThemeManager.accentColor.b, 0.3)
} else {
return mouseArea.containsMouse ?
Qt.lighter(Data.ThemeManager.accentColor, 1.2) :
Qt.lighter(Data.ThemeManager.bgColor, 1.15)
}
}
border.width: isActive ? 2 : 1
border.color: isActive ? Data.ThemeManager.accentColor : Qt.lighter(Data.ThemeManager.bgColor, 1.3)
signal clicked()
signal mouseChanged(bool containsMouse)
property bool isHovered: mouseArea.containsMouse
readonly property alias containsMouse: mouseArea.containsMouse
// Smooth color transitions
Behavior on color {
ColorAnimation {
duration: 200
easing.type: Easing.OutCubic
}
}
Behavior on border.color {
ColorAnimation {
duration: 200
easing.type: Easing.OutCubic
}
}
// Hover scale animation
scale: isHovered ? 1.05 : 1.0
Behavior on scale {
NumberAnimation {
duration: 200
easing.type: Easing.OutCubic
}
}
// Button content with icon and optional label
Column {
anchors.centerIn: parent
spacing: 2
// System action icon
Text {
text: root.iconText
font.family: "Material Symbols Outlined"
font.pixelSize: 16
anchors.horizontalCenter: parent.horizontalCenter
color: {
if (root.isActive) {
return root.isHovered ? "#ffffff" : Data.ThemeManager.accentColor
} else {
return root.isHovered ? "#ffffff" : Data.ThemeManager.accentColor
}
}
Behavior on color {
ColorAnimation {
duration: 200
easing.type: Easing.OutCubic
}
}
}
// Optional text label
Label {
text: root.labelText
font.family: "Roboto"
font.pixelSize: 8
color: {
if (root.isActive) {
return root.isHovered ? "#ffffff" : Data.ThemeManager.accentColor
} else {
return root.isHovered ? "#ffffff" : Data.ThemeManager.accentColor
}
}
anchors.horizontalCenter: parent.horizontalCenter
font.weight: root.isActive ? Font.Bold : Font.Medium
visible: root.labelText !== ""
Behavior on color {
ColorAnimation {
duration: 200
easing.type: Easing.OutCubic
}
}
}
}
// Click and hover handling
MouseArea {
id: mouseArea
anchors.fill: parent
hoverEnabled: true
onContainsMouseChanged: root.mouseChanged(containsMouse)
onClicked: root.clicked()
}
}

View file

@ -0,0 +1,93 @@
import QtQuick
import QtQuick.Layouts
import QtQuick.Controls
// System action buttons
RowLayout {
id: root
required property var shell
spacing: 8
signal systemActionRequested(string action)
signal mouseChanged(bool containsMouse)
readonly property bool containsMouse: lockButton.containsMouse ||
rebootButton.containsMouse ||
shutdownButton.containsMouse
onContainsMouseChanged: root.mouseChanged(containsMouse)
opacity: visible ? 1 : 0
Behavior on opacity {
NumberAnimation {
duration: 300
easing.type: Easing.OutCubic
}
}
// Lock Button
SystemButton {
id: lockButton
Layout.fillHeight: true
Layout.fillWidth: true
shell: root.shell
iconText: "lock"
onClicked: {
console.log("Lock button clicked")
console.log("root.shell:", root.shell)
console.log("root.shell.lockscreen:", root.shell ? root.shell.lockscreen : "shell is null")
// Directly trigger custom lockscreen
if (root.shell && root.shell.lockscreen) {
console.log("Calling root.shell.lockscreen.lock()")
root.shell.lockscreen.lock()
} else {
console.log("Fallback to systemActionRequested")
// Fallback to system action for compatibility
root.systemActionRequested("lock")
}
}
onMouseChanged: function(containsMouse) {
if (!containsMouse && !root.containsMouse) {
root.mouseChanged(false)
}
}
}
// Reboot Button
SystemButton {
id: rebootButton
Layout.fillHeight: true
Layout.fillWidth: true
shell: root.shell
iconText: "restart_alt"
onClicked: root.systemActionRequested("reboot")
onMouseChanged: function(containsMouse) {
if (!containsMouse && !root.containsMouse) {
root.mouseChanged(false)
}
}
}
// Shutdown Button
SystemButton {
id: shutdownButton
Layout.fillHeight: true
Layout.fillWidth: true
shell: root.shell
iconText: "power_settings_new"
onClicked: root.systemActionRequested("shutdown")
onMouseChanged: function(containsMouse) {
if (!containsMouse && !root.containsMouse) {
root.mouseChanged(false)
}
}
}
}

View file

@ -0,0 +1,666 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Qt5Compat.GraphicalEffects
import Quickshell.Services.Mpris
import "root:/Data" as Data
// Music player with MPRIS integration
Rectangle {
id: musicPlayer
property var shell
property var currentPlayer: null
property real currentPosition: 0
property int selectedPlayerIndex: 0
color: "transparent"
// Get all available players
function getAvailablePlayers() {
if (!Mpris.players || !Mpris.players.values) {
return []
}
let allPlayers = Mpris.players.values
let controllablePlayers = []
for (let i = 0; i < allPlayers.length; i++) {
let player = allPlayers[i]
if (player && player.canControl) {
controllablePlayers.push(player)
}
}
return controllablePlayers
}
// Find the active player (either selected or first available)
function findActivePlayer() {
let availablePlayers = getAvailablePlayers()
if (availablePlayers.length === 0) {
return null
}
// Auto-switch to playing player if enabled
if (Data.Settings.autoSwitchPlayer) {
for (let i = 0; i < availablePlayers.length; i++) {
if (availablePlayers[i].isPlaying) {
selectedPlayerIndex = i
return availablePlayers[i]
}
}
}
// Use selected player if valid, otherwise use first available
if (selectedPlayerIndex < availablePlayers.length) {
return availablePlayers[selectedPlayerIndex]
} else {
selectedPlayerIndex = 0
return availablePlayers[0]
}
}
// Update current player
function updateCurrentPlayer() {
let newPlayer = findActivePlayer()
if (newPlayer !== currentPlayer) {
currentPlayer = newPlayer
currentPosition = currentPlayer ? currentPlayer.position : 0
}
}
// Timer to update progress bar position
Timer {
id: positionTimer
interval: 1000
running: currentPlayer && currentPlayer.isPlaying
repeat: true
onTriggered: {
if (currentPlayer) {
currentPosition = currentPlayer.position
}
}
}
// Timer to check for auto-switching to playing players
Timer {
id: autoSwitchTimer
interval: 2000 // Check every 2 seconds
running: Data.Settings.autoSwitchPlayer
repeat: true
onTriggered: {
if (Data.Settings.autoSwitchPlayer) {
let availablePlayers = getAvailablePlayers()
for (let i = 0; i < availablePlayers.length; i++) {
if (availablePlayers[i].isPlaying && selectedPlayerIndex !== i) {
selectedPlayerIndex = i
updateCurrentPlayer()
updatePlayerList()
break
}
}
}
}
}
// Update player list for dropdown
function updatePlayerList() {
if (!playerComboBox) return
let availablePlayers = getAvailablePlayers()
let playerNames = availablePlayers.map(player => player.identity || "Unknown Player")
playerComboBox.model = playerNames
if (selectedPlayerIndex >= playerNames.length) {
selectedPlayerIndex = 0
}
playerComboBox.currentIndex = selectedPlayerIndex
}
// Monitor for player changes
Connections {
target: Mpris.players
function onValuesChanged() {
updatePlayerList()
updateCurrentPlayer()
}
function onRowsInserted() {
updatePlayerList()
updateCurrentPlayer()
}
function onRowsRemoved() {
updatePlayerList()
updateCurrentPlayer()
}
function onObjectInsertedPost() {
updatePlayerList()
updateCurrentPlayer()
}
function onObjectRemovedPost() {
updatePlayerList()
updateCurrentPlayer()
}
}
// Monitor for settings changes
Connections {
target: Data.Settings
function onAutoSwitchPlayerChanged() {
console.log("Auto-switch player setting changed to:", Data.Settings.autoSwitchPlayer)
updateCurrentPlayer()
}
function onAlwaysShowPlayerDropdownChanged() {
console.log("Always show dropdown setting changed to:", Data.Settings.alwaysShowPlayerDropdown)
// Dropdown visibility is automatically handled by the binding
}
}
Component.onCompleted: {
updatePlayerList()
updateCurrentPlayer()
}
Column {
anchors.fill: parent
spacing: 10
// No music player available state
Item {
width: parent.width
height: parent.height
visible: !currentPlayer
Column {
anchors.centerIn: parent
spacing: 16
Text {
anchors.horizontalCenter: parent.horizontalCenter
text: "music_note"
font.family: "Material Symbols Outlined"
font.pixelSize: 48
color: Qt.rgba(Data.ThemeManager.fgColor.r, Data.ThemeManager.fgColor.g, Data.ThemeManager.fgColor.b, 0.3)
}
Text {
anchors.horizontalCenter: parent.horizontalCenter
text: getAvailablePlayers().length > 0 ? "No controllable player selected" : "No music player detected"
color: Qt.rgba(Data.ThemeManager.fgColor.r, Data.ThemeManager.fgColor.g, Data.ThemeManager.fgColor.b, 0.6)
font.family: "Roboto"
font.pixelSize: 14
}
Text {
anchors.horizontalCenter: parent.horizontalCenter
text: getAvailablePlayers().length > 0 ? "Select a player from the dropdown above" : "Start a music player to see controls"
color: Qt.rgba(Data.ThemeManager.fgColor.r, Data.ThemeManager.fgColor.g, Data.ThemeManager.fgColor.b, 0.4)
font.family: "Roboto"
font.pixelSize: 12
}
}
}
// Music player controls
Column {
width: parent.width
spacing: 12
visible: currentPlayer
// Player info and artwork
Rectangle {
width: parent.width
height: 130
radius: 20
color: Qt.darker(Data.ThemeManager.bgColor, 1.1)
border.color: Qt.rgba(Data.ThemeManager.accentColor.r, Data.ThemeManager.accentColor.g, Data.ThemeManager.accentColor.b, 0.2)
border.width: 1
Row {
anchors.fill: parent
anchors.margins: 16
spacing: 16
// Album artwork
Rectangle {
id: albumArtwork
width: 90
height: 90
radius: 20
color: Qt.darker(Data.ThemeManager.bgColor, 1.3)
border.color: Qt.rgba(Data.ThemeManager.accentColor.r, Data.ThemeManager.accentColor.g, Data.ThemeManager.accentColor.b, 0.3)
border.width: 1
Image {
id: albumArt
anchors.fill: parent
anchors.margins: 2
fillMode: Image.PreserveAspectCrop
smooth: true
source: currentPlayer ? (currentPlayer.trackArtUrl || "") : ""
visible: source.toString() !== ""
// Rounded corners using layer
layer.enabled: true
layer.effect: OpacityMask {
cached: true // Cache to reduce ShaderEffect issues
maskSource: Rectangle {
width: albumArt.width
height: albumArt.height
radius: 20
visible: false
}
}
}
// Fallback music icon
Text {
anchors.centerIn: parent
text: "album"
font.family: "Material Symbols Outlined"
font.pixelSize: 32
color: Qt.rgba(Data.ThemeManager.fgColor.r, Data.ThemeManager.fgColor.g, Data.ThemeManager.fgColor.b, 0.4)
visible: !albumArt.visible
}
}
// Track info
Column {
width: parent.width - albumArtwork.width - parent.spacing
height: parent.height
spacing: 4
Text {
width: parent.width
text: currentPlayer ? (currentPlayer.trackTitle || "Unknown Track") : ""
color: Data.ThemeManager.fgColor
font.family: "Roboto"
font.pixelSize: 18
font.bold: true
elide: Text.ElideRight
wrapMode: Text.Wrap
maximumLineCount: 2
}
Text {
width: parent.width
text: currentPlayer ? (currentPlayer.trackArtist || "Unknown Artist") : ""
color: Qt.rgba(Data.ThemeManager.fgColor.r, Data.ThemeManager.fgColor.g, Data.ThemeManager.fgColor.b, 0.8)
font.family: "Roboto"
font.pixelSize: 18
elide: Text.ElideRight
}
Text {
width: parent.width
text: currentPlayer ? (currentPlayer.trackAlbum || "Unknown Album") : ""
color: Qt.rgba(Data.ThemeManager.fgColor.r, Data.ThemeManager.fgColor.g, Data.ThemeManager.fgColor.b, 0.6)
font.family: "Roboto"
font.pixelSize: 15
elide: Text.ElideRight
}
}
}
}
// Interactive progress bar with seek functionality
Rectangle {
id: progressBarBackground
width: parent.width
height: 8
radius: 20
color: Qt.rgba(Data.ThemeManager.fgColor.r, Data.ThemeManager.fgColor.g, Data.ThemeManager.fgColor.b, 0.15)
property real progressRatio: currentPlayer && currentPlayer.length > 0 ?
(currentPosition / currentPlayer.length) : 0
Rectangle {
id: progressFill
width: progressBarBackground.progressRatio * parent.width
height: parent.height
radius: parent.radius
color: Data.ThemeManager.accentColor
Behavior on width {
NumberAnimation { duration: 200 }
}
}
// Interactive progress handle (circle)
Rectangle {
id: progressHandle
width: 16
height: 16
radius: 8
color: Data.ThemeManager.accentColor
border.color: Qt.lighter(Data.ThemeManager.accentColor, 1.3)
border.width: 1
x: Math.max(0, Math.min(parent.width - width, progressFill.width - width/2))
anchors.verticalCenter: parent.verticalCenter
visible: currentPlayer && currentPlayer.length > 0
scale: progressMouseArea.containsMouse || progressMouseArea.pressed ? 1.2 : 1.0
Behavior on scale {
NumberAnimation { duration: 150 }
}
}
// Mouse area for seeking
MouseArea {
id: progressMouseArea
anchors.fill: parent
hoverEnabled: true
enabled: currentPlayer && currentPlayer.length > 0 && currentPlayer.canSeek
onClicked: function(mouse) {
if (currentPlayer && currentPlayer.length > 0) {
let ratio = mouse.x / width
let seekPosition = ratio * currentPlayer.length
currentPlayer.position = seekPosition
currentPosition = seekPosition
}
}
onPositionChanged: function(mouse) {
if (pressed && currentPlayer && currentPlayer.length > 0) {
let ratio = Math.max(0, Math.min(1, mouse.x / width))
let seekPosition = ratio * currentPlayer.length
currentPlayer.position = seekPosition
currentPosition = seekPosition
}
}
}
}
// Player selection dropdown (conditional visibility)
Rectangle {
width: parent.width
height: 38
radius: 20
color: Qt.darker(Data.ThemeManager.bgColor, 1.1)
border.color: Qt.rgba(Data.ThemeManager.accentColor.r, Data.ThemeManager.accentColor.g, Data.ThemeManager.accentColor.b, 0.2)
border.width: 1
visible: {
let playerCount = getAvailablePlayers().length
let alwaysShow = Data.Settings.alwaysShowPlayerDropdown
let shouldShow = alwaysShow || playerCount > 1
return shouldShow
}
Row {
anchors.fill: parent
anchors.margins: 6
anchors.leftMargin: 12
spacing: 8
Text {
anchors.verticalCenter: parent.verticalCenter
text: "Player:"
color: Qt.rgba(Data.ThemeManager.fgColor.r, Data.ThemeManager.fgColor.g, Data.ThemeManager.fgColor.b, 0.7)
font.family: "Roboto"
font.pixelSize: 12
font.bold: true
}
ComboBox {
id: playerComboBox
anchors.verticalCenter: parent.verticalCenter
width: parent.width - parent.children[0].width - parent.spacing
height: 26
model: []
onActivated: function(index) {
selectedPlayerIndex = index
updateCurrentPlayer()
}
background: Rectangle {
color: Qt.darker(Data.ThemeManager.bgColor, 1.3)
border.color: Qt.rgba(Data.ThemeManager.accentColor.r, Data.ThemeManager.accentColor.g, Data.ThemeManager.accentColor.b, 0.2)
border.width: 1
radius: 20
}
contentItem: Text {
anchors.left: parent.left
anchors.leftMargin: 10
anchors.right: parent.right
anchors.rightMargin: 22
anchors.verticalCenter: parent.verticalCenter
text: playerComboBox.currentText || "No players"
color: Data.ThemeManager.fgColor
font.family: "Roboto"
font.pixelSize: 12
font.bold: true
elide: Text.ElideRight
verticalAlignment: Text.AlignVCenter
}
indicator: Text {
anchors.right: parent.right
anchors.rightMargin: 4
anchors.verticalCenter: parent.verticalCenter
text: "expand_more"
font.family: "Material Symbols Outlined"
font.pixelSize: 12
color: Qt.rgba(Data.ThemeManager.fgColor.r, Data.ThemeManager.fgColor.g, Data.ThemeManager.fgColor.b, 0.6)
}
popup: Popup {
y: playerComboBox.height + 2
width: playerComboBox.width
implicitHeight: contentItem.implicitHeight + 4
background: Rectangle {
color: Qt.darker(Data.ThemeManager.bgColor, 1.2)
border.color: Qt.rgba(Data.ThemeManager.accentColor.r, Data.ThemeManager.accentColor.g, Data.ThemeManager.accentColor.b, 0.3)
border.width: 1
radius: 20
}
contentItem: ListView {
clip: true
implicitHeight: contentHeight
model: playerComboBox.popup.visible ? playerComboBox.delegateModel : null
currentIndex: playerComboBox.highlightedIndex
ScrollIndicator.vertical: ScrollIndicator { }
}
}
delegate: ItemDelegate {
width: playerComboBox.width
height: 28
background: Rectangle {
color: parent.hovered ? Qt.rgba(Data.ThemeManager.accentColor.r, Data.ThemeManager.accentColor.g, Data.ThemeManager.accentColor.b, 0.15) : "transparent"
radius: 20
}
contentItem: Text {
anchors.left: parent.left
anchors.leftMargin: 10
anchors.verticalCenter: parent.verticalCenter
text: modelData || ""
color: Data.ThemeManager.fgColor
font.family: "Roboto"
font.pixelSize: 12
font.bold: true
elide: Text.ElideRight
verticalAlignment: Text.AlignVCenter
}
}
}
}
}
// Media controls
Row {
width: parent.width
height: 35
spacing: 6
// Previous button
Rectangle {
width: (parent.width - parent.spacing * 4) * 0.2
height: parent.height
radius: height / 2
color: previousButton.containsMouse ? Qt.rgba(Data.ThemeManager.accentColor.r, Data.ThemeManager.accentColor.g, Data.ThemeManager.accentColor.b, 0.2) : Qt.darker(Data.ThemeManager.bgColor, 1.1)
border.color: Qt.rgba(Data.ThemeManager.accentColor.r, Data.ThemeManager.accentColor.g, Data.ThemeManager.accentColor.b, 0.3)
border.width: 1
MouseArea {
id: previousButton
anchors.fill: parent
hoverEnabled: true
enabled: currentPlayer && currentPlayer.canGoPrevious
onClicked: if (currentPlayer) currentPlayer.previous()
}
Text {
anchors.centerIn: parent
text: "skip_previous"
font.family: "Material Symbols Outlined"
font.pixelSize: 18
color: previousButton.enabled ? Data.ThemeManager.accentColor : Qt.rgba(Data.ThemeManager.fgColor.r, Data.ThemeManager.fgColor.g, Data.ThemeManager.fgColor.b, 0.3)
}
}
// Play/Pause button
Rectangle {
width: (parent.width - parent.spacing * 4) * 0.3
height: parent.height
radius: height / 2
color: playButton.containsMouse ? Qt.rgba(Data.ThemeManager.accentColor.r, Data.ThemeManager.accentColor.g, Data.ThemeManager.accentColor.b, 0.2) : Qt.darker(Data.ThemeManager.bgColor, 1.1)
border.color: Data.ThemeManager.accentColor
border.width: 2
MouseArea {
id: playButton
anchors.fill: parent
hoverEnabled: true
enabled: currentPlayer && (currentPlayer.canPlay || currentPlayer.canPause)
onClicked: {
if (currentPlayer) {
if (currentPlayer.isPlaying) {
currentPlayer.pause()
} else {
currentPlayer.play()
}
}
}
}
Text {
anchors.centerIn: parent
text: currentPlayer && currentPlayer.isPlaying ? "pause" : "play_arrow"
font.family: "Material Symbols Outlined"
font.pixelSize: 20
color: playButton.enabled ? Data.ThemeManager.accentColor : Qt.rgba(Data.ThemeManager.fgColor.r, Data.ThemeManager.fgColor.g, Data.ThemeManager.fgColor.b, 0.3)
}
}
// Next button
Rectangle {
width: (parent.width - parent.spacing * 4) * 0.2
height: parent.height
radius: height / 2
color: nextButton.containsMouse ? Qt.rgba(Data.ThemeManager.accentColor.r, Data.ThemeManager.accentColor.g, Data.ThemeManager.accentColor.b, 0.2) : Qt.darker(Data.ThemeManager.bgColor, 1.1)
border.color: Qt.rgba(Data.ThemeManager.accentColor.r, Data.ThemeManager.accentColor.g, Data.ThemeManager.accentColor.b, 0.3)
border.width: 1
MouseArea {
id: nextButton
anchors.fill: parent
hoverEnabled: true
enabled: currentPlayer && currentPlayer.canGoNext
onClicked: if (currentPlayer) currentPlayer.next()
}
Text {
anchors.centerIn: parent
text: "skip_next"
font.family: "Material Symbols Outlined"
font.pixelSize: 18
color: nextButton.enabled ? Data.ThemeManager.accentColor : Qt.rgba(Data.ThemeManager.fgColor.r, Data.ThemeManager.fgColor.g, Data.ThemeManager.fgColor.b, 0.3)
}
}
// Shuffle button
Rectangle {
width: (parent.width - parent.spacing * 4) * 0.15
height: parent.height
radius: height / 2
color: shuffleButton.containsMouse ? Qt.rgba(Data.ThemeManager.accentColor.r, Data.ThemeManager.accentColor.g, Data.ThemeManager.accentColor.b, 0.2) : Qt.darker(Data.ThemeManager.bgColor, 1.1)
border.color: currentPlayer && currentPlayer.shuffle ? Data.ThemeManager.accentColor : Qt.rgba(Data.ThemeManager.accentColor.r, Data.ThemeManager.accentColor.g, Data.ThemeManager.accentColor.b, 0.3)
border.width: 1
MouseArea {
id: shuffleButton
anchors.fill: parent
hoverEnabled: true
enabled: currentPlayer && currentPlayer.canControl && currentPlayer.shuffleSupported
onClicked: {
if (currentPlayer && currentPlayer.shuffleSupported) {
currentPlayer.shuffle = !currentPlayer.shuffle
}
}
}
Text {
anchors.centerIn: parent
text: "shuffle"
font.family: "Material Symbols Outlined"
font.pixelSize: 12
color: shuffleButton.enabled ?
(currentPlayer && currentPlayer.shuffle ? Data.ThemeManager.accentColor : Qt.rgba(Data.ThemeManager.fgColor.r, Data.ThemeManager.fgColor.g, Data.ThemeManager.fgColor.b, 0.6)) :
Qt.rgba(Data.ThemeManager.fgColor.r, Data.ThemeManager.fgColor.g, Data.ThemeManager.fgColor.b, 0.3)
}
}
// Repeat button
Rectangle {
width: (parent.width - parent.spacing * 4) * 0.15
height: parent.height
radius: height / 2
color: repeatButton.containsMouse ? Qt.rgba(Data.ThemeManager.accentColor.r, Data.ThemeManager.accentColor.g, Data.ThemeManager.accentColor.b, 0.2) : Qt.darker(Data.ThemeManager.bgColor, 1.1)
border.color: currentPlayer && currentPlayer.loopState !== MprisLoopState.None ? Data.ThemeManager.accentColor : Qt.rgba(Data.ThemeManager.accentColor.r, Data.ThemeManager.accentColor.g, Data.ThemeManager.accentColor.b, 0.3)
border.width: 1
MouseArea {
id: repeatButton
anchors.fill: parent
hoverEnabled: true
enabled: currentPlayer && currentPlayer.canControl && currentPlayer.loopSupported
onClicked: {
if (currentPlayer && currentPlayer.loopSupported) {
if (currentPlayer.loopState === MprisLoopState.None) {
currentPlayer.loopState = MprisLoopState.Track
} else if (currentPlayer.loopState === MprisLoopState.Track) {
currentPlayer.loopState = MprisLoopState.Playlist
} else {
currentPlayer.loopState = MprisLoopState.None
}
}
}
}
Text {
anchors.centerIn: parent
text: currentPlayer && currentPlayer.loopState === MprisLoopState.Track ? "repeat_one" : "repeat"
font.family: "Material Symbols Outlined"
font.pixelSize: 12
color: repeatButton.enabled ?
(currentPlayer && currentPlayer.loopState !== MprisLoopState.None ? Data.ThemeManager.accentColor : Qt.rgba(Data.ThemeManager.fgColor.r, Data.ThemeManager.fgColor.g, Data.ThemeManager.fgColor.b, 0.3)) :
Qt.rgba(Data.ThemeManager.fgColor.r, Data.ThemeManager.fgColor.g, Data.ThemeManager.fgColor.b, 0.3)
}
}
}
}
}
}

View file

@ -0,0 +1,125 @@
import QtQuick
import "../../tabs" as Tabs
// Tab container with sliding animation
Item {
id: tabContainer
// Properties from parent
required property var shell
required property bool isRecording
required property var triggerMouseArea
property int currentTab: 0
// Signals to forward
signal recordingRequested()
signal stopRecordingRequested()
signal systemActionRequested(string action)
signal performanceActionRequested(string action)
// Hover detection combining all tab hovers
property bool isHovered: {
const tabHovers = [
mainDashboard.isHovered,
true, // Calendar tab should stay open when active
true, // Clipboard tab should stay open when active
true, // Notification tab should stay open when active
true, // Wallpaper tab should stay open when active
true, // Music tab should stay open when active
true // Settings tab should stay open when active
]
return tabHovers[currentTab] || false
}
// Track when text inputs have focus for keyboard management
property bool textInputFocused: currentTab === 6 && settingsTab.anyTextInputFocused
clip: true
// Sliding content container
Row {
id: slidingRow
width: parent.width * 7 // 7 tabs wide
height: parent.height
spacing: 0
// Animate horizontal position based on current tab
x: -tabContainer.currentTab * tabContainer.width
Behavior on x {
NumberAnimation {
duration: 300
easing.type: Easing.OutCubic
}
}
// Tab 0: Main Dashboard
Tabs.MainDashboard {
id: mainDashboard
width: tabContainer.width
height: parent.height
shell: tabContainer.shell
isRecording: tabContainer.isRecording
triggerMouseArea: tabContainer.triggerMouseArea
onRecordingRequested: tabContainer.recordingRequested()
onStopRecordingRequested: tabContainer.stopRecordingRequested()
onSystemActionRequested: function(action) { tabContainer.systemActionRequested(action) }
onPerformanceActionRequested: function(action) { tabContainer.performanceActionRequested(action) }
}
// Tab 1: Calendar
Tabs.CalendarTab {
id: calendarTab
width: tabContainer.width
height: parent.height
shell: tabContainer.shell
isActive: tabContainer.currentTab === 1 || Math.abs(tabContainer.currentTab - 1) <= 1
}
// Tab 2: Clipboard
Tabs.ClipboardTab {
id: clipboardTab
width: tabContainer.width
height: parent.height
shell: tabContainer.shell
isActive: tabContainer.currentTab === 2 || Math.abs(tabContainer.currentTab - 2) <= 1
}
// Tab 3: Notifications
Tabs.NotificationTab {
id: notificationTab
width: tabContainer.width
height: parent.height
shell: tabContainer.shell
isActive: tabContainer.currentTab === 3 || Math.abs(tabContainer.currentTab - 3) <= 1
}
// Tab 4: Wallpapers
Tabs.WallpaperTab {
id: wallpaperTab
width: tabContainer.width
height: parent.height
isActive: tabContainer.currentTab === 4 || Math.abs(tabContainer.currentTab - 4) <= 1
}
// Tab 5: Music
Tabs.MusicTab {
id: musicTab
width: tabContainer.width
height: parent.height
shell: tabContainer.shell
isActive: tabContainer.currentTab === 5 || Math.abs(tabContainer.currentTab - 5) <= 1
}
// Tab 6: Settings
Tabs.SettingsTab {
id: settingsTab
width: tabContainer.width
height: parent.height
shell: tabContainer.shell
isActive: tabContainer.currentTab === 6 || Math.abs(tabContainer.currentTab - 6) <= 1
}
}
}

View file

@ -0,0 +1,132 @@
import QtQuick
import "root:/Data" as Data
// Tab navigation sidebar
Item {
id: tabNavigation
property int currentTab: 0
property var tabIcons: []
property bool containsMouse: sidebarMouseArea.containsMouse || tabColumn.containsMouse
MouseArea {
id: sidebarMouseArea
anchors.fill: parent
hoverEnabled: true
propagateComposedEvents: true
}
// Tab button background - matches system controls
Rectangle {
width: 38
height: tabColumn.height + 12
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: parent.top
color: Qt.lighter(Data.ThemeManager.bgColor, 1.15)
radius: 19
border.width: 1
border.color: Qt.lighter(Data.ThemeManager.bgColor, 1.3)
// Subtle inner shadow effect
Rectangle {
anchors.fill: parent
anchors.margins: 1
color: Qt.darker(Data.ThemeManager.bgColor, 1.05)
radius: parent.radius - 1
opacity: 0.3
}
}
// Tab icon buttons
Column {
id: tabColumn
spacing: 6
anchors.top: parent.top
anchors.topMargin: 6
anchors.horizontalCenter: parent.horizontalCenter
property bool containsMouse: {
for (let i = 0; i < tabRepeater.count; i++) {
const tab = tabRepeater.itemAt(i)
if (tab && tab.mouseArea && tab.mouseArea.containsMouse) {
return true
}
}
return false
}
Repeater {
id: tabRepeater
model: 7
delegate: Rectangle {
width: 30
height: 30
radius: 15
// Dynamic background based on state
color: {
if (tabNavigation.currentTab === index) {
return Data.ThemeManager.accentColor
} else if (tabMouseArea.containsMouse) {
return Qt.rgba(Data.ThemeManager.accentColor.r, Data.ThemeManager.accentColor.g, Data.ThemeManager.accentColor.b, 0.15)
} else {
return "transparent"
}
}
// Subtle shadow for active tab
Rectangle {
anchors.fill: parent
radius: parent.radius
color: "transparent"
border.color: Qt.rgba(Data.ThemeManager.accentColor.r, Data.ThemeManager.accentColor.g, Data.ThemeManager.accentColor.b, 0.3)
border.width: tabNavigation.currentTab === index ? 0 : (tabMouseArea.containsMouse ? 1 : 0)
visible: tabNavigation.currentTab !== index
}
property alias mouseArea: tabMouseArea
MouseArea {
id: tabMouseArea
anchors.fill: parent
hoverEnabled: true
onClicked: {
tabNavigation.currentTab = index
}
}
Text {
anchors.centerIn: parent
text: tabNavigation.tabIcons[index] || ""
font.family: "Material Symbols Outlined"
font.pixelSize: 16
color: {
if (tabNavigation.currentTab === index) {
return Data.ThemeManager.bgColor
} else if (tabMouseArea.containsMouse) {
return Data.ThemeManager.accentColor
} else {
return Qt.rgba(Data.ThemeManager.fgColor.r, Data.ThemeManager.fgColor.g, Data.ThemeManager.fgColor.b, 0.7)
}
}
// Smooth color transitions
Behavior on color {
ColorAnimation { duration: 150 }
}
}
// Smooth transitions
Behavior on color {
ColorAnimation { duration: 150 }
}
// Subtle scale effect on hover
scale: tabMouseArea.containsMouse ? 1.05 : 1.0
Behavior on scale {
NumberAnimation { duration: 150; easing.type: Easing.OutCubic }
}
}
}
}
}

View file

@ -0,0 +1,622 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import "root:/Data" as Data
// Appearance settings content
Column {
width: parent.width
spacing: 20
// Theme Setting in Collapsible Section
SettingsCategory {
width: parent.width
title: "Theme Setting"
icon: "palette"
content: Component {
Column {
width: parent.width
spacing: 30 // Increased spacing between major sections
// Dark/Light Mode Switch
Column {
width: parent.width
spacing: 12
Text {
text: "Theme Mode"
color: Data.ThemeManager.fgColor
font.pixelSize: 15
font.bold: true
font.family: "Roboto"
}
Row {
spacing: 16
anchors.horizontalCenter: parent.horizontalCenter
Text {
text: "Light"
color: Data.ThemeManager.fgColor
font.pixelSize: 14
font.family: "Roboto"
anchors.verticalCenter: parent.verticalCenter
}
// Toggle switch - enhanced design
Rectangle {
width: 64
height: 32
radius: 16
color: Data.ThemeManager.currentTheme.type === "dark" ?
Qt.lighter(Data.ThemeManager.accentColor, 0.8) :
Qt.rgba(Data.ThemeManager.fgColor.r, Data.ThemeManager.fgColor.g, Data.ThemeManager.fgColor.b, 0.2)
border.width: 2
border.color: Data.ThemeManager.currentTheme.type === "dark" ?
Data.ThemeManager.accentColor :
Qt.rgba(Data.ThemeManager.fgColor.r, Data.ThemeManager.fgColor.g, Data.ThemeManager.fgColor.b, 0.4)
anchors.verticalCenter: parent.verticalCenter
// Inner track shadow
Rectangle {
anchors.fill: parent
anchors.margins: 2
radius: parent.radius - 2
color: "transparent"
border.width: 1
border.color: Qt.rgba(0, 0, 0, 0.1)
}
// Toggle handle
Rectangle {
id: toggleHandle
width: 26
height: 26
radius: 13
color: Data.ThemeManager.currentTheme.type === "dark" ?
Data.ThemeManager.bgColor : Data.ThemeManager.panelBackground
border.width: 2
border.color: Data.ThemeManager.currentTheme.type === "dark" ?
Data.ThemeManager.accentColor : Data.ThemeManager.fgColor
anchors.verticalCenter: parent.verticalCenter
x: Data.ThemeManager.currentTheme.type === "dark" ? parent.width - width - 3 : 3
// Handle shadow
Rectangle {
anchors.centerIn: parent
anchors.verticalCenterOffset: 1
width: parent.width - 2
height: parent.height - 2
radius: parent.radius - 1
color: Qt.rgba(0, 0, 0, 0.1)
z: -1
}
// Handle highlight
Rectangle {
anchors.centerIn: parent
width: parent.width - 6
height: parent.height - 6
radius: parent.radius - 3
color: Qt.rgba(255, 255, 255, 0.15)
}
Behavior on x {
NumberAnimation {
duration: 250
easing.type: Easing.OutBack
easing.overshoot: 0.3
}
}
Behavior on border.color {
ColorAnimation { duration: 200 }
}
}
// Background color transition
Behavior on color {
ColorAnimation { duration: 200 }
}
Behavior on border.color {
ColorAnimation { duration: 200 }
}
MouseArea {
anchors.fill: parent
hoverEnabled: true
onClicked: {
console.log("Theme switch clicked, current:", Data.ThemeManager.currentThemeId)
var currentFamily = Data.ThemeManager.currentThemeId.replace(/_dark$|_light$/, "")
var newType = Data.ThemeManager.currentTheme.type === "dark" ? "light" : "dark"
var newThemeId = currentFamily + "_" + newType
console.log("Switching to:", newThemeId)
Data.ThemeManager.setTheme(newThemeId)
// Force update the settings if currentTheme isn't being saved properly
if (!Data.Settings.currentTheme) {
Data.Settings.currentTheme = newThemeId
Data.Settings.saveSettings()
}
}
onEntered: {
parent.scale = 1.05
}
onExited: {
parent.scale = 1.0
}
}
Behavior on scale {
NumberAnimation { duration: 150; easing.type: Easing.OutQuad }
}
}
Text {
text: "Dark"
color: Data.ThemeManager.fgColor
font.pixelSize: 14
font.family: "Roboto"
anchors.verticalCenter: parent.verticalCenter
}
}
}
// Separator
Rectangle {
width: parent.width - 40
height: 1
color: Qt.rgba(Data.ThemeManager.fgColor.r, Data.ThemeManager.fgColor.g, Data.ThemeManager.fgColor.b, 0.1)
anchors.horizontalCenter: parent.horizontalCenter
}
// Theme Selection
Column {
width: parent.width
spacing: 12
Text {
text: "Theme Family"
color: Data.ThemeManager.fgColor
font.pixelSize: 15
font.bold: true
font.family: "Roboto"
}
Text {
text: "Choose your preferred theme family"
color: Qt.rgba(Data.ThemeManager.fgColor.r, Data.ThemeManager.fgColor.g, Data.ThemeManager.fgColor.b, 0.7)
font.pixelSize: 13
font.family: "Roboto"
anchors.horizontalCenter: parent.horizontalCenter
}
// Compact 2x2 grid for themes
GridLayout {
columns: 2
columnSpacing: 8
rowSpacing: 8
anchors.horizontalCenter: parent.horizontalCenter
property var themeFamily: {
var currentFamily = Data.ThemeManager.currentThemeId.replace(/_dark$|_light$/, "")
return currentFamily
}
property var themeFamilies: [
{ id: "oxocarbon", name: "Oxocarbon", description: "IBM Carbon" },
{ id: "dracula", name: "Dracula", description: "Vibrant" },
{ id: "gruvbox", name: "Gruvbox", description: "Retro" },
{ id: "catppuccin", name: "Catppuccin", description: "Pastel" },
{ id: "matugen", name: "Matugen", description: "Generated" }
]
Repeater {
model: parent.themeFamilies
delegate: Rectangle {
Layout.preferredWidth: 140
Layout.preferredHeight: 50
radius: 10
color: parent.themeFamily === modelData.id ?
Data.ThemeManager.accentColor : Qt.lighter(Data.ThemeManager.bgColor, 1.15)
border.width: parent.themeFamily === modelData.id ? 2 : 1
border.color: parent.themeFamily === modelData.id ?
Data.ThemeManager.accentColor : Qt.rgba(Data.ThemeManager.fgColor.r, Data.ThemeManager.fgColor.g, Data.ThemeManager.fgColor.b, 0.3)
Row {
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
anchors.leftMargin: 10
spacing: 6
// Compact theme preview colors
Row {
spacing: 1
property var previewTheme: Data.ThemeManager.themes[modelData.id + "_" + Data.ThemeManager.currentTheme.type] || Data.ThemeManager.themes[modelData.id + "_dark"]
Rectangle { width: 4; height: 14; radius: 1; color: parent.previewTheme.base00 }
Rectangle { width: 4; height: 14; radius: 1; color: parent.previewTheme.base0E }
Rectangle { width: 4; height: 14; radius: 1; color: parent.previewTheme.base0D }
Rectangle { width: 4; height: 14; radius: 1; color: parent.previewTheme.base0B }
}
Column {
spacing: 1
anchors.verticalCenter: parent.verticalCenter
Text {
text: modelData.name
color: parent.parent.parent.parent.themeFamily === modelData.id ?
Data.ThemeManager.bgColor : Data.ThemeManager.fgColor
font.pixelSize: 12
font.bold: parent.parent.parent.parent.themeFamily === modelData.id
font.family: "Roboto"
}
Text {
text: modelData.description
color: parent.parent.parent.parent.themeFamily === modelData.id ?
Qt.rgba(Data.ThemeManager.bgColor.r, Data.ThemeManager.bgColor.g, Data.ThemeManager.bgColor.b, 0.8) :
Qt.rgba(Data.ThemeManager.fgColor.r, Data.ThemeManager.fgColor.g, Data.ThemeManager.fgColor.b, 0.6)
font.pixelSize: 9
font.family: "Roboto"
}
}
}
MouseArea {
anchors.fill: parent
hoverEnabled: true
onClicked: {
var themeType = Data.ThemeManager.currentTheme.type
var newThemeId = modelData.id + "_" + themeType
console.log("Theme card clicked:", newThemeId)
Data.ThemeManager.setTheme(newThemeId)
}
onEntered: {
parent.scale = 1.02
}
onExited: {
parent.scale = 1.0
}
}
Behavior on scale {
NumberAnimation { duration: 150; easing.type: Easing.OutQuad }
}
}
}
}
}
// Separator
Rectangle {
width: parent.width - 40
height: 1
color: Qt.rgba(Data.ThemeManager.fgColor.r, Data.ThemeManager.fgColor.g, Data.ThemeManager.fgColor.b, 0.1)
anchors.horizontalCenter: parent.horizontalCenter
}
// Accent Colors
Column {
width: parent.width
spacing: 12
Text {
text: "Accent Colors"
color: Data.ThemeManager.fgColor
font.pixelSize: 15
font.bold: true
font.family: "Roboto"
}
Text {
text: "Choose your preferred accent color for " + Data.ThemeManager.currentTheme.name
color: Qt.rgba(Data.ThemeManager.fgColor.r, Data.ThemeManager.fgColor.g, Data.ThemeManager.fgColor.b, 0.7)
font.pixelSize: 13
font.family: "Roboto"
wrapMode: Text.Wrap
width: parent.width
anchors.horizontalCenter: parent.horizontalCenter
}
// Compact flow layout for accent colors
Flow {
anchors.horizontalCenter: parent.horizontalCenter
width: parent.width - 20 // Margins to prevent clipping
spacing: 8
property var accentColors: {
var currentFamily = Data.ThemeManager.currentThemeId.replace(/_dark$|_light$/, "")
var themeColors = []
// Theme-specific accent colors - reduced to 5 per theme for compactness
if (currentFamily === "dracula") {
themeColors.push(
{ name: "Magenta", dark: "#ff79c6", light: "#e91e63" },
{ name: "Purple", dark: "#bd93f9", light: "#6c7ce0" },
{ name: "Cyan", dark: "#8be9fd", light: "#17a2b8" },
{ name: "Green", dark: "#50fa7b", light: "#27ae60" },
{ name: "Orange", dark: "#ffb86c", light: "#f39c12" }
)
} else if (currentFamily === "gruvbox") {
themeColors.push(
{ name: "Orange", dark: "#fe8019", light: "#d65d0e" },
{ name: "Red", dark: "#fb4934", light: "#cc241d" },
{ name: "Yellow", dark: "#fabd2f", light: "#d79921" },
{ name: "Green", dark: "#b8bb26", light: "#98971a" },
{ name: "Purple", dark: "#d3869b", light: "#b16286" }
)
} else if (currentFamily === "catppuccin") {
themeColors.push(
{ name: "Mauve", dark: "#cba6f7", light: "#8839ef" },
{ name: "Blue", dark: "#89b4fa", light: "#1e66f5" },
{ name: "Teal", dark: "#94e2d5", light: "#179299" },
{ name: "Green", dark: "#a6e3a1", light: "#40a02b" },
{ name: "Peach", dark: "#fab387", light: "#fe640b" }
)
} else if (currentFamily === "matugen") {
// Use dynamic matugen colors if available
if (Data.ThemeManager.matugen && Data.ThemeManager.matugen.isMatugenActive()) {
themeColors.push(
{ name: "Primary", dark: Data.ThemeManager.matugen.getMatugenColor("primary") || "#adc6ff", light: Data.ThemeManager.matugen.getMatugenColor("primary") || "#0f62fe" },
{ name: "Secondary", dark: Data.ThemeManager.matugen.getMatugenColor("secondary") || "#bfc6dc", light: Data.ThemeManager.matugen.getMatugenColor("secondary") || "#6272a4" },
{ name: "Tertiary", dark: Data.ThemeManager.matugen.getMatugenColor("tertiary") || "#debcdf", light: Data.ThemeManager.matugen.getMatugenColor("tertiary") || "#b16286" },
{ name: "Surface", dark: Data.ThemeManager.matugen.getMatugenColor("surface_tint") || "#adc6ff", light: Data.ThemeManager.matugen.getMatugenColor("surface_tint") || "#0f62fe" },
{ name: "Error", dark: Data.ThemeManager.matugen.getMatugenColor("error") || "#ffb4ab", light: Data.ThemeManager.matugen.getMatugenColor("error") || "#ba1a1a" }
)
} else {
// Fallback matugen colors
themeColors.push(
{ name: "Primary", dark: "#adc6ff", light: "#0f62fe" },
{ name: "Secondary", dark: "#bfc6dc", light: "#6272a4" },
{ name: "Tertiary", dark: "#debcdf", light: "#b16286" },
{ name: "Surface", dark: "#adc6ff", light: "#0f62fe" },
{ name: "Error", dark: "#ffb4ab", light: "#ba1a1a" }
)
}
} else { // oxocarbon and fallback
themeColors.push(
{ name: "Purple", dark: "#be95ff", light: "#8a3ffc" },
{ name: "Blue", dark: "#78a9ff", light: "#0f62fe" },
{ name: "Cyan", dark: "#3ddbd9", light: "#007d79" },
{ name: "Green", dark: "#42be65", light: "#198038" },
{ name: "Pink", dark: "#ff7eb6", light: "#d12771" }
)
}
return themeColors
}
Repeater {
model: parent.accentColors
delegate: Rectangle {
width: 60
height: 50
radius: 10
color: Qt.lighter(Data.ThemeManager.bgColor, 1.15)
border.width: Data.ThemeManager.accentColor.toString() === (Data.ThemeManager.currentTheme.type === "dark" ? modelData.dark : modelData.light) ? 3 : 1
border.color: Data.ThemeManager.accentColor.toString() === (Data.ThemeManager.currentTheme.type === "dark" ? modelData.dark : modelData.light) ?
Data.ThemeManager.accentColor : Qt.rgba(Data.ThemeManager.fgColor.r, Data.ThemeManager.fgColor.g, Data.ThemeManager.fgColor.b, 0.3)
Column {
anchors.centerIn: parent
spacing: 4
Rectangle {
width: 20
height: 20
radius: 10
color: Data.ThemeManager.currentTheme.type === "dark" ? modelData.dark : modelData.light
anchors.horizontalCenter: parent.horizontalCenter
}
Text {
text: modelData.name
color: Data.ThemeManager.fgColor
font.pixelSize: 9
font.family: "Roboto"
font.bold: true
anchors.horizontalCenter: parent.horizontalCenter
}
}
MouseArea {
anchors.fill: parent
hoverEnabled: true
onClicked: {
// Set custom accent
Data.Settings.useCustomAccent = true
Data.ThemeManager.setCustomAccent(modelData.dark, modelData.light)
}
onEntered: {
parent.scale = 1.05
}
onExited: {
parent.scale = 1.0
}
}
Behavior on scale {
NumberAnimation { duration: 150; easing.type: Easing.OutQuad }
}
}
}
}
}
}
}
}
// Animation Settings in Collapsible Section
SettingsCategory {
width: parent.width
title: "Animation Settings"
icon: "animation"
content: Component {
Column {
width: parent.width
spacing: 20
Text {
text: "Configure workspace change animations"
color: Qt.rgba(Data.ThemeManager.fgColor.r, Data.ThemeManager.fgColor.g, Data.ThemeManager.fgColor.b, 0.7)
font.pixelSize: 13
font.family: "Roboto"
anchors.horizontalCenter: parent.horizontalCenter
}
// Workspace Burst Toggle
Row {
width: parent.width
height: 40
Column {
anchors.verticalCenter: parent.verticalCenter
width: parent.width - 80
Text {
text: "Workspace Burst Effect"
color: Data.ThemeManager.fgColor
font.pixelSize: 14
font.bold: true
font.family: "Roboto"
}
Text {
text: "Expanding rings when switching workspaces"
color: Qt.rgba(Data.ThemeManager.fgColor.r, Data.ThemeManager.fgColor.g, Data.ThemeManager.fgColor.b, 0.6)
font.pixelSize: 11
font.family: "Roboto"
}
}
// Toggle switch for burst
Rectangle {
width: 50
height: 25
radius: 12.5
anchors.verticalCenter: parent.verticalCenter
anchors.right: parent.right
color: Data.Settings.workspaceBurstEnabled ?
Qt.lighter(Data.ThemeManager.accentColor, 0.8) :
Qt.rgba(Data.ThemeManager.fgColor.r, Data.ThemeManager.fgColor.g, Data.ThemeManager.fgColor.b, 0.2)
border.width: 1
border.color: Data.Settings.workspaceBurstEnabled ?
Data.ThemeManager.accentColor :
Qt.rgba(Data.ThemeManager.fgColor.r, Data.ThemeManager.fgColor.g, Data.ThemeManager.fgColor.b, 0.4)
Rectangle {
width: 20
height: 20
radius: 10
color: Data.ThemeManager.bgColor
border.width: 1.5
border.color: Data.Settings.workspaceBurstEnabled ? Data.ThemeManager.accentColor : Data.ThemeManager.fgColor
anchors.verticalCenter: parent.verticalCenter
x: Data.Settings.workspaceBurstEnabled ? parent.width - width - 2.5 : 2.5
Behavior on x {
NumberAnimation {
duration: 200
easing.type: Easing.OutQuad
}
}
}
Behavior on color { ColorAnimation { duration: 200 } }
Behavior on border.color { ColorAnimation { duration: 200 } }
MouseArea {
anchors.fill: parent
onClicked: {
Data.Settings.workspaceBurstEnabled = !Data.Settings.workspaceBurstEnabled
}
}
}
}
// Workspace Glow Toggle
Row {
width: parent.width
height: 40
Column {
anchors.verticalCenter: parent.verticalCenter
width: parent.width - 80
Text {
text: "Workspace Shadow Glow"
color: Data.ThemeManager.fgColor
font.pixelSize: 14
font.bold: true
font.family: "Roboto"
}
Text {
text: "Accent color glow in workspace shadow"
color: Qt.rgba(Data.ThemeManager.fgColor.r, Data.ThemeManager.fgColor.g, Data.ThemeManager.fgColor.b, 0.6)
font.pixelSize: 11
font.family: "Roboto"
}
}
// Toggle switch for glow
Rectangle {
width: 50
height: 25
radius: 12.5
anchors.verticalCenter: parent.verticalCenter
anchors.right: parent.right
color: Data.Settings.workspaceGlowEnabled ?
Qt.lighter(Data.ThemeManager.accentColor, 0.8) :
Qt.rgba(Data.ThemeManager.fgColor.r, Data.ThemeManager.fgColor.g, Data.ThemeManager.fgColor.b, 0.2)
border.width: 1
border.color: Data.Settings.workspaceGlowEnabled ?
Data.ThemeManager.accentColor :
Qt.rgba(Data.ThemeManager.fgColor.r, Data.ThemeManager.fgColor.g, Data.ThemeManager.fgColor.b, 0.4)
Rectangle {
width: 20
height: 20
radius: 10
color: Data.ThemeManager.bgColor
border.width: 1.5
border.color: Data.Settings.workspaceGlowEnabled ? Data.ThemeManager.accentColor : Data.ThemeManager.fgColor
anchors.verticalCenter: parent.verticalCenter
x: Data.Settings.workspaceGlowEnabled ? parent.width - width - 2.5 : 2.5
Behavior on x {
NumberAnimation {
duration: 200
easing.type: Easing.OutQuad
}
}
}
Behavior on color { ColorAnimation { duration: 200 } }
Behavior on border.color { ColorAnimation { duration: 200 } }
MouseArea {
anchors.fill: parent
onClicked: {
Data.Settings.workspaceGlowEnabled = !Data.Settings.workspaceGlowEnabled
}
}
}
}
}
}
}
}

View file

@ -0,0 +1,121 @@
import QtQuick
import QtQuick.Controls
import "root:/Data" as Data
// Music Player settings content
Column {
width: parent.width
spacing: 20
// Auto-switch to active player
Column {
width: parent.width
spacing: 12
Text {
text: "Auto-switch to Active Player"
color: Data.ThemeManager.fgColor
font.pixelSize: 16
font.bold: true
font.family: "Roboto"
}
Text {
text: "Automatically switch to the player that starts playing music"
color: Qt.rgba(Data.ThemeManager.fgColor.r, Data.ThemeManager.fgColor.g, Data.ThemeManager.fgColor.b, 0.7)
font.pixelSize: 13
font.family: "Roboto"
wrapMode: Text.Wrap
width: parent.width
}
Rectangle {
width: 200
height: 35
radius: 18
color: Data.Settings.autoSwitchPlayer ? Data.ThemeManager.accentColor : Qt.lighter(Data.ThemeManager.bgColor, 1.15)
border.width: 1
border.color: Data.ThemeManager.accentColor
Behavior on color {
ColorAnimation { duration: 200 }
}
Text {
anchors.centerIn: parent
text: Data.Settings.autoSwitchPlayer ? "Enabled" : "Disabled"
color: Data.Settings.autoSwitchPlayer ? Data.ThemeManager.bgColor : Data.ThemeManager.fgColor
font.pixelSize: 14
font.bold: true
font.family: "Roboto"
Behavior on color {
ColorAnimation { duration: 200 }
}
}
MouseArea {
anchors.fill: parent
onClicked: {
Data.Settings.autoSwitchPlayer = !Data.Settings.autoSwitchPlayer
}
}
}
}
// Always show player dropdown
Column {
width: parent.width
spacing: 12
Text {
text: "Always Show Player Dropdown"
color: Data.ThemeManager.fgColor
font.pixelSize: 16
font.bold: true
font.family: "Roboto"
}
Text {
text: "Show the player selection dropdown even with only one player"
color: Qt.rgba(Data.ThemeManager.fgColor.r, Data.ThemeManager.fgColor.g, Data.ThemeManager.fgColor.b, 0.7)
font.pixelSize: 13
font.family: "Roboto"
wrapMode: Text.Wrap
width: parent.width
}
Rectangle {
width: 200
height: 35
radius: 18
color: Data.Settings.alwaysShowPlayerDropdown ? Data.ThemeManager.accentColor : Qt.lighter(Data.ThemeManager.bgColor, 1.15)
border.width: 1
border.color: Data.ThemeManager.accentColor
Behavior on color {
ColorAnimation { duration: 200 }
}
Text {
anchors.centerIn: parent
text: Data.Settings.alwaysShowPlayerDropdown ? "Enabled" : "Disabled"
color: Data.Settings.alwaysShowPlayerDropdown ? Data.ThemeManager.bgColor : Data.ThemeManager.fgColor
font.pixelSize: 14
font.bold: true
font.family: "Roboto"
Behavior on color {
ColorAnimation { duration: 200 }
}
}
MouseArea {
anchors.fill: parent
onClicked: {
Data.Settings.alwaysShowPlayerDropdown = !Data.Settings.alwaysShowPlayerDropdown
}
}
}
}
}

View file

@ -0,0 +1,517 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import "root:/Data" as Data
// Night Light settings content
Item {
id: nightLightSettings
width: parent.width
height: contentColumn.height
Column {
id: contentColumn
width: parent.width
spacing: 20
// Night Light Enable Toggle
Row {
width: parent.width
spacing: 16
Column {
width: parent.width - nightLightToggle.width - 16
spacing: 4
Text {
text: "Enable Night Light"
color: Data.ThemeManager.fgColor
font.pixelSize: 16
font.bold: true
font.family: "Roboto"
}
Text {
text: "Reduces blue light to help protect your eyes and improve sleep"
color: Qt.rgba(Data.ThemeManager.fgColor.r, Data.ThemeManager.fgColor.g, Data.ThemeManager.fgColor.b, 0.7)
font.pixelSize: 13
font.family: "Roboto"
wrapMode: Text.Wrap
}
}
Rectangle {
id: nightLightToggle
width: 50
height: 28
radius: 14
color: Data.Settings.nightLightEnabled ? Data.ThemeManager.accentColor : Qt.rgba(Data.ThemeManager.fgColor.r, Data.ThemeManager.fgColor.g, Data.ThemeManager.fgColor.b, 0.3)
Behavior on color {
ColorAnimation { duration: 200 }
}
Rectangle {
width: 20
height: 20
radius: 10
color: Data.ThemeManager.bgColor
x: Data.Settings.nightLightEnabled ? parent.width - width - 4 : 4
anchors.verticalCenter: parent.verticalCenter
Behavior on x {
NumberAnimation { duration: 200; easing.type: Easing.OutCubic }
}
}
MouseArea {
anchors.fill: parent
hoverEnabled: true
onClicked: {
Data.Settings.nightLightEnabled = !Data.Settings.nightLightEnabled
}
onEntered: {
parent.scale = 1.05
}
onExited: {
parent.scale = 1.0
}
}
Behavior on scale {
NumberAnimation { duration: 150; easing.type: Easing.OutQuad }
}
}
}
// Warmth Level Slider
Column {
width: parent.width
spacing: 12
Text {
text: "Warmth Level"
color: Data.ThemeManager.fgColor
font.pixelSize: 16
font.bold: true
font.family: "Roboto"
}
Text {
text: "Adjust how warm the screen filter appears"
color: Qt.rgba(Data.ThemeManager.fgColor.r, Data.ThemeManager.fgColor.g, Data.ThemeManager.fgColor.b, 0.7)
font.pixelSize: 13
font.family: "Roboto"
wrapMode: Text.Wrap
width: parent.width
}
Row {
width: parent.width
spacing: 12
Text {
anchors.verticalCenter: parent.verticalCenter
text: "Cool"
color: Qt.rgba(Data.ThemeManager.fgColor.r, Data.ThemeManager.fgColor.g, Data.ThemeManager.fgColor.b, 0.6)
font.pixelSize: 12
font.family: "Roboto"
}
Slider {
id: warmthSlider
width: parent.width - 120
height: 30
from: 0.1
to: 1.0
value: Data.Settings.nightLightWarmth || 0.4
stepSize: 0.1
onValueChanged: {
Data.Settings.nightLightWarmth = value
}
background: Rectangle {
anchors.verticalCenter: parent.verticalCenter
width: parent.width
height: 6
radius: 3
color: Qt.rgba(Data.ThemeManager.fgColor.r, Data.ThemeManager.fgColor.g, Data.ThemeManager.fgColor.b, 0.2)
Rectangle {
width: warmthSlider.visualPosition * parent.width
height: parent.height
radius: parent.radius
color: Qt.rgba(1.0, 0.8 - warmthSlider.value * 0.3, 0.4, 1.0)
}
}
handle: Rectangle {
x: warmthSlider.leftPadding + warmthSlider.visualPosition * (warmthSlider.availableWidth - width)
y: warmthSlider.topPadding + warmthSlider.availableHeight / 2 - height / 2
width: 20
height: 20
radius: 10
color: Data.ThemeManager.accentColor
border.color: Qt.lighter(Data.ThemeManager.accentColor, 1.2)
border.width: 2
}
}
Text {
anchors.verticalCenter: parent.verticalCenter
text: "Warm"
color: Qt.rgba(Data.ThemeManager.fgColor.r, Data.ThemeManager.fgColor.g, Data.ThemeManager.fgColor.b, 0.6)
font.pixelSize: 12
font.family: "Roboto"
}
}
}
// Auto-enable Toggle
Row {
width: parent.width
spacing: 16
Column {
width: parent.width - autoToggle.width - 16
spacing: 4
Text {
text: "Auto-enable Schedule"
color: Data.ThemeManager.fgColor
font.pixelSize: 16
font.bold: true
font.family: "Roboto"
}
Text {
text: "Automatically turn on night light at sunset/bedtime"
color: Qt.rgba(Data.ThemeManager.fgColor.r, Data.ThemeManager.fgColor.g, Data.ThemeManager.fgColor.b, 0.7)
font.pixelSize: 13
font.family: "Roboto"
wrapMode: Text.Wrap
}
}
Rectangle {
id: autoToggle
width: 50
height: 28
radius: 14
color: Data.Settings.nightLightAuto ? Data.ThemeManager.accentColor : Qt.rgba(Data.ThemeManager.fgColor.r, Data.ThemeManager.fgColor.g, Data.ThemeManager.fgColor.b, 0.3)
Behavior on color {
ColorAnimation { duration: 200 }
}
Rectangle {
width: 20
height: 20
radius: 10
color: Data.ThemeManager.bgColor
x: Data.Settings.nightLightAuto ? parent.width - width - 4 : 4
anchors.verticalCenter: parent.verticalCenter
Behavior on x {
NumberAnimation { duration: 200; easing.type: Easing.OutCubic }
}
}
MouseArea {
anchors.fill: parent
hoverEnabled: true
onClicked: {
Data.Settings.nightLightAuto = !Data.Settings.nightLightAuto
}
onEntered: {
parent.scale = 1.05
}
onExited: {
parent.scale = 1.0
}
}
Behavior on scale {
NumberAnimation { duration: 150; easing.type: Easing.OutQuad }
}
}
}
// Schedule Time Controls - visible when auto-enable is on
Column {
width: parent.width
spacing: 16
visible: Data.Settings.nightLightAuto
opacity: Data.Settings.nightLightAuto ? 1.0 : 0.0
Behavior on opacity {
NumberAnimation { duration: 200 }
}
Text {
text: "Schedule Times"
color: Data.ThemeManager.fgColor
font.pixelSize: 16
font.bold: true
font.family: "Roboto"
}
// Start and End Time Row
Row {
width: parent.width
spacing: 20
// Start Time
Column {
id: startTimeColumn
width: (parent.width - parent.spacing) / 2
spacing: 8
Text {
text: "Start Time"
color: Data.ThemeManager.fgColor
font.pixelSize: 14
font.bold: true
font.family: "Roboto"
}
Text {
text: "Night light turns on"
color: Qt.rgba(Data.ThemeManager.fgColor.r, Data.ThemeManager.fgColor.g, Data.ThemeManager.fgColor.b, 0.7)
font.pixelSize: 12
font.family: "Roboto"
}
Rectangle {
id: startTimeButton
width: parent.width
height: 40
radius: 8
color: Qt.lighter(Data.ThemeManager.bgColor, 1.15)
border.width: 1
border.color: Qt.rgba(Data.ThemeManager.fgColor.r, Data.ThemeManager.fgColor.g, Data.ThemeManager.fgColor.b, 0.3)
Row {
anchors.centerIn: parent
spacing: 8
Text {
text: (Data.Settings.nightLightStartHour || 20).toString().padStart(2, '0') + ":00"
color: Data.ThemeManager.fgColor
font.pixelSize: 16
font.bold: true
font.family: "Roboto"
anchors.verticalCenter: parent.verticalCenter
}
}
MouseArea {
anchors.fill: parent
onClicked: {
startTimePopup.open()
}
}
}
// Start Time Popup
Popup {
id: startTimePopup
width: startTimeButton.width
height: 170
modal: true
focus: true
closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside
y: startTimeButton.y - height - 10
x: startTimeButton.x
dim: false
background: Rectangle {
color: Data.ThemeManager.bgColor
radius: 12
border.width: 2
border.color: Data.ThemeManager.accentColor
}
Column {
anchors.centerIn: parent
spacing: 12
Text {
text: "Select Start Hour"
color: Data.ThemeManager.fgColor
font.pixelSize: 14
font.bold: true
font.family: "Roboto"
anchors.horizontalCenter: parent.horizontalCenter
}
GridLayout {
columns: 6
columnSpacing: 6
rowSpacing: 6
anchors.horizontalCenter: parent.horizontalCenter
Repeater {
model: 24
delegate: Rectangle {
width: 24
height: 24
radius: 4
color: (Data.Settings.nightLightStartHour || 20) === modelData ?
Data.ThemeManager.accentColor : Qt.lighter(Data.ThemeManager.bgColor, 1.15)
border.width: 1
border.color: Qt.rgba(Data.ThemeManager.fgColor.r, Data.ThemeManager.fgColor.g, Data.ThemeManager.fgColor.b, 0.3)
Text {
anchors.centerIn: parent
text: modelData.toString().padStart(2, '0')
color: (Data.Settings.nightLightStartHour || 20) === modelData ?
Data.ThemeManager.bgColor : Data.ThemeManager.fgColor
font.pixelSize: 10
font.bold: true
font.family: "Roboto"
}
MouseArea {
anchors.fill: parent
onClicked: {
Data.Settings.nightLightStartHour = modelData
startTimePopup.close()
}
}
}
}
}
}
}
}
// End Time
Column {
id: endTimeColumn
width: (parent.width - parent.spacing) / 2
spacing: 8
Text {
text: "End Time"
color: Data.ThemeManager.fgColor
font.pixelSize: 14
font.bold: true
font.family: "Roboto"
}
Text {
text: "Night light turns off"
color: Qt.rgba(Data.ThemeManager.fgColor.r, Data.ThemeManager.fgColor.g, Data.ThemeManager.fgColor.b, 0.7)
font.pixelSize: 12
font.family: "Roboto"
}
Rectangle {
id: endTimeButton
width: parent.width
height: 40
radius: 8
color: Qt.lighter(Data.ThemeManager.bgColor, 1.15)
border.width: 1
border.color: Qt.rgba(Data.ThemeManager.fgColor.r, Data.ThemeManager.fgColor.g, Data.ThemeManager.fgColor.b, 0.3)
Row {
anchors.centerIn: parent
spacing: 8
Text {
text: (Data.Settings.nightLightEndHour || 6).toString().padStart(2, '0') + ":00"
color: Data.ThemeManager.fgColor
font.pixelSize: 16
font.bold: true
font.family: "Roboto"
anchors.verticalCenter: parent.verticalCenter
}
}
MouseArea {
anchors.fill: parent
onClicked: {
endTimePopup.open()
}
}
}
// End Time Popup
Popup {
id: endTimePopup
width: endTimeButton.width
height: 170
modal: true
focus: true
closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside
y: endTimeButton.y - height - 10
x: endTimeButton.x
dim: false
background: Rectangle {
color: Data.ThemeManager.bgColor
radius: 12
border.width: 2
border.color: Data.ThemeManager.accentColor
}
Column {
anchors.centerIn: parent
spacing: 12
Text {
text: "Select End Hour"
color: Data.ThemeManager.fgColor
font.pixelSize: 14
font.bold: true
font.family: "Roboto"
anchors.horizontalCenter: parent.horizontalCenter
}
GridLayout {
columns: 6
columnSpacing: 6
rowSpacing: 6
anchors.horizontalCenter: parent.horizontalCenter
Repeater {
model: 24
delegate: Rectangle {
width: 24
height: 24
radius: 4
color: (Data.Settings.nightLightEndHour || 6) === modelData ?
Data.ThemeManager.accentColor : Qt.lighter(Data.ThemeManager.bgColor, 1.15)
border.width: 1
border.color: Qt.rgba(Data.ThemeManager.fgColor.r, Data.ThemeManager.fgColor.g, Data.ThemeManager.fgColor.b, 0.3)
Text {
anchors.centerIn: parent
text: modelData.toString().padStart(2, '0')
color: (Data.Settings.nightLightEndHour || 6) === modelData ?
Data.ThemeManager.bgColor : Data.ThemeManager.fgColor
font.pixelSize: 10
font.bold: true
font.family: "Roboto"
}
MouseArea {
anchors.fill: parent
onClicked: {
Data.Settings.nightLightEndHour = modelData
endTimePopup.close()
}
}
}
}
}
}
}
}
}
}
}
}

View file

@ -0,0 +1,531 @@
import QtQuick
import QtQuick.Controls
import "root:/Data" as Data
// Notification settings content
Item {
id: notificationSettings
width: parent.width
height: contentColumn.height
// Expose the text input focus for parent keyboard management
property bool anyTextInputFocused: appNameInput.activeFocus
Column {
id: contentColumn
width: parent.width
spacing: 20
// Display Time Setting
Column {
width: parent.width
spacing: 12
Text {
text: "Display Time"
color: Data.ThemeManager.fgColor
font.pixelSize: 16
font.bold: true
font.family: "Roboto"
}
Text {
text: "How long notifications stay visible on screen"
color: Qt.rgba(Data.ThemeManager.fgColor.r, Data.ThemeManager.fgColor.g, Data.ThemeManager.fgColor.b, 0.7)
font.pixelSize: 13
font.family: "Roboto"
wrapMode: Text.Wrap
width: parent.width
}
Row {
spacing: 16
width: parent.width
Slider {
id: displayTimeSlider
width: parent.width - timeLabel.width - 16
height: 30
from: 2000
to: 15000
stepSize: 1000
value: Data.Settings.displayTime
onValueChanged: {
Data.Settings.displayTime = value
}
background: Rectangle {
width: displayTimeSlider.availableWidth
height: 6
radius: 3
color: Qt.rgba(Data.ThemeManager.fgColor.r, Data.ThemeManager.fgColor.g, Data.ThemeManager.fgColor.b, 0.3)
anchors.verticalCenter: parent.verticalCenter
Rectangle {
width: displayTimeSlider.visualPosition * parent.width
height: parent.height
radius: parent.radius
color: Data.ThemeManager.accentColor
}
}
handle: Rectangle {
x: displayTimeSlider.leftPadding + displayTimeSlider.visualPosition * (displayTimeSlider.availableWidth - width)
y: displayTimeSlider.topPadding + displayTimeSlider.availableHeight / 2 - height / 2
width: 20
height: 20
radius: 10
color: Data.ThemeManager.accentColor
border.color: Qt.lighter(Data.ThemeManager.accentColor, 1.2)
border.width: 2
scale: displayTimeSlider.pressed ? 1.2 : 1.0
Behavior on scale {
NumberAnimation { duration: 150 }
}
}
}
Text {
id: timeLabel
text: (displayTimeSlider.value / 1000).toFixed(1) + "s"
color: Data.ThemeManager.fgColor
font.pixelSize: 14
font.family: "Roboto"
anchors.verticalCenter: parent.verticalCenter
width: 40
}
}
}
// Max History Items
Column {
width: parent.width
spacing: 12
Text {
text: "History Limit"
color: Data.ThemeManager.fgColor
font.pixelSize: 16
font.bold: true
font.family: "Roboto"
}
Text {
text: "Maximum number of notifications to keep in history"
color: Qt.rgba(Data.ThemeManager.fgColor.r, Data.ThemeManager.fgColor.g, Data.ThemeManager.fgColor.b, 0.7)
font.pixelSize: 13
font.family: "Roboto"
wrapMode: Text.Wrap
width: parent.width
}
Row {
spacing: 16
width: parent.width
Slider {
id: historySlider
width: parent.width - historyLabel.width - 16
height: 30
from: 10
to: 100
stepSize: 5
value: Data.Settings.historyLimit
onValueChanged: {
Data.Settings.historyLimit = value
}
background: Rectangle {
width: historySlider.availableWidth
height: 6
radius: 3
color: Qt.rgba(Data.ThemeManager.fgColor.r, Data.ThemeManager.fgColor.g, Data.ThemeManager.fgColor.b, 0.3)
anchors.verticalCenter: parent.verticalCenter
Rectangle {
width: historySlider.visualPosition * parent.width
height: parent.height
radius: parent.radius
color: Data.ThemeManager.accentColor
}
}
handle: Rectangle {
x: historySlider.leftPadding + historySlider.visualPosition * (historySlider.availableWidth - width)
y: historySlider.topPadding + historySlider.availableHeight / 2 - height / 2
width: 20
height: 20
radius: 10
color: Data.ThemeManager.accentColor
border.color: Qt.lighter(Data.ThemeManager.accentColor, 1.2)
border.width: 2
scale: historySlider.pressed ? 1.2 : 1.0
Behavior on scale {
NumberAnimation { duration: 150 }
}
}
}
Text {
id: historyLabel
text: historySlider.value + " items"
color: Data.ThemeManager.fgColor
font.pixelSize: 14
font.family: "Roboto"
anchors.verticalCenter: parent.verticalCenter
width: 60
}
}
}
// Ignored Apps Setting
Column {
width: parent.width
spacing: 12
Text {
text: "Ignored Applications"
color: Data.ThemeManager.fgColor
font.pixelSize: 16
font.bold: true
font.family: "Roboto"
}
Text {
text: "Applications that won't show notifications"
color: Qt.rgba(Data.ThemeManager.fgColor.r, Data.ThemeManager.fgColor.g, Data.ThemeManager.fgColor.b, 0.7)
font.pixelSize: 13
font.family: "Roboto"
wrapMode: Text.Wrap
width: parent.width
}
// Current ignored apps list
Rectangle {
width: parent.width
height: Math.max(100, ignoredAppsFlow.height + 16)
radius: 12
color: Qt.lighter(Data.ThemeManager.bgColor, 1.15)
border.width: 1
border.color: Qt.rgba(Data.ThemeManager.fgColor.r, Data.ThemeManager.fgColor.g, Data.ThemeManager.fgColor.b, 0.3)
Flow {
id: ignoredAppsFlow
anchors.fill: parent
anchors.margins: 8
spacing: 6
Repeater {
model: Data.Settings.ignoredApps
delegate: Rectangle {
width: appNameText.width + removeButton.width + 16
height: 28
radius: 14
color: Qt.rgba(Data.ThemeManager.accentColor.r, Data.ThemeManager.accentColor.g, Data.ThemeManager.accentColor.b, 0.15)
border.width: 1
border.color: Qt.rgba(Data.ThemeManager.accentColor.r, Data.ThemeManager.accentColor.g, Data.ThemeManager.accentColor.b, 0.3)
Row {
anchors.centerIn: parent
spacing: 4
Text {
id: appNameText
anchors.verticalCenter: parent.verticalCenter
text: modelData
color: Data.ThemeManager.fgColor
font.pixelSize: 12
font.family: "Roboto"
}
Rectangle {
id: removeButton
width: 18
height: 18
radius: 9
color: removeMouseArea.containsMouse ?
Qt.rgba(1, 0.3, 0.3, 0.8) : Qt.rgba(Data.ThemeManager.fgColor.r, Data.ThemeManager.fgColor.g, Data.ThemeManager.fgColor.b, 0.5)
Behavior on color {
ColorAnimation { duration: 150 }
}
Text {
anchors.centerIn: parent
text: "×"
color: "white"
font.pixelSize: 12
font.bold: true
}
MouseArea {
id: removeMouseArea
anchors.fill: parent
hoverEnabled: true
onClicked: {
Data.Settings.removeIgnoredApp(modelData)
}
}
}
}
}
}
// Add new app button
Rectangle {
width: addAppText.width + 36
height: 28
radius: 14
color: addAppMouseArea.containsMouse ?
Qt.rgba(Data.ThemeManager.accentColor.r, Data.ThemeManager.accentColor.g, Data.ThemeManager.accentColor.b, 0.2) :
Qt.lighter(Data.ThemeManager.bgColor, 1.2)
border.width: 2
border.color: Data.ThemeManager.accentColor
Behavior on color {
ColorAnimation { duration: 150 }
}
Row {
anchors.centerIn: parent
spacing: 6
Text {
anchors.verticalCenter: parent.verticalCenter
text: "add"
font.family: "Material Symbols Outlined"
font.pixelSize: 14
color: Data.ThemeManager.accentColor
}
Text {
id: addAppText
anchors.verticalCenter: parent.verticalCenter
text: "Add App"
color: Data.ThemeManager.accentColor
font.pixelSize: 12
font.bold: true
font.family: "Roboto"
}
}
MouseArea {
id: addAppMouseArea
anchors.fill: parent
hoverEnabled: true
onClicked: addAppPopup.open()
}
}
}
}
// Quick suggestions
Column {
width: parent.width
spacing: 8
Text {
text: "Common Apps"
color: Qt.rgba(Data.ThemeManager.fgColor.r, Data.ThemeManager.fgColor.g, Data.ThemeManager.fgColor.b, 0.7)
font.pixelSize: 12
font.family: "Roboto"
}
Flow {
width: parent.width
spacing: 6
Repeater {
model: ["Discord", "Spotify", "Steam", "Firefox", "Chrome", "VSCode", "Slack"]
delegate: Rectangle {
width: suggestedAppText.width + 16
height: 24
radius: 12
color: suggestionMouseArea.containsMouse ?
Qt.rgba(Data.ThemeManager.accentColor.r, Data.ThemeManager.accentColor.g, Data.ThemeManager.accentColor.b, 0.1) :
"transparent"
border.width: 1
border.color: Qt.rgba(Data.ThemeManager.fgColor.r, Data.ThemeManager.fgColor.g, Data.ThemeManager.fgColor.b, 0.3)
Text {
id: suggestedAppText
anchors.centerIn: parent
text: modelData
color: Qt.rgba(Data.ThemeManager.fgColor.r, Data.ThemeManager.fgColor.g, Data.ThemeManager.fgColor.b, 0.7)
font.pixelSize: 11
font.family: "Roboto"
}
MouseArea {
id: suggestionMouseArea
anchors.fill: parent
hoverEnabled: true
onClicked: {
Data.Settings.addIgnoredApp(modelData)
}
}
}
}
}
}
}
}
// Add app popup
Popup {
id: addAppPopup
parent: notificationSettings
width: 280
height: 160
x: (parent.width - width) / 2
y: (parent.height - height) / 2
modal: true
focus: true
closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside
background: Rectangle {
color: Data.ThemeManager.bgColor
border.color: Data.ThemeManager.accentColor
border.width: 2
radius: 20
}
Column {
anchors.centerIn: parent
spacing: 16
width: parent.width - 40
Text {
anchors.horizontalCenter: parent.horizontalCenter
text: "Add Ignored App"
color: Data.ThemeManager.accentColor
font.pixelSize: 16
font.bold: true
font.family: "Roboto"
}
Rectangle {
width: parent.width
height: 40
radius: 20
color: Qt.lighter(Data.ThemeManager.bgColor, 1.15)
border.width: appNameInput.activeFocus ? 2 : 1
border.color: appNameInput.activeFocus ? Data.ThemeManager.accentColor : Qt.rgba(Data.ThemeManager.fgColor.r, Data.ThemeManager.fgColor.g, Data.ThemeManager.fgColor.b, 0.3)
Behavior on border.color {
ColorAnimation { duration: 150 }
}
TextInput {
id: appNameInput
anchors.fill: parent
anchors.margins: 12
color: Data.ThemeManager.fgColor
font.pixelSize: 14
font.family: "Roboto"
selectByMouse: true
clip: true
verticalAlignment: TextInput.AlignVCenter
focus: true
activeFocusOnTab: true
inputMethodHints: Qt.ImhNone
Keys.onPressed: function(event) {
if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) {
addAppButton.clicked()
event.accepted = true
}
}
// Placeholder text implementation
Text {
anchors.fill: parent
anchors.margins: 12
text: "App name (e.g. Discord)"
color: Qt.rgba(Data.ThemeManager.fgColor.r, Data.ThemeManager.fgColor.g, Data.ThemeManager.fgColor.b, 0.5)
font.pixelSize: 14
font.family: "Roboto"
verticalAlignment: Text.AlignVCenter
visible: appNameInput.text === ""
}
}
}
Row {
anchors.horizontalCenter: parent.horizontalCenter
spacing: 12
Rectangle {
width: 80
height: 32
radius: 16
color: cancelMouseArea.containsMouse ? Qt.rgba(Data.ThemeManager.fgColor.r, Data.ThemeManager.fgColor.g, Data.ThemeManager.fgColor.b, 0.1) : "transparent"
border.width: 1
border.color: Qt.rgba(Data.ThemeManager.fgColor.r, Data.ThemeManager.fgColor.g, Data.ThemeManager.fgColor.b, 0.3)
Text {
anchors.centerIn: parent
text: "Cancel"
color: Data.ThemeManager.fgColor
font.pixelSize: 12
font.family: "Roboto"
}
MouseArea {
id: cancelMouseArea
anchors.fill: parent
hoverEnabled: true
onClicked: {
appNameInput.text = ""
addAppPopup.close()
}
}
}
Rectangle {
id: addAppButton
width: 80
height: 32
radius: 16
color: addMouseArea.containsMouse ? Qt.lighter(Data.ThemeManager.accentColor, 1.1) : Data.ThemeManager.accentColor
signal clicked()
onClicked: {
if (appNameInput.text.trim() !== "") {
if (Data.Settings.addIgnoredApp(appNameInput.text.trim())) {
appNameInput.text = ""
addAppPopup.close()
}
}
}
Text {
anchors.centerIn: parent
text: "Add"
color: Data.ThemeManager.bgColor
font.pixelSize: 12
font.bold: true
font.family: "Roboto"
}
MouseArea {
id: addMouseArea
anchors.fill: parent
hoverEnabled: true
onClicked: parent.clicked()
}
}
}
}
onOpened: {
appNameInput.forceActiveFocus()
}
}
}

View file

@ -0,0 +1,104 @@
import QtQuick
import QtQuick.Controls
import "root:/Data" as Data
// Reusable collapsible settings category component
Item {
id: categoryRoot
property string title: ""
property string icon: ""
property bool expanded: false
property alias content: contentLoader.sourceComponent
height: headerRect.height + (expanded ? contentLoader.height + 20 : 0)
Behavior on height {
NumberAnimation {
duration: 250
easing.type: Easing.OutCubic
}
}
// Category header
Rectangle {
id: headerRect
width: parent.width
height: 50
radius: 12
color: expanded ? Qt.rgba(Data.ThemeManager.accentColor.r, Data.ThemeManager.accentColor.g, Data.ThemeManager.accentColor.b, 0.1) :
Qt.lighter(Data.ThemeManager.bgColor, 1.15)
border.width: expanded ? 2 : 1
border.color: expanded ? Data.ThemeManager.accentColor : Qt.rgba(Data.ThemeManager.fgColor.r, Data.ThemeManager.fgColor.g, Data.ThemeManager.fgColor.b, 0.3)
Row {
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
anchors.leftMargin: 16
spacing: 12
Text {
anchors.verticalCenter: parent.verticalCenter
text: categoryRoot.icon
font.family: "Material Symbols Outlined"
font.pixelSize: 20
color: expanded ? Data.ThemeManager.accentColor : Data.ThemeManager.fgColor
}
Text {
anchors.verticalCenter: parent.verticalCenter
text: categoryRoot.title
color: expanded ? Data.ThemeManager.accentColor : Data.ThemeManager.fgColor
font.pixelSize: 16
font.bold: true
font.family: "Roboto"
}
}
// Expand/collapse arrow
Text {
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
anchors.rightMargin: 16
text: expanded ? "expand_less" : "expand_more"
font.family: "Material Symbols Outlined"
font.pixelSize: 20
color: expanded ? Data.ThemeManager.accentColor : Data.ThemeManager.fgColor
Behavior on rotation {
NumberAnimation {
duration: 250
easing.type: Easing.OutCubic
}
}
}
MouseArea {
anchors.fill: parent
onClicked: {
categoryRoot.expanded = !categoryRoot.expanded
}
}
}
// Category content
Loader {
id: contentLoader
anchors.top: headerRect.bottom
anchors.left: parent.left
anchors.right: parent.right
anchors.topMargin: expanded ? 20 : 0
anchors.leftMargin: 16
anchors.rightMargin: 16
visible: expanded
opacity: expanded ? 1 : 0
Behavior on opacity {
NumberAnimation {
duration: 250
easing.type: Easing.OutCubic
}
}
}
}

View file

@ -0,0 +1,131 @@
import QtQuick
import QtQuick.Controls
import "root:/Data" as Data
// System settings content
Item {
id: systemSettings
width: parent.width
height: contentColumn.height
// Expose the text input focus for parent keyboard management
property bool anyTextInputFocused: videoPathInput.activeFocus || wallpaperDirectoryInput.activeFocus
Column {
id: contentColumn
width: parent.width
spacing: 20
// Video Recording Path
Column {
width: parent.width
spacing: 8
Text {
text: "Video Recording Path"
color: Data.ThemeManager.fgColor
font.pixelSize: 16
font.bold: true
font.family: "Roboto"
}
Rectangle {
width: parent.width
height: 40
radius: 8
color: Qt.lighter(Data.ThemeManager.bgColor, 1.15)
border.width: videoPathInput.activeFocus ? 2 : 1
border.color: videoPathInput.activeFocus ? Data.ThemeManager.accentColor : Qt.rgba(Data.ThemeManager.fgColor.r, Data.ThemeManager.fgColor.g, Data.ThemeManager.fgColor.b, 0.3)
Behavior on border.color {
ColorAnimation { duration: 150 }
}
TextInput {
id: videoPathInput
anchors.fill: parent
anchors.margins: 12
text: Data.Settings.videoPath
color: Data.ThemeManager.fgColor
font.pixelSize: 14
font.family: "Roboto"
selectByMouse: true
clip: true
verticalAlignment: TextInput.AlignVCenter
focus: true
activeFocusOnTab: true
inputMethodHints: Qt.ImhNone
onTextChanged: {
Data.Settings.videoPath = text
}
Keys.onPressed: function(event) {
// Allow default text input behavior
}
}
MouseArea {
anchors.fill: parent
onClicked: {
videoPathInput.forceActiveFocus()
}
}
}
}
// Wallpaper Directory
Column {
width: parent.width
spacing: 8
Text {
text: "Wallpaper Directory"
color: Data.ThemeManager.fgColor
font.pixelSize: 16
font.bold: true
font.family: "Roboto"
}
Rectangle {
width: parent.width
height: 40
radius: 8
color: Qt.lighter(Data.ThemeManager.bgColor, 1.15)
border.width: wallpaperDirectoryInput.activeFocus ? 2 : 1
border.color: wallpaperDirectoryInput.activeFocus ? Data.ThemeManager.accentColor : Qt.rgba(Data.ThemeManager.fgColor.r, Data.ThemeManager.fgColor.g, Data.ThemeManager.fgColor.b, 0.3)
Behavior on border.color {
ColorAnimation { duration: 150 }
}
TextInput {
id: wallpaperDirectoryInput
anchors.fill: parent
anchors.margins: 12
text: Data.Settings.wallpaperDirectory
color: Data.ThemeManager.fgColor
font.pixelSize: 14
font.family: "Roboto"
selectByMouse: true
clip: true
verticalAlignment: TextInput.AlignVCenter
focus: true
activeFocusOnTab: true
inputMethodHints: Qt.ImhNone
onTextChanged: {
Data.Settings.wallpaperDirectory = text
}
MouseArea {
anchors.fill: parent
onClicked: {
wallpaperDirectoryInput.forceActiveFocus()
}
}
}
}
}
}
}

View file

@ -0,0 +1,197 @@
import QtQuick
import QtQuick.Controls
import "root:/Data" as Data
// Weather settings content
Item {
id: weatherSettings
width: parent.width
height: contentColumn.height
required property var shell
// Expose the text input focus for parent keyboard management
property bool anyTextInputFocused: locationInput.activeFocus
Column {
id: contentColumn
width: parent.width
spacing: 20
// Location Setting
Column {
width: parent.width
spacing: 8
Text {
text: "Location"
color: Data.ThemeManager.fgColor
font.pixelSize: 16
font.bold: true
font.family: "Roboto"
}
Row {
width: parent.width
spacing: 12
Rectangle {
width: parent.width - applyButton.width - 12
height: 40
radius: 8
color: Qt.lighter(Data.ThemeManager.bgColor, 1.15)
border.width: locationInput.activeFocus ? 2 : 1
border.color: locationInput.activeFocus ? Data.ThemeManager.accentColor : Qt.rgba(Data.ThemeManager.fgColor.r, Data.ThemeManager.fgColor.g, Data.ThemeManager.fgColor.b, 0.3)
Behavior on border.color {
ColorAnimation { duration: 150 }
}
TextInput {
id: locationInput
anchors.fill: parent
anchors.margins: 12
text: Data.Settings.weatherLocation
color: Data.ThemeManager.fgColor
font.pixelSize: 14
font.family: "Roboto"
selectByMouse: true
clip: true
verticalAlignment: TextInput.AlignVCenter
focus: true
activeFocusOnTab: true
inputMethodHints: Qt.ImhNone
Keys.onPressed: function(event) {
if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) {
applyButton.clicked()
event.accepted = true
}
}
MouseArea {
anchors.fill: parent
onClicked: {
locationInput.forceActiveFocus()
}
}
}
}
Rectangle {
id: applyButton
width: 80
height: 40
radius: 8
color: applyMouseArea.containsMouse ? Qt.lighter(Data.ThemeManager.accentColor, 1.1) : Data.ThemeManager.accentColor
signal clicked()
onClicked: {
Data.Settings.weatherLocation = locationInput.text
weatherSettings.shell.weatherService.loadWeather()
}
Text {
anchors.centerIn: parent
text: "Apply"
color: Data.ThemeManager.bgColor
font.pixelSize: 12
font.bold: true
font.family: "Roboto"
}
MouseArea {
id: applyMouseArea
anchors.fill: parent
hoverEnabled: true
onClicked: parent.clicked()
}
}
}
}
// Temperature Units
Column {
width: parent.width
spacing: 12
Text {
text: "Temperature Units"
color: Data.ThemeManager.fgColor
font.pixelSize: 16
font.bold: true
font.family: "Roboto"
}
Row {
spacing: 12
Rectangle {
width: 80
height: 35
radius: 18
color: !Data.Settings.useFahrenheit ? Data.ThemeManager.accentColor : Qt.lighter(Data.ThemeManager.bgColor, 1.15)
border.width: 1
border.color: Data.ThemeManager.accentColor
Behavior on color {
ColorAnimation { duration: 200 }
}
Text {
anchors.centerIn: parent
text: "°C"
color: !Data.Settings.useFahrenheit ? Data.ThemeManager.bgColor : Data.ThemeManager.fgColor
font.pixelSize: 14
font.bold: true
font.family: "Roboto"
Behavior on color {
ColorAnimation { duration: 200 }
}
}
MouseArea {
anchors.fill: parent
onClicked: {
Data.Settings.useFahrenheit = false
}
}
}
Rectangle {
width: 80
height: 35
radius: 18
color: Data.Settings.useFahrenheit ? Data.ThemeManager.accentColor : Qt.lighter(Data.ThemeManager.bgColor, 1.15)
border.width: 1
border.color: Data.ThemeManager.accentColor
Behavior on color {
ColorAnimation { duration: 200 }
}
Text {
anchors.centerIn: parent
text: "°F"
color: Data.Settings.useFahrenheit ? Data.ThemeManager.bgColor : Data.ThemeManager.fgColor
font.pixelSize: 14
font.bold: true
font.family: "Roboto"
Behavior on color {
ColorAnimation { duration: 200 }
}
}
MouseArea {
anchors.fill: parent
onClicked: {
Data.Settings.useFahrenheit = true
}
}
}
}
}
}
}

View file

@ -0,0 +1,98 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import "root:/Data" as Data
// Dual-button notification and clipboard history toggle bar
Rectangle {
id: root
width: 42
color: Qt.darker(Data.ThemeManager.bgColor, 1.15)
radius: 12
z: 2 // Above notification history overlay
required property bool notificationHistoryVisible
required property bool clipboardHistoryVisible
required property var notificationHistory
signal notificationToggleRequested()
signal clipboardToggleRequested()
// Combined hover state for parent component tracking
property bool containsMouse: notifButtonMouseArea.containsMouse || clipButtonMouseArea.containsMouse
property real buttonHeight: 38
height: buttonHeight * 2 + 4 // Two buttons with spacing
Item {
anchors.fill: parent
anchors.margins: 2
// Notifications toggle button (top half)
Rectangle {
id: notificationPill
anchors {
top: parent.top
left: parent.left
right: parent.right
bottom: parent.verticalCenter
bottomMargin: 2 // Half of button spacing
}
radius: 12
color: notifButtonMouseArea.containsMouse || root.notificationHistoryVisible ?
Qt.rgba(Data.ThemeManager.accentColor.r, Data.ThemeManager.accentColor.g, Data.ThemeManager.accentColor.b, 0.2) :
Qt.rgba(Data.ThemeManager.fgColor.r, Data.ThemeManager.fgColor.g, Data.ThemeManager.fgColor.b, 0.05)
border.color: notifButtonMouseArea.containsMouse || root.notificationHistoryVisible ? Data.ThemeManager.accentColor : "transparent"
border.width: 1
MouseArea {
id: notifButtonMouseArea
anchors.fill: parent
hoverEnabled: true
onClicked: root.notificationToggleRequested()
}
Label {
anchors.centerIn: parent
text: "notifications"
font.family: "Material Symbols Outlined"
font.pixelSize: 16
color: notifButtonMouseArea.containsMouse || root.notificationHistoryVisible ?
Data.ThemeManager.accentColor : Data.ThemeManager.fgColor
}
}
// Clipboard toggle button (bottom half)
Rectangle {
id: clipboardPill
anchors {
top: parent.verticalCenter
left: parent.left
right: parent.right
bottom: parent.bottom
topMargin: 2 // Half of button spacing
}
radius: 12
color: clipButtonMouseArea.containsMouse || root.clipboardHistoryVisible ?
Qt.rgba(Data.ThemeManager.accentColor.r, Data.ThemeManager.accentColor.g, Data.ThemeManager.accentColor.b, 0.2) :
Qt.rgba(Data.ThemeManager.fgColor.r, Data.ThemeManager.fgColor.g, Data.ThemeManager.fgColor.b, 0.05)
border.color: clipButtonMouseArea.containsMouse || root.clipboardHistoryVisible ? Data.ThemeManager.accentColor : "transparent"
border.width: 1
MouseArea {
id: clipButtonMouseArea
anchors.fill: parent
hoverEnabled: true
onClicked: root.clipboardToggleRequested()
}
Label {
anchors.centerIn: parent
text: "content_paste"
font.family: "Material Symbols Outlined"
font.pixelSize: 16
color: clipButtonMouseArea.containsMouse || root.clipboardHistoryVisible ?
Data.ThemeManager.accentColor : Data.ThemeManager.fgColor
}
}
}
}

View file

@ -0,0 +1,52 @@
import QtQuick
// Top-edge hover trigger
Rectangle {
id: root
width: 360
height: 1
color: "red"
anchors.top: parent.top
signal triggered()
// Hover detection area at screen top edge
MouseArea {
id: mouseArea
anchors.fill: parent
hoverEnabled: true
property bool isHovered: containsMouse
// Timer coordination
onIsHoveredChanged: {
if (isHovered) {
showTimer.start()
hideTimer.stop()
} else {
hideTimer.start()
showTimer.stop()
}
}
onEntered: hideTimer.stop()
}
// Delayed show trigger to prevent accidental activation
Timer {
id: showTimer
interval: 200
onTriggered: root.triggered()
}
// Hide delay timer (controlled by parent)
Timer {
id: hideTimer
interval: 500
}
// Public interface
readonly property alias containsMouse: mouseArea.containsMouse
function stopHideTimer() { hideTimer.stop() }
function startHideTimer() { hideTimer.start() }
}

View file

@ -0,0 +1,226 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import "root:/Data" as Data
// System tray context menu
Rectangle {
id: root
width: parent.width
height: visible ? calculatedHeight : 0
visible: false
enabled: visible
clip: true
color: Data.ThemeManager.bgColor
border.color: Data.ThemeManager.accentColor
border.width: 2
radius: 20
required property var menu
required property var systemTrayY
required property var systemTrayHeight
property bool containsMouse: trayMenuMouseArea.containsMouse
property bool menuJustOpened: false
property point triggerPoint: Qt.point(0, 0)
property Item originalParent
property int totalCount: opener.children ? opener.children.values.length : 0
signal hideRequested()
MouseArea {
id: trayMenuMouseArea
anchors.fill: parent
hoverEnabled: true
preventStealing: true
propagateComposedEvents: false
}
onVisibleChanged: {
if (visible) {
menuJustOpened = true
Qt.callLater(function() {
menuJustOpened = false
})
}
}
function toggle() {
visible = !visible
if (visible) {
menuJustOpened = true
Qt.callLater(function() {
menuJustOpened = false
})
}
}
function show(point, parentItem) {
visible = true
menuJustOpened = true
Qt.callLater(function() {
menuJustOpened = false
})
}
function hide() {
visible = false
menuJustOpened = false
// Small delay before notifying hide to prevent control panel flicker
Qt.callLater(function() {
hideRequested()
})
}
// Smart positioning to avoid screen edges
y: {
var preferredY = systemTrayY + systemTrayHeight + 10
var availableSpace = parent.height - preferredY - 20
if (calculatedHeight > availableSpace) {
return systemTrayY - height - 10
}
return preferredY
}
Behavior on height {
NumberAnimation { duration: 200; easing.type: Easing.OutCubic }
}
// Dynamic height calculation based on menu item count and types
property int calculatedHeight: {
if (totalCount === 0) return 40
var separatorCount = 0
var regularItemCount = 0
if (opener.children && opener.children.values) {
for (var i = 0; i < opener.children.values.length; i++) {
if (opener.children.values[i].isSeparator) {
separatorCount++
} else {
regularItemCount++
}
}
}
// Calculate total height: separators + grid rows + margins
var separatorHeight = separatorCount * 12
var regularItemRows = Math.ceil(regularItemCount / 2)
var regularItemHeight = regularItemRows * 32
return Math.max(80, 35 + separatorHeight + regularItemHeight + 40)
}
// Menu opener handles the native menu integration
QsMenuOpener {
id: opener
menu: root.menu
}
// Grid layout for menu items (2 columns)
GridView {
id: gridView
anchors.fill: parent
anchors.margins: 20
cellWidth: width / 2
cellHeight: 32
interactive: false
flow: GridView.FlowLeftToRight
layoutDirection: Qt.LeftToRight
model: ScriptModel {
values: opener.children ? [...opener.children.values] : []
}
delegate: Item {
id: entry
required property var modelData
required property int index
width: gridView.cellWidth - 4
height: modelData.isSeparator ? 12 : 30
// Separator line
Rectangle {
anchors.fill: parent
anchors.leftMargin: 8
anchors.rightMargin: 8
anchors.topMargin: 4
anchors.bottomMargin: 4
visible: modelData.isSeparator
color: "transparent"
Rectangle {
anchors.centerIn: parent
width: parent.width * 0.8
height: 1
color: Qt.darker(Data.ThemeManager.accentColor, 1.5)
opacity: 0.6
}
}
// Regular menu item
Rectangle {
id: itemBackground
anchors.fill: parent
anchors.margins: 2
visible: !modelData.isSeparator
color: "transparent"
radius: 6
RowLayout {
anchors.fill: parent
anchors.leftMargin: 8
anchors.rightMargin: 8
spacing: 6
Image {
Layout.preferredWidth: 16
Layout.preferredHeight: 16
source: modelData?.icon ?? ""
visible: (modelData?.icon ?? "") !== ""
fillMode: Image.PreserveAspectFit
}
Text {
Layout.fillWidth: true
color: mouseArea.containsMouse ? Data.ThemeManager.accentColor : Data.ThemeManager.fgColor
text: modelData?.text ?? ""
font.pixelSize: 11
font.family: "Roboto"
verticalAlignment: Text.AlignVCenter
elide: Text.ElideRight
maximumLineCount: 1
}
}
MouseArea {
id: mouseArea
anchors.fill: parent
hoverEnabled: true
enabled: (modelData?.enabled ?? true) && root.visible && !modelData.isSeparator
onEntered: itemBackground.color = Qt.rgba(Data.ThemeManager.accentColor.r, Data.ThemeManager.accentColor.g, Data.ThemeManager.accentColor.b, 0.15)
onExited: itemBackground.color = "transparent"
onClicked: {
modelData.triggered()
root.hide()
}
}
}
}
}
// Empty state indicator
Item {
anchors.centerIn: gridView
visible: gridView.count === 0
Label {
anchors.centerIn: parent
text: "No tray items available"
color: Qt.darker(Data.ThemeManager.fgColor, 2)
font.pixelSize: 14
font.family: "Roboto"
}
}
}

View file

@ -0,0 +1,149 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Qt5Compat.GraphicalEffects
import "root:/Data" as Data
// Wallpaper selector grid
Item {
id: root
property bool isVisible: false
signal visibilityChanged(bool visible)
// Use all space provided by parent
anchors.fill: parent
visible: isVisible
enabled: visible
clip: true
property bool containsMouse: wallpaperSelectorMouseArea.containsMouse || scrollView.containsMouse
property bool menuJustOpened: false
// Hover state management for auto-hide functionality
onContainsMouseChanged: {
if (containsMouse) {
hideTimer.stop()
} else if (!menuJustOpened && !isVisible) {
hideTimer.restart()
}
}
onVisibleChanged: {
if (visible) {
menuJustOpened = true
hideTimer.stop()
Qt.callLater(function() {
menuJustOpened = false
})
}
}
MouseArea {
id: wallpaperSelectorMouseArea
anchors.fill: parent
hoverEnabled: true
preventStealing: false
propagateComposedEvents: true
}
// Scrollable wallpaper grid with memory-conscious loading
ScrollView {
id: scrollView
anchors.fill: parent
clip: true
property bool containsMouse: gridMouseArea.containsMouse
MouseArea {
id: gridMouseArea
anchors.fill: parent
hoverEnabled: true
propagateComposedEvents: true
}
GridView {
id: wallpaperGrid
anchors.fill: parent
cellWidth: parent.width / 2 - 8 // 2-column layout with spacing
cellHeight: cellWidth * 0.6 // Wallpaper aspect ratio
model: Data.WallpaperManager.wallpaperList
cacheBuffer: 0 // Memory optimization - no cache buffer
leftMargin: 4
rightMargin: 4
topMargin: 4
bottomMargin: 4
delegate: Item {
width: wallpaperGrid.cellWidth - 8
height: wallpaperGrid.cellHeight - 8
Rectangle {
id: wallpaperItem
anchors.fill: parent
anchors.margins: 4
color: Qt.darker(Data.ThemeManager.bgColor, 1.2)
radius: 20
Behavior on scale {
NumberAnimation { duration: 150; easing.type: Easing.OutCubic }
}
// Wallpaper preview image with viewport-based loading
Image {
id: wallpaperImage
anchors.fill: parent
anchors.margins: 4
source: modelData
fillMode: Image.PreserveAspectCrop
asynchronous: true
cache: false // Memory optimization - no image caching
sourceSize.width: Math.min(width, 150) // Reduced resolution for memory
sourceSize.height: Math.min(height, 90)
// Only load when visible in viewport - major memory optimization
visible: parent.parent.y >= wallpaperGrid.contentY - parent.parent.height &&
parent.parent.y <= wallpaperGrid.contentY + wallpaperGrid.height
// Layer effects disabled for memory savings
// layer.enabled: true
// layer.effect: OpacityMask {
// maskSource: Rectangle {
// width: wallpaperImage.width
// height: wallpaperImage.height
// radius: 18
// }
// }
}
// Current wallpaper selection indicator
Rectangle {
visible: modelData === Data.WallpaperManager.currentWallpaper
anchors.fill: parent
radius: parent.radius
color: "transparent"
border.color: Data.ThemeManager.accentColor
border.width: 2
}
// Hover and click handling
MouseArea {
anchors.fill: parent
hoverEnabled: true
onEntered: wallpaperItem.scale = 1.05
onExited: wallpaperItem.scale = 1.0
onClicked: {
Data.WallpaperManager.setWallpaper(modelData)
// Stays in wallpaper tab after selection
}
}
}
}
}
}
Component.onCompleted: {
// Use lazy loading to only load wallpapers when this component is actually used
Data.WallpaperManager.ensureWallpapersLoaded()
}
}

View file

@ -0,0 +1,75 @@
import QtQuick
import QtQuick.Controls
import "root:/Data" as Data
// Calendar button
Rectangle {
id: calendarButton
width: 40
height: 80
color: Qt.darker(Data.ThemeManager.bgColor, 1.15)
radius: 20
property bool containsMouse: calendarMouseArea.containsMouse
property bool calendarVisible: false
property var calendarPopup: null
property var shell: null // Shell reference from parent
signal entered()
signal exited()
// Hover state management
onContainsMouseChanged: {
if (containsMouse) {
entered()
} else {
exited()
}
}
MouseArea {
id: calendarMouseArea
anchors.fill: parent
hoverEnabled: true
onClicked: {
toggleCalendar()
}
}
// Calendar icon
Label {
anchors.centerIn: parent
text: "calendar_month"
font.pixelSize: 24
font.family: "Material Symbols Outlined"
color: calendarButton.containsMouse || calendarButton.calendarVisible ? Data.ThemeManager.accentColor : Data.ThemeManager.fgColor
}
// Toggle calendar popup
function toggleCalendar() {
if (!calendarPopup) {
var component = Qt.createComponent("root:/Widgets/Calendar/CalendarPopup.qml")
if (component.status === Component.Ready) {
calendarPopup = component.createObject(calendarButton.parent, {
"targetX": calendarButton.x + calendarButton.width + 10,
"shell": calendarButton.shell
})
} else if (component.status === Component.Error) {
console.log("Error loading calendar:", component.errorString())
return
}
}
if (calendarPopup) {
calendarVisible = !calendarVisible
calendarPopup.setClickMode(calendarVisible)
}
}
function hideCalendar() {
if (calendarPopup) {
calendarVisible = false
calendarPopup.setClickMode(false)
}
}
}

View file

@ -0,0 +1,297 @@
import QtQuick
import QtQuick.Controls
import Quickshell
import Quickshell.Wayland
import "root:/Data" as Data
// Night light widget with pure Qt overlay (no external dependencies)
Rectangle {
id: root
property var shell: null
color: Qt.darker(Data.ThemeManager.bgColor, 1.15)
radius: 20
property bool containsMouse: nightLightMouseArea.containsMouse
property bool isActive: Data.Settings.nightLightEnabled
property real warmth: Data.Settings.nightLightWarmth // 0=no filter, 1=very warm (0-1 scale)
property real strength: isActive ? warmth : 0
property bool autoSchedulerActive: false // Flag to prevent manual override during auto changes
signal entered()
signal exited()
// Night light overlay window
property var overlayWindow: null
// Hover state management for parent components
onContainsMouseChanged: {
if (containsMouse) {
entered()
} else {
exited()
}
}
// Background with warm tint when active
Rectangle {
anchors.fill: parent
radius: parent.radius
color: isActive ? Qt.rgba(1.0, 0.6, 0.2, 0.15) : "transparent"
Behavior on color {
ColorAnimation { duration: 300 }
}
}
MouseArea {
id: nightLightMouseArea
anchors.fill: parent
hoverEnabled: true
// Right-click to cycle through warmth levels
acceptedButtons: Qt.LeftButton | Qt.RightButton
onClicked: function(mouse) {
if (mouse.button === Qt.RightButton) {
cycleWarmth()
} else {
toggleNightLight()
}
}
}
// Night light icon with dynamic color
Text {
anchors.centerIn: parent
text: isActive ? "light_mode" : "dark_mode"
font.pixelSize: 24
font.family: "Material Symbols Outlined"
color: isActive ?
Qt.rgba(1.0, 0.8 - strength * 0.3, 0.4 - strength * 0.2, 1.0) : // Warm orange when active
(containsMouse ? Data.ThemeManager.accentColor : Data.ThemeManager.fgColor)
Behavior on color {
ColorAnimation { duration: 200 }
}
}
// Warmth indicator dots
Row {
anchors.bottom: parent.bottom
anchors.bottomMargin: 6
anchors.horizontalCenter: parent.horizontalCenter
spacing: 3
visible: isActive && containsMouse
Repeater {
model: 3
delegate: Rectangle {
width: 4
height: 4
radius: 2
color: index < Math.ceil(warmth * 3) ?
Qt.rgba(1.0, 0.7 - index * 0.2, 0.3, 0.8) :
Qt.rgba(Data.ThemeManager.fgColor.r, Data.ThemeManager.fgColor.g, Data.ThemeManager.fgColor.b, 0.3)
Behavior on color {
ColorAnimation { duration: 150 }
}
}
}
}
// Watch for settings changes
Connections {
target: Data.Settings
function onNightLightEnabledChanged() {
if (Data.Settings.nightLightEnabled) {
createOverlay()
} else {
removeOverlay()
}
// Set manual override flag if this wasn't an automatic change
if (!autoSchedulerActive) {
Data.Settings.nightLightManualOverride = true
Data.Settings.nightLightManuallyEnabled = Data.Settings.nightLightEnabled
console.log("Manual night light change detected - override enabled, manually set to:", Data.Settings.nightLightEnabled)
}
}
function onNightLightWarmthChanged() {
updateOverlay()
}
}
// Functions to control night light
function toggleNightLight() {
Data.Settings.nightLightEnabled = !Data.Settings.nightLightEnabled
}
function cycleWarmth() {
// Cycle through warmth levels: 0.2 -> 0.4 -> 0.6 -> 1.0 -> 0.2
var newWarmth = warmth >= 1.0 ? 0.2 : (warmth >= 0.6 ? 1.0 : warmth + 0.2)
Data.Settings.nightLightWarmth = newWarmth
}
function createOverlay() {
if (overlayWindow) return
var qmlString = `
import QtQuick
import Quickshell
import Quickshell.Wayland
PanelWindow {
id: nightLightOverlay
screen: Quickshell.primaryScreen || Quickshell.screens[0]
anchors.top: true
anchors.left: true
anchors.right: true
anchors.bottom: true
color: "transparent"
WlrLayershell.layer: WlrLayershell.Overlay
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
WlrLayershell.namespace: "quickshell-nightlight"
exclusiveZone: 0
// Click-through overlay
mask: Region {}
Rectangle {
id: overlayRect
anchors.fill: parent
color: "transparent" // Initial color, will be set by parent
// Smooth transitions when warmth changes
Behavior on color {
ColorAnimation { duration: 300 }
}
}
// Function to update overlay color
function updateColor(newWarmth) {
overlayRect.color = Qt.rgba(1.0, 0.8 - newWarmth * 0.4, 0.3 - newWarmth * 0.25, 0.1 + newWarmth * 0.2)
}
}
`
try {
overlayWindow = Qt.createQmlObject(qmlString, root)
// Set initial color
updateOverlay()
} catch (e) {
console.error("Failed to create night light overlay:", e)
}
}
function updateOverlay() {
if (overlayWindow && overlayWindow.updateColor) {
overlayWindow.updateColor(warmth)
}
}
function removeOverlay() {
if (overlayWindow) {
overlayWindow.destroy()
overlayWindow = null
}
}
// Preset warmth levels for easy access
function setLow() { Data.Settings.nightLightWarmth = 0.2 } // Light warmth
function setMedium() { Data.Settings.nightLightWarmth = 0.4 } // Medium warmth
function setHigh() { Data.Settings.nightLightWarmth = 0.6 } // High warmth
function setMax() { Data.Settings.nightLightWarmth = 1.0 } // Maximum warmth
// Auto-enable based on time (basic sunset/sunrise simulation)
Timer {
interval: 60000 // Check every minute
running: true
repeat: true
onTriggered: checkAutoEnable()
}
function checkAutoEnable() {
if (!Data.Settings.nightLightAuto) return
var now = new Date()
var hour = now.getHours()
var minute = now.getMinutes()
var startHour = Data.Settings.nightLightStartHour || 20
var endHour = Data.Settings.nightLightEndHour || 6
// Handle overnight schedules (e.g., 20:00 to 6:00)
var shouldBeActive = false
if (startHour > endHour) {
// Overnight: active from startHour onwards OR before endHour
shouldBeActive = (hour >= startHour || hour < endHour)
} else if (startHour < endHour) {
// Same day: active between startHour and endHour
shouldBeActive = (hour >= startHour && hour < endHour)
} else {
// startHour === endHour: never auto-enable
shouldBeActive = false
}
// Debug logging
console.log(`Night Light Auto Check: ${hour}:${minute.toString().padStart(2, '0')} - Should be active: ${shouldBeActive}, Currently active: ${Data.Settings.nightLightEnabled}, Manual override: ${Data.Settings.nightLightManualOverride}`)
// Smart override logic - only block conflicting actions
if (Data.Settings.nightLightManualOverride) {
// If user manually enabled, allow auto-disable but block auto-enable
if (Data.Settings.nightLightManuallyEnabled && !shouldBeActive && Data.Settings.nightLightEnabled) {
console.log("Auto-disabling night light (respecting schedule after manual enable)")
autoSchedulerActive = true
Data.Settings.nightLightEnabled = false
Data.Settings.nightLightManualOverride = false // Reset after respecting schedule
autoSchedulerActive = false
return
}
// If user manually disabled, block auto-enable until next cycle
else if (!Data.Settings.nightLightManuallyEnabled && shouldBeActive && !Data.Settings.nightLightEnabled) {
// Check if this is the start of a new schedule cycle
var isNewCycle = (hour === startHour && minute === 0)
if (isNewCycle) {
console.log("New schedule cycle starting - resetting manual override")
Data.Settings.nightLightManualOverride = false
} else {
console.log("Manual disable override active - skipping auto-enable")
return
}
}
// Other cases - reset override and continue
else {
Data.Settings.nightLightManualOverride = false
}
}
// Auto-enable when schedule starts
if (shouldBeActive && !Data.Settings.nightLightEnabled) {
console.log("Auto-enabling night light")
autoSchedulerActive = true
Data.Settings.nightLightEnabled = true
autoSchedulerActive = false
}
// Auto-disable when schedule ends
else if (!shouldBeActive && Data.Settings.nightLightEnabled) {
console.log("Auto-disabling night light")
autoSchedulerActive = true
Data.Settings.nightLightEnabled = false
autoSchedulerActive = false
}
}
// Cleanup on destruction
Component.onDestruction: {
removeOverlay()
}
// Initialize overlay state based on settings
Component.onCompleted: {
if (Data.Settings.nightLightEnabled) {
createOverlay()
}
}
}

View file

@ -0,0 +1,67 @@
import QtQuick
import QtQuick.Layouts
import QtQuick.Controls
import "root:/Data" as Data
// Screen recording toggle button
Rectangle {
id: root
required property var shell
required property bool isRecording
radius: 20
signal recordingRequested()
signal stopRecordingRequested()
signal mouseChanged(bool containsMouse)
// Dynamic color: accent when recording/hovered, gray otherwise
color: isRecording ? Data.ThemeManager.accentColor :
(mouseArea.containsMouse ? Data.ThemeManager.accentColor : Qt.darker(Data.ThemeManager.bgColor, 1.15))
property bool isHovered: mouseArea.containsMouse
readonly property alias containsMouse: mouseArea.containsMouse
// Button content with icon and text
RowLayout {
anchors.centerIn: parent
spacing: 10
// Recording state icon
Text {
text: isRecording ? "stop_circle" : "radio_button_unchecked"
font.family: "Material Symbols Outlined"
font.pixelSize: 16
color: isRecording || mouseArea.containsMouse ? "#ffffff" : Data.ThemeManager.fgColor
Layout.alignment: Qt.AlignVCenter
}
// Recording state label
Label {
text: isRecording ? "Stop Recording" : "Start Recording"
font.family: "Roboto"
font.pixelSize: 13
font.weight: Font.Medium
color: isRecording || mouseArea.containsMouse ? "#ffffff" : Data.ThemeManager.fgColor
Layout.alignment: Qt.AlignVCenter
}
}
// Click handling and hover detection
MouseArea {
id: mouseArea
anchors.fill: parent
hoverEnabled: true
onContainsMouseChanged: root.mouseChanged(containsMouse)
onClicked: {
if (isRecording) {
root.stopRecordingRequested()
} else {
root.recordingRequested()
}
}
}
}

View file

@ -0,0 +1,45 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import "root:/Data" as Data
// Simple theme toggle button with hover feedback
Rectangle {
id: root
property var shell: null
color: Qt.darker(Data.ThemeManager.bgColor, 1.15)
radius: 20
property bool containsMouse: themeMouseArea.containsMouse
property bool menuJustOpened: false
signal entered()
signal exited()
// Hover state management for parent components
onContainsMouseChanged: {
if (containsMouse) {
entered()
} else if (!menuJustOpened) {
exited()
}
}
MouseArea {
id: themeMouseArea
anchors.fill: parent
hoverEnabled: true
onClicked: {
Data.ThemeManager.toggleTheme()
}
}
// Theme toggle icon with color feedback
Label {
anchors.centerIn: parent
text: "contrast"
font.pixelSize: 24
font.family: "Material Symbols Outlined"
color: containsMouse ? Data.ThemeManager.accentColor : Data.ThemeManager.fgColor
}
}

View file

@ -0,0 +1,243 @@
import Quickshell.Io
import QtQuick
import QtQuick.Controls
import Qt5Compat.GraphicalEffects
import "root:/Data/" as Data
// User profile card
Rectangle {
id: root
required property var shell
property url avatarSource: Data.Settings.avatarSource
property string userName: "" // will be set by process output
property string userInfo: "" // will hold uptime string
property bool isActive: false
property bool isHovered: false // track hover state
radius: 20
width: 220
height: 80
// Dynamic color based on hover and active states
color: {
if (isActive) {
return isHovered ?
Qt.lighter(Data.ThemeManager.accentColor, 1.1) :
Qt.rgba(Data.ThemeManager.accentColor.r, Data.ThemeManager.accentColor.g, Data.ThemeManager.accentColor.b, 0.3)
} else {
return isHovered ?
Qt.lighter(Data.ThemeManager.accentColor, 1.2) :
Qt.lighter(Data.ThemeManager.bgColor, 1.15)
}
}
border.width: isActive ? 2 : 1
border.color: isActive ? Data.ThemeManager.accentColor : Qt.lighter(Data.ThemeManager.bgColor, 1.3)
Row {
anchors.fill: parent
anchors.margins: 14
spacing: 12
anchors.verticalCenter: parent.verticalCenter
// Avatar
Rectangle {
id: avatarCircle
width: 52
height: 52
radius: 20
clip: true
border.color: Data.ThemeManager.accentColor
border.width: 3
color: "transparent"
Image {
id: avatarImage
anchors.fill: parent
anchors.margins: 2
source: Data.Settings.avatarSource
fillMode: Image.PreserveAspectCrop
cache: false
visible: false // Hidden for masking
asynchronous: true
sourceSize.width: 48 // Memory optimization
sourceSize.height: 48
}
// Apply circular mask to avatar
OpacityMask {
anchors.fill: avatarImage
source: avatarImage
cached: true // Cache to reduce ShaderEffect issues
maskSource: Rectangle {
width: avatarImage.width
height: avatarImage.height
radius: 18 // Proportional to parent radius
visible: false
}
}
}
// User information text
Column {
spacing: 4
anchors.verticalCenter: parent.verticalCenter
width: parent.width - avatarCircle.width - gifContainer.width - parent.spacing * 2
Text {
width: parent.width
text: root.userName === "" ? "Loading..." : root.userName
font.family: "Roboto"
font.pixelSize: 16
font.bold: true
color: isHovered || root.isActive ? "#ffffff" : Data.ThemeManager.accentColor
elide: Text.ElideRight
maximumLineCount: 1
}
Text {
width: parent.width
text: root.userInfo === "" ? "Loading uptime..." : root.userInfo
font.family: "Roboto"
font.pixelSize: 11
font.bold: true
color: isHovered || root.isActive ? "#cccccc" : Qt.lighter(Data.ThemeManager.accentColor, 1.6)
elide: Text.ElideRight
maximumLineCount: 1
}
}
// Animated GIF with rounded corners
Rectangle {
id: gifContainer
width: 80
height: 80
radius: 12
color: "transparent"
anchors.verticalCenter: parent.verticalCenter
AnimatedImage {
id: animatedImage
source: "root:/Assets/UserProfile.gif"
anchors.fill: parent
fillMode: Image.PreserveAspectFit
playing: true
cache: false
speed: 1.0
asynchronous: true
}
// Apply rounded corner mask to GIF
layer.enabled: true
layer.effect: OpacityMask {
cached: true // Cache to reduce ShaderEffect issues
maskSource: Rectangle {
width: gifContainer.width
height: gifContainer.height
radius: gifContainer.radius
visible: false
}
}
}
}
MouseArea {
id: mouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onEntered: root.isHovered = true
onExited: root.isHovered = false
}
// Get current username
Process {
id: usernameProcess
running: true // Always run to get username
command: ["sh", "-c", "whoami"]
stdout: SplitParser {
splitMarker: "\n"
onRead: (data) => {
const line = data.trim();
if (line.length > 0) {
root.userName = line.charAt(0).toUpperCase() + line.slice(1);
}
}
}
}
// Get system uptime with parsing for readable format
Process {
id: uptimeProcess
running: false
command: ["sh", "-c", "uptime"] // Use basic uptime command
stdout: SplitParser {
splitMarker: "\n"
onRead: (data) => {
const line = data.trim();
if (line.length > 0) {
// Parse uptime output: " 10:30:00 up 1:23, 2 users, load average: 0.08, 0.02, 0.01"
const match = line.match(/up\s+(.+?),\s+\d+\s+user/);
if (match && match[1]) {
root.userInfo = "Up: " + match[1].trim();
} else {
// Fallback parsing for different uptime formats
const upIndex = line.indexOf("up ");
if (upIndex !== -1) {
const afterUp = line.substring(upIndex + 3);
const commaIndex = afterUp.indexOf(",");
if (commaIndex !== -1) {
root.userInfo = "Up: " + afterUp.substring(0, commaIndex).trim();
} else {
root.userInfo = "Up: " + afterUp.trim();
}
} else {
root.userInfo = "Uptime unknown";
}
}
} else {
root.userInfo = "Uptime unknown";
}
}
}
stderr: SplitParser {
splitMarker: "\n"
onRead: (data) => {
console.log("Uptime error:", data);
root.userInfo = "Uptime error";
}
}
}
// Update uptime every 5 minutes
Timer {
id: uptimeTimer
interval: 300000 // Update every 5 minutes
running: true // Always run the uptime timer
repeat: true
onTriggered: {
uptimeProcess.running = false
uptimeProcess.running = true
}
}
Component.onCompleted: {
uptimeProcess.running = true // Start uptime process on component load
}
Component.onDestruction: {
if (usernameProcess.running) {
usernameProcess.running = false
}
if (uptimeProcess.running) {
uptimeProcess.running = false
}
if (uptimeTimer.running) {
uptimeTimer.running = false
}
}
}

View file

@ -0,0 +1,354 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import "root:/Data" as Data
// Weather display widget
Rectangle {
id: root
required property var shell
color: Qt.darker(Data.ThemeManager.bgColor, 1.15)
radius: 20
property bool containsMouse: weatherMouseArea.containsMouse || (forecastPopup.visible && forecastPopup.containsMouse)
property bool menuJustOpened: false
signal entered()
signal exited()
// Hover state management for parent components
onContainsMouseChanged: {
if (containsMouse) {
entered()
} else if (!menuJustOpened && !forecastPopup.visible) {
exited()
}
}
// Maps WMO weather condition codes and text descriptions to Material Design icons
function getWeatherIcon(condition) {
if (!condition) return "light_mode"
const c = condition.toString()
// WMO weather interpretation codes to Material Design icons
const iconMap = {
"0": "light_mode", // Clear sky
"1": "light_mode", // Mainly clear
"2": "cloud", // Partly cloudy
"3": "cloud", // Overcast
"45": "foggy", // Fog
"48": "foggy", // Depositing rime fog
"51": "water_drop", // Light drizzle
"53": "water_drop", // Moderate drizzle
"55": "water_drop", // Dense drizzle
"61": "water_drop", // Slight rain
"63": "water_drop", // Moderate rain
"65": "water_drop", // Heavy rain
"71": "ac_unit", // Slight snow
"73": "ac_unit", // Moderate snow
"75": "ac_unit", // Heavy snow
"80": "water_drop", // Slight rain showers
"81": "water_drop", // Moderate rain showers
"82": "water_drop", // Violent rain showers
"95": "thunderstorm", // Thunderstorm
"96": "thunderstorm", // Thunderstorm with light hail
"99": "thunderstorm" // Thunderstorm with heavy hail
}
if (iconMap[c]) return iconMap[c]
// Fallback text matching for non-WMO weather APIs
const textMap = {
"clear sky": "light_mode",
"mainly clear": "light_mode",
"partly cloudy": "cloud",
"overcast": "cloud",
"fog": "foggy",
"drizzle": "water_drop",
"rain": "water_drop",
"snow": "ac_unit",
"thunderstorm": "thunderstorm"
}
const lower = condition.toLowerCase()
for (let key in textMap) {
if (lower.includes(key)) return textMap[key]
}
return "help" // Unknown condition fallback
}
// Hover trigger for forecast popup
MouseArea {
id: weatherMouseArea
anchors.fill: parent
hoverEnabled: true
onEntered: {
menuJustOpened = true
forecastPopup.open()
Qt.callLater(() => menuJustOpened = false)
}
onExited: {
if (!forecastPopup.containsMouse && !menuJustOpened) {
forecastPopup.close()
}
}
}
// Compact weather display (icon and temperature)
RowLayout {
id: weatherLayout
anchors.centerIn: parent
spacing: 8
ColumnLayout {
spacing: 2
Layout.alignment: Qt.AlignVCenter
// Weather condition icon
Label {
text: {
if (shell.weatherLoading) return "refresh"
if (!shell.weatherData) return "help"
return root.getWeatherIcon(shell.weatherData.currentCondition)
}
font.pixelSize: 28
font.family: "Material Symbols Outlined"
color: Data.ThemeManager.accentColor
Layout.alignment: Qt.AlignHCenter
}
// Current temperature
Label {
text: {
if (shell.weatherLoading) return "Loading..."
if (!shell.weatherData) return "No weather data"
return shell.weatherData.currentTemp
}
color: Data.ThemeManager.fgColor
font.family: "Roboto"
font.pixelSize: 20
font.bold: true
Layout.alignment: Qt.AlignHCenter
}
}
}
// Forecast popup
Popup {
id: forecastPopup
y: parent.height + 28
x: Math.min(0, parent.width - width)
width: 300
height: 226
padding: 12
background: Rectangle {
color: Qt.darker(Data.ThemeManager.bgColor, 1.15)
radius: 20
border.width: 1
border.color: Qt.lighter(Data.ThemeManager.bgColor, 1.3)
}
property bool containsMouse: forecastMouseArea.containsMouse
onVisibleChanged: {
if (visible) {
entered()
} else if (!weatherMouseArea.containsMouse && !menuJustOpened) {
exited()
}
}
// Hover area for popup persistence
MouseArea {
id: forecastMouseArea
anchors.fill: parent
hoverEnabled: true
onExited: {
if (!weatherMouseArea.containsMouse && !menuJustOpened) {
forecastPopup.close()
}
}
}
ColumnLayout {
id: forecastColumn
anchors.fill: parent
anchors.margins: 10
spacing: 8
// Current weather detailed view
RowLayout {
Layout.fillWidth: true
spacing: 12
// Large weather icon
Label {
text: shell.weatherData ? root.getWeatherIcon(shell.weatherData.currentCondition) : ""
font.pixelSize: 48
font.family: "Material Symbols Outlined"
color: Data.ThemeManager.accentColor
}
ColumnLayout {
Layout.fillWidth: true
spacing: 4
// Weather condition description
Label {
text: shell.weatherData ? shell.weatherData.currentCondition : ""
color: Data.ThemeManager.fgColor
font.family: "Roboto"
font.pixelSize: 14
font.bold: true
Layout.fillWidth: true
elide: Text.ElideRight
}
// Weather metrics: temperature, wind, direction
RowLayout {
spacing: 8
Layout.fillWidth: true
Layout.alignment: Qt.AlignVCenter | Qt.AlignLeft
// Temperature metric
RowLayout {
spacing: 4
Layout.alignment: Qt.AlignVCenter
Label {
text: "thermostat"
font.family: "Material Symbols Outlined"
font.pixelSize: 12
color: Data.ThemeManager.accentColor
}
Label {
text: shell.weatherData ? shell.weatherData.currentTemp : ""
color: Data.ThemeManager.fgColor
font.family: "Roboto"
font.pixelSize: 12
}
}
Rectangle {
width: 1
height: 12
color: Qt.lighter(Data.ThemeManager.bgColor, 1.3)
}
// Wind speed metric
RowLayout {
spacing: 4
Layout.alignment: Qt.AlignVCenter
Label {
text: "air"
font.family: "Material Symbols Outlined"
font.pixelSize: 12
color: Data.ThemeManager.accentColor
}
Label {
text: {
if (!shell.weatherData || !shell.weatherData.details) return ""
const windInfo = shell.weatherData.details.find(d => d.startsWith("Wind:"))
return windInfo ? windInfo.split(": ")[1] : ""
}
color: Data.ThemeManager.fgColor
font.family: "Roboto"
font.pixelSize: 12
}
}
Rectangle {
width: 1
height: 12
color: Qt.lighter(Data.ThemeManager.bgColor, 1.3)
}
// Wind direction metric
RowLayout {
spacing: 4
Layout.alignment: Qt.AlignVCenter
Label {
text: "explore"
font.family: "Material Symbols Outlined"
font.pixelSize: 12
color: Data.ThemeManager.accentColor
}
Label {
text: {
if (!shell.weatherData || !shell.weatherData.details) return ""
const dirInfo = shell.weatherData.details.find(d => d.startsWith("Direction:"))
return dirInfo ? dirInfo.split(": ")[1] : ""
}
color: Data.ThemeManager.fgColor
font.family: "Roboto"
font.pixelSize: 12
}
}
Item {
Layout.fillWidth: true
}
}
}
}
// Section separator
Rectangle {
height: 1
Layout.fillWidth: true
color: Qt.lighter(Data.ThemeManager.bgColor, 1.3)
}
Label {
text: "3-Day Forecast"
color: Data.ThemeManager.accentColor
font.family: "Roboto"
font.pixelSize: 12
font.bold: true
}
// Three-column forecast cards
Row {
spacing: 8
Layout.fillWidth: true
Repeater {
model: shell.weatherData ? shell.weatherData.forecast : []
delegate: Column {
width: (parent.width - 16) / 3
spacing: 2
// Day name
Label {
text: modelData.dayName
color: Data.ThemeManager.fgColor
font.family: "Roboto"
font.pixelSize: 10
font.bold: true
anchors.horizontalCenter: parent.horizontalCenter
}
// Weather icon
Label {
text: root.getWeatherIcon(modelData.condition)
font.pixelSize: 16
font.family: "Material Symbols Outlined"
color: Data.ThemeManager.accentColor
anchors.horizontalCenter: parent.horizontalCenter
}
// Temperature range
Label {
text: modelData.minTemp + "° - " + modelData.maxTemp + "°"
color: Data.ThemeManager.fgColor
font.family: "Roboto"
font.pixelSize: 10
anchors.horizontalCenter: parent.horizontalCenter
}
}
}
}
}
}
}

View file

@ -0,0 +1,147 @@
import QtQuick
import QtQuick.Controls
import "root:/Data" as Data
// Calendar tab content
Item {
id: calendarTab
required property var shell
property bool isActive: false
Column {
anchors.fill: parent
spacing: 12
Text {
text: "Calendar"
color: Data.ThemeManager.accentColor
font.pixelSize: 18
font.bold: true
font.family: "Roboto"
}
Rectangle {
width: parent.width
height: parent.height - parent.children[0].height - parent.spacing
color: Qt.lighter(Data.ThemeManager.bgColor, 1.2)
radius: 20
clip: true
Loader {
anchors.fill: parent
anchors.margins: 16
active: calendarTab.isActive
sourceComponent: active ? calendarComponent : null
}
}
}
Component {
id: calendarComponent
Item {
id: calendarRoot
property var shell: calendarTab.shell
readonly property date currentDate: new Date()
property int month: currentDate.getMonth()
property int year: currentDate.getFullYear()
readonly property int currentDay: currentDate.getDate()
Column {
anchors.fill: parent
anchors.margins: 8
spacing: 8
// Month/Year header
Text {
text: Qt.locale("en_US").monthName(calendarRoot.month) + " " + calendarRoot.year
color: Data.ThemeManager.accentColor
font.bold: true
width: parent.width
horizontalAlignment: Text.AlignHCenter
font.pixelSize: 16
height: 24
}
// Weekday headers (Monday-Sunday)
Grid {
columns: 7
rowSpacing: 2
columnSpacing: 0
width: parent.width
height: 18
Repeater {
model: ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]
delegate: Text {
text: modelData
color: Data.ThemeManager.fgColor
font.bold: true
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
width: parent.width / 7
height: 18
font.pixelSize: 11
}
}
}
// Calendar grid - single unified grid
Grid {
columns: 7
rowSpacing: 3
columnSpacing: 3
width: parent.width
property int firstDayOfMonth: new Date(calendarRoot.year, calendarRoot.month, 1).getDay()
property int daysInMonth: new Date(calendarRoot.year, calendarRoot.month + 1, 0).getDate()
property int startOffset: (firstDayOfMonth === 0) ? 6 : firstDayOfMonth - 1 // Convert Sunday=0 to Monday=0
property int prevMonthDays: new Date(calendarRoot.year, calendarRoot.month, 0).getDate()
// Single repeater for all 42 calendar cells (6 weeks × 7 days)
Repeater {
model: 42
delegate: Rectangle {
width: (parent.width - (parent.columnSpacing * 6)) / 7
height: 26
radius: 13
// Calculate which day this cell represents
readonly property int dayNumber: {
if (index < parent.startOffset) {
// Previous month
return parent.prevMonthDays - parent.startOffset + index + 1
} else if (index < parent.startOffset + parent.daysInMonth) {
// Current month
return index - parent.startOffset + 1
} else {
// Next month
return index - parent.startOffset - parent.daysInMonth + 1
}
}
readonly property bool isCurrentMonth: index >= parent.startOffset && index < (parent.startOffset + parent.daysInMonth)
readonly property bool isToday: isCurrentMonth && dayNumber === calendarRoot.currentDay &&
calendarRoot.month === calendarRoot.currentDate.getMonth() &&
calendarRoot.year === calendarRoot.currentDate.getFullYear()
color: isToday ? Data.ThemeManager.accentColor :
isCurrentMonth ? Data.ThemeManager.bgColor : Qt.darker(Data.ThemeManager.bgColor, 1.4)
Text {
text: dayNumber
anchors.centerIn: parent
color: isToday ? Data.ThemeManager.bgColor :
isCurrentMonth ? Data.ThemeManager.fgColor : Qt.darker(Data.ThemeManager.fgColor, 1.5)
font.bold: isToday
font.pixelSize: 12
font.family: "Roboto"
}
}
}
}
}
}
}
}

View file

@ -0,0 +1,110 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import "root:/Data" as Data
import "root:/Widgets/System" as System
// Clipboard tab content
Item {
id: clipboardTab
required property var shell
property bool isActive: false
Column {
anchors.fill: parent
spacing: 16
RowLayout {
width: parent.width
spacing: 16
Text {
text: "Clipboard History"
color: Data.ThemeManager.accentColor
font.pixelSize: 18
font.bold: true
font.family: "Roboto"
}
Item { Layout.fillWidth: true }
Rectangle {
width: clearClipText.implicitWidth + 16
height: 24
radius: 12
color: clearClipMouseArea.containsMouse ? Qt.rgba(Data.ThemeManager.accentColor.r, Data.ThemeManager.accentColor.g, Data.ThemeManager.accentColor.b, 0.2) : "transparent"
border.color: Data.ThemeManager.accentColor
border.width: 1
Text {
id: clearClipText
anchors.centerIn: parent
text: "Clear All"
color: Data.ThemeManager.accentColor
font.family: "Roboto"
font.pixelSize: 11
}
MouseArea {
id: clearClipMouseArea
anchors.fill: parent
hoverEnabled: true
onClicked: {
if (clipboardLoader.item && clipboardLoader.item.children[0]) {
let clipComponent = clipboardLoader.item.children[0]
if (clipComponent.clearClipboardHistory) {
clipComponent.clearClipboardHistory()
}
}
}
}
}
}
Rectangle {
width: parent.width
height: parent.height - parent.children[0].height - parent.spacing
color: Qt.lighter(Data.ThemeManager.bgColor, 1.2)
radius: 20
clip: true
Loader {
id: clipboardLoader
anchors.fill: parent
anchors.margins: 20
active: clipboardTab.isActive
sourceComponent: active ? clipboardHistoryComponent : null
onLoaded: {
if (item && item.children[0]) {
item.children[0].refreshClipboardHistory()
}
}
}
}
}
Component {
id: clipboardHistoryComponent
Item {
anchors.fill: parent
System.Cliphist {
id: cliphistComponent
anchors.fill: parent
shell: clipboardTab.shell
Component.onCompleted: {
for (let i = 0; i < children.length; i++) {
let child = children[i]
if (child.objectName === "contentColumn" || child.toString().includes("ColumnLayout")) {
if (child.children && child.children.length > 0) {
child.children[0].visible = false
}
}
}
}
}
}
}
}

View file

@ -0,0 +1,155 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import "root:/Data" as Data
import "root:/Widgets/System" as System
import "../components/widgets" as Widgets
import "../components/controls" as Controls
import "../components/system" as SystemComponents
// Main dashboard content (tab 0)
Item {
id: mainDashboard
// Properties from parent
required property var shell
required property bool isRecording
required property var triggerMouseArea
// Signals to forward
signal recordingRequested()
signal stopRecordingRequested()
signal systemActionRequested(string action)
signal performanceActionRequested(string action)
// Hover detection for auto-hide
property bool isHovered: {
const mouseStates = {
userProfileHovered: userProfile ? userProfile.isHovered : false,
weatherDisplayHovered: weatherDisplay ? weatherDisplay.containsMouse : false,
recordingButtonHovered: recordingButton ? recordingButton.isHovered : false,
controlsHovered: controls ? controls.containsMouse : false,
trayHovered: trayMouseArea ? trayMouseArea.containsMouse : false,
systemTrayHovered: systemTrayModule ? systemTrayModule.containsMouse : false,
trayMenuHovered: inlineTrayMenu ? inlineTrayMenu.containsMouse : false,
trayMenuVisible: inlineTrayMenu ? inlineTrayMenu.visible : false
}
return Object.values(mouseStates).some(state => state)
}
// Night Light overlay controller (invisible - manages screen overlay)
Widgets.NightLight {
id: nightLightController
shell: mainDashboard.shell
visible: false // This widget manages overlay windows, doesn't need to be visible
}
Column {
anchors.fill: parent
spacing: 28
// User profile row with weather
Row {
width: parent.width
spacing: 18
Widgets.UserProfile {
id: userProfile
width: parent.width - weatherDisplay.width - parent.spacing
height: 80
shell: mainDashboard.shell
}
Widgets.WeatherDisplay {
id: weatherDisplay
width: parent.width * 0.18
height: userProfile.height
shell: mainDashboard.shell
}
}
// Recording and system controls section
Column {
width: parent.width
spacing: 28
Widgets.RecordingButton {
id: recordingButton
width: parent.width
height: 48
shell: mainDashboard.shell
isRecording: mainDashboard.isRecording
onRecordingRequested: mainDashboard.recordingRequested()
onStopRecordingRequested: mainDashboard.stopRecordingRequested()
}
Controls.Controls {
id: controls
width: parent.width
isRecording: mainDashboard.isRecording
shell: mainDashboard.shell
onPerformanceActionRequested: function(action) { mainDashboard.performanceActionRequested(action) }
onSystemActionRequested: function(action) { mainDashboard.systemActionRequested(action) }
}
}
// System tray integration with menu
Column {
id: systemTraySection
width: parent.width
spacing: 8
property bool containsMouse: trayMouseArea.containsMouse || systemTrayModule.containsMouse
Rectangle {
id: trayBackground
width: parent.width
height: 40
radius: 20
color: Qt.darker(Data.ThemeManager.bgColor, 1.15)
property bool isActive: false
MouseArea {
id: trayMouseArea
anchors.fill: parent
anchors.margins: -10
hoverEnabled: true
propagateComposedEvents: true
preventStealing: false
onEntered: trayBackground.isActive = true
onExited: {
// Only deactivate if we're not hovering over tray menu or system tray module
if (!inlineTrayMenu.visible && !inlineTrayMenu.containsMouse) {
Qt.callLater(function() {
if (!systemTrayModule.containsMouse && !inlineTrayMenu.containsMouse && !inlineTrayMenu.visible) {
trayBackground.isActive = false
}
})
}
}
}
System.SystemTray {
id: systemTrayModule
anchors.centerIn: parent
shell: mainDashboard.shell
bar: parent
trayMenu: inlineTrayMenu
}
}
}
SystemComponents.TrayMenu {
id: inlineTrayMenu
parent: mainDashboard
width: parent.width
menu: null
systemTrayY: systemTraySection.y
systemTrayHeight: systemTraySection.height
z: 100 // High z-index to appear above other content
onHideRequested: trayBackground.isActive = false
}
}
}

View file

@ -0,0 +1,46 @@
import QtQuick
import "root:/Data" as Data
import "../components/media" as Media
// Music tab content
Item {
id: musicTab
required property var shell
property bool isActive: false
Column {
anchors.fill: parent
spacing: 16
Text {
text: "Music Player"
color: Data.ThemeManager.accentColor
font.pixelSize: 18
font.bold: true
font.family: "Roboto"
}
Rectangle {
width: parent.width
height: parent.height - parent.children[0].height - parent.spacing
color: Qt.lighter(Data.ThemeManager.bgColor, 1.2)
radius: 20
clip: true
Loader {
anchors.fill: parent
anchors.margins: 20
active: musicTab.isActive
sourceComponent: active ? musicPlayerComponent : null
}
}
}
Component {
id: musicPlayerComponent
Media.MusicPlayer {
shell: musicTab.shell
}
}
}

View file

@ -0,0 +1,106 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import "root:/Data" as Data
import "root:/Widgets/Notifications" as Notifications
// Notification tab content
Item {
id: notificationTab
required property var shell
property bool isActive: false
Column {
anchors.fill: parent
spacing: 16
RowLayout {
width: parent.width
spacing: 16
Text {
text: "Notification History"
color: Data.ThemeManager.accentColor
font.pixelSize: 18
font.bold: true
font.family: "Roboto"
}
Text {
text: "(" + (notificationTab.shell.notificationHistory ? notificationTab.shell.notificationHistory.count : 0) + ")"
color: Data.ThemeManager.fgColor
font.family: "Roboto"
font.pixelSize: 12
opacity: 0.7
Layout.alignment: Qt.AlignVCenter
}
Item { Layout.fillWidth: true }
Rectangle {
width: clearNotifText.implicitWidth + 16
height: 24
radius: 12
color: clearNotifMouseArea.containsMouse ? Qt.rgba(Data.ThemeManager.accentColor.r, Data.ThemeManager.accentColor.g, Data.ThemeManager.accentColor.b, 0.2) : "transparent"
border.color: Data.ThemeManager.accentColor
border.width: 1
Text {
id: clearNotifText
anchors.centerIn: parent
text: "Clear All"
color: Data.ThemeManager.accentColor
font.family: "Roboto"
font.pixelSize: 11
}
MouseArea {
id: clearNotifMouseArea
anchors.fill: parent
hoverEnabled: true
onClicked: notificationTab.shell.notificationHistory.clear()
}
}
}
Rectangle {
width: parent.width
height: parent.height - parent.children[0].height - parent.spacing
color: Qt.lighter(Data.ThemeManager.bgColor, 1.2)
radius: 20
clip: true
Loader {
anchors.fill: parent
anchors.margins: 20
active: notificationTab.isActive
sourceComponent: active ? notificationHistoryComponent : null
}
}
}
Component {
id: notificationHistoryComponent
Item {
anchors.fill: parent
Notifications.NotificationHistory {
anchors.fill: parent
shell: notificationTab.shell
clip: true
Component.onCompleted: {
for (let i = 0; i < children.length; i++) {
let child = children[i]
if (child.objectName === "contentColumn" || child.toString().includes("ColumnLayout")) {
if (child.children && child.children.length > 0) {
child.children[0].visible = false
}
}
}
}
}
}
}
}

View file

@ -0,0 +1,153 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import "root:/Data" as Data
import "../components/settings" as SettingsComponents
// Settings tab content with modular, collapsible categories
Item {
id: settingsTab
required property var shell
property bool isActive: false
// Track when any text input has focus for keyboard management
property bool anyTextInputFocused: {
try {
return (notificationSettings && notificationSettings.anyTextInputFocused) ||
(systemSettings && systemSettings.anyTextInputFocused) ||
(weatherSettings && weatherSettings.anyTextInputFocused)
} catch (e) {
return false
}
}
// Header
Text {
id: header
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
anchors.margins: 20
text: "Settings"
color: Data.ThemeManager.accentColor
font.pixelSize: 24
font.bold: true
font.family: "Roboto"
}
// Scrollable content
ScrollView {
anchors.top: header.bottom
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: parent.bottom
anchors.topMargin: 16
anchors.leftMargin: 20
anchors.rightMargin: 20
anchors.bottomMargin: 20
clip: true
contentWidth: width - 5 // Reserve space for scrollbar
ScrollBar.vertical.policy: ScrollBar.AsNeeded
ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
Column {
width: parent.width - 15 // Match contentWidth
spacing: 16
// VISUAL SETTINGS
// Appearance Category
SettingsComponents.SettingsCategory {
id: appearanceCategory
width: parent.width
title: "Appearance"
icon: "palette"
content: Component {
SettingsComponents.AppearanceSettings {
width: parent.width
}
}
}
// CORE SYSTEM SETTINGS
// System Category
SettingsComponents.SettingsCategory {
id: systemCategory
width: parent.width
title: "System"
icon: "settings"
content: Component {
SettingsComponents.SystemSettings {
id: systemSettings
width: parent.width
}
}
}
// Notifications Category
SettingsComponents.SettingsCategory {
id: notificationsCategory
width: parent.width
title: "Notifications"
icon: "notifications"
content: Component {
SettingsComponents.NotificationSettings {
id: notificationSettings
width: parent.width
}
}
}
// 🎵 MEDIA & EXTERNAL SERVICES
// Music Player Category
SettingsComponents.SettingsCategory {
id: musicPlayerCategory
width: parent.width
title: "Music Player"
icon: "music_note"
content: Component {
SettingsComponents.MusicPlayerSettings {
width: parent.width
}
}
}
// Weather Category
SettingsComponents.SettingsCategory {
id: weatherCategory
width: parent.width
title: "Weather"
icon: "wb_sunny"
content: Component {
SettingsComponents.WeatherSettings {
id: weatherSettings
width: parent.width
shell: settingsTab.shell
}
}
}
// ACCESSIBILITY & COMFORT
// Night Light Category
SettingsComponents.SettingsCategory {
id: nightLightCategory
width: parent.width
title: "Night Light"
icon: "dark_mode"
content: Component {
SettingsComponents.NightLightSettings {
width: parent.width
}
}
}
}
}
}

View file

@ -0,0 +1,45 @@
import QtQuick
import "root:/Data" as Data
import "../components/system" as SystemComponents
// Wallpaper tab content
Item {
id: wallpaperTab
property bool isActive: false
Column {
anchors.fill: parent
spacing: 16
Text {
text: "Wallpapers"
color: Data.ThemeManager.accentColor
font.pixelSize: 18
font.bold: true
font.family: "Roboto"
}
Rectangle {
width: parent.width
height: parent.height - parent.children[0].height - parent.spacing
color: Qt.lighter(Data.ThemeManager.bgColor, 1.2)
radius: 20
clip: true
Loader {
anchors.fill: parent
anchors.margins: 20
active: wallpaperTab.isActive
sourceComponent: active ? wallpaperSelectorComponent : null
}
}
}
Component {
id: wallpaperSelectorComponent
SystemComponents.WallpaperSelector {
isVisible: parent && parent.parent && parent.parent.visible
}
}
}

View file

@ -0,0 +1,262 @@
import QtQuick
import QtQuick.Controls
import Qt5Compat.GraphicalEffects
import "root:/Data" as Data
// Authentication area
Column {
id: authColumn
anchors.centerIn: parent
anchors.verticalCenterOffset: 80
spacing: 20
width: 300
required property bool isVisible
required property string errorMessage
required property bool isAuthenticating
required property bool authSuccess
required property string usernameText
signal passwordEntered(string password)
// Expose password field
readonly property alias passwordField: passwordField
// Subtle slide up animation (after main slide)
transform: Translate {
id: authTransform
y: isVisible ? 0 : 50
Behavior on y {
SequentialAnimation {
PauseAnimation { duration: 600 } // Wait for main slide and time
NumberAnimation {
duration: 600
easing.type: Easing.OutBack
}
}
}
}
opacity: isVisible ? 1.0 : 0.0
Behavior on opacity {
SequentialAnimation {
PauseAnimation { duration: 700 } // Wait for time to appear
NumberAnimation {
duration: 600
easing.type: Easing.OutCubic
}
}
}
// User avatar with circular masking
Rectangle {
id: avatarContainer
width: 100
height: 100
radius: 50
anchors.horizontalCenter: parent.horizontalCenter
color: "transparent"
border.color: Data.ThemeManager.accentColor
border.width: 4
clip: true
// Scale animation for avatar
scale: isVisible ? 1.0 : 0.0
Behavior on scale {
SequentialAnimation {
PauseAnimation { duration: 1000 } // Wait for auth area to appear
NumberAnimation {
duration: 400
easing.type: Easing.OutBack
}
}
}
Image {
id: avatarImage
anchors.fill: parent
anchors.margins: 4
source: Data.Settings.avatarSource
fillMode: Image.PreserveAspectCrop
cache: false
visible: false // Hidden for masking
asynchronous: true
sourceSize.width: 92
sourceSize.height: 92
}
// Apply circular mask to avatar
OpacityMask {
anchors.fill: avatarImage
source: avatarImage
maskSource: Rectangle {
width: avatarImage.width
height: avatarImage.height
radius: 46
visible: false
}
}
// Fallback icon if avatar fails to load
Text {
anchors.centerIn: parent
text: "person"
font.family: "Material Symbols Outlined"
font.pixelSize: 48
color: Data.ThemeManager.accentColor
visible: avatarImage.status !== Image.Ready
}
}
// Username display
Text {
id: usernameDisplay
anchors.horizontalCenter: parent.horizontalCenter
font.family: "FiraCode Nerd Font"
font.pixelSize: 18
color: Data.ThemeManager.primaryText
text: usernameText
}
// Password input field
Rectangle {
width: parent.width
height: 50
radius: 25
color: Data.ThemeManager.withOpacity(Data.ThemeManager.bgLighter, 0.4)
border.color: passwordField.activeFocus ? Data.ThemeManager.accentColor : Data.ThemeManager.withOpacity(Data.ThemeManager.border, 0.6)
border.width: 2
TextInput {
id: passwordField
anchors.fill: parent
anchors.margins: 15
echoMode: TextInput.Normal
font.family: "FiraCode Nerd Font"
font.pixelSize: 16
color: "transparent" // Hide the actual text
selectionColor: Data.ThemeManager.accentColor
selectByMouse: true
focus: isVisible
onAccepted: {
if (text.length > 0) {
passwordEntered(text)
}
}
// Password mask with better spaced dots
Row {
id: passwordDotsRow
anchors.verticalCenter: parent.verticalCenter
anchors.left: parent.left
spacing: 8
visible: passwordField.text.length > 0
property int previousLength: 0
Repeater {
id: passwordRepeater
model: passwordField.text.length
delegate: Rectangle {
id: passwordDot
width: 8
height: 8
radius: 4
color: Data.ThemeManager.primaryText
property bool isNewDot: index >= passwordDotsRow.previousLength
// Only animate new dots, existing ones stay visible
scale: isNewDot ? 0 : 1.0
opacity: isNewDot ? 0 : 1.0
ParallelAnimation {
running: passwordDot.isNewDot
NumberAnimation {
target: passwordDot
property: "scale"
from: 0
to: 1.0
duration: 200
easing.type: Easing.OutCubic
}
NumberAnimation {
target: passwordDot
property: "opacity"
from: 0
to: 1.0
duration: 150
easing.type: Easing.OutQuad
}
}
}
}
// Track length changes to identify new dots
Connections {
target: passwordField
function onTextChanged() {
// Update previous length after a short delay to allow new dots to be marked as new
Qt.callLater(function() {
passwordDotsRow.previousLength = passwordField.text.length
})
}
}
}
// Placeholder text
Text {
anchors.verticalCenter: parent.verticalCenter
anchors.left: parent.left
text: "Password"
font.family: passwordField.font.family
font.pixelSize: passwordField.font.pixelSize
color: Data.ThemeManager.withOpacity(Data.ThemeManager.secondaryText, 0.7)
visible: passwordField.text.length === 0 && !passwordField.activeFocus
}
}
}
// Error message
Text {
id: errorText
anchors.horizontalCenter: parent.horizontalCenter
font.family: "FiraCode Nerd Font"
font.pixelSize: 14
color: Data.ThemeManager.errorColor
text: errorMessage
visible: errorMessage !== ""
wrapMode: Text.WordWrap
width: parent.width
horizontalAlignment: Text.AlignHCenter
}
// Authentication status
Text {
id: statusText
anchors.horizontalCenter: parent.horizontalCenter
font.family: "FiraCode Nerd Font"
font.pixelSize: 14
color: authSuccess ? Data.ThemeManager.success : Data.ThemeManager.accentColorBright
text: {
if (authSuccess) return "Authentication successful!"
if (isAuthenticating) return "Authenticating..."
return ""
}
visible: isAuthenticating || authSuccess
}
// Public function to clear password
function clearPassword() {
passwordField.text = ""
passwordField.focus = true
}
// Public function to focus password field
function focusPassword() {
passwordField.focus = true
}
}

View file

@ -0,0 +1,299 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Qt5Compat.GraphicalEffects
import Quickshell
import Quickshell.Wayland
import Quickshell.Io
import "root:/Data" as Data
import "root:/Core" as Core
// Custom lockscreen
PanelWindow {
id: lockScreen
required property var shell
property bool isLocked: false
property bool isAuthenticated: false
property string errorMessage: ""
property int failedAttempts: 0
property bool isAuthenticating: false
property bool authSuccess: false
property string usernameText: "Enter Password"
// Animation state - controlled by timer for proper timing
property bool animateIn: false
// Full screen coverage
screen: Quickshell.primaryScreen || Quickshell.screens[0]
anchors.top: true
anchors.left: true
anchors.right: true
anchors.bottom: true
color: "transparent"
// Top layer to block everything
WlrLayershell.layer: WlrLayershell.Overlay
WlrLayershell.exclusiveZone: -1
WlrLayershell.keyboardFocus: WlrKeyboardFocus.Exclusive
WlrLayershell.namespace: "quickshell-lockscreen"
visible: isLocked
// Timer for slide-in animation - more reliable than Qt.callLater
Timer {
id: slideInTimer
interval: 100 // Short delay to ensure window is fully rendered
running: false
onTriggered: {
console.log("slideInTimer triggered, setting animateIn = true")
animateIn = true
}
}
// Timer for slide-out animation before hiding window
Timer {
id: slideOutTimer
interval: 1000 // Wait for slide animation to complete
running: false
onTriggered: {
isLocked = false
authArea.clearPassword()
errorMessage = ""
failedAttempts = 0
authSuccess = false
}
}
// Timer to show success message before unlocking
Timer {
id: successTimer
interval: 1200 // Show success message for 1.2 seconds
running: false
onTriggered: {
unlock()
}
}
// Reset animation state when window becomes invisible
onVisibleChanged: {
if (!visible) {
animateIn = false
authSuccess = false
slideInTimer.stop()
slideOutTimer.stop()
successTimer.stop()
}
}
// Background component
LockscreenBackground {
id: background
isVisible: lockScreen.visible
}
// Main lockscreen content with slide-from-top animation
Item {
id: mainContent
anchors.fill: parent
focus: true // Enable focus for keyboard handling
// Dramatic slide animation - starts off-screen, slides down when animateIn is true
transform: Translate {
id: mainTransform
y: lockScreen.animateIn ? 0 : -lockScreen.height
Behavior on y {
NumberAnimation {
duration: 800
easing.type: Easing.OutCubic
}
}
}
// Scale animation for extra drama
scale: lockScreen.animateIn ? 1.0 : 0.98
Behavior on scale {
NumberAnimation {
duration: 800
easing.type: Easing.OutCubic
}
}
// Keyboard shortcuts
Keys.onPressed: function(event) {
if (event.key === Qt.Key_Escape) {
authArea.clearPassword()
errorMessage = ""
} else if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) {
if (authArea.passwordField.text.length > 0) {
authenticate(authArea.passwordField.text)
}
}
}
// Authentication area component
AuthenticationArea {
id: authArea
isVisible: lockScreen.animateIn
errorMessage: lockScreen.errorMessage
isAuthenticating: lockScreen.isAuthenticating
authSuccess: lockScreen.authSuccess
usernameText: lockScreen.usernameText
onPasswordEntered: function(password) {
authenticate(password)
}
}
// Power buttons component
PowerButtons {
id: powerButtons
isVisible: lockScreen.animateIn
onRebootRequested: rebootProcess.running = true
onShutdownRequested: shutdownProcess.running = true
}
}
// Authentication process using proper PAM authentication
Process {
id: authProcess
property string password: ""
command: ["sh", "-c", "echo '" + password.replace(/'/g, "'\"'\"'") + "' | sudo -S -k true"]
running: false
onExited: function(exitCode) {
isAuthenticating = false
if (exitCode === 0) {
// Authentication successful
isAuthenticated = true
errorMessage = ""
authSuccess = true
// Show success message for a brief moment before unlocking
successTimer.start()
} else {
// Authentication failed
failedAttempts++
errorMessage = failedAttempts === 1 ? "Incorrect password" : `Incorrect password (${failedAttempts} attempts)`
authArea.clearPassword()
// Add delay for failed attempts
if (failedAttempts >= 3) {
lockoutTimer.start()
}
}
}
}
// Lockout timer for failed attempts
Timer {
id: lockoutTimer
interval: 30000 // 30 second lockout
onTriggered: {
errorMessage = ""
authArea.passwordField.enabled = true
authArea.focusPassword()
}
}
// Reboot process
Process {
id: rebootProcess
command: ["systemctl", "reboot"]
running: false
onExited: function(exitCode) { console.log("Reboot process completed with exit code:", exitCode) }
}
// Shutdown process
Process {
id: shutdownProcess
command: ["systemctl", "poweroff"]
running: false
onExited: function(exitCode) { console.log("Shutdown process completed with exit code:", exitCode) }
}
// Get current username
Process {
id: usernameProcess
command: ["whoami"]
running: lockScreen.isLocked
stdout: SplitParser {
onRead: function(data) {
const username = data.trim()
if (username) {
usernameText = username.charAt(0).toUpperCase() + username.slice(1)
}
}
}
}
// Public functions
function lock() {
console.log("Lockscreen.lock() called")
// Reset animation state FIRST, before making window visible
animateIn = false
isLocked = true
isAuthenticated = false
authSuccess = false
errorMessage = ""
failedAttempts = 0
authArea.clearPassword()
usernameProcess.running = true
// Trigger slide animation after a short delay
console.log("Starting slideInTimer")
slideInTimer.start()
}
function unlock() {
console.log("Lockscreen.unlock() called")
// Start slide-out animation first
animateIn = false
// Use timer for reliable timing before completely hiding
slideOutTimer.start()
}
function authenticate(password) {
if (isAuthenticating || password.length === 0) return
console.log("Authenticating...")
isAuthenticating = true
authSuccess = false
errorMessage = ""
// Use sudo authentication
authProcess.password = password
authProcess.running = true
}
// Focus management when locked state changes
onIsLockedChanged: {
console.log("isLocked changed to:", isLocked)
if (isLocked) {
mainContent.focus = true
authArea.focusPassword()
}
}
// Clean up processes on destruction
Component.onDestruction: {
if (authProcess.running) authProcess.running = false
if (rebootProcess.running) rebootProcess.running = false
if (shutdownProcess.running) shutdownProcess.running = false
if (usernameProcess.running) usernameProcess.running = false
if (lockoutTimer.running) lockoutTimer.running = false
if (slideInTimer.running) slideInTimer.running = false
if (slideOutTimer.running) slideOutTimer.running = false
if (successTimer.running) successTimer.running = false
}
}

View file

@ -0,0 +1,59 @@
import QtQuick
import Qt5Compat.GraphicalEffects
import "root:/Data" as Data
// Background with wallpaper
Rectangle {
id: backgroundContainer
anchors.fill: parent
color: Data.ThemeManager.bgColor
required property bool isVisible
// Fade-in animation for the whole background
opacity: isVisible ? 1.0 : 0.0
Behavior on opacity {
NumberAnimation {
duration: 500
easing.type: Easing.OutCubic
}
}
// Wallpaper background
Image {
id: wallpaperImage
anchors.fill: parent
source: Data.WallpaperManager.currentWallpaper ? "file://" + Data.WallpaperManager.currentWallpaper : ""
fillMode: Image.PreserveAspectCrop
asynchronous: true
cache: false
}
// Dark overlay
Rectangle {
anchors.fill: parent
color: Data.ThemeManager.withOpacity(Data.ThemeManager.bgColor, 0.8)
}
// Blur effect overlay
GaussianBlur {
anchors.fill: wallpaperImage
source: wallpaperImage
radius: 32
samples: 65
// Blur animation - starts less blurred and increases
Behavior on radius {
NumberAnimation {
duration: 1200
easing.type: Easing.OutCubic
}
}
Component.onCompleted: {
if (isVisible) {
radius = 32
}
}
}
}

View file

@ -0,0 +1,87 @@
import QtQuick
import "root:/Data" as Data
// Reboot and shutdown buttons positioned at bottom right
Row {
anchors.right: parent.right
anchors.bottom: parent.bottom
anchors.margins: 40
spacing: 16
required property bool isVisible
signal rebootRequested()
signal shutdownRequested()
// Fade in with delay
opacity: isVisible ? 1.0 : 0.0
Behavior on opacity {
SequentialAnimation {
PauseAnimation { duration: 900 } // Wait for most elements to appear
NumberAnimation {
duration: 500
easing.type: Easing.OutCubic
}
}
}
// Reboot button
Rectangle {
width: 45
height: 45
radius: 22
color: rebootMouseArea.containsMouse ? Data.ThemeManager.withOpacity(Data.ThemeManager.secondaryText, 0.3) : Data.ThemeManager.withOpacity(Data.ThemeManager.secondaryText, 0.2)
border.color: Data.ThemeManager.withOpacity(Data.ThemeManager.secondaryText, 0.6)
border.width: 1
Text {
anchors.centerIn: parent
text: "restart_alt"
font.family: "Material Symbols Outlined"
font.pixelSize: 20
color: Data.ThemeManager.secondaryText
}
MouseArea {
id: rebootMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: rebootRequested()
}
Behavior on color {
ColorAnimation { duration: 200 }
}
}
// Shutdown button
Rectangle {
width: 45
height: 45
radius: 22
color: shutdownMouseArea.containsMouse ? Data.ThemeManager.withOpacity(Data.ThemeManager.secondaryText, 0.3) : Data.ThemeManager.withOpacity(Data.ThemeManager.secondaryText, 0.2)
border.color: Data.ThemeManager.withOpacity(Data.ThemeManager.secondaryText, 0.6)
border.width: 1
Text {
anchors.centerIn: parent
text: "power_settings_new"
font.family: "Material Symbols Outlined"
font.pixelSize: 18
color: Data.ThemeManager.secondaryText
}
MouseArea {
id: shutdownMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: shutdownRequested()
}
Behavior on color {
ColorAnimation { duration: 200 }
}
}
}

View file

@ -0,0 +1,72 @@
import QtQuick
import "root:/Data" as Data
// Time and date display
Column {
id: timeColumn
anchors.top: parent.top
anchors.left: parent.left
anchors.topMargin: 60
anchors.leftMargin: 60
spacing: 16
required property bool isVisible
// Subtle slide-left animation (after main slide)
transform: Translate {
id: timeTransform
x: isVisible ? 0 : -100
Behavior on x {
SequentialAnimation {
PauseAnimation { duration: 400 } // Wait for main slide to be visible
NumberAnimation {
duration: 500
easing.type: Easing.OutQuart
}
}
}
}
opacity: isVisible ? 1.0 : 0.0
Behavior on opacity {
SequentialAnimation {
PauseAnimation { duration: 500 } // Wait for main slide
NumberAnimation {
duration: 500
easing.type: Easing.OutCubic
}
}
}
// Current time
Text {
id: timeText
font.family: "Roboto"
font.pixelSize: 84
font.weight: Font.ExtraLight
color: Data.ThemeManager.brightText
text: Qt.formatTime(new Date(), "hh:mm")
}
// Current date
Text {
id: dateText
font.family: "Roboto"
font.pixelSize: 28
font.weight: Font.Light
color: Data.ThemeManager.secondaryText
text: Qt.formatDate(new Date(), "dddd, MMMM d, yyyy")
}
// Time update timer
Timer {
id: timeTimer
interval: 1000
running: isVisible
repeat: true
onTriggered: {
timeText.text = Qt.formatTime(new Date(), "hh:mm")
dateText.text = Qt.formatDate(new Date(), "dddd, MMMM d, yyyy")
}
}
}

View file

@ -0,0 +1,367 @@
// System notification manager
import QtQuick
import QtQuick.Controls
import Quickshell.Services.Notifications
import "root:/Data" as Data
import "root:/Core" as Core
Item {
id: root
required property var shell
required property var notificationServer
// Dynamic height based on visible notifications
property int calculatedHeight: Math.min(notifications.length, maxNotifications) * 100 + 100 // Add 100px for bottom margin
// Simple array to store notifications with tracking
property var notifications: []
property int maxNotifications: 5
property var animatedNotificationIds: ({}) // Track which notifications have been animated
// Handle new notifications
Connections {
target: notificationServer
function onNotification(notification) {
if (!notification || !notification.id) return
// Filter empty notifications
if (!notification.appName && !notification.summary && !notification.body) {
return
}
// Filter ignored applications (case-insensitive) - same logic as NotificationService
var shouldIgnore = false
if (notification.appName && Data.Settings.ignoredApps && Data.Settings.ignoredApps.length > 0) {
for (var i = 0; i < Data.Settings.ignoredApps.length; i++) {
if (Data.Settings.ignoredApps[i].toLowerCase() === notification.appName.toLowerCase()) {
shouldIgnore = true
break
}
}
}
if (shouldIgnore) {
return // Don't display ignored notifications
}
// Create simple notification object
let newNotification = {
"id": notification.id,
"appName": notification.appName || "App",
"summary": notification.summary || "",
"body": notification.body || "",
"timestamp": Date.now(),
"shouldSlideOut": false,
"icon": notification.icon || notification.image || notification.appIcon || "",
"rawNotification": notification // Keep reference to original
}
// Add to beginning
notifications.unshift(newNotification)
// Trigger model update first to let new notification animate
notificationsChanged()
// Delay trimming to let new notification animate
if (notifications.length > maxNotifications) {
trimTimer.restart()
}
}
}
// Timer to delay trimming notifications (let new ones animate first)
Timer {
id: trimTimer
interval: 500 // Wait 500ms before trimming
running: false
repeat: false
onTriggered: {
if (notifications.length > maxNotifications) {
notifications = notifications.slice(0, maxNotifications)
notificationsChanged()
}
}
}
// Global timer to check for expired notifications
Timer {
id: cleanupTimer
interval: Math.min(500, Data.Settings.displayTime / 10) // Check every 500ms or 1/10th of display time, whichever is shorter
running: true
repeat: true
onTriggered: {
let currentTime = Date.now()
let hasExpiredNotifications = false
// Mark notifications older than displayTime setting for slide-out
for (let i = 0; i < notifications.length; i++) {
let notification = notifications[i]
let age = currentTime - notification.timestamp
if (age >= Data.Settings.displayTime && !notification.shouldSlideOut) {
notification.shouldSlideOut = true
hasExpiredNotifications = true
}
}
// Trigger update if any notifications were marked for slide-out
if (hasExpiredNotifications) {
notificationsChanged()
}
}
}
function removeNotification(notificationId) {
let initialLength = notifications.length
notifications = notifications.filter(function(n) { return n.id !== notificationId })
if (notifications.length !== initialLength) {
// Remove from animated tracking
delete animatedNotificationIds[notificationId]
notificationsChanged()
}
}
// Simple Column with Repeater
Column {
anchors.right: parent.right
anchors.left: parent.left
anchors.top: parent.top
anchors.bottom: parent.bottom
anchors.leftMargin: 40 // Create space on left for top-left corner
anchors.rightMargin: Data.Settings.borderWidth + 20 // Border width plus corner space
anchors.bottomMargin: 100 // Create more space at bottom for bottom corner
spacing: 0
Repeater {
model: notifications.length // Show all notifications, not just maxNotifications
delegate: Rectangle {
id: notificationRect
property var notification: index < notifications.length ? notifications[index] : null
width: 400
height: 100
color: Data.ThemeManager.bgColor
// Only bottom visible notification gets bottom-left radius
radius: 0
bottomLeftRadius: index === Math.min(notifications.length, maxNotifications) - 1 && index < maxNotifications ? 15 : 0
// Only show if within maxNotifications limit
visible: index < maxNotifications
// Animation state
property bool hasSlideIn: false
// Check for expiration and start slide-out if needed
onNotificationChanged: {
if (notification && notification.shouldSlideOut && !slideOutAnimation.running) {
slideOutAnimation.start()
}
}
// Start off-screen for new notifications
Component.onCompleted: {
if (notification) {
// Check if notification should slide out immediately
if (notification.shouldSlideOut) {
slideOutAnimation.start()
return
}
// Check if this notification is truly new (recently added)
let notificationAge = Date.now() - notification.timestamp
let shouldAnimate = !animatedNotificationIds[notification.id] && notificationAge < 1000 // Only animate if less than 1 second old
if (shouldAnimate) {
x = 420
opacity = 0
hasSlideIn = false
slideInAnimation.start()
// Mark as animated
animatedNotificationIds[notification.id] = true
} else {
x = 0
opacity = 1
hasSlideIn = true
// Mark as animated if not already
animatedNotificationIds[notification.id] = true
}
}
}
// Slide-in animation
ParallelAnimation {
id: slideInAnimation
NumberAnimation {
target: notificationRect
property: "x"
to: 0
duration: 300
easing.type: Easing.OutCubic
}
NumberAnimation {
target: notificationRect
property: "opacity"
to: 1
duration: 300
easing.type: Easing.OutCubic
}
onFinished: {
hasSlideIn = true
}
}
// Slide-out animation
ParallelAnimation {
id: slideOutAnimation
NumberAnimation {
target: notificationRect
property: "x"
to: 420
duration: 250
easing.type: Easing.InCubic
}
NumberAnimation {
target: notificationRect
property: "opacity"
to: 0
duration: 250
easing.type: Easing.InCubic
}
onFinished: {
if (notification) {
removeNotification(notification.id)
}
}
}
// Click to dismiss
MouseArea {
anchors.fill: parent
onClicked: slideOutAnimation.start()
}
// Content
Row {
anchors.fill: parent
anchors.margins: 15
spacing: 12
// App icon
Rectangle {
width: 32
height: 32
radius: 16
color: Qt.rgba(255, 255, 255, 0.1)
anchors.verticalCenter: parent.verticalCenter
// Application icon (if available)
Image {
id: appIcon
source: {
if (!notification || !notification.icon) return ""
let icon = notification.icon
// Apply same processing as tray system
if (icon.includes("?path=")) {
const [name, path] = icon.split("?path=");
const fileName = name.substring(name.lastIndexOf("/") + 1);
return `file://${path}/${fileName}`;
}
// Handle file paths properly
if (icon.startsWith('/')) {
return "file://" + icon
}
return icon
}
anchors.fill: parent
anchors.margins: 2
fillMode: Image.PreserveAspectFit
smooth: true
visible: source.toString() !== ""
onStatusChanged: {
// Icon status handling can be added here if needed
}
}
// Fallback text (first letter of app name)
Text {
anchors.centerIn: parent
text: notification && notification.appName ? notification.appName.charAt(0).toUpperCase() : "!"
color: Data.ThemeManager.accentColor
font.family: "Roboto"
font.pixelSize: 16
font.bold: true
visible: !appIcon.visible
}
}
// Content
Column {
anchors.verticalCenter: parent.verticalCenter
width: parent.width - 60
spacing: 4
Text {
text: notification ? notification.appName : ""
color: Data.ThemeManager.accentColor
font.family: "Roboto"
font.bold: true
font.pixelSize: 15
width: Math.min(parent.width, 250) // Earlier line break
elide: Text.ElideRight
}
Text {
text: notification ? notification.summary : ""
color: Data.ThemeManager.fgColor
font.family: "Roboto"
font.pixelSize: 14
width: Math.min(parent.width, 250) // Earlier line break
wrapMode: Text.Wrap
maximumLineCount: 2
elide: Text.ElideRight
visible: text.length > 0
}
Text {
text: notification ? notification.body : ""
color: Qt.lighter(Data.ThemeManager.fgColor, 1.3)
font.family: "Roboto"
font.pixelSize: 13
width: Math.min(parent.width, 250) // Earlier line break
wrapMode: Text.Wrap
maximumLineCount: 2
elide: Text.ElideRight
visible: text.length > 0
}
}
}
// Top corner for first notification
Core.Corners {
position: "bottomright"
size: 1.3
fillColor: Data.ThemeManager.bgColor
offsetX: -361
offsetY: -13
visible: index === 0 && index < maxNotifications
}
// Bottom corner for last visible notification
Core.Corners {
position: "bottomright"
size: 1.3
fillColor: Data.ThemeManager.bgColor
offsetX: 39
offsetY: 78
visible: index === Math.min(notifications.length, maxNotifications) - 1 && index < maxNotifications
}
}
}
}
}

View file

@ -0,0 +1,263 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Qt5Compat.GraphicalEffects
import "root:/Data" as Data
// Notification history viewer
Item {
id: root
implicitHeight: 400
required property var shell
property bool hovered: false
property real targetX: 0
ColumnLayout {
anchors.fill: parent
anchors.margins: 16
spacing: 12
// Header with title, count, and clear all button
RowLayout {
Layout.fillWidth: true
Layout.preferredHeight: 40
spacing: 8
Text {
text: "Notification History"
color: Data.ThemeManager.accentColor
font.pixelSize: 18
font.bold: true
font.family: "Roboto"
}
Text {
text: "(" + (shell.notificationHistory ? shell.notificationHistory.count : 0) + ")"
color: Data.ThemeManager.fgColor
font.family: "Roboto"
font.pixelSize: 12
opacity: 0.7
}
Item { Layout.fillWidth: true }
Rectangle {
visible: shell.notificationHistory && shell.notificationHistory.count > 0
width: clearText.implicitWidth + 16
height: 24
radius: 12
color: clearMouseArea.containsMouse ? Qt.rgba(Data.ThemeManager.accentColor.r, Data.ThemeManager.accentColor.g, Data.ThemeManager.accentColor.b, 0.2) : "transparent"
border.color: Data.ThemeManager.accentColor
border.width: 1
Text {
id: clearText
anchors.centerIn: parent
text: "Clear All"
color: Data.ThemeManager.accentColor
font.family: "Roboto"
font.pixelSize: 11
}
MouseArea {
id: clearMouseArea
anchors.fill: parent
hoverEnabled: true
onClicked: shell.notificationHistory.clear()
}
}
}
// Scrollable notification list
Item {
Layout.fillWidth: true
Layout.fillHeight: true
ScrollView {
id: scrollView
anchors.fill: parent
clip: true
ScrollBar.vertical: ScrollBar {
policy: ScrollBar.AsNeeded
interactive: true
visible: notificationListView.contentHeight > notificationListView.height
contentItem: Rectangle {
implicitWidth: 6
radius: width / 2
color: parent.pressed ? Data.ThemeManager.accentColor
: parent.hovered ? Qt.lighter(Data.ThemeManager.accentColor, 1.2)
: Qt.rgba(Data.ThemeManager.accentColor.r, Data.ThemeManager.accentColor.g, Data.ThemeManager.accentColor.b, 0.7)
}
}
ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
ListView {
id: notificationListView
model: shell.notificationHistory
spacing: 12
cacheBuffer: 50 // Memory optimization
reuseItems: true
boundsBehavior: Flickable.StopAtBounds
maximumFlickVelocity: 2500
flickDeceleration: 1500
clip: true
interactive: true
// Smooth scrolling behavior
property real targetY: contentY
Behavior on targetY {
NumberAnimation {
duration: 200
easing.type: Easing.OutQuad
}
}
onTargetYChanged: {
if (!moving && !dragging) {
contentY = targetY
}
}
delegate: Rectangle {
width: notificationListView.width
height: Math.max(80, contentLayout.implicitHeight + 24)
radius: 8
color: mouseArea.containsMouse ? Qt.darker(Data.ThemeManager.bgColor, 1.15) : Qt.darker(Data.ThemeManager.bgColor, 1.1)
border.color: Data.ThemeManager.accentColor
border.width: 1
// View optimization - only render visible items
visible: y + height > notificationListView.contentY - height &&
y < notificationListView.contentY + notificationListView.height + height
MouseArea {
id: mouseArea
anchors.fill: parent
hoverEnabled: true
}
// Main notification content layout
RowLayout {
id: contentLayout
anchors.fill: parent
anchors.margins: 12
spacing: 12
// App icon area
Item {
Layout.preferredWidth: 24
Layout.preferredHeight: 24
Layout.alignment: Qt.AlignTop
Image {
width: 24
height: 24
source: model.icon || ""
visible: source.toString() !== ""
}
}
// Notification text content
ColumnLayout {
Layout.fillWidth: true
Layout.fillHeight: true
spacing: 6
// App name and timestamp row
RowLayout {
Layout.fillWidth: true
Text {
Layout.fillWidth: true
text: model.appName || "Unknown"
color: Data.ThemeManager.accentColor
font.family: "Roboto"
font.pixelSize: 13
font.bold: true
}
Text {
text: Qt.formatDateTime(model.timestamp, "hh:mm")
color: Data.ThemeManager.fgColor
font.family: "Roboto"
font.pixelSize: 10
opacity: 0.7
}
}
// Notification summary
Text {
Layout.fillWidth: true
visible: model.summary && model.summary.length > 0
text: model.summary || ""
color: Data.ThemeManager.fgColor
font.family: "Roboto"
font.pixelSize: 13
font.bold: true
wrapMode: Text.WordWrap
lineHeight: 1.2
}
// Notification body text
Text {
Layout.fillWidth: true
visible: model.body && model.body.length > 0
text: model.body || ""
color: Data.ThemeManager.fgColor
font.family: "Roboto"
font.pixelSize: 12
opacity: 0.9
wrapMode: Text.WordWrap
maximumLineCount: 4
elide: Text.ElideRight
lineHeight: 1.2
}
}
}
// Individual delete button
Rectangle {
width: 24
height: 24
radius: 12
anchors.right: parent.right
anchors.top: parent.top
anchors.margins: 8
color: deleteArea.containsMouse ? Qt.rgba(255, 0, 0, 0.2) : "transparent"
border.color: deleteArea.containsMouse ? "#ff4444" : Data.ThemeManager.fgColor
border.width: 1
opacity: deleteArea.containsMouse ? 1 : 0.5
Text {
anchors.centerIn: parent
text: "×"
color: deleteArea.containsMouse ? "#ff4444" : Data.ThemeManager.fgColor
font.family: "Roboto"
font.pixelSize: 16
}
MouseArea {
id: deleteArea
anchors.fill: parent
hoverEnabled: true
onClicked: shell.notificationHistory.remove(model.index)
}
}
}
}
}
// Empty state message
Text {
anchors.centerIn: parent
visible: !notificationListView.count
text: "No notifications"
color: Data.ThemeManager.fgColor
font.family: "Roboto"
font.pixelSize: 14
opacity: 0.7
}
}
}
}

View file

@ -0,0 +1,143 @@
import QtQuick
import QtQuick.Layouts
import QtQuick.Controls
import QtQuick.Effects
import Quickshell
import Quickshell.Io
import "root:/Data" as Data
import "root:/Core/" as Core
import "./modules" as Modules
// Top panel wrapper with recording
Item {
id: topPanelRoot
required property var shell
visible: true
property bool isRecording: false
property var recordingProcess: null
property string lastError: ""
property bool wallpaperSelectorVisible: false
signal slideBarVisibilityChanged(bool visible)
function triggerTopPanel() {
panel.show()
}
// Auto-trigger panel
onVisibleChanged: {
if (visible) {
triggerTopPanel()
}
}
// Main panel instance
Modules.Panel {
id: panel
shell: topPanelRoot.shell
isRecording: topPanelRoot.isRecording
anchors.top: topPanelRoot.top
anchors.right: topPanelRoot.right
anchors.topMargin: 8
anchors.rightMargin: 8
onVisibleChanged: slideBarVisibilityChanged(visible)
onRecordingRequested: startRecording()
onStopRecordingRequested: {
stopRecording()
// Hide entire TopPanel after stop recording
if (topPanelRoot.parent && topPanelRoot.parent.hide) {
topPanelRoot.parent.hide()
}
}
onSystemActionRequested: function(action) {
performSystemAction(action)
// Hide entire TopPanel after system action
if (topPanelRoot.parent && topPanelRoot.parent.hide) {
topPanelRoot.parent.hide()
}
}
onPerformanceActionRequested: function(action) {
performPerformanceAction(action)
// Hide entire TopPanel after performance action
if (topPanelRoot.parent && topPanelRoot.parent.hide) {
topPanelRoot.parent.hide()
}
}
}
// Start screen recording
function startRecording() {
var currentDate = new Date()
var hours = String(currentDate.getHours()).padStart(2, '0')
var minutes = String(currentDate.getMinutes()).padStart(2, '0')
var day = String(currentDate.getDate()).padStart(2, '0')
var month = String(currentDate.getMonth() + 1).padStart(2, '0')
var year = currentDate.getFullYear()
var filename = hours + "-" + minutes + "-" + day + "-" + month + "-" + year + ".mp4"
var outputPath = Data.Settings.videoPath + filename
var command = "gpu-screen-recorder -w portal -f 60 -a default_output -o " + outputPath
var qmlString = 'import Quickshell.Io; Process { command: ["sh", "-c", "' + command + '"]; running: true }'
recordingProcess = Qt.createQmlObject(qmlString, topPanelRoot)
isRecording = true
}
// Stop recording with cleanup
function stopRecording() {
if (recordingProcess && isRecording) {
var stopQmlString = 'import Quickshell.Io; Process { command: ["sh", "-c", "pkill -SIGINT -f \'gpu-screen-recorder.*portal\'"]; running: true; onExited: function() { destroy() } }'
var stopProcess = Qt.createQmlObject(stopQmlString, topPanelRoot)
var cleanupTimer = Qt.createQmlObject('import QtQuick; Timer { interval: 3000; running: true; repeat: false }', topPanelRoot)
cleanupTimer.triggered.connect(function() {
if (recordingProcess) {
recordingProcess.running = false
recordingProcess.destroy()
recordingProcess = null
}
var forceKillQml = 'import Quickshell.Io; Process { command: ["sh", "-c", "pkill -9 -f \'gpu-screen-recorder.*portal\' 2>/dev/null || true"]; running: true; onExited: function() { destroy() } }'
var forceKillProcess = Qt.createQmlObject(forceKillQml, topPanelRoot)
cleanupTimer.destroy()
})
}
isRecording = false
}
// System action router (lock, reboot, shutdown)
function performSystemAction(action) {
switch(action) {
case "lock":
Core.ProcessManager.lock()
break
case "reboot":
Core.ProcessManager.reboot()
break
case "shutdown":
Core.ProcessManager.shutdown()
break
}
}
function performPerformanceAction(action) {
// Performance actions handled silently
}
// Clean up processes on destruction
Component.onDestruction: {
if (recordingProcess) {
recordingProcess.running = false
recordingProcess.destroy()
recordingProcess = null
}
}
}

View file

@ -0,0 +1,72 @@
import QtQuick
import QtQuick.Controls
import "root:/Data" as Data
// Calendar button for the top panel
Rectangle {
id: calendarButton
width: 40
height: 80
color: Qt.darker(Data.ThemeManager.bgColor, 1.15)
radius: 20
property bool containsMouse: calendarMouseArea.containsMouse
property bool calendarVisible: false
property var calendarPopup: null
property var shell: null // Shell reference from parent
signal entered()
signal exited()
onContainsMouseChanged: {
if (containsMouse) {
entered()
} else {
exited()
}
}
MouseArea {
id: calendarMouseArea
anchors.fill: parent
hoverEnabled: true
onClicked: {
toggleCalendar()
}
}
Label {
anchors.centerIn: parent
text: "calendar_month"
font.pixelSize: 24
font.family: "Material Symbols Outlined"
color: calendarButton.containsMouse || calendarButton.calendarVisible ? Data.ThemeManager.accentColor : Data.ThemeManager.fgColor
}
function toggleCalendar() {
if (!calendarPopup) {
var component = Qt.createComponent("root:/Widgets/Calendar/CalendarPopup.qml")
if (component.status === Component.Ready) {
calendarPopup = component.createObject(calendarButton.parent, {
"targetX": calendarButton.x + calendarButton.width + 10,
"shell": calendarButton.shell
})
} else if (component.status === Component.Error) {
console.log("Error loading calendar:", component.errorString())
return
}
}
if (calendarPopup) {
calendarVisible = !calendarVisible
calendarPopup.setClickMode(calendarVisible)
}
}
function hideCalendar() {
if (calendarPopup) {
calendarVisible = false
calendarPopup.setClickMode(false)
}
}
}

View file

@ -0,0 +1,103 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import "root:/Data" as Data
Row {
id: root
spacing: 16
visible: true
height: 80
required property bool isRecording
required property var shell
signal performanceActionRequested(string action)
signal systemActionRequested(string action)
signal mouseChanged(bool containsMouse)
// Add hover tracking property
property bool containsMouse: performanceSection.containsMouse || systemSection.containsMouse
onContainsMouseChanged: mouseChanged(containsMouse)
Rectangle {
id: performanceSection
width: (parent.width - parent.spacing) / 2
height: parent.height
radius: 20
color: Qt.darker(Data.ThemeManager.bgColor, 1.15)
visible: true
// Add hover tracking for performance section
property bool containsMouse: performanceMouseArea.containsMouse || performanceControls.containsMouse
MouseArea {
id: performanceMouseArea
anchors.fill: parent
hoverEnabled: true
propagateComposedEvents: true
onContainsMouseChanged: {
if (containsMouse) {
performanceSection.containsMouse = true
} else if (!performanceControls.containsMouse) {
performanceSection.containsMouse = false
}
}
}
PerformanceControls {
id: performanceControls
anchors.fill: parent
anchors.margins: 12
shell: root.shell
onPerformanceActionRequested: function(action) { root.performanceActionRequested(action) }
onMouseChanged: function(containsMouse) {
if (containsMouse) {
performanceSection.containsMouse = true
} else if (!performanceMouseArea.containsMouse) {
performanceSection.containsMouse = false
}
}
}
}
Rectangle {
id: systemSection
width: (parent.width - parent.spacing) / 2
height: parent.height
radius: 20
color: Qt.darker(Data.ThemeManager.bgColor, 1.15)
visible: true
// Add hover tracking for system section
property bool containsMouse: systemMouseArea.containsMouse || systemControls.containsMouse
MouseArea {
id: systemMouseArea
anchors.fill: parent
hoverEnabled: true
propagateComposedEvents: true
onContainsMouseChanged: {
if (containsMouse) {
systemSection.containsMouse = true
} else if (!systemControls.containsMouse) {
systemSection.containsMouse = false
}
}
}
SystemControls {
id: systemControls
anchors.fill: parent
anchors.margins: 12
shell: root.shell
onSystemActionRequested: function(action) { root.systemActionRequested(action) }
onMouseChanged: function(containsMouse) {
if (containsMouse) {
systemSection.containsMouse = true
} else if (!systemMouseArea.containsMouse) {
systemSection.containsMouse = false
}
}
}
}
}

View file

@ -0,0 +1,98 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import "root:/Data" as Data
Rectangle {
id: root
width: 42
color: Qt.darker(Data.ThemeManager.bgColor, 1.15)
radius: 12
z: 2 // Keep it above notification history
required property bool notificationHistoryVisible
required property bool clipboardHistoryVisible
required property var notificationHistory
signal notificationToggleRequested()
signal clipboardToggleRequested()
// Add containsMouse property for panel hover tracking
property bool containsMouse: notifButtonMouseArea.containsMouse || clipButtonMouseArea.containsMouse
// Ensure minimum height for buttons even when recording
property real buttonHeight: 38
height: buttonHeight * 2 + 4 // 4px spacing between buttons
Item {
anchors.fill: parent
anchors.margins: 2
// Top pill (Notifications)
Rectangle {
id: notificationPill
anchors {
top: parent.top
left: parent.left
right: parent.right
bottom: parent.verticalCenter
bottomMargin: 2 // Half of the spacing
}
radius: 12
color: notifButtonMouseArea.containsMouse || root.notificationHistoryVisible ?
Qt.rgba(Data.ThemeManager.accentColor.r, Data.ThemeManager.accentColor.g, Data.ThemeManager.accentColor.b, 0.2) :
Qt.rgba(Data.ThemeManager.fgColor.r, Data.ThemeManager.fgColor.g, Data.ThemeManager.fgColor.b, 0.05)
border.color: notifButtonMouseArea.containsMouse || root.notificationHistoryVisible ? Data.ThemeManager.accentColor : "transparent"
border.width: 1
MouseArea {
id: notifButtonMouseArea
anchors.fill: parent
hoverEnabled: true
onClicked: root.notificationToggleRequested()
}
Label {
anchors.centerIn: parent
text: "notifications"
font.family: "Material Symbols Outlined"
font.pixelSize: 16
color: notifButtonMouseArea.containsMouse || root.notificationHistoryVisible ?
Data.ThemeManager.accentColor : Data.ThemeManager.fgColor
}
}
// Bottom pill (Clipboard)
Rectangle {
id: clipboardPill
anchors {
top: parent.verticalCenter
left: parent.left
right: parent.right
bottom: parent.bottom
topMargin: 2 // Half of the spacing
}
radius: 12
color: clipButtonMouseArea.containsMouse || root.clipboardHistoryVisible ?
Qt.rgba(Data.ThemeManager.accentColor.r, Data.ThemeManager.accentColor.g, Data.ThemeManager.accentColor.b, 0.2) :
Qt.rgba(Data.ThemeManager.fgColor.r, Data.ThemeManager.fgColor.g, Data.ThemeManager.fgColor.b, 0.05)
border.color: clipButtonMouseArea.containsMouse || root.clipboardHistoryVisible ? Data.ThemeManager.accentColor : "transparent"
border.width: 1
MouseArea {
id: clipButtonMouseArea
anchors.fill: parent
hoverEnabled: true
onClicked: root.clipboardToggleRequested()
}
Label {
anchors.centerIn: parent
text: "content_paste"
font.family: "Material Symbols Outlined"
font.pixelSize: 16
color: clipButtonMouseArea.containsMouse || root.clipboardHistoryVisible ?
Data.ThemeManager.accentColor : Data.ThemeManager.fgColor
}
}
}
}

View file

@ -0,0 +1,704 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Qt5Compat.GraphicalEffects
import Quickshell
import Quickshell.Services.SystemTray
import "root:/Data" as Data
import "root:/Core" as Core
import "root:/Widgets/System" as System
import "root:/Widgets/Notifications" as Notifications
import "." as Modules
// Main tabbed panel
Item {
id: root
// Size calculation
width: mainContainer.implicitWidth + 18
height: mainContainer.implicitHeight + 18
required property var shell
property bool isShown: false
property int currentTab: 0 // 0=main, 1=calendar, 2=clipboard, 3=notifications, 4=wallpapers
property real bgOpacity: 0.0
property bool isRecording: false
property var tabIcons: ["widgets", "calendar_month", "content_paste", "notifications", "wallpaper"]
signal recordingRequested()
signal stopRecordingRequested()
signal systemActionRequested(string action)
signal performanceActionRequested(string action)
// Animation state management
visible: opacity > 0
opacity: 0
x: width
property var tabNames: ["Main", "Calendar", "Clipboard", "Notifications", "Wallpapers"]
Behavior on opacity {
NumberAnimation { duration: 200; easing.type: Easing.OutCubic }
}
Behavior on x {
NumberAnimation { duration: 200; easing.type: Easing.OutCubic }
}
// Background with bottom-only rounded corners
Rectangle {
anchors.fill: parent
color: Data.ThemeManager.bgColor
topLeftRadius: 0
topRightRadius: 0
bottomLeftRadius: 20
bottomRightRadius: 20
}
// Shadow effect preparation
Rectangle {
id: shadowSource
anchors.fill: mainContainer
color: "transparent"
visible: false
bottomLeftRadius: 20
bottomRightRadius: 20
}
DropShadow {
anchors.fill: shadowSource
horizontalOffset: 0
verticalOffset: 2
radius: 8.0
samples: 17
color: "#80000000"
source: shadowSource
z: 1
}
// Main container with tab-based content layout
Rectangle {
id: mainContainer
anchors.fill: parent
anchors.margins: 9
color: "transparent"
radius: 12
implicitWidth: 600 // Fixed width for consistency
implicitHeight: 360
Behavior on height {
NumberAnimation { duration: 200; easing.type: Easing.OutCubic }
}
MouseArea {
id: backgroundMouseArea
anchors.fill: parent
hoverEnabled: true
propagateComposedEvents: true
}
// Left sidebar with tab navigation
Item {
id: tabSidebar
width: 40
height: parent.height
anchors.left: parent.left
anchors.leftMargin: 9
anchors.top: parent.top
anchors.topMargin: 54
property bool containsMouse: sidebarMouseArea.containsMouse || tabColumn.containsMouse
// Tab button background
Rectangle {
width: 36
height: tabColumn.height + 8
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: parent.top
color: Qt.darker(Data.ThemeManager.bgColor, 1.05)
radius: 18
border.color: Qt.darker(Data.ThemeManager.bgColor, 1.2)
border.width: 1
}
MouseArea {
id: sidebarMouseArea
anchors.fill: parent
hoverEnabled: true
propagateComposedEvents: true
onEntered: hideTimer.stop()
onExited: {
if (!root.isHovered) {
hideTimer.restart()
}
}
}
// Tab icon buttons
Column {
id: tabColumn
spacing: 4
anchors.top: parent.top
anchors.topMargin: 4
anchors.horizontalCenter: parent.horizontalCenter
property bool containsMouse: {
for (let i = 0; i < tabRepeater.count; i++) {
let tab = tabRepeater.itemAt(i)
if (tab && tab.children[0] && tab.children[0].containsMouse) {
return true
}
}
return false
}
Repeater {
id: tabRepeater
model: 5
delegate: Rectangle {
width: 32
height: 32
radius: 16
color: currentTab === index ? Data.ThemeManager.accentColor : Qt.darker(Data.ThemeManager.bgColor, 1.15)
property bool isHovered: tabMouseArea.containsMouse
MouseArea {
id: tabMouseArea
anchors.fill: parent
hoverEnabled: true
onClicked: root.currentTab = index
onEntered: hideTimer.stop()
onExited: {
if (!root.isHovered) {
hideTimer.restart()
}
}
}
Text {
anchors.centerIn: parent
text: root.tabIcons[index]
font.family: "Material Symbols Outlined"
font.pixelSize: 16
color: currentTab === index ? Data.ThemeManager.bgColor :
parent.isHovered ? Data.ThemeManager.accentColor : Data.ThemeManager.fgColor
}
}
}
}
}
// Main content area (positioned right of tab sidebar)
Column {
id: mainColumn
width: parent.width - tabSidebar.width - 45
anchors.left: tabSidebar.right
anchors.leftMargin: 9
anchors.top: parent.top
anchors.margins: 18
spacing: 28
clip: true
// Tab 0: Main dashboard content
Column {
width: parent.width
spacing: 28
visible: root.currentTab === 0
// User profile row with theme toggle and weather
Row {
width: parent.width
spacing: 18
UserProfile {
id: userProfile
width: parent.width - themeToggle.width - weatherDisplay.width - (parent.spacing * 2)
height: 80
shell: root.shell
}
ThemeToggle {
id: themeToggle
width: 40
height: userProfile.height
}
WeatherDisplay {
id: weatherDisplay
width: parent.width * 0.18
height: userProfile.height
shell: root.shell
onEntered: hideTimer.stop()
onExited: hideTimer.restart()
visible: root.visible
enabled: visible
}
}
// Controls section
Row {
width: parent.width
spacing: 18
Column {
width: parent.width
spacing: 28
RecordingButton {
id: recordingButton
width: parent.width
height: 48
shell: root.shell
isRecording: root.isRecording
onRecordingRequested: root.recordingRequested()
onStopRecordingRequested: root.stopRecordingRequested()
}
Controls {
id: controls
width: parent.width
isRecording: root.isRecording
shell: root.shell
onPerformanceActionRequested: function(action) { root.performanceActionRequested(action) }
onSystemActionRequested: function(action) { root.systemActionRequested(action) }
onMouseChanged: function(containsMouse) {
if (containsMouse) {
hideTimer.stop()
} else if (!root.isHovered) {
hideTimer.restart()
}
}
}
}
}
// System tray section with inline menu
Column {
id: systemTraySection
width: parent.width
spacing: 8
property bool containsMouse: trayMouseArea.containsMouse || systemTrayModule.containsMouse
Rectangle {
id: trayBackground
width: parent.width
height: 40
radius: 20
color: Qt.darker(Data.ThemeManager.bgColor, 1.15)
property bool isActive: false
MouseArea {
id: trayMouseArea
anchors.fill: parent
anchors.margins: -10
hoverEnabled: true
propagateComposedEvents: true
preventStealing: false
onEntered: trayBackground.isActive = true
onExited: {
if (!inlineTrayMenu.visible) {
Qt.callLater(function() {
if (!systemTrayModule.containsMouse) {
trayBackground.isActive = false
}
})
}
}
}
System.SystemTray {
id: systemTrayModule
anchors.centerIn: parent
shell: root.shell
bar: parent
trayMenu: inlineTrayMenu
}
}
}
TrayMenu {
id: inlineTrayMenu
parent: mainContainer
width: parent.width
menu: null
systemTrayY: systemTraySection.y
systemTrayHeight: systemTraySection.height
onHideRequested: trayBackground.isActive = false
}
}
// Tab 1: Calendar content with lazy loading
Column {
width: parent.width
height: 310
visible: root.currentTab === 1
spacing: 16
Text {
text: "Calendar"
color: Data.ThemeManager.accentColor
font.pixelSize: 18
font.bold: true
font.family: "FiraCode Nerd Font"
}
Rectangle {
width: parent.width
height: parent.height - parent.children[0].height - parent.spacing
color: Qt.lighter(Data.ThemeManager.bgColor, 1.2)
radius: 20
clip: true
Loader {
anchors.fill: parent
anchors.margins: 20
active: visible && root.currentTab === 1
source: active ? "root:/Widgets/Calendar/Calendar.qml" : ""
onLoaded: {
if (item) {
item.shell = root.shell
}
}
}
}
}
// Tab 2: Clipboard history with clear button
Column {
width: parent.width
height: 310
visible: root.currentTab === 2
spacing: 16
RowLayout {
width: parent.width
spacing: 16
Text {
text: "Clipboard History"
color: Data.ThemeManager.accentColor
font.pixelSize: 18
font.bold: true
font.family: "Roboto"
}
Item { Layout.fillWidth: true }
Rectangle {
width: clearClipText.implicitWidth + 16
height: 24
radius: 12
color: clearClipMouseArea.containsMouse ? Qt.rgba(Data.ThemeManager.accentColor.r, Data.ThemeManager.accentColor.g, Data.ThemeManager.accentColor.b, 0.2) : "transparent"
border.color: Data.ThemeManager.accentColor
border.width: 1
Text {
id: clearClipText
anchors.centerIn: parent
text: "Clear All"
color: Data.ThemeManager.accentColor
font.family: "Roboto"
font.pixelSize: 11
}
MouseArea {
id: clearClipMouseArea
anchors.fill: parent
hoverEnabled: true
onClicked: {
// Navigate to clipboard component and call clear
let clipLoader = parent.parent.parent.children[1].children[0]
if (clipLoader && clipLoader.item && clipLoader.item.children[0]) {
let clipComponent = clipLoader.item.children[0]
if (clipComponent.clearClipboardHistory) {
clipComponent.clearClipboardHistory()
}
}
}
}
}
}
Rectangle {
width: parent.width
height: parent.height - parent.children[0].height - parent.spacing
color: Qt.lighter(Data.ThemeManager.bgColor, 1.2)
radius: 20
clip: true
Loader {
anchors.fill: parent
anchors.margins: 20
active: visible && root.currentTab === 2
sourceComponent: active ? clipboardHistoryComponent : null
onLoaded: {
if (item && item.children[0]) {
item.children[0].refreshClipboardHistory()
}
}
}
}
}
// Tab 3: Notification history with clear button
Column {
width: parent.width
height: 310
visible: root.currentTab === 3
spacing: 16
RowLayout {
width: parent.width
spacing: 16
Text {
text: "Notification History"
color: Data.ThemeManager.accentColor
font.pixelSize: 18
font.bold: true
font.family: "Roboto"
}
Text {
text: "(" + (root.shell.notificationHistory ? root.shell.notificationHistory.count : 0) + ")"
color: Data.ThemeManager.fgColor
font.family: "Roboto"
font.pixelSize: 12
opacity: 0.7
Layout.alignment: Qt.AlignVCenter
}
Item { Layout.fillWidth: true }
Rectangle {
width: clearNotifText.implicitWidth + 16
height: 24
radius: 12
color: clearNotifMouseArea.containsMouse ? Qt.rgba(Data.ThemeManager.accentColor.r, Data.ThemeManager.accentColor.g, Data.ThemeManager.accentColor.b, 0.2) : "transparent"
border.color: Data.ThemeManager.accentColor
border.width: 1
Text {
id: clearNotifText
anchors.centerIn: parent
text: "Clear All"
color: Data.ThemeManager.accentColor
font.family: "Roboto"
font.pixelSize: 11
}
MouseArea {
id: clearNotifMouseArea
anchors.fill: parent
hoverEnabled: true
onClicked: root.shell.notificationHistory.clear()
}
}
}
Rectangle {
width: parent.width
height: parent.height - parent.children[0].height - parent.spacing
color: Qt.lighter(Data.ThemeManager.bgColor, 1.2)
radius: 20
clip: true
Loader {
anchors.fill: parent
anchors.margins: 20
active: visible && root.currentTab === 3
sourceComponent: active ? notificationHistoryComponent : null
}
}
}
// Tab 4: Wallpaper selector
Column {
width: parent.width
height: 310
visible: root.currentTab === 4
spacing: 16
Text {
text: "Wallpapers"
color: Data.ThemeManager.accentColor
font.pixelSize: 18
font.bold: true
font.family: "Roboto"
}
Rectangle {
width: parent.width
height: parent.height - parent.children[0].height - parent.spacing
color: Qt.lighter(Data.ThemeManager.bgColor, 1.2)
radius: 20
clip: true
Loader {
anchors.fill: parent
anchors.margins: 20
active: visible && root.currentTab === 4
sourceComponent: active ? wallpaperSelectorComponent : null
}
}
}
}
}
// Lazy-loaded components for tab content
Component {
id: clipboardHistoryComponent
Item {
anchors.fill: parent
System.Cliphist {
id: cliphistComponent
anchors.fill: parent
shell: root.shell
// Hide built-in header (we provide our own)
Component.onCompleted: {
for (let i = 0; i < children.length; i++) {
let child = children[i]
if (child.objectName === "contentColumn" || child.toString().includes("ColumnLayout")) {
if (child.children && child.children.length > 0) {
child.children[0].visible = false
}
}
}
}
}
}
}
Component {
id: notificationHistoryComponent
Item {
anchors.fill: parent
Notifications.NotificationHistory {
anchors.fill: parent
shell: root.shell
clip: true
// Hide built-in header (we provide our own)
Component.onCompleted: {
for (let i = 0; i < children.length; i++) {
let child = children[i]
if (child.objectName === "contentColumn" || child.toString().includes("ColumnLayout")) {
if (child.children && child.children.length > 0) {
child.children[0].visible = false
}
}
}
}
}
}
}
Component {
id: wallpaperSelectorComponent
Modules.WallpaperSelector {
isVisible: parent && parent.parent && parent.parent.visible
}
}
// Complex hover state calculation for auto-hide behavior
property bool isHovered: {
const menuStates = {
inlineMenuActive: inlineTrayMenu.menuJustOpened || inlineTrayMenu.visible,
trayActive: trayBackground.isActive,
tabContentActive: currentTab !== 0
}
if (menuStates.inlineMenuActive || menuStates.trayActive || menuStates.tabContentActive) return true
const mouseStates = {
backgroundHovered: backgroundMouseArea.containsMouse,
recordingHovered: recordingButton.containsMouse,
controlsHovered: controls.containsMouse,
profileHovered: userProfile.isHovered,
themeToggleHovered: themeToggle.containsMouse,
systemTrayHovered: systemTraySection.containsMouse ||
trayMouseArea.containsMouse ||
systemTrayModule.containsMouse,
menuHovered: inlineTrayMenu.containsMouse,
weatherHovered: weatherDisplay.containsMouse,
tabSidebarHovered: tabSidebar.containsMouse,
mainContentHovered: mainColumn.children[0].visible && backgroundMouseArea.containsMouse
}
return Object.values(mouseStates).some(state => state)
}
// Auto-hide timer
Timer {
id: hideTimer
interval: 500
repeat: false
onTriggered: hide()
}
onIsHoveredChanged: {
if (isHovered) {
hideTimer.stop()
} else if (!inlineTrayMenu.visible && !trayBackground.isActive && !tabSidebar.containsMouse && !tabColumn.containsMouse) {
hideTimer.restart()
}
}
function show() {
if (isShown) return
isShown = true
hideTimer.stop()
opacity = 1
x = 0
}
function hide() {
if (!isShown || inlineTrayMenu.menuJustOpened || inlineTrayMenu.visible) return
// Only hide on main tab when nothing is hovered
if (currentTab === 0 && !isHovered) {
isShown = false
x = width
opacity = 0
// Hide parent TopPanel as well
if (parent && parent.parent && parent.parent.hide) {
parent.parent.hide()
}
}
}
Component.onCompleted: {
Qt.callLater(function() {
mainColumn.visible = true
})
}
// Border integration corners
Core.Corners {
id: topLeftCorner
position: "bottomright"
size: 1.3
fillColor: Data.ThemeManager.bgColor
offsetX: 0
offsetY: 0
}
Core.Corners {
id: topRightCorner
position: "bottomleft"
size: 1.3
fillColor: Data.ThemeManager.bgColor
offsetX: root.width
offsetY: 0
}
}

View file

@ -0,0 +1,127 @@
import QtQuick
import QtQuick.Controls
import Quickshell.Services.UPower
Column {
id: root
required property var shell
spacing: 8
signal performanceActionRequested(string action)
signal mouseChanged(bool containsMouse)
readonly property bool containsMouse: performanceButton.containsMouse ||
balancedButton.containsMouse ||
powerSaverButton.containsMouse
// Safe property access with fallbacks
readonly property bool upowerReady: typeof PowerProfiles !== 'undefined' && PowerProfiles
readonly property int currentProfile: upowerReady ? PowerProfiles.profile : 0
onContainsMouseChanged: root.mouseChanged(containsMouse)
opacity: visible ? 1 : 0
Behavior on opacity {
NumberAnimation {
duration: 300
easing.type: Easing.OutCubic
}
}
Row {
spacing: 8
width: parent.width
// Performance Profile Button
SystemButton {
id: performanceButton
width: (parent.width - parent.spacing * 2) / 3
height: 52
shell: root.shell
iconText: "speed"
isActive: root.upowerReady && (typeof PowerProfile !== 'undefined') ?
root.currentProfile === PowerProfile.Performance : false
onClicked: {
if (root.upowerReady && typeof PowerProfile !== 'undefined') {
PowerProfiles.profile = PowerProfile.Performance
root.performanceActionRequested("performance")
} else {
console.warn("PowerProfiles not available")
}
}
onMouseChanged: function(containsMouse) {
if (!containsMouse && !root.containsMouse) {
root.mouseChanged(false)
}
}
}
// Balanced Profile Button
SystemButton {
id: balancedButton
width: (parent.width - parent.spacing * 2) / 3
height: 52
shell: root.shell
iconText: "balance"
isActive: root.upowerReady && (typeof PowerProfile !== 'undefined') ?
root.currentProfile === PowerProfile.Balanced : false
onClicked: {
if (root.upowerReady && typeof PowerProfile !== 'undefined') {
PowerProfiles.profile = PowerProfile.Balanced
root.performanceActionRequested("balanced")
} else {
console.warn("PowerProfiles not available")
}
}
onMouseChanged: function(containsMouse) {
if (!containsMouse && !root.containsMouse) {
root.mouseChanged(false)
}
}
}
// Power Saver Profile Button
SystemButton {
id: powerSaverButton
width: (parent.width - parent.spacing * 2) / 3
height: 52
shell: root.shell
iconText: "battery_saver"
isActive: root.upowerReady && (typeof PowerProfile !== 'undefined') ?
root.currentProfile === PowerProfile.PowerSaver : false
onClicked: {
if (root.upowerReady && typeof PowerProfile !== 'undefined') {
PowerProfiles.profile = PowerProfile.PowerSaver
root.performanceActionRequested("powersaver")
} else {
console.warn("PowerProfiles not available")
}
}
onMouseChanged: function(containsMouse) {
if (!containsMouse && !root.containsMouse) {
root.mouseChanged(false)
}
}
}
}
// Optional: Add a small delay to ensure services are ready
Component.onCompleted: {
// Small delay to ensure UPower service is fully initialized
Qt.callLater(function() {
if (!root.upowerReady) {
console.warn("UPower service not ready - performance controls may not work correctly")
}
})
}
}

View file

@ -0,0 +1,61 @@
import QtQuick
import QtQuick.Layouts
import QtQuick.Controls
import "root:/Data" as Data
Rectangle {
id: root
required property var shell
required property bool isRecording
radius: 20
signal recordingRequested()
signal stopRecordingRequested()
signal mouseChanged(bool containsMouse)
// Gray by default, accent color on hover or when recording
color: isRecording ? Data.ThemeManager.accentColor :
(mouseArea.containsMouse ? Data.ThemeManager.accentColor : Qt.darker(Data.ThemeManager.bgColor, 1.15))
property bool isHovered: mouseArea.containsMouse
readonly property alias containsMouse: mouseArea.containsMouse
RowLayout {
anchors.centerIn: parent
spacing: 10
Text {
text: isRecording ? "stop_circle" : "radio_button_unchecked"
font.family: "Material Symbols Outlined"
font.pixelSize: 16
color: isRecording || mouseArea.containsMouse ? "#ffffff" : Data.ThemeManager.fgColor
Layout.alignment: Qt.AlignVCenter
}
Label {
text: isRecording ? "Stop Recording" : "Start Recording"
font.pixelSize: 13
font.weight: Font.Medium
color: isRecording || mouseArea.containsMouse ? "#ffffff" : Data.ThemeManager.fgColor
Layout.alignment: Qt.AlignVCenter
}
}
MouseArea {
id: mouseArea
anchors.fill: parent
hoverEnabled: true
onContainsMouseChanged: root.mouseChanged(containsMouse)
onClicked: {
if (isRecording) {
root.stopRecordingRequested()
} else {
root.recordingRequested()
}
}
}
}

View file

@ -0,0 +1,116 @@
import QtQuick
import QtQuick.Controls
import "root:/Data" as Data
Rectangle {
id: root
required property var shell
required property string iconText
property string labelText: ""
// Add active state property
property bool isActive: false
radius: 20
// Modified color logic to handle active state
color: {
if (isActive) {
return mouseArea.containsMouse ?
Qt.lighter(Data.ThemeManager.accentColor, 1.1) :
Qt.rgba(Data.ThemeManager.accentColor.r, Data.ThemeManager.accentColor.g, Data.ThemeManager.accentColor.b, 0.3)
} else {
return mouseArea.containsMouse ?
Qt.lighter(Data.ThemeManager.accentColor, 1.2) :
Qt.lighter(Data.ThemeManager.bgColor, 1.15)
}
}
border.width: isActive ? 2 : 1
border.color: isActive ? Data.ThemeManager.accentColor : Qt.lighter(Data.ThemeManager.bgColor, 1.3)
signal clicked()
signal mouseChanged(bool containsMouse)
property bool isHovered: mouseArea.containsMouse
readonly property alias containsMouse: mouseArea.containsMouse
Behavior on color {
ColorAnimation {
duration: 200
easing.type: Easing.OutCubic
}
}
Behavior on border.color {
ColorAnimation {
duration: 200
easing.type: Easing.OutCubic
}
}
scale: isHovered ? 1.05 : 1.0
Behavior on scale {
NumberAnimation {
duration: 200
easing.type: Easing.OutCubic
}
}
Column {
anchors.centerIn: parent
spacing: 2
Text {
text: root.iconText
font.family: "Material Symbols Outlined"
font.pixelSize: 16
anchors.horizontalCenter: parent.horizontalCenter
color: {
if (root.isActive) {
return root.isHovered ? "#ffffff" : Data.ThemeManager.accentColor
} else {
return root.isHovered ? "#ffffff" : Data.ThemeManager.accentColor
}
}
Behavior on color {
ColorAnimation {
duration: 200
easing.type: Easing.OutCubic
}
}
}
Label {
text: root.labelText
font.family: "Roboto"
font.pixelSize: 8
color: {
if (root.isActive) {
return root.isHovered ? "#ffffff" : Data.ThemeManager.accentColor
} else {
return root.isHovered ? "#ffffff" : Data.ThemeManager.accentColor
}
}
anchors.horizontalCenter: parent.horizontalCenter
font.weight: root.isActive ? Font.Bold : Font.Medium
visible: root.labelText !== ""
Behavior on color {
ColorAnimation {
duration: 200
easing.type: Easing.OutCubic
}
}
}
}
MouseArea {
id: mouseArea
anchors.fill: parent
hoverEnabled: true
onContainsMouseChanged: root.mouseChanged(containsMouse)
onClicked: root.clicked()
}
}

View file

@ -0,0 +1,92 @@
import QtQuick
import QtQuick.Layouts
import QtQuick.Controls
RowLayout {
id: root
required property var shell
spacing: 8
signal systemActionRequested(string action)
signal mouseChanged(bool containsMouse)
readonly property bool containsMouse: lockButton.containsMouse ||
rebootButton.containsMouse ||
shutdownButton.containsMouse
onContainsMouseChanged: root.mouseChanged(containsMouse)
opacity: visible ? 1 : 0
Behavior on opacity {
NumberAnimation {
duration: 300
easing.type: Easing.OutCubic
}
}
// Lock Button
SystemButton {
id: lockButton
Layout.fillHeight: true
Layout.fillWidth: true
shell: root.shell
iconText: "lock"
onClicked: {
console.log("Lock button clicked")
console.log("root.shell:", root.shell)
console.log("root.shell.lockscreen:", root.shell ? root.shell.lockscreen : "shell is null")
// Directly trigger custom lockscreen
if (root.shell && root.shell.lockscreen) {
console.log("Calling root.shell.lockscreen.lock()")
root.shell.lockscreen.lock()
} else {
console.log("Fallback to systemActionRequested")
// Fallback to system action for compatibility
root.systemActionRequested("lock")
}
}
onMouseChanged: function(containsMouse) {
if (!containsMouse && !root.containsMouse) {
root.mouseChanged(false)
}
}
}
// Reboot Button
SystemButton {
id: rebootButton
Layout.fillHeight: true
Layout.fillWidth: true
shell: root.shell
iconText: "restart_alt"
onClicked: root.systemActionRequested("reboot")
onMouseChanged: function(containsMouse) {
if (!containsMouse && !root.containsMouse) {
root.mouseChanged(false)
}
}
}
// Shutdown Button
SystemButton {
id: shutdownButton
Layout.fillHeight: true
Layout.fillWidth: true
shell: root.shell
iconText: "power_settings_new"
onClicked: root.systemActionRequested("shutdown")
onMouseChanged: function(containsMouse) {
if (!containsMouse && !root.containsMouse) {
root.mouseChanged(false)
}
}
}
}

View file

@ -0,0 +1,42 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import "root:/Data" as Data
Rectangle {
id: root
property var shell: null
color: Qt.darker(Data.ThemeManager.bgColor, 1.15)
radius: 20
property bool containsMouse: themeMouseArea.containsMouse
property bool menuJustOpened: false
signal entered()
signal exited()
onContainsMouseChanged: {
if (containsMouse) {
entered()
} else if (!menuJustOpened) {
exited()
}
}
MouseArea {
id: themeMouseArea
anchors.fill: parent
hoverEnabled: true
onClicked: {
Data.ThemeManager.toggleTheme()
}
}
Label {
anchors.centerIn: parent
text: "contrast"
font.pixelSize: 24
font.family: "Material Symbols Outlined"
color: containsMouse ? Data.ThemeManager.accentColor : Data.ThemeManager.fgColor
}
}

View file

@ -0,0 +1,48 @@
import QtQuick
Rectangle {
id: root
width: 360
height: 1
color: "red"
anchors.top: parent.top
signal triggered()
MouseArea {
id: mouseArea
anchors.fill: parent
hoverEnabled: true
property bool isHovered: containsMouse
onIsHoveredChanged: {
if (isHovered) {
showTimer.start()
hideTimer.stop()
} else {
hideTimer.start()
showTimer.stop()
}
}
onEntered: hideTimer.stop()
}
// Smooth show/hide timers
Timer {
id: showTimer
interval: 200
onTriggered: root.triggered()
}
Timer {
id: hideTimer
interval: 500
}
// Exposed properties and functions
readonly property alias containsMouse: mouseArea.containsMouse
function stopHideTimer() { hideTimer.stop() }
function startHideTimer() { hideTimer.start() }
}

View file

@ -0,0 +1,212 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import "root:/Data" as Data
Rectangle {
id: root
width: parent.width
height: visible ? calculatedHeight : 0
visible: false
enabled: visible
clip: true
color: Data.ThemeManager.bgColor
radius: 20
required property var menu
required property var systemTrayY
required property var systemTrayHeight
property bool containsMouse: trayMenuMouseArea.containsMouse
property bool menuJustOpened: false
property point triggerPoint: Qt.point(0, 0)
property Item originalParent
property int totalCount: opener.children ? opener.children.values.length : 0
signal hideRequested()
MouseArea {
id: trayMenuMouseArea
anchors.fill: parent
hoverEnabled: true
preventStealing: true
propagateComposedEvents: false
}
onVisibleChanged: {
if (visible) {
menuJustOpened = true
Qt.callLater(function() {
menuJustOpened = false
})
}
}
function toggle() {
visible = !visible
if (visible) {
menuJustOpened = true
Qt.callLater(function() {
menuJustOpened = false
})
}
}
function show(point, parentItem) {
visible = true
menuJustOpened = true
Qt.callLater(function() {
menuJustOpened = false
})
}
function hide() {
visible = false
menuJustOpened = false
hideRequested()
}
y: {
var preferredY = systemTrayY + systemTrayHeight + 10
var availableSpace = parent.height - preferredY - 20
if (calculatedHeight > availableSpace) {
return systemTrayY - height - 10
}
return preferredY
}
Behavior on height {
NumberAnimation { duration: 200; easing.type: Easing.OutCubic }
}
property int calculatedHeight: {
if (totalCount === 0) return 40
var separatorCount = 0
var regularItemCount = 0
if (opener.children && opener.children.values) {
for (var i = 0; i < opener.children.values.length; i++) {
if (opener.children.values[i].isSeparator) {
separatorCount++
} else {
regularItemCount++
}
}
}
var separatorHeight = separatorCount * 12
var regularItemRows = Math.ceil(regularItemCount / 2)
var regularItemHeight = regularItemRows * 32
return Math.max(80, 35 + separatorHeight + regularItemHeight + 40)
}
QsMenuOpener {
id: opener
menu: root.menu
}
GridView {
id: gridView
anchors.fill: parent
anchors.margins: 20
cellWidth: width / 2
cellHeight: 32
interactive: false
flow: GridView.FlowLeftToRight
layoutDirection: Qt.LeftToRight
model: ScriptModel {
values: opener.children ? [...opener.children.values] : []
}
delegate: Item {
id: entry
required property var modelData
required property int index
width: gridView.cellWidth - 4
height: modelData.isSeparator ? 12 : 30
Rectangle {
anchors.fill: parent
anchors.leftMargin: 8
anchors.rightMargin: 8
anchors.topMargin: 4
anchors.bottomMargin: 4
visible: modelData.isSeparator
color: "transparent"
Rectangle {
anchors.centerIn: parent
width: parent.width * 0.8
height: 1
color: Qt.darker(Data.ThemeManager.accentColor, 1.5)
opacity: 0.6
}
}
Rectangle {
id: itemBackground
anchors.fill: parent
anchors.margins: 2
visible: !modelData.isSeparator
color: "transparent"
radius: 6
RowLayout {
anchors.fill: parent
anchors.leftMargin: 8
anchors.rightMargin: 8
spacing: 6
Image {
Layout.preferredWidth: 16
Layout.preferredHeight: 16
source: modelData?.icon ?? ""
visible: (modelData?.icon ?? "") !== ""
fillMode: Image.PreserveAspectFit
}
Text {
Layout.fillWidth: true
color: mouseArea.containsMouse ? Data.ThemeManager.accentColor : Data.ThemeManager.fgColor
text: modelData?.text ?? ""
font.pixelSize: 11
font.family: "FiraCode Nerd Font"
verticalAlignment: Text.AlignVCenter
elide: Text.ElideRight
maximumLineCount: 1
}
}
MouseArea {
id: mouseArea
anchors.fill: parent
hoverEnabled: true
enabled: (modelData?.enabled ?? true) && root.visible && !modelData.isSeparator
onEntered: itemBackground.color = Qt.rgba(Data.ThemeManager.accentColor.r, Data.ThemeManager.accentColor.g, Data.ThemeManager.accentColor.b, 0.15)
onExited: itemBackground.color = "transparent"
onClicked: {
modelData.triggered()
root.hide()
}
}
}
}
}
Item {
anchors.centerIn: gridView
visible: gridView.count === 0
Label {
anchors.centerIn: parent
text: "No tray items available"
color: Qt.darker(Data.ThemeManager.fgColor, 2)
font.pixelSize: 14
font.family: "FiraCode Nerd Font"
}
}
}

View file

@ -0,0 +1,234 @@
import Quickshell.Io
import QtQuick
import QtQuick.Controls
import Qt5Compat.GraphicalEffects
import "root:/Data/" as Data
Rectangle {
id: root
required property var shell
property url avatarSource: Data.Settings.avatarSource
property string userName: "" // will be set by process output
property string userInfo: "" // will hold uptime string
property bool isActive: false
property bool isHovered: false // track hover state
radius: 20
width: 220
height: 80
color: {
if (isActive) {
return isHovered ?
Qt.lighter(Data.ThemeManager.accentColor, 1.1) :
Qt.rgba(Data.ThemeManager.accentColor.r, Data.ThemeManager.accentColor.g, Data.ThemeManager.accentColor.b, 0.3)
} else {
return isHovered ?
Qt.lighter(Data.ThemeManager.accentColor, 1.2) :
Qt.lighter(Data.ThemeManager.bgColor, 1.15)
}
}
border.width: isActive ? 2 : 1
border.color: isActive ? Data.ThemeManager.accentColor : Qt.lighter(Data.ThemeManager.bgColor, 1.3)
Row {
anchors.fill: parent
anchors.margins: 14
spacing: 12
anchors.verticalCenter: parent.verticalCenter
Rectangle {
id: avatarCircle
width: 52
height: 52
radius: 20
clip: true
border.color: Data.ThemeManager.accentColor
border.width: 3
color: "transparent"
Image {
id: avatarImage
anchors.fill: parent
anchors.margins: 2
source: Data.Settings.avatarSource
fillMode: Image.PreserveAspectCrop
cache: false
visible: false // Hide the original image
asynchronous: true
sourceSize.width: 48 // Limit image resolution to save memory
sourceSize.height: 48
}
OpacityMask {
anchors.fill: avatarImage
source: avatarImage
maskSource: Rectangle {
width: avatarImage.width
height: avatarImage.height
radius: 18 // Proportionally smaller than parent (48/52 * 20 18)
visible: false
}
}
}
Column {
spacing: 4
anchors.verticalCenter: parent.verticalCenter
width: parent.width - avatarCircle.width - gifContainer.width - parent.spacing * 2
Text {
width: parent.width
text: root.userName === "" ? "Loading..." : root.userName
font.family: "Roboto"
font.pixelSize: 16
font.bold: true
color: isHovered || root.isActive ? Data.ThemeManager.bgColor : Data.ThemeManager.accentColor
elide: Text.ElideRight
maximumLineCount: 1
}
Text {
width: parent.width
text: root.userInfo === "" ? "Loading uptime..." : root.userInfo
font.family: "Roboto"
font.pixelSize: 11
font.bold: true
color: isHovered || root.isActive ? "#cccccc" : Qt.lighter(Data.ThemeManager.accentColor, 1.6)
elide: Text.ElideRight
maximumLineCount: 1
}
}
Rectangle {
id: gifContainer
width: 80
height: 80
radius: 12
color: "transparent"
anchors.verticalCenter: parent.verticalCenter
AnimatedImage {
id: animatedImage
source: "root:/Assets/UserProfile.gif"
anchors.fill: parent
fillMode: Image.PreserveAspectFit
playing: true
cache: false
speed: 1.0
asynchronous: true
}
// Always enable layer effects for rounded corners
layer.enabled: true
layer.effect: OpacityMask {
maskSource: Rectangle {
width: gifContainer.width
height: gifContainer.height
radius: gifContainer.radius
visible: false
}
}
}
}
MouseArea {
id: mouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onEntered: root.isHovered = true
onExited: root.isHovered = false
}
Process {
id: usernameProcess
running: true // Always run to get username
command: ["sh", "-c", "whoami"]
stdout: SplitParser {
splitMarker: "\n"
onRead: (data) => {
const line = data.trim();
if (line.length > 0) {
root.userName = line.charAt(0).toUpperCase() + line.slice(1);
}
}
}
}
Process {
id: uptimeProcess
running: false
command: ["sh", "-c", "uptime"] // Use basic uptime command
stdout: SplitParser {
splitMarker: "\n"
onRead: (data) => {
const line = data.trim();
if (line.length > 0) {
// Parse traditional uptime output: " 10:30:00 up 1:23, 2 users, load average: 0.08, 0.02, 0.01"
const match = line.match(/up\s+(.+?),\s+\d+\s+user/);
if (match && match[1]) {
root.userInfo = "Up: " + match[1].trim();
} else {
// Fallback parsing
const upIndex = line.indexOf("up ");
if (upIndex !== -1) {
const afterUp = line.substring(upIndex + 3);
const commaIndex = afterUp.indexOf(",");
if (commaIndex !== -1) {
root.userInfo = "Up: " + afterUp.substring(0, commaIndex).trim();
} else {
root.userInfo = "Up: " + afterUp.trim();
}
} else {
root.userInfo = "Uptime unknown";
}
}
} else {
root.userInfo = "Uptime unknown";
}
}
}
stderr: SplitParser {
splitMarker: "\n"
onRead: (data) => {
console.log("Uptime error:", data);
root.userInfo = "Uptime error";
}
}
}
Timer {
id: uptimeTimer
interval: 300000 // Update every 5 minutes
running: true // Always run the uptime timer
repeat: true
onTriggered: {
uptimeProcess.running = false
uptimeProcess.running = true
}
}
Component.onCompleted: {
uptimeProcess.running = true // Start uptime process on component load
}
Component.onDestruction: {
if (usernameProcess.running) {
usernameProcess.running = false
}
if (uptimeProcess.running) {
uptimeProcess.running = false
}
if (uptimeTimer.running) {
uptimeTimer.running = false
}
}
}

View file

@ -0,0 +1,145 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Qt5Compat.GraphicalEffects
import "root:/Data" as Data
Item {
id: root
property bool isVisible: false
signal visibilityChanged(bool visible)
// Use all space provided by parent
anchors.fill: parent
visible: isVisible
enabled: visible
clip: true
property bool containsMouse: wallpaperSelectorMouseArea.containsMouse || scrollView.containsMouse
property bool menuJustOpened: false
onContainsMouseChanged: {
if (containsMouse) {
hideTimer.stop()
} else if (!menuJustOpened && !isVisible) {
hideTimer.restart()
}
}
onVisibleChanged: {
if (visible) {
menuJustOpened = true
hideTimer.stop()
Qt.callLater(function() {
menuJustOpened = false
})
}
}
MouseArea {
id: wallpaperSelectorMouseArea
anchors.fill: parent
hoverEnabled: true
preventStealing: false
propagateComposedEvents: true
}
// Wallpaper grid - use all available space
ScrollView {
id: scrollView
anchors.fill: parent
clip: true
property bool containsMouse: gridMouseArea.containsMouse
MouseArea {
id: gridMouseArea
anchors.fill: parent
hoverEnabled: true
propagateComposedEvents: true
}
GridView {
id: wallpaperGrid
anchors.fill: parent
cellWidth: parent.width / 2 - 8 // 2 columns with spacing for bigger previews
cellHeight: cellWidth * 0.6 // Aspect ratio for wallpapers
model: Data.WallpaperManager.wallpaperList
cacheBuffer: 0 // Disable cache buffer to save massive memory
leftMargin: 4
rightMargin: 4
topMargin: 4
bottomMargin: 4
delegate: Item {
width: wallpaperGrid.cellWidth - 8
height: wallpaperGrid.cellHeight - 8
Rectangle {
id: wallpaperItem
anchors.fill: parent
anchors.margins: 4
color: Qt.darker(Data.ThemeManager.bgColor, 1.2)
radius: 20
Behavior on scale {
NumberAnimation { duration: 150; easing.type: Easing.OutCubic }
}
Image {
id: wallpaperImage
anchors.fill: parent
anchors.margins: 4
source: modelData
fillMode: Image.PreserveAspectCrop
asynchronous: true
cache: false // Disable caching to save massive memory
sourceSize.width: Math.min(width, 150) // Further reduced from 200 to 150
sourceSize.height: Math.min(height, 90) // Further reduced from 120 to 90
// Only load when item is visible in viewport
visible: parent.parent.y >= wallpaperGrid.contentY - parent.parent.height &&
parent.parent.y <= wallpaperGrid.contentY + wallpaperGrid.height
// Disable layer effects to save memory
// layer.enabled: true
// layer.effect: OpacityMask {
// maskSource: Rectangle {
// width: wallpaperImage.width
// height: wallpaperImage.height
// radius: 18 // Slightly smaller than parent to account for margins
// }
// }
}
// Selected indicator
Rectangle {
visible: modelData === Data.WallpaperManager.currentWallpaper
anchors.fill: parent
radius: parent.radius
color: "transparent"
border.color: Data.ThemeManager.accentColor
border.width: 2
}
MouseArea {
anchors.fill: parent
hoverEnabled: true
onEntered: wallpaperItem.scale = 1.05
onExited: wallpaperItem.scale = 1.0
onClicked: {
Data.WallpaperManager.setWallpaper(modelData)
// Removed the close behavior - stays in wallpaper tab
}
}
}
}
}
}
Component.onCompleted: {
// Use lazy loading to only load wallpapers when this component is actually used
Data.WallpaperManager.ensureWallpapersLoaded()
}
}

View file

@ -0,0 +1,323 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import "root:/Data" as Data
Rectangle {
id: root
required property var shell
color: Qt.darker(Data.ThemeManager.bgColor, 1.15)
radius: 20
property bool containsMouse: weatherMouseArea.containsMouse || (forecastPopup.visible && forecastPopup.containsMouse)
property bool menuJustOpened: false
signal entered()
signal exited()
onContainsMouseChanged: {
if (containsMouse) {
entered()
} else if (!menuJustOpened && !forecastPopup.visible) {
exited()
}
}
function getWeatherIcon(condition) {
if (!condition) return "light_mode"
const c = condition.toString()
const iconMap = {
"0": "light_mode",
"1": "light_mode",
"2": "cloud",
"3": "cloud",
"45": "foggy",
"48": "foggy",
"51": "water_drop",
"53": "water_drop",
"55": "water_drop",
"61": "water_drop",
"63": "water_drop",
"65": "water_drop",
"71": "ac_unit",
"73": "ac_unit",
"75": "ac_unit",
"80": "water_drop",
"81": "water_drop",
"82": "water_drop",
"95": "thunderstorm",
"96": "thunderstorm",
"99": "thunderstorm"
}
if (iconMap[c]) return iconMap[c]
const textMap = {
"clear sky": "light_mode",
"mainly clear": "light_mode",
"partly cloudy": "cloud",
"overcast": "cloud",
"fog": "foggy",
"drizzle": "water_drop",
"rain": "water_drop",
"snow": "ac_unit",
"thunderstorm": "thunderstorm"
}
const lower = condition.toLowerCase()
for (let key in textMap) {
if (lower.includes(key)) return textMap[key]
}
return "help"
}
MouseArea {
id: weatherMouseArea
anchors.fill: parent
hoverEnabled: true
onEntered: {
menuJustOpened = true
forecastPopup.open()
Qt.callLater(() => menuJustOpened = false)
}
onExited: {
if (!forecastPopup.containsMouse && !menuJustOpened) {
forecastPopup.close()
}
}
}
RowLayout {
id: weatherLayout
anchors.centerIn: parent
spacing: 8
ColumnLayout {
spacing: 2
Layout.alignment: Qt.AlignVCenter
Label {
text: {
if (shell.weatherLoading) return "refresh"
if (!shell.weatherData) return "help"
return root.getWeatherIcon(shell.weatherData.currentCondition)
}
font.pixelSize: 28
font.family: "Material Symbols Outlined"
color: Data.ThemeManager.accentColor
Layout.alignment: Qt.AlignHCenter
}
Label {
text: {
if (shell.weatherLoading) return "Loading..."
if (!shell.weatherData) return "No weather data"
return shell.weatherData.currentTemp
}
color: Data.ThemeManager.fgColor
font.pixelSize: 20
font.bold: true
Layout.alignment: Qt.AlignHCenter
}
}
}
Popup {
id: forecastPopup
y: parent.height + 28
x: Math.min(0, parent.width - width)
width: 300
height: 226
padding: 12
background: Rectangle {
color: Qt.darker(Data.ThemeManager.bgColor, 1.15)
radius: 20
border.width: 1
border.color: Qt.lighter(Data.ThemeManager.bgColor, 1.3)
}
property bool containsMouse: forecastMouseArea.containsMouse
onVisibleChanged: {
if (visible) {
entered()
} else if (!weatherMouseArea.containsMouse && !menuJustOpened) {
exited()
}
}
MouseArea {
id: forecastMouseArea
anchors.fill: parent
hoverEnabled: true
onExited: {
if (!weatherMouseArea.containsMouse && !menuJustOpened) {
forecastPopup.close()
}
}
}
ColumnLayout {
id: forecastColumn
anchors.fill: parent
anchors.margins: 10
spacing: 8
RowLayout {
Layout.fillWidth: true
spacing: 12
Label {
text: shell.weatherData ? root.getWeatherIcon(shell.weatherData.currentCondition) : ""
font.pixelSize: 48
font.family: "Material Symbols Outlined"
color: Data.ThemeManager.accentColor
}
ColumnLayout {
Layout.fillWidth: true
spacing: 4
Label {
text: shell.weatherData ? shell.weatherData.currentCondition : ""
color: Data.ThemeManager.fgColor
font.pixelSize: 14
font.bold: true
Layout.fillWidth: true
elide: Text.ElideRight
}
RowLayout {
spacing: 8
Layout.fillWidth: true
Layout.alignment: Qt.AlignVCenter | Qt.AlignLeft
RowLayout {
spacing: 4
Layout.alignment: Qt.AlignVCenter
Label {
text: "thermostat"
font.family: "Material Symbols Outlined"
font.pixelSize: 12
color: Data.ThemeManager.accentColor
}
Label {
text: shell.weatherData ? shell.weatherData.currentTemp : ""
color: Data.ThemeManager.fgColor
font.pixelSize: 12
}
}
Rectangle {
width: 1
height: 12
color: Qt.lighter(Data.ThemeManager.bgColor, 1.3)
}
RowLayout {
spacing: 4
Layout.alignment: Qt.AlignVCenter
Label {
text: "air"
font.family: "Material Symbols Outlined"
font.pixelSize: 12
color: Data.ThemeManager.accentColor
}
Label {
text: {
if (!shell.weatherData || !shell.weatherData.details) return ""
const windInfo = shell.weatherData.details.find(d => d.startsWith("Wind:"))
return windInfo ? windInfo.split(": ")[1] : ""
}
color: Data.ThemeManager.fgColor
font.pixelSize: 12
}
}
Rectangle {
width: 1
height: 12
color: Qt.lighter(Data.ThemeManager.bgColor, 1.3)
}
RowLayout {
spacing: 4
Layout.alignment: Qt.AlignVCenter
Label {
text: "explore"
font.family: "Material Symbols Outlined"
font.pixelSize: 12
color: Data.ThemeManager.accentColor
}
Label {
text: {
if (!shell.weatherData || !shell.weatherData.details) return ""
const dirInfo = shell.weatherData.details.find(d => d.startsWith("Direction:"))
return dirInfo ? dirInfo.split(": ")[1] : ""
}
color: Data.ThemeManager.fgColor
font.pixelSize: 12
}
}
Item {
Layout.fillWidth: true
}
}
}
}
Rectangle {
height: 1
Layout.fillWidth: true
color: Qt.lighter(Data.ThemeManager.bgColor, 1.3)
}
Label {
text: "3-Day Forecast"
color: Data.ThemeManager.accentColor
font.pixelSize: 12
font.bold: true
}
Row {
spacing: 8
Layout.fillWidth: true
Repeater {
model: shell.weatherData ? shell.weatherData.forecast : []
delegate: Column {
width: (parent.width - 16) / 3
spacing: 2
Label {
text: modelData.dayName
color: Data.ThemeManager.fgColor
font.pixelSize: 10
font.bold: true
anchors.horizontalCenter: parent.horizontalCenter
}
Label {
text: root.getWeatherIcon(modelData.condition)
font.pixelSize: 16
font.family: "Material Symbols Outlined"
color: Data.ThemeManager.accentColor
anchors.horizontalCenter: parent.horizontalCenter
}
Label {
text: modelData.minTemp + "° - " + modelData.maxTemp + "°"
color: Data.ThemeManager.fgColor
font.pixelSize: 10
anchors.horizontalCenter: parent.horizontalCenter
}
}
}
}
}
}
}

View file

@ -0,0 +1,515 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import QtQuick.Shapes
import Quickshell
import Quickshell.Io
import "root:/Data" as Data
// Clipboard history manager with cliphist integration
Item {
id: root
required property var shell
property string selectedWidget: "cliphist"
property bool isVisible: false
property real bgOpacity: 0.0
transformOrigin: Item.Center
function show() { showAnimation.start() }
function hide() { hideAnimation.start() }
function toggle() { isVisible ? hide() : show() }
// Smooth show/hide animations
ParallelAnimation {
id: showAnimation
PropertyAction { target: root; property: "isVisible"; value: true }
PropertyAnimation { target: root; property: "opacity"; from: 0.0; to: 1.0; duration: 200; easing.type: Easing.OutCubic }
PropertyAnimation { target: root; property: "scale"; from: 0.9; to: 1.0; duration: 200; easing.type: Easing.OutCubic }
}
ParallelAnimation {
id: hideAnimation
PropertyAnimation { target: root; property: "opacity"; to: 0.0; duration: 150; easing.type: Easing.InCubic }
PropertyAnimation { target: root; property: "scale"; to: 0.95; duration: 150; easing.type: Easing.InCubic }
PropertyAction { target: root; property: "isVisible"; value: false }
}
ColumnLayout {
id: contentColumn
anchors.fill: parent
spacing: 12
// Header
RowLayout {
Layout.fillWidth: true
Layout.preferredHeight: 30
Label {
text: "Clipboard History"
font.pixelSize: 16
font.weight: Font.Medium
color: Data.ThemeManager.fgColor
Layout.fillWidth: true
}
Button {
id: clearButton
text: "Clear"
implicitWidth: 60
implicitHeight: 25
background: Rectangle {
radius: 12
color: parent.down ? Qt.darker(Data.ThemeManager.accentColor, 1.2) :
parent.hovered ? Qt.lighter(Data.ThemeManager.accentColor, 1.1) :
Qt.rgba(Data.ThemeManager.accentColor.r, Data.ThemeManager.accentColor.g, Data.ThemeManager.accentColor.b, 0.8)
}
contentItem: Label {
text: parent.text
font.pixelSize: 11
color: Data.ThemeManager.fgColor
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
}
onClicked: {
clearClipboardHistory()
clickScale.target = clearButton
clickScale.start()
}
}
}
// Scrollable clipboard history list
Item {
Layout.fillWidth: true
Layout.fillHeight: true
ScrollView {
id: scrollView
anchors.fill: parent
clip: true
ScrollBar.vertical: ScrollBar {
policy: ScrollBar.AsNeeded
interactive: true
visible: cliphistList.contentHeight > cliphistList.height
contentItem: Rectangle {
implicitWidth: 6
radius: width / 2
color: parent.pressed ? Data.ThemeManager.accentColor
: parent.hovered ? Qt.lighter(Data.ThemeManager.accentColor, 1.2)
: Qt.rgba(Data.ThemeManager.accentColor.r, Data.ThemeManager.accentColor.g, Data.ThemeManager.accentColor.b, 0.7)
}
}
ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
ListView {
id: cliphistList
model: cliphistModel
spacing: 6
cacheBuffer: 50 // Memory optimization
reuseItems: true
boundsBehavior: Flickable.StopAtBounds
maximumFlickVelocity: 2500
flickDeceleration: 1500
// Smooth scrolling behavior
property real targetY: contentY
Behavior on targetY {
NumberAnimation {
duration: 200
easing.type: Easing.OutQuad
}
}
onTargetYChanged: {
if (!moving && !dragging) {
contentY = targetY
}
}
delegate: Rectangle {
width: cliphistList.width
height: Math.max(50, contentText.contentHeight + 20)
radius: 8
color: mouseArea.containsMouse ? Qt.darker(Data.ThemeManager.bgColor, 1.15) : Qt.darker(Data.ThemeManager.bgColor, 1.1)
border.color: Data.ThemeManager.accentColor
border.width: 1
// View optimization - only render visible items
visible: y + height > cliphistList.contentY - height &&
y < cliphistList.contentY + cliphistList.height + height
RowLayout {
anchors.fill: parent
anchors.margins: 10
spacing: 10
// Content type icon
Label {
text: model.type === "image" ? "🖼️" : model.type === "url" ? "🔗" : "📝"
font.pixelSize: 16
Layout.alignment: Qt.AlignTop
}
ColumnLayout {
Layout.fillWidth: true
Layout.fillHeight: true
spacing: 4
Label {
id: contentText
text: model.type === "image" ? "[Image Data]" :
(model.content.length > 100 ? model.content.substring(0, 100) + "..." : model.content)
font.pixelSize: 12
color: Data.ThemeManager.fgColor
wrapMode: Text.WordWrap
Layout.fillWidth: true
Layout.fillHeight: true
elide: Text.ElideRight
maximumLineCount: 4
}
RowLayout {
Layout.fillWidth: true
Item { Layout.fillWidth: true }
Label {
text: model.type === "image" ? "Image" : (model.content.length + " chars")
font.pixelSize: 10
color: Qt.darker(Data.ThemeManager.fgColor, 1.5)
}
}
}
}
MouseArea {
id: mouseArea
anchors.fill: parent
hoverEnabled: true
onClicked: mouse => {
if (mouse.button === Qt.LeftButton) {
copyToClipboard(model.id, model.type)
clickScale.target = parent
clickScale.start()
}
}
}
}
// Empty state message
Label {
anchors.centerIn: parent
text: "No clipboard history\nCopy something to get started"
font.pixelSize: 14
color: Qt.darker(Data.ThemeManager.fgColor, 1.5)
horizontalAlignment: Text.AlignHCenter
visible: cliphistList.count === 0
opacity: 0.7
}
}
}
}
}
// Click feedback animation
NumberAnimation {
id: clickScale
property Item target
properties: "scale"
from: 0.95
to: 1.0
duration: 150
easing.type: Easing.OutCubic
}
ListModel { id: cliphistModel }
property var currentEntries: []
// Main cliphist process for fetching clipboard history
Process {
id: cliphistProcess
command: ["cliphist", "list"]
running: false
property var tempEntries: []
onRunningChanged: {
if (running) {
tempEntries = []
} else {
// Process completed, apply smart diff update
updateModelIfChanged(tempEntries)
}
}
stdout: SplitParser {
onRead: data => {
try {
const line = data.toString().trim()
// Skip empty lines and error messages
if (line === "" || line.includes("ERROR") || line.includes("WARN") ||
line.includes("error:") || line.includes("warning:")) {
return
}
// Parse cliphist output format: ID + spaces + content
const match = line.match(/^(\d+)\s+(.+)$/)
if (match) {
const id = match[1]
const content = match[2]
cliphistProcess.tempEntries.push({
id: id,
content: content,
type: detectContentType(content)
})
} else {
console.log("Failed to parse line:", line)
}
} catch (e) {
console.error("Error parsing cliphist line:", e)
}
}
}
}
// Clear entire clipboard history
Process {
id: clearCliphistProcess
command: ["cliphist", "wipe"]
running: false
onRunningChanged: {
if (!running) {
cliphistModel.clear()
currentEntries = []
console.log("Clipboard history cleared")
}
}
stderr: SplitParser {
onRead: data => {
console.error("Clear clipboard error:", data.toString())
}
}
}
// Delete specific clipboard entry
Process {
id: deleteEntryProcess
property string entryId: ""
command: ["cliphist", "delete-query", entryId]
running: false
onRunningChanged: {
if (!running && entryId !== "") {
// Remove deleted entry from model
for (let i = 0; i < cliphistModel.count; i++) {
if (cliphistModel.get(i).id === entryId) {
cliphistModel.remove(i)
currentEntries = currentEntries.filter(entry => entry.id !== entryId)
break
}
}
console.log("Deleted entry:", entryId)
entryId = ""
}
}
stderr: SplitParser {
onRead: data => {
console.error("Delete entry error:", data.toString())
}
}
}
// Copy plain text to clipboard
Process {
id: copyTextProcess
property string textToCopy: ""
command: ["wl-copy", textToCopy]
running: false
stderr: SplitParser {
onRead: data => {
console.error("wl-copy error:", data.toString())
}
}
}
// Copy from clipboard history
Process {
id: copyHistoryProcess
property string entryId: ""
command: ["sh", "-c", "printf '%s' '" + entryId + "' | cliphist decode | wl-copy"]
running: false
stderr: SplitParser {
onRead: data => {
console.error("Copy history error:", data.toString())
}
}
}
// Periodic refresh timer (disabled by default)
Timer {
id: refreshTimer
interval: 30000
running: false // Only enable when needed
repeat: true
onTriggered: {
if (!cliphistProcess.running && root.isVisible) {
refreshClipboardHistory()
}
}
}
// Component initialization
Component.onCompleted: {
refreshClipboardHistory()
}
onIsVisibleChanged: {
if (isVisible && cliphistModel.count === 0) {
refreshClipboardHistory()
}
}
// Smart model update - only changes when content differs
function updateModelIfChanged(newEntries) {
// Quick length check
if (newEntries.length !== currentEntries.length) {
updateModel(newEntries)
return
}
// Compare content for changes
let hasChanges = false
for (let i = 0; i < newEntries.length; i++) {
if (i >= currentEntries.length ||
newEntries[i].id !== currentEntries[i].id ||
newEntries[i].content !== currentEntries[i].content) {
hasChanges = true
break
}
}
if (hasChanges) {
updateModel(newEntries)
}
}
// Efficient model update with scroll position preservation
function updateModel(newEntries) {
const scrollPos = cliphistList.contentY
// Remove obsolete items
for (let i = cliphistModel.count - 1; i >= 0; i--) {
const modelItem = cliphistModel.get(i)
const found = newEntries.some(entry => entry.id === modelItem.id)
if (!found) {
cliphistModel.remove(i)
}
}
// Add or update items
for (let i = 0; i < newEntries.length; i++) {
const newEntry = newEntries[i]
let found = false
// Check if item exists and update position
for (let j = 0; j < cliphistModel.count; j++) {
const modelItem = cliphistModel.get(j)
if (modelItem.id === newEntry.id) {
if (modelItem.content !== newEntry.content) {
cliphistModel.set(j, newEntry)
}
if (j !== i && i < cliphistModel.count) {
cliphistModel.move(j, i, 1)
}
found = true
break
}
}
// Add new item
if (!found) {
if (i < cliphistModel.count) {
cliphistModel.insert(i, newEntry)
} else {
cliphistModel.append(newEntry)
}
}
}
// Restore scroll position
cliphistList.contentY = scrollPos
currentEntries = newEntries.slice()
}
// Content type detection based on patterns
function detectContentType(content) {
// Binary/image data detection
if (content.includes('\x00') || content.startsWith('\x89PNG') || content.startsWith('\xFF\xD8\xFF')) {
return "image"
}
if (content.includes('[[ binary data ') || content.includes('<selection>')) {
return "image"
}
// URL detection
if (/^https?:\/\/\S+$/.test(content.trim())) return "url"
// Code detection
if (content.includes('\n') && (content.includes('{') || content.includes('function') || content.includes('=>'))) return "code"
// Command detection
if (content.startsWith('sudo ') || content.startsWith('pacman ') || content.startsWith('apt ')) return "command"
return "text"
}
function formatTimestamp(timestamp) {
const now = new Date()
const entryDate = new Date(parseInt(timestamp))
const diff = (now - entryDate) / 1000
if (diff < 60) return "Just now"
if (diff < 3600) return Math.floor(diff / 60) + " min ago"
if (diff < 86400) return Math.floor(diff / 3600) + " hour" + (Math.floor(diff / 3600) === 1 ? "" : "s") + " ago"
return Qt.formatDateTime(entryDate, "MMM d h:mm AP")
}
function clearClipboardHistory() {
clearCliphistProcess.running = true
}
function deleteClipboardEntry(entryId) {
deleteEntryProcess.entryId = entryId
deleteEntryProcess.running = true
}
function refreshClipboardHistory() {
cliphistProcess.running = true
}
// Copy handler - chooses appropriate method based on content type
function copyToClipboard(entryIdOrText, contentType) {
if (contentType === "image" || typeof entryIdOrText === "string" && entryIdOrText.match(/^\d+$/)) {
// Use cliphist decode for binary data and numbered entries
copyHistoryProcess.entryId = entryIdOrText
copyHistoryProcess.running = true
} else {
// Use wl-copy for plain text
copyTextProcess.textToCopy = entryIdOrText
copyTextProcess.running = true
}
}
// Clean up all processes on destruction
Component.onDestruction: {
cliphistProcess.running = false
clearCliphistProcess.running = false
deleteEntryProcess.running = false
copyTextProcess.running = false
copyHistoryProcess.running = false
}
}

View file

@ -0,0 +1,161 @@
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import "root:/Data/" as Data
// Custom system tray menu
Rectangle {
id: trayMenu
implicitWidth: 360
implicitHeight: Math.max(40, listView.contentHeight + 12 + 16)
clip: true
color: Data.ThemeManager.bgColor
border.color: Data.ThemeManager.accentColor
border.width: 3
radius: 20
visible: false
enabled: visible
property QsMenuHandle menu
property point triggerPoint: Qt.point(0, 0)
property Item originalParent
// Menu opener handles native menu integration
QsMenuOpener {
id: opener
menu: trayMenu.menu
}
// Full-screen overlay to capture outside clicks
Rectangle {
id: overlay
x: -trayMenu.x
y: -trayMenu.y
width: Screen.width
height: Screen.height
color: "transparent"
visible: trayMenu.visible
z: -1
MouseArea {
anchors.fill: parent
enabled: trayMenu.visible
acceptedButtons: Qt.AllButtons
onPressed: {
trayMenu.hide()
}
}
}
// Flatten hierarchical menu structure into single list
function flattenMenuItems(menuHandle) {
var result = [];
if (!menuHandle || !menuHandle.children) {
return result;
}
var childrenArray = [];
for (var i = 0; i < menuHandle.children.length; i++) {
childrenArray.push(menuHandle.children[i]);
}
for (var i = 0; i < childrenArray.length; i++) {
var item = childrenArray[i];
if (item.isSeparator) {
result.push(item);
} else if (item.menu) {
// Add parent item and its submenu items
result.push(item);
var submenuItems = flattenMenuItems(item.menu);
result = result.concat(submenuItems);
} else {
result.push(item);
}
}
return result;
}
// Menu item list
ListView {
id: listView
anchors.fill: parent
anchors.margins: 6
anchors.topMargin: 3
anchors.bottomMargin: 9
model: ScriptModel {
values: flattenMenuItems(opener.menu)
}
interactive: false
delegate: Rectangle {
id: entry
required property var modelData
width: listView.width - 12
height: modelData.isSeparator ? 10 : 28
color: modelData.isSeparator ? Data.ThemeManager.bgColor : (mouseArea.containsMouse ? Data.ThemeManager.highlightBg : "transparent")
radius: modelData.isSeparator ? 0 : 4
// Separator line rendering
Item {
anchors.fill: parent
visible: modelData.isSeparator
Rectangle {
anchors.horizontalCenter: parent.horizontalCenter
anchors.verticalCenter: parent.verticalCenter
width: parent.width * 0.85
height: 1
color: Data.ThemeManager.accentColor
opacity: 0.3
}
}
// Menu item content (text and icon)
RowLayout {
anchors.fill: parent
anchors.leftMargin: 8
anchors.rightMargin: 8
spacing: 6
visible: !modelData.isSeparator
Text {
Layout.fillWidth: true
color: (modelData?.enabled ?? true) ? Data.ThemeManager.fgColor : Qt.darker(Data.ThemeManager.fgColor, 1.8)
text: modelData?.text ?? ""
font.pixelSize: 12
font.family: "FiraCode Nerd Font"
verticalAlignment: Text.AlignVCenter
elide: Text.ElideRight
maximumLineCount: 1
}
Image {
Layout.preferredWidth: 14
Layout.preferredHeight: 14
source: modelData?.icon ?? ""
visible: (modelData?.icon ?? "") !== ""
fillMode: Image.PreserveAspectFit
}
}
// Click handling
MouseArea {
id: mouseArea
anchors.fill: parent
hoverEnabled: true
enabled: (modelData?.enabled ?? true) && trayMenu.visible && !modelData.isSeparator
onClicked: {
if (modelData) {
modelData.triggered()
trayMenu.hide()
}
}
}
}
}
}

View file

@ -0,0 +1,442 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import QtQuick.Shapes
import Qt5Compat.GraphicalEffects
import Quickshell.Io
import "root:/Data" as Data
import "root:/Core" as Core
// Niri workspace indicator
Rectangle {
id: root
property ListModel workspaces: ListModel {}
property int currentWorkspace: -1
property bool isDestroying: false
// Signal for workspace change bursts
signal workspaceChanged(int workspaceId, color accentColor)
// MASTER ANIMATION CONTROLLER - drives Desktop overlay burst effect
property real masterProgress: 0.0
property bool effectsActive: false
property color effectColor: Data.ThemeManager.accent
// Single master animation that controls Desktop overlay burst
function triggerUnifiedWave() {
effectColor = Data.ThemeManager.accent
masterAnimation.restart()
}
SequentialAnimation {
id: masterAnimation
PropertyAction {
target: root
property: "effectsActive"
value: true
}
NumberAnimation {
target: root
property: "masterProgress"
from: 0.0
to: 1.0
duration: 1000
easing.type: Easing.OutQuint
}
PropertyAction {
target: root
property: "effectsActive"
value: false
}
PropertyAction {
target: root
property: "masterProgress"
value: 0.0
}
}
color: Data.ThemeManager.bgColor
width: 32
height: workspaceColumn.implicitHeight + 24
// Smooth height animation
Behavior on height {
NumberAnimation {
duration: 300
easing.type: Easing.OutCubic
}
}
// Right-side rounded corners
topRightRadius: width / 2
bottomRightRadius: width / 2
topLeftRadius: 0
bottomLeftRadius: 0
// Wave effects overlay - unified animation system (DISABLED - using Desktop overlay)
Item {
id: waveEffects
anchors.fill: parent
visible: false // Disabled in favor of unified overlay
z: 2
}
// Niri event stream listener
Process {
id: niriProcess
command: ["niri", "msg", "event-stream"]
running: true
stdout: SplitParser {
onRead: data => {
const lines = data.split('\n');
for (const line of lines) {
if (line.trim()) {
parseNiriEvent(line.trim());
}
}
}
}
onExited: {
// Auto-restart on failure to maintain workspace sync (but not during destruction)
if (exitCode !== 0 && !root.isDestroying) {
Qt.callLater(() => running = true);
}
}
}
// Parse Niri event stream messages
function parseNiriEvent(line) {
try {
// Handle workspace focus changes
if (line.startsWith("Workspace focused: ")) {
const workspaceId = parseInt(line.replace("Workspace focused: ", ""));
if (!isNaN(workspaceId)) {
const previousWorkspace = root.currentWorkspace;
root.currentWorkspace = workspaceId;
updateWorkspaceFocus(workspaceId);
// Trigger burst effect if workspace actually changed
if (previousWorkspace !== workspaceId && previousWorkspace !== -1) {
root.triggerUnifiedWave();
root.workspaceChanged(workspaceId, Data.ThemeManager.accent);
}
}
}
// Handle workspace list updates
else if (line.startsWith("Workspaces changed: ")) {
const workspaceData = line.replace("Workspaces changed: ", "");
parseWorkspaceList(workspaceData);
}
} catch (e) {
console.log("Error parsing niri event:", e);
}
}
// Update workspace focus states
function updateWorkspaceFocus(focusedWorkspaceId) {
for (let i = 0; i < root.workspaces.count; i++) {
const workspace = root.workspaces.get(i);
const wasFocused = workspace.isFocused;
const isFocused = workspace.id === focusedWorkspaceId;
const isActive = workspace.id === focusedWorkspaceId;
// Only update changed properties to trigger animations
if (wasFocused !== isFocused) {
root.workspaces.setProperty(i, "isFocused", isFocused);
root.workspaces.setProperty(i, "isActive", isActive);
}
}
}
// Parse workspace data from Niri's Rust-style output format
function parseWorkspaceList(data) {
try {
const workspaceMatches = data.match(/Workspace \{[^}]+\}/g);
if (!workspaceMatches) {
return;
}
const newWorkspaces = [];
for (const match of workspaceMatches) {
const idMatch = match.match(/id: (\d+)/);
const idxMatch = match.match(/idx: (\d+)/);
const nameMatch = match.match(/name: Some\("([^"]+)"\)|name: None/);
const outputMatch = match.match(/output: Some\("([^"]+)"\)/);
const isActiveMatch = match.match(/is_active: (true|false)/);
const isFocusedMatch = match.match(/is_focused: (true|false)/);
const isUrgentMatch = match.match(/is_urgent: (true|false)/);
if (idMatch && idxMatch && outputMatch) {
const workspace = {
id: parseInt(idMatch[1]),
idx: parseInt(idxMatch[1]),
name: nameMatch && nameMatch[1] ? nameMatch[1] : "",
output: outputMatch[1],
isActive: isActiveMatch ? isActiveMatch[1] === "true" : false,
isFocused: isFocusedMatch ? isFocusedMatch[1] === "true" : false,
isUrgent: isUrgentMatch ? isUrgentMatch[1] === "true" : false
};
newWorkspaces.push(workspace);
if (workspace.isFocused) {
root.currentWorkspace = workspace.id;
}
}
}
// Sort by index and update model
newWorkspaces.sort((a, b) => a.idx - b.idx);
root.workspaces.clear();
root.workspaces.append(newWorkspaces);
} catch (e) {
console.log("Error parsing workspace list:", e);
}
}
// Vertical workspace indicator pills
Column {
id: workspaceColumn
anchors.centerIn: parent
spacing: 6
Repeater {
model: root.workspaces
Rectangle {
id: workspacePill
// Dynamic sizing based on focus state
width: model.isFocused ? 18 : 16
height: model.isFocused ? 36 : 22
radius: width / 2
scale: model.isFocused ? 1.0 : 0.9
// Material Design 3 inspired colors
color: {
if (model.isFocused) {
return Data.ThemeManager.accent;
}
if (model.isActive) {
return Qt.rgba(Data.ThemeManager.accent.r, Data.ThemeManager.accent.g, Data.ThemeManager.accent.b, 0.5);
}
if (model.isUrgent) {
return Data.ThemeManager.error;
}
return Qt.rgba(Data.ThemeManager.primaryText.r, Data.ThemeManager.primaryText.g, Data.ThemeManager.primaryText.b, 0.4);
}
// Workspace pill burst overlay (DISABLED - using unified overlay)
Rectangle {
id: pillBurst
anchors.centerIn: parent
width: parent.width + 8
height: parent.height + 8
radius: width / 2
color: Data.ThemeManager.accent
opacity: 0 // Disabled in favor of unified overlay
visible: false
z: -1
}
// Subtle pulse for inactive pills during workspace changes
Rectangle {
id: inactivePillPulse
anchors.fill: parent
radius: parent.radius
color: Data.ThemeManager.accent
opacity: {
// Only pulse inactive pills during effects
if (model.isFocused || !root.effectsActive) return 0
// More subtle pulse that peaks mid-animation
if (root.masterProgress < 0.3) {
return (root.masterProgress / 0.3) * 0.15
} else if (root.masterProgress < 0.7) {
return 0.15
} else {
return 0.15 * (1.0 - (root.masterProgress - 0.7) / 0.3)
}
}
z: -0.5 // Behind the pill content but visible
}
// Enhanced corner shadows for burst effect (DISABLED - using unified overlay)
Rectangle {
id: cornerBurst
anchors.centerIn: parent
width: parent.width + 4
height: parent.height + 4
radius: width / 2
color: "transparent"
border.color: Data.ThemeManager.accent
border.width: 0 // Disabled
opacity: 0 // Disabled in favor of unified overlay
visible: false
z: 1
}
// Elevation shadow
Rectangle {
anchors.fill: parent
anchors.topMargin: model.isFocused ? 1 : 0
anchors.leftMargin: model.isFocused ? 0.5 : 0
anchors.rightMargin: model.isFocused ? -0.5 : 0
anchors.bottomMargin: model.isFocused ? -1 : 0
radius: parent.radius
color: Qt.rgba(0, 0, 0, model.isFocused ? 0.15 : 0)
z: -1
visible: model.isFocused
Behavior on color { ColorAnimation { duration: 200 } }
}
// Smooth Material Design transitions
Behavior on width {
NumberAnimation {
duration: 300
easing.type: Easing.OutCubic
}
}
Behavior on height {
NumberAnimation {
duration: 300
easing.type: Easing.OutCubic
}
}
Behavior on scale {
NumberAnimation {
duration: 250
easing.type: Easing.OutCubic
}
}
Behavior on color {
ColorAnimation {
duration: 200
}
}
// Workspace number text
Text {
anchors.centerIn: parent
text: model.idx.toString()
color: model.isFocused ? Data.ThemeManager.background : Data.ThemeManager.primaryText
font.pixelSize: model.isFocused ? 10 : 8
font.bold: model.isFocused
font.family: "Roboto, sans-serif"
visible: model.isFocused || model.isActive
Behavior on font.pixelSize { NumberAnimation { duration: 200 } }
Behavior on color { ColorAnimation { duration: 200 } }
}
MouseArea {
anchors.fill: parent
hoverEnabled: true
onClicked: {
// Switch workspace via Niri command
switchProcess.command = ["niri", "msg", "action", "focus-workspace", model.idx.toString()];
switchProcess.running = true;
}
// Hover feedback
onEntered: {
if (!model.isFocused) {
workspacePill.color = Qt.rgba(Data.ThemeManager.primaryText.r, Data.ThemeManager.primaryText.g, Data.ThemeManager.primaryText.b, 0.6);
}
}
onExited: {
// Reset to normal color
if (!model.isFocused) {
if (model.isActive) {
workspacePill.color = Qt.rgba(Data.ThemeManager.accent.r, Data.ThemeManager.accent.g, Data.ThemeManager.accent.b, 0.5);
} else if (model.isUrgent) {
workspacePill.color = Data.ThemeManager.error;
} else {
workspacePill.color = Qt.rgba(Data.ThemeManager.primaryText.r, Data.ThemeManager.primaryText.g, Data.ThemeManager.primaryText.b, 0.4);
}
}
}
}
}
}
}
// Workspace switching command process
Process {
id: switchProcess
running: false
onExited: {
running = false
if (exitCode !== 0) {
console.log("Failed to switch workspace:", exitCode);
}
}
}
// Border integration corners
Core.Corners {
id: topLeftCorner
position: "topleft"
size: 1.3
fillColor: Data.ThemeManager.bgColor
offsetX: -41
offsetY: -25
}
// Top-left corner wave overlay (DISABLED - using unified overlay)
Shape {
id: topLeftWave
width: topLeftCorner.width
height: topLeftCorner.height
x: topLeftCorner.x
y: topLeftCorner.y
visible: false // Disabled in favor of unified overlay
preferredRendererType: Shape.CurveRenderer
layer.enabled: true
layer.samples: 4
}
Core.Corners {
id: bottomLeftCorner
position: "bottomleft"
size: 1.3
fillColor: Data.ThemeManager.bgColor
offsetX: -41
offsetY: 78
}
// Bottom-left corner wave overlay (DISABLED - using unified overlay)
Shape {
id: bottomLeftWave
width: bottomLeftCorner.width
height: bottomLeftCorner.height
x: bottomLeftCorner.x
y: bottomLeftCorner.y
visible: false // Disabled in favor of unified overlay
preferredRendererType: Shape.CurveRenderer
layer.enabled: true
layer.samples: 4
}
// Clean up processes on destruction
Component.onDestruction: {
root.isDestroying = true
if (niriProcess.running) {
niriProcess.running = false
}
if (switchProcess.running) {
switchProcess.running = false
}
}
}

View file

@ -0,0 +1,228 @@
import QtQuick
import Quickshell
import Quickshell.Io
import QtQuick.Layouts
import QtQuick.Shapes
import "root:/Data/" as Data
import "root:/Core" as Core
Item {
id: osd
property var shell
QtObject {
id: modeEnum
readonly property int volume: 0
readonly property int brightness: 1
}
property int mode: -1
property int lastVolume: -1
property int lastBrightness: -1
width: osdBackground.width
height: osdBackground.height
visible: false
Timer {
id: hideTimer
interval: 2500
onTriggered: hideOsd()
}
FileView {
id: brightnessFile
path: "/tmp/brightness_osd_level"
watchChanges: true
blockLoading: true
onLoaded: updateBrightness()
onFileChanged: {
brightnessFile.reload();
updateBrightness();
}
function updateBrightness() {
const val = parseInt(brightnessFile.text());
if (!isNaN(val) && val !== lastBrightness) {
lastBrightness = val;
mode = modeEnum.brightness;
showOsd();
}
}
}
Connections {
target: shell
function onVolumeChanged() {
if (shell.volume !== lastVolume && lastVolume !== -1) {
lastVolume = shell.volume;
mode = modeEnum.volume;
showOsd();
}
lastVolume = shell.volume;
}
}
Component.onCompleted: {
if (shell?.volume !== undefined)
lastVolume = shell.volume;
}
function showOsd() {
if (!osd.visible) {
osd.visible = true;
slideInAnimation.start();
}
hideTimer.restart();
}
function hideOsd() {
slideOutAnimation.start();
}
NumberAnimation {
id: slideInAnimation
target: osdBackground
property: "x"
from: osd.width
to: 0
duration: 300
easing.type: Easing.OutCubic
}
NumberAnimation {
id: slideOutAnimation
target: osdBackground
property: "x"
from: 0
to: osd.width
duration: 250
easing.type: Easing.InCubic
onFinished: {
osd.visible = false;
osdBackground.x = 0;
}
}
Rectangle {
id: osdBackground
width: 45
height: 250
color: Data.ThemeManager.bgColor
topLeftRadius: 20
bottomLeftRadius: 20
Column {
anchors.fill: parent
anchors.margins: 16
spacing: 12
Text {
id: osdIcon
font.family: "Roboto"
font.pixelSize: 16
color: Data.ThemeManager.fgColor
text: {
if (mode === modeEnum.volume) {
if (!shell || shell.volume === undefined)
return "󰝟";
const vol = shell.volume;
return vol === 0 ? "󰝟" : vol < 33 ? "󰕿" : vol < 66 ? "󰖀" : "󰕾";
} else if (mode === modeEnum.brightness) {
const b = lastBrightness;
return b < 0 ? "󰃞" : b < 33 ? "󰃟" : b < 66 ? "󰃠" : "󰃝";
}
return "";
}
anchors.horizontalCenter: parent.horizontalCenter
Behavior on text {
SequentialAnimation {
PropertyAnimation {
target: osdIcon
property: "scale"
to: 1.2
duration: 100
}
PropertyAnimation {
target: osdIcon
property: "scale"
to: 1.0
duration: 100
}
}
}
}
Rectangle {
width: 10
height: parent.height - osdIcon.height - osdLabel.height - 36
radius: 5
color: Qt.darker(Data.ThemeManager.accentColor, 1.5)
border.color: Qt.darker(Data.ThemeManager.accentColor, 2.0)
border.width: 1
anchors.horizontalCenter: parent.horizontalCenter
Rectangle {
id: fillBar
width: parent.width - 2
radius: parent.radius - 1
x: 1
color: Data.ThemeManager.accentColor
anchors.bottom: parent.bottom
anchors.bottomMargin: 1
height: {
const val = mode === modeEnum.volume ? shell?.volume : lastBrightness;
const maxHeight = parent.height - 2;
return maxHeight * Math.max(0, Math.min(1, val / 100));
}
Behavior on height {
NumberAnimation {
duration: 250
easing.type: Easing.OutCubic
}
}
}
}
Text {
id: osdLabel
text: {
const val = mode === modeEnum.volume ? shell?.volume : lastBrightness;
return val >= 0 ? val + "%" : "0%";
}
font.pixelSize: 10
font.weight: Font.Bold
color: Data.ThemeManager.fgColor
anchors.horizontalCenter: parent.horizontalCenter
Behavior on text {
PropertyAnimation {
target: osdLabel
property: "opacity"
from: 0.7
to: 1.0
duration: 150
}
}
}
}
}
Core.Corners {
position: "bottomright"
size: 1.3
fillColor: Data.ThemeManager.bgColor
offsetX: 39 + osdBackground.x
offsetY: 78
}
Core.Corners {
position: "topright"
size: 1.3
fillColor: Data.ThemeManager.bgColor
offsetX: 39 + osdBackground.x
offsetY: -26
}
}

View file

@ -0,0 +1,181 @@
import QtQuick
import QtQuick.Layouts
import QtQuick.Controls
import Quickshell
import Quickshell.Services.SystemTray
import "root:/Data" as Data
// System tray with optimized icon caching
Row {
property var bar
property var shell
property var trayMenu
spacing: 8
Layout.alignment: Qt.AlignVCenter
property bool containsMouse: false
property var systemTray: SystemTray
// Custom icon cache for memory optimization
property var iconCache: ({})
property var iconCacheCount: ({})
// Cache cleanup to prevent memory leaks
Timer {
interval: 120000
repeat: true
running: systemTray.items.length > 0
onTriggered: {
// Decrement counters and remove unused icons
for (let icon in iconCacheCount) {
iconCacheCount[icon]--
if (iconCacheCount[icon] <= 0) {
delete iconCache[icon]
delete iconCacheCount[icon]
}
}
// Enforce maximum cache size
const maxCacheSize = 10;
const cacheKeys = Object.keys(iconCache);
if (cacheKeys.length > maxCacheSize) {
const toRemove = cacheKeys.slice(0, cacheKeys.length - maxCacheSize);
toRemove.forEach(key => {
delete iconCache[key];
delete iconCacheCount[key];
});
}
}
}
Repeater {
model: systemTray.items
delegate: Item {
width: 24
height: 24
property bool isHovered: trayMouseArea.containsMouse
onIsHoveredChanged: updateParentHoverState()
Component.onCompleted: updateParentHoverState()
function updateParentHoverState() {
let anyHovered = false
for (let i = 0; i < parent.children.length; i++) {
if (parent.children[i].isHovered) {
anyHovered = true
break
}
}
parent.containsMouse = anyHovered
}
// Hover animations
scale: isHovered ? 1.15 : 1.0
Behavior on scale {
enabled: isHovered
NumberAnimation {
duration: 150
easing.type: Easing.OutCubic
}
}
rotation: isHovered ? 5 : 0
Behavior on rotation {
enabled: isHovered
NumberAnimation {
duration: 200
easing.type: Easing.OutCubic
}
}
Image {
id: trayIcon
anchors.centerIn: parent
width: 18
height: 18
sourceSize.width: 18
sourceSize.height: 18
smooth: false // Memory savings
asynchronous: true
cache: false // Use custom cache instead
source: {
let icon = modelData?.icon || "";
if (!icon) return "";
// Return cached icon if available
if (iconCache[icon]) {
iconCacheCount[icon] = 2
return iconCache[icon];
}
// Process icon path
let finalPath = icon;
if (icon.includes("?path=")) {
const [name, path] = icon.split("?path=");
const fileName = name.substring(name.lastIndexOf("/") + 1);
finalPath = `file://${path}/${fileName}`;
}
// Cache the processed path
iconCache[icon] = finalPath;
iconCacheCount[icon] = 2;
return finalPath;
}
opacity: status === Image.Ready ? 1 : 0
Behavior on opacity {
NumberAnimation {
duration: 300
easing.type: Easing.OutCubic
}
}
}
Component.onDestruction: {
let icon = modelData?.icon || "";
if (icon) {
delete iconCache[icon];
delete iconCacheCount[icon];
}
}
MouseArea {
id: trayMouseArea
anchors.fill: parent
hoverEnabled: true
acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
onClicked: (mouse) => {
if (!modelData) return;
if (mouse.button === Qt.LeftButton) {
if (trayMenu && trayMenu.visible) {
trayMenu.hide()
}
if (!modelData.onlyMenu) {
modelData.activate()
}
} else if (mouse.button === Qt.MiddleButton) {
if (trayMenu && trayMenu.visible) {
trayMenu.hide()
}
modelData.secondaryActivate && modelData.secondaryActivate()
} else if (mouse.button === Qt.RightButton) {
if (trayMenu && trayMenu.visible) {
trayMenu.hide()
return
}
// Show context menu if available
if (modelData.hasMenu && modelData.menu && trayMenu) {
trayMenu.menu = modelData.menu
const iconCenter = Qt.point(width / 2, height)
const iconPos = mapToItem(trayMenu.parent, 0, 0)
const menuX = iconPos.x - (trayMenu.width / 2) + (width / 2)
const menuY = iconPos.y + height + 15
trayMenu.show(Qt.point(menuX, menuY), trayMenu.parent)
}
}
}
}
}
}
}

View file

@ -0,0 +1,188 @@
import QtQuick
import Quickshell
import Quickshell.Wayland
import QtQuick.Layouts
import QtQuick.Shapes
import "root:/Data/" as Data
import "root:/Core" as Core
// Volume OSD with slide animation
Item {
id: volumeOsd
property var shell
// Size and visibility
width: osdBackground.width
height: osdBackground.height
visible: false
// Auto-hide timer (2.5 seconds of inactivity)
Timer {
id: hideTimer
interval: 2500
onTriggered: hideOsd()
}
property int lastVolume: -1
// Monitor volume changes from shell and trigger OSD
Connections {
target: shell
function onVolumeChanged() {
if (shell.volume !== lastVolume && lastVolume !== -1) {
showOsd()
}
lastVolume = shell.volume
}
}
Component.onCompleted: {
// Initialize lastVolume on startup
if (shell && shell.volume !== undefined) {
lastVolume = shell.volume
}
}
// Show OSD
function showOsd() {
if (!volumeOsd.visible) {
volumeOsd.visible = true
slideInAnimation.start()
}
hideTimer.restart()
}
// Start slide-out animation to hide OSD
function hideOsd() {
slideOutAnimation.start()
}
// Slide in from right edge
NumberAnimation {
id: slideInAnimation
target: osdBackground
property: "x"
from: volumeOsd.width
to: 0
duration: 300
easing.type: Easing.OutCubic
}
// Slide out to right edge
NumberAnimation {
id: slideOutAnimation
target: osdBackground
property: "x"
from: 0
to: volumeOsd.width
duration: 250
easing.type: Easing.InCubic
onFinished: {
volumeOsd.visible = false
osdBackground.x = 0 // Reset position
}
}
Rectangle {
id: osdBackground
width: 45
height: 250
color: Data.ThemeManager.bgColor
topLeftRadius: 20
bottomLeftRadius: 20
Column {
anchors.fill: parent
anchors.margins: 16
spacing: 12
// Dynamic volume icon
Text {
id: volumeIcon
font.family: "Roboto"
font.pixelSize: 16
color: Data.ThemeManager.fgColor
text: {
if (!shell || shell.volume === undefined) return "󰝟"
var vol = shell.volume
if (vol === 0) return "󰝟" // Muted
else if (vol < 33) return "󰕿" // Low
else if (vol < 66) return "󰖀" // Medium
else return "󰕾" // High
}
anchors.horizontalCenter: parent.horizontalCenter
// Scale animation on volume change
Behavior on text {
SequentialAnimation {
PropertyAnimation { target: volumeIcon; property: "scale"; to: 1.2; duration: 100 }
PropertyAnimation { target: volumeIcon; property: "scale"; to: 1.0; duration: 100 }
}
}
}
// Vertical volume bar
Rectangle {
width: 10
height: parent.height - volumeIcon.height - volumeLabel.height - 36
radius: 5
color: Qt.darker(Data.ThemeManager.accentColor, 1.5)
border.color: Qt.darker(Data.ThemeManager.accentColor, 2.0)
border.width: 1
anchors.horizontalCenter: parent.horizontalCenter
// Animated volume fill indicator
Rectangle {
id: volumeFill
width: parent.width - 2
radius: parent.radius - 1
x: 1
color: Data.ThemeManager.accentColor
anchors.bottom: parent.bottom
anchors.bottomMargin: 1
height: {
if (!shell || shell.volume === undefined) return 0
var maxHeight = parent.height - 2
return maxHeight * Math.max(0, Math.min(1, shell.volume / 100))
}
Behavior on height {
NumberAnimation { duration: 250; easing.type: Easing.OutCubic }
}
}
}
// Volume percentage text
Text {
id: volumeLabel
text: (shell && shell.volume !== undefined ? shell.volume + "%" : "0%")
font.pixelSize: 10
font.weight: Font.Bold
color: Data.ThemeManager.fgColor
anchors.horizontalCenter: parent.horizontalCenter
// Fade animation on volume change
Behavior on text {
PropertyAnimation { target: volumeLabel; property: "opacity"; from: 0.7; to: 1.0; duration: 150 }
}
}
}
}
Core.Corners {
id: bottomRightCorner
position: "bottomright"
size: 1.3
fillColor: Data.ThemeManager.bgColor
offsetX: 39 + osdBackground.x
offsetY: 78
}
Core.Corners {
id: topRightCorner
position: "topright"
size: 1.3
fillColor: Data.ThemeManager.bgColor
offsetX: 39 + osdBackground.x
offsetY: -26
}
}

View file

@ -0,0 +1,56 @@
import QtQuick
import QtQuick.Controls
import Quickshell
import Quickshell.Hyprland
import "root:/Data" as Data
// Hyprland workspace indicator
Column {
id: root
property var shell
spacing: 8
Repeater {
model: Hyprland.workspaces
delegate: Rectangle {
width: 22
height: 22
radius: 6
color: modelData.active ? Data.ThemeManager.accentColor : "transparent"
border.color: Data.ThemeManager.accentColor
border.width: modelData.active ? 0 : 1
opacity: modelData.active ? 1 : 0.6
Text {
anchors.centerIn: parent
text: modelData.name || modelData.id
color: modelData.active ? Data.ThemeManager.bgColor : Data.ThemeManager.fgColor
font.family: "Roboto"
font.pixelSize: 12
font.bold: modelData.active
}
MouseArea {
anchors.fill: parent
onClicked: modelData.activate()
onPressAndHold: {
// Move focused window to workspace (regular workspaces only)
if (modelData.id > 0) {
Hyprland.dispatch(`movetoworkspace ${modelData.id}`)
}
}
}
}
}
// Workspace synchronization
Connections {
target: Hyprland
function onFocusedWorkspaceChanged() {
Hyprland.refreshWorkspaces()
}
}
Component.onCompleted: Hyprland.refreshWorkspaces()
}