3 Commits
Author SHA1 Message Date
AramJonghu 4ed95c9475 ci: rework Dockerfile image + buildx dep added
JS/TS / fmt & lint (pull_request) Successful in 16s
Python / fmt & lint (pull_request) Successful in 30s
Rust / build, clippy & fmt (pull_request) Canceled after 5s
Python / test & nuitka build (pull_request) Canceled after 37s
C++ / fmt (pull_request) Failing after 5s
C++ / build & lint (pull_request) Failing after 10s
2026-10-23 03:48:13 +02:00
AramJonghu 068319a4cb refactor(workflows): combined steps where it makes sense 2026-10-23 03:05:40 +02:00
AramJonghu bdb95af7a1 fix(ci): missing dependency wayland-scanner 2026-10-23 03:05:04 +02:00
99 changed files with 1218 additions and 5933 deletions
+1 -1
View File
@@ -18,7 +18,7 @@ jobs:
uses: actions/checkout@v4
- name: Install Docker CLI
run: apk add --no-cache docker-cli
run: apk add --no-cache docker-cli docker-cli-buildx
- name: Login to registry
run: echo "${{ secrets.REGISTRY_TOKEN }}" | docker login git.aramjonghu.dev --username aramjonghu --password-stdin
+1 -19
View File
@@ -20,28 +20,10 @@ jobs:
-exec clang-format -i --style=file {} +
git diff --exit-code && echo "clang-format: passed"
build:
build & lint:
runs-on: alpine
container:
image: git.aramjonghu.dev/aramjonghu/zshell-ci:latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Configure
run: cmake -B build -G Ninja -DENABLE_MODULES=plugin -DCMAKE_BUILD_TYPE=Release
- name: Build
run: ninja -C build
clang-tidy:
runs-on: alpine
container:
image: git.aramjonghu.dev/aramjonghu/zshell-ci:latest
steps:
- name: Checkout
uses: actions/checkout@v4
+1 -15
View File
@@ -4,7 +4,7 @@ on:
pull_request:
jobs:
fmt:
fmt & lint:
runs-on: alpine
container: node:26-alpine
@@ -24,20 +24,6 @@ jobs:
else
echo "No JS/TS files found"
fi
lint:
runs-on: alpine
container: node:26-alpine
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Install tools
run: |
apk add --no-cache \
git
- name: ESLint
run: |
if [ -n "$(find . \( -iname "*.js" -o -iname "*.jsx" -o -iname "*.ts" -o -iname "*.tsx" -o -iname "*.mjs" -o -iname "*.cjs" \) -print -quit)" ]; then
+2 -2
View File
@@ -7,7 +7,7 @@ on:
pull_request:
jobs:
static:
fmt & lint:
runs-on: debian
container: node:26-trixie-slim
@@ -33,7 +33,7 @@ jobs:
. .venv/bin/activate
ruff check .
verify:
test & nuitka build:
if: always()
runs-on: debian
container: node:26-trixie-slim
+21 -86
View File
@@ -3,8 +3,9 @@ name: Rust
on:
pull_request:
# jobs can be separated if we integrate tests
jobs:
build:
build, clippy & fmt:
runs-on: alpine
container: node:26-alpine
env:
@@ -34,7 +35,9 @@ jobs:
apk add --no-cache \
git \
cargo \
rust
rust \
rust-clippy \
rustfmt
- name: Cargo check
run: |
@@ -50,90 +53,6 @@ jobs:
else
echo "No Rust project found"
fi
fmt:
runs-on: alpine
container: node:26-alpine
env:
CARGO_HOME: ${{ github.workspace }}/.cargo
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Cache cargo packages
uses: actions/cache@v4
env:
cache-name: cache-cargo-packages
with:
path: |
.cargo/registry
.cargo/git
target
key: rust-fmt-${{ runner.os }}-${{ env.cache-name }}-${{ hashFiles('**/Cargo.lock') }}
restore-keys: |
rust-${{ runner.os }}-build-${{ env.cache-name }}-
rust-${{ runner.os }}-build-
rust-
- name: Install tools
run: |
apk add --no-cache \
git \
cargo \
rust \
rustfmt
- name: Format check
run: |
if [ -n "$(find . -name "Cargo.toml" -print -quit)" ]; then
status=0
for manifest in $(find . -name "Cargo.toml"); do
cargo fmt --manifest-path "$manifest" --check && \
echo "$manifest: formatting OK" || \
{ echo "$manifest: needs formatting"; status=1; }
done
exit $status
elif [ -n "$(find . -name "*.rs" -print -quit)" ]; then
echo "Rust files found but no Cargo.toml"
exit 1
else
echo "No Rust project found"
fi
clippy:
runs-on: alpine
container: node:26-alpine
env:
CARGO_HOME: ${{ github.workspace }}/.cargo
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Cache cargo packages
uses: actions/cache@v4
env:
cache-name: cache-cargo-packages
with:
path: |
.cargo/registry
.cargo/git
target
key: rust-clippy-${{ runner.os }}-${{ env.cache-name }}-${{ hashFiles('**/Cargo.lock') }}
restore-keys: |
rust-${{ runner.os }}-build-${{ env.cache-name }}-
rust-${{ runner.os }}-build-
rust-
- name: Install tools
run: |
apk add --no-cache \
git \
cargo \
rust \
rust-clippy
- name: Clippy
run: |
if [ -n "$(find . -name "Cargo.toml" -print -quit)" ]; then
@@ -150,3 +69,19 @@ jobs:
else
echo "No Rust project found"
fi
- name: Format check
run: |
if [ -n "$(find . -name "Cargo.toml" -print -quit)" ]; then
status=0
for manifest in $(find . -name "Cargo.toml"); do
cargo fmt --manifest-path "$manifest" --check && \
echo "$manifest: formatting OK" || \
{ echo "$manifest: needs formatting"; status=1; }
done
exit $status
elif [ -n "$(find . -name "*.rs" -print -quit)" ]; then
echo "Rust files found but no Cargo.toml"
exit 1
else
echo "No Rust project found"
fi
+1 -1
View File
@@ -16,7 +16,7 @@ CustomRect {
property color activeOnColor
property bool checked
property real checkedRadius: Tokens.rounding.medium
property real defaultRadius: Math.min(width, height) / 2
property real defaultRadius: Tokens.rounding.large
property color disabledColor: Qt.alpha(Colors.palette.m3onSurface, 0.1)
property color disabledOnColor: Qt.alpha(Colors.palette.m3onSurface, 0.38)
property bool fillWidth
-2
View File
@@ -34,7 +34,6 @@ BusyIndicator {
anchors.fill: parent
bgColor: root.bgColor
fgColor: root.fgColor
hasEndIndicator: false
padding: root.padding
rotation: manager.rotation
startAngle: manager.startFraction * 360
@@ -54,7 +53,6 @@ BusyIndicator {
Anim {
duration: manager.completeEndDuration * Tokens.anim.durations.scale
properties: "opacity,internalStrokeWidth"
type: Anim.DefaultEffects
}
}
-1
View File
@@ -150,7 +150,6 @@ TextFieldBase {
sourceComponent: MaterialIcon {
color: Colors.palette.m3onSurfaceVariant
font.pointSize: Tokens.font.size.larger
text: root.leadingIcon
fontStyle: Tokens.font.icon.builders.medium.scale(0.9).build()
}
+39 -41
View File
@@ -2,57 +2,55 @@ import QtQuick
import ZShell.Config
import qs.Services
Item {
CustomClippingRect {
id: track
required property var scrollBar
required property var mouseArea
ClipRect {
radius: Tokens.rounding.full
anchors.left: parent.left
anchors.right: parent.right
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: parent.bottom
implicitHeight: handle.implicitHeight
radius: Tokens.rounding.full
CustomRect {
id: handle
anchors.bottom: parent.bottom
implicitHeight: handle.implicitHeight
CustomRect {
id: handle
anchors.bottom: parent.bottom
color: Colors.palette.m3secondary
implicitWidth: track.scrollBar.width * track.scrollBar.effectiveSize
implicitHeight: track.mouseArea.pressed || track.mouseArea.containsMouse ? Tokens.padding.extraSmall * 2 : Tokens.padding.extraSmall
opacity: {
if (!track.scrollBar.enabled)
return 0;
if (track.scrollBar.size === 1)
return 0;
if (track.mouseArea.pressed)
return 1;
if (track.mouseArea.containsMouse)
return 0.8;
if (track.scrollBar.policy === CustomScrollBar.AlwaysOn || track.scrollBar.shouldBeActive)
return 0.6;
color: Colors.palette.m3secondary
implicitWidth: track.scrollBar.width * track.scrollBar.effectiveSize
implicitHeight: track.mouseArea.pressed || track.mouseArea.containsMouse ? Tokens.padding.extraSmall * 2 : Tokens.padding.extraSmall
opacity: {
if (!track.scrollBar.enabled)
return 0;
}
radius: Tokens.rounding.full
x: track.scrollBar.reversed ? track.scrollBar.width * (1 + track.scrollBar.nonAnimY) * track.scrollBar.travelScale : track.scrollBar.width * track.scrollBar.nonAnimY * track.scrollBar.travelScale
if (track.scrollBar.size === 1)
return 0;
if (track.mouseArea.pressed)
return 1;
if (track.mouseArea.containsMouse)
return 0.8;
if (track.scrollBar.policy === CustomScrollBar.AlwaysOn || track.scrollBar.shouldBeActive)
return 0.6;
return 0;
}
radius: Tokens.rounding.full
x: track.scrollBar.reversed ? track.scrollBar.width * (1 + track.scrollBar.nonAnimY) * track.scrollBar.travelScale : track.scrollBar.width * track.scrollBar.nonAnimY * track.scrollBar.travelScale
Behavior on implicitHeight {
Anim {}
}
Behavior on opacity {
Anim {
type: Anim.DefaultEffects
}
Behavior on implicitHeight {
Anim {}
}
Behavior on opacity {
Anim {
type: Anim.DefaultEffects
}
}
MouseArea {
acceptedButtons: Qt.NoButton
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
hoverEnabled: true
}
MouseArea {
acceptedButtons: Qt.NoButton
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
hoverEnabled: true
}
}
}
+1 -1
View File
@@ -12,7 +12,7 @@ TextField {
implicitHeight: contentHeight + topPadding + bottomPadding
implicitWidth: contentWidth + leftPadding + rightPadding
placeholderTextColor: Colors.palette.m3onSurfaceVariant // No anim cause placeholder is custom
renderType: TextField.QtRendering
renderType: echoMode === TextField.Password ? TextField.QtRendering : TextField.QtRendering
selectedTextColor: color
selectionColor: Qt.alpha(Colors.palette.m3primary, 0.4)
verticalAlignment: TextInput.AlignVCenter
+1 -1
View File
@@ -30,7 +30,7 @@ CustomFlickable {
type: Anim.SlowEffects
}
}
layer.textureSize: Qt.size(width * dpr, height * dpr)
// layer.textureSize: Qt.size(width * dpr, height * dpr)
layer.effect: Mask {
maskSource: mask
+39 -41
View File
@@ -2,57 +2,55 @@ import QtQuick
import ZShell.Config
import qs.Services
Item {
CustomClippingRect {
id: track
required property var scrollBar
required property var mouseArea
ClipRect {
anchors.bottom: parent.bottom
anchors.bottom: parent.bottom
anchors.right: parent.right
anchors.top: parent.top
implicitWidth: handle.implicitWidth
radius: Tokens.rounding.full
CustomRect {
id: handle
anchors.right: parent.right
anchors.top: parent.top
radius: Tokens.rounding.full
implicitWidth: handle.implicitWidth
CustomRect {
id: handle
anchors.right: parent.right
color: Colors.palette.m3secondary
implicitHeight: track.scrollBar.height * track.scrollBar.effectiveSize
implicitWidth: track.mouseArea.pressed || track.mouseArea.containsMouse ? Tokens.padding.extraSmall * 2 : Tokens.padding.extraSmall
opacity: {
if (!track.scrollBar.enabled)
return 0;
if (track.scrollBar.size === 1)
return 0;
if (track.mouseArea.pressed)
return 1;
if (track.mouseArea.containsMouse)
return 0.8;
if (track.scrollBar.policy === CustomScrollBar.AlwaysOn || track.scrollBar.shouldBeActive)
return 0.6;
color: Colors.palette.m3secondary
implicitHeight: track.scrollBar.height * track.scrollBar.effectiveSize
implicitWidth: track.mouseArea.pressed || track.mouseArea.containsMouse ? Tokens.padding.extraSmall * 2 : Tokens.padding.extraSmall
opacity: {
if (!track.scrollBar.enabled)
return 0;
}
radius: Tokens.rounding.full
y: track.scrollBar.reversed ? track.scrollBar.height * (1 + track.scrollBar.nonAnimY) * track.scrollBar.travelScale : track.scrollBar.height * track.scrollBar.nonAnimY * track.scrollBar.travelScale
if (track.scrollBar.size === 1)
return 0;
if (track.mouseArea.pressed)
return 1;
if (track.mouseArea.containsMouse)
return 0.8;
if (track.scrollBar.policy === CustomScrollBar.AlwaysOn || track.scrollBar.shouldBeActive)
return 0.6;
return 0;
}
radius: Tokens.rounding.full
y: track.scrollBar.reversed ? track.scrollBar.height * (1 + track.scrollBar.nonAnimY) * track.scrollBar.travelScale : track.scrollBar.height * track.scrollBar.nonAnimY * track.scrollBar.travelScale
Behavior on implicitWidth {
Anim {}
}
Behavior on opacity {
Anim {
type: Anim.DefaultEffects
}
Behavior on implicitWidth {
Anim {}
}
Behavior on opacity {
Anim {
type: Anim.DefaultEffects
}
}
MouseArea {
acceptedButtons: Qt.NoButton
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
hoverEnabled: true
}
MouseArea {
acceptedButtons: Qt.NoButton
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
hoverEnabled: true
}
}
}
+2 -1
View File
@@ -59,6 +59,7 @@ Scope {
name: "Bar-Exclusion"
screen: root.screen
mask: Region {}
mask: Region {
}
}
}
+6 -7
View File
@@ -19,11 +19,15 @@ import ZShell.Config
Item {
id: root
required property Item bar
required property real borderThickness
readonly property alias clipboard: clipboard
readonly property alias dashboard: dashboard
readonly property alias dashboardWrapper: dashboardWrapper
readonly property alias dock: dock
readonly property alias drawing: drawing
required property var drawingItem
required property EdgeGeometry geometry
readonly property alias launcher: launcher
readonly property alias notifications: notifications
readonly property alias osd: osd
@@ -32,19 +36,14 @@ Item {
readonly property alias popoutsWrapper: popouts
readonly property alias resources: resources
readonly property alias resourcesWrapper: resourcesWrapper
required property ShellScreen screen
required property ScreenState screenState
readonly property alias settings: settings
readonly property alias settingsWrapper: settingsWrapper
readonly property alias sidebar: sidebar
readonly property alias toasts: toasts
readonly property alias utilities: utilities
required property Item bar
required property real borderThickness
required property var drawingItem
required property EdgeGeometry geometry
required property ShellScreen screen
required property ScreenState screenState
anchors.bottomMargin: geometry.insetBottom(borderThickness)
anchors.fill: parent
anchors.leftMargin: geometry.insetLeft(borderThickness)
-1
View File
@@ -11,7 +11,6 @@ import ZShell.Blobs
import qs.Daemons
import qs.Components
import qs.Modules.Bar
import qs.Modules.Bar.Popouts.Network as N
import ZShell.Config
import qs.Helpers
import qs.Drawers
+28 -12
View File
@@ -27,21 +27,27 @@ ColumnLayout {
CustomText {
Layout.alignment: Qt.AlignVCenter
color: Colors.palette.m3secondary
font: Tokens.font.headline.builders.large.scale(3 * root.centerScale).weight(Font.Bold).width(30).build()
font.bold: true
font.family: Config.appearance.font.family.clock
font.pointSize: Math.floor(Tokens.font.size.extraLarge * 3 * root.centerScale)
text: Time.hourStr
}
CustomText {
Layout.alignment: Qt.AlignVCenter
color: Colors.palette.m3primary
font: Tokens.font.headline.builders.large.scale(3 * root.centerScale).weight(Font.Bold).width(30).build()
font.bold: true
font.family: Config.appearance.font.family.clock
font.pointSize: Math.floor(Tokens.font.size.extraLarge * 3 * root.centerScale)
text: ":"
}
CustomText {
Layout.alignment: Qt.AlignVCenter
color: Colors.palette.m3secondary
font: Tokens.font.headline.builders.large.scale(3 * root.centerScale).weight(Font.Bold).width(30).build()
font.bold: true
font.family: Config.appearance.font.family.clock
font.pointSize: Math.floor(Tokens.font.size.extraLarge * 3 * root.centerScale)
text: Time.minuteStr
}
}
@@ -50,11 +56,13 @@ ColumnLayout {
Layout.alignment: Qt.AlignHCenter
Layout.topMargin: -Tokens.padding.large * 2
color: Colors.palette.m3tertiary
font: Tokens.font.title.builders.medium.weight(Font.DemiBold).build()
font.bold: true
font.family: Config.appearance.font.family.mono
font.pointSize: Math.floor(Tokens.font.size.extraLarge * root.centerScale)
text: Time.format("dddd, d MMMM yyyy")
}
ClipRect {
CustomClippingRect {
Layout.alignment: Qt.AlignHCenter
Layout.topMargin: Tokens.spacing.largeIncreased * 2
color: Colors.tPalette.m3surfaceContainer
@@ -65,7 +73,7 @@ ColumnLayout {
MaterialIcon {
anchors.centerIn: parent
color: Colors.palette.m3onSurfaceVariant
fontStyle: Tokens.font.icon.size(root.centerWidth / 4).build()
font.pointSize: Math.floor(root.centerWidth / 4)
text: "person"
}
@@ -80,7 +88,9 @@ ColumnLayout {
CustomText {
Layout.alignment: Qt.AlignHCenter
color: Colors.palette.m3onSurfaceVariant
font: Tokens.font.body.large
font.family: Config.appearance.font.family.mono
font.pointSize: Tokens.font.size.normal
font.weight: 600
text: root.greeter.username
visible: text.length > 0
}
@@ -145,7 +155,8 @@ ColumnLayout {
}
Behavior on opacity {
Anim {}
Anim {
}
}
}
@@ -180,6 +191,7 @@ ColumnLayout {
anchors.centerIn: parent
color: root.greeter.buffer && !root.greeter.launching ? Colors.palette.m3onPrimary : Colors.palette.m3onSurface
font.weight: 500
text: root.greeter.launching ? "hourglass_top" : "arrow_forward"
}
}
@@ -192,7 +204,8 @@ ColumnLayout {
implicitHeight: Math.max(message.implicitHeight, stateMessage.implicitHeight)
Behavior on implicitHeight {
Anim {}
Anim {
}
}
CustomText {
@@ -232,10 +245,12 @@ ColumnLayout {
wrapMode: Text.WrapAtWordBoundaryOrAnywhere
Behavior on opacity {
Anim {}
Anim {
}
}
Behavior on scale {
Anim {}
Anim {
}
}
onMsgChanged: {
@@ -277,7 +292,8 @@ ColumnLayout {
anchors.left: parent.left
anchors.right: parent.right
color: isError ? Colors.palette.m3error : Colors.palette.m3primary
font: Tokens.font.mono.medium
font.family: Config.appearance.font.family.mono
font.pointSize: Tokens.font.size.small
horizontalAlignment: Qt.AlignHCenter
opacity: 0
scale: 0.7
+1 -1
View File
@@ -20,7 +20,7 @@ BusyIndicator {
property int animState
property color bgColor: Colors.palette.m3secondaryContainer
property color fgColor: Colors.palette.m3primary
property real implicitSize: Tokens.font.body.medium.pointSize * 3
property real implicitSize: Tokens.font.size.normal * 3
property real internalStrokeWidth: strokeWidth
readonly property alias progress: manager.progress
property real strokeWidth: Tokens.padding.extraSmall * 0.8
-41
View File
@@ -1,41 +0,0 @@
import QtQuick
import ZShell.Components
Item {
id: root
property real radius
property real topLeftRadius: radius
property real topRightRadius: radius
property real bottomRightRadius: radius
property real bottomLeftRadius: radius
property color color: "transparent"
default property alias contentData: contentItem.data
Item {
id: contentItem
anchors.fill: parent
}
ShaderEffectSource {
id: source
hideSource: true
sourceItem: contentItem
}
RoundedMask {
anchors.fill: parent
source: source
topLeftRadius: root.topLeftRadius
topRightRadius: root.topRightRadius
bottomRightRadius: root.bottomRightRadius
bottomLeftRadius: root.bottomLeftRadius
color: root.color
}
Behavior on color {
CAnim {}
}
}
-23
View File
@@ -1,36 +1,13 @@
import QtQuick
import qs.Services
Flickable {
id: root
property bool doneFakeFlick
interactive: !ShellState.forActive().isDrawing
maximumFlickVelocity: 3000
rebound: Transition {
onRunningChanged: {
if (!running && !root.doneFakeFlick) {
root.doneFakeFlick = true;
root.flick(1, 1);
root.flick(-1, -1);
Qt.callLater(() => root.cancelFlick());
}
}
Anim {
properties: "x,y"
}
}
Timer {
interval: 10
running: root.doneFakeFlick
onTriggered: {
root.doneFakeFlick = false;
root.returnToBounds();
}
}
}
-23
View File
@@ -1,36 +1,13 @@
import QtQuick
import qs.Services
ListView {
id: root
property bool doneFakeFlick
interactive: !ShellState.forActive()?.isDrawing
maximumFlickVelocity: 3000
rebound: Transition {
// onRunningChanged: {
// if (!running && !root.doneFakeFlick) {
// root.doneFakeFlick = true;
// root.flick(1, 1);
// root.flick(-1, -1);
// Qt.callLater(() => root.cancelFlick());
// }
// }
Anim {
properties: "x,y"
type: Anim.DefaultEffects
}
}
// Timer {
// interval: 10
// running: root.doneFakeFlick
//
// onTriggered: {
// root.doneFakeFlick = false;
// }
// }
}
-1
View File
@@ -4,7 +4,6 @@ MouseArea {
property int scrollAccumulatedY: 0
function onWheel(event: WheelEvent): void {
event.accepted = false;
}
onWheel: event => {
+2 -1
View File
@@ -6,6 +6,7 @@ Rectangle {
color: "transparent"
Behavior on color {
CAnim {}
CAnim {
}
}
}
+7 -9
View File
@@ -14,9 +14,10 @@ Text {
property real animateTo: 1
color: Colors.palette.m3onSurface
font: Tokens.font.body.small
font.family: Config.appearance.font.family.sans
font.pointSize: Tokens.font.size.normal
linkColor: Colors.palette.m3onPrimaryFixedVariant
renderType: Text.QtRendering
renderType: Text.NativeRendering
textFormat: Text.PlainText
Behavior on color {
@@ -27,25 +28,22 @@ Text {
enabled: root.animate
SequentialAnimation {
TAnim {
target: root
Anim {
to: root.animateFrom
type: Anim.FastEffects
}
PropertyAction {
}
TAnim {
target: root
Anim {
to: root.animateTo
type: Anim.DefaultEffects
}
}
}
component TAnim: Anim {
component Anim: NumberAnimation {
duration: root.animateDuration / 2
easing.type: Easing.BezierSpline
properties: root.animateProp.split(",").length > 1 ? root.animateProp : ""
property: root.animateProp.split(",").length === 1 ? root.animateProp : ""
target: root
+52 -281
View File
@@ -1,305 +1,76 @@
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Shapes
import QtQuick.Controls
import ZShell.Config
import qs.Services
TextFieldBase {
TextField {
id: root
enum TextFieldType {
Outlined,
Filled
}
background: null
color: Colors.palette.m3onSurface
cursorVisible: !readOnly
font.family: Config.appearance.font.family.sans
font.pointSize: Tokens.font.size.smaller
placeholderTextColor: Colors.palette.m3outline
renderType: echoMode === TextField.Password ? TextField.QtRendering : TextField.NativeRendering
readonly property int clampedRadius: Math.min(horizontalPadding, Math.min(width, height) / 2, radius)
readonly property string effectiveSupportingText: isError && errorText ? errorText : supportingText
readonly property int filledOffset: type === CustomTextField.Filled ? Tokens.spacing.small : 0
readonly property int leadingOffset: leadingIcon ? leadingIconLoader.width + leadingIconLoader.anchors.leftMargin : 0
readonly property real smallFontScale: smallFontSize / font.pointSize
readonly property int supportingTextOffset: effectiveSupportingText ? supportingTextLoader.height + Tokens.spacing.extraSmall : 0
readonly property int trailingOffset: trailingIcon ? trailingIconLoader.width + trailingIconLoader.anchors.rightMargin : 0
readonly property bool valid: !validate || (!text && emptyIsValid) || (validate instanceof RegExp ? validate.test(text) : !!validate(text))
property int horizontalPadding: Tokens.padding.large
property int verticalPadding: Tokens.padding.large
property int radius: Tokens.rounding.small
property string supportingText
property string trailingIcon
property int smallFontSize: Tokens.font.label.small.pointSize
property int type: CustomTextField.Outlined
property var validate // Regex or function
property bool emptyIsValid: true
property string errorText
property bool isError
property string leadingIcon
bottomPadding: verticalPadding + supportingTextOffset - filledOffset
leftPadding: horizontalPadding + leadingOffset
rightPadding: horizontalPadding + trailingOffset
topPadding: verticalPadding + filledOffset
background: Loader {
anchors.bottomMargin: root.supportingTextOffset
anchors.fill: parent
sourceComponent: root.type === CustomTextField.Filled ? filledComp : outlineComp
StateLayer {
id: stateLayer
cursorShape: Qt.IBeamCursor
enabled: !root.activeFocus
manualPressOverride: tapHandler.pressed
radius: root.type === CustomTextField.Outlined ? root.clampedRadius : 0
topLeftRadius: root.clampedRadius
topRightRadius: root.clampedRadius
onClicked: root.focus = true
Behavior on color {
CAnim {
}
}
cursorDelegate: CustomRect {
id: cursor
onEditingFinished: {
if (!valid)
isError = true;
}
onPressed: {
if (stateLayer.enabled)
stateLayer.press(stateLayer.mouseX, stateLayer.mouseY);
}
onTextEdited: {
if (isError)
isError = false;
}
property bool disableBlink
Item {
id: contentWrapper
color: Colors.palette.m3primary
implicitWidth: 2
radius: Tokens.rounding.medium
anchors.bottomMargin: root.supportingTextOffset
anchors.fill: parent
CustomText {
id: placeholder
anchors.left: parent.left
anchors.leftMargin: root.leftPadding
anchors.topMargin: Tokens.padding.extraSmall
anchors.verticalCenter: parent.verticalCenter
color: root.isError ? Colors.palette.m3error : (root.activeFocus ? Colors.palette.m3primary : root.text ? Colors.palette.m3outline : root.placeholderTextColor)
font.family: root.font.family
font.pointSize: root.font.pointSize
font.variableAxes: root.font.variableAxes
font.weight: root.font.weight
renderType: Text.QtRendering
text: root.placeholderText
states: [
State {
name: "smallOutlined"
when: root.type === CustomTextField.Outlined && (root.activeFocus || root.text)
PropertyChanges {
placeholder.anchors.leftMargin: -(1 - root.smallFontScale) * placeholder.width / 2 + root.horizontalPadding
placeholder.scale: root.smallFontScale
}
AnchorChanges {
anchors.verticalCenter: contentWrapper.top
target: placeholder
}
},
State {
name: "smallFilled"
when: root.type === CustomTextField.Filled && (root.activeFocus || root.text)
PropertyChanges {
placeholder.anchors.leftMargin: -(1 - root.smallFontScale) * placeholder.width / 2 + root.horizontalPadding + root.leadingOffset
placeholder.scale: root.smallFontScale
}
AnchorChanges {
anchors.top: contentWrapper.top
anchors.verticalCenter: undefined
target: placeholder
}
}
]
transitions: Transition {
Anim {
properties: "scale,leftMargin"
type: Anim.DefaultEffects
}
AnchorAnim {
duration: Tokens.anim.durations.expressiveDefaultEffects
easing: Tokens.anim.expressiveDefaultEffects
}
Behavior on opacity {
Anim {
duration: Tokens.anim.durations.small
}
}
Loader {
id: leadingIconLoader
active: root.leadingIcon
anchors.left: parent.left
anchors.leftMargin: Tokens.padding.small
anchors.verticalCenter: parent.verticalCenter
sourceComponent: MaterialIcon {
color: Colors.palette.m3onSurfaceVariant
text: root.leadingIcon
fontStyle: Tokens.font.icon.builders.medium.scale(0.9).build()
}
}
Loader {
id: trailingIconLoader
active: root.trailingIcon
anchors.right: parent.right
anchors.rightMargin: Tokens.padding.small
anchors.verticalCenter: parent.verticalCenter
sourceComponent: MaterialIcon {
color: Colors.palette.m3onSurfaceVariant
text: root.trailingIcon
fontStyle: Tokens.font.icon.builders.medium.scale(0.9).build()
}
}
}
Loader {
id: supportingTextLoader
active: root.effectiveSupportingText
anchors.bottom: parent.bottom
anchors.left: parent.left
anchors.leftMargin: root.horizontalPadding
sourceComponent: CustomText {
color: root.isError ? Colors.palette.m3error : Colors.palette.m3onSurfaceVariant
font: Tokens.font.label.small
text: root.effectiveSupportingText
}
}
TapHandler {
id: tapHandler
}
Component {
id: outlineComp
Shape {
id: bg
asynchronous: true
preferredRendererType: Shape.CurveRenderer
ShapePath {
id: path
readonly property real inset: strokeWidth / 2
readonly property real outlineGap: placeholder.width * root.smallFontScale + Tokens.spacing.small * 2
property real outlineGapScale: root.activeFocus || root.text ? 1 : 0
capStyle: ShapePath.RoundCap
fillColor: "transparent"
startX: path.inset + root.horizontalPadding - root.clampedRadius + path.outlineGap * (1 - path.outlineGapScale) / 2 + path.outlineGap * path.outlineGapScale
startY: path.inset
strokeColor: root.isError ? Colors.palette.m3error : (root.activeFocus ? Colors.palette.m3primary : Colors.palette.m3outline)
strokeWidth: root.activeFocus ? 2 : 1
Behavior on outlineGapScale {
Anim {
type: Anim.DefaultEffects
}
}
Behavior on strokeColor {
CAnim {}
}
Behavior on strokeWidth {
Anim {}
}
PathLine {
x: bg.width - path.inset - root.clampedRadius
y: path.inset
}
PathArc {
radiusX: root.clampedRadius
radiusY: root.clampedRadius
x: bg.width - path.inset
y: path.inset + root.clampedRadius
}
PathLine {
x: bg.width - path.inset
y: bg.height - path.inset - root.clampedRadius
}
PathArc {
radiusX: root.clampedRadius
radiusY: root.clampedRadius
x: bg.width - path.inset - root.clampedRadius
y: bg.height - path.inset
}
PathLine {
x: path.inset + root.clampedRadius
y: bg.height - path.inset
}
PathArc {
radiusX: root.clampedRadius
radiusY: root.clampedRadius
x: path.inset
y: bg.height - path.inset - root.clampedRadius
}
PathLine {
x: path.inset
y: path.inset + root.clampedRadius
}
PathArc {
radiusX: root.clampedRadius
radiusY: root.clampedRadius
x: path.inset + root.clampedRadius
y: path.inset
}
PathLine {
x: path.inset + root.horizontalPadding - root.clampedRadius / 2 + path.outlineGap * (1 - path.outlineGapScale) / 2
y: path.inset
Connections {
function onCursorPositionChanged(): void {
if (root.activeFocus && root.cursorVisible) {
cursor.opacity = 1;
cursor.disableBlink = true;
enableBlink.restart();
}
}
target: root
}
Timer {
id: enableBlink
interval: 100
onTriggered: cursor.disableBlink = false
}
Timer {
interval: 500
repeat: true
running: root.activeFocus && root.cursorVisible && !cursor.disableBlink
triggeredOnStart: true
onTriggered: parent.opacity = parent.opacity === 1 ? 0 : 1
}
Binding {
cursor.opacity: 0
when: !root.activeFocus || !root.cursorVisible
}
}
Component {
id: filledComp
CustomRect {
color: root.activeFocus ? Colors.tPalette.m3surfaceContainerHighest : Colors.tPalette.m3surfaceContainerHigh
topLeftRadius: root.clampedRadius
topRightRadius: root.clampedRadius
CustomRect {
anchors.bottom: parent.bottom
anchors.left: parent.left
anchors.right: parent.right
color: root.isError ? Colors.palette.m3error : (root.activeFocus ? Colors.palette.m3primary : Colors.palette.m3outline)
implicitHeight: root.activeFocus ? 2 : 1
Behavior on implicitHeight {
Anim {}
}
}
Behavior on placeholderTextColor {
CAnim {
}
}
}
+2 -3
View File
@@ -1,16 +1,15 @@
import QtQuick
import QtQuick.Controls
import ZShell.Config
import qs.Services
TextInput {
renderType: Text.NativeRendering
selectedTextColor: Colors.palette.m3onSecondaryContainer
selectionColor: Colors.tPalette.colSecondaryContainer
selectionColor: Colors.tPalette.m3onSecondaryContainer
font {
family: Config.appearance?.font.family.sans ?? "sans-serif"
hintingPreference: Font.PreferFullHinting
pixelSize: Config.appearance?.font.size.normal ?? 15
pixelSize: Tokens?.font.size.normal ?? 15
}
}
-79
View File
@@ -1,79 +0,0 @@
import QtQuick
import Quickshell
import ZShell.Config
Image {
id: root
property int fadeInAnim: Anim.DefaultEffects
property int fadeInLargeAnim: Anim.StandardLarge
property int fadeOutAnim: Anim.FastEffects
property bool fadingOut
property bool hadPrevious
property bool preventInit
function maybeStartInAnim(): void {
if (!preventInit && !opacityInAnim.running && status === Image.Ready) {
opacityInAnim.type = hadPrevious ? fadeInAnim : fadeInLargeAnim;
opacityInAnim.start();
}
}
asynchronous: true
fillMode: Image.PreserveAspectCrop
opacity: 0
retainWhileLoading: true
sourceSize: {
const dpr = (QsWindow.window as QsWindow)?.devicePixelRatio ?? 1;
return Qt.size(width * dpr, height * dpr);
}
Anim on opacity {
id: opacityInAnim
running: false
to: 1
}
Behavior on source {
SequentialAnimation {
ScriptAction {
script: opacityInAnim.stop()
}
PropertyAction {
property: "fadingOut"
target: root
value: true
}
Anim {
property: "opacity"
target: root
to: 0
type: root.fadeOutAnim
}
PropertyAction {
property: "fadingOut"
target: root
value: false
}
PropertyAction {
property: "hadPrevious"
target: root
value: root.source
}
PropertyAction {
}
ScriptAction {
script: root.maybeStartInAnim()
}
}
}
onPreventInitChanged: maybeStartInAnim()
onStatusChanged: maybeStartInAnim()
}
-115
View File
@@ -1,115 +0,0 @@
import QtQuick
import Quickshell
import M3Shapes
import ZShell.Config
import qs.Services
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: Colors.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();
}
}
}
+9 -7
View File
@@ -1,14 +1,16 @@
import QtQuick
import ZShell.Config
import qs.Services
CustomText {
id: root
property real fill: 0
property real fill
property int grade: Colors.light ? 0 : -25
property font fontStyle: Tokens.font.icon.small
renderType: Text.NativeRendering
font: Tokens.font.icon.size(fontStyle.pointSize).weight(fontStyle.weight).vaxes(fontStyle.variableAxes).fill(fill.toFixed(1)).grade(grade).build()
font.family: "Material Symbols Rounded"
font.pointSize: Tokens.font.size.larger
font.variableAxes: ({
FILL: fill.toFixed(1),
GRAD: grade,
opsz: fontInfo.pixelSize,
wght: fontInfo.weight
})
}
+8 -6
View File
@@ -1,6 +1,8 @@
import QtQuick
import QtQuick.Shapes
import ZShell
import ZShell.Components
import qs.Helpers
import ZShell.Config
import qs.Services
@@ -11,6 +13,7 @@ MouseArea {
property alias bottomRightRadius: base.bottomRightRadius
property real circleRadius
property alias color: base.color
property bool disabled
readonly property real endRadius: {
const d1 = distSq(0, 0);
const d2 = distSq(width, 0);
@@ -19,7 +22,6 @@ MouseArea {
return (Math.sqrt(Math.max(d1, d2, d3, d4)) + (shapeMorph ? 24 : 0)) * 1.3;
}
property real endRadiusAtPress
property bool manualHoverOverride
property bool manualPressOverride
property real pressX: width / 2
property real pressY: height / 2
@@ -27,7 +29,7 @@ MouseArea {
readonly property alias rect: base
property bool shapeMorph
property bool showHoverBackground: true
property real stateOpacity: containsMouse || manualHoverOverride ? 0.08 : 0
property real stateOpacity: containsMouse ? 0.08 : 0
property alias topLeftRadius: base.topLeftRadius
property alias topRightRadius: base.topRightRadius
@@ -50,8 +52,8 @@ MouseArea {
}
anchors.fill: parent
cursorShape: !enabled ? undefined : Qt.PointingHandCursor
enabled: parent.enabled
cursorShape: disabled ? undefined : Qt.PointingHandCursor
enabled: !disabled
hoverEnabled: true
Behavior on stateOpacity {
@@ -98,13 +100,13 @@ MouseArea {
id: base
anchors.fill: parent
bottomLeftRadius: root.parent?.bottomLeftRadius ?? radius ?? 0
bottomRightRadius: root.parent?.bottomRightRadius ?? radius ?? 0
color: Colors.palette.m3onSurface
opacity: root.stateOpacity
// Pick up radius from parent if it has one (parent can be anything with radius props)
// qmllint disable missing-property
radius: root.parent?.radius ?? 0
bottomLeftRadius: root.parent?.bottomLeftRadius ?? radius ?? 0
bottomRightRadius: root.parent?.bottomRightRadius ?? radius ?? 0
topLeftRadius: root.parent?.topLeftRadius ?? radius ?? 0
topRightRadius: root.parent?.topRightRadius ?? radius ?? 0
// qmllint enable missing-property
+39 -16
View File
@@ -13,16 +13,34 @@ RowLayout {
spacing: Tokens.spacing.largeIncreased * 2
CustomRect {
Layout.fillHeight: true
ColumnLayout {
Layout.fillWidth: true
topLeftRadius: Tokens.rounding.largeIncreased
bottomLeftRadius: Tokens.rounding.largeIncreased
color: Colors.tPalette.m3surfaceContainer
radius: Tokens.rounding.small
spacing: Tokens.spacing.medium
UserDock {
greeter: root.greeter
CustomRect {
Layout.fillWidth: true
color: Colors.tPalette.m3surfaceContainer
implicitHeight: weather.implicitHeight
radius: Tokens.rounding.small
topLeftRadius: Tokens.rounding.largeIncreased
WeatherInfo {
id: weather
rootHeight: root.height
}
}
CustomClippingRect {
Layout.fillHeight: true
Layout.fillWidth: true
bottomLeftRadius: Tokens.rounding.largeIncreased
color: Colors.tPalette.m3surfaceContainer
radius: Tokens.rounding.small
UserDock {
greeter: root.greeter
}
}
}
@@ -31,16 +49,21 @@ RowLayout {
screenHeight: root.screenHeight
}
CustomRect {
Layout.fillHeight: true
ColumnLayout {
Layout.fillWidth: true
bottomRightRadius: Tokens.rounding.largeIncreased
color: Colors.tPalette.m3surfaceContainer
radius: Tokens.rounding.small
topRightRadius: Tokens.rounding.largeIncreased
spacing: Tokens.spacing.medium
SessionDock {
greeter: root.greeter
CustomRect {
Layout.fillHeight: true
Layout.fillWidth: true
bottomRightRadius: Tokens.rounding.largeIncreased
color: Colors.tPalette.m3surfaceContainer
radius: Tokens.rounding.small
topRightRadius: Tokens.rounding.largeIncreased
SessionDock {
greeter: root.greeter
}
}
}
}
+2 -1
View File
@@ -134,7 +134,8 @@ Scope {
id: lockIcon
anchors.centerIn: parent
fontStyle: Tokens.font.icon.builders.extraLarge.size(Tokens.font.icon.extraLarge.pointSize * 3).build()
font.bold: true
font.pointSize: Tokens.font.size.extraLarge * 4
text: "lock"
}
+15 -8
View File
@@ -39,7 +39,8 @@ Item {
anchors.centerIn: parent
animate: true
color: root.greeter.launching ? Colors.palette.m3secondary : Colors.palette.m3outline
font: Tokens.font.mono.medium
font.family: Config.appearance.font.family.mono
font.pointSize: Tokens.font.size.normal
opacity: root.buffer ? 0 : 1
text: {
if (root.greeter.launching)
@@ -50,7 +51,8 @@ Item {
}
Behavior on opacity {
Anim {}
Anim {
}
}
}
@@ -62,13 +64,15 @@ Item {
anchors.verticalCenter: parent.verticalCenter
color: Colors.palette.m3onSurface
elide: Text.ElideLeft
font: Tokens.font.mono.medium
font.family: Config.appearance.font.family.mono
font.pointSize: Tokens.font.size.normal
horizontalAlignment: Qt.AlignHCenter
opacity: root.greeter.echoResponse && root.buffer ? 1 : 0
text: root.buffer
Behavior on opacity {
Anim {}
Anim {
}
}
}
@@ -85,7 +89,7 @@ Item {
anchors.centerIn: parent
anchors.horizontalCenterOffset: implicitWidth > root.width ? -(implicitWidth - root.width) / 2 : 0
implicitHeight: Tokens.font.body.medium
implicitHeight: Tokens.font.size.normal
implicitWidth: fullWidth
interactive: false
opacity: root.greeter.echoResponse ? 0 : 1
@@ -103,7 +107,8 @@ Item {
scale: 0
Behavior on opacity {
Anim {}
Anim {
}
}
Behavior on scale {
Anim {
@@ -151,13 +156,15 @@ Item {
Behavior on implicitWidth {
id: imWidthBehavior
Anim {}
Anim {
}
}
model: ScriptModel {
values: root.buffer.split("")
}
Behavior on opacity {
Anim {}
Anim {
}
}
}
}
+17 -11
View File
@@ -13,19 +13,22 @@ ColumnLayout {
anchors.fill: parent
anchors.margins: Tokens.padding.large
spacing: Tokens.spacing.large
spacing: Tokens.spacing.small
CustomText {
Layout.fillWidth: true
color: Colors.palette.m3outline
elide: Text.ElideRight
font: Tokens.font.mono.builders.small.weight(Font.Medium).build()
font.family: Config.appearance.font.family.mono
font.weight: 500
text: root.greeter.sessions.length > 0 ? qsTr("%1 session%2").arg(root.greeter.sessions.length).arg(root.greeter.sessions.length === 1 ? "" : "s") : qsTr("Sessions")
}
Item {
CustomClippingRect {
Layout.fillHeight: true
Layout.fillWidth: true
color: "transparent"
radius: Tokens.rounding.small
Loader {
active: opacity > 0
@@ -43,14 +46,16 @@ ColumnLayout {
MaterialIcon {
Layout.alignment: Qt.AlignHCenter
color: Colors.palette.m3outlineVariant
fontStyle: Tokens.font.icon.large
font.pointSize: Tokens.font.size.extraLarge * 2
text: "desktop_windows"
}
CustomText {
Layout.alignment: Qt.AlignHCenter
color: Colors.palette.m3outlineVariant
font: Tokens.font.headline.builders.small.width(90).build()
font.family: Config.appearance.font.family.mono
font.pointSize: Tokens.font.size.large
font.weight: 500
text: qsTr("No Sessions Found")
}
}
@@ -63,7 +68,6 @@ ColumnLayout {
clip: true
currentIndex: root.greeter.sessionIndex
highlightFollowsCurrentItem: false
boundsBehavior: ListView.StopAtBounds
model: root.greeter.sessions
spacing: Tokens.spacing.small
@@ -76,7 +80,7 @@ ColumnLayout {
anchors.left: parent?.left
anchors.right: parent?.right
implicitHeight: row.implicitHeight + Tokens.padding.small * 2
radius: Tokens.rounding.large
radius: Tokens.rounding.medium - Tokens.padding.small
StateLayer {
onClicked: {
@@ -93,7 +97,7 @@ ColumnLayout {
MaterialIcon {
color: session.index === sessions.currentIndex ? Colors.palette.m3onPrimary : Colors.palette.m3onSurfaceVariant
fontStyle: Tokens.font.icon.extraLarge
font.pointSize: Tokens.font.size.extraLarge
text: modelData.kind === "x11" ? "tv" : "desktop_windows"
}
@@ -105,7 +109,8 @@ ColumnLayout {
Layout.fillWidth: true
color: session.index === sessions.currentIndex ? Colors.palette.m3onPrimary : Colors.palette.m3onSurface
elide: Text.ElideRight
font: Tokens.font.body.builders.medium.weight(Font.Bold).build()
font.pointSize: Tokens.font.size.normal
font.weight: 600
text: modelData.name
}
@@ -113,7 +118,8 @@ ColumnLayout {
Layout.fillWidth: true
color: session.index === sessions.currentIndex ? Colors.palette.m3onPrimaryFixedVariant : Colors.palette.m3onSurfaceVariant
elide: Text.ElideRight
font: Tokens.font.mono.small
font.family: Config.appearance.font.family.mono
font.pointSize: Tokens.font.size.small
text: modelData.kind
}
}
@@ -123,7 +129,7 @@ ColumnLayout {
color: Colors.palette.m3primary
implicitHeight: sessions.currentItem?.implicitHeight ?? 0
implicitWidth: sessions.width
radius: Tokens.rounding.large
radius: Tokens.rounding.medium - Tokens.padding.small
y: sessions.currentItem?.y ?? 0
Behavior on y {
+16 -11
View File
@@ -14,19 +14,22 @@ ColumnLayout {
anchors.fill: parent
anchors.margins: Tokens.padding.large
spacing: Tokens.spacing.large
spacing: Tokens.spacing.small
CustomText {
Layout.fillWidth: true
color: Colors.palette.m3outline
elide: Text.ElideRight
font: Tokens.font.mono.builders.small.weight(Font.Medium).build()
font.family: Config.appearance.font.family.mono
font.weight: 500
text: root.greeter.users.length > 0 ? qsTr("%1 user%2").arg(root.greeter.users.length).arg(root.greeter.users.length === 1 ? "" : "s") : qsTr("Users")
}
Item {
CustomClippingRect {
Layout.fillHeight: true
Layout.fillWidth: true
color: "transparent"
radius: Tokens.rounding.small
Loader {
active: opacity > 0
@@ -45,14 +48,16 @@ ColumnLayout {
Layout.alignment: Qt.AlignHCenter
color: Colors.palette.m3outlineVariant
fill: 1
fontStyle: Tokens.font.icon.large
font.pointSize: Tokens.font.size.extraLarge * 2
text: "account_circle"
}
CustomText {
Layout.alignment: Qt.AlignHCenter
color: Colors.palette.m3outlineVariant
font: Tokens.font.headline.builders.small.width(90).build()
font.family: Config.appearance.font.family.mono
font.pointSize: Tokens.font.size.large
font.weight: 500
text: qsTr("No Users Found")
}
}
@@ -65,7 +70,6 @@ ColumnLayout {
clip: true
currentIndex: root.greeter.selectedUser ? root.greeter.users.indexOf(root.greeter.selectedUser) : -1
highlightFollowsCurrentItem: false
boundsBehavior: ListView.StopAtBounds
model: root.greeter.users
spacing: Tokens.spacing.small
@@ -78,7 +82,7 @@ ColumnLayout {
anchors.left: parent?.left
anchors.right: parent?.right
implicitHeight: row.implicitHeight + Tokens.padding.small * 2
radius: Tokens.rounding.large
radius: Tokens.rounding.medium - Tokens.padding.small
StateLayer {
onClicked: {
@@ -93,7 +97,7 @@ ColumnLayout {
anchors.margins: Tokens.padding.small
spacing: Tokens.spacing.medium
ClipRect {
CustomClippingRect {
Layout.preferredHeight: 50
Layout.preferredWidth: 50
radius: Tokens.rounding.full
@@ -102,7 +106,7 @@ ColumnLayout {
anchors.centerIn: parent
color: user.index === users.currentIndex ? Colors.palette.m3onPrimary : Colors.palette.m3onSurfaceVariant
fill: 1
fontStyle: Tokens.font.icon.extraLarge
font.pointSize: Tokens.font.size.extraLarge
text: modelData.kind === "x11" ? "tv" : "account_circle"
}
@@ -120,7 +124,8 @@ ColumnLayout {
Layout.fillWidth: true
color: user.index === users.currentIndex ? Colors.palette.m3onPrimary : Colors.palette.m3onSurface
elide: Text.ElideRight
font: Tokens.font.body.builders.medium.weight(Font.Bold).build()
font.pointSize: Tokens.font.size.normal
font.weight: 600
text: modelData.username
}
}
@@ -130,7 +135,7 @@ ColumnLayout {
color: Colors.palette.m3primary
implicitHeight: users.currentItem?.implicitHeight ?? 0
implicitWidth: users.width
radius: Tokens.rounding.large
radius: Tokens.rounding.medium - Tokens.padding.small
y: users.currentItem?.y ?? 0
Behavior on y {
+171
View File
@@ -0,0 +1,171 @@
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Layouts
import ZShell.Config
import qs.Components
import qs.Helpers
import qs.Services
ColumnLayout {
id: root
required property int rootHeight
anchors.left: parent.left
anchors.margins: Tokens.padding.large * 2
anchors.right: parent.right
spacing: Tokens.spacing.small
Loader {
Layout.alignment: Qt.AlignHCenter
Layout.bottomMargin: -Tokens.padding.large
Layout.topMargin: Tokens.padding.large * 2
active: root.rootHeight > 610
visible: active
sourceComponent: CustomText {
color: Colors.palette.m3primary
font.pointSize: Tokens.font.size.extraLarge
font.weight: 500
text: qsTr("Weather")
}
}
RowLayout {
Layout.fillWidth: true
spacing: Tokens.spacing.largeIncreased
MaterialIcon {
animate: true
color: Colors.palette.m3secondary
font.pointSize: Tokens.font.size.extraLarge * 2.5
text: Weather.icon
}
ColumnLayout {
spacing: Tokens.spacing.small
CustomText {
Layout.fillWidth: true
animate: true
color: Colors.palette.m3secondary
elide: Text.ElideRight
font.pointSize: Tokens.font.size.large
font.weight: 500
text: Weather.description
}
CustomText {
Layout.fillWidth: true
animate: true
color: Colors.palette.m3onSurfaceVariant
elide: Text.ElideRight
font.pointSize: Tokens.font.size.normal
text: qsTr("Humidity: %1%").arg(Weather.humidity)
}
}
Loader {
Layout.rightMargin: Tokens.padding.small
active: root.width > 400
visible: active
sourceComponent: ColumnLayout {
spacing: Tokens.spacing.small
CustomText {
Layout.fillWidth: true
animate: true
color: Colors.palette.m3primary
elide: Text.ElideLeft
font.pointSize: Tokens.font.size.extraLarge
font.weight: 500
horizontalAlignment: Text.AlignRight
text: Weather.temp
}
CustomText {
Layout.fillWidth: true
animate: true
color: Colors.palette.m3outline
elide: Text.ElideLeft
font.pointSize: Tokens.font.size.smaller
horizontalAlignment: Text.AlignRight
text: qsTr("Feels like: %1").arg(Weather.feelsLike)
}
}
}
}
Loader {
id: forecastLoader
Layout.bottomMargin: Tokens.padding.large * 2
Layout.fillWidth: true
Layout.topMargin: Tokens.spacing.small
active: root.rootHeight > 820
visible: active
sourceComponent: RowLayout {
spacing: Tokens.spacing.largeIncreased
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);
const start = Math.max(0, Weather.currentHourIndex);
return forecast.slice(start, start + count);
}
ColumnLayout {
id: forecastHour
required property var modelData
Layout.fillWidth: true
spacing: Tokens.spacing.small
CustomText {
Layout.fillWidth: true
color: Colors.palette.m3outline
font.pointSize: Tokens.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: Tokens.font.size.extraLarge * 1.5
font.weight: 500
text: forecastHour.modelData?.icon ?? "cloud_alert"
}
CustomText {
Layout.alignment: Qt.AlignHCenter
color: Colors.palette.m3secondary
font.pointSize: Tokens.font.size.larger
text: Config.services.useFahrenheit ? `${forecastHour.modelData?.tempF ?? 0}°F` : `${forecastHour.modelData?.tempC ?? 0}°C`
}
}
}
}
}
Timer {
interval: 900000 // 15 minutes
repeat: true
running: true
triggeredOnStart: true
onTriggered: Weather.reload()
}
}
+10
View File
@@ -77,6 +77,16 @@ Singleton {
"99": "thunderstorm"
})
function getAppCategoryIcon(name: string, fallback: string): string {
const categories = DesktopEntries.heuristicLookup(name)?.categories;
if (categories)
for (const [key, value] of Object.entries(categoryIcons))
if (categories.includes(key))
return value;
return fallback;
}
function getAppIcon(name: string, fallback: string): string {
const icon = DesktopEntries.heuristicLookup(name)?.icon;
if (fallback !== "undefined")
-122
View File
@@ -1,122 +0,0 @@
pragma Singleton
import Quickshell
import Quickshell.Networking
import QtQuick
Singleton {
id: root
property bool active: false
readonly property list<Network> connectedNetworks: networks.filter(n => n.connected)
property Network connectingNetwork
readonly property list<NetworkDevice> devices: Networking.devices.values
property Network failedNetwork
readonly property list<Network> knownNetworks: networks.filter(n => n.known && !n.connected)
readonly property list<Network> networks: {
const list = [];
for (const d of wifiDevices) {
for (const n of d.networks.values) {
if (!list.includes(n))
list.push(n);
}
}
return list;
}
property bool scanning: false
readonly property list<Network> unknownNetworks: networks.filter(n => !n.known)
readonly property list<WifiDevice> wifiDevices: devices.filter(d => wifiDevice(d))
readonly property bool wifiEnabled: Networking.wifiEnabled
signal networkConnected
function forgetNetwork(target): void {
if (!target) {
return;
}
target.forget();
}
function isSecure(security): bool {
return (security === WifiSecurityType.WpaPsk || security === WifiSecurityType.Wpa2Psk || security === WifiSecurityType.Sae);
}
function requestConnect(network: Network, secret = undefined): void {
connectingNetwork = network;
if (secret === undefined || secret === "") {
network.connect();
} else {
(network as WifiNetwork).connectWithPsk(secret);
}
}
function setScan(value: bool): void {
for (const d of wifiDevices) {
d.scannerEnabled = value;
}
}
function setWifi(value: bool): void {
Networking.wifiEnabled = value;
}
function wifiDevice(dev): bool {
return dev.type === DeviceType.Wifi;
}
onActiveChanged: {
for (const d of wifiDevices)
d.scannerEnabled = active;
}
Timer {
id: flushFailed
interval: 5000
running: root.failedNetwork
onTriggered: {
root.failedNetwork = null;
}
}
Timer {
id: checkConnection
interval: 500
repeat: true
running: root.connectingNetwork
onTriggered: {
var completedNetwork;
switch (root.connectingNetwork.state) {
case ConnectionState.Connecting:
break;
case ConnectionState.Connected:
completedNetwork = root.connectingNetwork;
root.networkConnected();
break;
case ConnectionState.Disconnected:
root.failedNetwork = root.connectingNetwork;
completedNetwork = root.connectingNetwork;
break;
}
if (completedNetwork) {
root.connectingNetwork = null;
}
}
}
Timer {
interval: 10000
running: checkConnection.running
onTriggered: {
root.failedNetwork = root.connectingNetwork;
root.connectingNetwork = null;
}
}
}
-31
View File
@@ -1,31 +0,0 @@
import QtQuick
import ZShell.Config
import qs.Services
import qs.Modules.Settings.Common
import qs.Modules.Bar.Popouts.Network
import qs.Components
import qs.Helpers
CustomText {
required property var modelData
bottomPadding: Tokens.spacing.extraSmall
color: Colors.palette.m3onSurfaceVariant
font.pointSize: Tokens.font.size.smaller
text: {
if (modelData.type === "known") {
const count = Network.knownNetworks.filter(n => !n.connected).length;
return qsTr("%1 known network%2").arg(count).arg(count === 1 ? "" : "s");
}
if (modelData.type === "unknown") {
const count = Network.unknownNetworks.filter(n => !n.connected).length;
return qsTr("%1 network%2 available").arg(count).arg(count === 1 ? "" : "s");
}
return qsTr("object unknown");
}
topPadding: Tokens.spacing.small
}
-118
View File
@@ -1,118 +0,0 @@
import QtQuick
import QtQuick.Layouts
import ZShell.Config
import qs.Modules.Settings.Common
import qs.Modules.Bar.Popouts.Network
import qs.Components
import qs.Helpers
import qs.Helpers.Network
import qs.Services
ConnectedRect {
id: networkItem
readonly property int connectedCount: Network.connectedNetworks.length
readonly property bool connecting: Network.connectingNetwork === modelData
property int horizontalPadding: Tokens.padding.largeIncreased
readonly property bool inConnectedGroup: index < connectedCount
// sectioning first / last item in list reminder
// if index === network.connectedNetworks.length - 1
// if index === network.connectedNetworks.length - 1 + network.knownNetworks.length - 2
// if index === network.connectedNetworks.length - 1 + network.knownNetworks.length
// list.model.values.length - 1
required property int index
readonly property bool isSecure: Network.isSecure(modelData.security)
required property ListView list
required property var modelData
property int verticalPadding: Tokens.padding.smaller
Layout.fillWidth: true
first: !inConnectedGroup && index === connectedCount + 1
implicitHeight: rowLayout.implicitHeight + verticalPadding * 2
last: inConnectedGroup ? index === connectedCount - 1 : index === (list?.count ?? -1)
radius: Tokens.rounding.small
RowLayout {
id: rowLayout
Layout.preferredHeight: visible ? rowLayout.implicitHeight + Tokens.padding.smaller * 2 : 0
anchors.fill: parent
anchors.leftMargin: networkItem.horizontalPadding
anchors.rightMargin: networkItem.horizontalPadding
Column {
id: column
Layout.fillWidth: true
CustomText {
Layout.fillWidth: true
Layout.leftMargin: Tokens.spacing.extraSmall
Layout.rightMargin: Tokens.spacing.extraSmall
color: networkItem.modelData.active ? Colors.palette.m3primary : Colors.palette.m3onSurface
elide: Text.ElideRight
text: networkItem.modelData.name ? networkItem.modelData.name : qsTr("Unknown Network")
}
CustomText {
id: subtext
anchors.left: parent.left
anchors.right: parent.right
color: Colors.palette.m3outline
elide: Text.ElideRight
font.pointSize: Tokens.font.size.small
text: networkItem.modelData.connected ? qsTr("Connected: %1 strength").arg(Math.round(networkItem.modelData.signalStrength * 100) + "%") : qsTr("%1 strength").arg(Math.round(networkItem.modelData.signalStrength * 100) + "%")
}
}
Item {
Layout.alignment: Qt.AlignRight
implicitHeight: icon.height
implicitWidth: icon.width
MaterialIcon {
id: icon
color: networkItem.modelData.active ? Colors.palette.m3primary : Colors.palette.m3onSurfaceVariant
font.pointSize: Tokens.font.size.large
opacity: networkItem.connecting ? 0 : 1
text: Icons.getNetworkIcon(networkItem.modelData.signalStrength * 100, networkItem.isSecure)
Behavior on opacity {
Anim {
}
}
}
Loader {
active: opacity > 0
anchors.fill: parent
opacity: networkItem.connecting ? 1 : 0
Behavior on opacity {
Anim {
}
}
sourceComponent: CircularIndicator {
bgColor: "transparent"
running: true
}
}
}
}
StateLayer {
color: networkItem.modelData.active ? Colors.palette.m3onPrimary : Colors.palette.m3onSurface
onClicked: {
if (networkItem.isSecure && !networkItem.known) {
PopupManager.requestOpen(Qt.resolvedUrl("../../Modules/Bar/Popouts/Network/PSKOverlay.qml"), {
network: networkItem.modelData
});
} else {
Network.requestConnect(networkItem.modelData);
}
}
}
}
-74
View File
@@ -1,74 +0,0 @@
import QtQuick
import QtQuick.Layouts
import ZShell.Config
import qs.Services
import qs.Components
import qs.Modules.Settings.Common
CustomSwitch {
id: toggleItem
readonly property alias bg: bg
property alias first: bg.first
property alias last: bg.last
property string subtext
Layout.fillWidth: true
cLayer: 2
horizontalPadding: Tokens.padding.largeIncreased
implicitHeight: Math.max(implicitContentHeight, implicitIndicatorHeight) + verticalPadding * 2
implicitWidth: implicitContentWidth + implicitIndicatorWidth + horizontalPadding * 2
indicator.anchors.right: right
indicator.anchors.rightMargin: toggleItem.horizontalPadding
indicator.anchors.verticalCenter: verticalCenter
verticalPadding: Tokens.padding.normal
background: ConnectedRect {
id: bg
StateLayer {
id: stateLayer
manualPressOverride: toggleItem.pressed
}
}
contentItem: Item {
anchors.left: parent.left
anchors.leftMargin: toggleItem.horizontalPadding
anchors.right: toggleItem.indicator.left
anchors.rightMargin: Tokens.spacing.normal
implicitHeight: column.implicitHeight
implicitWidth: column.implicitWidth
ColumnLayout {
id: column
anchors.left: parent.left
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
spacing: 0
CustomText {
id: label
Layout.fillWidth: true
elide: Text.ElideRight
font.pointSize: Tokens.font.size.smaller
text: toggleItem.text
}
CustomText {
id: subtext
Layout.fillWidth: true
color: Colors.palette.m3outline
elide: Text.ElideRight
font.pointSize: Tokens.font.size.small
text: toggleItem.subtext
visible: toggleItem.subtext
}
}
}
onPressed: stateLayer.press(stateLayer.mouseX, stateLayer.mouseY)
}
+9
View File
@@ -206,6 +206,15 @@ GridLayout {
}
}
DelegateChoice {
roleValue: "network"
delegate: EntryWrapper {
NetworkWidget {
}
}
}
DelegateChoice {
roleValue: "media"
+49
View File
@@ -0,0 +1,49 @@
pragma ComponentBehavior: Bound
import Quickshell
import QtQuick
import QtQuick.Effects
import ZShell.Config
import qs.Components
import qs.Services
Item {
id: root
required property Item bar
required property ScreenState screenState
anchors.fill: parent
CustomRect {
anchors.fill: parent
anchors.margins: -1
color: Colors.palette.m3surface
layer.enabled: true
layer.effect: MultiEffect {
maskEnabled: true
maskInverted: true
maskSource: mask
maskSpreadAtMin: 1
maskThresholdMin: 0.5
}
}
Item {
id: mask
anchors.fill: parent
layer.enabled: true
visible: false
Rectangle {
anchors.fill: parent
anchors.margins: Config.bar.border + 1
anchors.topMargin: root.bar.implicitHeight + 1
radius: Config.bar.border > 0 ? Config.bar.rounding : 0
topLeftRadius: Config.bar.rounding
topRightRadius: Config.bar.rounding
}
}
}
-22
View File
@@ -105,28 +105,6 @@ WidgetBase {
}
}
DelegateChoice {
roleValue: "bluetooth"
delegate: EntryWrapper {
name: "bluetooth"
BluetoothWidget {
}
}
}
DelegateChoice {
roleValue: "network"
delegate: EntryWrapper {
name: "network"
NetworkWidget {
}
}
}
DelegateChoice {
roleValue: "power"
@@ -1,14 +0,0 @@
import QtQuick
import QtQuick.Layouts
import ZShell.Config
import qs.Services
import qs.Components
MaterialIcon {
Layout.alignment: Qt.AlignVCenter
animate: true
color: Colors.palette.m3onSurface // Network.connected ? root.textColor : Colors.palette.m3error
fill: 1
font.pointSize: Tokens.font.size.larger
text: "bluetooth"
}
@@ -1,14 +0,0 @@
import QtQuick
import QtQuick.Layouts
import ZShell.Config
import qs.Services
import qs.Components
MaterialIcon {
Layout.alignment: Qt.AlignVCenter
animate: true
color: Colors.palette.m3onSurface // Network.connected ? root.textColor : Colors.palette.m3error
fill: 1
font.pointSize: Tokens.font.size.larger
text: "android_wifi_4_bar"
}
+2 -2
View File
@@ -134,7 +134,7 @@ Item {
node: Audio.source
}
ClipRect {
CustomClippingRect {
anchors.fill: parent
radius: parent.radius
@@ -249,7 +249,7 @@ Item {
node: appBox.modelData
}
ClipRect {
CustomClippingRect {
anchors.fill: parent
radius: parent.radius
-385
View File
@@ -1,385 +0,0 @@
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Layouts
import QtQml.Models
import Quickshell
import Quickshell.Bluetooth
import ZShell.Components
import ZShell.Config
import qs.Services
import qs.Components
import qs.Helpers
CustomClippingRect {
id: root
readonly property list<var> btConnected: Bluetooth.devices.values.filter(d => d.connected)
implicitHeight: layout.implicitHeight + layout.anchors.margins * 2
implicitWidth: 500
ColumnLayout {
id: layout
anchors.fill: parent
anchors.margins: Tokens.padding.larger
spacing: Tokens.spacing.extraSmall / 2
CustomText {
Layout.rightMargin: Tokens.padding.extraSmall
Layout.topMargin: Tokens.padding.normal
font.pointSize: Tokens.font.size.large
text: qsTr("Bluetooth")
}
Toggle {
checked: Bluetooth.defaultAdapter?.enabled ?? false // qmllint disable unresolved-type
first: true
subtext: qsTr("Toggle bluetooth device state")
text: checked ? qsTr("Enabled") : qsTr("Disabled")
onToggled: {
const adapter = Bluetooth.defaultAdapter; // qmllint disable unresolved-type
if (adapter)
adapter.enabled = checked;
}
}
Toggle {
checked: Bluetooth.defaultAdapter?.discovering ?? false // qmllint disable unresolved-type
last: root.btConnected.length === 0
subtext: qsTr("Enable scanning for new devices")
text: qsTr("Discover")
onToggled: {
const adapter = Bluetooth.defaultAdapter; // qmllint disable unresolved-type
if (adapter)
adapter.discovering = checked;
}
}
ListView {
id: rows
Layout.fillWidth: true
Layout.preferredHeight: contentHeight
interactive: false
section.property: "connected"
spacing: Tokens.spacing.extraSmall / 2
add: Transition {
Anim {
from: 0
property: "opacity"
to: 1
}
}
delegate: DelegateChooser {
role: "isDivider"
DelegateChoice {
roleValue: true
delegate: StatusDivider {
}
}
DelegateChoice {
delegate: BluetoothItem {
list: rows
width: rows.width
}
}
}
displaced: Transition {
Anim {
easing.type: Easing.OutCubic
property: "y"
}
}
model: ScriptModel {
objectProp: "address"
values: {
const byName = (a, b) => a.name.localeCompare(b.name);
return [...[...Bluetooth.devices.values].filter(d => d.connected).sort(byName).slice(0, 5),
{
isDivider: true
},
...[...Bluetooth.devices.values].filter(d => !d.connected).sort(byName).slice(0, 5)];
}
}
move: Transition {
Anim {
easing.type: Easing.OutCubic
property: "y"
}
}
remove: Transition {
Anim {
property: "opacity"
to: 0
}
}
section.delegate: Rectangle {
required property string section
color: "transparent"
height: childrenRect.height
width: ListView.view.width
CustomText {
text: parent.section
}
}
}
}
component BluetoothItem: ConnectedRect {
id: btItem
readonly property int connectedCount: root.btConnected.length
readonly property bool connecting: Network.connectingNetwork === modelData
property int horizontalPadding: Tokens.padding.largeIncreased
readonly property bool inConnectedGroup: index < connectedCount
required property int index
required property ListView list
readonly property bool loading: modelData.state === BluetoothDeviceState.Connecting || modelData.state === BluetoothDeviceState.Disconnecting // qmllint disable unresolved-type
required property BluetoothDevice modelData
property int verticalPadding: Tokens.padding.smaller
first: !inConnectedGroup && index === connectedCount + 1
implicitHeight: rowLayout.implicitHeight + verticalPadding * 2
last: inConnectedGroup ? index === connectedCount - 1 : index === list.count - 1
radius: Tokens.rounding.small
Timer {
interval: 50
running: btItem.ListView.delayRemove
onTriggered: btItem.ListView.delayRemove = false
}
Connections {
function onConnectedChanged(): void {
if (!btItem.modelData.connected)
btItem.ListView.delayRemove = true;
}
function onPairedChanged(): void {
if (btItem.modelData.paired)
btItem.modelData.connect();
}
target: btItem.modelData
}
RowLayout {
id: rowLayout
anchors.fill: parent
anchors.leftMargin: btItem.horizontalPadding
anchors.rightMargin: btItem.horizontalPadding
Column {
id: column
Layout.fillWidth: true
CustomText {
Layout.fillWidth: true
Layout.leftMargin: Tokens.spacing.extraSmall
Layout.rightMargin: Tokens.spacing.extraSmall
elide: Text.ElideRight
text: btItem.modelData.name
}
CustomText {
id: subtext
anchors.left: parent.left
anchors.right: parent.right
color: Colors.palette.m3outline
elide: Text.ElideRight
font.pointSize: Tokens.font.size.small
text: btItem.modelData.connected ? (btItem.modelData.batteryAvailable ? qsTr("%1 battery left").arg(Math.round(btItem.modelData.battery * 100) + "%") : qsTr("Battery status unknown")) : btItem.modelData.address
}
}
Item {
Layout.alignment: Qt.AlignRight
implicitHeight: icon.height
implicitWidth: icon.width
MaterialIcon {
id: icon
font.pointSize: Tokens.font.size.large
opacity: btItem.connecting ? 0 : 1
text: btItem.modelData.batteryAvailable ? Icons.getBatteryIcon(btItem.modelData.battery) : "battery_unknown"
visible: btItem.modelData.state === BluetoothDeviceState.Connected // qmllint disable unresolved-type
Behavior on opacity {
Anim {
}
}
}
Loader {
active: opacity > 0
anchors.fill: parent
opacity: btItem.connecting ? 1 : 0
Behavior on opacity {
Anim {
}
}
sourceComponent: CircularIndicator {
bgColor: "transparent"
running: true
}
}
}
ButtonRow {
spacing: Tokens.spacing.small
IconTextButton {
font.pointSize: Tokens.font.size.small
icon: btItem.modelData.connected ? "link_off" : btItem.modelData.paired ? "link" : "add_link"
text: btItem.modelData.connected ? qsTr("Disconnect") : btItem.modelData.paired ? qsTr("Connect") : qsTr("Pair")
onClicked: {
if (btItem.modelData.connected)
btItem.modelData.disconnect();
else if (btItem.modelData.paired)
btItem.modelData.connect();
else if (!btItem.modelData.paired)
btItem.modelData.pair();
else if (btItem.modelData.pairing)
btItem.modelData.cancelPair();
}
}
}
}
}
component ConnectedRect: CustomRect {
id: bg
property bool first
property bool last
bottomLeftRadius: last ? Tokens.rounding.large : Tokens.rounding.extraSmall
bottomRightRadius: last ? Tokens.rounding.large : Tokens.rounding.extraSmall
color: Colors.tPalette.m3surfaceContainer
topLeftRadius: first ? Tokens.rounding.large : Tokens.rounding.extraSmall
topRightRadius: first ? Tokens.rounding.large : Tokens.rounding.extraSmall
Behavior on bottomLeftRadius {
Anim {
}
}
Behavior on bottomRightRadius {
Anim {
}
}
Behavior on topLeftRadius {
Anim {
}
}
Behavior on topRightRadius {
Anim {
}
}
}
component StatusDivider: CustomText {
bottomPadding: Tokens.spacing.extraSmall
color: Colors.palette.m3onSurfaceVariant
font.pointSize: Tokens.font.size.smaller
text: {
const devices = Bluetooth.devices.values; // qmllint disable unresolved-type
const connectedCount = devices.filter(d => d.connected).length;
let available = qsTr("%1 device%2 available").arg(devices.length - connectedCount).arg(devices.length === 1 ? "" : "s");
return available;
}
topPadding: Tokens.spacing.small
}
component Toggle: CustomSwitch {
id: toggleItem
readonly property alias bg: bg
property alias first: bg.first
property alias last: bg.last
property string subtext
Layout.fillWidth: true
cLayer: 2
horizontalPadding: Tokens.padding.largeIncreased
implicitHeight: Math.max(implicitContentHeight, implicitIndicatorHeight) + verticalPadding * 2
implicitWidth: implicitContentWidth + implicitIndicatorWidth + horizontalPadding * 2
indicator.anchors.right: right
indicator.anchors.rightMargin: toggleItem.horizontalPadding
indicator.anchors.verticalCenter: verticalCenter
verticalPadding: Tokens.padding.normal
background: ConnectedRect {
id: bg
StateLayer {
id: stateLayer
manualPressOverride: toggleItem.pressed
}
}
contentItem: Item {
anchors.left: parent.left
anchors.leftMargin: toggleItem.horizontalPadding
anchors.right: toggleItem.indicator.left
anchors.rightMargin: Tokens.spacing.normal
implicitHeight: column.implicitHeight
implicitWidth: column.implicitWidth
Column {
id: column
anchors.left: parent.left
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
spacing: 0
CustomText {
id: label
anchors.left: parent.left
anchors.right: parent.right
elide: Text.ElideRight
font: {
const f = Qt.font(label.font);
f.pointSize = Tokens.font.size.smaller;
return f;
}
text: toggleItem.text
}
CustomText {
id: subtext
anchors.left: parent.left
anchors.right: parent.right
color: Colors.palette.m3outline
elide: Text.ElideRight
font: {
const f = Qt.font(subtext.font);
f.pointSize = Tokens.font.size.small;
return f;
}
text: toggleItem.subtext
visible: toggleItem.subtext
}
}
}
onPressed: stateLayer.press(stateLayer.mouseX, stateLayer.mouseY)
}
}
-7
View File
@@ -82,13 +82,6 @@ Item {
}
}
Popout {
name: "bluetooth"
sourceComponent: BluetoothPopout {
}
}
Popout {
name: "updates"
@@ -1,71 +0,0 @@
import QtQuick
import ZShell.Config
import qs.Services
import qs.Helpers.Network
import qs.Components
CustomRect {
id: root
readonly property bool closing: PopupManager.closed
readonly property url currentPopup: PopupManager.currentPopup
property bool shouldBeVisible: loader.status === Loader.Ready
color: Qt.alpha(Colors.palette.m3shadow, 0.3)
opacity: shouldBeVisible && !closing ? 1 : 0
visible: opacity > 0
Behavior on opacity {
Anim {
}
}
onVisibleChanged: {
if (!visible) {
PopupManager.requestClose();
PopupManager.currentPopup = "";
}
}
CustomMouseArea {
anchors.fill: parent
hoverEnabled: true
preventStealing: true
propagateComposedEvents: false
onClicked: {
const insideItemWidth = mouseX < loader.x + loader.item.width && mouseX > loader.x;
const insideItemHeight = mouseY < loader.y + loader.item.height && mouseY > loader.y;
if (insideItemHeight && insideItemWidth)
return;
PopupManager.requestClose();
}
}
Loader {
id: loader
anchors.centerIn: parent
Connections {
function onCurrentPopupChanged(): void {
if (PopupManager.currentPopup) {
loader.setSource(PopupManager.currentPopup, PopupManager.currentPopupProps);
} else {
loader.source = null;
}
}
target: PopupManager
}
}
Connections {
function onNetworkConnected(): void {
PopupManager.requestClose();
}
target: Network
}
}
-102
View File
@@ -1,102 +0,0 @@
import QtQuick
import QtQuick.Layouts
import ZShell.Components
import ZShell.Config
import qs.Services
import qs.Components
import qs.Helpers.Network
CustomClippingRect {
id: root
required property var network
color: Colors.palette.m3surfaceContainer
implicitHeight: layout.implicitHeight + layout.anchors.margins * 2
implicitWidth: layout.implicitWidth + layout.anchors.margins * 2
radius: Tokens.rounding.large
ColumnLayout {
id: layout
anchors.fill: parent
anchors.margins: Tokens.padding.extraLarge
spacing: Tokens.spacing.medium
MaterialIcon {
Layout.alignment: Qt.AlignHCenter
font.pointSize: Tokens.font.size.extraLarge * 2
text: "lock"
}
CustomText {
Layout.alignment: Qt.AlignHCenter
color: Colors.palette.m3onSurfaceVariant
text: qsTr("Connect to %1").arg(root.network.name)
}
CustomTextField {
id: passwordField
Layout.fillWidth: true
Layout.preferredWidth: 400
echoMode: CustomTextField.Password
errorText: qsTr("Failed to connect with given password")
isError: Network.failedNetwork
leadingIcon: "lock"
passwordMaskDelay: 5000
placeholderText: qsTr("Input password")
}
ButtonRow {
id: buttonRow
Layout.fillWidth: true
spacing: Tokens.spacing.extraSmall
IconTextButton {
fillWidth: true
font.pointSize: Tokens.font.size.normal
icon: "close"
inactiveColor: Colors.layer(Colors.palette.m3surfaceContainerHighest, 2)
inactiveOnColor: Colors.palette.m3onSurfaceVariant
isRound: true
isToggle: false
shapeMorph: true
text: "Cancel"
onClicked: PopupManager.requestClose()
}
IconTextButton {
fillWidth: true
font.pointSize: Tokens.font.size.normal
icon: Network.connectingNetwork ? "" : "check"
inactiveColor: Network.connectingNetwork ? Colors.palette.m3primaryContainer : Network.failedNetwork ? Colors.palette.m3error : Colors.palette.m3primary
inactiveOnColor: Network.connectingNetwork ? Colors.palette.m3onPrimaryContainer : Network.failedNetwork ? Colors.palette.m3onError : Colors.palette.m3onPrimary
isRound: true
isToggle: false
shapeMorph: true
text: Network.connectingNetwork ? "" : "Apply"
onClicked: Network.requestConnect(root.network, passwordField.text)
Loader {
active: opacity > 0
anchors.centerIn: parent
opacity: Network.connectingNetwork ? 1 : 0
Behavior on opacity {
Anim {
}
}
sourceComponent: CircularIndicator {
bgColor: "transparent"
implicitSize: buttonRow.height - Tokens.padding.normal
running: true
}
}
}
}
}
}
@@ -1,22 +0,0 @@
pragma Singleton
import Quickshell
import QtQuick
Singleton {
id: root
property bool closed: true
property url currentPopup: ""
property var currentPopupProps: ({})
function requestClose(): void {
closed = true;
}
function requestOpen(source: url, properties = {}): void {
currentPopupProps = properties;
currentPopup = source;
closed = false;
}
}
+16 -122
View File
@@ -1,142 +1,36 @@
pragma ComponentBehavior: Bound
import Quickshell
import Quickshell.Networking
import QtQuick
import QtQuick.Layouts
import Quickshell
import ZShell.Components
import ZShell.Config
import qs.Services
import qs.Modules.Settings.Common
import qs.Modules.Bar.Popouts.Network
import qs.Components
import qs.Helpers.Network
import ZShell.Config
import qs.Modules
import qs.Helpers as Helpers
CustomClippingRect {
Item {
id: root
required property var wrapper
anchors.horizontalCenter: parent.horizontalCenter
implicitHeight: networkPopContent.height + networkPopContent.anchors.margins * 2
implicitWidth: 500 + 8 * 2
radius: (20 - Tokens.padding.small) * Tokens.rounding.scale
Component.onCompleted: Network.active = true
Component.onDestruction: Network.active = false
ColumnLayout {
id: networkPopContent
id: layout
anchors.left: parent.left
anchors.margins: Tokens.padding.large
anchors.right: parent.right
anchors.top: parent.top
spacing: Tokens.spacing.extraSmall / 2
spacing: 8
CustomText {
Layout.preferredHeight: visible ? implicitHeight : 0
Layout.rightMargin: Tokens.padding.extraSmall
font.pointSize: Tokens.font.size.large
text: qsTr("Wifi")
}
Repeater {
model: Helpers.Network.devices
Toggle {
Layout.preferredHeight: visible ? implicitHeight : 0
checked: Network.wifiEnabled
first: true
last: Network.connectedNetworks.length === 0
subtext: qsTr("Toggle WiFi device state")
text: checked ? qsTr("Enabled") : qsTr("Disabled")
CustomRadioButton {
id: network
onToggled: Network.setWifi(checked)
}
required property NetworkDevice modelData
ListView {
id: rows
Layout.fillWidth: true
Layout.preferredHeight: contentHeight
interactive: false
section.property: "connected"
spacing: Tokens.spacing.extraSmall / 2
add: Transition {
Anim {
from: 0
property: "opacity"
to: 1
}
}
delegate: DelegateChooser {
role: "isDivider"
DelegateChoice {
roleValue: true
delegate: NetworkDivider {
}
}
DelegateChoice {
delegate: NetworkItem {
list: rows
width: rows.width
}
}
}
displaced: Transition {
Anim {
easing.type: Easing.OutCubic
property: "y"
}
}
model: ScriptModel {
values: {
const byStrengthName = (a, b) => b.signalStrength - a.signalStrength || a.name.localeCompare(b.name);
const cNetworks = [...Network.connectedNetworks];
const kNetworks = [...Network.knownNetworks];
const uNetworks = [...Network.unknownNetworks];
return [...cNetworks.sort(byStrengthName), ...(kNetworks.length > 0 ? [
{
isDivider: true,
type: "known"
},
...kNetworks.sort(byStrengthName)] : []), ...(uNetworks.length > 0 ? [
{
isDivider: true,
type: "unknown"
},
...uNetworks.sort(byStrengthName)] : [])];
}
}
move: Transition {
Anim {
easing.type: Easing.OutCubic
property: "y"
}
}
remove: Transition {
Anim {
property: "opacity"
to: 0
}
}
section.delegate: Rectangle {
required property string section
color: "transparent"
height: childrenRect.height
width: ListView.view.width
CustomText {
text: parent.section
}
checked: Helpers.Network.activeDevice?.name === modelData.name
text: modelData.description
visible: modelData.name !== "lo"
}
}
}
NetworkOverlay {
anchors.fill: parent
}
}
+3 -3
View File
@@ -25,7 +25,7 @@ Item {
searchField.forceActiveFocus();
}
ClipRect {
CustomClippingRect {
id: search
anchors.left: parent.left
@@ -54,7 +54,7 @@ Item {
}
}
ClipRect {
CustomClippingRect {
id: preview
anchors.bottom: parent.bottom
@@ -228,7 +228,7 @@ Item {
anchors.fill: parent
spacing: Tokens.spacing.small
ClipRect {
CustomClippingRect {
id: textRect
Layout.fillHeight: true
+1 -1
View File
@@ -14,7 +14,7 @@ Row {
padding: 20
spacing: 12
ClipRect {
CustomClippingRect {
color: Colors.layer(Colors.palette.m3surfaceContainerHigh, 2)
implicitHeight: info.implicitHeight
implicitWidth: info.implicitHeight
+1 -1
View File
@@ -16,7 +16,7 @@ Item {
implicitWidth: 1000
implicitHeight: 320
ClipRect {
CustomClippingRect {
anchors.fill: parent
radius: root.radius
@@ -46,7 +46,7 @@ CustomListView {
anchors.bottom: parent.bottom
spacing: Tokens.spacing.extraSmall / 2
ClipRect {
CustomClippingRect {
id: iconClip
Layout.fillHeight: true
@@ -25,7 +25,7 @@ Item {
onClicked: root.close()
}
ClipRect {
CustomClippingRect {
id: popupBackground
readonly property real padding: 4
@@ -40,7 +40,7 @@ Item {
visible: false
z: 999
ClipRect {
CustomClippingRect {
id: popupBackground
readonly property real padding: Tokens.padding.extraSmall
+1 -1
View File
@@ -19,7 +19,7 @@ Item {
implicitHeight: tileHeight + Tokens.padding.large * 2
implicitWidth: tileWidth
ClipRect {
CustomClippingRect {
id: image
anchors.horizontalCenter: parent.horizontalCenter
+1 -1
View File
@@ -55,7 +55,7 @@ ColumnLayout {
text: Time.format("dddd, d MMMM yyyy")
}
ClipRect {
CustomClippingRect {
Layout.alignment: Qt.AlignHCenter
Layout.topMargin: Tokens.spacing.largeIncreased * 2
color: Colors.tPalette.m3surfaceContainer
+1 -1
View File
@@ -5,7 +5,7 @@ import qs.Helpers
import ZShell.Config
import qs.Services
ClipRect {
CustomClippingRect {
id: root
required property var lock
+2 -2
View File
@@ -2,12 +2,12 @@ import QtQuick
import QtQuick.Layouts
import Quickshell
import ZShell.Components
import ZShell.Config
import qs.Components
import ZShell.Config
import qs.Helpers
import qs.Services
ClipRect {
CustomClippingRect {
id: root
required property var lock
+3 -2
View File
@@ -24,11 +24,12 @@ ColumnLayout {
Layout.fillWidth: true
color: Colors.palette.m3outline
elide: Text.ElideRight
font: Tokens.font.mono.builders.small.weight(Font.Medium).build()
font.family: Config.appearance.font.family.mono
font.weight: 500
text: NotifServer.list.length > 0 ? qsTr("%1 notification%2").arg(NotifServer.list.length).arg(NotifServer.list.length === 1 ? "" : "s") : qsTr("Notifications")
}
ClipRect {
ClippingRectangle {
id: clipRect
Layout.fillHeight: true
+1 -1
View File
@@ -85,7 +85,7 @@ CustomRect {
}
}
ClipRect {
ClippingRectangle {
anchors.fill: parent
color: root.urgency === "critical" ? Colors.palette.m3error : root.urgency === "low" ? Colors.layer(Colors.palette.m3surfaceContainerHighest, 3) : Colors.palette.m3secondaryContainer
radius: Tokens.rounding.full
+1 -2
View File
@@ -1,13 +1,12 @@
import Quickshell
import Quickshell.Widgets
import QtQuick
import qs.Components
import qs.Paths
Item {
id: root
ClipRect {
ClippingRectangle {
anchors.fill: parent
radius: Tokens.rounding.full
+1 -1
View File
@@ -108,7 +108,7 @@ CustomRect {
visible: root.hasImage || root.hasAppIcon
width: Config.notifs.sizes.image
sourceComponent: ClipRect {
sourceComponent: CustomClippingRect {
color: root.modelData.urgency === NotificationUrgency.Critical ? Colors.palette.m3error : root.modelData.urgency === NotificationUrgency.Low ? Colors.layer(Colors.palette.m3surfaceContainerHighest, 2) : Colors.palette.m3secondaryContainer
implicitHeight: Config.notifs.sizes.image
implicitWidth: Config.notifs.sizes.image
+1 -1
View File
@@ -5,7 +5,7 @@ import ZShell.Config
import qs.Components
import qs.Services
ClipRect {
CustomClippingRect {
id: root
property real animPerc: UPower.displayDevice.percentage
+1 -1
View File
@@ -19,7 +19,7 @@ CustomRect {
implicitWidth: 450
radius: Tokens.rounding.largeIncreased
ClipRect {
CustomClippingRect {
anchors.fill: parent
radius: parent.radius
+1 -1
View File
@@ -149,7 +149,7 @@ ColumnLayout {
}
}
CustomFlickable {
Flickable {
id: flickable
Layout.fillHeight: true
+1 -1
View File
@@ -25,7 +25,7 @@ Item {
anchors.fill: parent
spacing: Tokens.spacing.small
ClipRect {
CustomClippingRect {
id: imgWrapper
Layout.fillWidth: true
+28 -30
View File
@@ -100,13 +100,16 @@ Item {
visible: opacity > 0
Behavior on opacity {
Anim {}
Anim {
}
}
Behavior on scale {
Anim {}
Anim {
}
}
Behavior on y {
Anim {}
Anim {
}
}
Component.onCompleted: {
@@ -117,8 +120,9 @@ Item {
Qt.callLater(syncCropToScreen);
}
Loader {
id: selectorLoader
ButtonRow {
id: screenSelector
anchors.bottom: imageSlot.top
anchors.bottomMargin: Tokens.spacing.medium
anchors.left: parent.left
@@ -126,32 +130,25 @@ Item {
anchors.right: parent.right
anchors.rightMargin: Tokens.padding.extraLarge * 2
anchors.top: parent.top
spacing: Tokens.spacing.small
active: wrapper.screens.length > 1
Repeater {
model: wrapper.screens
sourceComponent: ButtonRow {
id: screenSelector
delegate: TextButton {
required property int index
readonly property bool isCurrent: wrapper.selectedScreenIndex === index
required property var modelData
spacing: Tokens.spacing.small
fillWidth: true
font: Tokens.font.body.builders.large.weight(Font.DemiBold).build()
inactiveColor: isCurrent ? Colors.palette.m3primary : Qt.alpha(Colors.palette.m3surfaceContainer, 0.7)
inactiveOnColor: isCurrent ? Colors.palette.m3onPrimary : Qt.alpha(Colors.palette.m3onSurface, 0.7)
isRound: true
shapeMorph: true
text: modelData.name
Repeater {
model: wrapper.screens
delegate: TextButton {
required property int index
readonly property bool isCurrent: wrapper.selectedScreenIndex === index
required property var modelData
fillWidth: true
font: Tokens.font.body.builders.large.weight(Font.DemiBold).build()
inactiveColor: isCurrent ? Colors.palette.m3primary : Qt.alpha(Colors.palette.m3surfaceContainer, 0.7)
inactiveOnColor: isCurrent ? Colors.palette.m3onPrimary : Qt.alpha(Colors.palette.m3onSurface, 0.7)
isRound: true
shapeMorph: true
text: modelData.name
onClicked: wrapper.selectScreen(index)
}
onClicked: wrapper.selectScreen(index)
}
}
}
@@ -225,9 +222,9 @@ Item {
anchors.left: parent.left
anchors.right: parent.right
anchors.top: parent.top
anchors.topMargin: selectorLoader.active ? wrapper.buttonRowHeight + Tokens.spacing.medium : Tokens.spacing.medium
anchors.topMargin: wrapper.buttonRowHeight + Tokens.spacing.medium
ClipRect {
CustomClippingRect {
id: fitBox
readonly property real aspect: scaledImg.implicitHeight > 0 ? scaledImg.implicitWidth / scaledImg.implicitHeight : 1
@@ -360,7 +357,8 @@ Item {
width: Math.round(baseWidth / zoom)
Behavior on color {
CAnim {}
CAnim {
}
}
Component.onCompleted: {
-2
View File
@@ -38,7 +38,6 @@ Item {
}
BlobInvertedRect {
id: blobRect
anchors.fill: parent
borderBottom: Tokens.padding.small
borderLeft: navPane.width + navPane.anchors.margins * 2
@@ -66,7 +65,6 @@ Item {
anchors.bottom: parent.bottom
anchors.left: navPane.right
anchors.leftMargin: navPane.anchors.margins + anchors.margins
anchors.bottomMargin: blobRect.borderBottom
anchors.margins: Tokens.padding.extraLarge
anchors.right: parent.right
anchors.top: parent.top
+2 -23
View File
@@ -2,8 +2,8 @@ pragma Singleton
import QtQuick
import QtQuick.Layouts
import ZShell.Config
import qs.Components
import ZShell.Config
import qs.Modules.Settings.Common
import qs.Modules.Settings.Pages
import qs.Modules.Settings.Pages.Wallpaper
@@ -13,7 +13,6 @@ import qs.Modules.Settings.Pages.Panels
import qs.Modules.Settings.Pages.Panels.Bar
import qs.Modules.Settings.Pages.Panels.Sidebar
import qs.Modules.Settings.Pages.Services
import qs.Modules.Settings.Pages.Network
import qs.Services
QtObject {
@@ -48,27 +47,7 @@ QtObject {
// Connectivity
Component {
StackPage {
Component {
NetworkPage {}
}
Component {
AllNetworks {}
}
Component {
SavedNetworks {}
}
Component {
AddNetwork {}
}
Component {
OldNetwork {}
}
}
PlaceholderComp {}
},
Component {
PlaceholderComp {}
@@ -1,256 +0,0 @@
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Layouts
import Quickshell
import ZShell.Config
import ZShell.Internal
import ZShell.Blobs
import ZShell.Services
import ZShell.Components
import qs.Components
import qs.Modules.Settings.Common
PageBase {
id: root
property bool connecting: false
property bool failed: false
// Security model: index 0 = none, 1 = WPA/WPA2/WPA3 personal.
readonly property bool secured: securitySelect.active !== noneItem
property bool success: false
function submit(): void {
const ssid = ssidField.text.trim();
if (ssid.length === 0) {
ssidField.isError = true;
ssidField.forceActiveFocus();
return;
}
if (root.secured && passwordField.text.length < 8) {
passwordField.isError = true;
passwordField.forceActiveFocus();
return;
}
root.failed = false;
root.connecting = true;
Nmcli.addHiddenNetwork(ssid, root.secured ? passwordField.text : "", root.secured ? "wpa" : "none", hiddenToggle.checked, result => {
root.connecting = false;
if (result && result.success) {
root.success = true;
root.nState.closeSubPage();
} else {
root.failed = true;
if (root.secured)
passwordField.isError = true;
// Clean up the half-created profile so a retry starts fresh.
Nmcli.forgetNetwork(ssid);
}
});
}
isSubPage: true
title: Tr.tr("Add network")
ColumnLayout {
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: parent.top
spacing: Tokens.spacing.large
width: root.cappedWidth
Connections {
function onSubPageClosed(): void {
if (root.success)
return;
const ssid = ssidField.text.trim();
if (ssid)
Nmcli.forgetNetwork(ssid);
}
target: root.nState
}
CustomText {
Layout.fillWidth: true
Layout.leftMargin: Tokens.padding.extraSmall
color: Colours.palette.m3onSurfaceVariant
font: Tokens.font.body.small
text: Tr.tr("Enter the details below to manually connect to a network.")
wrapMode: Text.WordWrap
}
CustomTextField {
id: ssidField
Layout.fillWidth: true
Layout.topMargin: Tokens.spacing.extraSmall
errorText: Tr.tr("Network name is required")
inputMethodHints: Qt.ImhNoAutoUppercase | Qt.ImhNoPredictiveText
leadingIcon: "wifi"
// TRANSLATORS: SSID is a protocol term, leave it untranslated
placeholderText: Tr.tr("Network name (SSID)")
// TRANSLATORS: sample network name, translate the e.g. but the name is a placeholder
supportingText: Tr.tr("e.g. MyHiddenNetwork")
onAccepted: root.secured ? passwordField.forceActiveFocus() : root.submit()
}
ToggleRow {
id: hiddenToggle
checked: true
first: true
subtext: Tr.tr("Actively probe for a network that doesn't broadcast its name")
text: Tr.tr("Hidden network")
}
SelectRow {
id: securitySelect
Layout.topMargin: Tokens.spacing.extraSmall / 2 - parent.spacing
fallbackIcon: "lock"
// TRANSLATORS: WPA/WPA2/WPA3 are protocol names, leave them untranslated
fallbackText: Tr.tr("WPA/WPA2/WPA3 Personal")
// label: Tr.tr("Security")
last: !root.secured
Behavior on bottomLeftRadius {
Anim {
type: Anim.DefaultEffects
}
}
Behavior on bottomRightRadius {
Anim {
type: Anim.DefaultEffects
}
}
menuItems: [
MenuItem {
icon: "lock"
// TRANSLATORS: WPA/WPA2/WPA3 are protocol names, leave them untranslated
text: Tr.tr("WPA/WPA2/WPA3 Personal")
},
MenuItem {
id: noneItem
icon: "lock_open"
text: Tr.trCtx("None (open)", "wifi security type")
}
]
}
Item {
Layout.bottomMargin: root.secured ? 0 : -parent.spacing
Layout.fillWidth: true
implicitHeight: root.secured ? passwordField.implicitHeight : 0
opacity: root.secured ? 1 : 0
Behavior on Layout.bottomMargin {
Anim {
type: Anim.DefaultEffects
}
}
Behavior on implicitHeight {
Anim {
type: Anim.DefaultEffects
}
}
Behavior on opacity {
Anim {
type: Anim.DefaultEffects
}
}
CustomTextField {
id: passwordField
anchors.left: parent.left
anchors.right: parent.right
echoMode: TextInput.Password
enabled: root.secured
errorText: root.failed ? Tr.tr("Connection failed — check the password") : Tr.tr("Password must be at least 8 characters")
leadingIcon: "key"
placeholderText: Tr.tr("Password")
// TRANSLATORS: WPA is a protocol name, leave it untranslated
supportingText: Tr.tr("WPA passwords are at least 8 characters")
onAccepted: root.submit()
}
}
RowLayout {
Layout.alignment: Qt.AlignRight
Layout.topMargin: Tokens.spacing.extraSmall - parent.spacing
spacing: Tokens.spacing.small
TextButton {
Layout.fillHeight: true
horizontalPadding: Tokens.padding.extraLarge
isRound: true
text: Tr.trCtx("Cancel", "button")
type: TextButton.Tonal
onClicked: root.nState.closeSubPage()
}
// Connect button — swaps to a loading spinner while connecting,
// matching the Wi-Fi list connect animation.
ButtonBase {
id: connectBtn
implicitHeight: connectMetrics.height + Tokens.padding.medium * 2
implicitWidth: connectMetrics.width + Tokens.padding.extraLarge * 2
// inactiveColour: Colours.palette.m3primary
// inactiveOnColour: Colours.palette.m3onPrimary
isRound: true
shapeMorph: true
// stateLayer.disabled: root.connecting || ssidField.text.trim().length === 0
onClicked: {
if (!root.connecting && ssidField.text.trim().length > 0)
root.submit();
}
TextMetrics {
id: connectMetrics
font: connectBtn.font
text: Tr.tr("Connect")
}
Anim {
id: connectContent
// anchors.centerIn: parent
// inAnimType: Anim.SlowEffects
// outAnimType: Anim.SlowEffects
// sourceComp: root.connecting ? connectLoadingComp : connectTextComp
}
Component {
id: connectLoadingComp
LoadingIndicator {
color: connectBtn.onColour
implicitSize: Math.round(Tokens.font.body.medium.pointSize * 1.4)
}
}
Component {
id: connectTextComp
CustomText {
color: connectBtn.onColour
font: connectBtn.font
text: connectMetrics.text
}
}
}
}
}
}
@@ -1,345 +0,0 @@
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Layouts
import ZShell.Config
import qs.Components
import qs.Helpers.Network
import qs.Services
import qs.Modules.Settings.Common
PageBase {
id: root
flickable.bottomMargin: Tokens.padding.extraExtraLarge * 2 // Extra scrolling space at the bottom
isSubPage: true
title: Tr.tr("All networks")
ColumnLayout {
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: parent.top
// spacing: Tokens.spacing.extraSmall / 2
width: root.cappedWidth
Timer {
interval: 300 // GlobalConfig.nexus.networkRescanInterval
repeat: true
running: root.visible && Nmcli.wifiEnabled
triggeredOnStart: true
onTriggered: Nmcli.rescanWifi()
}
ConnectedRect {
Layout.fillWidth: true
first: true
implicitHeight: headerLayout.implicitHeight + Tokens.padding.medium * 2
RowLayout {
id: headerLayout
anchors.left: parent.left
anchors.leftMargin: Tokens.padding.largeIncreased
anchors.right: parent.right
anchors.rightMargin: Tokens.padding.large
anchors.verticalCenter: parent.verticalCenter
anchors.verticalCenterOffset: 1
// spacing: Tokens.spacing.extraSmall / 2
CustomText {
font: Tokens.font.title.small
text: Tr.trCtx("Filters", "network filter heading")
}
Item {
Layout.fillWidth: true
}
FilterButton {
id: savedFilter
function internalFilter(ap: Nmcli.AccessPoint): bool {
return Nmcli.hasSavedProfile(ap.ssid);
}
bottomLeftRadius: pressed ? pressedRadius : implicitHeight / 2
text: Tr.trCtx("Saved", "network filter")
topLeftRadius: pressed ? pressedRadius : implicitHeight / 2
Behavior on bottomLeftRadius {
Anim {
type: Anim.DefaultEffects
}
}
Behavior on topLeftRadius {
Anim {
type: Anim.DefaultEffects
}
}
}
FilterButton {
id: secureFilter
function internalFilter(ap: Nmcli.AccessPoint): bool {
return ap.security !== "none";
}
text: Tr.trCtx("Secured", "network filter")
}
FilterButton {
id: highFreqFilter
function internalFilter(ap: Nmcli.AccessPoint): bool {
return ap.frequency >= 4900 && ap.frequency <= 5900;
}
text: Tr.trCtx("5 GHz", "network filter")
}
FilterButton {
id: lowFreqFilter
function internalFilter(ap: Nmcli.AccessPoint): bool {
return ap.frequency >= 2400 && ap.frequency <= 2500;
}
bottomRightRadius: pressed ? pressedRadius : implicitHeight / 2
text: Tr.trCtx("2.4 GHz", "network filter")
topRightRadius: pressed ? pressedRadius : implicitHeight / 2
Behavior on bottomRightRadius {
Anim {
type: Anim.DefaultEffects
}
}
Behavior on topRightRadius {
Anim {
type: Anim.DefaultEffects
}
}
}
}
}
NetworkList {
id: networkList
function networkFilter(ap: Nmcli.AccessPoint): bool {
return savedFilter.filter(ap) && secureFilter.filter(ap) && highFreqFilter.filter(ap) && lowFreqFilter.filter(ap);
}
enableFilter: true
last: true
// nState: root.nState
}
}
component FilterButton: IconTextButton {
id: filter
property int filterState // 0 = default, 1 = on, 2 = negate
function filter(ap: Nmcli.AccessPoint): bool {
if (filterState === 0)
return true;
if (filterState === 1)
return internalFilter(ap);
return !internalFilter(ap);
}
function internalFilter(ap: Nmcli.AccessPoint): bool {
return true;
}
defaultRadius: filterState > 0 ? implicitHeight / 2 : Tokens.rounding.small
icon: ["check_indeterminate_small", "check", "close"][filterState]
// inactiveColour: [Colours.palette.m3secondaryContainer, Colours.palette.m3secondary, Colours.palette.m3tertiary][filterState]
// inactiveOnColour: [Colours.palette.m3onSecondaryContainer, Colours.palette.m3onSecondary, Colours.palette.m3onTertiary][filterState]
pressedRadius: Tokens.rounding.extraSmall
// spacing: Tokens.spacing.extraSmall
onClicked: filterState = (filterState + 1) % 3
}
component NetworkList: ItemList {
id: netList
property bool enableFilter
property int limit: 0 // 0 = show all
// required property NexusState nState
signal networkSelected(ap: Nmcli.AccessPoint)
function networkFilter(ap: Nmcli.AccessPoint): bool {
return true;
}
extraHeight: Nmcli.scanning ? Tokens.rounding.extraSmall : 0 // Inline so it isn't affected by anim
list.anchors.top: scanningIndicator.bottom
placeholderIcon: Nmcli.wifiEnabled ? "wifi_find" : "signal_wifi_off"
placeholderText: Nmcli.wifiEnabled ? Tr.tr("No networks found") : Tr.tr("Wi-Fi disabled")
showList: Nmcli.wifiEnabled
delegate: StateLayer {
id: network
property bool currentSelected
required property int index
required property var modelData
property real textOpacity: disabled ? 0.5 : 1
anchors.fill: undefined
anchors.left: netList.list.contentItem.left
anchors.right: netList.list.contentItem.right
bottomLeftRadius: netList?.last && index === netList.list.count - 1 ? Tokens.rounding.extraLarge : radius
bottomRightRadius: netList?.last && index === netList.list.count - 1 ? Tokens.rounding.extraLarge : radius
// disabled: currentSelected || Nmcli.connectingSsid() === modelData.ssid
implicitHeight: networkLayout.implicitHeight + networkLayout.anchors.margins * 2
radius: Tokens.rounding.extraSmall
Behavior on textOpacity {
Anim {
type: Anim.DefaultEffects
}
}
onClicked: {
if (!modelData.active) {
NetworkConnection.handleConnect(modelData);
currentSelected = true;
netList.networkSelected(modelData);
} else {
// Active network: open its detail/settings sub-page.
netList.nState.selectedNetworkSsid = modelData.ssid;
netList.nState.networkDetailsFromSaved = false;
netList.nState.openSubPage(3);
}
}
Connections {
function onActiveChanged(): void {
if (network.modelData.active)
network.currentSelected = false;
}
target: network.modelData
}
Connections {
function onNetworkSelected(ap: Nmcli.AccessPoint): void {
if (ap !== network.modelData)
network.currentSelected = false;
}
target: netList
}
RowLayout {
id: networkLayout
anchors.fill: parent
anchors.leftMargin: Tokens.padding.extraLarge
anchors.margins: Tokens.padding.large
anchors.rightMargin: Tokens.padding.extraLarge
// spacing: Tokens.spacing.extraSmall
MaterialIcon {
color: network.modelData.active ? Colours.palette.m3primary : Colours.palette.m3onSurfaceVariant
font.pointSize: Tokens.font.size.normal
opacity: network.textOpacity
text: Icons.getNetworkIcon(network.modelData.strength)
}
ColumnLayout {
Layout.fillWidth: true
opacity: network.textOpacity
// spacing: 0
CustomText {
Layout.fillWidth: true
elide: Text.ElideRight
font: Tokens.font.body.small
text: network.modelData.ssid
}
CustomText {
Layout.fillWidth: true
color: Colours.palette.m3outline
elide: Text.ElideRight
font: Tokens.font.label.small
text: {
const net = network.modelData;
let status = "";
if (net.active)
status = Tr.trCtx("Connected", "network connected");
else if (Nmcli.hasSavedProfile(net.ssid))
status = Tr.trCtx("Saved", "network saved");
if (status)
// TRANSLATORS: %1 = security type, %2 = connection status
return Tr.trCtx("Security: %1 • %2", "network security and status").arg(net.security).arg(status);
return Tr.trCtx("Security: %1", "network security").arg(net.security);
}
}
}
// Anim {
// // sourceComp: Nmcli.connectingSsid() === network.modelData.ssid ? loadingComp : iconComp
//
// Component {
// id: iconComp
//
// MaterialIcon {
// color: network.modelData.active ? Colours.palette.m3primary : Colours.palette.m3onSurfaceVariant
// fontStyle: Tokens.font.icon.medium
// opacity: network.textOpacity
// text: network.modelData.active ? "settings" : "lock"
// }
// }
//
// Component {
// id: loadingComp
//
// LoadingIndicator {
// implicitSize: Math.round(Tokens.font.icon.medium.pointSize * 1.3)
// }
// }
// }
}
}
// model: ScriptModel {
// values: {
// const connecting = Nmcli.connectingSsid();
// // Lower rank sorts higher in the list
// const rank = n => n.active ? 0 : n.ssid === connecting ? 1 : Nmcli.hasSavedProfile(n.ssid) ? 2 : 3;
// const sorted = [...Nmcli.networks].sort((a, b) => rank(a) - rank(b) || b.strength - a.strength);
// if (netList.limit > 0 && sorted.length > netList.limit)
// sorted.length = netList.limit;
// return netList.enableFilter ? sorted.filter(netList.networkFilter) : sorted;
// }
// }
// StyledProgressBar {
// id: scanningIndicator
//
// anchors.left: parent.left
// anchors.margins: 1
// anchors.right: parent.right
// implicitHeight: Nmcli.scanning ? Tokens.rounding.extraSmall : 0
// indeterminate: true
//
// Behavior on implicitHeight {
// Anim {
// type: Anim.DefaultEffects
// }
// }
// }
}
}
@@ -1,55 +0,0 @@
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Layouts
import Quickshell
import ZShell.Config
import qs.Modules.Settings.Common
import qs.Modules.Bar.Popouts.Network
import qs.Components
import qs.Helpers.Network
import qs.Services
PageBase {
id: root
title: qsTr("Begin page")
ColumnLayout {
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: parent.top
spacing: Tokens.spacing.extraSmall / 2
width: root.cappedWidth
NavRow {
first: true
icon: "dock_to_bottom"
// settingAnchor: "panels-bar"
text: qsTr("Show all networks")
onClicked: root.sState.openSubPage(1)
}
NavRow {
icon: "dock_to_bottom"
text: qsTr("Saved networks")
onClicked: root.sState.openSubPage(2)
}
NavRow {
icon: "dock_to_bottom"
text: qsTr("Add network")
onClicked: root.sState.openSubPage(3)
}
NavRow {
icon: "dock_to_bottom"
last: true
text: qsTr("Old Network Page")
onClicked: root.sState.openSubPage(4)
}
}
}
@@ -1,252 +0,0 @@
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Layouts
import Quickshell
import ZShell.Config
import qs.Modules.Settings.Common
import qs.Modules.Bar.Popouts.Network
import qs.Components
import qs.Helpers.Network
import qs.Services
PageBase {
id: root
isSubPage: true
title: qsTr("Old network page")
Component.onCompleted: Network.active = true
Component.onDestruction: Network.active = false
ColumnLayout {
id: networkContent
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: parent.top
spacing: Tokens.spacing.extraSmall / 2
width: root.cappedWidth
Toggle {
Layout.preferredHeight: visible ? implicitHeight : 0
checked: Network.wifiEnabled
first: true
last: Network.connectedNetworks.length === 0
subtext: qsTr("Toggle WiFi device state")
text: checked ? qsTr("Enabled") : qsTr("Disabled")
onToggled: Network.setWifi(checked)
}
ListView {
id: rows
Layout.fillWidth: true
Layout.preferredHeight: contentHeight
interactive: false
section.property: "connected"
spacing: Tokens.spacing.extraSmall / 2
add: Transition {
Anim {
from: 0
property: "opacity"
to: 1
}
}
delegate: DelegateChooser {
role: "isDivider"
DelegateChoice {
roleValue: true
delegate: NetworkDivider {
}
}
DelegateChoice {
roleValue: false
delegate: KnownNetworkManager {
}
}
DelegateChoice {
delegate: NetworkItem {
list: rows
width: rows.width
}
}
}
displaced: Transition {
Anim {
easing.type: Easing.OutCubic
property: "y"
}
}
model: ScriptModel {
values: {
const byStrengthName = (a, b) => b.signalStrength - a.signalStrength || a.name.localeCompare(b.name);
const cNetworks = [...Network.connectedNetworks];
const kNetworks = [...Network.knownNetworks];
const uNetworks = [...Network.unknownNetworks];
return [...cNetworks.sort(byStrengthName), ...(kNetworks.length > 0 ? [
{
isDivider: true,
type: "known"
},
...kNetworks.sort(byStrengthName),
{
isDivider: false
}
] : []), ...(uNetworks.length > 0 ? [
{
isDivider: true,
type: "unknown"
},
...uNetworks.sort(byStrengthName)] : [])];
}
}
move: Transition {
Anim {
easing.type: Easing.OutCubic
property: "y"
}
}
remove: Transition {
Anim {
property: "opacity"
to: 0
}
}
section.delegate: Rectangle {
required property string section
color: "transparent"
height: childrenRect.height
width: ListView.view.width
CustomText {
text: parent.section
}
}
}
NetworkOverlay {
anchors.centerIn: parent
}
}
component KnownNetworkManager: NetworkSelect {
acceptLabel: qsTr("Remove")
enabled: Object.keys(model).length > 0
header: qsTr("Manage known networks")
icon: "Network_Manage"
label: qsTr("Manage known networks")
model: Network.networks.filter(n => n.known && !n.connected).map(n => ({
id: n.name,
label: n.name
}))
rootParent: root.flickable
width: rows.width
onAccepted: {
if (!selectedItem)
return;
const networkToForget = Network.networks.find(n => n.name === selectedItem);
Network.forgetNetwork(networkToForget);
}
}
component NetworkSelect: DialogRowButton {
id: klist
required property var model
property var selectedItem
function keyFor(item: var): string {
return item.id;
}
function labelFor(item: var): string {
return item.label;
}
acceptAllowed: !!selectedItem
horizontalContentMargin: -Tokens.padding.extraSmall
separateContent: true
content: Component {
VerticalFadeListView {
bottomMargin: Tokens.padding.large
model: klist.model
spacing: 0
topMargin: Tokens.padding.large
delegate: CustomRect {
id: item
required property var modelData
readonly property bool selected: klist.selectedItem === klist.keyFor(modelData)
anchors.left: ListView.view.contentItem.left
anchors.margins: 1 // Gets cut off for some reason without this
anchors.right: ListView.view.contentItem.right
color: Qt.alpha(Colors.palette.m3tertiaryContainer, selected ? 1 : 0)
implicitHeight: label.implicitHeight + Tokens.padding.small * 2
radius: stateLayer.pressed ? Tokens.rounding.extraSmall : selected ? Tokens.rounding.largeIncreased : Tokens.rounding.medium
Behavior on radius {
Anim {
type: Anim.SlowEffects
}
}
StateLayer {
id: stateLayer
onClicked: klist.selectedItem = klist.keyFor(item.modelData)
}
CustomText {
id: label
anchors.left: parent.left
anchors.margins: Tokens.padding.large
anchors.right: item.selected ? checkIcon.left : parent.right
anchors.rightMargin: item.selected ? Tokens.spacing.medium : anchors.margins
anchors.verticalCenter: parent.verticalCenter
color: item.selected ? Colors.palette.m3onTertiaryContainer : Colors.palette.m3onSurface
elide: Text.ElideRight
font.pointSize: Tokens.font.size.smaller
text: klist.labelFor(item.modelData)
}
MaterialIcon {
id: checkIcon
anchors.margins: Tokens.padding.large
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
color: Colors.palette.m3onTertiaryContainer
font.pointSize: Tokens.font.size.normal
opacity: item.selected ? 1 : 0
text: "check"
Behavior on opacity {
Anim {
type: Anim.SlowEffects
}
}
}
}
}
}
onOpenChanged: {
if (open)
selectedItem = null;
}
}
}
@@ -1,118 +0,0 @@
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Layouts
import Quickshell
import ZShell.Config
import ZShell.Internal
import ZShell.Blobs
import ZShell.Services
import ZShell.Components
import qs.Components
import qs.Modules.Settings.Common
PageBase {
id: root
isSubPage: true
title: Tr.tr("Saved networks")
Component.onCompleted: Nmcli.loadSavedConnections(() => {})
ColumnLayout {
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: parent.top
spacing: Tokens.spacing.extraSmall / 2
width: root.cappedWidth
ItemList {
id: savedList
first: true
last: true
placeholderIcon: "wifi_find"
placeholderText: Tr.tr("No saved networks")
showList: true
delegate: StateLayer {
id: saved
readonly property var ap: Nmcli.findNetwork(modelData)
required property int index
readonly property bool isActive: !!Nmcli.active && Nmcli.active.ssid === modelData
required property var modelData
anchors.fill: undefined
anchors.left: savedList.list.contentItem.left
anchors.right: savedList.list.contentItem.right
bottomLeftRadius: index === savedList?.list.count - 1 ? Tokens.rounding.extraLarge : radius
bottomRightRadius: index === savedList?.list.count - 1 ? Tokens.rounding.extraLarge : radius
implicitHeight: savedLayout.implicitHeight + savedLayout.anchors.margins * 2
radius: Tokens.rounding.extraSmall
topLeftRadius: index === 0 ? Tokens.rounding.extraLarge : radius
topRightRadius: index === 0 ? Tokens.rounding.extraLarge : radius
onClicked: {
root.nState.selectedNetworkSsid = saved.modelData;
root.nState.networkDetailsFromSaved = true;
root.nState.openSubPage(3); // Shared network detail/edit sub-page
}
RowLayout {
id: savedLayout
anchors.fill: parent
anchors.leftMargin: Tokens.padding.extraLarge
anchors.margins: Tokens.padding.large
anchors.rightMargin: Tokens.padding.extraLarge
spacing: Tokens.spacing.medium
MaterialIcon {
color: saved.isActive ? Colours.palette.m3primary : Colours.palette.m3onSurfaceVariant
font.pointSize: Tokens.font.size.normal
text: saved.ap ? Icons.getNetworkIcon(saved.ap.strength, !["", "none"].includes(Nmcli.savedSecurityFor(saved.modelData))) : "signal_wifi_off"
}
ColumnLayout {
Layout.fillWidth: true
spacing: 0
CustomText {
Layout.fillWidth: true
elide: Text.ElideRight
font: Tokens.font.body.small
text: saved.modelData
}
CustomText {
Layout.fillWidth: true
color: saved.isActive ? Colours.palette.m3primary : Colours.palette.m3outline
elide: Text.ElideRight
font: Tokens.font.label.small
text: {
let security;
if (saved.ap)
security = saved.ap.security || Tr.trCtx("Open", "wifi security type");
else
security = Nmcli.securityLabel(Nmcli.savedSecurityFor(saved.modelData)) || Tr.trCtx("Unknown", "unknown wifi security");
if (saved.isActive)
// TRANSLATORS: %1 = security type
return Tr.trCtx("Connected • %1", "network connected with security").arg(security);
return security;
}
}
}
MaterialIcon {
color: Colours.palette.m3onSurfaceVariant
font.pointSize: Tokens.font.size.normal
text: "chevron_right"
}
}
}
model: ScriptModel {
values: [...Nmcli.savedConnectionSsids].sort((a, b) => a.localeCompare(b))
}
}
}
}
@@ -8,9 +8,7 @@ PageBase {
readonly property var builtinIcons: ({
audio: qsTr("Speakers"),
microphone: qsTr("Microphone"),
power: qsTr("Battery"),
network: qsTr("Network"),
bluetooth: qsTr("Bluetooth")
power: qsTr("Battery")
})
isSubPage: true
@@ -95,15 +93,5 @@ PageBase {
onToggled: Config.bar.popouts.statusIcons = checked
}
ToggleRow {
checked: Config.bar.popouts.network
last: true
settingAnchor: "bar-status-network-popout"
subtext: qsTr("Show a details popout when hovering the network icon")
text: qsTr("Network popout on hover")
onToggled: Config.bar.popouts.network = checked
}
}
}
+2 -53
View File
@@ -3,7 +3,6 @@ pragma ComponentBehavior: Bound
import QtQuick
import Quickshell
import QtQuick.Layouts
import ZShell.Services
import ZShell.Components
import ZShell.Config
import ZShell.Llm
@@ -91,9 +90,7 @@ Item {
signal requestClose
readonly property bool atNewestEnd: !listLoader.item || listLoader.item.contentY < 4
readonly property bool importing: importIds.length > 0
property var importIds: []
property var pendingAttachments: []
function attachFiles(): void {
@@ -439,7 +436,7 @@ Item {
width: attachments.cellWidth
height: width
opacity: modelData ? 1 : 0
ClipRect {
CustomClippingRect {
anchors.fill: parent
anchors.margins: Tokens.spacing.extraSmall
@@ -544,7 +541,7 @@ Item {
bg.border.color: Colors.palette.m3outlineVariant
bg.color: Colors.tPalette.m3surfaceContainerLowest
fastGlow: LlmClient.busy
glow.opacity: !ChatState.narrowSidebarExpanded ? (input.activeFocus && (!LlmClient.busy || ChatState.isWindow) ? 1 : 0) : 0
glow.opacity: input.activeFocus && (!LlmClient.busy || ChatState.isWindow)
font: Tokens.font.body.small
implicitHeight: Math.min(root.height / 5, contentHeight + topPadding + bottomPadding)
focus: true
@@ -581,52 +578,4 @@ Item {
root.send(text);
}
}
Connections {
target: DropImporter
function onImportFinished(id: int, paths: var, errors: var): void {
if (!root.importIds.includes(id))
return;
root.importIds = root.importIds.filter(i => i !== id);
if (paths.length > 0) {
const added = root.chatData.addAttachments(paths);
root.pendingAttachments = root.pendingAttachments.concat(added);
}
for (const e of errors)
console.warn("Drop import:", e);
}
}
DropArea {
id: dropArea
anchors.fill: parent
onEntered: drag => {
drag.accepted = drag.hasUrls || drag.hasText || drag.hasHtml || drag.formats.some(f => f.startsWith("image/"));
}
onDropped: drop => {
const formats = drop.formats;
const urls = drop.hasUrls ? drop.urls : [];
const text = drop.hasText ? drop.text : "";
const html = drop.hasHtml ? drop.html : "";
const imgFormat = formats.find(f => f.startsWith("image/"));
const raw = imgFormat ? drop.getDataAsArrayBuffer(imgFormat) : new ArrayBuffer(0);
switch (DropImporter.decide(urls, text, html, raw.byteLength > 0)) {
case DropImporter.Attach:
root.importIds = [...root.importIds, DropImporter.importAttachments(urls, html, raw)];
break;
case DropImporter.InsertText:
input.insert(input.cursorPosition, text || urls.map(u => u.toString()).join("\n"));
root.focusInput();
break;
}
drop.acceptProposedAction();
}
}
}
+25 -77
View File
@@ -91,50 +91,6 @@ CustomRect {
opacity: root.slim ? 0 : 1
state: titleText.readOnly ? "static" : "editing"
states: [
State {
name: "static"
PropertyChanges {
staticText.opacity: 1
}
PropertyChanges {
titleText.opacity: 0
}
},
State {
name: "editing"
PropertyChanges {
staticText.opacity: 0
}
PropertyChanges {
titleText.opacity: 1
}
}
]
transitions: [
// Going static: titleText fades out (animated), staticText snaps in
Transition {
to: "static"
Anim {
target: titleText
property: "opacity"
type: Anim.SlowEffects
}
},
// Going editing: staticText fades out (animated), titleText snaps in
Transition {
to: "editing"
Anim {
target: staticText
property: "opacity"
type: Anim.SlowEffects
}
}
]
Behavior on opacity {
Anim {
type: Anim.DefaultEffects
@@ -150,7 +106,7 @@ CustomRect {
color: Colors.layer(Colors.palette.m3surfaceContainerHighest, 1)
implicitHeight: titleText.implicitHeight + Tokens.padding.extraSmall * 2
radius: Tokens.rounding.full
implicitWidth: titleText.width + Tokens.padding.large * 2
implicitWidth: titleText.implicitWidth + Tokens.padding.large * 2
opacity: titleText.readOnly ? 0 : 1
Behavior on opacity {
@@ -166,6 +122,11 @@ CustomRect {
anchors.right: parent.right
implicitHeight: titleText.implicitHeight + Tokens.padding.small * 2
layer.enabled: true
layer.effect: OpacityMask {
maskSource: fadeMask
}
TextFieldBase {
id: titleText
@@ -173,12 +134,10 @@ CustomRect {
anchors.leftMargin: readOnly ? 0 : Tokens.padding.large
anchors.top: parent.top
anchors.topMargin: readOnly ? Tokens.padding.small : Tokens.padding.extraSmall * 2
width: Math.min(implicitWidth, parent.width - Tokens.padding.large * 2)
font: Tokens.font.body.medium
text: root.modelData.title || qsTr("New chat")
readOnly: true
color: Colors.palette.m3onSurface
opacity: 0
Behavior on anchors.leftMargin {
Anim {}
@@ -187,11 +146,6 @@ CustomRect {
Anim {}
}
function cancelTitleEdit(): void {
text = root.modelData.title;
readOnly = true;
}
onEditingFinished: root.modelData.title = text
onReadOnlyChanged: {
if (!readOnly) {
@@ -202,24 +156,27 @@ CustomRect {
}
}
}
}
CustomText {
id: staticText
CustomRect {
id: fadeMask
anchors.left: parent.left
anchors.leftMargin: titleText.readOnly ? 0 : Tokens.padding.large
anchors.top: parent.top
anchors.topMargin: titleText.readOnly ? Tokens.padding.small : Tokens.padding.extraSmall * 2
anchors.right: parent.right
font: titleText.font
text: titleText.text
elide: CustomText.ElideRight
anchors.fill: titleWrapper
Behavior on anchors.leftMargin {
Anim {}
layer.enabled: true
visible: false
gradient: Gradient {
orientation: Gradient.Horizontal
GradientStop {
color: Qt.rgba(1, 1, 1, 1.0)
position: 0.85
}
Behavior on anchors.topMargin {
Anim {}
GradientStop {
color: Qt.rgba(1, 1, 1, 0)
position: 1.0
}
}
}
@@ -366,13 +323,7 @@ CustomRect {
inactiveColor: Colors.layer(Colors.palette.m3surfaceContainerHighest, 3)
inactiveOnColor: Colors.palette.m3onSurfaceVariant
onClicked: {
if (!titleText.readOnly) {
titleText.cancelTitleEdit();
return;
}
root.remove(root.modelData);
}
onClicked: root.remove(root.modelData)
}
IconButton {
@@ -415,10 +366,7 @@ CustomRect {
}
StateLayer {
onClicked: {
root.expanded = !root.expanded;
titleText.readOnly = true;
}
onClicked: root.expanded = !root.expanded
}
MaterialIcon {
+3 -21
View File
@@ -140,7 +140,7 @@ Item {
label.transform: Rotation {
origin.y: fabRoot.label.height / 2
origin.x: fabRoot.label.width / 2
angle: ChatState.fabExpanded && (ChatState.narrowSidebarExpanded || !ChatState.isWide || !ChatState.isWindow) ? 135 : 0
angle: ChatState.fabExpanded && (ChatState.narrowSidebarExpanded && !ChatState.isWide || !ChatState.isWindow) ? 135 : 0
Behavior on angle {
Anim {}
@@ -148,9 +148,8 @@ Item {
}
onClicked: {
if ((root.slim && !ChatState.narrowSidebarExpanded) || ChatState.shrinkSidebarOverride) {
if (root.slim && !ChatState.narrowSidebarExpanded) {
root.requestExpand();
ChatState.shrinkSidebarOverride = false;
return;
}
@@ -184,10 +183,6 @@ Item {
name: "pop out in window"
icon: "open_in_new"
}
ListElement {
name: "shrink sidebar"
icon: "left_panel_close"
}
}
CustomRect {
@@ -201,18 +196,7 @@ Item {
radius: sLayer.pressed ? Tokens.rounding.small : implicitHeight / 2
color: Colors.palette.m3primaryContainer
visible: {
const n = modelData.name;
const w = ChatState.isWindow;
const wide = ChatState.isWide;
if (n === "pop out in window" && w)
return false;
if (n === "shrink sidebar" && (!w || !wide))
return false;
else
return true;
}
visible: !(modelData.name === "pop out in window" && ChatState.isWindow)
clip: true
Behavior on radius {
@@ -313,8 +297,6 @@ Item {
root.newChatRequest();
else if (name === "pop out in window")
Detach.create();
else if (name === "shrink sidebar")
ChatState.shrinkSidebarOverride = true;
ChatState.fabExpanded = false;
}
+1 -4
View File
@@ -19,7 +19,7 @@ Singleton {
property bool settledSlim: false
property bool settledWide: false
property bool narrowSidebarExpanded: false
property bool shrinkSidebarOverride: false
property bool windowIsWide: false
property ShellScreen screen
property bool sidebarVisible: ShellState.forActive().sidebar
@@ -28,9 +28,6 @@ Singleton {
root.currentIdx = -1;
}
onIsWideChanged: if (isWide)
narrowSidebarExpanded = false
onSidebarVisibleChanged: if (!sidebarVisible)
fabExpanded = false
}
@@ -337,7 +337,7 @@ Item {
}
}
ClipRect {
CustomClippingRect {
id: expandedTool
anchors.top: collapsedRow.bottom
@@ -220,8 +220,9 @@ CustomRect {
Layout.alignment: Qt.AlignVCenter
color: root.codeHeaderColor
font: Tokens.font.label.builders.small.capitalization(Font.Capitalize).build()
font: Tokens.font.label.small
text: CodeIcons.name(root.language)
visible: root.language.length > 0
}
Item {
@@ -251,7 +252,7 @@ CustomRect {
}
}
CustomRect {
CustomClippingRect {
id: codeRect
anchors.left: parent.left
@@ -264,7 +265,6 @@ CustomRect {
implicitWidth: codeText.implicitWidth + codeFlick.anchors.margins * 2
implicitHeight: codeText.implicitHeight + codeFlick.anchors.margins * 2
color: CodeColors.active.bg
clip: true
Flickable {
id: codeFlick
@@ -272,9 +272,6 @@ CustomRect {
anchors.fill: parent
anchors.margins: Tokens.padding.small
boundsBehavior: Flickable.StopAtBounds
contentWidth: codeText.implicitWidth
CustomScrollBar.horizontal: CustomScrollBar {
flickable: codeFlick
parent: codeFlick.parent
@@ -283,13 +280,16 @@ CustomRect {
anchors.right: parent.right
}
CustomText {
TextAreaBase.flickable: TextAreaBase {
id: codeText
color: CodeColors.active.normal
layer.enabled: true
clip: false
font: Tokens.font.mono.small
textFormat: Text.RichText
textFormat: root.colored ? Text.RichText : Text.PlainText
text: root.colored ? root.html : root.plainForDisplay(root.code)
readOnly: true
}
}
}
File diff suppressed because one or more lines are too long
@@ -125,7 +125,7 @@ Item {
current: root.current
}
ClipRect {
CustomClippingRect {
id: expandedRect
anchors.left: parent.left
+18 -9
View File
@@ -21,8 +21,12 @@ Item {
property bool chatOpen: ChatState.chatSession !== null
readonly property bool isWide: root.width >= root.breakpoint
readonly property bool wideExpandedSidebar: isWide && !ChatState.shrinkSidebarOverride
readonly property bool narrowExpandedSidebar: !isWide && ChatState.narrowSidebarExpanded
readonly property bool showList: isWide || !chatOpen
readonly property bool showContent: !isWide && chatOpen
function openConversation(conv: ChatSession, index: int): void {
}
BlobGroup {
id: blobGroup
@@ -50,9 +54,9 @@ Item {
anchors.left: parent.left
anchors.bottom: parent.bottom
anchors.margins: Tokens.padding.medium
implicitWidth: root.wideExpandedSidebar || root.narrowExpandedSidebar ? Config.sidebar.sizes.width : root.iconSize
implicitWidth: root.isWide || ChatState.narrowSidebarExpanded ? Config.sidebar.sizes.width : root.iconSize
highlight: true
slim: !root.wideExpandedSidebar && !root.narrowExpandedSidebar
slim: !root.isWide && !ChatState.narrowSidebarExpanded
z: 1
Behavior on implicitWidth {
@@ -82,13 +86,13 @@ Item {
}
}
Item {
CustomClippingRect {
anchors.fill: parent
anchors.leftMargin: invertedRect.borderLeft
anchors.topMargin: invertedRect.borderTop
anchors.bottomMargin: invertedRect.borderBottom
anchors.rightMargin: invertedRect.borderRight
clip: true
radius: invertedRect.radius
children: [contentArea, dim]
}
@@ -96,10 +100,9 @@ Item {
CustomRect {
id: dim
anchors.fill: parent
radius: invertedRect.radius
color: Colors.palette.m3shadow
opacity: root.narrowExpandedSidebar ? 0.3 : 0
opacity: ChatState.narrowSidebarExpanded && !root.isWide ? 0.3 : 0
z: 1
Behavior on opacity {
@@ -152,7 +155,7 @@ Item {
id: dimArea
anchors.fill: parent
preventStealing: true
enabled: root.narrowExpandedSidebar
enabled: !root.isWide && ChatState.narrowSidebarExpanded
cursorShape: undefined
onClicked: {
@@ -172,6 +175,12 @@ Item {
}
}
Binding {
target: ChatState
property: "windowIsWide"
value: root.showList
}
Binding {
target: ChatState
property: "isWide"
+70
View File
@@ -17,6 +17,76 @@ Item {
Layout.fillWidth: true
implicitHeight: flickable.contentHeight
layer.enabled: true
layer.smooth: true
layer.effect: Mask {
maskSource: gradientMask
}
Item {
id: gradientMask
anchors.fill: parent
layer.enabled: true
visible: false
Rectangle {
anchors.fill: parent
gradient: Gradient {
orientation: Gradient.Horizontal
GradientStop {
color: Qt.rgba(0, 0, 0, 0)
position: 0
}
GradientStop {
color: Qt.rgba(0, 0, 0, 1)
position: 0.1
}
GradientStop {
color: Qt.rgba(0, 0, 0, 1)
position: 0.9
}
GradientStop {
color: Qt.rgba(0, 0, 0, 0)
position: 1
}
}
}
Rectangle {
anchors.bottom: parent.bottom
anchors.left: parent.left
anchors.top: parent.top
implicitWidth: parent.width / 2
opacity: flickable.contentX > 0 ? 0 : 1
Behavior on opacity {
Anim {
type: Anim.DefaultEffects
}
}
}
Rectangle {
anchors.bottom: parent.bottom
anchors.right: parent.right
anchors.top: parent.top
implicitWidth: parent.width / 2
opacity: flickable.contentX < flickable.contentWidth - parent.width ? 0 : 1
Behavior on opacity {
Anim {
type: Anim.DefaultEffects
}
}
}
}
CustomFlickable {
id: flickable
+1 -1
View File
@@ -112,7 +112,7 @@ CustomRect {
}
}
ClipRect {
CustomClippingRect {
anchors.fill: parent
color: root.urgency === NotificationUrgency.Critical ? Colors.palette.m3error : root.urgency === NotificationUrgency.Low ? Colors.layer(Colors.palette.m3surfaceContainerHigh, 3) : Colors.palette.m3secondaryContainer
radius: Tokens.rounding.full
+1 -1
View File
@@ -52,7 +52,7 @@ FontBuilder FontBuilder::letterSpacing(qreal spacing, bool absolute) {
return *this;
}
FontBuilder FontBuilder::capitalization(QFont::Capitalization cap) {
FontBuilder FontBuilder::capitalisation(QFont::Capitalization cap) {
m_font.setCapitalization(cap);
return *this;
}
+1 -1
View File
@@ -23,7 +23,7 @@ class FontBuilder {
[[nodiscard]] Q_INVOKABLE FontBuilder
letterSpacing(qreal spacing, bool absolute = true);
[[nodiscard]] Q_INVOKABLE FontBuilder
capitalization(QFont::Capitalization cap);
capitalisation(QFont::Capitalization cap);
[[nodiscard]] Q_INVOKABLE FontBuilder vaxis(const QString& tag, float value);
[[nodiscard]] Q_INVOKABLE FontBuilder vaxes(const QVariantMap& axes);
[[nodiscard]] Q_INVOKABLE QFont build() const;
+1 -14
View File
@@ -662,17 +662,6 @@ void WallpaperService::drawSurface(WallpaperSurface* surface) {
return;
}
// Release the context when this scope ends (see FrameService::drawSurface).
// Holding it across the draw loop's sleep serializes the two raw-EGL clients on
// NVIDIA, which makes eglSwapBuffers fail and starves other processes' contexts.
struct ContextRelease {
EGLDisplay display;
~ContextRelease() {
eglMakeCurrent(
display, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT);
}
} contextRelease{m_eglDisplay};
QVariantMap crops;
QSize size;
bool upload = false;
@@ -712,9 +701,7 @@ void WallpaperService::drawSurface(WallpaperSurface* surface) {
glDrawArrays(GL_TRIANGLES, 0, 3);
if (!eglSwapBuffers(m_eglDisplay, target))
qWarning()
<< "WallpaperService: eglSwapBuffers failed, eglGetError = 0x"
<< QString::number(unsigned(eglGetError()), 16);
qWarning() << "WallpaperService: eglSwapBuffers failed";
}
void WallpaperService::addSurface(const QString& screen) {
-2
View File
@@ -22,12 +22,10 @@ qml_module(ZShell-services
networkusage.hpp networkusage.cpp
lyriccandidate.hpp lyriccandidate.cpp
lyrics.hpp lyrics.cpp
dropimporter.hpp dropimporter.cpp
LIBRARIES
Qt6::Core
Qt6::Qml
Qt6::Concurrent
Qt6::Network
PkgConfig::Pipewire
PkgConfig::Aubio
PkgConfig::Cava
-263
View File
@@ -1,263 +0,0 @@
#include "dropimporter.hpp"
#include <QDir>
#include <QFile>
#include <QFileInfo>
#include <QMetaObject>
#include <QMimeDatabase>
#include <QNetworkReply>
#include <QRegularExpression>
namespace {
constexpr qint64 kMaxBytes = 100 * 1024 * 1024;
bool isHttp(const QUrl& u) {
return u.scheme() == QLatin1String("http") ||
u.scheme() == QLatin1String("https");
}
QString firstImgSrc(const QString& html) {
static const QRegularExpression re(
R"(<img\b[^>]*?\bsrc\s*=\s*["']([^"']+)["'])",
QRegularExpression::CaseInsensitiveOption);
const auto m = re.match(html);
if (!m.hasMatch()) return {};
return m.captured(1).replace(QLatin1String("&amp;"), QLatin1String("&"));
}
bool looksLikeUrl(const QString& text) {
const QString t = text.trimmed();
if (t.isEmpty() || t.contains(QRegularExpression(QStringLiteral("\\s"))))
return false;
const QUrl u(t);
return u.isValid() && (isHttp(u) || u.isLocalFile());
}
QString sanitize(QString name) {
name = QFileInfo(name).fileName();
name.replace(
QRegularExpression(QStringLiteral("[\\\\/:*?\"<>|\\x00-\\x1f]")),
QStringLiteral("_"));
return name.left(120);
}
QString fileNameFor(QNetworkReply* reply, const QUrl& url) {
static const QRegularExpression cd(
QStringLiteral("filename\\*?=(?:UTF-8'')?\"?([^\";]+)\"?"),
QRegularExpression::CaseInsensitiveOption);
const auto m =
cd.match(QString::fromUtf8(reply->rawHeader("Content-Disposition")));
QString name = m.hasMatch()
? QUrl::fromPercentEncoding(m.captured(1).toUtf8())
: url.fileName();
name = sanitize(name);
return name.isEmpty() ? QStringLiteral("download") : name;
}
} // namespace
namespace ZShell::Services {
DropImporter::DropImporter(QObject* parent) : QObject(parent) {}
DropImporter::Decision DropImporter::decide(
const QList<QUrl>& urls,
const QString& text,
const QString& html,
bool hasRawImage) const {
for (const QUrl& u : urls)
if (u.isLocalFile()) return Attach;
const QString src = firstImgSrc(html);
if (hasRawImage ||
(!src.isEmpty() && (text.trimmed().isEmpty() || looksLikeUrl(text))))
return Attach;
QMimeDatabase db;
for (const QUrl& u : urls) {
if (!isHttp(u) || QFileInfo(u.path()).suffix().isEmpty()) continue;
const QMimeType mt = db.mimeTypeForUrl(u);
if (!mt.isDefault() && !mt.inherits(QStringLiteral("text/html")))
return Attach;
}
if (!text.isEmpty() || !urls.isEmpty()) return InsertText;
return Ignore;
}
int DropImporter::importAttachments(
const QList<QUrl>& urls, const QString& html, const QByteArray& rawImage) {
const int id = ++m_nextId;
m_requests[id].pending = 1;
if (!rawImage.isEmpty()) {
saveBytes(id, rawImage);
} else if (const QString src = firstImgSrc(html); !src.isEmpty()) {
if (src.startsWith(QLatin1String("data:"), Qt::CaseInsensitive)) {
const int comma = src.indexOf(QLatin1Char(','));
if (comma < 0) {
failWith(id, tr("Malformed data URI"));
} else {
const bool b64 = src.left(comma).contains(
QLatin1String(";base64"), Qt::CaseInsensitive);
const QString payload = src.mid(comma + 1);
saveBytes(
id,
b64 ? QByteArray::fromBase64(payload.toLatin1())
: QByteArray::fromPercentEncoding(payload.toUtf8()));
}
} else {
const QUrl u(src);
if (u.isLocalFile())
addLocal(id, u);
else if (isHttp(u))
download(id, u);
else
failWith(id, tr("Unsupported image source"));
}
} else {
for (const QUrl& u : urls) {
if (u.isLocalFile())
addLocal(id, u);
else if (isHttp(u))
download(id, u);
else
failWith(id, tr("Unsupported URL: %1").arg(u.toString()));
}
}
QMetaObject::invokeMethod(
this, [this, id] { finishUnit(id); }, Qt::QueuedConnection);
return id;
}
void DropImporter::addLocal(int id, const QUrl& url) {
const QFileInfo fi(url.toLocalFile());
if (!fi.exists())
failWith(id, tr("File not found: %1").arg(fi.filePath()));
else if (fi.isDir())
failWith(id, tr("Folders can't be attached: %1").arg(fi.fileName()));
else if (fi.size() > kMaxBytes)
failWith(id, tr("File too large: %1").arg(fi.fileName()));
else
succeed(id, fi.absoluteFilePath());
}
void DropImporter::saveBytes(int id, const QByteArray& bytes) {
if (bytes.isEmpty() || bytes.size() > kMaxBytes) {
failWith(id, tr("Empty or oversized data"));
return;
}
const QString dir = newSubdir();
if (dir.isEmpty()) {
failWith(id, tr("No temp directory"));
return;
}
const QString suffix =
QMimeDatabase().mimeTypeForData(bytes).preferredSuffix();
const QString path =
dir + QStringLiteral("/dropped") +
(suffix.isEmpty() ? QString() : QLatin1Char('.') + suffix);
QFile f(path);
if (!f.open(QIODevice::WriteOnly) || f.write(bytes) != bytes.size()) {
failWith(id, tr("Could not write temp file"));
return;
}
f.close();
succeed(id, path);
}
void DropImporter::download(int id, const QUrl& url) {
const QString dir = newSubdir();
if (dir.isEmpty()) {
failWith(id, tr("No temp directory"));
return;
}
++m_requests[id].pending;
QNetworkRequest req(url);
req.setAttribute(
QNetworkRequest::RedirectPolicyAttribute,
QNetworkRequest::NoLessSafeRedirectPolicy);
QNetworkReply* reply = m_nam.get(req);
auto* file = new QFile(dir + QStringLiteral("/download.part"), reply);
connect(reply, &QNetworkReply::readyRead, this, [reply, file] {
if (!file->isOpen() && !file->open(QIODevice::WriteOnly)) {
reply->abort();
return;
}
file->write(reply->readAll());
if (file->size() > kMaxBytes) reply->abort();
});
connect(
reply,
&QNetworkReply::finished,
this,
[this, reply, file, id, dir, url] {
reply->deleteLater();
const bool tooBig = file->size() > kMaxBytes;
const bool wrote = file->isOpen();
file->close();
if (tooBig) {
failWith(id, tr("Download too large: %1").arg(url.toString()));
} else if (reply->error() != QNetworkReply::NoError) {
failWith(
id,
tr("Download failed (%1): %2")
.arg(url.toString(), reply->errorString()));
} else if (!wrote) {
failWith(id, tr("Empty response: %1").arg(url.toString()));
} else {
const QString name = fileNameFor(reply, url);
QString finalPath = dir + QLatin1Char('/') + name;
if (QFile::rename(file->fileName(), finalPath)) {
if (QFileInfo(name).suffix().isEmpty()) {
const QString sfx =
QMimeDatabase()
.mimeTypeForFile(
finalPath, QMimeDatabase::MatchContent)
.preferredSuffix();
if (!sfx.isEmpty() &&
QFile::rename(
finalPath, finalPath + QLatin1Char('.') + sfx))
finalPath += QLatin1Char('.') + sfx;
}
succeed(id, finalPath);
} else {
failWith(id, tr("Could not store download"));
}
}
finishUnit(id);
});
}
void DropImporter::succeed(int id, const QString& path) {
if (auto it = m_requests.find(id); it != m_requests.end())
it->files.append(path);
}
void DropImporter::failWith(int id, const QString& message) {
if (auto it = m_requests.find(id); it != m_requests.end())
it->errors.append(message);
}
void DropImporter::finishUnit(int id) {
auto it = m_requests.find(id);
if (it == m_requests.end() || --it->pending > 0) return;
const Request r = *it;
m_requests.erase(it);
emit importFinished(id, r.files, r.errors);
}
QString DropImporter::newSubdir() {
if (!m_tempDir.isValid()) return {};
const QString p = m_tempDir.filePath(QString::number(++m_counter));
return QDir().mkpath(p) ? p : QString();
}
} // namespace ZShell::Services
-62
View File
@@ -1,62 +0,0 @@
#pragma once
#include <QHash>
#include <QList>
#include <QNetworkAccessManager>
#include <QObject>
#include <QQmlEngine>
#include <QStringList>
#include <QTemporaryDir>
#include <QUrl>
namespace ZShell::Services {
class DropImporter : public QObject {
Q_OBJECT
QML_ELEMENT
QML_SINGLETON
public:
enum Decision { Ignore, InsertText, Attach };
Q_ENUM(Decision)
explicit DropImporter(QObject* parent = nullptr);
Q_INVOKABLE Decision decide(
const QList<QUrl>& urls,
const QString& text,
const QString& html,
bool hasRawImage) const;
Q_INVOKABLE int importAttachments(
const QList<QUrl>& urls,
const QString& html,
const QByteArray& rawImage);
signals:
void importFinished(
int id, const QStringList& paths, const QStringList& errors);
private:
struct Request {
int pending = 0;
QStringList files;
QStringList errors;
};
void addLocal(int id, const QUrl& url);
void download(int id, const QUrl& url);
void saveBytes(int id, const QByteArray& bytes);
void succeed(int id, const QString& path);
void failWith(int id, const QString& message);
void finishUnit(int id);
QString newSubdir();
QNetworkAccessManager m_nam;
QTemporaryDir m_tempDir;
QHash<int, Request> m_requests;
int m_nextId = 0;
int m_counter = 0;
};
} // namespace ZShell::Services
-1834
View File
File diff suppressed because it is too large Load Diff
+25 -53
View File
@@ -1,56 +1,28 @@
FROM archlinux:latest
RUN pacman -Syyu --noconfirm
RUN pacman -Syu --noconfirm \
&& pacman -S --needed --noconfirm \
base-devel cmake ninja clang lld nodejs tree-sitter \
jkqtplotter cmark-gfm git \
qt6-base qt6-declarative qt6-shadertools qt6-5compat \
qt6-canvaspainter qt6-wayland \
libqalculate pipewire aubio cava lm_sensors glib2 \
&& pacman -Scc --noconfirm
# Build toolchain
RUN pacman -S --noconfirm \
base-devel \
cmake \
ninja \
clang \
lld \
nodejs \
tree-sitter \
jkqtplotter \
cmark-gfm \
git
# Qt6
RUN pacman -S --noconfirm \
qt6-base \
qt6-declarative \
qt6-shadertools \
qt6-5compat \
qt6-canvaspainter
# Project-specific dependencies
RUN pacman -S --noconfirm \
libqalculate \
pipewire \
aubio \
cava \
lm_sensors \
glib2
# AUR: libcava (provides development headers for cava)
RUN pacman -Syu --noconfirm && \
pacman -S --needed --noconfirm \
sudo \
meson \
autoconf-archive \
python-tqdm && \
useradd -m builder && \
echo "builder ALL=(ALL) NOPASSWD: ALL" >> /etc/sudoers
RUN for i in 1 2 3 4 5; do \
rm -rf /tmp/libcava && \
git clone --depth 1 https://aur.archlinux.org/libcava.git /tmp/libcava && \
break || sleep 5; \
done && \
test -d /tmp/libcava/.git && \
chown -R builder /tmp/libcava
RUN su builder -c "cd /tmp/libcava && makepkg -si --noconfirm" && \
rm -rf /tmp/libcava && \
userdel -r builder && \
sed -i '/^builder/d' /etc/sudoers
RUN pacman -Syu --noconfirm \
&& pacman -S --needed --noconfirm \
sudo meson autoconf-archive python-tqdm \
&& useradd -m builder \
&& printf '%s\n' 'builder ALL=(ALL) NOPASSWD: ALL' > /etc/sudoers.d/builder \
&& chmod 0440 /etc/sudoers.d/builder \
&& for i in 1 2 3 4 5; do \
rm -rf /tmp/libcava; \
if git clone --depth 1 https://aur.archlinux.org/libcava.git /tmp/libcava; then break; fi; \
sleep 5; \
done \
&& test -d /tmp/libcava/.git \
&& chown -R builder:builder /tmp/libcava \
&& su builder -c "cd /tmp/libcava && makepkg -si --noconfirm" \
&& rm -rf /tmp/libcava /etc/sudoers.d/builder \
&& userdel -r builder \
&& pacman -Scc --noconfirm
+2
View File
@@ -1,5 +1,7 @@
//@ pragma UseQApplication
//@ pragma DefaultEnv QSG_RENDER_LOOP=threaded
// @ pragma DefaultEnv QSG_RHI_BACKEND=vulkan
//@ pragma DefaultEnv QSG_NO_VSYNC=1
//@ pragma DefaultEnv QS_NO_RELOAD_POPUP=1
//@ pragma DefaultEnv QT_QUICK_FLICKABLE_WHEEL_DECELERATION=10000
//@ pragma DropExpensiveFonts