mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-04-03 20:32:07 -04:00
initial structure refactor
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -1,485 +0,0 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import Quickshell
|
||||
import Quickshell.Services.UPower
|
||||
import Quickshell.Wayland
|
||||
import Quickshell.Widgets
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
|
||||
PanelWindow {
|
||||
id: root
|
||||
|
||||
property bool batteryPopupVisible: false
|
||||
|
||||
function isActiveProfile(profile) {
|
||||
if (typeof PowerProfiles === "undefined")
|
||||
return false;
|
||||
|
||||
return PowerProfiles.profile === profile;
|
||||
}
|
||||
|
||||
function setProfile(profile) {
|
||||
if (typeof PowerProfiles === "undefined") {
|
||||
ToastService.showError("power-profiles-daemon not available");
|
||||
return ;
|
||||
}
|
||||
PowerProfiles.profile = profile;
|
||||
if (PowerProfiles.profile !== profile)
|
||||
ToastService.showError("Failed to set power profile");
|
||||
}
|
||||
|
||||
visible: batteryPopupVisible
|
||||
implicitWidth: 400
|
||||
implicitHeight: 300
|
||||
WlrLayershell.layer: WlrLayershell.Overlay
|
||||
WlrLayershell.exclusiveZone: -1
|
||||
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
|
||||
color: "transparent"
|
||||
|
||||
anchors {
|
||||
top: true
|
||||
left: true
|
||||
right: true
|
||||
bottom: true
|
||||
}
|
||||
|
||||
// Click outside to dismiss overlay
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
onClicked: {
|
||||
batteryPopupVisible = false;
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: Math.min(380, parent.width - Theme.spacingL * 2)
|
||||
height: Math.min(450, parent.height - Theme.barHeight - Theme.spacingS * 2)
|
||||
x: Math.max(Theme.spacingL, parent.width - width - Theme.spacingL)
|
||||
y: Theme.barHeight + Theme.spacingS
|
||||
color: Theme.popupBackground()
|
||||
radius: Theme.cornerRadiusLarge
|
||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
|
||||
border.width: 1
|
||||
opacity: batteryPopupVisible ? 1 : 0
|
||||
scale: batteryPopupVisible ? 1 : 0.85
|
||||
|
||||
// Prevent click-through to background
|
||||
MouseArea {
|
||||
// Consume the click to prevent it from reaching the background
|
||||
|
||||
anchors.fill: parent
|
||||
onClicked: {
|
||||
}
|
||||
}
|
||||
|
||||
ScrollView {
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingL
|
||||
clip: true
|
||||
|
||||
Column {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingL
|
||||
|
||||
// Header
|
||||
Row {
|
||||
width: parent.width
|
||||
|
||||
Text {
|
||||
text: BatteryService.batteryAvailable ? "Battery Information" : "Power Management"
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Item {
|
||||
width: parent.width - 200
|
||||
height: 1
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: 32
|
||||
height: 32
|
||||
radius: 16
|
||||
color: closeBatteryArea.containsMouse ? Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.12) : "transparent"
|
||||
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
text: "close"
|
||||
font.family: Theme.iconFont
|
||||
font.pixelSize: Theme.iconSize - 4
|
||||
color: closeBatteryArea.containsMouse ? Theme.error : Theme.surfaceText
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: closeBatteryArea
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
batteryPopupVisible = false;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: 80
|
||||
radius: Theme.cornerRadius
|
||||
color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.5)
|
||||
border.color: BatteryService.isCharging ? Theme.primary : (BatteryService.isLowBattery ? Theme.error : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12))
|
||||
border.width: BatteryService.isCharging || BatteryService.isLowBattery ? 2 : 1
|
||||
visible: BatteryService.batteryAvailable
|
||||
|
||||
Row {
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Theme.spacingL
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Theme.spacingL
|
||||
|
||||
Text {
|
||||
text: Theme.getBatteryIcon(BatteryService.batteryLevel, BatteryService.isCharging, BatteryService.batteryAvailable)
|
||||
font.family: Theme.iconFont
|
||||
font.pixelSize: Theme.iconSizeLarge
|
||||
color: {
|
||||
if (BatteryService.isLowBattery && !BatteryService.isCharging)
|
||||
return Theme.error;
|
||||
|
||||
if (BatteryService.isCharging)
|
||||
return Theme.primary;
|
||||
|
||||
return Theme.surfaceText;
|
||||
}
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Column {
|
||||
spacing: 2
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
Row {
|
||||
spacing: Theme.spacingM
|
||||
|
||||
Text {
|
||||
text: BatteryService.batteryLevel + "%"
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
color: {
|
||||
if (BatteryService.isLowBattery && !BatteryService.isCharging)
|
||||
return Theme.error;
|
||||
|
||||
if (BatteryService.isCharging)
|
||||
return Theme.primary;
|
||||
|
||||
return Theme.surfaceText;
|
||||
}
|
||||
font.weight: Font.Bold
|
||||
}
|
||||
|
||||
Text {
|
||||
text: BatteryService.batteryStatus
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: {
|
||||
if (BatteryService.isLowBattery && !BatteryService.isCharging)
|
||||
return Theme.error;
|
||||
|
||||
if (BatteryService.isCharging)
|
||||
return Theme.primary;
|
||||
|
||||
return Theme.surfaceText;
|
||||
}
|
||||
font.weight: Font.Medium
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Text {
|
||||
text: {
|
||||
let time = BatteryService.formatTimeRemaining();
|
||||
if (time !== "Unknown")
|
||||
return BatteryService.isCharging ? "Time until full: " + time : "Time remaining: " + time;
|
||||
|
||||
return "";
|
||||
}
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
|
||||
visible: text.length > 0
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// No battery info card
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: 80
|
||||
radius: Theme.cornerRadius
|
||||
color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.5)
|
||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
|
||||
border.width: 1
|
||||
visible: !BatteryService.batteryAvailable
|
||||
|
||||
Row {
|
||||
anchors.centerIn: parent
|
||||
spacing: Theme.spacingL
|
||||
|
||||
Text {
|
||||
text: Theme.getBatteryIcon(0, false, false)
|
||||
font.family: Theme.iconFont
|
||||
font.pixelSize: 36
|
||||
color: Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Column {
|
||||
spacing: Theme.spacingS
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
Text {
|
||||
text: "No Battery Detected"
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
}
|
||||
|
||||
Text {
|
||||
text: "Power profile management is available"
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Battery details
|
||||
Column {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingM
|
||||
visible: BatteryService.batteryAvailable
|
||||
|
||||
Text {
|
||||
text: "Battery Details"
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
}
|
||||
|
||||
Row {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingXL
|
||||
|
||||
// Health
|
||||
Column {
|
||||
spacing: 2
|
||||
width: (parent.width - Theme.spacingXL) / 2
|
||||
|
||||
Text {
|
||||
text: "Health"
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
|
||||
font.weight: Font.Medium
|
||||
}
|
||||
|
||||
Text {
|
||||
text: BatteryService.batteryHealth
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: {
|
||||
if (BatteryService.batteryHealth === "N/A")
|
||||
return Theme.surfaceText;
|
||||
|
||||
var healthNum = parseInt(BatteryService.batteryHealth);
|
||||
return healthNum < 80 ? Theme.error : Theme.surfaceText;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Capacity
|
||||
Column {
|
||||
spacing: 2
|
||||
width: (parent.width - Theme.spacingXL) / 2
|
||||
|
||||
Text {
|
||||
text: "Capacity"
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
|
||||
font.weight: Font.Medium
|
||||
}
|
||||
|
||||
Text {
|
||||
text: BatteryService.batteryCapacity > 0 ? BatteryService.batteryCapacity.toFixed(1) + " Wh" : "Unknown"
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceText
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Power profiles
|
||||
Column {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingM
|
||||
visible: true
|
||||
|
||||
Text {
|
||||
text: "Power Profile"
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
}
|
||||
|
||||
Column {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingS
|
||||
|
||||
Repeater {
|
||||
model: (typeof PowerProfiles !== "undefined") ? [PowerProfile.PowerSaver, PowerProfile.Balanced].concat(PowerProfiles.hasPerformanceProfile ? [PowerProfile.Performance] : []) : [PowerProfile.PowerSaver, PowerProfile.Balanced, PowerProfile.Performance]
|
||||
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: 50
|
||||
radius: Theme.cornerRadius
|
||||
color: profileArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : (batteryControlPopup.isActiveProfile(modelData) ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.16) : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08))
|
||||
border.color: batteryControlPopup.isActiveProfile(modelData) ? Theme.primary : "transparent"
|
||||
border.width: 2
|
||||
|
||||
Row {
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Theme.spacingL
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Theme.spacingM
|
||||
|
||||
Text {
|
||||
text: Theme.getPowerProfileIcon(modelData)
|
||||
font.family: Theme.iconFont
|
||||
font.pixelSize: Theme.iconSize
|
||||
color: batteryControlPopup.isActiveProfile(modelData) ? Theme.primary : Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Column {
|
||||
spacing: 2
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
Text {
|
||||
text: Theme.getPowerProfileLabel(modelData)
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: batteryControlPopup.isActiveProfile(modelData) ? Theme.primary : Theme.surfaceText
|
||||
font.weight: batteryControlPopup.isActiveProfile(modelData) ? Font.Medium : Font.Normal
|
||||
}
|
||||
|
||||
Text {
|
||||
text: Theme.getPowerProfileDescription(modelData)
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: profileArea
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
batteryControlPopup.setProfile(modelData);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Degradation reason warning
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: 60
|
||||
radius: Theme.cornerRadius
|
||||
color: Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.12)
|
||||
border.color: Theme.error
|
||||
border.width: 2
|
||||
visible: (typeof PowerProfiles !== "undefined") && PowerProfiles.degradationReason !== PerformanceDegradationReason.None
|
||||
|
||||
Row {
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Theme.spacingL
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Theme.spacingM
|
||||
|
||||
Text {
|
||||
text: "warning"
|
||||
font.family: Theme.iconFont
|
||||
font.pixelSize: Theme.iconSize
|
||||
color: Theme.error
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Column {
|
||||
spacing: 2
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
Text {
|
||||
text: "Power Profile Degradation"
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.error
|
||||
font.weight: Font.Medium
|
||||
}
|
||||
|
||||
Text {
|
||||
text: (typeof PowerProfiles !== "undefined") ? PerformanceDegradationReason.toString(PowerProfiles.degradationReason) : ""
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.8)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: Theme.mediumDuration
|
||||
easing.type: Theme.emphasizedEasing
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Behavior on scale {
|
||||
NumberAnimation {
|
||||
duration: Theme.mediumDuration
|
||||
easing.type: Theme.emphasizedEasing
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -1,164 +0,0 @@
|
||||
import QtQuick
|
||||
import Quickshell.Services.UPower
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
|
||||
Rectangle {
|
||||
id: batteryWidget
|
||||
|
||||
property bool batteryPopupVisible: false
|
||||
|
||||
signal toggleBatteryPopup()
|
||||
|
||||
width: BatteryService.batteryAvailable ? 70 : 40
|
||||
height: 30
|
||||
radius: Theme.cornerRadius
|
||||
color: batteryArea.containsMouse || batteryPopupVisible ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.16) : Qt.rgba(Theme.secondary.r, Theme.secondary.g, Theme.secondary.b, 0.08)
|
||||
visible: true
|
||||
|
||||
Row {
|
||||
anchors.centerIn: parent
|
||||
spacing: 4
|
||||
|
||||
Text {
|
||||
text: Theme.getBatteryIcon(BatteryService.batteryLevel, BatteryService.isCharging, BatteryService.batteryAvailable)
|
||||
font.family: Theme.iconFont
|
||||
font.pixelSize: Theme.iconSize - 6
|
||||
color: {
|
||||
if (!BatteryService.batteryAvailable)
|
||||
return Theme.surfaceText;
|
||||
|
||||
if (BatteryService.isLowBattery && !BatteryService.isCharging)
|
||||
return Theme.error;
|
||||
|
||||
if (BatteryService.isCharging)
|
||||
return Theme.primary;
|
||||
|
||||
return Theme.surfaceText;
|
||||
}
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
SequentialAnimation on opacity {
|
||||
running: BatteryService.isCharging
|
||||
loops: Animation.Infinite
|
||||
|
||||
NumberAnimation {
|
||||
to: 0.6
|
||||
duration: 1000
|
||||
easing.type: Easing.InOutQuad
|
||||
}
|
||||
|
||||
NumberAnimation {
|
||||
to: 1
|
||||
duration: 1000
|
||||
easing.type: Easing.InOutQuad
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Text {
|
||||
text: BatteryService.batteryLevel + "%"
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.weight: Font.Medium
|
||||
color: {
|
||||
if (!BatteryService.batteryAvailable)
|
||||
return Theme.surfaceText;
|
||||
|
||||
if (BatteryService.isLowBattery && !BatteryService.isCharging)
|
||||
return Theme.error;
|
||||
|
||||
if (BatteryService.isCharging)
|
||||
return Theme.primary;
|
||||
|
||||
return Theme.surfaceText;
|
||||
}
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
visible: BatteryService.batteryAvailable
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: batteryArea
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
toggleBatteryPopup();
|
||||
}
|
||||
}
|
||||
|
||||
// Tooltip on hover
|
||||
Rectangle {
|
||||
id: batteryTooltip
|
||||
|
||||
width: Math.max(120, tooltipText.contentWidth + Theme.spacingM * 2)
|
||||
height: tooltipText.contentHeight + Theme.spacingS * 2
|
||||
radius: Theme.cornerRadius
|
||||
color: Theme.surfaceContainer
|
||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
|
||||
border.width: 1
|
||||
visible: batteryArea.containsMouse && !batteryPopupVisible
|
||||
anchors.bottom: parent.top
|
||||
anchors.bottomMargin: Theme.spacingS
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
opacity: batteryArea.containsMouse ? 1 : 0
|
||||
|
||||
Column {
|
||||
anchors.centerIn: parent
|
||||
spacing: 2
|
||||
|
||||
Text {
|
||||
id: tooltipText
|
||||
|
||||
text: {
|
||||
if (!BatteryService.batteryAvailable) {
|
||||
if (typeof PowerProfiles === "undefined")
|
||||
return "Power Management";
|
||||
|
||||
switch (PowerProfiles.profile) {
|
||||
case PowerProfile.PowerSaver:
|
||||
return "Power Profile: Power Saver";
|
||||
case PowerProfile.Performance:
|
||||
return "Power Profile: Performance";
|
||||
default:
|
||||
return "Power Profile: Balanced";
|
||||
}
|
||||
}
|
||||
let status = BatteryService.batteryStatus;
|
||||
let level = BatteryService.batteryLevel + "%";
|
||||
let time = BatteryService.formatTimeRemaining();
|
||||
if (time !== "Unknown")
|
||||
return status + " • " + level + " • " + time;
|
||||
else
|
||||
return status + " • " + level;
|
||||
}
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceText
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Theme.standardEasing
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Theme.standardEasing
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,270 +0,0 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Effects
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
|
||||
Column {
|
||||
id: calendarWidget
|
||||
|
||||
property date displayDate: new Date()
|
||||
property date selectedDate: new Date()
|
||||
|
||||
function loadEventsForMonth() {
|
||||
if (!CalendarService || !CalendarService.khalAvailable)
|
||||
return ;
|
||||
|
||||
// Calculate date range with padding
|
||||
let firstDay = new Date(displayDate.getFullYear(), displayDate.getMonth(), 1);
|
||||
let dayOfWeek = firstDay.getDay();
|
||||
let startDate = new Date(firstDay);
|
||||
startDate.setDate(startDate.getDate() - dayOfWeek - 7); // Extra week padding
|
||||
let lastDay = new Date(displayDate.getFullYear(), displayDate.getMonth() + 1, 0);
|
||||
let endDate = new Date(lastDay);
|
||||
endDate.setDate(endDate.getDate() + (6 - lastDay.getDay()) + 7); // Extra week padding
|
||||
CalendarService.loadEvents(startDate, endDate);
|
||||
}
|
||||
|
||||
spacing: Theme.spacingM
|
||||
// Load events when display date changes
|
||||
onDisplayDateChanged: {
|
||||
loadEventsForMonth();
|
||||
}
|
||||
Component.onCompleted: {
|
||||
loadEventsForMonth();
|
||||
}
|
||||
|
||||
// Load events when calendar service becomes available
|
||||
Connections {
|
||||
function onKhalAvailableChanged() {
|
||||
if (CalendarService && CalendarService.khalAvailable)
|
||||
loadEventsForMonth();
|
||||
|
||||
}
|
||||
|
||||
target: CalendarService
|
||||
enabled: CalendarService !== null
|
||||
}
|
||||
|
||||
// Month navigation header
|
||||
Row {
|
||||
width: parent.width
|
||||
height: 40
|
||||
|
||||
Rectangle {
|
||||
width: 40
|
||||
height: 40
|
||||
radius: Theme.cornerRadius
|
||||
color: prevMonthArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
|
||||
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
text: "chevron_left"
|
||||
font.family: Theme.iconFont
|
||||
font.pixelSize: Theme.iconSize
|
||||
color: Theme.primary
|
||||
font.weight: Theme.iconFontWeight
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: prevMonthArea
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
let newDate = new Date(displayDate);
|
||||
newDate.setMonth(newDate.getMonth() - 1);
|
||||
displayDate = newDate;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Text {
|
||||
width: parent.width - 80
|
||||
height: 40
|
||||
text: Qt.formatDate(displayDate, "MMMM yyyy")
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: 40
|
||||
height: 40
|
||||
radius: Theme.cornerRadius
|
||||
color: nextMonthArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
|
||||
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
text: "chevron_right"
|
||||
font.family: Theme.iconFont
|
||||
font.pixelSize: Theme.iconSize
|
||||
color: Theme.primary
|
||||
font.weight: Theme.iconFontWeight
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: nextMonthArea
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
let newDate = new Date(displayDate);
|
||||
newDate.setMonth(newDate.getMonth() + 1);
|
||||
displayDate = newDate;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Days of week header
|
||||
Row {
|
||||
width: parent.width
|
||||
height: 32
|
||||
|
||||
Repeater {
|
||||
model: ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]
|
||||
|
||||
Rectangle {
|
||||
width: parent.width / 7
|
||||
height: 32
|
||||
color: "transparent"
|
||||
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
text: modelData
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.6)
|
||||
font.weight: Font.Medium
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Calendar grid
|
||||
Grid {
|
||||
property date firstDay: {
|
||||
let date = new Date(displayDate.getFullYear(), displayDate.getMonth(), 1);
|
||||
let dayOfWeek = date.getDay();
|
||||
date.setDate(date.getDate() - dayOfWeek);
|
||||
return date;
|
||||
}
|
||||
|
||||
width: parent.width
|
||||
height: 200 // Fixed height for calendar
|
||||
columns: 7
|
||||
rows: 6
|
||||
|
||||
Repeater {
|
||||
model: 42
|
||||
|
||||
Rectangle {
|
||||
property date dayDate: {
|
||||
let date = new Date(parent.firstDay);
|
||||
date.setDate(date.getDate() + index);
|
||||
return date;
|
||||
}
|
||||
property bool isCurrentMonth: dayDate.getMonth() === displayDate.getMonth()
|
||||
property bool isToday: dayDate.toDateString() === new Date().toDateString()
|
||||
property bool isSelected: dayDate.toDateString() === selectedDate.toDateString()
|
||||
|
||||
width: parent.width / 7
|
||||
height: parent.height / 6
|
||||
color: isSelected ? Theme.primary : isToday ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : dayArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : "transparent"
|
||||
radius: Theme.cornerRadiusSmall
|
||||
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
text: dayDate.getDate()
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: isSelected ? Theme.surface : isToday ? Theme.primary : isCurrentMonth ? Theme.surfaceText : Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.4)
|
||||
font.weight: isToday || isSelected ? Font.Medium : Font.Normal
|
||||
}
|
||||
|
||||
// Event indicator - full-width elegant bar
|
||||
Rectangle {
|
||||
// Use a lighter tint of primary for selected state
|
||||
|
||||
id: eventIndicator
|
||||
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.margins: 2
|
||||
height: 3
|
||||
radius: 1.5
|
||||
visible: CalendarService && CalendarService.khalAvailable && CalendarService.hasEventsForDate(dayDate)
|
||||
// Dynamic color based on state with opacity
|
||||
color: {
|
||||
if (isSelected)
|
||||
return Qt.lighter(Theme.primary, 1.3);
|
||||
else if (isToday)
|
||||
return Theme.primary;
|
||||
else
|
||||
return Theme.primary;
|
||||
}
|
||||
opacity: {
|
||||
if (isSelected)
|
||||
return 0.9;
|
||||
else if (isToday)
|
||||
return 0.8;
|
||||
else
|
||||
return 0.6;
|
||||
}
|
||||
// Subtle animation on hover
|
||||
scale: dayArea.containsMouse ? 1.05 : 1
|
||||
|
||||
Behavior on scale {
|
||||
NumberAnimation {
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Theme.standardEasing
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Theme.standardEasing
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Theme.standardEasing
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: dayArea
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
selectedDate = dayDate;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,233 +0,0 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Effects
|
||||
import Quickshell
|
||||
import Quickshell.Services.Mpris
|
||||
import Quickshell.Wayland
|
||||
import Quickshell.Widgets
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
|
||||
PanelWindow {
|
||||
id: root
|
||||
|
||||
readonly property bool hasActiveMedia: MprisController.activePlayer !== null
|
||||
property bool calendarVisible: false
|
||||
|
||||
visible: calendarVisible
|
||||
onVisibleChanged: {
|
||||
if (visible && CalendarService) {
|
||||
CalendarService.loadCurrentMonth();
|
||||
}
|
||||
}
|
||||
implicitWidth: 480
|
||||
implicitHeight: 600
|
||||
WlrLayershell.layer: WlrLayershell.Overlay
|
||||
WlrLayershell.exclusiveZone: -1
|
||||
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
|
||||
color: "transparent"
|
||||
|
||||
anchors {
|
||||
top: true
|
||||
left: true
|
||||
right: true
|
||||
bottom: true
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: mainContainer
|
||||
|
||||
function calculateWidth() {
|
||||
let baseWidth = 320;
|
||||
if (leftWidgets.hasAnyWidgets)
|
||||
return Math.min(parent.width * 0.9, 600);
|
||||
|
||||
return Math.min(parent.width * 0.7, 400);
|
||||
}
|
||||
|
||||
function calculateHeight() {
|
||||
let contentHeight = Theme.spacingM * 2; // margins
|
||||
// Main row with widgets and calendar
|
||||
let widgetHeight = 160;
|
||||
// Media widget always present
|
||||
widgetHeight += 140 + Theme.spacingM;
|
||||
// Weather widget always present
|
||||
let calendarHeight = 300;
|
||||
let mainRowHeight = Math.max(widgetHeight, calendarHeight);
|
||||
contentHeight += mainRowHeight + Theme.spacingM;
|
||||
// Add events widget height - use calculated height instead of actual
|
||||
if (CalendarService && CalendarService.khalAvailable) {
|
||||
let hasEvents = eventsWidget.selectedDateEvents && eventsWidget.selectedDateEvents.length > 0;
|
||||
let eventsHeight = hasEvents ? Math.min(300, 80 + eventsWidget.selectedDateEvents.length * 60) : 120;
|
||||
contentHeight += eventsHeight;
|
||||
}
|
||||
return Math.min(contentHeight, parent.height * 0.9);
|
||||
}
|
||||
|
||||
width: calculateWidth()
|
||||
height: calculateHeight()
|
||||
x: (parent.width - width) / 2
|
||||
y: Theme.barHeight + 4
|
||||
color: Theme.surfaceContainer
|
||||
radius: Theme.cornerRadiusLarge
|
||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
|
||||
border.width: 1
|
||||
layer.enabled: true
|
||||
opacity: calendarVisible ? 1 : 0
|
||||
scale: calendarVisible ? 1 : 0.92
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
color: Qt.rgba(Theme.surfaceTint.r, Theme.surfaceTint.g, Theme.surfaceTint.b, 0.04)
|
||||
radius: parent.radius
|
||||
|
||||
SequentialAnimation on opacity {
|
||||
running: calendarVisible
|
||||
loops: Animation.Infinite
|
||||
|
||||
NumberAnimation {
|
||||
to: 0.08
|
||||
duration: Theme.extraLongDuration
|
||||
easing.type: Theme.standardEasing
|
||||
}
|
||||
|
||||
NumberAnimation {
|
||||
to: 0.02
|
||||
duration: Theme.extraLongDuration
|
||||
easing.type: Theme.standardEasing
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Update height when calendar service events change
|
||||
Connections {
|
||||
function onEventsByDateChanged() {
|
||||
mainContainer.height = mainContainer.calculateHeight();
|
||||
}
|
||||
|
||||
function onKhalAvailableChanged() {
|
||||
mainContainer.height = mainContainer.calculateHeight();
|
||||
}
|
||||
|
||||
target: CalendarService
|
||||
enabled: CalendarService !== null
|
||||
}
|
||||
|
||||
// Update height when events widget's selectedDateEvents changes
|
||||
Connections {
|
||||
function onSelectedDateEventsChanged() {
|
||||
mainContainer.height = mainContainer.calculateHeight();
|
||||
}
|
||||
|
||||
target: eventsWidget
|
||||
enabled: eventsWidget !== null
|
||||
}
|
||||
|
||||
Column {
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingM
|
||||
spacing: Theme.spacingM
|
||||
|
||||
// Main row with widgets and calendar
|
||||
Row {
|
||||
width: parent.width
|
||||
height: {
|
||||
let widgetHeight = 160; // Media widget always present
|
||||
widgetHeight += 140 + Theme.spacingM; // Weather widget always present
|
||||
let calendarHeight = 300;
|
||||
return Math.max(widgetHeight, calendarHeight);
|
||||
}
|
||||
spacing: Theme.spacingM
|
||||
|
||||
// Left section for widgets
|
||||
Column {
|
||||
id: leftWidgets
|
||||
|
||||
property bool hasAnyWidgets: true // Always show media widget and weather widget
|
||||
|
||||
width: hasAnyWidgets ? parent.width * 0.45 : 0
|
||||
height: childrenRect.height
|
||||
spacing: Theme.spacingM
|
||||
visible: hasAnyWidgets
|
||||
anchors.top: parent.top
|
||||
|
||||
MediaPlayerWidget {
|
||||
visible: true // Always visible - shows placeholder when no media
|
||||
width: parent.width
|
||||
height: 160
|
||||
}
|
||||
|
||||
WeatherWidget {
|
||||
visible: true // Always visible - shows placeholder when no weather
|
||||
width: parent.width
|
||||
height: 140
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Right section for calendar
|
||||
CalendarWidget {
|
||||
id: calendarWidget
|
||||
|
||||
width: leftWidgets.hasAnyWidgets ? parent.width * 0.55 - Theme.spacingL : parent.width
|
||||
height: parent.height
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Full-width events widget below
|
||||
EventsWidget {
|
||||
id: eventsWidget
|
||||
|
||||
width: parent.width
|
||||
selectedDate: calendarWidget.selectedDate
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
layer.effect: MultiEffect {
|
||||
shadowEnabled: true
|
||||
shadowHorizontalOffset: 0
|
||||
shadowVerticalOffset: 4
|
||||
shadowBlur: 0.5
|
||||
shadowColor: Qt.rgba(0, 0, 0, 0.15)
|
||||
shadowOpacity: 0.15
|
||||
}
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: Theme.longDuration
|
||||
easing.type: Theme.emphasizedEasing
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Behavior on scale {
|
||||
NumberAnimation {
|
||||
duration: Theme.longDuration
|
||||
easing.type: Theme.emphasizedEasing
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Behavior on height {
|
||||
NumberAnimation {
|
||||
duration: Theme.mediumDuration
|
||||
easing.type: Theme.standardEasing
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
z: -1
|
||||
onClicked: {
|
||||
calendarVisible = false;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,319 +0,0 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Effects
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
|
||||
// Events widget for selected date - Material Design 3 style
|
||||
Rectangle {
|
||||
id: eventsWidget
|
||||
|
||||
property date selectedDate: new Date()
|
||||
property var selectedDateEvents: []
|
||||
property bool hasEvents: selectedDateEvents && selectedDateEvents.length > 0
|
||||
property bool shouldShow: CalendarService && CalendarService.khalAvailable
|
||||
|
||||
function updateSelectedDateEvents() {
|
||||
if (CalendarService && CalendarService.khalAvailable) {
|
||||
let events = CalendarService.getEventsForDate(selectedDate);
|
||||
console.log("EventsWidget: Updating events for", Qt.formatDate(selectedDate, "yyyy-MM-dd"), "found", events.length, "events");
|
||||
selectedDateEvents = events;
|
||||
} else {
|
||||
selectedDateEvents = [];
|
||||
}
|
||||
}
|
||||
|
||||
onSelectedDateEventsChanged: {
|
||||
console.log("EventsWidget: selectedDateEvents changed, count:", selectedDateEvents.length);
|
||||
eventsList.model = selectedDateEvents;
|
||||
}
|
||||
width: parent.width
|
||||
height: shouldShow ? (hasEvents ? Math.min(300, 80 + selectedDateEvents.length * 60) : 120) : 0
|
||||
radius: Theme.cornerRadiusLarge
|
||||
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.12)
|
||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
|
||||
border.width: 1
|
||||
visible: shouldShow
|
||||
// Material elevation shadow
|
||||
layer.enabled: true
|
||||
Component.onCompleted: {
|
||||
updateSelectedDateEvents();
|
||||
}
|
||||
onSelectedDateChanged: {
|
||||
updateSelectedDateEvents();
|
||||
}
|
||||
|
||||
// Update events when selected date or events change
|
||||
Connections {
|
||||
function onEventsByDateChanged() {
|
||||
updateSelectedDateEvents();
|
||||
}
|
||||
|
||||
function onKhalAvailableChanged() {
|
||||
updateSelectedDateEvents();
|
||||
}
|
||||
|
||||
target: CalendarService
|
||||
enabled: CalendarService !== null
|
||||
}
|
||||
|
||||
// Header - always visible when widget is shown
|
||||
Row {
|
||||
id: headerRow
|
||||
|
||||
anchors.top: parent.top
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.margins: Theme.spacingL
|
||||
spacing: Theme.spacingS
|
||||
|
||||
Text {
|
||||
text: "event"
|
||||
font.family: Theme.iconFont
|
||||
font.pixelSize: Theme.iconSize - 2
|
||||
color: Theme.primary
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Text {
|
||||
text: hasEvents ? (Qt.formatDate(selectedDate, "MMM d") + " • " + (selectedDateEvents.length === 1 ? "1 event" : selectedDateEvents.length + " events")) : Qt.formatDate(selectedDate, "MMM d")
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// No events placeholder - centered in entire widget (not just content area)
|
||||
Column {
|
||||
anchors.centerIn: parent
|
||||
spacing: Theme.spacingXS
|
||||
visible: !hasEvents
|
||||
|
||||
Text {
|
||||
text: "event_busy"
|
||||
font.family: Theme.iconFont
|
||||
font.pixelSize: Theme.iconSize + 8
|
||||
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.3)
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
}
|
||||
|
||||
Text {
|
||||
text: "No events"
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.5)
|
||||
font.weight: Font.Normal
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Events list - positioned below header when there are events
|
||||
ListView {
|
||||
id: eventsList
|
||||
|
||||
anchors.top: headerRow.bottom
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.margins: Theme.spacingL
|
||||
anchors.topMargin: Theme.spacingM
|
||||
visible: opacity > 0
|
||||
opacity: hasEvents ? 1 : 0
|
||||
clip: true
|
||||
spacing: Theme.spacingS
|
||||
boundsMovement: Flickable.StopAtBounds
|
||||
boundsBehavior: Flickable.StopAtBounds
|
||||
|
||||
ScrollBar.vertical: ScrollBar {
|
||||
policy: eventsList.contentHeight > eventsList.height ? ScrollBar.AsNeeded : ScrollBar.AlwaysOff
|
||||
}
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: Theme.mediumDuration
|
||||
easing.type: Theme.emphasizedEasing
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
delegate: Rectangle {
|
||||
width: eventsList.width
|
||||
height: eventContent.implicitHeight + Theme.spacingM
|
||||
radius: Theme.cornerRadius
|
||||
color: {
|
||||
if (modelData.url && eventMouseArea.containsMouse)
|
||||
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12);
|
||||
else if (eventMouseArea.containsMouse)
|
||||
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.06);
|
||||
return Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.06);
|
||||
}
|
||||
border.color: {
|
||||
if (modelData.url && eventMouseArea.containsMouse)
|
||||
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.3);
|
||||
else if (eventMouseArea.containsMouse)
|
||||
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.15);
|
||||
return "transparent";
|
||||
}
|
||||
border.width: 1
|
||||
|
||||
// Event indicator strip
|
||||
Rectangle {
|
||||
width: 4
|
||||
height: parent.height - 8
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: 4
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
radius: 2
|
||||
color: Theme.primary
|
||||
opacity: 0.8
|
||||
}
|
||||
|
||||
Column {
|
||||
id: eventContent
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.leftMargin: Theme.spacingL + 4
|
||||
anchors.rightMargin: Theme.spacingM
|
||||
spacing: 6
|
||||
|
||||
Text {
|
||||
width: parent.width
|
||||
text: modelData.title
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
elide: Text.ElideRight
|
||||
wrapMode: Text.Wrap
|
||||
maximumLineCount: 2
|
||||
}
|
||||
|
||||
Item {
|
||||
width: parent.width
|
||||
height: Math.max(timeRow.height, locationRow.height)
|
||||
|
||||
Row {
|
||||
id: timeRow
|
||||
|
||||
spacing: 4
|
||||
anchors.left: parent.left
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
Text {
|
||||
text: "schedule"
|
||||
font.family: Theme.iconFont
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Text {
|
||||
text: {
|
||||
if (modelData.allDay) {
|
||||
return "All day";
|
||||
} else {
|
||||
let timeFormat = Prefs.use24HourClock ? "H:mm" : "h:mm AP";
|
||||
let startTime = Qt.formatTime(modelData.start, timeFormat);
|
||||
if (modelData.start.toDateString() !== modelData.end.toDateString() || modelData.start.getTime() !== modelData.end.getTime())
|
||||
return startTime + " – " + Qt.formatTime(modelData.end, timeFormat);
|
||||
|
||||
return startTime;
|
||||
}
|
||||
}
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
|
||||
font.weight: Font.Normal
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Row {
|
||||
id: locationRow
|
||||
|
||||
spacing: 4
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
visible: modelData.location !== ""
|
||||
|
||||
Text {
|
||||
text: "location_on"
|
||||
font.family: Theme.iconFont
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Text {
|
||||
text: modelData.location
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
|
||||
elide: Text.ElideRight
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
maximumLineCount: 1
|
||||
width: Math.min(implicitWidth, 200)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: eventMouseArea
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: modelData.url ? Qt.PointingHandCursor : Qt.ArrowCursor
|
||||
enabled: modelData.url !== ""
|
||||
onClicked: {
|
||||
if (modelData.url && modelData.url !== "") {
|
||||
if (Qt.openUrlExternally(modelData.url) === false)
|
||||
console.warn("Couldn't open", modelData.url);
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Theme.standardEasing
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Behavior on border.color {
|
||||
ColorAnimation {
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Theme.standardEasing
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
layer.effect: MultiEffect {
|
||||
shadowEnabled: true
|
||||
shadowHorizontalOffset: 0
|
||||
shadowVerticalOffset: 2
|
||||
shadowBlur: 0.25
|
||||
shadowColor: Qt.rgba(0, 0, 0, 0.1)
|
||||
shadowOpacity: 0.1
|
||||
}
|
||||
|
||||
Behavior on height {
|
||||
NumberAnimation {
|
||||
duration: Theme.mediumDuration
|
||||
easing.type: Theme.emphasizedEasing
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,456 +0,0 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Effects
|
||||
import Quickshell
|
||||
import Quickshell.Services.Mpris
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
|
||||
Rectangle {
|
||||
id: mediaPlayerWidget
|
||||
|
||||
property MprisPlayer activePlayer: MprisController.activePlayer
|
||||
property string lastValidTitle: ""
|
||||
property string lastValidArtist: ""
|
||||
property string lastValidAlbum: ""
|
||||
property string lastValidArtUrl: ""
|
||||
property real currentPosition: 0
|
||||
|
||||
// Simple progress ratio calculation
|
||||
function ratio() {
|
||||
return activePlayer && activePlayer.length > 0 ? currentPosition / activePlayer.length : 0;
|
||||
}
|
||||
|
||||
onActivePlayerChanged: {
|
||||
if (!activePlayer)
|
||||
updateTimer.start();
|
||||
else
|
||||
updateTimer.stop();
|
||||
}
|
||||
width: parent.width
|
||||
height: parent.height
|
||||
radius: Theme.cornerRadiusLarge
|
||||
color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.4)
|
||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
|
||||
border.width: 1
|
||||
layer.enabled: true
|
||||
|
||||
Timer {
|
||||
id: updateTimer
|
||||
|
||||
interval: 2000
|
||||
running: {
|
||||
// Run when no active player (for cache clearing) OR when playing (for position updates)
|
||||
return (!activePlayer) ||
|
||||
(activePlayer && activePlayer.playbackState === MprisPlaybackState.Playing &&
|
||||
activePlayer.length > 0 && !progressMouseArea.isSeeking);
|
||||
}
|
||||
repeat: true
|
||||
onTriggered: {
|
||||
if (!activePlayer) {
|
||||
// Clear cache when no player
|
||||
lastValidTitle = "";
|
||||
lastValidArtist = "";
|
||||
lastValidAlbum = "";
|
||||
lastValidArtUrl = "";
|
||||
stop(); // Stop after clearing cache
|
||||
} else if (activePlayer.playbackState === MprisPlaybackState.Playing && !progressMouseArea.isSeeking) {
|
||||
// Update position when playing
|
||||
currentPosition = activePlayer.position;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Backend events
|
||||
Connections {
|
||||
function onPositionChanged() {
|
||||
if (!progressMouseArea.isSeeking)
|
||||
currentPosition = activePlayer.position;
|
||||
|
||||
}
|
||||
|
||||
function onPostTrackChanged() {
|
||||
currentPosition = activePlayer && activePlayer.position || 0;
|
||||
}
|
||||
|
||||
function onTrackTitleChanged() {
|
||||
currentPosition = activePlayer && activePlayer.position || 0;
|
||||
}
|
||||
|
||||
target: activePlayer
|
||||
}
|
||||
|
||||
Item {
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingS
|
||||
|
||||
// Placeholder when no media - centered in entire widget
|
||||
Column {
|
||||
anchors.centerIn: parent
|
||||
spacing: Theme.spacingS
|
||||
visible: (!activePlayer && !lastValidTitle) || (activePlayer && activePlayer.trackTitle === "" && lastValidTitle === "")
|
||||
|
||||
Text {
|
||||
text: "music_note"
|
||||
font.family: Theme.iconFont
|
||||
font.pixelSize: Theme.iconSize + 8
|
||||
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.5)
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
}
|
||||
|
||||
Text {
|
||||
text: "No Media Playing"
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Active content in a column
|
||||
Column {
|
||||
anchors.fill: parent
|
||||
spacing: Theme.spacingS
|
||||
visible: activePlayer && activePlayer.trackTitle !== "" || lastValidTitle !== ""
|
||||
|
||||
// Normal media info when playing
|
||||
Row {
|
||||
width: parent.width
|
||||
height: 60
|
||||
spacing: Theme.spacingM
|
||||
|
||||
// Album Art
|
||||
Rectangle {
|
||||
width: 60
|
||||
height: 60
|
||||
radius: Theme.cornerRadius
|
||||
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.3)
|
||||
|
||||
Item {
|
||||
anchors.fill: parent
|
||||
clip: true
|
||||
|
||||
Image {
|
||||
id: albumArt
|
||||
|
||||
anchors.fill: parent
|
||||
source: activePlayer && activePlayer.trackArtUrl || lastValidArtUrl || ""
|
||||
onSourceChanged: {
|
||||
if (activePlayer && activePlayer.trackArtUrl)
|
||||
lastValidArtUrl = activePlayer.trackArtUrl;
|
||||
|
||||
}
|
||||
fillMode: Image.PreserveAspectCrop
|
||||
smooth: true
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
visible: albumArt.status !== Image.Ready
|
||||
color: "transparent"
|
||||
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
text: "album"
|
||||
font.family: Theme.iconFont
|
||||
font.pixelSize: 28
|
||||
color: Theme.surfaceVariantText
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Track Info
|
||||
Column {
|
||||
width: parent.width - 60 - Theme.spacingM
|
||||
height: parent.height
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
Text {
|
||||
text: activePlayer && activePlayer.trackTitle || lastValidTitle || "Unknown Track"
|
||||
onTextChanged: {
|
||||
if (activePlayer && activePlayer.trackTitle)
|
||||
lastValidTitle = activePlayer.trackTitle;
|
||||
|
||||
}
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
font.weight: Font.Bold
|
||||
color: Theme.surfaceText
|
||||
width: parent.width
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
|
||||
Text {
|
||||
text: activePlayer && activePlayer.trackArtist || lastValidArtist || "Unknown Artist"
|
||||
onTextChanged: {
|
||||
if (activePlayer && activePlayer.trackArtist)
|
||||
lastValidArtist = activePlayer.trackArtist;
|
||||
|
||||
}
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.8)
|
||||
width: parent.width
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
|
||||
Text {
|
||||
text: activePlayer && activePlayer.trackAlbum || lastValidAlbum || ""
|
||||
onTextChanged: {
|
||||
if (activePlayer && activePlayer.trackAlbum)
|
||||
lastValidAlbum = activePlayer.trackAlbum;
|
||||
|
||||
}
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.6)
|
||||
width: parent.width
|
||||
elide: Text.ElideRight
|
||||
visible: text.length > 0
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Progress bar
|
||||
Item {
|
||||
id: progressBarContainer
|
||||
|
||||
width: parent.width
|
||||
height: 24
|
||||
|
||||
Rectangle {
|
||||
id: progressBarBackground
|
||||
|
||||
width: parent.width
|
||||
height: 6
|
||||
radius: 3
|
||||
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.3)
|
||||
visible: activePlayer !== null
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
Rectangle {
|
||||
id: progressFill
|
||||
|
||||
height: parent.height
|
||||
radius: parent.radius
|
||||
color: Theme.primary
|
||||
width: parent.width * ratio()
|
||||
|
||||
Behavior on width {
|
||||
NumberAnimation {
|
||||
duration: 100
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Drag handle
|
||||
Rectangle {
|
||||
id: progressHandle
|
||||
|
||||
width: 12
|
||||
height: 12
|
||||
radius: 6
|
||||
color: Theme.primary
|
||||
border.color: Qt.lighter(Theme.primary, 1.3)
|
||||
border.width: 1
|
||||
x: Math.max(0, Math.min(parent.width - width, progressFill.width - width / 2))
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
visible: activePlayer && activePlayer.length > 0
|
||||
scale: progressMouseArea.containsMouse || progressMouseArea.pressed ? 1.2 : 1
|
||||
|
||||
Behavior on scale {
|
||||
NumberAnimation {
|
||||
duration: 150
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: progressMouseArea
|
||||
|
||||
property bool isSeeking: false
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
enabled: activePlayer && activePlayer.length > 0 && activePlayer.canSeek
|
||||
preventStealing: true
|
||||
onPressed: function(mouse) {
|
||||
isSeeking = true;
|
||||
if (activePlayer && activePlayer.length > 0) {
|
||||
let ratio = Math.max(0, Math.min(1, mouse.x / progressBarBackground.width));
|
||||
let seekPosition = ratio * activePlayer.length;
|
||||
activePlayer.position = seekPosition;
|
||||
currentPosition = seekPosition;
|
||||
}
|
||||
}
|
||||
onReleased: {
|
||||
isSeeking = false;
|
||||
}
|
||||
onPositionChanged: function(mouse) {
|
||||
if (pressed && isSeeking && activePlayer && activePlayer.length > 0) {
|
||||
let ratio = Math.max(0, Math.min(1, mouse.x / progressBarBackground.width));
|
||||
let seekPosition = ratio * activePlayer.length;
|
||||
activePlayer.position = seekPosition;
|
||||
currentPosition = seekPosition;
|
||||
}
|
||||
}
|
||||
onClicked: function(mouse) {
|
||||
if (activePlayer && activePlayer.length > 0) {
|
||||
let ratio = Math.max(0, Math.min(1, mouse.x / progressBarBackground.width));
|
||||
let seekPosition = ratio * activePlayer.length;
|
||||
activePlayer.position = seekPosition;
|
||||
currentPosition = seekPosition;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Global mouse area for drag tracking
|
||||
MouseArea {
|
||||
id: progressGlobalMouseArea
|
||||
|
||||
anchors.fill: parent.parent.parent // Fill the entire media player widget
|
||||
enabled: progressMouseArea.isSeeking
|
||||
visible: false
|
||||
preventStealing: true
|
||||
onPositionChanged: function(mouse) {
|
||||
if (progressMouseArea.isSeeking && activePlayer && activePlayer.length > 0) {
|
||||
let globalPos = mapToItem(progressBarBackground, mouse.x, mouse.y);
|
||||
let ratio = Math.max(0, Math.min(1, globalPos.x / progressBarBackground.width));
|
||||
let seekPosition = ratio * activePlayer.length;
|
||||
activePlayer.position = seekPosition;
|
||||
currentPosition = seekPosition;
|
||||
}
|
||||
}
|
||||
onReleased: {
|
||||
progressMouseArea.isSeeking = false;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Control buttons - always visible
|
||||
Item {
|
||||
width: parent.width
|
||||
height: 32
|
||||
visible: activePlayer !== null
|
||||
|
||||
Row {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
spacing: Theme.spacingM
|
||||
height: parent.height
|
||||
|
||||
// Previous button
|
||||
Rectangle {
|
||||
width: 28
|
||||
height: 28
|
||||
radius: 14
|
||||
color: prevBtnArea.containsMouse ? Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.12) : "transparent"
|
||||
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
text: "skip_previous"
|
||||
font.family: Theme.iconFont
|
||||
font.pixelSize: 16
|
||||
color: Theme.surfaceText
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: prevBtnArea
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
if (!activePlayer)
|
||||
return ;
|
||||
|
||||
// >8 s → jump to start, otherwise previous track
|
||||
if (currentPosition > 8 && activePlayer.canSeek) {
|
||||
activePlayer.position = 0;
|
||||
currentPosition = 0;
|
||||
} else {
|
||||
activePlayer.previous();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Play/Pause button
|
||||
Rectangle {
|
||||
width: 32
|
||||
height: 32
|
||||
radius: 16
|
||||
color: Theme.primary
|
||||
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
text: activePlayer && activePlayer.playbackState === MprisPlaybackState.Playing ? "pause" : "play_arrow"
|
||||
font.family: Theme.iconFont
|
||||
font.pixelSize: 20
|
||||
color: Theme.background
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: activePlayer && activePlayer.togglePlaying()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Next button
|
||||
Rectangle {
|
||||
width: 28
|
||||
height: 28
|
||||
radius: 14
|
||||
color: nextBtnArea.containsMouse ? Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.12) : "transparent"
|
||||
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
text: "skip_next"
|
||||
font.family: Theme.iconFont
|
||||
font.pixelSize: 16
|
||||
color: Theme.surfaceText
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: nextBtnArea
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: activePlayer && activePlayer.next()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
layer.effect: MultiEffect {
|
||||
shadowEnabled: true
|
||||
shadowHorizontalOffset: 0
|
||||
shadowVerticalOffset: 2
|
||||
shadowBlur: 0.5
|
||||
shadowColor: Qt.rgba(0, 0, 0, 0.1)
|
||||
shadowOpacity: 0.1
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,202 +0,0 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Effects
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
|
||||
Rectangle {
|
||||
id: weatherWidget
|
||||
|
||||
width: parent.width
|
||||
height: parent.height
|
||||
radius: Theme.cornerRadiusLarge
|
||||
color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.4)
|
||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
|
||||
border.width: 1
|
||||
layer.enabled: true
|
||||
|
||||
// Placeholder when no weather - centered in entire widget
|
||||
Column {
|
||||
anchors.centerIn: parent
|
||||
spacing: Theme.spacingS
|
||||
visible: !WeatherService.weather.available || WeatherService.weather.temp === 0
|
||||
|
||||
Text {
|
||||
text: "cloud_off"
|
||||
font.family: Theme.iconFont
|
||||
font.pixelSize: Theme.iconSize + 8
|
||||
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.5)
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
}
|
||||
|
||||
Text {
|
||||
text: "No Weather Data"
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Weather content when available - original Column structure
|
||||
Column {
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingL
|
||||
spacing: Theme.spacingS
|
||||
visible: WeatherService.weather.available && WeatherService.weather.temp !== 0
|
||||
|
||||
// Weather header info
|
||||
Item {
|
||||
width: parent.width
|
||||
height: 60
|
||||
|
||||
Row {
|
||||
anchors.centerIn: parent
|
||||
spacing: Theme.spacingL
|
||||
|
||||
// Weather icon
|
||||
Text {
|
||||
text: WeatherService.getWeatherIcon(WeatherService.weather.wCode)
|
||||
font.family: Theme.iconFont
|
||||
font.pixelSize: Theme.iconSize + 8
|
||||
color: Theme.primary
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Column {
|
||||
spacing: Theme.spacingXS
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
Text {
|
||||
text: (Prefs.useFahrenheit ? WeatherService.weather.tempF : WeatherService.weather.temp) + "°" + (Prefs.useFahrenheit ? "F" : "C")
|
||||
font.pixelSize: Theme.fontSizeXLarge
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Light
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
if (WeatherService.weather.available)
|
||||
Prefs.setTemperatureUnit(!Prefs.useFahrenheit);
|
||||
|
||||
}
|
||||
enabled: WeatherService.weather.available
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Text {
|
||||
text: WeatherService.weather.city || ""
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
|
||||
visible: text.length > 0
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Weather details grid
|
||||
Grid {
|
||||
columns: 2
|
||||
spacing: Theme.spacingM
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
|
||||
Row {
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
Text {
|
||||
text: "humidity_low"
|
||||
font.family: Theme.iconFont
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Text {
|
||||
text: WeatherService.weather.humidity ? WeatherService.weather.humidity + "%" : "--"
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Row {
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
Text {
|
||||
text: "air"
|
||||
font.family: Theme.iconFont
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Text {
|
||||
text: WeatherService.weather.wind || "--"
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Row {
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
Text {
|
||||
text: "wb_twilight"
|
||||
font.family: Theme.iconFont
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Text {
|
||||
text: WeatherService.weather.sunrise || "--"
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Row {
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
Text {
|
||||
text: "bedtime"
|
||||
font.family: Theme.iconFont
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Text {
|
||||
text: WeatherService.weather.sunset || "--"
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
layer.effect: MultiEffect {
|
||||
shadowEnabled: true
|
||||
shadowHorizontalOffset: 0
|
||||
shadowVerticalOffset: 2
|
||||
shadowBlur: 0.5
|
||||
shadowColor: Qt.rgba(0, 0, 0, 0.1)
|
||||
shadowOpacity: 0.1
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,959 +0,0 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import Quickshell.Wayland
|
||||
import Quickshell.Widgets
|
||||
import qs.Common
|
||||
|
||||
PanelWindow {
|
||||
id: clipboardHistory
|
||||
|
||||
property bool isVisible: false
|
||||
property int totalCount: 0
|
||||
// Use the global Theme singleton
|
||||
property var activeTheme: Theme
|
||||
// Confirmation dialog state
|
||||
property bool showClearConfirmation: false
|
||||
// Clipboard entries model
|
||||
property var clipboardEntries: []
|
||||
|
||||
function updateFilteredModel() {
|
||||
filteredClipboardModel.clear();
|
||||
for (let i = 0; i < clipboardModel.count; i++) {
|
||||
const entry = clipboardModel.get(i).entry;
|
||||
if (searchField.text.trim().length === 0) {
|
||||
filteredClipboardModel.append({
|
||||
"entry": entry
|
||||
});
|
||||
} else {
|
||||
const content = getEntryPreview(entry).toLowerCase();
|
||||
if (content.includes(searchField.text.toLowerCase()))
|
||||
filteredClipboardModel.append({
|
||||
"entry": entry
|
||||
});
|
||||
|
||||
}
|
||||
}
|
||||
// Update total count
|
||||
clipboardHistory.totalCount = filteredClipboardModel.count;
|
||||
}
|
||||
|
||||
function toggle() {
|
||||
if (isVisible)
|
||||
hide();
|
||||
else
|
||||
show();
|
||||
}
|
||||
|
||||
function show() {
|
||||
clipboardHistory.isVisible = true;
|
||||
searchField.focus = true;
|
||||
refreshClipboard();
|
||||
console.log("ClipboardHistory: Opening and refreshing");
|
||||
}
|
||||
|
||||
function hide() {
|
||||
clipboardHistory.isVisible = false;
|
||||
searchField.focus = false;
|
||||
searchField.text = "";
|
||||
// Clean up temporary image files
|
||||
cleanupTempFiles();
|
||||
}
|
||||
|
||||
function cleanupTempFiles() {
|
||||
cleanupProcess.command = ["sh", "-c", "rm -f /tmp/clipboard_preview_*.png"];
|
||||
cleanupProcess.running = true;
|
||||
}
|
||||
|
||||
function refreshClipboard() {
|
||||
clipboardProcess.running = true;
|
||||
}
|
||||
|
||||
function copyEntry(entry) {
|
||||
const entryId = entry.split('\t')[0];
|
||||
copyProcess.command = ["sh", "-c", `cliphist decode ${entryId} | wl-copy`];
|
||||
copyProcess.running = true;
|
||||
// Simply hide the clipboard interface
|
||||
console.log("ClipboardHistory: Entry copied, hiding interface");
|
||||
hide();
|
||||
}
|
||||
|
||||
function deleteEntry(entry) {
|
||||
// Use the full entry line for deletion
|
||||
console.log("Deleting entry:", entry);
|
||||
deleteProcess.command = ["sh", "-c", `echo '${entry.replace(/'/g, "'\\''")}' | cliphist delete`];
|
||||
deleteProcess.running = true;
|
||||
}
|
||||
|
||||
function clearAll() {
|
||||
clearProcess.running = true;
|
||||
}
|
||||
|
||||
function getEntryPreview(entry) {
|
||||
// Remove cliphist ID prefix and clean up content
|
||||
let content = entry.replace(/^\s*\d+\s+/, "");
|
||||
// Handle different content types
|
||||
if (content.includes("image/") || content.includes("binary data") || /\.(png|jpg|jpeg|gif|bmp|webp)/i.test(content)) {
|
||||
// Extract dimensions if available
|
||||
const dimensionMatch = content.match(/(\d+)x(\d+)/);
|
||||
if (dimensionMatch)
|
||||
return `Image ${dimensionMatch[1]}×${dimensionMatch[2]}`;
|
||||
|
||||
// Extract file type if available
|
||||
const typeMatch = content.match(/\b(png|jpg|jpeg|gif|bmp|webp)\b/i);
|
||||
if (typeMatch)
|
||||
return `Image (${typeMatch[1].toUpperCase()})`;
|
||||
|
||||
return "Image";
|
||||
}
|
||||
// Truncate long text
|
||||
if (content.length > 100)
|
||||
return content.substring(0, 100) + "...";
|
||||
|
||||
return content;
|
||||
}
|
||||
|
||||
function getEntryType(entry) {
|
||||
// Improved image detection
|
||||
if (entry.includes("image/") || entry.includes("binary data") || /\.(png|jpg|jpeg|gif|bmp|webp)/i.test(entry) || /\b(png|jpg|jpeg|gif|bmp|webp)\b/i.test(entry))
|
||||
return "image";
|
||||
|
||||
if (entry.length > 200)
|
||||
return "long_text";
|
||||
|
||||
return "text";
|
||||
}
|
||||
|
||||
// Window properties
|
||||
color: "transparent"
|
||||
visible: isVisible
|
||||
WlrLayershell.layer: WlrLayershell.Overlay
|
||||
WlrLayershell.exclusiveZone: -1
|
||||
WlrLayershell.keyboardFocus: isVisible ? WlrKeyboardFocus.Exclusive : WlrKeyboardFocus.None
|
||||
|
||||
anchors {
|
||||
top: true
|
||||
left: true
|
||||
right: true
|
||||
bottom: true
|
||||
}
|
||||
|
||||
ListModel {
|
||||
id: clipboardModel
|
||||
}
|
||||
|
||||
ListModel {
|
||||
id: filteredClipboardModel
|
||||
}
|
||||
|
||||
// Background overlay
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
color: Qt.rgba(0, 0, 0, 0.5)
|
||||
opacity: clipboardHistory.isVisible ? 1 : 0
|
||||
visible: clipboardHistory.isVisible
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
enabled: clipboardHistory.isVisible
|
||||
onClicked: clipboardHistory.hide()
|
||||
}
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: activeTheme.mediumDuration
|
||||
easing.type: activeTheme.emphasizedEasing
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Main clipboard container
|
||||
Rectangle {
|
||||
id: clipboardContainer
|
||||
|
||||
width: Math.min(500, parent.width - 200)
|
||||
height: Math.min(500, parent.height - 100)
|
||||
anchors.centerIn: parent
|
||||
color: activeTheme.popupBackground()
|
||||
radius: activeTheme.cornerRadiusXLarge
|
||||
border.color: Qt.rgba(activeTheme.outline.r, activeTheme.outline.g, activeTheme.outline.b, 0.08)
|
||||
border.width: 1
|
||||
opacity: clipboardHistory.isVisible ? 1 : 0
|
||||
scale: clipboardHistory.isVisible ? 1 : 0.9
|
||||
|
||||
// Header section
|
||||
Column {
|
||||
id: headerSection
|
||||
|
||||
anchors.top: parent.top
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.margins: activeTheme.spacingXL
|
||||
spacing: activeTheme.spacingL
|
||||
|
||||
// Title and actions
|
||||
Item {
|
||||
width: parent.width
|
||||
height: 40
|
||||
|
||||
Text {
|
||||
id: titleText
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: "Clipboard History" + (clipboardHistory.totalCount > 0 ? ` (${clipboardHistory.totalCount})` : "")
|
||||
font.pixelSize: activeTheme.fontSizeLarge + 4
|
||||
font.weight: Font.Bold
|
||||
color: activeTheme.surfaceText
|
||||
}
|
||||
|
||||
Row {
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: activeTheme.spacingS
|
||||
|
||||
// Clear all button
|
||||
Rectangle {
|
||||
id: clearAllButton
|
||||
|
||||
width: 40
|
||||
height: 32
|
||||
radius: activeTheme.cornerRadius
|
||||
color: clearArea.containsMouse ? Qt.rgba(activeTheme.primary.r, activeTheme.primary.g, activeTheme.primary.b, 0.12) : "transparent"
|
||||
visible: clipboardHistory.totalCount > 0
|
||||
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
text: "delete_sweep"
|
||||
font.family: activeTheme.iconFont
|
||||
font.pixelSize: activeTheme.iconSize
|
||||
color: clearArea.containsMouse ? activeTheme.primary : activeTheme.surfaceText
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: clearArea
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: showClearConfirmation = true
|
||||
}
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: activeTheme.shortDuration
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Close button
|
||||
Rectangle {
|
||||
width: 40
|
||||
height: 32
|
||||
radius: activeTheme.cornerRadius
|
||||
color: closeArea.containsMouse ? Qt.rgba(activeTheme.primary.r, activeTheme.primary.g, activeTheme.primary.b, 0.12) : "transparent"
|
||||
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
text: "close"
|
||||
font.family: activeTheme.iconFont
|
||||
font.pixelSize: activeTheme.iconSize
|
||||
color: closeArea.containsMouse ? activeTheme.primary : activeTheme.surfaceText
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: closeArea
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: clipboardHistory.hide()
|
||||
}
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: activeTheme.shortDuration
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Search field
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: 48
|
||||
radius: activeTheme.cornerRadiusLarge
|
||||
color: Qt.rgba(activeTheme.surfaceVariant.r, activeTheme.surfaceVariant.g, activeTheme.surfaceVariant.b, activeTheme.getContentBackgroundAlpha() * 0.4)
|
||||
border.color: searchField.focus ? activeTheme.primary : Qt.rgba(activeTheme.outline.r, activeTheme.outline.g, activeTheme.outline.b, 0.08)
|
||||
border.width: searchField.focus ? 2 : 1
|
||||
|
||||
Row {
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: activeTheme.spacingL
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: activeTheme.spacingM
|
||||
|
||||
Text {
|
||||
text: "search"
|
||||
font.family: activeTheme.iconFont
|
||||
font.pixelSize: activeTheme.iconSize
|
||||
color: searchField.focus ? activeTheme.primary : Qt.rgba(activeTheme.surfaceText.r, activeTheme.surfaceText.g, activeTheme.surfaceText.b, 0.6)
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
TextInput {
|
||||
id: searchField
|
||||
|
||||
width: parent.parent.width - 80
|
||||
height: parent.parent.height
|
||||
font.pixelSize: activeTheme.fontSizeLarge
|
||||
color: activeTheme.surfaceText
|
||||
verticalAlignment: TextInput.AlignVCenter
|
||||
selectByMouse: true
|
||||
onTextChanged: updateFilteredModel()
|
||||
Keys.onPressed: (event) => {
|
||||
if (event.key === Qt.Key_Escape)
|
||||
clipboardHistory.hide();
|
||||
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.IBeamCursor
|
||||
acceptedButtons: Qt.NoButton
|
||||
}
|
||||
|
||||
// Placeholder text
|
||||
Text {
|
||||
text: "Search clipboard entries..."
|
||||
font: searchField.font
|
||||
color: Qt.rgba(activeTheme.surfaceText.r, activeTheme.surfaceText.g, activeTheme.surfaceText.b, 0.6)
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
visible: searchField.text.length === 0 && !searchField.focus
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Behavior on border.color {
|
||||
ColorAnimation {
|
||||
duration: activeTheme.shortDuration
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Clipboard entries
|
||||
Rectangle {
|
||||
anchors.top: headerSection.bottom
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.margins: activeTheme.spacingXL
|
||||
anchors.topMargin: activeTheme.spacingL
|
||||
color: "transparent"
|
||||
|
||||
ScrollView {
|
||||
anchors.fill: parent
|
||||
clip: true
|
||||
// Improve scrolling responsiveness
|
||||
ScrollBar.vertical.policy: ScrollBar.AsNeeded
|
||||
ScrollBar.vertical.width: 12
|
||||
ScrollBar.vertical.minimumSize: 0.1 // Minimum scrollbar handle size
|
||||
// Enable faster scrolling
|
||||
wheelEnabled: true
|
||||
|
||||
ListView {
|
||||
id: clipboardList
|
||||
|
||||
// Make mouse wheel scrolling more responsive
|
||||
property real wheelStepSize: 60
|
||||
|
||||
model: filteredClipboardModel
|
||||
spacing: activeTheme.spacingS
|
||||
// Improve scrolling performance
|
||||
cacheBuffer: 100
|
||||
boundsBehavior: Flickable.StopAtBounds
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
acceptedButtons: Qt.NoButton
|
||||
onWheel: (wheel) => {
|
||||
var delta = wheel.angleDelta.y;
|
||||
var steps = delta / 120; // Standard wheel step
|
||||
clipboardList.contentY -= steps * clipboardList.wheelStepSize;
|
||||
// Ensure we stay within bounds
|
||||
if (clipboardList.contentY < 0)
|
||||
clipboardList.contentY = 0;
|
||||
else if (clipboardList.contentY > clipboardList.contentHeight - clipboardList.height)
|
||||
clipboardList.contentY = Math.max(0, clipboardList.contentHeight - clipboardList.height);
|
||||
}
|
||||
}
|
||||
|
||||
delegate: Rectangle {
|
||||
property string entryType: getEntryType(model.entry)
|
||||
property string entryPreview: getEntryPreview(model.entry)
|
||||
property int entryIndex: index + 1
|
||||
|
||||
width: clipboardList.width - 16 // Account for scrollbar space
|
||||
height: Math.max(60, contentColumn.implicitHeight + activeTheme.spacingM * 2)
|
||||
radius: activeTheme.cornerRadius
|
||||
color: entryArea.containsMouse ? Qt.rgba(activeTheme.primary.r, activeTheme.primary.g, activeTheme.primary.b, 0.08) : Qt.rgba(activeTheme.surfaceVariant.r, activeTheme.surfaceVariant.g, activeTheme.surfaceVariant.b, 0.05)
|
||||
border.color: Qt.rgba(activeTheme.outline.r, activeTheme.outline.g, activeTheme.outline.b, 0.1)
|
||||
border.width: 1
|
||||
|
||||
Row {
|
||||
anchors.fill: parent
|
||||
anchors.margins: activeTheme.spacingM
|
||||
spacing: activeTheme.spacingL
|
||||
|
||||
// Index number
|
||||
Rectangle {
|
||||
width: 24
|
||||
height: 24
|
||||
radius: 12
|
||||
color: Qt.rgba(activeTheme.primary.r, activeTheme.primary.g, activeTheme.primary.b, 0.2)
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
text: entryIndex.toString()
|
||||
font.pixelSize: activeTheme.fontSizeSmall
|
||||
font.weight: Font.Bold
|
||||
color: activeTheme.primary
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Entry content
|
||||
Row {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
width: parent.width - 80 // Adjusted for index number and delete button
|
||||
spacing: activeTheme.spacingM
|
||||
|
||||
// Image preview - actual image display for images
|
||||
Rectangle {
|
||||
property string entryId: model.entry ? model.entry.split('\t')[0] : ""
|
||||
property string tempImagePath: "/tmp/clipboard_preview_" + entryId + ".png"
|
||||
|
||||
width: entryType === "image" ? 48 : 0
|
||||
height: entryType === "image" ? 36 : 0
|
||||
radius: activeTheme.cornerRadiusSmall
|
||||
color: Qt.rgba(activeTheme.surfaceVariant.r, activeTheme.surfaceVariant.g, activeTheme.surfaceVariant.b, 0.1)
|
||||
border.color: Qt.rgba(activeTheme.outline.r, activeTheme.outline.g, activeTheme.outline.b, 0.2)
|
||||
border.width: 1
|
||||
visible: entryType === "image"
|
||||
clip: true
|
||||
|
||||
// Actual image preview using cliphist decode
|
||||
Image {
|
||||
id: imagePreview
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.margins: 1
|
||||
fillMode: Image.PreserveAspectCrop
|
||||
asynchronous: true
|
||||
cache: false
|
||||
source: parent.entryType === "image" && parent.entryId ? "file://" + parent.tempImagePath : ""
|
||||
Component.onCompleted: {
|
||||
if (parent.entryType === "image" && parent.entryId) {
|
||||
// Simple approach: use shell redirection to write to file
|
||||
imageDecodeProcess.entryId = parent.entryId;
|
||||
imageDecodeProcess.tempPath = parent.tempImagePath;
|
||||
imageDecodeProcess.imagePreview = imagePreview;
|
||||
imageDecodeProcess.command = ["sh", "-c", `cliphist decode ${parent.entryId} > "${parent.tempImagePath}" 2>/dev/null`];
|
||||
imageDecodeProcess.running = true;
|
||||
}
|
||||
}
|
||||
onStatusChanged: {
|
||||
if (status === Image.Error)
|
||||
console.warn("Failed to load clipboard image from:", source);
|
||||
|
||||
}
|
||||
|
||||
// Fallback icon when image fails to load or is loading
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
text: imagePreview.status === Image.Loading ? "hourglass_empty" : imagePreview.status === Image.Error ? "broken_image" : "photo"
|
||||
font.family: activeTheme.iconFont
|
||||
font.pixelSize: imagePreview.status === Image.Loading ? 14 : 18
|
||||
color: imagePreview.status === Image.Error ? activeTheme.error : activeTheme.primary
|
||||
visible: imagePreview.status !== Image.Ready
|
||||
|
||||
SequentialAnimation on opacity {
|
||||
running: imagePreview.status === Image.Loading
|
||||
loops: Animation.Infinite
|
||||
|
||||
NumberAnimation {
|
||||
to: 0.3
|
||||
duration: 500
|
||||
}
|
||||
|
||||
NumberAnimation {
|
||||
to: 1
|
||||
duration: 500
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Column {
|
||||
id: contentColumn
|
||||
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
width: parent.width - (entryType === "image" ? 60 : 0)
|
||||
spacing: activeTheme.spacingXS
|
||||
|
||||
Text {
|
||||
text: {
|
||||
switch (entryType) {
|
||||
case "image":
|
||||
return "Image • " + entryPreview;
|
||||
case "long_text":
|
||||
return "Long Text";
|
||||
default:
|
||||
return "Text";
|
||||
}
|
||||
}
|
||||
font.pixelSize: activeTheme.fontSizeSmall
|
||||
color: activeTheme.primary
|
||||
font.weight: Font.Medium
|
||||
width: parent.width
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
|
||||
Text {
|
||||
text: entryPreview
|
||||
font.pixelSize: activeTheme.fontSizeMedium
|
||||
color: activeTheme.surfaceText
|
||||
width: parent.width
|
||||
wrapMode: Text.WordWrap
|
||||
maximumLineCount: entryType === "long_text" ? 3 : 1
|
||||
elide: Text.ElideRight
|
||||
visible: true // Show preview for all entry types including images
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Actions - Single centered delete button
|
||||
Rectangle {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
width: 32
|
||||
height: 32
|
||||
radius: activeTheme.cornerRadius
|
||||
color: deleteArea.containsMouse ? Qt.rgba(activeTheme.primary.r, activeTheme.primary.g, activeTheme.primary.b, 0.12) : "transparent"
|
||||
z: 100 // Ensure it's above other elements
|
||||
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
text: "delete"
|
||||
font.family: activeTheme.iconFont
|
||||
font.pixelSize: activeTheme.iconSize - 4
|
||||
color: deleteArea.containsMouse ? activeTheme.primary : activeTheme.surfaceText
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: deleteArea
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
z: 101 // Ensure click area is above everything
|
||||
onClicked: (mouse) => {
|
||||
console.log("Delete clicked for entry:", model.entry);
|
||||
deleteEntry(model.entry);
|
||||
// Prevent the click from propagating to the entry area
|
||||
mouse.accepted = true;
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: activeTheme.shortDuration
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: entryArea
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.rightMargin: 40 // Leave space for delete button
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: copyEntry(model.entry)
|
||||
}
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: activeTheme.shortDuration
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Empty state
|
||||
Column {
|
||||
anchors.centerIn: parent
|
||||
spacing: activeTheme.spacingL
|
||||
visible: clipboardHistory.totalCount === 0
|
||||
|
||||
Text {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
text: "content_paste_off"
|
||||
font.family: activeTheme.iconFont
|
||||
font.pixelSize: activeTheme.iconSizeLarge + 16
|
||||
color: Qt.rgba(activeTheme.surfaceText.r, activeTheme.surfaceText.g, activeTheme.surfaceText.b, 0.3)
|
||||
}
|
||||
|
||||
Text {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
text: "No clipboard history"
|
||||
font.pixelSize: activeTheme.fontSizeLarge
|
||||
color: Qt.rgba(activeTheme.surfaceText.r, activeTheme.surfaceText.g, activeTheme.surfaceText.b, 0.6)
|
||||
}
|
||||
|
||||
Text {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
text: "Copy something to see it here"
|
||||
font.pixelSize: activeTheme.fontSizeMedium
|
||||
color: Qt.rgba(activeTheme.surfaceText.r, activeTheme.surfaceText.g, activeTheme.surfaceText.b, 0.4)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Clear All Confirmation Dialog
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
color: Qt.rgba(0, 0, 0, 0.4)
|
||||
visible: showClearConfirmation
|
||||
z: 999
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
onClicked: clipboardHistory.showClearConfirmation = false
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.centerIn: parent
|
||||
width: 350
|
||||
height: 200 // Increased height for better spacing
|
||||
radius: activeTheme.cornerRadiusLarge
|
||||
color: activeTheme.popupBackground()
|
||||
border.color: Qt.rgba(activeTheme.outline.r, activeTheme.outline.g, activeTheme.outline.b, 0.08)
|
||||
border.width: 1
|
||||
visible: showClearConfirmation
|
||||
z: 1000
|
||||
|
||||
Column {
|
||||
anchors.centerIn: parent
|
||||
spacing: activeTheme.spacingL
|
||||
width: parent.width - 40
|
||||
|
||||
// Add top padding
|
||||
Item {
|
||||
width: 1
|
||||
height: activeTheme.spacingM
|
||||
}
|
||||
|
||||
Text {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
text: "warning"
|
||||
font.family: activeTheme.iconFont
|
||||
font.pixelSize: activeTheme.iconSizeLarge
|
||||
color: activeTheme.error
|
||||
}
|
||||
|
||||
Text {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
text: "Clear All Clipboard History?"
|
||||
font.pixelSize: activeTheme.fontSizeLarge
|
||||
font.weight: Font.Bold
|
||||
color: activeTheme.surfaceText
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
}
|
||||
|
||||
Text {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
text: "This action cannot be undone. All clipboard entries will be permanently deleted."
|
||||
font.pixelSize: activeTheme.fontSizeMedium
|
||||
color: Qt.rgba(activeTheme.surfaceText.r, activeTheme.surfaceText.g, activeTheme.surfaceText.b, 0.7)
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
wrapMode: Text.WordWrap
|
||||
width: parent.width
|
||||
}
|
||||
|
||||
Row {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
spacing: activeTheme.spacingM
|
||||
|
||||
// Cancel button
|
||||
Rectangle {
|
||||
width: 100
|
||||
height: 40
|
||||
radius: activeTheme.cornerRadius
|
||||
color: cancelArea.containsMouse ? Qt.rgba(activeTheme.primary.r, activeTheme.primary.g, activeTheme.primary.b, 0.08) : "transparent"
|
||||
border.color: activeTheme.primary
|
||||
border.width: 1
|
||||
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
text: "Cancel"
|
||||
font.pixelSize: activeTheme.fontSizeMedium
|
||||
font.weight: Font.Medium
|
||||
color: activeTheme.primary
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: cancelArea
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: clipboardHistory.showClearConfirmation = false
|
||||
}
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: activeTheme.shortDuration
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Clear button
|
||||
Rectangle {
|
||||
width: 100
|
||||
height: 40
|
||||
radius: activeTheme.cornerRadius
|
||||
color: confirmArea.containsMouse ? Qt.rgba(activeTheme.primary.r, activeTheme.primary.g, activeTheme.primary.b, 0.8) : activeTheme.primary
|
||||
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
text: "Clear All"
|
||||
font.pixelSize: activeTheme.fontSizeMedium
|
||||
font.weight: Font.Medium
|
||||
color: activeTheme.surface
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: confirmArea
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
clipboardHistory.showClearConfirmation = false;
|
||||
clearAll();
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: activeTheme.shortDuration
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Add some bottom padding
|
||||
Item {
|
||||
width: 1
|
||||
height: activeTheme.spacingM
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: activeTheme.mediumDuration
|
||||
easing.type: activeTheme.emphasizedEasing
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Behavior on scale {
|
||||
NumberAnimation {
|
||||
duration: activeTheme.mediumDuration
|
||||
easing.type: activeTheme.emphasizedEasing
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Clipboard processes
|
||||
Process {
|
||||
id: cleanupProcess
|
||||
|
||||
running: false
|
||||
onExited: (exitCode) => {
|
||||
if (exitCode === 0)
|
||||
console.log("Temporary image files cleaned up");
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
// Force the Image component to reload
|
||||
|
||||
id: imageDecodeProcess
|
||||
|
||||
property string entryId: ""
|
||||
property string tempPath: ""
|
||||
property var imagePreview: null
|
||||
|
||||
running: false
|
||||
onExited: (exitCode) => {
|
||||
if (exitCode === 0 && imagePreview && tempPath)
|
||||
Qt.callLater(function() {
|
||||
imagePreview.source = "";
|
||||
imagePreview.source = "file://" + tempPath;
|
||||
});
|
||||
|
||||
}
|
||||
onStarted: {
|
||||
console.log("Starting image decode for entry:", entryId, "to path:", tempPath);
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: clipboardProcess
|
||||
|
||||
command: ["cliphist", "list"]
|
||||
running: false
|
||||
onStarted: {
|
||||
clipboardHistory.clipboardEntries = [];
|
||||
clipboardModel.clear();
|
||||
console.log("ClipboardHistory: Starting cliphist process...");
|
||||
}
|
||||
onExited: (exitCode) => {
|
||||
if (exitCode === 0)
|
||||
updateFilteredModel();
|
||||
else
|
||||
console.warn("ClipboardHistory: Failed to load clipboard history");
|
||||
}
|
||||
// Handle keyboard shortcuts
|
||||
Keys.onPressed: (event) => {
|
||||
if (event.key === Qt.Key_Escape)
|
||||
clipboardHistory.hide();
|
||||
|
||||
}
|
||||
Component.onCompleted: {
|
||||
focus = true;
|
||||
}
|
||||
|
||||
stdout: SplitParser {
|
||||
splitMarker: "\n"
|
||||
onRead: (line) => {
|
||||
if (line.trim()) {
|
||||
clipboardHistory.clipboardEntries.push(line);
|
||||
clipboardModel.append({
|
||||
"entry": line
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Process {
|
||||
id: copyProcess
|
||||
|
||||
running: false
|
||||
onExited: (exitCode) => {
|
||||
if (exitCode !== 0)
|
||||
console.warn("ClipboardHistory: Failed to copy entry");
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: deleteProcess
|
||||
|
||||
running: false
|
||||
onExited: (exitCode) => {
|
||||
if (exitCode === 0)
|
||||
refreshClipboard();
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: clearProcess
|
||||
|
||||
command: ["cliphist", "wipe"]
|
||||
running: false
|
||||
onExited: (exitCode) => {
|
||||
if (exitCode === 0) {
|
||||
clipboardHistory.clipboardEntries = [];
|
||||
clipboardModel.clear();
|
||||
updateFilteredModel();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
IpcHandler {
|
||||
function open() {
|
||||
console.log("ClipboardHistory: IPC open() called");
|
||||
clipboardHistory.show();
|
||||
return "CLIPBOARD_OPEN_SUCCESS";
|
||||
}
|
||||
|
||||
function close() {
|
||||
console.log("ClipboardHistory: IPC close() called");
|
||||
clipboardHistory.hide();
|
||||
return "CLIPBOARD_CLOSE_SUCCESS";
|
||||
}
|
||||
|
||||
function toggle() {
|
||||
console.log("ClipboardHistory: IPC toggle() called");
|
||||
clipboardHistory.toggle();
|
||||
return "CLIPBOARD_TOGGLE_SUCCESS";
|
||||
}
|
||||
|
||||
target: "clipboard"
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,731 +0,0 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import Quickshell.Services.Pipewire
|
||||
import Quickshell.Widgets
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
|
||||
Item {
|
||||
id: audioTab
|
||||
|
||||
property int audioSubTab: 0 // 0: Output, 1: Input
|
||||
readonly property real volumeLevel: (AudioService.sink && AudioService.sink.audio && AudioService.sink.audio.volume * 100) || 0
|
||||
readonly property real micLevel: (AudioService.source && AudioService.source.audio && AudioService.source.audio.volume * 100) || 0
|
||||
readonly property bool volumeMuted: (AudioService.sink && AudioService.sink.audio && AudioService.sink.audio.muted) || false
|
||||
readonly property bool micMuted: (AudioService.source && AudioService.source.audio && AudioService.source.audio.muted) || false
|
||||
readonly property string currentSinkDisplayName: AudioService.sink ? AudioService.displayName(AudioService.sink) : ""
|
||||
readonly property string currentSourceDisplayName: AudioService.source ? AudioService.displayName(AudioService.source) : ""
|
||||
|
||||
Column {
|
||||
anchors.fill: parent
|
||||
spacing: Theme.spacingM
|
||||
|
||||
// Audio Sub-tabs
|
||||
Row {
|
||||
width: parent.width
|
||||
height: 40
|
||||
spacing: 2
|
||||
|
||||
Rectangle {
|
||||
width: parent.width / 2 - 1
|
||||
height: parent.height
|
||||
radius: Theme.cornerRadius
|
||||
color: audioTab.audioSubTab === 0 ? Theme.primary : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.3)
|
||||
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
text: "Output"
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: audioTab.audioSubTab === 0 ? Theme.primaryText : Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: audioTab.audioSubTab = 0
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: parent.width / 2 - 1
|
||||
height: parent.height
|
||||
radius: Theme.cornerRadius
|
||||
color: audioTab.audioSubTab === 1 ? Theme.primary : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.3)
|
||||
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
text: "Input"
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: audioTab.audioSubTab === 1 ? Theme.primaryText : Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: audioTab.audioSubTab = 1
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Output Tab Content
|
||||
ScrollView {
|
||||
width: parent.width
|
||||
height: parent.height - 48
|
||||
visible: audioTab.audioSubTab === 0
|
||||
clip: true
|
||||
|
||||
Column {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingL
|
||||
|
||||
// Volume Control
|
||||
Column {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingM
|
||||
|
||||
Text {
|
||||
text: "Volume"
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
}
|
||||
|
||||
Row {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingM
|
||||
|
||||
Text {
|
||||
text: audioTab.volumeMuted ? "volume_off" : "volume_down"
|
||||
font.family: Theme.iconFont
|
||||
font.pixelSize: Theme.iconSize
|
||||
color: audioTab.volumeMuted ? Theme.error : Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
if (AudioService.sink && AudioService.sink.audio)
|
||||
AudioService.sink.audio.muted = !AudioService.sink.audio.muted;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Item {
|
||||
id: volumeSliderContainer
|
||||
|
||||
width: parent.width - 80
|
||||
height: 32
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
Rectangle {
|
||||
id: volumeSliderTrack
|
||||
|
||||
width: parent.width
|
||||
height: 8
|
||||
radius: 4
|
||||
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.3)
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
Rectangle {
|
||||
id: volumeSliderFill
|
||||
|
||||
width: parent.width * (audioTab.volumeLevel / 100)
|
||||
height: parent.height
|
||||
radius: parent.radius
|
||||
color: Theme.primary
|
||||
|
||||
Behavior on width {
|
||||
NumberAnimation {
|
||||
duration: 100
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Draggable handle
|
||||
Rectangle {
|
||||
id: volumeHandle
|
||||
|
||||
width: 18
|
||||
height: 18
|
||||
radius: 9
|
||||
color: Theme.primary
|
||||
border.color: Qt.lighter(Theme.primary, 1.3)
|
||||
border.width: 2
|
||||
x: Math.max(0, Math.min(parent.width - width, volumeSliderFill.width - width / 2))
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
scale: volumeMouseArea.containsMouse || volumeMouseArea.pressed ? 1.2 : 1
|
||||
|
||||
Behavior on scale {
|
||||
NumberAnimation {
|
||||
duration: 150
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: volumeMouseArea
|
||||
|
||||
property bool isDragging: false
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
preventStealing: true
|
||||
onPressed: (mouse) => {
|
||||
isDragging = true;
|
||||
let ratio = Math.max(0, Math.min(1, mouse.x / volumeSliderTrack.width));
|
||||
let newVolume = Math.round(ratio * 100);
|
||||
if (AudioService.sink && AudioService.sink.audio) {
|
||||
AudioService.sink.audio.muted = false;
|
||||
AudioService.sink.audio.volume = newVolume / 100;
|
||||
}
|
||||
}
|
||||
onReleased: {
|
||||
isDragging = false;
|
||||
}
|
||||
onPositionChanged: (mouse) => {
|
||||
if (pressed && isDragging) {
|
||||
let ratio = Math.max(0, Math.min(1, mouse.x / volumeSliderTrack.width));
|
||||
let newVolume = Math.round(ratio * 100);
|
||||
if (AudioService.sink && AudioService.sink.audio) {
|
||||
AudioService.sink.audio.muted = false;
|
||||
AudioService.sink.audio.volume = newVolume / 100;
|
||||
}
|
||||
}
|
||||
}
|
||||
onClicked: (mouse) => {
|
||||
let ratio = Math.max(0, Math.min(1, mouse.x / volumeSliderTrack.width));
|
||||
let newVolume = Math.round(ratio * 100);
|
||||
if (AudioService.sink && AudioService.sink.audio) {
|
||||
AudioService.sink.audio.muted = false;
|
||||
AudioService.sink.audio.volume = newVolume / 100;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Global mouse area for drag tracking
|
||||
MouseArea {
|
||||
id: volumeGlobalMouseArea
|
||||
|
||||
anchors.fill: parent.parent.parent.parent.parent // Fill the entire control center
|
||||
enabled: volumeMouseArea.isDragging
|
||||
visible: false
|
||||
preventStealing: true
|
||||
onPositionChanged: (mouse) => {
|
||||
if (volumeMouseArea.isDragging) {
|
||||
let globalPos = mapToItem(volumeSliderTrack, mouse.x, mouse.y);
|
||||
let ratio = Math.max(0, Math.min(1, globalPos.x / volumeSliderTrack.width));
|
||||
let newVolume = Math.round(ratio * 100);
|
||||
if (AudioService.sink && AudioService.sink.audio) {
|
||||
AudioService.sink.audio.muted = false;
|
||||
AudioService.sink.audio.volume = newVolume / 100;
|
||||
}
|
||||
}
|
||||
}
|
||||
onReleased: {
|
||||
volumeMouseArea.isDragging = false;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Text {
|
||||
text: "volume_up"
|
||||
font.family: Theme.iconFont
|
||||
font.pixelSize: Theme.iconSize
|
||||
color: Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Output Devices
|
||||
Column {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingM
|
||||
|
||||
Text {
|
||||
text: "Output Device"
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
}
|
||||
|
||||
// Current device indicator
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: 35
|
||||
radius: Theme.cornerRadius
|
||||
color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12)
|
||||
border.color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.3)
|
||||
border.width: 1
|
||||
visible: AudioService.sink !== null
|
||||
|
||||
Row {
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Theme.spacingM
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Theme.spacingS
|
||||
|
||||
Text {
|
||||
text: "check_circle"
|
||||
font.family: Theme.iconFont
|
||||
font.pixelSize: Theme.iconSize - 4
|
||||
color: Theme.primary
|
||||
}
|
||||
|
||||
Text {
|
||||
text: "Current: " + (audioTab.currentSinkDisplayName || "None")
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.primary
|
||||
font.weight: Font.Medium
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Real audio devices
|
||||
Repeater {
|
||||
model: {
|
||||
if (!Pipewire.ready || !Pipewire.nodes || !Pipewire.nodes.values) return []
|
||||
let sinks = []
|
||||
for (let i = 0; i < Pipewire.nodes.values.length; i++) {
|
||||
let node = Pipewire.nodes.values[i]
|
||||
if (!node || node.isStream) continue
|
||||
if ((node.type & PwNodeType.AudioSink) === PwNodeType.AudioSink) {
|
||||
sinks.push(node)
|
||||
}
|
||||
}
|
||||
return sinks
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: 50
|
||||
radius: Theme.cornerRadius
|
||||
color: deviceArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : (modelData === AudioService.sink ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08))
|
||||
border.color: modelData === AudioService.sink ? Theme.primary : "transparent"
|
||||
border.width: 1
|
||||
|
||||
Row {
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Theme.spacingM
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Theme.spacingM
|
||||
|
||||
Text {
|
||||
text: {
|
||||
if (modelData.name.includes("bluez"))
|
||||
return "headset";
|
||||
else if (modelData.name.includes("hdmi"))
|
||||
return "tv";
|
||||
else if (modelData.name.includes("usb"))
|
||||
return "headset";
|
||||
else
|
||||
return "speaker";
|
||||
}
|
||||
font.family: Theme.iconFont
|
||||
font.pixelSize: Theme.iconSize
|
||||
color: modelData === AudioService.sink ? Theme.primary : Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Column {
|
||||
spacing: 2
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
Text {
|
||||
text: AudioService.displayName(modelData)
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: modelData === AudioService.sink ? Theme.primary : Theme.surfaceText
|
||||
font.weight: modelData === AudioService.sink ? Font.Medium : Font.Normal
|
||||
}
|
||||
|
||||
Text {
|
||||
text: {
|
||||
if (AudioService.subtitle(modelData.name) && AudioService.subtitle(modelData.name) !== "")
|
||||
return AudioService.subtitle(modelData.name) + (modelData === AudioService.sink ? " • Selected" : "");
|
||||
else
|
||||
return modelData === AudioService.sink ? "Selected" : "";
|
||||
}
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
|
||||
visible: text !== ""
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: deviceArea
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
if (modelData)
|
||||
Pipewire.preferredDefaultAudioSink = modelData;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Input Tab Content
|
||||
ScrollView {
|
||||
width: parent.width
|
||||
height: parent.height - 48
|
||||
visible: audioTab.audioSubTab === 1
|
||||
clip: true
|
||||
|
||||
Column {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingL
|
||||
|
||||
// Microphone Level Control
|
||||
Column {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingM
|
||||
|
||||
Text {
|
||||
text: "Microphone Level"
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
}
|
||||
|
||||
Row {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingM
|
||||
|
||||
Text {
|
||||
text: audioTab.micMuted ? "mic_off" : "mic"
|
||||
font.family: Theme.iconFont
|
||||
font.pixelSize: Theme.iconSize
|
||||
color: audioTab.micMuted ? Theme.error : Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
if (AudioService.source && AudioService.source.audio)
|
||||
AudioService.source.audio.muted = !AudioService.source.audio.muted;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Item {
|
||||
id: micSliderContainer
|
||||
|
||||
width: parent.width - 80
|
||||
height: 32
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
Rectangle {
|
||||
id: micSliderTrack
|
||||
|
||||
width: parent.width
|
||||
height: 8
|
||||
radius: 4
|
||||
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.3)
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
Rectangle {
|
||||
id: micSliderFill
|
||||
|
||||
width: parent.width * (audioTab.micLevel / 100)
|
||||
height: parent.height
|
||||
radius: parent.radius
|
||||
color: Theme.primary
|
||||
|
||||
Behavior on width {
|
||||
NumberAnimation {
|
||||
duration: 100
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Draggable handle
|
||||
Rectangle {
|
||||
id: micHandle
|
||||
|
||||
width: 18
|
||||
height: 18
|
||||
radius: 9
|
||||
color: Theme.primary
|
||||
border.color: Qt.lighter(Theme.primary, 1.3)
|
||||
border.width: 2
|
||||
x: Math.max(0, Math.min(parent.width - width, micSliderFill.width - width / 2))
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
scale: micMouseArea.containsMouse || micMouseArea.pressed ? 1.2 : 1
|
||||
|
||||
Behavior on scale {
|
||||
NumberAnimation {
|
||||
duration: 150
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: micMouseArea
|
||||
|
||||
property bool isDragging: false
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
preventStealing: true
|
||||
onPressed: (mouse) => {
|
||||
isDragging = true;
|
||||
let ratio = Math.max(0, Math.min(1, mouse.x / micSliderTrack.width));
|
||||
let newMicLevel = Math.round(ratio * 100);
|
||||
if (AudioService.source && AudioService.source.audio) {
|
||||
AudioService.source.audio.muted = false;
|
||||
AudioService.source.audio.volume = newMicLevel / 100;
|
||||
}
|
||||
}
|
||||
onReleased: {
|
||||
isDragging = false;
|
||||
}
|
||||
onPositionChanged: (mouse) => {
|
||||
if (pressed && isDragging) {
|
||||
let ratio = Math.max(0, Math.min(1, mouse.x / micSliderTrack.width));
|
||||
let newMicLevel = Math.round(ratio * 100);
|
||||
if (AudioService.source && AudioService.source.audio) {
|
||||
AudioService.source.audio.muted = false;
|
||||
AudioService.source.audio.volume = newMicLevel / 100;
|
||||
}
|
||||
}
|
||||
}
|
||||
onClicked: (mouse) => {
|
||||
let ratio = Math.max(0, Math.min(1, mouse.x / micSliderTrack.width));
|
||||
let newMicLevel = Math.round(ratio * 100);
|
||||
if (AudioService.source && AudioService.source.audio) {
|
||||
AudioService.source.audio.muted = false;
|
||||
AudioService.source.audio.volume = newMicLevel / 100;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Global mouse area for drag tracking
|
||||
MouseArea {
|
||||
id: micGlobalMouseArea
|
||||
|
||||
anchors.fill: parent.parent.parent.parent.parent // Fill the entire control center
|
||||
enabled: micMouseArea.isDragging
|
||||
visible: false
|
||||
preventStealing: true
|
||||
onPositionChanged: (mouse) => {
|
||||
if (micMouseArea.isDragging) {
|
||||
let globalPos = mapToItem(micSliderTrack, mouse.x, mouse.y);
|
||||
let ratio = Math.max(0, Math.min(1, globalPos.x / micSliderTrack.width));
|
||||
let newMicLevel = Math.round(ratio * 100);
|
||||
if (AudioService.source && AudioService.source.audio) {
|
||||
AudioService.source.audio.muted = false;
|
||||
AudioService.source.audio.volume = newMicLevel / 100;
|
||||
}
|
||||
}
|
||||
}
|
||||
onReleased: {
|
||||
micMouseArea.isDragging = false;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Text {
|
||||
text: "mic"
|
||||
font.family: Theme.iconFont
|
||||
font.pixelSize: Theme.iconSize
|
||||
color: Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Input Devices
|
||||
Column {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingM
|
||||
|
||||
Text {
|
||||
text: "Input Device"
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
}
|
||||
|
||||
// Current device indicator
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: 35
|
||||
radius: Theme.cornerRadius
|
||||
color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12)
|
||||
border.color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.3)
|
||||
border.width: 1
|
||||
visible: AudioService.source !== null
|
||||
|
||||
Row {
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Theme.spacingM
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Theme.spacingS
|
||||
|
||||
Text {
|
||||
text: "check_circle"
|
||||
font.family: Theme.iconFont
|
||||
font.pixelSize: Theme.iconSize - 4
|
||||
color: Theme.primary
|
||||
}
|
||||
|
||||
Text {
|
||||
text: "Current: " + (audioTab.currentSourceDisplayName || "None")
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.primary
|
||||
font.weight: Font.Medium
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Real audio input devices
|
||||
Repeater {
|
||||
model: {
|
||||
if (!Pipewire.ready || !Pipewire.nodes || !Pipewire.nodes.values) return []
|
||||
let sources = []
|
||||
for (let i = 0; i < Pipewire.nodes.values.length; i++) {
|
||||
let node = Pipewire.nodes.values[i]
|
||||
if (!node || node.isStream) continue
|
||||
if ((node.type & PwNodeType.AudioSource) === PwNodeType.AudioSource && !node.name.includes(".monitor")) {
|
||||
sources.push(node)
|
||||
}
|
||||
}
|
||||
return sources
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: 50
|
||||
radius: Theme.cornerRadius
|
||||
color: sourceArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : (modelData === AudioService.source ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08))
|
||||
border.color: modelData === AudioService.source ? Theme.primary : "transparent"
|
||||
border.width: 1
|
||||
|
||||
Row {
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Theme.spacingM
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Theme.spacingM
|
||||
|
||||
Text {
|
||||
text: {
|
||||
if (modelData.name.includes("bluez"))
|
||||
return "headset_mic";
|
||||
else if (modelData.name.includes("usb"))
|
||||
return "headset_mic";
|
||||
else
|
||||
return "mic";
|
||||
}
|
||||
font.family: Theme.iconFont
|
||||
font.pixelSize: Theme.iconSize
|
||||
color: modelData === AudioService.source ? Theme.primary : Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Column {
|
||||
spacing: 2
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
Text {
|
||||
text: AudioService.displayName(modelData)
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: modelData === AudioService.source ? Theme.primary : Theme.surfaceText
|
||||
font.weight: modelData === AudioService.source ? Font.Medium : Font.Normal
|
||||
}
|
||||
|
||||
Text {
|
||||
text: {
|
||||
if (AudioService.subtitle(modelData.name) && AudioService.subtitle(modelData.name) !== "")
|
||||
return AudioService.subtitle(modelData.name) + (modelData === AudioService.source ? " • Selected" : "");
|
||||
else
|
||||
return modelData === AudioService.source ? "Selected" : "";
|
||||
}
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
|
||||
visible: text !== ""
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: sourceArea
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
if (modelData)
|
||||
Pipewire.preferredDefaultAudioSource = modelData;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,796 +0,0 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import Quickshell
|
||||
import Quickshell.Bluetooth
|
||||
import Quickshell.Io
|
||||
import Quickshell.Widgets
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
|
||||
Item {
|
||||
id: bluetoothTab
|
||||
|
||||
ScrollView {
|
||||
anchors.fill: parent
|
||||
clip: true
|
||||
ScrollBar.vertical.policy: ScrollBar.AsNeeded
|
||||
ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
|
||||
|
||||
Column {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingL
|
||||
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: 60
|
||||
radius: Theme.cornerRadius
|
||||
color: bluetoothToggle.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : (BluetoothService.adapter && BluetoothService.adapter.enabled ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.16) : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.12))
|
||||
border.color: BluetoothService.adapter && BluetoothService.adapter.enabled ? Theme.primary : "transparent"
|
||||
border.width: 2
|
||||
|
||||
Row {
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Theme.spacingL
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Theme.spacingM
|
||||
|
||||
Text {
|
||||
text: "bluetooth"
|
||||
font.family: Theme.iconFont
|
||||
font.pixelSize: Theme.iconSizeLarge
|
||||
color: BluetoothService.adapter && BluetoothService.adapter.enabled ? Theme.primary : Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Column {
|
||||
spacing: 2
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
Text {
|
||||
text: "Bluetooth"
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
color: BluetoothService.adapter && BluetoothService.adapter.enabled ? Theme.primary : Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
}
|
||||
|
||||
Text {
|
||||
text: BluetoothService.adapter && BluetoothService.adapter.enabled ? "Enabled" : "Disabled"
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: bluetoothToggle
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
if (BluetoothService.adapter) {
|
||||
BluetoothService.adapter.enabled = !BluetoothService.adapter.enabled;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Column {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingM
|
||||
visible: BluetoothService.adapter && BluetoothService.adapter.enabled
|
||||
|
||||
Text {
|
||||
text: "Paired Devices"
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: BluetoothService.adapter && BluetoothService.adapter.devices ? BluetoothService.adapter.devices.values.filter((dev) => {
|
||||
return dev && dev.paired;
|
||||
}) : []
|
||||
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: 60
|
||||
radius: Theme.cornerRadius
|
||||
color: btDeviceArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : (modelData.connected ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08))
|
||||
border.color: modelData.connected ? Theme.primary : "transparent"
|
||||
border.width: 1
|
||||
|
||||
Row {
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Theme.spacingM
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Theme.spacingM
|
||||
|
||||
Text {
|
||||
text: BluetoothService.getDeviceIcon(modelData)
|
||||
font.family: Theme.iconFont
|
||||
font.pixelSize: Theme.iconSize
|
||||
color: modelData.connected ? Theme.primary : Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Column {
|
||||
spacing: 2
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
Text {
|
||||
text: modelData.name || modelData.deviceName
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: modelData.connected ? Theme.primary : Theme.surfaceText
|
||||
font.weight: modelData.connected ? Font.Medium : Font.Normal
|
||||
}
|
||||
|
||||
Row {
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
Text {
|
||||
text: modelData.connected ? "Connected" : "Disconnected"
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
|
||||
}
|
||||
|
||||
Text {
|
||||
text: {
|
||||
if (modelData.batteryAvailable && modelData.battery > 0)
|
||||
return "• " + Math.round(modelData.battery * 100) + "%";
|
||||
|
||||
var btBattery = BatteryService.bluetoothDevices.find((dev) => {
|
||||
return dev.name === (modelData.name || modelData.deviceName) || dev.name.toLowerCase().includes((modelData.name || modelData.deviceName).toLowerCase()) || (modelData.name || modelData.deviceName).toLowerCase().includes(dev.name.toLowerCase());
|
||||
});
|
||||
return btBattery ? "• " + btBattery.percentage + "%" : "";
|
||||
}
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
|
||||
visible: text.length > 0
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: btMenuButton
|
||||
|
||||
width: 32
|
||||
height: 32
|
||||
radius: Theme.cornerRadius
|
||||
color: btMenuButtonArea.containsMouse ? Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.08) : "transparent"
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: Theme.spacingM
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
Text {
|
||||
text: "more_vert"
|
||||
font.family: Theme.iconFont
|
||||
font.weight: Theme.iconFontWeight
|
||||
font.pixelSize: Theme.iconSize
|
||||
color: Theme.surfaceText
|
||||
opacity: 0.6
|
||||
anchors.centerIn: parent
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: btMenuButtonArea
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
bluetoothContextMenuWindow.deviceData = modelData;
|
||||
let localPos = btMenuButtonArea.mapToItem(bluetoothTab, btMenuButtonArea.width / 2, btMenuButtonArea.height);
|
||||
bluetoothContextMenuWindow.show(localPos.x, localPos.y);
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Theme.shortDuration
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: btDeviceArea
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.rightMargin: 40
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
BluetoothService.debugDevice(modelData);
|
||||
if (modelData.connected) {
|
||||
modelData.disconnect();
|
||||
} else {
|
||||
modelData.connect();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Column {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingM
|
||||
visible: BluetoothService.adapter && BluetoothService.adapter.enabled
|
||||
|
||||
Row {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingM
|
||||
|
||||
Text {
|
||||
text: "Available Devices"
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Item {
|
||||
width: 1
|
||||
height: 1
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: Math.max(140, scanText.contentWidth + Theme.spacingL * 2)
|
||||
height: 36
|
||||
radius: Theme.cornerRadius
|
||||
color: scanArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08)
|
||||
border.color: Theme.primary
|
||||
border.width: 1
|
||||
|
||||
Row {
|
||||
anchors.centerIn: parent
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
Text {
|
||||
text: BluetoothService.adapter && BluetoothService.adapter.discovering ? "stop" : "bluetooth_searching"
|
||||
font.family: Theme.iconFont
|
||||
font.pixelSize: Theme.iconSize - 4
|
||||
color: Theme.primary
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Text {
|
||||
id: scanText
|
||||
|
||||
text: BluetoothService.adapter && BluetoothService.adapter.discovering ? "Stop Scanning" : "Start Scanning"
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.primary
|
||||
font.weight: Font.Medium
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: scanArea
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
if (BluetoothService.adapter) {
|
||||
BluetoothService.adapter.discovering = !BluetoothService.adapter.discovering;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: {
|
||||
if (!BluetoothService.adapter || !BluetoothService.adapter.discovering || !Bluetooth.devices)
|
||||
return [];
|
||||
|
||||
var filtered = Bluetooth.devices.values.filter((dev) => {
|
||||
return dev && !dev.paired && !dev.pairing && !dev.blocked && (dev.signalStrength === undefined || dev.signalStrength > 0);
|
||||
});
|
||||
return BluetoothService.sortDevices(filtered);
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
property bool canConnect: BluetoothService.canConnect(modelData)
|
||||
|
||||
width: parent.width
|
||||
height: 70
|
||||
radius: Theme.cornerRadius
|
||||
color: {
|
||||
if (availableDeviceArea.containsMouse)
|
||||
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08);
|
||||
|
||||
if (modelData.pairing)
|
||||
return Qt.rgba(Theme.warning.r, Theme.warning.g, Theme.warning.b, 0.12);
|
||||
|
||||
if (modelData.blocked)
|
||||
return Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.08);
|
||||
|
||||
return Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08);
|
||||
}
|
||||
border.color: {
|
||||
if (modelData.pairing)
|
||||
return Theme.warning;
|
||||
|
||||
if (modelData.blocked)
|
||||
return Theme.error;
|
||||
|
||||
return Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2);
|
||||
}
|
||||
border.width: 1
|
||||
|
||||
Row {
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Theme.spacingM
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Theme.spacingM
|
||||
|
||||
Text {
|
||||
text: BluetoothService.getDeviceIcon(modelData)
|
||||
font.family: Theme.iconFont
|
||||
font.pixelSize: Theme.iconSize
|
||||
color: {
|
||||
if (modelData.pairing)
|
||||
return Theme.warning;
|
||||
|
||||
if (modelData.blocked)
|
||||
return Theme.error;
|
||||
|
||||
return Theme.surfaceText;
|
||||
}
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Column {
|
||||
spacing: 2
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
Text {
|
||||
text: modelData.name || modelData.deviceName
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: {
|
||||
if (modelData.pairing)
|
||||
return Theme.warning;
|
||||
|
||||
if (modelData.blocked)
|
||||
return Theme.error;
|
||||
|
||||
return Theme.surfaceText;
|
||||
}
|
||||
font.weight: modelData.pairing ? Font.Medium : Font.Normal
|
||||
}
|
||||
|
||||
Row {
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
Row {
|
||||
spacing: Theme.spacingS
|
||||
|
||||
Text {
|
||||
text: {
|
||||
if (modelData.pairing)
|
||||
return "Pairing...";
|
||||
if (modelData.blocked)
|
||||
return "Blocked";
|
||||
return BluetoothService.getSignalStrength(modelData);
|
||||
}
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: {
|
||||
if (modelData.pairing)
|
||||
return Theme.warning;
|
||||
|
||||
if (modelData.blocked)
|
||||
return Theme.error;
|
||||
|
||||
return Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7);
|
||||
}
|
||||
}
|
||||
|
||||
Text {
|
||||
text: BluetoothService.getSignalIcon(modelData)
|
||||
font.family: Theme.iconFont
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
|
||||
visible: modelData.signalStrength !== undefined && modelData.signalStrength > 0 && !modelData.pairing && !modelData.blocked
|
||||
}
|
||||
|
||||
Text {
|
||||
text: (modelData.signalStrength !== undefined && modelData.signalStrength > 0) ? modelData.signalStrength + "%" : ""
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.5)
|
||||
visible: modelData.signalStrength !== undefined && modelData.signalStrength > 0 && !modelData.pairing && !modelData.blocked
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: 80
|
||||
height: 28
|
||||
radius: Theme.cornerRadiusSmall
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: Theme.spacingM
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
color: {
|
||||
if (!canConnect && !modelData.pairing)
|
||||
return Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.3);
|
||||
|
||||
if (actionButtonArea.containsMouse)
|
||||
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12);
|
||||
|
||||
return "transparent";
|
||||
}
|
||||
border.color: canConnect || modelData.pairing ? Theme.primary : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
|
||||
border.width: 1
|
||||
opacity: canConnect || modelData.pairing ? 1 : 0.5
|
||||
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
text: {
|
||||
if (modelData.pairing)
|
||||
return "Pairing...";
|
||||
|
||||
if (modelData.blocked)
|
||||
return "Blocked";
|
||||
|
||||
return "Connect";
|
||||
}
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: canConnect || modelData.pairing ? Theme.primary : Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.5)
|
||||
font.weight: Font.Medium
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: actionButtonArea
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: canConnect ? Qt.PointingHandCursor : Qt.ArrowCursor
|
||||
enabled: canConnect
|
||||
onClicked: {
|
||||
modelData && modelData.connect();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: availableDeviceArea
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.rightMargin: 90 // Don't overlap with action button
|
||||
hoverEnabled: true
|
||||
cursorShape: canConnect ? Qt.PointingHandCursor : Qt.ArrowCursor
|
||||
enabled: canConnect
|
||||
onClicked: {
|
||||
modelData && modelData.connect();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Column {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingM
|
||||
visible: {
|
||||
if (!BluetoothService.adapter || !BluetoothService.adapter.discovering || !Bluetooth.devices)
|
||||
return false;
|
||||
|
||||
var availableCount = Bluetooth.devices.values.filter((dev) => {
|
||||
return dev && !dev.paired && !dev.pairing && !dev.blocked && (dev.signalStrength === undefined || dev.signalStrength > 0);
|
||||
}).length;
|
||||
|
||||
return availableCount === 0;
|
||||
}
|
||||
|
||||
Row {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
spacing: Theme.spacingM
|
||||
|
||||
Text {
|
||||
text: "sync"
|
||||
font.family: Theme.iconFont
|
||||
font.pixelSize: Theme.iconSizeLarge
|
||||
color: Theme.primary
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
RotationAnimation on rotation {
|
||||
running: true
|
||||
loops: Animation.Infinite
|
||||
from: 0
|
||||
to: 360
|
||||
duration: 2000
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Text {
|
||||
text: "Scanning for devices..."
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Text {
|
||||
text: "Make sure your device is in pairing mode"
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Text {
|
||||
text: "No devices found. Put your device in pairing mode and click Start Scanning."
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
|
||||
visible: {
|
||||
if (!BluetoothService.adapter || !Bluetooth.devices)
|
||||
return true;
|
||||
|
||||
var availableCount = Bluetooth.devices.values.filter((dev) => {
|
||||
return dev && !dev.paired && !dev.pairing && !dev.blocked && (dev.signalStrength === undefined || dev.signalStrength > 0);
|
||||
}).length;
|
||||
|
||||
return availableCount === 0 && !BluetoothService.adapter.discovering;
|
||||
}
|
||||
wrapMode: Text.WordWrap
|
||||
width: parent.width
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: bluetoothContextMenuWindow
|
||||
|
||||
property var deviceData: null
|
||||
property bool menuVisible: false
|
||||
|
||||
function show(x, y) {
|
||||
const menuWidth = 160;
|
||||
const menuHeight = menuColumn.implicitHeight + Theme.spacingS * 2;
|
||||
let finalX = x - menuWidth / 2;
|
||||
let finalY = y;
|
||||
finalX = Math.max(0, Math.min(finalX, bluetoothTab.width - menuWidth));
|
||||
finalY = Math.max(0, Math.min(finalY, bluetoothTab.height - menuHeight));
|
||||
bluetoothContextMenuWindow.x = finalX;
|
||||
bluetoothContextMenuWindow.y = finalY;
|
||||
bluetoothContextMenuWindow.visible = true;
|
||||
bluetoothContextMenuWindow.menuVisible = true;
|
||||
}
|
||||
|
||||
function hide() {
|
||||
bluetoothContextMenuWindow.menuVisible = false;
|
||||
Qt.callLater(() => {
|
||||
bluetoothContextMenuWindow.visible = false;
|
||||
});
|
||||
}
|
||||
|
||||
visible: false
|
||||
width: 160
|
||||
height: menuColumn.implicitHeight + Theme.spacingS * 2
|
||||
radius: Theme.cornerRadiusLarge
|
||||
color: Theme.popupBackground()
|
||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
|
||||
border.width: 1
|
||||
z: 1000
|
||||
opacity: menuVisible ? 1 : 0
|
||||
scale: menuVisible ? 1 : 0.85
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
anchors.topMargin: 4
|
||||
anchors.leftMargin: 2
|
||||
anchors.rightMargin: -2
|
||||
anchors.bottomMargin: -4
|
||||
radius: parent.radius
|
||||
color: Qt.rgba(0, 0, 0, 0.15)
|
||||
z: parent.z - 1
|
||||
}
|
||||
|
||||
Column {
|
||||
id: menuColumn
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingS
|
||||
spacing: 1
|
||||
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: 32
|
||||
radius: Theme.cornerRadiusSmall
|
||||
color: connectArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
|
||||
|
||||
Row {
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Theme.spacingS
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Theme.spacingS
|
||||
|
||||
Text {
|
||||
text: bluetoothContextMenuWindow.deviceData && bluetoothContextMenuWindow.deviceData.connected ? "link_off" : "link"
|
||||
font.family: Theme.iconFont
|
||||
font.pixelSize: Theme.iconSize - 2
|
||||
color: Theme.surfaceText
|
||||
opacity: 0.7
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Text {
|
||||
text: bluetoothContextMenuWindow.deviceData && bluetoothContextMenuWindow.deviceData.connected ? "Disconnect" : "Connect"
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Normal
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: connectArea
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
if (bluetoothContextMenuWindow.deviceData) {
|
||||
if (bluetoothContextMenuWindow.deviceData.connected) {
|
||||
bluetoothContextMenuWindow.deviceData.disconnect();
|
||||
} else {
|
||||
bluetoothContextMenuWindow.deviceData.connect();
|
||||
}
|
||||
}
|
||||
bluetoothContextMenuWindow.hide();
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Theme.standardEasing
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: parent.width - Theme.spacingS * 2
|
||||
height: 5
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
color: "transparent"
|
||||
|
||||
Rectangle {
|
||||
anchors.centerIn: parent
|
||||
width: parent.width
|
||||
height: 1
|
||||
color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: 32
|
||||
radius: Theme.cornerRadiusSmall
|
||||
color: forgetArea.containsMouse ? Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.12) : "transparent"
|
||||
|
||||
Row {
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Theme.spacingS
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Theme.spacingS
|
||||
|
||||
Text {
|
||||
text: "delete"
|
||||
font.family: Theme.iconFont
|
||||
font.pixelSize: Theme.iconSize - 2
|
||||
color: forgetArea.containsMouse ? Theme.error : Theme.surfaceText
|
||||
opacity: 0.7
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Text {
|
||||
text: "Forget Device"
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: forgetArea.containsMouse ? Theme.error : Theme.surfaceText
|
||||
font.weight: Font.Normal
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: forgetArea
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
if (bluetoothContextMenuWindow.deviceData) {
|
||||
bluetoothContextMenuWindow.deviceData.forget();
|
||||
}
|
||||
bluetoothContextMenuWindow.hide();
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Theme.standardEasing
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: Theme.mediumDuration
|
||||
easing.type: Theme.emphasizedEasing
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Behavior on scale {
|
||||
NumberAnimation {
|
||||
duration: Theme.mediumDuration
|
||||
easing.type: Theme.emphasizedEasing
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
visible: bluetoothContextMenuWindow.visible
|
||||
onClicked: {
|
||||
bluetoothContextMenuWindow.hide();
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
x: bluetoothContextMenuWindow.x
|
||||
y: bluetoothContextMenuWindow.y
|
||||
width: bluetoothContextMenuWindow.width
|
||||
height: bluetoothContextMenuWindow.height
|
||||
onClicked: {
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,778 +0,0 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Effects
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import Quickshell.Wayland
|
||||
import Quickshell.Widgets
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
|
||||
PanelWindow {
|
||||
id: root
|
||||
|
||||
property bool controlCenterVisible: false
|
||||
property string currentTab: "network" // "network", "audio", "bluetooth", "display"
|
||||
property bool powerOptionsExpanded: false
|
||||
|
||||
visible: controlCenterVisible
|
||||
onVisibleChanged: {
|
||||
// Enable/disable WiFi auto-refresh based on control center visibility
|
||||
WifiService.autoRefreshEnabled = visible && NetworkService.wifiEnabled;
|
||||
// Stop bluetooth scanning when control center is closed
|
||||
if (!visible && BluetoothService.adapter && BluetoothService.adapter.discovering) {
|
||||
BluetoothService.adapter.discovering = false;
|
||||
}
|
||||
// Refresh uptime when opened
|
||||
if (visible && UserInfoService) {
|
||||
UserInfoService.getUptime();
|
||||
}
|
||||
}
|
||||
implicitWidth: 600
|
||||
implicitHeight: 500
|
||||
WlrLayershell.layer: WlrLayershell.Overlay
|
||||
WlrLayershell.exclusiveZone: -1
|
||||
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
|
||||
color: "transparent"
|
||||
|
||||
anchors {
|
||||
top: true
|
||||
left: true
|
||||
right: true
|
||||
bottom: true
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: Math.min(600, Screen.width - Theme.spacingL * 2)
|
||||
height: root.powerOptionsExpanded ? 570 : 500
|
||||
x: Math.max(Theme.spacingL, Screen.width - width - Theme.spacingL)
|
||||
y: Theme.barHeight + Theme.spacingXS
|
||||
color: Theme.popupBackground()
|
||||
radius: Theme.cornerRadiusLarge
|
||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
|
||||
border.width: 1
|
||||
opacity: controlCenterVisible ? 1 : 0
|
||||
// TopBar dropdown animation - optimized for performance
|
||||
transform: [
|
||||
Scale {
|
||||
id: scaleTransform
|
||||
|
||||
origin.x: 600 // Use fixed width since popup is max 600px wide
|
||||
origin.y: 0
|
||||
xScale: controlCenterVisible ? 1 : 0.95
|
||||
yScale: controlCenterVisible ? 1 : 0.8
|
||||
},
|
||||
Translate {
|
||||
id: translateTransform
|
||||
|
||||
x: controlCenterVisible ? 0 : 15 // Slide slightly left when hidden
|
||||
y: controlCenterVisible ? 0 : -30
|
||||
}
|
||||
]
|
||||
// Single coordinated animation for better performance
|
||||
states: [
|
||||
State {
|
||||
name: "visible"
|
||||
when: controlCenterVisible
|
||||
|
||||
PropertyChanges {
|
||||
target: scaleTransform
|
||||
xScale: 1
|
||||
yScale: 1
|
||||
}
|
||||
|
||||
PropertyChanges {
|
||||
target: translateTransform
|
||||
x: 0
|
||||
y: 0
|
||||
}
|
||||
|
||||
},
|
||||
State {
|
||||
name: "hidden"
|
||||
when: !controlCenterVisible
|
||||
|
||||
PropertyChanges {
|
||||
target: scaleTransform
|
||||
xScale: 0.95
|
||||
yScale: 0.8
|
||||
}
|
||||
|
||||
PropertyChanges {
|
||||
target: translateTransform
|
||||
x: 15
|
||||
y: -30
|
||||
}
|
||||
|
||||
}
|
||||
]
|
||||
transitions: [
|
||||
Transition {
|
||||
from: "*"
|
||||
to: "*"
|
||||
|
||||
ParallelAnimation {
|
||||
NumberAnimation {
|
||||
targets: [scaleTransform, translateTransform]
|
||||
properties: "xScale,yScale,x,y"
|
||||
duration: Theme.mediumDuration
|
||||
easing.type: Theme.emphasizedEasing
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
]
|
||||
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingL
|
||||
spacing: Theme.spacingM
|
||||
|
||||
// Elegant User Header
|
||||
Column {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingL
|
||||
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: 90
|
||||
radius: Theme.cornerRadiusLarge
|
||||
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, Theme.getContentBackgroundAlpha() * 0.4)
|
||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
|
||||
border.width: 1
|
||||
|
||||
Row {
|
||||
anchors.left: parent.left
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.leftMargin: Theme.spacingL
|
||||
anchors.rightMargin: Theme.spacingL
|
||||
spacing: Theme.spacingL
|
||||
|
||||
// Profile Picture Container
|
||||
Item {
|
||||
id: avatarContainer
|
||||
|
||||
property bool hasImage: profileImageLoader.status === Image.Ready
|
||||
|
||||
width: 64
|
||||
height: 64
|
||||
|
||||
// This rectangle provides the themed ring via its border.
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
radius: width / 2
|
||||
color: "transparent"
|
||||
border.color: Theme.primary
|
||||
border.width: 1 // The ring is 1px thick.
|
||||
visible: parent.hasImage
|
||||
}
|
||||
|
||||
// Hidden Image loader. Its only purpose is to load the texture.
|
||||
Image {
|
||||
id: profileImageLoader
|
||||
|
||||
source: {
|
||||
if (Prefs.profileImage === "")
|
||||
return "";
|
||||
|
||||
if (Prefs.profileImage.startsWith("/"))
|
||||
return "file://" + Prefs.profileImage;
|
||||
|
||||
return Prefs.profileImage;
|
||||
}
|
||||
smooth: true
|
||||
asynchronous: true
|
||||
mipmap: true
|
||||
cache: true
|
||||
visible: false // This item is never shown directly.
|
||||
}
|
||||
|
||||
MultiEffect {
|
||||
anchors.fill: parent
|
||||
anchors.margins: 5
|
||||
source: profileImageLoader
|
||||
maskEnabled: true
|
||||
maskSource: circularMask
|
||||
visible: avatarContainer.hasImage
|
||||
maskThresholdMin: 0.5
|
||||
maskSpreadAtMin: 1
|
||||
}
|
||||
|
||||
Item {
|
||||
id: circularMask
|
||||
|
||||
width: 64 - 10
|
||||
height: 64 - 10
|
||||
layer.enabled: true
|
||||
layer.smooth: true
|
||||
visible: false
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
radius: width / 2
|
||||
color: "black"
|
||||
antialiasing: true
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Fallback for when there is no image.
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
radius: width / 2
|
||||
color: Theme.primary
|
||||
visible: !parent.hasImage
|
||||
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
text: "person"
|
||||
font.family: Theme.iconFont
|
||||
font.pixelSize: Theme.iconSize + 8
|
||||
color: Theme.primaryText
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Error icon for when the image fails to load.
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
text: "warning"
|
||||
font.family: Theme.iconFont
|
||||
font.pixelSize: Theme.iconSize + 8
|
||||
color: Theme.primaryText
|
||||
visible: Prefs.profileImage !== "" && profileImageLoader.status === Image.Error
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// User Info Text
|
||||
Column {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
Text {
|
||||
text: UserInfoService.fullName || UserInfoService.username || "User"
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
}
|
||||
|
||||
Text {
|
||||
text: "Uptime: " + (UserInfoService.uptime || "Unknown")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
font.weight: Font.Normal
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Action Buttons - Power and Settings
|
||||
Row {
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.rightMargin: Theme.spacingL
|
||||
spacing: Theme.spacingS
|
||||
|
||||
// Power Button
|
||||
Rectangle {
|
||||
width: 40
|
||||
height: 40
|
||||
radius: 20
|
||||
color: powerButton.containsMouse || root.powerOptionsExpanded ? Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.12) : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.5)
|
||||
|
||||
Rectangle {
|
||||
anchors.centerIn: parent
|
||||
width: parent.width
|
||||
height: parent.height
|
||||
radius: parent.radius
|
||||
color: "transparent"
|
||||
clip: true
|
||||
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
text: root.powerOptionsExpanded ? "expand_less" : "power_settings_new"
|
||||
font.family: Theme.iconFont
|
||||
font.pixelSize: Theme.iconSize - 2
|
||||
color: powerButton.containsMouse || root.powerOptionsExpanded ? Theme.error : Theme.surfaceText
|
||||
|
||||
Behavior on text {
|
||||
// Smooth icon transition
|
||||
SequentialAnimation {
|
||||
NumberAnimation {
|
||||
target: parent
|
||||
property: "opacity"
|
||||
to: 0
|
||||
duration: Theme.shortDuration / 2
|
||||
easing.type: Theme.standardEasing
|
||||
}
|
||||
|
||||
PropertyAction {
|
||||
target: parent
|
||||
property: "text"
|
||||
}
|
||||
|
||||
NumberAnimation {
|
||||
target: parent
|
||||
property: "opacity"
|
||||
to: 1
|
||||
duration: Theme.shortDuration / 2
|
||||
easing.type: Theme.standardEasing
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: powerButton
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
root.powerOptionsExpanded = !root.powerOptionsExpanded;
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Theme.standardEasing
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Settings Button
|
||||
Rectangle {
|
||||
width: 40
|
||||
height: 40
|
||||
radius: 20
|
||||
color: settingsButton.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.5)
|
||||
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
text: "settings"
|
||||
font.family: Theme.iconFont
|
||||
font.pixelSize: Theme.iconSize - 2
|
||||
color: Theme.surfaceText
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: settingsButton
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
controlCenterVisible = false;
|
||||
settingsPopup.settingsVisible = true;
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Theme.standardEasing
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Animated Collapsible Power Options (optimized)
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: root.powerOptionsExpanded ? 60 : 0
|
||||
radius: Theme.cornerRadius
|
||||
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, Theme.getContentBackgroundAlpha() * 0.4)
|
||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
|
||||
border.width: root.powerOptionsExpanded ? 1 : 0
|
||||
opacity: root.powerOptionsExpanded ? 1 : 0
|
||||
clip: true
|
||||
|
||||
Row {
|
||||
anchors.centerIn: parent
|
||||
spacing: Theme.spacingL
|
||||
visible: root.powerOptionsExpanded
|
||||
|
||||
// Logout
|
||||
Rectangle {
|
||||
width: 100
|
||||
height: 34
|
||||
radius: Theme.cornerRadius
|
||||
color: logoutButton.containsMouse ? Qt.rgba(Theme.warning.r, Theme.warning.g, Theme.warning.b, 0.12) : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.5)
|
||||
|
||||
Row {
|
||||
anchors.centerIn: parent
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
Text {
|
||||
text: "logout"
|
||||
font.family: Theme.iconFont
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: logoutButton.containsMouse ? Theme.warning : Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Text {
|
||||
text: "Logout"
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: logoutButton.containsMouse ? Theme.warning : Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: logoutButton
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
root.powerOptionsExpanded = false;
|
||||
if (typeof root !== "undefined" && root.powerConfirmDialog) {
|
||||
root.powerConfirmDialog.powerConfirmAction = "logout";
|
||||
root.powerConfirmDialog.powerConfirmTitle = "Logout";
|
||||
root.powerConfirmDialog.powerConfirmMessage = "Are you sure you want to logout?";
|
||||
root.powerConfirmDialog.powerConfirmVisible = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Theme.standardEasing
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Reboot
|
||||
Rectangle {
|
||||
width: 100
|
||||
height: 34
|
||||
radius: Theme.cornerRadius
|
||||
color: rebootButton.containsMouse ? Qt.rgba(Theme.warning.r, Theme.warning.g, Theme.warning.b, 0.12) : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.5)
|
||||
|
||||
Row {
|
||||
anchors.centerIn: parent
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
Text {
|
||||
text: "restart_alt"
|
||||
font.family: Theme.iconFont
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: rebootButton.containsMouse ? Theme.warning : Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Text {
|
||||
text: "Restart"
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: rebootButton.containsMouse ? Theme.warning : Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: rebootButton
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
root.powerOptionsExpanded = false;
|
||||
if (typeof root !== "undefined" && root.powerConfirmDialog) {
|
||||
root.powerConfirmDialog.powerConfirmAction = "reboot";
|
||||
root.powerConfirmDialog.powerConfirmTitle = "Restart";
|
||||
root.powerConfirmDialog.powerConfirmMessage = "Are you sure you want to restart?";
|
||||
root.powerConfirmDialog.powerConfirmVisible = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Theme.standardEasing
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Shutdown
|
||||
Rectangle {
|
||||
width: 100
|
||||
height: 34
|
||||
radius: Theme.cornerRadius
|
||||
color: shutdownButton.containsMouse ? Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.12) : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.5)
|
||||
|
||||
Row {
|
||||
anchors.centerIn: parent
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
Text {
|
||||
text: "power_settings_new"
|
||||
font.family: Theme.iconFont
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: shutdownButton.containsMouse ? Theme.error : Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Text {
|
||||
text: "Shutdown"
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: shutdownButton.containsMouse ? Theme.error : Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: shutdownButton
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
root.powerOptionsExpanded = false;
|
||||
if (typeof root !== "undefined" && root.powerConfirmDialog) {
|
||||
root.powerConfirmDialog.powerConfirmAction = "poweroff";
|
||||
root.powerConfirmDialog.powerConfirmTitle = "Shutdown";
|
||||
root.powerConfirmDialog.powerConfirmMessage = "Are you sure you want to shutdown?";
|
||||
root.powerConfirmDialog.powerConfirmVisible = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Theme.standardEasing
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Single coordinated animation for power options
|
||||
Behavior on height {
|
||||
NumberAnimation {
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Theme.standardEasing
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Theme.standardEasing
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Tab buttons
|
||||
Row {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
Repeater {
|
||||
model: {
|
||||
let tabs = [{
|
||||
"name": "Network",
|
||||
"icon": "wifi",
|
||||
"id": "network",
|
||||
"available": true
|
||||
}];
|
||||
// Always show audio
|
||||
tabs.push({
|
||||
"name": "Audio",
|
||||
"icon": "volume_up",
|
||||
"id": "audio",
|
||||
"available": true
|
||||
});
|
||||
// Show Bluetooth only if available
|
||||
if (BluetoothService.available)
|
||||
tabs.push({
|
||||
"name": "Bluetooth",
|
||||
"icon": "bluetooth",
|
||||
"id": "bluetooth",
|
||||
"available": true
|
||||
});
|
||||
|
||||
// Always show display
|
||||
tabs.push({
|
||||
"name": "Display",
|
||||
"icon": "brightness_6",
|
||||
"id": "display",
|
||||
"available": true
|
||||
});
|
||||
return tabs;
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
property int tabCount: {
|
||||
let count = 3; // Network + Audio + Display (always visible)
|
||||
if (BluetoothService.available)
|
||||
count++;
|
||||
|
||||
return count;
|
||||
}
|
||||
|
||||
width: (parent.width - Theme.spacingXS * (tabCount - 1)) / tabCount
|
||||
height: 40
|
||||
radius: Theme.cornerRadius
|
||||
color: root.currentTab === modelData.id ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.16) : tabArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : "transparent"
|
||||
|
||||
Row {
|
||||
anchors.centerIn: parent
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
Text {
|
||||
text: modelData.icon
|
||||
font.family: Theme.iconFont
|
||||
font.pixelSize: Theme.iconSize - 4
|
||||
color: root.currentTab === modelData.id ? Theme.primary : Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Text {
|
||||
text: modelData.name
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: root.currentTab === modelData.id ? Theme.primary : Theme.surfaceText
|
||||
font.weight: root.currentTab === modelData.id ? Font.Medium : Font.Normal
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: tabArea
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
root.currentTab = modelData.id;
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Theme.standardEasing
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Tab content area
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
Layout.fillHeight: true
|
||||
radius: Theme.cornerRadius
|
||||
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, Theme.getContentBackgroundAlpha() * 0.1)
|
||||
|
||||
// Network Tab
|
||||
NetworkTab {
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingM
|
||||
visible: root.currentTab === "network"
|
||||
}
|
||||
|
||||
// Audio Tab
|
||||
AudioTab {
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingM
|
||||
visible: root.currentTab === "audio"
|
||||
}
|
||||
|
||||
// Bluetooth Tab
|
||||
BluetoothTab {
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingM
|
||||
visible: BluetoothService.available && root.currentTab === "bluetooth"
|
||||
}
|
||||
|
||||
// Display Tab
|
||||
DisplayTab {
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingM
|
||||
visible: root.currentTab === "display"
|
||||
}
|
||||
|
||||
Behavior on height {
|
||||
NumberAnimation {
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Theme.standardEasing
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Power menu height animation
|
||||
Behavior on height {
|
||||
NumberAnimation {
|
||||
duration: Theme.shortDuration // Faster for height changes
|
||||
easing.type: Theme.standardEasing
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: Theme.mediumDuration
|
||||
easing.type: Theme.emphasizedEasing
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Click outside to close
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
z: -1
|
||||
onClicked: {
|
||||
controlCenterVisible = false;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,223 +0,0 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import Quickshell.Widgets
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
ScrollView {
|
||||
id: displayTab
|
||||
|
||||
clip: true
|
||||
|
||||
property var brightnessDebounceTimer: Timer {
|
||||
interval: BrightnessService.ddcAvailable ? 500 : 50 // 500ms for slow DDC (i2c), 50ms for fast laptop backlight
|
||||
repeat: false
|
||||
property int pendingValue: 0
|
||||
onTriggered: {
|
||||
console.log("Debounce timer fired, setting brightness to:", pendingValue);
|
||||
BrightnessService.setBrightness(pendingValue);
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingL
|
||||
|
||||
// Brightness Control
|
||||
Column {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingM
|
||||
visible: BrightnessService.brightnessAvailable
|
||||
|
||||
Text {
|
||||
text: "Brightness"
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
}
|
||||
|
||||
CustomSlider {
|
||||
width: parent.width
|
||||
value: BrightnessService.brightnessLevel
|
||||
leftIcon: "brightness_low"
|
||||
rightIcon: "brightness_high"
|
||||
enabled: BrightnessService.brightnessAvailable
|
||||
onSliderValueChanged: function(newValue) {
|
||||
console.log("Slider changed to:", newValue);
|
||||
brightnessDebounceTimer.pendingValue = newValue;
|
||||
brightnessDebounceTimer.restart();
|
||||
}
|
||||
onSliderDragFinished: function(finalValue) {
|
||||
console.log("Drag finished, immediate set:", finalValue);
|
||||
brightnessDebounceTimer.stop();
|
||||
BrightnessService.setBrightness(finalValue);
|
||||
}
|
||||
}
|
||||
|
||||
Text {
|
||||
text: "using ddc - changes may take a moment to apply"
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
visible: BrightnessService.ddcAvailable && !BrightnessService.laptopBacklightAvailable
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Display settings
|
||||
Column {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingM
|
||||
|
||||
Text {
|
||||
text: "Display Settings"
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
}
|
||||
|
||||
// Mode toggles row (Night Mode + Light/Dark Mode)
|
||||
Row {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingM
|
||||
|
||||
// Night mode toggle
|
||||
Rectangle {
|
||||
width: (parent.width - Theme.spacingM) / 2
|
||||
height: 80
|
||||
radius: Theme.cornerRadius
|
||||
color: Prefs.nightModeEnabled ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : (nightModeToggle.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08))
|
||||
border.color: Prefs.nightModeEnabled ? Theme.primary : "transparent"
|
||||
border.width: Prefs.nightModeEnabled ? 1 : 0
|
||||
|
||||
Column {
|
||||
anchors.centerIn: parent
|
||||
spacing: Theme.spacingS
|
||||
|
||||
Text {
|
||||
text: Prefs.nightModeEnabled ? "nightlight" : "dark_mode"
|
||||
font.family: Theme.iconFont
|
||||
font.pixelSize: Theme.iconSizeLarge
|
||||
color: Prefs.nightModeEnabled ? Theme.primary : Theme.surfaceText
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
}
|
||||
|
||||
Text {
|
||||
text: "Night Mode"
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Prefs.nightModeEnabled ? Theme.primary : Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: nightModeToggle
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
if (Prefs.nightModeEnabled) {
|
||||
// Disable night mode - kill any running color temperature processes
|
||||
nightModeDisableProcess.running = true;
|
||||
Prefs.setNightModeEnabled(false);
|
||||
} else {
|
||||
// Enable night mode using wlsunset or redshift
|
||||
nightModeEnableProcess.running = true;
|
||||
Prefs.setNightModeEnabled(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Light/Dark mode toggle
|
||||
Rectangle {
|
||||
width: (parent.width - Theme.spacingM) / 2
|
||||
height: 80
|
||||
radius: Theme.cornerRadius
|
||||
color: Theme.isLightMode ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : (lightModeToggle.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08))
|
||||
border.color: Theme.isLightMode ? Theme.primary : "transparent"
|
||||
border.width: Theme.isLightMode ? 1 : 0
|
||||
|
||||
Column {
|
||||
anchors.centerIn: parent
|
||||
spacing: Theme.spacingS
|
||||
|
||||
Text {
|
||||
text: Theme.isLightMode ? "light_mode" : "palette"
|
||||
font.family: Theme.iconFont
|
||||
font.pixelSize: Theme.iconSizeLarge
|
||||
color: Theme.isLightMode ? Theme.primary : Theme.surfaceText
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
}
|
||||
|
||||
Text {
|
||||
text: Theme.isLightMode ? "Light Mode" : "Dark Mode"
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.isLightMode ? Theme.primary : Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: lightModeToggle
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
Theme.toggleLightMode();
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Theme.standardEasing
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Night mode processes
|
||||
Process {
|
||||
id: nightModeEnableProcess
|
||||
|
||||
command: ["bash", "-c", "if command -v wlsunset > /dev/null; then pkill wlsunset; wlsunset -t 3000 & elif command -v redshift > /dev/null; then pkill redshift; redshift -P -O 3000 & else echo 'No night mode tool available'; fi"]
|
||||
running: false
|
||||
onExited: (exitCode) => {
|
||||
if (exitCode !== 0) {
|
||||
console.warn("Failed to enable night mode");
|
||||
Prefs.setNightModeEnabled(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: nightModeDisableProcess
|
||||
|
||||
command: ["bash", "-c", "pkill wlsunset; pkill redshift; if command -v wlsunset > /dev/null; then wlsunset -t 6500 -T 6500 & sleep 1; pkill wlsunset; elif command -v redshift > /dev/null; then redshift -P -O 6500; redshift -x; fi"]
|
||||
running: false
|
||||
onExited: (exitCode) => {
|
||||
if (exitCode !== 0)
|
||||
console.warn("Failed to disable night mode");
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,963 +0,0 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import Quickshell.Widgets
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
|
||||
Item {
|
||||
// Default to WiFi when nothing is connected
|
||||
|
||||
id: networkTab
|
||||
|
||||
property int networkSubTab: {
|
||||
// Default to WiFi tab if WiFi is connected, otherwise Ethernet
|
||||
if (NetworkService.networkStatus === "wifi")
|
||||
return 1;
|
||||
else if (NetworkService.networkStatus === "ethernet")
|
||||
return 0;
|
||||
else
|
||||
return 1;
|
||||
}
|
||||
|
||||
Column {
|
||||
anchors.fill: parent
|
||||
spacing: Theme.spacingM
|
||||
|
||||
// Network sub-tabs
|
||||
Row {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
Rectangle {
|
||||
width: (parent.width - Theme.spacingXS) / 2
|
||||
height: 36
|
||||
radius: Theme.cornerRadiusSmall
|
||||
color: networkTab.networkSubTab === 0 ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.16) : ethernetTabArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : "transparent"
|
||||
|
||||
Row {
|
||||
anchors.centerIn: parent
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
Text {
|
||||
text: "lan"
|
||||
font.family: Theme.iconFont
|
||||
font.pixelSize: Theme.iconSize - 4
|
||||
color: networkTab.networkSubTab === 0 ? Theme.primary : Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Text {
|
||||
text: "Ethernet"
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: networkTab.networkSubTab === 0 ? Theme.primary : Theme.surfaceText
|
||||
font.weight: networkTab.networkSubTab === 0 ? Font.Medium : Font.Normal
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: ethernetTabArea
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
networkTab.networkSubTab = 0;
|
||||
WifiService.autoRefreshEnabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: (parent.width - Theme.spacingXS) / 2
|
||||
height: 36
|
||||
radius: Theme.cornerRadiusSmall
|
||||
color: networkTab.networkSubTab === 1 ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.16) : wifiTabArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : "transparent"
|
||||
|
||||
Row {
|
||||
anchors.centerIn: parent
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
Text {
|
||||
text: NetworkService.wifiEnabled ? "wifi" : "wifi_off"
|
||||
font.family: Theme.iconFont
|
||||
font.pixelSize: Theme.iconSize - 4
|
||||
color: networkTab.networkSubTab === 1 ? Theme.primary : Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Text {
|
||||
text: "Wi-Fi"
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: networkTab.networkSubTab === 1 ? Theme.primary : Theme.surfaceText
|
||||
font.weight: networkTab.networkSubTab === 1 ? Font.Medium : Font.Normal
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: wifiTabArea
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
networkTab.networkSubTab = 1;
|
||||
WifiService.autoRefreshEnabled = true;
|
||||
if (NetworkService.wifiEnabled)
|
||||
WifiService.scanWifi();
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Ethernet Tab Content
|
||||
Flickable {
|
||||
width: parent.width
|
||||
height: parent.height - 48
|
||||
visible: networkTab.networkSubTab === 0
|
||||
clip: true
|
||||
contentWidth: width
|
||||
contentHeight: ethernetContent.height
|
||||
boundsBehavior: Flickable.StopAtBounds
|
||||
flickDeceleration: 8000
|
||||
maximumFlickVelocity: 15000
|
||||
|
||||
Column {
|
||||
id: ethernetContent
|
||||
|
||||
width: parent.width
|
||||
spacing: Theme.spacingL
|
||||
|
||||
// Ethernet status card
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: 70
|
||||
radius: Theme.cornerRadius
|
||||
color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.5)
|
||||
border.color: NetworkService.networkStatus === "ethernet" ? Theme.primary : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
|
||||
border.width: NetworkService.networkStatus === "ethernet" ? 2 : 1
|
||||
visible: true
|
||||
|
||||
Row {
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Theme.spacingL
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Theme.spacingM
|
||||
|
||||
Text {
|
||||
text: "lan"
|
||||
font.family: Theme.iconFont
|
||||
font.pixelSize: Theme.iconSizeLarge - 4
|
||||
color: networkTab.networkStatus === "ethernet" ? Theme.primary : Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Column {
|
||||
spacing: 4
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
Text {
|
||||
text: networkTab.networkStatus === "ethernet" ? (networkTab.ethernetInterface || "Ethernet") : "Ethernet"
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: networkTab.networkStatus === "ethernet" ? Theme.primary : Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
}
|
||||
|
||||
Text {
|
||||
text: NetworkService.ethernetConnected ? (NetworkService.ethernetIP || "Connected") : "Disconnected"
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Force Ethernet preference button
|
||||
Rectangle {
|
||||
width: 150
|
||||
height: 30
|
||||
color: networkTab.networkStatus === "ethernet" ? Theme.primary : Theme.surface
|
||||
border.color: Theme.primary
|
||||
border.width: 1
|
||||
radius: 6
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
z: 10
|
||||
opacity: networkTab.changingNetworkPreference ? 0.6 : 1
|
||||
visible: NetworkService.networkStatus !== "ethernet" && NetworkService.wifiAvailable && NetworkService.wifiEnabled
|
||||
|
||||
Row {
|
||||
anchors.centerIn: parent
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
Text {
|
||||
id: ethernetPreferenceIcon
|
||||
|
||||
text: networkTab.changingNetworkPreference ? "sync" : ""
|
||||
font.family: Theme.iconFont
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: networkTab.networkStatus === "ethernet" ? Theme.background : Theme.primary
|
||||
visible: networkTab.changingNetworkPreference
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
rotation: networkTab.changingNetworkPreference ? ethernetPreferenceIcon.rotation : 0
|
||||
|
||||
RotationAnimation {
|
||||
target: ethernetPreferenceIcon
|
||||
property: "rotation"
|
||||
running: networkTab.changingNetworkPreference
|
||||
from: 0
|
||||
to: 360
|
||||
duration: 1000
|
||||
loops: Animation.Infinite
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Text {
|
||||
text: networkTab.changingNetworkPreference ? "Switching..." : (networkTab.networkStatus === "ethernet" ? "" : "Prefer over WiFi")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: networkTab.networkStatus === "ethernet" ? Theme.background : Theme.primary
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
font.weight: Font.Medium
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
propagateComposedEvents: false
|
||||
enabled: !networkTab.changingNetworkPreference
|
||||
onClicked: {
|
||||
console.log("*** ETHERNET PREFERENCE BUTTON CLICKED ***");
|
||||
if (networkTab.networkStatus !== "ethernet") {
|
||||
console.log("Setting preference to ethernet");
|
||||
NetworkService.setNetworkPreference("ethernet");
|
||||
} else {
|
||||
console.log("Setting preference to auto");
|
||||
NetworkService.setNetworkPreference("auto");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Theme.standardEasing
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Ethernet control button
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: 50
|
||||
radius: Theme.cornerRadius
|
||||
color: ethernetControlArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08)
|
||||
|
||||
Row {
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Theme.spacingM
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Theme.spacingM
|
||||
|
||||
Text {
|
||||
text: networkTab.ethernetConnected ? "link_off" : "link"
|
||||
font.family: Theme.iconFont
|
||||
font.pixelSize: Theme.iconSize
|
||||
color: networkTab.ethernetConnected ? Theme.error : Theme.primary
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Text {
|
||||
text: networkTab.ethernetConnected ? "Disconnect Ethernet" : "Connect Ethernet"
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: ethernetControlArea
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
NetworkService.toggleNetworkConnection("ethernet");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
ScrollBar.vertical: ScrollBar {
|
||||
policy: ScrollBar.AsNeeded
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// WiFi Tab Content
|
||||
Flickable {
|
||||
width: parent.width
|
||||
height: parent.height - 48
|
||||
visible: networkTab.networkSubTab === 1
|
||||
clip: true
|
||||
contentWidth: width
|
||||
contentHeight: wifiContent.height
|
||||
boundsBehavior: Flickable.StopAtBounds
|
||||
flickDeceleration: 8000
|
||||
maximumFlickVelocity: 15000
|
||||
|
||||
Column {
|
||||
id: wifiContent
|
||||
|
||||
width: parent.width
|
||||
spacing: Theme.spacingM
|
||||
|
||||
|
||||
// Current WiFi connection (if connected)
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: 60
|
||||
radius: Theme.cornerRadius
|
||||
color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.5)
|
||||
border.color: NetworkService.networkStatus === "wifi" ? Theme.primary : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
|
||||
border.width: NetworkService.networkStatus === "wifi" ? 2 : 1
|
||||
visible: NetworkService.wifiAvailable
|
||||
|
||||
// WiFi icon
|
||||
Text {
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Theme.spacingL
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: {
|
||||
if (!NetworkService.wifiEnabled) {
|
||||
return "wifi_off";
|
||||
} else if (NetworkService.networkStatus === "wifi") {
|
||||
return WifiService.wifiSignalStrength === "excellent" ? "wifi" : WifiService.wifiSignalStrength === "good" ? "wifi_2_bar" : WifiService.wifiSignalStrength === "fair" ? "wifi_1_bar" : WifiService.wifiSignalStrength === "poor" ? "wifi_calling_3" : "wifi";
|
||||
} else {
|
||||
return "wifi";
|
||||
}
|
||||
}
|
||||
font.family: Theme.iconFont
|
||||
font.pixelSize: Theme.iconSize
|
||||
color: NetworkService.networkStatus === "wifi" ? Theme.primary : Theme.surfaceText
|
||||
}
|
||||
|
||||
// WiFi info text
|
||||
Column {
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Theme.spacingL + Theme.iconSize + Theme.spacingM
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: Theme.spacingL + 48 + Theme.spacingM
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: 4
|
||||
|
||||
Text {
|
||||
text: {
|
||||
if (!NetworkService.wifiEnabled) {
|
||||
return "WiFi is off";
|
||||
} else if (NetworkService.wifiEnabled && WifiService.currentWifiSSID) {
|
||||
return WifiService.currentWifiSSID || "Connected";
|
||||
} else {
|
||||
return "Not Connected";
|
||||
}
|
||||
}
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: NetworkService.networkStatus === "wifi" ? Theme.primary : Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
}
|
||||
|
||||
Text {
|
||||
text: {
|
||||
if (!NetworkService.wifiEnabled) {
|
||||
return "Turn on WiFi to see available networks";
|
||||
} else if (NetworkService.wifiEnabled && WifiService.currentWifiSSID) {
|
||||
return NetworkService.wifiIP || "Connected";
|
||||
} else {
|
||||
return "Select a network below";
|
||||
}
|
||||
}
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
|
||||
}
|
||||
}
|
||||
|
||||
// WiFi toggle switch
|
||||
Rectangle {
|
||||
width: 48
|
||||
height: 24
|
||||
radius: 12
|
||||
color: NetworkService.wifiEnabled ? Theme.primary : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.3)
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: Theme.spacingL
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
opacity: NetworkService.wifiToggling ? 0.6 : 1
|
||||
|
||||
Rectangle {
|
||||
id: toggleHandle
|
||||
width: 20
|
||||
height: 20
|
||||
radius: 10
|
||||
color: Theme.surface
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
x: NetworkService.wifiEnabled ? parent.width - width - 2 : 2
|
||||
|
||||
Behavior on x {
|
||||
NumberAnimation {
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Theme.emphasizedEasing
|
||||
}
|
||||
}
|
||||
|
||||
// Subtle shadow/glow effect
|
||||
Rectangle {
|
||||
anchors.centerIn: parent
|
||||
width: parent.width + 2
|
||||
height: parent.height + 2
|
||||
radius: (parent.width + 2) / 2
|
||||
color: "transparent"
|
||||
border.color: Qt.rgba(0, 0, 0, 0.1)
|
||||
border.width: 1
|
||||
z: -1
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: wifiToggleArea
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
NetworkService.toggleWifiRadio();
|
||||
// Refresh network status and WiFi info after toggle with delay
|
||||
refreshTimer.triggered = true;
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Theme.standardEasing
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Theme.standardEasing
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Force WiFi preference button
|
||||
Rectangle {
|
||||
width: 150
|
||||
height: 30
|
||||
color: networkTab.networkStatus === "wifi" ? Theme.primary : Theme.surface
|
||||
border.color: Theme.primary
|
||||
border.width: 1
|
||||
radius: 6
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: Theme.spacingL + 48 + Theme.spacingM
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
opacity: networkTab.changingNetworkPreference ? 0.6 : 1
|
||||
visible: NetworkService.networkStatus !== "wifi" && NetworkService.ethernetConnected && NetworkService.wifiEnabled
|
||||
|
||||
Row {
|
||||
anchors.centerIn: parent
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
Text {
|
||||
id: wifiPreferenceIcon
|
||||
|
||||
text: networkTab.changingNetworkPreference ? "sync" : ""
|
||||
font.family: Theme.iconFont
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: networkTab.networkStatus === "wifi" ? Theme.background : Theme.primary
|
||||
visible: networkTab.changingNetworkPreference
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
rotation: networkTab.changingNetworkPreference ? wifiPreferenceIcon.rotation : 0
|
||||
|
||||
RotationAnimation {
|
||||
target: wifiPreferenceIcon
|
||||
property: "rotation"
|
||||
running: networkTab.changingNetworkPreference
|
||||
from: 0
|
||||
to: 360
|
||||
duration: 1000
|
||||
loops: Animation.Infinite
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Text {
|
||||
text: NetworkService.changingNetworkPreference ? "Switching..." : "Prefer over Ethernet"
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: NetworkService.networkStatus === "wifi" ? Theme.background : Theme.primary
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
font.weight: Font.Medium
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
propagateComposedEvents: false
|
||||
enabled: !networkTab.changingNetworkPreference
|
||||
onClicked: {
|
||||
console.log("Force WiFi preference clicked");
|
||||
if (NetworkService.networkStatus !== "wifi")
|
||||
NetworkService.setNetworkPreference("wifi");
|
||||
else
|
||||
NetworkService.setNetworkPreference("auto");
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Theme.standardEasing
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Available WiFi Networks
|
||||
Column {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingS
|
||||
visible: NetworkService.wifiEnabled
|
||||
|
||||
Row {
|
||||
width: parent.width
|
||||
|
||||
Text {
|
||||
text: "Available Networks"
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
}
|
||||
|
||||
Item {
|
||||
width: parent.width - 200
|
||||
height: 1
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: 32
|
||||
height: 32
|
||||
radius: 16
|
||||
color: refreshArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : WifiService.isScanning ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.06) : "transparent"
|
||||
|
||||
Text {
|
||||
id: refreshIcon
|
||||
|
||||
anchors.centerIn: parent
|
||||
text: WifiService.isScanning ? "sync" : "refresh"
|
||||
font.family: Theme.iconFont
|
||||
font.pixelSize: Theme.iconSize - 4
|
||||
color: Theme.surfaceText
|
||||
rotation: WifiService.isScanning ? refreshIcon.rotation : 0
|
||||
|
||||
RotationAnimation {
|
||||
target: refreshIcon
|
||||
property: "rotation"
|
||||
running: WifiService.isScanning
|
||||
from: 0
|
||||
to: 360
|
||||
duration: 1000
|
||||
loops: Animation.Infinite
|
||||
}
|
||||
|
||||
Behavior on rotation {
|
||||
RotationAnimation {
|
||||
duration: 200
|
||||
easing.type: Easing.OutQuad
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: refreshArea
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
enabled: !WifiService.isScanning
|
||||
onClicked: {
|
||||
if (NetworkService.wifiEnabled)
|
||||
WifiService.scanWifi();
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Connection status indicator
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: 32
|
||||
radius: Theme.cornerRadius
|
||||
color: {
|
||||
if (WifiService.connectionStatus === "connecting")
|
||||
return Qt.rgba(Theme.warning.r, Theme.warning.g, Theme.warning.b, 0.12);
|
||||
else if (WifiService.connectionStatus === "failed")
|
||||
return Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.12);
|
||||
else if (WifiService.connectionStatus === "connected")
|
||||
return Qt.rgba(Theme.success.r, Theme.success.g, Theme.success.b, 0.12);
|
||||
return "transparent";
|
||||
}
|
||||
border.color: {
|
||||
if (WifiService.connectionStatus === "connecting")
|
||||
return Qt.rgba(Theme.warning.r, Theme.warning.g, Theme.warning.b, 0.3);
|
||||
else if (WifiService.connectionStatus === "failed")
|
||||
return Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.3);
|
||||
else if (WifiService.connectionStatus === "connected")
|
||||
return Qt.rgba(Theme.success.r, Theme.success.g, Theme.success.b, 0.3);
|
||||
return "transparent";
|
||||
}
|
||||
border.width: WifiService.connectionStatus !== "" ? 1 : 0
|
||||
visible: WifiService.connectionStatus !== ""
|
||||
|
||||
Row {
|
||||
anchors.centerIn: parent
|
||||
spacing: Theme.spacingS
|
||||
|
||||
Text {
|
||||
id: connectionIcon
|
||||
|
||||
text: {
|
||||
if (WifiService.connectionStatus === "connecting")
|
||||
return "sync";
|
||||
|
||||
if (WifiService.connectionStatus === "failed")
|
||||
return "error";
|
||||
|
||||
if (WifiService.connectionStatus === "connected")
|
||||
return "check_circle";
|
||||
|
||||
return "";
|
||||
}
|
||||
font.family: Theme.iconFont
|
||||
font.pixelSize: Theme.iconSize - 6
|
||||
color: {
|
||||
if (WifiService.connectionStatus === "connecting")
|
||||
return Theme.warning;
|
||||
|
||||
if (WifiService.connectionStatus === "failed")
|
||||
return Theme.error;
|
||||
|
||||
if (WifiService.connectionStatus === "connected")
|
||||
return Theme.success;
|
||||
|
||||
return Theme.surfaceText;
|
||||
}
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
rotation: WifiService.connectionStatus === "connecting" ? connectionIcon.rotation : 0
|
||||
|
||||
RotationAnimation {
|
||||
target: connectionIcon
|
||||
property: "rotation"
|
||||
running: WifiService.connectionStatus === "connecting"
|
||||
from: 0
|
||||
to: 360
|
||||
duration: 1000
|
||||
loops: Animation.Infinite
|
||||
}
|
||||
|
||||
Behavior on rotation {
|
||||
RotationAnimation {
|
||||
duration: 200
|
||||
easing.type: Easing.OutQuad
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Text {
|
||||
text: {
|
||||
if (WifiService.connectionStatus === "connecting")
|
||||
return "Connecting to " + WifiService.connectingSSID;
|
||||
|
||||
if (WifiService.connectionStatus === "failed")
|
||||
return "Failed to connect to " + WifiService.connectingSSID;
|
||||
|
||||
if (WifiService.connectionStatus === "connected")
|
||||
return "Connected to " + WifiService.connectingSSID;
|
||||
|
||||
return "";
|
||||
}
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: {
|
||||
if (WifiService.connectionStatus === "connecting")
|
||||
return Theme.warning;
|
||||
|
||||
if (WifiService.connectionStatus === "failed")
|
||||
return Theme.error;
|
||||
|
||||
if (WifiService.connectionStatus === "connected")
|
||||
return Theme.success;
|
||||
|
||||
return Theme.surfaceText;
|
||||
}
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Theme.standardEasing
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// WiFi networks list (only show if WiFi is available and enabled)
|
||||
Repeater {
|
||||
model: NetworkService.wifiAvailable && NetworkService.wifiEnabled ? WifiService.wifiNetworks : []
|
||||
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: 42
|
||||
radius: Theme.cornerRadiusSmall
|
||||
color: networkArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : modelData.connected ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
|
||||
border.color: modelData.connected ? Theme.primary : "transparent"
|
||||
border.width: modelData.connected ? 1 : 0
|
||||
|
||||
Item {
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingS
|
||||
|
||||
// Signal strength icon
|
||||
Text {
|
||||
id: signalIcon
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: modelData.signalStrength === "excellent" ? "wifi" : modelData.signalStrength === "good" ? "wifi_2_bar" : modelData.signalStrength === "fair" ? "wifi_1_bar" : modelData.signalStrength === "poor" ? "wifi_calling_3" : "wifi"
|
||||
font.family: Theme.iconFont
|
||||
font.pixelSize: Theme.iconSize
|
||||
color: modelData.connected ? Theme.primary : Theme.surfaceText
|
||||
}
|
||||
|
||||
// Network info
|
||||
Column {
|
||||
anchors.left: signalIcon.right
|
||||
anchors.leftMargin: Theme.spacingS
|
||||
anchors.right: rightIcons.left
|
||||
anchors.rightMargin: Theme.spacingS
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: 2
|
||||
|
||||
Text {
|
||||
width: parent.width
|
||||
text: modelData.ssid
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: modelData.connected ? Theme.primary : Theme.surfaceText
|
||||
font.weight: modelData.connected ? Font.Medium : Font.Normal
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
|
||||
Text {
|
||||
width: parent.width
|
||||
text: {
|
||||
if (modelData.connected)
|
||||
return "Connected";
|
||||
|
||||
if (modelData.saved)
|
||||
return "Saved" + (modelData.secured ? " • Secured" : " • Open");
|
||||
|
||||
return modelData.secured ? "Secured" : "Open";
|
||||
}
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Right side icons
|
||||
Row {
|
||||
id: rightIcons
|
||||
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
// Lock icon (if secured)
|
||||
Text {
|
||||
text: "lock"
|
||||
font.family: Theme.iconFont
|
||||
font.pixelSize: Theme.iconSize - 6
|
||||
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.6)
|
||||
visible: modelData.secured
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
// Forget button (for saved networks)
|
||||
Rectangle {
|
||||
width: 28
|
||||
height: 28
|
||||
radius: 14
|
||||
color: forgetArea.containsMouse ? Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.12) : "transparent"
|
||||
visible: modelData.saved || modelData.connected
|
||||
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
text: "delete"
|
||||
font.family: Theme.iconFont
|
||||
font.pixelSize: Theme.iconSize - 6
|
||||
color: forgetArea.containsMouse ? Theme.error : Theme.surfaceText
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: forgetArea
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
WifiService.forgetWifiNetwork(modelData.ssid);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
// Already connected, do nothing or show info
|
||||
|
||||
id: networkArea
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
if (modelData.connected)
|
||||
return ;
|
||||
|
||||
if (modelData.saved) {
|
||||
// Saved network, connect directly
|
||||
WifiService.connectToWifi(modelData.ssid);
|
||||
} else if (modelData.secured) {
|
||||
// Secured network, need password - use root dialog
|
||||
wifiPasswordDialog.wifiPasswordSSID = modelData.ssid;
|
||||
wifiPasswordDialog.wifiPasswordInput = "";
|
||||
wifiPasswordDialog.wifiPasswordDialogVisible = true;
|
||||
} else {
|
||||
// Open network, connect directly
|
||||
WifiService.connectToWifi(modelData.ssid);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// WiFi disabled message
|
||||
Column {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingM
|
||||
visible: !NetworkService.wifiEnabled
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
|
||||
Text {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
text: "wifi_off"
|
||||
font.family: Theme.iconFont
|
||||
font.pixelSize: 48
|
||||
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.3)
|
||||
}
|
||||
|
||||
Text {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
text: "WiFi is turned off"
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.6)
|
||||
}
|
||||
|
||||
Text {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
text: "Turn on WiFi to see available networks"
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.4)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
ScrollBar.vertical: ScrollBar {
|
||||
policy: ScrollBar.AsNeeded
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Timer for refreshing network status after WiFi toggle
|
||||
Timer {
|
||||
id: refreshTimer
|
||||
interval: 2000
|
||||
running: networkTab.visible && refreshTimer.triggered
|
||||
property bool triggered: false
|
||||
onTriggered: {
|
||||
NetworkService.refreshNetworkStatus();
|
||||
if (NetworkService.wifiEnabled) {
|
||||
WifiService.scanWifi();
|
||||
}
|
||||
triggered = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Auto-refresh when WiFi state changes
|
||||
Connections {
|
||||
target: NetworkService
|
||||
function onWifiEnabledChanged() {
|
||||
if (NetworkService.wifiEnabled && networkTab.visible) {
|
||||
// When WiFi is enabled, scan and update info (only if tab is visible)
|
||||
WifiService.scanWifi();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,63 +0,0 @@
|
||||
import "."
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
|
||||
Rectangle {
|
||||
id: cpuWidget
|
||||
|
||||
property bool showPercentage: true
|
||||
property bool showIcon: true
|
||||
|
||||
width: 55
|
||||
height: 30
|
||||
radius: Theme.cornerRadius
|
||||
color: cpuArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.16) : Qt.rgba(Theme.secondary.r, Theme.secondary.g, Theme.secondary.b, 0.08)
|
||||
|
||||
MouseArea {
|
||||
id: cpuArea
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
ProcessMonitorService.setSortBy("cpu");
|
||||
processListDropdown.toggle();
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
anchors.centerIn: parent
|
||||
spacing: 3
|
||||
|
||||
// CPU icon
|
||||
Text {
|
||||
text: "memory" // Material Design memory icon (swapped from RAM widget)
|
||||
font.family: Theme.iconFont
|
||||
font.pixelSize: Theme.iconSize - 8
|
||||
font.weight: Theme.iconFontWeight
|
||||
color: {
|
||||
if (SystemMonitorService.cpuUsage > 80)
|
||||
return Theme.error;
|
||||
|
||||
if (SystemMonitorService.cpuUsage > 60)
|
||||
return Theme.warning;
|
||||
|
||||
return Theme.surfaceText;
|
||||
}
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
// Percentage text
|
||||
Text {
|
||||
text: (SystemMonitorService.cpuUsage || 0).toFixed(0) + "%"
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.weight: Font.Medium
|
||||
color: Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
148
Widgets/DankGridView.qml
Normal file
148
Widgets/DankGridView.qml
Normal file
@@ -0,0 +1,148 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import Quickshell
|
||||
import Quickshell.Widgets
|
||||
import qs.Common
|
||||
|
||||
ScrollView {
|
||||
id: gridView
|
||||
|
||||
property alias model: grid.model
|
||||
property int currentIndex: 0
|
||||
property int columns: 4
|
||||
property bool adaptiveColumns: false
|
||||
property int minCellWidth: 120
|
||||
property int maxCellWidth: 160
|
||||
property int cellPadding: 8
|
||||
property real iconSizeRatio: 0.6
|
||||
property int maxIconSize: 56
|
||||
property int minIconSize: 32
|
||||
property real wheelStepSize: 60
|
||||
|
||||
signal itemClicked(int index, var modelData)
|
||||
signal itemHovered(int index)
|
||||
|
||||
clip: true
|
||||
ScrollBar.vertical.policy: ScrollBar.AsNeeded
|
||||
ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
|
||||
|
||||
GridView {
|
||||
id: grid
|
||||
|
||||
property int baseCellWidth: adaptiveColumns ?
|
||||
Math.max(minCellWidth, Math.min(maxCellWidth, width / columns)) :
|
||||
(width - Theme.spacingS * 2) / columns
|
||||
property int baseCellHeight: baseCellWidth + 20
|
||||
property int actualColumns: adaptiveColumns ? Math.floor(width / cellWidth) : columns
|
||||
property int remainingSpace: width - (actualColumns * cellWidth)
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingS
|
||||
cellWidth: baseCellWidth
|
||||
cellHeight: baseCellHeight
|
||||
leftMargin: Math.max(Theme.spacingS, remainingSpace / 2)
|
||||
rightMargin: leftMargin
|
||||
focus: true
|
||||
interactive: true
|
||||
flickDeceleration: 8000
|
||||
maximumFlickVelocity: 15000
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
acceptedButtons: Qt.NoButton
|
||||
propagateComposedEvents: true
|
||||
z: -1
|
||||
onWheel: function(wheel) {
|
||||
var delta = wheel.angleDelta.y;
|
||||
var steps = delta / 120;
|
||||
grid.contentY -= steps * wheelStepSize;
|
||||
if (grid.contentY < 0)
|
||||
grid.contentY = 0;
|
||||
else if (grid.contentY > grid.contentHeight - grid.height)
|
||||
grid.contentY = Math.max(0, grid.contentHeight - grid.height);
|
||||
}
|
||||
}
|
||||
|
||||
delegate: Rectangle {
|
||||
width: grid.cellWidth - cellPadding
|
||||
height: grid.cellHeight - cellPadding
|
||||
radius: Theme.cornerRadiusLarge
|
||||
color: currentIndex === index ?
|
||||
Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.16) :
|
||||
mouseArea.containsMouse ?
|
||||
Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) :
|
||||
Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.03)
|
||||
border.color: currentIndex === index ?
|
||||
Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.3) :
|
||||
Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
|
||||
border.width: currentIndex === index ? 2 : 1
|
||||
|
||||
Column {
|
||||
anchors.centerIn: parent
|
||||
spacing: Theme.spacingS
|
||||
|
||||
Item {
|
||||
property int iconSize: Math.min(maxIconSize, Math.max(minIconSize, grid.cellWidth * iconSizeRatio))
|
||||
|
||||
width: iconSize
|
||||
height: iconSize
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
|
||||
IconImage {
|
||||
id: iconImg
|
||||
anchors.fill: parent
|
||||
source: (model.icon) ? Quickshell.iconPath(model.icon, "") : ""
|
||||
smooth: true
|
||||
asynchronous: true
|
||||
visible: status === Image.Ready
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
visible: !iconImg.visible
|
||||
color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.1)
|
||||
radius: Theme.cornerRadiusLarge
|
||||
border.width: 1
|
||||
border.color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.2)
|
||||
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
text: (model.name && model.name.length > 0) ? model.name.charAt(0).toUpperCase() : "A"
|
||||
font.pixelSize: Math.min(28, parent.width * 0.5)
|
||||
color: Theme.primary
|
||||
font.weight: Font.Bold
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Text {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
width: grid.cellWidth - 12
|
||||
text: model.name || ""
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
elide: Text.ElideRight
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
maximumLineCount: 2
|
||||
wrapMode: Text.WordWrap
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: mouseArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
z: 10
|
||||
onEntered: {
|
||||
currentIndex = index;
|
||||
itemHovered(index);
|
||||
}
|
||||
onClicked: {
|
||||
itemClicked(index, model);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
26
Widgets/DankIcon.qml
Normal file
26
Widgets/DankIcon.qml
Normal file
@@ -0,0 +1,26 @@
|
||||
import QtQuick
|
||||
import qs.Common
|
||||
|
||||
Text {
|
||||
id: icon
|
||||
|
||||
property alias name: icon.text
|
||||
property alias size: icon.font.pixelSize
|
||||
property alias color: icon.color
|
||||
property bool filled: false
|
||||
readonly property string iconFont : {
|
||||
var families = Qt.fontFamilies();
|
||||
if (families.indexOf("Material Symbols Rounded") !== -1) {
|
||||
return "Material Symbols Rounded";
|
||||
} else {
|
||||
return "Material Icons Round";
|
||||
}
|
||||
}
|
||||
|
||||
font.family: iconFont
|
||||
font.pixelSize: Theme.iconSize
|
||||
font.weight: filled ? Font.Medium : Font.Normal
|
||||
color: Theme.surfaceText
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
}
|
||||
145
Widgets/DankListView.qml
Normal file
145
Widgets/DankListView.qml
Normal file
@@ -0,0 +1,145 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import Quickshell
|
||||
import Quickshell.Widgets
|
||||
import qs.Common
|
||||
|
||||
ScrollView {
|
||||
id: listView
|
||||
|
||||
property alias model: list.model
|
||||
property int currentIndex: 0
|
||||
property int itemHeight: 72
|
||||
property int iconSize: 56
|
||||
property real wheelStepSize: 60
|
||||
property bool showDescription: true
|
||||
property int itemSpacing: Theme.spacingS
|
||||
|
||||
signal itemClicked(int index, var modelData)
|
||||
signal itemHovered(int index)
|
||||
|
||||
clip: true
|
||||
ScrollBar.vertical.policy: ScrollBar.AlwaysOn
|
||||
ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
|
||||
|
||||
ListView {
|
||||
id: list
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.margins: itemSpacing
|
||||
spacing: listView.itemSpacing
|
||||
focus: true
|
||||
interactive: true
|
||||
currentIndex: listView.currentIndex
|
||||
flickDeceleration: 8000
|
||||
maximumFlickVelocity: 15000
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
acceptedButtons: Qt.NoButton
|
||||
propagateComposedEvents: true
|
||||
z: -1
|
||||
onWheel: function(wheel) {
|
||||
var delta = wheel.angleDelta.y;
|
||||
var steps = delta / 120;
|
||||
list.contentY -= steps * wheelStepSize;
|
||||
if (list.contentY < 0)
|
||||
list.contentY = 0;
|
||||
else if (list.contentY > list.contentHeight - list.height)
|
||||
list.contentY = Math.max(0, list.contentHeight - list.height);
|
||||
}
|
||||
}
|
||||
|
||||
delegate: Rectangle {
|
||||
width: list.width
|
||||
height: itemHeight
|
||||
radius: Theme.cornerRadiusLarge
|
||||
color: ListView.isCurrentItem ?
|
||||
Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.16) :
|
||||
mouseArea.containsMouse ?
|
||||
Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) :
|
||||
Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.03)
|
||||
border.color: ListView.isCurrentItem ?
|
||||
Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.3) :
|
||||
Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
|
||||
border.width: ListView.isCurrentItem ? 2 : 1
|
||||
|
||||
Row {
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingM
|
||||
spacing: Theme.spacingL
|
||||
|
||||
Item {
|
||||
width: iconSize
|
||||
height: iconSize
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
IconImage {
|
||||
id: iconImg
|
||||
anchors.fill: parent
|
||||
source: (model.icon) ? Quickshell.iconPath(model.icon, "") : ""
|
||||
smooth: true
|
||||
asynchronous: true
|
||||
visible: status === Image.Ready
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
visible: !iconImg.visible
|
||||
color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.1)
|
||||
radius: Theme.cornerRadiusLarge
|
||||
border.width: 1
|
||||
border.color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.2)
|
||||
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
text: (model.name && model.name.length > 0) ? model.name.charAt(0).toUpperCase() : "A"
|
||||
font.pixelSize: iconSize * 0.4
|
||||
color: Theme.primary
|
||||
font.weight: Font.Bold
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
width: parent.width - iconSize - Theme.spacingL
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
Text {
|
||||
width: parent.width
|
||||
text: model.name || ""
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
|
||||
Text {
|
||||
width: parent.width
|
||||
text: model.comment || "Application"
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceVariantText
|
||||
elide: Text.ElideRight
|
||||
visible: showDescription && model.comment && model.comment.length > 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: mouseArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
z: 10
|
||||
onEntered: {
|
||||
listView.currentIndex = index;
|
||||
itemHovered(index);
|
||||
}
|
||||
onClicked: {
|
||||
itemClicked(index, model);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import QtQuick
|
||||
import qs.Common
|
||||
import qs.Widgets
|
||||
|
||||
Item {
|
||||
id: slider
|
||||
@@ -40,10 +41,9 @@ Item {
|
||||
spacing: Theme.spacingM
|
||||
|
||||
// Left icon
|
||||
Text {
|
||||
text: slider.leftIcon
|
||||
font.family: Theme.iconFont
|
||||
font.pixelSize: Theme.iconSize
|
||||
DankIcon {
|
||||
name: slider.leftIcon
|
||||
size: Theme.iconSize
|
||||
color: slider.enabled ? Theme.surfaceText : Theme.surfaceVariantText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
visible: slider.leftIcon.length > 0
|
||||
@@ -205,10 +205,9 @@ Item {
|
||||
}
|
||||
|
||||
// Right icon
|
||||
Text {
|
||||
text: slider.rightIcon
|
||||
font.family: Theme.iconFont
|
||||
font.pixelSize: Theme.iconSize
|
||||
DankIcon {
|
||||
name: slider.rightIcon
|
||||
size: Theme.iconSize
|
||||
color: slider.enabled ? Theme.surfaceText : Theme.surfaceVariantText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
visible: slider.rightIcon.length > 0
|
||||
87
Widgets/DankTabBar.qml
Normal file
87
Widgets/DankTabBar.qml
Normal file
@@ -0,0 +1,87 @@
|
||||
import QtQuick
|
||||
import qs.Common
|
||||
import qs.Widgets
|
||||
|
||||
Item {
|
||||
id: tabBar
|
||||
|
||||
property alias model: tabRepeater.model
|
||||
property int currentIndex: 0
|
||||
property int spacing: Theme.spacingXS
|
||||
property int tabHeight: 36
|
||||
property bool showIcons: true
|
||||
property bool equalWidthTabs: true
|
||||
|
||||
signal tabClicked(int index)
|
||||
|
||||
height: tabHeight
|
||||
|
||||
Row {
|
||||
id: tabRow
|
||||
anchors.fill: parent
|
||||
spacing: tabBar.spacing
|
||||
|
||||
Repeater {
|
||||
id: tabRepeater
|
||||
|
||||
Rectangle {
|
||||
property int tabCount: tabRepeater.count
|
||||
property bool isActive: tabBar.currentIndex === index
|
||||
property bool hasIcon: tabBar.showIcons && modelData.icon && modelData.icon.length > 0
|
||||
property bool hasText: modelData.text && modelData.text.length > 0
|
||||
|
||||
width: tabBar.equalWidthTabs ?
|
||||
(tabBar.width - tabBar.spacing * (tabCount - 1)) / tabCount :
|
||||
contentRow.implicitWidth + Theme.spacingM * 2
|
||||
height: tabBar.tabHeight
|
||||
radius: Theme.cornerRadiusSmall
|
||||
color: isActive ?
|
||||
Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.16) :
|
||||
tabArea.containsMouse ?
|
||||
Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) :
|
||||
"transparent"
|
||||
|
||||
Row {
|
||||
id: contentRow
|
||||
anchors.centerIn: parent
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
DankIcon {
|
||||
name: modelData.icon || ""
|
||||
size: Theme.iconSize - 4
|
||||
color: parent.parent.isActive ? Theme.primary : Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
visible: parent.parent.hasIcon
|
||||
}
|
||||
|
||||
Text {
|
||||
text: modelData.text || ""
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: parent.parent.isActive ? Theme.primary : Theme.surfaceText
|
||||
font.weight: parent.parent.isActive ? Font.Medium : Font.Normal
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
visible: parent.parent.hasText
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: tabArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
tabBar.currentIndex = index
|
||||
tabBar.tabClicked(index)
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Theme.standardEasing
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
75
Widgets/DankToggle.qml
Normal file
75
Widgets/DankToggle.qml
Normal file
@@ -0,0 +1,75 @@
|
||||
import QtQuick
|
||||
import qs.Common
|
||||
|
||||
Item {
|
||||
id: toggle
|
||||
|
||||
property bool checked: false
|
||||
property bool enabled: true
|
||||
property bool toggling: false
|
||||
|
||||
signal clicked()
|
||||
|
||||
width: 48
|
||||
height: 24
|
||||
|
||||
Rectangle {
|
||||
id: toggleTrack
|
||||
width: parent.width
|
||||
height: parent.height
|
||||
radius: height / 2
|
||||
color: toggle.checked ? Theme.primary : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.3)
|
||||
opacity: toggle.toggling ? 0.6 : 1
|
||||
|
||||
Rectangle {
|
||||
id: toggleHandle
|
||||
width: 20
|
||||
height: 20
|
||||
radius: 10
|
||||
color: Theme.surface
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
x: toggle.checked ? parent.width - width - 2 : 2
|
||||
|
||||
Behavior on x {
|
||||
NumberAnimation {
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Theme.emphasizedEasing
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.centerIn: parent
|
||||
width: parent.width + 2
|
||||
height: parent.height + 2
|
||||
radius: (parent.width + 2) / 2
|
||||
color: "transparent"
|
||||
border.color: Qt.rgba(0, 0, 0, 0.1)
|
||||
border.width: 1
|
||||
z: -1
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: toggleArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: toggle.enabled ? Qt.PointingHandCursor : Qt.ArrowCursor
|
||||
enabled: toggle.enabled
|
||||
onClicked: toggle.clicked()
|
||||
}
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Theme.standardEasing
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Theme.standardEasing
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,361 +0,0 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import Quickshell
|
||||
import Quickshell.Wayland
|
||||
import Quickshell.Widgets
|
||||
import qs.Common
|
||||
|
||||
PanelWindow {
|
||||
id: inputDialog
|
||||
|
||||
property bool dialogVisible: false
|
||||
property string dialogTitle: "Input Required"
|
||||
property string dialogSubtitle: "Please enter the required information"
|
||||
property string inputPlaceholder: "Enter text"
|
||||
property string inputValue: ""
|
||||
property bool isPassword: false
|
||||
property string confirmButtonText: "Confirm"
|
||||
property string cancelButtonText: "Cancel"
|
||||
|
||||
signal confirmed(string value)
|
||||
signal cancelled()
|
||||
|
||||
function showDialog(title, subtitle, placeholder, isPass, confirmText, cancelText) {
|
||||
dialogTitle = title || "Input Required";
|
||||
dialogSubtitle = subtitle || "Please enter the required information";
|
||||
inputPlaceholder = placeholder || "Enter text";
|
||||
isPassword = isPass || false;
|
||||
confirmButtonText = confirmText || "Confirm";
|
||||
cancelButtonText = cancelText || "Cancel";
|
||||
inputValue = "";
|
||||
dialogVisible = true;
|
||||
}
|
||||
|
||||
function hideDialog() {
|
||||
dialogVisible = false;
|
||||
inputValue = "";
|
||||
}
|
||||
|
||||
visible: dialogVisible
|
||||
WlrLayershell.layer: WlrLayershell.Overlay
|
||||
WlrLayershell.exclusiveZone: -1
|
||||
WlrLayershell.keyboardFocus: dialogVisible ? WlrKeyboardFocus.Exclusive : WlrKeyboardFocus.None
|
||||
color: "transparent"
|
||||
onVisibleChanged: {
|
||||
if (visible) {
|
||||
textInput.forceActiveFocus();
|
||||
textInput.text = inputValue;
|
||||
}
|
||||
}
|
||||
|
||||
anchors {
|
||||
top: true
|
||||
left: true
|
||||
right: true
|
||||
bottom: true
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
color: Qt.rgba(0, 0, 0, 0.5)
|
||||
opacity: dialogVisible ? 1 : 0
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
onClicked: {
|
||||
inputDialog.cancelled();
|
||||
hideDialog();
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: Theme.mediumDuration
|
||||
easing.type: Theme.standardEasing
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: Math.min(400, parent.width - Theme.spacingL * 2)
|
||||
height: Math.min(250, parent.height - Theme.spacingL * 2)
|
||||
anchors.centerIn: parent
|
||||
color: Theme.surfaceContainer
|
||||
radius: Theme.cornerRadiusLarge
|
||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
|
||||
border.width: 1
|
||||
opacity: dialogVisible ? 1 : 0
|
||||
scale: dialogVisible ? 1 : 0.9
|
||||
|
||||
Column {
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingL
|
||||
spacing: Theme.spacingL
|
||||
|
||||
// Header
|
||||
Row {
|
||||
width: parent.width
|
||||
|
||||
Column {
|
||||
width: parent.width - 40
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
Text {
|
||||
text: dialogTitle
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
}
|
||||
|
||||
Text {
|
||||
text: dialogSubtitle
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
|
||||
width: parent.width
|
||||
elide: Text.ElideRight
|
||||
wrapMode: Text.WordWrap
|
||||
maximumLineCount: 2
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: 32
|
||||
height: 32
|
||||
radius: 16
|
||||
color: closeDialogArea.containsMouse ? Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.12) : "transparent"
|
||||
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
text: "close"
|
||||
font.family: Theme.iconFont
|
||||
font.pixelSize: Theme.iconSize - 4
|
||||
color: closeDialogArea.containsMouse ? Theme.error : Theme.surfaceText
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: closeDialogArea
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
inputDialog.cancelled();
|
||||
hideDialog();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Text input
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: 50
|
||||
radius: Theme.cornerRadius
|
||||
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08)
|
||||
border.color: textInput.activeFocus ? Theme.primary : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
|
||||
border.width: textInput.activeFocus ? 2 : 1
|
||||
|
||||
TextInput {
|
||||
id: textInput
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingM
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceText
|
||||
echoMode: isPassword && !showPasswordCheckbox.checked ? TextInput.Password : TextInput.Normal
|
||||
verticalAlignment: TextInput.AlignVCenter
|
||||
cursorVisible: activeFocus
|
||||
selectByMouse: true
|
||||
onTextChanged: {
|
||||
inputValue = text;
|
||||
}
|
||||
onAccepted: {
|
||||
inputDialog.confirmed(inputValue);
|
||||
hideDialog();
|
||||
}
|
||||
Component.onCompleted: {
|
||||
if (dialogVisible)
|
||||
forceActiveFocus();
|
||||
|
||||
}
|
||||
|
||||
Text {
|
||||
anchors.fill: parent
|
||||
text: inputPlaceholder
|
||||
font: parent.font
|
||||
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.5)
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
visible: parent.text.length === 0
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
cursorShape: Qt.IBeamCursor
|
||||
onClicked: {
|
||||
textInput.forceActiveFocus();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Show password checkbox (only visible for password inputs)
|
||||
Row {
|
||||
spacing: Theme.spacingS
|
||||
visible: isPassword
|
||||
|
||||
Rectangle {
|
||||
id: showPasswordCheckbox
|
||||
|
||||
property bool checked: false
|
||||
|
||||
width: 20
|
||||
height: 20
|
||||
radius: 4
|
||||
color: checked ? Theme.primary : "transparent"
|
||||
border.color: checked ? Theme.primary : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.5)
|
||||
border.width: 2
|
||||
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
text: "check"
|
||||
font.family: Theme.iconFont
|
||||
font.pixelSize: 12
|
||||
color: Theme.background
|
||||
visible: parent.checked
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
showPasswordCheckbox.checked = !showPasswordCheckbox.checked;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Text {
|
||||
text: "Show password"
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Buttons
|
||||
Item {
|
||||
width: parent.width
|
||||
height: 40
|
||||
|
||||
Row {
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Theme.spacingM
|
||||
|
||||
Rectangle {
|
||||
width: Math.max(70, cancelText.contentWidth + Theme.spacingM * 2)
|
||||
height: 36
|
||||
radius: Theme.cornerRadius
|
||||
color: cancelArea.containsMouse ? Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.08) : "transparent"
|
||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
|
||||
border.width: 1
|
||||
|
||||
Text {
|
||||
id: cancelText
|
||||
|
||||
anchors.centerIn: parent
|
||||
text: cancelButtonText
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: cancelArea
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
inputDialog.cancelled();
|
||||
hideDialog();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: Math.max(80, confirmText.contentWidth + Theme.spacingM * 2)
|
||||
height: 36
|
||||
radius: Theme.cornerRadius
|
||||
color: confirmArea.containsMouse ? Qt.darker(Theme.primary, 1.1) : Theme.primary
|
||||
enabled: inputValue.length > 0
|
||||
opacity: enabled ? 1 : 0.5
|
||||
|
||||
Text {
|
||||
id: confirmText
|
||||
|
||||
anchors.centerIn: parent
|
||||
text: confirmButtonText
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.background
|
||||
font.weight: Font.Medium
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: confirmArea
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
enabled: parent.enabled
|
||||
onClicked: {
|
||||
inputDialog.confirmed(inputValue);
|
||||
hideDialog();
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Theme.standardEasing
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: Theme.mediumDuration
|
||||
easing.type: Theme.emphasizedEasing
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Behavior on scale {
|
||||
NumberAnimation {
|
||||
duration: Theme.mediumDuration
|
||||
easing.type: Theme.emphasizedEasing
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,847 +0,0 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import Quickshell
|
||||
import Quickshell.Wayland
|
||||
import Quickshell.Widgets
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
|
||||
PanelWindow {
|
||||
id: notificationPopup
|
||||
|
||||
// Expose key child objects for testing
|
||||
// Expose the currently visible quickReplyField for testing
|
||||
property TextField quickReplyField: null
|
||||
// Expose the currently visible iconContainer for testing
|
||||
property Item iconContainer: null
|
||||
// Expose the currently visible expandedContent for testing
|
||||
property Column expandedContent: null
|
||||
// Expose the currently visible hoverArea for testing
|
||||
property MouseArea hoverArea: null
|
||||
|
||||
objectName: "notificationPopup"
|
||||
visible: NotificationService.groupedPopups.length > 0
|
||||
WlrLayershell.layer: WlrLayershell.Overlay
|
||||
WlrLayershell.exclusiveZone: -1
|
||||
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
|
||||
color: "transparent"
|
||||
implicitWidth: 400
|
||||
implicitHeight: notificationsList.height + 32
|
||||
|
||||
anchors {
|
||||
top: true
|
||||
right: true
|
||||
}
|
||||
|
||||
margins {
|
||||
top: Theme.barHeight
|
||||
right: 12
|
||||
}
|
||||
|
||||
Column {
|
||||
id: notificationsList
|
||||
|
||||
anchors.top: parent.top
|
||||
anchors.right: parent.right
|
||||
anchors.topMargin: 16
|
||||
anchors.rightMargin: 16
|
||||
spacing: Theme.spacingM
|
||||
width: 380
|
||||
|
||||
Repeater {
|
||||
model: NotificationService.groupedPopups
|
||||
|
||||
delegate: Rectangle {
|
||||
required property var modelData
|
||||
// Context detection for popup
|
||||
readonly property bool isPopupContext: true
|
||||
readonly property bool expanded: NotificationService.expandedGroups[modelData.key] || false
|
||||
|
||||
width: parent.width
|
||||
height: {
|
||||
let calculatedHeight;
|
||||
if (expanded) {
|
||||
// Calculate expanded height properly: header (48) + spacing + notifications
|
||||
let headerHeight = 48 + Theme.spacingM;
|
||||
let maxNotificationsInPopup = Math.min(modelData.notifications.length, 5);
|
||||
let notificationHeight = maxNotificationsInPopup * (60 + Theme.spacingS);
|
||||
calculatedHeight = headerHeight + notificationHeight + Theme.spacingL * 2;
|
||||
} else {
|
||||
// Collapsed height: header (72) + quick reply if present
|
||||
calculatedHeight = 72 + Theme.spacingS * 2;
|
||||
if (modelData.latestNotification.notification.hasInlineReply)
|
||||
calculatedHeight += 36 + Theme.spacingS;
|
||||
|
||||
calculatedHeight += Theme.spacingL * 2;
|
||||
}
|
||||
// Add extra height for single notifications in popup context
|
||||
if (isPopupContext && modelData.count === 1)
|
||||
calculatedHeight += 12;
|
||||
|
||||
return calculatedHeight;
|
||||
}
|
||||
radius: Theme.cornerRadiusLarge
|
||||
color: Theme.popupBackground()
|
||||
border.color: modelData.latestNotification.urgency === 2 ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.3) : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
|
||||
border.width: modelData.latestNotification.urgency === 2 ? 2 : 1
|
||||
// Stabilize layout during content changes
|
||||
clip: true
|
||||
opacity: notificationPopup.visible ? 1 : 0
|
||||
scale: notificationPopup.visible ? 1 : 0.98
|
||||
|
||||
// Priority indicator for urgent notifications
|
||||
Rectangle {
|
||||
width: 4
|
||||
height: parent.height - 16
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: 2
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
radius: 2
|
||||
color: Theme.primary
|
||||
visible: modelData.latestNotification.urgency === 2
|
||||
}
|
||||
|
||||
// Collapsed view - shows app header and latest notification
|
||||
Column {
|
||||
id: collapsedContent
|
||||
|
||||
anchors.top: parent.top
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.margins: Theme.spacingL
|
||||
spacing: Theme.spacingS
|
||||
visible: !expanded
|
||||
|
||||
// App header with group info
|
||||
Item {
|
||||
width: parent.width
|
||||
height: 72 // Increased height for better text spacing
|
||||
|
||||
// Round app icon with proper API usage
|
||||
Item {
|
||||
id: iconContainer
|
||||
|
||||
Component.onCompleted: {
|
||||
// Expose this iconContainer to the root for testing if visible
|
||||
notificationPopup.iconContainer = iconContainer;
|
||||
}
|
||||
width: 48
|
||||
height: 48
|
||||
anchors.left: parent.left
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
Rectangle {
|
||||
width: 48
|
||||
height: 48
|
||||
radius: 24
|
||||
color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.1)
|
||||
border.color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.3)
|
||||
border.width: 1
|
||||
clip: true
|
||||
|
||||
IconImage {
|
||||
anchors.fill: parent
|
||||
anchors.margins: 6
|
||||
source: {
|
||||
if (modelData.latestNotification.appIcon && modelData.latestNotification.appIcon !== "")
|
||||
return Quickshell.iconPath(modelData.latestNotification.appIcon, "");
|
||||
|
||||
return "";
|
||||
}
|
||||
visible: status === Image.Ready
|
||||
onStatusChanged: {
|
||||
if (status === Image.Error || status === Image.Null || source === "")
|
||||
fallbackIcon.visible = true;
|
||||
else if (status === Image.Ready)
|
||||
fallbackIcon.visible = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback icon - show by default, hide when real icon loads
|
||||
Text {
|
||||
id: fallbackIcon
|
||||
|
||||
anchors.centerIn: parent
|
||||
visible: true // Start visible, hide when real icon loads
|
||||
text: {
|
||||
// Use first letter of app name as fallback
|
||||
const appName = modelData.appName || "?";
|
||||
return appName.charAt(0).toUpperCase();
|
||||
}
|
||||
font.pixelSize: 20
|
||||
font.weight: Font.Bold
|
||||
color: Theme.primaryText
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Count badge for multiple notifications - smaller circle
|
||||
Rectangle {
|
||||
width: 18
|
||||
height: 18
|
||||
radius: 9
|
||||
color: Theme.primary
|
||||
anchors.top: parent.top
|
||||
anchors.right: parent.right
|
||||
anchors.topMargin: -2
|
||||
anchors.rightMargin: -2
|
||||
visible: modelData.count > 1
|
||||
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
text: modelData.count > 99 ? "99+" : modelData.count.toString()
|
||||
color: Theme.primaryText
|
||||
font.pixelSize: 9
|
||||
font.weight: Font.Bold
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// App info and latest notification content
|
||||
Column {
|
||||
anchors.left: iconContainer.right
|
||||
anchors.leftMargin: Theme.spacingM
|
||||
anchors.right: controlsContainer.left
|
||||
anchors.rightMargin: Theme.spacingM
|
||||
anchors.top: parent.top
|
||||
anchors.topMargin: Theme.spacingS
|
||||
spacing: Theme.spacingS
|
||||
|
||||
// App name and timestamp on same line
|
||||
Text {
|
||||
width: parent.width
|
||||
text: {
|
||||
if (modelData.latestNotification.timeStr.length > 0)
|
||||
return modelData.appName + " • " + modelData.latestNotification.timeStr;
|
||||
else
|
||||
return modelData.appName;
|
||||
}
|
||||
color: Theme.surfaceVariantText
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.weight: Font.Medium
|
||||
elide: Text.ElideRight
|
||||
maximumLineCount: 1
|
||||
}
|
||||
|
||||
// Latest notification title (emphasized)
|
||||
Text {
|
||||
text: modelData.latestNotification.summary
|
||||
color: Theme.surfaceText
|
||||
font.pixelSize: Theme.fontSizeMedium + 1 // Slightly larger for emphasis
|
||||
font.weight: Font.Medium
|
||||
width: parent.width
|
||||
elide: Text.ElideRight
|
||||
maximumLineCount: 1
|
||||
visible: text.length > 0
|
||||
}
|
||||
|
||||
// Latest notification body (smaller, secondary)
|
||||
Text {
|
||||
text: modelData.latestNotification.body
|
||||
color: Theme.surfaceVariantText
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
width: parent.width
|
||||
elide: Text.ElideRight
|
||||
maximumLineCount: modelData.count > 1 ? 1 : 2 // More space for single notifications
|
||||
wrapMode: Text.WordWrap
|
||||
visible: text.length > 0
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Expand/dismiss controls - use anchored layout for stability
|
||||
Item {
|
||||
id: controlsContainer
|
||||
|
||||
width: 72
|
||||
height: 32
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
Rectangle {
|
||||
width: 32
|
||||
height: 32
|
||||
radius: 16
|
||||
anchors.left: parent.left
|
||||
color: expandArea.containsMouse ? Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.08) : "transparent"
|
||||
visible: modelData.count > 1
|
||||
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
text: "expand_more"
|
||||
font.family: Theme.iconFont
|
||||
font.pixelSize: 18
|
||||
color: Theme.surfaceText
|
||||
rotation: expanded ? 180 : 0
|
||||
|
||||
Behavior on rotation {
|
||||
NumberAnimation {
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Theme.standardEasing
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
// ...existing code...
|
||||
id: expandArea
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
console.log("Expand clicked - pausing timer");
|
||||
dismissTimer.stop();
|
||||
NotificationService.toggleGroupExpansion(modelData.key);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: 32
|
||||
height: 32
|
||||
radius: 16
|
||||
anchors.right: parent.right
|
||||
color: dismissArea.containsMouse ? Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.08) : "transparent"
|
||||
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
text: "close"
|
||||
font.family: Theme.iconFont
|
||||
font.pixelSize: 16
|
||||
color: Theme.surfaceText
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: dismissArea
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: NotificationService.dismissGroup(modelData.key)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Quick reply for conversations (only if latest notification supports it)
|
||||
Row {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingS
|
||||
visible: modelData.latestNotification.notification.hasInlineReply && !expanded
|
||||
|
||||
Rectangle {
|
||||
width: parent.width - 60
|
||||
height: 36
|
||||
radius: 18
|
||||
color: Theme.surfaceContainer
|
||||
border.color: quickReplyField.activeFocus ? Theme.primary : Theme.outline
|
||||
border.width: 1
|
||||
|
||||
TextField {
|
||||
id: quickReplyField
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingS
|
||||
placeholderText: modelData.latestNotification.notification.inlineReplyPlaceholder || "Quick reply..."
|
||||
color: Theme.surfaceText
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
onAccepted: {
|
||||
if (text.length > 0) {
|
||||
modelData.latestNotification.notification.sendInlineReply(text);
|
||||
text = "";
|
||||
}
|
||||
}
|
||||
|
||||
background: Item {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: 52
|
||||
height: 36
|
||||
radius: 18
|
||||
color: quickReplyField.text.length > 0 ? Theme.primary : Theme.surfaceContainer
|
||||
border.color: quickReplyField.text.length > 0 ? "transparent" : Theme.outline
|
||||
border.width: quickReplyField.text.length > 0 ? 0 : 1
|
||||
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
text: "send"
|
||||
font.family: Theme.iconFont
|
||||
font.pixelSize: 16
|
||||
color: quickReplyField.text.length > 0 ? Theme.primaryText : Theme.surfaceVariantText
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
enabled: quickReplyField.text.length > 0
|
||||
cursorShape: enabled ? Qt.PointingHandCursor : Qt.ArrowCursor
|
||||
onClicked: {
|
||||
modelData.latestNotification.notification.sendInlineReply(quickReplyField.text);
|
||||
quickReplyField.text = "";
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Theme.standardEasing
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Expanded view - shows all notifications stacked
|
||||
Column {
|
||||
id: expandedContent
|
||||
|
||||
Component.onCompleted: {
|
||||
// Expose this expandedContent to the root for testing if visible
|
||||
notificationPopup.expandedContent = expandedContent;
|
||||
}
|
||||
anchors.top: parent.top
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.margins: Theme.spacingL
|
||||
spacing: Theme.spacingM
|
||||
visible: expanded
|
||||
|
||||
// Group header with fixed anchored positioning
|
||||
Item {
|
||||
width: parent.width
|
||||
height: 48
|
||||
|
||||
// Round app icon - fixed position on left
|
||||
Rectangle {
|
||||
width: 40
|
||||
height: 40
|
||||
radius: 20
|
||||
anchors.left: parent.left
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.1)
|
||||
border.color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.3)
|
||||
border.width: 1
|
||||
clip: true
|
||||
|
||||
IconImage {
|
||||
anchors.fill: parent
|
||||
anchors.margins: 4
|
||||
source: modelData.latestNotification.appIcon ? Quickshell.iconPath(modelData.latestNotification.appIcon, "") : ""
|
||||
visible: status === Image.Ready
|
||||
}
|
||||
|
||||
// Fallback for expanded view
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
visible: !modelData.latestNotification.appIcon || modelData.latestNotification.appIcon === ""
|
||||
text: {
|
||||
const appName = modelData.appName || "?";
|
||||
return appName.charAt(0).toUpperCase();
|
||||
}
|
||||
font.pixelSize: 16
|
||||
font.weight: Font.Bold
|
||||
color: Theme.primaryText
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// App name and count badge - centered area
|
||||
Text {
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: 52
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: modelData.appName
|
||||
color: Theme.surfaceText
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
font.weight: Font.Bold
|
||||
}
|
||||
|
||||
// Controls container - fixed position on right
|
||||
Item {
|
||||
width: 72
|
||||
height: 32
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
Rectangle {
|
||||
width: 32
|
||||
height: 32
|
||||
radius: 16
|
||||
anchors.left: parent.left
|
||||
color: collapseArea.containsMouse ? Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.08) : "transparent"
|
||||
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
text: "expand_less"
|
||||
font.family: Theme.iconFont
|
||||
font.pixelSize: 18
|
||||
color: Theme.surfaceText
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: collapseArea
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
console.log("Expand clicked - pausing timer");
|
||||
dismissTimer.stop();
|
||||
NotificationService.toggleGroupExpansion(modelData.key);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: 32
|
||||
height: 32
|
||||
radius: 16
|
||||
anchors.right: parent.right
|
||||
color: dismissAllArea.containsMouse ? Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.08) : "transparent"
|
||||
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
text: "close"
|
||||
font.family: Theme.iconFont
|
||||
font.pixelSize: 16
|
||||
color: Theme.surfaceText
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: dismissAllArea
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: NotificationService.dismissGroup(modelData.key)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Stacked individual notifications with smooth transitions
|
||||
Column {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingS
|
||||
|
||||
Repeater {
|
||||
model: modelData.notifications.slice(0, 5) // Show max 5 in popup
|
||||
|
||||
delegate: Rectangle {
|
||||
required property var modelData
|
||||
|
||||
width: parent.width
|
||||
height: notifContent.height + Theme.spacingM * 2
|
||||
radius: Theme.cornerRadius
|
||||
color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.5)
|
||||
border.color: modelData.urgency === 2 ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.2) : "transparent"
|
||||
border.width: modelData.urgency === 2 ? 1 : 0
|
||||
// Stabilize layout during dismiss operations
|
||||
clip: true
|
||||
|
||||
Item {
|
||||
id: notifContent
|
||||
|
||||
anchors.top: parent.top
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.margins: Theme.spacingM
|
||||
height: Math.max(32, contentColumn.height)
|
||||
|
||||
// Small round notification icon/avatar - fixed position on left
|
||||
Rectangle {
|
||||
width: 32
|
||||
height: 32
|
||||
radius: 16
|
||||
anchors.left: parent.left
|
||||
anchors.top: parent.top
|
||||
color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.1)
|
||||
border.color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.2)
|
||||
border.width: 1
|
||||
clip: true
|
||||
|
||||
IconImage {
|
||||
anchors.fill: parent
|
||||
anchors.margins: 3
|
||||
source: modelData.appIcon ? Quickshell.iconPath(modelData.appIcon, "") : ""
|
||||
visible: status === Image.Ready
|
||||
}
|
||||
|
||||
// Fallback for individual notifications
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
visible: !modelData.appIcon || modelData.appIcon === ""
|
||||
text: {
|
||||
const appName = modelData.appName || "?";
|
||||
return appName.charAt(0).toUpperCase();
|
||||
}
|
||||
font.pixelSize: 12
|
||||
font.weight: Font.Bold
|
||||
color: Theme.primaryText
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Individual dismiss button - fixed position on right
|
||||
Rectangle {
|
||||
width: 24
|
||||
height: 24
|
||||
radius: 12
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
color: individualDismissArea.containsMouse ? Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.08) : "transparent"
|
||||
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
text: "close"
|
||||
font.family: Theme.iconFont
|
||||
font.pixelSize: 12
|
||||
color: Theme.surfaceVariantText
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: individualDismissArea
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: NotificationService.dismissNotification(modelData)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Notification content - fills space between icon and dismiss button
|
||||
Column {
|
||||
id: contentColumn
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: 44
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: 36
|
||||
anchors.top: parent.top
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
// Title and timestamp
|
||||
Text {
|
||||
text: modelData.summary
|
||||
color: Theme.surfaceText
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.weight: Font.Medium
|
||||
width: parent.width
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
|
||||
// Body text
|
||||
Text {
|
||||
text: modelData.body
|
||||
color: Theme.surfaceVariantText
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
width: parent.width
|
||||
wrapMode: Text.WordWrap
|
||||
maximumLineCount: 2
|
||||
elide: Text.ElideRight
|
||||
visible: text.length > 0
|
||||
}
|
||||
|
||||
// Individual notification inline reply
|
||||
Row {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingS
|
||||
visible: modelData.notification.hasInlineReply
|
||||
|
||||
Rectangle {
|
||||
width: parent.width - 50
|
||||
height: 28
|
||||
radius: 14
|
||||
color: Theme.surface
|
||||
border.color: replyField.activeFocus ? Theme.primary : Theme.outline
|
||||
border.width: 1
|
||||
|
||||
TextField {
|
||||
id: replyField
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingXS
|
||||
placeholderText: modelData.notification.inlineReplyPlaceholder || "Reply..."
|
||||
color: Theme.surfaceText
|
||||
font.pixelSize: 11
|
||||
onAccepted: {
|
||||
if (text.length > 0) {
|
||||
modelData.notification.sendInlineReply(text);
|
||||
text = "";
|
||||
}
|
||||
}
|
||||
|
||||
background: Item {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: 42
|
||||
height: 28
|
||||
radius: 14
|
||||
color: replyField.text.length > 0 ? Theme.primary : Theme.surfaceContainer
|
||||
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
text: "send"
|
||||
font.family: Theme.iconFont
|
||||
font.pixelSize: 12
|
||||
color: replyField.text.length > 0 ? Theme.primaryText : Theme.surfaceVariantText
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
enabled: replyField.text.length > 0
|
||||
cursorShape: enabled ? Qt.PointingHandCursor : Qt.ArrowCursor
|
||||
onClicked: {
|
||||
modelData.notification.sendInlineReply(replyField.text);
|
||||
replyField.text = "";
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Hover to pause auto-dismiss - MUST be properly configured
|
||||
MouseArea {
|
||||
id: hoverArea
|
||||
|
||||
Component.onCompleted: {
|
||||
// Expose this hoverArea to the root for testing if visible
|
||||
notificationPopup.hoverArea = hoverArea;
|
||||
}
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
acceptedButtons: Qt.NoButton
|
||||
z: 10 // Higher z-order to ensure hover detection
|
||||
propagateComposedEvents: true
|
||||
onEntered: {
|
||||
console.log("Notification hover entered - pausing timer");
|
||||
dismissTimer.stop();
|
||||
}
|
||||
onExited: {
|
||||
console.log("Notification hover exited - resuming timer");
|
||||
if (modelData.latestNotification.popup && !expanded)
|
||||
dismissTimer.restart();
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// Auto-dismiss timer - properly pauses on hover
|
||||
Timer {
|
||||
id: dismissTimer
|
||||
|
||||
running: modelData.latestNotification.popup && !expanded
|
||||
interval: modelData.latestNotification.notification.expireTimeout > 0 ? modelData.latestNotification.notification.expireTimeout * 1000 : 5000
|
||||
onTriggered: {
|
||||
console.log("Timer triggered - hover state:", hoverArea.containsMouse, "expanded:", expanded);
|
||||
if (!hoverArea.containsMouse && !expanded) {
|
||||
console.log("Dismissing notification");
|
||||
modelData.latestNotification.popup = false;
|
||||
} else {
|
||||
console.log("Conditions not met - not dismissing");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Smooth popup animations
|
||||
transform: Translate {
|
||||
x: notificationPopup.visible ? 0 : 400
|
||||
|
||||
Behavior on x {
|
||||
NumberAnimation {
|
||||
duration: 350
|
||||
easing.type: Easing.OutCubic
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: 300
|
||||
easing.type: Easing.OutCubic
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Behavior on scale {
|
||||
NumberAnimation {
|
||||
duration: 350
|
||||
easing.type: Easing.OutCubic
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Behavior on height {
|
||||
enabled: !isPopupContext // Disable automatic height animation in popup to prevent glitches
|
||||
|
||||
SequentialAnimation {
|
||||
PauseAnimation {
|
||||
duration: 25
|
||||
}
|
||||
|
||||
NumberAnimation {
|
||||
duration: Theme.mediumDuration
|
||||
easing.type: Theme.emphasizedEasing
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
// Smooth height animation
|
||||
|
||||
Behavior on implicitHeight {
|
||||
NumberAnimation {
|
||||
duration: Theme.mediumDuration
|
||||
easing.type: Theme.emphasizedEasing
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,220 +0,0 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import Quickshell.Wayland
|
||||
import Quickshell.Widgets
|
||||
import qs.Common
|
||||
|
||||
PanelWindow {
|
||||
id: root
|
||||
|
||||
property bool powerConfirmVisible: false
|
||||
property string powerConfirmAction: ""
|
||||
property string powerConfirmTitle: ""
|
||||
property string powerConfirmMessage: ""
|
||||
|
||||
function executePowerAction(action) {
|
||||
console.log("Executing power action:", action);
|
||||
let command = [];
|
||||
switch (action) {
|
||||
case "logout":
|
||||
command = ["niri", "msg", "action", "quit", "-s"];
|
||||
break;
|
||||
case "suspend":
|
||||
command = ["systemctl", "suspend"];
|
||||
break;
|
||||
case "reboot":
|
||||
command = ["systemctl", "reboot"];
|
||||
break;
|
||||
case "poweroff":
|
||||
command = ["systemctl", "poweroff"];
|
||||
break;
|
||||
}
|
||||
if (command.length > 0) {
|
||||
powerActionProcess.command = command;
|
||||
powerActionProcess.running = true;
|
||||
}
|
||||
}
|
||||
|
||||
visible: powerConfirmVisible
|
||||
implicitWidth: 400
|
||||
implicitHeight: 300
|
||||
WlrLayershell.layer: WlrLayershell.Overlay
|
||||
WlrLayershell.exclusiveZone: -1
|
||||
WlrLayershell.keyboardFocus: WlrKeyboardFocus.OnDemand
|
||||
color: "transparent"
|
||||
|
||||
anchors {
|
||||
top: true
|
||||
left: true
|
||||
right: true
|
||||
bottom: true
|
||||
}
|
||||
|
||||
// Darkened background
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
color: "black"
|
||||
opacity: 0.5
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: Math.min(400, parent.width - Theme.spacingL * 2)
|
||||
height: Math.min(200, parent.height - Theme.spacingL * 2)
|
||||
anchors.centerIn: parent
|
||||
color: Theme.surfaceContainer
|
||||
radius: Theme.cornerRadiusLarge
|
||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
|
||||
border.width: 1
|
||||
opacity: powerConfirmVisible ? 1 : 0
|
||||
scale: powerConfirmVisible ? 1 : 0.9
|
||||
|
||||
Column {
|
||||
anchors.centerIn: parent
|
||||
width: parent.width - Theme.spacingL * 2
|
||||
spacing: Theme.spacingL
|
||||
|
||||
// Title
|
||||
Text {
|
||||
text: powerConfirmTitle
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
color: {
|
||||
switch (powerConfirmAction) {
|
||||
case "poweroff":
|
||||
return Theme.error;
|
||||
case "reboot":
|
||||
return Theme.warning;
|
||||
default:
|
||||
return Theme.surfaceText;
|
||||
}
|
||||
}
|
||||
font.weight: Font.Medium
|
||||
width: parent.width
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
}
|
||||
|
||||
// Message
|
||||
Text {
|
||||
text: powerConfirmMessage
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceText
|
||||
width: parent.width
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
wrapMode: Text.WordWrap
|
||||
}
|
||||
|
||||
Item {
|
||||
height: Theme.spacingL
|
||||
}
|
||||
|
||||
// Buttons
|
||||
Row {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
spacing: Theme.spacingM
|
||||
|
||||
// Cancel button
|
||||
Rectangle {
|
||||
width: 120
|
||||
height: 40
|
||||
radius: Theme.cornerRadius
|
||||
color: cancelButton.containsMouse ? Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.12) : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.3)
|
||||
|
||||
Text {
|
||||
text: "Cancel"
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
anchors.centerIn: parent
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: cancelButton
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
powerConfirmVisible = false;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Confirm button
|
||||
Rectangle {
|
||||
width: 120
|
||||
height: 40
|
||||
radius: Theme.cornerRadius
|
||||
color: {
|
||||
let baseColor;
|
||||
switch (powerConfirmAction) {
|
||||
case "poweroff":
|
||||
baseColor = Theme.error;
|
||||
break;
|
||||
case "reboot":
|
||||
baseColor = Theme.warning;
|
||||
break;
|
||||
default:
|
||||
baseColor = Theme.primary;
|
||||
break;
|
||||
}
|
||||
return confirmButton.containsMouse ? Qt.rgba(baseColor.r, baseColor.g, baseColor.b, 0.9) : baseColor;
|
||||
}
|
||||
|
||||
Text {
|
||||
text: "Confirm"
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.primaryText
|
||||
font.weight: Font.Medium
|
||||
anchors.centerIn: parent
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: confirmButton
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
powerConfirmVisible = false;
|
||||
executePowerAction(powerConfirmAction);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: Theme.mediumDuration
|
||||
easing.type: Theme.emphasizedEasing
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Behavior on scale {
|
||||
NumberAnimation {
|
||||
duration: Theme.mediumDuration
|
||||
easing.type: Theme.emphasizedEasing
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Process {
|
||||
id: powerActionProcess
|
||||
|
||||
running: false
|
||||
onExited: (exitCode) => {
|
||||
if (exitCode !== 0)
|
||||
console.error("Power action failed with exit code:", exitCode);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,328 +0,0 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import Quickshell.Wayland
|
||||
import Quickshell.Widgets
|
||||
import qs.Common
|
||||
|
||||
PanelWindow {
|
||||
id: root
|
||||
|
||||
property bool powerMenuVisible: false
|
||||
|
||||
visible: powerMenuVisible
|
||||
implicitWidth: 400
|
||||
implicitHeight: 320
|
||||
WlrLayershell.layer: WlrLayershell.Overlay
|
||||
WlrLayershell.exclusiveZone: -1
|
||||
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
|
||||
color: "transparent"
|
||||
|
||||
anchors {
|
||||
top: true
|
||||
left: true
|
||||
right: true
|
||||
bottom: true
|
||||
}
|
||||
|
||||
// Click outside to dismiss overlay
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
onClicked: {
|
||||
powerMenuVisible = false;
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: Math.min(320, parent.width - Theme.spacingL * 2)
|
||||
height: 320 // Fixed height to prevent cropping
|
||||
x: Math.max(Theme.spacingL, parent.width - width - Theme.spacingL)
|
||||
y: Theme.barHeight + Theme.spacingXS
|
||||
color: Theme.popupBackground()
|
||||
radius: Theme.cornerRadiusLarge
|
||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
|
||||
border.width: 1
|
||||
opacity: powerMenuVisible ? 1 : 0
|
||||
scale: powerMenuVisible ? 1 : 0.85
|
||||
|
||||
// Prevent click-through to background
|
||||
MouseArea {
|
||||
// Consume the click to prevent it from reaching the background
|
||||
|
||||
anchors.fill: parent
|
||||
onClicked: {
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingL
|
||||
spacing: Theme.spacingM
|
||||
|
||||
// Header
|
||||
Row {
|
||||
width: parent.width
|
||||
|
||||
Text {
|
||||
text: "Power Options"
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Item {
|
||||
width: parent.width - 150
|
||||
height: 1
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: 32
|
||||
height: 32
|
||||
radius: 16
|
||||
color: closePowerArea.containsMouse ? Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.12) : "transparent"
|
||||
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
text: "close"
|
||||
font.family: Theme.iconFont
|
||||
font.pixelSize: Theme.iconSize - 4
|
||||
color: closePowerArea.containsMouse ? Theme.error : Theme.surfaceText
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: closePowerArea
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
powerMenuVisible = false;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Power options
|
||||
Column {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingS
|
||||
|
||||
// Log Out
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: 50
|
||||
radius: Theme.cornerRadius
|
||||
color: logoutArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08)
|
||||
|
||||
Row {
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Theme.spacingM
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Theme.spacingM
|
||||
|
||||
Text {
|
||||
text: "logout"
|
||||
font.family: Theme.iconFont
|
||||
font.pixelSize: Theme.iconSize
|
||||
color: Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Text {
|
||||
text: "Log Out"
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: logoutArea
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
powerMenuVisible = false;
|
||||
root.powerConfirmAction = "logout";
|
||||
root.powerConfirmTitle = "Log Out";
|
||||
root.powerConfirmMessage = "Are you sure you want to log out?";
|
||||
root.powerConfirmVisible = true;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Suspend
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: 50
|
||||
radius: Theme.cornerRadius
|
||||
color: suspendArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08)
|
||||
|
||||
Row {
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Theme.spacingM
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Theme.spacingM
|
||||
|
||||
Text {
|
||||
text: "bedtime"
|
||||
font.family: Theme.iconFont
|
||||
font.pixelSize: Theme.iconSize
|
||||
color: Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Text {
|
||||
text: "Suspend"
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: suspendArea
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
powerMenuVisible = false;
|
||||
root.powerConfirmAction = "suspend";
|
||||
root.powerConfirmTitle = "Suspend";
|
||||
root.powerConfirmMessage = "Are you sure you want to suspend the system?";
|
||||
root.powerConfirmVisible = true;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Reboot
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: 50
|
||||
radius: Theme.cornerRadius
|
||||
color: rebootArea.containsMouse ? Qt.rgba(Theme.warning.r, Theme.warning.g, Theme.warning.b, 0.08) : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08)
|
||||
|
||||
Row {
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Theme.spacingM
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Theme.spacingM
|
||||
|
||||
Text {
|
||||
text: "restart_alt"
|
||||
font.family: Theme.iconFont
|
||||
font.pixelSize: Theme.iconSize
|
||||
color: rebootArea.containsMouse ? Theme.warning : Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Text {
|
||||
text: "Reboot"
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: rebootArea.containsMouse ? Theme.warning : Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: rebootArea
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
powerMenuVisible = false;
|
||||
root.powerConfirmAction = "reboot";
|
||||
root.powerConfirmTitle = "Reboot";
|
||||
root.powerConfirmMessage = "Are you sure you want to reboot the system?";
|
||||
root.powerConfirmVisible = true;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Power Off
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: 50
|
||||
radius: Theme.cornerRadius
|
||||
color: powerOffArea.containsMouse ? Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.08) : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08)
|
||||
|
||||
Row {
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Theme.spacingM
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Theme.spacingM
|
||||
|
||||
Text {
|
||||
text: "power_settings_new"
|
||||
font.family: Theme.iconFont
|
||||
font.pixelSize: Theme.iconSize
|
||||
color: powerOffArea.containsMouse ? Theme.error : Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Text {
|
||||
text: "Power Off"
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: powerOffArea.containsMouse ? Theme.error : Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: powerOffArea
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
powerMenuVisible = false;
|
||||
root.powerConfirmAction = "poweroff";
|
||||
root.powerConfirmTitle = "Power Off";
|
||||
root.powerConfirmMessage = "Are you sure you want to power off the system?";
|
||||
root.powerConfirmVisible = true;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: Theme.mediumDuration
|
||||
easing.type: Theme.emphasizedEasing
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Behavior on scale {
|
||||
NumberAnimation {
|
||||
duration: Theme.mediumDuration
|
||||
easing.type: Theme.emphasizedEasing
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,63 +0,0 @@
|
||||
import "."
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
|
||||
Rectangle {
|
||||
id: ramWidget
|
||||
|
||||
property bool showPercentage: true
|
||||
property bool showIcon: true
|
||||
|
||||
width: 55
|
||||
height: 30
|
||||
radius: Theme.cornerRadius
|
||||
color: ramArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.16) : Qt.rgba(Theme.secondary.r, Theme.secondary.g, Theme.secondary.b, 0.08)
|
||||
|
||||
MouseArea {
|
||||
id: ramArea
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
ProcessMonitorService.setSortBy("memory");
|
||||
processListDropdown.toggle();
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
anchors.centerIn: parent
|
||||
spacing: 3
|
||||
|
||||
// RAM icon
|
||||
Text {
|
||||
text: "developer_board" // Material Design CPU/processor icon (swapped from CPU widget)
|
||||
font.family: Theme.iconFont
|
||||
font.pixelSize: Theme.iconSize - 8
|
||||
font.weight: Theme.iconFontWeight
|
||||
color: {
|
||||
if (SystemMonitorService.memoryUsage > 90)
|
||||
return Theme.error;
|
||||
|
||||
if (SystemMonitorService.memoryUsage > 75)
|
||||
return Theme.warning;
|
||||
|
||||
return Theme.surfaceText;
|
||||
}
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
// Percentage text
|
||||
Text {
|
||||
text: (SystemMonitorService.memoryUsage || 0).toFixed(0) + "%"
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.weight: Font.Medium
|
||||
color: Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,693 +0,0 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Effects
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import Quickshell.Wayland
|
||||
import Quickshell.Widgets
|
||||
import qs.Common
|
||||
|
||||
PanelWindow {
|
||||
id: settingsPopup
|
||||
|
||||
property bool settingsVisible: false
|
||||
|
||||
signal closingPopup()
|
||||
|
||||
onSettingsVisibleChanged: {
|
||||
if (!settingsVisible)
|
||||
closingPopup();
|
||||
|
||||
}
|
||||
visible: settingsVisible
|
||||
implicitWidth: 600
|
||||
implicitHeight: 700
|
||||
WlrLayershell.layer: WlrLayershell.Overlay
|
||||
WlrLayershell.exclusiveZone: -1
|
||||
WlrLayershell.keyboardFocus: WlrKeyboardFocus.OnDemand
|
||||
color: "transparent"
|
||||
|
||||
anchors {
|
||||
top: true
|
||||
left: true
|
||||
right: true
|
||||
bottom: true
|
||||
}
|
||||
|
||||
// Darkened background
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
color: "black"
|
||||
opacity: 0.5
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
onClicked: settingsPopup.settingsVisible = false
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Main settings panel - spotlight-like centered appearance
|
||||
Rectangle {
|
||||
id: mainPanel
|
||||
|
||||
width: Math.min(600, parent.width - Theme.spacingXL * 2)
|
||||
height: Math.min(700, parent.height - Theme.spacingXL * 2)
|
||||
anchors.centerIn: parent
|
||||
color: Theme.popupBackground()
|
||||
radius: Theme.cornerRadiusLarge
|
||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
|
||||
border.width: 1
|
||||
// Simple opacity and scale control tied directly to settingsVisible
|
||||
opacity: settingsPopup.settingsVisible ? 1 : 0
|
||||
scale: settingsPopup.settingsVisible ? 1 : 0.95
|
||||
// Add shadow effect
|
||||
layer.enabled: true
|
||||
|
||||
Column {
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingL
|
||||
spacing: Theme.spacingL
|
||||
|
||||
// Header
|
||||
Row {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingM
|
||||
|
||||
Text {
|
||||
text: "settings"
|
||||
font.family: Theme.iconFont
|
||||
font.pixelSize: Theme.iconSize
|
||||
color: Theme.primary
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Text {
|
||||
text: "Settings"
|
||||
font.pixelSize: Theme.fontSizeXLarge
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Item {
|
||||
width: parent.width - 175 // Spacer to push close button to the right
|
||||
height: 1
|
||||
}
|
||||
|
||||
// Close button
|
||||
Rectangle {
|
||||
width: 32
|
||||
height: 32
|
||||
radius: Theme.cornerRadius
|
||||
color: closeButton.containsMouse ? Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.12) : "transparent"
|
||||
|
||||
Text {
|
||||
text: "close"
|
||||
font.family: Theme.iconFont
|
||||
font.pixelSize: Theme.iconSize - 4
|
||||
color: Theme.surfaceText
|
||||
anchors.centerIn: parent
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: closeButton
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: settingsPopup.settingsVisible = false
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Settings sections
|
||||
ScrollView {
|
||||
width: parent.width
|
||||
height: parent.height - 80
|
||||
clip: true
|
||||
|
||||
Column {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingL
|
||||
|
||||
// Profile Settings
|
||||
SettingsSection {
|
||||
title: "Profile"
|
||||
iconName: "person"
|
||||
|
||||
content: Column {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingM
|
||||
|
||||
// Profile Image Preview and Input
|
||||
Column {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingM
|
||||
|
||||
Text {
|
||||
text: "Profile Image"
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
}
|
||||
|
||||
// Profile Image Preview with circular crop
|
||||
Row {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingM
|
||||
|
||||
// Circular profile image preview
|
||||
Item {
|
||||
id: avatarContainer
|
||||
|
||||
property bool hasImage: avatarImageSource.status === Image.Ready
|
||||
|
||||
width: 54
|
||||
height: 54
|
||||
|
||||
// This rectangle provides the themed ring via its border.
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
radius: width / 2
|
||||
color: "transparent"
|
||||
border.color: Theme.primary
|
||||
border.width: 1 // The ring is 1px thick.
|
||||
visible: parent.hasImage
|
||||
}
|
||||
|
||||
// Hidden Image loader. Its only purpose is to load the texture.
|
||||
Image {
|
||||
id: avatarImageSource
|
||||
|
||||
source: {
|
||||
if (profileImageInput.text === "")
|
||||
return "";
|
||||
|
||||
if (profileImageInput.text.startsWith("/"))
|
||||
return "file://" + profileImageInput.text;
|
||||
|
||||
return profileImageInput.text;
|
||||
}
|
||||
smooth: true
|
||||
asynchronous: true
|
||||
mipmap: true
|
||||
cache: true
|
||||
visible: false // This item is never shown directly.
|
||||
}
|
||||
|
||||
MultiEffect {
|
||||
anchors.fill: parent
|
||||
anchors.margins: 5
|
||||
source: avatarImageSource
|
||||
maskEnabled: true
|
||||
maskSource: settingsCircularMask
|
||||
visible: avatarContainer.hasImage
|
||||
maskThresholdMin: 0.5
|
||||
maskSpreadAtMin: 1
|
||||
}
|
||||
|
||||
Item {
|
||||
id: settingsCircularMask
|
||||
|
||||
width: 54 - 10
|
||||
height: 54 - 10
|
||||
layer.enabled: true
|
||||
layer.smooth: true
|
||||
visible: false
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
radius: width / 2
|
||||
color: "black"
|
||||
antialiasing: true
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Fallback for when there is no image.
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
radius: width / 2
|
||||
color: Theme.primary
|
||||
visible: !parent.hasImage
|
||||
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
text: "person"
|
||||
font.family: Theme.iconFont
|
||||
font.pixelSize: Theme.iconSize + 8
|
||||
color: Theme.primaryText
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Error icon for when the image fails to load.
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
text: "warning"
|
||||
font.family: Theme.iconFont
|
||||
font.pixelSize: Theme.iconSize + 8
|
||||
color: Theme.primaryText
|
||||
visible: profileImageInput.text !== "" && avatarImageSource.status === Image.Error
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Input field
|
||||
Column {
|
||||
width: parent.width - 80 - Theme.spacingM
|
||||
spacing: Theme.spacingS
|
||||
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: 48
|
||||
radius: Theme.cornerRadius
|
||||
color: Theme.surfaceVariant
|
||||
border.color: profileImageInput.activeFocus ? Theme.primary : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.3)
|
||||
border.width: profileImageInput.activeFocus ? 2 : 1
|
||||
|
||||
TextInput {
|
||||
id: profileImageInput
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingM
|
||||
verticalAlignment: TextInput.AlignVCenter
|
||||
color: Theme.surfaceText
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
text: Prefs.profileImage
|
||||
selectByMouse: true
|
||||
onEditingFinished: {
|
||||
Prefs.setProfileImage(text);
|
||||
}
|
||||
|
||||
// Placeholder text
|
||||
Text {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: "Enter image path or URL..."
|
||||
color: Qt.rgba(Theme.surfaceVariantText.r, Theme.surfaceVariantText.g, Theme.surfaceVariantText.b, 0.6)
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
visible: profileImageInput.text.length === 0 && !profileImageInput.activeFocus
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.IBeamCursor
|
||||
acceptedButtons: Qt.NoButton
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Text {
|
||||
text: "Local filesystem path or URL to an image file."
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
wrapMode: Text.WordWrap
|
||||
width: parent.width
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Clock Settings
|
||||
SettingsSection {
|
||||
title: "Clock & Time"
|
||||
iconName: "schedule"
|
||||
|
||||
content: Column {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingM
|
||||
|
||||
SettingsToggle {
|
||||
text: "24-Hour Format"
|
||||
description: "Use 24-hour time format instead of 12-hour AM/PM"
|
||||
checked: Prefs.use24HourClock
|
||||
onToggled: (checked) => {
|
||||
return Prefs.setClockFormat(checked);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Weather Settings
|
||||
SettingsSection {
|
||||
title: "Weather"
|
||||
iconName: "wb_sunny"
|
||||
|
||||
content: Column {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingM
|
||||
|
||||
SettingsToggle {
|
||||
text: "Fahrenheit"
|
||||
description: "Use Fahrenheit instead of Celsius for temperature"
|
||||
checked: Prefs.useFahrenheit
|
||||
onToggled: (checked) => {
|
||||
return Prefs.setTemperatureUnit(checked);
|
||||
}
|
||||
}
|
||||
|
||||
// Weather Location Setting
|
||||
Column {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingS
|
||||
|
||||
Text {
|
||||
text: "Location"
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: 48
|
||||
radius: Theme.cornerRadius
|
||||
color: Theme.surfaceVariant
|
||||
border.color: weatherLocationInput.activeFocus ? Theme.primary : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.3)
|
||||
border.width: weatherLocationInput.activeFocus ? 2 : 1
|
||||
|
||||
TextInput {
|
||||
id: weatherLocationInput
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingM
|
||||
verticalAlignment: TextInput.AlignVCenter
|
||||
color: Theme.surfaceText
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
text: Prefs.weatherLocationOverride
|
||||
selectByMouse: true
|
||||
onEditingFinished: {
|
||||
Prefs.setWeatherLocationOverride(text);
|
||||
}
|
||||
|
||||
// Placeholder text
|
||||
Text {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: "Enter location..."
|
||||
color: Qt.rgba(Theme.surfaceVariantText.r, Theme.surfaceVariantText.g, Theme.surfaceVariantText.b, 0.6)
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
visible: weatherLocationInput.text.length === 0 && !weatherLocationInput.activeFocus
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.IBeamCursor
|
||||
acceptedButtons: Qt.NoButton
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Text {
|
||||
text: "Examples: \"New York, NY\", \"London\", \"Tokyo\""
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
wrapMode: Text.WordWrap
|
||||
width: parent.width
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Widget Visibility Settings
|
||||
SettingsSection {
|
||||
title: "Top Bar Widgets"
|
||||
iconName: "widgets"
|
||||
|
||||
content: Column {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingM
|
||||
|
||||
SettingsToggle {
|
||||
text: "Focused Window"
|
||||
description: "Show the currently focused application in the top bar"
|
||||
checked: Prefs.showFocusedWindow
|
||||
onToggled: (checked) => {
|
||||
return Prefs.setShowFocusedWindow(checked);
|
||||
}
|
||||
}
|
||||
|
||||
SettingsToggle {
|
||||
text: "Weather Widget"
|
||||
description: "Display weather information in the top bar"
|
||||
checked: Prefs.showWeather
|
||||
onToggled: (checked) => {
|
||||
return Prefs.setShowWeather(checked);
|
||||
}
|
||||
}
|
||||
|
||||
SettingsToggle {
|
||||
text: "Media Controls"
|
||||
description: "Show currently playing media in the top bar"
|
||||
checked: Prefs.showMusic
|
||||
onToggled: (checked) => {
|
||||
return Prefs.setShowMusic(checked);
|
||||
}
|
||||
}
|
||||
|
||||
SettingsToggle {
|
||||
text: "Clipboard Button"
|
||||
description: "Show clipboard access button in the top bar"
|
||||
checked: Prefs.showClipboard
|
||||
onToggled: (checked) => {
|
||||
return Prefs.setShowClipboard(checked);
|
||||
}
|
||||
}
|
||||
|
||||
SettingsToggle {
|
||||
text: "System Resources"
|
||||
description: "Display CPU and RAM usage indicators"
|
||||
checked: Prefs.showSystemResources
|
||||
onToggled: (checked) => {
|
||||
return Prefs.setShowSystemResources(checked);
|
||||
}
|
||||
}
|
||||
|
||||
SettingsToggle {
|
||||
text: "System Tray"
|
||||
description: "Show system tray icons in the top bar"
|
||||
checked: Prefs.showSystemTray
|
||||
onToggled: (checked) => {
|
||||
return Prefs.setShowSystemTray(checked);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Display Settings
|
||||
SettingsSection {
|
||||
title: "Display & Appearance"
|
||||
iconName: "palette"
|
||||
|
||||
content: Column {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingL
|
||||
|
||||
SettingsToggle {
|
||||
text: "Night Mode"
|
||||
description: "Apply warm color temperature to reduce eye strain"
|
||||
checked: Prefs.nightModeEnabled
|
||||
onToggled: (checked) => {
|
||||
Prefs.setNightModeEnabled(checked);
|
||||
if (checked)
|
||||
nightModeEnableProcess.running = true;
|
||||
else
|
||||
nightModeDisableProcess.running = true;
|
||||
}
|
||||
}
|
||||
|
||||
SettingsToggle {
|
||||
text: "Light Mode"
|
||||
description: "Use light theme instead of dark theme"
|
||||
checked: Prefs.isLightMode
|
||||
onToggled: (checked) => {
|
||||
Prefs.setLightMode(checked);
|
||||
Theme.isLightMode = checked;
|
||||
}
|
||||
}
|
||||
|
||||
// Top Bar Transparency
|
||||
Column {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingS
|
||||
|
||||
Text {
|
||||
text: "Top Bar Transparency"
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
}
|
||||
|
||||
CustomSlider {
|
||||
width: parent.width
|
||||
value: Math.round(Prefs.topBarTransparency * 100)
|
||||
minimum: 0
|
||||
maximum: 100
|
||||
leftIcon: "opacity"
|
||||
rightIcon: "circle"
|
||||
unit: "%"
|
||||
showValue: true
|
||||
onSliderDragFinished: (finalValue) => {
|
||||
let transparencyValue = finalValue / 100;
|
||||
Prefs.setTopBarTransparency(transparencyValue);
|
||||
}
|
||||
}
|
||||
|
||||
Text {
|
||||
text: "Adjust the transparency of the top bar background"
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
wrapMode: Text.WordWrap
|
||||
width: parent.width
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Popup Transparency
|
||||
Column {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingS
|
||||
|
||||
Text {
|
||||
text: "Popup Transparency"
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
}
|
||||
|
||||
CustomSlider {
|
||||
width: parent.width
|
||||
value: Math.round(Prefs.popupTransparency * 100)
|
||||
minimum: 0
|
||||
maximum: 100
|
||||
leftIcon: "blur_on"
|
||||
rightIcon: "circle"
|
||||
unit: "%"
|
||||
showValue: true
|
||||
onSliderDragFinished: (finalValue) => {
|
||||
let transparencyValue = finalValue / 100;
|
||||
Prefs.setPopupTransparency(transparencyValue);
|
||||
}
|
||||
}
|
||||
|
||||
Text {
|
||||
text: "Adjust transparency for dialogs, menus, and popups"
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
wrapMode: Text.WordWrap
|
||||
width: parent.width
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Theme Picker
|
||||
Column {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingS
|
||||
|
||||
Text {
|
||||
text: "Theme Color"
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
}
|
||||
|
||||
ThemePicker {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: Theme.mediumDuration
|
||||
easing.type: Theme.emphasizedEasing
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Behavior on scale {
|
||||
NumberAnimation {
|
||||
duration: Theme.mediumDuration
|
||||
easing.type: Theme.emphasizedEasing
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
layer.effect: MultiEffect {
|
||||
shadowEnabled: true
|
||||
shadowHorizontalOffset: 0
|
||||
shadowVerticalOffset: 8
|
||||
shadowBlur: 1
|
||||
shadowColor: Qt.rgba(0, 0, 0, 0.3)
|
||||
shadowOpacity: 0.3
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Night mode processes
|
||||
Process {
|
||||
id: nightModeEnableProcess
|
||||
|
||||
command: ["bash", "-c", "if command -v wlsunset > /dev/null; then pkill wlsunset; wlsunset -t 3000 & elif command -v redshift > /dev/null; then pkill redshift; redshift -P -O 3000 & else echo 'No night mode tool available'; fi"]
|
||||
running: false
|
||||
onExited: (exitCode) => {
|
||||
if (exitCode !== 0) {
|
||||
console.warn("Failed to enable night mode");
|
||||
Prefs.setNightModeEnabled(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: nightModeDisableProcess
|
||||
|
||||
command: ["bash", "-c", "pkill wlsunset; pkill redshift; if command -v wlsunset > /dev/null; then wlsunset -t 6500 -T 6500 & sleep 1; pkill wlsunset; elif command -v redshift > /dev/null; then redshift -P -O 6500; redshift -x; fi"]
|
||||
running: false
|
||||
onExited: (exitCode) => {
|
||||
if (exitCode !== 0)
|
||||
console.warn("Failed to disable night mode");
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// Keyboard focus and shortcuts
|
||||
FocusScope {
|
||||
anchors.fill: parent
|
||||
focus: settingsPopup.settingsVisible
|
||||
Keys.onEscapePressed: settingsPopup.settingsVisible = false
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
import QtQuick
|
||||
import qs.Common
|
||||
|
||||
Column {
|
||||
id: root
|
||||
|
||||
property string title: ""
|
||||
property string iconName: ""
|
||||
property alias content: contentLoader.sourceComponent
|
||||
|
||||
width: parent.width
|
||||
spacing: Theme.spacingM
|
||||
|
||||
// Section header
|
||||
Row {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingS
|
||||
|
||||
Text {
|
||||
text: iconName
|
||||
font.family: Theme.iconFont
|
||||
font.pixelSize: Theme.iconSize - 2
|
||||
color: Theme.primary
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Text {
|
||||
text: title
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Divider
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: 1
|
||||
color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
|
||||
}
|
||||
|
||||
// Content
|
||||
Loader {
|
||||
id: contentLoader
|
||||
|
||||
width: parent.width
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,120 +0,0 @@
|
||||
import QtQuick
|
||||
import qs.Common
|
||||
|
||||
Rectangle {
|
||||
id: root
|
||||
|
||||
property string text: ""
|
||||
property string description: ""
|
||||
property bool checked: false
|
||||
|
||||
signal toggled(bool checked)
|
||||
|
||||
width: parent.width
|
||||
height: 60
|
||||
radius: Theme.cornerRadius
|
||||
color: toggleArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08)
|
||||
|
||||
Row {
|
||||
anchors.left: parent.left
|
||||
anchors.right: toggle.left
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.leftMargin: Theme.spacingM
|
||||
anchors.rightMargin: Theme.spacingM
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
Column {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
Text {
|
||||
text: root.text
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
}
|
||||
|
||||
Text {
|
||||
text: root.description
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
wrapMode: Text.WordWrap
|
||||
width: Math.min(implicitWidth, root.width - 120)
|
||||
visible: root.description.length > 0
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Toggle switch
|
||||
Rectangle {
|
||||
id: toggle
|
||||
|
||||
width: 48
|
||||
height: 24
|
||||
radius: 12
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: Theme.spacingM
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
color: root.checked ? Theme.primary : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.5)
|
||||
|
||||
Rectangle {
|
||||
id: toggleHandle
|
||||
|
||||
width: 20
|
||||
height: 20
|
||||
radius: 10
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
x: root.checked ? parent.width - width - 2 : 2
|
||||
color: root.checked ? Theme.primaryText : Theme.surfaceText
|
||||
|
||||
Behavior on x {
|
||||
NumberAnimation {
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Theme.standardEasing
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Theme.standardEasing
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Theme.standardEasing
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: toggleArea
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
root.checked = !root.checked;
|
||||
root.toggled(root.checked);
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Theme.standardEasing
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,928 +0,0 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Effects
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import Quickshell.Wayland
|
||||
import Quickshell.Widgets
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
|
||||
PanelWindow {
|
||||
id: spotlightLauncher
|
||||
|
||||
property bool spotlightOpen: false
|
||||
property var filteredApps: []
|
||||
property int selectedIndex: 0
|
||||
property int maxResults: 50
|
||||
property var categories: {
|
||||
var allCategories = AppSearchService.getAllCategories().filter((cat) => {
|
||||
return cat !== "Education" && cat !== "Science";
|
||||
});
|
||||
// Insert "Recents" after "All"
|
||||
var result = ["All", "Recents"];
|
||||
return result.concat(allCategories.filter((cat) => {
|
||||
return cat !== "All";
|
||||
}));
|
||||
}
|
||||
property string selectedCategory: "All"
|
||||
property string viewMode: Prefs.spotlightLauncherViewMode // "list" or "grid"
|
||||
|
||||
// ...existing code...
|
||||
function show() {
|
||||
console.log("SpotlightLauncher: show() called");
|
||||
spotlightOpen = true;
|
||||
console.log("SpotlightLauncher: spotlightOpen set to", spotlightOpen);
|
||||
searchDebounceTimer.stop(); // Stop any pending search
|
||||
updateFilteredApps(); // Immediate update when showing
|
||||
Qt.callLater(function() {
|
||||
searchField.forceActiveFocus();
|
||||
searchField.selectAll();
|
||||
});
|
||||
}
|
||||
|
||||
function hide() {
|
||||
spotlightOpen = false;
|
||||
searchDebounceTimer.stop(); // Stop any pending search
|
||||
searchField.text = "";
|
||||
selectedIndex = 0;
|
||||
selectedCategory = "All";
|
||||
updateFilteredApps();
|
||||
}
|
||||
|
||||
function toggle() {
|
||||
if (spotlightOpen)
|
||||
hide();
|
||||
else
|
||||
show();
|
||||
}
|
||||
|
||||
function updateFilteredApps() {
|
||||
filteredApps = [];
|
||||
selectedIndex = 0;
|
||||
var apps = [];
|
||||
var searchQuery = searchField.text;
|
||||
if (searchQuery.length === 0) {
|
||||
// Show apps from category
|
||||
if (selectedCategory === "All") {
|
||||
// For "All" category, show all available apps
|
||||
apps = AppSearchService.applications || [];
|
||||
} else if (selectedCategory === "Recents") {
|
||||
// For "Recents" category, get recent apps from Prefs and filter out non-existent ones
|
||||
var recentApps = Prefs.getRecentApps();
|
||||
apps = recentApps.map((recentApp) => {
|
||||
return AppSearchService.getAppByExec(recentApp.exec);
|
||||
}).filter((app) => {
|
||||
return app !== null && !app.noDisplay;
|
||||
});
|
||||
} else {
|
||||
// For specific categories, limit results
|
||||
var categoryApps = AppSearchService.getAppsInCategory(selectedCategory);
|
||||
apps = categoryApps.slice(0, maxResults);
|
||||
}
|
||||
} else {
|
||||
// Search with category filter
|
||||
if (selectedCategory === "All") {
|
||||
// For "All" category, search all apps without limit
|
||||
apps = AppSearchService.searchApplications(searchQuery);
|
||||
} else if (selectedCategory === "Recents") {
|
||||
// For "Recents" category, search within recent apps
|
||||
var recentApps = Prefs.getRecentApps();
|
||||
var recentDesktopEntries = recentApps.map((recentApp) => {
|
||||
return AppSearchService.getAppByExec(recentApp.exec);
|
||||
}).filter((app) => {
|
||||
return app !== null && !app.noDisplay;
|
||||
});
|
||||
if (recentDesktopEntries.length > 0) {
|
||||
var allSearchResults = AppSearchService.searchApplications(searchQuery);
|
||||
var recentNames = new Set(recentDesktopEntries.map((app) => {
|
||||
return app.name;
|
||||
}));
|
||||
// Filter search results to only include recent apps
|
||||
apps = allSearchResults.filter((searchApp) => {
|
||||
return recentNames.has(searchApp.name);
|
||||
});
|
||||
} else {
|
||||
apps = [];
|
||||
}
|
||||
} else {
|
||||
// For specific categories, filter search results by category
|
||||
var categoryApps = AppSearchService.getAppsInCategory(selectedCategory);
|
||||
if (categoryApps.length > 0) {
|
||||
var allSearchResults = AppSearchService.searchApplications(searchQuery);
|
||||
var categoryNames = new Set(categoryApps.map((app) => {
|
||||
return app.name;
|
||||
}));
|
||||
// Filter search results to only include apps from the selected category
|
||||
apps = allSearchResults.filter((searchApp) => {
|
||||
return categoryNames.has(searchApp.name);
|
||||
}).slice(0, maxResults);
|
||||
} else {
|
||||
apps = [];
|
||||
}
|
||||
}
|
||||
}
|
||||
// Convert to our format - batch operations for better performance
|
||||
filteredApps = apps.map((app) => {
|
||||
return ({
|
||||
"name": app.name,
|
||||
"exec": app.execString || "",
|
||||
"icon": app.icon || "application-x-executable",
|
||||
"comment": app.comment || "",
|
||||
"categories": app.categories || [],
|
||||
"desktopEntry": app
|
||||
});
|
||||
});
|
||||
// Clear and repopulate model efficiently
|
||||
filteredModel.clear();
|
||||
filteredApps.forEach((app) => {
|
||||
return filteredModel.append(app);
|
||||
});
|
||||
}
|
||||
|
||||
function launchApp(app) {
|
||||
Prefs.addRecentApp(app);
|
||||
if (app.desktopEntry) {
|
||||
app.desktopEntry.execute();
|
||||
} else {
|
||||
var cleanExec = app.exec.replace(/%[fFuU]/g, "").trim();
|
||||
console.log("Spotlight: Launching app directly:", cleanExec);
|
||||
Quickshell.execDetached(["sh", "-c", cleanExec]);
|
||||
}
|
||||
hide();
|
||||
}
|
||||
|
||||
function selectNext() {
|
||||
if (filteredApps.length > 0) {
|
||||
if (viewMode === "grid") {
|
||||
// Grid navigation: move by columns (6 columns)
|
||||
var columnsCount = 6;
|
||||
selectedIndex = Math.min(selectedIndex + columnsCount, filteredApps.length - 1);
|
||||
} else {
|
||||
// List navigation: next item
|
||||
selectedIndex = (selectedIndex + 1) % filteredApps.length;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function selectPrevious() {
|
||||
if (filteredApps.length > 0) {
|
||||
if (viewMode === "grid") {
|
||||
// Grid navigation: move by columns (6 columns)
|
||||
var columnsCount = 6;
|
||||
selectedIndex = Math.max(selectedIndex - columnsCount, 0);
|
||||
} else {
|
||||
// List navigation: previous item
|
||||
selectedIndex = selectedIndex > 0 ? selectedIndex - 1 : filteredApps.length - 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function selectNextInRow() {
|
||||
if (filteredApps.length > 0 && viewMode === "grid")
|
||||
selectedIndex = Math.min(selectedIndex + 1, filteredApps.length - 1);
|
||||
|
||||
}
|
||||
|
||||
function selectPreviousInRow() {
|
||||
if (filteredApps.length > 0 && viewMode === "grid")
|
||||
selectedIndex = Math.max(selectedIndex - 1, 0);
|
||||
|
||||
}
|
||||
|
||||
function launchSelected() {
|
||||
if (filteredApps.length > 0 && selectedIndex >= 0 && selectedIndex < filteredApps.length)
|
||||
launchApp(filteredApps[selectedIndex]);
|
||||
|
||||
}
|
||||
|
||||
WlrLayershell.layer: WlrLayershell.Overlay
|
||||
WlrLayershell.exclusiveZone: -1
|
||||
WlrLayershell.keyboardFocus: spotlightOpen ? WlrKeyboardFocus.Exclusive : WlrKeyboardFocus.None
|
||||
WlrLayershell.namespace: "quickshell-spotlight"
|
||||
visible: spotlightOpen
|
||||
onVisibleChanged: {
|
||||
console.log("SpotlightLauncher visibility changed to:", visible);
|
||||
}
|
||||
color: "transparent"
|
||||
Component.onCompleted: {
|
||||
console.log("SpotlightLauncher: Component.onCompleted called - component loaded successfully!");
|
||||
var allCategories = AppSearchService.getAllCategories().filter((cat) => {
|
||||
return cat !== "Education" && cat !== "Science";
|
||||
});
|
||||
// Insert "Recents" after "All"
|
||||
var result = ["All", "Recents"];
|
||||
categories = result.concat(allCategories.filter((cat) => {
|
||||
return cat !== "All";
|
||||
}));
|
||||
}
|
||||
|
||||
// Search debouncing
|
||||
Timer {
|
||||
id: searchDebounceTimer
|
||||
|
||||
interval: 50
|
||||
repeat: false
|
||||
onTriggered: updateFilteredApps()
|
||||
}
|
||||
|
||||
anchors {
|
||||
top: true
|
||||
left: true
|
||||
right: true
|
||||
bottom: true
|
||||
}
|
||||
|
||||
ListModel {
|
||||
id: filteredModel
|
||||
}
|
||||
|
||||
Connections {
|
||||
function onReadyChanged() {
|
||||
if (AppSearchService.ready) {
|
||||
var allCategories = AppSearchService.getAllCategories().filter((cat) => {
|
||||
return cat !== "Education" && cat !== "Science";
|
||||
});
|
||||
// Insert "Recents" after "All"
|
||||
var result = ["All", "Recents"];
|
||||
categories = result.concat(allCategories.filter((cat) => {
|
||||
return cat !== "All";
|
||||
}));
|
||||
if (spotlightOpen)
|
||||
updateFilteredApps();
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
target: DesktopEntries
|
||||
}
|
||||
|
||||
Connections {
|
||||
function onApplicationsChanged() {
|
||||
console.log("SpotlightLauncher: DesktopEntries.applicationsChanged signal received");
|
||||
// Update categories when applications change
|
||||
if (AppSearchService.ready) {
|
||||
console.log("SpotlightLauncher: Updating categories and apps due to applicationsChanged");
|
||||
var allCategories = AppSearchService.getAllCategories().filter((cat) => {
|
||||
return cat !== "Education" && cat !== "Science";
|
||||
});
|
||||
var result = ["All", "Recents"];
|
||||
categories = result.concat(allCategories.filter((cat) => {
|
||||
return cat !== "All";
|
||||
}));
|
||||
if (spotlightOpen)
|
||||
updateFilteredApps();
|
||||
|
||||
} else {
|
||||
console.log("SpotlightLauncher: AppSearchService not ready, skipping update");
|
||||
}
|
||||
}
|
||||
|
||||
target: DesktopEntries
|
||||
}
|
||||
|
||||
// Dimmed overlay background
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
color: Qt.rgba(0, 0, 0, 0.4)
|
||||
opacity: spotlightOpen ? 1 : 0
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
enabled: spotlightOpen
|
||||
onClicked: hide()
|
||||
}
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Theme.standardEasing
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Main container with search and results
|
||||
Rectangle {
|
||||
// Margins and spacing
|
||||
// Categories (2 rows)
|
||||
|
||||
id: mainContainer
|
||||
|
||||
width: 600
|
||||
height: {
|
||||
// Fixed height to prevent shrinking - consistent experience
|
||||
let baseHeight = Theme.spacingXL * 2 + Theme.spacingL * 3;
|
||||
// Add category section height if visible
|
||||
if (categories.length > 1 || filteredModel.count > 0)
|
||||
baseHeight += 36 * 2 + Theme.spacingS + Theme.spacingM;
|
||||
|
||||
// Add search field height
|
||||
baseHeight += 56;
|
||||
// Add fixed results height for consistent size
|
||||
let fixedResultsHeight = 400;
|
||||
// Always same height regardless of content
|
||||
baseHeight += fixedResultsHeight;
|
||||
// Ensure reasonable bounds
|
||||
return Math.min(Math.max(baseHeight, 500), parent.height - 40);
|
||||
}
|
||||
anchors.centerIn: parent
|
||||
color: Theme.popupBackground()
|
||||
radius: Theme.cornerRadiusXLarge
|
||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
|
||||
border.width: 1
|
||||
layer.enabled: true
|
||||
opacity: spotlightOpen ? 1 : 0
|
||||
scale: spotlightOpen ? 1 : 0.96
|
||||
|
||||
Column {
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingXL
|
||||
spacing: Theme.spacingL
|
||||
|
||||
// Combined row for categories and view mode toggle
|
||||
Column {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingM
|
||||
visible: categories.length > 1 || filteredModel.count > 0
|
||||
|
||||
// Categories organized in 2 rows: 4 + 5
|
||||
Column {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingS
|
||||
|
||||
// Top row: All, Development, Graphics, Internet (4 items)
|
||||
Row {
|
||||
property var topRowCategories: ["All", "Recents", "Development", "Graphics"]
|
||||
|
||||
width: parent.width
|
||||
spacing: Theme.spacingS
|
||||
|
||||
Repeater {
|
||||
model: parent.topRowCategories.filter((cat) => {
|
||||
return categories.includes(cat);
|
||||
})
|
||||
|
||||
Rectangle {
|
||||
height: 36
|
||||
width: (parent.width - (parent.topRowCategories.length - 1) * Theme.spacingS) / parent.topRowCategories.length
|
||||
radius: Theme.cornerRadiusLarge
|
||||
color: selectedCategory === modelData ? Theme.primary : "transparent"
|
||||
border.color: selectedCategory === modelData ? "transparent" : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.3)
|
||||
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
text: modelData
|
||||
color: selectedCategory === modelData ? Theme.surface : Theme.surfaceText
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
font.weight: selectedCategory === modelData ? Font.Medium : Font.Normal
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
selectedCategory = modelData;
|
||||
updateFilteredApps();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Bottom row: Media, Office, Settings, System, Utilities (5 items)
|
||||
Row {
|
||||
property var bottomRowCategories: ["Internet", "Media", "Office", "Settings", "System"]
|
||||
|
||||
width: parent.width
|
||||
spacing: Theme.spacingS
|
||||
|
||||
Repeater {
|
||||
model: parent.bottomRowCategories.filter((cat) => {
|
||||
return categories.includes(cat);
|
||||
})
|
||||
|
||||
Rectangle {
|
||||
height: 36
|
||||
width: (parent.width - (parent.bottomRowCategories.length - 1) * Theme.spacingS) / parent.bottomRowCategories.length
|
||||
radius: Theme.cornerRadiusLarge
|
||||
color: selectedCategory === modelData ? Theme.primary : "transparent"
|
||||
border.color: selectedCategory === modelData ? "transparent" : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.3)
|
||||
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
text: modelData
|
||||
color: selectedCategory === modelData ? Theme.surface : Theme.surfaceText
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
font.weight: selectedCategory === modelData ? Font.Medium : Font.Normal
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
selectedCategory = modelData;
|
||||
updateFilteredApps();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Search field with view toggle buttons
|
||||
Row {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingM
|
||||
|
||||
Rectangle {
|
||||
id: searchContainer
|
||||
|
||||
width: parent.width - 80 - Theme.spacingM // Leave space for view toggle buttons
|
||||
height: 56
|
||||
radius: Theme.cornerRadiusLarge
|
||||
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, Theme.getContentBackgroundAlpha() * 0.7)
|
||||
border.width: searchField.activeFocus ? 2 : 1
|
||||
border.color: searchField.activeFocus ? Theme.primary : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
|
||||
|
||||
Row {
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: Theme.spacingL
|
||||
anchors.rightMargin: Theme.spacingL
|
||||
spacing: Theme.spacingM
|
||||
|
||||
Text {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: "search"
|
||||
font.family: Theme.iconFont
|
||||
font.pixelSize: Theme.iconSize
|
||||
color: searchField.activeFocus ? Theme.primary : Theme.surfaceVariantText
|
||||
}
|
||||
|
||||
TextInput {
|
||||
id: searchField
|
||||
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
width: parent.width - parent.spacing - Theme.iconSize - 32
|
||||
height: parent.height - Theme.spacingS
|
||||
color: Theme.surfaceText
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
focus: spotlightOpen
|
||||
selectByMouse: true
|
||||
onTextChanged: {
|
||||
searchDebounceTimer.restart();
|
||||
}
|
||||
Keys.onPressed: (event) => {
|
||||
if (event.key === Qt.Key_Escape) {
|
||||
hide();
|
||||
event.accepted = true;
|
||||
} else if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) {
|
||||
launchSelected();
|
||||
event.accepted = true;
|
||||
} else if (event.key === Qt.Key_Down) {
|
||||
selectNext();
|
||||
event.accepted = true;
|
||||
} else if (event.key === Qt.Key_Up) {
|
||||
selectPrevious();
|
||||
event.accepted = true;
|
||||
} else if (event.key === Qt.Key_Right && viewMode === "grid") {
|
||||
selectNextInRow();
|
||||
event.accepted = true;
|
||||
} else if (event.key === Qt.Key_Left && viewMode === "grid") {
|
||||
selectPreviousInRow();
|
||||
event.accepted = true;
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.IBeamCursor
|
||||
acceptedButtons: Qt.NoButton
|
||||
}
|
||||
|
||||
Text {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: "Search applications..."
|
||||
color: Theme.surfaceVariantText
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
visible: searchField.text.length === 0 && !searchField.activeFocus
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Behavior on border.color {
|
||||
ColorAnimation {
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Theme.standardEasing
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// View mode toggle buttons next to search bar
|
||||
Row {
|
||||
spacing: Theme.spacingXS
|
||||
visible: filteredModel.count > 0
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
// List view button
|
||||
Rectangle {
|
||||
width: 36
|
||||
height: 36
|
||||
radius: Theme.cornerRadiusLarge
|
||||
color: viewMode === "list" ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : listViewArea.containsMouse ? Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08) : "transparent"
|
||||
border.color: viewMode === "list" ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.3) : "transparent"
|
||||
border.width: 1
|
||||
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
text: "view_list"
|
||||
font.family: Theme.iconFont
|
||||
font.pixelSize: 18
|
||||
color: viewMode === "list" ? Theme.primary : Theme.surfaceText
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: listViewArea
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
viewMode = "list";
|
||||
Prefs.setSpotlightLauncherViewMode("list");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Grid view button
|
||||
Rectangle {
|
||||
width: 36
|
||||
height: 36
|
||||
radius: Theme.cornerRadiusLarge
|
||||
color: viewMode === "grid" ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : gridViewArea.containsMouse ? Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08) : "transparent"
|
||||
border.color: viewMode === "grid" ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.3) : "transparent"
|
||||
border.width: 1
|
||||
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
text: "grid_view"
|
||||
font.family: Theme.iconFont
|
||||
font.pixelSize: 18
|
||||
color: viewMode === "grid" ? Theme.primary : Theme.surfaceText
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: gridViewArea
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
viewMode = "grid";
|
||||
Prefs.setSpotlightLauncherViewMode("grid");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Results container
|
||||
Rectangle {
|
||||
id: resultsContainer
|
||||
|
||||
width: parent.width
|
||||
height: parent.height - y // Use remaining space
|
||||
color: "transparent"
|
||||
|
||||
// List view
|
||||
ListView {
|
||||
id: resultsList
|
||||
|
||||
// Make mouse wheel scrolling more responsive
|
||||
property real wheelStepSize: 60
|
||||
|
||||
anchors.fill: parent
|
||||
visible: viewMode === "list"
|
||||
model: filteredModel
|
||||
currentIndex: selectedIndex
|
||||
clip: true
|
||||
focus: spotlightOpen
|
||||
interactive: true
|
||||
flickDeceleration: 8000
|
||||
maximumFlickVelocity: 15000
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
acceptedButtons: Qt.NoButton
|
||||
propagateComposedEvents: true
|
||||
z: -1
|
||||
onWheel: (wheel) => {
|
||||
var delta = wheel.angleDelta.y;
|
||||
var steps = delta / 120; // Standard wheel step
|
||||
resultsList.contentY -= steps * resultsList.wheelStepSize;
|
||||
// Ensure we stay within bounds
|
||||
if (resultsList.contentY < 0)
|
||||
resultsList.contentY = 0;
|
||||
else if (resultsList.contentY > resultsList.contentHeight - resultsList.height)
|
||||
resultsList.contentY = Math.max(0, resultsList.contentHeight - resultsList.height);
|
||||
}
|
||||
}
|
||||
|
||||
delegate: Rectangle {
|
||||
width: resultsList.width
|
||||
height: 60
|
||||
radius: Theme.cornerRadius
|
||||
color: ListView.isCurrentItem ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : listMouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.06) : "transparent"
|
||||
|
||||
Row {
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingM
|
||||
spacing: Theme.spacingL
|
||||
|
||||
Item {
|
||||
width: 40
|
||||
height: 40
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
IconImage {
|
||||
id: listIconImg
|
||||
|
||||
anchors.fill: parent
|
||||
source: model.icon ? Quickshell.iconPath(model.icon, "") : ""
|
||||
smooth: true
|
||||
asynchronous: true
|
||||
visible: status === Image.Ready
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
visible: !listIconImg.visible
|
||||
color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.1)
|
||||
radius: Theme.cornerRadiusLarge
|
||||
border.width: 1
|
||||
border.color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.2)
|
||||
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
text: (model.name && model.name.length > 0) ? model.name.charAt(0).toUpperCase() : "A"
|
||||
font.pixelSize: 20
|
||||
color: Theme.primary
|
||||
font.weight: Font.Bold
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Column {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: 2
|
||||
|
||||
Text {
|
||||
text: model.name
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
|
||||
Text {
|
||||
text: model.comment
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceVariantText
|
||||
visible: model.comment && model.comment.length > 0
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: listMouseArea
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
z: 10
|
||||
onEntered: selectedIndex = index
|
||||
onClicked: launchApp(model)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
ScrollBar.vertical: ScrollBar {
|
||||
policy: ScrollBar.AlwaysOn
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Grid view scroll container
|
||||
ScrollView {
|
||||
anchors.fill: parent
|
||||
clip: true
|
||||
visible: viewMode === "grid"
|
||||
ScrollBar.vertical.policy: ScrollBar.AsNeeded
|
||||
ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
|
||||
|
||||
GridView {
|
||||
id: resultsGrid
|
||||
|
||||
// Responsive cell sizes based on screen width - wider cells for better fill
|
||||
property int baseCellWidth: Math.max(120, Math.min(160, width / 6))
|
||||
property int baseCellHeight: baseCellWidth + 20
|
||||
// Center the grid content with better fill
|
||||
property int columnsCount: Math.floor(width / cellWidth)
|
||||
property int remainingSpace: width - (columnsCount * cellWidth)
|
||||
// Make mouse wheel scrolling more responsive
|
||||
property real wheelStepSize: 60
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingS
|
||||
model: filteredModel
|
||||
focus: spotlightOpen
|
||||
interactive: true
|
||||
flickDeceleration: 8000
|
||||
maximumFlickVelocity: 15000
|
||||
cellWidth: baseCellWidth
|
||||
cellHeight: baseCellHeight
|
||||
leftMargin: Math.max(Theme.spacingXS, remainingSpace / 2)
|
||||
rightMargin: leftMargin
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
acceptedButtons: Qt.NoButton
|
||||
propagateComposedEvents: true
|
||||
z: -1
|
||||
onWheel: (wheel) => {
|
||||
var delta = wheel.angleDelta.y;
|
||||
var steps = delta / 120; // Standard wheel step
|
||||
resultsGrid.contentY -= steps * resultsGrid.wheelStepSize;
|
||||
// Ensure we stay within bounds
|
||||
if (resultsGrid.contentY < 0)
|
||||
resultsGrid.contentY = 0;
|
||||
else if (resultsGrid.contentY > resultsGrid.contentHeight - resultsGrid.height)
|
||||
resultsGrid.contentY = Math.max(0, resultsGrid.contentHeight - resultsGrid.height);
|
||||
}
|
||||
}
|
||||
|
||||
delegate: Rectangle {
|
||||
width: resultsGrid.cellWidth - 8
|
||||
height: resultsGrid.cellHeight - 8
|
||||
radius: Theme.cornerRadius
|
||||
color: selectedIndex === index ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : gridMouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.06) : "transparent"
|
||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
|
||||
border.width: 1
|
||||
|
||||
Column {
|
||||
anchors.centerIn: parent
|
||||
spacing: Theme.spacingS
|
||||
|
||||
Item {
|
||||
property int iconSize: Math.min(48, Math.max(32, resultsGrid.cellWidth * 0.55))
|
||||
|
||||
width: iconSize
|
||||
height: iconSize
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
|
||||
IconImage {
|
||||
id: iconImg
|
||||
|
||||
anchors.fill: parent
|
||||
source: model.icon ? Quickshell.iconPath(model.icon, "") : ""
|
||||
smooth: true
|
||||
asynchronous: true
|
||||
visible: status === Image.Ready
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
visible: !iconImg.visible
|
||||
color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.1)
|
||||
radius: Theme.cornerRadiusLarge
|
||||
border.width: 1
|
||||
border.color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.2)
|
||||
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
text: (model.name && model.name.length > 0) ? model.name.charAt(0).toUpperCase() : "A"
|
||||
font.pixelSize: Math.min(24, parent.width * 0.5)
|
||||
color: Theme.primary
|
||||
font.weight: Font.Bold
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Text {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
width: resultsGrid.cellWidth - 12
|
||||
text: model.name
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
elide: Text.ElideRight
|
||||
wrapMode: Text.Wrap
|
||||
maximumLineCount: 2
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: gridMouseArea
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
z: 10
|
||||
onEntered: selectedIndex = index
|
||||
onClicked: launchApp(model)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
layer.effect: MultiEffect {
|
||||
shadowEnabled: true
|
||||
shadowHorizontalOffset: 0
|
||||
shadowVerticalOffset: 8
|
||||
shadowBlur: 1 // radius/32
|
||||
shadowColor: Qt.rgba(0, 0, 0, 0.3)
|
||||
shadowOpacity: 0.3
|
||||
}
|
||||
// Center-screen fade with subtle scale
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: Theme.mediumDuration
|
||||
easing.type: Theme.emphasizedEasing
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Behavior on scale {
|
||||
NumberAnimation {
|
||||
duration: Theme.mediumDuration
|
||||
easing.type: Theme.emphasizedEasing
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
IpcHandler {
|
||||
function open() {
|
||||
console.log("SpotlightLauncher: IPC open() called");
|
||||
spotlightLauncher.show();
|
||||
return "SPOTLIGHT_OPEN_SUCCESS";
|
||||
}
|
||||
|
||||
function close() {
|
||||
console.log("SpotlightLauncher: IPC close() called");
|
||||
spotlightLauncher.hide();
|
||||
return "SPOTLIGHT_CLOSE_SUCCESS";
|
||||
}
|
||||
|
||||
function toggle() {
|
||||
console.log("SpotlightLauncher: IPC toggle() called");
|
||||
spotlightLauncher.toggle();
|
||||
return "SPOTLIGHT_TOGGLE_SUCCESS";
|
||||
}
|
||||
|
||||
target: "spotlight"
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,340 +0,0 @@
|
||||
import QtQuick
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
|
||||
Column {
|
||||
id: themePicker
|
||||
|
||||
spacing: Theme.spacingS
|
||||
|
||||
Text {
|
||||
text: "Current Theme: " + (Theme.isDynamicTheme ? "Auto" : (Theme.currentThemeIndex < Theme.themes.length ? Theme.themes[Theme.currentThemeIndex].name : "Blue"))
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
}
|
||||
|
||||
// Theme description
|
||||
Text {
|
||||
text: {
|
||||
if (Theme.isDynamicTheme)
|
||||
return "Wallpaper-based dynamic colors";
|
||||
|
||||
var descriptions = ["Material blue inspired by modern interfaces", "Deep blue inspired by material 3", "Rich purple tones for BB elegance", "Natural green for productivity", "Energetic orange for creativity", "Bold red for impact", "Cool cyan for tranquility", "Vibrant pink for expression", "Warm amber for comfort", "Soft coral for gentle warmth"];
|
||||
return descriptions[Theme.currentThemeIndex] || "Select a theme";
|
||||
}
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
wrapMode: Text.WordWrap
|
||||
width: Math.min(parent.width, 200)
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
}
|
||||
|
||||
// Grid layout for 10 themes (2 rows of 5)
|
||||
Column {
|
||||
spacing: Theme.spacingS
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
|
||||
// First row - Blue, Deep Blue, Purple, Green, Orange
|
||||
Row {
|
||||
spacing: Theme.spacingM
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
|
||||
Repeater {
|
||||
model: 5
|
||||
|
||||
Rectangle {
|
||||
width: 32
|
||||
height: 32
|
||||
radius: 16
|
||||
color: Theme.themes[index].primary
|
||||
border.color: Theme.outline
|
||||
border.width: (Theme.currentThemeIndex === index && !Theme.isDynamicTheme) ? 2 : 1
|
||||
scale: (Theme.currentThemeIndex === index && !Theme.isDynamicTheme) ? 1.1 : 1
|
||||
|
||||
// Theme name tooltip
|
||||
Rectangle {
|
||||
width: nameText.contentWidth + Theme.spacingS * 2
|
||||
height: nameText.contentHeight + Theme.spacingXS * 2
|
||||
color: Theme.surfaceContainer
|
||||
border.color: Theme.outline
|
||||
border.width: 1
|
||||
radius: Theme.cornerRadiusSmall
|
||||
anchors.bottom: parent.top
|
||||
anchors.bottomMargin: Theme.spacingXS
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
visible: mouseArea.containsMouse
|
||||
|
||||
Text {
|
||||
id: nameText
|
||||
|
||||
text: Theme.themes[index].name
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceText
|
||||
anchors.centerIn: parent
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: mouseArea
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
Theme.switchTheme(index, false);
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on scale {
|
||||
NumberAnimation {
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Theme.emphasizedEasing
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Behavior on border.width {
|
||||
NumberAnimation {
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Theme.emphasizedEasing
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Second row - Red, Cyan, Pink, Amber, Coral
|
||||
Row {
|
||||
spacing: Theme.spacingM
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
|
||||
Repeater {
|
||||
model: 5
|
||||
|
||||
Rectangle {
|
||||
property int themeIndex: index + 5
|
||||
|
||||
width: 32
|
||||
height: 32
|
||||
radius: 16
|
||||
color: themeIndex < Theme.themes.length ? Theme.themes[themeIndex].primary : "transparent"
|
||||
border.color: Theme.outline
|
||||
border.width: Theme.currentThemeIndex === themeIndex ? 2 : 1
|
||||
visible: themeIndex < Theme.themes.length
|
||||
scale: Theme.currentThemeIndex === themeIndex ? 1.1 : 1
|
||||
|
||||
// Theme name tooltip
|
||||
Rectangle {
|
||||
width: nameText2.contentWidth + Theme.spacingS * 2
|
||||
height: nameText2.contentHeight + Theme.spacingXS * 2
|
||||
color: Theme.surfaceContainer
|
||||
border.color: Theme.outline
|
||||
border.width: 1
|
||||
radius: Theme.cornerRadiusSmall
|
||||
anchors.bottom: parent.top
|
||||
anchors.bottomMargin: Theme.spacingXS
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
visible: mouseArea2.containsMouse && themeIndex < Theme.themes.length
|
||||
|
||||
Text {
|
||||
id: nameText2
|
||||
|
||||
text: themeIndex < Theme.themes.length ? Theme.themes[themeIndex].name : ""
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceText
|
||||
anchors.centerIn: parent
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: mouseArea2
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
if (themeIndex < Theme.themes.length)
|
||||
Theme.switchTheme(themeIndex);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on scale {
|
||||
NumberAnimation {
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Theme.emphasizedEasing
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Behavior on border.width {
|
||||
NumberAnimation {
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Theme.emphasizedEasing
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Spacer for better visual separation
|
||||
Item {
|
||||
width: 1
|
||||
height: Theme.spacingM
|
||||
}
|
||||
|
||||
// Auto theme button - prominent oval below the grid
|
||||
Rectangle {
|
||||
width: 120
|
||||
height: 40
|
||||
radius: 20
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
color: {
|
||||
if (ToastService.wallpaperErrorStatus === "error" || ToastService.wallpaperErrorStatus === "matugen_missing")
|
||||
return Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.12);
|
||||
else
|
||||
return Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.3);
|
||||
}
|
||||
border.color: {
|
||||
if (ToastService.wallpaperErrorStatus === "error" || ToastService.wallpaperErrorStatus === "matugen_missing")
|
||||
return Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.5);
|
||||
else if (Theme.isDynamicTheme)
|
||||
return Theme.primary;
|
||||
else
|
||||
return Theme.outline;
|
||||
}
|
||||
border.width: Theme.isDynamicTheme ? 2 : 1
|
||||
scale: Theme.isDynamicTheme ? 1.1 : (autoMouseArea.containsMouse ? 1.02 : 1)
|
||||
|
||||
Row {
|
||||
anchors.centerIn: parent
|
||||
spacing: Theme.spacingS
|
||||
|
||||
Text {
|
||||
text: {
|
||||
if (ToastService.wallpaperErrorStatus === "error" || ToastService.wallpaperErrorStatus === "matugen_missing")
|
||||
return "error";
|
||||
else
|
||||
return "palette";
|
||||
}
|
||||
font.family: Theme.iconFont
|
||||
font.pixelSize: 16
|
||||
color: {
|
||||
if (ToastService.wallpaperErrorStatus === "error" || ToastService.wallpaperErrorStatus === "matugen_missing")
|
||||
return Theme.error;
|
||||
else
|
||||
return Theme.surfaceText;
|
||||
}
|
||||
font.weight: Theme.iconFontWeight
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Text {
|
||||
text: {
|
||||
if (ToastService.wallpaperErrorStatus === "error")
|
||||
return "Error";
|
||||
else if (ToastService.wallpaperErrorStatus === "matugen_missing")
|
||||
return "No matugen";
|
||||
else
|
||||
return "Auto";
|
||||
}
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: {
|
||||
if (ToastService.wallpaperErrorStatus === "error" || ToastService.wallpaperErrorStatus === "matugen_missing")
|
||||
return Theme.error;
|
||||
else
|
||||
return Theme.surfaceText;
|
||||
}
|
||||
font.weight: Font.Medium
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: autoMouseArea
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
Theme.switchTheme(10, true);
|
||||
}
|
||||
}
|
||||
|
||||
// Tooltip for Auto button
|
||||
Rectangle {
|
||||
width: autoTooltipText.contentWidth + Theme.spacingM * 2
|
||||
height: autoTooltipText.contentHeight + Theme.spacingS * 2
|
||||
color: Theme.surfaceContainer
|
||||
border.color: Theme.outline
|
||||
border.width: 1
|
||||
radius: Theme.cornerRadiusSmall
|
||||
anchors.bottom: parent.top
|
||||
anchors.bottomMargin: Theme.spacingS
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
visible: autoMouseArea.containsMouse && (!Theme.isDynamicTheme || ToastService.wallpaperErrorStatus === "error" || ToastService.wallpaperErrorStatus === "matugen_missing")
|
||||
|
||||
Text {
|
||||
id: autoTooltipText
|
||||
|
||||
text: {
|
||||
if (ToastService.wallpaperErrorStatus === "error")
|
||||
return "Wallpaper symlink missing at ~/quickshell/current_wallpaper";
|
||||
else if (ToastService.wallpaperErrorStatus === "matugen_missing")
|
||||
return "Install matugen package for dynamic themes";
|
||||
else
|
||||
return "Dynamic wallpaper-based colors";
|
||||
}
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: (ToastService.wallpaperErrorStatus === "error" || ToastService.wallpaperErrorStatus === "matugen_missing") ? Theme.error : Theme.surfaceText
|
||||
anchors.centerIn: parent
|
||||
wrapMode: Text.WordWrap
|
||||
width: Math.min(implicitWidth, 250)
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Behavior on scale {
|
||||
NumberAnimation {
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Theme.emphasizedEasing
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Theme.mediumDuration
|
||||
easing.type: Theme.standardEasing
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Behavior on border.color {
|
||||
ColorAnimation {
|
||||
duration: Theme.mediumDuration
|
||||
easing.type: Theme.standardEasing
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,135 +0,0 @@
|
||||
import QtQuick
|
||||
import QtQuick.Effects
|
||||
import Quickshell
|
||||
import Quickshell.Wayland
|
||||
import Quickshell.Widgets
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
|
||||
PanelWindow {
|
||||
id: root
|
||||
|
||||
visible: ToastService.toastVisible
|
||||
WlrLayershell.layer: WlrLayershell.Overlay
|
||||
WlrLayershell.exclusiveZone: -1
|
||||
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
|
||||
color: "transparent"
|
||||
|
||||
anchors {
|
||||
top: true
|
||||
left: true
|
||||
right: true
|
||||
bottom: true
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: toast
|
||||
|
||||
width: Math.min(400, Screen.width - Theme.spacingL * 2)
|
||||
height: toastContent.height + Theme.spacingL * 2
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
y: Theme.barHeight + Theme.spacingL
|
||||
color: {
|
||||
switch (ToastService.currentLevel) {
|
||||
case ToastService.levelError:
|
||||
return Theme.error;
|
||||
case ToastService.levelWarn:
|
||||
return Theme.warning;
|
||||
case ToastService.levelInfo:
|
||||
return Theme.primary;
|
||||
default:
|
||||
return Theme.primary;
|
||||
}
|
||||
}
|
||||
radius: Theme.cornerRadiusLarge
|
||||
layer.enabled: true
|
||||
opacity: ToastService.toastVisible ? 0.9 : 0
|
||||
scale: ToastService.toastVisible ? 1 : 0.9
|
||||
|
||||
Row {
|
||||
id: toastContent
|
||||
|
||||
anchors.centerIn: parent
|
||||
spacing: Theme.spacingM
|
||||
|
||||
Text {
|
||||
text: {
|
||||
switch (ToastService.currentLevel) {
|
||||
case ToastService.levelError:
|
||||
return "error";
|
||||
case ToastService.levelWarn:
|
||||
return "warning";
|
||||
case ToastService.levelInfo:
|
||||
return "info";
|
||||
default:
|
||||
return "info";
|
||||
}
|
||||
}
|
||||
font.family: Theme.iconFont
|
||||
font.pixelSize: Theme.iconSize
|
||||
color: Theme.background
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Text {
|
||||
text: ToastService.currentMessage
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.background
|
||||
font.weight: Font.Medium
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
width: Math.min(implicitWidth, 300)
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
onClicked: ToastService.hideToast()
|
||||
}
|
||||
|
||||
layer.effect: MultiEffect {
|
||||
shadowEnabled: true
|
||||
shadowHorizontalOffset: 0
|
||||
shadowVerticalOffset: 4
|
||||
shadowBlur: 0.8
|
||||
shadowColor: Qt.rgba(0, 0, 0, 0.3)
|
||||
shadowOpacity: 0.3
|
||||
}
|
||||
|
||||
transform: Translate {
|
||||
y: ToastService.toastVisible ? 0 : -20
|
||||
}
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: Theme.mediumDuration
|
||||
easing.type: Theme.emphasizedEasing
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Behavior on scale {
|
||||
NumberAnimation {
|
||||
duration: Theme.mediumDuration
|
||||
easing.type: Theme.emphasizedEasing
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Theme.standardEasing
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Makes the background transparent to mouse events
|
||||
mask: Region {
|
||||
item: toast
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,117 +0,0 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import Quickshell.Services.Mpris
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property var audioLevels: [0, 0, 0, 0]
|
||||
readonly property MprisPlayer activePlayer: MprisController.activePlayer
|
||||
readonly property bool hasActiveMedia: activePlayer !== null
|
||||
property bool cavaAvailable: false
|
||||
|
||||
width: 20
|
||||
height: Theme.iconSize
|
||||
|
||||
Process {
|
||||
id: cavaCheck
|
||||
|
||||
command: ["which", "cava"]
|
||||
running: true
|
||||
onExited: (exitCode) => {
|
||||
root.cavaAvailable = exitCode === 0;
|
||||
if (root.cavaAvailable) {
|
||||
console.log("cava found - enabling real audio visualization");
|
||||
cavaProcess.running = Qt.binding(() => {
|
||||
return root.hasActiveMedia && root.activePlayer && root.activePlayer.playbackState === MprisPlaybackState.Playing;
|
||||
});
|
||||
} else {
|
||||
console.log("cava not found - using fallback animation");
|
||||
fallbackTimer.running = Qt.binding(() => {
|
||||
return root.hasActiveMedia && root.activePlayer && root.activePlayer.playbackState === MprisPlaybackState.Playing;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: cavaProcess
|
||||
|
||||
running: false
|
||||
command: ["sh", "-c", `printf '[general]\nmode=normal\nframerate=30\nautosens=0\nsensitivity=50\nbars=4\n[output]\nmethod=raw\nraw_target=/dev/stdout\ndata_format=ascii\nchannels=mono\nmono_option=average\n[smoothing]\nnoise_reduction=20' | cava -p /dev/stdin`]
|
||||
onRunningChanged: {
|
||||
if (!running)
|
||||
root.audioLevels = [0, 0, 0, 0];
|
||||
|
||||
}
|
||||
|
||||
stdout: SplitParser {
|
||||
splitMarker: "\n"
|
||||
onRead: (data) => {
|
||||
if (data.trim()) {
|
||||
let points = data.split(";").map((p) => {
|
||||
return parseFloat(p.trim());
|
||||
}).filter((p) => {
|
||||
return !isNaN(p);
|
||||
});
|
||||
if (points.length >= 4)
|
||||
root.audioLevels = [points[0], points[1], points[2], points[3]];
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: fallbackTimer
|
||||
|
||||
running: false
|
||||
interval: 100
|
||||
repeat: true
|
||||
onTriggered: {
|
||||
root.audioLevels = [Math.random() * 40 + 10, Math.random() * 60 + 20, Math.random() * 50 + 15, Math.random() * 35 + 20];
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
anchors.centerIn: parent
|
||||
spacing: 2
|
||||
|
||||
Repeater {
|
||||
model: 4
|
||||
|
||||
Rectangle {
|
||||
width: 3
|
||||
height: {
|
||||
if (root.activePlayer && root.activePlayer.playbackState === MprisPlaybackState.Playing && root.audioLevels.length > index) {
|
||||
const rawLevel = root.audioLevels[index] || 0;
|
||||
const scaledLevel = Math.sqrt(Math.min(Math.max(rawLevel, 0), 100) / 100) * 100;
|
||||
const maxHeight = Theme.iconSize - 2;
|
||||
const minHeight = 3;
|
||||
return minHeight + (scaledLevel / 100) * (maxHeight - minHeight);
|
||||
}
|
||||
return 3;
|
||||
}
|
||||
radius: 1.5
|
||||
color: Theme.primary
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
Behavior on height {
|
||||
NumberAnimation {
|
||||
duration: 80
|
||||
easing.type: Easing.OutQuad
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,77 +0,0 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import qs.Common
|
||||
|
||||
Rectangle {
|
||||
id: root
|
||||
|
||||
property date currentDate: new Date()
|
||||
|
||||
signal clockClicked()
|
||||
|
||||
width: clockRow.implicitWidth + Theme.spacingS * 2
|
||||
height: 30
|
||||
radius: Theme.cornerRadius
|
||||
color: clockMouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.08)
|
||||
Component.onCompleted: {
|
||||
root.currentDate = systemClock.date;
|
||||
}
|
||||
|
||||
Row {
|
||||
id: clockRow
|
||||
|
||||
anchors.centerIn: parent
|
||||
spacing: Theme.spacingS
|
||||
|
||||
Text {
|
||||
text: Prefs.use24HourClock ? Qt.formatTime(root.currentDate, "H:mm") : Qt.formatTime(root.currentDate, "h:mm AP")
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Text {
|
||||
text: "•"
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.5)
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Text {
|
||||
text: Qt.formatDate(root.currentDate, "ddd d")
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
SystemClock {
|
||||
id: systemClock
|
||||
|
||||
precision: SystemClock.Seconds
|
||||
onDateChanged: root.currentDate = systemClock.date
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: clockMouseArea
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
root.clockClicked();
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Theme.standardEasing
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,141 +0,0 @@
|
||||
import QtQuick
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
|
||||
Rectangle {
|
||||
id: root
|
||||
|
||||
property bool isActive: false
|
||||
|
||||
signal clicked()
|
||||
|
||||
width: Math.max(80, controlIndicators.implicitWidth + Theme.spacingS * 2)
|
||||
height: 30
|
||||
radius: Theme.cornerRadius
|
||||
color: controlCenterArea.containsMouse || root.isActive ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.16) : Qt.rgba(Theme.secondary.r, Theme.secondary.g, Theme.secondary.b, 0.08)
|
||||
|
||||
Row {
|
||||
id: controlIndicators
|
||||
|
||||
anchors.centerIn: parent
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
// Network Status Icon
|
||||
Text {
|
||||
text: {
|
||||
if (NetworkService.networkStatus === "ethernet") {
|
||||
return "lan";
|
||||
} else if (NetworkService.networkStatus === "wifi") {
|
||||
switch (WifiService.wifiSignalStrength) {
|
||||
case "excellent":
|
||||
return "wifi";
|
||||
case "good":
|
||||
return "wifi_2_bar";
|
||||
case "fair":
|
||||
return "wifi_1_bar";
|
||||
case "poor":
|
||||
return "wifi_calling_3";
|
||||
default:
|
||||
return "wifi";
|
||||
}
|
||||
} else {
|
||||
return "wifi_off";
|
||||
}
|
||||
}
|
||||
font.family: Theme.iconFont
|
||||
font.pixelSize: Theme.iconSize - 8
|
||||
font.weight: Theme.iconFontWeight
|
||||
color: NetworkService.networkStatus !== "disconnected" ? Theme.primary : Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.5)
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
visible: true
|
||||
}
|
||||
|
||||
// Bluetooth Icon (when available and enabled) - moved next to network
|
||||
Text {
|
||||
text: "bluetooth"
|
||||
font.family: Theme.iconFont
|
||||
font.pixelSize: Theme.iconSize - 8
|
||||
font.weight: Theme.iconFontWeight
|
||||
color: BluetoothService.enabled ? Theme.primary : Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.5)
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
visible: BluetoothService.available && BluetoothService.enabled
|
||||
}
|
||||
|
||||
// Audio Icon with scroll wheel support
|
||||
Rectangle {
|
||||
width: audioIcon.implicitWidth + 4
|
||||
height: audioIcon.implicitHeight + 4
|
||||
color: "transparent"
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
Text {
|
||||
id: audioIcon
|
||||
|
||||
text: AudioService.sinkMuted ? "volume_off" : AudioService.volumeLevel < 33 ? "volume_down" : "volume_up"
|
||||
font.family: Theme.iconFont
|
||||
font.pixelSize: Theme.iconSize - 8
|
||||
font.weight: Theme.iconFontWeight
|
||||
color: audioWheelArea.containsMouse || controlCenterArea.containsMouse || root.isActive ? Theme.primary : Theme.surfaceText
|
||||
anchors.centerIn: parent
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
// Scroll up - increase volume
|
||||
// Scroll down - decrease volume
|
||||
|
||||
id: audioWheelArea
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
acceptedButtons: Qt.NoButton
|
||||
onWheel: function(wheelEvent) {
|
||||
let delta = wheelEvent.angleDelta.y;
|
||||
let currentVolume = (AudioService.sink && AudioService.sink.audio && AudioService.sink.audio.volume * 100) || 0;
|
||||
let newVolume;
|
||||
if (delta > 0)
|
||||
newVolume = Math.min(100, currentVolume + 5);
|
||||
else
|
||||
newVolume = Math.max(0, currentVolume - 5);
|
||||
if (AudioService.sink && AudioService.sink.audio) {
|
||||
AudioService.sink.audio.muted = false;
|
||||
AudioService.sink.audio.volume = newVolume / 100;
|
||||
}
|
||||
wheelEvent.accepted = true;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Microphone Icon (when active)
|
||||
Text {
|
||||
text: "mic"
|
||||
font.family: Theme.iconFont
|
||||
font.pixelSize: Theme.iconSize - 8
|
||||
font.weight: Theme.iconFontWeight
|
||||
color: Theme.primary
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
visible: false // TODO: Add mic detection
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: controlCenterArea
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
root.clicked();
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Theme.standardEasing
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,85 +0,0 @@
|
||||
import QtQuick
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
|
||||
Rectangle {
|
||||
id: root
|
||||
|
||||
width: Math.max(contentRow.implicitWidth + Theme.spacingS * 2, 60)
|
||||
height: 30
|
||||
radius: Theme.cornerRadius
|
||||
color: mouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.08)
|
||||
clip: true
|
||||
visible: FocusedWindowService.niriAvailable && (FocusedWindowService.focusedAppName || FocusedWindowService.focusedWindowTitle)
|
||||
|
||||
Row {
|
||||
id: contentRow
|
||||
|
||||
anchors.centerIn: parent
|
||||
spacing: Theme.spacingS
|
||||
|
||||
Text {
|
||||
id: appText
|
||||
|
||||
text: FocusedWindowService.focusedAppName || ""
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
font.weight: Font.Medium
|
||||
color: Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
// Limit app name width
|
||||
elide: Text.ElideRight
|
||||
maximumLineCount: 1
|
||||
width: Math.min(implicitWidth, 120)
|
||||
}
|
||||
|
||||
Text {
|
||||
text: "•"
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.5)
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
visible: appText.text && titleText.text
|
||||
}
|
||||
|
||||
Text {
|
||||
id: titleText
|
||||
|
||||
text: FocusedWindowService.focusedWindowTitle || ""
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
font.weight: Font.Medium
|
||||
color: Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
// Limit title width - increased for longer titles
|
||||
elide: Text.ElideRight
|
||||
maximumLineCount: 1
|
||||
width: Math.min(implicitWidth, 350)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
// Non-interactive widget - just provides hover state for visual feedback
|
||||
|
||||
id: mouseArea
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
}
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Theme.standardEasing
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Smooth width animation when the text changes
|
||||
Behavior on width {
|
||||
NumberAnimation {
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Theme.standardEasing
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
import QtQuick
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
|
||||
Rectangle {
|
||||
id: root
|
||||
|
||||
signal clicked()
|
||||
|
||||
property bool isActive: false
|
||||
|
||||
readonly property bool nerdFontAvailable: Qt.fontFamilies()
|
||||
.indexOf("Symbols Nerd Font") !== -1
|
||||
|
||||
width: 40
|
||||
height: 30
|
||||
radius: Theme.cornerRadius
|
||||
color: launcherArea.containsMouse || isActive ? Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.12) : Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.08)
|
||||
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
text: nerdFontAvailable && OSDetectorService.osLogo || "apps"
|
||||
font.family: nerdFontAvailable && OSDetectorService.osLogo ? "Symbols Nerd Font" : Theme.iconFont
|
||||
font.pixelSize: Theme.iconSize - 6
|
||||
font.weight: Theme.iconFontWeight
|
||||
color: Theme.surfaceText
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: launcherArea
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: root.clicked()
|
||||
}
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Theme.standardEasing
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,258 +0,0 @@
|
||||
import QtQuick
|
||||
import Quickshell.Services.Mpris
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
|
||||
Rectangle {
|
||||
id: root
|
||||
|
||||
readonly property MprisPlayer activePlayer: MprisController.activePlayer
|
||||
readonly property bool playerAvailable: activePlayer !== null
|
||||
readonly property int contentWidth: Math.min(280, mediaRow.implicitWidth + Theme.spacingS * 2)
|
||||
|
||||
signal clicked()
|
||||
|
||||
height: 30
|
||||
radius: Theme.cornerRadius
|
||||
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.08)
|
||||
states: [
|
||||
State {
|
||||
name: "shown"
|
||||
when: playerAvailable
|
||||
|
||||
PropertyChanges {
|
||||
target: root
|
||||
opacity: 1
|
||||
width: contentWidth
|
||||
}
|
||||
|
||||
},
|
||||
State {
|
||||
name: "hidden"
|
||||
when: !playerAvailable
|
||||
|
||||
PropertyChanges {
|
||||
target: root
|
||||
opacity: 0
|
||||
width: 0
|
||||
}
|
||||
|
||||
}
|
||||
]
|
||||
transitions: [
|
||||
Transition {
|
||||
from: "shown"
|
||||
to: "hidden"
|
||||
|
||||
SequentialAnimation {
|
||||
PauseAnimation {
|
||||
duration: 500
|
||||
}
|
||||
|
||||
NumberAnimation {
|
||||
properties: "opacity,width"
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Theme.standardEasing
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
},
|
||||
Transition {
|
||||
from: "hidden"
|
||||
to: "shown"
|
||||
|
||||
NumberAnimation {
|
||||
properties: "opacity,width"
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Theme.standardEasing
|
||||
}
|
||||
|
||||
}
|
||||
]
|
||||
|
||||
Row {
|
||||
id: mediaRow
|
||||
|
||||
anchors.centerIn: parent
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
// Media info section (clickable to open full player)
|
||||
Row {
|
||||
id: mediaInfo
|
||||
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
AudioVisualization {
|
||||
width: 20
|
||||
height: Theme.iconSize
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Text {
|
||||
id: mediaText
|
||||
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
width: 140
|
||||
text: {
|
||||
if (!activePlayer || !activePlayer.trackTitle)
|
||||
return "";
|
||||
|
||||
let identity = activePlayer.identity || "";
|
||||
let isWebMedia = identity.toLowerCase().includes("firefox") || identity.toLowerCase().includes("chrome") || identity.toLowerCase().includes("chromium") || identity.toLowerCase().includes("edge") || identity.toLowerCase().includes("safari");
|
||||
let title = "";
|
||||
let subtitle = "";
|
||||
if (isWebMedia && activePlayer.trackTitle) {
|
||||
title = activePlayer.trackTitle;
|
||||
subtitle = activePlayer.trackArtist || identity;
|
||||
} else {
|
||||
title = activePlayer.trackTitle || "Unknown Track";
|
||||
subtitle = activePlayer.trackArtist || "";
|
||||
}
|
||||
if (title.length > 20)
|
||||
title = title.substring(0, 20) + "...";
|
||||
|
||||
if (subtitle.length > 22)
|
||||
subtitle = subtitle.substring(0, 22) + "...";
|
||||
|
||||
return subtitle.length > 0 ? title + " • " + subtitle : title;
|
||||
}
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
elide: Text.ElideRight
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: root.clicked()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Control buttons
|
||||
Row {
|
||||
spacing: Theme.spacingXS
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
// Previous button
|
||||
Rectangle {
|
||||
width: 20
|
||||
height: 20
|
||||
radius: 10
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
color: prevArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
|
||||
visible: root.playerAvailable
|
||||
opacity: (activePlayer && activePlayer.canGoPrevious) ? 1 : 0.3
|
||||
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
text: "skip_previous"
|
||||
font.family: Theme.iconFont
|
||||
font.pixelSize: 12
|
||||
color: Theme.surfaceText
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: prevArea
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
if (activePlayer)
|
||||
activePlayer.previous();
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Play/Pause button
|
||||
Rectangle {
|
||||
width: 24
|
||||
height: 24
|
||||
radius: 12
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
color: activePlayer && activePlayer.playbackState === 1 ? Theme.primary : Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12)
|
||||
visible: root.playerAvailable
|
||||
opacity: activePlayer ? 1 : 0.3
|
||||
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
text: activePlayer && activePlayer.playbackState === 1 ? "pause" : "play_arrow"
|
||||
font.family: Theme.iconFont
|
||||
font.pixelSize: 14
|
||||
color: activePlayer && activePlayer.playbackState === 1 ? Theme.background : Theme.primary
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
if (activePlayer)
|
||||
activePlayer.togglePlaying();
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Next button
|
||||
Rectangle {
|
||||
width: 20
|
||||
height: 20
|
||||
radius: 10
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
color: nextArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
|
||||
visible: playerAvailable
|
||||
opacity: (activePlayer && activePlayer.canGoNext) ? 1 : 0.3
|
||||
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
text: "skip_next"
|
||||
font.family: Theme.iconFont
|
||||
font.pixelSize: 12
|
||||
color: Theme.surfaceText
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: nextArea
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
if (activePlayer)
|
||||
activePlayer.next();
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Theme.standardEasing
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Behavior on width {
|
||||
NumberAnimation {
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Theme.standardEasing
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,58 +0,0 @@
|
||||
import QtQuick
|
||||
import qs.Common
|
||||
|
||||
Rectangle {
|
||||
id: root
|
||||
|
||||
property bool hasUnread: false
|
||||
property bool isActive: false
|
||||
|
||||
signal clicked()
|
||||
|
||||
width: 40
|
||||
height: 30
|
||||
radius: Theme.cornerRadius
|
||||
color: notificationArea.containsMouse || root.isActive ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.16) : Qt.rgba(Theme.secondary.r, Theme.secondary.g, Theme.secondary.b, 0.08)
|
||||
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
text: "notifications"
|
||||
font.family: Theme.iconFont
|
||||
font.pixelSize: Theme.iconSize - 6
|
||||
font.weight: Theme.iconFontWeight
|
||||
color: notificationArea.containsMouse || root.isActive ? Theme.primary : Theme.surfaceText
|
||||
}
|
||||
|
||||
// Notification dot indicator
|
||||
Rectangle {
|
||||
width: 8
|
||||
height: 8
|
||||
radius: 4
|
||||
color: Theme.error
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
anchors.rightMargin: 6
|
||||
anchors.topMargin: 6
|
||||
visible: root.hasUnread
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: notificationArea
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
root.clicked();
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Theme.standardEasing
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,107 +0,0 @@
|
||||
import QtQuick
|
||||
import Quickshell.Services.SystemTray
|
||||
import qs.Common
|
||||
|
||||
Rectangle {
|
||||
id: root
|
||||
|
||||
signal menuRequested(var menu, var item, real x, real y)
|
||||
|
||||
width: Math.max(40, systemTrayRow.implicitWidth + Theme.spacingS * 2)
|
||||
height: 30
|
||||
radius: Theme.cornerRadius
|
||||
color: Qt.rgba(Theme.secondary.r, Theme.secondary.g, Theme.secondary.b, 0.08)
|
||||
visible: systemTrayRow.children.length > 0
|
||||
|
||||
Row {
|
||||
id: systemTrayRow
|
||||
|
||||
anchors.centerIn: parent
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
Repeater {
|
||||
model: SystemTray.items
|
||||
|
||||
delegate: Rectangle {
|
||||
property var trayItem: modelData
|
||||
|
||||
width: 24
|
||||
height: 24
|
||||
radius: Theme.cornerRadiusSmall
|
||||
color: trayItemArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
|
||||
|
||||
Image {
|
||||
anchors.centerIn: parent
|
||||
width: 18
|
||||
height: 18
|
||||
source: {
|
||||
let icon = trayItem && trayItem.icon;
|
||||
if (typeof icon === 'string' || icon instanceof String) {
|
||||
if (icon.includes("?path=")) {
|
||||
const [name, path] = icon.split("?path=");
|
||||
const fileName = name.substring(name.lastIndexOf("/") + 1);
|
||||
return `file://${path}/${fileName}`;
|
||||
}
|
||||
return icon;
|
||||
}
|
||||
return ""; // Return empty string if icon is not a string
|
||||
}
|
||||
asynchronous: true
|
||||
smooth: true
|
||||
fillMode: Image.PreserveAspectFit
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: trayItemArea
|
||||
|
||||
anchors.fill: parent
|
||||
acceptedButtons: Qt.LeftButton | Qt.RightButton
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: (mouse) => {
|
||||
if (!trayItem)
|
||||
return ;
|
||||
|
||||
if (mouse.button === Qt.LeftButton) {
|
||||
if (!trayItem.onlyMenu)
|
||||
trayItem.activate();
|
||||
|
||||
} else if (mouse.button === Qt.RightButton) {
|
||||
if (trayItem && trayItem.hasMenu)
|
||||
customTrayMenu.showMenu(mouse.x, mouse.y);
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
QtObject {
|
||||
id: customTrayMenu
|
||||
|
||||
property bool menuVisible: false
|
||||
|
||||
function showMenu(x, y) {
|
||||
root.menuRequested(customTrayMenu, trayItem, x, y);
|
||||
menuVisible = true;
|
||||
}
|
||||
|
||||
function hideMenu() {
|
||||
menuVisible = false;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Theme.standardEasing
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,287 +0,0 @@
|
||||
import "../../Common/Utilities.js" as Utils
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Effects
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import Quickshell.Services.Mpris
|
||||
import Quickshell.Services.Notifications
|
||||
import Quickshell.Services.SystemTray
|
||||
import Quickshell.Wayland
|
||||
import Quickshell.Widgets
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
PanelWindow {
|
||||
// Proxy objects for external connections
|
||||
|
||||
id: root
|
||||
|
||||
property var modelData
|
||||
property string screenName: modelData.name
|
||||
// Transparency property for the top bar background
|
||||
property real backgroundTransparency: Prefs.topBarTransparency
|
||||
// Notification properties
|
||||
readonly property int notificationCount: NotificationService.notifications.length
|
||||
|
||||
screen: modelData
|
||||
implicitHeight: Theme.barHeight - 4
|
||||
color: "transparent"
|
||||
|
||||
Connections {
|
||||
function onTopBarTransparencyChanged() {
|
||||
root.backgroundTransparency = Prefs.topBarTransparency;
|
||||
}
|
||||
|
||||
target: Prefs
|
||||
}
|
||||
|
||||
QtObject {
|
||||
id: notificationHistory
|
||||
|
||||
property int count: 0
|
||||
}
|
||||
|
||||
anchors {
|
||||
top: true
|
||||
left: true
|
||||
right: true
|
||||
}
|
||||
|
||||
// Floating panel container with margins
|
||||
Item {
|
||||
anchors.fill: parent
|
||||
anchors.margins: 2
|
||||
anchors.topMargin: 6
|
||||
anchors.bottomMargin: 0
|
||||
anchors.leftMargin: 8
|
||||
anchors.rightMargin: 8
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
radius: Theme.cornerRadiusXLarge
|
||||
color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, root.backgroundTransparency)
|
||||
layer.enabled: true
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
color: "transparent"
|
||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
|
||||
border.width: 1
|
||||
radius: parent.radius
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
color: Qt.rgba(Theme.surfaceTint.r, Theme.surfaceTint.g, Theme.surfaceTint.b, 0.04)
|
||||
radius: parent.radius
|
||||
|
||||
SequentialAnimation on opacity {
|
||||
running: false
|
||||
loops: Animation.Infinite
|
||||
|
||||
NumberAnimation {
|
||||
to: 0.08
|
||||
duration: Theme.extraLongDuration
|
||||
easing.type: Theme.standardEasing
|
||||
}
|
||||
|
||||
NumberAnimation {
|
||||
to: 0.02
|
||||
duration: Theme.extraLongDuration
|
||||
easing.type: Theme.standardEasing
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
layer.effect: MultiEffect {
|
||||
shadowEnabled: true
|
||||
shadowHorizontalOffset: 0
|
||||
shadowVerticalOffset: 4
|
||||
shadowBlur: 0.5 // radius/32, adjusted for visual match
|
||||
shadowColor: Qt.rgba(0, 0, 0, 0.15)
|
||||
shadowOpacity: 0.15
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Item {
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: Theme.spacingM
|
||||
anchors.rightMargin: Theme.spacingM
|
||||
anchors.topMargin: Theme.spacingXS
|
||||
anchors.bottomMargin: Theme.spacingXS
|
||||
clip: true
|
||||
|
||||
Row {
|
||||
id: leftSection
|
||||
|
||||
height: parent.height
|
||||
spacing: Theme.spacingXS
|
||||
anchors.left: parent.left
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
LauncherButton {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
isActive: launcher.isVisible
|
||||
onClicked: {
|
||||
appLauncher.toggle();
|
||||
}
|
||||
}
|
||||
|
||||
WorkspaceSwitcher {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
screenName: root.screenName
|
||||
}
|
||||
|
||||
FocusedAppWidget {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
visible: Prefs.showFocusedWindow
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
ClockWidget {
|
||||
id: clockWidget
|
||||
|
||||
anchors.centerIn: parent
|
||||
onClockClicked: {
|
||||
centerCommandCenter.calendarVisible = !centerCommandCenter.calendarVisible;
|
||||
}
|
||||
}
|
||||
|
||||
MediaWidget {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.right: clockWidget.left
|
||||
anchors.rightMargin: Theme.spacingS
|
||||
visible: Prefs.showMusic && MprisController.activePlayer
|
||||
onClicked: {
|
||||
centerCommandCenter.calendarVisible = !centerCommandCenter.calendarVisible;
|
||||
}
|
||||
}
|
||||
|
||||
WeatherWidget {
|
||||
id: weatherWidget
|
||||
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.left: clockWidget.right
|
||||
anchors.leftMargin: Theme.spacingS
|
||||
visible: Prefs.showWeather && WeatherService.weather.available && WeatherService.weather.temp > 0 && WeatherService.weather.tempF > 0
|
||||
onClicked: {
|
||||
centerCommandCenter.calendarVisible = !centerCommandCenter.calendarVisible;
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
id: rightSection
|
||||
|
||||
height: parent.height
|
||||
spacing: Theme.spacingXS
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
SystemTrayWidget {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
visible: Prefs.showSystemTray
|
||||
onMenuRequested: (menu, item, x, y) => {
|
||||
trayMenuPopup.currentTrayMenu = menu;
|
||||
trayMenuPopup.currentTrayItem = item;
|
||||
trayMenuPopup.trayMenuX = rightSection.x + rightSection.width - 400 - Theme.spacingL;
|
||||
trayMenuPopup.trayMenuY = Theme.barHeight - Theme.spacingXS;
|
||||
trayMenuPopup.showTrayMenu = true;
|
||||
menu.menuVisible = true;
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: 40
|
||||
height: 30
|
||||
radius: Theme.cornerRadius
|
||||
color: clipboardArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : Qt.rgba(Theme.secondary.r, Theme.secondary.g, Theme.secondary.b, 0.08)
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
visible: Prefs.showClipboard
|
||||
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
text: "content_paste"
|
||||
font.family: Theme.iconFont
|
||||
font.pixelSize: Theme.iconSize - 6
|
||||
font.weight: Theme.iconFontWeight
|
||||
color: Theme.surfaceText
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: clipboardArea
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
clipboardHistoryPopup.toggle();
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Theme.standardEasing
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// System Monitor Widgets
|
||||
CpuMonitorWidget {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
visible: Prefs.showSystemResources
|
||||
}
|
||||
|
||||
RamMonitorWidget {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
visible: Prefs.showSystemResources
|
||||
}
|
||||
|
||||
NotificationCenterButton {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
hasUnread: root.notificationCount > 0
|
||||
isActive: notificationCenter.notificationHistoryVisible
|
||||
onClicked: {
|
||||
notificationCenter.notificationHistoryVisible = !notificationCenter.notificationHistoryVisible;
|
||||
}
|
||||
}
|
||||
|
||||
// Battery Widget
|
||||
BatteryWidget {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
batteryPopupVisible: batteryControlPopup.batteryPopupVisible
|
||||
onToggleBatteryPopup: {
|
||||
batteryControlPopup.batteryPopupVisible = !batteryControlPopup.batteryPopupVisible;
|
||||
}
|
||||
}
|
||||
|
||||
ControlCenterButton {
|
||||
// Bluetooth devices are automatically updated via signals
|
||||
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
isActive: controlCenterPopup.controlCenterVisible
|
||||
onClicked: {
|
||||
controlCenterPopup.controlCenterVisible = !controlCenterPopup.controlCenterVisible;
|
||||
if (controlCenterPopup.controlCenterVisible) {
|
||||
if (NetworkService.wifiEnabled)
|
||||
WifiService.scanWifi();
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,65 +0,0 @@
|
||||
import QtQuick
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
|
||||
Rectangle {
|
||||
id: root
|
||||
|
||||
signal clicked()
|
||||
|
||||
// Visibility is now controlled by TopBar.qml
|
||||
width: visible ? Math.min(100, weatherRow.implicitWidth + Theme.spacingS * 2) : 0
|
||||
height: 30
|
||||
radius: Theme.cornerRadius
|
||||
color: weatherArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.08)
|
||||
|
||||
Row {
|
||||
id: weatherRow
|
||||
|
||||
anchors.centerIn: parent
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
Text {
|
||||
text: WeatherService.getWeatherIcon(WeatherService.weather.wCode)
|
||||
font.family: Theme.iconFont
|
||||
font.pixelSize: Theme.iconSize - 4
|
||||
color: Theme.primary
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Text {
|
||||
text: (Prefs.useFahrenheit ? WeatherService.weather.tempF : WeatherService.weather.temp) + "°" + (Prefs.useFahrenheit ? "F" : "C")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: weatherArea
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: root.clicked()
|
||||
}
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Theme.standardEasing
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Behavior on width {
|
||||
NumberAnimation {
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Theme.standardEasing
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,124 +0,0 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
|
||||
Rectangle {
|
||||
id: root
|
||||
|
||||
property string screenName: ""
|
||||
property int currentWorkspace: getDisplayActiveWorkspace()
|
||||
property var workspaceList: getDisplayWorkspaces()
|
||||
|
||||
function getDisplayWorkspaces() {
|
||||
if (!NiriWorkspaceService.niriAvailable || NiriWorkspaceService.allWorkspaces.length === 0)
|
||||
return [1, 2];
|
||||
|
||||
if (!root.screenName)
|
||||
return NiriWorkspaceService.getCurrentOutputWorkspaceNumbers();
|
||||
|
||||
var displayWorkspaces = [];
|
||||
for (var i = 0; i < NiriWorkspaceService.allWorkspaces.length; i++) {
|
||||
var ws = NiriWorkspaceService.allWorkspaces[i];
|
||||
if (ws.output === root.screenName)
|
||||
displayWorkspaces.push(ws.idx + 1);
|
||||
|
||||
}
|
||||
return displayWorkspaces.length > 0 ? displayWorkspaces : [1, 2];
|
||||
}
|
||||
|
||||
function getDisplayActiveWorkspace() {
|
||||
if (!NiriWorkspaceService.niriAvailable || NiriWorkspaceService.allWorkspaces.length === 0)
|
||||
return 1;
|
||||
|
||||
if (!root.screenName)
|
||||
return NiriWorkspaceService.getCurrentWorkspaceNumber();
|
||||
|
||||
for (var i = 0; i < NiriWorkspaceService.allWorkspaces.length; i++) {
|
||||
var ws = NiriWorkspaceService.allWorkspaces[i];
|
||||
if (ws.output === root.screenName && ws.is_active)
|
||||
return ws.idx + 1;
|
||||
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
width: Math.max(120, workspaceRow.implicitWidth + Theme.spacingL * 2)
|
||||
height: 30
|
||||
radius: Theme.cornerRadiusLarge
|
||||
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.08)
|
||||
visible: NiriWorkspaceService.niriAvailable
|
||||
|
||||
Connections {
|
||||
function onAllWorkspacesChanged() {
|
||||
root.workspaceList = root.getDisplayWorkspaces();
|
||||
root.currentWorkspace = root.getDisplayActiveWorkspace();
|
||||
}
|
||||
|
||||
function onFocusedWorkspaceIndexChanged() {
|
||||
root.currentWorkspace = root.getDisplayActiveWorkspace();
|
||||
}
|
||||
|
||||
function onNiriAvailableChanged() {
|
||||
if (NiriWorkspaceService.niriAvailable) {
|
||||
root.workspaceList = root.getDisplayWorkspaces();
|
||||
root.currentWorkspace = root.getDisplayActiveWorkspace();
|
||||
}
|
||||
}
|
||||
|
||||
target: NiriWorkspaceService
|
||||
}
|
||||
|
||||
Row {
|
||||
id: workspaceRow
|
||||
|
||||
anchors.centerIn: parent
|
||||
spacing: Theme.spacingS
|
||||
|
||||
Repeater {
|
||||
model: root.workspaceList
|
||||
|
||||
Rectangle {
|
||||
property bool isActive: modelData === root.currentWorkspace
|
||||
property bool isHovered: mouseArea.containsMouse
|
||||
property int sequentialNumber: index + 1
|
||||
|
||||
width: isActive ? Theme.spacingXL + Theme.spacingM : Theme.spacingL + Theme.spacingXS
|
||||
height: Theme.spacingM
|
||||
radius: height / 2
|
||||
color: isActive ? Theme.primary : isHovered ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.5) : Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.3)
|
||||
|
||||
MouseArea {
|
||||
id: mouseArea
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
Quickshell.execDetached(["niri", "msg", "action", "focus-workspace", modelData.toString()]);
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on width {
|
||||
NumberAnimation {
|
||||
duration: Theme.mediumDuration
|
||||
easing.type: Theme.emphasizedEasing
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Theme.mediumDuration
|
||||
easing.type: Theme.emphasizedEasing
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,188 +0,0 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import Quickshell
|
||||
import Quickshell.Wayland
|
||||
import Quickshell.Widgets
|
||||
import qs.Common
|
||||
|
||||
PanelWindow {
|
||||
id: root
|
||||
|
||||
property bool showTrayMenu: false
|
||||
property real trayMenuX: 0
|
||||
property real trayMenuY: 0
|
||||
property var currentTrayMenu: null
|
||||
property var currentTrayItem: null
|
||||
|
||||
visible: showTrayMenu
|
||||
WlrLayershell.layer: WlrLayershell.Overlay
|
||||
WlrLayershell.exclusiveZone: -1
|
||||
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
|
||||
color: "transparent"
|
||||
|
||||
anchors {
|
||||
top: true
|
||||
left: true
|
||||
right: true
|
||||
bottom: true
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: menuContainer
|
||||
|
||||
x: trayMenuX
|
||||
y: trayMenuY
|
||||
width: Math.max(180, Math.min(300, menuList.maxTextWidth + Theme.spacingL * 2))
|
||||
height: Math.max(60, menuList.contentHeight + Theme.spacingS * 2)
|
||||
color: Theme.popupBackground()
|
||||
radius: Theme.cornerRadiusLarge
|
||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
|
||||
border.width: 1
|
||||
// Material 3 animations
|
||||
opacity: showTrayMenu ? 1 : 0
|
||||
scale: showTrayMenu ? 1 : 0.85
|
||||
|
||||
// Material 3 drop shadow
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
anchors.topMargin: 4
|
||||
anchors.leftMargin: 2
|
||||
anchors.rightMargin: -2
|
||||
anchors.bottomMargin: -4
|
||||
radius: parent.radius
|
||||
color: Qt.rgba(0, 0, 0, 0.15)
|
||||
z: parent.z - 1
|
||||
}
|
||||
|
||||
Item {
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingS
|
||||
|
||||
QsMenuOpener {
|
||||
id: menuOpener
|
||||
|
||||
menu: currentTrayItem && currentTrayItem.hasMenu ? currentTrayItem.menu : null
|
||||
}
|
||||
|
||||
// Custom menu styling using ListView
|
||||
ListView {
|
||||
id: menuList
|
||||
|
||||
// Calculate maximum text width for dynamic menu sizing
|
||||
property real maxTextWidth: {
|
||||
let maxWidth = 0;
|
||||
if (model && model.values) {
|
||||
for (let i = 0; i < model.values.length; i++) {
|
||||
const item = model.values[i];
|
||||
if (item && item.text) {
|
||||
const textWidth = textMetrics.advanceWidth * item.text.length * 0.6;
|
||||
maxWidth = Math.max(maxWidth, textWidth);
|
||||
}
|
||||
}
|
||||
}
|
||||
return Math.min(maxWidth, 280); // Cap at reasonable width
|
||||
}
|
||||
|
||||
anchors.fill: parent
|
||||
spacing: 1
|
||||
|
||||
TextMetrics {
|
||||
id: textMetrics
|
||||
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
text: "M"
|
||||
}
|
||||
|
||||
model: menuOpener.children
|
||||
|
||||
delegate: Rectangle {
|
||||
width: ListView.view.width
|
||||
height: modelData.isSeparator ? 5 : 28
|
||||
radius: modelData.isSeparator ? 0 : Theme.cornerRadiusSmall
|
||||
color: modelData.isSeparator ? "transparent" : (menuItemArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent")
|
||||
|
||||
// Separator line
|
||||
Rectangle {
|
||||
visible: modelData.isSeparator
|
||||
anchors.centerIn: parent
|
||||
width: parent.width - Theme.spacingS * 2
|
||||
height: 1
|
||||
color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
|
||||
}
|
||||
|
||||
// Menu item content
|
||||
Row {
|
||||
visible: !modelData.isSeparator
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Theme.spacingS
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
Text {
|
||||
text: modelData.text || ""
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Normal
|
||||
elide: Text.ElideRight
|
||||
maximumLineCount: 1
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: menuItemArea
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: modelData.isSeparator ? Qt.ArrowCursor : Qt.PointingHandCursor
|
||||
enabled: !modelData.isSeparator
|
||||
onClicked: {
|
||||
if (modelData.triggered)
|
||||
modelData.triggered();
|
||||
|
||||
showTrayMenu = false;
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Theme.standardEasing
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: Theme.mediumDuration
|
||||
easing.type: Theme.emphasizedEasing
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Behavior on scale {
|
||||
NumberAnimation {
|
||||
duration: Theme.mediumDuration
|
||||
easing.type: Theme.emphasizedEasing
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Click outside to close
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
z: -1
|
||||
onClicked: {
|
||||
showTrayMenu = false;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,332 +0,0 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import Quickshell
|
||||
import Quickshell.Wayland
|
||||
import Quickshell.Widgets
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
|
||||
PanelWindow {
|
||||
id: root
|
||||
|
||||
property bool wifiPasswordDialogVisible: false
|
||||
property string wifiPasswordSSID: ""
|
||||
property string wifiPasswordInput: ""
|
||||
|
||||
visible: wifiPasswordDialogVisible
|
||||
WlrLayershell.layer: WlrLayershell.Overlay
|
||||
WlrLayershell.exclusiveZone: -1
|
||||
WlrLayershell.keyboardFocus: wifiPasswordDialogVisible ? WlrKeyboardFocus.Exclusive : WlrKeyboardFocus.None
|
||||
color: "transparent"
|
||||
onVisibleChanged: {
|
||||
if (visible)
|
||||
passwordInput.forceActiveFocus();
|
||||
|
||||
}
|
||||
|
||||
anchors {
|
||||
top: true
|
||||
left: true
|
||||
right: true
|
||||
bottom: true
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
color: Qt.rgba(0, 0, 0, 0.5)
|
||||
opacity: wifiPasswordDialogVisible ? 1 : 0
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
onClicked: {
|
||||
wifiPasswordDialogVisible = false;
|
||||
wifiPasswordInput = "";
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: Theme.mediumDuration
|
||||
easing.type: Theme.standardEasing
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: Math.min(400, parent.width - Theme.spacingL * 2)
|
||||
height: Math.min(250, parent.height - Theme.spacingL * 2)
|
||||
anchors.centerIn: parent
|
||||
color: Theme.surfaceContainer
|
||||
radius: Theme.cornerRadiusLarge
|
||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
|
||||
border.width: 1
|
||||
opacity: wifiPasswordDialogVisible ? 1 : 0
|
||||
scale: wifiPasswordDialogVisible ? 1 : 0.9
|
||||
|
||||
Column {
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingL
|
||||
spacing: Theme.spacingL
|
||||
|
||||
// Header
|
||||
Row {
|
||||
width: parent.width
|
||||
|
||||
Column {
|
||||
width: parent.width - 40
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
Text {
|
||||
text: "Connect to Wi-Fi"
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
}
|
||||
|
||||
Text {
|
||||
text: "Enter password for \"" + wifiPasswordSSID + "\""
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
|
||||
width: parent.width
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: 32
|
||||
height: 32
|
||||
radius: 16
|
||||
color: closeDialogArea.containsMouse ? Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.12) : "transparent"
|
||||
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
text: "close"
|
||||
font.family: Theme.iconFont
|
||||
font.pixelSize: Theme.iconSize - 4
|
||||
color: closeDialogArea.containsMouse ? Theme.error : Theme.surfaceText
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: closeDialogArea
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
wifiPasswordDialogVisible = false;
|
||||
wifiPasswordInput = "";
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Password input
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: 50
|
||||
radius: Theme.cornerRadius
|
||||
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08)
|
||||
border.color: passwordInput.activeFocus ? Theme.primary : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
|
||||
border.width: passwordInput.activeFocus ? 2 : 1
|
||||
|
||||
TextInput {
|
||||
id: passwordInput
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingM
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceText
|
||||
echoMode: showPasswordCheckbox.checked ? TextInput.Normal : TextInput.Password
|
||||
verticalAlignment: TextInput.AlignVCenter
|
||||
cursorVisible: activeFocus
|
||||
selectByMouse: true
|
||||
onTextChanged: {
|
||||
wifiPasswordInput = text;
|
||||
}
|
||||
onAccepted: {
|
||||
WifiService.connectToWifiWithPassword(wifiPasswordSSID, wifiPasswordInput);
|
||||
}
|
||||
Component.onCompleted: {
|
||||
if (wifiPasswordDialogVisible)
|
||||
forceActiveFocus();
|
||||
|
||||
}
|
||||
|
||||
Text {
|
||||
anchors.fill: parent
|
||||
text: "Enter password"
|
||||
font: parent.font
|
||||
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.5)
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
visible: parent.text.length === 0
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
cursorShape: Qt.IBeamCursor
|
||||
onClicked: {
|
||||
passwordInput.forceActiveFocus();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Show password checkbox
|
||||
Row {
|
||||
spacing: Theme.spacingS
|
||||
|
||||
Rectangle {
|
||||
id: showPasswordCheckbox
|
||||
|
||||
property bool checked: false
|
||||
|
||||
width: 20
|
||||
height: 20
|
||||
radius: 4
|
||||
color: checked ? Theme.primary : "transparent"
|
||||
border.color: checked ? Theme.primary : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.5)
|
||||
border.width: 2
|
||||
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
text: "check"
|
||||
font.family: Theme.iconFont
|
||||
font.pixelSize: 12
|
||||
color: Theme.background
|
||||
visible: parent.checked
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
showPasswordCheckbox.checked = !showPasswordCheckbox.checked;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Text {
|
||||
text: "Show password"
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Buttons
|
||||
Item {
|
||||
width: parent.width
|
||||
height: 40
|
||||
|
||||
Row {
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Theme.spacingM
|
||||
|
||||
Rectangle {
|
||||
width: Math.max(70, cancelText.contentWidth + Theme.spacingM * 2)
|
||||
height: 36
|
||||
radius: Theme.cornerRadius
|
||||
color: cancelArea.containsMouse ? Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.08) : "transparent"
|
||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
|
||||
border.width: 1
|
||||
|
||||
Text {
|
||||
id: cancelText
|
||||
|
||||
anchors.centerIn: parent
|
||||
text: "Cancel"
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: cancelArea
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
wifiPasswordDialogVisible = false;
|
||||
wifiPasswordInput = "";
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: Math.max(80, connectText.contentWidth + Theme.spacingM * 2)
|
||||
height: 36
|
||||
radius: Theme.cornerRadius
|
||||
color: connectArea.containsMouse ? Qt.darker(Theme.primary, 1.1) : Theme.primary
|
||||
enabled: wifiPasswordInput.length > 0
|
||||
opacity: enabled ? 1 : 0.5
|
||||
|
||||
Text {
|
||||
id: connectText
|
||||
|
||||
anchors.centerIn: parent
|
||||
text: "Connect"
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.background
|
||||
font.weight: Font.Medium
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: connectArea
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
enabled: parent.enabled
|
||||
onClicked: {
|
||||
WifiService.connectToWifiWithPassword(wifiPasswordSSID, wifiPasswordInput);
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Theme.standardEasing
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: Theme.mediumDuration
|
||||
easing.type: Theme.emphasizedEasing
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Behavior on scale {
|
||||
NumberAnimation {
|
||||
duration: Theme.mediumDuration
|
||||
easing.type: Theme.emphasizedEasing
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user