feat: init quickshell
This commit is contained in:
142
modules/desktop/quickshell/qml/Widgets/Panel/TopPanel.qml
Normal file
142
modules/desktop/quickshell/qml/Widgets/Panel/TopPanel.qml
Normal file
@@ -0,0 +1,142 @@
|
||||
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: ""
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
683
modules/desktop/quickshell/qml/Widgets/Panel/modules/Panel.qml
Normal file
683
modules/desktop/quickshell/qml/Widgets/Panel/modules/Panel.qml
Normal file
@@ -0,0 +1,683 @@
|
||||
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
|
||||
property real bgOpacity: 0.0
|
||||
property bool isRecording: false
|
||||
|
||||
property var tabIcons: ["widgets", "calendar_month", "content_paste", "notifications"]
|
||||
|
||||
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"]
|
||||
|
||||
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: "monospace"
|
||||
}
|
||||
|
||||
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: "monospace"
|
||||
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: "monospace"
|
||||
}
|
||||
|
||||
Text {
|
||||
text: "(" + (root.shell.notificationHistory ? root.shell.notificationHistory.count : 0) + ")"
|
||||
color: Data.ThemeManager.fgColor
|
||||
font.family: "monospace"
|
||||
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: "monospace"
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
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");
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
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: "monospace"
|
||||
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()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
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();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,216 @@
|
||||
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"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,228 @@
|
||||
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: "monospace"
|
||||
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: "monospace"
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,332 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user