diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b2b9fc4..ddbaea7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -53,7 +53,21 @@ jobs: test: needs: changes if: needs.changes.outputs.rust == 'true' - runs-on: macos-latest + strategy: + fail-fast: false + matrix: + include: + - os: macos-latest + gifsicle-path: vendor/gifsicle/gifsicle + skip_gifsicle: '' + # todo: prebuilt gifsicle.exe for ci and windows release + - os: windows-latest + gifsicle-path: vendor/gifsicle/gifsicle.exe + skip_gifsicle: '1' + + runs-on: ${{ matrix.os }} + env: + CI_SKIP_GIFSICLE: ${{ matrix.skip_gifsicle }} steps: - name: Checkout @@ -74,21 +88,47 @@ jobs: workspaces: src-tauri - name: Cache Homebrew - uses: actions/cache@v4 + if: runner.os == 'macOS' + uses: actions/cache@v5 with: path: ~/Library/Caches/Homebrew key: brew-${{ runner.os }}-${{ runner.arch }} restore-keys: | brew-${{ runner.os }}- - - name: Install system dependencies + - name: Install dav1d (macOS) + if: runner.os == 'macOS' run: brew install dav1d + - name: Cache vcpkg (Windows) + if: runner.os == 'Windows' + uses: actions/cache@v5 + with: + path: ${{ env.VCPKG_INSTALLATION_ROOT }}/installed/x64-windows + key: vcpkg-dav1d-x64-windows-1 + restore-keys: | + vcpkg-dav1d-x64-windows- + + - name: Install dav1d (Windows) + if: runner.os == 'Windows' + shell: pwsh + run: | + choco install pkgconfiglite -y + vcpkg install dav1d:x64-windows + Add-Content -Path $env:GITHUB_ENV -Value "PKG_CONFIG_PATH=$env:VCPKG_INSTALLATION_ROOT\installed\x64-windows\lib\pkgconfig" + Add-Content -Path $env:GITHUB_ENV -Value "PATH=$env:VCPKG_INSTALLATION_ROOT\installed\x64-windows\bin;$env:PATH" + + - name: Set VC runtime path (Windows) + if: runner.os == 'Windows' + shell: pwsh + run: ./scripts/set-vc-runtime-env.ps1 + - name: Cache gifsicle binary - uses: actions/cache@v4 + if: runner.os == 'macOS' + uses: actions/cache@v5 id: gifsicle-cache with: - path: vendor/gifsicle/gifsicle + path: ${{ matrix.gifsicle-path }} key: gifsicle-${{ runner.os }}-${{ runner.arch }}-1.96 - name: Install dependencies diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000..fce7308 --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,148 @@ +name: publish + +on: + workflow_dispatch: + inputs: + test_tag: + description: draft tag for this windows test build + required: true + default: v1.3.1-windows-test1 + +jobs: + publish-windows: + permissions: + contents: write + runs-on: windows-latest + + steps: + - name: Checkout + uses: actions/checkout@v5 + + - name: Setup Node.js + uses: actions/setup-node@v5 + with: + node-version-file: .nvmrc + cache: npm + + - name: Install Rust stable + uses: dtolnay/rust-toolchain@stable + + - name: Rust cache + uses: Swatinem/rust-cache@v2 + with: + workspaces: src-tauri + + - name: Cache vcpkg + uses: actions/cache@v5 + with: + path: ${{ env.VCPKG_INSTALLATION_ROOT }}/installed/x64-windows + key: vcpkg-dav1d-x64-windows-1 + restore-keys: | + vcpkg-dav1d-x64-windows- + + - name: Install dav1d + shell: pwsh + run: | + choco install pkgconfiglite -y + vcpkg install dav1d:x64-windows + Add-Content -Path $env:GITHUB_ENV -Value "PKG_CONFIG_PATH=$env:VCPKG_INSTALLATION_ROOT\installed\x64-windows\lib\pkgconfig" + Add-Content -Path $env:GITHUB_ENV -Value "PATH=$env:VCPKG_INSTALLATION_ROOT\installed\x64-windows\bin;$env:PATH" + + - name: Set VC runtime path + shell: pwsh + run: ./scripts/set-vc-runtime-env.ps1 + + - name: Setup MSYS2 + uses: msys2/setup-msys2@v2 + with: + msystem: MINGW64 + update: true + install: >- + msys/make + mingw-w64-x86_64-gcc + mingw-w64-x86_64-gcc-libs + mingw-w64-x86_64-winpthreads + mingw-w64-x86_64-make + autoconf + automake + libtool + pkg-config + + - name: Cache gifsicle binary + uses: actions/cache@v5 + with: + path: vendor/gifsicle + key: gifsicle-${{ runner.os }}-${{ runner.arch }}-1.96-mingw-3 + + - name: Install dependencies + run: npm ci --ignore-scripts + env: + HUSKY: 0 + + - name: Install gifsicle + shell: msys2 {0} + run: | + set -euo pipefail + export PATH="/mingw64/bin:$PATH" + ROOT="$GITHUB_WORKSPACE" + GIFSICLE_DIR="$ROOT/vendor/gifsicle" + GIFSICLE="$GIFSICLE_DIR/gifsicle.exe" + MINGW_DLLS=(libwinpthread-1.dll libgcc_s_seh-1.dll) + + verify_gifsicle() { + if [ ! -f "$GIFSICLE" ]; then + return 1 + fi + "$GIFSICLE" --version | grep -F 1.96 + for dll in "${MINGW_DLLS[@]}"; do + if [ ! -f "$GIFSICLE_DIR/$dll" ]; then + echo "gifsicle: missing bundled $dll" + return 1 + fi + done + } + + bundle_mingw_dlls() { + for dll in "${MINGW_DLLS[@]}"; do + cp "/mingw64/bin/$dll" "$GIFSICLE_DIR/" + echo "gifsicle: bundled $dll" + done + } + + if verify_gifsicle; then + echo "gifsicle: cache hit" + exit 0 + fi + + export LDFLAGS="-static -static-libgcc" + MAKE="$(command -v mingw32-make || command -v make)" + VERSION=1.96 + mkdir -p "$ROOT/vendor/source" "$GIFSICLE_DIR" + cd "$ROOT/vendor/source" + if [ ! -f "gifsicle-$VERSION.tar.gz" ]; then + curl -fsSL -o "gifsicle-$VERSION.tar.gz" "https://www.lcdf.org/gifsicle/gifsicle-$VERSION.tar.gz" + fi + rm -rf "gifsicle-$VERSION" + tar xzf "gifsicle-$VERSION.tar.gz" + cd "gifsicle-$VERSION" + ./configure --disable-gifview --disable-gifdiff --disable-dependency-tracking \ + --prefix="$GIFSICLE_DIR" --bindir="$GIFSICLE_DIR" "MAKE=$MAKE" \ + "LDFLAGS=$LDFLAGS" + "$MAKE" install + bundle_mingw_dlls + "$GIFSICLE" --version + verify_gifsicle + + - name: Build and release + uses: tauri-apps/tauri-action@v0 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} + TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }} + with: + tagName: ${{ github.event.inputs.test_tag }} + releaseName: 'DropSlim Windows test (${{ github.event.inputs.test_tag }})' + releaseBody: 'Windows test build only — not a full release. App version remains 1.3.1.' + releaseDraft: true + prerelease: true + args: '--bundles nsis' diff --git a/.github/workflows/trigger-website.yml b/.github/workflows/trigger-website.yml index 0129d63..7720f5c 100644 --- a/.github/workflows/trigger-website.yml +++ b/.github/workflows/trigger-website.yml @@ -6,6 +6,7 @@ on: jobs: dispatch: + if: github.event.release.prerelease == false runs-on: ubuntu-latest steps: - name: Notify DropSlim_Website diff --git a/.gitignore b/.gitignore index 5d6b429..3262d8b 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,10 @@ dist/ dist-ui/ src-tauri/target/ src-tauri/resources/ +src-tauri/dav1d.dll +src-tauri/vcruntime140.dll +src-tauri/vcruntime140_1.dll +src-tauri/msvcp140.dll src-tauri/gen/ vendor/gifsicle/gifsicle @@ -15,6 +19,7 @@ build/icon.iconset/ build/icon.icns src-tauri/icons/icon.icns src-tauri/icons/icon.png +src-tauri/icons/icon.ico .env .release.env diff --git a/README.md b/README.md index 03e9662..513c9e2 100644 --- a/README.md +++ b/README.md @@ -2,9 +2,9 @@ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE.md) [![CI](https://github.com/onza/DropSlim/actions/workflows/ci.yml/badge.svg)](https://github.com/onza/DropSlim/actions/workflows/ci.yml) -Make large image files **small** — on your Mac, with drag & drop. Local, fast, privacy, free, and open source. +Make large image files **small** — on your computer, with drag & drop. Local, fast, private, free, and open source. -Drop your images. DropSlim crunches the numbers. Done. Everything stays on your Mac — no account, no server. +Drop your images. DropSlim crunches the numbers. Done. Everything stays on your machine — no account, no server.
@@ -12,19 +12,19 @@ Drop your images. DropSlim crunches the numbers. Done. Everything stays on your - **Drag & drop** — drop files or folders on the window; whole folders are processed recursively - **Small and fast** — lightweight app, fast compression; built with imagequant, oxipng, zenjpeg, OXVG, gifsicle, WebP, and AVIF encoders -- **Runs offline** — no internet required; compression runs entirely on your Mac +- **Runs offline** — no internet required; compression runs entirely on your computer - **Privacy** — no upload, no tracking; your images never leave your machine - **Batch processing** — hundreds of files in one go; new file alongside the original, or replace in place -- **All common formats** — PNG, JPEG, HEIC, GIF, SVG, WebP, and AVIF -- **Open with DropSlim** — right-click an image in Finder → **Open With** → DropSlim -- **Review results** in a simple list and reveal outputs in Finder +- **Common formats** — PNG, JPEG, GIF, SVG, WebP, and AVIF. **HEIC on macOS only** +- **Open with DropSlim** (macOS) — right-click an image in Finder → **Open With** → DropSlim +- **Review results** in a simple list and reveal outputs in Finder or File Explorer - **Open source** — [MIT](LICENSE.md) - **Languages** — English, German, French, Spanish, Italian, Japanese, Brazilian Portuguese ### Where the optimized file is saved depends on your settings - **`.min` suffix on** (default): writes a new file next to the original, e.g. `photo.png` → `photo.min.png`. The source file stays untouched. -- **`.min` suffix off**: replaces the original file in place with the optimized version. +- **`.min` suffix off**: overwrites the original in place when saving in the same folder and **`minified` subfolder** is off. With the subfolder on, the optimized file goes into `minified/` under the original filename and the source stays untouched. - **`minified` subfolder**: saves into a `minified/` folder (with or without `.min`, depending on the suffix setting). - **Custom save folder**: turn off **Save optimized files in same folder** in Settings — **Choose folder** appears; click **Open** to pick a destination. @@ -42,6 +42,20 @@ Requires macOS 11 (Big Sur) or later and an **Apple Silicon** Mac (M1 or newer).
+## Install (Windows) + +Requires **Windows 10 or later** (64-bit). + +1. Download **`DropSlim_*_x64-setup.exe`** from **[GitHub Releases](https://github.com/onza/DropSlim/releases)**. +2. If your browser blocks the download, choose **Keep** or **Keep anyway**. +3. Run the installer. +4. If **Microsoft Defender SmartScreen** shows _"Windows protected your PC"_: + - Click **More info** + - Click **Run anyway** +5. Follow the setup wizard and start DropSlim from the Start menu or desktop shortcut. + +
+ ## Translations UI translations were **generated with AI** and may contain errors or awkward wording. **Corrections are welcome** — if you spot a mistake, please [open an issue](https://github.com/onza/DropSlim/issues) or submit a pull request with an updated string in `ui/i18n/locales/`. @@ -50,13 +64,13 @@ UI translations were **generated with AI** and may contain errors or awkward wor ## A Frustrating Side Note -DropSlim is open source and free. Nevertheless, in the Apple ecosystem, it apparently costs money just to download an app, drag it into the “Applications” folder, and open it without any further hassle. +DropSlim is open source and free. Nevertheless, it appears that on both major desktop platforms, it costs developers money to allow users to simply download, install and open an app without any further hassle. -That’s because Apple requires me to join its paid Developer Program, which costs 99 EUR per year. On top of that: identity verification with a wait time of 5 days, certificate signing requests, Developer ID certificates, app-specific passwords, authorizations, and a registration process that feels like the administration from “Asterix Conquers Rome” digitized the A38 pass to make breathing fresh air subject to approval. +**On macOS**, Apple requires a paid Developer Program (99 EUR per year). On top of that: identity verification with a wait time of 5 days, certificate signing requests, Developer ID certificates, app-specific passwords, authorizations, and a registration process that feels like the administration from "Asterix Conquers Rome" digitized the A38 pass to make breathing fresh air subject to approval. This project covers that fee, so Mac users get a normal install. -None of this makes the app any better. It merely serves to satisfy Gatekeeper so that app users aren’t greeted with _“Apple couldn’t verify…”_ and a useless “Done” button, only to then have to click an “Open Anyway” button buried deep in the security settings. +**On Windows**, SmartScreen treats unsigned installers as suspicious. A commercial code-signing certificate (roughly 100+ EUR per year, from a certificate authority or cloud signing service) is the paid entry ticket to a smoother path; even then, reputation builds slowly and rarely helps a small open-source project. This project does not pay for Windows signing — use **More info** → **Run anyway** (steps above). The installer is still safe if you download it from this repository or the [project website](https://dropslim.app/). -This project covers that fee, so you don’t have to worry about it. If any developer knows a trick to bypass the Developer Program without compromising the installation process, I’d be very happy to hear from you :) +None of this makes the app any better. It only buys a smoother install experience. If you know a legitimate way around either gate without compromising security, I’d be happy to hear from you :)
diff --git a/package-lock.json b/package-lock.json index 1a40ca0..efc015b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -28,6 +28,7 @@ "lint-staged": "^15.5.2", "prettier": "^3.6.2", "sharp": "^0.34.5", + "to-ico": "^1.1.5", "vite": "^6.4.3", "vitest": "^3.2.4" } @@ -2127,6 +2128,36 @@ "dev": true, "license": "Python-2.0" }, + "node_modules/arrify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz", + "integrity": "sha512-3CYzex9M9FGQjCGMGyi6/31c8GJbgb0qGyrx5HWxPd0aCwh4cB2YjMb2Xf9UuoogrMrlO9cTqnB5rI5GHZTcUA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/asn1": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz", + "integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": "~2.1.0" + } + }, + "node_modules/assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8" + } + }, "node_modules/assertion-error": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", @@ -2137,6 +2168,13 @@ "node": ">=12" } }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true, + "license": "MIT" + }, "node_modules/available-typed-arrays": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", @@ -2153,6 +2191,23 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/aws-sign2": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", + "integrity": "sha512-08kcGqnYf/YmjoRhfxyu+CLxBjUtHLXLXX/vUfx9l2LYzG3c1m61nrpyFUZI6zeS+Li/wWMMidD9KgrqtGq3mA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "*" + } + }, + "node_modules/aws4": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.13.2.tgz", + "integrity": "sha512-lHe62zvbTB5eEABUVi/AwVh0ZKY9rMMDhmm+eeyuuUQbQ3+J+fONVQOZyj+DdrvD4BY33uYniyRJ4UJIaSKAfw==", + "dev": true, + "license": "MIT" + }, "node_modules/base64-js": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", @@ -2174,6 +2229,26 @@ ], "license": "MIT" }, + "node_modules/bcrypt-pbkdf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", + "integrity": "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tweetnacl": "^0.14.3" + } + }, + "node_modules/bignumber.js": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-2.4.0.tgz", + "integrity": "sha512-uw4ra6Cv483Op/ebM0GBKKfxZlSmn6NgFRby5L3yGTlunLj53KQgndDlqy2WVFOwgvurocApYkSud0aO+mvrpQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, "node_modules/bin-build": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/bin-build/-/bin-build-3.0.0.tgz", @@ -2191,6 +2266,13 @@ "node": ">=4" } }, + "node_modules/bmp-js": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/bmp-js/-/bmp-js-0.0.1.tgz", + "integrity": "sha512-OS74Rlt0Aynu2mTPmY9RZOUOXlqWecFIILFXr70vv16/xCZnFxvri9IKkF1IGxQ8r9dOE62qGNpKxXx8Lko8bg==", + "dev": true, + "license": "MIT" + }, "node_modules/braces": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", @@ -2257,6 +2339,16 @@ "node": "*" } }, + "node_modules/buffer-equal": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal/-/buffer-equal-0.0.1.tgz", + "integrity": "sha512-RgSV6InVQ9ODPdLWJ5UAqBqJBOg370Nz6ZQtRzpt6nUjc8v0St97uJ4PYC6NztqIScrAXafKM3mZPMygSe1ggA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/buffer-fill": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/buffer-fill/-/buffer-fill-1.0.0.tgz", @@ -2334,6 +2426,13 @@ "node": ">=6" } }, + "node_modules/caseless": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", + "integrity": "sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==", + "dev": true, + "license": "Apache-2.0" + }, "node_modules/caw": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/caw/-/caw-2.0.1.tgz", @@ -2350,6 +2449,16 @@ "node": ">=4" } }, + "node_modules/centra": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/centra/-/centra-2.7.0.tgz", + "integrity": "sha512-PbFMgMSrmgx6uxCdm57RUos9Tc3fclMvhLSATYN39XsDV29B89zZ3KA89jmY0vwSGazyU+uerqwa6t+KaodPcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6" + } + }, "node_modules/chai": { "version": "5.3.3", "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", @@ -2454,6 +2563,19 @@ "dev": true, "license": "MIT" }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/commander": { "version": "13.1.0", "resolved": "https://registry.npmjs.org/commander/-/commander-13.1.0.tgz", @@ -2517,6 +2639,19 @@ "node": ">= 8" } }, + "node_modules/dashdash": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", + "integrity": "sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g==", + "dev": true, + "license": "MIT", + "dependencies": { + "assert-plus": "^1.0.0" + }, + "engines": { + "node": ">=0.10" + } + }, "node_modules/debug": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", @@ -2800,6 +2935,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/detect-libc": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", @@ -2810,6 +2955,12 @@ "node": ">=8" } }, + "node_modules/dom-walk": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/dom-walk/-/dom-walk-0.1.2.tgz", + "integrity": "sha512-6QvTW9mrGeIegrFXdtQi9pk7O/nSK6lSdXW2eqUspN5LWD7UTji2Fqw5V2YLjBpHEoU9Xl/eUWNpDeZvoyOv2w==", + "dev": true + }, "node_modules/download": { "version": "6.2.5", "resolved": "https://registry.npmjs.org/download/-/download-6.2.5.tgz", @@ -2970,6 +3121,17 @@ "dev": true, "license": "BSD-3-Clause" }, + "node_modules/ecc-jsbn": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", + "integrity": "sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw==", + "dev": true, + "license": "MIT", + "dependencies": { + "jsbn": "~0.1.0", + "safer-buffer": "^2.1.0" + } + }, "node_modules/emoji-regex": { "version": "10.6.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", @@ -3040,6 +3202,13 @@ "node": ">= 0.4" } }, + "node_modules/es6-promise": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-3.3.1.tgz", + "integrity": "sha512-SOp9Phqvqn7jtEUxPWdWfWoLmyt2VaJ6MpvP9Comy1MceMXqE6bxvaTu4iaxpYYPzhny28Lc+M87/c2cPK6lDg==", + "dev": true, + "license": "MIT" + }, "node_modules/esbuild": { "version": "0.25.12", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", @@ -3457,6 +3626,12 @@ "dev": true, "license": "ISC" }, + "node_modules/exif-parser": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/exif-parser/-/exif-parser-0.1.12.tgz", + "integrity": "sha512-c2bQfLNbMzLPmzQuOr8fy0csy84WmwnER81W88DzTp9CYNPJ6yzOj2EZAh9pywYpqHnshVLHQJ8WzldAyfY+Iw==", + "dev": true + }, "node_modules/expect-type": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", @@ -3494,6 +3669,23 @@ "node": ">=4" } }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "dev": true, + "license": "MIT" + }, + "node_modules/extsprintf": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", + "integrity": "sha512-11Ndz7Nv+mvAC1j0ktTa7fAb0vLyGGX+rMHNBYQviQDGU0Hw7lhctJANqbPhu9nV9/izT/IntTgZ7Im/9LJs9g==", + "dev": true, + "engines": [ + "node >=0.6.0" + ], + "license": "MIT" + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -3642,6 +3834,27 @@ "dev": true, "license": "ISC" }, + "node_modules/follow-redirects": { + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz", + "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, "node_modules/for-each": { "version": "0.3.5", "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", @@ -3658,6 +3871,31 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/forever-agent": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", + "integrity": "sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "*" + } + }, + "node_modules/form-data": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", + "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.6", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 0.12" + } + }, "node_modules/fs-constants": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", @@ -3768,6 +4006,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/getpass": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", + "integrity": "sha512-0fzj9JxOLfJ+XGLhR8ze3unN0KZCgZwiSSDz168VERjK8Wl8kVSdcu2kspd4s4wtAa1y/qrVRiAA0WclVsu0ng==", + "dev": true, + "license": "MIT", + "dependencies": { + "assert-plus": "^1.0.0" + } + }, "node_modules/glob-parent": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", @@ -3781,6 +4029,17 @@ "node": ">=10.13.0" } }, + "node_modules/global": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/global/-/global-4.4.0.tgz", + "integrity": "sha512-wv/LAoHdRE3BeTGz53FAamhGlPLhlssK45usmGFThIi4XqnBmjKQ16u+RNbP7WvigRZDxUsM0J3gcQ5yicaL0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "min-document": "^2.19.0", + "process": "^0.11.10" + } + }, "node_modules/globals": { "version": "16.5.0", "resolved": "https://registry.npmjs.org/globals/-/globals-16.5.0.tgz", @@ -3814,6 +4073,31 @@ "dev": true, "license": "ISC" }, + "node_modules/har-schema": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", + "integrity": "sha512-Oqluz6zhGX8cyRaTQlFMPw80bSJVG2x/cFb8ZPhUILGgHka9SsokCCOQgpveePerqidZOrT14ipqfJb7ILcW5Q==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=4" + } + }, + "node_modules/har-validator": { + "version": "5.1.5", + "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.5.tgz", + "integrity": "sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w==", + "deprecated": "this library is no longer supported", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.3", + "har-schema": "^2.0.0" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", @@ -3902,6 +4186,22 @@ "node": ">= 0.4" } }, + "node_modules/http-signature": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", + "integrity": "sha512-CAbnr6Rz4CYQkLYUtSNXxQPUH2gK8f3iWexVlsnMeD+GjlsQ0Xsy1cOX+mN3dtxYomRy21CiOzU8Uhw6OwncEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "assert-plus": "^1.0.0", + "jsprim": "^1.2.2", + "sshpk": "^1.7.0" + }, + "engines": { + "node": ">=0.8", + "npm": ">=1.3.7" + } + }, "node_modules/human-signals": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-5.0.0.tgz", @@ -3959,6 +4259,19 @@ "node": ">= 4" } }, + "node_modules/image-size": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/image-size/-/image-size-0.5.5.tgz", + "integrity": "sha512-6TDAlDPZxUFCv+fuOkIoXT/V/f3Qbq8e37p+YOiYrUv3v9cc3/6x78VdfPgFVaB9dZYeLUfKgHRebpkm/oP2VQ==", + "dev": true, + "license": "MIT", + "bin": { + "image-size": "bin/image-size.js" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/import-fresh": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", @@ -4000,6 +4313,16 @@ "dev": true, "license": "ISC" }, + "node_modules/ip-regex": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/ip-regex/-/ip-regex-1.0.3.tgz", + "integrity": "sha512-HjpCHTuxbR/6jWJroc/VN+npo5j0T4Vv2TAI5qdEHQx7hsL767MeccGFSsLtF694EiZKTSEqgoeU6DtGFCcuqQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/is-callable": { "version": "1.2.7", "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", @@ -4036,6 +4359,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/is-function": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-function/-/is-function-1.0.2.tgz", + "integrity": "sha512-lw7DUp0aWXYg+CBCN+JKkcE0Q2RayZnSvnZBlwgxHBQhqt5pZNVy4Ri7H9GmmXkdu7LUthszM+Tor1u/2iBcpQ==", + "dev": true, + "license": "MIT" + }, "node_modules/is-glob": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", @@ -4122,6 +4452,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==", + "dev": true, + "license": "MIT" + }, "node_modules/isarray": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", @@ -4136,6 +4473,13 @@ "dev": true, "license": "ISC" }, + "node_modules/isstream": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", + "integrity": "sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==", + "dev": true, + "license": "MIT" + }, "node_modules/isurl": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/isurl/-/isurl-1.0.0.tgz", @@ -4150,6 +4494,62 @@ "node": ">= 4" } }, + "node_modules/jimp": { + "version": "0.2.28", + "resolved": "https://registry.npmjs.org/jimp/-/jimp-0.2.28.tgz", + "integrity": "sha512-9HT7DA279xkTlry2oG30s6AtOUglNiY2UdyYpj0yNI4/NBv8PmdNC0gcldgMU4HqvbUlrM3+v+6GaHnTkH23JQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "bignumber.js": "^2.1.0", + "bmp-js": "0.0.3", + "es6-promise": "^3.0.2", + "exif-parser": "^0.1.9", + "file-type": "^3.1.0", + "jpeg-js": "^0.2.0", + "load-bmfont": "^1.2.3", + "mime": "^1.3.4", + "mkdirp": "0.5.1", + "pixelmatch": "^4.0.0", + "pngjs": "^3.0.0", + "read-chunk": "^1.0.1", + "request": "^2.65.0", + "stream-to-buffer": "^0.1.0", + "tinycolor2": "^1.1.2", + "url-regex": "^3.0.0" + } + }, + "node_modules/jimp/node_modules/bmp-js": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/bmp-js/-/bmp-js-0.0.3.tgz", + "integrity": "sha512-epsm3Z92j5xwek9p97pVw3KbsNc0F4QnbYh+N93SpbJYuHFQQ/UAh6K+bKFGyLePH3Hudtl/Sa95Quqp0gX8IQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/jimp/node_modules/file-type": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-3.9.0.tgz", + "integrity": "sha512-RLoqTXE8/vPmMuTI88DAzhMYC99I8BWv7zYP4A1puo5HIjEJ5EX48ighy4ZyKMG9EDXxBgW6e++cn7d1xuFghA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/jimp/node_modules/jpeg-js": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/jpeg-js/-/jpeg-js-0.2.0.tgz", + "integrity": "sha512-Ni9PffhJtYtdD7VwxH6V2MnievekGfUefosGCHadog0/jAevRu6HPjYeMHbUemn0IPE8d4wGa8UsOGsX+iKy2g==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/jpeg-js": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/jpeg-js/-/jpeg-js-0.1.2.tgz", + "integrity": "sha512-CiRVjMKBUp6VYtGwzRjrdnro41yMwKGOWdP9ATXqmixdz2n7KHNwdqthTYSSbOKj/Ha79Gct1sA8ZQpse55TYA==", + "dev": true, + "license": "BSD-3-Clause" + }, "node_modules/js-tokens": { "version": "9.0.1", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", @@ -4180,6 +4580,13 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", + "integrity": "sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==", + "dev": true, + "license": "MIT" + }, "node_modules/json-buffer": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", @@ -4187,6 +4594,13 @@ "dev": true, "license": "MIT" }, + "node_modules/json-schema": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", + "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==", + "dev": true, + "license": "(AFL-2.1 OR BSD-3-Clause)" + }, "node_modules/json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", @@ -4201,6 +4615,29 @@ "dev": true, "license": "MIT" }, + "node_modules/json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", + "dev": true, + "license": "ISC" + }, + "node_modules/jsprim": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.2.tgz", + "integrity": "sha512-P2bSOMAc/ciLz6DzgjVlGJP9+BrJWu5UDGK70C2iweC5QBIeFf0ZXRvGjEj2uYgrY2MkAAhsSWHDWlFtEroZWw==", + "dev": true, + "license": "MIT", + "dependencies": { + "assert-plus": "1.0.0", + "extsprintf": "1.3.0", + "json-schema": "0.4.0", + "verror": "1.10.0" + }, + "engines": { + "node": ">=0.6.0" + } + }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", @@ -4637,6 +5074,23 @@ "node": ">=18.0.0" } }, + "node_modules/load-bmfont": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/load-bmfont/-/load-bmfont-1.4.2.tgz", + "integrity": "sha512-qElWkmjW9Oq1F9EI5Gt7aD9zcdHb9spJCW1L/dmPf7KzCCEJxq8nhHz5eCgI9aMf7vrG/wyaCqdsI+Iy9ZTlog==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-equal": "0.0.1", + "mime": "^1.3.4", + "parse-bmfont-ascii": "^1.0.3", + "parse-bmfont-binary": "^1.0.5", + "parse-bmfont-xml": "^1.1.4", + "phin": "^3.7.1", + "xhr": "^2.0.1", + "xtend": "^4.0.0" + } + }, "node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -4787,6 +5241,19 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "dev": true, + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/mime-db": { "version": "1.54.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", @@ -4797,6 +5264,29 @@ "node": ">= 0.6" } }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/mimic-fn": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz", @@ -4833,6 +5323,37 @@ "node": ">=4" } }, + "node_modules/min-document": { + "version": "2.19.2", + "resolved": "https://registry.npmjs.org/min-document/-/min-document-2.19.2.tgz", + "integrity": "sha512-8S5I8db/uZN8r9HSLFVWPdJCvYOejMcEC82VIzNUc6Zkklf/d1gg2psfE79/vyhWOj4+J8MtwmoOz3TmvaGu5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "dom-walk": "^0.1.0" + } + }, + "node_modules/minimist": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", + "integrity": "sha512-miQKw5Hv4NS1Psg2517mV4e4dYNaO3++hjAvLOAzKqZ61rH8NS1SK+vbfBWZ5PY/Me/bEWhUwqMghEW5Fb9T7Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/mkdirp": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "integrity": "sha512-SknJC52obPfGQPnjIkXbmA6+5H15E+fR+E4iR2oQ3zzCLbd7/ONua69R/Gw7AgkTLsRG+r5fzksYwWe1AgTyWA==", + "deprecated": "Legacy versions of mkdirp are no longer supported. Please update to mkdirp 1.x. (Note that the API surface has changed to use Promises in 1.x.)", + "dev": true, + "license": "MIT", + "dependencies": { + "minimist": "0.0.8" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -4919,6 +5440,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/oauth-sign": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", + "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "*" + } + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -5077,6 +5608,51 @@ "node": ">=6" } }, + "node_modules/parse-bmfont-ascii": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/parse-bmfont-ascii/-/parse-bmfont-ascii-1.0.6.tgz", + "integrity": "sha512-U4RrVsUFCleIOBsIGYOMKjn9PavsGOXxbvYGtMOEfnId0SVNsgehXh1DxUdVPLoxd5mvcEtvmKs2Mmf0Mpa1ZA==", + "dev": true, + "license": "MIT" + }, + "node_modules/parse-bmfont-binary": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/parse-bmfont-binary/-/parse-bmfont-binary-1.0.6.tgz", + "integrity": "sha512-GxmsRea0wdGdYthjuUeWTMWPqm2+FAd4GI8vCvhgJsFnoGhTrLhXDDupwTo7rXVAgaLIGoVHDZS9p/5XbSqeWA==", + "dev": true, + "license": "MIT" + }, + "node_modules/parse-bmfont-xml": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/parse-bmfont-xml/-/parse-bmfont-xml-1.1.6.tgz", + "integrity": "sha512-0cEliVMZEhrFDwMh4SxIyVJpqYoOWDJ9P895tFuS+XuNzI5UBmBk5U5O4KuJdTnZpSBI4LFA2+ZiJaiwfSwlMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "xml-parse-from-string": "^1.0.0", + "xml2js": "^0.5.0" + } + }, + "node_modules/parse-headers": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/parse-headers/-/parse-headers-2.0.6.tgz", + "integrity": "sha512-Tz11t3uKztEW5FEVZnj1ox8GKblWn+PvHY9TmJV5Mll2uHEwRdR/5Li1OlXoECjLYkApdhWy44ocONwXLiKO5A==", + "dev": true, + "license": "MIT" + }, + "node_modules/parse-png": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/parse-png/-/parse-png-1.1.2.tgz", + "integrity": "sha512-Ge6gDV9T5zhkWHmjvnNiyhPTCIoY7W+FC7qWPtuL2lIGZAFxxqTRG/ouEXsH9qkw+HzYiPEU/tFcxOCEDTP1Yw==", + "dev": true, + "license": "MIT", + "dependencies": { + "pngjs": "^3.2.0" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -5121,6 +5697,27 @@ "dev": true, "license": "MIT" }, + "node_modules/performance-now": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", + "integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==", + "dev": true, + "license": "MIT" + }, + "node_modules/phin": { + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/phin/-/phin-3.7.1.tgz", + "integrity": "sha512-GEazpTWwTZaEQ9RhL7Nyz0WwqilbqgLahDM3D0hxWwmVDI52nXEybHqiN6/elwpkJBhcuj+WbBu+QfT0uhPGfQ==", + "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", + "dev": true, + "license": "MIT", + "dependencies": { + "centra": "^2.7.0" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -5177,6 +5774,29 @@ "node": ">=0.10.0" } }, + "node_modules/pixelmatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/pixelmatch/-/pixelmatch-4.0.2.tgz", + "integrity": "sha512-J8B6xqiO37sU/gkcMglv6h5Jbd9xNER7aHzpfRdNmV4IbQBzBpe4l9XmbG+xPF/znacgu2jfEw+wHffaq/YkXA==", + "dev": true, + "license": "ISC", + "dependencies": { + "pngjs": "^3.0.0" + }, + "bin": { + "pixelmatch": "bin/pixelmatch" + } + }, + "node_modules/pngjs": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-3.4.0.tgz", + "integrity": "sha512-NCrCHhWmnQklfH4MtJMRjZ2a8c80qXeMlQMv2uVp9ISJMTt562SbGd6n2oq0PaPgKm7Z6pL9E2UlLIhC+SHL3w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, "node_modules/possible-typed-array-names": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", @@ -5242,6 +5862,16 @@ "url": "https://github.com/prettier/prettier?sponsor=1" } }, + "node_modules/process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6.0" + } + }, "node_modules/process-nextick-args": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", @@ -5263,6 +5893,19 @@ "dev": true, "license": "ISC" }, + "node_modules/psl": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.15.0.tgz", + "integrity": "sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "funding": { + "url": "https://github.com/sponsors/lupomontero" + } + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -5273,6 +5916,101 @@ "node": ">=6" } }, + "node_modules/qs": { + "version": "6.5.5", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.5.tgz", + "integrity": "sha512-mzR4sElr1bfCaPJe7m8ilJ6ZXdDaGoObcYR0ZHSsktM/Lt21MVHj5De30GQH2eiZ1qGRTO7LCAzQsUeXTNexWQ==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/read-chunk": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/read-chunk/-/read-chunk-1.0.1.tgz", + "integrity": "sha512-5NLTTdX45dKFtG8CX5pKmvS9V5u9wBE+gkklN7xhDuhq3pA2I4O7ALfKxosCMcLHOhkxj6GNacZhfXtp5nlCdg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/request": { + "version": "2.88.2", + "resolved": "https://registry.npmjs.org/request/-/request-2.88.2.tgz", + "integrity": "sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==", + "deprecated": "request has been deprecated, see https://github.com/request/request/issues/3142", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "aws-sign2": "~0.7.0", + "aws4": "^1.8.0", + "caseless": "~0.12.0", + "combined-stream": "~1.0.6", + "extend": "~3.0.2", + "forever-agent": "~0.6.1", + "form-data": "~2.3.2", + "har-validator": "~5.1.3", + "http-signature": "~1.2.0", + "is-typedarray": "~1.0.0", + "isstream": "~0.1.2", + "json-stringify-safe": "~5.0.1", + "mime-types": "~2.1.19", + "oauth-sign": "~0.9.0", + "performance-now": "^2.1.0", + "qs": "~6.5.2", + "safe-buffer": "^5.1.2", + "tough-cookie": "~2.5.0", + "tunnel-agent": "^0.6.0", + "uuid": "^3.3.2" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/resize-img": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/resize-img/-/resize-img-1.1.2.tgz", + "integrity": "sha512-/4nKUmuNPuM6gYTWad136ica81baOVjpesgv8FGaIvP0KWcbCWahOWBKaM4tFoM+aVcSA+qQDg28pcnIzFRpJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "bmp-js": "0.0.1", + "file-type": "^3.8.0", + "get-stream": "^2.0.0", + "jimp": "^0.2.21", + "jpeg-js": "^0.1.1", + "parse-png": "^1.1.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/resize-img/node_modules/file-type": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-3.9.0.tgz", + "integrity": "sha512-RLoqTXE8/vPmMuTI88DAzhMYC99I8BWv7zYP4A1puo5HIjEJ5EX48ighy4ZyKMG9EDXxBgW6e++cn7d1xuFghA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resize-img/node_modules/get-stream": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-2.3.1.tgz", + "integrity": "sha512-AUGhbbemXxrZJRD5cDvKtQxLuYaIbNtDTK8YqupCI393Q2KSTreEsLUN3ZxAWFGiKTzL6nKuzfcIvieflUX9qA==", + "dev": true, + "license": "MIT", + "dependencies": { + "object-assign": "^4.0.1", + "pinkie-promise": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/resolve-from": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", @@ -5386,6 +6124,23 @@ ], "license": "MIT" }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true, + "license": "MIT" + }, + "node_modules/sax": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.6.0.tgz", + "integrity": "sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=11.0.0" + } + }, "node_modules/seek-bzip": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/seek-bzip/-/seek-bzip-1.0.6.tgz", @@ -5586,6 +6341,32 @@ "node": ">=0.10.0" } }, + "node_modules/sshpk": { + "version": "1.18.0", + "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.18.0.tgz", + "integrity": "sha512-2p2KJZTSqQ/I3+HX42EpYOa2l3f8Erv8MWKsy2I9uf4wA7yFIkXRffYdsx86y6z4vHtV8u7g+pPlr8/4ouAxsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "asn1": "~0.2.3", + "assert-plus": "^1.0.0", + "bcrypt-pbkdf": "^1.0.0", + "dashdash": "^1.12.0", + "ecc-jsbn": "~0.1.1", + "getpass": "^0.1.1", + "jsbn": "~0.1.0", + "safer-buffer": "^2.0.2", + "tweetnacl": "~0.14.0" + }, + "bin": { + "sshpk-conv": "bin/sshpk-conv", + "sshpk-sign": "bin/sshpk-sign", + "sshpk-verify": "bin/sshpk-verify" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/stackback": { "version": "0.0.2", "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", @@ -5600,6 +6381,29 @@ "dev": true, "license": "MIT" }, + "node_modules/stream-to": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/stream-to/-/stream-to-0.2.2.tgz", + "integrity": "sha512-Kg1BSDTwgGiVMtTCJNlo7kk/xzL33ZuZveEBRt6rXw+f1WLK/8kmz2NVCT/Qnv0JkV85JOHcLhD82mnXsR3kPw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/stream-to-buffer": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/stream-to-buffer/-/stream-to-buffer-0.1.0.tgz", + "integrity": "sha512-Da4WoKaZyu3nf+bIdIifh7IPkFjARBnBK+pYqn0EUJqksjV9afojjaCCHUemH30Jmu7T2qcKvlZm2ykN38uzaw==", + "dev": true, + "license": "MIT", + "dependencies": { + "stream-to": "~0.2.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/string-argv": { "version": "0.3.2", "resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.2.tgz", @@ -5787,6 +6591,13 @@ "dev": true, "license": "MIT" }, + "node_modules/tinycolor2": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/tinycolor2/-/tinycolor2-1.6.0.tgz", + "integrity": "sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw==", + "dev": true, + "license": "MIT" + }, "node_modules/tinyexec": { "version": "0.3.2", "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", @@ -5856,6 +6667,23 @@ "node": ">= 0.4" } }, + "node_modules/to-ico": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/to-ico/-/to-ico-1.1.5.tgz", + "integrity": "sha512-5kIh7m7bkIlqIESEZkL8gAMMzucXKfPe3hX2FoDY5HEAfD9OJU+Qh9b6Enp74w0qRcxVT5ejss66PHKqc3AVkg==", + "dev": true, + "license": "MIT", + "dependencies": { + "arrify": "^1.0.1", + "buffer-alloc": "^1.1.0", + "image-size": "^0.5.0", + "parse-png": "^1.0.0", + "resize-img": "^1.1.0" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -5869,6 +6697,20 @@ "node": ">=8.0" } }, + "node_modules/tough-cookie": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", + "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "psl": "^1.1.28", + "punycode": "^2.1.1" + }, + "engines": { + "node": ">=0.8" + } + }, "node_modules/trim-repeated": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/trim-repeated/-/trim-repeated-1.0.0.tgz", @@ -5913,6 +6755,13 @@ "node": "*" } }, + "node_modules/tweetnacl": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==", + "dev": true, + "license": "Unlicense" + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -5971,6 +6820,19 @@ "punycode": "^2.1.0" } }, + "node_modules/url-regex": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/url-regex/-/url-regex-3.2.0.tgz", + "integrity": "sha512-dQ9cJzMou5OKr6ZzfvwJkCq3rC72PNXhqz0v3EIhF4a3Np+ujr100AhUx2cKx5ei3iymoJpJrPB3sVSEMdqAeg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ip-regex": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/url-to-options": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/url-to-options/-/url-to-options-1.0.1.tgz", @@ -5999,6 +6861,21 @@ "uuid": "bin/uuid" } }, + "node_modules/verror": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", + "integrity": "sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw==", + "dev": true, + "engines": [ + "node >=0.6.0" + ], + "license": "MIT", + "dependencies": { + "assert-plus": "^1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "^1.2.0" + } + }, "node_modules/vite": { "version": "6.4.3", "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.3.tgz", @@ -6273,6 +7150,50 @@ "dev": true, "license": "ISC" }, + "node_modules/xhr": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/xhr/-/xhr-2.6.0.tgz", + "integrity": "sha512-/eCGLb5rxjx5e3mF1A7s+pLlR6CGyqWN91fv1JgER5mVWg1MZmlhBvy9kjcsOdRk8RrIujotWyJamfyrp+WIcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "global": "~4.4.0", + "is-function": "^1.0.1", + "parse-headers": "^2.0.0", + "xtend": "^4.0.0" + } + }, + "node_modules/xml-parse-from-string": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/xml-parse-from-string/-/xml-parse-from-string-1.0.1.tgz", + "integrity": "sha512-ErcKwJTF54uRzzNMXq2X5sMIy88zJvfN2DmdoQvy7PAFJ+tPRU6ydWuOKNMyfmOjdyBQTFREi60s0Y0SyI0G0g==", + "dev": true, + "license": "MIT" + }, + "node_modules/xml2js": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.5.0.tgz", + "integrity": "sha512-drPFnkQJik/O+uPKpqSgr22mpuFHqKdbS835iAQrUC73L2F5WkboIRd63ai/2Yg6I1jzifPFKH2NTK+cfglkIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "sax": ">=0.6.0", + "xmlbuilder": "~11.0.0" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/xmlbuilder": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", + "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4.0" + } + }, "node_modules/xtend": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", diff --git a/package.json b/package.json index e4d8d8d..74c6fb8 100644 --- a/package.json +++ b/package.json @@ -56,6 +56,7 @@ "lint-staged": "^15.5.2", "prettier": "^3.6.2", "sharp": "^0.34.5", + "to-ico": "^1.1.5", "vite": "^6.4.3", "vitest": "^3.2.4" }, diff --git a/scripts/build-icons.mjs b/scripts/build-icons.mjs index 4bd5672..705e664 100644 --- a/scripts/build-icons.mjs +++ b/scripts/build-icons.mjs @@ -1,8 +1,16 @@ import { execSync } from 'node:child_process' -import { cpSync, existsSync, mkdirSync, readFileSync, rmSync } from 'node:fs' +import { + cpSync, + existsSync, + mkdirSync, + readFileSync, + rmSync, + writeFileSync, +} from 'node:fs' import path from 'node:path' import { fileURLToPath } from 'node:url' import sharp from 'sharp' +import toIco from 'to-ico' const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..') const source = path.join(root, 'assets/icon/icon-1024.png') @@ -22,6 +30,7 @@ const iconSizes = [ [512, 'icon_512x512.png'], [1024, 'icon_512x512@2x.png'], ] +const icoSizes = [16, 24, 32, 48, 64, 128, 256] async function maskIcon(inputPath, outputPath, canvas = 1024) { const art = Math.round(canvas * 0.82) @@ -58,39 +67,56 @@ async function maskIcon(inputPath, outputPath, canvas = 1024) { .toFile(outputPath) } -if (!existsSync(source)) { - console.error(`build-icons: missing ${source}`) - process.exit(1) +async function writeIco(inputPath, outputPath) { + const buffers = await Promise.all( + icoSizes.map((size) => + sharp(readFileSync(inputPath)) + .resize(size, size, { fit: 'cover' }) + .png() + .toBuffer() + ) + ) + + writeFileSync(outputPath, await toIco(buffers)) } -try { - execSync('command -v iconutil >/dev/null') -} catch { - console.error( - 'build-icons: iconutil not found (macOS required to build .icns)' - ) +if (!existsSync(source)) { + console.error(`build-icons: missing ${source}`) process.exit(1) } mkdirSync(buildDir, { recursive: true }) await maskIcon(source, macosIcon, 1024) -rmSync(iconset, { recursive: true, force: true }) -mkdirSync(iconset, { recursive: true }) - -for (const [size, fileName] of iconSizes) { - const output = path.join(iconset, fileName) - execSync(`sips -z ${size} ${size} "${macosIcon}" --out "${output}"`, { - stdio: 'ignore', - }) -} - -execSync(`iconutil -c icns "${iconset}" -o "${icns}"`, { stdio: 'inherit' }) - const iconsDir = path.join(root, 'src-tauri/icons') mkdirSync(iconsDir, { recursive: true }) -cpSync(icns, path.join(iconsDir, 'icon.icns')) cpSync(macosIcon, path.join(iconsDir, 'icon.png')) -rmSync(iconset, { recursive: true, force: true }) +await writeIco(macosIcon, path.join(iconsDir, 'icon.ico')) + +if (process.platform === 'darwin') { + try { + execSync('command -v iconutil >/dev/null') + } catch { + console.warn('build-icons: iconutil not found, skipping .icns generation') + } + + if (existsSync('/usr/bin/iconutil')) { + rmSync(iconset, { recursive: true, force: true }) + mkdirSync(iconset, { recursive: true }) + + for (const [size, fileName] of iconSizes) { + const output = path.join(iconset, fileName) + execSync(`sips -z ${size} ${size} "${macosIcon}" --out "${output}"`, { + stdio: 'ignore', + }) + } + + execSync(`iconutil -c icns "${iconset}" -o "${icns}"`, { stdio: 'inherit' }) + cpSync(icns, path.join(iconsDir, 'icon.icns')) + rmSync(iconset, { recursive: true, force: true }) + } +} else { + console.log('build-icons: skipping .icns generation (not macOS)') +} console.log('build-icons: ok (icon-1024.png → src-tauri/icons/)') diff --git a/scripts/install-gifsicle.mjs b/scripts/install-gifsicle.mjs index dac0b1d..6c94833 100644 --- a/scripts/install-gifsicle.mjs +++ b/scripts/install-gifsicle.mjs @@ -16,6 +16,8 @@ const binaryName = process.platform === 'win32' ? 'gifsicle.exe' : 'gifsicle' const binaryPath = path.join(vendorDir, binaryName) const versionPattern = /1\.96/ +// windows ci: no configure/make on windows-latest +// todo: prebuilt gifsicle.exe for ci and release instead of skip if (process.env.CI_SKIP_GIFSICLE === '1') { console.log('gifsicle: skipped (CI_SKIP_GIFSICLE)') process.exit(0) diff --git a/scripts/prepare-resources.mjs b/scripts/prepare-resources.mjs index fcd19b0..6c1d0a6 100644 --- a/scripts/prepare-resources.mjs +++ b/scripts/prepare-resources.mjs @@ -4,9 +4,49 @@ import path from 'node:path' import { fileURLToPath } from 'node:url' const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..') -const target = path.join(root, 'src-tauri', 'resources') -const gifsicleSource = path.join(root, 'vendor', 'gifsicle', 'gifsicle') -const gifsicleTarget = path.join(target, 'vendor', 'gifsicle', 'gifsicle') +const tauriDir = path.join(root, 'src-tauri') +const target = path.join(tauriDir, 'resources') +const gifsicleBinary = + process.platform === 'win32' ? 'gifsicle.exe' : 'gifsicle' +const gifsicleSourceDir = path.join(root, 'vendor', 'gifsicle') +const gifsicleTargetDir = path.join(target, 'vendor', 'gifsicle') +const gifsicleTarget = path.join(gifsicleTargetDir, gifsicleBinary) + +const gifsicleSource = path.join(gifsicleSourceDir, gifsicleBinary) + +const copyGifsicleBundle = () => { + fs.rmSync(gifsicleTargetDir, { recursive: true, force: true }) + fs.mkdirSync(gifsicleTargetDir, { recursive: true }) + + if (!fs.existsSync(gifsicleSource)) { + console.error(`prepare-resources: gifsicle missing at ${gifsicleSource}`) + console.error('prepare-resources: run npm ci first') + process.exit(1) + } + + for (const entry of fs.readdirSync(gifsicleSourceDir)) { + const source = path.join(gifsicleSourceDir, entry) + if (!fs.statSync(source).isFile()) { + continue + } + + const dest = path.join(gifsicleTargetDir, entry) + fs.copyFileSync(source, dest) + if (process.platform !== 'win32' && entry === gifsicleBinary) { + fs.chmodSync(dest, 0o755) + } + } + + if (process.platform === 'win32') { + const requiredDlls = ['libwinpthread-1.dll', 'libgcc_s_seh-1.dll'] + for (const dll of requiredDlls) { + if (!fs.existsSync(path.join(gifsicleTargetDir, dll))) { + console.error(`prepare-resources: ${dll} missing next to gifsicle`) + process.exit(1) + } + } + } +} const releaseBuild = process.env.DROPSLIM_RELEASE === '1' const signingIdentity = releaseBuild @@ -15,6 +55,68 @@ const signingIdentity = releaseBuild const isReleaseSign = Boolean(signingIdentity && signingIdentity !== '-') +const vcRuntimeDlls = ['vcruntime140.dll', 'vcruntime140_1.dll', 'msvcp140.dll'] + +const copyWindowsNativeDeps = () => { + if (process.platform !== 'win32') { + return + } + + const vcpkgRoot = + process.env.VCPKG_INSTALLATION_ROOT || process.env.VCPKG_ROOT || '' + const dav1dSource = vcpkgRoot + ? path.join(vcpkgRoot, 'installed', 'x64-windows', 'bin', 'dav1d.dll') + : '' + + if (!dav1dSource || !fs.existsSync(dav1dSource)) { + console.error('prepare-resources: dav1d.dll missing for Windows bundle') + console.error( + 'prepare-resources: install with vcpkg install dav1d:x64-windows and set VCPKG_INSTALLATION_ROOT' + ) + process.exit(1) + } + + fs.copyFileSync(dav1dSource, path.join(tauriDir, 'dav1d.dll')) + console.log(`prepare-resources: bundled dav1d.dll from ${dav1dSource}`) + + const vcRuntimeDir = process.env.DROPSLIM_VC_RUNTIME_DIR + if ( + !vcRuntimeDir || + !fs.existsSync(path.join(vcRuntimeDir, 'vcruntime140.dll')) + ) { + console.error( + 'prepare-resources: DROPSLIM_VC_RUNTIME_DIR not set or invalid' + ) + console.error( + 'prepare-resources: run scripts/set-vc-runtime-env.ps1 on Windows CI' + ) + process.exit(1) + } + + for (const dll of vcRuntimeDlls) { + const source = path.join(vcRuntimeDir, dll) + if (!fs.existsSync(source)) { + console.error(`prepare-resources: ${dll} missing in ${vcRuntimeDir}`) + process.exit(1) + } + + fs.copyFileSync(source, path.join(tauriDir, dll)) + console.log(`prepare-resources: bundled ${dll}`) + } +} + +// .gitkeep satisfies tauri resources/**/* when gifsicle is skipped +// windows still needs dav1d.dll — tauri.windows.conf.json lists it as a bundle resource +if (process.env.CI_SKIP_GIFSICLE === '1') { + fs.rmSync(target, { recursive: true, force: true }) + const keep = path.join(target, 'vendor', 'gifsicle', '.gitkeep') + fs.mkdirSync(path.dirname(keep), { recursive: true }) + fs.writeFileSync(keep, '') + console.log('prepare-resources: skipped gifsicle (CI_SKIP_GIFSICLE)') + copyWindowsNativeDeps() + process.exit(0) +} + const signBinary = (filePath) => { const args = ['--force', '--sign', signingIdentity] @@ -52,16 +154,7 @@ const signBinary = (filePath) => { } fs.rmSync(target, { recursive: true, force: true }) -fs.mkdirSync(path.dirname(gifsicleTarget), { recursive: true }) - -if (!fs.existsSync(gifsicleSource)) { - console.error(`prepare-resources: gifsicle missing at ${gifsicleSource}`) - console.error('prepare-resources: run npm ci first') - process.exit(1) -} - -fs.copyFileSync(gifsicleSource, gifsicleTarget) -fs.chmodSync(gifsicleTarget, 0o755) +copyGifsicleBundle() if (process.platform === 'darwin') { console.log( @@ -72,4 +165,6 @@ if (process.platform === 'darwin') { console.log('prepare-resources: signing skipped (not macOS)') } +copyWindowsNativeDeps() + console.log(`prepare-resources: ok (${target})`) diff --git a/scripts/set-vc-runtime-env.ps1 b/scripts/set-vc-runtime-env.ps1 new file mode 100644 index 0000000..b2fdd04 --- /dev/null +++ b/scripts/set-vc-runtime-env.ps1 @@ -0,0 +1,11 @@ +$crt = Get-ChildItem -Path "$env:ProgramFiles\Microsoft Visual Studio\*\*\VC\Redist\MSVC\*\x64\Microsoft.VC*.CRT" -Directory -ErrorAction SilentlyContinue | + Sort-Object { try { [version]$_.Parent.Parent.Name } catch { '0.0' } } -Descending | + Select-Object -First 1 + +if (-not $crt) { + Write-Error 'VC runtime CRT directory not found' + exit 1 +} + +Write-Host "Using VC runtime from $($crt.FullName)" +Add-Content -Path $env:GITHUB_ENV -Value "DROPSLIM_VC_RUNTIME_DIR=$($crt.FullName)" diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index ad96a96..670f1d8 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -194,6 +194,28 @@ dependencies = [ "stable_deref_trait", ] +[[package]] +name = "ashpd" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2f3f79755c74fd155000314eb349864caa787c6592eace6c6882dad873d9c39" +dependencies = [ + "async-fs", + "async-net", + "enumflags2", + "futures-channel", + "futures-util", + "rand 0.9.4", + "raw-window-handle", + "serde", + "serde_repr", + "url", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "zbus", +] + [[package]] name = "async-broadcast" version = "0.7.2" @@ -232,6 +254,17 @@ dependencies = [ "slab", ] +[[package]] +name = "async-fs" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8034a681df4aed8b8edbd7fbe472401ecf009251c8b40556b304567052e294c5" +dependencies = [ + "async-lock", + "blocking", + "futures-lite", +] + [[package]] name = "async-io" version = "2.6.0" @@ -261,6 +294,17 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "async-net" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b948000fad4873c1c9339d60f2623323a0cfd3816e5181033c6a5cb68b2accf7" +dependencies = [ + "async-io", + "blocking", + "futures-lite", +] + [[package]] name = "async-process" version = "2.5.0" @@ -1240,6 +1284,15 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "dlib" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab8ecd87370524b461f8557c119c405552c396ed91fc0a8eec68679eab26f94a" +dependencies = [ + "libloading", +] + [[package]] name = "dlopen2" version = "0.8.2" @@ -1278,6 +1331,12 @@ dependencies = [ "tendril", ] +[[package]] +name = "downcast-rs" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2" + [[package]] name = "dpi" version = "0.1.2" @@ -1304,6 +1363,7 @@ dependencies = [ "oxvg_optimiser", "png 0.18.1", "ravif", + "rfd", "serde", "serde_json", "tauri", @@ -3902,6 +3962,12 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "pollster" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f3a9f18d041e6d0e102a0a46750538147e5e8992d3b4873aaafee2520b00ce3" + [[package]] name = "potential_utf" version = "0.1.5" @@ -4310,6 +4376,30 @@ dependencies = [ "web-sys", ] +[[package]] +name = "rfd" +version = "0.15.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef2bee61e6cffa4635c72d7d81a84294e28f0930db0ddcb0f66d10244674ebed" +dependencies = [ + "ashpd", + "block2", + "dispatch2", + "js-sys", + "log", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", + "pollster", + "raw-window-handle", + "urlencoding", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "windows-sys 0.59.0", +] + [[package]] name = "rgb" version = "0.8.53" @@ -4536,6 +4626,12 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "scoped-tls" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" + [[package]] name = "scopeguard" version = "1.2.0" @@ -6245,6 +6341,66 @@ dependencies = [ "semver", ] +[[package]] +name = "wayland-backend" +version = "0.3.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2857dd20b54e916ec7253b3d6b4d5c4d7d4ca2c33c2e11c6c76a99bd8744755d" +dependencies = [ + "cc", + "downcast-rs", + "rustix", + "scoped-tls", + "smallvec", + "wayland-sys", +] + +[[package]] +name = "wayland-client" +version = "0.31.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "645c7c96bb74690c3189b5c9cb4ca1627062bb23693a4fad9d8c3de958260144" +dependencies = [ + "bitflags 2.13.0", + "rustix", + "wayland-backend", + "wayland-scanner", +] + +[[package]] +name = "wayland-protocols" +version = "0.32.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23d0c813de3daa2ed6520af85a3bd49b0e722a3078506899aa9686fea58dc4b6" +dependencies = [ + "bitflags 2.13.0", + "wayland-backend", + "wayland-client", + "wayland-scanner", +] + +[[package]] +name = "wayland-scanner" +version = "0.31.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c324a910fd86ebdc364a3e61ec1f11737d3b1d6c273c0239ee8ff4bc0d24b4a" +dependencies = [ + "proc-macro2", + "quick-xml", + "quote", +] + +[[package]] +name = "wayland-sys" +version = "0.31.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8eab23fefc9e41f8e841df4a9c707e8a8c4ed26e944ef69297184de2785e3be" +dependencies = [ + "dlib", + "log", + "pkg-config", +] + [[package]] name = "web-sys" version = "0.3.100" @@ -7337,6 +7493,7 @@ dependencies = [ "endi", "enumflags2", "serde", + "url", "winnow 1.0.3", "zvariant_derive", "zvariant_utils", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 5e65e94..32319d1 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -48,3 +48,6 @@ objc2-image-io = "0.3" [target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies] tauri-plugin-updater = "2" tauri-plugin-process = "2" + +[target.'cfg(not(target_os = "macos"))'.dependencies] +rfd = "0.15" diff --git a/src-tauri/build.rs b/src-tauri/build.rs index 1effb6b..911a61c 100644 --- a/src-tauri/build.rs +++ b/src-tauri/build.rs @@ -2,6 +2,7 @@ fn main() { println!("cargo:rerun-if-changed=../assets/icon/icon-1024.png"); println!("cargo:rerun-if-changed=../scripts/build-icons.mjs"); println!("cargo:rerun-if-changed=icons/icon.png"); + #[cfg(target_os = "macos")] println!("cargo:rerun-if-changed=icons/icon.icns"); tauri_build::build() } diff --git a/src-tauri/src/commands/pick_paths.rs b/src-tauri/src/commands/pick_paths.rs index c7f6e88..29912a9 100644 --- a/src-tauri/src/commands/pick_paths.rs +++ b/src-tauri/src/commands/pick_paths.rs @@ -1,5 +1,5 @@ -use crate::macos_dialog; use crate::native_ui::load_strings; +use crate::platform::dialogs; use tauri::AppHandle; #[tauri::command] @@ -8,7 +8,7 @@ pub async fn pick_paths(app: AppHandle) -> Result, String> { let (tx, rx) = tokio::sync::oneshot::channel(); app.run_on_main_thread(move || { - let _ = tx.send(macos_dialog::pick_paths(&strings)); + let _ = tx.send(dialogs::pick_paths(&strings)); }) .map_err(|error| error.to_string())?; @@ -21,7 +21,7 @@ pub async fn pick_save_folder(app: AppHandle) -> Result, String> { let (tx, rx) = tokio::sync::oneshot::channel(); app.run_on_main_thread(move || { - let _ = tx.send(macos_dialog::pick_save_folder(&strings)); + let _ = tx.send(dialogs::pick_save_folder(&strings)); }) .map_err(|error| error.to_string())?; diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 0c728f6..cd35b6b 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -1,13 +1,15 @@ mod app_menu; mod commands; -mod macos_dialog; mod native_ui; pub mod optimize; +mod platform; mod startup_paths; use commands::startup::{emit_startup_paths, focus_main_window, StartupState}; use std::sync::Mutex; -use tauri::{Manager, RunEvent}; +use tauri::Manager; +#[cfg(target_os = "macos")] +use tauri::RunEvent; #[cfg_attr(mobile, tauri::mobile_entry_point)] pub fn run() { @@ -81,6 +83,7 @@ pub fn run() { .build(tauri::generate_context!()) .expect("error while building tauri application"); + #[cfg(target_os = "macos")] app.run(|app_handle, event| { if let RunEvent::Opened { urls } = event { let paths: Vec = urls @@ -95,4 +98,7 @@ pub fn run() { } } }); + + #[cfg(not(target_os = "macos"))] + app.run(|_, _| {}); } diff --git a/src-tauri/src/optimize/heic.rs b/src-tauri/src/optimize/heic.rs index cd1e322..13a4aad 100644 --- a/src-tauri/src/optimize/heic.rs +++ b/src-tauri/src/optimize/heic.rs @@ -99,7 +99,7 @@ pub fn optimize_heic(_input: &Path, _output: &Path) -> Result<(), ErrorPayload> Err(ErrorPayload::heic_unsupported_platform()) } -#[cfg(test)] +#[cfg(all(test, target_os = "macos"))] mod tests { use super::*; use std::fs; @@ -110,7 +110,6 @@ mod tests { } #[test] - #[cfg(target_os = "macos")] fn optimizes_heic_fixture() { let input = heic_fixture(); assert!( diff --git a/src-tauri/src/optimize/image.rs b/src-tauri/src/optimize/image.rs index a581f0b..74f20b9 100644 --- a/src-tauri/src/optimize/image.rs +++ b/src-tauri/src/optimize/image.rs @@ -169,16 +169,26 @@ fn optimize_png(input: &Path, output: &Path) -> Result<(), String> { } fn optimize_gif(input: &Path, output: &Path, gifsicle: &Path) -> Result<(), String> { - let status = Command::new(gifsicle) - .args([ - "-o", - &output.to_string_lossy(), - &input.to_string_lossy(), - "-O3", - "-i", - ]) - .status() - .map_err(|error| error.to_string())?; + let mut command = Command::new(gifsicle); + command.args([ + "-o", + &output.to_string_lossy(), + &input.to_string_lossy(), + "-O3", + "-i", + ]); + + #[cfg(windows)] + { + use std::os::windows::process::CommandExt; + + command.creation_flags(0x08000000); + if let Some(dir) = gifsicle.parent() { + command.current_dir(dir); + } + } + + let status = command.status().map_err(|error| error.to_string())?; if status.success() { Ok(()) diff --git a/src-tauri/src/optimize/summary.rs b/src-tauri/src/optimize/summary.rs index 9b2e8e9..4a15d26 100644 --- a/src-tauri/src/optimize/summary.rs +++ b/src-tauri/src/optimize/summary.rs @@ -136,8 +136,8 @@ mod tests { build_optimize_summary_payload(100_000, 40_000, None), SummaryPayload::Saved { percent: 60, - from: "100 KB".into(), - to: "40 KB".into(), + from: format_bytes(100_000), + to: format_bytes(40_000), } ); } @@ -147,7 +147,7 @@ mod tests { assert_eq!( build_optimize_summary_payload(1_000, 1_200, None), SummaryPayload::AlreadyOptimized { - size: "1 KB".into() + size: format_bytes(1_200), } ); } @@ -157,7 +157,7 @@ mod tests { assert_eq!( build_optimize_summary_payload(100_000, 50_000, Some(50_000)), SummaryPayload::AlreadyOptimized { - size: "50 KB".into() + size: format_bytes(50_000), } ); } @@ -168,8 +168,8 @@ mod tests { build_optimize_summary_payload(100_000, 40_000, Some(50_000)), SummaryPayload::SavedMore { percent: 20, - from: "50 KB".into(), - to: "40 KB".into(), + from: format_bytes(50_000), + to: format_bytes(40_000), } ); } diff --git a/src-tauri/src/optimize/tools.rs b/src-tauri/src/optimize/tools.rs index e39906b..0e2c2b7 100644 --- a/src-tauri/src/optimize/tools.rs +++ b/src-tauri/src/optimize/tools.rs @@ -1,10 +1,15 @@ use std::path::{Path, PathBuf}; pub fn gifsicle_path(project_root: &Path) -> Option { + let binary_name = if cfg!(windows) { + "gifsicle.exe" + } else { + "gifsicle" + }; let candidate = project_root .join("vendor") .join("gifsicle") - .join("gifsicle"); + .join(binary_name); candidate.exists().then_some(candidate) } diff --git a/src-tauri/src/platform/dialogs/desktop.rs b/src-tauri/src/platform/dialogs/desktop.rs new file mode 100644 index 0000000..221a4f1 --- /dev/null +++ b/src-tauri/src/platform/dialogs/desktop.rs @@ -0,0 +1,23 @@ +use crate::native_ui::NativeUiStrings; +use crate::optimize::formats::SUPPORTED_EXTENSIONS; +use rfd::FileDialog; + +pub fn pick_paths(strings: &NativeUiStrings) -> Result, String> { + Ok(FileDialog::new() + .set_title(&strings.pick_images) + .add_filter("Images", SUPPORTED_EXTENSIONS) + .pick_files() + .unwrap_or_default() + .into_iter() + .map(|path| path.to_string_lossy().into_owned()) + .collect()) +} + +pub fn pick_save_folder(strings: &NativeUiStrings) -> Result, String> { + Ok(FileDialog::new() + .set_title(&strings.pick_save_folder) + .pick_folder() + .into_iter() + .map(|path| path.to_string_lossy().into_owned()) + .collect()) +} diff --git a/src-tauri/src/macos_dialog.rs b/src-tauri/src/platform/dialogs/macos.rs similarity index 79% rename from src-tauri/src/macos_dialog.rs rename to src-tauri/src/platform/dialogs/macos.rs index 75397e7..37fd067 100644 --- a/src-tauri/src/macos_dialog.rs +++ b/src-tauri/src/platform/dialogs/macos.rs @@ -1,6 +1,5 @@ use crate::native_ui::NativeUiStrings; -#[cfg(target_os = "macos")] struct OpenPanelOptions { title: String, choose_files: bool, @@ -9,7 +8,6 @@ struct OpenPanelOptions { create_directories: bool, } -#[cfg(target_os = "macos")] fn run_open_panel(options: OpenPanelOptions) -> Result, String> { use objc2::MainThreadMarker; use objc2_app_kit::{NSModalResponseOK, NSOpenPanel}; @@ -42,7 +40,6 @@ fn run_open_panel(options: OpenPanelOptions) -> Result, String> { Ok(paths) } -#[cfg(target_os = "macos")] pub fn pick_paths(strings: &NativeUiStrings) -> Result, String> { run_open_panel(OpenPanelOptions { title: strings.pick_images.clone(), @@ -53,7 +50,6 @@ pub fn pick_paths(strings: &NativeUiStrings) -> Result, String> { }) } -#[cfg(target_os = "macos")] pub fn pick_save_folder(strings: &NativeUiStrings) -> Result, String> { let mut paths = run_open_panel(OpenPanelOptions { title: strings.pick_save_folder.clone(), @@ -66,13 +62,3 @@ pub fn pick_save_folder(strings: &NativeUiStrings) -> Result, String paths.truncate(1); Ok(paths) } - -#[cfg(not(target_os = "macos"))] -pub fn pick_save_folder(_strings: &NativeUiStrings) -> Result, String> { - Err("pick_save_folder is only supported on macOS".to_string()) -} - -#[cfg(not(target_os = "macos"))] -pub fn pick_paths(_strings: &NativeUiStrings) -> Result, String> { - Err("pick_paths is only supported on macOS".to_string()) -} diff --git a/src-tauri/src/platform/dialogs/mod.rs b/src-tauri/src/platform/dialogs/mod.rs new file mode 100644 index 0000000..db0c33e --- /dev/null +++ b/src-tauri/src/platform/dialogs/mod.rs @@ -0,0 +1,11 @@ +#[cfg(target_os = "macos")] +mod macos; + +#[cfg(not(target_os = "macos"))] +mod desktop; + +#[cfg(target_os = "macos")] +pub use macos::{pick_paths, pick_save_folder}; + +#[cfg(not(target_os = "macos"))] +pub use desktop::{pick_paths, pick_save_folder}; diff --git a/src-tauri/src/platform/mod.rs b/src-tauri/src/platform/mod.rs new file mode 100644 index 0000000..807bd7f --- /dev/null +++ b/src-tauri/src/platform/mod.rs @@ -0,0 +1 @@ +pub mod dialogs; diff --git a/src-tauri/src/startup_paths.rs b/src-tauri/src/startup_paths.rs index eb9d8da..7db9906 100644 --- a/src-tauri/src/startup_paths.rs +++ b/src-tauri/src/startup_paths.rs @@ -1,5 +1,6 @@ use std::path::Path; +#[cfg(target_os = "macos")] const APP_EXECUTABLE_PATTERN: &str = ".app/Contents/MacOS/"; pub fn is_startup_path(arg: &str) -> bool { @@ -26,10 +27,11 @@ pub fn is_startup_path(arg: &str) -> bool { } fn is_app_executable_path(resolved: &Path) -> bool { - let resolved_str = resolved.to_string_lossy(); - - if resolved_str.contains(APP_EXECUTABLE_PATTERN) { - return true; + #[cfg(target_os = "macos")] + { + if resolved.to_string_lossy().contains(APP_EXECUTABLE_PATTERN) { + return true; + } } if let Ok(exec_path) = std::env::current_exe() { @@ -89,6 +91,7 @@ mod tests { assert!(!is_startup_path("photo.png")); } + #[cfg(target_os = "macos")] #[test] fn rejects_app_executable_paths() { assert!(!is_startup_path( @@ -96,6 +99,14 @@ mod tests { )); } + #[test] + fn rejects_current_executable_path() { + let exec = std::env::current_exe().expect("current exe"); + let exec = exec.to_string_lossy().to_string(); + + assert!(!is_startup_path(&exec)); + } + #[test] fn parses_file_paths_from_args() { let stamp = SystemTime::now() @@ -106,10 +117,11 @@ mod tests { fs::write(&tmp, b"x").expect("write temp file"); let tmp = tmp.to_string_lossy().to_string(); - let parsed = parse_startup_args([ - "/Applications/DropSlim.app/Contents/MacOS/dropslim", - tmp.as_str(), - ]); + let exec = std::env::current_exe() + .expect("current exe") + .to_string_lossy() + .to_string(); + let parsed = parse_startup_args([exec.as_str(), tmp.as_str()]); assert_eq!(parsed, vec![tmp]); diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index cf37a8b..1281fe0 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -20,9 +20,6 @@ "minWidth": 480, "minHeight": 840, "resizable": true, - "transparent": true, - "titleBarStyle": "Overlay", - "acceptFirstMouse": true, "dragDropEnabled": true } ], @@ -33,8 +30,8 @@ "bundle": { "active": true, "createUpdaterArtifacts": true, - "targets": ["dmg", "app"], - "icon": ["icons/icon.png", "icons/icon.icns"], + "targets": "all", + "icon": ["icons/icon.png"], "fileAssociations": [ { "ext": [ @@ -52,18 +49,7 @@ "role": "Editor" } ], - "resources": ["resources/**/*"], - "macOS": { - "minimumSystemVersion": "11.0", - "signingIdentity": "-", - "hardenedRuntime": true, - "entitlements": "entitlements.plist", - "dmg": { - "appPosition": { "x": 168, "y": 240 }, - "applicationFolderPosition": { "x": 372, "y": 240 }, - "windowSize": { "width": 660, "height": 400 } - } - } + "resources": ["resources/**/*"] }, "plugins": { "updater": { diff --git a/src-tauri/tauri.macos.conf.json b/src-tauri/tauri.macos.conf.json new file mode 100644 index 0000000..a1da396 --- /dev/null +++ b/src-tauri/tauri.macos.conf.json @@ -0,0 +1,26 @@ +{ + "app": { + "windows": [ + { + "transparent": true, + "titleBarStyle": "Overlay", + "acceptFirstMouse": true + } + ] + }, + "bundle": { + "targets": ["dmg", "app"], + "icon": ["icons/icon.png", "icons/icon.icns"], + "macOS": { + "minimumSystemVersion": "11.0", + "signingIdentity": "-", + "hardenedRuntime": true, + "entitlements": "entitlements.plist", + "dmg": { + "appPosition": { "x": 168, "y": 240 }, + "applicationFolderPosition": { "x": 372, "y": 240 }, + "windowSize": { "width": 660, "height": 400 } + } + } + } +} diff --git a/src-tauri/tauri.windows.conf.json b/src-tauri/tauri.windows.conf.json new file mode 100644 index 0000000..4babe80 --- /dev/null +++ b/src-tauri/tauri.windows.conf.json @@ -0,0 +1,30 @@ +{ + "app": { + "windows": [ + { + "label": "main", + "title": "DropSlim", + "width": 480, + "height": 840, + "minWidth": 480, + "minHeight": 840, + "resizable": true, + "dragDropEnabled": true, + "transparent": false, + "titleBarStyle": "Visible", + "acceptFirstMouse": false + } + ] + }, + "bundle": { + "targets": ["nsis"], + "icon": ["icons/icon.ico", "icons/icon.png"], + "resources": { + "resources/": "resources/", + "dav1d.dll": "dav1d.dll", + "vcruntime140.dll": "vcruntime140.dll", + "vcruntime140_1.dll": "vcruntime140_1.dll", + "msvcp140.dll": "msvcp140.dll" + } + } +} diff --git a/src-tauri/tests/optimize_integration.rs b/src-tauri/tests/optimize_integration.rs index 706355b..45c2c57 100644 --- a/src-tauri/tests/optimize_integration.rs +++ b/src-tauri/tests/optimize_integration.rs @@ -35,7 +35,9 @@ fn create_raster_fixture(dir: &TempDir, name: &str, create: impl FnOnce(&Path)) path } -fn require_gifsicle() -> PathBuf { +// skips when gifsicle is missing (e.g. windows ci with ci_skip_gifsicle=1) +// todo: bundle gifsicle.exe and run gif integration tests on windows +fn require_gifsicle() -> Option { let gifsicle = project_root() .join("vendor") .join("gifsicle") @@ -46,13 +48,14 @@ fn require_gifsicle() -> PathBuf { }); if !gifsicle.exists() { - panic!( - "gifsicle not found at {} — run npm ci to install vendor binaries", + eprintln!( + "skip: gifsicle not found at {} — run npm ci to install vendor binaries", gifsicle.display() ); + return None; } - gifsicle + Some(gifsicle) } fn write_animated_gif(path: &Path) { @@ -177,7 +180,9 @@ fn optimizes_heic_fixture() { #[test] fn optimizes_gif_fixture() { - require_gifsicle(); + if require_gifsicle().is_none() { + return; + } let dir = tempfile::tempdir().expect("tempdir"); let input = create_raster_fixture(&dir, "sample.gif", |path| { @@ -191,7 +196,9 @@ fn optimizes_gif_fixture() { #[test] fn optimizes_animated_gif_preserving_frames() { - require_gifsicle(); + if require_gifsicle().is_none() { + return; + } let dir = tempfile::tempdir().expect("tempdir"); let input = dir.path().join("animated.gif"); diff --git a/ui/main.js b/ui/main.js index e4a47af..21721b8 100644 --- a/ui/main.js +++ b/ui/main.js @@ -1,4 +1,5 @@ import { getVersion } from '@tauri-apps/api/app' +import { type } from '@tauri-apps/plugin-os' import { createDropslimApi, initApi } from './api.js' import { initI18n, onLocaleChange } from './i18n/index.js' import { syncNativeUi } from './native.js' @@ -6,7 +7,16 @@ import { initRenderer } from './renderer.js' import { maybeCheckForUpdates } from './updates.js' const boot = async () => { - document.body.classList.add('platform-mac') + const osType = type() + + if (osType === 'macos') { + document.body.classList.add('platform-mac') + } else { + document.body.classList.add('platform-desktop') + if (osType === 'windows') { + document.body.classList.add('platform-win') + } + } const core = await initApi() const api = createDropslimApi(core) diff --git a/ui/styles/dropslim.css b/ui/styles/dropslim.css index 8045a48..ab67f80 100644 --- a/ui/styles/dropslim.css +++ b/ui/styles/dropslim.css @@ -43,6 +43,7 @@ body, background: transparent !important; } +/* macos */ body.platform-mac::before { content: ''; inset: 0; @@ -51,6 +52,12 @@ body.platform-mac::before { z-index: 0; } +/* windows & linux */ +body.platform-desktop, +body.platform-desktop .wrapper { + background: rgb(var(--color-surface-rgb)) !important; +} + @media (prefers-color-scheme: light) { body.platform-mac::before { background: rgb(255 255 255 / 0.32);