From 1e5a60f2b1145cb9f9619adf944ba70f0aa9c51e Mon Sep 17 00:00:00 2001 From: Joel Jothiprakasam Date: Mon, 1 Jun 2026 21:12:23 -0700 Subject: [PATCH 01/40] Video playback in snap Signed-off-by: Joel Jothiprakasam --- .../message/attachment/other_file.dart | 2 +- .../parts/resolved_file_content.dart | 3 +- .../parts/upload_progress_content.dart | 3 +- snap/snapcraft.yaml | 40 +++++++++++++++++++ 4 files changed, 43 insertions(+), 5 deletions(-) diff --git a/lib/app/layouts/conversation_view/widgets/message/attachment/other_file.dart b/lib/app/layouts/conversation_view/widgets/message/attachment/other_file.dart index 4b6a147b9a..f5accdd0ef 100644 --- a/lib/app/layouts/conversation_view/widgets/message/attachment/other_file.dart +++ b/lib/app/layouts/conversation_view/widgets/message/attachment/other_file.dart @@ -97,7 +97,7 @@ class OtherFile extends StatelessWidget { final currentChat = ChatStateScope.maybeChatOf(context); return InkWell( onTap: () async { - if (attachment.mimeStart == "image" || (attachment.mimeStart == "video" && !isSnap)) { + if (attachment.mimeStart == "image" || (attachment.mimeStart == "video")) { Navigator.of(Get.context!).push( ThemeSwitcher.buildPageRoute( builder: (context) => FullscreenMediaHolder( diff --git a/lib/app/layouts/conversation_view/widgets/message/attachment/parts/resolved_file_content.dart b/lib/app/layouts/conversation_view/widgets/message/attachment/parts/resolved_file_content.dart index 51d5e5a4a6..cfa1865863 100644 --- a/lib/app/layouts/conversation_view/widgets/message/attachment/parts/resolved_file_content.dart +++ b/lib/app/layouts/conversation_view/widgets/message/attachment/parts/resolved_file_content.dart @@ -10,7 +10,6 @@ import 'package:bluebubbles/app/state/chat_state_scope.dart'; import 'package:bluebubbles/app/state/message_state_scope.dart'; import 'package:bluebubbles/app/layouts/fullscreen_media/fullscreen_holder.dart'; import 'package:bluebubbles/database/models.dart'; -import 'package:bluebubbles/helpers/helpers.dart'; import 'package:bluebubbles/services/services.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; @@ -96,7 +95,7 @@ class ResolvedFileContent extends StatelessWidget { ); } - if (attachment.mimeStart == "video" && !SettingsSvc.settings.highPerfMode.value && !isSnap) { + if (attachment.mimeStart == "video" && !SettingsSvc.settings.highPerfMode.value) { return VideoPlayer( attachment: attachment, file: file, diff --git a/lib/app/layouts/conversation_view/widgets/message/attachment/parts/upload_progress_content.dart b/lib/app/layouts/conversation_view/widgets/message/attachment/parts/upload_progress_content.dart index d8dd3db757..eb53c67628 100644 --- a/lib/app/layouts/conversation_view/widgets/message/attachment/parts/upload_progress_content.dart +++ b/lib/app/layouts/conversation_view/widgets/message/attachment/parts/upload_progress_content.dart @@ -4,7 +4,6 @@ import 'package:bluebubbles/app/layouts/conversation_view/widgets/message/attach import 'package:bluebubbles/app/layouts/conversation_view/widgets/message/attachment/video_player.dart'; import 'package:bluebubbles/app/state/attachment_state_scope.dart'; import 'package:bluebubbles/app/state/message_state_scope.dart'; -import 'package:bluebubbles/helpers/helpers.dart'; import 'package:bluebubbles/services/services.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; @@ -38,7 +37,7 @@ class UploadProgressContent extends StatelessWidget { ), ); } - if (previewFile != null && attachment.mimeStart == "video" && !SettingsSvc.settings.highPerfMode.value && !isSnap) { + if (previewFile != null && attachment.mimeStart == "video" && !SettingsSvc.settings.highPerfMode.value) { return VideoPlayer( attachment: attachment, file: previewFile, diff --git a/snap/snapcraft.yaml b/snap/snapcraft.yaml index cbdb521d33..649f7c87a4 100644 --- a/snap/snapcraft.yaml +++ b/snap/snapcraft.yaml @@ -136,6 +136,46 @@ parts: - zenity stage: - -usr/lib/*/libasound* + - -usr/lib/*/libmpv.so* + + mpv: + plugin: meson + source: https://github.com/mpv-player/mpv.git + source-tag: v0.41.0 + source-depth: 1 + meson-parameters: + - --prefix=/usr + - -Dlibmpv=true + - -Dcplayer=false + - -Dmanpage-build=disabled + - -Dalsa=disabled + - -Db_ndebug=true + build-packages: + - pkg-config + - libavcodec-dev + - libavfilter-dev + - libavformat-dev + - libavutil-dev + - libswresample-dev + - libswscale-dev + - libass-dev + - libplacebo-dev + - libdrm-dev + - libegl1-mesa-dev + - libgbm-dev + - libgl-dev + - libx11-dev + - libxext-dev + - libxkbcommon-dev + - libxpresent-dev + - libxrandr-dev + - libxss-dev + - libxv-dev + - libpulse-dev + - libjpeg-dev + prime: + - usr/lib/*/libmpv.so* + - usr/lib/libmpv.so* gpu-2404: after: From 1092751b3f50368f3b465f43c8787c3240fc70b2 Mon Sep 17 00:00:00 2001 From: Joel Jothiprakasam Date: Mon, 1 Jun 2026 23:06:43 -0700 Subject: [PATCH 02/40] Potentially fix network connectivity listener for some users Signed-off-by: Joel Jothiprakasam --- pubspec.lock | 21 +++++++++++---------- pubspec.yaml | 6 +++++- 2 files changed, 16 insertions(+), 11 deletions(-) diff --git a/pubspec.lock b/pubspec.lock index db0090735e..dbd8300fd2 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -388,10 +388,11 @@ packages: connectivity_plus: dependency: "direct main" description: - name: connectivity_plus - sha256: "62ffa266d9a23b79fb3fcbc206afc00bb979417ba57b1324c546b5aab95ba057" - url: "https://pub.dev" - source: hosted + path: "packages/connectivity_plus/connectivity_plus" + ref: "2b614414ce95d920880765d07cbb9759699a4563" + resolved-ref: "2b614414ce95d920880765d07cbb9759699a4563" + url: "https://github.com/edde746/plus_plugins" + source: git version: "7.1.1" connectivity_plus_platform_interface: dependency: transitive @@ -3112,26 +3113,26 @@ packages: dependency: transitive description: name: test - sha256: "280d6d890011ca966ad08df7e8a4ddfab0fb3aa49f96ed6de56e3521347a9ae7" + sha256: "8d9ceddbab833f180fbefed08afa76d7c03513dfdba87ffcec2718b02bbcbf20" url: "https://pub.dev" source: hosted - version: "1.30.0" + version: "1.31.0" test_api: dependency: transitive description: name: test_api - sha256: "8161c84903fd860b26bfdefb7963b3f0b68fee7adea0f59ef805ecca346f0c7a" + sha256: "949a932224383300f01be9221c39180316445ecb8e7547f70a41a35bf421fb9e" url: "https://pub.dev" source: hosted - version: "0.7.10" + version: "0.7.11" test_core: dependency: transitive description: name: test_core - sha256: "0381bd1585d1a924763c308100f2138205252fb90c9d4eeaf28489ee65ccde51" + sha256: "1991d4cfe85d5043241acac92962c3977c8d2f2add1ee73130c7b286417d1d34" url: "https://pub.dev" source: hosted - version: "0.6.16" + version: "0.6.17" throttling: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index c24d060365..26dcc2c147 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -34,7 +34,11 @@ dependencies: chunked_stream: ^1.4.2 collection: ^1.19.0 confetti: ^0.8.0 - connectivity_plus: ^7.0.0 + connectivity_plus: + git: + url: https://github.com/edde746/plus_plugins + ref: 2b614414ce95d920880765d07cbb9759699a4563 + path: packages/connectivity_plus/connectivity_plus crop_your_image: ^2.0.0 csslib: ^1.0.2 cupertino_icons: ^1.0.8 From 18b3120ce6efa1c887e931d76d65f17df92c34ef Mon Sep 17 00:00:00 2001 From: Joel Jothiprakasam Date: Mon, 1 Jun 2026 23:07:59 -0700 Subject: [PATCH 03/40] Bump versions Signed-off-by: Joel Jothiprakasam --- flatpak/app.bluebubbles.BlueBubbles.metainfo.xml | 3 ++- linux/build.sh | 2 +- pubspec.yaml | 2 +- snap/snapcraft.yaml | 2 +- windows/bluebubbles_installer_script.iss | 2 +- windows/runner/Runner.rc | 4 ++-- 6 files changed, 8 insertions(+), 7 deletions(-) diff --git a/flatpak/app.bluebubbles.BlueBubbles.metainfo.xml b/flatpak/app.bluebubbles.BlueBubbles.metainfo.xml index 681e050bcc..0574d929a8 100644 --- a/flatpak/app.bluebubbles.BlueBubbles.metainfo.xml +++ b/flatpak/app.bluebubbles.BlueBubbles.metainfo.xml @@ -66,7 +66,8 @@ - + + https://github.com/BlueBubblesApp/bluebubbles-app/releases/tag/v1.15.7%2B76-desktop diff --git a/linux/build.sh b/linux/build.sh index 913eaa23b5..5f036bf9be 100755 --- a/linux/build.sh +++ b/linux/build.sh @@ -18,7 +18,7 @@ fi # Inject version number into version.json tmp=$(mktemp) chmod 644 "$tmp" -jq '.version = "1.15.100.0"' build/linux/$folder/release/bundle/data/flutter_assets/version.json > "$tmp" && mv "$tmp" build/linux/$folder/release/bundle/data/flutter_assets/version.json +jq '.version = "1.15.101.0"' build/linux/$folder/release/bundle/data/flutter_assets/version.json > "$tmp" && mv "$tmp" build/linux/$folder/release/bundle/data/flutter_assets/version.json chmod +x build/linux/$folder/release/bundle/bluebubbles tar czvf bluebubbles-linux-"$arch".tar.gz -C build/linux/$folder/release/bundle . diff --git a/pubspec.yaml b/pubspec.yaml index 26dcc2c147..f717496558 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -326,7 +326,7 @@ msix_config: display_name: BlueBubbles publisher_display_name: BlueBubbles identity_name: 23344BlueBubbles.BlueBubbles - msix_version: 1.15.100.0 + msix_version: 1.15.101.0 publisher: CN=BEC9154D-191E-4375-BF30-698BD4C141C4 vs_generated_images_folder_path: windows/icons logo_path: assets/icon/icon.ico diff --git a/snap/snapcraft.yaml b/snap/snapcraft.yaml index 649f7c87a4..d2e3b50f4d 100644 --- a/snap/snapcraft.yaml +++ b/snap/snapcraft.yaml @@ -1,6 +1,6 @@ name: bluebubbles title: BlueBubbles -version: 1.15.100.0 +version: 1.15.101.0 summary: BlueBubbles client for Linux description: BlueBubbles is an open-source and cross-platform ecosystem of apps aimed to bring iMessage to Android, Windows, Linux, and more! With BlueBubbles, you'll be able to send messages, media, and much more to your friends and family. license: Apache-2.0 diff --git a/windows/bluebubbles_installer_script.iss b/windows/bluebubbles_installer_script.iss index 2581e04b4d..d9ab77e4b7 100644 --- a/windows/bluebubbles_installer_script.iss +++ b/windows/bluebubbles_installer_script.iss @@ -2,7 +2,7 @@ ; SEE THE DOCUMENTATION FOR DETAILS ON CREATING INNO SETUP SCRIPT FILES! #define MyAppName "BlueBubbles" -#define MyAppVersion "1.15.100.0" +#define MyAppVersion "1.15.101.0" #define MyAppPublisher "BlueBubbles" #define MyAppURL "https://bluebubbles.app/" #define MyAppExeName "bluebubbles_app.exe" diff --git a/windows/runner/Runner.rc b/windows/runner/Runner.rc index dd6f0ed358..b81094739f 100644 --- a/windows/runner/Runner.rc +++ b/windows/runner/Runner.rc @@ -59,8 +59,8 @@ IDI_APP_ICON ICON "resources\\app_icon.ico" // // Version // -#define VERSION_AS_NUMBER 1,15,100,0 -#define VERSION_AS_STRING "1.15.100.0" +#define VERSION_AS_NUMBER 1,15,101,0 +#define VERSION_AS_STRING "1.15.101.0" VS_VERSION_INFO VERSIONINFO FILEVERSION VERSION_AS_NUMBER From 388b272cfc8ec6fdfb9c2ec0e1d3619ab432fc31 Mon Sep 17 00:00:00 2001 From: Joel Jothiprakasam Date: Mon, 1 Jun 2026 23:09:07 -0700 Subject: [PATCH 04/40] More version Signed-off-by: Joel Jothiprakasam --- snap/snapcraft.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/snap/snapcraft.yaml b/snap/snapcraft.yaml index d2e3b50f4d..5fc53cf447 100644 --- a/snap/snapcraft.yaml +++ b/snap/snapcraft.yaml @@ -117,8 +117,8 @@ parts: - alsa-mixin - fmedia source: - - on amd64: https://github.com/BlueBubblesApp/bluebubbles-app/releases/download/v2.0.0%2B85-desktop-b.1/bluebubbles-linux-x86_64.tar.gz - - on arm64: https://github.com/BlueBubblesApp/bluebubbles-app/releases/download/v2.0.0%2B85-desktop-b.1/bluebubbles-linux-aarch64.tar.gz + - on amd64: https://github.com/BlueBubblesApp/bluebubbles-app/releases/download/v2.0.0%2B86-desktop-b.2/bluebubbles-linux-x86_64.tar.gz + - on arm64: https://github.com/BlueBubblesApp/bluebubbles-app/releases/download/v2.0.0%2B86-desktop-b.2/bluebubbles-linux-aarch64.tar.gz plugin: nil override-build: | set -eux From 9b7ec7a5a9e74343ea317b2e609dfea30afe66fc Mon Sep 17 00:00:00 2001 From: Joel Jothiprakasam Date: Mon, 1 Jun 2026 23:51:50 -0700 Subject: [PATCH 05/40] Fix originalUrl Priority Signed-off-by: Joel Jothiprakasam --- .../widgets/message/interactive/interactive_holder.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/app/layouts/conversation_view/widgets/message/interactive/interactive_holder.dart b/lib/app/layouts/conversation_view/widgets/message/interactive/interactive_holder.dart index 1903996876..6d6bcd5348 100644 --- a/lib/app/layouts/conversation_view/widgets/message/interactive/interactive_holder.dart +++ b/lib/app/layouts/conversation_view/widgets/message/interactive/interactive_holder.dart @@ -64,7 +64,7 @@ class _InteractiveHolderState extends State with AutomaticKee if (payloadData == null) { url = message.url; } else if (payloadData!.type == PayloadType.url) { - url = payloadData!.urlData!.first.url ?? payloadData!.urlData!.first.originalUrl; + url = payloadData!.urlData!.first.originalUrl ?? payloadData!.urlData!.first.url; } else { url = payloadData!.appData!.first.url; } From 6090692f6a5aa380c301803faa03134e5951346f Mon Sep 17 00:00:00 2001 From: Joel Jothiprakasam Date: Mon, 1 Jun 2026 23:55:04 -0700 Subject: [PATCH 06/40] invis ink wasn't actually invis (at least on desktop Signed-off-by: Joel Jothiprakasam --- .../conversation_view/widgets/message/misc/bubble_effects.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/app/layouts/conversation_view/widgets/message/misc/bubble_effects.dart b/lib/app/layouts/conversation_view/widgets/message/misc/bubble_effects.dart index 275239d91c..0fb52a93fa 100644 --- a/lib/app/layouts/conversation_view/widgets/message/misc/bubble_effects.dart +++ b/lib/app/layouts/conversation_view/widgets/message/misc/bubble_effects.dart @@ -229,7 +229,7 @@ class _BubbleEffectsState extends State with SingleTickerProvider connectUpper: false, ), child: BackdropFilter( - filter: ImageFilter.blur(sigmaX: 15, sigmaY: 15), + filter: ImageFilter.blur(sigmaX: 5, sigmaY: 5), child: Particles( key: UniqueKey(), height: size.height, From 3ab5c52bf0873a4148e7131a16fef2b3dc66be2c Mon Sep 17 00:00:00 2001 From: Joel Jothiprakasam Date: Thu, 11 Jun 2026 14:37:34 -0700 Subject: [PATCH 07/40] async/await Signed-off-by: Joel Jothiprakasam --- .../theme_studio/widgets/theme_management_section.dart | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/app/layouts/settings/pages/theming/theme_studio/widgets/theme_management_section.dart b/lib/app/layouts/settings/pages/theming/theme_studio/widgets/theme_management_section.dart index d686d1b22a..5ee89f6ea6 100644 --- a/lib/app/layouts/settings/pages/theming/theme_studio/widgets/theme_management_section.dart +++ b/lib/app/layouts/settings/pages/theming/theme_studio/widgets/theme_management_section.dart @@ -95,9 +95,9 @@ class ThemeManagementSection extends StatelessWidget { ), trailing: Switch( value: controller.activeTheme.gradientBg, - onChanged: (v) => _toggleGradient(context, v), + onChanged: (v) async => await _toggleGradient(context, v), ), - onTap: () => _toggleGradient(context, !controller.activeTheme.gradientBg), + onTap: () async => await _toggleGradient(context, !controller.activeTheme.gradientBg), ), ], ], @@ -239,13 +239,13 @@ class ThemeManagementSection extends StatelessWidget { } } - void _toggleGradient(BuildContext context, bool value) { + Future _toggleGradient(BuildContext context, bool value) async { controller.activeTheme.gradientBg = value; controller.activeTheme.save(); if (controller.isDark.value) { - ThemeSvc.changeTheme(context, dark: controller.activeTheme); + await ThemeSvc.changeTheme(context, dark: controller.activeTheme); } else { - ThemeSvc.changeTheme(context, light: controller.activeTheme); + await ThemeSvc.changeTheme(context, light: controller.activeTheme); } controller.bump(); } From 0caf5cb6109911445b7785a5f6a27d47e8d66ee3 Mon Sep 17 00:00:00 2001 From: Joel Jothiprakasam Date: Thu, 11 Jun 2026 14:38:14 -0700 Subject: [PATCH 08/40] Shared prefs work across isolates on windows/linux now. And corruption protection Signed-off-by: Joel Jothiprakasam --- lib/services/backend/settings/CLAUDE.md | 8 + .../desktop_shared_preferences_store.dart | 305 ++++++++++++++++++ .../settings/shared_preferences_service.dart | 12 + pubspec.lock | 6 +- pubspec.yaml | 3 + 5 files changed, 331 insertions(+), 3 deletions(-) create mode 100644 lib/services/backend/settings/desktop_shared_preferences_store.dart diff --git a/lib/services/backend/settings/CLAUDE.md b/lib/services/backend/settings/CLAUDE.md index a54bd0712e..2e47fd733a 100644 --- a/lib/services/backend/settings/CLAUDE.md +++ b/lib/services/backend/settings/CLAUDE.md @@ -39,3 +39,11 @@ final themeName = PrefsSvc.theme.getSelectedDarkTheme(); Use category helpers instead of `PrefsSvc.i` direct access. Keep raw `PrefsSvc.i` usage restricted to helper implementation internals. For app settings (everything in the `Settings` class), use `SettingsSvc` instead. `PrefsSvc` is only for low-level bootstrap values. + +--- + +### `desktop_shared_preferences_store.dart` — `DesktopSharedPreferencesStore` + +Custom `SharedPreferencesAsyncPlatform` backend registered on Windows/Linux only (in `SharedPreferencesService.init()`, so it covers every isolate). The stock desktop backends cache the prefs JSON per isolate, so writes from the GlobalIsolate/sync isolate silently revert keys written by the main isolate (flutter/flutter#143844), and non-atomic writes can corrupt the file on crash (flutter/flutter#89211). + +This store never caches, serializes writes across isolates/processes via an exclusively-created lock file, writes atomically (temp file + rename), and keeps a `.bak` snapshot refreshed on every successful write. A corrupt file is quarantined and restored from that backup (at startup and on mid-session reads). Storage path/format match the stock implementation. Do not reintroduce stock `shared_preferences` desktop backends or cache prefs values across isolates. diff --git a/lib/services/backend/settings/desktop_shared_preferences_store.dart b/lib/services/backend/settings/desktop_shared_preferences_store.dart new file mode 100644 index 0000000000..a09f574fe5 --- /dev/null +++ b/lib/services/backend/settings/desktop_shared_preferences_store.dart @@ -0,0 +1,305 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; + +import 'package:bluebubbles/utils/logger/logger.dart'; +import 'package:flutter/foundation.dart' show debugPrint; +import 'package:path/path.dart' as p; +import 'package:path_provider_linux/path_provider_linux.dart'; +import 'package:path_provider_windows/path_provider_windows.dart'; +import 'package:shared_preferences_platform_interface/shared_preferences_async_platform_interface.dart'; +import 'package:shared_preferences_platform_interface/types.dart'; + +/// Windows/Linux replacement for the stock shared_preferences backend. +/// +/// The stock `shared_preferences_windows`/`_linux` stores keep a per-isolate +/// in-memory snapshot of the JSON file, loaded once on first access, and every +/// write rewrites the whole file from that snapshot. With prefs used from the +/// main isolate, GlobalIsolate, and sync isolate, any write from one isolate +/// silently reverts keys written by another since that isolate started +/// (https://github.com/flutter/flutter/issues/143844). The stock store also +/// writes the file in place, so killing the app mid-write truncates it and the +/// legacy `SharedPreferences.getInstance()` then throws on every launch +/// (https://github.com/flutter/flutter/issues/89211). +/// +/// This store fixes both: +/// - No cache: every operation re-reads the file, so no isolate ever writes +/// from a stale snapshot. +/// - Writes are serialized across isolates AND processes with an exclusively +/// created lock file. (`RandomAccessFile.lock` is not enough: POSIX fcntl +/// locks are process-owned and do not exclude isolates within one process.) +/// - Writes are atomic: temp file + rename, so the store can never be +/// truncated by a crash mid-write. +/// - Every successful write also refreshes a `.bak` copy (under the same +/// lock), and an unparseable file is quarantined and restored from that +/// backup — at registration and on mid-session reads — instead of crashing +/// the app or losing all settings. +/// +/// Storage location and format are identical to the stock implementation, so +/// existing user data carries over untouched. Custom file names via +/// platform-specific [SharedPreferencesOptions] subclasses are not supported; +/// the app only ever uses the defaults. +base class DesktopSharedPreferencesStore extends SharedPreferencesAsyncPlatform { + DesktopSharedPreferencesStore._(); + + static const String _tag = 'DesktopPrefsStore'; + static const String _fileName = 'shared_preferences.json'; + static const String _backupSuffix = '.bak'; + static const Duration _staleLockTimeout = Duration(seconds: 10); + static const Duration _lockRetryDelay = Duration(milliseconds: 5); + static const int _renameAttempts = 5; + + String? _cachedDirectoryPath; + Future _writeQueue = Future.value(); + + /// Registers this store as the [SharedPreferencesAsyncPlatform] and + /// recovers a corrupt preferences file (quarantine + restore from backup) + /// before the legacy `SharedPreferences.getInstance()` (which throws on + /// unparseable JSON) gets a chance to read it. Must be called before any + /// prefs access in every isolate; only valid on Windows and Linux. + static Future register() async { + final store = DesktopSharedPreferencesStore._(); + await store._recoverCorruptFile(); + SharedPreferencesAsyncPlatform.instance = store; + } + + /// This store registers (and recovers/migrates) before [BaseLogger] exists + /// in every isolate's init sequence, so fall back to the console for + /// messages logged during that window. + static void _log(String message) { + try { + Logger.warn(message, tag: _tag); + } catch (_) { + debugPrint('[$_tag] $message'); + } + } + + @override + Future setString(String key, String value, SharedPreferencesOptions options) => + _mutate((prefs) => prefs[key] = value); + + @override + Future setBool(String key, bool value, SharedPreferencesOptions options) => + _mutate((prefs) => prefs[key] = value); + + @override + Future setDouble(String key, double value, SharedPreferencesOptions options) => + _mutate((prefs) => prefs[key] = value); + + @override + Future setInt(String key, int value, SharedPreferencesOptions options) => + _mutate((prefs) => prefs[key] = value); + + @override + Future setStringList(String key, List value, SharedPreferencesOptions options) => + _mutate((prefs) => prefs[key] = value); + + @override + Future getString(String key, SharedPreferencesOptions options) async => + (await _readFile())[key] as String?; + + @override + Future getBool(String key, SharedPreferencesOptions options) async => + (await _readFile())[key] as bool?; + + @override + Future getDouble(String key, SharedPreferencesOptions options) async => + (await _readFile())[key] as double?; + + @override + Future getInt(String key, SharedPreferencesOptions options) async => (await _readFile())[key] as int?; + + @override + Future?> getStringList(String key, SharedPreferencesOptions options) async => + ((await _readFile())[key] as List?)?.cast().toList(); + + @override + Future clear(ClearPreferencesParameters parameters, SharedPreferencesOptions options) { + final Set? allowList = parameters.filter.allowList; + return _mutate((prefs) => prefs.removeWhere((key, _) => allowList == null || allowList.contains(key))); + } + + @override + Future> getPreferences( + GetPreferencesParameters parameters, + SharedPreferencesOptions options, + ) async { + final Map prefs = await _readFile(); + final Set? allowList = parameters.filter.allowList; + if (allowList != null) { + prefs.removeWhere((key, _) => !allowList.contains(key)); + } + return prefs; + } + + @override + Future> getKeys(GetPreferencesParameters parameters, SharedPreferencesOptions options) async => + (await getPreferences(parameters, options)).keys.toSet(); + + Future _getDirectoryPath() async { + if (_cachedDirectoryPath != null) return _cachedDirectoryPath!; + // Instantiated directly (instead of going through path_provider) so this + // works in background isolates without plugin registration, exactly like + // the stock implementations do. + final String? directory = Platform.isWindows + ? await PathProviderWindows().getApplicationSupportPath() + : await PathProviderLinux().getApplicationSupportPath(); + if (directory == null) { + throw const FileSystemException('Unable to resolve the application support directory for preferences'); + } + return _cachedDirectoryPath = directory; + } + + Future _getDataFile() async => File(p.join(await _getDirectoryPath(), _fileName)); + + Future _getBackupFile() async => File('${(await _getDataFile()).path}$_backupSuffix'); + + /// Returns the parsed contents of [file], `{}` for an existing-but-empty + /// file, or null when the file is missing or unparseable. + Map? _parseFile(File file) { + try { + if (!file.existsSync()) return null; + final String contents = file.readAsStringSync(); + if (contents.isEmpty) return {}; + final Object? decoded = json.decode(contents); + return decoded is Map ? decoded.cast() : null; + } on FormatException catch (e) { + _log('Failed to parse ${file.path}: $e'); + return null; + } on FileSystemException catch (e) { + _log('Failed to read ${file.path}: $e'); + return null; + } + } + + Future> _readFile() async { + Map? prefs = _parseFile(await _getDataFile()); + if (prefs == null) { + prefs = _parseFile(await _getBackupFile()); + if (prefs != null) { + _log('Preferences file unreadable, using backup'); + } + } + return prefs ?? {}; + } + + /// Read-modify-write under the cross-isolate lock. Operations within this + /// isolate are additionally serialized through [_writeQueue] so they queue + /// up instead of spinning against each other on the lock file. + Future _mutate(void Function(Map prefs) mutator) { + final Future result = _writeQueue.then((_) => _locked(() async { + final Map prefs = await _readFile(); + mutator(prefs); + await _atomicWrite(prefs); + })); + _writeQueue = result.catchError((Object e) { + _log('Write failed: $e'); + }); + return result; + } + + /// Runs [action] while holding an exclusively created lock file — the only + /// primitive that excludes both other isolates and other processes on all + /// desktop platforms. + Future _locked(Future Function() action) async { + final File lockFile = File('${(await _getDataFile()).path}.lock'); + while (true) { + try { + lockFile.createSync(recursive: true, exclusive: true); + break; + } on FileSystemException { + _breakStaleLock(lockFile); + await Future.delayed(_lockRetryDelay); + } + } + try { + return await action(); + } finally { + try { + lockFile.deleteSync(); + } on FileSystemException { + // Best effort — a leftover lock is broken by _breakStaleLock later. + } + } + } + + /// Deletes the lock file if its holder appears to have died while holding it. + void _breakStaleLock(File lockFile) { + try { + if (DateTime.now().difference(lockFile.lastModifiedSync()) > _staleLockTimeout) { + _log('Breaking stale preferences lock'); + lockFile.deleteSync(); + } + } on FileSystemException { + // Lock was released between the failed create and now — just retry. + } + } + + /// Writes to a temp file and renames it over the data file so a crash + /// mid-write can never leave a truncated store behind. Then refreshes the + /// backup from the just-written file — encoded from memory and still under + /// the lock, so the backup is always a complete, parseable snapshot. + Future _atomicWrite(Map prefs) async { + final File file = await _getDataFile(); + final File tmp = File('${file.path}.tmp'); + try { + final RandomAccessFile raf = tmp.openSync(mode: FileMode.write); + try { + raf.writeStringSync(json.encode(prefs)); + raf.flushSync(); + } finally { + raf.closeSync(); + } + await _renameWithRetry(tmp, file.path); + } on FileSystemException catch (e) { + _log('Failed to save preferences: $e'); + return; + } + try { + file.copySync((await _getBackupFile()).path); + } on FileSystemException catch (e) { + _log('Failed to refresh preferences backup: $e'); + } + } + + /// Renaming over the data file fails on Windows while a concurrent reader + /// (e.g. the stock legacy store reading at startup) briefly holds it open, + /// so retry a few times before giving up. + Future _renameWithRetry(File tmp, String targetPath) async { + for (int attempt = 1; ; attempt++) { + try { + tmp.renameSync(targetPath); + return; + } on FileSystemException { + if (attempt >= _renameAttempts) rethrow; + await Future.delayed(const Duration(milliseconds: 10)); + } + } + } + + /// Quarantines an unparseable preferences file and restores the last good + /// backup in its place, so startup recovers prior settings instead of + /// throwing on every launch or starting empty. Also seeds the backup on + /// first run so recovery works before the first write. + Future _recoverCorruptFile() => _locked(() async { + final File file = await _getDataFile(); + final File backup = await _getBackupFile(); + try { + if (_parseFile(file) != null) { + if (!backup.existsSync()) file.copySync(backup.path); + return; + } + if (file.existsSync()) { + final String quarantinePath = '${file.path}.corrupt-${DateTime.now().millisecondsSinceEpoch}'; + _log('Quarantining corrupt preferences file to $quarantinePath'); + file.renameSync(quarantinePath); + } + if (_parseFile(backup) != null) { + _log('Restoring preferences from backup'); + backup.copySync(file.path); + } + } on FileSystemException catch (e) { + _log('Corrupt-file recovery failed: $e'); + } + }); +} diff --git a/lib/services/backend/settings/shared_preferences_service.dart b/lib/services/backend/settings/shared_preferences_service.dart index 07d4e016a3..f949a29087 100644 --- a/lib/services/backend/settings/shared_preferences_service.dart +++ b/lib/services/backend/settings/shared_preferences_service.dart @@ -1,6 +1,10 @@ +import 'dart:io'; + +import 'package:flutter/foundation.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/util/legacy_to_async_migration_util.dart'; import 'package:bluebubbles/services/backend/settings/actions/shared_preferences_admin_actions.dart'; +import 'package:bluebubbles/services/backend/settings/desktop_shared_preferences_store.dart'; import 'package:bluebubbles/services/backend/settings/actions/shared_preferences_database_actions.dart'; import 'package:bluebubbles/services/backend/settings/actions/shared_preferences_desktop_actions.dart'; import 'package:bluebubbles/services/backend/settings/actions/shared_preferences_firebase_actions.dart'; @@ -28,6 +32,14 @@ class SharedPreferencesService { late final SharedPreferencesSystemActions system; Future init({bool headless = false}) async { + // The stock Windows/Linux backends cache per isolate and clobber each + // other's writes; swap in the cross-isolate-safe store before anything + // (including the legacy migration below) touches prefs. Must run in every + // isolate, which this init does. macOS/mobile backends are already safe. + if (!kIsWeb && (Platform.isWindows || Platform.isLinux)) { + await DesktopSharedPreferencesStore.register(); + } + const sharedPreferencesOptions = SharedPreferencesOptions(); final SharedPreferences prefs = await SharedPreferences.getInstance(); await migrateLegacySharedPreferencesToSharedPreferencesAsyncIfNecessary( diff --git a/pubspec.lock b/pubspec.lock index dbd8300fd2..09fae1be53 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -2288,7 +2288,7 @@ packages: source: hosted version: "2.6.0" path_provider_linux: - dependency: transitive + dependency: "direct main" description: name: path_provider_linux sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279 @@ -2304,7 +2304,7 @@ packages: source: hosted version: "2.1.2" path_provider_windows: - dependency: transitive + dependency: "direct main" description: name: path_provider_windows sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7 @@ -2785,7 +2785,7 @@ packages: source: hosted version: "2.4.1" shared_preferences_platform_interface: - dependency: transitive + dependency: "direct main" description: name: shared_preferences_platform_interface sha256: "649dc798a33931919ea356c4305c2d1f81619ea6e92244070b520187b5140ef9" diff --git a/pubspec.yaml b/pubspec.yaml index f717496558..7bf17b2392 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -137,6 +137,8 @@ dependencies: particles_flutter: ^2.0.2 pasteboard: ^0.5.0 path_provider: ^2.1.5 # no web support + path_provider_linux: ^2.2.1 # desktop shared_preferences store path resolution + path_provider_windows: ^2.3.0 # desktop shared_preferences store path resolution pdf: ^3.11.3 permission_handler: ^12.0.1 # mobile only photo_manager: ^3.9.0 # only mobile; 3.9.0 has Kotlin classpath conflict with AGP 8.9/Kotlin 2.1 @@ -153,6 +155,7 @@ dependencies: secure_application: ^4.1.0 # no linux support share_plus: ^12.0.1 # sharing files not supported on Linux, doesn't work properly on Windows shared_preferences: ^2.5.3 + shared_preferences_platform_interface: ^2.4.2 # custom desktop store (see desktop_shared_preferences_store.dart) shimmer: ^3.0.0 simple_animations: ^5.1.0 skeletonizer: ^2.1.3 From 99871c4f11a07d51dd791d55f6682b27c503c1a1 Mon Sep 17 00:00:00 2001 From: Joel Jothiprakasam Date: Thu, 11 Jun 2026 14:52:09 -0700 Subject: [PATCH 09/40] Re-add lastOpenedChat functionality now that prefs works properly Signed-off-by: Joel Jothiprakasam --- .../layouts/chat_creator/chat_creator.dart | 14 ++++---- .../chat_creator/chat_creator_controller.dart | 24 ++++++------- .../dialogs/conversation_peek_view.dart | 2 +- .../pages/conversation_list.dart | 22 ++++++++++++ .../widgets/header/header_widgets.dart | 10 +++--- .../pages/conversation_view.dart | 2 +- .../pages/misc/troubleshoot_panel.dart | 1 - .../settings/pages/theming/theming_panel.dart | 2 +- lib/services/backend/settings/CLAUDE.md | 1 + .../shared_preferences_messaging_actions.dart | 11 ++++++ .../backend/settings/settings_service.dart | 2 +- lib/services/backend_ui_interop/intents.dart | 2 +- lib/services/ui/chat/chats_service.dart | 36 ++++++++++++------- .../ui/chat/conversation_view_controller.dart | 2 +- 14 files changed, 87 insertions(+), 44 deletions(-) diff --git a/lib/app/layouts/chat_creator/chat_creator.dart b/lib/app/layouts/chat_creator/chat_creator.dart index 697df3bcff..c6da777f3f 100644 --- a/lib/app/layouts/chat_creator/chat_creator.dart +++ b/lib/app/layouts/chat_creator/chat_creator.dart @@ -98,7 +98,7 @@ class ChatCreatorState extends State with ThemeHelpers { oldText = addressController.text; // if user has typed stuff, remove the message view and show filtered results if (addressController.text.isNotEmpty && fakeController.value != null) { - ChatsSvc.setAllInactive(); + await ChatsSvc.setAllInactive(); oldController = fakeController.value; fakeController.value = null; } @@ -166,7 +166,7 @@ class ChatCreatorState extends State with ThemeHelpers { Future findExistingChat({bool checkDeleted = false, bool update = true}) async { // no selected items, remove message view if (selectedContacts.isEmpty) { - ChatsSvc.setAllInactive(); + await ChatsSvc.setAllInactive(); fakeController.value = null; return null; } @@ -201,7 +201,7 @@ class ChatCreatorState extends State with ThemeHelpers { // if match, show message view, otherwise hide it if (update) { if (existingChat != null) { - ChatsSvc.setActiveChat(existingChat, clearNotifications: false); + await ChatsSvc.setActiveChat(existingChat, clearNotifications: false); ChatsSvc.activeChat!.controller = cvc(existingChat); // Get or create the MessagesService for this chat @@ -228,7 +228,7 @@ class ChatCreatorState extends State with ThemeHelpers { fakeController.value = ChatsSvc.activeChat!.controller; } else { - ChatsSvc.setAllInactive(); + await ChatsSvc.setAllInactive(); fakeController.value = null; messagesService = null; } @@ -379,7 +379,7 @@ class ChatCreatorState extends State with ThemeHelpers { ), Obx(() => MessageTypeToggle( selectedService: selectedService.value, - onToggle: (index) { + onToggle: (index) async { selectedContacts.clear(); addressController.text = ""; if (index == 0) { @@ -389,7 +389,7 @@ class ChatCreatorState extends State with ThemeHelpers { selectedService.value = ChatServiceType.sms; filteredChats.value = List.from(existingChats.where((e) => !e.isIMessage)); } - ChatsSvc.setAllInactive(); + await ChatsSvc.setAllInactive(); fakeController.value = null; }, )), @@ -493,7 +493,7 @@ class ChatCreatorState extends State with ThemeHelpers { final existingChat = chat; // Ensure fakeController is set up for this chat if (fakeController.value == null) { - ChatsSvc.setActiveChat(existingChat, clearNotifications: false); + await ChatsSvc.setActiveChat(existingChat, clearNotifications: false); ChatsSvc.activeChat!.controller = cvc(existingChat); fakeController.value = ChatsSvc.activeChat!.controller; } diff --git a/lib/app/layouts/chat_creator/chat_creator_controller.dart b/lib/app/layouts/chat_creator/chat_creator_controller.dart index f808e78617..7cfbe3cd9b 100644 --- a/lib/app/layouts/chat_creator/chat_creator_controller.dart +++ b/lib/app/layouts/chat_creator/chat_creator_controller.dart @@ -84,7 +84,7 @@ class ChatCreatorController extends StatefulController { // If the user has typed something while a chat is displayed, hide the // message view so the filtered search results are shown instead. if (text.isNotEmpty && activeController.value != null) { - deactivateExistingChat(); + await deactivateExistingChat(); } // If the user cleared the field and contacts are still selected, @@ -278,7 +278,7 @@ class ChatCreatorController extends StatefulController { filteredChats.value = result.chats; filteredContacts.value = result.contacts; if (selectedContacts.isEmpty) { - deactivateExistingChat(); + await deactivateExistingChat(); } else { await findExistingChat(); } @@ -288,13 +288,13 @@ class ChatCreatorController extends StatefulController { // Service type toggle // --------------------------------------------------------------------------- - void onServiceChanged(ChatServiceType service) { + Future onServiceChanged(ChatServiceType service) async { if (selectedService.value == service) return; selectedService.value = service; selectedContacts.clear(); addressController.text = ''; currentQuery.value = ''; - deactivateExistingChat(); + await deactivateExistingChat(); filteredChats.value = _allChats.where(_chatMatchesService).toList(); filteredContacts.value = _allContacts.where(_contactHasAddressForService).toList(); } @@ -305,7 +305,7 @@ class ChatCreatorController extends StatefulController { Future findExistingChat({bool checkDeleted = false, bool update = true}) async { if (selectedContacts.isEmpty) { - deactivateExistingChat(); + await deactivateExistingChat(); return null; } @@ -364,9 +364,9 @@ class ChatCreatorController extends StatefulController { if (update) { if (existingChat != null) { - _activateExistingChat(existingChat); + await _activateExistingChat(existingChat); } else { - deactivateExistingChat(); + await deactivateExistingChat(); } } @@ -378,8 +378,8 @@ class ChatCreatorController extends StatefulController { return existingChat; } - void _activateExistingChat(Chat chat, {bool transferText = true}) { - ChatsSvc.setActiveChat(chat, clearNotifications: false); + Future _activateExistingChat(Chat chat, {bool transferText = true}) async { + await ChatsSvc.setActiveChat(chat, clearNotifications: false); ChatsSvc.activeChat!.controller = cvc(chat); // Only create a new MessagesService if necessary. @@ -413,8 +413,8 @@ class ChatCreatorController extends StatefulController { activeController.value = newCVC; } - void deactivateExistingChat() { - ChatsSvc.setAllInactive(); + Future deactivateExistingChat() async { + await ChatsSvc.setAllInactive(); activeController.value = null; messagesService = null; } @@ -632,7 +632,7 @@ class ChatCreatorController extends StatefulController { // transferText: false — content is captured above and will go into pendingSend; // writing it into the CVC's textController would leave stale text visible in // the destination ConversationView if the clear races with Flutter rendering. - _activateExistingChat(resolvedChat, transferText: false); + await _activateExistingChat(resolvedChat, transferText: false); } // Pre-seed the messagesService struct with any messages already synced to the diff --git a/lib/app/layouts/conversation_list/dialogs/conversation_peek_view.dart b/lib/app/layouts/conversation_list/dialogs/conversation_peek_view.dart index 2ce4ccdaa3..3ef58deb48 100644 --- a/lib/app/layouts/conversation_list/dialogs/conversation_peek_view.dart +++ b/lib/app/layouts/conversation_list/dialogs/conversation_peek_view.dart @@ -62,7 +62,7 @@ class _ConversationPeekViewState extends State @override void initState() { super.initState(); - ChatsSvc.setActiveChat(widget.chat, clearNotifications: false); + ChatsSvc.setActiveChatSync(widget.chat, clearNotifications: false); ChatsSvc.activeChat!.controller = cvController; // Initialize messages service with message states for proper reactivity diff --git a/lib/app/layouts/conversation_list/pages/conversation_list.dart b/lib/app/layouts/conversation_list/pages/conversation_list.dart index e42206584e..076f3c33b9 100644 --- a/lib/app/layouts/conversation_list/pages/conversation_list.dart +++ b/lib/app/layouts/conversation_list/pages/conversation_list.dart @@ -9,6 +9,7 @@ import 'package:bluebubbles/app/layouts/conversation_list/widgets/initial_widget import 'package:bluebubbles/app/layouts/conversation_list/widgets/tile/conversation_tile.dart'; import 'package:bluebubbles/app/layouts/conversation_list/widgets/tile/material_conversation_tile.dart'; import 'package:bluebubbles/app/layouts/conversation_list/widgets/tile/samsung_conversation_tile.dart'; +import 'package:bluebubbles/app/layouts/conversation_view/pages/conversation_view.dart'; import 'package:bluebubbles/app/wrappers/bb_scaffold.dart'; import 'package:bluebubbles/app/wrappers/stateful_boilerplate.dart'; import 'package:bluebubbles/app/wrappers/tablet_mode_wrapper.dart'; @@ -143,6 +144,27 @@ class _ConversationListState extends CustomState route.isFirst, + ); + }); + } + } } @override diff --git a/lib/app/layouts/conversation_list/widgets/header/header_widgets.dart b/lib/app/layouts/conversation_list/widgets/header/header_widgets.dart index 79fbb5cf1a..39a6830029 100644 --- a/lib/app/layouts/conversation_list/widgets/header/header_widgets.dart +++ b/lib/app/layouts/conversation_list/widgets/header/header_widgets.dart @@ -497,7 +497,7 @@ Future goToSearch(BuildContext context) async { Future goToFindMy(BuildContext context) async { final currentChat = ChatsSvc.activeChat?.chat; NavigationSvc.closeAllConversationView(context); - ChatsSvc.setAllInactive(); + await ChatsSvc.setAllInactive(); await Navigator.of(Get.context!).push( ThemeSwitcher.buildPageRoute( builder: (BuildContext context) { @@ -506,7 +506,7 @@ Future goToFindMy(BuildContext context) async { ), ); if (currentChat != null) { - ChatsSvc.setActiveChat(currentChat); + await ChatsSvc.setActiveChat(currentChat); if (SettingsSvc.settings.tabletMode.value) { NavigationSvc.pushAndRemoveUntil( context, @@ -580,7 +580,7 @@ void goToUnknownSenders(BuildContext context) { Future goToSettings(BuildContext context) async { final currentChat = ChatsSvc.activeChat?.chat; NavigationSvc.closeAllConversationView(context); - ChatsSvc.setAllInactive(); + await ChatsSvc.setAllInactive(); await Navigator.of(Get.context!).push( ThemeSwitcher.buildPageRoute( builder: (BuildContext context) { @@ -589,7 +589,7 @@ Future goToSettings(BuildContext context) async { ), ); if (currentChat != null) { - ChatsSvc.setActiveChat(currentChat); + await ChatsSvc.setActiveChat(currentChat); if (SettingsSvc.settings.tabletMode.value) { NavigationSvc.pushAndRemoveUntil( context, @@ -597,7 +597,7 @@ Future goToSettings(BuildContext context) async { chat: currentChat, ), (route) => route.isFirst, - ).onError((error, stackTrace) => ChatsSvc.setAllInactive()); + ).onError((error, stackTrace) => ChatsSvc.setAllInactiveSync()); } else { cvc(currentChat).close(); } diff --git a/lib/app/layouts/conversation_view/pages/conversation_view.dart b/lib/app/layouts/conversation_view/pages/conversation_view.dart index a64f3dd657..3cd17d0c4b 100644 --- a/lib/app/layouts/conversation_view/pages/conversation_view.dart +++ b/lib/app/layouts/conversation_view/pages/conversation_view.dart @@ -66,7 +66,7 @@ class ConversationViewState extends State with ThemeHelpers with ThemeHelpers title: "Delete a Chat", subtitle: "Permanently deletes a selected chat, all its messages, and all its participants. Use this to simulate a brand-new chat arrival."), - const SettingsDivider(padding: EdgeInsets.only(left: 16.0)), ]), if (kIsDesktop) const SizedBox(height: 100), ], diff --git a/lib/app/layouts/settings/pages/theming/theming_panel.dart b/lib/app/layouts/settings/pages/theming/theming_panel.dart index e17117c0c4..5b70dafd1e 100644 --- a/lib/app/layouts/settings/pages/theming/theming_panel.dart +++ b/lib/app/layouts/settings/pages/theming/theming_panel.dart @@ -124,7 +124,7 @@ class _ThemingPanelState extends CustomState '${_replyToMessagePartPrefix}_$chatGuid'; + String? getLastOpenedChat() => service.i.getString(_lastOpenedChatKey); + + Future setLastOpenedChat(String chatGuid) async { + await service.i.setString(_lastOpenedChatKey, chatGuid); + } + + Future clearLastOpenedChat() async { + await service.i.remove(_lastOpenedChatKey); + } + Future saveReplyToMessageState({ required String chatGuid, String? messageGuid, diff --git a/lib/services/backend/settings/settings_service.dart b/lib/services/backend/settings/settings_service.dart index d869014c1e..ba2cf13ef8 100644 --- a/lib/services/backend/settings/settings_service.dart +++ b/lib/services/backend/settings/settings_service.dart @@ -281,7 +281,7 @@ class SettingsService { Navigator.of(context).pop(); NavigationSvc.closeSettings(context); NavigationSvc.closeAllConversationView(context); - ChatsSvc.setAllInactive(); + await ChatsSvc.setAllInactive(); await Navigator.of(Get.context!).push( ThemeSwitcher.buildPageRoute( builder: (BuildContext context) { diff --git a/lib/services/backend_ui_interop/intents.dart b/lib/services/backend_ui_interop/intents.dart index e069d15533..40b894225b 100644 --- a/lib/services/backend_ui_interop/intents.dart +++ b/lib/services/backend_ui_interop/intents.dart @@ -25,7 +25,7 @@ class OpenSettingsAction extends Action { if (SettingsSvc.settings.finishedSetup.value) { final currentChat = ChatsSvc.activeChat?.chat; NavigationSvc.closeAllConversationView(context); - ChatsSvc.setAllInactive(); + await ChatsSvc.setAllInactive(); await Navigator.of(Get.context!).push( ThemeSwitcher.buildPageRoute( builder: (BuildContext context) { diff --git a/lib/services/ui/chat/chats_service.dart b/lib/services/ui/chat/chats_service.dart index e912011a65..e432a0140e 100644 --- a/lib/services/ui/chat/chats_service.dart +++ b/lib/services/ui/chat/chats_service.dart @@ -746,8 +746,8 @@ class ChatsService { // ========== Chat Lifecycle Management Methods (migrated from ChatManager) ========== /// Set all chats to inactive synchronously - void _setAllInactiveSync({bool clearActive = true}) { - Logger.debug('Setting chats to inactive (clearActive: $clearActive)'); + void setAllInactiveSync({bool save = true, bool clearActive = true}) { + Logger.debug('Setting chats to inactive (save: $save, clearActive: $clearActive)'); String? skip; if (clearActive) { @@ -762,22 +762,28 @@ class ChatsService { state.updateActiveInternal(false); state.updateAliveInternal(false); }); + + if (save) { + EventDispatcherSvc.emit("update-highlight", null); + unawaited(PrefsSvc.messaging.clearLastOpenedChat()); + } } - /// Set all chats to inactive - void setAllInactive() async { + /// Set all chats to inactive asynchronously + Future setAllInactive() async { Logger.debug('Setting all chats to inactive'); - _setAllInactiveSync(); + await PrefsSvc.messaging.clearLastOpenedChat(); + setAllInactiveSync(save: false); } /// Set a chat as the active chat - void setActiveChat(Chat chat, {bool clearNotifications = true}) async { - _setActiveChatSync(chat, clearNotifications: clearNotifications); + Future setActiveChat(Chat chat, {bool clearNotifications = true}) async { + await PrefsSvc.messaging.setLastOpenedChat(chat.guid); + setActiveChatSync(chat, clearNotifications: clearNotifications, save: false); } - /// Set a chat as the active chat synchronously. - /// This does NOT save the last opened chat to preferences - void _setActiveChatSync(Chat chat, {bool clearNotifications = true}) { + /// Set a chat as the active chat synchronously + void setActiveChatSync(Chat chat, {bool clearNotifications = true, bool save = true}) { EventDispatcherSvc.emit("update-highlight", chat.guid); Logger.debug('Setting active chat to ${chat.guid} (${chat.displayName})'); @@ -789,7 +795,7 @@ class ChatsService { chatState.updateActiveAndAliveInternal(true); // Clear all other chats to inactive - _setAllInactiveSync(clearActive: false); + setAllInactiveSync(save: false, clearActive: false); if (clearNotifications) { // Defer the observable update to avoid updating during build phase @@ -797,6 +803,10 @@ class ChatsService { setChatHasUnread(chatState.chat, false, force: true); }); } + + if (save) { + unawaited(PrefsSvc.messaging.setLastOpenedChat(chat.guid)); + } } } @@ -841,7 +851,7 @@ class ChatsService { // Handle active chat cleanup if (activeChat?.chat.guid == chat.guid) { NavigationSvc.closeAllConversationView(Get.context!); - setAllInactive(); + await setAllInactive(); await Future.delayed(const Duration(milliseconds: 500)); } @@ -874,7 +884,7 @@ class ChatsService { // Handle active chat cleanup if (activeChat?.chat.guid == chat.guid) { NavigationSvc.closeAllConversationView(Get.context!); - setAllInactive(); + await setAllInactive(); await Future.delayed(const Duration(milliseconds: 500)); } diff --git a/lib/services/ui/chat/conversation_view_controller.dart b/lib/services/ui/chat/conversation_view_controller.dart index a83aa6961a..9d54de7e66 100644 --- a/lib/services/ui/chat/conversation_view_controller.dart +++ b/lib/services/ui/chat/conversation_view_controller.dart @@ -242,7 +242,7 @@ class ConversationViewController extends StatefulController with GetSingleTicker void close() { updateSmartReplyLayout(visible: false, height: 0); EventDispatcherSvc.emit("update-highlight", null); - ChatsSvc.setAllInactive(); + ChatsSvc.setAllInactiveSync(); Get.delete(tag: tag); } From eecccffae06ea40545efe9cb80a13240c45958db Mon Sep 17 00:00:00 2001 From: Joel Jothiprakasam Date: Thu, 11 Jun 2026 14:57:43 -0700 Subject: [PATCH 10/40] Remove Tenor gif picker Signed-off-by: Joel Jothiprakasam --- docs/models.md | 1 - .../text_field/text_field_icon_bar.dart | 95 ------------------- lib/database/CLAUDE.md | 2 +- lib/database/io/CLAUDE.md | 1 - lib/database/io/tenor.dart | 6 -- lib/database/models.dart | 1 - lib/main.dart | 3 - pubspec.yaml | 1 - 8 files changed, 1 insertion(+), 109 deletions(-) delete mode 100644 lib/database/io/tenor.dart diff --git a/docs/models.md b/docs/models.md index d26a10f21c..40ffd29042 100644 --- a/docs/models.md +++ b/docs/models.md @@ -106,7 +106,6 @@ Relations: `handles` -- `ToMany` (N:M, owning side) | File | Purpose | |------|---------| | `fcm_data.dart` | FCM tokens and Firebase auth credentials -- persisted per device | -| `tenor.dart` | GIF search result metadata (Tenor API) | | `launch_at_startup.dart` | Auto-launch configuration (desktop only) | --- diff --git a/lib/app/layouts/conversation_view/widgets/text_field/text_field_icon_bar.dart b/lib/app/layouts/conversation_view/widgets/text_field/text_field_icon_bar.dart index 290effd5ee..ddbb66ddd2 100644 --- a/lib/app/layouts/conversation_view/widgets/text_field/text_field_icon_bar.dart +++ b/lib/app/layouts/conversation_view/widgets/text_field/text_field_icon_bar.dart @@ -1,5 +1,3 @@ -import 'dart:async'; - import 'package:bluebubbles/app/layouts/conversation_view/widgets/text_field/conversation_text_field_local_controller.dart'; import 'package:bluebubbles/database/models.dart'; import 'package:bluebubbles/helpers/helpers.dart'; @@ -11,9 +9,7 @@ import 'package:file_picker/file_picker.dart' hide PlatformFile; import 'package:flutter/cupertino.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_dotenv/flutter_dotenv.dart'; import 'package:get/get.dart'; -import 'package:tenor_flutter/tenor_flutter.dart'; import 'package:universal_io/io.dart'; /// Left-side icon buttons in the conversation text field row: @@ -131,97 +127,6 @@ class TextFieldIconBar extends StatelessWidget { }, ), ), - if (!kIsWeb && !Platform.isAndroid) - IconButton( - icon: Icon(Icons.gif, color: context.theme.colorScheme.outline, size: 28), - onPressed: () async { - if (kIsDesktop || kIsWeb) { - controller.showingOverlays = true; - } - Tenor tenor = Tenor(apiKey: kIsWeb ? TENOR_API_KEY : dotenv.get('TENOR_API_KEY')); - TextEditingController tenorController = TextEditingController(); - FocusNode focus = FocusNode(); - Future resultFuture = tenor.showAsBottomSheet( - maxExtent: 0.8, - minExtent: 0.5, - debounce: const Duration(seconds: 1), - context: context, - searchFieldController: tenorController, - // Copied and slightly modified from source, just so I can autofocus - searchFieldWidget: Padding( - padding: const EdgeInsets.all(8), - child: Stack( - alignment: Alignment.center, - children: [ - TextField( - focusNode: focus, - controller: tenorController, - decoration: InputDecoration( - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(8), - borderSide: const BorderSide( - width: 0, - style: BorderStyle.none, - ), - ), - contentPadding: const EdgeInsets.fromLTRB(28, 5, 32, 7), - filled: true, - hintStyle: const TenorSearchFieldStyle().hintStyle, - hintText: "Search Tenor", - isCollapsed: true, - isDense: true, - ), - style: context.theme.textTheme.bodyMedium!, - ), - const Positioned( - left: 4, - child: Icon( - Icons.search, - color: Color(0xFF8A8A86), - size: 22, - ), - ), - ], - ), - ), - style: TenorStyle( - color: context.theme.colorScheme.surfaceContainerHighest, - attributionStyle: TenorAttributionStyle(brightnes: context.theme.brightness), - tabBarStyle: TenorTabBarStyle( - decoration: BoxDecoration( - color: context.theme.colorScheme.surfaceContainerHighest, - borderRadius: BorderRadius.circular(8)), - indicator: BoxDecoration( - color: context.theme.colorScheme.primary, - borderRadius: BorderRadius.circular(8), - ), - labelColor: context.theme.colorScheme.onSurface, - unselectedLabelColor: context.theme.colorScheme.onSurface.withValues(alpha: 0.5), - ), - ), - ); - focus.requestFocus(); - TenorResult? result = await resultFuture; - if (kIsDesktop || kIsWeb) { - controller.showingOverlays = false; - } - final selectedGif = result?.media.tinyGif ?? result?.media.tinyGifTransparent; - if (result != null && selectedGif != null) { - final response = await HttpSvc.downloadFromUrl(selectedGif.url); - if (response.statusCode == 200) { - try { - final Uint8List data = response.data; - controller.pickedAttachments.add(PlatformFile( - path: null, - name: "${result.id}.gif", - size: data.length, - bytes: data, - )); - return; - } catch (_) {} - } - } - }), if (kIsDesktop || kIsWeb) IconButton( icon: Icon(_iOS ? CupertinoIcons.smiley_fill : Icons.emoji_emotions, diff --git a/lib/database/CLAUDE.md b/lib/database/CLAUDE.md index 13cc83e27e..280a5761ac 100644 --- a/lib/database/CLAUDE.md +++ b/lib/database/CLAUDE.md @@ -11,7 +11,7 @@ Conditional imports resolve the correct implementation at compile time. ## Key Entities (`io/`) → `io/CLAUDE.md` - `chat.dart`, `message.dart`, `attachment.dart`, `handle.dart` -- `contact.dart`, `contact_v2.dart`, `theme.dart`, `fcm_data.dart`, `tenor.dart` +- `contact.dart`, `contact_v2.dart`, `theme.dart`, `fcm_data.dart` ## Key Shared Models (`global/`) → `global/CLAUDE.md` - `settings.dart`, `message_part.dart`, `attributed_body.dart` diff --git a/lib/database/io/CLAUDE.md b/lib/database/io/CLAUDE.md index 7cf9fb2cb5..defcd0e0e5 100644 --- a/lib/database/io/CLAUDE.md +++ b/lib/database/io/CLAUDE.md @@ -17,7 +17,6 @@ After any `@Entity` annotation change: **`dart run build_runner build`** | `theme_entry.dart` | reference to a Theme record | — | | `theme_object.dart` | theme metadata wrapper | — | | `fcm_data.dart` | FCM tokens and Firebase auth credentials | — | -| `tenor.dart` | GIF search result metadata | — | | `launch_at_startup.dart` | startup behavior configuration | — | ## Rules diff --git a/lib/database/io/tenor.dart b/lib/database/io/tenor.dart deleted file mode 100644 index bf76d61090..0000000000 --- a/lib/database/io/tenor.dart +++ /dev/null @@ -1,6 +0,0 @@ -/// THIS FILE IS A PLACEHOLDER FILE SO ANDROID / DESKTOP WILL STILL COMPILE -/// THE REAL API KEY IS PLACED WITHIN /html/tenor.dart (LOCAL ONLY!!) -/// DO NOT CHECK /html/tenor.dart INTO VCS -library bluebubbles; - -const TENOR_API_KEY = ""; diff --git a/lib/database/models.dart b/lib/database/models.dart index ca02b7355a..3cdb905d8f 100644 --- a/lib/database/models.dart +++ b/lib/database/models.dart @@ -21,7 +21,6 @@ export 'package:bluebubbles/database/io/theme_entry.dart' if (dart.library.html) 'package:bluebubbles/models/html/theme_entry.dart'; export 'package:bluebubbles/database/io/theme_object.dart' if (dart.library.html) 'package:bluebubbles/models/html/theme_object.dart'; -export 'package:bluebubbles/database/io/tenor.dart' if (dart.library.html) 'package:bluebubbles/models/html/tenor.dart'; export 'package:bluebubbles/database/global/platform_file.dart'; export 'package:bluebubbles/database/global/settings.dart'; export 'package:bluebubbles/database/global/attributed_body.dart'; diff --git a/lib/main.dart b/lib/main.dart index 549bbd8bfc..908a19d5fb 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -170,9 +170,6 @@ Future initApp(bool bubble, List arguments) async { SocketSvc.init(); } }); - - /* ----- GIPHY API KEY INITIALIZATION ----- */ - await dotenv.load(fileName: '.env', isOptional: true); } /* ----- EMOJI FONT INITIALIZATION ----- */ diff --git a/pubspec.yaml b/pubspec.yaml index 7bf17b2392..1c892ac1d1 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -300,7 +300,6 @@ flutter: - assets/badges/badge-8.ico - assets/badges/badge-9.ico - assets/badges/badge-10.ico - - .env # An image asset can refer to one or more resolution-specific "variants", see # https://flutter.dev/assets-and-images/#resolution-aware. # For details regarding adding assets from package dependencies, see From 1e83b4286a833919879d85bc1d3ecbb24d9bd494 Mon Sep 17 00:00:00 2001 From: Joel Jothiprakasam Date: Thu, 11 Jun 2026 16:04:06 -0700 Subject: [PATCH 11/40] Switch back to old drop impl Signed-off-by: Joel Jothiprakasam --- .../pages/handlers/drop_zone_manager.dart | 99 +++++++++++++++---- .../pages/messages_view.dart | 14 +-- .../widgets/messages_view_components.dart | 4 +- linux/flutter/generated_plugin_registrant.cc | 12 ++- linux/flutter/generated_plugins.cmake | 3 +- macos/Flutter/GeneratedPluginRegistrant.swift | 6 +- pubspec.lock | 56 +++++++++-- pubspec.yaml | 3 +- .../flutter/generated_plugin_registrant.cc | 9 +- windows/flutter/generated_plugins.cmake | 3 +- 10 files changed, 165 insertions(+), 44 deletions(-) diff --git a/lib/app/layouts/conversation_view/pages/handlers/drop_zone_manager.dart b/lib/app/layouts/conversation_view/pages/handlers/drop_zone_manager.dart index 53a18f39e2..0dfe8dcdec 100644 --- a/lib/app/layouts/conversation_view/pages/handlers/drop_zone_manager.dart +++ b/lib/app/layouts/conversation_view/pages/handlers/drop_zone_manager.dart @@ -4,8 +4,9 @@ import 'dart:typed_data'; import 'package:bluebubbles/database/global/platform_file.dart'; import 'package:bluebubbles/services/services.dart'; -import 'package:desktop_drop/desktop_drop.dart'; import 'package:get/get.dart'; +import 'package:path/path.dart' hide context; +import 'package:super_drag_and_drop/super_drag_and_drop.dart'; import 'package:universal_io/io.dart'; /// Manages drag-and-drop file operations on the message list. @@ -26,13 +27,30 @@ class DropZoneManager { DropZoneManager({required this.controller}); - /// Check if drop is allowed (only copy operations with file format items) - void onDropOver(DropEventDetails event) { + /// Check if drop is allowed (any copy operation carrying files) + DropOperation onDropOver(DropOverEvent event) { + if (!event.session.allowedOperations.contains(DropOperation.copy)) { + dragging.value = false; + return DropOperation.forbidden; + } + + numFiles.value = event.session.items + .where((item) => + item.canProvide(Formats.fileUri) || + Formats.standardFormats.whereType().any((f) => item.canProvide(f))) + .length; + + if (numFiles.value == 0) { + dragging.value = false; + return DropOperation.forbidden; + } + dragging.value = true; + return DropOperation.copy; } /// Handle drag leaving the drop zone - void onDropLeave(DropEventDetails event) { + void onDropLeave(DropEvent event) { dragging.value = false; } @@ -41,26 +59,73 @@ class DropZoneManager { /// Awaits all reader callbacks before returning so the native drop session /// stays alive until data has been fully read. Future onPerformDrop( - DropDoneDetails event, + PerformDropEvent event, ConversationViewController controller, ) async { - for (DropItem item in event.files) { - if (await FileSystemEntity.type(item.path) != FileSystemEntityType.file) continue; + final reads = >[]; - Uint8List bytes = await item.readAsBytes(); - String fileName = item.name; + for (DropItem item in event.session.items) { + final reader = item.dataReader; + if (reader == null) continue; - if (fileName.isEmpty) { - fileName = "Dragged_File_${controller.pickedAttachments.length + 1}"; - } + FileFormat? format = reader.getFormats(Formats.standardFormats).whereType().firstOrNull; - controller.pickedAttachments.add(PlatformFile( - name: fileName, - size: bytes.length, - bytes: bytes, - )); + // getFile/getValue deliver data via callback; track each read with a + // completer so the drop session isn't torn down before the data arrives. + final completer = Completer(); + + if (format != null) { + reads.add(completer.future); + reader.getFile(format, (file) async { + try { + Uint8List bytes = await file.readAll(); + String fileName = file.fileName ?? ""; + + // On Linux, the file path is encoded as UTF-8 bytes + if (Platform.isLinux) { + final filePath = String.fromCharCodes(bytes); + File linuxFile = File(filePath); + bytes = await linuxFile.readAsBytes(); + fileName = basename(filePath); + } + + if (fileName.isEmpty) { + fileName = "Dragged_File_${controller.pickedAttachments.length + 1}"; + } + + controller.pickedAttachments.add(PlatformFile( + name: fileName, + size: bytes.length, + bytes: bytes, + )); + } finally { + completer.complete(); + } + }, onError: (_) => completer.complete()); + } else if (reader.canProvide(Formats.fileUri)) { + // No standard format matched (arbitrary file type) — read straight + // from the dropped file's path instead. + reads.add(completer.future); + reader.getValue(Formats.fileUri, (uri) async { + try { + if (uri == null) return; + final file = File(uri.toFilePath()); + if (!await file.exists()) return; + + final bytes = await file.readAsBytes(); + controller.pickedAttachments.add(PlatformFile( + name: basename(file.path), + size: bytes.length, + bytes: bytes, + )); + } finally { + completer.complete(); + } + }, onError: (_) => completer.complete()); + } } + await Future.wait(reads); dragging.value = false; } } diff --git a/lib/app/layouts/conversation_view/pages/messages_view.dart b/lib/app/layouts/conversation_view/pages/messages_view.dart index d31067c07e..269fd51ca6 100644 --- a/lib/app/layouts/conversation_view/pages/messages_view.dart +++ b/lib/app/layouts/conversation_view/pages/messages_view.dart @@ -14,12 +14,12 @@ import 'package:bluebubbles/services/services.dart'; import 'package:bluebubbles/utils/logger/logger.dart'; import 'package:collection/collection.dart'; import 'package:defer_pointer/defer_pointer.dart'; -import 'package:desktop_drop/desktop_drop.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:scroll_to_index/scroll_to_index.dart'; +import 'package:super_drag_and_drop/super_drag_and_drop.dart'; import 'handlers/drop_zone_manager.dart'; import 'handlers/message_animation_orchestrator.dart'; @@ -558,11 +558,12 @@ class MessagesViewState extends State with MessagesServiceMixin, T @override Widget build(BuildContext context) { - return DropTarget( - onDragEntered: (DropEventDetails details) => dropZoneManager.onDropOver(details), - onDragUpdated: (DropEventDetails details) => dropZoneManager.onDropOver(details), - onDragExited: (DropEventDetails details) => dropZoneManager.onDropLeave(details), - onDragDone: (DropDoneDetails details) async => await dropZoneManager.onPerformDrop(details, controller), + return DropRegion( + hitTestBehavior: HitTestBehavior.translucent, + formats: Formats.standardFormats, + onDropOver: (DropOverEvent event) => dropZoneManager.onDropOver(event), + onDropLeave: (DropEvent event) => dropZoneManager.onDropLeave(event), + onPerformDrop: (PerformDropEvent event) async => await dropZoneManager.onPerformDrop(event, controller), child: GestureDetector( behavior: HitTestBehavior.deferToChild, onHorizontalDragUpdate: (details) { @@ -717,6 +718,7 @@ class MessagesViewState extends State with MessagesServiceMixin, T ), DragDropOverlay( dragging: dropZoneManager.dragging, + numFiles: dropZoneManager.numFiles, ), ], )), diff --git a/lib/app/layouts/conversation_view/widgets/messages_view_components.dart b/lib/app/layouts/conversation_view/widgets/messages_view_components.dart index c26c1ebb13..96565807b1 100644 --- a/lib/app/layouts/conversation_view/widgets/messages_view_components.dart +++ b/lib/app/layouts/conversation_view/widgets/messages_view_components.dart @@ -286,9 +286,11 @@ class DragDropOverlay extends StatelessWidget { const DragDropOverlay({ super.key, required this.dragging, + required this.numFiles, }); final RxBool dragging; + final RxInt numFiles; @override Widget build(BuildContext context) { @@ -307,7 +309,7 @@ class DragDropOverlay extends StatelessWidget { size: 50, ), Text( - "Attach File(s)", + "Attach ${numFiles.value} File${numFiles.value > 1 ? 's' : ''}", style: context.theme.textTheme.headlineLarge!.copyWith(color: context.theme.colorScheme.primary), ), ], diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc index f7da4b1ec7..28a2d40ad4 100644 --- a/linux/flutter/generated_plugin_registrant.cc +++ b/linux/flutter/generated_plugin_registrant.cc @@ -7,7 +7,6 @@ #include "generated_plugin_registrant.h" #include -#include #include #include #include @@ -15,6 +14,7 @@ #include #include #include +#include #include #include #include @@ -25,6 +25,7 @@ #include #include #include +#include #include #include #include @@ -34,9 +35,6 @@ void fl_register_plugins(FlPluginRegistry* registry) { g_autoptr(FlPluginRegistrar) bitsdojo_window_linux_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "BitsdojoWindowPlugin"); bitsdojo_window_plugin_register_with_registrar(bitsdojo_window_linux_registrar); - g_autoptr(FlPluginRegistrar) desktop_drop_registrar = - fl_plugin_registry_get_registrar_for_plugin(registry, "DesktopDropPlugin"); - desktop_drop_plugin_register_with_registrar(desktop_drop_registrar); g_autoptr(FlPluginRegistrar) desktop_webview_auth_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "DesktopWebviewAuthPlugin"); desktop_webview_auth_plugin_register_with_registrar(desktop_webview_auth_registrar); @@ -58,6 +56,9 @@ void fl_register_plugins(FlPluginRegistry* registry) { g_autoptr(FlPluginRegistrar) gtk_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "GtkPlugin"); gtk_plugin_register_with_registrar(gtk_registrar); + g_autoptr(FlPluginRegistrar) irondash_engine_context_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "IrondashEngineContextPlugin"); + irondash_engine_context_plugin_register_with_registrar(irondash_engine_context_registrar); g_autoptr(FlPluginRegistrar) local_notifier_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "LocalNotifierPlugin"); local_notifier_plugin_register_with_registrar(local_notifier_registrar); @@ -88,6 +89,9 @@ void fl_register_plugins(FlPluginRegistry* registry) { g_autoptr(FlPluginRegistrar) sqlite3_flutter_libs_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "Sqlite3FlutterLibsPlugin"); sqlite3_flutter_libs_plugin_register_with_registrar(sqlite3_flutter_libs_registrar); + g_autoptr(FlPluginRegistrar) super_native_extensions_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "SuperNativeExtensionsPlugin"); + super_native_extensions_plugin_register_with_registrar(super_native_extensions_registrar); g_autoptr(FlPluginRegistrar) system_tray_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "SystemTrayPlugin"); system_tray_plugin_register_with_registrar(system_tray_registrar); diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake index 0ae17f4963..eb3ebc090c 100644 --- a/linux/flutter/generated_plugins.cmake +++ b/linux/flutter/generated_plugins.cmake @@ -4,7 +4,6 @@ list(APPEND FLUTTER_PLUGIN_LIST bitsdojo_window_linux - desktop_drop desktop_webview_auth dynamic_color emoji_picker_flutter @@ -12,6 +11,7 @@ list(APPEND FLUTTER_PLUGIN_LIST flutter_acrylic flutter_timezone gtk + irondash_engine_context local_notifier maps_launcher media_kit_libs_linux @@ -22,6 +22,7 @@ list(APPEND FLUTTER_PLUGIN_LIST record_linux screen_retriever_linux sqlite3_flutter_libs + super_native_extensions system_tray tray_manager url_launcher_linux diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index d10a9bd76c..4761c0407d 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -9,7 +9,6 @@ import app_install_date import app_links import bitsdojo_window_macos import connectivity_plus -import desktop_drop import desktop_webview_auth import device_info_plus import dynamic_color @@ -23,6 +22,7 @@ import flutter_timezone import geolocator_apple import google_sign_in_ios import in_app_review +import irondash_engine_context import local_auth_darwin import local_notifier import macos_window_utils @@ -42,6 +42,7 @@ import share_plus import shared_preferences_foundation import sqlite3_flutter_libs import store_checker +import super_native_extensions import system_tray import tray_manager import url_launcher_macos @@ -54,7 +55,6 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { AppLinksMacosPlugin.register(with: registry.registrar(forPlugin: "AppLinksMacosPlugin")) BitsdojoWindowPlugin.register(with: registry.registrar(forPlugin: "BitsdojoWindowPlugin")) ConnectivityPlusPlugin.register(with: registry.registrar(forPlugin: "ConnectivityPlusPlugin")) - DesktopDropPlugin.register(with: registry.registrar(forPlugin: "DesktopDropPlugin")) DesktopWebviewAuthPlugin.register(with: registry.registrar(forPlugin: "DesktopWebviewAuthPlugin")) DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin")) DynamicColorPlugin.register(with: registry.registrar(forPlugin: "DynamicColorPlugin")) @@ -68,6 +68,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { GeolocatorPlugin.register(with: registry.registrar(forPlugin: "GeolocatorPlugin")) FLTGoogleSignInPlugin.register(with: registry.registrar(forPlugin: "FLTGoogleSignInPlugin")) InAppReviewPlugin.register(with: registry.registrar(forPlugin: "InAppReviewPlugin")) + IrondashEngineContextPlugin.register(with: registry.registrar(forPlugin: "IrondashEngineContextPlugin")) LocalAuthPlugin.register(with: registry.registrar(forPlugin: "LocalAuthPlugin")) LocalNotifierPlugin.register(with: registry.registrar(forPlugin: "LocalNotifierPlugin")) MacOSWindowUtilsPlugin.register(with: registry.registrar(forPlugin: "MacOSWindowUtilsPlugin")) @@ -87,6 +88,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) Sqlite3FlutterLibsPlugin.register(with: registry.registrar(forPlugin: "Sqlite3FlutterLibsPlugin")) StoreCheckerPlugin.register(with: registry.registrar(forPlugin: "StoreCheckerPlugin")) + SuperNativeExtensionsPlugin.register(with: registry.registrar(forPlugin: "SuperNativeExtensionsPlugin")) SystemTrayPlugin.register(with: registry.registrar(forPlugin: "SystemTrayPlugin")) TrayManagerPlugin.register(with: registry.registrar(forPlugin: "TrayManagerPlugin")) UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) diff --git a/pubspec.lock b/pubspec.lock index 09fae1be53..bc0f7976ff 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -546,14 +546,6 @@ packages: url: "https://pub.dev" source: hosted version: "0.0.2" - desktop_drop: - dependency: "direct main" - description: - name: desktop_drop - sha256: aa1e797255bfbc76f9eb5aa4f61e5b68dbf69962ab1be6495816d2f251bc0d1f - url: "https://pub.dev" - source: hosted - version: "0.7.1" desktop_webview_auth: dependency: "direct main" description: @@ -1742,6 +1734,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.5" + irondash_engine_context: + dependency: transitive + description: + name: irondash_engine_context + sha256: "2bb0bc13dfda9f5aaef8dde06ecc5feb1379f5bb387d59716d799554f3f305d7" + url: "https://pub.dev" + source: hosted + version: "0.5.5" + irondash_message_channel: + dependency: transitive + description: + name: irondash_message_channel + sha256: b4101669776509c76133b8917ab8cfc704d3ad92a8c450b92934dd8884a2f060 + url: "https://pub.dev" + source: hosted + version: "0.7.0" jni: dependency: transitive description: @@ -2408,6 +2416,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.15.0" + pixel_snap: + dependency: transitive + description: + name: pixel_snap + sha256: "677410ea37b07cd37ecb6d5e6c0d8d7615a7cf3bd92ba406fd1ac57e937d1fb0" + url: "https://pub.dev" + source: hosted + version: "0.1.5" platform: dependency: transitive description: @@ -3045,6 +3061,30 @@ packages: url: "https://pub.dev" source: hosted version: "1.2.0" + super_clipboard: + dependency: transitive + description: + name: super_clipboard + sha256: e73f3bb7e66cc9260efa1dc507f979138e7e106c3521e2dda2d0311f6d728a16 + url: "https://pub.dev" + source: hosted + version: "0.9.1" + super_drag_and_drop: + dependency: "direct main" + description: + name: super_drag_and_drop + sha256: "8946913a021cb617c35e36cfe57e8b817335643d7ee9bbc83d6e11760136bd1c" + url: "https://pub.dev" + source: hosted + version: "0.9.1" + super_native_extensions: + dependency: transitive + description: + name: super_native_extensions + sha256: b9611dcb68f1047d6f3ef11af25e4e68a21b1a705bbcc3eb8cb4e9f5c3148569 + url: "https://pub.dev" + source: hosted + version: "0.9.1" supercharged: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index 1c892ac1d1..34fd5f020d 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -43,7 +43,6 @@ dependencies: csslib: ^1.0.2 cupertino_icons: ^1.0.8 defer_pointer: ^0.0.2 - desktop_drop: ^0.7.1 desktop_webview_auth: git: url: https://github.com/BlueBubblesApp/flutter_desktop_webview_auth.git @@ -167,6 +166,7 @@ dependencies: store_checker: ^1.8.0 synchronized: ^3.3.0+3 system_tray: ^2.0.3 + super_drag_and_drop: ^0.9.1 supercharged: ^2.1.1 system_info2: ^4.0.0 tenor_flutter: ^1.0.3 # Pinned because of flutter SDK @@ -325,6 +325,7 @@ flutter: # see https://flutter.dev/custom-fonts/#from-packages msix_config: + output_path: windows display_name: BlueBubbles publisher_display_name: BlueBubbles identity_name: 23344BlueBubbles.BlueBubbles diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index 27524e6037..4cdfd0225b 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -9,7 +9,6 @@ #include #include #include -#include #include #include #include @@ -17,6 +16,7 @@ #include #include #include +#include #include #include #include @@ -31,6 +31,7 @@ #include #include #include +#include #include #include #include @@ -44,8 +45,6 @@ void RegisterPlugins(flutter::PluginRegistry* registry) { registry->GetRegistrarForPlugin("BitsdojoWindowPlugin")); ConnectivityPlusWindowsPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("ConnectivityPlusWindowsPlugin")); - DesktopDropPluginRegisterWithRegistrar( - registry->GetRegistrarForPlugin("DesktopDropPlugin")); DesktopWebviewAuthPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("DesktopWebviewAuthPlugin")); DynamicColorPluginCApiRegisterWithRegistrar( @@ -60,6 +59,8 @@ void RegisterPlugins(flutter::PluginRegistry* registry) { registry->GetRegistrarForPlugin("FlutterTimezonePluginCApi")); GeolocatorWindowsRegisterWithRegistrar( registry->GetRegistrarForPlugin("GeolocatorWindows")); + IrondashEngineContextPluginCApiRegisterWithRegistrar( + registry->GetRegistrarForPlugin("IrondashEngineContextPluginCApi")); LocalAuthPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("LocalAuthPlugin")); LocalNotifierPluginRegisterWithRegistrar( @@ -88,6 +89,8 @@ void RegisterPlugins(flutter::PluginRegistry* registry) { registry->GetRegistrarForPlugin("SharePlusWindowsPluginCApi")); Sqlite3FlutterLibsPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("Sqlite3FlutterLibsPlugin")); + SuperNativeExtensionsPluginCApiRegisterWithRegistrar( + registry->GetRegistrarForPlugin("SuperNativeExtensionsPluginCApi")); SystemTrayPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("SystemTrayPlugin")); TrayManagerPluginRegisterWithRegistrar( diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index 707e038b4a..c3b33c15f6 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -6,7 +6,6 @@ list(APPEND FLUTTER_PLUGIN_LIST app_links bitsdojo_window_windows connectivity_plus - desktop_drop desktop_webview_auth dynamic_color emoji_picker_flutter @@ -14,6 +13,7 @@ list(APPEND FLUTTER_PLUGIN_LIST flutter_acrylic flutter_timezone geolocator_windows + irondash_engine_context local_auth_windows local_notifier maps_launcher @@ -28,6 +28,7 @@ list(APPEND FLUTTER_PLUGIN_LIST secure_application share_plus sqlite3_flutter_libs + super_native_extensions system_tray tray_manager url_launcher_windows From 8f03deaaa278331253b75335440dd2ead2e4b70e Mon Sep 17 00:00:00 2001 From: Joel Jothiprakasam Date: Thu, 11 Jun 2026 16:04:32 -0700 Subject: [PATCH 12/40] Add windows build script. Improve installer cleanup Signed-off-by: Joel Jothiprakasam --- windows/.gitignore | 5 ++-- windows/CLAUDE.md | 1 + windows/bluebubbles_installer_script.iss | 21 +++++------------ windows/build.ps1 | 30 ++++++++++++++++++++++++ 4 files changed, 40 insertions(+), 17 deletions(-) create mode 100644 windows/build.ps1 diff --git a/windows/.gitignore b/windows/.gitignore index d156b41d34..3c7eda6ecb 100644 --- a/windows/.gitignore +++ b/windows/.gitignore @@ -19,5 +19,6 @@ x86/ /.vs/ cmake-build-debug -# Generated executable -bluebubbles-windows.exe \ No newline at end of file +# Generated installers +bluebubbles-windows.exe +*.msix \ No newline at end of file diff --git a/windows/CLAUDE.md b/windows/CLAUDE.md index b9ab0ab556..8d67dc3122 100644 --- a/windows/CLAUDE.md +++ b/windows/CLAUDE.md @@ -7,6 +7,7 @@ - `utils.cpp/h` — UTF-8 / UTF-16 helpers ## Installer +- `build.ps1` — release build script: cleans Release dir, `dart run msix:create`, then compiles the installer (mirrors `linux/build.sh`) - `bluebubbles_installer_script.iss` — Inno Setup installer definition - `CodeDependencies.iss` — installer dependency declarations diff --git a/windows/bluebubbles_installer_script.iss b/windows/bluebubbles_installer_script.iss index d9ab77e4b7..a69469283d 100644 --- a/windows/bluebubbles_installer_script.iss +++ b/windows/bluebubbles_installer_script.iss @@ -59,22 +59,13 @@ Source: "{#ProjectRoot}\build\windows\x64\runner\Release\*.dll"; DestDir: "{app} Source: "{#ProjectRoot}\build\windows\x64\runner\Release\data\*"; DestDir: "{app}\data"; Flags: ignoreversion recursesubdirs createallsubdirs ; NOTE: Don't use "Flags: ignoreversion" on any shared system files -; Having old versions of these causes crashes for some users +; Wipe all DLLs before install; [Files] re-copies the current set right after. +; This automatically removes leftovers from removed plugins (e.g. desktop_drop, +; screen_brightness) and old runtime DLLs that caused crashes for some users. [InstallDelete] -Type: files; Name: "{app}\api-ms-win-*.dll" -Type: files; Name: "{app}\concrt140.dll" -Type: files; Name: "{app}\libc++.dll" -Type: files; Name: "{app}\media_kit_native_event_loop.dll" -Type: files; Name: "{app}\msvcp140*.dll" -Type: files; Name: "{app}\screen_brightness_windows_plugin.dll" -Type: files; Name: "{app}\ucrtbase.dll" -Type: files; Name: "{app}\ucrtbased.dll" -Type: files; Name: "{app}\vccorlib140.dll" -Type: files; Name: "{app}\vccorlib140d.dll" -Type: files; Name: "{app}\vcruntime140.dll" -Type: files; Name: "{app}\vcruntime140_1.dll" -Type: files; Name: "{app}\vcruntime140_1d.dll" -Type: files; Name: "{app}\vcruntime140d.dll" +Type: files; Name: "{app}\*.dll" +Type: files; Name: "{app}\*.lib" +Type: files; Name: "{app}\*.exp" [Icons] Name: "{autoprograms}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}" diff --git a/windows/build.ps1 b/windows/build.ps1 new file mode 100644 index 0000000000..29c00b9705 --- /dev/null +++ b/windows/build.ps1 @@ -0,0 +1,30 @@ +# Windows release build — mirrors linux/build.sh. +# Builds the app, packages the MSIX, then compiles the Inno Setup installer. +# Outputs: +# windows\bluebubbles.msix +# windows\bluebubbles-windows.exe +$ErrorActionPreference = 'Stop' + +$flutter = if ($env:FLUTTER_CMD) { $env:FLUTTER_CMD } else { 'flutter' } +$iscc = if ($env:ISCC_PATH) { $env:ISCC_PATH } else { "${env:ProgramFiles(x86)}\Inno Setup 6\ISCC.exe" } +if (-not (Test-Path $iscc)) { throw "Inno Setup compiler not found at '$iscc'. Install Inno Setup 6 or set ISCC_PATH." } + +Set-Location (Join-Path $PSScriptRoot '..') + +# Clean the Release output first: the installer ships Release\*.dll wholesale, +# so leftovers from removed plugins would get packaged into the installer. +$releaseDir = 'build\windows\x64\runner\Release' +if (Test-Path $releaseDir) { Remove-Item $releaseDir -Recurse -Force } + +& $flutter pub get +if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + +# Runs `flutter build windows --release` and packages the result as an MSIX +& dart run msix:create +if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + +# Compile the Inno Setup installer +& $iscc 'windows\bluebubbles_installer_script.iss' +if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + +Get-FileHash 'windows\bluebubbles.msix', 'windows\bluebubbles-windows.exe' -Algorithm SHA256 | Format-List Path, Hash From 7c69485ecae39549ed175bd16fb308d26464fb57 Mon Sep 17 00:00:00 2001 From: Joel Jothiprakasam Date: Thu, 11 Jun 2026 16:21:55 -0700 Subject: [PATCH 13/40] Fix json format Signed-off-by: Joel Jothiprakasam --- .vscode/settings.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index 14a5d443a0..ff22efbff7 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -3,6 +3,6 @@ "[dart]": { "editor.rulers": [ 120 - ], + ] } } \ No newline at end of file From 706a950e044ca1a8b84fae6dae0864280ef88536 Mon Sep 17 00:00:00 2001 From: Joel Jothiprakasam Date: Thu, 11 Jun 2026 16:26:17 -0700 Subject: [PATCH 14/40] Update build scripts to set flutter version Signed-off-by: Joel Jothiprakasam --- linux/build.sh | 19 ++++++++++++++++--- windows/build.ps1 | 33 +++++++++++++++++++++++++-------- 2 files changed, 41 insertions(+), 11 deletions(-) diff --git a/linux/build.sh b/linux/build.sh index 5f036bf9be..25e0808344 100755 --- a/linux/build.sh +++ b/linux/build.sh @@ -1,12 +1,25 @@ #!/bin/bash trap "exit" INT -if [ -z ${FLUTTER_CMD+x} ]; then FLUTTER_CMD="flutter"; fi set -eux +# Flutter version to build with; override with the FLUTTER_VERSION env var. +FLUTTER_VERSION="${FLUTTER_VERSION:-3.44.2}" + cd "$(dirname "$0")/.." -"$FLUTTER_CMD" pub get -"$FLUTTER_CMD" build linux --release -v +# Switch the project to the pinned Flutter version via fvm. +# Set FLUTTER_CMD to bypass fvm and use a preinstalled Flutter instead. +if [ -z ${FLUTTER_CMD+x} ]; then + fvm use "$FLUTTER_VERSION" --force + FLUTTER_CMD="fvm flutter" +fi + +# Clean the bundle output first: the tarball packages it wholesale, so +# leftover libs from removed plugins would get shipped. +rm -rf build/linux + +$FLUTTER_CMD pub get +$FLUTTER_CMD build linux --release -v arch=$(uname -m) if [[ $arch == "x86_64" ]]; then diff --git a/windows/build.ps1 b/windows/build.ps1 index 29c00b9705..bbc4a0c419 100644 --- a/windows/build.ps1 +++ b/windows/build.ps1 @@ -1,30 +1,47 @@ -# Windows release build — mirrors linux/build.sh. +# Windows release build script. Run from the root of the repository. Requires Inno Setup 6 to be installed. # Builds the app, packages the MSIX, then compiles the Inno Setup installer. # Outputs: # windows\bluebubbles.msix # windows\bluebubbles-windows.exe $ErrorActionPreference = 'Stop' -$flutter = if ($env:FLUTTER_CMD) { $env:FLUTTER_CMD } else { 'flutter' } +# Flutter version to build with; override with the FLUTTER_VERSION env var. +$flutterVersion = if ($env:FLUTTER_VERSION) { $env:FLUTTER_VERSION } else { '3.44.2' } + $iscc = if ($env:ISCC_PATH) { $env:ISCC_PATH } else { "${env:ProgramFiles(x86)}\Inno Setup 6\ISCC.exe" } if (-not (Test-Path $iscc)) { throw "Inno Setup compiler not found at '$iscc'. Install Inno Setup 6 or set ISCC_PATH." } Set-Location (Join-Path $PSScriptRoot '..') +# Runs a command and aborts the build if it fails. +function Invoke-Checked { + param([Parameter(Mandatory)][string[]]$Command, [Parameter(ValueFromRemainingArguments)][string[]]$Rest) + & $Command[0] @($Command | Select-Object -Skip 1) @Rest + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } +} + +# Switch the project to the pinned Flutter version via fvm. +# Set FLUTTER_CMD to bypass fvm and use a preinstalled Flutter instead. +if ($env:FLUTTER_CMD) { + $flutterCmd = $env:FLUTTER_CMD -split ' ' + $dartCmd = @('dart') +} else { + Invoke-Checked @('fvm') use $flutterVersion --force + $flutterCmd = 'fvm', 'flutter' + $dartCmd = 'fvm', 'dart' +} + # Clean the Release output first: the installer ships Release\*.dll wholesale, # so leftovers from removed plugins would get packaged into the installer. $releaseDir = 'build\windows\x64\runner\Release' if (Test-Path $releaseDir) { Remove-Item $releaseDir -Recurse -Force } -& $flutter pub get -if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } +Invoke-Checked $flutterCmd pub get # Runs `flutter build windows --release` and packages the result as an MSIX -& dart run msix:create -if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } +Invoke-Checked $dartCmd run msix:create # Compile the Inno Setup installer -& $iscc 'windows\bluebubbles_installer_script.iss' -if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } +Invoke-Checked @($iscc) 'windows\bluebubbles_installer_script.iss' Get-FileHash 'windows\bluebubbles.msix', 'windows\bluebubbles-windows.exe' -Algorithm SHA256 | Format-List Path, Hash From 74fe9cc62ceccf31abcc6e346d8156486b4cab66 Mon Sep 17 00:00:00 2001 From: Joel Jothiprakasam Date: Thu, 11 Jun 2026 16:33:01 -0700 Subject: [PATCH 15/40] Don't need this anymore Signed-off-by: Joel Jothiprakasam --- lib/main.dart | 1 - pubspec.lock | 8 -------- pubspec.yaml | 1 - 3 files changed, 10 deletions(-) diff --git a/lib/main.dart b/lib/main.dart index 908a19d5fb..967642f952 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -27,7 +27,6 @@ import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart' hide Priority; import 'package:flutter/services.dart'; import 'package:flutter_acrylic/flutter_acrylic.dart'; -import 'package:flutter_dotenv/flutter_dotenv.dart'; import 'package:flutter_timezone/flutter_timezone.dart'; import 'package:get/get.dart'; import 'package:google_ml_kit/google_ml_kit.dart' hide Message; diff --git a/pubspec.lock b/pubspec.lock index bc0f7976ff..51c5bc208e 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -850,14 +850,6 @@ packages: url: "https://pub.dev" source: hosted version: "0.7.0" - flutter_dotenv: - dependency: "direct main" - description: - name: flutter_dotenv - sha256: d4130c4a43e0b13fefc593bc3961f2cb46e30cb79e253d4a526b1b5d24ae1ce4 - url: "https://pub.dev" - source: hosted - version: "6.0.0" flutter_image_compress: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index 34fd5f020d..ba7153e359 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -73,7 +73,6 @@ dependencies: flutter_acrylic: ^1.1.4 flutter_audio_waveforms: ^1.2.1+8 flutter_displaymode: ^0.7.0 # android only - flutter_dotenv: ^6.0.0 flutter_image_compress: ^2.4.0 flutter_improved_scrolling: ^0.0.3 flutter_isolate: ^2.1.0 From f710e99c68f68424c320833d0fe73fb3c200b39f Mon Sep 17 00:00:00 2001 From: Joel Jothiprakasam Date: Thu, 11 Jun 2026 16:42:08 -0700 Subject: [PATCH 16/40] github workflow Signed-off-by: Joel Jothiprakasam --- .github/workflows/desktop-builds.yml | 120 +++++++++++++++++++++++++++ 1 file changed, 120 insertions(+) create mode 100644 .github/workflows/desktop-builds.yml diff --git a/.github/workflows/desktop-builds.yml b/.github/workflows/desktop-builds.yml new file mode 100644 index 0000000000..8b44642de1 --- /dev/null +++ b/.github/workflows/desktop-builds.yml @@ -0,0 +1,120 @@ +name: Desktop Builds + +on: + push: + tags: + - "v*" + branches: + - "joel/2.0/**" # TEMP: for testing the workflow; remove before merge + workflow_dispatch: + +permissions: + contents: write # attach artifacts to releases on tag builds + +jobs: + windows: + runs-on: windows-latest + timeout-minutes: 60 + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Install fvm + run: choco install fvm -y + + # Keyed on the build script so a version bump in it invalidates the cache + - name: Cache Flutter SDK (fvm) + uses: actions/cache@v4 + with: + path: ~/fvm + key: fvm-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('windows/build.ps1') }} + + - name: Cache pub packages + uses: actions/cache@v4 + with: + path: ~/AppData/Local/Pub/Cache + key: pub-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('pubspec.lock') }} + + # Inno Setup 6 and Rust (for super_native_extensions) are preinstalled + # on the windows-latest runner image. + - name: Build + run: .\windows\build.ps1 + shell: pwsh + + - name: Upload artifacts + uses: actions/upload-artifact@v4 + with: + name: bluebubbles-windows + path: | + windows/bluebubbles.msix + windows/bluebubbles-windows.exe + + - name: Attach to release + if: startsWith(github.ref, 'refs/tags/') + uses: softprops/action-gh-release@v2 + with: + files: | + windows/bluebubbles.msix + windows/bluebubbles-windows.exe + + linux: + strategy: + fail-fast: false + matrix: + include: + - arch: x86_64 + runner: ubuntu-latest + - arch: aarch64 + runner: ubuntu-24.04-arm + runs-on: ${{ matrix.runner }} + timeout-minutes: 60 + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Install system dependencies + run: | + sudo apt-get update + sudo apt-get install -y \ + clang \ + cmake \ + ninja-build \ + pkg-config \ + libgtk-3-dev \ + libwebkit2gtk-4.1-dev \ + libmpv-dev \ + libayatana-appindicator3-dev \ + libnotify-dev + + - name: Install fvm + run: | + curl -fsSL https://fvm.app/install.sh | bash + echo "$HOME/.fvm_flutter/bin" >> "$GITHUB_PATH" + + # Keyed on the build script so a version bump in it invalidates the cache + - name: Cache Flutter SDK (fvm) + uses: actions/cache@v4 + with: + path: ~/fvm + key: fvm-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('linux/build.sh') }} + + - name: Cache pub packages + uses: actions/cache@v4 + with: + path: ~/.pub-cache + key: pub-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('pubspec.lock') }} + + - name: Build + run: bash linux/build.sh + + - name: Upload artifacts + uses: actions/upload-artifact@v4 + with: + name: bluebubbles-linux-${{ matrix.arch }} + path: bluebubbles-linux-${{ matrix.arch }}.tar.gz + + - name: Attach to release + if: startsWith(github.ref, 'refs/tags/') + uses: softprops/action-gh-release@v2 + with: + files: bluebubbles-linux-${{ matrix.arch }}.tar.gz From 8000a775c4bb439ad74d4306209d3dbe6523930b Mon Sep 17 00:00:00 2001 From: Joel Jothiprakasam Date: Thu, 11 Jun 2026 16:45:58 -0700 Subject: [PATCH 17/40] Fix linux fvm Signed-off-by: Joel Jothiprakasam --- .github/workflows/desktop-builds.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/desktop-builds.yml b/.github/workflows/desktop-builds.yml index 8b44642de1..23d8bc46ba 100644 --- a/.github/workflows/desktop-builds.yml +++ b/.github/workflows/desktop-builds.yml @@ -89,7 +89,7 @@ jobs: - name: Install fvm run: | curl -fsSL https://fvm.app/install.sh | bash - echo "$HOME/.fvm_flutter/bin" >> "$GITHUB_PATH" + echo "$HOME/fvm/bin" >> "$GITHUB_PATH" # Keyed on the build script so a version bump in it invalidates the cache - name: Cache Flutter SDK (fvm) From 8294f93887c902e16a9f6c38356023d3c77a7f08 Mon Sep 17 00:00:00 2001 From: Joel Jothiprakasam Date: Thu, 11 Jun 2026 16:55:51 -0700 Subject: [PATCH 18/40] Fix workflow versions Signed-off-by: Joel Jothiprakasam --- .github/workflows/desktop-builds.yml | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/.github/workflows/desktop-builds.yml b/.github/workflows/desktop-builds.yml index 23d8bc46ba..df604d4b6e 100644 --- a/.github/workflows/desktop-builds.yml +++ b/.github/workflows/desktop-builds.yml @@ -17,20 +17,20 @@ jobs: timeout-minutes: 60 steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Install fvm run: choco install fvm -y # Keyed on the build script so a version bump in it invalidates the cache - name: Cache Flutter SDK (fvm) - uses: actions/cache@v4 + uses: actions/cache@v5 with: path: ~/fvm key: fvm-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('windows/build.ps1') }} - name: Cache pub packages - uses: actions/cache@v4 + uses: actions/cache@v5 with: path: ~/AppData/Local/Pub/Cache key: pub-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('pubspec.lock') }} @@ -42,7 +42,7 @@ jobs: shell: pwsh - name: Upload artifacts - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: bluebubbles-windows path: | @@ -51,7 +51,7 @@ jobs: - name: Attach to release if: startsWith(github.ref, 'refs/tags/') - uses: softprops/action-gh-release@v2 + uses: softprops/action-gh-release@v3 with: files: | windows/bluebubbles.msix @@ -70,7 +70,7 @@ jobs: timeout-minutes: 60 steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Install system dependencies run: | @@ -93,13 +93,13 @@ jobs: # Keyed on the build script so a version bump in it invalidates the cache - name: Cache Flutter SDK (fvm) - uses: actions/cache@v4 + uses: actions/cache@v5 with: path: ~/fvm key: fvm-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('linux/build.sh') }} - name: Cache pub packages - uses: actions/cache@v4 + uses: actions/cache@v5 with: path: ~/.pub-cache key: pub-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('pubspec.lock') }} @@ -108,13 +108,13 @@ jobs: run: bash linux/build.sh - name: Upload artifacts - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: bluebubbles-linux-${{ matrix.arch }} path: bluebubbles-linux-${{ matrix.arch }}.tar.gz - name: Attach to release if: startsWith(github.ref, 'refs/tags/') - uses: softprops/action-gh-release@v2 + uses: softprops/action-gh-release@v3 with: files: bluebubbles-linux-${{ matrix.arch }}.tar.gz From 8100a64be3faddbc2ac2681912cf2775d9f2b7bd Mon Sep 17 00:00:00 2001 From: Joel Jothiprakasam Date: Thu, 11 Jun 2026 17:03:09 -0700 Subject: [PATCH 19/40] Bump versions Signed-off-by: Joel Jothiprakasam --- flatpak/app.bluebubbles.BlueBubbles.metainfo.xml | 1 + linux/build.sh | 2 +- pubspec.yaml | 2 +- snap/snapcraft.yaml | 6 +++--- windows/bluebubbles_installer_script.iss | 2 +- windows/runner/Runner.rc | 4 ++-- 6 files changed, 9 insertions(+), 8 deletions(-) diff --git a/flatpak/app.bluebubbles.BlueBubbles.metainfo.xml b/flatpak/app.bluebubbles.BlueBubbles.metainfo.xml index 0574d929a8..91b7a34b70 100644 --- a/flatpak/app.bluebubbles.BlueBubbles.metainfo.xml +++ b/flatpak/app.bluebubbles.BlueBubbles.metainfo.xml @@ -66,6 +66,7 @@ + diff --git a/linux/build.sh b/linux/build.sh index 25e0808344..1a32f8b44c 100755 --- a/linux/build.sh +++ b/linux/build.sh @@ -31,7 +31,7 @@ fi # Inject version number into version.json tmp=$(mktemp) chmod 644 "$tmp" -jq '.version = "1.15.101.0"' build/linux/$folder/release/bundle/data/flutter_assets/version.json > "$tmp" && mv "$tmp" build/linux/$folder/release/bundle/data/flutter_assets/version.json +jq '.version = "1.15.102.0"' build/linux/$folder/release/bundle/data/flutter_assets/version.json > "$tmp" && mv "$tmp" build/linux/$folder/release/bundle/data/flutter_assets/version.json chmod +x build/linux/$folder/release/bundle/bluebubbles tar czvf bluebubbles-linux-"$arch".tar.gz -C build/linux/$folder/release/bundle . diff --git a/pubspec.yaml b/pubspec.yaml index ba7153e359..87035414a4 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -328,7 +328,7 @@ msix_config: display_name: BlueBubbles publisher_display_name: BlueBubbles identity_name: 23344BlueBubbles.BlueBubbles - msix_version: 1.15.101.0 + msix_version: 1.15.102.0 publisher: CN=BEC9154D-191E-4375-BF30-698BD4C141C4 vs_generated_images_folder_path: windows/icons logo_path: assets/icon/icon.ico diff --git a/snap/snapcraft.yaml b/snap/snapcraft.yaml index 5fc53cf447..453ee169f2 100644 --- a/snap/snapcraft.yaml +++ b/snap/snapcraft.yaml @@ -1,6 +1,6 @@ name: bluebubbles title: BlueBubbles -version: 1.15.101.0 +version: 1.15.102.0 summary: BlueBubbles client for Linux description: BlueBubbles is an open-source and cross-platform ecosystem of apps aimed to bring iMessage to Android, Windows, Linux, and more! With BlueBubbles, you'll be able to send messages, media, and much more to your friends and family. license: Apache-2.0 @@ -117,8 +117,8 @@ parts: - alsa-mixin - fmedia source: - - on amd64: https://github.com/BlueBubblesApp/bluebubbles-app/releases/download/v2.0.0%2B86-desktop-b.2/bluebubbles-linux-x86_64.tar.gz - - on arm64: https://github.com/BlueBubblesApp/bluebubbles-app/releases/download/v2.0.0%2B86-desktop-b.2/bluebubbles-linux-aarch64.tar.gz + - on amd64: https://github.com/BlueBubblesApp/bluebubbles-app/releases/download/v2.0.0%2B87-desktop-b.3/bluebubbles-linux-x86_64.tar.gz + - on arm64: https://github.com/BlueBubblesApp/bluebubbles-app/releases/download/v2.0.0%2B87-desktop-b.3/bluebubbles-linux-aarch64.tar.gz plugin: nil override-build: | set -eux diff --git a/windows/bluebubbles_installer_script.iss b/windows/bluebubbles_installer_script.iss index a69469283d..2bc7cec885 100644 --- a/windows/bluebubbles_installer_script.iss +++ b/windows/bluebubbles_installer_script.iss @@ -2,7 +2,7 @@ ; SEE THE DOCUMENTATION FOR DETAILS ON CREATING INNO SETUP SCRIPT FILES! #define MyAppName "BlueBubbles" -#define MyAppVersion "1.15.101.0" +#define MyAppVersion "1.15.102.0" #define MyAppPublisher "BlueBubbles" #define MyAppURL "https://bluebubbles.app/" #define MyAppExeName "bluebubbles_app.exe" diff --git a/windows/runner/Runner.rc b/windows/runner/Runner.rc index b81094739f..289ac45e2f 100644 --- a/windows/runner/Runner.rc +++ b/windows/runner/Runner.rc @@ -59,8 +59,8 @@ IDI_APP_ICON ICON "resources\\app_icon.ico" // // Version // -#define VERSION_AS_NUMBER 1,15,101,0 -#define VERSION_AS_STRING "1.15.101.0" +#define VERSION_AS_NUMBER 1,15,102,0 +#define VERSION_AS_STRING "1.15.102.0" VS_VERSION_INFO VERSIONINFO FILEVERSION VERSION_AS_NUMBER From ffa3e372bc8458bbce180605c97e3b40989faf08 Mon Sep 17 00:00:00 2001 From: Joel Jothiprakasam Date: Thu, 11 Jun 2026 17:11:39 -0700 Subject: [PATCH 20/40] Remove testing Signed-off-by: Joel Jothiprakasam --- .github/workflows/desktop-builds.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/desktop-builds.yml b/.github/workflows/desktop-builds.yml index df604d4b6e..74da9a96f0 100644 --- a/.github/workflows/desktop-builds.yml +++ b/.github/workflows/desktop-builds.yml @@ -4,8 +4,6 @@ on: push: tags: - "v*" - branches: - - "joel/2.0/**" # TEMP: for testing the workflow; remove before merge workflow_dispatch: permissions: From 1ee6ba92bfc31d3005390817ecd7c3a88c93f604 Mon Sep 17 00:00:00 2001 From: Joel Jothiprakasam Date: Thu, 11 Jun 2026 18:22:57 -0700 Subject: [PATCH 21/40] Change windows installer name. Don't attach msix to release Signed-off-by: Joel Jothiprakasam --- .github/workflows/desktop-builds.yml | 8 ++++---- windows/.gitignore | 2 +- windows/bluebubbles_installer_script.iss | 2 +- windows/build.ps1 | 6 +++--- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/.github/workflows/desktop-builds.yml b/.github/workflows/desktop-builds.yml index 74da9a96f0..f9c8de6d58 100644 --- a/.github/workflows/desktop-builds.yml +++ b/.github/workflows/desktop-builds.yml @@ -39,21 +39,21 @@ jobs: run: .\windows\build.ps1 shell: pwsh + # The msix is for internal use only — uploaded as a run artifact, + # never attached to releases. - name: Upload artifacts uses: actions/upload-artifact@v7 with: name: bluebubbles-windows path: | windows/bluebubbles.msix - windows/bluebubbles-windows.exe + windows/bluebubbles_installer.exe - name: Attach to release if: startsWith(github.ref, 'refs/tags/') uses: softprops/action-gh-release@v3 with: - files: | - windows/bluebubbles.msix - windows/bluebubbles-windows.exe + files: windows/bluebubbles_installer.exe linux: strategy: diff --git a/windows/.gitignore b/windows/.gitignore index 3c7eda6ecb..2b075dda84 100644 --- a/windows/.gitignore +++ b/windows/.gitignore @@ -20,5 +20,5 @@ x86/ cmake-build-debug # Generated installers -bluebubbles-windows.exe +bluebubbles_installer.exe *.msix \ No newline at end of file diff --git a/windows/bluebubbles_installer_script.iss b/windows/bluebubbles_installer_script.iss index 2bc7cec885..05c6641754 100644 --- a/windows/bluebubbles_installer_script.iss +++ b/windows/bluebubbles_installer_script.iss @@ -27,7 +27,7 @@ DisableProgramGroupPage=yes ;PrivilegesRequired=lowest PrivilegesRequiredOverridesAllowed=dialog OutputDir=. -OutputBaseFilename=bluebubbles-windows +OutputBaseFilename=bluebubbles_installer SetupIconFile={#ProjectRoot}\assets\icon\icon.ico Compression=lzma SolidCompression=yes diff --git a/windows/build.ps1 b/windows/build.ps1 index bbc4a0c419..21a0ba6057 100644 --- a/windows/build.ps1 +++ b/windows/build.ps1 @@ -1,8 +1,8 @@ # Windows release build script. Run from the root of the repository. Requires Inno Setup 6 to be installed. # Builds the app, packages the MSIX, then compiles the Inno Setup installer. # Outputs: -# windows\bluebubbles.msix -# windows\bluebubbles-windows.exe +# windows\bluebubbles.msix (internal use only — not attached to releases) +# windows\bluebubbles_installer.exe $ErrorActionPreference = 'Stop' # Flutter version to build with; override with the FLUTTER_VERSION env var. @@ -44,4 +44,4 @@ Invoke-Checked $dartCmd run msix:create # Compile the Inno Setup installer Invoke-Checked @($iscc) 'windows\bluebubbles_installer_script.iss' -Get-FileHash 'windows\bluebubbles.msix', 'windows\bluebubbles-windows.exe' -Algorithm SHA256 | Format-List Path, Hash +Get-FileHash 'windows\bluebubbles.msix', 'windows\bluebubbles_installer.exe' -Algorithm SHA256 | Format-List Path, Hash From 8ef6eb06d8b3e6ce5ee31c97706d34cb7346ed63 Mon Sep 17 00:00:00 2001 From: Joel Jothiprakasam Date: Thu, 11 Jun 2026 18:30:37 -0700 Subject: [PATCH 22/40] Split windows artifacts so msix download is easier Signed-off-by: Joel Jothiprakasam --- .github/workflows/desktop-builds.yml | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/.github/workflows/desktop-builds.yml b/.github/workflows/desktop-builds.yml index f9c8de6d58..1858941a84 100644 --- a/.github/workflows/desktop-builds.yml +++ b/.github/workflows/desktop-builds.yml @@ -39,15 +39,19 @@ jobs: run: .\windows\build.ps1 shell: pwsh - # The msix is for internal use only — uploaded as a run artifact, - # never attached to releases. - - name: Upload artifacts + - name: Upload installer artifact + uses: actions/upload-artifact@v7 + with: + name: bluebubbles-installer + path: windows/bluebubbles_installer.exe + + # The msix is for the MS Store only — uploaded as its own run artifact + # for easy download, never attached to releases. + - name: Upload msix artifact uses: actions/upload-artifact@v7 with: - name: bluebubbles-windows - path: | - windows/bluebubbles.msix - windows/bluebubbles_installer.exe + name: bluebubbles-msix + path: windows/bluebubbles.msix - name: Attach to release if: startsWith(github.ref, 'refs/tags/') From fcabcc3dc7ae5007698607cdd5db92a12fdfd1a9 Mon Sep 17 00:00:00 2001 From: Joel Jothiprakasam Date: Thu, 11 Jun 2026 19:19:48 -0700 Subject: [PATCH 23/40] enforce pub lockfile for CI Signed-off-by: Joel Jothiprakasam --- linux/build.sh | 2 +- windows/build.ps1 | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/linux/build.sh b/linux/build.sh index 1a32f8b44c..33980ab69b 100755 --- a/linux/build.sh +++ b/linux/build.sh @@ -18,7 +18,7 @@ fi # leftover libs from removed plugins would get shipped. rm -rf build/linux -$FLUTTER_CMD pub get +$FLUTTER_CMD pub get --enforce-lockfile $FLUTTER_CMD build linux --release -v arch=$(uname -m) diff --git a/windows/build.ps1 b/windows/build.ps1 index 21a0ba6057..aa6a8e109f 100644 --- a/windows/build.ps1 +++ b/windows/build.ps1 @@ -36,7 +36,7 @@ if ($env:FLUTTER_CMD) { $releaseDir = 'build\windows\x64\runner\Release' if (Test-Path $releaseDir) { Remove-Item $releaseDir -Recurse -Force } -Invoke-Checked $flutterCmd pub get +Invoke-Checked $flutterCmd pub get --enforce-lockfile # Runs `flutter build windows --release` and packages the result as an MSIX Invoke-Checked $dartCmd run msix:create From 36641dfa4770da3f32548717354e72545a116e36 Mon Sep 17 00:00:00 2001 From: Joel Jothiprakasam Date: Thu, 11 Jun 2026 21:38:34 -0700 Subject: [PATCH 24/40] support vs2026 Signed-off-by: Joel Jothiprakasam --- windows/CMakeLists.txt | 6 + windows/CodeDependencies.iss | 600 ++++++++++++++++++++++++----------- 2 files changed, 413 insertions(+), 193 deletions(-) diff --git a/windows/CMakeLists.txt b/windows/CMakeLists.txt index 1bd8cd03c5..6506873ea5 100644 --- a/windows/CMakeLists.txt +++ b/windows/CMakeLists.txt @@ -29,6 +29,12 @@ set(CMAKE_CXX_FLAGS_PROFILE "${CMAKE_CXX_FLAGS_RELEASE}") # Use Unicode for all projects. add_definitions(-DUNICODE -D_UNICODE) +# VS2026 (MSVC 14.50+) turns the deprecation into a +# hard error (STL1011). Several plugins (local_auth_windows, +# permission_handler_windows, flutter_local_notifications_windows) still +# include it, so silence the error until they migrate to C++20 . +add_definitions(-D_SILENCE_EXPERIMENTAL_COROUTINE_DEPRECATION_WARNINGS) + # Compilation settings that should be applied to most targets. function(APPLY_STANDARD_SETTINGS TARGET) target_compile_features(${TARGET} PUBLIC cxx_std_17) diff --git a/windows/CodeDependencies.iss b/windows/CodeDependencies.iss index 830368f6dd..c46b2b9677 100644 --- a/windows/CodeDependencies.iss +++ b/windows/CodeDependencies.iss @@ -11,34 +11,41 @@ type Checksum: String; ForceSuccess: Boolean; RestartAfter: Boolean; + Components: String; end; var - Dependency_Memo: String; Dependency_List: array of TDependency_Entry; - Dependency_NeedToRestart, Dependency_ForceX86: Boolean; + Dependency_NeedToRestart, Dependency_ForceX86, Dependency_ForceX64: Boolean; + Dependency_Components: String; Dependency_DownloadPage: TDownloadWizardPage; +function Dependency_IsEntryActive(const Entry: TDependency_Entry): Boolean; +begin + Result := (Entry.Components = '') or WizardIsComponentSelected(Entry.Components); +end; + procedure Dependency_Add(const Filename, Parameters, Title, URL, Checksum: String; const ForceSuccess, RestartAfter: Boolean); var Dependency: TDependency_Entry; DependencyCount: Integer; begin - Dependency_Memo := Dependency_Memo + #13#10 + '%1' + Title; - Dependency.Filename := Filename; Dependency.Parameters := Parameters; Dependency.Title := Title; if FileExists(ExpandConstant('{tmp}{\}') + Filename) then begin Dependency.URL := ''; + Log('Dependency queued (already in tmp): ' + Title); end else begin Dependency.URL := URL; + Log('Dependency queued for download: ' + Title); end; Dependency.Checksum := Checksum; Dependency.ForceSuccess := ForceSuccess; Dependency.RestartAfter := RestartAfter; + Dependency.Components := Dependency_Components; DependencyCount := GetArrayLength(Dependency_List); SetArrayLength(Dependency_List, DependencyCount + 1); @@ -54,7 +61,7 @@ end; function Dependency_PrepareToInstall(var NeedsRestart: Boolean): String; var - DependencyCount, DependencyIndex, ResultCode: Integer; + DependencyCount, DependencyIndex, ActiveCount, ActiveIndex, ResultCode: Integer; Retry: Boolean; TempValue: String; begin @@ -64,6 +71,9 @@ begin Dependency_DownloadPage.Show; for DependencyIndex := 0 to DependencyCount - 1 do begin + if not Dependency_IsEntryActive(Dependency_List[DependencyIndex]) then begin + continue; + end; if Dependency_List[DependencyIndex].URL <> '' then begin Dependency_DownloadPage.Clear; Dependency_DownloadPage.Add(Dependency_List[DependencyIndex].URL, Dependency_List[DependencyIndex].Filename, Dependency_List[DependencyIndex].Checksum); @@ -76,6 +86,7 @@ begin Dependency_DownloadPage.Download; except if Dependency_DownloadPage.AbortedByUser then begin + Log('Download aborted by user: ' + Dependency_List[DependencyIndex].Title); Result := Dependency_List[DependencyIndex].Title; DependencyIndex := DependencyCount; end else begin @@ -95,9 +106,22 @@ begin end; if Result = '' then begin + ActiveCount := 0; + for DependencyIndex := 0 to DependencyCount - 1 do begin + if Dependency_IsEntryActive(Dependency_List[DependencyIndex]) then begin + ActiveCount := ActiveCount + 1; + end; + end; + + ActiveIndex := 0; for DependencyIndex := 0 to DependencyCount - 1 do begin + if not Dependency_IsEntryActive(Dependency_List[DependencyIndex]) then begin + Log('Dependency skipped (component not selected): ' + Dependency_List[DependencyIndex].Title); + continue; + end; + ActiveIndex := ActiveIndex + 1; Dependency_DownloadPage.SetText(Dependency_List[DependencyIndex].Title, ''); - Dependency_DownloadPage.SetProgress(DependencyIndex + 1, DependencyCount + 1); + Dependency_DownloadPage.SetProgress(ActiveIndex, ActiveCount + 1); while True do begin ResultCode := 0; @@ -106,6 +130,7 @@ begin #else if ShellExec('', ExpandConstant('{tmp}{\}') + Dependency_List[DependencyIndex].Filename, Dependency_List[DependencyIndex].Parameters, '', SW_SHOWNORMAL, ewWaitUntilTerminated, ResultCode) then begin #endif + Log('Dependency exit code ' + IntToStr(ResultCode) + ': ' + Dependency_List[DependencyIndex].Title); if Dependency_List[DependencyIndex].RestartAfter then begin if DependencyIndex = DependencyCount - 1 then begin Dependency_NeedToRestart := True; @@ -123,6 +148,8 @@ begin end else if ResultCode = 3010 then begin // ERROR_SUCCESS_REBOOT_REQUIRED (3010) Dependency_NeedToRestart := True; break; + end else if ResultCode = 1638 then begin // ERROR_PRODUCT_VERSION (1638) + break; end; end; @@ -143,6 +170,7 @@ begin end; if NeedsRestart then begin + Log('Dependency requires restart: registering RunOnce to resume setup'); TempValue := '"' + ExpandConstant('{srcexe}') + '" /restart=1 /LANG="' + ExpandConstant('{language}') + '" /DIR="' + WizardDirValue + '" /GROUP="' + WizardGroupValue + '" /TYPE="' + WizardSetupType(False) + '" /COMPONENTS="' + WizardSelectedComponents(False) + '" /TASKS="' + WizardSelectedTasks(False) + '"'; if WizardNoIcons then begin TempValue := TempValue + ' /NOICONS'; @@ -159,6 +187,9 @@ end; #endif function Dependency_UpdateReadyMemo(const Space, NewLine, MemoUserInfoInfo, MemoDirInfo, MemoTypeInfo, MemoComponentsInfo, MemoGroupInfo, MemoTasksInfo: String): String; +var + DependencyIndex: Integer; + DependencyMemo: String; begin Result := ''; if MemoUserInfoInfo <> '' then begin @@ -180,11 +211,18 @@ begin Result := Result + MemoTasksInfo; end; - if Dependency_Memo <> '' then begin + DependencyMemo := ''; + for DependencyIndex := 0 to GetArrayLength(Dependency_List) - 1 do begin + if Dependency_IsEntryActive(Dependency_List[DependencyIndex]) then begin + DependencyMemo := DependencyMemo + #13#10 + '%1' + Dependency_List[DependencyIndex].Title; + end; + end; + + if DependencyMemo <> '' then begin if MemoTasksInfo = '' then begin Result := Result + SetupMessage(msgReadyMemoTasks); end; - Result := Result + FmtMessage(Dependency_Memo, [Space]); + Result := Result + FmtMessage(DependencyMemo, [Space]); end; end; @@ -194,14 +232,21 @@ begin Result := Dependency_NeedToRestart; end; +function Dependency_IsArm64: Boolean; +begin + Result := not Dependency_ForceX86 and not Dependency_ForceX64 and IsArm64; +end; + function Dependency_IsX64: Boolean; begin - Result := not Dependency_ForceX86 and Is64BitInstallMode; + Result := not Dependency_ForceX86 and (Is64BitInstallMode or (Dependency_ForceX64 and IsX64Compatible)); end; -function Dependency_String(const x86, x64: String): String; +function Dependency_String(const x86, x64, arm64: String): String; begin - if Dependency_IsX64 then begin + if Dependency_IsArm64 then begin + Result := arm64; + end else if Dependency_IsX64 then begin Result := x64; end else begin Result := x86; @@ -210,38 +255,66 @@ end; function Dependency_ArchSuffix: String; begin - Result := Dependency_String('', '_x64'); + Result := Dependency_String('', '_x64', '_arm64'); end; function Dependency_ArchTitle: String; begin - Result := Dependency_String(' (x86)', ' (x64)'); + Result := Dependency_String(' (x86)', ' (x64)', ' (arm64)'); end; -function Dependency_IsNetCoreInstalled(Runtime: String; Major, Minor, Revision: Word): Boolean; +function Dependency_PassiveOrQuiet(const Passive, Quiet: String): String; +begin + if WizardSilent then begin + Result := Quiet; + end else begin + Result := Passive; + end; +end; + +var + Dependency_NetCoreRuntimesArch: String; + Dependency_NetCoreRuntimes: TArrayOfString; + +procedure Dependency_ListNetCoreRuntimes; var - Path: String; + Arch, Path: String; ResultCode: Integer; Output: TExecOutput; +begin + Arch := Dependency_String('x86', 'x64', 'arm64'); + if Dependency_NetCoreRuntimesArch = Arch then begin + exit; + end; + Dependency_NetCoreRuntimesArch := Arch; + SetArrayLength(Dependency_NetCoreRuntimes, 0); + + if not RegQueryStringValue(HKLM32, 'SOFTWARE\dotnet\Setup\InstalledVersions\' + Arch, 'InstallLocation', Path) or not FileExists(Path + 'dotnet.exe') then begin + Path := ExpandConstant(Dependency_String('{commonpf32}', '{commonpf64}', '{commonpf64}')) + '\dotnet\'; + end; + if ExecAndCaptureOutput(Path + 'dotnet.exe', '--list-runtimes', '', SW_HIDE, ewWaitUntilTerminated, ResultCode, Output) and (ResultCode = 0) then begin + Dependency_NetCoreRuntimes := Output.StdOut; + end; +end; + +function Dependency_IsNetCoreInstalled(Runtime: String; Major, Minor, Revision: Word): Boolean; +var LineIndex: Integer; LineParts: TArrayOfString; PackedVersion: Int64; LineMajor, LineMinor, LineRevision, LineBuild: Word; begin - if not RegQueryStringValue(HKLM32, 'SOFTWARE\dotnet\Setup\InstalledVersions\x' + Dependency_String('86', '64'), 'InstallLocation', Path) or not FileExists(Path + 'dotnet.exe') then begin - Path := ExpandConstant(Dependency_String('{commonpf32}', '{commonpf64}')) + '\dotnet\'; - end; - if ExecAndCaptureOutput(Path + 'dotnet.exe', '--list-runtimes', '', SW_HIDE, ewWaitUntilTerminated, ResultCode, Output) and (ResultCode = 0) then begin - for LineIndex := 0 to Length(Output.StdOut) - 1 do begin - LineParts := StringSplit(Trim(Output.StdOut[LineIndex]), [' '], stExcludeEmpty); + Dependency_ListNetCoreRuntimes; - if (Length(LineParts) > 1) and (Lowercase(LineParts[0]) = Lowercase(Runtime)) and StrToVersion(LineParts[1], PackedVersion) then begin - UnpackVersionComponents(PackedVersion, LineMajor, LineMinor, LineRevision, LineBuild); + for LineIndex := 0 to Length(Dependency_NetCoreRuntimes) - 1 do begin + LineParts := StringSplit(Trim(Dependency_NetCoreRuntimes[LineIndex]), [' '], stExcludeEmpty); - if (LineMajor = Major) and (LineMinor = Minor) and (LineRevision >= Revision) then begin - Result := True; - exit; - end; + if (Length(LineParts) > 1) and (Lowercase(LineParts[0]) = Lowercase(Runtime)) and StrToVersion(LineParts[1], PackedVersion) then begin + UnpackVersionComponents(PackedVersion, LineMajor, LineMinor, LineRevision, LineBuild); + + if (LineMajor = Major) and (LineMinor = Minor) and (LineRevision >= Revision) then begin + Result := True; + exit; end; end; end; @@ -253,7 +326,7 @@ begin // https://dotnet.microsoft.com/download/dotnet-framework/net35-sp1 if not IsDotNetInstalled(net35, 1) then begin Dependency_Add('dotnetfx35.exe', - '/lang:enu /passive /norestart', + '/lang:enu ' + Dependency_PassiveOrQuiet('/passive', '/q') + ' /norestart', '.NET Framework 3.5 Service Pack 1', 'https://download.microsoft.com/download/2/0/E/20E90413-712F-438C-988E-FDAA79A8AC3D/dotnetfx35.exe', '', False, False); @@ -265,7 +338,7 @@ begin // https://dotnet.microsoft.com/download/dotnet-framework/net40 if not IsDotNetInstalled(net4full, 0) then begin Dependency_Add('dotNetFx40_Full_setup.exe', - '/lcid ' + IntToStr(GetUILanguage) + ' /passive /norestart', + '/lcid ' + IntToStr(GetUILanguage) + ' ' + Dependency_PassiveOrQuiet('/passive', '/q') + ' /norestart', '.NET Framework 4.0', 'https://download.microsoft.com/download/1/B/E/1BE39E79-7E39-46A3-96FF-047F95396215/dotNetFx40_Full_setup.exe', '', False, False); @@ -277,7 +350,7 @@ begin // https://dotnet.microsoft.com/download/dotnet-framework/net452 if not IsDotNetInstalled(net452, 0) then begin Dependency_Add('dotnetfx45.exe', - '/lcid ' + IntToStr(GetUILanguage) + ' /passive /norestart', + '/lcid ' + IntToStr(GetUILanguage) + ' ' + Dependency_PassiveOrQuiet('/passive', '/q') + ' /norestart', '.NET Framework 4.5.2', 'https://go.microsoft.com/fwlink/?LinkId=397707', '', False, False); @@ -289,7 +362,7 @@ begin // https://dotnet.microsoft.com/download/dotnet-framework/net462 if not IsDotNetInstalled(net462, 0) then begin Dependency_Add('dotnetfx46.exe', - '/lcid ' + IntToStr(GetUILanguage) + ' /passive /norestart', + '/lcid ' + IntToStr(GetUILanguage) + ' ' + Dependency_PassiveOrQuiet('/passive', '/q') + ' /norestart', '.NET Framework 4.6.2', 'https://go.microsoft.com/fwlink/?linkid=780596', '', False, False); @@ -301,7 +374,7 @@ begin // https://dotnet.microsoft.com/download/dotnet-framework/net472 if not IsDotNetInstalled(net472, 0) then begin Dependency_Add('dotnetfx47.exe', - '/lcid ' + IntToStr(GetUILanguage) + ' /passive /norestart', + '/lcid ' + IntToStr(GetUILanguage) + ' ' + Dependency_PassiveOrQuiet('/passive', '/q') + ' /norestart', '.NET Framework 4.7.2', 'https://go.microsoft.com/fwlink/?LinkId=863262', '', False, False); @@ -313,7 +386,7 @@ begin // https://dotnet.microsoft.com/download/dotnet-framework/net48 if not IsDotNetInstalled(net48, 0) then begin Dependency_Add('dotnetfx48.exe', - '/lcid ' + IntToStr(GetUILanguage) + ' /passive /norestart', + '/lcid ' + IntToStr(GetUILanguage) + ' ' + Dependency_PassiveOrQuiet('/passive', '/q') + ' /norestart', '.NET Framework 4.8', 'https://go.microsoft.com/fwlink/?LinkId=2085155', '', False, False); @@ -325,46 +398,41 @@ begin // https://dotnet.microsoft.com/download/dotnet-framework/net481 if not IsDotNetInstalled(net481, 0) then begin Dependency_Add('dotnetfx481.exe', - '/lcid ' + IntToStr(GetUILanguage) + ' /passive /norestart', + '/lcid ' + IntToStr(GetUILanguage) + ' ' + Dependency_PassiveOrQuiet('/passive', '/q') + ' /norestart', '.NET Framework 4.8.1', 'https://go.microsoft.com/fwlink/?LinkId=2203304', '', False, False); end; end; +procedure Dependency_AddNetCore(const Prefix, Title, URL: String); +begin + Dependency_Add(Prefix + Dependency_ArchSuffix + '.exe', + '/lcid ' + IntToStr(GetUILanguage) + ' ' + Dependency_PassiveOrQuiet('/passive', '/quiet') + ' /norestart', + Title + Dependency_ArchTitle, + URL, + '', False, False); +end; + procedure Dependency_AddNetCore31; begin // https://dotnet.microsoft.com/download/dotnet-core/3.1 if not Dependency_IsNetCoreInstalled('Microsoft.NETCore.App', 3, 1, 32) then begin - Dependency_Add('netcore31' + Dependency_ArchSuffix + '.exe', - '/lcid ' + IntToStr(GetUILanguage) + ' /passive /norestart', - '.NET Core Runtime 3.1.32' + Dependency_ArchTitle, - Dependency_String('https://download.visualstudio.microsoft.com/download/pr/de4b3438-24a2-4d1d-a845-97355cf97b71/515abb880478b49f7c1bced8fbf07b16/dotnet-runtime-3.1.32-win-x86.exe', 'https://download.visualstudio.microsoft.com/download/pr/476eba79-f17f-49c8-a213-0f24a22cd026/37c02de81ff5b76ac57a5427462395f1/dotnet-runtime-3.1.32-win-x64.exe'), - '', False, False); + Dependency_AddNetCore('netcore31', '.NET Core Runtime 3.1.32', Dependency_String('https://download.visualstudio.microsoft.com/download/pr/de4b3438-24a2-4d1d-a845-97355cf97b71/515abb880478b49f7c1bced8fbf07b16/dotnet-runtime-3.1.32-win-x86.exe', 'https://download.visualstudio.microsoft.com/download/pr/476eba79-f17f-49c8-a213-0f24a22cd026/37c02de81ff5b76ac57a5427462395f1/dotnet-runtime-3.1.32-win-x64.exe', 'https://download.visualstudio.microsoft.com/download/pr/476eba79-f17f-49c8-a213-0f24a22cd026/37c02de81ff5b76ac57a5427462395f1/dotnet-runtime-3.1.32-win-x64.exe')); end; end; procedure Dependency_AddNetCore31Asp; begin - // https://dotnet.microsoft.com/download/dotnet-core/3.1 if not Dependency_IsNetCoreInstalled('Microsoft.AspNetCore.App', 3, 1, 32) then begin - Dependency_Add('netcore31asp' + Dependency_ArchSuffix + '.exe', - '/lcid ' + IntToStr(GetUILanguage) + ' /passive /norestart', - 'ASP.NET Core Runtime 3.1.32' + Dependency_ArchTitle, - Dependency_String('https://download.visualstudio.microsoft.com/download/pr/63b482d2-04b2-4dd4-baaf-d1e78de80738/40321091c872f4e77337b68fc61a5a07/aspnetcore-runtime-3.1.32-win-x86.exe', 'https://download.visualstudio.microsoft.com/download/pr/98910750-2644-472c-ab2b-17f315ccb953/c2a4c223ee11e2eec7d13744e7a45547/aspnetcore-runtime-3.1.32-win-x64.exe'), - '', False, False); + Dependency_AddNetCore('netcore31asp', 'ASP.NET Core Runtime 3.1.32', Dependency_String('https://download.visualstudio.microsoft.com/download/pr/63b482d2-04b2-4dd4-baaf-d1e78de80738/40321091c872f4e77337b68fc61a5a07/aspnetcore-runtime-3.1.32-win-x86.exe', 'https://download.visualstudio.microsoft.com/download/pr/98910750-2644-472c-ab2b-17f315ccb953/c2a4c223ee11e2eec7d13744e7a45547/aspnetcore-runtime-3.1.32-win-x64.exe', 'https://download.visualstudio.microsoft.com/download/pr/98910750-2644-472c-ab2b-17f315ccb953/c2a4c223ee11e2eec7d13744e7a45547/aspnetcore-runtime-3.1.32-win-x64.exe')); end; end; procedure Dependency_AddNetCore31Desktop; begin - // https://dotnet.microsoft.com/download/dotnet-core/3.1 if not Dependency_IsNetCoreInstalled('Microsoft.WindowsDesktop.App', 3, 1, 32) then begin - Dependency_Add('netcore31desktop' + Dependency_ArchSuffix + '.exe', - '/lcid ' + IntToStr(GetUILanguage) + ' /passive /norestart', - '.NET Desktop Runtime 3.1.32' + Dependency_ArchTitle, - Dependency_String('https://download.visualstudio.microsoft.com/download/pr/3f353d2c-0431-48c5-bdf6-fbbe8f901bb5/542a4af07c1df5136a98a1c2df6f3d62/windowsdesktop-runtime-3.1.32-win-x86.exe', 'https://download.visualstudio.microsoft.com/download/pr/b92958c6-ae36-4efa-aafe-569fced953a5/1654639ef3b20eb576174c1cc200f33a/windowsdesktop-runtime-3.1.32-win-x64.exe'), - '', False, False); + Dependency_AddNetCore('netcore31desktop', '.NET Desktop Runtime 3.1.32', Dependency_String('https://download.visualstudio.microsoft.com/download/pr/3f353d2c-0431-48c5-bdf6-fbbe8f901bb5/542a4af07c1df5136a98a1c2df6f3d62/windowsdesktop-runtime-3.1.32-win-x86.exe', 'https://download.visualstudio.microsoft.com/download/pr/b92958c6-ae36-4efa-aafe-569fced953a5/1654639ef3b20eb576174c1cc200f33a/windowsdesktop-runtime-3.1.32-win-x64.exe', 'https://download.visualstudio.microsoft.com/download/pr/b92958c6-ae36-4efa-aafe-569fced953a5/1654639ef3b20eb576174c1cc200f33a/windowsdesktop-runtime-3.1.32-win-x64.exe')); end; end; @@ -372,35 +440,21 @@ procedure Dependency_AddDotNet50; begin // https://dotnet.microsoft.com/download/dotnet/5.0 if not Dependency_IsNetCoreInstalled('Microsoft.NETCore.App', 5, 0, 17) then begin - Dependency_Add('dotnet50' + Dependency_ArchSuffix + '.exe', - '/lcid ' + IntToStr(GetUILanguage) + ' /passive /norestart', - '.NET Runtime 5.0.17' + Dependency_ArchTitle, - Dependency_String('https://download.visualstudio.microsoft.com/download/pr/54683c13-6b04-4d7d-b4d4-1f055b50ea43/e99048e2840d57040e8312058853a5b9/dotnet-runtime-5.0.17-win-x86.exe', 'https://download.visualstudio.microsoft.com/download/pr/a0832b5a-6900-442b-af79-6ffddddd6ba4/e2df0b25dd851ee0b38a86947dd0e42e/dotnet-runtime-5.0.17-win-x64.exe'), - '', False, False); + Dependency_AddNetCore('dotnet50', '.NET Runtime 5.0.17', Dependency_String('https://aka.ms/dotnet/5.0/dotnet-runtime-win-x86.exe', 'https://aka.ms/dotnet/5.0/dotnet-runtime-win-x64.exe', 'https://aka.ms/dotnet/5.0/dotnet-runtime-win-arm64.exe')); end; end; procedure Dependency_AddDotNet50Asp; begin - // https://dotnet.microsoft.com/download/dotnet/5.0 if not Dependency_IsNetCoreInstalled('Microsoft.AspNetCore.App', 5, 0, 17) then begin - Dependency_Add('dotnet50asp' + Dependency_ArchSuffix + '.exe', - '/lcid ' + IntToStr(GetUILanguage) + ' /passive /norestart', - 'ASP.NET Core Runtime 5.0.17' + Dependency_ArchTitle, - Dependency_String('https://download.visualstudio.microsoft.com/download/pr/4bfa247d-321d-4b29-a34b-62320849059b/8df7a17d9aad4044efe9b5b1c423e82c/aspnetcore-runtime-5.0.17-win-x86.exe', 'https://download.visualstudio.microsoft.com/download/pr/3789ec90-2717-424f-8b9c-3adbbcea6c16/2085cc5ff077b8789ff938015392e406/aspnetcore-runtime-5.0.17-win-x64.exe'), - '', False, False); + Dependency_AddNetCore('dotnet50asp', 'ASP.NET Core Runtime 5.0.17', Dependency_String('https://aka.ms/dotnet/5.0/aspnetcore-runtime-win-x86.exe', 'https://aka.ms/dotnet/5.0/aspnetcore-runtime-win-x64.exe', 'https://aka.ms/dotnet/5.0/aspnetcore-runtime-win-x64.exe')); end; end; procedure Dependency_AddDotNet50Desktop; begin - // https://dotnet.microsoft.com/download/dotnet/5.0 if not Dependency_IsNetCoreInstalled('Microsoft.WindowsDesktop.App', 5, 0, 17) then begin - Dependency_Add('dotnet50desktop' + Dependency_ArchSuffix + '.exe', - '/lcid ' + IntToStr(GetUILanguage) + ' /passive /norestart', - '.NET Desktop Runtime 5.0.17' + Dependency_ArchTitle, - Dependency_String('https://download.visualstudio.microsoft.com/download/pr/b6fe5f2a-95f4-46f1-9824-f5994f10bc69/db5ec9b47ec877b5276f83a185fdb6a0/windowsdesktop-runtime-5.0.17-win-x86.exe', 'https://download.visualstudio.microsoft.com/download/pr/3aa4e942-42cd-4bf5-afe7-fc23bd9c69c5/64da54c8864e473c19a7d3de15790418/windowsdesktop-runtime-5.0.17-win-x64.exe'), - '', False, False); + Dependency_AddNetCore('dotnet50desktop', '.NET Desktop Runtime 5.0.17', Dependency_String('https://aka.ms/dotnet/5.0/windowsdesktop-runtime-win-x86.exe', 'https://aka.ms/dotnet/5.0/windowsdesktop-runtime-win-x64.exe', 'https://aka.ms/dotnet/5.0/windowsdesktop-runtime-win-arm64.exe')); end; end; @@ -408,35 +462,21 @@ procedure Dependency_AddDotNet60; begin // https://dotnet.microsoft.com/download/dotnet/6.0 if not Dependency_IsNetCoreInstalled('Microsoft.NETCore.App', 6, 0, 36) then begin - Dependency_Add('dotnet60' + Dependency_ArchSuffix + '.exe', - '/lcid ' + IntToStr(GetUILanguage) + ' /passive /norestart', - '.NET Runtime 6.0.36' + Dependency_ArchTitle, - Dependency_String('https://download.visualstudio.microsoft.com/download/pr/727d79cb-6a4c-4a6b-bd9e-af99ad62de0b/5cd3550f1589a2f1b3a240c745dd1023/dotnet-runtime-6.0.36-win-x86.exe', 'https://download.visualstudio.microsoft.com/download/pr/1a5fc50a-9222-4f33-8f73-3c78485a55c7/1cb55899b68fcb9d98d206ba56f28b66/dotnet-runtime-6.0.36-win-x64.exe'), - '', False, False); + Dependency_AddNetCore('dotnet60', '.NET Runtime 6.0.36', Dependency_String('https://aka.ms/dotnet/6.0/dotnet-runtime-win-x86.exe', 'https://aka.ms/dotnet/6.0/dotnet-runtime-win-x64.exe', 'https://aka.ms/dotnet/6.0/dotnet-runtime-win-arm64.exe')); end; end; procedure Dependency_AddDotNet60Asp; begin - // https://dotnet.microsoft.com/download/dotnet/6.0 if not Dependency_IsNetCoreInstalled('Microsoft.AspNetCore.App', 6, 0, 36) then begin - Dependency_Add('dotnet60asp' + Dependency_ArchSuffix + '.exe', - '/lcid ' + IntToStr(GetUILanguage) + ' /passive /norestart', - 'ASP.NET Core Runtime 6.0.36' + Dependency_ArchTitle, - Dependency_String('https://download.visualstudio.microsoft.com/download/pr/8cfa7f46-88f2-4521-a2d8-59b827420344/447de18a48115ac0fe6f381f0528e7a5/aspnetcore-runtime-6.0.36-win-x86.exe', 'https://download.visualstudio.microsoft.com/download/pr/0f0ea01c-ef7c-4493-8960-d1e9269b718b/3f95c5bd383be65c2c3384e9fa984078/aspnetcore-runtime-6.0.36-win-x64.exe'), - '', False, False); + Dependency_AddNetCore('dotnet60asp', 'ASP.NET Core Runtime 6.0.36', Dependency_String('https://aka.ms/dotnet/6.0/aspnetcore-runtime-win-x86.exe', 'https://aka.ms/dotnet/6.0/aspnetcore-runtime-win-x64.exe', 'https://aka.ms/dotnet/6.0/aspnetcore-runtime-win-x64.exe')); end; end; procedure Dependency_AddDotNet60Desktop; begin - // https://dotnet.microsoft.com/download/dotnet/6.0 if not Dependency_IsNetCoreInstalled('Microsoft.WindowsDesktop.App', 6, 0, 36) then begin - Dependency_Add('dotnet60desktop' + Dependency_ArchSuffix + '.exe', - '/lcid ' + IntToStr(GetUILanguage) + ' /passive /norestart', - '.NET Desktop Runtime 6.0.36' + Dependency_ArchTitle, - Dependency_String('https://download.visualstudio.microsoft.com/download/pr/cdc314df-4a4c-4709-868d-b974f336f77f/acd5ab7637e456c8a3aa667661324f6d/windowsdesktop-runtime-6.0.36-win-x86.exe', 'https://download.visualstudio.microsoft.com/download/pr/f6b6c5dc-e02d-4738-9559-296e938dabcb/b66d365729359df8e8ea131197715076/windowsdesktop-runtime-6.0.36-win-x64.exe'), - '', False, False); + Dependency_AddNetCore('dotnet60desktop', '.NET Desktop Runtime 6.0.36', Dependency_String('https://aka.ms/dotnet/6.0/windowsdesktop-runtime-win-x86.exe', 'https://aka.ms/dotnet/6.0/windowsdesktop-runtime-win-x64.exe', 'https://aka.ms/dotnet/6.0/windowsdesktop-runtime-win-arm64.exe')); end; end; @@ -444,119 +484,114 @@ procedure Dependency_AddDotNet70; begin // https://dotnet.microsoft.com/download/dotnet/7.0 if not Dependency_IsNetCoreInstalled('Microsoft.NETCore.App', 7, 0, 20) then begin - Dependency_Add('dotnet70' + Dependency_ArchSuffix + '.exe', - '/lcid ' + IntToStr(GetUILanguage) + ' /passive /norestart', - '.NET Runtime 7.0.20' + Dependency_ArchTitle, - Dependency_String('https://download.visualstudio.microsoft.com/download/pr/b2e820bd-b591-43df-ab10-1eeb7998cc18/661ca79db4934c6247f5c7a809a62238/dotnet-runtime-7.0.20-win-x86.exe', 'https://download.visualstudio.microsoft.com/download/pr/be7eaed0-4e32-472b-b53e-b08ac3433a22/fc99a5977c57cbfb93b4afb401953818/dotnet-runtime-7.0.20-win-x64.exe'), - '', False, False); + Dependency_AddNetCore('dotnet70', '.NET Runtime 7.0.20', Dependency_String('https://aka.ms/dotnet/7.0/dotnet-runtime-win-x86.exe', 'https://aka.ms/dotnet/7.0/dotnet-runtime-win-x64.exe', 'https://aka.ms/dotnet/7.0/dotnet-runtime-win-arm64.exe')); end; end; procedure Dependency_AddDotNet70Asp; begin - // https://dotnet.microsoft.com/download/dotnet/7.0 if not Dependency_IsNetCoreInstalled('Microsoft.AspNetCore.App', 7, 0, 20) then begin - Dependency_Add('dotnet70asp' + Dependency_ArchSuffix + '.exe', - '/lcid ' + IntToStr(GetUILanguage) + ' /passive /norestart', - 'ASP.NET Core Runtime 7.0.20' + Dependency_ArchTitle, - Dependency_String('https://download.visualstudio.microsoft.com/download/pr/d84ac38e-a248-4c8d-b1fe-4ee092d6b4b1/9f0bf370619ab3da8869e467827a6dc6/aspnetcore-runtime-7.0.20-win-x86.exe', 'https://download.visualstudio.microsoft.com/download/pr/10651a65-8afc-46e3-9287-fecb0e68504e/4c2bf0cdb44612f29d9b3f901098e13e/aspnetcore-runtime-7.0.20-win-x64.exe'), - '', False, False); + Dependency_AddNetCore('dotnet70asp', 'ASP.NET Core Runtime 7.0.20', Dependency_String('https://aka.ms/dotnet/7.0/aspnetcore-runtime-win-x86.exe', 'https://aka.ms/dotnet/7.0/aspnetcore-runtime-win-x64.exe', 'https://aka.ms/dotnet/7.0/aspnetcore-runtime-win-arm64.exe')); end; end; procedure Dependency_AddDotNet70Desktop; begin - // https://dotnet.microsoft.com/download/dotnet/7.0 if not Dependency_IsNetCoreInstalled('Microsoft.WindowsDesktop.App', 7, 0, 20) then begin - Dependency_Add('dotnet70desktop' + Dependency_ArchSuffix + '.exe', - '/lcid ' + IntToStr(GetUILanguage) + ' /passive /norestart', - '.NET Desktop Runtime 7.0.20' + Dependency_ArchTitle, - Dependency_String('https://download.visualstudio.microsoft.com/download/pr/b840017b-c69f-4724-a152-11020a0039e6/b74aa12e4ee765a3387a7dcd4ba56187/windowsdesktop-runtime-7.0.20-win-x86.exe', 'https://download.visualstudio.microsoft.com/download/pr/08bbfe8f-812d-479f-803b-23ea0bffce47/c320e4b037f3e92ab7ea92c3d7ea3ca1/windowsdesktop-runtime-7.0.20-win-x64.exe'), - '', False, False); + Dependency_AddNetCore('dotnet70desktop', '.NET Desktop Runtime 7.0.20', Dependency_String('https://aka.ms/dotnet/7.0/windowsdesktop-runtime-win-x86.exe', 'https://aka.ms/dotnet/7.0/windowsdesktop-runtime-win-x64.exe', 'https://aka.ms/dotnet/7.0/windowsdesktop-runtime-win-arm64.exe')); end; end; - procedure Dependency_AddDotNet80; begin // https://dotnet.microsoft.com/download/dotnet/8.0 - if not Dependency_IsNetCoreInstalled('Microsoft.NETCore.App', 8, 0, 13) then begin - Dependency_Add('dotnet80' + Dependency_ArchSuffix + '.exe', - '/lcid ' + IntToStr(GetUILanguage) + ' /passive /norestart', - '.NET Runtime 8.0.13' + Dependency_ArchTitle, - Dependency_String('https://download.visualstudio.microsoft.com/download/pr/5bac19ad-0711-4eba-a5a3-5e818c5f2fdf/cdec118c18b8457fe4d3ff918f78b4bd/dotnet-runtime-8.0.13-win-x86.exe', 'https://download.visualstudio.microsoft.com/download/pr/9c2068f2-dd3e-46cb-a88d-3c2d35b5181f/9ce26210851b0720c5382c6cd056b126/dotnet-runtime-8.0.13-win-x64.exe'), - '', False, False); + if not Dependency_IsNetCoreInstalled('Microsoft.NETCore.App', 8, 0, 28) then begin + Dependency_AddNetCore('dotnet80', '.NET Runtime 8.0.28', Dependency_String('https://aka.ms/dotnet/8.0/dotnet-runtime-win-x86.exe', 'https://aka.ms/dotnet/8.0/dotnet-runtime-win-x64.exe', 'https://aka.ms/dotnet/8.0/dotnet-runtime-win-arm64.exe')); end; end; procedure Dependency_AddDotNet80Asp; begin - // https://dotnet.microsoft.com/download/dotnet/8.0 - if not Dependency_IsNetCoreInstalled('Microsoft.AspNetCore.App', 8, 0, 13) then begin - Dependency_Add('dotnet80asp' + Dependency_ArchSuffix + '.exe', - '/lcid ' + IntToStr(GetUILanguage) + ' /passive /norestart', - 'ASP.NET Core Runtime 8.0.13' + Dependency_ArchTitle, - Dependency_String('https://download.visualstudio.microsoft.com/download/pr/b11da59f-561b-466b-bfa8-d2dfc9b5bf48/f8dce6a44fd7be61ff97fe4949e57015/aspnetcore-runtime-8.0.13-win-x86.exe', 'https://download.visualstudio.microsoft.com/download/pr/86b8931f-09f6-4fce-b546-8139350da0c4/d6a5f16bcf81e0b5e9a733b892b1240f/aspnetcore-runtime-8.0.13-win-x64.exe'), - '', False, False); + if not Dependency_IsNetCoreInstalled('Microsoft.AspNetCore.App', 8, 0, 28) then begin + Dependency_AddNetCore('dotnet80asp', 'ASP.NET Core Runtime 8.0.28', Dependency_String('https://aka.ms/dotnet/8.0/aspnetcore-runtime-win-x86.exe', 'https://aka.ms/dotnet/8.0/aspnetcore-runtime-win-x64.exe', 'https://aka.ms/dotnet/8.0/aspnetcore-runtime-win-arm64.exe')); end; end; procedure Dependency_AddDotNet80Desktop; begin - // https://dotnet.microsoft.com/download/dotnet/8.0 - if not Dependency_IsNetCoreInstalled('Microsoft.WindowsDesktop.App', 8, 0, 13) then begin - Dependency_Add('dotnet80desktop' + Dependency_ArchSuffix + '.exe', - '/lcid ' + IntToStr(GetUILanguage) + ' /passive /norestart', - '.NET Desktop Runtime 8.0.13' + Dependency_ArchTitle, - Dependency_String('https://download.visualstudio.microsoft.com/download/pr/b1827c52-ec83-4b3e-8d24-f321276bcdea/812e8d5871111cdc02cc82209c7d45fd/windowsdesktop-runtime-8.0.13-win-x86.exe', 'https://download.visualstudio.microsoft.com/download/pr/fc8c9dea-8180-4dad-bf1b-5f229cf47477/c3f0536639ab40f1470b6bad5e1b95b8/windowsdesktop-runtime-8.0.13-win-x64.exe'), - '', False, False); + if not Dependency_IsNetCoreInstalled('Microsoft.WindowsDesktop.App', 8, 0, 28) then begin + Dependency_AddNetCore('dotnet80desktop', '.NET Desktop Runtime 8.0.28', Dependency_String('https://aka.ms/dotnet/8.0/windowsdesktop-runtime-win-x86.exe', 'https://aka.ms/dotnet/8.0/windowsdesktop-runtime-win-x64.exe', 'https://aka.ms/dotnet/8.0/windowsdesktop-runtime-win-arm64.exe')); end; end; procedure Dependency_AddDotNet90; begin // https://dotnet.microsoft.com/download/dotnet/9.0 - if not Dependency_IsNetCoreInstalled('Microsoft.NETCore.App', 9, 0, 4) then begin - Dependency_Add('dotnet90' + Dependency_ArchSuffix + '.exe', - '/lcid ' + IntToStr(GetUILanguage) + ' /passive /norestart', - '.NET Runtime 9.0.4' + Dependency_ArchTitle, - Dependency_String('https://builds.dotnet.microsoft.com/dotnet/Runtime/9.0.4/dotnet-runtime-9.0.4-win-x86.exe', 'https://builds.dotnet.microsoft.com/dotnet/Runtime/9.0.4/dotnet-runtime-9.0.4-win-x64.exe'), - '', False, False); + if not Dependency_IsNetCoreInstalled('Microsoft.NETCore.App', 9, 0, 17) then begin + Dependency_AddNetCore('dotnet90', '.NET Runtime 9.0.17', Dependency_String('https://aka.ms/dotnet/9.0/dotnet-runtime-win-x86.exe', 'https://aka.ms/dotnet/9.0/dotnet-runtime-win-x64.exe', 'https://aka.ms/dotnet/9.0/dotnet-runtime-win-arm64.exe')); end; end; procedure Dependency_AddDotNet90Asp; begin - // https://dotnet.microsoft.com/download/dotnet/9.0 - if not Dependency_IsNetCoreInstalled('Microsoft.AspNetCore.App', 9, 0, 4) then begin - Dependency_Add('dotnet90asp' + Dependency_ArchSuffix + '.exe', - '/lcid ' + IntToStr(GetUILanguage) + ' /passive /norestart', - 'ASP.NET Core Runtime 9.0.4' + Dependency_ArchTitle, - Dependency_String('https://builds.dotnet.microsoft.com/dotnet/aspnetcore/Runtime/9.0.4/aspnetcore-runtime-9.0.4-win-x86.exe', 'https://builds.dotnet.microsoft.com/dotnet/aspnetcore/Runtime/9.0.4/aspnetcore-runtime-9.0.4-win-x64.exe'), - '', False, False); + if not Dependency_IsNetCoreInstalled('Microsoft.AspNetCore.App', 9, 0, 17) then begin + Dependency_AddNetCore('dotnet90asp', 'ASP.NET Core Runtime 9.0.17', Dependency_String('https://aka.ms/dotnet/9.0/aspnetcore-runtime-win-x86.exe', 'https://aka.ms/dotnet/9.0/aspnetcore-runtime-win-x64.exe', 'https://aka.ms/dotnet/9.0/aspnetcore-runtime-win-arm64.exe')); end; end; procedure Dependency_AddDotNet90Desktop; begin - // https://dotnet.microsoft.com/download/dotnet/9.0 - if not Dependency_IsNetCoreInstalled('Microsoft.WindowsDesktop.App', 9, 0, 4) then begin - Dependency_Add('dotnet90desktop' + Dependency_ArchSuffix + '.exe', - '/lcid ' + IntToStr(GetUILanguage) + ' /passive /norestart', - '.NET Desktop Runtime 9.0.4' + Dependency_ArchTitle, - Dependency_String('https://builds.dotnet.microsoft.com/dotnet/WindowsDesktop/9.0.4/windowsdesktop-runtime-9.0.4-win-x86.exe', 'https://builds.dotnet.microsoft.com/dotnet/WindowsDesktop/9.0.4/windowsdesktop-runtime-9.0.4-win-x64.exe'), + if not Dependency_IsNetCoreInstalled('Microsoft.WindowsDesktop.App', 9, 0, 17) then begin + Dependency_AddNetCore('dotnet90desktop', '.NET Desktop Runtime 9.0.17', Dependency_String('https://aka.ms/dotnet/9.0/windowsdesktop-runtime-win-x86.exe', 'https://aka.ms/dotnet/9.0/windowsdesktop-runtime-win-x64.exe', 'https://aka.ms/dotnet/9.0/windowsdesktop-runtime-win-arm64.exe')); + end; +end; + +procedure Dependency_AddDotNet100; +begin + // https://dotnet.microsoft.com/download/dotnet/10.0 + if not Dependency_IsNetCoreInstalled('Microsoft.NETCore.App', 10, 0, 9) then begin + Dependency_AddNetCore('dotnet100', '.NET Runtime 10.0.9', Dependency_String('https://aka.ms/dotnet/10.0/dotnet-runtime-win-x86.exe', 'https://aka.ms/dotnet/10.0/dotnet-runtime-win-x64.exe', 'https://aka.ms/dotnet/10.0/dotnet-runtime-win-arm64.exe')); + end; +end; + +procedure Dependency_AddDotNet100Asp; +begin + if not Dependency_IsNetCoreInstalled('Microsoft.AspNetCore.App', 10, 0, 9) then begin + Dependency_AddNetCore('dotnet100asp', 'ASP.NET Core Runtime 10.0.9', Dependency_String('https://aka.ms/dotnet/10.0/aspnetcore-runtime-win-x86.exe', 'https://aka.ms/dotnet/10.0/aspnetcore-runtime-win-x64.exe', 'https://aka.ms/dotnet/10.0/aspnetcore-runtime-win-arm64.exe')); + end; +end; + +procedure Dependency_AddDotNet100Desktop; +begin + if not Dependency_IsNetCoreInstalled('Microsoft.WindowsDesktop.App', 10, 0, 9) then begin + Dependency_AddNetCore('dotnet100desktop', '.NET Desktop Runtime 10.0.9', Dependency_String('https://aka.ms/dotnet/10.0/windowsdesktop-runtime-win-x86.exe', 'https://aka.ms/dotnet/10.0/windowsdesktop-runtime-win-x64.exe', 'https://aka.ms/dotnet/10.0/windowsdesktop-runtime-win-arm64.exe')); + end; +end; + +procedure Dependency_AddDotNetHosting(const Major, Patch: Integer; const URL: String); +begin + // https://dotnet.microsoft.com/download/dotnet + if not Dependency_IsNetCoreInstalled('Microsoft.AspNetCore.App', Major, 0, Patch) then begin + Dependency_Add('dotnet' + IntToStr(Major) + '0hosting.exe', + '/lcid ' + IntToStr(GetUILanguage) + ' ' + Dependency_PassiveOrQuiet('/passive', '/quiet') + ' /norestart', + 'ASP.NET Core ' + IntToStr(Major) + '.0 Hosting Bundle', + URL, '', False, False); end; end; +procedure Dependency_AddDotNet80Hosting; begin Dependency_AddDotNetHosting(8, 28, 'https://aka.ms/dotnet/8.0/dotnet-hosting-win.exe'); end; +procedure Dependency_AddDotNet90Hosting; begin Dependency_AddDotNetHosting(9, 17, 'https://aka.ms/dotnet/9.0/dotnet-hosting-win.exe'); end; +procedure Dependency_AddDotNet100Hosting; begin Dependency_AddDotNetHosting(10, 9, 'https://aka.ms/dotnet/10.0/dotnet-hosting-win.exe'); end; + procedure Dependency_AddVC2005; begin // https://www.microsoft.com/en-us/download/details.aspx?id=26347 - if not IsMsiProductInstalled(Dependency_String('{86C9D5AA-F00C-4921-B3F2-C60AF92E2844}', '{A8D19029-8E5C-4E22-8011-48070F9E796E}'), PackVersionComponents(8, 0, 61000, 0)) then begin + if not IsMsiProductInstalled(Dependency_String('{86C9D5AA-F00C-4921-B3F2-C60AF92E2844}', '{A8D19029-8E5C-4E22-8011-48070F9E796E}', '{A8D19029-8E5C-4E22-8011-48070F9E796E}'), PackVersionComponents(8, 0, 61000, 0)) then begin Dependency_Add('vcredist2005' + Dependency_ArchSuffix + '.exe', '/q', 'Visual C++ 2005 Service Pack 1 Redistributable' + Dependency_ArchTitle, - Dependency_String('https://download.microsoft.com/download/8/B/4/8B42259F-5D70-43F4-AC2E-4B208FD8D66A/vcredist_x86.EXE', 'https://download.microsoft.com/download/8/B/4/8B42259F-5D70-43F4-AC2E-4B208FD8D66A/vcredist_x64.EXE'), + Dependency_String('https://download.microsoft.com/download/8/B/4/8B42259F-5D70-43F4-AC2E-4B208FD8D66A/vcredist_x86.EXE', 'https://download.microsoft.com/download/8/B/4/8B42259F-5D70-43F4-AC2E-4B208FD8D66A/vcredist_x64.EXE', 'https://download.microsoft.com/download/8/B/4/8B42259F-5D70-43F4-AC2E-4B208FD8D66A/vcredist_x64.EXE'), '', False, False); end; end; @@ -564,11 +599,11 @@ end; procedure Dependency_AddVC2008; begin // https://www.microsoft.com/en-us/download/details.aspx?id=26368 - if not IsMsiProductInstalled(Dependency_String('{DE2C306F-A067-38EF-B86C-03DE4B0312F9}', '{FDA45DDF-8E17-336F-A3ED-356B7B7C688A}'), PackVersionComponents(9, 0, 30729, 6161)) then begin + if not IsMsiProductInstalled(Dependency_String('{DE2C306F-A067-38EF-B86C-03DE4B0312F9}', '{FDA45DDF-8E17-336F-A3ED-356B7B7C688A}', '{FDA45DDF-8E17-336F-A3ED-356B7B7C688A}'), PackVersionComponents(9, 0, 30729, 6161)) then begin Dependency_Add('vcredist2008' + Dependency_ArchSuffix + '.exe', '/q', 'Visual C++ 2008 Service Pack 1 Redistributable' + Dependency_ArchTitle, - Dependency_String('https://download.microsoft.com/download/5/D/8/5D8C65CB-C849-4025-8E95-C3966CAFD8AE/vcredist_x86.exe', 'https://download.microsoft.com/download/5/D/8/5D8C65CB-C849-4025-8E95-C3966CAFD8AE/vcredist_x64.exe'), + Dependency_String('https://download.microsoft.com/download/5/D/8/5D8C65CB-C849-4025-8E95-C3966CAFD8AE/vcredist_x86.exe', 'https://download.microsoft.com/download/5/D/8/5D8C65CB-C849-4025-8E95-C3966CAFD8AE/vcredist_x64.exe', 'https://download.microsoft.com/download/5/D/8/5D8C65CB-C849-4025-8E95-C3966CAFD8AE/vcredist_x64.exe'), '', False, False); end; end; @@ -576,11 +611,11 @@ end; procedure Dependency_AddVC2010; begin // https://www.microsoft.com/en-us/download/details.aspx?id=26999 - if not IsMsiProductInstalled(Dependency_String('{1F4F1D2A-D9DA-32CF-9909-48485DA06DD5}', '{5B75F761-BAC8-33BC-A381-464DDDD813A3}'), PackVersionComponents(10, 0, 40219, 0)) then begin + if not IsMsiProductInstalled(Dependency_String('{1F4F1D2A-D9DA-32CF-9909-48485DA06DD5}', '{5B75F761-BAC8-33BC-A381-464DDDD813A3}', '{5B75F761-BAC8-33BC-A381-464DDDD813A3}'), PackVersionComponents(10, 0, 40219, 0)) then begin Dependency_Add('vcredist2010' + Dependency_ArchSuffix + '.exe', - '/passive /norestart', + Dependency_PassiveOrQuiet('/passive', '/q') + ' /norestart', 'Visual C++ 2010 Service Pack 1 Redistributable' + Dependency_ArchTitle, - Dependency_String('https://download.microsoft.com/download/1/6/5/165255E7-1014-4D0A-B094-B6A430A6BFFC/vcredist_x86.exe', 'https://download.microsoft.com/download/1/6/5/165255E7-1014-4D0A-B094-B6A430A6BFFC/vcredist_x64.exe'), + Dependency_String('https://download.microsoft.com/download/1/6/5/165255E7-1014-4D0A-B094-B6A430A6BFFC/vcredist_x86.exe', 'https://download.microsoft.com/download/1/6/5/165255E7-1014-4D0A-B094-B6A430A6BFFC/vcredist_x64.exe', 'https://download.microsoft.com/download/1/6/5/165255E7-1014-4D0A-B094-B6A430A6BFFC/vcredist_x64.exe'), '', False, False); end; end; @@ -588,11 +623,11 @@ end; procedure Dependency_AddVC2012; begin // https://www.microsoft.com/en-us/download/details.aspx?id=30679 - if not IsMsiProductInstalled(Dependency_String('{4121ED58-4BD9-3E7B-A8B5-9F8BAAE045B7}', '{EFA6AFA1-738E-3E00-8101-FD03B86B29D1}'), PackVersionComponents(11, 0, 61030, 0)) then begin + if not IsMsiProductInstalled(Dependency_String('{4121ED58-4BD9-3E7B-A8B5-9F8BAAE045B7}', '{EFA6AFA1-738E-3E00-8101-FD03B86B29D1}', '{EFA6AFA1-738E-3E00-8101-FD03B86B29D1}'), PackVersionComponents(11, 0, 61030, 0)) then begin Dependency_Add('vcredist2012' + Dependency_ArchSuffix + '.exe', - '/passive /norestart', + Dependency_PassiveOrQuiet('/passive', '/quiet') + ' /norestart', 'Visual C++ 2012 Update 4 Redistributable' + Dependency_ArchTitle, - Dependency_String('https://download.microsoft.com/download/1/6/B/16B06F60-3B20-4FF2-B699-5E9B7962F9AE/VSU_4/vcredist_x86.exe', 'https://download.microsoft.com/download/1/6/B/16B06F60-3B20-4FF2-B699-5E9B7962F9AE/VSU_4/vcredist_x64.exe'), + Dependency_String('https://download.microsoft.com/download/1/6/B/16B06F60-3B20-4FF2-B699-5E9B7962F9AE/VSU_4/vcredist_x86.exe', 'https://download.microsoft.com/download/1/6/B/16B06F60-3B20-4FF2-B699-5E9B7962F9AE/VSU_4/vcredist_x64.exe', 'https://download.microsoft.com/download/1/6/B/16B06F60-3B20-4FF2-B699-5E9B7962F9AE/VSU_4/vcredist_x64.exe'), '', False, False); end; end; @@ -600,32 +635,38 @@ end; procedure Dependency_AddVC2013; begin // https://support.microsoft.com/en-us/help/4032938 - if not IsMsiProductInstalled(Dependency_String('{B59F5BF1-67C8-3802-8E59-2CE551A39FC5}', '{20400CF0-DE7C-327E-9AE4-F0F38D9085F8}'), PackVersionComponents(12, 0, 40664, 0)) then begin + if not IsMsiProductInstalled(Dependency_String('{B59F5BF1-67C8-3802-8E59-2CE551A39FC5}', '{20400CF0-DE7C-327E-9AE4-F0F38D9085F8}', '{20400CF0-DE7C-327E-9AE4-F0F38D9085F8}'), PackVersionComponents(12, 0, 40664, 0)) then begin Dependency_Add('vcredist2013' + Dependency_ArchSuffix + '.exe', - '/passive /norestart', + Dependency_PassiveOrQuiet('/passive', '/quiet') + ' /norestart', 'Visual C++ 2013 Update 5 Redistributable' + Dependency_ArchTitle, - Dependency_String('https://download.visualstudio.microsoft.com/download/pr/10912113/5da66ddebb0ad32ebd4b922fd82e8e25/vcredist_x86.exe', 'https://download.visualstudio.microsoft.com/download/pr/10912041/cee5d6bca2ddbcd039da727bf4acb48a/vcredist_x64.exe'), + Dependency_String('https://download.visualstudio.microsoft.com/download/pr/10912113/5da66ddebb0ad32ebd4b922fd82e8e25/vcredist_x86.exe', 'https://download.visualstudio.microsoft.com/download/pr/10912041/cee5d6bca2ddbcd039da727bf4acb48a/vcredist_x64.exe', 'https://download.visualstudio.microsoft.com/download/pr/10912041/cee5d6bca2ddbcd039da727bf4acb48a/vcredist_x64.exe'), '', False, False); end; end; -procedure Dependency_AddVC2015To2022; +procedure Dependency_AddVC14; +var + Version: String; + PackedVersion: Int64; begin - // https://docs.microsoft.com/en-us/cpp/windows/latest-supported-vc-redist - if not IsMsiProductInstalled(Dependency_String('{65E5BD06-6392-3027-8C26-853107D3CF1A}', '{36F68A90-239C-34DF-B58C-64B30153CE35}'), PackVersionComponents(14, 42, 34433, 0)) then begin - Dependency_Add('vcredist2022' + Dependency_ArchSuffix + '.exe', - '/passive /norestart', - 'Visual C++ 2015-2022 Redistributable' + Dependency_ArchTitle, - Dependency_String('https://aka.ms/vs/17/release/vc_redist.x86.exe', 'https://aka.ms/vs/17/release/vc_redist.x64.exe'), + // https://learn.microsoft.com/en-us/cpp/windows/latest-supported-vc-redist + if RegQueryStringValue(HKLM, 'SOFTWARE\Microsoft\VisualStudio\14.0\VC\Runtimes\' + Dependency_String('x86', 'x64', 'arm64'), 'Version', Version) and (Copy(Version, 1, 1) = 'v') then begin + Delete(Version, 1, 1); + end; + if not StrToVersion(Version, PackedVersion) or (ComparePackedVersion(PackedVersion, PackVersionComponents(14, 51, 36247, 0)) < 0) then begin + Dependency_Add('vcredist14' + Dependency_ArchSuffix + '.exe', + Dependency_PassiveOrQuiet('/passive', '/quiet') + ' /norestart', + 'Visual C++ v14 Redistributable' + Dependency_ArchTitle, + Dependency_String('https://aka.ms/vc14/vc_redist.x86.exe', 'https://aka.ms/vc14/vc_redist.x64.exe', 'https://aka.ms/vc14/vc_redist.arm64.exe'), '', False, False); end; end; +procedure Dependency_AddVC2015To2019; begin Dependency_AddVC14; end; +procedure Dependency_AddVC2015To2022; begin Dependency_AddVC14; end; + procedure Dependency_AddDirectX; begin -#ifdef Dependency_Files_DirectX - ExtractTemporaryFile('dxwebsetup.exe'); -#endif // https://www.microsoft.com/en-us/download/details.aspx?id=35 Dependency_Add('dxwebsetup.exe', '/q', @@ -642,9 +683,9 @@ begin // https://www.microsoft.com/en-us/download/details.aspx?id=30438 if not RegQueryStringValue(HKLM, 'SOFTWARE\Microsoft\Microsoft SQL Server\MSSQL10_50.MSSQLSERVER\MSSQLServer\CurrentVersion', 'CurrentVersion', Version) or not StrToVersion(Version, PackedVersion) or (ComparePackedVersion(PackedVersion, PackVersionComponents(10, 50, 4000, 0)) < 0) then begin Dependency_Add('sql2008express' + Dependency_ArchSuffix + '.exe', - '/QS /IACCEPTSQLSERVERLICENSETERMS /ACTION=INSTALL /FEATURES=SQL /INSTANCENAME=MSSQLSERVER', + Dependency_PassiveOrQuiet('/QS', '/Q') + ' /IACCEPTSQLSERVERLICENSETERMS /ACTION=INSTALL /FEATURES=SQL /INSTANCENAME=MSSQLSERVER', 'SQL Server 2008 R2 Service Pack 2 Express', - Dependency_String('https://download.microsoft.com/download/0/4/B/04BE03CD-EAF3-4797-9D8D-2E08E316C998/SQLEXPR32_x86_ENU.exe', 'https://download.microsoft.com/download/0/4/B/04BE03CD-EAF3-4797-9D8D-2E08E316C998/SQLEXPR_x64_ENU.exe'), + Dependency_String('https://download.microsoft.com/download/0/4/B/04BE03CD-EAF3-4797-9D8D-2E08E316C998/SQLEXPR32_x86_ENU.exe', 'https://download.microsoft.com/download/0/4/B/04BE03CD-EAF3-4797-9D8D-2E08E316C998/SQLEXPR_x64_ENU.exe', 'https://download.microsoft.com/download/0/4/B/04BE03CD-EAF3-4797-9D8D-2E08E316C998/SQLEXPR_x64_ENU.exe'), '', False, False); end; end; @@ -657,9 +698,9 @@ begin // https://www.microsoft.com/en-us/download/details.aspx?id=56042 if not RegQueryStringValue(HKLM, 'SOFTWARE\Microsoft\Microsoft SQL Server\MSSQL11.MSSQLSERVER\MSSQLServer\CurrentVersion', 'CurrentVersion', Version) or not StrToVersion(Version, PackedVersion) or (ComparePackedVersion(PackedVersion, PackVersionComponents(11, 0, 7001, 0)) < 0) then begin Dependency_Add('sql2012express' + Dependency_ArchSuffix + '.exe', - '/QS /IACCEPTSQLSERVERLICENSETERMS /ACTION=INSTALL /FEATURES=SQL /INSTANCENAME=MSSQLSERVER', + Dependency_PassiveOrQuiet('/QS', '/Q') + ' /IACCEPTSQLSERVERLICENSETERMS /ACTION=INSTALL /FEATURES=SQL /INSTANCENAME=MSSQLSERVER', 'SQL Server 2012 Service Pack 4 Express', - Dependency_String('https://download.microsoft.com/download/B/D/E/BDE8FAD6-33E5-44F6-B714-348F73E602B6/SQLEXPR32_x86_ENU.exe', 'https://download.microsoft.com/download/B/D/E/BDE8FAD6-33E5-44F6-B714-348F73E602B6/SQLEXPR_x64_ENU.exe'), + Dependency_String('https://download.microsoft.com/download/B/D/E/BDE8FAD6-33E5-44F6-B714-348F73E602B6/SQLEXPR32_x86_ENU.exe', 'https://download.microsoft.com/download/B/D/E/BDE8FAD6-33E5-44F6-B714-348F73E602B6/SQLEXPR_x64_ENU.exe', 'https://download.microsoft.com/download/B/D/E/BDE8FAD6-33E5-44F6-B714-348F73E602B6/SQLEXPR_x64_ENU.exe'), '', False, False); end; end; @@ -672,9 +713,9 @@ begin // https://www.microsoft.com/en-us/download/details.aspx?id=57473 if not RegQueryStringValue(HKLM, 'SOFTWARE\Microsoft\Microsoft SQL Server\MSSQL12.MSSQLSERVER\MSSQLServer\CurrentVersion', 'CurrentVersion', Version) or not StrToVersion(Version, PackedVersion) or (ComparePackedVersion(PackedVersion, PackVersionComponents(12, 0, 6024, 0)) < 0) then begin Dependency_Add('sql2014express' + Dependency_ArchSuffix + '.exe', - '/QS /IACCEPTSQLSERVERLICENSETERMS /ACTION=INSTALL /FEATURES=SQL /INSTANCENAME=MSSQLSERVER', + Dependency_PassiveOrQuiet('/QS', '/Q') + ' /IACCEPTSQLSERVERLICENSETERMS /ACTION=INSTALL /FEATURES=SQL /INSTANCENAME=MSSQLSERVER', 'SQL Server 2014 Service Pack 3 Express', - Dependency_String('https://download.microsoft.com/download/3/9/F/39F968FA-DEBB-4960-8F9E-0E7BB3035959/SQLEXPR32_x86_ENU.exe', 'https://download.microsoft.com/download/3/9/F/39F968FA-DEBB-4960-8F9E-0E7BB3035959/SQLEXPR_x64_ENU.exe'), + Dependency_String('https://download.microsoft.com/download/3/9/F/39F968FA-DEBB-4960-8F9E-0E7BB3035959/SQLEXPR32_x86_ENU.exe', 'https://download.microsoft.com/download/3/9/F/39F968FA-DEBB-4960-8F9E-0E7BB3035959/SQLEXPR_x64_ENU.exe', 'https://download.microsoft.com/download/3/9/F/39F968FA-DEBB-4960-8F9E-0E7BB3035959/SQLEXPR_x64_ENU.exe'), '', False, False); end; end; @@ -687,7 +728,7 @@ begin // https://www.microsoft.com/en-us/download/details.aspx?id=103447 if not RegQueryStringValue(HKLM, 'SOFTWARE\Microsoft\Microsoft SQL Server\MSSQL13.MSSQLSERVER\MSSQLServer\CurrentVersion', 'CurrentVersion', Version) or not StrToVersion(Version, PackedVersion) or (ComparePackedVersion(PackedVersion, PackVersionComponents(13, 0, 6404, 1)) < 0) then begin Dependency_Add('sql2016express' + Dependency_ArchSuffix + '.exe', - '/QS /IACCEPTSQLSERVERLICENSETERMS /ACTION=INSTALL /FEATURES=SQL /INSTANCENAME=MSSQLSERVER', + Dependency_PassiveOrQuiet('/QS', '/Q') + ' /IACCEPTSQLSERVERLICENSETERMS /ACTION=INSTALL /FEATURES=SQL /INSTANCENAME=MSSQLSERVER', 'SQL Server 2016 Service Pack 3 Express', 'https://download.microsoft.com/download/f/a/8/fa83d147-63d1-449c-b22d-5fef9bd5bb46/SQLServer2016-SSEI-Expr.exe', '', False, False); @@ -702,7 +743,7 @@ begin // https://www.microsoft.com/en-us/download/details.aspx?id=55994 if not RegQueryStringValue(HKLM, 'SOFTWARE\Microsoft\Microsoft SQL Server\MSSQL14.MSSQLSERVER\MSSQLServer\CurrentVersion', 'CurrentVersion', Version) or not StrToVersion(Version, PackedVersion) or (ComparePackedVersion(PackedVersion, PackVersionComponents(14, 0, 0, 0)) < 0) then begin Dependency_Add('sql2017express' + Dependency_ArchSuffix + '.exe', - '/QS /IACCEPTSQLSERVERLICENSETERMS /ACTION=INSTALL /FEATURES=SQL /INSTANCENAME=MSSQLSERVER', + Dependency_PassiveOrQuiet('/QS', '/Q') + ' /IACCEPTSQLSERVERLICENSETERMS /ACTION=INSTALL /FEATURES=SQL /INSTANCENAME=MSSQLSERVER', 'SQL Server 2017 Express', 'https://download.microsoft.com/download/5/E/9/5E9B18CC-8FD5-467E-B5BF-BADE39C51F73/SQLServer2017-SSEI-Expr.exe', '', False, False); @@ -717,7 +758,7 @@ begin // https://www.microsoft.com/en-us/download/details.aspx?id=101064 if not RegQueryStringValue(HKLM, 'SOFTWARE\Microsoft\Microsoft SQL Server\MSSQL15.MSSQLSERVER\MSSQLServer\CurrentVersion', 'CurrentVersion', Version) or not StrToVersion(Version, PackedVersion) or (ComparePackedVersion(PackedVersion, PackVersionComponents(15, 0, 0, 0)) < 0) then begin Dependency_Add('sql2019express' + Dependency_ArchSuffix + '.exe', - '/QS /IACCEPTSQLSERVERLICENSETERMS /ACTION=INSTALL /FEATURES=SQL /INSTANCENAME=MSSQLSERVER', + Dependency_PassiveOrQuiet('/QS', '/Q') + ' /IACCEPTSQLSERVERLICENSETERMS /ACTION=INSTALL /FEATURES=SQL /INSTANCENAME=MSSQLSERVER', 'SQL Server 2019 Express', 'https://download.microsoft.com/download/7/f/8/7f8a9c43-8c8a-4f7c-9f92-83c18d96b681/SQL2019-SSEI-Expr.exe', '', False, False); @@ -732,17 +773,57 @@ begin // https://www.microsoft.com/en-us/download/details.aspx?id=104781 if not RegQueryStringValue(HKLM, 'SOFTWARE\Microsoft\Microsoft SQL Server\MSSQL16.MSSQLSERVER\MSSQLServer\CurrentVersion', 'CurrentVersion', Version) or not StrToVersion(Version, PackedVersion) or (ComparePackedVersion(PackedVersion, PackVersionComponents(16, 0, 1000, 6)) < 0) then begin Dependency_Add('sql2022express' + Dependency_ArchSuffix + '.exe', - '/QS /IACCEPTSQLSERVERLICENSETERMS /ACTION=INSTALL /FEATURES=SQL /INSTANCENAME=MSSQLSERVER', + Dependency_PassiveOrQuiet('/QS', '/Q') + ' /IACCEPTSQLSERVERLICENSETERMS /ACTION=INSTALL /FEATURES=SQL /INSTANCENAME=MSSQLSERVER', 'SQL Server 2022 Express', 'https://go.microsoft.com/fwlink/p/?linkid=2216019', '', False, False); end; end; +procedure Dependency_AddSql2025Express; +var + Version: String; + PackedVersion: Int64; +begin + // https://www.microsoft.com/en-us/sql-server/sql-server-downloads + if not RegQueryStringValue(HKLM, 'SOFTWARE\Microsoft\Microsoft SQL Server\MSSQL17.MSSQLSERVER\MSSQLServer\CurrentVersion', 'CurrentVersion', Version) or not StrToVersion(Version, PackedVersion) or (ComparePackedVersion(PackedVersion, PackVersionComponents(17, 0, 1000, 7)) < 0) then begin + Dependency_Add('sql2025express' + Dependency_ArchSuffix + '.exe', + Dependency_PassiveOrQuiet('/QS', '/Q') + ' /IACCEPTSQLSERVERLICENSETERMS /ACTION=INSTALL /FEATURES=SQL /INSTANCENAME=MSSQLSERVER', + 'SQL Server 2025 Express', + 'https://download.microsoft.com/download/7ab8f535-7eb8-4b16-82eb-eca0fa2d38f3/SQL2025-SSEI-Expr.exe', + '', False, False); + end; +end; + +procedure Dependency_AddSqlOleDb19; +begin + // https://learn.microsoft.com/en-us/sql/connect/oledb/download-oledb-driver-for-sql-server + if not RegValueExists(HKLM, 'SOFTWARE\Microsoft\MSOLEDBSQL19', 'InstalledVersion') then begin + Dependency_Add('msoledbsql' + Dependency_ArchSuffix + '.msi', + '/qn /norestart IACCEPTMSOLEDBSQLLICENSETERMS=YES', + 'Microsoft OLE DB Driver 19 for SQL Server' + Dependency_ArchTitle, + Dependency_String('https://go.microsoft.com/fwlink/?linkid=2364026', 'https://go.microsoft.com/fwlink/?linkid=2364027', 'https://go.microsoft.com/fwlink/?linkid=2364027'), + '', False, False); + end; +end; + +procedure Dependency_AddSqlOdbc18; +begin + // https://learn.microsoft.com/en-us/sql/connect/odbc/download-odbc-driver-for-sql-server + if not RegKeyExists(HKLM, 'SOFTWARE\ODBC\ODBCINST.INI\ODBC Driver 18 for SQL Server') then begin + Dependency_Add('msodbcsql' + Dependency_ArchSuffix + '.msi', + '/qn /norestart IACCEPTMSODBCSQLLICENSETERMS=YES', + 'Microsoft ODBC Driver 18 for SQL Server' + Dependency_ArchTitle, + Dependency_String('https://go.microsoft.com/fwlink/?linkid=2358335', 'https://go.microsoft.com/fwlink/?linkid=2358430', 'https://go.microsoft.com/fwlink/?linkid=2358431'), + '', False, False); + end; +end; + procedure Dependency_AddWebView2; begin // https://developer.microsoft.com/en-us/microsoft-edge/webview2 - if not RegValueExists(HKLM, Dependency_String('SOFTWARE', 'SOFTWARE\WOW6432Node') + '\Microsoft\EdgeUpdate\Clients\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}', 'pv') then begin + if not (RegValueExists(HKLM, Dependency_String('SOFTWARE', 'SOFTWARE\WOW6432Node', 'SOFTWARE\WOW6432Node') + '\Microsoft\EdgeUpdate\Clients\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}', 'pv') + or RegValueExists(HKCU, 'SOFTWARE\Microsoft\EdgeUpdate\Clients\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}', 'pv')) then begin Dependency_Add('MicrosoftEdgeWebview2Setup.exe', '/silent /install', 'WebView2 Runtime', @@ -751,18 +832,6 @@ begin end; end; -procedure Dependency_AddAccessDatabaseEngine2010; -begin - // https://www.microsoft.com/en-us/download/details.aspx?id=13255 - if not RegKeyExists(HKLM, 'SOFTWARE\Microsoft\Office\14.0\Access Connectivity Engine\Engines\ACE') then begin - Dependency_Add('AccessDatabaseEngine2010' + Dependency_ArchSuffix + '.exe', - '/quiet', - 'Microsoft Access Database Engine 2010' + Dependency_ArchTitle, - Dependency_String('https://download.microsoft.com/download/2/4/3/24375141-E08D-4803-AB0E-10F2E3A07AAA/AccessDatabaseEngine.exe', 'https://download.microsoft.com/download/2/4/3/24375141-E08D-4803-AB0E-10F2E3A07AAA/AccessDatabaseEngine_X64.exe'), - '', False, False); - end; -end; - procedure Dependency_AddAccessDatabaseEngine2016; begin // https://www.microsoft.com/en-us/download/details.aspx?id=54920 @@ -770,12 +839,157 @@ begin Dependency_Add('AccessDatabaseEngine2016' + Dependency_ArchSuffix + '.exe', '/quiet', 'Microsoft Access Database Engine 2016' + Dependency_ArchTitle, - Dependency_String('https://download.microsoft.com/download/3/5/C/35C84C36-661A-44E6-9324-8786B8DBE231/accessdatabaseengine.exe', 'https://download.microsoft.com/download/3/5/C/35C84C36-661A-44E6-9324-8786B8DBE231/accessdatabaseengine_X64.exe'), + Dependency_String('https://download.microsoft.com/download/3/5/C/35C84C36-661A-44E6-9324-8786B8DBE231/accessdatabaseengine.exe', 'https://download.microsoft.com/download/3/5/C/35C84C36-661A-44E6-9324-8786B8DBE231/accessdatabaseengine_X64.exe', 'https://download.microsoft.com/download/3/5/C/35C84C36-661A-44E6-9324-8786B8DBE231/accessdatabaseengine_X64.exe'), '', False, False); end; end; -[Files] -#ifdef Dependency_Path_DirectX -Source: "{#Dependency_Path_DirectX}dxwebsetup.exe"; Flags: dontcopy noencryption -#endif +procedure Dependency_AddVSTORuntime; +begin + // https://learn.microsoft.com/en-us/visualstudio/vsto/how-to-install-the-visual-studio-tools-for-office-runtime-redistributable + if not RegKeyExists(HKLM, Dependency_String('SOFTWARE', 'SOFTWARE\WOW6432Node', 'SOFTWARE\WOW6432Node') + '\Microsoft\VSTO Runtime Setup\v4R') then begin + Dependency_Add('vstor_redist.exe', + '/q /norestart', + 'Visual Studio 2010 Tools for Office Runtime', + 'https://download.microsoft.com/download/5/d/2/5d24f8f8-efbb-4b63-aa33-3785e3104713/vstor_redist.exe', + '', False, False); + end; +end; + +var + Dependency_WinAppRuntimePackages: TArrayOfString; + Dependency_WinAppRuntimePackagesListed: Boolean; + +// the Windows App Runtime ships per channel side-by-side; apps need the channel they were built against +function Dependency_IsWinAppRuntimeInstalled(const Channel: String): Boolean; +var + ResultCode, LineIndex: Integer; + Output: TExecOutput; +begin + if not Dependency_WinAppRuntimePackagesListed then begin + Dependency_WinAppRuntimePackagesListed := True; + if ExecAndCaptureOutput('powershell.exe', '-NoProfile -ExecutionPolicy Bypass -Command "(Get-AppxPackage -AllUsers Microsoft.WindowsAppRuntime.*).Name"', '', SW_HIDE, ewWaitUntilTerminated, ResultCode, Output) and (ResultCode = 0) then begin + Dependency_WinAppRuntimePackages := Output.StdOut; + end; + end; + + for LineIndex := 0 to Length(Dependency_WinAppRuntimePackages) - 1 do begin + if Trim(Dependency_WinAppRuntimePackages[LineIndex]) = 'Microsoft.WindowsAppRuntime.' + Channel then begin + Result := True; + exit; + end; + end; + Result := False; +end; + +procedure Dependency_AddWinAppRuntime(const Channel, URL: String); +begin + // https://learn.microsoft.com/en-us/windows/apps/windows-app-sdk/downloads + if not Dependency_IsWinAppRuntimeInstalled(Channel) then begin + Dependency_Add('windowsappruntime' + Channel + Dependency_ArchSuffix + '.exe', + '--quiet', + 'Windows App Runtime ' + Channel + Dependency_ArchTitle, + URL, + '', False, False); + end; +end; + +procedure Dependency_AddWinAppRuntime20; begin Dependency_AddWinAppRuntime('2.0', Dependency_String('https://aka.ms/windowsappsdk/2.0/2.0.1/windowsappruntimeinstall-x86.exe', 'https://aka.ms/windowsappsdk/2.0/2.0.1/windowsappruntimeinstall-x64.exe', 'https://aka.ms/windowsappsdk/2.0/2.0.1/windowsappruntimeinstall-arm64.exe')); end; +procedure Dependency_AddWinAppRuntime21; begin Dependency_AddWinAppRuntime('2.1', Dependency_String('https://aka.ms/windowsappsdk/2.1/2.1.3/windowsappruntimeinstall-x86.exe', 'https://aka.ms/windowsappsdk/2.1/2.1.3/windowsappruntimeinstall-x64.exe', 'https://aka.ms/windowsappsdk/2.1/2.1.3/windowsappruntimeinstall-arm64.exe')); end; + +var + Dependency_JavaMajor: Integer; + Dependency_JavaMajorDetected: Boolean; + +function Dependency_GetJavaMajor: Integer; +var + JavaExe, Line: String; + ResultCode, LineIndex, QuotePos: Integer; + Output: TExecOutput; + Parts: TArrayOfString; +begin + if not Dependency_JavaMajorDetected then begin + Dependency_JavaMajorDetected := True; + Dependency_JavaMajor := 0; + + // detect whichever java.exe an app would actually use: JAVA_HOME, else PATH + JavaExe := GetEnv('JAVA_HOME'); + if (JavaExe <> '') and FileExists(JavaExe + '\bin\java.exe') then begin + JavaExe := JavaExe + '\bin\java.exe'; + end else begin + JavaExe := 'java.exe'; + end; + + // `java -version` prints to stderr + if ExecAndCaptureOutput(JavaExe, '-version', '', SW_HIDE, ewWaitUntilTerminated, ResultCode, Output) and (ResultCode = 0) then begin + for LineIndex := 0 to Length(Output.StdErr) - 1 do begin + Line := Output.StdErr[LineIndex]; + QuotePos := Pos('version "', Line); + if QuotePos > 0 then begin + Parts := StringSplit(Copy(Line, QuotePos + 9, Length(Line)), ['.'], stExcludeEmpty); + if Length(Parts) > 0 then begin + Dependency_JavaMajor := StrToIntDef(Parts[0], 0); + if (Dependency_JavaMajor = 1) and (Length(Parts) > 1) then begin + Dependency_JavaMajor := StrToIntDef(Parts[1], 0); // legacy "1.8.0_x" -> 8 + end; + end; + break; + end; + end; + end; + end; + + Result := Dependency_JavaMajor; +end; + +procedure Dependency_AddJava(const Major: Integer; const URL: String); +begin + // https://learn.microsoft.com/en-us/java/openjdk/download + if (URL <> '') and (Dependency_GetJavaMajor < Major) then begin + Dependency_Add('openjdk-' + IntToStr(Major) + Dependency_ArchSuffix + '.msi', + '/quiet /norestart ADDLOCAL=FeatureMain,FeatureEnvironment,FeatureJavaHome', + 'OpenJDK ' + IntToStr(Major) + Dependency_ArchTitle, + URL, + '', False, False); + end; +end; + +// Java 8 has no Microsoft build (and is still shipped 32-bit), so it comes from Eclipse Temurin +procedure Dependency_AddJava8; begin Dependency_AddJava(8, Dependency_String('https://api.adoptium.net/v3/installer/latest/8/ga/windows/x86/jdk/hotspot/normal/eclipse', 'https://api.adoptium.net/v3/installer/latest/8/ga/windows/x64/jdk/hotspot/normal/eclipse', 'https://api.adoptium.net/v3/installer/latest/8/ga/windows/x64/jdk/hotspot/normal/eclipse')); end; +procedure Dependency_AddJava11; begin Dependency_AddJava(11, Dependency_String('', 'https://aka.ms/download-jdk/microsoft-jdk-11-windows-x64.msi', 'https://aka.ms/download-jdk/microsoft-jdk-11-windows-aarch64.msi')); end; +procedure Dependency_AddJava17; begin Dependency_AddJava(17, Dependency_String('', 'https://aka.ms/download-jdk/microsoft-jdk-17-windows-x64.msi', 'https://aka.ms/download-jdk/microsoft-jdk-17-windows-aarch64.msi')); end; +procedure Dependency_AddJava21; begin Dependency_AddJava(21, Dependency_String('', 'https://aka.ms/download-jdk/microsoft-jdk-21-windows-x64.msi', 'https://aka.ms/download-jdk/microsoft-jdk-21-windows-aarch64.msi')); end; +procedure Dependency_AddJava25; begin Dependency_AddJava(25, Dependency_String('', 'https://aka.ms/download-jdk/microsoft-jdk-25-windows-x64.msi', 'https://aka.ms/download-jdk/microsoft-jdk-25-windows-aarch64.msi')); end; + +function Dependency_IsPythonInstalled(const Tag: String): Boolean; +begin + Result := RegKeyExists(HKLM, 'Software\Python\PythonCore\' + Tag + '\InstallPath') + or RegKeyExists(HKLM, 'Software\Wow6432Node\Python\PythonCore\' + Tag + '\InstallPath') + or RegKeyExists(HKCU, 'Software\Python\PythonCore\' + Tag + '\InstallPath'); +end; + +procedure Dependency_AddPython(const Minor, URL: String); +begin + // https://www.python.org/downloads/windows/ + if not Dependency_IsPythonInstalled(Minor + Dependency_String('-32', '', '-arm64')) then begin + Dependency_Add('python' + Minor + Dependency_ArchSuffix + '.exe', + Dependency_PassiveOrQuiet('/passive', '/quiet') + ' InstallAllUsers=1 PrependPath=1', + 'Python ' + Minor + Dependency_ArchTitle, + URL, + '', False, False); + end; +end; + +procedure Dependency_AddPython313; begin Dependency_AddPython('3.13', Dependency_String('https://www.python.org/ftp/python/3.13.13/python-3.13.13.exe', 'https://www.python.org/ftp/python/3.13.13/python-3.13.13-amd64.exe', 'https://www.python.org/ftp/python/3.13.13/python-3.13.13-arm64.exe')); end; + +procedure Dependency_AddPowerShell7; +begin + // https://github.com/PowerShell/PowerShell/releases + if not FileExists(ExpandConstant(Dependency_String('{commonpf32}', '{commonpf64}', '{commonpf64}')) + '\PowerShell\7\pwsh.exe') then begin + Dependency_Add('powershell7' + Dependency_ArchSuffix + '.msi', + Dependency_PassiveOrQuiet('/passive', '/quiet') + ' /norestart', + 'PowerShell 7.6.2' + Dependency_ArchTitle, + Dependency_String('https://github.com/PowerShell/PowerShell/releases/download/v7.6.2/PowerShell-7.6.2-win-x86.msi', 'https://github.com/PowerShell/PowerShell/releases/download/v7.6.2/PowerShell-7.6.2-win-x64.msi', 'https://github.com/PowerShell/PowerShell/releases/download/v7.6.2/PowerShell-7.6.2-win-arm64.msi'), + '', False, False); + end; +end; \ No newline at end of file From e0b3f5b69618d416f25f82873df5fdd072d67008 Mon Sep 17 00:00:00 2001 From: Joel Jothiprakasam Date: Thu, 11 Jun 2026 21:56:00 -0700 Subject: [PATCH 25/40] Test virustotal integration Signed-off-by: Joel Jothiprakasam --- .github/workflows/desktop-builds.yml | 60 ++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) diff --git a/.github/workflows/desktop-builds.yml b/.github/workflows/desktop-builds.yml index 1858941a84..486cb15a46 100644 --- a/.github/workflows/desktop-builds.yml +++ b/.github/workflows/desktop-builds.yml @@ -4,6 +4,10 @@ on: push: tags: - "v*" + # TEMP: remove after verifying the VirusTotal job — runs full builds on + # every push to this branch. + branches: + - "joel/2.0/desktop/2.0.0.0" workflow_dispatch: permissions: @@ -120,3 +124,59 @@ jobs: uses: softprops/action-gh-release@v3 with: files: bluebubbles-linux-${{ matrix.arch }}.tar.gz + + # Scans the user-facing downloads with VirusTotal and, on tag builds, + # appends the analysis links to the release notes. The msix is not scanned — + # it is MS Store-only, where Microsoft does its own vetting. Runs as a + # single job after all builds so only one writer touches the release body. + virustotal: + needs: [windows, linux] + runs-on: ubuntu-latest + timeout-minutes: 15 + env: + VT_API_KEY: ${{ secrets.VT_API_KEY }} + steps: + - name: Download artifacts + uses: actions/download-artifact@v8 + with: + path: artifacts + + - name: VirusTotal scan + id: virustotal + if: env.VT_API_KEY != '' + uses: crazy-max/ghaction-virustotal@v5 + with: + vt_api_key: ${{ env.VT_API_KEY }} + files: | + artifacts/bluebubbles-installer/bluebubbles_installer.exe + artifacts/bluebubbles-linux-x86_64/bluebubbles-linux-x86_64.tar.gz + artifacts/bluebubbles-linux-aarch64/bluebubbles-linux-aarch64.tar.gz + + # analysis output is comma-separated = pairs. + # The links go to the job summary on every run, and into vt_links.body + # for the release append on tag builds. + - name: Format scan links + id: vt_links + if: steps.virustotal.outputs.analysis != '' + env: + ANALYSIS: ${{ steps.virustotal.outputs.analysis }} + run: | + format_links() { + echo '### VirusTotal scans' + tr ',' '\n' <<< "$ANALYSIS" | while IFS='=' read -r file url; do + echo "- [\`$(basename "$file")\`]($url)" + done + } + format_links >> "$GITHUB_STEP_SUMMARY" + { + echo 'body<> "$GITHUB_OUTPUT" + + - name: Append to release notes + if: startsWith(github.ref, 'refs/tags/') && steps.vt_links.outputs.body != '' + uses: softprops/action-gh-release@v3 + with: + append_body: true + body: ${{ steps.vt_links.outputs.body }} From 08af168602cf4083527e5ce47de3efaa4a4d48ac Mon Sep 17 00:00:00 2001 From: Joel Jothiprakasam Date: Thu, 11 Jun 2026 22:24:18 -0700 Subject: [PATCH 26/40] Use vs2026 Signed-off-by: Joel Jothiprakasam --- .github/workflows/desktop-builds.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/desktop-builds.yml b/.github/workflows/desktop-builds.yml index 486cb15a46..aad55f9ea0 100644 --- a/.github/workflows/desktop-builds.yml +++ b/.github/workflows/desktop-builds.yml @@ -15,7 +15,7 @@ permissions: jobs: windows: - runs-on: windows-latest + runs-on: windows-2025-vs2026 timeout-minutes: 60 steps: - name: Checkout code @@ -38,7 +38,7 @@ jobs: key: pub-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('pubspec.lock') }} # Inno Setup 6 and Rust (for super_native_extensions) are preinstalled - # on the windows-latest runner image. + # on the windows-2025-vs2026 runner image. - name: Build run: .\windows\build.ps1 shell: pwsh From 0f86a32fad4f5ba9ccb65dbb30c061e518de08c4 Mon Sep 17 00:00:00 2001 From: Joel Jothiprakasam Date: Thu, 11 Jun 2026 22:57:25 -0700 Subject: [PATCH 27/40] Test splitting debug info Signed-off-by: Joel Jothiprakasam --- .github/workflows/desktop-builds.yml | 10 ++++++++-- pubspec.yaml | 1 + 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/.github/workflows/desktop-builds.yml b/.github/workflows/desktop-builds.yml index aad55f9ea0..e2650e089c 100644 --- a/.github/workflows/desktop-builds.yml +++ b/.github/workflows/desktop-builds.yml @@ -15,7 +15,7 @@ permissions: jobs: windows: - runs-on: windows-2025-vs2026 + runs-on: windows-latest timeout-minutes: 60 steps: - name: Checkout code @@ -38,7 +38,7 @@ jobs: key: pub-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('pubspec.lock') }} # Inno Setup 6 and Rust (for super_native_extensions) are preinstalled - # on the windows-2025-vs2026 runner image. + # on the windows-latest runner image. - name: Build run: .\windows\build.ps1 shell: pwsh @@ -49,6 +49,12 @@ jobs: name: bluebubbles-installer path: windows/bluebubbles_installer.exe + - name: Upload debug symbols + uses: actions/upload-artifact@v7 + with: + name: bluebubbles-windows-symbols + path: build/windows/symbols/ + # The msix is for the MS Store only — uploaded as its own run artifact # for easy download, never attached to releases. - name: Upload msix artifact diff --git a/pubspec.yaml b/pubspec.yaml index 87035414a4..96f449db8d 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -324,6 +324,7 @@ flutter: # see https://flutter.dev/custom-fonts/#from-packages msix_config: + windows_build_args: --split-debug-info=build/windows/symbols output_path: windows display_name: BlueBubbles publisher_display_name: BlueBubbles From b0a70029420518573513275375c7d0358f5462a6 Mon Sep 17 00:00:00 2001 From: Joel Jothiprakasam Date: Thu, 11 Jun 2026 23:15:38 -0700 Subject: [PATCH 28/40] Lmao why did that work. Remove testing Signed-off-by: Joel Jothiprakasam --- .github/workflows/desktop-builds.yml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.github/workflows/desktop-builds.yml b/.github/workflows/desktop-builds.yml index e2650e089c..316fbab3d7 100644 --- a/.github/workflows/desktop-builds.yml +++ b/.github/workflows/desktop-builds.yml @@ -4,10 +4,6 @@ on: push: tags: - "v*" - # TEMP: remove after verifying the VirusTotal job — runs full builds on - # every push to this branch. - branches: - - "joel/2.0/desktop/2.0.0.0" workflow_dispatch: permissions: From f698838e148fea0c2f3a3f212d4fbf1711652bb7 Mon Sep 17 00:00:00 2001 From: Joel Jothiprakasam Date: Fri, 19 Jun 2026 14:25:31 -0700 Subject: [PATCH 29/40] Test signpath signing Signed-off-by: Joel Jothiprakasam --- .github/workflows/desktop-builds.yml | 165 +++++++++++++++++++++------ pubspec.yaml | 1 - windows/CLAUDE.md | 6 +- windows/build.ps1 | 28 ++++- 4 files changed, 161 insertions(+), 39 deletions(-) diff --git a/.github/workflows/desktop-builds.yml b/.github/workflows/desktop-builds.yml index 316fbab3d7..59ab93220f 100644 --- a/.github/workflows/desktop-builds.yml +++ b/.github/workflows/desktop-builds.yml @@ -2,17 +2,45 @@ name: Desktop Builds on: push: + # TEMP: run on this branch until the workflow lands on master (workflow_dispatch + # only works from the default branch). Remove the branches list after merge. + branches: + - joel/2.0/desktop/2.0.0.0 tags: - "v*" workflow_dispatch: + inputs: + sign: + description: "Run SignPath signing on this manual build (test the signing pipeline without a release tag)" + type: boolean + default: false permissions: contents: write # attach artifacts to releases on tag builds +# Code signing is handled by SignPath. +# TEMP wiring: the test-vs-release config is hardcoded by event for now — +# branch push (this branch) -> test-signing policy + test cert +# v* tag -> release-signing policy + release cert +# (signing-policy-slug and SIGNED_MSIX_PUBLISHER below). This will be parameterized +# back into repo vars later. +# Still required in repo settings (shared by both): +# Secret SIGNPATH_API_TOKEN — SignPath CI user API token +# Var SIGNPATH_ORGANIZATION_ID — SignPath organization id (GUID) +# Var SIGNPATH_PROJECT_SLUG — SignPath project slug +# Var SIGNPATH_ARTIFACT_CONFIG_INSTALLER — artifact configuration slug for the Inno installer +# Var SIGNPATH_ARTIFACT_CONFIG_MSIX — artifact configuration slug for the msix +# When SIGNPATH_API_TOKEN is unset, signing is skipped and nothing is attached to releases. + jobs: windows: runs-on: windows-latest timeout-minutes: 60 + env: + SIGNPATH_API_TOKEN: ${{ secrets.SIGNPATH_API_TOKEN }} + # TEMP: release cert on tags, test cert on branch pushes (baked into the msix at build time). + SIGNED_MSIX_PUBLISHER: "${{ startsWith(github.ref, 'refs/tags/') && 'CN=SignPath Foundation,O=SignPath Foundation,C=US,S=DE,L=Lewes' || 'CN=Test certificate for ''BlueBubbles [OSS]''' }}" + SIGNED_MSIX_IDENTITY: BlueBubbles.BlueBubbles steps: - name: Checkout code uses: actions/checkout@v6 @@ -40,30 +68,98 @@ jobs: shell: pwsh - name: Upload installer artifact + id: upload-installer uses: actions/upload-artifact@v7 with: name: bluebubbles-installer path: windows/bluebubbles_installer.exe + # The directly-distributed msix, still unsigned — SignPath signs it below. + # Only built when SIGNED_MSIX_PUBLISHER is configured. + - name: Upload msix artifact (to be signed) + id: upload-msix + if: ${{ hashFiles('windows/bluebubbles-signed.msix') != '' }} + uses: actions/upload-artifact@v7 + with: + name: bluebubbles-msix + path: windows/bluebubbles-signed.msix + - name: Upload debug symbols uses: actions/upload-artifact@v7 with: name: bluebubbles-windows-symbols path: build/windows/symbols/ - # The msix is for the MS Store only — uploaded as its own run artifact - # for easy download, never attached to releases. - - name: Upload msix artifact + # The store msix is for the MS Store only — uploaded as its own run artifact + # for easy download, never signed by SignPath or attached to releases. + - name: Upload store msix artifact uses: actions/upload-artifact@v7 with: - name: bluebubbles-msix + name: bluebubbles-msix-store path: windows/bluebubbles.msix - - name: Attach to release - if: startsWith(github.ref, 'refs/tags/') + # --- SignPath signing: tag builds, or manual builds with sign=true --- + # SignPath downloads the artifact uploaded above (by id), signs it per the + # project's artifact configuration, and extracts the signed result locally. + - name: Sign installer (SignPath) + id: sign-installer + if: ${{ env.SIGNPATH_API_TOKEN != '' && (startsWith(github.ref, 'refs/tags/') || github.ref == 'refs/heads/joel/2.0/desktop/2.0.0.0' || inputs.sign) }} + uses: signpath/github-action-submit-signing-request@v2 + with: + api-token: ${{ env.SIGNPATH_API_TOKEN }} + organization-id: ${{ vars.SIGNPATH_ORGANIZATION_ID }} + project-slug: ${{ vars.SIGNPATH_PROJECT_SLUG }} + # TEMP: release-signing on tags, test-signing on branch pushes. + signing-policy-slug: ${{ startsWith(github.ref, 'refs/tags/') && 'release-signing' || 'test-signing' }} + artifact-configuration-slug: ${{ vars.SIGNPATH_ARTIFACT_CONFIG_INSTALLER }} + github-artifact-id: ${{ steps.upload-installer.outputs.artifact-id }} + wait-for-completion: true + output-artifact-directory: signed/installer + + - name: Sign msix (SignPath) + id: sign-msix + if: ${{ env.SIGNPATH_API_TOKEN != '' && steps.upload-msix.outputs.artifact-id != '' && (startsWith(github.ref, 'refs/tags/') || github.ref == 'refs/heads/joel/2.0/desktop/2.0.0.0' || inputs.sign) }} + uses: signpath/github-action-submit-signing-request@v2 + with: + api-token: ${{ env.SIGNPATH_API_TOKEN }} + organization-id: ${{ vars.SIGNPATH_ORGANIZATION_ID }} + project-slug: ${{ vars.SIGNPATH_PROJECT_SLUG }} + # TEMP: release-signing on tags, test-signing on branch pushes. + signing-policy-slug: ${{ startsWith(github.ref, 'refs/tags/') && 'release-signing' || 'test-signing' }} + artifact-configuration-slug: ${{ vars.SIGNPATH_ARTIFACT_CONFIG_MSIX }} + github-artifact-id: ${{ steps.upload-msix.outputs.artifact-id }} + wait-for-completion: true + output-artifact-directory: signed/msix + + # Re-upload the signed results under their own names so VirusTotal and the + # release step can pick them up. Output paths follow the SignPath artifact + # configuration — adjust if yours nests the files differently. + - name: Upload signed installer + if: ${{ steps.sign-installer.outcome == 'success' }} + uses: actions/upload-artifact@v7 + with: + name: bluebubbles-installer-signed + path: signed/installer/bluebubbles_installer.exe + + - name: Upload signed msix + if: ${{ steps.sign-msix.outcome == 'success' }} + uses: actions/upload-artifact@v7 + with: + name: bluebubbles-msix-signed + path: signed/msix/bluebubbles-signed.msix + + # On tag builds, the signed artifacts are what gets released. + - name: Attach signed installer to release + if: ${{ startsWith(github.ref, 'refs/tags/') && steps.sign-installer.outcome == 'success' }} + uses: softprops/action-gh-release@v3 + with: + files: signed/installer/bluebubbles_installer.exe + + - name: Attach signed msix to release + if: ${{ startsWith(github.ref, 'refs/tags/') && steps.sign-msix.outcome == 'success' }} uses: softprops/action-gh-release@v3 with: - files: windows/bluebubbles_installer.exe + files: signed/msix/bluebubbles-signed.msix linux: strategy: @@ -127,10 +223,11 @@ jobs: with: files: bluebubbles-linux-${{ matrix.arch }}.tar.gz - # Scans the user-facing downloads with VirusTotal and, on tag builds, - # appends the analysis links to the release notes. The msix is not scanned — - # it is MS Store-only, where Microsoft does its own vetting. Runs as a - # single job after all builds so only one writer touches the release body. + # Scans the user-facing downloads with VirusTotal and writes the analysis links + # to the job summary. Prefers the SignPath-signed installer/msix when present (so + # the released binaries are what gets scanned); falls back to the unsigned + # installer otherwise. The store msix is not scanned — Microsoft vets Store + # submissions. virustotal: needs: [windows, linux] runs-on: ubuntu-latest @@ -143,42 +240,44 @@ jobs: with: path: artifacts + - name: Select files to scan + id: select + run: | + files=() + if [ -f artifacts/bluebubbles-installer-signed/bluebubbles_installer.exe ]; then + files+=("artifacts/bluebubbles-installer-signed/bluebubbles_installer.exe") + else + files+=("artifacts/bluebubbles-installer/bluebubbles_installer.exe") + fi + [ -f artifacts/bluebubbles-msix-signed/bluebubbles-signed.msix ] && \ + files+=("artifacts/bluebubbles-msix-signed/bluebubbles-signed.msix") + [ -f artifacts/bluebubbles-linux-x86_64/bluebubbles-linux-x86_64.tar.gz ] && \ + files+=("artifacts/bluebubbles-linux-x86_64/bluebubbles-linux-x86_64.tar.gz") + [ -f artifacts/bluebubbles-linux-aarch64/bluebubbles-linux-aarch64.tar.gz ] && \ + files+=("artifacts/bluebubbles-linux-aarch64/bluebubbles-linux-aarch64.tar.gz") + { + echo 'files<> "$GITHUB_OUTPUT" + - name: VirusTotal scan id: virustotal if: env.VT_API_KEY != '' uses: crazy-max/ghaction-virustotal@v5 with: vt_api_key: ${{ env.VT_API_KEY }} - files: | - artifacts/bluebubbles-installer/bluebubbles_installer.exe - artifacts/bluebubbles-linux-x86_64/bluebubbles-linux-x86_64.tar.gz - artifacts/bluebubbles-linux-aarch64/bluebubbles-linux-aarch64.tar.gz + files: ${{ steps.select.outputs.files }} # analysis output is comma-separated = pairs. - # The links go to the job summary on every run, and into vt_links.body - # for the release append on tag builds. - name: Format scan links - id: vt_links if: steps.virustotal.outputs.analysis != '' env: ANALYSIS: ${{ steps.virustotal.outputs.analysis }} run: | - format_links() { + { echo '### VirusTotal scans' tr ',' '\n' <<< "$ANALYSIS" | while IFS='=' read -r file url; do echo "- [\`$(basename "$file")\`]($url)" done - } - format_links >> "$GITHUB_STEP_SUMMARY" - { - echo 'body<> "$GITHUB_OUTPUT" - - - name: Append to release notes - if: startsWith(github.ref, 'refs/tags/') && steps.vt_links.outputs.body != '' - uses: softprops/action-gh-release@v3 - with: - append_body: true - body: ${{ steps.vt_links.outputs.body }} + } >> "$GITHUB_STEP_SUMMARY" diff --git a/pubspec.yaml b/pubspec.yaml index 96f449db8d..64a16d99aa 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -333,7 +333,6 @@ msix_config: publisher: CN=BEC9154D-191E-4375-BF30-698BD4C141C4 vs_generated_images_folder_path: windows/icons logo_path: assets/icon/icon.ico - store: true icons_background_color: transparent languages: en-us architecture: x64 diff --git a/windows/CLAUDE.md b/windows/CLAUDE.md index 8d67dc3122..ee431f0725 100644 --- a/windows/CLAUDE.md +++ b/windows/CLAUDE.md @@ -7,9 +7,13 @@ - `utils.cpp/h` — UTF-8 / UTF-16 helpers ## Installer -- `build.ps1` — release build script: cleans Release dir, `dart run msix:create`, then compiles the installer (mirrors `linux/build.sh`) +- `build.ps1` — release build script: cleans Release dir, builds two MSIX variants, then compiles the installer (mirrors `linux/build.sh`) + - `--store` build → `bluebubbles.msix` (MS Store; Microsoft signs it; store identity/publisher from `pubspec.yaml`) + - signed build → `bluebubbles-signed.msix` (directly distributed; unsigned, SignPath signs it in CI). Only built when `SIGNED_MSIX_PUBLISHER` env is set; that value must equal the SignPath cert's subject DN. + - `store:` is deliberately absent from `pubspec.yaml` (msix forces store mode if present and can't be overridden via CLI); pass `--store` for the store build instead. - `bluebubbles_installer_script.iss` — Inno Setup installer definition - `CodeDependencies.iss` — installer dependency declarations +- SignPath code signing is wired in `.github/workflows/desktop-builds.yml` (installer + signed msix, on tags or manual `sign=true`) ## Key Flutter-Side Files for Windows - `lib/utils/window_effects.dart` — Mica/acrylic transparency (`flutter_acrylic`) diff --git a/windows/build.ps1 b/windows/build.ps1 index aa6a8e109f..29d84e52e2 100644 --- a/windows/build.ps1 +++ b/windows/build.ps1 @@ -1,7 +1,8 @@ # Windows release build script. Run from the root of the repository. Requires Inno Setup 6 to be installed. # Builds the app, packages the MSIX, then compiles the Inno Setup installer. # Outputs: -# windows\bluebubbles.msix (internal use only — not attached to releases) +# windows\bluebubbles.msix (MS Store submission only — not attached to releases) +# windows\bluebubbles-signed.msix (directly-distributed, unsigned; SignPath signs it in CI — only when SIGNED_MSIX_PUBLISHER is set) # windows\bluebubbles_installer.exe $ErrorActionPreference = 'Stop' @@ -38,10 +39,29 @@ if (Test-Path $releaseDir) { Remove-Item $releaseDir -Recurse -Force } Invoke-Checked $flutterCmd pub get --enforce-lockfile -# Runs `flutter build windows --release` and packages the result as an MSIX -Invoke-Checked $dartCmd run msix:create +# Runs `flutter build windows --release` and packages the result as the MS Store +# MSIX (windows\bluebubbles.msix). Microsoft signs this one, so pass --store +# explicitly (store mode is no longer set in pubspec.yaml). +Invoke-Checked $dartCmd run msix:create --store + +# Build the directly-distributed MSIX, left unsigned for SignPath to sign in CI. +# Reuses the Release output from the store build above. SIGNED_MSIX_PUBLISHER must +# equal the SignPath certificate's subject DN, or Windows will reject the signature. +# Skipped when unset (e.g. local builds without signing configured). +if ($env:SIGNED_MSIX_PUBLISHER) { + $signedArgs = @( + '--build-windows', 'false', + '--sign-msix', 'false', + '--publisher', $env:SIGNED_MSIX_PUBLISHER, + '--output-name', 'bluebubbles-signed' + ) + if ($env:SIGNED_MSIX_IDENTITY) { $signedArgs += @('--identity-name', $env:SIGNED_MSIX_IDENTITY) } + Invoke-Checked $dartCmd run msix:create @signedArgs +} # Compile the Inno Setup installer Invoke-Checked @($iscc) 'windows\bluebubbles_installer_script.iss' -Get-FileHash 'windows\bluebubbles.msix', 'windows\bluebubbles_installer.exe' -Algorithm SHA256 | Format-List Path, Hash +$hashTargets = @('windows\bluebubbles.msix', 'windows\bluebubbles_installer.exe') +if (Test-Path 'windows\bluebubbles-signed.msix') { $hashTargets += 'windows\bluebubbles-signed.msix' } +Get-FileHash $hashTargets -Algorithm SHA256 | Format-List Path, Hash From 379a8bd0ca696d824afeb5d555bd665164b68031 Mon Sep 17 00:00:00 2001 From: Joel Jothiprakasam Date: Fri, 19 Jun 2026 15:23:18 -0700 Subject: [PATCH 30/40] Rename unsigned builds Signed-off-by: Joel Jothiprakasam --- .github/workflows/desktop-builds.yml | 81 ++++++++++++---------------- windows/CLAUDE.md | 6 +-- windows/build.ps1 | 22 ++++---- 3 files changed, 49 insertions(+), 60 deletions(-) diff --git a/.github/workflows/desktop-builds.yml b/.github/workflows/desktop-builds.yml index 59ab93220f..8e433ee754 100644 --- a/.github/workflows/desktop-builds.yml +++ b/.github/workflows/desktop-builds.yml @@ -2,7 +2,7 @@ name: Desktop Builds on: push: - # TEMP: run on this branch until the workflow lands on master (workflow_dispatch + # TEMP: build this branch until the workflow lands on master (workflow_dispatch # only works from the default branch). Remove the branches list after merge. branches: - joel/2.0/desktop/2.0.0.0 @@ -18,13 +18,10 @@ on: permissions: contents: write # attach artifacts to releases on tag builds -# Code signing is handled by SignPath. -# TEMP wiring: the test-vs-release config is hardcoded by event for now — -# branch push (this branch) -> test-signing policy + test cert -# v* tag -> release-signing policy + release cert -# (signing-policy-slug and SIGNED_MSIX_PUBLISHER below). This will be parameterized -# back into repo vars later. -# Still required in repo settings (shared by both): +# SignPath code signing. TEMP: test-vs-release config is hardcoded by event for now — +# branch push -> test-signing policy + test cert; v* tag -> release-signing + release cert +# (signing-policy-slug and SIGNED_MSIX_PUBLISHER below). To be parameterized into repo vars later. +# Required repo settings (shared by both): # Secret SIGNPATH_API_TOKEN — SignPath CI user API token # Var SIGNPATH_ORGANIZATION_ID — SignPath organization id (GUID) # Var SIGNPATH_PROJECT_SLUG — SignPath project slug @@ -67,22 +64,22 @@ jobs: run: .\windows\build.ps1 shell: pwsh - - name: Upload installer artifact + - name: Upload unsigned installer artifact id: upload-installer uses: actions/upload-artifact@v7 with: - name: bluebubbles-installer + name: bluebubbles-installer-unsigned path: windows/bluebubbles_installer.exe - # The directly-distributed msix, still unsigned — SignPath signs it below. + # The directly-distributed msix, unsigned until SignPath signs it below. # Only built when SIGNED_MSIX_PUBLISHER is configured. - - name: Upload msix artifact (to be signed) + - name: Upload unsigned msix artifact id: upload-msix - if: ${{ hashFiles('windows/bluebubbles-signed.msix') != '' }} + if: ${{ hashFiles('windows/bluebubbles.msix') != '' }} uses: actions/upload-artifact@v7 with: - name: bluebubbles-msix - path: windows/bluebubbles-signed.msix + name: bluebubbles-msix-unsigned + path: windows/bluebubbles.msix - name: Upload debug symbols uses: actions/upload-artifact@v7 @@ -90,17 +87,14 @@ jobs: name: bluebubbles-windows-symbols path: build/windows/symbols/ - # The store msix is for the MS Store only — uploaded as its own run artifact - # for easy download, never signed by SignPath or attached to releases. + # Store msix — uploaded for MS Store submission only, never released here. - name: Upload store msix artifact uses: actions/upload-artifact@v7 with: name: bluebubbles-msix-store - path: windows/bluebubbles.msix + path: windows/bluebubbles-store.msix # --- SignPath signing: tag builds, or manual builds with sign=true --- - # SignPath downloads the artifact uploaded above (by id), signs it per the - # project's artifact configuration, and extracts the signed result locally. - name: Sign installer (SignPath) id: sign-installer if: ${{ env.SIGNPATH_API_TOKEN != '' && (startsWith(github.ref, 'refs/tags/') || github.ref == 'refs/heads/joel/2.0/desktop/2.0.0.0' || inputs.sign) }} @@ -114,7 +108,7 @@ jobs: artifact-configuration-slug: ${{ vars.SIGNPATH_ARTIFACT_CONFIG_INSTALLER }} github-artifact-id: ${{ steps.upload-installer.outputs.artifact-id }} wait-for-completion: true - output-artifact-directory: signed/installer + output-artifact-directory: out/installer - name: Sign msix (SignPath) id: sign-msix @@ -129,37 +123,34 @@ jobs: artifact-configuration-slug: ${{ vars.SIGNPATH_ARTIFACT_CONFIG_MSIX }} github-artifact-id: ${{ steps.upload-msix.outputs.artifact-id }} wait-for-completion: true - output-artifact-directory: signed/msix + output-artifact-directory: out/msix - # Re-upload the signed results under their own names so VirusTotal and the - # release step can pick them up. Output paths follow the SignPath artifact - # configuration — adjust if yours nests the files differently. - - name: Upload signed installer + # Output paths follow the SignPath artifact configuration — adjust if yours nests differently. + - name: Upload installer if: ${{ steps.sign-installer.outcome == 'success' }} uses: actions/upload-artifact@v7 with: - name: bluebubbles-installer-signed - path: signed/installer/bluebubbles_installer.exe + name: bluebubbles-installer + path: out/installer/bluebubbles_installer.exe - - name: Upload signed msix + - name: Upload msix if: ${{ steps.sign-msix.outcome == 'success' }} uses: actions/upload-artifact@v7 with: - name: bluebubbles-msix-signed - path: signed/msix/bluebubbles-signed.msix + name: bluebubbles-msix + path: out/msix/bluebubbles.msix - # On tag builds, the signed artifacts are what gets released. - - name: Attach signed installer to release + - name: Attach installer to release if: ${{ startsWith(github.ref, 'refs/tags/') && steps.sign-installer.outcome == 'success' }} uses: softprops/action-gh-release@v3 with: - files: signed/installer/bluebubbles_installer.exe + files: out/installer/bluebubbles_installer.exe - - name: Attach signed msix to release + - name: Attach msix to release if: ${{ startsWith(github.ref, 'refs/tags/') && steps.sign-msix.outcome == 'success' }} uses: softprops/action-gh-release@v3 with: - files: signed/msix/bluebubbles-signed.msix + files: out/msix/bluebubbles.msix linux: strategy: @@ -223,11 +214,9 @@ jobs: with: files: bluebubbles-linux-${{ matrix.arch }}.tar.gz - # Scans the user-facing downloads with VirusTotal and writes the analysis links - # to the job summary. Prefers the SignPath-signed installer/msix when present (so - # the released binaries are what gets scanned); falls back to the unsigned - # installer otherwise. The store msix is not scanned — Microsoft vets Store - # submissions. + # Scans the released downloads with VirusTotal and writes the analysis links to the + # job summary. Prefers the released installer/msix when present; falls back to the + # unsigned installer otherwise. The store msix is not scanned — Microsoft vets it. virustotal: needs: [windows, linux] runs-on: ubuntu-latest @@ -244,13 +233,13 @@ jobs: id: select run: | files=() - if [ -f artifacts/bluebubbles-installer-signed/bluebubbles_installer.exe ]; then - files+=("artifacts/bluebubbles-installer-signed/bluebubbles_installer.exe") - else + if [ -f artifacts/bluebubbles-installer/bluebubbles_installer.exe ]; then files+=("artifacts/bluebubbles-installer/bluebubbles_installer.exe") + else + files+=("artifacts/bluebubbles-installer-unsigned/bluebubbles_installer.exe") fi - [ -f artifacts/bluebubbles-msix-signed/bluebubbles-signed.msix ] && \ - files+=("artifacts/bluebubbles-msix-signed/bluebubbles-signed.msix") + [ -f artifacts/bluebubbles-msix/bluebubbles.msix ] && \ + files+=("artifacts/bluebubbles-msix/bluebubbles.msix") [ -f artifacts/bluebubbles-linux-x86_64/bluebubbles-linux-x86_64.tar.gz ] && \ files+=("artifacts/bluebubbles-linux-x86_64/bluebubbles-linux-x86_64.tar.gz") [ -f artifacts/bluebubbles-linux-aarch64/bluebubbles-linux-aarch64.tar.gz ] && \ diff --git a/windows/CLAUDE.md b/windows/CLAUDE.md index ee431f0725..23e1916662 100644 --- a/windows/CLAUDE.md +++ b/windows/CLAUDE.md @@ -8,12 +8,12 @@ ## Installer - `build.ps1` — release build script: cleans Release dir, builds two MSIX variants, then compiles the installer (mirrors `linux/build.sh`) - - `--store` build → `bluebubbles.msix` (MS Store; Microsoft signs it; store identity/publisher from `pubspec.yaml`) - - signed build → `bluebubbles-signed.msix` (directly distributed; unsigned, SignPath signs it in CI). Only built when `SIGNED_MSIX_PUBLISHER` env is set; that value must equal the SignPath cert's subject DN. + - `--store` build → `bluebubbles-store.msix` (MS Store; Microsoft signs it; store identity/publisher from `pubspec.yaml`) + - sideload build → `bluebubbles.msix` (directly distributed; unsigned, SignPath signs it in CI). Only built when `SIGNED_MSIX_PUBLISHER` env is set; that value must equal the SignPath cert's subject DN. - `store:` is deliberately absent from `pubspec.yaml` (msix forces store mode if present and can't be overridden via CLI); pass `--store` for the store build instead. - `bluebubbles_installer_script.iss` — Inno Setup installer definition - `CodeDependencies.iss` — installer dependency declarations -- SignPath code signing is wired in `.github/workflows/desktop-builds.yml` (installer + signed msix, on tags or manual `sign=true`) +- SignPath code signing is wired in `.github/workflows/desktop-builds.yml` (installer + msix, on tags or manual `sign=true`) ## Key Flutter-Side Files for Windows - `lib/utils/window_effects.dart` — Mica/acrylic transparency (`flutter_acrylic`) diff --git a/windows/build.ps1 b/windows/build.ps1 index 29d84e52e2..6295d81e98 100644 --- a/windows/build.ps1 +++ b/windows/build.ps1 @@ -1,8 +1,8 @@ # Windows release build script. Run from the root of the repository. Requires Inno Setup 6 to be installed. # Builds the app, packages the MSIX, then compiles the Inno Setup installer. # Outputs: -# windows\bluebubbles.msix (MS Store submission only — not attached to releases) -# windows\bluebubbles-signed.msix (directly-distributed, unsigned; SignPath signs it in CI — only when SIGNED_MSIX_PUBLISHER is set) +# windows\bluebubbles-store.msix (MS Store submission only — not attached to releases) +# windows\bluebubbles.msix (directly-distributed, unsigned; SignPath signs it in CI — only when SIGNED_MSIX_PUBLISHER is set) # windows\bluebubbles_installer.exe $ErrorActionPreference = 'Stop' @@ -39,29 +39,29 @@ if (Test-Path $releaseDir) { Remove-Item $releaseDir -Recurse -Force } Invoke-Checked $flutterCmd pub get --enforce-lockfile -# Runs `flutter build windows --release` and packages the result as the MS Store -# MSIX (windows\bluebubbles.msix). Microsoft signs this one, so pass --store +# Runs `flutter build windows --release` and packages the MS Store MSIX +# (windows\bluebubbles-store.msix). Microsoft signs this one, so pass --store # explicitly (store mode is no longer set in pubspec.yaml). -Invoke-Checked $dartCmd run msix:create --store +Invoke-Checked $dartCmd run msix:create --store --output-name bluebubbles-store # Build the directly-distributed MSIX, left unsigned for SignPath to sign in CI. # Reuses the Release output from the store build above. SIGNED_MSIX_PUBLISHER must # equal the SignPath certificate's subject DN, or Windows will reject the signature. # Skipped when unset (e.g. local builds without signing configured). if ($env:SIGNED_MSIX_PUBLISHER) { - $signedArgs = @( + $msixArgs = @( '--build-windows', 'false', '--sign-msix', 'false', '--publisher', $env:SIGNED_MSIX_PUBLISHER, - '--output-name', 'bluebubbles-signed' + '--output-name', 'bluebubbles' ) - if ($env:SIGNED_MSIX_IDENTITY) { $signedArgs += @('--identity-name', $env:SIGNED_MSIX_IDENTITY) } - Invoke-Checked $dartCmd run msix:create @signedArgs + if ($env:SIGNED_MSIX_IDENTITY) { $msixArgs += @('--identity-name', $env:SIGNED_MSIX_IDENTITY) } + Invoke-Checked $dartCmd run msix:create @msixArgs } # Compile the Inno Setup installer Invoke-Checked @($iscc) 'windows\bluebubbles_installer_script.iss' -$hashTargets = @('windows\bluebubbles.msix', 'windows\bluebubbles_installer.exe') -if (Test-Path 'windows\bluebubbles-signed.msix') { $hashTargets += 'windows\bluebubbles-signed.msix' } +$hashTargets = @('windows\bluebubbles-store.msix', 'windows\bluebubbles_installer.exe') +if (Test-Path 'windows\bluebubbles.msix') { $hashTargets += 'windows\bluebubbles.msix' } Get-FileHash $hashTargets -Algorithm SHA256 | Format-List Path, Hash From 078ac79a43d632a2bc191faf4ba75f14b48300a4 Mon Sep 17 00:00:00 2001 From: Joel Jothiprakasam Date: Fri, 19 Jun 2026 15:26:01 -0700 Subject: [PATCH 31/40] Change msix detection to support sideloading Signed-off-by: Joel Jothiprakasam --- lib/helpers/types/constants.dart | 2 -- lib/helpers/types/helpers/misc_helpers.dart | 4 +--- .../backend/filesystem/filesystem_service.dart | 14 +++++++++----- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/lib/helpers/types/constants.dart b/lib/helpers/types/constants.dart index c8817a620f..dfd5e23bef 100644 --- a/lib/helpers/types/constants.dart +++ b/lib/helpers/types/constants.dart @@ -227,8 +227,6 @@ final RegExp emojiRegex = RegExp( const bigEmojiScaleFactor = 3.0; const appName = "BlueBubbles"; -const msStorePackageName = "23344BlueBubbles.BlueBubbles"; -const windowsAppPackageName = "23344BlueBubbles.BlueBubbles_2fva2ntdzvhtw!bluebubbles"; const randomAvatarBackgroundColors = [ // Pinks diff --git a/lib/helpers/types/helpers/misc_helpers.dart b/lib/helpers/types/helpers/misc_helpers.dart index 1343188fb7..d510c1e1a6 100644 --- a/lib/helpers/types/helpers/misc_helpers.dart +++ b/lib/helpers/types/helpers/misc_helpers.dart @@ -1,5 +1,4 @@ import 'package:async_task/async_task.dart'; -import 'package:bluebubbles/helpers/types/constants.dart'; import 'package:bluebubbles/utils/logger/task_logger.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; @@ -55,8 +54,7 @@ bool get isFlatpak => !kIsWeb && Platform.isLinux && Platform.environment.contai bool get isMsix => !kIsWeb && Platform.isWindows && - Platform.resolvedExecutable.contains('WindowsApps') && - Platform.resolvedExecutable.contains(msStorePackageName); + Platform.resolvedExecutable.contains('WindowsApps'); /// From https://github.com/modulovalue/dart_intersperse/blob/master/lib/src/intersperse.dart Iterable intersperse(T element, Iterable iterable) sync* { diff --git a/lib/services/backend/filesystem/filesystem_service.dart b/lib/services/backend/filesystem/filesystem_service.dart index 80c4dbc15b..9a87299d8c 100644 --- a/lib/services/backend/filesystem/filesystem_service.dart +++ b/lib/services/backend/filesystem/filesystem_service.dart @@ -133,14 +133,18 @@ class FilesystemService { appDocDir = (kIsDesktop ? await getApplicationSupportDirectory() : await getApplicationDocumentsDirectory()); if (isMsix) { final String appDataRoot = joinAll(split(appDocDir.absolute.path).slice(0, 4)); - final Directory msStoreLocation = Directory(join(appDataRoot, "Local", "Packages", - "23344BlueBubbles.BlueBubbles_2fva2ntdzvhtw", "LocalCache", "Roaming", "BlueBubbles", "bluebubbles")); + // Family name (Name_PublisherHash) from the install dir; hash varies per variant. + final exeSegments = split(Platform.resolvedExecutable); + final fullNameParts = exeSegments[exeSegments.indexOf('WindowsApps') + 1].split('_'); + final packageFamilyName = '${fullNameParts.first}_${fullNameParts.last}'; + final Directory msixLocation = Directory(join(appDataRoot, "Local", "Packages", + packageFamilyName, "LocalCache", "Roaming", "BlueBubbles", "bluebubbles")); // Check if the non-msix directory exists final Directory nonMsixLocation = Directory(join(appDataRoot, "Roaming", "BlueBubbles", "bluebubbles")); - if (!msStoreLocation.existsSync() && nonMsixLocation.existsSync()) { - await copyPath(nonMsixLocation.path, msStoreLocation.path); + if (!msixLocation.existsSync() && nonMsixLocation.existsSync()) { + await copyPath(nonMsixLocation.path, msixLocation.path); } - appDocDir = msStoreLocation; + appDocDir = msixLocation; } if (!headless) { final file = await rootBundle.load("assets/images/no-video-preview.png"); From 23b858237523d4ba287cf8f1fdd9c8d640384688 Mon Sep 17 00:00:00 2001 From: Joel Jothiprakasam Date: Fri, 19 Jun 2026 19:57:19 -0700 Subject: [PATCH 32/40] Add splash screen on desktop so some UI shows up while stuff is loading Signed-off-by: Joel Jothiprakasam --- lib/helpers/backend/startup_tasks.dart | 12 + lib/main.dart | 38 ++- .../filesystem/filesystem_service.dart | 2 + windows/runner/CMakeLists.txt | 3 + windows/runner/flutter_window.cpp | 30 ++ windows/runner/flutter_window.h | 4 + windows/runner/main.cpp | 7 + windows/runner/splash_screen.cpp | 316 ++++++++++++++++++ windows/runner/splash_screen.h | 21 ++ 9 files changed, 423 insertions(+), 10 deletions(-) create mode 100644 windows/runner/splash_screen.cpp create mode 100644 windows/runner/splash_screen.h diff --git a/lib/helpers/backend/startup_tasks.dart b/lib/helpers/backend/startup_tasks.dart index f8085631c0..2d51844abb 100644 --- a/lib/helpers/backend/startup_tasks.dart +++ b/lib/helpers/backend/startup_tasks.dart @@ -30,6 +30,11 @@ class WindowEntry { class StartupTasks { static final Completer uiReady = Completer(); + /// User-facing description of the current startup phase, surfaced by the + /// desktop splash screen while services initialize. Updated by + /// [initStartupServices] (main isolate only — isolate init paths don't drive UI). + static final ValueNotifier status = ValueNotifier("Starting..."); + static Future waitForUI() async { await uiReady.future; } @@ -151,6 +156,7 @@ class StartupTasks { static Future initStartupServices({bool isBubble = false}) async { debugPrint("Initializing startup services..."); + status.value = "Loading settings..."; await _initCoreServices(headless: false); final startupInteropReady = _preRegisterInteropServices( @@ -166,10 +172,13 @@ class StartupTasks { // The next thing we need to do is initialize the database. // If the database is not initialized, we cannot do anything. Logger.info("Initializing database..."); + status.value = "Opening database..."; await Database.init(); Logger.info("Database initialized"); startupInteropReady.complete(); + status.value = "Starting services..."; + // Register the global isolate Logger.info("Registering isolates..."); GetIt.I.registerSingleton(GlobalIsolate()); @@ -215,6 +224,7 @@ class StartupTasks { // Parallelize independent services for faster startup Logger.info("Waiting for services to be ready..."); + status.value = "Loading contacts..."; await Future.wait([ ThemeSvc.init(), IntentsSvc.init(), @@ -231,6 +241,7 @@ class StartupTasks { HandleSvc.init(); Logger.info("Registering ChatsService, SocketService, and NotificationsService..."); + status.value = "Loading chats..."; GetIt.I.registerSingleton(ChatsService()); GetIt.I.registerSingleton(SocketService()); await _waitForInterop(notifications: true); @@ -243,6 +254,7 @@ class StartupTasks { dispose: (svc) => svc.dispose(), ); + status.value = "Finishing up..."; Logger.info( "Startup services initialization complete! Running localhost detection then starting incremental sync..."); diff --git a/lib/main.dart b/lib/main.dart index 967642f952..e0bcfc6ee5 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -65,6 +65,21 @@ Future bubble() async { Future initApp(bool bubble, List arguments) async { runZonedGuarded>(() async { WidgetsFlutterBinding.ensureInitialized(); + + /* ----- DESKTOP NATIVE SPLASH STATUS ----- */ + // Pushes startup status to the native splash; detached once it's dismissed. + void Function()? detachSplashStatus; + if (kIsDesktop && !bubble && arguments.firstOrNull != "minimized") { + const splashChannel = MethodChannel('bluebubbles/splash'); + void pushStatus() { + splashChannel.invokeMethod('setStatus', StartupTasks.status.value).catchError((_) => null); + } + + StartupTasks.status.addListener(pushStatus); + pushStatus(); + detachSplashStatus = () => StartupTasks.status.removeListener(pushStatus); + } + await StartupTasks.initStartupServices(isBubble: bubble); /* ----- RANDOM STUFF INITIALIZATION ----- */ @@ -138,23 +153,22 @@ Future initApp(bool bubble, List arguments) async { await windowManager.setMinimumSize(const Size(300, 300)); Display primary = await ScreenRetriever.instance.getPrimaryDisplay(); - Size size = await windowManager.getSize(); - double width = PrefsSvc.desktop.getWindowWidth() ?? size.width; - double height = PrefsSvc.desktop.getWindowHeight() ?? size.height; + double width = PrefsSvc.desktop.getWindowWidth() ?? 1280; + double height = PrefsSvc.desktop.getWindowHeight() ?? 720; width = width.clamp(300, max(300, primary.size.width)); height = height.clamp(300, max(300, primary.size.height)); - await windowManager.setSize(Size(width, height)); - await PrefsSvc.desktop.setWindowDimensions(width: width, height: height); - await windowManager.setAlignment(Alignment.center); - Offset offset = await windowManager.getPosition(); - double? posX = PrefsSvc.desktop.getWindowX() ?? offset.dx; - double? posY = PrefsSvc.desktop.getWindowY() ?? offset.dy; + final centered = await calcWindowPosition(Size(width, height), Alignment.center); + double posX = PrefsSvc.desktop.getWindowX() ?? centered.dx; + double posY = PrefsSvc.desktop.getWindowY() ?? centered.dy; posX = posX.clamp(0, max(0, primary.size.width - width)); posY = posY.clamp(0, max(0, primary.size.height - height)); - await windowManager.setPosition(Offset(posX, posY), animate: true); + + // Apply size and position in one call. + await windowManager.setBounds(Rect.fromLTWH(posX, posY, width, height)); + await PrefsSvc.desktop.setWindowDimensions(width: width, height: height); await PrefsSvc.desktop.setWindowOffsets(x: posX, y: posY); await windowManager.setTitle('BlueBubbles'); @@ -163,6 +177,10 @@ Future initApp(bool bubble, List arguments) async { } else { await windowManager.hide(); } + try { + await const MethodChannel('bluebubbles/splash').invokeMethod('closeSplash'); + } catch (_) {} + detachSplashStatus?.call(); bool shouldAuthenticate = SettingsSvc.canAuthenticate && SettingsSvc.settings.shouldSecure.value; if (!shouldAuthenticate) { ChatsSvc.init(); diff --git a/lib/services/backend/filesystem/filesystem_service.dart b/lib/services/backend/filesystem/filesystem_service.dart index 9a87299d8c..56ad6a3dda 100644 --- a/lib/services/backend/filesystem/filesystem_service.dart +++ b/lib/services/backend/filesystem/filesystem_service.dart @@ -1,4 +1,5 @@ import 'package:bluebubbles/helpers/helpers.dart'; +import 'package:bluebubbles/helpers/backend/startup_tasks.dart'; import 'package:bluebubbles/database/database.dart'; import 'package:bluebubbles/services/backend/java_dart_interop/method_channel_service.dart'; import 'package:characters/characters.dart'; @@ -142,6 +143,7 @@ class FilesystemService { // Check if the non-msix directory exists final Directory nonMsixLocation = Directory(join(appDataRoot, "Roaming", "BlueBubbles", "bluebubbles")); if (!msixLocation.existsSync() && nonMsixLocation.existsSync()) { + if (!headless) StartupTasks.status.value = "Copying data from previous version..."; await copyPath(nonMsixLocation.path, msixLocation.path); } appDocDir = msixLocation; diff --git a/windows/runner/CMakeLists.txt b/windows/runner/CMakeLists.txt index 394917c053..06926ddf06 100644 --- a/windows/runner/CMakeLists.txt +++ b/windows/runner/CMakeLists.txt @@ -9,6 +9,7 @@ project(runner LANGUAGES CXX) add_executable(${BINARY_NAME} WIN32 "flutter_window.cpp" "main.cpp" + "splash_screen.cpp" "utils.cpp" "win32_window.cpp" "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" @@ -34,6 +35,8 @@ target_compile_definitions(${BINARY_NAME} PRIVATE "NOMINMAX") # dependencies here. target_link_libraries(${BINARY_NAME} PRIVATE flutter flutter_wrapper_app) target_link_libraries(${BINARY_NAME} PRIVATE "dwmapi.lib") +target_link_libraries(${BINARY_NAME} PRIVATE "gdiplus.lib") +target_link_libraries(${BINARY_NAME} PRIVATE "version.lib") target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}") # Run the Flutter tool portions of the build. This must not be removed. diff --git a/windows/runner/flutter_window.cpp b/windows/runner/flutter_window.cpp index 955ee3038f..135dd1f54c 100644 --- a/windows/runner/flutter_window.cpp +++ b/windows/runner/flutter_window.cpp @@ -1,8 +1,12 @@ #include "flutter_window.h" +#include +#include + #include #include "flutter/generated_plugin_registrant.h" +#include "splash_screen.h" FlutterWindow::FlutterWindow(const flutter::DartProject& project) : project_(project) {} @@ -27,6 +31,32 @@ bool FlutterWindow::OnCreate() { RegisterPlugins(flutter_controller_->engine()); SetChildContent(flutter_controller_->view()->GetNativeWindow()); + // Channel the Flutter side uses to dismiss the native splash once its own + // splash window is on screen (see ShowSplashScreen in main.cpp). + splash_channel_ = std::make_unique>( + flutter_controller_->engine()->messenger(), "bluebubbles/splash", + &flutter::StandardMethodCodec::GetInstance()); + splash_channel_->SetMethodCallHandler( + [](const flutter::MethodCall<>& call, + std::unique_ptr> result) { + if (call.method_name() == "setStatus") { + if (const auto* status = std::get_if(call.arguments())) { + int len = MultiByteToWideChar(CP_UTF8, 0, status->c_str(), -1, nullptr, 0); + std::wstring wide(len > 0 ? len - 1 : 0, L'\0'); + if (len > 0) { + MultiByteToWideChar(CP_UTF8, 0, status->c_str(), -1, &wide[0], len); + } + SetSplashStatus(wide); + } + result->Success(); + } else if (call.method_name() == "closeSplash") { + CloseSplashScreen(); + result->Success(); + } else { + result->NotImplemented(); + } + }); + flutter_controller_->engine()->SetNextFrameCallback([&]() { this->Show(); }); diff --git a/windows/runner/flutter_window.h b/windows/runner/flutter_window.h index 6da0652f05..8474ba1ccf 100644 --- a/windows/runner/flutter_window.h +++ b/windows/runner/flutter_window.h @@ -3,6 +3,7 @@ #include #include +#include #include @@ -28,6 +29,9 @@ class FlutterWindow : public Win32Window { // The Flutter instance hosted by this window. std::unique_ptr flutter_controller_; + + // Channel used to dismiss the native splash screen. + std::unique_ptr> splash_channel_; }; #endif // RUNNER_FLUTTER_WINDOW_H_ diff --git a/windows/runner/main.cpp b/windows/runner/main.cpp index 02ab388b4b..a8bf4ca7ec 100644 --- a/windows/runner/main.cpp +++ b/windows/runner/main.cpp @@ -2,7 +2,10 @@ #include #include +#include + #include "flutter_window.h" +#include "splash_screen.h" #include "utils.h" #include @@ -10,6 +13,10 @@ auto bdw = bitsdojo_window_configure(BDW_CUSTOM_FRAME | BDW_HIDE_ON_STARTUP); int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, _In_ wchar_t *command_line, _In_ int show_command) { + if (std::wstring(command_line ? command_line : L"").find(L"minimized") == std::wstring::npos) { + ShowSplashScreen(instance); + } + // Attach to console when present (e.g., 'flutter run') or create a // new console when running with a debugger. if (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent()) { diff --git a/windows/runner/splash_screen.cpp b/windows/runner/splash_screen.cpp new file mode 100644 index 0000000000..7be3b4433b --- /dev/null +++ b/windows/runner/splash_screen.cpp @@ -0,0 +1,316 @@ +#include "splash_screen.h" + +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +// Rounded-corner support (Windows 11); redefined in case the SDK is older. +#ifndef DWMWA_WINDOW_CORNER_PREFERENCE +#define DWMWA_WINDOW_CORNER_PREFERENCE 33 +#endif +#ifndef DWMWCP_ROUND +#define DWMWCP_ROUND 2 +#endif + +// gdiplus.h depends on the min/max macros, which the build disables via +// NOMINMAX. Inject std::min/std::max into the Gdiplus namespace before the +// header so it compiles. +namespace Gdiplus { +using std::max; +using std::min; +} // namespace Gdiplus +#include + +#include "resource.h" + +#pragma comment(lib, "gdiplus.lib") +#pragma comment(lib, "version.lib") + +namespace { + +constexpr wchar_t kSplashClassName[] = L"BlueBubblesSplashWindow"; + +// Logical (96-DPI) layout; scaled per-monitor. +constexpr int kWindowW = 320; +constexpr int kWindowH = 240; +constexpr int kIcon = 72; +constexpr int kIconTop = 34; +constexpr int kVersionTop = 116; +constexpr int kVersionHeight = 16; +constexpr int kSpinnerTop = 148; +constexpr int kSpinner = 26; +constexpr int kSpinnerStroke = 3; +constexpr int kStatusTop = 190; +constexpr int kStatusHeight = 18; +constexpr int kVersionFont = 12; +constexpr int kStatusFont = 12; + +constexpr UINT_PTR kTimerId = 1; +constexpr UINT WM_SPLASH_STATUS = WM_APP + 1; + +std::atomic g_splash_hwnd{nullptr}; +HANDLE g_splash_thread = nullptr; +HICON g_splash_icon = nullptr; +HINSTANCE g_instance = nullptr; +ULONG_PTR g_gdiplus_token = 0; +COLORREF g_bg_color = RGB(28, 28, 30); +bool g_dark = true; +double g_scale = 1.0; +int g_angle = 0; + +std::mutex g_status_mutex; +std::wstring g_status = L"Starting..."; + +// "v" plus " (MSIX)" only when packaged — built in ShowSplashScreen. +std::wstring g_version_line; + +// Matches the system "apps use light theme" preference. +bool IsDarkMode() { + DWORD value = 1; + DWORD size = sizeof(value); + HKEY key; + if (RegOpenKeyExW(HKEY_CURRENT_USER, + L"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize", + 0, KEY_READ, &key) == ERROR_SUCCESS) { + RegQueryValueExW(key, L"AppsUseLightTheme", nullptr, nullptr, + reinterpret_cast(&value), &size); + RegCloseKey(key); + } + return value == 0; // 0 => dark +} + +// Reads the FileVersion embedded in the running exe (windows/runner/Runner.rc), +// which is the authoritative version for the binary (e.g. "1.15.102.0"). +std::wstring ExeFileVersion() { + wchar_t path[MAX_PATH]; + if (GetModuleFileNameW(nullptr, path, MAX_PATH) == 0) return L""; + DWORD handle = 0; + DWORD size = GetFileVersionInfoSizeW(path, &handle); + if (size == 0) return L""; + std::vector data(size); + if (!GetFileVersionInfoW(path, handle, size, data.data())) return L""; + VS_FIXEDFILEINFO* info = nullptr; + UINT len = 0; + if (!VerQueryValueW(data.data(), L"\\", reinterpret_cast(&info), &len) || info == nullptr) { + return L""; + } + return std::to_wstring(HIWORD(info->dwFileVersionMS)) + L"." + + std::to_wstring(LOWORD(info->dwFileVersionMS)) + L"." + + std::to_wstring(HIWORD(info->dwFileVersionLS)) + L"." + + std::to_wstring(LOWORD(info->dwFileVersionLS)); +} + +// True when running from an MSIX package (has package identity). +bool IsMsix() { + UINT32 length = 0; + return GetCurrentPackageFullName(&length, nullptr) != APPMODEL_ERROR_NO_PACKAGE; +} + +std::wstring BuildVersionLine() { + std::wstring version = ExeFileVersion(); + if (version.empty()) version = L"?"; + return L"v" + version + (IsMsix() ? L" (MSIX)" : L""); +} + +int S(int logical) { return static_cast(logical * g_scale); } + +void DrawCenteredText(Gdiplus::Graphics& g, const std::wstring& text, int top, int height, + int font_size, BYTE alpha, int client_w) { + BYTE channel = g_dark ? 255 : 0; + Gdiplus::SolidBrush brush(Gdiplus::Color(alpha, channel, channel, channel)); + Gdiplus::FontFamily family(L"Segoe UI"); + Gdiplus::Font font(&family, static_cast(S(font_size)), + Gdiplus::FontStyleRegular, Gdiplus::UnitPixel); + Gdiplus::StringFormat fmt; + fmt.SetAlignment(Gdiplus::StringAlignmentCenter); + fmt.SetLineAlignment(Gdiplus::StringAlignmentCenter); + fmt.SetFormatFlags(Gdiplus::StringFormatFlagsNoWrap); + fmt.SetTrimming(Gdiplus::StringTrimmingEllipsisCharacter); + Gdiplus::RectF rect(0, static_cast(S(top)), + static_cast(client_w), + static_cast(S(height))); + g.DrawString(text.c_str(), -1, &font, rect, &fmt, &brush); +} + +void Paint(HWND hwnd) { + PAINTSTRUCT ps; + HDC hdc = BeginPaint(hwnd, &ps); + RECT rc; + GetClientRect(hwnd, &rc); + int w = rc.right; + int h = rc.bottom; + + // Double-buffer so the spinner animation doesn't flicker. + HDC mem = CreateCompatibleDC(hdc); + HBITMAP bmp = CreateCompatibleBitmap(hdc, w, h); + HBITMAP old_bmp = static_cast(SelectObject(mem, bmp)); + + HBRUSH bg = CreateSolidBrush(g_bg_color); + FillRect(mem, &rc, bg); + DeleteObject(bg); + + if (g_splash_icon) { + int icon = S(kIcon); + DrawIconEx(mem, (w - icon) / 2, S(kIconTop), g_splash_icon, icon, icon, 0, nullptr, DI_NORMAL); + } + + std::wstring status; + { + std::lock_guard lock(g_status_mutex); + status = g_status; + } + + { + Gdiplus::Graphics g(mem); + g.SetSmoothingMode(Gdiplus::SmoothingModeAntiAlias); + g.SetTextRenderingHint(Gdiplus::TextRenderingHintClearTypeGridFit); + + DrawCenteredText(g, g_version_line, kVersionTop, kVersionHeight, kVersionFont, 120, w); + DrawCenteredText(g, status, kStatusTop, kStatusHeight, kStatusFont, 165, w); + + // Rotating arc spinner in the BlueBubbles brand blue. + Gdiplus::Pen pen(Gdiplus::Color(255, 25, 130, 252), static_cast(S(kSpinnerStroke))); + pen.SetStartCap(Gdiplus::LineCapRound); + pen.SetEndCap(Gdiplus::LineCapRound); + int spin = S(kSpinner); + g.DrawArc(&pen, (w - spin) / 2, S(kSpinnerTop), spin, spin, + static_cast(g_angle), 270.0f); + } + + BitBlt(hdc, 0, 0, w, h, mem, 0, 0, SRCCOPY); + SelectObject(mem, old_bmp); + DeleteObject(bmp); + DeleteDC(mem); + EndPaint(hwnd, &ps); +} + +LRESULT CALLBACK SplashWndProc(HWND hwnd, UINT message, WPARAM wparam, LPARAM lparam) { + switch (message) { + case WM_TIMER: + g_angle = (g_angle + 12) % 360; + InvalidateRect(hwnd, nullptr, FALSE); + return 0; + case WM_SPLASH_STATUS: + InvalidateRect(hwnd, nullptr, FALSE); + return 0; + case WM_PAINT: + Paint(hwnd); + return 0; + case WM_DESTROY: + KillTimer(hwnd, kTimerId); + PostQuitMessage(0); + return 0; + } + return DefWindowProc(hwnd, message, wparam, lparam); +} + +// Runs the splash window on its own thread with its own message loop so the +// spinner keeps animating while the main thread blocks initializing Flutter. +DWORD WINAPI SplashThreadProc(LPVOID) { + Gdiplus::GdiplusStartupInput startup_input; + Gdiplus::GdiplusStartup(&g_gdiplus_token, &startup_input, nullptr); + + WNDCLASSW wc = {}; + wc.style = CS_DROPSHADOW; // elevation/drop shadow for the borderless window + wc.lpfnWndProc = SplashWndProc; + wc.hInstance = g_instance; + wc.lpszClassName = kSplashClassName; + wc.hCursor = LoadCursor(nullptr, IDC_ARROW); + RegisterClassW(&wc); + + POINT cursor; + GetCursorPos(&cursor); + HMONITOR monitor = MonitorFromPoint(cursor, MONITOR_DEFAULTTONEAREST); + UINT dpi = FlutterDesktopGetDpiForMonitor(monitor); + if (dpi < 48) dpi = 96; // guard against early/garbage values -> 0-size window + g_scale = dpi / 96.0; + int win_w = S(kWindowW); + int win_h = S(kWindowH); + + MONITORINFO mi = {sizeof(MONITORINFO)}; + GetMonitorInfo(monitor, &mi); + int x = mi.rcWork.left + (mi.rcWork.right - mi.rcWork.left - win_w) / 2; + int y = mi.rcWork.top + (mi.rcWork.bottom - mi.rcWork.top - win_h) / 2; + + int icon_px = S(kIcon); + g_splash_icon = static_cast(LoadImage(g_instance, MAKEINTRESOURCE(IDI_APP_ICON), + IMAGE_ICON, icon_px, icon_px, LR_DEFAULTCOLOR)); + + HWND hwnd = CreateWindowExW(WS_EX_TOOLWINDOW | WS_EX_TOPMOST, kSplashClassName, L"BlueBubbles", + WS_POPUP, x, y, win_w, win_h, nullptr, nullptr, g_instance, nullptr); + g_splash_hwnd = hwnd; + if (!hwnd) { + if (g_splash_icon) { + DestroyIcon(g_splash_icon); + g_splash_icon = nullptr; + } + Gdiplus::GdiplusShutdown(g_gdiplus_token); + return 0; + } + + // Rounded corners (Windows 11; no-op on older Windows). + DWORD corner = DWMWCP_ROUND; + DwmSetWindowAttribute(hwnd, DWMWA_WINDOW_CORNER_PREFERENCE, &corner, sizeof(corner)); + + ShowWindow(hwnd, SW_SHOW); + UpdateWindow(hwnd); + SetTimer(hwnd, kTimerId, 30, nullptr); + + MSG msg; + while (GetMessage(&msg, nullptr, 0, 0)) { + TranslateMessage(&msg); + DispatchMessage(&msg); + } + + if (g_splash_icon) { + DestroyIcon(g_splash_icon); + g_splash_icon = nullptr; + } + Gdiplus::GdiplusShutdown(g_gdiplus_token); + g_splash_hwnd = nullptr; + return 0; +} + +} // namespace + +void ShowSplashScreen(HINSTANCE instance) { + if (g_splash_thread) { + return; + } + g_instance = instance; + g_dark = IsDarkMode(); + g_bg_color = g_dark ? RGB(28, 28, 30) : RGB(255, 255, 255); + g_version_line = BuildVersionLine(); + g_splash_thread = CreateThread(nullptr, 0, SplashThreadProc, nullptr, 0, nullptr); +} + +void SetSplashStatus(const std::wstring& status) { + { + std::lock_guard lock(g_status_mutex); + g_status = status; + } + HWND hwnd = g_splash_hwnd; + if (hwnd) { + PostMessageW(hwnd, WM_SPLASH_STATUS, 0, 0); + } +} + +void CloseSplashScreen() { + HWND hwnd = g_splash_hwnd; + if (hwnd) { + // The window lives on the splash thread; ask it to close itself. + PostMessageW(hwnd, WM_CLOSE, 0, 0); + } + if (g_splash_thread) { + WaitForSingleObject(g_splash_thread, 2000); + CloseHandle(g_splash_thread); + g_splash_thread = nullptr; + } +} diff --git a/windows/runner/splash_screen.h b/windows/runner/splash_screen.h new file mode 100644 index 0000000000..58e4ee8e37 --- /dev/null +++ b/windows/runner/splash_screen.h @@ -0,0 +1,21 @@ +#ifndef RUNNER_SPLASH_SCREEN_H_ +#define RUNNER_SPLASH_SCREEN_H_ + +#include + +#include + +// Shows a small, centered native splash window displaying the app logo. +// Intended to be called once at startup, before the Flutter engine is +// initialized, so the user sees something the instant the process launches. +void ShowSplashScreen(HINSTANCE instance); + +// Updates the status line shown beneath the spinner. Safe to call from the +// platform (UI) thread; the splash repaints on its own thread. +void SetSplashStatus(const std::wstring& status); + +// Closes the native splash window if it is open. Safe to call multiple times +// and from the platform (UI) thread once Flutter has shown its own window. +void CloseSplashScreen(); + +#endif // RUNNER_SPLASH_SCREEN_H_ From 7d67a523e88edeae31935527350df694c2e6177e Mon Sep 17 00:00:00 2001 From: Joel Jothiprakasam Date: Fri, 19 Jun 2026 21:04:55 -0700 Subject: [PATCH 33/40] Break build into phases so that the insides can be signed before the installer is signed Signed-off-by: Joel Jothiprakasam --- .github/workflows/desktop-builds.yml | 76 +++++++++++++++----- windows/CLAUDE.md | 7 +- windows/build.ps1 | 102 +++++++++++++++++---------- 3 files changed, 129 insertions(+), 56 deletions(-) diff --git a/.github/workflows/desktop-builds.yml b/.github/workflows/desktop-builds.yml index 8e433ee754..5ba49ac303 100644 --- a/.github/workflows/desktop-builds.yml +++ b/.github/workflows/desktop-builds.yml @@ -18,16 +18,11 @@ on: permissions: contents: write # attach artifacts to releases on tag builds -# SignPath code signing. TEMP: test-vs-release config is hardcoded by event for now — -# branch push -> test-signing policy + test cert; v* tag -> release-signing + release cert -# (signing-policy-slug and SIGNED_MSIX_PUBLISHER below). To be parameterized into repo vars later. -# Required repo settings (shared by both): -# Secret SIGNPATH_API_TOKEN — SignPath CI user API token -# Var SIGNPATH_ORGANIZATION_ID — SignPath organization id (GUID) -# Var SIGNPATH_PROJECT_SLUG — SignPath project slug -# Var SIGNPATH_ARTIFACT_CONFIG_INSTALLER — artifact configuration slug for the Inno installer -# Var SIGNPATH_ARTIFACT_CONFIG_MSIX — artifact configuration slug for the msix -# When SIGNPATH_API_TOKEN is unset, signing is skipped and nothing is attached to releases. +# SignPath code signing. Required repo settings: +# Secret SIGNPATH_API_TOKEN +# Var SIGNPATH_ORGANIZATION_ID / SIGNPATH_PROJECT_SLUG +# Var SIGNPATH_ARTIFACT_CONFIG_APP / _INSTALLER / _MSIX +# TEMP: branch push -> test-signing + test cert; v* tag -> release-signing + release cert. jobs: windows: @@ -60,8 +55,56 @@ jobs: # Inno Setup 6 and Rust (for super_native_extensions) are preinstalled # on the windows-latest runner image. - - name: Build - run: .\windows\build.ps1 + - name: Build app (phase 1) + run: .\windows\build.ps1 -Phase Build + shell: pwsh + + # sign=true when: token present AND (release tag, build branch, or manual sign=true). + - name: Determine signing + id: gate + shell: bash + run: | + if [ -n "$SIGNPATH_API_TOKEN" ] && { [[ "$GITHUB_REF" == refs/tags/* ]] || [ "$GITHUB_REF" = "refs/heads/joel/2.0/desktop/2.0.0.0" ] || [ "${{ inputs.sign }}" = "true" ]; }; then + echo "sign=true" >> "$GITHUB_OUTPUT" + else + echo "sign=false" >> "$GITHUB_OUTPUT" + fi + + # Sign the inner binaries before packaging so both the msix and installer ship them signed. + - name: Zip app payload + if: ${{ steps.gate.outputs.sign == 'true' }} + shell: pwsh + run: Compress-Archive -Path 'build\windows\x64\runner\Release\*' -DestinationPath 'windows\bluebubbles-payload.zip' -Force + + - name: Upload app payload artifact + id: upload-payload + if: ${{ steps.gate.outputs.sign == 'true' }} + uses: actions/upload-artifact@v7 + with: + name: bluebubbles-payload-unsigned + path: windows/bluebubbles-payload.zip + + - name: Sign app payload (SignPath) + id: sign-payload + if: ${{ steps.gate.outputs.sign == 'true' }} + uses: signpath/github-action-submit-signing-request@v2 + with: + api-token: ${{ env.SIGNPATH_API_TOKEN }} + organization-id: ${{ vars.SIGNPATH_ORGANIZATION_ID }} + project-slug: ${{ vars.SIGNPATH_PROJECT_SLUG }} + signing-policy-slug: ${{ startsWith(github.ref, 'refs/tags/') && 'release-signing' || 'test-signing' }} + artifact-configuration-slug: ${{ vars.SIGNPATH_ARTIFACT_CONFIG_APP }} + github-artifact-id: ${{ steps.upload-payload.outputs.artifact-id }} + wait-for-completion: true + output-artifact-directory: out/payload + + - name: Replace Release binaries with signed payload + if: ${{ steps.gate.outputs.sign == 'true' }} + shell: pwsh + run: Expand-Archive -Path 'out\payload\bluebubbles-payload.zip' -DestinationPath 'build\windows\x64\runner\Release' -Force + + - name: Package installer + msix (phase 2) + run: .\windows\build.ps1 -Phase Package shell: pwsh - name: Upload unsigned installer artifact @@ -71,7 +114,6 @@ jobs: name: bluebubbles-installer-unsigned path: windows/bluebubbles_installer.exe - # The directly-distributed msix, unsigned until SignPath signs it below. # Only built when SIGNED_MSIX_PUBLISHER is configured. - name: Upload unsigned msix artifact id: upload-msix @@ -94,16 +136,15 @@ jobs: name: bluebubbles-msix-store path: windows/bluebubbles-store.msix - # --- SignPath signing: tag builds, or manual builds with sign=true --- + # Sign the outer wrappers (inner binaries already signed above; msix config is container-only). - name: Sign installer (SignPath) id: sign-installer - if: ${{ env.SIGNPATH_API_TOKEN != '' && (startsWith(github.ref, 'refs/tags/') || github.ref == 'refs/heads/joel/2.0/desktop/2.0.0.0' || inputs.sign) }} + if: ${{ steps.gate.outputs.sign == 'true' }} uses: signpath/github-action-submit-signing-request@v2 with: api-token: ${{ env.SIGNPATH_API_TOKEN }} organization-id: ${{ vars.SIGNPATH_ORGANIZATION_ID }} project-slug: ${{ vars.SIGNPATH_PROJECT_SLUG }} - # TEMP: release-signing on tags, test-signing on branch pushes. signing-policy-slug: ${{ startsWith(github.ref, 'refs/tags/') && 'release-signing' || 'test-signing' }} artifact-configuration-slug: ${{ vars.SIGNPATH_ARTIFACT_CONFIG_INSTALLER }} github-artifact-id: ${{ steps.upload-installer.outputs.artifact-id }} @@ -112,13 +153,12 @@ jobs: - name: Sign msix (SignPath) id: sign-msix - if: ${{ env.SIGNPATH_API_TOKEN != '' && steps.upload-msix.outputs.artifact-id != '' && (startsWith(github.ref, 'refs/tags/') || github.ref == 'refs/heads/joel/2.0/desktop/2.0.0.0' || inputs.sign) }} + if: ${{ steps.gate.outputs.sign == 'true' && steps.upload-msix.outputs.artifact-id != '' }} uses: signpath/github-action-submit-signing-request@v2 with: api-token: ${{ env.SIGNPATH_API_TOKEN }} organization-id: ${{ vars.SIGNPATH_ORGANIZATION_ID }} project-slug: ${{ vars.SIGNPATH_PROJECT_SLUG }} - # TEMP: release-signing on tags, test-signing on branch pushes. signing-policy-slug: ${{ startsWith(github.ref, 'refs/tags/') && 'release-signing' || 'test-signing' }} artifact-configuration-slug: ${{ vars.SIGNPATH_ARTIFACT_CONFIG_MSIX }} github-artifact-id: ${{ steps.upload-msix.outputs.artifact-id }} diff --git a/windows/CLAUDE.md b/windows/CLAUDE.md index 23e1916662..8429601537 100644 --- a/windows/CLAUDE.md +++ b/windows/CLAUDE.md @@ -7,13 +7,16 @@ - `utils.cpp/h` — UTF-8 / UTF-16 helpers ## Installer -- `build.ps1` — release build script: cleans Release dir, builds two MSIX variants, then compiles the installer (mirrors `linux/build.sh`) +- `build.ps1` — release build script (mirrors `linux/build.sh`). `-Phase` splits it so CI can sign the app payload between building and packaging: + - `-Phase Build` → cleans Release dir, builds the app + `--store` MSIX, then stops (leaves `build\windows\x64\runner\Release` ready to sign) + - `-Phase Package` → builds the sideload MSIX + Inno installer from the (now-signed) Release dir + - `-Phase All` (default) → both back-to-back, for local builds with no signing round-trip - `--store` build → `bluebubbles-store.msix` (MS Store; Microsoft signs it; store identity/publisher from `pubspec.yaml`) - sideload build → `bluebubbles.msix` (directly distributed; unsigned, SignPath signs it in CI). Only built when `SIGNED_MSIX_PUBLISHER` env is set; that value must equal the SignPath cert's subject DN. - `store:` is deliberately absent from `pubspec.yaml` (msix forces store mode if present and can't be overridden via CLI); pass `--store` for the store build instead. - `bluebubbles_installer_script.iss` — Inno Setup installer definition - `CodeDependencies.iss` — installer dependency declarations -- SignPath code signing is wired in `.github/workflows/desktop-builds.yml` (installer + msix, on tags or manual `sign=true`) +- SignPath code signing is wired in `.github/workflows/desktop-builds.yml`: the app payload (`bluebubbles_app.exe` + bundled DLLs) is signed first, then the Inno installer and msix wrappers are signed (on tags or manual `sign=true`). So the binaries the user runs are signed regardless of which artifact they install. ## Key Flutter-Side Files for Windows - `lib/utils/window_effects.dart` — Mica/acrylic transparency (`flutter_acrylic`) diff --git a/windows/build.ps1 b/windows/build.ps1 index 6295d81e98..9ae11fcce5 100644 --- a/windows/build.ps1 +++ b/windows/build.ps1 @@ -1,17 +1,28 @@ # Windows release build script. Run from the root of the repository. Requires Inno Setup 6 to be installed. -# Builds the app, packages the MSIX, then compiles the Inno Setup installer. +# +# Phases (so CI can sign the app payload between building and packaging): +# -Phase Build build the app + store MSIX, then stop, leaving build\windows\x64\runner\Release +# ready for SignPath to sign the inner binaries. +# -Phase Package package the sideload MSIX + Inno installer from the (now-signed) Release\ dir. +# -Phase All both, back-to-back (default — local builds with no signing round-trip). +# # Outputs: -# windows\bluebubbles-store.msix (MS Store submission only — not attached to releases) -# windows\bluebubbles.msix (directly-distributed, unsigned; SignPath signs it in CI — only when SIGNED_MSIX_PUBLISHER is set) -# windows\bluebubbles_installer.exe +# windows\bluebubbles-store.msix (Build/All) MS Store submission only — not attached to releases +# windows\bluebubbles.msix (Package/All) directly-distributed, unsigned; SignPath signs it in CI +# (only when SIGNED_MSIX_PUBLISHER is set) +# windows\bluebubbles_installer.exe (Package/All) +param( + [ValidateSet('All', 'Build', 'Package')] + [string]$Phase = 'All' +) + +Set-PSDebug -Trace 1 + $ErrorActionPreference = 'Stop' # Flutter version to build with; override with the FLUTTER_VERSION env var. $flutterVersion = if ($env:FLUTTER_VERSION) { $env:FLUTTER_VERSION } else { '3.44.2' } -$iscc = if ($env:ISCC_PATH) { $env:ISCC_PATH } else { "${env:ProgramFiles(x86)}\Inno Setup 6\ISCC.exe" } -if (-not (Test-Path $iscc)) { throw "Inno Setup compiler not found at '$iscc'. Install Inno Setup 6 or set ISCC_PATH." } - Set-Location (Join-Path $PSScriptRoot '..') # Runs a command and aborts the build if it fails. @@ -32,36 +43,55 @@ if ($env:FLUTTER_CMD) { $dartCmd = 'fvm', 'dart' } -# Clean the Release output first: the installer ships Release\*.dll wholesale, -# so leftovers from removed plugins would get packaged into the installer. $releaseDir = 'build\windows\x64\runner\Release' -if (Test-Path $releaseDir) { Remove-Item $releaseDir -Recurse -Force } - -Invoke-Checked $flutterCmd pub get --enforce-lockfile - -# Runs `flutter build windows --release` and packages the MS Store MSIX -# (windows\bluebubbles-store.msix). Microsoft signs this one, so pass --store -# explicitly (store mode is no longer set in pubspec.yaml). -Invoke-Checked $dartCmd run msix:create --store --output-name bluebubbles-store - -# Build the directly-distributed MSIX, left unsigned for SignPath to sign in CI. -# Reuses the Release output from the store build above. SIGNED_MSIX_PUBLISHER must -# equal the SignPath certificate's subject DN, or Windows will reject the signature. -# Skipped when unset (e.g. local builds without signing configured). -if ($env:SIGNED_MSIX_PUBLISHER) { - $msixArgs = @( - '--build-windows', 'false', - '--sign-msix', 'false', - '--publisher', $env:SIGNED_MSIX_PUBLISHER, - '--output-name', 'bluebubbles' - ) - if ($env:SIGNED_MSIX_IDENTITY) { $msixArgs += @('--identity-name', $env:SIGNED_MSIX_IDENTITY) } - Invoke-Checked $dartCmd run msix:create @msixArgs + +if ($Phase -ne 'Package') { + # --- Build phase: produce the Release\ output and the store MSIX --- + + # Clean the Release output first: the installer ships Release\*.dll wholesale, + # so leftovers from removed plugins would get packaged into the installer. + if (Test-Path $releaseDir) { Remove-Item $releaseDir -Recurse -Force } + + Invoke-Checked $flutterCmd pub get --enforce-lockfile + + # Runs `flutter build windows --release` and packages the MS Store MSIX + # (windows\bluebubbles-store.msix). Microsoft signs this one, so pass --store + # explicitly (store mode is no longer set in pubspec.yaml). Built from the + # unsigned Release output — Microsoft re-signs the package at ingestion. + Invoke-Checked $dartCmd run msix:create --store --output-name bluebubbles-store + + Get-FileHash 'windows\bluebubbles-store.msix' -Algorithm SHA256 | Format-List Path, Hash } -# Compile the Inno Setup installer -Invoke-Checked @($iscc) 'windows\bluebubbles_installer_script.iss' +if ($Phase -ne 'Build') { + # --- Package phase: wrap the Release\ binaries (signed by CI in between) --- + + $iscc = if ($env:ISCC_PATH) { $env:ISCC_PATH } else { "${env:ProgramFiles(x86)}\Inno Setup 6\ISCC.exe" } + if (-not (Test-Path $iscc)) { throw "Inno Setup compiler not found at '$iscc'. Install Inno Setup 6 or set ISCC_PATH." } + + if (-not (Test-Path $releaseDir)) { throw "Release output '$releaseDir' not found — run the Build phase first." } + + # Build the directly-distributed MSIX, left unsigned for SignPath to sign in CI. + # Reuses the Release output from the store build above. SIGNED_MSIX_PUBLISHER must + # equal the SignPath certificate's subject DN, or Windows will reject the signature. + # Skipped when unset (e.g. local builds without signing configured). + if ($env:SIGNED_MSIX_PUBLISHER) { + $msixArgs = @( + '--build-windows', 'false', + '--sign-msix', 'false', + '--publisher', $env:SIGNED_MSIX_PUBLISHER, + '--output-name', 'bluebubbles' + ) + if ($env:SIGNED_MSIX_IDENTITY) { $msixArgs += @('--identity-name', $env:SIGNED_MSIX_IDENTITY) } + Invoke-Checked $dartCmd run msix:create @msixArgs + } + + # Compile the Inno Setup installer + Invoke-Checked @($iscc) 'windows\bluebubbles_installer_script.iss' + + $hashTargets = @('windows\bluebubbles_installer.exe') + if (Test-Path 'windows\bluebubbles.msix') { $hashTargets += 'windows\bluebubbles.msix' } + Get-FileHash $hashTargets -Algorithm SHA256 | Format-List Path, Hash +} -$hashTargets = @('windows\bluebubbles-store.msix', 'windows\bluebubbles_installer.exe') -if (Test-Path 'windows\bluebubbles.msix') { $hashTargets += 'windows\bluebubbles.msix' } -Get-FileHash $hashTargets -Algorithm SHA256 | Format-List Path, Hash +Set-PSDebug -Trace 0 From 31e288d272514fbf285b8fe45a20c9aaf114074d Mon Sep 17 00:00:00 2001 From: Joel Jothiprakasam Date: Fri, 19 Jun 2026 21:39:34 -0700 Subject: [PATCH 34/40] Remove debug Signed-off-by: Joel Jothiprakasam --- windows/build.ps1 | 4 ---- 1 file changed, 4 deletions(-) diff --git a/windows/build.ps1 b/windows/build.ps1 index 9ae11fcce5..f950898aa4 100644 --- a/windows/build.ps1 +++ b/windows/build.ps1 @@ -16,8 +16,6 @@ param( [string]$Phase = 'All' ) -Set-PSDebug -Trace 1 - $ErrorActionPreference = 'Stop' # Flutter version to build with; override with the FLUTTER_VERSION env var. @@ -93,5 +91,3 @@ if ($Phase -ne 'Build') { if (Test-Path 'windows\bluebubbles.msix') { $hashTargets += 'windows\bluebubbles.msix' } Get-FileHash $hashTargets -Algorithm SHA256 | Format-List Path, Hash } - -Set-PSDebug -Trace 0 From 6bfd0ad583db55530a05c1570d19adf0aae85386 Mon Sep 17 00:00:00 2001 From: Joel Jothiprakasam Date: Fri, 19 Jun 2026 21:40:12 -0700 Subject: [PATCH 35/40] Don't double zip Signed-off-by: Joel Jothiprakasam --- .github/workflows/desktop-builds.yml | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/.github/workflows/desktop-builds.yml b/.github/workflows/desktop-builds.yml index 5ba49ac303..bb5c15f9a4 100644 --- a/.github/workflows/desktop-builds.yml +++ b/.github/workflows/desktop-builds.yml @@ -74,7 +74,7 @@ jobs: - name: Zip app payload if: ${{ steps.gate.outputs.sign == 'true' }} shell: pwsh - run: Compress-Archive -Path 'build\windows\x64\runner\Release\*' -DestinationPath 'windows\bluebubbles-payload.zip' -Force + run: Compress-Archive -Path 'build\windows\x64\runner\Release\*' -DestinationPath 'windows\bluebubbles-payload-unsigned.zip' -Force - name: Upload app payload artifact id: upload-payload @@ -82,7 +82,8 @@ jobs: uses: actions/upload-artifact@v7 with: name: bluebubbles-payload-unsigned - path: windows/bluebubbles-payload.zip + path: windows/bluebubbles-payload-unsigned.zip + archive: false - name: Sign app payload (SignPath) id: sign-payload @@ -101,7 +102,9 @@ jobs: - name: Replace Release binaries with signed payload if: ${{ steps.gate.outputs.sign == 'true' }} shell: pwsh - run: Expand-Archive -Path 'out\payload\bluebubbles-payload.zip' -DestinationPath 'build\windows\x64\runner\Release' -Force + run: | + Remove-Item -Path 'build\windows\x64\runner\Release' -Recurse -Force + Move-Item -Path 'out\payload' -Destination 'build\windows\x64\runner\Release' -Force - name: Package installer + msix (phase 2) run: .\windows\build.ps1 -Phase Package From a9cd5f88ab7f5903891f3947115e3032d5a802bd Mon Sep 17 00:00:00 2001 From: Joel Jothiprakasam Date: Fri, 19 Jun 2026 22:22:00 -0700 Subject: [PATCH 36/40] different signing config for test and release msix Signed-off-by: Joel Jothiprakasam --- .github/workflows/desktop-builds.yml | 24 +++++++++++++++++++----- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/.github/workflows/desktop-builds.yml b/.github/workflows/desktop-builds.yml index bb5c15f9a4..cbd40640d4 100644 --- a/.github/workflows/desktop-builds.yml +++ b/.github/workflows/desktop-builds.yml @@ -154,20 +154,34 @@ jobs: wait-for-completion: true output-artifact-directory: out/installer - - name: Sign msix (SignPath) + - name: Sign release msix (SignPath) id: sign-msix - if: ${{ steps.gate.outputs.sign == 'true' && steps.upload-msix.outputs.artifact-id != '' }} + if: ${{ steps.gate.outputs.sign == 'true' && startsWith(github.ref, 'refs/tags/') && steps.upload-msix.outputs.artifact-id != '' }} uses: signpath/github-action-submit-signing-request@v2 with: api-token: ${{ env.SIGNPATH_API_TOKEN }} organization-id: ${{ vars.SIGNPATH_ORGANIZATION_ID }} project-slug: ${{ vars.SIGNPATH_PROJECT_SLUG }} - signing-policy-slug: ${{ startsWith(github.ref, 'refs/tags/') && 'release-signing' || 'test-signing' }} + signing-policy-slug: 'release-signing' artifact-configuration-slug: ${{ vars.SIGNPATH_ARTIFACT_CONFIG_MSIX }} github-artifact-id: ${{ steps.upload-msix.outputs.artifact-id }} wait-for-completion: true output-artifact-directory: out/msix + - name: Sign test msix (SignPath) + id: sign-msix-test + if: ${{ steps.gate.outputs.sign == 'true' && !startsWith(github.ref, 'refs/tags/') && steps.upload-msix.outputs.artifact-id != '' }} + uses: signpath/github-action-submit-signing-request@v2 + with: + api-token: ${{ env.SIGNPATH_API_TOKEN }} + organization-id: ${{ vars.SIGNPATH_ORGANIZATION_ID }} + project-slug: ${{ vars.SIGNPATH_PROJECT_SLUG }} + signing-policy-slug: 'test-signing' + artifact-configuration-slug: ${{ vars.SIGNPATH_ARTIFACT_CONFIG_MSIX_TEST }} + github-artifact-id: ${{ steps.upload-msix.outputs.artifact-id }} + wait-for-completion: true + output-artifact-directory: out/msix + # Output paths follow the SignPath artifact configuration — adjust if yours nests differently. - name: Upload installer if: ${{ steps.sign-installer.outcome == 'success' }} @@ -177,7 +191,7 @@ jobs: path: out/installer/bluebubbles_installer.exe - name: Upload msix - if: ${{ steps.sign-msix.outcome == 'success' }} + if: ${{ steps.sign-msix.outcome == 'success' || steps.sign-msix-test.outcome == 'success' }} uses: actions/upload-artifact@v7 with: name: bluebubbles-msix @@ -190,7 +204,7 @@ jobs: files: out/installer/bluebubbles_installer.exe - name: Attach msix to release - if: ${{ startsWith(github.ref, 'refs/tags/') && steps.sign-msix.outcome == 'success' }} + if: ${{ startsWith(github.ref, 'refs/tags/') && (steps.sign-msix.outcome == 'success' || steps.sign-msix-test.outcome == 'success') }} uses: softprops/action-gh-release@v3 with: files: out/msix/bluebubbles.msix From b0c5281351947fdb480a5c21379ad287893b45c1 Mon Sep 17 00:00:00 2001 From: Joel Jothiprakasam Date: Sat, 20 Jun 2026 23:26:11 -0700 Subject: [PATCH 37/40] Force messages to show up as they happen Signed-off-by: Joel Jothiprakasam --- lib/helpers/backend/startup_tasks.dart | 17 +++++++++++------ .../backend/filesystem/filesystem_service.dart | 2 +- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/lib/helpers/backend/startup_tasks.dart b/lib/helpers/backend/startup_tasks.dart index 712080f717..1ea5f88859 100644 --- a/lib/helpers/backend/startup_tasks.dart +++ b/lib/helpers/backend/startup_tasks.dart @@ -39,6 +39,11 @@ class StartupTasks { await uiReady.future; } + static Future setSplashStatus(String value) async { + status.value = value; + if (kIsDesktop) await Future.delayed(Duration.zero); + } + static Completer _preRegisterInteropServices({ required bool headless, required bool isBubble, @@ -160,7 +165,7 @@ class StartupTasks { static Future initStartupServices({bool isBubble = false}) async { debugPrint("Initializing startup services..."); - status.value = "Loading settings..."; + await setSplashStatus("Loading settings..."); await _initCoreServices(headless: false); final startupInteropReady = _preRegisterInteropServices( @@ -176,12 +181,12 @@ class StartupTasks { // The next thing we need to do is initialize the database. // If the database is not initialized, we cannot do anything. Logger.info("Initializing database..."); - status.value = "Opening database..."; + await setSplashStatus("Opening database..."); await Database.init(); Logger.info("Database initialized"); startupInteropReady.complete(); - status.value = "Starting services..."; + await setSplashStatus("Starting services..."); // Register the global isolate Logger.info("Registering isolates..."); @@ -228,7 +233,7 @@ class StartupTasks { // Parallelize independent services for faster startup Logger.info("Waiting for services to be ready..."); - status.value = "Loading contacts..."; + await setSplashStatus("Loading contacts..."); await Future.wait([ ThemeSvc.init(), IntentsSvc.init(), @@ -245,7 +250,7 @@ class StartupTasks { HandleSvc.init(); Logger.info("Registering ChatsService, SocketService, and NotificationsService..."); - status.value = "Loading chats..."; + await setSplashStatus("Loading chats..."); GetIt.I.registerSingleton(ChatsService()); GetIt.I.registerSingleton(SocketService()); await _waitForInterop(notifications: true); @@ -258,7 +263,7 @@ class StartupTasks { dispose: (svc) => svc.dispose(), ); - status.value = "Finishing up..."; + await setSplashStatus("Finishing up..."); Logger.info( "Startup services initialization complete! Running localhost detection then starting incremental sync..."); diff --git a/lib/services/backend/filesystem/filesystem_service.dart b/lib/services/backend/filesystem/filesystem_service.dart index 56ad6a3dda..f02370d330 100644 --- a/lib/services/backend/filesystem/filesystem_service.dart +++ b/lib/services/backend/filesystem/filesystem_service.dart @@ -143,7 +143,7 @@ class FilesystemService { // Check if the non-msix directory exists final Directory nonMsixLocation = Directory(join(appDataRoot, "Roaming", "BlueBubbles", "bluebubbles")); if (!msixLocation.existsSync() && nonMsixLocation.existsSync()) { - if (!headless) StartupTasks.status.value = "Copying data from previous version..."; + if (!headless) await StartupTasks.setSplashStatus("Copying data from previous version..."); await copyPath(nonMsixLocation.path, msixLocation.path); } appDocDir = msixLocation; From eb9a0e1c00cd1996038ba99484010214500cdbe1 Mon Sep 17 00:00:00 2001 From: Joel Jothiprakasam Date: Sat, 20 Jun 2026 23:44:45 -0700 Subject: [PATCH 38/40] flutter analyze on PR. Remove temp branch-specific stuff Signed-off-by: Joel Jothiprakasam --- .github/workflows/desktop-builds.yml | 27 ++--- .github/workflows/pr-check.yml | 144 +++++++++++++++++++++++++++ 2 files changed, 155 insertions(+), 16 deletions(-) create mode 100644 .github/workflows/pr-check.yml diff --git a/.github/workflows/desktop-builds.yml b/.github/workflows/desktop-builds.yml index cbd40640d4..c063c5374d 100644 --- a/.github/workflows/desktop-builds.yml +++ b/.github/workflows/desktop-builds.yml @@ -2,10 +2,6 @@ name: Desktop Builds on: push: - # TEMP: build this branch until the workflow lands on master (workflow_dispatch - # only works from the default branch). Remove the branches list after merge. - branches: - - joel/2.0/desktop/2.0.0.0 tags: - "v*" workflow_dispatch: @@ -18,11 +14,8 @@ on: permissions: contents: write # attach artifacts to releases on tag builds -# SignPath code signing. Required repo settings: -# Secret SIGNPATH_API_TOKEN -# Var SIGNPATH_ORGANIZATION_ID / SIGNPATH_PROJECT_SLUG -# Var SIGNPATH_ARTIFACT_CONFIG_APP / _INSTALLER / _MSIX -# TEMP: branch push -> test-signing + test cert; v* tag -> release-signing + release cert. +env: + FLUTTER_VERSION: "3.44.2" jobs: windows: @@ -30,7 +23,7 @@ jobs: timeout-minutes: 60 env: SIGNPATH_API_TOKEN: ${{ secrets.SIGNPATH_API_TOKEN }} - # TEMP: release cert on tags, test cert on branch pushes (baked into the msix at build time). + # Must match the signing cert subject: release cert on tags, test cert on manual builds. SIGNED_MSIX_PUBLISHER: "${{ startsWith(github.ref, 'refs/tags/') && 'CN=SignPath Foundation,O=SignPath Foundation,C=US,S=DE,L=Lewes' || 'CN=Test certificate for ''BlueBubbles [OSS]''' }}" SIGNED_MSIX_IDENTITY: BlueBubbles.BlueBubbles steps: @@ -40,12 +33,13 @@ jobs: - name: Install fvm run: choco install fvm -y - # Keyed on the build script so a version bump in it invalidates the cache + # Keyed on the Flutter version so the SDK is only re-downloaded when the + # version actually changes — not on every build-script edit. - name: Cache Flutter SDK (fvm) uses: actions/cache@v5 with: path: ~/fvm - key: fvm-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('windows/build.ps1') }} + key: fvm-${{ runner.os }}-${{ runner.arch }}-${{ env.FLUTTER_VERSION }} - name: Cache pub packages uses: actions/cache@v5 @@ -59,12 +53,12 @@ jobs: run: .\windows\build.ps1 -Phase Build shell: pwsh - # sign=true when: token present AND (release tag, build branch, or manual sign=true). + # sign=true when: token present AND (release tag or manual sign=true). - name: Determine signing id: gate shell: bash run: | - if [ -n "$SIGNPATH_API_TOKEN" ] && { [[ "$GITHUB_REF" == refs/tags/* ]] || [ "$GITHUB_REF" = "refs/heads/joel/2.0/desktop/2.0.0.0" ] || [ "${{ inputs.sign }}" = "true" ]; }; then + if [ -n "$SIGNPATH_API_TOKEN" ] && { [[ "$GITHUB_REF" == refs/tags/* ]] || [ "${{ inputs.sign }}" = "true" ]; }; then echo "sign=true" >> "$GITHUB_OUTPUT" else echo "sign=false" >> "$GITHUB_OUTPUT" @@ -243,12 +237,13 @@ jobs: curl -fsSL https://fvm.app/install.sh | bash echo "$HOME/fvm/bin" >> "$GITHUB_PATH" - # Keyed on the build script so a version bump in it invalidates the cache + # Keyed on the Flutter version so the SDK is only re-downloaded when the + # version actually changes — not on every build-script edit. - name: Cache Flutter SDK (fvm) uses: actions/cache@v5 with: path: ~/fvm - key: fvm-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('linux/build.sh') }} + key: fvm-${{ runner.os }}-${{ runner.arch }}-${{ env.FLUTTER_VERSION }} - name: Cache pub packages uses: actions/cache@v5 diff --git a/.github/workflows/pr-check.yml b/.github/workflows/pr-check.yml new file mode 100644 index 0000000000..9708761739 --- /dev/null +++ b/.github/workflows/pr-check.yml @@ -0,0 +1,144 @@ +name: PR Check + +# Lightweight gate for pull requests: static analysis + an unsigned compile per +# desktop platform. No signing, no packaging (msix/installer), no release/VirusTotal — +# those live in desktop-builds.yml and run on tags. This just proves the tree analyzes +# clean (no new warnings/errors) and still builds. + +on: + pull_request: + types: [opened, synchronize, reopened, ready_for_review] + workflow_dispatch: + +# Cancel superseded runs when a PR is pushed again. +concurrency: + group: pr-check-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +env: + # Matches the pin in windows/build.ps1 and linux/build.sh. + FLUTTER_VERSION: "3.44.2" + +jobs: + analyze: + runs-on: ubuntu-latest + timeout-minutes: 20 + steps: + - name: Checkout code + uses: actions/checkout@v6 + + - name: Install fvm + run: | + curl -fsSL https://fvm.app/install.sh | bash + echo "$HOME/fvm/bin" >> "$GITHUB_PATH" + + - name: Cache Flutter SDK (fvm) + uses: actions/cache@v5 + with: + path: ~/fvm + key: fvm-${{ runner.os }}-${{ runner.arch }}-${{ env.FLUTTER_VERSION }} + + - name: Cache pub packages + uses: actions/cache@v5 + with: + path: ~/.pub-cache + key: pub-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('pubspec.lock') }} + + - name: Select Flutter version + run: fvm use "$FLUTTER_VERSION" --force + + - name: Pub get + run: fvm flutter pub get --enforce-lockfile + + # --no-fatal-infos: the codebase carries ~270 pre-existing info-level lints + # (deprecations, use_build_context_synchronously). Fail only on new + # warnings/errors a PR introduces, not on that legacy baseline. + - name: Analyze + run: fvm flutter analyze --no-fatal-infos + + build-windows: + # Skip the heavy compile on draft PRs; analyze still runs for fast feedback. + if: ${{ !github.event.pull_request.draft }} + runs-on: windows-latest + timeout-minutes: 60 + steps: + - name: Checkout code + uses: actions/checkout@v6 + + - name: Install fvm + run: choco install fvm -y + + - name: Cache Flutter SDK (fvm) + uses: actions/cache@v5 + with: + path: ~/fvm + key: fvm-${{ runner.os }}-${{ runner.arch }}-${{ env.FLUTTER_VERSION }} + + - name: Cache pub packages + uses: actions/cache@v5 + with: + path: ~/AppData/Local/Pub/Cache + key: pub-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('pubspec.lock') }} + + - name: Select Flutter version + run: fvm use $env:FLUTTER_VERSION --force + shell: pwsh + + - name: Pub get + run: fvm flutter pub get --enforce-lockfile + shell: pwsh + + # Compile only — no msix/installer packaging (that's the release workflow). + - name: Build Windows (unsigned) + run: fvm flutter build windows --release + shell: pwsh + + build-linux: + # Skip the heavy compile on draft PRs; analyze still runs for fast feedback. + if: ${{ !github.event.pull_request.draft }} + runs-on: ubuntu-latest + timeout-minutes: 60 + steps: + - name: Checkout code + uses: actions/checkout@v6 + + - name: Install system dependencies + run: | + sudo apt-get update + sudo apt-get install -y \ + clang \ + cmake \ + ninja-build \ + pkg-config \ + libgtk-3-dev \ + libwebkit2gtk-4.1-dev \ + libmpv-dev \ + libayatana-appindicator3-dev \ + libnotify-dev + + - name: Install fvm + run: | + curl -fsSL https://fvm.app/install.sh | bash + echo "$HOME/fvm/bin" >> "$GITHUB_PATH" + + - name: Cache Flutter SDK (fvm) + uses: actions/cache@v5 + with: + path: ~/fvm + key: fvm-${{ runner.os }}-${{ runner.arch }}-${{ env.FLUTTER_VERSION }} + + - name: Cache pub packages + uses: actions/cache@v5 + with: + path: ~/.pub-cache + key: pub-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('pubspec.lock') }} + + - name: Select Flutter version + run: fvm use "$FLUTTER_VERSION" --force + + - name: Pub get + run: fvm flutter pub get --enforce-lockfile + + # Compile only — no tarball packaging (that's the release workflow). + - name: Build Linux (unsigned) + run: fvm flutter build linux --release From 060706f09d22f684ecec2155bd3834a1f8f9728d Mon Sep 17 00:00:00 2001 From: Joel Jothiprakasam Date: Sat, 20 Jun 2026 23:55:45 -0700 Subject: [PATCH 39/40] ensure no pub get without lockfile enforcement Signed-off-by: Joel Jothiprakasam --- .github/workflows/pr-check.yml | 10 +++++++--- linux/build.sh | 4 +++- windows/build.ps1 | 6 ++++-- 3 files changed, 14 insertions(+), 6 deletions(-) diff --git a/.github/workflows/pr-check.yml b/.github/workflows/pr-check.yml index 9708761739..1e6bb252ff 100644 --- a/.github/workflows/pr-check.yml +++ b/.github/workflows/pr-check.yml @@ -53,8 +53,10 @@ jobs: # --no-fatal-infos: the codebase carries ~270 pre-existing info-level lints # (deprecations, use_build_context_synchronously). Fail only on new # warnings/errors a PR introduces, not on that legacy baseline. + # --no-pub: reuse the lockfile-enforced resolution from the step above + # (analyze otherwise re-runs pub get without --enforce-lockfile). - name: Analyze - run: fvm flutter analyze --no-fatal-infos + run: fvm flutter analyze --no-pub --no-fatal-infos build-windows: # Skip the heavy compile on draft PRs; analyze still runs for fast feedback. @@ -89,8 +91,9 @@ jobs: shell: pwsh # Compile only — no msix/installer packaging (that's the release workflow). + # --no-pub: reuse the lockfile-enforced resolution from the Pub get step. - name: Build Windows (unsigned) - run: fvm flutter build windows --release + run: fvm flutter build windows --release --no-pub shell: pwsh build-linux: @@ -140,5 +143,6 @@ jobs: run: fvm flutter pub get --enforce-lockfile # Compile only — no tarball packaging (that's the release workflow). + # --no-pub: reuse the lockfile-enforced resolution from the Pub get step. - name: Build Linux (unsigned) - run: fvm flutter build linux --release + run: fvm flutter build linux --release --no-pub diff --git a/linux/build.sh b/linux/build.sh index 33980ab69b..0b5d8e2df0 100755 --- a/linux/build.sh +++ b/linux/build.sh @@ -19,7 +19,9 @@ fi rm -rf build/linux $FLUTTER_CMD pub get --enforce-lockfile -$FLUTTER_CMD build linux --release -v +# --no-pub: reuse the lockfile-enforced resolution above (build otherwise re-runs +# pub get without --enforce-lockfile). +$FLUTTER_CMD build linux --release -v --no-pub arch=$(uname -m) if [[ $arch == "x86_64" ]]; then diff --git a/windows/build.ps1 b/windows/build.ps1 index f950898aa4..4c77375ec2 100644 --- a/windows/build.ps1 +++ b/windows/build.ps1 @@ -52,11 +52,13 @@ if ($Phase -ne 'Package') { Invoke-Checked $flutterCmd pub get --enforce-lockfile - # Runs `flutter build windows --release` and packages the MS Store MSIX + # Runs `flutter build windows` and packages the MS Store MSIX # (windows\bluebubbles-store.msix). Microsoft signs this one, so pass --store # explicitly (store mode is no longer set in pubspec.yaml). Built from the # unsigned Release output — Microsoft re-signs the package at ingestion. - Invoke-Checked $dartCmd run msix:create --store --output-name bluebubbles-store + # --windows-build-args=--no-pub: the inner `flutter build windows` reuses the + # lockfile-enforced resolution above instead of re-running pub get unenforced. + Invoke-Checked $dartCmd run msix:create --store '--windows-build-args=--no-pub' --output-name bluebubbles-store Get-FileHash 'windows\bluebubbles-store.msix' -Algorithm SHA256 | Format-List Path, Hash } From 64529e11a155b013a75ff56fb1654cb30cf8d2a4 Mon Sep 17 00:00:00 2001 From: Joel Jothiprakasam Date: Sun, 21 Jun 2026 01:23:14 -0700 Subject: [PATCH 40/40] Fix #3054 Signed-off-by: Joel Jothiprakasam --- lib/main.dart | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/main.dart b/lib/main.dart index e0bcfc6ee5..802a48f124 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -211,6 +211,7 @@ Future initApp(bool bubble, List arguments) async { home: Main( lightTheme: light, darkTheme: dark, + savedThemeMode: await AdaptiveTheme.getThemeMode(), ))); } else { runApp(FailureToStart(e: exception, s: stacktrace)); @@ -273,8 +274,9 @@ class DesktopWindowListener extends WindowListener { class Main extends StatelessWidget { final ThemeData darkTheme; final ThemeData lightTheme; + final AdaptiveThemeMode? savedThemeMode; - const Main({super.key, required this.lightTheme, required this.darkTheme}); + const Main({super.key, required this.lightTheme, required this.darkTheme, this.savedThemeMode}); @override Widget build(BuildContext context) { @@ -283,7 +285,7 @@ class Main extends StatelessWidget { textSelectionTheme: TextSelectionThemeData(selectionColor: lightTheme.colorScheme.primary)), dark: darkTheme.copyWith(textSelectionTheme: TextSelectionThemeData(selectionColor: darkTheme.colorScheme.primary)), - initial: AdaptiveThemeMode.system, + initial: savedThemeMode ?? AdaptiveThemeMode.system, builder: (theme, darkTheme) => GetMaterialApp( debugShowCheckedModeBanner: false, title: 'BlueBubbles',