wip
This commit is contained in:
parent
32edcff102
commit
0296117901
110 changed files with 9713 additions and 5 deletions
8
README.bak.md
Normal file
8
README.bak.md
Normal file
|
@ -0,0 +1,8 @@
|
|||
# quickshell
|
||||
|
||||
## **current modules**
|
||||
|
||||
#### [activate-linux](activate-linux.qml)
|
||||
|
||||
Puts in a small windows like 'Activate Linux' watermark on the bottom right
|
||||
screen
|
64
README.md
64
README.md
|
@ -1,8 +1,62 @@
|
|||
# quickshell
|
||||
<h1 align=center>caelestia-shell</h1>
|
||||
|
||||
## **current modules**
|
||||
<div align=center>
|
||||
|
||||
#### [activate-linux](activate-linux.qml)
|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
Puts in a small windows like 'Activate Linux' watermark on the bottom right
|
||||
screen
|
||||
</div>
|
||||
|
||||
https://github.com/user-attachments/assets/0840f496-575c-4ca6-83a8-87bb01a85c5f
|
||||
|
||||
## Components
|
||||
|
||||
- Widgets: [`Quickshell`](https://quickshell.outfoxxed.me)
|
||||
- Window manager: [`Hyprland`](https://hyprland.org)
|
||||
- Dots: [`caelestia`](https://github.com/caelestia-dots)
|
||||
|
||||
## Installation
|
||||
|
||||
### Automated installation (recommended)
|
||||
|
||||
Install [`caelestia-scripts`](https://github.com/caelestia-dots/scripts) and run `caelestia install shell`.
|
||||
|
||||
### Manual installation
|
||||
|
||||
Install all [dependencies](https://github.com/caelestia-dots/scripts/blob/main/install/shell.fish#L10), then
|
||||
clone this repo into `$XDG_CONFIG_HOME/quickshell/caelestia` and run `qs -c caelestia`.
|
||||
|
||||
## Usage
|
||||
|
||||
All keybinds are accessible via Hyprland [global shortcuts](https://wiki.hyprland.org/Configuring/Binds/#dbus-global-shortcuts).
|
||||
For a preconfigured setup, install [`caelestia-hypr`](https://github.com/caelestia-dots/hypr) via `caelestia install hypr` or see
|
||||
[this file](https://github.com/caelestia-dots/hypr/blob/main/hyprland/keybinds.conf#L1-L29) for an example on how to use global
|
||||
shortcuts.
|
||||
|
||||
There is only one IPC command as of now which can be used to get details about the currently active MPRIS player.
|
||||
```sh
|
||||
caelestia shell mpris getActive <prop>
|
||||
```
|
||||
|
||||
## Credits
|
||||
|
||||
Thanks to the Hyprland discord community (especially the homies in #rice-discussion) for all the help and suggestions
|
||||
for improving these dots!
|
||||
|
||||
A special thanks to [@outfoxxed](https://github.com/outfoxxed) for making Quickshell and the effort put into fixing issues
|
||||
and implementing various feature requests.
|
||||
|
||||
Another special thanks to [@end_4](https://github.com/end-4) for his [config](https://github.com/end-4/dots-hyprland)
|
||||
which helped me a lot with learning how to use Quickshell.
|
||||
|
||||
## Stonks 📈
|
||||
|
||||
<a href="https://www.star-history.com/#caelestia-dots/shell&Date">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=caelestia-dots/shell&type=Date&theme=dark" />
|
||||
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/svg?repos=caelestia-dots/shell&type=Date" />
|
||||
<img alt="Star History Chart" src="https://api.star-history.com/svg?repos=caelestia-dots/shell&type=Date" />
|
||||
</picture>
|
||||
</a>
|
||||
|
|
BIN
assets/bongocat.gif
Normal file
BIN
assets/bongocat.gif
Normal file
Binary file not shown.
After Width: | Height: | Size: 8.3 KiB |
BIN
assets/kurukuru.gif
Normal file
BIN
assets/kurukuru.gif
Normal file
Binary file not shown.
After Width: | Height: | Size: 97 KiB |
73
assets/realtime-beat-detector.py
Executable file
73
assets/realtime-beat-detector.py
Executable file
|
@ -0,0 +1,73 @@
|
|||
#!/bin/python
|
||||
|
||||
import pyaudio
|
||||
import numpy as np
|
||||
import aubio
|
||||
import signal
|
||||
import sys
|
||||
|
||||
from typing import List, Tuple
|
||||
|
||||
|
||||
class BeatDetector:
|
||||
def __init__(self, buf_size: int):
|
||||
self.buf_size: int = buf_size
|
||||
|
||||
# Set up pyaudio and aubio beat detector
|
||||
self.audio: pyaudio.PyAudio = pyaudio.PyAudio()
|
||||
samplerate: int = 44100
|
||||
|
||||
self.stream: pyaudio.Stream = self.audio.open(
|
||||
format=pyaudio.paFloat32,
|
||||
channels=1,
|
||||
rate=samplerate,
|
||||
input=True,
|
||||
frames_per_buffer=self.buf_size,
|
||||
stream_callback=self._pyaudio_callback
|
||||
)
|
||||
|
||||
fft_size: int = self.buf_size * 2
|
||||
|
||||
# tempo detection
|
||||
self.tempo: aubio.tempo = aubio.tempo("default", fft_size, self.buf_size, samplerate)
|
||||
|
||||
# this one is called every time enough audio data (buf_size) has been read by the stream
|
||||
def _pyaudio_callback(self, in_data, frame_count, time_info, status):
|
||||
# Interpret a buffer as a 1-dimensional array (aubio do not work with raw data)
|
||||
audio_data = np.frombuffer(in_data, dtype=np.float32)
|
||||
# true if beat present
|
||||
beat = self.tempo(audio_data)
|
||||
|
||||
# if beat detected, calculate BPM and send to OSC
|
||||
if beat[0]:
|
||||
print(self.tempo.get_bpm(), flush=True)
|
||||
|
||||
return None, pyaudio.paContinue # Tell pyAudio to continue
|
||||
|
||||
def __del__(self):
|
||||
self.stream.close()
|
||||
self.audio.terminate()
|
||||
print('--- Stopped ---')
|
||||
|
||||
|
||||
# main
|
||||
def main():
|
||||
bd = BeatDetector(512)
|
||||
|
||||
# capture ctrl+c to stop gracefully process
|
||||
def signal_handler(none, frame):
|
||||
bd.stream.stop_stream()
|
||||
bd.stream.close()
|
||||
bd.audio.terminate()
|
||||
print(' ===> Ctrl + C')
|
||||
sys.exit(0)
|
||||
|
||||
signal.signal(signal.SIGINT, signal_handler)
|
||||
|
||||
# Audio processing happens in separate thread, so put this thread to sleep
|
||||
signal.pause()
|
||||
|
||||
|
||||
# main run
|
||||
if __name__ == "__main__":
|
||||
main()
|
84
config/Appearance.qml
Normal file
84
config/Appearance.qml
Normal file
|
@ -0,0 +1,84 @@
|
|||
pragma Singleton
|
||||
|
||||
import Quickshell
|
||||
import QtQuick
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
readonly property Rounding rounding: Rounding {}
|
||||
readonly property Spacing spacing: Spacing {}
|
||||
readonly property Padding padding: Padding {}
|
||||
readonly property Font font: Font {}
|
||||
readonly property Anim anim: Anim {}
|
||||
|
||||
component Rounding: QtObject {
|
||||
readonly property int small: 12
|
||||
readonly property int normal: 17
|
||||
readonly property int large: 25
|
||||
readonly property int full: 1000
|
||||
}
|
||||
|
||||
component Spacing: QtObject {
|
||||
readonly property int small: 7
|
||||
readonly property int smaller: 10
|
||||
readonly property int normal: 12
|
||||
readonly property int larger: 15
|
||||
readonly property int large: 20
|
||||
}
|
||||
|
||||
component Padding: QtObject {
|
||||
readonly property int small: 5
|
||||
readonly property int smaller: 7
|
||||
readonly property int normal: 10
|
||||
readonly property int larger: 12
|
||||
readonly property int large: 15
|
||||
}
|
||||
|
||||
component FontFamily: QtObject {
|
||||
readonly property string sans: "IBM Plex Sans"
|
||||
readonly property string mono: "JetBrains Mono NF"
|
||||
readonly property string material: "Material Symbols Rounded"
|
||||
}
|
||||
|
||||
component FontSize: QtObject {
|
||||
readonly property int small: 11
|
||||
readonly property int smaller: 12
|
||||
readonly property int normal: 13
|
||||
readonly property int larger: 15
|
||||
readonly property int large: 18
|
||||
readonly property int extraLarge: 28
|
||||
}
|
||||
|
||||
component Font: QtObject {
|
||||
readonly property FontFamily family: FontFamily {}
|
||||
readonly property FontSize size: FontSize {}
|
||||
}
|
||||
|
||||
component AnimCurves: QtObject {
|
||||
readonly property list<real> emphasized: [0.05, 0, 2 / 15, 0.06, 1 / 6, 0.4, 5 / 24, 0.82, 0.25, 1, 1, 1]
|
||||
readonly property list<real> emphasizedAccel: [0.3, 0, 0.8, 0.15, 1, 1]
|
||||
readonly property list<real> emphasizedDecel: [0.05, 0.7, 0.1, 1, 1, 1]
|
||||
readonly property list<real> standard: [0.2, 0, 0, 1, 1, 1]
|
||||
readonly property list<real> standardAccel: [0.3, 0, 1, 1, 1, 1]
|
||||
readonly property list<real> standardDecel: [0, 0, 0, 1, 1, 1]
|
||||
readonly property list<real> expressiveFastSpatial: [0.42, 1.67, 0.21, 0.9, 1, 1]
|
||||
readonly property list<real> expressiveDefaultSpatial: [0.38, 1.21, 0.22, 1, 1, 1]
|
||||
readonly property list<real> expressiveEffects: [0.34, 0.8, 0.34, 1, 1, 1]
|
||||
}
|
||||
|
||||
component AnimDurations: QtObject {
|
||||
readonly property int small: 200
|
||||
readonly property int normal: 400
|
||||
readonly property int large: 600
|
||||
readonly property int extraLarge: 1000
|
||||
readonly property int expressiveFastSpatial: 350
|
||||
readonly property int expressiveDefaultSpatial: 500
|
||||
readonly property int expressiveEffects: 200
|
||||
}
|
||||
|
||||
component Anim: QtObject {
|
||||
readonly property AnimCurves curves: AnimCurves {}
|
||||
readonly property AnimDurations durations: AnimDurations {}
|
||||
}
|
||||
}
|
30
config/BarConfig.qml
Normal file
30
config/BarConfig.qml
Normal file
|
@ -0,0 +1,30 @@
|
|||
pragma Singleton
|
||||
|
||||
import Quickshell
|
||||
import QtQuick
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
readonly property Sizes sizes: Sizes {}
|
||||
readonly property Workspaces workspaces: Workspaces {}
|
||||
|
||||
component Sizes: QtObject {
|
||||
property int innerHeight: 30
|
||||
property int windowPreviewSize: 400
|
||||
property int trayMenuWidth: 300
|
||||
property int batteryWidth: 200
|
||||
}
|
||||
|
||||
component Workspaces: QtObject {
|
||||
property int shown: 5
|
||||
property bool rounded: true
|
||||
property bool activeIndicator: true
|
||||
property bool occupiedBg: false
|
||||
property bool showWindows: true
|
||||
property bool activeTrail: false
|
||||
property string label: " "
|
||||
property string occupiedLabel: " "
|
||||
property string activeLabel: " "
|
||||
}
|
||||
}
|
13
config/BorderConfig.qml
Normal file
13
config/BorderConfig.qml
Normal file
|
@ -0,0 +1,13 @@
|
|||
pragma Singleton
|
||||
|
||||
import "root:/services"
|
||||
import Quickshell
|
||||
import QtQuick
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
readonly property color colour: Colours.palette.m3surface
|
||||
readonly property int thickness: Appearance.padding.normal
|
||||
readonly property int rounding: Appearance.rounding.large
|
||||
}
|
26
config/DashboardConfig.qml
Normal file
26
config/DashboardConfig.qml
Normal file
|
@ -0,0 +1,26 @@
|
|||
pragma Singleton
|
||||
|
||||
import Quickshell
|
||||
import QtQuick
|
||||
|
||||
Singleton {
|
||||
readonly property int mediaUpdateInterval: 500
|
||||
readonly property int visualiserBars: 45
|
||||
readonly property Sizes sizes: Sizes {}
|
||||
|
||||
component Sizes: QtObject {
|
||||
readonly property int tabIndicatorHeight: 3
|
||||
readonly property int tabIndicatorSpacing: 5
|
||||
readonly property int infoWidth: 200
|
||||
readonly property int infoIconSize: 25
|
||||
readonly property int dateTimeWidth: 110
|
||||
readonly property int mediaWidth: 200
|
||||
readonly property int mediaProgressSweep: 180
|
||||
readonly property int mediaProgressThickness: 8
|
||||
readonly property int resourceProgessThickness: 10
|
||||
readonly property int weatherWidth: 250
|
||||
readonly property int mediaCoverArtSize: 150
|
||||
readonly property int mediaVisualiserSize: 80
|
||||
readonly property int resourceSize: 200
|
||||
}
|
||||
}
|
18
config/LauncherConfig.qml
Normal file
18
config/LauncherConfig.qml
Normal file
|
@ -0,0 +1,18 @@
|
|||
pragma Singleton
|
||||
|
||||
import Quickshell
|
||||
import QtQuick
|
||||
|
||||
Singleton {
|
||||
readonly property int maxShown: 8
|
||||
readonly property int maxWallpapers: 9 // Warning: even numbers look bad
|
||||
readonly property string actionPrefix: ">"
|
||||
readonly property Sizes sizes: Sizes {}
|
||||
|
||||
component Sizes: QtObject {
|
||||
readonly property int itemWidth: 600
|
||||
readonly property int itemHeight: 57
|
||||
readonly property int wallpaperWidth: 280
|
||||
readonly property int wallpaperHeight: 200
|
||||
}
|
||||
}
|
19
config/NotifsConfig.qml
Normal file
19
config/NotifsConfig.qml
Normal file
|
@ -0,0 +1,19 @@
|
|||
pragma Singleton
|
||||
|
||||
import Quickshell
|
||||
import QtQuick
|
||||
|
||||
Singleton {
|
||||
readonly property bool expire: false
|
||||
readonly property int defaultExpireTimeout: 3000
|
||||
readonly property real clearThreshold: 0.3
|
||||
readonly property int expandThreshold: 20
|
||||
readonly property bool actionOnClick: false
|
||||
readonly property Sizes sizes: Sizes {}
|
||||
|
||||
component Sizes: QtObject {
|
||||
readonly property int width: 400
|
||||
readonly property int image: 41
|
||||
readonly property int badge: 20
|
||||
}
|
||||
}
|
14
config/OsdConfig.qml
Normal file
14
config/OsdConfig.qml
Normal file
|
@ -0,0 +1,14 @@
|
|||
pragma Singleton
|
||||
|
||||
import Quickshell
|
||||
import QtQuick
|
||||
|
||||
Singleton {
|
||||
readonly property int hideDelay: 2000
|
||||
readonly property Sizes sizes: Sizes {}
|
||||
|
||||
component Sizes: QtObject {
|
||||
readonly property int sliderWidth: 30
|
||||
readonly property int sliderHeight: 150
|
||||
}
|
||||
}
|
13
config/SessionConfig.qml
Normal file
13
config/SessionConfig.qml
Normal file
|
@ -0,0 +1,13 @@
|
|||
pragma Singleton
|
||||
|
||||
import Quickshell
|
||||
import QtQuick
|
||||
|
||||
Singleton {
|
||||
readonly property int dragThreshold: 30
|
||||
readonly property Sizes sizes: Sizes {}
|
||||
|
||||
component Sizes: QtObject {
|
||||
readonly property int button: 80
|
||||
}
|
||||
}
|
37
modules/Shortcuts.qml
Normal file
37
modules/Shortcuts.qml
Normal file
|
@ -0,0 +1,37 @@
|
|||
import "root:/widgets"
|
||||
import "root:/services"
|
||||
import Quickshell
|
||||
|
||||
Scope {
|
||||
id: root
|
||||
|
||||
property bool launcherInterrupted
|
||||
|
||||
CustomShortcut {
|
||||
name: "session"
|
||||
description: "Toggle session menu"
|
||||
onPressed: {
|
||||
const visibilities = Visibilities.getForActive();
|
||||
visibilities.session = !visibilities.session;
|
||||
}
|
||||
}
|
||||
|
||||
CustomShortcut {
|
||||
name: "launcher"
|
||||
description: "Toggle launcher"
|
||||
onPressed: root.launcherInterrupted = false
|
||||
onReleased: {
|
||||
if (!root.launcherInterrupted) {
|
||||
const visibilities = Visibilities.getForActive();
|
||||
visibilities.launcher = !visibilities.launcher;
|
||||
}
|
||||
root.launcherInterrupted = false;
|
||||
}
|
||||
}
|
||||
|
||||
CustomShortcut {
|
||||
name: "launcherInterrupt"
|
||||
description: "Interrupt launcher keybind"
|
||||
onPressed: root.launcherInterrupted = true
|
||||
}
|
||||
}
|
26
modules/background/Background.qml
Normal file
26
modules/background/Background.qml
Normal file
|
@ -0,0 +1,26 @@
|
|||
import "root:/widgets"
|
||||
import Quickshell
|
||||
import Quickshell.Wayland
|
||||
|
||||
Variants {
|
||||
model: Quickshell.screens
|
||||
|
||||
StyledWindow {
|
||||
id: win
|
||||
|
||||
required property ShellScreen modelData
|
||||
|
||||
screen: modelData
|
||||
name: "background"
|
||||
WlrLayershell.exclusionMode: ExclusionMode.Ignore
|
||||
WlrLayershell.layer: WlrLayer.Background
|
||||
color: "black"
|
||||
|
||||
anchors.top: true
|
||||
anchors.bottom: true
|
||||
anchors.left: true
|
||||
anchors.right: true
|
||||
|
||||
Wallpaper {}
|
||||
}
|
||||
}
|
77
modules/background/Wallpaper.qml
Normal file
77
modules/background/Wallpaper.qml
Normal file
|
@ -0,0 +1,77 @@
|
|||
pragma ComponentBehavior: Bound
|
||||
|
||||
import "root:/widgets"
|
||||
import "root:/services"
|
||||
import "root:/config"
|
||||
import QtQuick
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property url source: Wallpapers.current ? `file://${Wallpapers.current}` : ""
|
||||
property Image current: one
|
||||
|
||||
anchors.fill: parent
|
||||
|
||||
onSourceChanged: {
|
||||
if (current === one)
|
||||
two.update();
|
||||
else
|
||||
one.update();
|
||||
}
|
||||
|
||||
Img {
|
||||
id: one
|
||||
}
|
||||
|
||||
Img {
|
||||
id: two
|
||||
}
|
||||
|
||||
component Img: CachingImage {
|
||||
id: img
|
||||
|
||||
function update(): void {
|
||||
const srcPath = `${root.source}`.slice(7);
|
||||
if (thumbnail.originalPath === srcPath) {
|
||||
root.current = this;
|
||||
} else
|
||||
path = srcPath;
|
||||
}
|
||||
|
||||
anchors.fill: parent
|
||||
|
||||
loadOriginal: true
|
||||
asynchronous: true
|
||||
cache: false
|
||||
fillMode: Image.PreserveAspectCrop
|
||||
|
||||
opacity: 0
|
||||
scale: Wallpapers.showPreview ? 1 : 0.8
|
||||
|
||||
onStatusChanged: {
|
||||
if (status === Image.Ready)
|
||||
root.current = this;
|
||||
}
|
||||
|
||||
states: State {
|
||||
name: "visible"
|
||||
when: root.current === img
|
||||
|
||||
PropertyChanges {
|
||||
img.opacity: 1
|
||||
img.scale: 1
|
||||
}
|
||||
}
|
||||
|
||||
transitions: Transition {
|
||||
NumberAnimation {
|
||||
target: img
|
||||
properties: "opacity,scale"
|
||||
duration: Appearance.anim.durations.normal
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: Appearance.anim.curves.standard
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
174
modules/bar/Bar.qml
Normal file
174
modules/bar/Bar.qml
Normal file
|
@ -0,0 +1,174 @@
|
|||
import "root:/widgets"
|
||||
import "root:/services"
|
||||
import "root:/config"
|
||||
import "root:/modules/bar/popouts" as BarPopouts
|
||||
import "components"
|
||||
import "components/workspaces"
|
||||
import Quickshell
|
||||
import QtQuick
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
required property ShellScreen screen
|
||||
required property BarPopouts.Wrapper popouts
|
||||
|
||||
function checkPopout(y: real): void {
|
||||
const spacing = Appearance.spacing.small;
|
||||
const aw = activeWindow.child;
|
||||
const awy = activeWindow.y + aw.y;
|
||||
|
||||
const ty = tray.y;
|
||||
const th = tray.implicitHeight;
|
||||
const trayItems = tray.items;
|
||||
|
||||
const n = statusIconsInner.network;
|
||||
const ny = statusIcons.y + statusIconsInner.y + n.y - spacing / 2;
|
||||
|
||||
const bls = statusIcons.y + statusIconsInner.y + statusIconsInner.bs - spacing / 2;
|
||||
const ble = statusIcons.y + statusIconsInner.y + statusIconsInner.be + spacing / 2;
|
||||
|
||||
const b = statusIconsInner.battery;
|
||||
const by = statusIcons.y + statusIconsInner.y + b.y - spacing / 2;
|
||||
|
||||
if (y >= awy && y <= awy + aw.implicitHeight) {
|
||||
popouts.currentName = "activewindow";
|
||||
popouts.currentCenter = Qt.binding(() => activeWindow.y + aw.y + aw.implicitHeight / 2);
|
||||
popouts.hasCurrent = true;
|
||||
} else if (y > ty && y < ty + th) {
|
||||
const index = Math.floor(((y - ty) / th) * trayItems.count);
|
||||
const item = trayItems.itemAt(index);
|
||||
|
||||
popouts.currentName = `traymenu${index}`;
|
||||
popouts.currentCenter = Qt.binding(() => tray.y + item.y + item.implicitHeight / 2);
|
||||
popouts.hasCurrent = true;
|
||||
} else if (y >= ny && y <= ny + n.implicitHeight + spacing) {
|
||||
popouts.currentName = "network";
|
||||
popouts.currentCenter = Qt.binding(() => statusIcons.y + statusIconsInner.y + n.y + n.implicitHeight / 2);
|
||||
popouts.hasCurrent = true;
|
||||
} else if (y >= bls && y <= ble) {
|
||||
popouts.currentName = "bluetooth";
|
||||
popouts.currentCenter = Qt.binding(() => statusIcons.y + statusIconsInner.y + statusIconsInner.bs + (statusIconsInner.be - statusIconsInner.bs) / 2);
|
||||
popouts.hasCurrent = true;
|
||||
} else if (y >= by && y <= by + b.implicitHeight + spacing) {
|
||||
popouts.currentName = "battery";
|
||||
popouts.currentCenter = Qt.binding(() => statusIcons.y + statusIconsInner.y + b.y + b.implicitHeight / 2);
|
||||
popouts.hasCurrent = true;
|
||||
} else {
|
||||
popouts.hasCurrent = false;
|
||||
}
|
||||
}
|
||||
|
||||
anchors.top: parent.top
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.left: parent.left
|
||||
|
||||
implicitWidth: child.implicitWidth + BorderConfig.thickness * 2
|
||||
|
||||
Item {
|
||||
id: child
|
||||
|
||||
anchors.top: parent.top
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
|
||||
implicitWidth: Math.max(osIcon.implicitWidth, workspaces.implicitWidth, activeWindow.implicitWidth, tray.implicitWidth, clock.implicitWidth, statusIcons.implicitWidth, power.implicitWidth)
|
||||
|
||||
OsIcon {
|
||||
id: osIcon
|
||||
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.top: parent.top
|
||||
anchors.topMargin: Appearance.padding.large
|
||||
}
|
||||
|
||||
StyledRect {
|
||||
id: workspaces
|
||||
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.top: osIcon.bottom
|
||||
anchors.topMargin: Appearance.spacing.normal
|
||||
|
||||
radius: Appearance.rounding.full
|
||||
color: Colours.palette.m3surfaceContainer
|
||||
|
||||
implicitWidth: workspacesInner.implicitWidth + Appearance.padding.small * 2
|
||||
implicitHeight: workspacesInner.implicitHeight + Appearance.padding.small * 2
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: -BorderConfig.thickness
|
||||
anchors.rightMargin: -BorderConfig.thickness
|
||||
|
||||
onWheel: event => {
|
||||
const activeWs = Hyprland.activeClient?.workspace?.name;
|
||||
if (activeWs?.startsWith("special:"))
|
||||
Hyprland.dispatch(`togglespecialworkspace ${activeWs.slice(8)}`);
|
||||
else if (event.angleDelta.y < 0 || Hyprland.activeWsId > 1)
|
||||
Hyprland.dispatch(`workspace r${event.angleDelta.y > 0 ? "-" : "+"}1`);
|
||||
}
|
||||
}
|
||||
|
||||
Workspaces {
|
||||
id: workspacesInner
|
||||
|
||||
anchors.centerIn: parent
|
||||
}
|
||||
}
|
||||
|
||||
ActiveWindow {
|
||||
id: activeWindow
|
||||
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.top: workspaces.bottom
|
||||
anchors.bottom: tray.top
|
||||
anchors.margins: Appearance.spacing.large
|
||||
|
||||
monitor: Brightness.getMonitorForScreen(root.screen)
|
||||
}
|
||||
|
||||
Tray {
|
||||
id: tray
|
||||
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.bottom: clock.top
|
||||
anchors.bottomMargin: Appearance.spacing.larger
|
||||
}
|
||||
|
||||
Clock {
|
||||
id: clock
|
||||
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.bottom: statusIcons.top
|
||||
anchors.bottomMargin: Appearance.spacing.normal
|
||||
}
|
||||
|
||||
StyledRect {
|
||||
id: statusIcons
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.bottom: power.top
|
||||
anchors.bottomMargin: Appearance.spacing.normal
|
||||
|
||||
radius: Appearance.rounding.full
|
||||
color: Colours.palette.m3surfaceContainer
|
||||
|
||||
implicitHeight: statusIconsInner.implicitHeight + Appearance.padding.normal * 2
|
||||
|
||||
StatusIcons {
|
||||
id: statusIconsInner
|
||||
|
||||
anchors.centerIn: parent
|
||||
}
|
||||
}
|
||||
|
||||
Power {
|
||||
id: power
|
||||
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.bottomMargin: Appearance.padding.large
|
||||
}
|
||||
}
|
||||
}
|
140
modules/bar/components/ActiveWindow.qml
Normal file
140
modules/bar/components/ActiveWindow.qml
Normal file
|
@ -0,0 +1,140 @@
|
|||
pragma ComponentBehavior: Bound
|
||||
|
||||
import "root:/widgets"
|
||||
import "root:/services"
|
||||
import "root:/utils"
|
||||
import "root:/config"
|
||||
import QtQuick
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
required property Brightness.Monitor monitor
|
||||
property color colour: Colours.palette.m3primary
|
||||
readonly property Item child: child
|
||||
|
||||
implicitWidth: child.implicitWidth
|
||||
implicitHeight: child.implicitHeight
|
||||
|
||||
MouseArea {
|
||||
anchors.top: parent.top
|
||||
anchors.bottom: child.top
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
|
||||
onWheel: event => {
|
||||
if (event.angleDelta.y > 0)
|
||||
Audio.setVolume(Audio.volume + 0.1);
|
||||
else if (event.angleDelta.y < 0)
|
||||
Audio.setVolume(Audio.volume - 0.1);
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.top: child.bottom
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
|
||||
onWheel: event => {
|
||||
const monitor = root.monitor;
|
||||
if (event.angleDelta.y > 0)
|
||||
monitor.setBrightness(monitor.brightness + 0.1);
|
||||
else if (event.angleDelta.y < 0)
|
||||
monitor.setBrightness(monitor.brightness - 0.1);
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
id: child
|
||||
|
||||
property Item current: text1
|
||||
|
||||
anchors.centerIn: parent
|
||||
|
||||
clip: true
|
||||
implicitWidth: Math.max(icon.implicitWidth, current.implicitHeight)
|
||||
implicitHeight: icon.implicitHeight + current.implicitWidth + current.anchors.topMargin
|
||||
|
||||
MaterialIcon {
|
||||
id: icon
|
||||
|
||||
animate: true
|
||||
text: Icons.getAppCategoryIcon(Hyprland.activeClient?.wmClass, "desktop_windows")
|
||||
color: root.colour
|
||||
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
}
|
||||
|
||||
Title {
|
||||
id: text1
|
||||
}
|
||||
|
||||
Title {
|
||||
id: text2
|
||||
}
|
||||
|
||||
TextMetrics {
|
||||
id: metrics
|
||||
|
||||
text: Hyprland.activeClient?.title ?? qsTr("Desktop")
|
||||
font.pointSize: Appearance.font.size.smaller
|
||||
font.family: Appearance.font.family.mono
|
||||
elide: Qt.ElideRight
|
||||
elideWidth: root.height - icon.height
|
||||
|
||||
onTextChanged: {
|
||||
const next = child.current === text1 ? text2 : text1;
|
||||
next.text = elidedText;
|
||||
child.current = next;
|
||||
}
|
||||
onElideWidthChanged: child.current.text = elidedText
|
||||
}
|
||||
|
||||
Behavior on implicitWidth {
|
||||
NumberAnimation {
|
||||
duration: Appearance.anim.durations.normal
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: Appearance.anim.curves.emphasized
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on implicitHeight {
|
||||
NumberAnimation {
|
||||
duration: Appearance.anim.durations.normal
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: Appearance.anim.curves.emphasized
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
component Title: StyledText {
|
||||
id: text
|
||||
|
||||
anchors.horizontalCenter: icon.horizontalCenter
|
||||
anchors.top: icon.bottom
|
||||
anchors.topMargin: Appearance.spacing.small
|
||||
|
||||
font.pointSize: metrics.font.pointSize
|
||||
font.family: metrics.font.family
|
||||
color: root.colour
|
||||
opacity: child.current === this ? 1 : 0
|
||||
|
||||
transform: Rotation {
|
||||
angle: 90
|
||||
origin.x: text.implicitHeight / 2
|
||||
origin.y: text.implicitHeight / 2
|
||||
}
|
||||
|
||||
width: implicitHeight
|
||||
height: implicitWidth
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: Appearance.anim.durations.normal
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: Appearance.anim.curves.standard
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
33
modules/bar/components/Clock.qml
Normal file
33
modules/bar/components/Clock.qml
Normal file
|
@ -0,0 +1,33 @@
|
|||
import "root:/widgets"
|
||||
import "root:/services"
|
||||
import "root:/config"
|
||||
import QtQuick
|
||||
|
||||
Column {
|
||||
id: root
|
||||
|
||||
property color colour: Colours.palette.m3tertiary
|
||||
|
||||
spacing: Appearance.spacing.small
|
||||
|
||||
MaterialIcon {
|
||||
id: icon
|
||||
|
||||
text: "calendar_month"
|
||||
color: root.colour
|
||||
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
id: text
|
||||
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
|
||||
horizontalAlignment: StyledText.AlignHCenter
|
||||
text: Time.format("hh\nmm")
|
||||
font.pointSize: Appearance.font.size.smaller
|
||||
font.family: Appearance.font.family.mono
|
||||
color: root.colour
|
||||
}
|
||||
}
|
11
modules/bar/components/OsIcon.qml
Normal file
11
modules/bar/components/OsIcon.qml
Normal file
|
@ -0,0 +1,11 @@
|
|||
import "root:/widgets"
|
||||
import "root:/services"
|
||||
import "root:/utils"
|
||||
import "root:/config"
|
||||
|
||||
StyledText {
|
||||
text: Icons.osIcon
|
||||
font.pointSize: Appearance.font.size.smaller
|
||||
font.family: Appearance.font.family.mono
|
||||
color: Colours.palette.m3tertiary
|
||||
}
|
27
modules/bar/components/Power.qml
Normal file
27
modules/bar/components/Power.qml
Normal file
|
@ -0,0 +1,27 @@
|
|||
import "root:/widgets"
|
||||
import "root:/services"
|
||||
import "root:/config"
|
||||
import Quickshell
|
||||
|
||||
MaterialIcon {
|
||||
text: "power_settings_new"
|
||||
color: Colours.palette.m3error
|
||||
font.bold: true
|
||||
font.pointSize: Appearance.font.size.normal
|
||||
|
||||
StateLayer {
|
||||
anchors.fill: undefined
|
||||
anchors.centerIn: parent
|
||||
anchors.horizontalCenterOffset: 1
|
||||
|
||||
implicitWidth: parent.implicitHeight + Appearance.padding.small * 2
|
||||
implicitHeight: implicitWidth
|
||||
|
||||
radius: Appearance.rounding.full
|
||||
|
||||
function onClicked(): void {
|
||||
const v = Visibilities.screens[QsWindow.window.screen];
|
||||
v.session = !v.session;
|
||||
}
|
||||
}
|
||||
}
|
114
modules/bar/components/StatusIcons.qml
Normal file
114
modules/bar/components/StatusIcons.qml
Normal file
|
@ -0,0 +1,114 @@
|
|||
import "root:/widgets"
|
||||
import "root:/services"
|
||||
import "root:/utils"
|
||||
import "root:/config"
|
||||
import Quickshell
|
||||
import Quickshell.Services.UPower
|
||||
import QtQuick
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property color colour: Colours.palette.m3secondary
|
||||
|
||||
readonly property Item network: network
|
||||
readonly property real bs: bluetooth.y
|
||||
readonly property real be: repeater.count > 0 ? devices.y + devices.implicitHeight : bluetooth.y + bluetooth.implicitHeight
|
||||
readonly property Item battery: battery
|
||||
|
||||
clip: true
|
||||
implicitWidth: Math.max(network.implicitWidth, bluetooth.implicitWidth, devices.implicitWidth, battery.implicitWidth)
|
||||
implicitHeight: network.implicitHeight + bluetooth.implicitHeight + bluetooth.anchors.topMargin + (repeater.count > 0 ? devices.implicitHeight + devices.anchors.topMargin : 0) + battery.implicitHeight + battery.anchors.topMargin
|
||||
|
||||
MaterialIcon {
|
||||
id: network
|
||||
|
||||
animate: true
|
||||
text: Network.active ? Icons.getNetworkIcon(Network.active.strength ?? 0) : "wifi_off"
|
||||
color: root.colour
|
||||
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
}
|
||||
|
||||
MaterialIcon {
|
||||
id: bluetooth
|
||||
|
||||
anchors.horizontalCenter: network.horizontalCenter
|
||||
anchors.top: network.bottom
|
||||
anchors.topMargin: Appearance.spacing.small
|
||||
|
||||
animate: true
|
||||
text: Bluetooth.powered ? "bluetooth" : "bluetooth_disabled"
|
||||
color: root.colour
|
||||
}
|
||||
|
||||
Column {
|
||||
id: devices
|
||||
|
||||
anchors.horizontalCenter: bluetooth.horizontalCenter
|
||||
anchors.top: bluetooth.bottom
|
||||
anchors.topMargin: Appearance.spacing.small
|
||||
|
||||
Repeater {
|
||||
id: repeater
|
||||
|
||||
model: ScriptModel {
|
||||
values: Bluetooth.devices.filter(d => d.connected)
|
||||
}
|
||||
|
||||
MaterialIcon {
|
||||
required property Bluetooth.Device modelData
|
||||
|
||||
animate: true
|
||||
text: Icons.getBluetoothIcon(modelData.icon)
|
||||
color: root.colour
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MaterialIcon {
|
||||
id: battery
|
||||
|
||||
anchors.horizontalCenter: devices.horizontalCenter
|
||||
anchors.top: repeater.count > 0 ? devices.bottom : bluetooth.bottom
|
||||
anchors.topMargin: Appearance.spacing.small
|
||||
|
||||
animate: true
|
||||
text: {
|
||||
if (!UPower.displayDevice.isLaptopBattery) {
|
||||
if (PowerProfiles.profile === PowerProfile.PowerSaver)
|
||||
return "energy_savings_leaf";
|
||||
if (PowerProfiles.profile === PowerProfile.Performance)
|
||||
return "rocket_launch";
|
||||
return "balance";
|
||||
}
|
||||
|
||||
const perc = UPower.displayDevice.percentage;
|
||||
const charging = !UPower.onBattery;
|
||||
if (perc === 1)
|
||||
return charging ? "battery_charging_full" : "battery_full";
|
||||
let level = Math.floor(perc * 7);
|
||||
if (charging && (level === 4 || level === 1))
|
||||
level--;
|
||||
return charging ? `battery_charging_${(level + 3) * 10}` : `battery_${level}_bar`;
|
||||
}
|
||||
color: !UPower.onBattery || UPower.displayDevice.percentage > 0.2 ? root.colour : Colours.palette.m3error
|
||||
fill: 1
|
||||
}
|
||||
|
||||
Behavior on implicitWidth {
|
||||
NumberAnimation {
|
||||
duration: Appearance.anim.durations.normal
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: Appearance.anim.curves.emphasized
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on implicitHeight {
|
||||
NumberAnimation {
|
||||
duration: Appearance.anim.durations.normal
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: Appearance.anim.curves.emphasized
|
||||
}
|
||||
}
|
||||
}
|
56
modules/bar/components/Tray.qml
Normal file
56
modules/bar/components/Tray.qml
Normal file
|
@ -0,0 +1,56 @@
|
|||
import "root:/config"
|
||||
import Quickshell.Services.SystemTray
|
||||
import QtQuick
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
readonly property Repeater items: items
|
||||
|
||||
clip: true
|
||||
visible: width > 0 && height > 0 // To avoid warnings about being visible with no size
|
||||
|
||||
implicitWidth: layout.implicitWidth
|
||||
implicitHeight: layout.implicitHeight
|
||||
|
||||
Column {
|
||||
id: layout
|
||||
|
||||
spacing: Appearance.spacing.small
|
||||
|
||||
add: Transition {
|
||||
NumberAnimation {
|
||||
properties: "scale"
|
||||
from: 0
|
||||
to: 1
|
||||
duration: Appearance.anim.durations.normal
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: Appearance.anim.curves.standardDecel
|
||||
}
|
||||
}
|
||||
|
||||
Repeater {
|
||||
id: items
|
||||
|
||||
model: SystemTray.items
|
||||
|
||||
TrayItem {}
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on implicitWidth {
|
||||
NumberAnimation {
|
||||
duration: Appearance.anim.durations.normal
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: Appearance.anim.curves.emphasized
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on implicitHeight {
|
||||
NumberAnimation {
|
||||
duration: Appearance.anim.durations.normal
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: Appearance.anim.curves.emphasized
|
||||
}
|
||||
}
|
||||
}
|
48
modules/bar/components/TrayItem.qml
Normal file
48
modules/bar/components/TrayItem.qml
Normal file
|
@ -0,0 +1,48 @@
|
|||
pragma ComponentBehavior: Bound
|
||||
|
||||
import "root:/widgets"
|
||||
import "root:/config"
|
||||
import Quickshell
|
||||
import Quickshell.Widgets
|
||||
import Quickshell.Services.SystemTray
|
||||
import QtQuick
|
||||
|
||||
MouseArea {
|
||||
id: root
|
||||
|
||||
required property SystemTrayItem modelData
|
||||
|
||||
acceptedButtons: Qt.LeftButton | Qt.RightButton
|
||||
implicitWidth: Appearance.font.size.small * 2
|
||||
implicitHeight: Appearance.font.size.small * 2
|
||||
|
||||
onClicked: event => {
|
||||
if (event.button === Qt.LeftButton)
|
||||
modelData.activate();
|
||||
else if (modelData.hasMenu)
|
||||
menu.open();
|
||||
}
|
||||
|
||||
// TODO custom menu
|
||||
QsMenuAnchor {
|
||||
id: menu
|
||||
|
||||
menu: root.modelData.menu
|
||||
anchor.window: this.QsWindow.window
|
||||
}
|
||||
|
||||
IconImage {
|
||||
id: icon
|
||||
|
||||
source: {
|
||||
let icon = root.modelData.icon;
|
||||
if (icon.includes("?path=")) {
|
||||
const [name, path] = icon.split("?path=");
|
||||
icon = `file://${path}/${name.slice(name.lastIndexOf("/") + 1)}`;
|
||||
}
|
||||
return icon;
|
||||
}
|
||||
asynchronous: true
|
||||
anchors.fill: parent
|
||||
}
|
||||
}
|
111
modules/bar/components/workspaces/ActiveIndicator.qml
Normal file
111
modules/bar/components/workspaces/ActiveIndicator.qml
Normal file
|
@ -0,0 +1,111 @@
|
|||
import "root:/widgets"
|
||||
import "root:/services"
|
||||
import "root:/config"
|
||||
import QtQuick
|
||||
import QtQuick.Effects
|
||||
|
||||
StyledRect {
|
||||
id: root
|
||||
|
||||
required property list<Workspace> workspaces
|
||||
required property Item mask
|
||||
required property real maskWidth
|
||||
required property real maskHeight
|
||||
required property int groupOffset
|
||||
|
||||
readonly property int currentWsIdx: Hyprland.activeWsId - 1 - groupOffset
|
||||
property real leading: getWsY(currentWsIdx)
|
||||
property real trailing: getWsY(currentWsIdx)
|
||||
property real currentSize: workspaces[currentWsIdx]?.size ?? 0
|
||||
property real offset: Math.min(leading, trailing)
|
||||
property real size: {
|
||||
const s = Math.abs(leading - trailing) + currentSize;
|
||||
if (BarConfig.workspaces.activeTrail && lastWs > currentWsIdx)
|
||||
return Math.min(getWsY(lastWs) + (workspaces[lastWs]?.size ?? 0) - offset, s);
|
||||
return s;
|
||||
}
|
||||
|
||||
property int cWs
|
||||
property int lastWs
|
||||
|
||||
function getWsY(idx: int): real {
|
||||
let y = 0;
|
||||
for (let i = 0; i < idx; i++)
|
||||
y += workspaces[i]?.size ?? 0;
|
||||
return y;
|
||||
}
|
||||
|
||||
onCurrentWsIdxChanged: {
|
||||
lastWs = cWs;
|
||||
cWs = currentWsIdx;
|
||||
}
|
||||
|
||||
clip: true
|
||||
x: 1
|
||||
y: offset + 1
|
||||
implicitWidth: BarConfig.sizes.innerHeight - 2
|
||||
implicitHeight: size - 2
|
||||
radius: BarConfig.workspaces.rounded ? Appearance.rounding.full : 0
|
||||
color: Colours.palette.m3primary
|
||||
|
||||
StyledRect {
|
||||
id: base
|
||||
|
||||
visible: false
|
||||
anchors.fill: parent
|
||||
color: Colours.palette.m3onPrimary
|
||||
}
|
||||
|
||||
MultiEffect {
|
||||
source: base
|
||||
maskSource: root.mask
|
||||
maskEnabled: true
|
||||
maskSpreadAtMin: 1
|
||||
maskThresholdMin: 0.5
|
||||
|
||||
x: 0
|
||||
y: -parent.offset
|
||||
implicitWidth: root.maskWidth
|
||||
implicitHeight: root.maskHeight
|
||||
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
}
|
||||
|
||||
Behavior on leading {
|
||||
enabled: BarConfig.workspaces.activeTrail
|
||||
|
||||
Anim {}
|
||||
}
|
||||
|
||||
Behavior on trailing {
|
||||
enabled: BarConfig.workspaces.activeTrail
|
||||
|
||||
Anim {
|
||||
duration: Appearance.anim.durations.normal * 2
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on currentSize {
|
||||
enabled: BarConfig.workspaces.activeTrail
|
||||
|
||||
Anim {}
|
||||
}
|
||||
|
||||
Behavior on offset {
|
||||
enabled: !BarConfig.workspaces.activeTrail
|
||||
|
||||
Anim {}
|
||||
}
|
||||
|
||||
Behavior on size {
|
||||
enabled: !BarConfig.workspaces.activeTrail
|
||||
|
||||
Anim {}
|
||||
}
|
||||
|
||||
component Anim: NumberAnimation {
|
||||
duration: Appearance.anim.durations.normal
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: Appearance.anim.curves.emphasized
|
||||
}
|
||||
}
|
99
modules/bar/components/workspaces/OccupiedBg.qml
Normal file
99
modules/bar/components/workspaces/OccupiedBg.qml
Normal file
|
@ -0,0 +1,99 @@
|
|||
pragma ComponentBehavior: Bound
|
||||
|
||||
import "root:/widgets"
|
||||
import "root:/services"
|
||||
import "root:/config"
|
||||
import Quickshell
|
||||
import QtQuick
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
required property list<Workspace> workspaces
|
||||
required property var occupied
|
||||
required property int groupOffset
|
||||
|
||||
property list<var> pills: []
|
||||
|
||||
onOccupiedChanged: {
|
||||
let count = 0;
|
||||
const start = groupOffset;
|
||||
const end = start + BarConfig.workspaces.shown;
|
||||
for (const [ws, occ] of Object.entries(occupied)) {
|
||||
if (ws > start && ws <= end && occ) {
|
||||
if (!occupied[ws - 1]) {
|
||||
if (pills[count])
|
||||
pills[count].start = ws;
|
||||
else
|
||||
pills.push(pillComp.createObject(root, {
|
||||
start: ws
|
||||
}));
|
||||
count++;
|
||||
}
|
||||
if (!occupied[ws + 1])
|
||||
pills[count - 1].end = ws;
|
||||
}
|
||||
}
|
||||
if (pills.length > count)
|
||||
pills.splice(count, pills.length - count).forEach(p => p.destroy());
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: ScriptModel {
|
||||
values: root.pills.filter(p => p)
|
||||
}
|
||||
|
||||
StyledRect {
|
||||
id: rect
|
||||
|
||||
required property var modelData
|
||||
|
||||
readonly property Workspace start: root.workspaces[modelData.start - 1 - root.groupOffset] ?? null
|
||||
readonly property Workspace end: root.workspaces[modelData.end - 1 - root.groupOffset] ?? null
|
||||
|
||||
color: Colours.alpha(Colours.palette.m3surfaceContainerHigh, true)
|
||||
radius: BarConfig.workspaces.rounded ? Appearance.rounding.full : 0
|
||||
|
||||
x: start?.x ?? 0
|
||||
y: start?.y ?? 0
|
||||
implicitWidth: BarConfig.sizes.innerHeight
|
||||
implicitHeight: end?.y + end?.height - start?.y
|
||||
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
|
||||
scale: 0
|
||||
Component.onCompleted: scale = 1
|
||||
|
||||
Behavior on scale {
|
||||
Anim {
|
||||
easing.bezierCurve: Appearance.anim.curves.standardDecel
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on x {
|
||||
Anim {}
|
||||
}
|
||||
|
||||
Behavior on y {
|
||||
Anim {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
component Anim: NumberAnimation {
|
||||
duration: Appearance.anim.durations.normal
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: Appearance.anim.curves.standard
|
||||
}
|
||||
|
||||
component Pill: QtObject {
|
||||
property int start
|
||||
property int end
|
||||
}
|
||||
|
||||
Component {
|
||||
id: pillComp
|
||||
|
||||
Pill {}
|
||||
}
|
||||
}
|
93
modules/bar/components/workspaces/Workspace.qml
Normal file
93
modules/bar/components/workspaces/Workspace.qml
Normal file
|
@ -0,0 +1,93 @@
|
|||
import "root:/widgets"
|
||||
import "root:/services"
|
||||
import "root:/utils"
|
||||
import "root:/config"
|
||||
import Quickshell
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
required property int index
|
||||
required property var occupied
|
||||
required property int groupOffset
|
||||
|
||||
readonly property bool isWorkspace: true // Flag for finding workspace children
|
||||
// Unanimated prop for others to use as reference
|
||||
readonly property real size: childrenRect.height + (hasWindows ? Appearance.padding.normal : 0)
|
||||
|
||||
readonly property int ws: groupOffset + index + 1
|
||||
readonly property bool isOccupied: occupied[ws] ?? false
|
||||
readonly property bool hasWindows: isOccupied && BarConfig.workspaces.showWindows
|
||||
|
||||
Layout.preferredWidth: childrenRect.width
|
||||
Layout.preferredHeight: size
|
||||
|
||||
StyledText {
|
||||
id: indicator
|
||||
|
||||
readonly property string label: BarConfig.workspaces.label || root.ws
|
||||
readonly property string occupiedLabel: BarConfig.workspaces.occupiedLabel || label
|
||||
readonly property string activeLabel: BarConfig.workspaces.activeLabel || (root.isOccupied ? occupiedLabel : label)
|
||||
|
||||
animate: true
|
||||
text: Hyprland.activeWsId === root.ws ? activeLabel : root.isOccupied ? occupiedLabel : label
|
||||
color: BarConfig.workspaces.occupiedBg || root.isOccupied || Hyprland.activeWsId === root.ws ? Colours.palette.m3onSurface : Colours.palette.m3outlineVariant
|
||||
horizontalAlignment: StyledText.AlignHCenter
|
||||
verticalAlignment: StyledText.AlignVCenter
|
||||
|
||||
width: BarConfig.sizes.innerHeight
|
||||
height: BarConfig.sizes.innerHeight
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: windows
|
||||
|
||||
active: BarConfig.workspaces.showWindows
|
||||
asynchronous: true
|
||||
|
||||
anchors.horizontalCenter: indicator.horizontalCenter
|
||||
anchors.top: indicator.bottom
|
||||
|
||||
sourceComponent: Column {
|
||||
spacing: Appearance.spacing.small
|
||||
|
||||
add: Transition {
|
||||
Anim {
|
||||
properties: "scale"
|
||||
from: 0
|
||||
to: 1
|
||||
easing.bezierCurve: Appearance.anim.curves.standardDecel
|
||||
}
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: ScriptModel {
|
||||
values: Hyprland.clients.filter(c => c.workspace?.id === root.ws)
|
||||
}
|
||||
|
||||
MaterialIcon {
|
||||
required property Hyprland.Client modelData
|
||||
|
||||
text: Icons.getAppCategoryIcon(modelData.wmClass, "terminal")
|
||||
color: Colours.palette.m3onSurfaceVariant
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on Layout.preferredWidth {
|
||||
Anim {}
|
||||
}
|
||||
|
||||
Behavior on Layout.preferredHeight {
|
||||
Anim {}
|
||||
}
|
||||
|
||||
component Anim: NumberAnimation {
|
||||
duration: Appearance.anim.durations.normal
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: Appearance.anim.curves.standard
|
||||
}
|
||||
}
|
75
modules/bar/components/workspaces/Workspaces.qml
Normal file
75
modules/bar/components/workspaces/Workspaces.qml
Normal file
|
@ -0,0 +1,75 @@
|
|||
pragma ComponentBehavior: Bound
|
||||
|
||||
import "root:/widgets"
|
||||
import "root:/services"
|
||||
import "root:/config"
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
readonly property list<Workspace> workspaces: layout.children.filter(c => c.isWorkspace).sort((w1, w2) => w1.ws - w2.ws)
|
||||
readonly property var occupied: Hyprland.workspaces.values.reduce((acc, curr) => {
|
||||
acc[curr.id] = curr.lastIpcObject.windows > 0;
|
||||
return acc;
|
||||
}, {})
|
||||
readonly property int groupOffset: Math.floor((Hyprland.activeWsId - 1) / BarConfig.workspaces.shown) * BarConfig.workspaces.shown
|
||||
|
||||
implicitWidth: layout.implicitWidth
|
||||
implicitHeight: layout.implicitHeight
|
||||
|
||||
ColumnLayout {
|
||||
id: layout
|
||||
|
||||
spacing: 0
|
||||
layer.enabled: true
|
||||
layer.smooth: true
|
||||
|
||||
Repeater {
|
||||
model: BarConfig.workspaces.shown
|
||||
|
||||
Workspace {
|
||||
occupied: root.occupied
|
||||
groupOffset: root.groupOffset
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
active: BarConfig.workspaces.occupiedBg
|
||||
asynchronous: true
|
||||
|
||||
z: -1
|
||||
anchors.fill: parent
|
||||
|
||||
sourceComponent: OccupiedBg {
|
||||
workspaces: root.workspaces
|
||||
occupied: root.occupied
|
||||
groupOffset: root.groupOffset
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
active: BarConfig.workspaces.activeIndicator
|
||||
asynchronous: true
|
||||
|
||||
sourceComponent: ActiveIndicator {
|
||||
workspaces: root.workspaces
|
||||
mask: layout
|
||||
maskWidth: root.width
|
||||
maskHeight: root.height
|
||||
groupOffset: root.groupOffset
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
|
||||
onPressed: event => {
|
||||
const ws = layout.childAt(event.x, event.y).index + root.groupOffset + 1;
|
||||
if (Hyprland.activeWsId !== ws)
|
||||
Hyprland.dispatch(`workspace ${ws}`);
|
||||
}
|
||||
}
|
||||
}
|
75
modules/bar/popouts/ActiveWindow.qml
Normal file
75
modules/bar/popouts/ActiveWindow.qml
Normal file
|
@ -0,0 +1,75 @@
|
|||
import "root:/widgets"
|
||||
import "root:/services"
|
||||
import "root:/utils"
|
||||
import "root:/config"
|
||||
import Quickshell.Widgets
|
||||
import Quickshell.Wayland
|
||||
import QtQuick
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
implicitWidth: Hyprland.activeClient ? child.implicitWidth : -Appearance.padding.large * 2
|
||||
implicitHeight: child.implicitHeight
|
||||
|
||||
Column {
|
||||
id: child
|
||||
|
||||
anchors.centerIn: parent
|
||||
spacing: Appearance.spacing.normal
|
||||
|
||||
Row {
|
||||
id: detailsRow
|
||||
|
||||
spacing: Appearance.spacing.normal
|
||||
|
||||
IconImage {
|
||||
id: icon
|
||||
|
||||
implicitSize: details.implicitHeight
|
||||
source: Icons.getAppIcon(Hyprland.activeClient?.wmClass ?? "", "image-missing")
|
||||
}
|
||||
|
||||
Column {
|
||||
id: details
|
||||
|
||||
StyledText {
|
||||
text: Hyprland.activeClient?.title ?? ""
|
||||
font.pointSize: Appearance.font.size.normal
|
||||
|
||||
elide: Text.ElideRight
|
||||
width: preview.implicitWidth - icon.implicitWidth - detailsRow.spacing
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: Hyprland.activeClient?.wmClass ?? ""
|
||||
color: Colours.palette.m3onSurfaceVariant
|
||||
|
||||
elide: Text.ElideRight
|
||||
width: preview.implicitWidth - icon.implicitWidth - detailsRow.spacing
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ClippingWrapperRectangle {
|
||||
color: "transparent"
|
||||
radius: Appearance.rounding.small
|
||||
|
||||
ScreencopyView {
|
||||
id: preview
|
||||
|
||||
captureSource: ToplevelManager.toplevels.values.find(t => t.title === Hyprland.activeClient?.title) ?? null
|
||||
live: visible
|
||||
|
||||
constraintSize.width: BarConfig.sizes.windowPreviewSize
|
||||
constraintSize.height: BarConfig.sizes.windowPreviewSize
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
component Anim: NumberAnimation {
|
||||
duration: Appearance.anim.durations.normal
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: Appearance.anim.curves.emphasized
|
||||
}
|
||||
}
|
74
modules/bar/popouts/Background.qml
Normal file
74
modules/bar/popouts/Background.qml
Normal file
|
@ -0,0 +1,74 @@
|
|||
import "root:/services"
|
||||
import "root:/config"
|
||||
import QtQuick
|
||||
import QtQuick.Shapes
|
||||
|
||||
ShapePath {
|
||||
id: root
|
||||
|
||||
required property Wrapper wrapper
|
||||
required property bool invertBottomRounding
|
||||
readonly property real rounding: BorderConfig.rounding
|
||||
readonly property bool flatten: wrapper.width < rounding * 2
|
||||
readonly property real roundingX: flatten ? wrapper.width / 2 : rounding
|
||||
property real ibr: invertBottomRounding ? -1 : 1
|
||||
|
||||
strokeWidth: -1
|
||||
fillColor: BorderConfig.colour
|
||||
|
||||
PathArc {
|
||||
relativeX: root.roundingX
|
||||
relativeY: root.rounding
|
||||
radiusX: Math.min(root.rounding, root.wrapper.width)
|
||||
radiusY: root.rounding
|
||||
direction: PathArc.Counterclockwise
|
||||
}
|
||||
PathLine {
|
||||
relativeX: root.wrapper.width - root.roundingX * 2
|
||||
relativeY: 0
|
||||
}
|
||||
PathArc {
|
||||
relativeX: root.roundingX
|
||||
relativeY: root.rounding
|
||||
radiusX: Math.min(root.rounding, root.wrapper.width)
|
||||
radiusY: root.rounding
|
||||
}
|
||||
PathLine {
|
||||
relativeX: 0
|
||||
relativeY: root.wrapper.height - root.rounding * 2
|
||||
}
|
||||
PathArc {
|
||||
relativeX: -root.roundingX * root.ibr
|
||||
relativeY: root.rounding
|
||||
radiusX: Math.min(root.rounding, root.wrapper.width)
|
||||
radiusY: root.rounding
|
||||
direction: root.ibr < 0 ? PathArc.Counterclockwise : PathArc.Clockwise
|
||||
}
|
||||
PathLine {
|
||||
relativeX: -(root.wrapper.width - root.roundingX - root.roundingX * root.ibr)
|
||||
relativeY: 0
|
||||
}
|
||||
PathArc {
|
||||
relativeX: -root.roundingX
|
||||
relativeY: root.rounding
|
||||
radiusX: Math.min(root.rounding, root.wrapper.width)
|
||||
radiusY: root.rounding
|
||||
direction: PathArc.Counterclockwise
|
||||
}
|
||||
|
||||
Behavior on fillColor {
|
||||
ColorAnimation {
|
||||
duration: Appearance.anim.durations.normal
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: Appearance.anim.curves.standard
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on ibr {
|
||||
NumberAnimation {
|
||||
duration: Appearance.anim.durations.normal
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: Appearance.anim.curves.standard
|
||||
}
|
||||
}
|
||||
}
|
235
modules/bar/popouts/Battery.qml
Normal file
235
modules/bar/popouts/Battery.qml
Normal file
|
@ -0,0 +1,235 @@
|
|||
pragma ComponentBehavior: Bound
|
||||
|
||||
import "root:/widgets"
|
||||
import "root:/services"
|
||||
import "root:/config"
|
||||
import Quickshell.Services.UPower
|
||||
import QtQuick
|
||||
|
||||
Column {
|
||||
id: root
|
||||
|
||||
spacing: Appearance.spacing.normal
|
||||
width: BarConfig.sizes.batteryWidth
|
||||
|
||||
StyledText {
|
||||
text: UPower.displayDevice.isLaptopBattery ? qsTr("Remaining: %1%").arg(Math.round(UPower.displayDevice.percentage * 100)) : qsTr("No battery detected")
|
||||
}
|
||||
|
||||
StyledText {
|
||||
function formatSeconds(s: int, fallback: string): string {
|
||||
const day = Math.floor(s / 86400);
|
||||
const hr = Math.floor(s / 3600) % 60;
|
||||
const min = Math.floor(s / 60) % 60;
|
||||
|
||||
let comps = [];
|
||||
if (day > 0)
|
||||
comps.push(`${day} days`);
|
||||
if (hr > 0)
|
||||
comps.push(`${hr} hours`);
|
||||
if (min > 0)
|
||||
comps.push(`${min} mins`);
|
||||
|
||||
return comps.join(", ") || fallback;
|
||||
}
|
||||
|
||||
text: UPower.displayDevice.isLaptopBattery ? qsTr("Time %1: %2").arg(UPower.onBattery ? "remaining" : "until charged").arg(UPower.onBattery ? formatSeconds(UPower.displayDevice.timeToEmpty, "Calculating...") : formatSeconds(UPower.displayDevice.timeToFull, "Fully charged!")) : qsTr("Power profile: %1").arg(PowerProfile.toString(PowerProfiles.profile))
|
||||
}
|
||||
|
||||
Loader {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
|
||||
active: PowerProfiles.degradationReason !== PerformanceDegradationReason.None
|
||||
asynchronous: true
|
||||
|
||||
height: active ? (item?.implicitHeight ?? 0) : 0
|
||||
|
||||
sourceComponent: StyledRect {
|
||||
implicitWidth: child.implicitWidth + Appearance.padding.normal * 2
|
||||
implicitHeight: child.implicitHeight + Appearance.padding.smaller * 2
|
||||
|
||||
color: Colours.palette.m3error
|
||||
radius: Appearance.rounding.normal
|
||||
|
||||
Column {
|
||||
id: child
|
||||
|
||||
anchors.centerIn: parent
|
||||
|
||||
Row {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
spacing: Appearance.spacing.small
|
||||
|
||||
MaterialIcon {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.verticalCenterOffset: -font.pointSize / 10
|
||||
|
||||
text: "warning"
|
||||
color: Colours.palette.m3onError
|
||||
}
|
||||
|
||||
StyledText {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: qsTr("Performance Degraded")
|
||||
color: Colours.palette.m3onError
|
||||
font.family: Appearance.font.family.mono
|
||||
font.weight: 500
|
||||
}
|
||||
|
||||
MaterialIcon {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.verticalCenterOffset: -font.pointSize / 10
|
||||
|
||||
text: "warning"
|
||||
color: Colours.palette.m3onError
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
|
||||
text: qsTr("Reason: %1").arg(PerformanceDegradationReason.toString(PowerProfiles.degradationReason))
|
||||
color: Colours.palette.m3onError
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StyledRect {
|
||||
id: profiles
|
||||
|
||||
property string current: {
|
||||
const p = PowerProfiles.profile;
|
||||
if (p === PowerProfile.PowerSaver)
|
||||
return saver.icon;
|
||||
if (p === PowerProfile.Performance)
|
||||
return perf.icon;
|
||||
return balance.icon;
|
||||
}
|
||||
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
|
||||
implicitWidth: saver.implicitHeight + balance.implicitHeight + perf.implicitHeight + Appearance.padding.normal * 2 + Appearance.spacing.large * 2
|
||||
implicitHeight: Math.max(saver.implicitHeight, balance.implicitHeight, perf.implicitHeight) + Appearance.padding.small * 2
|
||||
|
||||
color: Colours.palette.m3surfaceContainer
|
||||
radius: Appearance.rounding.full
|
||||
|
||||
StyledRect {
|
||||
id: indicator
|
||||
|
||||
color: Colours.palette.m3primary
|
||||
radius: Appearance.rounding.full
|
||||
state: profiles.current
|
||||
|
||||
states: [
|
||||
State {
|
||||
name: saver.icon
|
||||
|
||||
Fill {
|
||||
item: saver
|
||||
}
|
||||
},
|
||||
State {
|
||||
name: balance.icon
|
||||
|
||||
Fill {
|
||||
item: balance
|
||||
}
|
||||
},
|
||||
State {
|
||||
name: perf.icon
|
||||
|
||||
Fill {
|
||||
item: perf
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
transitions: Transition {
|
||||
AnchorAnimation {
|
||||
duration: Appearance.anim.durations.normal
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: Appearance.anim.curves.emphasized
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Profile {
|
||||
id: saver
|
||||
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Appearance.padding.small
|
||||
|
||||
profile: PowerProfile.PowerSaver
|
||||
icon: "energy_savings_leaf"
|
||||
}
|
||||
|
||||
Profile {
|
||||
id: balance
|
||||
|
||||
anchors.centerIn: parent
|
||||
|
||||
profile: PowerProfile.Balanced
|
||||
icon: "balance"
|
||||
}
|
||||
|
||||
Profile {
|
||||
id: perf
|
||||
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: Appearance.padding.small
|
||||
|
||||
profile: PowerProfile.Performance
|
||||
icon: "rocket_launch"
|
||||
}
|
||||
}
|
||||
|
||||
component Fill: AnchorChanges {
|
||||
required property Item item
|
||||
|
||||
target: indicator
|
||||
anchors.left: item.left
|
||||
anchors.right: item.right
|
||||
anchors.top: item.top
|
||||
anchors.bottom: item.bottom
|
||||
}
|
||||
|
||||
component Profile: Item {
|
||||
required property string icon
|
||||
required property int profile
|
||||
|
||||
implicitWidth: icon.implicitHeight + Appearance.padding.small * 2
|
||||
implicitHeight: icon.implicitHeight + Appearance.padding.small * 2
|
||||
|
||||
StateLayer {
|
||||
radius: Appearance.rounding.full
|
||||
color: profiles.current === parent.icon ? Colours.palette.m3onPrimary : Colours.palette.m3onSurface
|
||||
|
||||
function onClicked(): void {
|
||||
PowerProfiles.profile = parent.profile;
|
||||
}
|
||||
}
|
||||
|
||||
MaterialIcon {
|
||||
id: icon
|
||||
|
||||
anchors.centerIn: parent
|
||||
|
||||
text: parent.icon
|
||||
font.pointSize: Appearance.font.size.large
|
||||
color: profiles.current === text ? Colours.palette.m3onPrimary : Colours.palette.m3onSurface
|
||||
fill: profiles.current === text ? 1 : 0
|
||||
|
||||
Behavior on fill {
|
||||
NumberAnimation {
|
||||
duration: Appearance.anim.durations.normal
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: Appearance.anim.curves.standard
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
18
modules/bar/popouts/Bluetooth.qml
Normal file
18
modules/bar/popouts/Bluetooth.qml
Normal file
|
@ -0,0 +1,18 @@
|
|||
import "root:/widgets"
|
||||
import "root:/services"
|
||||
import "root:/config"
|
||||
import QtQuick
|
||||
|
||||
Column {
|
||||
id: root
|
||||
|
||||
spacing: Appearance.spacing.normal
|
||||
|
||||
StyledText {
|
||||
text: qsTr("Bluetooth %1").arg(Bluetooth.powered ? "enabled" : "disabled")
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: Bluetooth.devices.some(d => d.connected) ? qsTr("Connected to: %1").arg(Bluetooth.devices.filter(d => d.connected).map(d => d.alias).join(", ")) : qsTr("No devices connected")
|
||||
}
|
||||
}
|
175
modules/bar/popouts/Content.qml
Normal file
175
modules/bar/popouts/Content.qml
Normal file
|
@ -0,0 +1,175 @@
|
|||
pragma ComponentBehavior: Bound
|
||||
|
||||
import "root:/services"
|
||||
import "root:/config"
|
||||
import Quickshell
|
||||
import Quickshell.Services.SystemTray
|
||||
import QtQuick
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
required property ShellScreen screen
|
||||
|
||||
property string currentName
|
||||
property real currentCenter
|
||||
property bool hasCurrent
|
||||
|
||||
anchors.centerIn: parent
|
||||
|
||||
implicitWidth: hasCurrent ? (content.children.find(c => c.shouldBeActive)?.implicitWidth ?? 0) + Appearance.padding.large * 2 : 0
|
||||
implicitHeight: (content.children.find(c => c.shouldBeActive)?.implicitHeight ?? 0) + Appearance.padding.large * 2
|
||||
|
||||
Item {
|
||||
id: content
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.margins: Appearance.padding.large
|
||||
|
||||
clip: true
|
||||
|
||||
Popout {
|
||||
name: "activewindow"
|
||||
source: "ActiveWindow.qml"
|
||||
}
|
||||
|
||||
Popout {
|
||||
name: "network"
|
||||
source: "Network.qml"
|
||||
}
|
||||
|
||||
Popout {
|
||||
name: "bluetooth"
|
||||
source: "Bluetooth.qml"
|
||||
}
|
||||
|
||||
Popout {
|
||||
name: "battery"
|
||||
source: "Battery.qml"
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: ScriptModel {
|
||||
values: [...SystemTray.items.values]
|
||||
}
|
||||
|
||||
Popout {
|
||||
id: trayMenu
|
||||
|
||||
required property SystemTrayItem modelData
|
||||
required property int index
|
||||
|
||||
name: `traymenu${index}`
|
||||
sourceComponent: trayMenuComp
|
||||
|
||||
Connections {
|
||||
target: root
|
||||
|
||||
function onHasCurrentChanged(): void {
|
||||
if (root.hasCurrent && trayMenu.shouldBeActive) {
|
||||
trayMenu.sourceComponent = null;
|
||||
trayMenu.sourceComponent = trayMenuComp;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: trayMenuComp
|
||||
|
||||
TrayMenu {
|
||||
popouts: root
|
||||
trayItem: trayMenu.modelData.menu
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on implicitWidth {
|
||||
Anim {
|
||||
easing.bezierCurve: Appearance.anim.curves.emphasized
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on implicitHeight {
|
||||
enabled: root.implicitWidth > 0
|
||||
|
||||
Anim {
|
||||
easing.bezierCurve: Appearance.anim.curves.emphasized
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on currentCenter {
|
||||
enabled: root.implicitWidth > 0
|
||||
|
||||
NumberAnimation {
|
||||
duration: Appearance.anim.durations.normal
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: Appearance.anim.curves.emphasized
|
||||
}
|
||||
}
|
||||
|
||||
component Popout: Loader {
|
||||
id: popout
|
||||
|
||||
required property string name
|
||||
property bool shouldBeActive: root.currentName === name
|
||||
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.right: parent.right
|
||||
|
||||
opacity: 0
|
||||
scale: 0.8
|
||||
active: false
|
||||
asynchronous: true
|
||||
|
||||
states: State {
|
||||
name: "active"
|
||||
when: popout.shouldBeActive
|
||||
|
||||
PropertyChanges {
|
||||
popout.active: true
|
||||
popout.opacity: 1
|
||||
popout.scale: 1
|
||||
}
|
||||
}
|
||||
|
||||
transitions: [
|
||||
Transition {
|
||||
from: "active"
|
||||
to: ""
|
||||
|
||||
SequentialAnimation {
|
||||
Anim {
|
||||
properties: "opacity,scale"
|
||||
duration: Appearance.anim.durations.small
|
||||
}
|
||||
PropertyAction {
|
||||
target: popout
|
||||
property: "active"
|
||||
}
|
||||
}
|
||||
},
|
||||
Transition {
|
||||
from: ""
|
||||
to: "active"
|
||||
|
||||
SequentialAnimation {
|
||||
PropertyAction {
|
||||
target: popout
|
||||
property: "active"
|
||||
}
|
||||
Anim {
|
||||
properties: "opacity,scale"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
component Anim: NumberAnimation {
|
||||
duration: Appearance.anim.durations.normal
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: Appearance.anim.curves.standard
|
||||
}
|
||||
}
|
22
modules/bar/popouts/Network.qml
Normal file
22
modules/bar/popouts/Network.qml
Normal file
|
@ -0,0 +1,22 @@
|
|||
import "root:/widgets"
|
||||
import "root:/services"
|
||||
import "root:/config"
|
||||
import QtQuick
|
||||
|
||||
Column {
|
||||
id: root
|
||||
|
||||
spacing: Appearance.spacing.normal
|
||||
|
||||
StyledText {
|
||||
text: qsTr("Connected to: %1").arg(Network.active?.ssid ?? "None")
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: qsTr("Strength: %1/100").arg(Network.active?.strength ?? 0)
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: qsTr("Frequency: %1 MHz").arg(Network.active?.frequency ?? 0)
|
||||
}
|
||||
}
|
237
modules/bar/popouts/TrayMenu.qml
Normal file
237
modules/bar/popouts/TrayMenu.qml
Normal file
|
@ -0,0 +1,237 @@
|
|||
pragma ComponentBehavior: Bound
|
||||
|
||||
import "root:/widgets"
|
||||
import "root:/services"
|
||||
import "root:/config"
|
||||
import Quickshell
|
||||
import Quickshell.Widgets
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
|
||||
StackView {
|
||||
id: root
|
||||
|
||||
required property Item popouts
|
||||
required property QsMenuHandle trayItem
|
||||
|
||||
implicitWidth: currentItem.implicitWidth
|
||||
implicitHeight: currentItem.implicitHeight
|
||||
|
||||
initialItem: SubMenu {
|
||||
handle: root.trayItem
|
||||
}
|
||||
|
||||
pushEnter: Anim {}
|
||||
pushExit: Anim {}
|
||||
popEnter: Anim {}
|
||||
popExit: Anim {}
|
||||
|
||||
component Anim: Transition {
|
||||
NumberAnimation {
|
||||
duration: 0
|
||||
}
|
||||
}
|
||||
|
||||
component SubMenu: Column {
|
||||
id: menu
|
||||
|
||||
required property QsMenuHandle handle
|
||||
property bool isSubMenu
|
||||
property bool shown
|
||||
|
||||
padding: Appearance.padding.smaller
|
||||
spacing: Appearance.spacing.small
|
||||
|
||||
opacity: shown ? 1 : 0
|
||||
scale: shown ? 1 : 0.8
|
||||
|
||||
Component.onCompleted: shown = true
|
||||
StackView.onActivating: shown = true
|
||||
StackView.onDeactivating: shown = false
|
||||
StackView.onRemoved: destroy()
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: Appearance.anim.durations.normal
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: Appearance.anim.curves.standard
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on scale {
|
||||
NumberAnimation {
|
||||
duration: Appearance.anim.durations.normal
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: Appearance.anim.curves.standard
|
||||
}
|
||||
}
|
||||
|
||||
QsMenuOpener {
|
||||
id: menuOpener
|
||||
|
||||
menu: menu.handle
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: menuOpener.children
|
||||
|
||||
StyledRect {
|
||||
id: item
|
||||
|
||||
required property QsMenuEntry modelData
|
||||
|
||||
implicitWidth: BarConfig.sizes.trayMenuWidth
|
||||
implicitHeight: modelData.isSeparator ? 1 : children.implicitHeight
|
||||
|
||||
radius: Appearance.rounding.full
|
||||
color: modelData.isSeparator ? Colours.palette.m3outlineVariant : "transparent"
|
||||
|
||||
Loader {
|
||||
id: children
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
|
||||
active: !item.modelData.isSeparator
|
||||
asynchronous: true
|
||||
|
||||
sourceComponent: Item {
|
||||
implicitHeight: label.implicitHeight
|
||||
|
||||
StateLayer {
|
||||
anchors.margins: -Appearance.padding.small / 2
|
||||
anchors.leftMargin: -Appearance.padding.smaller
|
||||
anchors.rightMargin: -Appearance.padding.smaller
|
||||
|
||||
radius: item.radius
|
||||
disabled: !item.modelData.enabled
|
||||
|
||||
function onClicked(): void {
|
||||
const entry = item.modelData;
|
||||
if (entry.hasChildren)
|
||||
root.push(subMenuComp.createObject(null, {
|
||||
handle: entry,
|
||||
isSubMenu: true
|
||||
}));
|
||||
else {
|
||||
item.modelData.triggered();
|
||||
root.popouts.hasCurrent = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: icon
|
||||
|
||||
anchors.left: parent.left
|
||||
|
||||
active: item.modelData.icon !== ""
|
||||
asynchronous: true
|
||||
|
||||
sourceComponent: IconImage {
|
||||
implicitSize: label.implicitHeight
|
||||
|
||||
source: item.modelData.icon
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
id: label
|
||||
|
||||
anchors.left: icon.right
|
||||
anchors.leftMargin: icon.active ? Appearance.spacing.smaller : 0
|
||||
|
||||
text: labelMetrics.elidedText
|
||||
color: item.modelData.enabled ? Colours.palette.m3onSurface : Colours.palette.m3outline
|
||||
}
|
||||
|
||||
TextMetrics {
|
||||
id: labelMetrics
|
||||
|
||||
text: item.modelData.text
|
||||
font.pointSize: label.font.pointSize
|
||||
font.family: label.font.family
|
||||
|
||||
elide: Text.ElideRight
|
||||
elideWidth: BarConfig.sizes.trayMenuWidth - (icon.active ? icon.implicitWidth + label.anchors.leftMargin : 0) - (expand.active ? expand.implicitWidth + Appearance.spacing.normal : 0)
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: expand
|
||||
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.right: parent.right
|
||||
|
||||
active: item.modelData.hasChildren
|
||||
asynchronous: true
|
||||
|
||||
sourceComponent: MaterialIcon {
|
||||
text: "chevron_right"
|
||||
color: item.modelData.enabled ? Colours.palette.m3onSurface : Colours.palette.m3outline
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
active: menu.isSubMenu
|
||||
asynchronous: true
|
||||
|
||||
sourceComponent: Item {
|
||||
implicitWidth: back.implicitWidth
|
||||
implicitHeight: back.implicitHeight + Appearance.spacing.small / 2
|
||||
|
||||
Item {
|
||||
anchors.bottom: parent.bottom
|
||||
implicitWidth: back.implicitWidth
|
||||
implicitHeight: back.implicitHeight
|
||||
|
||||
StyledRect {
|
||||
anchors.fill: parent
|
||||
anchors.margins: -Appearance.padding.small / 2
|
||||
anchors.leftMargin: -Appearance.padding.smaller
|
||||
anchors.rightMargin: -Appearance.padding.smaller * 2
|
||||
|
||||
radius: Appearance.rounding.full
|
||||
color: Colours.palette.m3secondaryContainer
|
||||
|
||||
StateLayer {
|
||||
radius: parent.radius
|
||||
color: Colours.palette.m3onSecondaryContainer
|
||||
|
||||
function onClicked(): void {
|
||||
root.pop();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
id: back
|
||||
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
MaterialIcon {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: "chevron_left"
|
||||
color: Colours.palette.m3onSecondaryContainer
|
||||
}
|
||||
|
||||
StyledText {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: qsTr("Back")
|
||||
color: Colours.palette.m3onSecondaryContainer
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: subMenuComp
|
||||
|
||||
SubMenu {}
|
||||
}
|
||||
}
|
25
modules/bar/popouts/Wrapper.qml
Normal file
25
modules/bar/popouts/Wrapper.qml
Normal file
|
@ -0,0 +1,25 @@
|
|||
import "root:/services"
|
||||
import "root:/config"
|
||||
import Quickshell
|
||||
import QtQuick
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
required property ShellScreen screen
|
||||
|
||||
property alias currentName: content.currentName
|
||||
property alias currentCenter: content.currentCenter
|
||||
property alias hasCurrent: content.hasCurrent
|
||||
|
||||
visible: width > 0 && height > 0
|
||||
|
||||
implicitWidth: content.implicitWidth
|
||||
implicitHeight: content.implicitHeight
|
||||
|
||||
Content {
|
||||
id: content
|
||||
|
||||
screen: root.screen
|
||||
}
|
||||
}
|
69
modules/dashboard/Background.qml
Normal file
69
modules/dashboard/Background.qml
Normal file
|
@ -0,0 +1,69 @@
|
|||
import QtQuick
|
||||
import QtQuick.Shapes
|
||||
import "root:/config"
|
||||
import "root:/services"
|
||||
|
||||
ShapePath {
|
||||
id: root
|
||||
|
||||
required property Wrapper wrapper
|
||||
readonly property real rounding: BorderConfig.rounding
|
||||
readonly property bool flatten: wrapper.height < rounding * 2
|
||||
readonly property real roundingY: flatten ? wrapper.height / 2 : rounding
|
||||
|
||||
strokeWidth: -1
|
||||
fillColor: BorderConfig.colour
|
||||
|
||||
PathArc {
|
||||
relativeX: root.rounding
|
||||
relativeY: root.roundingY
|
||||
radiusX: root.rounding
|
||||
radiusY: Math.min(root.rounding, root.wrapper.height)
|
||||
}
|
||||
|
||||
PathLine {
|
||||
relativeX: 0
|
||||
relativeY: root.wrapper.height - root.roundingY * 2
|
||||
}
|
||||
|
||||
PathArc {
|
||||
relativeX: root.rounding
|
||||
relativeY: root.roundingY
|
||||
radiusX: root.rounding
|
||||
radiusY: Math.min(root.rounding, root.wrapper.height)
|
||||
direction: PathArc.Counterclockwise
|
||||
}
|
||||
|
||||
PathLine {
|
||||
relativeX: root.wrapper.width - root.rounding * 2
|
||||
relativeY: 0
|
||||
}
|
||||
|
||||
PathArc {
|
||||
relativeX: root.rounding
|
||||
relativeY: -root.roundingY
|
||||
radiusX: root.rounding
|
||||
radiusY: Math.min(root.rounding, root.wrapper.height)
|
||||
direction: PathArc.Counterclockwise
|
||||
}
|
||||
|
||||
PathLine {
|
||||
relativeX: 0
|
||||
relativeY: -(root.wrapper.height - root.roundingY * 2)
|
||||
}
|
||||
|
||||
PathArc {
|
||||
relativeX: root.rounding
|
||||
relativeY: -root.roundingY
|
||||
radiusX: root.rounding
|
||||
radiusY: Math.min(root.rounding, root.wrapper.height)
|
||||
}
|
||||
|
||||
Behavior on fillColor {
|
||||
ColorAnimation {
|
||||
duration: Appearance.anim.durations.normal
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: Appearance.anim.curves.standard
|
||||
}
|
||||
}
|
||||
}
|
123
modules/dashboard/Content.qml
Normal file
123
modules/dashboard/Content.qml
Normal file
|
@ -0,0 +1,123 @@
|
|||
import "root:/widgets"
|
||||
import "root:/services"
|
||||
import "root:/config"
|
||||
import Quickshell
|
||||
import Quickshell.Widgets
|
||||
import QtQuick
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
required property PersistentProperties visibilities
|
||||
readonly property real nonAnimWidth: view.implicitWidth + viewWrapper.anchors.margins * 2
|
||||
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.bottom: parent.bottom
|
||||
|
||||
implicitWidth: nonAnimWidth
|
||||
implicitHeight: tabs.implicitHeight + tabs.anchors.topMargin + view.implicitHeight + viewWrapper.anchors.margins * 2
|
||||
|
||||
Tabs {
|
||||
id: tabs
|
||||
|
||||
anchors.top: parent.top
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.topMargin: Appearance.padding.normal
|
||||
anchors.margins: Appearance.padding.large
|
||||
|
||||
nonAnimWidth: root.nonAnimWidth
|
||||
currentIndex: view.currentIndex
|
||||
}
|
||||
|
||||
ClippingRectangle {
|
||||
id: viewWrapper
|
||||
|
||||
anchors.top: tabs.bottom
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.margins: Appearance.padding.large
|
||||
|
||||
radius: Appearance.rounding.normal
|
||||
color: "transparent"
|
||||
|
||||
Flickable {
|
||||
id: view
|
||||
|
||||
readonly property int currentIndex: tabs.currentIndex
|
||||
readonly property Item currentItem: row.children[currentIndex]
|
||||
|
||||
anchors.fill: parent
|
||||
|
||||
flickableDirection: Flickable.HorizontalFlick
|
||||
|
||||
implicitWidth: currentItem.implicitWidth
|
||||
implicitHeight: currentItem.implicitHeight
|
||||
|
||||
contentX: currentItem.x
|
||||
contentWidth: row.implicitWidth
|
||||
contentHeight: row.implicitHeight
|
||||
|
||||
onContentXChanged: {
|
||||
if (!moving)
|
||||
return;
|
||||
|
||||
const x = contentX - currentItem.x;
|
||||
if (x > currentItem.implicitWidth / 2)
|
||||
tabs.bar.incrementCurrentIndex();
|
||||
else if (x < -currentItem.implicitWidth / 2)
|
||||
tabs.bar.decrementCurrentIndex();
|
||||
}
|
||||
|
||||
onDragEnded: {
|
||||
const x = contentX - currentItem.x;
|
||||
if (x > currentItem.implicitWidth / 10)
|
||||
tabs.bar.incrementCurrentIndex();
|
||||
else if (x < -currentItem.implicitWidth / 10)
|
||||
tabs.bar.decrementCurrentIndex();
|
||||
else
|
||||
contentX = Qt.binding(() => currentItem.x);
|
||||
}
|
||||
|
||||
Row {
|
||||
id: row
|
||||
|
||||
Dash {
|
||||
shouldUpdate: visible && this === view.currentItem
|
||||
}
|
||||
|
||||
Media {
|
||||
shouldUpdate: visible && this === view.currentItem
|
||||
visibilities: root.visibilities
|
||||
}
|
||||
|
||||
Performance {}
|
||||
}
|
||||
|
||||
Behavior on contentX {
|
||||
NumberAnimation {
|
||||
duration: Appearance.anim.durations.normal
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: Appearance.anim.curves.standard
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on implicitWidth {
|
||||
NumberAnimation {
|
||||
duration: Appearance.anim.durations.large
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: Appearance.anim.curves.emphasized
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on implicitHeight {
|
||||
NumberAnimation {
|
||||
duration: Appearance.anim.durations.large
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: Appearance.anim.curves.emphasized
|
||||
}
|
||||
}
|
||||
}
|
86
modules/dashboard/Dash.qml
Normal file
86
modules/dashboard/Dash.qml
Normal file
|
@ -0,0 +1,86 @@
|
|||
import "root:/widgets"
|
||||
import "root:/services"
|
||||
import "root:/config"
|
||||
import "dash"
|
||||
import QtQuick.Layouts
|
||||
|
||||
GridLayout {
|
||||
id: root
|
||||
|
||||
required property bool shouldUpdate
|
||||
|
||||
rowSpacing: Appearance.spacing.normal
|
||||
columnSpacing: Appearance.spacing.normal
|
||||
|
||||
Rect {
|
||||
Layout.column: 2
|
||||
Layout.columnSpan: 3
|
||||
Layout.preferredWidth: user.implicitWidth
|
||||
Layout.preferredHeight: user.implicitHeight
|
||||
|
||||
User {
|
||||
id: user
|
||||
}
|
||||
}
|
||||
|
||||
Rect {
|
||||
Layout.row: 0
|
||||
Layout.columnSpan: 2
|
||||
Layout.preferredWidth: DashboardConfig.sizes.weatherWidth
|
||||
Layout.fillHeight: true
|
||||
|
||||
Weather {}
|
||||
}
|
||||
|
||||
Rect {
|
||||
Layout.row: 1
|
||||
Layout.preferredWidth: dateTime.implicitWidth
|
||||
Layout.fillHeight: true
|
||||
|
||||
DateTime {
|
||||
id: dateTime
|
||||
}
|
||||
}
|
||||
|
||||
Rect {
|
||||
Layout.row: 1
|
||||
Layout.column: 1
|
||||
Layout.columnSpan: 3
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: calendar.implicitHeight
|
||||
|
||||
Calendar {
|
||||
id: calendar
|
||||
}
|
||||
}
|
||||
|
||||
Rect {
|
||||
Layout.row: 1
|
||||
Layout.column: 4
|
||||
Layout.preferredWidth: resources.implicitWidth
|
||||
Layout.fillHeight: true
|
||||
|
||||
Resources {
|
||||
id: resources
|
||||
}
|
||||
}
|
||||
|
||||
Rect {
|
||||
Layout.row: 0
|
||||
Layout.column: 5
|
||||
Layout.rowSpan: 2
|
||||
Layout.preferredWidth: media.implicitWidth
|
||||
Layout.fillHeight: true
|
||||
|
||||
Media {
|
||||
id: media
|
||||
|
||||
shouldUpdate: root.shouldUpdate
|
||||
}
|
||||
}
|
||||
|
||||
component Rect: StyledRect {
|
||||
radius: Appearance.rounding.small
|
||||
color: Colours.palette.m3surfaceContainer
|
||||
}
|
||||
}
|
594
modules/dashboard/Media.qml
Normal file
594
modules/dashboard/Media.qml
Normal file
|
@ -0,0 +1,594 @@
|
|||
pragma ComponentBehavior: Bound
|
||||
|
||||
import "root:/widgets"
|
||||
import "root:/services"
|
||||
import "root:/utils"
|
||||
import "root:/config"
|
||||
import Quickshell
|
||||
import Quickshell.Widgets
|
||||
import Quickshell.Services.Mpris
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Effects
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
required property bool shouldUpdate
|
||||
required property PersistentProperties visibilities
|
||||
|
||||
property real playerProgress: {
|
||||
const active = Players.active;
|
||||
return active?.length ? active.position / active.length : 0;
|
||||
}
|
||||
|
||||
function lengthStr(length: int): string {
|
||||
if (length < 0)
|
||||
return "-1:-1";
|
||||
return `${Math.floor(length / 60)}:${Math.floor(length % 60).toString().padStart(2, "0")}`;
|
||||
}
|
||||
|
||||
implicitWidth: cover.implicitWidth + DashboardConfig.sizes.mediaVisualiserSize * 2 + details.implicitWidth + details.anchors.leftMargin + bongocat.implicitWidth + bongocat.anchors.leftMargin * 2 + Appearance.padding.large * 2
|
||||
implicitHeight: Math.max(cover.implicitHeight + DashboardConfig.sizes.mediaVisualiserSize * 2, details.implicitHeight, bongocat.implicitHeight) + Appearance.padding.large * 2
|
||||
|
||||
Behavior on playerProgress {
|
||||
NumberAnimation {
|
||||
duration: Appearance.anim.durations.large
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: Appearance.anim.curves.standard
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
running: root.shouldUpdate && (Players.active?.isPlaying ?? false)
|
||||
interval: DashboardConfig.mediaUpdateInterval
|
||||
triggeredOnStart: true
|
||||
repeat: true
|
||||
onTriggered: Players.active?.positionChanged()
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: Cava
|
||||
|
||||
function onValuesChanged(): void {
|
||||
if (root.shouldUpdate)
|
||||
visualiser.requestPaint();
|
||||
}
|
||||
}
|
||||
|
||||
Canvas {
|
||||
id: visualiser
|
||||
|
||||
readonly property real centerX: width / 2
|
||||
readonly property real centerY: height / 2
|
||||
readonly property real innerX: cover.implicitWidth / 2 + Appearance.spacing.small
|
||||
readonly property real innerY: cover.implicitHeight / 2 + Appearance.spacing.small
|
||||
property color colour: Colours.palette.m3primary
|
||||
|
||||
anchors.fill: cover
|
||||
anchors.margins: -DashboardConfig.sizes.mediaVisualiserSize
|
||||
|
||||
onColourChanged: requestPaint()
|
||||
|
||||
onPaint: {
|
||||
const ctx = getContext("2d");
|
||||
ctx.reset();
|
||||
|
||||
const values = Cava.values;
|
||||
const len = values.length;
|
||||
|
||||
ctx.strokeStyle = colour;
|
||||
ctx.lineWidth = 360 / len - Appearance.spacing.small / 4;
|
||||
ctx.lineCap = "round";
|
||||
|
||||
const size = DashboardConfig.sizes.mediaVisualiserSize;
|
||||
const cx = centerX;
|
||||
const cy = centerY;
|
||||
const rx = innerX + ctx.lineWidth / 2;
|
||||
const ry = innerY + ctx.lineWidth / 2;
|
||||
|
||||
for (let i = 0; i < len; i++) {
|
||||
const v = Math.max(1, Math.min(100, values[i]));
|
||||
|
||||
const angle = i * 2 * Math.PI / len;
|
||||
const magnitude = v / 100 * size;
|
||||
const cos = Math.cos(angle);
|
||||
const sin = Math.sin(angle);
|
||||
|
||||
ctx.moveTo(cx + rx * cos, cy + ry * sin);
|
||||
ctx.lineTo(cx + (rx + magnitude) * cos, cy + (ry + magnitude) * sin);
|
||||
}
|
||||
|
||||
ctx.stroke();
|
||||
}
|
||||
|
||||
Behavior on colour {
|
||||
ColorAnimation {
|
||||
duration: Appearance.anim.durations.normal
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: Appearance.anim.curves.standard
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StyledClippingRect {
|
||||
id: cover
|
||||
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Appearance.padding.large + DashboardConfig.sizes.mediaVisualiserSize
|
||||
|
||||
implicitWidth: DashboardConfig.sizes.mediaCoverArtSize
|
||||
implicitHeight: DashboardConfig.sizes.mediaCoverArtSize
|
||||
|
||||
color: Colours.palette.m3surfaceContainerHigh
|
||||
radius: Appearance.rounding.full
|
||||
|
||||
MaterialIcon {
|
||||
anchors.centerIn: parent
|
||||
|
||||
text: "art_track"
|
||||
color: Colours.palette.m3onSurfaceVariant
|
||||
font.pointSize: (parent.width * 0.4) || 1
|
||||
}
|
||||
|
||||
Image {
|
||||
id: image
|
||||
|
||||
anchors.fill: parent
|
||||
|
||||
source: Players.active?.trackArtUrl ?? ""
|
||||
asynchronous: true
|
||||
fillMode: Image.PreserveAspectCrop
|
||||
sourceSize.width: width
|
||||
sourceSize.height: height
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
id: details
|
||||
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.left: visualiser.right
|
||||
anchors.leftMargin: Appearance.spacing.normal
|
||||
|
||||
spacing: Appearance.spacing.small
|
||||
|
||||
StyledText {
|
||||
id: title
|
||||
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
|
||||
animate: true
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
text: (Players.active?.trackTitle ?? qsTr("No media")) || qsTr("Unknown title")
|
||||
color: Colours.palette.m3primary
|
||||
font.pointSize: Appearance.font.size.normal
|
||||
|
||||
width: parent.implicitWidth
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
|
||||
StyledText {
|
||||
id: album
|
||||
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
|
||||
animate: true
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
text: (Players.active?.trackAlbum ?? qsTr("No media")) || qsTr("Unknown album")
|
||||
color: Colours.palette.m3outline
|
||||
font.pointSize: Appearance.font.size.small
|
||||
|
||||
width: parent.implicitWidth
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
|
||||
StyledText {
|
||||
id: artist
|
||||
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
|
||||
animate: true
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
text: (Players.active?.trackArtist ?? qsTr("No media")) || qsTr("Unknown artist")
|
||||
color: Colours.palette.m3secondary
|
||||
|
||||
width: parent.implicitWidth
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
|
||||
Row {
|
||||
id: controls
|
||||
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
|
||||
spacing: Appearance.spacing.small
|
||||
|
||||
Control {
|
||||
icon: "skip_previous"
|
||||
canUse: Players.active?.canGoPrevious ?? false
|
||||
|
||||
function onClicked(): void {
|
||||
Players.active?.previous();
|
||||
}
|
||||
}
|
||||
|
||||
Control {
|
||||
icon: Players.active?.isPlaying ? "pause" : "play_arrow"
|
||||
canUse: Players.active?.canTogglePlaying ?? false
|
||||
primary: true
|
||||
|
||||
function onClicked(): void {
|
||||
Players.active?.togglePlaying();
|
||||
}
|
||||
}
|
||||
|
||||
Control {
|
||||
icon: "skip_next"
|
||||
canUse: Players.active?.canGoNext ?? false
|
||||
|
||||
function onClicked(): void {
|
||||
Players.active?.next();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Slider {
|
||||
id: slider
|
||||
|
||||
implicitWidth: controls.implicitWidth * 1.5
|
||||
implicitHeight: Appearance.padding.normal * 3
|
||||
|
||||
value: root.playerProgress
|
||||
onMoved: {
|
||||
const active = Players.active;
|
||||
if (active?.canSeek && active?.positionSupported)
|
||||
active.position = value * active.length;
|
||||
}
|
||||
|
||||
background: Item {
|
||||
StyledRect {
|
||||
anchors.top: parent.top
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.left: parent.left
|
||||
anchors.topMargin: slider.implicitHeight / 3
|
||||
anchors.bottomMargin: slider.implicitHeight / 3
|
||||
|
||||
implicitWidth: slider.handle.x - slider.implicitHeight / 6
|
||||
|
||||
color: Colours.palette.m3primary
|
||||
radius: Appearance.rounding.full
|
||||
topRightRadius: slider.implicitHeight / 15
|
||||
bottomRightRadius: slider.implicitHeight / 15
|
||||
}
|
||||
|
||||
StyledRect {
|
||||
anchors.top: parent.top
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.right: parent.right
|
||||
anchors.topMargin: slider.implicitHeight / 3
|
||||
anchors.bottomMargin: slider.implicitHeight / 3
|
||||
|
||||
implicitWidth: parent.width - slider.handle.x - slider.handle.implicitWidth - slider.implicitHeight / 6
|
||||
|
||||
color: Colours.palette.m3surfaceContainer
|
||||
radius: Appearance.rounding.full
|
||||
topLeftRadius: slider.implicitHeight / 15
|
||||
bottomLeftRadius: slider.implicitHeight / 15
|
||||
}
|
||||
}
|
||||
|
||||
handle: StyledRect {
|
||||
id: rect
|
||||
|
||||
x: slider.visualPosition * slider.availableWidth
|
||||
|
||||
implicitWidth: slider.implicitHeight / 4.5
|
||||
implicitHeight: slider.implicitHeight
|
||||
|
||||
color: Colours.palette.m3primary
|
||||
radius: Appearance.rounding.full
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onPressed: event => event.accepted = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
|
||||
implicitHeight: Math.max(position.implicitHeight, length.implicitHeight)
|
||||
|
||||
StyledText {
|
||||
id: position
|
||||
|
||||
anchors.left: parent.left
|
||||
|
||||
text: root.lengthStr(Players.active?.position ?? -1)
|
||||
color: Colours.palette.m3onSurfaceVariant
|
||||
font.pointSize: Appearance.font.size.small
|
||||
}
|
||||
|
||||
StyledText {
|
||||
id: length
|
||||
|
||||
anchors.right: parent.right
|
||||
|
||||
text: root.lengthStr(Players.active?.length ?? -1)
|
||||
color: Colours.palette.m3onSurfaceVariant
|
||||
font.pointSize: Appearance.font.size.small
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
|
||||
spacing: Appearance.spacing.small
|
||||
|
||||
Control {
|
||||
icon: "flip_to_front"
|
||||
canUse: Players.active?.canRaise ?? false
|
||||
fontSize: Appearance.font.size.larger
|
||||
padding: Appearance.padding.small
|
||||
fill: false
|
||||
color: Colours.palette.m3surfaceContainer
|
||||
|
||||
function onClicked(): void {
|
||||
Players.active?.raise();
|
||||
root.visibilities.dashboard = false;
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: playerSelector
|
||||
|
||||
property bool expanded
|
||||
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
implicitWidth: slider.implicitWidth / 2
|
||||
implicitHeight: currentPlayer.implicitHeight + Appearance.padding.small * 2
|
||||
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: expanded = !expanded
|
||||
|
||||
RectangularShadow {
|
||||
anchors.fill: playerSelectorBg
|
||||
|
||||
opacity: playerSelector.expanded ? 1 : 0
|
||||
radius: playerSelectorBg.radius
|
||||
color: Colours.palette.m3shadow
|
||||
blur: 5
|
||||
spread: 0
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: Appearance.anim.durations.normal
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: Appearance.anim.curves.standard
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StyledRect {
|
||||
id: playerSelectorBg
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.bottom: parent.bottom
|
||||
|
||||
implicitHeight: playersWrapper.implicitHeight + Appearance.padding.small * 2
|
||||
|
||||
color: Colours.palette.m3secondaryContainer
|
||||
radius: Appearance.rounding.normal
|
||||
|
||||
Item {
|
||||
id: playersWrapper
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.margins: Appearance.padding.small
|
||||
|
||||
clip: true
|
||||
implicitHeight: playerSelector.expanded && Players.list.length > 1 ? players.implicitHeight : currentPlayer.implicitHeight
|
||||
|
||||
Column {
|
||||
id: players
|
||||
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.bottom: parent.bottom
|
||||
|
||||
spacing: Appearance.spacing.small
|
||||
|
||||
Repeater {
|
||||
model: Players.list.filter(p => p !== Players.active)
|
||||
|
||||
Row {
|
||||
id: player
|
||||
|
||||
required property MprisPlayer modelData
|
||||
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
spacing: Appearance.spacing.small
|
||||
|
||||
IconImage {
|
||||
id: playerIcon
|
||||
|
||||
source: Icons.getAppIcon(player.modelData.identity, "image-missing")
|
||||
implicitSize: Math.round(identity.implicitHeight * 0.9)
|
||||
}
|
||||
|
||||
StyledText {
|
||||
id: identity
|
||||
|
||||
text: identityMetrics.elidedText
|
||||
color: Colours.palette.m3onSecondaryContainer
|
||||
|
||||
TextMetrics {
|
||||
id: identityMetrics
|
||||
|
||||
text: player.modelData.identity
|
||||
font.family: identity.font.family
|
||||
font.pointSize: identity.font.pointSize
|
||||
elide: Text.ElideRight
|
||||
elideWidth: playerSelector.implicitWidth - playerIcon.implicitWidth - player.spacing - Appearance.padding.smaller * 2
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
|
||||
anchors.fill: parent
|
||||
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
Players.manualActive = player.modelData;
|
||||
playerSelector.expanded = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
implicitHeight: 1
|
||||
|
||||
StyledRect {
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.margins: -Appearance.padding.normal
|
||||
color: Colours.palette.m3secondary
|
||||
implicitHeight: 1
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
id: currentPlayer
|
||||
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
spacing: Appearance.spacing.small
|
||||
|
||||
IconImage {
|
||||
id: currentIcon
|
||||
|
||||
source: Icons.getAppIcon(Players.active?.identity ?? "", "multimedia-player")
|
||||
implicitSize: Math.round(currentIdentity.implicitHeight * 0.9)
|
||||
}
|
||||
|
||||
StyledText {
|
||||
id: currentIdentity
|
||||
|
||||
animate: true
|
||||
text: currentIdentityMetrics.elidedText
|
||||
color: Colours.palette.m3onSecondaryContainer
|
||||
|
||||
TextMetrics {
|
||||
id: currentIdentityMetrics
|
||||
|
||||
text: Players.active?.identity ?? "No players"
|
||||
font.family: currentIdentity.font.family
|
||||
font.pointSize: currentIdentity.font.pointSize
|
||||
elide: Text.ElideRight
|
||||
elideWidth: playerSelector.implicitWidth - currentIcon.implicitWidth - currentPlayer.spacing - Appearance.padding.smaller * 2
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on implicitHeight {
|
||||
NumberAnimation {
|
||||
duration: Appearance.anim.durations.normal
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: Appearance.anim.curves.emphasized
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Control {
|
||||
icon: "delete"
|
||||
canUse: Players.active?.canQuit ?? false
|
||||
fontSize: Appearance.font.size.larger
|
||||
padding: Appearance.padding.small
|
||||
fill: false
|
||||
color: Colours.palette.m3surfaceContainer
|
||||
|
||||
function onClicked(): void {
|
||||
Players.active?.quit();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
id: bongocat
|
||||
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.left: details.right
|
||||
anchors.leftMargin: Appearance.spacing.normal
|
||||
|
||||
implicitWidth: visualiser.width
|
||||
implicitHeight: visualiser.height
|
||||
|
||||
AnimatedImage {
|
||||
anchors.centerIn: parent
|
||||
|
||||
width: visualiser.width * 0.75
|
||||
height: visualiser.height * 0.75
|
||||
|
||||
playing: root.shouldUpdate && (Players.active?.isPlaying ?? false)
|
||||
speed: BeatDetector.bpm / 300
|
||||
source: "root:/assets/bongocat.gif"
|
||||
asynchronous: true
|
||||
fillMode: AnimatedImage.PreserveAspectFit
|
||||
}
|
||||
}
|
||||
component Control: StyledRect {
|
||||
id: control
|
||||
|
||||
required property string icon
|
||||
required property bool canUse
|
||||
property int fontSize: Appearance.font.size.extraLarge
|
||||
property int padding
|
||||
property bool fill: true
|
||||
property bool primary
|
||||
function onClicked(): void {
|
||||
}
|
||||
|
||||
implicitWidth: Math.max(icon.implicitWidth, icon.implicitHeight) + padding * 2
|
||||
implicitHeight: implicitWidth
|
||||
|
||||
radius: Appearance.rounding.full
|
||||
color: primary && canUse ? Colours.palette.m3primary : "transparent"
|
||||
|
||||
StateLayer {
|
||||
disabled: !control.canUse
|
||||
radius: parent.radius
|
||||
color: control.primary ? Colours.palette.m3onPrimary : Colours.palette.m3onSurface
|
||||
|
||||
function onClicked(): void {
|
||||
control.onClicked();
|
||||
}
|
||||
}
|
||||
|
||||
MaterialIcon {
|
||||
id: icon
|
||||
|
||||
anchors.centerIn: parent
|
||||
anchors.verticalCenterOffset: font.pointSize * 0.05
|
||||
|
||||
animate: true
|
||||
fill: control.fill ? 1 : 0
|
||||
text: control.icon
|
||||
color: control.canUse ? control.primary ? Colours.palette.m3onPrimary : Colours.palette.m3onSurface : Colours.palette.m3outline
|
||||
font.pointSize: control.fontSize
|
||||
}
|
||||
}
|
||||
}
|
230
modules/dashboard/Performance.qml
Normal file
230
modules/dashboard/Performance.qml
Normal file
|
@ -0,0 +1,230 @@
|
|||
import "root:/widgets"
|
||||
import "root:/services"
|
||||
import "root:/config"
|
||||
import QtQuick
|
||||
|
||||
Row {
|
||||
id: root
|
||||
|
||||
spacing: Appearance.spacing.large * 3
|
||||
padding: Appearance.padding.large
|
||||
leftPadding: padding * 2
|
||||
rightPadding: padding * 3
|
||||
|
||||
Resource {
|
||||
value1: Math.min(1, SystemUsage.gpuTemp / 90)
|
||||
value2: SystemUsage.gpuPerc
|
||||
|
||||
label1: `${Math.ceil(SystemUsage.gpuTemp)}°C`
|
||||
label2: `${Math.round(SystemUsage.gpuPerc * 100)}%`
|
||||
|
||||
sublabel1: qsTr("GPU temp")
|
||||
sublabel2: qsTr("Usage")
|
||||
}
|
||||
|
||||
Resource {
|
||||
primary: true
|
||||
|
||||
value1: Math.min(1, SystemUsage.cpuTemp / 90)
|
||||
value2: SystemUsage.cpuPerc
|
||||
|
||||
label1: `${Math.ceil(SystemUsage.cpuTemp)}°C`
|
||||
label2: `${Math.round(SystemUsage.cpuPerc * 100)}%`
|
||||
|
||||
sublabel1: qsTr("CPU temp")
|
||||
sublabel2: qsTr("Usage")
|
||||
}
|
||||
|
||||
Resource {
|
||||
value1: SystemUsage.memPerc
|
||||
value2: SystemUsage.storagePerc
|
||||
|
||||
label1: {
|
||||
const fmt = SystemUsage.formatKib(SystemUsage.memUsed);
|
||||
return `${+fmt.value.toFixed(1)}${fmt.unit}`;
|
||||
}
|
||||
label2: {
|
||||
const fmt = SystemUsage.formatKib(SystemUsage.storageUsed);
|
||||
return `${Math.floor(fmt.value)}${fmt.unit}`;
|
||||
}
|
||||
|
||||
sublabel1: qsTr("Memory")
|
||||
sublabel2: qsTr("Storage")
|
||||
}
|
||||
|
||||
component Resource: Item {
|
||||
id: res
|
||||
|
||||
required property real value1
|
||||
required property real value2
|
||||
required property string sublabel1
|
||||
required property string sublabel2
|
||||
required property string label1
|
||||
required property string label2
|
||||
|
||||
property bool primary
|
||||
readonly property real primaryMult: primary ? 1.2 : 1
|
||||
|
||||
readonly property real thickness: DashboardConfig.sizes.resourceProgessThickness * primaryMult
|
||||
|
||||
property color fg1: Colours.palette.m3primary
|
||||
property color fg2: Colours.palette.m3secondary
|
||||
property color bg1: Colours.palette.m3primaryContainer
|
||||
property color bg2: Colours.palette.m3secondaryContainer
|
||||
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
implicitWidth: DashboardConfig.sizes.resourceSize * primaryMult
|
||||
implicitHeight: DashboardConfig.sizes.resourceSize * primaryMult
|
||||
|
||||
onValue1Changed: canvas.requestPaint()
|
||||
onValue2Changed: canvas.requestPaint()
|
||||
onFg1Changed: canvas.requestPaint()
|
||||
onFg2Changed: canvas.requestPaint()
|
||||
onBg1Changed: canvas.requestPaint()
|
||||
onBg2Changed: canvas.requestPaint()
|
||||
|
||||
Column {
|
||||
anchors.centerIn: parent
|
||||
|
||||
StyledText {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
|
||||
text: res.label1
|
||||
font.pointSize: Appearance.font.size.extraLarge * res.primaryMult
|
||||
}
|
||||
|
||||
StyledText {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
|
||||
text: res.sublabel1
|
||||
color: Colours.palette.m3onSurfaceVariant
|
||||
font.pointSize: Appearance.font.size.smaller * res.primaryMult
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
anchors.horizontalCenter: parent.right
|
||||
anchors.top: parent.verticalCenter
|
||||
anchors.horizontalCenterOffset: -res.thickness / 2
|
||||
anchors.topMargin: res.thickness / 2 + Appearance.spacing.small
|
||||
|
||||
StyledText {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
|
||||
text: res.label2
|
||||
font.pointSize: Appearance.font.size.smaller * res.primaryMult
|
||||
}
|
||||
|
||||
StyledText {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
|
||||
text: res.sublabel2
|
||||
color: Colours.palette.m3onSurfaceVariant
|
||||
font.pointSize: Appearance.font.size.small * res.primaryMult
|
||||
}
|
||||
}
|
||||
|
||||
Canvas {
|
||||
id: canvas
|
||||
|
||||
readonly property real centerX: width / 2
|
||||
readonly property real centerY: height / 2
|
||||
|
||||
readonly property real arc1Start: degToRad(45)
|
||||
readonly property real arc1End: degToRad(220)
|
||||
readonly property real arc2Start: degToRad(230)
|
||||
readonly property real arc2End: degToRad(360)
|
||||
|
||||
function degToRad(deg: int): real {
|
||||
return deg * Math.PI / 180;
|
||||
}
|
||||
|
||||
anchors.fill: parent
|
||||
|
||||
onPaint: {
|
||||
const ctx = getContext("2d");
|
||||
ctx.reset();
|
||||
|
||||
ctx.lineWidth = res.thickness;
|
||||
ctx.lineCap = "round";
|
||||
|
||||
const radius = (Math.min(width, height) - ctx.lineWidth) / 2;
|
||||
const cx = centerX;
|
||||
const cy = centerY;
|
||||
const a1s = arc1Start;
|
||||
const a1e = arc1End;
|
||||
const a2s = arc2Start;
|
||||
const a2e = arc2End;
|
||||
|
||||
ctx.beginPath();
|
||||
ctx.arc(cx, cy, radius, a1s, a1e, false);
|
||||
ctx.strokeStyle = res.bg1;
|
||||
ctx.stroke();
|
||||
|
||||
ctx.beginPath();
|
||||
ctx.arc(cx, cy, radius, a1s, (a1e - a1s) * res.value1 + a1s, false);
|
||||
ctx.strokeStyle = res.fg1;
|
||||
ctx.stroke();
|
||||
|
||||
ctx.beginPath();
|
||||
ctx.arc(cx, cy, radius, a2s, a2e, false);
|
||||
ctx.strokeStyle = res.bg2;
|
||||
ctx.stroke();
|
||||
|
||||
ctx.beginPath();
|
||||
ctx.arc(cx, cy, radius, a2s, (a2e - a2s) * res.value2 + a2s, false);
|
||||
ctx.strokeStyle = res.fg2;
|
||||
ctx.stroke();
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on value1 {
|
||||
NumberAnimation {
|
||||
duration: Appearance.anim.durations.normal
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: Appearance.anim.curves.standard
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on value2 {
|
||||
NumberAnimation {
|
||||
duration: Appearance.anim.durations.normal
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: Appearance.anim.curves.standard
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on fg1 {
|
||||
ColorAnimation {
|
||||
duration: Appearance.anim.durations.normal
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: Appearance.anim.curves.standard
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on fg2 {
|
||||
ColorAnimation {
|
||||
duration: Appearance.anim.durations.normal
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: Appearance.anim.curves.standard
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on bg1 {
|
||||
ColorAnimation {
|
||||
duration: Appearance.anim.durations.normal
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: Appearance.anim.curves.standard
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on bg2 {
|
||||
ColorAnimation {
|
||||
duration: Appearance.anim.durations.normal
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: Appearance.anim.curves.standard
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
249
modules/dashboard/Tabs.qml
Normal file
249
modules/dashboard/Tabs.qml
Normal file
|
@ -0,0 +1,249 @@
|
|||
import "root:/widgets"
|
||||
import "root:/services"
|
||||
import "root:/config"
|
||||
import Quickshell.Widgets
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
required property real nonAnimWidth
|
||||
property alias currentIndex: bar.currentIndex
|
||||
readonly property TabBar bar: bar
|
||||
|
||||
implicitHeight: bar.implicitHeight + indicator.implicitHeight + indicator.anchors.topMargin + separator.implicitHeight
|
||||
|
||||
TabBar {
|
||||
id: bar
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
|
||||
background: null
|
||||
|
||||
Tab {
|
||||
iconName: "dashboard"
|
||||
text: qsTr("Dashboard")
|
||||
}
|
||||
|
||||
Tab {
|
||||
iconName: "queue_music"
|
||||
text: qsTr("Media")
|
||||
}
|
||||
|
||||
Tab {
|
||||
iconName: "speed"
|
||||
text: qsTr("Performance")
|
||||
}
|
||||
|
||||
Tab {
|
||||
iconName: "workspaces"
|
||||
text: qsTr("Workspaces")
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
id: indicator
|
||||
|
||||
anchors.top: bar.bottom
|
||||
anchors.topMargin: DashboardConfig.sizes.tabIndicatorSpacing
|
||||
|
||||
implicitWidth: bar.currentItem.implicitWidth
|
||||
implicitHeight: DashboardConfig.sizes.tabIndicatorHeight
|
||||
|
||||
x: {
|
||||
const tab = bar.currentItem;
|
||||
const width = (root.nonAnimWidth - DashboardConfig.sizes.tabIndicatorSpacing * (bar.count - 1) * 2) / bar.count
|
||||
return width * tab.TabBar.index + (width - tab.implicitWidth) / 2;
|
||||
}
|
||||
|
||||
clip: true
|
||||
|
||||
StyledRect {
|
||||
anchors.top: parent.top
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
implicitHeight: parent.implicitHeight * 2
|
||||
|
||||
color: Colours.palette.m3primary
|
||||
radius: Appearance.rounding.full
|
||||
|
||||
}
|
||||
|
||||
Behavior on x {
|
||||
Anim {}
|
||||
}
|
||||
|
||||
Behavior on implicitWidth {
|
||||
Anim {}
|
||||
}
|
||||
}
|
||||
|
||||
StyledRect {
|
||||
id: separator
|
||||
|
||||
anchors.top: indicator.bottom
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
|
||||
implicitHeight: 1
|
||||
color: Colours.palette.m3outlineVariant
|
||||
}
|
||||
|
||||
component Tab: TabButton {
|
||||
id: tab
|
||||
|
||||
required property string iconName
|
||||
readonly property bool current: TabBar.tabBar.currentItem === this
|
||||
|
||||
background: null
|
||||
|
||||
contentItem: MouseArea {
|
||||
id: mouse
|
||||
|
||||
implicitWidth: Math.max(icon.width, label.width)
|
||||
implicitHeight: icon.height + label.height
|
||||
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
|
||||
onPressed: ({x,y}) => {
|
||||
tab.TabBar.tabBar.setCurrentIndex(tab.TabBar.index);
|
||||
|
||||
const stateY = stateWrapper.y;
|
||||
rippleAnim.x = x;
|
||||
rippleAnim.y = y - stateY;
|
||||
|
||||
const dist = (ox,oy) => ox * ox + oy * oy;
|
||||
const stateEndY = stateY + stateWrapper.height;
|
||||
rippleAnim.radius = Math.sqrt(Math.max(dist(0, stateY), dist(0, stateEndY), dist(width, stateY), dist(width, stateEndY)));
|
||||
|
||||
rippleAnim.restart();
|
||||
}
|
||||
onWheel: event => {
|
||||
if (event.angleDelta.y < 0)
|
||||
tab.TabBar.tabBar.incrementCurrentIndex();
|
||||
else if (event.angleDelta.y > 0)
|
||||
tab.TabBar.tabBar.decrementCurrentIndex();
|
||||
}
|
||||
|
||||
SequentialAnimation {
|
||||
id: rippleAnim
|
||||
|
||||
property real x
|
||||
property real y
|
||||
property real radius
|
||||
|
||||
PropertyAction {
|
||||
target: ripple
|
||||
property: "x"
|
||||
value: rippleAnim.x
|
||||
}
|
||||
PropertyAction {
|
||||
target: ripple
|
||||
property: "y"
|
||||
value: rippleAnim.y
|
||||
}
|
||||
PropertyAction {
|
||||
target: ripple
|
||||
property: "opacity"
|
||||
value: 0.1
|
||||
}
|
||||
ParallelAnimation {
|
||||
Anim {
|
||||
target: ripple
|
||||
properties: "implicitWidth,implicitHeight"
|
||||
from: 0
|
||||
to: rippleAnim.radius * 2
|
||||
duration: Appearance.anim.durations.large
|
||||
easing.bezierCurve: Appearance.anim.curves.standardDecel
|
||||
}
|
||||
Anim {
|
||||
target: ripple
|
||||
property: "opacity"
|
||||
to: 0
|
||||
duration: Appearance.anim.durations.large
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: Appearance.anim.curves.standardDecel
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ClippingRectangle {
|
||||
id: stateWrapper
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
implicitHeight: parent.height + DashboardConfig.sizes.tabIndicatorSpacing * 2
|
||||
|
||||
color: "transparent"
|
||||
radius: Appearance.rounding.small
|
||||
|
||||
StyledRect {
|
||||
id: stateLayer
|
||||
|
||||
anchors.fill: parent
|
||||
|
||||
color: tab.current ? Colours.palette.m3primary : Colours.palette.m3onSurface
|
||||
opacity: mouse.pressed ? 0.1 : tab.hovered ? 0.08 : 0
|
||||
|
||||
Behavior on opacity {
|
||||
Anim {}
|
||||
}
|
||||
}
|
||||
|
||||
StyledRect {
|
||||
id: ripple
|
||||
|
||||
radius: Appearance.rounding.full
|
||||
color: tab.current ? Colours.palette.m3primary : Colours.palette.m3onSurface
|
||||
opacity: 0
|
||||
|
||||
transform: Translate {
|
||||
x: -ripple.width / 2
|
||||
y: -ripple.height / 2
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MaterialIcon {
|
||||
id: icon
|
||||
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.bottom: label.top
|
||||
|
||||
text: tab.iconName
|
||||
color: tab.current ? Colours.palette.m3primary : Colours.palette.m3onSurfaceVariant
|
||||
fill: tab.current ? 1 : 0
|
||||
font.pointSize: Appearance.font.size.large
|
||||
|
||||
Behavior on fill {
|
||||
NumberAnimation {
|
||||
duration: Appearance.anim.durations.normal
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: Appearance.anim.curves.standard
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
id: label
|
||||
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.bottom: parent.bottom
|
||||
|
||||
text: tab.text
|
||||
color: tab.current ? Colours.palette.m3primary : Colours.palette.m3onSurfaceVariant
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
component Anim: NumberAnimation {
|
||||
duration: Appearance.anim.durations.normal
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: Appearance.anim.curves.standard
|
||||
}
|
||||
}
|
55
modules/dashboard/Wrapper.qml
Normal file
55
modules/dashboard/Wrapper.qml
Normal file
|
@ -0,0 +1,55 @@
|
|||
import QtQuick
|
||||
import Quickshell
|
||||
import "root:/config"
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
required property PersistentProperties visibilities
|
||||
|
||||
visible: height > 0
|
||||
implicitHeight: 0
|
||||
implicitWidth: content.implicitWidth
|
||||
|
||||
states: State {
|
||||
name: "visible"
|
||||
when: root.visibilities.dashboard
|
||||
|
||||
PropertyChanges {
|
||||
root.implicitHeight: content.implicitHeight
|
||||
}
|
||||
}
|
||||
|
||||
transitions: [
|
||||
Transition {
|
||||
from: ""
|
||||
to: "visible"
|
||||
|
||||
NumberAnimation {
|
||||
target: root
|
||||
property: "implicitHeight"
|
||||
duration: Appearance.anim.durations.expressiveDefaultSpatial
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial
|
||||
}
|
||||
},
|
||||
Transition {
|
||||
from: "visible"
|
||||
to: ""
|
||||
|
||||
NumberAnimation {
|
||||
target: root
|
||||
property: "implicitHeight"
|
||||
duration: Appearance.anim.durations.normal
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: Appearance.anim.curves.emphasized
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
Content {
|
||||
id: content
|
||||
|
||||
visibilities: root.visibilities
|
||||
}
|
||||
}
|
70
modules/dashboard/dash/Calendar.qml
Normal file
70
modules/dashboard/dash/Calendar.qml
Normal file
|
@ -0,0 +1,70 @@
|
|||
import "root:/widgets"
|
||||
import "root:/services"
|
||||
import "root:/config"
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
|
||||
Column {
|
||||
id: root
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
padding: Appearance.padding.large
|
||||
spacing: Appearance.spacing.small
|
||||
|
||||
DayOfWeekRow {
|
||||
id: days
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.margins: parent.padding
|
||||
|
||||
delegate: StyledText {
|
||||
required property var model
|
||||
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
text: model.shortName
|
||||
font.family: Appearance.font.family.sans
|
||||
font.weight: 500
|
||||
}
|
||||
}
|
||||
|
||||
MonthGrid {
|
||||
id: grid
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.margins: parent.padding
|
||||
|
||||
spacing: 3
|
||||
|
||||
delegate: Item {
|
||||
id: day
|
||||
|
||||
required property var model
|
||||
|
||||
implicitWidth: implicitHeight
|
||||
implicitHeight: text.implicitHeight + Appearance.padding.small * 2
|
||||
|
||||
StyledRect {
|
||||
anchors.centerIn: parent
|
||||
|
||||
implicitWidth: parent.implicitHeight
|
||||
implicitHeight: parent.implicitHeight
|
||||
|
||||
radius: Appearance.rounding.full
|
||||
color: model.today ? Colours.palette.m3primary : "transparent"
|
||||
|
||||
StyledText {
|
||||
id: text
|
||||
|
||||
anchors.centerIn: parent
|
||||
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
text: grid.locale.toString(day.model.date, "d")
|
||||
color: day.model.today ? Colours.palette.m3onPrimary : day.model.month === grid.month ? Colours.palette.m3onSurfaceVariant : Colours.palette.m3outline
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
71
modules/dashboard/dash/DateTime.qml
Normal file
71
modules/dashboard/dash/DateTime.qml
Normal file
|
@ -0,0 +1,71 @@
|
|||
import "root:/widgets"
|
||||
import "root:/services"
|
||||
import "root:/config"
|
||||
import QtQuick
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
anchors.top: parent.top
|
||||
anchors.bottom: parent.bottom
|
||||
implicitWidth: DashboardConfig.sizes.dateTimeWidth
|
||||
|
||||
StyledText {
|
||||
id: hours
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
anchors.topMargin: (root.height - (hours.implicitHeight + sep.implicitHeight + sep.anchors.topMargin + mins.implicitHeight + mins.anchors.topMargin + date.implicitHeight + date.anchors.topMargin)) / 2
|
||||
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
text: Time.format("HH")
|
||||
color: Colours.palette.m3secondary
|
||||
font.pointSize: Appearance.font.size.extraLarge
|
||||
font.weight: 500
|
||||
}
|
||||
|
||||
StyledText {
|
||||
id: sep
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: hours.bottom
|
||||
anchors.topMargin: -font.pointSize * 0.5
|
||||
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
text: "•••"
|
||||
color: Colours.palette.m3primary
|
||||
font.pointSize: Appearance.font.size.extraLarge * 0.9
|
||||
}
|
||||
|
||||
StyledText {
|
||||
id: mins
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: sep.bottom
|
||||
anchors.topMargin: -sep.font.pointSize * 0.45
|
||||
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
text: Time.format("MM")
|
||||
color: Colours.palette.m3secondary
|
||||
font.pointSize: Appearance.font.size.extraLarge
|
||||
font.weight: 500
|
||||
}
|
||||
|
||||
StyledText {
|
||||
id: date
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: mins.bottom
|
||||
anchors.topMargin: Appearance.spacing.normal
|
||||
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
text: Time.format("ddd, d")
|
||||
color: Colours.palette.m3tertiary
|
||||
font.pointSize: Appearance.font.size.normal
|
||||
font.weight: 500
|
||||
}
|
||||
}
|
262
modules/dashboard/dash/Media.qml
Normal file
262
modules/dashboard/dash/Media.qml
Normal file
|
@ -0,0 +1,262 @@
|
|||
import "root:/widgets"
|
||||
import "root:/services"
|
||||
import "root:/config"
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import Quickshell.Widgets
|
||||
import QtQuick
|
||||
import QtQuick.Shapes
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
required property bool shouldUpdate
|
||||
|
||||
property real playerProgress: {
|
||||
const active = Players.active;
|
||||
return active?.length ? active.position / active.length : 0;
|
||||
}
|
||||
|
||||
anchors.top: parent.top
|
||||
anchors.bottom: parent.bottom
|
||||
implicitWidth: DashboardConfig.sizes.mediaWidth
|
||||
|
||||
Behavior on playerProgress {
|
||||
NumberAnimation {
|
||||
duration: Appearance.anim.durations.large
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: Appearance.anim.curves.standard
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
running: root.shouldUpdate && (Players.active?.isPlaying ?? false)
|
||||
interval: DashboardConfig.mediaUpdateInterval
|
||||
triggeredOnStart: true
|
||||
repeat: true
|
||||
onTriggered: Players.active?.positionChanged()
|
||||
}
|
||||
|
||||
Shape {
|
||||
preferredRendererType: Shape.CurveRenderer
|
||||
|
||||
ShapePath {
|
||||
fillColor: "transparent"
|
||||
strokeColor: Colours.palette.m3surfaceContainerHigh
|
||||
strokeWidth: DashboardConfig.sizes.mediaProgressThickness
|
||||
capStyle: ShapePath.RoundCap
|
||||
|
||||
PathAngleArc {
|
||||
centerX: cover.x + cover.width / 2
|
||||
centerY: cover.y + cover.height / 2
|
||||
radiusX: (cover.width + DashboardConfig.sizes.mediaProgressThickness) / 2 + Appearance.spacing.small
|
||||
radiusY: (cover.height + DashboardConfig.sizes.mediaProgressThickness) / 2 + Appearance.spacing.small
|
||||
startAngle: -90 - DashboardConfig.sizes.mediaProgressSweep / 2
|
||||
sweepAngle: DashboardConfig.sizes.mediaProgressSweep
|
||||
}
|
||||
|
||||
Behavior on strokeColor {
|
||||
ColorAnimation {
|
||||
duration: Appearance.anim.durations.normal
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: Appearance.anim.curves.standard
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ShapePath {
|
||||
fillColor: "transparent"
|
||||
strokeColor: Colours.palette.m3primary
|
||||
strokeWidth: DashboardConfig.sizes.mediaProgressThickness
|
||||
capStyle: ShapePath.RoundCap
|
||||
|
||||
PathAngleArc {
|
||||
centerX: cover.x + cover.width / 2
|
||||
centerY: cover.y + cover.height / 2
|
||||
radiusX: (cover.width + DashboardConfig.sizes.mediaProgressThickness) / 2 + Appearance.spacing.small
|
||||
radiusY: (cover.height + DashboardConfig.sizes.mediaProgressThickness) / 2 + Appearance.spacing.small
|
||||
startAngle: -90 - DashboardConfig.sizes.mediaProgressSweep / 2
|
||||
sweepAngle: DashboardConfig.sizes.mediaProgressSweep * root.playerProgress
|
||||
}
|
||||
|
||||
Behavior on strokeColor {
|
||||
ColorAnimation {
|
||||
duration: Appearance.anim.durations.normal
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: Appearance.anim.curves.standard
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StyledClippingRect {
|
||||
id: cover
|
||||
|
||||
anchors.top: parent.top
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.margins: Appearance.padding.large + DashboardConfig.sizes.mediaProgressThickness + Appearance.spacing.small
|
||||
|
||||
implicitHeight: width
|
||||
color: Colours.palette.m3surfaceContainerHigh
|
||||
radius: Appearance.rounding.full
|
||||
|
||||
MaterialIcon {
|
||||
anchors.centerIn: parent
|
||||
|
||||
text: "art_track"
|
||||
color: Colours.palette.m3onSurfaceVariant
|
||||
font.pointSize: (parent.width * 0.4) || 1
|
||||
}
|
||||
|
||||
Image {
|
||||
id: image
|
||||
|
||||
anchors.fill: parent
|
||||
|
||||
source: Players.active?.trackArtUrl ?? ""
|
||||
asynchronous: true
|
||||
fillMode: Image.PreserveAspectCrop
|
||||
sourceSize.width: width
|
||||
sourceSize.height: height
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
id: title
|
||||
|
||||
anchors.top: cover.bottom
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.topMargin: Appearance.spacing.normal
|
||||
|
||||
animate: true
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
text: (Players.active?.trackTitle ?? qsTr("No media")) || qsTr("Unknown title")
|
||||
color: Colours.palette.m3primary
|
||||
font.pointSize: Appearance.font.size.normal
|
||||
|
||||
width: parent.implicitWidth - Appearance.padding.large * 2
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
|
||||
StyledText {
|
||||
id: album
|
||||
|
||||
anchors.top: title.bottom
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.topMargin: Appearance.spacing.small
|
||||
|
||||
animate: true
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
text: (Players.active?.trackAlbum ?? qsTr("No media")) || qsTr("Unknown album")
|
||||
color: Colours.palette.m3outline
|
||||
font.pointSize: Appearance.font.size.small
|
||||
|
||||
width: parent.implicitWidth - Appearance.padding.large * 2
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
|
||||
StyledText {
|
||||
id: artist
|
||||
|
||||
anchors.top: album.bottom
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.topMargin: Appearance.spacing.small
|
||||
|
||||
animate: true
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
text: (Players.active?.trackArtist ?? qsTr("No media")) || qsTr("Unknown artist")
|
||||
color: Colours.palette.m3secondary
|
||||
|
||||
width: parent.implicitWidth - Appearance.padding.large * 2
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
|
||||
Row {
|
||||
id: controls
|
||||
|
||||
anchors.top: artist.bottom
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.topMargin: Appearance.spacing.smaller
|
||||
|
||||
spacing: Appearance.spacing.small
|
||||
|
||||
Control {
|
||||
icon: "skip_previous"
|
||||
canUse: Players.active?.canGoPrevious ?? false
|
||||
|
||||
function onClicked(): void {
|
||||
Players.active?.previous();
|
||||
}
|
||||
}
|
||||
|
||||
Control {
|
||||
icon: Players.active?.isPlaying ? "pause" : "play_arrow"
|
||||
canUse: Players.active?.canTogglePlaying ?? false
|
||||
|
||||
function onClicked(): void {
|
||||
Players.active?.togglePlaying();
|
||||
}
|
||||
}
|
||||
|
||||
Control {
|
||||
icon: "skip_next"
|
||||
canUse: Players.active?.canGoNext ?? false
|
||||
|
||||
function onClicked(): void {
|
||||
Players.active?.next();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
AnimatedImage {
|
||||
id: bongocat
|
||||
|
||||
anchors.top: controls.bottom
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.topMargin: Appearance.spacing.small
|
||||
anchors.bottomMargin: Appearance.padding.large
|
||||
anchors.margins: Appearance.padding.large * 2
|
||||
|
||||
playing: root.shouldUpdate && (Players.active?.isPlaying ?? false)
|
||||
speed: BeatDetector.bpm / 300
|
||||
source: "root:/assets/bongocat.gif"
|
||||
asynchronous: true
|
||||
fillMode: AnimatedImage.PreserveAspectFit
|
||||
}
|
||||
|
||||
component Control: StyledRect {
|
||||
id: control
|
||||
|
||||
required property string icon
|
||||
required property bool canUse
|
||||
function onClicked(): void {
|
||||
}
|
||||
|
||||
implicitWidth: Math.max(icon.implicitHeight, icon.implicitHeight) + Appearance.padding.small
|
||||
implicitHeight: implicitWidth
|
||||
|
||||
StateLayer {
|
||||
disabled: !control.canUse
|
||||
radius: Appearance.rounding.full
|
||||
|
||||
function onClicked(): void {
|
||||
control.onClicked();
|
||||
}
|
||||
}
|
||||
|
||||
MaterialIcon {
|
||||
id: icon
|
||||
|
||||
anchors.centerIn: parent
|
||||
anchors.verticalCenterOffset: font.pointSize * 0.05
|
||||
|
||||
animate: true
|
||||
text: control.icon
|
||||
color: control.canUse ? Colours.palette.m3onSurface : Colours.palette.m3outline
|
||||
font.pointSize: Appearance.font.size.large
|
||||
}
|
||||
}
|
||||
}
|
85
modules/dashboard/dash/Resources.qml
Normal file
85
modules/dashboard/dash/Resources.qml
Normal file
|
@ -0,0 +1,85 @@
|
|||
import "root:/widgets"
|
||||
import "root:/services"
|
||||
import "root:/config"
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
|
||||
Row {
|
||||
id: root
|
||||
|
||||
anchors.top: parent.top
|
||||
anchors.bottom: parent.bottom
|
||||
|
||||
padding: Appearance.padding.large
|
||||
spacing: Appearance.spacing.normal
|
||||
|
||||
Resource {
|
||||
icon: "memory"
|
||||
value: SystemUsage.cpuPerc
|
||||
colour: Colours.palette.m3primary
|
||||
}
|
||||
|
||||
Resource {
|
||||
icon: "memory_alt"
|
||||
value: SystemUsage.memPerc
|
||||
colour: Colours.palette.m3secondary
|
||||
}
|
||||
|
||||
Resource {
|
||||
icon: "hard_disk"
|
||||
value: SystemUsage.storagePerc
|
||||
colour: Colours.palette.m3tertiary
|
||||
}
|
||||
|
||||
component Resource: Item {
|
||||
id: res
|
||||
|
||||
required property string icon
|
||||
required property real value
|
||||
required property color colour
|
||||
|
||||
anchors.top: parent.top
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.margins: Appearance.padding.large
|
||||
implicitWidth: icon.implicitWidth
|
||||
|
||||
StyledRect {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.top: parent.top
|
||||
anchors.bottom: icon.top
|
||||
anchors.bottomMargin: Appearance.spacing.small
|
||||
|
||||
implicitWidth: DashboardConfig.sizes.resourceProgessThickness
|
||||
|
||||
color: Colours.palette.m3surfaceContainerHigh
|
||||
radius: Appearance.rounding.full
|
||||
|
||||
StyledRect {
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.bottom: parent.bottom
|
||||
implicitHeight: res.value * parent.height
|
||||
|
||||
color: res.colour
|
||||
radius: Appearance.rounding.full
|
||||
}
|
||||
}
|
||||
|
||||
MaterialIcon {
|
||||
id: icon
|
||||
|
||||
anchors.bottom: parent.bottom
|
||||
|
||||
text: res.icon
|
||||
color: res.colour
|
||||
}
|
||||
|
||||
Behavior on value {
|
||||
NumberAnimation {
|
||||
duration: Appearance.anim.durations.large
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: Appearance.anim.curves.standard
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
116
modules/dashboard/dash/User.qml
Normal file
116
modules/dashboard/dash/User.qml
Normal file
|
@ -0,0 +1,116 @@
|
|||
import "root:/widgets"
|
||||
import "root:/services"
|
||||
import "root:/config"
|
||||
import "root:/utils"
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import QtQuick
|
||||
|
||||
Row {
|
||||
id: root
|
||||
|
||||
padding: Appearance.padding.large
|
||||
spacing: Appearance.spacing.large
|
||||
|
||||
StyledClippingRect {
|
||||
implicitWidth: info.implicitHeight
|
||||
implicitHeight: info.implicitHeight
|
||||
|
||||
radius: Appearance.rounding.full
|
||||
color: Colours.palette.m3surfaceContainerHigh
|
||||
|
||||
MaterialIcon {
|
||||
anchors.centerIn: parent
|
||||
|
||||
text: "person"
|
||||
fill: 1
|
||||
font.pointSize: (info.implicitHeight / 2) || 1
|
||||
}
|
||||
|
||||
CachingImage {
|
||||
anchors.fill: parent
|
||||
path: `${Paths.home}/.face`
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
id: info
|
||||
|
||||
spacing: Appearance.spacing.normal
|
||||
|
||||
InfoLine {
|
||||
icon: Icons.osIcon
|
||||
text: Icons.osName
|
||||
colour: Colours.palette.m3primary
|
||||
}
|
||||
|
||||
InfoLine {
|
||||
icon: "select_window_2"
|
||||
text: Quickshell.env("XDG_CURRENT_DESKTOP") || Quickshell.env("XDG_SESSION_DESKTOP")
|
||||
colour: Colours.palette.m3secondary
|
||||
}
|
||||
|
||||
InfoLine {
|
||||
icon: "timer"
|
||||
text: uptimeProc.uptime
|
||||
colour: Colours.palette.m3tertiary
|
||||
|
||||
Timer {
|
||||
running: true
|
||||
repeat: true
|
||||
interval: 15000
|
||||
onTriggered: uptimeProc.running = true
|
||||
}
|
||||
|
||||
Process {
|
||||
id: uptimeProc
|
||||
|
||||
property string uptime
|
||||
|
||||
running: true
|
||||
command: ["uptime", "-p"]
|
||||
stdout: SplitParser {
|
||||
onRead: data => uptimeProc.uptime = data
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
component InfoLine: Item {
|
||||
id: line
|
||||
|
||||
required property string icon
|
||||
required property string text
|
||||
required property color colour
|
||||
|
||||
implicitWidth: icon.implicitWidth + text.width + text.anchors.leftMargin
|
||||
implicitHeight: Math.max(icon.implicitHeight, text.implicitHeight)
|
||||
|
||||
MaterialIcon {
|
||||
id: icon
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: (DashboardConfig.sizes.infoIconSize - implicitWidth) / 2
|
||||
|
||||
text: line.icon
|
||||
color: line.colour
|
||||
font.pointSize: Appearance.font.size.normal
|
||||
font.variableAxes: ({
|
||||
FILL: 1
|
||||
})
|
||||
}
|
||||
|
||||
StyledText {
|
||||
id: text
|
||||
|
||||
anchors.verticalCenter: icon.verticalCenter
|
||||
anchors.left: icon.right
|
||||
anchors.leftMargin: icon.anchors.leftMargin
|
||||
text: `: ${line.text}`
|
||||
font.pointSize: Appearance.font.size.normal
|
||||
|
||||
width: DashboardConfig.sizes.infoWidth
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
}
|
||||
}
|
63
modules/dashboard/dash/Weather.qml
Normal file
63
modules/dashboard/dash/Weather.qml
Normal file
|
@ -0,0 +1,63 @@
|
|||
import "root:/widgets"
|
||||
import "root:/services"
|
||||
import "root:/config"
|
||||
import "root:/utils"
|
||||
import QtQuick
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
anchors.centerIn: parent
|
||||
|
||||
implicitWidth: icon.implicitWidth + info.implicitWidth + info.anchors.leftMargin
|
||||
|
||||
onVisibleChanged: {
|
||||
if (visible)
|
||||
Weather.reload();
|
||||
}
|
||||
|
||||
MaterialIcon {
|
||||
id: icon
|
||||
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.left: parent.left
|
||||
|
||||
animate: true
|
||||
text: Weather.icon || "cloud_alert"
|
||||
color: Colours.palette.m3secondary
|
||||
font.pointSize: Appearance.font.size.extraLarge * 2
|
||||
font.variableAxes: ({
|
||||
opsz: Appearance.font.size.extraLarge * 1.2
|
||||
})
|
||||
}
|
||||
|
||||
Column {
|
||||
id: info
|
||||
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.left: icon.right
|
||||
anchors.leftMargin: Appearance.spacing.large
|
||||
|
||||
spacing: Appearance.spacing.small
|
||||
|
||||
StyledText {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
|
||||
animate: true
|
||||
text: `${Weather.temperature}°C`
|
||||
color: Colours.palette.m3primary
|
||||
font.pointSize: Appearance.font.size.extraLarge
|
||||
font.weight: 500
|
||||
}
|
||||
|
||||
StyledText {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
|
||||
animate: true
|
||||
text: Weather.description || qsTr("No weather")
|
||||
|
||||
elide: Text.ElideRight
|
||||
width: Math.min(implicitWidth, root.parent.width - icon.implicitWidth - info.anchors.leftMargin - Appearance.padding.large * 2)
|
||||
}
|
||||
}
|
||||
}
|
66
modules/drawers/Backgrounds.qml
Normal file
66
modules/drawers/Backgrounds.qml
Normal file
|
@ -0,0 +1,66 @@
|
|||
import "root:/services"
|
||||
import "root:/config"
|
||||
import "root:/modules/osd" as Osd
|
||||
import "root:/modules/notifications" as Notifications
|
||||
import "root:/modules/session" as Session
|
||||
import "root:/modules/launcher" as Launcher
|
||||
import "root:/modules/dashboard" as Dashboard
|
||||
import "root:/modules/bar/popouts" as BarPopouts
|
||||
import QtQuick
|
||||
import QtQuick.Shapes
|
||||
|
||||
Shape {
|
||||
id: root
|
||||
|
||||
required property Panels panels
|
||||
required property Item bar
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.margins: BorderConfig.thickness
|
||||
anchors.leftMargin: bar.implicitWidth
|
||||
preferredRendererType: Shape.CurveRenderer
|
||||
opacity: Colours.transparency.enabled ? Colours.transparency.base : 1
|
||||
|
||||
Osd.Background {
|
||||
wrapper: panels.osd
|
||||
|
||||
startX: root.width - panels.session.width
|
||||
startY: (root.height - wrapper.height) / 2 - rounding
|
||||
}
|
||||
|
||||
Notifications.Background {
|
||||
wrapper: panels.notifications
|
||||
|
||||
startX: root.width
|
||||
startY: 0
|
||||
}
|
||||
|
||||
Session.Background {
|
||||
wrapper: panels.session
|
||||
|
||||
startX: root.width
|
||||
startY: (root.height - wrapper.height) / 2 - rounding
|
||||
}
|
||||
|
||||
Launcher.Background {
|
||||
wrapper: panels.launcher
|
||||
|
||||
startX: (root.width - wrapper.width) / 2 - rounding
|
||||
startY: root.height
|
||||
}
|
||||
|
||||
Dashboard.Background {
|
||||
wrapper: panels.dashboard
|
||||
|
||||
startX: (root.width - wrapper.width) / 2 - rounding
|
||||
startY: 0
|
||||
}
|
||||
|
||||
BarPopouts.Background {
|
||||
wrapper: panels.popouts
|
||||
invertBottomRounding: wrapper.y + wrapper.height + 1 >= root.height
|
||||
|
||||
startX: 0
|
||||
startY: wrapper.y - rounding
|
||||
}
|
||||
}
|
47
modules/drawers/Border.qml
Normal file
47
modules/drawers/Border.qml
Normal file
|
@ -0,0 +1,47 @@
|
|||
import "root:/widgets"
|
||||
import "root:/services"
|
||||
import "root:/config"
|
||||
import Quickshell
|
||||
import QtQuick
|
||||
import QtQuick.Effects
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
required property Item bar
|
||||
|
||||
anchors.fill: parent
|
||||
|
||||
StyledRect {
|
||||
id: rect
|
||||
|
||||
anchors.fill: parent
|
||||
color: Colours.alpha(BorderConfig.colour, false)
|
||||
visible: false
|
||||
}
|
||||
|
||||
Item {
|
||||
id: mask
|
||||
|
||||
anchors.fill: parent
|
||||
layer.enabled: true
|
||||
visible: false
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
anchors.margins: BorderConfig.thickness
|
||||
anchors.leftMargin: root.bar.implicitWidth
|
||||
radius: BorderConfig.rounding
|
||||
}
|
||||
}
|
||||
|
||||
MultiEffect {
|
||||
anchors.fill: parent
|
||||
maskEnabled: true
|
||||
maskInverted: true
|
||||
maskSource: mask
|
||||
source: rect
|
||||
maskThresholdMin: 0.5
|
||||
maskSpreadAtMin: 1
|
||||
}
|
||||
}
|
147
modules/drawers/Drawers.qml
Normal file
147
modules/drawers/Drawers.qml
Normal file
|
@ -0,0 +1,147 @@
|
|||
pragma ComponentBehavior: Bound
|
||||
|
||||
import "root:/widgets"
|
||||
import "root:/services"
|
||||
import "root:/config"
|
||||
import "root:/modules/bar"
|
||||
import Quickshell
|
||||
import Quickshell.Wayland
|
||||
import Quickshell.Hyprland
|
||||
import QtQuick
|
||||
import QtQuick.Effects
|
||||
|
||||
Variants {
|
||||
model: Quickshell.screens
|
||||
|
||||
Scope {
|
||||
id: scope
|
||||
|
||||
required property ShellScreen modelData
|
||||
|
||||
Exclusions {
|
||||
screen: scope.modelData
|
||||
bar: bar
|
||||
}
|
||||
|
||||
StyledWindow {
|
||||
id: win
|
||||
|
||||
screen: scope.modelData
|
||||
name: "drawers"
|
||||
WlrLayershell.exclusionMode: ExclusionMode.Ignore
|
||||
WlrLayershell.keyboardFocus: visibilities.launcher || visibilities.session ? WlrKeyboardFocus.OnDemand : WlrKeyboardFocus.None
|
||||
|
||||
mask: Region {
|
||||
x: bar.implicitWidth
|
||||
y: BorderConfig.thickness
|
||||
width: win.width - bar.implicitWidth - BorderConfig.thickness
|
||||
height: win.height - BorderConfig.thickness * 2
|
||||
intersection: Intersection.Xor
|
||||
|
||||
regions: regions.instances
|
||||
}
|
||||
|
||||
anchors.top: true
|
||||
anchors.bottom: true
|
||||
anchors.left: true
|
||||
anchors.right: true
|
||||
|
||||
Variants {
|
||||
id: regions
|
||||
|
||||
model: panels.children
|
||||
|
||||
Region {
|
||||
required property Item modelData
|
||||
|
||||
x: modelData.x + bar.implicitWidth
|
||||
y: modelData.y + BorderConfig.thickness
|
||||
width: modelData.width
|
||||
height: modelData.height
|
||||
intersection: Intersection.Subtract
|
||||
}
|
||||
}
|
||||
|
||||
HyprlandFocusGrab {
|
||||
active: visibilities.launcher || visibilities.session
|
||||
windows: [win]
|
||||
onCleared: {
|
||||
visibilities.launcher = false;
|
||||
visibilities.session = false;
|
||||
}
|
||||
}
|
||||
|
||||
StyledRect {
|
||||
anchors.fill: parent
|
||||
opacity: visibilities.session ? 0.5 : 0
|
||||
color: Colours.palette.m3scrim
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: Appearance.anim.durations.normal
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: Appearance.anim.curves.standard
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
id: background
|
||||
|
||||
anchors.fill: parent
|
||||
visible: false
|
||||
|
||||
Border {
|
||||
bar: bar
|
||||
}
|
||||
|
||||
Backgrounds {
|
||||
panels: panels
|
||||
bar: bar
|
||||
}
|
||||
}
|
||||
|
||||
MultiEffect {
|
||||
anchors.fill: source
|
||||
source: background
|
||||
shadowEnabled: true
|
||||
blurMax: 15
|
||||
shadowColor: Qt.alpha(Colours.palette.m3shadow, 0.7)
|
||||
}
|
||||
|
||||
PersistentProperties {
|
||||
id: visibilities
|
||||
|
||||
property bool osd
|
||||
property bool session
|
||||
property bool launcher
|
||||
property bool dashboard
|
||||
|
||||
Component.onCompleted: Visibilities.screens[scope.modelData] = this
|
||||
}
|
||||
|
||||
Interactions {
|
||||
screen: scope.modelData
|
||||
popouts: panels.popouts
|
||||
visibilities: visibilities
|
||||
panels: panels
|
||||
bar: bar
|
||||
|
||||
Panels {
|
||||
id: panels
|
||||
|
||||
screen: scope.modelData
|
||||
visibilities: visibilities
|
||||
bar: bar
|
||||
}
|
||||
}
|
||||
|
||||
Bar {
|
||||
id: bar
|
||||
|
||||
screen: scope.modelData
|
||||
popouts: panels.popouts
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
36
modules/drawers/Exclusions.qml
Normal file
36
modules/drawers/Exclusions.qml
Normal file
|
@ -0,0 +1,36 @@
|
|||
pragma ComponentBehavior: Bound
|
||||
|
||||
import "root:/widgets"
|
||||
import "root:/config"
|
||||
import Quickshell
|
||||
import QtQuick
|
||||
|
||||
Scope {
|
||||
id: root
|
||||
|
||||
required property ShellScreen screen
|
||||
required property Item bar
|
||||
|
||||
ExclusionZone {
|
||||
anchors.left: true
|
||||
exclusiveZone: root.bar.implicitWidth
|
||||
}
|
||||
|
||||
ExclusionZone {
|
||||
anchors.top: true
|
||||
}
|
||||
|
||||
ExclusionZone {
|
||||
anchors.right: true
|
||||
}
|
||||
|
||||
ExclusionZone {
|
||||
anchors.bottom: true
|
||||
}
|
||||
|
||||
component ExclusionZone: StyledWindow {
|
||||
screen: root.screen
|
||||
name: "border-exclusion"
|
||||
exclusiveZone: BorderConfig.thickness
|
||||
}
|
||||
}
|
83
modules/drawers/Interactions.qml
Normal file
83
modules/drawers/Interactions.qml
Normal file
|
@ -0,0 +1,83 @@
|
|||
import "root:/services"
|
||||
import "root:/config"
|
||||
import "root:/modules/bar/popouts" as BarPopouts
|
||||
import "root:/modules/osd" as Osd
|
||||
import Quickshell
|
||||
import QtQuick
|
||||
|
||||
MouseArea {
|
||||
id: root
|
||||
|
||||
required property ShellScreen screen
|
||||
required property BarPopouts.Wrapper popouts
|
||||
required property PersistentProperties visibilities
|
||||
required property Panels panels
|
||||
required property Item bar
|
||||
|
||||
property bool osdHovered
|
||||
property point dragStart
|
||||
|
||||
function withinPanelHeight(panel: Item, x: real, y: real): bool {
|
||||
const panelY = BorderConfig.thickness + panel.y;
|
||||
return y >= panelY - BorderConfig.rounding && y <= panelY + panel.height + BorderConfig.rounding;
|
||||
}
|
||||
|
||||
function inRightPanel(panel: Item, x: real, y: real): bool {
|
||||
return x > bar.implicitWidth + panel.x && withinPanelHeight(panel, x, y);
|
||||
}
|
||||
|
||||
function inTopPanel(panel: Item, x: real, y: real): bool {
|
||||
const panelX = bar.implicitWidth + panel.x;
|
||||
return y < BorderConfig.thickness + panel.y + panel.height && x >= panelX - BorderConfig.rounding && x <= panelX + panel.width + BorderConfig.rounding;
|
||||
}
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
|
||||
onPressed: event => dragStart = Qt.point(event.x, event.y)
|
||||
onContainsMouseChanged: {
|
||||
if (!containsMouse) {
|
||||
visibilities.osd = false;
|
||||
osdHovered = false;
|
||||
visibilities.dashboard = false;
|
||||
popouts.hasCurrent = false;
|
||||
}
|
||||
}
|
||||
|
||||
onPositionChanged: ({x, y}) => {
|
||||
// Show osd on hover
|
||||
const showOsd = inRightPanel(panels.osd, x, y);
|
||||
visibilities.osd = showOsd;
|
||||
osdHovered = showOsd;
|
||||
|
||||
// Show/hide session on drag
|
||||
if (pressed && withinPanelHeight(panels.session, x, y)) {
|
||||
const dragX = x - dragStart.x;
|
||||
if (dragX < -SessionConfig.dragThreshold)
|
||||
visibilities.session = true;
|
||||
else if (dragX > SessionConfig.dragThreshold)
|
||||
visibilities.session = false;
|
||||
}
|
||||
|
||||
// Show dashboard on hover
|
||||
visibilities.dashboard = inTopPanel(panels.dashboard, x, y);
|
||||
|
||||
// Show popouts on hover
|
||||
const popout = panels.popouts;
|
||||
if (x < bar.implicitWidth + popout.width) {
|
||||
if (x < bar.implicitWidth)
|
||||
// Handle like part of bar
|
||||
bar.checkPopout(y);
|
||||
else
|
||||
// Keep on hover
|
||||
popouts.hasCurrent = withinPanelHeight(popout, x, y);
|
||||
} else
|
||||
popouts.hasCurrent = false;
|
||||
}
|
||||
|
||||
Osd.Interactions {
|
||||
screen: root.screen
|
||||
visibilities: root.visibilities
|
||||
hovered: root.osdHovered
|
||||
}
|
||||
}
|
93
modules/drawers/Panels.qml
Normal file
93
modules/drawers/Panels.qml
Normal file
|
@ -0,0 +1,93 @@
|
|||
import "root:/services"
|
||||
import "root:/config"
|
||||
import "root:/modules/osd" as Osd
|
||||
import "root:/modules/notifications" as Notifications
|
||||
import "root:/modules/session" as Session
|
||||
import "root:/modules/launcher" as Launcher
|
||||
import "root:/modules/dashboard" as Dashboard
|
||||
import "root:/modules/bar/popouts" as BarPopouts
|
||||
import Quickshell
|
||||
import QtQuick
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
required property ShellScreen screen
|
||||
required property PersistentProperties visibilities
|
||||
required property Item bar
|
||||
|
||||
readonly property Osd.Wrapper osd: osd
|
||||
readonly property Notifications.Wrapper notifications: notifications
|
||||
readonly property Session.Wrapper session: session
|
||||
readonly property Launcher.Wrapper launcher: launcher
|
||||
readonly property Dashboard.Wrapper dashboard: dashboard
|
||||
readonly property BarPopouts.Wrapper popouts: popouts
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.margins: BorderConfig.thickness
|
||||
anchors.leftMargin: bar.implicitWidth
|
||||
|
||||
Component.onCompleted: Visibilities.panels[screen] = this
|
||||
|
||||
Osd.Wrapper {
|
||||
id: osd
|
||||
|
||||
clip: root.visibilities.session
|
||||
screen: root.screen
|
||||
visibility: root.visibilities.osd
|
||||
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: session.width
|
||||
}
|
||||
|
||||
Notifications.Wrapper {
|
||||
id: notifications
|
||||
|
||||
anchors.top: parent.top
|
||||
anchors.right: parent.right
|
||||
}
|
||||
|
||||
Session.Wrapper {
|
||||
id: session
|
||||
|
||||
visibilities: root.visibilities
|
||||
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.right: parent.right
|
||||
}
|
||||
|
||||
Launcher.Wrapper {
|
||||
id: launcher
|
||||
|
||||
visibilities: root.visibilities
|
||||
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.bottom: parent.bottom
|
||||
}
|
||||
|
||||
Dashboard.Wrapper {
|
||||
id: dashboard
|
||||
|
||||
visibilities: root.visibilities
|
||||
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.top: parent.top
|
||||
}
|
||||
|
||||
BarPopouts.Wrapper {
|
||||
id: popouts
|
||||
|
||||
screen: root.screen
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.verticalCenter: parent.top
|
||||
anchors.verticalCenterOffset: {
|
||||
const off = root.popouts.currentCenter - BorderConfig.thickness;
|
||||
const diff = root.height - Math.floor(off + implicitHeight / 2);
|
||||
if (diff < 0)
|
||||
return off + diff;
|
||||
return off;
|
||||
}
|
||||
}
|
||||
}
|
69
modules/launcher/ActionItem.qml
Normal file
69
modules/launcher/ActionItem.qml
Normal file
|
@ -0,0 +1,69 @@
|
|||
import "root:/widgets"
|
||||
import "root:/services"
|
||||
import "root:/config"
|
||||
import QtQuick
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
required property Actions.Action modelData
|
||||
required property var list
|
||||
|
||||
implicitHeight: LauncherConfig.sizes.itemHeight
|
||||
|
||||
anchors.left: parent?.left
|
||||
anchors.right: parent?.right
|
||||
|
||||
StateLayer {
|
||||
radius: Appearance.rounding.full
|
||||
|
||||
function onClicked(): void {
|
||||
root.modelData?.onClicked(root.list);
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: Appearance.padding.larger
|
||||
anchors.rightMargin: Appearance.padding.larger
|
||||
anchors.margins: Appearance.padding.smaller
|
||||
|
||||
MaterialIcon {
|
||||
id: icon
|
||||
|
||||
text: root.modelData?.icon ?? ""
|
||||
font.pointSize: Appearance.font.size.extraLarge
|
||||
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Item {
|
||||
anchors.left: icon.right
|
||||
anchors.leftMargin: Appearance.spacing.larger
|
||||
anchors.verticalCenter: icon.verticalCenter
|
||||
|
||||
implicitWidth: parent.width - icon.width
|
||||
implicitHeight: name.implicitHeight + desc.implicitHeight
|
||||
|
||||
StyledText {
|
||||
id: name
|
||||
|
||||
text: root.modelData?.name ?? ""
|
||||
font.pointSize: Appearance.font.size.normal
|
||||
}
|
||||
|
||||
StyledText {
|
||||
id: desc
|
||||
|
||||
text: root.modelData?.desc ?? ""
|
||||
font.pointSize: Appearance.font.size.small
|
||||
color: Colours.alpha(Colours.palette.m3outline, true)
|
||||
|
||||
elide: Text.ElideRight
|
||||
width: root.width - icon.width - Appearance.rounding.normal * 2
|
||||
|
||||
anchors.top: name.bottom
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
130
modules/launcher/Actions.qml
Normal file
130
modules/launcher/Actions.qml
Normal file
|
@ -0,0 +1,130 @@
|
|||
pragma Singleton
|
||||
|
||||
import "root:/utils/scripts/fuzzysort.js" as Fuzzy
|
||||
import "root:/services"
|
||||
import "root:/config"
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import QtQuick
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
readonly property list<Action> list: [
|
||||
Action {
|
||||
name: qsTr("Scheme")
|
||||
desc: qsTr("Change the current colour scheme")
|
||||
icon: "palette"
|
||||
|
||||
function onClicked(list: AppList): void {
|
||||
root.autocomplete(list, "scheme");
|
||||
}
|
||||
},
|
||||
Action {
|
||||
name: qsTr("Wallpaper")
|
||||
desc: qsTr("Change the current wallpaper")
|
||||
icon: "image"
|
||||
|
||||
function onClicked(list: AppList): void {
|
||||
root.autocomplete(list, "wallpaper");
|
||||
}
|
||||
},
|
||||
Action {
|
||||
name: qsTr("Variant")
|
||||
desc: qsTr("Change the current scheme variant")
|
||||
icon: "colors"
|
||||
|
||||
function onClicked(list: AppList): void {
|
||||
root.autocomplete(list, "variant");
|
||||
}
|
||||
},
|
||||
Action {
|
||||
name: qsTr("Transparency")
|
||||
desc: qsTr("Change shell transparency")
|
||||
icon: "opacity"
|
||||
|
||||
function onClicked(list: AppList): void {
|
||||
root.autocomplete(list, "transparency");
|
||||
}
|
||||
},
|
||||
Action {
|
||||
name: qsTr("Light")
|
||||
desc: qsTr("Change the scheme to light mode")
|
||||
icon: "light_mode"
|
||||
|
||||
function onClicked(list: AppList): void {
|
||||
list.visibilities.launcher = false;
|
||||
Colours.setMode("light");
|
||||
}
|
||||
},
|
||||
Action {
|
||||
name: qsTr("Dark")
|
||||
desc: qsTr("Change the scheme to dark mode")
|
||||
icon: "dark_mode"
|
||||
|
||||
function onClicked(list: AppList): void {
|
||||
list.visibilities.launcher = false;
|
||||
Colours.setMode("dark");
|
||||
}
|
||||
},
|
||||
Action {
|
||||
name: qsTr("Lock")
|
||||
desc: qsTr("Lock the current session")
|
||||
icon: "lock"
|
||||
|
||||
function onClicked(list: AppList): void {
|
||||
list.visibilities.launcher = false;
|
||||
lock.running = true;
|
||||
}
|
||||
},
|
||||
Action {
|
||||
name: qsTr("Sleep")
|
||||
desc: qsTr("Suspend then hibernate")
|
||||
icon: "bedtime"
|
||||
|
||||
function onClicked(list: AppList): void {
|
||||
list.visibilities.launcher = false;
|
||||
sleep.running = true;
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
readonly property list<var> preppedActions: list.map(a => ({
|
||||
name: Fuzzy.prepare(a.name),
|
||||
desc: Fuzzy.prepare(a.desc),
|
||||
action: a
|
||||
}))
|
||||
|
||||
function fuzzyQuery(search: string): var {
|
||||
return Fuzzy.go(search.slice(LauncherConfig.actionPrefix.length), preppedActions, {
|
||||
all: true,
|
||||
keys: ["name", "desc"],
|
||||
scoreFn: r => r[0].score > 0 ? r[0].score * 0.9 + r[1].score * 0.1 : 0
|
||||
}).map(r => r.obj.action);
|
||||
}
|
||||
|
||||
function autocomplete(list: AppList, text: string): void {
|
||||
list.search.text = `${LauncherConfig.actionPrefix}${text} `;
|
||||
}
|
||||
|
||||
Process {
|
||||
id: lock
|
||||
|
||||
command: ["loginctl", "lock-session"]
|
||||
}
|
||||
|
||||
Process {
|
||||
id: sleep
|
||||
|
||||
command: ["systemctl", "suspend-then-hibernate"]
|
||||
}
|
||||
|
||||
component Action: QtObject {
|
||||
required property string name
|
||||
required property string desc
|
||||
required property string icon
|
||||
|
||||
function onClicked(list: AppList): void {
|
||||
}
|
||||
}
|
||||
}
|
72
modules/launcher/AppItem.qml
Normal file
72
modules/launcher/AppItem.qml
Normal file
|
@ -0,0 +1,72 @@
|
|||
import "root:/widgets"
|
||||
import "root:/services"
|
||||
import "root:/config"
|
||||
import Quickshell
|
||||
import Quickshell.Widgets
|
||||
import QtQuick
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
required property DesktopEntry modelData
|
||||
required property PersistentProperties visibilities
|
||||
|
||||
implicitHeight: LauncherConfig.sizes.itemHeight
|
||||
|
||||
anchors.left: parent?.left
|
||||
anchors.right: parent?.right
|
||||
|
||||
StateLayer {
|
||||
radius: Appearance.rounding.full
|
||||
|
||||
function onClicked(): void {
|
||||
Apps.launch(root.modelData);
|
||||
root.visibilities.launcher = false;
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: Appearance.padding.larger
|
||||
anchors.rightMargin: Appearance.padding.larger
|
||||
anchors.margins: Appearance.padding.smaller
|
||||
|
||||
IconImage {
|
||||
id: icon
|
||||
|
||||
source: Quickshell.iconPath(root.modelData?.icon, "image-missing")
|
||||
implicitSize: parent.height * 0.8
|
||||
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Item {
|
||||
anchors.left: icon.right
|
||||
anchors.leftMargin: Appearance.spacing.normal
|
||||
anchors.verticalCenter: icon.verticalCenter
|
||||
|
||||
implicitWidth: parent.width - icon.width
|
||||
implicitHeight: name.implicitHeight + comment.implicitHeight
|
||||
|
||||
StyledText {
|
||||
id: name
|
||||
|
||||
text: root.modelData?.name ?? ""
|
||||
font.pointSize: Appearance.font.size.normal
|
||||
}
|
||||
|
||||
StyledText {
|
||||
id: comment
|
||||
|
||||
text: (root.modelData?.comment || root.modelData?.genericName || root.modelData?.name) ?? ""
|
||||
font.pointSize: Appearance.font.size.small
|
||||
color: Colours.alpha(Colours.palette.m3outline, true)
|
||||
|
||||
elide: Text.ElideRight
|
||||
width: root.width - icon.width - Appearance.rounding.normal * 2
|
||||
|
||||
anchors.top: name.bottom
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
160
modules/launcher/AppList.qml
Normal file
160
modules/launcher/AppList.qml
Normal file
|
@ -0,0 +1,160 @@
|
|||
pragma ComponentBehavior: Bound
|
||||
|
||||
import "root:/widgets"
|
||||
import "root:/services"
|
||||
import "root:/config"
|
||||
import Quickshell
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
|
||||
ListView {
|
||||
id: root
|
||||
|
||||
required property int padding
|
||||
required property TextField search
|
||||
required property PersistentProperties visibilities
|
||||
|
||||
property bool isAction: search.text.startsWith(LauncherConfig.actionPrefix)
|
||||
|
||||
function getModelValues() {
|
||||
let text = search.text;
|
||||
if (isAction)
|
||||
return Actions.fuzzyQuery(text);
|
||||
if (text.startsWith(LauncherConfig.actionPrefix))
|
||||
text = search.text.slice(LauncherConfig.actionPrefix.length);
|
||||
return Apps.fuzzyQuery(text);
|
||||
}
|
||||
|
||||
model: ScriptModel {
|
||||
values: root.getModelValues()
|
||||
onValuesChanged: root.currentIndex = 0
|
||||
}
|
||||
|
||||
spacing: Appearance.spacing.small
|
||||
orientation: Qt.Vertical
|
||||
implicitHeight: (LauncherConfig.sizes.itemHeight + spacing) * Math.min(LauncherConfig.maxShown, count) - spacing
|
||||
|
||||
highlightMoveDuration: Appearance.anim.durations.normal
|
||||
highlightResizeDuration: 0
|
||||
|
||||
highlight: StyledRect {
|
||||
radius: Appearance.rounding.full
|
||||
color: Colours.palette.m3onSurface
|
||||
opacity: 0.08
|
||||
}
|
||||
|
||||
delegate: isAction ? actionItem : appItem
|
||||
|
||||
ScrollBar.vertical: StyledScrollBar {}
|
||||
|
||||
add: Transition {
|
||||
Anim {
|
||||
properties: "opacity,scale"
|
||||
from: 0
|
||||
to: 1
|
||||
}
|
||||
}
|
||||
|
||||
remove: Transition {
|
||||
Anim {
|
||||
properties: "opacity,scale"
|
||||
from: 1
|
||||
to: 0
|
||||
}
|
||||
}
|
||||
|
||||
move: Transition {
|
||||
Anim {
|
||||
property: "y"
|
||||
}
|
||||
Anim {
|
||||
properties: "opacity,scale"
|
||||
to: 1
|
||||
}
|
||||
}
|
||||
|
||||
addDisplaced: Transition {
|
||||
Anim {
|
||||
property: "y"
|
||||
duration: Appearance.anim.durations.small
|
||||
}
|
||||
Anim {
|
||||
properties: "opacity,scale"
|
||||
to: 1
|
||||
}
|
||||
}
|
||||
|
||||
displaced: Transition {
|
||||
Anim {
|
||||
property: "y"
|
||||
}
|
||||
Anim {
|
||||
properties: "opacity,scale"
|
||||
to: 1
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: appItem
|
||||
|
||||
AppItem {
|
||||
visibilities: root.visibilities
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: actionItem
|
||||
|
||||
ActionItem {
|
||||
list: root
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on isAction {
|
||||
SequentialAnimation {
|
||||
ParallelAnimation {
|
||||
Anim {
|
||||
target: root
|
||||
property: "opacity"
|
||||
from: 1
|
||||
to: 0
|
||||
duration: Appearance.anim.durations.small
|
||||
easing.bezierCurve: Appearance.anim.curves.standardAccel
|
||||
}
|
||||
Anim {
|
||||
target: root
|
||||
property: "scale"
|
||||
from: 1
|
||||
to: 0.9
|
||||
duration: Appearance.anim.durations.small
|
||||
easing.bezierCurve: Appearance.anim.curves.standardAccel
|
||||
}
|
||||
}
|
||||
PropertyAction {}
|
||||
ParallelAnimation {
|
||||
Anim {
|
||||
target: root
|
||||
property: "opacity"
|
||||
from: 0
|
||||
to: 1
|
||||
duration: Appearance.anim.durations.small
|
||||
easing.bezierCurve: Appearance.anim.curves.standardDecel
|
||||
}
|
||||
Anim {
|
||||
target: root
|
||||
property: "scale"
|
||||
from: 0.9
|
||||
to: 1
|
||||
duration: Appearance.anim.durations.small
|
||||
easing.bezierCurve: Appearance.anim.curves.standardDecel
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
component Anim: NumberAnimation {
|
||||
duration: Appearance.anim.durations.normal
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: Appearance.anim.curves.standard
|
||||
}
|
||||
}
|
63
modules/launcher/Background.qml
Normal file
63
modules/launcher/Background.qml
Normal file
|
@ -0,0 +1,63 @@
|
|||
import "root:/services"
|
||||
import "root:/config"
|
||||
import QtQuick
|
||||
import QtQuick.Shapes
|
||||
|
||||
ShapePath {
|
||||
id: root
|
||||
|
||||
required property Wrapper wrapper
|
||||
readonly property real rounding: BorderConfig.rounding
|
||||
readonly property bool flatten: wrapper.height < rounding * 2
|
||||
readonly property real roundingY: flatten ? wrapper.height / 2 : rounding
|
||||
|
||||
strokeWidth: -1
|
||||
fillColor: BorderConfig.colour
|
||||
|
||||
PathArc {
|
||||
relativeX: root.rounding
|
||||
relativeY: -root.roundingY
|
||||
radiusX: root.rounding
|
||||
radiusY: Math.min(root.rounding, root.wrapper.height)
|
||||
direction: PathArc.Counterclockwise
|
||||
}
|
||||
PathLine {
|
||||
relativeX: 0
|
||||
relativeY: -(root.wrapper.height - root.roundingY * 2)
|
||||
}
|
||||
PathArc {
|
||||
relativeX: root.rounding
|
||||
relativeY: -root.roundingY
|
||||
radiusX: root.rounding
|
||||
radiusY: Math.min(root.rounding, root.wrapper.height)
|
||||
}
|
||||
PathLine {
|
||||
relativeX: root.wrapper.width - root.rounding * 2
|
||||
relativeY: 0
|
||||
}
|
||||
PathArc {
|
||||
relativeX: root.rounding
|
||||
relativeY: root.roundingY
|
||||
radiusX: root.rounding
|
||||
radiusY: Math.min(root.rounding, root.wrapper.height)
|
||||
}
|
||||
PathLine {
|
||||
relativeX: 0
|
||||
relativeY: root.wrapper.height - root.roundingY * 2
|
||||
}
|
||||
PathArc {
|
||||
relativeX: root.rounding
|
||||
relativeY: root.roundingY
|
||||
radiusX: root.rounding
|
||||
radiusY: Math.min(root.rounding, root.wrapper.height)
|
||||
direction: PathArc.Counterclockwise
|
||||
}
|
||||
|
||||
Behavior on fillColor {
|
||||
ColorAnimation {
|
||||
duration: Appearance.anim.durations.normal
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: Appearance.anim.curves.standard
|
||||
}
|
||||
}
|
||||
}
|
168
modules/launcher/Content.qml
Normal file
168
modules/launcher/Content.qml
Normal file
|
@ -0,0 +1,168 @@
|
|||
pragma ComponentBehavior: Bound
|
||||
|
||||
import "root:/widgets"
|
||||
import "root:/services"
|
||||
import "root:/config"
|
||||
import Quickshell
|
||||
import QtQuick
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
required property PersistentProperties visibilities
|
||||
readonly property int padding: Appearance.padding.large
|
||||
readonly property int rounding: Appearance.rounding.large
|
||||
|
||||
implicitWidth: listWrapper.width + padding * 2
|
||||
implicitHeight: searchWrapper.height + listWrapper.height + padding * 2
|
||||
|
||||
anchors.top: parent.top
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
|
||||
Item {
|
||||
id: listWrapper
|
||||
|
||||
implicitWidth: list.width
|
||||
implicitHeight: list.height + root.padding
|
||||
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.bottom: searchWrapper.top
|
||||
anchors.bottomMargin: root.padding
|
||||
|
||||
ContentList {
|
||||
id: list
|
||||
|
||||
visibilities: root.visibilities
|
||||
search: search
|
||||
padding: root.padding
|
||||
rounding: root.rounding
|
||||
}
|
||||
}
|
||||
|
||||
StyledRect {
|
||||
id: searchWrapper
|
||||
|
||||
color: Colours.alpha(Colours.palette.m3surfaceContainer, true)
|
||||
radius: Appearance.rounding.full
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.margins: root.padding
|
||||
|
||||
implicitHeight: Math.max(searchIcon.implicitHeight, search.implicitHeight, clearIcon.implicitHeight)
|
||||
|
||||
MaterialIcon {
|
||||
id: searchIcon
|
||||
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: root.padding
|
||||
|
||||
text: "search"
|
||||
color: Colours.palette.m3onSurfaceVariant
|
||||
}
|
||||
|
||||
StyledTextField {
|
||||
id: search
|
||||
|
||||
anchors.left: searchIcon.right
|
||||
anchors.right: clearIcon.left
|
||||
anchors.leftMargin: Appearance.spacing.small
|
||||
anchors.rightMargin: Appearance.spacing.small
|
||||
|
||||
topPadding: Appearance.padding.larger
|
||||
bottomPadding: Appearance.padding.larger
|
||||
|
||||
placeholderText: qsTr("Type \"%1\" for commands").arg(LauncherConfig.actionPrefix)
|
||||
background: null
|
||||
|
||||
onAccepted: {
|
||||
const currentItem = list.currentList?.currentItem;
|
||||
if (currentItem) {
|
||||
if (list.showWallpapers) {
|
||||
Wallpapers.setWallpaper(currentItem.modelData.path);
|
||||
root.visibilities.launcher = false;
|
||||
} else if (text.startsWith(LauncherConfig.actionPrefix)) {
|
||||
currentItem.modelData.onClicked(list.currentList);
|
||||
} else {
|
||||
Apps.launch(currentItem.modelData);
|
||||
root.visibilities.launcher = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Keys.onUpPressed: list.currentList?.decrementCurrentIndex()
|
||||
Keys.onDownPressed: list.currentList?.incrementCurrentIndex()
|
||||
|
||||
Keys.onEscapePressed: root.visibilities.launcher = false
|
||||
|
||||
Connections {
|
||||
target: root.visibilities
|
||||
|
||||
function onLauncherChanged(): void {
|
||||
if (root.visibilities.launcher)
|
||||
search.forceActiveFocus();
|
||||
else {
|
||||
search.text = "";
|
||||
const current = list.currentList;
|
||||
if (current)
|
||||
current.currentIndex = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MaterialIcon {
|
||||
id: clearIcon
|
||||
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: root.padding
|
||||
|
||||
width: search.text ? implicitWidth : implicitWidth / 2
|
||||
opacity: {
|
||||
if (!search.text)
|
||||
return 0;
|
||||
if (mouse.pressed)
|
||||
return 0.7;
|
||||
if (mouse.hovered)
|
||||
return 0.8;
|
||||
return 1;
|
||||
}
|
||||
|
||||
text: "close"
|
||||
color: Colours.palette.m3onSurfaceVariant
|
||||
|
||||
MouseArea {
|
||||
id: mouse
|
||||
|
||||
property bool hovered
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: search.text ? Qt.PointingHandCursor : undefined
|
||||
|
||||
onEntered: hovered = true
|
||||
onExited: hovered = false
|
||||
onClicked: search.text = ""
|
||||
}
|
||||
|
||||
Behavior on width {
|
||||
NumberAnimation {
|
||||
duration: Appearance.anim.durations.small
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: Appearance.anim.curves.standard
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: Appearance.anim.durations.small
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: Appearance.anim.curves.standard
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
188
modules/launcher/ContentList.qml
Normal file
188
modules/launcher/ContentList.qml
Normal file
|
@ -0,0 +1,188 @@
|
|||
pragma ComponentBehavior: Bound
|
||||
|
||||
import "root:/widgets"
|
||||
import "root:/services"
|
||||
import "root:/config"
|
||||
import Quickshell
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
required property PersistentProperties visibilities
|
||||
required property TextField search
|
||||
required property int padding
|
||||
required property int rounding
|
||||
|
||||
property bool showWallpapers: search.text.startsWith(`${LauncherConfig.actionPrefix}wallpaper `)
|
||||
property var currentList: (showWallpapers ? wallpaperList : appList).item
|
||||
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.bottom: parent.bottom
|
||||
|
||||
clip: true
|
||||
state: showWallpapers ? "wallpapers" : "apps"
|
||||
|
||||
states: [
|
||||
State {
|
||||
name: "apps"
|
||||
|
||||
PropertyChanges {
|
||||
root.implicitWidth: LauncherConfig.sizes.itemWidth
|
||||
root.implicitHeight: Math.max(empty.height, appList.height)
|
||||
appList.active: true
|
||||
}
|
||||
|
||||
AnchorChanges {
|
||||
anchors.left: root.parent.left
|
||||
anchors.right: root.parent.right
|
||||
}
|
||||
},
|
||||
State {
|
||||
name: "wallpapers"
|
||||
|
||||
PropertyChanges {
|
||||
root.implicitWidth: Math.max(LauncherConfig.sizes.itemWidth, wallpaperList.width)
|
||||
root.implicitHeight: LauncherConfig.sizes.wallpaperHeight
|
||||
wallpaperList.active: true
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
transitions: Transition {
|
||||
SequentialAnimation {
|
||||
NumberAnimation {
|
||||
target: root
|
||||
property: "opacity"
|
||||
from: 1
|
||||
to: 0
|
||||
duration: Appearance.anim.durations.small
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: Appearance.anim.curves.standard
|
||||
}
|
||||
PropertyAction {
|
||||
targets: [appList, wallpaperList]
|
||||
properties: "active"
|
||||
}
|
||||
ParallelAnimation {
|
||||
NumberAnimation {
|
||||
target: root
|
||||
properties: "implicitWidth,implicitHeight"
|
||||
duration: Appearance.anim.durations.large
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: Appearance.anim.curves.emphasized
|
||||
}
|
||||
NumberAnimation {
|
||||
target: root
|
||||
property: "opacity"
|
||||
from: 0
|
||||
to: 1
|
||||
duration: Appearance.anim.durations.large
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: Appearance.anim.curves.standard
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: appList
|
||||
|
||||
active: false
|
||||
asynchronous: true
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
|
||||
sourceComponent: AppList {
|
||||
padding: root.padding
|
||||
search: root.search
|
||||
visibilities: root.visibilities
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: wallpaperList
|
||||
|
||||
active: false
|
||||
asynchronous: true
|
||||
|
||||
anchors.top: parent.top
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
|
||||
sourceComponent: WallpaperList {
|
||||
search: root.search
|
||||
visibilities: root.visibilities
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
id: empty
|
||||
|
||||
opacity: root.currentList?.count === 0 ? 1 : 0
|
||||
scale: root.currentList?.count === 0 ? 1 : 0.5
|
||||
|
||||
implicitWidth: icon.width + text.width + Appearance.spacing.small
|
||||
implicitHeight: icon.height
|
||||
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
MaterialIcon {
|
||||
id: icon
|
||||
|
||||
text: "manage_search"
|
||||
color: Colours.palette.m3onSurfaceVariant
|
||||
font.pointSize: Appearance.font.size.extraLarge
|
||||
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
id: text
|
||||
|
||||
anchors.left: icon.right
|
||||
anchors.leftMargin: Appearance.spacing.small
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
text: qsTr("No results")
|
||||
color: Colours.palette.m3onSurfaceVariant
|
||||
font.pointSize: Appearance.font.size.larger
|
||||
font.weight: 500
|
||||
}
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: Appearance.anim.durations.normal
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: Appearance.anim.curves.standard
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on scale {
|
||||
NumberAnimation {
|
||||
duration: Appearance.anim.durations.normal
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: Appearance.anim.curves.standard
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on implicitWidth {
|
||||
NumberAnimation {
|
||||
duration: Appearance.anim.durations.large
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: Appearance.anim.curves.emphasizedDecel
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on implicitHeight {
|
||||
NumberAnimation {
|
||||
duration: Appearance.anim.durations.large
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: Appearance.anim.curves.emphasizedDecel
|
||||
}
|
||||
}
|
||||
}
|
109
modules/launcher/WallpaperItem.qml
Normal file
109
modules/launcher/WallpaperItem.qml
Normal file
|
@ -0,0 +1,109 @@
|
|||
import "root:/widgets"
|
||||
import "root:/services"
|
||||
import "root:/config"
|
||||
import Quickshell
|
||||
import QtQuick
|
||||
import QtQuick.Effects
|
||||
|
||||
StyledRect {
|
||||
id: root
|
||||
|
||||
required property Wallpapers.Wallpaper modelData
|
||||
required property PersistentProperties visibilities
|
||||
|
||||
scale: 0.5
|
||||
opacity: 0
|
||||
z: PathView.z ?? 0
|
||||
|
||||
Component.onCompleted: {
|
||||
scale = Qt.binding(() => PathView.isCurrentItem ? 1 : PathView.onPath ? 0.8 : 0);
|
||||
opacity = Qt.binding(() => PathView.onPath ? 1 : 0);
|
||||
}
|
||||
|
||||
implicitWidth: image.width + Appearance.padding.larger * 2
|
||||
implicitHeight: image.height + label.height + Appearance.spacing.small / 2 + Appearance.padding.large + Appearance.padding.normal
|
||||
|
||||
StateLayer {
|
||||
radius: Appearance.rounding.normal
|
||||
|
||||
function onClicked(): void {
|
||||
Wallpapers.setWallpaper(root.modelData.path);
|
||||
root.visibilities.launcher = false;
|
||||
}
|
||||
}
|
||||
|
||||
CachingImage {
|
||||
id: image
|
||||
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
y: Appearance.padding.large
|
||||
|
||||
visible: false
|
||||
path: root.modelData.path
|
||||
smooth: !root.PathView.view.moving
|
||||
|
||||
width: LauncherConfig.sizes.wallpaperWidth
|
||||
height: width / 16 * 9
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: mask
|
||||
|
||||
layer.enabled: true
|
||||
layer.smooth: true
|
||||
visible: false
|
||||
anchors.fill: image
|
||||
radius: Appearance.rounding.normal
|
||||
}
|
||||
|
||||
RectangularShadow {
|
||||
opacity: root.PathView.isCurrentItem ? 0.7 : 0
|
||||
anchors.fill: mask
|
||||
radius: mask.radius
|
||||
color: Colours.palette.m3shadow
|
||||
blur: 10
|
||||
spread: 3
|
||||
|
||||
Behavior on opacity {
|
||||
Anim {}
|
||||
}
|
||||
}
|
||||
|
||||
MultiEffect {
|
||||
anchors.fill: image
|
||||
source: image
|
||||
maskEnabled: true
|
||||
maskSource: mask
|
||||
maskSpreadAtMin: 1
|
||||
maskThresholdMin: 0.5
|
||||
}
|
||||
|
||||
StyledText {
|
||||
id: label
|
||||
|
||||
anchors.top: image.bottom
|
||||
anchors.topMargin: Appearance.spacing.small / 2
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
|
||||
width: image.width - Appearance.padding.normal * 2
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
elide: Text.ElideRight
|
||||
renderType: Text.QtRendering
|
||||
text: root.modelData.name
|
||||
font.pointSize: Appearance.font.size.normal
|
||||
}
|
||||
|
||||
Behavior on scale {
|
||||
Anim {}
|
||||
}
|
||||
|
||||
Behavior on opacity {
|
||||
Anim {}
|
||||
}
|
||||
|
||||
component Anim: NumberAnimation {
|
||||
duration: Appearance.anim.durations.normal
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: Appearance.anim.curves.standard
|
||||
}
|
||||
}
|
79
modules/launcher/WallpaperList.qml
Normal file
79
modules/launcher/WallpaperList.qml
Normal file
|
@ -0,0 +1,79 @@
|
|||
import "root:/widgets"
|
||||
import "root:/services"
|
||||
import "root:/config"
|
||||
import Quickshell
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
|
||||
PathView {
|
||||
id: root
|
||||
|
||||
required property TextField search
|
||||
required property PersistentProperties visibilities
|
||||
readonly property int numItems: {
|
||||
const screenWidth = QsWindow.window?.screen.width * 0.8;
|
||||
if (!screenWidth)
|
||||
return 0;
|
||||
const itemWidth = LauncherConfig.sizes.wallpaperWidth * 0.8;
|
||||
const max = LauncherConfig.maxWallpapers;
|
||||
if (max * itemWidth > screenWidth) {
|
||||
const items = Math.floor(screenWidth / itemWidth);
|
||||
return items > 1 && items % 2 === 0 ? items - 1 : items;
|
||||
}
|
||||
return max;
|
||||
}
|
||||
|
||||
model: ScriptModel {
|
||||
readonly property string search: root.search.text.split(" ").slice(1).join(" ")
|
||||
|
||||
values: {
|
||||
const list = Wallpapers.fuzzyQuery(search);
|
||||
if (list.length > 1 && list.length % 2 === 0)
|
||||
list.length -= 1; // Always show odd number
|
||||
return list;
|
||||
}
|
||||
onValuesChanged: root.currentIndex = search ? 0 : values.findIndex(w => w.path === Wallpapers.actualCurrent)
|
||||
}
|
||||
|
||||
Component.onCompleted: currentIndex = Wallpapers.list.findIndex(w => w.path === Wallpapers.actualCurrent)
|
||||
Component.onDestruction: Wallpapers.stopPreview()
|
||||
|
||||
onCurrentItemChanged: {
|
||||
if (currentItem)
|
||||
Wallpapers.preview(currentItem.modelData.path);
|
||||
}
|
||||
|
||||
implicitWidth: Math.min(numItems, count) * (LauncherConfig.sizes.wallpaperWidth * 0.8 + Appearance.padding.larger * 2)
|
||||
pathItemCount: numItems
|
||||
cacheItemCount: 4
|
||||
|
||||
snapMode: PathView.SnapToItem
|
||||
preferredHighlightBegin: 0.5
|
||||
preferredHighlightEnd: 0.5
|
||||
highlightRangeMode: PathView.StrictlyEnforceRange
|
||||
|
||||
delegate: WallpaperItem {
|
||||
visibilities: root.visibilities
|
||||
}
|
||||
|
||||
path: Path {
|
||||
startY: root.height / 2
|
||||
|
||||
PathAttribute {
|
||||
name: "z"
|
||||
value: 0
|
||||
}
|
||||
PathLine {
|
||||
x: root.width / 2
|
||||
relativeY: 0
|
||||
}
|
||||
PathAttribute {
|
||||
name: "z"
|
||||
value: 1
|
||||
}
|
||||
PathLine {
|
||||
x: root.width
|
||||
relativeY: 0
|
||||
}
|
||||
}
|
||||
}
|
55
modules/launcher/Wrapper.qml
Normal file
55
modules/launcher/Wrapper.qml
Normal file
|
@ -0,0 +1,55 @@
|
|||
import "root:/config"
|
||||
import Quickshell
|
||||
import QtQuick
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
required property PersistentProperties visibilities
|
||||
|
||||
visible: height > 0
|
||||
implicitHeight: 0
|
||||
implicitWidth: content.implicitWidth
|
||||
|
||||
states: State {
|
||||
name: "visible"
|
||||
when: root.visibilities.launcher
|
||||
|
||||
PropertyChanges {
|
||||
root.implicitHeight: content.implicitHeight
|
||||
}
|
||||
}
|
||||
|
||||
transitions: [
|
||||
Transition {
|
||||
from: ""
|
||||
to: "visible"
|
||||
|
||||
NumberAnimation {
|
||||
target: root
|
||||
property: "implicitHeight"
|
||||
duration: Appearance.anim.durations.expressiveDefaultSpatial
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial
|
||||
}
|
||||
},
|
||||
Transition {
|
||||
from: "visible"
|
||||
to: ""
|
||||
|
||||
NumberAnimation {
|
||||
target: root
|
||||
property: "implicitHeight"
|
||||
duration: Appearance.anim.durations.normal
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: Appearance.anim.curves.emphasized
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
Content {
|
||||
id: content
|
||||
|
||||
visibilities: root.visibilities
|
||||
}
|
||||
}
|
66
modules/notifications/Background.qml
Normal file
66
modules/notifications/Background.qml
Normal file
|
@ -0,0 +1,66 @@
|
|||
import "root:/services"
|
||||
import "root:/config"
|
||||
import Quickshell
|
||||
import QtQuick
|
||||
import QtQuick.Shapes
|
||||
|
||||
ShapePath {
|
||||
id: root
|
||||
|
||||
required property Wrapper wrapper
|
||||
readonly property real rounding: BorderConfig.rounding
|
||||
readonly property bool flatten: wrapper.height < rounding * 2
|
||||
readonly property real roundingY: flatten ? wrapper.height / 2 : rounding
|
||||
property real fullHeightRounding: wrapper.height >= QsWindow.window?.height - BorderConfig.thickness * 2 ? -rounding : rounding
|
||||
|
||||
strokeWidth: -1
|
||||
fillColor: BorderConfig.colour
|
||||
|
||||
PathLine {
|
||||
relativeX: -(root.wrapper.width + root.rounding)
|
||||
relativeY: 0
|
||||
}
|
||||
PathArc {
|
||||
relativeX: root.rounding
|
||||
relativeY: root.roundingY
|
||||
radiusX: root.rounding
|
||||
radiusY: Math.min(root.rounding, root.wrapper.height)
|
||||
}
|
||||
PathLine {
|
||||
relativeX: 0
|
||||
relativeY: root.wrapper.height - root.roundingY * 2
|
||||
}
|
||||
PathArc {
|
||||
relativeX: root.fullHeightRounding
|
||||
relativeY: root.roundingY
|
||||
radiusX: Math.abs(root.fullHeightRounding)
|
||||
radiusY: Math.min(root.rounding, root.wrapper.height)
|
||||
direction: root.fullHeightRounding < 0 ? PathArc.Clockwise : PathArc.Counterclockwise
|
||||
}
|
||||
PathLine {
|
||||
relativeX: root.wrapper.height > 0 ? root.wrapper.width - root.rounding - root.fullHeightRounding : root.wrapper.width
|
||||
relativeY: 0
|
||||
}
|
||||
PathArc {
|
||||
relativeX: root.rounding
|
||||
relativeY: root.rounding
|
||||
radiusX: root.rounding
|
||||
radiusY: root.rounding
|
||||
}
|
||||
|
||||
Behavior on fillColor {
|
||||
ColorAnimation {
|
||||
duration: Appearance.anim.durations.normal
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: Appearance.anim.curves.standard
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on fullHeightRounding {
|
||||
NumberAnimation {
|
||||
duration: Appearance.anim.durations.normal
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: Appearance.anim.curves.standard
|
||||
}
|
||||
}
|
||||
}
|
162
modules/notifications/Content.qml
Normal file
162
modules/notifications/Content.qml
Normal file
|
@ -0,0 +1,162 @@
|
|||
import "root:/widgets"
|
||||
import "root:/services"
|
||||
import "root:/config"
|
||||
import Quickshell
|
||||
import Quickshell.Widgets
|
||||
import QtQuick
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
readonly property int padding: Appearance.padding.large
|
||||
|
||||
anchors.top: parent.top
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.right: parent.right
|
||||
|
||||
implicitWidth: NotifsConfig.sizes.width + padding * 2
|
||||
implicitHeight: {
|
||||
const count = list.count;
|
||||
if (count === 0)
|
||||
return 0;
|
||||
|
||||
let height = (count - 1) * Appearance.spacing.smaller;
|
||||
for (let i = 0; i < count; i++)
|
||||
height += list.itemAtIndex(i)?.nonAnimHeight ?? 0;
|
||||
|
||||
const screen = QsWindow.window?.screen;
|
||||
const visibilities = Visibilities.screens[screen];
|
||||
const panel = Visibilities.panels[screen];
|
||||
if (visibilities && panel) {
|
||||
if (visibilities.osd) {
|
||||
const h = panel.osd.y - BorderConfig.rounding * 2;
|
||||
if (height > h)
|
||||
height = h;
|
||||
}
|
||||
|
||||
if (visibilities.session) {
|
||||
const h = panel.session.y - BorderConfig.rounding * 2;
|
||||
if (height > h)
|
||||
height = h;
|
||||
}
|
||||
}
|
||||
|
||||
return Math.min((screen?.height ?? 0) - BorderConfig.thickness * 2, height + padding * 2);
|
||||
}
|
||||
|
||||
ClippingWrapperRectangle {
|
||||
anchors.fill: parent
|
||||
anchors.margins: root.padding
|
||||
|
||||
color: "transparent"
|
||||
radius: Appearance.rounding.normal
|
||||
|
||||
ListView {
|
||||
id: list
|
||||
|
||||
model: ScriptModel {
|
||||
values: [...Notifs.popups].reverse()
|
||||
}
|
||||
|
||||
anchors.fill: parent
|
||||
|
||||
orientation: Qt.Vertical
|
||||
spacing: 0
|
||||
cacheBuffer: QsWindow.window?.screen.height ?? 0
|
||||
|
||||
delegate: Item {
|
||||
id: wrapper
|
||||
|
||||
required property Notifs.Notif modelData
|
||||
required property int index
|
||||
readonly property alias nonAnimHeight: notif.nonAnimHeight
|
||||
property int idx
|
||||
|
||||
onIndexChanged: {
|
||||
if (index !== -1)
|
||||
idx = index;
|
||||
}
|
||||
|
||||
implicitWidth: notif.implicitWidth
|
||||
implicitHeight: notif.implicitHeight + (idx === 0 ? 0 : Appearance.spacing.smaller)
|
||||
|
||||
ListView.onRemove: removeAnim.start()
|
||||
|
||||
SequentialAnimation {
|
||||
id: removeAnim
|
||||
|
||||
PropertyAction {
|
||||
target: wrapper
|
||||
property: "ListView.delayRemove"
|
||||
value: true
|
||||
}
|
||||
PropertyAction {
|
||||
target: wrapper
|
||||
property: "enabled"
|
||||
value: false
|
||||
}
|
||||
PropertyAction {
|
||||
target: wrapper
|
||||
property: "implicitHeight"
|
||||
value: 0
|
||||
}
|
||||
PropertyAction {
|
||||
target: wrapper
|
||||
property: "z"
|
||||
value: 1
|
||||
}
|
||||
Anim {
|
||||
target: notif
|
||||
property: "x"
|
||||
to: (notif.x >= 0 ? NotifsConfig.sizes.width : -NotifsConfig.sizes.width) * 2
|
||||
duration: Appearance.anim.durations.normal
|
||||
easing.bezierCurve: Appearance.anim.curves.emphasized
|
||||
}
|
||||
PropertyAction {
|
||||
target: wrapper
|
||||
property: "ListView.delayRemove"
|
||||
value: false
|
||||
}
|
||||
}
|
||||
|
||||
ClippingRectangle {
|
||||
anchors.top: parent.top
|
||||
anchors.topMargin: wrapper.idx === 0 ? 0 : Appearance.spacing.smaller
|
||||
|
||||
color: "transparent"
|
||||
radius: notif.radius
|
||||
implicitWidth: notif.implicitWidth
|
||||
implicitHeight: notif.implicitHeight
|
||||
|
||||
Notification {
|
||||
id: notif
|
||||
|
||||
modelData: wrapper.modelData
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
move: Transition {
|
||||
Anim {
|
||||
property: "y"
|
||||
}
|
||||
}
|
||||
|
||||
displaced: Transition {
|
||||
Anim {
|
||||
property: "y"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on implicitHeight {
|
||||
Anim {}
|
||||
}
|
||||
|
||||
component Anim: NumberAnimation {
|
||||
duration: Appearance.anim.durations.expressiveDefaultSpatial
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial
|
||||
}
|
||||
}
|
489
modules/notifications/Notification.qml
Normal file
489
modules/notifications/Notification.qml
Normal file
|
@ -0,0 +1,489 @@
|
|||
pragma ComponentBehavior: Bound
|
||||
|
||||
import "root:/widgets"
|
||||
import "root:/services"
|
||||
import "root:/config"
|
||||
import Quickshell
|
||||
import Quickshell.Widgets
|
||||
import Quickshell.Services.Notifications
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
|
||||
StyledRect {
|
||||
id: root
|
||||
|
||||
required property Notifs.Notif modelData
|
||||
readonly property bool hasImage: modelData.image.length > 0
|
||||
readonly property bool hasAppIcon: modelData.appIcon.length > 0
|
||||
readonly property int nonAnimHeight: summary.implicitHeight + (root.expanded ? appName.height + body.height + actions.height + actions.anchors.topMargin : bodyPreview.height) + inner.anchors.margins * 2
|
||||
property bool expanded
|
||||
|
||||
color: root.modelData.urgency === NotificationUrgency.Critical ? Colours.palette.m3secondaryContainer : Colours.palette.m3surfaceContainer
|
||||
radius: Appearance.rounding.normal
|
||||
implicitWidth: NotifsConfig.sizes.width
|
||||
implicitHeight: inner.implicitHeight
|
||||
|
||||
x: NotifsConfig.sizes.width
|
||||
Component.onCompleted: x = 0
|
||||
|
||||
RetainableLock {
|
||||
object: root.modelData.notification
|
||||
locked: true
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
property int startY
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: pressed ? Qt.ClosedHandCursor : undefined
|
||||
acceptedButtons: Qt.LeftButton | Qt.MiddleButton
|
||||
preventStealing: true
|
||||
|
||||
onEntered: root.modelData.timer.stop()
|
||||
onExited: root.modelData.timer.start()
|
||||
|
||||
drag.target: parent
|
||||
drag.axis: Drag.XAxis
|
||||
|
||||
onPressed: event => {
|
||||
startY = event.y;
|
||||
if (event.button === Qt.MiddleButton)
|
||||
root.modelData.notification.dismiss();
|
||||
}
|
||||
onReleased: event => {
|
||||
if (Math.abs(root.x) < NotifsConfig.sizes.width * NotifsConfig.clearThreshold)
|
||||
root.x = 0;
|
||||
else
|
||||
root.modelData.popup = false;
|
||||
}
|
||||
onPositionChanged: event => {
|
||||
if (pressed) {
|
||||
const diffY = event.y - startY;
|
||||
if (Math.abs(diffY) > NotifsConfig.expandThreshold)
|
||||
root.expanded = diffY > 0;
|
||||
}
|
||||
}
|
||||
onClicked: event => {
|
||||
if (!NotifsConfig.actionOnClick || event.button !== Qt.LeftButton)
|
||||
return;
|
||||
|
||||
const actions = root.modelData.actions;
|
||||
if (actions?.length === 1)
|
||||
actions[0].invoke();
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on x {
|
||||
NumberAnimation {
|
||||
duration: Appearance.anim.durations.normal
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: Appearance.anim.curves.emphasizedDecel
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
id: inner
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
anchors.margins: Appearance.padding.normal
|
||||
|
||||
implicitHeight: root.nonAnimHeight
|
||||
|
||||
Behavior on implicitHeight {
|
||||
Anim {}
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: image
|
||||
|
||||
active: root.hasImage
|
||||
asynchronous: true
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.top: parent.top
|
||||
width: NotifsConfig.sizes.image
|
||||
height: NotifsConfig.sizes.image
|
||||
visible: root.hasImage || root.hasAppIcon
|
||||
|
||||
sourceComponent: ClippingRectangle {
|
||||
radius: Appearance.rounding.full
|
||||
implicitWidth: NotifsConfig.sizes.image
|
||||
implicitHeight: NotifsConfig.sizes.image
|
||||
|
||||
Image {
|
||||
anchors.fill: parent
|
||||
source: Qt.resolvedUrl(root.modelData.image)
|
||||
fillMode: Image.PreserveAspectCrop
|
||||
cache: false
|
||||
asynchronous: true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: appIcon
|
||||
|
||||
active: root.hasAppIcon || !root.hasImage
|
||||
asynchronous: true
|
||||
|
||||
anchors.horizontalCenter: root.hasImage ? undefined : image.horizontalCenter
|
||||
anchors.verticalCenter: root.hasImage ? undefined : image.verticalCenter
|
||||
anchors.right: root.hasImage ? image.right : undefined
|
||||
anchors.bottom: root.hasImage ? image.bottom : undefined
|
||||
|
||||
sourceComponent: StyledRect {
|
||||
radius: Appearance.rounding.full
|
||||
color: root.modelData.urgency === NotificationUrgency.Critical ? Colours.palette.m3error : root.modelData.urgency === NotificationUrgency.Low ? Colours.palette.m3surfaceContainerHighest : Colours.palette.m3tertiaryContainer
|
||||
implicitWidth: root.hasImage ? NotifsConfig.sizes.badge : NotifsConfig.sizes.image
|
||||
implicitHeight: root.hasImage ? NotifsConfig.sizes.badge : NotifsConfig.sizes.image
|
||||
|
||||
Loader {
|
||||
id: icon
|
||||
|
||||
active: root.hasAppIcon
|
||||
asynchronous: true
|
||||
|
||||
anchors.centerIn: parent
|
||||
visible: !root.modelData.appIcon.endsWith("symbolic")
|
||||
|
||||
width: Math.round(parent.width * 0.6)
|
||||
height: Math.round(parent.width * 0.6)
|
||||
|
||||
sourceComponent: IconImage {
|
||||
implicitSize: Math.round(parent.width * 0.6)
|
||||
source: Quickshell.iconPath(root.modelData.appIcon)
|
||||
asynchronous: true
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
active: root.modelData.appIcon.endsWith("symbolic")
|
||||
asynchronous: true
|
||||
anchors.fill: icon
|
||||
|
||||
sourceComponent: Colouriser {
|
||||
source: icon
|
||||
colorizationColor: root.modelData.urgency === NotificationUrgency.Critical ? Colours.palette.m3onError : root.modelData.urgency === NotificationUrgency.Low ? Colours.palette.m3onSurface : Colours.palette.m3onTertiaryContainer
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
active: !root.hasAppIcon
|
||||
asynchronous: true
|
||||
anchors.centerIn: parent
|
||||
|
||||
sourceComponent: MaterialIcon {
|
||||
text: {
|
||||
const summary = root.modelData.summary.toLowerCase();
|
||||
if (summary.includes("reboot"))
|
||||
return "restart_alt";
|
||||
if (summary.includes("recording"))
|
||||
return "screen_record";
|
||||
if (summary.includes("battery"))
|
||||
return "power";
|
||||
if (summary.includes("screenshot"))
|
||||
return "screenshot_monitor";
|
||||
if (summary.includes("welcome"))
|
||||
return "waving_hand";
|
||||
if (summary.includes("time") || summary.includes("a break"))
|
||||
return "schedule";
|
||||
if (summary.includes("installed"))
|
||||
return "download";
|
||||
if (summary.includes("update"))
|
||||
return "update";
|
||||
if (summary.startsWith("file"))
|
||||
return "folder_copy";
|
||||
if (root.modelData.urgency === NotificationUrgency.Critical)
|
||||
return "release_alert";
|
||||
return "chat";
|
||||
}
|
||||
|
||||
color: root.modelData.urgency === NotificationUrgency.Critical ? Colours.palette.m3onError : root.modelData.urgency === NotificationUrgency.Low ? Colours.palette.m3onSurface : Colours.palette.m3onTertiaryContainer
|
||||
font.pointSize: Appearance.font.size.large
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
id: appName
|
||||
|
||||
anchors.top: parent.top
|
||||
anchors.left: image.right
|
||||
anchors.leftMargin: Appearance.spacing.smaller
|
||||
|
||||
animate: true
|
||||
text: appNameMetrics.elidedText
|
||||
maximumLineCount: 1
|
||||
color: Colours.palette.m3onSurfaceVariant
|
||||
font.pointSize: Appearance.font.size.small
|
||||
|
||||
opacity: root.expanded ? 1 : 0
|
||||
|
||||
Behavior on opacity {
|
||||
Anim {}
|
||||
}
|
||||
}
|
||||
|
||||
TextMetrics {
|
||||
id: appNameMetrics
|
||||
|
||||
text: root.modelData.appName
|
||||
font.family: appName.font.family
|
||||
font.pointSize: appName.font.pointSize
|
||||
elide: Text.ElideRight
|
||||
elideWidth: expandBtn.x - time.width - timeSep.width - summary.x - Appearance.spacing.small * 3
|
||||
}
|
||||
|
||||
StyledText {
|
||||
id: summary
|
||||
|
||||
anchors.top: parent.top
|
||||
anchors.left: image.right
|
||||
anchors.leftMargin: Appearance.spacing.smaller
|
||||
|
||||
animate: true
|
||||
text: summaryMetrics.elidedText
|
||||
maximumLineCount: 1
|
||||
height: implicitHeight
|
||||
|
||||
states: State {
|
||||
name: "expanded"
|
||||
when: root.expanded
|
||||
|
||||
PropertyChanges {
|
||||
summary.maximumLineCount: undefined
|
||||
}
|
||||
|
||||
AnchorChanges {
|
||||
target: summary
|
||||
anchors.top: appName.bottom
|
||||
}
|
||||
}
|
||||
|
||||
transitions: Transition {
|
||||
PropertyAction {
|
||||
target: summary
|
||||
property: "maximumLineCount"
|
||||
}
|
||||
AnchorAnimation {
|
||||
duration: Appearance.anim.durations.normal
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: Appearance.anim.curves.standard
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on height {
|
||||
Anim {}
|
||||
}
|
||||
}
|
||||
|
||||
TextMetrics {
|
||||
id: summaryMetrics
|
||||
|
||||
text: root.modelData.summary
|
||||
font.family: summary.font.family
|
||||
font.pointSize: summary.font.pointSize
|
||||
elide: Text.ElideRight
|
||||
elideWidth: expandBtn.x - time.width - timeSep.width - summary.x - Appearance.spacing.small * 3
|
||||
}
|
||||
|
||||
StyledText {
|
||||
id: timeSep
|
||||
|
||||
anchors.top: parent.top
|
||||
anchors.left: summary.right
|
||||
anchors.leftMargin: Appearance.spacing.small
|
||||
|
||||
text: "•"
|
||||
color: Colours.palette.m3onSurfaceVariant
|
||||
font.pointSize: Appearance.font.size.small
|
||||
|
||||
states: State {
|
||||
name: "expanded"
|
||||
when: root.expanded
|
||||
|
||||
AnchorChanges {
|
||||
target: timeSep
|
||||
anchors.left: appName.right
|
||||
}
|
||||
}
|
||||
|
||||
transitions: Transition {
|
||||
AnchorAnimation {
|
||||
duration: Appearance.anim.durations.normal
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: Appearance.anim.curves.standard
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
id: time
|
||||
|
||||
anchors.top: parent.top
|
||||
anchors.left: timeSep.right
|
||||
anchors.leftMargin: Appearance.spacing.small
|
||||
|
||||
animate: true
|
||||
horizontalAlignment: Text.AlignLeft
|
||||
text: root.modelData.timeStr
|
||||
color: Colours.palette.m3onSurfaceVariant
|
||||
font.pointSize: Appearance.font.size.small
|
||||
}
|
||||
|
||||
Item {
|
||||
id: expandBtn
|
||||
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
|
||||
implicitWidth: expandIcon.height
|
||||
implicitHeight: expandIcon.height
|
||||
|
||||
StateLayer {
|
||||
radius: Appearance.rounding.full
|
||||
|
||||
function onClicked() {
|
||||
root.expanded = !root.expanded;
|
||||
}
|
||||
}
|
||||
|
||||
MaterialIcon {
|
||||
id: expandIcon
|
||||
|
||||
anchors.centerIn: parent
|
||||
|
||||
animate: true
|
||||
text: root.expanded ? "expand_less" : "expand_more"
|
||||
font.pointSize: Appearance.font.size.normal
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
id: bodyPreview
|
||||
|
||||
anchors.left: summary.left
|
||||
anchors.right: expandBtn.left
|
||||
anchors.top: summary.bottom
|
||||
anchors.rightMargin: Appearance.spacing.small
|
||||
|
||||
animate: true
|
||||
textFormat: Text.MarkdownText
|
||||
text: bodyPreviewMetrics.elidedText
|
||||
color: Colours.palette.m3onSurfaceVariant
|
||||
font.pointSize: Appearance.font.size.small
|
||||
|
||||
opacity: root.expanded ? 0 : 1
|
||||
|
||||
Behavior on opacity {
|
||||
Anim {}
|
||||
}
|
||||
}
|
||||
|
||||
TextMetrics {
|
||||
id: bodyPreviewMetrics
|
||||
|
||||
text: root.modelData.body
|
||||
font.family: bodyPreview.font.family
|
||||
font.pointSize: bodyPreview.font.pointSize
|
||||
elide: Text.ElideRight
|
||||
elideWidth: bodyPreview.width
|
||||
}
|
||||
|
||||
StyledText {
|
||||
id: body
|
||||
|
||||
anchors.left: summary.left
|
||||
anchors.right: expandBtn.left
|
||||
anchors.top: summary.bottom
|
||||
anchors.rightMargin: Appearance.spacing.small
|
||||
|
||||
animate: true
|
||||
textFormat: Text.MarkdownText
|
||||
text: root.modelData.body
|
||||
color: Colours.palette.m3onSurfaceVariant
|
||||
font.pointSize: Appearance.font.size.small
|
||||
wrapMode: Text.WrapAtWordBoundaryOrAnywhere
|
||||
|
||||
opacity: root.expanded ? 1 : 0
|
||||
|
||||
Behavior on opacity {
|
||||
Anim {}
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
id: actions
|
||||
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.top: body.bottom
|
||||
anchors.topMargin: Appearance.spacing.small
|
||||
|
||||
spacing: Appearance.spacing.smaller
|
||||
|
||||
opacity: root.expanded ? 1 : 0
|
||||
|
||||
Behavior on opacity {
|
||||
Anim {}
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: root.modelData.actions
|
||||
|
||||
delegate: StyledRect {
|
||||
id: action
|
||||
|
||||
required property NotificationAction modelData
|
||||
|
||||
radius: Appearance.rounding.full
|
||||
color: Colours.palette.m3surfaceContainerHigh
|
||||
|
||||
Layout.preferredWidth: actionText.width + Appearance.padding.normal * 2
|
||||
Layout.preferredHeight: actionText.height + Appearance.padding.small * 2
|
||||
implicitWidth: actionText.width + Appearance.padding.normal * 2
|
||||
implicitHeight: actionText.height + Appearance.padding.small * 2
|
||||
|
||||
StateLayer {
|
||||
radius: Appearance.rounding.full
|
||||
|
||||
function onClicked(): void {
|
||||
action.modelData.invoke();
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
id: actionText
|
||||
|
||||
anchors.centerIn: parent
|
||||
text: actionTextMetrics.elidedText
|
||||
color: Colours.palette.m3onSurfaceVariant
|
||||
font.pointSize: Appearance.font.size.small
|
||||
}
|
||||
|
||||
TextMetrics {
|
||||
id: actionTextMetrics
|
||||
|
||||
text: modelData.text
|
||||
font.family: actionText.font.family
|
||||
font.pointSize: actionText.font.pointSize
|
||||
elide: Text.ElideRight
|
||||
elideWidth: {
|
||||
const numActions = root.modelData.actions.length;
|
||||
return (inner.width - actions.spacing * (numActions - 1)) / numActions - Appearance.padding.normal * 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
component Anim: NumberAnimation {
|
||||
duration: Appearance.anim.durations.normal
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: Appearance.anim.curves.standard
|
||||
}
|
||||
}
|
14
modules/notifications/Wrapper.qml
Normal file
14
modules/notifications/Wrapper.qml
Normal file
|
@ -0,0 +1,14 @@
|
|||
import "root:/config"
|
||||
import QtQuick
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
visible: height > 0
|
||||
implicitHeight: content.implicitHeight
|
||||
implicitWidth: content.implicitWidth
|
||||
|
||||
Content {
|
||||
id: content
|
||||
}
|
||||
}
|
63
modules/osd/Background.qml
Normal file
63
modules/osd/Background.qml
Normal file
|
@ -0,0 +1,63 @@
|
|||
import "root:/services"
|
||||
import "root:/config"
|
||||
import QtQuick
|
||||
import QtQuick.Shapes
|
||||
|
||||
ShapePath {
|
||||
id: root
|
||||
|
||||
required property Wrapper wrapper
|
||||
readonly property real rounding: BorderConfig.rounding
|
||||
readonly property bool flatten: wrapper.width < rounding * 2
|
||||
readonly property real roundingX: flatten ? wrapper.width / 2 : rounding
|
||||
|
||||
strokeWidth: -1
|
||||
fillColor: BorderConfig.colour
|
||||
|
||||
PathArc {
|
||||
relativeX: -root.roundingX
|
||||
relativeY: root.rounding
|
||||
radiusX: Math.min(root.rounding, root.wrapper.width)
|
||||
radiusY: root.rounding
|
||||
}
|
||||
PathLine {
|
||||
relativeX: -(root.wrapper.width - root.roundingX * 2)
|
||||
relativeY: 0
|
||||
}
|
||||
PathArc {
|
||||
relativeX: -root.roundingX
|
||||
relativeY: root.rounding
|
||||
radiusX: Math.min(root.rounding, root.wrapper.width)
|
||||
radiusY: root.rounding
|
||||
direction: PathArc.Counterclockwise
|
||||
}
|
||||
PathLine {
|
||||
relativeX: 0
|
||||
relativeY: root.wrapper.height - root.rounding * 2
|
||||
}
|
||||
PathArc {
|
||||
relativeX: root.roundingX
|
||||
relativeY: root.rounding
|
||||
radiusX: Math.min(root.rounding, root.wrapper.width)
|
||||
radiusY: root.rounding
|
||||
direction: PathArc.Counterclockwise
|
||||
}
|
||||
PathLine {
|
||||
relativeX: root.wrapper.width - root.roundingX * 2
|
||||
relativeY: 0
|
||||
}
|
||||
PathArc {
|
||||
relativeX: root.roundingX
|
||||
relativeY: root.rounding
|
||||
radiusX: Math.min(root.rounding, root.wrapper.width)
|
||||
radiusY: root.rounding
|
||||
}
|
||||
|
||||
Behavior on fillColor {
|
||||
ColorAnimation {
|
||||
duration: Appearance.anim.durations.normal
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: Appearance.anim.curves.standard
|
||||
}
|
||||
}
|
||||
}
|
43
modules/osd/Content.qml
Normal file
43
modules/osd/Content.qml
Normal file
|
@ -0,0 +1,43 @@
|
|||
import "root:/widgets"
|
||||
import "root:/services"
|
||||
import "root:/config"
|
||||
import QtQuick
|
||||
|
||||
Column {
|
||||
id: root
|
||||
|
||||
required property Brightness.Monitor monitor
|
||||
|
||||
padding: Appearance.padding.large
|
||||
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.left: parent.left
|
||||
|
||||
spacing: Appearance.spacing.normal
|
||||
|
||||
VerticalSlider {
|
||||
icon: {
|
||||
if (Audio.muted)
|
||||
return "no_sound";
|
||||
if (value >= 0.5)
|
||||
return "volume_up";
|
||||
if (value > 0)
|
||||
return "volume_down";
|
||||
return "volume_mute";
|
||||
}
|
||||
value: Audio.volume
|
||||
onMoved: Audio.setVolume(value)
|
||||
|
||||
implicitWidth: OsdConfig.sizes.sliderWidth
|
||||
implicitHeight: OsdConfig.sizes.sliderHeight
|
||||
}
|
||||
|
||||
VerticalSlider {
|
||||
icon: `brightness_${(Math.round(value * 6) + 1)}`
|
||||
value: root.monitor?.brightness ?? 0
|
||||
onMoved: root.monitor?.setBrightness(value)
|
||||
|
||||
implicitWidth: OsdConfig.sizes.sliderWidth
|
||||
implicitHeight: OsdConfig.sizes.sliderHeight
|
||||
}
|
||||
}
|
48
modules/osd/Interactions.qml
Normal file
48
modules/osd/Interactions.qml
Normal file
|
@ -0,0 +1,48 @@
|
|||
import "root:/services"
|
||||
import "root:/config"
|
||||
import Quickshell
|
||||
import QtQuick
|
||||
|
||||
Scope {
|
||||
id: root
|
||||
|
||||
required property ShellScreen screen
|
||||
required property PersistentProperties visibilities
|
||||
required property bool hovered
|
||||
readonly property Brightness.Monitor monitor: Brightness.getMonitorForScreen(screen)
|
||||
|
||||
function show(): void {
|
||||
root.visibilities.osd = true;
|
||||
timer.restart();
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: Audio
|
||||
|
||||
function onMutedChanged(): void {
|
||||
root.show();
|
||||
}
|
||||
|
||||
function onVolumeChanged(): void {
|
||||
root.show();
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: root.monitor
|
||||
|
||||
function onBrightnessChanged(): void {
|
||||
root.show();
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: timer
|
||||
|
||||
interval: OsdConfig.hideDelay
|
||||
onTriggered: {
|
||||
if (!root.hovered)
|
||||
root.visibilities.osd = false;
|
||||
}
|
||||
}
|
||||
}
|
57
modules/osd/Wrapper.qml
Normal file
57
modules/osd/Wrapper.qml
Normal file
|
@ -0,0 +1,57 @@
|
|||
import "root:/services"
|
||||
import "root:/config"
|
||||
import Quickshell
|
||||
import QtQuick
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
required property ShellScreen screen
|
||||
required property bool visibility
|
||||
|
||||
visible: width > 0
|
||||
implicitWidth: 0
|
||||
implicitHeight: content.implicitHeight
|
||||
|
||||
states: State {
|
||||
name: "visible"
|
||||
when: root.visibility
|
||||
|
||||
PropertyChanges {
|
||||
root.implicitWidth: content.implicitWidth
|
||||
}
|
||||
}
|
||||
|
||||
transitions: [
|
||||
Transition {
|
||||
from: ""
|
||||
to: "visible"
|
||||
|
||||
NumberAnimation {
|
||||
target: root
|
||||
property: "implicitWidth"
|
||||
duration: Appearance.anim.durations.expressiveFastSpatial
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial
|
||||
}
|
||||
},
|
||||
Transition {
|
||||
from: "visible"
|
||||
to: ""
|
||||
|
||||
NumberAnimation {
|
||||
target: root
|
||||
property: "implicitWidth"
|
||||
duration: Appearance.anim.durations.normal
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: Appearance.anim.curves.emphasized
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
Content {
|
||||
id: content
|
||||
|
||||
monitor: Brightness.getMonitorForScreen(root.screen)
|
||||
}
|
||||
}
|
63
modules/session/Background.qml
Normal file
63
modules/session/Background.qml
Normal file
|
@ -0,0 +1,63 @@
|
|||
import "root:/services"
|
||||
import "root:/config"
|
||||
import QtQuick
|
||||
import QtQuick.Shapes
|
||||
|
||||
ShapePath {
|
||||
id: root
|
||||
|
||||
required property Wrapper wrapper
|
||||
readonly property real rounding: BorderConfig.rounding
|
||||
readonly property bool flatten: wrapper.width < rounding * 2
|
||||
readonly property real roundingX: flatten ? wrapper.width / 2 : rounding
|
||||
|
||||
strokeWidth: -1
|
||||
fillColor: BorderConfig.colour
|
||||
|
||||
PathArc {
|
||||
relativeX: -root.roundingX
|
||||
relativeY: root.rounding
|
||||
radiusX: Math.min(root.rounding, root.wrapper.width)
|
||||
radiusY: root.rounding
|
||||
}
|
||||
PathLine {
|
||||
relativeX: -(root.wrapper.width - root.roundingX * 2)
|
||||
relativeY: 0
|
||||
}
|
||||
PathArc {
|
||||
relativeX: -root.roundingX
|
||||
relativeY: root.rounding
|
||||
radiusX: Math.min(root.rounding, root.wrapper.width)
|
||||
radiusY: root.rounding
|
||||
direction: PathArc.Counterclockwise
|
||||
}
|
||||
PathLine {
|
||||
relativeX: 0
|
||||
relativeY: root.wrapper.height - root.rounding * 2
|
||||
}
|
||||
PathArc {
|
||||
relativeX: root.roundingX
|
||||
relativeY: root.rounding
|
||||
radiusX: Math.min(root.rounding, root.wrapper.width)
|
||||
radiusY: root.rounding
|
||||
direction: PathArc.Counterclockwise
|
||||
}
|
||||
PathLine {
|
||||
relativeX: root.wrapper.width - root.roundingX * 2
|
||||
relativeY: 0
|
||||
}
|
||||
PathArc {
|
||||
relativeX: root.roundingX
|
||||
relativeY: root.rounding
|
||||
radiusX: Math.min(root.rounding, root.wrapper.width)
|
||||
radiusY: root.rounding
|
||||
}
|
||||
|
||||
Behavior on fillColor {
|
||||
ColorAnimation {
|
||||
duration: Appearance.anim.durations.normal
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: Appearance.anim.curves.standard
|
||||
}
|
||||
}
|
||||
}
|
119
modules/session/Content.qml
Normal file
119
modules/session/Content.qml
Normal file
|
@ -0,0 +1,119 @@
|
|||
pragma ComponentBehavior: Bound
|
||||
|
||||
import "root:/widgets"
|
||||
import "root:/services"
|
||||
import "root:/config"
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import QtQuick
|
||||
|
||||
Column {
|
||||
id: root
|
||||
|
||||
required property PersistentProperties visibilities
|
||||
|
||||
padding: Appearance.padding.large
|
||||
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.left: parent.left
|
||||
|
||||
spacing: Appearance.spacing.large
|
||||
|
||||
SessionButton {
|
||||
id: logout
|
||||
|
||||
icon: "logout"
|
||||
command: ["uwsm", "stop"]
|
||||
|
||||
KeyNavigation.down: shutdown
|
||||
|
||||
Connections {
|
||||
target: root.visibilities
|
||||
|
||||
function onSessionChanged(): void {
|
||||
if (root.visibilities.session)
|
||||
logout.focus = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SessionButton {
|
||||
id: shutdown
|
||||
|
||||
icon: "power_settings_new"
|
||||
command: ["systemctl", "poweroff"]
|
||||
|
||||
KeyNavigation.up: logout
|
||||
KeyNavigation.down: hibernate
|
||||
}
|
||||
|
||||
AnimatedImage {
|
||||
width: SessionConfig.sizes.button
|
||||
height: SessionConfig.sizes.button
|
||||
sourceSize.width: width
|
||||
sourceSize.height: height
|
||||
|
||||
playing: visible
|
||||
asynchronous: true
|
||||
speed: 0.7
|
||||
source: "root:/assets/kurukuru.gif"
|
||||
}
|
||||
|
||||
SessionButton {
|
||||
id: hibernate
|
||||
|
||||
icon: "downloading"
|
||||
command: ["systemctl", "hibernate"]
|
||||
|
||||
KeyNavigation.up: shutdown
|
||||
KeyNavigation.down: reboot
|
||||
}
|
||||
|
||||
SessionButton {
|
||||
id: reboot
|
||||
|
||||
icon: "cached"
|
||||
command: ["systemctl", "reboot"]
|
||||
|
||||
KeyNavigation.up: hibernate
|
||||
}
|
||||
|
||||
component SessionButton: StyledRect {
|
||||
id: button
|
||||
|
||||
required property string icon
|
||||
required property list<string> command
|
||||
|
||||
implicitWidth: SessionConfig.sizes.button
|
||||
implicitHeight: SessionConfig.sizes.button
|
||||
|
||||
radius: Appearance.rounding.large
|
||||
color: button.activeFocus ? Colours.palette.m3secondaryContainer : Colours.palette.m3surfaceContainer
|
||||
|
||||
Keys.onEnterPressed: proc.startDetached()
|
||||
Keys.onReturnPressed: proc.startDetached()
|
||||
Keys.onEscapePressed: root.visibilities.session = false
|
||||
|
||||
Process {
|
||||
id: proc
|
||||
|
||||
command: button.command
|
||||
}
|
||||
|
||||
StateLayer {
|
||||
radius: parent.radius
|
||||
|
||||
function onClicked(): void {
|
||||
proc.startDetached();
|
||||
}
|
||||
}
|
||||
|
||||
MaterialIcon {
|
||||
anchors.centerIn: parent
|
||||
|
||||
text: button.icon
|
||||
color: button.activeFocus ? Colours.palette.m3onSecondaryContainer : Colours.palette.m3onSurface
|
||||
font.pointSize: Appearance.font.size.extraLarge
|
||||
}
|
||||
}
|
||||
}
|
56
modules/session/Wrapper.qml
Normal file
56
modules/session/Wrapper.qml
Normal file
|
@ -0,0 +1,56 @@
|
|||
import "root:/services"
|
||||
import "root:/config"
|
||||
import Quickshell
|
||||
import QtQuick
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
required property PersistentProperties visibilities
|
||||
|
||||
visible: width > 0
|
||||
implicitWidth: 0
|
||||
implicitHeight: content.implicitHeight
|
||||
|
||||
states: State {
|
||||
name: "visible"
|
||||
when: root.visibilities.session
|
||||
|
||||
PropertyChanges {
|
||||
root.implicitWidth: content.implicitWidth
|
||||
}
|
||||
}
|
||||
|
||||
transitions: [
|
||||
Transition {
|
||||
from: ""
|
||||
to: "visible"
|
||||
|
||||
NumberAnimation {
|
||||
target: root
|
||||
property: "implicitWidth"
|
||||
duration: Appearance.anim.durations.expressiveFastSpatial
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial
|
||||
}
|
||||
},
|
||||
Transition {
|
||||
from: "visible"
|
||||
to: ""
|
||||
|
||||
NumberAnimation {
|
||||
target: root
|
||||
property: "implicitWidth"
|
||||
duration: root.visibilities.osd ? Appearance.anim.durations.expressiveFastSpatial : Appearance.anim.durations.normal
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: root.visibilities.osd ? Appearance.anim.curves.expressiveFastSpatial : Appearance.anim.curves.emphasized
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
Content {
|
||||
id: content
|
||||
|
||||
visibilities: root.visibilities
|
||||
}
|
||||
}
|
8
run.fish
Executable file
8
run.fish
Executable file
|
@ -0,0 +1,8 @@
|
|||
#!/bin/fish
|
||||
|
||||
set -l dbus 'quickshell.dbus.properties.warning = false;quickshell.dbus.dbusmenu.warning = false' # System tray dbus property errors
|
||||
set -l notifs 'quickshell.service.notifications.warning = false' # Notification server warnings on reload
|
||||
set -l sni 'quickshell.service.sni.host.warning = false' # StatusNotifierItem warnings on reload
|
||||
set -l process 'QProcess: Destroyed while process' # Long running processes on reload
|
||||
|
||||
qs -p (dirname (status filename)) --log-rules "$dbus;$notifs;$sni" | grep -vE -e $process
|
37
services/Apps.qml
Normal file
37
services/Apps.qml
Normal file
|
@ -0,0 +1,37 @@
|
|||
pragma Singleton
|
||||
|
||||
import "root:/utils/scripts/fuzzysort.js" as Fuzzy
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
readonly property list<DesktopEntry> list: DesktopEntries.applications.values.filter(a => !a.noDisplay).sort((a, b) => a.name.localeCompare(b.name))
|
||||
readonly property list<var> preppedApps: list.map(a => ({
|
||||
name: Fuzzy.prepare(a.name),
|
||||
comment: Fuzzy.prepare(a.comment),
|
||||
entry: a
|
||||
}))
|
||||
|
||||
function fuzzyQuery(search: string): var { // Idk why list<DesktopEntry> doesn't work
|
||||
return Fuzzy.go(search, preppedApps, {
|
||||
all: true,
|
||||
keys: ["name", "comment"],
|
||||
scoreFn: r => r[0].score > 0 ? r[0].score * 0.9 + r[1].score * 0.1 : 0
|
||||
}).map(r => r.obj.entry);
|
||||
}
|
||||
|
||||
function launch(entry: DesktopEntry): void {
|
||||
launchProc.entry = entry;
|
||||
launchProc.startDetached();
|
||||
}
|
||||
|
||||
Process {
|
||||
id: launchProc
|
||||
|
||||
property DesktopEntry entry
|
||||
|
||||
command: ["app2unit", "--", `${entry?.id}.desktop`]
|
||||
}
|
||||
}
|
25
services/Audio.qml
Normal file
25
services/Audio.qml
Normal file
|
@ -0,0 +1,25 @@
|
|||
pragma Singleton
|
||||
|
||||
import Quickshell
|
||||
import Quickshell.Services.Pipewire
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
readonly property PwNode sink: Pipewire.defaultAudioSink
|
||||
readonly property PwNode source: Pipewire.defaultAudioSource
|
||||
|
||||
readonly property bool muted: sink?.audio?.muted ?? false
|
||||
readonly property real volume: sink?.audio?.volume ?? 0
|
||||
|
||||
function setVolume(volume: real): void {
|
||||
if (sink?.ready && sink?.audio) {
|
||||
sink.audio.muted = false;
|
||||
sink.audio.volume = volume;
|
||||
}
|
||||
}
|
||||
|
||||
PwObjectTracker {
|
||||
objects: [Pipewire.defaultAudioSink, Pipewire.defaultAudioSource]
|
||||
}
|
||||
}
|
18
services/BeatDetector.qml
Normal file
18
services/BeatDetector.qml
Normal file
|
@ -0,0 +1,18 @@
|
|||
pragma Singleton
|
||||
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
property real bpm
|
||||
|
||||
Process {
|
||||
running: true
|
||||
command: [`${Quickshell.shellRoot}/assets/realtime-beat-detector.py`]
|
||||
stdout: SplitParser {
|
||||
onRead: data => root.bpm = parseFloat(data)
|
||||
}
|
||||
}
|
||||
}
|
80
services/Bluetooth.qml
Normal file
80
services/Bluetooth.qml
Normal file
|
@ -0,0 +1,80 @@
|
|||
pragma Singleton
|
||||
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import QtQuick
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
property bool powered
|
||||
property bool discovering
|
||||
readonly property list<Device> devices: []
|
||||
|
||||
Process {
|
||||
running: true
|
||||
command: ["bluetoothctl"]
|
||||
stdout: SplitParser {
|
||||
onRead: {
|
||||
getInfo.running = true;
|
||||
getDevices.running = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: getInfo
|
||||
running: true
|
||||
command: ["sh", "-c", "bluetoothctl show | paste -s"]
|
||||
stdout: SplitParser {
|
||||
onRead: data => {
|
||||
root.powered = data.includes("Powered: yes");
|
||||
root.discovering = data.includes("Discovering: yes");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: getDevices
|
||||
running: true
|
||||
command: ["fish", "-c", `for a in (bluetoothctl devices | cut -d ' ' -f 2); bluetoothctl info $a | jq -R 'reduce (inputs / ":") as [$key, $value] ({}; .[$key | ltrimstr("\t")] = ($value | ltrimstr(" ")))' | jq -c --arg addr $a '.Address = $addr'; end | jq -sc`]
|
||||
stdout: SplitParser {
|
||||
onRead: data => {
|
||||
const devices = JSON.parse(data).filter(d => d.Name);
|
||||
const rDevices = root.devices;
|
||||
|
||||
const destroyed = rDevices.filter(rd => !devices.find(d => d.Address === rd.address));
|
||||
for (const device of destroyed)
|
||||
rDevices.splice(rDevices.indexOf(device), 1).forEach(d => d.destroy());
|
||||
|
||||
for (const device of devices) {
|
||||
const match = rDevices.find(d => d.address === device.Address);
|
||||
if (match) {
|
||||
match.lastIpcObject = device;
|
||||
} else {
|
||||
rDevices.push(deviceComp.createObject(root, {
|
||||
lastIpcObject: device
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
component Device: QtObject {
|
||||
required property var lastIpcObject
|
||||
readonly property string name: lastIpcObject.Name
|
||||
readonly property string alias: lastIpcObject.Alias
|
||||
readonly property string address: lastIpcObject.Address
|
||||
readonly property string icon: lastIpcObject.Icon
|
||||
readonly property bool connected: lastIpcObject.Connected === "yes"
|
||||
readonly property bool paired: lastIpcObject.Paired === "yes"
|
||||
readonly property bool trusted: lastIpcObject.Trusted === "yes"
|
||||
}
|
||||
|
||||
Component {
|
||||
id: deviceComp
|
||||
|
||||
Device {}
|
||||
}
|
||||
}
|
118
services/Brightness.qml
Normal file
118
services/Brightness.qml
Normal file
|
@ -0,0 +1,118 @@
|
|||
pragma Singleton
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import "root:/widgets"
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import QtQuick
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
property var ddcMonitors: []
|
||||
readonly property list<Monitor> monitors: variants.instances
|
||||
|
||||
function getMonitorForScreen(screen: ShellScreen): var {
|
||||
return monitors.find(m => m.modelData === screen);
|
||||
}
|
||||
|
||||
function increaseBrightness(): void {
|
||||
const focusedName = Hyprland.focusedMonitor.name;
|
||||
const monitor = monitors.find(m => focusedName === m.modelData.name);
|
||||
if (monitor)
|
||||
monitor.setBrightness(monitor.brightness + 0.1);
|
||||
}
|
||||
|
||||
function decreaseBrightness(): void {
|
||||
const focusedName = Hyprland.focusedMonitor.name;
|
||||
const monitor = monitors.find(m => focusedName === m.modelData.name);
|
||||
if (monitor)
|
||||
monitor.setBrightness(monitor.brightness - 0.1);
|
||||
}
|
||||
|
||||
reloadableId: "brightness"
|
||||
|
||||
onMonitorsChanged: {
|
||||
ddcMonitors = [];
|
||||
ddcProc.running = true;
|
||||
}
|
||||
|
||||
Variants {
|
||||
id: variants
|
||||
|
||||
model: Quickshell.screens
|
||||
|
||||
Monitor {}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: ddcProc
|
||||
|
||||
command: ["ddcutil", "detect", "--brief"]
|
||||
stdout: SplitParser {
|
||||
splitMarker: "\n\n"
|
||||
onRead: data => {
|
||||
if (data.startsWith("Display ")) {
|
||||
const lines = data.split("\n").map(l => l.trim());
|
||||
root.ddcMonitors.push({
|
||||
model: lines.find(l => l.startsWith("Monitor:")).split(":")[2],
|
||||
busNum: lines.find(l => l.startsWith("I2C bus:")).split("/dev/i2c-")[1]
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
onExited: root.ddcMonitorsChanged()
|
||||
}
|
||||
|
||||
Process {
|
||||
id: setProc
|
||||
}
|
||||
|
||||
CustomShortcut {
|
||||
name: "brightnessUp"
|
||||
onPressed: root.increaseBrightness()
|
||||
}
|
||||
|
||||
CustomShortcut {
|
||||
name: "brightnessDown"
|
||||
onPressed: root.decreaseBrightness()
|
||||
}
|
||||
|
||||
component Monitor: QtObject {
|
||||
id: monitor
|
||||
|
||||
required property ShellScreen modelData
|
||||
readonly property bool isDdc: root.ddcMonitors.some(m => m.model === modelData.model)
|
||||
readonly property string busNum: root.ddcMonitors.find(m => m.model === modelData.model)?.busNum ?? ""
|
||||
property real brightness
|
||||
|
||||
readonly property Process initProc: Process {
|
||||
stdout: SplitParser {
|
||||
onRead: data => {
|
||||
const [, , , current, max] = data.split(" ");
|
||||
monitor.brightness = parseInt(current) / parseInt(max);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function setBrightness(value: real): void {
|
||||
value = Math.max(0, Math.min(1, value));
|
||||
const rounded = Math.round(value * 100);
|
||||
if (Math.round(brightness * 100) === rounded)
|
||||
return;
|
||||
brightness = value;
|
||||
setProc.command = isDdc ? ["ddcutil", "-b", busNum, "setvcp", "10", rounded] : ["brightnessctl", "s", `${rounded}%`];
|
||||
setProc.startDetached();
|
||||
}
|
||||
|
||||
onBusNumChanged: {
|
||||
initProc.command = isDdc ? ["ddcutil", "-b", busNum, "getvcp", "10", "--brief"] : ["sh", "-c", `echo "a b c $(brightnessctl g) $(brightnessctl m)"`];
|
||||
initProc.running = true;
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
initProc.command = isDdc ? ["ddcutil", "-b", busNum, "getvcp", "10", "--brief"] : ["sh", "-c", `echo "a b c $(brightnessctl g) $(brightnessctl m)"`];
|
||||
initProc.running = true;
|
||||
}
|
||||
}
|
||||
}
|
19
services/Cava.qml
Normal file
19
services/Cava.qml
Normal file
|
@ -0,0 +1,19 @@
|
|||
pragma Singleton
|
||||
|
||||
import "root:/config"
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
property list<int> values
|
||||
|
||||
Process {
|
||||
running: true
|
||||
command: ["sh", "-c", `printf '[general]\nframerate=60\nbars=${DashboardConfig.visualiserBars}\n[output]\nchannels=mono\nmethod=raw\nraw_target=/dev/stdout\ndata_format=ascii\nascii_max_range=100' | cava -p /dev/stdin`]
|
||||
stdout: SplitParser {
|
||||
onRead: data => root.values = data.slice(0, -1).split(";").map(v => parseInt(v, 10))
|
||||
}
|
||||
}
|
||||
}
|
153
services/Colours.qml
Normal file
153
services/Colours.qml
Normal file
|
@ -0,0 +1,153 @@
|
|||
pragma Singleton
|
||||
|
||||
import "root:/config"
|
||||
import "root:/utils"
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import QtQuick
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
readonly property list<string> colourNames: ["rosewater", "flamingo", "pink", "mauve", "red", "maroon", "peach", "yellow", "green", "teal", "sky", "sapphire", "blue", "lavender"]
|
||||
|
||||
property bool showPreview
|
||||
property bool endPreviewOnNextChange
|
||||
property bool light
|
||||
readonly property Colours palette: showPreview ? preview : current
|
||||
readonly property Colours current: Colours {}
|
||||
readonly property Colours preview: Colours {}
|
||||
readonly property Transparency transparency: Transparency {}
|
||||
|
||||
function alpha(c: color, layer: bool): color {
|
||||
if (!transparency.enabled)
|
||||
return c;
|
||||
c = Qt.rgba(c.r, c.g, c.b, layer ? transparency.layers : transparency.base);
|
||||
if (layer)
|
||||
c.hsvValue = Math.max(0, Math.min(1, c.hslLightness + (light ? -0.2 : 0.2))); // TODO: edit based on colours (hue or smth)
|
||||
return c;
|
||||
}
|
||||
|
||||
function on(c: color): color {
|
||||
if (c.hslLightness < 0.5)
|
||||
return Qt.hsla(c.hslHue, c.hslSaturation, 0.9, 1);
|
||||
return Qt.hsla(c.hslHue, c.hslSaturation, 0.1, 1);
|
||||
}
|
||||
|
||||
function load(data: string, isPreview: bool): void {
|
||||
const colours = isPreview ? preview : current;
|
||||
for (const line of data.trim().split("\n")) {
|
||||
let [name, colour] = line.split(" ");
|
||||
name = name.trim();
|
||||
name = colourNames.includes(name) ? name : `m3${name}`;
|
||||
if (colours.hasOwnProperty(name))
|
||||
colours[name] = `#${colour.trim()}`;
|
||||
}
|
||||
|
||||
if (!isPreview || (isPreview && endPreviewOnNextChange)) {
|
||||
showPreview = false;
|
||||
endPreviewOnNextChange = false;
|
||||
}
|
||||
}
|
||||
|
||||
function setMode(mode: string): void {
|
||||
setModeProc.command = ["caelestia", "scheme", "dynamic", "default", mode];
|
||||
setModeProc.startDetached();
|
||||
}
|
||||
|
||||
Process {
|
||||
id: setModeProc
|
||||
}
|
||||
|
||||
FileView {
|
||||
path: `${Paths.state}/scheme/current-mode.txt`
|
||||
watchChanges: true
|
||||
onFileChanged: reload()
|
||||
onLoaded: root.light = text() === "light"
|
||||
}
|
||||
|
||||
FileView {
|
||||
path: `${Paths.state}/scheme/current.txt`
|
||||
watchChanges: true
|
||||
onFileChanged: reload()
|
||||
onLoaded: root.load(text(), false)
|
||||
}
|
||||
|
||||
component Transparency: QtObject {
|
||||
readonly property bool enabled: false
|
||||
readonly property real base: 0.78
|
||||
readonly property real layers: 0.58
|
||||
}
|
||||
|
||||
component Colours: QtObject {
|
||||
property color m3primary_paletteKeyColor: "#7870AB"
|
||||
property color m3secondary_paletteKeyColor: "#78748A"
|
||||
property color m3tertiary_paletteKeyColor: "#976A7D"
|
||||
property color m3neutral_paletteKeyColor: "#79767D"
|
||||
property color m3neutral_variant_paletteKeyColor: "#797680"
|
||||
property color m3background: "#141318"
|
||||
property color m3onBackground: "#E5E1E9"
|
||||
property color m3surface: "#141318"
|
||||
property color m3surfaceDim: "#141318"
|
||||
property color m3surfaceBright: "#3A383E"
|
||||
property color m3surfaceContainerLowest: "#0E0D13"
|
||||
property color m3surfaceContainerLow: "#1C1B20"
|
||||
property color m3surfaceContainer: "#201F25"
|
||||
property color m3surfaceContainerHigh: "#2B292F"
|
||||
property color m3surfaceContainerHighest: "#35343A"
|
||||
property color m3onSurface: "#E5E1E9"
|
||||
property color m3surfaceVariant: "#48454E"
|
||||
property color m3onSurfaceVariant: "#C9C5D0"
|
||||
property color m3inverseSurface: "#E5E1E9"
|
||||
property color m3inverseOnSurface: "#312F36"
|
||||
property color m3outline: "#938F99"
|
||||
property color m3outlineVariant: "#48454E"
|
||||
property color m3shadow: "#000000"
|
||||
property color m3scrim: "#000000"
|
||||
property color m3surfaceTint: "#C8BFFF"
|
||||
property color m3primary: "#C8BFFF"
|
||||
property color m3onPrimary: "#30285F"
|
||||
property color m3primaryContainer: "#473F77"
|
||||
property color m3onPrimaryContainer: "#E5DEFF"
|
||||
property color m3inversePrimary: "#5F5791"
|
||||
property color m3secondary: "#C9C3DC"
|
||||
property color m3onSecondary: "#312E41"
|
||||
property color m3secondaryContainer: "#484459"
|
||||
property color m3onSecondaryContainer: "#E5DFF9"
|
||||
property color m3tertiary: "#ECB8CD"
|
||||
property color m3onTertiary: "#482536"
|
||||
property color m3tertiaryContainer: "#B38397"
|
||||
property color m3onTertiaryContainer: "#000000"
|
||||
property color m3error: "#EA8DC1"
|
||||
property color m3onError: "#690005"
|
||||
property color m3errorContainer: "#93000A"
|
||||
property color m3onErrorContainer: "#FFDAD6"
|
||||
property color m3primaryFixed: "#E5DEFF"
|
||||
property color m3primaryFixedDim: "#C8BFFF"
|
||||
property color m3onPrimaryFixed: "#1B1149"
|
||||
property color m3onPrimaryFixedVariant: "#473F77"
|
||||
property color m3secondaryFixed: "#E5DFF9"
|
||||
property color m3secondaryFixedDim: "#C9C3DC"
|
||||
property color m3onSecondaryFixed: "#1C192B"
|
||||
property color m3onSecondaryFixedVariant: "#484459"
|
||||
property color m3tertiaryFixed: "#FFD8E7"
|
||||
property color m3tertiaryFixedDim: "#ECB8CD"
|
||||
property color m3onTertiaryFixed: "#301121"
|
||||
property color m3onTertiaryFixedVariant: "#613B4C"
|
||||
|
||||
property color rosewater: "#B8C4FF"
|
||||
property color flamingo: "#DBB9F8"
|
||||
property color pink: "#F3B3E3"
|
||||
property color mauve: "#D0BDFE"
|
||||
property color red: "#F8B3D1"
|
||||
property color maroon: "#F6B2DA"
|
||||
property color peach: "#E4B7F4"
|
||||
property color yellow: "#C3C0FF"
|
||||
property color green: "#ADC6FF"
|
||||
property color teal: "#D4BBFC"
|
||||
property color sky: "#CBBEFF"
|
||||
property color sapphire: "#BDC2FF"
|
||||
property color blue: "#C7BFFF"
|
||||
property color lavender: "#EAB5ED"
|
||||
}
|
||||
}
|
114
services/Hyprland.qml
Normal file
114
services/Hyprland.qml
Normal file
|
@ -0,0 +1,114 @@
|
|||
pragma Singleton
|
||||
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import Quickshell.Hyprland
|
||||
import QtQuick
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
readonly property list<Client> clients: []
|
||||
readonly property var workspaces: Hyprland.workspaces
|
||||
readonly property var monitors: Hyprland.monitors
|
||||
property Client activeClient: null
|
||||
readonly property HyprlandWorkspace activeWorkspace: focusedMonitor?.activeWorkspace ?? null
|
||||
readonly property HyprlandMonitor focusedMonitor: Hyprland.focusedMonitor
|
||||
readonly property int activeWsId: activeWorkspace?.id ?? 1
|
||||
property point cursorPos
|
||||
|
||||
function reload() {
|
||||
Hyprland.refreshWorkspaces();
|
||||
Hyprland.refreshMonitors();
|
||||
getClients.running = true;
|
||||
getActiveClient.running = true;
|
||||
}
|
||||
|
||||
function dispatch(request: string): void {
|
||||
Hyprland.dispatch(request);
|
||||
}
|
||||
|
||||
Component.onCompleted: reload()
|
||||
|
||||
Connections {
|
||||
target: Hyprland
|
||||
|
||||
function onRawEvent(event: HyprlandEvent): void {
|
||||
if (!event.name.endsWith("v2"))
|
||||
root.reload();
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: getClients
|
||||
command: ["sh", "-c", "hyprctl -j clients | jq -c"]
|
||||
stdout: SplitParser {
|
||||
onRead: data => {
|
||||
const clients = JSON.parse(data);
|
||||
const rClients = root.clients;
|
||||
|
||||
const destroyed = rClients.filter(rc => !clients.find(c => c.address === rc.address));
|
||||
for (const client of destroyed)
|
||||
rClients.splice(rClients.indexOf(client), 1).forEach(c => c.destroy());
|
||||
|
||||
for (const client of clients) {
|
||||
const match = rClients.find(c => c.address === client.address);
|
||||
if (match) {
|
||||
match.lastIpcObject = client;
|
||||
} else {
|
||||
rClients.push(clientComp.createObject(root, {
|
||||
lastIpcObject: client
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: getActiveClient
|
||||
command: ["hyprctl", "-j", "activewindow"]
|
||||
stdout: SplitParser {
|
||||
splitMarker: ""
|
||||
onRead: data => {
|
||||
const client = JSON.parse(data);
|
||||
const rClient = root.activeClient;
|
||||
if (client.address) {
|
||||
if (rClient)
|
||||
rClient.lastIpcObject = client;
|
||||
else
|
||||
root.activeClient = clientComp.createObject(root, {
|
||||
lastIpcObject: client
|
||||
});
|
||||
} else if (rClient) {
|
||||
rClient.destroy();
|
||||
root.activeClient = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
component Client: QtObject {
|
||||
required property var lastIpcObject
|
||||
readonly property string address: lastIpcObject.address
|
||||
readonly property string wmClass: lastIpcObject.class
|
||||
readonly property string title: lastIpcObject.title
|
||||
readonly property string initialClass: lastIpcObject.initialClass
|
||||
readonly property string initialTitle: lastIpcObject.initialTitle
|
||||
readonly property int x: lastIpcObject.at[0]
|
||||
readonly property int y: lastIpcObject.at[1]
|
||||
readonly property int width: lastIpcObject.size[0]
|
||||
readonly property int height: lastIpcObject.size[1]
|
||||
readonly property HyprlandWorkspace workspace: Hyprland.workspaces.values.find(w => w.id === lastIpcObject.workspace.id) ?? null
|
||||
readonly property bool floating: lastIpcObject.floating
|
||||
readonly property bool fullscreen: lastIpcObject.fullscreen
|
||||
readonly property int pid: lastIpcObject.pid
|
||||
readonly property int focusHistoryId: lastIpcObject.focusHistoryID
|
||||
}
|
||||
|
||||
Component {
|
||||
id: clientComp
|
||||
|
||||
Client {}
|
||||
}
|
||||
}
|
68
services/Network.qml
Normal file
68
services/Network.qml
Normal file
|
@ -0,0 +1,68 @@
|
|||
pragma Singleton
|
||||
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import QtQuick
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
readonly property list<AccessPoint> networks: []
|
||||
readonly property AccessPoint active: networks.find(n => n.active) ?? null
|
||||
|
||||
reloadableId: "network"
|
||||
|
||||
Process {
|
||||
running: true
|
||||
command: ["nmcli", "m"]
|
||||
stdout: SplitParser {
|
||||
onRead: getNetworks.running = true
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: getNetworks
|
||||
running: true
|
||||
command: ["sh", "-c", `nmcli -g ACTIVE,SIGNAL,FREQ,SSID d w | jq -cR '[(inputs / ":") | select(.[3] | length >= 4)]'`]
|
||||
stdout: SplitParser {
|
||||
onRead: data => {
|
||||
const networks = JSON.parse(data).map(n => [n[0] === "yes", parseInt(n[1]), parseInt(n[2]), n[3]]);
|
||||
const rNetworks = root.networks;
|
||||
|
||||
const destroyed = rNetworks.filter(rn => !networks.find(n => n[2] === rn.frequency && n[3] === rn.ssid));
|
||||
for (const network of destroyed)
|
||||
rNetworks.splice(rNetworks.indexOf(network), 1).forEach(n => n.destroy());
|
||||
|
||||
for (const network of networks) {
|
||||
const match = rNetworks.find(n => n.frequency === network[2] && n.ssid === network[3]);
|
||||
if (match) {
|
||||
match.active = network[0];
|
||||
match.strength = network[1];
|
||||
match.frequency = network[2];
|
||||
match.ssid = network[3];
|
||||
} else {
|
||||
rNetworks.push(apComp.createObject(root, {
|
||||
active: network[0],
|
||||
strength: network[1],
|
||||
frequency: network[2],
|
||||
ssid: network[3]
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
component AccessPoint: QtObject {
|
||||
required property string ssid
|
||||
required property int strength
|
||||
required property int frequency
|
||||
required property bool active
|
||||
}
|
||||
|
||||
Component {
|
||||
id: apComp
|
||||
|
||||
AccessPoint {}
|
||||
}
|
||||
}
|
98
services/Notifs.qml
Normal file
98
services/Notifs.qml
Normal file
|
@ -0,0 +1,98 @@
|
|||
pragma Singleton
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import "root:/widgets"
|
||||
import "root:/config"
|
||||
import Quickshell
|
||||
import Quickshell.Services.Notifications
|
||||
import QtQuick
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
readonly property list<Notif> list: []
|
||||
readonly property list<Notif> popups: list.filter(n => n.popup)
|
||||
|
||||
NotificationServer {
|
||||
id: server
|
||||
|
||||
keepOnReload: false
|
||||
actionsSupported: true
|
||||
bodyHyperlinksSupported: true
|
||||
bodyImagesSupported: true
|
||||
bodyMarkupSupported: true
|
||||
imageSupported: true
|
||||
|
||||
onNotification: notif => {
|
||||
notif.tracked = true;
|
||||
|
||||
root.list.push(notifComp.createObject(root, {
|
||||
popup: true,
|
||||
notification: notif
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
CustomShortcut {
|
||||
name: "clearNotifs"
|
||||
description: "Clear all notifications"
|
||||
onPressed: {
|
||||
for (const notif of root.list)
|
||||
notif.popup = false;
|
||||
}
|
||||
}
|
||||
|
||||
component Notif: QtObject {
|
||||
id: notif
|
||||
|
||||
property bool popup
|
||||
readonly property date time: new Date()
|
||||
readonly property string timeStr: {
|
||||
const diff = Time.date.getTime() - time.getTime();
|
||||
const m = Math.floor(diff / 60000);
|
||||
const h = Math.floor(m / 60);
|
||||
|
||||
if (h < 1 && m < 1)
|
||||
return "now";
|
||||
if (h < 1)
|
||||
return `${m}m`;
|
||||
return `${h}h`;
|
||||
}
|
||||
|
||||
required property Notification notification
|
||||
readonly property string summary: notification.summary
|
||||
readonly property string body: notification.body
|
||||
readonly property string appIcon: notification.appIcon
|
||||
readonly property string appName: notification.appName
|
||||
readonly property string image: notification.image
|
||||
readonly property var urgency: notification.urgency // Idk why NotificationUrgency doesn't work
|
||||
readonly property list<NotificationAction> actions: notification.actions
|
||||
|
||||
readonly property Timer timer: Timer {
|
||||
running: true
|
||||
interval: notif.notification.expireTimeout > 0 ? notif.notification.expireTimeout : NotifsConfig.defaultExpireTimeout
|
||||
onTriggered: {
|
||||
if (NotifsConfig.expire)
|
||||
notif.popup = false;
|
||||
}
|
||||
}
|
||||
|
||||
readonly property Connections conn: Connections {
|
||||
target: notif.notification.Retainable
|
||||
|
||||
function onDropped(): void {
|
||||
root.list.splice(root.list.indexOf(notif), 1);
|
||||
}
|
||||
|
||||
function onAboutToDestroy(): void {
|
||||
notif.destroy();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: notifComp
|
||||
|
||||
Notif {}
|
||||
}
|
||||
}
|
59
services/Players.qml
Normal file
59
services/Players.qml
Normal file
|
@ -0,0 +1,59 @@
|
|||
pragma Singleton
|
||||
|
||||
import "root:/widgets"
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import Quickshell.Services.Mpris
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
readonly property list<MprisPlayer> list: Mpris.players.values
|
||||
readonly property MprisPlayer active: manualActive ?? list.find(p => p.identity === "Spotify") ?? list[0] ?? null
|
||||
property MprisPlayer manualActive
|
||||
|
||||
CustomShortcut {
|
||||
name: "mediaToggle"
|
||||
description: "Toggle media playback"
|
||||
onPressed: {
|
||||
const active = root.active;
|
||||
if (active && active.canTogglePlaying)
|
||||
active.togglePlaying();
|
||||
}
|
||||
}
|
||||
|
||||
CustomShortcut {
|
||||
name: "mediaPrev"
|
||||
description: "Previous track"
|
||||
onPressed: {
|
||||
const active = root.active;
|
||||
if (active && active.canGoPrevious)
|
||||
active.previous();
|
||||
}
|
||||
}
|
||||
|
||||
CustomShortcut {
|
||||
name: "mediaNext"
|
||||
description: "Next track"
|
||||
onPressed: {
|
||||
const active = root.active;
|
||||
if (active && active.canGoNext)
|
||||
active.next();
|
||||
}
|
||||
}
|
||||
|
||||
CustomShortcut {
|
||||
name: "mediaStop"
|
||||
description: "Stop media playback"
|
||||
onPressed: root.active?.stop()
|
||||
}
|
||||
|
||||
IpcHandler {
|
||||
target: "mpris"
|
||||
|
||||
function getActive(prop: string): string {
|
||||
const active = root.active;
|
||||
return active ? active[prop] ?? "Invalid property" : "No active player";
|
||||
}
|
||||
}
|
||||
}
|
173
services/SystemUsage.qml
Normal file
173
services/SystemUsage.qml
Normal file
|
@ -0,0 +1,173 @@
|
|||
pragma Singleton
|
||||
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import QtQuick
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
property real cpuPerc
|
||||
property real cpuTemp
|
||||
property real gpuPerc
|
||||
property real gpuTemp
|
||||
property int memUsed
|
||||
property int memTotal
|
||||
readonly property real memPerc: memTotal > 0 ? memUsed / memTotal : 0
|
||||
property int storageUsed
|
||||
property int storageTotal
|
||||
property real storagePerc: storageTotal > 0 ? storageUsed / storageTotal : 0
|
||||
|
||||
property int lastCpuIdle
|
||||
property int lastCpuTotal
|
||||
|
||||
function formatKib(kib: int): var {
|
||||
const mib = 1024;
|
||||
const gib = 1024 ** 2;
|
||||
const tib = 1024 ** 3;
|
||||
|
||||
if (kib >= tib)
|
||||
return {
|
||||
value: kib / tib,
|
||||
unit: "TiB"
|
||||
};
|
||||
if (kib >= gib)
|
||||
return {
|
||||
value: kib / gib,
|
||||
unit: "GiB"
|
||||
};
|
||||
if (kib >= mib)
|
||||
return {
|
||||
value: kib / mib,
|
||||
unit: "MiB"
|
||||
};
|
||||
return {
|
||||
value: kib,
|
||||
unit: "KiB"
|
||||
};
|
||||
}
|
||||
|
||||
Timer {
|
||||
running: true
|
||||
interval: 3000
|
||||
repeat: true
|
||||
onTriggered: {
|
||||
stat.reload();
|
||||
meminfo.reload();
|
||||
storage.running = true;
|
||||
cpuTemp.running = true;
|
||||
gpuUsage.running = true;
|
||||
gpuTemp.running = true;
|
||||
}
|
||||
}
|
||||
|
||||
FileView {
|
||||
id: stat
|
||||
|
||||
path: "/proc/stat"
|
||||
onLoaded: {
|
||||
const data = text().match(/^cpu\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)/);
|
||||
if (data) {
|
||||
const stats = data.slice(1).map(n => parseInt(n, 10));
|
||||
const total = stats.reduce((a, b) => a + b, 0);
|
||||
const idle = stats[3];
|
||||
|
||||
const totalDiff = total - root.lastCpuTotal;
|
||||
const idleDiff = idle - root.lastCpuIdle;
|
||||
root.cpuPerc = totalDiff > 0 ? (1 - idleDiff / totalDiff) : 0;
|
||||
|
||||
root.lastCpuTotal = total;
|
||||
root.lastCpuIdle = idle;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
FileView {
|
||||
id: meminfo
|
||||
|
||||
path: "/proc/meminfo"
|
||||
onLoaded: {
|
||||
const data = text();
|
||||
root.memTotal = parseInt(data.match(/MemTotal: *(\d+)/)[1], 10) || 1;
|
||||
root.memUsed = (root.memTotal - parseInt(data.match(/MemAvailable: *(\d+)/)[1], 10)) || 0;
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: storage
|
||||
|
||||
running: true
|
||||
command: ["sh", "-c", "df | grep '^/dev/' | awk '{print $3, $4}'"]
|
||||
stdout: SplitParser {
|
||||
splitMarker: ""
|
||||
onRead: data => {
|
||||
let used = 0;
|
||||
let avail = 0;
|
||||
for (const line of data.trim().split("\n")) {
|
||||
const [u, a] = line.split(" ");
|
||||
used += parseInt(u, 10);
|
||||
avail += parseInt(a, 10);
|
||||
}
|
||||
root.storageUsed = used;
|
||||
root.storageTotal = used + avail;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: cpuTemp
|
||||
|
||||
running: true
|
||||
command: ["fish", "-c", "cat /sys/class/thermal/thermal_zone*/temp | string join ' '"]
|
||||
stdout: SplitParser {
|
||||
onRead: data => {
|
||||
const temps = data.trim().split(" ");
|
||||
const sum = temps.reduce((acc, d) => acc + parseInt(d, 10), 0);
|
||||
root.cpuTemp = sum / temps.length / 1000;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: gpuUsage
|
||||
|
||||
running: true
|
||||
command: ["sh", "-c", "cat /sys/class/drm/card*/device/gpu_busy_percent"]
|
||||
stdout: SplitParser {
|
||||
splitMarker: ""
|
||||
onRead: data => {
|
||||
const percs = data.trim().split("\n");
|
||||
const sum = percs.reduce((acc, d) => acc + parseInt(d, 10), 0);
|
||||
root.gpuPerc = sum / percs.length / 100;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: gpuTemp
|
||||
|
||||
running: true
|
||||
command: ["sh", "-c", "sensors | jq -nRc '[inputs]'"]
|
||||
stdout: SplitParser {
|
||||
onRead: data => {
|
||||
let eligible = false;
|
||||
let sum = 0;
|
||||
let count = 0;
|
||||
for (const line of JSON.parse(data)) {
|
||||
if (line === "Adapter: PCI adapter")
|
||||
eligible = true;
|
||||
else if (line === "")
|
||||
eligible = false;
|
||||
else if (eligible) {
|
||||
const match = line.match(/^(temp[0-9]+|GPU core|edge)+:\s+\+([0-9]+\.[0-9]+)°C/);
|
||||
if (match) {
|
||||
sum += parseFloat(match[2]);
|
||||
count++;
|
||||
}
|
||||
}
|
||||
}
|
||||
root.gpuTemp = count > 0 ? sum / count : 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
72
services/Thumbnailer.qml
Normal file
72
services/Thumbnailer.qml
Normal file
|
@ -0,0 +1,72 @@
|
|||
pragma Singleton
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import "root:/utils"
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import QtQuick
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
readonly property string thumbDir: `${Paths.cache}/thumbnails`.slice(7)
|
||||
|
||||
function go(obj: var): var {
|
||||
return thumbComp.createObject(obj, {
|
||||
originalPath: obj.path,
|
||||
width: obj.width,
|
||||
height: obj.height,
|
||||
loadOriginal: obj.loadOriginal
|
||||
});
|
||||
}
|
||||
|
||||
component Thumbnail: QtObject {
|
||||
id: obj
|
||||
|
||||
required property string originalPath
|
||||
required property int width
|
||||
required property int height
|
||||
required property bool loadOriginal
|
||||
|
||||
property string path
|
||||
|
||||
readonly property Process proc: Process {
|
||||
running: true
|
||||
command: ["fish", "-c", `
|
||||
set -l path "${root.thumbDir}/$(sha1sum ${obj.originalPath} | cut -d ' ' -f 1)@${obj.width}x${obj.height}-exact.png"
|
||||
if test -f $path
|
||||
echo $path
|
||||
else
|
||||
echo 'start'
|
||||
set -l size (identify -ping -format '%w\n%h' ${obj.originalPath})
|
||||
if test $size[1] -gt ${obj.width} -o $size[2] -gt ${obj.height}
|
||||
magick ${obj.originalPath} -${obj.width > 1024 || obj.height > 1024 ? "resize" : "thumbnail"} ${obj.width}x${obj.height}^ -background none -gravity center -extent ${obj.width}x${obj.height} -unsharp 0x.5 $path
|
||||
else
|
||||
cp ${obj.originalPath} $path
|
||||
end
|
||||
echo $path
|
||||
end`]
|
||||
stdout: SplitParser {
|
||||
onRead: data => {
|
||||
if (data === "start") {
|
||||
if (obj.loadOriginal)
|
||||
obj.path = obj.originalPath;
|
||||
} else {
|
||||
obj.path = data;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function reload(): void {
|
||||
proc.signal(9);
|
||||
proc.running = true;
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: thumbComp
|
||||
|
||||
Thumbnail {}
|
||||
}
|
||||
}
|
20
services/Time.qml
Normal file
20
services/Time.qml
Normal file
|
@ -0,0 +1,20 @@
|
|||
pragma Singleton
|
||||
|
||||
import Quickshell
|
||||
|
||||
Singleton {
|
||||
property alias enabled: clock.enabled
|
||||
readonly property date date: clock.date
|
||||
readonly property int hours: clock.hours
|
||||
readonly property int minutes: clock.minutes
|
||||
readonly property int seconds: clock.seconds
|
||||
|
||||
function format(fmt: string): string {
|
||||
return Qt.formatDateTime(clock.date, fmt);
|
||||
}
|
||||
|
||||
SystemClock {
|
||||
id: clock
|
||||
precision: SystemClock.Seconds
|
||||
}
|
||||
}
|
12
services/Visibilities.qml
Normal file
12
services/Visibilities.qml
Normal file
|
@ -0,0 +1,12 @@
|
|||
pragma Singleton
|
||||
|
||||
import Quickshell
|
||||
|
||||
Singleton {
|
||||
property var screens: ({})
|
||||
property var panels: ({})
|
||||
|
||||
function getForActive(): PersistentProperties {
|
||||
return Object.entries(screens).find(s => s[0].slice(s[0].indexOf('"') + 1, s[0].lastIndexOf('"')) === Hyprland.focusedMonitor.name)[1];
|
||||
}
|
||||
}
|
102
services/Wallpapers.qml
Normal file
102
services/Wallpapers.qml
Normal file
|
@ -0,0 +1,102 @@
|
|||
pragma Singleton
|
||||
|
||||
import "root:/utils/scripts/fuzzysort.js" as Fuzzy
|
||||
import "root:/utils"
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import QtQuick
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
readonly property string currentNamePath: `${Paths.state}/wallpaper/last.txt`.slice(7)
|
||||
readonly property string path: `${Paths.pictures}/Wallpapers`.slice(7)
|
||||
|
||||
readonly property list<Wallpaper> list: wallpapers.instances
|
||||
property bool showPreview: false
|
||||
readonly property string current: showPreview ? previewPath : actualCurrent
|
||||
property string previewPath
|
||||
property string actualCurrent
|
||||
|
||||
readonly property list<var> preppedWalls: list.map(w => ({
|
||||
name: Fuzzy.prepare(w.name),
|
||||
path: Fuzzy.prepare(w.path),
|
||||
wall: w
|
||||
}))
|
||||
|
||||
function fuzzyQuery(search: string): var {
|
||||
return Fuzzy.go(search, preppedWalls, {
|
||||
all: true,
|
||||
keys: ["name", "path"],
|
||||
scoreFn: r => r[0].score * 0.9 + r[1].score * 0.1
|
||||
}).map(r => r.obj.wall);
|
||||
}
|
||||
|
||||
function setWallpaper(path: string): void {
|
||||
actualCurrent = path;
|
||||
setWall.path = path;
|
||||
setWall.startDetached();
|
||||
}
|
||||
|
||||
function preview(path: string): void {
|
||||
previewPath = path;
|
||||
showPreview = true;
|
||||
getPreviewColoursProc.running = true;
|
||||
}
|
||||
|
||||
function stopPreview(): void {
|
||||
showPreview = false;
|
||||
Colours.endPreviewOnNextChange = true;
|
||||
}
|
||||
|
||||
reloadableId: "wallpapers"
|
||||
|
||||
FileView {
|
||||
path: root.currentNamePath
|
||||
watchChanges: true
|
||||
onFileChanged: reload()
|
||||
onLoaded: root.actualCurrent = text().trim()
|
||||
}
|
||||
|
||||
Process {
|
||||
id: getPreviewColoursProc
|
||||
|
||||
command: ["caelestia", "scheme", "print", root.previewPath]
|
||||
stdout: SplitParser {
|
||||
splitMarker: ""
|
||||
onRead: data => {
|
||||
Colours.load(data, true);
|
||||
Colours.showPreview = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: setWall
|
||||
|
||||
property string path
|
||||
|
||||
command: ["caelestia", "wallpaper", "-f", path]
|
||||
}
|
||||
|
||||
Process {
|
||||
running: true
|
||||
command: ["fd", ".", root.path, "-t", "f", "-e", "jpg", "-e", "jpeg", "-e", "png", "-e", "svg"]
|
||||
stdout: SplitParser {
|
||||
splitMarker: ""
|
||||
onRead: data => wallpapers.model = data.trim().split("\n")
|
||||
}
|
||||
}
|
||||
|
||||
Variants {
|
||||
id: wallpapers
|
||||
|
||||
Wallpaper {}
|
||||
}
|
||||
|
||||
component Wallpaper: QtObject {
|
||||
required property string modelData
|
||||
readonly property string path: modelData
|
||||
readonly property string name: path.slice(path.lastIndexOf("/") + 1, path.lastIndexOf("."))
|
||||
}
|
||||
}
|
32
services/Weather.qml
Normal file
32
services/Weather.qml
Normal file
|
@ -0,0 +1,32 @@
|
|||
pragma Singleton
|
||||
|
||||
import "root:/utils"
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
property string icon
|
||||
property string description
|
||||
property real temperature
|
||||
|
||||
function reload(): void {
|
||||
wttrProc.running = true;
|
||||
}
|
||||
|
||||
Process {
|
||||
id: wttrProc
|
||||
|
||||
running: true
|
||||
command: ["fish", "-c", `curl "https://wttr.in/$(curl ipinfo.io | jq -r '.city' | string replace -a ' ' '%20')?format=j1" | jq -c '.current_condition[0] | {code: .weatherCode, desc: .weatherDesc[0].value, temp: .temp_C}'`]
|
||||
stdout: SplitParser {
|
||||
onRead: data => {
|
||||
const json = JSON.parse(data);
|
||||
root.icon = Icons.getWeatherIcon(json.code);
|
||||
root.description = json.desc;
|
||||
root.temperature = parseFloat(json.temp);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
10
shell.qml
Normal file
10
shell.qml
Normal file
|
@ -0,0 +1,10 @@
|
|||
import "modules"
|
||||
import "modules/drawers"
|
||||
import "modules/background"
|
||||
import Quickshell
|
||||
|
||||
ShellRoot {
|
||||
Drawers {}
|
||||
|
||||
Shortcuts {}
|
||||
}
|
222
utils/Icons.qml
Normal file
222
utils/Icons.qml
Normal file
|
@ -0,0 +1,222 @@
|
|||
pragma Singleton
|
||||
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
readonly property var osIcons: ({
|
||||
almalinux: "",
|
||||
alpine: "",
|
||||
arch: "",
|
||||
archcraft: "",
|
||||
arcolinux: "",
|
||||
artix: "",
|
||||
centos: "",
|
||||
debian: "",
|
||||
devuan: "",
|
||||
elementary: "",
|
||||
endeavouros: "",
|
||||
fedora: "",
|
||||
freebsd: "",
|
||||
garuda: "",
|
||||
gentoo: "",
|
||||
hyperbola: "",
|
||||
kali: "",
|
||||
linuxmint: "",
|
||||
mageia: "",
|
||||
openmandriva: "",
|
||||
manjaro: "",
|
||||
neon: "",
|
||||
nixos: "",
|
||||
opensuse: "",
|
||||
suse: "",
|
||||
sles: "",
|
||||
sles_sap: "",
|
||||
"opensuse-tumbleweed": "",
|
||||
parrot: "",
|
||||
pop: "",
|
||||
raspbian: "",
|
||||
rhel: "",
|
||||
rocky: "",
|
||||
slackware: "",
|
||||
solus: "",
|
||||
steamos: "",
|
||||
tails: "",
|
||||
trisquel: "",
|
||||
ubuntu: "",
|
||||
vanilla: "",
|
||||
void: "",
|
||||
zorin: ""
|
||||
})
|
||||
|
||||
readonly property var weatherIcons: ({
|
||||
"113": "clear_day",
|
||||
"116": "partly_cloudy_day",
|
||||
"119": "cloud",
|
||||
"122": "cloud",
|
||||
"143": "foggy",
|
||||
"176": "rainy",
|
||||
"179": "rainy",
|
||||
"182": "rainy",
|
||||
"185": "rainy",
|
||||
"200": "thunderstorm",
|
||||
"227": "cloudy_snowing",
|
||||
"230": "snowing_heavy",
|
||||
"248": "foggy",
|
||||
"260": "foggy",
|
||||
"263": "rainy",
|
||||
"266": "rainy",
|
||||
"281": "rainy",
|
||||
"284": "rainy",
|
||||
"293": "rainy",
|
||||
"296": "rainy",
|
||||
"299": "rainy",
|
||||
"302": "weather_hail",
|
||||
"305": "rainy",
|
||||
"308": "weather_hail",
|
||||
"311": "rainy",
|
||||
"314": "rainy",
|
||||
"317": "rainy",
|
||||
"320": "cloudy_snowing",
|
||||
"323": "cloudy_snowing",
|
||||
"326": "cloudy_snowing",
|
||||
"329": "snowing_heavy",
|
||||
"332": "snowing_heavy",
|
||||
"335": "snowing",
|
||||
"338": "snowing_heavy",
|
||||
"350": "rainy",
|
||||
"353": "rainy",
|
||||
"356": "rainy",
|
||||
"359": "weather_hail",
|
||||
"362": "rainy",
|
||||
"365": "rainy",
|
||||
"368": "cloudy_snowing",
|
||||
"371": "snowing",
|
||||
"374": "rainy",
|
||||
"377": "rainy",
|
||||
"386": "thunderstorm",
|
||||
"389": "thunderstorm",
|
||||
"392": "thunderstorm",
|
||||
"395": "snowing"
|
||||
})
|
||||
|
||||
readonly property var desktopEntrySubs: ({})
|
||||
|
||||
readonly property var categoryIcons: ({
|
||||
WebBrowser: "web",
|
||||
Printing: "print",
|
||||
Security: "security",
|
||||
Network: "chat",
|
||||
Archiving: "archive",
|
||||
Compression: "archive",
|
||||
Development: "code",
|
||||
IDE: "code",
|
||||
TextEditor: "edit_note",
|
||||
Audio: "music_note",
|
||||
Music: "music_note",
|
||||
Player: "music_note",
|
||||
Recorder: "mic",
|
||||
Game: "sports_esports",
|
||||
FileTools: "files",
|
||||
FileManager: "files",
|
||||
Filesystem: "files",
|
||||
FileTransfer: "files",
|
||||
Settings: "settings",
|
||||
DesktopSettings: "settings",
|
||||
HardwareSettings: "settings",
|
||||
TerminalEmulator: "terminal",
|
||||
ConsoleOnly: "terminal",
|
||||
Utility: "build",
|
||||
Monitor: "monitor_heart",
|
||||
Midi: "graphic_eq",
|
||||
Mixer: "graphic_eq",
|
||||
AudioVideoEditing: "video_settings",
|
||||
AudioVideo: "music_video",
|
||||
Video: "videocam",
|
||||
Building: "construction",
|
||||
Graphics: "photo_library",
|
||||
"2DGraphics": "photo_library",
|
||||
RasterGraphics: "photo_library",
|
||||
TV: "tv",
|
||||
System: "host",
|
||||
Office: "content_paste"
|
||||
})
|
||||
|
||||
property string osIcon: ""
|
||||
property string osName
|
||||
|
||||
function getDesktopEntry(name: string): DesktopEntry {
|
||||
name = name.toLowerCase().replace(/ /g, "-");
|
||||
|
||||
if (desktopEntrySubs.hasOwnProperty(name))
|
||||
name = desktopEntrySubs[name];
|
||||
|
||||
return DesktopEntries.applications.values.find(a => a.id.toLowerCase() === name) ?? null;
|
||||
}
|
||||
|
||||
function getAppIcon(name: string, fallback: string): string {
|
||||
return Quickshell.iconPath(getDesktopEntry(name)?.icon, fallback);
|
||||
}
|
||||
|
||||
function getAppCategoryIcon(name: string, fallback: string): string {
|
||||
const categories = getDesktopEntry(name)?.categories;
|
||||
|
||||
if (categories)
|
||||
for (const [key, value] of Object.entries(categoryIcons))
|
||||
if (categories.includes(key))
|
||||
return value;
|
||||
return fallback;
|
||||
}
|
||||
|
||||
function getNetworkIcon(strength: int): string {
|
||||
if (strength >= 80)
|
||||
return "signal_wifi_4_bar";
|
||||
if (strength >= 60)
|
||||
return "network_wifi_3_bar";
|
||||
if (strength >= 40)
|
||||
return "network_wifi_2_bar";
|
||||
if (strength >= 20)
|
||||
return "network_wifi_1_bar";
|
||||
return "signal_wifi_0_bar";
|
||||
}
|
||||
|
||||
function getBluetoothIcon(icon: string): string {
|
||||
if (icon.includes("headset") || icon.includes("headphones"))
|
||||
return "headphones";
|
||||
if (icon.includes("audio"))
|
||||
return "speaker";
|
||||
if (icon.includes("phone"))
|
||||
return "smartphone";
|
||||
return "bluetooth";
|
||||
}
|
||||
|
||||
function getWeatherIcon(code: string): string {
|
||||
if (weatherIcons.hasOwnProperty(code))
|
||||
return weatherIcons[code];
|
||||
return "air";
|
||||
}
|
||||
|
||||
FileView {
|
||||
path: "/etc/os-release"
|
||||
onLoaded: {
|
||||
const lines = text().split("\n");
|
||||
let osId = lines.find(l => l.startsWith("ID="))?.split("=")[1];
|
||||
if (root.osIcons.hasOwnProperty(osId))
|
||||
root.osIcon = root.osIcons[osId];
|
||||
else {
|
||||
const osIdLike = lines.find(l => l.startsWith("ID_LIKE="))?.split("=")[1];
|
||||
if (osIdLike)
|
||||
for (const id of osIdLike.split(" "))
|
||||
if (root.osIcons.hasOwnProperty(id))
|
||||
return root.osIcon = root.osIcons[id];
|
||||
}
|
||||
|
||||
let nameLine = lines.find(l => l.startsWith("PRETTY_NAME="));
|
||||
if (!nameLine)
|
||||
nameLine = lines.find(l => l.startsWith("NAME="));
|
||||
root.osName = nameLine.split("=")[1].slice(1, -1);
|
||||
}
|
||||
}
|
||||
}
|
15
utils/Paths.qml
Normal file
15
utils/Paths.qml
Normal file
|
@ -0,0 +1,15 @@
|
|||
pragma Singleton
|
||||
|
||||
import Quickshell
|
||||
import Qt.labs.platform
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
readonly property url home: StandardPaths.standardLocations(StandardPaths.HomeLocation)[0]
|
||||
readonly property url pictures: StandardPaths.standardLocations(StandardPaths.PicturesLocation)[0]
|
||||
|
||||
readonly property url data: `${StandardPaths.standardLocations(StandardPaths.GenericDataLocation)[0]}/caelestia`
|
||||
readonly property url state: `${StandardPaths.standardLocations(StandardPaths.GenericStateLocation)[0]}/caelestia`
|
||||
readonly property url cache: `${StandardPaths.standardLocations(StandardPaths.GenericCacheLocation)[0]}/caelestia`
|
||||
}
|
679
utils/scripts/fuzzysort.js
Normal file
679
utils/scripts/fuzzysort.js
Normal file
|
@ -0,0 +1,679 @@
|
|||
.pragma library
|
||||
|
||||
var single = (search, target) => {
|
||||
if(!search || !target) return NULL
|
||||
|
||||
var preparedSearch = getPreparedSearch(search)
|
||||
if(!isPrepared(target)) target = getPrepared(target)
|
||||
|
||||
var searchBitflags = preparedSearch.bitflags
|
||||
if((searchBitflags & target._bitflags) !== searchBitflags) return NULL
|
||||
|
||||
return algorithm(preparedSearch, target)
|
||||
}
|
||||
|
||||
var go = (search, targets, options) => {
|
||||
if(!search) return options?.all ? all(targets, options) : noResults
|
||||
|
||||
var preparedSearch = getPreparedSearch(search)
|
||||
var searchBitflags = preparedSearch.bitflags
|
||||
var containsSpace = preparedSearch.containsSpace
|
||||
|
||||
var threshold = denormalizeScore( options?.threshold || 0 )
|
||||
var limit = options?.limit || INFINITY
|
||||
|
||||
var resultsLen = 0; var limitedCount = 0
|
||||
var targetsLen = targets.length
|
||||
|
||||
function push_result(result) {
|
||||
if(resultsLen < limit) { q.add(result); ++resultsLen }
|
||||
else {
|
||||
++limitedCount
|
||||
if(result._score > q.peek()._score) q.replaceTop(result)
|
||||
}
|
||||
}
|
||||
|
||||
// This code is copy/pasted 3 times for performance reasons [options.key, options.keys, no keys]
|
||||
|
||||
// options.key
|
||||
if(options?.key) {
|
||||
var key = options.key
|
||||
for(var i = 0; i < targetsLen; ++i) { var obj = targets[i]
|
||||
var target = getValue(obj, key)
|
||||
if(!target) continue
|
||||
if(!isPrepared(target)) target = getPrepared(target)
|
||||
|
||||
if((searchBitflags & target._bitflags) !== searchBitflags) continue
|
||||
var result = algorithm(preparedSearch, target)
|
||||
if(result === NULL) continue
|
||||
if(result._score < threshold) continue
|
||||
|
||||
result.obj = obj
|
||||
push_result(result)
|
||||
}
|
||||
|
||||
// options.keys
|
||||
} else if(options?.keys) {
|
||||
var keys = options.keys
|
||||
var keysLen = keys.length
|
||||
|
||||
outer: for(var i = 0; i < targetsLen; ++i) { var obj = targets[i]
|
||||
|
||||
{ // early out based on bitflags
|
||||
var keysBitflags = 0
|
||||
for (var keyI = 0; keyI < keysLen; ++keyI) {
|
||||
var key = keys[keyI]
|
||||
var target = getValue(obj, key)
|
||||
if(!target) { tmpTargets[keyI] = noTarget; continue }
|
||||
if(!isPrepared(target)) target = getPrepared(target)
|
||||
tmpTargets[keyI] = target
|
||||
|
||||
keysBitflags |= target._bitflags
|
||||
}
|
||||
|
||||
if((searchBitflags & keysBitflags) !== searchBitflags) continue
|
||||
}
|
||||
|
||||
if(containsSpace) for(let i=0; i<preparedSearch.spaceSearches.length; i++) keysSpacesBestScores[i] = NEGATIVE_INFINITY
|
||||
|
||||
for (var keyI = 0; keyI < keysLen; ++keyI) {
|
||||
target = tmpTargets[keyI]
|
||||
if(target === noTarget) { tmpResults[keyI] = noTarget; continue }
|
||||
|
||||
tmpResults[keyI] = algorithm(preparedSearch, target, /*allowSpaces=*/false, /*allowPartialMatch=*/containsSpace)
|
||||
if(tmpResults[keyI] === NULL) { tmpResults[keyI] = noTarget; continue }
|
||||
|
||||
// todo: this seems weird and wrong. like what if our first match wasn't good. this should just replace it instead of averaging with it
|
||||
// if our second match isn't good we ignore it instead of averaging with it
|
||||
if(containsSpace) for(let i=0; i<preparedSearch.spaceSearches.length; i++) {
|
||||
if(allowPartialMatchScores[i] > -1000) {
|
||||
if(keysSpacesBestScores[i] > NEGATIVE_INFINITY) {
|
||||
var tmp = (keysSpacesBestScores[i] + allowPartialMatchScores[i]) / 4/*bonus score for having multiple matches*/
|
||||
if(tmp > keysSpacesBestScores[i]) keysSpacesBestScores[i] = tmp
|
||||
}
|
||||
}
|
||||
if(allowPartialMatchScores[i] > keysSpacesBestScores[i]) keysSpacesBestScores[i] = allowPartialMatchScores[i]
|
||||
}
|
||||
}
|
||||
|
||||
if(containsSpace) {
|
||||
for(let i=0; i<preparedSearch.spaceSearches.length; i++) { if(keysSpacesBestScores[i] === NEGATIVE_INFINITY) continue outer }
|
||||
} else {
|
||||
var hasAtLeast1Match = false
|
||||
for(let i=0; i < keysLen; i++) { if(tmpResults[i]._score !== NEGATIVE_INFINITY) { hasAtLeast1Match = true; break } }
|
||||
if(!hasAtLeast1Match) continue
|
||||
}
|
||||
|
||||
var objResults = new KeysResult(keysLen)
|
||||
for(let i=0; i < keysLen; i++) { objResults[i] = tmpResults[i] }
|
||||
|
||||
if(containsSpace) {
|
||||
var score = 0
|
||||
for(let i=0; i<preparedSearch.spaceSearches.length; i++) score += keysSpacesBestScores[i]
|
||||
} else {
|
||||
// todo could rewrite this scoring to be more similar to when there's spaces
|
||||
// if we match multiple keys give us bonus points
|
||||
var score = NEGATIVE_INFINITY
|
||||
for(let i=0; i<keysLen; i++) {
|
||||
var result = objResults[i]
|
||||
if(result._score > -1000) {
|
||||
if(score > NEGATIVE_INFINITY) {
|
||||
var tmp = (score + result._score) / 4/*bonus score for having multiple matches*/
|
||||
if(tmp > score) score = tmp
|
||||
}
|
||||
}
|
||||
if(result._score > score) score = result._score
|
||||
}
|
||||
}
|
||||
|
||||
objResults.obj = obj
|
||||
objResults._score = score
|
||||
if(options?.scoreFn) {
|
||||
score = options.scoreFn(objResults)
|
||||
if(!score) continue
|
||||
score = denormalizeScore(score)
|
||||
objResults._score = score
|
||||
}
|
||||
|
||||
if(score < threshold) continue
|
||||
push_result(objResults)
|
||||
}
|
||||
|
||||
// no keys
|
||||
} else {
|
||||
for(var i = 0; i < targetsLen; ++i) { var target = targets[i]
|
||||
if(!target) continue
|
||||
if(!isPrepared(target)) target = getPrepared(target)
|
||||
|
||||
if((searchBitflags & target._bitflags) !== searchBitflags) continue
|
||||
var result = algorithm(preparedSearch, target)
|
||||
if(result === NULL) continue
|
||||
if(result._score < threshold) continue
|
||||
|
||||
push_result(result)
|
||||
}
|
||||
}
|
||||
|
||||
if(resultsLen === 0) return noResults
|
||||
var results = new Array(resultsLen)
|
||||
for(var i = resultsLen - 1; i >= 0; --i) results[i] = q.poll()
|
||||
results.total = resultsLen + limitedCount
|
||||
return results
|
||||
}
|
||||
|
||||
|
||||
// this is written as 1 function instead of 2 for minification. perf seems fine ...
|
||||
// except when minified. the perf is very slow
|
||||
var highlight = (result, open='<b>', close='</b>') => {
|
||||
var callback = typeof open === 'function' ? open : undefined
|
||||
|
||||
var target = result.target
|
||||
var targetLen = target.length
|
||||
var indexes = result.indexes
|
||||
var highlighted = ''
|
||||
var matchI = 0
|
||||
var indexesI = 0
|
||||
var opened = false
|
||||
var parts = []
|
||||
|
||||
for(var i = 0; i < targetLen; ++i) { var char = target[i]
|
||||
if(indexes[indexesI] === i) {
|
||||
++indexesI
|
||||
if(!opened) { opened = true
|
||||
if(callback) {
|
||||
parts.push(highlighted); highlighted = ''
|
||||
} else {
|
||||
highlighted += open
|
||||
}
|
||||
}
|
||||
|
||||
if(indexesI === indexes.length) {
|
||||
if(callback) {
|
||||
highlighted += char
|
||||
parts.push(callback(highlighted, matchI++)); highlighted = ''
|
||||
parts.push(target.substr(i+1))
|
||||
} else {
|
||||
highlighted += char + close + target.substr(i+1)
|
||||
}
|
||||
break
|
||||
}
|
||||
} else {
|
||||
if(opened) { opened = false
|
||||
if(callback) {
|
||||
parts.push(callback(highlighted, matchI++)); highlighted = ''
|
||||
} else {
|
||||
highlighted += close
|
||||
}
|
||||
}
|
||||
}
|
||||
highlighted += char
|
||||
}
|
||||
|
||||
return callback ? parts : highlighted
|
||||
}
|
||||
|
||||
|
||||
var prepare = (target) => {
|
||||
if(typeof target === 'number') target = ''+target
|
||||
else if(typeof target !== 'string') target = ''
|
||||
var info = prepareLowerInfo(target)
|
||||
return new_result(target, {_targetLower:info._lower, _targetLowerCodes:info.lowerCodes, _bitflags:info.bitflags})
|
||||
}
|
||||
|
||||
var cleanup = () => { preparedCache.clear(); preparedSearchCache.clear() }
|
||||
|
||||
|
||||
// Below this point is only internal code
|
||||
// Below this point is only internal code
|
||||
// Below this point is only internal code
|
||||
// Below this point is only internal code
|
||||
|
||||
|
||||
class Result {
|
||||
get ['indexes']() { return this._indexes.slice(0, this._indexes.len).sort((a,b)=>a-b) }
|
||||
set ['indexes'](indexes) { return this._indexes = indexes }
|
||||
['highlight'](open, close) { return highlight(this, open, close) }
|
||||
get ['score']() { return normalizeScore(this._score) }
|
||||
set ['score'](score) { this._score = denormalizeScore(score) }
|
||||
}
|
||||
|
||||
class KeysResult extends Array {
|
||||
get ['score']() { return normalizeScore(this._score) }
|
||||
set ['score'](score) { this._score = denormalizeScore(score) }
|
||||
}
|
||||
|
||||
var new_result = (target, options) => {
|
||||
const result = new Result()
|
||||
result['target'] = target
|
||||
result['obj'] = options.obj ?? NULL
|
||||
result._score = options._score ?? NEGATIVE_INFINITY
|
||||
result._indexes = options._indexes ?? []
|
||||
result._targetLower = options._targetLower ?? ''
|
||||
result._targetLowerCodes = options._targetLowerCodes ?? NULL
|
||||
result._nextBeginningIndexes = options._nextBeginningIndexes ?? NULL
|
||||
result._bitflags = options._bitflags ?? 0
|
||||
return result
|
||||
}
|
||||
|
||||
|
||||
var normalizeScore = score => {
|
||||
if(score === NEGATIVE_INFINITY) return 0
|
||||
if(score > 1) return score
|
||||
return Math.E ** ( ((-score + 1)**.04307 - 1) * -2)
|
||||
}
|
||||
var denormalizeScore = normalizedScore => {
|
||||
if(normalizedScore === 0) return NEGATIVE_INFINITY
|
||||
if(normalizedScore > 1) return normalizedScore
|
||||
return 1 - Math.pow((Math.log(normalizedScore) / -2 + 1), 1 / 0.04307)
|
||||
}
|
||||
|
||||
|
||||
var prepareSearch = (search) => {
|
||||
if(typeof search === 'number') search = ''+search
|
||||
else if(typeof search !== 'string') search = ''
|
||||
search = search.trim()
|
||||
var info = prepareLowerInfo(search)
|
||||
|
||||
var spaceSearches = []
|
||||
if(info.containsSpace) {
|
||||
var searches = search.split(/\s+/)
|
||||
searches = [...new Set(searches)] // distinct
|
||||
for(var i=0; i<searches.length; i++) {
|
||||
if(searches[i] === '') continue
|
||||
var _info = prepareLowerInfo(searches[i])
|
||||
spaceSearches.push({lowerCodes:_info.lowerCodes, _lower:searches[i].toLowerCase(), containsSpace:false})
|
||||
}
|
||||
}
|
||||
|
||||
return {lowerCodes: info.lowerCodes, _lower: info._lower, containsSpace: info.containsSpace, bitflags: info.bitflags, spaceSearches: spaceSearches}
|
||||
}
|
||||
|
||||
|
||||
|
||||
var getPrepared = (target) => {
|
||||
if(target.length > 999) return prepare(target) // don't cache huge targets
|
||||
var targetPrepared = preparedCache.get(target)
|
||||
if(targetPrepared !== undefined) return targetPrepared
|
||||
targetPrepared = prepare(target)
|
||||
preparedCache.set(target, targetPrepared)
|
||||
return targetPrepared
|
||||
}
|
||||
var getPreparedSearch = (search) => {
|
||||
if(search.length > 999) return prepareSearch(search) // don't cache huge searches
|
||||
var searchPrepared = preparedSearchCache.get(search)
|
||||
if(searchPrepared !== undefined) return searchPrepared
|
||||
searchPrepared = prepareSearch(search)
|
||||
preparedSearchCache.set(search, searchPrepared)
|
||||
return searchPrepared
|
||||
}
|
||||
|
||||
|
||||
var all = (targets, options) => {
|
||||
var results = []; results.total = targets.length // this total can be wrong if some targets are skipped
|
||||
|
||||
var limit = options?.limit || INFINITY
|
||||
|
||||
if(options?.key) {
|
||||
for(var i=0;i<targets.length;i++) { var obj = targets[i]
|
||||
var target = getValue(obj, options.key)
|
||||
if(target == NULL) continue
|
||||
if(!isPrepared(target)) target = getPrepared(target)
|
||||
var result = new_result(target.target, {_score: target._score, obj: obj})
|
||||
results.push(result); if(results.length >= limit) return results
|
||||
}
|
||||
} else if(options?.keys) {
|
||||
for(var i=0;i<targets.length;i++) { var obj = targets[i]
|
||||
var objResults = new KeysResult(options.keys.length)
|
||||
for (var keyI = options.keys.length - 1; keyI >= 0; --keyI) {
|
||||
var target = getValue(obj, options.keys[keyI])
|
||||
if(!target) { objResults[keyI] = noTarget; continue }
|
||||
if(!isPrepared(target)) target = getPrepared(target)
|
||||
target._score = NEGATIVE_INFINITY
|
||||
target._indexes.len = 0
|
||||
objResults[keyI] = target
|
||||
}
|
||||
objResults.obj = obj
|
||||
objResults._score = NEGATIVE_INFINITY
|
||||
results.push(objResults); if(results.length >= limit) return results
|
||||
}
|
||||
} else {
|
||||
for(var i=0;i<targets.length;i++) { var target = targets[i]
|
||||
if(target == NULL) continue
|
||||
if(!isPrepared(target)) target = getPrepared(target)
|
||||
target._score = NEGATIVE_INFINITY
|
||||
target._indexes.len = 0
|
||||
results.push(target); if(results.length >= limit) return results
|
||||
}
|
||||
}
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
|
||||
var algorithm = (preparedSearch, prepared, allowSpaces=false, allowPartialMatch=false) => {
|
||||
if(allowSpaces===false && preparedSearch.containsSpace) return algorithmSpaces(preparedSearch, prepared, allowPartialMatch)
|
||||
|
||||
var searchLower = preparedSearch._lower
|
||||
var searchLowerCodes = preparedSearch.lowerCodes
|
||||
var searchLowerCode = searchLowerCodes[0]
|
||||
var targetLowerCodes = prepared._targetLowerCodes
|
||||
var searchLen = searchLowerCodes.length
|
||||
var targetLen = targetLowerCodes.length
|
||||
var searchI = 0 // where we at
|
||||
var targetI = 0 // where you at
|
||||
var matchesSimpleLen = 0
|
||||
|
||||
// very basic fuzzy match; to remove non-matching targets ASAP!
|
||||
// walk through target. find sequential matches.
|
||||
// if all chars aren't found then exit
|
||||
for(;;) {
|
||||
var isMatch = searchLowerCode === targetLowerCodes[targetI]
|
||||
if(isMatch) {
|
||||
matchesSimple[matchesSimpleLen++] = targetI
|
||||
++searchI; if(searchI === searchLen) break
|
||||
searchLowerCode = searchLowerCodes[searchI]
|
||||
}
|
||||
++targetI; if(targetI >= targetLen) return NULL // Failed to find searchI
|
||||
}
|
||||
|
||||
var searchI = 0
|
||||
var successStrict = false
|
||||
var matchesStrictLen = 0
|
||||
|
||||
var nextBeginningIndexes = prepared._nextBeginningIndexes
|
||||
if(nextBeginningIndexes === NULL) nextBeginningIndexes = prepared._nextBeginningIndexes = prepareNextBeginningIndexes(prepared.target)
|
||||
targetI = matchesSimple[0]===0 ? 0 : nextBeginningIndexes[matchesSimple[0]-1]
|
||||
|
||||
// Our target string successfully matched all characters in sequence!
|
||||
// Let's try a more advanced and strict test to improve the score
|
||||
// only count it as a match if it's consecutive or a beginning character!
|
||||
var backtrackCount = 0
|
||||
if(targetI !== targetLen) for(;;) {
|
||||
if(targetI >= targetLen) {
|
||||
// We failed to find a good spot for this search char, go back to the previous search char and force it forward
|
||||
if(searchI <= 0) break // We failed to push chars forward for a better match
|
||||
|
||||
++backtrackCount; if(backtrackCount > 200) break // exponential backtracking is taking too long, just give up and return a bad match
|
||||
|
||||
--searchI
|
||||
var lastMatch = matchesStrict[--matchesStrictLen]
|
||||
targetI = nextBeginningIndexes[lastMatch]
|
||||
|
||||
} else {
|
||||
var isMatch = searchLowerCodes[searchI] === targetLowerCodes[targetI]
|
||||
if(isMatch) {
|
||||
matchesStrict[matchesStrictLen++] = targetI
|
||||
++searchI; if(searchI === searchLen) { successStrict = true; break }
|
||||
++targetI
|
||||
} else {
|
||||
targetI = nextBeginningIndexes[targetI]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// check if it's a substring match
|
||||
var substringIndex = searchLen <= 1 ? -1 : prepared._targetLower.indexOf(searchLower, matchesSimple[0]) // perf: this is slow
|
||||
var isSubstring = !!~substringIndex
|
||||
var isSubstringBeginning = !isSubstring ? false : substringIndex===0 || prepared._nextBeginningIndexes[substringIndex-1] === substringIndex
|
||||
|
||||
// if it's a substring match but not at a beginning index, let's try to find a substring starting at a beginning index for a better score
|
||||
if(isSubstring && !isSubstringBeginning) {
|
||||
for(var i=0; i<nextBeginningIndexes.length; i=nextBeginningIndexes[i]) {
|
||||
if(i <= substringIndex) continue
|
||||
|
||||
for(var s=0; s<searchLen; s++) if(searchLowerCodes[s] !== prepared._targetLowerCodes[i+s]) break
|
||||
if(s === searchLen) { substringIndex = i; isSubstringBeginning = true; break }
|
||||
}
|
||||
}
|
||||
|
||||
// tally up the score & keep track of matches for highlighting later
|
||||
// if it's a simple match, we'll switch to a substring match if a substring exists
|
||||
// if it's a strict match, we'll switch to a substring match only if that's a better score
|
||||
|
||||
var calculateScore = matches => {
|
||||
var score = 0
|
||||
|
||||
var extraMatchGroupCount = 0
|
||||
for(var i = 1; i < searchLen; ++i) {
|
||||
if(matches[i] - matches[i-1] !== 1) {score -= matches[i]; ++extraMatchGroupCount}
|
||||
}
|
||||
var unmatchedDistance = matches[searchLen-1] - matches[0] - (searchLen-1)
|
||||
|
||||
score -= (12+unmatchedDistance) * extraMatchGroupCount // penality for more groups
|
||||
|
||||
if(matches[0] !== 0) score -= matches[0]*matches[0]*.2 // penality for not starting near the beginning
|
||||
|
||||
if(!successStrict) {
|
||||
score *= 1000
|
||||
} else {
|
||||
// successStrict on a target with too many beginning indexes loses points for being a bad target
|
||||
var uniqueBeginningIndexes = 1
|
||||
for(var i = nextBeginningIndexes[0]; i < targetLen; i=nextBeginningIndexes[i]) ++uniqueBeginningIndexes
|
||||
|
||||
if(uniqueBeginningIndexes > 24) score *= (uniqueBeginningIndexes-24)*10 // quite arbitrary numbers here ...
|
||||
}
|
||||
|
||||
score -= (targetLen - searchLen)/2 // penality for longer targets
|
||||
|
||||
if(isSubstring) score /= 1+searchLen*searchLen*1 // bonus for being a full substring
|
||||
if(isSubstringBeginning) score /= 1+searchLen*searchLen*1 // bonus for substring starting on a beginningIndex
|
||||
|
||||
score -= (targetLen - searchLen)/2 // penality for longer targets
|
||||
|
||||
return score
|
||||
}
|
||||
|
||||
if(!successStrict) {
|
||||
if(isSubstring) for(var i=0; i<searchLen; ++i) matchesSimple[i] = substringIndex+i // at this point it's safe to overwrite matchehsSimple with substr matches
|
||||
var matchesBest = matchesSimple
|
||||
var score = calculateScore(matchesBest)
|
||||
} else {
|
||||
if(isSubstringBeginning) {
|
||||
for(var i=0; i<searchLen; ++i) matchesSimple[i] = substringIndex+i // at this point it's safe to overwrite matchehsSimple with substr matches
|
||||
var matchesBest = matchesSimple
|
||||
var score = calculateScore(matchesSimple)
|
||||
} else {
|
||||
var matchesBest = matchesStrict
|
||||
var score = calculateScore(matchesStrict)
|
||||
}
|
||||
}
|
||||
|
||||
prepared._score = score
|
||||
|
||||
for(var i = 0; i < searchLen; ++i) prepared._indexes[i] = matchesBest[i]
|
||||
prepared._indexes.len = searchLen
|
||||
|
||||
const result = new Result()
|
||||
result.target = prepared.target
|
||||
result._score = prepared._score
|
||||
result._indexes = prepared._indexes
|
||||
return result
|
||||
}
|
||||
var algorithmSpaces = (preparedSearch, target, allowPartialMatch) => {
|
||||
var seen_indexes = new Set()
|
||||
var score = 0
|
||||
var result = NULL
|
||||
|
||||
var first_seen_index_last_search = 0
|
||||
var searches = preparedSearch.spaceSearches
|
||||
var searchesLen = searches.length
|
||||
var changeslen = 0
|
||||
|
||||
// Return _nextBeginningIndexes back to its normal state
|
||||
var resetNextBeginningIndexes = () => {
|
||||
for(let i=changeslen-1; i>=0; i--) target._nextBeginningIndexes[nextBeginningIndexesChanges[i*2 + 0]] = nextBeginningIndexesChanges[i*2 + 1]
|
||||
}
|
||||
|
||||
var hasAtLeast1Match = false
|
||||
for(var i=0; i<searchesLen; ++i) {
|
||||
allowPartialMatchScores[i] = NEGATIVE_INFINITY
|
||||
var search = searches[i]
|
||||
|
||||
result = algorithm(search, target)
|
||||
if(allowPartialMatch) {
|
||||
if(result === NULL) continue
|
||||
hasAtLeast1Match = true
|
||||
} else {
|
||||
if(result === NULL) {resetNextBeginningIndexes(); return NULL}
|
||||
}
|
||||
|
||||
// if not the last search, we need to mutate _nextBeginningIndexes for the next search
|
||||
var isTheLastSearch = i === searchesLen - 1
|
||||
if(!isTheLastSearch) {
|
||||
var indexes = result._indexes
|
||||
|
||||
var indexesIsConsecutiveSubstring = true
|
||||
for(let i=0; i<indexes.len-1; i++) {
|
||||
if(indexes[i+1] - indexes[i] !== 1) {
|
||||
indexesIsConsecutiveSubstring = false; break;
|
||||
}
|
||||
}
|
||||
|
||||
if(indexesIsConsecutiveSubstring) {
|
||||
var newBeginningIndex = indexes[indexes.len-1] + 1
|
||||
var toReplace = target._nextBeginningIndexes[newBeginningIndex-1]
|
||||
for(let i=newBeginningIndex-1; i>=0; i--) {
|
||||
if(toReplace !== target._nextBeginningIndexes[i]) break
|
||||
target._nextBeginningIndexes[i] = newBeginningIndex
|
||||
nextBeginningIndexesChanges[changeslen*2 + 0] = i
|
||||
nextBeginningIndexesChanges[changeslen*2 + 1] = toReplace
|
||||
changeslen++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
score += result._score / searchesLen
|
||||
allowPartialMatchScores[i] = result._score / searchesLen
|
||||
|
||||
// dock points based on order otherwise "c man" returns Manifest.cpp instead of CheatManager.h
|
||||
if(result._indexes[0] < first_seen_index_last_search) {
|
||||
score -= (first_seen_index_last_search - result._indexes[0]) * 2
|
||||
}
|
||||
first_seen_index_last_search = result._indexes[0]
|
||||
|
||||
for(var j=0; j<result._indexes.len; ++j) seen_indexes.add(result._indexes[j])
|
||||
}
|
||||
|
||||
if(allowPartialMatch && !hasAtLeast1Match) return NULL
|
||||
|
||||
resetNextBeginningIndexes()
|
||||
|
||||
// allows a search with spaces that's an exact substring to score well
|
||||
var allowSpacesResult = algorithm(preparedSearch, target, /*allowSpaces=*/true)
|
||||
if(allowSpacesResult !== NULL && allowSpacesResult._score > score) {
|
||||
if(allowPartialMatch) {
|
||||
for(var i=0; i<searchesLen; ++i) {
|
||||
allowPartialMatchScores[i] = allowSpacesResult._score / searchesLen
|
||||
}
|
||||
}
|
||||
return allowSpacesResult
|
||||
}
|
||||
|
||||
if(allowPartialMatch) result = target
|
||||
result._score = score
|
||||
|
||||
var i = 0
|
||||
for (let index of seen_indexes) result._indexes[i++] = index
|
||||
result._indexes.len = i
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// we use this instead of just .normalize('NFD').replace(/[\u0300-\u036f]/g, '') because that screws with japanese characters
|
||||
var remove_accents = (str) => str.replace(/\p{Script=Latin}+/gu, match => match.normalize('NFD')).replace(/[\u0300-\u036f]/g, '')
|
||||
|
||||
var prepareLowerInfo = (str) => {
|
||||
str = remove_accents(str)
|
||||
var strLen = str.length
|
||||
var lower = str.toLowerCase()
|
||||
var lowerCodes = [] // new Array(strLen) sparse array is too slow
|
||||
var bitflags = 0
|
||||
var containsSpace = false // space isn't stored in bitflags because of how searching with a space works
|
||||
|
||||
for(var i = 0; i < strLen; ++i) {
|
||||
var lowerCode = lowerCodes[i] = lower.charCodeAt(i)
|
||||
|
||||
if(lowerCode === 32) {
|
||||
containsSpace = true
|
||||
continue // it's important that we don't set any bitflags for space
|
||||
}
|
||||
|
||||
var bit = lowerCode>=97&&lowerCode<=122 ? lowerCode-97 // alphabet
|
||||
: lowerCode>=48&&lowerCode<=57 ? 26 // numbers
|
||||
// 3 bits available
|
||||
: lowerCode<=127 ? 30 // other ascii
|
||||
: 31 // other utf8
|
||||
bitflags |= 1<<bit
|
||||
}
|
||||
|
||||
return {lowerCodes:lowerCodes, bitflags:bitflags, containsSpace:containsSpace, _lower:lower}
|
||||
}
|
||||
var prepareBeginningIndexes = (target) => {
|
||||
var targetLen = target.length
|
||||
var beginningIndexes = []; var beginningIndexesLen = 0
|
||||
var wasUpper = false
|
||||
var wasAlphanum = false
|
||||
for(var i = 0; i < targetLen; ++i) {
|
||||
var targetCode = target.charCodeAt(i)
|
||||
var isUpper = targetCode>=65&&targetCode<=90
|
||||
var isAlphanum = isUpper || targetCode>=97&&targetCode<=122 || targetCode>=48&&targetCode<=57
|
||||
var isBeginning = isUpper && !wasUpper || !wasAlphanum || !isAlphanum
|
||||
wasUpper = isUpper
|
||||
wasAlphanum = isAlphanum
|
||||
if(isBeginning) beginningIndexes[beginningIndexesLen++] = i
|
||||
}
|
||||
return beginningIndexes
|
||||
}
|
||||
var prepareNextBeginningIndexes = (target) => {
|
||||
target = remove_accents(target)
|
||||
var targetLen = target.length
|
||||
var beginningIndexes = prepareBeginningIndexes(target)
|
||||
var nextBeginningIndexes = [] // new Array(targetLen) sparse array is too slow
|
||||
var lastIsBeginning = beginningIndexes[0]
|
||||
var lastIsBeginningI = 0
|
||||
for(var i = 0; i < targetLen; ++i) {
|
||||
if(lastIsBeginning > i) {
|
||||
nextBeginningIndexes[i] = lastIsBeginning
|
||||
} else {
|
||||
lastIsBeginning = beginningIndexes[++lastIsBeginningI]
|
||||
nextBeginningIndexes[i] = lastIsBeginning===undefined ? targetLen : lastIsBeginning
|
||||
}
|
||||
}
|
||||
return nextBeginningIndexes
|
||||
}
|
||||
|
||||
var preparedCache = new Map()
|
||||
var preparedSearchCache = new Map()
|
||||
|
||||
// the theory behind these being globals is to reduce garbage collection by not making new arrays
|
||||
var matchesSimple = []; var matchesStrict = []
|
||||
var nextBeginningIndexesChanges = [] // allows straw berry to match strawberry well, by modifying the end of a substring to be considered a beginning index for the rest of the search
|
||||
var keysSpacesBestScores = []; var allowPartialMatchScores = []
|
||||
var tmpTargets = []; var tmpResults = []
|
||||
|
||||
// prop = 'key' 2.5ms optimized for this case, seems to be about as fast as direct obj[prop]
|
||||
// prop = 'key1.key2' 10ms
|
||||
// prop = ['key1', 'key2'] 27ms
|
||||
// prop = obj => obj.tags.join() ??ms
|
||||
var getValue = (obj, prop) => {
|
||||
var tmp = obj[prop]; if(tmp !== undefined) return tmp
|
||||
if(typeof prop === 'function') return prop(obj) // this should run first. but that makes string props slower
|
||||
var segs = prop
|
||||
if(!Array.isArray(prop)) segs = prop.split('.')
|
||||
var len = segs.length
|
||||
var i = -1
|
||||
while (obj && (++i < len)) obj = obj[segs[i]]
|
||||
return obj
|
||||
}
|
||||
|
||||
var isPrepared = (x) => { return typeof x === 'object' && typeof x._bitflags === 'number' }
|
||||
var INFINITY = Infinity; var NEGATIVE_INFINITY = -INFINITY
|
||||
var noResults = []; noResults.total = 0
|
||||
var NULL = null
|
||||
|
||||
var noTarget = prepare('')
|
||||
|
||||
// Hacked version of https://github.com/lemire/FastPriorityQueue.js
|
||||
var fastpriorityqueue=r=>{var e=[],o=0,a={},v=r=>{for(var a=0,v=e[a],c=1;c<o;){var s=c+1;a=c,s<o&&e[s]._score<e[c]._score&&(a=s),e[a-1>>1]=e[a],c=1+(a<<1)}for(var f=a-1>>1;a>0&&v._score<e[f]._score;f=(a=f)-1>>1)e[a]=e[f];e[a]=v};return a.add=(r=>{var a=o;e[o++]=r;for(var v=a-1>>1;a>0&&r._score<e[v]._score;v=(a=v)-1>>1)e[a]=e[v];e[a]=r}),a.poll=(r=>{if(0!==o){var a=e[0];return e[0]=e[--o],v(),a}}),a.peek=(r=>{if(0!==o)return e[0]}),a.replaceTop=(r=>{e[0]=r,v()}),a}
|
||||
var q = fastpriorityqueue() // reuse this
|
||||
|
14
widgets/CachingImage.qml
Normal file
14
widgets/CachingImage.qml
Normal file
|
@ -0,0 +1,14 @@
|
|||
import "root:/services"
|
||||
import QtQuick
|
||||
|
||||
Image {
|
||||
id: root
|
||||
|
||||
property string path
|
||||
property bool loadOriginal
|
||||
readonly property Thumbnailer.Thumbnail thumbnail: Thumbnailer.go(this)
|
||||
|
||||
source: thumbnail.path ? `file://${thumbnail.path}` : ""
|
||||
asynchronous: true
|
||||
fillMode: Image.PreserveAspectCrop
|
||||
}
|
15
widgets/Colouriser.qml
Normal file
15
widgets/Colouriser.qml
Normal file
|
@ -0,0 +1,15 @@
|
|||
import "root:/config"
|
||||
import QtQuick
|
||||
import QtQuick.Effects
|
||||
|
||||
MultiEffect {
|
||||
colorization: 1
|
||||
|
||||
Behavior on colorizationColor {
|
||||
ColorAnimation {
|
||||
duration: Appearance.anim.durations.normal
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: Appearance.anim.curves.standard
|
||||
}
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue