diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 17d4c08..1b6c0d0 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -53,9 +53,6 @@ jobs: export PATH="$PUB_BIN:$PATH" npm run setup:flutter - - name: Install Flutter dependencies - run: fvm flutter pub get - - name: Check formatting run: npm run format:check @@ -138,9 +135,6 @@ jobs: sudo apt-get update sudo apt-get install -y clang cmake ninja-build pkg-config libgtk-3-dev libepoxy-dev libmpv-dev mpv libayatana-appindicator3-dev libunwind-dev - - name: Flutter pub get - run: fvm flutter pub get - - name: Build smoke shell: bash env: diff --git a/.vscode/settings.json b/.vscode/settings.json index 5f9fcbc..d87f5be 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -12,5 +12,6 @@ "UDZO", "volname", "xcrun" - ] -} + ], + "dart.flutterSdkPath": ".fvm/flutter_sdk" +} \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index d8818a4..b98bf3f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,15 +1,15 @@ - +> [!NOTE] +> πŸ…±οΈ This is a Beta build. # ⬇️ Downloads | Windows | macOS | Linux | | :--- | :--- | :--- | -| **MSI:** [x64](https://github.com/BurntToasters/Dacx/releases/download/v0.11.0/Dacx-Windows-x64.msi) | **[Universal DMG](https://github.com/BurntToasters/Dacx/releases/download/v0.11.0/Dacx-macOS.dmg)** | **AppImage:** [x64](https://github.com/BurntToasters/Dacx/releases/download/v0.11.0/Dacx-Linux-x86_64.AppImage) | -| | **[Universal ZIP](https://github.com/BurntToasters/Dacx/releases/download/v0.11.0/Dacx-macOS.zip)** | **DEB:** [x64](https://github.com/BurntToasters/Dacx/releases/download/v0.11.0/Dacx-Linux-amd64.deb) | -| | | **RPM:** [x64](https://github.com/BurntToasters/Dacx/releases/download/v0.11.0/Dacx-Linux-x86_64.rpm) | -| | | **Flatpak:** [x64](https://github.com/BurntToasters/Dacx/releases/download/v0.11.0/Dacx-Linux-x86_64.flatpak) | -| | | **TAR (Generic Linux):** [x64](https://github.com/BurntToasters/Dacx/releases/download/v0.11.0/Dacx-Linux-x86_64.tar.gz) | +| **MSI:** [x64](https://github.com/BurntToasters/Dacx/releases/download/v0.11.1-beta.1/Dacx-Windows-x64.msi) | **[Universal DMG](https://github.com/BurntToasters/Dacx/releases/download/v0.11.1-beta.1/Dacx-macOS.dmg)** | **AppImage:** [x64](https://github.com/BurntToasters/Dacx/releases/download/v0.11.1-beta.1/Dacx-Linux-x86_64.AppImage) | +| | **[Universal ZIP](https://github.com/BurntToasters/Dacx/releases/download/v0.11.1-beta.1/Dacx-macOS.zip)** | **DEB:** [x64](https://github.com/BurntToasters/Dacx/releases/download/v0.11.1-beta.1/Dacx-Linux-amd64.deb) | +| | | **RPM:** [x64](https://github.com/BurntToasters/Dacx/releases/download/v0.11.1-beta.1/Dacx-Linux-x86_64.rpm) | +| | | **Flatpak:** [x64](https://github.com/BurntToasters/Dacx/releases/download/v0.11.1-beta.1/Dacx-Linux-x86_64.flatpak) | +| | | **TAR (Generic Linux):** [x64](https://github.com/BurntToasters/Dacx/releases/download/v0.11.1-beta.1/Dacx-Linux-x86_64.tar.gz) | > [!IMPORTANT] > The `.asc` files are my normal GPG signatures which you can verify using my GPG Public Key: https://tuxedo.rosie.run/GPG/BurntToasters_0xF2FBC20F_public.asc. @@ -18,6 +18,15 @@ ### ℹ️ Enjoying Dacx? Consider [❀️ Supporting Me! ❀️](https://rosie.run/support) +## Changes in `v0.11.1-beta.1:` + +- **Fix - Windows tray:** Minimize-to-tray left an empty notification-area slot because the tray API requires a multi-size `.ico`, not a PNG; tray and bundled assets now use `assets/icon/icon.ico` generated from `assets/icon/icon.png`. +- **Fix - macOS tray:** The menu bar tray icon uses a monochrome template PNG (`tray_icon_template.png`) so it renders correctly in light and dark menu bars. +- **Codebase:** `npm run icons:normalize` builds `assets/icon/icon.ico` and copies the same bytes to `windows/runner/resources/app_icon.ico` for the executable and MSI. +- **Codebase:** `npm run setup:flutter` repairs a broken global `fvm` CLI, reinstalls the pinned SDK when kernel checks fail, and runs `flutter pub get` plus `gen-l10n`. VM sync (`npm run vi`, `b`, `r`) and `setup.js` call it after `npm ci` / `npm install`. +- **PKG:** `npm run u2` runs `fvm flutter gen-l10n` after dependency updates so committed l10n outputs stay in sync. +- **PKG:** Updated packages. + ## Changes in `v0.11.0:` - **NEW - Windows code signing:** WOO HOO!! Windows Codesigning is here! - After a good while of not having it, Windows Binaries are now signed by Azure Artifact Signing! diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c96cb08..964c299 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -5,13 +5,14 @@ Thanks for helping improve Dacx. ## Development setup 1. Install Node.js 22+, Dart (for FVM), and platform build deps per [README.md](README.md). -2. `npm ci && npm run setup:flutter` -3. `fvm flutter pub get` -4. Run locally: `npm run dev` (or `dev:win` / `dev:mac` / `dev:linux`) +2. `npm ci && npm run setup:flutter` β€” installs or repairs the global `fvm` CLI, pins the Flutter SDK from `.fvmrc`, runs `flutter pub get`, and regenerates l10n (`gen-l10n`). Re-run this if you see `Invalid kernel binary format version` from FVM. +3. Run locally: `npm run dev` (or `dev:win` / `dev:mac` / `dev:linux`) Run every project Flutter or Dart command through FVM: `fvm flutter ...` or `fvm dart ...`. Never call the system `flutter` or `dart` directly. +VS Code / Cursor uses `"dart.flutterSdkPath": ".fvm/flutter_sdk"` (see `.vscode/settings.json`). That symlink is recreated when you bump `.fvmrc` and run `npm run setup:flutter` or `fvm use`; you do not edit the version number in settings by hand. + ## Quality gates (run before opening a PR) ```bash diff --git a/README.md b/README.md index 3006fd9..9fc00b1 100644 --- a/README.md +++ b/README.md @@ -83,8 +83,8 @@ Dacx is a desktop music and video player focused on speed and low overhead, with # Install Node.js dependencies (build scripts) npm install -# Install Flutter dependencies through FVM -fvm flutter pub get +# Install / repair FVM and Flutter (.fvmrc), then fetch packages and l10n +npm run setup:flutter # Run in development mode npm run dev diff --git a/assets/icon/icon.ico b/assets/icon/icon.ico new file mode 100644 index 0000000..24aff42 Binary files /dev/null and b/assets/icon/icon.ico differ diff --git a/assets/icon/tray_icon_template.png b/assets/icon/tray_icon_template.png new file mode 100644 index 0000000..11b9a15 Binary files /dev/null and b/assets/icon/tray_icon_template.png differ diff --git a/docs/QA.md b/docs/QA.md index e52e3ff..090d449 100644 --- a/docs/QA.md +++ b/docs/QA.md @@ -34,7 +34,7 @@ Tick items as you go before a stable cut. Fix failures as they surface rather th - [ ] Windows: Jump List recents + taskbar progress while playing - [ ] macOS: File menu + Dock menu New Window / Open; Open Recent β†’ Clear Menu - [ ] Linux AppImage: Check for Updates mentions AppManager and/or replacing the AppImage; deb/rpm shows package guidance (not β€œportable”) -- [ ] Minimize to tray (Appearance, off by default): close hides; tray Show restores; tray Quit exits +- [ ] Minimize to tray (Appearance, off by default): close hides; tray icon is visible on Windows and macOS; tray Show restores; tray Quit exits ## Settings / updates diff --git a/flatpak/run.rosie.dacx.yaml b/flatpak/run.rosie.dacx.yaml index 28053a8..1e128b7 100644 --- a/flatpak/run.rosie.dacx.yaml +++ b/flatpak/run.rosie.dacx.yaml @@ -1,4 +1,4 @@ -# x-version: 0.11.0 +# x-version: 0.11.1-beta.1 app-id: run.rosie.dacx runtime: org.freedesktop.Platform runtime-version: "25.08" diff --git a/lib/services/tray_service.dart b/lib/services/tray_service.dart index 8e00870..ed1784a 100644 --- a/lib/services/tray_service.dart +++ b/lib/services/tray_service.dart @@ -2,8 +2,6 @@ import 'dart:async'; import 'dart:io'; import 'package:flutter/foundation.dart'; -import 'package:flutter/services.dart'; -import 'package:path/path.dart' as p; import 'package:tray_manager/tray_manager.dart'; import 'package:window_manager/window_manager.dart'; @@ -19,26 +17,27 @@ class TrayService with TrayListener { bool _initialized = false; String _showLabel = 'Show Dacx'; String _quitLabel = 'Quit'; - String? _cachedIconPath; static bool get isSupported => !kIsWeb && (Platform.isWindows || Platform.isMacOS || Platform.isLinux); bool get isInitialized => _initialized; - /// Materialize the bundled PNG into a temp file (tray APIs need a disk path). - Future _resolveIconPath() async { - if (_cachedIconPath != null && File(_cachedIconPath!).existsSync()) { - return _cachedIconPath!; + /// Bundled asset path passed to [trayManager.setIcon] (relative to flutter_assets). + /// + /// Windows loads icons via `LoadImage` with [IMAGE_ICON], so a multi-size `.ico` + /// is required. macOS uses a monochrome template PNG for the menu bar. + @visibleForTesting + static String trayIconAssetPath({bool? isWindows, bool? isMacOS}) { + final onWindows = isWindows ?? Platform.isWindows; + final onMacOS = isMacOS ?? Platform.isMacOS; + if (onWindows) { + return 'assets/icon/icon.ico'; } - final data = await rootBundle.load('assets/icon/icon.png'); - final out = File(p.join(Directory.systemTemp.path, 'dacx_tray_icon.png')); - await out.writeAsBytes( - data.buffer.asUint8List(data.offsetInBytes, data.lengthInBytes), - flush: true, - ); - _cachedIconPath = out.path; - return out.path; + if (onMacOS) { + return 'assets/icon/tray_icon_template.png'; + } + return 'assets/icon/icon.png'; } Future init({ @@ -53,8 +52,12 @@ class TrayService with TrayListener { return; } try { - final icon = await _resolveIconPath(); - await trayManager.setIcon(icon); + final iconPath = trayIconAssetPath(); + if (Platform.isMacOS) { + await trayManager.setIcon(iconPath, isTemplate: true, iconSize: 22); + } else { + await trayManager.setIcon(iconPath); + } await trayManager.setToolTip('Dacx'); await _applyMenu(); trayManager.addListener(this); diff --git a/linux/packaging/control.template b/linux/packaging/control.template index 9198e05..9fe414c 100644 --- a/linux/packaging/control.template +++ b/linux/packaging/control.template @@ -1,5 +1,5 @@ Package: dacx -Version: 0.11.0 +Version: 0.11.1~beta.1 Section: sound Priority: optional Architecture: amd64 diff --git a/linux/packaging/icons/hicolor/128x128/apps/dacx.png b/linux/packaging/icons/hicolor/128x128/apps/dacx.png index 86f2ace..d7b740d 100644 Binary files a/linux/packaging/icons/hicolor/128x128/apps/dacx.png and b/linux/packaging/icons/hicolor/128x128/apps/dacx.png differ diff --git a/linux/packaging/icons/hicolor/16x16/apps/dacx.png b/linux/packaging/icons/hicolor/16x16/apps/dacx.png index 8fa6628..d193dbd 100644 Binary files a/linux/packaging/icons/hicolor/16x16/apps/dacx.png and b/linux/packaging/icons/hicolor/16x16/apps/dacx.png differ diff --git a/linux/packaging/icons/hicolor/256x256/apps/dacx.png b/linux/packaging/icons/hicolor/256x256/apps/dacx.png index a05f81c..87c4c5b 100644 Binary files a/linux/packaging/icons/hicolor/256x256/apps/dacx.png and b/linux/packaging/icons/hicolor/256x256/apps/dacx.png differ diff --git a/linux/packaging/icons/hicolor/32x32/apps/dacx.png b/linux/packaging/icons/hicolor/32x32/apps/dacx.png index 99bb33b..7d8b1dd 100644 Binary files a/linux/packaging/icons/hicolor/32x32/apps/dacx.png and b/linux/packaging/icons/hicolor/32x32/apps/dacx.png differ diff --git a/linux/packaging/icons/hicolor/48x48/apps/dacx.png b/linux/packaging/icons/hicolor/48x48/apps/dacx.png index 036e6dd..81a7d02 100644 Binary files a/linux/packaging/icons/hicolor/48x48/apps/dacx.png and b/linux/packaging/icons/hicolor/48x48/apps/dacx.png differ diff --git a/linux/packaging/icons/hicolor/512x512/apps/dacx.png b/linux/packaging/icons/hicolor/512x512/apps/dacx.png index d5c5ba1..7ccb303 100644 Binary files a/linux/packaging/icons/hicolor/512x512/apps/dacx.png and b/linux/packaging/icons/hicolor/512x512/apps/dacx.png differ diff --git a/linux/packaging/icons/hicolor/64x64/apps/dacx.png b/linux/packaging/icons/hicolor/64x64/apps/dacx.png index fbc8a96..096cb07 100644 Binary files a/linux/packaging/icons/hicolor/64x64/apps/dacx.png and b/linux/packaging/icons/hicolor/64x64/apps/dacx.png differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png index f40dcf4..0dd54a9 100644 Binary files a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png and b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png index bec6a0a..ac8d3d3 100644 Binary files a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png and b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png index 23f7ad2..3047574 100644 Binary files a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png and b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png index b0725d2..5b13a3b 100644 Binary files a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png and b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png index 6849aea..052b68d 100644 Binary files a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png and b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png index 6f835b3..d3b0c8b 100644 Binary files a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png and b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png index 68b35a5..497b143 100644 Binary files a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png and b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png differ diff --git a/package-lock.json b/package-lock.json index 06ffd94..d11a4b6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "dacx", - "version": "0.11.0", + "version": "0.11.1-beta.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "dacx", - "version": "0.11.0", + "version": "0.11.1-beta.1", "license": "GPL-3.0-only", "devDependencies": { "cross-spawn": "^7.0.6", diff --git a/package.json b/package.json index 9b0c5f3..ce5b1db 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "dacx", - "version": "0.11.0", + "version": "0.11.1-beta.1", "private": true, "description": "Fast, lightweight cross-platform music and video player for Windows, macOS, and Linux.", "license": "GPL-3.0-only", @@ -74,7 +74,7 @@ "//_r": "WARNING: r resets the current git branch, deletes unstaged files, switches to main and resets it.", "r": "git fetch origin && git reset --hard && git clean -fd && git switch -C main origin/main && git reset --hard origin/main && git clean -fd && npm run vi && npm run gitprune:force", "u": "npm update && fvm flutter pub upgrade && npm run sync-version && node scripts/update-metainfo.js && npm run format && npm run test:all", - "u2": "npm update && fvm flutter pub upgrade && npm run sync-version", + "u2": "npm update && fvm flutter pub upgrade && npm run sync-version && fvm flutter gen-l10n", "win-compiler:x64": "powershell -NoProfile -ExecutionPolicy Bypass -NoExit -Command \"& 'C:\\Program Files\\Microsoft Visual Studio\\18\\Community\\Common7\\Tools\\Launch-VsDevShell.ps1' -SkipAutomaticLocation\"", "win-compiler:arm64": "powershell -NoProfile -ExecutionPolicy Bypass -NoExit -Command \"& 'C:\\Program Files\\Microsoft Visual Studio\\18\\Community\\Common7\\Tools\\Launch-VsDevShell.ps1' -SkipAutomaticLocation -Arch arm64 -HostArch amd64\"", "flatpak:bundle": "node scripts/flatpak-bundle.js", diff --git a/pubspec.lock b/pubspec.lock index 524ec68..2752644 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -361,10 +361,10 @@ packages: dependency: "direct main" description: name: package_info_plus - sha256: f5c435dc0e0d461e5b32471a870f769b6a1cc46930637efe24fbc535314e78ad + sha256: "127e1751e37ffb2ff4658beeaca77bad0c27bf5f932bd3a501c2296926d4b481" url: "https://pub.dev" source: hosted - version: "10.2.0" + version: "10.2.1" package_info_plus_platform_interface: dependency: transitive description: @@ -433,18 +433,18 @@ packages: dependency: transitive description: name: posix - sha256: "185ef7606574f789b40f289c233efa52e96dead518aed988e040a10737febb07" + sha256: bc1bad54ad2b735816e31f8d4600cfde6c7839975085ddfbca48b6c9f7c4044e url: "https://pub.dev" source: hosted - version: "6.5.0" + version: "6.5.2" safe_local_storage: dependency: transitive description: name: safe_local_storage - sha256: "7483b3d5e8976f0bd263647c03b96131ee8e43f48b56fa8a8ec459e8515d74b0" + sha256: "494b982d5edb71030650ea463d939670e91b232b588323dc75229d2c5f23e7b7" url: "https://pub.dev" source: hosted - version: "2.0.4" + version: "2.0.6" screen_retriever: dependency: transitive description: @@ -590,10 +590,10 @@ packages: dependency: transitive description: name: synchronized - sha256: "93b153dcb6a26dcddee6ca087dd634b53e38c10b5aa163e8e49501a776456153" + sha256: "61894a1956de6b4fc1aefd0892e109514a1a706cbece3ac59decd90ff5a7a423" url: "https://pub.dev" source: hosted - version: "3.4.1" + version: "3.4.1+1" term_glyph: dependency: transitive description: @@ -710,10 +710,10 @@ packages: dependency: transitive description: name: uuid - sha256: "1fef9e8e11e2991bb773070d4656b7bd5d850967a2456cfc83cf47925ba79489" + sha256: "9b129329f58692f6e6578329498a8fe9fbe98f090beb764ffbb8ee2eadd01dcd" url: "https://pub.dev" source: hosted - version: "4.5.3" + version: "4.6.0" vector_math: dependency: transitive description: @@ -734,18 +734,18 @@ packages: dependency: transitive description: name: wakelock_plus - sha256: "824c5bba0f800e86d32e57d3d1843c531f090005cc89d9a837933e6601093d53" + sha256: "7253bca0fcf40d8413ddfcf4d2a1fa0a82475e79be25a4f2c564b695c9351486" url: "https://pub.dev" source: hosted - version: "1.6.1" + version: "1.7.0" wakelock_plus_platform_interface: dependency: transitive description: name: wakelock_plus_platform_interface - sha256: b13f99e992e7ae6a152e16c5559d3c07ff445b13330192662494e614ca3e7d7b + sha256: "0618d1799f0b28bcf98255b4ee8313e6fc4d38589dc4ee5fe5840d57d1aff6da" url: "https://pub.dev" source: hosted - version: "1.5.1" + version: "1.6.0" web: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index b51b85a..19ca221 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,7 +1,7 @@ name: dacx description: "Quick, lightweight cross-platform media player." publish_to: 'none' -version: 0.11.0+1100 +version: 0.11.1-beta.1+1101 environment: sdk: ^3.10.7 @@ -49,4 +49,5 @@ flutter: assets: - assets/cacert.pem - assets/icon/icon.png - - windows/runner/resources/app_icon.ico + - assets/icon/icon.ico + - assets/icon/tray_icon_template.png diff --git a/run.rosie.dacx.metainfo.xml b/run.rosie.dacx.metainfo.xml index 8b3b8f9..40f6234 100644 --- a/run.rosie.dacx.metainfo.xml +++ b/run.rosie.dacx.metainfo.xml @@ -87,6 +87,11 @@ application/pls+xml + + +

Fix Windows system tray icon (multi-size ICO). macOS menu bar tray template icon. Unified Windows icon.ico from icon.png. FVM setup repairs SDK and regenerates l10n.

+
+
diff --git a/scripts/normalize-iconsets.js b/scripts/normalize-iconsets.js index 286268e..e06823e 100644 --- a/scripts/normalize-iconsets.js +++ b/scripts/normalize-iconsets.js @@ -8,8 +8,10 @@ * Source: assets/icon/icon.png (1024Γ—1024 recommended) * * Outputs: - * Windows: windows/runner/resources/app_icon.ico (multi-size ICO) + * Windows: assets/icon/icon.ico (multi-size ICO from icon.png) + * windows/runner/resources/app_icon.ico (copy: exe, MSI, tray) * macOS: macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_*.png + * assets/icon/tray_icon_template.png (menu bar template) * Linux: linux/packaging/icons/ (hicolor sizes) */ @@ -45,31 +47,42 @@ function hasMagick() { } } -// Windows: multi-size .ico via ImageMagick +// Windows: multi-size .ico via ImageMagick (exe, installer, system tray) -function generateWindows() { - console.log("\n── Windows ──"); - const outDir = path.join(root, "windows", "runner", "resources"); - ensureDir(outDir); - const ico = path.join(outDir, "app_icon.ico"); +/** @type {number[]} DPI-friendly sizes for Windows 10/11 shell + tray */ +const WIN_ICO_SIZES = [16, 20, 24, 32, 40, 48, 64, 128, 256]; - // ICO embeds 16, 24, 32, 48, 64, 128, 256 - const sizes = [16, 24, 32, 48, 64, 128, 256]; - const resizeArgs = sizes - .map((s) => `\\( "${SOURCE}" -resize ${s}x${s} \\)`) - .join(" "); - - // On Windows, magick uses parentheses differently +function magickWindowsIcoCommand(outPath) { if (process.platform === "win32") { - const winArgs = sizes - .map((s) => `( "${SOURCE}" -resize ${s}x${s} )`) - .join(" "); - run(`magick ${winArgs} "${ico}"`); - } else { - run(`magick ${resizeArgs} "${ico}"`); + const layers = WIN_ICO_SIZES.map( + (s) => `( "${SOURCE}" -resize ${s}x${s} )`, + ).join(" "); + return `magick ${layers} "${outPath}"`; } + const layers = WIN_ICO_SIZES.map( + (s) => `\\( "${SOURCE}" -resize ${s}x${s} \\)`, + ).join(" "); + return `magick ${layers} "${outPath}"`; +} - console.log(` βœ“ ${path.relative(root, ico)}`); +function generateWindows() { + console.log("\n── Windows ──"); + const canonical = path.join(root, "assets", "icon", "icon.ico"); + const runnerIco = path.join( + root, + "windows", + "runner", + "resources", + "app_icon.ico", + ); + ensureDir(path.dirname(canonical)); + ensureDir(path.dirname(runnerIco)); + run(magickWindowsIcoCommand(canonical)); + fs.copyFileSync(canonical, runnerIco); + console.log(` βœ“ ${path.relative(root, canonical)}`); + console.log( + ` βœ“ ${path.relative(root, runnerIco)} (Runner.rc + WiX; same bytes as icon.ico)`, + ); } // macOS: individual PNGs for AppIcon.appiconset @@ -95,6 +108,24 @@ function generateMacOS() { } } +// macOS menu bar: black template PNG (transparent background) + +function generateTrayTemplate() { + console.log("\n── macOS tray template ──"); + const out = path.join(root, "assets", "icon", "tray_icon_template.png"); + ensureDir(path.dirname(out)); + // Flood-fill the app-icon background, then force RGB to black (keep alpha). + const floodfill = + '-fuzz 22% -fill none -draw "color 64,10 floodfill" ' + + "-channel RGB -evaluate set 0 +channel"; + if (process.platform === "win32") { + run(`magick "${SOURCE}" -resize 128x128 ${floodfill} "${out}"`); + } else { + run(`magick "${SOURCE}" -resize 128x128 ${floodfill} "${out}"`); + } + console.log(` βœ“ ${path.relative(root, out)}`); +} + // Linux: hicolor icon theme PNGs function generateLinux() { @@ -188,6 +219,7 @@ function main() { generateWindows(); generateMacOS(); + generateTrayTemplate(); generateMacOSDocumentIcons(); generateLinux(); diff --git a/scripts/package-release.js b/scripts/package-release.js index 170ad81..1cf8354 100644 --- a/scripts/package-release.js +++ b/scripts/package-release.js @@ -734,14 +734,12 @@ function writeWindowsWixV4Source(buildDir, wxsPath, audioIconFileName) { return lines; } - const appIconPath = path.join( - root, - "windows", - "runner", - "resources", - "app_icon.ico", - ); - const hasAppIcon = fs.existsSync(appIconPath); + const appIconCandidates = [ + path.join(root, "assets", "icon", "icon.ico"), + path.join(root, "windows", "runner", "resources", "app_icon.ico"), + ]; + const appIconPath = appIconCandidates.find((p) => fs.existsSync(p)); + const hasAppIcon = appIconPath != null; const msiVersion = toMsiVersion(VERSION); const iconBlock = hasAppIcon ? [ diff --git a/scripts/setup-flutter.js b/scripts/setup-flutter.js index 87c0f97..27d10ae 100644 --- a/scripts/setup-flutter.js +++ b/scripts/setup-flutter.js @@ -3,7 +3,7 @@ // quickly when already in sync. import { execSync } from 'node:child_process'; -import { appendFileSync, existsSync, readFileSync } from 'node:fs'; +import { appendFileSync, existsSync, readFileSync, writeFileSync } from 'node:fs'; import { delimiter, join } from 'node:path'; import { homedir } from 'node:os'; import crossSpawn from 'cross-spawn'; @@ -38,7 +38,7 @@ function run(cmd, args, opts = {}) { return result; } -function capture(cmd, args) { +function capture(cmd, args, { allowFailure = false } = {}) { const result = crossSpawn.sync(cmd, args, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'], @@ -46,10 +46,25 @@ function capture(cmd, args) { windowsHide: true, }); if (result.error) throw result.error; - if ((result.status ?? 1) !== 0) { - throw new Error(result.stderr || `${cmd} ${args.join(' ')} exited with ${result.status}`); + const stdout = (result.stdout || '').trim(); + const stderr = (result.stderr || '').trim(); + const combined = `${stdout}\n${stderr}`.trim(); + if ((result.status ?? 1) !== 0 && !allowFailure) { + throw new Error(combined || `${cmd} ${args.join(' ')} exited with ${result.status}`); + } + return { status: result.status ?? 1, stdout, stderr, combined }; +} + +function captureText(cmd, args, opts) { + const { combined, status } = capture(cmd, args, opts); + if (status !== 0 && !opts?.allowFailure) { + throw new Error(combined || `${cmd} ${args.join(' ')} exited with ${status}`); } - return (result.stdout || '').trim(); + return combined; +} + +function dartKernelMismatch(text) { + return /Invalid kernel binary format version/i.test(text); } function has(cmd) { @@ -88,29 +103,100 @@ console.log(`Pinned Flutter version: ${pinned}`); if (!has('fvm')) { console.log('β–Ά fvm not found; installing via `dart pub global activate fvm`'); run('dart', ['pub', 'global', 'activate', 'fvm']); -} - -let installed = ''; -try { - installed = capture('fvm', ['api', 'list']); -} catch { - installed = ''; -} -if (installed.includes(`"${pinned}"`)) { - console.log(`βœ“ Flutter ${pinned} already installed via fvm`); } else { - console.log(`β–Ά Installing Flutter ${pinned} via fvm`); - run('fvm', ['install', pinned]); + const fvmProbe = capture('fvm', ['--version'], { allowFailure: true }); + if (fvmProbe.status !== 0 || dartKernelMismatch(fvmProbe.combined)) { + console.log( + 'β–Ά Repairing global fvm (Dart SDK / kernel mismatch on the fvm CLI)', + ); + run('dart', ['pub', 'global', 'activate', 'fvm']); + } } -// Verify by running `fvm flutter --version` so any download/setup completes. -run('fvm', ['flutter', '--version']); +console.log(`β–Ά Ensuring Flutter ${pinned} is installed via fvm`); +run('fvm', ['install', pinned]); + +console.log(`β–Ά Pinning project to Flutter ${pinned}`); +run('fvm', ['use', pinned, '--force']); +syncVscodeFlutterSdkPath(); + +ensurePinnedSdkHealthy(pinned); + +console.log('β–Ά Resolving Dart/Flutter package graph and regenerating l10n'); +run('fvm', ['flutter', 'pub', 'get']); +run('fvm', ['flutter', 'gen-l10n']); // Persist `fvm` on the user's shell PATH so subsequent npm scripts that call // `fvm flutter` directly resolve it. Idempotent; checked before writing. ensurePubBinOnPath(); console.log(`βœ“ fvm flutter pinned to ${pinned} and ready`); +/** Stable VS Code path; FVM retargets `.fvm/flutter_sdk` on each `fvm use`. */ +const VSCODE_FLUTTER_SDK = '.fvm/flutter_sdk'; + +function syncVscodeFlutterSdkPath() { + const dir = join(process.cwd(), '.vscode'); + const file = join(dir, 'settings.json'); + if (!existsSync(dir)) return; + + let settings = {}; + if (existsSync(file)) { + try { + settings = JSON.parse(readFileSync(file, 'utf8')); + } catch { + console.warn('⚠ Could not parse .vscode/settings.json; skipping SDK path sync'); + return; + } + } + + if (settings['dart.flutterSdkPath'] === VSCODE_FLUTTER_SDK) return; + + settings['dart.flutterSdkPath'] = VSCODE_FLUTTER_SDK; + writeFileSync(file, `${JSON.stringify(settings, null, 2)}\n`, 'utf8'); + console.log(`βœ“ VS Code Flutter SDK path β†’ ${VSCODE_FLUTTER_SDK}`); +} + +function ensurePinnedSdkHealthy(version) { + const probe = () => { + const flutter = capture('fvm', ['flutter', '--version'], { allowFailure: true }); + const dart = capture('fvm', ['dart', '--version'], { allowFailure: true }); + const broken = + flutter.status !== 0 || + dart.status !== 0 || + dartKernelMismatch(flutter.combined) || + dartKernelMismatch(dart.combined); + return { broken, detail: flutter.combined || dart.combined }; + }; + + let { broken, detail } = probe(); + if (!broken) { + console.log('βœ“ Pinned Flutter/Dart SDK responds cleanly'); + return; + } + + console.log('β–Ά Pinned SDK health check failed; reinstalling cached Flutter'); + if (detail) { + console.log(` ${detail.split('\n').slice(0, 3).join('\n ')}`); + } + + run('fvm', ['remove', version]); + run('fvm', ['install', version]); + run('fvm', ['use', version, '--force']); + + ({ broken, detail } = probe()); + if (broken) { + console.error(''); + console.error('βœ– FVM Flutter SDK still unhealthy after reinstall.'); + console.error( + ' Try: dart pub global activate fvm && npm run setup:flutter', + ); + console.error(' Or install Flutter manually and run `fvm use` in this repo.'); + if (detail) console.error(` Last output: ${detail}`); + process.exit(1); + } + console.log('βœ“ Reinstalled pinned Flutter/Dart SDK'); +} + function ensurePubBinOnPath() { const bin = pubBinDir(); const userPath = process.env.PATH || ''; diff --git a/scripts/setup.js b/scripts/setup.js index 8eb1b71..4b60dd4 100644 --- a/scripts/setup.js +++ b/scripts/setup.js @@ -52,6 +52,15 @@ function header(msg) { console.log(`\n${'─'.repeat(60)}\n ${msg}\n${'─'.repeat(60)}`); } +function hasBootstrapDart() { + return hasCmd('dart'); +} + +function ensureDartOrFlutterForFvm() { + if (hasBootstrapDart()) return true; + return resolveFlutterOnWindowsPath(); +} + function resolveFlutterOnWindowsPath() { if (platform() !== 'win32') return hasCmd('flutter'); if (hasCmd('flutter')) return true; @@ -76,7 +85,7 @@ function resolveFlutterOnWindowsPath() { function printWindowsFlutterInstallHelp() { console.error( - '\nβœ– Flutter is not available on PATH.\n' + + '\nβœ– Dart or Flutter is not available on PATH (needed to bootstrap FVM).\n' + ' Install Flutter manually on Windows, restart terminal, then re-run setup.\n' + ' Docs: https://docs.flutter.dev/install/manual\n\n' + ' Quick PowerShell option:\n' + @@ -113,12 +122,12 @@ function printMacXcodeInstallHelp() { function setupWindows() { header('Dacx: Windows setup'); - // Flutter - if (!resolveFlutterOnWindowsPath()) { + // Dart (for FVM) or a Flutter SDK on PATH β€” setup:flutter pins .fvmrc afterward. + if (!ensureDartOrFlutterForFvm()) { printWindowsFlutterInstallHelp(); process.exit(1); } - console.log('βœ” Flutter found'); + console.log('βœ” Dart or Flutter found (FVM bootstrap)'); // Visual Studio Build Tools check console.log('\n⚠ Windows desktop builds require Visual Studio Build Tools with the'); @@ -175,13 +184,13 @@ function setupMac() { console.log('βœ” Homebrew found'); } - // Flutter - if (!hasCmd('flutter')) { + // Flutter (or Dart alone) β€” FVM pins the project SDK in commonSetup. + if (!hasCmd('flutter') && !hasBootstrapDart()) { console.log('\nInstalling Flutter via Homebrew...'); run('brew install --cask flutter'); console.log('\n⚠ You may need to restart your terminal / re-open SSH session so flutter is on PATH.'); } else { - console.log('βœ” Flutter found'); + console.log('βœ” Dart or Flutter found (FVM bootstrap)'); } // CocoaPods @@ -219,8 +228,8 @@ function setupLinux() { run(`sudo apt-get update`); run(`sudo apt-get install -y ${packages.join(' ')}`); - // Flutter via snap - if (!hasCmd('flutter')) { + // Flutter via snap (or existing Dart) β€” FVM pins the project SDK in commonSetup. + if (!hasCmd('flutter') && !hasBootstrapDart()) { console.log('\nInstalling Flutter via snap...'); if (hasCmd('snap')) { run('sudo snap install flutter --classic'); @@ -302,20 +311,16 @@ function setupLinux() { function commonSetup() { header('Common setup'); - // Flutter desktop support + console.log('\nInstalling npm packages...'); + run('npm install'); + + console.log('\nPinning Flutter via FVM (.fvmrc)...'); + run('npm run setup:flutter'); + const desktopDevice = target === 'win' ? 'windows' : target === 'mac' ? 'macos' : 'linux'; run(`fvm flutter config --enable-${desktopDevice}-desktop`, { allowFail: true }); - // doctor run('fvm flutter doctor -v', { allowFail: true }); - - // Dart/Flutter - console.log('\nInstalling Dart/Flutter packages...'); - run('fvm flutter pub get'); - - // npm - console.log('\nInstalling npm packages...'); - run('npm install'); } switch (target) { diff --git a/scripts/update-metainfo.js b/scripts/update-metainfo.js index 42e317e..4b1f93d 100644 --- a/scripts/update-metainfo.js +++ b/scripts/update-metainfo.js @@ -56,6 +56,18 @@ function run({ now = new Date() } = {}) { const releaseIndent = `${baseIndent} `; const newReleaseTag = `${releaseIndent}`; + function refreshReleaseTag(rawTag) { + const tag = rawTag.trim(); + const versionMatch = tag.match(/version="([^"]+)"/); + const tagVersion = versionMatch ? versionMatch[1] : ""; + if (tagVersion !== version) return tag; + + if (tag.includes("")) { + return tag.replace(/\bdate="[^"]*"/, `date="${dateStr}"`); + } + return ``; + } + const releasesSectionRegex = /[\s\S]*?<\/releases>/; const releasesSectionMatch = xml.match(releasesSectionRegex); if (!releasesSectionMatch) { @@ -76,7 +88,7 @@ function run({ now = new Date() } = {}) { if (tagVersion === version) { if (!replacedCurrentVersion) { - rebuiltEntries.push(newReleaseTag.trim()); + rebuiltEntries.push(refreshReleaseTag(tag)); replacedCurrentVersion = true; } continue; diff --git a/scripts/vi.js b/scripts/vi.js index ac07424..2503a58 100644 --- a/scripts/vi.js +++ b/scripts/vi.js @@ -1,4 +1,5 @@ #!/usr/bin/env node +// VM branch sync (npm run vi / b / r): reset to upstream, npm ci, then FVM pin + pub get + gen-l10n. import { execSync } from 'child_process'; import crossSpawn from 'cross-spawn'; @@ -17,6 +18,7 @@ try { run('git', ['clean', '-fd']); run('git', ['pull']); run('npm', ['ci']); + run('npm', ['run', 'setup:flutter']); const branch = execSync('git rev-parse --abbrev-ref HEAD', { encoding: 'utf8' }).trim(); const green = '\x1b[32m'; diff --git a/test/services/tray_service_test.dart b/test/services/tray_service_test.dart new file mode 100644 index 0000000..07a0dde --- /dev/null +++ b/test/services/tray_service_test.dart @@ -0,0 +1,40 @@ +import 'dart:io'; + +import 'package:dacx/services/tray_service.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + group('TrayService.trayIconAssetPath', () { + test('Windows uses bundled ICO (LoadImage IMAGE_ICON)', () { + expect( + TrayService.trayIconAssetPath(isWindows: true, isMacOS: false), + 'assets/icon/icon.ico', + ); + }); + + test('macOS uses monochrome template PNG', () { + expect( + TrayService.trayIconAssetPath(isWindows: false, isMacOS: true), + 'assets/icon/tray_icon_template.png', + ); + }); + + test('Linux uses full-color PNG asset', () { + expect( + TrayService.trayIconAssetPath(isWindows: false, isMacOS: false), + 'assets/icon/icon.png', + ); + }); + + test('matches current platform when overrides omitted', () { + final path = TrayService.trayIconAssetPath(); + if (Platform.isWindows) { + expect(path, endsWith('.ico')); + } else if (Platform.isMacOS) { + expect(path, contains('tray_icon_template')); + } else { + expect(path, 'assets/icon/icon.png'); + } + }); + }); +} diff --git a/windows/runner/resources/app_icon.ico b/windows/runner/resources/app_icon.ico index 756945a..24aff42 100644 Binary files a/windows/runner/resources/app_icon.ico and b/windows/runner/resources/app_icon.ico differ