Compare commits
5
Commits
v0.3.0
..
f75fef3f55
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f75fef3f55 | ||
|
|
246ee67fb3 | ||
|
|
5b5f38a48d | ||
|
|
8d4b910e23 | ||
|
|
5e78b3b492 |
@@ -0,0 +1,100 @@
|
|||||||
|
name: Bug Report
|
||||||
|
about: Something broke
|
||||||
|
title: "bug: "
|
||||||
|
labels:
|
||||||
|
- bug
|
||||||
|
|
||||||
|
body:
|
||||||
|
- type: textarea
|
||||||
|
id: description
|
||||||
|
attributes:
|
||||||
|
label: What happened
|
||||||
|
placeholder: Describe the bug
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: expected
|
||||||
|
attributes:
|
||||||
|
label: What should happen
|
||||||
|
placeholder: Expected behavior
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: steps
|
||||||
|
attributes:
|
||||||
|
label: Steps to reproduce
|
||||||
|
placeholder: |
|
||||||
|
1.
|
||||||
|
2.
|
||||||
|
3.
|
||||||
|
render: shell
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: dropdown
|
||||||
|
id: os
|
||||||
|
attributes:
|
||||||
|
label: OS
|
||||||
|
options:
|
||||||
|
- NixOS
|
||||||
|
- Arch
|
||||||
|
- Debian / Ubuntu
|
||||||
|
- Fedora
|
||||||
|
- Alpine
|
||||||
|
- Artix
|
||||||
|
- Gentoo
|
||||||
|
- Other
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
|
||||||
|
- type: input
|
||||||
|
id: other-os
|
||||||
|
attributes:
|
||||||
|
label: Other OS (if "Other" above)
|
||||||
|
placeholder: e.g. Void, Gentoo, FreeBSD
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
|
||||||
|
- type: dropdown
|
||||||
|
id: init
|
||||||
|
attributes:
|
||||||
|
label: Init system
|
||||||
|
description: Relevant for UWSM configuration
|
||||||
|
options:
|
||||||
|
- systemd
|
||||||
|
- non-systemd (openrc / runit / s6 / ...)
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
|
||||||
|
- type: input
|
||||||
|
id: qt-version
|
||||||
|
attributes:
|
||||||
|
label: Qt version
|
||||||
|
placeholder: 6.7
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
|
||||||
|
- type: input
|
||||||
|
id: commit
|
||||||
|
attributes:
|
||||||
|
label: Branch / commit
|
||||||
|
placeholder: main / abc1234
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: logs
|
||||||
|
attributes:
|
||||||
|
label: Logs / backtrace / screenshots
|
||||||
|
render: shell
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: workaround
|
||||||
|
attributes:
|
||||||
|
label: Found a workaround?
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
name: Feature Request
|
||||||
|
about: Idea or improvement
|
||||||
|
title: "feat: "
|
||||||
|
labels:
|
||||||
|
- enhancement
|
||||||
|
|
||||||
|
body:
|
||||||
|
- type: textarea
|
||||||
|
id: problem
|
||||||
|
attributes:
|
||||||
|
label: What's missing / annoying
|
||||||
|
placeholder: The use case or pain point
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: solution
|
||||||
|
attributes:
|
||||||
|
label: How should it work
|
||||||
|
placeholder: Describe the desired behavior
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: alternatives
|
||||||
|
attributes:
|
||||||
|
label: Other approaches considered
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
|
||||||
|
- type: dropdown
|
||||||
|
id: scope
|
||||||
|
attributes:
|
||||||
|
label: Area
|
||||||
|
options:
|
||||||
|
- Core / Daemon
|
||||||
|
- UI / Components
|
||||||
|
- Plugin system
|
||||||
|
- CMake / Build
|
||||||
|
- CI / Tooling
|
||||||
|
- Documentation
|
||||||
|
- Other
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: context
|
||||||
|
attributes:
|
||||||
|
label: Mockups / references / notes
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
name: Pull Request
|
||||||
|
about: Submit changes
|
||||||
|
title: ""
|
||||||
|
labels: []
|
||||||
|
|
||||||
|
body:
|
||||||
|
- type: textarea
|
||||||
|
id: description
|
||||||
|
attributes:
|
||||||
|
label: What & why
|
||||||
|
placeholder: Describe the change and motivation
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: input
|
||||||
|
id: issue
|
||||||
|
attributes:
|
||||||
|
label: Related issue
|
||||||
|
placeholder: "#123"
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
|
||||||
|
- type: dropdown
|
||||||
|
id: type
|
||||||
|
attributes:
|
||||||
|
label: Type
|
||||||
|
options:
|
||||||
|
- Bug fix
|
||||||
|
- New feature
|
||||||
|
- Refactor / style
|
||||||
|
- CI / build / tooling
|
||||||
|
- Documentation
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: checkboxes
|
||||||
|
id: tested
|
||||||
|
attributes:
|
||||||
|
label: Tested
|
||||||
|
options:
|
||||||
|
- label: Build succeeds
|
||||||
|
- label: Existing tests pass
|
||||||
|
- label: Manually verified
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: breaking
|
||||||
|
attributes:
|
||||||
|
label: Breaking changes?
|
||||||
|
placeholder: API/ABI breakage? Migration path?
|
||||||
|
|
||||||
|
- type: checkboxes
|
||||||
|
id: checklist
|
||||||
|
attributes:
|
||||||
|
label: Checklist
|
||||||
|
options:
|
||||||
|
- label: CI passes (format, lint, typecheck)
|
||||||
|
- label: Clean commits
|
||||||
|
- label: No new warnings
|
||||||
|
- label: Docs updated if needed
|
||||||
@@ -81,6 +81,29 @@ jobs:
|
|||||||
cd cli
|
cd cli
|
||||||
python -m pytest tests/ -v
|
python -m pytest tests/ -v
|
||||||
|
|
||||||
|
typecheck:
|
||||||
|
runs-on: alpine
|
||||||
|
container: node:26-alpine
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Install tools
|
||||||
|
run: |
|
||||||
|
apk add --no-cache \
|
||||||
|
git \
|
||||||
|
python3 \
|
||||||
|
py3-pip
|
||||||
|
python3 -m venv .venv
|
||||||
|
. .venv/bin/activate
|
||||||
|
pip install --no-cache-dir basedpyright
|
||||||
|
|
||||||
|
- name: Type check
|
||||||
|
run: |
|
||||||
|
. .venv/bin/activate
|
||||||
|
basedpyright
|
||||||
|
|
||||||
buildcheck:
|
buildcheck:
|
||||||
runs-on: alpine
|
runs-on: alpine
|
||||||
container: node:26-alpine
|
container: node:26-alpine
|
||||||
|
|||||||
+2
-22
@@ -36,15 +36,13 @@ set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
|||||||
set(CMAKE_CXX_EXTENSIONS OFF)
|
set(CMAKE_CXX_EXTENSIONS OFF)
|
||||||
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/lib")
|
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/lib")
|
||||||
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
|
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
|
||||||
set(ENABLE_MODULES "plugin;shell;m3shapes" CACHE STRING "Modules to build/install")
|
set(ENABLE_MODULES "plugin;shell" CACHE STRING "Modules to build/install")
|
||||||
|
|
||||||
set(INSTALL_LIBDIR "usr/lib/ZShell" CACHE STRING "Library install dir")
|
set(INSTALL_LIBDIR "usr/lib/ZShell" CACHE STRING "Library install dir")
|
||||||
set(INSTALL_QMLDIR "usr/lib/qt6/qml" CACHE STRING "QML install dir")
|
set(INSTALL_QMLDIR "usr/lib/qt6/qml" CACHE STRING "QML install dir")
|
||||||
set(INSTALL_QSCONFDIR "etc/xdg/quickshell/zshell" CACHE STRING "Quickshell config install dir")
|
set(INSTALL_QSCONFDIR "etc/xdg/quickshell/zshell" CACHE STRING "Quickshell config install dir")
|
||||||
set(INSTALL_GREETERCONFDIR "etc/xdg/quickshell/zshell-greeter" CACHE STRING "Quickshell greeter install dir")
|
set(INSTALL_GREETERCONFDIR "etc/xdg/quickshell/zshell-greeter" CACHE STRING "Quickshell greeter install dir")
|
||||||
|
|
||||||
set(CMAKE_INSTALL_MESSAGE NEVER)
|
|
||||||
|
|
||||||
add_compile_options(
|
add_compile_options(
|
||||||
-Wall -Wextra -Wpedantic -Wshadow -Wconversion
|
-Wall -Wextra -Wpedantic -Wshadow -Wconversion
|
||||||
-Wold-style-cast -Wnull-dereference -Wdouble-promotion
|
-Wold-style-cast -Wnull-dereference -Wdouble-promotion
|
||||||
@@ -152,6 +150,7 @@ endif()
|
|||||||
|
|
||||||
if("plugin" IN_LIST ENABLE_MODULES)
|
if("plugin" IN_LIST ENABLE_MODULES)
|
||||||
add_subdirectory(Plugins)
|
add_subdirectory(Plugins)
|
||||||
|
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
if("shell" IN_LIST ENABLE_MODULES)
|
if("shell" IN_LIST ENABLE_MODULES)
|
||||||
@@ -168,22 +167,3 @@ if("shell" IN_LIST ENABLE_MODULES)
|
|||||||
# Greeter
|
# Greeter
|
||||||
install(DIRECTORY Greeter/ DESTINATION "${INSTALL_GREETERCONFDIR}")
|
install(DIRECTORY Greeter/ DESTINATION "${INSTALL_GREETERCONFDIR}")
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
if("m3shapes" IN_LIST ENABLE_MODULES)
|
|
||||||
message(STATUS "Fetching M3Shapes module")
|
|
||||||
include(FetchContent)
|
|
||||||
set(M3SHAPES_REV bdc327b29f95394a732baf3c9b19658ba23755b6)
|
|
||||||
FetchContent_Declare(
|
|
||||||
m3shapes_external
|
|
||||||
GIT_REPOSITORY https://github.com/soramanew/m3shapes.git
|
|
||||||
GIT_TAG ${M3SHAPES_REV}
|
|
||||||
SOURCE_DIR "${CMAKE_BINARY_DIR}/_deps/m3shapes-${M3SHAPES_REV}"
|
|
||||||
)
|
|
||||||
FetchContent_MakeAvailable(m3shapes_external)
|
|
||||||
message(STATUS "Done fetching M3Shapes module")
|
|
||||||
|
|
||||||
# Fix m3shapes wrong rpath
|
|
||||||
if(TARGET m3shapesplugin)
|
|
||||||
set_target_properties(m3shapesplugin PROPERTIES INSTALL_RPATH "$ORIGIN")
|
|
||||||
endif()
|
|
||||||
endif()
|
|
||||||
|
|||||||
@@ -1,114 +0,0 @@
|
|||||||
import QtQuick
|
|
||||||
import Quickshell
|
|
||||||
import M3Shapes
|
|
||||||
import qs.Config
|
|
||||||
|
|
||||||
MaterialShape {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
property bool animated: true
|
|
||||||
property real cRotation
|
|
||||||
property bool containsIcon
|
|
||||||
property real dampingRatio: 0.6
|
|
||||||
property real lRotation
|
|
||||||
property int morphAnimRotation: 60
|
|
||||||
property real morphScale: 0.14
|
|
||||||
property alias rotateAnimDuration: rotateAnim.duration
|
|
||||||
property int shapeIndex
|
|
||||||
property list<int> shapes: {
|
|
||||||
if (containsIcon)
|
|
||||||
return [MaterialShape.SoftBurst, MaterialShape.Cookie9Sided, MaterialShape.Pill, MaterialShape.Sunny, MaterialShape.Cookie4Sided, MaterialShape.Oval];
|
|
||||||
return [MaterialShape.SoftBurst, MaterialShape.Cookie9Sided, MaterialShape.Pentagon, MaterialShape.Pill, MaterialShape.Sunny, MaterialShape.Cookie4Sided, MaterialShape.Oval];
|
|
||||||
}
|
|
||||||
readonly property real springDuration: {
|
|
||||||
const wn = Math.sqrt(stiffness);
|
|
||||||
const r = -dampingRatio * wn;
|
|
||||||
const c = 1 / Math.sqrt(1 - dampingRatio * dampingRatio);
|
|
||||||
return Math.log(visibilityThreshold / c) / r;
|
|
||||||
}
|
|
||||||
readonly property real springMaxVelocity: {
|
|
||||||
const wn = Math.sqrt(stiffness);
|
|
||||||
const factor = Math.exp(-z * Math.acos(z) / Math.sqrt(1 - z * z));
|
|
||||||
return wn * factor;
|
|
||||||
}
|
|
||||||
property bool springSettled: true
|
|
||||||
property real stiffness: 180
|
|
||||||
property real thisLRotation
|
|
||||||
property real visibilityThreshold: 0.075
|
|
||||||
|
|
||||||
function spring(t: real): var {
|
|
||||||
const wn = Math.sqrt(stiffness);
|
|
||||||
const za = dampingRatio * wn;
|
|
||||||
|
|
||||||
const wd = wn * Math.sqrt(1 - dampingRatio * dampingRatio);
|
|
||||||
const r = za / wd;
|
|
||||||
const pos = 1 - Math.exp(-za * t) * (Math.cos(wd * t) + r * Math.sin(wd * t));
|
|
||||||
const vel = Math.exp(-za * t) * (wn * wn / wd) * Math.sin(wd * t);
|
|
||||||
|
|
||||||
return [pos, vel];
|
|
||||||
}
|
|
||||||
|
|
||||||
color: DynamicColors.palette.m3primary
|
|
||||||
implicitSize: 38
|
|
||||||
toShape: shapes[0]
|
|
||||||
|
|
||||||
RotationAnimation on cRotation {
|
|
||||||
id: rotateAnim
|
|
||||||
|
|
||||||
duration: 4666
|
|
||||||
easing.type: Easing.Linear
|
|
||||||
from: 0
|
|
||||||
loops: Animation.Infinite
|
|
||||||
running: root.animated
|
|
||||||
to: 360
|
|
||||||
}
|
|
||||||
Behavior on color {
|
|
||||||
CAnim {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ElapsedTimer {
|
|
||||||
id: timer
|
|
||||||
}
|
|
||||||
|
|
||||||
FrameAnimation {
|
|
||||||
running: root.animated && !root.springSettled
|
|
||||||
|
|
||||||
onTriggered: {
|
|
||||||
const t = timer.elapsed();
|
|
||||||
|
|
||||||
if (t >= root.springDuration) {
|
|
||||||
root.springSettled = true;
|
|
||||||
} else {
|
|
||||||
const [pos, vel] = root.spring(t);
|
|
||||||
root.morphProgress = Math.min(1, pos); // Overshooting the morph looks weird
|
|
||||||
root.thisLRotation = pos * root.morphAnimRotation;
|
|
||||||
root.scale = 1 + vel * root.morphScale / root.springMaxVelocity;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Timer {
|
|
||||||
interval: 650
|
|
||||||
repeat: true
|
|
||||||
running: root.animated
|
|
||||||
triggeredOnStart: true
|
|
||||||
|
|
||||||
onTriggered: {
|
|
||||||
root.beginBatchUpdate();
|
|
||||||
root.fromShape = root.toShape;
|
|
||||||
root.shapeIndex = (root.shapeIndex + 1) % root.shapes.length;
|
|
||||||
root.toShape = root.shapes[root.shapeIndex];
|
|
||||||
root.morphProgress = 0;
|
|
||||||
|
|
||||||
root.rotation = root.rotation;
|
|
||||||
root.lRotation = (root.lRotation + root.thisLRotation) % 360;
|
|
||||||
root.thisLRotation = 0;
|
|
||||||
root.rotation = Qt.binding(() => root.cRotation + root.lRotation + root.thisLRotation);
|
|
||||||
|
|
||||||
root.springSettled = false;
|
|
||||||
timer.restart();
|
|
||||||
root.endBatchUpdate();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -6,55 +6,25 @@ import qs.Effects
|
|||||||
CustomListView {
|
CustomListView {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
property real endFadeOpacity: fadeShouldBeActive(false) ? 0 : 1
|
property real bottomFadeOpacity: fadeShouldBeActive(false) ? 0 : 1
|
||||||
property real fadeAmount: 0.1
|
property real fadeAmount: 0.1
|
||||||
readonly property bool horizontal: orientation === ListView.Horizontal
|
property real topFadeOpacity: fadeShouldBeActive(true) ? 0 : 1
|
||||||
property real startFadeOpacity: fadeShouldBeActive(true) ? 0 : 1
|
|
||||||
|
|
||||||
function contentSize(): real {
|
|
||||||
return horizontal ? contentWidth : contentHeight;
|
|
||||||
}
|
|
||||||
|
|
||||||
function fadeShouldBeActive(isStart: bool): bool {
|
function fadeShouldBeActive(isStart: bool): bool {
|
||||||
// When content is smaller than flickable size, hide fade when rebound starts.
|
// When content is smaller than flickable size, hide fade when rebound starts
|
||||||
if (contentSize() + marginStart() + marginEnd() < viewportSize() && rebound.running && ((isStart ? overshootStart() > 0 : overshootStart() < 0))) {
|
if (contentHeight + topMargin + bottomMargin < height && rebound.running && ((isStart ? verticalOvershoot > 0 : verticalOvershoot < 0)))
|
||||||
return false;
|
return false;
|
||||||
}
|
|
||||||
|
|
||||||
if (isStart)
|
if (isStart)
|
||||||
return visibleStart() > 0;
|
return visibleArea.yPosition > 0;
|
||||||
|
return visibleArea.yPosition + visibleArea.heightRatio < 1;
|
||||||
return visibleStart() + visibleRatio() < 1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function marginEnd(): real {
|
flickableDirection: Flickable.VerticalFlick
|
||||||
return horizontal ? rightMargin : bottomMargin;
|
|
||||||
}
|
|
||||||
|
|
||||||
function marginStart(): real {
|
|
||||||
return horizontal ? leftMargin : topMargin;
|
|
||||||
}
|
|
||||||
|
|
||||||
function overshootStart(): real {
|
|
||||||
return horizontal ? horizontalOvershoot : verticalOvershoot;
|
|
||||||
}
|
|
||||||
|
|
||||||
function viewportSize(): real {
|
|
||||||
return horizontal ? width : height;
|
|
||||||
}
|
|
||||||
|
|
||||||
function visibleRatio(): real {
|
|
||||||
return horizontal ? visibleArea.widthRatio : visibleArea.heightRatio;
|
|
||||||
}
|
|
||||||
|
|
||||||
function visibleStart(): real {
|
|
||||||
return horizontal ? visibleArea.xPosition : visibleArea.yPosition;
|
|
||||||
}
|
|
||||||
|
|
||||||
flickableDirection: horizontal ? Flickable.HorizontalFlick : Flickable.VerticalFlick
|
|
||||||
layer.enabled: true
|
layer.enabled: true
|
||||||
|
orientation: ListView.Vertical
|
||||||
|
|
||||||
Behavior on endFadeOpacity {
|
Behavior on bottomFadeOpacity {
|
||||||
Anim {
|
Anim {
|
||||||
type: Anim.SlowEffects
|
type: Anim.SlowEffects
|
||||||
}
|
}
|
||||||
@@ -70,10 +40,10 @@ CustomListView {
|
|||||||
visible: false
|
visible: false
|
||||||
|
|
||||||
gradient: Gradient {
|
gradient: Gradient {
|
||||||
orientation: root.horizontal ? Gradient.Horizontal : Gradient.Vertical
|
orientation: Gradient.Vertical
|
||||||
|
|
||||||
GradientStop {
|
GradientStop {
|
||||||
color: Qt.rgba(0, 0, 0, root.startFadeOpacity)
|
color: Qt.rgba(0, 0, 0, root.topFadeOpacity)
|
||||||
position: 0
|
position: 0
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -88,13 +58,13 @@ CustomListView {
|
|||||||
}
|
}
|
||||||
|
|
||||||
GradientStop {
|
GradientStop {
|
||||||
color: Qt.rgba(0, 0, 0, root.endFadeOpacity)
|
color: Qt.rgba(0, 0, 0, root.bottomFadeOpacity)
|
||||||
position: 1
|
position: 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Behavior on startFadeOpacity {
|
Behavior on topFadeOpacity {
|
||||||
Anim {
|
Anim {
|
||||||
type: Anim.SlowEffects
|
type: Anim.SlowEffects
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,46 +0,0 @@
|
|||||||
import QtQuick
|
|
||||||
import QtQuick.Shapes
|
|
||||||
import qs.Config
|
|
||||||
|
|
||||||
Shape {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
property real amplitude: 3
|
|
||||||
property color color: DynamicColors.palette.m3surfaceContainer
|
|
||||||
readonly property real waveHeight: amplitude * 2
|
|
||||||
property int waves: 4
|
|
||||||
|
|
||||||
asynchronous: true
|
|
||||||
preferredRendererType: Shape.CurveRenderer
|
|
||||||
|
|
||||||
ShapePath {
|
|
||||||
fillColor: root.color
|
|
||||||
strokeColor: "transparent"
|
|
||||||
strokeWidth: 0
|
|
||||||
|
|
||||||
Behavior on fillColor {
|
|
||||||
CAnim {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
PathSvg {
|
|
||||||
path: {
|
|
||||||
const w = root.width;
|
|
||||||
const h = root.height;
|
|
||||||
const a = root.amplitude;
|
|
||||||
const n = Math.max(1, root.waves);
|
|
||||||
const wl = w / n;
|
|
||||||
const half = wl / 2;
|
|
||||||
|
|
||||||
let d = `M 0,${a} `;
|
|
||||||
for (let i = 0; i < n; ++i) {
|
|
||||||
const x = i * wl;
|
|
||||||
d += `Q ${x + half / 2},${-a} ${x + half},${a} `;
|
|
||||||
d += `Q ${x + half + half / 2},${3 * a} ${x + wl},${a} `;
|
|
||||||
}
|
|
||||||
d += `L ${w},${h} L 0,${h} Z`;
|
|
||||||
return d;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+3
-31
@@ -1,12 +1,12 @@
|
|||||||
pragma Singleton
|
pragma Singleton
|
||||||
|
|
||||||
import QtQml
|
|
||||||
import Quickshell
|
import Quickshell
|
||||||
import Quickshell.Io
|
import Quickshell.Io
|
||||||
import Quickshell.Services.Mpris
|
import Quickshell.Services.Mpris
|
||||||
|
import QtQml
|
||||||
import ZShell
|
import ZShell
|
||||||
import qs.Components
|
|
||||||
import qs.Config
|
import qs.Config
|
||||||
|
import qs.Components
|
||||||
|
|
||||||
Singleton {
|
Singleton {
|
||||||
id: root
|
id: root
|
||||||
@@ -15,24 +15,7 @@ Singleton {
|
|||||||
readonly property list<MprisPlayer> list: Mpris.players.values
|
readonly property list<MprisPlayer> list: Mpris.players.values
|
||||||
property alias manualActive: props.manualActive
|
property alias manualActive: props.manualActive
|
||||||
|
|
||||||
function getArtUrl(player: MprisPlayer): string {
|
|
||||||
if (!player)
|
|
||||||
return "";
|
|
||||||
if (player.trackArtUrl)
|
|
||||||
return player.trackArtUrl;
|
|
||||||
|
|
||||||
const url = player.metadata["xesam:url"] ?? "";
|
|
||||||
if (url.startsWith("https://www.youtube.com/watch")) {
|
|
||||||
// Fallback for youtube
|
|
||||||
const id = url.match(/[?&]v=([\w-]{11})/)?.[1];
|
|
||||||
return id ? `https://img.youtube.com/vi/${id}/hqdefault.jpg` : "";
|
|
||||||
}
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
|
|
||||||
function getIdentity(player: MprisPlayer): string {
|
function getIdentity(player: MprisPlayer): string {
|
||||||
if (!player)
|
|
||||||
return "";
|
|
||||||
const alias = Config.services.playerAliases.find(a => a.from === player.identity);
|
const alias = Config.services.playerAliases.find(a => a.from === player.identity);
|
||||||
return alias?.to ?? player.identity;
|
return alias?.to ?? player.identity;
|
||||||
}
|
}
|
||||||
@@ -42,12 +25,9 @@ Singleton {
|
|||||||
if (!Config.utilities.toasts.nowPlaying) {
|
if (!Config.utilities.toasts.nowPlaying) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (root.active.trackArtist != "" && root.active.trackTitle != "") {
|
|
||||||
Toaster.toast(qsTr("Now Playing"), qsTr("%1 - %2").arg(root.active.trackArtist).arg(root.active.trackTitle), "music_note");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
target: root.active
|
target: active
|
||||||
}
|
}
|
||||||
|
|
||||||
PersistentProperties {
|
PersistentProperties {
|
||||||
@@ -58,10 +38,8 @@ Singleton {
|
|||||||
reloadableId: "players"
|
reloadableId: "players"
|
||||||
}
|
}
|
||||||
|
|
||||||
// qmllint disable unresolved-type
|
|
||||||
CustomShortcut {
|
CustomShortcut {
|
||||||
description: "Toggle media playback"
|
description: "Toggle media playback"
|
||||||
// qmllint enable unresolved-type
|
|
||||||
name: "mediaToggle"
|
name: "mediaToggle"
|
||||||
|
|
||||||
onPressed: {
|
onPressed: {
|
||||||
@@ -71,10 +49,8 @@ Singleton {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// qmllint disable unresolved-type
|
|
||||||
CustomShortcut {
|
CustomShortcut {
|
||||||
description: "Previous track"
|
description: "Previous track"
|
||||||
// qmllint enable unresolved-type
|
|
||||||
name: "mediaPrev"
|
name: "mediaPrev"
|
||||||
|
|
||||||
onPressed: {
|
onPressed: {
|
||||||
@@ -84,10 +60,8 @@ Singleton {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// qmllint disable unresolved-type
|
|
||||||
CustomShortcut {
|
CustomShortcut {
|
||||||
description: "Next track"
|
description: "Next track"
|
||||||
// qmllint enable unresolved-type
|
|
||||||
name: "mediaNext"
|
name: "mediaNext"
|
||||||
|
|
||||||
onPressed: {
|
onPressed: {
|
||||||
@@ -97,10 +71,8 @@ Singleton {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// qmllint disable unresolved-type
|
|
||||||
CustomShortcut {
|
CustomShortcut {
|
||||||
description: "Stop media playback"
|
description: "Stop media playback"
|
||||||
// qmllint enable unresolved-type
|
|
||||||
name: "mediaStop"
|
name: "mediaStop"
|
||||||
|
|
||||||
onPressed: root.active?.stop()
|
onPressed: root.active?.stop()
|
||||||
|
|||||||
+32
-107
@@ -1,7 +1,7 @@
|
|||||||
pragma Singleton
|
pragma Singleton
|
||||||
|
|
||||||
import QtQuick
|
|
||||||
import Quickshell
|
import Quickshell
|
||||||
|
import QtQuick
|
||||||
import ZShell
|
import ZShell
|
||||||
import qs.Config
|
import qs.Config
|
||||||
|
|
||||||
@@ -12,15 +12,15 @@ Singleton {
|
|||||||
property var cc
|
property var cc
|
||||||
property string city
|
property string city
|
||||||
readonly property string description: cc?.weatherDesc ?? qsTr("No weather")
|
readonly property string description: cc?.weatherDesc ?? qsTr("No weather")
|
||||||
readonly property string feelsLike: formatTemp(cc?.feelsLikeC)
|
readonly property string feelsLike: `${cc?.feelsLikeC ?? 0}°C`
|
||||||
property list<var> forecast
|
property list<var> forecast
|
||||||
property list<var> hourlyForecast
|
property list<var> hourlyForecast
|
||||||
readonly property int humidity: cc?.humidity ?? 0
|
readonly property int humidity: cc?.humidity ?? 0
|
||||||
readonly property string icon: cc ? Icons.getWeatherIcon(cc.weatherCode) : "cloud_alert"
|
readonly property string icon: cc ? Icons.getWeatherIcon(cc.weatherCode) : "cloud_alert"
|
||||||
property string loc
|
property string loc
|
||||||
readonly property string sunrise: cc ? Qt.formatDateTime(new Date(cc.sunrise), Config.services.useTwelveHourClock ? "h:mm A" : "h:mm") : "--:--"
|
readonly property string sunrise: cc ? Qt.formatDateTime(new Date(cc.sunrise), "h:mm") : "--:--"
|
||||||
readonly property string sunset: cc ? Qt.formatDateTime(new Date(cc.sunset), Config.services.useTwelveHourClock ? "h:mm A" : "h:mm") : "--:--"
|
readonly property string sunset: cc ? Qt.formatDateTime(new Date(cc.sunset), "h:mm") : "--:--"
|
||||||
readonly property string temp: formatTemp(cc?.tempC)
|
readonly property string temp: `${cc?.tempC ?? 0}°C`
|
||||||
readonly property real windSpeed: cc?.windSpeed ?? 0
|
readonly property real windSpeed: cc?.windSpeed ?? 0
|
||||||
|
|
||||||
function fetchCityFromCoords(coords: string): void {
|
function fetchCityFromCoords(coords: string): void {
|
||||||
@@ -29,48 +29,29 @@ Singleton {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const [lat, lon] = coords.split(",").map(s => s.trim());
|
const [lat, lon] = coords.split(",");
|
||||||
const lang = Qt.locale().name.split("_")[0] || "en";
|
const url = `https://nominatim.openstreetmap.org/reverse?lat=${lat}&lon=${lon}&format=geocodejson`;
|
||||||
|
Requests.get(url, text => {
|
||||||
const fallbackToBigDataCloud = () => {
|
|
||||||
const fallbackUrl = `https://api.bigdatacloud.net/data/reverse-geocode-client?latitude=${lat}&longitude=${lon}&localityLanguage=${lang}`;
|
|
||||||
Requests.get(fallbackUrl, text => {
|
|
||||||
const geo = JSON.parse(text);
|
|
||||||
const geoCity = geo.city || geo.locality;
|
|
||||||
if (geoCity) {
|
|
||||||
city = fixCityName(geoCity);
|
|
||||||
cachedCities.set(coords, city);
|
|
||||||
} else {
|
|
||||||
city = "Unknown City";
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const nominatimUrl = `https://nominatim.openstreetmap.org/reverse?lat=${lat}&lon=${lon}&format=geocodejson&accept-language=${lang}`;
|
|
||||||
Requests.get(nominatimUrl, text => {
|
|
||||||
const geo = JSON.parse(text).features?.[0]?.properties.geocoding;
|
const geo = JSON.parse(text).features?.[0]?.properties.geocoding;
|
||||||
if (geo) {
|
if (geo) {
|
||||||
const geoCity = geo.type === "city" ? geo.name : geo.city;
|
const geoCity = geo.type === "city" ? geo.name : geo.city;
|
||||||
if (geoCity) {
|
city = geoCity;
|
||||||
city = fixCityName(geoCity);
|
cachedCities.set(coords, geoCity);
|
||||||
cachedCities.set(coords, city);
|
} else {
|
||||||
return;
|
city = "Unknown City";
|
||||||
}
|
|
||||||
}
|
}
|
||||||
fallbackToBigDataCloud();
|
});
|
||||||
}, fallbackToBigDataCloud);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function fetchCoordsFromCity(cityName: string): void {
|
function fetchCoordsFromCity(cityName: string): void {
|
||||||
const lang = Qt.locale().name.split("_")[0] || "en";
|
const url = `https://geocoding-api.open-meteo.com/v1/search?name=${encodeURIComponent(cityName)}&count=1&language=en&format=json`;
|
||||||
const url = `https://geocoding-api.open-meteo.com/v1/search?name=${encodeURIComponent(cityName)}&count=1&language=${lang}&format=json`;
|
|
||||||
|
|
||||||
Requests.get(url, text => {
|
Requests.get(url, text => {
|
||||||
const json = JSON.parse(text);
|
const json = JSON.parse(text);
|
||||||
if (json.results && json.results.length > 0) {
|
if (json.results && json.results.length > 0) {
|
||||||
const result = json.results[0];
|
const result = json.results[0];
|
||||||
loc = result.latitude + "," + result.longitude;
|
loc = result.latitude + "," + result.longitude;
|
||||||
city = fixCityName(result.name);
|
city = result.name;
|
||||||
} else {
|
} else {
|
||||||
loc = "";
|
loc = "";
|
||||||
reload();
|
reload();
|
||||||
@@ -91,21 +72,25 @@ Singleton {
|
|||||||
cc = {
|
cc = {
|
||||||
weatherCode: json.current.weather_code,
|
weatherCode: json.current.weather_code,
|
||||||
weatherDesc: getWeatherCondition(json.current.weather_code),
|
weatherDesc: getWeatherCondition(json.current.weather_code),
|
||||||
tempC: json.current.temperature_2m,
|
tempC: Math.round(json.current.temperature_2m),
|
||||||
feelsLikeC: json.current.apparent_temperature,
|
tempF: Math.round(toFahrenheit(json.current.temperature_2m)),
|
||||||
|
feelsLikeC: Math.round(json.current.apparent_temperature),
|
||||||
|
feelsLikeF: Math.round(toFahrenheit(json.current.apparent_temperature)),
|
||||||
humidity: json.current.relative_humidity_2m,
|
humidity: json.current.relative_humidity_2m,
|
||||||
windSpeed: json.current.wind_speed_10m,
|
windSpeed: json.current.wind_speed_10m,
|
||||||
isDay: json.current.is_day,
|
isDay: json.current.is_day,
|
||||||
sunrise: json.daily.sunrise[0].replace("T", " "),
|
sunrise: json.daily.sunrise[0],
|
||||||
sunset: json.daily.sunset[0].replace("T", " ")
|
sunset: json.daily.sunset[0]
|
||||||
};
|
};
|
||||||
|
|
||||||
const forecastList = [];
|
const forecastList = [];
|
||||||
for (let i = 0; i < json.daily.time.length; i++)
|
for (let i = 0; i < json.daily.time.length; i++)
|
||||||
forecastList.push({
|
forecastList.push({
|
||||||
date: json.daily.time[i].replace(/-/g, "/"),
|
date: json.daily.time[i],
|
||||||
maxTempC: json.daily.temperature_2m_max[i],
|
maxTempC: Math.round(json.daily.temperature_2m_max[i]),
|
||||||
minTempC: json.daily.temperature_2m_min[i],
|
maxTempF: Math.round(toFahrenheit(json.daily.temperature_2m_max[i])),
|
||||||
|
minTempC: Math.round(json.daily.temperature_2m_min[i]),
|
||||||
|
minTempF: Math.round(toFahrenheit(json.daily.temperature_2m_min[i])),
|
||||||
weatherCode: json.daily.weather_code[i],
|
weatherCode: json.daily.weather_code[i],
|
||||||
icon: Icons.getWeatherIcon(json.daily.weather_code[i])
|
icon: Icons.getWeatherIcon(json.daily.weather_code[i])
|
||||||
});
|
});
|
||||||
@@ -114,8 +99,7 @@ Singleton {
|
|||||||
const hourlyList = [];
|
const hourlyList = [];
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
for (let i = 0; i < json.hourly.time.length; i++) {
|
for (let i = 0; i < json.hourly.time.length; i++) {
|
||||||
const time = new Date(json.hourly.time[i].replace("T", " "));
|
const time = new Date(json.hourly.time[i]);
|
||||||
|
|
||||||
if (time < now)
|
if (time < now)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
@@ -123,7 +107,7 @@ Singleton {
|
|||||||
timestamp: json.hourly.time[i],
|
timestamp: json.hourly.time[i],
|
||||||
hour: time.getHours(),
|
hour: time.getHours(),
|
||||||
tempC: Math.round(json.hourly.temperature_2m[i]),
|
tempC: Math.round(json.hourly.temperature_2m[i]),
|
||||||
precipChance: json.hourly.precipitation_probability[i],
|
tempF: Math.round(toFahrenheit(json.hourly.temperature_2m[i])),
|
||||||
weatherCode: json.hourly.weather_code[i],
|
weatherCode: json.hourly.weather_code[i],
|
||||||
icon: Icons.getWeatherIcon(json.hourly.weather_code[i])
|
icon: Icons.getWeatherIcon(json.hourly.weather_code[i])
|
||||||
});
|
});
|
||||||
@@ -132,59 +116,6 @@ Singleton {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function fixCityName(cityName: string): string {
|
|
||||||
if (!cityName)
|
|
||||||
return "";
|
|
||||||
const mapping = {
|
|
||||||
// Polish
|
|
||||||
"Poznan": "Poznań",
|
|
||||||
"Wroclaw": "Wrocław",
|
|
||||||
"Krakow": "Kraków",
|
|
||||||
"Gdansk": "Gdańsk",
|
|
||||||
"Lodz": "Łódź",
|
|
||||||
"Rzeszow": "Rzeszów",
|
|
||||||
"Torun": "Toruń",
|
|
||||||
"Bialystok": "Białystok",
|
|
||||||
"Czestochowa": "Częstochowa",
|
|
||||||
"Plock": "Płock",
|
|
||||||
"Ruda Slaska": "Ruda Śląska",
|
|
||||||
"Dabrowa Gornicza": "Dąbrowa Górnicza",
|
|
||||||
"Elblag": "Elbląg",
|
|
||||||
"Gorzow Wielkopolski": "Gorzów Wielkopolski",
|
|
||||||
"Zielona Gora": "Zielona Góra",
|
|
||||||
"Slupsk": "Słupsk",
|
|
||||||
|
|
||||||
// German
|
|
||||||
"Munchen": "München",
|
|
||||||
"Koln": "Köln",
|
|
||||||
"Dusseldorf": "Düsseldorf",
|
|
||||||
"Nurnberg": "Nürnberg",
|
|
||||||
|
|
||||||
// French & Spanish & Portuguese
|
|
||||||
"Sao Paulo": "São Paulo",
|
|
||||||
"Montreal": "Montréal",
|
|
||||||
"Quebec": "Québec",
|
|
||||||
"Bogota": "Bogotá",
|
|
||||||
"Medellin": "Medellín",
|
|
||||||
"Cordoba": "Córdoba",
|
|
||||||
|
|
||||||
// Turkish
|
|
||||||
"Istanbul": "İstanbul",
|
|
||||||
"Izmir": "İzmir",
|
|
||||||
|
|
||||||
// Scandinavian & others
|
|
||||||
"Malmo": "Malmö",
|
|
||||||
"Goteborg": "Göteborg",
|
|
||||||
"Zurich": "Zürich",
|
|
||||||
"Geneve": "Genève"
|
|
||||||
};
|
|
||||||
return mapping[cityName] || cityName;
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatTemp(temp: var): string {
|
|
||||||
return Config.services.useFahrenheit ? `${temp !== undefined ? Math.round(toFahrenheit(temp)) : "--"}°F` : `${temp !== undefined ? Math.round(temp) : "--"}°C`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getWeatherCondition(code: string): string {
|
function getWeatherCondition(code: string): string {
|
||||||
const conditions = {
|
const conditions = {
|
||||||
"0": "Clear",
|
"0": "Clear",
|
||||||
@@ -223,9 +154,9 @@ Singleton {
|
|||||||
if (!loc || loc.indexOf(",") === -1)
|
if (!loc || loc.indexOf(",") === -1)
|
||||||
return "";
|
return "";
|
||||||
|
|
||||||
const [lat, lon] = loc.split(",").map(s => s.trim());
|
const [lat, lon] = loc.split(",");
|
||||||
const baseUrl = "https://api.open-meteo.com/v1/forecast";
|
const baseUrl = "https://api.open-meteo.com/v1/forecast";
|
||||||
const params = ["latitude=" + lat, "longitude=" + lon, "hourly=weather_code,temperature_2m,precipitation_probability", "daily=weather_code,temperature_2m_max,temperature_2m_min,sunrise,sunset", "current=temperature_2m,relative_humidity_2m,apparent_temperature,is_day,weather_code,wind_speed_10m", "timezone=auto", "forecast_days=7"];
|
const params = ["latitude=" + lat, "longitude=" + lon, "hourly=weather_code,temperature_2m", "daily=weather_code,temperature_2m_max,temperature_2m_min,sunrise,sunset", "current=temperature_2m,relative_humidity_2m,apparent_temperature,is_day,weather_code,wind_speed_10m", "timezone=auto", "forecast_days=7"];
|
||||||
|
|
||||||
return baseUrl + "?" + params.join("&");
|
return baseUrl + "?" + params.join("&");
|
||||||
}
|
}
|
||||||
@@ -258,14 +189,7 @@ Singleton {
|
|||||||
|
|
||||||
onLocChanged: fetchWeatherData()
|
onLocChanged: fetchWeatherData()
|
||||||
|
|
||||||
Connections {
|
// Refresh current location hourly
|
||||||
function onWeatherLocationChanged(): void {
|
|
||||||
root.reload();
|
|
||||||
}
|
|
||||||
|
|
||||||
target: Config.services
|
|
||||||
}
|
|
||||||
|
|
||||||
Timer {
|
Timer {
|
||||||
interval: 3600000 // 1 hour
|
interval: 3600000 // 1 hour
|
||||||
repeat: true
|
repeat: true
|
||||||
@@ -276,5 +200,6 @@ Singleton {
|
|||||||
|
|
||||||
ElapsedTimer {
|
ElapsedTimer {
|
||||||
id: timer
|
id: timer
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+41
-31
@@ -4,66 +4,76 @@ import qs.Components
|
|||||||
import qs.Helpers
|
import qs.Helpers
|
||||||
import qs.Config
|
import qs.Config
|
||||||
|
|
||||||
CustomClippingRect {
|
RowLayout {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
required property var lock
|
required property var lock
|
||||||
|
|
||||||
implicitHeight: layout.implicitHeight
|
spacing: Appearance.spacing.large * 2
|
||||||
implicitWidth: layout.implicitWidth
|
|
||||||
radius: Appearance.rounding.large
|
|
||||||
|
|
||||||
RowLayout {
|
ColumnLayout {
|
||||||
id: layout
|
Layout.fillWidth: true
|
||||||
|
spacing: Appearance.spacing.normal
|
||||||
|
|
||||||
anchors.fill: parent
|
CustomRect {
|
||||||
spacing: Appearance.spacing.large * 2
|
|
||||||
|
|
||||||
ColumnLayout {
|
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
spacing: Appearance.spacing.normal
|
color: DynamicColors.tPalette.m3surfaceContainer
|
||||||
|
implicitHeight: weather.implicitHeight
|
||||||
|
radius: Appearance.rounding.small
|
||||||
|
topLeftRadius: Appearance.rounding.large
|
||||||
|
|
||||||
WeatherInfo {
|
WeatherInfo {
|
||||||
id: weather
|
id: weather
|
||||||
|
|
||||||
Layout.fillWidth: true
|
|
||||||
rootHeight: root.height
|
rootHeight: root.height
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
CustomRect {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
color: DynamicColors.tPalette.m3surfaceContainer
|
||||||
|
implicitHeight: resources.implicitHeight
|
||||||
|
radius: Appearance.rounding.small
|
||||||
|
|
||||||
Resources {
|
Resources {
|
||||||
id: resources
|
id: resources
|
||||||
|
|
||||||
Layout.fillWidth: true
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
CustomClippingRect {
|
||||||
|
Layout.fillHeight: true
|
||||||
|
Layout.fillWidth: true
|
||||||
|
bottomLeftRadius: Appearance.rounding.large
|
||||||
|
color: DynamicColors.tPalette.m3surfaceContainer
|
||||||
|
radius: Appearance.rounding.small
|
||||||
|
|
||||||
Media {
|
Media {
|
||||||
id: media
|
id: media
|
||||||
|
|
||||||
Layout.fillHeight: true
|
|
||||||
Layout.fillWidth: true
|
|
||||||
lock: root.lock
|
lock: root.lock
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Center {
|
Center {
|
||||||
lock: root.lock
|
lock: root.lock
|
||||||
}
|
}
|
||||||
|
|
||||||
ColumnLayout {
|
ColumnLayout {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
spacing: Appearance.spacing.normal
|
||||||
|
|
||||||
|
CustomRect {
|
||||||
|
Layout.fillHeight: true
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
spacing: Appearance.spacing.normal
|
bottomRightRadius: Appearance.rounding.large
|
||||||
|
color: DynamicColors.tPalette.m3surfaceContainer
|
||||||
|
radius: Appearance.rounding.small
|
||||||
|
topRightRadius: Appearance.rounding.large
|
||||||
|
|
||||||
CustomRect {
|
NotifDock {
|
||||||
Layout.fillHeight: true
|
lock: root.lock
|
||||||
Layout.fillWidth: true
|
|
||||||
bottomRightRadius: Appearance.rounding.large
|
|
||||||
color: DynamicColors.tPalette.m3surfaceContainer
|
|
||||||
radius: Appearance.rounding.small
|
|
||||||
topRightRadius: Appearance.rounding.large
|
|
||||||
|
|
||||||
NotifDock {
|
|
||||||
lock: root.lock
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+145
-54
@@ -1,110 +1,201 @@
|
|||||||
|
pragma ComponentBehavior: Bound
|
||||||
|
|
||||||
import QtQuick
|
import QtQuick
|
||||||
import QtQuick.Layouts
|
import QtQuick.Layouts
|
||||||
import Quickshell
|
import qs.Modules
|
||||||
import ZShell.Components
|
|
||||||
import qs.Components
|
import qs.Components
|
||||||
import qs.Config
|
|
||||||
import qs.Helpers
|
import qs.Helpers
|
||||||
|
import qs.Config
|
||||||
|
|
||||||
CustomClippingRect {
|
Item {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
required property var lock
|
required property var lock
|
||||||
|
|
||||||
color: DynamicColors.tPalette.m3surfaceContainer
|
anchors.fill: parent
|
||||||
implicitHeight: layout.implicitHeight + layout.anchors.margins * 2
|
|
||||||
radius: Appearance.rounding.small
|
|
||||||
|
|
||||||
FadeImage {
|
|
||||||
id: image
|
|
||||||
|
|
||||||
|
Image {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
asynchronous: true
|
asynchronous: true
|
||||||
fillMode: Image.PreserveAspectCrop
|
fillMode: Image.PreserveAspectCrop
|
||||||
layer.enabled: true
|
layer.enabled: true
|
||||||
opacity: status === Image.Ready ? 1 : 0
|
opacity: status === Image.Ready ? 1 : 0
|
||||||
source: Players.getArtUrl(Players.active)
|
source: Players.active?.trackArtUrl ?? ""
|
||||||
sourceSize: {
|
sourceSize.height: height
|
||||||
const dpr = (QsWindow.window as QsWindow)?.devicePixelRatio ?? 1;
|
sourceSize.width: width
|
||||||
return Qt.size(width * dpr, height * dpr);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
layer.effect: OpacityMask {
|
||||||
|
maskSource: mask
|
||||||
|
}
|
||||||
Behavior on opacity {
|
Behavior on opacity {
|
||||||
Anim {
|
Anim {
|
||||||
type: Anim.StandardExtraLarge
|
duration: Appearance.anim.durations.extraLarge
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
CustomRect {
|
Rectangle {
|
||||||
anchors.fill: parent
|
id: mask
|
||||||
color: DynamicColors.palette.m3surface
|
|
||||||
opacity: 0.7
|
anchors.fill: parent
|
||||||
|
layer.enabled: true
|
||||||
|
visible: false
|
||||||
|
|
||||||
|
gradient: Gradient {
|
||||||
|
orientation: Gradient.Horizontal
|
||||||
|
|
||||||
|
GradientStop {
|
||||||
|
color: Qt.rgba(0, 0, 0, 0.5)
|
||||||
|
position: 0
|
||||||
|
}
|
||||||
|
|
||||||
|
GradientStop {
|
||||||
|
color: Qt.rgba(0, 0, 0, 0.2)
|
||||||
|
position: 0.4
|
||||||
|
}
|
||||||
|
|
||||||
|
GradientStop {
|
||||||
|
color: Qt.rgba(0, 0, 0, 0)
|
||||||
|
position: 0.8
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ColumnLayout {
|
ColumnLayout {
|
||||||
id: layout
|
id: layout
|
||||||
|
|
||||||
anchors.left: parent.left
|
anchors.fill: parent
|
||||||
anchors.margins: Appearance.padding.extraLarge
|
anchors.margins: Appearance.padding.large
|
||||||
anchors.right: parent.right
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
CustomText {
|
||||||
spacing: Appearance.spacing.extraSmall
|
Layout.bottomMargin: Appearance.spacing.larger
|
||||||
|
Layout.topMargin: Appearance.padding.large
|
||||||
|
color: DynamicColors.palette.m3onSurfaceVariant
|
||||||
|
font.family: Appearance.font.family.mono
|
||||||
|
font.weight: 500
|
||||||
|
text: qsTr("Now playing")
|
||||||
|
}
|
||||||
|
|
||||||
CustomText {
|
CustomText {
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
animate: true
|
animate: true
|
||||||
color: DynamicColors.palette.m3primary
|
color: DynamicColors.palette.m3primary
|
||||||
elide: Text.ElideRight
|
elide: Text.ElideRight
|
||||||
font.pointSize: Appearance.font.size.medium
|
font.family: Appearance.font.family.mono
|
||||||
|
font.pointSize: Appearance.font.size.large
|
||||||
|
font.weight: 600
|
||||||
horizontalAlignment: Text.AlignHCenter
|
horizontalAlignment: Text.AlignHCenter
|
||||||
text: (Players.active?.trackTitle ?? qsTr("Nothing playing")) || qsTr("Unknown track")
|
text: Players.active?.trackArtist ?? qsTr("No media")
|
||||||
}
|
}
|
||||||
|
|
||||||
CustomText {
|
CustomText {
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
animate: true
|
animate: true
|
||||||
color: DynamicColors.palette.m3onSurfaceVariant
|
|
||||||
elide: Text.ElideRight
|
elide: Text.ElideRight
|
||||||
font.pointSize: Appearance.font.size.small
|
font.family: Appearance.font.family.mono
|
||||||
|
font.pointSize: Appearance.font.size.larger
|
||||||
horizontalAlignment: Text.AlignHCenter
|
horizontalAlignment: Text.AlignHCenter
|
||||||
text: (Players.active?.trackArtist ?? qsTr("Try playing some music!")) || qsTr("Unknown artist")
|
text: Players.active?.trackTitle ?? qsTr("No media")
|
||||||
}
|
}
|
||||||
|
|
||||||
ButtonRow {
|
RowLayout {
|
||||||
Layout.alignment: Qt.AlignHCenter
|
Layout.alignment: Qt.AlignHCenter
|
||||||
Layout.topMargin: Appearance.spacing.small
|
Layout.bottomMargin: Appearance.padding.large
|
||||||
spacing: Appearance.spacing.extraSmall
|
Layout.topMargin: Appearance.spacing.large * 1.2
|
||||||
|
spacing: Appearance.spacing.large
|
||||||
|
|
||||||
|
PlayerControl {
|
||||||
|
function onClicked(): void {
|
||||||
|
if (Players.active?.canGoPrevious)
|
||||||
|
Players.active.previous();
|
||||||
|
}
|
||||||
|
|
||||||
IconButton {
|
|
||||||
enabled: Players.active?.canGoPrevious
|
|
||||||
icon: "skip_previous"
|
icon: "skip_previous"
|
||||||
isRound: true
|
|
||||||
shapeMorph: true
|
|
||||||
type: IconButton.Tonal
|
|
||||||
|
|
||||||
onClicked: Players.active?.previous()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
IconButton {
|
PlayerControl {
|
||||||
checked: Players.active?.isPlaying ?? false
|
function onClicked(): void {
|
||||||
enabled: Players.active?.canTogglePlaying
|
if (Players.active?.canTogglePlaying)
|
||||||
icon: Players.active?.isPlaying ? "pause" : "play_arrow"
|
Players.active.togglePlaying();
|
||||||
implicitWidth: implicitHeight + Appearance.padding.largeIncreased * 2
|
}
|
||||||
isRound: true
|
|
||||||
shapeMorph: true
|
|
||||||
|
|
||||||
onClicked: Players.active?.togglePlaying()
|
active: Players.active?.isPlaying ?? false
|
||||||
|
animate: true
|
||||||
|
icon: active ? "pause" : "play_arrow"
|
||||||
|
level: active ? 2 : 1
|
||||||
|
set_color: "Primary"
|
||||||
}
|
}
|
||||||
|
|
||||||
IconButton {
|
PlayerControl {
|
||||||
enabled: Players.active?.canGoNext
|
function onClicked(): void {
|
||||||
|
if (Players.active?.canGoNext)
|
||||||
|
Players.active.next();
|
||||||
|
}
|
||||||
|
|
||||||
icon: "skip_next"
|
icon: "skip_next"
|
||||||
isRound: true
|
}
|
||||||
shapeMorph: true
|
}
|
||||||
type: IconButton.Tonal
|
}
|
||||||
|
|
||||||
onClicked: Players.active?.next()
|
component PlayerControl: CustomRect {
|
||||||
|
id: control
|
||||||
|
|
||||||
|
property bool active
|
||||||
|
property alias animate: controlIcon.animate
|
||||||
|
property alias icon: controlIcon.text
|
||||||
|
property int level: 1
|
||||||
|
property string set_color: "Secondary"
|
||||||
|
|
||||||
|
function onClicked(): void {
|
||||||
|
}
|
||||||
|
|
||||||
|
Layout.preferredWidth: implicitWidth + (controlState.pressed ? Appearance.padding.normal * 2 : active ? Appearance.padding.small * 2 : 0)
|
||||||
|
color: active ? DynamicColors.palette[`m3${set_color.toLowerCase()}`] : DynamicColors.palette[`m3${set_color.toLowerCase()}Container`]
|
||||||
|
implicitHeight: controlIcon.implicitHeight + Appearance.padding.normal * 2
|
||||||
|
implicitWidth: controlIcon.implicitWidth + Appearance.padding.large * 2
|
||||||
|
radius: active || controlState.pressed ? Appearance.rounding.small : Appearance.rounding.normal
|
||||||
|
|
||||||
|
Behavior on Layout.preferredWidth {
|
||||||
|
Anim {
|
||||||
|
duration: Appearance.anim.durations.expressiveFastSpatial
|
||||||
|
easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Behavior on radius {
|
||||||
|
Anim {
|
||||||
|
duration: Appearance.anim.durations.expressiveFastSpatial
|
||||||
|
easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Elevation {
|
||||||
|
anchors.fill: parent
|
||||||
|
level: controlState.containsMouse && !controlState.pressed ? control.level + 1 : control.level
|
||||||
|
radius: parent.radius
|
||||||
|
z: -1
|
||||||
|
}
|
||||||
|
|
||||||
|
StateLayer {
|
||||||
|
id: controlState
|
||||||
|
|
||||||
|
color: control.active ? DynamicColors.palette[`m3on${control.set_color}`] : DynamicColors.palette[`m3on${control.set_color}Container`]
|
||||||
|
|
||||||
|
onClicked: {
|
||||||
|
control.onClicked();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MaterialIcon {
|
||||||
|
id: controlIcon
|
||||||
|
|
||||||
|
anchors.centerIn: parent
|
||||||
|
color: control.active ? DynamicColors.palette[`m3on${control.set_color}`] : DynamicColors.palette[`m3on${control.set_color}Container`]
|
||||||
|
fill: control.active ? 1 : 0
|
||||||
|
font.pointSize: Appearance.font.size.large
|
||||||
|
|
||||||
|
Behavior on fill {
|
||||||
|
Anim {
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+47
-48
@@ -1,82 +1,81 @@
|
|||||||
pragma ComponentBehavior: Bound
|
|
||||||
|
|
||||||
import QtQuick
|
import QtQuick
|
||||||
import QtQuick.Layouts
|
import QtQuick.Layouts
|
||||||
import M3Shapes
|
|
||||||
import ZShell.Services
|
import ZShell.Services
|
||||||
import qs.Components
|
import qs.Components
|
||||||
|
import qs.Helpers
|
||||||
import qs.Config
|
import qs.Config
|
||||||
import qs.Effects
|
|
||||||
|
|
||||||
CustomRect {
|
GridLayout {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
readonly property real fontScale: {
|
anchors.left: parent.left
|
||||||
const diff = width / 391 - 1; // 391 is the width at 1080 height screen
|
anchors.margins: Appearance.padding.large
|
||||||
return 1 + Math.pow(Math.abs(diff), 0.8) * Math.sign(diff);
|
anchors.right: parent.right
|
||||||
}
|
columnSpacing: Appearance.spacing.large
|
||||||
|
columns: 2
|
||||||
color: DynamicColors.tPalette.m3surfaceContainer
|
rowSpacing: Appearance.spacing.large
|
||||||
implicitHeight: layout.implicitHeight + layout.anchors.margins * 2
|
rows: 1
|
||||||
radius: Appearance.rounding.small
|
|
||||||
|
|
||||||
ServiceRef {
|
|
||||||
service: Cpu
|
|
||||||
}
|
|
||||||
|
|
||||||
ServiceRef {
|
ServiceRef {
|
||||||
service: Memory
|
service: Memory
|
||||||
}
|
}
|
||||||
|
|
||||||
ServiceRef {
|
ServiceRef {
|
||||||
service: Storage
|
service: Cpu
|
||||||
}
|
}
|
||||||
|
|
||||||
RowLayout {
|
Resource {
|
||||||
id: layout
|
Layout.bottomMargin: Appearance.padding.large
|
||||||
|
Layout.topMargin: Appearance.padding.large
|
||||||
anchors.fill: parent
|
fgColor: DynamicColors.palette.m3primary
|
||||||
anchors.margins: Appearance.padding.large
|
icon: "memory"
|
||||||
spacing: Appearance.spacing.large
|
value: Cpu.percentage
|
||||||
|
|
||||||
Resource {
|
|
||||||
id: cpu
|
|
||||||
|
|
||||||
fgColor: DynamicColors.palette.m3primary
|
|
||||||
icon: "memory"
|
|
||||||
value: Cpu.percentage
|
|
||||||
}
|
|
||||||
|
|
||||||
Resource {
|
|
||||||
fgColor: DynamicColors.palette.m3tertiary
|
|
||||||
icon: "memory_alt"
|
|
||||||
value: Memory.percentage
|
|
||||||
}
|
|
||||||
|
|
||||||
Resource {
|
|
||||||
fgColor: DynamicColors.palette.m3secondary
|
|
||||||
icon: "hard_disk"
|
|
||||||
value: Storage.percentage
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
component Resource: CircularProgress {
|
Resource {
|
||||||
|
Layout.bottomMargin: Appearance.padding.large
|
||||||
|
Layout.topMargin: Appearance.padding.large
|
||||||
|
fgColor: DynamicColors.palette.m3secondary
|
||||||
|
icon: "memory_alt"
|
||||||
|
value: Memory.percentage
|
||||||
|
}
|
||||||
|
|
||||||
|
component Resource: CustomRect {
|
||||||
id: res
|
id: res
|
||||||
|
|
||||||
|
required property color fgColor
|
||||||
required property string icon
|
required property string icon
|
||||||
|
required property real value
|
||||||
|
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
implicitSize: width
|
color: DynamicColors.layer(DynamicColors.palette.m3surfaceContainerHigh, 2)
|
||||||
|
implicitHeight: width
|
||||||
|
radius: Appearance.rounding.large
|
||||||
|
|
||||||
Behavior on clampedVal {
|
Behavior on value {
|
||||||
Anim {
|
Anim {
|
||||||
|
duration: Appearance.anim.durations.large
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
CircularProgress {
|
||||||
|
id: circ
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
bgColor: DynamicColors.layer(DynamicColors.palette.m3surfaceContainerHighest, 3)
|
||||||
|
fgColor: res.fgColor
|
||||||
|
padding: Appearance.padding.large * 3
|
||||||
|
strokeWidth: width < 200 ? Appearance.padding.smaller : Appearance.padding.normal
|
||||||
|
value: res.value
|
||||||
|
}
|
||||||
|
|
||||||
MaterialIcon {
|
MaterialIcon {
|
||||||
|
id: icon
|
||||||
|
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
color: res.fgColor
|
color: res.fgColor
|
||||||
font.pointSize: Appearance.font.size.extraLarge
|
font.pointSize: (circ.arcRadius * 0.7) || 1
|
||||||
|
font.weight: 600
|
||||||
text: res.icon
|
text: res.icon
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,62 +0,0 @@
|
|||||||
import QtQuick
|
|
||||||
import QtQuick.Layouts
|
|
||||||
import qs.Components
|
|
||||||
import qs.Config
|
|
||||||
import qs.Helpers
|
|
||||||
|
|
||||||
ColumnLayout {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
required property int rootHeight
|
|
||||||
|
|
||||||
spacing: Appearance.spacing.extraSmall
|
|
||||||
|
|
||||||
CustomText {
|
|
||||||
Layout.alignment: Qt.AlignHCenter
|
|
||||||
animate: true
|
|
||||||
color: DynamicColors.palette.m3onSurfaceVariant
|
|
||||||
font.pointSize: Appearance.font.size.large
|
|
||||||
text: Weather.description
|
|
||||||
}
|
|
||||||
|
|
||||||
RowLayout {
|
|
||||||
Layout.alignment: Qt.AlignHCenter
|
|
||||||
spacing: Appearance.spacing.small
|
|
||||||
|
|
||||||
CustomText {
|
|
||||||
id: temp
|
|
||||||
|
|
||||||
animate: true
|
|
||||||
color: DynamicColors.palette.m3primary
|
|
||||||
font.pointSize: Appearance.font.size.large
|
|
||||||
text: Weather.temp
|
|
||||||
}
|
|
||||||
|
|
||||||
MaterialIcon {
|
|
||||||
animate: true
|
|
||||||
color: DynamicColors.palette.m3secondary
|
|
||||||
text: Weather.icon
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
CustomText {
|
|
||||||
Layout.alignment: Qt.AlignHCenter
|
|
||||||
animate: true
|
|
||||||
color: DynamicColors.palette.m3onSurfaceVariant
|
|
||||||
font.pointSize: Appearance.font.size.large
|
|
||||||
text: qsTr("Feels like %1").arg(Weather.temp)
|
|
||||||
visible: root.rootHeight > 550
|
|
||||||
}
|
|
||||||
|
|
||||||
CustomText {
|
|
||||||
Layout.alignment: Qt.AlignHCenter
|
|
||||||
animate: true
|
|
||||||
color: DynamicColors.palette.m3onSurfaceVariant
|
|
||||||
font.pointSize: Appearance.font.size.medium
|
|
||||||
text: {
|
|
||||||
const today = Weather.forecast[0];
|
|
||||||
return qsTr("High %1 • Low %2").arg(Weather.formatTemp(today?.maxTempC)).arg(Weather.formatTemp(today?.minTempC));
|
|
||||||
}
|
|
||||||
visible: root.rootHeight > 550
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,103 +0,0 @@
|
|||||||
import QtQuick
|
|
||||||
import QtQuick.Layouts
|
|
||||||
import M3Shapes
|
|
||||||
import ZShell
|
|
||||||
import qs.Components
|
|
||||||
import qs.Helpers
|
|
||||||
import qs.Config
|
|
||||||
|
|
||||||
CustomRect {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
color: DynamicColors.layer(DynamicColors.palette.m3surfaceContainerHigh, 2)
|
|
||||||
implicitHeight: header.anchors.margins + header.implicitHeight + Appearance.spacing.small + layout.implicitHeight + layout.anchors.bottomMargin
|
|
||||||
radius: Appearance.rounding.small
|
|
||||||
|
|
||||||
RowLayout {
|
|
||||||
id: header
|
|
||||||
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.margins: Appearance.padding.largeIncreased
|
|
||||||
anchors.top: parent.top
|
|
||||||
spacing: Appearance.spacing.small
|
|
||||||
|
|
||||||
MaterialIcon {
|
|
||||||
Layout.topMargin: Math.round(fontInfo.pointSize * 0.12)
|
|
||||||
font.pointSize: Appearance.font.size.medium
|
|
||||||
text: "schedule"
|
|
||||||
}
|
|
||||||
|
|
||||||
CustomText {
|
|
||||||
id: title
|
|
||||||
|
|
||||||
font.pointSize: Appearance.font.size.medium
|
|
||||||
text: qsTr("Hourly forecast")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
VerticalFadeListView {
|
|
||||||
id: layout
|
|
||||||
|
|
||||||
anchors.bottom: parent.bottom
|
|
||||||
anchors.bottomMargin: Appearance.padding.largeIncreased
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.margins: Appearance.padding.large
|
|
||||||
anchors.right: parent.right
|
|
||||||
implicitHeight: contentItem.childrenRect.height
|
|
||||||
model: Weather.hourlyForecast
|
|
||||||
orientation: VerticalFadeListView.Horizontal
|
|
||||||
spacing: Appearance.spacing.normal
|
|
||||||
|
|
||||||
delegate: ColumnLayout {
|
|
||||||
id: hour
|
|
||||||
|
|
||||||
readonly property var cond: modelData
|
|
||||||
required property int index
|
|
||||||
required property var modelData
|
|
||||||
|
|
||||||
spacing: Appearance.spacing.extraSmall
|
|
||||||
|
|
||||||
MaterialShape {
|
|
||||||
Layout.alignment: Qt.AlignHCenter
|
|
||||||
color: Qt.alpha(DynamicColors.palette.m3primary, hour.index === 0 ? 1 : 0)
|
|
||||||
implicitSize: temp.implicitHeight + Appearance.padding.normal * 2
|
|
||||||
shape: MaterialShape.Cookie4Sided
|
|
||||||
|
|
||||||
Behavior on color {
|
|
||||||
CAnim {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
CustomText {
|
|
||||||
id: temp
|
|
||||||
|
|
||||||
anchors.centerIn: parent
|
|
||||||
color: hour.index === 0 ? DynamicColors.palette.m3onPrimary : DynamicColors.palette.m3onSurface
|
|
||||||
font.pointSize: Appearance.font.size.medium
|
|
||||||
text: Weather.formatTemp(hour.cond.tempC).slice(0, -1) // Remove C/F
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MaterialIcon {
|
|
||||||
Layout.alignment: Qt.AlignHCenter
|
|
||||||
color: DynamicColors.palette.m3secondary
|
|
||||||
font.pointSize: Appearance.font.size.extraLarge
|
|
||||||
text: hour.cond.icon
|
|
||||||
}
|
|
||||||
|
|
||||||
CustomText {
|
|
||||||
Layout.alignment: Qt.AlignHCenter
|
|
||||||
color: DynamicColors.palette.m3primary
|
|
||||||
text: hour.cond.precipChance + "%"
|
|
||||||
}
|
|
||||||
|
|
||||||
CustomText {
|
|
||||||
Layout.alignment: Qt.AlignHCenter
|
|
||||||
Layout.topMargin: Appearance.spacing.extraSmall
|
|
||||||
color: DynamicColors.palette.m3onSurfaceVariant
|
|
||||||
font.pointSize: Appearance.font.size.small
|
|
||||||
text: hour.index === 0 ? qsTr("Now") : Qt.formatDateTime(new Date(hour.cond.timestamp.replace("T", " ")), Config.services.useTwelveHourClock ? "ha" : "hh:00")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+150
-34
@@ -1,23 +1,162 @@
|
|||||||
|
pragma ComponentBehavior: Bound
|
||||||
|
|
||||||
import QtQuick
|
import QtQuick
|
||||||
import qs.Modules.Lock.Weather
|
import QtQuick.Layouts
|
||||||
import qs.Config
|
|
||||||
import qs.Components
|
import qs.Components
|
||||||
import qs.Helpers
|
import qs.Helpers
|
||||||
|
import qs.Config
|
||||||
|
|
||||||
CustomRect {
|
ColumnLayout {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
required property int rootHeight
|
required property int rootHeight
|
||||||
readonly property bool showForecast: rootHeight >= 700
|
|
||||||
|
|
||||||
color: DynamicColors.tPalette.m3surfaceContainer
|
anchors.left: parent.left
|
||||||
implicitHeight: {
|
anchors.margins: Appearance.padding.large * 2
|
||||||
const base = brief.implicitHeight + brief.anchors.topMargin;
|
anchors.right: parent.right
|
||||||
if (showForecast)
|
spacing: Appearance.spacing.small
|
||||||
return base + Appearance.spacing.large + forecast.implicitHeight + forecast.anchors.margins;
|
|
||||||
return base + brief.anchors.topMargin;
|
Loader {
|
||||||
|
Layout.alignment: Qt.AlignHCenter
|
||||||
|
Layout.bottomMargin: -Appearance.padding.large
|
||||||
|
Layout.topMargin: Appearance.padding.large * 2
|
||||||
|
active: root.rootHeight > 610
|
||||||
|
visible: active
|
||||||
|
|
||||||
|
sourceComponent: CustomText {
|
||||||
|
color: DynamicColors.palette.m3primary
|
||||||
|
font.pointSize: Appearance.font.size.extraLarge
|
||||||
|
font.weight: 500
|
||||||
|
text: qsTr("Weather")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
spacing: Appearance.spacing.large
|
||||||
|
|
||||||
|
MaterialIcon {
|
||||||
|
animate: true
|
||||||
|
color: DynamicColors.palette.m3secondary
|
||||||
|
font.pointSize: Appearance.font.size.extraLarge * 2.5
|
||||||
|
text: Weather.icon
|
||||||
|
}
|
||||||
|
|
||||||
|
ColumnLayout {
|
||||||
|
spacing: Appearance.spacing.small
|
||||||
|
|
||||||
|
CustomText {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
animate: true
|
||||||
|
color: DynamicColors.palette.m3secondary
|
||||||
|
elide: Text.ElideRight
|
||||||
|
font.pointSize: Appearance.font.size.large
|
||||||
|
font.weight: 500
|
||||||
|
text: Weather.description
|
||||||
|
}
|
||||||
|
|
||||||
|
CustomText {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
animate: true
|
||||||
|
color: DynamicColors.palette.m3onSurfaceVariant
|
||||||
|
elide: Text.ElideRight
|
||||||
|
font.pointSize: Appearance.font.size.normal
|
||||||
|
text: qsTr("Humidity: %1%").arg(Weather.humidity)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Loader {
|
||||||
|
Layout.rightMargin: Appearance.padding.smaller
|
||||||
|
active: root.width > 400
|
||||||
|
visible: active
|
||||||
|
|
||||||
|
sourceComponent: ColumnLayout {
|
||||||
|
spacing: Appearance.spacing.small
|
||||||
|
|
||||||
|
CustomText {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
animate: true
|
||||||
|
color: DynamicColors.palette.m3primary
|
||||||
|
elide: Text.ElideLeft
|
||||||
|
font.pointSize: Appearance.font.size.extraLarge
|
||||||
|
font.weight: 500
|
||||||
|
horizontalAlignment: Text.AlignRight
|
||||||
|
text: Weather.temp
|
||||||
|
}
|
||||||
|
|
||||||
|
CustomText {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
animate: true
|
||||||
|
color: DynamicColors.palette.m3outline
|
||||||
|
elide: Text.ElideLeft
|
||||||
|
font.pointSize: Appearance.font.size.smaller
|
||||||
|
horizontalAlignment: Text.AlignRight
|
||||||
|
text: qsTr("Feels like: %1").arg(Weather.feelsLike)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Loader {
|
||||||
|
id: forecastLoader
|
||||||
|
|
||||||
|
Layout.bottomMargin: Appearance.padding.large * 2
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.topMargin: Appearance.spacing.smaller
|
||||||
|
active: root.rootHeight > 820
|
||||||
|
visible: active
|
||||||
|
|
||||||
|
sourceComponent: RowLayout {
|
||||||
|
spacing: Appearance.spacing.large
|
||||||
|
|
||||||
|
Repeater {
|
||||||
|
model: {
|
||||||
|
const forecast = Weather.hourlyForecast;
|
||||||
|
const count = root.width < 320 ? 3 : root.width < 400 ? 4 : 5;
|
||||||
|
if (!forecast)
|
||||||
|
return Array.from({
|
||||||
|
length: count
|
||||||
|
}, () => null);
|
||||||
|
|
||||||
|
return forecast.slice(0, count);
|
||||||
|
}
|
||||||
|
|
||||||
|
ColumnLayout {
|
||||||
|
id: forecastHour
|
||||||
|
|
||||||
|
required property var modelData
|
||||||
|
|
||||||
|
Layout.fillWidth: true
|
||||||
|
spacing: Appearance.spacing.small
|
||||||
|
|
||||||
|
CustomText {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
color: DynamicColors.palette.m3outline
|
||||||
|
font.pointSize: Appearance.font.size.larger
|
||||||
|
horizontalAlignment: Text.AlignHCenter
|
||||||
|
text: {
|
||||||
|
const hour = forecastHour.modelData?.hour ?? 0;
|
||||||
|
return hour > 12 ? `${(hour - 12).toString().padStart(2, "0")} PM` : `${hour.toString().padStart(2, "0")} AM`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MaterialIcon {
|
||||||
|
Layout.alignment: Qt.AlignHCenter
|
||||||
|
font.pointSize: Appearance.font.size.extraLarge * 1.5
|
||||||
|
font.weight: 500
|
||||||
|
text: forecastHour.modelData?.icon ?? "cloud_alert"
|
||||||
|
}
|
||||||
|
|
||||||
|
CustomText {
|
||||||
|
Layout.alignment: Qt.AlignHCenter
|
||||||
|
color: DynamicColors.palette.m3secondary
|
||||||
|
font.pointSize: Appearance.font.size.larger
|
||||||
|
text: Config.services.useFahrenheit ? `${forecastHour.modelData?.tempF ?? 0}°F` : `${forecastHour.modelData?.tempC ?? 0}°C`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
radius: Appearance.rounding.small
|
|
||||||
|
|
||||||
Timer {
|
Timer {
|
||||||
interval: 900000 // 15 minutes
|
interval: 900000 // 15 minutes
|
||||||
@@ -27,27 +166,4 @@ CustomRect {
|
|||||||
|
|
||||||
onTriggered: Weather.reload()
|
onTriggered: Weather.reload()
|
||||||
}
|
}
|
||||||
|
|
||||||
BriefInfo {
|
|
||||||
id: brief
|
|
||||||
|
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
|
||||||
anchors.top: parent.top
|
|
||||||
anchors.topMargin: Appearance.padding.extraLarge
|
|
||||||
rootHeight: root.rootHeight
|
|
||||||
}
|
|
||||||
|
|
||||||
Loader {
|
|
||||||
id: forecast
|
|
||||||
|
|
||||||
active: root.showForecast
|
|
||||||
anchors.bottom: parent.bottom
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.margins: Appearance.padding.large
|
|
||||||
anchors.right: parent.right
|
|
||||||
asynchronous: true
|
|
||||||
|
|
||||||
sourceComponent: Forecast {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,31 +32,6 @@ Item {
|
|||||||
implicitHeight: width
|
implicitHeight: width
|
||||||
radius: Appearance.rounding.large
|
radius: Appearance.rounding.large
|
||||||
|
|
||||||
Loader {
|
|
||||||
active: opacity > 0
|
|
||||||
anchors.centerIn: parent
|
|
||||||
opacity: img.status === Image.Ready ? 0 : 1
|
|
||||||
|
|
||||||
Behavior on opacity {
|
|
||||||
Anim {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
sourceComponent: CustomRect {
|
|
||||||
color: DynamicColors.palette.m3primaryContainer
|
|
||||||
implicitHeight: loadingIndicator.implicitSize + Appearance.padding.large * 2
|
|
||||||
implicitWidth: loadingIndicator.implicitSize + Appearance.padding.large * 2
|
|
||||||
radius: Appearance.rounding.full
|
|
||||||
|
|
||||||
LoadingIndicator {
|
|
||||||
id: loadingIndicator
|
|
||||||
|
|
||||||
anchors.centerIn: parent
|
|
||||||
containsIcon: true
|
|
||||||
implicitSize: Math.min(imgWrapper.width, imgWrapper.height) * 0.3
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Image {
|
Image {
|
||||||
id: img
|
id: img
|
||||||
|
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ PageBase {
|
|||||||
Layout.topMargin: Appearance.spacing.extraSmall / 2 - parent.spacing
|
Layout.topMargin: Appearance.spacing.extraSmall / 2 - parent.spacing
|
||||||
active: root.clockFormats.find(item => item.value === Config.general.dateFormat)
|
active: root.clockFormats.find(item => item.value === Config.general.dateFormat)
|
||||||
first: true
|
first: true
|
||||||
|
last: true
|
||||||
menuItems: root.clockFormats
|
menuItems: root.clockFormats
|
||||||
settingAnchor: "bar-clock-format"
|
settingAnchor: "bar-clock-format"
|
||||||
subtext: qsTr("Change how time is displayed in the widget")
|
subtext: qsTr("Change how time is displayed in the widget")
|
||||||
@@ -57,15 +58,5 @@ PageBase {
|
|||||||
Config.general.dateFormat = item.value;
|
Config.general.dateFormat = item.value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ToggleRow {
|
|
||||||
checked: Config.services.useTwelveHourClock
|
|
||||||
last: true
|
|
||||||
settingAnchor: "bar-clock-twelve-hour"
|
|
||||||
subtext: qsTr("Format timestamps for twelve or twenty-four hours in UI")
|
|
||||||
text: qsTr("Twelve hour clock")
|
|
||||||
|
|
||||||
onToggled: Config.services.useTwelveHourClock = checked
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -54,6 +54,10 @@ select = [
|
|||||||
"RUF",
|
"RUF",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[tool.basedpyright]
|
||||||
|
typeCheckingMode = "basic"
|
||||||
|
include = ["cli/src"]
|
||||||
|
|
||||||
[tool.pytest.ini_options]
|
[tool.pytest.ini_options]
|
||||||
testpaths = ["cli/tests"]
|
testpaths = ["cli/tests"]
|
||||||
pythonpath = ["cli/src"]
|
pythonpath = ["cli/src"]
|
||||||
|
|||||||
Reference in New Issue
Block a user