diff --git a/.github/workflows/android.yml b/.github/workflows/android.yml index b9b6207..cf64422 100644 --- a/.github/workflows/android.yml +++ b/.github/workflows/android.yml @@ -27,7 +27,7 @@ jobs: KEYSTORE_CONTENT: ${{ secrets.KEYSTORE_CONTENT }} - name: Build app - run: nix develop --command bash -c "flutter pub get && dart scripts/generate.dart && flutter pub run build_runner build && flutter build apk --target-platform android-arm64 --dart-define=GIT_COMMIT=${{ github.sha }}" + run: nix develop --command bash -c "flutter pub get && dart scripts/generate.dart && flutter pub run build_runner build && flutter build apk --target-platform android-arm64 --split-per-abi --dart-define=GIT_COMMIT=${{ github.sha }}" env: KEYSTORE_PATH: ../../keystore.jks KEYSTORE_PASSWORD: ${{ secrets.KEYSTORE_PASSWORD }} @@ -36,4 +36,4 @@ jobs: uses: actions/upload-artifact@v6 with: name: APK - path: build/app/outputs/flutter-apk/app-release.apk + path: build/app/outputs/flutter-apk/app-arm64-v8a-release.apk diff --git a/android/build.gradle.kts b/android/build.gradle.kts index f82adc9..441eb71 100644 --- a/android/build.gradle.kts +++ b/android/build.gradle.kts @@ -16,23 +16,6 @@ subprojects { project.layout.buildDirectory.value(newSubprojectBuildDir) } -subprojects { - if (name == "flutter_avif_android") { - tasks.withType().configureEach { - exclude("**/FlutterAvifPlugin.kt") - } - afterEvaluate { - extensions.configure { - compileSdk = 36 - compileOptions { - sourceCompatibility = JavaVersion.VERSION_21 - targetCompatibility = JavaVersion.VERSION_21 - } - } - } - } -} - subprojects { project.evaluationDependsOn(":app") } diff --git a/gomuks b/gomuks index ad275f7..7e43a63 160000 --- a/gomuks +++ b/gomuks @@ -1 +1 @@ -Subproject commit ad275f7613894d9d2fa4d16d733a6816e2978de9 +Subproject commit 7e43a63e578b0d6f52269d65297198077bf5b45d diff --git a/hook/build.dart b/hook/build.dart index 9315e04..8726642 100644 --- a/hook/build.dart +++ b/hook/build.dart @@ -122,7 +122,7 @@ Future main(List args) => build(args, (input, output) async { "sqlite_fts5", "goolm", // goheif/dav1d is not supported on Android or iOS, would need to be fixed upstream - if (targetOS == OS.android || targetOS == OS.iOS) "noheic", + "noheic", ].join(","); final archiveFile = targetOS == OS.iOS diff --git a/lib/controllers/client.dart b/lib/controllers/client.dart index a282f81..52b49ee 100644 --- a/lib/controllers/client.dart +++ b/lib/controllers/client.dart @@ -31,6 +31,7 @@ import "package:nexus/models/requests/get_room_state.dart"; import "package:nexus/models/requests/get_space_hierarchy.dart"; import "package:nexus/models/requests/join_room.dart"; import "package:nexus/models/profile_response.dart"; +import "package:nexus/models/push_notification.dart"; import "package:nexus/models/recovery_key.dart"; import "package:nexus/models/requests/oauth/exchange_token.dart"; import "package:nexus/models/requests/oauth/get_auth_url.dart"; @@ -47,7 +48,6 @@ import "package:nexus/models/requests/set_membership.dart"; import "package:nexus/models/requests/set_state.dart"; import "package:nexus/models/requests/upload_media.dart"; import "package:nexus/models/room.dart"; -import "package:nexus/models/room_metadata.dart"; import "package:nexus/models/room_summary.dart"; import "package:nexus/models/space_hierarchy.dart"; import "package:nexus/models/spec_versions_response.dart"; @@ -128,18 +128,15 @@ class ClientController extends AsyncNotifier { } } - Future<(Event, RoomMetadata)?> handlePush(Map data) async { + Future handlePush(Map data) async { final response = await callGomuksMethod( data, (handle, data) async => GomuksHandlePush(handle, data), ); - return response == null + return response?["notification"] == null ? null - : ( - Event.fromJson(response["event"]), - RoomMetadata.fromJson(response["room"]), - ); + : .fromJson(response["notification"]); } Future _sendCommand( diff --git a/lib/controllers/gomuks_listener.dart b/lib/controllers/gomuks_listener.dart index bcf873a..96a21db 100644 --- a/lib/controllers/gomuks_listener.dart +++ b/lib/controllers/gomuks_listener.dart @@ -143,6 +143,19 @@ class GomuksListenerController extends AsyncNotifier { } } + /// Waits for gomuks to report the client's device ID + Future deviceId() { + final completer = Completer(); + final subscription = ref.listen( + ClientStateController.provider.select((value) => value?.deviceId), + (_, next) { + if (next != null && !completer.isCompleted) completer.complete(next); + }, + fireImmediately: true, + ); + return completer.future.whenComplete(subscription.close); + } + static Future _start( int handle, Pointer< diff --git a/lib/controllers/notification.dart b/lib/controllers/notification.dart index 27c2f91..2356c32 100644 --- a/lib/controllers/notification.dart +++ b/lib/controllers/notification.dart @@ -1,4 +1,5 @@ import "dart:async"; +import "dart:convert"; import "dart:io"; import "package:flutter/services.dart"; @@ -103,24 +104,75 @@ class NotificationController return true; } + /// Whether notifications can currently be shown, without prompting + Future permissionsGranted() async { + if (Platform.isLinux) { + return true; + } + + final controller = await future; + + if (Platform.isIOS) { + return (await controller + .resolvePlatformSpecificImplementation< + IOSFlutterLocalNotificationsPlugin + >() + ?.checkPermissions()) + ?.isEnabled ?? + true; + } else if (Platform.isMacOS) { + return (await controller + .resolvePlatformSpecificImplementation< + MacOSFlutterLocalNotificationsPlugin + >() + ?.checkPermissions()) + ?.isEnabled ?? + true; + } else if (Platform.isAndroid) { + return await controller + .resolvePlatformSpecificImplementation< + AndroidFlutterLocalNotificationsPlugin + >() + ?.areNotificationsEnabled() ?? + true; + } + + return true; + } + Future send({ required int id, required String title, String? body, File? icon, String? payload, + String? senderName, + File? senderAvatar, + String? selfName, + bool isGroup = false, + bool sound = true, + DateTime? timestamp, + String? threadId, }) async { debugPrint("Sending notification for $id"); + // In DMs the title is already the sender, so only prefix in groups + final prefix = isGroup ? senderName : null; + if (Platform.isLinux) { - final portal = await ref.watch(PortalController.provider.future); + final portal = await ref.read(PortalController.provider.future); + const escape = HtmlEscape(HtmlEscapeMode.element); await portal.notification.addNotification( id.toString(), title: title, - body: body, + body: prefix == null || body == null ? body : "$prefix: $body", + markupBody: prefix == null || body == null + ? null + : "${escape.convert(prefix)}: ${escape.convert(body)}", defaultAction: "app.event", defaultActionTarget: payload, + sound: sound ? null : const XdgNotificationSoundSilent(), category: XdgNotificationCategory.imReceived, priority: .normal, buttons: [ @@ -133,25 +185,64 @@ class NotificationController ], ); } else { + final controller = await future; + + // Append to the conversation already shown under this id, if any + final existingStyle = Platform.isAndroid + ? await controller + .resolvePlatformSpecificImplementation< + AndroidFlutterLocalNotificationsPlugin + >() + ?.getActiveNotificationMessagingStyle(id: id) + .onError((_, _) => null) + : null; + + final darwin = DarwinNotificationDetails( + presentSound: sound, + threadIdentifier: threadId, + ); final notificationDetails = NotificationDetails( android: .new( "messages", "Messages", + silent: !sound, largeIcon: icon == null ? null : FilePathAndroidBitmap(icon.path), + styleInformation: senderName == null || body == null + ? null + : MessagingStyleInformation( + .new(name: selfName ?? "You"), + conversationTitle: isGroup ? title : null, + groupConversation: isGroup, + messages: [ + ...?existingStyle?.messages, + .new( + body, + timestamp ?? .now(), + .new( + name: senderName, + icon: senderAvatar == null + ? null + : BitmapFilePathAndroidIcon(senderAvatar.path), + ), + ), + ], + ), + ), + iOS: darwin, + macOS: darwin, + windows: .new( + audio: sound ? null : .silent(), + timestamp: timestamp, + header: threadId == null + ? null + : .new(id: threadId, title: title, arguments: payload ?? ""), ), - // TODO: See if icons can be added to iOS, macOS, and Windows - // notifications (#68) - iOS: .new(), - macOS: .new(), - windows: .new(), ); - final controller = await future; - await controller.show( id: id, title: title, - body: body, + body: prefix == null || body == null ? body : "$prefix: $body", notificationDetails: notificationDetails, payload: payload, ); diff --git a/lib/controllers/settings_sections.dart b/lib/controllers/settings_sections.dart index c21d7ef..1ca07a8 100644 --- a/lib/controllers/settings_sections.dart +++ b/lib/controllers/settings_sections.dart @@ -46,7 +46,7 @@ class SettingsSectionsController icon: Icon(icon), title: title, subtitle: Text(description), - initialValue: settings.theme, + value: settings.theme, options: ThemeMode.values, getName: (option) => toBeginningOfSentenceCase(option.name), onChanged: (value) => ref @@ -105,7 +105,7 @@ class SettingsSectionsController icon: Icon(icon), title: title, subtitle: Text(description), - initialValue: settings.roomOrder, + value: settings.roomOrder, options: RoomOrder.values, getName: (option) => switch (option) { .unreadFirst => "Mentioned, then unread", @@ -194,7 +194,6 @@ class SettingsSectionsController final current = pusherRegistered.value; return DialogListTile( - key: ValueKey(current), title: title, subtitle: Text( unifiedPushAllowed.maybeWhen( @@ -204,8 +203,7 @@ class SettingsSectionsController description, ), icon: Icon(icon), - required: false, - initialValue: current, + value: current, options: [ null, ...?distributors.value, @@ -227,21 +225,32 @@ class SettingsSectionsController try { loading.value = true; if (value != null) { - if (await ref + final messenger = ScaffoldMessenger.of( + context, + ); + if (!await ref .read( NotificationController .provider .notifier, ) .requestPermissions()) { - await ref - .read( - UnifiedPushController - .provider - .notifier, - ) - .register(value); + messenger.showSnackBar( + .new( + content: Text( + "Notifications are disabled. Enable them for Nexus in your system settings.", + ), + ), + ); + return; } + await ref + .read( + UnifiedPushController + .provider + .notifier, + ) + .register(value); } else { await ref .read( @@ -283,7 +292,7 @@ class SettingsSectionsController icon: Icon(icon), title: title, subtitle: Text(description), - initialValue: ref + value: ref .watch(AccountDataController.provider) .invitePermissionConfig .defaultAction, diff --git a/lib/controllers/unified_push.dart b/lib/controllers/unified_push.dart index 8dcb295..e862d8d 100644 --- a/lib/controllers/unified_push.dart +++ b/lib/controllers/unified_push.dart @@ -10,9 +10,7 @@ import "package:nexus/controllers/notification.dart"; import "package:nexus/controllers/push_key.dart"; import "package:nexus/main.dart"; import "package:nexus/controllers/client.dart"; -import "package:nexus/controllers/client_state.dart"; -import "package:nexus/models/content/message.dart"; -import "package:nexus/models/content/sticker.dart"; +import "package:nexus/controllers/gomuks_listener.dart"; import "package:unifiedpush/unifiedpush.dart"; import "package:unifiedpush_storage_shared_preferences/storage.dart"; import "package:window_manager/window_manager.dart"; @@ -74,47 +72,67 @@ class UnifiedPushController extends AsyncNotifier { }, onMessage: (message, instance) async { debugPrint("UP message received for $instance"); + if (await deregisterIfDisabled(instance)) { + if (isInBackground) exit(0); + return; + } + if (message.decrypted == false) { throw Exception( "Failed to decrypt notification. Try toggling off and on UnifiedPush in settings.", ); } - final pushResponse = await ref + final notification = await ref .read(ClientController.provider.notifier) .handlePush(json.decode(String.fromCharCodes(message.content))); - if (pushResponse == null) return; - - final (event, roomMetadata) = pushResponse; - - if (event.unreadType?.has(.notify) != true || + if (notification == null || (!isInBackground && await windowManager.isFocused().onError((_, _) => true) && - ref.read(SelectedRoomController.provider) == event.roomId)) { + ref.read(SelectedRoomController.provider) == + notification.roomId)) { if (isInBackground) exit(0); return; } - final icon = roomMetadata.avatar == null + final client = ref.read(ClientController.provider.notifier); + + final icon = notification.roomAvatar == null ? null - : await ref - .read(ClientController.provider.notifier) - .downloadMedia( - .new(mxc: roomMetadata.avatar!, isAvatar: true), - ); + : await client.downloadMedia( + .new(mxc: notification.roomAvatar!, isAvatar: true), + ); + + final pmp = notification.perMessageProfile; + final senderAvatarUrl = pmp?.avatar ?? notification.sender.avatar; + // Only Android can show the sender's avatar + final senderAvatar = senderAvatarUrl == null || !Platform.isAndroid + ? null + : await client.downloadMedia( + .new(mxc: senderAvatarUrl, isAvatar: true), + ); await ref .read(NotificationController.provider.notifier) .send( - id: event.eventId.hashCode & 0x7fffffff, - title: roomMetadata.name ?? "New Event", + // Android stacks messages into one conversation per room + id: + (Platform.isAndroid + ? notification.roomId + : notification.eventId) + .hashCode & + 0x7fffffff, + title: notification.roomName, icon: icon, - payload: event.rowId.toString(), - body: switch (event.content) { - MessageContent(:final body?) || - StickerContent(:final body) => body, - _ => null, - }, + payload: notification.eventRowId.toString(), + body: notification.text, + senderName: pmp?.name ?? notification.sender.name, + senderAvatar: senderAvatar, + selfName: notification.self.name, + isGroup: !notification.isDm, + sound: notification.sound, + timestamp: notification.timestamp, + threadId: notification.roomId, ); if (isInBackground) exit(0); @@ -122,9 +140,11 @@ class UnifiedPushController extends AsyncNotifier { onUnregistered: deregister, ); - if (!registered) return null; + if (!registered || isInBackground) return null; try { + if (await deregisterIfDisabled()) return null; + return await register(); } catch (error, stackTrace) { showError(error, stackTrace); @@ -143,19 +163,9 @@ class UnifiedPushController extends AsyncNotifier { state = .loading(); try { - final deviceIdCompleter = Completer(); - final subscription = ref.listen( - ClientStateController.provider.select((value) => value?.deviceId), - (_, next) { - if (next != null && !deviceIdCompleter.isCompleted) { - deviceIdCompleter.complete(next); - } - }, - fireImmediately: true, - ); - final deviceId = await deviceIdCompleter.future.whenComplete( - subscription.close, - ); + final deviceId = await ref + .read(GomuksListenerController.provider.notifier) + .deviceId(); await Future(() async { final capabilities = await ref @@ -201,12 +211,27 @@ class UnifiedPushController extends AsyncNotifier { } } - Future deregister([String? instance]) async { - final clientState = ref.read(ClientStateController.provider); + Future deregisterIfDisabled([String? notificationInstance]) async { + if (await ref + .read(NotificationController.provider.notifier) + .permissionsGranted()) { + return false; + } - final keyProvider = PushKeyController.provider( - instance ?? clientState!.deviceId!, - ); + final instance = + notificationInstance ?? + await ref.read(GomuksListenerController.provider.notifier).deviceId(); + final key = await ref.read(PushKeyController.provider(instance).future); + if (key != null) await deregister(instance); + return true; + } + + Future deregister([String? instance]) async { + instance ??= await ref + .read(GomuksListenerController.provider.notifier) + .deviceId(); + + final keyProvider = PushKeyController.provider(instance); final key = await ref.read(keyProvider.future); if (key != null) { @@ -220,7 +245,7 @@ class UnifiedPushController extends AsyncNotifier { ); } - await UnifiedPush.unregister(instance ?? clientState!.deviceId!); + await UnifiedPush.unregister(instance); state = .data(null); } diff --git a/lib/helpers/mxc_image.dart b/lib/helpers/mxc_image.dart index 1623029..4013397 100644 --- a/lib/helpers/mxc_image.dart +++ b/lib/helpers/mxc_image.dart @@ -1,8 +1,10 @@ +import "dart:async"; import "dart:io"; +import "dart:isolate"; import "dart:ui" as ui; +import "package:avif_native/avif_native.dart"; import "package:flutter/widgets.dart"; -import "package:flutter_avif/flutter_avif.dart"; import "package:flutter_riverpod/flutter_riverpod.dart"; import "package:nexus/controllers/client.dart"; import "package:nexus/models/requests/download_media.dart"; @@ -24,13 +26,9 @@ class MxcImage(final WidgetRef ref, final DownloadMediaRequest request) final bytes = await File(file.path).readAsBytes(); // Flutter can't decode AVIF on Linux/Windows (or older Android/iOS), so we do it ourselves - final avifType = isAvifFile(bytes.sublist(0, 16.clamp(0, bytes.length))); - if (avifType != .unknown) { - final codec = avifType == .avif - ? SingleFrameAvifCodec(bytes: bytes) - : MultiFrameAvifCodec(key: hashCode, avifBytes: bytes); - await codec.ready(); - return AvifCodecAdapter(codec); + if (String.fromCharCodes(bytes.skip(4).take(8)) + case "ftypavif" || "ftypavis") { + return AvifCodec(await Isolate.run(() => decodeAvif(bytes))); } return decode(await .fromUint8List(bytes)); @@ -44,21 +42,33 @@ class MxcImage(final WidgetRef ref, final DownloadMediaRequest request) int get hashCode => request.hashCode; } -class AvifCodecAdapter(final AvifCodec codec) implements ui.Codec { - @override - int get frameCount => codec.frameCount; +class AvifCodec(final AvifImageData data) implements ui.Codec { + var _frameIndex = 0; @override - int get repetitionCount => frameCount > 1 ? -1 : 0; + int get frameCount => data.frames.length; + + // libavif uses -2 for unknown, which we treat as infinite like browsers do + @override + int get repetitionCount => + data.repetitionCount < 0 ? -1 : data.repetitionCount; @override - Future getNextFrame() async { - final frame = await codec.getNextFrame(); - return AvifFrameInfo(frame.duration, frame.image); + Future getNextFrame() { + final frame = data.frames[_frameIndex++ % frameCount]; + final completer = Completer(); + ui.decodeImageFromPixels( + frame.rgba, + frame.width, + frame.height, + .rgba8888, + (image) => completer.complete(AvifFrameInfo(frame.duration, image)), + ); + return completer.future; } @override - void dispose() => codec.dispose(); + void dispose() {} } class AvifFrameInfo( diff --git a/lib/models/push_notification.dart b/lib/models/push_notification.dart new file mode 100644 index 0000000..859b457 --- /dev/null +++ b/lib/models/push_notification.dart @@ -0,0 +1,44 @@ +import "package:freezed_annotation/freezed_annotation.dart"; +import "package:nexus/models/epoch_date_time_converter.dart"; + +part "push_notification.freezed.dart"; +part "push_notification.g.dart"; + +@Freezed(toJson: false, fromJson: false) +@JsonSerializable(createToJson: false) +class const PushNotification({ + @EpochDateTimeConverter() required final DateTime timestamp, + + required final String eventId, + @JsonKey(name: "event_rowid") required final int eventRowId, + + required final String roomId, + required final String roomName, + final Uri? roomAvatar, + final bool isDm = false, + + required final NotificationUser sender, + required final NotificationUser self, + final NotificationUser? perMessageProfile, + + required final String text, + final Uri? image, + + final bool mention = false, + final bool reply = false, + final bool sound = false, +}) with _$PushNotification { + factory PushNotification.fromJson(Map json) => + _$PushNotificationFromJson(json); +} + +@Freezed(toJson: false, fromJson: false) +@JsonSerializable(createToJson: false) +class const NotificationUser({ + required final String id, + required final String name, + final Uri? avatar, +}) with _$NotificationUser { + factory NotificationUser.fromJson(Map json) => + _$NotificationUserFromJson(json); +} diff --git a/lib/widgets/reaction_row.dart b/lib/widgets/reaction_row.dart index edef81c..0b90781 100644 --- a/lib/widgets/reaction_row.dart +++ b/lib/widgets/reaction_row.dart @@ -53,6 +53,12 @@ class const ReactionRow(final Event event, {super.key}) extends ConsumerWidget { ref, .new(mxc: Uri.parse(reaction)), ), + errorBuilder: (context, _, _) => Icon( + Icons.broken_image, + color: Theme.of(context) + .colorScheme + .error, + ), ) : Text(reaction, overflow: .ellipsis), ), diff --git a/lib/widgets/reset_encryption_sheet.dart b/lib/widgets/reset_encryption_sheet.dart index a3044a1..8482a8f 100644 --- a/lib/widgets/reset_encryption_sheet.dart +++ b/lib/widgets/reset_encryption_sheet.dart @@ -5,6 +5,7 @@ import "package:nexus/controllers/client.dart"; import "package:nexus/controllers/client_state.dart"; import "package:nexus/helpers/extensions/show_adaptive_sheet.dart"; import "package:nexus/main.dart"; +import "package:nexus/models/client_state.dart"; import "package:nexus/widgets/recovery_key_sheet.dart"; import "package:nexus/widgets/sheet_layout.dart"; @@ -16,11 +17,12 @@ class const ResetEncryptionSheet({ }) extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final isReset = ref.watch( + final verificationState = ref.watch( ClientStateController.provider.select( - (state) => state?.verificationState.hasCrossSigning ?? false, + (state) => state?.verificationState, ), ); + final isReset = verificationState?.hasCrossSigning ?? false; final usePassphrase = useState(false); final passphraseController = useTextEditingController(); @@ -95,6 +97,13 @@ class const ResetEncryptionSheet({ ), ], children: [ + if (verificationState case VerificationState( + hasCrossSigning: true, + hasSecretStorage: false, + )) + const Text( + "Your account has an encryption identity, but no recovery key. If you're still signed in on another client, you can set up a recovery key there instead, then restart Nexus and use it to verify. Otherwise, continue with the reset:", + ), Text( isReset ? "This replaces your encryption identity with a new one. Your other sessions will need to be verified again, and any messages you can't currently decrypt may become unreadable." diff --git a/lib/widgets/settings/dialog_list_tile.dart b/lib/widgets/settings/dialog_list_tile.dart index afa79f3..d802d7b 100644 --- a/lib/widgets/settings/dialog_list_tile.dart +++ b/lib/widgets/settings/dialog_list_tile.dart @@ -4,53 +4,35 @@ import "package:flutter_riverpod/flutter_riverpod.dart"; import "package:nexus/widgets/settings/radio_sheet.dart"; final class const DialogListTile({ - required final T? initialValue, + required final T? value, required final Icon icon, required final String title, required final List options, required final void Function(T value)? onChanged, required final String Function(T option) getName, final Widget? subtitle, - final bool required = true, super.key, }) extends ConsumerWidget { @override - Widget build(BuildContext context, WidgetRef ref) => FormField( - validator: (value) => - value == null && required == true ? "This field is required." : null, - initialValue: initialValue, - builder: (field) => InputDecorator( - decoration: InputDecoration( - errorText: field.errorText, - contentPadding: EdgeInsets.zero, - enabledBorder: InputBorder.none, + Widget build(BuildContext context, WidgetRef ref) => ListTile( + enabled: onChanged != null, + onTap: () => context.showAdaptiveSheet( + (context) => RadioSheet( + title: title, + getName: getName, + onChanged: onChanged, + value: value, + options: options, ), - child: ListTile( - enabled: onChanged != null, - onTap: () => context.showAdaptiveSheet( - (context) => RadioSheet( - title: title, - getName: getName, - onChanged: onChanged == null - ? null - : (value) { - field.didChange(value); - onChanged!.call(value); - }, - value: field.value, - options: options, - ), - ), - title: Text(title), - subtitle: subtitle, - leading: icon, - trailing: Chip( - label: Text( - field.value == null ? "None" : getName(field.value as T), - overflow: TextOverflow.ellipsis, - style: onChanged == null ? .new(color: Colors.grey) : null, - ), - ), + ), + title: Text(title), + subtitle: subtitle, + leading: icon, + trailing: Chip( + label: Text( + value == null ? "None" : getName(value as T), + overflow: TextOverflow.ellipsis, + style: onChanged == null ? .new(color: Colors.grey) : null, ), ), ); diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc index d95018b..b2ee366 100644 --- a/linux/flutter/generated_plugin_registrant.cc +++ b/linux/flutter/generated_plugin_registrant.cc @@ -9,7 +9,6 @@ #include #include #include -#include #include #include #include @@ -28,9 +27,6 @@ void fl_register_plugins(FlPluginRegistry* registry) { g_autoptr(FlPluginRegistrar) file_selector_linux_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "FileSelectorPlugin"); file_selector_plugin_register_with_registrar(file_selector_linux_registrar); - g_autoptr(FlPluginRegistrar) flutter_avif_linux_registrar = - fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterAvifLinuxPlugin"); - flutter_avif_linux_plugin_register_with_registrar(flutter_avif_linux_registrar); g_autoptr(FlPluginRegistrar) media_kit_libs_linux_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "MediaKitLibsLinuxPlugin"); media_kit_libs_linux_plugin_register_with_registrar(media_kit_libs_linux_registrar); diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake index 7d212bd..eea220b 100644 --- a/linux/flutter/generated_plugins.cmake +++ b/linux/flutter/generated_plugins.cmake @@ -6,7 +6,6 @@ list(APPEND FLUTTER_PLUGIN_LIST app_links_linux dynamic_color file_selector_linux - flutter_avif_linux media_kit_libs_linux media_kit_video pasteboard diff --git a/linux/nix/devshell.nix b/linux/nix/devshell.nix index bfa5408..6b2b94d 100644 --- a/linux/nix/devshell.nix +++ b/linux/nix/devshell.nix @@ -9,7 +9,6 @@ let ]; cmakeVersions = [ "3.22.1" ]; platformVersions = [ - "31" "34" "36" ]; @@ -48,17 +47,7 @@ pkgs.mkShell { GRADLE_OPTS = "-Dorg.gradle.project.android.aapt2FromMavenOverride=${ANDROID_HOME}/build-tools/36.0.0/aapt2"; } - // lib.optionalAttrs pkgs.stdenv.isLinux ( - let - arch = if pkgs.stdenv.hostPlatform.isAarch64 then "arm64" else "x64"; - in - { - LD_LIBRARY_PATH = lib.concatStringsSep ":" [ - "./build/native_assets/linux" - "./build/linux/${arch}/debug/bundle/lib" - "./build/linux/${arch}/release/bundle/lib" - (lib.makeLibraryPath [ pkgs.zlib ]) - ]; - } - ); + // lib.optionalAttrs pkgs.stdenv.isLinux { + LD_LIBRARY_PATH = "./build/native_assets/linux:${lib.makeLibraryPath [ pkgs.zlib ]}"; + }; } diff --git a/linux/nix/pkg/avif_native.nix b/linux/nix/pkg/avif_native.nix new file mode 100644 index 0000000..16d0fc0 --- /dev/null +++ b/linux/nix/pkg/avif_native.nix @@ -0,0 +1,49 @@ +# avif_native's build hook downloads its prebuilt library, which the sandbox +# can't do, so bundle it and point the manifest at the local copy instead +{ + stdenv, + fetchurl, + jq, +}: +let + artifacts = { + x86_64-linux = { + arch = "x64"; + sha256 = "252b064f8ff4f0f265bc44294d4a7ed703dd7199688942c272dd8aa8d3039d5a"; + }; + aarch64-linux = { + arch = "arm64"; + sha256 = "cdcf17cfd18b9b0f965b72615ddf11246121114577dc570d0bba25964e56c726"; + }; + }; + inherit (artifacts.${stdenv.hostPlatform.system}) arch sha256; + key = "linux-${arch}-decode-dav1d"; +in +{ version, src, ... }: +stdenv.mkDerivation { + pname = "avif_native"; + inherit version src; + inherit (src) passthru; + + nativeBuildInputs = [ jq ]; + + library = fetchurl { + url = "https://github.com/Zhangw1998/avif_native/releases/download/native-v0.1.0/${key}-libavif_native.so.gz"; + inherit sha256; + }; + + # The hook checksums the library, so it must stay byte-identical + dontFixup = true; + + installPhase = '' + runHook preInstall + + cp -r . $out + chmod -R u+w $out + gunzip -c $library > $out/native/libavif_native.so + jq '.artifacts["${key}"].path = "libavif_native.so"' \ + native/manifest.json > $out/native/manifest.json + + runHook postInstall + ''; +} diff --git a/linux/nix/pkg/default.nix b/linux/nix/pkg/default.nix index 2c66fb8..f2bbbff 100644 --- a/linux/nix/pkg/default.nix +++ b/linux/nix/pkg/default.nix @@ -34,6 +34,8 @@ flutter.buildFlutterApplication { autoPubspecLock = src + "/pubspec.lock"; + customSourceBuilders.avif_native = callPackage ./avif_native.nix { }; + gitHashes = { linkify = "sha256-TpMD6+0zyY6i9l+6d8ErnVufmepCv362rCtnbOht/z4="; fab_m3e = "sha256-5V9m6yY0jimx2gh7lQHYg4HDKQ4Wrl0ATmBhw5DkQMI="; diff --git a/linux/nix/pkg/gomuks.nix b/linux/nix/pkg/gomuks.nix index 604f5be..6c383d2 100644 --- a/linux/nix/pkg/gomuks.nix +++ b/linux/nix/pkg/gomuks.nix @@ -17,7 +17,7 @@ buildGoModule ( src = "${src}/gomuks"; - vendorHash = "sha256-+0iFK2ribhiNskXgPz7vslncuOgPMB/ZmFSfDOcz6ww="; + vendorHash = "sha256-nd0NEKXscgrFE/gBEW2aP+YeZNTOdbRa4QDLTRwIAZU="; buildPhase = '' runHook preBuild diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index c801836..2a15f97 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -8,7 +8,6 @@ import Foundation import app_links import dynamic_color import file_selector_macos -import flutter_avif_macos import flutter_local_notifications import media_kit_libs_macos_video import media_kit_video @@ -16,7 +15,6 @@ import package_info_plus import pasteboard import screen_retriever_macos import shared_preferences_foundation -import sqflite_darwin import url_launcher_macos import wakelock_plus import webcrypto @@ -26,7 +24,6 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { AppLinksMacosPlugin.register(with: registry.registrar(forPlugin: "AppLinksMacosPlugin")) DynamicColorPlugin.register(with: registry.registrar(forPlugin: "DynamicColorPlugin")) FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin")) - FlutterAvifPlugin.register(with: registry.registrar(forPlugin: "FlutterAvifPlugin")) FlutterLocalNotificationsPlugin.register(with: registry.registrar(forPlugin: "FlutterLocalNotificationsPlugin")) MediaKitLibsMacosVideoPlugin.register(with: registry.registrar(forPlugin: "MediaKitLibsMacosVideoPlugin")) MediaKitVideoPlugin.register(with: registry.registrar(forPlugin: "MediaKitVideoPlugin")) @@ -34,7 +31,6 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { PasteboardPlugin.register(with: registry.registrar(forPlugin: "PasteboardPlugin")) ScreenRetrieverMacosPlugin.register(with: registry.registrar(forPlugin: "ScreenRetrieverMacosPlugin")) SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) - SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin")) UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) WakelockPlusMacosPlugin.register(with: registry.registrar(forPlugin: "WakelockPlusMacosPlugin")) WebcryptoPlugin.register(with: registry.registrar(forPlugin: "WebcryptoPlugin")) diff --git a/pubspec.lock b/pubspec.lock index bf9c326..1958e04 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -105,6 +105,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.13.1" + avif_native: + dependency: "direct main" + description: + name: avif_native + sha256: "0a374a7ddc6e48d671792bd33efd9f46f199389cdcd42561210bd886facbc62e" + url: "https://pub.dev" + source: hosted + version: "0.1.1" boolean_selector: dependency: transitive description: @@ -313,14 +321,6 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.0" - exif: - dependency: transitive - description: - name: exif - sha256: a7980fdb3b7ffcd0b035e5b8a5e1eef7cadfe90ea6a4e85ebb62f87b96c7a172 - url: "https://pub.dev" - source: hosted - version: "3.3.0" fab_m3e: dependency: "direct overridden" description: @@ -455,70 +455,6 @@ packages: description: flutter source: sdk version: "0.0.0" - flutter_avif: - dependency: "direct main" - description: - name: flutter_avif - sha256: "6035f073189c1ae134affa73338c2eca79bf412e9abdb8f8628fdefcccff4444" - url: "https://pub.dev" - source: hosted - version: "3.1.0" - flutter_avif_android: - dependency: transitive - description: - name: flutter_avif_android - sha256: "1da135ea0d74225fae3154f954f9ba4cf7aa5e56b30d9851fcfc0ddecab9d8c5" - url: "https://pub.dev" - source: hosted - version: "3.1.0" - flutter_avif_ios: - dependency: transitive - description: - name: flutter_avif_ios - sha256: "3daaa599d8fe0193d3ade6cafa1fd4f166d4063b8d499c7c311ce1d170b2759f" - url: "https://pub.dev" - source: hosted - version: "3.1.0" - flutter_avif_linux: - dependency: transitive - description: - name: flutter_avif_linux - sha256: dd6179edca12c720761b3acc678d7f15208eb8647adbac5a6a90f52d4ba56e2d - url: "https://pub.dev" - source: hosted - version: "3.1.0" - flutter_avif_macos: - dependency: transitive - description: - name: flutter_avif_macos - sha256: "8e39f365dfc5713d4527f07a92399184857ea30eced85f82171bfd0a45e2c73b" - url: "https://pub.dev" - source: hosted - version: "3.1.0" - flutter_avif_platform_interface: - dependency: transitive - description: - name: flutter_avif_platform_interface - sha256: f5c110bdb7de2d4ab4902bfe2ec331fe24bc19b6d54f00ac4fa165effc0edcda - url: "https://pub.dev" - source: hosted - version: "3.1.0" - flutter_avif_web: - dependency: transitive - description: - name: flutter_avif_web - sha256: db2ff395b08cbfe9116e48ac65c428ba21a9159972ea019f8d7be580ff6cf6e8 - url: "https://pub.dev" - source: hosted - version: "3.1.0" - flutter_avif_windows: - dependency: transitive - description: - name: flutter_avif_windows - sha256: e0c7722df305a6621f4ecdd83098025c705bcf0e8ce9519e03c6f515abcf351d - url: "https://pub.dev" - source: hosted - version: "3.1.0" flutter_blurhash: dependency: "direct main" description: @@ -527,14 +463,6 @@ packages: url: "https://pub.dev" source: hosted version: "0.9.1" - flutter_cache_manager: - dependency: transitive - description: - name: flutter_cache_manager - sha256: f94fc6f5480977e3eb7b622ed488f671ba96c971bd0e274e6bd0f0ba9b044816 - url: "https://pub.dev" - source: hosted - version: "3.4.5" flutter_file_dialog: dependency: "direct main" description: @@ -1265,14 +1193,6 @@ packages: url: "https://pub.dev" source: hosted version: "5.0.6" - protobuf: - dependency: transitive - description: - name: protobuf - sha256: "68645b24e0716782e58948f8467fd42a880f255096a821f9e7d0ec625b00c84d" - url: "https://pub.dev" - source: hosted - version: "3.1.0" pub_semver: dependency: transitive description: @@ -1486,54 +1406,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.10.2" - sprintf: - dependency: transitive - description: - name: sprintf - sha256: "1fc9ffe69d4df602376b52949af107d8f5703b77cda567c4d7d86a0693120f23" - url: "https://pub.dev" - source: hosted - version: "7.0.0" - sqflite: - dependency: transitive - description: - name: sqflite - sha256: "273218a1678360a3127fcf15e543381d9b813d413d783bc6e9a53eed4d2eb8fe" - url: "https://pub.dev" - source: hosted - version: "2.4.4+2" - sqflite_android: - dependency: transitive - description: - name: sqflite_android - sha256: e4f659c14bdd14464127fcd234a7a96f61fe0f539b8568e5a38f99f5a568a835 - url: "https://pub.dev" - source: hosted - version: "2.4.4+1" - sqflite_common: - dependency: transitive - description: - name: sqflite_common - sha256: b80e94ff0d551d5f1f8c7d14348d29b576cca3ae30f6e195a7c380552c32b1e0 - url: "https://pub.dev" - source: hosted - version: "2.5.14" - sqflite_darwin: - dependency: transitive - description: - name: sqflite_darwin - sha256: "8de68fc306040f992aaf378750c16f95e4298c4bb150dd45091d75e42b1f08ee" - url: "https://pub.dev" - source: hosted - version: "2.4.4+1" - sqflite_platform_interface: - dependency: transitive - description: - name: sqflite_platform_interface - sha256: c9177b2f3150ed0647b90a94307c28452fe0a5401863044216aed83addd6c7c5 - url: "https://pub.dev" - source: hosted - version: "2.4.2" stack_trace: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index b276bea..c93eaac 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -109,7 +109,7 @@ dependencies: pasteboard: 0.5.0 flutter_file_dialog: 3.3.3 mime: 2.1.0 - flutter_avif: 3.1.0 + avif_native: 0.1.1 dev_dependencies: build_runner: 2.16.2 flutter_lints: 6.0.0 @@ -133,3 +133,8 @@ flutter_launcher_icons: macos: generate: true image_path: assets/mobile.png + +hooks: + user_defines: + avif_native: + features: decode diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index 13fb733..95357c4 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 @@ -25,8 +24,6 @@ void RegisterPlugins(flutter::PluginRegistry* registry) { registry->GetRegistrarForPlugin("DynamicColorPluginCApi")); FileSelectorWindowsRegisterWithRegistrar( registry->GetRegistrarForPlugin("FileSelectorWindows")); - FlutterAvifWindowsPluginRegisterWithRegistrar( - registry->GetRegistrarForPlugin("FlutterAvifWindowsPlugin")); MediaKitLibsWindowsVideoPluginCApiRegisterWithRegistrar( registry->GetRegistrarForPlugin("MediaKitLibsWindowsVideoPluginCApi")); MediaKitVideoPluginCApiRegisterWithRegistrar( diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index 1822f53..a7b2473 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -6,7 +6,6 @@ list(APPEND FLUTTER_PLUGIN_LIST app_links dynamic_color file_selector_windows - flutter_avif_windows media_kit_libs_windows_video media_kit_video pasteboard