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