Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0fc1a123fa | ||
|
|
672cb07547 | ||
|
|
e3b2d33a11 | ||
|
|
2a40696292 | ||
|
|
80ee714e91 | ||
|
|
2c572c3250 | ||
|
|
0912a8e40c | ||
|
|
0d17621180 | ||
|
|
0ae6d6fc55 | ||
|
|
9300da258e | ||
|
|
298cbbf424 | ||
|
|
b19b630a77 | ||
|
|
75f0f2071d | ||
|
|
bfea718547 | ||
|
|
5505ac19ba | ||
|
|
9d2dc281ec | ||
|
|
05c8851143 | ||
|
|
1cbe9a9f8a | ||
|
|
71b25bff49 | ||
|
|
87433334c7 | ||
|
|
6d5609fe5b | ||
|
|
ffefd3e202 | ||
|
|
2db6a57c0b | ||
|
|
0c5871e108 | ||
|
|
6ad9fe5e07 | ||
|
|
8c11aead18 | ||
|
|
d013be0f9a | ||
|
|
69774e7759 | ||
|
|
cdf66bff23 | ||
|
|
562770595f | ||
|
|
9126b04b10 | ||
|
|
55dab8f7ce | ||
|
|
c62695bfff | ||
|
|
2b06a33508 | ||
|
|
4d731c5a65 | ||
|
|
53836aec1a | ||
|
|
355106cb2b | ||
|
|
865b5bda53 | ||
|
|
2b89c8e4a1 | ||
|
|
61019204d8 | ||
|
|
951c31ab3d | ||
|
|
60831de7c5 | ||
|
|
38b9362515 | ||
|
|
deaeac4dbd | ||
|
|
157bdd8fd9 | ||
|
|
45350210cc | ||
|
|
85944ec966 | ||
|
|
3c9df82594 | ||
|
|
1cdcecae68 | ||
|
|
d2368c8c10 | ||
|
|
b8f79c51ae | ||
|
|
25e1610e2b | ||
|
|
49a65becff | ||
|
|
87c9f85ac7 | ||
|
|
532e5a0a03 | ||
|
|
dcc535d213 | ||
|
|
e347671edd | ||
|
|
d4e4152bdc | ||
|
|
728bdc85bc | ||
|
|
944694966b | ||
|
|
1e31e14de8 | ||
|
|
658be09da2 | ||
|
|
467e44bb9f | ||
|
|
fed9372b35 | ||
|
|
b2cddea636 | ||
|
|
396825d785 | ||
|
|
f439456ff4 | ||
|
|
4a18edfc78 | ||
|
|
3eecbc16bc | ||
|
|
437f935f73 | ||
|
|
b939224e9d | ||
|
|
644a1e0c78 | ||
|
|
3f7bbc01aa | ||
|
|
bc9d8af0fe | ||
|
|
01d521b42d | ||
|
|
7d65578a5f | ||
|
|
77ec1c87f8 | ||
|
|
f883b3d7fe | ||
|
|
e685500698 | ||
|
|
04481ae7e1 | ||
|
|
82a660229b | ||
|
|
ce6e314fde | ||
|
|
0b92cbb3b4 | ||
|
|
6a15be7b15 | ||
|
|
01c63ebead | ||
|
|
ecfa1115cd | ||
|
|
d08b2b7ac6 | ||
|
|
4df31d7564 | ||
|
|
5c6882d2a8 | ||
|
|
6a80961b2b | ||
|
|
2366901b24 | ||
|
|
ea4d5376f6 |
+3
-3
@@ -1,15 +1,15 @@
|
||||
---
|
||||
BasedOnStyle: LLVM
|
||||
AccessModifierOffset: 0
|
||||
AlignAfterOpenBracket: Align
|
||||
AlignAfterOpenBracket: AlwaysBreak
|
||||
AlignConsecutiveAssignments: false
|
||||
AlignConsecutiveDeclarations: false
|
||||
AlignOperands: Align
|
||||
AllowAllParametersOfDeclarationOnNextLine: true
|
||||
AllowShortFunctionsOnASingleLine: Inline
|
||||
AllowShortIfStatementsOnASingleLine: Never
|
||||
AllowShortIfStatementsOnASingleLine: WithoutElse
|
||||
AlwaysBreakBeforeMultilineStrings: false
|
||||
AlwaysBreakTemplateDeclarations: Yes
|
||||
AlwaysBreakTemplateDeclarations: MultiLine
|
||||
BinPackArguments: false
|
||||
BinPackParameters: false
|
||||
BreakBeforeBraces: Attach
|
||||
|
||||
+15
-19
@@ -2,29 +2,25 @@
|
||||
Checks: >
|
||||
-*,
|
||||
bugprone-*,
|
||||
-bugprone-throwing-static-initialization,
|
||||
-bugprone-invalid-enum-default-initialization,
|
||||
-bugprone-suspicious-include,
|
||||
-bugprone-branch-clone,
|
||||
-bugprone-easily-swappable-parameters,
|
||||
-bugprone-narrowing-conversions,
|
||||
-bugprone-implicit-widening-of-multiplication-result,
|
||||
clang-analyzer-*,
|
||||
modernize-*,
|
||||
-modernize-use-trailing-return-type,
|
||||
performance-*,
|
||||
readability-braces-around-statements,
|
||||
cppcoreguidelines-init-variables,
|
||||
misc-no-recursion,
|
||||
concurrency-mt-unsafe,
|
||||
readability-else-after-return,
|
||||
readability-identifier-naming,
|
||||
readability-make-member-function-const,
|
||||
readability-redundant-*,
|
||||
-readability-redundant-inline-specifier,
|
||||
readability-simplify-*,
|
||||
CheckOptions:
|
||||
readability-identifier-naming.ClassCase: CamelCase
|
||||
readability-identifier-naming.EnumCase: CamelCase
|
||||
readability-identifier-naming.FunctionCase: camelBack
|
||||
readability-identifier-naming.MemberCase: camelBack
|
||||
readability-identifier-naming.MemberPrefix: m_
|
||||
readability-identifier-naming.MethodCase: camelBack
|
||||
readability-identifier-naming.NamespaceCase: CamelCase
|
||||
readability-identifier-naming.ParameterCase: camelBack
|
||||
readability-identifier-naming.PrivateMemberPrefix: m_
|
||||
readability-identifier-naming.StaticConstantCase: UPPER_CASE
|
||||
readability-identifier-naming.StaticConstantPrefix: k
|
||||
readability-identifier-naming.VariableCase: camelBack
|
||||
WarningsAsErrors: "*"
|
||||
modernize-use-override,
|
||||
modernize-use-nullptr,
|
||||
modernize-deprecated-headers,
|
||||
HeaderFilterRegex: ".*"
|
||||
FormatStyle: file
|
||||
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
name: Rebuild CI Image
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: "0 6 * * 1"
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: alpine
|
||||
container:
|
||||
image: node:26-alpine
|
||||
env:
|
||||
IMAGE: git.aramjonghu.dev/aramjonghu/zshell-ci:latest
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install Docker CLI
|
||||
run: apk add --no-cache docker-cli
|
||||
|
||||
- name: Login to registry
|
||||
run: echo "${{ secrets.REGISTRY_TOKEN }}" | docker login git.aramjonghu.dev --username aramjonghu --password-stdin
|
||||
|
||||
- name: Build image
|
||||
run: docker build -t "$IMAGE" -f ci/Dockerfile .
|
||||
|
||||
- name: Push image
|
||||
run: docker push "$IMAGE"
|
||||
@@ -0,0 +1,62 @@
|
||||
name: C++
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
fmt:
|
||||
runs-on: alpine
|
||||
container:
|
||||
image: git.aramjonghu.dev/aramjonghu/zshell-ci:latest
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Format check
|
||||
run: |
|
||||
find . \( -name '*.cpp' -o -name '*.h' -o -name '*.hpp' \) \
|
||||
-not -path './build/*' \
|
||||
-exec clang-format -i --style=file {} +
|
||||
git diff --exit-code && echo "clang-format: passed"
|
||||
|
||||
build:
|
||||
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
|
||||
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
|
||||
|
||||
- name: clang-tidy
|
||||
run: |
|
||||
sed -i 's/-mno-direct-extern-access //g' build/compile_commands.json
|
||||
set -o pipefail
|
||||
run-clang-tidy -p build -j $(nproc) -header-filter="Plugins/.*" 2>&1 | \
|
||||
grep -v 'Suppressed\|non-user\|non-system\|unknown argument.*mno-direct\|Found compiler error\|warnings generated\.\|^Use -header-filter'
|
||||
@@ -1,10 +1,10 @@
|
||||
name: Lint & Format (JS/TS)
|
||||
name: JS/TS
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
lint-format:
|
||||
fmt:
|
||||
runs-on: alpine
|
||||
container: node:26-alpine
|
||||
|
||||
@@ -18,7 +18,6 @@ jobs:
|
||||
git
|
||||
|
||||
- name: Prettier
|
||||
continue-on-error: true
|
||||
run: |
|
||||
if [ -n "$(find . \( -iname "*.js" -o -iname "*.jsx" -o -iname "*.ts" -o -iname "*.tsx" -o -iname "*.mjs" -o -iname "*.cjs" \) -print -quit)" ]; then
|
||||
npx --yes prettier --check "**/*.{js,jsx,ts,tsx,mjs,cjs}" --ignore-path .prettierignore
|
||||
@@ -26,6 +25,19 @@ jobs:
|
||||
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
|
||||
@@ -1,85 +0,0 @@
|
||||
name: Lint & Format (Rust)
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
lint-format:
|
||||
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-${{ runner.os }}-build-${{ 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 \
|
||||
rust-clippy
|
||||
|
||||
- id: format-check
|
||||
name: Format check
|
||||
continue-on-error: true
|
||||
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
|
||||
|
||||
- id: clippy
|
||||
name: Clippy
|
||||
run: |
|
||||
if [ -n "$(find . -name "Cargo.toml" -print -quit)" ]; then
|
||||
status=0
|
||||
for manifest in $(find . -name "Cargo.toml"); do
|
||||
cargo clippy --manifest-path "$manifest" --all-targets --all-features -- -D warnings && \
|
||||
echo "$manifest: Clippy passed" || \
|
||||
{ echo "$manifest: Clippy failed"; 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
|
||||
|
||||
- name: Check results
|
||||
if: always()
|
||||
run: |
|
||||
if [ "${{ steps.format-check.outcome }}" = "failure" ] || [ "${{ steps.clippy.outcome }}" = "failure" ]; then
|
||||
echo "One or more checks failed"
|
||||
exit 1
|
||||
fi
|
||||
echo "All checks passed"
|
||||
@@ -4,7 +4,7 @@ on:
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
lint-format:
|
||||
fmt:
|
||||
runs-on: alpine
|
||||
container: node:26-alpine
|
||||
|
||||
@@ -23,11 +23,28 @@ jobs:
|
||||
pip install --no-cache-dir ruff
|
||||
|
||||
- name: Format check
|
||||
continue-on-error: true
|
||||
run: |
|
||||
. .venv/bin/activate
|
||||
ruff format --check .
|
||||
|
||||
lint:
|
||||
runs-on: alpine
|
||||
container: node:26-alpine
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install tools
|
||||
run: |
|
||||
apk add --no-cache \
|
||||
git \
|
||||
python3 \
|
||||
py3-pip
|
||||
python3 -m venv .venv
|
||||
. .venv/bin/activate
|
||||
pip install --no-cache-dir ruff
|
||||
|
||||
- name: Lint
|
||||
run: |
|
||||
. .venv/bin/activate
|
||||
@@ -63,3 +80,30 @@ jobs:
|
||||
. .venv/bin/activate
|
||||
cd cli
|
||||
python -m pytest tests/ -v
|
||||
|
||||
buildcheck:
|
||||
runs-on: alpine
|
||||
container: node:26-alpine
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install tools
|
||||
run: |
|
||||
apk add --no-cache \
|
||||
git \
|
||||
python3 \
|
||||
py3-pip \
|
||||
build-base \
|
||||
python3-dev \
|
||||
gcc \
|
||||
g++
|
||||
python3 -m venv .venv
|
||||
. .venv/bin/activate
|
||||
pip install --no-cache-dir nuitka
|
||||
|
||||
- name: Nuitka module check
|
||||
run: |
|
||||
. .venv/bin/activate
|
||||
nuitka --module --include-package=zshell cli/src/zshell/
|
||||
@@ -0,0 +1,152 @@
|
||||
name: Rust
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
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-build-${{ 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
|
||||
|
||||
- name: Cargo check
|
||||
run: |
|
||||
if [ -n "$(find . -name "Cargo.toml" -print -quit)" ]; then
|
||||
for manifest in $(find . -name "Cargo.toml"); do
|
||||
cargo check --manifest-path "$manifest" && \
|
||||
echo "$manifest: check passed" || \
|
||||
{ echo "$manifest: check failed"; exit 1; }
|
||||
done
|
||||
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
|
||||
|
||||
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
|
||||
status=0
|
||||
for manifest in $(find . -name "Cargo.toml"); do
|
||||
cargo clippy --manifest-path "$manifest" --all-targets --all-features -- -D warnings && \
|
||||
echo "$manifest: Clippy passed" || \
|
||||
{ echo "$manifest: Clippy failed"; 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
|
||||
+155
-10
@@ -1,37 +1,163 @@
|
||||
cmake_minimum_required(VERSION 3.19)
|
||||
|
||||
project(ZShell)
|
||||
if(NOT DEFINED VERSION)
|
||||
execute_process(
|
||||
COMMAND git describe --tags --abbrev=0
|
||||
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
|
||||
OUTPUT_VARIABLE GIT_LAST_TAG
|
||||
OUTPUT_STRIP_TRAILING_WHITESPACE
|
||||
ERROR_QUIET
|
||||
)
|
||||
|
||||
if (GIT_LAST_TAG)
|
||||
string(REGEX REPLACE "^v" "" GIT_LAST_TAG "${GIT_LAST_TAG}")
|
||||
|
||||
string(REPLACE "." ";" VERSION_LIST "${GIT_LAST_TAG}")
|
||||
list(GET VERSION_LIST 0 VERSION_MAJOR)
|
||||
list(GET VERSION_LIST 1 VERSION_MINOR)
|
||||
|
||||
execute_process(
|
||||
COMMAND git rev-list v${VERSION_MAJOR}.${VERSION_MINOR}.0..HEAD --count
|
||||
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
|
||||
OUTPUT_VARIABLE VERSION_PATCH
|
||||
OUTPUT_STRIP_TRAILING_WHITESPACE
|
||||
ERROR_QUIET
|
||||
)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
set(VERSION "${VERSION_MAJOR}.${VERSION_MINOR}.${VERSION_PATCH}")
|
||||
|
||||
project(ZShell VERSION ${VERSION_MAJOR}.${VERSION_MINOR}.${VERSION_PATCH})
|
||||
message(STATUS "ZShell version: ${VERSION}")
|
||||
|
||||
set(CMAKE_CXX_STANDARD 20)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
set(CMAKE_CXX_EXTENSIONS OFF)
|
||||
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/lib")
|
||||
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
|
||||
set(ENABLE_MODULES "plugin;shell" CACHE STRING "Modules to build/install")
|
||||
set(ENABLE_MODULES "plugin;shell;m3shapes" CACHE STRING "Modules to build/install")
|
||||
|
||||
set(INSTALL_LIBDIR "usr/lib/ZShell" CACHE STRING "Library install dir")
|
||||
set(INSTALL_QMLDIR "usr/lib/qt6/qml" CACHE STRING "QML install dir")
|
||||
set(INSTALL_QSCONFDIR "etc/xdg/quickshell/zshell" CACHE STRING "Quickshell config install dir")
|
||||
set(INSTALL_GREETERCONFDIR "etc/xdg/quickshell/zshell-greeter" CACHE STRING "Quickshell greeter install dir")
|
||||
|
||||
set(CMAKE_INSTALL_MESSAGE NEVER)
|
||||
|
||||
add_compile_options(
|
||||
-Wall -Wextra -Wpedantic -Wshadow -Wconversion
|
||||
-Wold-style-cast -Wnull-dereference -Wdouble-promotion
|
||||
-Wformat=2 -Wfloat-equal -Woverloaded-virtual
|
||||
-Wsign-conversion -Wredundant-decls -Wswitch
|
||||
-Wunreachable-code
|
||||
-Wall -Wextra -Wpedantic -Wshadow -Wconversion
|
||||
-Wold-style-cast -Wnull-dereference -Wdouble-promotion
|
||||
-Wformat=2 -Wfloat-equal -Woverloaded-virtual
|
||||
-Wsign-conversion -Wredundant-decls -Wswitch
|
||||
-Wunreachable-code
|
||||
)
|
||||
|
||||
|
||||
if("shell" IN_LIST ENABLE_MODULES)
|
||||
# Build settings index
|
||||
find_package(Python3 COMPONENTS Interpreter REQUIRED)
|
||||
set(SETTINGS_INDEX_JSON "${CMAKE_BINARY_DIR}/settings-index.json")
|
||||
execute_process(
|
||||
COMMAND ${Python3_EXECUTABLE}
|
||||
"${CMAKE_SOURCE_DIR}/scripts/build-settings-index.py"
|
||||
"${CMAKE_SOURCE_DIR}/Modules/Settings"
|
||||
"${SETTINGS_INDEX_JSON}"
|
||||
RESULT_VARIABLE SETTINGS_INDEX_RESULT
|
||||
)
|
||||
if(NOT SETTINGS_INDEX_RESULT EQUAL 0)
|
||||
message(FATAL_ERROR "Failed to build settings search index")
|
||||
endif()
|
||||
|
||||
# Nuitka compilation
|
||||
set(ZSHELL_CLI_BUILD_DIR "${CMAKE_BINARY_DIR}/zshell-cli")
|
||||
set(ZSHELL_CLI_DIST "${ZSHELL_CLI_BUILD_DIR}/zshell.dist")
|
||||
set(ZSHELL_CLI_SRC "${CMAKE_SOURCE_DIR}/cli/src/zshell")
|
||||
|
||||
find_program(NUITKA_EXECUTABLE nuitka)
|
||||
|
||||
if(NUITKA_EXECUTABLE)
|
||||
file(GLOB_RECURSE ZSHELL_CLI_SOURCES CONFIGURE_DEPENDS
|
||||
"${ZSHELL_CLI_SRC}/*.py"
|
||||
)
|
||||
file(GLOB_RECURSE ZSHELL_CLI_ASSETS CONFIGURE_DEPENDS
|
||||
"${ZSHELL_CLI_SRC}/assets/*"
|
||||
)
|
||||
|
||||
add_custom_command(
|
||||
OUTPUT "${ZSHELL_CLI_DIST}/zshell-cli"
|
||||
COMMAND ${CMAKE_COMMAND} -E make_directory "${ZSHELL_CLI_BUILD_DIR}"
|
||||
COMMAND ${CMAKE_COMMAND} -E rm -rf "${ZSHELL_CLI_DIST}"
|
||||
|
||||
COMMAND
|
||||
${NUITKA_EXECUTABLE}
|
||||
--standalone
|
||||
--include-data-dir=${CMAKE_SOURCE_DIR}/cli/src/zshell/assets=zshell/assets
|
||||
--output-dir=${ZSHELL_CLI_BUILD_DIR}
|
||||
--output-filename=zshell-cli
|
||||
${CMAKE_SOURCE_DIR}/cli/src/zshell/
|
||||
|
||||
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}/cli
|
||||
DEPENDS ${ZSHELL_CLI_SOURCES} ${ZSHELL_CLI_ASSETS}
|
||||
)
|
||||
|
||||
add_custom_target(zshell-cli ALL DEPENDS "${ZSHELL_CLI_DIST}/zshell-cli")
|
||||
|
||||
install(PROGRAMS "${ZSHELL_CLI_DIST}/zshell-cli" DESTINATION "${INSTALL_LIBDIR}/zshell-cli")
|
||||
install(DIRECTORY "${ZSHELL_CLI_DIST}/" DESTINATION "${INSTALL_LIBDIR}/zshell-cli" PATTERN "zshell-cli" EXCLUDE)
|
||||
|
||||
configure_file(
|
||||
"${CMAKE_SOURCE_DIR}/Plugins/cmake/zshell-cli.cmake.in"
|
||||
"${CMAKE_BINARY_DIR}/Plugins/cmake/zshell-cli.cmake"
|
||||
@ONLY
|
||||
)
|
||||
install(SCRIPT "${CMAKE_BINARY_DIR}/Plugins/cmake/zshell-cli.cmake")
|
||||
else()
|
||||
message(STATUS "Nuitka not found, building zshell-cli as a Python wheel instead")
|
||||
|
||||
find_package(Python3 COMPONENTS Interpreter REQUIRED)
|
||||
|
||||
set(ZSHELL_CLI_DIR "${CMAKE_SOURCE_DIR}")
|
||||
set(ZSHELL_CLI_DIST_DIR "${ZSHELL_CLI_DIR}/dist")
|
||||
set(ZSHELL_CLI_WHEEL_STAMP "${CMAKE_BINARY_DIR}/zshell-cli/wheel.stamp")
|
||||
|
||||
file(GLOB_RECURSE ZSHELL_CLI_SOURCES CONFIGURE_DEPENDS
|
||||
"${ZSHELL_CLI_DIR}/src/zshell/*.py"
|
||||
)
|
||||
file(GLOB_RECURSE ZSHELL_CLI_ASSETS CONFIGURE_DEPENDS
|
||||
"${ZSHELL_CLI_DIR}/src/zshell/assets/*"
|
||||
)
|
||||
|
||||
add_custom_command(
|
||||
OUTPUT "${ZSHELL_CLI_WHEEL_STAMP}"
|
||||
COMMAND ${CMAKE_COMMAND} -E make_directory "${CMAKE_BINARY_DIR}/zshell-cli"
|
||||
COMMAND ${CMAKE_COMMAND} -E rm -rf "${ZSHELL_CLI_DIST_DIR}"
|
||||
COMMAND ${Python3_EXECUTABLE} -m build --wheel --no-isolation
|
||||
COMMAND ${CMAKE_COMMAND} -E touch "${ZSHELL_CLI_WHEEL_STAMP}"
|
||||
WORKING_DIRECTORY "${ZSHELL_CLI_DIR}"
|
||||
DEPENDS ${ZSHELL_CLI_SOURCES} ${ZSHELL_CLI_ASSETS}
|
||||
COMMENT "Building zshell-cli python wheel"
|
||||
)
|
||||
|
||||
add_custom_target(zshell-cli ALL DEPENDS "${ZSHELL_CLI_WHEEL_STAMP}")
|
||||
|
||||
configure_file(
|
||||
"${CMAKE_SOURCE_DIR}/Plugins/cmake/zshell-cli-python.cmake.in"
|
||||
"${CMAKE_BINARY_DIR}/Plugins/cmake/zshell-cli-python.cmake"
|
||||
@ONLY
|
||||
)
|
||||
install(SCRIPT "${CMAKE_BINARY_DIR}/Plugins/cmake/zshell-cli-python.cmake")
|
||||
endif()
|
||||
endif()
|
||||
|
||||
if("plugin" IN_LIST ENABLE_MODULES)
|
||||
add_subdirectory(Plugins)
|
||||
|
||||
endif()
|
||||
|
||||
if("shell" IN_LIST ENABLE_MODULES)
|
||||
foreach(dir assets scripts Components Config Modules Daemons Drawers Effects Helpers Paths)
|
||||
install(DIRECTORY ${dir} DESTINATION "${INSTALL_QSCONFDIR}")
|
||||
endforeach()
|
||||
install(DIRECTORY ${dir} DESTINATION "${INSTALL_QSCONFDIR}")
|
||||
endforeach()
|
||||
|
||||
# Disable watching for changes
|
||||
file(READ shell.qml SHELL_QML)
|
||||
@@ -42,3 +168,22 @@ if("shell" IN_LIST ENABLE_MODULES)
|
||||
# Greeter
|
||||
install(DIRECTORY Greeter/ DESTINATION "${INSTALL_GREETERCONFDIR}")
|
||||
endif()
|
||||
|
||||
if("m3shapes" IN_LIST ENABLE_MODULES)
|
||||
message(STATUS "Fetching M3Shapes module")
|
||||
include(FetchContent)
|
||||
set(M3SHAPES_REV bdc327b29f95394a732baf3c9b19658ba23755b6)
|
||||
FetchContent_Declare(
|
||||
m3shapes_external
|
||||
GIT_REPOSITORY https://github.com/soramanew/m3shapes.git
|
||||
GIT_TAG ${M3SHAPES_REV}
|
||||
SOURCE_DIR "${CMAKE_BINARY_DIR}/_deps/m3shapes-${M3SHAPES_REV}"
|
||||
)
|
||||
FetchContent_MakeAvailable(m3shapes_external)
|
||||
message(STATUS "Done fetching M3Shapes module")
|
||||
|
||||
# Fix m3shapes wrong rpath
|
||||
if(TARGET m3shapesplugin)
|
||||
set_target_properties(m3shapesplugin PROPERTIES INSTALL_RPATH "$ORIGIN")
|
||||
endif()
|
||||
endif()
|
||||
|
||||
@@ -1,135 +0,0 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import qs.Config
|
||||
|
||||
ColumnLayout {
|
||||
id: root
|
||||
|
||||
default property alias content: contentColumn.data
|
||||
property string description: ""
|
||||
property bool expanded: false
|
||||
property bool nested: false
|
||||
property bool showBackground: false
|
||||
required property string title
|
||||
|
||||
signal toggleRequested
|
||||
|
||||
Layout.fillWidth: true
|
||||
spacing: Appearance.spacing.small
|
||||
|
||||
Item {
|
||||
id: sectionHeaderItem
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: Math.max(titleRow.implicitHeight + Appearance.padding.normal * 2, 48)
|
||||
|
||||
RowLayout {
|
||||
id: titleRow
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Appearance.padding.normal
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: Appearance.padding.normal
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Appearance.spacing.normal
|
||||
|
||||
CustomText {
|
||||
font.pointSize: Appearance.font.size.larger
|
||||
font.weight: 500
|
||||
text: root.title
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
MaterialIcon {
|
||||
color: DynamicColors.palette.m3onSurfaceVariant
|
||||
font.pointSize: Appearance.font.size.normal
|
||||
rotation: root.expanded ? 180 : 0
|
||||
text: "expand_more"
|
||||
|
||||
Behavior on rotation {
|
||||
Anim {
|
||||
duration: Appearance.anim.durations.small
|
||||
easing.bezierCurve: Appearance.anim.curves.standard
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StateLayer {
|
||||
anchors.fill: parent
|
||||
color: DynamicColors.palette.m3onSurface
|
||||
radius: Appearance.rounding.normal
|
||||
showHoverBackground: false
|
||||
|
||||
onClicked: {
|
||||
root.toggleRequested();
|
||||
root.expanded = !root.expanded;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
id: contentWrapper
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: root.expanded ? (contentColumn.implicitHeight + Appearance.spacing.small * 2) : 0
|
||||
clip: true
|
||||
|
||||
Behavior on Layout.preferredHeight {
|
||||
Anim {
|
||||
easing.bezierCurve: Appearance.anim.curves.standard
|
||||
}
|
||||
}
|
||||
|
||||
CustomRect {
|
||||
id: backgroundRect
|
||||
|
||||
anchors.fill: parent
|
||||
color: DynamicColors.transparency.enabled ? DynamicColors.layer(DynamicColors.palette.m3surfaceContainer, root.nested ? 3 : 2) : (root.nested ? DynamicColors.palette.m3surfaceContainerHigh : DynamicColors.palette.m3surfaceContainer)
|
||||
opacity: root.showBackground && root.expanded ? 1.0 : 0.0
|
||||
radius: Appearance.rounding.normal
|
||||
visible: root.showBackground
|
||||
|
||||
Behavior on opacity {
|
||||
Anim {
|
||||
easing.bezierCurve: Appearance.anim.curves.standard
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
id: contentColumn
|
||||
|
||||
anchors.bottomMargin: Appearance.spacing.small
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Appearance.padding.normal
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: Appearance.padding.normal
|
||||
opacity: root.expanded ? 1.0 : 0.0
|
||||
spacing: Appearance.spacing.small
|
||||
y: Appearance.spacing.small
|
||||
|
||||
Behavior on opacity {
|
||||
Anim {
|
||||
easing.bezierCurve: Appearance.anim.curves.standard
|
||||
}
|
||||
}
|
||||
|
||||
CustomText {
|
||||
id: descriptionText
|
||||
|
||||
Layout.bottomMargin: root.description !== "" ? Appearance.spacing.small : 0
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: root.description !== "" ? Appearance.spacing.smaller : 0
|
||||
color: DynamicColors.palette.m3onSurfaceVariant
|
||||
font.pointSize: Appearance.font.size.small
|
||||
text: root.description
|
||||
visible: root.description !== ""
|
||||
wrapMode: Text.Wrap
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,70 +0,0 @@
|
||||
import QtQuick
|
||||
import QtQuick.Templates
|
||||
import qs.Config
|
||||
|
||||
Slider {
|
||||
id: root
|
||||
|
||||
property color nonPeakColor: DynamicColors.tPalette.m3primary
|
||||
required property real peak
|
||||
property color peakColor: DynamicColors.palette.m3primary
|
||||
|
||||
background: Item {
|
||||
CustomRect {
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.bottomMargin: root.implicitHeight / 3
|
||||
anchors.left: parent.left
|
||||
anchors.top: parent.top
|
||||
anchors.topMargin: root.implicitHeight / 3
|
||||
bottomRightRadius: root.implicitHeight / 15
|
||||
color: root.nonPeakColor
|
||||
implicitWidth: root.handle.x - root.implicitHeight
|
||||
radius: Appearance.rounding.full
|
||||
topRightRadius: root.implicitHeight / 15
|
||||
|
||||
CustomRect {
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.left: parent.left
|
||||
anchors.top: parent.top
|
||||
bottomRightRadius: root.implicitHeight / 15
|
||||
color: root.peakColor
|
||||
implicitWidth: parent.width * root.peak
|
||||
radius: Appearance.rounding.full
|
||||
topRightRadius: root.implicitHeight / 15
|
||||
|
||||
Behavior on implicitWidth {
|
||||
Anim {
|
||||
duration: 50
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
CustomRect {
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.bottomMargin: root.implicitHeight / 3
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
anchors.topMargin: root.implicitHeight / 3
|
||||
bottomLeftRadius: root.implicitHeight / 15
|
||||
color: DynamicColors.tPalette.m3surfaceContainer
|
||||
implicitWidth: root.implicitWidth - root.handle.x - root.handle.implicitWidth - root.implicitHeight
|
||||
radius: Appearance.rounding.full
|
||||
topLeftRadius: root.implicitHeight / 15
|
||||
}
|
||||
}
|
||||
handle: CustomRect {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
color: DynamicColors.palette.m3primary
|
||||
implicitHeight: 15
|
||||
implicitWidth: 5
|
||||
radius: Appearance.rounding.full
|
||||
x: root.visualPosition * root.availableWidth - implicitWidth / 2
|
||||
|
||||
MouseArea {
|
||||
acceptedButtons: Qt.NoButton
|
||||
anchors.fill: parent
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,69 +0,0 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls.Basic
|
||||
|
||||
BusyIndicator {
|
||||
id: control
|
||||
|
||||
property int busySize: 64
|
||||
property color color: delegate.color
|
||||
|
||||
contentItem: Item {
|
||||
implicitHeight: control.busySize
|
||||
implicitWidth: control.busySize
|
||||
|
||||
Item {
|
||||
id: item
|
||||
|
||||
height: control.busySize
|
||||
opacity: control.running ? 1 : 0
|
||||
width: control.busySize
|
||||
x: parent.width / 2 - (control.busySize / 2)
|
||||
y: parent.height / 2 - (control.busySize / 2)
|
||||
|
||||
Behavior on opacity {
|
||||
OpacityAnimator {
|
||||
duration: 250
|
||||
}
|
||||
}
|
||||
|
||||
RotationAnimator {
|
||||
duration: 1250
|
||||
from: 0
|
||||
loops: Animation.Infinite
|
||||
running: control.visible && control.running
|
||||
target: item
|
||||
to: 360
|
||||
}
|
||||
|
||||
Repeater {
|
||||
id: repeater
|
||||
|
||||
model: 6
|
||||
|
||||
CustomRect {
|
||||
id: delegate
|
||||
|
||||
required property int index
|
||||
|
||||
color: control.color
|
||||
implicitHeight: 10
|
||||
implicitWidth: 10
|
||||
radius: 5
|
||||
x: item.width / 2 - width / 2
|
||||
y: item.height / 2 - height / 2
|
||||
|
||||
transform: [
|
||||
Translate {
|
||||
y: -Math.min(item.width, item.height) * 0.5 + 5
|
||||
},
|
||||
Rotation {
|
||||
angle: delegate.index / repeater.count * 360
|
||||
origin.x: 5
|
||||
origin.y: 5
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import qs.Config
|
||||
|
||||
Button {
|
||||
id: control
|
||||
|
||||
property color bgColor: DynamicColors.palette.m3primary
|
||||
property int radius: Appearance.rounding.smallest / 2
|
||||
property color textColor: DynamicColors.palette.m3onPrimary
|
||||
|
||||
background: CustomRect {
|
||||
color: control.bgColor
|
||||
opacity: control.enabled ? 1.0 : 0.5
|
||||
radius: control.radius
|
||||
}
|
||||
contentItem: CustomText {
|
||||
color: control.textColor
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
opacity: control.enabled ? 1.0 : 0.5
|
||||
text: control.text
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
}
|
||||
|
||||
StateLayer {
|
||||
radius: control.radius
|
||||
|
||||
onClicked: {
|
||||
control.clicked();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,169 +0,0 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import QtQuick.Controls
|
||||
import qs.Config
|
||||
|
||||
ComboBox {
|
||||
id: root
|
||||
|
||||
property int cornerRadius: Appearance.rounding.normal
|
||||
property int fieldHeight: 42
|
||||
property bool filled: true
|
||||
property real focusRingOpacity: 0.70
|
||||
property int hPadding: 16
|
||||
property int menuCornerRadius: 16
|
||||
property int menuRowHeight: 46
|
||||
property int menuVisibleRows: 7
|
||||
property bool preferPopupWindow: false
|
||||
|
||||
hoverEnabled: true
|
||||
implicitHeight: fieldHeight
|
||||
implicitWidth: 240
|
||||
spacing: 8
|
||||
|
||||
// ---------- Field background (filled/outlined + state layers + focus ring) ----------
|
||||
background: Item {
|
||||
anchors.fill: parent
|
||||
|
||||
CustomRect {
|
||||
id: container
|
||||
|
||||
anchors.fill: parent
|
||||
color: DynamicColors.palette.m3surfaceVariant
|
||||
radius: root.cornerRadius
|
||||
|
||||
StateLayer {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- Content ----------
|
||||
contentItem: RowLayout {
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: root.hPadding
|
||||
anchors.rightMargin: root.hPadding
|
||||
spacing: 12
|
||||
|
||||
// Display text
|
||||
CustomText {
|
||||
Layout.fillWidth: true
|
||||
color: root.enabled ? DynamicColors.palette.m3onSurface : DynamicColors.palette.m3onSurfaceVariant
|
||||
elide: Text.ElideRight
|
||||
font.pixelSize: 16
|
||||
font.weight: Font.Medium
|
||||
text: root.currentText
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
}
|
||||
|
||||
// Indicator chevron (simple, replace with your icon system)
|
||||
CustomText {
|
||||
color: root.enabled ? DynamicColors.palette.m3onSurfaceVariant : DynamicColors.palette.m3onSurfaceVariant
|
||||
rotation: root.popup.visible ? 180 : 0
|
||||
text: "▾"
|
||||
transformOrigin: Item.Center
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
|
||||
Behavior on rotation {
|
||||
NumberAnimation {
|
||||
duration: 140
|
||||
easing.type: Easing.OutCubic
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
popup: Popup {
|
||||
id: p
|
||||
|
||||
implicitHeight: list.contentItem.height + Appearance.padding.small * 2
|
||||
implicitWidth: root.width
|
||||
modal: true
|
||||
popupType: root.preferPopupWindow ? Popup.Window : Popup.Item
|
||||
y: -list.currentIndex * (root.menuRowHeight + Appearance.spacing.small) - Appearance.padding.small
|
||||
|
||||
background: CustomRect {
|
||||
color: DynamicColors.palette.m3surface
|
||||
radius: root.menuCornerRadius
|
||||
}
|
||||
contentItem: ListView {
|
||||
id: list
|
||||
|
||||
anchors.bottomMargin: Appearance.padding.small
|
||||
anchors.fill: parent
|
||||
anchors.topMargin: Appearance.padding.small
|
||||
clip: true
|
||||
currentIndex: root.currentIndex
|
||||
model: root.delegateModel
|
||||
spacing: Appearance.spacing.small
|
||||
|
||||
delegate: CustomRect {
|
||||
required property int index
|
||||
required property var modelData
|
||||
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
color: (index === root.currentIndex) ? DynamicColors.palette.m3primary : "transparent"
|
||||
implicitHeight: root.menuRowHeight
|
||||
implicitWidth: p.implicitWidth - Appearance.padding.small * 2
|
||||
radius: Appearance.rounding.normal - Appearance.padding.small
|
||||
|
||||
RowLayout {
|
||||
anchors.fill: parent
|
||||
spacing: 10
|
||||
|
||||
CustomText {
|
||||
Layout.fillWidth: true
|
||||
color: DynamicColors.palette.m3onSurface
|
||||
elide: Text.ElideRight
|
||||
font.pixelSize: 15
|
||||
text: modelData
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
}
|
||||
|
||||
CustomText {
|
||||
color: DynamicColors.palette.m3onSurfaceVariant
|
||||
text: "✓"
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
visible: index === root.currentIndex
|
||||
}
|
||||
}
|
||||
|
||||
StateLayer {
|
||||
onClicked: {
|
||||
root.currentIndex = index;
|
||||
p.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Expressive-ish open/close motion: subtle scale+fade (tune to taste). :contentReference[oaicite:5]{index=5}
|
||||
enter: Transition {
|
||||
Anim {
|
||||
from: 0
|
||||
property: "opacity"
|
||||
to: 1
|
||||
}
|
||||
|
||||
Anim {
|
||||
from: 0.98
|
||||
property: "scale"
|
||||
to: 1.0
|
||||
}
|
||||
}
|
||||
exit: Transition {
|
||||
Anim {
|
||||
from: 1
|
||||
property: "opacity"
|
||||
to: 0
|
||||
}
|
||||
}
|
||||
|
||||
Elevation {
|
||||
anchors.fill: parent
|
||||
level: 2
|
||||
radius: root.menuCornerRadius
|
||||
z: -1
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,12 +4,30 @@ import qs.Helpers
|
||||
Flickable {
|
||||
id: root
|
||||
|
||||
property bool doneFakeFlick
|
||||
|
||||
interactive: !Visibilities.getForActive().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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ ListView {
|
||||
|
||||
property bool doneFakeFlick
|
||||
|
||||
interactive: !Visibilities.getForActive().isDrawing
|
||||
interactive: !Visibilities.getForActive()?.isDrawing
|
||||
maximumFlickVelocity: 3000
|
||||
|
||||
rebound: Transition {
|
||||
|
||||
+97
-133
@@ -1,168 +1,132 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import QtQuick.Templates
|
||||
import qs.Config
|
||||
|
||||
RowLayout {
|
||||
DoubleSpinBox {
|
||||
id: root
|
||||
|
||||
property string displayText: root.value.toString()
|
||||
property bool isEditing: false
|
||||
property real max: Infinity
|
||||
property real min: -Infinity
|
||||
property alias repeatRate: timer.interval
|
||||
property real step: 1
|
||||
property real value
|
||||
property int cLayer: 1
|
||||
property int repeatDecay: 50
|
||||
property int repeatRate: 400
|
||||
|
||||
signal valueModified(value: real)
|
||||
function decrease(): void {
|
||||
let newValue = Math.max(from, value - stepSize);
|
||||
const decimals = stepSize < 1 ? Math.max(1, Math.ceil(-Math.log10(stepSize))) : 0;
|
||||
newValue = Math.round(newValue * Math.pow(10, decimals)) / Math.pow(10, decimals);
|
||||
value = newValue;
|
||||
valueModified();
|
||||
}
|
||||
|
||||
function increase(): void {
|
||||
let newValue = Math.min(to, value + stepSize);
|
||||
const decimals = stepSize < 1 ? Math.max(1, Math.ceil(-Math.log10(stepSize))) : 0;
|
||||
newValue = Math.round(newValue * Math.pow(10, decimals)) / Math.pow(10, decimals);
|
||||
value = newValue;
|
||||
valueModified();
|
||||
}
|
||||
|
||||
decimals: stepSize < 1 ? Math.max(1, Math.ceil(-Math.log10(stepSize))) : 0
|
||||
editable: true
|
||||
implicitHeight: Math.max(up.indicator.implicitHeight, down.indicator.implicitHeight, contentItem.implicitHeight) + topPadding + bottomPadding
|
||||
implicitWidth: contentItem.implicitWidth + leftPadding + rightPadding
|
||||
leftPadding: up.indicator.implicitWidth + Appearance.spacing.extraSmall / 2
|
||||
rightPadding: down.indicator.implicitWidth + Appearance.spacing.extraSmall / 2
|
||||
spacing: Appearance.spacing.small
|
||||
|
||||
onValueChanged: {
|
||||
if (!root.isEditing) {
|
||||
root.displayText = root.value.toString();
|
||||
}
|
||||
}
|
||||
|
||||
CustomTextField {
|
||||
id: textField
|
||||
|
||||
color: root.enabled ? DynamicColors.palette.m3onSurface : Qt.alpha(DynamicColors.palette.m3onSurface, 0.5)
|
||||
implicitHeight: upButton.implicitHeight
|
||||
contentItem: TextFieldBase {
|
||||
horizontalAlignment: TextField.AlignHCenter
|
||||
implicitWidth: 65
|
||||
inputMethodHints: Qt.ImhFormattedNumbersOnly
|
||||
leftPadding: Appearance.padding.normal
|
||||
padding: Appearance.padding.small
|
||||
rightPadding: Appearance.padding.normal
|
||||
text: root.isEditing ? text : root.displayText
|
||||
leftPadding: Appearance.padding.larger
|
||||
readOnly: !root.editable
|
||||
rightPadding: Appearance.padding.larger
|
||||
text: root.textFromValue(root.value, root.locale)
|
||||
validator: root.validator
|
||||
|
||||
background: CustomRect {
|
||||
color: root.enabled ? DynamicColors.tPalette.m3surfaceContainerHigh : DynamicColors.tPalette.m3surfaceContainerLow
|
||||
implicitWidth: 100
|
||||
radius: Appearance.rounding.full
|
||||
}
|
||||
validator: DoubleValidator {
|
||||
bottom: root.min
|
||||
decimals: root.step < 1 ? Math.max(1, Math.ceil(-Math.log10(root.step))) : 0
|
||||
top: root.max
|
||||
}
|
||||
|
||||
onAccepted: {
|
||||
const numValue = parseFloat(text);
|
||||
if (!isNaN(numValue)) {
|
||||
const clampedValue = Math.max(root.min, Math.min(root.max, numValue));
|
||||
root.value = clampedValue;
|
||||
root.displayText = clampedValue.toString();
|
||||
root.valueModified(clampedValue);
|
||||
} else {
|
||||
text = root.displayText;
|
||||
}
|
||||
root.isEditing = false;
|
||||
}
|
||||
onActiveFocusChanged: {
|
||||
if (activeFocus) {
|
||||
root.isEditing = true;
|
||||
} else {
|
||||
root.isEditing = false;
|
||||
root.displayText = root.value.toString();
|
||||
}
|
||||
}
|
||||
onEditingFinished: {
|
||||
if (text !== root.displayText) {
|
||||
const numValue = parseFloat(text);
|
||||
if (!isNaN(numValue)) {
|
||||
const clampedValue = Math.max(root.min, Math.min(root.max, numValue));
|
||||
root.value = clampedValue;
|
||||
root.displayText = clampedValue.toString();
|
||||
root.valueModified(clampedValue);
|
||||
} else {
|
||||
text = root.displayText;
|
||||
}
|
||||
}
|
||||
root.isEditing = false;
|
||||
color: DynamicColors.layer(DynamicColors.palette.m3surfaceContainerHighest, root.cLayer)
|
||||
radius: Appearance.rounding.extraSmall
|
||||
}
|
||||
}
|
||||
down.indicator: IconButton {
|
||||
id: downButton
|
||||
|
||||
CustomRect {
|
||||
bottomRightRadius: pressed ? Appearance.rounding.small : Appearance.rounding.extraSmall
|
||||
color: enabled ? DynamicColors.layer(DynamicColors.palette.m3surfaceContainerHighest, root.cLayer) : disabledColor
|
||||
disabledColor: Qt.alpha(DynamicColors.palette.m3surfaceContainerHighest, 0.4)
|
||||
icon: "remove"
|
||||
isRound: true
|
||||
label.anchors.horizontalCenterOffset: pressed ? 0 : 2
|
||||
padding: Appearance.padding.extraSmall
|
||||
topRightRadius: pressed ? Appearance.rounding.small : Appearance.rounding.extraSmall
|
||||
type: IconButton.Text
|
||||
|
||||
Behavior on bottomRightRadius {
|
||||
Anim {
|
||||
type: Anim.DefaultEffects
|
||||
}
|
||||
}
|
||||
Behavior on label.anchors.horizontalCenterOffset {
|
||||
Anim {
|
||||
type: Anim.DefaultEffects
|
||||
}
|
||||
}
|
||||
Behavior on topRightRadius {
|
||||
Anim {
|
||||
type: Anim.DefaultEffects
|
||||
}
|
||||
}
|
||||
}
|
||||
up.indicator: IconButton {
|
||||
id: upButton
|
||||
|
||||
color: root.enabled ? DynamicColors.palette.m3primary : DynamicColors.layer(DynamicColors.palette.m3surfaceContainerHighest, 1)
|
||||
implicitHeight: upIcon.implicitHeight + Appearance.padding.small * 2
|
||||
implicitWidth: implicitHeight
|
||||
radius: Appearance.rounding.full
|
||||
anchors.right: parent.right
|
||||
bottomLeftRadius: pressed ? Appearance.rounding.small : Appearance.rounding.extraSmall
|
||||
color: enabled ? DynamicColors.layer(DynamicColors.palette.m3surfaceContainerHighest, root.cLayer) : disabledColor
|
||||
disabledColor: Qt.alpha(DynamicColors.palette.m3surfaceContainerHighest, 0.4)
|
||||
icon: "add"
|
||||
isRound: true
|
||||
label.anchors.horizontalCenterOffset: pressed ? 0 : -2
|
||||
padding: Appearance.padding.extraSmall
|
||||
topLeftRadius: pressed ? Appearance.rounding.small : Appearance.rounding.extraSmall
|
||||
type: IconButton.Text
|
||||
|
||||
StateLayer {
|
||||
id: upState
|
||||
|
||||
color: DynamicColors.palette.m3onPrimary
|
||||
|
||||
onClicked: {
|
||||
let newValue = Math.min(root.max, root.value + root.step);
|
||||
// Round to avoid floating point precision errors
|
||||
const decimals = root.step < 1 ? Math.max(1, Math.ceil(-Math.log10(root.step))) : 0;
|
||||
newValue = Math.round(newValue * Math.pow(10, decimals)) / Math.pow(10, decimals);
|
||||
root.value = newValue;
|
||||
root.displayText = newValue.toString();
|
||||
root.valueModified(newValue);
|
||||
Behavior on bottomLeftRadius {
|
||||
Anim {
|
||||
type: Anim.DefaultEffects
|
||||
}
|
||||
onPressAndHold: timer.start()
|
||||
onReleased: timer.stop()
|
||||
}
|
||||
|
||||
MaterialIcon {
|
||||
id: upIcon
|
||||
|
||||
anchors.centerIn: parent
|
||||
color: root.enabled ? DynamicColors.palette.m3onPrimary : Qt.alpha(DynamicColors.palette.m3onSurface, 0.5)
|
||||
text: "keyboard_arrow_up"
|
||||
}
|
||||
}
|
||||
|
||||
CustomRect {
|
||||
color: root.enabled ? DynamicColors.palette.m3primary : DynamicColors.layer(DynamicColors.palette.m3surfaceContainerHighest, 1)
|
||||
implicitHeight: downIcon.implicitHeight + Appearance.padding.small * 2
|
||||
implicitWidth: implicitHeight
|
||||
radius: Appearance.rounding.full
|
||||
|
||||
StateLayer {
|
||||
id: downState
|
||||
|
||||
color: DynamicColors.palette.m3onPrimary
|
||||
|
||||
onClicked: {
|
||||
let newValue = Math.max(root.min, root.value - root.step);
|
||||
// Round to avoid floating point precision errors
|
||||
const decimals = root.step < 1 ? Math.max(1, Math.ceil(-Math.log10(root.step))) : 0;
|
||||
newValue = Math.round(newValue * Math.pow(10, decimals)) / Math.pow(10, decimals);
|
||||
root.value = newValue;
|
||||
root.displayText = newValue.toString();
|
||||
root.valueModified(newValue);
|
||||
Behavior on label.anchors.horizontalCenterOffset {
|
||||
Anim {
|
||||
type: Anim.DefaultEffects
|
||||
}
|
||||
onPressAndHold: timer.start()
|
||||
onReleased: timer.stop()
|
||||
}
|
||||
|
||||
MaterialIcon {
|
||||
id: downIcon
|
||||
|
||||
anchors.centerIn: parent
|
||||
color: root.enabled ? DynamicColors.palette.m3onPrimary : Qt.alpha(DynamicColors.palette.m3onSurface, 0.5)
|
||||
text: "keyboard_arrow_down"
|
||||
Behavior on topLeftRadius {
|
||||
Anim {
|
||||
type: Anim.DefaultEffects
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: timer
|
||||
|
||||
interval: 100
|
||||
interval: root.repeatRate
|
||||
repeat: true
|
||||
running: upButton.pressed || downButton.pressed
|
||||
triggeredOnStart: true
|
||||
|
||||
onRunningChanged: {
|
||||
if (!running)
|
||||
interval = root.repeatRate;
|
||||
}
|
||||
onTriggered: {
|
||||
if (upState.pressed)
|
||||
upState.onClicked();
|
||||
else if (downState.pressed)
|
||||
downState.onClicked();
|
||||
if (upButton.pressed)
|
||||
root.increase();
|
||||
else if (downButton.pressed)
|
||||
root.decrease();
|
||||
if (interval > root.repeatDecay)
|
||||
interval -= root.repeatDecay;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+267
-49
@@ -1,77 +1,295 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Shapes
|
||||
import qs.Config
|
||||
|
||||
TextField {
|
||||
TextFieldBase {
|
||||
id: root
|
||||
|
||||
property int cursorHeight: root.height
|
||||
enum TextFieldType {
|
||||
Outlined,
|
||||
Filled
|
||||
}
|
||||
|
||||
background: null
|
||||
color: DynamicColors.palette.m3onSurface
|
||||
cursorVisible: !readOnly
|
||||
font.family: Appearance.font.family.sans
|
||||
font.pointSize: Appearance.font.size.smaller
|
||||
placeholderTextColor: DynamicColors.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
|
||||
property bool emptyIsValid: true
|
||||
property string errorText
|
||||
readonly property int filledOffset: type === CustomTextField.Filled ? Appearance.spacing.small : 0
|
||||
readonly property int horizontalPadding: Appearance.padding.large
|
||||
property bool isError
|
||||
property string leadingIcon
|
||||
readonly property int leadingOffset: leadingIcon ? leadingIconLoader.width + leadingIconLoader.anchors.leftMargin : 0
|
||||
readonly property real outlineGap: placeholder.width * root.smallFontScale + Appearance.spacing.extraSmall * 2
|
||||
property real outlineGapScale: activeFocus || text ? 1 : 0
|
||||
property int radius: Appearance.rounding.small
|
||||
readonly property real smallFontScale: smallFontSize / font.pointSize
|
||||
property int smallFontSize: Appearance.font.size.small
|
||||
property string supportingText
|
||||
readonly property int supportingTextOffset: effectiveSupportingText ? supportingTextLoader.height + Appearance.spacing.extraSmall : 0
|
||||
property string trailingIcon
|
||||
readonly property int trailingOffset: trailingIcon ? trailingIconLoader.width + trailingIconLoader.anchors.rightMargin : 0
|
||||
property int type: CustomTextField.Outlined
|
||||
readonly property bool valid: !validate || (!text && emptyIsValid) || (validate instanceof RegExp ? validate.test(text) : !!validate(text))
|
||||
property var validate // Regex or function
|
||||
|
||||
Behavior on color {
|
||||
CAnim {
|
||||
bottomPadding: Appearance.padding.large + supportingTextOffset - filledOffset
|
||||
leftPadding: horizontalPadding + leadingOffset
|
||||
rightPadding: horizontalPadding + trailingOffset
|
||||
topPadding: Appearance.padding.large + 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
|
||||
}
|
||||
}
|
||||
cursorDelegate: CustomRect {
|
||||
id: cursor
|
||||
|
||||
property bool disableBlink
|
||||
|
||||
color: DynamicColors.palette.m3primary
|
||||
implicitWidth: 2
|
||||
radius: Appearance.rounding.normal
|
||||
|
||||
Behavior on opacity {
|
||||
Anim {
|
||||
duration: Appearance.anim.durations.small
|
||||
}
|
||||
Behavior on outlineGapScale {
|
||||
Anim {
|
||||
type: Anim.DefaultEffects
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
function onCursorPositionChanged(): void {
|
||||
if (root.activeFocus && root.cursorVisible) {
|
||||
cursor.opacity = 1;
|
||||
cursor.disableBlink = true;
|
||||
enableBlink.restart();
|
||||
onEditingFinished: {
|
||||
if (!valid)
|
||||
isError = true;
|
||||
}
|
||||
onPressed: {
|
||||
if (stateLayer.enabled)
|
||||
stateLayer.press(stateLayer.mouseX, stateLayer.mouseY);
|
||||
}
|
||||
onTextEdited: {
|
||||
if (isError)
|
||||
isError = false;
|
||||
}
|
||||
|
||||
Item {
|
||||
id: contentWrapper
|
||||
|
||||
anchors.bottomMargin: root.supportingTextOffset
|
||||
anchors.fill: parent
|
||||
|
||||
CustomText {
|
||||
id: placeholder
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: root.leftPadding
|
||||
anchors.topMargin: Appearance.padding.extraSmall
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
color: root.isError ? DynamicColors.palette.m3error : (root.activeFocus ? DynamicColors.palette.m3primary : root.text ? DynamicColors.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 + -Appearance.spacing.extraSmall
|
||||
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: Appearance.anim.durations.expressiveEffects
|
||||
easing: Appearance.anim.curves.expressiveDefaultEffects
|
||||
}
|
||||
}
|
||||
|
||||
target: root
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: enableBlink
|
||||
Loader {
|
||||
id: leadingIconLoader
|
||||
|
||||
interval: 100
|
||||
active: root.leadingIcon
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Appearance.padding.larger
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
onTriggered: cursor.disableBlink = false
|
||||
sourceComponent: MaterialIcon {
|
||||
color: DynamicColors.palette.m3onSurfaceVariant
|
||||
font.pointSize: Appearance.font.size.large
|
||||
text: root.leadingIcon
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
interval: 500
|
||||
repeat: true
|
||||
running: root.activeFocus && root.cursorVisible && !cursor.disableBlink
|
||||
triggeredOnStart: true
|
||||
Loader {
|
||||
id: trailingIconLoader
|
||||
|
||||
onTriggered: parent.opacity = parent.opacity === 1 ? 0 : 1
|
||||
}
|
||||
active: root.trailingIcon
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: Appearance.padding.larger
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
Binding {
|
||||
cursor.opacity: 0
|
||||
when: !root.activeFocus || !root.cursorVisible
|
||||
sourceComponent: MaterialIcon {
|
||||
color: DynamicColors.palette.m3onSurfaceVariant
|
||||
font.pointSize: Appearance.font.size.large
|
||||
text: root.trailingIcon
|
||||
}
|
||||
}
|
||||
}
|
||||
Behavior on placeholderTextColor {
|
||||
CAnim {
|
||||
|
||||
Loader {
|
||||
id: supportingTextLoader
|
||||
|
||||
active: root.effectiveSupportingText
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: root.horizontalPadding
|
||||
|
||||
sourceComponent: CustomText {
|
||||
color: root.isError ? DynamicColors.palette.m3error : DynamicColors.palette.m3onSurfaceVariant
|
||||
font.pointSize: Appearance.font.size.small
|
||||
text: root.effectiveSupportingText
|
||||
}
|
||||
}
|
||||
|
||||
TapHandler {
|
||||
id: tapHandler
|
||||
}
|
||||
|
||||
Component {
|
||||
id: outlineComp
|
||||
|
||||
Shape {
|
||||
id: bg
|
||||
|
||||
asynchronous: true
|
||||
preferredRendererType: Shape.CurveRenderer
|
||||
|
||||
ShapePath {
|
||||
capStyle: ShapePath.RoundCap
|
||||
fillColor: "transparent"
|
||||
startX: root.horizontalPadding - root.clampedRadius + root.outlineGap * (1 - root.outlineGapScale) / 2 + root.outlineGap * root.outlineGapScale
|
||||
strokeColor: root.isError ? DynamicColors.palette.m3error : (root.activeFocus ? DynamicColors.palette.m3primary : DynamicColors.palette.m3outline)
|
||||
strokeWidth: root.activeFocus ? 2 : 1
|
||||
|
||||
Behavior on strokeColor {
|
||||
CAnim {
|
||||
}
|
||||
}
|
||||
Behavior on strokeWidth {
|
||||
Anim {
|
||||
}
|
||||
}
|
||||
|
||||
PathLine {
|
||||
x: bg.width - root.clampedRadius
|
||||
}
|
||||
|
||||
PathArc {
|
||||
radiusX: root.clampedRadius
|
||||
radiusY: root.clampedRadius
|
||||
x: bg.width
|
||||
y: root.clampedRadius
|
||||
}
|
||||
|
||||
PathLine {
|
||||
x: bg.width
|
||||
y: bg.height - root.clampedRadius
|
||||
}
|
||||
|
||||
PathArc {
|
||||
radiusX: root.clampedRadius
|
||||
radiusY: root.clampedRadius
|
||||
x: bg.width - root.clampedRadius
|
||||
y: bg.height
|
||||
}
|
||||
|
||||
PathLine {
|
||||
x: root.clampedRadius
|
||||
y: bg.height
|
||||
}
|
||||
|
||||
PathArc {
|
||||
radiusX: root.clampedRadius
|
||||
radiusY: root.clampedRadius
|
||||
x: 0
|
||||
y: bg.height - root.clampedRadius
|
||||
}
|
||||
|
||||
PathLine {
|
||||
x: 0
|
||||
y: root.clampedRadius
|
||||
}
|
||||
|
||||
PathArc {
|
||||
radiusX: root.clampedRadius
|
||||
radiusY: root.clampedRadius
|
||||
x: root.clampedRadius
|
||||
y: 0
|
||||
}
|
||||
|
||||
PathLine {
|
||||
x: root.horizontalPadding - root.clampedRadius + root.outlineGap * (1 - root.outlineGapScale) / 2
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: filledComp
|
||||
|
||||
CustomRect {
|
||||
color: root.activeFocus ? DynamicColors.tPalette.m3surfaceContainerHighest : DynamicColors.tPalette.m3surfaceContainerHigh
|
||||
topLeftRadius: root.clampedRadius
|
||||
topRightRadius: root.clampedRadius
|
||||
|
||||
CustomRect {
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
color: root.isError ? DynamicColors.palette.m3error : (root.activeFocus ? DynamicColors.palette.m3primary : DynamicColors.palette.m3outline)
|
||||
implicitHeight: root.activeFocus ? 2 : 1
|
||||
|
||||
Behavior on implicitHeight {
|
||||
Anim {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import qs.Components
|
||||
|
||||
ToolTip {
|
||||
id: root
|
||||
|
||||
property bool alternativeVisibleCondition: false
|
||||
property bool extraVisibleCondition: true
|
||||
readonly property bool internalVisibleCondition: (extraVisibleCondition && (parent.hovered === undefined || parent?.hovered)) || alternativeVisibleCondition
|
||||
|
||||
background: null
|
||||
horizontalPadding: 10
|
||||
verticalPadding: 5
|
||||
visible: internalVisibleCondition
|
||||
|
||||
contentItem: CustomTooltipContent {
|
||||
id: contentItem
|
||||
|
||||
horizontalPadding: root.horizontalPadding
|
||||
shown: root.internalVisibleCondition
|
||||
text: root.text
|
||||
verticalPadding: root.verticalPadding
|
||||
}
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
import QtQuick
|
||||
import qs.Components
|
||||
import qs.Config
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property real horizontalPadding: 10
|
||||
property bool isVisible: backgroundRectangle.implicitHeight > 0
|
||||
property bool shown: false
|
||||
required property string text
|
||||
property real verticalPadding: 5
|
||||
|
||||
implicitHeight: tooltipTextObject.implicitHeight + 2 * root.verticalPadding
|
||||
implicitWidth: tooltipTextObject.implicitWidth + 2 * root.horizontalPadding
|
||||
|
||||
Rectangle {
|
||||
id: backgroundRectangle
|
||||
|
||||
clip: true
|
||||
color: DynamicColors.tPalette.m3inverseSurface ?? "#3C4043"
|
||||
implicitHeight: shown ? (tooltipTextObject.implicitHeight + 2 * root.verticalPadding) : 0
|
||||
implicitWidth: shown ? (tooltipTextObject.implicitWidth + 2 * root.horizontalPadding) : 0
|
||||
opacity: shown ? 1 : 0
|
||||
radius: Appearance.rounding.smallest
|
||||
|
||||
Behavior on implicitHeight {
|
||||
Anim {
|
||||
}
|
||||
}
|
||||
Behavior on implicitWidth {
|
||||
Anim {
|
||||
}
|
||||
}
|
||||
Behavior on opacity {
|
||||
Anim {
|
||||
}
|
||||
}
|
||||
|
||||
anchors {
|
||||
bottom: root.bottom
|
||||
horizontalCenter: root.horizontalCenter
|
||||
}
|
||||
|
||||
CustomText {
|
||||
id: tooltipTextObject
|
||||
|
||||
anchors.centerIn: parent
|
||||
color: DynamicColors.palette.m3inverseOnSurface ?? "#FFFFFF"
|
||||
text: root.text
|
||||
wrapMode: Text.Wrap
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
import qs.Config
|
||||
import QtQuick
|
||||
|
||||
CustomRect {
|
||||
required property int extra
|
||||
|
||||
anchors.margins: 8
|
||||
anchors.right: parent.right
|
||||
color: DynamicColors.palette.m3tertiary
|
||||
implicitHeight: count.implicitHeight + 4 * 2
|
||||
implicitWidth: count.implicitWidth + 8 * 2
|
||||
opacity: extra > 0 ? 1 : 0
|
||||
radius: Appearance.rounding.smallest
|
||||
scale: extra > 0 ? 1 : 0.5
|
||||
|
||||
Behavior on opacity {
|
||||
Anim {
|
||||
duration: MaterialEasing.expressiveEffectsTime
|
||||
}
|
||||
}
|
||||
Behavior on scale {
|
||||
Anim {
|
||||
duration: MaterialEasing.expressiveEffectsTime
|
||||
easing.bezierCurve: MaterialEasing.expressiveEffects
|
||||
}
|
||||
}
|
||||
|
||||
Elevation {
|
||||
anchors.fill: parent
|
||||
level: 2
|
||||
opacity: parent.opacity
|
||||
radius: parent.radius
|
||||
z: -1
|
||||
}
|
||||
|
||||
CustomText {
|
||||
id: count
|
||||
|
||||
anchors.centerIn: parent
|
||||
animate: parent.opacity > 0
|
||||
color: DynamicColors.palette.m3onTertiary
|
||||
text: qsTr("+%1").arg(parent.extra)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import qs.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()
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import qs.Config
|
||||
|
||||
IconButton {
|
||||
id: root
|
||||
|
||||
required property bool shouldBeVisible
|
||||
|
||||
opacity: 0
|
||||
scale: 0
|
||||
visible: root.scale > 0
|
||||
|
||||
Behavior on opacity {
|
||||
Anim {
|
||||
duration: Appearance.anim.durations.small
|
||||
}
|
||||
}
|
||||
Behavior on scale {
|
||||
Anim {
|
||||
}
|
||||
}
|
||||
|
||||
onShouldBeVisibleChanged: {
|
||||
if (root.shouldBeVisible) {
|
||||
root.opacity = 1;
|
||||
root.scale = 1;
|
||||
} else {
|
||||
root.opacity = 0;
|
||||
root.scale = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -63,6 +63,7 @@ ButtonBase {
|
||||
id: label
|
||||
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
Layout.topMargin: -1
|
||||
color: root.onColor
|
||||
font: root.font
|
||||
}
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import M3Shapes
|
||||
import qs.Config
|
||||
|
||||
MaterialShape {
|
||||
id: root
|
||||
|
||||
property bool animated: true
|
||||
property real cRotation
|
||||
property bool containsIcon
|
||||
property real dampingRatio: 0.6
|
||||
property real lRotation
|
||||
property int morphAnimRotation: 60
|
||||
property real morphScale: 0.14
|
||||
property alias rotateAnimDuration: rotateAnim.duration
|
||||
property int shapeIndex
|
||||
property list<int> shapes: {
|
||||
if (containsIcon)
|
||||
return [MaterialShape.SoftBurst, MaterialShape.Cookie9Sided, MaterialShape.Pill, MaterialShape.Sunny, MaterialShape.Cookie4Sided, MaterialShape.Oval];
|
||||
return [MaterialShape.SoftBurst, MaterialShape.Cookie9Sided, MaterialShape.Pentagon, MaterialShape.Pill, MaterialShape.Sunny, MaterialShape.Cookie4Sided, MaterialShape.Oval];
|
||||
}
|
||||
readonly property real springDuration: {
|
||||
const wn = Math.sqrt(stiffness);
|
||||
const r = -dampingRatio * wn;
|
||||
const c = 1 / Math.sqrt(1 - dampingRatio * dampingRatio);
|
||||
return Math.log(visibilityThreshold / c) / r;
|
||||
}
|
||||
readonly property real springMaxVelocity: {
|
||||
const wn = Math.sqrt(stiffness);
|
||||
const factor = Math.exp(-z * Math.acos(z) / Math.sqrt(1 - z * z));
|
||||
return wn * factor;
|
||||
}
|
||||
property bool springSettled: true
|
||||
property real stiffness: 180
|
||||
property real thisLRotation
|
||||
property real visibilityThreshold: 0.075
|
||||
|
||||
function spring(t: real): var {
|
||||
const wn = Math.sqrt(stiffness);
|
||||
const za = dampingRatio * wn;
|
||||
|
||||
const wd = wn * Math.sqrt(1 - dampingRatio * dampingRatio);
|
||||
const r = za / wd;
|
||||
const pos = 1 - Math.exp(-za * t) * (Math.cos(wd * t) + r * Math.sin(wd * t));
|
||||
const vel = Math.exp(-za * t) * (wn * wn / wd) * Math.sin(wd * t);
|
||||
|
||||
return [pos, vel];
|
||||
}
|
||||
|
||||
color: DynamicColors.palette.m3primary
|
||||
implicitSize: 38
|
||||
toShape: shapes[0]
|
||||
|
||||
RotationAnimation on cRotation {
|
||||
id: rotateAnim
|
||||
|
||||
duration: 4666
|
||||
easing.type: Easing.Linear
|
||||
from: 0
|
||||
loops: Animation.Infinite
|
||||
running: root.animated
|
||||
to: 360
|
||||
}
|
||||
Behavior on color {
|
||||
CAnim {
|
||||
}
|
||||
}
|
||||
|
||||
ElapsedTimer {
|
||||
id: timer
|
||||
}
|
||||
|
||||
FrameAnimation {
|
||||
running: root.animated && !root.springSettled
|
||||
|
||||
onTriggered: {
|
||||
const t = timer.elapsed();
|
||||
|
||||
if (t >= root.springDuration) {
|
||||
root.springSettled = true;
|
||||
} else {
|
||||
const [pos, vel] = root.spring(t);
|
||||
root.morphProgress = Math.min(1, pos); // Overshooting the morph looks weird
|
||||
root.thisLRotation = pos * root.morphAnimRotation;
|
||||
root.scale = 1 + vel * root.morphScale / root.springMaxVelocity;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
interval: 650
|
||||
repeat: true
|
||||
running: root.animated
|
||||
triggeredOnStart: true
|
||||
|
||||
onTriggered: {
|
||||
root.beginBatchUpdate();
|
||||
root.fromShape = root.toShape;
|
||||
root.shapeIndex = (root.shapeIndex + 1) % root.shapes.length;
|
||||
root.toShape = root.shapes[root.shapeIndex];
|
||||
root.morphProgress = 0;
|
||||
|
||||
root.rotation = root.rotation;
|
||||
root.lRotation = (root.lRotation + root.thisLRotation) % 360;
|
||||
root.thisLRotation = 0;
|
||||
root.rotation = Qt.binding(() => root.cRotation + root.lRotation + root.thisLRotation);
|
||||
|
||||
root.springSettled = false;
|
||||
timer.restart();
|
||||
root.endBatchUpdate();
|
||||
}
|
||||
}
|
||||
}
|
||||
+2
-1
@@ -30,8 +30,9 @@ MouseArea {
|
||||
signal itemSelected(item: MenuItem)
|
||||
|
||||
anchors.fill: parent
|
||||
cursorShape: undefined
|
||||
cursorShape: expanded ? Qt.ArrowCursor : undefined
|
||||
enabled: expanded
|
||||
hoverEnabled: expanded
|
||||
layer.enabled: opacity < 1
|
||||
opacity: expanded ? 1 : 0
|
||||
parent: {
|
||||
|
||||
@@ -1,76 +0,0 @@
|
||||
import QtQuick
|
||||
|
||||
Path {
|
||||
id: root
|
||||
|
||||
required property real viewHeight
|
||||
required property real viewWidth
|
||||
|
||||
startX: root.viewWidth / 2
|
||||
startY: 0
|
||||
|
||||
PathAttribute {
|
||||
name: "itemOpacity"
|
||||
value: 0.25
|
||||
}
|
||||
|
||||
PathLine {
|
||||
x: root.viewWidth / 2
|
||||
y: root.viewHeight * (1 / 6)
|
||||
}
|
||||
|
||||
PathAttribute {
|
||||
name: "itemOpacity"
|
||||
value: 0.45
|
||||
}
|
||||
|
||||
PathLine {
|
||||
x: root.viewWidth / 2
|
||||
y: root.viewHeight * (2 / 6)
|
||||
}
|
||||
|
||||
PathAttribute {
|
||||
name: "itemOpacity"
|
||||
value: 0.70
|
||||
}
|
||||
|
||||
PathLine {
|
||||
x: root.viewWidth / 2
|
||||
y: root.viewHeight * (3 / 6)
|
||||
}
|
||||
|
||||
PathAttribute {
|
||||
name: "itemOpacity"
|
||||
value: 1.00
|
||||
}
|
||||
|
||||
PathLine {
|
||||
x: root.viewWidth / 2
|
||||
y: root.viewHeight * (4 / 6)
|
||||
}
|
||||
|
||||
PathAttribute {
|
||||
name: "itemOpacity"
|
||||
value: 0.70
|
||||
}
|
||||
|
||||
PathLine {
|
||||
x: root.viewWidth / 2
|
||||
y: root.viewHeight * (5 / 6)
|
||||
}
|
||||
|
||||
PathAttribute {
|
||||
name: "itemOpacity"
|
||||
value: 0.45
|
||||
}
|
||||
|
||||
PathLine {
|
||||
x: root.viewWidth / 2
|
||||
y: root.viewHeight
|
||||
}
|
||||
|
||||
PathAttribute {
|
||||
name: "itemOpacity"
|
||||
value: 0.25
|
||||
}
|
||||
}
|
||||
@@ -1,180 +0,0 @@
|
||||
import QtQuick
|
||||
import QtQuick.Effects
|
||||
import qs.Config
|
||||
|
||||
Elevation {
|
||||
id: root
|
||||
|
||||
required property int currentIndex
|
||||
property bool expanded
|
||||
required property int from
|
||||
property color insideTextColor: DynamicColors.palette.m3onPrimary
|
||||
property int itemHeight
|
||||
property int listHeight: 200
|
||||
property color outsideTextColor: DynamicColors.palette.m3onSurfaceVariant
|
||||
readonly property var spinnerModel: root.range(root.from, root.to)
|
||||
required property int to
|
||||
property Item triggerItem
|
||||
|
||||
signal itemSelected(item: int)
|
||||
|
||||
function range(first, last) {
|
||||
let out = [];
|
||||
for (let i = first; i <= last; ++i)
|
||||
out.push(i);
|
||||
return out;
|
||||
}
|
||||
|
||||
implicitHeight: root.expanded ? view.implicitHeight : 0
|
||||
level: root.expanded ? 2 : 0
|
||||
radius: itemHeight / 2
|
||||
visible: implicitHeight > 0
|
||||
z: root.expanded ? 100 : 0
|
||||
|
||||
Behavior on implicitHeight {
|
||||
Anim {
|
||||
}
|
||||
}
|
||||
|
||||
onExpandedChanged: {
|
||||
if (!root.expanded)
|
||||
root.itemSelected(view.currentIndex + 1);
|
||||
}
|
||||
|
||||
Component {
|
||||
id: spinnerDelegate
|
||||
|
||||
Item {
|
||||
id: wrapper
|
||||
|
||||
readonly property color delegateTextColor: wrapper.PathView.view ? wrapper.PathView.view.delegateTextColor : "white"
|
||||
required property var modelData
|
||||
|
||||
height: root.itemHeight
|
||||
opacity: wrapper.PathView.itemOpacity
|
||||
visible: wrapper.PathView.onPath
|
||||
width: wrapper.PathView.view ? wrapper.PathView.view.width : 0
|
||||
z: wrapper.PathView.isCurrentItem ? 100 : Math.round(wrapper.PathView.itemScale * 100)
|
||||
|
||||
CustomText {
|
||||
anchors.centerIn: parent
|
||||
color: wrapper.delegateTextColor
|
||||
font.pointSize: Appearance.font.size.large
|
||||
text: wrapper.modelData
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
CustomClippingRect {
|
||||
anchors.fill: parent
|
||||
color: DynamicColors.palette.m3surfaceContainer
|
||||
radius: parent.radius
|
||||
z: root.z
|
||||
|
||||
// Main visible spinner: normal/outside text color
|
||||
PathView {
|
||||
id: view
|
||||
|
||||
property color delegateTextColor: root.outsideTextColor
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
clip: true
|
||||
currentIndex: root.currentIndex - 1
|
||||
delegate: spinnerDelegate
|
||||
dragMargin: width
|
||||
highlightRangeMode: PathView.StrictlyEnforceRange
|
||||
implicitHeight: root.listHeight
|
||||
model: root.spinnerModel
|
||||
pathItemCount: 7
|
||||
preferredHighlightBegin: 0.5
|
||||
preferredHighlightEnd: 0.5
|
||||
snapMode: PathView.SnapToItem
|
||||
|
||||
path: PathMenu {
|
||||
viewHeight: view.height
|
||||
viewWidth: view.width
|
||||
}
|
||||
}
|
||||
|
||||
// The selection rectangle itself
|
||||
CustomRect {
|
||||
id: selectionRect
|
||||
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
color: DynamicColors.palette.m3primary
|
||||
height: root.itemHeight
|
||||
radius: root.itemHeight / 2
|
||||
width: parent.width
|
||||
z: 2
|
||||
}
|
||||
|
||||
// Hidden source: same PathView, but with the "inside selection" text color
|
||||
Item {
|
||||
id: selectedTextSource
|
||||
|
||||
anchors.fill: parent
|
||||
layer.enabled: true
|
||||
visible: false
|
||||
|
||||
PathView {
|
||||
id: selectedTextView
|
||||
|
||||
property color delegateTextColor: root.insideTextColor
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
clip: true
|
||||
currentIndex: view.currentIndex
|
||||
delegate: spinnerDelegate
|
||||
dragMargin: view.dragMargin
|
||||
highlightRangeMode: view.highlightRangeMode
|
||||
implicitHeight: root.listHeight
|
||||
interactive: false
|
||||
model: view.model
|
||||
|
||||
// Keep this PathView visually locked to the real one
|
||||
offset: view.offset
|
||||
pathItemCount: view.pathItemCount
|
||||
preferredHighlightBegin: view.preferredHighlightBegin
|
||||
preferredHighlightEnd: view.preferredHighlightEnd
|
||||
snapMode: view.snapMode
|
||||
|
||||
path: PathMenu {
|
||||
viewHeight: selectedTextView.height
|
||||
viewWidth: selectedTextView.width
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Mask matching the selection rectangle
|
||||
Item {
|
||||
id: selectionMask
|
||||
|
||||
anchors.fill: parent
|
||||
layer.enabled: true
|
||||
visible: false
|
||||
|
||||
CustomRect {
|
||||
color: "white"
|
||||
height: selectionRect.height
|
||||
radius: selectionRect.radius
|
||||
width: selectionRect.width
|
||||
x: selectionRect.x
|
||||
y: selectionRect.y
|
||||
}
|
||||
}
|
||||
|
||||
// Only show the "inside selection" text where the mask exists
|
||||
MultiEffect {
|
||||
anchors.fill: selectedTextSource
|
||||
maskEnabled: true
|
||||
maskInverted: false
|
||||
maskSource: selectionMask
|
||||
source: selectedTextSource
|
||||
z: 3
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
import QtQuick
|
||||
import qs.Config
|
||||
|
||||
TextFieldBase {
|
||||
id: root
|
||||
|
||||
readonly property alias bg: bg
|
||||
readonly property alias clearIcon: clearIcon
|
||||
readonly property alias searchIcon: searchIcon
|
||||
|
||||
bottomPadding: Appearance.padding.large
|
||||
leftPadding: searchIcon.width + searchIcon.anchors.leftMargin + Appearance.spacing.small
|
||||
rightPadding: clearIcon.width + clearIcon.anchors.rightMargin + Appearance.spacing.small
|
||||
topPadding: Appearance.padding.large
|
||||
|
||||
background: CustomRect {
|
||||
id: bg
|
||||
|
||||
anchors.fill: parent
|
||||
color: DynamicColors.tPalette.m3surfaceContainer
|
||||
radius: Appearance.rounding.full
|
||||
|
||||
StateLayer {
|
||||
id: stateLayer
|
||||
|
||||
cursorShape: Qt.IBeamCursor
|
||||
enabled: !root.activeFocus
|
||||
manualPressOverride: tapHandler.pressed
|
||||
|
||||
onClicked: root.focus = true
|
||||
}
|
||||
}
|
||||
|
||||
onPressed: {
|
||||
if (stateLayer.enabled)
|
||||
stateLayer.press(stateLayer.mouseX, stateLayer.mouseY);
|
||||
}
|
||||
|
||||
CustomText {
|
||||
id: placeholder
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: root.leftPadding
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
color: root.placeholderTextColor
|
||||
font: root.font
|
||||
opacity: root.text ? 0 : 1
|
||||
text: root.placeholderText
|
||||
|
||||
Behavior on opacity {
|
||||
Anim {
|
||||
type: Anim.DefaultEffects
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MaterialIcon {
|
||||
id: searchIcon
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Appearance.padding.large
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
color: DynamicColors.palette.m3onSurfaceVariant
|
||||
font.pointSize: Appearance.font.size.large
|
||||
text: "search"
|
||||
}
|
||||
|
||||
IconButton {
|
||||
id: clearIcon
|
||||
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: Appearance.padding.larger
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
enabled: root.text
|
||||
icon: "clear"
|
||||
opacity: root.text ? 1 : 0
|
||||
radius: Appearance.rounding.full
|
||||
radiusMorph: false
|
||||
stateLayer.hoverEnabled: enabled
|
||||
type: IconButton.Text
|
||||
|
||||
Behavior on opacity {
|
||||
Anim {
|
||||
type: Anim.DefaultEffects
|
||||
}
|
||||
}
|
||||
|
||||
onClicked: root.clear()
|
||||
}
|
||||
|
||||
TapHandler {
|
||||
id: tapHandler
|
||||
}
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import qs.Config
|
||||
|
||||
CustomRect {
|
||||
id: root
|
||||
|
||||
required property string label
|
||||
required property real max
|
||||
required property real min
|
||||
property var onValueModified: function (value) {}
|
||||
property real step: 1
|
||||
required property real value
|
||||
|
||||
Layout.fillWidth: true
|
||||
color: DynamicColors.layer(DynamicColors.palette.m3surfaceContainer, 2)
|
||||
implicitHeight: row.implicitHeight + Appearance.padding.large * 2
|
||||
radius: Appearance.rounding.normal
|
||||
|
||||
Behavior on implicitHeight {
|
||||
Anim {
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
id: row
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.margins: Appearance.padding.large
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Appearance.spacing.normal
|
||||
|
||||
CustomText {
|
||||
Layout.fillWidth: true
|
||||
text: root.label
|
||||
}
|
||||
|
||||
CustomSpinBox {
|
||||
max: root.max
|
||||
min: root.min
|
||||
step: root.step
|
||||
value: root.value
|
||||
|
||||
onValueModified: value => {
|
||||
root.onValueModified(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -20,6 +20,7 @@ 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 +28,7 @@ MouseArea {
|
||||
readonly property alias rect: base
|
||||
property bool shapeMorph
|
||||
property bool showHoverBackground: true
|
||||
property real stateOpacity: containsMouse ? 0.08 : 0
|
||||
property real stateOpacity: containsMouse || manualHoverOverride ? 0.08 : 0
|
||||
property alias topLeftRadius: base.topLeftRadius
|
||||
property alias topRightRadius: base.topRightRadius
|
||||
|
||||
@@ -51,7 +52,7 @@ MouseArea {
|
||||
|
||||
anchors.fill: parent
|
||||
cursorShape: !enabled ? undefined : Qt.PointingHandCursor
|
||||
enabled: parent.enabled && !Visibilities.getForActive().isDrawing
|
||||
enabled: parent.enabled && !Visibilities.getForActive()?.isDrawing
|
||||
hoverEnabled: true
|
||||
|
||||
Behavior on stateOpacity {
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
import QtQuick
|
||||
import QtQuick.Templates
|
||||
import qs.Config
|
||||
|
||||
TextField {
|
||||
id: root
|
||||
|
||||
color: DynamicColors.palette.m3onSurface
|
||||
cursorVisible: !readOnly
|
||||
font.pointSize: Appearance.font.size.small
|
||||
implicitHeight: contentHeight + topPadding + bottomPadding
|
||||
implicitWidth: contentWidth + leftPadding + rightPadding
|
||||
placeholderTextColor: DynamicColors.palette.m3onSurfaceVariant // No anim cause placeholder is custom
|
||||
renderType: echoMode === TextField.Password ? TextField.QtRendering : TextField.NativeRendering
|
||||
selectedTextColor: color
|
||||
selectionColor: Qt.alpha(DynamicColors.palette.m3primary, 0.4)
|
||||
verticalAlignment: TextInput.AlignVCenter
|
||||
|
||||
Behavior on color {
|
||||
CAnim {
|
||||
}
|
||||
}
|
||||
cursorDelegate: Item {
|
||||
}
|
||||
Behavior on selectionColor {
|
||||
CAnim {
|
||||
}
|
||||
}
|
||||
|
||||
CustomRect {
|
||||
id: cursor
|
||||
|
||||
property bool disableBlink
|
||||
|
||||
color: DynamicColors.palette.m3primary
|
||||
implicitHeight: root.cursorRectangle.height
|
||||
implicitWidth: 1.5
|
||||
radius: Appearance.rounding.large
|
||||
x: root.cursorRectangle.x
|
||||
y: root.cursorRectangle.y
|
||||
|
||||
Behavior on opacity {
|
||||
Anim {
|
||||
type: Anim.StandardSmall
|
||||
}
|
||||
}
|
||||
Behavior on x {
|
||||
Anim {
|
||||
duration: Appearance.anim.durations.expressiveFastEffects
|
||||
easing.bezierCurve: [0.2, 1, 0.21, 1, 1, 1]
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
function onCursorPositionChanged(): void {
|
||||
if (root.activeFocus && root.cursorVisible) {
|
||||
cursor.opacity = 1;
|
||||
cursor.disableBlink = true;
|
||||
enableBlink.restart();
|
||||
}
|
||||
}
|
||||
|
||||
target: root
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: enableBlink
|
||||
|
||||
interval: 500
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import qs.Effects
|
||||
|
||||
CustomFlickable {
|
||||
id: root
|
||||
|
||||
property real bottomFadeOpacity: fadeShouldBeActive(false) ? 0 : 1
|
||||
property real fadeAmount: 0.1
|
||||
property real topFadeOpacity: fadeShouldBeActive(true) ? 0 : 1
|
||||
|
||||
function fadeShouldBeActive(isStart: bool): bool {
|
||||
// When content is smaller than flickable size, hide fade when rebound starts
|
||||
if (contentHeight + topMargin + bottomMargin < height && rebound.running && ((isStart ? verticalOvershoot > 0 : verticalOvershoot < 0)))
|
||||
return false;
|
||||
|
||||
if (isStart)
|
||||
return visibleArea.yPosition > 0;
|
||||
return visibleArea.yPosition + visibleArea.heightRatio < 1;
|
||||
}
|
||||
|
||||
flickableDirection: Flickable.VerticalFlick
|
||||
layer.enabled: true
|
||||
|
||||
Behavior on bottomFadeOpacity {
|
||||
Anim {
|
||||
type: Anim.SlowEffects
|
||||
}
|
||||
}
|
||||
layer.effect: Mask {
|
||||
maskSource: mask
|
||||
|
||||
Rectangle {
|
||||
id: mask
|
||||
|
||||
anchors.fill: parent
|
||||
layer.enabled: true
|
||||
visible: false
|
||||
|
||||
gradient: Gradient {
|
||||
orientation: Gradient.Vertical
|
||||
|
||||
GradientStop {
|
||||
color: Qt.rgba(0, 0, 0, root.topFadeOpacity)
|
||||
position: 0
|
||||
}
|
||||
|
||||
GradientStop {
|
||||
color: Qt.rgba(0, 0, 0, 1)
|
||||
position: root.fadeAmount
|
||||
}
|
||||
|
||||
GradientStop {
|
||||
color: Qt.rgba(0, 0, 0, 1)
|
||||
position: 1 - root.fadeAmount
|
||||
}
|
||||
|
||||
GradientStop {
|
||||
color: Qt.rgba(0, 0, 0, root.bottomFadeOpacity)
|
||||
position: 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Behavior on topFadeOpacity {
|
||||
Anim {
|
||||
type: Anim.SlowEffects
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import qs.Effects
|
||||
|
||||
CustomListView {
|
||||
id: root
|
||||
|
||||
property real endFadeOpacity: fadeShouldBeActive(false) ? 0 : 1
|
||||
property real fadeAmount: 0.1
|
||||
readonly property bool horizontal: orientation === ListView.Horizontal
|
||||
property real startFadeOpacity: fadeShouldBeActive(true) ? 0 : 1
|
||||
|
||||
function contentSize(): real {
|
||||
return horizontal ? contentWidth : contentHeight;
|
||||
}
|
||||
|
||||
function fadeShouldBeActive(isStart: bool): bool {
|
||||
// When content is smaller than flickable size, hide fade when rebound starts.
|
||||
if (contentSize() + marginStart() + marginEnd() < viewportSize() && rebound.running && ((isStart ? overshootStart() > 0 : overshootStart() < 0))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (isStart)
|
||||
return visibleStart() > 0;
|
||||
|
||||
return visibleStart() + visibleRatio() < 1;
|
||||
}
|
||||
|
||||
function marginEnd(): real {
|
||||
return horizontal ? rightMargin : bottomMargin;
|
||||
}
|
||||
|
||||
function marginStart(): real {
|
||||
return horizontal ? leftMargin : topMargin;
|
||||
}
|
||||
|
||||
function overshootStart(): real {
|
||||
return horizontal ? horizontalOvershoot : verticalOvershoot;
|
||||
}
|
||||
|
||||
function viewportSize(): real {
|
||||
return horizontal ? width : height;
|
||||
}
|
||||
|
||||
function visibleRatio(): real {
|
||||
return horizontal ? visibleArea.widthRatio : visibleArea.heightRatio;
|
||||
}
|
||||
|
||||
function visibleStart(): real {
|
||||
return horizontal ? visibleArea.xPosition : visibleArea.yPosition;
|
||||
}
|
||||
|
||||
flickableDirection: horizontal ? Flickable.HorizontalFlick : Flickable.VerticalFlick
|
||||
layer.enabled: true
|
||||
|
||||
Behavior on endFadeOpacity {
|
||||
Anim {
|
||||
type: Anim.SlowEffects
|
||||
}
|
||||
}
|
||||
layer.effect: Mask {
|
||||
maskSource: mask
|
||||
|
||||
Rectangle {
|
||||
id: mask
|
||||
|
||||
anchors.fill: parent
|
||||
layer.enabled: true
|
||||
visible: false
|
||||
|
||||
gradient: Gradient {
|
||||
orientation: root.horizontal ? Gradient.Horizontal : Gradient.Vertical
|
||||
|
||||
GradientStop {
|
||||
color: Qt.rgba(0, 0, 0, root.startFadeOpacity)
|
||||
position: 0
|
||||
}
|
||||
|
||||
GradientStop {
|
||||
color: Qt.rgba(0, 0, 0, 1)
|
||||
position: root.fadeAmount
|
||||
}
|
||||
|
||||
GradientStop {
|
||||
color: Qt.rgba(0, 0, 0, 1)
|
||||
position: 1 - root.fadeAmount
|
||||
}
|
||||
|
||||
GradientStop {
|
||||
color: Qt.rgba(0, 0, 0, root.endFadeOpacity)
|
||||
position: 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Behavior on startFadeOpacity {
|
||||
Anim {
|
||||
type: Anim.SlowEffects
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import QtQuick
|
||||
import QtQuick.Shapes
|
||||
import qs.Config
|
||||
|
||||
Shape {
|
||||
id: root
|
||||
|
||||
property real amplitude: 3
|
||||
property color color: DynamicColors.palette.m3surfaceContainer
|
||||
readonly property real waveHeight: amplitude * 2
|
||||
property int waves: 4
|
||||
|
||||
asynchronous: true
|
||||
preferredRendererType: Shape.CurveRenderer
|
||||
|
||||
ShapePath {
|
||||
fillColor: root.color
|
||||
strokeColor: "transparent"
|
||||
strokeWidth: 0
|
||||
|
||||
Behavior on fillColor {
|
||||
CAnim {
|
||||
}
|
||||
}
|
||||
|
||||
PathSvg {
|
||||
path: {
|
||||
const w = root.width;
|
||||
const h = root.height;
|
||||
const a = root.amplitude;
|
||||
const n = Math.max(1, root.waves);
|
||||
const wl = w / n;
|
||||
const half = wl / 2;
|
||||
|
||||
let d = `M 0,${a} `;
|
||||
for (let i = 0; i < n; ++i) {
|
||||
const x = i * wl;
|
||||
d += `Q ${x + half / 2},${-a} ${x + half},${a} `;
|
||||
d += `Q ${x + half + half / 2},${3 * a} ${x + wl},${a} `;
|
||||
}
|
||||
d += `L ${w},${h} L 0,${h} Z`;
|
||||
return d;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -72,6 +72,14 @@ JsonObject {
|
||||
property bool upower: true
|
||||
}
|
||||
component Tray: JsonObject {
|
||||
property bool recolorIcons: false
|
||||
property bool showAudio: true
|
||||
property bool showBluetooth: false
|
||||
property bool showMicrophone: true
|
||||
property bool showNetwork: false
|
||||
property bool showOnHover: false
|
||||
property bool showPower: true
|
||||
property bool showWifi: false
|
||||
property int trayIconSize: 24
|
||||
}
|
||||
}
|
||||
|
||||
+48
-359
@@ -12,7 +12,7 @@ Singleton {
|
||||
|
||||
property alias appearance: adapter.appearance
|
||||
property alias background: adapter.background
|
||||
property alias barConfig: adapter.barConfig
|
||||
property alias bar: adapter.bar
|
||||
property alias clipboard: adapter.clipboard
|
||||
property alias colors: adapter.colors
|
||||
property alias dashboard: adapter.dashboard
|
||||
@@ -20,6 +20,7 @@ Singleton {
|
||||
property alias general: adapter.general
|
||||
property alias launcher: adapter.launcher
|
||||
property alias lock: adapter.lock
|
||||
property bool migratingLegacyBar: false
|
||||
property alias notifs: adapter.notifs
|
||||
property alias osd: adapter.osd
|
||||
property alias overview: adapter.overview
|
||||
@@ -29,6 +30,41 @@ Singleton {
|
||||
property alias sidebar: adapter.sidebar
|
||||
property alias utilities: adapter.utilities
|
||||
|
||||
function applyLegacyBarConfig(legacy): void {
|
||||
const tray = legacy.tray ?? {};
|
||||
const popouts = legacy.popouts ?? {};
|
||||
|
||||
bar.autoHide = pick(legacy.autoHide, bar.autoHide);
|
||||
bar.hideWhenNotif = pick(legacy.hideWhenNotif, bar.hideWhenNotif);
|
||||
bar.rounding = pick(legacy.rounding, bar.rounding);
|
||||
bar.border = pick(legacy.border, bar.border);
|
||||
bar.smoothing = pick(legacy.smoothing, bar.smoothing);
|
||||
bar.height = pick(legacy.height, bar.height);
|
||||
bar.tray.trayIconSize = pick(tray.trayIconSize, bar.tray.trayIconSize);
|
||||
bar.popouts.tray = pick(popouts.tray, bar.popouts.tray);
|
||||
bar.popouts.audio = pick(popouts.audio, bar.popouts.audio);
|
||||
bar.popouts.activeWindow = pick(popouts.activeWindow, bar.popouts.activeWindow);
|
||||
bar.popouts.resources = pick(popouts.resources, bar.popouts.resources);
|
||||
bar.popouts.clock = pick(popouts.clock, bar.popouts.clock);
|
||||
bar.popouts.network = pick(popouts.network, bar.popouts.network);
|
||||
bar.popouts.upower = pick(popouts.upower, bar.popouts.upower);
|
||||
bar.entries = pick(legacy.entries, bar.entries);
|
||||
}
|
||||
|
||||
function migrateLegacyBarConfig(raw): bool {
|
||||
if (raw.bar !== undefined || raw.barConfig === undefined)
|
||||
return false;
|
||||
|
||||
migratingLegacyBar = true;
|
||||
applyLegacyBarConfig(raw.barConfig);
|
||||
migratingLegacyBar = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
function pick(value, fallback) {
|
||||
return value === undefined ? fallback : value;
|
||||
}
|
||||
|
||||
function save(): void {
|
||||
saveTimer.restart();
|
||||
recentlySaved = true;
|
||||
@@ -39,348 +75,6 @@ Singleton {
|
||||
saveTimer.restart();
|
||||
}
|
||||
|
||||
function serializeAppearance(): var {
|
||||
return {
|
||||
rounding: {
|
||||
scale: appearance.rounding.scale
|
||||
},
|
||||
spacing: {
|
||||
scale: appearance.spacing.scale
|
||||
},
|
||||
padding: {
|
||||
scale: appearance.padding.scale
|
||||
},
|
||||
deform: {
|
||||
scale: appearance.deform.scale
|
||||
},
|
||||
font: {
|
||||
family: {
|
||||
sans: appearance.font.family.sans,
|
||||
mono: appearance.font.family.mono,
|
||||
material: appearance.font.family.material,
|
||||
clock: appearance.font.family.clock
|
||||
},
|
||||
size: {
|
||||
scale: appearance.font.size.scale
|
||||
}
|
||||
},
|
||||
anim: {
|
||||
mediaGifSpeedAdjustment: appearance.anim.mediaGifSpeedAdjustment,
|
||||
sessionGifSpeed: appearance.anim.sessionGifSpeed,
|
||||
durations: {
|
||||
scale: appearance.anim.durations.scale
|
||||
}
|
||||
},
|
||||
transparency: {
|
||||
enabled: appearance.transparency.enabled,
|
||||
base: appearance.transparency.base,
|
||||
layers: appearance.transparency.layers
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function serializeBackground(): var {
|
||||
return {
|
||||
wallFadeDuration: background.wallFadeDuration,
|
||||
enabled: background.enabled,
|
||||
alignX: background.alignX,
|
||||
sourceClipX: background.sourceClipX,
|
||||
sourceClipY: background.sourceClipY,
|
||||
sourceClipW: background.sourceClipW,
|
||||
sourceClipH: background.sourceClipH,
|
||||
alignY: background.alignY,
|
||||
zoom: background.zoom
|
||||
};
|
||||
}
|
||||
|
||||
function serializeBar(): var {
|
||||
return {
|
||||
autoHide: barConfig.autoHide,
|
||||
hideWhenNotif: barConfig.hideWhenNotif,
|
||||
rounding: barConfig.rounding,
|
||||
border: barConfig.border,
|
||||
smoothing: barConfig.smoothing,
|
||||
height: barConfig.height,
|
||||
tray: {
|
||||
trayIconSize: barConfig.tray.trayIconSize
|
||||
},
|
||||
popouts: {
|
||||
tray: barConfig.popouts.tray,
|
||||
audio: barConfig.popouts.audio,
|
||||
activeWindow: barConfig.popouts.activeWindow,
|
||||
resources: barConfig.popouts.resources,
|
||||
clock: barConfig.popouts.clock,
|
||||
network: barConfig.popouts.network,
|
||||
upower: barConfig.popouts.upower
|
||||
},
|
||||
entries: barConfig.entries
|
||||
};
|
||||
}
|
||||
|
||||
function serializeClipboard(): var {
|
||||
return {
|
||||
enabled: clipboard.enabled,
|
||||
maxEntriesShown: clipboard.maxEntriesShown,
|
||||
sizes: {
|
||||
width: clipboard.sizes.width,
|
||||
previewWidth: clipboard.sizes.previewWidth,
|
||||
minPreviewWidth: clipboard.sizes.minPreviewWidth,
|
||||
itemHeight: clipboard.sizes.itemHeight
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function serializeColors(): var {
|
||||
return {
|
||||
schemeType: colors.schemeType,
|
||||
presets: {
|
||||
name: colors.presets.name,
|
||||
variant: colors.presets.variant,
|
||||
accent: colors.presets.accent
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function serializeConfig(): var {
|
||||
return {
|
||||
barConfig: serializeBar(),
|
||||
lock: serializeLock(),
|
||||
general: serializeGeneral(),
|
||||
services: serializeServices(),
|
||||
notifs: serializeNotifs(),
|
||||
sidebar: serializeSidebar(),
|
||||
utilities: serializeUtilities(),
|
||||
dashboard: serializeDashboard(),
|
||||
appearance: serializeAppearance(),
|
||||
osd: serializeOsd(),
|
||||
background: serializeBackground(),
|
||||
launcher: serializeLauncher(),
|
||||
colors: serializeColors(),
|
||||
dock: serializeDock(),
|
||||
screenshot: serializeScreenshot(),
|
||||
clipboard: serializeClipboard()
|
||||
};
|
||||
}
|
||||
|
||||
function serializeDashboard(): var {
|
||||
return {
|
||||
enabled: dashboard.enabled,
|
||||
mediaUpdateInterval: dashboard.mediaUpdateInterval,
|
||||
resourceUpdateInterval: dashboard.resourceUpdateInterval,
|
||||
dragThreshold: dashboard.dragThreshold,
|
||||
performance: {
|
||||
showBattery: dashboard.performance.showBattery,
|
||||
showGpu: dashboard.performance.showGpu,
|
||||
showCpu: dashboard.performance.showCpu,
|
||||
showMemory: dashboard.performance.showMemory,
|
||||
showStorage: dashboard.performance.showStorage,
|
||||
showNetwork: dashboard.performance.showNetwork
|
||||
},
|
||||
sizes: {
|
||||
tabIndicatorHeight: dashboard.sizes.tabIndicatorHeight,
|
||||
tabIndicatorSpacing: dashboard.sizes.tabIndicatorSpacing,
|
||||
infoWidth: dashboard.sizes.infoWidth,
|
||||
infoIconSize: dashboard.sizes.infoIconSize,
|
||||
dateTimeWidth: dashboard.sizes.dateTimeWidth,
|
||||
mediaWidth: dashboard.sizes.mediaWidth,
|
||||
mediaProgressSweep: dashboard.sizes.mediaProgressSweep,
|
||||
mediaProgressThickness: dashboard.sizes.mediaProgressThickness,
|
||||
resourceProgessThickness: dashboard.sizes.resourceProgessThickness,
|
||||
weatherWidth: dashboard.sizes.weatherWidth,
|
||||
mediaCoverArtSize: dashboard.sizes.mediaCoverArtSize,
|
||||
mediaVisualizerSize: dashboard.sizes.mediaVisualizerSize,
|
||||
resourceSize: dashboard.sizes.resourceSize
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function serializeDock(): var {
|
||||
return {
|
||||
enable: dock.enable,
|
||||
height: dock.height,
|
||||
hoverToReveal: dock.hoverToReveal,
|
||||
pinnedApps: dock.pinnedApps,
|
||||
pinnedOnStartup: dock.pinnedOnStartup,
|
||||
ignoredAppRegexes: dock.ignoredAppRegexes
|
||||
};
|
||||
}
|
||||
|
||||
function serializeGeneral(): var {
|
||||
return {
|
||||
logo: general.logo,
|
||||
wallpaperPath: general.wallpaperPath,
|
||||
showOverFullscreen: general.showOverFullscreen,
|
||||
desktopIcons: general.desktopIcons,
|
||||
dateFormat: general.dateFormat,
|
||||
color: {
|
||||
mode: general.color.mode,
|
||||
smart: general.color.smart,
|
||||
scheduleDark: general.color.scheduleDark,
|
||||
scheduleHyprsunset: general.color.scheduleHyprsunset,
|
||||
scheduleHyprsunsetStart: general.color.scheduleHyprsunsetStart,
|
||||
hyprsunsetTemp: general.color.hyprsunsetTemp,
|
||||
scheduleHyprsunsetEnd: general.color.scheduleHyprsunsetEnd,
|
||||
schemeGeneration: general.color.schemeGeneration,
|
||||
scheduleDarkStart: general.color.scheduleDarkStart,
|
||||
scheduleDarkEnd: general.color.scheduleDarkEnd,
|
||||
neovimColors: general.color.neovimColors
|
||||
},
|
||||
apps: {
|
||||
terminal: general.apps.terminal,
|
||||
audio: general.apps.audio,
|
||||
playback: general.apps.playback,
|
||||
explorer: general.apps.explorer
|
||||
},
|
||||
idle: {
|
||||
timeouts: general.idle.timeouts
|
||||
},
|
||||
battery: {
|
||||
popupThresholds: general.battery.popupThresholds,
|
||||
critPerc: general.battery.critPerc
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function serializeLauncher(): var {
|
||||
return {
|
||||
maxAppsShown: launcher.maxAppsShown,
|
||||
maxWallpapers: launcher.maxWallpapers,
|
||||
uwsm: launcher.uwsm,
|
||||
actionPrefix: launcher.actionPrefix,
|
||||
specialPrefix: launcher.specialPrefix,
|
||||
useFuzzy: {
|
||||
apps: launcher.useFuzzy.apps,
|
||||
actions: launcher.useFuzzy.actions,
|
||||
schemes: launcher.useFuzzy.schemes,
|
||||
variants: launcher.useFuzzy.variants,
|
||||
wallpapers: launcher.useFuzzy.wallpapers
|
||||
},
|
||||
sizes: {
|
||||
itemWidth: launcher.sizes.itemWidth,
|
||||
itemHeight: launcher.sizes.itemHeight,
|
||||
wallpaperWidth: launcher.sizes.wallpaperWidth,
|
||||
wallpaperHeight: launcher.sizes.wallpaperHeight
|
||||
},
|
||||
actions: launcher.actions
|
||||
};
|
||||
}
|
||||
|
||||
function serializeLock(): var {
|
||||
return {
|
||||
recolorLogo: lock.recolorLogo,
|
||||
enableFprint: lock.enableFprint,
|
||||
showNotifContent: lock.showNotifContent,
|
||||
showNotifIcon: lock.showNotifIcon,
|
||||
maxFprintTries: lock.maxFprintTries,
|
||||
blurAmount: lock.blurAmount,
|
||||
sizes: {
|
||||
heightMult: lock.sizes.heightMult,
|
||||
ratio: lock.sizes.ratio,
|
||||
centerWidth: lock.sizes.centerWidth
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function serializeNotifs(): var {
|
||||
return {
|
||||
expire: notifs.expire,
|
||||
defaultExpireTimeout: notifs.defaultExpireTimeout,
|
||||
appNotifCooldown: notifs.appNotifCooldown,
|
||||
clearThreshold: notifs.clearThreshold,
|
||||
expandThreshold: notifs.expandThreshold,
|
||||
actionOnClick: notifs.actionOnClick,
|
||||
groupPreviewNum: notifs.groupPreviewNum,
|
||||
sizes: {
|
||||
width: notifs.sizes.width,
|
||||
image: notifs.sizes.image,
|
||||
badge: notifs.sizes.badge
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function serializeOsd(): var {
|
||||
return {
|
||||
enabled: osd.enabled,
|
||||
hideDelay: osd.hideDelay,
|
||||
enableBrightness: osd.enableBrightness,
|
||||
enableMicrophone: osd.enableMicrophone,
|
||||
allMonBrightness: osd.allMonBrightness,
|
||||
sizes: {
|
||||
sliderWidth: osd.sizes.sliderWidth,
|
||||
sliderHeight: osd.sizes.sliderHeight
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function serializeScreenshot(): var {
|
||||
return {
|
||||
enable_pp: screenshot.enable_pp,
|
||||
mode: screenshot.mode,
|
||||
radius: screenshot.radius,
|
||||
shadow: screenshot.shadow,
|
||||
rounding: screenshot.rounding,
|
||||
shadow_blur: screenshot.shadow_blur,
|
||||
shadow_color: screenshot.shadow_color,
|
||||
shadow_offset_x: screenshot.shadow_offset_x,
|
||||
shadow_offset_y: screenshot.shadow_offset_y
|
||||
};
|
||||
}
|
||||
|
||||
function serializeServices(): var {
|
||||
return {
|
||||
weatherLocation: services.weatherLocation,
|
||||
updates: services.updates,
|
||||
useFahrenheit: services.useFahrenheit,
|
||||
minBrightness: services.minBrightness,
|
||||
ddcutilService: services.ddcutilService,
|
||||
useTwelveHourClock: services.useTwelveHourClock,
|
||||
gpuType: services.gpuType,
|
||||
audioIncrement: services.audioIncrement,
|
||||
brightnessIncrement: services.brightnessIncrement,
|
||||
maxVolume: services.maxVolume,
|
||||
defaultPlayer: services.defaultPlayer,
|
||||
playerAliases: services.playerAliases,
|
||||
visualizerBars: services.visualizerBars
|
||||
};
|
||||
}
|
||||
|
||||
function serializeSidebar(): var {
|
||||
return {
|
||||
enabled: sidebar.enabled,
|
||||
sizes: {
|
||||
width: sidebar.sizes.width
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function serializeUtilities(): var {
|
||||
return {
|
||||
enabled: utilities.enabled,
|
||||
maxToasts: utilities.maxToasts,
|
||||
sizes: {
|
||||
width: utilities.sizes.width,
|
||||
toastWidth: utilities.sizes.toastWidth
|
||||
},
|
||||
toasts: {
|
||||
configLoaded: utilities.toasts.configLoaded,
|
||||
chargingChanged: utilities.toasts.chargingChanged,
|
||||
gameModeChanged: utilities.toasts.gameModeChanged,
|
||||
dndChanged: utilities.toasts.dndChanged,
|
||||
audioOutputChanged: utilities.toasts.audioOutputChanged,
|
||||
audioInputChanged: utilities.toasts.audioInputChanged,
|
||||
capsLockChanged: utilities.toasts.capsLockChanged,
|
||||
numLockChanged: utilities.toasts.numLockChanged,
|
||||
kbLayoutChanged: utilities.toasts.kbLayoutChanged,
|
||||
vpnChanged: utilities.toasts.vpnChanged,
|
||||
nowPlaying: utilities.toasts.nowPlaying
|
||||
},
|
||||
vpn: {
|
||||
enabled: utilities.vpn.enabled,
|
||||
provider: utilities.vpn.provider
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
ElapsedTimer {
|
||||
id: timer
|
||||
}
|
||||
@@ -392,20 +86,7 @@ Singleton {
|
||||
|
||||
onTriggered: {
|
||||
timer.restart();
|
||||
try {
|
||||
let config = {};
|
||||
try {
|
||||
config = JSON.parse(fileView.text());
|
||||
} catch (e) {
|
||||
config = {};
|
||||
}
|
||||
|
||||
config = root.serializeConfig();
|
||||
|
||||
fileView.setText(JSON.stringify(config, null, 4));
|
||||
} catch (e) {
|
||||
Toaster.toast(qsTr("Failed to serialize config"), e.message, "settings_alert", Toast.Error);
|
||||
}
|
||||
fileView.writeAdapter();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -425,6 +106,10 @@ Singleton {
|
||||
path: `${Paths.config}/config.json`
|
||||
watchChanges: true
|
||||
|
||||
onAdapterUpdated: {
|
||||
if (!root.migratingLegacyBar)
|
||||
root.save();
|
||||
}
|
||||
onFileChanged: {
|
||||
if (!root.recentlySaved) {
|
||||
timer.restart();
|
||||
@@ -441,7 +126,11 @@ Singleton {
|
||||
ModeScheduler.checkStartup();
|
||||
Hyprsunset.checkStartup();
|
||||
try {
|
||||
JSON.parse(text());
|
||||
const raw = JSON.parse(text());
|
||||
const migrated = root.migrateLegacyBarConfig(raw);
|
||||
if (migrated)
|
||||
root.save();
|
||||
|
||||
const elapsed = timer.elapsedMs();
|
||||
|
||||
if (adapter.utilities.toasts.configLoaded && !root.recentlySaved) {
|
||||
@@ -462,7 +151,7 @@ Singleton {
|
||||
}
|
||||
property BackgroundConfig background: BackgroundConfig {
|
||||
}
|
||||
property BarConfig barConfig: BarConfig {
|
||||
property BarConfig bar: BarConfig {
|
||||
}
|
||||
property ClipboardConfig clipboard: ClipboardConfig {
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import Quickshell.Io
|
||||
|
||||
JsonObject {
|
||||
property int dragThreshold: 50
|
||||
property bool enabled: true
|
||||
property int mediaUpdateInterval: 500
|
||||
property Performance performance: Performance {
|
||||
@@ -11,12 +10,14 @@ JsonObject {
|
||||
}
|
||||
|
||||
component Performance: JsonObject {
|
||||
property bool enabled: true
|
||||
property bool showBattery: true
|
||||
property bool showCpu: true
|
||||
property bool showGpu: true
|
||||
property bool showMemory: true
|
||||
property bool showNetwork: true
|
||||
property bool showStorage: true
|
||||
property bool showVram: true
|
||||
}
|
||||
component Sizes: JsonObject {
|
||||
readonly property int dateTimeWidth: 110
|
||||
|
||||
@@ -84,6 +84,11 @@ JsonObject {
|
||||
dangerous: false
|
||||
},
|
||||
]
|
||||
property int dragThreshold: 50
|
||||
property bool enableDangerousActions: false
|
||||
property bool enabled: true
|
||||
property list<string> favoriteApps: []
|
||||
property list<string> hiddenApps: []
|
||||
property int maxAppsShown: 10
|
||||
property int maxWallpapers: 7
|
||||
property Sizes sizes: Sizes {
|
||||
|
||||
@@ -9,6 +9,7 @@ JsonObject {
|
||||
property bool expire: true
|
||||
property int groupPreviewNum: 3
|
||||
property bool openExpanded: false
|
||||
property bool showInFullscreen: true
|
||||
property Sizes sizes: Sizes {
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import Quickshell.Io
|
||||
|
||||
JsonObject {
|
||||
property int dragThreshold: 30
|
||||
property bool enabled: true
|
||||
property Sizes sizes: Sizes {
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ Singleton {
|
||||
property alias server: server
|
||||
|
||||
function shouldThrottle(appName: string): bool {
|
||||
if (props.dnd)
|
||||
if (props.dnd || [...Visibilities.screens.values()].some(v => v.sidebar))
|
||||
return false;
|
||||
|
||||
const key = (appName || "unknown").trim().toLowerCase();
|
||||
@@ -49,11 +49,6 @@ Singleton {
|
||||
if (loaded) {
|
||||
saveTimer.restart();
|
||||
}
|
||||
if (root.list.length > 0) {
|
||||
HasNotifications.hasNotifications = true;
|
||||
} else {
|
||||
HasNotifications.hasNotifications = false;
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
|
||||
@@ -44,7 +44,7 @@ Scope {
|
||||
}
|
||||
|
||||
component ExclusionZone: CustomWindow {
|
||||
exclusiveZone: Config.barConfig.border
|
||||
exclusiveZone: Config.bar.border
|
||||
implicitHeight: 1
|
||||
implicitWidth: 1
|
||||
name: "Bar-Exclusion"
|
||||
|
||||
+15
-24
@@ -21,15 +21,15 @@ Item {
|
||||
required property PersistentProperties visibilities
|
||||
|
||||
function inBottomPanel(panel: Item, x: real, y: real): bool {
|
||||
return y > root.height - panel.height - Config.barConfig.border && withinPanelWidth(panel, x, y);
|
||||
return y > root.height - panel.height - Config.bar.border && withinPanelWidth(panel, x, y);
|
||||
}
|
||||
|
||||
function inLeftPanel(panel: Item, x: real, y: real): bool {
|
||||
return x < panel.x + panel.width + Config.barConfig.border && withinPanelHeight(panel, x, y);
|
||||
return x < panel.x + panel.width + Config.bar.border && withinPanelHeight(panel, x, y);
|
||||
}
|
||||
|
||||
function inRightPanel(panel: Item, x: real, y: real): bool {
|
||||
return x > panel.x - Config.barConfig.border && withinPanelHeight(panel, x, y);
|
||||
return x > panel.x - Config.bar.border && withinPanelHeight(panel, x, y);
|
||||
}
|
||||
|
||||
function inTopPanel(panel: Item, x: real, y: real): bool {
|
||||
@@ -75,7 +75,7 @@ Item {
|
||||
const dragX = x - centroid.pressPosition.x;
|
||||
const dragY = y - centroid.pressPosition.y;
|
||||
|
||||
if (centroid.pressPosition.y >= root.screen.height - Config.barConfig.border && centroid.pressPosition.x > root.screen.width / 5 && dragY < -200)
|
||||
if (centroid.pressPosition.y >= root.screen.height - Config.bar.border && centroid.pressPosition.x > root.screen.width / 5 && dragY < -Config.launcher.dragThreshold)
|
||||
root.visibilities.launcher = true;
|
||||
|
||||
if (root.singleGestureTriggered)
|
||||
@@ -91,7 +91,7 @@ Item {
|
||||
}
|
||||
}
|
||||
|
||||
if (centroid.pressPosition.y > root.screen.height - Config.barConfig.border && centroid.pressPosition.x < root.screen.width / 5 && dragY < -50)
|
||||
if (centroid.pressPosition.y > root.screen.height - Config.bar.border && centroid.pressPosition.x < root.screen.width / 5 && dragY < -50)
|
||||
root.visibilities.clipboard = true;
|
||||
|
||||
if (!Config.dock.hoverToReveal && centroid.pressPosition.y > root.screen.height - root.bar.implicitHeight && centroid.pressPosition.x > root.screen.width / 5 && !root.visibilities.launcher)
|
||||
@@ -100,17 +100,17 @@ Item {
|
||||
root.singleGestureTriggered = true;
|
||||
}
|
||||
|
||||
if (centroid.pressPosition.x > root.screen.width - Config.barConfig.border && centroid.pressPosition.y < (root.screen.height / 2) && dragX < -20) {
|
||||
if (centroid.pressPosition.x > root.screen.width - Config.bar.border && centroid.pressPosition.y < (root.screen.height / 2) && dragX < -Config.sidebar.dragThreshold) {
|
||||
root.visibilities.sidebar = true;
|
||||
root.singleGestureTriggered = true;
|
||||
}
|
||||
|
||||
if (centroid.pressPosition.x >= root.screen.width - Config.barConfig.border && centroid.pressPosition.y > (root.screen.height / 2) && dragX < -20) {
|
||||
if (centroid.pressPosition.x >= root.screen.width - Config.bar.border && centroid.pressPosition.y > (root.screen.height / 2) && dragX < -20) {
|
||||
Hypr.dispatch(`hl.dsp.focus({ workspace = 'r+1', on_current_monitor = true })`);
|
||||
root.singleGestureTriggered = true;
|
||||
}
|
||||
|
||||
if (centroid.pressPosition.x <= Config.barConfig.border && dragX > 20) {
|
||||
if (centroid.pressPosition.x <= Config.bar.border && dragX > 20) {
|
||||
Hypr.dispatch(`hl.dsp.focus({ workspace = 'r-1', on_current_monitor = true })`);
|
||||
root.singleGestureTriggered = true;
|
||||
}
|
||||
@@ -170,11 +170,11 @@ Item {
|
||||
root.panels.osd.hovered = false;
|
||||
}
|
||||
|
||||
if (!root.popouts.currentName.startsWith("traymenu")) {
|
||||
if (!root.popouts.currentName.startsWith("traymenu") || Config.bar.tray.showOnHover) {
|
||||
root.popouts.hasCurrent = false;
|
||||
}
|
||||
|
||||
if (Config.barConfig.autoHide)
|
||||
if (Config.bar.autoHide)
|
||||
root.bar.isHovered = false;
|
||||
}
|
||||
}
|
||||
@@ -193,10 +193,10 @@ Item {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!root.visibilities.bar && Config.barConfig.autoHide && y < root.bar.implicitHeight)
|
||||
if (!root.visibilities.bar && Config.bar.autoHide && y < root.bar.implicitHeight)
|
||||
root.bar.isHovered = true;
|
||||
|
||||
if (root.panels.sidebar.width === 0) {
|
||||
if (root.panels.sidebar.offsetScale === 1) {
|
||||
const showOsd = root.inRightPanel(root.panels.osdWrapper, x, y);
|
||||
|
||||
if (showOsd) {
|
||||
@@ -204,7 +204,7 @@ Item {
|
||||
root.panels.osd.hovered = true;
|
||||
}
|
||||
} else {
|
||||
const outOfSidebar = x < root.width - root.panels.sidebar.width;
|
||||
const outOfSidebar = x < root.width - root.panels.sidebar.width * (1 - root.panels.sidebar.offsetScale);
|
||||
const showOsd = outOfSidebar && root.inRightPanel(root.panels.osdWrapper, x, y);
|
||||
|
||||
if (!root.osdShortcutActive) {
|
||||
@@ -298,6 +298,8 @@ Item {
|
||||
root.visibilities.sidebar = false;
|
||||
root.panels.popouts.hasCurrent = false;
|
||||
root.visibilities.launcher = false;
|
||||
} else {
|
||||
Config.save();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -309,17 +311,6 @@ Item {
|
||||
}
|
||||
}
|
||||
|
||||
function onUtilitiesChanged() {
|
||||
if (root.visibilities.utilities) {
|
||||
const inUtilitiesArea = root.inBottomPanel(root.panels.utilities, root.mouseX, root.mouseY);
|
||||
if (!inUtilitiesArea) {
|
||||
root.utilitiesShortcutActive = true;
|
||||
}
|
||||
} else {
|
||||
root.utilitiesShortcutActive = false;
|
||||
}
|
||||
}
|
||||
|
||||
target: root.visibilities
|
||||
}
|
||||
}
|
||||
|
||||
+3
-7
@@ -184,18 +184,14 @@ Item {
|
||||
Item {
|
||||
id: settingsWrapper
|
||||
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.top: parent.top
|
||||
anchors.fill: parent
|
||||
clip: true
|
||||
implicitHeight: settings.implicitHeight * (1 - settings.offsetScale)
|
||||
implicitWidth: settings.implicitWidth
|
||||
|
||||
Settings.Wrapper {
|
||||
id: settings
|
||||
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.top: parent.top
|
||||
// anchors.centerIn: parent
|
||||
anchors.centerIn: parent
|
||||
anchors.verticalCenterOffset: (-implicitHeight - 5 - ((root.height - implicitHeight) / 2)) * offsetScale
|
||||
panels: root
|
||||
screen: root.screen
|
||||
visibilities: root.visibilities
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import qs.Modules.Bar as Bar
|
||||
|
||||
Region {
|
||||
id: root
|
||||
|
||||
required property Bar.BarLoader bar
|
||||
readonly property real borderThickness: win.borderThickness
|
||||
readonly property alias menuPopoutRegion: menuPopoutRegion
|
||||
required property Panels panels
|
||||
required property var win
|
||||
|
||||
height: win.height - bar.implicitHeight - win.borderThickness - win.dragMaskPadding * 2
|
||||
intersection: Intersection.Xor
|
||||
width: win.width - win.borderThickness * 2 - win.dragMaskPadding * 2
|
||||
x: win.borderThickness + win.dragMaskPadding
|
||||
y: bar.implicitHeight + win.dragMaskPadding
|
||||
|
||||
R {
|
||||
panel: root.panels.dashboardWrapper
|
||||
}
|
||||
|
||||
R {
|
||||
panel: root.panels.launcher
|
||||
}
|
||||
|
||||
R {
|
||||
id: sidebarRegion
|
||||
|
||||
panel: root.panels.sidebar
|
||||
width: panel.width * (1 - root.panels.sidebar.offsetScale) + root.borderThickness
|
||||
x: root.win.width - width
|
||||
}
|
||||
|
||||
R {
|
||||
panel: root.panels.osdWrapper
|
||||
width: panel.width * (1 - root.panels.osd.offsetScale) + root.borderThickness
|
||||
}
|
||||
|
||||
R {
|
||||
panel: root.panels.notifications
|
||||
}
|
||||
|
||||
R {
|
||||
height: panel.height * (1 - root.panels.utilities.offsetScale) + root.borderThickness
|
||||
panel: root.panels.utilities
|
||||
y: root.win.height - height
|
||||
}
|
||||
|
||||
R {
|
||||
panel: root.panels.popoutsWrapper
|
||||
}
|
||||
|
||||
R {
|
||||
panel: root.panels.resourcesWrapper
|
||||
}
|
||||
|
||||
R {
|
||||
panel: root.panels.settings
|
||||
}
|
||||
|
||||
R {
|
||||
panel: root.panels.dock
|
||||
}
|
||||
|
||||
R {
|
||||
panel: root.panels.clipboard
|
||||
}
|
||||
|
||||
Region {
|
||||
id: menuPopoutRegion
|
||||
|
||||
intersection: Intersection.Subtract
|
||||
}
|
||||
|
||||
component R: Region {
|
||||
required property Item panel
|
||||
|
||||
height: panel.height
|
||||
intersection: Intersection.Subtract
|
||||
width: panel.width
|
||||
x: panel.x + root.borderThickness
|
||||
y: panel.y + root.bar.implicitHeight
|
||||
}
|
||||
}
|
||||
+25
-58
@@ -1,6 +1,7 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Effects
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
@@ -18,14 +19,14 @@ CustomWindow {
|
||||
id: root
|
||||
|
||||
readonly property alias bar: bar
|
||||
readonly property real borderLayoutThickness: hasFullscreen ? 0 : Config.barConfig.border
|
||||
readonly property real borderRounding: Config.barConfig.rounding * (1 - fsTransitionProg)
|
||||
readonly property real borderThickness: Config.barConfig.border * (1 - fsTransitionProg)
|
||||
readonly property real borderLayoutThickness: hasFullscreen ? 0 : Config.bar.border
|
||||
readonly property real borderRounding: Config.bar.rounding * (1 - fsTransitionProg)
|
||||
readonly property real borderThickness: Config.bar.border * (1 - fsTransitionProg)
|
||||
readonly property int dragMaskPadding: {
|
||||
if (focusGrab.active)
|
||||
return 0;
|
||||
|
||||
if (monitor?.lastIpcObject.specialWorkspace?.name || monitor?.activeWorkspace.lastIpcObject.windows > 0)
|
||||
if (monitor?.lastIpcObject.specialWorkspace?.name || monitor?.activeWorkspace?.lastIpcObject.windows > 0)
|
||||
return 0;
|
||||
|
||||
return 100;
|
||||
@@ -44,7 +45,7 @@ CustomWindow {
|
||||
readonly property bool hasFullscreenOnNormalWs: monitor?.activeWorkspace?.toplevels.values.some(t => t.lastIpcObject.fullscreen > 1) ?? false
|
||||
readonly property bool hasSpecialWorkspace: (monitor?.lastIpcObject.specialWorkspace?.name.length ?? 0) > 0
|
||||
readonly property alias interactionWrapper: interactions
|
||||
readonly property alias menuRegion: menuPopoutRegion
|
||||
readonly property alias menuRegion: regions.menuPopoutRegion
|
||||
readonly property HyprlandMonitor monitor: Hypr.monitorFor(screen)
|
||||
property var root: Quickshell.shellDir
|
||||
readonly property real sdfBorderOffset: 2 * fsTransitionProg
|
||||
@@ -56,7 +57,7 @@ CustomWindow {
|
||||
WlrLayershell.layer: (fsTransitionProg > 0 && Config.general.showOverFullscreen) || (hasSpecialWorkspace && hasFullscreenOnNormalWs) ? WlrLayer.Overlay : WlrLayer.Top
|
||||
color: "transparent"
|
||||
contentItem.focus: true
|
||||
mask: visibilities.isDrawing ? null : (hasFullscreen ? emptyRegion : region)
|
||||
mask: visibilities.isDrawing ? null : (hasFullscreen ? emptyRegion : regions)
|
||||
name: "Bar"
|
||||
|
||||
Behavior on fsTransitionProg {
|
||||
@@ -69,7 +70,7 @@ CustomWindow {
|
||||
}
|
||||
|
||||
contentItem.Keys.onEscapePressed: {
|
||||
if (Config.barConfig.autoHide)
|
||||
if (Config.bar.autoHide)
|
||||
visibilities.bar = false;
|
||||
visibilities.launcher = false;
|
||||
visibilities.sidebar = false;
|
||||
@@ -93,12 +94,6 @@ CustomWindow {
|
||||
panels.popouts.hasCurrent = false;
|
||||
}
|
||||
|
||||
Region {
|
||||
id: menuPopoutRegion
|
||||
|
||||
intersection: Intersection.Subtract
|
||||
}
|
||||
|
||||
Region {
|
||||
id: emptyRegion
|
||||
|
||||
@@ -106,17 +101,21 @@ CustomWindow {
|
||||
width: panels.notifications.width
|
||||
x: panels.notifications.x + root.borderThickness
|
||||
y: panels.notifications.y + bar.implicitHeight
|
||||
|
||||
Region {
|
||||
height: panels.osd.height
|
||||
width: panels.osdWrapper.width * (1 - panels.osd.offsetScale) + root.borderThickness
|
||||
x: root.width - width
|
||||
y: panels.osdWrapper.y + bar.implicitHeight
|
||||
}
|
||||
}
|
||||
|
||||
Region {
|
||||
id: region
|
||||
Regions {
|
||||
id: regions
|
||||
|
||||
height: root.height - bar.implicitHeight - root.borderThickness - root.dragMaskPadding * 2
|
||||
intersection: Intersection.Xor
|
||||
regions: [...popoutRegions.instances, menuPopoutRegion]
|
||||
width: root.width - root.borderThickness * 2 - root.dragMaskPadding * 2
|
||||
x: root.borderThickness + root.dragMaskPadding
|
||||
y: bar.implicitHeight + root.dragMaskPadding
|
||||
bar: bar
|
||||
panels: panels
|
||||
win: root
|
||||
}
|
||||
|
||||
anchors {
|
||||
@@ -126,26 +125,10 @@ CustomWindow {
|
||||
top: true
|
||||
}
|
||||
|
||||
Variants {
|
||||
id: popoutRegions
|
||||
|
||||
model: panels.children
|
||||
|
||||
Region {
|
||||
required property Item modelData
|
||||
|
||||
height: modelData.height
|
||||
intersection: Intersection.Subtract
|
||||
width: modelData.width
|
||||
x: modelData.x + root.borderThickness
|
||||
y: modelData.y + bar.implicitHeight
|
||||
}
|
||||
}
|
||||
|
||||
HyprlandFocusGrab {
|
||||
id: focusGrab
|
||||
|
||||
active: visibilities.dock || visibilities.resources || visibilities.launcher || visibilities.sidebar || visibilities.dashboard || visibilities.settings || visibilities.clipboard || (panels.popouts.hasCurrent && panels.popouts.currentName.startsWith("traymenu"))
|
||||
active: visibilities.dock || visibilities.resources || visibilities.launcher || visibilities.sidebar || visibilities.dashboard || visibilities.settings || visibilities.clipboard || (panels.popouts.hasCurrent && panels.popouts.currentName.startsWith("traymenu") && (!Config.bar.tray.showOnHover || (panels.popouts.current as StackView)?.depth > 1))
|
||||
windows: [root]
|
||||
|
||||
onCleared: {
|
||||
@@ -179,19 +162,11 @@ CustomWindow {
|
||||
Component.onCompleted: Visibilities.load(root.screen, this)
|
||||
}
|
||||
|
||||
IpcHandler {
|
||||
function toggleLauncher(fix: string): void {
|
||||
visibilities.launcher = !visibilities.launcher;
|
||||
}
|
||||
|
||||
target: "visibilities"
|
||||
}
|
||||
|
||||
Binding {
|
||||
property: "bar"
|
||||
target: visibilities
|
||||
value: visibilities.sidebar || visibilities.dashboard || visibilities.osd || (!Config.barConfig.hideWhenNotif && visibilities.notif) || visibilities.resources || visibilities.settings || bar.isHovered
|
||||
when: Config.barConfig.autoHide
|
||||
value: visibilities.sidebar || visibilities.dashboard || visibilities.osd || (!Config.bar.hideWhenNotif && visibilities.notif) || visibilities.resources || visibilities.settings || bar.isHovered
|
||||
when: Config.bar.autoHide
|
||||
}
|
||||
|
||||
Item {
|
||||
@@ -211,7 +186,7 @@ CustomWindow {
|
||||
id: blobGroup
|
||||
|
||||
color: root.surfaceColor
|
||||
smoothing: Config.barConfig.smoothing
|
||||
smoothing: Config.bar.smoothing
|
||||
}
|
||||
|
||||
BlobInvertedRect {
|
||||
@@ -323,17 +298,9 @@ CustomWindow {
|
||||
PanelBg {
|
||||
id: settingsBg
|
||||
|
||||
property real extraHeight: 0.2
|
||||
|
||||
deformAmount: 0.03
|
||||
implicitHeight: panels.settings.height * (1 + extraHeight)
|
||||
implicitWidth: panels.settings.width
|
||||
panel: panels.settings
|
||||
radius: Appearance.rounding.large
|
||||
topLeftRadius: Appearance.rounding.large + Appearance.padding.smaller
|
||||
topRightRadius: Appearance.rounding.large + Appearance.padding.smaller
|
||||
x: panels.settingsWrapper.x + panels.settings.x + root.borderThickness
|
||||
y: panels.settingsWrapper.y + panels.settings.y + bar.implicitHeight - panels.settings.height * extraHeight
|
||||
radius: Appearance.rounding.large + Appearance.padding.normal
|
||||
}
|
||||
|
||||
PanelBg {
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
import QtQuick.Effects
|
||||
|
||||
MultiEffect {
|
||||
maskEnabled: true
|
||||
maskSpreadAtMin: 1
|
||||
maskThresholdMin: 0.5
|
||||
}
|
||||
@@ -30,18 +30,6 @@ RowLayout {
|
||||
}
|
||||
}
|
||||
|
||||
// CustomRect {
|
||||
// Layout.fillWidth: true
|
||||
// color: DynamicColors.tPalette.m3surfaceContainer
|
||||
// implicitHeight: resources.implicitHeight
|
||||
// radius: Appearance.rounding.small
|
||||
//
|
||||
// Resources {
|
||||
// id: resources
|
||||
//
|
||||
// }
|
||||
// }
|
||||
|
||||
CustomClippingRect {
|
||||
Layout.fillHeight: true
|
||||
Layout.fillWidth: true
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
pragma Singleton
|
||||
|
||||
import Quickshell
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
function getTrayIcon(id: string, icon: string): string {
|
||||
if (icon.includes("?path=")) {
|
||||
const [name, path] = icon.split("?path=");
|
||||
icon = Qt.resolvedUrl(`${path}/${name.slice(name.lastIndexOf("/") + 1)}`);
|
||||
} else if (icon.includes("qspixmap") && id === "chrome_status_icon_1") {
|
||||
icon = icon.replace("qspixmap", "icon/discord-tray");
|
||||
}
|
||||
return icon;
|
||||
}
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
pragma Singleton
|
||||
import QtQuick
|
||||
|
||||
QtObject {
|
||||
id: root
|
||||
|
||||
property Item activeMenu: null
|
||||
property Item activeTrigger: null
|
||||
|
||||
function close(menu) {
|
||||
if (!menu)
|
||||
return;
|
||||
|
||||
if (activeMenu === menu) {
|
||||
activeMenu = null;
|
||||
activeTrigger = null;
|
||||
}
|
||||
|
||||
menu.expanded = false;
|
||||
}
|
||||
|
||||
function closeActive() {
|
||||
if (activeMenu)
|
||||
activeMenu.expanded = false;
|
||||
|
||||
activeMenu = null;
|
||||
activeTrigger = null;
|
||||
}
|
||||
|
||||
function forget(menu) {
|
||||
if (activeMenu === menu) {
|
||||
activeMenu = null;
|
||||
activeTrigger = null;
|
||||
}
|
||||
}
|
||||
|
||||
function hit(item, scenePos) {
|
||||
if (!item || !item.visible)
|
||||
return false;
|
||||
|
||||
const p = item.mapFromItem(null, scenePos.x, scenePos.y);
|
||||
return item.contains(p);
|
||||
}
|
||||
|
||||
function open(menu, trigger) {
|
||||
if (activeMenu && activeMenu !== menu)
|
||||
activeMenu.expanded = false;
|
||||
|
||||
activeMenu = menu;
|
||||
activeTrigger = trigger || null;
|
||||
menu.expanded = true;
|
||||
}
|
||||
|
||||
function toggle(menu, trigger) {
|
||||
if (activeMenu === menu && menu.expanded)
|
||||
close(menu);
|
||||
else
|
||||
open(menu, trigger);
|
||||
}
|
||||
}
|
||||
@@ -1,409 +0,0 @@
|
||||
pragma Singleton
|
||||
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import QtQuick
|
||||
import qs.Config
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
property string autoGpuType: "NONE"
|
||||
property string cpuName: ""
|
||||
property real cpuPerc
|
||||
property real cpuTemp
|
||||
|
||||
// Individual disks: Array of { mount, used, total, free, perc }
|
||||
property var disks: []
|
||||
property real gpuMemTotal: 0
|
||||
property real gpuMemUsed
|
||||
property real gpuPerc
|
||||
property real gpuTemp
|
||||
readonly property string gpuType: Config.services.gpuType.toUpperCase() || autoGpuType
|
||||
property real lastCpuIdle
|
||||
property real lastCpuTotal
|
||||
readonly property real memPerc: memTotal > 0 ? memUsed / memTotal : 0
|
||||
property real memTotal
|
||||
property real memUsed
|
||||
property int refCount
|
||||
readonly property real storagePerc: {
|
||||
let totalUsed = 0;
|
||||
let totalSize = 0;
|
||||
for (const disk of disks) {
|
||||
totalUsed += disk.used;
|
||||
totalSize += disk.total;
|
||||
}
|
||||
return totalSize > 0 ? totalUsed / totalSize : 0;
|
||||
}
|
||||
|
||||
function cleanCpuName(name: string): string {
|
||||
return name.replace(/\(R\)/gi, "").replace(/\(TM\)/gi, "").replace(/CPU/gi, "").replace(/\d+th Gen /gi, "").replace(/\d+nd Gen /gi, "").replace(/\d+rd Gen /gi, "").replace(/\d+st Gen /gi, "").replace(/Core /gi, "").replace(/Processor/gi, "").replace(/\s+/g, " ").trim();
|
||||
}
|
||||
|
||||
function cleanGpuName(name: string): string {
|
||||
return name.replace(/NVIDIA GeForce /gi, "").replace(/NVIDIA /gi, "").replace(/AMD Radeon /gi, "").replace(/AMD /gi, "").replace(/Intel /gi, "").replace(/\(R\)/gi, "").replace(/\(TM\)/gi, "").replace(/Graphics/gi, "").replace(/\s+/g, " ").trim();
|
||||
}
|
||||
|
||||
function formatKib(kib: real): var {
|
||||
const mib = 1024;
|
||||
const gib = 1024 ** 2;
|
||||
const tib = 1024 ** 3;
|
||||
|
||||
if (kib >= tib)
|
||||
return {
|
||||
value: kib / tib,
|
||||
unit: "TiB"
|
||||
};
|
||||
if (kib >= gib)
|
||||
return {
|
||||
value: kib / gib,
|
||||
unit: "GiB"
|
||||
};
|
||||
if (kib >= mib)
|
||||
return {
|
||||
value: kib / mib,
|
||||
unit: "MiB"
|
||||
};
|
||||
return {
|
||||
value: kib,
|
||||
unit: "KiB"
|
||||
};
|
||||
}
|
||||
|
||||
Timer {
|
||||
interval: Config.dashboard.resourceUpdateInterval
|
||||
repeat: true
|
||||
running: root.refCount > 0
|
||||
triggeredOnStart: true
|
||||
|
||||
onTriggered: {
|
||||
stat.reload();
|
||||
meminfo.reload();
|
||||
if (root.gpuType === "GENERIC")
|
||||
gpuUsage.running = true;
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
interval: 60000 * 120
|
||||
repeat: true
|
||||
running: true
|
||||
triggeredOnStart: true
|
||||
|
||||
onTriggered: {
|
||||
storage.running = true;
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
interval: Config.dashboard.resourceUpdateInterval * 5
|
||||
repeat: true
|
||||
running: root.refCount > 0
|
||||
triggeredOnStart: true
|
||||
|
||||
onTriggered: {
|
||||
sensors.running = true;
|
||||
}
|
||||
}
|
||||
|
||||
FileView {
|
||||
id: cpuinfoInit
|
||||
|
||||
path: "/proc/cpuinfo"
|
||||
|
||||
onLoaded: {
|
||||
const nameMatch = text().match(/model name\s*:\s*(.+)/);
|
||||
if (nameMatch)
|
||||
root.cpuName = root.cleanCpuName(nameMatch[1]);
|
||||
}
|
||||
}
|
||||
|
||||
FileView {
|
||||
id: stat
|
||||
|
||||
path: "/proc/stat"
|
||||
|
||||
onLoaded: {
|
||||
const data = text().match(/^cpu\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)/);
|
||||
if (data) {
|
||||
const stats = data.slice(1).map(n => parseInt(n, 10));
|
||||
const total = stats.reduce((a, b) => a + b, 0);
|
||||
const idle = stats[3] + (stats[4] ?? 0);
|
||||
|
||||
const totalDiff = total - root.lastCpuTotal;
|
||||
const idleDiff = idle - root.lastCpuIdle;
|
||||
const newCpuPerc = totalDiff > 0 ? (1 - idleDiff / totalDiff) : 0;
|
||||
|
||||
root.lastCpuTotal = total;
|
||||
root.lastCpuIdle = idle;
|
||||
|
||||
if (Math.abs(newCpuPerc - root.cpuPerc) >= 0.01)
|
||||
root.cpuPerc = newCpuPerc;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
FileView {
|
||||
id: meminfo
|
||||
|
||||
path: "/proc/meminfo"
|
||||
|
||||
onLoaded: {
|
||||
const data = text();
|
||||
const total = parseInt(data.match(/MemTotal: *(\d+)/)[1], 10) || 1;
|
||||
const used = (root.memTotal - parseInt(data.match(/MemAvailable: *(\d+)/)[1], 10)) || 0;
|
||||
|
||||
if (root.memTotal !== total)
|
||||
root.memTotal = total;
|
||||
|
||||
if (Math.abs(used - root.memUsed) >= 16384)
|
||||
root.memUsed = used;
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: storage
|
||||
|
||||
command: ["lsblk", "-b", "-o", "NAME,SIZE,TYPE,FSUSED,FSSIZE", "-P"]
|
||||
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
const diskMap = {}; // Map disk name -> { name, totalSize, used, fsTotal }
|
||||
const lines = text.trim().split("\n");
|
||||
|
||||
for (const line of lines) {
|
||||
if (line.trim() === "")
|
||||
continue;
|
||||
const nameMatch = line.match(/NAME="([^"]+)"/);
|
||||
const sizeMatch = line.match(/SIZE="([^"]+)"/);
|
||||
const typeMatch = line.match(/TYPE="([^"]+)"/);
|
||||
const fsusedMatch = line.match(/FSUSED="([^"]*)"/);
|
||||
const fssizeMatch = line.match(/FSSIZE="([^"]*)"/);
|
||||
|
||||
if (!nameMatch || !typeMatch)
|
||||
continue;
|
||||
|
||||
const name = nameMatch[1];
|
||||
const type = typeMatch[1];
|
||||
const size = parseInt(sizeMatch?.[1] || "0", 10);
|
||||
const fsused = parseInt(fsusedMatch?.[1] || "0", 10);
|
||||
const fssize = parseInt(fssizeMatch?.[1] || "0", 10);
|
||||
|
||||
if (type === "disk") {
|
||||
// Skip zram (swap) devices
|
||||
if (name.startsWith("zram"))
|
||||
continue;
|
||||
|
||||
// Initialize disk entry
|
||||
if (!diskMap[name]) {
|
||||
diskMap[name] = {
|
||||
name: name,
|
||||
totalSize: size,
|
||||
used: 0,
|
||||
fsTotal: 0
|
||||
};
|
||||
}
|
||||
} else if (type === "part") {
|
||||
// Find parent disk (remove trailing numbers/p+numbers)
|
||||
let parentDisk = name.replace(/p?\d+$/, "");
|
||||
// For nvme devices like nvme0n1p1, parent is nvme0n1
|
||||
if (name.match(/nvme\d+n\d+p\d+/))
|
||||
parentDisk = name.replace(/p\d+$/, "");
|
||||
|
||||
// Aggregate partition usage to parent disk
|
||||
if (diskMap[parentDisk]) {
|
||||
diskMap[parentDisk].used += fsused;
|
||||
diskMap[parentDisk].fsTotal += fssize;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const diskList = [];
|
||||
let totalUsed = 0;
|
||||
let totalSize = 0;
|
||||
|
||||
for (const diskName of Object.keys(diskMap).sort()) {
|
||||
const disk = diskMap[diskName];
|
||||
// Use filesystem total if available, otherwise use disk size
|
||||
const total = disk.fsTotal > 0 ? disk.fsTotal : disk.totalSize;
|
||||
const used = disk.used;
|
||||
const perc = total > 0 ? used / total : 0;
|
||||
|
||||
// Convert bytes to KiB for consistency with formatKib
|
||||
diskList.push({
|
||||
mount: disk.name // Using 'mount' property for compatibility
|
||||
,
|
||||
used: used / 1024,
|
||||
total: total / 1024,
|
||||
free: (total - used) / 1024,
|
||||
perc: perc
|
||||
});
|
||||
|
||||
totalUsed += used;
|
||||
totalSize += total;
|
||||
}
|
||||
|
||||
root.disks = diskList;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: gpuNameDetect
|
||||
|
||||
command: ["sh", "-c", "nvidia-smi --query-gpu=name --format=csv,noheader 2>/dev/null || lspci 2>/dev/null | grep -i 'vga\\|3d\\|display' | head -1"]
|
||||
running: true
|
||||
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
const output = text.trim();
|
||||
if (!output)
|
||||
return;
|
||||
|
||||
// Check if it's from nvidia-smi (clean GPU name)
|
||||
if (output.toLowerCase().includes("nvidia") || output.toLowerCase().includes("geforce") || output.toLowerCase().includes("rtx") || output.toLowerCase().includes("gtx")) {
|
||||
root.gpuName = root.cleanGpuName(output);
|
||||
} else {
|
||||
// Parse lspci output: extract name from brackets or after colon
|
||||
const bracketMatch = output.match(/\[([^\]]+)\]/);
|
||||
if (bracketMatch) {
|
||||
root.gpuName = root.cleanGpuName(bracketMatch[1]);
|
||||
} else {
|
||||
const colonMatch = output.match(/:\s*(.+)/);
|
||||
if (colonMatch)
|
||||
root.gpuName = root.cleanGpuName(colonMatch[1]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: gpuTypeCheck
|
||||
|
||||
command: ["sh", "-c", "if command -v nvidia-smi &>/dev/null && nvidia-smi -L &>/dev/null; then echo NVIDIA; elif ls /sys/class/drm/card*/device/gpu_busy_percent 2>/dev/null | grep -q .; then echo GENERIC; else echo NONE; fi"]
|
||||
running: !Config.services.gpuType
|
||||
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: root.autoGpuType = text.trim()
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: oneshotMem
|
||||
|
||||
command: ["nvidia-smi", "--query-gpu=memory.total", "--format=csv,noheader,nounits"]
|
||||
running: root.gpuType === "NVIDIA" && root.gpuMemTotal === 0
|
||||
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
root.gpuMemTotal = Number(this.text.trim());
|
||||
oneshotMem.running = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: gpuUsageNvidia
|
||||
|
||||
command: ["/usr/bin/nvidia-smi", "--query-gpu=utilization.gpu,temperature.gpu,memory.used", "--format=csv,noheader,nounits", "-lms", "1000"]
|
||||
running: root.refCount > 0 && root.gpuType === "NVIDIA"
|
||||
|
||||
stdout: SplitParser {
|
||||
onRead: data => {
|
||||
const parts = String(data).trim().split(/\s*,\s*/);
|
||||
if (parts.length < 3)
|
||||
return;
|
||||
|
||||
const usageRaw = parseInt(parts[0], 10);
|
||||
const tempRaw = parseInt(parts[1], 10);
|
||||
const memRaw = parseInt(parts[2], 10);
|
||||
|
||||
if (!Number.isFinite(usageRaw) || !Number.isFinite(tempRaw) || !Number.isFinite(memRaw))
|
||||
return;
|
||||
|
||||
const newGpuPerc = Math.max(0, Math.min(1, usageRaw / 100));
|
||||
const newGpuTemp = tempRaw;
|
||||
const newGpuMemUsed = root.gpuMemTotal > 0 ? Math.max(0, Math.min(1, memRaw / root.gpuMemTotal)) : 0;
|
||||
|
||||
// Only publish meaningful changes to avoid needless binding churn / repaints
|
||||
if (Math.abs(root.gpuPerc - newGpuPerc) >= 0.01)
|
||||
root.gpuPerc = newGpuPerc;
|
||||
|
||||
if (Math.abs(root.gpuTemp - newGpuTemp) >= 1)
|
||||
root.gpuTemp = newGpuTemp;
|
||||
|
||||
if (Math.abs(root.gpuMemUsed - newGpuMemUsed) >= 0.01)
|
||||
root.gpuMemUsed = newGpuMemUsed;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: gpuUsage
|
||||
|
||||
command: root.gpuType === "GENERIC" ? ["sh", "-c", "cat /sys/class/drm/card*/device/gpu_busy_percent"] : ["echo"]
|
||||
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
if (root.gpuType === "GENERIC") {
|
||||
const percs = text.trim().split("\n");
|
||||
const sum = percs.reduce((acc, d) => acc + parseInt(d, 10), 0);
|
||||
root.gpuPerc = sum / percs.length / 100;
|
||||
} else {
|
||||
root.gpuPerc = 0;
|
||||
root.gpuTemp = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: sensors
|
||||
|
||||
command: ["sensors"]
|
||||
environment: ({
|
||||
LANG: "C.UTF-8",
|
||||
LC_ALL: "C.UTF-8"
|
||||
})
|
||||
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
let cpuTemp = text.match(/(?:Package id [0-9]+|Tdie):\s+((\+|-)[0-9.]+)(°| )C/);
|
||||
if (!cpuTemp)
|
||||
// If AMD Tdie pattern failed, try fallback on Tctl
|
||||
cpuTemp = text.match(/Tctl:\s+((\+|-)[0-9.]+)(°| )C/);
|
||||
|
||||
if (cpuTemp && Math.abs(parseFloat(cpuTemp[1]) - root.cpuTemp) >= 0.5)
|
||||
root.cpuTemp = parseFloat(cpuTemp[1]);
|
||||
|
||||
if (root.gpuType !== "GENERIC")
|
||||
return;
|
||||
|
||||
let eligible = false;
|
||||
let sum = 0;
|
||||
let count = 0;
|
||||
|
||||
for (const line of text.trim().split("\n")) {
|
||||
if (line === "Adapter: PCI adapter")
|
||||
eligible = true;
|
||||
else if (line === "")
|
||||
eligible = false;
|
||||
else if (eligible) {
|
||||
let match = line.match(/^(temp[0-9]+|GPU core|edge)+:\s+\+([0-9]+\.[0-9]+)(°| )C/);
|
||||
if (!match)
|
||||
// Fall back to junction/mem if GPU doesn't have edge temp (for AMD GPUs)
|
||||
match = line.match(/^(junction|mem)+:\s+\+([0-9]+\.[0-9]+)(°| )C/);
|
||||
|
||||
if (match) {
|
||||
sum += parseFloat(match[2]);
|
||||
count++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
root.gpuTemp = count > 0 ? sum / count : 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
import QtQuick
|
||||
|
||||
Text {
|
||||
renderType: Text.NativeRendering
|
||||
textFormat: Text.PlainText
|
||||
}
|
||||
@@ -1,288 +0,0 @@
|
||||
pragma Singleton
|
||||
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
property list<DesktopEntry> entryList: []
|
||||
property var preppedIcons: []
|
||||
property var preppedIds: []
|
||||
property var preppedNames: []
|
||||
|
||||
// Dynamic fixups
|
||||
property var regexSubstitutions: [
|
||||
{
|
||||
"regex": /^steam_app_(\d+)$/,
|
||||
"replace": "steam_icon_$1"
|
||||
},
|
||||
{
|
||||
"regex": /Minecraft.*/,
|
||||
"replace": "minecraft-launcher"
|
||||
},
|
||||
{
|
||||
"regex": /.*polkit.*/,
|
||||
"replace": "system-lock-screen"
|
||||
},
|
||||
{
|
||||
"regex": /gcr.prompter/,
|
||||
"replace": "system-lock-screen"
|
||||
}
|
||||
]
|
||||
property real scoreThreshold: 0.2
|
||||
|
||||
// Manual overrides for tricky apps
|
||||
property var substitutions: ({
|
||||
"code-url-handler": "visual-studio-code",
|
||||
"Code": "visual-studio-code",
|
||||
"gnome-tweaks": "org.gnome.tweaks",
|
||||
"pavucontrol-qt": "pavucontrol",
|
||||
"wps": "wps-office2019-kprometheus",
|
||||
"wpsoffice": "wps-office2019-kprometheus",
|
||||
"footclient": "foot"
|
||||
})
|
||||
|
||||
function checkCleanMatch(str) {
|
||||
if (!str || str.length <= 3)
|
||||
return null;
|
||||
if (typeof DesktopEntries === 'undefined' || !DesktopEntries.byId)
|
||||
return null;
|
||||
|
||||
// Aggressive fallback: strip all separators
|
||||
const cleanStr = str.toLowerCase().replace(/[\.\-_]/g, '');
|
||||
const list = Array.from(entryList);
|
||||
|
||||
for (let i = 0; i < list.length; i++) {
|
||||
const entry = list[i];
|
||||
const cleanId = (entry.id || "").toLowerCase().replace(/[\.\-_]/g, '');
|
||||
if (cleanId.includes(cleanStr) || cleanStr.includes(cleanId)) {
|
||||
return entry;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function checkFuzzySearch(str) {
|
||||
if (typeof FuzzySort === 'undefined')
|
||||
return null;
|
||||
|
||||
// Check filenames (IDs) first
|
||||
if (preppedIds.length > 0) {
|
||||
let results = fuzzyQuery(str, preppedIds);
|
||||
if (results.length === 0) {
|
||||
const underscored = str.replace(/-/g, '_').toLowerCase();
|
||||
if (underscored !== str)
|
||||
results = fuzzyQuery(underscored, preppedIds);
|
||||
}
|
||||
if (results.length > 0)
|
||||
return results[0];
|
||||
}
|
||||
|
||||
// Then icons
|
||||
if (preppedIcons.length > 0) {
|
||||
const results = fuzzyQuery(str, preppedIcons);
|
||||
if (results.length > 0)
|
||||
return results[0];
|
||||
}
|
||||
|
||||
// Then names
|
||||
if (preppedNames.length > 0) {
|
||||
const results = fuzzyQuery(str, preppedNames);
|
||||
if (results.length > 0)
|
||||
return results[0];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// --- Lookup Helpers ---
|
||||
|
||||
function checkHeuristic(str) {
|
||||
if (typeof DesktopEntries !== 'undefined' && DesktopEntries.heuristicLookup) {
|
||||
const entry = DesktopEntries.heuristicLookup(str);
|
||||
if (entry)
|
||||
return entry;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function checkRegex(str) {
|
||||
for (let i = 0; i < regexSubstitutions.length; i++) {
|
||||
const sub = regexSubstitutions[i];
|
||||
const replaced = str.replace(sub.regex, sub.replace);
|
||||
if (replaced !== str) {
|
||||
return findAppEntry(replaced);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function checkSimpleTransforms(str) {
|
||||
if (typeof DesktopEntries === 'undefined' || !DesktopEntries.byId)
|
||||
return null;
|
||||
|
||||
const lower = str.toLowerCase();
|
||||
|
||||
const variants = [str, lower, getFromReverseDomain(str), getFromReverseDomain(str)?.toLowerCase(), normalizeWithHyphens(str), str.replace(/_/g, '-').toLowerCase(), str.replace(/-/g, '_').toLowerCase()];
|
||||
|
||||
for (let i = 0; i < variants.length; i++) {
|
||||
const variant = variants[i];
|
||||
if (variant) {
|
||||
const entry = DesktopEntries.byId(variant);
|
||||
if (entry)
|
||||
return entry;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function checkSubstitutions(str) {
|
||||
let effectiveStr = substitutions[str];
|
||||
if (!effectiveStr)
|
||||
effectiveStr = substitutions[str.toLowerCase()];
|
||||
|
||||
if (effectiveStr && effectiveStr !== str) {
|
||||
return findAppEntry(effectiveStr);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function distroLogoPath() {
|
||||
try {
|
||||
return (typeof OSInfo !== 'undefined' && OSInfo.distroIconPath) ? OSInfo.distroIconPath : "";
|
||||
} catch (e) {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
// Robust lookup strategy
|
||||
function findAppEntry(str) {
|
||||
if (!str || str.length === 0)
|
||||
return null;
|
||||
|
||||
let result = null;
|
||||
|
||||
if (result = checkHeuristic(str))
|
||||
return result;
|
||||
if (result = checkSubstitutions(str))
|
||||
return result;
|
||||
if (result = checkRegex(str))
|
||||
return result;
|
||||
if (result = checkSimpleTransforms(str))
|
||||
return result;
|
||||
if (result = checkFuzzySearch(str))
|
||||
return result;
|
||||
if (result = checkCleanMatch(str))
|
||||
return result;
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function fuzzyQuery(search, preppedData) {
|
||||
if (!search || !preppedData || preppedData.length === 0)
|
||||
return [];
|
||||
return FuzzySort.go(search, preppedData, {
|
||||
all: true,
|
||||
key: "name"
|
||||
}).map(r => r.obj.entry);
|
||||
}
|
||||
|
||||
function getFromReverseDomain(str) {
|
||||
if (!str)
|
||||
return "";
|
||||
return str.split('.').slice(-1)[0];
|
||||
}
|
||||
|
||||
// Deprecated shim
|
||||
function guessIcon(str) {
|
||||
const entry = findAppEntry(str);
|
||||
return entry ? entry.icon : "image-missing";
|
||||
}
|
||||
|
||||
function iconExists(iconName) {
|
||||
if (!iconName || iconName.length === 0)
|
||||
return false;
|
||||
if (iconName.startsWith("/"))
|
||||
return true;
|
||||
|
||||
const path = Quickshell.iconPath(iconName, true);
|
||||
return path && path.length > 0 && !path.includes("image-missing");
|
||||
}
|
||||
|
||||
function iconForAppId(appId, fallbackName) {
|
||||
const fallback = fallbackName || "application-x-executable";
|
||||
if (!appId)
|
||||
return iconFromName(fallback, fallback);
|
||||
|
||||
const entry = findAppEntry(appId);
|
||||
if (entry) {
|
||||
return iconFromName(entry.icon, fallback);
|
||||
}
|
||||
|
||||
return iconFromName(appId, fallback);
|
||||
}
|
||||
|
||||
function iconFromName(iconName, fallbackName) {
|
||||
const fallback = fallbackName || "application-x-executable";
|
||||
try {
|
||||
if (iconName && typeof Quickshell !== 'undefined' && Quickshell.iconPath) {
|
||||
const p = Quickshell.iconPath(iconName, fallback);
|
||||
if (p && p !== "")
|
||||
return p;
|
||||
}
|
||||
} catch (e) {}
|
||||
|
||||
try {
|
||||
return Quickshell.iconPath ? (Quickshell.iconPath(fallback, true) || "") : "";
|
||||
} catch (e2) {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeWithHyphens(str) {
|
||||
if (!str)
|
||||
return "";
|
||||
return str.toLowerCase().replace(/\s+/g, "-");
|
||||
}
|
||||
|
||||
function refreshEntries() {
|
||||
if (typeof DesktopEntries === 'undefined')
|
||||
return;
|
||||
|
||||
const values = Array.from(DesktopEntries.applications.values);
|
||||
if (values) {
|
||||
entryList = values.sort((a, b) => a.name.localeCompare(b.name));
|
||||
updatePreppedData();
|
||||
}
|
||||
}
|
||||
|
||||
function updatePreppedData() {
|
||||
if (typeof FuzzySort === 'undefined')
|
||||
return;
|
||||
|
||||
const list = Array.from(entryList);
|
||||
preppedNames = list.map(a => ({
|
||||
name: FuzzySort.prepare(`${a.name} `),
|
||||
entry: a
|
||||
}));
|
||||
preppedIcons = list.map(a => ({
|
||||
name: FuzzySort.prepare(`${a.icon} `),
|
||||
entry: a
|
||||
}));
|
||||
preppedIds = list.map(a => ({
|
||||
name: FuzzySort.prepare(`${a.id} `),
|
||||
entry: a
|
||||
}));
|
||||
}
|
||||
|
||||
Component.onCompleted: refreshEntries()
|
||||
|
||||
Connections {
|
||||
function onValuesChanged() {
|
||||
refreshEntries();
|
||||
}
|
||||
|
||||
target: DesktopEntries.applications
|
||||
}
|
||||
}
|
||||
@@ -1,77 +0,0 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import qs.Components
|
||||
import qs.Helpers
|
||||
import qs.Config
|
||||
|
||||
GridLayout {
|
||||
id: root
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.margins: Appearance.padding.large
|
||||
anchors.right: parent.right
|
||||
columnSpacing: Appearance.spacing.large
|
||||
columns: 2
|
||||
rowSpacing: Appearance.spacing.large
|
||||
rows: 1
|
||||
|
||||
Ref {
|
||||
service: SystemUsage
|
||||
}
|
||||
|
||||
Resource {
|
||||
Layout.bottomMargin: Appearance.padding.large
|
||||
Layout.topMargin: Appearance.padding.large
|
||||
colour: DynamicColors.palette.m3primary
|
||||
icon: "memory"
|
||||
value: SystemUsage.cpuPerc
|
||||
}
|
||||
|
||||
Resource {
|
||||
Layout.bottomMargin: Appearance.padding.large
|
||||
Layout.topMargin: Appearance.padding.large
|
||||
colour: DynamicColors.palette.m3secondary
|
||||
icon: "memory_alt"
|
||||
value: SystemUsage.memPerc
|
||||
}
|
||||
|
||||
component Resource: CustomRect {
|
||||
id: res
|
||||
|
||||
required property color colour
|
||||
required property string icon
|
||||
required property real value
|
||||
|
||||
Layout.fillWidth: true
|
||||
color: DynamicColors.layer(DynamicColors.palette.m3surfaceContainerHigh, 2)
|
||||
implicitHeight: width
|
||||
radius: Appearance.rounding.large
|
||||
|
||||
Behavior on value {
|
||||
Anim {
|
||||
duration: Appearance.anim.durations.large
|
||||
}
|
||||
}
|
||||
|
||||
CircularProgress {
|
||||
id: circ
|
||||
|
||||
anchors.fill: parent
|
||||
bgColour: DynamicColors.layer(DynamicColors.palette.m3surfaceContainerHighest, 3)
|
||||
fgColour: res.colour
|
||||
padding: Appearance.padding.large * 3
|
||||
strokeWidth: width < 200 ? Appearance.padding.smaller : Appearance.padding.normal
|
||||
value: res.value
|
||||
}
|
||||
|
||||
MaterialIcon {
|
||||
id: icon
|
||||
|
||||
anchors.centerIn: parent
|
||||
color: res.colour
|
||||
font.pointSize: (circ.arcRadius * 0.7) || 1
|
||||
font.weight: 600
|
||||
text: res.icon
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,5 +9,5 @@ export EGL_PLATFORM=gbm
|
||||
if command -v start-hyprland >/dev/null 2>&1; then
|
||||
exec start-hyprland -- -c /etc/zshell-greeter/zshell-hyprland.conf
|
||||
else
|
||||
exec Hyprland -c /etc/zshell-greeter/zshell-hyprland.conf
|
||||
exec Hyprland -c /etc/zshell-greeter/zshell-hyprland.lua
|
||||
fi
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
monitor = ,preferred,auto,1
|
||||
|
||||
env = XDG_SESSION_TYPE,wayland
|
||||
env = QT_QPA_PLATFORM,wayland
|
||||
env = QT_WAYLAND_DISABLE_WINDOWDECORATION,1
|
||||
|
||||
misc {
|
||||
disable_hyprland_logo = true
|
||||
disable_splash_rendering = true
|
||||
}
|
||||
|
||||
exec = sh -lc 'qs -c zshell-greeter; hyprctl dispatch exit'
|
||||
@@ -0,0 +1,14 @@
|
||||
hl.env("XDG_SESSION_TYPE", "wayland")
|
||||
hl.env("QT_QPA_PLATFORM", "wayland")
|
||||
hl.env("QT_WAYLAND_DISABLE_WINDOWDECORATION", "1")
|
||||
|
||||
hl.config({
|
||||
misc = {
|
||||
disable_hyprland_logo = true,
|
||||
disable_splash_rendering = true,
|
||||
},
|
||||
})
|
||||
|
||||
hl.on("hyprland.start", function()
|
||||
hl.exec_cmd("sh -lc 'qs -c zshell-greeter; hyprctl dispatch exit'")
|
||||
end)
|
||||
+15
-42
@@ -11,8 +11,13 @@ Singleton {
|
||||
id: root
|
||||
|
||||
property bool appleDisplayPresent: false
|
||||
readonly property var ddcMonitorMap: {
|
||||
const map = {};
|
||||
for (const m of ddcMonitors)
|
||||
map[m.connector] = m;
|
||||
return map;
|
||||
}
|
||||
property list<var> ddcMonitors: []
|
||||
property list<var> ddcServiceMon: []
|
||||
readonly property list<Monitor> monitors: variants.instances
|
||||
|
||||
function decreaseBrightness(): void {
|
||||
@@ -56,8 +61,6 @@ Singleton {
|
||||
|
||||
onMonitorsChanged: {
|
||||
ddcMonitors = [];
|
||||
ddcServiceMon = [];
|
||||
ddcServiceProc.running = true;
|
||||
ddcProc.running = true;
|
||||
}
|
||||
|
||||
@@ -92,26 +95,6 @@ Singleton {
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: ddcServiceProc
|
||||
|
||||
command: ["ddcutil-client", "detect"]
|
||||
|
||||
// running: true
|
||||
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
const t = text.replace(/\r\n/g, "\n").trim();
|
||||
|
||||
const output = ("\n" + t).split(/\n(?=display:\s*\d+\s*\n)/).filter(b => b.startsWith("display:")).map(b => ({
|
||||
display: Number(b.match(/^display:\s*(\d+)/m)?.[1] ?? -1),
|
||||
name: (b.match(/^\s*product_name:\s*(.*)$/m)?.[1] ?? "").trim()
|
||||
})).filter(d => d.display > 0);
|
||||
root.ddcServiceMon = output;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
CustomShortcut {
|
||||
description: "Increase brightness"
|
||||
name: "brightnessUp"
|
||||
@@ -183,16 +166,12 @@ Singleton {
|
||||
id: monitor
|
||||
|
||||
property real brightness
|
||||
readonly property string busNum: root.ddcMonitors.find(m => m.connector === modelData.name)?.busNum ?? ""
|
||||
readonly property string displayNum: root.ddcServiceMon.find(m => m.name === modelData.model)?.display ?? ""
|
||||
readonly property string busNum: ddcInfo?.busNum ?? ""
|
||||
readonly property var ddcInfo: root.ddcMonitorMap[modelData.name] ?? null
|
||||
readonly property Process initProc: Process {
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
if (monitor.isDdcService) {
|
||||
const output = text.split("\n").filter(o => o.startsWith("vcp_current_value:"))[0].split(":")[1];
|
||||
const val = parseInt(output.trim());
|
||||
monitor.brightness = val / 100;
|
||||
} else if (monitor.isAppleDisplay) {
|
||||
if (monitor.isAppleDisplay) {
|
||||
const val = parseInt(text.trim());
|
||||
monitor.brightness = val / 101;
|
||||
} else {
|
||||
@@ -203,12 +182,11 @@ Singleton {
|
||||
}
|
||||
}
|
||||
readonly property bool isAppleDisplay: root.appleDisplayPresent && modelData.model.startsWith("StudioDisplay")
|
||||
readonly property bool isDdc: root.ddcMonitors.some(m => m.connector === modelData.name)
|
||||
readonly property bool isDdcService: Config.services.ddcutilService
|
||||
readonly property bool isDdc: ddcInfo !== null
|
||||
required property ShellScreen modelData
|
||||
property real queuedBrightness: NaN
|
||||
readonly property Timer timer: Timer {
|
||||
interval: 500
|
||||
interval: 400
|
||||
|
||||
onTriggered: {
|
||||
if (!isNaN(monitor.queuedBrightness)) {
|
||||
@@ -219,9 +197,7 @@ Singleton {
|
||||
}
|
||||
|
||||
function initBrightness(): void {
|
||||
if (isDdcService)
|
||||
initProc.command = ["ddcutil-client", "-d", displayNum, "getvcp", "10"];
|
||||
else if (isAppleDisplay)
|
||||
if (isAppleDisplay)
|
||||
initProc.command = ["asdbctl", "get"];
|
||||
else if (isDdc)
|
||||
initProc.command = ["ddcutil", "-b", busNum, "getvcp", "10", "--brief"];
|
||||
@@ -237,28 +213,25 @@ Singleton {
|
||||
if (Math.round(brightness * 100) === rounded)
|
||||
return;
|
||||
|
||||
if ((isDdc || isDdcService) && timer.running) {
|
||||
if (isDdc && timer.running) {
|
||||
queuedBrightness = value;
|
||||
return;
|
||||
}
|
||||
|
||||
brightness = value;
|
||||
|
||||
if (isDdcService)
|
||||
Quickshell.execDetached(["ddcutil-client", "-d", displayNum, "setvcp", "10", rounded]);
|
||||
else if (isAppleDisplay)
|
||||
if (isAppleDisplay)
|
||||
Quickshell.execDetached(["asdbctl", "set", rounded]);
|
||||
else if (isDdc)
|
||||
Quickshell.execDetached(["ddcutil", "--disable-dynamic-sleep", "--sleep-multiplier", ".1", "--skip-ddc-checks", "-b", busNum, "setvcp", "10", rounded]);
|
||||
else
|
||||
Quickshell.execDetached(["brightnessctl", "s", `${rounded}%`]);
|
||||
|
||||
if (isDdc || isDdcService)
|
||||
if (isDdc)
|
||||
timer.restart();
|
||||
}
|
||||
|
||||
Component.onCompleted: initBrightness()
|
||||
onBusNumChanged: initBrightness()
|
||||
onDisplayNumChanged: initBrightness()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,97 +0,0 @@
|
||||
pragma Singleton
|
||||
|
||||
import Quickshell
|
||||
import qs.Helpers
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
property int displayMonth: new Date().getMonth()
|
||||
property int displayYear: new Date().getFullYear()
|
||||
readonly property int weekStartDay: 1 // 0 = Sunday, 1 = Monday
|
||||
|
||||
function getISOWeekNumber(date: var): int {
|
||||
const d = new Date(Date.UTC(date.getFullYear(), date.getMonth(), date.getDate()));
|
||||
const dayNum = d.getUTCDay() || 7;
|
||||
d.setUTCDate(d.getUTCDate() + 4 - dayNum);
|
||||
const yearStart = new Date(Date.UTC(d.getUTCFullYear(), 0, 1));
|
||||
return Math.ceil((((d - yearStart) / 86400000) + 1) / 7);
|
||||
}
|
||||
|
||||
function getWeekNumbers(month: int, year: int): var {
|
||||
const days = getWeeksForMonth(month, year);
|
||||
const weekNumbers = [];
|
||||
let lastWeekNumber = -1;
|
||||
|
||||
for (let i = 0; i < days.length; i++) {
|
||||
// Only add week numbers for days that belong to the current month
|
||||
if (days[i].isCurrentMonth) {
|
||||
const dayDate = new Date(days[i].year, days[i].month, days[i].day);
|
||||
const weekNumber = getISOWeekNumber(dayDate);
|
||||
|
||||
// Only push if this is a new week
|
||||
if (weekNumber !== lastWeekNumber) {
|
||||
weekNumbers.push(weekNumber);
|
||||
lastWeekNumber = weekNumber;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return weekNumbers;
|
||||
}
|
||||
|
||||
function getWeekStartIndex(month: int, year: int): int {
|
||||
const today = new Date();
|
||||
if (today.getMonth() !== month || today.getFullYear() !== year) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const days = getWeeksForMonth(month, year);
|
||||
for (let i = 0; i < days.length; i++) {
|
||||
if (days[i].isToday) {
|
||||
// Return the start index of the week containing today
|
||||
return Math.floor(i / 7) * 7;
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
function getWeeksForMonth(month: int, year: int): var {
|
||||
const firstDayOfMonth = new Date(year, month, 1);
|
||||
const lastDayOfMonth = new Date(year, month + 1, 0);
|
||||
|
||||
const days = [];
|
||||
let currentDate = new Date(year, month, 1);
|
||||
|
||||
// Back up to the start of the first week (Sunday or Monday based on locale)
|
||||
const dayOfWeek = firstDayOfMonth.getDay();
|
||||
const daysToBackup = (dayOfWeek - root.weekStartDay + 7) % 7;
|
||||
currentDate.setDate(currentDate.getDate() - daysToBackup);
|
||||
|
||||
// Collect all days, including padding from previous/next month to complete the weeks
|
||||
while (true) {
|
||||
days.push({
|
||||
day: currentDate.getDate(),
|
||||
month: currentDate.getMonth(),
|
||||
year: currentDate.getFullYear(),
|
||||
isCurrentMonth: currentDate.getMonth() === month,
|
||||
isToday: isDateToday(currentDate)
|
||||
});
|
||||
|
||||
currentDate.setDate(currentDate.getDate() + 1);
|
||||
|
||||
// Stop after we've completed a full week following the last day of the month
|
||||
if (currentDate > lastDayOfMonth && days.length % 7 === 0) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return days;
|
||||
}
|
||||
|
||||
function isDateToday(date: var): bool {
|
||||
const today = new Date();
|
||||
return date.getDate() === today.getDate() && date.getMonth() === today.getMonth() && date.getFullYear() === today.getFullYear();
|
||||
}
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
pragma Singleton
|
||||
|
||||
import Quickshell
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
function getTrayIcon(id: string, icon: string): string {
|
||||
if (icon.includes("?path=")) {
|
||||
const [name, path] = icon.split("?path=");
|
||||
icon = Qt.resolvedUrl(`${path}/${name.slice(name.lastIndexOf("/") + 1)}`);
|
||||
} else if (icon.includes("qspixmap") && id === "chrome_status_icon_1") {
|
||||
icon = icon.replace("qspixmap", "icon/discord-tray");
|
||||
}
|
||||
return icon;
|
||||
}
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
pragma Singleton
|
||||
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
property alias hasNotifications: adapter.hasNotifications
|
||||
|
||||
JsonObject {
|
||||
id: adapter
|
||||
|
||||
property bool hasNotifications: false
|
||||
}
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
pragma Singleton
|
||||
|
||||
import Quickshell
|
||||
import Quickshell.Hyprland
|
||||
import qs.Helpers
|
||||
|
||||
Singleton {
|
||||
function getInitialTitle(callback) {
|
||||
let activeWindow = Hypr.activeToplevel.title;
|
||||
let activeClass = Hypr.activeToplevel.lastIpcObject.class.toString();
|
||||
let regex = new RegExp(activeClass, "i");
|
||||
|
||||
const evalTitle = activeWindow.match(regex);
|
||||
callback(evalTitle);
|
||||
}
|
||||
}
|
||||
@@ -9,31 +9,25 @@ import qs.Config
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
// Private properties
|
||||
property real _downloadSpeed: 0
|
||||
property real _downloadTotal: 0
|
||||
|
||||
// Initial readings for calculating totals
|
||||
property real _initialRxBytes: 0
|
||||
property real _initialTxBytes: 0
|
||||
property bool _initialized: false
|
||||
|
||||
// Previous readings for calculating speed
|
||||
property real _prevRxBytes: 0
|
||||
property real _prevTimestamp: 0
|
||||
property real _prevTxBytes: 0
|
||||
property real _uploadSpeed: 0
|
||||
property real _uploadTotal: 0
|
||||
|
||||
// History buffers for sparkline
|
||||
readonly property CircularBuffer downloadBuffer: _downloadBuffer
|
||||
|
||||
// Current speeds in bytes per second
|
||||
readonly property real downloadSpeed: _downloadSpeed
|
||||
|
||||
// Total bytes transferred since tracking started
|
||||
readonly property real downloadTotal: _downloadTotal
|
||||
readonly property int historyLength: 30
|
||||
readonly property int historyLength: Config.dashboard.performance.showVram ? 70 : 30
|
||||
property int refCount: 0
|
||||
readonly property CircularBuffer uploadBuffer: _uploadBuffer
|
||||
readonly property real uploadSpeed: _uploadSpeed
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
pragma Singleton
|
||||
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
property alias centerX: notifCenterSpacing.centerX
|
||||
|
||||
JsonAdapter {
|
||||
id: notifCenterSpacing
|
||||
|
||||
property int centerX
|
||||
}
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
pragma Singleton
|
||||
|
||||
import Quickshell.Io
|
||||
import Quickshell
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
property alias notifPath: storage.notifPath
|
||||
|
||||
JsonAdapter {
|
||||
id: storage
|
||||
|
||||
property string notifPath: Quickshell.statePath("notifications.json")
|
||||
}
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
pragma Singleton
|
||||
|
||||
import Quickshell
|
||||
|
||||
Singleton {
|
||||
}
|
||||
+31
-3
@@ -1,12 +1,12 @@
|
||||
pragma Singleton
|
||||
|
||||
import QtQml
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import Quickshell.Services.Mpris
|
||||
import QtQml
|
||||
import ZShell
|
||||
import qs.Config
|
||||
import qs.Components
|
||||
import qs.Config
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
@@ -15,7 +15,24 @@ Singleton {
|
||||
readonly property list<MprisPlayer> list: Mpris.players.values
|
||||
property alias manualActive: props.manualActive
|
||||
|
||||
function getArtUrl(player: MprisPlayer): string {
|
||||
if (!player)
|
||||
return "";
|
||||
if (player.trackArtUrl)
|
||||
return player.trackArtUrl;
|
||||
|
||||
const url = player.metadata["xesam:url"] ?? "";
|
||||
if (url.startsWith("https://www.youtube.com/watch")) {
|
||||
// Fallback for youtube
|
||||
const id = url.match(/[?&]v=([\w-]{11})/)?.[1];
|
||||
return id ? `https://img.youtube.com/vi/${id}/hqdefault.jpg` : "";
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
function getIdentity(player: MprisPlayer): string {
|
||||
if (!player)
|
||||
return "";
|
||||
const alias = Config.services.playerAliases.find(a => a.from === player.identity);
|
||||
return alias?.to ?? player.identity;
|
||||
}
|
||||
@@ -25,9 +42,12 @@ Singleton {
|
||||
if (!Config.utilities.toasts.nowPlaying) {
|
||||
return;
|
||||
}
|
||||
if (root.active.trackArtist != "" && root.active.trackTitle != "") {
|
||||
Toaster.toast(qsTr("Now Playing"), qsTr("%1 - %2").arg(root.active.trackArtist).arg(root.active.trackTitle), "music_note");
|
||||
}
|
||||
}
|
||||
|
||||
target: active
|
||||
target: root.active
|
||||
}
|
||||
|
||||
PersistentProperties {
|
||||
@@ -38,8 +58,10 @@ Singleton {
|
||||
reloadableId: "players"
|
||||
}
|
||||
|
||||
// qmllint disable unresolved-type
|
||||
CustomShortcut {
|
||||
description: "Toggle media playback"
|
||||
// qmllint enable unresolved-type
|
||||
name: "mediaToggle"
|
||||
|
||||
onPressed: {
|
||||
@@ -49,8 +71,10 @@ Singleton {
|
||||
}
|
||||
}
|
||||
|
||||
// qmllint disable unresolved-type
|
||||
CustomShortcut {
|
||||
description: "Previous track"
|
||||
// qmllint enable unresolved-type
|
||||
name: "mediaPrev"
|
||||
|
||||
onPressed: {
|
||||
@@ -60,8 +84,10 @@ Singleton {
|
||||
}
|
||||
}
|
||||
|
||||
// qmllint disable unresolved-type
|
||||
CustomShortcut {
|
||||
description: "Next track"
|
||||
// qmllint enable unresolved-type
|
||||
name: "mediaNext"
|
||||
|
||||
onPressed: {
|
||||
@@ -71,8 +97,10 @@ Singleton {
|
||||
}
|
||||
}
|
||||
|
||||
// qmllint disable unresolved-type
|
||||
CustomShortcut {
|
||||
description: "Stop media playback"
|
||||
// qmllint enable unresolved-type
|
||||
name: "mediaStop"
|
||||
|
||||
onPressed: root.active?.stop()
|
||||
|
||||
@@ -1,49 +0,0 @@
|
||||
pragma Singleton
|
||||
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import ZShell.Models
|
||||
import qs.Config
|
||||
import qs.Modules
|
||||
import qs.Helpers
|
||||
import qs.Paths
|
||||
|
||||
Searcher {
|
||||
id: root
|
||||
|
||||
property string actualCurrent: WallpaperPath.currentWallpaperPath
|
||||
readonly property string current: showPreview ? previewPath : actualCurrent
|
||||
property string previewPath
|
||||
property bool showPreview: false
|
||||
|
||||
function preview(path: string): void {
|
||||
previewPath = path;
|
||||
showPreview = true;
|
||||
}
|
||||
|
||||
function setWallpaper(path: string): void {
|
||||
actualCurrent = path;
|
||||
WallpaperPath.currentWallpaperPath = path;
|
||||
Quickshell.execDetached(["sh", "-c", `python3 ${Quickshell.shellPath("scripts/LockScreenBg.py")} --input_image=${root.actualCurrent} --output_path=${Paths.state}/lockscreen_bg.png`]);
|
||||
}
|
||||
|
||||
function stopPreview(): void {
|
||||
showPreview = false;
|
||||
Quickshell.execDetached(["sh", "-c", `python3 ${Quickshell.shellPath("scripts/SchemeColorGen.py")} --path=${root.actualCurrent} --thumbnail=${Paths.cache}/imagecache/thumbnail.jpg --output=${Paths.state}/scheme.json --scheme=${Config.colors.schemeType}`]);
|
||||
}
|
||||
|
||||
extraOpts: useFuzzy ? ({}) : ({
|
||||
forward: false
|
||||
})
|
||||
key: "relativePath"
|
||||
list: wallpapers.entries
|
||||
useFuzzy: true
|
||||
|
||||
FileSystemModel {
|
||||
id: wallpapers
|
||||
|
||||
filter: FileSystemModel.Images
|
||||
path: Config.general.wallpaperPath
|
||||
recursive: true
|
||||
}
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
pragma Singleton
|
||||
import QtQuick
|
||||
|
||||
QtObject {
|
||||
id: root
|
||||
|
||||
property Item activeMenu: null
|
||||
property Item activeTrigger: null
|
||||
|
||||
function close(menu) {
|
||||
if (!menu)
|
||||
return;
|
||||
|
||||
if (activeMenu === menu) {
|
||||
activeMenu = null;
|
||||
activeTrigger = null;
|
||||
}
|
||||
|
||||
menu.expanded = false;
|
||||
}
|
||||
|
||||
function closeActive() {
|
||||
if (activeMenu)
|
||||
activeMenu.expanded = false;
|
||||
|
||||
activeMenu = null;
|
||||
activeTrigger = null;
|
||||
}
|
||||
|
||||
function forget(menu) {
|
||||
if (activeMenu === menu) {
|
||||
activeMenu = null;
|
||||
activeTrigger = null;
|
||||
}
|
||||
}
|
||||
|
||||
function hit(item, scenePos) {
|
||||
if (!item || !item.visible)
|
||||
return false;
|
||||
|
||||
const p = item.mapFromItem(null, scenePos.x, scenePos.y);
|
||||
return item.contains(p);
|
||||
}
|
||||
|
||||
function open(menu, trigger) {
|
||||
if (activeMenu && activeMenu !== menu)
|
||||
activeMenu.expanded = false;
|
||||
|
||||
activeMenu = menu;
|
||||
activeTrigger = trigger || null;
|
||||
menu.expanded = true;
|
||||
}
|
||||
|
||||
function toggle(menu, trigger) {
|
||||
if (activeMenu === menu && menu.expanded)
|
||||
close(menu);
|
||||
else
|
||||
open(menu, trigger);
|
||||
}
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
pragma Singleton
|
||||
|
||||
import Quickshell
|
||||
import QtQuick
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
property string highlightedSetting: ""
|
||||
|
||||
function clear() {
|
||||
highlightedSetting = "";
|
||||
}
|
||||
|
||||
function highlight(settingName: string) {
|
||||
highlightedSetting = settingName;
|
||||
highlightTimer.restart();
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: highlightTimer
|
||||
|
||||
interval: 2000
|
||||
|
||||
onTriggered: root.clear()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
pragma Singleton
|
||||
|
||||
import Quickshell
|
||||
|
||||
Singleton {
|
||||
property var _regexCache: ({})
|
||||
|
||||
function testRegexList(filterList: list<string>, target: string): bool {
|
||||
const regexChecker = /^\^.*\$$/;
|
||||
for (const filter of filterList) {
|
||||
if (regexChecker.test(filter)) {
|
||||
let re = _regexCache[filter];
|
||||
if (!re) {
|
||||
re = new RegExp(filter);
|
||||
_regexCache[filter] = re;
|
||||
}
|
||||
if (re.test(target))
|
||||
return true;
|
||||
} else {
|
||||
if (filter === target)
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
+64
-6
@@ -1,17 +1,30 @@
|
||||
pragma Singleton
|
||||
|
||||
import qs.Config
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import QtQuick
|
||||
import qs.Config
|
||||
import qs.Paths
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
property string boardName
|
||||
property string boardVendor
|
||||
readonly property string device: {
|
||||
if (!boardName)
|
||||
return boardVendor;
|
||||
if (!boardVendor || boardName.toLowerCase().startsWith(boardVendor.toLowerCase()))
|
||||
return boardName;
|
||||
return `${boardVendor} ${boardName}`;
|
||||
}
|
||||
property string firmware
|
||||
property string hostname
|
||||
property bool isDefaultLogo: true
|
||||
property string kernel
|
||||
property string osId
|
||||
property list<string> osIdLike
|
||||
property string osLogo
|
||||
property string osLogo: Qt.resolvedUrl(`${Quickshell.shellDir}/assets/logo.svg`)
|
||||
property string osName
|
||||
property string osPrettyName
|
||||
readonly property string shell: Quickshell.env("SHELL").split("/").pop()
|
||||
@@ -19,6 +32,12 @@ Singleton {
|
||||
readonly property string user: Quickshell.env("USER")
|
||||
readonly property string wm: Quickshell.env("XDG_CURRENT_DESKTOP") || Quickshell.env("XDG_SESSION_DESKTOP")
|
||||
|
||||
function sanitiseDmi(s: string): string {
|
||||
const t = s.trim();
|
||||
const junk = ["to be filled by o.e.m.", "system product name", "system manufacturer", "system version", "default string", "o.e.m.", "not specified", "not applicable", "unknown", "none", ""];
|
||||
return junk.includes(t.toLowerCase()) ? "" : t;
|
||||
}
|
||||
|
||||
FileView {
|
||||
id: osRelease
|
||||
|
||||
@@ -35,7 +54,10 @@ Singleton {
|
||||
root.osIdLike = fd("ID_LIKE").split(" ");
|
||||
|
||||
const logo = Quickshell.iconPath(fd("LOGO"), true);
|
||||
if (Config.general.logo) {
|
||||
if (Config.general.logo === "zshell") {
|
||||
root.osLogo = Qt.resolvedUrl(`${Quickshell.shellDir}/assets/logo.svg`);
|
||||
root.isDefaultLogo = true;
|
||||
} else if (Config.general.logo) {
|
||||
root.osLogo = Quickshell.iconPath(Config.general.logo, true) || "file://" + Paths.absolutePath(Config.general.logo);
|
||||
root.isDefaultLogo = false;
|
||||
} else if (logo) {
|
||||
@@ -53,6 +75,39 @@ Singleton {
|
||||
target: Config.general
|
||||
}
|
||||
|
||||
FileView {
|
||||
path: "/proc/sys/kernel/osrelease"
|
||||
|
||||
onLoaded: root.kernel = text().trim()
|
||||
}
|
||||
|
||||
FileView {
|
||||
path: "/proc/sys/kernel/hostname"
|
||||
|
||||
onLoaded: root.hostname = text().trim()
|
||||
}
|
||||
|
||||
FileView {
|
||||
path: "/sys/class/dmi/id/sys_vendor"
|
||||
printErrors: false
|
||||
|
||||
onLoaded: root.boardVendor = root.sanitiseDmi(text())
|
||||
}
|
||||
|
||||
FileView {
|
||||
path: "/sys/class/dmi/id/product_name"
|
||||
printErrors: false
|
||||
|
||||
onLoaded: root.boardName = root.sanitiseDmi(text())
|
||||
}
|
||||
|
||||
FileView {
|
||||
path: "/sys/class/dmi/id/bios_version"
|
||||
printErrors: false
|
||||
|
||||
onLoaded: root.firmware = root.sanitiseDmi(text())
|
||||
}
|
||||
|
||||
Timer {
|
||||
interval: 15000
|
||||
repeat: true
|
||||
@@ -69,12 +124,15 @@ Singleton {
|
||||
onLoaded: {
|
||||
const up = parseInt(text().split(" ")[0] ?? 0);
|
||||
|
||||
const hours = Math.floor(up / 3600);
|
||||
const days = Math.floor(up / 86400);
|
||||
const hours = Math.floor((up % 86400) / 3600);
|
||||
const minutes = Math.floor((up % 3600) / 60);
|
||||
|
||||
let str = "";
|
||||
if (days > 0)
|
||||
str += `${days} day${days === 1 ? "" : "s"}`;
|
||||
if (hours > 0)
|
||||
str += `${hours} hour${hours === 1 ? "" : "s"}`;
|
||||
str += `${str ? ", " : ""}${hours} hour${hours === 1 ? "" : "s"}`;
|
||||
if (minutes > 0 || !str)
|
||||
str += `${str ? ", " : ""}${minutes} minute${minutes === 1 ? "" : "s"}`;
|
||||
root.uptime = str;
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
import QtQuick
|
||||
|
||||
Text {
|
||||
renderType: Text.NativeRendering
|
||||
textFormat: Text.PlainText
|
||||
}
|
||||
@@ -1,288 +0,0 @@
|
||||
pragma Singleton
|
||||
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
property list<DesktopEntry> entryList: []
|
||||
property var preppedIcons: []
|
||||
property var preppedIds: []
|
||||
property var preppedNames: []
|
||||
|
||||
// Dynamic fixups
|
||||
property var regexSubstitutions: [
|
||||
{
|
||||
"regex": /^steam_app_(\d+)$/,
|
||||
"replace": "steam_icon_$1"
|
||||
},
|
||||
{
|
||||
"regex": /Minecraft.*/,
|
||||
"replace": "minecraft-launcher"
|
||||
},
|
||||
{
|
||||
"regex": /.*polkit.*/,
|
||||
"replace": "system-lock-screen"
|
||||
},
|
||||
{
|
||||
"regex": /gcr.prompter/,
|
||||
"replace": "system-lock-screen"
|
||||
}
|
||||
]
|
||||
property real scoreThreshold: 0.2
|
||||
|
||||
// Manual overrides for tricky apps
|
||||
property var substitutions: ({
|
||||
"code-url-handler": "visual-studio-code",
|
||||
"Code": "visual-studio-code",
|
||||
"gnome-tweaks": "org.gnome.tweaks",
|
||||
"pavucontrol-qt": "pavucontrol",
|
||||
"wps": "wps-office2019-kprometheus",
|
||||
"wpsoffice": "wps-office2019-kprometheus",
|
||||
"footclient": "foot"
|
||||
})
|
||||
|
||||
function checkCleanMatch(str) {
|
||||
if (!str || str.length <= 3)
|
||||
return null;
|
||||
if (typeof DesktopEntries === 'undefined' || !DesktopEntries.byId)
|
||||
return null;
|
||||
|
||||
// Aggressive fallback: strip all separators
|
||||
const cleanStr = str.toLowerCase().replace(/[\.\-_]/g, '');
|
||||
const list = Array.from(entryList);
|
||||
|
||||
for (let i = 0; i < list.length; i++) {
|
||||
const entry = list[i];
|
||||
const cleanId = (entry.id || "").toLowerCase().replace(/[\.\-_]/g, '');
|
||||
if (cleanId.includes(cleanStr) || cleanStr.includes(cleanId)) {
|
||||
return entry;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function checkFuzzySearch(str) {
|
||||
if (typeof FuzzySort === 'undefined')
|
||||
return null;
|
||||
|
||||
// Check filenames (IDs) first
|
||||
if (preppedIds.length > 0) {
|
||||
let results = fuzzyQuery(str, preppedIds);
|
||||
if (results.length === 0) {
|
||||
const underscored = str.replace(/-/g, '_').toLowerCase();
|
||||
if (underscored !== str)
|
||||
results = fuzzyQuery(underscored, preppedIds);
|
||||
}
|
||||
if (results.length > 0)
|
||||
return results[0];
|
||||
}
|
||||
|
||||
// Then icons
|
||||
if (preppedIcons.length > 0) {
|
||||
const results = fuzzyQuery(str, preppedIcons);
|
||||
if (results.length > 0)
|
||||
return results[0];
|
||||
}
|
||||
|
||||
// Then names
|
||||
if (preppedNames.length > 0) {
|
||||
const results = fuzzyQuery(str, preppedNames);
|
||||
if (results.length > 0)
|
||||
return results[0];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// --- Lookup Helpers ---
|
||||
|
||||
function checkHeuristic(str) {
|
||||
if (typeof DesktopEntries !== 'undefined' && DesktopEntries.heuristicLookup) {
|
||||
const entry = DesktopEntries.heuristicLookup(str);
|
||||
if (entry)
|
||||
return entry;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function checkRegex(str) {
|
||||
for (let i = 0; i < regexSubstitutions.length; i++) {
|
||||
const sub = regexSubstitutions[i];
|
||||
const replaced = str.replace(sub.regex, sub.replace);
|
||||
if (replaced !== str) {
|
||||
return findAppEntry(replaced);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function checkSimpleTransforms(str) {
|
||||
if (typeof DesktopEntries === 'undefined' || !DesktopEntries.byId)
|
||||
return null;
|
||||
|
||||
const lower = str.toLowerCase();
|
||||
|
||||
const variants = [str, lower, getFromReverseDomain(str), getFromReverseDomain(str)?.toLowerCase(), normalizeWithHyphens(str), str.replace(/_/g, '-').toLowerCase(), str.replace(/-/g, '_').toLowerCase()];
|
||||
|
||||
for (let i = 0; i < variants.length; i++) {
|
||||
const variant = variants[i];
|
||||
if (variant) {
|
||||
const entry = DesktopEntries.byId(variant);
|
||||
if (entry)
|
||||
return entry;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function checkSubstitutions(str) {
|
||||
let effectiveStr = substitutions[str];
|
||||
if (!effectiveStr)
|
||||
effectiveStr = substitutions[str.toLowerCase()];
|
||||
|
||||
if (effectiveStr && effectiveStr !== str) {
|
||||
return findAppEntry(effectiveStr);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function distroLogoPath() {
|
||||
try {
|
||||
return (typeof OSInfo !== 'undefined' && OSInfo.distroIconPath) ? OSInfo.distroIconPath : "";
|
||||
} catch (e) {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
// Robust lookup strategy
|
||||
function findAppEntry(str) {
|
||||
if (!str || str.length === 0)
|
||||
return null;
|
||||
|
||||
let result = null;
|
||||
|
||||
if (result = checkHeuristic(str))
|
||||
return result;
|
||||
if (result = checkSubstitutions(str))
|
||||
return result;
|
||||
if (result = checkRegex(str))
|
||||
return result;
|
||||
if (result = checkSimpleTransforms(str))
|
||||
return result;
|
||||
if (result = checkFuzzySearch(str))
|
||||
return result;
|
||||
if (result = checkCleanMatch(str))
|
||||
return result;
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function fuzzyQuery(search, preppedData) {
|
||||
if (!search || !preppedData || preppedData.length === 0)
|
||||
return [];
|
||||
return FuzzySort.go(search, preppedData, {
|
||||
all: true,
|
||||
key: "name"
|
||||
}).map(r => r.obj.entry);
|
||||
}
|
||||
|
||||
function getFromReverseDomain(str) {
|
||||
if (!str)
|
||||
return "";
|
||||
return str.split('.').slice(-1)[0];
|
||||
}
|
||||
|
||||
// Deprecated shim
|
||||
function guessIcon(str) {
|
||||
const entry = findAppEntry(str);
|
||||
return entry ? entry.icon : "image-missing";
|
||||
}
|
||||
|
||||
function iconExists(iconName) {
|
||||
if (!iconName || iconName.length === 0)
|
||||
return false;
|
||||
if (iconName.startsWith("/"))
|
||||
return true;
|
||||
|
||||
const path = Quickshell.iconPath(iconName, true);
|
||||
return path && path.length > 0 && !path.includes("image-missing");
|
||||
}
|
||||
|
||||
function iconForAppId(appId, fallbackName) {
|
||||
const fallback = fallbackName || "application-x-executable";
|
||||
if (!appId)
|
||||
return iconFromName(fallback, fallback);
|
||||
|
||||
const entry = findAppEntry(appId);
|
||||
if (entry) {
|
||||
return iconFromName(entry.icon, fallback);
|
||||
}
|
||||
|
||||
return iconFromName(appId, fallback);
|
||||
}
|
||||
|
||||
function iconFromName(iconName, fallbackName) {
|
||||
const fallback = fallbackName || "application-x-executable";
|
||||
try {
|
||||
if (iconName && typeof Quickshell !== 'undefined' && Quickshell.iconPath) {
|
||||
const p = Quickshell.iconPath(iconName, fallback);
|
||||
if (p && p !== "")
|
||||
return p;
|
||||
}
|
||||
} catch (e) {}
|
||||
|
||||
try {
|
||||
return Quickshell.iconPath ? (Quickshell.iconPath(fallback, true) || "") : "";
|
||||
} catch (e2) {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeWithHyphens(str) {
|
||||
if (!str)
|
||||
return "";
|
||||
return str.toLowerCase().replace(/\s+/g, "-");
|
||||
}
|
||||
|
||||
function refreshEntries() {
|
||||
if (typeof DesktopEntries === 'undefined')
|
||||
return;
|
||||
|
||||
const values = Array.from(DesktopEntries.applications.values);
|
||||
if (values) {
|
||||
entryList = values.sort((a, b) => a.name.localeCompare(b.name));
|
||||
updatePreppedData();
|
||||
}
|
||||
}
|
||||
|
||||
function updatePreppedData() {
|
||||
if (typeof FuzzySort === 'undefined')
|
||||
return;
|
||||
|
||||
const list = Array.from(entryList);
|
||||
preppedNames = list.map(a => ({
|
||||
name: FuzzySort.prepare(`${a.name} `),
|
||||
entry: a
|
||||
}));
|
||||
preppedIcons = list.map(a => ({
|
||||
name: FuzzySort.prepare(`${a.icon} `),
|
||||
entry: a
|
||||
}));
|
||||
preppedIds = list.map(a => ({
|
||||
name: FuzzySort.prepare(`${a.id} `),
|
||||
entry: a
|
||||
}));
|
||||
}
|
||||
|
||||
Component.onCompleted: refreshEntries()
|
||||
|
||||
Connections {
|
||||
function onValuesChanged() {
|
||||
refreshEntries();
|
||||
}
|
||||
|
||||
target: DesktopEntries.applications
|
||||
}
|
||||
}
|
||||
@@ -21,6 +21,13 @@ Searcher {
|
||||
property bool recentlyChanged
|
||||
property bool showPreview: false
|
||||
|
||||
function getCategoryFor(w: FileSystemEntry): string {
|
||||
let category = w.parentDir.slice(Paths.wallsdir.length + 1);
|
||||
if (category.includes("/"))
|
||||
category = category.slice(0, category.indexOf("/"));
|
||||
return category;
|
||||
}
|
||||
|
||||
function getCrop(screen: string): var {
|
||||
return root.crops[screen];
|
||||
}
|
||||
|
||||
+107
-32
@@ -1,7 +1,7 @@
|
||||
pragma Singleton
|
||||
|
||||
import Quickshell
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import ZShell
|
||||
import qs.Config
|
||||
|
||||
@@ -12,15 +12,15 @@ Singleton {
|
||||
property var cc
|
||||
property string city
|
||||
readonly property string description: cc?.weatherDesc ?? qsTr("No weather")
|
||||
readonly property string feelsLike: `${cc?.feelsLikeC ?? 0}°C`
|
||||
readonly property string feelsLike: formatTemp(cc?.feelsLikeC)
|
||||
property list<var> forecast
|
||||
property list<var> hourlyForecast
|
||||
readonly property int humidity: cc?.humidity ?? 0
|
||||
readonly property string icon: cc ? Icons.getWeatherIcon(cc.weatherCode) : "cloud_alert"
|
||||
property string loc
|
||||
readonly property string sunrise: cc ? Qt.formatDateTime(new Date(cc.sunrise), "h:mm") : "--:--"
|
||||
readonly property string sunset: cc ? Qt.formatDateTime(new Date(cc.sunset), "h:mm") : "--:--"
|
||||
readonly property string temp: `${cc?.tempC ?? 0}°C`
|
||||
readonly property string sunrise: cc ? Qt.formatDateTime(new Date(cc.sunrise), Config.services.useTwelveHourClock ? "h:mm A" : "h:mm") : "--:--"
|
||||
readonly property string sunset: cc ? Qt.formatDateTime(new Date(cc.sunset), Config.services.useTwelveHourClock ? "h:mm A" : "h:mm") : "--:--"
|
||||
readonly property string temp: formatTemp(cc?.tempC)
|
||||
readonly property real windSpeed: cc?.windSpeed ?? 0
|
||||
|
||||
function fetchCityFromCoords(coords: string): void {
|
||||
@@ -29,29 +29,48 @@ Singleton {
|
||||
return;
|
||||
}
|
||||
|
||||
const [lat, lon] = coords.split(",");
|
||||
const url = `https://nominatim.openstreetmap.org/reverse?lat=${lat}&lon=${lon}&format=geocodejson`;
|
||||
Requests.get(url, text => {
|
||||
const [lat, lon] = coords.split(",").map(s => s.trim());
|
||||
const lang = Qt.locale().name.split("_")[0] || "en";
|
||||
|
||||
const fallbackToBigDataCloud = () => {
|
||||
const fallbackUrl = `https://api.bigdatacloud.net/data/reverse-geocode-client?latitude=${lat}&longitude=${lon}&localityLanguage=${lang}`;
|
||||
Requests.get(fallbackUrl, text => {
|
||||
const geo = JSON.parse(text);
|
||||
const geoCity = geo.city || geo.locality;
|
||||
if (geoCity) {
|
||||
city = fixCityName(geoCity);
|
||||
cachedCities.set(coords, city);
|
||||
} else {
|
||||
city = "Unknown City";
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const nominatimUrl = `https://nominatim.openstreetmap.org/reverse?lat=${lat}&lon=${lon}&format=geocodejson&accept-language=${lang}`;
|
||||
Requests.get(nominatimUrl, text => {
|
||||
const geo = JSON.parse(text).features?.[0]?.properties.geocoding;
|
||||
if (geo) {
|
||||
const geoCity = geo.type === "city" ? geo.name : geo.city;
|
||||
city = geoCity;
|
||||
cachedCities.set(coords, geoCity);
|
||||
} else {
|
||||
city = "Unknown City";
|
||||
if (geoCity) {
|
||||
city = fixCityName(geoCity);
|
||||
cachedCities.set(coords, city);
|
||||
return;
|
||||
}
|
||||
}
|
||||
});
|
||||
fallbackToBigDataCloud();
|
||||
}, fallbackToBigDataCloud);
|
||||
}
|
||||
|
||||
function fetchCoordsFromCity(cityName: string): void {
|
||||
const url = `https://geocoding-api.open-meteo.com/v1/search?name=${encodeURIComponent(cityName)}&count=1&language=en&format=json`;
|
||||
const lang = Qt.locale().name.split("_")[0] || "en";
|
||||
const url = `https://geocoding-api.open-meteo.com/v1/search?name=${encodeURIComponent(cityName)}&count=1&language=${lang}&format=json`;
|
||||
|
||||
Requests.get(url, text => {
|
||||
const json = JSON.parse(text);
|
||||
if (json.results && json.results.length > 0) {
|
||||
const result = json.results[0];
|
||||
loc = result.latitude + "," + result.longitude;
|
||||
city = result.name;
|
||||
city = fixCityName(result.name);
|
||||
} else {
|
||||
loc = "";
|
||||
reload();
|
||||
@@ -72,25 +91,21 @@ Singleton {
|
||||
cc = {
|
||||
weatherCode: json.current.weather_code,
|
||||
weatherDesc: getWeatherCondition(json.current.weather_code),
|
||||
tempC: Math.round(json.current.temperature_2m),
|
||||
tempF: Math.round(toFahrenheit(json.current.temperature_2m)),
|
||||
feelsLikeC: Math.round(json.current.apparent_temperature),
|
||||
feelsLikeF: Math.round(toFahrenheit(json.current.apparent_temperature)),
|
||||
tempC: json.current.temperature_2m,
|
||||
feelsLikeC: json.current.apparent_temperature,
|
||||
humidity: json.current.relative_humidity_2m,
|
||||
windSpeed: json.current.wind_speed_10m,
|
||||
isDay: json.current.is_day,
|
||||
sunrise: json.daily.sunrise[0],
|
||||
sunset: json.daily.sunset[0]
|
||||
sunrise: json.daily.sunrise[0].replace("T", " "),
|
||||
sunset: json.daily.sunset[0].replace("T", " ")
|
||||
};
|
||||
|
||||
const forecastList = [];
|
||||
for (let i = 0; i < json.daily.time.length; i++)
|
||||
forecastList.push({
|
||||
date: json.daily.time[i],
|
||||
maxTempC: Math.round(json.daily.temperature_2m_max[i]),
|
||||
maxTempF: Math.round(toFahrenheit(json.daily.temperature_2m_max[i])),
|
||||
minTempC: Math.round(json.daily.temperature_2m_min[i]),
|
||||
minTempF: Math.round(toFahrenheit(json.daily.temperature_2m_min[i])),
|
||||
date: json.daily.time[i].replace(/-/g, "/"),
|
||||
maxTempC: json.daily.temperature_2m_max[i],
|
||||
minTempC: json.daily.temperature_2m_min[i],
|
||||
weatherCode: json.daily.weather_code[i],
|
||||
icon: Icons.getWeatherIcon(json.daily.weather_code[i])
|
||||
});
|
||||
@@ -99,7 +114,8 @@ Singleton {
|
||||
const hourlyList = [];
|
||||
const now = new Date();
|
||||
for (let i = 0; i < json.hourly.time.length; i++) {
|
||||
const time = new Date(json.hourly.time[i]);
|
||||
const time = new Date(json.hourly.time[i].replace("T", " "));
|
||||
|
||||
if (time < now)
|
||||
continue;
|
||||
|
||||
@@ -107,7 +123,7 @@ Singleton {
|
||||
timestamp: json.hourly.time[i],
|
||||
hour: time.getHours(),
|
||||
tempC: Math.round(json.hourly.temperature_2m[i]),
|
||||
tempF: Math.round(toFahrenheit(json.hourly.temperature_2m[i])),
|
||||
precipChance: json.hourly.precipitation_probability[i],
|
||||
weatherCode: json.hourly.weather_code[i],
|
||||
icon: Icons.getWeatherIcon(json.hourly.weather_code[i])
|
||||
});
|
||||
@@ -116,6 +132,59 @@ Singleton {
|
||||
});
|
||||
}
|
||||
|
||||
function fixCityName(cityName: string): string {
|
||||
if (!cityName)
|
||||
return "";
|
||||
const mapping = {
|
||||
// Polish
|
||||
"Poznan": "Poznań",
|
||||
"Wroclaw": "Wrocław",
|
||||
"Krakow": "Kraków",
|
||||
"Gdansk": "Gdańsk",
|
||||
"Lodz": "Łódź",
|
||||
"Rzeszow": "Rzeszów",
|
||||
"Torun": "Toruń",
|
||||
"Bialystok": "Białystok",
|
||||
"Czestochowa": "Częstochowa",
|
||||
"Plock": "Płock",
|
||||
"Ruda Slaska": "Ruda Śląska",
|
||||
"Dabrowa Gornicza": "Dąbrowa Górnicza",
|
||||
"Elblag": "Elbląg",
|
||||
"Gorzow Wielkopolski": "Gorzów Wielkopolski",
|
||||
"Zielona Gora": "Zielona Góra",
|
||||
"Slupsk": "Słupsk",
|
||||
|
||||
// German
|
||||
"Munchen": "München",
|
||||
"Koln": "Köln",
|
||||
"Dusseldorf": "Düsseldorf",
|
||||
"Nurnberg": "Nürnberg",
|
||||
|
||||
// French & Spanish & Portuguese
|
||||
"Sao Paulo": "São Paulo",
|
||||
"Montreal": "Montréal",
|
||||
"Quebec": "Québec",
|
||||
"Bogota": "Bogotá",
|
||||
"Medellin": "Medellín",
|
||||
"Cordoba": "Córdoba",
|
||||
|
||||
// Turkish
|
||||
"Istanbul": "İstanbul",
|
||||
"Izmir": "İzmir",
|
||||
|
||||
// Scandinavian & others
|
||||
"Malmo": "Malmö",
|
||||
"Goteborg": "Göteborg",
|
||||
"Zurich": "Zürich",
|
||||
"Geneve": "Genève"
|
||||
};
|
||||
return mapping[cityName] || cityName;
|
||||
}
|
||||
|
||||
function formatTemp(temp: var): string {
|
||||
return Config.services.useFahrenheit ? `${temp !== undefined ? Math.round(toFahrenheit(temp)) : "--"}°F` : `${temp !== undefined ? Math.round(temp) : "--"}°C`;
|
||||
}
|
||||
|
||||
function getWeatherCondition(code: string): string {
|
||||
const conditions = {
|
||||
"0": "Clear",
|
||||
@@ -154,9 +223,9 @@ Singleton {
|
||||
if (!loc || loc.indexOf(",") === -1)
|
||||
return "";
|
||||
|
||||
const [lat, lon] = loc.split(",");
|
||||
const [lat, lon] = loc.split(",").map(s => s.trim());
|
||||
const baseUrl = "https://api.open-meteo.com/v1/forecast";
|
||||
const params = ["latitude=" + lat, "longitude=" + lon, "hourly=weather_code,temperature_2m", "daily=weather_code,temperature_2m_max,temperature_2m_min,sunrise,sunset", "current=temperature_2m,relative_humidity_2m,apparent_temperature,is_day,weather_code,wind_speed_10m", "timezone=auto", "forecast_days=7"];
|
||||
const params = ["latitude=" + lat, "longitude=" + lon, "hourly=weather_code,temperature_2m,precipitation_probability", "daily=weather_code,temperature_2m_max,temperature_2m_min,sunrise,sunset", "current=temperature_2m,relative_humidity_2m,apparent_temperature,is_day,weather_code,wind_speed_10m", "timezone=auto", "forecast_days=7"];
|
||||
|
||||
return baseUrl + "?" + params.join("&");
|
||||
}
|
||||
@@ -189,7 +258,14 @@ Singleton {
|
||||
|
||||
onLocChanged: fetchWeatherData()
|
||||
|
||||
// Refresh current location hourly
|
||||
Connections {
|
||||
function onWeatherLocationChanged(): void {
|
||||
root.reload();
|
||||
}
|
||||
|
||||
target: Config.services
|
||||
}
|
||||
|
||||
Timer {
|
||||
interval: 3600000 // 1 hour
|
||||
repeat: true
|
||||
@@ -200,6 +276,5 @@ Singleton {
|
||||
|
||||
ElapsedTimer {
|
||||
id: timer
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,56 +0,0 @@
|
||||
import Quickshell.Io
|
||||
|
||||
JsonObject {
|
||||
property list<var> week_0: [0, 0, 0, 0, 0, 0,]
|
||||
property list<var> week_1: [0, 0, 0, 0, 0, 0,]
|
||||
property list<var> week_10: [0, 0, 0, 0, 0, 0,]
|
||||
property list<var> week_11: [0, 0, 0, 0, 0, 0,]
|
||||
property list<var> week_12: [0, 0, 0, 0, 0, 0,]
|
||||
property list<var> week_13: [0, 0, 0, 0, 0, 0,]
|
||||
property list<var> week_14: [0, 0, 0, 0, 0, 0,]
|
||||
property list<var> week_15: [0, 0, 0, 0, 0, 0,]
|
||||
property list<var> week_16: [0, 0, 0, 0, 0, 0,]
|
||||
property list<var> week_17: [0, 0, 0, 0, 0, 0,]
|
||||
property list<var> week_18: [0, 0, 0, 0, 0, 0,]
|
||||
property list<var> week_19: [0, 0, 0, 0, 0, 0,]
|
||||
property list<var> week_2: [0, 0, 0, 0, 0, 0,]
|
||||
property list<var> week_20: [0, 0, 0, 0, 0, 0,]
|
||||
property list<var> week_21: [0, 0, 0, 0, 0, 0,]
|
||||
property list<var> week_22: [0, 0, 0, 0, 0, 0,]
|
||||
property list<var> week_23: [0, 0, 0, 0, 0, 0,]
|
||||
property list<var> week_24: [0, 0, 0, 0, 0, 0,]
|
||||
property list<var> week_25: [0, 0, 0, 0, 0, 0,]
|
||||
property list<var> week_26: [0, 0, 0, 0, 0, 0,]
|
||||
property list<var> week_27: [0, 0, 0, 0, 0, 0,]
|
||||
property list<var> week_28: [0, 0, 0, 0, 0, 0,]
|
||||
property list<var> week_29: [0, 0, 0, 0, 0, 0,]
|
||||
property list<var> week_3: [0, 0, 0, 0, 0, 0,]
|
||||
property list<var> week_30: [0, 0, 0, 0, 0, 0,]
|
||||
property list<var> week_31: [0, 0, 0, 0, 0, 0,]
|
||||
property list<var> week_32: [0, 0, 0, 0, 0, 0,]
|
||||
property list<var> week_33: [0, 0, 0, 0, 0, 0,]
|
||||
property list<var> week_34: [0, 0, 0, 0, 0, 0,]
|
||||
property list<var> week_35: [0, 0, 0, 0, 0, 0,]
|
||||
property list<var> week_36: [0, 0, 0, 0, 0, 0,]
|
||||
property list<var> week_37: [0, 0, 0, 0, 0, 0,]
|
||||
property list<var> week_38: [0, 0, 0, 0, 0, 0,]
|
||||
property list<var> week_39: [0, 0, 0, 0, 0, 0,]
|
||||
property list<var> week_4: [0, 0, 0, 0, 0, 0,]
|
||||
property list<var> week_40: [0, 0, 0, 0, 0, 0,]
|
||||
property list<var> week_41: [0, 0, 0, 0, 0, 0,]
|
||||
property list<var> week_42: [0, 0, 0, 0, 0, 0,]
|
||||
property list<var> week_43: [0, 0, 0, 0, 0, 0,]
|
||||
property list<var> week_44: [0, 0, 0, 0, 0, 0,]
|
||||
property list<var> week_45: [0, 0, 0, 0, 0, 0,]
|
||||
property list<var> week_46: [0, 0, 0, 0, 0, 0,]
|
||||
property list<var> week_47: [0, 0, 0, 0, 0, 0,]
|
||||
property list<var> week_48: [0, 0, 0, 0, 0, 0,]
|
||||
property list<var> week_49: [0, 0, 0, 0, 0, 0,]
|
||||
property list<var> week_5: [0, 0, 0, 0, 0, 0,]
|
||||
property list<var> week_50: [0, 0, 0, 0, 0, 0,]
|
||||
property list<var> week_51: [0, 0, 0, 0, 0, 0,]
|
||||
property list<var> week_6: [0, 0, 0, 0, 0, 0,]
|
||||
property list<var> week_7: [0, 0, 0, 0, 0, 0,]
|
||||
property list<var> week_8: [0, 0, 0, 0, 0, 0,]
|
||||
property list<var> week_9: [0, 0, 0, 0, 0, 0,]
|
||||
}
|
||||
+4
-20
@@ -3,12 +3,9 @@ pragma ComponentBehavior: Bound
|
||||
import Quickshell
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import qs.Components
|
||||
import qs.Modules
|
||||
import qs.Config
|
||||
import qs.Helpers
|
||||
import qs.Modules.SysTray
|
||||
import qs.Modules.SysTray.Widgets
|
||||
import qs.Modules.Network
|
||||
import qs.Modules.Updates
|
||||
|
||||
@@ -26,7 +23,7 @@ RowLayout {
|
||||
const ch = childAt(x, height / 2) as WrappedLoader;
|
||||
|
||||
if (!ch || ch?.id === "spacer") {
|
||||
if (!popouts.currentName.startsWith("traymenu"))
|
||||
if (!popouts.currentName.startsWith("traymenu") || Config.bar.tray.showOnHover)
|
||||
popouts.hasCurrent = false;
|
||||
return;
|
||||
}
|
||||
@@ -48,7 +45,7 @@ RowLayout {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!popouts.currentName.startsWith("traymenu"))
|
||||
if (!popouts.currentName.startsWith("traymenu") || Config.bar.tray.showOnHover)
|
||||
popouts.hasCurrent = false;
|
||||
}
|
||||
|
||||
@@ -57,19 +54,7 @@ RowLayout {
|
||||
const item = ch.item;
|
||||
const itemWidth = item.implicitWidth;
|
||||
|
||||
if (id === "audio" && Config.barConfig.popouts.audio) {
|
||||
popouts.currentName = "audio";
|
||||
popouts.currentCenter = Qt.binding(() => item.mapToItem(root, itemWidth / 2, 0).x);
|
||||
popouts.hasCurrent = true;
|
||||
} else if (id === "network" && Config.barConfig.popouts.network) {
|
||||
popouts.currentName = "network";
|
||||
popouts.currentCenter = Qt.binding(() => item.mapToItem(root, itemWidth / 2, 0).x);
|
||||
popouts.hasCurrent = true;
|
||||
} else if (id === "upower" && Config.barConfig.popouts.upower) {
|
||||
popouts.currentName = "upower";
|
||||
popouts.currentCenter = Qt.binding(() => item.mapToItem(root, itemWidth / 2, 0).x);
|
||||
popouts.hasCurrent = true;
|
||||
} else if (id === "updates") {
|
||||
if (id === "updates") {
|
||||
popouts.currentName = "updates";
|
||||
popouts.currentCenter = Qt.binding(() => item.mapToItem(root, itemWidth / 2, 0).x);
|
||||
popouts.hasCurrent = true;
|
||||
@@ -81,8 +66,7 @@ RowLayout {
|
||||
Repeater {
|
||||
id: repeater
|
||||
|
||||
// model: Config.barConfig.entries.filted(n => n.index > 50).sort(n => n.index)
|
||||
model: Config.barConfig.entries
|
||||
model: Config.bar.entries
|
||||
|
||||
DelegateChooser {
|
||||
role: "id"
|
||||
|
||||
@@ -15,15 +15,15 @@ import qs.Modules.Network
|
||||
Item {
|
||||
id: root
|
||||
|
||||
readonly property int contentHeight: Config.barConfig.height + padding * 2
|
||||
readonly property int exclusiveZone: Config.barConfig.autoHide ? Config.barConfig.border : contentHeight
|
||||
readonly property int contentHeight: Config.bar.height + padding * 2
|
||||
readonly property int exclusiveZone: Config.bar.autoHide ? Config.bar.border : contentHeight
|
||||
required property bool fullscreen
|
||||
property bool isHovered
|
||||
readonly property int padding: Math.max(Appearance.padding.smaller, Config.barConfig.border)
|
||||
readonly property int padding: Math.max(Appearance.padding.smaller, Config.bar.border)
|
||||
required property Wrapper popouts
|
||||
required property ClipWrapper popoutsWrapper
|
||||
required property ShellScreen screen
|
||||
readonly property bool shouldBeVisible: !fullscreen && (!Config.barConfig.autoHide || visibilities.bar || isHovered)
|
||||
readonly property bool shouldBeVisible: !fullscreen && (!Config.bar.autoHide || visibilities.bar || isHovered)
|
||||
readonly property int vPadding: 6
|
||||
required property PersistentProperties visibilities
|
||||
|
||||
@@ -31,8 +31,8 @@ Item {
|
||||
content.item?.checkPopout(x);
|
||||
}
|
||||
|
||||
implicitHeight: fullscreen ? 0 : Config.barConfig.border
|
||||
visible: height > Config.barConfig.border
|
||||
implicitHeight: fullscreen ? 0 : Config.bar.border
|
||||
visible: height > Config.bar.border
|
||||
|
||||
states: State {
|
||||
name: "visible"
|
||||
|
||||
@@ -38,11 +38,11 @@ Item {
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
anchors.margins: Config.barConfig.border + 1
|
||||
anchors.margins: Config.bar.border + 1
|
||||
anchors.topMargin: root.bar.implicitHeight + 1
|
||||
radius: Config.barConfig.border > 0 ? Config.barConfig.rounding : 0
|
||||
topLeftRadius: Config.barConfig.rounding
|
||||
topRightRadius: Config.barConfig.rounding
|
||||
radius: Config.bar.border > 0 ? Config.bar.rounding : 0
|
||||
topLeftRadius: Config.bar.rounding
|
||||
topRightRadius: Config.bar.rounding
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,21 +34,11 @@ Item {
|
||||
implicitHeight: 50
|
||||
radius: Appearance.rounding.full
|
||||
|
||||
MaterialIcon {
|
||||
id: searchIcon
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.margins: Appearance.padding.large
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: "search"
|
||||
}
|
||||
|
||||
CustomTextField {
|
||||
SearchBar {
|
||||
id: searchField
|
||||
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.left: searchIcon.right
|
||||
anchors.leftMargin: Appearance.spacing.small
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
color: DynamicColors.palette.m3onSurface
|
||||
@@ -170,7 +160,6 @@ Item {
|
||||
id: lineText
|
||||
|
||||
color: DynamicColors.palette.m3onSurface
|
||||
font.family: ClipHistory.previewIsCode ? Appearance.font.family.mono : Appearance.font.family.sans
|
||||
height: lineText.paintedHeight + Appearance.padding.extraSmall * 2
|
||||
text: lineRow.modelData.text.trim()
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
@@ -336,6 +325,8 @@ Item {
|
||||
}
|
||||
model: ScriptModel {
|
||||
values: ClipHistory.fuzzyQuery(searchField.text)
|
||||
|
||||
onValuesChanged: view.currentIndex = 0
|
||||
}
|
||||
|
||||
onCurrentItemChanged: {
|
||||
|
||||
+1
-1
@@ -14,7 +14,7 @@ CustomRect {
|
||||
required property PersistentProperties visibilities
|
||||
|
||||
color: visibilities.dashboard ? DynamicColors.palette.m3primary : DynamicColors.tPalette.m3surfaceContainer
|
||||
implicitHeight: Config.barConfig.height + Appearance.padding.smallest * 2
|
||||
implicitHeight: Config.bar.height + Appearance.padding.smallest * 2
|
||||
implicitWidth: timeText.contentWidth + Appearance.padding.normal * 2
|
||||
radius: Appearance.rounding.full
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ CustomRect {
|
||||
property bool tempEnabled: Hyprsunset.enabled
|
||||
|
||||
color: root.tempEnabled ? DynamicColors.palette.m3primary : DynamicColors.tPalette.m3surfaceContainer
|
||||
implicitHeight: Config.barConfig.height + Appearance.padding.smallest * 2
|
||||
implicitHeight: Config.bar.height + Appearance.padding.smallest * 2
|
||||
implicitWidth: implicitHeight
|
||||
radius: Appearance.rounding.full
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ import qs.Config
|
||||
CustomListView {
|
||||
id: root
|
||||
|
||||
required property CustomTextField search
|
||||
required property SearchBar search
|
||||
required property PersistentProperties visibilities
|
||||
|
||||
highlightFollowsCurrentItem: false
|
||||
|
||||
+44
-110
@@ -16,13 +16,13 @@ Item {
|
||||
readonly property int rounding: Appearance.rounding.large
|
||||
required property PersistentProperties visibilities
|
||||
|
||||
implicitHeight: searchWrapper.height + listWrapper.height + padding * 2
|
||||
implicitHeight: search.height + listWrapper.height + padding * 2
|
||||
implicitWidth: listWrapper.width + padding * 2
|
||||
|
||||
Item {
|
||||
id: listWrapper
|
||||
|
||||
anchors.bottom: searchWrapper.top
|
||||
anchors.bottom: search.top
|
||||
anchors.bottomMargin: root.padding
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
implicitHeight: list.height + root.padding
|
||||
@@ -32,7 +32,7 @@ Item {
|
||||
id: list
|
||||
|
||||
content: root
|
||||
maxHeight: root.maxHeight - searchWrapper.implicitHeight - root.padding * 3
|
||||
maxHeight: root.maxHeight - search.implicitHeight - root.padding * 3
|
||||
padding: root.padding
|
||||
panels: root.panels
|
||||
rounding: root.rounding
|
||||
@@ -41,131 +41,65 @@ Item {
|
||||
}
|
||||
}
|
||||
|
||||
CustomRect {
|
||||
id: searchWrapper
|
||||
SearchBar {
|
||||
id: search
|
||||
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.left: parent.left
|
||||
anchors.margins: root.padding
|
||||
anchors.right: parent.right
|
||||
color: DynamicColors.layer(DynamicColors.palette.m3surfaceContainer, 2)
|
||||
implicitHeight: Math.max(searchIcon.implicitHeight, search.implicitHeight, clearIcon.implicitHeight)
|
||||
radius: Appearance.rounding.smallest
|
||||
bottomPadding: Appearance.padding.larger
|
||||
placeholderText: qsTr("Type \"%1\" for commands").arg(Config.launcher.actionPrefix)
|
||||
topPadding: Appearance.padding.larger
|
||||
|
||||
MaterialIcon {
|
||||
id: searchIcon
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: root.padding + 10
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
color: DynamicColors.palette.m3onSurfaceVariant
|
||||
text: "search"
|
||||
}
|
||||
|
||||
CustomTextField {
|
||||
id: search
|
||||
|
||||
anchors.left: searchIcon.right
|
||||
anchors.leftMargin: Appearance.spacing.small
|
||||
anchors.right: clearIcon.left
|
||||
anchors.rightMargin: Appearance.spacing.small
|
||||
bottomPadding: Appearance.padding.larger
|
||||
placeholderText: qsTr("Type \"%1\" for commands").arg(Config.launcher.actionPrefix)
|
||||
topPadding: Appearance.padding.larger
|
||||
|
||||
Component.onCompleted: forceActiveFocus()
|
||||
Keys.onDownPressed: list.currentList?.decrementCurrentIndex()
|
||||
Keys.onEscapePressed: root.visibilities.launcher = false
|
||||
Keys.onPressed: event => {
|
||||
if (!Config.launcher.vimKeybinds)
|
||||
return;
|
||||
|
||||
if (event.modifiers & Qt.ControlModifier) {
|
||||
if (event.key === Qt.Key_J) {
|
||||
list.currentList?.incrementCurrentIndex();
|
||||
event.accepted = true;
|
||||
} else if (event.key === Qt.Key_K) {
|
||||
list.currentList?.decrementCurrentIndex();
|
||||
event.accepted = true;
|
||||
}
|
||||
} else if (event.key === Qt.Key_Tab) {
|
||||
Component.onCompleted: forceActiveFocus()
|
||||
Keys.onDownPressed: list.currentList?.decrementCurrentIndex()
|
||||
Keys.onEscapePressed: root.visibilities.launcher = false
|
||||
Keys.onPressed: event => {
|
||||
if (event.modifiers & Qt.ControlModifier) {
|
||||
if (event.key === Qt.Key_J) {
|
||||
list.currentList?.incrementCurrentIndex();
|
||||
event.accepted = true;
|
||||
} else if (event.key === Qt.Key_Backtab || (event.key === Qt.Key_Tab && (event.modifiers & Qt.ShiftModifier))) {
|
||||
} else if (event.key === Qt.Key_K) {
|
||||
list.currentList?.decrementCurrentIndex();
|
||||
event.accepted = true;
|
||||
}
|
||||
} else if (event.key === Qt.Key_Tab) {
|
||||
list.currentList?.incrementCurrentIndex();
|
||||
event.accepted = true;
|
||||
} else if (event.key === Qt.Key_Backtab || (event.key === Qt.Key_Tab && (event.modifiers & Qt.ShiftModifier))) {
|
||||
list.currentList?.decrementCurrentIndex();
|
||||
event.accepted = true;
|
||||
}
|
||||
Keys.onUpPressed: list.currentList?.incrementCurrentIndex()
|
||||
onAccepted: {
|
||||
const currentItem = list.currentList?.currentItem;
|
||||
if (currentItem) {
|
||||
if (list.showWallpapers) {
|
||||
if (DynamicColors.scheme === "dynamic" && currentItem.modelData.path !== Wallpapers.actualCurrent)
|
||||
Wallpapers.previewColourLock = true;
|
||||
Wallpapers.setWallpaper(currentItem.modelData.path);
|
||||
root.visibilities.launcher = false;
|
||||
} else if (text.startsWith(Config.launcher.actionPrefix)) {
|
||||
if (text.startsWith(`${Config.launcher.actionPrefix}calc `))
|
||||
currentItem.onClicked();
|
||||
else
|
||||
currentItem.modelData.onClicked(list.currentList);
|
||||
} else {
|
||||
Apps.launch(currentItem.modelData);
|
||||
root.visibilities.launcher = false;
|
||||
}
|
||||
}
|
||||
Keys.onUpPressed: list.currentList?.incrementCurrentIndex()
|
||||
onAccepted: {
|
||||
const currentItem = list.currentList?.currentItem;
|
||||
if (currentItem) {
|
||||
if (list.showWallpapers) {
|
||||
if (DynamicColors.scheme === "dynamic" && currentItem.modelData.path !== Wallpapers.actualCurrent)
|
||||
Wallpapers.previewColourLock = true;
|
||||
Wallpapers.setWallpaper(currentItem.modelData.path);
|
||||
root.visibilities.launcher = false;
|
||||
} else if (text.startsWith(Config.launcher.actionPrefix)) {
|
||||
if (text.startsWith(`${Config.launcher.actionPrefix}calc `))
|
||||
currentItem.onClicked();
|
||||
else
|
||||
currentItem.modelData.onClicked(list.currentList);
|
||||
} else {
|
||||
Apps.launch(currentItem.modelData);
|
||||
root.visibilities.launcher = false;
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
function onLauncherChanged(): void {
|
||||
if (!root.visibilities.launcher)
|
||||
search.text = "";
|
||||
}
|
||||
|
||||
target: root.visibilities
|
||||
}
|
||||
}
|
||||
|
||||
MaterialIcon {
|
||||
id: clearIcon
|
||||
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: root.padding + 10
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
color: DynamicColors.palette.m3onSurfaceVariant
|
||||
opacity: {
|
||||
if (!search.text)
|
||||
return 0;
|
||||
if (mouse.pressed)
|
||||
return 0.7;
|
||||
if (mouse.containsMouse)
|
||||
return 0.8;
|
||||
return 1;
|
||||
}
|
||||
text: "close"
|
||||
width: search.text ? implicitWidth : implicitWidth / 2
|
||||
|
||||
Behavior on opacity {
|
||||
Anim {
|
||||
duration: Appearance.anim.durations.small
|
||||
}
|
||||
}
|
||||
Behavior on width {
|
||||
Anim {
|
||||
duration: Appearance.anim.durations.small
|
||||
}
|
||||
Connections {
|
||||
function onLauncherChanged(): void {
|
||||
if (!root.visibilities.launcher)
|
||||
search.text = "";
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: mouse
|
||||
|
||||
anchors.fill: parent
|
||||
cursorShape: search.text ? Qt.PointingHandCursor : undefined
|
||||
hoverEnabled: true
|
||||
|
||||
onClicked: search.text = ""
|
||||
}
|
||||
target: root.visibilities
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ Item {
|
||||
required property int padding
|
||||
required property var panels
|
||||
required property int rounding
|
||||
required property CustomTextField search
|
||||
required property SearchBar search
|
||||
readonly property bool showWallpapers: search.text.startsWith(`${Config.launcher.actionPrefix}wallpaper `)
|
||||
required property PersistentProperties visibilities
|
||||
|
||||
|
||||
@@ -65,5 +65,20 @@ Item {
|
||||
width: root.width - icon.width - Appearance.rounding.normal * 2
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: favoriteIcon
|
||||
|
||||
active: root.modelData && Strings.testRegexList(Config.launcher.favoriteApps, root.modelData.id)
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
asynchronous: true
|
||||
|
||||
sourceComponent: MaterialIcon {
|
||||
color: DynamicColors.palette.m3primary
|
||||
fill: 1
|
||||
text: "favorite"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ Searcher {
|
||||
Variants {
|
||||
id: variants
|
||||
|
||||
model: Config.launcher.actions.filter(a => (a.enabled ?? true))
|
||||
model: Config.launcher.actions.filter(a => (a.enabled ?? true) && (Config.launcher.enableDangerousActions || !(a.dangerous ?? false)))
|
||||
|
||||
Action {
|
||||
}
|
||||
|
||||
@@ -74,7 +74,8 @@ Searcher {
|
||||
AppDb {
|
||||
id: appDb
|
||||
|
||||
entries: DesktopEntries.applications.values
|
||||
entries: DesktopEntries.applications.values.filter(a => !Strings.testRegexList(Config.launcher.hiddenApps, a.id))
|
||||
favoriteApps: Config.launcher.favoriteApps
|
||||
path: `${Paths.state}/apps.sqlite`
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ PathView {
|
||||
let outerMargins = 0;
|
||||
if ((visibilities.utilities || visibilities.sidebar) && panels.utilities.implicitWidth > outerMargins)
|
||||
outerMargins = panels.utilities.implicitWidth;
|
||||
const maxWidth = screen.width - Config.barConfig.rounding * 4 - (barMargins + outerMargins) * 2;
|
||||
const maxWidth = screen.width - Config.bar.rounding * 4 - (barMargins + outerMargins) * 2;
|
||||
|
||||
if (maxWidth <= 0)
|
||||
return 0;
|
||||
@@ -37,7 +37,7 @@ PathView {
|
||||
return visible;
|
||||
}
|
||||
required property var panels
|
||||
required property CustomTextField search
|
||||
required property SearchBar search
|
||||
required property var visibilities
|
||||
|
||||
cacheItemCount: 4
|
||||
|
||||
@@ -15,7 +15,7 @@ Item {
|
||||
if (visibilities.resources && panels.resourcesWrapper.x + panels.resourcesWrapper.width > root.x)
|
||||
max -= panels.resources.nonAnimHeight;
|
||||
if (panels.popouts.hasCurrent)
|
||||
if (panels.popouts.current.x + panels.popouts.current.width > root.x && panels.popouts.current.x < root.x + root.width)
|
||||
if (panels.popouts.current?.x + panels.popouts.current?.width > root.x && panels.popouts.current?.x < root.x + root.width)
|
||||
max -= panels.popouts.nonAnimHeight;
|
||||
return max;
|
||||
}
|
||||
|
||||
+31
-41
@@ -4,76 +4,66 @@ import qs.Components
|
||||
import qs.Helpers
|
||||
import qs.Config
|
||||
|
||||
RowLayout {
|
||||
CustomClippingRect {
|
||||
id: root
|
||||
|
||||
required property var lock
|
||||
|
||||
spacing: Appearance.spacing.large * 2
|
||||
implicitHeight: layout.implicitHeight
|
||||
implicitWidth: layout.implicitWidth
|
||||
radius: Appearance.rounding.large
|
||||
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Appearance.spacing.normal
|
||||
RowLayout {
|
||||
id: layout
|
||||
|
||||
CustomRect {
|
||||
anchors.fill: parent
|
||||
spacing: Appearance.spacing.large * 2
|
||||
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
color: DynamicColors.tPalette.m3surfaceContainer
|
||||
implicitHeight: weather.implicitHeight
|
||||
radius: Appearance.rounding.small
|
||||
topLeftRadius: Appearance.rounding.large
|
||||
spacing: Appearance.spacing.normal
|
||||
|
||||
WeatherInfo {
|
||||
id: weather
|
||||
|
||||
Layout.fillWidth: true
|
||||
rootHeight: root.height
|
||||
}
|
||||
}
|
||||
|
||||
CustomRect {
|
||||
Layout.fillWidth: true
|
||||
color: DynamicColors.tPalette.m3surfaceContainer
|
||||
implicitHeight: resources.implicitHeight
|
||||
radius: Appearance.rounding.small
|
||||
|
||||
Resources {
|
||||
id: resources
|
||||
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
}
|
||||
|
||||
CustomClippingRect {
|
||||
Layout.fillHeight: true
|
||||
Layout.fillWidth: true
|
||||
bottomLeftRadius: Appearance.rounding.large
|
||||
color: DynamicColors.tPalette.m3surfaceContainer
|
||||
radius: Appearance.rounding.small
|
||||
|
||||
Media {
|
||||
id: media
|
||||
|
||||
Layout.fillHeight: true
|
||||
Layout.fillWidth: true
|
||||
lock: root.lock
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Center {
|
||||
lock: root.lock
|
||||
}
|
||||
Center {
|
||||
lock: root.lock
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Appearance.spacing.normal
|
||||
|
||||
CustomRect {
|
||||
Layout.fillHeight: true
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
bottomRightRadius: Appearance.rounding.large
|
||||
color: DynamicColors.tPalette.m3surfaceContainer
|
||||
radius: Appearance.rounding.small
|
||||
topRightRadius: Appearance.rounding.large
|
||||
spacing: Appearance.spacing.normal
|
||||
|
||||
NotifDock {
|
||||
lock: root.lock
|
||||
CustomRect {
|
||||
Layout.fillHeight: true
|
||||
Layout.fillWidth: true
|
||||
bottomRightRadius: Appearance.rounding.large
|
||||
color: DynamicColors.tPalette.m3surfaceContainer
|
||||
radius: Appearance.rounding.small
|
||||
topRightRadius: Appearance.rounding.large
|
||||
|
||||
NotifDock {
|
||||
lock: root.lock
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -149,36 +149,71 @@ WlSessionLockSurface {
|
||||
Image {
|
||||
id: background
|
||||
|
||||
anchors.bottomMargin: -8 - lockContent.positions[lockContent.positionIndex].y
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: -8 + lockContent.positions[lockContent.positionIndex].x
|
||||
anchors.rightMargin: -8 - lockContent.positions[lockContent.positionIndex].x
|
||||
anchors.topMargin: -8 + lockContent.positions[lockContent.positionIndex].y
|
||||
fillMode: Image.PreserveAspectCrop
|
||||
source: WallpaperPath.lockscreenBg
|
||||
|
||||
Behavior on anchors.bottomMargin {
|
||||
Anim {
|
||||
duration: 5000
|
||||
}
|
||||
}
|
||||
Behavior on anchors.leftMargin {
|
||||
Anim {
|
||||
duration: 5000
|
||||
}
|
||||
}
|
||||
Behavior on anchors.rightMargin {
|
||||
Anim {
|
||||
duration: 5000
|
||||
}
|
||||
}
|
||||
Behavior on anchors.topMargin {
|
||||
Anim {
|
||||
duration: 5000
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
id: lockContent
|
||||
|
||||
property int positionIndex: 0
|
||||
readonly property var positions: [Qt.point(0, 0), Qt.point(4, 0), Qt.point(4, 4), Qt.point(0, 4), Qt.point(-4, 4), Qt.point(-4, 0), Qt.point(-4, -4), Qt.point(0, -4), Qt.point(4, -4),]
|
||||
readonly property int radius: size / 4 * Appearance.rounding.scale
|
||||
readonly property int size: lockIcon.implicitHeight + Appearance.padding.large * 4
|
||||
|
||||
anchors.centerIn: parent
|
||||
anchors.horizontalCenterOffset: positions[positionIndex].x
|
||||
anchors.verticalCenterOffset: positions[positionIndex].y
|
||||
implicitHeight: size
|
||||
implicitWidth: size
|
||||
scale: 0
|
||||
|
||||
// MultiEffect {
|
||||
// anchors.fill: lockBg
|
||||
// autoPaddingEnabled: false
|
||||
// blur: 1
|
||||
// blurEnabled: true
|
||||
// blurMax: 64
|
||||
// maskEnabled: true
|
||||
// maskSource: lockBg
|
||||
//
|
||||
// source: ShaderEffectSource {
|
||||
// sourceItem: background
|
||||
// sourceRect: Qt.rect(lockBg.x, lockBg.y, lockBg.width, lockBg, height)
|
||||
// }
|
||||
// }
|
||||
Behavior on anchors.horizontalCenterOffset {
|
||||
Anim {
|
||||
duration: 5000
|
||||
}
|
||||
}
|
||||
Behavior on anchors.verticalCenterOffset {
|
||||
Anim {
|
||||
duration: 5000
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
interval: 120000
|
||||
repeat: true
|
||||
running: true
|
||||
|
||||
onTriggered: {
|
||||
lockContent.positionIndex = (lockContent.positionIndex + 1) % lockContent.positions.length;
|
||||
}
|
||||
}
|
||||
|
||||
CustomRect {
|
||||
id: lockBg
|
||||
|
||||
+54
-145
@@ -1,201 +1,110 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import qs.Modules
|
||||
import Quickshell
|
||||
import ZShell.Components
|
||||
import qs.Components
|
||||
import qs.Helpers
|
||||
import qs.Config
|
||||
import qs.Helpers
|
||||
|
||||
Item {
|
||||
CustomClippingRect {
|
||||
id: root
|
||||
|
||||
required property var lock
|
||||
|
||||
anchors.fill: parent
|
||||
color: DynamicColors.tPalette.m3surfaceContainer
|
||||
implicitHeight: layout.implicitHeight + layout.anchors.margins * 2
|
||||
radius: Appearance.rounding.small
|
||||
|
||||
FadeImage {
|
||||
id: image
|
||||
|
||||
Image {
|
||||
anchors.fill: parent
|
||||
asynchronous: true
|
||||
fillMode: Image.PreserveAspectCrop
|
||||
layer.enabled: true
|
||||
opacity: status === Image.Ready ? 1 : 0
|
||||
source: Players.active?.trackArtUrl ?? ""
|
||||
sourceSize.height: height
|
||||
sourceSize.width: width
|
||||
|
||||
layer.effect: OpacityMask {
|
||||
maskSource: mask
|
||||
source: Players.getArtUrl(Players.active)
|
||||
sourceSize: {
|
||||
const dpr = (QsWindow.window as QsWindow)?.devicePixelRatio ?? 1;
|
||||
return Qt.size(width * dpr, height * dpr);
|
||||
}
|
||||
|
||||
Behavior on opacity {
|
||||
Anim {
|
||||
duration: Appearance.anim.durations.extraLarge
|
||||
type: Anim.StandardExtraLarge
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: mask
|
||||
|
||||
anchors.fill: parent
|
||||
layer.enabled: true
|
||||
visible: false
|
||||
|
||||
gradient: Gradient {
|
||||
orientation: Gradient.Horizontal
|
||||
|
||||
GradientStop {
|
||||
color: Qt.rgba(0, 0, 0, 0.5)
|
||||
position: 0
|
||||
}
|
||||
|
||||
GradientStop {
|
||||
color: Qt.rgba(0, 0, 0, 0.2)
|
||||
position: 0.4
|
||||
}
|
||||
|
||||
GradientStop {
|
||||
color: Qt.rgba(0, 0, 0, 0)
|
||||
position: 0.8
|
||||
}
|
||||
CustomRect {
|
||||
anchors.fill: parent
|
||||
color: DynamicColors.palette.m3surface
|
||||
opacity: 0.7
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
id: layout
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.margins: Appearance.padding.large
|
||||
|
||||
CustomText {
|
||||
Layout.bottomMargin: Appearance.spacing.larger
|
||||
Layout.topMargin: Appearance.padding.large
|
||||
color: DynamicColors.palette.m3onSurfaceVariant
|
||||
font.family: Appearance.font.family.mono
|
||||
font.weight: 500
|
||||
text: qsTr("Now playing")
|
||||
}
|
||||
anchors.left: parent.left
|
||||
anchors.margins: Appearance.padding.extraLarge
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Appearance.spacing.extraSmall
|
||||
|
||||
CustomText {
|
||||
Layout.fillWidth: true
|
||||
animate: true
|
||||
color: DynamicColors.palette.m3primary
|
||||
elide: Text.ElideRight
|
||||
font.family: Appearance.font.family.mono
|
||||
font.pointSize: Appearance.font.size.large
|
||||
font.weight: 600
|
||||
font.pointSize: Appearance.font.size.medium
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
text: Players.active?.trackArtist ?? qsTr("No media")
|
||||
text: (Players.active?.trackTitle ?? qsTr("Nothing playing")) || qsTr("Unknown track")
|
||||
}
|
||||
|
||||
CustomText {
|
||||
Layout.fillWidth: true
|
||||
animate: true
|
||||
color: DynamicColors.palette.m3onSurfaceVariant
|
||||
elide: Text.ElideRight
|
||||
font.family: Appearance.font.family.mono
|
||||
font.pointSize: Appearance.font.size.larger
|
||||
font.pointSize: Appearance.font.size.small
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
text: Players.active?.trackTitle ?? qsTr("No media")
|
||||
text: (Players.active?.trackArtist ?? qsTr("Try playing some music!")) || qsTr("Unknown artist")
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
ButtonRow {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
Layout.bottomMargin: Appearance.padding.large
|
||||
Layout.topMargin: Appearance.spacing.large * 1.2
|
||||
spacing: Appearance.spacing.large
|
||||
|
||||
PlayerControl {
|
||||
function onClicked(): void {
|
||||
if (Players.active?.canGoPrevious)
|
||||
Players.active.previous();
|
||||
}
|
||||
Layout.topMargin: Appearance.spacing.small
|
||||
spacing: Appearance.spacing.extraSmall
|
||||
|
||||
IconButton {
|
||||
enabled: Players.active?.canGoPrevious
|
||||
icon: "skip_previous"
|
||||
isRound: true
|
||||
shapeMorph: true
|
||||
type: IconButton.Tonal
|
||||
|
||||
onClicked: Players.active?.previous()
|
||||
}
|
||||
|
||||
PlayerControl {
|
||||
function onClicked(): void {
|
||||
if (Players.active?.canTogglePlaying)
|
||||
Players.active.togglePlaying();
|
||||
}
|
||||
IconButton {
|
||||
checked: Players.active?.isPlaying ?? false
|
||||
enabled: Players.active?.canTogglePlaying
|
||||
icon: Players.active?.isPlaying ? "pause" : "play_arrow"
|
||||
implicitWidth: implicitHeight + Appearance.padding.largeIncreased * 2
|
||||
isRound: true
|
||||
shapeMorph: true
|
||||
|
||||
active: Players.active?.isPlaying ?? false
|
||||
animate: true
|
||||
icon: active ? "pause" : "play_arrow"
|
||||
level: active ? 2 : 1
|
||||
set_color: "Primary"
|
||||
onClicked: Players.active?.togglePlaying()
|
||||
}
|
||||
|
||||
PlayerControl {
|
||||
function onClicked(): void {
|
||||
if (Players.active?.canGoNext)
|
||||
Players.active.next();
|
||||
}
|
||||
|
||||
IconButton {
|
||||
enabled: Players.active?.canGoNext
|
||||
icon: "skip_next"
|
||||
}
|
||||
}
|
||||
}
|
||||
isRound: true
|
||||
shapeMorph: true
|
||||
type: IconButton.Tonal
|
||||
|
||||
component PlayerControl: CustomRect {
|
||||
id: control
|
||||
|
||||
property bool active
|
||||
property alias animate: controlIcon.animate
|
||||
property alias icon: controlIcon.text
|
||||
property int level: 1
|
||||
property string set_color: "Secondary"
|
||||
|
||||
function onClicked(): void {
|
||||
}
|
||||
|
||||
Layout.preferredWidth: implicitWidth + (controlState.pressed ? Appearance.padding.normal * 2 : active ? Appearance.padding.small * 2 : 0)
|
||||
color: active ? DynamicColors.palette[`m3${set_color.toLowerCase()}`] : DynamicColors.palette[`m3${set_color.toLowerCase()}Container`]
|
||||
implicitHeight: controlIcon.implicitHeight + Appearance.padding.normal * 2
|
||||
implicitWidth: controlIcon.implicitWidth + Appearance.padding.large * 2
|
||||
radius: active || controlState.pressed ? Appearance.rounding.small : Appearance.rounding.normal
|
||||
|
||||
Behavior on Layout.preferredWidth {
|
||||
Anim {
|
||||
duration: Appearance.anim.durations.expressiveFastSpatial
|
||||
easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial
|
||||
}
|
||||
}
|
||||
Behavior on radius {
|
||||
Anim {
|
||||
duration: Appearance.anim.durations.expressiveFastSpatial
|
||||
easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial
|
||||
}
|
||||
}
|
||||
|
||||
Elevation {
|
||||
anchors.fill: parent
|
||||
level: controlState.containsMouse && !controlState.pressed ? control.level + 1 : control.level
|
||||
radius: parent.radius
|
||||
z: -1
|
||||
}
|
||||
|
||||
StateLayer {
|
||||
id: controlState
|
||||
|
||||
color: control.active ? DynamicColors.palette[`m3on${control.set_color}`] : DynamicColors.palette[`m3on${control.set_color}Container`]
|
||||
|
||||
onClicked: {
|
||||
control.onClicked();
|
||||
}
|
||||
}
|
||||
|
||||
MaterialIcon {
|
||||
id: controlIcon
|
||||
|
||||
anchors.centerIn: parent
|
||||
color: control.active ? DynamicColors.palette[`m3on${control.set_color}`] : DynamicColors.palette[`m3on${control.set_color}Container`]
|
||||
fill: control.active ? 1 : 0
|
||||
font.pointSize: Appearance.font.size.large
|
||||
|
||||
Behavior on fill {
|
||||
Anim {
|
||||
}
|
||||
onClicked: Players.active?.next()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+48
-47
@@ -1,81 +1,82 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import M3Shapes
|
||||
import ZShell.Services
|
||||
import qs.Components
|
||||
import qs.Helpers
|
||||
import qs.Config
|
||||
import qs.Effects
|
||||
|
||||
GridLayout {
|
||||
CustomRect {
|
||||
id: root
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.margins: Appearance.padding.large
|
||||
anchors.right: parent.right
|
||||
columnSpacing: Appearance.spacing.large
|
||||
columns: 2
|
||||
rowSpacing: Appearance.spacing.large
|
||||
rows: 1
|
||||
readonly property real fontScale: {
|
||||
const diff = width / 391 - 1; // 391 is the width at 1080 height screen
|
||||
return 1 + Math.pow(Math.abs(diff), 0.8) * Math.sign(diff);
|
||||
}
|
||||
|
||||
color: DynamicColors.tPalette.m3surfaceContainer
|
||||
implicitHeight: layout.implicitHeight + layout.anchors.margins * 2
|
||||
radius: Appearance.rounding.small
|
||||
|
||||
ServiceRef {
|
||||
service: Cpu
|
||||
}
|
||||
|
||||
ServiceRef {
|
||||
service: Memory
|
||||
}
|
||||
|
||||
ServiceRef {
|
||||
service: Cpu
|
||||
service: Storage
|
||||
}
|
||||
|
||||
Resource {
|
||||
Layout.bottomMargin: Appearance.padding.large
|
||||
Layout.topMargin: Appearance.padding.large
|
||||
fgColor: DynamicColors.palette.m3primary
|
||||
icon: "memory"
|
||||
value: Cpu.percentage
|
||||
RowLayout {
|
||||
id: layout
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.margins: Appearance.padding.large
|
||||
spacing: Appearance.spacing.large
|
||||
|
||||
Resource {
|
||||
id: cpu
|
||||
|
||||
fgColor: DynamicColors.palette.m3primary
|
||||
icon: "memory"
|
||||
value: Cpu.percentage
|
||||
}
|
||||
|
||||
Resource {
|
||||
fgColor: DynamicColors.palette.m3tertiary
|
||||
icon: "memory_alt"
|
||||
value: Memory.percentage
|
||||
}
|
||||
|
||||
Resource {
|
||||
fgColor: DynamicColors.palette.m3secondary
|
||||
icon: "hard_disk"
|
||||
value: Storage.percentage
|
||||
}
|
||||
}
|
||||
|
||||
Resource {
|
||||
Layout.bottomMargin: Appearance.padding.large
|
||||
Layout.topMargin: Appearance.padding.large
|
||||
fgColor: DynamicColors.palette.m3secondary
|
||||
icon: "memory_alt"
|
||||
value: Memory.percentage
|
||||
}
|
||||
|
||||
component Resource: CustomRect {
|
||||
component Resource: CircularProgress {
|
||||
id: res
|
||||
|
||||
required property color fgColor
|
||||
required property string icon
|
||||
required property real value
|
||||
|
||||
Layout.fillWidth: true
|
||||
color: DynamicColors.layer(DynamicColors.palette.m3surfaceContainerHigh, 2)
|
||||
implicitHeight: width
|
||||
radius: Appearance.rounding.large
|
||||
implicitSize: width
|
||||
|
||||
Behavior on value {
|
||||
Behavior on clampedVal {
|
||||
Anim {
|
||||
duration: Appearance.anim.durations.large
|
||||
}
|
||||
}
|
||||
|
||||
CircularProgress {
|
||||
id: circ
|
||||
|
||||
anchors.fill: parent
|
||||
bgColor: DynamicColors.layer(DynamicColors.palette.m3surfaceContainerHighest, 3)
|
||||
fgColor: res.fgColor
|
||||
padding: Appearance.padding.large * 3
|
||||
strokeWidth: width < 200 ? Appearance.padding.smaller : Appearance.padding.normal
|
||||
value: res.value
|
||||
}
|
||||
|
||||
MaterialIcon {
|
||||
id: icon
|
||||
|
||||
anchors.centerIn: parent
|
||||
color: res.fgColor
|
||||
font.pointSize: (circ.arcRadius * 0.7) || 1
|
||||
font.weight: 600
|
||||
font.pointSize: Appearance.font.size.extraLarge
|
||||
text: res.icon
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import qs.Components
|
||||
import qs.Config
|
||||
import qs.Helpers
|
||||
|
||||
ColumnLayout {
|
||||
id: root
|
||||
|
||||
required property int rootHeight
|
||||
|
||||
spacing: Appearance.spacing.extraSmall
|
||||
|
||||
CustomText {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
animate: true
|
||||
color: DynamicColors.palette.m3onSurfaceVariant
|
||||
font.pointSize: Appearance.font.size.large
|
||||
text: Weather.description
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
spacing: Appearance.spacing.small
|
||||
|
||||
CustomText {
|
||||
id: temp
|
||||
|
||||
animate: true
|
||||
color: DynamicColors.palette.m3primary
|
||||
font.pointSize: Appearance.font.size.large
|
||||
text: Weather.temp
|
||||
}
|
||||
|
||||
MaterialIcon {
|
||||
animate: true
|
||||
color: DynamicColors.palette.m3secondary
|
||||
text: Weather.icon
|
||||
}
|
||||
}
|
||||
|
||||
CustomText {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
animate: true
|
||||
color: DynamicColors.palette.m3onSurfaceVariant
|
||||
font.pointSize: Appearance.font.size.large
|
||||
text: qsTr("Feels like %1").arg(Weather.temp)
|
||||
visible: root.rootHeight > 550
|
||||
}
|
||||
|
||||
CustomText {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
animate: true
|
||||
color: DynamicColors.palette.m3onSurfaceVariant
|
||||
font.pointSize: Appearance.font.size.medium
|
||||
text: {
|
||||
const today = Weather.forecast[0];
|
||||
return qsTr("High %1 • Low %2").arg(Weather.formatTemp(today?.maxTempC)).arg(Weather.formatTemp(today?.minTempC));
|
||||
}
|
||||
visible: root.rootHeight > 550
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import M3Shapes
|
||||
import ZShell
|
||||
import qs.Components
|
||||
import qs.Helpers
|
||||
import qs.Config
|
||||
|
||||
CustomRect {
|
||||
id: root
|
||||
|
||||
color: DynamicColors.layer(DynamicColors.palette.m3surfaceContainerHigh, 2)
|
||||
implicitHeight: header.anchors.margins + header.implicitHeight + Appearance.spacing.small + layout.implicitHeight + layout.anchors.bottomMargin
|
||||
radius: Appearance.rounding.small
|
||||
|
||||
RowLayout {
|
||||
id: header
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.margins: Appearance.padding.largeIncreased
|
||||
anchors.top: parent.top
|
||||
spacing: Appearance.spacing.small
|
||||
|
||||
MaterialIcon {
|
||||
Layout.topMargin: Math.round(fontInfo.pointSize * 0.12)
|
||||
font.pointSize: Appearance.font.size.medium
|
||||
text: "schedule"
|
||||
}
|
||||
|
||||
CustomText {
|
||||
id: title
|
||||
|
||||
font.pointSize: Appearance.font.size.medium
|
||||
text: qsTr("Hourly forecast")
|
||||
}
|
||||
}
|
||||
|
||||
VerticalFadeListView {
|
||||
id: layout
|
||||
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.bottomMargin: Appearance.padding.largeIncreased
|
||||
anchors.left: parent.left
|
||||
anchors.margins: Appearance.padding.large
|
||||
anchors.right: parent.right
|
||||
implicitHeight: contentItem.childrenRect.height
|
||||
model: Weather.hourlyForecast
|
||||
orientation: VerticalFadeListView.Horizontal
|
||||
spacing: Appearance.spacing.normal
|
||||
|
||||
delegate: ColumnLayout {
|
||||
id: hour
|
||||
|
||||
readonly property var cond: modelData
|
||||
required property int index
|
||||
required property var modelData
|
||||
|
||||
spacing: Appearance.spacing.extraSmall
|
||||
|
||||
MaterialShape {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
color: Qt.alpha(DynamicColors.palette.m3primary, hour.index === 0 ? 1 : 0)
|
||||
implicitSize: temp.implicitHeight + Appearance.padding.normal * 2
|
||||
shape: MaterialShape.Cookie4Sided
|
||||
|
||||
Behavior on color {
|
||||
CAnim {
|
||||
}
|
||||
}
|
||||
|
||||
CustomText {
|
||||
id: temp
|
||||
|
||||
anchors.centerIn: parent
|
||||
color: hour.index === 0 ? DynamicColors.palette.m3onPrimary : DynamicColors.palette.m3onSurface
|
||||
font.pointSize: Appearance.font.size.medium
|
||||
text: Weather.formatTemp(hour.cond.tempC).slice(0, -1) // Remove C/F
|
||||
}
|
||||
}
|
||||
|
||||
MaterialIcon {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
color: DynamicColors.palette.m3secondary
|
||||
font.pointSize: Appearance.font.size.extraLarge
|
||||
text: hour.cond.icon
|
||||
}
|
||||
|
||||
CustomText {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
color: DynamicColors.palette.m3primary
|
||||
text: hour.cond.precipChance + "%"
|
||||
}
|
||||
|
||||
CustomText {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
Layout.topMargin: Appearance.spacing.extraSmall
|
||||
color: DynamicColors.palette.m3onSurfaceVariant
|
||||
font.pointSize: Appearance.font.size.small
|
||||
text: hour.index === 0 ? qsTr("Now") : Qt.formatDateTime(new Date(hour.cond.timestamp.replace("T", " ")), Config.services.useTwelveHourClock ? "ha" : "hh:00")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+34
-150
@@ -1,162 +1,23 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import qs.Modules.Lock.Weather
|
||||
import qs.Config
|
||||
import qs.Components
|
||||
import qs.Helpers
|
||||
import qs.Config
|
||||
|
||||
ColumnLayout {
|
||||
CustomRect {
|
||||
id: root
|
||||
|
||||
required property int rootHeight
|
||||
readonly property bool showForecast: rootHeight >= 700
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.margins: Appearance.padding.large * 2
|
||||
anchors.right: parent.right
|
||||
spacing: Appearance.spacing.small
|
||||
|
||||
Loader {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
Layout.bottomMargin: -Appearance.padding.large
|
||||
Layout.topMargin: Appearance.padding.large * 2
|
||||
active: root.rootHeight > 610
|
||||
visible: active
|
||||
|
||||
sourceComponent: CustomText {
|
||||
color: DynamicColors.palette.m3primary
|
||||
font.pointSize: Appearance.font.size.extraLarge
|
||||
font.weight: 500
|
||||
text: qsTr("Weather")
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Appearance.spacing.large
|
||||
|
||||
MaterialIcon {
|
||||
animate: true
|
||||
color: DynamicColors.palette.m3secondary
|
||||
font.pointSize: Appearance.font.size.extraLarge * 2.5
|
||||
text: Weather.icon
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
spacing: Appearance.spacing.small
|
||||
|
||||
CustomText {
|
||||
Layout.fillWidth: true
|
||||
animate: true
|
||||
color: DynamicColors.palette.m3secondary
|
||||
elide: Text.ElideRight
|
||||
font.pointSize: Appearance.font.size.large
|
||||
font.weight: 500
|
||||
text: Weather.description
|
||||
}
|
||||
|
||||
CustomText {
|
||||
Layout.fillWidth: true
|
||||
animate: true
|
||||
color: DynamicColors.palette.m3onSurfaceVariant
|
||||
elide: Text.ElideRight
|
||||
font.pointSize: Appearance.font.size.normal
|
||||
text: qsTr("Humidity: %1%").arg(Weather.humidity)
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
Layout.rightMargin: Appearance.padding.smaller
|
||||
active: root.width > 400
|
||||
visible: active
|
||||
|
||||
sourceComponent: ColumnLayout {
|
||||
spacing: Appearance.spacing.small
|
||||
|
||||
CustomText {
|
||||
Layout.fillWidth: true
|
||||
animate: true
|
||||
color: DynamicColors.palette.m3primary
|
||||
elide: Text.ElideLeft
|
||||
font.pointSize: Appearance.font.size.extraLarge
|
||||
font.weight: 500
|
||||
horizontalAlignment: Text.AlignRight
|
||||
text: Weather.temp
|
||||
}
|
||||
|
||||
CustomText {
|
||||
Layout.fillWidth: true
|
||||
animate: true
|
||||
color: DynamicColors.palette.m3outline
|
||||
elide: Text.ElideLeft
|
||||
font.pointSize: Appearance.font.size.smaller
|
||||
horizontalAlignment: Text.AlignRight
|
||||
text: qsTr("Feels like: %1").arg(Weather.feelsLike)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: forecastLoader
|
||||
|
||||
Layout.bottomMargin: Appearance.padding.large * 2
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: Appearance.spacing.smaller
|
||||
active: root.rootHeight > 820
|
||||
visible: active
|
||||
|
||||
sourceComponent: RowLayout {
|
||||
spacing: Appearance.spacing.large
|
||||
|
||||
Repeater {
|
||||
model: {
|
||||
const forecast = Weather.hourlyForecast;
|
||||
const count = root.width < 320 ? 3 : root.width < 400 ? 4 : 5;
|
||||
if (!forecast)
|
||||
return Array.from({
|
||||
length: count
|
||||
}, () => null);
|
||||
|
||||
return forecast.slice(0, count);
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
id: forecastHour
|
||||
|
||||
required property var modelData
|
||||
|
||||
Layout.fillWidth: true
|
||||
spacing: Appearance.spacing.small
|
||||
|
||||
CustomText {
|
||||
Layout.fillWidth: true
|
||||
color: DynamicColors.palette.m3outline
|
||||
font.pointSize: Appearance.font.size.larger
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
text: {
|
||||
const hour = forecastHour.modelData?.hour ?? 0;
|
||||
return hour > 12 ? `${(hour - 12).toString().padStart(2, "0")} PM` : `${hour.toString().padStart(2, "0")} AM`;
|
||||
}
|
||||
}
|
||||
|
||||
MaterialIcon {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
font.pointSize: Appearance.font.size.extraLarge * 1.5
|
||||
font.weight: 500
|
||||
text: forecastHour.modelData?.icon ?? "cloud_alert"
|
||||
}
|
||||
|
||||
CustomText {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
color: DynamicColors.palette.m3secondary
|
||||
font.pointSize: Appearance.font.size.larger
|
||||
text: Config.services.useFahrenheit ? `${forecastHour.modelData?.tempF ?? 0}°F` : `${forecastHour.modelData?.tempC ?? 0}°C`
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
color: DynamicColors.tPalette.m3surfaceContainer
|
||||
implicitHeight: {
|
||||
const base = brief.implicitHeight + brief.anchors.topMargin;
|
||||
if (showForecast)
|
||||
return base + Appearance.spacing.large + forecast.implicitHeight + forecast.anchors.margins;
|
||||
return base + brief.anchors.topMargin;
|
||||
}
|
||||
radius: Appearance.rounding.small
|
||||
|
||||
Timer {
|
||||
interval: 900000 // 15 minutes
|
||||
@@ -166,4 +27,27 @@ ColumnLayout {
|
||||
|
||||
onTriggered: Weather.reload()
|
||||
}
|
||||
|
||||
BriefInfo {
|
||||
id: brief
|
||||
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.top: parent.top
|
||||
anchors.topMargin: Appearance.padding.extraLarge
|
||||
rootHeight: root.rootHeight
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: forecast
|
||||
|
||||
active: root.showForecast
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.left: parent.left
|
||||
anchors.margins: Appearance.padding.large
|
||||
anchors.right: parent.right
|
||||
asynchronous: true
|
||||
|
||||
sourceComponent: Forecast {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ CustomRect {
|
||||
readonly property int textWidth: Math.min(metrics.width, 200)
|
||||
|
||||
color: DynamicColors.tPalette.m3surfaceContainer
|
||||
implicitHeight: Config.barConfig.height + Appearance.padding.smallest * 2
|
||||
implicitHeight: Config.bar.height + Appearance.padding.smallest * 2
|
||||
implicitWidth: layout.implicitWidth + Appearance.padding.normal * 2
|
||||
radius: Appearance.rounding.full
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user