mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-08-02 03:28:28 -04:00
Compare commits
3 Commits
c67b185076
...
qml-common
| Author | SHA1 | Date | |
|---|---|---|---|
| 990d86d481 | |||
| 526cb157fd | |||
| 92ba96d9f9 |
@@ -0,0 +1,56 @@
|
||||
name: Sync flake.lock to dank-qml-common
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
paths:
|
||||
- "dank-qml-common"
|
||||
branches:
|
||||
- master
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
sync:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Create GitHub App token
|
||||
id: app_token
|
||||
uses: actions/create-github-app-token@v1
|
||||
with:
|
||||
app-id: ${{ secrets.APP_ID }}
|
||||
private-key: ${{ secrets.APP_PRIVATE_KEY }}
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
token: ${{ steps.app_token.outputs.token }}
|
||||
|
||||
- name: Install Nix
|
||||
uses: cachix/install-nix-action@v31
|
||||
|
||||
- name: Point flake input at the submodule commit
|
||||
run: |
|
||||
set -euo pipefail
|
||||
submodule_rev=$(git ls-tree HEAD dank-qml-common --object-only)
|
||||
flake_rev=$(python3 -c "import json; print(json.load(open('flake.lock'))['nodes']['dank-qml-common']['locked']['rev'])")
|
||||
[ "$submodule_rev" = "$flake_rev" ] && { echo "flake.lock already matches $submodule_rev"; exit 0; }
|
||||
nix flake lock --override-input dank-qml-common "github:AvengeMedia/dank-qml-common/$submodule_rev"
|
||||
|
||||
- name: Commit and push flake.lock update
|
||||
env:
|
||||
GH_TOKEN: ${{ steps.app_token.outputs.token }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if git diff --quiet flake.lock; then
|
||||
echo "No changes to flake.lock"
|
||||
exit 0
|
||||
fi
|
||||
git config user.name "dms-ci[bot]"
|
||||
git config user.email "dms-ci[bot]@users.noreply.github.com"
|
||||
git add flake.lock
|
||||
git commit -m "nix: sync flake.lock to dank-qml-common submodule"
|
||||
git pull --rebase origin ${{ github.ref_name }}
|
||||
git push https://x-access-token:${GH_TOKEN}@github.com/${{ github.repository }}.git HEAD:${{ github.ref_name }}
|
||||
@@ -27,6 +27,8 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Install flatpak
|
||||
run: sudo apt update && sudo apt install -y flatpak
|
||||
|
||||
@@ -10,6 +10,8 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Install flatpak
|
||||
run: sudo apt update && sudo apt install -y flatpak
|
||||
|
||||
@@ -42,6 +42,7 @@ jobs:
|
||||
with:
|
||||
ref: ${{ inputs.tag }}
|
||||
fetch-depth: 0
|
||||
submodules: recursive
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v6
|
||||
@@ -191,11 +192,13 @@ jobs:
|
||||
with:
|
||||
ref: ${{ inputs.tag }}
|
||||
fetch-depth: 0
|
||||
submodules: recursive
|
||||
|
||||
- name: Fetch updated tag after version bump
|
||||
run: |
|
||||
git fetch origin --force tag ${TAG}
|
||||
git checkout ${TAG}
|
||||
git submodule update --init --recursive
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v6
|
||||
@@ -252,6 +255,7 @@ jobs:
|
||||
- **`dankinstall-arm64.gz`** - Installer binary for ARM64 systems
|
||||
- **`dms-cli-<version>.tar.gz`** - Go source code with vendored modules (for distro packaging)
|
||||
- **`dms-qml.tar.gz`** - QML source code only
|
||||
- **`dms-source.tar.gz`** - Full repository source with bundled DankCommon (for distro packaging)
|
||||
|
||||
### Checksums
|
||||
- **`*.sha256`** - SHA256 checksums for verifying download integrity
|
||||
@@ -299,6 +303,22 @@ jobs:
|
||||
# Copy completions
|
||||
cp _core_assets/completion.* _release_assets/ 2>/dev/null || true
|
||||
|
||||
# Replace the DankCommon symlink with real submodule content for packaging
|
||||
rm quickshell/DankCommon
|
||||
cp -r dank-qml-common/DankCommon quickshell/DankCommon
|
||||
|
||||
# Create full source tarball (GitHub tag archives never contain submodule content)
|
||||
VERSION_NUM=${TAG#v}
|
||||
tar --exclude='.git' \
|
||||
--exclude='.github' \
|
||||
--exclude='dank-qml-common' \
|
||||
--exclude='_release_assets' \
|
||||
--exclude='_core_assets' \
|
||||
--exclude='RELEASE_BODY.md' \
|
||||
--transform "s,^\.,DankMaterialShell-${VERSION_NUM},S" \
|
||||
-czf _release_assets/dms-source.tar.gz .
|
||||
(cd _release_assets && sha256sum dms-source.tar.gz > dms-source.tar.gz.sha256)
|
||||
|
||||
# Create QML source package (exclude build artifacts and git files)
|
||||
# Copy root LICENSE and CONTRIBUTING.md to quickshell/ for packaging
|
||||
cp LICENSE CONTRIBUTING.md quickshell/
|
||||
|
||||
@@ -36,6 +36,7 @@ jobs:
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
submodules: recursive
|
||||
|
||||
- name: Check for updates
|
||||
id: check
|
||||
@@ -199,6 +200,7 @@ jobs:
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
submodules: recursive
|
||||
|
||||
- name: Wait before OBS upload
|
||||
run: sleep 3
|
||||
|
||||
@@ -36,6 +36,7 @@ jobs:
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
submodules: recursive
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
@@ -94,6 +95,7 @@ jobs:
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
submodules: recursive
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v6
|
||||
|
||||
@@ -83,6 +83,7 @@ jobs:
|
||||
with:
|
||||
ref: ${{ github.event.release.tag_name || (github.event.inputs.version && (startsWith(github.event.inputs.version, 'v') && github.event.inputs.version || format('v{0}', github.event.inputs.version))) || github.ref }}
|
||||
fetch-depth: 0
|
||||
submodules: recursive
|
||||
|
||||
- name: Record checked-out source commit
|
||||
run: echo "SOURCE_COMMIT=$(git rev-parse HEAD)" >> "$GITHUB_ENV"
|
||||
@@ -225,7 +226,7 @@ jobs:
|
||||
if [ -n "$RELEASE_VER" ] && { [ "$BUILD_DMS" = "true" ] || [ "$BUILD_GREETER" = "true" ]; }; then
|
||||
echo "🔧 Updating stable templates for $ARCHIVE_TAG"
|
||||
TARBALL="$(mktemp)"
|
||||
curl -fsSL -o "$TARBALL" "https://github.com/${{ github.repository }}/archive/refs/tags/${ARCHIVE_TAG}.tar.gz"
|
||||
curl -fsSL -o "$TARBALL" "https://github.com/${{ github.repository }}/releases/download/${ARCHIVE_TAG}/dms-source.tar.gz"
|
||||
RELEASE_CHECKSUM="$(sha256sum "$TARBALL" | cut -d' ' -f1)"
|
||||
rm -f "$TARBALL"
|
||||
|
||||
@@ -246,7 +247,7 @@ jobs:
|
||||
# into $wrksrc (create_wrksrc=yes handles the rest).
|
||||
SRC_CACHE="hostdir/sources/dms-git-${GIT_VER}"
|
||||
mkdir -p "$SRC_CACHE"
|
||||
tar -czf "${SRC_CACHE}/dms-git-${GIT_VER}.tar.gz" \
|
||||
tar -czhf "${SRC_CACHE}/dms-git-${GIT_VER}.tar.gz" \
|
||||
--exclude=void-packages \
|
||||
--exclude=r2-repo \
|
||||
--exclude=.git \
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
[submodule "dank-qml-common"]
|
||||
path = dank-qml-common
|
||||
url = https://github.com/AvengeMedia/dank-qml-common.git
|
||||
@@ -6,6 +6,14 @@ To contribute fork this repository, make your changes, and open a pull request.
|
||||
|
||||
## Setup
|
||||
|
||||
Clone with submodules — the shared widget library ([dank-qml-common](https://github.com/AvengeMedia/dank-qml-common)) is vendored at `dank-qml-common/` and symlinked into `quickshell/DankCommon`:
|
||||
|
||||
```bash
|
||||
git clone --recurse-submodules https://github.com/AvengeMedia/DankMaterialShell.git
|
||||
# or, in an existing clone:
|
||||
git submodule update --init
|
||||
```
|
||||
|
||||
Install [prek](https://prek.j178.dev/) then activate pre-commit hooks:
|
||||
|
||||
```bash
|
||||
@@ -28,6 +36,24 @@ This will provide:
|
||||
|
||||
The dev shell automatically creates the `.qmlls.ini` file in the `quickshell/` directory.
|
||||
|
||||
## Shared widgets (dank-qml-common)
|
||||
|
||||
Everything under `quickshell/DankCommon/` (core widgets, the file browser, scroll physics, bundled fonts) is shared across the DMS suite and lives in the `dank-qml-common` submodule. It is a normal git worktree:
|
||||
|
||||
1. Edit files under `dank-qml-common/` (or through the `quickshell/DankCommon` symlink — same files) and test in the running shell; hot reload works as usual. For isolated widget work, the library is its own runnable config with a gallery: `qs -c dank-qml-common`.
|
||||
2. Commit and PR those changes in the `dank-qml-common` repo: `cd dank-qml-common && git switch -c my-change`, push, open the PR there.
|
||||
3. Once merged, bump the pointer here: `make update-common` (updates the submodule and the nix flake input together), then commit alongside any DMS-side changes. If you only bump the submodule, CI syncs `flake.lock` to it automatically on master.
|
||||
|
||||
The submodule URL in `.gitmodules` is HTTPS so CI and anonymous clones keep working. To push over SSH instead of being prompted for credentials, add a push rewrite to your git config — fetches stay HTTPS, pushes use SSH:
|
||||
|
||||
```bash
|
||||
git config --global url."git@github.com:AvengeMedia/".pushInsteadOf "https://github.com/AvengeMedia/"
|
||||
```
|
||||
|
||||
Shared widgets read app-provided singletons (`Theme`, `SettingsData`, ...) through a documented contract — see the dank-qml-common README. If your change needs a new contract property, add it to the library's stub singletons in the same PR, then to `quickshell/Common/` here when you bump.
|
||||
|
||||
Files in `quickshell/Widgets/`, `quickshell/Common/`, and `quickshell/Modals/FileBrowser/` that moved to the library remain in place as thin wrappers, so `import qs.Widgets`, `qs.Common`, and `qs.Modals.FileBrowser` keep working for the shell and for plugins.
|
||||
|
||||
## VSCode Setup
|
||||
|
||||
This is a monorepo, the easiest thing to do is to open an editor in either `quickshell`, `core`, or both depending on which part of the project you are working on.
|
||||
@@ -104,6 +130,8 @@ Text {
|
||||
|
||||
Preferably, try to keep new terms to a minimum and re-use existing terms where possible. See `quickshell/translations/en.json` for the list of existing terms. (This isn't always possible obviously, but instead of using `Auto-connect` you would use `Autoconnect` since it's already translated)
|
||||
|
||||
Strings inside `quickshell/DankCommon/` are owned by the dank-qml-common repo but stay in the DMS POEditor project — extraction here deliberately skips them, and `scripts/i18nsync.py sync` uploads the union of app terms and the submodule's terms instead (common terms carry the `dank-qml-common` tag). On download the sync splits the exports: app translations go to `quickshell/translations/poexports/`, common translations go to `dank-qml-common/DankCommon/translations/poexports/` for you to commit in that repo and bump. At runtime `I18n` merges both catalogs (app terms win). Other apps (dankcalendar) keep their own POEditor projects and merge the `dank-qml-common`-tagged terms from the DMS project.
|
||||
|
||||
### GO (`core` directory)
|
||||
|
||||
1. Install the [Go Extension](https://code.visualstudio.com/docs/languages/go)
|
||||
|
||||
@@ -35,6 +35,12 @@ clean:
|
||||
lint-qml:
|
||||
@./quickshell/scripts/qmllint-entrypoints.sh
|
||||
|
||||
# Pull the latest dank-qml-common and pin it everywhere it is consumed
|
||||
# (submodule pointer + nix flake input). Commit both in one change.
|
||||
update-common:
|
||||
git submodule update --remote --merge dank-qml-common
|
||||
nix --extra-experimental-features 'nix-command flakes' flake update dank-qml-common
|
||||
|
||||
# Installation targets
|
||||
install-bin:
|
||||
@echo "Installing $(BINARY_NAME) to $(INSTALL_DIR)..."
|
||||
@@ -43,8 +49,9 @@ install-bin:
|
||||
|
||||
install-shell:
|
||||
@echo "Installing shell files to $(SHELL_INSTALL_DIR)..."
|
||||
@test -e $(SHELL_DIR)/DankCommon/Widgets/DankIcon.qml || { echo "DankCommon missing: run git submodule update --init"; exit 1; }
|
||||
@mkdir -p $(SHELL_INSTALL_DIR)
|
||||
@cp -r $(SHELL_DIR)/* $(SHELL_INSTALL_DIR)/
|
||||
@cp -rL $(SHELL_DIR)/* $(SHELL_INSTALL_DIR)/
|
||||
@rm -rf $(SHELL_INSTALL_DIR)/.git* $(SHELL_INSTALL_DIR)/.github
|
||||
@echo "Shell files installed"
|
||||
|
||||
|
||||
@@ -250,6 +250,10 @@ func updateOtherDistros() error {
|
||||
return fmt.Errorf("failed to fetch changes: %w", err)
|
||||
}
|
||||
|
||||
if err := updateSubmodules(); err != nil {
|
||||
return fmt.Errorf("failed to update submodules: %w", err)
|
||||
}
|
||||
|
||||
if currentTag != "" {
|
||||
latestTagCmd := exec.Command("git", "tag", "-l", "v*", "--sort=-version:refname")
|
||||
latestTagOutput, err := latestTagCmd.Output()
|
||||
@@ -291,6 +295,10 @@ func updateOtherDistros() error {
|
||||
return fmt.Errorf("update cancelled")
|
||||
}
|
||||
|
||||
if err := updateSubmodules(); err != nil {
|
||||
return fmt.Errorf("failed to update submodules: %w", err)
|
||||
}
|
||||
|
||||
fmt.Printf("\nUpdate complete! Updated from %s to %s\n", currentTag, latestTag)
|
||||
return nil
|
||||
}
|
||||
@@ -320,10 +328,21 @@ func updateOtherDistros() error {
|
||||
return fmt.Errorf("update cancelled")
|
||||
}
|
||||
|
||||
if err := updateSubmodules(); err != nil {
|
||||
return fmt.Errorf("failed to update submodules: %w", err)
|
||||
}
|
||||
|
||||
fmt.Println("\nUpdate complete!")
|
||||
return nil
|
||||
}
|
||||
|
||||
func updateSubmodules() error {
|
||||
submoduleCmd := exec.Command("git", "submodule", "update", "--init", "--recursive")
|
||||
submoduleCmd.Stdout = os.Stdout
|
||||
submoduleCmd.Stderr = os.Stderr
|
||||
return submoduleCmd.Run()
|
||||
}
|
||||
|
||||
func offerReclone(dmsPath string) bool {
|
||||
fmt.Println("\nWould you like to backup and re-clone the repository? (y/N): ")
|
||||
reader := bufio.NewReader(os.Stdin)
|
||||
@@ -342,7 +361,7 @@ func offerReclone(dmsPath string) bool {
|
||||
}
|
||||
|
||||
fmt.Println("Cloning fresh copy...")
|
||||
cloneCmd := exec.Command("git", "clone", "https://github.com/AvengeMedia/DankMaterialShell.git", dmsPath)
|
||||
cloneCmd := exec.Command("git", "clone", "--recurse-submodules", "https://github.com/AvengeMedia/DankMaterialShell.git", dmsPath)
|
||||
cloneCmd.Stdout = os.Stdout
|
||||
cloneCmd.Stderr = os.Stderr
|
||||
if err := cloneCmd.Run(); err != nil {
|
||||
|
||||
@@ -520,7 +520,7 @@ func (m *ManualPackageInstaller) installDankMaterialShell(ctx context.Context, v
|
||||
Progress: 0.90,
|
||||
Step: "Cloning DankMaterialShell...",
|
||||
IsComplete: false,
|
||||
CommandInfo: "git clone https://github.com/AvengeMedia/DankMaterialShell.git",
|
||||
CommandInfo: "git clone --recurse-submodules https://github.com/AvengeMedia/DankMaterialShell.git",
|
||||
}
|
||||
|
||||
configDir := filepath.Dir(dmsPath)
|
||||
@@ -528,7 +528,7 @@ func (m *ManualPackageInstaller) installDankMaterialShell(ctx context.Context, v
|
||||
return fmt.Errorf("failed to create quickshell config directory: %w", err)
|
||||
}
|
||||
|
||||
cloneCmd := exec.CommandContext(ctx, "git", "clone",
|
||||
cloneCmd := exec.CommandContext(ctx, "git", "clone", "--recurse-submodules",
|
||||
"https://github.com/AvengeMedia/DankMaterialShell.git", dmsPath)
|
||||
if err := cloneCmd.Run(); err != nil {
|
||||
return fmt.Errorf("failed to clone DankMaterialShell: %w", err)
|
||||
@@ -553,6 +553,8 @@ func (m *ManualPackageInstaller) installDankMaterialShell(ctx context.Context, v
|
||||
return nil
|
||||
}
|
||||
|
||||
m.syncDMSSubmodules(ctx, dmsPath)
|
||||
|
||||
m.log(fmt.Sprintf("Checked out latest tag: %s", latestTag))
|
||||
m.log("DankMaterialShell cloned successfully")
|
||||
return nil
|
||||
@@ -591,6 +593,8 @@ func (m *ManualPackageInstaller) installDankMaterialShell(ctx context.Context, v
|
||||
return nil
|
||||
}
|
||||
|
||||
m.syncDMSSubmodules(ctx, dmsPath)
|
||||
|
||||
m.log("DankMaterialShell updated successfully (git variant)")
|
||||
return nil
|
||||
}
|
||||
@@ -609,10 +613,19 @@ func (m *ManualPackageInstaller) installDankMaterialShell(ctx context.Context, v
|
||||
return nil
|
||||
}
|
||||
|
||||
m.syncDMSSubmodules(ctx, dmsPath)
|
||||
|
||||
m.log(fmt.Sprintf("Updated to tag: %s", latestTag))
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *ManualPackageInstaller) syncDMSSubmodules(ctx context.Context, dmsPath string) {
|
||||
submoduleCmd := exec.CommandContext(ctx, "git", "-C", dmsPath, "submodule", "update", "--init", "--recursive")
|
||||
if err := submoduleCmd.Run(); err != nil {
|
||||
m.logError("Failed to update submodules", err)
|
||||
}
|
||||
}
|
||||
|
||||
func (m *ManualPackageInstaller) installXwaylandSatellite(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||
m.log("Installing xwayland-satellite from source...")
|
||||
|
||||
|
||||
Submodule
+1
Submodule dank-qml-common added at 5942bc1c60
@@ -70,7 +70,7 @@ override_dh_auto_install:
|
||||
|
||||
mkdir -p debian/dms-git/usr/share/quickshell/dms debian/dms-git/usr/lib/systemd/user
|
||||
if [ -d quickshell ]; then \
|
||||
cp -r quickshell/* debian/dms-git/usr/share/quickshell/dms/; \
|
||||
cp -rL quickshell/* debian/dms-git/usr/share/quickshell/dms/; \
|
||||
install -Dm644 assets/systemd/dms.service debian/dms-git/usr/lib/systemd/user/dms.service; \
|
||||
install -Dm644 assets/dms-open.desktop debian/dms-git/usr/share/applications/dms-open.desktop; \
|
||||
install -Dm644 assets/com.danklinux.dms.desktop debian/dms-git/usr/share/applications/com.danklinux.dms.desktop; \
|
||||
|
||||
@@ -20,7 +20,7 @@ override_dh_auto_install:
|
||||
fi; \
|
||||
if [ -n "$$SOURCE_DIR" ]; then \
|
||||
mkdir -p debian/dms-greeter/usr/share/quickshell/dms-greeter && \
|
||||
( cd $$SOURCE_DIR && tar cf - --exclude=debian . ) | \
|
||||
( cd $$SOURCE_DIR && tar chf - --exclude=debian . ) | \
|
||||
( cd debian/dms-greeter/usr/share/quickshell/dms-greeter && tar xf - ) && \
|
||||
install -Dm755 $$SOURCE_DIR/Modules/Greetd/assets/dms-greeter \
|
||||
debian/dms-greeter/usr/bin/dms-greeter && \
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<service name="download_url">
|
||||
<param name="protocol">https</param>
|
||||
<param name="host">github.com</param>
|
||||
<param name="path">/AvengeMedia/DankMaterialShell/archive/refs/tags/v1.2.3.tar.gz</param>
|
||||
<param name="path">/AvengeMedia/DankMaterialShell/releases/download/v1.2.3/dms-source.tar.gz</param>
|
||||
<param name="filename">dms-source.tar.gz</param>
|
||||
</service>
|
||||
<!-- Download amd64 binary -->
|
||||
|
||||
@@ -63,7 +63,7 @@ override_dh_auto_install:
|
||||
mv "$$SOURCE_DIR" DankMaterialShell-$(UPSTREAM_VERSION); \
|
||||
fi
|
||||
if [ -d DankMaterialShell-$(UPSTREAM_VERSION) ]; then \
|
||||
cp -r DankMaterialShell-$(UPSTREAM_VERSION)/quickshell/* debian/dms/usr/share/quickshell/dms/; \
|
||||
cp -rL DankMaterialShell-$(UPSTREAM_VERSION)/quickshell/* debian/dms/usr/share/quickshell/dms/; \
|
||||
install -Dm644 DankMaterialShell-$(UPSTREAM_VERSION)/assets/systemd/dms.service debian/dms/usr/lib/systemd/user/dms.service; \
|
||||
install -Dm644 DankMaterialShell-$(UPSTREAM_VERSION)/assets/dms-open.desktop debian/dms/usr/share/applications/dms-open.desktop; \
|
||||
install -Dm644 DankMaterialShell-$(UPSTREAM_VERSION)/assets/com.danklinux.dms.desktop debian/dms/usr/share/applications/com.danklinux.dms.desktop; \
|
||||
|
||||
@@ -216,7 +216,7 @@ update_debian_dms_service() {
|
||||
return 0
|
||||
fi
|
||||
|
||||
sed -i "s|/archive/refs/tags/v[0-9][^\"]*\.tar\.gz|/archive/refs/tags/v${base_version}.tar.gz|" "$service_path"
|
||||
sed -i "s|/releases/download/v[0-9][^\"]*/dms-source\.tar\.gz|/releases/download/v${base_version}/dms-source.tar.gz|" "$service_path"
|
||||
sed -i "s|/releases/download/v[0-9][^\"]*/dms-distropkg-amd64\.gz|/releases/download/v${base_version}/dms-distropkg-amd64.gz|" "$service_path"
|
||||
sed -i "s|/releases/download/v[0-9][^\"]*/dms-distropkg-arm64\.gz|/releases/download/v${base_version}/dms-distropkg-arm64.gz|" "$service_path"
|
||||
}
|
||||
@@ -542,8 +542,8 @@ if [[ "$UPLOAD_OPENSUSE" == true ]] && [[ "$UPLOAD_DEBIAN" == false ]] && [[ -f
|
||||
if [[ -n "$GIT_URL" ]]; then
|
||||
echo " Cloning git source from: $GIT_URL (revision: ${GIT_REVISION:-master})"
|
||||
SOURCE_DIR="$TEMP_DIR/dms-git-source"
|
||||
if git clone --depth 1 --branch "${GIT_REVISION:-master}" "$GIT_URL" "$SOURCE_DIR" 2>/dev/null ||
|
||||
git clone --depth 1 "$GIT_URL" "$SOURCE_DIR" 2>/dev/null; then
|
||||
if git clone --depth 1 --recurse-submodules --shallow-submodules --branch "${GIT_REVISION:-master}" "$GIT_URL" "$SOURCE_DIR" 2>/dev/null ||
|
||||
git clone --depth 1 --recurse-submodules --shallow-submodules "$GIT_URL" "$SOURCE_DIR" 2>/dev/null; then
|
||||
cd "$SOURCE_DIR"
|
||||
if [[ -n "$GIT_REVISION" ]]; then
|
||||
git checkout "$GIT_REVISION" 2>/dev/null || true
|
||||
@@ -586,7 +586,7 @@ if [[ "$UPLOAD_OPENSUSE" == true ]] && [[ "$UPLOAD_DEBIAN" == false ]] && [[ -f
|
||||
echo " Creating $SOURCE0 (directory: $EXPECTED_DIR)"
|
||||
mkdir -p "$EXPECTED_DIR"
|
||||
cp -a "$SOURCE_DIR"/. "$EXPECTED_DIR/"
|
||||
tar -czf "$WORK_DIR/$SOURCE0" "$EXPECTED_DIR"
|
||||
tar -czhf "$WORK_DIR/$SOURCE0" "$EXPECTED_DIR"
|
||||
rm -rf "$EXPECTED_DIR"
|
||||
echo " Created $SOURCE0 ($(stat -c%s "$WORK_DIR/$SOURCE0" 2>/dev/null || echo 0) bytes)"
|
||||
;;
|
||||
@@ -604,7 +604,7 @@ if [[ "$UPLOAD_OPENSUSE" == true ]] && [[ "$UPLOAD_DEBIAN" == false ]] && [[ -f
|
||||
|
||||
echo " Creating $SOURCE0 (directory: $EXPECTED_DIR)"
|
||||
cp -r "$SOURCE_DIR" "$EXPECTED_DIR"
|
||||
tar -czf "$WORK_DIR/$SOURCE0" "$EXPECTED_DIR"
|
||||
tar -czhf "$WORK_DIR/$SOURCE0" "$EXPECTED_DIR"
|
||||
rm -rf "$EXPECTED_DIR"
|
||||
echo " Created $SOURCE0 ($(stat -c%s "$WORK_DIR/$SOURCE0" 2>/dev/null || echo 0) bytes)"
|
||||
|
||||
@@ -656,8 +656,8 @@ if [[ "$UPLOAD_DEBIAN" == true ]] && [[ -d "distro/debian/$PACKAGE/debian" ]]; t
|
||||
if [[ -n "$GIT_URL" ]]; then
|
||||
echo " Cloning git source from: $GIT_URL (revision: ${GIT_REVISION:-master})"
|
||||
SOURCE_DIR="$TEMP_DIR/dms-git-source"
|
||||
if git clone --depth 1 --branch "${GIT_REVISION:-master}" "$GIT_URL" "$SOURCE_DIR" 2>/dev/null ||
|
||||
git clone --depth 1 "$GIT_URL" "$SOURCE_DIR" 2>/dev/null; then
|
||||
if git clone --depth 1 --recurse-submodules --shallow-submodules --branch "${GIT_REVISION:-master}" "$GIT_URL" "$SOURCE_DIR" 2>/dev/null ||
|
||||
git clone --depth 1 --recurse-submodules --shallow-submodules "$GIT_URL" "$SOURCE_DIR" 2>/dev/null; then
|
||||
cd "$SOURCE_DIR"
|
||||
if [[ -n "$GIT_REVISION" ]]; then
|
||||
git checkout "$GIT_REVISION" 2>/dev/null || true
|
||||
@@ -820,11 +820,11 @@ if [[ "$UPLOAD_DEBIAN" == true ]] && [[ -d "distro/debian/$PACKAGE/debian" ]]; t
|
||||
mkdir -p "$EXPECTED_DIR"
|
||||
cp -a "$SOURCE_DIR"/. "$EXPECTED_DIR/"
|
||||
if [[ "$SOURCE0" == *.tar.xz ]]; then
|
||||
tar --sort=name --mtime='2000-01-01 00:00:00' --owner=0 --group=0 -cJf "$WORK_DIR/$SOURCE0" "$EXPECTED_DIR"
|
||||
tar --sort=name --mtime='2000-01-01 00:00:00' --owner=0 --group=0 -cJhf "$WORK_DIR/$SOURCE0" "$EXPECTED_DIR"
|
||||
elif [[ "$SOURCE0" == *.tar.bz2 ]]; then
|
||||
tar --sort=name --mtime='2000-01-01 00:00:00' --owner=0 --group=0 -cjf "$WORK_DIR/$SOURCE0" "$EXPECTED_DIR"
|
||||
tar --sort=name --mtime='2000-01-01 00:00:00' --owner=0 --group=0 -cjhf "$WORK_DIR/$SOURCE0" "$EXPECTED_DIR"
|
||||
else
|
||||
tar --sort=name --mtime='2000-01-01 00:00:00' --owner=0 --group=0 -czf "$WORK_DIR/$SOURCE0" "$EXPECTED_DIR"
|
||||
tar --sort=name --mtime='2000-01-01 00:00:00' --owner=0 --group=0 -czhf "$WORK_DIR/$SOURCE0" "$EXPECTED_DIR"
|
||||
fi
|
||||
rm -rf "$EXPECTED_DIR"
|
||||
echo " Created $SOURCE0 ($(stat -c%s "$WORK_DIR/$SOURCE0" 2>/dev/null || echo 0) bytes)"
|
||||
@@ -835,11 +835,11 @@ if [[ "$UPLOAD_DEBIAN" == true ]] && [[ -d "distro/debian/$PACKAGE/debian" ]]; t
|
||||
echo " Creating $SOURCE0 (directory: $EXPECTED_DIR)"
|
||||
cp -r "$SOURCE_DIR" "$EXPECTED_DIR"
|
||||
if [[ "$SOURCE0" == *.tar.xz ]]; then
|
||||
tar --sort=name --mtime='2000-01-01 00:00:00' --owner=0 --group=0 -cJf "$WORK_DIR/$SOURCE0" "$EXPECTED_DIR"
|
||||
tar --sort=name --mtime='2000-01-01 00:00:00' --owner=0 --group=0 -cJhf "$WORK_DIR/$SOURCE0" "$EXPECTED_DIR"
|
||||
elif [[ "$SOURCE0" == *.tar.bz2 ]]; then
|
||||
tar --sort=name --mtime='2000-01-01 00:00:00' --owner=0 --group=0 -cjf "$WORK_DIR/$SOURCE0" "$EXPECTED_DIR"
|
||||
tar --sort=name --mtime='2000-01-01 00:00:00' --owner=0 --group=0 -cjhf "$WORK_DIR/$SOURCE0" "$EXPECTED_DIR"
|
||||
else
|
||||
tar --sort=name --mtime='2000-01-01 00:00:00' --owner=0 --group=0 -czf "$WORK_DIR/$SOURCE0" "$EXPECTED_DIR"
|
||||
tar --sort=name --mtime='2000-01-01 00:00:00' --owner=0 --group=0 -czhf "$WORK_DIR/$SOURCE0" "$EXPECTED_DIR"
|
||||
fi
|
||||
rm -rf "$EXPECTED_DIR"
|
||||
echo " Created $SOURCE0 ($(stat -c%s "$WORK_DIR/$SOURCE0" 2>/dev/null || echo 0) bytes)"
|
||||
@@ -849,11 +849,11 @@ if [[ "$UPLOAD_DEBIAN" == true ]] && [[ -d "distro/debian/$PACKAGE/debian" ]]; t
|
||||
echo " Creating $SOURCE0 (directory: $EXPECTED_DIR)"
|
||||
cp -r "$SOURCE_DIR" "$EXPECTED_DIR"
|
||||
if [[ "$SOURCE0" == *.tar.xz ]]; then
|
||||
tar --sort=name --mtime='2000-01-01 00:00:00' --owner=0 --group=0 -cJf "$WORK_DIR/$SOURCE0" "$EXPECTED_DIR"
|
||||
tar --sort=name --mtime='2000-01-01 00:00:00' --owner=0 --group=0 -cJhf "$WORK_DIR/$SOURCE0" "$EXPECTED_DIR"
|
||||
elif [[ "$SOURCE0" == *.tar.bz2 ]]; then
|
||||
tar --sort=name --mtime='2000-01-01 00:00:00' --owner=0 --group=0 -cjf "$WORK_DIR/$SOURCE0" "$EXPECTED_DIR"
|
||||
tar --sort=name --mtime='2000-01-01 00:00:00' --owner=0 --group=0 -cjhf "$WORK_DIR/$SOURCE0" "$EXPECTED_DIR"
|
||||
else
|
||||
tar --sort=name --mtime='2000-01-01 00:00:00' --owner=0 --group=0 -czf "$WORK_DIR/$SOURCE0" "$EXPECTED_DIR"
|
||||
tar --sort=name --mtime='2000-01-01 00:00:00' --owner=0 --group=0 -czhf "$WORK_DIR/$SOURCE0" "$EXPECTED_DIR"
|
||||
fi
|
||||
rm -rf "$EXPECTED_DIR"
|
||||
echo " Created $SOURCE0 ($(stat -c%s "$WORK_DIR/$SOURCE0" 2>/dev/null || echo 0) bytes)"
|
||||
@@ -863,11 +863,11 @@ if [[ "$UPLOAD_DEBIAN" == true ]] && [[ -d "distro/debian/$PACKAGE/debian" ]]; t
|
||||
echo " Creating $SOURCE0 (directory: $DIR_NAME)"
|
||||
cp -r "$SOURCE_DIR" "$DIR_NAME"
|
||||
if [[ "$SOURCE0" == *.tar.xz ]]; then
|
||||
tar --sort=name --mtime='2000-01-01 00:00:00' --owner=0 --group=0 -cJf "$WORK_DIR/$SOURCE0" "$DIR_NAME"
|
||||
tar --sort=name --mtime='2000-01-01 00:00:00' --owner=0 --group=0 -cJhf "$WORK_DIR/$SOURCE0" "$DIR_NAME"
|
||||
elif [[ "$SOURCE0" == *.tar.bz2 ]]; then
|
||||
tar --sort=name --mtime='2000-01-01 00:00:00' --owner=0 --group=0 -cjf "$WORK_DIR/$SOURCE0" "$DIR_NAME"
|
||||
tar --sort=name --mtime='2000-01-01 00:00:00' --owner=0 --group=0 -cjhf "$WORK_DIR/$SOURCE0" "$DIR_NAME"
|
||||
else
|
||||
tar --sort=name --mtime='2000-01-01 00:00:00' --owner=0 --group=0 -czf "$WORK_DIR/$SOURCE0" "$DIR_NAME"
|
||||
tar --sort=name --mtime='2000-01-01 00:00:00' --owner=0 --group=0 -czhf "$WORK_DIR/$SOURCE0" "$DIR_NAME"
|
||||
fi
|
||||
rm -rf "$DIR_NAME"
|
||||
echo " Created $SOURCE0 ($(stat -c%s "$WORK_DIR/$SOURCE0" 2>/dev/null || echo 0) bytes)"
|
||||
|
||||
@@ -344,7 +344,7 @@ EOF
|
||||
|
||||
if [ ! -f "dms-source.tar.gz" ]; then
|
||||
info "Downloading dms source for QML files..."
|
||||
if wget -O dms-source.tar.gz "https://github.com/AvengeMedia/DankMaterialShell/archive/refs/tags/v${VERSION}.tar.gz"; then
|
||||
if wget -O dms-source.tar.gz "https://github.com/AvengeMedia/DankMaterialShell/releases/download/v${VERSION}/dms-source.tar.gz"; then
|
||||
success "source tarball downloaded"
|
||||
else
|
||||
error "Failed to download dms-source.tar.gz"
|
||||
@@ -356,7 +356,7 @@ EOF
|
||||
info "Downloading source for dms-greeter..."
|
||||
if [ ! -f "dms-greeter-source.tar.gz" ]; then
|
||||
info "Downloading dms-greeter source..."
|
||||
if wget -O dms-greeter-source.tar.gz "https://github.com/AvengeMedia/DankMaterialShell/archive/refs/tags/v${VERSION}.tar.gz"; then
|
||||
if wget -O dms-greeter-source.tar.gz "https://github.com/AvengeMedia/DankMaterialShell/releases/download/v${VERSION}/dms-source.tar.gz"; then
|
||||
success "source tarball downloaded"
|
||||
else
|
||||
error "Failed to download dms-greeter-source.tar.gz"
|
||||
@@ -386,7 +386,7 @@ if [ "$IS_GIT_PACKAGE" = true ] && [ -n "$GIT_REPO" ]; then
|
||||
|
||||
info "Cloning $GIT_REPO from GitHub (getting latest commit info)..."
|
||||
TEMP_CLONE=$(mktemp -d "$TEMP_BASE/ppa_clone_XXXXXX")
|
||||
if git clone "https://github.com/$GIT_REPO.git" "$TEMP_CLONE"; then
|
||||
if git clone --recurse-submodules --shallow-submodules "https://github.com/$GIT_REPO.git" "$TEMP_CLONE"; then
|
||||
GIT_COMMIT_HASH=$(cd "$TEMP_CLONE" && git rev-parse --short HEAD)
|
||||
GIT_COMMIT_COUNT=$(cd "$TEMP_CLONE" && git rev-list --count HEAD)
|
||||
UPSTREAM_VERSION=$(cd "$TEMP_CLONE" && git tag -l "v*" | sed 's/^v//' | sort -V | tail -1)
|
||||
|
||||
@@ -60,7 +60,7 @@ override_dh_auto_install:
|
||||
|
||||
# Install QML files from git clone
|
||||
mkdir -p debian/dms-git/usr/share/quickshell/dms
|
||||
cp -r dms-git-repo/* debian/dms-git/usr/share/quickshell/dms/
|
||||
cp -rL dms-git-repo/* debian/dms-git/usr/share/quickshell/dms/
|
||||
|
||||
# Remove unnecessary directories
|
||||
rm -rf debian/dms-git/usr/share/quickshell/dms/core
|
||||
|
||||
@@ -26,7 +26,7 @@ override_dh_auto_build:
|
||||
override_dh_auto_install:
|
||||
# Install greeter files to shared data location
|
||||
mkdir -p debian/dms-greeter/usr/share/quickshell/dms-greeter
|
||||
cp -r DankMaterialShell-$(BASE_VERSION)/quickshell/* debian/dms-greeter/usr/share/quickshell/dms-greeter/
|
||||
cp -rL DankMaterialShell-$(BASE_VERSION)/quickshell/* debian/dms-greeter/usr/share/quickshell/dms-greeter/
|
||||
|
||||
# Install launcher script
|
||||
install -Dm755 DankMaterialShell-$(BASE_VERSION)/quickshell/Modules/Greetd/assets/dms-greeter \
|
||||
|
||||
@@ -63,7 +63,7 @@ override_dh_auto_install:
|
||||
|
||||
# Install QML files from source tarball
|
||||
mkdir -p debian/dms/usr/share/quickshell/dms
|
||||
cp -r DankMaterialShell-$(BASE_VERSION)/* debian/dms/usr/share/quickshell/dms/
|
||||
cp -rL DankMaterialShell-$(BASE_VERSION)/* debian/dms/usr/share/quickshell/dms/
|
||||
|
||||
# Remove unnecessary directories
|
||||
rm -rf debian/dms/usr/share/quickshell/dms/core
|
||||
|
||||
@@ -11,7 +11,7 @@ short_desc="DankMaterialShell greeter for greetd"
|
||||
maintainer="AvengeMedia <AvengeMedia.US@gmail.com>"
|
||||
license="MIT"
|
||||
homepage="https://danklinux.com"
|
||||
distfiles="https://github.com/AvengeMedia/DankMaterialShell/archive/refs/tags/v${version}.tar.gz"
|
||||
distfiles="https://github.com/AvengeMedia/DankMaterialShell/releases/download/v${version}/dms-source.tar.gz"
|
||||
checksum=14f2678d6a15223ba069d1b8c8a21a5564b735d190c231f62ed44fd8bf48677c
|
||||
|
||||
depends="greetd quickshell acl-progs dbus elogind pam_rundir mesa-dri"
|
||||
|
||||
@@ -19,7 +19,7 @@ maintainer="AvengeMedia <AvengeMedia.US@gmail.com>"
|
||||
license="MIT"
|
||||
homepage="https://danklinux.com"
|
||||
changelog="https://github.com/AvengeMedia/DankMaterialShell/releases"
|
||||
distfiles="https://github.com/AvengeMedia/DankMaterialShell/archive/refs/tags/v${version}.tar.gz"
|
||||
distfiles="https://github.com/AvengeMedia/DankMaterialShell/releases/download/v${version}/dms-source.tar.gz"
|
||||
checksum=14f2678d6a15223ba069d1b8c8a21a5564b735d190c231f62ed44fd8bf48677c
|
||||
|
||||
# Optional feature deps are listed in distro/void/README.md.
|
||||
|
||||
Generated
+17
@@ -1,5 +1,21 @@
|
||||
{
|
||||
"nodes": {
|
||||
"dank-qml-common": {
|
||||
"flake": false,
|
||||
"locked": {
|
||||
"lastModified": 1784332064,
|
||||
"narHash": "sha256-n0LKLirPnNSXYxElK1IpnybEwU0nnyso4kpsEFX88U0=",
|
||||
"owner": "AvengeMedia",
|
||||
"repo": "dank-qml-common",
|
||||
"rev": "5942bc1c60b4538940922b58112c8ae125880891",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "AvengeMedia",
|
||||
"repo": "dank-qml-common",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"flake-compat": {
|
||||
"flake": false,
|
||||
"locked": {
|
||||
@@ -34,6 +50,7 @@
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"dank-qml-common": "dank-qml-common",
|
||||
"flake-compat": "flake-compat",
|
||||
"nixpkgs": "nixpkgs"
|
||||
}
|
||||
|
||||
@@ -7,12 +7,17 @@
|
||||
url = "github:NixOS/flake-compat";
|
||||
flake = false;
|
||||
};
|
||||
dank-qml-common = {
|
||||
url = "github:AvengeMedia/dank-qml-common";
|
||||
flake = false;
|
||||
};
|
||||
};
|
||||
|
||||
outputs =
|
||||
{
|
||||
self,
|
||||
nixpkgs,
|
||||
dank-qml-common,
|
||||
...
|
||||
}:
|
||||
let
|
||||
@@ -125,6 +130,10 @@
|
||||
mkdir -p $out/share/quickshell/dms
|
||||
cp -r ${rootSrc}/quickshell/. $out/share/quickshell/dms/
|
||||
|
||||
rm -f $out/share/quickshell/dms/DankCommon
|
||||
cp -r ${dank-qml-common}/DankCommon $out/share/quickshell/dms/DankCommon
|
||||
chmod -R u+w $out/share/quickshell/dms/DankCommon
|
||||
|
||||
chmod u+w $out/share/quickshell/dms/VERSION
|
||||
echo "${version}" > $out/share/quickshell/dms/VERSION
|
||||
|
||||
|
||||
@@ -141,13 +141,15 @@ DankMaterialShell/
|
||||
│ │ ├── ipp/ # Internet Printing Protocol
|
||||
│ │ └── syncmap/ # Thread-safe map
|
||||
│ └── go.mod # Go module definition
|
||||
├── dank-qml-common/ # Shared widget library (git submodule)
|
||||
├── quickshell/ # QML frontend (UI layer) - see "QML Frontend Architecture" below
|
||||
│ ├── shell.qml # Main entry point
|
||||
│ ├── Services/ # IPC client wrappers
|
||||
│ ├── Modules/ # UI components
|
||||
│ ├── Widgets/ # Reusable controls
|
||||
│ ├── Modals/ # Full-screen overlays
|
||||
│ └── Common/ # Shared resources
|
||||
│ ├── Common/ # Shared resources
|
||||
│ └── DankCommon/ # Symlink to ../dank-qml-common/DankCommon
|
||||
├── distro/ # Distribution packaging
|
||||
│ ├── arch/ # AUR packages
|
||||
│ ├── fedora/ # RPM specs
|
||||
@@ -490,6 +492,8 @@ import qs.Services // For service access
|
||||
import qs.Widgets // For reusable widgets (DankIcon, etc.)
|
||||
```
|
||||
|
||||
Many widgets in `Widgets/`, `Common/`, and `Modals/FileBrowser/` are thin wrappers over the shared dank-qml-common library (`quickshell/DankCommon/`, a submodule symlink). Keep importing them through `qs.Widgets`, `qs.Common`, and `qs.Modals.FileBrowser` — the wrappers are the stable API for the shell and for plugins. Edit the implementations in `dank-qml-common/` (see CONTRIBUTING.md, "Shared widgets").
|
||||
|
||||
#### Go Import Order
|
||||
|
||||
Follow standard Go conventions:
|
||||
|
||||
@@ -1,66 +1,11 @@
|
||||
pragma Singleton
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import qs.DankCommon.Common as DankCommon
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
readonly property Rounding rounding: Rounding {}
|
||||
readonly property Spacing spacing: Spacing {}
|
||||
readonly property FontSize fontSize: FontSize {}
|
||||
readonly property Anim anim: Anim {}
|
||||
|
||||
component Rounding: QtObject {
|
||||
readonly property int small: 8
|
||||
readonly property int normal: 12
|
||||
readonly property int large: 16
|
||||
readonly property int extraLarge: 24
|
||||
readonly property int full: 1000
|
||||
}
|
||||
|
||||
component Spacing: QtObject {
|
||||
readonly property int small: 4
|
||||
readonly property int normal: 8
|
||||
readonly property int large: 12
|
||||
readonly property int extraLarge: 16
|
||||
readonly property int huge: 24
|
||||
}
|
||||
|
||||
component FontSize: QtObject {
|
||||
readonly property int small: 12
|
||||
readonly property int normal: 14
|
||||
readonly property int large: 16
|
||||
readonly property int extraLarge: 20
|
||||
readonly property int huge: 24
|
||||
}
|
||||
|
||||
component AnimCurves: QtObject {
|
||||
readonly property list<real> standard: [0.2, 0, 0, 1, 1, 1]
|
||||
readonly property list<real> standardAccel: [0.3, 0, 1, 1, 1, 1]
|
||||
readonly property list<real> standardDecel: [0, 0, 0, 1, 1, 1]
|
||||
readonly property list<real> emphasized: [0.05, 0, 2 / 15, 0.06, 1
|
||||
/ 6, 0.4, 5 / 24, 0.82, 0.25, 1, 1, 1]
|
||||
readonly property list<real> emphasizedAccel: [0.3, 0, 0.8, 0.15, 1, 1]
|
||||
readonly property list<real> emphasizedDecel: [0.05, 0.7, 0.1, 1, 1, 1]
|
||||
readonly property list<real> expressiveFastSpatial: [0.42, 1.67, 0.21, 0.9, 1, 1]
|
||||
readonly property list<real> expressiveDefaultSpatial: [0.38, 1.21, 0.22, 1, 1, 1]
|
||||
readonly property list<real> expressiveEffects: [0.34, 0.8, 0.34, 1, 1, 1]
|
||||
}
|
||||
|
||||
component AnimDurations: QtObject {
|
||||
readonly property int quick: 150
|
||||
readonly property int normal: 300
|
||||
readonly property int slow: 500
|
||||
readonly property int extraSlow: 1000
|
||||
readonly property int expressiveFastSpatial: 350
|
||||
readonly property int expressiveDefaultSpatial: 500
|
||||
readonly property int expressiveEffects: 200
|
||||
}
|
||||
|
||||
component Anim: QtObject {
|
||||
readonly property AnimCurves curves: AnimCurves {}
|
||||
readonly property AnimDurations durations: AnimDurations {}
|
||||
}
|
||||
readonly property var rounding: DankCommon.Appearance.rounding
|
||||
readonly property var spacing: DankCommon.Appearance.spacing
|
||||
readonly property var fontSize: DankCommon.Appearance.fontSize
|
||||
readonly property var anim: DankCommon.Appearance.anim
|
||||
}
|
||||
|
||||
@@ -1,9 +1,3 @@
|
||||
import QtQuick
|
||||
import qs.Common
|
||||
import qs.DankCommon.Common as DankCommon
|
||||
|
||||
// Reusable NumberAnimation wrapper
|
||||
NumberAnimation {
|
||||
duration: Theme.expressiveDurations.normal
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: Theme.expressiveCurves.standard
|
||||
}
|
||||
DankCommon.DankAnim {}
|
||||
|
||||
@@ -1,9 +1,3 @@
|
||||
import QtQuick
|
||||
import qs.Common
|
||||
import qs.DankCommon.Common as DankCommon
|
||||
|
||||
// Reusable ColorAnimation wrapper
|
||||
ColorAnimation {
|
||||
duration: Theme.expressiveDurations.normal
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: Theme.expressiveCurves.standard
|
||||
}
|
||||
DankCommon.DankColorAnim {}
|
||||
|
||||
@@ -1,64 +1,3 @@
|
||||
import QtQuick
|
||||
import Quickshell.Io
|
||||
import qs.DankCommon.Common as DankCommon
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property alias path: socket.path
|
||||
property alias parser: socket.parser
|
||||
property bool connected: false
|
||||
property bool linkUp: false
|
||||
|
||||
property int reconnectBaseMs: 400
|
||||
property int reconnectMaxMs: 15000
|
||||
|
||||
property int _reconnectAttempt: 0
|
||||
|
||||
signal connectionStateChanged
|
||||
|
||||
onConnectedChanged: {
|
||||
socket.connected = connected;
|
||||
}
|
||||
|
||||
Socket {
|
||||
id: socket
|
||||
|
||||
onConnectionStateChanged: {
|
||||
root.linkUp = connected;
|
||||
root.connectionStateChanged();
|
||||
if (connected) {
|
||||
root._reconnectAttempt = 0;
|
||||
return;
|
||||
}
|
||||
if (root.connected) {
|
||||
root._scheduleReconnect();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: reconnectTimer
|
||||
interval: 0
|
||||
repeat: false
|
||||
onTriggered: {
|
||||
socket.connected = false;
|
||||
Qt.callLater(() => socket.connected = true);
|
||||
}
|
||||
}
|
||||
|
||||
function send(data) {
|
||||
const json = typeof data === "string" ? data : JSON.stringify(data);
|
||||
const message = json.endsWith("\n") ? json : json + "\n";
|
||||
socket.write(message);
|
||||
socket.flush();
|
||||
}
|
||||
|
||||
function _scheduleReconnect() {
|
||||
const pow = Math.min(_reconnectAttempt, 10);
|
||||
const base = Math.min(reconnectBaseMs * Math.pow(2, pow), reconnectMaxMs);
|
||||
const jitter = Math.floor(Math.random() * Math.floor(base / 4));
|
||||
reconnectTimer.interval = base + jitter;
|
||||
reconnectTimer.restart();
|
||||
_reconnectAttempt++;
|
||||
}
|
||||
}
|
||||
DankCommon.DankSocket {}
|
||||
|
||||
@@ -1,54 +1,3 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
import qs.DankCommon.Common as DankCommon
|
||||
|
||||
import QtQuick
|
||||
import qs.Common
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property var level: Theme.elevationLevel2
|
||||
property string direction: Theme.elevationLightDirection
|
||||
property real fallbackOffset: 4
|
||||
|
||||
property color targetColor: "white"
|
||||
property real targetRadius: Theme.cornerRadius
|
||||
property real topLeftRadius: targetRadius
|
||||
property real topRightRadius: targetRadius
|
||||
property real bottomLeftRadius: targetRadius
|
||||
property real bottomRightRadius: targetRadius
|
||||
property color borderColor: "transparent"
|
||||
property real borderWidth: 0
|
||||
|
||||
property real sourceX: 0
|
||||
property real sourceY: 0
|
||||
property real sourceWidth: width
|
||||
property real sourceHeight: height
|
||||
|
||||
property bool shadowEnabled: Theme.elevationEnabled
|
||||
property real shadowBlurPx: level && level.blurPx !== undefined ? level.blurPx : 0
|
||||
property real shadowSpreadPx: level && level.spreadPx !== undefined ? level.spreadPx : 0
|
||||
property real shadowOffsetX: Theme.elevationOffsetXFor(level, direction, fallbackOffset)
|
||||
property real shadowOffsetY: Theme.elevationOffsetYFor(level, direction, fallbackOffset)
|
||||
property color shadowColor: Theme.elevationShadowColor(level)
|
||||
property real shadowOpacity: 1
|
||||
|
||||
readonly property var _ambient: Theme.elevationAmbient(level)
|
||||
readonly property real _pad: shadowEnabled ? Math.ceil(Math.max(shadowBlurPx + shadowSpreadPx + Math.max(Math.abs(shadowOffsetX), Math.abs(shadowOffsetY)), _ambient.blurPx + _ambient.spreadPx) + 2) : 0
|
||||
|
||||
ShaderEffect {
|
||||
anchors.fill: parent
|
||||
anchors.margins: -root._pad
|
||||
fragmentShader: Qt.resolvedUrl("../Shaders/qsb/elevation_rect.frag.qsb")
|
||||
|
||||
property real widthPx: width
|
||||
property real heightPx: height
|
||||
property real borderWidth: root.borderWidth
|
||||
property vector4d rectPx: Qt.vector4d(root._pad + root.sourceX, root._pad + root.sourceY, root.sourceWidth, root.sourceHeight)
|
||||
property vector4d cornerRadius: Qt.vector4d(root.topLeftRadius, root.topRightRadius, root.bottomRightRadius, root.bottomLeftRadius)
|
||||
property vector4d fillColor: Qt.vector4d(root.targetColor.r, root.targetColor.g, root.targetColor.b, root.targetColor.a)
|
||||
property vector4d borderColor: Qt.vector4d(root.borderColor.r, root.borderColor.g, root.borderColor.b, root.borderColor.a)
|
||||
property vector4d shadowColor: Qt.vector4d(root.shadowColor.r, root.shadowColor.g, root.shadowColor.b, root.shadowEnabled ? root.shadowColor.a * root.shadowOpacity : 0)
|
||||
property vector4d shadowParam: Qt.vector4d(Math.max(0, root.shadowBlurPx), root.shadowSpreadPx, root.shadowOffsetX, root.shadowOffsetY)
|
||||
property vector4d ambientParam: Qt.vector4d(root._ambient.blurPx, root._ambient.spreadPx, root.shadowEnabled ? root._ambient.alpha * root.shadowOpacity : 0, 0)
|
||||
}
|
||||
}
|
||||
DankCommon.ElevationShadow {}
|
||||
|
||||
+73
-12
@@ -25,6 +25,7 @@ Singleton {
|
||||
readonly property bool isRtl: _rtlLanguages.includes(_lang)
|
||||
|
||||
readonly property url translationsFolder: Qt.resolvedUrl("../translations/poexports")
|
||||
readonly property url commonTranslationsFolder: Qt.resolvedUrl("../DankCommon/translations/poexports")
|
||||
|
||||
readonly property alias folder: dir.folder
|
||||
property var presentLocales: ({
|
||||
@@ -32,8 +33,11 @@ Singleton {
|
||||
})
|
||||
property var translations: ({})
|
||||
property bool translationsLoaded: false
|
||||
property var commonTranslations: ({})
|
||||
property bool commonTranslationsLoaded: false
|
||||
|
||||
property url _selectedPath: ""
|
||||
property url _commonSelectedPath: ""
|
||||
|
||||
FolderListModel {
|
||||
id: dir
|
||||
@@ -48,6 +52,18 @@ Singleton {
|
||||
}
|
||||
}
|
||||
|
||||
FolderListModel {
|
||||
id: commonDir
|
||||
folder: root.commonTranslationsFolder
|
||||
nameFilters: ["*.json"]
|
||||
showDirs: false
|
||||
showDotAndDotDot: false
|
||||
|
||||
onStatusChanged: if (status === FolderListModel.Ready) {
|
||||
root._pickCommonTranslation();
|
||||
}
|
||||
}
|
||||
|
||||
FileView {
|
||||
id: translationLoader
|
||||
path: root._selectedPath
|
||||
@@ -69,6 +85,22 @@ Singleton {
|
||||
}
|
||||
}
|
||||
|
||||
FileView {
|
||||
id: commonTranslationLoader
|
||||
path: root._commonSelectedPath
|
||||
printErrors: false
|
||||
|
||||
onLoaded: {
|
||||
try {
|
||||
root.commonTranslations = JSON.parse(text());
|
||||
root.commonTranslationsLoaded = true;
|
||||
log.info(`I18n: Loaded DankCommon translations (${Object.keys(root.commonTranslations).length} contexts)`);
|
||||
} catch (e) {
|
||||
log.warn("I18n: Error parsing DankCommon translations:", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function locale() {
|
||||
if (SessionData.timeLocale)
|
||||
return Qt.locale(SessionData.timeLocale);
|
||||
@@ -117,29 +149,58 @@ Singleton {
|
||||
log.warn("Falling back to built-in English strings");
|
||||
}
|
||||
|
||||
function _pickCommonTranslation() {
|
||||
const present = {};
|
||||
for (let i = 0; i < commonDir.count; i++) {
|
||||
const name = commonDir.get(i, "fileName");
|
||||
if (name && name.endsWith(".json"))
|
||||
present[name.slice(0, -5)] = true;
|
||||
}
|
||||
for (let i = 0; i < _candidates.length; i++) {
|
||||
if (!present[_candidates[i]])
|
||||
continue;
|
||||
_commonSelectedPath = commonTranslationsFolder + "/" + _candidates[i] + ".json";
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
function _lookup(table, term, context) {
|
||||
if (!table)
|
||||
return "";
|
||||
if (context && table[context] && table[context][term])
|
||||
return table[context][term];
|
||||
if (table[term] && table[term][term])
|
||||
return table[term][term];
|
||||
for (const c in table) {
|
||||
if (table[c] && table[c][term])
|
||||
return table[c][term];
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
// isRealContext is consumed by translations/extract_translations.py only:
|
||||
// pass a literal `true` (same line) to give (term, context) its own POEditor
|
||||
// translation slot. Lookup ignores it -- a real context exists as a bucket
|
||||
// in the export, a comment-only context does not.
|
||||
function tr(term, context, isRealContext) {
|
||||
if (!translationsLoaded || !translations)
|
||||
return term;
|
||||
if (context && translations[context] && translations[context][term])
|
||||
return translations[context][term];
|
||||
if (translations[term] && translations[term][term])
|
||||
return translations[term][term];
|
||||
for (const c in translations) {
|
||||
if (translations[c] && translations[c][term])
|
||||
return translations[c][term];
|
||||
if (translationsLoaded) {
|
||||
const hit = _lookup(translations, term, context);
|
||||
if (hit)
|
||||
return hit;
|
||||
}
|
||||
if (commonTranslationsLoaded) {
|
||||
const hit = _lookup(commonTranslations, term, context);
|
||||
if (hit)
|
||||
return hit;
|
||||
}
|
||||
return term;
|
||||
}
|
||||
|
||||
function trContext(context, term) {
|
||||
if (!translationsLoaded || !translations)
|
||||
return term;
|
||||
if (translations[context] && translations[context][term])
|
||||
if (translationsLoaded && translations[context] && translations[context][term])
|
||||
return translations[context][term];
|
||||
if (commonTranslationsLoaded && commonTranslations[context] && commonTranslations[context][term])
|
||||
return commonTranslations[context][term];
|
||||
return term;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,72 +1,13 @@
|
||||
pragma Singleton
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import qs.Common
|
||||
import qs.DankCommon.Common as DankCommon
|
||||
|
||||
// Reusable ListView/GridView transitions
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
// 0ms ViewTransitions break ListView delegate cleanup, so null the set when the shortest
|
||||
// duration truncates to 0. Keep this gate - don't inline these back into add/remove/etc.
|
||||
readonly property bool enabled: Math.floor(Theme.currentAnimationBaseDuration * 0.4) >= 1
|
||||
|
||||
readonly property Transition add: enabled ? _add : null
|
||||
readonly property Transition remove: null
|
||||
readonly property Transition displaced: enabled ? _displaced : null
|
||||
readonly property Transition move: enabled ? _move : null
|
||||
|
||||
readonly property int _staggerMs: Math.round(Theme.currentAnimationBaseDuration * 0.03)
|
||||
readonly property int _staggerCap: 8
|
||||
|
||||
readonly property Transition _add: Transition {
|
||||
id: addTransition
|
||||
|
||||
SequentialAnimation {
|
||||
PropertyAction {
|
||||
property: "opacity"
|
||||
value: 0
|
||||
}
|
||||
PauseAnimation {
|
||||
duration: Math.max(0, Math.min(addTransition.ViewTransition.index - (addTransition.ViewTransition.targetIndexes[0] ?? 0), root._staggerCap)) * root._staggerMs
|
||||
}
|
||||
DankAnim {
|
||||
property: "opacity"
|
||||
from: 0
|
||||
to: 1
|
||||
duration: Theme.expressiveDurations.fast
|
||||
easing.bezierCurve: Theme.expressiveCurves.emphasizedDecel
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
readonly property Transition _displaced: Transition {
|
||||
DankAnim {
|
||||
property: "y"
|
||||
duration: Theme.expressiveDurations.fast
|
||||
easing.bezierCurve: Theme.expressiveCurves.standard
|
||||
}
|
||||
DankAnim {
|
||||
property: "opacity"
|
||||
to: 1
|
||||
duration: Theme.expressiveDurations.fast
|
||||
easing.bezierCurve: Theme.expressiveCurves.standard
|
||||
}
|
||||
}
|
||||
|
||||
readonly property Transition _move: Transition {
|
||||
DankAnim {
|
||||
property: "y"
|
||||
duration: Theme.expressiveDurations.fast
|
||||
easing.bezierCurve: Theme.expressiveCurves.standard
|
||||
}
|
||||
DankAnim {
|
||||
property: "opacity"
|
||||
to: 1
|
||||
duration: Theme.expressiveDurations.fast
|
||||
easing.bezierCurve: Theme.expressiveCurves.standard
|
||||
}
|
||||
}
|
||||
readonly property bool enabled: DankCommon.ListViewTransitions.enabled
|
||||
readonly property Transition add: DankCommon.ListViewTransitions.add
|
||||
readonly property Transition remove: DankCommon.ListViewTransitions.remove
|
||||
readonly property Transition displaced: DankCommon.ListViewTransitions.displaced
|
||||
readonly property Transition move: DankCommon.ListViewTransitions.move
|
||||
}
|
||||
|
||||
@@ -108,6 +108,14 @@ Singleton {
|
||||
return themedIconPath(iconName) || DesktopService.resolveIconPath(iconName);
|
||||
}
|
||||
|
||||
function trashPath(path: string, callback): void {
|
||||
TrashService.trashPath(path, callback);
|
||||
}
|
||||
|
||||
function copyPathToClipboard(path: string): void {
|
||||
Quickshell.execDetached([Proc.dmsBin, "cl", "copy", path]);
|
||||
}
|
||||
|
||||
function resolveIconUrl(iconName: string): string {
|
||||
if (!iconName)
|
||||
return "";
|
||||
|
||||
+4
-148
@@ -1,157 +1,13 @@
|
||||
pragma Singleton
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import qs.Services
|
||||
import qs.DankCommon.Common as DankCommon
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
readonly property var log: Log.scoped("Proc")
|
||||
|
||||
readonly property int noTimeout: -1
|
||||
readonly property string dmsBin: Quickshell.env("DMS_EXECUTABLE") || "dms"
|
||||
property int defaultDebounceMs: 50
|
||||
property int defaultTimeoutMs: 10000
|
||||
property var _procDebouncers: ({})
|
||||
readonly property int noTimeout: DankCommon.Proc.noTimeout
|
||||
readonly property string dmsBin: DankCommon.Proc.dmsBin
|
||||
|
||||
function runCommand(id, command, callback, debounceMs, timeoutMs) {
|
||||
const wait = (typeof debounceMs === "number" && debounceMs >= 0) ? debounceMs : defaultDebounceMs;
|
||||
const timeout = (typeof timeoutMs === "number") ? timeoutMs : defaultTimeoutMs;
|
||||
let procId = id ? id : Math.random();
|
||||
const isRandomId = !id;
|
||||
|
||||
if (!_procDebouncers[procId]) {
|
||||
const t = debounceTimerComp.createObject(root);
|
||||
t.triggered.connect(function () {
|
||||
_launchProc(procId, isRandomId);
|
||||
});
|
||||
_procDebouncers[procId] = {
|
||||
timer: t,
|
||||
command: command,
|
||||
callback: callback,
|
||||
waitMs: wait,
|
||||
timeoutMs: timeout,
|
||||
isRandomId: isRandomId
|
||||
};
|
||||
} else {
|
||||
_procDebouncers[procId].command = command;
|
||||
_procDebouncers[procId].callback = callback;
|
||||
_procDebouncers[procId].waitMs = wait;
|
||||
_procDebouncers[procId].timeoutMs = timeout;
|
||||
}
|
||||
|
||||
const entry = _procDebouncers[procId];
|
||||
entry.timer.interval = entry.waitMs;
|
||||
entry.timer.restart();
|
||||
}
|
||||
|
||||
function _launchProc(id, isRandomId) {
|
||||
const entry = _procDebouncers[id];
|
||||
if (!entry)
|
||||
return;
|
||||
const proc = procComp.createObject(root, {
|
||||
command: entry.command
|
||||
});
|
||||
const timeoutTimer = debounceTimerComp.createObject(root);
|
||||
|
||||
let capturedOut = "";
|
||||
let capturedErr = "";
|
||||
let exitSeen = false;
|
||||
let exitCodeValue = -1;
|
||||
let outSeen = false;
|
||||
let errSeen = false;
|
||||
let timedOut = false;
|
||||
|
||||
timeoutTimer.interval = entry.timeoutMs;
|
||||
timeoutTimer.triggered.connect(function () {
|
||||
if (!exitSeen) {
|
||||
timedOut = true;
|
||||
proc.running = false;
|
||||
exitSeen = true;
|
||||
exitCodeValue = 124;
|
||||
maybeComplete();
|
||||
}
|
||||
});
|
||||
|
||||
proc.stdout.streamFinished.connect(function () {
|
||||
try {
|
||||
capturedOut = proc.stdout.text || "";
|
||||
} catch (e) {
|
||||
capturedOut = "";
|
||||
}
|
||||
outSeen = true;
|
||||
maybeComplete();
|
||||
});
|
||||
|
||||
proc.stderr.streamFinished.connect(function () {
|
||||
try {
|
||||
capturedErr = proc.stderr.text || "";
|
||||
} catch (e) {
|
||||
capturedErr = "";
|
||||
}
|
||||
errSeen = true;
|
||||
maybeComplete();
|
||||
});
|
||||
|
||||
proc.exited.connect(function (code) {
|
||||
timeoutTimer.stop();
|
||||
exitSeen = true;
|
||||
exitCodeValue = code;
|
||||
maybeComplete();
|
||||
});
|
||||
|
||||
function maybeComplete() {
|
||||
if (!exitSeen || !outSeen || !errSeen)
|
||||
return;
|
||||
timeoutTimer.stop();
|
||||
if (entry && entry.callback && typeof entry.callback === "function") {
|
||||
try {
|
||||
const safeOutput = capturedOut !== null && capturedOut !== undefined ? capturedOut : "";
|
||||
const safeExitCode = exitCodeValue !== null && exitCodeValue !== undefined ? exitCodeValue : -1;
|
||||
entry.callback(safeOutput, safeExitCode);
|
||||
} catch (e) {
|
||||
log.warn("runCommand callback error for command:", entry.command, "Error:", e);
|
||||
}
|
||||
}
|
||||
try {
|
||||
proc.destroy();
|
||||
} catch (_) {}
|
||||
try {
|
||||
timeoutTimer.destroy();
|
||||
} catch (_) {}
|
||||
|
||||
if (isRandomId || entry.isRandomId) {
|
||||
Qt.callLater(function () {
|
||||
if (_procDebouncers[id]) {
|
||||
try {
|
||||
_procDebouncers[id].timer.destroy();
|
||||
} catch (_) {}
|
||||
delete _procDebouncers[id];
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
proc.running = true;
|
||||
if (entry.timeoutMs !== noTimeout)
|
||||
timeoutTimer.start();
|
||||
}
|
||||
|
||||
Component {
|
||||
id: debounceTimerComp
|
||||
Timer {
|
||||
repeat: false
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: procComp
|
||||
Process {
|
||||
running: false
|
||||
stdout: StdioCollector {}
|
||||
stderr: StdioCollector {}
|
||||
}
|
||||
DankCommon.Proc.runCommand(id, command, callback, debounceMs, timeoutMs);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import qs.Common
|
||||
import qs.DankCommon.Common as DankCommon
|
||||
import qs.Services
|
||||
import qs.Modules.Greetd
|
||||
import "StockThemes.js" as StockThemes
|
||||
@@ -1230,16 +1231,28 @@ Singleton {
|
||||
|
||||
property string fontFamily: {
|
||||
if (typeof SessionData !== "undefined" && SessionData.isGreeterMode && typeof GreetdSettings !== "undefined") {
|
||||
return GreetdSettings.getEffectiveFontFamily();
|
||||
return resolvedFontFamily(GreetdSettings.getEffectiveFontFamily());
|
||||
}
|
||||
return typeof SettingsData !== "undefined" ? SettingsData.fontFamily : "Inter Variable";
|
||||
return typeof SettingsData !== "undefined" ? resolvedFontFamily(SettingsData.fontFamily) : DankCommon.Fonts.sans;
|
||||
}
|
||||
|
||||
property string monoFontFamily: {
|
||||
if (typeof SessionData !== "undefined" && SessionData.isGreeterMode && typeof GreetdSettings !== "undefined") {
|
||||
return GreetdSettings.monoFontFamily;
|
||||
return resolvedMonoFontFamily(GreetdSettings.monoFontFamily);
|
||||
}
|
||||
return typeof SettingsData !== "undefined" ? SettingsData.monoFontFamily : "Fira Code";
|
||||
return typeof SettingsData !== "undefined" ? resolvedMonoFontFamily(SettingsData.monoFontFamily) : DankCommon.Fonts.mono;
|
||||
}
|
||||
|
||||
function resolvedFontFamily(family) {
|
||||
if (family === defaultFontFamily)
|
||||
return DankCommon.Fonts.sans;
|
||||
return family;
|
||||
}
|
||||
|
||||
function resolvedMonoFontFamily(family) {
|
||||
if (family === defaultMonoFontFamily)
|
||||
return DankCommon.Fonts.mono;
|
||||
return family;
|
||||
}
|
||||
|
||||
property int fontWeight: {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Symlink
+1
@@ -0,0 +1 @@
|
||||
../dank-qml-common/DankCommon
|
||||
@@ -1,935 +1,3 @@
|
||||
import Qt.labs.folderlistmodel
|
||||
import QtCore
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import qs.Common
|
||||
import qs.Widgets
|
||||
import qs.DankCommon.Modals.FileBrowser as DankCommon
|
||||
|
||||
FocusScope {
|
||||
id: root
|
||||
|
||||
LayoutMirroring.enabled: I18n.isRtl
|
||||
LayoutMirroring.childrenInherit: true
|
||||
|
||||
property string homeDir: StandardPaths.writableLocation(StandardPaths.HomeLocation)
|
||||
property string docsDir: StandardPaths.writableLocation(StandardPaths.DocumentsLocation)
|
||||
property string musicDir: StandardPaths.writableLocation(StandardPaths.MusicLocation)
|
||||
property string videosDir: StandardPaths.writableLocation(StandardPaths.MoviesLocation)
|
||||
property string picsDir: StandardPaths.writableLocation(StandardPaths.PicturesLocation)
|
||||
property string downloadDir: StandardPaths.writableLocation(StandardPaths.DownloadLocation)
|
||||
property string desktopDir: StandardPaths.writableLocation(StandardPaths.DesktopLocation)
|
||||
property string currentPath: ""
|
||||
property var fileExtensions: ["*.*"]
|
||||
property alias filterExtensions: root.fileExtensions
|
||||
property string browserTitle: "Select File"
|
||||
property string browserIcon: "folder_open"
|
||||
property string browserType: "generic"
|
||||
property bool showHiddenFiles: false
|
||||
property int selectedIndex: -1
|
||||
property bool keyboardNavigationActive: false
|
||||
property bool backButtonFocused: false
|
||||
property bool saveMode: false
|
||||
property string defaultFileName: ""
|
||||
property int keyboardSelectionIndex: -1
|
||||
property bool keyboardSelectionRequested: false
|
||||
property bool showKeyboardHints: false
|
||||
property bool showFileInfo: false
|
||||
property string selectedFilePath: ""
|
||||
property string selectedFileName: ""
|
||||
property bool selectedFileIsDir: false
|
||||
property bool showOverwriteConfirmation: false
|
||||
property string pendingFilePath: ""
|
||||
property bool showSidebar: true
|
||||
property string viewMode: "grid"
|
||||
property string sortBy: "name"
|
||||
property bool sortAscending: true
|
||||
property int iconSizeIndex: 1
|
||||
property var iconSizes: [80, 120, 160, 200]
|
||||
property bool pathEditMode: false
|
||||
property bool pathInputHasFocus: false
|
||||
property int actualGridColumns: 5
|
||||
property bool _initialized: false
|
||||
property bool closeOnEscape: true
|
||||
property var windowControls: null
|
||||
|
||||
signal fileSelected(string path)
|
||||
signal closeRequested
|
||||
|
||||
function encodeFileUrl(path) {
|
||||
if (!path)
|
||||
return "";
|
||||
return "file://" + path.split('/').map(s => encodeURIComponent(s)).join('/');
|
||||
}
|
||||
|
||||
function initialize() {
|
||||
loadSettings();
|
||||
currentPath = getLastPath();
|
||||
_initialized = true;
|
||||
}
|
||||
|
||||
function reset() {
|
||||
currentPath = getLastPath();
|
||||
selectedIndex = -1;
|
||||
keyboardNavigationActive = false;
|
||||
backButtonFocused = false;
|
||||
}
|
||||
|
||||
function loadSettings() {
|
||||
const type = browserType || "default";
|
||||
const settings = CacheData.fileBrowserSettings[type];
|
||||
const isImageBrowser = ["wallpaper", "profile"].includes(browserType);
|
||||
|
||||
if (settings) {
|
||||
viewMode = settings.viewMode || (isImageBrowser ? "grid" : "list");
|
||||
sortBy = settings.sortBy || "name";
|
||||
sortAscending = settings.sortAscending !== undefined ? settings.sortAscending : true;
|
||||
iconSizeIndex = settings.iconSizeIndex !== undefined ? settings.iconSizeIndex : 1;
|
||||
showSidebar = settings.showSidebar !== undefined ? settings.showSidebar : true;
|
||||
} else {
|
||||
viewMode = isImageBrowser ? "grid" : "list";
|
||||
}
|
||||
}
|
||||
|
||||
function saveSettings() {
|
||||
if (!_initialized)
|
||||
return;
|
||||
const type = browserType || "default";
|
||||
let settings = CacheData.fileBrowserSettings;
|
||||
if (!settings[type]) {
|
||||
settings[type] = {};
|
||||
}
|
||||
settings[type].viewMode = viewMode;
|
||||
settings[type].sortBy = sortBy;
|
||||
settings[type].sortAscending = sortAscending;
|
||||
settings[type].iconSizeIndex = iconSizeIndex;
|
||||
settings[type].showSidebar = showSidebar;
|
||||
settings[type].lastPath = currentPath;
|
||||
CacheData.fileBrowserSettings = settings;
|
||||
|
||||
if (browserType === "wallpaper") {
|
||||
CacheData.wallpaperLastPath = currentPath;
|
||||
} else if (browserType === "profile") {
|
||||
CacheData.profileLastPath = currentPath;
|
||||
}
|
||||
|
||||
CacheData.saveCache();
|
||||
}
|
||||
|
||||
onViewModeChanged: saveSettings()
|
||||
onSortByChanged: saveSettings()
|
||||
onSortAscendingChanged: saveSettings()
|
||||
onIconSizeIndexChanged: saveSettings()
|
||||
onShowSidebarChanged: saveSettings()
|
||||
|
||||
function isImageFile(fileName) {
|
||||
if (!fileName)
|
||||
return false;
|
||||
const ext = fileName.toLowerCase().split('.').pop();
|
||||
return ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp', 'svg'].includes(ext);
|
||||
}
|
||||
|
||||
function getLastPath() {
|
||||
const type = browserType || "default";
|
||||
const settings = CacheData.fileBrowserSettings[type];
|
||||
const lastPath = settings?.lastPath || "";
|
||||
return (lastPath && lastPath !== "") ? lastPath : homeDir;
|
||||
}
|
||||
|
||||
function saveLastPath(path) {
|
||||
const type = browserType || "default";
|
||||
let settings = CacheData.fileBrowserSettings;
|
||||
if (!settings[type]) {
|
||||
settings[type] = {};
|
||||
}
|
||||
settings[type].lastPath = path;
|
||||
CacheData.fileBrowserSettings = settings;
|
||||
CacheData.saveCache();
|
||||
|
||||
if (browserType === "wallpaper") {
|
||||
CacheData.wallpaperLastPath = path;
|
||||
} else if (browserType === "profile") {
|
||||
CacheData.profileLastPath = path;
|
||||
}
|
||||
}
|
||||
|
||||
function setSelectedFileData(path, name, isDir) {
|
||||
selectedFilePath = path;
|
||||
selectedFileName = name;
|
||||
selectedFileIsDir = isDir;
|
||||
}
|
||||
|
||||
function openItemContextMenu(sender, localX, localY, path, name, isDir) {
|
||||
if (!sender)
|
||||
return;
|
||||
const pos = sender.mapToItem(root, localX, localY);
|
||||
itemContextMenu.showAt(root, pos.x, pos.y, path, name, isDir);
|
||||
}
|
||||
|
||||
function navigateUp() {
|
||||
const path = currentPath;
|
||||
if (path === homeDir)
|
||||
return;
|
||||
const lastSlash = path.lastIndexOf('/');
|
||||
if (lastSlash <= 0)
|
||||
return;
|
||||
const newPath = path.substring(0, lastSlash);
|
||||
if (newPath.length < homeDir.length) {
|
||||
currentPath = homeDir;
|
||||
saveLastPath(homeDir);
|
||||
} else {
|
||||
currentPath = newPath;
|
||||
saveLastPath(newPath);
|
||||
}
|
||||
}
|
||||
|
||||
function navigateTo(path) {
|
||||
currentPath = path;
|
||||
saveLastPath(path);
|
||||
selectedIndex = -1;
|
||||
backButtonFocused = false;
|
||||
}
|
||||
|
||||
function keyboardFileSelection(index) {
|
||||
if (index < 0)
|
||||
return;
|
||||
keyboardSelectionTimer.targetIndex = index;
|
||||
keyboardSelectionTimer.start();
|
||||
}
|
||||
|
||||
function executeKeyboardSelection(index) {
|
||||
keyboardSelectionIndex = index;
|
||||
keyboardSelectionRequested = true;
|
||||
}
|
||||
|
||||
function activateFile(path, name, isDir) {
|
||||
if (isDir) {
|
||||
navigateTo(path);
|
||||
return;
|
||||
}
|
||||
if (saveMode) {
|
||||
saveRow.fileName = name;
|
||||
pendingFilePath = path;
|
||||
showOverwriteConfirmation = true;
|
||||
} else {
|
||||
fileSelected(path);
|
||||
closeRequested();
|
||||
}
|
||||
}
|
||||
|
||||
function handleSaveFile(filePath) {
|
||||
var normalizedPath = filePath;
|
||||
if (!normalizedPath.startsWith("file://")) {
|
||||
normalizedPath = encodeFileUrl(filePath);
|
||||
}
|
||||
|
||||
var exists = false;
|
||||
var fileName = filePath.split('/').pop();
|
||||
|
||||
for (var i = 0; i < folderModel.count; i++) {
|
||||
if (folderModel.get(i, "fileName") === fileName && !folderModel.get(i, "fileIsDir")) {
|
||||
exists = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (exists) {
|
||||
pendingFilePath = normalizedPath;
|
||||
showOverwriteConfirmation = true;
|
||||
} else {
|
||||
fileSelected(normalizedPath);
|
||||
closeRequested();
|
||||
}
|
||||
}
|
||||
|
||||
onCurrentPathChanged: {
|
||||
selectedFilePath = "";
|
||||
selectedFileName = "";
|
||||
selectedFileIsDir = false;
|
||||
saveSettings();
|
||||
}
|
||||
|
||||
onSelectedIndexChanged: {
|
||||
if (selectedIndex >= 0 && folderModel && selectedIndex < folderModel.count) {
|
||||
selectedFilePath = "";
|
||||
selectedFileName = "";
|
||||
selectedFileIsDir = false;
|
||||
}
|
||||
}
|
||||
|
||||
property var steamPaths: [StandardPaths.writableLocation(StandardPaths.HomeLocation) + "/.steam/steam/steamapps/workshop/content/431960", StandardPaths.writableLocation(StandardPaths.HomeLocation) + "/.local/share/Steam/steamapps/workshop/content/431960", StandardPaths.writableLocation(StandardPaths.HomeLocation) + "/.var/app/com.valvesoftware.Steam/.local/share/Steam/steamapps/workshop/content/431960", StandardPaths.writableLocation(StandardPaths.HomeLocation) + "/snap/steam/common/.local/share/Steam/steamapps/workshop/content/431960"]
|
||||
|
||||
property var quickAccessLocations: [
|
||||
{
|
||||
"name": I18n.tr("Home"),
|
||||
"path": homeDir,
|
||||
"icon": "home"
|
||||
},
|
||||
{
|
||||
"name": I18n.tr("Documents"),
|
||||
"path": docsDir,
|
||||
"icon": "description"
|
||||
},
|
||||
{
|
||||
"name": I18n.tr("Downloads"),
|
||||
"path": downloadDir,
|
||||
"icon": "download"
|
||||
},
|
||||
{
|
||||
"name": I18n.tr("Pictures"),
|
||||
"path": picsDir,
|
||||
"icon": "image"
|
||||
},
|
||||
{
|
||||
"name": I18n.tr("Music"),
|
||||
"path": musicDir,
|
||||
"icon": "music_note"
|
||||
},
|
||||
{
|
||||
"name": I18n.tr("Videos"),
|
||||
"path": videosDir,
|
||||
"icon": "movie"
|
||||
},
|
||||
{
|
||||
"name": I18n.tr("Desktop"),
|
||||
"path": desktopDir,
|
||||
"icon": "computer"
|
||||
}
|
||||
]
|
||||
|
||||
FolderListModel {
|
||||
id: folderModel
|
||||
|
||||
showDirsFirst: true
|
||||
showDotAndDotDot: false
|
||||
showHidden: root.showHiddenFiles
|
||||
caseSensitive: false
|
||||
nameFilters: fileExtensions
|
||||
showFiles: true
|
||||
showDirs: true
|
||||
folder: encodeFileUrl(currentPath || homeDir)
|
||||
sortField: {
|
||||
switch (sortBy) {
|
||||
case "name":
|
||||
return FolderListModel.Name;
|
||||
case "size":
|
||||
return FolderListModel.Size;
|
||||
case "modified":
|
||||
return FolderListModel.Time;
|
||||
case "type":
|
||||
return FolderListModel.Type;
|
||||
default:
|
||||
return FolderListModel.Name;
|
||||
}
|
||||
}
|
||||
sortReversed: !sortAscending
|
||||
}
|
||||
|
||||
QtObject {
|
||||
id: keyboardController
|
||||
|
||||
property int totalItems: folderModel.count
|
||||
property int gridColumns: viewMode === "list" ? 1 : Math.max(1, actualGridColumns)
|
||||
|
||||
function handleKey(event) {
|
||||
if (event.key === Qt.Key_Escape && root.closeOnEscape) {
|
||||
closeRequested();
|
||||
event.accepted = true;
|
||||
return;
|
||||
}
|
||||
if (event.key === Qt.Key_F10) {
|
||||
showKeyboardHints = !showKeyboardHints;
|
||||
event.accepted = true;
|
||||
return;
|
||||
}
|
||||
if (event.key === Qt.Key_F1 || event.key === Qt.Key_I) {
|
||||
showFileInfo = !showFileInfo;
|
||||
event.accepted = true;
|
||||
return;
|
||||
}
|
||||
if ((event.modifiers & Qt.AltModifier && event.key === Qt.Key_Left) || event.key === Qt.Key_Backspace) {
|
||||
if (currentPath !== homeDir) {
|
||||
navigateUp();
|
||||
event.accepted = true;
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (!keyboardNavigationActive) {
|
||||
const isInitKey = event.key === Qt.Key_Tab || event.key === Qt.Key_Down || event.key === Qt.Key_Right || (event.key === Qt.Key_N && event.modifiers & Qt.ControlModifier) || (event.key === Qt.Key_J && event.modifiers & Qt.ControlModifier) || (event.key === Qt.Key_L && event.modifiers & Qt.ControlModifier);
|
||||
|
||||
if (isInitKey) {
|
||||
keyboardNavigationActive = true;
|
||||
if (currentPath !== homeDir) {
|
||||
backButtonFocused = true;
|
||||
selectedIndex = -1;
|
||||
} else {
|
||||
backButtonFocused = false;
|
||||
selectedIndex = 0;
|
||||
}
|
||||
event.accepted = true;
|
||||
}
|
||||
return;
|
||||
}
|
||||
switch (event.key) {
|
||||
case Qt.Key_Tab:
|
||||
if (backButtonFocused) {
|
||||
backButtonFocused = false;
|
||||
selectedIndex = 0;
|
||||
} else if (selectedIndex < totalItems - 1) {
|
||||
selectedIndex++;
|
||||
} else if (currentPath !== homeDir) {
|
||||
backButtonFocused = true;
|
||||
selectedIndex = -1;
|
||||
} else {
|
||||
selectedIndex = 0;
|
||||
}
|
||||
event.accepted = true;
|
||||
break;
|
||||
case Qt.Key_Backtab:
|
||||
if (backButtonFocused) {
|
||||
backButtonFocused = false;
|
||||
selectedIndex = totalItems - 1;
|
||||
} else if (selectedIndex > 0) {
|
||||
selectedIndex--;
|
||||
} else if (currentPath !== homeDir) {
|
||||
backButtonFocused = true;
|
||||
selectedIndex = -1;
|
||||
} else {
|
||||
selectedIndex = totalItems - 1;
|
||||
}
|
||||
event.accepted = true;
|
||||
break;
|
||||
case Qt.Key_N:
|
||||
if (event.modifiers & Qt.ControlModifier) {
|
||||
if (backButtonFocused) {
|
||||
backButtonFocused = false;
|
||||
selectedIndex = 0;
|
||||
} else if (selectedIndex < totalItems - 1) {
|
||||
selectedIndex++;
|
||||
}
|
||||
event.accepted = true;
|
||||
}
|
||||
break;
|
||||
case Qt.Key_P:
|
||||
if (event.modifiers & Qt.ControlModifier) {
|
||||
if (selectedIndex > 0) {
|
||||
selectedIndex--;
|
||||
} else if (currentPath !== homeDir) {
|
||||
backButtonFocused = true;
|
||||
selectedIndex = -1;
|
||||
}
|
||||
event.accepted = true;
|
||||
}
|
||||
break;
|
||||
case Qt.Key_J:
|
||||
if (event.modifiers & Qt.ControlModifier) {
|
||||
if (selectedIndex < totalItems - 1) {
|
||||
selectedIndex++;
|
||||
}
|
||||
event.accepted = true;
|
||||
}
|
||||
break;
|
||||
case Qt.Key_K:
|
||||
if (event.modifiers & Qt.ControlModifier) {
|
||||
if (selectedIndex > 0) {
|
||||
selectedIndex--;
|
||||
} else if (currentPath !== homeDir) {
|
||||
backButtonFocused = true;
|
||||
selectedIndex = -1;
|
||||
}
|
||||
event.accepted = true;
|
||||
}
|
||||
break;
|
||||
case Qt.Key_H:
|
||||
if (event.modifiers & Qt.ControlModifier) {
|
||||
if (!backButtonFocused && selectedIndex > 0) {
|
||||
selectedIndex--;
|
||||
} else if (currentPath !== homeDir) {
|
||||
backButtonFocused = true;
|
||||
selectedIndex = -1;
|
||||
}
|
||||
event.accepted = true;
|
||||
}
|
||||
break;
|
||||
case Qt.Key_L:
|
||||
if (event.modifiers & Qt.ControlModifier) {
|
||||
if (backButtonFocused) {
|
||||
backButtonFocused = false;
|
||||
selectedIndex = 0;
|
||||
} else if (selectedIndex < totalItems - 1) {
|
||||
selectedIndex++;
|
||||
}
|
||||
event.accepted = true;
|
||||
}
|
||||
break;
|
||||
case Qt.Key_Left:
|
||||
if (pathInputHasFocus)
|
||||
return;
|
||||
if (backButtonFocused)
|
||||
return;
|
||||
if (selectedIndex > 0) {
|
||||
selectedIndex--;
|
||||
} else if (currentPath !== homeDir) {
|
||||
backButtonFocused = true;
|
||||
selectedIndex = -1;
|
||||
}
|
||||
event.accepted = true;
|
||||
break;
|
||||
case Qt.Key_Right:
|
||||
if (pathInputHasFocus)
|
||||
return;
|
||||
if (backButtonFocused) {
|
||||
backButtonFocused = false;
|
||||
selectedIndex = 0;
|
||||
} else if (selectedIndex < totalItems - 1) {
|
||||
selectedIndex++;
|
||||
}
|
||||
event.accepted = true;
|
||||
break;
|
||||
case Qt.Key_Up:
|
||||
if (backButtonFocused) {
|
||||
backButtonFocused = false;
|
||||
if (gridColumns === 1) {
|
||||
selectedIndex = 0;
|
||||
} else {
|
||||
var col = selectedIndex % gridColumns;
|
||||
selectedIndex = Math.min(col, totalItems - 1);
|
||||
}
|
||||
} else if (selectedIndex >= gridColumns) {
|
||||
selectedIndex -= gridColumns;
|
||||
} else if (selectedIndex > 0 && gridColumns === 1) {
|
||||
selectedIndex--;
|
||||
} else if (currentPath !== homeDir) {
|
||||
backButtonFocused = true;
|
||||
selectedIndex = -1;
|
||||
}
|
||||
event.accepted = true;
|
||||
break;
|
||||
case Qt.Key_Down:
|
||||
if (backButtonFocused) {
|
||||
backButtonFocused = false;
|
||||
selectedIndex = 0;
|
||||
} else if (gridColumns === 1) {
|
||||
if (selectedIndex < totalItems - 1) {
|
||||
selectedIndex++;
|
||||
}
|
||||
} else {
|
||||
var newIndex = selectedIndex + gridColumns;
|
||||
if (newIndex < totalItems) {
|
||||
selectedIndex = newIndex;
|
||||
} else {
|
||||
var lastRowStart = Math.floor((totalItems - 1) / gridColumns) * gridColumns;
|
||||
var col = selectedIndex % gridColumns;
|
||||
var targetIndex = lastRowStart + col;
|
||||
if (targetIndex < totalItems && targetIndex > selectedIndex) {
|
||||
selectedIndex = targetIndex;
|
||||
}
|
||||
}
|
||||
}
|
||||
event.accepted = true;
|
||||
break;
|
||||
case Qt.Key_Return:
|
||||
case Qt.Key_Enter:
|
||||
case Qt.Key_Space:
|
||||
if (backButtonFocused) {
|
||||
navigateUp();
|
||||
} else if (selectedIndex >= 0 && selectedIndex < totalItems) {
|
||||
root.keyboardFileSelection(selectedIndex);
|
||||
}
|
||||
event.accepted = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: keyboardSelectionTimer
|
||||
|
||||
property int targetIndex: -1
|
||||
|
||||
interval: 1
|
||||
onTriggered: {
|
||||
executeKeyboardSelection(targetIndex);
|
||||
}
|
||||
}
|
||||
|
||||
focus: true
|
||||
|
||||
Keys.onPressed: event => {
|
||||
keyboardController.handleKey(event);
|
||||
}
|
||||
|
||||
Column {
|
||||
anchors.fill: parent
|
||||
spacing: 0
|
||||
|
||||
Item {
|
||||
width: parent.width
|
||||
height: 48
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
onPressed: if (windowControls)
|
||||
windowControls.tryStartMove()
|
||||
onDoubleClicked: if (windowControls)
|
||||
windowControls.tryToggleMaximize()
|
||||
}
|
||||
|
||||
Row {
|
||||
spacing: Theme.spacingM
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Theme.spacingL
|
||||
|
||||
DankIcon {
|
||||
name: browserIcon
|
||||
size: Theme.iconSizeLarge
|
||||
color: Theme.primary
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: browserTitle
|
||||
font.pixelSize: Theme.fontSizeXLarge
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: Theme.spacingM
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Theme.spacingS
|
||||
|
||||
DankActionButton {
|
||||
circular: false
|
||||
iconName: showHiddenFiles ? "visibility_off" : "visibility"
|
||||
iconSize: Theme.iconSize - 4
|
||||
iconColor: showHiddenFiles ? Theme.primary : Theme.surfaceText
|
||||
onClicked: showHiddenFiles = !showHiddenFiles
|
||||
}
|
||||
|
||||
DankActionButton {
|
||||
circular: false
|
||||
iconName: viewMode === "grid" ? "view_list" : "grid_view"
|
||||
iconSize: Theme.iconSize - 4
|
||||
iconColor: Theme.surfaceText
|
||||
onClicked: viewMode = viewMode === "grid" ? "list" : "grid"
|
||||
}
|
||||
|
||||
DankActionButton {
|
||||
circular: false
|
||||
iconName: iconSizeIndex === 0 ? "photo_size_select_small" : iconSizeIndex === 1 ? "photo_size_select_large" : iconSizeIndex === 2 ? "photo_size_select_actual" : "zoom_in"
|
||||
iconSize: Theme.iconSize - 4
|
||||
iconColor: Theme.surfaceText
|
||||
visible: viewMode === "grid"
|
||||
onClicked: iconSizeIndex = (iconSizeIndex + 1) % iconSizes.length
|
||||
}
|
||||
|
||||
DankActionButton {
|
||||
circular: false
|
||||
iconName: "info"
|
||||
iconSize: Theme.iconSize - 4
|
||||
iconColor: Theme.surfaceText
|
||||
onClicked: root.showKeyboardHints = !root.showKeyboardHints
|
||||
}
|
||||
|
||||
DankActionButton {
|
||||
visible: windowControls?.supported ?? false
|
||||
circular: false
|
||||
iconName: windowControls?.targetWindow?.maximized ? "fullscreen_exit" : "fullscreen"
|
||||
iconSize: Theme.iconSize - 4
|
||||
iconColor: Theme.surfaceText
|
||||
onClicked: if (windowControls)
|
||||
windowControls.tryToggleMaximize()
|
||||
}
|
||||
|
||||
DankActionButton {
|
||||
circular: false
|
||||
iconName: "close"
|
||||
iconSize: Theme.iconSize - 4
|
||||
iconColor: Theme.surfaceText
|
||||
onClicked: root.closeRequested()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StyledRect {
|
||||
width: parent.width
|
||||
height: 1
|
||||
color: Theme.outline
|
||||
}
|
||||
|
||||
Item {
|
||||
width: parent.width
|
||||
height: parent.height - 49
|
||||
|
||||
Row {
|
||||
anchors.fill: parent
|
||||
anchors.bottomMargin: root.saveMode ? 40 + Theme.spacingL * 2 : 0
|
||||
spacing: 0
|
||||
|
||||
Row {
|
||||
width: showSidebar ? 201 : 0
|
||||
height: parent.height
|
||||
spacing: 0
|
||||
visible: showSidebar
|
||||
|
||||
FileBrowserSidebar {
|
||||
height: parent.height
|
||||
quickAccessLocations: root.quickAccessLocations
|
||||
currentPath: root.currentPath
|
||||
onLocationSelected: path => navigateTo(path)
|
||||
}
|
||||
|
||||
StyledRect {
|
||||
width: 1
|
||||
height: parent.height
|
||||
color: Theme.outline
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
width: parent.width - (showSidebar ? 201 : 0)
|
||||
height: parent.height
|
||||
spacing: 0
|
||||
|
||||
FileBrowserNavigation {
|
||||
width: parent.width
|
||||
currentPath: root.currentPath
|
||||
homeDir: root.homeDir
|
||||
backButtonFocused: root.backButtonFocused
|
||||
keyboardNavigationActive: root.keyboardNavigationActive
|
||||
showSidebar: root.showSidebar
|
||||
pathEditMode: root.pathEditMode
|
||||
onNavigateUp: root.navigateUp()
|
||||
onNavigateTo: path => root.navigateTo(path)
|
||||
onPathInputFocusChanged: hasFocus => {
|
||||
root.pathInputHasFocus = hasFocus;
|
||||
if (hasFocus) {
|
||||
root.pathEditMode = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StyledRect {
|
||||
width: parent.width
|
||||
height: 1
|
||||
color: Theme.outline
|
||||
}
|
||||
|
||||
Item {
|
||||
id: gridContainer
|
||||
width: parent.width
|
||||
height: parent.height - 41
|
||||
clip: true
|
||||
|
||||
property real gridCellWidth: iconSizes[iconSizeIndex] + 24
|
||||
property real gridCellHeight: iconSizes[iconSizeIndex] + 56
|
||||
property real availableGridWidth: width - Theme.spacingM * 2
|
||||
property int gridColumns: Math.max(1, Math.floor(availableGridWidth / gridCellWidth))
|
||||
property real gridLeftMargin: Theme.spacingM + Math.max(0, (availableGridWidth - (gridColumns * gridCellWidth)) / 2)
|
||||
|
||||
onGridColumnsChanged: {
|
||||
root.actualGridColumns = gridColumns;
|
||||
}
|
||||
Component.onCompleted: {
|
||||
root.actualGridColumns = gridColumns;
|
||||
}
|
||||
|
||||
DankGridView {
|
||||
id: fileGrid
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: gridContainer.gridLeftMargin
|
||||
anchors.rightMargin: Theme.spacingM
|
||||
anchors.topMargin: Theme.spacingS
|
||||
anchors.bottomMargin: Theme.spacingS
|
||||
visible: viewMode === "grid"
|
||||
cellWidth: gridContainer.gridCellWidth
|
||||
cellHeight: gridContainer.gridCellHeight
|
||||
cacheBuffer: 260
|
||||
model: folderModel
|
||||
currentIndex: selectedIndex
|
||||
onCurrentIndexChanged: {
|
||||
if (keyboardNavigationActive && currentIndex >= 0)
|
||||
positionViewAtIndex(currentIndex, GridView.Contain);
|
||||
}
|
||||
|
||||
ScrollBar.vertical: DankScrollbar {
|
||||
id: gridScrollbar
|
||||
}
|
||||
|
||||
ScrollBar.horizontal: DankScrollbar {
|
||||
policy: ScrollBar.AlwaysOff
|
||||
}
|
||||
|
||||
delegate: FileBrowserGridDelegate {
|
||||
iconSizes: root.iconSizes
|
||||
iconSizeIndex: root.iconSizeIndex
|
||||
selectedIndex: root.selectedIndex
|
||||
keyboardNavigationActive: root.keyboardNavigationActive
|
||||
onItemClicked: (index, path, name, isDir) => {
|
||||
selectedIndex = index;
|
||||
setSelectedFileData(path, name, isDir);
|
||||
root.activateFile(path, name, isDir);
|
||||
}
|
||||
onItemSelected: (index, path, name, isDir) => {
|
||||
setSelectedFileData(path, name, isDir);
|
||||
}
|
||||
onItemContextMenuRequested: (sender, localX, localY, path, name, isDir) => {
|
||||
root.openItemContextMenu(sender, localX, localY, path, name, isDir);
|
||||
}
|
||||
|
||||
Connections {
|
||||
function onKeyboardSelectionRequestedChanged() {
|
||||
if (root.keyboardSelectionRequested && root.keyboardSelectionIndex === index) {
|
||||
root.keyboardSelectionRequested = false;
|
||||
selectedIndex = index;
|
||||
setSelectedFileData(filePath, fileName, fileIsDir);
|
||||
root.activateFile(filePath, fileName, fileIsDir);
|
||||
}
|
||||
}
|
||||
|
||||
target: root
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DankListView {
|
||||
id: fileList
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: Theme.spacingM
|
||||
anchors.rightMargin: Theme.spacingM
|
||||
anchors.topMargin: Theme.spacingS
|
||||
anchors.bottomMargin: Theme.spacingS
|
||||
visible: viewMode === "list"
|
||||
spacing: Theme.spacingXXS
|
||||
model: folderModel
|
||||
currentIndex: selectedIndex
|
||||
onCurrentIndexChanged: {
|
||||
if (keyboardNavigationActive && currentIndex >= 0)
|
||||
positionViewAtIndex(currentIndex, ListView.Contain);
|
||||
}
|
||||
|
||||
ScrollBar.vertical: DankScrollbar {
|
||||
id: listScrollbar
|
||||
}
|
||||
|
||||
delegate: FileBrowserListDelegate {
|
||||
width: fileList.width
|
||||
selectedIndex: root.selectedIndex
|
||||
keyboardNavigationActive: root.keyboardNavigationActive
|
||||
onItemClicked: (index, path, name, isDir) => {
|
||||
selectedIndex = index;
|
||||
setSelectedFileData(path, name, isDir);
|
||||
root.activateFile(path, name, isDir);
|
||||
}
|
||||
onItemSelected: (index, path, name, isDir) => {
|
||||
setSelectedFileData(path, name, isDir);
|
||||
}
|
||||
onItemContextMenuRequested: (sender, localX, localY, path, name, isDir) => {
|
||||
root.openItemContextMenu(sender, localX, localY, path, name, isDir);
|
||||
}
|
||||
|
||||
Connections {
|
||||
function onKeyboardSelectionRequestedChanged() {
|
||||
if (root.keyboardSelectionRequested && root.keyboardSelectionIndex === index) {
|
||||
root.keyboardSelectionRequested = false;
|
||||
selectedIndex = index;
|
||||
setSelectedFileData(filePath, fileName, fileIsDir);
|
||||
root.activateFile(filePath, fileName, fileIsDir);
|
||||
}
|
||||
}
|
||||
|
||||
target: root
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
FileBrowserSaveRow {
|
||||
id: saveRow
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.margins: Theme.spacingL
|
||||
saveMode: root.saveMode
|
||||
defaultFileName: root.defaultFileName
|
||||
currentPath: root.currentPath
|
||||
onSaveRequested: filePath => handleSaveFile(filePath)
|
||||
}
|
||||
|
||||
KeyboardHints {
|
||||
id: keyboardHints
|
||||
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.margins: Theme.spacingL
|
||||
showHints: root.showKeyboardHints
|
||||
}
|
||||
|
||||
FileInfo {
|
||||
id: fileInfo
|
||||
|
||||
anchors.top: parent.top
|
||||
anchors.right: parent.right
|
||||
anchors.margins: Theme.spacingL
|
||||
width: 300
|
||||
showFileInfo: root.showFileInfo
|
||||
selectedIndex: root.selectedIndex
|
||||
sourceFolderModel: folderModel
|
||||
currentPath: root.currentPath
|
||||
currentFileName: root.selectedFileName
|
||||
currentFileIsDir: root.selectedFileIsDir
|
||||
currentFileExtension: {
|
||||
if (root.selectedFileIsDir || !root.selectedFileName)
|
||||
return "";
|
||||
|
||||
var lastDot = root.selectedFileName.lastIndexOf('.');
|
||||
return lastDot > 0 ? root.selectedFileName.substring(lastDot + 1).toLowerCase() : "";
|
||||
}
|
||||
}
|
||||
|
||||
FileBrowserSortMenu {
|
||||
id: sortMenu
|
||||
anchors.top: parent.top
|
||||
anchors.right: parent.right
|
||||
anchors.topMargin: 120
|
||||
anchors.rightMargin: Theme.spacingL
|
||||
sortBy: root.sortBy
|
||||
sortAscending: root.sortAscending
|
||||
onSortBySelected: value => {
|
||||
root.sortBy = value;
|
||||
}
|
||||
onSortOrderSelected: ascending => {
|
||||
root.sortAscending = ascending;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
FileBrowserOverwriteDialog {
|
||||
anchors.fill: parent
|
||||
showDialog: showOverwriteConfirmation
|
||||
pendingFilePath: root.pendingFilePath
|
||||
onConfirmed: filePath => {
|
||||
showOverwriteConfirmation = false;
|
||||
fileSelected(filePath);
|
||||
pendingFilePath = "";
|
||||
Qt.callLater(() => root.closeRequested());
|
||||
}
|
||||
onCancelled: {
|
||||
showOverwriteConfirmation = false;
|
||||
pendingFilePath = "";
|
||||
}
|
||||
}
|
||||
|
||||
FileBrowserItemContextMenu {
|
||||
id: itemContextMenu
|
||||
parentFocusItem: root
|
||||
}
|
||||
}
|
||||
DankCommon.FileBrowserContent {}
|
||||
|
||||
@@ -1,252 +1,3 @@
|
||||
import QtQuick
|
||||
import Quickshell.Widgets
|
||||
import qs.Common
|
||||
import qs.Widgets
|
||||
import qs.DankCommon.Modals.FileBrowser as DankCommon
|
||||
|
||||
StyledRect {
|
||||
id: delegateRoot
|
||||
|
||||
required property bool fileIsDir
|
||||
required property string filePath
|
||||
required property string fileName
|
||||
required property int index
|
||||
|
||||
property bool weMode: false
|
||||
property var iconSizes: [80, 120, 160, 200]
|
||||
property int iconSizeIndex: 1
|
||||
property int selectedIndex: -1
|
||||
property bool keyboardNavigationActive: false
|
||||
|
||||
signal itemClicked(int index, string path, string name, bool isDir)
|
||||
signal itemSelected(int index, string path, string name, bool isDir)
|
||||
signal itemContextMenuRequested(var sender, real localX, real localY, string path, string name, bool isDir)
|
||||
|
||||
function getFileExtension(fileName) {
|
||||
const parts = fileName.split('.');
|
||||
if (parts.length > 1) {
|
||||
return parts[parts.length - 1].toLowerCase();
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
function determineFileType(fileName) {
|
||||
const ext = getFileExtension(fileName);
|
||||
|
||||
const imageExts = ["png", "jpg", "jpeg", "gif", "bmp", "webp", "svg", "ico", "jxl", "avif", "heif", "exr"];
|
||||
if (imageExts.includes(ext)) {
|
||||
return "image";
|
||||
}
|
||||
|
||||
const videoExts = ["mp4", "mkv", "avi", "mov", "webm", "flv", "wmv", "m4v"];
|
||||
if (videoExts.includes(ext)) {
|
||||
return "video";
|
||||
}
|
||||
|
||||
const audioExts = ["mp3", "wav", "flac", "ogg", "m4a", "aac", "wma"];
|
||||
if (audioExts.includes(ext)) {
|
||||
return "audio";
|
||||
}
|
||||
|
||||
const codeExts = ["js", "ts", "jsx", "tsx", "py", "go", "rs", "c", "cpp", "h", "java", "kt", "swift", "rb", "php", "html", "css", "scss", "json", "xml", "yaml", "yml", "toml", "sh", "bash", "zsh", "fish", "qml", "vue", "svelte"];
|
||||
if (codeExts.includes(ext)) {
|
||||
return "code";
|
||||
}
|
||||
|
||||
const docExts = ["txt", "md", "pdf", "doc", "docx", "odt", "rtf"];
|
||||
if (docExts.includes(ext)) {
|
||||
return "document";
|
||||
}
|
||||
|
||||
const archiveExts = ["zip", "tar", "gz", "bz2", "xz", "7z", "rar"];
|
||||
if (archiveExts.includes(ext)) {
|
||||
return "archive";
|
||||
}
|
||||
|
||||
if (!ext || fileName.indexOf('.') === -1) {
|
||||
return "binary";
|
||||
}
|
||||
|
||||
return "file";
|
||||
}
|
||||
|
||||
function isImageFile(fileName) {
|
||||
if (!fileName) {
|
||||
return false;
|
||||
}
|
||||
return determineFileType(fileName) === "image";
|
||||
}
|
||||
|
||||
function isVideoFile(fileName) {
|
||||
if (!fileName) {
|
||||
return false;
|
||||
}
|
||||
return determineFileType(fileName) === "video";
|
||||
}
|
||||
|
||||
property bool isImage: isImageFile(delegateRoot.fileName)
|
||||
property bool isVideo: isVideoFile(delegateRoot.fileName)
|
||||
|
||||
property string _xdgCacheHome: Paths.strip(Paths.xdgCache)
|
||||
property string _thumbnailSize: iconSizeIndex >= 2 ? "x-large" : "large"
|
||||
property int _thumbnailPx: iconSizeIndex >= 2 ? 512 : 256
|
||||
property string videoThumbnailPath: {
|
||||
if (!delegateRoot.fileIsDir && isVideo) {
|
||||
const hash = Qt.md5("file://" + delegateRoot.filePath);
|
||||
return _xdgCacheHome + "/thumbnails/" + _thumbnailSize + "/" + hash + ".png";
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
property string _videoThumb: ""
|
||||
property bool _thumbGenAttempted: false
|
||||
|
||||
// Probe the thumbnail optimistically; Image.Error triggers generation
|
||||
onVideoThumbnailPathChanged: {
|
||||
_thumbGenAttempted = false;
|
||||
_videoThumb = videoThumbnailPath;
|
||||
}
|
||||
|
||||
function generateVideoThumbnail() {
|
||||
if (_thumbGenAttempted)
|
||||
return;
|
||||
_thumbGenAttempted = true;
|
||||
_videoThumb = "";
|
||||
const thumbPath = videoThumbnailPath;
|
||||
const thumbDir = _xdgCacheHome + "/thumbnails/" + _thumbnailSize;
|
||||
const script = "mkdir -p \"$1\" && ffmpegthumbnailer -i \"$2\" -o \"$3\" -s " + _thumbnailPx + " -f";
|
||||
Proc.runCommand(null, ["sh", "-c", script, "thumb", thumbDir, delegateRoot.filePath, thumbPath], function (output, exitCode) {
|
||||
if (exitCode === 0)
|
||||
_videoThumb = thumbPath;
|
||||
});
|
||||
}
|
||||
|
||||
function getIconForFile(fileName) {
|
||||
const lowerName = fileName.toLowerCase();
|
||||
if (lowerName.startsWith("dockerfile")) {
|
||||
return "docker";
|
||||
}
|
||||
const ext = fileName.split('.').pop();
|
||||
return ext || "";
|
||||
}
|
||||
|
||||
width: weMode ? 245 : iconSizes[iconSizeIndex] + 16
|
||||
height: weMode ? 205 : iconSizes[iconSizeIndex] + 48
|
||||
radius: Theme.cornerRadius
|
||||
color: {
|
||||
if (keyboardNavigationActive && delegateRoot.index === selectedIndex)
|
||||
return Theme.surfacePressed;
|
||||
|
||||
return mouseArea.containsMouse ? Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency) : Theme.withAlpha(Theme.surfaceContainerHigh, 0);
|
||||
}
|
||||
border.color: keyboardNavigationActive && delegateRoot.index === selectedIndex ? Theme.primary : Theme.withAlpha(Theme.primary, 0)
|
||||
border.width: (keyboardNavigationActive && delegateRoot.index === selectedIndex) ? 2 : 0
|
||||
|
||||
Component.onCompleted: {
|
||||
if (keyboardNavigationActive && delegateRoot.index === selectedIndex)
|
||||
itemSelected(delegateRoot.index, delegateRoot.filePath, delegateRoot.fileName, delegateRoot.fileIsDir);
|
||||
}
|
||||
|
||||
onSelectedIndexChanged: {
|
||||
if (keyboardNavigationActive && selectedIndex === delegateRoot.index)
|
||||
itemSelected(delegateRoot.index, delegateRoot.filePath, delegateRoot.fileName, delegateRoot.fileIsDir);
|
||||
}
|
||||
|
||||
Column {
|
||||
anchors.centerIn: parent
|
||||
spacing: Theme.spacingS
|
||||
|
||||
Item {
|
||||
width: weMode ? 225 : (iconSizes[iconSizeIndex] - 8)
|
||||
height: weMode ? 165 : (iconSizes[iconSizeIndex] - 8)
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
|
||||
ClippingRectangle {
|
||||
anchors.fill: parent
|
||||
anchors.margins: 2
|
||||
radius: Theme.cornerRadius
|
||||
color: "transparent"
|
||||
|
||||
Image {
|
||||
id: gridPreviewImage
|
||||
anchors.fill: parent
|
||||
property var weExtensions: [".jpg", ".jpeg", ".png", ".webp", ".gif", ".bmp", ".tga", ".jxl", ".avif", ".heif", ".exr"]
|
||||
property int weExtIndex: 0
|
||||
property string imagePath: {
|
||||
if (weMode && delegateRoot.fileIsDir)
|
||||
return delegateRoot.filePath + "/preview" + weExtensions[weExtIndex];
|
||||
if (_videoThumb)
|
||||
return _videoThumb;
|
||||
return "";
|
||||
}
|
||||
source: imagePath ? "file://" + imagePath.split('/').map(s => encodeURIComponent(s)).join('/') : ""
|
||||
onStatusChanged: {
|
||||
if (status !== Image.Error)
|
||||
return;
|
||||
if (weMode && delegateRoot.fileIsDir) {
|
||||
if (weExtIndex < weExtensions.length - 1) {
|
||||
weExtIndex++;
|
||||
} else {
|
||||
imagePath = "";
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (_videoThumb)
|
||||
generateVideoThumbnail();
|
||||
}
|
||||
fillMode: Image.PreserveAspectCrop
|
||||
sourceSize.width: weMode ? 225 : iconSizes[iconSizeIndex]
|
||||
sourceSize.height: weMode ? 225 : iconSizes[iconSizeIndex]
|
||||
asynchronous: true
|
||||
visible: status === Image.Ready && ((!delegateRoot.fileIsDir && isVideo) || (weMode && delegateRoot.fileIsDir))
|
||||
}
|
||||
|
||||
CachingImage {
|
||||
anchors.fill: parent
|
||||
imagePath: !delegateRoot.fileIsDir && isImage ? delegateRoot.filePath : ""
|
||||
maxCacheSize: 256
|
||||
animate: false
|
||||
visible: !delegateRoot.fileIsDir && isImage
|
||||
}
|
||||
}
|
||||
|
||||
DankNFIcon {
|
||||
anchors.centerIn: parent
|
||||
name: delegateRoot.fileIsDir ? "folder" : getIconForFile(delegateRoot.fileName)
|
||||
size: iconSizes[iconSizeIndex] * 0.45
|
||||
color: delegateRoot.fileIsDir ? Theme.primary : Theme.surfaceText
|
||||
visible: (!delegateRoot.fileIsDir && !isImage && !(isVideo && gridPreviewImage.status === Image.Ready)) || (delegateRoot.fileIsDir && !weMode)
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: delegateRoot.fileName || ""
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceText
|
||||
width: delegateRoot.width - Theme.spacingM
|
||||
elide: Text.ElideRight
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
maximumLineCount: 2
|
||||
wrapMode: Text.Wrap
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: mouseArea
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
acceptedButtons: Qt.LeftButton | Qt.RightButton
|
||||
onClicked: mouse => {
|
||||
switch (mouse.button) {
|
||||
case Qt.LeftButton:
|
||||
itemClicked(delegateRoot.index, delegateRoot.filePath, delegateRoot.fileName, delegateRoot.fileIsDir);
|
||||
break;
|
||||
case Qt.RightButton:
|
||||
itemContextMenuRequested(delegateRoot, mouse.x, mouse.y, delegateRoot.filePath, delegateRoot.fileName, delegateRoot.fileIsDir);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
DankCommon.FileBrowserGridDelegate {}
|
||||
|
||||
@@ -1,153 +1,3 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import Quickshell
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
import qs.DankCommon.Modals.FileBrowser as DankCommon
|
||||
|
||||
Popup {
|
||||
id: root
|
||||
|
||||
property string filePath: ""
|
||||
property string fileName: ""
|
||||
property bool fileIsDir: false
|
||||
property var parentFocusItem: null
|
||||
|
||||
signal trashed
|
||||
signal menuClosed
|
||||
|
||||
readonly property var menuItems: [
|
||||
{
|
||||
text: I18n.tr("Move to Trash"),
|
||||
icon: "delete",
|
||||
action: trashItem,
|
||||
enabled: filePath.length > 0,
|
||||
dangerous: true
|
||||
},
|
||||
{
|
||||
text: I18n.tr("Copy path"),
|
||||
icon: "content_copy",
|
||||
action: copyPath,
|
||||
enabled: filePath.length > 0
|
||||
}
|
||||
]
|
||||
|
||||
function showAt(parentItem, localX, localY, path, name, isDir) {
|
||||
if (!parentItem)
|
||||
return;
|
||||
parent = parentItem;
|
||||
filePath = path || "";
|
||||
fileName = name || "";
|
||||
fileIsDir = !!isDir;
|
||||
x = Math.max(0, Math.min(parentItem.width - width, localX));
|
||||
y = Math.max(0, Math.min(parentItem.height - height, localY));
|
||||
open();
|
||||
}
|
||||
|
||||
function trashItem() {
|
||||
if (!filePath)
|
||||
return;
|
||||
TrashService.trashPath(filePath, ok => {
|
||||
if (ok)
|
||||
root.trashed();
|
||||
});
|
||||
close();
|
||||
}
|
||||
|
||||
function copyPath() {
|
||||
if (!filePath)
|
||||
return;
|
||||
Quickshell.execDetached(["dms", "cl", "copy", filePath]);
|
||||
close();
|
||||
}
|
||||
|
||||
width: 220
|
||||
height: menuColumn.implicitHeight + Theme.spacingS * 2
|
||||
padding: 0
|
||||
modal: false
|
||||
closePolicy: Popup.CloseOnEscape
|
||||
|
||||
onClosed: {
|
||||
closePolicy = Popup.CloseOnEscape;
|
||||
menuClosed();
|
||||
if (parentFocusItem)
|
||||
Qt.callLater(() => parentFocusItem.forceActiveFocus());
|
||||
}
|
||||
|
||||
onOpened: outsideClickTimer.start()
|
||||
|
||||
Timer {
|
||||
id: outsideClickTimer
|
||||
interval: 100
|
||||
onTriggered: root.closePolicy = Popup.CloseOnEscape | Popup.CloseOnPressOutside
|
||||
}
|
||||
|
||||
background: Rectangle {
|
||||
color: "transparent"
|
||||
}
|
||||
|
||||
contentItem: Rectangle {
|
||||
color: Theme.floatingSurface
|
||||
radius: Theme.cornerRadius
|
||||
border.color: BlurService.borderColor
|
||||
border.width: BlurService.borderWidth
|
||||
|
||||
Column {
|
||||
id: menuColumn
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingS
|
||||
spacing: 1
|
||||
|
||||
Repeater {
|
||||
model: root.menuItems
|
||||
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: 32
|
||||
radius: Theme.cornerRadius
|
||||
opacity: modelData.enabled ? 1 : 0.5
|
||||
color: {
|
||||
if (!modelData.enabled || !area.containsMouse)
|
||||
return "transparent";
|
||||
if (modelData.dangerous)
|
||||
return Theme.errorHover;
|
||||
return BlurService.hoverColor(Theme.widgetBaseHoverColor);
|
||||
}
|
||||
|
||||
Row {
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Theme.spacingS
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: Theme.spacingS
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Theme.spacingS
|
||||
|
||||
DankIcon {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
name: modelData.icon
|
||||
size: 16
|
||||
color: modelData.dangerous && area.containsMouse && modelData.enabled ? Theme.error : Theme.surfaceText
|
||||
}
|
||||
|
||||
StyledText {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: modelData.text
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: modelData.dangerous && area.containsMouse && modelData.enabled ? Theme.error : Theme.surfaceText
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: area
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
enabled: modelData.enabled
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: modelData.action()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
DankCommon.FileBrowserItemContextMenu {}
|
||||
|
||||
@@ -1,256 +1,3 @@
|
||||
import QtQuick
|
||||
import Quickshell.Widgets
|
||||
import qs.Common
|
||||
import qs.Widgets
|
||||
import qs.DankCommon.Modals.FileBrowser as DankCommon
|
||||
|
||||
StyledRect {
|
||||
id: listDelegateRoot
|
||||
|
||||
required property bool fileIsDir
|
||||
required property string filePath
|
||||
required property string fileName
|
||||
required property int index
|
||||
required property var fileModified
|
||||
required property int fileSize
|
||||
|
||||
property int selectedIndex: -1
|
||||
property bool keyboardNavigationActive: false
|
||||
|
||||
signal itemClicked(int index, string path, string name, bool isDir)
|
||||
signal itemSelected(int index, string path, string name, bool isDir)
|
||||
signal itemContextMenuRequested(var sender, real localX, real localY, string path, string name, bool isDir)
|
||||
|
||||
function getFileExtension(fileName) {
|
||||
const parts = fileName.split('.');
|
||||
if (parts.length > 1) {
|
||||
return parts[parts.length - 1].toLowerCase();
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
function determineFileType(fileName) {
|
||||
const ext = getFileExtension(fileName);
|
||||
|
||||
const imageExts = ["png", "jpg", "jpeg", "gif", "bmp", "webp", "svg", "ico", "jxl", "avif", "heif", "exr"];
|
||||
if (imageExts.includes(ext)) {
|
||||
return "image";
|
||||
}
|
||||
|
||||
const videoExts = ["mp4", "mkv", "avi", "mov", "webm", "flv", "wmv", "m4v"];
|
||||
if (videoExts.includes(ext)) {
|
||||
return "video";
|
||||
}
|
||||
|
||||
const audioExts = ["mp3", "wav", "flac", "ogg", "m4a", "aac", "wma"];
|
||||
if (audioExts.includes(ext)) {
|
||||
return "audio";
|
||||
}
|
||||
|
||||
const codeExts = ["js", "ts", "jsx", "tsx", "py", "go", "rs", "c", "cpp", "h", "java", "kt", "swift", "rb", "php", "html", "css", "scss", "json", "xml", "yaml", "yml", "toml", "sh", "bash", "zsh", "fish", "qml", "vue", "svelte"];
|
||||
if (codeExts.includes(ext)) {
|
||||
return "code";
|
||||
}
|
||||
|
||||
const docExts = ["txt", "md", "pdf", "doc", "docx", "odt", "rtf"];
|
||||
if (docExts.includes(ext)) {
|
||||
return "document";
|
||||
}
|
||||
|
||||
const archiveExts = ["zip", "tar", "gz", "bz2", "xz", "7z", "rar"];
|
||||
if (archiveExts.includes(ext)) {
|
||||
return "archive";
|
||||
}
|
||||
|
||||
if (!ext || fileName.indexOf('.') === -1) {
|
||||
return "binary";
|
||||
}
|
||||
|
||||
return "file";
|
||||
}
|
||||
|
||||
function isImageFile(fileName) {
|
||||
if (!fileName) {
|
||||
return false;
|
||||
}
|
||||
return determineFileType(fileName) === "image";
|
||||
}
|
||||
|
||||
function isVideoFile(fileName) {
|
||||
if (!fileName) {
|
||||
return false;
|
||||
}
|
||||
return determineFileType(fileName) === "video";
|
||||
}
|
||||
|
||||
property bool isImage: isImageFile(listDelegateRoot.fileName)
|
||||
property bool isVideo: isVideoFile(listDelegateRoot.fileName)
|
||||
|
||||
property string _xdgCacheHome: Paths.strip(Paths.xdgCache)
|
||||
property string videoThumbnailPath: {
|
||||
if (!listDelegateRoot.fileIsDir && isVideo) {
|
||||
const hash = Qt.md5("file://" + listDelegateRoot.filePath);
|
||||
return _xdgCacheHome + "/thumbnails/normal/" + hash + ".png";
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
property string _videoThumb: ""
|
||||
property bool _thumbGenAttempted: false
|
||||
|
||||
// Probe the thumbnail optimistically; Image.Error triggers generation
|
||||
onVideoThumbnailPathChanged: {
|
||||
_thumbGenAttempted = false;
|
||||
_videoThumb = videoThumbnailPath;
|
||||
}
|
||||
|
||||
function generateVideoThumbnail() {
|
||||
if (_thumbGenAttempted)
|
||||
return;
|
||||
_thumbGenAttempted = true;
|
||||
_videoThumb = "";
|
||||
const thumbPath = videoThumbnailPath;
|
||||
const thumbDir = _xdgCacheHome + "/thumbnails/normal";
|
||||
const script = "mkdir -p \"$1\" && ffmpegthumbnailer -i \"$2\" -o \"$3\" -s 128 -f";
|
||||
Proc.runCommand(null, ["sh", "-c", script, "thumb", thumbDir, listDelegateRoot.filePath, thumbPath], function (output, exitCode) {
|
||||
if (exitCode === 0)
|
||||
_videoThumb = thumbPath;
|
||||
});
|
||||
}
|
||||
|
||||
function getIconForFile(fileName) {
|
||||
const lowerName = fileName.toLowerCase();
|
||||
if (lowerName.startsWith("dockerfile")) {
|
||||
return "docker";
|
||||
}
|
||||
const ext = fileName.split('.').pop();
|
||||
return ext || "";
|
||||
}
|
||||
|
||||
function formatFileSize(size) {
|
||||
if (size < 1024)
|
||||
return size + " B";
|
||||
if (size < 1024 * 1024)
|
||||
return (size / 1024).toFixed(1) + " KB";
|
||||
if (size < 1024 * 1024 * 1024)
|
||||
return (size / (1024 * 1024)).toFixed(1) + " MB";
|
||||
return (size / (1024 * 1024 * 1024)).toFixed(1) + " GB";
|
||||
}
|
||||
|
||||
height: 44
|
||||
radius: Theme.cornerRadius
|
||||
color: {
|
||||
if (keyboardNavigationActive && listDelegateRoot.index === selectedIndex)
|
||||
return Theme.surfacePressed;
|
||||
return listMouseArea.containsMouse ? Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency) : Theme.withAlpha(Theme.surfaceContainerHigh, 0);
|
||||
}
|
||||
border.color: keyboardNavigationActive && listDelegateRoot.index === selectedIndex ? Theme.primary : Theme.withAlpha(Theme.primary, 0)
|
||||
border.width: (keyboardNavigationActive && listDelegateRoot.index === selectedIndex) ? 2 : 0
|
||||
|
||||
Component.onCompleted: {
|
||||
if (keyboardNavigationActive && listDelegateRoot.index === selectedIndex)
|
||||
itemSelected(listDelegateRoot.index, listDelegateRoot.filePath, listDelegateRoot.fileName, listDelegateRoot.fileIsDir);
|
||||
}
|
||||
|
||||
onSelectedIndexChanged: {
|
||||
if (keyboardNavigationActive && selectedIndex === listDelegateRoot.index)
|
||||
itemSelected(listDelegateRoot.index, listDelegateRoot.filePath, listDelegateRoot.fileName, listDelegateRoot.fileIsDir);
|
||||
}
|
||||
|
||||
Row {
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: Theme.spacingS
|
||||
anchors.rightMargin: Theme.spacingS
|
||||
spacing: Theme.spacingS
|
||||
|
||||
Item {
|
||||
width: 28
|
||||
height: 28
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
ClippingRectangle {
|
||||
anchors.fill: parent
|
||||
radius: Theme.cornerRadius
|
||||
color: "transparent"
|
||||
|
||||
Image {
|
||||
id: listPreviewImage
|
||||
anchors.fill: parent
|
||||
property string imagePath: _videoThumb
|
||||
source: imagePath ? "file://" + imagePath.split('/').map(s => encodeURIComponent(s)).join('/') : ""
|
||||
fillMode: Image.PreserveAspectCrop
|
||||
sourceSize.width: 32
|
||||
sourceSize.height: 32
|
||||
asynchronous: true
|
||||
visible: status === Image.Ready && !listDelegateRoot.fileIsDir && isVideo
|
||||
onStatusChanged: {
|
||||
if (status === Image.Error && _videoThumb)
|
||||
generateVideoThumbnail();
|
||||
}
|
||||
}
|
||||
|
||||
CachingImage {
|
||||
anchors.fill: parent
|
||||
imagePath: !listDelegateRoot.fileIsDir && isImage ? listDelegateRoot.filePath : ""
|
||||
maxCacheSize: 256
|
||||
animate: false
|
||||
visible: !listDelegateRoot.fileIsDir && isImage
|
||||
}
|
||||
}
|
||||
|
||||
DankNFIcon {
|
||||
anchors.centerIn: parent
|
||||
name: listDelegateRoot.fileIsDir ? "folder" : getIconForFile(listDelegateRoot.fileName)
|
||||
size: Theme.iconSize - 2
|
||||
color: listDelegateRoot.fileIsDir ? Theme.primary : Theme.surfaceText
|
||||
visible: listDelegateRoot.fileIsDir || (!isImage && !(isVideo && listPreviewImage.status === Image.Ready))
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: listDelegateRoot.fileName || ""
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceText
|
||||
width: parent.width - 280
|
||||
elide: Text.ElideRight
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
maximumLineCount: 1
|
||||
clip: true
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: listDelegateRoot.fileIsDir ? "" : formatFileSize(listDelegateRoot.fileSize)
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceTextMedium
|
||||
width: 70
|
||||
horizontalAlignment: Text.AlignRight
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: Qt.formatDateTime(listDelegateRoot.fileModified, "MMM d, yyyy h:mm AP")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceTextMedium
|
||||
width: 140
|
||||
horizontalAlignment: Text.AlignRight
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: listMouseArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
acceptedButtons: Qt.LeftButton | Qt.RightButton
|
||||
onClicked: mouse => {
|
||||
switch (mouse.button) {
|
||||
case Qt.LeftButton:
|
||||
itemClicked(listDelegateRoot.index, listDelegateRoot.filePath, listDelegateRoot.fileName, listDelegateRoot.fileIsDir);
|
||||
break;
|
||||
case Qt.RightButton:
|
||||
itemContextMenuRequested(listDelegateRoot, mouse.x, mouse.y, listDelegateRoot.filePath, listDelegateRoot.fileName, listDelegateRoot.fileIsDir);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
DankCommon.FileBrowserListDelegate {}
|
||||
|
||||
@@ -1,100 +1,3 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import qs.Common
|
||||
import qs.Widgets
|
||||
import qs.DankCommon.Modals.FileBrowser as DankCommon
|
||||
|
||||
FloatingWindow {
|
||||
id: fileBrowserModal
|
||||
|
||||
property bool disablePopupTransparency: true
|
||||
property string browserTitle: "Select File"
|
||||
property string browserIcon: "folder_open"
|
||||
property string browserType: "generic"
|
||||
property var fileExtensions: ["*.*"]
|
||||
property alias filterExtensions: fileBrowserModal.fileExtensions
|
||||
property bool showHiddenFiles: false
|
||||
property bool saveMode: false
|
||||
property string defaultFileName: ""
|
||||
property var parentModal: null
|
||||
parentWindow: parentModal
|
||||
property bool shouldHaveFocus: visible
|
||||
property bool allowFocusOverride: false
|
||||
property bool shouldBeVisible: visible
|
||||
property bool allowStacking: true
|
||||
|
||||
signal fileSelected(string path)
|
||||
signal dialogClosed
|
||||
|
||||
function open() {
|
||||
visible = true;
|
||||
}
|
||||
|
||||
function close() {
|
||||
visible = false;
|
||||
}
|
||||
|
||||
objectName: "fileBrowserModal"
|
||||
title: "Files - " + browserTitle
|
||||
minimumSize: Qt.size(500, 400)
|
||||
implicitWidth: 800
|
||||
implicitHeight: 600
|
||||
color: Theme.surfaceContainer
|
||||
visible: false
|
||||
|
||||
onClosed: close()
|
||||
|
||||
onVisibleChanged: {
|
||||
if (visible) {
|
||||
if (parentModal && "shouldHaveFocus" in parentModal) {
|
||||
parentModal.shouldHaveFocus = false;
|
||||
parentModal.allowFocusOverride = true;
|
||||
}
|
||||
Qt.callLater(() => {
|
||||
if (content) {
|
||||
content.reset();
|
||||
content.forceActiveFocus();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
if (parentModal && "allowFocusOverride" in parentModal) {
|
||||
parentModal.allowFocusOverride = false;
|
||||
parentModal.shouldHaveFocus = Qt.binding(() => parentModal.shouldBeVisible);
|
||||
}
|
||||
dialogClosed();
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: contentLoader
|
||||
anchors.fill: parent
|
||||
active: fileBrowserModal.visible
|
||||
sourceComponent: FileBrowserContent {
|
||||
id: content
|
||||
anchors.fill: parent
|
||||
focus: true
|
||||
closeOnEscape: false
|
||||
windowControls: fileBrowserModal.windowControlsRef
|
||||
|
||||
browserTitle: fileBrowserModal.browserTitle
|
||||
browserIcon: fileBrowserModal.browserIcon
|
||||
browserType: fileBrowserModal.browserType
|
||||
fileExtensions: fileBrowserModal.fileExtensions
|
||||
showHiddenFiles: fileBrowserModal.showHiddenFiles
|
||||
saveMode: fileBrowserModal.saveMode
|
||||
defaultFileName: fileBrowserModal.defaultFileName
|
||||
|
||||
Component.onCompleted: initialize()
|
||||
|
||||
onFileSelected: path => fileBrowserModal.fileSelected(path)
|
||||
onCloseRequested: fileBrowserModal.close()
|
||||
}
|
||||
}
|
||||
|
||||
property alias content: contentLoader.item
|
||||
property alias windowControlsRef: windowControls
|
||||
|
||||
FloatingWindowControls {
|
||||
id: windowControls
|
||||
targetWindow: fileBrowserModal
|
||||
}
|
||||
}
|
||||
DankCommon.FileBrowserModal {}
|
||||
|
||||
@@ -1,130 +1,3 @@
|
||||
import QtQuick
|
||||
import qs.Common
|
||||
import qs.Widgets
|
||||
import qs.DankCommon.Modals.FileBrowser as DankCommon
|
||||
|
||||
Row {
|
||||
id: navigation
|
||||
|
||||
property string currentPath: ""
|
||||
property string homeDir: ""
|
||||
property bool backButtonFocused: false
|
||||
property bool keyboardNavigationActive: false
|
||||
property bool showSidebar: true
|
||||
property bool pathEditMode: false
|
||||
property bool pathInputHasFocus: false
|
||||
|
||||
signal navigateUp
|
||||
signal navigateTo(string path)
|
||||
signal pathInputFocusChanged(bool hasFocus)
|
||||
|
||||
height: 40
|
||||
leftPadding: Theme.spacingM
|
||||
rightPadding: Theme.spacingM
|
||||
spacing: Theme.spacingS
|
||||
|
||||
StyledRect {
|
||||
width: 32
|
||||
height: 32
|
||||
radius: Theme.cornerRadius
|
||||
color: (backButtonMouseArea.containsMouse || (backButtonFocused && keyboardNavigationActive)) && currentPath !== homeDir ? Theme.surfaceVariant : Theme.withAlpha(Theme.surfaceVariant, 0)
|
||||
opacity: currentPath !== homeDir ? 1 : 0
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
DankIcon {
|
||||
anchors.centerIn: parent
|
||||
name: "arrow_back"
|
||||
size: Theme.iconSizeSmall
|
||||
color: Theme.surfaceText
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: backButtonMouseArea
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: currentPath !== homeDir
|
||||
cursorShape: currentPath !== homeDir ? Qt.PointingHandCursor : Qt.ArrowCursor
|
||||
enabled: currentPath !== homeDir
|
||||
onClicked: navigation.navigateUp()
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
width: Math.max(0, (parent?.width ?? 0) - 40 - Theme.spacingS - (showSidebar ? 0 : 80))
|
||||
height: 32
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
StyledRect {
|
||||
anchors.fill: parent
|
||||
radius: Theme.cornerRadius
|
||||
color: pathEditMode ? Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency) : Theme.withAlpha(Theme.surfaceContainer, 0)
|
||||
border.color: pathEditMode ? Theme.primary : Theme.withAlpha(Theme.primary, 0)
|
||||
border.width: pathEditMode ? 1 : 0
|
||||
visible: !pathEditMode
|
||||
|
||||
StyledText {
|
||||
id: pathDisplay
|
||||
text: currentPath.replace("file://", "")
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: Theme.spacingS
|
||||
anchors.rightMargin: Theme.spacingS
|
||||
elide: Text.ElideMiddle
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
maximumLineCount: 1
|
||||
wrapMode: Text.NoWrap
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
cursorShape: Qt.IBeamCursor
|
||||
onClicked: {
|
||||
pathEditMode = true;
|
||||
pathInput.text = currentPath.replace("file://", "");
|
||||
Qt.callLater(() => pathInput.forceActiveFocus());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DankTextField {
|
||||
id: pathInput
|
||||
anchors.fill: parent
|
||||
visible: pathEditMode
|
||||
topPadding: Theme.spacingXS
|
||||
bottomPadding: Theme.spacingXS
|
||||
onAccepted: {
|
||||
const newPath = text.trim();
|
||||
if (newPath !== "") {
|
||||
navigation.navigateTo(newPath);
|
||||
}
|
||||
pathEditMode = false;
|
||||
}
|
||||
Keys.onEscapePressed: {
|
||||
pathEditMode = false;
|
||||
}
|
||||
Keys.onDownPressed: {
|
||||
pathEditMode = false;
|
||||
}
|
||||
onActiveFocusChanged: {
|
||||
navigation.pathInputFocusChanged(activeFocus);
|
||||
if (!activeFocus && pathEditMode) {
|
||||
pathEditMode = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
spacing: Theme.spacingXS
|
||||
visible: !showSidebar
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
DankActionButton {
|
||||
circular: false
|
||||
iconName: "sort"
|
||||
iconSize: Theme.iconSize - 6
|
||||
iconColor: Theme.surfaceText
|
||||
}
|
||||
}
|
||||
}
|
||||
DankCommon.FileBrowserNavigation {}
|
||||
|
||||
@@ -1,127 +1,3 @@
|
||||
import QtQuick
|
||||
import qs.Common
|
||||
import qs.Widgets
|
||||
import qs.DankCommon.Modals.FileBrowser as DankCommon
|
||||
|
||||
Item {
|
||||
id: overwriteDialog
|
||||
|
||||
property bool showDialog: false
|
||||
property string pendingFilePath: ""
|
||||
|
||||
signal confirmed(string filePath)
|
||||
signal cancelled
|
||||
|
||||
visible: showDialog
|
||||
focus: showDialog
|
||||
|
||||
Keys.onEscapePressed: {
|
||||
cancelled();
|
||||
}
|
||||
|
||||
Keys.onReturnPressed: {
|
||||
confirmed(pendingFilePath);
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
color: Theme.shadowStrong
|
||||
opacity: 0.8
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
onClicked: {
|
||||
cancelled();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StyledRect {
|
||||
anchors.centerIn: parent
|
||||
width: 400
|
||||
height: 160
|
||||
color: Theme.surfaceContainer
|
||||
radius: Theme.cornerRadius
|
||||
border.color: Theme.outlineMedium
|
||||
border.width: 1
|
||||
|
||||
Column {
|
||||
anchors.centerIn: parent
|
||||
width: parent.width - Theme.spacingL * 2
|
||||
spacing: Theme.spacingM
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("File Already Exists")
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
font.weight: Font.Medium
|
||||
color: Theme.surfaceText
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("A file with this name already exists. Do you want to overwrite it?")
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceTextMedium
|
||||
width: parent.width
|
||||
wrapMode: Text.WordWrap
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
}
|
||||
|
||||
Row {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
spacing: Theme.spacingM
|
||||
|
||||
StyledRect {
|
||||
width: 80
|
||||
height: 36
|
||||
radius: Theme.cornerRadius
|
||||
color: cancelArea.containsMouse ? Qt.lighter(Theme.surfaceVariant, 1.2) : Theme.surfaceVariant
|
||||
border.color: Theme.outline
|
||||
border.width: 1
|
||||
|
||||
StyledText {
|
||||
anchors.centerIn: parent
|
||||
text: I18n.tr("Cancel")
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: cancelArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
cancelled();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StyledRect {
|
||||
width: 90
|
||||
height: 36
|
||||
radius: Theme.cornerRadius
|
||||
color: overwriteArea.containsMouse ? Qt.darker(Theme.primary, 1.1) : Theme.primary
|
||||
|
||||
StyledText {
|
||||
anchors.centerIn: parent
|
||||
text: I18n.tr("Overwrite")
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.background
|
||||
font.weight: Font.Medium
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: overwriteArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
confirmed(pendingFilePath);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
DankCommon.FileBrowserOverwriteDialog {}
|
||||
|
||||
@@ -1,75 +1,3 @@
|
||||
import QtQuick
|
||||
import qs.Common
|
||||
import qs.Widgets
|
||||
import qs.DankCommon.Modals.FileBrowser as DankCommon
|
||||
|
||||
Row {
|
||||
id: saveRow
|
||||
|
||||
property bool saveMode: false
|
||||
property string defaultFileName: ""
|
||||
property string currentPath: ""
|
||||
property alias fileName: fileNameInput.text
|
||||
|
||||
signal saveRequested(string filePath)
|
||||
|
||||
height: saveMode ? 40 : 0
|
||||
visible: saveMode
|
||||
spacing: Theme.spacingM
|
||||
|
||||
DankTextField {
|
||||
id: fileNameInput
|
||||
|
||||
width: parent.width - saveButton.width - Theme.spacingM
|
||||
height: 40
|
||||
text: defaultFileName
|
||||
placeholderText: I18n.tr("Enter filename...")
|
||||
ignoreLeftRightKeys: false
|
||||
focus: saveMode
|
||||
topPadding: Theme.spacingS
|
||||
bottomPadding: Theme.spacingS
|
||||
Component.onCompleted: {
|
||||
if (saveMode)
|
||||
Qt.callLater(() => {
|
||||
forceActiveFocus();
|
||||
});
|
||||
}
|
||||
onAccepted: {
|
||||
if (text.trim() !== "") {
|
||||
var basePath = currentPath.replace(/^file:\/\//, '');
|
||||
var fullPath = basePath + "/" + text.trim();
|
||||
fullPath = fullPath.replace(/\/+/g, '/');
|
||||
saveRequested(fullPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StyledRect {
|
||||
id: saveButton
|
||||
|
||||
width: 80
|
||||
height: 40
|
||||
color: fileNameInput.text.trim() !== "" ? Theme.primary : Theme.surfaceVariant
|
||||
radius: Theme.cornerRadius
|
||||
|
||||
StyledText {
|
||||
anchors.centerIn: parent
|
||||
text: I18n.tr("Save")
|
||||
color: fileNameInput.text.trim() !== "" ? Theme.primaryText : Theme.surfaceVariantText
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
}
|
||||
|
||||
StateLayer {
|
||||
stateColor: Theme.primary
|
||||
cornerRadius: Theme.cornerRadius
|
||||
enabled: fileNameInput.text.trim() !== ""
|
||||
onClicked: {
|
||||
if (fileNameInput.text.trim() !== "") {
|
||||
var basePath = currentPath.replace(/^file:\/\//, '');
|
||||
var fullPath = basePath + "/" + fileNameInput.text.trim();
|
||||
fullPath = fullPath.replace(/\/+/g, '/');
|
||||
saveRequested(fullPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
DankCommon.FileBrowserSaveRow {}
|
||||
|
||||
@@ -1,70 +1,3 @@
|
||||
import QtQuick
|
||||
import qs.Common
|
||||
import qs.Widgets
|
||||
import qs.DankCommon.Modals.FileBrowser as DankCommon
|
||||
|
||||
StyledRect {
|
||||
id: sidebar
|
||||
|
||||
property var quickAccessLocations: []
|
||||
property string currentPath: ""
|
||||
signal locationSelected(string path)
|
||||
|
||||
width: 200
|
||||
color: Theme.nestedSurface
|
||||
clip: true
|
||||
|
||||
Column {
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingS
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Quick Access")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceTextMedium
|
||||
font.weight: Font.Medium
|
||||
leftPadding: Theme.spacingS
|
||||
bottomPadding: Theme.spacingXS
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: quickAccessLocations
|
||||
|
||||
StyledRect {
|
||||
width: parent?.width ?? 0
|
||||
height: 38
|
||||
radius: Theme.cornerRadius
|
||||
color: quickAccessMouseArea.containsMouse ? Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency) : (currentPath === modelData?.path ? Theme.surfacePressed : Theme.withAlpha(Theme.surfacePressed, 0))
|
||||
|
||||
Row {
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: Theme.spacingM
|
||||
spacing: Theme.spacingS
|
||||
|
||||
DankIcon {
|
||||
name: modelData?.icon ?? ""
|
||||
size: Theme.iconSize - 2
|
||||
color: currentPath === modelData?.path ? Theme.primary : Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: modelData?.name ?? ""
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: currentPath === modelData?.path ? Theme.primary : Theme.surfaceText
|
||||
font.weight: currentPath === modelData?.path ? Font.Medium : Font.Normal
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: quickAccessMouseArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: locationSelected(modelData?.path ?? "")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
DankCommon.FileBrowserSidebar {}
|
||||
|
||||
@@ -1,189 +1,3 @@
|
||||
import QtQuick
|
||||
import qs.Common
|
||||
import qs.Widgets
|
||||
import qs.DankCommon.Modals.FileBrowser as DankCommon
|
||||
|
||||
StyledRect {
|
||||
id: sortMenu
|
||||
|
||||
property string sortBy: "name"
|
||||
property bool sortAscending: true
|
||||
property color surfaceColor: Theme.surfaceContainer
|
||||
|
||||
signal sortBySelected(string value)
|
||||
signal sortOrderSelected(bool ascending)
|
||||
|
||||
width: 200
|
||||
height: sortColumn.height + Theme.spacingM * 2
|
||||
color: surfaceColor
|
||||
radius: Theme.cornerRadius
|
||||
border.color: Theme.outlineMedium
|
||||
border.width: 1
|
||||
visible: false
|
||||
z: 100
|
||||
|
||||
Column {
|
||||
id: sortColumn
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
anchors.margins: Theme.spacingM
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
StyledText {
|
||||
text: "Sort By"
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceTextMedium
|
||||
font.weight: Font.Medium
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: [
|
||||
{
|
||||
"name": "Name",
|
||||
"value": "name"
|
||||
},
|
||||
{
|
||||
"name": "Size",
|
||||
"value": "size"
|
||||
},
|
||||
{
|
||||
"name": "Modified",
|
||||
"value": "modified"
|
||||
},
|
||||
{
|
||||
"name": "Type",
|
||||
"value": "type"
|
||||
}
|
||||
]
|
||||
|
||||
StyledRect {
|
||||
width: sortColumn?.width ?? 0
|
||||
height: 32
|
||||
radius: Theme.cornerRadius
|
||||
color: sortMouseArea.containsMouse ? Theme.surfaceVariant : (sortBy === modelData?.value ? Theme.surfacePressed : Theme.withAlpha(Theme.surfacePressed, 0))
|
||||
|
||||
Row {
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: Theme.spacingS
|
||||
spacing: Theme.spacingS
|
||||
|
||||
DankIcon {
|
||||
name: sortBy === modelData?.value ? "check" : ""
|
||||
size: Theme.iconSizeSmall
|
||||
color: Theme.primary
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
visible: sortBy === modelData?.value
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: modelData?.name ?? ""
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: sortBy === modelData?.value ? Theme.primary : Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: sortMouseArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
sortMenu.sortBySelected(modelData?.value ?? "name");
|
||||
sortMenu.visible = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StyledRect {
|
||||
width: sortColumn.width
|
||||
height: 1
|
||||
color: Theme.outline
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: "Order"
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceTextMedium
|
||||
font.weight: Font.Medium
|
||||
topPadding: Theme.spacingXS
|
||||
}
|
||||
|
||||
StyledRect {
|
||||
width: sortColumn?.width ?? 0
|
||||
height: 32
|
||||
radius: Theme.cornerRadius
|
||||
color: ascMouseArea.containsMouse ? Theme.surfaceVariant : (sortAscending ? Theme.surfacePressed : Theme.withAlpha(Theme.surfacePressed, 0))
|
||||
|
||||
Row {
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: Theme.spacingS
|
||||
spacing: Theme.spacingS
|
||||
|
||||
DankIcon {
|
||||
name: "arrow_upward"
|
||||
size: Theme.iconSizeSmall
|
||||
color: sortAscending ? Theme.primary : Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: "Ascending"
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: sortAscending ? Theme.primary : Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: ascMouseArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
sortMenu.sortOrderSelected(true);
|
||||
sortMenu.visible = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StyledRect {
|
||||
width: sortColumn?.width ?? 0
|
||||
height: 32
|
||||
radius: Theme.cornerRadius
|
||||
color: descMouseArea.containsMouse ? Theme.surfaceVariant : (!sortAscending ? Theme.surfacePressed : Theme.withAlpha(Theme.surfacePressed, 0))
|
||||
|
||||
Row {
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: Theme.spacingS
|
||||
spacing: Theme.spacingS
|
||||
|
||||
DankIcon {
|
||||
name: "arrow_downward"
|
||||
size: Theme.iconSizeSmall
|
||||
color: !sortAscending ? Theme.primary : Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: "Descending"
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: !sortAscending ? Theme.primary : Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: descMouseArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
sortMenu.sortOrderSelected(false);
|
||||
sortMenu.visible = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
DankCommon.FileBrowserSortMenu {}
|
||||
|
||||
@@ -1,236 +1,3 @@
|
||||
import QtQuick
|
||||
import Quickshell.Io
|
||||
import qs.Common
|
||||
import qs.Widgets
|
||||
import qs.DankCommon.Modals.FileBrowser as DankCommon
|
||||
|
||||
Rectangle {
|
||||
id: root
|
||||
|
||||
property bool showFileInfo: false
|
||||
property int selectedIndex: -1
|
||||
property var sourceFolderModel: null
|
||||
property string currentPath: ""
|
||||
|
||||
height: 200
|
||||
radius: Theme.cornerRadius
|
||||
color: Theme.withAlpha(Theme.surfaceContainer, 0.95)
|
||||
border.color: Theme.secondary
|
||||
border.width: 2
|
||||
opacity: showFileInfo ? 1 : 0
|
||||
z: 100
|
||||
|
||||
onShowFileInfoChanged: {
|
||||
if (showFileInfo && currentFileName && currentPath) {
|
||||
const fullPath = currentPath + "/" + currentFileName;
|
||||
fileStatProcess.selectedFilePath = fullPath;
|
||||
fileStatProcess.running = true;
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: fileStatProcess
|
||||
command: ["stat", "-c", "%y|%A|%s|%n", selectedFilePath]
|
||||
property string selectedFilePath: ""
|
||||
property var fileStats: null
|
||||
running: false
|
||||
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
if (text && text.trim()) {
|
||||
const parts = text.trim().split('|');
|
||||
if (parts.length >= 4) {
|
||||
fileStatProcess.fileStats = {
|
||||
"modifiedTime": parts[0],
|
||||
"permissions": parts[1],
|
||||
"size": parseInt(parts[2]) || 0,
|
||||
"fullPath": parts[3]
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onExited: function (exitCode) {}
|
||||
}
|
||||
|
||||
property string currentFileName: ""
|
||||
property bool currentFileIsDir: false
|
||||
property string currentFileExtension: ""
|
||||
|
||||
onCurrentFileNameChanged: {
|
||||
if (showFileInfo && currentFileName && currentPath) {
|
||||
const fullPath = currentPath + "/" + currentFileName;
|
||||
if (fullPath !== fileStatProcess.selectedFilePath) {
|
||||
fileStatProcess.selectedFilePath = fullPath;
|
||||
fileStatProcess.running = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function updateFileInfo(filePath, fileName, isDirectory) {
|
||||
if (filePath && filePath !== fileStatProcess.selectedFilePath) {
|
||||
fileStatProcess.selectedFilePath = filePath;
|
||||
currentFileName = fileName || "";
|
||||
currentFileIsDir = isDirectory || false;
|
||||
|
||||
let ext = "";
|
||||
if (!isDirectory && fileName) {
|
||||
const lastDot = fileName.lastIndexOf('.');
|
||||
if (lastDot > 0) {
|
||||
ext = fileName.substring(lastDot + 1).toLowerCase();
|
||||
}
|
||||
}
|
||||
currentFileExtension = ext;
|
||||
|
||||
if (showFileInfo) {
|
||||
fileStatProcess.running = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
readonly property var currentFileDisplayData: {
|
||||
if (selectedIndex < 0 || !sourceFolderModel) {
|
||||
return {
|
||||
"exists": false,
|
||||
"name": "No selection",
|
||||
"type": "",
|
||||
"size": "",
|
||||
"modified": "",
|
||||
"permissions": "",
|
||||
"extension": "",
|
||||
"position": "N/A"
|
||||
};
|
||||
}
|
||||
|
||||
const hasValidFile = currentFileName !== "";
|
||||
return {
|
||||
"exists": hasValidFile,
|
||||
"name": hasValidFile ? currentFileName : "Loading...",
|
||||
"type": currentFileIsDir ? "Directory" : "File",
|
||||
"size": fileStatProcess.fileStats ? formatFileSize(fileStatProcess.fileStats.size) : "Calculating...",
|
||||
"modified": fileStatProcess.fileStats ? formatDateTime(fileStatProcess.fileStats.modifiedTime) : "Loading...",
|
||||
"permissions": fileStatProcess.fileStats ? fileStatProcess.fileStats.permissions : "Loading...",
|
||||
"extension": currentFileExtension,
|
||||
"position": sourceFolderModel ? ((selectedIndex + 1) + " of " + sourceFolderModel.count) : "N/A"
|
||||
};
|
||||
}
|
||||
|
||||
Column {
|
||||
anchors.top: parent.top
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.margins: Theme.spacingM
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
Row {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingS
|
||||
|
||||
DankIcon {
|
||||
name: "info"
|
||||
size: Theme.iconSize
|
||||
color: Theme.secondary
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("File Information")
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
StyledText {
|
||||
text: currentFileDisplayData.name
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceText
|
||||
width: parent.width
|
||||
elide: Text.ElideMiddle
|
||||
wrapMode: Text.NoWrap
|
||||
font.weight: Font.Medium
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: currentFileDisplayData.type + (currentFileDisplayData.extension ? " (." + currentFileDisplayData.extension + ")" : "")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceTextMedium
|
||||
width: parent.width
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: currentFileDisplayData.size
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceTextMedium
|
||||
width: parent.width
|
||||
visible: currentFileDisplayData.exists && !currentFileIsDir
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: currentFileDisplayData.modified
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceTextMedium
|
||||
width: parent.width
|
||||
elide: Text.ElideRight
|
||||
visible: currentFileDisplayData.exists
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: currentFileDisplayData.permissions
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceTextMedium
|
||||
visible: currentFileDisplayData.exists
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: currentFileDisplayData.position
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceTextMedium
|
||||
width: parent.width
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("F1/I: Toggle • F10: Help")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceTextMedium
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.margins: Theme.spacingM
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
}
|
||||
|
||||
function formatFileSize(bytes) {
|
||||
if (bytes === 0 || !bytes) {
|
||||
return "0 B";
|
||||
}
|
||||
const k = 1024;
|
||||
const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i];
|
||||
}
|
||||
|
||||
function formatDateTime(dateTimeString) {
|
||||
if (!dateTimeString) {
|
||||
return "Unknown";
|
||||
}
|
||||
const parts = dateTimeString.split(' ');
|
||||
if (parts.length >= 2) {
|
||||
return parts[0] + " " + parts[1].split('.')[0];
|
||||
}
|
||||
return dateTimeString;
|
||||
}
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Theme.standardEasing
|
||||
}
|
||||
}
|
||||
}
|
||||
DankCommon.FileInfo {}
|
||||
|
||||
@@ -1,50 +1,3 @@
|
||||
import QtQuick
|
||||
import qs.Common
|
||||
import qs.Widgets
|
||||
import qs.DankCommon.Modals.FileBrowser as DankCommon
|
||||
|
||||
Rectangle {
|
||||
id: root
|
||||
|
||||
property bool showHints: false
|
||||
|
||||
height: 80
|
||||
radius: Theme.cornerRadius
|
||||
color: Theme.withAlpha(Theme.surfaceContainer, 0.95)
|
||||
border.color: Theme.primary
|
||||
border.width: 2
|
||||
opacity: showHints ? 1 : 0
|
||||
z: 100
|
||||
|
||||
Column {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.margins: Theme.spacingS
|
||||
spacing: Theme.spacingXXS
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Tab/Shift+Tab: Nav • ←→↑↓: Grid Nav • Enter/Space: Select")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceText
|
||||
width: parent.width
|
||||
wrapMode: Text.WordWrap
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Alt+←/Backspace: Back • F1/I: File Info • F10: Help • Esc: Close")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceText
|
||||
width: parent.width
|
||||
wrapMode: Text.WordWrap
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Theme.standardEasing
|
||||
}
|
||||
}
|
||||
}
|
||||
DankCommon.KeyboardHints {}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import QtQuick
|
||||
import QtQuick.Effects
|
||||
import qs.Common
|
||||
import qs.DankCommon.Common as DankCommon
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
@@ -176,14 +177,9 @@ Item {
|
||||
text: "DANK LINUX"
|
||||
font.pixelSize: parent.compactLogo ? 32 : 48
|
||||
font.weight: Font.Bold
|
||||
font.family: interFont.name
|
||||
font.family: DankCommon.Fonts.sans
|
||||
color: Theme.surfaceText
|
||||
antialiasing: true
|
||||
|
||||
FontLoader {
|
||||
id: interFont
|
||||
source: Qt.resolvedUrl("../../assets/fonts/inter/InterVariable.ttf")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -35,6 +35,8 @@ Plugins let you add custom widgets to DankBar and Control Center. They're discov
|
||||
- Supports left, center, and right sections
|
||||
- Supports any dankbar position (top/left/right/bottom)
|
||||
|
||||
Many widgets are implemented in the shared [dank-qml-common](https://github.com/AvengeMedia/dank-qml-common) library and re-exported by DMS. Plugins should keep importing `qs.Common`, `qs.Services`, `qs.Widgets`, and `qs.Modules.Plugins` — these remain the supported plugin API and are unaffected by where a widget is implemented.
|
||||
|
||||
## Plugin Structure
|
||||
|
||||
Each plugin must be a directory in `$CONFIGPATH/DankMaterialShell/plugins/` containing:
|
||||
|
||||
@@ -1,54 +0,0 @@
|
||||
#version 450
|
||||
|
||||
// Standalone rounded rect with border and M3 elevation shadow as one SDF.
|
||||
|
||||
layout(location = 0) in vec2 qt_TexCoord0;
|
||||
layout(location = 0) out vec4 fragColor;
|
||||
|
||||
layout(std140, binding = 0) uniform buf {
|
||||
mat4 qt_Matrix;
|
||||
float qt_Opacity;
|
||||
float widthPx;
|
||||
float heightPx;
|
||||
float borderWidth;
|
||||
vec4 rectPx; // rounded rect in item px: x, y, w, h
|
||||
vec4 cornerRadius; // topLeft, topRight, bottomRight, bottomLeft
|
||||
vec4 fillColor; // straight (non-premultiplied) rgba
|
||||
vec4 borderColor; // straight rgba
|
||||
vec4 shadowColor; // straight rgba; a = 0 disables both shadow terms
|
||||
vec4 shadowParam; // key: x = blur px, y = spread px, z,w = offset px
|
||||
vec4 ambientParam; // ambient: x = blur px, y = spread px, z = alpha
|
||||
} ubuf;
|
||||
|
||||
float sdRoundBox4(vec2 p, vec2 c, vec2 hs, vec4 r) {
|
||||
p -= c;
|
||||
float rr = (p.x >= 0.0) ? (p.y >= 0.0 ? r.z : r.y) : (p.y >= 0.0 ? r.w : r.x);
|
||||
rr = min(rr, min(hs.x, hs.y));
|
||||
vec2 q = abs(p) - hs + rr;
|
||||
return min(max(q.x, q.y), 0.0) + length(max(q, vec2(0.0))) - rr;
|
||||
}
|
||||
|
||||
float rectDist(vec2 px) {
|
||||
vec2 hs = ubuf.rectPx.zw * 0.5;
|
||||
return sdRoundBox4(px, ubuf.rectPx.xy + hs, hs, ubuf.cornerRadius);
|
||||
}
|
||||
|
||||
void main() {
|
||||
vec2 px = qt_TexCoord0 * vec2(ubuf.widthPx, ubuf.heightPx);
|
||||
float d = rectDist(px);
|
||||
float fw = max(fwidth(d), 1e-4);
|
||||
float cov = 1.0 - smoothstep(-fw, fw, d);
|
||||
float covInner = 1.0 - smoothstep(-fw, fw, d + ubuf.borderWidth);
|
||||
vec4 col = vec4(ubuf.fillColor.rgb, 1.0) * (ubuf.fillColor.a * covInner)
|
||||
+ vec4(ubuf.borderColor.rgb, 1.0) * (ubuf.borderColor.a * max(0.0, cov - covInner));
|
||||
if (ubuf.shadowColor.a > 0.0) {
|
||||
float dk = rectDist(px - ubuf.shadowParam.zw) - ubuf.shadowParam.y;
|
||||
float bk = max(ubuf.shadowParam.x, fw);
|
||||
float covK = 1.0 - smoothstep(-bk, bk, dk);
|
||||
float ba = max(ubuf.ambientParam.x, fw);
|
||||
float covA = 1.0 - smoothstep(-ba, ba, d - ubuf.ambientParam.y);
|
||||
float sh = 1.0 - (1.0 - covK * ubuf.shadowColor.a) * (1.0 - covA * ubuf.ambientParam.z);
|
||||
col += vec4(ubuf.shadowColor.rgb, 1.0) * (sh * (1.0 - cov));
|
||||
}
|
||||
fragColor = col * ubuf.qt_Opacity;
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
#version 450
|
||||
|
||||
layout(location = 0) in vec2 qt_TexCoord0;
|
||||
layout(location = 0) out vec4 fragColor;
|
||||
|
||||
layout(std140, binding = 0) uniform buf {
|
||||
mat4 qt_Matrix;
|
||||
float qt_Opacity;
|
||||
float widthPx;
|
||||
float heightPx;
|
||||
float cornerRadiusPx;
|
||||
float rippleCenterX;
|
||||
float rippleCenterY;
|
||||
float rippleRadius;
|
||||
float rippleOpacity;
|
||||
float offsetX;
|
||||
float offsetY;
|
||||
float parentWidth;
|
||||
float parentHeight;
|
||||
vec4 rippleCol;
|
||||
} ubuf;
|
||||
|
||||
float sdRoundRect(vec2 p, vec2 b, float r) {
|
||||
vec2 q = abs(p) - (b - vec2(r));
|
||||
return length(max(q, 0.0)) + min(max(q.x, q.y), 0.0) - r;
|
||||
}
|
||||
|
||||
void main() {
|
||||
if (ubuf.rippleOpacity <= 0.0 || ubuf.rippleRadius <= 0.0) {
|
||||
fragColor = vec4(0.0);
|
||||
return;
|
||||
}
|
||||
|
||||
vec2 px = qt_TexCoord0 * vec2(ubuf.widthPx, ubuf.heightPx) + vec2(ubuf.offsetX, ubuf.offsetY);
|
||||
|
||||
float circleD = length(px - vec2(ubuf.rippleCenterX, ubuf.rippleCenterY)) - ubuf.rippleRadius;
|
||||
float aaCircle = max(fwidth(circleD), 0.001);
|
||||
float circleMask = 1.0 - smoothstep(-aaCircle, aaCircle, circleD);
|
||||
if (circleMask <= 0.0) {
|
||||
fragColor = vec4(0.0);
|
||||
return;
|
||||
}
|
||||
|
||||
float rrMask = 1.0;
|
||||
if (ubuf.cornerRadiusPx > 0.0) {
|
||||
vec2 halfSize = vec2(ubuf.parentWidth, ubuf.parentHeight) * 0.5;
|
||||
float r = clamp(ubuf.cornerRadiusPx, 0.0, min(halfSize.x, halfSize.y));
|
||||
float rrD = sdRoundRect(px - halfSize, halfSize, r);
|
||||
float aaRR = max(fwidth(rrD), 0.001);
|
||||
rrMask = 1.0 - smoothstep(-aaRR, aaRR, rrD);
|
||||
}
|
||||
|
||||
float mask = circleMask * rrMask;
|
||||
if (mask <= 0.0) {
|
||||
fragColor = vec4(0.0);
|
||||
return;
|
||||
}
|
||||
|
||||
float a = ubuf.rippleCol.a * ubuf.rippleOpacity * mask * ubuf.qt_Opacity;
|
||||
fragColor = vec4(ubuf.rippleCol.rgb * a, a);
|
||||
}
|
||||
@@ -1,115 +0,0 @@
|
||||
#version 450
|
||||
|
||||
layout(location = 0) in vec2 qt_TexCoord0;
|
||||
layout(location = 0) out vec4 fragColor;
|
||||
|
||||
layout(std140, binding = 0) uniform buf {
|
||||
mat4 qt_Matrix;
|
||||
float qt_Opacity;
|
||||
float widthPx;
|
||||
float heightPx;
|
||||
float value;
|
||||
float actualValue;
|
||||
float phase;
|
||||
float ampPx;
|
||||
float wavelengthPx;
|
||||
float lineWidthPx;
|
||||
float showActual;
|
||||
vec4 fillColor;
|
||||
vec4 trackColor;
|
||||
vec4 playheadColor;
|
||||
vec4 actualColor;
|
||||
} ubuf;
|
||||
|
||||
const float TAU = 6.28318530718;
|
||||
const float AA = 0.75; // pixel-space antialias band
|
||||
|
||||
// Signed distance to a rounded box centered at the origin.
|
||||
float sdRoundBar(vec2 p, vec2 halfSize, float r) {
|
||||
vec2 q = abs(p) - halfSize + vec2(r);
|
||||
return length(max(q, 0.0)) + min(max(q.x, q.y), 0.0) - r;
|
||||
}
|
||||
|
||||
// Composite a straight-alpha color over a premultiplied accumulator.
|
||||
vec4 blendOver(vec4 dst, vec3 rgb, float a) {
|
||||
return vec4(rgb * a + dst.rgb * (1.0 - a), a + dst.a * (1.0 - a));
|
||||
}
|
||||
|
||||
void main() {
|
||||
float w = ubuf.widthPx;
|
||||
float h = ubuf.heightPx;
|
||||
vec2 px = vec2(qt_TexCoord0.x * w, qt_TexCoord0.y * h);
|
||||
|
||||
float mid = h * 0.5;
|
||||
float halfW = ubuf.lineWidthPx * 0.5;
|
||||
float k = TAU / max(ubuf.wavelengthPx, 1e-3);
|
||||
|
||||
float playX = clamp(ubuf.value, 0.0, 1.0) * w;
|
||||
float actualX = clamp(ubuf.actualValue, 0.0, 1.0) * w;
|
||||
bool seeking = ubuf.showActual > 0.5;
|
||||
|
||||
float loX = min(playX, actualX);
|
||||
float hiX = max(playX, actualX);
|
||||
float fillEnd = seeking ? loX : playX; // filled progress ends here
|
||||
float actStart = seeking ? loX : playX; // seek-preview segment
|
||||
float actEnd = seeking ? hiX : playX;
|
||||
float trackStart = seeking ? hiX : playX; // unplayed remainder
|
||||
|
||||
// Perpendicular distance to the animated sine stroke.
|
||||
float ang = k * px.x + ubuf.phase;
|
||||
float wy = mid + ubuf.ampPx * sin(ang);
|
||||
float slope = ubuf.ampPx * k * cos(ang);
|
||||
float dWave = abs(px.y - wy) / sqrt(1.0 + slope * slope);
|
||||
float aaW = AA;
|
||||
float waveStroke = 1.0 - smoothstep(halfW - aaW, halfW + aaW, dWave);
|
||||
|
||||
// Straight remainder line.
|
||||
float dLine = abs(px.y - mid);
|
||||
float aaL = AA;
|
||||
float lineStroke = 1.0 - smoothstep(halfW - aaL, halfW + aaL, dLine);
|
||||
|
||||
vec4 col = vec4(0.0);
|
||||
|
||||
// 1. Track (unplayed remainder), to the right of the progress head.
|
||||
{
|
||||
float m = lineStroke * step(trackStart, px.x);
|
||||
col = blendOver(col, ubuf.trackColor.rgb, ubuf.trackColor.a * m);
|
||||
}
|
||||
|
||||
// 2. Seek-preview segment (only while seeking).
|
||||
if (seeking) {
|
||||
float m = waveStroke * step(actStart, px.x) * step(px.x, actEnd);
|
||||
col = blendOver(col, ubuf.actualColor.rgb, ubuf.actualColor.a * m);
|
||||
}
|
||||
|
||||
// 3. Filled progress wave.
|
||||
{
|
||||
float m = waveStroke * step(halfW, px.x) * step(px.x, fillEnd);
|
||||
// Rounded start cap.
|
||||
float capS = length(px - vec2(halfW, mid + ubuf.ampPx * sin(k * halfW + ubuf.phase))) - halfW;
|
||||
float capM = 1.0 - smoothstep(-aaW, aaW, capS);
|
||||
m = max(m, capM * step(halfW - 1.0, px.x));
|
||||
col = blendOver(col, ubuf.fillColor.rgb, ubuf.fillColor.a * m);
|
||||
}
|
||||
|
||||
// 4. Actual-position marker (only while seeking).
|
||||
if (seeking) {
|
||||
float amH = max(ubuf.lineWidthPx + 4.0, 10.0);
|
||||
float d = sdRoundBar(px - vec2(actualX, mid), vec2(1.0, amH * 0.5), 1.0);
|
||||
float aa = AA;
|
||||
float m = 1.0 - smoothstep(-aa, aa, d);
|
||||
col = blendOver(col, ubuf.actualColor.rgb, ubuf.actualColor.a * m);
|
||||
}
|
||||
|
||||
// 5. Playhead pill (on top).
|
||||
{
|
||||
float phW = 3.5;
|
||||
float phH = max(ubuf.lineWidthPx + 12.0, 16.0);
|
||||
float d = sdRoundBar(px - vec2(playX, mid), vec2(phW * 0.5, phH * 0.5), phW * 0.5);
|
||||
float aa = AA;
|
||||
float m = 1.0 - smoothstep(-aa, aa, d);
|
||||
col = blendOver(col, ubuf.playheadColor.rgb, ubuf.playheadColor.a * m);
|
||||
}
|
||||
|
||||
fragColor = col * ubuf.qt_Opacity;
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,129 +1,3 @@
|
||||
import QtQuick
|
||||
import Quickshell.Widgets
|
||||
import qs.Common
|
||||
import qs.DankCommon.Widgets as DankCommon
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
required property string iconValue
|
||||
required property int iconSize
|
||||
property string fallbackText: "A"
|
||||
property color iconColor: Theme.surfaceText
|
||||
property color colorOverride: "transparent"
|
||||
property real brightnessOverride: 0.0
|
||||
property real contrastOverride: 0.0
|
||||
property real saturationOverride: 0.0
|
||||
property color fallbackBackgroundColor: Theme.surfaceLight
|
||||
property color fallbackTextColor: Theme.primary
|
||||
property real materialIconSizeAdjustment: Theme.spacingM
|
||||
property real unicodeIconScale: 0.7
|
||||
property real fallbackTextScale: 0.4
|
||||
property real iconMargins: 0
|
||||
property real fallbackLeftMargin: 0
|
||||
property real fallbackRightMargin: 0
|
||||
property real fallbackTopMargin: 0
|
||||
property real fallbackBottomMargin: 0
|
||||
|
||||
readonly property bool isMaterial: iconValue && iconValue.startsWith("material:")
|
||||
readonly property bool isUnicode: iconValue && iconValue.startsWith("unicode:")
|
||||
readonly property bool isSvgCorner: iconValue && iconValue.startsWith("svg+corner:")
|
||||
readonly property bool isSvg: iconValue && !isSvgCorner && iconValue.startsWith("svg:")
|
||||
readonly property bool isImage: iconValue && iconValue.startsWith("image:")
|
||||
readonly property bool hasColorOverride: colorOverride.a > 0
|
||||
readonly property string materialName: isMaterial ? iconValue.substring(9) : ""
|
||||
readonly property string unicodeChar: isUnicode ? iconValue.substring(8) : ""
|
||||
readonly property string imagePath: isImage ? iconValue.substring(6) : ""
|
||||
readonly property string svgSource: {
|
||||
if (isSvgCorner) {
|
||||
const parts = iconValue.substring(11).split("|");
|
||||
return parts[0] || "";
|
||||
}
|
||||
if (isSvg)
|
||||
return iconValue.substring(4);
|
||||
return "";
|
||||
}
|
||||
readonly property string svgCornerIcon: isSvgCorner ? (iconValue.substring(11).split("|")[1] || "") : ""
|
||||
readonly property bool hasSpecialPrefix: isMaterial || isUnicode || isSvg || isSvgCorner || isImage
|
||||
readonly property string iconPath: {
|
||||
if (hasSpecialPrefix || !iconValue)
|
||||
return "";
|
||||
return Paths.resolveIconPath(iconValue);
|
||||
}
|
||||
|
||||
visible: iconValue !== undefined && iconValue !== ""
|
||||
|
||||
DankIcon {
|
||||
anchors.centerIn: parent
|
||||
name: root.materialName
|
||||
size: root.iconSize - root.materialIconSizeAdjustment
|
||||
color: root.hasColorOverride ? root.colorOverride : root.iconColor
|
||||
visible: root.isMaterial
|
||||
}
|
||||
|
||||
StyledText {
|
||||
anchors.centerIn: parent
|
||||
text: root.unicodeChar
|
||||
font.pixelSize: root.iconSize * root.unicodeIconScale
|
||||
color: root.hasColorOverride ? root.colorOverride : root.iconColor
|
||||
visible: root.isUnicode
|
||||
}
|
||||
|
||||
DankSVGIcon {
|
||||
anchors.centerIn: parent
|
||||
source: root.svgSource
|
||||
size: root.iconSize
|
||||
cornerIcon: root.svgCornerIcon
|
||||
colorOverride: root.colorOverride
|
||||
brightnessOverride: root.brightnessOverride
|
||||
contrastOverride: root.contrastOverride
|
||||
saturationOverride: root.saturationOverride
|
||||
visible: root.isSvg || root.isSvgCorner
|
||||
}
|
||||
|
||||
CachingImage {
|
||||
id: cachingImg
|
||||
anchors.fill: parent
|
||||
imagePath: root.imagePath
|
||||
maxCacheSize: root.iconSize * 2
|
||||
animate: false
|
||||
visible: root.isImage && status === Image.Ready
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: iconImgLoader
|
||||
anchors.fill: parent
|
||||
anchors.margins: root.iconMargins
|
||||
active: !root.hasSpecialPrefix && root.iconPath !== ""
|
||||
sourceComponent: IconImage {
|
||||
anchors.fill: parent
|
||||
source: root.iconPath
|
||||
backer.sourceSize: Qt.size(root.iconSize * 2, root.iconSize * 2)
|
||||
mipmap: true
|
||||
asynchronous: true
|
||||
visible: status === Image.Ready
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: fallbackRect
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: root.fallbackLeftMargin
|
||||
anchors.rightMargin: root.fallbackRightMargin
|
||||
anchors.topMargin: root.fallbackTopMargin
|
||||
anchors.bottomMargin: root.fallbackBottomMargin
|
||||
visible: !root.hasSpecialPrefix && (root.iconPath === "" || !iconImgLoader.item || iconImgLoader.item.status !== Image.Ready)
|
||||
color: root.fallbackBackgroundColor
|
||||
radius: Theme.cornerRadius
|
||||
border.width: 0
|
||||
border.color: Theme.primarySelected
|
||||
|
||||
StyledText {
|
||||
anchors.centerIn: parent
|
||||
text: root.fallbackText
|
||||
font.pixelSize: root.iconSize * root.fallbackTextScale
|
||||
color: root.fallbackTextColor
|
||||
font.weight: Font.Bold
|
||||
}
|
||||
}
|
||||
}
|
||||
DankCommon.AppIconRenderer {}
|
||||
|
||||
@@ -1,132 +1,3 @@
|
||||
import QtQuick
|
||||
import qs.Common
|
||||
import qs.DankCommon.Widgets as DankCommon
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property string imagePath: ""
|
||||
property int maxCacheSize: 512
|
||||
property int status: isAnimated ? animatedImg.status : staticImg.status
|
||||
property int fillMode: Image.PreserveAspectCrop
|
||||
// AnimatedImage decodes full-size on the GUI thread and is never cached;
|
||||
// disable for thumbnail grids
|
||||
property bool animate: true
|
||||
property bool _fromCache: false
|
||||
|
||||
readonly property bool isRemoteUrl: imagePath.startsWith("http://") || imagePath.startsWith("https://")
|
||||
readonly property bool isAnimated: {
|
||||
if (!animate || !imagePath)
|
||||
return false;
|
||||
const lower = imagePath.toLowerCase();
|
||||
return lower.endsWith(".gif") || lower.endsWith(".webp");
|
||||
}
|
||||
readonly property string normalizedPath: {
|
||||
if (!imagePath)
|
||||
return "";
|
||||
if (isRemoteUrl)
|
||||
return imagePath;
|
||||
if (imagePath.startsWith("file://"))
|
||||
return imagePath.substring(7);
|
||||
return imagePath;
|
||||
}
|
||||
|
||||
function djb2Hash(str) {
|
||||
if (!str)
|
||||
return "";
|
||||
let hash = 5381;
|
||||
for (let i = 0; i < str.length; i++) {
|
||||
hash = ((hash << 5) + hash) + str.charCodeAt(i);
|
||||
hash = hash & 0x7FFFFFFF;
|
||||
}
|
||||
return hash.toString(16).padStart(8, '0');
|
||||
}
|
||||
|
||||
readonly property string imageHash: normalizedPath ? djb2Hash(normalizedPath) : ""
|
||||
readonly property string cacheFileName: imageHash && !isRemoteUrl && !isAnimated ? `${imageHash}@${maxCacheSize}x${maxCacheSize}.png` : ""
|
||||
readonly property string cachePath: cacheFileName ? `${Paths.stringify(Paths.imagecache)}/${cacheFileName}` : ""
|
||||
readonly property string encodedImagePath: {
|
||||
if (!normalizedPath)
|
||||
return "";
|
||||
if (isRemoteUrl)
|
||||
return normalizedPath;
|
||||
return "file://" + normalizedPath.split('/').map(s => encodeURIComponent(s)).join('/');
|
||||
}
|
||||
|
||||
AnimatedImage {
|
||||
id: animatedImg
|
||||
anchors.fill: parent
|
||||
visible: root.isAnimated
|
||||
asynchronous: true
|
||||
fillMode: root.fillMode
|
||||
source: root.isAnimated ? root.imagePath : ""
|
||||
playing: visible && status === AnimatedImage.Ready
|
||||
}
|
||||
|
||||
Image {
|
||||
id: staticImg
|
||||
anchors.fill: parent
|
||||
visible: !root.isAnimated
|
||||
asynchronous: true
|
||||
fillMode: root.fillMode
|
||||
sourceSize.width: root.maxCacheSize
|
||||
sourceSize.height: root.maxCacheSize
|
||||
smooth: true
|
||||
|
||||
onStatusChanged: {
|
||||
switch (status) {
|
||||
case Image.Error:
|
||||
if (!root._fromCache)
|
||||
return;
|
||||
root._fromCache = false;
|
||||
source = root.encodedImagePath;
|
||||
return;
|
||||
case Image.Ready:
|
||||
if (root._fromCache || root.isRemoteUrl || !root.cachePath)
|
||||
return;
|
||||
if (!visible || width <= 0 || height <= 0 || !Window.window?.visible)
|
||||
return;
|
||||
const grabPath = root.cachePath;
|
||||
grabToImage(res => {
|
||||
res.saveToFile(grabPath);
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Derives everything from a local snapshot of imagePath: sibling property
|
||||
// bindings (isRemoteUrl, encodedImagePath, ...) are still stale when
|
||||
// onImagePathChanged runs, so reading them here routes remote URLs down
|
||||
// the local-file branch on the first path change
|
||||
function resolveSource() {
|
||||
const path = imagePath;
|
||||
if (!path) {
|
||||
_fromCache = false;
|
||||
staticImg.source = "";
|
||||
return;
|
||||
}
|
||||
const lower = path.toLowerCase();
|
||||
if (animate && (lower.endsWith(".gif") || lower.endsWith(".webp")))
|
||||
return;
|
||||
if (path.startsWith("http://") || path.startsWith("https://")) {
|
||||
_fromCache = false;
|
||||
staticImg.source = path;
|
||||
return;
|
||||
}
|
||||
const stripped = path.startsWith("file://") ? path.substring(7) : path;
|
||||
const encoded = "file://" + stripped.split('/').map(s => encodeURIComponent(s)).join('/');
|
||||
const hash = djb2Hash(stripped);
|
||||
if (!hash) {
|
||||
_fromCache = false;
|
||||
staticImg.source = encoded;
|
||||
return;
|
||||
}
|
||||
// Cache-first; a miss errors and falls back to encodedImagePath
|
||||
_fromCache = true;
|
||||
staticImg.source = `${Paths.stringify(Paths.imagecache)}/${hash}@${maxCacheSize}x${maxCacheSize}.png`;
|
||||
}
|
||||
|
||||
onImagePathChanged: resolveSource()
|
||||
// During creation onImagePathChanged fires before sibling properties (maxCacheSize) initialize
|
||||
onCachePathChanged: resolveSource()
|
||||
}
|
||||
DankCommon.CachingImage {}
|
||||
|
||||
@@ -1,45 +1,3 @@
|
||||
import QtQuick
|
||||
import qs.Common
|
||||
import qs.Widgets
|
||||
import qs.DankCommon.Widgets as DankCommon
|
||||
|
||||
StyledRect {
|
||||
id: root
|
||||
|
||||
property string iconName: ""
|
||||
property int iconSize: Theme.iconSize - 4
|
||||
property color iconColor: Theme.surfaceText
|
||||
property color backgroundColor: "transparent"
|
||||
property bool circular: true
|
||||
property int buttonSize: 32
|
||||
property var tooltipText: null
|
||||
property string tooltipSide: "bottom"
|
||||
readonly property alias pressed: stateLayer.pressed
|
||||
|
||||
signal clicked
|
||||
signal entered
|
||||
signal exited
|
||||
|
||||
width: buttonSize
|
||||
height: buttonSize
|
||||
radius: Theme.cornerRadius
|
||||
color: backgroundColor
|
||||
|
||||
DankIcon {
|
||||
anchors.centerIn: parent
|
||||
name: root.iconName
|
||||
size: root.iconSize
|
||||
color: root.iconColor
|
||||
}
|
||||
|
||||
StateLayer {
|
||||
id: stateLayer
|
||||
disabled: !root.enabled
|
||||
stateColor: Theme.primary
|
||||
cornerRadius: root.radius
|
||||
onClicked: root.clicked()
|
||||
onEntered: root.entered()
|
||||
onExited: root.exited()
|
||||
tooltipText: root.tooltipText
|
||||
tooltipSide: root.tooltipSide
|
||||
}
|
||||
}
|
||||
DankCommon.DankActionButton {}
|
||||
|
||||
@@ -1,28 +1,3 @@
|
||||
import QtQuick
|
||||
import qs.DankCommon.Widgets as DankCommon
|
||||
|
||||
SequentialAnimation {
|
||||
id: root
|
||||
|
||||
property Item target
|
||||
property real minOpacity: 0.3
|
||||
property int pulseDuration: 600
|
||||
|
||||
loops: Animation.Infinite
|
||||
|
||||
NumberAnimation {
|
||||
target: root.target
|
||||
property: "opacity"
|
||||
to: root.minOpacity
|
||||
duration: root.pulseDuration
|
||||
easing.type: Easing.InOutQuad
|
||||
}
|
||||
NumberAnimation {
|
||||
target: root.target
|
||||
property: "opacity"
|
||||
to: 1.0
|
||||
duration: root.pulseDuration
|
||||
easing.type: Easing.InOutQuad
|
||||
}
|
||||
|
||||
onStopped: if (root.target) root.target.opacity = 1.0
|
||||
}
|
||||
DankCommon.DankBlink {}
|
||||
|
||||
@@ -1,99 +1,3 @@
|
||||
import QtQuick
|
||||
import qs.Common
|
||||
import qs.Widgets
|
||||
import qs.DankCommon.Widgets as DankCommon
|
||||
|
||||
Rectangle {
|
||||
id: root
|
||||
|
||||
property string text: ""
|
||||
property string iconName: ""
|
||||
property int iconSize: Theme.iconSizeSmall
|
||||
property bool hovered: mouseArea.containsMouse
|
||||
property bool pressed: mouseArea.pressed
|
||||
property color backgroundColor: Theme.buttonBg
|
||||
property color textColor: Theme.buttonText
|
||||
property int buttonHeight: 40
|
||||
property int horizontalPadding: Theme.spacingL
|
||||
property bool enableScaleAnimation: false
|
||||
property bool enableRipple: typeof SettingsData !== "undefined" ? (SettingsData.enableRippleEffects ?? true) : true
|
||||
|
||||
signal clicked
|
||||
|
||||
width: Math.max(contentRow.implicitWidth + horizontalPadding * 2, 64)
|
||||
height: buttonHeight
|
||||
radius: Theme.cornerRadius
|
||||
color: backgroundColor
|
||||
opacity: enabled ? 1 : 0.4
|
||||
scale: (enableScaleAnimation && pressed) ? 0.98 : 1.0
|
||||
|
||||
Behavior on scale {
|
||||
enabled: enableScaleAnimation && Theme.currentAnimationSpeed !== SettingsData.AnimationSpeed.None
|
||||
NumberAnimation {
|
||||
easing.type: Easing.BezierSpline
|
||||
duration: 100
|
||||
easing.bezierCurve: Theme.expressiveCurves.standard
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: stateLayer
|
||||
anchors.fill: parent
|
||||
radius: parent.radius
|
||||
color: {
|
||||
if (pressed)
|
||||
return Theme.withAlpha(root.textColor, 0.20);
|
||||
if (hovered)
|
||||
return Theme.withAlpha(root.textColor, 0.12);
|
||||
return Theme.withAlpha(root.textColor, 0);
|
||||
}
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Theme.shorterDuration
|
||||
easing.type: Theme.standardEasing
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DankRipple {
|
||||
id: rippleLayer
|
||||
rippleColor: root.textColor
|
||||
cornerRadius: root.radius
|
||||
enableRipple: root.enableRipple
|
||||
}
|
||||
|
||||
Row {
|
||||
id: contentRow
|
||||
anchors.centerIn: parent
|
||||
spacing: Theme.spacingS
|
||||
|
||||
DankIcon {
|
||||
name: root.iconName
|
||||
size: root.iconSize
|
||||
color: root.textColor
|
||||
visible: root.iconName !== ""
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: root.text
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
font.weight: Font.Medium
|
||||
color: root.textColor
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: mouseArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: enabled ? Qt.PointingHandCursor : Qt.ArrowCursor
|
||||
enabled: root.enabled
|
||||
onPressed: mouse => {
|
||||
if (root.enableRipple)
|
||||
rippleLayer.trigger(mouse.x, mouse.y);
|
||||
}
|
||||
onClicked: root.clicked()
|
||||
}
|
||||
}
|
||||
DankCommon.DankButton {}
|
||||
|
||||
@@ -1,271 +1,3 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import qs.Common
|
||||
import qs.Widgets
|
||||
import qs.DankCommon.Widgets as DankCommon
|
||||
|
||||
Row {
|
||||
id: root
|
||||
|
||||
function checkParentDisablesTransparency() {
|
||||
let p = parent;
|
||||
while (p) {
|
||||
if (p.disablePopupTransparency === true)
|
||||
return true;
|
||||
p = p.parent;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
property var model: []
|
||||
property int currentIndex: -1
|
||||
property string selectionMode: "single"
|
||||
property bool multiSelect: selectionMode === "multi"
|
||||
property var initialSelection: []
|
||||
property var currentSelection: initialSelection
|
||||
property bool checkEnabled: true
|
||||
property string size: "medium"
|
||||
property int buttonHeight: size === "small" ? 32 : 40
|
||||
property int minButtonWidth: size === "small" ? 56 : 64
|
||||
property int buttonPadding: size === "small" ? Theme.spacingM : Theme.spacingL
|
||||
property int checkIconSize: size === "small" ? Theme.iconSizeSmall - 2 : Theme.iconSizeSmall
|
||||
property int textSize: size === "small" ? Theme.fontSizeSmall : Theme.fontSizeMedium
|
||||
property bool userInteracted: false
|
||||
property bool usePopupTransparency: !checkParentDisablesTransparency()
|
||||
property real maximumWidth: -1
|
||||
readonly property real _segmentCap: {
|
||||
const count = model?.length ?? 0;
|
||||
if (maximumWidth <= 0 || count === 0)
|
||||
return -1;
|
||||
return (maximumWidth - spacing * (count - 1)) / count - 4;
|
||||
}
|
||||
|
||||
signal selectionChanged(int index, bool selected)
|
||||
signal animationCompleted
|
||||
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
Timer {
|
||||
id: animationTimer
|
||||
interval: Theme.shortDuration
|
||||
onTriggered: {
|
||||
root.userInteracted = false;
|
||||
root.animationCompleted();
|
||||
}
|
||||
}
|
||||
|
||||
function isSelected(index) {
|
||||
if (multiSelect) {
|
||||
return repeater.itemAt(index)?.selected || false;
|
||||
}
|
||||
return index === currentIndex;
|
||||
}
|
||||
|
||||
function selectItem(index) {
|
||||
userInteracted = true;
|
||||
if (multiSelect) {
|
||||
const modelValue = model[index];
|
||||
let newSelection = [...currentSelection];
|
||||
const isCurrentlySelected = newSelection.includes(modelValue);
|
||||
|
||||
if (isCurrentlySelected) {
|
||||
newSelection = newSelection.filter(item => item !== modelValue);
|
||||
} else {
|
||||
newSelection.push(modelValue);
|
||||
}
|
||||
|
||||
currentSelection = newSelection;
|
||||
selectionChanged(index, !isCurrentlySelected);
|
||||
animationTimer.restart();
|
||||
} else {
|
||||
const oldIndex = currentIndex;
|
||||
selectionChanged(index, true);
|
||||
if (oldIndex !== index && oldIndex >= 0) {
|
||||
selectionChanged(oldIndex, false);
|
||||
}
|
||||
animationTimer.restart();
|
||||
}
|
||||
}
|
||||
|
||||
Repeater {
|
||||
id: repeater
|
||||
model: ScriptModel {
|
||||
values: root.model
|
||||
}
|
||||
|
||||
delegate: Rectangle {
|
||||
id: segment
|
||||
|
||||
property bool selected: multiSelect ? root.currentSelection.includes(modelData) : (index === root.currentIndex)
|
||||
property bool hovered: mouseArea.containsMouse
|
||||
property bool pressed: mouseArea.pressed
|
||||
property bool isFirst: index === 0
|
||||
property bool isLast: index === repeater.count - 1
|
||||
property bool visualFirst: I18n.isRtl ? isLast : isFirst
|
||||
property bool visualLast: I18n.isRtl ? isFirst : isLast
|
||||
property bool prevSelected: index > 0 ? root.isSelected(index - 1) : false
|
||||
property bool nextSelected: index < repeater.count - 1 ? root.isSelected(index + 1) : false
|
||||
|
||||
readonly property real contentNaturalWidth: (checkIcon.visible ? checkIcon.width + contentRow.spacing : 0) + buttonText.implicitWidth
|
||||
|
||||
width: {
|
||||
const natural = Math.max(contentNaturalWidth + root.buttonPadding * 2, root.minButtonWidth);
|
||||
const capped = root._segmentCap > 0 ? Math.min(natural, Math.max(root._segmentCap, root.minButtonWidth)) : natural;
|
||||
return capped + (selected ? 4 : 0);
|
||||
}
|
||||
height: root.buttonHeight
|
||||
|
||||
color: selected ? Theme.buttonBg : (root.usePopupTransparency ? Theme.withAlpha(Theme.surfaceVariant, Theme.popupTransparency) : Theme.surfaceVariant)
|
||||
border.color: "transparent"
|
||||
border.width: 0
|
||||
|
||||
topLeftRadius: (visualFirst || selected) ? Theme.cornerRadius : Math.min(4, Theme.cornerRadius)
|
||||
bottomLeftRadius: (visualFirst || selected) ? Theme.cornerRadius : Math.min(4, Theme.cornerRadius)
|
||||
topRightRadius: (visualLast || selected) ? Theme.cornerRadius : Math.min(4, Theme.cornerRadius)
|
||||
bottomRightRadius: (visualLast || selected) ? Theme.cornerRadius : Math.min(4, Theme.cornerRadius)
|
||||
|
||||
Behavior on width {
|
||||
enabled: root.userInteracted
|
||||
NumberAnimation {
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Theme.standardEasing
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on topLeftRadius {
|
||||
enabled: root.userInteracted
|
||||
NumberAnimation {
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Theme.standardEasing
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on topRightRadius {
|
||||
enabled: root.userInteracted
|
||||
NumberAnimation {
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Theme.standardEasing
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on bottomLeftRadius {
|
||||
enabled: root.userInteracted
|
||||
NumberAnimation {
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Theme.standardEasing
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on bottomRightRadius {
|
||||
enabled: root.userInteracted
|
||||
NumberAnimation {
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Theme.standardEasing
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on color {
|
||||
enabled: root.userInteracted
|
||||
ColorAnimation {
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Theme.standardEasing
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: stateLayer
|
||||
anchors.fill: parent
|
||||
topLeftRadius: parent.topLeftRadius
|
||||
bottomLeftRadius: parent.bottomLeftRadius
|
||||
topRightRadius: parent.topRightRadius
|
||||
bottomRightRadius: parent.bottomRightRadius
|
||||
color: {
|
||||
if (pressed)
|
||||
return selected ? Theme.buttonPressed : Theme.surfaceTextHover;
|
||||
if (hovered)
|
||||
return selected ? Theme.buttonHover : Theme.surfaceTextHover;
|
||||
return "transparent";
|
||||
}
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Theme.shorterDuration
|
||||
easing.type: Theme.standardEasing
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DankRipple {
|
||||
id: segmentRipple
|
||||
cornerRadius: Theme.cornerRadius
|
||||
rippleColor: segment.selected ? Theme.buttonText : Theme.surfaceVariantText
|
||||
}
|
||||
|
||||
Item {
|
||||
id: contentItem
|
||||
anchors.centerIn: parent
|
||||
implicitWidth: contentRow.implicitWidth
|
||||
implicitHeight: contentRow.implicitHeight
|
||||
|
||||
Row {
|
||||
id: contentRow
|
||||
spacing: Theme.spacingS
|
||||
|
||||
DankIcon {
|
||||
id: checkIcon
|
||||
name: "check"
|
||||
size: root.checkIconSize
|
||||
color: segment.selected ? Theme.buttonText : Theme.surfaceVariantText
|
||||
visible: root.checkEnabled && segment.selected
|
||||
opacity: segment.selected ? 1 : 0
|
||||
scale: segment.selected ? 1 : 0.6
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
Behavior on opacity {
|
||||
enabled: root.userInteracted
|
||||
NumberAnimation {
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Theme.standardEasing
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on scale {
|
||||
enabled: root.userInteracted
|
||||
NumberAnimation {
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Theme.emphasizedEasing
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
id: buttonText
|
||||
|
||||
readonly property real capAvailable: {
|
||||
if (root._segmentCap <= 0)
|
||||
return -1;
|
||||
const cap = Math.max(root._segmentCap, root.minButtonWidth);
|
||||
return Math.max(0, cap - root.buttonPadding * 2 - (checkIcon.visible ? checkIcon.width + contentRow.spacing : 0));
|
||||
}
|
||||
|
||||
text: typeof modelData === "string" ? modelData : modelData.text || ""
|
||||
font.pixelSize: root.textSize
|
||||
font.weight: segment.selected ? Font.Medium : Font.Normal
|
||||
color: segment.selected ? Theme.buttonText : Theme.surfaceVariantText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
width: capAvailable < 0 ? implicitWidth : Math.min(implicitWidth, capAvailable)
|
||||
maximumLineCount: 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: mouseArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onPressed: mouse => segmentRipple.trigger(mouse.x, mouse.y)
|
||||
onClicked: root.selectItem(index)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
DankCommon.DankButtonGroup {}
|
||||
|
||||
@@ -1,131 +1,3 @@
|
||||
import QtQuick
|
||||
import QtQuick.Window
|
||||
import Quickshell.Widgets
|
||||
import qs.Common
|
||||
import qs.Widgets
|
||||
import qs.DankCommon.Widgets as DankCommon
|
||||
|
||||
Rectangle {
|
||||
id: root
|
||||
|
||||
property string imageSource: ""
|
||||
property string fallbackIcon: "notifications"
|
||||
property string fallbackText: ""
|
||||
property bool cacheImages: true
|
||||
property bool hasImage: imageSource !== ""
|
||||
readonly property bool shouldProbe: imageSource !== "" && !imageSource.startsWith("image://")
|
||||
readonly property bool isAnimated: shouldProbe && probe.status === Image.Ready && probe.frameCount > 1
|
||||
readonly property bool probeSettled: probe.status === Image.Ready || probe.status === Image.Error
|
||||
readonly property var activeImage: {
|
||||
if (isAnimated)
|
||||
return probe;
|
||||
if (staticImage.status === Image.Ready)
|
||||
return staticImage;
|
||||
if (probe.status === Image.Ready && probe.source !== "")
|
||||
return probe;
|
||||
return staticImage;
|
||||
}
|
||||
property int imageStatus: activeImage.status
|
||||
|
||||
signal imageSaved(string filePath)
|
||||
|
||||
property string _pendingSavePath: ""
|
||||
property var _attachedWindow: root.Window.window
|
||||
|
||||
on_AttachedWindowChanged: {
|
||||
if (_attachedWindow && _pendingSavePath !== "") {
|
||||
Qt.callLater(function () {
|
||||
if (root._pendingSavePath !== "") {
|
||||
let path = root._pendingSavePath;
|
||||
root._pendingSavePath = "";
|
||||
root.saveImageToFile(path);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function saveImageToFile(filePath) {
|
||||
if (activeImage.status !== Image.Ready)
|
||||
return false;
|
||||
|
||||
if (!activeImage.Window.window) {
|
||||
_pendingSavePath = filePath;
|
||||
return true;
|
||||
}
|
||||
|
||||
activeImage.grabToImage(function (result) {
|
||||
if (result && result.saveToFile(filePath)) {
|
||||
root.imageSaved(filePath);
|
||||
}
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
radius: width / 2
|
||||
color: Theme.primaryHover
|
||||
border.color: "transparent"
|
||||
border.width: 0
|
||||
|
||||
ClippingRectangle {
|
||||
anchors.fill: parent
|
||||
anchors.margins: 2
|
||||
radius: Math.min(width, height) / 2
|
||||
color: "transparent"
|
||||
|
||||
// Probes as AnimatedImage to read frameCount; retires once staticImage is ready.
|
||||
AnimatedImage {
|
||||
id: probe
|
||||
anchors.fill: parent
|
||||
asynchronous: true
|
||||
fillMode: Image.PreserveAspectCrop
|
||||
smooth: true
|
||||
mipmap: true
|
||||
cache: root.cacheImages
|
||||
visible: root.activeImage === probe && probe.status === Image.Ready && root.imageSource !== ""
|
||||
source: root.shouldProbe && (root.isAnimated || staticImage.status !== Image.Ready) ? root.imageSource : ""
|
||||
}
|
||||
|
||||
// Takes over once the probe settles on a non-animated image, then latches.
|
||||
Image {
|
||||
id: staticImage
|
||||
anchors.fill: parent
|
||||
asynchronous: true
|
||||
fillMode: Image.PreserveAspectCrop
|
||||
smooth: true
|
||||
mipmap: true
|
||||
cache: root.cacheImages
|
||||
visible: root.activeImage === staticImage && staticImage.status === Image.Ready && root.imageSource !== ""
|
||||
sourceSize.width: Math.max(width * 2, 128)
|
||||
sourceSize.height: Math.max(height * 2, 128)
|
||||
source: {
|
||||
if (!root.shouldProbe)
|
||||
return root.imageSource;
|
||||
if ((root.probeSettled && !root.isAnimated) || staticImage.status !== Image.Null)
|
||||
return root.imageSource;
|
||||
return "";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
AppIconRenderer {
|
||||
anchors.centerIn: parent
|
||||
width: Math.round(parent.width * 0.75)
|
||||
height: width
|
||||
visible: (root.activeImage.status !== Image.Ready || root.imageSource === "") && root.fallbackIcon !== ""
|
||||
iconValue: root.fallbackIcon
|
||||
iconSize: width
|
||||
iconColor: Theme.surfaceVariantText
|
||||
materialIconSizeAdjustment: 0
|
||||
fallbackText: root.fallbackText
|
||||
fallbackBackgroundColor: "transparent"
|
||||
fallbackTextColor: Theme.surfaceVariantText
|
||||
}
|
||||
|
||||
StyledText {
|
||||
anchors.centerIn: parent
|
||||
visible: root.imageSource === "" && root.fallbackIcon === "" && root.fallbackText !== ""
|
||||
text: root.fallbackText
|
||||
font.pixelSize: Math.max(12, parent.width * 0.5)
|
||||
font.weight: Font.Bold
|
||||
color: Theme.surfaceVariantText
|
||||
}
|
||||
}
|
||||
DankCommon.DankCircularImage {}
|
||||
|
||||
@@ -1,133 +1,3 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import qs.Common
|
||||
import qs.Widgets
|
||||
import qs.DankCommon.Widgets as DankCommon
|
||||
|
||||
ColumnLayout {
|
||||
id: root
|
||||
|
||||
required property string title
|
||||
property string description: ""
|
||||
property bool expanded: false
|
||||
property bool showBackground: false
|
||||
property alias headerColor: headerRect.color
|
||||
|
||||
signal toggleRequested
|
||||
|
||||
spacing: Theme.spacingS
|
||||
Layout.fillWidth: true
|
||||
|
||||
Rectangle {
|
||||
id: headerRect
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: Math.max(titleRow.implicitHeight + Theme.paddingM * 2, 48)
|
||||
radius: Theme.cornerRadius
|
||||
color: "transparent"
|
||||
|
||||
RowLayout {
|
||||
id: titleRow
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: Theme.paddingM
|
||||
anchors.rightMargin: Theme.paddingM
|
||||
spacing: Theme.spacingM
|
||||
|
||||
StyledText {
|
||||
text: root.title
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
font.weight: Font.Medium
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
DankIcon {
|
||||
name: "expand_more"
|
||||
size: Theme.iconSizeSmall
|
||||
rotation: root.expanded ? 180 : 0
|
||||
|
||||
Behavior on rotation {
|
||||
enabled: Theme.currentAnimationSpeed !== SettingsData.AnimationSpeed.None
|
||||
NumberAnimation {
|
||||
easing.type: Easing.BezierSpline
|
||||
duration: Theme.shortDuration
|
||||
easing.bezierCurve: Theme.expressiveCurves.standard
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StateLayer {
|
||||
anchors.fill: parent
|
||||
onClicked: {
|
||||
root.toggleRequested();
|
||||
root.expanded = !root.expanded;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
default property alias content: contentColumn.data
|
||||
|
||||
Item {
|
||||
id: contentWrapper
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: root.expanded ? (contentColumn.implicitHeight + Theme.spacingS * 2) : 0
|
||||
clip: true
|
||||
|
||||
Behavior on Layout.preferredHeight {
|
||||
enabled: Theme.currentAnimationSpeed !== SettingsData.AnimationSpeed.None
|
||||
DankAnim {
|
||||
duration: Theme.expressiveDurations.expressiveDefaultSpatial
|
||||
easing.bezierCurve: Theme.expressiveCurves.standard
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: backgroundRect
|
||||
anchors.fill: parent
|
||||
radius: Theme.cornerRadius
|
||||
color: Theme.surfaceContainer
|
||||
opacity: root.showBackground && root.expanded ? 1.0 : 0.0
|
||||
visible: root.showBackground
|
||||
|
||||
Behavior on opacity {
|
||||
enabled: Theme.currentAnimationSpeed !== SettingsData.AnimationSpeed.None
|
||||
NumberAnimation {
|
||||
easing.type: Easing.BezierSpline
|
||||
duration: Theme.shortDuration
|
||||
easing.bezierCurve: Theme.expressiveCurves.standard
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
id: contentColumn
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
y: Theme.spacingS
|
||||
anchors.leftMargin: Theme.paddingM
|
||||
anchors.rightMargin: Theme.paddingM
|
||||
anchors.bottomMargin: Theme.spacingS
|
||||
spacing: Theme.spacingS
|
||||
opacity: root.expanded ? 1.0 : 0.0
|
||||
|
||||
Behavior on opacity {
|
||||
enabled: Theme.currentAnimationSpeed !== SettingsData.AnimationSpeed.None
|
||||
NumberAnimation {
|
||||
easing.type: Easing.BezierSpline
|
||||
duration: Theme.shortDuration
|
||||
easing.bezierCurve: Theme.expressiveCurves.standard
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
id: descriptionText
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: root.description !== "" ? Theme.spacingXS : 0
|
||||
Layout.bottomMargin: root.description !== "" ? Theme.spacingS : 0
|
||||
visible: root.description !== ""
|
||||
text: root.description
|
||||
color: Theme.surfaceTextSecondary
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
wrapMode: Text.Wrap
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
DankCommon.DankCollapsibleSection {}
|
||||
|
||||
@@ -1,62 +1,3 @@
|
||||
import QtQuick
|
||||
import qs.Common
|
||||
import qs.DankCommon.Widgets as DankCommon
|
||||
|
||||
// Premultiplied-alpha color tween: bind `to`, read `value`. Plain
|
||||
// ColorAnimation lerps raw RGBA and flashes when the endpoints differ
|
||||
// in both color and alpha (translucent <-> opaque).
|
||||
QtObject {
|
||||
id: root
|
||||
|
||||
property color to
|
||||
property bool animated: true
|
||||
property int duration: Theme.mediumDuration
|
||||
property int easingType: Theme.emphasizedEasing
|
||||
|
||||
property color _from: to
|
||||
property color _target: to
|
||||
property real _mix: 1
|
||||
property bool _ready: false
|
||||
|
||||
readonly property color value: {
|
||||
const from = _from;
|
||||
const target = _target;
|
||||
const alpha = from.a + (target.a - from.a) * _mix;
|
||||
if (alpha <= 0)
|
||||
return Qt.rgba(target.r, target.g, target.b, 0);
|
||||
const mix = (a, b) => (a * from.a + (b * target.a - a * from.a) * _mix) / alpha;
|
||||
return Qt.rgba(mix(from.r, target.r), mix(from.g, target.g), mix(from.b, target.b), alpha);
|
||||
}
|
||||
|
||||
readonly property NumberAnimation _anim: NumberAnimation {
|
||||
target: root
|
||||
property: "_mix"
|
||||
from: 0
|
||||
to: 1
|
||||
duration: root.duration
|
||||
easing.type: root.easingType
|
||||
}
|
||||
|
||||
onToChanged: {
|
||||
if (!_ready || !animated) {
|
||||
_anim.stop();
|
||||
_from = to;
|
||||
_target = to;
|
||||
_mix = 1;
|
||||
return;
|
||||
}
|
||||
if (Qt.colorEqual(to, _target))
|
||||
return;
|
||||
const current = value;
|
||||
_anim.stop();
|
||||
_from = current;
|
||||
_target = to;
|
||||
_mix = 0;
|
||||
_anim.restart();
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
_from = to;
|
||||
_target = to;
|
||||
_ready = true;
|
||||
}
|
||||
}
|
||||
DankCommon.DankColorAnimation {}
|
||||
|
||||
@@ -1,45 +1,3 @@
|
||||
import QtQuick
|
||||
import qs.Common
|
||||
import qs.DankCommon.Widgets as DankCommon
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property color swatchColor: "transparent"
|
||||
property color ringColor: Theme.outline
|
||||
property real minPreviewAlpha: 0.4
|
||||
readonly property bool translucent: swatchColor.a > 0 && swatchColor.a < 1
|
||||
readonly property color displayColor: translucent ? Theme.withAlpha(swatchColor, Math.max(swatchColor.a, minPreviewAlpha)) : swatchColor
|
||||
|
||||
Loader {
|
||||
anchors.fill: parent
|
||||
active: root.translucent
|
||||
sourceComponent: Component {
|
||||
Canvas {
|
||||
onPaint: {
|
||||
const ctx = getContext("2d");
|
||||
ctx.reset();
|
||||
ctx.beginPath();
|
||||
ctx.arc(width / 2, height / 2, width / 2, 0, 2 * Math.PI);
|
||||
ctx.clip();
|
||||
const s = Math.max(2, Math.round(width / 4));
|
||||
for (let y = 0; y < height; y += s) {
|
||||
for (let x = 0; x < width; x += s) {
|
||||
ctx.fillStyle = (((x / s) + (y / s)) % 2 === 0) ? "#ffffff" : "#bdbdbd";
|
||||
ctx.fillRect(x, y, s, s);
|
||||
}
|
||||
}
|
||||
}
|
||||
onVisibleChanged: if (visible)
|
||||
requestPaint()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
radius: width / 2
|
||||
color: root.displayColor
|
||||
border.color: root.ringColor
|
||||
border.width: 1
|
||||
}
|
||||
}
|
||||
DankCommon.DankColorSwatch {}
|
||||
|
||||
@@ -1,555 +1,3 @@
|
||||
import "../Common/fzf.js" as Fzf
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import Quickshell
|
||||
import qs.Common
|
||||
import qs.Widgets
|
||||
import qs.DankCommon.Widgets as DankCommon
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
LayoutMirroring.enabled: I18n.isRtl
|
||||
LayoutMirroring.childrenInherit: true
|
||||
|
||||
function checkParentDisablesTransparency() {
|
||||
let p = parent;
|
||||
while (p) {
|
||||
if (p.disablePopupTransparency === true)
|
||||
return true;
|
||||
p = p.parent;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
property string text: ""
|
||||
property string description: ""
|
||||
property string currentValue: ""
|
||||
property var options: []
|
||||
property var optionIcons: []
|
||||
property bool enableFuzzySearch: false
|
||||
property var optionIconMap: ({})
|
||||
property var optionColorMap: ({})
|
||||
|
||||
function rebuildIconMap() {
|
||||
const map = {};
|
||||
for (let i = 0; i < options.length; i++) {
|
||||
if (optionIcons.length > i)
|
||||
map[options[i]] = optionIcons[i];
|
||||
}
|
||||
optionIconMap = map;
|
||||
}
|
||||
|
||||
onOptionsChanged: rebuildIconMap()
|
||||
onOptionIconsChanged: rebuildIconMap()
|
||||
|
||||
property int popupWidthOffset: 0
|
||||
property int maxPopupHeight: 400
|
||||
property bool openUpwards: false
|
||||
property int popupWidth: 0
|
||||
property bool alignPopupRight: false
|
||||
property int dropdownWidth: 200
|
||||
property bool compactMode: text === "" && description === ""
|
||||
property bool showTrigger: true
|
||||
property Item popupAnchorItem: null
|
||||
property bool addHorizontalPadding: false
|
||||
property string emptyText: ""
|
||||
property bool usePopupTransparency: !checkParentDisablesTransparency()
|
||||
property var transientSurfaceTracker: null
|
||||
|
||||
signal valueChanged(string value)
|
||||
|
||||
property bool menuOpen: false
|
||||
|
||||
onMenuOpenChanged: transientSurfaceTracker?.setActive(root, menuOpen, null)
|
||||
|
||||
function closeDropdownMenu() {
|
||||
if (!root.menuOpen && !dropdownMenu.opened && !dropdownMenu.visible)
|
||||
return;
|
||||
root.menuOpen = false;
|
||||
dropdownMenu.close();
|
||||
}
|
||||
|
||||
function positionDropdownMenu() {
|
||||
let currentIndex = root.options.indexOf(root.currentValue);
|
||||
listView.positionViewAtIndex(currentIndex >= 0 ? currentIndex : 0, ListView.Beginning);
|
||||
|
||||
const anchorItem = root.popupAnchorItem || dropdown;
|
||||
const pos = anchorItem.mapToItem(Overlay.overlay, 0, 0);
|
||||
const popupW = dropdownMenu.width;
|
||||
const popupH = dropdownMenu.height;
|
||||
const overlayH = Overlay.overlay.height;
|
||||
const goUp = root.openUpwards || pos.y + anchorItem.height + popupH + 4 > overlayH;
|
||||
dropdownMenu.x = root.alignPopupRight ? pos.x + anchorItem.width - popupW : pos.x - (root.popupWidthOffset / 2);
|
||||
dropdownMenu.y = goUp ? pos.y - popupH - 4 : pos.y + anchorItem.height + 4;
|
||||
}
|
||||
|
||||
function showDropdownMenu() {
|
||||
if (root.options.length === 0)
|
||||
return;
|
||||
if (root.menuOpen)
|
||||
return;
|
||||
|
||||
root.menuOpen = true;
|
||||
dropdownMenu.open();
|
||||
positionDropdownMenu();
|
||||
|
||||
if (root.enableFuzzySearch)
|
||||
searchField.forceActiveFocus();
|
||||
}
|
||||
|
||||
function openDropdownMenu() {
|
||||
if (root.menuOpen) {
|
||||
closeDropdownMenu();
|
||||
return;
|
||||
}
|
||||
showDropdownMenu();
|
||||
}
|
||||
|
||||
function resetSearch() {
|
||||
searchField.text = "";
|
||||
dropdownMenu.fzfFinder = null;
|
||||
dropdownMenu.searchQuery = "";
|
||||
dropdownMenu.selectedIndex = -1;
|
||||
}
|
||||
|
||||
width: !showTrigger ? 0 : (compactMode ? dropdownWidth : parent.width)
|
||||
implicitHeight: !showTrigger ? 0 : (compactMode ? 40 : Math.max(60, labelColumn.implicitHeight + Theme.spacingM))
|
||||
|
||||
Component.onDestruction: {
|
||||
transientSurfaceTracker?.unregister(root);
|
||||
if (root.menuOpen || dropdownMenu.opened || dropdownMenu.visible)
|
||||
dropdownMenu.close();
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: root.transientSurfaceTracker
|
||||
ignoreUnknownSignals: true
|
||||
|
||||
function onCloseRequested() {
|
||||
root.closeDropdownMenu();
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
id: labelColumn
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.right: dropdown.left
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.leftMargin: root.addHorizontalPadding ? Theme.spacingM : 0
|
||||
anchors.rightMargin: Theme.spacingL
|
||||
spacing: Theme.spacingXS
|
||||
visible: !root.compactMode && root.showTrigger
|
||||
|
||||
StyledText {
|
||||
text: root.text
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
width: parent.width
|
||||
horizontalAlignment: Text.AlignLeft
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: root.description
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
visible: description.length > 0
|
||||
wrapMode: Text.WordWrap
|
||||
width: parent.width
|
||||
horizontalAlignment: Text.AlignLeft
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: dropdown
|
||||
|
||||
visible: root.showTrigger
|
||||
width: root.compactMode ? parent.width : (root.popupWidth === -1 ? undefined : (root.popupWidth > 0 ? root.popupWidth : root.dropdownWidth))
|
||||
height: 40
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: root.addHorizontalPadding && !root.compactMode ? Theme.spacingM : 0
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
radius: Theme.cornerRadius
|
||||
color: dropdownArea.containsMouse || dropdownMenu.visible ? Theme.surfaceContainerHigh : (root.usePopupTransparency ? Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency) : Theme.surfaceContainer)
|
||||
border.color: dropdownMenu.visible ? Theme.primary : Theme.outlineHeavy
|
||||
border.width: dropdownMenu.visible ? 2 : 1
|
||||
|
||||
MouseArea {
|
||||
id: dropdownArea
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: root.openDropdownMenu()
|
||||
}
|
||||
|
||||
Row {
|
||||
id: contentRow
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.right: expandIcon.left
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.leftMargin: Theme.spacingM
|
||||
anchors.rightMargin: Theme.spacingS
|
||||
spacing: Theme.spacingS
|
||||
|
||||
DankColorSwatch {
|
||||
id: triggerSwatch
|
||||
|
||||
width: 16
|
||||
height: 16
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
visible: root.optionColorMap[root.currentValue] !== undefined
|
||||
swatchColor: visible ? root.optionColorMap[root.currentValue] : "transparent"
|
||||
}
|
||||
|
||||
DankIcon {
|
||||
id: triggerIcon
|
||||
|
||||
name: root.optionIconMap[root.currentValue] ?? ""
|
||||
size: 18
|
||||
color: Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
visible: name !== ""
|
||||
}
|
||||
|
||||
StyledText {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: root.currentValue !== "" ? root.currentValue : root.emptyText
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: root.currentValue !== "" ? Theme.surfaceText : Theme.outline
|
||||
width: contentRow.width - (triggerSwatch.visible ? triggerSwatch.width + contentRow.spacing : 0) - (triggerIcon.visible ? triggerIcon.width + contentRow.spacing : 0)
|
||||
elide: Text.ElideRight
|
||||
wrapMode: Text.NoWrap
|
||||
horizontalAlignment: Text.AlignLeft
|
||||
}
|
||||
}
|
||||
|
||||
DankIcon {
|
||||
id: expandIcon
|
||||
|
||||
name: dropdownMenu.visible ? "expand_less" : "expand_more"
|
||||
size: 20
|
||||
color: Theme.surfaceText
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.rightMargin: Theme.spacingS
|
||||
|
||||
Behavior on rotation {
|
||||
NumberAnimation {
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Theme.standardEasing
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Popup {
|
||||
id: dropdownMenu
|
||||
|
||||
property string searchQuery: ""
|
||||
property var filteredOptions: {
|
||||
if (!root.enableFuzzySearch || searchQuery.length === 0)
|
||||
return root.options;
|
||||
if (!fzfFinder)
|
||||
return root.options;
|
||||
return fzfFinder.find(searchQuery).map(r => r.item);
|
||||
}
|
||||
property int selectedIndex: -1
|
||||
property var fzfFinder: null
|
||||
|
||||
function initFinder() {
|
||||
fzfFinder = new Fzf.Finder(root.options, {
|
||||
"selector": option => option,
|
||||
"limit": 50,
|
||||
"casing": "case-insensitive",
|
||||
"sort": true,
|
||||
"tiebreakers": [(a, b, selector) => selector(a.item).length - selector(b.item).length]
|
||||
});
|
||||
}
|
||||
|
||||
function selectNext() {
|
||||
if (filteredOptions.length === 0)
|
||||
return;
|
||||
selectedIndex = (selectedIndex + 1) % filteredOptions.length;
|
||||
listView.positionViewAtIndex(selectedIndex, ListView.Contain);
|
||||
}
|
||||
|
||||
function selectPrevious() {
|
||||
if (filteredOptions.length === 0)
|
||||
return;
|
||||
selectedIndex = selectedIndex <= 0 ? filteredOptions.length - 1 : selectedIndex - 1;
|
||||
listView.positionViewAtIndex(selectedIndex, ListView.Contain);
|
||||
}
|
||||
|
||||
function selectCurrent() {
|
||||
if (selectedIndex < 0 || selectedIndex >= filteredOptions.length)
|
||||
return;
|
||||
root.currentValue = filteredOptions[selectedIndex];
|
||||
root.valueChanged(filteredOptions[selectedIndex]);
|
||||
close();
|
||||
}
|
||||
|
||||
onOpened: {
|
||||
root.menuOpen = true;
|
||||
selectedIndex = -1;
|
||||
if (searchField.text.length > 0) {
|
||||
initFinder();
|
||||
searchQuery = searchField.text;
|
||||
} else {
|
||||
fzfFinder = null;
|
||||
searchQuery = "";
|
||||
}
|
||||
}
|
||||
|
||||
onClosed: root.menuOpen = false
|
||||
|
||||
parent: root.Overlay.overlay
|
||||
width: root.popupWidth === -1 ? undefined : (root.popupWidth > 0 ? root.popupWidth : (dropdown.width + root.popupWidthOffset))
|
||||
height: {
|
||||
let h = root.enableFuzzySearch ? 54 : 0;
|
||||
if (root.options.length === 0 && root.emptyText !== "")
|
||||
h += 32;
|
||||
else
|
||||
h += Math.min(filteredOptions.length, 10) * 36;
|
||||
return Math.min(root.maxPopupHeight, h + 16);
|
||||
}
|
||||
padding: 0
|
||||
modal: true
|
||||
dim: false
|
||||
closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside
|
||||
|
||||
enter: Transition {
|
||||
NumberAnimation {
|
||||
property: "scale"
|
||||
from: 0.9
|
||||
to: 1
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Theme.emphasizedEasing
|
||||
}
|
||||
NumberAnimation {
|
||||
property: "opacity"
|
||||
from: 0
|
||||
to: 1
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Theme.standardEasing
|
||||
}
|
||||
}
|
||||
|
||||
exit: Transition {
|
||||
NumberAnimation {
|
||||
property: "scale"
|
||||
from: 1
|
||||
to: 0.9
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Theme.emphasizedEasing
|
||||
}
|
||||
NumberAnimation {
|
||||
property: "opacity"
|
||||
from: 1
|
||||
to: 0
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Theme.standardEasing
|
||||
}
|
||||
}
|
||||
|
||||
background: Rectangle {
|
||||
color: "transparent"
|
||||
}
|
||||
|
||||
contentItem: Rectangle {
|
||||
id: contentSurface
|
||||
|
||||
LayoutMirroring.enabled: I18n.isRtl
|
||||
LayoutMirroring.childrenInherit: true
|
||||
color: Theme.withAlpha(Theme.surfaceContainer, 1)
|
||||
border.color: Theme.primary
|
||||
border.width: 2
|
||||
radius: Theme.cornerRadius
|
||||
|
||||
ElevationShadow {
|
||||
id: shadowLayer
|
||||
anchors.fill: parent
|
||||
z: -1
|
||||
level: Theme.elevationLevel2
|
||||
fallbackOffset: 4
|
||||
targetRadius: contentSurface.radius
|
||||
targetColor: contentSurface.color
|
||||
borderColor: contentSurface.border.color
|
||||
borderWidth: contentSurface.border.width
|
||||
shadowEnabled: Theme.elevationEnabled && SettingsData.popoutElevationEnabled
|
||||
}
|
||||
|
||||
Column {
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingS
|
||||
|
||||
Rectangle {
|
||||
id: searchContainer
|
||||
|
||||
width: parent.width
|
||||
height: 42
|
||||
visible: root.enableFuzzySearch
|
||||
radius: Theme.cornerRadius
|
||||
color: root.usePopupTransparency ? Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency) : Theme.surfaceContainerHigh
|
||||
|
||||
DankTextField {
|
||||
id: searchField
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.margins: 1
|
||||
placeholderText: I18n.tr("Search...")
|
||||
topPadding: Theme.spacingS
|
||||
bottomPadding: Theme.spacingS
|
||||
onTextChanged: searchDebounce.restart()
|
||||
Keys.onDownPressed: dropdownMenu.selectNext()
|
||||
Keys.onUpPressed: dropdownMenu.selectPrevious()
|
||||
Keys.onReturnPressed: dropdownMenu.selectCurrent()
|
||||
Keys.onEnterPressed: dropdownMenu.selectCurrent()
|
||||
Keys.onPressed: event => {
|
||||
if (!(event.modifiers & Qt.ControlModifier))
|
||||
return;
|
||||
switch (event.key) {
|
||||
case Qt.Key_N:
|
||||
case Qt.Key_J:
|
||||
dropdownMenu.selectNext();
|
||||
event.accepted = true;
|
||||
break;
|
||||
case Qt.Key_P:
|
||||
case Qt.Key_K:
|
||||
dropdownMenu.selectPrevious();
|
||||
event.accepted = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: searchDebounce
|
||||
interval: 50
|
||||
onTriggered: {
|
||||
if (!dropdownMenu.fzfFinder)
|
||||
dropdownMenu.initFinder();
|
||||
dropdownMenu.searchQuery = searchField.text;
|
||||
dropdownMenu.selectedIndex = -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
width: 1
|
||||
height: Theme.spacingXS
|
||||
visible: root.enableFuzzySearch
|
||||
}
|
||||
|
||||
Item {
|
||||
width: parent.width
|
||||
height: 32
|
||||
visible: root.options.length === 0 && root.emptyText !== ""
|
||||
|
||||
StyledText {
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.leftMargin: Theme.spacingS
|
||||
anchors.rightMargin: Theme.spacingS
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: root.emptyText
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceVariantText
|
||||
horizontalAlignment: Text.AlignLeft
|
||||
}
|
||||
}
|
||||
|
||||
DankListView {
|
||||
id: listView
|
||||
|
||||
width: parent.width
|
||||
height: parent.height - (root.enableFuzzySearch ? searchContainer.height + Theme.spacingXS : 0) - (root.options.length === 0 && root.emptyText !== "" ? 32 : 0)
|
||||
clip: true
|
||||
visible: root.options.length > 0
|
||||
model: ScriptModel {
|
||||
values: dropdownMenu.filteredOptions
|
||||
}
|
||||
spacing: Theme.spacingXXS
|
||||
|
||||
interactive: true
|
||||
flickDeceleration: 1500
|
||||
maximumFlickVelocity: 2000
|
||||
boundsBehavior: Flickable.DragAndOvershootBounds
|
||||
boundsMovement: Flickable.FollowBoundsBehavior
|
||||
pressDelay: 0
|
||||
flickableDirection: Flickable.VerticalFlick
|
||||
|
||||
delegate: Rectangle {
|
||||
id: delegateRoot
|
||||
|
||||
required property var modelData
|
||||
required property int index
|
||||
property bool isSelected: dropdownMenu.selectedIndex === index
|
||||
property bool isCurrentValue: root.currentValue === modelData
|
||||
property string iconName: root.optionIconMap[modelData] ?? ""
|
||||
property var swatchColor: root.optionColorMap[modelData]
|
||||
|
||||
width: ListView.view.width
|
||||
height: 32
|
||||
radius: Theme.cornerRadius
|
||||
color: isSelected ? Theme.primaryHover : optionArea.containsMouse ? Theme.primaryHoverLight : Theme.withAlpha(Theme.primaryHoverLight, 0)
|
||||
|
||||
Row {
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.leftMargin: Theme.spacingS
|
||||
anchors.rightMargin: Theme.spacingS
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Theme.spacingS
|
||||
|
||||
DankColorSwatch {
|
||||
id: optionSwatch
|
||||
|
||||
width: 16
|
||||
height: 16
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
visible: delegateRoot.swatchColor !== undefined
|
||||
swatchColor: visible ? delegateRoot.swatchColor : Theme.withAlpha(delegateRoot.swatchColor, 0)
|
||||
ringColor: delegateRoot.isCurrentValue ? Theme.primary : Theme.outline
|
||||
}
|
||||
|
||||
DankIcon {
|
||||
name: delegateRoot.iconName
|
||||
size: 18
|
||||
color: delegateRoot.isCurrentValue ? Theme.primary : Theme.surfaceText
|
||||
visible: name !== ""
|
||||
}
|
||||
|
||||
StyledText {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: delegateRoot.modelData
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: delegateRoot.isCurrentValue ? Theme.primary : Theme.surfaceText
|
||||
font.weight: delegateRoot.isCurrentValue ? Font.Medium : Font.Normal
|
||||
width: root.popupWidth > 0 ? undefined : (delegateRoot.width - parent.x - Theme.spacingS * 2 - (optionSwatch.visible ? optionSwatch.width + parent.spacing : 0))
|
||||
elide: root.popupWidth > 0 ? Text.ElideNone : Text.ElideRight
|
||||
wrapMode: Text.NoWrap
|
||||
horizontalAlignment: Text.AlignLeft
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: optionArea
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
root.currentValue = delegateRoot.modelData;
|
||||
root.valueChanged(delegateRoot.modelData);
|
||||
root.closeDropdownMenu();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
DankCommon.DankDropdown {}
|
||||
|
||||
@@ -1,108 +1,3 @@
|
||||
import QtQuick
|
||||
import qs.Common
|
||||
import qs.Widgets
|
||||
import qs.DankCommon.Widgets as DankCommon
|
||||
|
||||
Flow {
|
||||
id: root
|
||||
|
||||
property var model: []
|
||||
property int currentIndex: 0
|
||||
property int chipHeight: 32
|
||||
property int chipPadding: Theme.spacingM
|
||||
property bool showCheck: true
|
||||
property bool showCounts: true
|
||||
|
||||
signal selectionChanged(int index)
|
||||
|
||||
spacing: Theme.spacingS
|
||||
width: parent ? parent.width : 400
|
||||
|
||||
Repeater {
|
||||
model: root.model
|
||||
|
||||
Rectangle {
|
||||
id: chip
|
||||
required property var modelData
|
||||
required property int index
|
||||
|
||||
property bool selected: index === root.currentIndex
|
||||
property bool hovered: mouseArea.containsMouse
|
||||
property bool pressed: mouseArea.pressed
|
||||
property string label: typeof modelData === "string" ? modelData : (modelData.label || "")
|
||||
property int count: typeof modelData === "object" ? (modelData.count || 0) : 0
|
||||
property bool showCount: root.showCounts && count > 0
|
||||
|
||||
width: contentRow.implicitWidth + root.chipPadding * 2
|
||||
height: root.chipHeight
|
||||
radius: height / 2
|
||||
|
||||
color: selected ? Theme.primary : Theme.surfaceVariant
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Theme.standardEasing
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
radius: parent.radius
|
||||
color: {
|
||||
if (pressed)
|
||||
return chip.selected ? Theme.primaryPressed : Theme.surfaceTextHover;
|
||||
if (hovered)
|
||||
return chip.selected ? Theme.primaryHover : Theme.surfaceTextHover;
|
||||
return "transparent";
|
||||
}
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Theme.shorterDuration
|
||||
easing.type: Theme.standardEasing
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DankRipple {
|
||||
id: chipRipple
|
||||
cornerRadius: chip.radius
|
||||
rippleColor: chip.selected ? Theme.primaryText : Theme.surfaceVariantText
|
||||
}
|
||||
|
||||
Row {
|
||||
id: contentRow
|
||||
anchors.centerIn: parent
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
DankIcon {
|
||||
name: "check"
|
||||
size: 16
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
color: Theme.primaryText
|
||||
visible: root.showCheck && chip.selected
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: chip.label + (chip.showCount ? " (" + chip.count + ")" : "")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.weight: chip.selected ? Font.Medium : Font.Normal
|
||||
color: chip.selected ? Theme.primaryText : Theme.surfaceVariantText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: mouseArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onPressed: mouse => chipRipple.trigger(mouse.x, mouse.y)
|
||||
onClicked: {
|
||||
root.currentIndex = chip.index;
|
||||
root.selectionChanged(chip.index);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
DankCommon.DankFilterChips {}
|
||||
|
||||
@@ -1,179 +1,3 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import qs.Widgets
|
||||
import "ScrollConstants.js" as Scroll
|
||||
import qs.DankCommon.Widgets as DankCommon
|
||||
|
||||
Flickable {
|
||||
id: flickable
|
||||
|
||||
property alias verticalScrollBar: vbar
|
||||
property real mouseWheelSpeed: Scroll.mouseWheelSpeed
|
||||
property real momentumVelocity: 0
|
||||
property bool isMomentumActive: false
|
||||
property real friction: Scroll.friction
|
||||
property bool _scrollBarActive: false
|
||||
|
||||
flickDeceleration: Scroll.flickDeceleration
|
||||
maximumFlickVelocity: Scroll.maximumFlickVelocity
|
||||
boundsBehavior: Flickable.StopAtBounds
|
||||
boundsMovement: Flickable.FollowBoundsBehavior
|
||||
pressDelay: 0
|
||||
flickableDirection: Flickable.VerticalFlick
|
||||
|
||||
WheelHandler {
|
||||
id: wheelHandler
|
||||
|
||||
property real touchpadSpeed: Scroll.touchpadSpeed
|
||||
property real momentumRetention: Scroll.momentumRetention
|
||||
property real lastWheelTime: 0
|
||||
property real momentum: 0
|
||||
property var velocitySamples: []
|
||||
property bool sessionUsedMouseWheel: false
|
||||
|
||||
function startMomentum() {
|
||||
flickable.isMomentumActive = true;
|
||||
momentumAnim.running = true;
|
||||
}
|
||||
|
||||
acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad
|
||||
|
||||
onWheel: event => {
|
||||
vbar._scrollBarActive = true;
|
||||
vbar.hideTimer.restart();
|
||||
|
||||
const currentTime = Date.now();
|
||||
const timeDelta = currentTime - lastWheelTime;
|
||||
lastWheelTime = currentTime;
|
||||
|
||||
const hasPixel = event.pixelDelta && event.pixelDelta.y !== 0;
|
||||
const deltaY = event.angleDelta.y;
|
||||
const isTraditionalMouse = !hasPixel && Math.abs(deltaY) >= 120 && (Math.abs(deltaY) % 120) === 0;
|
||||
const isHighDpiMouse = !hasPixel && !isTraditionalMouse && deltaY !== 0;
|
||||
const isTouchpad = hasPixel;
|
||||
|
||||
if (isTraditionalMouse) {
|
||||
sessionUsedMouseWheel = true;
|
||||
momentumAnim.running = false;
|
||||
flickable.isMomentumActive = false;
|
||||
velocitySamples = [];
|
||||
momentum = 0;
|
||||
flickable.momentumVelocity = 0;
|
||||
|
||||
const lines = Math.round(Math.abs(deltaY) / 120);
|
||||
const scrollAmount = (deltaY > 0 ? -lines : lines) * flickable.mouseWheelSpeed;
|
||||
let newY = flickable.contentY + scrollAmount;
|
||||
newY = Math.max(0, Math.min(flickable.contentHeight - flickable.height, newY));
|
||||
|
||||
if (flickable.flicking) {
|
||||
flickable.cancelFlick();
|
||||
}
|
||||
|
||||
flickable.contentY = newY;
|
||||
} else if (isHighDpiMouse) {
|
||||
sessionUsedMouseWheel = true;
|
||||
momentumAnim.running = false;
|
||||
flickable.isMomentumActive = false;
|
||||
velocitySamples = [];
|
||||
momentum = 0;
|
||||
flickable.momentumVelocity = 0;
|
||||
|
||||
let delta = deltaY / 8 * touchpadSpeed;
|
||||
let newY = flickable.contentY - delta;
|
||||
newY = Math.max(0, Math.min(flickable.contentHeight - flickable.height, newY));
|
||||
|
||||
if (flickable.flicking) {
|
||||
flickable.cancelFlick();
|
||||
}
|
||||
|
||||
flickable.contentY = newY;
|
||||
} else if (isTouchpad) {
|
||||
sessionUsedMouseWheel = false;
|
||||
momentumAnim.running = false;
|
||||
flickable.isMomentumActive = false;
|
||||
|
||||
let delta = event.pixelDelta.y * touchpadSpeed;
|
||||
|
||||
velocitySamples.push({
|
||||
"delta": delta,
|
||||
"time": currentTime
|
||||
});
|
||||
velocitySamples = velocitySamples.filter(s => currentTime - s.time < Scroll.velocitySampleWindowMs);
|
||||
|
||||
if (velocitySamples.length > 1) {
|
||||
const totalDelta = velocitySamples.reduce((sum, s) => sum + s.delta, 0);
|
||||
const timeSpan = currentTime - velocitySamples[0].time;
|
||||
if (timeSpan > 0) {
|
||||
flickable.momentumVelocity = Math.max(-Scroll.maxMomentumVelocity, Math.min(Scroll.maxMomentumVelocity, totalDelta / timeSpan * 1000));
|
||||
}
|
||||
}
|
||||
|
||||
if (timeDelta < Scroll.momentumTimeThreshold) {
|
||||
momentum = momentum * momentumRetention + delta * Scroll.momentumDeltaFactor;
|
||||
delta += momentum;
|
||||
} else {
|
||||
momentum = 0;
|
||||
}
|
||||
|
||||
let newY = flickable.contentY - delta;
|
||||
newY = Math.max(0, Math.min(flickable.contentHeight - flickable.height, newY));
|
||||
|
||||
if (flickable.flicking) {
|
||||
flickable.cancelFlick();
|
||||
}
|
||||
|
||||
flickable.contentY = newY;
|
||||
}
|
||||
|
||||
event.accepted = true;
|
||||
}
|
||||
|
||||
onActiveChanged: {
|
||||
if (!active) {
|
||||
if (!sessionUsedMouseWheel && Math.abs(flickable.momentumVelocity) >= Scroll.minMomentumVelocity) {
|
||||
startMomentum();
|
||||
} else {
|
||||
velocitySamples = [];
|
||||
flickable.momentumVelocity = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onMovementStarted: {
|
||||
vbar._scrollBarActive = true;
|
||||
vbar.hideTimer.stop();
|
||||
}
|
||||
onMovementEnded: vbar.hideTimer.restart()
|
||||
|
||||
FrameAnimation {
|
||||
id: momentumAnim
|
||||
running: false
|
||||
|
||||
onTriggered: {
|
||||
const dt = frameTime;
|
||||
const newY = flickable.contentY - flickable.momentumVelocity * dt;
|
||||
const maxY = Math.max(0, flickable.contentHeight - flickable.height);
|
||||
|
||||
if (newY < 0 || newY > maxY) {
|
||||
flickable.contentY = newY < 0 ? 0 : maxY;
|
||||
running = false;
|
||||
flickable.isMomentumActive = false;
|
||||
flickable.momentumVelocity = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
flickable.contentY = newY;
|
||||
flickable.momentumVelocity *= Math.pow(flickable.friction, dt / 0.016);
|
||||
|
||||
if (Math.abs(flickable.momentumVelocity) < Scroll.momentumStopThreshold) {
|
||||
running = false;
|
||||
flickable.isMomentumActive = false;
|
||||
flickable.momentumVelocity = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ScrollBar.vertical: DankScrollbar {
|
||||
id: vbar
|
||||
}
|
||||
}
|
||||
DankCommon.DankFlickable {}
|
||||
|
||||
@@ -1,175 +1,3 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import qs.Widgets
|
||||
import "ScrollConstants.js" as Scroll
|
||||
import qs.DankCommon.Widgets as DankCommon
|
||||
|
||||
GridView {
|
||||
id: gridView
|
||||
|
||||
property real momentumVelocity: 0
|
||||
property bool isMomentumActive: false
|
||||
property real friction: Scroll.friction
|
||||
|
||||
flickDeceleration: Scroll.flickDeceleration
|
||||
maximumFlickVelocity: Scroll.maximumFlickVelocity
|
||||
boundsBehavior: Flickable.StopAtBounds
|
||||
boundsMovement: Flickable.FollowBoundsBehavior
|
||||
pressDelay: 0
|
||||
flickableDirection: Flickable.VerticalFlick
|
||||
|
||||
onMovementStarted: {
|
||||
vbar._scrollBarActive = true;
|
||||
vbar.hideTimer.stop();
|
||||
}
|
||||
onMovementEnded: vbar.hideTimer.restart()
|
||||
|
||||
WheelHandler {
|
||||
id: wheelHandler
|
||||
|
||||
property real mouseWheelSpeed: Scroll.mouseWheelSpeed
|
||||
property real touchpadSpeed: Scroll.touchpadSpeed
|
||||
property real momentumRetention: Scroll.momentumRetention
|
||||
property real lastWheelTime: 0
|
||||
property real momentum: 0
|
||||
property var velocitySamples: []
|
||||
property bool sessionUsedMouseWheel: false
|
||||
|
||||
function startMomentum() {
|
||||
isMomentumActive = true;
|
||||
momentumAnim.running = true;
|
||||
}
|
||||
|
||||
acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad
|
||||
onWheel: event => {
|
||||
vbar._scrollBarActive = true;
|
||||
vbar.hideTimer.restart();
|
||||
|
||||
const currentTime = Date.now();
|
||||
const timeDelta = currentTime - lastWheelTime;
|
||||
lastWheelTime = currentTime;
|
||||
|
||||
const hasPixel = event.pixelDelta && event.pixelDelta.y !== 0;
|
||||
const deltaY = event.angleDelta.y;
|
||||
const isTraditionalMouse = !hasPixel && Math.abs(deltaY) >= 120 && (Math.abs(deltaY) % 120) === 0;
|
||||
const isHighDpiMouse = !hasPixel && !isTraditionalMouse && deltaY !== 0;
|
||||
const isTouchpad = hasPixel;
|
||||
|
||||
if (isTraditionalMouse) {
|
||||
sessionUsedMouseWheel = true;
|
||||
momentumAnim.running = false;
|
||||
isMomentumActive = false;
|
||||
velocitySamples = [];
|
||||
momentum = 0;
|
||||
momentumVelocity = 0;
|
||||
|
||||
const lines = Math.round(Math.abs(deltaY) / 120);
|
||||
const scrollAmount = (deltaY > 0 ? -lines : lines) * cellHeight * 0.35;
|
||||
let newY = contentY + scrollAmount;
|
||||
newY = Math.max(0, Math.min(contentHeight - height, newY));
|
||||
|
||||
if (flicking) {
|
||||
cancelFlick();
|
||||
}
|
||||
|
||||
contentY = newY;
|
||||
} else if (isHighDpiMouse) {
|
||||
sessionUsedMouseWheel = true;
|
||||
momentumAnim.running = false;
|
||||
isMomentumActive = false;
|
||||
velocitySamples = [];
|
||||
momentum = 0;
|
||||
momentumVelocity = 0;
|
||||
|
||||
let delta = deltaY / 120 * cellHeight * 1.2;
|
||||
let newY = contentY - delta;
|
||||
newY = Math.max(0, Math.min(contentHeight - height, newY));
|
||||
|
||||
if (flicking) {
|
||||
cancelFlick();
|
||||
}
|
||||
|
||||
contentY = newY;
|
||||
} else if (isTouchpad) {
|
||||
sessionUsedMouseWheel = false;
|
||||
momentumAnim.running = false;
|
||||
isMomentumActive = false;
|
||||
|
||||
let delta = event.pixelDelta.y * touchpadSpeed;
|
||||
|
||||
velocitySamples.push({
|
||||
"delta": delta,
|
||||
"time": currentTime
|
||||
});
|
||||
velocitySamples = velocitySamples.filter(s => currentTime - s.time < Scroll.velocitySampleWindowMs);
|
||||
|
||||
if (velocitySamples.length > 1) {
|
||||
const totalDelta = velocitySamples.reduce((sum, s) => sum + s.delta, 0);
|
||||
const timeSpan = currentTime - velocitySamples[0].time;
|
||||
if (timeSpan > 0) {
|
||||
momentumVelocity = Math.max(-Scroll.maxMomentumVelocity, Math.min(Scroll.maxMomentumVelocity, totalDelta / timeSpan * 1000));
|
||||
}
|
||||
}
|
||||
|
||||
if (timeDelta < Scroll.momentumTimeThreshold) {
|
||||
momentum = momentum * momentumRetention + delta * Scroll.momentumDeltaFactor;
|
||||
delta += momentum;
|
||||
} else {
|
||||
momentum = 0;
|
||||
}
|
||||
|
||||
let newY = contentY - delta;
|
||||
newY = Math.max(0, Math.min(contentHeight - height, newY));
|
||||
|
||||
if (flicking) {
|
||||
cancelFlick();
|
||||
}
|
||||
|
||||
contentY = newY;
|
||||
}
|
||||
|
||||
event.accepted = true;
|
||||
}
|
||||
onActiveChanged: {
|
||||
if (!active) {
|
||||
if (!sessionUsedMouseWheel && Math.abs(momentumVelocity) >= Scroll.minMomentumVelocity) {
|
||||
startMomentum();
|
||||
} else {
|
||||
velocitySamples = [];
|
||||
momentumVelocity = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
FrameAnimation {
|
||||
id: momentumAnim
|
||||
running: false
|
||||
|
||||
onTriggered: {
|
||||
const dt = frameTime;
|
||||
const newY = contentY - momentumVelocity * dt;
|
||||
const maxY = Math.max(0, contentHeight - height);
|
||||
|
||||
if (newY < 0 || newY > maxY) {
|
||||
contentY = newY < 0 ? 0 : maxY;
|
||||
running = false;
|
||||
isMomentumActive = false;
|
||||
momentumVelocity = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
contentY = newY;
|
||||
momentumVelocity *= Math.pow(friction, dt / 0.016);
|
||||
|
||||
if (Math.abs(momentumVelocity) < Scroll.momentumStopThreshold) {
|
||||
running = false;
|
||||
isMomentumActive = false;
|
||||
momentumVelocity = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ScrollBar.vertical: DankScrollbar {
|
||||
id: vbar
|
||||
}
|
||||
}
|
||||
DankCommon.DankGridView {}
|
||||
|
||||
@@ -1,72 +1,3 @@
|
||||
import QtQuick
|
||||
import qs.Common
|
||||
import qs.DankCommon.Widgets as DankCommon
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property alias name: icon.text
|
||||
property alias size: icon.font.pixelSize
|
||||
property alias color: icon.color
|
||||
property bool filled: false
|
||||
property real fill: filled ? 1.0 : 0.0
|
||||
property int grade: Theme.isLightMode ? 0 : -25
|
||||
property int weight: filled ? 500 : 400
|
||||
property bool smoothTransform: false
|
||||
|
||||
implicitWidth: Math.round(size)
|
||||
implicitHeight: Math.round(size)
|
||||
|
||||
signal rotationCompleted
|
||||
|
||||
FontLoader {
|
||||
id: materialSymbolsFont
|
||||
source: Qt.resolvedUrl("../assets/fonts/material-design-icons/variablefont/MaterialSymbolsRounded[FILL,GRAD,opsz,wght].ttf")
|
||||
}
|
||||
|
||||
StyledText {
|
||||
id: icon
|
||||
|
||||
anchors.fill: parent
|
||||
|
||||
font.family: materialSymbolsFont.name
|
||||
font.pixelSize: Math.round(Theme.fontSizeMedium)
|
||||
font.weight: root.weight
|
||||
font.hintingPreference: Font.PreferNoHinting
|
||||
color: Theme.surfaceText
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
renderType: root.smoothTransform ? Text.QtRendering : Text.NativeRendering
|
||||
|
||||
font.variableAxes: {
|
||||
"FILL": root.fill.toFixed(1),
|
||||
"GRAD": root.grade,
|
||||
"opsz": 24,
|
||||
"wght": root.weight
|
||||
}
|
||||
|
||||
Behavior on font.weight {
|
||||
NumberAnimation {
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Theme.standardEasing
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on fill {
|
||||
NumberAnimation {
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Theme.standardEasing
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: rotationTimer
|
||||
interval: 16
|
||||
repeat: false
|
||||
onTriggered: root.rotationCompleted()
|
||||
}
|
||||
|
||||
onRotationChanged: {
|
||||
rotationTimer.restart();
|
||||
}
|
||||
}
|
||||
DankCommon.DankIcon {}
|
||||
|
||||
@@ -1,259 +1,3 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import qs.Common
|
||||
import qs.Widgets
|
||||
import qs.DankCommon.Widgets as DankCommon
|
||||
|
||||
Rectangle {
|
||||
id: root
|
||||
|
||||
property string currentIcon: ""
|
||||
property string iconType: "icon"
|
||||
|
||||
signal iconSelected(string iconName, string iconType)
|
||||
|
||||
width: 240
|
||||
height: 32
|
||||
radius: Theme.cornerRadius
|
||||
color: Theme.surfaceContainer
|
||||
border.color: iconPopup.visible ? Theme.primary : Theme.outline
|
||||
border.width: 1
|
||||
|
||||
property var iconCategories: [
|
||||
{
|
||||
"name": I18n.tr("Numbers"),
|
||||
"icons": ["looks_one", "looks_two", "looks_3", "looks_4", "looks_5", "looks_6", "filter_1", "filter_2", "filter_3", "filter_4", "filter_5", "filter_6", "filter_7", "filter_8", "filter_9", "filter_9_plus", "plus_one", "exposure_plus_1", "exposure_plus_2"]
|
||||
},
|
||||
{
|
||||
"name": I18n.tr("Workspace"),
|
||||
"icons": ["work", "laptop", "desktop_windows", "folder", "view_module", "dashboard", "apps", "grid_view"]
|
||||
},
|
||||
{
|
||||
"name": I18n.tr("Development"),
|
||||
"icons": ["code", "terminal", "bug_report", "build", "engineering", "integration_instructions", "data_object", "schema", "api", "webhook"]
|
||||
},
|
||||
{
|
||||
"name": I18n.tr("Communication"),
|
||||
"icons": ["chat", "mail", "forum", "message", "video_call", "call", "contacts", "group", "notifications", "campaign"]
|
||||
},
|
||||
{
|
||||
"name": I18n.tr("Media"),
|
||||
"icons": ["music_note", "headphones", "mic", "videocam", "photo", "movie", "library_music", "album", "radio", "volume_up"]
|
||||
},
|
||||
{
|
||||
"name": I18n.tr("System"),
|
||||
"icons": ["memory", "storage", "developer_board", "monitor", "keyboard", "mouse", "battery_std", "wifi", "bluetooth", "security", "settings"]
|
||||
},
|
||||
{
|
||||
"name": I18n.tr("Navigation"),
|
||||
"icons": ["home", "arrow_forward", "arrow_back", "expand_more", "expand_less", "menu", "close", "search", "filter_list", "sort"]
|
||||
},
|
||||
{
|
||||
"name": I18n.tr("Actions"),
|
||||
"icons": ["add", "remove", "edit", "delete", "save", "download", "upload", "share", "content_copy", "content_paste", "content_cut", "undo", "redo"]
|
||||
},
|
||||
{
|
||||
"name": I18n.tr("Status"),
|
||||
"icons": ["check", "error", "warning", "info", "done", "pending", "schedule", "update", "sync", "offline_bolt"]
|
||||
},
|
||||
{
|
||||
"name": I18n.tr("Fun"),
|
||||
"icons": ["celebration", "cake", "star", "favorite", "pets", "sports_esports", "local_fire_department", "bolt", "auto_awesome", "diamond"]
|
||||
}
|
||||
]
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
if (iconPopup.visible) {
|
||||
iconPopup.close();
|
||||
return;
|
||||
}
|
||||
const pos = root.mapToItem(Overlay.overlay, 0, 0);
|
||||
const popupHeight = 500;
|
||||
const overlayHeight = Overlay.overlay?.height ?? 800;
|
||||
iconPopup.x = pos.x;
|
||||
if (pos.y + root.height + popupHeight + 4 > overlayHeight) {
|
||||
iconPopup.y = pos.y - popupHeight - 4;
|
||||
} else {
|
||||
iconPopup.y = pos.y + root.height + 4;
|
||||
}
|
||||
iconPopup.open();
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
anchors.left: parent.left
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.leftMargin: Theme.spacingS
|
||||
spacing: Theme.spacingS
|
||||
|
||||
DankIcon {
|
||||
name: (root.iconType === "icon" && root.currentIcon) ? root.currentIcon : (root.iconType === "text" ? "text_fields" : "add")
|
||||
size: 16
|
||||
color: root.currentIcon ? Theme.surfaceText : Theme.outline
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: root.currentIcon ? root.currentIcon : I18n.tr("Choose icon")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: root.currentIcon ? Theme.surfaceText : Theme.outline
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
width: 160
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
}
|
||||
|
||||
DankIcon {
|
||||
name: iconPopup.visible ? "expand_less" : "expand_more"
|
||||
size: 16
|
||||
color: Theme.outline
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: Theme.spacingS
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Popup {
|
||||
id: iconPopup
|
||||
|
||||
parent: root.Overlay.overlay
|
||||
width: 320
|
||||
height: Math.min(500, dropdownContent.implicitHeight + 32)
|
||||
padding: 0
|
||||
modal: true
|
||||
dim: false
|
||||
closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside
|
||||
|
||||
background: Rectangle {
|
||||
color: "transparent"
|
||||
}
|
||||
|
||||
contentItem: Rectangle {
|
||||
id: contentSurface
|
||||
color: Theme.surface
|
||||
radius: Theme.cornerRadius
|
||||
|
||||
ElevationShadow {
|
||||
id: shadowLayer
|
||||
anchors.fill: parent
|
||||
z: -1
|
||||
level: Theme.elevationLevel2
|
||||
fallbackOffset: 4
|
||||
targetRadius: contentSurface.radius
|
||||
targetColor: contentSurface.color
|
||||
shadowOpacity: Theme.elevationLevel2 && Theme.elevationLevel2.alpha !== undefined ? Theme.elevationLevel2.alpha : 0.25
|
||||
shadowEnabled: Theme.elevationEnabled
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: 24
|
||||
height: 24
|
||||
radius: 12
|
||||
color: closeMouseArea.containsMouse ? Theme.errorHover : Theme.withAlpha(Theme.errorHover, 0)
|
||||
anchors.top: parent.top
|
||||
anchors.right: parent.right
|
||||
anchors.topMargin: Theme.spacingS
|
||||
anchors.rightMargin: Theme.spacingS
|
||||
z: 1
|
||||
|
||||
DankIcon {
|
||||
name: "close"
|
||||
size: 16
|
||||
color: closeMouseArea.containsMouse ? Theme.error : Theme.outline
|
||||
anchors.centerIn: parent
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: closeMouseArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: iconPopup.close()
|
||||
}
|
||||
}
|
||||
|
||||
DankFlickable {
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingS
|
||||
contentHeight: dropdownContent.height
|
||||
clip: true
|
||||
pressDelay: 0
|
||||
|
||||
Column {
|
||||
id: dropdownContent
|
||||
width: parent.width
|
||||
spacing: Theme.spacingM
|
||||
|
||||
Repeater {
|
||||
model: root.iconCategories
|
||||
|
||||
Column {
|
||||
required property var modelData
|
||||
width: parent.width
|
||||
spacing: Theme.spacingS
|
||||
|
||||
StyledText {
|
||||
text: modelData.name
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.weight: Font.Medium
|
||||
color: Theme.surfaceText
|
||||
}
|
||||
|
||||
Flow {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
Repeater {
|
||||
model: modelData.icons
|
||||
|
||||
Rectangle {
|
||||
required property string modelData
|
||||
width: 36
|
||||
height: 36
|
||||
radius: Theme.cornerRadius
|
||||
color: iconMouseArea.containsMouse ? Theme.primaryHover : Theme.withAlpha(Theme.primaryHover, 0)
|
||||
border.color: root.currentIcon === modelData ? Theme.primary : Theme.withAlpha(Theme.primary, 0)
|
||||
border.width: 2
|
||||
|
||||
DankIcon {
|
||||
name: parent.modelData
|
||||
size: 20
|
||||
color: root.currentIcon === parent.modelData ? Theme.primary : Theme.surfaceText
|
||||
anchors.centerIn: parent
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: iconMouseArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
root.iconSelected(parent.modelData, "icon");
|
||||
iconPopup.close();
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Theme.standardEasing
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function setIcon(iconName, type) {
|
||||
root.iconType = type;
|
||||
root.iconType = "icon";
|
||||
root.currentIcon = iconName;
|
||||
}
|
||||
}
|
||||
DankCommon.DankIconPicker {}
|
||||
|
||||
@@ -1,277 +1,3 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import qs.Common
|
||||
import qs.Widgets
|
||||
import "ScrollConstants.js" as Scroll
|
||||
import qs.DankCommon.Widgets as DankCommon
|
||||
|
||||
ListView {
|
||||
id: listView
|
||||
|
||||
property real scrollBarTopMargin: 0
|
||||
property real mouseWheelSpeed: Scroll.mouseWheelSpeed
|
||||
property real savedY: 0
|
||||
property bool justChanged: false
|
||||
property bool isUserScrolling: false
|
||||
property real momentumVelocity: 0
|
||||
property bool isMomentumActive: false
|
||||
property real friction: Scroll.friction
|
||||
|
||||
flickDeceleration: Scroll.flickDeceleration
|
||||
maximumFlickVelocity: Scroll.maximumFlickVelocity
|
||||
boundsBehavior: Flickable.StopAtBounds
|
||||
boundsMovement: Flickable.FollowBoundsBehavior
|
||||
pressDelay: 0
|
||||
flickableDirection: Flickable.VerticalFlick
|
||||
|
||||
add: ListViewTransitions.add
|
||||
remove: ListViewTransitions.remove
|
||||
displaced: ListViewTransitions.displaced
|
||||
move: ListViewTransitions.move
|
||||
|
||||
// QQmlDelegateModel can release a delegate back to its cache without hiding it,
|
||||
// leaving a stale row painted over live ones. Hide anything the view no longer claims.
|
||||
Connections {
|
||||
target: listView.model?.objectName !== undefined ? listView.model : null
|
||||
ignoreUnknownSignals: true
|
||||
function onRowsInserted() {
|
||||
orphanSweep.arm();
|
||||
}
|
||||
function onRowsRemoved() {
|
||||
orphanSweep.arm();
|
||||
}
|
||||
function onRowsMoved() {
|
||||
orphanSweep.arm();
|
||||
}
|
||||
function onModelReset() {
|
||||
orphanSweep.arm();
|
||||
}
|
||||
}
|
||||
|
||||
FrameAnimation {
|
||||
id: orphanSweep
|
||||
|
||||
property int frames: 0
|
||||
|
||||
function arm() {
|
||||
frames = 0;
|
||||
running = true;
|
||||
}
|
||||
|
||||
onTriggered: {
|
||||
const kids = listView.contentItem.children;
|
||||
const rows = [];
|
||||
for (let i = 0; i < kids.length; i++) {
|
||||
const c = kids[i];
|
||||
if (!c || c.index === undefined)
|
||||
continue;
|
||||
const claimed = listView.itemAtIndex(c.index) === c;
|
||||
if (claimed && !c.visible) {
|
||||
c.visible = true;
|
||||
continue;
|
||||
}
|
||||
if (c.visible)
|
||||
rows.push({
|
||||
item: c,
|
||||
claimed: claimed
|
||||
});
|
||||
}
|
||||
for (let a = 0; a < rows.length; a++) {
|
||||
if (rows[a].claimed)
|
||||
continue;
|
||||
for (let b = 0; b < rows.length; b++) {
|
||||
if (a === b || !rows[b].claimed)
|
||||
continue;
|
||||
if (Math.abs(rows[a].item.y - rows[b].item.y) < rows[b].item.height / 2) {
|
||||
rows[a].item.visible = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (++frames >= 3)
|
||||
running = false;
|
||||
}
|
||||
}
|
||||
|
||||
onMovementStarted: {
|
||||
isUserScrolling = true;
|
||||
vbar._scrollBarActive = true;
|
||||
vbar.hideTimer.stop();
|
||||
}
|
||||
onMovementEnded: {
|
||||
isUserScrolling = false;
|
||||
vbar.hideTimer.restart();
|
||||
}
|
||||
|
||||
onContentYChanged: {
|
||||
if (!justChanged && isUserScrolling) {
|
||||
savedY = contentY;
|
||||
}
|
||||
justChanged = false;
|
||||
}
|
||||
|
||||
onModelChanged: {
|
||||
justChanged = true;
|
||||
contentY = savedY;
|
||||
}
|
||||
|
||||
WheelHandler {
|
||||
id: wheelHandler
|
||||
property real touchpadSpeed: Scroll.touchpadSpeed
|
||||
property real lastWheelTime: 0
|
||||
property real momentum: 0
|
||||
property var velocitySamples: []
|
||||
property bool sessionUsedMouseWheel: false
|
||||
|
||||
function startMomentum() {
|
||||
isMomentumActive = true;
|
||||
momentumAnim.running = true;
|
||||
}
|
||||
|
||||
acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad
|
||||
|
||||
onWheel: event => {
|
||||
isUserScrolling = true;
|
||||
vbar._scrollBarActive = true;
|
||||
vbar.hideTimer.restart();
|
||||
|
||||
const currentTime = Date.now();
|
||||
const timeDelta = currentTime - lastWheelTime;
|
||||
lastWheelTime = currentTime;
|
||||
|
||||
const hasPixel = event.pixelDelta && event.pixelDelta.y !== 0;
|
||||
const deltaY = event.angleDelta.y;
|
||||
const isTraditionalMouse = !hasPixel && Math.abs(deltaY) >= 120 && (Math.abs(deltaY) % 120) === 0;
|
||||
const isHighDpiMouse = !hasPixel && !isTraditionalMouse && deltaY !== 0;
|
||||
const isTouchpad = hasPixel;
|
||||
|
||||
if (isTraditionalMouse) {
|
||||
sessionUsedMouseWheel = true;
|
||||
momentumAnim.running = false;
|
||||
isMomentumActive = false;
|
||||
velocitySamples = [];
|
||||
momentum = 0;
|
||||
momentumVelocity = 0;
|
||||
|
||||
const lines = Math.round(Math.abs(deltaY) / 120);
|
||||
const scrollAmount = (deltaY > 0 ? -lines : lines) * mouseWheelSpeed;
|
||||
let newY = listView.contentY + scrollAmount;
|
||||
const maxY = Math.max(0, listView.contentHeight - listView.height + listView.originY);
|
||||
newY = Math.max(listView.originY, Math.min(maxY, newY));
|
||||
|
||||
if (listView.flicking) {
|
||||
listView.cancelFlick();
|
||||
}
|
||||
|
||||
listView.contentY = newY;
|
||||
savedY = newY;
|
||||
} else if (isHighDpiMouse) {
|
||||
sessionUsedMouseWheel = true;
|
||||
momentumAnim.running = false;
|
||||
isMomentumActive = false;
|
||||
velocitySamples = [];
|
||||
momentum = 0;
|
||||
momentumVelocity = 0;
|
||||
|
||||
let delta = deltaY / 8 * touchpadSpeed;
|
||||
let newY = listView.contentY - delta;
|
||||
const maxY = Math.max(0, listView.contentHeight - listView.height + listView.originY);
|
||||
newY = Math.max(listView.originY, Math.min(maxY, newY));
|
||||
|
||||
if (listView.flicking) {
|
||||
listView.cancelFlick();
|
||||
}
|
||||
|
||||
listView.contentY = newY;
|
||||
savedY = newY;
|
||||
} else if (isTouchpad) {
|
||||
sessionUsedMouseWheel = false;
|
||||
momentumAnim.running = false;
|
||||
isMomentumActive = false;
|
||||
|
||||
let delta = event.pixelDelta.y * touchpadSpeed;
|
||||
|
||||
velocitySamples.push({
|
||||
"delta": delta,
|
||||
"time": currentTime
|
||||
});
|
||||
velocitySamples = velocitySamples.filter(s => currentTime - s.time < Scroll.velocitySampleWindowMs);
|
||||
|
||||
if (velocitySamples.length > 1) {
|
||||
const totalDelta = velocitySamples.reduce((sum, s) => sum + s.delta, 0);
|
||||
const timeSpan = currentTime - velocitySamples[0].time;
|
||||
if (timeSpan > 0) {
|
||||
momentumVelocity = Math.max(-Scroll.maxMomentumVelocity, Math.min(Scroll.maxMomentumVelocity, totalDelta / timeSpan * 1000));
|
||||
}
|
||||
}
|
||||
|
||||
if (timeDelta < Scroll.momentumTimeThreshold) {
|
||||
momentum = momentum * Scroll.momentumRetention + delta * Scroll.momentumDeltaFactor;
|
||||
delta += momentum;
|
||||
} else {
|
||||
momentum = 0;
|
||||
}
|
||||
|
||||
let newY = listView.contentY - delta;
|
||||
const maxY = Math.max(0, listView.contentHeight - listView.height + listView.originY);
|
||||
newY = Math.max(listView.originY, Math.min(maxY, newY));
|
||||
|
||||
if (listView.flicking) {
|
||||
listView.cancelFlick();
|
||||
}
|
||||
|
||||
listView.contentY = newY;
|
||||
savedY = newY;
|
||||
}
|
||||
|
||||
event.accepted = true;
|
||||
}
|
||||
|
||||
onActiveChanged: {
|
||||
if (!active) {
|
||||
isUserScrolling = false;
|
||||
if (!sessionUsedMouseWheel && Math.abs(momentumVelocity) >= Scroll.minMomentumVelocity) {
|
||||
startMomentum();
|
||||
} else {
|
||||
velocitySamples = [];
|
||||
momentumVelocity = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
FrameAnimation {
|
||||
id: momentumAnim
|
||||
running: false
|
||||
|
||||
onTriggered: {
|
||||
const dt = frameTime;
|
||||
const newY = contentY - momentumVelocity * dt;
|
||||
const maxY = Math.max(0, contentHeight - height + originY);
|
||||
const minY = originY;
|
||||
|
||||
if (newY < minY || newY > maxY) {
|
||||
contentY = newY < minY ? minY : maxY;
|
||||
savedY = contentY;
|
||||
running = false;
|
||||
isMomentumActive = false;
|
||||
momentumVelocity = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
contentY = newY;
|
||||
savedY = newY;
|
||||
momentumVelocity *= Math.pow(friction, dt / 0.016);
|
||||
|
||||
if (Math.abs(momentumVelocity) < Scroll.momentumStopThreshold) {
|
||||
running = false;
|
||||
isMomentumActive = false;
|
||||
momentumVelocity = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ScrollBar.vertical: DankScrollbar {
|
||||
id: vbar
|
||||
topPadding: listView.scrollBarTopMargin
|
||||
}
|
||||
}
|
||||
DankCommon.DankListView {}
|
||||
|
||||
@@ -1,271 +1,3 @@
|
||||
import QtQuick
|
||||
import qs.Common
|
||||
import qs.Widgets
|
||||
import qs.DankCommon.Widgets as DankCommon
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
activeFocusOnTab: true
|
||||
|
||||
KeyNavigation.tab: keyNavigationTab
|
||||
KeyNavigation.backtab: keyNavigationBacktab
|
||||
|
||||
onActiveFocusChanged: {
|
||||
if (activeFocus) {
|
||||
locationInput.forceActiveFocus();
|
||||
}
|
||||
}
|
||||
|
||||
property string currentLocation: ""
|
||||
property string placeholderText: I18n.tr("Search for a location...")
|
||||
property bool _internalChange: false
|
||||
property bool isLoading: false
|
||||
property string currentSearchText: ""
|
||||
property Item keyNavigationTab: null
|
||||
property Item keyNavigationBacktab: null
|
||||
|
||||
signal locationSelected(string displayName, string coordinates)
|
||||
|
||||
function resetSearchState() {
|
||||
locationSearchTimer.stop();
|
||||
dropdownHideTimer.stop();
|
||||
isLoading = false;
|
||||
searchResultsModel.clear();
|
||||
}
|
||||
|
||||
// CJK city names are commonly two code points (北京, 東京, 서울).
|
||||
function canSearch(t) {
|
||||
return t.length > 2 || (t.length >= 2 && /[-ヿ㐀-䶿一-鿿豈-가-]/.test(t));
|
||||
}
|
||||
|
||||
width: parent.width
|
||||
height: searchInputField.height + (searchDropdown.visible ? searchDropdown.height : 0)
|
||||
|
||||
ListModel {
|
||||
id: searchResultsModel
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: locationSearchTimer
|
||||
|
||||
interval: 500
|
||||
running: false
|
||||
repeat: false
|
||||
onTriggered: {
|
||||
if (root.canSearch(locationInput.text)) {
|
||||
searchResultsModel.clear();
|
||||
root.isLoading = true;
|
||||
const searchLocation = locationInput.text;
|
||||
root.currentSearchText = searchLocation;
|
||||
const encodedLocation = encodeURIComponent(searchLocation);
|
||||
const searchUrl = "https://nominatim.openstreetmap.org/search?q=" + encodedLocation + "&format=json&limit=5&addressdetails=1";
|
||||
Proc.runCommand("locationSearch", [Proc.dmsBin, "dl", "-4", "--timeout", "10", searchUrl], (output, exitCode) => {
|
||||
root.isLoading = false;
|
||||
if (exitCode !== 0) {
|
||||
searchResultsModel.clear();
|
||||
return;
|
||||
}
|
||||
if (root.currentSearchText !== locationInput.text)
|
||||
return;
|
||||
const raw = output.trim();
|
||||
searchResultsModel.clear();
|
||||
if (!raw || raw[0] !== "[") {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const data = JSON.parse(raw);
|
||||
if (data.length === 0) {
|
||||
return;
|
||||
}
|
||||
for (var i = 0; i < Math.min(data.length, 5); i++) {
|
||||
const location = data[i];
|
||||
if (location.display_name && location.lat && location.lon) {
|
||||
const parts = location.display_name.split(', ');
|
||||
let cleanName = parts[0];
|
||||
if (parts.length > 1) {
|
||||
const state = parts[parts.length - 2];
|
||||
if (state && state !== cleanName)
|
||||
cleanName += `, ${state}`;
|
||||
}
|
||||
const query = `${location.lat},${location.lon}`;
|
||||
searchResultsModel.append({
|
||||
"name": cleanName,
|
||||
"query": query
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (e) {}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: dropdownHideTimer
|
||||
|
||||
interval: 200
|
||||
running: false
|
||||
repeat: false
|
||||
onTriggered: {
|
||||
if (!locationInput.getActiveFocus() && !searchDropdown.hovered)
|
||||
root.resetSearchState();
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
id: searchInputField
|
||||
|
||||
width: parent.width
|
||||
height: 48
|
||||
|
||||
DankTextField {
|
||||
id: locationInput
|
||||
|
||||
width: parent.width
|
||||
height: parent.height
|
||||
leftIconName: "search"
|
||||
placeholderText: root.placeholderText
|
||||
text: ""
|
||||
backgroundColor: Theme.surfaceVariant
|
||||
normalBorderColor: Theme.primarySelected
|
||||
focusedBorderColor: Theme.primary
|
||||
keyNavigationTab: root.keyNavigationTab
|
||||
keyNavigationBacktab: root.keyNavigationBacktab
|
||||
onTextEdited: {
|
||||
if (root._internalChange)
|
||||
return;
|
||||
if (getActiveFocus()) {
|
||||
if (root.canSearch(text)) {
|
||||
root.isLoading = true;
|
||||
locationSearchTimer.restart();
|
||||
} else {
|
||||
root.resetSearchState();
|
||||
}
|
||||
}
|
||||
}
|
||||
onFocusStateChanged: hasFocus => {
|
||||
if (hasFocus) {
|
||||
dropdownHideTimer.stop();
|
||||
} else {
|
||||
dropdownHideTimer.start();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DankIcon {
|
||||
name: root.isLoading ? "hourglass_empty" : (searchResultsModel.count > 0 ? "check_circle" : "error")
|
||||
size: Theme.iconSize - 4
|
||||
color: root.isLoading ? Theme.surfaceVariantText : (searchResultsModel.count > 0 ? Theme.primary : Theme.error)
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: Theme.spacingM
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
opacity: (locationInput.getActiveFocus() && root.canSearch(locationInput.text)) ? 1 : 0
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Theme.standardEasing
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StyledRect {
|
||||
id: searchDropdown
|
||||
|
||||
property bool hovered: false
|
||||
|
||||
width: parent.width
|
||||
height: Math.min(Math.max(searchResultsModel.count * 38 + Theme.spacingS * 2, 50), 200)
|
||||
y: searchInputField.height
|
||||
radius: Theme.cornerRadius
|
||||
color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
|
||||
border.color: Theme.primarySelected
|
||||
border.width: 1
|
||||
visible: locationInput.getActiveFocus() && root.canSearch(locationInput.text) && (searchResultsModel.count > 0 || root.isLoading)
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
onEntered: {
|
||||
parent.hovered = true;
|
||||
dropdownHideTimer.stop();
|
||||
}
|
||||
onExited: {
|
||||
parent.hovered = false;
|
||||
if (!locationInput.getActiveFocus())
|
||||
dropdownHideTimer.start();
|
||||
}
|
||||
acceptedButtons: Qt.NoButton
|
||||
}
|
||||
|
||||
Item {
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingS
|
||||
|
||||
DankListView {
|
||||
id: searchResultsList
|
||||
|
||||
anchors.fill: parent
|
||||
clip: true
|
||||
model: searchResultsModel
|
||||
spacing: Theme.spacingXXS
|
||||
|
||||
delegate: StyledRect {
|
||||
width: searchResultsList.width
|
||||
height: 36
|
||||
radius: Theme.cornerRadius
|
||||
color: resultMouseArea.containsMouse ? Theme.surfaceLight : Theme.withAlpha(Theme.surfaceLight, 0)
|
||||
|
||||
Row {
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingM
|
||||
spacing: Theme.spacingS
|
||||
|
||||
DankIcon {
|
||||
name: "place"
|
||||
size: Theme.iconSize - 6
|
||||
color: Theme.surfaceVariantText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: model.name || "Unknown"
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
elide: Text.ElideRight
|
||||
width: parent.width - 30
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: resultMouseArea
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
root._internalChange = true;
|
||||
const selectedName = model.name;
|
||||
const selectedQuery = model.query;
|
||||
locationInput.text = selectedName;
|
||||
root.locationSelected(selectedName, selectedQuery);
|
||||
root.resetSearchState();
|
||||
locationInput.setFocus(false);
|
||||
root._internalChange = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
anchors.centerIn: parent
|
||||
text: root.isLoading ? "Searching..." : "No locations found"
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceVariantText
|
||||
visible: searchResultsList.count === 0 && root.canSearch(locationInput.text)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
DankCommon.DankLocationSearch {}
|
||||
|
||||
@@ -1,164 +1,3 @@
|
||||
import QtQuick
|
||||
import qs.Common
|
||||
import qs.DankCommon.Widgets as DankCommon
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property string name: ""
|
||||
property int size: Theme.fontSizeMedium
|
||||
property alias color: icon.color
|
||||
|
||||
width: size
|
||||
height: size
|
||||
visible: text.length > 0
|
||||
|
||||
// This is for file browser, particularly - might want another map later for app IDs
|
||||
readonly property var iconMap: ({
|
||||
// --- Distribution logos ---
|
||||
"debian": "\u{f08da}",
|
||||
"arch": "\u{f08c7}",
|
||||
"archcraft": "\u{f345}",
|
||||
"guix": "\u{f325}",
|
||||
"fedora": "\u{f08db}",
|
||||
"nixos": "\u{f1105}",
|
||||
"ubuntu": "\u{f0548}",
|
||||
"gentoo": "\u{f08e8}",
|
||||
"endeavouros": "\u{f322}",
|
||||
"manjaro": "\u{f160a}",
|
||||
"opensuse": "\u{f314}",
|
||||
"artix": "\u{f31f}",
|
||||
"void": "\u{f32e}",
|
||||
|
||||
// --- special types ---
|
||||
"folder": "\u{F024B}",
|
||||
"file": "\u{F0214}",
|
||||
|
||||
// --- special filenames (no extension) ---
|
||||
"docker": "\u{F0868}",
|
||||
"makefile": "\u{F09EE}",
|
||||
"license": "\u{F09EE}",
|
||||
"readme": "\u{F0354}",
|
||||
|
||||
// --- programming languages ---
|
||||
"rs": "\u{F1617}",
|
||||
"dart": "\u{e798}",
|
||||
"go": "\u{F07D3}",
|
||||
"py": "\u{F0320}",
|
||||
"js": "\u{F031E}",
|
||||
"jsx": "\u{F031E}",
|
||||
"ts": "\u{F06E6}",
|
||||
"tsx": "\u{F06E6}",
|
||||
"java": "\u{F0B37}",
|
||||
"c": "\u{F0671}",
|
||||
"cpp": "\u{F0672}",
|
||||
"cxx": "\u{F0672}",
|
||||
"h": "\u{F0672}",
|
||||
"hpp": "\u{F0672}",
|
||||
"cs": "\u{F031B}",
|
||||
"html": "\u{e60e}",
|
||||
"htm": "\u{e60e}",
|
||||
"css": "\u{E6b8}",
|
||||
"scss": "\u{F031C}",
|
||||
"less": "\u{F031C}",
|
||||
"md": "\u{F0354}",
|
||||
"markdown": "\u{F0354}",
|
||||
"json": "\u{eb0f}",
|
||||
"jsonc": "\u{eb0f}",
|
||||
"yaml": "\u{e8eb}",
|
||||
"yml": "\u{e8eb}",
|
||||
"xml": "\u{F09EE}",
|
||||
"sql": "\u{f1c0}",
|
||||
|
||||
// --- scripts / shells ---
|
||||
"sh": "\u{f0bc1}",
|
||||
"bash": "\u{f0bc1}",
|
||||
"zsh": "\u{f0bc1}",
|
||||
"fish": "\u{f0bc1}",
|
||||
"ps1": "\u{f0bc1}",
|
||||
"bat": "\u{f0bc1}",
|
||||
|
||||
// --- data / config ---
|
||||
"toml": "\u{e6b2}",
|
||||
"ini": "\u{F09EE}",
|
||||
"conf": "\u{F09EE}",
|
||||
"cfg": "\u{F09EE}",
|
||||
"csv": "\u{eefc}",
|
||||
"tsv": "\u{F021C}",
|
||||
|
||||
// --- docs / office ---
|
||||
"pdf": "\u{F0226}",
|
||||
"doc": "\u{F09EE}",
|
||||
"docx": "\u{F09EE}",
|
||||
"rtf": "\u{F09EE}",
|
||||
"ppt": "\u{F09EE}",
|
||||
"pptx": "\u{F09EE}",
|
||||
"log": "\u{F09EE}",
|
||||
"xls": "\u{F021C}",
|
||||
"xlsx": "\u{F021C}",
|
||||
|
||||
// --- images ---
|
||||
"ico": "\u{F021F}",
|
||||
|
||||
// --- audio / video ---
|
||||
"mp3": "\u{e638}",
|
||||
"wav": "\u{e638}",
|
||||
"flac": "\u{e638}",
|
||||
"ogg": "\u{e638}",
|
||||
"mp4": "\u{f0567}",
|
||||
"mkv": "\u{f0567}",
|
||||
"webm": "\u{f0567}",
|
||||
"mov": "\u{f0567}",
|
||||
|
||||
// --- archives / packages ---
|
||||
"zip": "\u{e6aa}",
|
||||
"tar": "\u{f003c}",
|
||||
"gz": "\u{f003c}",
|
||||
"bz2": "\u{f003c}",
|
||||
"7z": "\u{f003c}",
|
||||
|
||||
// --- containers / infra / cloud ---
|
||||
"dockerfile": "\u{F0868}",
|
||||
"yml.k8s": "\u{F09EE}",
|
||||
"yaml.k8s": "\u{F09EE}",
|
||||
"tf": "\u{F09EE}",
|
||||
"tfvars": "\u{F09EE}",
|
||||
|
||||
// --- moon phases
|
||||
"moon_new": "\u{F0F64}",
|
||||
"moon_waxing_crescent": "\u{F0F67}",
|
||||
"moon_first_quarter": "\u{F0F61}",
|
||||
"moon_waxing_gibbous": "\u{F0F68}",
|
||||
"moon_full": "\u{F0F62}",
|
||||
"moon_waning_gibbous": "\u{F0F66}",
|
||||
"moon_last_quarter": "\u{F0F63}",
|
||||
"moon_waning_crescent": "\u{F0F65}"
|
||||
})
|
||||
|
||||
readonly property string text: iconMap[name] || iconMap["file"] || ""
|
||||
|
||||
function getIconForFile(fileName) {
|
||||
const lowerName = fileName.toLowerCase();
|
||||
if (lowerName.startsWith("dockerfile")) {
|
||||
return "docker";
|
||||
}
|
||||
const ext = fileName.split('.').pop();
|
||||
return ext || "";
|
||||
}
|
||||
|
||||
FontLoader {
|
||||
id: firaCodeFont
|
||||
source: Qt.resolvedUrl("../assets/fonts/nerd-fonts/FiraCodeNerdFont-Regular.ttf")
|
||||
}
|
||||
|
||||
StyledText {
|
||||
id: icon
|
||||
|
||||
anchors.centerIn: parent
|
||||
|
||||
font.family: firaCodeFont.name
|
||||
font.pixelSize: root.size
|
||||
color: Theme.surfaceText
|
||||
text: root.text
|
||||
antialiasing: true
|
||||
}
|
||||
}
|
||||
DankCommon.DankNFIcon {}
|
||||
|
||||
@@ -1,73 +1,3 @@
|
||||
import QtQuick
|
||||
import qs.Common
|
||||
import qs.DankCommon.Widgets as DankCommon
|
||||
|
||||
Column {
|
||||
id: root
|
||||
property string text: ""
|
||||
property var incrementTooltipText: ""
|
||||
property var decrementTooltipText: ""
|
||||
property var onIncrement: undefined
|
||||
property var onDecrement: undefined
|
||||
property string incrementIconName: "keyboard_arrow_up"
|
||||
property string decrementIconName: "keyboard_arrow_down"
|
||||
|
||||
property bool incrementEnabled: true
|
||||
property bool decrementEnabled: true
|
||||
|
||||
property color textColor: Theme.surfaceText
|
||||
property color iconColor: Theme.withAlpha(Theme.surfaceText, 0.5)
|
||||
property color backgroundColor: Theme.primary
|
||||
|
||||
property int textSize: Theme.fontSizeSmall
|
||||
property var iconSize: 12
|
||||
property int buttonSize: 20
|
||||
property int horizontalPadding: Theme.spacingL
|
||||
|
||||
readonly property bool effectiveIncrementEnabled: root.onIncrement ? root.incrementEnabled : false
|
||||
readonly property bool effectiveDecrementEnabled: root.onDecrement ? root.decrementEnabled : false
|
||||
|
||||
width: Math.max(buttonSize * 2, root.implicitWidth + horizontalPadding * 2)
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
DankActionButton {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
enabled: root.effectiveIncrementEnabled
|
||||
iconColor: root.effectiveIncrementEnabled ? root.iconColor : Theme.blendAlpha(root.iconColor, 0.5)
|
||||
iconSize: root.iconSize
|
||||
buttonSize: root.buttonSize
|
||||
iconName: root.incrementIconName
|
||||
onClicked: if (typeof root.onIncrement === 'function')
|
||||
root.onIncrement()
|
||||
tooltipText: root.incrementTooltipText
|
||||
}
|
||||
|
||||
Row {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
Item {
|
||||
width: 5
|
||||
height: 1
|
||||
}
|
||||
StyledText {
|
||||
isMonospace: true
|
||||
text: root.text
|
||||
font.pixelSize: root.textSize
|
||||
color: root.textColor
|
||||
}
|
||||
Item {
|
||||
width: 5
|
||||
height: 1
|
||||
}
|
||||
}
|
||||
|
||||
DankActionButton {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
enabled: root.effectiveDecrementEnabled
|
||||
iconColor: root.effectiveDecrementEnabled ? root.iconColor : Theme.blendAlpha(root.iconColor, 0.5)
|
||||
iconSize: root.iconSize
|
||||
buttonSize: root.buttonSize
|
||||
iconName: root.decrementIconName
|
||||
onClicked: if (typeof root.onDecrement === 'function')
|
||||
root.onDecrement()
|
||||
tooltipText: root.decrementTooltipText
|
||||
}
|
||||
}
|
||||
DankCommon.DankNumberStepper {}
|
||||
|
||||
@@ -1,105 +1,3 @@
|
||||
import QtQuick
|
||||
import qs.Common
|
||||
import qs.DankCommon.Widgets as DankCommon
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property color rippleColor: Theme.primary
|
||||
property real cornerRadius: 0
|
||||
property bool enableRipple: typeof SettingsData !== "undefined" ? (SettingsData.enableRippleEffects ?? true) : true
|
||||
property int animationDuration: Theme.expressiveDurations.expressiveDefaultSpatial
|
||||
|
||||
property real _rippleX: 0
|
||||
property real _rippleY: 0
|
||||
property real _rippleMaxRadius: 0
|
||||
readonly property alias animating: rippleAnim.running
|
||||
|
||||
anchors.fill: parent
|
||||
|
||||
function trigger(x, y) {
|
||||
if (!enableRipple || Theme.currentAnimationSpeed === SettingsData.AnimationSpeed.None)
|
||||
return;
|
||||
|
||||
_rippleX = x;
|
||||
_rippleY = y;
|
||||
|
||||
const dist = (ox, oy) => ox * ox + oy * oy;
|
||||
_rippleMaxRadius = Math.sqrt(Math.max(dist(x, y), dist(x, height - y), dist(width - x, y), dist(width - x, height - y)));
|
||||
|
||||
rippleAnim.restart();
|
||||
}
|
||||
|
||||
SequentialAnimation {
|
||||
id: rippleAnim
|
||||
|
||||
PropertyAction {
|
||||
target: rippleFx
|
||||
property: "rippleCenterX"
|
||||
value: root._rippleX
|
||||
}
|
||||
PropertyAction {
|
||||
target: rippleFx
|
||||
property: "rippleCenterY"
|
||||
value: root._rippleY
|
||||
}
|
||||
PropertyAction {
|
||||
target: rippleFx
|
||||
property: "rippleRadius"
|
||||
value: 0
|
||||
}
|
||||
PropertyAction {
|
||||
target: rippleFx
|
||||
property: "rippleOpacity"
|
||||
value: 0.10
|
||||
}
|
||||
|
||||
ParallelAnimation {
|
||||
DankAnim {
|
||||
target: rippleFx
|
||||
property: "rippleRadius"
|
||||
from: 0
|
||||
to: root._rippleMaxRadius
|
||||
duration: root.animationDuration
|
||||
easing.bezierCurve: Theme.expressiveCurves.standardDecel
|
||||
}
|
||||
SequentialAnimation {
|
||||
PauseAnimation {
|
||||
duration: Math.round(root.animationDuration * 0.6)
|
||||
}
|
||||
DankAnim {
|
||||
target: rippleFx
|
||||
property: "rippleOpacity"
|
||||
to: 0
|
||||
duration: root.animationDuration
|
||||
easing.bezierCurve: Theme.expressiveCurves.standard
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ShaderEffect {
|
||||
id: rippleFx
|
||||
visible: rippleAnim.running
|
||||
|
||||
property real rippleCenterX: 0
|
||||
property real rippleCenterY: 0
|
||||
property real rippleRadius: 0
|
||||
property real rippleOpacity: 0
|
||||
|
||||
x: Math.max(0, rippleCenterX - rippleRadius)
|
||||
y: Math.max(0, rippleCenterY - rippleRadius)
|
||||
width: Math.max(0, Math.min(root.width, rippleCenterX + rippleRadius) - x)
|
||||
height: Math.max(0, Math.min(root.height, rippleCenterY + rippleRadius) - y)
|
||||
|
||||
property real widthPx: width
|
||||
property real heightPx: height
|
||||
property real cornerRadiusPx: root.cornerRadius
|
||||
property real offsetX: x
|
||||
property real offsetY: y
|
||||
property real parentWidth: root.width
|
||||
property real parentHeight: root.height
|
||||
property vector4d rippleCol: Qt.vector4d(root.rippleColor.r, root.rippleColor.g, root.rippleColor.b, root.rippleColor.a)
|
||||
|
||||
fragmentShader: Qt.resolvedUrl("../Shaders/qsb/ripple.frag.qsb")
|
||||
}
|
||||
}
|
||||
DankCommon.DankRipple {}
|
||||
|
||||
@@ -1,81 +1,3 @@
|
||||
import QtQuick
|
||||
import QtQuick.Effects
|
||||
import Quickshell.Widgets
|
||||
import qs.Common
|
||||
import qs.DankCommon.Widgets as DankCommon
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
required property string source
|
||||
property int size: 24
|
||||
property string cornerIcon: ""
|
||||
property int cornerIconSize: Math.max(10, size * 0.4)
|
||||
property color cornerIconColor: Theme.surfaceText
|
||||
property color cornerIconBackground: Theme.surface
|
||||
property color colorOverride: "transparent"
|
||||
property real brightnessOverride: 0.0
|
||||
property real contrastOverride: 0.0
|
||||
property real saturationOverride: 0.0
|
||||
|
||||
readonly property bool hasCornerIcon: cornerIcon !== ""
|
||||
readonly property bool hasColorOverride: colorOverride.a > 0
|
||||
readonly property bool hasColorEffect: hasColorOverride || brightnessOverride !== 0.0 || contrastOverride !== 0.0 || saturationOverride !== 0.0
|
||||
readonly property string resolvedSource: {
|
||||
if (!source)
|
||||
return "";
|
||||
if (source.startsWith("file://"))
|
||||
return source;
|
||||
if (source.startsWith("/"))
|
||||
return "file://" + source;
|
||||
if (source.startsWith("qrc:"))
|
||||
return source;
|
||||
return source;
|
||||
}
|
||||
|
||||
implicitWidth: size
|
||||
implicitHeight: size
|
||||
|
||||
IconImage {
|
||||
id: iconImage
|
||||
anchors.fill: parent
|
||||
source: root.resolvedSource
|
||||
smooth: true
|
||||
mipmap: true
|
||||
asynchronous: true
|
||||
implicitSize: root.size * 2
|
||||
backer.sourceSize.width: root.size * 2
|
||||
backer.sourceSize.height: root.size * 2
|
||||
backer.cache: true
|
||||
layer.enabled: root.hasColorEffect
|
||||
layer.smooth: true
|
||||
layer.mipmap: true
|
||||
layer.effect: MultiEffect {
|
||||
saturation: root.saturationOverride
|
||||
colorization: root.hasColorOverride ? 1 : 0
|
||||
colorizationColor: root.colorOverride
|
||||
brightness: root.brightnessOverride
|
||||
contrast: root.contrastOverride
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
visible: root.hasCornerIcon
|
||||
anchors.right: parent.right
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.rightMargin: -2
|
||||
anchors.bottomMargin: -2
|
||||
width: root.cornerIconSize + 4
|
||||
height: root.cornerIconSize + 4
|
||||
radius: width / 2
|
||||
color: root.cornerIconBackground
|
||||
border.width: 1
|
||||
border.color: Theme.surfaceLight
|
||||
|
||||
DankIcon {
|
||||
anchors.centerIn: parent
|
||||
name: root.cornerIcon
|
||||
size: root.cornerIconSize
|
||||
color: root.cornerIconColor
|
||||
}
|
||||
}
|
||||
}
|
||||
DankCommon.DankSVGIcon {}
|
||||
|
||||
@@ -1,46 +1,3 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import qs.Common
|
||||
import qs.DankCommon.Widgets as DankCommon
|
||||
|
||||
ScrollBar {
|
||||
id: scrollbar
|
||||
|
||||
property var targetFlickable: null
|
||||
readonly property var _target: targetFlickable ?? parent
|
||||
|
||||
property bool _scrollBarActive: false
|
||||
property alias hideTimer: hideScrollBarTimer
|
||||
property bool _isParentMoving: _target && (_target.moving || _target.flicking || _target.isMomentumActive)
|
||||
property bool _shouldShow: pressed || hovered || active || _isParentMoving || _scrollBarActive
|
||||
|
||||
policy: (_target && _target.contentHeight > _target.height) ? ScrollBar.AsNeeded : ScrollBar.AlwaysOff
|
||||
minimumSize: 0.08
|
||||
implicitWidth: 10
|
||||
interactive: true
|
||||
hoverEnabled: true
|
||||
z: 1000
|
||||
opacity: (policy !== ScrollBar.AlwaysOff && _shouldShow) ? 1.0 : 0.0
|
||||
visible: policy !== ScrollBar.AlwaysOff
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: 160
|
||||
easing.type: Easing.OutQuad
|
||||
}
|
||||
}
|
||||
|
||||
contentItem: Rectangle {
|
||||
implicitWidth: 6
|
||||
radius: width / 2
|
||||
color: scrollbar.pressed ? Theme.primary : scrollbar._shouldShow ? Theme.outline : Theme.outlineMedium
|
||||
opacity: scrollbar.pressed ? 1.0 : scrollbar._shouldShow ? 1.0 : 0.6
|
||||
}
|
||||
|
||||
background: Item {}
|
||||
|
||||
Timer {
|
||||
id: hideScrollBarTimer
|
||||
interval: 1200
|
||||
onTriggered: scrollbar._scrollBarActive = false
|
||||
}
|
||||
}
|
||||
DankCommon.DankScrollbar {}
|
||||
|
||||
@@ -1,308 +1,3 @@
|
||||
import QtQuick
|
||||
import qs.Common
|
||||
import qs.Widgets
|
||||
import qs.DankCommon.Widgets as DankCommon
|
||||
|
||||
Item {
|
||||
id: slider
|
||||
|
||||
function checkParentDisablesTransparency() {
|
||||
let p = parent;
|
||||
while (p) {
|
||||
if (p.disablePopupTransparency === true)
|
||||
return true;
|
||||
p = p.parent;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
property int value: 50
|
||||
property int minimum: 0
|
||||
property int maximum: 100
|
||||
property int step: 1
|
||||
property string leftIcon: ""
|
||||
property string rightIcon: ""
|
||||
property string unit: "%"
|
||||
property bool showValue: true
|
||||
property bool isDragging: false
|
||||
property bool wheelEnabled: true
|
||||
property bool centerMinimum: false
|
||||
property real valueOverride: -1
|
||||
property bool alwaysShowValue: false
|
||||
readonly property bool containsMouse: sliderMouseArea.containsMouse
|
||||
|
||||
property color thumbOutlineColor: Theme.surfaceContainer
|
||||
property color trackColor: enabled ? Theme.outline : Theme.outline
|
||||
property bool usePopupTransparency: !checkParentDisablesTransparency()
|
||||
property real trackOpacity: usePopupTransparency ? Theme.popupTransparency : 1.0
|
||||
|
||||
signal sliderValueChanged(int newValue)
|
||||
signal sliderDragFinished(int finalValue)
|
||||
|
||||
height: 48
|
||||
|
||||
function updateValueFromPosition(x) {
|
||||
let ratio = Math.max(0, Math.min(1, (x - sliderHandle.width / 2) / (sliderTrack.width - sliderHandle.width)));
|
||||
if (centerMinimum)
|
||||
ratio = Math.max(0, (ratio - 0.5) * 2);
|
||||
let rawValue = minimum + ratio * (maximum - minimum);
|
||||
let newValue = step > 1 ? Math.round(rawValue / step) * step : Math.round(rawValue);
|
||||
newValue = Math.max(minimum, Math.min(maximum, newValue));
|
||||
if (newValue !== value) {
|
||||
value = newValue;
|
||||
sliderValueChanged(newValue);
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
anchors.centerIn: parent
|
||||
width: parent.width
|
||||
spacing: Theme.spacingM
|
||||
|
||||
DankIcon {
|
||||
name: slider.leftIcon
|
||||
size: Theme.iconSize
|
||||
color: slider.enabled ? Theme.surfaceText : Theme.onSurface_38
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
visible: slider.leftIcon.length > 0
|
||||
}
|
||||
|
||||
StyledRect {
|
||||
id: sliderTrack
|
||||
|
||||
property int leftIconWidth: slider.leftIcon.length > 0 ? Theme.iconSize : 0
|
||||
property int rightIconWidth: slider.rightIcon.length > 0 ? Theme.iconSize : 0
|
||||
|
||||
width: parent.width - (leftIconWidth + rightIconWidth + (slider.leftIcon.length > 0 ? Theme.spacingM : 0) + (slider.rightIcon.length > 0 ? Theme.spacingM : 0))
|
||||
height: 12
|
||||
radius: Theme.cornerRadius
|
||||
color: Theme.withAlpha(slider.trackColor, slider.trackOpacity)
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
clip: false
|
||||
|
||||
StyledRect {
|
||||
id: sliderFill
|
||||
height: parent.height
|
||||
radius: Theme.cornerRadius
|
||||
topRightRadius: 0
|
||||
bottomRightRadius: 0
|
||||
width: {
|
||||
const range = slider.maximum - slider.minimum;
|
||||
const rawRatio = range === 0 ? 0 : (slider.value - slider.minimum) / range;
|
||||
const ratio = slider.centerMinimum ? (0.5 + rawRatio * 0.5) : rawRatio;
|
||||
const travel = sliderTrack.width - sliderHandle.width;
|
||||
const handleLeft = travel * ratio;
|
||||
const endPoint = handleLeft - 3;
|
||||
return Math.max(0, Math.min(sliderTrack.width, endPoint));
|
||||
}
|
||||
color: slider.enabled ? Theme.primary : Theme.withAlpha(Theme.onSurface, 0.12)
|
||||
}
|
||||
|
||||
StyledRect {
|
||||
id: sliderHandle
|
||||
|
||||
property bool active: sliderMouseArea.containsMouse || sliderMouseArea.pressed || slider.isDragging
|
||||
|
||||
width: 4
|
||||
height: 20
|
||||
radius: Theme.cornerRadius
|
||||
x: {
|
||||
const range = slider.maximum - slider.minimum;
|
||||
const rawRatio = range === 0 ? 0 : (slider.value - slider.minimum) / range;
|
||||
const ratio = slider.centerMinimum ? (0.5 + rawRatio * 0.5) : rawRatio;
|
||||
const travel = sliderTrack.width - width;
|
||||
return Math.max(0, Math.min(travel, travel * ratio));
|
||||
}
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
color: slider.enabled ? Theme.primary : Theme.withAlpha(Theme.onSurface, 0.12)
|
||||
border.width: 0
|
||||
border.color: slider.thumbOutlineColor
|
||||
|
||||
StyledRect {
|
||||
anchors.fill: parent
|
||||
radius: Theme.cornerRadius
|
||||
color: Theme.onPrimary
|
||||
opacity: slider.enabled ? (sliderMouseArea.pressed ? 0.16 : (sliderMouseArea.containsMouse ? 0.08 : 0)) : 0
|
||||
visible: opacity > 0
|
||||
}
|
||||
|
||||
StyledRect {
|
||||
anchors.centerIn: parent
|
||||
width: parent.width + 20
|
||||
height: parent.height + 20
|
||||
radius: width / 2
|
||||
color: "transparent"
|
||||
border.width: 2
|
||||
border.color: Theme.primary
|
||||
opacity: slider.enabled && slider.focus ? 0.3 : 0
|
||||
visible: opacity > 0
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: ripple
|
||||
anchors.centerIn: parent
|
||||
width: 0
|
||||
height: 0
|
||||
radius: width / 2
|
||||
color: Theme.onPrimary
|
||||
opacity: 0
|
||||
|
||||
function start() {
|
||||
opacity = 0.16;
|
||||
width = 0;
|
||||
height = 0;
|
||||
rippleAnimation.start();
|
||||
}
|
||||
|
||||
SequentialAnimation {
|
||||
id: rippleAnimation
|
||||
NumberAnimation {
|
||||
target: ripple
|
||||
properties: "width,height"
|
||||
to: 28
|
||||
duration: 180
|
||||
}
|
||||
NumberAnimation {
|
||||
target: ripple
|
||||
property: "opacity"
|
||||
to: 0
|
||||
duration: 150
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TapHandler {
|
||||
acceptedButtons: Qt.LeftButton
|
||||
onPressedChanged: {
|
||||
if (pressed && slider.enabled) {
|
||||
ripple.start();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
scale: active ? 1.05 : 1.0
|
||||
|
||||
Behavior on scale {
|
||||
NumberAnimation {
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Theme.standardEasing
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
id: sliderContainer
|
||||
|
||||
anchors.fill: parent
|
||||
|
||||
MouseArea {
|
||||
id: sliderMouseArea
|
||||
|
||||
property bool isDragging: false
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.topMargin: -10
|
||||
anchors.bottomMargin: -10
|
||||
hoverEnabled: true
|
||||
cursorShape: enabled ? Qt.PointingHandCursor : Qt.ArrowCursor
|
||||
enabled: slider.enabled
|
||||
preventStealing: true
|
||||
acceptedButtons: Qt.LeftButton
|
||||
onWheel: wheelEvent => {
|
||||
if (!slider.wheelEnabled) {
|
||||
wheelEvent.accepted = false;
|
||||
return;
|
||||
}
|
||||
let wheelStep = slider.step > 1 ? slider.step : Math.max(1, (maximum - minimum) / 100);
|
||||
let newValue = wheelEvent.angleDelta.y > 0 ? Math.min(maximum, value + wheelStep) : Math.max(minimum, value - wheelStep);
|
||||
if (slider.step > 1)
|
||||
newValue = Math.round(newValue / slider.step) * slider.step;
|
||||
newValue = Math.round(newValue);
|
||||
if (newValue !== value) {
|
||||
value = newValue;
|
||||
sliderValueChanged(newValue);
|
||||
}
|
||||
wheelEvent.accepted = true;
|
||||
}
|
||||
onPressed: mouse => {
|
||||
if (slider.enabled) {
|
||||
slider.isDragging = true;
|
||||
sliderMouseArea.isDragging = true;
|
||||
updateValueFromPosition(mouse.x);
|
||||
}
|
||||
}
|
||||
onReleased: {
|
||||
if (slider.enabled) {
|
||||
slider.isDragging = false;
|
||||
sliderMouseArea.isDragging = false;
|
||||
slider.sliderDragFinished(slider.value);
|
||||
}
|
||||
}
|
||||
onPositionChanged: mouse => {
|
||||
if (pressed && slider.isDragging && slider.enabled) {
|
||||
updateValueFromPosition(mouse.x);
|
||||
}
|
||||
}
|
||||
onClicked: mouse => {
|
||||
if (slider.enabled && !slider.isDragging) {
|
||||
updateValueFromPosition(mouse.x);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StyledRect {
|
||||
id: valueTooltip
|
||||
|
||||
width: tooltipText.reservedWidth + Theme.spacingS * 2
|
||||
height: tooltipText.contentHeight + Theme.spacingXS * 2
|
||||
radius: Theme.cornerRadius
|
||||
color: Theme.surfaceContainer
|
||||
border.color: Theme.outline
|
||||
border.width: 1
|
||||
anchors.bottom: parent.top
|
||||
anchors.bottomMargin: Theme.spacingM
|
||||
x: Math.max(0, Math.min(parent.width - width, sliderHandle.x + sliderHandle.width / 2 - width / 2))
|
||||
visible: slider.alwaysShowValue ? slider.showValue : ((sliderMouseArea.containsMouse && slider.showValue) || (slider.isDragging && slider.showValue))
|
||||
opacity: visible ? 1 : 0
|
||||
|
||||
NumericText {
|
||||
id: tooltipText
|
||||
|
||||
text: (slider.valueOverride >= 0 ? Math.round(slider.valueOverride) : slider.value) + slider.unit
|
||||
reserveText: {
|
||||
let widest = "";
|
||||
const samples = [slider.minimum, slider.maximum];
|
||||
if (slider.valueOverride >= 0)
|
||||
samples.push(slider.valueOverride);
|
||||
for (let i = 0; i < samples.length; i++) {
|
||||
const candidate = Math.round(samples[i]) + slider.unit;
|
||||
if (candidate.length > widest.length)
|
||||
widest = candidate;
|
||||
}
|
||||
return widest;
|
||||
}
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
anchors.centerIn: parent
|
||||
font.hintingPreference: Font.PreferFullHinting
|
||||
}
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Theme.standardEasing
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DankIcon {
|
||||
name: slider.rightIcon
|
||||
size: Theme.iconSize
|
||||
color: slider.enabled ? Theme.surfaceText : Theme.onSurface_38
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
visible: slider.rightIcon.length > 0
|
||||
}
|
||||
}
|
||||
}
|
||||
DankCommon.DankSlider {}
|
||||
|
||||
@@ -1,108 +1,3 @@
|
||||
import QtQuick
|
||||
import QtQuick.Shapes
|
||||
import qs.Common
|
||||
import qs.DankCommon.Widgets as DankCommon
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property real size: 48
|
||||
property real strokeWidth: Math.max(2, size / 12)
|
||||
property color color: Theme.primary
|
||||
property bool running: visible
|
||||
|
||||
implicitWidth: size
|
||||
implicitHeight: size
|
||||
|
||||
onRunningChanged: {
|
||||
if (running)
|
||||
return;
|
||||
arc.rotation = 0;
|
||||
arc.startAngle = 0;
|
||||
arc.sweepAngle = 16;
|
||||
}
|
||||
|
||||
Item {
|
||||
id: rotator
|
||||
|
||||
anchors.fill: parent
|
||||
|
||||
RotationAnimator on rotation {
|
||||
from: 0
|
||||
to: 360
|
||||
duration: 1568
|
||||
loops: Animation.Infinite
|
||||
running: root.running
|
||||
}
|
||||
|
||||
Shape {
|
||||
id: arc
|
||||
|
||||
property real startAngle: 0
|
||||
property real sweepAngle: 16
|
||||
|
||||
anchors.fill: parent
|
||||
antialiasing: true
|
||||
preferredRendererType: Shape.CurveRenderer
|
||||
|
||||
ShapePath {
|
||||
strokeColor: root.color
|
||||
strokeWidth: root.strokeWidth
|
||||
fillColor: "transparent"
|
||||
capStyle: ShapePath.RoundCap
|
||||
|
||||
PathAngleArc {
|
||||
centerX: arc.width / 2
|
||||
centerY: arc.height / 2
|
||||
radiusX: Math.max(1, (Math.min(arc.width, arc.height) - root.strokeWidth) / 2)
|
||||
radiusY: radiusX
|
||||
startAngle: arc.startAngle
|
||||
sweepAngle: arc.sweepAngle
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SequentialAnimation {
|
||||
running: root.running
|
||||
loops: Animation.Infinite
|
||||
|
||||
NumberAnimation {
|
||||
target: arc
|
||||
property: "sweepAngle"
|
||||
from: 16
|
||||
to: 270
|
||||
duration: 666
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: Anims.standard
|
||||
}
|
||||
|
||||
ParallelAnimation {
|
||||
NumberAnimation {
|
||||
target: arc
|
||||
property: "startAngle"
|
||||
from: 0
|
||||
to: 254
|
||||
duration: 666
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: Anims.standard
|
||||
}
|
||||
|
||||
NumberAnimation {
|
||||
target: arc
|
||||
property: "sweepAngle"
|
||||
from: 270
|
||||
to: 16
|
||||
duration: 666
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: Anims.standard
|
||||
}
|
||||
}
|
||||
|
||||
ScriptAction {
|
||||
script: {
|
||||
arc.rotation = (arc.rotation + 254) % 360;
|
||||
arc.startAngle = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
DankCommon.DankSpinner {}
|
||||
|
||||
@@ -1,270 +1,3 @@
|
||||
import QtQuick
|
||||
import qs.Common
|
||||
import qs.Widgets
|
||||
import qs.DankCommon.Widgets as DankCommon
|
||||
|
||||
FocusScope {
|
||||
id: tabBar
|
||||
|
||||
property alias model: tabRepeater.model
|
||||
property int currentIndex: 0
|
||||
property int spacing: Theme.spacingL
|
||||
property int tabHeight: 56
|
||||
property bool showIcons: true
|
||||
property bool equalWidthTabs: true
|
||||
property bool enableArrowNavigation: true
|
||||
property Item nextFocusTarget: null
|
||||
property Item previousFocusTarget: null
|
||||
|
||||
signal tabClicked(int index)
|
||||
signal actionTriggered(int index)
|
||||
|
||||
focus: false
|
||||
activeFocusOnTab: true
|
||||
height: tabHeight
|
||||
|
||||
KeyNavigation.tab: nextFocusTarget
|
||||
KeyNavigation.down: nextFocusTarget
|
||||
KeyNavigation.backtab: previousFocusTarget
|
||||
KeyNavigation.up: previousFocusTarget
|
||||
|
||||
Keys.onPressed: event => {
|
||||
if (!tabBar.activeFocus || tabRepeater.count === 0)
|
||||
return;
|
||||
function findSelectableIndex(startIndex, step) {
|
||||
let idx = startIndex;
|
||||
for (let i = 0; i < tabRepeater.count; i++) {
|
||||
idx = (idx + step + tabRepeater.count) % tabRepeater.count;
|
||||
const item = tabRepeater.itemAt(idx);
|
||||
if (item && !item.isAction)
|
||||
return idx;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
const goToIndex = nextIndex => {
|
||||
if (nextIndex >= 0 && nextIndex !== tabBar.currentIndex) {
|
||||
tabBar.currentIndex = nextIndex;
|
||||
tabBar.tabClicked(nextIndex);
|
||||
}
|
||||
};
|
||||
|
||||
const resolveTarget = item => {
|
||||
if (!item)
|
||||
return null;
|
||||
|
||||
if (item.focusTarget)
|
||||
return resolveTarget(item.focusTarget);
|
||||
|
||||
return item;
|
||||
};
|
||||
|
||||
const focusItem = item => {
|
||||
const target = resolveTarget(item);
|
||||
if (!target)
|
||||
return false;
|
||||
|
||||
if (target.requestFocus) {
|
||||
Qt.callLater(() => target.requestFocus());
|
||||
return true;
|
||||
}
|
||||
|
||||
if (target.forceActiveFocus) {
|
||||
Qt.callLater(() => target.forceActiveFocus());
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
if (event.key === Qt.Key_Right && tabBar.enableArrowNavigation) {
|
||||
const baseIndex = (tabBar.currentIndex >= 0 && tabBar.currentIndex < tabRepeater.count) ? tabBar.currentIndex : -1;
|
||||
const nextIndex = findSelectableIndex(baseIndex, 1);
|
||||
if (nextIndex >= 0) {
|
||||
goToIndex(nextIndex);
|
||||
event.accepted = true;
|
||||
}
|
||||
} else if (event.key === Qt.Key_Left && tabBar.enableArrowNavigation) {
|
||||
const baseIndex = (tabBar.currentIndex >= 0 && tabBar.currentIndex < tabRepeater.count) ? tabBar.currentIndex : 0;
|
||||
const nextIndex = findSelectableIndex(baseIndex, -1);
|
||||
if (nextIndex >= 0) {
|
||||
goToIndex(nextIndex);
|
||||
event.accepted = true;
|
||||
}
|
||||
} else if (event.key === Qt.Key_Tab && (event.modifiers & Qt.ShiftModifier)) {
|
||||
if (focusItem(tabBar.previousFocusTarget)) {
|
||||
event.accepted = true;
|
||||
}
|
||||
} else if (event.key === Qt.Key_Tab || event.key === Qt.Key_Down) {
|
||||
if (focusItem(tabBar.nextFocusTarget)) {
|
||||
event.accepted = true;
|
||||
}
|
||||
} else if (event.key === Qt.Key_Up) {
|
||||
if (focusItem(tabBar.previousFocusTarget)) {
|
||||
event.accepted = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
id: tabRow
|
||||
anchors.fill: parent
|
||||
spacing: tabBar.spacing
|
||||
|
||||
Repeater {
|
||||
id: tabRepeater
|
||||
|
||||
Item {
|
||||
id: tabItem
|
||||
property bool isAction: modelData && modelData.isAction === true
|
||||
property bool isActive: !isAction && tabBar.currentIndex === index
|
||||
property bool hasIcon: tabBar.showIcons && modelData && modelData.icon && modelData.icon.length > 0
|
||||
property bool hasText: modelData && modelData.text && modelData.text.length > 0
|
||||
|
||||
width: tabBar.equalWidthTabs ? (tabBar.width - tabBar.spacing * Math.max(0, tabRepeater.count - 1)) / Math.max(1, tabRepeater.count) : Math.max(contentCol.implicitWidth + Theme.spacingXL, 64)
|
||||
height: tabBar.tabHeight
|
||||
|
||||
Column {
|
||||
id: contentCol
|
||||
anchors.centerIn: parent
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
DankIcon {
|
||||
name: modelData.icon || ""
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
size: Theme.iconSize
|
||||
color: tabItem.isActive ? Theme.primary : Theme.surfaceText
|
||||
visible: hasIcon
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: modelData.text || ""
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: tabItem.isActive ? Theme.primary : Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
visible: hasText
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: stateLayer
|
||||
anchors.fill: parent
|
||||
color: Theme.surfaceTint
|
||||
opacity: tabArea.pressed ? 0.12 : (tabArea.containsMouse ? 0.08 : 0)
|
||||
visible: opacity > 0
|
||||
radius: Theme.cornerRadius
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Theme.standardEasing
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DankRipple {
|
||||
id: tabRipple
|
||||
cornerRadius: Theme.cornerRadius
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: tabArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onPressed: mouse => tabRipple.trigger(mouse.x, mouse.y)
|
||||
onClicked: {
|
||||
if (tabItem.isAction) {
|
||||
tabBar.actionTriggered(index);
|
||||
} else {
|
||||
tabBar.tabClicked(index);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: indicator
|
||||
y: parent.height + 7
|
||||
height: 3
|
||||
width: 60
|
||||
topLeftRadius: Theme.cornerRadius
|
||||
topRightRadius: Theme.cornerRadius
|
||||
bottomLeftRadius: 0
|
||||
bottomRightRadius: 0
|
||||
color: Theme.primary
|
||||
visible: false
|
||||
|
||||
property bool animationEnabled: false
|
||||
property bool initialSetupComplete: false
|
||||
|
||||
Behavior on x {
|
||||
enabled: indicator.animationEnabled
|
||||
NumberAnimation {
|
||||
duration: Theme.mediumDuration
|
||||
easing.type: Theme.standardEasing
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on width {
|
||||
enabled: indicator.animationEnabled
|
||||
NumberAnimation {
|
||||
duration: Theme.mediumDuration
|
||||
easing.type: Theme.standardEasing
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: 1
|
||||
y: parent.height + 10
|
||||
color: Theme.outlineStrong
|
||||
}
|
||||
|
||||
function updateIndicator() {
|
||||
if (tabRepeater.count === 0 || currentIndex < 0 || currentIndex >= tabRepeater.count) {
|
||||
indicator.visible = false;
|
||||
indicator.initialSetupComplete = false;
|
||||
return;
|
||||
}
|
||||
|
||||
const item = tabRepeater.itemAt(currentIndex);
|
||||
if (!item || item.isAction) {
|
||||
return;
|
||||
}
|
||||
|
||||
const tabPos = item.mapToItem(tabBar, 0, 0);
|
||||
const tabCenterX = tabPos.x + item.width / 2;
|
||||
const indicatorWidth = 60;
|
||||
|
||||
if (tabPos.x < 10 && currentIndex > 0) {
|
||||
Qt.callLater(updateIndicator);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!indicator.initialSetupComplete) {
|
||||
indicator.animationEnabled = false;
|
||||
indicator.width = indicatorWidth;
|
||||
indicator.x = tabCenterX - indicatorWidth / 2;
|
||||
indicator.visible = true;
|
||||
indicator.initialSetupComplete = true;
|
||||
indicator.animationEnabled = true;
|
||||
} else {
|
||||
indicator.width = indicatorWidth;
|
||||
indicator.x = tabCenterX - indicatorWidth / 2;
|
||||
indicator.visible = true;
|
||||
}
|
||||
}
|
||||
|
||||
function snapIndicator() {
|
||||
indicator.initialSetupComplete = false;
|
||||
updateIndicator();
|
||||
}
|
||||
|
||||
onCurrentIndexChanged: {
|
||||
Qt.callLater(updateIndicator);
|
||||
}
|
||||
onWidthChanged: Qt.callLater(updateIndicator)
|
||||
}
|
||||
DankCommon.DankTabBar {}
|
||||
|
||||
@@ -1,31 +1,3 @@
|
||||
import QtQuick
|
||||
import qs.Common
|
||||
import qs.DankCommon.Widgets as DankCommon
|
||||
|
||||
Rectangle {
|
||||
id: root
|
||||
|
||||
property bool shown: true
|
||||
property bool blinkOn: true
|
||||
|
||||
readonly property int flashTime: Qt.styleHints.cursorFlashTime
|
||||
|
||||
function resetBlink() {
|
||||
blinkOn = true;
|
||||
blinkTimer.restart();
|
||||
}
|
||||
|
||||
width: 2
|
||||
radius: 1
|
||||
color: Theme.primary
|
||||
visible: shown && blinkOn
|
||||
|
||||
onShownChanged: resetBlink()
|
||||
|
||||
Timer {
|
||||
id: blinkTimer
|
||||
running: root.shown && root.flashTime > 1
|
||||
interval: root.flashTime / 2
|
||||
repeat: true
|
||||
onTriggered: root.blinkOn = !root.blinkOn
|
||||
}
|
||||
}
|
||||
DankCommon.DankTextCursor {}
|
||||
|
||||
@@ -1,290 +1,3 @@
|
||||
import QtQuick
|
||||
import qs.Common
|
||||
import qs.Widgets
|
||||
import qs.DankCommon.Widgets as DankCommon
|
||||
|
||||
StyledRect {
|
||||
id: root
|
||||
|
||||
LayoutMirroring.enabled: I18n.isRtl
|
||||
LayoutMirroring.childrenInherit: true
|
||||
|
||||
KeyNavigation.tab: keyNavigationTab
|
||||
KeyNavigation.backtab: keyNavigationBacktab
|
||||
|
||||
function checkParentDisablesTransparency() {
|
||||
let p = parent;
|
||||
while (p) {
|
||||
if (p.disablePopupTransparency === true)
|
||||
return true;
|
||||
p = p.parent;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
property alias text: textInput.text
|
||||
property alias cursorPosition: textInput.cursorPosition
|
||||
property string placeholderText: ""
|
||||
property string labelText: ""
|
||||
property alias font: textInput.font
|
||||
property alias textColor: textInput.color
|
||||
property alias echoMode: textInput.echoMode
|
||||
property alias validator: textInput.validator
|
||||
property alias maximumLength: textInput.maximumLength
|
||||
property string leftIconName: ""
|
||||
property int leftIconSize: Theme.iconSize
|
||||
property color leftIconColor: Theme.surfaceVariantText
|
||||
property color leftIconFocusedColor: Theme.primary
|
||||
property bool showClearButton: false
|
||||
property bool showPasswordToggle: false
|
||||
property real rightAccessoryWidth: 0
|
||||
property bool passwordVisible: false
|
||||
property bool usePopupTransparency: !checkParentDisablesTransparency()
|
||||
property color backgroundColor: usePopupTransparency ? Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency) : Theme.surfaceContainerHigh
|
||||
property color focusedBorderColor: Theme.primary
|
||||
property color normalBorderColor: Theme.outlineMedium
|
||||
property color placeholderColor: Theme.outlineButton
|
||||
property real borderWidth: 1
|
||||
property real focusedBorderWidth: 2
|
||||
property real cornerRadius: Theme.cornerRadius
|
||||
readonly property real leftPadding: Theme.spacingM + (leftIconName ? leftIconSize + Theme.spacingM : 0)
|
||||
readonly property real rightPadding: {
|
||||
let p = Theme.spacingS + rightAccessoryWidth;
|
||||
if (showPasswordToggle)
|
||||
p += 20 + Theme.spacingXS;
|
||||
if (showClearButton && text.length > 0)
|
||||
p += 20 + Theme.spacingXS;
|
||||
return p;
|
||||
}
|
||||
property real topPadding: Theme.spacingS
|
||||
property real bottomPadding: Theme.spacingS
|
||||
property bool ignoreLeftRightKeys: false
|
||||
property bool ignoreUpDownKeys: false
|
||||
property bool ignoreTabKeys: false
|
||||
property var keyForwardTargets: []
|
||||
property Item keyNavigationTab: null
|
||||
property Item keyNavigationBacktab: null
|
||||
|
||||
signal textEdited
|
||||
signal editingFinished
|
||||
signal accepted
|
||||
signal focusStateChanged(bool hasFocus)
|
||||
|
||||
function getActiveFocus() {
|
||||
return textInput.activeFocus;
|
||||
}
|
||||
function setFocus(value) {
|
||||
textInput.focus = value;
|
||||
}
|
||||
function forceActiveFocus() {
|
||||
textInput.forceActiveFocus();
|
||||
}
|
||||
function selectAll() {
|
||||
textInput.selectAll();
|
||||
}
|
||||
function clear() {
|
||||
textInput.clear();
|
||||
}
|
||||
function insertText(str) {
|
||||
textInput.insert(textInput.cursorPosition, str);
|
||||
}
|
||||
|
||||
readonly property real labelBandHeight: Math.round(Theme.fontSizeSmall * 1.4) + Theme.spacingXS * 2
|
||||
|
||||
width: 200
|
||||
height: labelText !== "" ? Math.round(Theme.fontSizeMedium * 3) + labelBandHeight : Math.round(Theme.fontSizeMedium * 3)
|
||||
radius: cornerRadius
|
||||
color: backgroundColor
|
||||
border.color: textInput.activeFocus ? focusedBorderColor : normalBorderColor
|
||||
border.width: textInput.activeFocus ? focusedBorderWidth : borderWidth
|
||||
|
||||
DankIcon {
|
||||
id: leftIcon
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Theme.spacingM
|
||||
anchors.verticalCenter: textInput.verticalCenter
|
||||
name: leftIconName
|
||||
size: leftIconSize
|
||||
color: textInput.activeFocus ? leftIconFocusedColor : leftIconColor
|
||||
visible: leftIconName !== ""
|
||||
}
|
||||
|
||||
StyledText {
|
||||
id: fieldLabel
|
||||
|
||||
anchors.left: textInput.left
|
||||
anchors.right: textInput.right
|
||||
anchors.top: parent.top
|
||||
anchors.topMargin: Theme.spacingXS
|
||||
text: root.labelText
|
||||
visible: root.labelText !== ""
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: textInput.activeFocus ? Theme.primary : Theme.surfaceVariantText
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
|
||||
TextInput {
|
||||
id: textInput
|
||||
|
||||
anchors.left: leftIcon.visible ? leftIcon.right : parent.left
|
||||
anchors.leftMargin: Theme.spacingM
|
||||
anchors.right: rightButtonsRow.left
|
||||
anchors.rightMargin: rightButtonsRow.visible ? Theme.spacingS : Theme.spacingM
|
||||
anchors.top: parent.top
|
||||
anchors.topMargin: root.labelText !== "" ? root.labelBandHeight : root.topPadding
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.bottomMargin: root.bottomPadding
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
font.family: Theme.fontFamily
|
||||
color: Theme.surfaceText
|
||||
selectionColor: Theme.primaryContainer
|
||||
selectedTextColor: Theme.primary
|
||||
horizontalAlignment: TextInput.AlignLeft
|
||||
verticalAlignment: TextInput.AlignVCenter
|
||||
selectByMouse: !root.ignoreLeftRightKeys
|
||||
clip: true
|
||||
activeFocusOnTab: true
|
||||
KeyNavigation.tab: root.keyNavigationTab
|
||||
KeyNavigation.backtab: root.keyNavigationBacktab
|
||||
onTextChanged: root.textEdited()
|
||||
onEditingFinished: root.editingFinished()
|
||||
onAccepted: root.accepted()
|
||||
onActiveFocusChanged: root.focusStateChanged(activeFocus)
|
||||
Keys.forwardTo: root.keyForwardTargets
|
||||
Keys.onLeftPressed: event => {
|
||||
if (root.ignoreLeftRightKeys) {
|
||||
event.accepted = true;
|
||||
} else {
|
||||
// Allow normal TextInput cursor movement
|
||||
event.accepted = false;
|
||||
}
|
||||
}
|
||||
Keys.onRightPressed: event => {
|
||||
if (root.ignoreLeftRightKeys) {
|
||||
event.accepted = true;
|
||||
} else {
|
||||
event.accepted = false;
|
||||
}
|
||||
}
|
||||
Keys.onPressed: event => {
|
||||
if (root.ignoreTabKeys && (event.key === Qt.Key_Tab || event.key === Qt.Key_Backtab)) {
|
||||
event.accepted = false;
|
||||
for (var i = 0; i < root.keyForwardTargets.length; i++) {
|
||||
if (root.keyForwardTargets[i])
|
||||
root.keyForwardTargets[i].Keys.pressed(event);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (root.ignoreUpDownKeys && (event.key === Qt.Key_Up || event.key === Qt.Key_Down)) {
|
||||
event.accepted = false;
|
||||
for (var i = 0; i < root.keyForwardTargets.length; i++) {
|
||||
if (root.keyForwardTargets[i])
|
||||
root.keyForwardTargets[i].Keys.pressed(event);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if ((event.modifiers & (Qt.ControlModifier | Qt.AltModifier | Qt.MetaModifier)) && root.keyForwardTargets.length > 0) {
|
||||
for (var i = 0; i < root.keyForwardTargets.length; i++) {
|
||||
if (root.keyForwardTargets[i])
|
||||
root.keyForwardTargets[i].Keys.pressed(event);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.IBeamCursor
|
||||
acceptedButtons: Qt.NoButton
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
id: rightButtonsRow
|
||||
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: Theme.spacingS + root.rightAccessoryWidth
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Theme.spacingXS
|
||||
visible: showPasswordToggle || (showClearButton && text.length > 0)
|
||||
|
||||
StyledRect {
|
||||
id: passwordToggleButton
|
||||
|
||||
width: 20
|
||||
height: 20
|
||||
radius: 10
|
||||
color: passwordToggleArea.containsMouse ? Theme.outlineStrong : Theme.withAlpha(Theme.outlineStrong, 0)
|
||||
visible: showPasswordToggle
|
||||
|
||||
DankIcon {
|
||||
anchors.centerIn: parent
|
||||
name: passwordVisible ? "visibility_off" : "visibility"
|
||||
size: 14
|
||||
color: passwordToggleArea.containsMouse ? Theme.outline : Theme.surfaceVariantText
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: passwordToggleArea
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: passwordVisible = !passwordVisible
|
||||
}
|
||||
}
|
||||
|
||||
StyledRect {
|
||||
id: clearButton
|
||||
|
||||
width: 20
|
||||
height: 20
|
||||
radius: 10
|
||||
color: clearArea.containsMouse ? Theme.outlineStrong : Theme.withAlpha(Theme.outlineStrong, 0)
|
||||
visible: showClearButton && text.length > 0
|
||||
|
||||
DankIcon {
|
||||
anchors.centerIn: parent
|
||||
name: "close"
|
||||
size: 14
|
||||
color: clearArea.containsMouse ? Theme.outline : Theme.surfaceVariantText
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: clearArea
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: textInput.text = ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
id: placeholderLabel
|
||||
|
||||
anchors.fill: textInput
|
||||
text: root.placeholderText
|
||||
font: textInput.font
|
||||
color: placeholderColor
|
||||
horizontalAlignment: Text.AlignLeft
|
||||
verticalAlignment: textInput.verticalAlignment
|
||||
visible: textInput.text.length === 0 && !textInput.activeFocus
|
||||
elide: I18n.isRtl ? Text.ElideLeft : Text.ElideRight
|
||||
}
|
||||
|
||||
Behavior on border.color {
|
||||
ColorAnimation {
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Theme.standardEasing
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on border.width {
|
||||
NumberAnimation {
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Theme.standardEasing
|
||||
}
|
||||
}
|
||||
}
|
||||
DankCommon.DankTextField {}
|
||||
|
||||
@@ -1,194 +1,3 @@
|
||||
import QtQuick
|
||||
import qs.Common
|
||||
import qs.Widgets
|
||||
import qs.DankCommon.Widgets as DankCommon
|
||||
|
||||
Item {
|
||||
id: toggle
|
||||
|
||||
LayoutMirroring.enabled: I18n.isRtl
|
||||
LayoutMirroring.childrenInherit: true
|
||||
|
||||
// API
|
||||
property bool checked: false
|
||||
property bool toggling: false
|
||||
property string text: ""
|
||||
property string description: ""
|
||||
property color descriptionColor: Theme.surfaceVariantText
|
||||
property bool hideText: false
|
||||
|
||||
signal clicked
|
||||
signal toggled(bool checked)
|
||||
signal toggleCompleted(bool checked)
|
||||
|
||||
readonly property bool showText: text && !hideText
|
||||
|
||||
readonly property int trackWidth: 52
|
||||
readonly property int trackHeight: 30
|
||||
readonly property int insetCircle: 24
|
||||
|
||||
width: showText ? parent.width : trackWidth
|
||||
height: showText ? Math.max(trackHeight, textColumn.implicitHeight + Theme.spacingM * 2) : trackHeight
|
||||
|
||||
function handleClick() {
|
||||
if (!enabled)
|
||||
return;
|
||||
clicked();
|
||||
toggled(!checked);
|
||||
}
|
||||
|
||||
StyledRect {
|
||||
id: background
|
||||
anchors.fill: parent
|
||||
radius: showText ? Theme.cornerRadius : 0
|
||||
color: "transparent"
|
||||
visible: showText
|
||||
|
||||
StateLayer {
|
||||
visible: showText
|
||||
disabled: !toggle.enabled
|
||||
stateColor: Theme.primary
|
||||
cornerRadius: parent.radius
|
||||
onClicked: toggle.handleClick()
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
anchors.left: parent.left
|
||||
anchors.right: toggleTrack.left
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.leftMargin: Theme.spacingM
|
||||
anchors.rightMargin: Theme.spacingM
|
||||
spacing: Theme.spacingXS
|
||||
visible: showText
|
||||
|
||||
Column {
|
||||
id: textColumn
|
||||
width: parent.width
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
StyledText {
|
||||
text: toggle.text
|
||||
font.pixelSize: Appearance.fontSize.normal
|
||||
font.weight: Font.Medium
|
||||
opacity: toggle.enabled ? 1 : 0.4
|
||||
width: parent.width
|
||||
horizontalAlignment: Text.AlignLeft
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: toggle.description
|
||||
font.pixelSize: Appearance.fontSize.small
|
||||
color: toggle.descriptionColor
|
||||
wrapMode: Text.WordWrap
|
||||
width: parent.width
|
||||
visible: toggle.description.length > 0
|
||||
horizontalAlignment: Text.AlignLeft
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StyledRect {
|
||||
id: toggleTrack
|
||||
|
||||
width: showText ? trackWidth : Math.max(parent.width, trackWidth)
|
||||
height: showText ? trackHeight : Math.max(parent.height, trackHeight)
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: showText ? Theme.spacingM : 0
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
radius: Theme.cornerRadius
|
||||
|
||||
// Distinguish disabled checked vs unchecked so unchecked disabled switches don't look enabled
|
||||
color: !toggle.enabled ? (toggle.checked ? Qt.alpha(Theme.surfaceText, 0.12) : Theme.withAlpha(Qt.alpha(Theme.surfaceText, 0.12), 0)) : (toggle.checked ? Theme.primary : Theme.surfaceVariantAlpha)
|
||||
opacity: toggle.toggling ? 0.6 : 1
|
||||
|
||||
// M3 disabled unchecked border: on surface 12% opacity
|
||||
border.color: toggle.checked ? Theme.withAlpha(Theme.outline, 0) : (!toggle.enabled ? Qt.alpha(Theme.surfaceText, 0.12) : Theme.outline)
|
||||
|
||||
readonly property int pad: Math.round((height - thumb.width) / 2)
|
||||
readonly property int edgeLeft: pad
|
||||
readonly property int edgeRight: width - thumb.width - pad
|
||||
|
||||
StyledRect {
|
||||
id: thumb
|
||||
|
||||
width: toggle.checked ? insetCircle : insetCircle - 4
|
||||
height: toggle.checked ? insetCircle : insetCircle - 4
|
||||
radius: Theme.cornerRadius
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
// M3 disabled thumb:
|
||||
// checked = solid surface | unchecked = outlined off-state thumb
|
||||
color: !toggle.enabled ? (toggle.checked ? Theme.surface : Theme.withAlpha(Theme.surface, 0)) : (toggle.checked ? Theme.surface : Theme.outline)
|
||||
border.color: !toggle.enabled ? (toggle.checked ? Theme.withAlpha(Qt.alpha(Theme.surfaceText, 0.38), 0) : Qt.alpha(Theme.surfaceText, 0.38)) : Theme.outline
|
||||
border.width: (toggle.checked && toggle.enabled) ? 1 : 2
|
||||
|
||||
x: toggle.checked ? toggleTrack.edgeRight : toggleTrack.edgeLeft
|
||||
|
||||
Behavior on x {
|
||||
SequentialAnimation {
|
||||
NumberAnimation {
|
||||
duration: Appearance.anim.durations.normal
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: Appearance.anim.curves.emphasizedDecel
|
||||
}
|
||||
ScriptAction {
|
||||
script: {
|
||||
toggle.toggleCompleted(toggle.checked);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation {
|
||||
duration: Appearance.anim.durations.normal
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: Appearance.anim.curves.emphasized
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on border.width {
|
||||
NumberAnimation {
|
||||
duration: Appearance.anim.durations.normal
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: Appearance.anim.curves.emphasized
|
||||
}
|
||||
}
|
||||
|
||||
DankIcon {
|
||||
id: checkIcon
|
||||
anchors.centerIn: parent
|
||||
name: "check"
|
||||
size: 20
|
||||
// M3 disabled icon: on surface 38%
|
||||
color: toggle.enabled ? Theme.surfaceText : Qt.alpha(Theme.surfaceText, 0.38)
|
||||
filled: true
|
||||
opacity: (toggle.checked && toggle.enabled) ? 1 : 0
|
||||
scale: (toggle.checked && toggle.enabled) ? 1 : 0.6
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: Anims.durShort
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: Anims.emphasized
|
||||
}
|
||||
}
|
||||
Behavior on scale {
|
||||
NumberAnimation {
|
||||
duration: Anims.durShort
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: Anims.emphasized
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StateLayer {
|
||||
disabled: !toggle.enabled
|
||||
stateColor: Theme.primary
|
||||
cornerRadius: parent.radius
|
||||
onClicked: toggle.handleClick()
|
||||
}
|
||||
}
|
||||
}
|
||||
DankCommon.DankToggle {}
|
||||
|
||||
@@ -1,154 +1,3 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import qs.Common
|
||||
import qs.DankCommon.Widgets as DankCommon
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property string text: ""
|
||||
|
||||
function show(text, item, offsetX, offsetY, preferredSide) {
|
||||
if (!item)
|
||||
return;
|
||||
|
||||
let windowContentItem = item.Window?.window?.contentItem;
|
||||
if (!windowContentItem) {
|
||||
let current = item;
|
||||
while (current) {
|
||||
if (current.Window?.window?.contentItem) {
|
||||
windowContentItem = current.Window.window.contentItem;
|
||||
break;
|
||||
}
|
||||
current = current.parent;
|
||||
}
|
||||
}
|
||||
if (!windowContentItem)
|
||||
return;
|
||||
|
||||
tooltip.parent = windowContentItem;
|
||||
tooltip.text = text;
|
||||
|
||||
const itemPos = item.mapToItem(windowContentItem, 0, 0);
|
||||
const parentWidth = windowContentItem.width;
|
||||
const parentHeight = windowContentItem.height;
|
||||
const tooltipWidth = tooltip.implicitWidth;
|
||||
const tooltipHeight = tooltip.implicitHeight;
|
||||
|
||||
const side = preferredSide || _determineBestSide(itemPos, item, parentWidth, parentHeight, tooltipWidth, tooltipHeight);
|
||||
|
||||
let targetX = 0;
|
||||
let targetY = 0;
|
||||
|
||||
switch (side) {
|
||||
case "left":
|
||||
targetX = itemPos.x - tooltipWidth - 8;
|
||||
targetY = itemPos.y + (item.height - tooltipHeight) / 2;
|
||||
break;
|
||||
case "right":
|
||||
targetX = itemPos.x + item.width + 8;
|
||||
targetY = itemPos.y + (item.height - tooltipHeight) / 2;
|
||||
break;
|
||||
case "top":
|
||||
targetX = itemPos.x + (item.width - tooltipWidth) / 2;
|
||||
targetY = itemPos.y - tooltipHeight - 8;
|
||||
break;
|
||||
case "bottom":
|
||||
default:
|
||||
targetX = itemPos.x + (item.width - tooltipWidth) / 2;
|
||||
targetY = itemPos.y + item.height + 8;
|
||||
break;
|
||||
}
|
||||
|
||||
tooltip.x = Math.max(4, Math.min(parentWidth - tooltipWidth - 4, targetX + (offsetX || 0)));
|
||||
tooltip.y = Math.max(4, Math.min(parentHeight - tooltipHeight - 4, targetY + (offsetY || 0)));
|
||||
|
||||
tooltip.open();
|
||||
}
|
||||
|
||||
function _determineBestSide(itemPos, item, parentWidth, parentHeight, tooltipWidth, tooltipHeight) {
|
||||
const itemCenterX = itemPos.x + item.width / 2;
|
||||
const itemCenterY = itemPos.y + item.height / 2;
|
||||
|
||||
const spaceLeft = itemPos.x;
|
||||
const spaceRight = parentWidth - (itemPos.x + item.width);
|
||||
const spaceTop = itemPos.y;
|
||||
const spaceBottom = parentHeight - (itemPos.y + item.height);
|
||||
|
||||
if (spaceRight >= tooltipWidth + 16) {
|
||||
return "right";
|
||||
}
|
||||
if (spaceLeft >= tooltipWidth + 16) {
|
||||
return "left";
|
||||
}
|
||||
if (spaceBottom >= tooltipHeight + 16) {
|
||||
return "bottom";
|
||||
}
|
||||
if (spaceTop >= tooltipHeight + 16) {
|
||||
return "top";
|
||||
}
|
||||
|
||||
if (itemCenterX > parentWidth / 2) {
|
||||
return "left";
|
||||
}
|
||||
return "right";
|
||||
}
|
||||
|
||||
function hide() {
|
||||
tooltip.close();
|
||||
}
|
||||
|
||||
Popup {
|
||||
id: tooltip
|
||||
|
||||
property string text: ""
|
||||
|
||||
leftPadding: Theme.spacingM
|
||||
rightPadding: Theme.spacingM
|
||||
topPadding: Theme.spacingS
|
||||
bottomPadding: Theme.spacingS
|
||||
closePolicy: Popup.NoAutoClose
|
||||
modal: false
|
||||
dim: false
|
||||
|
||||
background: Rectangle {
|
||||
color: Theme.surfaceContainerHigh
|
||||
radius: Theme.cornerRadius
|
||||
border.width: 1
|
||||
border.color: Theme.outlineMedium
|
||||
}
|
||||
|
||||
contentItem: Text {
|
||||
id: textContent
|
||||
|
||||
width: Math.min(implicitWidth, 500)
|
||||
text: tooltip.text
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceText
|
||||
wrapMode: Text.NoWrap
|
||||
maximumLineCount: 1
|
||||
elide: Text.ElideRight
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
}
|
||||
|
||||
enter: Transition {
|
||||
NumberAnimation {
|
||||
property: "opacity"
|
||||
from: 0
|
||||
to: 1
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Theme.standardEasing
|
||||
}
|
||||
}
|
||||
|
||||
exit: Transition {
|
||||
NumberAnimation {
|
||||
property: "opacity"
|
||||
from: 1
|
||||
to: 0
|
||||
duration: Theme.shorterDuration
|
||||
easing.type: Theme.standardEasing
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
DankCommon.DankTooltipV2 {}
|
||||
|
||||
@@ -1,115 +1,3 @@
|
||||
import QtQuick
|
||||
import qs.DankCommon.Widgets as DankCommon
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
readonly property real edgeSize: 8
|
||||
required property var targetWindow
|
||||
readonly property bool canMaximize: targetWindow.minimumSize.width !== targetWindow.maximumSize.width || targetWindow.minimumSize.height !== targetWindow.maximumSize.height
|
||||
|
||||
anchors.fill: parent
|
||||
|
||||
function tryStartMove() {
|
||||
targetWindow.startSystemMove();
|
||||
}
|
||||
|
||||
function tryStartResize(edges) {
|
||||
if (!canMaximize)
|
||||
return;
|
||||
targetWindow.startSystemResize(edges);
|
||||
}
|
||||
|
||||
function tryToggleMaximize() {
|
||||
if (!canMaximize)
|
||||
return;
|
||||
targetWindow.maximized = !targetWindow.maximized;
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
visible: root.canMaximize
|
||||
height: root.edgeSize
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
anchors.leftMargin: 6
|
||||
anchors.rightMargin: 6
|
||||
cursorShape: Qt.SizeVerCursor
|
||||
onPressed: root.tryStartResize(Qt.TopEdge)
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
visible: root.canMaximize
|
||||
width: root.edgeSize
|
||||
anchors.left: parent.left
|
||||
anchors.top: parent.top
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.topMargin: 6
|
||||
anchors.bottomMargin: 6
|
||||
cursorShape: Qt.SizeHorCursor
|
||||
onPressed: root.tryStartResize(Qt.LeftEdge)
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
visible: root.canMaximize
|
||||
width: root.edgeSize
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.topMargin: 6
|
||||
anchors.bottomMargin: 6
|
||||
cursorShape: Qt.SizeHorCursor
|
||||
onPressed: root.tryStartResize(Qt.RightEdge)
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
visible: root.canMaximize
|
||||
width: root.edgeSize
|
||||
height: root.edgeSize
|
||||
anchors.left: parent.left
|
||||
anchors.top: parent.top
|
||||
cursorShape: Qt.SizeFDiagCursor
|
||||
onPressed: root.tryStartResize(Qt.LeftEdge | Qt.TopEdge)
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
visible: root.canMaximize
|
||||
width: root.edgeSize
|
||||
height: root.edgeSize
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
cursorShape: Qt.SizeBDiagCursor
|
||||
onPressed: root.tryStartResize(Qt.RightEdge | Qt.TopEdge)
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
visible: root.canMaximize
|
||||
height: root.edgeSize
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.leftMargin: 6
|
||||
anchors.rightMargin: 6
|
||||
cursorShape: Qt.SizeVerCursor
|
||||
onPressed: root.tryStartResize(Qt.BottomEdge)
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
visible: root.canMaximize
|
||||
width: root.edgeSize
|
||||
height: root.edgeSize
|
||||
anchors.left: parent.left
|
||||
anchors.bottom: parent.bottom
|
||||
cursorShape: Qt.SizeBDiagCursor
|
||||
onPressed: root.tryStartResize(Qt.LeftEdge | Qt.BottomEdge)
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
visible: root.canMaximize
|
||||
width: root.edgeSize
|
||||
height: root.edgeSize
|
||||
anchors.right: parent.right
|
||||
anchors.bottom: parent.bottom
|
||||
cursorShape: Qt.SizeFDiagCursor
|
||||
onPressed: root.tryStartResize(Qt.RightEdge | Qt.BottomEdge)
|
||||
}
|
||||
}
|
||||
DankCommon.FloatingWindowControls {}
|
||||
|
||||
@@ -1,64 +1,3 @@
|
||||
import QtQuick
|
||||
import qs.Common
|
||||
import qs.DankCommon.Widgets as DankCommon
|
||||
|
||||
// Wave progress indicator: track, animated fill, seek preview and playhead are
|
||||
// all drawn in a single fragment shader (Shaders/frag/wave_progress.frag).
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property real value: 0
|
||||
property real actualValue: value
|
||||
property bool showActualPlaybackState: false
|
||||
property real lineWidth: 2
|
||||
property real wavelength: 20
|
||||
property real amp: 1.6
|
||||
property real phase: 0.0
|
||||
property bool isPlaying: false
|
||||
property real currentAmp: 1.6
|
||||
property color trackColor: Theme.withAlpha(Theme.surfaceVariant, 0.40)
|
||||
property color fillColor: Theme.primary
|
||||
property color playheadColor: Theme.primary
|
||||
property color actualProgressColor: Theme.onSurface_38
|
||||
|
||||
Behavior on currentAmp {
|
||||
NumberAnimation {
|
||||
duration: 300
|
||||
easing.type: Easing.OutCubic
|
||||
}
|
||||
}
|
||||
onIsPlayingChanged: currentAmp = isPlaying ? amp : 0
|
||||
Component.onCompleted: currentAmp = isPlaying ? amp : 0
|
||||
|
||||
ShaderEffect {
|
||||
anchors.fill: parent
|
||||
blending: true
|
||||
|
||||
readonly property real widthPx: width
|
||||
readonly property real heightPx: height
|
||||
readonly property real value: root.value
|
||||
readonly property real actualValue: root.actualValue
|
||||
readonly property real phase: root.phase
|
||||
readonly property real ampPx: root.currentAmp
|
||||
readonly property real wavelengthPx: root.wavelength
|
||||
readonly property real lineWidthPx: root.lineWidth
|
||||
readonly property real showActual: root.showActualPlaybackState ? 1.0 : 0.0
|
||||
readonly property color fillColor: root.fillColor
|
||||
readonly property color trackColor: root.trackColor
|
||||
readonly property color playheadColor: root.playheadColor
|
||||
readonly property color actualColor: root.actualProgressColor
|
||||
|
||||
fragmentShader: Qt.resolvedUrl("../Shaders/qsb/wave_progress.frag.qsb")
|
||||
}
|
||||
|
||||
signal frameTicked
|
||||
|
||||
FrameAnimation {
|
||||
running: root.visible && (root.isPlaying || root.currentAmp > 0) && (root.Window.window?.visible ?? false)
|
||||
onTriggered: {
|
||||
if (!root.isPlaying)
|
||||
return;
|
||||
root.phase = (root.phase + 0.03 * frameTime * 60) % 6.28318530718;
|
||||
root.frameTicked();
|
||||
}
|
||||
}
|
||||
}
|
||||
DankCommon.M3WaveProgress {}
|
||||
|
||||
@@ -1,22 +1,3 @@
|
||||
import QtQuick
|
||||
import qs.Common
|
||||
import qs.DankCommon.Widgets as DankCommon
|
||||
|
||||
StyledText {
|
||||
id: root
|
||||
|
||||
property string reserveText: ""
|
||||
readonly property real reservedWidth: reserveText !== "" ? Math.max(contentWidth, reserveMetrics.width) : contentWidth
|
||||
|
||||
isMonospace: true
|
||||
wrapMode: Text.NoWrap
|
||||
|
||||
StyledTextMetrics {
|
||||
id: reserveMetrics
|
||||
isMonospace: root.isMonospace
|
||||
font.pixelSize: root.font.pixelSize
|
||||
font.family: root.font.family
|
||||
font.weight: root.font.weight
|
||||
font.hintingPreference: root.font.hintingPreference
|
||||
text: root.reserveText
|
||||
}
|
||||
}
|
||||
DankCommon.NumericText {}
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
.pragma library
|
||||
|
||||
const friction = 0.96;
|
||||
const touchpadSpeed = 3.5;
|
||||
const mouseWheelSpeed = 60;
|
||||
const momentumRetention = 0.92;
|
||||
const momentumDeltaFactor = 0.15;
|
||||
const maxMomentumVelocity = 2500;
|
||||
const minMomentumVelocity = 50;
|
||||
const momentumStopThreshold = 5;
|
||||
const velocitySampleWindowMs = 100;
|
||||
const momentumTimeThreshold = 50;
|
||||
const flickDeceleration = 1500;
|
||||
const maximumFlickVelocity = 2000;
|
||||
@@ -1,74 +1,3 @@
|
||||
import QtQuick
|
||||
import qs.Common
|
||||
import qs.DankCommon.Widgets as DankCommon
|
||||
|
||||
MouseArea {
|
||||
id: root
|
||||
|
||||
property bool disabled: false
|
||||
property color stateColor: Theme.surfaceText
|
||||
property real cornerRadius: parent && parent.radius !== undefined ? parent.radius : Theme.cornerRadius
|
||||
property var tooltipText: null
|
||||
property string tooltipSide: "bottom"
|
||||
property bool enableRipple: typeof SettingsData !== "undefined" ? (SettingsData.enableRippleEffects ?? true) : true
|
||||
|
||||
readonly property real stateOpacity: disabled ? 0 : pressed ? 0.12 : containsMouse ? 0.08 : 0
|
||||
|
||||
anchors.fill: parent
|
||||
cursorShape: disabled ? undefined : Qt.PointingHandCursor
|
||||
hoverEnabled: true
|
||||
|
||||
onPressed: mouse => {
|
||||
if (!disabled && enableRipple) {
|
||||
rippleLayer.trigger(mouse.x, mouse.y);
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: stateRect
|
||||
anchors.fill: parent
|
||||
radius: root.cornerRadius
|
||||
color: Theme.withAlpha(stateColor, stateOpacity)
|
||||
|
||||
Behavior on color {
|
||||
enabled: Theme.currentAnimationSpeed !== SettingsData.AnimationSpeed.None
|
||||
DankColorAnim {
|
||||
duration: Theme.shorterDuration
|
||||
easing.bezierCurve: Theme.expressiveCurves.standardDecel
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DankRipple {
|
||||
id: rippleLayer
|
||||
anchors.fill: parent
|
||||
rippleColor: root.stateColor
|
||||
cornerRadius: root.cornerRadius
|
||||
enableRipple: root.enableRipple
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: hoverDelay
|
||||
interval: 400
|
||||
repeat: false
|
||||
onTriggered: {
|
||||
tooltip.show(root.tooltipText, root, 0, 0, root.tooltipSide);
|
||||
}
|
||||
}
|
||||
|
||||
onEntered: {
|
||||
if (!tooltipText)
|
||||
return;
|
||||
hoverDelay.restart();
|
||||
}
|
||||
|
||||
onExited: {
|
||||
if (!tooltipText)
|
||||
return;
|
||||
hoverDelay.stop();
|
||||
tooltip.hide();
|
||||
}
|
||||
|
||||
DankTooltipV2 {
|
||||
id: tooltip
|
||||
}
|
||||
}
|
||||
DankCommon.StateLayer {}
|
||||
|
||||
@@ -1,29 +1,3 @@
|
||||
import QtQuick
|
||||
import qs.Common
|
||||
import qs.DankCommon.Widgets as DankCommon
|
||||
|
||||
Rectangle {
|
||||
color: "transparent"
|
||||
radius: Appearance.rounding.normal
|
||||
|
||||
readonly property var standardAnimation: {
|
||||
"duration": Appearance.anim.durations.normal,
|
||||
"easing.type": Easing.BezierSpline,
|
||||
"easing.bezierCurve": Appearance.anim.curves.standard
|
||||
}
|
||||
|
||||
Behavior on radius {
|
||||
NumberAnimation {
|
||||
duration: standardAnimation.duration
|
||||
easing.type: standardAnimation["easing.type"]
|
||||
easing.bezierCurve: standardAnimation["easing.bezierCurve"]
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: standardAnimation.duration
|
||||
easing.type: standardAnimation["easing.type"]
|
||||
easing.bezierCurve: standardAnimation["easing.bezierCurve"]
|
||||
}
|
||||
}
|
||||
}
|
||||
DankCommon.StyledRect {}
|
||||
|
||||
@@ -1,76 +1,3 @@
|
||||
import QtQuick
|
||||
import qs.Common
|
||||
import qs.DankCommon.Widgets as DankCommon
|
||||
|
||||
Text {
|
||||
property bool isMonospace: false
|
||||
FontLoader {
|
||||
id: interFont
|
||||
source: Qt.resolvedUrl("../assets/fonts/inter/InterVariable.ttf")
|
||||
}
|
||||
|
||||
FontLoader {
|
||||
id: firaCodeFont
|
||||
source: Qt.resolvedUrl("../assets/fonts/nerd-fonts/FiraCodeNerdFont-Regular.ttf")
|
||||
}
|
||||
|
||||
readonly property string resolvedFontFamily: {
|
||||
const requestedFont = isMonospace ? Theme.monoFontFamily : Theme.fontFamily;
|
||||
const defaultFont = isMonospace ? Theme.defaultMonoFontFamily : Theme.defaultFontFamily;
|
||||
|
||||
if (requestedFont === defaultFont) {
|
||||
return isMonospace ? firaCodeFont.name : interFont.name;
|
||||
}
|
||||
return requestedFont;
|
||||
}
|
||||
|
||||
readonly property int resolvedRenderType: {
|
||||
switch (SettingsData.textRenderType) {
|
||||
case SettingsData.TextRenderType.Qt:
|
||||
return Text.QtRendering;
|
||||
case SettingsData.TextRenderType.Curve:
|
||||
return Text.CurveRendering;
|
||||
default:
|
||||
return Text.NativeRendering;
|
||||
}
|
||||
}
|
||||
|
||||
readonly property int resolvedRenderQuality: {
|
||||
switch (SettingsData.textRenderQuality) {
|
||||
case SettingsData.TextRenderQuality.Low:
|
||||
return Text.LowRenderTypeQuality;
|
||||
case SettingsData.TextRenderQuality.Normal:
|
||||
return Text.NormalRenderTypeQuality;
|
||||
case SettingsData.TextRenderQuality.High:
|
||||
return Text.HighRenderTypeQuality;
|
||||
case SettingsData.TextRenderQuality.VeryHigh:
|
||||
return Text.VeryHighRenderTypeQuality;
|
||||
default:
|
||||
return Text.DefaultRenderTypeQuality;
|
||||
}
|
||||
}
|
||||
|
||||
readonly property var standardAnimation: {
|
||||
"duration": Appearance.anim.durations.normal,
|
||||
"easing.type": Easing.BezierSpline,
|
||||
"easing.bezierCurve": Appearance.anim.curves.standard
|
||||
}
|
||||
|
||||
color: Theme.surfaceText
|
||||
font.pixelSize: Appearance.fontSize.normal
|
||||
font.family: resolvedFontFamily
|
||||
font.weight: Theme.fontWeight
|
||||
textFormat: Text.PlainText
|
||||
wrapMode: Text.WordWrap
|
||||
elide: Text.ElideRight
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
renderType: resolvedRenderType
|
||||
renderTypeQuality: resolvedRenderQuality
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: standardAnimation.duration
|
||||
easing.type: standardAnimation["easing.type"]
|
||||
easing.bezierCurve: standardAnimation["easing.bezierCurve"]
|
||||
}
|
||||
}
|
||||
}
|
||||
DankCommon.StyledText {}
|
||||
|
||||
@@ -1,24 +1,3 @@
|
||||
import QtQuick
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
import qs.DankCommon.Widgets as DankCommon
|
||||
|
||||
TextMetrics {
|
||||
property bool isMonospace: false
|
||||
|
||||
readonly property string resolvedFontFamily: {
|
||||
const requestedFont = isMonospace ? SettingsData.monoFontFamily : SettingsData.fontFamily;
|
||||
const defaultFont = isMonospace ? Theme.defaultMonoFontFamily : Theme.defaultFontFamily;
|
||||
|
||||
if (requestedFont === defaultFont) {
|
||||
const availableFonts = Qt.fontFamilies();
|
||||
if (!availableFonts.includes(requestedFont)) {
|
||||
return isMonospace ? "Monospace" : "DejaVu Sans";
|
||||
}
|
||||
}
|
||||
return requestedFont;
|
||||
}
|
||||
|
||||
font.pixelSize: Appearance.fontSize.normal
|
||||
font.family: resolvedFontFamily
|
||||
font.weight: SettingsData.fontWeight
|
||||
}
|
||||
DankCommon.StyledTextMetrics {}
|
||||
|
||||
@@ -1,81 +1,3 @@
|
||||
import QtQuick
|
||||
import QtQuick.Effects
|
||||
import Quickshell
|
||||
import Quickshell.Widgets
|
||||
import qs.Common
|
||||
import qs.Widgets
|
||||
import qs.DankCommon.Widgets as DankCommon
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property string colorOverride: ""
|
||||
property real brightnessOverride: 0.5
|
||||
property real contrastOverride: 1
|
||||
|
||||
readonly property bool hasColorOverride: colorOverride !== ""
|
||||
|
||||
property bool useNerdFont: false
|
||||
property string nerdFontIcon: ""
|
||||
|
||||
IconImage {
|
||||
id: iconImage
|
||||
anchors.fill: parent
|
||||
visible: !root.useNerdFont
|
||||
|
||||
smooth: true
|
||||
asynchronous: true
|
||||
layer.enabled: hasColorOverride
|
||||
|
||||
layer.effect: MultiEffect {
|
||||
colorization: 1
|
||||
colorizationColor: colorOverride
|
||||
brightness: brightnessOverride
|
||||
contrast: contrastOverride
|
||||
}
|
||||
}
|
||||
|
||||
DankNFIcon {
|
||||
id: nfIcon
|
||||
anchors.centerIn: parent
|
||||
visible: root.useNerdFont
|
||||
name: root.nerdFontIcon
|
||||
size: Math.min(root.width, root.height)
|
||||
color: hasColorOverride ? colorOverride : Theme.surfaceText
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
Proc.runCommand(null, ["sh", "-c", ". /etc/os-release && echo $ID"], (output, exitCode) => {
|
||||
if (!root || exitCode !== 0 || !output) return
|
||||
const distroId = output.trim()
|
||||
if (!distroId) return
|
||||
|
||||
const supportedDistroNFs = ["debian", "arch", "archcraft", "fedora", "nixos", "ubuntu", "guix", "gentoo", "endeavouros", "manjaro", "opensuse"]
|
||||
if (supportedDistroNFs.includes(distroId)) {
|
||||
if (!root) return
|
||||
root.useNerdFont = true
|
||||
root.nerdFontIcon = distroId
|
||||
return
|
||||
}
|
||||
|
||||
Proc.runCommand(null, ["sh", "-c", ". /etc/os-release && echo $LOGO"], (logoOutput, logoExitCode) => {
|
||||
if (!root || !iconImage || logoExitCode !== 0 || !logoOutput) return
|
||||
const logo = logoOutput.trim()
|
||||
if (!logo) return
|
||||
|
||||
if (logo === "cachyos") {
|
||||
iconImage.source = "file:///usr/share/icons/cachyos.svg"
|
||||
return
|
||||
}
|
||||
if (logo === "guix-icon") {
|
||||
iconImage.source = "file:///run/current-system/profile/share/icons/hicolor/scalable/apps/guix-icon.svg"
|
||||
return
|
||||
}
|
||||
if (logo === "zirconium") {
|
||||
iconImage.source = "file:///usr/share/zirconium/pixmaps/logo-z.svg"
|
||||
return
|
||||
}
|
||||
iconImage.source = Quickshell.iconPath(logo, true)
|
||||
}, 0)
|
||||
}, 0)
|
||||
}
|
||||
}
|
||||
DankCommon.SystemLogo {}
|
||||
|
||||
Binary file not shown.
@@ -1,92 +0,0 @@
|
||||
Copyright (c) 2016 The Inter Project Authors (https://github.com/rsms/inter)
|
||||
|
||||
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||
This license is copied below, and is also available with a FAQ at:
|
||||
http://scripts.sil.org/OFL
|
||||
|
||||
-----------------------------------------------------------
|
||||
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||
-----------------------------------------------------------
|
||||
|
||||
PREAMBLE
|
||||
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||
development of collaborative font projects, to support the font creation
|
||||
efforts of academic and linguistic communities, and to provide a free and
|
||||
open framework in which fonts may be shared and improved in partnership
|
||||
with others.
|
||||
|
||||
The OFL allows the licensed fonts to be used, studied, modified and
|
||||
redistributed freely as long as they are not sold by themselves. The
|
||||
fonts, including any derivative works, can be bundled, embedded,
|
||||
redistributed and/or sold with any software provided that any reserved
|
||||
names are not used by derivative works. The fonts and derivatives,
|
||||
however, cannot be released under any other type of license. The
|
||||
requirement for fonts to remain under this license does not apply
|
||||
to any document created using the fonts or their derivatives.
|
||||
|
||||
DEFINITIONS
|
||||
"Font Software" refers to the set of files released by the Copyright
|
||||
Holder(s) under this license and clearly marked as such. This may
|
||||
include source files, build scripts and documentation.
|
||||
|
||||
"Reserved Font Name" refers to any names specified as such after the
|
||||
copyright statement(s).
|
||||
|
||||
"Original Version" refers to the collection of Font Software components as
|
||||
distributed by the Copyright Holder(s).
|
||||
|
||||
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||
or substituting -- in part or in whole -- any of the components of the
|
||||
Original Version, by changing formats or by porting the Font Software to a
|
||||
new environment.
|
||||
|
||||
"Author" refers to any designer, engineer, programmer, technical
|
||||
writer or other person who contributed to the Font Software.
|
||||
|
||||
PERMISSION AND CONDITIONS
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||
redistribute, and sell modified and unmodified copies of the Font
|
||||
Software, subject to the following conditions:
|
||||
|
||||
1) Neither the Font Software nor any of its individual components,
|
||||
in Original or Modified Versions, may be sold by itself.
|
||||
|
||||
2) Original or Modified Versions of the Font Software may be bundled,
|
||||
redistributed and/or sold with any software, provided that each copy
|
||||
contains the above copyright notice and this license. These can be
|
||||
included either as stand-alone text files, human-readable headers or
|
||||
in the appropriate machine-readable metadata fields within text or
|
||||
binary files as long as those fields can be easily viewed by the user.
|
||||
|
||||
3) No Modified Version of the Font Software may use the Reserved Font
|
||||
Name(s) unless explicit written permission is granted by the corresponding
|
||||
Copyright Holder. This restriction only applies to the primary font name as
|
||||
presented to the users.
|
||||
|
||||
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||
Software shall not be used to promote, endorse or advertise any
|
||||
Modified Version, except to acknowledge the contribution(s) of the
|
||||
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||
permission.
|
||||
|
||||
5) The Font Software, modified or unmodified, in part or in whole,
|
||||
must be distributed entirely under this license, and must not be
|
||||
distributed under any other license. The requirement for fonts to
|
||||
remain under this license does not apply to any document created
|
||||
using the Font Software.
|
||||
|
||||
TERMINATION
|
||||
This license becomes null and void if any of the above conditions are
|
||||
not met.
|
||||
|
||||
DISCLAIMER
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user