diff --git a/flutter_local_notifications/docs/windows-setup.md b/flutter_local_notifications/docs/windows-setup.md index 84178184c..e2f9f9f16 100644 --- a/flutter_local_notifications/docs/windows-setup.md +++ b/flutter_local_notifications/docs/windows-setup.md @@ -33,6 +33,14 @@ The [msix package](https://pub.dev/packages/msix) can help generate an MSIX inst | Package name | `identity_name` | `appUserModelId` | | Unique ID | `toast_activator.clsid` | `guid` | -The display name is set in the MSIX and cannot be changed in Dart. The GUID/CLSID, as the name implies, needs to be _globally unique_. Avoid using IDs from tutorials as other apps on the user's device may be using them as well. Instead, use [online GUID generators](https://guidgenerator.com) to generate a new, unique GUID, and use that for your MSIX and Dart options. +The display name is set in the MSIX and cannot be changed in Dart. The GUID/CLSID, as the name implies, needs to be _globally unique_. Avoid using IDs from tutorials as other apps on the user's device may be using them as well. Instead, use [online GUID generators](https://guidgenerator.com) to generate a new, unique GUID, and use that for your MSIX and Dart options. For a full example, see the `msix_config` section of [the example app's `pubspec.yaml`](https://github.com/MaikuB/flutter_local_notifications/blob/master/flutter_local_notifications/example/pubspec.yaml). + +## Custom icons + +For MSIX builds, you'll need to specify the `logo_path` variable in your MSIX configuration (see above). For debug and non-MSIX release builds, your icon will need to be a Flutter asset. To show a custom icon for a specific notification, use a [WindowsImage] with [WindowsImagePlacement.appLogoOverride] in your notification details. + +> [!Note] +> +> For non-MSIX builds, the path to the icon will be set at runtime and won't change until your app is launched again. If, for example, your app is launched, a notification is scheduled, your app is closed, then your app's installation directory changes, the notification will not have an icon. diff --git a/flutter_local_notifications/example/lib/main.dart b/flutter_local_notifications/example/lib/main.dart index 6167847a0..399b9d423 100644 --- a/flutter_local_notifications/example/lib/main.dart +++ b/flutter_local_notifications/example/lib/main.dart @@ -1121,7 +1121,7 @@ class _HomePageState extends State { WindowsAction( content: 'Image', arguments: 'image', - imageUri: WindowsImage.getAssetUri('icons/coworker.png'), + imageUri: WindowsAssetUtils.getAssetUri('icons/coworker.png'), ), const WindowsAction( content: 'Context', diff --git a/flutter_local_notifications/example/lib/windows.dart b/flutter_local_notifications/example/lib/windows.dart index 025c8b186..32e0a13e9 100644 --- a/flutter_local_notifications/example/lib/windows.dart +++ b/flutter_local_notifications/example/lib/windows.dart @@ -11,6 +11,7 @@ const WindowsInitializationSettings initSettings = appName: 'Flutter Local Notifications Example', appUserModelId: 'Com.Dexterous.FlutterLocalNotificationsExample', guid: 'd49b0314-ee7a-4626-bf79-97cdb8a991bb', + iconAssetPath: 'icons/coworker.png', ); class _WindowsXmlBuilder extends StatefulWidget { @@ -240,7 +241,7 @@ Future _showWindowsNotificationWithImages() => windows: WindowsNotificationDetails( images: [ WindowsImage( - WindowsImage.getAssetUri('icons/4.0x/app_icon_density.png'), + WindowsAssetUtils.getAssetUri('icons/4.0x/app_icon_density.png'), altText: 'A beautiful image', ), ], @@ -260,7 +261,7 @@ Future _showWindowsNotificationWithGroups() => WindowsRow([ WindowsColumn([ WindowsImage( - WindowsImage.getAssetUri('icons/coworker.png'), + WindowsAssetUtils.getAssetUri('icons/coworker.png'), altText: 'A local image', ), const WindowsNotificationText( diff --git a/flutter_local_notifications/example/pubspec.yaml b/flutter_local_notifications/example/pubspec.yaml index 21edacde6..a9db0f408 100644 --- a/flutter_local_notifications/example/pubspec.yaml +++ b/flutter_local_notifications/example/pubspec.yaml @@ -41,6 +41,7 @@ msix_config: store: false install_certificate: false output_name: example + logo_path: icons/app_icon.png toast_activator: clsid: "d49b0314-ee7a-4626-bf79-97cdb8a991bb" arguments: "msix-args" diff --git a/flutter_local_notifications_windows/lib/flutter_local_notifications_windows.dart b/flutter_local_notifications_windows/lib/flutter_local_notifications_windows.dart index ed3b9c824..8cccfd146 100644 --- a/flutter_local_notifications_windows/lib/flutter_local_notifications_windows.dart +++ b/flutter_local_notifications_windows/lib/flutter_local_notifications_windows.dart @@ -1,3 +1,4 @@ export 'src/details.dart'; -export 'src/msix/stub.dart' if (dart.library.ffi) 'src/msix/ffi.dart'; +export 'src/msix.dart'; export 'src/plugin/stub.dart' if (dart.library.ffi) 'src/plugin/ffi.dart'; +export 'src/utils.dart'; diff --git a/flutter_local_notifications_windows/lib/src/details/initialization_settings.dart b/flutter_local_notifications_windows/lib/src/details/initialization_settings.dart index 507d85929..97377489a 100644 --- a/flutter_local_notifications_windows/lib/src/details/initialization_settings.dart +++ b/flutter_local_notifications_windows/lib/src/details/initialization_settings.dart @@ -1,3 +1,5 @@ +import 'notification_parts.dart'; + /// Plugin initialization settings for Windows. class WindowsInitializationSettings { /// Creates a new settings object for initializing this plugin on Windows. @@ -5,7 +7,7 @@ class WindowsInitializationSettings { required this.appName, required this.appUserModelId, required this.guid, - this.iconPath, + required this.iconAssetPath, }); /// The name of the app that should be shown in the notification toast. @@ -21,6 +23,14 @@ class WindowsInitializationSettings { /// The GUID that identifies the notification activation callback. final String guid; - /// The path to the icon of the notification. - final String? iconPath; + /// The asset path for the default icon. + /// + /// This icon must be a Flutter asset declared in the Pubspec, and will be + /// shown by default. To override it on a specific notification (say, to show + /// a user profile picture instead), use a [WindowsImage] with + /// [WindowsImagePlacement.appLogoOverride] in your notification details. + /// + /// Note that for MSIX releases, you must configure the default icon in your + /// MSIX configuration. See the Windows Setup Guide for more details. + final String iconAssetPath; } diff --git a/flutter_local_notifications_windows/lib/src/details/notification_audio.dart b/flutter_local_notifications_windows/lib/src/details/notification_audio.dart index 4c5ff8474..1ba3e11e6 100644 --- a/flutter_local_notifications_windows/lib/src/details/notification_audio.dart +++ b/flutter_local_notifications_windows/lib/src/details/notification_audio.dart @@ -1,4 +1,4 @@ -import '../../flutter_local_notifications_windows.dart'; +import '../msix.dart'; /// A preset sound for a Windows notification. enum WindowsNotificationSound { diff --git a/flutter_local_notifications_windows/lib/src/details/notification_parts.dart b/flutter_local_notifications_windows/lib/src/details/notification_parts.dart index 54e079292..04ade114b 100644 --- a/flutter_local_notifications_windows/lib/src/details/notification_parts.dart +++ b/flutter_local_notifications_windows/lib/src/details/notification_parts.dart @@ -1,7 +1,4 @@ -import 'dart:io'; - -import 'package:flutter/foundation.dart'; -import '../../flutter_local_notifications_windows.dart'; +import '../utils.dart'; /// A text or image element in a Windows notification. /// @@ -37,15 +34,16 @@ enum WindowsImageCrop { /// depending on if your app is packaged as an MSIX. Refer to the following: /// /// | URI | Debug | Release (EXE) | Release (MSIX) | -/// |--------|--------|--------|--------| -/// | `http(s)://` | ❌ | ❌ | ✅ | -/// | `ms-appx://` | ❌ | ❌ | ✅ | -/// | `file:///` | ✅ | ✅ | 🟨 | +/// |-----------------|-- --|----|-----| +/// | `http(s)://` | ❌ | ❌ | ✅ | +/// | `ms-appx://` | ❌ | ❌ | ✅ | +/// | `file:///` | ✅ | ✅ | 🟨 | /// | `getAssetUri()` | ✅ | ✅ | ✅ | /// /// Each URI type has different uses: -/// - For Flutter assets, use [getAssetUri], which return the correct file URI -/// for debug and release (exe) builds, and an `ms-appx` URI in MSIX builds. +/// - For Flutter assets, use [WindowsAssetUtils.getAssetUri], which return the +/// correct file URI for debug and release (exe) builds, and an `ms-appx` URI in +/// MSIX builds. /// - For images from the web, use an `https` or `http` URI, but note that /// these only work in MSIX apps. If you need a network image without using /// MSIX, consider downloading it directly and using a file URI after. Also @@ -67,24 +65,6 @@ class WindowsImage extends WindowsNotificationPart { this.crop, }); - /// Returns a URI for a [Flutter asset](https://docs.flutter.dev/ui/assets/assets-and-images#loading-images). - /// - /// - In debug mode, resolves to a file URI to the asset itself - /// - In non-MSIX release builds, resolves to a file URI to the bundled asset - /// - In MSIX releases, resolves to an `ms-appx` URI from [Msix.getAssetUri]. - static Uri getAssetUri(String assetName) { - if (kDebugMode) { - return Uri.file(File(assetName).absolute.path, windows: true); - } else if (MsixUtils.hasPackageIdentity()) { - return MsixUtils.getAssetUri(assetName); - } else { - return Uri.file( - File('data/flutter_assets/$assetName').absolute.path, - windows: true, - ); - } - } - /// Whether Windows should add URL query parameters when fetching the image. final bool addQueryParams; diff --git a/flutter_local_notifications_windows/lib/src/details/notification_progress.dart b/flutter_local_notifications_windows/lib/src/details/notification_progress.dart index f7847a422..69ec18b87 100644 --- a/flutter_local_notifications_windows/lib/src/details/notification_progress.dart +++ b/flutter_local_notifications_windows/lib/src/details/notification_progress.dart @@ -1,5 +1,3 @@ -import '../../flutter_local_notifications_windows.dart'; - /// A progress bar in a Windows notification. /// /// To update the progress after the notification has been shown, diff --git a/flutter_local_notifications_windows/lib/src/details/notification_to_xml.dart b/flutter_local_notifications_windows/lib/src/details/notification_to_xml.dart index 67184c1b1..d58e30bd4 100644 --- a/flutter_local_notifications_windows/lib/src/details/notification_to_xml.dart +++ b/flutter_local_notifications_windows/lib/src/details/notification_to_xml.dart @@ -1,6 +1,6 @@ import 'package:xml/xml.dart'; -import '../../flutter_local_notifications_windows.dart'; +import '../details.dart'; import 'xml/details.dart'; export 'xml/progress.dart'; diff --git a/flutter_local_notifications_windows/lib/src/msix.dart b/flutter_local_notifications_windows/lib/src/msix.dart new file mode 100644 index 000000000..d956ac886 --- /dev/null +++ b/flutter_local_notifications_windows/lib/src/msix.dart @@ -0,0 +1 @@ +export 'msix/stub.dart' if (dart.library.ffi) 'msix/ffi.dart'; diff --git a/flutter_local_notifications_windows/lib/src/msix/ffi.dart b/flutter_local_notifications_windows/lib/src/msix/ffi.dart index a80eb122d..2648d1c39 100644 --- a/flutter_local_notifications_windows/lib/src/msix/ffi.dart +++ b/flutter_local_notifications_windows/lib/src/msix/ffi.dart @@ -20,7 +20,7 @@ class MsixUtils { /// /// These functions will simply do nothing or return empty data in apps /// without package identity. Additionally: - /// - [WindowsImage.getAssetUri] will return a `file:///` or `ms-appx:///` URI, + /// - [WindowsAssetUtils.getAssetUri] will return a `file:///` or `ms-appx:///` URI, /// depending on whether the app is running in debug, release, or as an MSIX. /// - [WindowsNotificationAudio.asset] takes an audio file to use for apps /// with package identity, and a preset fallbacks for apps without. diff --git a/flutter_local_notifications_windows/lib/src/msix/stub.dart b/flutter_local_notifications_windows/lib/src/msix/stub.dart index 49f148a8e..6e3e41dbf 100644 --- a/flutter_local_notifications_windows/lib/src/msix/stub.dart +++ b/flutter_local_notifications_windows/lib/src/msix/stub.dart @@ -16,7 +16,7 @@ class MsixUtils { /// /// These functions will simply do nothing or return empty data in apps /// without package identity. Additionally: - /// - [WindowsImage.getAssetUri] will return a `file:///` or `ms-appx:///` URI, + /// - [WindowsAssetUtils.getAssetUri] will return a `file:///` or `ms-appx:///` URI, /// depending on whether the app is running in debug, release, or as an MSIX. /// - [WindowsNotificationAudio.asset] takes an audio file to use for apps /// with package identity, and a preset fallbacks for apps without. diff --git a/flutter_local_notifications_windows/lib/src/plugin/ffi.dart b/flutter_local_notifications_windows/lib/src/plugin/ffi.dart index 9e13258eb..6ecf014e1 100644 --- a/flutter_local_notifications_windows/lib/src/plugin/ffi.dart +++ b/flutter_local_notifications_windows/lib/src/plugin/ffi.dart @@ -6,6 +6,7 @@ import '../details.dart'; import '../details/notification_to_xml.dart'; import '../ffi/bindings.dart'; import '../ffi/utils.dart'; +import '../utils.dart'; import 'base.dart'; @@ -85,7 +86,9 @@ class FlutterLocalNotificationsWindows extends WindowsNotificationsBase { settings.appUserModelId.toNativeUtf8(allocator: arena); final Pointer guid = settings.guid.toNativeUtf8(allocator: arena); final Pointer iconPath = - settings.iconPath?.toNativeUtf8(allocator: arena) ?? nullptr; + WindowsAssetUtils.getAssetFile(settings.iconAssetPath) + ?.windowsPath.toNativeUtf8(allocator: arena) + ?? nullptr; final NativeNotificationCallback callback = NativeCallable.listener( _globalLaunchCallback, diff --git a/flutter_local_notifications_windows/lib/src/utils.dart b/flutter_local_notifications_windows/lib/src/utils.dart new file mode 100644 index 000000000..e29ec4c98 --- /dev/null +++ b/flutter_local_notifications_windows/lib/src/utils.dart @@ -0,0 +1,48 @@ +import 'dart:io'; + +import 'package:flutter/foundation.dart'; + +import 'msix.dart'; + +/// Utility methods for resolving Flutter assets for different build modes. +class WindowsAssetUtils { + /// Returns a [File] for a [Flutter asset](https://docs.flutter.dev/ui/assets/assets-and-images#loading-images). + /// + /// - In debug mode, resolves to a file to the asset itself + /// - In non-MSIX release builds, resolves to a file to the bundled asset + /// - In MSIX releases, resolves to null + /// + /// MSIX bundles don't support getting the file path for assets. Use + /// [getAssetUri] to get an `ms-appx`-style [Uri] instead of a [File]. + static File? getAssetFile(String assetPath) { + if (kDebugMode) { + return File(assetPath); + } else if (MsixUtils.hasPackageIdentity()) { + return null; // msix has its own icon in the msix_config + } else { + return File('data/flutter_assets/$assetPath'); + } + } + + /// Returns a [Uri] for a [Flutter asset](https://docs.flutter.dev/ui/assets/assets-and-images#loading-images). + /// + /// - In debug mode, resolves to a file URI to the asset itself + /// - In non-MSIX release builds, resolves to a file URI to the bundled asset + /// - In MSIX releases, resolves to `ms-appx` URI from [MsixUtils.getAssetUri] + static Uri getAssetUri(String assetPath) { + if (kDebugMode) { + return Uri.file(File(assetPath).windowsPath, windows: true); + } else if (MsixUtils.hasPackageIdentity()) { + return MsixUtils.getAssetUri(assetPath); + } else { + final File file = File('data/flutter_assets/$assetPath'); + return Uri.file(file.windowsPath, windows: true); + } + } +} + +/// Utility methods for files on Windows. +extension WindowsFileUtils on File { + /// Returns a Windows-style path for this file. + String get windowsPath => absolute.path.replaceAll('/', r'\'); +} diff --git a/flutter_local_notifications_windows/test/bindings_test.dart b/flutter_local_notifications_windows/test/bindings_test.dart index 0f1c921ab..dd64804fc 100644 --- a/flutter_local_notifications_windows/test/bindings_test.dart +++ b/flutter_local_notifications_windows/test/bindings_test.dart @@ -5,6 +5,7 @@ const WindowsInitializationSettings settings = WindowsInitializationSettings( appName: 'Test app', appUserModelId: 'com.test.test', guid: 'a8c22b55-049e-422f-b30f-863694de08c8', + iconAssetPath: 'icon.png', ); const Map bindings = { diff --git a/flutter_local_notifications_windows/test/details_test.dart b/flutter_local_notifications_windows/test/details_test.dart index ea90159cd..476012afa 100644 --- a/flutter_local_notifications_windows/test/details_test.dart +++ b/flutter_local_notifications_windows/test/details_test.dart @@ -6,6 +6,7 @@ const WindowsInitializationSettings settings = WindowsInitializationSettings( appName: 'Test app', appUserModelId: 'com.test.test', guid: 'a8c22b55-049e-422f-b30f-863694de08c8', + iconAssetPath: 'icon.png', ); extension PluginUtils on FlutterLocalNotificationsWindows { @@ -66,7 +67,7 @@ void main() => group('Details:', () { buttonStyle: WindowsButtonStyle.success, inputId: 'input-id', tooltip: 'tooltip', - imageUri: WindowsImage.getAssetUri('test/icon.png'), + imageUri: WindowsAssetUtils.getAssetUri('test/icon.png'), ); plugin ..testDetails(const WindowsNotificationDetails( @@ -97,7 +98,7 @@ void main() => group('Details:', () { const WindowsColumn emptyColumn = WindowsColumn([]); final WindowsImage image = WindowsImage( - WindowsImage.getAssetUri('test/icon.png'), + WindowsAssetUtils.getAssetUri('test/icon.png'), altText: 'an icon', ); const WindowsNotificationText text = @@ -136,7 +137,7 @@ void main() => group('Details:', () { test('Images', () async { final WindowsImage simpleImage = WindowsImage( - WindowsImage.getAssetUri('asset.png'), + WindowsAssetUtils.getAssetUri('asset.png'), altText: 'an icon', ); final WindowsImage complexImage = WindowsImage( diff --git a/flutter_local_notifications_windows/test/plugin_test.dart b/flutter_local_notifications_windows/test/plugin_test.dart index 7564ec322..30e995bf4 100644 --- a/flutter_local_notifications_windows/test/plugin_test.dart +++ b/flutter_local_notifications_windows/test/plugin_test.dart @@ -9,12 +9,14 @@ const WindowsInitializationSettings goodSettings = appName: 'test', appUserModelId: 'com.test.test', guid: 'a8c22b55-049e-422f-b30f-863694de08c8', + iconAssetPath: 'icon.png', ); const WindowsInitializationSettings badSettings = WindowsInitializationSettings( appName: 'test', appUserModelId: 'com.test.test', guid: '123', + iconAssetPath: 'icon.png', ); void main() => group('Plugin', () { diff --git a/flutter_local_notifications_windows/test/scheduled_test.dart b/flutter_local_notifications_windows/test/scheduled_test.dart index 08e2cbbf9..b2c8521d0 100644 --- a/flutter_local_notifications_windows/test/scheduled_test.dart +++ b/flutter_local_notifications_windows/test/scheduled_test.dart @@ -4,9 +4,11 @@ import 'package:timezone/data/latest_all.dart'; import 'package:timezone/standalone.dart'; const WindowsInitializationSettings settings = WindowsInitializationSettings( - appName: 'Test app', - appUserModelId: 'com.test.test', - guid: 'a8c22b55-049e-422f-b30f-863694de08c8'); + appName: 'Test app', + appUserModelId: 'com.test.test', + guid: 'a8c22b55-049e-422f-b30f-863694de08c8', + iconAssetPath: 'icon.png', +); void main() => group('Schedules', () { final FlutterLocalNotificationsWindows plugin = diff --git a/flutter_local_notifications_windows/test/xml_test.dart b/flutter_local_notifications_windows/test/xml_test.dart index e2998f190..23a825083 100644 --- a/flutter_local_notifications_windows/test/xml_test.dart +++ b/flutter_local_notifications_windows/test/xml_test.dart @@ -5,6 +5,7 @@ const WindowsInitializationSettings settings = WindowsInitializationSettings( appName: 'test', appUserModelId: 'com.test.test', guid: 'a8c22b55-049e-422f-b30f-863694de08c8', + iconAssetPath: 'icon.png', ); const String emptyXml = '';