1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2025-12-05 21:15:38 -05:00

Compare commits

...

7 Commits

Author SHA1 Message Date
bbedward
734456785f matugen: log worker messages 2025-11-27 00:53:32 -05:00
bbedward
4f24312432 matugen: always set color scheme on exit 2025-11-27 00:31:56 -05:00
bbedward
d79b1ff3b4 displays: show physical resolution/mode instead of logical
fixes #819
2025-11-26 23:54:19 -05:00
bbedward
bbe1c1f1e0 filebrowser: re-add layer surface version 2025-11-26 23:51:59 -05:00
purian23
1978e67401 Update dms-cli for OBS packages 2025-11-26 23:27:33 -05:00
purian23
e129e4a2d0 Update dms-cli for nightly builds 2025-11-26 22:17:49 -05:00
Lucas
f7f1bbbdd2 nix: fix NixOS systemd service PATH (#823) 2025-11-26 18:30:06 -05:00
27 changed files with 1412 additions and 1216 deletions

View File

@@ -188,23 +188,28 @@ jobs:
sed -i "s|<param name=\"revision\">v[0-9.]*</param>|<param name=\"revision\">$VERSION</param>|" "$service" sed -i "s|<param name=\"revision\">v[0-9.]*</param>|<param name=\"revision\">$VERSION</param>|" "$service"
fi fi
done done
- name: Install Go
uses: actions/setup-go@v5
with:
go-version: '1.24'
- name: Install OSC - name: Install OSC
run: | run: |
sudo apt-get update sudo apt-get update
sudo apt-get install -y osc sudo apt-get install -y osc
mkdir -p ~/.config/osc mkdir -p ~/.config/osc
cat > ~/.config/osc/oscrc << EOF cat > ~/.config/osc/oscrc << EOF
[general] [general]
apiurl = https://api.opensuse.org apiurl = https://api.opensuse.org
[https://api.opensuse.org] [https://api.opensuse.org]
user = ${{ secrets.OBS_USERNAME }} user = ${{ secrets.OBS_USERNAME }}
pass = ${{ secrets.OBS_PASSWORD }} pass = ${{ secrets.OBS_PASSWORD }}
EOF EOF
chmod 600 ~/.config/osc/oscrc chmod 600 ~/.config/osc/oscrc
- name: Upload to OBS - name: Upload to OBS
env: env:
FORCE_REBUILD: ${{ github.event_name == 'workflow_dispatch' && 'true' || '' }} FORCE_REBUILD: ${{ github.event_name == 'workflow_dispatch' && 'true' || '' }}

View File

@@ -24,6 +24,12 @@ jobs:
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: '1.24'
cache: false
- name: Install build dependencies - name: Install build dependencies
run: | run: |
sudo apt-get update sudo apt-get update

View File

@@ -1,5 +1,5 @@
<services> <services>
<!-- Pull full git repository for master branch --> <!-- Git source and vendoring -->
<service name="tar_scm" mode="disabled"> <service name="tar_scm" mode="disabled">
<param name="scm">git</param> <param name="scm">git</param>
<param name="url">https://github.com/AvengeMedia/DankMaterialShell.git</param> <param name="url">https://github.com/AvengeMedia/DankMaterialShell.git</param>
@@ -10,7 +10,7 @@
<param name="file">*.tar</param> <param name="file">*.tar</param>
<param name="compression">gz</param> <param name="compression">gz</param>
</service> </service>
<!-- Download pre-built binaries (fallback for Debian 13 with Go 1.22) --> <!-- Binary downloads kept for backwards compat - removed after successful source builds
<service name="download_url"> <service name="download_url">
<param name="protocol">https</param> <param name="protocol">https</param>
<param name="host">github.com</param> <param name="host">github.com</param>
@@ -21,4 +21,5 @@
<param name="host">github.com</param> <param name="host">github.com</param>
<param name="path">/AvengeMedia/DankMaterialShell/releases/latest/download/dms-distropkg-arm64.gz</param> <param name="path">/AvengeMedia/DankMaterialShell/releases/latest/download/dms-distropkg-arm64.gz</param>
</service> </service>
-->
</services> </services>

View File

@@ -1,8 +1,12 @@
dms-git (0.6.2+git) nightly; urgency=medium dms-git (0.6.2+git5) nightly; urgency=medium
* Fix debian/rules to use source at root level (native format)
* Remove incorrect dms-git-source subdirectory references
* Fix Build-Depends parsing path issue in obs-upload.sh auto-increment
* Fix Build-Depends parsing in obs-upload.sh for proper dependency extraction
* Build dms binary from source for true git version strings * Build dms binary from source for true git version strings
* Match Fedora COPR git build behavior * Match Fedora COPR git build behavior
* Now shows proper git version (e.g., v0.6.2-11-g12e91534) * Now shows proper git version (e.g., v0.6.2-11-g12e91534)
* Add golang-go and make as build dependencies * Add golang-go and make as build dependencies
-- Avenge Media <AvengeMedia.US@gmail.com> Fri, 22 Nov 2025 00:00:00 -0500 -- Avenge Media <AvengeMedia.US@gmail.com> Wed, 27 Nov 2025 04:15:00 -0500

View File

@@ -2,7 +2,8 @@ Source: dms-git
Section: x11 Section: x11
Priority: optional Priority: optional
Maintainer: Avenge Media <AvengeMedia.US@gmail.com> Maintainer: Avenge Media <AvengeMedia.US@gmail.com>
Build-Depends: debhelper-compat (= 13) Build-Depends: debhelper-compat (= 13),
golang-go | golang (>= 2:1.22~) | golang-any
Standards-Version: 4.6.2 Standards-Version: 4.6.2
Homepage: https://github.com/AvengeMedia/DankMaterialShell Homepage: https://github.com/AvengeMedia/DankMaterialShell
Vcs-Browser: https://github.com/AvengeMedia/DankMaterialShell Vcs-Browser: https://github.com/AvengeMedia/DankMaterialShell

View File

@@ -1,31 +1,55 @@
#!/usr/bin/make -f #!/usr/bin/make -f
export DH_VERBOSE = 1
# Version info from changelog
DEB_VERSION := $(shell dpkg-parsechangelog -S Version) DEB_VERSION := $(shell dpkg-parsechangelog -S Version)
UPSTREAM_VERSION := $(shell echo $(DEB_VERSION) | sed 's/-[^-]*$$//') UPSTREAM_VERSION := $(shell echo $(DEB_VERSION) | sed 's/-[^-]*$$//')
DEB_HOST_ARCH := $(shell dpkg-architecture -qDEB_HOST_ARCH) DEB_HOST_ARCH := $(shell dpkg-architecture -qDEB_HOST_ARCH)
# Go needs writable directories for cache
export HOME := $(CURDIR)/debian/tmp-home
export GOCACHE := $(CURDIR)/debian/tmp-home/go-cache
export GOMODCACHE := $(CURDIR)/debian/tmp-home/go-mod
export GOTOOLCHAIN := local
%: %:
dh $@ dh $@
override_dh_auto_build: override_dh_auto_build:
# Create Go cache directories
mkdir -p $(HOME) $(GOCACHE) $(GOMODCACHE)
# Verify core directory exists (native package format has source at root)
test -d core || (echo "ERROR: core directory not found!" && exit 1)
# Patch go.mod to use Go 1.24 base version (Debian 13 has 1.23.x, may vary)
sed -i 's/^go 1\.24\.[0-9]*/go 1.24/' core/go.mod
# Extract version info for embedding
VERSION="$(UPSTREAM_VERSION)"
COMMIT=$$(echo "$(UPSTREAM_VERSION)" | grep -oP '(?<=git)[0-9]+\.[a-f0-9]+' | cut -d. -f2 | head -c8 || echo "unknown")
# Build dms-cli from source using vendored dependencies
# Architecture mapping: Debian amd64/arm64 -> Makefile amd64/arm64
if [ "$(DEB_HOST_ARCH)" = "amd64" ]; then \ if [ "$(DEB_HOST_ARCH)" = "amd64" ]; then \
if [ -f dms-distropkg-amd64.gz ]; then \ MAKE_ARCH=amd64; \
gunzip -c dms-distropkg-amd64.gz > dms; \ BINARY_NAME=dms-linux-amd64; \
elif [ -f ../SOURCES/dms-distropkg-amd64.gz ]; then \
gunzip -c ../SOURCES/dms-distropkg-amd64.gz > dms; \
else \
echo "ERROR: dms-distropkg-amd64.gz not found!" && exit 1; \
fi \
elif [ "$(DEB_HOST_ARCH)" = "arm64" ]; then \ elif [ "$(DEB_HOST_ARCH)" = "arm64" ]; then \
if [ -f dms-distropkg-arm64.gz ]; then \ MAKE_ARCH=arm64; \
gunzip -c dms-distropkg-arm64.gz > dms; \ BINARY_NAME=dms-linux-arm64; \
elif [ -f ../SOURCES/dms-distropkg-arm64.gz ]; then \
gunzip -c ../SOURCES/dms-distropkg-arm64.gz > dms; \
else \
echo "ERROR: dms-distropkg-arm64.gz not found!" && exit 1; \
fi \
else \ else \
echo "Unsupported architecture: $(DEB_HOST_ARCH)" && exit 1; \ echo "ERROR: Unsupported architecture: $(DEB_HOST_ARCH)" && exit 1; \
fi; \
echo "Building with VERSION=$$VERSION COMMIT=$$COMMIT ARCH=$$MAKE_ARCH"; \
cd core && $(MAKE) GOFLAGS="-mod=vendor" dist ARCH=$$MAKE_ARCH VERSION="$$VERSION" COMMIT="$$COMMIT"
# Copy binary to expected location
if [ "$(DEB_HOST_ARCH)" = "amd64" ]; then \
cp core/bin/dms-linux-amd64 dms; \
elif [ "$(DEB_HOST_ARCH)" = "arm64" ]; then \
cp core/bin/dms-linux-arm64 dms; \
fi fi
chmod +x dms chmod +x dms
@@ -36,11 +60,8 @@ override_dh_auto_install:
if [ -d quickshell ]; then \ if [ -d quickshell ]; then \
cp -r quickshell/* debian/dms-git/usr/share/quickshell/dms/; \ cp -r quickshell/* debian/dms-git/usr/share/quickshell/dms/; \
install -Dm644 quickshell/assets/systemd/dms.service debian/dms-git/usr/lib/systemd/user/dms.service; \ install -Dm644 quickshell/assets/systemd/dms.service debian/dms-git/usr/lib/systemd/user/dms.service; \
elif [ -d dms-git-source/quickshell ]; then \
cp -r dms-git-source/quickshell/* debian/dms-git/usr/share/quickshell/dms/; \
install -Dm644 dms-git-source/quickshell/assets/systemd/dms.service debian/dms-git/usr/lib/systemd/user/dms.service; \
else \ else \
echo "ERROR: quickshell directory not found (checked root and dms-git-source/)!" && \ echo "ERROR: quickshell directory not found!" && \
echo "Contents of current directory:" && ls -la && \ echo "Contents of current directory:" && ls -la && \
exit 1; \ exit 1; \
fi fi
@@ -49,6 +70,8 @@ override_dh_auto_install:
debian/dms-git/usr/share/quickshell/dms/distro debian/dms-git/usr/share/quickshell/dms/distro
override_dh_auto_clean: override_dh_auto_clean:
# Clean up build artifacts
rm -f dms rm -f dms
[ ! -d dms-git-source ] || rm -rf dms-git-source rm -rf core/bin
rm -rf debian/tmp-home
dh_auto_clean dh_auto_clean

View File

@@ -1 +1 @@
dms-distropkg-amd64.gz # dms-cli is built from source

View File

@@ -18,7 +18,7 @@ in {
systemd.user.services.dms = lib.mkIf cfg.systemd.enable { systemd.user.services.dms = lib.mkIf cfg.systemd.enable {
description = "DankMaterialShell"; description = "DankMaterialShell";
path = [cfg.quickshell.package]; path = lib.mkForce [];
partOf = ["graphical-session.target"]; partOf = ["graphical-session.target"];
after = ["graphical-session.target"]; after = ["graphical-session.target"];

View File

@@ -1,16 +1,16 @@
<services> <services>
<!-- Pull full git repository for master branch (QML code) --> <!-- Git source and vendoring -->
<service name="tar_scm"> <service name="tar_scm" mode="disabled">
<param name="scm">git</param> <param name="scm">git</param>
<param name="url">https://github.com/AvengeMedia/DankMaterialShell.git</param> <param name="url">https://github.com/AvengeMedia/DankMaterialShell.git</param>
<param name="revision">master</param> <param name="revision">master</param>
<param name="filename">dms-git-source</param> <param name="filename">dms-git-source</param>
</service> </service>
<service name="recompress"> <service name="recompress" mode="disabled">
<param name="file">*.tar</param> <param name="file">*.tar</param>
<param name="compression">gz</param> <param name="compression">gz</param>
</service> </service>
<!-- Download pre-built binaries --> <!-- Binary downloads removed - building from source
<service name="download_url"> <service name="download_url">
<param name="protocol">https</param> <param name="protocol">https</param>
<param name="host">github.com</param> <param name="host">github.com</param>
@@ -21,4 +21,5 @@
<param name="host">github.com</param> <param name="host">github.com</param>
<param name="path">/AvengeMedia/DankMaterialShell/releases/latest/download/dms-distropkg-arm64.gz</param> <param name="path">/AvengeMedia/DankMaterialShell/releases/latest/download/dms-distropkg-arm64.gz</param>
</service> </service>
-->
</services> </services>

View File

@@ -9,10 +9,10 @@ Summary: DankMaterialShell - Material 3 inspired shell (git nightly)
License: MIT License: MIT
URL: https://github.com/AvengeMedia/DankMaterialShell URL: https://github.com/AvengeMedia/DankMaterialShell
Source0: dms-git-source.tar.gz Source0: dms-git-source.tar.gz
Source1: dms-distropkg-amd64.gz
Source2: dms-distropkg-arm64.gz
BuildRequires: gzip BuildRequires: golang >= 1.22
BuildRequires: golang-packaging
BuildRequires: git-core
BuildRequires: systemd-rpm-macros BuildRequires: systemd-rpm-macros
Requires: (quickshell-git or quickshell) Requires: (quickshell-git or quickshell)
@@ -44,15 +44,40 @@ and fixes. Includes pre-built dms CLI binary and QML shell files.
%prep %prep
%setup -q -n dms-git-source %setup -q -n dms-git-source
%ifarch x86_64 # Verify vendored Go dependencies exist (vendored by obs-upload.sh before packaging)
gunzip -c %{SOURCE1} > dms # OBS build environment has no network access
%endif test -d core/vendor || (echo "ERROR: Go vendor directory missing!" && exit 1)
%ifarch aarch64
gunzip -c %{SOURCE2} > dms
%endif
chmod +x dms
%build %build
# Create Go cache directories (OBS build env may have restricted HOME)
export HOME=%{_builddir}/go-home
export GOCACHE=%{_builddir}/go-cache
export GOMODCACHE=%{_builddir}/go-mod
mkdir -p $HOME $GOCACHE $GOMODCACHE
# OBS has no network access, so use local toolchain only
export GOTOOLCHAIN=local
# Patch go.mod to use base Go version (e.g., go 1.24 instead of go 1.24.6)
sed -i 's/^go 1\.24\.[0-9]*/go 1.24/' core/go.mod
# Extract version info for embedding in binary
VERSION="%{version}"
COMMIT=$(echo "%{version}" | grep -oP '(?<=git)[0-9]+\.[a-f0-9]+' | cut -d. -f2 | head -c8 || echo "unknown")
# Build dms-cli from source using vendored dependencies
# Architecture mapping: RPM x86_64/aarch64 -> Makefile amd64/arm64
cd core
%ifarch x86_64
make GOFLAGS="-mod=vendor" dist ARCH=amd64 VERSION="$VERSION" COMMIT="$COMMIT"
mv bin/dms-linux-amd64 ../dms
%endif
%ifarch aarch64
make GOFLAGS="-mod=vendor" dist ARCH=arm64 VERSION="$VERSION" COMMIT="$COMMIT"
mv bin/dms-linux-arm64 ../dms
%endif
cd ..
chmod +x dms
%install %install
install -Dm755 dms %{buildroot}%{_bindir}/dms install -Dm755 dms %{buildroot}%{_bindir}/dms

View File

@@ -400,9 +400,32 @@ if [[ "$UPLOAD_DEBIAN" == true ]] && [[ -d "distro/debian/$PACKAGE/debian" ]]; t
fi fi
echo " Found source directory: $SOURCE_DIR" echo " Found source directory: $SOURCE_DIR"
# Vendor Go dependencies for dms-git
if [[ "$PACKAGE" == "dms-git" ]] && [[ -d "$SOURCE_DIR/core" ]]; then
echo " - Vendoring Go dependencies for offline OBS build..."
cd "$SOURCE_DIR/core"
if ! command -v go &> /dev/null; then
echo "ERROR: Go not found. Install Go to vendor dependencies."
echo " Install: sudo apt-get install golang-go (Debian/Ubuntu)"
echo " or: sudo dnf install golang (Fedora)"
exit 1
fi
# Vendor dependencies
go mod vendor
if [ ! -d "vendor" ]; then
echo "ERROR: Failed to vendor Go dependencies"
exit 1
fi
VENDOR_SIZE=$(du -sh vendor | cut -f1)
echo " ✓ Go dependencies vendored ($VENDOR_SIZE)"
cd "$REPO_ROOT"
fi
# Create OpenSUSE-compatible source tarballs BEFORE adding debian/ directory # Create OpenSUSE-compatible source tarballs BEFORE adding debian/ directory
# (OpenSUSE doesn't need debian/ directory)
if [[ "$UPLOAD_OPENSUSE" == true ]] && [[ -f "distro/opensuse/$PACKAGE.spec" ]]; then if [[ "$UPLOAD_OPENSUSE" == true ]] && [[ -f "distro/opensuse/$PACKAGE.spec" ]]; then
echo " - Creating OpenSUSE-compatible source tarballs" echo " - Creating OpenSUSE-compatible source tarballs"
@@ -518,16 +541,29 @@ if [[ "$UPLOAD_DEBIAN" == true ]] && [[ -d "distro/debian/$PACKAGE/debian" ]]; t
TARBALL_SIZE=$(stat -c%s "$WORK_DIR/$COMBINED_TARBALL" 2>/dev/null || stat -f%z "$WORK_DIR/$COMBINED_TARBALL" 2>/dev/null) TARBALL_SIZE=$(stat -c%s "$WORK_DIR/$COMBINED_TARBALL" 2>/dev/null || stat -f%z "$WORK_DIR/$COMBINED_TARBALL" 2>/dev/null)
TARBALL_MD5=$(md5sum "$WORK_DIR/$COMBINED_TARBALL" | cut -d' ' -f1) TARBALL_MD5=$(md5sum "$WORK_DIR/$COMBINED_TARBALL" | cut -d' ' -f1)
BUILD_DEPS="debhelper-compat (= 13)" # Extract Build-Depends from debian/control using awk for proper multi-line parsing
if [[ -f "distro/debian/$PACKAGE/debian/control" ]]; then if [[ -f "$REPO_ROOT/distro/debian/$PACKAGE/debian/control" ]]; then
CONTROL_DEPS=$(sed -n '/^Build-Depends:/,/^[A-Z]/p' "distro/debian/$PACKAGE/debian/control" | \ BUILD_DEPS=$(awk '
sed '/^Build-Depends:/s/^Build-Depends: *//' | \ /^Build-Depends:/ {
sed '/^[A-Z]/d' | \ in_build_deps=1;
tr '\n' ' ' | \ sub(/^Build-Depends:[[:space:]]*/, "");
sed 's/^[[:space:]]*//;s/[[:space:]]*$//;s/[[:space:]]\+/ /g') printf "%s", $0;
if [[ -n "$CONTROL_DEPS" && "$CONTROL_DEPS" != "" ]]; then next;
BUILD_DEPS="$CONTROL_DEPS" }
in_build_deps && /^[[:space:]]/ {
sub(/^[[:space:]]+/, " ");
printf "%s", $0;
next;
}
in_build_deps { exit; }
' "$REPO_ROOT/distro/debian/$PACKAGE/debian/control" | sed 's/[[:space:]]\+/ /g; s/^[[:space:]]*//; s/[[:space:]]*$//')
# If extraction failed or is empty, use default fallback
if [[ -z "$BUILD_DEPS" ]]; then
BUILD_DEPS="debhelper-compat (= 13)"
fi fi
else
BUILD_DEPS="debhelper-compat (= 13)"
fi fi
cat > "$WORK_DIR/$PACKAGE.dsc" << EOF cat > "$WORK_DIR/$PACKAGE.dsc" << EOF
@@ -774,16 +810,29 @@ if [[ "$UPLOAD_DEBIAN" == true ]] && [[ "$SOURCE_FORMAT" == *"native"* ]] && [[
TARBALL_SIZE=$(stat -c%s "$WORK_DIR/$COMBINED_TARBALL" 2>/dev/null || stat -f%z "$WORK_DIR/$COMBINED_TARBALL" 2>/dev/null) TARBALL_SIZE=$(stat -c%s "$WORK_DIR/$COMBINED_TARBALL" 2>/dev/null || stat -f%z "$WORK_DIR/$COMBINED_TARBALL" 2>/dev/null)
TARBALL_MD5=$(md5sum "$WORK_DIR/$COMBINED_TARBALL" | cut -d' ' -f1) TARBALL_MD5=$(md5sum "$WORK_DIR/$COMBINED_TARBALL" | cut -d' ' -f1)
BUILD_DEPS="debhelper-compat (= 13)" # Extract Build-Depends from debian/control using awk for proper multi-line parsing
if [[ -f "distro/debian/$PACKAGE/debian/control" ]]; then if [[ -f "$REPO_ROOT/distro/debian/$PACKAGE/debian/control" ]]; then
CONTROL_DEPS=$(sed -n '/^Build-Depends:/,/^[A-Z]/p' "distro/debian/$PACKAGE/debian/control" | \ BUILD_DEPS=$(awk '
sed '/^Build-Depends:/s/^Build-Depends: *//' | \ /^Build-Depends:/ {
sed '/^[A-Z]/d' | \ in_build_deps=1;
tr '\n' ' ' | \ sub(/^Build-Depends:[[:space:]]*/, "");
sed 's/^[[:space:]]*//;s/[[:space:]]*$//;s/[[:space:]]\+/ /g') printf "%s", $0;
if [[ -n "$CONTROL_DEPS" && "$CONTROL_DEPS" != "" ]]; then next;
BUILD_DEPS="$CONTROL_DEPS" }
in_build_deps && /^[[:space:]]/ {
sub(/^[[:space:]]+/, " ");
printf "%s", $0;
next;
}
in_build_deps { exit; }
' "$REPO_ROOT/distro/debian/$PACKAGE/debian/control" | sed 's/[[:space:]]\+/ /g; s/^[[:space:]]*//; s/[[:space:]]*$//')
# If extraction failed or is empty, use default fallback
if [[ -z "$BUILD_DEPS" ]]; then
BUILD_DEPS="debhelper-compat (= 13)"
fi fi
else
BUILD_DEPS="debhelper-compat (= 13)"
fi fi
cat > "$WORK_DIR/$PACKAGE.dsc" << EOF cat > "$WORK_DIR/$PACKAGE.dsc" << EOF

View File

@@ -296,6 +296,30 @@ if [ "$IS_GIT_PACKAGE" = true ] && [ -n "$GIT_REPO" ]; then
# Now clone to source directory (without .git for inclusion in package) # Now clone to source directory (without .git for inclusion in package)
rm -rf "$SOURCE_DIR" rm -rf "$SOURCE_DIR"
cp -r "$TEMP_CLONE" "$SOURCE_DIR" cp -r "$TEMP_CLONE" "$SOURCE_DIR"
# Save version info for dms-git build process
if [ "$PACKAGE_NAME" = "dms-git" ]; then
info "Saving version info to .dms-version for build process..."
echo "VERSION=${UPSTREAM_VERSION}+git${GIT_COMMIT_COUNT}.${GIT_COMMIT_HASH}" > "$SOURCE_DIR/.dms-version"
echo "COMMIT=${GIT_COMMIT_HASH}" >> "$SOURCE_DIR/.dms-version"
success "Version info saved: ${UPSTREAM_VERSION}+git${GIT_COMMIT_COUNT}.${GIT_COMMIT_HASH}"
# Vendor Go dependencies (Launchpad has no internet access)
info "Vendoring Go dependencies for offline build..."
cd "$SOURCE_DIR/core"
# Create vendor directory with all dependencies
go mod vendor
if [ ! -d "vendor" ]; then
error "Failed to vendor Go dependencies"
exit 1
fi
success "Go dependencies vendored successfully"
cd "$PACKAGE_DIR"
fi
rm -rf "$SOURCE_DIR/.git" rm -rf "$SOURCE_DIR/.git"
rm -rf "$TEMP_CLONE" rm -rf "$TEMP_CLONE"
@@ -349,21 +373,6 @@ if [ "$IS_GIT_PACKAGE" = true ] && [ -n "$GIT_REPO" ]; then
fi fi
fi fi
# Download pre-built binary for dms-git
# dms-git uses latest release binary with git master QML files
if [ "$PACKAGE_NAME" = "dms-git" ]; then
info "Downloading latest release binary for dms-git..."
if [ ! -f "dms-distropkg-amd64.gz" ]; then
if wget -O dms-distropkg-amd64.gz "https://github.com/AvengeMedia/DankMaterialShell/releases/latest/download/dms-distropkg-amd64.gz"; then
success "Latest release binary downloaded"
else
error "Failed to download dms-distropkg-amd64.gz"
exit 1
fi
else
info "Release binary already downloaded"
fi
fi
success "Source prepared for packaging" success "Source prepared for packaging"
else else

View File

@@ -218,12 +218,7 @@ if [ "$KEEP_BUILDS" = "false" ]; then
fi fi
;; ;;
dms-git) dms-git)
# Remove downloaded binary # Remove git source directory binary
if [ -f "$PACKAGE_DIR/dms-distropkg-amd64.gz" ]; then
rm -f "$PACKAGE_DIR/dms-distropkg-amd64.gz"
REMOVED=$((REMOVED + 1))
fi
# Remove git source directory
if [ -d "$PACKAGE_DIR/dms-git-repo" ]; then if [ -d "$PACKAGE_DIR/dms-git-repo" ]; then
rm -rf "$PACKAGE_DIR/dms-git-repo" rm -rf "$PACKAGE_DIR/dms-git-repo"
REMOVED=$((REMOVED + 1)) REMOVED=$((REMOVED + 1))

View File

@@ -1,5 +1,5 @@
dms-git (0.6.2+git2094.6cc6e7c8ppa1) questing; urgency=medium dms-git (0.6.2+git2169.f7f1bbbdppa10) questing; urgency=medium
* Git snapshot (commit 2094: 6cc6e7c8) * Git snapshot (commit 2169: f7f1bbbd)
-- Avenge Media <AvengeMedia.US@gmail.com> Sun, 23 Nov 2025 00:43:28 -0500 -- Avenge Media <AvengeMedia.US@gmail.com> Wed, 26 Nov 2025 21:43:12 -0500

View File

@@ -2,7 +2,8 @@ Source: dms-git
Section: x11 Section: x11
Priority: optional Priority: optional
Maintainer: Avenge Media <AvengeMedia.US@gmail.com> Maintainer: Avenge Media <AvengeMedia.US@gmail.com>
Build-Depends: debhelper-compat (= 13) Build-Depends: debhelper-compat (= 13),
golang-go (>= 2:1.22~)
Standards-Version: 4.6.2 Standards-Version: 4.6.2
Homepage: https://github.com/AvengeMedia/DankMaterialShell Homepage: https://github.com/AvengeMedia/DankMaterialShell
Vcs-Browser: https://github.com/AvengeMedia/DankMaterialShell Vcs-Browser: https://github.com/AvengeMedia/DankMaterialShell

View File

@@ -1 +1 @@
dms-git_0.6.2+git2094.6cc6e7c8ppa1_source.buildinfo x11 optional dms-git_0.6.2+git2169.f7f1bbbdppa10_source.buildinfo x11 optional

View File

@@ -6,19 +6,38 @@ export DH_VERBOSE = 1
GIT_DATE := $(shell date +%Y%m%d) GIT_DATE := $(shell date +%Y%m%d)
GIT_COMMIT := HEAD GIT_COMMIT := HEAD
# Go needs writable directories for its caches
export HOME := $(CURDIR)/debian/tmp-home
export GOCACHE := $(CURDIR)/debian/tmp-home/go-cache
export GOMODCACHE := $(CURDIR)/debian/tmp-home/go-mod
# Launchpad has no network access, so use local toolchain only
export GOTOOLCHAIN := local
%: %:
dh $@ dh $@
override_dh_auto_build: override_dh_auto_build:
# Git source is already included in source package (cloned by build-source.sh) # Create Go cache directories (sbuild sets HOME to non-existent path)
mkdir -p $(HOME) $(GOCACHE) $(GOMODCACHE)
# Git source is already included in source package (cloned by ppa-build.sh)
# Launchpad build environment has no internet access # Launchpad build environment has no internet access
test -d dms-git-repo || (echo "ERROR: dms-git-repo directory not found!" && exit 1) test -d dms-git-repo || (echo "ERROR: dms-git-repo directory not found!" && exit 1)
test -f dms-distropkg-amd64.gz || (echo "ERROR: dms-distropkg-amd64.gz not found!" && exit 1)
# Extract pre-built binary from latest release # Patch go.mod to use Go 1.24 base version (Ubuntu has 1.24.4, project requires 1.24.6)
# Note: For git versions, we use the latest release binary sed -i 's/^go 1\.24\.[0-9]*/go 1.24/' dms-git-repo/core/go.mod
# The QML files come from git master
gunzip -c dms-distropkg-amd64.gz > dms # Build dms-cli from source
@if [ -f dms-git-repo/.dms-version ]; then \
. dms-git-repo/.dms-version; \
echo "Building with VERSION=$$VERSION COMMIT=$$COMMIT"; \
cd dms-git-repo/core && $(MAKE) GOFLAGS="-mod=vendor" dist ARCH=amd64 VERSION="$$VERSION" COMMIT="$$COMMIT"; \
else \
echo "Warning: .dms-version not found, building without version info"; \
cd dms-git-repo/core && $(MAKE) GOFLAGS="-mod=vendor" dist ARCH=amd64; \
fi
cp dms-git-repo/core/bin/dms-linux-amd64 dms
chmod +x dms chmod +x dms
override_dh_auto_install: override_dh_auto_install:
@@ -39,7 +58,8 @@ override_dh_auto_install:
override_dh_auto_clean: override_dh_auto_clean:
# Don't delete dms-git-repo directory - it's part of the source package (native format) # Don't delete dms-git-repo directory - it's part of the source package (native format)
# Clean up build artifacts (but keep dms-distropkg-amd64.gz for Launchpad) # Clean up build artifacts
rm -f dms rm -f dms
# Don't remove dms-distropkg-amd64.gz - it needs to be included in the source package for Launchpad builds rm -rf dms-git-repo/core/bin
rm -rf debian/tmp-home
dh_auto_clean dh_auto_clean

View File

@@ -1 +1 @@
dms-distropkg-amd64.gz # No pre-built binaries needed - dms-cli is built from source

View File

@@ -1,4 +1,3 @@
# Include files that are normally excluded by .gitignore # Include files that are normally excluded by .gitignore
# These are needed for the build process on Launchpad (which has no internet access) # These are needed for the build process on Launchpad (which has no internet access)
tar-ignore = !dms-distropkg-amd64.gz
tar-ignore = !dms-git-repo tar-ignore = !dms-git-repo

View File

@@ -1099,6 +1099,12 @@ Singleton {
Process { Process {
id: systemThemeGenerator id: systemThemeGenerator
running: false running: false
stdout: SplitParser {
onRead: data => console.info("Theme worker:", data)
}
stderr: SplitParser {
onRead: data => console.warn("Theme worker:", data)
}
onExited: exitCode => { onExited: exitCode => {
workerRunning = false; workerRunning = false;

View File

@@ -0,0 +1,891 @@
import Qt.labs.folderlistmodel
import QtCore
import QtQuick
import QtQuick.Controls
import qs.Common
import qs.Widgets
FocusScope {
id: root
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
signal fileSelected(string path)
signal closeRequested
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 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 handleSaveFile(filePath) {
var normalizedPath = filePath;
if (!normalizedPath.startsWith("file://")) {
normalizedPath = "file://" + 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": "Home",
"path": homeDir,
"icon": "home"
},
{
"name": "Documents",
"path": docsDir,
"icon": "description"
},
{
"name": "Downloads",
"path": downloadDir,
"icon": "download"
},
{
"name": "Pictures",
"path": picsDir,
"icon": "image"
},
{
"name": "Music",
"path": musicDir,
"icon": "music_note"
},
{
"name": "Videos",
"path": videosDir,
"icon": "movie"
},
{
"name": "Desktop",
"path": desktopDir,
"icon": "computer"
}
]
FolderListModel {
id: folderModel
showDirsFirst: true
showDotAndDotDot: false
showHidden: root.showHiddenFiles
nameFilters: fileExtensions
showFiles: true
showDirs: true
folder: currentPath ? "file://" + currentPath : "file://" + 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) {
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
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 {
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
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: ScrollBar {
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);
if (isDir) {
navigateTo(path);
} else {
fileSelected(path);
root.closeRequested();
}
}
onItemSelected: (index, path, name, isDir) => {
setSelectedFileData(path, name, isDir);
}
Connections {
function onKeyboardSelectionRequestedChanged() {
if (root.keyboardSelectionRequested && root.keyboardSelectionIndex === index) {
root.keyboardSelectionRequested = false;
selectedIndex = index;
setSelectedFileData(filePath, fileName, fileIsDir);
if (fileIsDir) {
navigateTo(filePath);
} else {
fileSelected(filePath);
root.closeRequested();
}
}
}
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: 2
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);
if (isDir) {
navigateTo(path);
} else {
fileSelected(path);
root.closeRequested();
}
}
onItemSelected: (index, path, name, isDir) => {
setSelectedFileData(path, name, isDir);
}
Connections {
function onKeyboardSelectionRequestedChanged() {
if (root.keyboardSelectionRequested && root.keyboardSelectionIndex === index) {
root.keyboardSelectionRequested = false;
selectedIndex = index;
setSelectedFileData(filePath, fileName, fileIsDir);
if (fileIsDir) {
navigateTo(filePath);
} else {
fileSelected(filePath);
root.closeRequested();
}
}
}
target: root
}
}
}
}
}
}
FileBrowserSaveRow {
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 = "";
}
}
}
}

View File

@@ -1,54 +1,19 @@
import Qt.labs.folderlistmodel
import QtCore
import QtQuick import QtQuick
import QtQuick.Controls
import Quickshell import Quickshell
import qs.Common import qs.Common
import qs.Modals.FileBrowser
import qs.Widgets
FloatingWindow { FloatingWindow {
id: fileBrowserModal id: fileBrowserModal
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: fileBrowserModal.fileExtensions
property string browserTitle: "Select File" property string browserTitle: "Select File"
property string browserIcon: "folder_open" property string browserIcon: "folder_open"
property string browserType: "generic" property string browserType: "generic"
property var fileExtensions: ["*.*"]
property alias filterExtensions: fileBrowserModal.fileExtensions
property bool showHiddenFiles: false property bool showHiddenFiles: false
property int selectedIndex: -1
property bool keyboardNavigationActive: false
property bool backButtonFocused: false
property bool saveMode: false property bool saveMode: false
property string defaultFileName: "" 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 var parentModal: null property var parentModal: null
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 shouldHaveFocus: visible property bool shouldHaveFocus: visible
property bool allowFocusOverride: false property bool allowFocusOverride: false
property bool shouldBeVisible: visible property bool shouldBeVisible: visible
@@ -65,152 +30,6 @@ FloatingWindow {
visible = false; visible = 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 navigateUp() {
const path = currentPath;
if (path === homeDir)
return;
const lastSlash = path.lastIndexOf('/');
if (lastSlash > 0) {
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) {
keyboardSelectionTimer.targetIndex = index;
keyboardSelectionTimer.start();
}
}
function executeKeyboardSelection(index) {
keyboardSelectionIndex = index;
keyboardSelectionRequested = true;
}
function handleSaveFile(filePath) {
var normalizedPath = filePath;
if (!normalizedPath.startsWith("file://")) {
normalizedPath = "file://" + 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);
fileBrowserModal.close();
}
}
objectName: "fileBrowserModal" objectName: "fileBrowserModal"
title: "Files - " + browserTitle title: "Files - " + browserTitle
minimumSize: Qt.size(500, 400) minimumSize: Qt.size(500, 400)
@@ -219,718 +38,39 @@ FloatingWindow {
color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency) color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
visible: false visible: false
Component.onCompleted: {
loadSettings();
currentPath = getLastPath();
_initialized = true;
}
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 int currentPathIndex: 0
onVisibleChanged: { onVisibleChanged: {
if (visible) { if (visible) {
if (parentModal) { if (parentModal) {
parentModal.shouldHaveFocus = false; parentModal.shouldHaveFocus = false;
parentModal.allowFocusOverride = true; parentModal.allowFocusOverride = true;
} }
currentPath = getLastPath(); content.reset();
selectedIndex = -1; Qt.callLater(() => content.forceActiveFocus());
keyboardNavigationActive = false;
backButtonFocused = false;
Qt.callLater(() => {
if (contentFocusScope) {
contentFocusScope.forceActiveFocus();
}
});
} else { } else {
if (parentModal) { if (parentModal) {
parentModal.allowFocusOverride = false; parentModal.allowFocusOverride = false;
parentModal.shouldHaveFocus = Qt.binding(() => { parentModal.shouldHaveFocus = Qt.binding(() => parentModal.shouldBeVisible);
return parentModal.shouldBeVisible;
});
} }
dialogClosed(); dialogClosed();
} }
} }
onCurrentPathChanged: {
selectedFilePath = "";
selectedFileName = "";
selectedFileIsDir = false;
saveSettings();
}
onSelectedIndexChanged: {
if (selectedIndex >= 0 && folderModel && selectedIndex < folderModel.count) {
selectedFilePath = "";
selectedFileName = "";
selectedFileIsDir = false;
}
}
FolderListModel {
id: folderModel
showDirsFirst: true
showDotAndDotDot: false
showHidden: fileBrowserModal.showHiddenFiles
nameFilters: fileExtensions
showFiles: true
showDirs: true
folder: currentPath ? "file://" + currentPath : "file://" + 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
}
property var quickAccessLocations: [
{
"name": "Home",
"path": homeDir,
"icon": "home"
},
{
"name": "Documents",
"path": docsDir,
"icon": "description"
},
{
"name": "Downloads",
"path": downloadDir,
"icon": "download"
},
{
"name": "Pictures",
"path": picsDir,
"icon": "image"
},
{
"name": "Music",
"path": musicDir,
"icon": "music_note"
},
{
"name": "Videos",
"path": videosDir,
"icon": "movie"
},
{
"name": "Desktop",
"path": desktopDir,
"icon": "computer"
}
]
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) {
close();
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)
fileBrowserModal.keyboardFileSelection(selectedIndex);
event.accepted = true;
break;
}
}
}
Timer {
id: keyboardSelectionTimer
property int targetIndex: -1
interval: 1
onTriggered: {
executeKeyboardSelection(targetIndex);
}
}
FocusScope {
id: contentFocusScope
FileBrowserContent {
id: content
anchors.fill: parent anchors.fill: parent
focus: true focus: true
Keys.onPressed: event => { browserTitle: fileBrowserModal.browserTitle
keyboardController.handleKey(event); browserIcon: fileBrowserModal.browserIcon
} browserType: fileBrowserModal.browserType
fileExtensions: fileBrowserModal.fileExtensions
showHiddenFiles: fileBrowserModal.showHiddenFiles
saveMode: fileBrowserModal.saveMode
defaultFileName: fileBrowserModal.defaultFileName
Column { Component.onCompleted: initialize()
anchors.fill: parent
spacing: 0
Item { onFileSelected: path => fileBrowserModal.fileSelected(path)
width: parent.width onCloseRequested: fileBrowserModal.close()
height: 48
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: fileBrowserModal.showKeyboardHints = !fileBrowserModal.showKeyboardHints
}
DankActionButton {
circular: false
iconName: "close"
iconSize: Theme.iconSize - 4
iconColor: Theme.surfaceText
onClicked: fileBrowserModal.close()
}
}
}
StyledRect {
width: parent.width
height: 1
color: Theme.outline
}
Item {
width: parent.width
height: parent.height - 49
Row {
anchors.fill: parent
spacing: 0
Row {
width: showSidebar ? 201 : 0
height: parent.height
spacing: 0
visible: showSidebar
FileBrowserSidebar {
height: parent.height
quickAccessLocations: fileBrowserModal.quickAccessLocations
currentPath: fileBrowserModal.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: fileBrowserModal.currentPath
homeDir: fileBrowserModal.homeDir
backButtonFocused: fileBrowserModal.backButtonFocused
keyboardNavigationActive: fileBrowserModal.keyboardNavigationActive
showSidebar: fileBrowserModal.showSidebar
pathEditMode: fileBrowserModal.pathEditMode
onNavigateUp: fileBrowserModal.navigateUp()
onNavigateTo: path => fileBrowserModal.navigateTo(path)
onPathInputFocusChanged: hasFocus => {
fileBrowserModal.pathInputHasFocus = hasFocus;
if (hasFocus) {
fileBrowserModal.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: {
fileBrowserModal.actualGridColumns = gridColumns;
}
Component.onCompleted: {
fileBrowserModal.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: ScrollBar {
policy: ScrollBar.AlwaysOff
}
delegate: FileBrowserGridDelegate {
iconSizes: fileBrowserModal.iconSizes
iconSizeIndex: fileBrowserModal.iconSizeIndex
selectedIndex: fileBrowserModal.selectedIndex
keyboardNavigationActive: fileBrowserModal.keyboardNavigationActive
onItemClicked: (index, path, name, isDir) => {
selectedIndex = index;
setSelectedFileData(path, name, isDir);
if (isDir) {
navigateTo(path);
} else {
fileSelected(path);
fileBrowserModal.close();
}
}
onItemSelected: (index, path, name, isDir) => {
setSelectedFileData(path, name, isDir);
}
Connections {
function onKeyboardSelectionRequestedChanged() {
if (fileBrowserModal.keyboardSelectionRequested && fileBrowserModal.keyboardSelectionIndex === index) {
fileBrowserModal.keyboardSelectionRequested = false;
selectedIndex = index;
setSelectedFileData(filePath, fileName, fileIsDir);
if (fileIsDir) {
navigateTo(filePath);
} else {
fileSelected(filePath);
fileBrowserModal.close();
}
}
}
target: fileBrowserModal
}
}
}
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: 2
model: folderModel
currentIndex: selectedIndex
onCurrentIndexChanged: {
if (keyboardNavigationActive && currentIndex >= 0)
positionViewAtIndex(currentIndex, ListView.Contain);
}
ScrollBar.vertical: DankScrollbar {
id: listScrollbar
}
delegate: FileBrowserListDelegate {
width: fileList.width
selectedIndex: fileBrowserModal.selectedIndex
keyboardNavigationActive: fileBrowserModal.keyboardNavigationActive
onItemClicked: (index, path, name, isDir) => {
selectedIndex = index;
setSelectedFileData(path, name, isDir);
if (isDir) {
navigateTo(path);
} else {
fileSelected(path);
fileBrowserModal.close();
}
}
onItemSelected: (index, path, name, isDir) => {
setSelectedFileData(path, name, isDir);
}
Connections {
function onKeyboardSelectionRequestedChanged() {
if (fileBrowserModal.keyboardSelectionRequested && fileBrowserModal.keyboardSelectionIndex === index) {
fileBrowserModal.keyboardSelectionRequested = false;
selectedIndex = index;
setSelectedFileData(filePath, fileName, fileIsDir);
if (fileIsDir) {
navigateTo(filePath);
} else {
fileSelected(filePath);
fileBrowserModal.close();
}
}
}
target: fileBrowserModal
}
}
}
}
}
}
FileBrowserSaveRow {
anchors.bottom: parent.bottom
anchors.left: parent.left
anchors.right: parent.right
anchors.margins: Theme.spacingL
saveMode: fileBrowserModal.saveMode
defaultFileName: fileBrowserModal.defaultFileName
currentPath: fileBrowserModal.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: fileBrowserModal.showKeyboardHints
}
FileInfo {
id: fileInfo
anchors.top: parent.top
anchors.right: parent.right
anchors.margins: Theme.spacingL
width: 300
showFileInfo: fileBrowserModal.showFileInfo
selectedIndex: fileBrowserModal.selectedIndex
sourceFolderModel: folderModel
currentPath: fileBrowserModal.currentPath
currentFileName: fileBrowserModal.selectedFileName
currentFileIsDir: fileBrowserModal.selectedFileIsDir
currentFileExtension: {
if (fileBrowserModal.selectedFileIsDir || !fileBrowserModal.selectedFileName)
return "";
var lastDot = fileBrowserModal.selectedFileName.lastIndexOf('.');
return lastDot > 0 ? fileBrowserModal.selectedFileName.substring(lastDot + 1).toLowerCase() : "";
}
}
FileBrowserSortMenu {
id: sortMenu
anchors.top: parent.top
anchors.right: parent.right
anchors.topMargin: 120
anchors.rightMargin: Theme.spacingL
sortBy: fileBrowserModal.sortBy
sortAscending: fileBrowserModal.sortAscending
onSortBySelected: value => {
fileBrowserModal.sortBy = value;
}
onSortOrderSelected: ascending => {
fileBrowserModal.sortAscending = ascending;
}
}
}
FileBrowserOverwriteDialog {
anchors.fill: parent
showDialog: showOverwriteConfirmation
pendingFilePath: fileBrowserModal.pendingFilePath
onConfirmed: filePath => {
showOverwriteConfirmation = false;
fileSelected(filePath);
pendingFilePath = "";
Qt.callLater(() => fileBrowserModal.close());
}
onCancelled: {
showOverwriteConfirmation = false;
pendingFilePath = "";
}
}
}
} }
} }

View File

@@ -0,0 +1,63 @@
import QtQuick
import Quickshell.Wayland
import qs.Common
import qs.Modals.Common
DankModal {
id: fileBrowserSurfaceModal
property string browserTitle: "Select File"
property string browserIcon: "folder_open"
property string browserType: "generic"
property var fileExtensions: ["*.*"]
property alias filterExtensions: fileBrowserSurfaceModal.fileExtensions
property bool showHiddenFiles: false
property bool saveMode: false
property string defaultFileName: ""
property var parentPopout: null
signal fileSelected(string path)
layerNamespace: "dms:filebrowser"
modalWidth: 800
modalHeight: 600
backgroundColor: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
closeOnEscapeKey: true
closeOnBackgroundClick: true
allowStacking: true
keepPopoutsOpen: true
onBackgroundClicked: close()
onOpened: {
if (parentPopout) {
parentPopout.customKeyboardFocus = WlrKeyboardFocus.None;
}
content.reset();
Qt.callLater(() => content.forceActiveFocus());
}
onDialogClosed: {
if (parentPopout) {
parentPopout.customKeyboardFocus = null;
}
}
directContent: FileBrowserContent {
id: content
focus: true
browserTitle: fileBrowserSurfaceModal.browserTitle
browserIcon: fileBrowserSurfaceModal.browserIcon
browserType: fileBrowserSurfaceModal.browserType
fileExtensions: fileBrowserSurfaceModal.fileExtensions
showHiddenFiles: fileBrowserSurfaceModal.showHiddenFiles
saveMode: fileBrowserSurfaceModal.saveMode
defaultFileName: fileBrowserSurfaceModal.defaultFileName
Component.onCompleted: initialize()
onFileSelected: path => fileBrowserSurfaceModal.fileSelected(path)
onCloseRequested: fileBrowserSurfaceModal.close()
}
}

View File

@@ -3,12 +3,8 @@ import QtCore
import QtQuick import QtQuick
import QtQuick.Controls import QtQuick.Controls
import QtQuick.Effects import QtQuick.Effects
import QtQuick.Layouts
import Quickshell
import Quickshell.Wayland
import qs.Common import qs.Common
import qs.Modals.FileBrowser import qs.Modals.FileBrowser
import qs.Services
import qs.Widgets import qs.Widgets
Item { Item {
@@ -38,222 +34,222 @@ Item {
function getCurrentWallpaper() { function getCurrentWallpaper() {
if (SessionData.perMonitorWallpaper && targetScreenName) { if (SessionData.perMonitorWallpaper && targetScreenName) {
return SessionData.getMonitorWallpaper(targetScreenName) return SessionData.getMonitorWallpaper(targetScreenName);
} }
return SessionData.wallpaperPath return SessionData.wallpaperPath;
} }
function setCurrentWallpaper(path) { function setCurrentWallpaper(path) {
if (SessionData.perMonitorWallpaper && targetScreenName) { if (SessionData.perMonitorWallpaper && targetScreenName) {
SessionData.setMonitorWallpaper(targetScreenName, path) SessionData.setMonitorWallpaper(targetScreenName, path);
} else { } else {
SessionData.setWallpaper(path) SessionData.setWallpaper(path);
} }
} }
onCurrentPageChanged: { onCurrentPageChanged: {
if (currentPage !== lastPage) { if (currentPage !== lastPage) {
enableAnimation = false enableAnimation = false;
lastPage = currentPage lastPage = currentPage;
} }
updateSelectedFileName() updateSelectedFileName();
} }
onGridIndexChanged: { onGridIndexChanged: {
updateSelectedFileName() updateSelectedFileName();
} }
onVisibleChanged: { onVisibleChanged: {
if (visible && active) { if (visible && active) {
setInitialSelection() setInitialSelection();
} }
} }
Component.onCompleted: { Component.onCompleted: {
loadWallpaperDirectory() loadWallpaperDirectory();
} }
onActiveChanged: { onActiveChanged: {
if (active && visible) { if (active && visible) {
setInitialSelection() setInitialSelection();
} }
} }
function handleKeyEvent(event) { function handleKeyEvent(event) {
const columns = 4 const columns = 4;
const currentCol = gridIndex % columns const currentCol = gridIndex % columns;
const visibleCount = Math.min(itemsPerPage, wallpaperFolderModel.count - currentPage * itemsPerPage) const visibleCount = Math.min(itemsPerPage, wallpaperFolderModel.count - currentPage * itemsPerPage);
if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) { if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) {
if (gridIndex >= 0 && gridIndex < visibleCount) { if (gridIndex >= 0 && gridIndex < visibleCount) {
const absoluteIndex = currentPage * itemsPerPage + gridIndex const absoluteIndex = currentPage * itemsPerPage + gridIndex;
if (absoluteIndex < wallpaperFolderModel.count) { if (absoluteIndex < wallpaperFolderModel.count) {
const filePath = wallpaperFolderModel.get(absoluteIndex, "filePath") const filePath = wallpaperFolderModel.get(absoluteIndex, "filePath");
if (filePath) { if (filePath) {
setCurrentWallpaper(filePath.toString().replace(/^file:\/\//, '')) setCurrentWallpaper(filePath.toString().replace(/^file:\/\//, ''));
} }
} }
} }
return true return true;
} }
if (event.key === Qt.Key_Right) { if (event.key === Qt.Key_Right) {
if (gridIndex + 1 < visibleCount) { if (gridIndex + 1 < visibleCount) {
gridIndex++ gridIndex++;
} else if (currentPage < totalPages - 1) { } else if (currentPage < totalPages - 1) {
gridIndex = 0 gridIndex = 0;
currentPage++ currentPage++;
} }
return true return true;
} }
if (event.key === Qt.Key_Left) { if (event.key === Qt.Key_Left) {
if (gridIndex > 0) { if (gridIndex > 0) {
gridIndex-- gridIndex--;
} else if (currentPage > 0) { } else if (currentPage > 0) {
currentPage-- currentPage--;
const prevPageCount = Math.min(itemsPerPage, wallpaperFolderModel.count - currentPage * itemsPerPage) const prevPageCount = Math.min(itemsPerPage, wallpaperFolderModel.count - currentPage * itemsPerPage);
gridIndex = prevPageCount - 1 gridIndex = prevPageCount - 1;
} }
return true return true;
} }
if (event.key === Qt.Key_Down) { if (event.key === Qt.Key_Down) {
if (gridIndex + columns < visibleCount) { if (gridIndex + columns < visibleCount) {
gridIndex += columns gridIndex += columns;
} else if (currentPage < totalPages - 1) { } else if (currentPage < totalPages - 1) {
gridIndex = currentCol gridIndex = currentCol;
currentPage++ currentPage++;
} }
return true return true;
} }
if (event.key === Qt.Key_Up) { if (event.key === Qt.Key_Up) {
if (gridIndex >= columns) { if (gridIndex >= columns) {
gridIndex -= columns gridIndex -= columns;
} else if (currentPage > 0) { } else if (currentPage > 0) {
currentPage-- currentPage--;
const prevPageCount = Math.min(itemsPerPage, wallpaperFolderModel.count - currentPage * itemsPerPage) const prevPageCount = Math.min(itemsPerPage, wallpaperFolderModel.count - currentPage * itemsPerPage);
const prevPageRows = Math.ceil(prevPageCount / columns) const prevPageRows = Math.ceil(prevPageCount / columns);
gridIndex = (prevPageRows - 1) * columns + currentCol gridIndex = (prevPageRows - 1) * columns + currentCol;
gridIndex = Math.min(gridIndex, prevPageCount - 1) gridIndex = Math.min(gridIndex, prevPageCount - 1);
} }
return true return true;
} }
if (event.key === Qt.Key_PageUp && currentPage > 0) { if (event.key === Qt.Key_PageUp && currentPage > 0) {
gridIndex = 0 gridIndex = 0;
currentPage-- currentPage--;
return true return true;
} }
if (event.key === Qt.Key_PageDown && currentPage < totalPages - 1) { if (event.key === Qt.Key_PageDown && currentPage < totalPages - 1) {
gridIndex = 0 gridIndex = 0;
currentPage++ currentPage++;
return true return true;
} }
if (event.key === Qt.Key_Home && event.modifiers & Qt.ControlModifier) { if (event.key === Qt.Key_Home && event.modifiers & Qt.ControlModifier) {
gridIndex = 0 gridIndex = 0;
currentPage = 0 currentPage = 0;
return true return true;
} }
if (event.key === Qt.Key_End && event.modifiers & Qt.ControlModifier) { if (event.key === Qt.Key_End && event.modifiers & Qt.ControlModifier) {
currentPage = totalPages - 1 currentPage = totalPages - 1;
const lastPageCount = Math.min(itemsPerPage, wallpaperFolderModel.count - currentPage * itemsPerPage) const lastPageCount = Math.min(itemsPerPage, wallpaperFolderModel.count - currentPage * itemsPerPage);
gridIndex = Math.max(0, lastPageCount - 1) gridIndex = Math.max(0, lastPageCount - 1);
return true return true;
} }
return false return false;
} }
function setInitialSelection() { function setInitialSelection() {
const currentWallpaper = getCurrentWallpaper() const currentWallpaper = getCurrentWallpaper();
if (!currentWallpaper || wallpaperFolderModel.count === 0) { if (!currentWallpaper || wallpaperFolderModel.count === 0) {
gridIndex = 0 gridIndex = 0;
updateSelectedFileName() updateSelectedFileName();
Qt.callLater(() => { Qt.callLater(() => {
enableAnimation = true enableAnimation = true;
}) });
return return;
} }
for (var i = 0; i < wallpaperFolderModel.count; i++) { for (var i = 0; i < wallpaperFolderModel.count; i++) {
const filePath = wallpaperFolderModel.get(i, "filePath") const filePath = wallpaperFolderModel.get(i, "filePath");
if (filePath && filePath.toString().replace(/^file:\/\//, '') === currentWallpaper) { if (filePath && filePath.toString().replace(/^file:\/\//, '') === currentWallpaper) {
const targetPage = Math.floor(i / itemsPerPage) const targetPage = Math.floor(i / itemsPerPage);
const targetIndex = i % itemsPerPage const targetIndex = i % itemsPerPage;
currentPage = targetPage currentPage = targetPage;
gridIndex = targetIndex gridIndex = targetIndex;
updateSelectedFileName() updateSelectedFileName();
Qt.callLater(() => { Qt.callLater(() => {
enableAnimation = true enableAnimation = true;
}) });
return return;
} }
} }
gridIndex = 0 gridIndex = 0;
updateSelectedFileName() updateSelectedFileName();
Qt.callLater(() => { Qt.callLater(() => {
enableAnimation = true enableAnimation = true;
}) });
} }
function loadWallpaperDirectory() { function loadWallpaperDirectory() {
const currentWallpaper = getCurrentWallpaper() const currentWallpaper = getCurrentWallpaper();
if (!currentWallpaper || currentWallpaper.startsWith("#")) { if (!currentWallpaper || currentWallpaper.startsWith("#")) {
if (CacheData.wallpaperLastPath && CacheData.wallpaperLastPath !== "") { if (CacheData.wallpaperLastPath && CacheData.wallpaperLastPath !== "") {
wallpaperDir = CacheData.wallpaperLastPath wallpaperDir = CacheData.wallpaperLastPath;
} else { } else {
wallpaperDir = "" wallpaperDir = "";
} }
return return;
} }
wallpaperDir = currentWallpaper.substring(0, currentWallpaper.lastIndexOf('/')) wallpaperDir = currentWallpaper.substring(0, currentWallpaper.lastIndexOf('/'));
} }
function updateSelectedFileName() { function updateSelectedFileName() {
if (wallpaperFolderModel.count === 0) { if (wallpaperFolderModel.count === 0) {
selectedFileName = "" selectedFileName = "";
return return;
} }
const absoluteIndex = currentPage * itemsPerPage + gridIndex const absoluteIndex = currentPage * itemsPerPage + gridIndex;
if (absoluteIndex < wallpaperFolderModel.count) { if (absoluteIndex < wallpaperFolderModel.count) {
const filePath = wallpaperFolderModel.get(absoluteIndex, "filePath") const filePath = wallpaperFolderModel.get(absoluteIndex, "filePath");
if (filePath) { if (filePath) {
const pathStr = filePath.toString().replace(/^file:\/\//, '') const pathStr = filePath.toString().replace(/^file:\/\//, '');
selectedFileName = pathStr.substring(pathStr.lastIndexOf('/') + 1) selectedFileName = pathStr.substring(pathStr.lastIndexOf('/') + 1);
return return;
} }
} }
selectedFileName = "" selectedFileName = "";
} }
Connections { Connections {
target: SessionData target: SessionData
function onWallpaperPathChanged() { function onWallpaperPathChanged() {
loadWallpaperDirectory() loadWallpaperDirectory();
if (visible && active) { if (visible && active) {
setInitialSelection() setInitialSelection();
} }
} }
function onMonitorWallpapersChanged() { function onMonitorWallpapersChanged() {
loadWallpaperDirectory() loadWallpaperDirectory();
if (visible && active) { if (visible && active) {
setInitialSelection() setInitialSelection();
} }
} }
} }
onTargetScreenNameChanged: { onTargetScreenNameChanged: {
loadWallpaperDirectory() loadWallpaperDirectory();
if (visible && active) { if (visible && active) {
setInitialSelection() setInitialSelection();
} }
} }
@@ -262,17 +258,17 @@ Item {
function onCountChanged() { function onCountChanged() {
if (wallpaperFolderModel.status === FolderListModel.Ready) { if (wallpaperFolderModel.status === FolderListModel.Ready) {
if (visible && active) { if (visible && active) {
setInitialSelection() setInitialSelection();
} }
updateSelectedFileName() updateSelectedFileName();
} }
} }
function onStatusChanged() { function onStatusChanged() {
if (wallpaperFolderModel.status === FolderListModel.Ready && wallpaperFolderModel.count > 0) { if (wallpaperFolderModel.status === FolderListModel.Ready && wallpaperFolderModel.count > 0) {
if (visible && active) { if (visible && active) {
setInitialSelection() setInitialSelection();
} }
updateSelectedFileName() updateSelectedFileName();
} }
} }
} }
@@ -290,51 +286,27 @@ Item {
folder: wallpaperDir ? "file://" + wallpaperDir : "" folder: wallpaperDir ? "file://" + wallpaperDir : ""
} }
Loader { FileBrowserSurfaceModal {
id: wallpaperBrowserLoader id: wallpaperBrowser
active: false
asynchronous: true
onActiveChanged: { browserTitle: I18n.tr("Select Wallpaper Directory", "wallpaper directory file browser title")
if (active && parentPopout) { browserIcon: "folder_open"
parentPopout.WlrLayershell.keyboardFocus = WlrKeyboardFocus.None browserType: "wallpaper"
} showHiddenFiles: false
} fileExtensions: ["*.jpg", "*.jpeg", "*.png", "*.bmp", "*.gif", "*.webp"]
parentPopout: root.parentPopout
sourceComponent: FileBrowserModal {
Component.onCompleted: { onFileSelected: path => {
open() const cleanPath = path.replace(/^file:\/\//, '');
} setCurrentWallpaper(cleanPath);
browserTitle: I18n.tr("Select Wallpaper Directory", "wallpaper directory file browser title")
browserIcon: "folder_open" const dirPath = cleanPath.substring(0, cleanPath.lastIndexOf('/'));
browserType: "wallpaper" if (dirPath) {
showHiddenFiles: false wallpaperDir = dirPath;
fileExtensions: ["*.jpg", "*.jpeg", "*.png", "*.bmp", "*.gif", "*.webp"] CacheData.wallpaperLastPath = dirPath;
allowStacking: true CacheData.saveCache();
onFileSelected: path => {
const cleanPath = path.replace(/^file:\/\//, '')
setCurrentWallpaper(cleanPath)
const dirPath = cleanPath.substring(0, cleanPath.lastIndexOf('/'))
if (dirPath) {
wallpaperDir = dirPath
CacheData.wallpaperLastPath = dirPath
CacheData.saveCache()
}
close()
}
onDialogClosed: {
if (parentPopout) {
if (CompositorService.isHyprland) {
parentPopout.WlrLayershell.keyboardFocus = WlrKeyboardFocus.OnDemand
} else {
parentPopout.WlrLayershell.keyboardFocus = WlrKeyboardFocus.Exclusive
}
}
Qt.callLater(() => wallpaperBrowserLoader.active = false)
} }
close();
} }
} }
@@ -376,41 +348,41 @@ Item {
} }
model: { model: {
const startIndex = currentPage * itemsPerPage const startIndex = currentPage * itemsPerPage;
const endIndex = Math.min(startIndex + itemsPerPage, wallpaperFolderModel.count) const endIndex = Math.min(startIndex + itemsPerPage, wallpaperFolderModel.count);
const items = [] const items = [];
for (var i = startIndex; i < endIndex; i++) { for (var i = startIndex; i < endIndex; i++) {
const filePath = wallpaperFolderModel.get(i, "filePath") const filePath = wallpaperFolderModel.get(i, "filePath");
if (filePath) { if (filePath) {
items.push(filePath.toString().replace(/^file:\/\//, '')) items.push(filePath.toString().replace(/^file:\/\//, ''));
} }
} }
return items return items;
} }
onModelChanged: { onModelChanged: {
const clampedIndex = model.length > 0 ? Math.min(Math.max(0, gridIndex), model.length - 1) : 0 const clampedIndex = model.length > 0 ? Math.min(Math.max(0, gridIndex), model.length - 1) : 0;
if (gridIndex !== clampedIndex) { if (gridIndex !== clampedIndex) {
gridIndex = clampedIndex gridIndex = clampedIndex;
} }
} }
onCountChanged: { onCountChanged: {
if (count > 0) { if (count > 0) {
const clampedIndex = Math.min(gridIndex, count - 1) const clampedIndex = Math.min(gridIndex, count - 1);
currentIndex = clampedIndex currentIndex = clampedIndex;
positionViewAtIndex(clampedIndex, GridView.Contain) positionViewAtIndex(clampedIndex, GridView.Contain);
} }
enableAnimation = true enableAnimation = true;
} }
Connections { Connections {
target: root target: root
function onGridIndexChanged() { function onGridIndexChanged() {
if (wallpaperGrid.count > 0) { if (wallpaperGrid.count > 0) {
wallpaperGrid.currentIndex = gridIndex wallpaperGrid.currentIndex = gridIndex;
if (!enableAnimation) { if (!enableAnimation) {
wallpaperGrid.positionViewAtIndex(gridIndex, GridView.Contain) wallpaperGrid.positionViewAtIndex(gridIndex, GridView.Contain);
} }
} }
} }
@@ -487,9 +459,9 @@ Item {
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
onClicked: { onClicked: {
gridIndex = index gridIndex = index;
if (modelData) { if (modelData) {
setCurrentWallpaper(modelData) setCurrentWallpaper(modelData);
} }
} }
} }
@@ -535,7 +507,7 @@ Item {
opacity: enabled ? 1.0 : 0.3 opacity: enabled ? 1.0 : 0.3
onClicked: { onClicked: {
if (currentPage > 0) { if (currentPage > 0) {
currentPage-- currentPage--;
} }
} }
} }
@@ -557,7 +529,7 @@ Item {
opacity: enabled ? 1.0 : 0.3 opacity: enabled ? 1.0 : 0.3
onClicked: { onClicked: {
if (currentPage < totalPages - 1) { if (currentPage < totalPages - 1) {
currentPage++ currentPage++;
} }
} }
} }
@@ -570,7 +542,7 @@ Item {
iconSize: 20 iconSize: 20
buttonSize: 32 buttonSize: 32
opacity: 0.7 opacity: 0.7
onClicked: wallpaperBrowserLoader.active = true onClicked: wallpaperBrowser.open()
} }
} }

View File

@@ -685,8 +685,16 @@ Item {
Row { Row {
spacing: Theme.spacingS spacing: Theme.spacingS
property var wlrOutput: WlrOutputService.wlrOutputAvailable ? WlrOutputService.getOutput(modelData.name) : null
property var currentMode: wlrOutput?.currentMode
StyledText { StyledText {
text: modelData.width + "×" + modelData.height text: {
if (parent.currentMode) {
return parent.currentMode.width + "×" + parent.currentMode.height + "@" + Math.round(parent.currentMode.refresh / 1000) + "Hz"
}
return modelData.width + "×" + modelData.height
}
font.pixelSize: Theme.fontSizeSmall font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText color: Theme.surfaceVariantText
} }

View File

@@ -9,7 +9,6 @@ import qs.Widgets
Item { Item {
id: personalizationTab id: personalizationTab
property var wallpaperBrowser: wallpaperBrowserLoader.item
property var parentModal: null property var parentModal: null
property var cachedFontFamilies: [] property var cachedFontFamilies: []
property bool fontsEnumerated: false property bool fontsEnumerated: false
@@ -207,9 +206,7 @@ Item {
MouseArea { MouseArea {
anchors.fill: parent anchors.fill: parent
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
onClicked: { onClicked: mainWallpaperBrowser.open()
wallpaperBrowserLoader.active = true;
}
} }
} }
@@ -598,7 +595,7 @@ Item {
anchors.fill: parent anchors.fill: parent
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
onClicked: { onClicked: {
lightWallpaperBrowserLoader.active = true; lightWallpaperBrowser.open();
} }
} }
} }
@@ -787,7 +784,7 @@ Item {
anchors.fill: parent anchors.fill: parent
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
onClicked: { onClicked: {
darkWallpaperBrowserLoader.active = true; darkWallpaperBrowser.open();
} }
} }
} }
@@ -2140,86 +2137,56 @@ Item {
} }
} }
Loader { FileBrowserModal {
id: wallpaperBrowserLoader id: mainWallpaperBrowser
active: false
asynchronous: true
sourceComponent: FileBrowserModal { parentModal: personalizationTab.parentModal
parentModal: personalizationTab.parentModal browserTitle: I18n.tr("Select Wallpaper", "wallpaper file browser title")
Component.onCompleted: { browserIcon: "wallpaper"
open(); browserType: "wallpaper"
} showHiddenFiles: true
browserTitle: I18n.tr("Select Wallpaper", "wallpaper file browser title") fileExtensions: ["*.jpg", "*.jpeg", "*.png", "*.bmp", "*.gif", "*.webp"]
browserIcon: "wallpaper" onFileSelected: path => {
browserType: "wallpaper" if (SessionData.perMonitorWallpaper) {
showHiddenFiles: true SessionData.setMonitorWallpaper(selectedMonitorName, path);
fileExtensions: ["*.jpg", "*.jpeg", "*.png", "*.bmp", "*.gif", "*.webp"] } else {
onFileSelected: path => { SessionData.setWallpaper(path);
if (SessionData.perMonitorWallpaper) {
SessionData.setMonitorWallpaper(selectedMonitorName, path);
} else {
SessionData.setWallpaper(path);
}
close();
}
onDialogClosed: {
Qt.callLater(() => wallpaperBrowserLoader.active = false);
} }
close();
} }
} }
Loader { FileBrowserModal {
id: lightWallpaperBrowserLoader id: lightWallpaperBrowser
active: false
asynchronous: true
sourceComponent: FileBrowserModal { parentModal: personalizationTab.parentModal
parentModal: personalizationTab.parentModal browserTitle: I18n.tr("Select Wallpaper", "light mode wallpaper file browser title")
Component.onCompleted: { browserIcon: "light_mode"
open(); browserType: "wallpaper"
} showHiddenFiles: true
browserTitle: I18n.tr("Select Wallpaper", "light mode wallpaper file browser title") fileExtensions: ["*.jpg", "*.jpeg", "*.png", "*.bmp", "*.gif", "*.webp"]
browserIcon: "light_mode" onFileSelected: path => {
browserType: "wallpaper" SessionData.wallpaperPathLight = path;
showHiddenFiles: true SessionData.syncWallpaperForCurrentMode();
fileExtensions: ["*.jpg", "*.jpeg", "*.png", "*.bmp", "*.gif", "*.webp"] SessionData.saveSettings();
onFileSelected: path => { close();
SessionData.wallpaperPathLight = path;
SessionData.syncWallpaperForCurrentMode();
SessionData.saveSettings();
close();
}
onDialogClosed: {
Qt.callLater(() => lightWallpaperBrowserLoader.active = false);
}
} }
} }
Loader { FileBrowserModal {
id: darkWallpaperBrowserLoader id: darkWallpaperBrowser
active: false
asynchronous: true
sourceComponent: FileBrowserModal { parentModal: personalizationTab.parentModal
parentModal: personalizationTab.parentModal browserTitle: I18n.tr("Select Wallpaper", "dark mode wallpaper file browser title")
Component.onCompleted: { browserIcon: "dark_mode"
open(); browserType: "wallpaper"
} showHiddenFiles: true
browserTitle: I18n.tr("Select Wallpaper", "dark mode wallpaper file browser title") fileExtensions: ["*.jpg", "*.jpeg", "*.png", "*.bmp", "*.gif", "*.webp"]
browserIcon: "dark_mode" onFileSelected: path => {
browserType: "wallpaper" SessionData.wallpaperPathDark = path;
showHiddenFiles: true SessionData.syncWallpaperForCurrentMode();
fileExtensions: ["*.jpg", "*.jpeg", "*.png", "*.bmp", "*.gif", "*.webp"] SessionData.saveSettings();
onFileSelected: path => { close();
SessionData.wallpaperPathDark = path;
SessionData.syncWallpaperForCurrentMode();
SessionData.saveSettings();
close();
}
onDialogClosed: {
Qt.callLater(() => darkWallpaperBrowserLoader.active = false);
}
} }
} }
} }

View File

@@ -141,6 +141,17 @@ set_system_color_scheme() {
dconf write /org/gnome/desktop/interface/color-scheme "'$scheme'" 2>/dev/null || true dconf write /org/gnome/desktop/interface/color-scheme "'$scheme'" 2>/dev/null || true
} }
sync_color_scheme_on_exit() {
[[ "$SYNC_MODE_WITH_PORTAL" != "true" ]] && return
[[ ! -f "$DESIRED_JSON" ]] && return
local json mode
json=$(cat "$DESIRED_JSON" 2>/dev/null) || return
mode=$(read_json_field "$json" "mode")
[[ -n "$mode" ]] && set_system_color_scheme "$mode"
}
trap sync_color_scheme_on_exit EXIT
refresh_gtk() { refresh_gtk() {
local mode="$1" local mode="$1"
local gtk_css="$CONFIG_DIR/gtk-3.0/gtk.css" local gtk_css="$CONFIG_DIR/gtk-3.0/gtk.css"
@@ -249,7 +260,6 @@ build_once() {
refresh_gtk "$mode" refresh_gtk "$mode"
setup_vscode_extension "code" "$HOME/.vscode/extensions/local.dynamic-base16-dankshell-0.0.1" "$HOME/.vscode" setup_vscode_extension "code" "$HOME/.vscode/extensions/local.dynamic-base16-dankshell-0.0.1" "$HOME/.vscode"
setup_vscode_extension "codium" "$HOME/.vscode-oss/extensions/local.dynamic-base16-dankshell-0.0.1" "$HOME/.vscode-oss" setup_vscode_extension "codium" "$HOME/.vscode-oss/extensions/local.dynamic-base16-dankshell-0.0.1" "$HOME/.vscode-oss"
set_system_color_scheme "$mode"
signal_terminals signal_terminals
return 0 return 0