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,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
}
}
}
}
}
}
}