add crypto
This commit is contained in:
parent
90cbe489f6
commit
af6a3bce3e
120 changed files with 24616 additions and 462 deletions
|
|
@ -0,0 +1,125 @@
|
|||
// Calendar.qml
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import QtQuick.Controls
|
||||
import "root:/Data" as Data
|
||||
|
||||
// Calendar widget with navigation
|
||||
Rectangle {
|
||||
id: calendarRoot
|
||||
property var shell
|
||||
|
||||
radius: 20
|
||||
color: Qt.lighter(Data.ThemeManager.bgColor, 1.2)
|
||||
|
||||
readonly property date currentDate: new Date()
|
||||
property int month: currentDate.getMonth()
|
||||
property int year: currentDate.getFullYear()
|
||||
readonly property int currentDay: currentDate.getDate()
|
||||
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
anchors.margins: 12
|
||||
spacing: 12
|
||||
|
||||
// Month/Year header
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: 8
|
||||
|
||||
// Reusable navigation button
|
||||
component NavButton: AbstractButton {
|
||||
property alias buttonText: buttonLabel.text
|
||||
implicitWidth: 30
|
||||
implicitHeight: 30
|
||||
|
||||
background: Rectangle {
|
||||
radius: 15
|
||||
color: parent.down ? Qt.darker(Data.ThemeManager.accentColor, 1.2) :
|
||||
parent.hovered ? Qt.lighter(Data.ThemeManager.highlightBg, 1.1) : Data.ThemeManager.highlightBg
|
||||
}
|
||||
|
||||
Text {
|
||||
id: buttonLabel
|
||||
anchors.centerIn: parent
|
||||
color: Data.ThemeManager.fgColor
|
||||
font.pixelSize: 16
|
||||
font.bold: true
|
||||
}
|
||||
}
|
||||
|
||||
// Current month and year display
|
||||
Text {
|
||||
text: Qt.locale("en_US").monthName(calendarRoot.month) + " " + calendarRoot.year
|
||||
color: Data.ThemeManager.accentColor
|
||||
font.bold: true
|
||||
Layout.fillWidth: true
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
font.pixelSize: 18
|
||||
}
|
||||
}
|
||||
|
||||
// Weekday headers (Monday-Sunday)
|
||||
Grid {
|
||||
columns: 7
|
||||
rowSpacing: 4
|
||||
columnSpacing: 0
|
||||
Layout.leftMargin: 2
|
||||
Layout.fillWidth: true
|
||||
|
||||
Repeater {
|
||||
model: ["M", "T", "W", "T", "F", "S", "S"]
|
||||
delegate: Text {
|
||||
text: modelData
|
||||
color: Data.ThemeManager.fgColor
|
||||
font.bold: true
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
width: parent.width / 7
|
||||
font.pixelSize: 14
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Calendar grid
|
||||
MonthGrid {
|
||||
id: monthGrid
|
||||
month: calendarRoot.month
|
||||
year: calendarRoot.year
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
spacing: 4
|
||||
leftPadding: 0
|
||||
rightPadding: 0
|
||||
locale: Qt.locale("en_US")
|
||||
implicitHeight: 400
|
||||
|
||||
delegate: Rectangle {
|
||||
width: 30
|
||||
height: 30
|
||||
radius: 15
|
||||
|
||||
readonly property bool isCurrentMonth: model.month === calendarRoot.month
|
||||
readonly property bool isToday: model.day === calendarRoot.currentDay &&
|
||||
model.month === calendarRoot.currentDate.getMonth() &&
|
||||
calendarRoot.year === calendarRoot.currentDate.getFullYear() &&
|
||||
isCurrentMonth
|
||||
|
||||
// Dynamic styling: today = accent color, current month = normal, other months = dimmed
|
||||
color: isToday ? Data.ThemeManager.accentColor :
|
||||
isCurrentMonth ? Data.ThemeManager.bgColor : Qt.darker(Data.ThemeManager.bgColor, 1.4)
|
||||
|
||||
Text {
|
||||
text: model.day
|
||||
anchors.centerIn: parent
|
||||
color: isToday ? Data.ThemeManager.bgColor :
|
||||
isCurrentMonth ? Data.ThemeManager.fgColor : Qt.darker(Data.ThemeManager.fgColor, 1.5)
|
||||
font.bold: isToday
|
||||
font.pixelSize: 14
|
||||
font.family: "Roboto"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,121 @@
|
|||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import "root:/Data" as Data
|
||||
|
||||
// Calendar popup with animations
|
||||
Popup {
|
||||
id: calendarPopup
|
||||
property bool hovered: false
|
||||
property bool clickMode: false // Persistent mode - stays open until clicked again
|
||||
property var shell
|
||||
property int targetX: 0
|
||||
readonly property int targetY: Screen.height - height
|
||||
|
||||
width: 280
|
||||
height: 280
|
||||
modal: false
|
||||
focus: true
|
||||
closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside
|
||||
padding: 15
|
||||
|
||||
// Animation state properties
|
||||
property bool _visible: false
|
||||
property real animX: targetX - 20
|
||||
property real animOpacity: 0
|
||||
|
||||
x: animX
|
||||
y: targetY
|
||||
opacity: animOpacity
|
||||
visible: _visible
|
||||
|
||||
// Smooth slide-in animation
|
||||
Behavior on animX {
|
||||
NumberAnimation { duration: 200; easing.type: Easing.InOutQuad }
|
||||
}
|
||||
Behavior on animOpacity {
|
||||
NumberAnimation { duration: 200; easing.type: Easing.InOutQuad }
|
||||
}
|
||||
|
||||
// Hover mode: show/hide based on mouse state
|
||||
onHoveredChanged: {
|
||||
if (!clickMode) {
|
||||
if (hovered) {
|
||||
_visible = true
|
||||
animX = targetX
|
||||
animOpacity = 1
|
||||
} else {
|
||||
animX = targetX - 20
|
||||
animOpacity = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Click mode: persistent visibility toggle
|
||||
onClickModeChanged: {
|
||||
if (clickMode) {
|
||||
_visible = true
|
||||
animX = targetX
|
||||
animOpacity = 1
|
||||
} else {
|
||||
animX = targetX - 20
|
||||
animOpacity = 0
|
||||
}
|
||||
}
|
||||
|
||||
// Hide when animation completes
|
||||
onAnimOpacityChanged: {
|
||||
if (animOpacity === 0 && !hovered && !clickMode) {
|
||||
_visible = false
|
||||
}
|
||||
}
|
||||
|
||||
function setHovered(state) {
|
||||
hovered = state
|
||||
}
|
||||
|
||||
function setClickMode(state) {
|
||||
clickMode = state
|
||||
}
|
||||
|
||||
// Hover detection
|
||||
MouseArea {
|
||||
id: hoverArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
propagateComposedEvents: true
|
||||
anchors.margins: 10 // Larger area to reduce flicker
|
||||
|
||||
onEntered: {
|
||||
if (!clickMode) {
|
||||
setHovered(true)
|
||||
}
|
||||
}
|
||||
onExited: {
|
||||
if (!clickMode) {
|
||||
// Delayed exit check to prevent hover flicker
|
||||
Qt.callLater(() => {
|
||||
if (!hoverArea.containsMouse) {
|
||||
setHovered(false)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Lazy-loaded calendar content
|
||||
Loader {
|
||||
anchors.fill: parent
|
||||
active: calendarPopup._visible
|
||||
source: active ? "Calendar.qml" : ""
|
||||
onLoaded: {
|
||||
if (item) {
|
||||
item.shell = calendarPopup.shell
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
background: Rectangle {
|
||||
color: Data.ThemeManager.bgColor
|
||||
topRightRadius: 20
|
||||
}
|
||||
}
|
||||
64
modules/home/services/quickshell/qml/Widgets/Clock.qml
Normal file
64
modules/home/services/quickshell/qml/Widgets/Clock.qml
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import Quickshell
|
||||
import "root:/Data" as Data
|
||||
import "root:/Core" as Core
|
||||
|
||||
// Clock with border integration
|
||||
Item {
|
||||
id: clockRoot
|
||||
width: clockBackground.width
|
||||
height: clockBackground.height
|
||||
|
||||
Rectangle {
|
||||
id: clockBackground
|
||||
width: clockText.implicitWidth + 24
|
||||
height: 32
|
||||
|
||||
color: Data.ThemeManager.bgColor
|
||||
|
||||
// Rounded corner for border integration
|
||||
topRightRadius: height / 2
|
||||
|
||||
Text {
|
||||
id: clockText
|
||||
anchors.centerIn: parent
|
||||
font.family: "Roboto"
|
||||
font.pixelSize: 14
|
||||
font.bold: true
|
||||
color: Data.ThemeManager.accentColor
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
text: Qt.formatTime(new Date(), "HH:mm")
|
||||
}
|
||||
}
|
||||
|
||||
// Update every minute
|
||||
Timer {
|
||||
interval: 60000
|
||||
running: true
|
||||
repeat: true
|
||||
onTriggered: clockText.text = Qt.formatTime(new Date(), "HH:mm")
|
||||
}
|
||||
|
||||
// Border integration corner pieces
|
||||
Core.Corners {
|
||||
id: topLeftCorner
|
||||
position: "topleft"
|
||||
size: 1.3
|
||||
fillColor: Data.ThemeManager.bgColor
|
||||
offsetX: -39
|
||||
offsetY: -26
|
||||
z: 0 // Same z-level as clock background
|
||||
}
|
||||
|
||||
Core.Corners {
|
||||
id: topLeftCorner2
|
||||
position: "topleft"
|
||||
size: 1.3
|
||||
fillColor: Data.ThemeManager.bgColor
|
||||
offsetX: 20
|
||||
offsetY: 6
|
||||
z: 0 // Same z-level as clock background
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,136 @@
|
|||
import QtQuick
|
||||
import "root:/Data" as Data
|
||||
import "root:/Core" as Core
|
||||
|
||||
// Main control panel coordinator - handles recording and system actions
|
||||
Item {
|
||||
id: controlPanelContainer
|
||||
|
||||
required property var shell
|
||||
property bool isRecording: false
|
||||
property int currentTab: 0 // 0=main, 1=calendar, 2=clipboard, 3=notifications, 4=wallpapers, 5=music, 6=settings
|
||||
property var tabIcons: ["widgets", "calendar_month", "content_paste", "notifications", "wallpaper", "music_note", "settings"]
|
||||
|
||||
property bool isShown: false
|
||||
property var recordingProcess: null
|
||||
|
||||
signal recordingRequested()
|
||||
signal stopRecordingRequested()
|
||||
signal systemActionRequested(string action)
|
||||
signal performanceActionRequested(string action)
|
||||
|
||||
// Screen recording
|
||||
onRecordingRequested: {
|
||||
var currentDate = new Date()
|
||||
var hours = String(currentDate.getHours()).padStart(2, '0')
|
||||
var minutes = String(currentDate.getMinutes()).padStart(2, '0')
|
||||
var day = String(currentDate.getDate()).padStart(2, '0')
|
||||
var month = String(currentDate.getMonth() + 1).padStart(2, '0')
|
||||
var year = currentDate.getFullYear()
|
||||
|
||||
var filename = hours + "-" + minutes + "-" + day + "-" + month + "-" + year + ".mp4"
|
||||
var outputPath = Data.Settings.videoPath + filename
|
||||
var command = "gpu-screen-recorder -w portal -f 60 -a default_output -o " + outputPath
|
||||
|
||||
var qmlString = 'import Quickshell.Io; Process { command: ["sh", "-c", "' + command + '"]; running: true }'
|
||||
|
||||
try {
|
||||
recordingProcess = Qt.createQmlObject(qmlString, controlPanelContainer)
|
||||
isRecording = true
|
||||
} catch (e) {
|
||||
console.error("Failed to start recording:", e)
|
||||
}
|
||||
}
|
||||
|
||||
// Stop recording with cleanup
|
||||
onStopRecordingRequested: {
|
||||
if (recordingProcess && isRecording) {
|
||||
var stopQmlString = 'import Quickshell.Io; Process { command: ["sh", "-c", "pkill -SIGINT -f \'gpu-screen-recorder.*portal\'"]; running: true; onExited: function() { destroy() } }'
|
||||
|
||||
try {
|
||||
var stopProcess = Qt.createQmlObject(stopQmlString, controlPanelContainer)
|
||||
|
||||
var cleanupTimer = Qt.createQmlObject('import QtQuick; Timer { interval: 3000; running: true; repeat: false }', controlPanelContainer)
|
||||
cleanupTimer.triggered.connect(function() {
|
||||
if (recordingProcess) {
|
||||
recordingProcess.running = false
|
||||
recordingProcess.destroy()
|
||||
recordingProcess = null
|
||||
}
|
||||
|
||||
var forceKillQml = 'import Quickshell.Io; Process { command: ["sh", "-c", "pkill -9 -f \'gpu-screen-recorder.*portal\' 2>/dev/null || true"]; running: true; onExited: function() { destroy() } }'
|
||||
var forceKillProcess = Qt.createQmlObject(forceKillQml, controlPanelContainer)
|
||||
|
||||
cleanupTimer.destroy()
|
||||
})
|
||||
} catch (e) {
|
||||
console.error("Failed to stop recording:", e)
|
||||
}
|
||||
}
|
||||
isRecording = false
|
||||
}
|
||||
|
||||
// System action routing
|
||||
onSystemActionRequested: function(action) {
|
||||
switch(action) {
|
||||
case "lock":
|
||||
Core.ProcessManager.lock()
|
||||
break
|
||||
case "reboot":
|
||||
Core.ProcessManager.reboot()
|
||||
break
|
||||
case "shutdown":
|
||||
Core.ProcessManager.shutdown()
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
onPerformanceActionRequested: function(action) {
|
||||
console.log("Performance action requested:", action)
|
||||
}
|
||||
|
||||
// Control panel window component
|
||||
ControlPanelWindow {
|
||||
id: controlPanelWindow
|
||||
|
||||
// Pass through properties
|
||||
shell: controlPanelContainer.shell
|
||||
isRecording: controlPanelContainer.isRecording
|
||||
currentTab: controlPanelContainer.currentTab
|
||||
tabIcons: controlPanelContainer.tabIcons
|
||||
isShown: controlPanelContainer.isShown
|
||||
|
||||
// Bind state changes back to parent
|
||||
onCurrentTabChanged: controlPanelContainer.currentTab = currentTab
|
||||
onIsShownChanged: controlPanelContainer.isShown = isShown
|
||||
|
||||
// Forward signals
|
||||
onRecordingRequested: controlPanelContainer.recordingRequested()
|
||||
onStopRecordingRequested: controlPanelContainer.stopRecordingRequested()
|
||||
onSystemActionRequested: function(action) { controlPanelContainer.systemActionRequested(action) }
|
||||
onPerformanceActionRequested: function(action) { controlPanelContainer.performanceActionRequested(action) }
|
||||
}
|
||||
|
||||
// Clean up processes on destruction
|
||||
Component.onDestruction: {
|
||||
if (recordingProcess) {
|
||||
try {
|
||||
if (recordingProcess.running) {
|
||||
recordingProcess.terminate()
|
||||
}
|
||||
recordingProcess.destroy()
|
||||
} catch (e) {
|
||||
console.warn("Error cleaning up recording process:", e)
|
||||
}
|
||||
recordingProcess = null
|
||||
}
|
||||
|
||||
// Force kill any remaining gpu-screen-recorder processes
|
||||
var forceCleanupCmd = 'import Quickshell.Io; Process { command: ["sh", "-c", "pkill -9 -f gpu-screen-recorder 2>/dev/null || true"]; running: true; onExited: function() { destroy() } }'
|
||||
try {
|
||||
Qt.createQmlObject(forceCleanupCmd, controlPanelContainer)
|
||||
} catch (e) {
|
||||
console.warn("Error in force cleanup:", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,106 @@
|
|||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import Qt5Compat.GraphicalEffects
|
||||
import "root:/Data" as Data
|
||||
import "./components/navigation" as Navigation
|
||||
|
||||
// Panel content with tab layout - now clean and organized!
|
||||
Item {
|
||||
id: contentRoot
|
||||
|
||||
// Properties passed from parent
|
||||
required property var shell
|
||||
required property bool isRecording
|
||||
property int currentTab: 0
|
||||
property var tabIcons: []
|
||||
required property var triggerMouseArea
|
||||
|
||||
// Signals to forward to parent
|
||||
signal recordingRequested()
|
||||
signal stopRecordingRequested()
|
||||
signal systemActionRequested(string action)
|
||||
signal performanceActionRequested(string action)
|
||||
|
||||
// Hover detection for auto-hide
|
||||
property bool isHovered: {
|
||||
const mouseStates = {
|
||||
triggerHovered: triggerMouseArea.containsMouse,
|
||||
backgroundHovered: backgroundMouseArea.containsMouse,
|
||||
tabSidebarHovered: tabNavigation.containsMouse,
|
||||
tabContainerHovered: tabContainer.isHovered,
|
||||
tabContentActive: currentTab !== 0, // Non-main tabs stay open
|
||||
tabNavigationActive: tabNavigation.containsMouse
|
||||
}
|
||||
return Object.values(mouseStates).some(state => state)
|
||||
}
|
||||
|
||||
// Expose text input focus state for keyboard management
|
||||
property bool textInputFocused: tabContainer.textInputFocused
|
||||
|
||||
// Panel background with bottom-only rounded corners
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
color: Data.ThemeManager.bgColor
|
||||
topLeftRadius: 0
|
||||
topRightRadius: 0
|
||||
bottomLeftRadius: 20
|
||||
bottomRightRadius: 20
|
||||
z: -10 // Far behind everything to avoid layering conflicts
|
||||
}
|
||||
|
||||
// Main content container with tab layout
|
||||
Rectangle {
|
||||
id: mainContainer
|
||||
anchors.fill: parent
|
||||
anchors.margins: 9
|
||||
color: "transparent"
|
||||
radius: 12
|
||||
|
||||
MouseArea {
|
||||
id: backgroundMouseArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
propagateComposedEvents: true
|
||||
property alias containsMouse: backgroundMouseArea.containsMouse
|
||||
}
|
||||
|
||||
// Left sidebar with tab navigation
|
||||
Navigation.TabNavigation {
|
||||
id: tabNavigation
|
||||
width: 40
|
||||
height: parent.height
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: 9
|
||||
anchors.top: parent.top
|
||||
anchors.topMargin: 18
|
||||
|
||||
currentTab: contentRoot.currentTab
|
||||
tabIcons: contentRoot.tabIcons
|
||||
|
||||
onCurrentTabChanged: contentRoot.currentTab = currentTab
|
||||
}
|
||||
|
||||
// Main tab content area with sliding animation
|
||||
Navigation.TabContainer {
|
||||
id: tabContainer
|
||||
width: parent.width - tabNavigation.width - 45
|
||||
height: parent.height - 36
|
||||
anchors.left: tabNavigation.right
|
||||
anchors.leftMargin: 9
|
||||
anchors.top: parent.top
|
||||
anchors.topMargin: 18
|
||||
|
||||
shell: contentRoot.shell
|
||||
isRecording: contentRoot.isRecording
|
||||
triggerMouseArea: contentRoot.triggerMouseArea
|
||||
currentTab: contentRoot.currentTab
|
||||
|
||||
onRecordingRequested: contentRoot.recordingRequested()
|
||||
onStopRecordingRequested: contentRoot.stopRecordingRequested()
|
||||
onSystemActionRequested: function(action) { contentRoot.systemActionRequested(action) }
|
||||
onPerformanceActionRequested: function(action) { contentRoot.performanceActionRequested(action) }
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,171 @@
|
|||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import Qt5Compat.GraphicalEffects
|
||||
import Quickshell
|
||||
import Quickshell.Wayland
|
||||
import "root:/Data" as Data
|
||||
import "root:/Core" as Core
|
||||
|
||||
// Control panel window and trigger
|
||||
PanelWindow {
|
||||
id: controlPanelWindow
|
||||
|
||||
// Properties passed from parent ControlPanel
|
||||
required property var shell
|
||||
required property bool isRecording
|
||||
property int currentTab: 0
|
||||
property var tabIcons: []
|
||||
property bool isShown: false
|
||||
|
||||
// Signals to forward to parent
|
||||
signal recordingRequested()
|
||||
signal stopRecordingRequested()
|
||||
signal systemActionRequested(string action)
|
||||
signal performanceActionRequested(string action)
|
||||
|
||||
screen: Quickshell.primaryScreen || Quickshell.screens[0]
|
||||
anchors.top: true
|
||||
anchors.left: true
|
||||
anchors.right: true
|
||||
margins.bottom: 0
|
||||
margins.left: (screen ? screen.width / 2 - 400 : 0) // Centered
|
||||
margins.right: (screen ? screen.width / 2 - 400 : 0)
|
||||
implicitWidth: 640
|
||||
implicitHeight: isShown ? 400 : 8 // Expand/collapse animation
|
||||
|
||||
Behavior on implicitHeight {
|
||||
NumberAnimation {
|
||||
duration: 200
|
||||
easing.type: Easing.OutCubic
|
||||
}
|
||||
}
|
||||
exclusiveZone: (panelContent && panelContent.textInputFocused) ? -1 : 0
|
||||
color: "transparent"
|
||||
visible: true
|
||||
|
||||
WlrLayershell.namespace: "quickshell-controlpanel"
|
||||
WlrLayershell.keyboardFocus: (panelContent && panelContent.textInputFocused) ? WlrKeyboardFocus.Exclusive : WlrKeyboardFocus.OnDemand
|
||||
|
||||
// Hover trigger area at screen top
|
||||
MouseArea {
|
||||
id: triggerMouseArea
|
||||
anchors.top: parent.top
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
width: 600
|
||||
height: 8
|
||||
hoverEnabled: true
|
||||
onContainsMouseChanged: {
|
||||
if (containsMouse) {
|
||||
show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Main panel content
|
||||
ControlPanelContent {
|
||||
id: panelContent
|
||||
|
||||
width: 600
|
||||
height: 380
|
||||
|
||||
anchors.top: parent.top
|
||||
anchors.topMargin: 8 // Trigger area space
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
visible: isShown
|
||||
opacity: isShown ? 1.0 : 0.0
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: 150
|
||||
easing.type: Easing.OutCubic
|
||||
}
|
||||
}
|
||||
|
||||
// Pass through properties
|
||||
shell: controlPanelWindow.shell
|
||||
isRecording: controlPanelWindow.isRecording
|
||||
currentTab: controlPanelWindow.currentTab
|
||||
tabIcons: controlPanelWindow.tabIcons
|
||||
triggerMouseArea: triggerMouseArea
|
||||
|
||||
// Bind state changes
|
||||
onCurrentTabChanged: controlPanelWindow.currentTab = currentTab
|
||||
|
||||
// Forward signals
|
||||
onRecordingRequested: controlPanelWindow.recordingRequested()
|
||||
onStopRecordingRequested: controlPanelWindow.stopRecordingRequested()
|
||||
onSystemActionRequested: function(action) { controlPanelWindow.systemActionRequested(action) }
|
||||
onPerformanceActionRequested: function(action) { controlPanelWindow.performanceActionRequested(action) }
|
||||
|
||||
// Hover state management
|
||||
onIsHoveredChanged: {
|
||||
if (isHovered) {
|
||||
hideTimer.stop()
|
||||
} else {
|
||||
hideTimer.restart()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Border integration corners (positioned to match panel edges)
|
||||
Core.Corners {
|
||||
id: controlPanelLeftCorner
|
||||
position: "bottomright"
|
||||
size: 1.3
|
||||
fillColor: Data.ThemeManager.bgColor
|
||||
offsetX: -661
|
||||
offsetY: -313
|
||||
visible: isShown
|
||||
z: 1 // Higher z-index to render above shadow effects
|
||||
|
||||
// Disable implicit animations to prevent corner sliding
|
||||
Behavior on x { enabled: false }
|
||||
Behavior on y { enabled: false }
|
||||
}
|
||||
|
||||
Core.Corners {
|
||||
id: controlPanelRightCorner
|
||||
position: "bottomleft"
|
||||
size: 1.3
|
||||
fillColor: Data.ThemeManager.bgColor
|
||||
offsetX: 661
|
||||
offsetY: -313
|
||||
visible: isShown
|
||||
z: 1 // Higher z-index to render above shadow effects
|
||||
|
||||
Behavior on x { enabled: false }
|
||||
Behavior on y { enabled: false }
|
||||
}
|
||||
|
||||
// Auto-hide timer
|
||||
Timer {
|
||||
id: hideTimer
|
||||
interval: 400
|
||||
repeat: false
|
||||
onTriggered: hide()
|
||||
}
|
||||
|
||||
function show() {
|
||||
if (isShown) return
|
||||
isShown = true
|
||||
hideTimer.stop()
|
||||
}
|
||||
|
||||
function hide() {
|
||||
if (!isShown) return
|
||||
// Only hide if on main tab and nothing is being hovered
|
||||
if (currentTab === 0 && !panelContent.isHovered && !triggerMouseArea.containsMouse) {
|
||||
isShown = false
|
||||
}
|
||||
// For non-main tabs, only hide if explicitly not hovered and no trigger hover
|
||||
else if (currentTab !== 0 && !panelContent.isHovered && !triggerMouseArea.containsMouse) {
|
||||
// Add delay for non-main tabs to prevent accidental hiding
|
||||
Qt.callLater(function() {
|
||||
if (!panelContent.isHovered && !triggerMouseArea.containsMouse) {
|
||||
isShown = false
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,107 @@
|
|||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import "root:/Data" as Data
|
||||
import "." as Controls
|
||||
|
||||
// Dual-section control panel
|
||||
Row {
|
||||
id: root
|
||||
spacing: 16
|
||||
visible: true
|
||||
height: 80
|
||||
|
||||
required property bool isRecording
|
||||
required property var shell
|
||||
signal performanceActionRequested(string action)
|
||||
signal systemActionRequested(string action)
|
||||
signal mouseChanged(bool containsMouse)
|
||||
|
||||
// Combined hover state from both sections
|
||||
property bool containsMouse: performanceSection.containsMouse || systemSection.containsMouse
|
||||
onContainsMouseChanged: mouseChanged(containsMouse)
|
||||
|
||||
// Performance controls section (left half)
|
||||
Rectangle {
|
||||
id: performanceSection
|
||||
width: (parent.width - parent.spacing) / 2
|
||||
height: parent.height
|
||||
radius: 20
|
||||
color: Qt.darker(Data.ThemeManager.bgColor, 1.15)
|
||||
visible: true
|
||||
|
||||
// Hover tracking with coordination between background and content
|
||||
property bool containsMouse: performanceMouseArea.containsMouse || performanceControls.containsMouse
|
||||
|
||||
MouseArea {
|
||||
id: performanceMouseArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
propagateComposedEvents: true
|
||||
onContainsMouseChanged: {
|
||||
if (containsMouse) {
|
||||
performanceSection.containsMouse = true
|
||||
} else if (!performanceControls.containsMouse) {
|
||||
performanceSection.containsMouse = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Controls.PerformanceControls {
|
||||
id: performanceControls
|
||||
anchors.fill: parent
|
||||
anchors.margins: 12
|
||||
shell: root.shell
|
||||
onPerformanceActionRequested: function(action) { root.performanceActionRequested(action) }
|
||||
onMouseChanged: function(containsMouse) {
|
||||
if (containsMouse) {
|
||||
performanceSection.containsMouse = true
|
||||
} else if (!performanceMouseArea.containsMouse) {
|
||||
performanceSection.containsMouse = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// System controls section (right half)
|
||||
Rectangle {
|
||||
id: systemSection
|
||||
width: (parent.width - parent.spacing) / 2
|
||||
height: parent.height
|
||||
radius: 20
|
||||
color: Qt.darker(Data.ThemeManager.bgColor, 1.15)
|
||||
visible: true
|
||||
|
||||
// Hover tracking with coordination between background and content
|
||||
property bool containsMouse: systemMouseArea.containsMouse || systemControls.containsMouse
|
||||
|
||||
MouseArea {
|
||||
id: systemMouseArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
propagateComposedEvents: true
|
||||
onContainsMouseChanged: {
|
||||
if (containsMouse) {
|
||||
systemSection.containsMouse = true
|
||||
} else if (!systemControls.containsMouse) {
|
||||
systemSection.containsMouse = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Controls.SystemControls {
|
||||
id: systemControls
|
||||
anchors.fill: parent
|
||||
anchors.margins: 12
|
||||
shell: root.shell
|
||||
onSystemActionRequested: function(action) { root.systemActionRequested(action) }
|
||||
onMouseChanged: function(containsMouse) {
|
||||
if (containsMouse) {
|
||||
systemSection.containsMouse = true
|
||||
} else if (!systemMouseArea.containsMouse) {
|
||||
systemSection.containsMouse = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,127 @@
|
|||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import Quickshell.Services.UPower
|
||||
|
||||
// Power profile controls
|
||||
Column {
|
||||
id: root
|
||||
required property var shell
|
||||
|
||||
spacing: 8
|
||||
signal performanceActionRequested(string action)
|
||||
signal mouseChanged(bool containsMouse)
|
||||
|
||||
readonly property bool containsMouse: performanceButton.containsMouse ||
|
||||
balancedButton.containsMouse ||
|
||||
powerSaverButton.containsMouse
|
||||
|
||||
// Safe UPower service access with fallback checks
|
||||
readonly property bool upowerReady: typeof PowerProfiles !== 'undefined' && PowerProfiles
|
||||
readonly property int currentProfile: upowerReady ? PowerProfiles.profile : 0
|
||||
|
||||
onContainsMouseChanged: root.mouseChanged(containsMouse)
|
||||
|
||||
opacity: visible ? 1 : 0
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: 300
|
||||
easing.type: Easing.OutCubic
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
spacing: 8
|
||||
width: parent.width
|
||||
|
||||
// Performance mode button
|
||||
SystemButton {
|
||||
id: performanceButton
|
||||
width: (parent.width - parent.spacing * 2) / 3
|
||||
height: 52
|
||||
|
||||
shell: root.shell
|
||||
iconText: "speed"
|
||||
|
||||
isActive: root.upowerReady && (typeof PowerProfile !== 'undefined') ?
|
||||
root.currentProfile === PowerProfile.Performance : false
|
||||
|
||||
onClicked: {
|
||||
if (root.upowerReady && typeof PowerProfile !== 'undefined') {
|
||||
PowerProfiles.profile = PowerProfile.Performance
|
||||
root.performanceActionRequested("performance")
|
||||
} else {
|
||||
console.warn("PowerProfiles not available")
|
||||
}
|
||||
}
|
||||
onMouseChanged: function(containsMouse) {
|
||||
if (!containsMouse && !root.containsMouse) {
|
||||
root.mouseChanged(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Balanced mode button
|
||||
SystemButton {
|
||||
id: balancedButton
|
||||
width: (parent.width - parent.spacing * 2) / 3
|
||||
height: 52
|
||||
|
||||
shell: root.shell
|
||||
iconText: "balance"
|
||||
|
||||
isActive: root.upowerReady && (typeof PowerProfile !== 'undefined') ?
|
||||
root.currentProfile === PowerProfile.Balanced : false
|
||||
|
||||
onClicked: {
|
||||
if (root.upowerReady && typeof PowerProfile !== 'undefined') {
|
||||
PowerProfiles.profile = PowerProfile.Balanced
|
||||
root.performanceActionRequested("balanced")
|
||||
} else {
|
||||
console.warn("PowerProfiles not available")
|
||||
}
|
||||
}
|
||||
onMouseChanged: function(containsMouse) {
|
||||
if (!containsMouse && !root.containsMouse) {
|
||||
root.mouseChanged(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Power saver mode button
|
||||
SystemButton {
|
||||
id: powerSaverButton
|
||||
width: (parent.width - parent.spacing * 2) / 3
|
||||
height: 52
|
||||
|
||||
shell: root.shell
|
||||
iconText: "battery_saver"
|
||||
|
||||
isActive: root.upowerReady && (typeof PowerProfile !== 'undefined') ?
|
||||
root.currentProfile === PowerProfile.PowerSaver : false
|
||||
|
||||
onClicked: {
|
||||
if (root.upowerReady && typeof PowerProfile !== 'undefined') {
|
||||
PowerProfiles.profile = PowerProfile.PowerSaver
|
||||
root.performanceActionRequested("powersaver")
|
||||
} else {
|
||||
console.warn("PowerProfiles not available")
|
||||
}
|
||||
}
|
||||
onMouseChanged: function(containsMouse) {
|
||||
if (!containsMouse && !root.containsMouse) {
|
||||
root.mouseChanged(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure UPower service initialization
|
||||
Component.onCompleted: {
|
||||
Qt.callLater(function() {
|
||||
if (!root.upowerReady) {
|
||||
console.warn("UPower service not ready - performance controls may not work correctly")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,122 @@
|
|||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import "root:/Data" as Data
|
||||
|
||||
// System button
|
||||
Rectangle {
|
||||
id: root
|
||||
required property var shell
|
||||
required property string iconText
|
||||
property string labelText: ""
|
||||
|
||||
property bool isActive: false
|
||||
|
||||
radius: 20
|
||||
|
||||
// Dynamic color based on active and hover states
|
||||
color: {
|
||||
if (isActive) {
|
||||
return mouseArea.containsMouse ?
|
||||
Qt.lighter(Data.ThemeManager.accentColor, 1.1) :
|
||||
Qt.rgba(Data.ThemeManager.accentColor.r, Data.ThemeManager.accentColor.g, Data.ThemeManager.accentColor.b, 0.3)
|
||||
} else {
|
||||
return mouseArea.containsMouse ?
|
||||
Qt.lighter(Data.ThemeManager.accentColor, 1.2) :
|
||||
Qt.lighter(Data.ThemeManager.bgColor, 1.15)
|
||||
}
|
||||
}
|
||||
|
||||
border.width: isActive ? 2 : 1
|
||||
border.color: isActive ? Data.ThemeManager.accentColor : Qt.lighter(Data.ThemeManager.bgColor, 1.3)
|
||||
|
||||
signal clicked()
|
||||
signal mouseChanged(bool containsMouse)
|
||||
property bool isHovered: mouseArea.containsMouse
|
||||
readonly property alias containsMouse: mouseArea.containsMouse
|
||||
|
||||
// Smooth color transitions
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: 200
|
||||
easing.type: Easing.OutCubic
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on border.color {
|
||||
ColorAnimation {
|
||||
duration: 200
|
||||
easing.type: Easing.OutCubic
|
||||
}
|
||||
}
|
||||
|
||||
// Hover scale animation
|
||||
scale: isHovered ? 1.05 : 1.0
|
||||
Behavior on scale {
|
||||
NumberAnimation {
|
||||
duration: 200
|
||||
easing.type: Easing.OutCubic
|
||||
}
|
||||
}
|
||||
|
||||
// Button content with icon and optional label
|
||||
Column {
|
||||
anchors.centerIn: parent
|
||||
spacing: 2
|
||||
|
||||
// System action icon
|
||||
Text {
|
||||
text: root.iconText
|
||||
font.family: "Material Symbols Outlined"
|
||||
font.pixelSize: 16
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
color: {
|
||||
if (root.isActive) {
|
||||
return root.isHovered ? "#ffffff" : Data.ThemeManager.accentColor
|
||||
} else {
|
||||
return root.isHovered ? "#ffffff" : Data.ThemeManager.accentColor
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: 200
|
||||
easing.type: Easing.OutCubic
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Optional text label
|
||||
Label {
|
||||
text: root.labelText
|
||||
font.family: "Roboto"
|
||||
font.pixelSize: 8
|
||||
color: {
|
||||
if (root.isActive) {
|
||||
return root.isHovered ? "#ffffff" : Data.ThemeManager.accentColor
|
||||
} else {
|
||||
return root.isHovered ? "#ffffff" : Data.ThemeManager.accentColor
|
||||
}
|
||||
}
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
font.weight: root.isActive ? Font.Bold : Font.Medium
|
||||
visible: root.labelText !== ""
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: 200
|
||||
easing.type: Easing.OutCubic
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Click and hover handling
|
||||
MouseArea {
|
||||
id: mouseArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
|
||||
onContainsMouseChanged: root.mouseChanged(containsMouse)
|
||||
onClicked: root.clicked()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,93 @@
|
|||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import QtQuick.Controls
|
||||
|
||||
// System action buttons
|
||||
RowLayout {
|
||||
id: root
|
||||
required property var shell
|
||||
|
||||
spacing: 8
|
||||
signal systemActionRequested(string action)
|
||||
signal mouseChanged(bool containsMouse)
|
||||
|
||||
readonly property bool containsMouse: lockButton.containsMouse ||
|
||||
rebootButton.containsMouse ||
|
||||
shutdownButton.containsMouse
|
||||
|
||||
onContainsMouseChanged: root.mouseChanged(containsMouse)
|
||||
|
||||
opacity: visible ? 1 : 0
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: 300
|
||||
easing.type: Easing.OutCubic
|
||||
}
|
||||
}
|
||||
|
||||
// Lock Button
|
||||
SystemButton {
|
||||
id: lockButton
|
||||
Layout.fillHeight: true
|
||||
Layout.fillWidth: true
|
||||
|
||||
shell: root.shell
|
||||
iconText: "lock"
|
||||
|
||||
onClicked: {
|
||||
console.log("Lock button clicked")
|
||||
console.log("root.shell:", root.shell)
|
||||
console.log("root.shell.lockscreen:", root.shell ? root.shell.lockscreen : "shell is null")
|
||||
|
||||
// Directly trigger custom lockscreen
|
||||
if (root.shell && root.shell.lockscreen) {
|
||||
console.log("Calling root.shell.lockscreen.lock()")
|
||||
root.shell.lockscreen.lock()
|
||||
} else {
|
||||
console.log("Fallback to systemActionRequested")
|
||||
// Fallback to system action for compatibility
|
||||
root.systemActionRequested("lock")
|
||||
}
|
||||
}
|
||||
onMouseChanged: function(containsMouse) {
|
||||
if (!containsMouse && !root.containsMouse) {
|
||||
root.mouseChanged(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Reboot Button
|
||||
SystemButton {
|
||||
id: rebootButton
|
||||
Layout.fillHeight: true
|
||||
Layout.fillWidth: true
|
||||
|
||||
shell: root.shell
|
||||
iconText: "restart_alt"
|
||||
|
||||
onClicked: root.systemActionRequested("reboot")
|
||||
onMouseChanged: function(containsMouse) {
|
||||
if (!containsMouse && !root.containsMouse) {
|
||||
root.mouseChanged(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Shutdown Button
|
||||
SystemButton {
|
||||
id: shutdownButton
|
||||
Layout.fillHeight: true
|
||||
Layout.fillWidth: true
|
||||
|
||||
shell: root.shell
|
||||
iconText: "power_settings_new"
|
||||
|
||||
onClicked: root.systemActionRequested("shutdown")
|
||||
onMouseChanged: function(containsMouse) {
|
||||
if (!containsMouse && !root.containsMouse) {
|
||||
root.mouseChanged(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,666 @@
|
|||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import Qt5Compat.GraphicalEffects
|
||||
import Quickshell.Services.Mpris
|
||||
import "root:/Data" as Data
|
||||
|
||||
// Music player with MPRIS integration
|
||||
Rectangle {
|
||||
id: musicPlayer
|
||||
|
||||
property var shell
|
||||
property var currentPlayer: null
|
||||
property real currentPosition: 0
|
||||
property int selectedPlayerIndex: 0
|
||||
|
||||
color: "transparent"
|
||||
|
||||
// Get all available players
|
||||
function getAvailablePlayers() {
|
||||
if (!Mpris.players || !Mpris.players.values) {
|
||||
return []
|
||||
}
|
||||
|
||||
let allPlayers = Mpris.players.values
|
||||
let controllablePlayers = []
|
||||
|
||||
for (let i = 0; i < allPlayers.length; i++) {
|
||||
let player = allPlayers[i]
|
||||
if (player && player.canControl) {
|
||||
controllablePlayers.push(player)
|
||||
}
|
||||
}
|
||||
|
||||
return controllablePlayers
|
||||
}
|
||||
|
||||
// Find the active player (either selected or first available)
|
||||
function findActivePlayer() {
|
||||
let availablePlayers = getAvailablePlayers()
|
||||
if (availablePlayers.length === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
// Auto-switch to playing player if enabled
|
||||
if (Data.Settings.autoSwitchPlayer) {
|
||||
for (let i = 0; i < availablePlayers.length; i++) {
|
||||
if (availablePlayers[i].isPlaying) {
|
||||
selectedPlayerIndex = i
|
||||
return availablePlayers[i]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Use selected player if valid, otherwise use first available
|
||||
if (selectedPlayerIndex < availablePlayers.length) {
|
||||
return availablePlayers[selectedPlayerIndex]
|
||||
} else {
|
||||
selectedPlayerIndex = 0
|
||||
return availablePlayers[0]
|
||||
}
|
||||
}
|
||||
|
||||
// Update current player
|
||||
function updateCurrentPlayer() {
|
||||
let newPlayer = findActivePlayer()
|
||||
if (newPlayer !== currentPlayer) {
|
||||
currentPlayer = newPlayer
|
||||
currentPosition = currentPlayer ? currentPlayer.position : 0
|
||||
}
|
||||
}
|
||||
|
||||
// Timer to update progress bar position
|
||||
Timer {
|
||||
id: positionTimer
|
||||
interval: 1000
|
||||
running: currentPlayer && currentPlayer.isPlaying
|
||||
repeat: true
|
||||
onTriggered: {
|
||||
if (currentPlayer) {
|
||||
currentPosition = currentPlayer.position
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Timer to check for auto-switching to playing players
|
||||
Timer {
|
||||
id: autoSwitchTimer
|
||||
interval: 2000 // Check every 2 seconds
|
||||
running: Data.Settings.autoSwitchPlayer
|
||||
repeat: true
|
||||
onTriggered: {
|
||||
if (Data.Settings.autoSwitchPlayer) {
|
||||
let availablePlayers = getAvailablePlayers()
|
||||
for (let i = 0; i < availablePlayers.length; i++) {
|
||||
if (availablePlayers[i].isPlaying && selectedPlayerIndex !== i) {
|
||||
selectedPlayerIndex = i
|
||||
updateCurrentPlayer()
|
||||
updatePlayerList()
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Update player list for dropdown
|
||||
function updatePlayerList() {
|
||||
if (!playerComboBox) return
|
||||
|
||||
let availablePlayers = getAvailablePlayers()
|
||||
let playerNames = availablePlayers.map(player => player.identity || "Unknown Player")
|
||||
|
||||
playerComboBox.model = playerNames
|
||||
|
||||
if (selectedPlayerIndex >= playerNames.length) {
|
||||
selectedPlayerIndex = 0
|
||||
}
|
||||
|
||||
playerComboBox.currentIndex = selectedPlayerIndex
|
||||
}
|
||||
|
||||
// Monitor for player changes
|
||||
Connections {
|
||||
target: Mpris.players
|
||||
function onValuesChanged() {
|
||||
updatePlayerList()
|
||||
updateCurrentPlayer()
|
||||
}
|
||||
function onRowsInserted() {
|
||||
updatePlayerList()
|
||||
updateCurrentPlayer()
|
||||
}
|
||||
function onRowsRemoved() {
|
||||
updatePlayerList()
|
||||
updateCurrentPlayer()
|
||||
}
|
||||
function onObjectInsertedPost() {
|
||||
updatePlayerList()
|
||||
updateCurrentPlayer()
|
||||
}
|
||||
function onObjectRemovedPost() {
|
||||
updatePlayerList()
|
||||
updateCurrentPlayer()
|
||||
}
|
||||
}
|
||||
|
||||
// Monitor for settings changes
|
||||
Connections {
|
||||
target: Data.Settings
|
||||
function onAutoSwitchPlayerChanged() {
|
||||
console.log("Auto-switch player setting changed to:", Data.Settings.autoSwitchPlayer)
|
||||
updateCurrentPlayer()
|
||||
}
|
||||
function onAlwaysShowPlayerDropdownChanged() {
|
||||
console.log("Always show dropdown setting changed to:", Data.Settings.alwaysShowPlayerDropdown)
|
||||
// Dropdown visibility is automatically handled by the binding
|
||||
}
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
updatePlayerList()
|
||||
updateCurrentPlayer()
|
||||
}
|
||||
|
||||
Column {
|
||||
anchors.fill: parent
|
||||
spacing: 10
|
||||
|
||||
// No music player available state
|
||||
Item {
|
||||
width: parent.width
|
||||
height: parent.height
|
||||
visible: !currentPlayer
|
||||
|
||||
Column {
|
||||
anchors.centerIn: parent
|
||||
spacing: 16
|
||||
|
||||
Text {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
text: "music_note"
|
||||
font.family: "Material Symbols Outlined"
|
||||
font.pixelSize: 48
|
||||
color: Qt.rgba(Data.ThemeManager.fgColor.r, Data.ThemeManager.fgColor.g, Data.ThemeManager.fgColor.b, 0.3)
|
||||
}
|
||||
|
||||
Text {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
text: getAvailablePlayers().length > 0 ? "No controllable player selected" : "No music player detected"
|
||||
color: Qt.rgba(Data.ThemeManager.fgColor.r, Data.ThemeManager.fgColor.g, Data.ThemeManager.fgColor.b, 0.6)
|
||||
font.family: "Roboto"
|
||||
font.pixelSize: 14
|
||||
}
|
||||
|
||||
Text {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
text: getAvailablePlayers().length > 0 ? "Select a player from the dropdown above" : "Start a music player to see controls"
|
||||
color: Qt.rgba(Data.ThemeManager.fgColor.r, Data.ThemeManager.fgColor.g, Data.ThemeManager.fgColor.b, 0.4)
|
||||
font.family: "Roboto"
|
||||
font.pixelSize: 12
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Music player controls
|
||||
Column {
|
||||
width: parent.width
|
||||
spacing: 12
|
||||
visible: currentPlayer
|
||||
|
||||
// Player info and artwork
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: 130
|
||||
radius: 20
|
||||
color: Qt.darker(Data.ThemeManager.bgColor, 1.1)
|
||||
border.color: Qt.rgba(Data.ThemeManager.accentColor.r, Data.ThemeManager.accentColor.g, Data.ThemeManager.accentColor.b, 0.2)
|
||||
border.width: 1
|
||||
|
||||
Row {
|
||||
anchors.fill: parent
|
||||
anchors.margins: 16
|
||||
spacing: 16
|
||||
|
||||
// Album artwork
|
||||
Rectangle {
|
||||
id: albumArtwork
|
||||
width: 90
|
||||
height: 90
|
||||
radius: 20
|
||||
color: Qt.darker(Data.ThemeManager.bgColor, 1.3)
|
||||
border.color: Qt.rgba(Data.ThemeManager.accentColor.r, Data.ThemeManager.accentColor.g, Data.ThemeManager.accentColor.b, 0.3)
|
||||
border.width: 1
|
||||
|
||||
Image {
|
||||
id: albumArt
|
||||
anchors.fill: parent
|
||||
anchors.margins: 2
|
||||
fillMode: Image.PreserveAspectCrop
|
||||
smooth: true
|
||||
source: currentPlayer ? (currentPlayer.trackArtUrl || "") : ""
|
||||
visible: source.toString() !== ""
|
||||
|
||||
// Rounded corners using layer
|
||||
layer.enabled: true
|
||||
layer.effect: OpacityMask {
|
||||
cached: true // Cache to reduce ShaderEffect issues
|
||||
maskSource: Rectangle {
|
||||
width: albumArt.width
|
||||
height: albumArt.height
|
||||
radius: 20
|
||||
visible: false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback music icon
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
text: "album"
|
||||
font.family: "Material Symbols Outlined"
|
||||
font.pixelSize: 32
|
||||
color: Qt.rgba(Data.ThemeManager.fgColor.r, Data.ThemeManager.fgColor.g, Data.ThemeManager.fgColor.b, 0.4)
|
||||
visible: !albumArt.visible
|
||||
}
|
||||
}
|
||||
|
||||
// Track info
|
||||
Column {
|
||||
width: parent.width - albumArtwork.width - parent.spacing
|
||||
height: parent.height
|
||||
spacing: 4
|
||||
|
||||
Text {
|
||||
width: parent.width
|
||||
text: currentPlayer ? (currentPlayer.trackTitle || "Unknown Track") : ""
|
||||
color: Data.ThemeManager.fgColor
|
||||
font.family: "Roboto"
|
||||
font.pixelSize: 18
|
||||
font.bold: true
|
||||
elide: Text.ElideRight
|
||||
wrapMode: Text.Wrap
|
||||
maximumLineCount: 2
|
||||
}
|
||||
|
||||
Text {
|
||||
width: parent.width
|
||||
text: currentPlayer ? (currentPlayer.trackArtist || "Unknown Artist") : ""
|
||||
color: Qt.rgba(Data.ThemeManager.fgColor.r, Data.ThemeManager.fgColor.g, Data.ThemeManager.fgColor.b, 0.8)
|
||||
font.family: "Roboto"
|
||||
font.pixelSize: 18
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
|
||||
Text {
|
||||
width: parent.width
|
||||
text: currentPlayer ? (currentPlayer.trackAlbum || "Unknown Album") : ""
|
||||
color: Qt.rgba(Data.ThemeManager.fgColor.r, Data.ThemeManager.fgColor.g, Data.ThemeManager.fgColor.b, 0.6)
|
||||
font.family: "Roboto"
|
||||
font.pixelSize: 15
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Interactive progress bar with seek functionality
|
||||
Rectangle {
|
||||
id: progressBarBackground
|
||||
width: parent.width
|
||||
height: 8
|
||||
radius: 20
|
||||
color: Qt.rgba(Data.ThemeManager.fgColor.r, Data.ThemeManager.fgColor.g, Data.ThemeManager.fgColor.b, 0.15)
|
||||
|
||||
property real progressRatio: currentPlayer && currentPlayer.length > 0 ?
|
||||
(currentPosition / currentPlayer.length) : 0
|
||||
|
||||
Rectangle {
|
||||
id: progressFill
|
||||
width: progressBarBackground.progressRatio * parent.width
|
||||
height: parent.height
|
||||
radius: parent.radius
|
||||
color: Data.ThemeManager.accentColor
|
||||
|
||||
Behavior on width {
|
||||
NumberAnimation { duration: 200 }
|
||||
}
|
||||
}
|
||||
|
||||
// Interactive progress handle (circle)
|
||||
Rectangle {
|
||||
id: progressHandle
|
||||
width: 16
|
||||
height: 16
|
||||
radius: 8
|
||||
color: Data.ThemeManager.accentColor
|
||||
border.color: Qt.lighter(Data.ThemeManager.accentColor, 1.3)
|
||||
border.width: 1
|
||||
|
||||
x: Math.max(0, Math.min(parent.width - width, progressFill.width - width/2))
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
visible: currentPlayer && currentPlayer.length > 0
|
||||
scale: progressMouseArea.containsMouse || progressMouseArea.pressed ? 1.2 : 1.0
|
||||
|
||||
Behavior on scale {
|
||||
NumberAnimation { duration: 150 }
|
||||
}
|
||||
}
|
||||
|
||||
// Mouse area for seeking
|
||||
MouseArea {
|
||||
id: progressMouseArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
enabled: currentPlayer && currentPlayer.length > 0 && currentPlayer.canSeek
|
||||
|
||||
onClicked: function(mouse) {
|
||||
if (currentPlayer && currentPlayer.length > 0) {
|
||||
let ratio = mouse.x / width
|
||||
let seekPosition = ratio * currentPlayer.length
|
||||
currentPlayer.position = seekPosition
|
||||
currentPosition = seekPosition
|
||||
}
|
||||
}
|
||||
|
||||
onPositionChanged: function(mouse) {
|
||||
if (pressed && currentPlayer && currentPlayer.length > 0) {
|
||||
let ratio = Math.max(0, Math.min(1, mouse.x / width))
|
||||
let seekPosition = ratio * currentPlayer.length
|
||||
currentPlayer.position = seekPosition
|
||||
currentPosition = seekPosition
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Player selection dropdown (conditional visibility)
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: 38
|
||||
radius: 20
|
||||
color: Qt.darker(Data.ThemeManager.bgColor, 1.1)
|
||||
border.color: Qt.rgba(Data.ThemeManager.accentColor.r, Data.ThemeManager.accentColor.g, Data.ThemeManager.accentColor.b, 0.2)
|
||||
border.width: 1
|
||||
visible: {
|
||||
let playerCount = getAvailablePlayers().length
|
||||
let alwaysShow = Data.Settings.alwaysShowPlayerDropdown
|
||||
let shouldShow = alwaysShow || playerCount > 1
|
||||
return shouldShow
|
||||
}
|
||||
|
||||
Row {
|
||||
anchors.fill: parent
|
||||
anchors.margins: 6
|
||||
anchors.leftMargin: 12
|
||||
spacing: 8
|
||||
|
||||
Text {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: "Player:"
|
||||
color: Qt.rgba(Data.ThemeManager.fgColor.r, Data.ThemeManager.fgColor.g, Data.ThemeManager.fgColor.b, 0.7)
|
||||
font.family: "Roboto"
|
||||
font.pixelSize: 12
|
||||
font.bold: true
|
||||
}
|
||||
|
||||
ComboBox {
|
||||
id: playerComboBox
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
width: parent.width - parent.children[0].width - parent.spacing
|
||||
height: 26
|
||||
model: []
|
||||
|
||||
onActivated: function(index) {
|
||||
selectedPlayerIndex = index
|
||||
updateCurrentPlayer()
|
||||
}
|
||||
|
||||
background: Rectangle {
|
||||
color: Qt.darker(Data.ThemeManager.bgColor, 1.3)
|
||||
border.color: Qt.rgba(Data.ThemeManager.accentColor.r, Data.ThemeManager.accentColor.g, Data.ThemeManager.accentColor.b, 0.2)
|
||||
border.width: 1
|
||||
radius: 20
|
||||
}
|
||||
|
||||
contentItem: Text {
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: 10
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: 22
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: playerComboBox.currentText || "No players"
|
||||
color: Data.ThemeManager.fgColor
|
||||
font.family: "Roboto"
|
||||
font.pixelSize: 12
|
||||
font.bold: true
|
||||
elide: Text.ElideRight
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
}
|
||||
|
||||
indicator: Text {
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: 4
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: "expand_more"
|
||||
font.family: "Material Symbols Outlined"
|
||||
font.pixelSize: 12
|
||||
color: Qt.rgba(Data.ThemeManager.fgColor.r, Data.ThemeManager.fgColor.g, Data.ThemeManager.fgColor.b, 0.6)
|
||||
}
|
||||
|
||||
popup: Popup {
|
||||
y: playerComboBox.height + 2
|
||||
width: playerComboBox.width
|
||||
implicitHeight: contentItem.implicitHeight + 4
|
||||
|
||||
background: Rectangle {
|
||||
color: Qt.darker(Data.ThemeManager.bgColor, 1.2)
|
||||
border.color: Qt.rgba(Data.ThemeManager.accentColor.r, Data.ThemeManager.accentColor.g, Data.ThemeManager.accentColor.b, 0.3)
|
||||
border.width: 1
|
||||
radius: 20
|
||||
}
|
||||
|
||||
contentItem: ListView {
|
||||
clip: true
|
||||
implicitHeight: contentHeight
|
||||
model: playerComboBox.popup.visible ? playerComboBox.delegateModel : null
|
||||
currentIndex: playerComboBox.highlightedIndex
|
||||
|
||||
ScrollIndicator.vertical: ScrollIndicator { }
|
||||
}
|
||||
}
|
||||
|
||||
delegate: ItemDelegate {
|
||||
width: playerComboBox.width
|
||||
height: 28
|
||||
|
||||
background: Rectangle {
|
||||
color: parent.hovered ? Qt.rgba(Data.ThemeManager.accentColor.r, Data.ThemeManager.accentColor.g, Data.ThemeManager.accentColor.b, 0.15) : "transparent"
|
||||
radius: 20
|
||||
}
|
||||
|
||||
contentItem: Text {
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: 10
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: modelData || ""
|
||||
color: Data.ThemeManager.fgColor
|
||||
font.family: "Roboto"
|
||||
font.pixelSize: 12
|
||||
font.bold: true
|
||||
elide: Text.ElideRight
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Media controls
|
||||
Row {
|
||||
width: parent.width
|
||||
height: 35
|
||||
spacing: 6
|
||||
|
||||
// Previous button
|
||||
Rectangle {
|
||||
width: (parent.width - parent.spacing * 4) * 0.2
|
||||
height: parent.height
|
||||
radius: height / 2
|
||||
color: previousButton.containsMouse ? Qt.rgba(Data.ThemeManager.accentColor.r, Data.ThemeManager.accentColor.g, Data.ThemeManager.accentColor.b, 0.2) : Qt.darker(Data.ThemeManager.bgColor, 1.1)
|
||||
border.color: Qt.rgba(Data.ThemeManager.accentColor.r, Data.ThemeManager.accentColor.g, Data.ThemeManager.accentColor.b, 0.3)
|
||||
border.width: 1
|
||||
|
||||
MouseArea {
|
||||
id: previousButton
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
enabled: currentPlayer && currentPlayer.canGoPrevious
|
||||
onClicked: if (currentPlayer) currentPlayer.previous()
|
||||
}
|
||||
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
text: "skip_previous"
|
||||
font.family: "Material Symbols Outlined"
|
||||
font.pixelSize: 18
|
||||
color: previousButton.enabled ? Data.ThemeManager.accentColor : Qt.rgba(Data.ThemeManager.fgColor.r, Data.ThemeManager.fgColor.g, Data.ThemeManager.fgColor.b, 0.3)
|
||||
}
|
||||
}
|
||||
|
||||
// Play/Pause button
|
||||
Rectangle {
|
||||
width: (parent.width - parent.spacing * 4) * 0.3
|
||||
height: parent.height
|
||||
radius: height / 2
|
||||
color: playButton.containsMouse ? Qt.rgba(Data.ThemeManager.accentColor.r, Data.ThemeManager.accentColor.g, Data.ThemeManager.accentColor.b, 0.2) : Qt.darker(Data.ThemeManager.bgColor, 1.1)
|
||||
border.color: Data.ThemeManager.accentColor
|
||||
border.width: 2
|
||||
|
||||
MouseArea {
|
||||
id: playButton
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
enabled: currentPlayer && (currentPlayer.canPlay || currentPlayer.canPause)
|
||||
onClicked: {
|
||||
if (currentPlayer) {
|
||||
if (currentPlayer.isPlaying) {
|
||||
currentPlayer.pause()
|
||||
} else {
|
||||
currentPlayer.play()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
text: currentPlayer && currentPlayer.isPlaying ? "pause" : "play_arrow"
|
||||
font.family: "Material Symbols Outlined"
|
||||
font.pixelSize: 20
|
||||
color: playButton.enabled ? Data.ThemeManager.accentColor : Qt.rgba(Data.ThemeManager.fgColor.r, Data.ThemeManager.fgColor.g, Data.ThemeManager.fgColor.b, 0.3)
|
||||
}
|
||||
}
|
||||
|
||||
// Next button
|
||||
Rectangle {
|
||||
width: (parent.width - parent.spacing * 4) * 0.2
|
||||
height: parent.height
|
||||
radius: height / 2
|
||||
color: nextButton.containsMouse ? Qt.rgba(Data.ThemeManager.accentColor.r, Data.ThemeManager.accentColor.g, Data.ThemeManager.accentColor.b, 0.2) : Qt.darker(Data.ThemeManager.bgColor, 1.1)
|
||||
border.color: Qt.rgba(Data.ThemeManager.accentColor.r, Data.ThemeManager.accentColor.g, Data.ThemeManager.accentColor.b, 0.3)
|
||||
border.width: 1
|
||||
|
||||
MouseArea {
|
||||
id: nextButton
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
enabled: currentPlayer && currentPlayer.canGoNext
|
||||
onClicked: if (currentPlayer) currentPlayer.next()
|
||||
}
|
||||
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
text: "skip_next"
|
||||
font.family: "Material Symbols Outlined"
|
||||
font.pixelSize: 18
|
||||
color: nextButton.enabled ? Data.ThemeManager.accentColor : Qt.rgba(Data.ThemeManager.fgColor.r, Data.ThemeManager.fgColor.g, Data.ThemeManager.fgColor.b, 0.3)
|
||||
}
|
||||
}
|
||||
|
||||
// Shuffle button
|
||||
Rectangle {
|
||||
width: (parent.width - parent.spacing * 4) * 0.15
|
||||
height: parent.height
|
||||
radius: height / 2
|
||||
color: shuffleButton.containsMouse ? Qt.rgba(Data.ThemeManager.accentColor.r, Data.ThemeManager.accentColor.g, Data.ThemeManager.accentColor.b, 0.2) : Qt.darker(Data.ThemeManager.bgColor, 1.1)
|
||||
border.color: currentPlayer && currentPlayer.shuffle ? Data.ThemeManager.accentColor : Qt.rgba(Data.ThemeManager.accentColor.r, Data.ThemeManager.accentColor.g, Data.ThemeManager.accentColor.b, 0.3)
|
||||
border.width: 1
|
||||
|
||||
MouseArea {
|
||||
id: shuffleButton
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
enabled: currentPlayer && currentPlayer.canControl && currentPlayer.shuffleSupported
|
||||
onClicked: {
|
||||
if (currentPlayer && currentPlayer.shuffleSupported) {
|
||||
currentPlayer.shuffle = !currentPlayer.shuffle
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
text: "shuffle"
|
||||
font.family: "Material Symbols Outlined"
|
||||
font.pixelSize: 12
|
||||
color: shuffleButton.enabled ?
|
||||
(currentPlayer && currentPlayer.shuffle ? Data.ThemeManager.accentColor : Qt.rgba(Data.ThemeManager.fgColor.r, Data.ThemeManager.fgColor.g, Data.ThemeManager.fgColor.b, 0.6)) :
|
||||
Qt.rgba(Data.ThemeManager.fgColor.r, Data.ThemeManager.fgColor.g, Data.ThemeManager.fgColor.b, 0.3)
|
||||
}
|
||||
}
|
||||
|
||||
// Repeat button
|
||||
Rectangle {
|
||||
width: (parent.width - parent.spacing * 4) * 0.15
|
||||
height: parent.height
|
||||
radius: height / 2
|
||||
color: repeatButton.containsMouse ? Qt.rgba(Data.ThemeManager.accentColor.r, Data.ThemeManager.accentColor.g, Data.ThemeManager.accentColor.b, 0.2) : Qt.darker(Data.ThemeManager.bgColor, 1.1)
|
||||
border.color: currentPlayer && currentPlayer.loopState !== MprisLoopState.None ? Data.ThemeManager.accentColor : Qt.rgba(Data.ThemeManager.accentColor.r, Data.ThemeManager.accentColor.g, Data.ThemeManager.accentColor.b, 0.3)
|
||||
border.width: 1
|
||||
|
||||
MouseArea {
|
||||
id: repeatButton
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
enabled: currentPlayer && currentPlayer.canControl && currentPlayer.loopSupported
|
||||
onClicked: {
|
||||
if (currentPlayer && currentPlayer.loopSupported) {
|
||||
if (currentPlayer.loopState === MprisLoopState.None) {
|
||||
currentPlayer.loopState = MprisLoopState.Track
|
||||
} else if (currentPlayer.loopState === MprisLoopState.Track) {
|
||||
currentPlayer.loopState = MprisLoopState.Playlist
|
||||
} else {
|
||||
currentPlayer.loopState = MprisLoopState.None
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
text: currentPlayer && currentPlayer.loopState === MprisLoopState.Track ? "repeat_one" : "repeat"
|
||||
font.family: "Material Symbols Outlined"
|
||||
font.pixelSize: 12
|
||||
color: repeatButton.enabled ?
|
||||
(currentPlayer && currentPlayer.loopState !== MprisLoopState.None ? Data.ThemeManager.accentColor : Qt.rgba(Data.ThemeManager.fgColor.r, Data.ThemeManager.fgColor.g, Data.ThemeManager.fgColor.b, 0.3)) :
|
||||
Qt.rgba(Data.ThemeManager.fgColor.r, Data.ThemeManager.fgColor.g, Data.ThemeManager.fgColor.b, 0.3)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,125 @@
|
|||
import QtQuick
|
||||
import "../../tabs" as Tabs
|
||||
|
||||
// Tab container with sliding animation
|
||||
Item {
|
||||
id: tabContainer
|
||||
|
||||
// Properties from parent
|
||||
required property var shell
|
||||
required property bool isRecording
|
||||
required property var triggerMouseArea
|
||||
property int currentTab: 0
|
||||
|
||||
// Signals to forward
|
||||
signal recordingRequested()
|
||||
signal stopRecordingRequested()
|
||||
signal systemActionRequested(string action)
|
||||
signal performanceActionRequested(string action)
|
||||
|
||||
// Hover detection combining all tab hovers
|
||||
property bool isHovered: {
|
||||
const tabHovers = [
|
||||
mainDashboard.isHovered,
|
||||
true, // Calendar tab should stay open when active
|
||||
true, // Clipboard tab should stay open when active
|
||||
true, // Notification tab should stay open when active
|
||||
true, // Wallpaper tab should stay open when active
|
||||
true, // Music tab should stay open when active
|
||||
true // Settings tab should stay open when active
|
||||
]
|
||||
return tabHovers[currentTab] || false
|
||||
}
|
||||
|
||||
// Track when text inputs have focus for keyboard management
|
||||
property bool textInputFocused: currentTab === 6 && settingsTab.anyTextInputFocused
|
||||
|
||||
clip: true
|
||||
|
||||
// Sliding content container
|
||||
Row {
|
||||
id: slidingRow
|
||||
width: parent.width * 7 // 7 tabs wide
|
||||
height: parent.height
|
||||
spacing: 0
|
||||
|
||||
// Animate horizontal position based on current tab
|
||||
x: -tabContainer.currentTab * tabContainer.width
|
||||
|
||||
Behavior on x {
|
||||
NumberAnimation {
|
||||
duration: 300
|
||||
easing.type: Easing.OutCubic
|
||||
}
|
||||
}
|
||||
|
||||
// Tab 0: Main Dashboard
|
||||
Tabs.MainDashboard {
|
||||
id: mainDashboard
|
||||
width: tabContainer.width
|
||||
height: parent.height
|
||||
|
||||
shell: tabContainer.shell
|
||||
isRecording: tabContainer.isRecording
|
||||
triggerMouseArea: tabContainer.triggerMouseArea
|
||||
|
||||
onRecordingRequested: tabContainer.recordingRequested()
|
||||
onStopRecordingRequested: tabContainer.stopRecordingRequested()
|
||||
onSystemActionRequested: function(action) { tabContainer.systemActionRequested(action) }
|
||||
onPerformanceActionRequested: function(action) { tabContainer.performanceActionRequested(action) }
|
||||
}
|
||||
|
||||
// Tab 1: Calendar
|
||||
Tabs.CalendarTab {
|
||||
id: calendarTab
|
||||
width: tabContainer.width
|
||||
height: parent.height
|
||||
shell: tabContainer.shell
|
||||
isActive: tabContainer.currentTab === 1 || Math.abs(tabContainer.currentTab - 1) <= 1
|
||||
}
|
||||
|
||||
// Tab 2: Clipboard
|
||||
Tabs.ClipboardTab {
|
||||
id: clipboardTab
|
||||
width: tabContainer.width
|
||||
height: parent.height
|
||||
shell: tabContainer.shell
|
||||
isActive: tabContainer.currentTab === 2 || Math.abs(tabContainer.currentTab - 2) <= 1
|
||||
}
|
||||
|
||||
// Tab 3: Notifications
|
||||
Tabs.NotificationTab {
|
||||
id: notificationTab
|
||||
width: tabContainer.width
|
||||
height: parent.height
|
||||
shell: tabContainer.shell
|
||||
isActive: tabContainer.currentTab === 3 || Math.abs(tabContainer.currentTab - 3) <= 1
|
||||
}
|
||||
|
||||
// Tab 4: Wallpapers
|
||||
Tabs.WallpaperTab {
|
||||
id: wallpaperTab
|
||||
width: tabContainer.width
|
||||
height: parent.height
|
||||
isActive: tabContainer.currentTab === 4 || Math.abs(tabContainer.currentTab - 4) <= 1
|
||||
}
|
||||
|
||||
// Tab 5: Music
|
||||
Tabs.MusicTab {
|
||||
id: musicTab
|
||||
width: tabContainer.width
|
||||
height: parent.height
|
||||
shell: tabContainer.shell
|
||||
isActive: tabContainer.currentTab === 5 || Math.abs(tabContainer.currentTab - 5) <= 1
|
||||
}
|
||||
|
||||
// Tab 6: Settings
|
||||
Tabs.SettingsTab {
|
||||
id: settingsTab
|
||||
width: tabContainer.width
|
||||
height: parent.height
|
||||
shell: tabContainer.shell
|
||||
isActive: tabContainer.currentTab === 6 || Math.abs(tabContainer.currentTab - 6) <= 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,132 @@
|
|||
import QtQuick
|
||||
import "root:/Data" as Data
|
||||
|
||||
// Tab navigation sidebar
|
||||
Item {
|
||||
id: tabNavigation
|
||||
|
||||
property int currentTab: 0
|
||||
property var tabIcons: []
|
||||
property bool containsMouse: sidebarMouseArea.containsMouse || tabColumn.containsMouse
|
||||
|
||||
MouseArea {
|
||||
id: sidebarMouseArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
propagateComposedEvents: true
|
||||
}
|
||||
|
||||
// Tab button background - matches system controls
|
||||
Rectangle {
|
||||
width: 38
|
||||
height: tabColumn.height + 12
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.top: parent.top
|
||||
color: Qt.lighter(Data.ThemeManager.bgColor, 1.15)
|
||||
radius: 19
|
||||
border.width: 1
|
||||
border.color: Qt.lighter(Data.ThemeManager.bgColor, 1.3)
|
||||
|
||||
// Subtle inner shadow effect
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
anchors.margins: 1
|
||||
color: Qt.darker(Data.ThemeManager.bgColor, 1.05)
|
||||
radius: parent.radius - 1
|
||||
opacity: 0.3
|
||||
}
|
||||
}
|
||||
|
||||
// Tab icon buttons
|
||||
Column {
|
||||
id: tabColumn
|
||||
spacing: 6
|
||||
anchors.top: parent.top
|
||||
anchors.topMargin: 6
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
|
||||
property bool containsMouse: {
|
||||
for (let i = 0; i < tabRepeater.count; i++) {
|
||||
const tab = tabRepeater.itemAt(i)
|
||||
if (tab && tab.mouseArea && tab.mouseArea.containsMouse) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
Repeater {
|
||||
id: tabRepeater
|
||||
model: 7
|
||||
delegate: Rectangle {
|
||||
width: 30
|
||||
height: 30
|
||||
radius: 15
|
||||
|
||||
// Dynamic background based on state
|
||||
color: {
|
||||
if (tabNavigation.currentTab === index) {
|
||||
return Data.ThemeManager.accentColor
|
||||
} else if (tabMouseArea.containsMouse) {
|
||||
return Qt.rgba(Data.ThemeManager.accentColor.r, Data.ThemeManager.accentColor.g, Data.ThemeManager.accentColor.b, 0.15)
|
||||
} else {
|
||||
return "transparent"
|
||||
}
|
||||
}
|
||||
|
||||
// Subtle shadow for active tab
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
radius: parent.radius
|
||||
color: "transparent"
|
||||
border.color: Qt.rgba(Data.ThemeManager.accentColor.r, Data.ThemeManager.accentColor.g, Data.ThemeManager.accentColor.b, 0.3)
|
||||
border.width: tabNavigation.currentTab === index ? 0 : (tabMouseArea.containsMouse ? 1 : 0)
|
||||
visible: tabNavigation.currentTab !== index
|
||||
}
|
||||
|
||||
property alias mouseArea: tabMouseArea
|
||||
|
||||
MouseArea {
|
||||
id: tabMouseArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
onClicked: {
|
||||
tabNavigation.currentTab = index
|
||||
}
|
||||
}
|
||||
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
text: tabNavigation.tabIcons[index] || ""
|
||||
font.family: "Material Symbols Outlined"
|
||||
font.pixelSize: 16
|
||||
color: {
|
||||
if (tabNavigation.currentTab === index) {
|
||||
return Data.ThemeManager.bgColor
|
||||
} else if (tabMouseArea.containsMouse) {
|
||||
return Data.ThemeManager.accentColor
|
||||
} else {
|
||||
return Qt.rgba(Data.ThemeManager.fgColor.r, Data.ThemeManager.fgColor.g, Data.ThemeManager.fgColor.b, 0.7)
|
||||
}
|
||||
}
|
||||
|
||||
// Smooth color transitions
|
||||
Behavior on color {
|
||||
ColorAnimation { duration: 150 }
|
||||
}
|
||||
}
|
||||
|
||||
// Smooth transitions
|
||||
Behavior on color {
|
||||
ColorAnimation { duration: 150 }
|
||||
}
|
||||
|
||||
// Subtle scale effect on hover
|
||||
scale: tabMouseArea.containsMouse ? 1.05 : 1.0
|
||||
Behavior on scale {
|
||||
NumberAnimation { duration: 150; easing.type: Easing.OutCubic }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,622 @@
|
|||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import "root:/Data" as Data
|
||||
|
||||
// Appearance settings content
|
||||
Column {
|
||||
width: parent.width
|
||||
spacing: 20
|
||||
|
||||
// Theme Setting in Collapsible Section
|
||||
SettingsCategory {
|
||||
width: parent.width
|
||||
title: "Theme Setting"
|
||||
icon: "palette"
|
||||
|
||||
content: Component {
|
||||
Column {
|
||||
width: parent.width
|
||||
spacing: 30 // Increased spacing between major sections
|
||||
|
||||
// Dark/Light Mode Switch
|
||||
Column {
|
||||
width: parent.width
|
||||
spacing: 12
|
||||
|
||||
Text {
|
||||
text: "Theme Mode"
|
||||
color: Data.ThemeManager.fgColor
|
||||
font.pixelSize: 15
|
||||
font.bold: true
|
||||
font.family: "Roboto"
|
||||
}
|
||||
|
||||
Row {
|
||||
spacing: 16
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
|
||||
Text {
|
||||
text: "Light"
|
||||
color: Data.ThemeManager.fgColor
|
||||
font.pixelSize: 14
|
||||
font.family: "Roboto"
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
// Toggle switch - enhanced design
|
||||
Rectangle {
|
||||
width: 64
|
||||
height: 32
|
||||
radius: 16
|
||||
color: Data.ThemeManager.currentTheme.type === "dark" ?
|
||||
Qt.lighter(Data.ThemeManager.accentColor, 0.8) :
|
||||
Qt.rgba(Data.ThemeManager.fgColor.r, Data.ThemeManager.fgColor.g, Data.ThemeManager.fgColor.b, 0.2)
|
||||
border.width: 2
|
||||
border.color: Data.ThemeManager.currentTheme.type === "dark" ?
|
||||
Data.ThemeManager.accentColor :
|
||||
Qt.rgba(Data.ThemeManager.fgColor.r, Data.ThemeManager.fgColor.g, Data.ThemeManager.fgColor.b, 0.4)
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
// Inner track shadow
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
anchors.margins: 2
|
||||
radius: parent.radius - 2
|
||||
color: "transparent"
|
||||
border.width: 1
|
||||
border.color: Qt.rgba(0, 0, 0, 0.1)
|
||||
}
|
||||
|
||||
// Toggle handle
|
||||
Rectangle {
|
||||
id: toggleHandle
|
||||
width: 26
|
||||
height: 26
|
||||
radius: 13
|
||||
color: Data.ThemeManager.currentTheme.type === "dark" ?
|
||||
Data.ThemeManager.bgColor : Data.ThemeManager.panelBackground
|
||||
border.width: 2
|
||||
border.color: Data.ThemeManager.currentTheme.type === "dark" ?
|
||||
Data.ThemeManager.accentColor : Data.ThemeManager.fgColor
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
x: Data.ThemeManager.currentTheme.type === "dark" ? parent.width - width - 3 : 3
|
||||
|
||||
// Handle shadow
|
||||
Rectangle {
|
||||
anchors.centerIn: parent
|
||||
anchors.verticalCenterOffset: 1
|
||||
width: parent.width - 2
|
||||
height: parent.height - 2
|
||||
radius: parent.radius - 1
|
||||
color: Qt.rgba(0, 0, 0, 0.1)
|
||||
z: -1
|
||||
}
|
||||
|
||||
// Handle highlight
|
||||
Rectangle {
|
||||
anchors.centerIn: parent
|
||||
width: parent.width - 6
|
||||
height: parent.height - 6
|
||||
radius: parent.radius - 3
|
||||
color: Qt.rgba(255, 255, 255, 0.15)
|
||||
}
|
||||
|
||||
Behavior on x {
|
||||
NumberAnimation {
|
||||
duration: 250
|
||||
easing.type: Easing.OutBack
|
||||
easing.overshoot: 0.3
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on border.color {
|
||||
ColorAnimation { duration: 200 }
|
||||
}
|
||||
}
|
||||
|
||||
// Background color transition
|
||||
Behavior on color {
|
||||
ColorAnimation { duration: 200 }
|
||||
}
|
||||
|
||||
Behavior on border.color {
|
||||
ColorAnimation { duration: 200 }
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
|
||||
onClicked: {
|
||||
console.log("Theme switch clicked, current:", Data.ThemeManager.currentThemeId)
|
||||
var currentFamily = Data.ThemeManager.currentThemeId.replace(/_dark$|_light$/, "")
|
||||
var newType = Data.ThemeManager.currentTheme.type === "dark" ? "light" : "dark"
|
||||
var newThemeId = currentFamily + "_" + newType
|
||||
console.log("Switching to:", newThemeId)
|
||||
Data.ThemeManager.setTheme(newThemeId)
|
||||
|
||||
// Force update the settings if currentTheme isn't being saved properly
|
||||
if (!Data.Settings.currentTheme) {
|
||||
Data.Settings.currentTheme = newThemeId
|
||||
Data.Settings.saveSettings()
|
||||
}
|
||||
}
|
||||
|
||||
onEntered: {
|
||||
parent.scale = 1.05
|
||||
}
|
||||
|
||||
onExited: {
|
||||
parent.scale = 1.0
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on scale {
|
||||
NumberAnimation { duration: 150; easing.type: Easing.OutQuad }
|
||||
}
|
||||
}
|
||||
|
||||
Text {
|
||||
text: "Dark"
|
||||
color: Data.ThemeManager.fgColor
|
||||
font.pixelSize: 14
|
||||
font.family: "Roboto"
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Separator
|
||||
Rectangle {
|
||||
width: parent.width - 40
|
||||
height: 1
|
||||
color: Qt.rgba(Data.ThemeManager.fgColor.r, Data.ThemeManager.fgColor.g, Data.ThemeManager.fgColor.b, 0.1)
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
}
|
||||
|
||||
// Theme Selection
|
||||
Column {
|
||||
width: parent.width
|
||||
spacing: 12
|
||||
|
||||
Text {
|
||||
text: "Theme Family"
|
||||
color: Data.ThemeManager.fgColor
|
||||
font.pixelSize: 15
|
||||
font.bold: true
|
||||
font.family: "Roboto"
|
||||
}
|
||||
|
||||
Text {
|
||||
text: "Choose your preferred theme family"
|
||||
color: Qt.rgba(Data.ThemeManager.fgColor.r, Data.ThemeManager.fgColor.g, Data.ThemeManager.fgColor.b, 0.7)
|
||||
font.pixelSize: 13
|
||||
font.family: "Roboto"
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
}
|
||||
|
||||
// Compact 2x2 grid for themes
|
||||
GridLayout {
|
||||
columns: 2
|
||||
columnSpacing: 8
|
||||
rowSpacing: 8
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
|
||||
property var themeFamily: {
|
||||
var currentFamily = Data.ThemeManager.currentThemeId.replace(/_dark$|_light$/, "")
|
||||
return currentFamily
|
||||
}
|
||||
|
||||
property var themeFamilies: [
|
||||
{ id: "oxocarbon", name: "Oxocarbon", description: "IBM Carbon" },
|
||||
{ id: "dracula", name: "Dracula", description: "Vibrant" },
|
||||
{ id: "gruvbox", name: "Gruvbox", description: "Retro" },
|
||||
{ id: "catppuccin", name: "Catppuccin", description: "Pastel" },
|
||||
{ id: "matugen", name: "Matugen", description: "Generated" }
|
||||
]
|
||||
|
||||
Repeater {
|
||||
model: parent.themeFamilies
|
||||
delegate: Rectangle {
|
||||
Layout.preferredWidth: 140
|
||||
Layout.preferredHeight: 50
|
||||
radius: 10
|
||||
color: parent.themeFamily === modelData.id ?
|
||||
Data.ThemeManager.accentColor : Qt.lighter(Data.ThemeManager.bgColor, 1.15)
|
||||
border.width: parent.themeFamily === modelData.id ? 2 : 1
|
||||
border.color: parent.themeFamily === modelData.id ?
|
||||
Data.ThemeManager.accentColor : Qt.rgba(Data.ThemeManager.fgColor.r, Data.ThemeManager.fgColor.g, Data.ThemeManager.fgColor.b, 0.3)
|
||||
|
||||
Row {
|
||||
anchors.left: parent.left
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.leftMargin: 10
|
||||
spacing: 6
|
||||
|
||||
// Compact theme preview colors
|
||||
Row {
|
||||
spacing: 1
|
||||
property var previewTheme: Data.ThemeManager.themes[modelData.id + "_" + Data.ThemeManager.currentTheme.type] || Data.ThemeManager.themes[modelData.id + "_dark"]
|
||||
Rectangle { width: 4; height: 14; radius: 1; color: parent.previewTheme.base00 }
|
||||
Rectangle { width: 4; height: 14; radius: 1; color: parent.previewTheme.base0E }
|
||||
Rectangle { width: 4; height: 14; radius: 1; color: parent.previewTheme.base0D }
|
||||
Rectangle { width: 4; height: 14; radius: 1; color: parent.previewTheme.base0B }
|
||||
}
|
||||
|
||||
Column {
|
||||
spacing: 1
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
Text {
|
||||
text: modelData.name
|
||||
color: parent.parent.parent.parent.themeFamily === modelData.id ?
|
||||
Data.ThemeManager.bgColor : Data.ThemeManager.fgColor
|
||||
font.pixelSize: 12
|
||||
font.bold: parent.parent.parent.parent.themeFamily === modelData.id
|
||||
font.family: "Roboto"
|
||||
}
|
||||
|
||||
Text {
|
||||
text: modelData.description
|
||||
color: parent.parent.parent.parent.themeFamily === modelData.id ?
|
||||
Qt.rgba(Data.ThemeManager.bgColor.r, Data.ThemeManager.bgColor.g, Data.ThemeManager.bgColor.b, 0.8) :
|
||||
Qt.rgba(Data.ThemeManager.fgColor.r, Data.ThemeManager.fgColor.g, Data.ThemeManager.fgColor.b, 0.6)
|
||||
font.pixelSize: 9
|
||||
font.family: "Roboto"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
|
||||
onClicked: {
|
||||
var themeType = Data.ThemeManager.currentTheme.type
|
||||
var newThemeId = modelData.id + "_" + themeType
|
||||
console.log("Theme card clicked:", newThemeId)
|
||||
Data.ThemeManager.setTheme(newThemeId)
|
||||
}
|
||||
|
||||
onEntered: {
|
||||
parent.scale = 1.02
|
||||
}
|
||||
|
||||
onExited: {
|
||||
parent.scale = 1.0
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on scale {
|
||||
NumberAnimation { duration: 150; easing.type: Easing.OutQuad }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Separator
|
||||
Rectangle {
|
||||
width: parent.width - 40
|
||||
height: 1
|
||||
color: Qt.rgba(Data.ThemeManager.fgColor.r, Data.ThemeManager.fgColor.g, Data.ThemeManager.fgColor.b, 0.1)
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
}
|
||||
|
||||
// Accent Colors
|
||||
Column {
|
||||
width: parent.width
|
||||
spacing: 12
|
||||
|
||||
Text {
|
||||
text: "Accent Colors"
|
||||
color: Data.ThemeManager.fgColor
|
||||
font.pixelSize: 15
|
||||
font.bold: true
|
||||
font.family: "Roboto"
|
||||
}
|
||||
|
||||
Text {
|
||||
text: "Choose your preferred accent color for " + Data.ThemeManager.currentTheme.name
|
||||
color: Qt.rgba(Data.ThemeManager.fgColor.r, Data.ThemeManager.fgColor.g, Data.ThemeManager.fgColor.b, 0.7)
|
||||
font.pixelSize: 13
|
||||
font.family: "Roboto"
|
||||
wrapMode: Text.Wrap
|
||||
width: parent.width
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
}
|
||||
|
||||
// Compact flow layout for accent colors
|
||||
Flow {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
width: parent.width - 20 // Margins to prevent clipping
|
||||
spacing: 8
|
||||
|
||||
property var accentColors: {
|
||||
var currentFamily = Data.ThemeManager.currentThemeId.replace(/_dark$|_light$/, "")
|
||||
var themeColors = []
|
||||
|
||||
// Theme-specific accent colors - reduced to 5 per theme for compactness
|
||||
if (currentFamily === "dracula") {
|
||||
themeColors.push(
|
||||
{ name: "Magenta", dark: "#ff79c6", light: "#e91e63" },
|
||||
{ name: "Purple", dark: "#bd93f9", light: "#6c7ce0" },
|
||||
{ name: "Cyan", dark: "#8be9fd", light: "#17a2b8" },
|
||||
{ name: "Green", dark: "#50fa7b", light: "#27ae60" },
|
||||
{ name: "Orange", dark: "#ffb86c", light: "#f39c12" }
|
||||
)
|
||||
} else if (currentFamily === "gruvbox") {
|
||||
themeColors.push(
|
||||
{ name: "Orange", dark: "#fe8019", light: "#d65d0e" },
|
||||
{ name: "Red", dark: "#fb4934", light: "#cc241d" },
|
||||
{ name: "Yellow", dark: "#fabd2f", light: "#d79921" },
|
||||
{ name: "Green", dark: "#b8bb26", light: "#98971a" },
|
||||
{ name: "Purple", dark: "#d3869b", light: "#b16286" }
|
||||
)
|
||||
} else if (currentFamily === "catppuccin") {
|
||||
themeColors.push(
|
||||
{ name: "Mauve", dark: "#cba6f7", light: "#8839ef" },
|
||||
{ name: "Blue", dark: "#89b4fa", light: "#1e66f5" },
|
||||
{ name: "Teal", dark: "#94e2d5", light: "#179299" },
|
||||
{ name: "Green", dark: "#a6e3a1", light: "#40a02b" },
|
||||
{ name: "Peach", dark: "#fab387", light: "#fe640b" }
|
||||
)
|
||||
} else if (currentFamily === "matugen") {
|
||||
// Use dynamic matugen colors if available
|
||||
if (Data.ThemeManager.matugen && Data.ThemeManager.matugen.isMatugenActive()) {
|
||||
themeColors.push(
|
||||
{ name: "Primary", dark: Data.ThemeManager.matugen.getMatugenColor("primary") || "#adc6ff", light: Data.ThemeManager.matugen.getMatugenColor("primary") || "#0f62fe" },
|
||||
{ name: "Secondary", dark: Data.ThemeManager.matugen.getMatugenColor("secondary") || "#bfc6dc", light: Data.ThemeManager.matugen.getMatugenColor("secondary") || "#6272a4" },
|
||||
{ name: "Tertiary", dark: Data.ThemeManager.matugen.getMatugenColor("tertiary") || "#debcdf", light: Data.ThemeManager.matugen.getMatugenColor("tertiary") || "#b16286" },
|
||||
{ name: "Surface", dark: Data.ThemeManager.matugen.getMatugenColor("surface_tint") || "#adc6ff", light: Data.ThemeManager.matugen.getMatugenColor("surface_tint") || "#0f62fe" },
|
||||
{ name: "Error", dark: Data.ThemeManager.matugen.getMatugenColor("error") || "#ffb4ab", light: Data.ThemeManager.matugen.getMatugenColor("error") || "#ba1a1a" }
|
||||
)
|
||||
} else {
|
||||
// Fallback matugen colors
|
||||
themeColors.push(
|
||||
{ name: "Primary", dark: "#adc6ff", light: "#0f62fe" },
|
||||
{ name: "Secondary", dark: "#bfc6dc", light: "#6272a4" },
|
||||
{ name: "Tertiary", dark: "#debcdf", light: "#b16286" },
|
||||
{ name: "Surface", dark: "#adc6ff", light: "#0f62fe" },
|
||||
{ name: "Error", dark: "#ffb4ab", light: "#ba1a1a" }
|
||||
)
|
||||
}
|
||||
} else { // oxocarbon and fallback
|
||||
themeColors.push(
|
||||
{ name: "Purple", dark: "#be95ff", light: "#8a3ffc" },
|
||||
{ name: "Blue", dark: "#78a9ff", light: "#0f62fe" },
|
||||
{ name: "Cyan", dark: "#3ddbd9", light: "#007d79" },
|
||||
{ name: "Green", dark: "#42be65", light: "#198038" },
|
||||
{ name: "Pink", dark: "#ff7eb6", light: "#d12771" }
|
||||
)
|
||||
}
|
||||
|
||||
return themeColors
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: parent.accentColors
|
||||
delegate: Rectangle {
|
||||
width: 60
|
||||
height: 50
|
||||
radius: 10
|
||||
color: Qt.lighter(Data.ThemeManager.bgColor, 1.15)
|
||||
border.width: Data.ThemeManager.accentColor.toString() === (Data.ThemeManager.currentTheme.type === "dark" ? modelData.dark : modelData.light) ? 3 : 1
|
||||
border.color: Data.ThemeManager.accentColor.toString() === (Data.ThemeManager.currentTheme.type === "dark" ? modelData.dark : modelData.light) ?
|
||||
Data.ThemeManager.accentColor : Qt.rgba(Data.ThemeManager.fgColor.r, Data.ThemeManager.fgColor.g, Data.ThemeManager.fgColor.b, 0.3)
|
||||
|
||||
Column {
|
||||
anchors.centerIn: parent
|
||||
spacing: 4
|
||||
|
||||
Rectangle {
|
||||
width: 20
|
||||
height: 20
|
||||
radius: 10
|
||||
color: Data.ThemeManager.currentTheme.type === "dark" ? modelData.dark : modelData.light
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
}
|
||||
|
||||
Text {
|
||||
text: modelData.name
|
||||
color: Data.ThemeManager.fgColor
|
||||
font.pixelSize: 9
|
||||
font.family: "Roboto"
|
||||
font.bold: true
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
|
||||
onClicked: {
|
||||
// Set custom accent
|
||||
Data.Settings.useCustomAccent = true
|
||||
Data.ThemeManager.setCustomAccent(modelData.dark, modelData.light)
|
||||
}
|
||||
|
||||
onEntered: {
|
||||
parent.scale = 1.05
|
||||
}
|
||||
|
||||
onExited: {
|
||||
parent.scale = 1.0
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on scale {
|
||||
NumberAnimation { duration: 150; easing.type: Easing.OutQuad }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Animation Settings in Collapsible Section
|
||||
SettingsCategory {
|
||||
width: parent.width
|
||||
title: "Animation Settings"
|
||||
icon: "animation"
|
||||
|
||||
content: Component {
|
||||
Column {
|
||||
width: parent.width
|
||||
spacing: 20
|
||||
|
||||
Text {
|
||||
text: "Configure workspace change animations"
|
||||
color: Qt.rgba(Data.ThemeManager.fgColor.r, Data.ThemeManager.fgColor.g, Data.ThemeManager.fgColor.b, 0.7)
|
||||
font.pixelSize: 13
|
||||
font.family: "Roboto"
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
}
|
||||
|
||||
// Workspace Burst Toggle
|
||||
Row {
|
||||
width: parent.width
|
||||
height: 40
|
||||
|
||||
Column {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
width: parent.width - 80
|
||||
|
||||
Text {
|
||||
text: "Workspace Burst Effect"
|
||||
color: Data.ThemeManager.fgColor
|
||||
font.pixelSize: 14
|
||||
font.bold: true
|
||||
font.family: "Roboto"
|
||||
}
|
||||
|
||||
Text {
|
||||
text: "Expanding rings when switching workspaces"
|
||||
color: Qt.rgba(Data.ThemeManager.fgColor.r, Data.ThemeManager.fgColor.g, Data.ThemeManager.fgColor.b, 0.6)
|
||||
font.pixelSize: 11
|
||||
font.family: "Roboto"
|
||||
}
|
||||
}
|
||||
|
||||
// Toggle switch for burst
|
||||
Rectangle {
|
||||
width: 50
|
||||
height: 25
|
||||
radius: 12.5
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.right: parent.right
|
||||
color: Data.Settings.workspaceBurstEnabled ?
|
||||
Qt.lighter(Data.ThemeManager.accentColor, 0.8) :
|
||||
Qt.rgba(Data.ThemeManager.fgColor.r, Data.ThemeManager.fgColor.g, Data.ThemeManager.fgColor.b, 0.2)
|
||||
border.width: 1
|
||||
border.color: Data.Settings.workspaceBurstEnabled ?
|
||||
Data.ThemeManager.accentColor :
|
||||
Qt.rgba(Data.ThemeManager.fgColor.r, Data.ThemeManager.fgColor.g, Data.ThemeManager.fgColor.b, 0.4)
|
||||
|
||||
Rectangle {
|
||||
width: 20
|
||||
height: 20
|
||||
radius: 10
|
||||
color: Data.ThemeManager.bgColor
|
||||
border.width: 1.5
|
||||
border.color: Data.Settings.workspaceBurstEnabled ? Data.ThemeManager.accentColor : Data.ThemeManager.fgColor
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
x: Data.Settings.workspaceBurstEnabled ? parent.width - width - 2.5 : 2.5
|
||||
|
||||
Behavior on x {
|
||||
NumberAnimation {
|
||||
duration: 200
|
||||
easing.type: Easing.OutQuad
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on color { ColorAnimation { duration: 200 } }
|
||||
Behavior on border.color { ColorAnimation { duration: 200 } }
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
onClicked: {
|
||||
Data.Settings.workspaceBurstEnabled = !Data.Settings.workspaceBurstEnabled
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Workspace Glow Toggle
|
||||
Row {
|
||||
width: parent.width
|
||||
height: 40
|
||||
|
||||
Column {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
width: parent.width - 80
|
||||
|
||||
Text {
|
||||
text: "Workspace Shadow Glow"
|
||||
color: Data.ThemeManager.fgColor
|
||||
font.pixelSize: 14
|
||||
font.bold: true
|
||||
font.family: "Roboto"
|
||||
}
|
||||
|
||||
Text {
|
||||
text: "Accent color glow in workspace shadow"
|
||||
color: Qt.rgba(Data.ThemeManager.fgColor.r, Data.ThemeManager.fgColor.g, Data.ThemeManager.fgColor.b, 0.6)
|
||||
font.pixelSize: 11
|
||||
font.family: "Roboto"
|
||||
}
|
||||
}
|
||||
|
||||
// Toggle switch for glow
|
||||
Rectangle {
|
||||
width: 50
|
||||
height: 25
|
||||
radius: 12.5
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.right: parent.right
|
||||
color: Data.Settings.workspaceGlowEnabled ?
|
||||
Qt.lighter(Data.ThemeManager.accentColor, 0.8) :
|
||||
Qt.rgba(Data.ThemeManager.fgColor.r, Data.ThemeManager.fgColor.g, Data.ThemeManager.fgColor.b, 0.2)
|
||||
border.width: 1
|
||||
border.color: Data.Settings.workspaceGlowEnabled ?
|
||||
Data.ThemeManager.accentColor :
|
||||
Qt.rgba(Data.ThemeManager.fgColor.r, Data.ThemeManager.fgColor.g, Data.ThemeManager.fgColor.b, 0.4)
|
||||
|
||||
Rectangle {
|
||||
width: 20
|
||||
height: 20
|
||||
radius: 10
|
||||
color: Data.ThemeManager.bgColor
|
||||
border.width: 1.5
|
||||
border.color: Data.Settings.workspaceGlowEnabled ? Data.ThemeManager.accentColor : Data.ThemeManager.fgColor
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
x: Data.Settings.workspaceGlowEnabled ? parent.width - width - 2.5 : 2.5
|
||||
|
||||
Behavior on x {
|
||||
NumberAnimation {
|
||||
duration: 200
|
||||
easing.type: Easing.OutQuad
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on color { ColorAnimation { duration: 200 } }
|
||||
Behavior on border.color { ColorAnimation { duration: 200 } }
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
onClicked: {
|
||||
Data.Settings.workspaceGlowEnabled = !Data.Settings.workspaceGlowEnabled
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,121 @@
|
|||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import "root:/Data" as Data
|
||||
|
||||
// Music Player settings content
|
||||
Column {
|
||||
width: parent.width
|
||||
spacing: 20
|
||||
|
||||
// Auto-switch to active player
|
||||
Column {
|
||||
width: parent.width
|
||||
spacing: 12
|
||||
|
||||
Text {
|
||||
text: "Auto-switch to Active Player"
|
||||
color: Data.ThemeManager.fgColor
|
||||
font.pixelSize: 16
|
||||
font.bold: true
|
||||
font.family: "Roboto"
|
||||
}
|
||||
|
||||
Text {
|
||||
text: "Automatically switch to the player that starts playing music"
|
||||
color: Qt.rgba(Data.ThemeManager.fgColor.r, Data.ThemeManager.fgColor.g, Data.ThemeManager.fgColor.b, 0.7)
|
||||
font.pixelSize: 13
|
||||
font.family: "Roboto"
|
||||
wrapMode: Text.Wrap
|
||||
width: parent.width
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: 200
|
||||
height: 35
|
||||
radius: 18
|
||||
color: Data.Settings.autoSwitchPlayer ? Data.ThemeManager.accentColor : Qt.lighter(Data.ThemeManager.bgColor, 1.15)
|
||||
border.width: 1
|
||||
border.color: Data.ThemeManager.accentColor
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation { duration: 200 }
|
||||
}
|
||||
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
text: Data.Settings.autoSwitchPlayer ? "Enabled" : "Disabled"
|
||||
color: Data.Settings.autoSwitchPlayer ? Data.ThemeManager.bgColor : Data.ThemeManager.fgColor
|
||||
font.pixelSize: 14
|
||||
font.bold: true
|
||||
font.family: "Roboto"
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation { duration: 200 }
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
onClicked: {
|
||||
Data.Settings.autoSwitchPlayer = !Data.Settings.autoSwitchPlayer
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Always show player dropdown
|
||||
Column {
|
||||
width: parent.width
|
||||
spacing: 12
|
||||
|
||||
Text {
|
||||
text: "Always Show Player Dropdown"
|
||||
color: Data.ThemeManager.fgColor
|
||||
font.pixelSize: 16
|
||||
font.bold: true
|
||||
font.family: "Roboto"
|
||||
}
|
||||
|
||||
Text {
|
||||
text: "Show the player selection dropdown even with only one player"
|
||||
color: Qt.rgba(Data.ThemeManager.fgColor.r, Data.ThemeManager.fgColor.g, Data.ThemeManager.fgColor.b, 0.7)
|
||||
font.pixelSize: 13
|
||||
font.family: "Roboto"
|
||||
wrapMode: Text.Wrap
|
||||
width: parent.width
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: 200
|
||||
height: 35
|
||||
radius: 18
|
||||
color: Data.Settings.alwaysShowPlayerDropdown ? Data.ThemeManager.accentColor : Qt.lighter(Data.ThemeManager.bgColor, 1.15)
|
||||
border.width: 1
|
||||
border.color: Data.ThemeManager.accentColor
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation { duration: 200 }
|
||||
}
|
||||
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
text: Data.Settings.alwaysShowPlayerDropdown ? "Enabled" : "Disabled"
|
||||
color: Data.Settings.alwaysShowPlayerDropdown ? Data.ThemeManager.bgColor : Data.ThemeManager.fgColor
|
||||
font.pixelSize: 14
|
||||
font.bold: true
|
||||
font.family: "Roboto"
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation { duration: 200 }
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
onClicked: {
|
||||
Data.Settings.alwaysShowPlayerDropdown = !Data.Settings.alwaysShowPlayerDropdown
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,517 @@
|
|||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import "root:/Data" as Data
|
||||
|
||||
// Night Light settings content
|
||||
Item {
|
||||
id: nightLightSettings
|
||||
width: parent.width
|
||||
height: contentColumn.height
|
||||
|
||||
Column {
|
||||
id: contentColumn
|
||||
width: parent.width
|
||||
spacing: 20
|
||||
|
||||
// Night Light Enable Toggle
|
||||
Row {
|
||||
width: parent.width
|
||||
spacing: 16
|
||||
|
||||
Column {
|
||||
width: parent.width - nightLightToggle.width - 16
|
||||
spacing: 4
|
||||
|
||||
Text {
|
||||
text: "Enable Night Light"
|
||||
color: Data.ThemeManager.fgColor
|
||||
font.pixelSize: 16
|
||||
font.bold: true
|
||||
font.family: "Roboto"
|
||||
}
|
||||
|
||||
Text {
|
||||
text: "Reduces blue light to help protect your eyes and improve sleep"
|
||||
color: Qt.rgba(Data.ThemeManager.fgColor.r, Data.ThemeManager.fgColor.g, Data.ThemeManager.fgColor.b, 0.7)
|
||||
font.pixelSize: 13
|
||||
font.family: "Roboto"
|
||||
wrapMode: Text.Wrap
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: nightLightToggle
|
||||
width: 50
|
||||
height: 28
|
||||
radius: 14
|
||||
color: Data.Settings.nightLightEnabled ? Data.ThemeManager.accentColor : Qt.rgba(Data.ThemeManager.fgColor.r, Data.ThemeManager.fgColor.g, Data.ThemeManager.fgColor.b, 0.3)
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation { duration: 200 }
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: 20
|
||||
height: 20
|
||||
radius: 10
|
||||
color: Data.ThemeManager.bgColor
|
||||
x: Data.Settings.nightLightEnabled ? parent.width - width - 4 : 4
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
Behavior on x {
|
||||
NumberAnimation { duration: 200; easing.type: Easing.OutCubic }
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
onClicked: {
|
||||
Data.Settings.nightLightEnabled = !Data.Settings.nightLightEnabled
|
||||
}
|
||||
onEntered: {
|
||||
parent.scale = 1.05
|
||||
}
|
||||
onExited: {
|
||||
parent.scale = 1.0
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on scale {
|
||||
NumberAnimation { duration: 150; easing.type: Easing.OutQuad }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Warmth Level Slider
|
||||
Column {
|
||||
width: parent.width
|
||||
spacing: 12
|
||||
|
||||
Text {
|
||||
text: "Warmth Level"
|
||||
color: Data.ThemeManager.fgColor
|
||||
font.pixelSize: 16
|
||||
font.bold: true
|
||||
font.family: "Roboto"
|
||||
}
|
||||
|
||||
Text {
|
||||
text: "Adjust how warm the screen filter appears"
|
||||
color: Qt.rgba(Data.ThemeManager.fgColor.r, Data.ThemeManager.fgColor.g, Data.ThemeManager.fgColor.b, 0.7)
|
||||
font.pixelSize: 13
|
||||
font.family: "Roboto"
|
||||
wrapMode: Text.Wrap
|
||||
width: parent.width
|
||||
}
|
||||
|
||||
Row {
|
||||
width: parent.width
|
||||
spacing: 12
|
||||
|
||||
Text {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: "Cool"
|
||||
color: Qt.rgba(Data.ThemeManager.fgColor.r, Data.ThemeManager.fgColor.g, Data.ThemeManager.fgColor.b, 0.6)
|
||||
font.pixelSize: 12
|
||||
font.family: "Roboto"
|
||||
}
|
||||
|
||||
Slider {
|
||||
id: warmthSlider
|
||||
width: parent.width - 120
|
||||
height: 30
|
||||
from: 0.1
|
||||
to: 1.0
|
||||
value: Data.Settings.nightLightWarmth || 0.4
|
||||
stepSize: 0.1
|
||||
|
||||
onValueChanged: {
|
||||
Data.Settings.nightLightWarmth = value
|
||||
}
|
||||
|
||||
background: Rectangle {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
width: parent.width
|
||||
height: 6
|
||||
radius: 3
|
||||
color: Qt.rgba(Data.ThemeManager.fgColor.r, Data.ThemeManager.fgColor.g, Data.ThemeManager.fgColor.b, 0.2)
|
||||
|
||||
Rectangle {
|
||||
width: warmthSlider.visualPosition * parent.width
|
||||
height: parent.height
|
||||
radius: parent.radius
|
||||
color: Qt.rgba(1.0, 0.8 - warmthSlider.value * 0.3, 0.4, 1.0)
|
||||
}
|
||||
}
|
||||
|
||||
handle: Rectangle {
|
||||
x: warmthSlider.leftPadding + warmthSlider.visualPosition * (warmthSlider.availableWidth - width)
|
||||
y: warmthSlider.topPadding + warmthSlider.availableHeight / 2 - height / 2
|
||||
width: 20
|
||||
height: 20
|
||||
radius: 10
|
||||
color: Data.ThemeManager.accentColor
|
||||
border.color: Qt.lighter(Data.ThemeManager.accentColor, 1.2)
|
||||
border.width: 2
|
||||
}
|
||||
}
|
||||
|
||||
Text {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: "Warm"
|
||||
color: Qt.rgba(Data.ThemeManager.fgColor.r, Data.ThemeManager.fgColor.g, Data.ThemeManager.fgColor.b, 0.6)
|
||||
font.pixelSize: 12
|
||||
font.family: "Roboto"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Auto-enable Toggle
|
||||
Row {
|
||||
width: parent.width
|
||||
spacing: 16
|
||||
|
||||
Column {
|
||||
width: parent.width - autoToggle.width - 16
|
||||
spacing: 4
|
||||
|
||||
Text {
|
||||
text: "Auto-enable Schedule"
|
||||
color: Data.ThemeManager.fgColor
|
||||
font.pixelSize: 16
|
||||
font.bold: true
|
||||
font.family: "Roboto"
|
||||
}
|
||||
|
||||
Text {
|
||||
text: "Automatically turn on night light at sunset/bedtime"
|
||||
color: Qt.rgba(Data.ThemeManager.fgColor.r, Data.ThemeManager.fgColor.g, Data.ThemeManager.fgColor.b, 0.7)
|
||||
font.pixelSize: 13
|
||||
font.family: "Roboto"
|
||||
wrapMode: Text.Wrap
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: autoToggle
|
||||
width: 50
|
||||
height: 28
|
||||
radius: 14
|
||||
color: Data.Settings.nightLightAuto ? Data.ThemeManager.accentColor : Qt.rgba(Data.ThemeManager.fgColor.r, Data.ThemeManager.fgColor.g, Data.ThemeManager.fgColor.b, 0.3)
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation { duration: 200 }
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: 20
|
||||
height: 20
|
||||
radius: 10
|
||||
color: Data.ThemeManager.bgColor
|
||||
x: Data.Settings.nightLightAuto ? parent.width - width - 4 : 4
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
Behavior on x {
|
||||
NumberAnimation { duration: 200; easing.type: Easing.OutCubic }
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
onClicked: {
|
||||
Data.Settings.nightLightAuto = !Data.Settings.nightLightAuto
|
||||
}
|
||||
onEntered: {
|
||||
parent.scale = 1.05
|
||||
}
|
||||
onExited: {
|
||||
parent.scale = 1.0
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on scale {
|
||||
NumberAnimation { duration: 150; easing.type: Easing.OutQuad }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Schedule Time Controls - visible when auto-enable is on
|
||||
Column {
|
||||
width: parent.width
|
||||
spacing: 16
|
||||
visible: Data.Settings.nightLightAuto
|
||||
opacity: Data.Settings.nightLightAuto ? 1.0 : 0.0
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation { duration: 200 }
|
||||
}
|
||||
|
||||
Text {
|
||||
text: "Schedule Times"
|
||||
color: Data.ThemeManager.fgColor
|
||||
font.pixelSize: 16
|
||||
font.bold: true
|
||||
font.family: "Roboto"
|
||||
}
|
||||
|
||||
// Start and End Time Row
|
||||
Row {
|
||||
width: parent.width
|
||||
spacing: 20
|
||||
|
||||
// Start Time
|
||||
Column {
|
||||
id: startTimeColumn
|
||||
width: (parent.width - parent.spacing) / 2
|
||||
spacing: 8
|
||||
|
||||
Text {
|
||||
text: "Start Time"
|
||||
color: Data.ThemeManager.fgColor
|
||||
font.pixelSize: 14
|
||||
font.bold: true
|
||||
font.family: "Roboto"
|
||||
}
|
||||
|
||||
Text {
|
||||
text: "Night light turns on"
|
||||
color: Qt.rgba(Data.ThemeManager.fgColor.r, Data.ThemeManager.fgColor.g, Data.ThemeManager.fgColor.b, 0.7)
|
||||
font.pixelSize: 12
|
||||
font.family: "Roboto"
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: startTimeButton
|
||||
width: parent.width
|
||||
height: 40
|
||||
radius: 8
|
||||
color: Qt.lighter(Data.ThemeManager.bgColor, 1.15)
|
||||
border.width: 1
|
||||
border.color: Qt.rgba(Data.ThemeManager.fgColor.r, Data.ThemeManager.fgColor.g, Data.ThemeManager.fgColor.b, 0.3)
|
||||
|
||||
Row {
|
||||
anchors.centerIn: parent
|
||||
spacing: 8
|
||||
|
||||
Text {
|
||||
text: (Data.Settings.nightLightStartHour || 20).toString().padStart(2, '0') + ":00"
|
||||
color: Data.ThemeManager.fgColor
|
||||
font.pixelSize: 16
|
||||
font.bold: true
|
||||
font.family: "Roboto"
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
onClicked: {
|
||||
startTimePopup.open()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Start Time Popup
|
||||
Popup {
|
||||
id: startTimePopup
|
||||
width: startTimeButton.width
|
||||
height: 170
|
||||
modal: true
|
||||
focus: true
|
||||
closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside
|
||||
y: startTimeButton.y - height - 10
|
||||
x: startTimeButton.x
|
||||
dim: false
|
||||
|
||||
background: Rectangle {
|
||||
color: Data.ThemeManager.bgColor
|
||||
radius: 12
|
||||
border.width: 2
|
||||
border.color: Data.ThemeManager.accentColor
|
||||
}
|
||||
|
||||
Column {
|
||||
anchors.centerIn: parent
|
||||
spacing: 12
|
||||
|
||||
Text {
|
||||
text: "Select Start Hour"
|
||||
color: Data.ThemeManager.fgColor
|
||||
font.pixelSize: 14
|
||||
font.bold: true
|
||||
font.family: "Roboto"
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
}
|
||||
|
||||
GridLayout {
|
||||
columns: 6
|
||||
columnSpacing: 6
|
||||
rowSpacing: 6
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
|
||||
Repeater {
|
||||
model: 24
|
||||
delegate: Rectangle {
|
||||
width: 24
|
||||
height: 24
|
||||
radius: 4
|
||||
color: (Data.Settings.nightLightStartHour || 20) === modelData ?
|
||||
Data.ThemeManager.accentColor : Qt.lighter(Data.ThemeManager.bgColor, 1.15)
|
||||
border.width: 1
|
||||
border.color: Qt.rgba(Data.ThemeManager.fgColor.r, Data.ThemeManager.fgColor.g, Data.ThemeManager.fgColor.b, 0.3)
|
||||
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
text: modelData.toString().padStart(2, '0')
|
||||
color: (Data.Settings.nightLightStartHour || 20) === modelData ?
|
||||
Data.ThemeManager.bgColor : Data.ThemeManager.fgColor
|
||||
font.pixelSize: 10
|
||||
font.bold: true
|
||||
font.family: "Roboto"
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
onClicked: {
|
||||
Data.Settings.nightLightStartHour = modelData
|
||||
startTimePopup.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// End Time
|
||||
Column {
|
||||
id: endTimeColumn
|
||||
width: (parent.width - parent.spacing) / 2
|
||||
spacing: 8
|
||||
|
||||
Text {
|
||||
text: "End Time"
|
||||
color: Data.ThemeManager.fgColor
|
||||
font.pixelSize: 14
|
||||
font.bold: true
|
||||
font.family: "Roboto"
|
||||
}
|
||||
|
||||
Text {
|
||||
text: "Night light turns off"
|
||||
color: Qt.rgba(Data.ThemeManager.fgColor.r, Data.ThemeManager.fgColor.g, Data.ThemeManager.fgColor.b, 0.7)
|
||||
font.pixelSize: 12
|
||||
font.family: "Roboto"
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: endTimeButton
|
||||
width: parent.width
|
||||
height: 40
|
||||
radius: 8
|
||||
color: Qt.lighter(Data.ThemeManager.bgColor, 1.15)
|
||||
border.width: 1
|
||||
border.color: Qt.rgba(Data.ThemeManager.fgColor.r, Data.ThemeManager.fgColor.g, Data.ThemeManager.fgColor.b, 0.3)
|
||||
|
||||
Row {
|
||||
anchors.centerIn: parent
|
||||
spacing: 8
|
||||
|
||||
Text {
|
||||
text: (Data.Settings.nightLightEndHour || 6).toString().padStart(2, '0') + ":00"
|
||||
color: Data.ThemeManager.fgColor
|
||||
font.pixelSize: 16
|
||||
font.bold: true
|
||||
font.family: "Roboto"
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
onClicked: {
|
||||
endTimePopup.open()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// End Time Popup
|
||||
Popup {
|
||||
id: endTimePopup
|
||||
width: endTimeButton.width
|
||||
height: 170
|
||||
modal: true
|
||||
focus: true
|
||||
closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside
|
||||
y: endTimeButton.y - height - 10
|
||||
x: endTimeButton.x
|
||||
dim: false
|
||||
|
||||
background: Rectangle {
|
||||
color: Data.ThemeManager.bgColor
|
||||
radius: 12
|
||||
border.width: 2
|
||||
border.color: Data.ThemeManager.accentColor
|
||||
}
|
||||
|
||||
Column {
|
||||
anchors.centerIn: parent
|
||||
spacing: 12
|
||||
|
||||
Text {
|
||||
text: "Select End Hour"
|
||||
color: Data.ThemeManager.fgColor
|
||||
font.pixelSize: 14
|
||||
font.bold: true
|
||||
font.family: "Roboto"
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
}
|
||||
|
||||
GridLayout {
|
||||
columns: 6
|
||||
columnSpacing: 6
|
||||
rowSpacing: 6
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
|
||||
Repeater {
|
||||
model: 24
|
||||
delegate: Rectangle {
|
||||
width: 24
|
||||
height: 24
|
||||
radius: 4
|
||||
color: (Data.Settings.nightLightEndHour || 6) === modelData ?
|
||||
Data.ThemeManager.accentColor : Qt.lighter(Data.ThemeManager.bgColor, 1.15)
|
||||
border.width: 1
|
||||
border.color: Qt.rgba(Data.ThemeManager.fgColor.r, Data.ThemeManager.fgColor.g, Data.ThemeManager.fgColor.b, 0.3)
|
||||
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
text: modelData.toString().padStart(2, '0')
|
||||
color: (Data.Settings.nightLightEndHour || 6) === modelData ?
|
||||
Data.ThemeManager.bgColor : Data.ThemeManager.fgColor
|
||||
font.pixelSize: 10
|
||||
font.bold: true
|
||||
font.family: "Roboto"
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
onClicked: {
|
||||
Data.Settings.nightLightEndHour = modelData
|
||||
endTimePopup.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,531 @@
|
|||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import "root:/Data" as Data
|
||||
|
||||
// Notification settings content
|
||||
Item {
|
||||
id: notificationSettings
|
||||
width: parent.width
|
||||
height: contentColumn.height
|
||||
|
||||
// Expose the text input focus for parent keyboard management
|
||||
property bool anyTextInputFocused: appNameInput.activeFocus
|
||||
|
||||
Column {
|
||||
id: contentColumn
|
||||
width: parent.width
|
||||
spacing: 20
|
||||
|
||||
// Display Time Setting
|
||||
Column {
|
||||
width: parent.width
|
||||
spacing: 12
|
||||
|
||||
Text {
|
||||
text: "Display Time"
|
||||
color: Data.ThemeManager.fgColor
|
||||
font.pixelSize: 16
|
||||
font.bold: true
|
||||
font.family: "Roboto"
|
||||
}
|
||||
|
||||
Text {
|
||||
text: "How long notifications stay visible on screen"
|
||||
color: Qt.rgba(Data.ThemeManager.fgColor.r, Data.ThemeManager.fgColor.g, Data.ThemeManager.fgColor.b, 0.7)
|
||||
font.pixelSize: 13
|
||||
font.family: "Roboto"
|
||||
wrapMode: Text.Wrap
|
||||
width: parent.width
|
||||
}
|
||||
|
||||
Row {
|
||||
spacing: 16
|
||||
width: parent.width
|
||||
|
||||
Slider {
|
||||
id: displayTimeSlider
|
||||
width: parent.width - timeLabel.width - 16
|
||||
height: 30
|
||||
from: 2000
|
||||
to: 15000
|
||||
stepSize: 1000
|
||||
value: Data.Settings.displayTime
|
||||
|
||||
onValueChanged: {
|
||||
Data.Settings.displayTime = value
|
||||
}
|
||||
|
||||
background: Rectangle {
|
||||
width: displayTimeSlider.availableWidth
|
||||
height: 6
|
||||
radius: 3
|
||||
color: Qt.rgba(Data.ThemeManager.fgColor.r, Data.ThemeManager.fgColor.g, Data.ThemeManager.fgColor.b, 0.3)
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
Rectangle {
|
||||
width: displayTimeSlider.visualPosition * parent.width
|
||||
height: parent.height
|
||||
radius: parent.radius
|
||||
color: Data.ThemeManager.accentColor
|
||||
}
|
||||
}
|
||||
|
||||
handle: Rectangle {
|
||||
x: displayTimeSlider.leftPadding + displayTimeSlider.visualPosition * (displayTimeSlider.availableWidth - width)
|
||||
y: displayTimeSlider.topPadding + displayTimeSlider.availableHeight / 2 - height / 2
|
||||
width: 20
|
||||
height: 20
|
||||
radius: 10
|
||||
color: Data.ThemeManager.accentColor
|
||||
border.color: Qt.lighter(Data.ThemeManager.accentColor, 1.2)
|
||||
border.width: 2
|
||||
|
||||
scale: displayTimeSlider.pressed ? 1.2 : 1.0
|
||||
|
||||
Behavior on scale {
|
||||
NumberAnimation { duration: 150 }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Text {
|
||||
id: timeLabel
|
||||
text: (displayTimeSlider.value / 1000).toFixed(1) + "s"
|
||||
color: Data.ThemeManager.fgColor
|
||||
font.pixelSize: 14
|
||||
font.family: "Roboto"
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
width: 40
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Max History Items
|
||||
Column {
|
||||
width: parent.width
|
||||
spacing: 12
|
||||
|
||||
Text {
|
||||
text: "History Limit"
|
||||
color: Data.ThemeManager.fgColor
|
||||
font.pixelSize: 16
|
||||
font.bold: true
|
||||
font.family: "Roboto"
|
||||
}
|
||||
|
||||
Text {
|
||||
text: "Maximum number of notifications to keep in history"
|
||||
color: Qt.rgba(Data.ThemeManager.fgColor.r, Data.ThemeManager.fgColor.g, Data.ThemeManager.fgColor.b, 0.7)
|
||||
font.pixelSize: 13
|
||||
font.family: "Roboto"
|
||||
wrapMode: Text.Wrap
|
||||
width: parent.width
|
||||
}
|
||||
|
||||
Row {
|
||||
spacing: 16
|
||||
width: parent.width
|
||||
|
||||
Slider {
|
||||
id: historySlider
|
||||
width: parent.width - historyLabel.width - 16
|
||||
height: 30
|
||||
from: 10
|
||||
to: 100
|
||||
stepSize: 5
|
||||
value: Data.Settings.historyLimit
|
||||
|
||||
onValueChanged: {
|
||||
Data.Settings.historyLimit = value
|
||||
}
|
||||
|
||||
background: Rectangle {
|
||||
width: historySlider.availableWidth
|
||||
height: 6
|
||||
radius: 3
|
||||
color: Qt.rgba(Data.ThemeManager.fgColor.r, Data.ThemeManager.fgColor.g, Data.ThemeManager.fgColor.b, 0.3)
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
Rectangle {
|
||||
width: historySlider.visualPosition * parent.width
|
||||
height: parent.height
|
||||
radius: parent.radius
|
||||
color: Data.ThemeManager.accentColor
|
||||
}
|
||||
}
|
||||
|
||||
handle: Rectangle {
|
||||
x: historySlider.leftPadding + historySlider.visualPosition * (historySlider.availableWidth - width)
|
||||
y: historySlider.topPadding + historySlider.availableHeight / 2 - height / 2
|
||||
width: 20
|
||||
height: 20
|
||||
radius: 10
|
||||
color: Data.ThemeManager.accentColor
|
||||
border.color: Qt.lighter(Data.ThemeManager.accentColor, 1.2)
|
||||
border.width: 2
|
||||
|
||||
scale: historySlider.pressed ? 1.2 : 1.0
|
||||
|
||||
Behavior on scale {
|
||||
NumberAnimation { duration: 150 }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Text {
|
||||
id: historyLabel
|
||||
text: historySlider.value + " items"
|
||||
color: Data.ThemeManager.fgColor
|
||||
font.pixelSize: 14
|
||||
font.family: "Roboto"
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
width: 60
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Ignored Apps Setting
|
||||
Column {
|
||||
width: parent.width
|
||||
spacing: 12
|
||||
|
||||
Text {
|
||||
text: "Ignored Applications"
|
||||
color: Data.ThemeManager.fgColor
|
||||
font.pixelSize: 16
|
||||
font.bold: true
|
||||
font.family: "Roboto"
|
||||
}
|
||||
|
||||
Text {
|
||||
text: "Applications that won't show notifications"
|
||||
color: Qt.rgba(Data.ThemeManager.fgColor.r, Data.ThemeManager.fgColor.g, Data.ThemeManager.fgColor.b, 0.7)
|
||||
font.pixelSize: 13
|
||||
font.family: "Roboto"
|
||||
wrapMode: Text.Wrap
|
||||
width: parent.width
|
||||
}
|
||||
|
||||
// Current ignored apps list
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: Math.max(100, ignoredAppsFlow.height + 16)
|
||||
radius: 12
|
||||
color: Qt.lighter(Data.ThemeManager.bgColor, 1.15)
|
||||
border.width: 1
|
||||
border.color: Qt.rgba(Data.ThemeManager.fgColor.r, Data.ThemeManager.fgColor.g, Data.ThemeManager.fgColor.b, 0.3)
|
||||
|
||||
Flow {
|
||||
id: ignoredAppsFlow
|
||||
anchors.fill: parent
|
||||
anchors.margins: 8
|
||||
spacing: 6
|
||||
|
||||
Repeater {
|
||||
model: Data.Settings.ignoredApps
|
||||
delegate: Rectangle {
|
||||
width: appNameText.width + removeButton.width + 16
|
||||
height: 28
|
||||
radius: 14
|
||||
color: Qt.rgba(Data.ThemeManager.accentColor.r, Data.ThemeManager.accentColor.g, Data.ThemeManager.accentColor.b, 0.15)
|
||||
border.width: 1
|
||||
border.color: Qt.rgba(Data.ThemeManager.accentColor.r, Data.ThemeManager.accentColor.g, Data.ThemeManager.accentColor.b, 0.3)
|
||||
|
||||
Row {
|
||||
anchors.centerIn: parent
|
||||
spacing: 4
|
||||
|
||||
Text {
|
||||
id: appNameText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: modelData
|
||||
color: Data.ThemeManager.fgColor
|
||||
font.pixelSize: 12
|
||||
font.family: "Roboto"
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: removeButton
|
||||
width: 18
|
||||
height: 18
|
||||
radius: 9
|
||||
color: removeMouseArea.containsMouse ?
|
||||
Qt.rgba(1, 0.3, 0.3, 0.8) : Qt.rgba(Data.ThemeManager.fgColor.r, Data.ThemeManager.fgColor.g, Data.ThemeManager.fgColor.b, 0.5)
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation { duration: 150 }
|
||||
}
|
||||
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
text: "×"
|
||||
color: "white"
|
||||
font.pixelSize: 12
|
||||
font.bold: true
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: removeMouseArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
onClicked: {
|
||||
Data.Settings.removeIgnoredApp(modelData)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add new app button
|
||||
Rectangle {
|
||||
width: addAppText.width + 36
|
||||
height: 28
|
||||
radius: 14
|
||||
color: addAppMouseArea.containsMouse ?
|
||||
Qt.rgba(Data.ThemeManager.accentColor.r, Data.ThemeManager.accentColor.g, Data.ThemeManager.accentColor.b, 0.2) :
|
||||
Qt.lighter(Data.ThemeManager.bgColor, 1.2)
|
||||
border.width: 2
|
||||
border.color: Data.ThemeManager.accentColor
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation { duration: 150 }
|
||||
}
|
||||
|
||||
Row {
|
||||
anchors.centerIn: parent
|
||||
spacing: 6
|
||||
|
||||
Text {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: "add"
|
||||
font.family: "Material Symbols Outlined"
|
||||
font.pixelSize: 14
|
||||
color: Data.ThemeManager.accentColor
|
||||
}
|
||||
|
||||
Text {
|
||||
id: addAppText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: "Add App"
|
||||
color: Data.ThemeManager.accentColor
|
||||
font.pixelSize: 12
|
||||
font.bold: true
|
||||
font.family: "Roboto"
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: addAppMouseArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
onClicked: addAppPopup.open()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Quick suggestions
|
||||
Column {
|
||||
width: parent.width
|
||||
spacing: 8
|
||||
|
||||
Text {
|
||||
text: "Common Apps"
|
||||
color: Qt.rgba(Data.ThemeManager.fgColor.r, Data.ThemeManager.fgColor.g, Data.ThemeManager.fgColor.b, 0.7)
|
||||
font.pixelSize: 12
|
||||
font.family: "Roboto"
|
||||
}
|
||||
|
||||
Flow {
|
||||
width: parent.width
|
||||
spacing: 6
|
||||
|
||||
Repeater {
|
||||
model: ["Discord", "Spotify", "Steam", "Firefox", "Chrome", "VSCode", "Slack"]
|
||||
delegate: Rectangle {
|
||||
width: suggestedAppText.width + 16
|
||||
height: 24
|
||||
radius: 12
|
||||
color: suggestionMouseArea.containsMouse ?
|
||||
Qt.rgba(Data.ThemeManager.accentColor.r, Data.ThemeManager.accentColor.g, Data.ThemeManager.accentColor.b, 0.1) :
|
||||
"transparent"
|
||||
border.width: 1
|
||||
border.color: Qt.rgba(Data.ThemeManager.fgColor.r, Data.ThemeManager.fgColor.g, Data.ThemeManager.fgColor.b, 0.3)
|
||||
|
||||
Text {
|
||||
id: suggestedAppText
|
||||
anchors.centerIn: parent
|
||||
text: modelData
|
||||
color: Qt.rgba(Data.ThemeManager.fgColor.r, Data.ThemeManager.fgColor.g, Data.ThemeManager.fgColor.b, 0.7)
|
||||
font.pixelSize: 11
|
||||
font.family: "Roboto"
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: suggestionMouseArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
onClicked: {
|
||||
Data.Settings.addIgnoredApp(modelData)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add app popup
|
||||
Popup {
|
||||
id: addAppPopup
|
||||
parent: notificationSettings
|
||||
width: 280
|
||||
height: 160
|
||||
x: (parent.width - width) / 2
|
||||
y: (parent.height - height) / 2
|
||||
modal: true
|
||||
focus: true
|
||||
closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside
|
||||
|
||||
background: Rectangle {
|
||||
color: Data.ThemeManager.bgColor
|
||||
border.color: Data.ThemeManager.accentColor
|
||||
border.width: 2
|
||||
radius: 20
|
||||
}
|
||||
|
||||
Column {
|
||||
anchors.centerIn: parent
|
||||
spacing: 16
|
||||
width: parent.width - 40
|
||||
|
||||
Text {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
text: "Add Ignored App"
|
||||
color: Data.ThemeManager.accentColor
|
||||
font.pixelSize: 16
|
||||
font.bold: true
|
||||
font.family: "Roboto"
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: 40
|
||||
radius: 20
|
||||
color: Qt.lighter(Data.ThemeManager.bgColor, 1.15)
|
||||
border.width: appNameInput.activeFocus ? 2 : 1
|
||||
border.color: appNameInput.activeFocus ? Data.ThemeManager.accentColor : Qt.rgba(Data.ThemeManager.fgColor.r, Data.ThemeManager.fgColor.g, Data.ThemeManager.fgColor.b, 0.3)
|
||||
|
||||
Behavior on border.color {
|
||||
ColorAnimation { duration: 150 }
|
||||
}
|
||||
|
||||
TextInput {
|
||||
id: appNameInput
|
||||
anchors.fill: parent
|
||||
anchors.margins: 12
|
||||
color: Data.ThemeManager.fgColor
|
||||
font.pixelSize: 14
|
||||
font.family: "Roboto"
|
||||
selectByMouse: true
|
||||
clip: true
|
||||
verticalAlignment: TextInput.AlignVCenter
|
||||
focus: true
|
||||
activeFocusOnTab: true
|
||||
inputMethodHints: Qt.ImhNone
|
||||
|
||||
Keys.onPressed: function(event) {
|
||||
if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) {
|
||||
addAppButton.clicked()
|
||||
event.accepted = true
|
||||
}
|
||||
}
|
||||
|
||||
// Placeholder text implementation
|
||||
Text {
|
||||
anchors.fill: parent
|
||||
anchors.margins: 12
|
||||
text: "App name (e.g. Discord)"
|
||||
color: Qt.rgba(Data.ThemeManager.fgColor.r, Data.ThemeManager.fgColor.g, Data.ThemeManager.fgColor.b, 0.5)
|
||||
font.pixelSize: 14
|
||||
font.family: "Roboto"
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
visible: appNameInput.text === ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
spacing: 12
|
||||
|
||||
Rectangle {
|
||||
width: 80
|
||||
height: 32
|
||||
radius: 16
|
||||
color: cancelMouseArea.containsMouse ? Qt.rgba(Data.ThemeManager.fgColor.r, Data.ThemeManager.fgColor.g, Data.ThemeManager.fgColor.b, 0.1) : "transparent"
|
||||
border.width: 1
|
||||
border.color: Qt.rgba(Data.ThemeManager.fgColor.r, Data.ThemeManager.fgColor.g, Data.ThemeManager.fgColor.b, 0.3)
|
||||
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
text: "Cancel"
|
||||
color: Data.ThemeManager.fgColor
|
||||
font.pixelSize: 12
|
||||
font.family: "Roboto"
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: cancelMouseArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
onClicked: {
|
||||
appNameInput.text = ""
|
||||
addAppPopup.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: addAppButton
|
||||
width: 80
|
||||
height: 32
|
||||
radius: 16
|
||||
color: addMouseArea.containsMouse ? Qt.lighter(Data.ThemeManager.accentColor, 1.1) : Data.ThemeManager.accentColor
|
||||
|
||||
signal clicked()
|
||||
onClicked: {
|
||||
if (appNameInput.text.trim() !== "") {
|
||||
if (Data.Settings.addIgnoredApp(appNameInput.text.trim())) {
|
||||
appNameInput.text = ""
|
||||
addAppPopup.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
text: "Add"
|
||||
color: Data.ThemeManager.bgColor
|
||||
font.pixelSize: 12
|
||||
font.bold: true
|
||||
font.family: "Roboto"
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: addMouseArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
onClicked: parent.clicked()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onOpened: {
|
||||
appNameInput.forceActiveFocus()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,104 @@
|
|||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import "root:/Data" as Data
|
||||
|
||||
// Reusable collapsible settings category component
|
||||
Item {
|
||||
id: categoryRoot
|
||||
|
||||
property string title: ""
|
||||
property string icon: ""
|
||||
property bool expanded: false
|
||||
property alias content: contentLoader.sourceComponent
|
||||
|
||||
height: headerRect.height + (expanded ? contentLoader.height + 20 : 0)
|
||||
|
||||
Behavior on height {
|
||||
NumberAnimation {
|
||||
duration: 250
|
||||
easing.type: Easing.OutCubic
|
||||
}
|
||||
}
|
||||
|
||||
// Category header
|
||||
Rectangle {
|
||||
id: headerRect
|
||||
width: parent.width
|
||||
height: 50
|
||||
radius: 12
|
||||
color: expanded ? Qt.rgba(Data.ThemeManager.accentColor.r, Data.ThemeManager.accentColor.g, Data.ThemeManager.accentColor.b, 0.1) :
|
||||
Qt.lighter(Data.ThemeManager.bgColor, 1.15)
|
||||
border.width: expanded ? 2 : 1
|
||||
border.color: expanded ? Data.ThemeManager.accentColor : Qt.rgba(Data.ThemeManager.fgColor.r, Data.ThemeManager.fgColor.g, Data.ThemeManager.fgColor.b, 0.3)
|
||||
|
||||
Row {
|
||||
anchors.left: parent.left
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.leftMargin: 16
|
||||
spacing: 12
|
||||
|
||||
Text {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: categoryRoot.icon
|
||||
font.family: "Material Symbols Outlined"
|
||||
font.pixelSize: 20
|
||||
color: expanded ? Data.ThemeManager.accentColor : Data.ThemeManager.fgColor
|
||||
}
|
||||
|
||||
Text {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: categoryRoot.title
|
||||
color: expanded ? Data.ThemeManager.accentColor : Data.ThemeManager.fgColor
|
||||
font.pixelSize: 16
|
||||
font.bold: true
|
||||
font.family: "Roboto"
|
||||
}
|
||||
}
|
||||
|
||||
// Expand/collapse arrow
|
||||
Text {
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.rightMargin: 16
|
||||
text: expanded ? "expand_less" : "expand_more"
|
||||
font.family: "Material Symbols Outlined"
|
||||
font.pixelSize: 20
|
||||
color: expanded ? Data.ThemeManager.accentColor : Data.ThemeManager.fgColor
|
||||
|
||||
Behavior on rotation {
|
||||
NumberAnimation {
|
||||
duration: 250
|
||||
easing.type: Easing.OutCubic
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
onClicked: {
|
||||
categoryRoot.expanded = !categoryRoot.expanded
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Category content
|
||||
Loader {
|
||||
id: contentLoader
|
||||
anchors.top: headerRect.bottom
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.topMargin: expanded ? 20 : 0
|
||||
anchors.leftMargin: 16
|
||||
anchors.rightMargin: 16
|
||||
|
||||
visible: expanded
|
||||
opacity: expanded ? 1 : 0
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: 250
|
||||
easing.type: Easing.OutCubic
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,131 @@
|
|||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import "root:/Data" as Data
|
||||
|
||||
// System settings content
|
||||
Item {
|
||||
id: systemSettings
|
||||
width: parent.width
|
||||
height: contentColumn.height
|
||||
|
||||
// Expose the text input focus for parent keyboard management
|
||||
property bool anyTextInputFocused: videoPathInput.activeFocus || wallpaperDirectoryInput.activeFocus
|
||||
|
||||
Column {
|
||||
id: contentColumn
|
||||
width: parent.width
|
||||
spacing: 20
|
||||
|
||||
// Video Recording Path
|
||||
Column {
|
||||
width: parent.width
|
||||
spacing: 8
|
||||
|
||||
Text {
|
||||
text: "Video Recording Path"
|
||||
color: Data.ThemeManager.fgColor
|
||||
font.pixelSize: 16
|
||||
font.bold: true
|
||||
font.family: "Roboto"
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: 40
|
||||
radius: 8
|
||||
color: Qt.lighter(Data.ThemeManager.bgColor, 1.15)
|
||||
border.width: videoPathInput.activeFocus ? 2 : 1
|
||||
border.color: videoPathInput.activeFocus ? Data.ThemeManager.accentColor : Qt.rgba(Data.ThemeManager.fgColor.r, Data.ThemeManager.fgColor.g, Data.ThemeManager.fgColor.b, 0.3)
|
||||
|
||||
Behavior on border.color {
|
||||
ColorAnimation { duration: 150 }
|
||||
}
|
||||
|
||||
TextInput {
|
||||
id: videoPathInput
|
||||
anchors.fill: parent
|
||||
anchors.margins: 12
|
||||
text: Data.Settings.videoPath
|
||||
color: Data.ThemeManager.fgColor
|
||||
font.pixelSize: 14
|
||||
font.family: "Roboto"
|
||||
selectByMouse: true
|
||||
clip: true
|
||||
verticalAlignment: TextInput.AlignVCenter
|
||||
focus: true
|
||||
activeFocusOnTab: true
|
||||
inputMethodHints: Qt.ImhNone
|
||||
|
||||
onTextChanged: {
|
||||
Data.Settings.videoPath = text
|
||||
}
|
||||
|
||||
Keys.onPressed: function(event) {
|
||||
// Allow default text input behavior
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
onClicked: {
|
||||
videoPathInput.forceActiveFocus()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Wallpaper Directory
|
||||
Column {
|
||||
width: parent.width
|
||||
spacing: 8
|
||||
|
||||
Text {
|
||||
text: "Wallpaper Directory"
|
||||
color: Data.ThemeManager.fgColor
|
||||
font.pixelSize: 16
|
||||
font.bold: true
|
||||
font.family: "Roboto"
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: 40
|
||||
radius: 8
|
||||
color: Qt.lighter(Data.ThemeManager.bgColor, 1.15)
|
||||
border.width: wallpaperDirectoryInput.activeFocus ? 2 : 1
|
||||
border.color: wallpaperDirectoryInput.activeFocus ? Data.ThemeManager.accentColor : Qt.rgba(Data.ThemeManager.fgColor.r, Data.ThemeManager.fgColor.g, Data.ThemeManager.fgColor.b, 0.3)
|
||||
|
||||
Behavior on border.color {
|
||||
ColorAnimation { duration: 150 }
|
||||
}
|
||||
|
||||
TextInput {
|
||||
id: wallpaperDirectoryInput
|
||||
anchors.fill: parent
|
||||
anchors.margins: 12
|
||||
text: Data.Settings.wallpaperDirectory
|
||||
color: Data.ThemeManager.fgColor
|
||||
font.pixelSize: 14
|
||||
font.family: "Roboto"
|
||||
selectByMouse: true
|
||||
clip: true
|
||||
verticalAlignment: TextInput.AlignVCenter
|
||||
focus: true
|
||||
activeFocusOnTab: true
|
||||
inputMethodHints: Qt.ImhNone
|
||||
|
||||
onTextChanged: {
|
||||
Data.Settings.wallpaperDirectory = text
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
onClicked: {
|
||||
wallpaperDirectoryInput.forceActiveFocus()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,197 @@
|
|||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import "root:/Data" as Data
|
||||
|
||||
// Weather settings content
|
||||
Item {
|
||||
id: weatherSettings
|
||||
width: parent.width
|
||||
height: contentColumn.height
|
||||
|
||||
required property var shell
|
||||
|
||||
// Expose the text input focus for parent keyboard management
|
||||
property bool anyTextInputFocused: locationInput.activeFocus
|
||||
|
||||
Column {
|
||||
id: contentColumn
|
||||
width: parent.width
|
||||
spacing: 20
|
||||
|
||||
// Location Setting
|
||||
Column {
|
||||
width: parent.width
|
||||
spacing: 8
|
||||
|
||||
Text {
|
||||
text: "Location"
|
||||
color: Data.ThemeManager.fgColor
|
||||
font.pixelSize: 16
|
||||
font.bold: true
|
||||
font.family: "Roboto"
|
||||
}
|
||||
|
||||
Row {
|
||||
width: parent.width
|
||||
spacing: 12
|
||||
|
||||
Rectangle {
|
||||
width: parent.width - applyButton.width - 12
|
||||
height: 40
|
||||
radius: 8
|
||||
color: Qt.lighter(Data.ThemeManager.bgColor, 1.15)
|
||||
border.width: locationInput.activeFocus ? 2 : 1
|
||||
border.color: locationInput.activeFocus ? Data.ThemeManager.accentColor : Qt.rgba(Data.ThemeManager.fgColor.r, Data.ThemeManager.fgColor.g, Data.ThemeManager.fgColor.b, 0.3)
|
||||
|
||||
Behavior on border.color {
|
||||
ColorAnimation { duration: 150 }
|
||||
}
|
||||
|
||||
TextInput {
|
||||
id: locationInput
|
||||
anchors.fill: parent
|
||||
anchors.margins: 12
|
||||
text: Data.Settings.weatherLocation
|
||||
color: Data.ThemeManager.fgColor
|
||||
font.pixelSize: 14
|
||||
font.family: "Roboto"
|
||||
selectByMouse: true
|
||||
clip: true
|
||||
verticalAlignment: TextInput.AlignVCenter
|
||||
focus: true
|
||||
activeFocusOnTab: true
|
||||
inputMethodHints: Qt.ImhNone
|
||||
|
||||
Keys.onPressed: function(event) {
|
||||
if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) {
|
||||
applyButton.clicked()
|
||||
event.accepted = true
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
onClicked: {
|
||||
locationInput.forceActiveFocus()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: applyButton
|
||||
width: 80
|
||||
height: 40
|
||||
radius: 8
|
||||
color: applyMouseArea.containsMouse ? Qt.lighter(Data.ThemeManager.accentColor, 1.1) : Data.ThemeManager.accentColor
|
||||
|
||||
signal clicked()
|
||||
onClicked: {
|
||||
Data.Settings.weatherLocation = locationInput.text
|
||||
weatherSettings.shell.weatherService.loadWeather()
|
||||
}
|
||||
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
text: "Apply"
|
||||
color: Data.ThemeManager.bgColor
|
||||
font.pixelSize: 12
|
||||
font.bold: true
|
||||
font.family: "Roboto"
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: applyMouseArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
onClicked: parent.clicked()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Temperature Units
|
||||
Column {
|
||||
width: parent.width
|
||||
spacing: 12
|
||||
|
||||
Text {
|
||||
text: "Temperature Units"
|
||||
color: Data.ThemeManager.fgColor
|
||||
font.pixelSize: 16
|
||||
font.bold: true
|
||||
font.family: "Roboto"
|
||||
}
|
||||
|
||||
Row {
|
||||
spacing: 12
|
||||
|
||||
Rectangle {
|
||||
width: 80
|
||||
height: 35
|
||||
radius: 18
|
||||
color: !Data.Settings.useFahrenheit ? Data.ThemeManager.accentColor : Qt.lighter(Data.ThemeManager.bgColor, 1.15)
|
||||
border.width: 1
|
||||
border.color: Data.ThemeManager.accentColor
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation { duration: 200 }
|
||||
}
|
||||
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
text: "°C"
|
||||
color: !Data.Settings.useFahrenheit ? Data.ThemeManager.bgColor : Data.ThemeManager.fgColor
|
||||
font.pixelSize: 14
|
||||
font.bold: true
|
||||
font.family: "Roboto"
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation { duration: 200 }
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
onClicked: {
|
||||
Data.Settings.useFahrenheit = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: 80
|
||||
height: 35
|
||||
radius: 18
|
||||
color: Data.Settings.useFahrenheit ? Data.ThemeManager.accentColor : Qt.lighter(Data.ThemeManager.bgColor, 1.15)
|
||||
border.width: 1
|
||||
border.color: Data.ThemeManager.accentColor
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation { duration: 200 }
|
||||
}
|
||||
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
text: "°F"
|
||||
color: Data.Settings.useFahrenheit ? Data.ThemeManager.bgColor : Data.ThemeManager.fgColor
|
||||
font.pixelSize: 14
|
||||
font.bold: true
|
||||
font.family: "Roboto"
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation { duration: 200 }
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
onClicked: {
|
||||
Data.Settings.useFahrenheit = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,98 @@
|
|||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import "root:/Data" as Data
|
||||
|
||||
// Dual-button notification and clipboard history toggle bar
|
||||
Rectangle {
|
||||
id: root
|
||||
width: 42
|
||||
color: Qt.darker(Data.ThemeManager.bgColor, 1.15)
|
||||
radius: 12
|
||||
z: 2 // Above notification history overlay
|
||||
|
||||
required property bool notificationHistoryVisible
|
||||
required property bool clipboardHistoryVisible
|
||||
required property var notificationHistory
|
||||
signal notificationToggleRequested()
|
||||
signal clipboardToggleRequested()
|
||||
|
||||
// Combined hover state for parent component tracking
|
||||
property bool containsMouse: notifButtonMouseArea.containsMouse || clipButtonMouseArea.containsMouse
|
||||
|
||||
property real buttonHeight: 38
|
||||
height: buttonHeight * 2 + 4 // Two buttons with spacing
|
||||
|
||||
Item {
|
||||
anchors.fill: parent
|
||||
anchors.margins: 2
|
||||
|
||||
// Notifications toggle button (top half)
|
||||
Rectangle {
|
||||
id: notificationPill
|
||||
anchors {
|
||||
top: parent.top
|
||||
left: parent.left
|
||||
right: parent.right
|
||||
bottom: parent.verticalCenter
|
||||
bottomMargin: 2 // Half of button spacing
|
||||
}
|
||||
radius: 12
|
||||
color: notifButtonMouseArea.containsMouse || root.notificationHistoryVisible ?
|
||||
Qt.rgba(Data.ThemeManager.accentColor.r, Data.ThemeManager.accentColor.g, Data.ThemeManager.accentColor.b, 0.2) :
|
||||
Qt.rgba(Data.ThemeManager.fgColor.r, Data.ThemeManager.fgColor.g, Data.ThemeManager.fgColor.b, 0.05)
|
||||
border.color: notifButtonMouseArea.containsMouse || root.notificationHistoryVisible ? Data.ThemeManager.accentColor : "transparent"
|
||||
border.width: 1
|
||||
|
||||
MouseArea {
|
||||
id: notifButtonMouseArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
onClicked: root.notificationToggleRequested()
|
||||
}
|
||||
|
||||
Label {
|
||||
anchors.centerIn: parent
|
||||
text: "notifications"
|
||||
font.family: "Material Symbols Outlined"
|
||||
font.pixelSize: 16
|
||||
color: notifButtonMouseArea.containsMouse || root.notificationHistoryVisible ?
|
||||
Data.ThemeManager.accentColor : Data.ThemeManager.fgColor
|
||||
}
|
||||
}
|
||||
|
||||
// Clipboard toggle button (bottom half)
|
||||
Rectangle {
|
||||
id: clipboardPill
|
||||
anchors {
|
||||
top: parent.verticalCenter
|
||||
left: parent.left
|
||||
right: parent.right
|
||||
bottom: parent.bottom
|
||||
topMargin: 2 // Half of button spacing
|
||||
}
|
||||
radius: 12
|
||||
color: clipButtonMouseArea.containsMouse || root.clipboardHistoryVisible ?
|
||||
Qt.rgba(Data.ThemeManager.accentColor.r, Data.ThemeManager.accentColor.g, Data.ThemeManager.accentColor.b, 0.2) :
|
||||
Qt.rgba(Data.ThemeManager.fgColor.r, Data.ThemeManager.fgColor.g, Data.ThemeManager.fgColor.b, 0.05)
|
||||
border.color: clipButtonMouseArea.containsMouse || root.clipboardHistoryVisible ? Data.ThemeManager.accentColor : "transparent"
|
||||
border.width: 1
|
||||
|
||||
MouseArea {
|
||||
id: clipButtonMouseArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
onClicked: root.clipboardToggleRequested()
|
||||
}
|
||||
|
||||
Label {
|
||||
anchors.centerIn: parent
|
||||
text: "content_paste"
|
||||
font.family: "Material Symbols Outlined"
|
||||
font.pixelSize: 16
|
||||
color: clipButtonMouseArea.containsMouse || root.clipboardHistoryVisible ?
|
||||
Data.ThemeManager.accentColor : Data.ThemeManager.fgColor
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,52 @@
|
|||
import QtQuick
|
||||
|
||||
// Top-edge hover trigger
|
||||
Rectangle {
|
||||
id: root
|
||||
width: 360
|
||||
height: 1
|
||||
color: "red"
|
||||
anchors.top: parent.top
|
||||
|
||||
signal triggered()
|
||||
|
||||
// Hover detection area at screen top edge
|
||||
MouseArea {
|
||||
id: mouseArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
|
||||
property bool isHovered: containsMouse
|
||||
|
||||
// Timer coordination
|
||||
onIsHoveredChanged: {
|
||||
if (isHovered) {
|
||||
showTimer.start()
|
||||
hideTimer.stop()
|
||||
} else {
|
||||
hideTimer.start()
|
||||
showTimer.stop()
|
||||
}
|
||||
}
|
||||
|
||||
onEntered: hideTimer.stop()
|
||||
}
|
||||
|
||||
// Delayed show trigger to prevent accidental activation
|
||||
Timer {
|
||||
id: showTimer
|
||||
interval: 200
|
||||
onTriggered: root.triggered()
|
||||
}
|
||||
|
||||
// Hide delay timer (controlled by parent)
|
||||
Timer {
|
||||
id: hideTimer
|
||||
interval: 500
|
||||
}
|
||||
|
||||
// Public interface
|
||||
readonly property alias containsMouse: mouseArea.containsMouse
|
||||
function stopHideTimer() { hideTimer.stop() }
|
||||
function startHideTimer() { hideTimer.start() }
|
||||
}
|
||||
|
|
@ -0,0 +1,226 @@
|
|||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import "root:/Data" as Data
|
||||
|
||||
// System tray context menu
|
||||
Rectangle {
|
||||
id: root
|
||||
width: parent.width
|
||||
height: visible ? calculatedHeight : 0
|
||||
visible: false
|
||||
enabled: visible
|
||||
clip: true
|
||||
color: Data.ThemeManager.bgColor
|
||||
border.color: Data.ThemeManager.accentColor
|
||||
border.width: 2
|
||||
radius: 20
|
||||
|
||||
required property var menu
|
||||
required property var systemTrayY
|
||||
required property var systemTrayHeight
|
||||
|
||||
property bool containsMouse: trayMenuMouseArea.containsMouse
|
||||
property bool menuJustOpened: false
|
||||
property point triggerPoint: Qt.point(0, 0)
|
||||
property Item originalParent
|
||||
property int totalCount: opener.children ? opener.children.values.length : 0
|
||||
|
||||
signal hideRequested()
|
||||
|
||||
MouseArea {
|
||||
id: trayMenuMouseArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
preventStealing: true
|
||||
propagateComposedEvents: false
|
||||
}
|
||||
|
||||
onVisibleChanged: {
|
||||
if (visible) {
|
||||
menuJustOpened = true
|
||||
Qt.callLater(function() {
|
||||
menuJustOpened = false
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function toggle() {
|
||||
visible = !visible
|
||||
if (visible) {
|
||||
menuJustOpened = true
|
||||
Qt.callLater(function() {
|
||||
menuJustOpened = false
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function show(point, parentItem) {
|
||||
visible = true
|
||||
menuJustOpened = true
|
||||
Qt.callLater(function() {
|
||||
menuJustOpened = false
|
||||
})
|
||||
}
|
||||
|
||||
function hide() {
|
||||
visible = false
|
||||
menuJustOpened = false
|
||||
// Small delay before notifying hide to prevent control panel flicker
|
||||
Qt.callLater(function() {
|
||||
hideRequested()
|
||||
})
|
||||
}
|
||||
|
||||
// Smart positioning to avoid screen edges
|
||||
y: {
|
||||
var preferredY = systemTrayY + systemTrayHeight + 10
|
||||
var availableSpace = parent.height - preferredY - 20
|
||||
if (calculatedHeight > availableSpace) {
|
||||
return systemTrayY - height - 10
|
||||
}
|
||||
return preferredY
|
||||
}
|
||||
|
||||
Behavior on height {
|
||||
NumberAnimation { duration: 200; easing.type: Easing.OutCubic }
|
||||
}
|
||||
|
||||
// Dynamic height calculation based on menu item count and types
|
||||
property int calculatedHeight: {
|
||||
if (totalCount === 0) return 40
|
||||
var separatorCount = 0
|
||||
var regularItemCount = 0
|
||||
|
||||
if (opener.children && opener.children.values) {
|
||||
for (var i = 0; i < opener.children.values.length; i++) {
|
||||
if (opener.children.values[i].isSeparator) {
|
||||
separatorCount++
|
||||
} else {
|
||||
regularItemCount++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate total height: separators + grid rows + margins
|
||||
var separatorHeight = separatorCount * 12
|
||||
var regularItemRows = Math.ceil(regularItemCount / 2)
|
||||
var regularItemHeight = regularItemRows * 32
|
||||
return Math.max(80, 35 + separatorHeight + regularItemHeight + 40)
|
||||
}
|
||||
|
||||
// Menu opener handles the native menu integration
|
||||
QsMenuOpener {
|
||||
id: opener
|
||||
menu: root.menu
|
||||
}
|
||||
|
||||
// Grid layout for menu items (2 columns)
|
||||
GridView {
|
||||
id: gridView
|
||||
anchors.fill: parent
|
||||
anchors.margins: 20
|
||||
cellWidth: width / 2
|
||||
cellHeight: 32
|
||||
interactive: false
|
||||
flow: GridView.FlowLeftToRight
|
||||
layoutDirection: Qt.LeftToRight
|
||||
|
||||
model: ScriptModel {
|
||||
values: opener.children ? [...opener.children.values] : []
|
||||
}
|
||||
|
||||
delegate: Item {
|
||||
id: entry
|
||||
required property var modelData
|
||||
required property int index
|
||||
|
||||
width: gridView.cellWidth - 4
|
||||
height: modelData.isSeparator ? 12 : 30
|
||||
|
||||
// Separator line
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: 8
|
||||
anchors.rightMargin: 8
|
||||
anchors.topMargin: 4
|
||||
anchors.bottomMargin: 4
|
||||
visible: modelData.isSeparator
|
||||
color: "transparent"
|
||||
|
||||
Rectangle {
|
||||
anchors.centerIn: parent
|
||||
width: parent.width * 0.8
|
||||
height: 1
|
||||
color: Qt.darker(Data.ThemeManager.accentColor, 1.5)
|
||||
opacity: 0.6
|
||||
}
|
||||
}
|
||||
|
||||
// Regular menu item
|
||||
Rectangle {
|
||||
id: itemBackground
|
||||
anchors.fill: parent
|
||||
anchors.margins: 2
|
||||
visible: !modelData.isSeparator
|
||||
color: "transparent"
|
||||
radius: 6
|
||||
|
||||
RowLayout {
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: 8
|
||||
anchors.rightMargin: 8
|
||||
spacing: 6
|
||||
|
||||
Image {
|
||||
Layout.preferredWidth: 16
|
||||
Layout.preferredHeight: 16
|
||||
source: modelData?.icon ?? ""
|
||||
visible: (modelData?.icon ?? "") !== ""
|
||||
fillMode: Image.PreserveAspectFit
|
||||
}
|
||||
|
||||
Text {
|
||||
Layout.fillWidth: true
|
||||
color: mouseArea.containsMouse ? Data.ThemeManager.accentColor : Data.ThemeManager.fgColor
|
||||
text: modelData?.text ?? ""
|
||||
font.pixelSize: 11
|
||||
font.family: "Roboto"
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
elide: Text.ElideRight
|
||||
maximumLineCount: 1
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: mouseArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
enabled: (modelData?.enabled ?? true) && root.visible && !modelData.isSeparator
|
||||
|
||||
onEntered: itemBackground.color = Qt.rgba(Data.ThemeManager.accentColor.r, Data.ThemeManager.accentColor.g, Data.ThemeManager.accentColor.b, 0.15)
|
||||
onExited: itemBackground.color = "transparent"
|
||||
onClicked: {
|
||||
modelData.triggered()
|
||||
root.hide()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Empty state indicator
|
||||
Item {
|
||||
anchors.centerIn: gridView
|
||||
visible: gridView.count === 0
|
||||
|
||||
Label {
|
||||
anchors.centerIn: parent
|
||||
text: "No tray items available"
|
||||
color: Qt.darker(Data.ThemeManager.fgColor, 2)
|
||||
font.pixelSize: 14
|
||||
font.family: "Roboto"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,149 @@
|
|||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import Qt5Compat.GraphicalEffects
|
||||
import "root:/Data" as Data
|
||||
|
||||
// Wallpaper selector grid
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property bool isVisible: false
|
||||
signal visibilityChanged(bool visible)
|
||||
|
||||
// Use all space provided by parent
|
||||
anchors.fill: parent
|
||||
visible: isVisible
|
||||
enabled: visible
|
||||
clip: true
|
||||
|
||||
property bool containsMouse: wallpaperSelectorMouseArea.containsMouse || scrollView.containsMouse
|
||||
property bool menuJustOpened: false
|
||||
|
||||
// Hover state management for auto-hide functionality
|
||||
onContainsMouseChanged: {
|
||||
if (containsMouse) {
|
||||
hideTimer.stop()
|
||||
} else if (!menuJustOpened && !isVisible) {
|
||||
hideTimer.restart()
|
||||
}
|
||||
}
|
||||
|
||||
onVisibleChanged: {
|
||||
if (visible) {
|
||||
menuJustOpened = true
|
||||
hideTimer.stop()
|
||||
Qt.callLater(function() {
|
||||
menuJustOpened = false
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: wallpaperSelectorMouseArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
preventStealing: false
|
||||
propagateComposedEvents: true
|
||||
}
|
||||
|
||||
// Scrollable wallpaper grid with memory-conscious loading
|
||||
ScrollView {
|
||||
id: scrollView
|
||||
anchors.fill: parent
|
||||
clip: true
|
||||
|
||||
property bool containsMouse: gridMouseArea.containsMouse
|
||||
|
||||
MouseArea {
|
||||
id: gridMouseArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
propagateComposedEvents: true
|
||||
}
|
||||
|
||||
GridView {
|
||||
id: wallpaperGrid
|
||||
anchors.fill: parent
|
||||
cellWidth: parent.width / 2 - 8 // 2-column layout with spacing
|
||||
cellHeight: cellWidth * 0.6 // Wallpaper aspect ratio
|
||||
model: Data.WallpaperManager.wallpaperList
|
||||
cacheBuffer: 0 // Memory optimization - no cache buffer
|
||||
leftMargin: 4
|
||||
rightMargin: 4
|
||||
topMargin: 4
|
||||
bottomMargin: 4
|
||||
|
||||
delegate: Item {
|
||||
width: wallpaperGrid.cellWidth - 8
|
||||
height: wallpaperGrid.cellHeight - 8
|
||||
|
||||
Rectangle {
|
||||
id: wallpaperItem
|
||||
anchors.fill: parent
|
||||
anchors.margins: 4
|
||||
color: Qt.darker(Data.ThemeManager.bgColor, 1.2)
|
||||
radius: 20
|
||||
|
||||
Behavior on scale {
|
||||
NumberAnimation { duration: 150; easing.type: Easing.OutCubic }
|
||||
}
|
||||
|
||||
// Wallpaper preview image with viewport-based loading
|
||||
Image {
|
||||
id: wallpaperImage
|
||||
anchors.fill: parent
|
||||
anchors.margins: 4
|
||||
source: modelData
|
||||
fillMode: Image.PreserveAspectCrop
|
||||
asynchronous: true
|
||||
cache: false // Memory optimization - no image caching
|
||||
sourceSize.width: Math.min(width, 150) // Reduced resolution for memory
|
||||
sourceSize.height: Math.min(height, 90)
|
||||
|
||||
// Only load when visible in viewport - major memory optimization
|
||||
visible: parent.parent.y >= wallpaperGrid.contentY - parent.parent.height &&
|
||||
parent.parent.y <= wallpaperGrid.contentY + wallpaperGrid.height
|
||||
|
||||
// Layer effects disabled for memory savings
|
||||
// layer.enabled: true
|
||||
// layer.effect: OpacityMask {
|
||||
// maskSource: Rectangle {
|
||||
// width: wallpaperImage.width
|
||||
// height: wallpaperImage.height
|
||||
// radius: 18
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
||||
// Current wallpaper selection indicator
|
||||
Rectangle {
|
||||
visible: modelData === Data.WallpaperManager.currentWallpaper
|
||||
anchors.fill: parent
|
||||
radius: parent.radius
|
||||
color: "transparent"
|
||||
border.color: Data.ThemeManager.accentColor
|
||||
border.width: 2
|
||||
}
|
||||
|
||||
// Hover and click handling
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
onEntered: wallpaperItem.scale = 1.05
|
||||
onExited: wallpaperItem.scale = 1.0
|
||||
onClicked: {
|
||||
Data.WallpaperManager.setWallpaper(modelData)
|
||||
// Stays in wallpaper tab after selection
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
// Use lazy loading to only load wallpapers when this component is actually used
|
||||
Data.WallpaperManager.ensureWallpapersLoaded()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,75 @@
|
|||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import "root:/Data" as Data
|
||||
|
||||
// Calendar button
|
||||
Rectangle {
|
||||
id: calendarButton
|
||||
width: 40
|
||||
height: 80
|
||||
color: Qt.darker(Data.ThemeManager.bgColor, 1.15)
|
||||
radius: 20
|
||||
|
||||
property bool containsMouse: calendarMouseArea.containsMouse
|
||||
property bool calendarVisible: false
|
||||
property var calendarPopup: null
|
||||
property var shell: null // Shell reference from parent
|
||||
|
||||
signal entered()
|
||||
signal exited()
|
||||
|
||||
// Hover state management
|
||||
onContainsMouseChanged: {
|
||||
if (containsMouse) {
|
||||
entered()
|
||||
} else {
|
||||
exited()
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: calendarMouseArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
onClicked: {
|
||||
toggleCalendar()
|
||||
}
|
||||
}
|
||||
|
||||
// Calendar icon
|
||||
Label {
|
||||
anchors.centerIn: parent
|
||||
text: "calendar_month"
|
||||
font.pixelSize: 24
|
||||
font.family: "Material Symbols Outlined"
|
||||
color: calendarButton.containsMouse || calendarButton.calendarVisible ? Data.ThemeManager.accentColor : Data.ThemeManager.fgColor
|
||||
}
|
||||
|
||||
// Toggle calendar popup
|
||||
function toggleCalendar() {
|
||||
if (!calendarPopup) {
|
||||
var component = Qt.createComponent("root:/Widgets/Calendar/CalendarPopup.qml")
|
||||
if (component.status === Component.Ready) {
|
||||
calendarPopup = component.createObject(calendarButton.parent, {
|
||||
"targetX": calendarButton.x + calendarButton.width + 10,
|
||||
"shell": calendarButton.shell
|
||||
})
|
||||
} else if (component.status === Component.Error) {
|
||||
console.log("Error loading calendar:", component.errorString())
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if (calendarPopup) {
|
||||
calendarVisible = !calendarVisible
|
||||
calendarPopup.setClickMode(calendarVisible)
|
||||
}
|
||||
}
|
||||
|
||||
function hideCalendar() {
|
||||
if (calendarPopup) {
|
||||
calendarVisible = false
|
||||
calendarPopup.setClickMode(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,297 @@
|
|||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import Quickshell
|
||||
import Quickshell.Wayland
|
||||
import "root:/Data" as Data
|
||||
|
||||
// Night light widget with pure Qt overlay (no external dependencies)
|
||||
Rectangle {
|
||||
id: root
|
||||
property var shell: null
|
||||
color: Qt.darker(Data.ThemeManager.bgColor, 1.15)
|
||||
radius: 20
|
||||
|
||||
property bool containsMouse: nightLightMouseArea.containsMouse
|
||||
property bool isActive: Data.Settings.nightLightEnabled
|
||||
property real warmth: Data.Settings.nightLightWarmth // 0=no filter, 1=very warm (0-1 scale)
|
||||
property real strength: isActive ? warmth : 0
|
||||
property bool autoSchedulerActive: false // Flag to prevent manual override during auto changes
|
||||
|
||||
signal entered()
|
||||
signal exited()
|
||||
|
||||
// Night light overlay window
|
||||
property var overlayWindow: null
|
||||
|
||||
// Hover state management for parent components
|
||||
onContainsMouseChanged: {
|
||||
if (containsMouse) {
|
||||
entered()
|
||||
} else {
|
||||
exited()
|
||||
}
|
||||
}
|
||||
|
||||
// Background with warm tint when active
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
radius: parent.radius
|
||||
color: isActive ? Qt.rgba(1.0, 0.6, 0.2, 0.15) : "transparent"
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation { duration: 300 }
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: nightLightMouseArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
|
||||
// Right-click to cycle through warmth levels
|
||||
acceptedButtons: Qt.LeftButton | Qt.RightButton
|
||||
onClicked: function(mouse) {
|
||||
if (mouse.button === Qt.RightButton) {
|
||||
cycleWarmth()
|
||||
} else {
|
||||
toggleNightLight()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Night light icon with dynamic color
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
text: isActive ? "light_mode" : "dark_mode"
|
||||
font.pixelSize: 24
|
||||
font.family: "Material Symbols Outlined"
|
||||
color: isActive ?
|
||||
Qt.rgba(1.0, 0.8 - strength * 0.3, 0.4 - strength * 0.2, 1.0) : // Warm orange when active
|
||||
(containsMouse ? Data.ThemeManager.accentColor : Data.ThemeManager.fgColor)
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation { duration: 200 }
|
||||
}
|
||||
}
|
||||
|
||||
// Warmth indicator dots
|
||||
Row {
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.bottomMargin: 6
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
spacing: 3
|
||||
visible: isActive && containsMouse
|
||||
|
||||
Repeater {
|
||||
model: 3
|
||||
delegate: Rectangle {
|
||||
width: 4
|
||||
height: 4
|
||||
radius: 2
|
||||
color: index < Math.ceil(warmth * 3) ?
|
||||
Qt.rgba(1.0, 0.7 - index * 0.2, 0.3, 0.8) :
|
||||
Qt.rgba(Data.ThemeManager.fgColor.r, Data.ThemeManager.fgColor.g, Data.ThemeManager.fgColor.b, 0.3)
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation { duration: 150 }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Watch for settings changes
|
||||
Connections {
|
||||
target: Data.Settings
|
||||
function onNightLightEnabledChanged() {
|
||||
if (Data.Settings.nightLightEnabled) {
|
||||
createOverlay()
|
||||
} else {
|
||||
removeOverlay()
|
||||
}
|
||||
|
||||
// Set manual override flag if this wasn't an automatic change
|
||||
if (!autoSchedulerActive) {
|
||||
Data.Settings.nightLightManualOverride = true
|
||||
Data.Settings.nightLightManuallyEnabled = Data.Settings.nightLightEnabled
|
||||
console.log("Manual night light change detected - override enabled, manually set to:", Data.Settings.nightLightEnabled)
|
||||
}
|
||||
}
|
||||
function onNightLightWarmthChanged() {
|
||||
updateOverlay()
|
||||
}
|
||||
}
|
||||
|
||||
// Functions to control night light
|
||||
function toggleNightLight() {
|
||||
Data.Settings.nightLightEnabled = !Data.Settings.nightLightEnabled
|
||||
}
|
||||
|
||||
function cycleWarmth() {
|
||||
// Cycle through warmth levels: 0.2 -> 0.4 -> 0.6 -> 1.0 -> 0.2
|
||||
var newWarmth = warmth >= 1.0 ? 0.2 : (warmth >= 0.6 ? 1.0 : warmth + 0.2)
|
||||
Data.Settings.nightLightWarmth = newWarmth
|
||||
}
|
||||
|
||||
function createOverlay() {
|
||||
if (overlayWindow) return
|
||||
|
||||
var qmlString = `
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Wayland
|
||||
|
||||
PanelWindow {
|
||||
id: nightLightOverlay
|
||||
screen: Quickshell.primaryScreen || Quickshell.screens[0]
|
||||
|
||||
anchors.top: true
|
||||
anchors.left: true
|
||||
anchors.right: true
|
||||
anchors.bottom: true
|
||||
|
||||
color: "transparent"
|
||||
|
||||
WlrLayershell.layer: WlrLayershell.Overlay
|
||||
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
|
||||
WlrLayershell.namespace: "quickshell-nightlight"
|
||||
exclusiveZone: 0
|
||||
|
||||
// Click-through overlay
|
||||
mask: Region {}
|
||||
|
||||
Rectangle {
|
||||
id: overlayRect
|
||||
anchors.fill: parent
|
||||
color: "transparent" // Initial color, will be set by parent
|
||||
|
||||
// Smooth transitions when warmth changes
|
||||
Behavior on color {
|
||||
ColorAnimation { duration: 300 }
|
||||
}
|
||||
}
|
||||
|
||||
// Function to update overlay color
|
||||
function updateColor(newWarmth) {
|
||||
overlayRect.color = Qt.rgba(1.0, 0.8 - newWarmth * 0.4, 0.3 - newWarmth * 0.25, 0.1 + newWarmth * 0.2)
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
try {
|
||||
overlayWindow = Qt.createQmlObject(qmlString, root)
|
||||
// Set initial color
|
||||
updateOverlay()
|
||||
} catch (e) {
|
||||
console.error("Failed to create night light overlay:", e)
|
||||
}
|
||||
}
|
||||
|
||||
function updateOverlay() {
|
||||
if (overlayWindow && overlayWindow.updateColor) {
|
||||
overlayWindow.updateColor(warmth)
|
||||
}
|
||||
}
|
||||
|
||||
function removeOverlay() {
|
||||
if (overlayWindow) {
|
||||
overlayWindow.destroy()
|
||||
overlayWindow = null
|
||||
}
|
||||
}
|
||||
|
||||
// Preset warmth levels for easy access
|
||||
function setLow() { Data.Settings.nightLightWarmth = 0.2 } // Light warmth
|
||||
function setMedium() { Data.Settings.nightLightWarmth = 0.4 } // Medium warmth
|
||||
function setHigh() { Data.Settings.nightLightWarmth = 0.6 } // High warmth
|
||||
function setMax() { Data.Settings.nightLightWarmth = 1.0 } // Maximum warmth
|
||||
|
||||
// Auto-enable based on time (basic sunset/sunrise simulation)
|
||||
Timer {
|
||||
interval: 60000 // Check every minute
|
||||
running: true
|
||||
repeat: true
|
||||
onTriggered: checkAutoEnable()
|
||||
}
|
||||
|
||||
function checkAutoEnable() {
|
||||
if (!Data.Settings.nightLightAuto) return
|
||||
|
||||
var now = new Date()
|
||||
var hour = now.getHours()
|
||||
var minute = now.getMinutes()
|
||||
var startHour = Data.Settings.nightLightStartHour || 20
|
||||
var endHour = Data.Settings.nightLightEndHour || 6
|
||||
|
||||
// Handle overnight schedules (e.g., 20:00 to 6:00)
|
||||
var shouldBeActive = false
|
||||
if (startHour > endHour) {
|
||||
// Overnight: active from startHour onwards OR before endHour
|
||||
shouldBeActive = (hour >= startHour || hour < endHour)
|
||||
} else if (startHour < endHour) {
|
||||
// Same day: active between startHour and endHour
|
||||
shouldBeActive = (hour >= startHour && hour < endHour)
|
||||
} else {
|
||||
// startHour === endHour: never auto-enable
|
||||
shouldBeActive = false
|
||||
}
|
||||
|
||||
// Debug logging
|
||||
console.log(`Night Light Auto Check: ${hour}:${minute.toString().padStart(2, '0')} - Should be active: ${shouldBeActive}, Currently active: ${Data.Settings.nightLightEnabled}, Manual override: ${Data.Settings.nightLightManualOverride}`)
|
||||
|
||||
// Smart override logic - only block conflicting actions
|
||||
if (Data.Settings.nightLightManualOverride) {
|
||||
// If user manually enabled, allow auto-disable but block auto-enable
|
||||
if (Data.Settings.nightLightManuallyEnabled && !shouldBeActive && Data.Settings.nightLightEnabled) {
|
||||
console.log("Auto-disabling night light (respecting schedule after manual enable)")
|
||||
autoSchedulerActive = true
|
||||
Data.Settings.nightLightEnabled = false
|
||||
Data.Settings.nightLightManualOverride = false // Reset after respecting schedule
|
||||
autoSchedulerActive = false
|
||||
return
|
||||
}
|
||||
// If user manually disabled, block auto-enable until next cycle
|
||||
else if (!Data.Settings.nightLightManuallyEnabled && shouldBeActive && !Data.Settings.nightLightEnabled) {
|
||||
// Check if this is the start of a new schedule cycle
|
||||
var isNewCycle = (hour === startHour && minute === 0)
|
||||
if (isNewCycle) {
|
||||
console.log("New schedule cycle starting - resetting manual override")
|
||||
Data.Settings.nightLightManualOverride = false
|
||||
} else {
|
||||
console.log("Manual disable override active - skipping auto-enable")
|
||||
return
|
||||
}
|
||||
}
|
||||
// Other cases - reset override and continue
|
||||
else {
|
||||
Data.Settings.nightLightManualOverride = false
|
||||
}
|
||||
}
|
||||
|
||||
// Auto-enable when schedule starts
|
||||
if (shouldBeActive && !Data.Settings.nightLightEnabled) {
|
||||
console.log("Auto-enabling night light")
|
||||
autoSchedulerActive = true
|
||||
Data.Settings.nightLightEnabled = true
|
||||
autoSchedulerActive = false
|
||||
}
|
||||
// Auto-disable when schedule ends
|
||||
else if (!shouldBeActive && Data.Settings.nightLightEnabled) {
|
||||
console.log("Auto-disabling night light")
|
||||
autoSchedulerActive = true
|
||||
Data.Settings.nightLightEnabled = false
|
||||
autoSchedulerActive = false
|
||||
}
|
||||
}
|
||||
|
||||
// Cleanup on destruction
|
||||
Component.onDestruction: {
|
||||
removeOverlay()
|
||||
}
|
||||
|
||||
// Initialize overlay state based on settings
|
||||
Component.onCompleted: {
|
||||
if (Data.Settings.nightLightEnabled) {
|
||||
createOverlay()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,67 @@
|
|||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import QtQuick.Controls
|
||||
import "root:/Data" as Data
|
||||
|
||||
// Screen recording toggle button
|
||||
Rectangle {
|
||||
id: root
|
||||
required property var shell
|
||||
required property bool isRecording
|
||||
radius: 20
|
||||
|
||||
signal recordingRequested()
|
||||
signal stopRecordingRequested()
|
||||
signal mouseChanged(bool containsMouse)
|
||||
|
||||
// Dynamic color: accent when recording/hovered, gray otherwise
|
||||
color: isRecording ? Data.ThemeManager.accentColor :
|
||||
(mouseArea.containsMouse ? Data.ThemeManager.accentColor : Qt.darker(Data.ThemeManager.bgColor, 1.15))
|
||||
|
||||
property bool isHovered: mouseArea.containsMouse
|
||||
readonly property alias containsMouse: mouseArea.containsMouse
|
||||
|
||||
// Button content with icon and text
|
||||
RowLayout {
|
||||
anchors.centerIn: parent
|
||||
spacing: 10
|
||||
|
||||
// Recording state icon
|
||||
Text {
|
||||
text: isRecording ? "stop_circle" : "radio_button_unchecked"
|
||||
font.family: "Material Symbols Outlined"
|
||||
font.pixelSize: 16
|
||||
color: isRecording || mouseArea.containsMouse ? "#ffffff" : Data.ThemeManager.fgColor
|
||||
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
}
|
||||
|
||||
// Recording state label
|
||||
Label {
|
||||
text: isRecording ? "Stop Recording" : "Start Recording"
|
||||
font.family: "Roboto"
|
||||
font.pixelSize: 13
|
||||
font.weight: Font.Medium
|
||||
color: isRecording || mouseArea.containsMouse ? "#ffffff" : Data.ThemeManager.fgColor
|
||||
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
}
|
||||
}
|
||||
|
||||
// Click handling and hover detection
|
||||
MouseArea {
|
||||
id: mouseArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
|
||||
onContainsMouseChanged: root.mouseChanged(containsMouse)
|
||||
|
||||
onClicked: {
|
||||
if (isRecording) {
|
||||
root.stopRecordingRequested()
|
||||
} else {
|
||||
root.recordingRequested()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import "root:/Data" as Data
|
||||
|
||||
// Simple theme toggle button with hover feedback
|
||||
Rectangle {
|
||||
id: root
|
||||
property var shell: null
|
||||
color: Qt.darker(Data.ThemeManager.bgColor, 1.15)
|
||||
radius: 20
|
||||
|
||||
property bool containsMouse: themeMouseArea.containsMouse
|
||||
property bool menuJustOpened: false
|
||||
|
||||
signal entered()
|
||||
signal exited()
|
||||
|
||||
// Hover state management for parent components
|
||||
onContainsMouseChanged: {
|
||||
if (containsMouse) {
|
||||
entered()
|
||||
} else if (!menuJustOpened) {
|
||||
exited()
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: themeMouseArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
onClicked: {
|
||||
Data.ThemeManager.toggleTheme()
|
||||
}
|
||||
}
|
||||
|
||||
// Theme toggle icon with color feedback
|
||||
Label {
|
||||
anchors.centerIn: parent
|
||||
text: "contrast"
|
||||
font.pixelSize: 24
|
||||
font.family: "Material Symbols Outlined"
|
||||
color: containsMouse ? Data.ThemeManager.accentColor : Data.ThemeManager.fgColor
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,243 @@
|
|||
import Quickshell.Io
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import Qt5Compat.GraphicalEffects
|
||||
import "root:/Data/" as Data
|
||||
|
||||
// User profile card
|
||||
Rectangle {
|
||||
id: root
|
||||
required property var shell
|
||||
property url avatarSource: Data.Settings.avatarSource
|
||||
property string userName: "" // will be set by process output
|
||||
property string userInfo: "" // will hold uptime string
|
||||
|
||||
property bool isActive: false
|
||||
property bool isHovered: false // track hover state
|
||||
|
||||
radius: 20
|
||||
width: 220
|
||||
height: 80
|
||||
|
||||
// Dynamic color based on hover and active states
|
||||
color: {
|
||||
if (isActive) {
|
||||
return isHovered ?
|
||||
Qt.lighter(Data.ThemeManager.accentColor, 1.1) :
|
||||
Qt.rgba(Data.ThemeManager.accentColor.r, Data.ThemeManager.accentColor.g, Data.ThemeManager.accentColor.b, 0.3)
|
||||
} else {
|
||||
return isHovered ?
|
||||
Qt.lighter(Data.ThemeManager.accentColor, 1.2) :
|
||||
Qt.lighter(Data.ThemeManager.bgColor, 1.15)
|
||||
}
|
||||
}
|
||||
|
||||
border.width: isActive ? 2 : 1
|
||||
border.color: isActive ? Data.ThemeManager.accentColor : Qt.lighter(Data.ThemeManager.bgColor, 1.3)
|
||||
|
||||
Row {
|
||||
anchors.fill: parent
|
||||
anchors.margins: 14
|
||||
spacing: 12
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
// Avatar
|
||||
Rectangle {
|
||||
id: avatarCircle
|
||||
width: 52
|
||||
height: 52
|
||||
radius: 20
|
||||
clip: true
|
||||
border.color: Data.ThemeManager.accentColor
|
||||
border.width: 3
|
||||
color: "transparent"
|
||||
|
||||
Image {
|
||||
id: avatarImage
|
||||
anchors.fill: parent
|
||||
anchors.margins: 2
|
||||
source: Data.Settings.avatarSource
|
||||
fillMode: Image.PreserveAspectCrop
|
||||
cache: false
|
||||
visible: false // Hidden for masking
|
||||
asynchronous: true
|
||||
sourceSize.width: 48 // Memory optimization
|
||||
sourceSize.height: 48
|
||||
}
|
||||
|
||||
// Apply circular mask to avatar
|
||||
OpacityMask {
|
||||
anchors.fill: avatarImage
|
||||
source: avatarImage
|
||||
cached: true // Cache to reduce ShaderEffect issues
|
||||
maskSource: Rectangle {
|
||||
width: avatarImage.width
|
||||
height: avatarImage.height
|
||||
radius: 18 // Proportional to parent radius
|
||||
visible: false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// User information text
|
||||
Column {
|
||||
spacing: 4
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
width: parent.width - avatarCircle.width - gifContainer.width - parent.spacing * 2
|
||||
|
||||
Text {
|
||||
width: parent.width
|
||||
text: root.userName === "" ? "Loading..." : root.userName
|
||||
font.family: "Roboto"
|
||||
font.pixelSize: 16
|
||||
font.bold: true
|
||||
color: isHovered || root.isActive ? "#ffffff" : Data.ThemeManager.accentColor
|
||||
elide: Text.ElideRight
|
||||
maximumLineCount: 1
|
||||
}
|
||||
|
||||
Text {
|
||||
width: parent.width
|
||||
text: root.userInfo === "" ? "Loading uptime..." : root.userInfo
|
||||
font.family: "Roboto"
|
||||
font.pixelSize: 11
|
||||
font.bold: true
|
||||
color: isHovered || root.isActive ? "#cccccc" : Qt.lighter(Data.ThemeManager.accentColor, 1.6)
|
||||
elide: Text.ElideRight
|
||||
maximumLineCount: 1
|
||||
}
|
||||
}
|
||||
|
||||
// Animated GIF with rounded corners
|
||||
Rectangle {
|
||||
id: gifContainer
|
||||
width: 80
|
||||
height: 80
|
||||
radius: 12
|
||||
color: "transparent"
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
AnimatedImage {
|
||||
id: animatedImage
|
||||
source: "root:/Assets/UserProfile.gif"
|
||||
anchors.fill: parent
|
||||
fillMode: Image.PreserveAspectFit
|
||||
playing: true
|
||||
cache: false
|
||||
speed: 1.0
|
||||
asynchronous: true
|
||||
}
|
||||
|
||||
// Apply rounded corner mask to GIF
|
||||
layer.enabled: true
|
||||
layer.effect: OpacityMask {
|
||||
cached: true // Cache to reduce ShaderEffect issues
|
||||
maskSource: Rectangle {
|
||||
width: gifContainer.width
|
||||
height: gifContainer.height
|
||||
radius: gifContainer.radius
|
||||
visible: false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: mouseArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onEntered: root.isHovered = true
|
||||
onExited: root.isHovered = false
|
||||
}
|
||||
|
||||
// Get current username
|
||||
Process {
|
||||
id: usernameProcess
|
||||
running: true // Always run to get username
|
||||
command: ["sh", "-c", "whoami"]
|
||||
|
||||
stdout: SplitParser {
|
||||
splitMarker: "\n"
|
||||
onRead: (data) => {
|
||||
const line = data.trim();
|
||||
if (line.length > 0) {
|
||||
root.userName = line.charAt(0).toUpperCase() + line.slice(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Get system uptime with parsing for readable format
|
||||
Process {
|
||||
id: uptimeProcess
|
||||
running: false
|
||||
command: ["sh", "-c", "uptime"] // Use basic uptime command
|
||||
|
||||
stdout: SplitParser {
|
||||
splitMarker: "\n"
|
||||
onRead: (data) => {
|
||||
const line = data.trim();
|
||||
if (line.length > 0) {
|
||||
// Parse uptime output: " 10:30:00 up 1:23, 2 users, load average: 0.08, 0.02, 0.01"
|
||||
const match = line.match(/up\s+(.+?),\s+\d+\s+user/);
|
||||
if (match && match[1]) {
|
||||
root.userInfo = "Up: " + match[1].trim();
|
||||
} else {
|
||||
// Fallback parsing for different uptime formats
|
||||
const upIndex = line.indexOf("up ");
|
||||
if (upIndex !== -1) {
|
||||
const afterUp = line.substring(upIndex + 3);
|
||||
const commaIndex = afterUp.indexOf(",");
|
||||
if (commaIndex !== -1) {
|
||||
root.userInfo = "Up: " + afterUp.substring(0, commaIndex).trim();
|
||||
} else {
|
||||
root.userInfo = "Up: " + afterUp.trim();
|
||||
}
|
||||
} else {
|
||||
root.userInfo = "Uptime unknown";
|
||||
}
|
||||
}
|
||||
} else {
|
||||
root.userInfo = "Uptime unknown";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
stderr: SplitParser {
|
||||
splitMarker: "\n"
|
||||
onRead: (data) => {
|
||||
console.log("Uptime error:", data);
|
||||
root.userInfo = "Uptime error";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Update uptime every 5 minutes
|
||||
Timer {
|
||||
id: uptimeTimer
|
||||
interval: 300000 // Update every 5 minutes
|
||||
running: true // Always run the uptime timer
|
||||
repeat: true
|
||||
onTriggered: {
|
||||
uptimeProcess.running = false
|
||||
uptimeProcess.running = true
|
||||
}
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
uptimeProcess.running = true // Start uptime process on component load
|
||||
}
|
||||
|
||||
Component.onDestruction: {
|
||||
if (usernameProcess.running) {
|
||||
usernameProcess.running = false
|
||||
}
|
||||
if (uptimeProcess.running) {
|
||||
uptimeProcess.running = false
|
||||
}
|
||||
if (uptimeTimer.running) {
|
||||
uptimeTimer.running = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,354 @@
|
|||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import "root:/Data" as Data
|
||||
|
||||
// Weather display widget
|
||||
Rectangle {
|
||||
id: root
|
||||
required property var shell
|
||||
color: Qt.darker(Data.ThemeManager.bgColor, 1.15)
|
||||
radius: 20
|
||||
|
||||
property bool containsMouse: weatherMouseArea.containsMouse || (forecastPopup.visible && forecastPopup.containsMouse)
|
||||
property bool menuJustOpened: false
|
||||
|
||||
signal entered()
|
||||
signal exited()
|
||||
|
||||
// Hover state management for parent components
|
||||
onContainsMouseChanged: {
|
||||
if (containsMouse) {
|
||||
entered()
|
||||
} else if (!menuJustOpened && !forecastPopup.visible) {
|
||||
exited()
|
||||
}
|
||||
}
|
||||
|
||||
// Maps WMO weather condition codes and text descriptions to Material Design icons
|
||||
function getWeatherIcon(condition) {
|
||||
if (!condition) return "light_mode"
|
||||
|
||||
const c = condition.toString()
|
||||
|
||||
// WMO weather interpretation codes to Material Design icons
|
||||
const iconMap = {
|
||||
"0": "light_mode", // Clear sky
|
||||
"1": "light_mode", // Mainly clear
|
||||
"2": "cloud", // Partly cloudy
|
||||
"3": "cloud", // Overcast
|
||||
"45": "foggy", // Fog
|
||||
"48": "foggy", // Depositing rime fog
|
||||
"51": "water_drop", // Light drizzle
|
||||
"53": "water_drop", // Moderate drizzle
|
||||
"55": "water_drop", // Dense drizzle
|
||||
"61": "water_drop", // Slight rain
|
||||
"63": "water_drop", // Moderate rain
|
||||
"65": "water_drop", // Heavy rain
|
||||
"71": "ac_unit", // Slight snow
|
||||
"73": "ac_unit", // Moderate snow
|
||||
"75": "ac_unit", // Heavy snow
|
||||
"80": "water_drop", // Slight rain showers
|
||||
"81": "water_drop", // Moderate rain showers
|
||||
"82": "water_drop", // Violent rain showers
|
||||
"95": "thunderstorm", // Thunderstorm
|
||||
"96": "thunderstorm", // Thunderstorm with light hail
|
||||
"99": "thunderstorm" // Thunderstorm with heavy hail
|
||||
}
|
||||
|
||||
if (iconMap[c]) return iconMap[c]
|
||||
|
||||
// Fallback text matching for non-WMO weather APIs
|
||||
const textMap = {
|
||||
"clear sky": "light_mode",
|
||||
"mainly clear": "light_mode",
|
||||
"partly cloudy": "cloud",
|
||||
"overcast": "cloud",
|
||||
"fog": "foggy",
|
||||
"drizzle": "water_drop",
|
||||
"rain": "water_drop",
|
||||
"snow": "ac_unit",
|
||||
"thunderstorm": "thunderstorm"
|
||||
}
|
||||
|
||||
const lower = condition.toLowerCase()
|
||||
for (let key in textMap) {
|
||||
if (lower.includes(key)) return textMap[key]
|
||||
}
|
||||
|
||||
return "help" // Unknown condition fallback
|
||||
}
|
||||
|
||||
// Hover trigger for forecast popup
|
||||
MouseArea {
|
||||
id: weatherMouseArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
onEntered: {
|
||||
menuJustOpened = true
|
||||
forecastPopup.open()
|
||||
Qt.callLater(() => menuJustOpened = false)
|
||||
}
|
||||
onExited: {
|
||||
if (!forecastPopup.containsMouse && !menuJustOpened) {
|
||||
forecastPopup.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Compact weather display (icon and temperature)
|
||||
RowLayout {
|
||||
id: weatherLayout
|
||||
anchors.centerIn: parent
|
||||
spacing: 8
|
||||
|
||||
ColumnLayout {
|
||||
spacing: 2
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
|
||||
// Weather condition icon
|
||||
Label {
|
||||
text: {
|
||||
if (shell.weatherLoading) return "refresh"
|
||||
if (!shell.weatherData) return "help"
|
||||
return root.getWeatherIcon(shell.weatherData.currentCondition)
|
||||
}
|
||||
font.pixelSize: 28
|
||||
font.family: "Material Symbols Outlined"
|
||||
color: Data.ThemeManager.accentColor
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
}
|
||||
|
||||
// Current temperature
|
||||
Label {
|
||||
text: {
|
||||
if (shell.weatherLoading) return "Loading..."
|
||||
if (!shell.weatherData) return "No weather data"
|
||||
return shell.weatherData.currentTemp
|
||||
}
|
||||
color: Data.ThemeManager.fgColor
|
||||
font.family: "Roboto"
|
||||
font.pixelSize: 20
|
||||
font.bold: true
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Forecast popup
|
||||
Popup {
|
||||
id: forecastPopup
|
||||
y: parent.height + 28
|
||||
x: Math.min(0, parent.width - width)
|
||||
width: 300
|
||||
height: 226
|
||||
padding: 12
|
||||
background: Rectangle {
|
||||
color: Qt.darker(Data.ThemeManager.bgColor, 1.15)
|
||||
radius: 20
|
||||
border.width: 1
|
||||
border.color: Qt.lighter(Data.ThemeManager.bgColor, 1.3)
|
||||
}
|
||||
|
||||
property bool containsMouse: forecastMouseArea.containsMouse
|
||||
|
||||
onVisibleChanged: {
|
||||
if (visible) {
|
||||
entered()
|
||||
} else if (!weatherMouseArea.containsMouse && !menuJustOpened) {
|
||||
exited()
|
||||
}
|
||||
}
|
||||
|
||||
// Hover area for popup persistence
|
||||
MouseArea {
|
||||
id: forecastMouseArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
onExited: {
|
||||
if (!weatherMouseArea.containsMouse && !menuJustOpened) {
|
||||
forecastPopup.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
id: forecastColumn
|
||||
anchors.fill: parent
|
||||
anchors.margins: 10
|
||||
spacing: 8
|
||||
|
||||
// Current weather detailed view
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: 12
|
||||
|
||||
// Large weather icon
|
||||
Label {
|
||||
text: shell.weatherData ? root.getWeatherIcon(shell.weatherData.currentCondition) : ""
|
||||
font.pixelSize: 48
|
||||
font.family: "Material Symbols Outlined"
|
||||
color: Data.ThemeManager.accentColor
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: 4
|
||||
|
||||
// Weather condition description
|
||||
Label {
|
||||
text: shell.weatherData ? shell.weatherData.currentCondition : ""
|
||||
color: Data.ThemeManager.fgColor
|
||||
font.family: "Roboto"
|
||||
font.pixelSize: 14
|
||||
font.bold: true
|
||||
Layout.fillWidth: true
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
|
||||
// Weather metrics: temperature, wind, direction
|
||||
RowLayout {
|
||||
spacing: 8
|
||||
Layout.fillWidth: true
|
||||
Layout.alignment: Qt.AlignVCenter | Qt.AlignLeft
|
||||
|
||||
// Temperature metric
|
||||
RowLayout {
|
||||
spacing: 4
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
Label {
|
||||
text: "thermostat"
|
||||
font.family: "Material Symbols Outlined"
|
||||
font.pixelSize: 12
|
||||
color: Data.ThemeManager.accentColor
|
||||
}
|
||||
Label {
|
||||
text: shell.weatherData ? shell.weatherData.currentTemp : ""
|
||||
color: Data.ThemeManager.fgColor
|
||||
font.family: "Roboto"
|
||||
font.pixelSize: 12
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: 1
|
||||
height: 12
|
||||
color: Qt.lighter(Data.ThemeManager.bgColor, 1.3)
|
||||
}
|
||||
|
||||
// Wind speed metric
|
||||
RowLayout {
|
||||
spacing: 4
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
Label {
|
||||
text: "air"
|
||||
font.family: "Material Symbols Outlined"
|
||||
font.pixelSize: 12
|
||||
color: Data.ThemeManager.accentColor
|
||||
}
|
||||
Label {
|
||||
text: {
|
||||
if (!shell.weatherData || !shell.weatherData.details) return ""
|
||||
const windInfo = shell.weatherData.details.find(d => d.startsWith("Wind:"))
|
||||
return windInfo ? windInfo.split(": ")[1] : ""
|
||||
}
|
||||
color: Data.ThemeManager.fgColor
|
||||
font.family: "Roboto"
|
||||
font.pixelSize: 12
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: 1
|
||||
height: 12
|
||||
color: Qt.lighter(Data.ThemeManager.bgColor, 1.3)
|
||||
}
|
||||
|
||||
// Wind direction metric
|
||||
RowLayout {
|
||||
spacing: 4
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
Label {
|
||||
text: "explore"
|
||||
font.family: "Material Symbols Outlined"
|
||||
font.pixelSize: 12
|
||||
color: Data.ThemeManager.accentColor
|
||||
}
|
||||
Label {
|
||||
text: {
|
||||
if (!shell.weatherData || !shell.weatherData.details) return ""
|
||||
const dirInfo = shell.weatherData.details.find(d => d.startsWith("Direction:"))
|
||||
return dirInfo ? dirInfo.split(": ")[1] : ""
|
||||
}
|
||||
color: Data.ThemeManager.fgColor
|
||||
font.family: "Roboto"
|
||||
font.pixelSize: 12
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Section separator
|
||||
Rectangle {
|
||||
height: 1
|
||||
Layout.fillWidth: true
|
||||
color: Qt.lighter(Data.ThemeManager.bgColor, 1.3)
|
||||
}
|
||||
|
||||
Label {
|
||||
text: "3-Day Forecast"
|
||||
color: Data.ThemeManager.accentColor
|
||||
font.family: "Roboto"
|
||||
font.pixelSize: 12
|
||||
font.bold: true
|
||||
}
|
||||
|
||||
// Three-column forecast cards
|
||||
Row {
|
||||
spacing: 8
|
||||
Layout.fillWidth: true
|
||||
|
||||
Repeater {
|
||||
model: shell.weatherData ? shell.weatherData.forecast : []
|
||||
delegate: Column {
|
||||
width: (parent.width - 16) / 3
|
||||
spacing: 2
|
||||
|
||||
// Day name
|
||||
Label {
|
||||
text: modelData.dayName
|
||||
color: Data.ThemeManager.fgColor
|
||||
font.family: "Roboto"
|
||||
font.pixelSize: 10
|
||||
font.bold: true
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
}
|
||||
|
||||
// Weather icon
|
||||
Label {
|
||||
text: root.getWeatherIcon(modelData.condition)
|
||||
font.pixelSize: 16
|
||||
font.family: "Material Symbols Outlined"
|
||||
color: Data.ThemeManager.accentColor
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
}
|
||||
|
||||
// Temperature range
|
||||
Label {
|
||||
text: modelData.minTemp + "° - " + modelData.maxTemp + "°"
|
||||
color: Data.ThemeManager.fgColor
|
||||
font.family: "Roboto"
|
||||
font.pixelSize: 10
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,147 @@
|
|||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import "root:/Data" as Data
|
||||
|
||||
// Calendar tab content
|
||||
Item {
|
||||
id: calendarTab
|
||||
|
||||
required property var shell
|
||||
property bool isActive: false
|
||||
|
||||
Column {
|
||||
anchors.fill: parent
|
||||
spacing: 12
|
||||
|
||||
Text {
|
||||
text: "Calendar"
|
||||
color: Data.ThemeManager.accentColor
|
||||
font.pixelSize: 18
|
||||
font.bold: true
|
||||
font.family: "Roboto"
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: parent.height - parent.children[0].height - parent.spacing
|
||||
color: Qt.lighter(Data.ThemeManager.bgColor, 1.2)
|
||||
radius: 20
|
||||
clip: true
|
||||
|
||||
Loader {
|
||||
anchors.fill: parent
|
||||
anchors.margins: 16
|
||||
active: calendarTab.isActive
|
||||
sourceComponent: active ? calendarComponent : null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: calendarComponent
|
||||
Item {
|
||||
id: calendarRoot
|
||||
property var shell: calendarTab.shell
|
||||
|
||||
readonly property date currentDate: new Date()
|
||||
property int month: currentDate.getMonth()
|
||||
property int year: currentDate.getFullYear()
|
||||
readonly property int currentDay: currentDate.getDate()
|
||||
|
||||
Column {
|
||||
anchors.fill: parent
|
||||
anchors.margins: 8
|
||||
spacing: 8
|
||||
|
||||
// Month/Year header
|
||||
Text {
|
||||
text: Qt.locale("en_US").monthName(calendarRoot.month) + " " + calendarRoot.year
|
||||
color: Data.ThemeManager.accentColor
|
||||
font.bold: true
|
||||
width: parent.width
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
font.pixelSize: 16
|
||||
height: 24
|
||||
}
|
||||
|
||||
// Weekday headers (Monday-Sunday)
|
||||
Grid {
|
||||
columns: 7
|
||||
rowSpacing: 2
|
||||
columnSpacing: 0
|
||||
width: parent.width
|
||||
height: 18
|
||||
|
||||
Repeater {
|
||||
model: ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]
|
||||
delegate: Text {
|
||||
text: modelData
|
||||
color: Data.ThemeManager.fgColor
|
||||
font.bold: true
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
width: parent.width / 7
|
||||
height: 18
|
||||
font.pixelSize: 11
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Calendar grid - single unified grid
|
||||
Grid {
|
||||
columns: 7
|
||||
rowSpacing: 3
|
||||
columnSpacing: 3
|
||||
width: parent.width
|
||||
|
||||
property int firstDayOfMonth: new Date(calendarRoot.year, calendarRoot.month, 1).getDay()
|
||||
property int daysInMonth: new Date(calendarRoot.year, calendarRoot.month + 1, 0).getDate()
|
||||
property int startOffset: (firstDayOfMonth === 0) ? 6 : firstDayOfMonth - 1 // Convert Sunday=0 to Monday=0
|
||||
property int prevMonthDays: new Date(calendarRoot.year, calendarRoot.month, 0).getDate()
|
||||
|
||||
// Single repeater for all 42 calendar cells (6 weeks × 7 days)
|
||||
Repeater {
|
||||
model: 42
|
||||
delegate: Rectangle {
|
||||
width: (parent.width - (parent.columnSpacing * 6)) / 7
|
||||
height: 26
|
||||
radius: 13
|
||||
|
||||
// Calculate which day this cell represents
|
||||
readonly property int dayNumber: {
|
||||
if (index < parent.startOffset) {
|
||||
// Previous month
|
||||
return parent.prevMonthDays - parent.startOffset + index + 1
|
||||
} else if (index < parent.startOffset + parent.daysInMonth) {
|
||||
// Current month
|
||||
return index - parent.startOffset + 1
|
||||
} else {
|
||||
// Next month
|
||||
return index - parent.startOffset - parent.daysInMonth + 1
|
||||
}
|
||||
}
|
||||
|
||||
readonly property bool isCurrentMonth: index >= parent.startOffset && index < (parent.startOffset + parent.daysInMonth)
|
||||
readonly property bool isToday: isCurrentMonth && dayNumber === calendarRoot.currentDay &&
|
||||
calendarRoot.month === calendarRoot.currentDate.getMonth() &&
|
||||
calendarRoot.year === calendarRoot.currentDate.getFullYear()
|
||||
|
||||
color: isToday ? Data.ThemeManager.accentColor :
|
||||
isCurrentMonth ? Data.ThemeManager.bgColor : Qt.darker(Data.ThemeManager.bgColor, 1.4)
|
||||
|
||||
Text {
|
||||
text: dayNumber
|
||||
anchors.centerIn: parent
|
||||
color: isToday ? Data.ThemeManager.bgColor :
|
||||
isCurrentMonth ? Data.ThemeManager.fgColor : Qt.darker(Data.ThemeManager.fgColor, 1.5)
|
||||
font.bold: isToday
|
||||
font.pixelSize: 12
|
||||
font.family: "Roboto"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,110 @@
|
|||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import "root:/Data" as Data
|
||||
import "root:/Widgets/System" as System
|
||||
|
||||
// Clipboard tab content
|
||||
Item {
|
||||
id: clipboardTab
|
||||
|
||||
required property var shell
|
||||
property bool isActive: false
|
||||
|
||||
Column {
|
||||
anchors.fill: parent
|
||||
spacing: 16
|
||||
|
||||
RowLayout {
|
||||
width: parent.width
|
||||
spacing: 16
|
||||
|
||||
Text {
|
||||
text: "Clipboard History"
|
||||
color: Data.ThemeManager.accentColor
|
||||
font.pixelSize: 18
|
||||
font.bold: true
|
||||
font.family: "Roboto"
|
||||
}
|
||||
|
||||
Item { Layout.fillWidth: true }
|
||||
|
||||
Rectangle {
|
||||
width: clearClipText.implicitWidth + 16
|
||||
height: 24
|
||||
radius: 12
|
||||
color: clearClipMouseArea.containsMouse ? Qt.rgba(Data.ThemeManager.accentColor.r, Data.ThemeManager.accentColor.g, Data.ThemeManager.accentColor.b, 0.2) : "transparent"
|
||||
border.color: Data.ThemeManager.accentColor
|
||||
border.width: 1
|
||||
|
||||
Text {
|
||||
id: clearClipText
|
||||
anchors.centerIn: parent
|
||||
text: "Clear All"
|
||||
color: Data.ThemeManager.accentColor
|
||||
font.family: "Roboto"
|
||||
font.pixelSize: 11
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: clearClipMouseArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
onClicked: {
|
||||
if (clipboardLoader.item && clipboardLoader.item.children[0]) {
|
||||
let clipComponent = clipboardLoader.item.children[0]
|
||||
if (clipComponent.clearClipboardHistory) {
|
||||
clipComponent.clearClipboardHistory()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: parent.height - parent.children[0].height - parent.spacing
|
||||
color: Qt.lighter(Data.ThemeManager.bgColor, 1.2)
|
||||
radius: 20
|
||||
clip: true
|
||||
|
||||
Loader {
|
||||
id: clipboardLoader
|
||||
anchors.fill: parent
|
||||
anchors.margins: 20
|
||||
active: clipboardTab.isActive
|
||||
sourceComponent: active ? clipboardHistoryComponent : null
|
||||
onLoaded: {
|
||||
if (item && item.children[0]) {
|
||||
item.children[0].refreshClipboardHistory()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: clipboardHistoryComponent
|
||||
Item {
|
||||
anchors.fill: parent
|
||||
|
||||
System.Cliphist {
|
||||
id: cliphistComponent
|
||||
anchors.fill: parent
|
||||
shell: clipboardTab.shell
|
||||
|
||||
Component.onCompleted: {
|
||||
for (let i = 0; i < children.length; i++) {
|
||||
let child = children[i]
|
||||
if (child.objectName === "contentColumn" || child.toString().includes("ColumnLayout")) {
|
||||
if (child.children && child.children.length > 0) {
|
||||
child.children[0].visible = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,155 @@
|
|||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import "root:/Data" as Data
|
||||
import "root:/Widgets/System" as System
|
||||
import "../components/widgets" as Widgets
|
||||
import "../components/controls" as Controls
|
||||
import "../components/system" as SystemComponents
|
||||
|
||||
// Main dashboard content (tab 0)
|
||||
Item {
|
||||
id: mainDashboard
|
||||
|
||||
// Properties from parent
|
||||
required property var shell
|
||||
required property bool isRecording
|
||||
required property var triggerMouseArea
|
||||
|
||||
// Signals to forward
|
||||
signal recordingRequested()
|
||||
signal stopRecordingRequested()
|
||||
signal systemActionRequested(string action)
|
||||
signal performanceActionRequested(string action)
|
||||
|
||||
// Hover detection for auto-hide
|
||||
property bool isHovered: {
|
||||
const mouseStates = {
|
||||
userProfileHovered: userProfile ? userProfile.isHovered : false,
|
||||
weatherDisplayHovered: weatherDisplay ? weatherDisplay.containsMouse : false,
|
||||
recordingButtonHovered: recordingButton ? recordingButton.isHovered : false,
|
||||
controlsHovered: controls ? controls.containsMouse : false,
|
||||
trayHovered: trayMouseArea ? trayMouseArea.containsMouse : false,
|
||||
systemTrayHovered: systemTrayModule ? systemTrayModule.containsMouse : false,
|
||||
trayMenuHovered: inlineTrayMenu ? inlineTrayMenu.containsMouse : false,
|
||||
trayMenuVisible: inlineTrayMenu ? inlineTrayMenu.visible : false
|
||||
}
|
||||
return Object.values(mouseStates).some(state => state)
|
||||
}
|
||||
|
||||
// Night Light overlay controller (invisible - manages screen overlay)
|
||||
Widgets.NightLight {
|
||||
id: nightLightController
|
||||
shell: mainDashboard.shell
|
||||
visible: false // This widget manages overlay windows, doesn't need to be visible
|
||||
}
|
||||
|
||||
Column {
|
||||
anchors.fill: parent
|
||||
spacing: 28
|
||||
|
||||
// User profile row with weather
|
||||
Row {
|
||||
width: parent.width
|
||||
spacing: 18
|
||||
|
||||
Widgets.UserProfile {
|
||||
id: userProfile
|
||||
width: parent.width - weatherDisplay.width - parent.spacing
|
||||
height: 80
|
||||
shell: mainDashboard.shell
|
||||
}
|
||||
|
||||
Widgets.WeatherDisplay {
|
||||
id: weatherDisplay
|
||||
width: parent.width * 0.18
|
||||
height: userProfile.height
|
||||
shell: mainDashboard.shell
|
||||
}
|
||||
}
|
||||
|
||||
// Recording and system controls section
|
||||
Column {
|
||||
width: parent.width
|
||||
spacing: 28
|
||||
|
||||
Widgets.RecordingButton {
|
||||
id: recordingButton
|
||||
width: parent.width
|
||||
height: 48
|
||||
shell: mainDashboard.shell
|
||||
isRecording: mainDashboard.isRecording
|
||||
|
||||
onRecordingRequested: mainDashboard.recordingRequested()
|
||||
onStopRecordingRequested: mainDashboard.stopRecordingRequested()
|
||||
}
|
||||
|
||||
Controls.Controls {
|
||||
id: controls
|
||||
width: parent.width
|
||||
isRecording: mainDashboard.isRecording
|
||||
shell: mainDashboard.shell
|
||||
onPerformanceActionRequested: function(action) { mainDashboard.performanceActionRequested(action) }
|
||||
onSystemActionRequested: function(action) { mainDashboard.systemActionRequested(action) }
|
||||
}
|
||||
}
|
||||
|
||||
// System tray integration with menu
|
||||
Column {
|
||||
id: systemTraySection
|
||||
width: parent.width
|
||||
spacing: 8
|
||||
|
||||
property bool containsMouse: trayMouseArea.containsMouse || systemTrayModule.containsMouse
|
||||
|
||||
Rectangle {
|
||||
id: trayBackground
|
||||
width: parent.width
|
||||
height: 40
|
||||
radius: 20
|
||||
color: Qt.darker(Data.ThemeManager.bgColor, 1.15)
|
||||
|
||||
property bool isActive: false
|
||||
|
||||
MouseArea {
|
||||
id: trayMouseArea
|
||||
anchors.fill: parent
|
||||
anchors.margins: -10
|
||||
hoverEnabled: true
|
||||
propagateComposedEvents: true
|
||||
preventStealing: false
|
||||
onEntered: trayBackground.isActive = true
|
||||
onExited: {
|
||||
// Only deactivate if we're not hovering over tray menu or system tray module
|
||||
if (!inlineTrayMenu.visible && !inlineTrayMenu.containsMouse) {
|
||||
Qt.callLater(function() {
|
||||
if (!systemTrayModule.containsMouse && !inlineTrayMenu.containsMouse && !inlineTrayMenu.visible) {
|
||||
trayBackground.isActive = false
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
System.SystemTray {
|
||||
id: systemTrayModule
|
||||
anchors.centerIn: parent
|
||||
shell: mainDashboard.shell
|
||||
bar: parent
|
||||
trayMenu: inlineTrayMenu
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SystemComponents.TrayMenu {
|
||||
id: inlineTrayMenu
|
||||
parent: mainDashboard
|
||||
width: parent.width
|
||||
menu: null
|
||||
systemTrayY: systemTraySection.y
|
||||
systemTrayHeight: systemTraySection.height
|
||||
z: 100 // High z-index to appear above other content
|
||||
onHideRequested: trayBackground.isActive = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
import QtQuick
|
||||
import "root:/Data" as Data
|
||||
import "../components/media" as Media
|
||||
|
||||
// Music tab content
|
||||
Item {
|
||||
id: musicTab
|
||||
|
||||
required property var shell
|
||||
property bool isActive: false
|
||||
|
||||
Column {
|
||||
anchors.fill: parent
|
||||
spacing: 16
|
||||
|
||||
Text {
|
||||
text: "Music Player"
|
||||
color: Data.ThemeManager.accentColor
|
||||
font.pixelSize: 18
|
||||
font.bold: true
|
||||
font.family: "Roboto"
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: parent.height - parent.children[0].height - parent.spacing
|
||||
color: Qt.lighter(Data.ThemeManager.bgColor, 1.2)
|
||||
radius: 20
|
||||
clip: true
|
||||
|
||||
Loader {
|
||||
anchors.fill: parent
|
||||
anchors.margins: 20
|
||||
active: musicTab.isActive
|
||||
sourceComponent: active ? musicPlayerComponent : null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: musicPlayerComponent
|
||||
Media.MusicPlayer {
|
||||
shell: musicTab.shell
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,106 @@
|
|||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import "root:/Data" as Data
|
||||
import "root:/Widgets/Notifications" as Notifications
|
||||
|
||||
// Notification tab content
|
||||
Item {
|
||||
id: notificationTab
|
||||
|
||||
required property var shell
|
||||
property bool isActive: false
|
||||
|
||||
Column {
|
||||
anchors.fill: parent
|
||||
spacing: 16
|
||||
|
||||
RowLayout {
|
||||
width: parent.width
|
||||
spacing: 16
|
||||
|
||||
Text {
|
||||
text: "Notification History"
|
||||
color: Data.ThemeManager.accentColor
|
||||
font.pixelSize: 18
|
||||
font.bold: true
|
||||
font.family: "Roboto"
|
||||
}
|
||||
|
||||
Text {
|
||||
text: "(" + (notificationTab.shell.notificationHistory ? notificationTab.shell.notificationHistory.count : 0) + ")"
|
||||
color: Data.ThemeManager.fgColor
|
||||
font.family: "Roboto"
|
||||
font.pixelSize: 12
|
||||
opacity: 0.7
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
}
|
||||
|
||||
Item { Layout.fillWidth: true }
|
||||
|
||||
Rectangle {
|
||||
width: clearNotifText.implicitWidth + 16
|
||||
height: 24
|
||||
radius: 12
|
||||
color: clearNotifMouseArea.containsMouse ? Qt.rgba(Data.ThemeManager.accentColor.r, Data.ThemeManager.accentColor.g, Data.ThemeManager.accentColor.b, 0.2) : "transparent"
|
||||
border.color: Data.ThemeManager.accentColor
|
||||
border.width: 1
|
||||
|
||||
Text {
|
||||
id: clearNotifText
|
||||
anchors.centerIn: parent
|
||||
text: "Clear All"
|
||||
color: Data.ThemeManager.accentColor
|
||||
font.family: "Roboto"
|
||||
font.pixelSize: 11
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: clearNotifMouseArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
onClicked: notificationTab.shell.notificationHistory.clear()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: parent.height - parent.children[0].height - parent.spacing
|
||||
color: Qt.lighter(Data.ThemeManager.bgColor, 1.2)
|
||||
radius: 20
|
||||
clip: true
|
||||
|
||||
Loader {
|
||||
anchors.fill: parent
|
||||
anchors.margins: 20
|
||||
active: notificationTab.isActive
|
||||
sourceComponent: active ? notificationHistoryComponent : null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: notificationHistoryComponent
|
||||
Item {
|
||||
anchors.fill: parent
|
||||
|
||||
Notifications.NotificationHistory {
|
||||
anchors.fill: parent
|
||||
shell: notificationTab.shell
|
||||
clip: true
|
||||
|
||||
Component.onCompleted: {
|
||||
for (let i = 0; i < children.length; i++) {
|
||||
let child = children[i]
|
||||
if (child.objectName === "contentColumn" || child.toString().includes("ColumnLayout")) {
|
||||
if (child.children && child.children.length > 0) {
|
||||
child.children[0].visible = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,153 @@
|
|||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import "root:/Data" as Data
|
||||
import "../components/settings" as SettingsComponents
|
||||
|
||||
// Settings tab content with modular, collapsible categories
|
||||
Item {
|
||||
id: settingsTab
|
||||
|
||||
required property var shell
|
||||
property bool isActive: false
|
||||
|
||||
// Track when any text input has focus for keyboard management
|
||||
property bool anyTextInputFocused: {
|
||||
try {
|
||||
return (notificationSettings && notificationSettings.anyTextInputFocused) ||
|
||||
(systemSettings && systemSettings.anyTextInputFocused) ||
|
||||
(weatherSettings && weatherSettings.anyTextInputFocused)
|
||||
} catch (e) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// Header
|
||||
Text {
|
||||
id: header
|
||||
anchors.top: parent.top
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.margins: 20
|
||||
text: "Settings"
|
||||
color: Data.ThemeManager.accentColor
|
||||
font.pixelSize: 24
|
||||
font.bold: true
|
||||
font.family: "Roboto"
|
||||
}
|
||||
|
||||
// Scrollable content
|
||||
ScrollView {
|
||||
anchors.top: header.bottom
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.topMargin: 16
|
||||
anchors.leftMargin: 20
|
||||
anchors.rightMargin: 20
|
||||
anchors.bottomMargin: 20
|
||||
|
||||
clip: true
|
||||
contentWidth: width - 5 // Reserve space for scrollbar
|
||||
|
||||
ScrollBar.vertical.policy: ScrollBar.AsNeeded
|
||||
ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
|
||||
|
||||
Column {
|
||||
width: parent.width - 15 // Match contentWidth
|
||||
spacing: 16
|
||||
|
||||
// VISUAL SETTINGS
|
||||
// Appearance Category
|
||||
SettingsComponents.SettingsCategory {
|
||||
id: appearanceCategory
|
||||
width: parent.width
|
||||
title: "Appearance"
|
||||
icon: "palette"
|
||||
|
||||
content: Component {
|
||||
SettingsComponents.AppearanceSettings {
|
||||
width: parent.width
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ⚙️ CORE SYSTEM SETTINGS
|
||||
// System Category
|
||||
SettingsComponents.SettingsCategory {
|
||||
id: systemCategory
|
||||
width: parent.width
|
||||
title: "System"
|
||||
icon: "settings"
|
||||
|
||||
content: Component {
|
||||
SettingsComponents.SystemSettings {
|
||||
id: systemSettings
|
||||
width: parent.width
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Notifications Category
|
||||
SettingsComponents.SettingsCategory {
|
||||
id: notificationsCategory
|
||||
width: parent.width
|
||||
title: "Notifications"
|
||||
icon: "notifications"
|
||||
|
||||
content: Component {
|
||||
SettingsComponents.NotificationSettings {
|
||||
id: notificationSettings
|
||||
width: parent.width
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 🎵 MEDIA & EXTERNAL SERVICES
|
||||
// Music Player Category
|
||||
SettingsComponents.SettingsCategory {
|
||||
id: musicPlayerCategory
|
||||
width: parent.width
|
||||
title: "Music Player"
|
||||
icon: "music_note"
|
||||
|
||||
content: Component {
|
||||
SettingsComponents.MusicPlayerSettings {
|
||||
width: parent.width
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Weather Category
|
||||
SettingsComponents.SettingsCategory {
|
||||
id: weatherCategory
|
||||
width: parent.width
|
||||
title: "Weather"
|
||||
icon: "wb_sunny"
|
||||
|
||||
content: Component {
|
||||
SettingsComponents.WeatherSettings {
|
||||
id: weatherSettings
|
||||
width: parent.width
|
||||
shell: settingsTab.shell
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ACCESSIBILITY & COMFORT
|
||||
// Night Light Category
|
||||
SettingsComponents.SettingsCategory {
|
||||
id: nightLightCategory
|
||||
width: parent.width
|
||||
title: "Night Light"
|
||||
icon: "dark_mode"
|
||||
|
||||
content: Component {
|
||||
SettingsComponents.NightLightSettings {
|
||||
width: parent.width
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
import QtQuick
|
||||
import "root:/Data" as Data
|
||||
import "../components/system" as SystemComponents
|
||||
|
||||
// Wallpaper tab content
|
||||
Item {
|
||||
id: wallpaperTab
|
||||
|
||||
property bool isActive: false
|
||||
|
||||
Column {
|
||||
anchors.fill: parent
|
||||
spacing: 16
|
||||
|
||||
Text {
|
||||
text: "Wallpapers"
|
||||
color: Data.ThemeManager.accentColor
|
||||
font.pixelSize: 18
|
||||
font.bold: true
|
||||
font.family: "Roboto"
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: parent.height - parent.children[0].height - parent.spacing
|
||||
color: Qt.lighter(Data.ThemeManager.bgColor, 1.2)
|
||||
radius: 20
|
||||
clip: true
|
||||
|
||||
Loader {
|
||||
anchors.fill: parent
|
||||
anchors.margins: 20
|
||||
active: wallpaperTab.isActive
|
||||
sourceComponent: active ? wallpaperSelectorComponent : null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: wallpaperSelectorComponent
|
||||
SystemComponents.WallpaperSelector {
|
||||
isVisible: parent && parent.parent && parent.parent.visible
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,262 @@
|
|||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import Qt5Compat.GraphicalEffects
|
||||
import "root:/Data" as Data
|
||||
|
||||
// Authentication area
|
||||
Column {
|
||||
id: authColumn
|
||||
anchors.centerIn: parent
|
||||
anchors.verticalCenterOffset: 80
|
||||
spacing: 20
|
||||
width: 300
|
||||
|
||||
required property bool isVisible
|
||||
required property string errorMessage
|
||||
required property bool isAuthenticating
|
||||
required property bool authSuccess
|
||||
required property string usernameText
|
||||
|
||||
signal passwordEntered(string password)
|
||||
|
||||
// Expose password field
|
||||
readonly property alias passwordField: passwordField
|
||||
|
||||
// Subtle slide up animation (after main slide)
|
||||
transform: Translate {
|
||||
id: authTransform
|
||||
y: isVisible ? 0 : 50
|
||||
Behavior on y {
|
||||
SequentialAnimation {
|
||||
PauseAnimation { duration: 600 } // Wait for main slide and time
|
||||
NumberAnimation {
|
||||
duration: 600
|
||||
easing.type: Easing.OutBack
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
opacity: isVisible ? 1.0 : 0.0
|
||||
Behavior on opacity {
|
||||
SequentialAnimation {
|
||||
PauseAnimation { duration: 700 } // Wait for time to appear
|
||||
NumberAnimation {
|
||||
duration: 600
|
||||
easing.type: Easing.OutCubic
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// User avatar with circular masking
|
||||
Rectangle {
|
||||
id: avatarContainer
|
||||
width: 100
|
||||
height: 100
|
||||
radius: 50
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
color: "transparent"
|
||||
border.color: Data.ThemeManager.accentColor
|
||||
border.width: 4
|
||||
clip: true
|
||||
|
||||
// Scale animation for avatar
|
||||
scale: isVisible ? 1.0 : 0.0
|
||||
Behavior on scale {
|
||||
SequentialAnimation {
|
||||
PauseAnimation { duration: 1000 } // Wait for auth area to appear
|
||||
NumberAnimation {
|
||||
duration: 400
|
||||
easing.type: Easing.OutBack
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Image {
|
||||
id: avatarImage
|
||||
anchors.fill: parent
|
||||
anchors.margins: 4
|
||||
source: Data.Settings.avatarSource
|
||||
fillMode: Image.PreserveAspectCrop
|
||||
cache: false
|
||||
visible: false // Hidden for masking
|
||||
asynchronous: true
|
||||
sourceSize.width: 92
|
||||
sourceSize.height: 92
|
||||
}
|
||||
|
||||
// Apply circular mask to avatar
|
||||
OpacityMask {
|
||||
anchors.fill: avatarImage
|
||||
source: avatarImage
|
||||
maskSource: Rectangle {
|
||||
width: avatarImage.width
|
||||
height: avatarImage.height
|
||||
radius: 46
|
||||
visible: false
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback icon if avatar fails to load
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
text: "person"
|
||||
font.family: "Material Symbols Outlined"
|
||||
font.pixelSize: 48
|
||||
color: Data.ThemeManager.accentColor
|
||||
visible: avatarImage.status !== Image.Ready
|
||||
}
|
||||
}
|
||||
|
||||
// Username display
|
||||
Text {
|
||||
id: usernameDisplay
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
font.family: "FiraCode Nerd Font"
|
||||
font.pixelSize: 18
|
||||
color: Data.ThemeManager.primaryText
|
||||
text: usernameText
|
||||
}
|
||||
|
||||
// Password input field
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: 50
|
||||
radius: 25
|
||||
color: Data.ThemeManager.withOpacity(Data.ThemeManager.bgLighter, 0.4)
|
||||
border.color: passwordField.activeFocus ? Data.ThemeManager.accentColor : Data.ThemeManager.withOpacity(Data.ThemeManager.border, 0.6)
|
||||
border.width: 2
|
||||
|
||||
TextInput {
|
||||
id: passwordField
|
||||
anchors.fill: parent
|
||||
anchors.margins: 15
|
||||
echoMode: TextInput.Normal
|
||||
font.family: "FiraCode Nerd Font"
|
||||
font.pixelSize: 16
|
||||
color: "transparent" // Hide the actual text
|
||||
selectionColor: Data.ThemeManager.accentColor
|
||||
selectByMouse: true
|
||||
focus: isVisible
|
||||
|
||||
onAccepted: {
|
||||
if (text.length > 0) {
|
||||
passwordEntered(text)
|
||||
}
|
||||
}
|
||||
|
||||
// Password mask with better spaced dots
|
||||
Row {
|
||||
id: passwordDotsRow
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.left: parent.left
|
||||
spacing: 8
|
||||
visible: passwordField.text.length > 0
|
||||
|
||||
property int previousLength: 0
|
||||
|
||||
Repeater {
|
||||
id: passwordRepeater
|
||||
model: passwordField.text.length
|
||||
delegate: Rectangle {
|
||||
id: passwordDot
|
||||
width: 8
|
||||
height: 8
|
||||
radius: 4
|
||||
color: Data.ThemeManager.primaryText
|
||||
|
||||
property bool isNewDot: index >= passwordDotsRow.previousLength
|
||||
|
||||
// Only animate new dots, existing ones stay visible
|
||||
scale: isNewDot ? 0 : 1.0
|
||||
opacity: isNewDot ? 0 : 1.0
|
||||
|
||||
ParallelAnimation {
|
||||
running: passwordDot.isNewDot
|
||||
|
||||
NumberAnimation {
|
||||
target: passwordDot
|
||||
property: "scale"
|
||||
from: 0
|
||||
to: 1.0
|
||||
duration: 200
|
||||
easing.type: Easing.OutCubic
|
||||
}
|
||||
|
||||
NumberAnimation {
|
||||
target: passwordDot
|
||||
property: "opacity"
|
||||
from: 0
|
||||
to: 1.0
|
||||
duration: 150
|
||||
easing.type: Easing.OutQuad
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Track length changes to identify new dots
|
||||
Connections {
|
||||
target: passwordField
|
||||
function onTextChanged() {
|
||||
// Update previous length after a short delay to allow new dots to be marked as new
|
||||
Qt.callLater(function() {
|
||||
passwordDotsRow.previousLength = passwordField.text.length
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Placeholder text
|
||||
Text {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.left: parent.left
|
||||
text: "Password"
|
||||
font.family: passwordField.font.family
|
||||
font.pixelSize: passwordField.font.pixelSize
|
||||
color: Data.ThemeManager.withOpacity(Data.ThemeManager.secondaryText, 0.7)
|
||||
visible: passwordField.text.length === 0 && !passwordField.activeFocus
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Error message
|
||||
Text {
|
||||
id: errorText
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
font.family: "FiraCode Nerd Font"
|
||||
font.pixelSize: 14
|
||||
color: Data.ThemeManager.errorColor
|
||||
text: errorMessage
|
||||
visible: errorMessage !== ""
|
||||
wrapMode: Text.WordWrap
|
||||
width: parent.width
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
}
|
||||
|
||||
// Authentication status
|
||||
Text {
|
||||
id: statusText
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
font.family: "FiraCode Nerd Font"
|
||||
font.pixelSize: 14
|
||||
color: authSuccess ? Data.ThemeManager.success : Data.ThemeManager.accentColorBright
|
||||
text: {
|
||||
if (authSuccess) return "Authentication successful!"
|
||||
if (isAuthenticating) return "Authenticating..."
|
||||
return ""
|
||||
}
|
||||
visible: isAuthenticating || authSuccess
|
||||
}
|
||||
|
||||
// Public function to clear password
|
||||
function clearPassword() {
|
||||
passwordField.text = ""
|
||||
passwordField.focus = true
|
||||
}
|
||||
|
||||
// Public function to focus password field
|
||||
function focusPassword() {
|
||||
passwordField.focus = true
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,299 @@
|
|||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import Qt5Compat.GraphicalEffects
|
||||
import Quickshell
|
||||
import Quickshell.Wayland
|
||||
import Quickshell.Io
|
||||
import "root:/Data" as Data
|
||||
import "root:/Core" as Core
|
||||
|
||||
// Custom lockscreen
|
||||
PanelWindow {
|
||||
id: lockScreen
|
||||
|
||||
required property var shell
|
||||
property bool isLocked: false
|
||||
property bool isAuthenticated: false
|
||||
property string errorMessage: ""
|
||||
property int failedAttempts: 0
|
||||
property bool isAuthenticating: false
|
||||
property bool authSuccess: false
|
||||
property string usernameText: "Enter Password"
|
||||
|
||||
// Animation state - controlled by timer for proper timing
|
||||
property bool animateIn: false
|
||||
|
||||
// Full screen coverage
|
||||
screen: Quickshell.primaryScreen || Quickshell.screens[0]
|
||||
anchors.top: true
|
||||
anchors.left: true
|
||||
anchors.right: true
|
||||
anchors.bottom: true
|
||||
color: "transparent"
|
||||
|
||||
// Top layer to block everything
|
||||
WlrLayershell.layer: WlrLayershell.Overlay
|
||||
WlrLayershell.exclusiveZone: -1
|
||||
WlrLayershell.keyboardFocus: WlrKeyboardFocus.Exclusive
|
||||
WlrLayershell.namespace: "quickshell-lockscreen"
|
||||
|
||||
visible: isLocked
|
||||
|
||||
// Timer for slide-in animation - more reliable than Qt.callLater
|
||||
Timer {
|
||||
id: slideInTimer
|
||||
interval: 100 // Short delay to ensure window is fully rendered
|
||||
running: false
|
||||
onTriggered: {
|
||||
console.log("slideInTimer triggered, setting animateIn = true")
|
||||
animateIn = true
|
||||
}
|
||||
}
|
||||
|
||||
// Timer for slide-out animation before hiding window
|
||||
Timer {
|
||||
id: slideOutTimer
|
||||
interval: 1000 // Wait for slide animation to complete
|
||||
running: false
|
||||
onTriggered: {
|
||||
isLocked = false
|
||||
authArea.clearPassword()
|
||||
errorMessage = ""
|
||||
failedAttempts = 0
|
||||
authSuccess = false
|
||||
}
|
||||
}
|
||||
|
||||
// Timer to show success message before unlocking
|
||||
Timer {
|
||||
id: successTimer
|
||||
interval: 1200 // Show success message for 1.2 seconds
|
||||
running: false
|
||||
onTriggered: {
|
||||
unlock()
|
||||
}
|
||||
}
|
||||
|
||||
// Reset animation state when window becomes invisible
|
||||
onVisibleChanged: {
|
||||
if (!visible) {
|
||||
animateIn = false
|
||||
authSuccess = false
|
||||
slideInTimer.stop()
|
||||
slideOutTimer.stop()
|
||||
successTimer.stop()
|
||||
}
|
||||
}
|
||||
|
||||
// Background component
|
||||
LockscreenBackground {
|
||||
id: background
|
||||
isVisible: lockScreen.visible
|
||||
}
|
||||
|
||||
// Main lockscreen content with slide-from-top animation
|
||||
Item {
|
||||
id: mainContent
|
||||
anchors.fill: parent
|
||||
focus: true // Enable focus for keyboard handling
|
||||
|
||||
// Dramatic slide animation - starts off-screen, slides down when animateIn is true
|
||||
transform: Translate {
|
||||
id: mainTransform
|
||||
y: lockScreen.animateIn ? 0 : -lockScreen.height
|
||||
Behavior on y {
|
||||
NumberAnimation {
|
||||
duration: 800
|
||||
easing.type: Easing.OutCubic
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Scale animation for extra drama
|
||||
scale: lockScreen.animateIn ? 1.0 : 0.98
|
||||
Behavior on scale {
|
||||
NumberAnimation {
|
||||
duration: 800
|
||||
easing.type: Easing.OutCubic
|
||||
}
|
||||
}
|
||||
|
||||
// Keyboard shortcuts
|
||||
Keys.onPressed: function(event) {
|
||||
if (event.key === Qt.Key_Escape) {
|
||||
authArea.clearPassword()
|
||||
errorMessage = ""
|
||||
} else if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) {
|
||||
if (authArea.passwordField.text.length > 0) {
|
||||
authenticate(authArea.passwordField.text)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
// Authentication area component
|
||||
AuthenticationArea {
|
||||
id: authArea
|
||||
isVisible: lockScreen.animateIn
|
||||
errorMessage: lockScreen.errorMessage
|
||||
isAuthenticating: lockScreen.isAuthenticating
|
||||
authSuccess: lockScreen.authSuccess
|
||||
usernameText: lockScreen.usernameText
|
||||
|
||||
onPasswordEntered: function(password) {
|
||||
authenticate(password)
|
||||
}
|
||||
}
|
||||
|
||||
// Power buttons component
|
||||
PowerButtons {
|
||||
id: powerButtons
|
||||
isVisible: lockScreen.animateIn
|
||||
|
||||
onRebootRequested: rebootProcess.running = true
|
||||
onShutdownRequested: shutdownProcess.running = true
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
// Authentication process using proper PAM authentication
|
||||
Process {
|
||||
id: authProcess
|
||||
property string password: ""
|
||||
command: ["sh", "-c", "echo '" + password.replace(/'/g, "'\"'\"'") + "' | sudo -S -k true"]
|
||||
running: false
|
||||
|
||||
onExited: function(exitCode) {
|
||||
isAuthenticating = false
|
||||
|
||||
if (exitCode === 0) {
|
||||
// Authentication successful
|
||||
isAuthenticated = true
|
||||
errorMessage = ""
|
||||
authSuccess = true
|
||||
// Show success message for a brief moment before unlocking
|
||||
successTimer.start()
|
||||
} else {
|
||||
// Authentication failed
|
||||
failedAttempts++
|
||||
errorMessage = failedAttempts === 1 ? "Incorrect password" : `Incorrect password (${failedAttempts} attempts)`
|
||||
authArea.clearPassword()
|
||||
|
||||
// Add delay for failed attempts
|
||||
if (failedAttempts >= 3) {
|
||||
lockoutTimer.start()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Lockout timer for failed attempts
|
||||
Timer {
|
||||
id: lockoutTimer
|
||||
interval: 30000 // 30 second lockout
|
||||
onTriggered: {
|
||||
errorMessage = ""
|
||||
authArea.passwordField.enabled = true
|
||||
authArea.focusPassword()
|
||||
}
|
||||
}
|
||||
|
||||
// Reboot process
|
||||
Process {
|
||||
id: rebootProcess
|
||||
command: ["systemctl", "reboot"]
|
||||
running: false
|
||||
onExited: function(exitCode) { console.log("Reboot process completed with exit code:", exitCode) }
|
||||
}
|
||||
|
||||
// Shutdown process
|
||||
Process {
|
||||
id: shutdownProcess
|
||||
command: ["systemctl", "poweroff"]
|
||||
running: false
|
||||
onExited: function(exitCode) { console.log("Shutdown process completed with exit code:", exitCode) }
|
||||
}
|
||||
|
||||
// Get current username
|
||||
Process {
|
||||
id: usernameProcess
|
||||
command: ["whoami"]
|
||||
running: lockScreen.isLocked
|
||||
|
||||
stdout: SplitParser {
|
||||
onRead: function(data) {
|
||||
const username = data.trim()
|
||||
if (username) {
|
||||
usernameText = username.charAt(0).toUpperCase() + username.slice(1)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Public functions
|
||||
function lock() {
|
||||
console.log("Lockscreen.lock() called")
|
||||
|
||||
// Reset animation state FIRST, before making window visible
|
||||
animateIn = false
|
||||
|
||||
isLocked = true
|
||||
isAuthenticated = false
|
||||
authSuccess = false
|
||||
errorMessage = ""
|
||||
failedAttempts = 0
|
||||
authArea.clearPassword()
|
||||
usernameProcess.running = true
|
||||
|
||||
// Trigger slide animation after a short delay
|
||||
console.log("Starting slideInTimer")
|
||||
slideInTimer.start()
|
||||
}
|
||||
|
||||
function unlock() {
|
||||
console.log("Lockscreen.unlock() called")
|
||||
|
||||
// Start slide-out animation first
|
||||
animateIn = false
|
||||
|
||||
// Use timer for reliable timing before completely hiding
|
||||
slideOutTimer.start()
|
||||
}
|
||||
|
||||
function authenticate(password) {
|
||||
if (isAuthenticating || password.length === 0) return
|
||||
|
||||
console.log("Authenticating...")
|
||||
isAuthenticating = true
|
||||
authSuccess = false
|
||||
errorMessage = ""
|
||||
|
||||
// Use sudo authentication
|
||||
authProcess.password = password
|
||||
authProcess.running = true
|
||||
}
|
||||
|
||||
// Focus management when locked state changes
|
||||
onIsLockedChanged: {
|
||||
console.log("isLocked changed to:", isLocked)
|
||||
if (isLocked) {
|
||||
mainContent.focus = true
|
||||
authArea.focusPassword()
|
||||
}
|
||||
}
|
||||
|
||||
// Clean up processes on destruction
|
||||
Component.onDestruction: {
|
||||
if (authProcess.running) authProcess.running = false
|
||||
if (rebootProcess.running) rebootProcess.running = false
|
||||
if (shutdownProcess.running) shutdownProcess.running = false
|
||||
if (usernameProcess.running) usernameProcess.running = false
|
||||
if (lockoutTimer.running) lockoutTimer.running = false
|
||||
if (slideInTimer.running) slideInTimer.running = false
|
||||
if (slideOutTimer.running) slideOutTimer.running = false
|
||||
if (successTimer.running) successTimer.running = false
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,59 @@
|
|||
import QtQuick
|
||||
import Qt5Compat.GraphicalEffects
|
||||
import "root:/Data" as Data
|
||||
|
||||
// Background with wallpaper
|
||||
Rectangle {
|
||||
id: backgroundContainer
|
||||
anchors.fill: parent
|
||||
color: Data.ThemeManager.bgColor
|
||||
|
||||
required property bool isVisible
|
||||
|
||||
// Fade-in animation for the whole background
|
||||
opacity: isVisible ? 1.0 : 0.0
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: 500
|
||||
easing.type: Easing.OutCubic
|
||||
}
|
||||
}
|
||||
|
||||
// Wallpaper background
|
||||
Image {
|
||||
id: wallpaperImage
|
||||
anchors.fill: parent
|
||||
source: Data.WallpaperManager.currentWallpaper ? "file://" + Data.WallpaperManager.currentWallpaper : ""
|
||||
fillMode: Image.PreserveAspectCrop
|
||||
asynchronous: true
|
||||
cache: false
|
||||
}
|
||||
|
||||
// Dark overlay
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
color: Data.ThemeManager.withOpacity(Data.ThemeManager.bgColor, 0.8)
|
||||
}
|
||||
|
||||
// Blur effect overlay
|
||||
GaussianBlur {
|
||||
anchors.fill: wallpaperImage
|
||||
source: wallpaperImage
|
||||
radius: 32
|
||||
samples: 65
|
||||
|
||||
// Blur animation - starts less blurred and increases
|
||||
Behavior on radius {
|
||||
NumberAnimation {
|
||||
duration: 1200
|
||||
easing.type: Easing.OutCubic
|
||||
}
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
if (isVisible) {
|
||||
radius = 32
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,87 @@
|
|||
import QtQuick
|
||||
import "root:/Data" as Data
|
||||
|
||||
// Reboot and shutdown buttons positioned at bottom right
|
||||
Row {
|
||||
anchors.right: parent.right
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.margins: 40
|
||||
spacing: 16
|
||||
|
||||
required property bool isVisible
|
||||
|
||||
signal rebootRequested()
|
||||
signal shutdownRequested()
|
||||
|
||||
// Fade in with delay
|
||||
opacity: isVisible ? 1.0 : 0.0
|
||||
Behavior on opacity {
|
||||
SequentialAnimation {
|
||||
PauseAnimation { duration: 900 } // Wait for most elements to appear
|
||||
NumberAnimation {
|
||||
duration: 500
|
||||
easing.type: Easing.OutCubic
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Reboot button
|
||||
Rectangle {
|
||||
width: 45
|
||||
height: 45
|
||||
radius: 22
|
||||
color: rebootMouseArea.containsMouse ? Data.ThemeManager.withOpacity(Data.ThemeManager.secondaryText, 0.3) : Data.ThemeManager.withOpacity(Data.ThemeManager.secondaryText, 0.2)
|
||||
border.color: Data.ThemeManager.withOpacity(Data.ThemeManager.secondaryText, 0.6)
|
||||
border.width: 1
|
||||
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
text: "restart_alt"
|
||||
font.family: "Material Symbols Outlined"
|
||||
font.pixelSize: 20
|
||||
color: Data.ThemeManager.secondaryText
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: rebootMouseArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: rebootRequested()
|
||||
}
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation { duration: 200 }
|
||||
}
|
||||
}
|
||||
|
||||
// Shutdown button
|
||||
Rectangle {
|
||||
width: 45
|
||||
height: 45
|
||||
radius: 22
|
||||
color: shutdownMouseArea.containsMouse ? Data.ThemeManager.withOpacity(Data.ThemeManager.secondaryText, 0.3) : Data.ThemeManager.withOpacity(Data.ThemeManager.secondaryText, 0.2)
|
||||
border.color: Data.ThemeManager.withOpacity(Data.ThemeManager.secondaryText, 0.6)
|
||||
border.width: 1
|
||||
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
text: "power_settings_new"
|
||||
font.family: "Material Symbols Outlined"
|
||||
font.pixelSize: 18
|
||||
color: Data.ThemeManager.secondaryText
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: shutdownMouseArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: shutdownRequested()
|
||||
}
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation { duration: 200 }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,72 @@
|
|||
import QtQuick
|
||||
import "root:/Data" as Data
|
||||
|
||||
// Time and date display
|
||||
Column {
|
||||
id: timeColumn
|
||||
anchors.top: parent.top
|
||||
anchors.left: parent.left
|
||||
anchors.topMargin: 60
|
||||
anchors.leftMargin: 60
|
||||
spacing: 16
|
||||
|
||||
required property bool isVisible
|
||||
|
||||
// Subtle slide-left animation (after main slide)
|
||||
transform: Translate {
|
||||
id: timeTransform
|
||||
x: isVisible ? 0 : -100
|
||||
Behavior on x {
|
||||
SequentialAnimation {
|
||||
PauseAnimation { duration: 400 } // Wait for main slide to be visible
|
||||
NumberAnimation {
|
||||
duration: 500
|
||||
easing.type: Easing.OutQuart
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
opacity: isVisible ? 1.0 : 0.0
|
||||
Behavior on opacity {
|
||||
SequentialAnimation {
|
||||
PauseAnimation { duration: 500 } // Wait for main slide
|
||||
NumberAnimation {
|
||||
duration: 500
|
||||
easing.type: Easing.OutCubic
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Current time
|
||||
Text {
|
||||
id: timeText
|
||||
font.family: "Roboto"
|
||||
font.pixelSize: 84
|
||||
font.weight: Font.ExtraLight
|
||||
color: Data.ThemeManager.brightText
|
||||
text: Qt.formatTime(new Date(), "hh:mm")
|
||||
}
|
||||
|
||||
// Current date
|
||||
Text {
|
||||
id: dateText
|
||||
font.family: "Roboto"
|
||||
font.pixelSize: 28
|
||||
font.weight: Font.Light
|
||||
color: Data.ThemeManager.secondaryText
|
||||
text: Qt.formatDate(new Date(), "dddd, MMMM d, yyyy")
|
||||
}
|
||||
|
||||
// Time update timer
|
||||
Timer {
|
||||
id: timeTimer
|
||||
interval: 1000
|
||||
running: isVisible
|
||||
repeat: true
|
||||
onTriggered: {
|
||||
timeText.text = Qt.formatTime(new Date(), "hh:mm")
|
||||
dateText.text = Qt.formatDate(new Date(), "dddd, MMMM d, yyyy")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,367 @@
|
|||
// System notification manager
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import Quickshell.Services.Notifications
|
||||
import "root:/Data" as Data
|
||||
import "root:/Core" as Core
|
||||
|
||||
Item {
|
||||
id: root
|
||||
required property var shell
|
||||
required property var notificationServer
|
||||
|
||||
// Dynamic height based on visible notifications
|
||||
property int calculatedHeight: Math.min(notifications.length, maxNotifications) * 100 + 100 // Add 100px for bottom margin
|
||||
|
||||
// Simple array to store notifications with tracking
|
||||
property var notifications: []
|
||||
property int maxNotifications: 5
|
||||
property var animatedNotificationIds: ({}) // Track which notifications have been animated
|
||||
|
||||
// Handle new notifications
|
||||
Connections {
|
||||
target: notificationServer
|
||||
function onNotification(notification) {
|
||||
if (!notification || !notification.id) return
|
||||
|
||||
// Filter empty notifications
|
||||
if (!notification.appName && !notification.summary && !notification.body) {
|
||||
return
|
||||
}
|
||||
|
||||
// Filter ignored applications (case-insensitive) - same logic as NotificationService
|
||||
var shouldIgnore = false
|
||||
if (notification.appName && Data.Settings.ignoredApps && Data.Settings.ignoredApps.length > 0) {
|
||||
for (var i = 0; i < Data.Settings.ignoredApps.length; i++) {
|
||||
if (Data.Settings.ignoredApps[i].toLowerCase() === notification.appName.toLowerCase()) {
|
||||
shouldIgnore = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (shouldIgnore) {
|
||||
return // Don't display ignored notifications
|
||||
}
|
||||
|
||||
// Create simple notification object
|
||||
let newNotification = {
|
||||
"id": notification.id,
|
||||
"appName": notification.appName || "App",
|
||||
"summary": notification.summary || "",
|
||||
"body": notification.body || "",
|
||||
"timestamp": Date.now(),
|
||||
"shouldSlideOut": false,
|
||||
"icon": notification.icon || notification.image || notification.appIcon || "",
|
||||
"rawNotification": notification // Keep reference to original
|
||||
}
|
||||
|
||||
// Add to beginning
|
||||
notifications.unshift(newNotification)
|
||||
|
||||
// Trigger model update first to let new notification animate
|
||||
notificationsChanged()
|
||||
|
||||
// Delay trimming to let new notification animate
|
||||
if (notifications.length > maxNotifications) {
|
||||
trimTimer.restart()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Timer to delay trimming notifications (let new ones animate first)
|
||||
Timer {
|
||||
id: trimTimer
|
||||
interval: 500 // Wait 500ms before trimming
|
||||
running: false
|
||||
repeat: false
|
||||
onTriggered: {
|
||||
if (notifications.length > maxNotifications) {
|
||||
notifications = notifications.slice(0, maxNotifications)
|
||||
notificationsChanged()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Global timer to check for expired notifications
|
||||
Timer {
|
||||
id: cleanupTimer
|
||||
interval: Math.min(500, Data.Settings.displayTime / 10) // Check every 500ms or 1/10th of display time, whichever is shorter
|
||||
running: true
|
||||
repeat: true
|
||||
onTriggered: {
|
||||
let currentTime = Date.now()
|
||||
let hasExpiredNotifications = false
|
||||
|
||||
// Mark notifications older than displayTime setting for slide-out
|
||||
for (let i = 0; i < notifications.length; i++) {
|
||||
let notification = notifications[i]
|
||||
let age = currentTime - notification.timestamp
|
||||
if (age >= Data.Settings.displayTime && !notification.shouldSlideOut) {
|
||||
notification.shouldSlideOut = true
|
||||
hasExpiredNotifications = true
|
||||
}
|
||||
}
|
||||
|
||||
// Trigger update if any notifications were marked for slide-out
|
||||
if (hasExpiredNotifications) {
|
||||
notificationsChanged()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function removeNotification(notificationId) {
|
||||
let initialLength = notifications.length
|
||||
notifications = notifications.filter(function(n) { return n.id !== notificationId })
|
||||
if (notifications.length !== initialLength) {
|
||||
// Remove from animated tracking
|
||||
delete animatedNotificationIds[notificationId]
|
||||
notificationsChanged()
|
||||
}
|
||||
}
|
||||
|
||||
// Simple Column with Repeater
|
||||
Column {
|
||||
anchors.right: parent.right
|
||||
anchors.left: parent.left
|
||||
anchors.top: parent.top
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.leftMargin: 40 // Create space on left for top-left corner
|
||||
anchors.rightMargin: Data.Settings.borderWidth + 20 // Border width plus corner space
|
||||
anchors.bottomMargin: 100 // Create more space at bottom for bottom corner
|
||||
spacing: 0
|
||||
|
||||
Repeater {
|
||||
model: notifications.length // Show all notifications, not just maxNotifications
|
||||
|
||||
delegate: Rectangle {
|
||||
id: notificationRect
|
||||
property var notification: index < notifications.length ? notifications[index] : null
|
||||
|
||||
width: 400
|
||||
height: 100
|
||||
color: Data.ThemeManager.bgColor
|
||||
|
||||
// Only bottom visible notification gets bottom-left radius
|
||||
radius: 0
|
||||
bottomLeftRadius: index === Math.min(notifications.length, maxNotifications) - 1 && index < maxNotifications ? 15 : 0
|
||||
|
||||
// Only show if within maxNotifications limit
|
||||
visible: index < maxNotifications
|
||||
|
||||
// Animation state
|
||||
property bool hasSlideIn: false
|
||||
|
||||
// Check for expiration and start slide-out if needed
|
||||
onNotificationChanged: {
|
||||
if (notification && notification.shouldSlideOut && !slideOutAnimation.running) {
|
||||
slideOutAnimation.start()
|
||||
}
|
||||
}
|
||||
|
||||
// Start off-screen for new notifications
|
||||
Component.onCompleted: {
|
||||
if (notification) {
|
||||
// Check if notification should slide out immediately
|
||||
if (notification.shouldSlideOut) {
|
||||
slideOutAnimation.start()
|
||||
return
|
||||
}
|
||||
|
||||
// Check if this notification is truly new (recently added)
|
||||
let notificationAge = Date.now() - notification.timestamp
|
||||
let shouldAnimate = !animatedNotificationIds[notification.id] && notificationAge < 1000 // Only animate if less than 1 second old
|
||||
|
||||
if (shouldAnimate) {
|
||||
x = 420
|
||||
opacity = 0
|
||||
hasSlideIn = false
|
||||
slideInAnimation.start()
|
||||
// Mark as animated
|
||||
animatedNotificationIds[notification.id] = true
|
||||
} else {
|
||||
x = 0
|
||||
opacity = 1
|
||||
hasSlideIn = true
|
||||
// Mark as animated if not already
|
||||
animatedNotificationIds[notification.id] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Slide-in animation
|
||||
ParallelAnimation {
|
||||
id: slideInAnimation
|
||||
NumberAnimation {
|
||||
target: notificationRect
|
||||
property: "x"
|
||||
to: 0
|
||||
duration: 300
|
||||
easing.type: Easing.OutCubic
|
||||
}
|
||||
NumberAnimation {
|
||||
target: notificationRect
|
||||
property: "opacity"
|
||||
to: 1
|
||||
duration: 300
|
||||
easing.type: Easing.OutCubic
|
||||
}
|
||||
onFinished: {
|
||||
hasSlideIn = true
|
||||
}
|
||||
}
|
||||
|
||||
// Slide-out animation
|
||||
ParallelAnimation {
|
||||
id: slideOutAnimation
|
||||
NumberAnimation {
|
||||
target: notificationRect
|
||||
property: "x"
|
||||
to: 420
|
||||
duration: 250
|
||||
easing.type: Easing.InCubic
|
||||
}
|
||||
NumberAnimation {
|
||||
target: notificationRect
|
||||
property: "opacity"
|
||||
to: 0
|
||||
duration: 250
|
||||
easing.type: Easing.InCubic
|
||||
}
|
||||
onFinished: {
|
||||
if (notification) {
|
||||
removeNotification(notification.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Click to dismiss
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
onClicked: slideOutAnimation.start()
|
||||
}
|
||||
|
||||
// Content
|
||||
Row {
|
||||
anchors.fill: parent
|
||||
anchors.margins: 15
|
||||
spacing: 12
|
||||
|
||||
// App icon
|
||||
Rectangle {
|
||||
width: 32
|
||||
height: 32
|
||||
radius: 16
|
||||
color: Qt.rgba(255, 255, 255, 0.1)
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
// Application icon (if available)
|
||||
Image {
|
||||
id: appIcon
|
||||
source: {
|
||||
if (!notification || !notification.icon) return ""
|
||||
|
||||
let icon = notification.icon
|
||||
|
||||
// Apply same processing as tray system
|
||||
if (icon.includes("?path=")) {
|
||||
const [name, path] = icon.split("?path=");
|
||||
const fileName = name.substring(name.lastIndexOf("/") + 1);
|
||||
return `file://${path}/${fileName}`;
|
||||
}
|
||||
|
||||
// Handle file paths properly
|
||||
if (icon.startsWith('/')) {
|
||||
return "file://" + icon
|
||||
}
|
||||
|
||||
return icon
|
||||
}
|
||||
anchors.fill: parent
|
||||
anchors.margins: 2
|
||||
fillMode: Image.PreserveAspectFit
|
||||
smooth: true
|
||||
visible: source.toString() !== ""
|
||||
|
||||
onStatusChanged: {
|
||||
// Icon status handling can be added here if needed
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback text (first letter of app name)
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
text: notification && notification.appName ? notification.appName.charAt(0).toUpperCase() : "!"
|
||||
color: Data.ThemeManager.accentColor
|
||||
font.family: "Roboto"
|
||||
font.pixelSize: 16
|
||||
font.bold: true
|
||||
visible: !appIcon.visible
|
||||
}
|
||||
}
|
||||
|
||||
// Content
|
||||
Column {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
width: parent.width - 60
|
||||
spacing: 4
|
||||
|
||||
Text {
|
||||
text: notification ? notification.appName : ""
|
||||
color: Data.ThemeManager.accentColor
|
||||
font.family: "Roboto"
|
||||
font.bold: true
|
||||
font.pixelSize: 15
|
||||
width: Math.min(parent.width, 250) // Earlier line break
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
|
||||
Text {
|
||||
text: notification ? notification.summary : ""
|
||||
color: Data.ThemeManager.fgColor
|
||||
font.family: "Roboto"
|
||||
font.pixelSize: 14
|
||||
width: Math.min(parent.width, 250) // Earlier line break
|
||||
wrapMode: Text.Wrap
|
||||
maximumLineCount: 2
|
||||
elide: Text.ElideRight
|
||||
visible: text.length > 0
|
||||
}
|
||||
|
||||
Text {
|
||||
text: notification ? notification.body : ""
|
||||
color: Qt.lighter(Data.ThemeManager.fgColor, 1.3)
|
||||
font.family: "Roboto"
|
||||
font.pixelSize: 13
|
||||
width: Math.min(parent.width, 250) // Earlier line break
|
||||
wrapMode: Text.Wrap
|
||||
maximumLineCount: 2
|
||||
elide: Text.ElideRight
|
||||
visible: text.length > 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Top corner for first notification
|
||||
Core.Corners {
|
||||
position: "bottomright"
|
||||
size: 1.3
|
||||
fillColor: Data.ThemeManager.bgColor
|
||||
offsetX: -361
|
||||
offsetY: -13
|
||||
visible: index === 0 && index < maxNotifications
|
||||
}
|
||||
|
||||
// Bottom corner for last visible notification
|
||||
Core.Corners {
|
||||
position: "bottomright"
|
||||
size: 1.3
|
||||
fillColor: Data.ThemeManager.bgColor
|
||||
offsetX: 39
|
||||
offsetY: 78
|
||||
visible: index === Math.min(notifications.length, maxNotifications) - 1 && index < maxNotifications
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,263 @@
|
|||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import Qt5Compat.GraphicalEffects
|
||||
import "root:/Data" as Data
|
||||
|
||||
// Notification history viewer
|
||||
Item {
|
||||
id: root
|
||||
implicitHeight: 400
|
||||
|
||||
required property var shell
|
||||
property bool hovered: false
|
||||
property real targetX: 0
|
||||
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
anchors.margins: 16
|
||||
spacing: 12
|
||||
|
||||
// Header with title, count, and clear all button
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: 40
|
||||
spacing: 8
|
||||
|
||||
Text {
|
||||
text: "Notification History"
|
||||
color: Data.ThemeManager.accentColor
|
||||
font.pixelSize: 18
|
||||
font.bold: true
|
||||
font.family: "Roboto"
|
||||
}
|
||||
|
||||
Text {
|
||||
text: "(" + (shell.notificationHistory ? shell.notificationHistory.count : 0) + ")"
|
||||
color: Data.ThemeManager.fgColor
|
||||
font.family: "Roboto"
|
||||
font.pixelSize: 12
|
||||
opacity: 0.7
|
||||
}
|
||||
|
||||
Item { Layout.fillWidth: true }
|
||||
|
||||
Rectangle {
|
||||
visible: shell.notificationHistory && shell.notificationHistory.count > 0
|
||||
width: clearText.implicitWidth + 16
|
||||
height: 24
|
||||
radius: 12
|
||||
color: clearMouseArea.containsMouse ? Qt.rgba(Data.ThemeManager.accentColor.r, Data.ThemeManager.accentColor.g, Data.ThemeManager.accentColor.b, 0.2) : "transparent"
|
||||
border.color: Data.ThemeManager.accentColor
|
||||
border.width: 1
|
||||
|
||||
Text {
|
||||
id: clearText
|
||||
anchors.centerIn: parent
|
||||
text: "Clear All"
|
||||
color: Data.ThemeManager.accentColor
|
||||
font.family: "Roboto"
|
||||
font.pixelSize: 11
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: clearMouseArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
onClicked: shell.notificationHistory.clear()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Scrollable notification list
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
|
||||
ScrollView {
|
||||
id: scrollView
|
||||
anchors.fill: parent
|
||||
clip: true
|
||||
ScrollBar.vertical: ScrollBar {
|
||||
policy: ScrollBar.AsNeeded
|
||||
interactive: true
|
||||
visible: notificationListView.contentHeight > notificationListView.height
|
||||
contentItem: Rectangle {
|
||||
implicitWidth: 6
|
||||
radius: width / 2
|
||||
color: parent.pressed ? Data.ThemeManager.accentColor
|
||||
: parent.hovered ? Qt.lighter(Data.ThemeManager.accentColor, 1.2)
|
||||
: Qt.rgba(Data.ThemeManager.accentColor.r, Data.ThemeManager.accentColor.g, Data.ThemeManager.accentColor.b, 0.7)
|
||||
}
|
||||
}
|
||||
ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
|
||||
|
||||
ListView {
|
||||
id: notificationListView
|
||||
model: shell.notificationHistory
|
||||
spacing: 12
|
||||
cacheBuffer: 50 // Memory optimization
|
||||
reuseItems: true
|
||||
boundsBehavior: Flickable.StopAtBounds
|
||||
maximumFlickVelocity: 2500
|
||||
flickDeceleration: 1500
|
||||
clip: true
|
||||
interactive: true
|
||||
|
||||
// Smooth scrolling behavior
|
||||
property real targetY: contentY
|
||||
Behavior on targetY {
|
||||
NumberAnimation {
|
||||
duration: 200
|
||||
easing.type: Easing.OutQuad
|
||||
}
|
||||
}
|
||||
|
||||
onTargetYChanged: {
|
||||
if (!moving && !dragging) {
|
||||
contentY = targetY
|
||||
}
|
||||
}
|
||||
|
||||
delegate: Rectangle {
|
||||
width: notificationListView.width
|
||||
height: Math.max(80, contentLayout.implicitHeight + 24)
|
||||
radius: 8
|
||||
color: mouseArea.containsMouse ? Qt.darker(Data.ThemeManager.bgColor, 1.15) : Qt.darker(Data.ThemeManager.bgColor, 1.1)
|
||||
border.color: Data.ThemeManager.accentColor
|
||||
border.width: 1
|
||||
|
||||
// View optimization - only render visible items
|
||||
visible: y + height > notificationListView.contentY - height &&
|
||||
y < notificationListView.contentY + notificationListView.height + height
|
||||
|
||||
MouseArea {
|
||||
id: mouseArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
}
|
||||
|
||||
// Main notification content layout
|
||||
RowLayout {
|
||||
id: contentLayout
|
||||
anchors.fill: parent
|
||||
anchors.margins: 12
|
||||
spacing: 12
|
||||
|
||||
// App icon area
|
||||
Item {
|
||||
Layout.preferredWidth: 24
|
||||
Layout.preferredHeight: 24
|
||||
Layout.alignment: Qt.AlignTop
|
||||
|
||||
Image {
|
||||
width: 24
|
||||
height: 24
|
||||
source: model.icon || ""
|
||||
visible: source.toString() !== ""
|
||||
}
|
||||
}
|
||||
|
||||
// Notification text content
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
spacing: 6
|
||||
|
||||
// App name and timestamp row
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
|
||||
Text {
|
||||
Layout.fillWidth: true
|
||||
text: model.appName || "Unknown"
|
||||
color: Data.ThemeManager.accentColor
|
||||
font.family: "Roboto"
|
||||
font.pixelSize: 13
|
||||
font.bold: true
|
||||
}
|
||||
|
||||
Text {
|
||||
text: Qt.formatDateTime(model.timestamp, "hh:mm")
|
||||
color: Data.ThemeManager.fgColor
|
||||
font.family: "Roboto"
|
||||
font.pixelSize: 10
|
||||
opacity: 0.7
|
||||
}
|
||||
}
|
||||
|
||||
// Notification summary
|
||||
Text {
|
||||
Layout.fillWidth: true
|
||||
visible: model.summary && model.summary.length > 0
|
||||
text: model.summary || ""
|
||||
color: Data.ThemeManager.fgColor
|
||||
font.family: "Roboto"
|
||||
font.pixelSize: 13
|
||||
font.bold: true
|
||||
wrapMode: Text.WordWrap
|
||||
lineHeight: 1.2
|
||||
}
|
||||
|
||||
// Notification body text
|
||||
Text {
|
||||
Layout.fillWidth: true
|
||||
visible: model.body && model.body.length > 0
|
||||
text: model.body || ""
|
||||
color: Data.ThemeManager.fgColor
|
||||
font.family: "Roboto"
|
||||
font.pixelSize: 12
|
||||
opacity: 0.9
|
||||
wrapMode: Text.WordWrap
|
||||
maximumLineCount: 4
|
||||
elide: Text.ElideRight
|
||||
lineHeight: 1.2
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Individual delete button
|
||||
Rectangle {
|
||||
width: 24
|
||||
height: 24
|
||||
radius: 12
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
anchors.margins: 8
|
||||
color: deleteArea.containsMouse ? Qt.rgba(255, 0, 0, 0.2) : "transparent"
|
||||
border.color: deleteArea.containsMouse ? "#ff4444" : Data.ThemeManager.fgColor
|
||||
border.width: 1
|
||||
opacity: deleteArea.containsMouse ? 1 : 0.5
|
||||
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
text: "×"
|
||||
color: deleteArea.containsMouse ? "#ff4444" : Data.ThemeManager.fgColor
|
||||
font.family: "Roboto"
|
||||
font.pixelSize: 16
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: deleteArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
onClicked: shell.notificationHistory.remove(model.index)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Empty state message
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
visible: !notificationListView.count
|
||||
text: "No notifications"
|
||||
color: Data.ThemeManager.fgColor
|
||||
font.family: "Roboto"
|
||||
font.pixelSize: 14
|
||||
opacity: 0.7
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
143
modules/home/services/quickshell/qml/Widgets/Panel/TopPanel.qml
Normal file
143
modules/home/services/quickshell/qml/Widgets/Panel/TopPanel.qml
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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,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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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,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() }
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
515
modules/home/services/quickshell/qml/Widgets/System/Cliphist.qml
Normal file
515
modules/home/services/quickshell/qml/Widgets/System/Cliphist.qml
Normal file
|
|
@ -0,0 +1,515 @@
|
|||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import QtQuick.Shapes
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import "root:/Data" as Data
|
||||
|
||||
// Clipboard history manager with cliphist integration
|
||||
Item {
|
||||
id: root
|
||||
required property var shell
|
||||
property string selectedWidget: "cliphist"
|
||||
|
||||
property bool isVisible: false
|
||||
property real bgOpacity: 0.0
|
||||
|
||||
transformOrigin: Item.Center
|
||||
|
||||
function show() { showAnimation.start() }
|
||||
function hide() { hideAnimation.start() }
|
||||
function toggle() { isVisible ? hide() : show() }
|
||||
|
||||
// Smooth show/hide animations
|
||||
ParallelAnimation {
|
||||
id: showAnimation
|
||||
PropertyAction { target: root; property: "isVisible"; value: true }
|
||||
PropertyAnimation { target: root; property: "opacity"; from: 0.0; to: 1.0; duration: 200; easing.type: Easing.OutCubic }
|
||||
PropertyAnimation { target: root; property: "scale"; from: 0.9; to: 1.0; duration: 200; easing.type: Easing.OutCubic }
|
||||
}
|
||||
|
||||
ParallelAnimation {
|
||||
id: hideAnimation
|
||||
PropertyAnimation { target: root; property: "opacity"; to: 0.0; duration: 150; easing.type: Easing.InCubic }
|
||||
PropertyAnimation { target: root; property: "scale"; to: 0.95; duration: 150; easing.type: Easing.InCubic }
|
||||
PropertyAction { target: root; property: "isVisible"; value: false }
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
id: contentColumn
|
||||
anchors.fill: parent
|
||||
spacing: 12
|
||||
|
||||
// Header
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: 30
|
||||
|
||||
Label {
|
||||
text: "Clipboard History"
|
||||
font.pixelSize: 16
|
||||
font.weight: Font.Medium
|
||||
color: Data.ThemeManager.fgColor
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
Button {
|
||||
id: clearButton
|
||||
text: "Clear"
|
||||
implicitWidth: 60
|
||||
implicitHeight: 25
|
||||
background: Rectangle {
|
||||
radius: 12
|
||||
color: parent.down ? Qt.darker(Data.ThemeManager.accentColor, 1.2) :
|
||||
parent.hovered ? Qt.lighter(Data.ThemeManager.accentColor, 1.1) :
|
||||
Qt.rgba(Data.ThemeManager.accentColor.r, Data.ThemeManager.accentColor.g, Data.ThemeManager.accentColor.b, 0.8)
|
||||
}
|
||||
contentItem: Label {
|
||||
text: parent.text
|
||||
font.pixelSize: 11
|
||||
color: Data.ThemeManager.fgColor
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
}
|
||||
onClicked: {
|
||||
clearClipboardHistory()
|
||||
clickScale.target = clearButton
|
||||
clickScale.start()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Scrollable clipboard history list
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
|
||||
ScrollView {
|
||||
id: scrollView
|
||||
anchors.fill: parent
|
||||
clip: true
|
||||
ScrollBar.vertical: ScrollBar {
|
||||
policy: ScrollBar.AsNeeded
|
||||
interactive: true
|
||||
visible: cliphistList.contentHeight > cliphistList.height
|
||||
contentItem: Rectangle {
|
||||
implicitWidth: 6
|
||||
radius: width / 2
|
||||
color: parent.pressed ? Data.ThemeManager.accentColor
|
||||
: parent.hovered ? Qt.lighter(Data.ThemeManager.accentColor, 1.2)
|
||||
: Qt.rgba(Data.ThemeManager.accentColor.r, Data.ThemeManager.accentColor.g, Data.ThemeManager.accentColor.b, 0.7)
|
||||
}
|
||||
}
|
||||
ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
|
||||
|
||||
ListView {
|
||||
id: cliphistList
|
||||
model: cliphistModel
|
||||
spacing: 6
|
||||
cacheBuffer: 50 // Memory optimization
|
||||
reuseItems: true
|
||||
boundsBehavior: Flickable.StopAtBounds
|
||||
maximumFlickVelocity: 2500
|
||||
flickDeceleration: 1500
|
||||
|
||||
// Smooth scrolling behavior
|
||||
property real targetY: contentY
|
||||
Behavior on targetY {
|
||||
NumberAnimation {
|
||||
duration: 200
|
||||
easing.type: Easing.OutQuad
|
||||
}
|
||||
}
|
||||
|
||||
onTargetYChanged: {
|
||||
if (!moving && !dragging) {
|
||||
contentY = targetY
|
||||
}
|
||||
}
|
||||
|
||||
delegate: Rectangle {
|
||||
width: cliphistList.width
|
||||
height: Math.max(50, contentText.contentHeight + 20)
|
||||
radius: 8
|
||||
color: mouseArea.containsMouse ? Qt.darker(Data.ThemeManager.bgColor, 1.15) : Qt.darker(Data.ThemeManager.bgColor, 1.1)
|
||||
border.color: Data.ThemeManager.accentColor
|
||||
border.width: 1
|
||||
|
||||
// View optimization - only render visible items
|
||||
visible: y + height > cliphistList.contentY - height &&
|
||||
y < cliphistList.contentY + cliphistList.height + height
|
||||
|
||||
RowLayout {
|
||||
anchors.fill: parent
|
||||
anchors.margins: 10
|
||||
spacing: 10
|
||||
|
||||
// Content type icon
|
||||
Label {
|
||||
text: model.type === "image" ? "🖼️" : model.type === "url" ? "🔗" : "📝"
|
||||
font.pixelSize: 16
|
||||
Layout.alignment: Qt.AlignTop
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
spacing: 4
|
||||
|
||||
Label {
|
||||
id: contentText
|
||||
text: model.type === "image" ? "[Image Data]" :
|
||||
(model.content.length > 100 ? model.content.substring(0, 100) + "..." : model.content)
|
||||
font.pixelSize: 12
|
||||
color: Data.ThemeManager.fgColor
|
||||
wrapMode: Text.WordWrap
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
elide: Text.ElideRight
|
||||
maximumLineCount: 4
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
Item { Layout.fillWidth: true }
|
||||
Label {
|
||||
text: model.type === "image" ? "Image" : (model.content.length + " chars")
|
||||
font.pixelSize: 10
|
||||
color: Qt.darker(Data.ThemeManager.fgColor, 1.5)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: mouseArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
onClicked: mouse => {
|
||||
if (mouse.button === Qt.LeftButton) {
|
||||
copyToClipboard(model.id, model.type)
|
||||
clickScale.target = parent
|
||||
clickScale.start()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Empty state message
|
||||
Label {
|
||||
anchors.centerIn: parent
|
||||
text: "No clipboard history\nCopy something to get started"
|
||||
font.pixelSize: 14
|
||||
color: Qt.darker(Data.ThemeManager.fgColor, 1.5)
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
visible: cliphistList.count === 0
|
||||
opacity: 0.7
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Click feedback animation
|
||||
NumberAnimation {
|
||||
id: clickScale
|
||||
property Item target
|
||||
properties: "scale"
|
||||
from: 0.95
|
||||
to: 1.0
|
||||
duration: 150
|
||||
easing.type: Easing.OutCubic
|
||||
}
|
||||
|
||||
ListModel { id: cliphistModel }
|
||||
|
||||
property var currentEntries: []
|
||||
|
||||
// Main cliphist process for fetching clipboard history
|
||||
Process {
|
||||
id: cliphistProcess
|
||||
command: ["cliphist", "list"]
|
||||
running: false
|
||||
|
||||
property var tempEntries: []
|
||||
|
||||
onRunningChanged: {
|
||||
if (running) {
|
||||
tempEntries = []
|
||||
} else {
|
||||
// Process completed, apply smart diff update
|
||||
updateModelIfChanged(tempEntries)
|
||||
}
|
||||
}
|
||||
|
||||
stdout: SplitParser {
|
||||
onRead: data => {
|
||||
try {
|
||||
const line = data.toString().trim()
|
||||
|
||||
// Skip empty lines and error messages
|
||||
if (line === "" || line.includes("ERROR") || line.includes("WARN") ||
|
||||
line.includes("error:") || line.includes("warning:")) {
|
||||
return
|
||||
}
|
||||
|
||||
// Parse cliphist output format: ID + spaces + content
|
||||
const match = line.match(/^(\d+)\s+(.+)$/)
|
||||
if (match) {
|
||||
const id = match[1]
|
||||
const content = match[2]
|
||||
|
||||
cliphistProcess.tempEntries.push({
|
||||
id: id,
|
||||
content: content,
|
||||
type: detectContentType(content)
|
||||
})
|
||||
} else {
|
||||
console.log("Failed to parse line:", line)
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Error parsing cliphist line:", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Clear entire clipboard history
|
||||
Process {
|
||||
id: clearCliphistProcess
|
||||
command: ["cliphist", "wipe"]
|
||||
running: false
|
||||
|
||||
onRunningChanged: {
|
||||
if (!running) {
|
||||
cliphistModel.clear()
|
||||
currentEntries = []
|
||||
console.log("Clipboard history cleared")
|
||||
}
|
||||
}
|
||||
|
||||
stderr: SplitParser {
|
||||
onRead: data => {
|
||||
console.error("Clear clipboard error:", data.toString())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Delete specific clipboard entry
|
||||
Process {
|
||||
id: deleteEntryProcess
|
||||
property string entryId: ""
|
||||
command: ["cliphist", "delete-query", entryId]
|
||||
running: false
|
||||
|
||||
onRunningChanged: {
|
||||
if (!running && entryId !== "") {
|
||||
// Remove deleted entry from model
|
||||
for (let i = 0; i < cliphistModel.count; i++) {
|
||||
if (cliphistModel.get(i).id === entryId) {
|
||||
cliphistModel.remove(i)
|
||||
currentEntries = currentEntries.filter(entry => entry.id !== entryId)
|
||||
break
|
||||
}
|
||||
}
|
||||
console.log("Deleted entry:", entryId)
|
||||
entryId = ""
|
||||
}
|
||||
}
|
||||
|
||||
stderr: SplitParser {
|
||||
onRead: data => {
|
||||
console.error("Delete entry error:", data.toString())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Copy plain text to clipboard
|
||||
Process {
|
||||
id: copyTextProcess
|
||||
property string textToCopy: ""
|
||||
command: ["wl-copy", textToCopy]
|
||||
running: false
|
||||
|
||||
stderr: SplitParser {
|
||||
onRead: data => {
|
||||
console.error("wl-copy error:", data.toString())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Copy from clipboard history
|
||||
Process {
|
||||
id: copyHistoryProcess
|
||||
property string entryId: ""
|
||||
command: ["sh", "-c", "printf '%s' '" + entryId + "' | cliphist decode | wl-copy"]
|
||||
running: false
|
||||
|
||||
stderr: SplitParser {
|
||||
onRead: data => {
|
||||
console.error("Copy history error:", data.toString())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Periodic refresh timer (disabled by default)
|
||||
Timer {
|
||||
id: refreshTimer
|
||||
interval: 30000
|
||||
running: false // Only enable when needed
|
||||
repeat: true
|
||||
onTriggered: {
|
||||
if (!cliphistProcess.running && root.isVisible) {
|
||||
refreshClipboardHistory()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Component initialization
|
||||
Component.onCompleted: {
|
||||
refreshClipboardHistory()
|
||||
}
|
||||
|
||||
onIsVisibleChanged: {
|
||||
if (isVisible && cliphistModel.count === 0) {
|
||||
refreshClipboardHistory()
|
||||
}
|
||||
}
|
||||
|
||||
// Smart model update - only changes when content differs
|
||||
function updateModelIfChanged(newEntries) {
|
||||
// Quick length check
|
||||
if (newEntries.length !== currentEntries.length) {
|
||||
updateModel(newEntries)
|
||||
return
|
||||
}
|
||||
|
||||
// Compare content for changes
|
||||
let hasChanges = false
|
||||
for (let i = 0; i < newEntries.length; i++) {
|
||||
if (i >= currentEntries.length ||
|
||||
newEntries[i].id !== currentEntries[i].id ||
|
||||
newEntries[i].content !== currentEntries[i].content) {
|
||||
hasChanges = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (hasChanges) {
|
||||
updateModel(newEntries)
|
||||
}
|
||||
}
|
||||
|
||||
// Efficient model update with scroll position preservation
|
||||
function updateModel(newEntries) {
|
||||
const scrollPos = cliphistList.contentY
|
||||
|
||||
// Remove obsolete items
|
||||
for (let i = cliphistModel.count - 1; i >= 0; i--) {
|
||||
const modelItem = cliphistModel.get(i)
|
||||
const found = newEntries.some(entry => entry.id === modelItem.id)
|
||||
if (!found) {
|
||||
cliphistModel.remove(i)
|
||||
}
|
||||
}
|
||||
|
||||
// Add or update items
|
||||
for (let i = 0; i < newEntries.length; i++) {
|
||||
const newEntry = newEntries[i]
|
||||
let found = false
|
||||
|
||||
// Check if item exists and update position
|
||||
for (let j = 0; j < cliphistModel.count; j++) {
|
||||
const modelItem = cliphistModel.get(j)
|
||||
if (modelItem.id === newEntry.id) {
|
||||
if (modelItem.content !== newEntry.content) {
|
||||
cliphistModel.set(j, newEntry)
|
||||
}
|
||||
if (j !== i && i < cliphistModel.count) {
|
||||
cliphistModel.move(j, i, 1)
|
||||
}
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Add new item
|
||||
if (!found) {
|
||||
if (i < cliphistModel.count) {
|
||||
cliphistModel.insert(i, newEntry)
|
||||
} else {
|
||||
cliphistModel.append(newEntry)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Restore scroll position
|
||||
cliphistList.contentY = scrollPos
|
||||
currentEntries = newEntries.slice()
|
||||
}
|
||||
|
||||
// Content type detection based on patterns
|
||||
function detectContentType(content) {
|
||||
// Binary/image data detection
|
||||
if (content.includes('\x00') || content.startsWith('\x89PNG') || content.startsWith('\xFF\xD8\xFF')) {
|
||||
return "image"
|
||||
}
|
||||
if (content.includes('[[ binary data ') || content.includes('<selection>')) {
|
||||
return "image"
|
||||
}
|
||||
// URL detection
|
||||
if (/^https?:\/\/\S+$/.test(content.trim())) return "url"
|
||||
// Code detection
|
||||
if (content.includes('\n') && (content.includes('{') || content.includes('function') || content.includes('=>'))) return "code"
|
||||
// Command detection
|
||||
if (content.startsWith('sudo ') || content.startsWith('pacman ') || content.startsWith('apt ')) return "command"
|
||||
return "text"
|
||||
}
|
||||
|
||||
function formatTimestamp(timestamp) {
|
||||
const now = new Date()
|
||||
const entryDate = new Date(parseInt(timestamp))
|
||||
const diff = (now - entryDate) / 1000
|
||||
|
||||
if (diff < 60) return "Just now"
|
||||
if (diff < 3600) return Math.floor(diff / 60) + " min ago"
|
||||
if (diff < 86400) return Math.floor(diff / 3600) + " hour" + (Math.floor(diff / 3600) === 1 ? "" : "s") + " ago"
|
||||
return Qt.formatDateTime(entryDate, "MMM d h:mm AP")
|
||||
}
|
||||
|
||||
function clearClipboardHistory() {
|
||||
clearCliphistProcess.running = true
|
||||
}
|
||||
|
||||
function deleteClipboardEntry(entryId) {
|
||||
deleteEntryProcess.entryId = entryId
|
||||
deleteEntryProcess.running = true
|
||||
}
|
||||
|
||||
function refreshClipboardHistory() {
|
||||
cliphistProcess.running = true
|
||||
}
|
||||
|
||||
// Copy handler - chooses appropriate method based on content type
|
||||
function copyToClipboard(entryIdOrText, contentType) {
|
||||
if (contentType === "image" || typeof entryIdOrText === "string" && entryIdOrText.match(/^\d+$/)) {
|
||||
// Use cliphist decode for binary data and numbered entries
|
||||
copyHistoryProcess.entryId = entryIdOrText
|
||||
copyHistoryProcess.running = true
|
||||
} else {
|
||||
// Use wl-copy for plain text
|
||||
copyTextProcess.textToCopy = entryIdOrText
|
||||
copyTextProcess.running = true
|
||||
}
|
||||
}
|
||||
|
||||
// Clean up all processes on destruction
|
||||
Component.onDestruction: {
|
||||
cliphistProcess.running = false
|
||||
clearCliphistProcess.running = false
|
||||
deleteEntryProcess.running = false
|
||||
copyTextProcess.running = false
|
||||
copyHistoryProcess.running = false
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,161 @@
|
|||
pragma ComponentBehavior: Bound
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import "root:/Data/" as Data
|
||||
|
||||
// Custom system tray menu
|
||||
Rectangle {
|
||||
id: trayMenu
|
||||
implicitWidth: 360
|
||||
implicitHeight: Math.max(40, listView.contentHeight + 12 + 16)
|
||||
clip: true
|
||||
color: Data.ThemeManager.bgColor
|
||||
border.color: Data.ThemeManager.accentColor
|
||||
border.width: 3
|
||||
radius: 20
|
||||
visible: false
|
||||
enabled: visible
|
||||
|
||||
property QsMenuHandle menu
|
||||
property point triggerPoint: Qt.point(0, 0)
|
||||
property Item originalParent
|
||||
|
||||
// Menu opener handles native menu integration
|
||||
QsMenuOpener {
|
||||
id: opener
|
||||
menu: trayMenu.menu
|
||||
}
|
||||
|
||||
// Full-screen overlay to capture outside clicks
|
||||
Rectangle {
|
||||
id: overlay
|
||||
x: -trayMenu.x
|
||||
y: -trayMenu.y
|
||||
width: Screen.width
|
||||
height: Screen.height
|
||||
color: "transparent"
|
||||
visible: trayMenu.visible
|
||||
z: -1
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
enabled: trayMenu.visible
|
||||
acceptedButtons: Qt.AllButtons
|
||||
onPressed: {
|
||||
trayMenu.hide()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Flatten hierarchical menu structure into single list
|
||||
function flattenMenuItems(menuHandle) {
|
||||
var result = [];
|
||||
if (!menuHandle || !menuHandle.children) {
|
||||
return result;
|
||||
}
|
||||
|
||||
var childrenArray = [];
|
||||
for (var i = 0; i < menuHandle.children.length; i++) {
|
||||
childrenArray.push(menuHandle.children[i]);
|
||||
}
|
||||
|
||||
for (var i = 0; i < childrenArray.length; i++) {
|
||||
var item = childrenArray[i];
|
||||
|
||||
if (item.isSeparator) {
|
||||
result.push(item);
|
||||
} else if (item.menu) {
|
||||
// Add parent item and its submenu items
|
||||
result.push(item);
|
||||
var submenuItems = flattenMenuItems(item.menu);
|
||||
result = result.concat(submenuItems);
|
||||
} else {
|
||||
result.push(item);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
// Menu item list
|
||||
ListView {
|
||||
id: listView
|
||||
anchors.fill: parent
|
||||
anchors.margins: 6
|
||||
anchors.topMargin: 3
|
||||
anchors.bottomMargin: 9
|
||||
model: ScriptModel {
|
||||
values: flattenMenuItems(opener.menu)
|
||||
}
|
||||
interactive: false
|
||||
|
||||
delegate: Rectangle {
|
||||
id: entry
|
||||
required property var modelData
|
||||
|
||||
width: listView.width - 12
|
||||
height: modelData.isSeparator ? 10 : 28
|
||||
color: modelData.isSeparator ? Data.ThemeManager.bgColor : (mouseArea.containsMouse ? Data.ThemeManager.highlightBg : "transparent")
|
||||
radius: modelData.isSeparator ? 0 : 4
|
||||
|
||||
// Separator line rendering
|
||||
Item {
|
||||
anchors.fill: parent
|
||||
visible: modelData.isSeparator
|
||||
|
||||
Rectangle {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
width: parent.width * 0.85
|
||||
height: 1
|
||||
color: Data.ThemeManager.accentColor
|
||||
opacity: 0.3
|
||||
}
|
||||
}
|
||||
|
||||
// Menu item content (text and icon)
|
||||
RowLayout {
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: 8
|
||||
anchors.rightMargin: 8
|
||||
spacing: 6
|
||||
visible: !modelData.isSeparator
|
||||
|
||||
Text {
|
||||
Layout.fillWidth: true
|
||||
color: (modelData?.enabled ?? true) ? Data.ThemeManager.fgColor : Qt.darker(Data.ThemeManager.fgColor, 1.8)
|
||||
text: modelData?.text ?? ""
|
||||
font.pixelSize: 12
|
||||
font.family: "FiraCode Nerd Font"
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
elide: Text.ElideRight
|
||||
maximumLineCount: 1
|
||||
}
|
||||
|
||||
Image {
|
||||
Layout.preferredWidth: 14
|
||||
Layout.preferredHeight: 14
|
||||
source: modelData?.icon ?? ""
|
||||
visible: (modelData?.icon ?? "") !== ""
|
||||
fillMode: Image.PreserveAspectFit
|
||||
}
|
||||
}
|
||||
|
||||
// Click handling
|
||||
MouseArea {
|
||||
id: mouseArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
enabled: (modelData?.enabled ?? true) && trayMenu.visible && !modelData.isSeparator
|
||||
|
||||
onClicked: {
|
||||
if (modelData) {
|
||||
modelData.triggered()
|
||||
trayMenu.hide()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,442 @@
|
|||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import QtQuick.Shapes
|
||||
import Qt5Compat.GraphicalEffects
|
||||
import Quickshell.Io
|
||||
import "root:/Data" as Data
|
||||
import "root:/Core" as Core
|
||||
|
||||
// Niri workspace indicator
|
||||
Rectangle {
|
||||
id: root
|
||||
|
||||
property ListModel workspaces: ListModel {}
|
||||
property int currentWorkspace: -1
|
||||
property bool isDestroying: false
|
||||
|
||||
// Signal for workspace change bursts
|
||||
signal workspaceChanged(int workspaceId, color accentColor)
|
||||
|
||||
// MASTER ANIMATION CONTROLLER - drives Desktop overlay burst effect
|
||||
property real masterProgress: 0.0
|
||||
property bool effectsActive: false
|
||||
property color effectColor: Data.ThemeManager.accent
|
||||
|
||||
// Single master animation that controls Desktop overlay burst
|
||||
function triggerUnifiedWave() {
|
||||
effectColor = Data.ThemeManager.accent
|
||||
masterAnimation.restart()
|
||||
}
|
||||
|
||||
SequentialAnimation {
|
||||
id: masterAnimation
|
||||
|
||||
PropertyAction {
|
||||
target: root
|
||||
property: "effectsActive"
|
||||
value: true
|
||||
}
|
||||
|
||||
NumberAnimation {
|
||||
target: root
|
||||
property: "masterProgress"
|
||||
from: 0.0
|
||||
to: 1.0
|
||||
duration: 1000
|
||||
easing.type: Easing.OutQuint
|
||||
}
|
||||
|
||||
PropertyAction {
|
||||
target: root
|
||||
property: "effectsActive"
|
||||
value: false
|
||||
}
|
||||
|
||||
PropertyAction {
|
||||
target: root
|
||||
property: "masterProgress"
|
||||
value: 0.0
|
||||
}
|
||||
}
|
||||
|
||||
color: Data.ThemeManager.bgColor
|
||||
width: 32
|
||||
height: workspaceColumn.implicitHeight + 24
|
||||
|
||||
// Smooth height animation
|
||||
Behavior on height {
|
||||
NumberAnimation {
|
||||
duration: 300
|
||||
easing.type: Easing.OutCubic
|
||||
}
|
||||
}
|
||||
|
||||
// Right-side rounded corners
|
||||
topRightRadius: width / 2
|
||||
bottomRightRadius: width / 2
|
||||
topLeftRadius: 0
|
||||
bottomLeftRadius: 0
|
||||
|
||||
// Wave effects overlay - unified animation system (DISABLED - using Desktop overlay)
|
||||
Item {
|
||||
id: waveEffects
|
||||
anchors.fill: parent
|
||||
visible: false // Disabled in favor of unified overlay
|
||||
z: 2
|
||||
}
|
||||
|
||||
// Niri event stream listener
|
||||
Process {
|
||||
id: niriProcess
|
||||
command: ["niri", "msg", "event-stream"]
|
||||
running: true
|
||||
|
||||
stdout: SplitParser {
|
||||
onRead: data => {
|
||||
const lines = data.split('\n');
|
||||
for (const line of lines) {
|
||||
if (line.trim()) {
|
||||
parseNiriEvent(line.trim());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onExited: {
|
||||
// Auto-restart on failure to maintain workspace sync (but not during destruction)
|
||||
if (exitCode !== 0 && !root.isDestroying) {
|
||||
Qt.callLater(() => running = true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Parse Niri event stream messages
|
||||
function parseNiriEvent(line) {
|
||||
try {
|
||||
// Handle workspace focus changes
|
||||
if (line.startsWith("Workspace focused: ")) {
|
||||
const workspaceId = parseInt(line.replace("Workspace focused: ", ""));
|
||||
if (!isNaN(workspaceId)) {
|
||||
const previousWorkspace = root.currentWorkspace;
|
||||
root.currentWorkspace = workspaceId;
|
||||
updateWorkspaceFocus(workspaceId);
|
||||
|
||||
// Trigger burst effect if workspace actually changed
|
||||
if (previousWorkspace !== workspaceId && previousWorkspace !== -1) {
|
||||
root.triggerUnifiedWave();
|
||||
root.workspaceChanged(workspaceId, Data.ThemeManager.accent);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Handle workspace list updates
|
||||
else if (line.startsWith("Workspaces changed: ")) {
|
||||
const workspaceData = line.replace("Workspaces changed: ", "");
|
||||
parseWorkspaceList(workspaceData);
|
||||
}
|
||||
} catch (e) {
|
||||
console.log("Error parsing niri event:", e);
|
||||
}
|
||||
}
|
||||
|
||||
// Update workspace focus states
|
||||
function updateWorkspaceFocus(focusedWorkspaceId) {
|
||||
for (let i = 0; i < root.workspaces.count; i++) {
|
||||
const workspace = root.workspaces.get(i);
|
||||
const wasFocused = workspace.isFocused;
|
||||
const isFocused = workspace.id === focusedWorkspaceId;
|
||||
const isActive = workspace.id === focusedWorkspaceId;
|
||||
|
||||
// Only update changed properties to trigger animations
|
||||
if (wasFocused !== isFocused) {
|
||||
root.workspaces.setProperty(i, "isFocused", isFocused);
|
||||
root.workspaces.setProperty(i, "isActive", isActive);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Parse workspace data from Niri's Rust-style output format
|
||||
function parseWorkspaceList(data) {
|
||||
try {
|
||||
const workspaceMatches = data.match(/Workspace \{[^}]+\}/g);
|
||||
if (!workspaceMatches) {
|
||||
return;
|
||||
}
|
||||
|
||||
const newWorkspaces = [];
|
||||
|
||||
for (const match of workspaceMatches) {
|
||||
const idMatch = match.match(/id: (\d+)/);
|
||||
const idxMatch = match.match(/idx: (\d+)/);
|
||||
const nameMatch = match.match(/name: Some\("([^"]+)"\)|name: None/);
|
||||
const outputMatch = match.match(/output: Some\("([^"]+)"\)/);
|
||||
const isActiveMatch = match.match(/is_active: (true|false)/);
|
||||
const isFocusedMatch = match.match(/is_focused: (true|false)/);
|
||||
const isUrgentMatch = match.match(/is_urgent: (true|false)/);
|
||||
|
||||
if (idMatch && idxMatch && outputMatch) {
|
||||
const workspace = {
|
||||
id: parseInt(idMatch[1]),
|
||||
idx: parseInt(idxMatch[1]),
|
||||
name: nameMatch && nameMatch[1] ? nameMatch[1] : "",
|
||||
output: outputMatch[1],
|
||||
isActive: isActiveMatch ? isActiveMatch[1] === "true" : false,
|
||||
isFocused: isFocusedMatch ? isFocusedMatch[1] === "true" : false,
|
||||
isUrgent: isUrgentMatch ? isUrgentMatch[1] === "true" : false
|
||||
};
|
||||
|
||||
newWorkspaces.push(workspace);
|
||||
|
||||
if (workspace.isFocused) {
|
||||
root.currentWorkspace = workspace.id;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Sort by index and update model
|
||||
newWorkspaces.sort((a, b) => a.idx - b.idx);
|
||||
root.workspaces.clear();
|
||||
root.workspaces.append(newWorkspaces);
|
||||
} catch (e) {
|
||||
console.log("Error parsing workspace list:", e);
|
||||
}
|
||||
}
|
||||
|
||||
// Vertical workspace indicator pills
|
||||
Column {
|
||||
id: workspaceColumn
|
||||
anchors.centerIn: parent
|
||||
spacing: 6
|
||||
|
||||
Repeater {
|
||||
model: root.workspaces
|
||||
|
||||
Rectangle {
|
||||
id: workspacePill
|
||||
|
||||
// Dynamic sizing based on focus state
|
||||
width: model.isFocused ? 18 : 16
|
||||
height: model.isFocused ? 36 : 22
|
||||
radius: width / 2
|
||||
scale: model.isFocused ? 1.0 : 0.9
|
||||
|
||||
// Material Design 3 inspired colors
|
||||
color: {
|
||||
if (model.isFocused) {
|
||||
return Data.ThemeManager.accent;
|
||||
}
|
||||
if (model.isActive) {
|
||||
return Qt.rgba(Data.ThemeManager.accent.r, Data.ThemeManager.accent.g, Data.ThemeManager.accent.b, 0.5);
|
||||
}
|
||||
if (model.isUrgent) {
|
||||
return Data.ThemeManager.error;
|
||||
}
|
||||
return Qt.rgba(Data.ThemeManager.primaryText.r, Data.ThemeManager.primaryText.g, Data.ThemeManager.primaryText.b, 0.4);
|
||||
}
|
||||
|
||||
// Workspace pill burst overlay (DISABLED - using unified overlay)
|
||||
Rectangle {
|
||||
id: pillBurst
|
||||
anchors.centerIn: parent
|
||||
width: parent.width + 8
|
||||
height: parent.height + 8
|
||||
radius: width / 2
|
||||
color: Data.ThemeManager.accent
|
||||
opacity: 0 // Disabled in favor of unified overlay
|
||||
visible: false
|
||||
z: -1
|
||||
}
|
||||
|
||||
// Subtle pulse for inactive pills during workspace changes
|
||||
Rectangle {
|
||||
id: inactivePillPulse
|
||||
anchors.fill: parent
|
||||
radius: parent.radius
|
||||
color: Data.ThemeManager.accent
|
||||
opacity: {
|
||||
// Only pulse inactive pills during effects
|
||||
if (model.isFocused || !root.effectsActive) return 0
|
||||
|
||||
// More subtle pulse that peaks mid-animation
|
||||
if (root.masterProgress < 0.3) {
|
||||
return (root.masterProgress / 0.3) * 0.15
|
||||
} else if (root.masterProgress < 0.7) {
|
||||
return 0.15
|
||||
} else {
|
||||
return 0.15 * (1.0 - (root.masterProgress - 0.7) / 0.3)
|
||||
}
|
||||
}
|
||||
z: -0.5 // Behind the pill content but visible
|
||||
}
|
||||
|
||||
// Enhanced corner shadows for burst effect (DISABLED - using unified overlay)
|
||||
Rectangle {
|
||||
id: cornerBurst
|
||||
anchors.centerIn: parent
|
||||
width: parent.width + 4
|
||||
height: parent.height + 4
|
||||
radius: width / 2
|
||||
color: "transparent"
|
||||
border.color: Data.ThemeManager.accent
|
||||
border.width: 0 // Disabled
|
||||
opacity: 0 // Disabled in favor of unified overlay
|
||||
visible: false
|
||||
z: 1
|
||||
}
|
||||
|
||||
// Elevation shadow
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
anchors.topMargin: model.isFocused ? 1 : 0
|
||||
anchors.leftMargin: model.isFocused ? 0.5 : 0
|
||||
anchors.rightMargin: model.isFocused ? -0.5 : 0
|
||||
anchors.bottomMargin: model.isFocused ? -1 : 0
|
||||
radius: parent.radius
|
||||
color: Qt.rgba(0, 0, 0, model.isFocused ? 0.15 : 0)
|
||||
z: -1
|
||||
visible: model.isFocused
|
||||
|
||||
Behavior on color { ColorAnimation { duration: 200 } }
|
||||
}
|
||||
|
||||
// Smooth Material Design transitions
|
||||
Behavior on width {
|
||||
NumberAnimation {
|
||||
duration: 300
|
||||
easing.type: Easing.OutCubic
|
||||
}
|
||||
}
|
||||
Behavior on height {
|
||||
NumberAnimation {
|
||||
duration: 300
|
||||
easing.type: Easing.OutCubic
|
||||
}
|
||||
}
|
||||
Behavior on scale {
|
||||
NumberAnimation {
|
||||
duration: 250
|
||||
easing.type: Easing.OutCubic
|
||||
}
|
||||
}
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: 200
|
||||
}
|
||||
}
|
||||
|
||||
// Workspace number text
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
text: model.idx.toString()
|
||||
color: model.isFocused ? Data.ThemeManager.background : Data.ThemeManager.primaryText
|
||||
font.pixelSize: model.isFocused ? 10 : 8
|
||||
font.bold: model.isFocused
|
||||
font.family: "Roboto, sans-serif"
|
||||
visible: model.isFocused || model.isActive
|
||||
|
||||
Behavior on font.pixelSize { NumberAnimation { duration: 200 } }
|
||||
Behavior on color { ColorAnimation { duration: 200 } }
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
onClicked: {
|
||||
// Switch workspace via Niri command
|
||||
switchProcess.command = ["niri", "msg", "action", "focus-workspace", model.idx.toString()];
|
||||
switchProcess.running = true;
|
||||
}
|
||||
|
||||
// Hover feedback
|
||||
onEntered: {
|
||||
if (!model.isFocused) {
|
||||
workspacePill.color = Qt.rgba(Data.ThemeManager.primaryText.r, Data.ThemeManager.primaryText.g, Data.ThemeManager.primaryText.b, 0.6);
|
||||
}
|
||||
}
|
||||
|
||||
onExited: {
|
||||
// Reset to normal color
|
||||
if (!model.isFocused) {
|
||||
if (model.isActive) {
|
||||
workspacePill.color = Qt.rgba(Data.ThemeManager.accent.r, Data.ThemeManager.accent.g, Data.ThemeManager.accent.b, 0.5);
|
||||
} else if (model.isUrgent) {
|
||||
workspacePill.color = Data.ThemeManager.error;
|
||||
} else {
|
||||
workspacePill.color = Qt.rgba(Data.ThemeManager.primaryText.r, Data.ThemeManager.primaryText.g, Data.ThemeManager.primaryText.b, 0.4);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Workspace switching command process
|
||||
Process {
|
||||
id: switchProcess
|
||||
running: false
|
||||
onExited: {
|
||||
running = false
|
||||
if (exitCode !== 0) {
|
||||
console.log("Failed to switch workspace:", exitCode);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Border integration corners
|
||||
Core.Corners {
|
||||
id: topLeftCorner
|
||||
position: "topleft"
|
||||
size: 1.3
|
||||
fillColor: Data.ThemeManager.bgColor
|
||||
offsetX: -41
|
||||
offsetY: -25
|
||||
}
|
||||
|
||||
// Top-left corner wave overlay (DISABLED - using unified overlay)
|
||||
Shape {
|
||||
id: topLeftWave
|
||||
width: topLeftCorner.width
|
||||
height: topLeftCorner.height
|
||||
x: topLeftCorner.x
|
||||
y: topLeftCorner.y
|
||||
visible: false // Disabled in favor of unified overlay
|
||||
preferredRendererType: Shape.CurveRenderer
|
||||
layer.enabled: true
|
||||
layer.samples: 4
|
||||
}
|
||||
|
||||
Core.Corners {
|
||||
id: bottomLeftCorner
|
||||
position: "bottomleft"
|
||||
size: 1.3
|
||||
fillColor: Data.ThemeManager.bgColor
|
||||
offsetX: -41
|
||||
offsetY: 78
|
||||
}
|
||||
|
||||
// Bottom-left corner wave overlay (DISABLED - using unified overlay)
|
||||
Shape {
|
||||
id: bottomLeftWave
|
||||
width: bottomLeftCorner.width
|
||||
height: bottomLeftCorner.height
|
||||
x: bottomLeftCorner.x
|
||||
y: bottomLeftCorner.y
|
||||
visible: false // Disabled in favor of unified overlay
|
||||
preferredRendererType: Shape.CurveRenderer
|
||||
layer.enabled: true
|
||||
layer.samples: 4
|
||||
}
|
||||
|
||||
// Clean up processes on destruction
|
||||
Component.onDestruction: {
|
||||
root.isDestroying = true
|
||||
|
||||
if (niriProcess.running) {
|
||||
niriProcess.running = false
|
||||
}
|
||||
if (switchProcess.running) {
|
||||
switchProcess.running = false
|
||||
}
|
||||
}
|
||||
}
|
||||
228
modules/home/services/quickshell/qml/Widgets/System/OSD.qml
Normal file
228
modules/home/services/quickshell/qml/Widgets/System/OSD.qml
Normal file
|
|
@ -0,0 +1,228 @@
|
|||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import QtQuick.Layouts
|
||||
import QtQuick.Shapes
|
||||
import "root:/Data/" as Data
|
||||
import "root:/Core" as Core
|
||||
|
||||
Item {
|
||||
id: osd
|
||||
property var shell
|
||||
|
||||
QtObject {
|
||||
id: modeEnum
|
||||
readonly property int volume: 0
|
||||
readonly property int brightness: 1
|
||||
}
|
||||
|
||||
property int mode: -1
|
||||
property int lastVolume: -1
|
||||
property int lastBrightness: -1
|
||||
|
||||
width: osdBackground.width
|
||||
height: osdBackground.height
|
||||
visible: false
|
||||
|
||||
Timer {
|
||||
id: hideTimer
|
||||
interval: 2500
|
||||
onTriggered: hideOsd()
|
||||
}
|
||||
|
||||
FileView {
|
||||
id: brightnessFile
|
||||
path: "/tmp/brightness_osd_level"
|
||||
watchChanges: true
|
||||
blockLoading: true
|
||||
|
||||
onLoaded: updateBrightness()
|
||||
onFileChanged: {
|
||||
brightnessFile.reload();
|
||||
updateBrightness();
|
||||
}
|
||||
|
||||
function updateBrightness() {
|
||||
const val = parseInt(brightnessFile.text());
|
||||
if (!isNaN(val) && val !== lastBrightness) {
|
||||
lastBrightness = val;
|
||||
mode = modeEnum.brightness;
|
||||
showOsd();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: shell
|
||||
function onVolumeChanged() {
|
||||
if (shell.volume !== lastVolume && lastVolume !== -1) {
|
||||
lastVolume = shell.volume;
|
||||
mode = modeEnum.volume;
|
||||
showOsd();
|
||||
}
|
||||
lastVolume = shell.volume;
|
||||
}
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
if (shell?.volume !== undefined)
|
||||
lastVolume = shell.volume;
|
||||
}
|
||||
|
||||
function showOsd() {
|
||||
if (!osd.visible) {
|
||||
osd.visible = true;
|
||||
slideInAnimation.start();
|
||||
}
|
||||
hideTimer.restart();
|
||||
}
|
||||
|
||||
function hideOsd() {
|
||||
slideOutAnimation.start();
|
||||
}
|
||||
|
||||
NumberAnimation {
|
||||
id: slideInAnimation
|
||||
target: osdBackground
|
||||
property: "x"
|
||||
from: osd.width
|
||||
to: 0
|
||||
duration: 300
|
||||
easing.type: Easing.OutCubic
|
||||
}
|
||||
|
||||
NumberAnimation {
|
||||
id: slideOutAnimation
|
||||
target: osdBackground
|
||||
property: "x"
|
||||
from: 0
|
||||
to: osd.width
|
||||
duration: 250
|
||||
easing.type: Easing.InCubic
|
||||
onFinished: {
|
||||
osd.visible = false;
|
||||
osdBackground.x = 0;
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: osdBackground
|
||||
width: 45
|
||||
height: 250
|
||||
color: Data.ThemeManager.bgColor
|
||||
topLeftRadius: 20
|
||||
bottomLeftRadius: 20
|
||||
|
||||
Column {
|
||||
anchors.fill: parent
|
||||
anchors.margins: 16
|
||||
spacing: 12
|
||||
|
||||
Text {
|
||||
id: osdIcon
|
||||
font.family: "Roboto"
|
||||
font.pixelSize: 16
|
||||
color: Data.ThemeManager.fgColor
|
||||
text: {
|
||||
if (mode === modeEnum.volume) {
|
||||
if (!shell || shell.volume === undefined)
|
||||
return "";
|
||||
const vol = shell.volume;
|
||||
return vol === 0 ? "" : vol < 33 ? "" : vol < 66 ? "" : "";
|
||||
} else if (mode === modeEnum.brightness) {
|
||||
const b = lastBrightness;
|
||||
return b < 0 ? "" : b < 33 ? "" : b < 66 ? "" : "";
|
||||
}
|
||||
return "";
|
||||
}
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
|
||||
Behavior on text {
|
||||
SequentialAnimation {
|
||||
PropertyAnimation {
|
||||
target: osdIcon
|
||||
property: "scale"
|
||||
to: 1.2
|
||||
duration: 100
|
||||
}
|
||||
PropertyAnimation {
|
||||
target: osdIcon
|
||||
property: "scale"
|
||||
to: 1.0
|
||||
duration: 100
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: 10
|
||||
height: parent.height - osdIcon.height - osdLabel.height - 36
|
||||
radius: 5
|
||||
color: Qt.darker(Data.ThemeManager.accentColor, 1.5)
|
||||
border.color: Qt.darker(Data.ThemeManager.accentColor, 2.0)
|
||||
border.width: 1
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
|
||||
Rectangle {
|
||||
id: fillBar
|
||||
width: parent.width - 2
|
||||
radius: parent.radius - 1
|
||||
x: 1
|
||||
color: Data.ThemeManager.accentColor
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.bottomMargin: 1
|
||||
height: {
|
||||
const val = mode === modeEnum.volume ? shell?.volume : lastBrightness;
|
||||
const maxHeight = parent.height - 2;
|
||||
return maxHeight * Math.max(0, Math.min(1, val / 100));
|
||||
}
|
||||
Behavior on height {
|
||||
NumberAnimation {
|
||||
duration: 250
|
||||
easing.type: Easing.OutCubic
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Text {
|
||||
id: osdLabel
|
||||
text: {
|
||||
const val = mode === modeEnum.volume ? shell?.volume : lastBrightness;
|
||||
return val >= 0 ? val + "%" : "0%";
|
||||
}
|
||||
font.pixelSize: 10
|
||||
font.weight: Font.Bold
|
||||
color: Data.ThemeManager.fgColor
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
|
||||
Behavior on text {
|
||||
PropertyAnimation {
|
||||
target: osdLabel
|
||||
property: "opacity"
|
||||
from: 0.7
|
||||
to: 1.0
|
||||
duration: 150
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Core.Corners {
|
||||
position: "bottomright"
|
||||
size: 1.3
|
||||
fillColor: Data.ThemeManager.bgColor
|
||||
offsetX: 39 + osdBackground.x
|
||||
offsetY: 78
|
||||
}
|
||||
|
||||
Core.Corners {
|
||||
position: "topright"
|
||||
size: 1.3
|
||||
fillColor: Data.ThemeManager.bgColor
|
||||
offsetX: 39 + osdBackground.x
|
||||
offsetY: -26
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,181 @@
|
|||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import QtQuick.Controls
|
||||
import Quickshell
|
||||
import Quickshell.Services.SystemTray
|
||||
import "root:/Data" as Data
|
||||
|
||||
// System tray with optimized icon caching
|
||||
Row {
|
||||
property var bar
|
||||
property var shell
|
||||
property var trayMenu
|
||||
spacing: 8
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
|
||||
property bool containsMouse: false
|
||||
property var systemTray: SystemTray
|
||||
|
||||
// Custom icon cache for memory optimization
|
||||
property var iconCache: ({})
|
||||
property var iconCacheCount: ({})
|
||||
|
||||
// Cache cleanup to prevent memory leaks
|
||||
Timer {
|
||||
interval: 120000
|
||||
repeat: true
|
||||
running: systemTray.items.length > 0
|
||||
onTriggered: {
|
||||
// Decrement counters and remove unused icons
|
||||
for (let icon in iconCacheCount) {
|
||||
iconCacheCount[icon]--
|
||||
if (iconCacheCount[icon] <= 0) {
|
||||
delete iconCache[icon]
|
||||
delete iconCacheCount[icon]
|
||||
}
|
||||
}
|
||||
|
||||
// Enforce maximum cache size
|
||||
const maxCacheSize = 10;
|
||||
const cacheKeys = Object.keys(iconCache);
|
||||
if (cacheKeys.length > maxCacheSize) {
|
||||
const toRemove = cacheKeys.slice(0, cacheKeys.length - maxCacheSize);
|
||||
toRemove.forEach(key => {
|
||||
delete iconCache[key];
|
||||
delete iconCacheCount[key];
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: systemTray.items
|
||||
delegate: Item {
|
||||
width: 24
|
||||
height: 24
|
||||
property bool isHovered: trayMouseArea.containsMouse
|
||||
|
||||
onIsHoveredChanged: updateParentHoverState()
|
||||
Component.onCompleted: updateParentHoverState()
|
||||
|
||||
function updateParentHoverState() {
|
||||
let anyHovered = false
|
||||
for (let i = 0; i < parent.children.length; i++) {
|
||||
if (parent.children[i].isHovered) {
|
||||
anyHovered = true
|
||||
break
|
||||
}
|
||||
}
|
||||
parent.containsMouse = anyHovered
|
||||
}
|
||||
|
||||
// Hover animations
|
||||
scale: isHovered ? 1.15 : 1.0
|
||||
Behavior on scale {
|
||||
enabled: isHovered
|
||||
NumberAnimation {
|
||||
duration: 150
|
||||
easing.type: Easing.OutCubic
|
||||
}
|
||||
}
|
||||
|
||||
rotation: isHovered ? 5 : 0
|
||||
Behavior on rotation {
|
||||
enabled: isHovered
|
||||
NumberAnimation {
|
||||
duration: 200
|
||||
easing.type: Easing.OutCubic
|
||||
}
|
||||
}
|
||||
|
||||
Image {
|
||||
id: trayIcon
|
||||
anchors.centerIn: parent
|
||||
width: 18
|
||||
height: 18
|
||||
sourceSize.width: 18
|
||||
sourceSize.height: 18
|
||||
smooth: false // Memory savings
|
||||
asynchronous: true
|
||||
cache: false // Use custom cache instead
|
||||
source: {
|
||||
let icon = modelData?.icon || "";
|
||||
if (!icon) return "";
|
||||
|
||||
// Return cached icon if available
|
||||
if (iconCache[icon]) {
|
||||
iconCacheCount[icon] = 2
|
||||
return iconCache[icon];
|
||||
}
|
||||
|
||||
// Process icon path
|
||||
let finalPath = icon;
|
||||
if (icon.includes("?path=")) {
|
||||
const [name, path] = icon.split("?path=");
|
||||
const fileName = name.substring(name.lastIndexOf("/") + 1);
|
||||
finalPath = `file://${path}/${fileName}`;
|
||||
}
|
||||
|
||||
// Cache the processed path
|
||||
iconCache[icon] = finalPath;
|
||||
iconCacheCount[icon] = 2;
|
||||
return finalPath;
|
||||
}
|
||||
opacity: status === Image.Ready ? 1 : 0
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: 300
|
||||
easing.type: Easing.OutCubic
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Component.onDestruction: {
|
||||
let icon = modelData?.icon || "";
|
||||
if (icon) {
|
||||
delete iconCache[icon];
|
||||
delete iconCacheCount[icon];
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: trayMouseArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
|
||||
onClicked: (mouse) => {
|
||||
if (!modelData) return;
|
||||
|
||||
if (mouse.button === Qt.LeftButton) {
|
||||
if (trayMenu && trayMenu.visible) {
|
||||
trayMenu.hide()
|
||||
}
|
||||
if (!modelData.onlyMenu) {
|
||||
modelData.activate()
|
||||
}
|
||||
} else if (mouse.button === Qt.MiddleButton) {
|
||||
if (trayMenu && trayMenu.visible) {
|
||||
trayMenu.hide()
|
||||
}
|
||||
modelData.secondaryActivate && modelData.secondaryActivate()
|
||||
} else if (mouse.button === Qt.RightButton) {
|
||||
if (trayMenu && trayMenu.visible) {
|
||||
trayMenu.hide()
|
||||
return
|
||||
}
|
||||
// Show context menu if available
|
||||
if (modelData.hasMenu && modelData.menu && trayMenu) {
|
||||
trayMenu.menu = modelData.menu
|
||||
const iconCenter = Qt.point(width / 2, height)
|
||||
const iconPos = mapToItem(trayMenu.parent, 0, 0)
|
||||
const menuX = iconPos.x - (trayMenu.width / 2) + (width / 2)
|
||||
const menuY = iconPos.y + height + 15
|
||||
trayMenu.show(Qt.point(menuX, menuY), trayMenu.parent)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,188 @@
|
|||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Wayland
|
||||
import QtQuick.Layouts
|
||||
import QtQuick.Shapes
|
||||
import "root:/Data/" as Data
|
||||
import "root:/Core" as Core
|
||||
|
||||
// Volume OSD with slide animation
|
||||
Item {
|
||||
id: volumeOsd
|
||||
property var shell
|
||||
|
||||
// Size and visibility
|
||||
width: osdBackground.width
|
||||
height: osdBackground.height
|
||||
visible: false
|
||||
|
||||
// Auto-hide timer (2.5 seconds of inactivity)
|
||||
Timer {
|
||||
id: hideTimer
|
||||
interval: 2500
|
||||
onTriggered: hideOsd()
|
||||
}
|
||||
|
||||
property int lastVolume: -1
|
||||
|
||||
// Monitor volume changes from shell and trigger OSD
|
||||
Connections {
|
||||
target: shell
|
||||
function onVolumeChanged() {
|
||||
if (shell.volume !== lastVolume && lastVolume !== -1) {
|
||||
showOsd()
|
||||
}
|
||||
lastVolume = shell.volume
|
||||
}
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
// Initialize lastVolume on startup
|
||||
if (shell && shell.volume !== undefined) {
|
||||
lastVolume = shell.volume
|
||||
}
|
||||
}
|
||||
|
||||
// Show OSD
|
||||
function showOsd() {
|
||||
if (!volumeOsd.visible) {
|
||||
volumeOsd.visible = true
|
||||
slideInAnimation.start()
|
||||
}
|
||||
hideTimer.restart()
|
||||
}
|
||||
|
||||
// Start slide-out animation to hide OSD
|
||||
function hideOsd() {
|
||||
slideOutAnimation.start()
|
||||
}
|
||||
|
||||
// Slide in from right edge
|
||||
NumberAnimation {
|
||||
id: slideInAnimation
|
||||
target: osdBackground
|
||||
property: "x"
|
||||
from: volumeOsd.width
|
||||
to: 0
|
||||
duration: 300
|
||||
easing.type: Easing.OutCubic
|
||||
}
|
||||
|
||||
// Slide out to right edge
|
||||
NumberAnimation {
|
||||
id: slideOutAnimation
|
||||
target: osdBackground
|
||||
property: "x"
|
||||
from: 0
|
||||
to: volumeOsd.width
|
||||
duration: 250
|
||||
easing.type: Easing.InCubic
|
||||
onFinished: {
|
||||
volumeOsd.visible = false
|
||||
osdBackground.x = 0 // Reset position
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: osdBackground
|
||||
width: 45
|
||||
height: 250
|
||||
color: Data.ThemeManager.bgColor
|
||||
topLeftRadius: 20
|
||||
bottomLeftRadius: 20
|
||||
|
||||
Column {
|
||||
anchors.fill: parent
|
||||
anchors.margins: 16
|
||||
spacing: 12
|
||||
|
||||
// Dynamic volume icon
|
||||
Text {
|
||||
id: volumeIcon
|
||||
font.family: "Roboto"
|
||||
font.pixelSize: 16
|
||||
color: Data.ThemeManager.fgColor
|
||||
text: {
|
||||
if (!shell || shell.volume === undefined) return ""
|
||||
var vol = shell.volume
|
||||
if (vol === 0) return "" // Muted
|
||||
else if (vol < 33) return "" // Low
|
||||
else if (vol < 66) return "" // Medium
|
||||
else return "" // High
|
||||
}
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
|
||||
// Scale animation on volume change
|
||||
Behavior on text {
|
||||
SequentialAnimation {
|
||||
PropertyAnimation { target: volumeIcon; property: "scale"; to: 1.2; duration: 100 }
|
||||
PropertyAnimation { target: volumeIcon; property: "scale"; to: 1.0; duration: 100 }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Vertical volume bar
|
||||
Rectangle {
|
||||
width: 10
|
||||
height: parent.height - volumeIcon.height - volumeLabel.height - 36
|
||||
radius: 5
|
||||
color: Qt.darker(Data.ThemeManager.accentColor, 1.5)
|
||||
border.color: Qt.darker(Data.ThemeManager.accentColor, 2.0)
|
||||
border.width: 1
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
|
||||
// Animated volume fill indicator
|
||||
Rectangle {
|
||||
id: volumeFill
|
||||
width: parent.width - 2
|
||||
radius: parent.radius - 1
|
||||
x: 1
|
||||
color: Data.ThemeManager.accentColor
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.bottomMargin: 1
|
||||
height: {
|
||||
if (!shell || shell.volume === undefined) return 0
|
||||
var maxHeight = parent.height - 2
|
||||
return maxHeight * Math.max(0, Math.min(1, shell.volume / 100))
|
||||
}
|
||||
Behavior on height {
|
||||
NumberAnimation { duration: 250; easing.type: Easing.OutCubic }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Volume percentage text
|
||||
Text {
|
||||
id: volumeLabel
|
||||
text: (shell && shell.volume !== undefined ? shell.volume + "%" : "0%")
|
||||
font.pixelSize: 10
|
||||
font.weight: Font.Bold
|
||||
color: Data.ThemeManager.fgColor
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
|
||||
// Fade animation on volume change
|
||||
Behavior on text {
|
||||
PropertyAnimation { target: volumeLabel; property: "opacity"; from: 0.7; to: 1.0; duration: 150 }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Core.Corners {
|
||||
id: bottomRightCorner
|
||||
position: "bottomright"
|
||||
size: 1.3
|
||||
fillColor: Data.ThemeManager.bgColor
|
||||
offsetX: 39 + osdBackground.x
|
||||
offsetY: 78
|
||||
}
|
||||
|
||||
Core.Corners {
|
||||
id: topRightCorner
|
||||
position: "topright"
|
||||
size: 1.3
|
||||
fillColor: Data.ThemeManager.bgColor
|
||||
offsetX: 39 + osdBackground.x
|
||||
offsetY: -26
|
||||
}
|
||||
}
|
||||
56
modules/home/services/quickshell/qml/Widgets/Workspace.qml
Normal file
56
modules/home/services/quickshell/qml/Widgets/Workspace.qml
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import Quickshell
|
||||
import Quickshell.Hyprland
|
||||
import "root:/Data" as Data
|
||||
|
||||
// Hyprland workspace indicator
|
||||
Column {
|
||||
id: root
|
||||
property var shell
|
||||
spacing: 8
|
||||
|
||||
Repeater {
|
||||
model: Hyprland.workspaces
|
||||
|
||||
delegate: Rectangle {
|
||||
width: 22
|
||||
height: 22
|
||||
radius: 6
|
||||
color: modelData.active ? Data.ThemeManager.accentColor : "transparent"
|
||||
border.color: Data.ThemeManager.accentColor
|
||||
border.width: modelData.active ? 0 : 1
|
||||
opacity: modelData.active ? 1 : 0.6
|
||||
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
text: modelData.name || modelData.id
|
||||
color: modelData.active ? Data.ThemeManager.bgColor : Data.ThemeManager.fgColor
|
||||
font.family: "Roboto"
|
||||
font.pixelSize: 12
|
||||
font.bold: modelData.active
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
onClicked: modelData.activate()
|
||||
onPressAndHold: {
|
||||
// Move focused window to workspace (regular workspaces only)
|
||||
if (modelData.id > 0) {
|
||||
Hyprland.dispatch(`movetoworkspace ${modelData.id}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Workspace synchronization
|
||||
Connections {
|
||||
target: Hyprland
|
||||
function onFocusedWorkspaceChanged() {
|
||||
Hyprland.refreshWorkspaces()
|
||||
}
|
||||
}
|
||||
|
||||
Component.onCompleted: Hyprland.refreshWorkspaces()
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue