Compare commits
51 Commits
68d4240fcb
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| b00e013752 | |||
| 3c7f2c2b75 | |||
| 4a813121c7 | |||
| f90e62d25b | |||
| e33041b66c | |||
| 88d867f1de | |||
| b09039cabb | |||
| dc82f5a0ae | |||
| 802b077362 | |||
| 9baa312d0e | |||
| f246b7e658 | |||
| d7de61fac8 | |||
| 006697b768 | |||
| 6cce500670 | |||
| 0203236575 | |||
| 1c5a25c8fa | |||
| 9f72a8abad | |||
| 04e2ca54be | |||
| f2ed2e7a4e | |||
| 3250231384 | |||
| 79d83cbe37 | |||
| 021ddae2d0 | |||
| b3c46e2df2 | |||
| e371c9cc7b | |||
| 7858c5b90d | |||
| 300cb0b2f8 | |||
| a90afb3154 | |||
| d97a6e0a48 | |||
| 02e99cca74 | |||
| cf4db3b8df | |||
| d0e1e2170c | |||
| 01fdddf0e8 | |||
| ce5bbb1dff | |||
| f5623585fb | |||
| 942b179572 | |||
| 8267743fb5 | |||
| 6f3e62bcf9 | |||
| d54487022f | |||
| 11a8f03215 | |||
| 7cceb3a91a | |||
| 4ef5f39f6d | |||
| 78f92f1dbf | |||
| 185e7618a4 | |||
| bc2f308673 | |||
| 89ee2ec974 | |||
| abe1a1e12f | |||
| 8e59d2dcec | |||
| 8ba73b061f | |||
| 9e0887c474 | |||
| 76c437da88 | |||
| afbe360b08 |
8
.prettierrc.json
Normal file
8
.prettierrc.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"tabWidth": 2,
|
||||
"printWidth": 90,
|
||||
"useTabs": false,
|
||||
"proseWrap": "always",
|
||||
"endOfLine": "lf",
|
||||
"embeddedLanguageFormatting": "auto"
|
||||
}
|
||||
85
Bar.qml
85
Bar.qml
@@ -1,16 +1,25 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
import QtQuick
|
||||
import QtQuick.Effects
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import Quickshell.Hyprland
|
||||
import "Bar"
|
||||
import Quickshell.Wayland
|
||||
import "./Bar"
|
||||
import "./Services/"
|
||||
import "Color.js" as Colors
|
||||
|
||||
Scope {
|
||||
Variants {
|
||||
model: Quickshell.screens
|
||||
id: panelDelegate
|
||||
|
||||
delegate: panelWindow
|
||||
model: Quickshell.screens
|
||||
}
|
||||
|
||||
Component {
|
||||
id: panelWindow
|
||||
|
||||
delegate: Component {
|
||||
PanelWindow {
|
||||
id: win
|
||||
|
||||
@@ -19,13 +28,33 @@ Scope {
|
||||
required property var modelData
|
||||
property HyprlandMonitor monitor: Hyprland.monitorFor(modelData)
|
||||
|
||||
WlrLayershell.keyboardFocus: monitor.focused == true ? IpcManager.focusState :
|
||||
WlrKeyboardFocus.None
|
||||
color: "transparent"
|
||||
exclusionMode: ExclusionMode.Normal
|
||||
exclusiveZone: barExclusionZone
|
||||
implicitHeight: barMaxHeight
|
||||
mask: maskRegion
|
||||
screen: modelData
|
||||
|
||||
mask: Region {
|
||||
Rectangle {
|
||||
id: blurOverlay
|
||||
|
||||
color: Colors.base
|
||||
implicitHeight: win.modelData.height
|
||||
implicitWidth: win.modelData.width
|
||||
opacity: IpcManager.logoutMenuOpen == true ? 0.8 : 0
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: 200
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Region {
|
||||
id: maskRegion
|
||||
|
||||
Region {
|
||||
item: left
|
||||
}
|
||||
@@ -45,23 +74,42 @@ Scope {
|
||||
top: true
|
||||
}
|
||||
|
||||
MultiEffect {
|
||||
anchors.fill: rowContainer
|
||||
shadowBlur: 1.5
|
||||
shadowColor: Colors.mantle
|
||||
shadowEnabled: true
|
||||
shadowHorizontalOffset: 5
|
||||
shadowOpacity: 1
|
||||
shadowVerticalOffset: 3
|
||||
source: rowContainer
|
||||
}
|
||||
|
||||
Item {
|
||||
id: rowContainer
|
||||
|
||||
anchors {
|
||||
fill: parent
|
||||
topMargin: 3
|
||||
}
|
||||
|
||||
anchors {
|
||||
left: parent.left
|
||||
leftMargin: 3
|
||||
right: parent.right
|
||||
rightMargin: 3
|
||||
top: parent.top
|
||||
}
|
||||
|
||||
Row {
|
||||
id: left
|
||||
|
||||
height: childrenRect.height
|
||||
|
||||
anchors {
|
||||
left: parent.left
|
||||
leftMargin: 7
|
||||
top: parent.top
|
||||
}
|
||||
|
||||
Left {
|
||||
WorkspaceSelector {
|
||||
monitor: win.monitor
|
||||
}
|
||||
}
|
||||
@@ -69,13 +117,12 @@ Scope {
|
||||
Row {
|
||||
id: center
|
||||
|
||||
height: childrenRect.height
|
||||
|
||||
anchors {
|
||||
horizontalCenter: parent.horizontalCenter
|
||||
top: parent.top
|
||||
}
|
||||
|
||||
Center {
|
||||
Time {
|
||||
monitor: win.monitor
|
||||
}
|
||||
}
|
||||
@@ -83,14 +130,23 @@ Scope {
|
||||
Row {
|
||||
id: right
|
||||
|
||||
height: childrenRect.height
|
||||
Layout.alignment: Qt.AlignTop
|
||||
spacing: 5
|
||||
|
||||
anchors {
|
||||
right: parent.right
|
||||
rightMargin: 7
|
||||
top: parent.top
|
||||
}
|
||||
|
||||
Right {
|
||||
Network {
|
||||
monitor: win.monitor
|
||||
}
|
||||
|
||||
Bluetooth {
|
||||
monitor: win.monitor
|
||||
}
|
||||
|
||||
Battery {
|
||||
monitor: win.monitor
|
||||
}
|
||||
}
|
||||
@@ -98,4 +154,3 @@ Scope {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
144
Bar/Battery.qml
Normal file
144
Bar/Battery.qml
Normal file
@@ -0,0 +1,144 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import Quickshell.Hyprland
|
||||
|
||||
import "../Color.js" as Colors
|
||||
import "../Components/"
|
||||
import "../Services/"
|
||||
|
||||
Container {
|
||||
id: root
|
||||
|
||||
property double batteryDisplayHeight: 100
|
||||
required property HyprlandMonitor monitor
|
||||
property bool opened: false
|
||||
property double radius: 9
|
||||
|
||||
animOffset: 150
|
||||
boxColor: Colors.green
|
||||
boxHeight: 25
|
||||
boxHeightOpened: root.batteryDisplayHeight
|
||||
boxRadius: radius
|
||||
boxWidth: 60
|
||||
boxWidthOpened: 410
|
||||
defaultItem: icon
|
||||
exclusiveMonitor: root.monitor
|
||||
exclusiveToScreen: true
|
||||
forceHidden: !BatteryManager.hasLaptopBattery
|
||||
openedItem: batteryDisplay
|
||||
|
||||
Component {
|
||||
id: icon
|
||||
|
||||
Item {
|
||||
BatteryGradient {}
|
||||
|
||||
CenteredText {
|
||||
property string component: "icon"
|
||||
|
||||
propo: true
|
||||
text: BatteryManager.getBatteryText()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: batteryDisplay
|
||||
|
||||
Item {
|
||||
id: batteryDisplayRoot
|
||||
|
||||
property double innerMargin: 5
|
||||
property double outerMargin: 6
|
||||
|
||||
BatteryGradient {}
|
||||
|
||||
Rectangle {
|
||||
id: batteryBackground
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.margins: batteryDisplayRoot.outerMargin
|
||||
color: Colors.surface1
|
||||
radius: 8
|
||||
|
||||
RowLayout {
|
||||
anchors.fill: parent
|
||||
|
||||
Rectangle {
|
||||
id: percentBox
|
||||
|
||||
property double percentBoxRadius: 5
|
||||
|
||||
Layout.fillHeight: true
|
||||
Layout.margins: batteryDisplayRoot.innerMargin
|
||||
color: Colors.text
|
||||
implicitWidth: root.batteryDisplayHeight - (batteryDisplayRoot.innerMargin * 2) - (
|
||||
batteryDisplayRoot.outerMargin * 2)
|
||||
radius: 5
|
||||
|
||||
Rectangle {
|
||||
color: Colors.green
|
||||
implicitHeight: (root.batteryDisplayHeight - (batteryDisplayRoot.innerMargin * 2) - (
|
||||
batteryDisplayRoot.outerMargin * 2)) * BatteryManager.mainBattery.percentage
|
||||
radius: percentBox.percentBoxRadius
|
||||
|
||||
anchors {
|
||||
bottom: parent.bottom
|
||||
left: parent.left
|
||||
right: parent.right
|
||||
}
|
||||
}
|
||||
|
||||
CenteredText {
|
||||
fontSize: 15
|
||||
text: BatteryManager.getBatteryPercentage(BatteryManager.mainBattery) + "%"
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
id: batColumn
|
||||
|
||||
property var batInfo: BatteryManager.getMainBatteryInfo()
|
||||
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
Layout.fillWidth: true
|
||||
|
||||
StyledText {
|
||||
color: Colors.text
|
||||
fontSize: 14
|
||||
text: "Battery - " + batColumn.batInfo["name"]
|
||||
}
|
||||
|
||||
StyledText {
|
||||
color: Colors.subtext1
|
||||
fontSize: 12
|
||||
fontWeight: 5
|
||||
text: batColumn.batInfo["energy"] + "/" + batColumn.batInfo["capacity"] + "Wh - "
|
||||
+ BatteryManager.getChargingRateFormat()
|
||||
}
|
||||
|
||||
StyledText {
|
||||
property var timeInfo: batColumn.batInfo["time"]
|
||||
|
||||
color: Colors.subtext1
|
||||
fontSize: 12
|
||||
fontWeight: 5
|
||||
text: BatteryManager.getTimeFormat()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
component BatteryGradient: MidpointGradient {
|
||||
anchors.fill: parent
|
||||
angle: -45
|
||||
blur: 0.6
|
||||
color: BatteryManager.getBatteryColor()
|
||||
midpoint: 0.55
|
||||
radius: radius
|
||||
}
|
||||
}
|
||||
165
Bar/Bluetooth.qml
Normal file
165
Bar/Bluetooth.qml
Normal file
@@ -0,0 +1,165 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import Quickshell.Hyprland
|
||||
|
||||
import "../Color.js" as Colors
|
||||
import "../Components/"
|
||||
import "../Services/"
|
||||
|
||||
Container {
|
||||
id: root
|
||||
|
||||
property double devicePlaqueHeight: 60
|
||||
property double devicePlaqueSpacing: 4
|
||||
required property HyprlandMonitor monitor
|
||||
|
||||
animOffset: 200
|
||||
boxColor: Colors.peach
|
||||
boxHeight: 25
|
||||
boxHeightOpened: (root.devicePlaqueHeight * BluetoothManager.getConnectedDevicesList().length) + (
|
||||
root.devicePlaqueSpacing * (BluetoothManager.getConnectedDevicesList().length + 1)) + 2
|
||||
boxWidth: 33
|
||||
boxWidthOpened: 350
|
||||
defaultItem: icon
|
||||
exclusiveMonitor: root.monitor
|
||||
exclusiveToScreen: true
|
||||
forceHidden: !BluetoothManager.anyConnected
|
||||
openedItem: deviceList
|
||||
|
||||
Component {
|
||||
id: icon
|
||||
|
||||
StyledText {
|
||||
property string component: "icon"
|
||||
|
||||
horizontalAlignment: Qt.AlignCenter
|
||||
propo: true
|
||||
text: ""
|
||||
verticalAlignment: Qt.AlignVCenter
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: deviceList
|
||||
|
||||
Column {
|
||||
id: deviceListRoot
|
||||
|
||||
property string component: "deviceList"
|
||||
property double plaqueHeight: root.devicePlaqueHeight
|
||||
property double plaqueSpacing: root.devicePlaqueSpacing
|
||||
|
||||
spacing: plaqueSpacing
|
||||
|
||||
Item {
|
||||
implicitHeight: 1
|
||||
implicitWidth: 10
|
||||
}
|
||||
|
||||
Repeater {
|
||||
delegate: devicePlaque
|
||||
model: BluetoothManager.getConnectedDevicesList()
|
||||
}
|
||||
|
||||
Component {
|
||||
id: devicePlaque
|
||||
|
||||
Rectangle {
|
||||
id: devicePlaqueRoot
|
||||
|
||||
readonly property variant deviceText: BluetoothManager.getDeviceText(modelData)
|
||||
readonly property double margin: 5
|
||||
required property var modelData
|
||||
|
||||
color: Colors.surface1
|
||||
implicitHeight: deviceListRoot.plaqueHeight
|
||||
radius: 10
|
||||
|
||||
anchors {
|
||||
left: parent.left
|
||||
leftMargin: devicePlaqueRoot.margin
|
||||
right: parent.right
|
||||
rightMargin: devicePlaqueRoot.margin
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
id: deviceInfoRow
|
||||
|
||||
property double margin: 7
|
||||
|
||||
spacing: margin * 2
|
||||
|
||||
anchors {
|
||||
fill: parent
|
||||
leftMargin: margin
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: iconContainer
|
||||
|
||||
property double boxRadius: 4
|
||||
|
||||
color: Colors.text
|
||||
implicitHeight: deviceListRoot.plaqueHeight - (deviceInfoRow.margin * 2)
|
||||
implicitWidth: deviceListRoot.plaqueHeight - (deviceInfoRow.margin * 2)
|
||||
radius: boxRadius
|
||||
|
||||
Rectangle {
|
||||
id: batteryIndicator
|
||||
|
||||
color: Colors.green
|
||||
implicitHeight: (deviceListRoot.plaqueHeight - (deviceInfoRow.margin * 2))
|
||||
* devicePlaqueRoot.deviceText["batteryRaw"]
|
||||
radius: iconContainer.boxRadius
|
||||
|
||||
anchors {
|
||||
bottom: parent.bottom
|
||||
left: parent.left
|
||||
right: parent.right
|
||||
}
|
||||
}
|
||||
|
||||
HoverHandler {
|
||||
id: batteryHoverHandler
|
||||
|
||||
onHoveredChanged: {
|
||||
if (batteryHoverHandler.hovered == true) {
|
||||
batteryText.text = devicePlaqueRoot.deviceText["battery"];
|
||||
} else {
|
||||
batteryText.text = devicePlaqueRoot.deviceText["icon"];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
CenteredText {
|
||||
id: batteryText
|
||||
|
||||
fontSize: 17
|
||||
text: devicePlaqueRoot.deviceText["icon"]
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
Layout.fillWidth: true
|
||||
|
||||
StyledText {
|
||||
color: Colors.text
|
||||
fontSize: 14
|
||||
text: devicePlaqueRoot.deviceText["name"]
|
||||
}
|
||||
|
||||
StyledText {
|
||||
color: Colors.subtext1
|
||||
fontSize: 10
|
||||
text: devicePlaqueRoot.deviceText["mac"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
263
Bar/Center.qml
263
Bar/Center.qml
@@ -1,263 +0,0 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import Quickshell.Hyprland
|
||||
import Quickshell.Services.Mpris
|
||||
import Quickshell.Services.Notifications
|
||||
import "../Color.js" as Colors
|
||||
import "../Components/"
|
||||
import "../Services/"
|
||||
import "./Center"
|
||||
|
||||
Container {
|
||||
id: root
|
||||
|
||||
property Notification latestNotification: NotificationManager.getLatestNotification() || null
|
||||
required property HyprlandMonitor monitor
|
||||
property NotificationServer notifServer: NotificationManager.notif
|
||||
property string previousState
|
||||
|
||||
boxColor: Colors.mauve
|
||||
defaultItem: time
|
||||
exclusiveMonitor: root.monitor
|
||||
exclusiveToScreen: true
|
||||
|
||||
states: [
|
||||
State {
|
||||
name: ""
|
||||
|
||||
PropertyChanges {
|
||||
root.boxHeight: 28
|
||||
root.boxRadius: 9
|
||||
root.boxWidth: 100
|
||||
root.visibleTopMargin: 0
|
||||
}
|
||||
|
||||
StateChangeScript {
|
||||
script: {
|
||||
if (root.previousState != "hovered") {
|
||||
root.stack.replace(time);
|
||||
}
|
||||
root.previousState = "";
|
||||
}
|
||||
}
|
||||
},
|
||||
State {
|
||||
name: "hovered"
|
||||
|
||||
PropertyChanges {
|
||||
root.boxHeight: 35
|
||||
root.boxRadius: 15
|
||||
root.boxWidth: 110
|
||||
root.visibleTopMargin: 0
|
||||
}
|
||||
|
||||
StateChangeScript {
|
||||
script: {
|
||||
root.previousState = "hovered";
|
||||
}
|
||||
}
|
||||
},
|
||||
State {
|
||||
name: "mprisToast"
|
||||
|
||||
PropertyChanges {
|
||||
root.boxHeight: 35
|
||||
root.boxRadius: 15
|
||||
root.boxWidth: 350
|
||||
root.visibleTopMargin: 0
|
||||
}
|
||||
|
||||
StateChangeScript {
|
||||
script: {
|
||||
root.previousState = "mpris";
|
||||
root.stack.replace(mpris);
|
||||
}
|
||||
}
|
||||
},
|
||||
State {
|
||||
name: "expanded"
|
||||
|
||||
PropertyChanges {
|
||||
root.boxHeight: 140
|
||||
root.boxRadius: 30
|
||||
root.boxWidth: 300
|
||||
root.visibleTopMargin: 0
|
||||
}
|
||||
|
||||
StateChangeScript {
|
||||
script: {
|
||||
if (root.state != "notifications") {
|
||||
root.stack.replace(fullTime);
|
||||
root.previousState = "expanded";
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
State {
|
||||
extend: "expanded"
|
||||
name: "notifications"
|
||||
|
||||
PropertyChanges {
|
||||
root.boxHeight: 320
|
||||
}
|
||||
},
|
||||
State {
|
||||
name: "notified"
|
||||
|
||||
PropertyChanges {
|
||||
root.boxHeight: 90
|
||||
root.boxRadius: 30
|
||||
root.boxWidth: 330
|
||||
root.visibleTopMargin: 20
|
||||
}
|
||||
|
||||
StateChangeScript {
|
||||
script: {
|
||||
root.stack.replace(notification);
|
||||
root.previousState = "notified";
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
hover.onHoveredChanged: {
|
||||
if (hover.hovered == true) {
|
||||
if (root.state == "notified") {
|
||||
root.state = "notified";
|
||||
notificationViewTimer.stop();
|
||||
} else if (MprisManager.getPlaying() == true) {
|
||||
root.state = "mprisToast";
|
||||
} else {
|
||||
root.state = "hovered";
|
||||
}
|
||||
} else {
|
||||
root.state = "";
|
||||
}
|
||||
}
|
||||
onLatestNotificationChanged: {
|
||||
if (latestNotification != null && Hyprland.focusedMonitor == monitor) {
|
||||
if (root.latestNotification.lastGeneration == false) {
|
||||
root.state = "notified";
|
||||
notificationViewTimer.restart();
|
||||
}
|
||||
}
|
||||
}
|
||||
tap.onTapped: {
|
||||
root.state == "hovered" ? root.state = "expanded" : undefined;
|
||||
}
|
||||
|
||||
Connections {
|
||||
function onTrackChanged() {
|
||||
if (Hyprland.focusedMonitor == root.monitor) {
|
||||
musicToastTimer.restart();
|
||||
if (root.state == "") {
|
||||
root.state = "mprisToast";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
target: MprisManager.defaultPlayer
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: notificationViewTimer
|
||||
|
||||
interval: 3000
|
||||
repeat: false
|
||||
running: false
|
||||
|
||||
onTriggered: {
|
||||
root.state = "";
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: musicToastTimer
|
||||
|
||||
interval: 3000
|
||||
repeat: false
|
||||
running: false
|
||||
|
||||
onTriggered: {
|
||||
if (!root.hover.hovered) {
|
||||
root.state = "";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: time
|
||||
|
||||
Item {
|
||||
CenteredText {
|
||||
text: Time.time
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: mpris
|
||||
|
||||
Mpris {
|
||||
TapHandler {
|
||||
onTapped: root.state = "expanded"
|
||||
}
|
||||
|
||||
TapHandler {
|
||||
acceptedButtons: Qt.RightButton
|
||||
|
||||
onTapped: MprisManager.skip()
|
||||
}
|
||||
|
||||
TapHandler {
|
||||
acceptedButtons: Qt.MiddleButton
|
||||
|
||||
onTapped: MprisManager.prev()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: fullTime
|
||||
|
||||
Expanded {
|
||||
NotificationList {
|
||||
id: notifList
|
||||
|
||||
animEnabled: false
|
||||
radius: root.boxRadius
|
||||
rootState: root.state
|
||||
server: root.notifServer
|
||||
state: ""
|
||||
|
||||
TapHandler {
|
||||
onTapped: {
|
||||
notifList.animEnabled = true;
|
||||
root.state = "notifications";
|
||||
Qt.callLater(() => {
|
||||
notifList.animEnabled = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: notification
|
||||
|
||||
NotificationDisplay {
|
||||
body: root.latestNotification.body
|
||||
summary: root.latestNotification.summary
|
||||
|
||||
TapHandler {
|
||||
onTapped: {
|
||||
root.state = "notifications";
|
||||
root.stack.replace(fullTime);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import "../../Color.js" as Colors
|
||||
import "../../Components/"
|
||||
import "../../Services/"
|
||||
|
||||
Item {
|
||||
id: fullTimeRoot
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
radius: 30
|
||||
|
||||
gradient: MidpointGradient {
|
||||
color: Colors.red
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
id: timeColumnContainer
|
||||
|
||||
anchors.fill: parent
|
||||
|
||||
Column {
|
||||
id: timeColumn
|
||||
|
||||
spacing: 5
|
||||
|
||||
anchors {
|
||||
centerIn: parent
|
||||
}
|
||||
|
||||
StyledText {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
color: Colors.surface0
|
||||
text: Time.time
|
||||
|
||||
font {
|
||||
pointSize: 30
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
color: Colors.surface1
|
||||
text: Time.date
|
||||
|
||||
font {
|
||||
pointSize: 9
|
||||
weight: 400
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,62 +0,0 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Quickshell.Services.Mpris
|
||||
import "../../Color.js" as Colors
|
||||
import "../../Components/"
|
||||
import "../../Services/"
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property MprisPlayer player: MprisManager.defaultPlayer
|
||||
|
||||
Rectangle {
|
||||
color: Colors.red
|
||||
radius: 5
|
||||
|
||||
anchors {
|
||||
bottomMargin: 15
|
||||
fill: parent
|
||||
leftMargin: 10
|
||||
rightMargin: parent.width - ((parent.width * MprisManager.pos) - 10)
|
||||
topMargin: 15
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
anchors {
|
||||
bottomMargin: 2
|
||||
fill: parent
|
||||
leftMargin: 7
|
||||
rightMargin: 7
|
||||
topMargin: 2
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillHeight: true
|
||||
Layout.preferredWidth: height
|
||||
|
||||
Image {
|
||||
anchors.fill: parent
|
||||
fillMode: Image.PreserveAspectFit
|
||||
source: MprisManager.defaultPlayer.trackArtUrl
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillHeight: true
|
||||
Layout.fillWidth: true
|
||||
|
||||
StyledText {
|
||||
elide: Qt.ElideRight
|
||||
horizontalAlignment: Qt.AlignHCenter
|
||||
text: MprisManager.defaultPlayer.trackArtist + " - " + MprisManager.defaultPlayer.trackTitle
|
||||
verticalAlignment: Qt.AlignVCenter
|
||||
|
||||
anchors {
|
||||
fill: parent
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Services.Notifications
|
||||
import "../../Color.js" as Colors
|
||||
import "../../Components/"
|
||||
import "../../Services/"
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
required property string body
|
||||
required property string summary
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
radius: 30
|
||||
|
||||
gradient: MidpointGradient {
|
||||
color: Colors.green
|
||||
midpoint: 0.7
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
anchors.centerIn: parent
|
||||
spacing: 5
|
||||
|
||||
StyledText {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
font.pointSize: 11
|
||||
text: root.summary
|
||||
}
|
||||
|
||||
StyledText {
|
||||
elide: Text.ElideRight
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
maximumLineCount: 2
|
||||
text: root.body
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
width: root.width - 20
|
||||
wrapMode: Text.WordWrap
|
||||
|
||||
font {
|
||||
pointSize: 10
|
||||
weight: 500
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,307 +0,0 @@
|
||||
import QtQuick
|
||||
import QtQuick.Effects
|
||||
import Quickshell
|
||||
import Quickshell.Services.Notifications
|
||||
import "../../Color.js" as Colors
|
||||
import "../../Components/"
|
||||
import "../../Services/"
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
required property bool animEnabled
|
||||
required property double radius
|
||||
required property string rootState
|
||||
required property NotificationServer server
|
||||
|
||||
Behavior on anchors.topMargin {
|
||||
enabled: root.animEnabled
|
||||
|
||||
SpringAnimation {
|
||||
damping: 0.35
|
||||
spring: 4
|
||||
}
|
||||
}
|
||||
states: [
|
||||
State {
|
||||
name: ""
|
||||
when: root.rootState != "notifications"
|
||||
|
||||
PropertyChanges {
|
||||
root.anchors.topMargin: root.parent.height - 21
|
||||
}
|
||||
},
|
||||
State {
|
||||
name: "expanded"
|
||||
when: root.rootState == "notifications"
|
||||
|
||||
PropertyChanges {
|
||||
root.anchors.topMargin: 5
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
anchors {
|
||||
bottom: parent.bottom
|
||||
left: parent.left
|
||||
right: parent.right
|
||||
top: parent.top
|
||||
topMargin: parent.height - 21
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: background
|
||||
|
||||
anchors.fill: parent
|
||||
color: Colors.surface0
|
||||
radius: root.radius - 3
|
||||
|
||||
states: [
|
||||
State {
|
||||
name: ""
|
||||
when: root.rootState != "notifications"
|
||||
|
||||
PropertyChanges {}
|
||||
},
|
||||
State {
|
||||
name: "expanded"
|
||||
when: root.rootState == "notifications"
|
||||
|
||||
PropertyChanges {
|
||||
background.anchors.bottomMargin: 5
|
||||
background.anchors.leftMargin: 5
|
||||
background.anchors.rightMargin: 5
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
anchors {
|
||||
bottomMargin: 15
|
||||
leftMargin: 20
|
||||
rightMargin: 20
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
id: notifListContainer
|
||||
|
||||
anchors.fill: parent
|
||||
clip: true
|
||||
opacity: 0
|
||||
|
||||
anchors {
|
||||
bottomMargin: 5
|
||||
leftMargin: 5
|
||||
rightMargin: 5
|
||||
topMargin: 5
|
||||
}
|
||||
|
||||
CenteredText {
|
||||
color: Colors.overlay0
|
||||
text: "No Notifications"
|
||||
visible: root.server.trackedNotifications.values.length <= 0
|
||||
}
|
||||
|
||||
ListView {
|
||||
id: notifScrollingList
|
||||
|
||||
anchors.fill: parent
|
||||
boundsBehavior: Flickable.StopAtBounds
|
||||
height: Math.min(contentHeight, parent.height)
|
||||
implicitHeight: background.height
|
||||
model: root.server.trackedNotifications.values.length
|
||||
spacing: 7
|
||||
|
||||
delegate: Item {
|
||||
id: notifRoot
|
||||
|
||||
property Notification currentNotif: root.server.trackedNotifications.values[(notifRoot.notifLen
|
||||
- 1) - index]
|
||||
required property real index
|
||||
property real notifLen: root.server.trackedNotifications.values.length
|
||||
|
||||
height: 50
|
||||
width: ListView.view.width
|
||||
|
||||
states: [
|
||||
State {
|
||||
name: ""
|
||||
when: notifHover.hovered == false
|
||||
},
|
||||
State {
|
||||
name: "hovered"
|
||||
when: notifHover.hovered == false
|
||||
|
||||
PropertyChanges {}
|
||||
}
|
||||
]
|
||||
|
||||
HoverHandler {
|
||||
id: notifHover
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: dismissButton
|
||||
|
||||
color: Colors.red
|
||||
implicitWidth: 100
|
||||
radius: 10
|
||||
|
||||
Behavior on anchors.rightMargin {
|
||||
NumberAnimation {
|
||||
duration: 100
|
||||
}
|
||||
}
|
||||
states: [
|
||||
State {
|
||||
name: ""
|
||||
when: notifHover.hovered == false
|
||||
},
|
||||
State {
|
||||
name: "hovered"
|
||||
when: notifHover.hovered == true
|
||||
|
||||
PropertyChanges {
|
||||
dismissButton.anchors.rightMargin: 5
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
StyledText {
|
||||
color: Colors.surface0
|
||||
font.pointSize: 15
|
||||
horizontalAlignment: Qt.AlignRight
|
||||
text: ""
|
||||
verticalAlignment: Qt.AlignVCenter
|
||||
|
||||
anchors {
|
||||
fill: parent
|
||||
rightMargin: 14
|
||||
}
|
||||
}
|
||||
|
||||
TapHandler {
|
||||
onTapped: {
|
||||
notifRoot.currentNotif.dismiss();
|
||||
}
|
||||
}
|
||||
|
||||
anchors {
|
||||
bottom: parent.bottom
|
||||
bottomMargin: 3
|
||||
right: parent.right
|
||||
rightMargin: 10
|
||||
top: parent.top
|
||||
topMargin: 3
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: textContainer
|
||||
|
||||
anchors.fill: parent
|
||||
clip: true
|
||||
color: Colors.surface2
|
||||
radius: 9
|
||||
|
||||
Behavior on anchors.rightMargin {
|
||||
NumberAnimation {
|
||||
duration: 100
|
||||
}
|
||||
}
|
||||
states: [
|
||||
State {
|
||||
name: ""
|
||||
when: notifHover.hovered == false
|
||||
},
|
||||
State {
|
||||
name: "hovered"
|
||||
when: notifHover.hovered == true
|
||||
|
||||
PropertyChanges {
|
||||
textContainer.anchors.rightMargin: 40
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
Column {
|
||||
id: textColumn
|
||||
|
||||
anchors {
|
||||
centerIn: parent
|
||||
left: parent.left
|
||||
right: parent.right
|
||||
}
|
||||
|
||||
StyledText {
|
||||
color: Colors.text
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
text: notifRoot.currentNotif.summary
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
width: notifRoot.width - 10
|
||||
}
|
||||
|
||||
StyledText {
|
||||
color: Colors.subtext1
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
text: notifRoot.currentNotif.body
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
width: notifRoot.width - 10
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
anchors {
|
||||
bottomMargin: 5
|
||||
leftMargin: 10
|
||||
rightMargin: 10
|
||||
topMargin: 5
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MultiEffect {
|
||||
id: notifListClipper
|
||||
|
||||
anchors.fill: notifListContainer
|
||||
maskEnabled: true
|
||||
maskSource: clipsource
|
||||
opacity: 0
|
||||
source: notifListContainer
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: 100
|
||||
}
|
||||
}
|
||||
states: [
|
||||
State {
|
||||
name: ""
|
||||
when: root.rootState != "notifications"
|
||||
|
||||
PropertyChanges {
|
||||
notifListClipper.opacity: 0
|
||||
}
|
||||
},
|
||||
State {
|
||||
name: "expanded"
|
||||
when: root.rootState == "notifications"
|
||||
|
||||
PropertyChanges {
|
||||
notifListClipper.opacity: 1
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: clipsource
|
||||
|
||||
anchors.fill: background
|
||||
color: "black"
|
||||
layer.enabled: true
|
||||
radius: background.radius
|
||||
visible: false
|
||||
}
|
||||
}
|
||||
70
Bar/Left.qml
70
Bar/Left.qml
@@ -1,70 +0,0 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import Quickshell.Hyprland
|
||||
import Quickshell.Services.Notifications
|
||||
import "../Color.js" as Colors
|
||||
import "../Components/"
|
||||
import "../Services/"
|
||||
import "./Center"
|
||||
import "./Left/"
|
||||
|
||||
Container {
|
||||
id: root
|
||||
|
||||
property double extraHoveredWidth: 0
|
||||
required property HyprlandMonitor monitor
|
||||
property double rowLeftMargin: 4
|
||||
property double rowSpacing: 3
|
||||
property double rowYmargin: 3
|
||||
property double selectorFocusedExtra: 15
|
||||
property double selectorHeight: 20
|
||||
property double selectorWidth: 20
|
||||
property double spacing: 3
|
||||
property double workspaceNumber: WorkspaceManager.getNumberOfWorkspaces(monitor)
|
||||
|
||||
boxHeight: 25
|
||||
boxWidth: (selectorWidth * workspaceNumber) + ((2 * rowLeftMargin) + (rowSpacing * (
|
||||
workspaceNumber - 1))) + extraHoveredWidth + (hoverHandler.hovered == true
|
||||
? selectorFocusedExtra : 0)
|
||||
exclusiveMonitor: monitor
|
||||
|
||||
defaultItem: WorkspaceContainer {
|
||||
extraHoveredWidth: root.extraHoveredWidth
|
||||
monitor: root.monitor
|
||||
rowLeftMargin: root.rowLeftMargin
|
||||
rowSpacing: root.rowSpacing
|
||||
rowYmargin: root.rowYmargin
|
||||
selectorFocusedExtra: root.selectorFocusedExtra
|
||||
selectorHeight: root.selectorHeight
|
||||
selectorWidth: root.selectorWidth
|
||||
}
|
||||
Behavior on rowYmargin {
|
||||
SpringAnimation {
|
||||
damping: 0.3
|
||||
spring: 4
|
||||
}
|
||||
}
|
||||
states: [
|
||||
State {
|
||||
name: ""
|
||||
when: !hoverHandler.hovered
|
||||
},
|
||||
State {
|
||||
name: "hovered"
|
||||
when: hoverHandler.hovered
|
||||
|
||||
PropertyChanges {
|
||||
root.boxHeight: 40
|
||||
root.extraHoveredWidth: 25
|
||||
root.rowYmargin: 5
|
||||
root.selectorWidth: 40
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
HoverHandler {
|
||||
id: hoverHandler
|
||||
}
|
||||
}
|
||||
@@ -1,86 +0,0 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import Quickshell.Hyprland
|
||||
import "../../Color.js" as Colors
|
||||
import "../../Components/"
|
||||
import "../../Services/"
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
required property double extraHoveredWidth
|
||||
required property HyprlandMonitor monitor
|
||||
required property double rowLeftMargin
|
||||
required property double rowSpacing
|
||||
required property double rowYmargin
|
||||
required property double selectorFocusedExtra
|
||||
required property double selectorHeight
|
||||
required property double selectorWidth
|
||||
|
||||
Item {
|
||||
id: newWorkspaceButton
|
||||
|
||||
implicitWidth: 20
|
||||
opacity: root.extraHoveredWidth > 0 ? 1 : 0
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: 100
|
||||
}
|
||||
}
|
||||
|
||||
TapHandler {
|
||||
id: tapHandler
|
||||
|
||||
onTapped: WorkspaceManager.activateNextFreeWorkspace()
|
||||
}
|
||||
|
||||
anchors {
|
||||
bottom: parent.bottom
|
||||
bottomMargin: 5 + root.rowYmargin
|
||||
right: parent.right
|
||||
rightMargin: 4
|
||||
top: parent.top
|
||||
topMargin: 5 + root.rowYmargin
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
color: Colors.green
|
||||
radius: 4
|
||||
|
||||
StyledText {
|
||||
anchors.centerIn: parent
|
||||
text: "+"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
spacing: root.rowSpacing
|
||||
|
||||
anchors {
|
||||
bottom: parent.bottom
|
||||
bottomMargin: root.rowYmargin
|
||||
left: parent.left
|
||||
leftMargin: root.rowLeftMargin
|
||||
top: parent.top
|
||||
topMargin: root.rowYmargin
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: WorkspaceManager.getWorkspacesForMonitor(root.monitor)
|
||||
|
||||
WorkspaceSelector {
|
||||
required property HyprlandWorkspace modelData
|
||||
|
||||
extraHoveredWidth: root.extraHoveredWidth
|
||||
selectorFocusedExtra: root.selectorFocusedExtra
|
||||
selectorHeight: root.selectorHeight
|
||||
selectorWidth: root.selectorWidth
|
||||
workspace: modelData
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,87 +0,0 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import Quickshell
|
||||
import Quickshell.Hyprland
|
||||
import "../../Color.js" as Colors
|
||||
import "../../Components/"
|
||||
import "../../Services/"
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
required property double extraHoveredWidth
|
||||
property bool hovered: extraHoveredWidth > 0 ? true : false
|
||||
property color selectorColor: Colors.surface2
|
||||
required property double selectorFocusedExtra
|
||||
required property double selectorHeight
|
||||
required property double selectorWidth
|
||||
required property HyprlandWorkspace workspace
|
||||
property string workspaceState: WorkspaceManager.getWorkspaceState(workspace)
|
||||
|
||||
implicitWidth: selectorWidth + (root.hovered == true ? (root.workspaceState == "focused"
|
||||
? selectorFocusedExtra : 0) : 0)
|
||||
|
||||
Behavior on implicitWidth {
|
||||
SpringAnimation {
|
||||
damping: 0.3
|
||||
spring: 4
|
||||
}
|
||||
}
|
||||
states: [
|
||||
State {
|
||||
name: ""
|
||||
when: root.workspaceState == "inactive"
|
||||
|
||||
PropertyChanges {
|
||||
root.selectorColor: Colors.surface2
|
||||
}
|
||||
},
|
||||
State {
|
||||
name: "active"
|
||||
when: root.workspaceState == "active"
|
||||
|
||||
PropertyChanges {
|
||||
root.selectorColor: Colors.overlay2
|
||||
}
|
||||
},
|
||||
State {
|
||||
name: "focused"
|
||||
when: root.workspaceState == "focused"
|
||||
|
||||
PropertyChanges {
|
||||
root.selectorColor: Colors.lavender
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
TapHandler {
|
||||
id: tapHandler
|
||||
|
||||
onTapped: root.workspace.activate()
|
||||
}
|
||||
|
||||
anchors {
|
||||
bottom: parent.bottom
|
||||
top: parent.top
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
color: root.selectorColor
|
||||
radius: 6
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: 100
|
||||
}
|
||||
}
|
||||
|
||||
anchors {
|
||||
fill: parent
|
||||
}
|
||||
|
||||
CenteredText {
|
||||
font.pointSize: 10.5
|
||||
text: root.workspace.id
|
||||
}
|
||||
}
|
||||
}
|
||||
114
Bar/Network.qml
Normal file
114
Bar/Network.qml
Normal file
@@ -0,0 +1,114 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import Quickshell.Hyprland
|
||||
|
||||
import "../Color.js" as Colors
|
||||
import "../Components/"
|
||||
import "../Services/"
|
||||
|
||||
Container {
|
||||
id: root
|
||||
|
||||
required property HyprlandMonitor monitor
|
||||
property double networkDisplayHeight: 80
|
||||
|
||||
animOffset: 250
|
||||
boxColor: Colors.red
|
||||
boxHeight: 25
|
||||
boxHeightOpened: root.networkDisplayHeight
|
||||
boxWidth: 33
|
||||
boxWidthOpened: 350
|
||||
defaultItem: icon
|
||||
exclusiveMonitor: root.monitor
|
||||
exclusiveToScreen: true
|
||||
forceHidden: !NetworkManager.connectedWifi
|
||||
openedItem: networkDisplay
|
||||
|
||||
Component {
|
||||
id: icon
|
||||
|
||||
StyledText {
|
||||
horizontalAlignment: Qt.AlignCenter
|
||||
propo: true
|
||||
text: NetworkManager.getNetworkDetails(NetworkManager.defaultAdapter)["icon"] || ""
|
||||
verticalAlignment: Qt.AlignVCenter
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: networkDisplay
|
||||
|
||||
Item {
|
||||
id: networkDisplayRoot
|
||||
|
||||
property double innerMargin: 5
|
||||
property double outerMargin: 6
|
||||
|
||||
Rectangle {
|
||||
id: networkDisplayBg
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.margins: networkDisplayRoot.outerMargin
|
||||
color: Colors.surface1
|
||||
radius: 8
|
||||
|
||||
RowLayout {
|
||||
anchors.fill: parent
|
||||
|
||||
Rectangle {
|
||||
id: iconBox
|
||||
|
||||
property double iconBoxRadius: 5
|
||||
|
||||
Layout.fillHeight: true
|
||||
Layout.margins: networkDisplayRoot.innerMargin
|
||||
color: Colors.red
|
||||
implicitWidth: root.networkDisplayHeight - (networkDisplayRoot.innerMargin * 2) - (
|
||||
networkDisplayRoot.outerMargin * 2)
|
||||
radius: 5
|
||||
|
||||
CenteredText {
|
||||
fontSize: 23
|
||||
propo: true
|
||||
text: NetworkManager.getNetworkDetails(NetworkManager.defaultAdapter)["icon"]
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
id: networkColumn
|
||||
|
||||
property var networkInfo: NetworkManager.getNetworkDetails(NetworkManager.defaultAdapter)
|
||||
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
Layout.fillWidth: true
|
||||
|
||||
StyledText {
|
||||
color: Colors.text
|
||||
fontSize: 14
|
||||
text: networkColumn.networkInfo["networkName"]
|
||||
}
|
||||
|
||||
StyledText {
|
||||
color: Colors.subtext1
|
||||
fontSize: 12
|
||||
fontWeight: 5
|
||||
text: networkColumn.networkInfo["ipAddress"] + " on "
|
||||
+ networkColumn.networkInfo["adapterName"]
|
||||
}
|
||||
|
||||
StyledText {
|
||||
property var timeInfo: networkColumn.networkInfo["time"]
|
||||
|
||||
color: Colors.subtext1
|
||||
fontSize: 12
|
||||
fontWeight: 5
|
||||
text: "Network Details " + (networkColumn.networkInfo["saved"] == true ? "Saved" : "Not Saved")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import Quickshell.Hyprland
|
||||
import Quickshell.Services.Notifications
|
||||
import "../Color.js" as Colors
|
||||
import "../Components/"
|
||||
import "../Services/"
|
||||
import "./Center"
|
||||
import "./Right/"
|
||||
|
||||
Row {
|
||||
id: root
|
||||
|
||||
required property HyprlandMonitor monitor
|
||||
|
||||
Bluetooth {
|
||||
exclusiveMonitor: root.monitor
|
||||
}
|
||||
}
|
||||
@@ -1,98 +0,0 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Bluetooth
|
||||
import "../../Color.js" as Colors
|
||||
import "../../Components/"
|
||||
import "../../Services/"
|
||||
import "Bluetooth"
|
||||
|
||||
Container {
|
||||
id: root
|
||||
|
||||
property bool shown: false
|
||||
|
||||
animOffset: 200
|
||||
boxColor: Colors.peach
|
||||
boxHeight: 25
|
||||
boxWidth: 30
|
||||
defaultItem: icon
|
||||
exclusiveToScreen: true
|
||||
forceHidden: !(root.shown || BluetoothManager.getConnected())
|
||||
hoverableWhenHidden: true
|
||||
state: ""
|
||||
|
||||
states: [
|
||||
State {
|
||||
name: ""
|
||||
|
||||
PropertyChanges {
|
||||
root.boxHeight: 25
|
||||
root.boxWidth: 30
|
||||
}
|
||||
|
||||
StateChangeScript {
|
||||
script: {
|
||||
root.stack.replace(icon);
|
||||
}
|
||||
}
|
||||
},
|
||||
State {
|
||||
name: "expanded"
|
||||
|
||||
PropertyChanges {
|
||||
root.boxHeight: 250
|
||||
root.boxRadius: 20
|
||||
root.boxWidth: 350
|
||||
}
|
||||
|
||||
StateChangeScript {
|
||||
script: {
|
||||
root.stack.replace(expanded);
|
||||
root.shown = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
hover.onHoveredChanged: {
|
||||
if (hover.hovered == false) {
|
||||
root.state = "";
|
||||
hoverIntervalTimer.start();
|
||||
} else {
|
||||
root.shown = true;
|
||||
hoverIntervalTimer.start();
|
||||
}
|
||||
}
|
||||
tap.onTapped: state = "expanded"
|
||||
|
||||
Timer {
|
||||
id: hoverIntervalTimer
|
||||
|
||||
interval: 500
|
||||
repeat: false
|
||||
running: false
|
||||
|
||||
onTriggered: {
|
||||
if (root.hover.hovered == false) {
|
||||
root.shown = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: icon
|
||||
|
||||
Item {
|
||||
CenteredText {
|
||||
text: BluetoothManager.getConnected() == true ? "" : ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: expanded
|
||||
|
||||
Expanded {}
|
||||
}
|
||||
}
|
||||
@@ -1,384 +0,0 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Quickshell.Bluetooth
|
||||
import "../../../Color.js" as Colors
|
||||
import "../../../Components/"
|
||||
import "../../../Services/"
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property double topBarHeight: 35
|
||||
|
||||
Rectangle {
|
||||
color: Colors.surface0
|
||||
radius: 17
|
||||
|
||||
anchors {
|
||||
bottomMargin: 5
|
||||
fill: parent
|
||||
leftMargin: 5
|
||||
rightMargin: 5
|
||||
topMargin: 5
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: topBar
|
||||
|
||||
property double topBarHeight: root.topBarHeight
|
||||
|
||||
color: Colors.peach
|
||||
implicitHeight: topBarHeight
|
||||
radius: 9
|
||||
|
||||
anchors {
|
||||
left: parent.left
|
||||
leftMargin: 5
|
||||
right: parent.right
|
||||
rightMargin: 5
|
||||
top: parent.top
|
||||
topMargin: 5
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
id: topBarRow
|
||||
|
||||
property double topBarRowPadding: 4
|
||||
|
||||
spacing: 3
|
||||
|
||||
anchors {
|
||||
left: parent.left
|
||||
leftMargin: topBarRowPadding
|
||||
right: parent.right
|
||||
rightMargin: topBarRowPadding
|
||||
top: parent.top
|
||||
topMargin: topBarRowPadding
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
// color: Qt.lighter(Colors.peach, 1.2)
|
||||
color: Colors.surface1
|
||||
implicitHeight: topBar.topBarHeight - (topBarRow.topBarRowPadding * 2)
|
||||
radius: 6
|
||||
|
||||
StyledText {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
color: Colors.text
|
||||
horizontalAlignment: Qt.AlignLeft
|
||||
text: " " + BluetoothManager.defaultAdapterName
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: scanButton
|
||||
|
||||
color: Qt.lighter(Colors.peach, 1.15)
|
||||
implicitHeight: topBar.topBarHeight - (topBarRow.topBarRowPadding * 2)
|
||||
implicitWidth: topBar.topBarHeight - (topBarRow.topBarRowPadding * 2)
|
||||
radius: 6
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: 100
|
||||
}
|
||||
}
|
||||
states: [
|
||||
State {
|
||||
name: "hovered"
|
||||
|
||||
PropertyChanges {
|
||||
scanButton.color: Colors.green
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
border {
|
||||
color: Colors.surface0
|
||||
width: 2
|
||||
}
|
||||
|
||||
HoverHandler {
|
||||
onHoveredChanged: hovered == true ? scanButton.state = "hovered" : scanButton.state = ""
|
||||
}
|
||||
|
||||
TapHandler {
|
||||
onTapped: {
|
||||
BluetoothManager.toggleDiscover();
|
||||
}
|
||||
}
|
||||
|
||||
CenteredText {
|
||||
id: scanButtonText
|
||||
|
||||
text: ""
|
||||
|
||||
NumberAnimation {
|
||||
id: animateRotation
|
||||
|
||||
duration: 1000
|
||||
from: 0
|
||||
loops: Animation.Infinite
|
||||
properties: "rotation"
|
||||
running: BluetoothManager.defaultAdapter.discovering
|
||||
target: scanButtonText
|
||||
to: 360
|
||||
|
||||
onStopped: {
|
||||
scanButton.rotation = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ListView {
|
||||
boundsBehavior: Flickable.StopAtBounds
|
||||
clip: true
|
||||
model: BluetoothManager.getDevicesList()
|
||||
spacing: 5
|
||||
|
||||
delegate: Item {
|
||||
id: deviceRoot
|
||||
|
||||
required property real index
|
||||
required property BluetoothDevice modelData
|
||||
|
||||
height: 55
|
||||
width: ListView.view.width
|
||||
|
||||
Rectangle {
|
||||
id: containerBox
|
||||
|
||||
anchors.fill: parent
|
||||
clip: true
|
||||
color: Colors.peach
|
||||
radius: 7
|
||||
|
||||
Rectangle {
|
||||
anchors.centerIn: parent
|
||||
color: Colors.green
|
||||
implicitHeight: parent.height - 10
|
||||
implicitWidth: parent.width * (deviceRoot.modelData.batteryAvailable == true
|
||||
? deviceRoot.modelData.battery : 0) - 10
|
||||
opacity: 0.7
|
||||
radius: 7
|
||||
visible: deviceRoot.modelData.batteryAvailable == true
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
anchors {
|
||||
bottomMargin: 4
|
||||
fill: parent
|
||||
leftMargin: 4
|
||||
rightMargin: 4
|
||||
topMargin: 4
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
Layout.fillHeight: true
|
||||
Layout.preferredWidth: height
|
||||
color: deviceRoot.modelData.connected ? Colors.green : Qt.lighter(Colors.peach, 1.25)
|
||||
radius: 5
|
||||
|
||||
CenteredText {
|
||||
color: Colors.surface0
|
||||
font.pointSize: 30
|
||||
text: BluetoothManager.getIcon(deviceRoot.modelData.icon)
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
|
||||
Rectangle {
|
||||
Layout.fillHeight: true
|
||||
Layout.fillWidth: true
|
||||
color: Qt.lighter(Colors.peach, 1.15)
|
||||
radius: 4
|
||||
|
||||
StyledText {
|
||||
anchors.fill: parent
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
elide: Qt.ElideRight
|
||||
text: deviceRoot.modelData.name
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
Layout.fillHeight: true
|
||||
Layout.fillWidth: true
|
||||
color: "transparent"
|
||||
|
||||
StyledText {
|
||||
anchors.fill: parent
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
color: Colors.surface2
|
||||
font.weight: 300
|
||||
text: deviceRoot.modelData.address
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillHeight: true
|
||||
Layout.preferredWidth: height
|
||||
|
||||
RowLayout {
|
||||
anchors.fill: parent
|
||||
|
||||
Rectangle {
|
||||
id: itemEntry
|
||||
|
||||
Layout.fillHeight: true
|
||||
Layout.fillWidth: true
|
||||
color: Colors.red
|
||||
radius: 3
|
||||
|
||||
Loader {
|
||||
id: deleteButtonLoader
|
||||
|
||||
anchors.fill: parent
|
||||
sourceComponent: forgetButton
|
||||
|
||||
states: [
|
||||
State {
|
||||
name: ""
|
||||
when: deviceRoot.modelData.connected == false
|
||||
|
||||
PropertyChanges {
|
||||
deleteButtonLoader.sourceComponent: forgetButton
|
||||
}
|
||||
},
|
||||
State {
|
||||
name: "connected"
|
||||
when: deviceRoot.modelData.connected == true
|
||||
|
||||
PropertyChanges {
|
||||
deleteButtonLoader.sourceComponent: disconnectButton
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
Component {
|
||||
id: disconnectButton
|
||||
|
||||
Item {
|
||||
anchors.fill: parent
|
||||
|
||||
TapHandler {
|
||||
onTapped: deviceRoot.modelData.disconnect()
|
||||
}
|
||||
|
||||
CenteredText {
|
||||
text: ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: forgetButton
|
||||
|
||||
Item {
|
||||
anchors.fill: parent
|
||||
|
||||
TapHandler {
|
||||
onTapped: deviceRoot.modelData.forget()
|
||||
}
|
||||
|
||||
CenteredText {
|
||||
text: ""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: greenButton
|
||||
|
||||
property Component loadedItem: connectButton
|
||||
|
||||
Layout.fillHeight: true
|
||||
Layout.fillWidth: true
|
||||
color: Colors.green
|
||||
radius: 3
|
||||
|
||||
states: [
|
||||
State {
|
||||
name: ""
|
||||
when: deviceRoot.modelData.paired == true
|
||||
|
||||
PropertyChanges {
|
||||
greenButton.color: Colors.green
|
||||
greenButton.loadedItem: connectButton
|
||||
}
|
||||
},
|
||||
State {
|
||||
name: "unpaired"
|
||||
when: deviceRoot.modelData.paired == false
|
||||
|
||||
PropertyChanges {
|
||||
greenButton.color: Colors.blue
|
||||
greenButton.loadedItem: pairButton
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
Loader {
|
||||
anchors.fill: parent
|
||||
sourceComponent: greenButton.loadedItem
|
||||
}
|
||||
|
||||
Component {
|
||||
id: connectButton
|
||||
|
||||
Item {
|
||||
anchors.fill: parent
|
||||
|
||||
CenteredText {
|
||||
text: ""
|
||||
}
|
||||
|
||||
TapHandler {
|
||||
onTapped: deviceRoot.modelData.connect()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: pairButton
|
||||
|
||||
Item {
|
||||
anchors.fill: parent
|
||||
|
||||
CenteredText {
|
||||
text: ""
|
||||
}
|
||||
|
||||
TapHandler {
|
||||
onTapped: {
|
||||
deviceRoot.modelData.pair();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
anchors {
|
||||
bottomMargin: 5
|
||||
fill: parent
|
||||
leftMargin: 5
|
||||
rightMargin: 5
|
||||
topMargin: 10 + root.topBarHeight
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
437
Bar/Time.qml
Normal file
437
Bar/Time.qml
Normal file
@@ -0,0 +1,437 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import Quickshell.Hyprland
|
||||
import Quickshell.Services.Notifications
|
||||
import Quickshell.Widgets
|
||||
|
||||
import "../Color.js" as Colors
|
||||
import "../Components/"
|
||||
import "../Services/"
|
||||
|
||||
Container {
|
||||
id: root
|
||||
|
||||
property Notification latestNotif
|
||||
property bool logout: false
|
||||
property double logoutHeight: 100
|
||||
property double logoutMargin: 7
|
||||
property double logoutSpacing: 5
|
||||
required property HyprlandMonitor monitor
|
||||
property bool mpris: false
|
||||
property double mprisHeight: 60
|
||||
property double mprisWidth: 350
|
||||
property double notificationHeight: 100
|
||||
property double notificationWidth: 350
|
||||
property bool notified: false
|
||||
|
||||
boxColor: Colors.mauve
|
||||
defaultItem: time
|
||||
exclusiveMonitor: root.monitor
|
||||
exclusiveToScreen: true
|
||||
|
||||
states: [
|
||||
State {
|
||||
name: "notified"
|
||||
when: root.notified == true
|
||||
|
||||
PropertyChanges {
|
||||
root.boxHeight: root.notificationHeight
|
||||
root.boxRadius: 12
|
||||
root.boxWidth: root.notificationWidth
|
||||
root.visibleTopMargin: 5
|
||||
}
|
||||
|
||||
StateChangeScript {
|
||||
script: {
|
||||
root.stack.replace(notification);
|
||||
notificationTimer.restart();
|
||||
}
|
||||
}
|
||||
},
|
||||
State {
|
||||
name: "mpris"
|
||||
when: root.mpris == true
|
||||
|
||||
PropertyChanges {
|
||||
root.boxHeight: root.mprisHeight
|
||||
root.boxRadius: 12
|
||||
root.boxWidth: root.mprisWidth
|
||||
root.visibleTopMargin: root.hovered == true ? 0 : 5
|
||||
}
|
||||
|
||||
StateChangeScript {
|
||||
script: {
|
||||
root.stack.replace(mprisToast);
|
||||
mprisTimer.restart();
|
||||
}
|
||||
}
|
||||
},
|
||||
State {
|
||||
name: "logout"
|
||||
when: root.logout == true
|
||||
|
||||
PropertyChanges {
|
||||
root.boxHeight: root.logoutHeight
|
||||
root.boxRadius: 13
|
||||
root.boxWidth: ((root.logoutHeight - (root.logoutMargin * 2)) * 5) + (root.logoutMargin * 2) + (
|
||||
root.logoutSpacing * 4)
|
||||
root.visibleTopMargin: 10
|
||||
}
|
||||
|
||||
StateChangeScript {
|
||||
script: {
|
||||
root.stack.replace(logoutMenu);
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
onHoveredChanged: {
|
||||
if (root.hovered == true && MprisManager.getPlaying() == true) {
|
||||
root.overriden = true;
|
||||
root.mpris = true;
|
||||
} else {
|
||||
root.overriden = false;
|
||||
root.mpris = true;
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
function onCloseLogoutMenu() {
|
||||
root.overriden = false;
|
||||
root.logout = false;
|
||||
}
|
||||
|
||||
function onLogoutMenu() {
|
||||
root.overriden = true;
|
||||
root.logout = true;
|
||||
}
|
||||
|
||||
target: IpcManager
|
||||
}
|
||||
|
||||
Connections {
|
||||
function onNewNotification() {
|
||||
notification => {
|
||||
if (notification.lastGeneration == false) {
|
||||
root.latestNotif = notification;
|
||||
root.overriden = true;
|
||||
root.notified = true;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
target: NotificationManager
|
||||
}
|
||||
|
||||
Connections {
|
||||
function onTrackChanged() {
|
||||
root.overriden = true;
|
||||
root.mpris = true;
|
||||
console.log("track changed");
|
||||
}
|
||||
|
||||
target: MprisManager.defaultPlayer
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: notificationTimer
|
||||
|
||||
interval: 2000
|
||||
repeat: false
|
||||
running: false
|
||||
|
||||
onTriggered: {
|
||||
root.notified = false;
|
||||
root.overriden = false;
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: mprisTimer
|
||||
|
||||
interval: 3000
|
||||
repeat: false
|
||||
running: false
|
||||
|
||||
onTriggered: {
|
||||
root.mpris = false;
|
||||
root.overriden = false;
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: logoutMenu
|
||||
|
||||
Item {
|
||||
id: logoutMenuRoot
|
||||
|
||||
property var logoutModel: [
|
||||
{
|
||||
color: Colors.red,
|
||||
text: "s",
|
||||
key: Qt.Key_S
|
||||
},
|
||||
{
|
||||
color: Colors.peach,
|
||||
text: "e",
|
||||
key: Qt.Key_E
|
||||
},
|
||||
{
|
||||
color: Colors.yellow,
|
||||
text: "r",
|
||||
key: Qt.Key_R
|
||||
},
|
||||
{
|
||||
color: Colors.green,
|
||||
text: "l",
|
||||
key: Qt.Key_L
|
||||
},
|
||||
{
|
||||
color: Colors.sky,
|
||||
text: "u",
|
||||
key: Qt.Key_U
|
||||
},
|
||||
]
|
||||
|
||||
Component.onCompleted: {
|
||||
forceActiveFocus();
|
||||
}
|
||||
Keys.onPressed: event => {
|
||||
if (event.key == Qt.Key_Escape) {
|
||||
IpcManager.closeLogoutMenu();
|
||||
} else {
|
||||
for (let button of logoutMenuRoot.logoutModel) {
|
||||
if (button.key == event.key) {
|
||||
event.accepted = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
spacing: root.logoutSpacing
|
||||
|
||||
anchors {
|
||||
fill: parent
|
||||
margins: root.logoutMargin
|
||||
}
|
||||
|
||||
Repeater {
|
||||
delegate: logoutButton
|
||||
model: logoutMenuRoot.logoutModel
|
||||
}
|
||||
|
||||
Component {
|
||||
id: logoutButton
|
||||
|
||||
Item {
|
||||
id: logoutButtonRoot
|
||||
|
||||
// Key yen isn't on my keyboard, acts as an errorless analog for no bind
|
||||
required property var modelData
|
||||
|
||||
Layout.fillHeight: true
|
||||
Layout.fillWidth: true
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
color: logoutButtonRoot.modelData.color
|
||||
radius: 7
|
||||
|
||||
StyledText {
|
||||
anchors.fill: parent
|
||||
horizontalAlignment: Qt.AlignHCenter
|
||||
text: logoutButtonRoot.modelData.text
|
||||
verticalAlignment: Qt.AlignVCenter
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: time
|
||||
|
||||
StyledText {
|
||||
property string component: "time"
|
||||
|
||||
horizontalAlignment: Qt.AlignCenter
|
||||
text: TimeManager.time
|
||||
verticalAlignment: Qt.AlignVCenter
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: notification
|
||||
|
||||
Item {
|
||||
id: notificationRoot
|
||||
|
||||
property double innerMargin: 5
|
||||
property double outerMargin: 6
|
||||
|
||||
function getInnerHeight() {
|
||||
let fullHeight = root.notificationHeight;
|
||||
let fullMargin = notificationRoot.innerMargin + notificationRoot.outerMargin;
|
||||
|
||||
return fullHeight - (fullMargin * 2);
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
color: Colors.surface1
|
||||
radius: 9
|
||||
|
||||
anchors {
|
||||
fill: parent
|
||||
margins: notificationRoot.outerMargin
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Rectangle {
|
||||
Layout.margins: notificationRoot.innerMargin
|
||||
color: Colors.mauve
|
||||
implicitHeight: notificationRoot.getInnerHeight()
|
||||
implicitWidth: notificationRoot.getInnerHeight()
|
||||
radius: 5
|
||||
|
||||
IconImage {
|
||||
anchors.fill: parent
|
||||
anchors.margins: 10
|
||||
source: root.latestNotif.image
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
property double textMargin: 4
|
||||
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 0
|
||||
Layout.margins: notificationRoot.innerMargin + textMargin
|
||||
|
||||
StyledText {
|
||||
color: Colors.text
|
||||
fontSize: 14
|
||||
text: root.latestNotif.summary
|
||||
}
|
||||
|
||||
StyledText {
|
||||
Layout.fillHeight: true
|
||||
Layout.maximumWidth: root.notificationWidth - ((notificationRoot.outerMargin * 2) + (
|
||||
notificationRoot.innerMargin * 3) + notificationRoot.getInnerHeight())
|
||||
color: Colors.subtext0
|
||||
elide: Qt.ElideRight
|
||||
fontWeight: 5
|
||||
maximumLineCount: 2
|
||||
text: root.latestNotif.body
|
||||
wrapMode: Text.WordWrap
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: mprisToast
|
||||
|
||||
Item {
|
||||
id: mprisToastRoot
|
||||
|
||||
property double innerMargin: 4
|
||||
property double outerMargin: 4
|
||||
property var trackInfo: MprisManager.getTrackInfo()
|
||||
|
||||
function getInnerHeight() {
|
||||
let fullHeight = root.mprisHeight;
|
||||
let fullMargin = mprisToastRoot.innerMargin + mprisToastRoot.outerMargin;
|
||||
|
||||
return fullHeight - (fullMargin * 2);
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
color: Colors.surface1
|
||||
radius: 9
|
||||
|
||||
anchors {
|
||||
fill: parent
|
||||
margins: mprisToastRoot.outerMargin
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Rectangle {
|
||||
Layout.margins: mprisToastRoot.innerMargin
|
||||
color: Colors.mauve
|
||||
implicitHeight: mprisToastRoot.getInnerHeight()
|
||||
implicitWidth: mprisToastRoot.getInnerHeight()
|
||||
radius: 5
|
||||
|
||||
IconImage {
|
||||
anchors.fill: parent
|
||||
source: mprisToastRoot.trackInfo["albumArt"]
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
property double textMargin: 1
|
||||
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 0
|
||||
Layout.margins: mprisToastRoot.innerMargin + textMargin
|
||||
|
||||
StyledText {
|
||||
Layout.maximumWidth: root.mprisWidth - ((mprisToastRoot.outerMargin * 2) + (
|
||||
mprisToastRoot.innerMargin * 5) + mprisToastRoot.getInnerHeight())
|
||||
color: Colors.text
|
||||
fontSize: 14
|
||||
text: mprisToastRoot.trackInfo["name"] + " - " + mprisToastRoot.trackInfo["artist"]
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillHeight: true
|
||||
Layout.fillWidth: true
|
||||
Layout.maximumWidth: root.mprisWidth - ((mprisToastRoot.outerMargin * 2) + (
|
||||
mprisToastRoot.innerMargin * 5) + mprisToastRoot.getInnerHeight())
|
||||
Layout.preferredWidth: root.mprisWidth - ((mprisToastRoot.outerMargin * 2) + (
|
||||
mprisToastRoot.innerMargin * 5) + mprisToastRoot.getInnerHeight())
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
color: Colors.overlay0
|
||||
radius: 100
|
||||
|
||||
Rectangle {
|
||||
color: Colors.mauve
|
||||
implicitWidth: parent.width * mprisToastRoot.trackInfo["lengthPercent"]
|
||||
radius: 100
|
||||
|
||||
anchors {
|
||||
bottom: parent.bottom
|
||||
left: parent.left
|
||||
top: parent.top
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: MprisManager.getTimeString()
|
||||
|
||||
anchors {
|
||||
horizontalCenter: parent.horizontalCenter
|
||||
verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
96
Bar/WorkspaceSelector.qml
Normal file
96
Bar/WorkspaceSelector.qml
Normal file
@@ -0,0 +1,96 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import Quickshell.Hyprland
|
||||
|
||||
import "../Color.js" as Colors
|
||||
import "../Components/"
|
||||
import "../Services/"
|
||||
|
||||
Container {
|
||||
id: root
|
||||
|
||||
property double barHeight: 25
|
||||
required property HyprlandMonitor monitor
|
||||
property double rowLeftMargin: 5
|
||||
property double workspaceButtonSpacing: 4
|
||||
property double workspaceCount: WorkspaceManager.getNumberOfWorkspaces(monitor)
|
||||
property double workspaceWidth: 17
|
||||
|
||||
boxHeight: barHeight
|
||||
boxRadius: 8
|
||||
boxWidth: (2 * rowLeftMargin) + (workspaceCount * workspaceWidth) + ((workspaceCount - 1)
|
||||
* workspaceButtonSpacing)
|
||||
defaultItem: placeholder
|
||||
exclusiveMonitor: monitor
|
||||
|
||||
Component {
|
||||
id: placeholder
|
||||
|
||||
Item {}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
spacing: root.workspaceButtonSpacing
|
||||
|
||||
anchors {
|
||||
bottom: parent.bottom
|
||||
left: parent.left
|
||||
leftMargin: root.rowLeftMargin
|
||||
top: parent.top
|
||||
}
|
||||
|
||||
Repeater {
|
||||
delegate: workspaceButton
|
||||
delegateModelAccess: DelegateModel.ReadOnly
|
||||
model: WorkspaceManager.getWorkspacesForMonitor(root.exclusiveMonitor)
|
||||
}
|
||||
|
||||
Component {
|
||||
id: workspaceButton
|
||||
|
||||
Item {
|
||||
id: buttonRoot
|
||||
|
||||
required property var modelData
|
||||
property double stateInt: WorkspaceManager.getWorkspaceStateIndex(modelData)
|
||||
property variant workspaceColors: [Colors.lavender, Colors.overlay2, Colors.surface2]
|
||||
property string workspaceId: modelData.id
|
||||
|
||||
Layout.alignment: Qt.AlignLeft
|
||||
implicitHeight: root.barHeight - 7
|
||||
implicitWidth: root.workspaceWidth
|
||||
|
||||
Behavior on implicitHeight {
|
||||
SpringAnimation {
|
||||
damping: 0.3
|
||||
spring: 4
|
||||
}
|
||||
}
|
||||
Behavior on implicitWidth {
|
||||
SpringAnimation {
|
||||
damping: 0.3
|
||||
spring: 4
|
||||
}
|
||||
}
|
||||
|
||||
TapHandler {
|
||||
id: tapHandler
|
||||
|
||||
onTapped: WorkspaceManager.activateWorkspaceById(buttonRoot.modelData.id)
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
color: buttonRoot.workspaceColors[buttonRoot.stateInt]
|
||||
radius: 4
|
||||
|
||||
CenteredText {
|
||||
text: buttonRoot.modelData.id
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,11 +9,17 @@ import "../Color.js" as Colors
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property bool _fullyHidden: false
|
||||
property bool _widthCollapsed: false
|
||||
property double animOffset: 0
|
||||
property color boxColor: Colors.surface0
|
||||
property double boxHeight: 28
|
||||
property double boxHeightOpened: 150
|
||||
property bool boxOpened: false
|
||||
property double boxRadius: 9
|
||||
property double boxRadiusOpened: 12
|
||||
property double boxWidth: 100
|
||||
property double boxWidthOpened: 200
|
||||
readonly property double calculatedTopMargin: {
|
||||
if (!root.exclusiveToScreen) {
|
||||
return visibleTopMargin;
|
||||
@@ -35,35 +41,125 @@ Item {
|
||||
property bool forceHidden: false
|
||||
property alias hover: hoverHandler
|
||||
property bool hoverableWhenHidden: false
|
||||
property bool hovered: false
|
||||
property Component openedItem
|
||||
property bool overriden: false
|
||||
property alias rect: container
|
||||
property alias stack: containerContent
|
||||
property alias tap: tapHandler
|
||||
property double visibleTopMargin: 0
|
||||
|
||||
implicitHeight: boxHeight
|
||||
implicitWidth: boxWidth
|
||||
implicitWidth: root._widthCollapsed ? 0 : root.boxWidth
|
||||
visible: !root._fullyHidden
|
||||
|
||||
Behavior on anchors.topMargin {
|
||||
animation: defaultCurve
|
||||
}
|
||||
Behavior on boxHeight {
|
||||
SpringAnimation {
|
||||
damping: 0.3
|
||||
spring: 4
|
||||
}
|
||||
}
|
||||
Behavior on boxRadius {
|
||||
SpringAnimation {
|
||||
damping: 0.3
|
||||
spring: 4
|
||||
}
|
||||
}
|
||||
Behavior on boxWidth {
|
||||
Behavior on implicitHeight {
|
||||
SpringAnimation {
|
||||
damping: 0.3
|
||||
spring: 4
|
||||
}
|
||||
}
|
||||
Behavior on implicitWidth {
|
||||
SpringAnimation {
|
||||
damping: 0.3
|
||||
spring: 4
|
||||
}
|
||||
}
|
||||
states: [
|
||||
State {
|
||||
name: "closed"
|
||||
when: root.hovered == false && root.boxOpened == false && root.overriden == false
|
||||
|
||||
PropertyChanges {
|
||||
container.radius: root.boxRadius
|
||||
root.implicitHeight: root.boxHeight
|
||||
root.implicitWidth: root.boxWidth
|
||||
}
|
||||
|
||||
StateChangeScript {
|
||||
script: {
|
||||
stack.replace(root.defaultItem);
|
||||
}
|
||||
}
|
||||
},
|
||||
State {
|
||||
name: "hovered"
|
||||
when: root.hovered == true && root.boxOpened == false && root.overriden == false
|
||||
|
||||
PropertyChanges {
|
||||
container.radius: root.boxRadius + 3
|
||||
root.implicitHeight: root.boxHeight + 2
|
||||
root.implicitWidth: root.boxWidth + 2
|
||||
}
|
||||
|
||||
StateChangeScript {
|
||||
script: {
|
||||
stack.replace(root.defaultItem);
|
||||
}
|
||||
}
|
||||
},
|
||||
State {
|
||||
name: "opened"
|
||||
when: root.boxOpened == true && root.hovered == true && root.overriden == false
|
||||
|
||||
PropertyChanges {
|
||||
container.radius: root.boxRadiusOpened
|
||||
root.implicitHeight: root.boxHeightOpened
|
||||
root.implicitWidth: root.boxWidthOpened
|
||||
}
|
||||
|
||||
StateChangeScript {
|
||||
script: {
|
||||
stack.replace(root.openedItem);
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
onForceHiddenChanged: {
|
||||
if (root.forceHidden == true) {
|
||||
hideTimer.restart();
|
||||
} else {
|
||||
hideTimer.stop();
|
||||
subHideTimer.stop();
|
||||
root._fullyHidden = false;
|
||||
root._widthCollapsed = false;
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: hideTimer
|
||||
|
||||
interval: 250
|
||||
repeat: false
|
||||
running: false
|
||||
|
||||
onTriggered: {
|
||||
root._widthCollapsed = true;
|
||||
subHideTimer.restart();
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: subHideTimer
|
||||
|
||||
interval: 200
|
||||
repeat: false
|
||||
running: false
|
||||
|
||||
onTriggered: {
|
||||
root._fullyHidden = true;
|
||||
}
|
||||
}
|
||||
|
||||
anchors {
|
||||
top: parent.top
|
||||
@@ -79,20 +175,28 @@ Item {
|
||||
|
||||
HoverHandler {
|
||||
id: hoverHandler
|
||||
|
||||
onHoveredChanged: {
|
||||
if (hoverHandler.hovered == true) {
|
||||
root.hovered = true;
|
||||
} else {
|
||||
root.hovered = false;
|
||||
root.boxOpened = false;
|
||||
if (root.state == "closed") {
|
||||
root.stack.replace(root.defaultItem);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TapHandler {
|
||||
id: tapHandler
|
||||
}
|
||||
|
||||
RectangularShadow {
|
||||
anchors.fill: container
|
||||
blur: 30
|
||||
color: Colors.mantle
|
||||
offset.x: 7
|
||||
offset.y: 3
|
||||
radius: container.radius
|
||||
spread: 10
|
||||
onTapped: {
|
||||
if (root.openedItem) {
|
||||
root.boxOpened = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
@@ -103,6 +207,12 @@ Item {
|
||||
color: root.boxColor
|
||||
radius: root.boxRadius
|
||||
|
||||
Behavior on radius {
|
||||
NumberAnimation {
|
||||
duration: 300
|
||||
}
|
||||
}
|
||||
|
||||
StackView {
|
||||
id: containerContent
|
||||
|
||||
|
||||
@@ -1,19 +1,62 @@
|
||||
import QtQuick
|
||||
import QtQuick.Shapes
|
||||
|
||||
Gradient {
|
||||
Item {
|
||||
id: root
|
||||
|
||||
required property color color
|
||||
property double midpoint: 0.6
|
||||
property real angle: 0 // Degrees; 0 = transparent left, colour right
|
||||
readonly property real angleRadians: angle * Math.PI / 180.0
|
||||
property real blur: 0.0 // 0 = sharp, 1 = very soft
|
||||
|
||||
// Public API
|
||||
property color color: "#4da3ffff"
|
||||
|
||||
// Half the diagonal ensures the gradient covers the entire item.
|
||||
readonly property real gradientRadius: Math.sqrt(width * width + height * height) / 2.0
|
||||
property real midpoint: 0.5 // 0..1
|
||||
property double radius: 10
|
||||
readonly property real safeBlur: Math.max(blur, 0.00001)
|
||||
readonly property real safeMidpoint: clamp01(midpoint)
|
||||
readonly property real transitionEnd: clamp01(safeMidpoint + safeBlur / 2.0)
|
||||
readonly property real transitionStart: clamp01(safeMidpoint - safeBlur / 2.0)
|
||||
readonly property color transparentColor: Qt.rgba(color.r, color.g, color.b, 0.0)
|
||||
|
||||
function clamp01(value) {
|
||||
return Math.max(0.0, Math.min(1.0, value));
|
||||
}
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: 100
|
||||
}
|
||||
}
|
||||
|
||||
Shape {
|
||||
anchors.fill: parent
|
||||
preferredRendererType: Shape.CurveRenderer
|
||||
|
||||
ShapePath {
|
||||
strokeWidth: -1
|
||||
|
||||
fillGradient: LinearGradient {
|
||||
x1: root.width / 2.0 - root.gradientRadius * Math.cos(root.angleRadians)
|
||||
x2: root.width / 2.0 + root.gradientRadius * Math.cos(root.angleRadians)
|
||||
y1: root.height / 2.0 - root.gradientRadius * Math.sin(root.angleRadians)
|
||||
y2: root.height / 2.0 + root.gradientRadius * Math.sin(root.angleRadians)
|
||||
|
||||
GradientStop {
|
||||
color: "transparent"
|
||||
color: root.transparentColor
|
||||
position: 0.0
|
||||
}
|
||||
|
||||
GradientStop {
|
||||
color: "transparent"
|
||||
position: root.midpoint
|
||||
color: root.transparentColor
|
||||
position: root.transitionStart
|
||||
}
|
||||
|
||||
GradientStop {
|
||||
color: root.color
|
||||
position: root.transitionEnd
|
||||
}
|
||||
|
||||
GradientStop {
|
||||
@@ -21,3 +64,12 @@ Gradient {
|
||||
position: 1.0
|
||||
}
|
||||
}
|
||||
|
||||
PathRectangle {
|
||||
height: root.height
|
||||
radius: Math.min(root.radius, Math.min(width, height) / 2.0)
|
||||
width: root.width
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,12 +2,18 @@ import QtQuick
|
||||
import "../Color.js" as Colors
|
||||
|
||||
Text {
|
||||
id: root
|
||||
|
||||
property double fontSize: 12
|
||||
property double fontWeight: 8
|
||||
property bool propo: false
|
||||
|
||||
color: Colors.base
|
||||
elide: Text.ElideRight
|
||||
|
||||
font {
|
||||
family: "FiraMono Nerd Font"
|
||||
pointSize: 12
|
||||
weight: 600
|
||||
family: root.propo == true ? "FiraMono Nerd Font Propo" : "FiraMono Nerd Font"
|
||||
pointSize: fontSize
|
||||
weight: root.fontWeight * 100
|
||||
}
|
||||
}
|
||||
|
||||
169
README.md
Normal file
169
README.md
Normal file
@@ -0,0 +1,169 @@
|
||||
# VoidBar
|
||||
|
||||
_Minimal, Verbose, Clean_
|
||||
|
||||
## Ethos
|
||||
|
||||
This bar is a retort to all of the "dynamic island" style bars that have been popular
|
||||
recently. I'm sure you know the ones, with a cramped, forced "minimalistic" aesthetic,
|
||||
that, even though marketed toward window manager users such as myself, usually end up
|
||||
being mouse driven spring animation slop.
|
||||
|
||||
Nobody wants that.
|
||||
|
||||
If you use something like Caelestia or Noctalia, sure, whatever. I'm not going to be some
|
||||
elitist and say they aren't without their uses. If you want a cohesive shell with theme
|
||||
switching and everything working perfectly out of the box, then they're good enough
|
||||
solutions for new users, or even experienced ones that just claim they don't want to waste
|
||||
their time writing from scratch.
|
||||
|
||||
However, everything outside of those shells is bullshit. Lately, all the shells I've seen
|
||||
have been "dynamic islands", bars that only show the time when dormant and have a shitload
|
||||
of features that you're never going to use effectively. They have no concept of workspace
|
||||
identifiers, instead opting for nondescript nerd icon blobs rather than the tried and true
|
||||
numbers that seem to always be more useful. Not that it matters, they're hidden anyway,
|
||||
until you change workspaces and they get shown for a second or two after the point that
|
||||
you actually needed to see them.
|
||||
|
||||
Then come the theme switchers, a sorry excuse for a feature that just ends up being a
|
||||
party trick at the end of the day. Changing wallpapers every so often I can get behind,
|
||||
but the whole theme? Really? The prophecy is borderline self fulfilling, you're going to
|
||||
pick a favorite eventually and end up never touching the switcher again, no matter how
|
||||
many variations of gruvbox you have configured. On top of that, are you really going to be
|
||||
switching wallpapers at runtime that often? I for one never even considered implementing a
|
||||
feature so distracting, opting instead to actually getting shit done, to the behest of
|
||||
everyone on r/unixporn.
|
||||
|
||||
The final gripe I have with these so called "minimal bars" is, ironically, the
|
||||
over-abundance of features. No, you don't need an android-style pull down menu to adjust
|
||||
your brightness, no, you don't need a button to change your screen scaling on the fly, and
|
||||
no, you don't need to be able to authenticate with your fucking desktop shell. Just use
|
||||
sudo like the rest of us. Many such features are jammed into bars like these, it's as if
|
||||
they need some standout feature in order to actually get used (oh wait). Bars shouldn't
|
||||
need a whole custom settings app. Bars shouldn't need tutorials for what keybinds to use
|
||||
to access some obscure feature. Bars shouldn't implement entire bluetooth management
|
||||
interfaces. Bars should show the time, what workspace you're on, and maybe, at a stretch,
|
||||
how much battery you have left. However, none of the geniuses over in the quickshell
|
||||
discussions tab can figure out how to put numbers where the workspace blobs are.
|
||||
|
||||
So, I had no choice to fix the problem.
|
||||
|
||||
### The problem
|
||||
|
||||
I like minimalistic apps, and minimalism in general. But there comes a point where being
|
||||
"minimal" either wraps around to being over the top and useless, or practically unusable.
|
||||
Theoretically, the most minimal bar is no bar at all, but that suffers from not existing
|
||||
by definition, so I did my best to come up with something that looks good, has utility,
|
||||
and, most of the time, stays out of your way when you don't care about what it says.
|
||||
|
||||
The first thing I did was to expand from a magical single pill to the more classical
|
||||
layout, a left, center and right section, ripped straight from waybar. If you only have a
|
||||
left or right section, then your screen looks lopsided, and if you only have a center
|
||||
section, then your bar either looks weirdly wide, or becomes just another useless capsule.
|
||||
As I have ordained, the left section is solely for the workspace switcher, the center is
|
||||
for the clock, and the right is for optional information, like bluetooth connection and
|
||||
battery level, which you wouldn't need on a desktop machine.
|
||||
|
||||
The second innovation I came up with (fr fr no cap) was removing useless information from
|
||||
the bar entirely. When wifi is disconnected, what good is it to have a "no wifi" icon
|
||||
taking up space on the screen? None at all, in fact. Instead, I have opted to hide unused
|
||||
modules entirely. For example, if no bluetooth devices are connected, instead of leaving a
|
||||
useless logo in the top right, the module slides up off of the screen, telling you that
|
||||
there is nothing connected simply through its absence. Plus, it looks better. That means
|
||||
that on a system with no battery, no wifi connection (ethernet hides the module), and no
|
||||
bluetooth devices connected, like my main pc most of the time, there are no needless icons
|
||||
cluttering the top right of the screen.
|
||||
|
||||
The third and final main feature that I implemented is monitor-exclusive modules. If you
|
||||
have multiple monitors, you most likely won't be looking at the second if you're doing
|
||||
work on the first. If that's the case, then why would you need a ticking clock on your
|
||||
second monitor? Why would you need an entire shell over there taking up space and being a
|
||||
potential distraction (yes I know I'm being melodramatic but stfu)? This was one of my
|
||||
main gripes with waybar, the fact that the same bar with the same information was
|
||||
plastered across all of your screens, regardless of whether the information was needed or
|
||||
not. A simple check can be done within an instance of the bar in quickshell, as to whether
|
||||
that instance of the bar is on the currently focused monitor. If it is? Display the
|
||||
modules. If not? Hide them away to keep the desktop clean. It's as simple as that.
|
||||
|
||||
No repeated information, no unnecesarry "disconnected" icons, and, most importantly, no
|
||||
useless features. Speaking of...
|
||||
|
||||
## Features
|
||||
|
||||
### Tl;Dr
|
||||
|
||||
- _Workspace switcher:_ Displaying only the workspaces on that monitor. When on a
|
||||
different monitor, highlights the focused workspace as to distinguish it from unfocused
|
||||
workspaces
|
||||
- _Clock:_ It's a clock. When a notification comes in, the clock is substituted for a
|
||||
notification popup. If music is playing (any playerctld compatible player), when the
|
||||
track changes a toast will appear, and when hovered over, the clock will be replaced
|
||||
with a music information screen.
|
||||
- _Network:_ Displays whether the machine is connected to wifi or not. Ethernet is assumed
|
||||
a permanent feature, and the module is thus hidden even though a network is connected.
|
||||
When clicked, shows the network name, local ip address, interface that is being used to
|
||||
connect, and whether the network has saved login information.
|
||||
- _Bluetooth:_ Displays whether there are any connected bluetooth devices. Does NOT
|
||||
function as a full bluetooth interface because I have bluetui for that. When clicked,
|
||||
displays connected devices, appropriate icons, and their mac addresses. The box
|
||||
containing the icon doubles as a battery indicator, and when the icon is hovered over it
|
||||
is replaced with a numerical percentage referring to the device's battery level
|
||||
- _Battery:_ Displays battery level, and state through coloring and icons. When unplugged,
|
||||
battery icon is reactive to every 10%. When plugged in but not recieving charge, the
|
||||
module is highlighted orange. when plugged in and recieving charge, the module is
|
||||
highlighted purple (the best color). When plugged in and full, the module is highlighted
|
||||
blue. Icons also change to reflect the battery state, using the `nf-md-battery` class of
|
||||
icons, as labeled on the nerd font website. When clicked on, The module shows the
|
||||
battery's UPower reference (eg `BAT0`), reported charge in Wh, change rate in watts for
|
||||
both charging and discharging, the time remaining until the battery is either fully
|
||||
charged or empty, and a numerical percentage alongside another vertical battery meter
|
||||
(same component as the bluetooth devices)
|
||||
|
||||
- Planned features (when I get around to it)
|
||||
- Logout menu (wlogout is still broken lol)
|
||||
- Notification menu/management
|
||||
- Lock screen
|
||||
- Volume control? (probably not, way-edges clears)
|
||||
- Interactible mpris player (skip and rewind)
|
||||
- Music indicator on the left of the clock when music is playing
|
||||
- Hover to show mpris thing when music is paused
|
||||
- Mpris firefox exclusion (I had to disable it in about:config lol)
|
||||
|
||||
### More rambling
|
||||
|
||||
If you read the above section, you will notice there is no mention of a theme switcher or
|
||||
launcher functionality. That is because I made a bar, not a theme switcher or a launcher.
|
||||
Other, more knowledgeable people have made those things already, and they are almost
|
||||
definately more feature rich and polished than anything you or I could make in qml. Once
|
||||
again, this bar has NO BULLSHIT!!! NONE!!!
|
||||
|
||||
One thing I found cool while making this bar is all the interesting information that most
|
||||
other bars will hide from you. Instead of what my cpu temperature is, I can see how fast
|
||||
my battery is charging down to 8 decimal places of precision. On top of this, because qml
|
||||
is just fancy javascript under the hood, you can do surprisingly complex data manipulation
|
||||
if needs be. Of course, you can pull out an old trusty `toFixed(2)` so that there aren't
|
||||
so many decimals, but beyond that, for example, the mpris api gives you track info in
|
||||
miliseconds, which means you have to make a utility function to convert from that format
|
||||
into a readable one, and then manipulate those values again, because they don't have
|
||||
leading zeros by default.
|
||||
|
||||
Because of how qml works, I worried less about how inefficient my polling code was, and
|
||||
how things were going to react with eachother, and update order, and instead focused on
|
||||
how I wanted the bar to act. I'll tell you a little secret, qml is a really really easy
|
||||
language to learn. That's how it's supposed to be, and why there's a quick in the name.
|
||||
Qml is to desktop apps what tailwind is to html, almost. You can throw together something
|
||||
that looks cohesive in a few minutes, and because of how state management works, adding
|
||||
reactivity like a hover animation is stupidly easy. Everything is predictable, and because
|
||||
you don't have to worry about how to update components, you can just throw in a few js
|
||||
functions here and there regardless of whether you need them. I have to tell you, having a
|
||||
bunch of heights and margins and then using a bunch of maths in a function to dynamically
|
||||
calculate the height of a window or a textbox is the coolest shit ever.
|
||||
|
||||
I will actually use this bar on my main machine. There might be some weird shit going on
|
||||
with the battery module, because the only machine I had while making this was a laptop,
|
||||
but once I get home everything will be resolved. This was not made to post on reddit. This
|
||||
wasn't made on a whim. I hate the state of places like r/unixporn, where, for every decent
|
||||
and original rice, there are 300 copycats or people just running a prefab shell with a
|
||||
custom wallpaper. And don't even get me going on the damn islands again. If I can make
|
||||
something like this in a few days, then so can you. Go make something, and thanks for
|
||||
reading <3
|
||||
162
Services/BatteryManager.qml
Normal file
162
Services/BatteryManager.qml
Normal file
@@ -0,0 +1,162 @@
|
||||
pragma Singleton
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Services.UPower
|
||||
|
||||
import "../Color.js" as Colors
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
property double batteryPercentage: root.mainBattery.percentage || 0
|
||||
property bool batteryPresent: root.mainBattery.ready || false
|
||||
property UPowerDevice mainBattery: UPower.displayDevice || undefined
|
||||
property bool hasLaptopBattery: root.mainBattery && root.mainBattery.isLaptopBattery
|
||||
|
||||
|
||||
|
||||
function getBatteryColor() {
|
||||
let criticalLevel = 20;
|
||||
let batteryPercentage = getBatteryPercentage(mainBattery);
|
||||
|
||||
if (mainBattery.state == UPowerDeviceState.FullyCharged) {
|
||||
return Colors.sky;
|
||||
} else if (mainBattery.state == UPowerDeviceState.Charging) {
|
||||
return Colors.mauve;
|
||||
} else if (mainBattery.state == UPowerDeviceState.PendingCharge | mainBattery.state
|
||||
== UPowerDeviceState.PendingDischarge) {
|
||||
return Colors.peach;
|
||||
} else if (batteryPercentage <= criticalLevel) {
|
||||
return Colors.red;
|
||||
} else {
|
||||
return Colors.green;
|
||||
}
|
||||
}
|
||||
|
||||
function getBatteryIcon(battery) {
|
||||
if (battery.state == UPowerDeviceState.Charging) {
|
||||
return "";
|
||||
} else if (battery.state == UPowerDeviceState.PendingCharge | battery.state
|
||||
== UPowerDeviceState.PendingDischarge) {
|
||||
return "";
|
||||
} else if (battery.state == UPowerDeviceState.Discharging | battery.state
|
||||
== UPowerDeviceState.FullyCharged) {
|
||||
let iconArr = ["", "", "", "", "", "", "", "", "", ""];
|
||||
let batteryCharge = battery.percentage;
|
||||
let chargeIndex = Math.ceil(batteryCharge * iconArr.length) - 1;
|
||||
return iconArr[chargeIndex];
|
||||
}
|
||||
}
|
||||
|
||||
function getBatteryName(battery) {
|
||||
let pathsplit = battery.nativePath.split("/");
|
||||
let batteryName = pathsplit[pathsplit.length];
|
||||
return pathsplit;
|
||||
}
|
||||
|
||||
function getBatteryPercentage(battery) {
|
||||
return battery.percentage * 100;
|
||||
}
|
||||
|
||||
function getBatteryText() {
|
||||
let icon = getBatteryIcon(mainBattery);
|
||||
let percentage = getBatteryPercentage(mainBattery);
|
||||
return icon + " " + percentage + "%";
|
||||
}
|
||||
|
||||
function getChargingRateFormat() {
|
||||
let rate = mainBattery.changeRate.toFixed(1);
|
||||
|
||||
let finalString = "harging at " + rate + "W";
|
||||
if (rate == 0) {
|
||||
finalString = "Battery Idle";
|
||||
} else if (rate <= 0 || mainBattery.state == UPowerDeviceState.Charging) {
|
||||
finalString = "C" + finalString;
|
||||
} else {
|
||||
finalString = "Disc" + finalString;
|
||||
}
|
||||
return finalString;
|
||||
}
|
||||
|
||||
function getMainBatteryInfo() {
|
||||
let battery = UPower.devices.values[0];
|
||||
let batteryState = battery.state;
|
||||
let batteryName = getBatteryName(battery);
|
||||
let time = getTimeFormat();
|
||||
return {
|
||||
"name": batteryName,
|
||||
"charge": battery.percentage,
|
||||
"energy": battery.energy.toFixed(1),
|
||||
"capacity": battery.energyCapacity.toFixed(1),
|
||||
"health": battery.healthPercentage
|
||||
};
|
||||
}
|
||||
|
||||
function getTimeFormat() {
|
||||
let state = mainBattery.state;
|
||||
let finalString = "";
|
||||
if (state == UPowerDeviceState.FullyCharged) {
|
||||
finalString += "Battery Full";
|
||||
} else if (state == UPowerDeviceState.Charging) {
|
||||
let time = mainBattery.timeToFull;
|
||||
if (time == 0) {
|
||||
return "Getting Time...";
|
||||
} else {
|
||||
let timeArr = secondsToTime(time);
|
||||
// If more than 1 hour
|
||||
if (timeArr.hours >= 1 || timeArr.hourNeedsIncrement == 1) {
|
||||
finalString += timeArr.Hours + " Hours ";
|
||||
// If minutes not 0
|
||||
if (timeArr.minutesRounded > 1) {
|
||||
finalString += timeArr.minutesRounded + " Minutes";
|
||||
}
|
||||
} else {
|
||||
finalString += timeArr.minutes + " Minutes ";
|
||||
finalString += timeArr.seconds + " Seconds";
|
||||
}
|
||||
}
|
||||
finalString += " Until Full";
|
||||
} else if (state == UPowerDeviceState.Discharging) {
|
||||
let time = mainBattery.timeToEmpty;
|
||||
if (time == 0) {
|
||||
return "Getting Time...";
|
||||
} else {
|
||||
let timeArr = secondsToTime(time);
|
||||
// If more than 1 hour
|
||||
if (timeArr.hours >= 1 || timeArr.hourNeedsIncrement == 1) {
|
||||
finalString += timeArr.hours + " Hours ";
|
||||
// If minutes not 0
|
||||
if (timeArr.minutesRounded > 1) {
|
||||
finalString += timeArr.minutesRounded + " Minutes";
|
||||
}
|
||||
} else {
|
||||
finalString += timeArr.minutes + " Minutes ";
|
||||
finalString += timeArr.seconds + " Seconds";
|
||||
}
|
||||
}
|
||||
finalString += " Remaining";
|
||||
} else {
|
||||
finalString += "Getting Time...";
|
||||
}
|
||||
return finalString;
|
||||
}
|
||||
|
||||
function secondsToTime(totalSeconds) {
|
||||
totalSeconds = Math.max(0, Number(totalSeconds));
|
||||
|
||||
const hours = Math.floor(totalSeconds / 3600);
|
||||
const minutes = Math.floor((totalSeconds % 3600) / 60);
|
||||
const seconds = Math.floor(totalSeconds % 60);
|
||||
|
||||
const roundedMinutes = minutes + (seconds >= 30 ? 1 : 0);
|
||||
const hourNeedsIncrement = roundedMinutes === 60;
|
||||
|
||||
return {
|
||||
hours,
|
||||
minutes,
|
||||
seconds,
|
||||
minutesRounded: hourNeedsIncrement ? 0 : roundedMinutes,
|
||||
hourNeedsIncrement
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -6,21 +6,59 @@ import Quickshell.Bluetooth
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
property BluetoothAdapter defaultAdapter: Bluetooth.defaultAdapter
|
||||
property string defaultAdapterName: Bluetooth.defaultAdapter.adapterId
|
||||
property BluetoothAdapter defaultAdapter: Bluetooth.defaultAdapter || null
|
||||
property string defaultAdapterName: defaultAdapter ? defaultAdapter.adapterId : ""
|
||||
|
||||
property bool anyConnected: root.defaultAdapter != null
|
||||
&& root.defaultAdapter.devices.values.some(device => device.connected)
|
||||
|
||||
function getConnected() {
|
||||
let deviceList = getDevicesList();
|
||||
for (let device in deviceList) {
|
||||
let currentDevice = deviceList[device];
|
||||
if (currentDevice.connected == true) {
|
||||
return true;
|
||||
return root.anyConnected;
|
||||
}
|
||||
|
||||
function getConnectedDevicesList() {
|
||||
let devicesList = getDevicesList();
|
||||
if (!devicesList) {
|
||||
return [];
|
||||
}
|
||||
let connectedList = [];
|
||||
for (let device of devicesList) {
|
||||
if (device.connected == true) {
|
||||
connectedList.push(device);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
return connectedList;
|
||||
}
|
||||
|
||||
function getDeviceText(device) {
|
||||
if (!device) {
|
||||
return undefined;
|
||||
}
|
||||
let deviceName = device.deviceName;
|
||||
let deviceIcon = getIcon(device.icon);
|
||||
let deviceMac = device.address;
|
||||
let hasBattery = device.batteryAvailable;
|
||||
let deviceBattery = deviceIcon;
|
||||
let deviceBatteryRaw = 0;
|
||||
if (hasBattery) {
|
||||
deviceBattery = device.battery * 100;
|
||||
deviceBatteryRaw = device.battery;
|
||||
}
|
||||
|
||||
return {
|
||||
"icon": deviceIcon,
|
||||
"name": deviceName,
|
||||
"mac": deviceMac,
|
||||
"hasBattery": hasBattery,
|
||||
"battery": deviceBattery,
|
||||
"batteryRaw": deviceBatteryRaw
|
||||
};
|
||||
}
|
||||
|
||||
function getDevicesList() {
|
||||
if (!root.defaultAdapter) {
|
||||
return [];
|
||||
}
|
||||
return root.defaultAdapter.devices.values;
|
||||
}
|
||||
|
||||
@@ -41,29 +79,4 @@ Singleton {
|
||||
|
||||
return icons[name] || "";
|
||||
}
|
||||
|
||||
function pairTrustConnect(device) {
|
||||
device.pair();
|
||||
device.trusted = true;
|
||||
device.connect();
|
||||
}
|
||||
|
||||
function toggleDiscover() {
|
||||
if (defaultAdapter.discovering == true) {
|
||||
defaultAdapter.discovering = false;
|
||||
} else {
|
||||
defaultAdapter.discovering = true;
|
||||
discoveringTimeout.running = true;
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: discoveringTimeout
|
||||
|
||||
interval: 15000
|
||||
repeat: false
|
||||
running: false
|
||||
|
||||
onTriggered: defaultAdapter.discovering = false
|
||||
}
|
||||
}
|
||||
|
||||
31
Services/IpcManager.qml
Normal file
31
Services/IpcManager.qml
Normal file
@@ -0,0 +1,31 @@
|
||||
pragma Singleton
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
|
||||
import Quickshell.Wayland
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
property var focusState: WlrKeyboardFocus.None
|
||||
property bool logoutMenuOpen: false
|
||||
|
||||
signal closeLogoutMenu
|
||||
signal logoutMenu
|
||||
|
||||
onCloseLogoutMenu: {
|
||||
root.focusState = WlrKeyboardFocus.None;
|
||||
root.logoutMenuOpen = false;
|
||||
}
|
||||
|
||||
IpcHandler {
|
||||
function openLogoutMenu(): void {
|
||||
root.logoutMenu();
|
||||
root.focusState = WlrKeyboardFocus.Exclusive;
|
||||
root.logoutMenuOpen = true;
|
||||
}
|
||||
|
||||
target: "root"
|
||||
}
|
||||
}
|
||||
@@ -6,8 +6,7 @@ import Quickshell.Services.Mpris
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
property MprisPlayer defaultPlayer: Mpris.players.values[0]
|
||||
property real pos: defaultPlayer.position / defaultPlayer.length
|
||||
property MprisPlayer defaultPlayer: Mpris.players.values[0] || null
|
||||
|
||||
function getPlaying() {
|
||||
if (!defaultPlayer) {
|
||||
@@ -17,6 +16,55 @@ Singleton {
|
||||
}
|
||||
}
|
||||
|
||||
function getTimeString() {
|
||||
let trackInfo = getTrackInfo();
|
||||
let position = trackInfo["position"];
|
||||
let length = trackInfo["length"];
|
||||
if (length.hours > 0) {
|
||||
return position.hours + ":" + position.minutes + ":" + position.seconds + "/" + length.hours
|
||||
+ ":" + length.minutes + ":" + length.seconds;
|
||||
} else {
|
||||
return position.minutes + ":" + position.seconds + "/" + length.minutes + ":" + length.seconds;
|
||||
}
|
||||
}
|
||||
|
||||
function getTrackInfo() {
|
||||
let trackName = root.defaultPlayer.trackTitle;
|
||||
let trackArtist = root.defaultPlayer.trackArtist;
|
||||
let trackAlbum = root.defaultPlayer.trackAlbum;
|
||||
let trackPosition = root.defaultPlayer.position;
|
||||
let trackLength = root.defaultPlayer.length;
|
||||
let trackArt = root.defaultPlayer.trackArtUrl;
|
||||
return {
|
||||
"name": trackName,
|
||||
"artist": trackArtist,
|
||||
"album": trackAlbum,
|
||||
"position": msToArray(trackPosition),
|
||||
"length": msToArray(trackLength),
|
||||
"lengthRemaining": msToArray(trackLength - trackPosition),
|
||||
"lengthPercent": trackPosition / trackLength,
|
||||
"albumArt": trackArt
|
||||
};
|
||||
}
|
||||
|
||||
function msToArray(time) {
|
||||
time = Math.max(0, Number(time));
|
||||
|
||||
const inthours = Math.floor(time / 3600000);
|
||||
const intminutes = Math.floor((time % 60000) / 60);
|
||||
const intseconds = Math.floor((time % 1000) % 60);
|
||||
|
||||
const hours = String(inthours).padStart(2, "0");
|
||||
const minutes = String(intminutes).padStart(2, "0");
|
||||
const seconds = String(intseconds).padStart(2, "0");
|
||||
|
||||
return {
|
||||
hours,
|
||||
minutes,
|
||||
seconds
|
||||
};
|
||||
}
|
||||
|
||||
function prev() {
|
||||
defaultPlayer.previous();
|
||||
}
|
||||
|
||||
122
Services/NetworkManager.qml
Normal file
122
Services/NetworkManager.qml
Normal file
@@ -0,0 +1,122 @@
|
||||
pragma Singleton
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import Quickshell.Networking
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
property bool connectedWifi: root.defaultAdapter != null && root.defaultAdapter.connected
|
||||
&& root.defaultAdapter.type == DeviceType.Wifi
|
||||
property NetworkDevice defaultAdapter: Networking.devices.values[0] || null
|
||||
property string ipAddress: "0.0.0.0"
|
||||
|
||||
function getConnectedNetworks(networksArr) {
|
||||
let connectedNetworks = [];
|
||||
|
||||
for (let network of networksArr) {
|
||||
if (network.state == ConnectionState.Connected) {
|
||||
connectedNetworks.push(network);
|
||||
}
|
||||
}
|
||||
return connectedNetworks;
|
||||
}
|
||||
|
||||
function getNetworkDetails(adapter) {
|
||||
if (!adapter) {
|
||||
return {
|
||||
"adapterName": "",
|
||||
"adapterConnected": false,
|
||||
"adapterIsWifi": false,
|
||||
"networkName": "",
|
||||
"networkStrength": 0,
|
||||
"icon": ""
|
||||
};
|
||||
}
|
||||
|
||||
let networksArr = adapter.networks.values;
|
||||
let connectedNetworks = getConnectedNetworks(networksArr);
|
||||
let deviceWifi = isDeviceWifi(adapter);
|
||||
|
||||
let primaryNetwork = connectedNetworks[0];
|
||||
if (!primaryNetwork) {
|
||||
return {
|
||||
"adapterName": adapter.name,
|
||||
"adapterConnected": adapter.connected,
|
||||
"adapterIsWifi": deviceWifi,
|
||||
"networkName": "No Network",
|
||||
"networkStrength": 0,
|
||||
"icon": ""
|
||||
};
|
||||
}
|
||||
|
||||
let networkName = primaryNetwork.name;
|
||||
let networkStrength = primaryNetwork.signalStrength;
|
||||
|
||||
let networkIcon = getWifiIcon(networkStrength);
|
||||
|
||||
let outputDict = {
|
||||
"adapterName": adapter.name,
|
||||
"adapterConnected": adapter.connected,
|
||||
"adapterIsWifi": deviceWifi,
|
||||
"networkName": networkName,
|
||||
"networkStrength": networkStrength,
|
||||
"icon": networkIcon,
|
||||
"saved": primaryNetwork.known,
|
||||
"ipAddress": root.ipAddress
|
||||
};
|
||||
return outputDict;
|
||||
}
|
||||
|
||||
function getWifiIcon(strength) {
|
||||
let iconArr = ["", "", "", ""];
|
||||
let strengthIndex = Math.ceil(strength * iconArr.length);
|
||||
return iconArr[strengthIndex - 1];
|
||||
}
|
||||
|
||||
function getWifiText() {
|
||||
let info = getNetworkDetails(defaultAdapter);
|
||||
|
||||
let adapter = info["adapterName"];
|
||||
let name = info["networkName"];
|
||||
|
||||
let finalString = adapter + " - " + name;
|
||||
return finalString;
|
||||
}
|
||||
|
||||
function isConnectedAndWifi() {
|
||||
return root.connectedWifi;
|
||||
}
|
||||
|
||||
function isDeviceWifi(adapter) {
|
||||
if (adapter.type == DeviceType.Wifi) {
|
||||
return true;
|
||||
} else if (adapter.type == DeviceType.Wired) {
|
||||
return false;
|
||||
} else {
|
||||
console.log(adapter.type);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function refreshIpAddress() {
|
||||
ipProcess.running = true;
|
||||
}
|
||||
|
||||
onDefaultAdapterChanged: {
|
||||
refreshIpAddress();
|
||||
}
|
||||
|
||||
Process {
|
||||
id: ipProcess
|
||||
|
||||
command: ["sh", "-c", "ip -4 -o addr show dev " + root.defaultAdapter.name
|
||||
+ " scope global | awk '{sub(\"/.*\", \"\", $4); printf $4}'"]
|
||||
running: true
|
||||
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: root.ipAddress = this.text
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,12 +2,15 @@ pragma Singleton
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Services.Notifications
|
||||
import "../Color.js" as Colors
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
property alias notif: server
|
||||
|
||||
signal newNotification(notificiation: Notification)
|
||||
|
||||
function getLatestNotification() {
|
||||
let notificationList = server.trackedNotifications.values;
|
||||
let len = notificationList.length;
|
||||
@@ -24,9 +27,11 @@ Singleton {
|
||||
actionsSupported: true
|
||||
bodyMarkupSupported: true
|
||||
bodySupported: true
|
||||
imageSupported: true
|
||||
|
||||
onNotification: notification => {
|
||||
notification.tracked = true;
|
||||
root.newNotification(notification);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,6 +62,17 @@ Singleton {
|
||||
}
|
||||
}
|
||||
|
||||
function getWorkspaceStateIndex(workspace) {
|
||||
let state = getWorkspaceState(workspace);
|
||||
if (state == "focused") {
|
||||
return 0;
|
||||
} else if (state == "active") {
|
||||
return 1;
|
||||
} else if (state == "inactive") {
|
||||
return 2;
|
||||
}
|
||||
}
|
||||
|
||||
function getWorkspacesForMonitor(monitor) {
|
||||
let allWorkspaces = getAllNumberedWorkspaces();
|
||||
let monitorWorkspaces = [];
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
module Services
|
||||
singleton Time 1.0 Time.qml
|
||||
singleton TimeManager 1.0 TimeManager.qml
|
||||
singleton NotificationManager 1.0 NotificationManager.qml
|
||||
singleton WorkspaceManager 1.0 WorkspaceManager.qml
|
||||
singleton BluetoothManager 1.0 BluetoothManager.qml
|
||||
singleton MprisManager 1.0 MprisManager.qml
|
||||
singleton BatteryManager 1.0 BatteryManager.qml
|
||||
singleton NetworkManager 1.0 NetworkManager.qml
|
||||
singleton IpcManager 1.0 IpcManager.qml
|
||||
|
||||
Reference in New Issue
Block a user