mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-01-24 21:42:51 -05:00
Modularlize the TopBar
This commit is contained in:
@@ -1,869 +0,0 @@
|
|||||||
import QtQuick
|
|
||||||
import QtQuick.Controls
|
|
||||||
import Qt5Compat.GraphicalEffects
|
|
||||||
import Quickshell
|
|
||||||
import Quickshell.Widgets
|
|
||||||
import Quickshell.Wayland
|
|
||||||
import Quickshell.Io
|
|
||||||
import Quickshell.Services.SystemTray
|
|
||||||
import Quickshell.Services.Notifications
|
|
||||||
import Quickshell.Services.Mpris
|
|
||||||
import "../Common"
|
|
||||||
import "../Services"
|
|
||||||
|
|
||||||
PanelWindow {
|
|
||||||
id: topBar
|
|
||||||
|
|
||||||
// modelData contains the screen from Quickshell.screens
|
|
||||||
property var modelData
|
|
||||||
screen: modelData
|
|
||||||
|
|
||||||
// Get the screen name (e.g., "DP-1", "DP-2")
|
|
||||||
property string screenName: modelData.name
|
|
||||||
|
|
||||||
anchors {
|
|
||||||
top: true
|
|
||||||
left: true
|
|
||||||
right: true
|
|
||||||
}
|
|
||||||
|
|
||||||
implicitHeight: Theme.barHeight - 4
|
|
||||||
color: "transparent"
|
|
||||||
|
|
||||||
// Audio visualization data
|
|
||||||
property list<real> audioLevels: [0, 0, 0, 0]
|
|
||||||
|
|
||||||
// Real-time audio visualization using cava (with fallback)
|
|
||||||
property bool cavaAvailable: false
|
|
||||||
|
|
||||||
Process {
|
|
||||||
id: cavaCheck
|
|
||||||
command: ["which", "cava"]
|
|
||||||
running: true
|
|
||||||
onExited: (exitCode) => {
|
|
||||||
topBar.cavaAvailable = exitCode === 0
|
|
||||||
if (topBar.cavaAvailable) {
|
|
||||||
console.log("cava found - creating config and enabling real audio visualization")
|
|
||||||
configWriter.running = true
|
|
||||||
} else {
|
|
||||||
console.log("cava not found - using fallback animation")
|
|
||||||
fallbackTimer.running = Qt.binding(() => root.hasActiveMedia && root.activePlayer?.playbackState === MprisPlaybackState.Playing)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create temporary config file for cava
|
|
||||||
Process {
|
|
||||||
id: configWriter
|
|
||||||
running: topBar.cavaAvailable
|
|
||||||
command: [
|
|
||||||
"sh", "-c",
|
|
||||||
`cat > /tmp/quickshell_cava_config << 'EOF'
|
|
||||||
[general]
|
|
||||||
mode = normal
|
|
||||||
framerate = 30
|
|
||||||
autosens = 0
|
|
||||||
sensitivity = 50
|
|
||||||
bars = 4
|
|
||||||
|
|
||||||
[output]
|
|
||||||
method = raw
|
|
||||||
raw_target = /dev/stdout
|
|
||||||
data_format = ascii
|
|
||||||
channels = mono
|
|
||||||
mono_option = average
|
|
||||||
|
|
||||||
[smoothing]
|
|
||||||
noise_reduction = 20
|
|
||||||
EOF`
|
|
||||||
]
|
|
||||||
|
|
||||||
onExited: {
|
|
||||||
// Start cava after config is written
|
|
||||||
if (topBar.cavaAvailable) {
|
|
||||||
cavaProcess.running = Qt.binding(() => root.hasActiveMedia && root.activePlayer?.playbackState === MprisPlaybackState.Playing)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Process {
|
|
||||||
id: cavaProcess
|
|
||||||
running: false
|
|
||||||
command: ["cava", "-p", "/tmp/quickshell_cava_config"]
|
|
||||||
|
|
||||||
stdout: SplitParser {
|
|
||||||
splitMarker: "\n"
|
|
||||||
onRead: data => {
|
|
||||||
if (data.trim()) {
|
|
||||||
// Parse semicolon-separated values from cava
|
|
||||||
let points = data.split(";").map(p => parseFloat(p.trim())).filter(p => !isNaN(p))
|
|
||||||
if (points.length >= 4) {
|
|
||||||
topBar.audioLevels = [points[0], points[1], points[2], points[3]]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onRunningChanged: {
|
|
||||||
if (!running) {
|
|
||||||
topBar.audioLevels = [0, 0, 0, 0]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fallback animation when cava is not available
|
|
||||||
Timer {
|
|
||||||
id: fallbackTimer
|
|
||||||
running: false
|
|
||||||
interval: 100
|
|
||||||
repeat: true
|
|
||||||
onTriggered: {
|
|
||||||
// Generate smooth random values for fallback (0-100 range)
|
|
||||||
topBar.audioLevels = [
|
|
||||||
Math.random() * 40 + 10, // 10-50
|
|
||||||
Math.random() * 60 + 20, // 20-80
|
|
||||||
Math.random() * 50 + 15, // 15-65
|
|
||||||
Math.random() * 35 + 20 // 20-55
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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, 0.75)
|
|
||||||
|
|
||||||
// Material 3 elevation shadow
|
|
||||||
layer.enabled: true
|
|
||||||
layer.effect: DropShadow {
|
|
||||||
horizontalOffset: 0
|
|
||||||
verticalOffset: 4
|
|
||||||
radius: 16
|
|
||||||
samples: 33
|
|
||||||
color: Qt.rgba(0, 0, 0, 0.15)
|
|
||||||
transparentBorder: true
|
|
||||||
}
|
|
||||||
|
|
||||||
// Subtle border for definition
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
// Subtle surface tint overlay with animation
|
|
||||||
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: true
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Item {
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.leftMargin: Theme.spacingM
|
|
||||||
anchors.rightMargin: Theme.spacingM
|
|
||||||
anchors.topMargin: Theme.spacingXS
|
|
||||||
anchors.bottomMargin: Theme.spacingXS
|
|
||||||
|
|
||||||
Row {
|
|
||||||
id: leftSection
|
|
||||||
height: parent.height
|
|
||||||
spacing: Theme.spacingXS
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
id: archLauncher
|
|
||||||
width: 40
|
|
||||||
height: 32
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: launcherArea.containsMouse ? 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)
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
|
|
||||||
Text {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
text: root.osLogo || "apps" // Use OS logo if detected, fallback to apps icon
|
|
||||||
font.family: root.osLogo ? "NerdFont" : 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: {
|
|
||||||
LauncherService.toggleAppLauncher()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Behavior on color {
|
|
||||||
ColorAnimation {
|
|
||||||
duration: Theme.shortDuration
|
|
||||||
easing.type: Theme.standardEasing
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
id: workspaceSwitcher
|
|
||||||
width: Math.max(120, workspaceRow.implicitWidth + Theme.spacingL * 2)
|
|
||||||
height: 32
|
|
||||||
radius: Theme.cornerRadiusLarge
|
|
||||||
color: Qt.rgba(Theme.surfaceContainerHigh.r, Theme.surfaceContainerHigh.g, Theme.surfaceContainerHigh.b, 0.8)
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
visible: NiriWorkspaceService.niriAvailable
|
|
||||||
|
|
||||||
|
|
||||||
// Use reactive properties from NiriWorkspaceService
|
|
||||||
property int currentWorkspace: getDisplayActiveWorkspace()
|
|
||||||
property var workspaceList: getDisplayWorkspaces()
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// Get workspaces for this display
|
|
||||||
function getDisplayWorkspaces() {
|
|
||||||
// Always return something for now, even if service isn't ready
|
|
||||||
if (!NiriWorkspaceService.niriAvailable || NiriWorkspaceService.allWorkspaces.length === 0) {
|
|
||||||
return [1, 2] // Fallback
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!topBar.screenName) {
|
|
||||||
return NiriWorkspaceService.getCurrentOutputWorkspaceNumbers()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Filter workspaces for this specific display
|
|
||||||
var displayWorkspaces = []
|
|
||||||
for (var i = 0; i < NiriWorkspaceService.allWorkspaces.length; i++) {
|
|
||||||
var ws = NiriWorkspaceService.allWorkspaces[i]
|
|
||||||
if (ws.output === topBar.screenName) {
|
|
||||||
displayWorkspaces.push(ws.idx + 1) // Convert to 1-based
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return displayWorkspaces.length > 0 ? displayWorkspaces : [1, 2]
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get active workspace for this display
|
|
||||||
function getDisplayActiveWorkspace() {
|
|
||||||
// Always return something for now, even if service isn't ready
|
|
||||||
if (!NiriWorkspaceService.niriAvailable || NiriWorkspaceService.allWorkspaces.length === 0) {
|
|
||||||
return 1 // Fallback
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!topBar.screenName) {
|
|
||||||
return NiriWorkspaceService.getCurrentWorkspaceNumber()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Find active workspace for this display (is_active, not is_focused)
|
|
||||||
for (var i = 0; i < NiriWorkspaceService.allWorkspaces.length; i++) {
|
|
||||||
var ws = NiriWorkspaceService.allWorkspaces[i]
|
|
||||||
if (ws.output === topBar.screenName && ws.is_active) {
|
|
||||||
return ws.idx + 1 // Convert to 1-based
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
|
|
||||||
// React to workspace changes
|
|
||||||
Connections {
|
|
||||||
target: NiriWorkspaceService
|
|
||||||
function onAllWorkspacesChanged() {
|
|
||||||
workspaceSwitcher.workspaceList = workspaceSwitcher.getDisplayWorkspaces()
|
|
||||||
workspaceSwitcher.currentWorkspace = workspaceSwitcher.getDisplayActiveWorkspace()
|
|
||||||
}
|
|
||||||
function onFocusedWorkspaceIndexChanged() {
|
|
||||||
workspaceSwitcher.currentWorkspace = workspaceSwitcher.getDisplayActiveWorkspace()
|
|
||||||
}
|
|
||||||
function onNiriAvailableChanged() {
|
|
||||||
if (NiriWorkspaceService.niriAvailable) {
|
|
||||||
workspaceSwitcher.workspaceList = workspaceSwitcher.getDisplayWorkspaces()
|
|
||||||
workspaceSwitcher.currentWorkspace = workspaceSwitcher.getDisplayActiveWorkspace()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Row {
|
|
||||||
id: workspaceRow
|
|
||||||
anchors.centerIn: parent
|
|
||||||
spacing: Theme.spacingS
|
|
||||||
|
|
||||||
Repeater {
|
|
||||||
model: workspaceSwitcher.workspaceList
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
property bool isActive: modelData === workspaceSwitcher.currentWorkspace
|
|
||||||
property bool isHovered: mouseArea.containsMouse
|
|
||||||
property int sequentialNumber: index + 1 // 1-based sequential number per monitor
|
|
||||||
|
|
||||||
|
|
||||||
width: isActive ? Theme.spacingXL + Theme.spacingS : Theme.spacingL
|
|
||||||
height: Theme.spacingS
|
|
||||||
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)
|
|
||||||
|
|
||||||
Behavior on width {
|
|
||||||
NumberAnimation {
|
|
||||||
duration: Theme.mediumDuration
|
|
||||||
easing.type: Theme.emphasizedEasing
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Behavior on color {
|
|
||||||
ColorAnimation {
|
|
||||||
duration: Theme.mediumDuration
|
|
||||||
easing.type: Theme.emphasizedEasing
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: mouseArea
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
|
|
||||||
onClicked: {
|
|
||||||
// Use sequential workspace number directly - niri focus-workspace uses 1,2,3,etc per monitor
|
|
||||||
Quickshell.execDetached(["niri", "msg", "action", "focus-workspace", sequentialNumber.toString()])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
id: clockContainer
|
|
||||||
width: {
|
|
||||||
let baseWidth = 200
|
|
||||||
if (root.hasActiveMedia) {
|
|
||||||
// Calculate width needed for media info + time/date + spacing + padding
|
|
||||||
let mediaWidth = 24 + Theme.spacingXS + mediaTitleText.implicitWidth + Theme.spacingM + 180
|
|
||||||
return Math.min(Math.max(mediaWidth, 300), parent.width - Theme.spacingL * 2)
|
|
||||||
} else if (root.weather.available) {
|
|
||||||
return Math.min(280, parent.width - Theme.spacingL * 2)
|
|
||||||
} else {
|
|
||||||
return Math.min(baseWidth, parent.width - Theme.spacingL * 2)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
height: 32
|
|
||||||
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)
|
|
||||||
anchors.centerIn: parent
|
|
||||||
|
|
||||||
Behavior on color {
|
|
||||||
ColorAnimation {
|
|
||||||
duration: Theme.shortDuration
|
|
||||||
easing.type: Theme.standardEasing
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
property date currentDate: new Date()
|
|
||||||
|
|
||||||
Row {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
|
|
||||||
// Media info or Weather info
|
|
||||||
Row {
|
|
||||||
spacing: Theme.spacingXS
|
|
||||||
visible: root.hasActiveMedia || root.weather.available
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
|
|
||||||
// Animated equalizer when media is playing
|
|
||||||
Item {
|
|
||||||
width: 20
|
|
||||||
height: Theme.iconSize
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
visible: root.hasActiveMedia
|
|
||||||
|
|
||||||
Row {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
spacing: 2
|
|
||||||
|
|
||||||
Repeater {
|
|
||||||
model: 4
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: 3
|
|
||||||
height: {
|
|
||||||
if (root.activePlayer?.playbackState === MprisPlaybackState.Playing && topBar.audioLevels.length > index) {
|
|
||||||
// Scale and compress audio data for better visual range
|
|
||||||
const rawLevel = topBar.audioLevels[index] || 0
|
|
||||||
// Use square root to compress high values and expand low values
|
|
||||||
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 // Minimum height when not playing
|
|
||||||
}
|
|
||||||
radius: 1.5
|
|
||||||
color: Theme.primary
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
|
|
||||||
Behavior on height {
|
|
||||||
NumberAnimation {
|
|
||||||
duration: 80 // Slightly slower for smoother movement
|
|
||||||
easing.type: Easing.OutQuad
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Song title when media is playing
|
|
||||||
Text {
|
|
||||||
id: mediaTitleText
|
|
||||||
text: root.activePlayer?.trackTitle || "Unknown Track"
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
color: Theme.surfaceText
|
|
||||||
font.weight: Font.Medium
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
visible: root.hasActiveMedia
|
|
||||||
width: Math.min(implicitWidth, clockContainer.width - 100)
|
|
||||||
elide: Text.ElideRight
|
|
||||||
}
|
|
||||||
|
|
||||||
// Weather icon when no media but weather available
|
|
||||||
Text {
|
|
||||||
text: WeatherService.getWeatherIcon(root.weather.wCode)
|
|
||||||
font.family: Theme.iconFont
|
|
||||||
font.pixelSize: Theme.iconSize - 2
|
|
||||||
color: Theme.surfaceText
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
visible: !root.hasActiveMedia && root.weather.available
|
|
||||||
}
|
|
||||||
|
|
||||||
// Weather temp when no media but weather available
|
|
||||||
Text {
|
|
||||||
text: (root.useFahrenheit ? root.weather.tempF : root.weather.temp) + "°" + (root.useFahrenheit ? "F" : "C")
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
color: Theme.surfaceText
|
|
||||||
font.weight: Font.Medium
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
visible: !root.hasActiveMedia && root.weather.available
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Separator
|
|
||||||
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: root.hasActiveMedia || root.weather.available
|
|
||||||
}
|
|
||||||
|
|
||||||
// Time and date
|
|
||||||
Row {
|
|
||||||
spacing: Theme.spacingS
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
|
|
||||||
Text {
|
|
||||||
text: Qt.formatTime(clockContainer.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(clockContainer.currentDate, "ddd d")
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
color: Theme.surfaceText
|
|
||||||
font.weight: Font.Medium
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Timer {
|
|
||||||
interval: 1000
|
|
||||||
running: true
|
|
||||||
repeat: true
|
|
||||||
onTriggered: {
|
|
||||||
clockContainer.currentDate = new Date()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: clockMouseArea
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
|
|
||||||
onClicked: {
|
|
||||||
root.calendarVisible = !root.calendarVisible
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Row {
|
|
||||||
id: rightSection
|
|
||||||
height: parent.height
|
|
||||||
spacing: Theme.spacingXS
|
|
||||||
anchors.right: parent.right
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: Math.max(40, systemTrayRow.implicitWidth + Theme.spacingS * 2)
|
|
||||||
height: 32
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: Qt.rgba(Theme.secondary.r, Theme.secondary.g, Theme.secondary.b, 0.08)
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
visible: systemTrayRow.children.length > 0
|
|
||||||
|
|
||||||
Row {
|
|
||||||
id: systemTrayRow
|
|
||||||
anchors.centerIn: parent
|
|
||||||
spacing: Theme.spacingXS
|
|
||||||
|
|
||||||
Repeater {
|
|
||||||
model: SystemTray.items
|
|
||||||
delegate: Rectangle {
|
|
||||||
width: 24
|
|
||||||
height: 24
|
|
||||||
radius: Theme.cornerRadiusSmall
|
|
||||||
color: trayItemArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
|
|
||||||
|
|
||||||
property var trayItem: modelData
|
|
||||||
|
|
||||||
Image {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
width: 18
|
|
||||||
height: 18
|
|
||||||
source: {
|
|
||||||
let icon = trayItem?.icon || "";
|
|
||||||
if (!icon) return "";
|
|
||||||
|
|
||||||
if (icon.includes("?path=")) {
|
|
||||||
const [name, path] = icon.split("?path=");
|
|
||||||
const fileName = name.substring(name.lastIndexOf("/") + 1);
|
|
||||||
return `file://${path}/${fileName}`;
|
|
||||||
}
|
|
||||||
return icon;
|
|
||||||
}
|
|
||||||
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.hasMenu) {
|
|
||||||
console.log("Right-click detected, showing menu for:", trayItem.title || "Unknown")
|
|
||||||
customTrayMenu.showMenu(mouse.x, mouse.y)
|
|
||||||
} else {
|
|
||||||
console.log("No menu available for:", trayItem.title || "Unknown")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Custom Material 3 styled menu
|
|
||||||
QtObject {
|
|
||||||
id: customTrayMenu
|
|
||||||
|
|
||||||
property bool menuVisible: false
|
|
||||||
|
|
||||||
function showMenu(x, y) {
|
|
||||||
root.currentTrayMenu = customTrayMenu
|
|
||||||
root.currentTrayItem = trayItem
|
|
||||||
|
|
||||||
// Simple positioning: right side of screen, below the panel
|
|
||||||
root.trayMenuX = rightSection.x + rightSection.width - 400 - Theme.spacingL
|
|
||||||
root.trayMenuY = Theme.barHeight + Theme.spacingS
|
|
||||||
|
|
||||||
console.log("Showing menu at:", root.trayMenuX, root.trayMenuY)
|
|
||||||
menuVisible = true
|
|
||||||
root.showTrayMenu = true
|
|
||||||
}
|
|
||||||
|
|
||||||
function hideMenu() {
|
|
||||||
menuVisible = false
|
|
||||||
root.showTrayMenu = false
|
|
||||||
root.currentTrayMenu = null
|
|
||||||
root.currentTrayItem = null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Behavior on color {
|
|
||||||
ColorAnimation {
|
|
||||||
duration: Theme.shortDuration
|
|
||||||
easing.type: Theme.standardEasing
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clipboard History Button
|
|
||||||
Rectangle {
|
|
||||||
width: 40
|
|
||||||
height: 32
|
|
||||||
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
|
|
||||||
|
|
||||||
Text {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
text: "content_paste" // Material icon for clipboard
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
RamMonitorWidget {
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// Notification Center Button
|
|
||||||
Rectangle {
|
|
||||||
width: 40
|
|
||||||
height: 32
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: notificationArea.containsMouse || root.notificationHistoryVisible ?
|
|
||||||
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)
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
|
|
||||||
property bool hasUnread: notificationHistory.count > 0
|
|
||||||
|
|
||||||
Text {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
text: "notifications" // Material icon for notifications
|
|
||||||
font.family: Theme.iconFont
|
|
||||||
font.pixelSize: Theme.iconSize - 6
|
|
||||||
font.weight: Theme.iconFontWeight
|
|
||||||
color: notificationArea.containsMouse || root.notificationHistoryVisible ?
|
|
||||||
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: parent.hasUnread
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: notificationArea
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
|
|
||||||
onClicked: {
|
|
||||||
root.notificationHistoryVisible = !root.notificationHistoryVisible
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Behavior on color {
|
|
||||||
ColorAnimation {
|
|
||||||
duration: Theme.shortDuration
|
|
||||||
easing.type: Theme.standardEasing
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Battery Widget
|
|
||||||
BatteryWidget {
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
// Control Center Indicators
|
|
||||||
Rectangle {
|
|
||||||
width: Math.max(80, controlIndicators.implicitWidth + Theme.spacingS * 2)
|
|
||||||
height: 32
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: controlCenterArea.containsMouse || root.controlCenterVisible ?
|
|
||||||
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)
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
|
|
||||||
Row {
|
|
||||||
id: controlIndicators
|
|
||||||
anchors.centerIn: parent
|
|
||||||
spacing: Theme.spacingXS
|
|
||||||
|
|
||||||
// Network Status Icon
|
|
||||||
Text {
|
|
||||||
text: {
|
|
||||||
if (root.networkStatus === "ethernet") return "lan"
|
|
||||||
else if (root.networkStatus === "wifi") {
|
|
||||||
switch (root.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: root.networkStatus !== "disconnected" ? Theme.primary : Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.5)
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
visible: true
|
|
||||||
}
|
|
||||||
|
|
||||||
// Audio Icon
|
|
||||||
Text {
|
|
||||||
text: root.volumeLevel === 0 ? "volume_off" :
|
|
||||||
root.volumeLevel < 33 ? "volume_down" : "volume_up"
|
|
||||||
font.family: Theme.iconFont
|
|
||||||
font.pixelSize: Theme.iconSize - 8
|
|
||||||
font.weight: Theme.iconFontWeight
|
|
||||||
color: controlCenterArea.containsMouse || root.controlCenterVisible ?
|
|
||||||
Theme.primary : Theme.surfaceText
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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
|
|
||||||
}
|
|
||||||
|
|
||||||
// Bluetooth Icon (when available and enabled)
|
|
||||||
Text {
|
|
||||||
text: "bluetooth"
|
|
||||||
font.family: Theme.iconFont
|
|
||||||
font.pixelSize: Theme.iconSize - 8
|
|
||||||
font.weight: Theme.iconFontWeight
|
|
||||||
color: root.bluetoothEnabled ? Theme.primary : Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.5)
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
visible: root.bluetoothAvailable && root.bluetoothEnabled
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: controlCenterArea
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
|
|
||||||
onClicked: {
|
|
||||||
root.controlCenterVisible = !root.controlCenterVisible
|
|
||||||
if (root.controlCenterVisible) {
|
|
||||||
// Refresh data when opening control center
|
|
||||||
WifiService.scanWifi()
|
|
||||||
BluetoothService.scanDevices()
|
|
||||||
// Audio sink info is automatically refreshed by AudioService
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Behavior on color {
|
|
||||||
ColorAnimation {
|
|
||||||
duration: Theme.shortDuration
|
|
||||||
easing.type: Theme.standardEasing
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Power Button
|
|
||||||
PowerButton {
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
137
Widgets/TopBar/AudioVisualization.qml
Normal file
137
Widgets/TopBar/AudioVisualization.qml
Normal file
@@ -0,0 +1,137 @@
|
|||||||
|
import QtQuick
|
||||||
|
import Quickshell
|
||||||
|
import Quickshell.Io
|
||||||
|
import Quickshell.Services.Mpris
|
||||||
|
import "../../Common"
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property list<real> audioLevels: [0, 0, 0, 0]
|
||||||
|
property bool hasActiveMedia: false
|
||||||
|
property var 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 - creating config and enabling real audio visualization")
|
||||||
|
configWriter.running = true
|
||||||
|
} else {
|
||||||
|
console.log("cava not found - using fallback animation")
|
||||||
|
fallbackTimer.running = Qt.binding(() => root.hasActiveMedia && root.activePlayer?.playbackState === MprisPlaybackState.Playing)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Process {
|
||||||
|
id: configWriter
|
||||||
|
running: root.cavaAvailable
|
||||||
|
command: [
|
||||||
|
"sh", "-c",
|
||||||
|
`cat > /tmp/quickshell_cava_config << 'EOF'
|
||||||
|
[general]
|
||||||
|
mode = normal
|
||||||
|
framerate = 30
|
||||||
|
autosens = 0
|
||||||
|
sensitivity = 50
|
||||||
|
bars = 4
|
||||||
|
|
||||||
|
[output]
|
||||||
|
method = raw
|
||||||
|
raw_target = /dev/stdout
|
||||||
|
data_format = ascii
|
||||||
|
channels = mono
|
||||||
|
mono_option = average
|
||||||
|
|
||||||
|
[smoothing]
|
||||||
|
noise_reduction = 20
|
||||||
|
EOF`
|
||||||
|
]
|
||||||
|
|
||||||
|
onExited: {
|
||||||
|
if (root.cavaAvailable) {
|
||||||
|
cavaProcess.running = Qt.binding(() => root.hasActiveMedia && root.activePlayer?.playbackState === MprisPlaybackState.Playing)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Process {
|
||||||
|
id: cavaProcess
|
||||||
|
running: false
|
||||||
|
command: ["cava", "-p", "/tmp/quickshell_cava_config"]
|
||||||
|
|
||||||
|
stdout: SplitParser {
|
||||||
|
splitMarker: "\n"
|
||||||
|
onRead: data => {
|
||||||
|
if (data.trim()) {
|
||||||
|
let points = data.split(";").map(p => parseFloat(p.trim())).filter(p => !isNaN(p))
|
||||||
|
if (points.length >= 4) {
|
||||||
|
root.audioLevels = [points[0], points[1], points[2], points[3]]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onRunningChanged: {
|
||||||
|
if (!running) {
|
||||||
|
root.audioLevels = [0, 0, 0, 0]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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?.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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
155
Widgets/TopBar/ClockWidget.qml
Normal file
155
Widgets/TopBar/ClockWidget.qml
Normal file
@@ -0,0 +1,155 @@
|
|||||||
|
import QtQuick
|
||||||
|
import Quickshell.Services.Mpris
|
||||||
|
import "../../Common"
|
||||||
|
import "../../Services"
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property bool hasActiveMedia: false
|
||||||
|
property var activePlayer: null
|
||||||
|
property bool weatherAvailable: false
|
||||||
|
property string weatherCode: ""
|
||||||
|
property int weatherTemp: 0
|
||||||
|
property int weatherTempF: 0
|
||||||
|
property bool useFahrenheit: false
|
||||||
|
property date currentDate: new Date()
|
||||||
|
|
||||||
|
signal clockClicked()
|
||||||
|
|
||||||
|
width: {
|
||||||
|
let baseWidth = 200
|
||||||
|
if (root.hasActiveMedia) {
|
||||||
|
let mediaWidth = 24 + Theme.spacingXS + mediaTitleText.implicitWidth + Theme.spacingM + 180
|
||||||
|
return Math.min(Math.max(mediaWidth, 300), parent.width - Theme.spacingL * 2)
|
||||||
|
} else if (root.weatherAvailable) {
|
||||||
|
return Math.min(280, parent.width - Theme.spacingL * 2)
|
||||||
|
} else {
|
||||||
|
return Math.min(baseWidth, parent.width - Theme.spacingL * 2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
height: 32
|
||||||
|
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)
|
||||||
|
|
||||||
|
Behavior on color {
|
||||||
|
ColorAnimation {
|
||||||
|
duration: Theme.shortDuration
|
||||||
|
easing.type: Theme.standardEasing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Row {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
spacing: Theme.spacingM
|
||||||
|
|
||||||
|
// Media info or Weather info
|
||||||
|
Row {
|
||||||
|
spacing: Theme.spacingXS
|
||||||
|
visible: root.hasActiveMedia || root.weatherAvailable
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
|
||||||
|
// Audio visualization placeholder - will be replaced by parent
|
||||||
|
Item {
|
||||||
|
id: audioVisualizationPlaceholder
|
||||||
|
width: 20
|
||||||
|
height: Theme.iconSize
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
visible: root.hasActiveMedia
|
||||||
|
}
|
||||||
|
|
||||||
|
// Song title when media is playing
|
||||||
|
Text {
|
||||||
|
id: mediaTitleText
|
||||||
|
text: root.activePlayer?.trackTitle || "Unknown Track"
|
||||||
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
|
color: Theme.surfaceText
|
||||||
|
font.weight: Font.Medium
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
visible: root.hasActiveMedia
|
||||||
|
width: Math.min(implicitWidth, root.width - 100)
|
||||||
|
elide: Text.ElideRight
|
||||||
|
}
|
||||||
|
|
||||||
|
// Weather icon when no media but weather available
|
||||||
|
Text {
|
||||||
|
text: WeatherService.getWeatherIcon(root.weatherCode)
|
||||||
|
font.family: Theme.iconFont
|
||||||
|
font.pixelSize: Theme.iconSize - 2
|
||||||
|
color: Theme.surfaceText
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
visible: !root.hasActiveMedia && root.weatherAvailable
|
||||||
|
}
|
||||||
|
|
||||||
|
// Weather temp when no media but weather available
|
||||||
|
Text {
|
||||||
|
text: (root.useFahrenheit ? root.weatherTempF : root.weatherTemp) + "°" + (root.useFahrenheit ? "F" : "C")
|
||||||
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
|
color: Theme.surfaceText
|
||||||
|
font.weight: Font.Medium
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
visible: !root.hasActiveMedia && root.weatherAvailable
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Separator
|
||||||
|
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: root.hasActiveMedia || root.weatherAvailable
|
||||||
|
}
|
||||||
|
|
||||||
|
// Time and date
|
||||||
|
Row {
|
||||||
|
spacing: Theme.spacingS
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Timer {
|
||||||
|
interval: 1000
|
||||||
|
running: true
|
||||||
|
repeat: true
|
||||||
|
onTriggered: {
|
||||||
|
root.currentDate = new Date()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: clockMouseArea
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
|
||||||
|
onClicked: {
|
||||||
|
root.clockClicked()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
104
Widgets/TopBar/ControlCenterButton.qml
Normal file
104
Widgets/TopBar/ControlCenterButton.qml
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
import QtQuick
|
||||||
|
import "../../Common"
|
||||||
|
import "../../Services"
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property string networkStatus: "disconnected"
|
||||||
|
property string wifiSignalStrength: "good"
|
||||||
|
property int volumeLevel: 50
|
||||||
|
property bool bluetoothAvailable: false
|
||||||
|
property bool bluetoothEnabled: false
|
||||||
|
property bool isActive: false
|
||||||
|
|
||||||
|
signal clicked()
|
||||||
|
|
||||||
|
width: Math.max(80, controlIndicators.implicitWidth + Theme.spacingS * 2)
|
||||||
|
height: 32
|
||||||
|
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 (root.networkStatus === "ethernet") return "lan"
|
||||||
|
else if (root.networkStatus === "wifi") {
|
||||||
|
switch (root.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: root.networkStatus !== "disconnected" ? Theme.primary : Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.5)
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
visible: true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Audio Icon
|
||||||
|
Text {
|
||||||
|
text: root.volumeLevel === 0 ? "volume_off" :
|
||||||
|
root.volumeLevel < 33 ? "volume_down" : "volume_up"
|
||||||
|
font.family: Theme.iconFont
|
||||||
|
font.pixelSize: Theme.iconSize - 8
|
||||||
|
font.weight: Theme.iconFontWeight
|
||||||
|
color: controlCenterArea.containsMouse || root.isActive ?
|
||||||
|
Theme.primary : Theme.surfaceText
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bluetooth Icon (when available and enabled)
|
||||||
|
Text {
|
||||||
|
text: "bluetooth"
|
||||||
|
font.family: Theme.iconFont
|
||||||
|
font.pixelSize: Theme.iconSize - 8
|
||||||
|
font.weight: Theme.iconFontWeight
|
||||||
|
color: root.bluetoothEnabled ? Theme.primary : Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.5)
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
visible: root.bluetoothAvailable && root.bluetoothEnabled
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
41
Widgets/TopBar/LauncherButton.qml
Normal file
41
Widgets/TopBar/LauncherButton.qml
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
import QtQuick
|
||||||
|
import "../../Common"
|
||||||
|
import "../../Services"
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
width: 40
|
||||||
|
height: 32
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: launcherArea.containsMouse ? 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)
|
||||||
|
|
||||||
|
property string osLogo: ""
|
||||||
|
|
||||||
|
Text {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
text: root.osLogo || "apps"
|
||||||
|
font.family: root.osLogo ? "NerdFont" : 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: {
|
||||||
|
LauncherService.toggleAppLauncher()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Behavior on color {
|
||||||
|
ColorAnimation {
|
||||||
|
duration: Theme.shortDuration
|
||||||
|
easing.type: Theme.standardEasing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
58
Widgets/TopBar/NotificationCenterButton.qml
Normal file
58
Widgets/TopBar/NotificationCenterButton.qml
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
import QtQuick
|
||||||
|
import "../../Common"
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property bool hasUnread: false
|
||||||
|
property bool isActive: false
|
||||||
|
signal clicked()
|
||||||
|
|
||||||
|
width: 40
|
||||||
|
height: 32
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
100
Widgets/TopBar/SystemTrayWidget.qml
Normal file
100
Widgets/TopBar/SystemTrayWidget.qml
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
import QtQuick
|
||||||
|
import Quickshell.Services.SystemTray
|
||||||
|
import "../../Common"
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
signal menuRequested(var menu, var item, real x, real y)
|
||||||
|
|
||||||
|
width: Math.max(40, systemTrayRow.implicitWidth + Theme.spacingS * 2)
|
||||||
|
height: 32
|
||||||
|
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 {
|
||||||
|
width: 24
|
||||||
|
height: 24
|
||||||
|
radius: Theme.cornerRadiusSmall
|
||||||
|
color: trayItemArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
|
||||||
|
|
||||||
|
property var trayItem: modelData
|
||||||
|
|
||||||
|
Image {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
width: 18
|
||||||
|
height: 18
|
||||||
|
source: {
|
||||||
|
let icon = trayItem?.icon || "";
|
||||||
|
if (!icon) return "";
|
||||||
|
|
||||||
|
if (icon.includes("?path=")) {
|
||||||
|
const [name, path] = icon.split("?path=");
|
||||||
|
const fileName = name.substring(name.lastIndexOf("/") + 1);
|
||||||
|
return `file://${path}/${fileName}`;
|
||||||
|
}
|
||||||
|
return icon;
|
||||||
|
}
|
||||||
|
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.hasMenu) {
|
||||||
|
console.log("Right-click detected, showing menu for:", trayItem.title || "Unknown")
|
||||||
|
customTrayMenu.showMenu(mouse.x, mouse.y)
|
||||||
|
} else {
|
||||||
|
console.log("No menu available for:", trayItem.title || "Unknown")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
292
Widgets/TopBar/TopBar.qml
Normal file
292
Widgets/TopBar/TopBar.qml
Normal file
@@ -0,0 +1,292 @@
|
|||||||
|
import QtQuick
|
||||||
|
import QtQuick.Controls
|
||||||
|
import Qt5Compat.GraphicalEffects
|
||||||
|
import Quickshell
|
||||||
|
import Quickshell.Widgets
|
||||||
|
import Quickshell.Wayland
|
||||||
|
import Quickshell.Io
|
||||||
|
import Quickshell.Services.SystemTray
|
||||||
|
import Quickshell.Services.Notifications
|
||||||
|
import Quickshell.Services.Mpris
|
||||||
|
import "../../Common"
|
||||||
|
import "../../Services"
|
||||||
|
import ".."
|
||||||
|
|
||||||
|
PanelWindow {
|
||||||
|
id: topBar
|
||||||
|
|
||||||
|
property var modelData
|
||||||
|
screen: modelData
|
||||||
|
property string screenName: modelData.name
|
||||||
|
|
||||||
|
// Properties exposed to shell
|
||||||
|
property bool hasActiveMedia: false
|
||||||
|
property var activePlayer: null
|
||||||
|
property bool weatherAvailable: false
|
||||||
|
property string weatherCode: ""
|
||||||
|
property int weatherTemp: 0
|
||||||
|
property int weatherTempF: 0
|
||||||
|
property bool useFahrenheit: false
|
||||||
|
property string osLogo: ""
|
||||||
|
property string networkStatus: "disconnected"
|
||||||
|
property string wifiSignalStrength: "good"
|
||||||
|
property int volumeLevel: 50
|
||||||
|
property bool bluetoothAvailable: false
|
||||||
|
property bool bluetoothEnabled: false
|
||||||
|
|
||||||
|
// Notification properties
|
||||||
|
property bool notificationHistoryVisible: false
|
||||||
|
property int notificationCount: 0
|
||||||
|
|
||||||
|
// Control center properties
|
||||||
|
property bool controlCenterVisible: false
|
||||||
|
|
||||||
|
// Calendar properties
|
||||||
|
property bool calendarVisible: false
|
||||||
|
|
||||||
|
// Clipboard properties
|
||||||
|
signal clipboardRequested()
|
||||||
|
|
||||||
|
// Tray menu properties
|
||||||
|
property bool showTrayMenu: false
|
||||||
|
property var currentTrayMenu: null
|
||||||
|
property var currentTrayItem: null
|
||||||
|
property real trayMenuX: 0
|
||||||
|
property real trayMenuY: 0
|
||||||
|
|
||||||
|
// Proxy objects for external connections
|
||||||
|
|
||||||
|
QtObject {
|
||||||
|
id: notificationHistory
|
||||||
|
property int count: 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// Battery widget and other widgets are imported from their original locations
|
||||||
|
// These will be handled by the parent shell
|
||||||
|
property alias batteryWidget: batteryWidgetProxy
|
||||||
|
property alias cpuMonitorWidget: cpuMonitorWidgetProxy
|
||||||
|
property alias ramMonitorWidget: ramMonitorWidgetProxy
|
||||||
|
property alias powerButton: powerButtonProxy
|
||||||
|
|
||||||
|
QtObject { id: batteryWidgetProxy }
|
||||||
|
QtObject { id: cpuMonitorWidgetProxy }
|
||||||
|
QtObject { id: ramMonitorWidgetProxy }
|
||||||
|
QtObject { id: powerButtonProxy }
|
||||||
|
|
||||||
|
anchors {
|
||||||
|
top: true
|
||||||
|
left: true
|
||||||
|
right: true
|
||||||
|
}
|
||||||
|
|
||||||
|
implicitHeight: Theme.barHeight - 4
|
||||||
|
color: "transparent"
|
||||||
|
|
||||||
|
// 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, 0.75)
|
||||||
|
|
||||||
|
layer.enabled: true
|
||||||
|
layer.effect: DropShadow {
|
||||||
|
horizontalOffset: 0
|
||||||
|
verticalOffset: 4
|
||||||
|
radius: 16
|
||||||
|
samples: 33
|
||||||
|
color: Qt.rgba(0, 0, 0, 0.15)
|
||||||
|
transparentBorder: 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: true
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.leftMargin: Theme.spacingM
|
||||||
|
anchors.rightMargin: Theme.spacingM
|
||||||
|
anchors.topMargin: Theme.spacingXS
|
||||||
|
anchors.bottomMargin: Theme.spacingXS
|
||||||
|
|
||||||
|
Row {
|
||||||
|
id: leftSection
|
||||||
|
height: parent.height
|
||||||
|
spacing: Theme.spacingXS
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
|
||||||
|
LauncherButton {
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
osLogo: topBar.osLogo
|
||||||
|
}
|
||||||
|
|
||||||
|
WorkspaceSwitcher {
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
screenName: topBar.screenName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ClockWidget {
|
||||||
|
id: clockWidget
|
||||||
|
anchors.centerIn: parent
|
||||||
|
hasActiveMedia: topBar.hasActiveMedia
|
||||||
|
activePlayer: topBar.activePlayer
|
||||||
|
weatherAvailable: topBar.weatherAvailable
|
||||||
|
weatherCode: topBar.weatherCode
|
||||||
|
weatherTemp: topBar.weatherTemp
|
||||||
|
weatherTempF: topBar.weatherTempF
|
||||||
|
useFahrenheit: topBar.useFahrenheit
|
||||||
|
|
||||||
|
onClockClicked: {
|
||||||
|
topBar.calendarVisible = !topBar.calendarVisible
|
||||||
|
}
|
||||||
|
|
||||||
|
// Insert audio visualization into the clock widget
|
||||||
|
AudioVisualization {
|
||||||
|
parent: clockWidget.children[0].children[0] // Row -> Row (media info)
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
hasActiveMedia: topBar.hasActiveMedia
|
||||||
|
activePlayer: topBar.activePlayer
|
||||||
|
visible: topBar.hasActiveMedia
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Row {
|
||||||
|
id: rightSection
|
||||||
|
height: parent.height
|
||||||
|
spacing: Theme.spacingXS
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
|
||||||
|
SystemTrayWidget {
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
onMenuRequested: (menu, item, x, y) => {
|
||||||
|
topBar.currentTrayMenu = menu
|
||||||
|
topBar.currentTrayItem = item
|
||||||
|
topBar.trayMenuX = rightSection.x + rightSection.width - 400 - Theme.spacingL
|
||||||
|
topBar.trayMenuY = Theme.barHeight + Theme.spacingS
|
||||||
|
console.log("Showing menu at:", topBar.trayMenuX, topBar.trayMenuY)
|
||||||
|
menu.menuVisible = true
|
||||||
|
topBar.showTrayMenu = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
width: 40
|
||||||
|
height: 32
|
||||||
|
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
|
||||||
|
|
||||||
|
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: {
|
||||||
|
topBar.clipboardRequested()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Behavior on color {
|
||||||
|
ColorAnimation {
|
||||||
|
duration: Theme.shortDuration
|
||||||
|
easing.type: Theme.standardEasing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// System Monitor Widgets
|
||||||
|
CpuMonitorWidget {
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
RamMonitorWidget {
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
NotificationCenterButton {
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
hasUnread: topBar.notificationCount > 0
|
||||||
|
isActive: topBar.notificationHistoryVisible
|
||||||
|
onClicked: {
|
||||||
|
topBar.notificationHistoryVisible = !topBar.notificationHistoryVisible
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Battery Widget
|
||||||
|
BatteryWidget {
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
ControlCenterButton {
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
networkStatus: topBar.networkStatus
|
||||||
|
wifiSignalStrength: topBar.wifiSignalStrength
|
||||||
|
volumeLevel: topBar.volumeLevel
|
||||||
|
bluetoothAvailable: topBar.bluetoothAvailable
|
||||||
|
bluetoothEnabled: topBar.bluetoothEnabled
|
||||||
|
isActive: topBar.controlCenterVisible
|
||||||
|
|
||||||
|
onClicked: {
|
||||||
|
topBar.controlCenterVisible = !topBar.controlCenterVisible
|
||||||
|
if (topBar.controlCenterVisible) {
|
||||||
|
WifiService.scanWifi()
|
||||||
|
BluetoothService.scanDevices()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Power Button
|
||||||
|
PowerButton {
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
123
Widgets/TopBar/WorkspaceSwitcher.qml
Normal file
123
Widgets/TopBar/WorkspaceSwitcher.qml
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
import QtQuick
|
||||||
|
import Quickshell
|
||||||
|
import "../../Common"
|
||||||
|
import "../../Services"
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property string screenName: ""
|
||||||
|
|
||||||
|
width: Math.max(120, workspaceRow.implicitWidth + Theme.spacingL * 2)
|
||||||
|
height: 32
|
||||||
|
radius: Theme.cornerRadiusLarge
|
||||||
|
color: Qt.rgba(Theme.surfaceContainerHigh.r, Theme.surfaceContainerHigh.g, Theme.surfaceContainerHigh.b, 0.8)
|
||||||
|
visible: NiriWorkspaceService.niriAvailable
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
Connections {
|
||||||
|
target: NiriWorkspaceService
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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.spacingS : Theme.spacingL
|
||||||
|
height: Theme.spacingS
|
||||||
|
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)
|
||||||
|
|
||||||
|
Behavior on width {
|
||||||
|
NumberAnimation {
|
||||||
|
duration: Theme.mediumDuration
|
||||||
|
easing.type: Theme.emphasizedEasing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Behavior on color {
|
||||||
|
ColorAnimation {
|
||||||
|
duration: Theme.mediumDuration
|
||||||
|
easing.type: Theme.emphasizedEasing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: mouseArea
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
|
||||||
|
onClicked: {
|
||||||
|
Quickshell.execDetached(["niri", "msg", "action", "focus-workspace", sequentialNumber.toString()])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
8
Widgets/TopBar/qmldir
Normal file
8
Widgets/TopBar/qmldir
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
TopBar 1.0 TopBar.qml
|
||||||
|
LauncherButton 1.0 LauncherButton.qml
|
||||||
|
WorkspaceSwitcher 1.0 WorkspaceSwitcher.qml
|
||||||
|
ClockWidget 1.0 ClockWidget.qml
|
||||||
|
SystemTrayWidget 1.0 SystemTrayWidget.qml
|
||||||
|
NotificationCenterButton 1.0 NotificationCenterButton.qml
|
||||||
|
ControlCenterButton 1.0 ControlCenterButton.qml
|
||||||
|
AudioVisualization 1.0 AudioVisualization.qml
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
TopBar 1.0 TopBar.qml
|
TopBar 1.0 TopBar/TopBar.qml
|
||||||
TrayMenuPopup 1.0 TrayMenuPopup.qml
|
TrayMenuPopup 1.0 TrayMenuPopup.qml
|
||||||
NotificationPopup 1.0 NotificationPopup.qml
|
NotificationPopup 1.0 NotificationPopup.qml
|
||||||
NotificationHistoryPopup 1.0 NotificationHistoryPopup.qml
|
NotificationHistoryPopup 1.0 NotificationHistoryPopup.qml
|
||||||
|
|||||||
41
shell.qml
41
shell.qml
@@ -283,6 +283,47 @@ ShellRoot {
|
|||||||
model: Quickshell.screens
|
model: Quickshell.screens
|
||||||
delegate: TopBar {
|
delegate: TopBar {
|
||||||
modelData: item
|
modelData: item
|
||||||
|
|
||||||
|
// Connect shell properties
|
||||||
|
hasActiveMedia: root.hasActiveMedia
|
||||||
|
activePlayer: root.activePlayer
|
||||||
|
weatherAvailable: root.weather.available
|
||||||
|
weatherCode: root.weather.wCode
|
||||||
|
weatherTemp: root.weather.temp
|
||||||
|
weatherTempF: root.weather.tempF
|
||||||
|
useFahrenheit: root.useFahrenheit
|
||||||
|
osLogo: root.osLogo
|
||||||
|
networkStatus: root.networkStatus
|
||||||
|
wifiSignalStrength: root.wifiSignalStrength
|
||||||
|
volumeLevel: root.volumeLevel
|
||||||
|
bluetoothAvailable: root.bluetoothAvailable
|
||||||
|
bluetoothEnabled: root.bluetoothEnabled
|
||||||
|
notificationHistoryVisible: root.notificationHistoryVisible
|
||||||
|
notificationCount: notificationHistory.count
|
||||||
|
controlCenterVisible: root.controlCenterVisible
|
||||||
|
calendarVisible: root.calendarVisible
|
||||||
|
|
||||||
|
// Connect tray menu properties
|
||||||
|
showTrayMenu: root.showTrayMenu
|
||||||
|
currentTrayMenu: root.currentTrayMenu
|
||||||
|
currentTrayItem: root.currentTrayItem
|
||||||
|
trayMenuX: root.trayMenuX
|
||||||
|
trayMenuY: root.trayMenuY
|
||||||
|
|
||||||
|
// Connect clipboard
|
||||||
|
onClipboardRequested: {
|
||||||
|
clipboardHistoryPopup.toggle()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Property change handlers
|
||||||
|
onCalendarVisibleChanged: root.calendarVisible = calendarVisible
|
||||||
|
onControlCenterVisibleChanged: root.controlCenterVisible = controlCenterVisible
|
||||||
|
onNotificationHistoryVisibleChanged: root.notificationHistoryVisible = notificationHistoryVisible
|
||||||
|
onShowTrayMenuChanged: root.showTrayMenu = showTrayMenu
|
||||||
|
onCurrentTrayMenuChanged: root.currentTrayMenu = currentTrayMenu
|
||||||
|
onCurrentTrayItemChanged: root.currentTrayItem = currentTrayItem
|
||||||
|
onTrayMenuXChanged: root.trayMenuX = trayMenuX
|
||||||
|
onTrayMenuYChanged: root.trayMenuY = trayMenuY
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user