Compare commits
	
		
			4 Commits
		
	
	
		
			2b237eaad9
			...
			3.0.0+107
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 825e6b5b6d | |||
| 2a3276973c | |||
| f4e10afa8f | |||
| 60c5e584be | 
@@ -51,8 +51,6 @@ android {
 | 
				
			|||||||
    buildTypes {
 | 
					    buildTypes {
 | 
				
			||||||
        release {
 | 
					        release {
 | 
				
			||||||
            signingConfig = signingConfigs.getByName("release")
 | 
					            signingConfig = signingConfigs.getByName("release")
 | 
				
			||||||
            minifyEnabled = true
 | 
					 | 
				
			||||||
            shrinkResources = true
 | 
					 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							@@ -10,6 +10,7 @@ import 'package:gap/gap.dart';
 | 
				
			|||||||
import 'package:island/pods/config.dart';
 | 
					import 'package:island/pods/config.dart';
 | 
				
			||||||
import 'package:island/pods/network.dart';
 | 
					import 'package:island/pods/network.dart';
 | 
				
			||||||
import 'package:island/services/udid.dart';
 | 
					import 'package:island/services/udid.dart';
 | 
				
			||||||
 | 
					import 'package:island/widgets/alert.dart';
 | 
				
			||||||
import 'package:island/widgets/app_scaffold.dart';
 | 
					import 'package:island/widgets/app_scaffold.dart';
 | 
				
			||||||
import 'package:styled_widget/styled_widget.dart';
 | 
					import 'package:styled_widget/styled_widget.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -204,12 +205,7 @@ class _OidcScreenState extends ConsumerState<OidcScreen> {
 | 
				
			|||||||
                      onPressed: () {
 | 
					                      onPressed: () {
 | 
				
			||||||
                        if (currentUrl != null) {
 | 
					                        if (currentUrl != null) {
 | 
				
			||||||
                          Clipboard.setData(ClipboardData(text: currentUrl!));
 | 
					                          Clipboard.setData(ClipboardData(text: currentUrl!));
 | 
				
			||||||
                          ScaffoldMessenger.of(context).showSnackBar(
 | 
					                          showSnackBar('copyToClipboard');
 | 
				
			||||||
                            SnackBar(
 | 
					 | 
				
			||||||
                              content: Text('copyToClipboard').tr(),
 | 
					 | 
				
			||||||
                              duration: const Duration(seconds: 1),
 | 
					 | 
				
			||||||
                            ),
 | 
					 | 
				
			||||||
                          );
 | 
					 | 
				
			||||||
                        }
 | 
					                        }
 | 
				
			||||||
                      },
 | 
					                      },
 | 
				
			||||||
                    ),
 | 
					                    ),
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -8,7 +8,6 @@ import 'package:island/models/activity.dart';
 | 
				
			|||||||
import 'package:island/pods/userinfo.dart';
 | 
					import 'package:island/pods/userinfo.dart';
 | 
				
			||||||
import 'package:island/route.gr.dart';
 | 
					import 'package:island/route.gr.dart';
 | 
				
			||||||
import 'package:island/services/responsive.dart';
 | 
					import 'package:island/services/responsive.dart';
 | 
				
			||||||
import 'package:island/widgets/alert.dart';
 | 
					 | 
				
			||||||
import 'package:island/widgets/app_scaffold.dart';
 | 
					import 'package:island/widgets/app_scaffold.dart';
 | 
				
			||||||
import 'package:island/models/post.dart';
 | 
					import 'package:island/models/post.dart';
 | 
				
			||||||
import 'package:island/widgets/check_in.dart';
 | 
					import 'package:island/widgets/check_in.dart';
 | 
				
			||||||
@@ -76,7 +75,6 @@ class ExploreScreen extends HookConsumerWidget {
 | 
				
			|||||||
            currentFilter.value = 'friends';
 | 
					            currentFilter.value = 'friends';
 | 
				
			||||||
            break;
 | 
					            break;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        showSnackBar('Browsing ${currentFilter.value}');
 | 
					 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      tabController.addListener(listener);
 | 
					      tabController.addListener(listener);
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,9 +1,57 @@
 | 
				
			|||||||
 | 
					import 'dart:async';
 | 
				
			||||||
import 'dart:developer';
 | 
					import 'dart:developer';
 | 
				
			||||||
import 'dart:io';
 | 
					import 'dart:io';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import 'package:dio/dio.dart';
 | 
					import 'package:dio/dio.dart';
 | 
				
			||||||
import 'package:firebase_messaging/firebase_messaging.dart';
 | 
					import 'package:firebase_messaging/firebase_messaging.dart';
 | 
				
			||||||
import 'package:flutter/foundation.dart';
 | 
					import 'package:flutter/foundation.dart';
 | 
				
			||||||
 | 
					import 'package:flutter/material.dart';
 | 
				
			||||||
 | 
					import 'package:flutter_riverpod/flutter_riverpod.dart';
 | 
				
			||||||
 | 
					import 'package:island/main.dart';
 | 
				
			||||||
 | 
					import 'package:island/models/user.dart';
 | 
				
			||||||
 | 
					import 'package:island/pods/websocket.dart';
 | 
				
			||||||
 | 
					import 'package:island/widgets/app_notification.dart';
 | 
				
			||||||
 | 
					import 'package:top_snackbar_flutter/top_snack_bar.dart';
 | 
				
			||||||
 | 
					import 'package:url_launcher/url_launcher_string.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					StreamSubscription<WebSocketPacket> setupNotificationListener(
 | 
				
			||||||
 | 
					  BuildContext context,
 | 
				
			||||||
 | 
					  WidgetRef ref,
 | 
				
			||||||
 | 
					) {
 | 
				
			||||||
 | 
					  final ws = ref.watch(websocketProvider);
 | 
				
			||||||
 | 
					  return ws.dataStream.listen((pkt) {
 | 
				
			||||||
 | 
					    if (pkt.type == "notifications.new") {
 | 
				
			||||||
 | 
					      final notification = SnNotification.fromJson(pkt.data!);
 | 
				
			||||||
 | 
					      showTopSnackBar(
 | 
				
			||||||
 | 
					        globalOverlay.currentState!,
 | 
				
			||||||
 | 
					        NotificationCard(notification: notification),
 | 
				
			||||||
 | 
					        onTap: () {
 | 
				
			||||||
 | 
					          if (notification.meta['action_uri'] != null) {
 | 
				
			||||||
 | 
					            var uri = notification.meta['action_uri'] as String;
 | 
				
			||||||
 | 
					            if (uri.startsWith('/')) {
 | 
				
			||||||
 | 
					              // In-app routes
 | 
				
			||||||
 | 
					              appRouter.pushPath(notification.meta['action_uri']);
 | 
				
			||||||
 | 
					            } else {
 | 
				
			||||||
 | 
					              // External URLs
 | 
				
			||||||
 | 
					              launchUrlString(uri);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        onDismissed: () {},
 | 
				
			||||||
 | 
					        dismissType: DismissType.onSwipe,
 | 
				
			||||||
 | 
					        displayDuration: const Duration(seconds: 5),
 | 
				
			||||||
 | 
					        snackBarPosition: SnackBarPosition.top,
 | 
				
			||||||
 | 
					        padding: EdgeInsets.only(
 | 
				
			||||||
 | 
					          left: 16,
 | 
				
			||||||
 | 
					          right: 16,
 | 
				
			||||||
 | 
					          // ignore: use_build_context_synchronously
 | 
				
			||||||
 | 
					          top: MediaQuery.of(context).padding.top + 24,
 | 
				
			||||||
 | 
					          bottom: 16,
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Future<void> subscribePushNotification(Dio apiClient) async {
 | 
					Future<void> subscribePushNotification(Dio apiClient) async {
 | 
				
			||||||
  await FirebaseMessaging.instance.requestPermission(
 | 
					  await FirebaseMessaging.instance.requestPermission(
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -11,7 +11,7 @@ export 'content/alert.native.dart'
 | 
				
			|||||||
void showSnackBar(String message, {SnackBarAction? action}) {
 | 
					void showSnackBar(String message, {SnackBarAction? action}) {
 | 
				
			||||||
  showTopSnackBar(
 | 
					  showTopSnackBar(
 | 
				
			||||||
    globalOverlay.currentState!,
 | 
					    globalOverlay.currentState!,
 | 
				
			||||||
    Card(child: Text(message).padding(horizontal: 24, vertical: 16)),
 | 
					    Card(child: Text(message).padding(horizontal: 20, vertical: 16)),
 | 
				
			||||||
    snackBarPosition: SnackBarPosition.bottom,
 | 
					    snackBarPosition: SnackBarPosition.bottom,
 | 
				
			||||||
  );
 | 
					  );
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,235 +1,18 @@
 | 
				
			|||||||
import 'dart:async';
 | 
					 | 
				
			||||||
import 'dart:developer';
 | 
					 | 
				
			||||||
import 'package:flutter/foundation.dart';
 | 
					 | 
				
			||||||
import 'package:flutter_hooks/flutter_hooks.dart';
 | 
					 | 
				
			||||||
import 'package:collection/collection.dart';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import 'package:flutter/material.dart';
 | 
					import 'package:flutter/material.dart';
 | 
				
			||||||
import 'package:freezed_annotation/freezed_annotation.dart';
 | 
					 | 
				
			||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
 | 
					import 'package:hooks_riverpod/hooks_riverpod.dart';
 | 
				
			||||||
import 'package:island/main.dart';
 | 
					 | 
				
			||||||
import 'package:island/models/user.dart';
 | 
					import 'package:island/models/user.dart';
 | 
				
			||||||
import 'package:island/pods/websocket.dart';
 | 
					 | 
				
			||||||
import 'package:island/widgets/content/cloud_files.dart';
 | 
					import 'package:island/widgets/content/cloud_files.dart';
 | 
				
			||||||
import 'package:material_symbols_icons/material_symbols_icons.dart';
 | 
					import 'package:material_symbols_icons/material_symbols_icons.dart';
 | 
				
			||||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
 | 
					 | 
				
			||||||
import 'package:styled_widget/styled_widget.dart';
 | 
					import 'package:styled_widget/styled_widget.dart';
 | 
				
			||||||
import 'package:url_launcher/url_launcher_string.dart';
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
part 'app_notification.freezed.dart';
 | 
					class NotificationCard extends HookConsumerWidget {
 | 
				
			||||||
part 'app_notification.g.dart';
 | 
					  final SnNotification notification;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class AppNotificationToast extends HookConsumerWidget {
 | 
					  const NotificationCard({super.key, required this.notification});
 | 
				
			||||||
  const AppNotificationToast({super.key});
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
  Widget build(BuildContext context, WidgetRef ref) {
 | 
					  Widget build(BuildContext context, WidgetRef ref) {
 | 
				
			||||||
    final notifications = ref.watch(appNotificationsProvider);
 | 
					    final icon = Symbols.info;
 | 
				
			||||||
 | 
					 | 
				
			||||||
    // Create a global key for AnimatedList
 | 
					 | 
				
			||||||
    final listKey = useMemoized(() => GlobalKey<AnimatedListState>());
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    // Track visual notification count (including those being animated out)
 | 
					 | 
				
			||||||
    final visualCount = useState(notifications.length);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    // Track notifications being removed to manage visual count
 | 
					 | 
				
			||||||
    final animatingOutIds = useState<Set<String>>({});
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    // Track previous notifications to detect changes
 | 
					 | 
				
			||||||
    final previousNotifications = usePrevious(notifications) ?? [];
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    // Handle notification changes
 | 
					 | 
				
			||||||
    useEffect(() {
 | 
					 | 
				
			||||||
      final currentIds = notifications.map((n) => n.data.id).toSet();
 | 
					 | 
				
			||||||
      final previousIds = previousNotifications.map((n) => n.data.id).toSet();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      // Find new notifications (added)
 | 
					 | 
				
			||||||
      final newIds = currentIds.difference(previousIds);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      // Update visual count for new notifications
 | 
					 | 
				
			||||||
      if (newIds.isNotEmpty) {
 | 
					 | 
				
			||||||
        visualCount.value += newIds.length;
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      // Insert new notifications with animation
 | 
					 | 
				
			||||||
      for (final id in newIds) {
 | 
					 | 
				
			||||||
        final index = notifications.indexWhere((n) => n.data.id == id);
 | 
					 | 
				
			||||||
        if (index != -1 &&
 | 
					 | 
				
			||||||
            listKey.currentState != null &&
 | 
					 | 
				
			||||||
            index >= 0 &&
 | 
					 | 
				
			||||||
            index <= notifications.length) {
 | 
					 | 
				
			||||||
          try {
 | 
					 | 
				
			||||||
            listKey.currentState!.insertItem(
 | 
					 | 
				
			||||||
              index,
 | 
					 | 
				
			||||||
              duration: const Duration(milliseconds: 150),
 | 
					 | 
				
			||||||
            );
 | 
					 | 
				
			||||||
          } catch (e) {
 | 
					 | 
				
			||||||
            // Log error but don't crash the app
 | 
					 | 
				
			||||||
            debugPrint('Error inserting notification: $e');
 | 
					 | 
				
			||||||
          }
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      return null;
 | 
					 | 
				
			||||||
    }, [notifications]);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    return Positioned(
 | 
					 | 
				
			||||||
      top: MediaQuery.of(context).padding.top + 50,
 | 
					 | 
				
			||||||
      left: 16,
 | 
					 | 
				
			||||||
      right: 16,
 | 
					 | 
				
			||||||
      child: SizedBox(
 | 
					 | 
				
			||||||
        // Use visualCount instead of notifications.length for height calculation
 | 
					 | 
				
			||||||
        height: visualCount.value * 80,
 | 
					 | 
				
			||||||
        child: AnimatedList(
 | 
					 | 
				
			||||||
          physics: NeverScrollableScrollPhysics(),
 | 
					 | 
				
			||||||
          padding: EdgeInsets.zero,
 | 
					 | 
				
			||||||
          key: listKey,
 | 
					 | 
				
			||||||
          initialItemCount: notifications.length,
 | 
					 | 
				
			||||||
          itemBuilder: (context, index, animation) {
 | 
					 | 
				
			||||||
            // Safely access notifications with bounds check
 | 
					 | 
				
			||||||
            if (index >= notifications.length) {
 | 
					 | 
				
			||||||
              return const SizedBox.shrink(); // Return empty widget if out of bounds
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            final notification = notifications[index];
 | 
					 | 
				
			||||||
            final now = DateTime.now();
 | 
					 | 
				
			||||||
            final createdAt = notification.createdAt ?? now;
 | 
					 | 
				
			||||||
            final duration =
 | 
					 | 
				
			||||||
                notification.duration ?? const Duration(seconds: 5);
 | 
					 | 
				
			||||||
            final elapsedTime = now.difference(createdAt);
 | 
					 | 
				
			||||||
            final remainingTime = duration - elapsedTime;
 | 
					 | 
				
			||||||
            final progress =
 | 
					 | 
				
			||||||
                1.0 -
 | 
					 | 
				
			||||||
                (remainingTime.inMilliseconds / duration.inMilliseconds).clamp(
 | 
					 | 
				
			||||||
                  0.0,
 | 
					 | 
				
			||||||
                  1.0,
 | 
					 | 
				
			||||||
                ); // Ensure progress is clamped
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            return SizeTransition(
 | 
					 | 
				
			||||||
              sizeFactor: animation.drive(
 | 
					 | 
				
			||||||
                CurveTween(curve: Curves.fastLinearToSlowEaseIn),
 | 
					 | 
				
			||||||
              ),
 | 
					 | 
				
			||||||
              child: _NotificationCard(
 | 
					 | 
				
			||||||
                notification: notification,
 | 
					 | 
				
			||||||
                progress: progress.clamp(0.0, 1.0),
 | 
					 | 
				
			||||||
                onDismiss: () {
 | 
					 | 
				
			||||||
                  // Find the current index before removal
 | 
					 | 
				
			||||||
                  final currentIndex = notifications.indexWhere(
 | 
					 | 
				
			||||||
                    (n) => n.data.id == notification.data.id,
 | 
					 | 
				
			||||||
                  );
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                  // Add to animating out set
 | 
					 | 
				
			||||||
                  final notificationId = notification.data.id;
 | 
					 | 
				
			||||||
                  if (!animatingOutIds.value.contains(notificationId)) {
 | 
					 | 
				
			||||||
                    animatingOutIds.value = {
 | 
					 | 
				
			||||||
                      ...animatingOutIds.value,
 | 
					 | 
				
			||||||
                      notificationId,
 | 
					 | 
				
			||||||
                    };
 | 
					 | 
				
			||||||
                  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                  if (currentIndex != -1 &&
 | 
					 | 
				
			||||||
                      listKey.currentState != null &&
 | 
					 | 
				
			||||||
                      currentIndex >= 0 &&
 | 
					 | 
				
			||||||
                      currentIndex < notifications.length) {
 | 
					 | 
				
			||||||
                    try {
 | 
					 | 
				
			||||||
                      // Remove the item with animation
 | 
					 | 
				
			||||||
                      listKey.currentState!.removeItem(
 | 
					 | 
				
			||||||
                        currentIndex,
 | 
					 | 
				
			||||||
                        (context, animation) => SizeTransition(
 | 
					 | 
				
			||||||
                          sizeFactor: animation.drive(
 | 
					 | 
				
			||||||
                            CurveTween(curve: Curves.fastLinearToSlowEaseIn),
 | 
					 | 
				
			||||||
                          ),
 | 
					 | 
				
			||||||
                          child: _NotificationCard(
 | 
					 | 
				
			||||||
                            notification: notification,
 | 
					 | 
				
			||||||
                            progress: progress.clamp(0.0, 1.0),
 | 
					 | 
				
			||||||
                            onDismiss:
 | 
					 | 
				
			||||||
                                () {}, // Empty because it's being removed
 | 
					 | 
				
			||||||
                          ),
 | 
					 | 
				
			||||||
                        ),
 | 
					 | 
				
			||||||
                        duration: const Duration(milliseconds: 150),
 | 
					 | 
				
			||||||
                        // When animation completes, update the visual count
 | 
					 | 
				
			||||||
                      );
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                      // Schedule decrementing the visual count after animation completes
 | 
					 | 
				
			||||||
                      Future.delayed(const Duration(milliseconds: 150), () {
 | 
					 | 
				
			||||||
                        if (animatingOutIds.value.contains(notificationId)) {
 | 
					 | 
				
			||||||
                          visualCount.value =
 | 
					 | 
				
			||||||
                              visualCount.value > 0 ? visualCount.value - 1 : 0;
 | 
					 | 
				
			||||||
                          animatingOutIds.value =
 | 
					 | 
				
			||||||
                              animatingOutIds.value
 | 
					 | 
				
			||||||
                                  .where((id) => id != notificationId)
 | 
					 | 
				
			||||||
                                  .toSet();
 | 
					 | 
				
			||||||
                        }
 | 
					 | 
				
			||||||
                      });
 | 
					 | 
				
			||||||
                    } catch (e) {
 | 
					 | 
				
			||||||
                      // Log error but don't crash the app
 | 
					 | 
				
			||||||
                      log('[Notification] Error removing notification: $e');
 | 
					 | 
				
			||||||
                      // Still update visual count in case of error
 | 
					 | 
				
			||||||
                      visualCount.value =
 | 
					 | 
				
			||||||
                          visualCount.value > 0 ? visualCount.value - 1 : 0;
 | 
					 | 
				
			||||||
                      animatingOutIds.value =
 | 
					 | 
				
			||||||
                          animatingOutIds.value
 | 
					 | 
				
			||||||
                              .where((id) => id != notificationId)
 | 
					 | 
				
			||||||
                              .toSet();
 | 
					 | 
				
			||||||
                    }
 | 
					 | 
				
			||||||
                  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                  // Actually remove from state
 | 
					 | 
				
			||||||
                  ref
 | 
					 | 
				
			||||||
                      .read(appNotificationsProvider.notifier)
 | 
					 | 
				
			||||||
                      .removeNotification(notification);
 | 
					 | 
				
			||||||
                },
 | 
					 | 
				
			||||||
              ),
 | 
					 | 
				
			||||||
            );
 | 
					 | 
				
			||||||
          },
 | 
					 | 
				
			||||||
        ),
 | 
					 | 
				
			||||||
      ),
 | 
					 | 
				
			||||||
    );
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
class _NotificationCard extends HookConsumerWidget {
 | 
					 | 
				
			||||||
  final AppNotification notification;
 | 
					 | 
				
			||||||
  final double progress;
 | 
					 | 
				
			||||||
  final VoidCallback onDismiss;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  const _NotificationCard({
 | 
					 | 
				
			||||||
    required this.notification,
 | 
					 | 
				
			||||||
    required this.progress,
 | 
					 | 
				
			||||||
    required this.onDismiss,
 | 
					 | 
				
			||||||
  });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  @override
 | 
					 | 
				
			||||||
  Widget build(BuildContext context, WidgetRef ref) {
 | 
					 | 
				
			||||||
    // Use state to track the current progress for smooth animation
 | 
					 | 
				
			||||||
    final progressState = useState(progress);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    // Use effect to update progress smoothly
 | 
					 | 
				
			||||||
    useEffect(() {
 | 
					 | 
				
			||||||
      if (progress < 1.0) {
 | 
					 | 
				
			||||||
        // Update progress every 16ms (roughly 60fps) for smooth animation
 | 
					 | 
				
			||||||
        final timer = Timer.periodic(const Duration(milliseconds: 16), (_) {
 | 
					 | 
				
			||||||
          final now = DateTime.now();
 | 
					 | 
				
			||||||
          final createdAt = notification.createdAt ?? now;
 | 
					 | 
				
			||||||
          final duration = notification.duration ?? const Duration(seconds: 5);
 | 
					 | 
				
			||||||
          final elapsedTime = now.difference(createdAt);
 | 
					 | 
				
			||||||
          final remainingTime = duration - elapsedTime;
 | 
					 | 
				
			||||||
          final newProgress = (1.0 -
 | 
					 | 
				
			||||||
                  (remainingTime.inMilliseconds / duration.inMilliseconds))
 | 
					 | 
				
			||||||
              .clamp(0.0, 1.0);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
          progressState.value = newProgress;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
          // Auto-dismiss when complete
 | 
					 | 
				
			||||||
          if (newProgress >= 1.0) {
 | 
					 | 
				
			||||||
            onDismiss();
 | 
					 | 
				
			||||||
          }
 | 
					 | 
				
			||||||
        });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        return timer.cancel;
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
      return null;
 | 
					 | 
				
			||||||
    }, [notification.createdAt, notification.duration]);
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return Card(
 | 
					    return Card(
 | 
				
			||||||
      elevation: 4,
 | 
					      elevation: 4,
 | 
				
			||||||
@@ -237,225 +20,52 @@ class _NotificationCard extends HookConsumerWidget {
 | 
				
			|||||||
      shape: RoundedRectangleBorder(
 | 
					      shape: RoundedRectangleBorder(
 | 
				
			||||||
        borderRadius: BorderRadius.vertical(bottom: Radius.circular(8)),
 | 
					        borderRadius: BorderRadius.vertical(bottom: Radius.circular(8)),
 | 
				
			||||||
      ),
 | 
					      ),
 | 
				
			||||||
      child: InkWell(
 | 
					      child: Column(
 | 
				
			||||||
        borderRadius: const BorderRadius.all(Radius.circular(8)),
 | 
					        crossAxisAlignment: CrossAxisAlignment.start,
 | 
				
			||||||
        onTap: () {
 | 
					        mainAxisSize: MainAxisSize.min,
 | 
				
			||||||
          if (notification.data.meta['action_uri'] != null) {
 | 
					        children: [
 | 
				
			||||||
            var uri = notification.data.meta['action_uri'] as String;
 | 
					          Padding(
 | 
				
			||||||
            if (uri.startsWith('/')) {
 | 
					            padding: const EdgeInsets.all(12),
 | 
				
			||||||
              // In-app routes
 | 
					            child: Row(
 | 
				
			||||||
              appRouter.pushPath(notification.data.meta['action_uri']);
 | 
					              crossAxisAlignment: CrossAxisAlignment.start,
 | 
				
			||||||
            } else {
 | 
					              children: [
 | 
				
			||||||
              // External URLs
 | 
					                if (notification.meta['pfp'] != null)
 | 
				
			||||||
              launchUrlString(uri);
 | 
					                  ProfilePictureWidget(
 | 
				
			||||||
            }
 | 
					                    fileId: notification.meta['pfp'],
 | 
				
			||||||
            onDismiss();
 | 
					                    radius: 12,
 | 
				
			||||||
          }
 | 
					                  ).padding(right: 12, top: 2)
 | 
				
			||||||
        },
 | 
					                else
 | 
				
			||||||
        child: Column(
 | 
					                  Icon(
 | 
				
			||||||
          crossAxisAlignment: CrossAxisAlignment.start,
 | 
					                    icon,
 | 
				
			||||||
          mainAxisSize: MainAxisSize.min,
 | 
					                    color: Theme.of(context).colorScheme.primary,
 | 
				
			||||||
          children: [
 | 
					                    size: 24,
 | 
				
			||||||
            // Progress indicator
 | 
					                  ).padding(right: 12),
 | 
				
			||||||
            if (progressState.value > 0 && progressState.value < 1.0)
 | 
					                Expanded(
 | 
				
			||||||
              AnimatedBuilder(
 | 
					                  child: Column(
 | 
				
			||||||
                animation: progressState,
 | 
					                    crossAxisAlignment: CrossAxisAlignment.start,
 | 
				
			||||||
                builder: (context, _) {
 | 
					                    children: [
 | 
				
			||||||
                  return LinearProgressIndicator(
 | 
					                      Text(
 | 
				
			||||||
                    borderRadius: BorderRadius.vertical(
 | 
					                        notification.title,
 | 
				
			||||||
                      top: Radius.circular(16),
 | 
					                        style: Theme.of(context).textTheme.titleMedium
 | 
				
			||||||
                    ),
 | 
					                            ?.copyWith(fontWeight: FontWeight.bold),
 | 
				
			||||||
                    value: 1.0 - progressState.value,
 | 
					                      ),
 | 
				
			||||||
                    backgroundColor: Colors.transparent,
 | 
					                      if (notification.content.isNotEmpty)
 | 
				
			||||||
                    color: Theme.of(context).colorScheme.tertiary,
 | 
					 | 
				
			||||||
                    minHeight: 3,
 | 
					 | 
				
			||||||
                    stopIndicatorColor: Colors.transparent,
 | 
					 | 
				
			||||||
                    stopIndicatorRadius: 0,
 | 
					 | 
				
			||||||
                  );
 | 
					 | 
				
			||||||
                },
 | 
					 | 
				
			||||||
              ),
 | 
					 | 
				
			||||||
            Padding(
 | 
					 | 
				
			||||||
              padding: const EdgeInsets.all(12),
 | 
					 | 
				
			||||||
              child: Row(
 | 
					 | 
				
			||||||
                crossAxisAlignment: CrossAxisAlignment.start,
 | 
					 | 
				
			||||||
                children: [
 | 
					 | 
				
			||||||
                  if (notification.data.meta['avatar'] != null)
 | 
					 | 
				
			||||||
                    ProfilePictureWidget(
 | 
					 | 
				
			||||||
                      fileId: notification.data.meta['avatar'],
 | 
					 | 
				
			||||||
                      radius: 12,
 | 
					 | 
				
			||||||
                    ).padding(right: 12, top: 2)
 | 
					 | 
				
			||||||
                  else if (notification.icon != null)
 | 
					 | 
				
			||||||
                    Icon(
 | 
					 | 
				
			||||||
                      notification.icon,
 | 
					 | 
				
			||||||
                      color: Theme.of(context).colorScheme.primary,
 | 
					 | 
				
			||||||
                      size: 24,
 | 
					 | 
				
			||||||
                    ).padding(right: 12),
 | 
					 | 
				
			||||||
                  Expanded(
 | 
					 | 
				
			||||||
                    child: Column(
 | 
					 | 
				
			||||||
                      crossAxisAlignment: CrossAxisAlignment.start,
 | 
					 | 
				
			||||||
                      children: [
 | 
					 | 
				
			||||||
                        Text(
 | 
					                        Text(
 | 
				
			||||||
                          notification.data.title,
 | 
					                          notification.content,
 | 
				
			||||||
                          style: Theme.of(context).textTheme.titleMedium
 | 
					                          style: Theme.of(context).textTheme.bodyMedium,
 | 
				
			||||||
                              ?.copyWith(fontWeight: FontWeight.bold),
 | 
					 | 
				
			||||||
                        ),
 | 
					                        ),
 | 
				
			||||||
                        if (notification.data.content.isNotEmpty)
 | 
					                      if (notification.subtitle.isNotEmpty)
 | 
				
			||||||
                          Text(
 | 
					                        Text(
 | 
				
			||||||
                            notification.data.content,
 | 
					                          notification.subtitle,
 | 
				
			||||||
                            style: Theme.of(context).textTheme.bodyMedium,
 | 
					                          style: Theme.of(context).textTheme.bodySmall,
 | 
				
			||||||
                          ),
 | 
					                        ),
 | 
				
			||||||
                        if (notification.data.subtitle.isNotEmpty)
 | 
					                    ],
 | 
				
			||||||
                          Text(
 | 
					 | 
				
			||||||
                            notification.data.subtitle,
 | 
					 | 
				
			||||||
                            style: Theme.of(context).textTheme.bodySmall,
 | 
					 | 
				
			||||||
                          ),
 | 
					 | 
				
			||||||
                      ],
 | 
					 | 
				
			||||||
                    ),
 | 
					 | 
				
			||||||
                  ),
 | 
					                  ),
 | 
				
			||||||
                  IconButton(
 | 
					                ),
 | 
				
			||||||
                    icon: const Icon(Symbols.close, size: 18),
 | 
					              ],
 | 
				
			||||||
                    onPressed: onDismiss,
 | 
					 | 
				
			||||||
                    padding: EdgeInsets.zero,
 | 
					 | 
				
			||||||
                    constraints: const BoxConstraints(),
 | 
					 | 
				
			||||||
                  ),
 | 
					 | 
				
			||||||
                ],
 | 
					 | 
				
			||||||
              ),
 | 
					 | 
				
			||||||
            ),
 | 
					            ),
 | 
				
			||||||
          ],
 | 
					          ),
 | 
				
			||||||
        ),
 | 
					        ],
 | 
				
			||||||
      ),
 | 
					 | 
				
			||||||
    );
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
@freezed
 | 
					 | 
				
			||||||
sealed class AppNotification with _$AppNotification {
 | 
					 | 
				
			||||||
  const factory AppNotification({
 | 
					 | 
				
			||||||
    required SnNotification data,
 | 
					 | 
				
			||||||
    @JsonKey(ignore: true) IconData? icon,
 | 
					 | 
				
			||||||
    @JsonKey(ignore: true) Duration? duration,
 | 
					 | 
				
			||||||
    @Default(null) DateTime? createdAt,
 | 
					 | 
				
			||||||
    @Default(false) @JsonKey(ignore: true) bool isAnimatingOut,
 | 
					 | 
				
			||||||
  }) = _AppNotification;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  factory AppNotification.fromJson(Map<String, dynamic> json) =>
 | 
					 | 
				
			||||||
      _$AppNotificationFromJson(json);
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// Using riverpod_generator for cleaner provider code
 | 
					 | 
				
			||||||
@riverpod
 | 
					 | 
				
			||||||
class AppNotifications extends _$AppNotifications {
 | 
					 | 
				
			||||||
  StreamSubscription? _subscription;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  @override
 | 
					 | 
				
			||||||
  List<AppNotification> build() {
 | 
					 | 
				
			||||||
    ref.onDispose(() {
 | 
					 | 
				
			||||||
      _subscription?.cancel();
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    _initWebSocketListener();
 | 
					 | 
				
			||||||
    return [];
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  void _initWebSocketListener() {
 | 
					 | 
				
			||||||
    final service = ref.read(websocketProvider);
 | 
					 | 
				
			||||||
    _subscription = service.dataStream.listen((packet) {
 | 
					 | 
				
			||||||
      // Handle notification packets
 | 
					 | 
				
			||||||
      if (packet.type == 'notifications.new') {
 | 
					 | 
				
			||||||
        try {
 | 
					 | 
				
			||||||
          final data = SnNotification.fromJson(packet.data!);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
          IconData? icon;
 | 
					 | 
				
			||||||
          switch (data.topic) {
 | 
					 | 
				
			||||||
            case 'general':
 | 
					 | 
				
			||||||
            default:
 | 
					 | 
				
			||||||
              icon = Symbols.info;
 | 
					 | 
				
			||||||
              break;
 | 
					 | 
				
			||||||
          }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
          addNotification(
 | 
					 | 
				
			||||||
            AppNotification(
 | 
					 | 
				
			||||||
              data: data,
 | 
					 | 
				
			||||||
              icon: icon,
 | 
					 | 
				
			||||||
              createdAt: data.createdAt.toLocal(),
 | 
					 | 
				
			||||||
              duration: const Duration(seconds: 5),
 | 
					 | 
				
			||||||
            ),
 | 
					 | 
				
			||||||
          );
 | 
					 | 
				
			||||||
        } catch (e) {
 | 
					 | 
				
			||||||
          log('[Notification] Error processing notification: $e');
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  void addNotification(AppNotification notification) {
 | 
					 | 
				
			||||||
    // Create a new notification with createdAt if not provided
 | 
					 | 
				
			||||||
    final newNotification =
 | 
					 | 
				
			||||||
        notification.createdAt == null
 | 
					 | 
				
			||||||
            ? notification.copyWith(createdAt: DateTime.now())
 | 
					 | 
				
			||||||
            : notification;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    // Add to state
 | 
					 | 
				
			||||||
    state = [...state, newNotification];
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    // Auto-remove notification after duration
 | 
					 | 
				
			||||||
    final duration = newNotification.duration ?? const Duration(seconds: 5);
 | 
					 | 
				
			||||||
    Future.delayed(duration, () {
 | 
					 | 
				
			||||||
      // Find the notification in the current state
 | 
					 | 
				
			||||||
      final notificationToRemove = state.firstWhereOrNull(
 | 
					 | 
				
			||||||
        (n) => n.data.id == newNotification.data.id,
 | 
					 | 
				
			||||||
      );
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      // Only proceed if the notification still exists in state
 | 
					 | 
				
			||||||
      if (notificationToRemove != null) {
 | 
					 | 
				
			||||||
        // Call removeNotification which will handle the animation
 | 
					 | 
				
			||||||
        removeNotification(notificationToRemove);
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  // Map to track notifications that are being animated out
 | 
					 | 
				
			||||||
  final Map<String, bool> _animatingNotifications = {};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  // Map to track which notifications should animate out
 | 
					 | 
				
			||||||
  final Map<String, bool> _animatingOutNotifications = {};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  void removeNotification(AppNotification notification) {
 | 
					 | 
				
			||||||
    final notificationId = notification.data.id;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    // If this notification is already being removed, don't do anything
 | 
					 | 
				
			||||||
    if (_animatingNotifications[notificationId] == true) {
 | 
					 | 
				
			||||||
      return;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    // Mark this notification as being removed
 | 
					 | 
				
			||||||
    _animatingNotifications[notificationId] = true;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    // Remove from state immediately - AnimatedList handles the animation
 | 
					 | 
				
			||||||
    state = state.where((n) => n.data.id != notificationId).toList();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    // Clean up tracking
 | 
					 | 
				
			||||||
    _animatingNotifications.remove(notificationId);
 | 
					 | 
				
			||||||
    _animatingOutNotifications.remove(notificationId);
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  // Helper method to check if a notification should animate out
 | 
					 | 
				
			||||||
  bool isAnimatingOut(String notificationId) {
 | 
					 | 
				
			||||||
    return _animatingOutNotifications[notificationId] == true;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  // Helper method to manually add a notification for testing
 | 
					 | 
				
			||||||
  void showNotification({
 | 
					 | 
				
			||||||
    required SnNotification data,
 | 
					 | 
				
			||||||
    IconData? icon,
 | 
					 | 
				
			||||||
    Duration? duration,
 | 
					 | 
				
			||||||
  }) {
 | 
					 | 
				
			||||||
    addNotification(
 | 
					 | 
				
			||||||
      AppNotification(
 | 
					 | 
				
			||||||
        data: data,
 | 
					 | 
				
			||||||
        icon: icon,
 | 
					 | 
				
			||||||
        duration: duration,
 | 
					 | 
				
			||||||
        createdAt: data.createdAt,
 | 
					 | 
				
			||||||
      ),
 | 
					      ),
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,190 +0,0 @@
 | 
				
			|||||||
// dart format width=80
 | 
					 | 
				
			||||||
// coverage:ignore-file
 | 
					 | 
				
			||||||
// GENERATED CODE - DO NOT MODIFY BY HAND
 | 
					 | 
				
			||||||
// ignore_for_file: type=lint
 | 
					 | 
				
			||||||
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
part of 'app_notification.dart';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// **************************************************************************
 | 
					 | 
				
			||||||
// FreezedGenerator
 | 
					 | 
				
			||||||
// **************************************************************************
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// dart format off
 | 
					 | 
				
			||||||
T _$identity<T>(T value) => value;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
/// @nodoc
 | 
					 | 
				
			||||||
mixin _$AppNotification implements DiagnosticableTreeMixin {
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 SnNotification get data;@JsonKey(ignore: true) IconData? get icon;@JsonKey(ignore: true) Duration? get duration; DateTime? get createdAt;@JsonKey(ignore: true) bool get isAnimatingOut;
 | 
					 | 
				
			||||||
/// Create a copy of AppNotification
 | 
					 | 
				
			||||||
/// with the given fields replaced by the non-null parameter values.
 | 
					 | 
				
			||||||
@JsonKey(includeFromJson: false, includeToJson: false)
 | 
					 | 
				
			||||||
@pragma('vm:prefer-inline')
 | 
					 | 
				
			||||||
$AppNotificationCopyWith<AppNotification> get copyWith => _$AppNotificationCopyWithImpl<AppNotification>(this as AppNotification, _$identity);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  /// Serializes this AppNotification to a JSON map.
 | 
					 | 
				
			||||||
  Map<String, dynamic> toJson();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
@override
 | 
					 | 
				
			||||||
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
 | 
					 | 
				
			||||||
  properties
 | 
					 | 
				
			||||||
    ..add(DiagnosticsProperty('type', 'AppNotification'))
 | 
					 | 
				
			||||||
    ..add(DiagnosticsProperty('data', data))..add(DiagnosticsProperty('icon', icon))..add(DiagnosticsProperty('duration', duration))..add(DiagnosticsProperty('createdAt', createdAt))..add(DiagnosticsProperty('isAnimatingOut', isAnimatingOut));
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
@override
 | 
					 | 
				
			||||||
bool operator ==(Object other) {
 | 
					 | 
				
			||||||
  return identical(this, other) || (other.runtimeType == runtimeType&&other is AppNotification&&(identical(other.data, data) || other.data == data)&&(identical(other.icon, icon) || other.icon == icon)&&(identical(other.duration, duration) || other.duration == duration)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.isAnimatingOut, isAnimatingOut) || other.isAnimatingOut == isAnimatingOut));
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
@JsonKey(includeFromJson: false, includeToJson: false)
 | 
					 | 
				
			||||||
@override
 | 
					 | 
				
			||||||
int get hashCode => Object.hash(runtimeType,data,icon,duration,createdAt,isAnimatingOut);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
@override
 | 
					 | 
				
			||||||
String toString({ DiagnosticLevel minLevel = DiagnosticLevel.info }) {
 | 
					 | 
				
			||||||
  return 'AppNotification(data: $data, icon: $icon, duration: $duration, createdAt: $createdAt, isAnimatingOut: $isAnimatingOut)';
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
/// @nodoc
 | 
					 | 
				
			||||||
abstract mixin class $AppNotificationCopyWith<$Res>  {
 | 
					 | 
				
			||||||
  factory $AppNotificationCopyWith(AppNotification value, $Res Function(AppNotification) _then) = _$AppNotificationCopyWithImpl;
 | 
					 | 
				
			||||||
@useResult
 | 
					 | 
				
			||||||
$Res call({
 | 
					 | 
				
			||||||
 SnNotification data,@JsonKey(ignore: true) IconData? icon,@JsonKey(ignore: true) Duration? duration, DateTime? createdAt,@JsonKey(ignore: true) bool isAnimatingOut
 | 
					 | 
				
			||||||
});
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
$SnNotificationCopyWith<$Res> get data;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
/// @nodoc
 | 
					 | 
				
			||||||
class _$AppNotificationCopyWithImpl<$Res>
 | 
					 | 
				
			||||||
    implements $AppNotificationCopyWith<$Res> {
 | 
					 | 
				
			||||||
  _$AppNotificationCopyWithImpl(this._self, this._then);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  final AppNotification _self;
 | 
					 | 
				
			||||||
  final $Res Function(AppNotification) _then;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
/// Create a copy of AppNotification
 | 
					 | 
				
			||||||
/// with the given fields replaced by the non-null parameter values.
 | 
					 | 
				
			||||||
@pragma('vm:prefer-inline') @override $Res call({Object? data = null,Object? icon = freezed,Object? duration = freezed,Object? createdAt = freezed,Object? isAnimatingOut = null,}) {
 | 
					 | 
				
			||||||
  return _then(_self.copyWith(
 | 
					 | 
				
			||||||
data: null == data ? _self.data : data // ignore: cast_nullable_to_non_nullable
 | 
					 | 
				
			||||||
as SnNotification,icon: freezed == icon ? _self.icon : icon // ignore: cast_nullable_to_non_nullable
 | 
					 | 
				
			||||||
as IconData?,duration: freezed == duration ? _self.duration : duration // ignore: cast_nullable_to_non_nullable
 | 
					 | 
				
			||||||
as Duration?,createdAt: freezed == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable
 | 
					 | 
				
			||||||
as DateTime?,isAnimatingOut: null == isAnimatingOut ? _self.isAnimatingOut : isAnimatingOut // ignore: cast_nullable_to_non_nullable
 | 
					 | 
				
			||||||
as bool,
 | 
					 | 
				
			||||||
  ));
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
/// Create a copy of AppNotification
 | 
					 | 
				
			||||||
/// with the given fields replaced by the non-null parameter values.
 | 
					 | 
				
			||||||
@override
 | 
					 | 
				
			||||||
@pragma('vm:prefer-inline')
 | 
					 | 
				
			||||||
$SnNotificationCopyWith<$Res> get data {
 | 
					 | 
				
			||||||
  
 | 
					 | 
				
			||||||
  return $SnNotificationCopyWith<$Res>(_self.data, (value) {
 | 
					 | 
				
			||||||
    return _then(_self.copyWith(data: value));
 | 
					 | 
				
			||||||
  });
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
/// @nodoc
 | 
					 | 
				
			||||||
@JsonSerializable()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
class _AppNotification with DiagnosticableTreeMixin implements AppNotification {
 | 
					 | 
				
			||||||
  const _AppNotification({required this.data, @JsonKey(ignore: true) this.icon, @JsonKey(ignore: true) this.duration, this.createdAt = null, @JsonKey(ignore: true) this.isAnimatingOut = false});
 | 
					 | 
				
			||||||
  factory _AppNotification.fromJson(Map<String, dynamic> json) => _$AppNotificationFromJson(json);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
@override final  SnNotification data;
 | 
					 | 
				
			||||||
@override@JsonKey(ignore: true) final  IconData? icon;
 | 
					 | 
				
			||||||
@override@JsonKey(ignore: true) final  Duration? duration;
 | 
					 | 
				
			||||||
@override@JsonKey() final  DateTime? createdAt;
 | 
					 | 
				
			||||||
@override@JsonKey(ignore: true) final  bool isAnimatingOut;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
/// Create a copy of AppNotification
 | 
					 | 
				
			||||||
/// with the given fields replaced by the non-null parameter values.
 | 
					 | 
				
			||||||
@override @JsonKey(includeFromJson: false, includeToJson: false)
 | 
					 | 
				
			||||||
@pragma('vm:prefer-inline')
 | 
					 | 
				
			||||||
_$AppNotificationCopyWith<_AppNotification> get copyWith => __$AppNotificationCopyWithImpl<_AppNotification>(this, _$identity);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
@override
 | 
					 | 
				
			||||||
Map<String, dynamic> toJson() {
 | 
					 | 
				
			||||||
  return _$AppNotificationToJson(this, );
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@override
 | 
					 | 
				
			||||||
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
 | 
					 | 
				
			||||||
  properties
 | 
					 | 
				
			||||||
    ..add(DiagnosticsProperty('type', 'AppNotification'))
 | 
					 | 
				
			||||||
    ..add(DiagnosticsProperty('data', data))..add(DiagnosticsProperty('icon', icon))..add(DiagnosticsProperty('duration', duration))..add(DiagnosticsProperty('createdAt', createdAt))..add(DiagnosticsProperty('isAnimatingOut', isAnimatingOut));
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
@override
 | 
					 | 
				
			||||||
bool operator ==(Object other) {
 | 
					 | 
				
			||||||
  return identical(this, other) || (other.runtimeType == runtimeType&&other is _AppNotification&&(identical(other.data, data) || other.data == data)&&(identical(other.icon, icon) || other.icon == icon)&&(identical(other.duration, duration) || other.duration == duration)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.isAnimatingOut, isAnimatingOut) || other.isAnimatingOut == isAnimatingOut));
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
@JsonKey(includeFromJson: false, includeToJson: false)
 | 
					 | 
				
			||||||
@override
 | 
					 | 
				
			||||||
int get hashCode => Object.hash(runtimeType,data,icon,duration,createdAt,isAnimatingOut);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
@override
 | 
					 | 
				
			||||||
String toString({ DiagnosticLevel minLevel = DiagnosticLevel.info }) {
 | 
					 | 
				
			||||||
  return 'AppNotification(data: $data, icon: $icon, duration: $duration, createdAt: $createdAt, isAnimatingOut: $isAnimatingOut)';
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
/// @nodoc
 | 
					 | 
				
			||||||
abstract mixin class _$AppNotificationCopyWith<$Res> implements $AppNotificationCopyWith<$Res> {
 | 
					 | 
				
			||||||
  factory _$AppNotificationCopyWith(_AppNotification value, $Res Function(_AppNotification) _then) = __$AppNotificationCopyWithImpl;
 | 
					 | 
				
			||||||
@override @useResult
 | 
					 | 
				
			||||||
$Res call({
 | 
					 | 
				
			||||||
 SnNotification data,@JsonKey(ignore: true) IconData? icon,@JsonKey(ignore: true) Duration? duration, DateTime? createdAt,@JsonKey(ignore: true) bool isAnimatingOut
 | 
					 | 
				
			||||||
});
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
@override $SnNotificationCopyWith<$Res> get data;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
/// @nodoc
 | 
					 | 
				
			||||||
class __$AppNotificationCopyWithImpl<$Res>
 | 
					 | 
				
			||||||
    implements _$AppNotificationCopyWith<$Res> {
 | 
					 | 
				
			||||||
  __$AppNotificationCopyWithImpl(this._self, this._then);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  final _AppNotification _self;
 | 
					 | 
				
			||||||
  final $Res Function(_AppNotification) _then;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
/// Create a copy of AppNotification
 | 
					 | 
				
			||||||
/// with the given fields replaced by the non-null parameter values.
 | 
					 | 
				
			||||||
@override @pragma('vm:prefer-inline') $Res call({Object? data = null,Object? icon = freezed,Object? duration = freezed,Object? createdAt = freezed,Object? isAnimatingOut = null,}) {
 | 
					 | 
				
			||||||
  return _then(_AppNotification(
 | 
					 | 
				
			||||||
data: null == data ? _self.data : data // ignore: cast_nullable_to_non_nullable
 | 
					 | 
				
			||||||
as SnNotification,icon: freezed == icon ? _self.icon : icon // ignore: cast_nullable_to_non_nullable
 | 
					 | 
				
			||||||
as IconData?,duration: freezed == duration ? _self.duration : duration // ignore: cast_nullable_to_non_nullable
 | 
					 | 
				
			||||||
as Duration?,createdAt: freezed == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable
 | 
					 | 
				
			||||||
as DateTime?,isAnimatingOut: null == isAnimatingOut ? _self.isAnimatingOut : isAnimatingOut // ignore: cast_nullable_to_non_nullable
 | 
					 | 
				
			||||||
as bool,
 | 
					 | 
				
			||||||
  ));
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
/// Create a copy of AppNotification
 | 
					 | 
				
			||||||
/// with the given fields replaced by the non-null parameter values.
 | 
					 | 
				
			||||||
@override
 | 
					 | 
				
			||||||
@pragma('vm:prefer-inline')
 | 
					 | 
				
			||||||
$SnNotificationCopyWith<$Res> get data {
 | 
					 | 
				
			||||||
  
 | 
					 | 
				
			||||||
  return $SnNotificationCopyWith<$Res>(_self.data, (value) {
 | 
					 | 
				
			||||||
    return _then(_self.copyWith(data: value));
 | 
					 | 
				
			||||||
  });
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// dart format on
 | 
					 | 
				
			||||||
@@ -1,48 +0,0 @@
 | 
				
			|||||||
// GENERATED CODE - DO NOT MODIFY BY HAND
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
part of 'app_notification.dart';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// **************************************************************************
 | 
					 | 
				
			||||||
// JsonSerializableGenerator
 | 
					 | 
				
			||||||
// **************************************************************************
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
_AppNotification _$AppNotificationFromJson(Map<String, dynamic> json) =>
 | 
					 | 
				
			||||||
    _AppNotification(
 | 
					 | 
				
			||||||
      data: SnNotification.fromJson(json['data'] as Map<String, dynamic>),
 | 
					 | 
				
			||||||
      createdAt:
 | 
					 | 
				
			||||||
          json['created_at'] == null
 | 
					 | 
				
			||||||
              ? null
 | 
					 | 
				
			||||||
              : DateTime.parse(json['created_at'] as String),
 | 
					 | 
				
			||||||
    );
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
Map<String, dynamic> _$AppNotificationToJson(_AppNotification instance) =>
 | 
					 | 
				
			||||||
    <String, dynamic>{
 | 
					 | 
				
			||||||
      'data': instance.data.toJson(),
 | 
					 | 
				
			||||||
      'created_at': instance.createdAt?.toIso8601String(),
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// **************************************************************************
 | 
					 | 
				
			||||||
// RiverpodGenerator
 | 
					 | 
				
			||||||
// **************************************************************************
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
String _$appNotificationsHash() => r'a7e7e1d1533e329b000d4b294e455b8420ec3c4d';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
/// See also [AppNotifications].
 | 
					 | 
				
			||||||
@ProviderFor(AppNotifications)
 | 
					 | 
				
			||||||
final appNotificationsProvider = AutoDisposeNotifierProvider<
 | 
					 | 
				
			||||||
  AppNotifications,
 | 
					 | 
				
			||||||
  List<AppNotification>
 | 
					 | 
				
			||||||
>.internal(
 | 
					 | 
				
			||||||
  AppNotifications.new,
 | 
					 | 
				
			||||||
  name: r'appNotificationsProvider',
 | 
					 | 
				
			||||||
  debugGetCreateSourceHash:
 | 
					 | 
				
			||||||
      const bool.fromEnvironment('dart.vm.product')
 | 
					 | 
				
			||||||
          ? null
 | 
					 | 
				
			||||||
          : _$appNotificationsHash,
 | 
					 | 
				
			||||||
  dependencies: null,
 | 
					 | 
				
			||||||
  allTransitiveDependencies: null,
 | 
					 | 
				
			||||||
);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
typedef _$AppNotifications = AutoDisposeNotifier<List<AppNotification>>;
 | 
					 | 
				
			||||||
// ignore_for_file: type=lint
 | 
					 | 
				
			||||||
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package
 | 
					 | 
				
			||||||
@@ -12,7 +12,6 @@ import 'package:island/pods/userinfo.dart';
 | 
				
			|||||||
import 'package:island/pods/websocket.dart';
 | 
					import 'package:island/pods/websocket.dart';
 | 
				
			||||||
import 'package:island/route.dart';
 | 
					import 'package:island/route.dart';
 | 
				
			||||||
import 'package:island/services/responsive.dart';
 | 
					import 'package:island/services/responsive.dart';
 | 
				
			||||||
import 'package:island/widgets/app_notification.dart';
 | 
					 | 
				
			||||||
import 'package:material_symbols_icons/material_symbols_icons.dart';
 | 
					import 'package:material_symbols_icons/material_symbols_icons.dart';
 | 
				
			||||||
import 'package:path_provider/path_provider.dart';
 | 
					import 'package:path_provider/path_provider.dart';
 | 
				
			||||||
import 'package:styled_widget/styled_widget.dart';
 | 
					import 'package:styled_widget/styled_widget.dart';
 | 
				
			||||||
@@ -26,19 +25,26 @@ class WindowScaffold extends HookConsumerWidget {
 | 
				
			|||||||
  Widget build(BuildContext context, WidgetRef ref) {
 | 
					  Widget build(BuildContext context, WidgetRef ref) {
 | 
				
			||||||
    // Add window resize listener for desktop platforms
 | 
					    // Add window resize listener for desktop platforms
 | 
				
			||||||
    useEffect(() {
 | 
					    useEffect(() {
 | 
				
			||||||
      if (!kIsWeb && (Platform.isWindows || Platform.isLinux || Platform.isMacOS)) {
 | 
					      if (!kIsWeb &&
 | 
				
			||||||
 | 
					          (Platform.isWindows || Platform.isLinux || Platform.isMacOS)) {
 | 
				
			||||||
        void saveWindowSize() {
 | 
					        void saveWindowSize() {
 | 
				
			||||||
          final size = appWindow.size;
 | 
					          final size = appWindow.size;
 | 
				
			||||||
          final settingsNotifier = ref.read(appSettingsNotifierProvider.notifier);
 | 
					          final settingsNotifier = ref.read(
 | 
				
			||||||
 | 
					            appSettingsNotifierProvider.notifier,
 | 
				
			||||||
 | 
					          );
 | 
				
			||||||
          settingsNotifier.setWindowSize(size);
 | 
					          settingsNotifier.setWindowSize(size);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        // Save window size when app is about to close
 | 
					        // Save window size when app is about to close
 | 
				
			||||||
        WidgetsBinding.instance.addObserver(_WindowSizeObserver(saveWindowSize));
 | 
					        WidgetsBinding.instance.addObserver(
 | 
				
			||||||
 | 
					          _WindowSizeObserver(saveWindowSize),
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return () {
 | 
					        return () {
 | 
				
			||||||
          // Cleanup observer when widget is disposed
 | 
					          // Cleanup observer when widget is disposed
 | 
				
			||||||
          WidgetsBinding.instance.removeObserver(_WindowSizeObserver(saveWindowSize));
 | 
					          WidgetsBinding.instance.removeObserver(
 | 
				
			||||||
 | 
					            _WindowSizeObserver(saveWindowSize),
 | 
				
			||||||
 | 
					          );
 | 
				
			||||||
        };
 | 
					        };
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
      return null;
 | 
					      return null;
 | 
				
			||||||
@@ -106,7 +112,6 @@ class WindowScaffold extends HookConsumerWidget {
 | 
				
			|||||||
              ],
 | 
					              ],
 | 
				
			||||||
            ),
 | 
					            ),
 | 
				
			||||||
            _WebSocketIndicator(),
 | 
					            _WebSocketIndicator(),
 | 
				
			||||||
            AppNotificationToast(),
 | 
					 | 
				
			||||||
          ],
 | 
					          ],
 | 
				
			||||||
        ),
 | 
					        ),
 | 
				
			||||||
      );
 | 
					      );
 | 
				
			||||||
@@ -114,11 +119,7 @@ class WindowScaffold extends HookConsumerWidget {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    return Stack(
 | 
					    return Stack(
 | 
				
			||||||
      fit: StackFit.expand,
 | 
					      fit: StackFit.expand,
 | 
				
			||||||
      children: [
 | 
					      children: [Positioned.fill(child: child), _WebSocketIndicator()],
 | 
				
			||||||
        Positioned.fill(child: child),
 | 
					 | 
				
			||||||
        _WebSocketIndicator(),
 | 
					 | 
				
			||||||
        AppNotificationToast(),
 | 
					 | 
				
			||||||
      ],
 | 
					 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@@ -136,7 +137,8 @@ class _WindowSizeObserver extends WidgetsBindingObserver {
 | 
				
			|||||||
    if (state == AppLifecycleState.paused ||
 | 
					    if (state == AppLifecycleState.paused ||
 | 
				
			||||||
        state == AppLifecycleState.detached ||
 | 
					        state == AppLifecycleState.detached ||
 | 
				
			||||||
        state == AppLifecycleState.hidden) {
 | 
					        state == AppLifecycleState.hidden) {
 | 
				
			||||||
      if (!kIsWeb && (Platform.isWindows || Platform.isLinux || Platform.isMacOS)) {
 | 
					      if (!kIsWeb &&
 | 
				
			||||||
 | 
					          (Platform.isWindows || Platform.isLinux || Platform.isMacOS)) {
 | 
				
			||||||
        onSaveWindowSize();
 | 
					        onSaveWindowSize();
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
@@ -144,7 +146,8 @@ class _WindowSizeObserver extends WidgetsBindingObserver {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
  bool operator ==(Object other) {
 | 
					  bool operator ==(Object other) {
 | 
				
			||||||
    return other is _WindowSizeObserver && other.onSaveWindowSize == onSaveWindowSize;
 | 
					    return other is _WindowSizeObserver &&
 | 
				
			||||||
 | 
					        other.onSaveWindowSize == onSaveWindowSize;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,7 +1,10 @@
 | 
				
			|||||||
 | 
					import 'dart:async';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import 'package:auto_route/auto_route.dart';
 | 
					import 'package:auto_route/auto_route.dart';
 | 
				
			||||||
import 'package:flutter/material.dart';
 | 
					import 'package:flutter/material.dart';
 | 
				
			||||||
import 'package:flutter_hooks/flutter_hooks.dart';
 | 
					import 'package:flutter_hooks/flutter_hooks.dart';
 | 
				
			||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
 | 
					import 'package:hooks_riverpod/hooks_riverpod.dart';
 | 
				
			||||||
 | 
					import 'package:island/services/notify.dart';
 | 
				
			||||||
import 'package:island/services/sharing_intent.dart';
 | 
					import 'package:island/services/sharing_intent.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@RoutePage()
 | 
					@RoutePage()
 | 
				
			||||||
@@ -11,10 +14,15 @@ class AppWrapper extends HookConsumerWidget {
 | 
				
			|||||||
  @override
 | 
					  @override
 | 
				
			||||||
  Widget build(BuildContext context, WidgetRef ref) {
 | 
					  Widget build(BuildContext context, WidgetRef ref) {
 | 
				
			||||||
    useEffect(() {
 | 
					    useEffect(() {
 | 
				
			||||||
 | 
					      StreamSubscription? ntySubs;
 | 
				
			||||||
 | 
					      Future(() {
 | 
				
			||||||
 | 
					        if (context.mounted) ntySubs = setupNotificationListener(context, ref);
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
      final sharingService = SharingIntentService();
 | 
					      final sharingService = SharingIntentService();
 | 
				
			||||||
      sharingService.initialize(context);
 | 
					      sharingService.initialize(context);
 | 
				
			||||||
      return () {
 | 
					      return () {
 | 
				
			||||||
        sharingService.dispose();
 | 
					        sharingService.dispose();
 | 
				
			||||||
 | 
					        ntySubs?.cancel();
 | 
				
			||||||
      };
 | 
					      };
 | 
				
			||||||
    }, const []);
 | 
					    }, const []);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -5,6 +5,7 @@ import 'package:gap/gap.dart';
 | 
				
			|||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
 | 
					import 'package:hooks_riverpod/hooks_riverpod.dart';
 | 
				
			||||||
import 'package:island/pods/call.dart';
 | 
					import 'package:island/pods/call.dart';
 | 
				
			||||||
import 'package:island/route.gr.dart';
 | 
					import 'package:island/route.gr.dart';
 | 
				
			||||||
 | 
					import 'package:island/widgets/alert.dart';
 | 
				
			||||||
import 'package:island/widgets/chat/call_participant_tile.dart';
 | 
					import 'package:island/widgets/chat/call_participant_tile.dart';
 | 
				
			||||||
import 'package:island/widgets/content/sheet.dart';
 | 
					import 'package:island/widgets/content/sheet.dart';
 | 
				
			||||||
import 'package:styled_widget/styled_widget.dart';
 | 
					import 'package:styled_widget/styled_widget.dart';
 | 
				
			||||||
@@ -175,14 +176,7 @@ class CallControlsBar extends HookConsumerWidget {
 | 
				
			|||||||
        },
 | 
					        },
 | 
				
			||||||
      );
 | 
					      );
 | 
				
			||||||
    } catch (e) {
 | 
					    } catch (e) {
 | 
				
			||||||
      if (context.mounted) {
 | 
					      showErrorAlert(e);
 | 
				
			||||||
        ScaffoldMessenger.of(context).showSnackBar(
 | 
					 | 
				
			||||||
          SnackBar(
 | 
					 | 
				
			||||||
            content: Text('${'failedToEnumerateDevices'.tr()}: $e'),
 | 
					 | 
				
			||||||
            backgroundColor: Colors.red,
 | 
					 | 
				
			||||||
          ),
 | 
					 | 
				
			||||||
        );
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -11,6 +11,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
 | 
				
			|||||||
import 'package:island/models/file.dart';
 | 
					import 'package:island/models/file.dart';
 | 
				
			||||||
import 'package:island/pods/config.dart';
 | 
					import 'package:island/pods/config.dart';
 | 
				
			||||||
import 'package:island/pods/network.dart';
 | 
					import 'package:island/pods/network.dart';
 | 
				
			||||||
 | 
					import 'package:island/widgets/alert.dart';
 | 
				
			||||||
import 'package:island/widgets/content/cloud_files.dart';
 | 
					import 'package:island/widgets/content/cloud_files.dart';
 | 
				
			||||||
import 'package:path/path.dart' show extension;
 | 
					import 'package:path/path.dart' show extension;
 | 
				
			||||||
import 'package:path_provider/path_provider.dart';
 | 
					import 'package:path_provider/path_provider.dart';
 | 
				
			||||||
@@ -215,14 +216,7 @@ class CloudFileZoomIn extends HookConsumerWidget {
 | 
				
			|||||||
          ),
 | 
					          ),
 | 
				
			||||||
        );
 | 
					        );
 | 
				
			||||||
      } catch (e) {
 | 
					      } catch (e) {
 | 
				
			||||||
        // Show error message
 | 
					        showErrorAlert(e);
 | 
				
			||||||
        if (!context.mounted) return;
 | 
					 | 
				
			||||||
        ScaffoldMessenger.of(context).showSnackBar(
 | 
					 | 
				
			||||||
          SnackBar(
 | 
					 | 
				
			||||||
            content: Text('Failed to save image: $e'),
 | 
					 | 
				
			||||||
            duration: const Duration(seconds: 2),
 | 
					 | 
				
			||||||
          ),
 | 
					 | 
				
			||||||
        );
 | 
					 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -106,7 +106,9 @@ class _PaymentContent extends ConsumerStatefulWidget {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
class _PaymentContentState extends ConsumerState<_PaymentContent> {
 | 
					class _PaymentContentState extends ConsumerState<_PaymentContent> {
 | 
				
			||||||
  static const String _pinStorageKey = 'app_pin_code';
 | 
					  static const String _pinStorageKey = 'app_pin_code';
 | 
				
			||||||
  static final _secureStorage = FlutterSecureStorage();
 | 
					  static final _secureStorage = FlutterSecureStorage(
 | 
				
			||||||
 | 
					    aOptions: AndroidOptions(encryptedSharedPreferences: true),
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  final LocalAuthentication _localAuth = LocalAuthentication();
 | 
					  final LocalAuthentication _localAuth = LocalAuthentication();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -292,13 +292,9 @@ class _ShareSheetState extends ConsumerState<ShareSheet> {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
      if (mounted) {
 | 
					      if (mounted) {
 | 
				
			||||||
        // Show success message
 | 
					        // Show success message
 | 
				
			||||||
        ScaffoldMessenger.of(context).showSnackBar(
 | 
					        showSnackBar(
 | 
				
			||||||
          SnackBar(
 | 
					          'shareToSpecificChatSuccess'.tr(
 | 
				
			||||||
            content: Text(
 | 
					            args: [chatRoom.name ?? 'directChat'.tr()],
 | 
				
			||||||
              'shareToSpecificChatSuccess'.tr(
 | 
					 | 
				
			||||||
                args: [chatRoom.name ?? 'directChat'.tr()],
 | 
					 | 
				
			||||||
              ),
 | 
					 | 
				
			||||||
            ),
 | 
					 | 
				
			||||||
          ),
 | 
					          ),
 | 
				
			||||||
        );
 | 
					        );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -9,7 +9,7 @@
 | 
				
			|||||||
#include <bitsdojo_window_linux/bitsdojo_window_plugin.h>
 | 
					#include <bitsdojo_window_linux/bitsdojo_window_plugin.h>
 | 
				
			||||||
#include <file_selector_linux/file_selector_plugin.h>
 | 
					#include <file_selector_linux/file_selector_plugin.h>
 | 
				
			||||||
#include <flutter_platform_alert/flutter_platform_alert_plugin.h>
 | 
					#include <flutter_platform_alert/flutter_platform_alert_plugin.h>
 | 
				
			||||||
#include <flutter_secure_storage/flutter_secure_storage_plugin.h>
 | 
					#include <flutter_secure_storage_linux/flutter_secure_storage_linux_plugin.h>
 | 
				
			||||||
#include <flutter_timezone/flutter_timezone_plugin.h>
 | 
					#include <flutter_timezone/flutter_timezone_plugin.h>
 | 
				
			||||||
#include <flutter_udid/flutter_udid_plugin.h>
 | 
					#include <flutter_udid/flutter_udid_plugin.h>
 | 
				
			||||||
#include <flutter_webrtc/flutter_web_r_t_c_plugin.h>
 | 
					#include <flutter_webrtc/flutter_web_r_t_c_plugin.h>
 | 
				
			||||||
@@ -33,9 +33,9 @@ void fl_register_plugins(FlPluginRegistry* registry) {
 | 
				
			|||||||
  g_autoptr(FlPluginRegistrar) flutter_platform_alert_registrar =
 | 
					  g_autoptr(FlPluginRegistrar) flutter_platform_alert_registrar =
 | 
				
			||||||
      fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterPlatformAlertPlugin");
 | 
					      fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterPlatformAlertPlugin");
 | 
				
			||||||
  flutter_platform_alert_plugin_register_with_registrar(flutter_platform_alert_registrar);
 | 
					  flutter_platform_alert_plugin_register_with_registrar(flutter_platform_alert_registrar);
 | 
				
			||||||
  g_autoptr(FlPluginRegistrar) flutter_secure_storage_registrar =
 | 
					  g_autoptr(FlPluginRegistrar) flutter_secure_storage_linux_registrar =
 | 
				
			||||||
      fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterSecureStoragePlugin");
 | 
					      fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterSecureStorageLinuxPlugin");
 | 
				
			||||||
  flutter_secure_storage_plugin_register_with_registrar(flutter_secure_storage_registrar);
 | 
					  flutter_secure_storage_linux_plugin_register_with_registrar(flutter_secure_storage_linux_registrar);
 | 
				
			||||||
  g_autoptr(FlPluginRegistrar) flutter_timezone_registrar =
 | 
					  g_autoptr(FlPluginRegistrar) flutter_timezone_registrar =
 | 
				
			||||||
      fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterTimezonePlugin");
 | 
					      fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterTimezonePlugin");
 | 
				
			||||||
  flutter_timezone_plugin_register_with_registrar(flutter_timezone_registrar);
 | 
					  flutter_timezone_plugin_register_with_registrar(flutter_timezone_registrar);
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -6,7 +6,7 @@ list(APPEND FLUTTER_PLUGIN_LIST
 | 
				
			|||||||
  bitsdojo_window_linux
 | 
					  bitsdojo_window_linux
 | 
				
			||||||
  file_selector_linux
 | 
					  file_selector_linux
 | 
				
			||||||
  flutter_platform_alert
 | 
					  flutter_platform_alert
 | 
				
			||||||
  flutter_secure_storage
 | 
					  flutter_secure_storage_linux
 | 
				
			||||||
  flutter_timezone
 | 
					  flutter_timezone
 | 
				
			||||||
  flutter_udid
 | 
					  flutter_udid
 | 
				
			||||||
  flutter_webrtc
 | 
					  flutter_webrtc
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -14,6 +14,7 @@ import firebase_core
 | 
				
			|||||||
import firebase_messaging
 | 
					import firebase_messaging
 | 
				
			||||||
import flutter_inappwebview_macos
 | 
					import flutter_inappwebview_macos
 | 
				
			||||||
import flutter_platform_alert
 | 
					import flutter_platform_alert
 | 
				
			||||||
 | 
					import flutter_secure_storage_macos
 | 
				
			||||||
import flutter_timezone
 | 
					import flutter_timezone
 | 
				
			||||||
import flutter_udid
 | 
					import flutter_udid
 | 
				
			||||||
import flutter_webrtc
 | 
					import flutter_webrtc
 | 
				
			||||||
@@ -47,6 +48,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
 | 
				
			|||||||
  FLTFirebaseMessagingPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseMessagingPlugin"))
 | 
					  FLTFirebaseMessagingPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseMessagingPlugin"))
 | 
				
			||||||
  InAppWebViewFlutterPlugin.register(with: registry.registrar(forPlugin: "InAppWebViewFlutterPlugin"))
 | 
					  InAppWebViewFlutterPlugin.register(with: registry.registrar(forPlugin: "InAppWebViewFlutterPlugin"))
 | 
				
			||||||
  FlutterPlatformAlertPlugin.register(with: registry.registrar(forPlugin: "FlutterPlatformAlertPlugin"))
 | 
					  FlutterPlatformAlertPlugin.register(with: registry.registrar(forPlugin: "FlutterPlatformAlertPlugin"))
 | 
				
			||||||
 | 
					  FlutterSecureStoragePlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStoragePlugin"))
 | 
				
			||||||
  FlutterTimezonePlugin.register(with: registry.registrar(forPlugin: "FlutterTimezonePlugin"))
 | 
					  FlutterTimezonePlugin.register(with: registry.registrar(forPlugin: "FlutterTimezonePlugin"))
 | 
				
			||||||
  FlutterUdidPlugin.register(with: registry.registrar(forPlugin: "FlutterUdidPlugin"))
 | 
					  FlutterUdidPlugin.register(with: registry.registrar(forPlugin: "FlutterUdidPlugin"))
 | 
				
			||||||
  FlutterWebRTCPlugin.register(with: registry.registrar(forPlugin: "FlutterWebRTCPlugin"))
 | 
					  FlutterWebRTCPlugin.register(with: registry.registrar(forPlugin: "FlutterWebRTCPlugin"))
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										48
									
								
								pubspec.lock
									
									
									
									
									
								
							
							
						
						
									
										48
									
								
								pubspec.lock
									
									
									
									
									
								
							@@ -895,10 +895,50 @@ packages:
 | 
				
			|||||||
    dependency: "direct main"
 | 
					    dependency: "direct main"
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
      name: flutter_secure_storage
 | 
					      name: flutter_secure_storage
 | 
				
			||||||
      sha256: "9f3dd2ac3b6875b0fde5b04734789c3ef35ba3965c18e99dd564a7a2f8056df6"
 | 
					      sha256: "9cad52d75ebc511adfae3d447d5d13da15a55a92c9410e50f67335b6d21d16ea"
 | 
				
			||||||
      url: "https://pub.dev"
 | 
					      url: "https://pub.dev"
 | 
				
			||||||
    source: hosted
 | 
					    source: hosted
 | 
				
			||||||
    version: "4.2.1"
 | 
					    version: "9.2.4"
 | 
				
			||||||
 | 
					  flutter_secure_storage_linux:
 | 
				
			||||||
 | 
					    dependency: transitive
 | 
				
			||||||
 | 
					    description:
 | 
				
			||||||
 | 
					      name: flutter_secure_storage_linux
 | 
				
			||||||
 | 
					      sha256: be76c1d24a97d0b98f8b54bce6b481a380a6590df992d0098f868ad54dc8f688
 | 
				
			||||||
 | 
					      url: "https://pub.dev"
 | 
				
			||||||
 | 
					    source: hosted
 | 
				
			||||||
 | 
					    version: "1.2.3"
 | 
				
			||||||
 | 
					  flutter_secure_storage_macos:
 | 
				
			||||||
 | 
					    dependency: transitive
 | 
				
			||||||
 | 
					    description:
 | 
				
			||||||
 | 
					      name: flutter_secure_storage_macos
 | 
				
			||||||
 | 
					      sha256: "6c0a2795a2d1de26ae202a0d78527d163f4acbb11cde4c75c670f3a0fc064247"
 | 
				
			||||||
 | 
					      url: "https://pub.dev"
 | 
				
			||||||
 | 
					    source: hosted
 | 
				
			||||||
 | 
					    version: "3.1.3"
 | 
				
			||||||
 | 
					  flutter_secure_storage_platform_interface:
 | 
				
			||||||
 | 
					    dependency: transitive
 | 
				
			||||||
 | 
					    description:
 | 
				
			||||||
 | 
					      name: flutter_secure_storage_platform_interface
 | 
				
			||||||
 | 
					      sha256: cf91ad32ce5adef6fba4d736a542baca9daf3beac4db2d04be350b87f69ac4a8
 | 
				
			||||||
 | 
					      url: "https://pub.dev"
 | 
				
			||||||
 | 
					    source: hosted
 | 
				
			||||||
 | 
					    version: "1.1.2"
 | 
				
			||||||
 | 
					  flutter_secure_storage_web:
 | 
				
			||||||
 | 
					    dependency: transitive
 | 
				
			||||||
 | 
					    description:
 | 
				
			||||||
 | 
					      name: flutter_secure_storage_web
 | 
				
			||||||
 | 
					      sha256: f4ebff989b4f07b2656fb16b47852c0aab9fed9b4ec1c70103368337bc1886a9
 | 
				
			||||||
 | 
					      url: "https://pub.dev"
 | 
				
			||||||
 | 
					    source: hosted
 | 
				
			||||||
 | 
					    version: "1.2.1"
 | 
				
			||||||
 | 
					  flutter_secure_storage_windows:
 | 
				
			||||||
 | 
					    dependency: transitive
 | 
				
			||||||
 | 
					    description:
 | 
				
			||||||
 | 
					      name: flutter_secure_storage_windows
 | 
				
			||||||
 | 
					      sha256: b20b07cb5ed4ed74fc567b78a72936203f587eba460af1df11281c9326cd3709
 | 
				
			||||||
 | 
					      url: "https://pub.dev"
 | 
				
			||||||
 | 
					    source: hosted
 | 
				
			||||||
 | 
					    version: "3.1.2"
 | 
				
			||||||
  flutter_svg:
 | 
					  flutter_svg:
 | 
				
			||||||
    dependency: "direct main"
 | 
					    dependency: "direct main"
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
@@ -1177,10 +1217,10 @@ packages:
 | 
				
			|||||||
    dependency: transitive
 | 
					    dependency: transitive
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
      name: js
 | 
					      name: js
 | 
				
			||||||
      sha256: "53385261521cc4a0c4658fd0ad07a7d14591cf8fc33abbceae306ddb974888dc"
 | 
					      sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3
 | 
				
			||||||
      url: "https://pub.dev"
 | 
					      url: "https://pub.dev"
 | 
				
			||||||
    source: hosted
 | 
					    source: hosted
 | 
				
			||||||
    version: "0.7.2"
 | 
					    version: "0.6.7"
 | 
				
			||||||
  json_annotation:
 | 
					  json_annotation:
 | 
				
			||||||
    dependency: "direct main"
 | 
					    dependency: "direct main"
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -16,7 +16,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev
 | 
				
			|||||||
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
 | 
					# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
 | 
				
			||||||
# In Windows, build-name is used as the major, minor, and patch parts
 | 
					# In Windows, build-name is used as the major, minor, and patch parts
 | 
				
			||||||
# of the product and file versions while build-number is used as the build suffix.
 | 
					# of the product and file versions while build-number is used as the build suffix.
 | 
				
			||||||
version: 3.0.0+106
 | 
					version: 3.0.0+107
 | 
				
			||||||
 | 
					
 | 
				
			||||||
environment:
 | 
					environment:
 | 
				
			||||||
  sdk: ^3.7.2
 | 
					  sdk: ^3.7.2
 | 
				
			||||||
@@ -117,7 +117,7 @@ dependencies:
 | 
				
			|||||||
  flutter_svg: ^2.1.0
 | 
					  flutter_svg: ^2.1.0
 | 
				
			||||||
  native_exif: ^0.6.2
 | 
					  native_exif: ^0.6.2
 | 
				
			||||||
  local_auth: ^2.3.0
 | 
					  local_auth: ^2.3.0
 | 
				
			||||||
  flutter_secure_storage: ^4.2.1
 | 
					  flutter_secure_storage: ^9.2.4
 | 
				
			||||||
  flutter_math_fork: ^0.7.4
 | 
					  flutter_math_fork: ^0.7.4
 | 
				
			||||||
  share_plus: ^11.0.0
 | 
					  share_plus: ^11.0.0
 | 
				
			||||||
  receive_sharing_intent: ^1.8.1
 | 
					  receive_sharing_intent: ^1.8.1
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -12,6 +12,7 @@
 | 
				
			|||||||
#include <firebase_core/firebase_core_plugin_c_api.h>
 | 
					#include <firebase_core/firebase_core_plugin_c_api.h>
 | 
				
			||||||
#include <flutter_inappwebview_windows/flutter_inappwebview_windows_plugin_c_api.h>
 | 
					#include <flutter_inappwebview_windows/flutter_inappwebview_windows_plugin_c_api.h>
 | 
				
			||||||
#include <flutter_platform_alert/flutter_platform_alert_plugin.h>
 | 
					#include <flutter_platform_alert/flutter_platform_alert_plugin.h>
 | 
				
			||||||
 | 
					#include <flutter_secure_storage_windows/flutter_secure_storage_windows_plugin.h>
 | 
				
			||||||
#include <flutter_timezone/flutter_timezone_plugin_c_api.h>
 | 
					#include <flutter_timezone/flutter_timezone_plugin_c_api.h>
 | 
				
			||||||
#include <flutter_udid/flutter_udid_plugin_c_api.h>
 | 
					#include <flutter_udid/flutter_udid_plugin_c_api.h>
 | 
				
			||||||
#include <flutter_webrtc/flutter_web_r_t_c_plugin.h>
 | 
					#include <flutter_webrtc/flutter_web_r_t_c_plugin.h>
 | 
				
			||||||
@@ -42,6 +43,8 @@ void RegisterPlugins(flutter::PluginRegistry* registry) {
 | 
				
			|||||||
      registry->GetRegistrarForPlugin("FlutterInappwebviewWindowsPluginCApi"));
 | 
					      registry->GetRegistrarForPlugin("FlutterInappwebviewWindowsPluginCApi"));
 | 
				
			||||||
  FlutterPlatformAlertPluginRegisterWithRegistrar(
 | 
					  FlutterPlatformAlertPluginRegisterWithRegistrar(
 | 
				
			||||||
      registry->GetRegistrarForPlugin("FlutterPlatformAlertPlugin"));
 | 
					      registry->GetRegistrarForPlugin("FlutterPlatformAlertPlugin"));
 | 
				
			||||||
 | 
					  FlutterSecureStorageWindowsPluginRegisterWithRegistrar(
 | 
				
			||||||
 | 
					      registry->GetRegistrarForPlugin("FlutterSecureStorageWindowsPlugin"));
 | 
				
			||||||
  FlutterTimezonePluginCApiRegisterWithRegistrar(
 | 
					  FlutterTimezonePluginCApiRegisterWithRegistrar(
 | 
				
			||||||
      registry->GetRegistrarForPlugin("FlutterTimezonePluginCApi"));
 | 
					      registry->GetRegistrarForPlugin("FlutterTimezonePluginCApi"));
 | 
				
			||||||
  FlutterUdidPluginCApiRegisterWithRegistrar(
 | 
					  FlutterUdidPluginCApiRegisterWithRegistrar(
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -9,6 +9,7 @@ list(APPEND FLUTTER_PLUGIN_LIST
 | 
				
			|||||||
  firebase_core
 | 
					  firebase_core
 | 
				
			||||||
  flutter_inappwebview_windows
 | 
					  flutter_inappwebview_windows
 | 
				
			||||||
  flutter_platform_alert
 | 
					  flutter_platform_alert
 | 
				
			||||||
 | 
					  flutter_secure_storage_windows
 | 
				
			||||||
  flutter_timezone
 | 
					  flutter_timezone
 | 
				
			||||||
  flutter_udid
 | 
					  flutter_udid
 | 
				
			||||||
  flutter_webrtc
 | 
					  flutter_webrtc
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user