Compare commits
	
		
			2 Commits
		
	
	
		
			36fb06b81c
			...
			00b9c4b957
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 00b9c4b957 | |||
| 6fbf3d9fc4 | 
@@ -4,6 +4,7 @@ import 'dart:io';
 | 
				
			|||||||
import 'package:croppy/croppy.dart';
 | 
					import 'package:croppy/croppy.dart';
 | 
				
			||||||
import 'package:easy_localization/easy_localization.dart' hide TextDirection;
 | 
					import 'package:easy_localization/easy_localization.dart' hide TextDirection;
 | 
				
			||||||
import 'package:firebase_core/firebase_core.dart';
 | 
					import 'package:firebase_core/firebase_core.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/material.dart';
 | 
				
			||||||
import 'package:flutter_hooks/flutter_hooks.dart';
 | 
					import 'package:flutter_hooks/flutter_hooks.dart';
 | 
				
			||||||
@@ -26,6 +27,7 @@ import 'package:relative_time/relative_time.dart';
 | 
				
			|||||||
import 'package:shared_preferences/shared_preferences.dart';
 | 
					import 'package:shared_preferences/shared_preferences.dart';
 | 
				
			||||||
import 'package:image_picker_platform_interface/image_picker_platform_interface.dart';
 | 
					import 'package:image_picker_platform_interface/image_picker_platform_interface.dart';
 | 
				
			||||||
import 'package:flutter_native_splash/flutter_native_splash.dart';
 | 
					import 'package:flutter_native_splash/flutter_native_splash.dart';
 | 
				
			||||||
 | 
					import 'package:url_launcher/url_launcher_string.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
void main() async {
 | 
					void main() async {
 | 
				
			||||||
  final widgetsBinding = WidgetsFlutterBinding.ensureInitialized();
 | 
					  final widgetsBinding = WidgetsFlutterBinding.ensureInitialized();
 | 
				
			||||||
@@ -111,6 +113,33 @@ class IslandApp extends HookConsumerWidget {
 | 
				
			|||||||
  Widget build(BuildContext context, WidgetRef ref) {
 | 
					  Widget build(BuildContext context, WidgetRef ref) {
 | 
				
			||||||
    final theme = ref.watch(themeProvider);
 | 
					    final theme = ref.watch(themeProvider);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    void handleMessage(RemoteMessage notification) {
 | 
				
			||||||
 | 
					      if (notification.data['action_uri'] != null) {
 | 
				
			||||||
 | 
					        var uri = notification.data['action_uri'] as String;
 | 
				
			||||||
 | 
					        if (uri.startsWith('/')) {
 | 
				
			||||||
 | 
					          // In-app routes
 | 
				
			||||||
 | 
					          _appRouter.pushPath(notification.data['action_uri']);
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					          // External links
 | 
				
			||||||
 | 
					          launchUrlString(uri);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    useEffect(() {
 | 
				
			||||||
 | 
					      Future(() async {
 | 
				
			||||||
 | 
					        RemoteMessage? initialMessage =
 | 
				
			||||||
 | 
					            await FirebaseMessaging.instance.getInitialMessage();
 | 
				
			||||||
 | 
					        if (initialMessage != null) {
 | 
				
			||||||
 | 
					          handleMessage(initialMessage);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        FirebaseMessaging.onMessageOpenedApp.listen(handleMessage);
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      return null;
 | 
				
			||||||
 | 
					    }, []);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    useEffect(() {
 | 
					    useEffect(() {
 | 
				
			||||||
      // Load userinfo
 | 
					      // Load userinfo
 | 
				
			||||||
      final userNotifier = ref.read(userInfoProvider.notifier);
 | 
					      final userNotifier = ref.read(userInfoProvider.notifier);
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,3 +1,4 @@
 | 
				
			|||||||
 | 
					import 'dart:async';
 | 
				
			||||||
import 'dart:io';
 | 
					import 'dart:io';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import 'package:auto_route/auto_route.dart';
 | 
					import 'package:auto_route/auto_route.dart';
 | 
				
			||||||
@@ -5,15 +6,22 @@ import 'package:bitsdojo_window/bitsdojo_window.dart';
 | 
				
			|||||||
import 'package:easy_localization/easy_localization.dart';
 | 
					import 'package:easy_localization/easy_localization.dart';
 | 
				
			||||||
import 'package:flutter/foundation.dart';
 | 
					import 'package:flutter/foundation.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/models/user.dart';
 | 
				
			||||||
import 'package:island/pods/userinfo.dart';
 | 
					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:json_annotation/json_annotation.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:riverpod_annotation/riverpod_annotation.dart';
 | 
				
			||||||
import 'package:styled_widget/styled_widget.dart';
 | 
					import 'package:styled_widget/styled_widget.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					part 'app_scaffold.freezed.dart';
 | 
				
			||||||
 | 
					part 'app_scaffold.g.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class WindowScaffold extends HookConsumerWidget {
 | 
					class WindowScaffold extends HookConsumerWidget {
 | 
				
			||||||
  final Widget child;
 | 
					  final Widget child;
 | 
				
			||||||
  final AppRouter router;
 | 
					  final AppRouter router;
 | 
				
			||||||
@@ -83,6 +91,7 @@ class WindowScaffold extends HookConsumerWidget {
 | 
				
			|||||||
              ],
 | 
					              ],
 | 
				
			||||||
            ),
 | 
					            ),
 | 
				
			||||||
            _WebSocketIndicator(),
 | 
					            _WebSocketIndicator(),
 | 
				
			||||||
 | 
					            _AppNotificationToast(),
 | 
				
			||||||
          ],
 | 
					          ],
 | 
				
			||||||
        ),
 | 
					        ),
 | 
				
			||||||
      );
 | 
					      );
 | 
				
			||||||
@@ -90,7 +99,7 @@ class WindowScaffold extends HookConsumerWidget {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    return Stack(
 | 
					    return Stack(
 | 
				
			||||||
      fit: StackFit.expand,
 | 
					      fit: StackFit.expand,
 | 
				
			||||||
      children: [child, _WebSocketIndicator()],
 | 
					      children: [child, _WebSocketIndicator(), _AppNotificationToast()],
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@@ -289,6 +298,71 @@ class _WebSocketIndicator extends HookConsumerWidget {
 | 
				
			|||||||
      indicatorText = 'connectionDisconnected';
 | 
					      indicatorText = 'connectionDisconnected';
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Add a test button for notifications when connected
 | 
				
			||||||
 | 
					    if (websocketState == WebSocketState.connected &&
 | 
				
			||||||
 | 
					        user.hasValue &&
 | 
				
			||||||
 | 
					        user.value != null) {
 | 
				
			||||||
 | 
					      // This is just for testing - you can remove this later
 | 
				
			||||||
 | 
					      Future.delayed(const Duration(milliseconds: 100), () {
 | 
				
			||||||
 | 
					        // Add a small button to the corner of the screen for testing
 | 
				
			||||||
 | 
					        WidgetsBinding.instance.addPostFrameCallback((_) {
 | 
				
			||||||
 | 
					          final overlay = Overlay.of(context);
 | 
				
			||||||
 | 
					          final entry = OverlayEntry(
 | 
				
			||||||
 | 
					            builder:
 | 
				
			||||||
 | 
					                (context) => Positioned(
 | 
				
			||||||
 | 
					                  right: 20,
 | 
				
			||||||
 | 
					                  bottom: 100,
 | 
				
			||||||
 | 
					                  child: Material(
 | 
				
			||||||
 | 
					                    color: Colors.transparent,
 | 
				
			||||||
 | 
					                    child: InkWell(
 | 
				
			||||||
 | 
					                      onTap: () {
 | 
				
			||||||
 | 
					                        final testNotification = SnNotification(
 | 
				
			||||||
 | 
					                          id: 'test-${DateTime.now().millisecondsSinceEpoch}',
 | 
				
			||||||
 | 
					                          createdAt: DateTime.now(),
 | 
				
			||||||
 | 
					                          updatedAt: DateTime.now(),
 | 
				
			||||||
 | 
					                          deletedAt: null,
 | 
				
			||||||
 | 
					                          topic: 'test',
 | 
				
			||||||
 | 
					                          title: 'Test Notification',
 | 
				
			||||||
 | 
					                          content: 'This is a test notification message',
 | 
				
			||||||
 | 
					                          priority: 1,
 | 
				
			||||||
 | 
					                          viewedAt: null,
 | 
				
			||||||
 | 
					                          accountId: 'test',
 | 
				
			||||||
 | 
					                          meta: {},
 | 
				
			||||||
 | 
					                        );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                        ref
 | 
				
			||||||
 | 
					                            .read(appNotificationsProvider.notifier)
 | 
				
			||||||
 | 
					                            .showNotification(
 | 
				
			||||||
 | 
					                              data: testNotification,
 | 
				
			||||||
 | 
					                              icon: Icons.notifications,
 | 
				
			||||||
 | 
					                              duration: const Duration(seconds: 5),
 | 
				
			||||||
 | 
					                            );
 | 
				
			||||||
 | 
					                      },
 | 
				
			||||||
 | 
					                      child: Container(
 | 
				
			||||||
 | 
					                        padding: const EdgeInsets.all(8),
 | 
				
			||||||
 | 
					                        decoration: BoxDecoration(
 | 
				
			||||||
 | 
					                          color: Colors.blue.withOpacity(0.7),
 | 
				
			||||||
 | 
					                          borderRadius: BorderRadius.circular(8),
 | 
				
			||||||
 | 
					                        ),
 | 
				
			||||||
 | 
					                        child: const Icon(
 | 
				
			||||||
 | 
					                          Icons.notifications,
 | 
				
			||||||
 | 
					                          color: Colors.white,
 | 
				
			||||||
 | 
					                        ),
 | 
				
			||||||
 | 
					                      ),
 | 
				
			||||||
 | 
					                    ),
 | 
				
			||||||
 | 
					                  ),
 | 
				
			||||||
 | 
					                ),
 | 
				
			||||||
 | 
					          );
 | 
				
			||||||
 | 
					          // Only add if not already added
 | 
				
			||||||
 | 
					          try {
 | 
				
			||||||
 | 
					            overlay.insert(entry);
 | 
				
			||||||
 | 
					          } catch (e) {
 | 
				
			||||||
 | 
					            // Ignore if already added
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return AnimatedPositioned(
 | 
					    return AnimatedPositioned(
 | 
				
			||||||
      duration: Duration(milliseconds: 1850),
 | 
					      duration: Duration(milliseconds: 1850),
 | 
				
			||||||
      top:
 | 
					      top:
 | 
				
			||||||
@@ -323,3 +397,236 @@ class _WebSocketIndicator extends HookConsumerWidget {
 | 
				
			|||||||
    );
 | 
					    );
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class _AppNotificationToast extends HookConsumerWidget {
 | 
				
			||||||
 | 
					  const _AppNotificationToast();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  Widget build(BuildContext context, WidgetRef ref) {
 | 
				
			||||||
 | 
					    final notifications = ref.watch(appNotificationsProvider);
 | 
				
			||||||
 | 
					    final isDesktop =
 | 
				
			||||||
 | 
					        !kIsWeb && (Platform.isMacOS || Platform.isWindows || Platform.isLinux);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // If no notifications, return empty container
 | 
				
			||||||
 | 
					    if (notifications.isEmpty) {
 | 
				
			||||||
 | 
					      return const SizedBox.shrink();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Get the most recent notification
 | 
				
			||||||
 | 
					    final notification = notifications.last;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Calculate position based on device type
 | 
				
			||||||
 | 
					    final safeAreaTop = MediaQuery.of(context).padding.top;
 | 
				
			||||||
 | 
					    final notificationTop = safeAreaTop + (isDesktop ? 30 : 10);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return Positioned(
 | 
				
			||||||
 | 
					      top: notificationTop,
 | 
				
			||||||
 | 
					      left: 16,
 | 
				
			||||||
 | 
					      right: 16,
 | 
				
			||||||
 | 
					      child: Column(
 | 
				
			||||||
 | 
					        children:
 | 
				
			||||||
 | 
					            notifications.map((notification) {
 | 
				
			||||||
 | 
					              // Calculate how long the notification has been visible
 | 
				
			||||||
 | 
					              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);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					              return _NotificationCard(
 | 
				
			||||||
 | 
					                notification: notification,
 | 
				
			||||||
 | 
					                progress: progress.clamp(0.0, 1.0),
 | 
				
			||||||
 | 
					                onDismiss: () {
 | 
				
			||||||
 | 
					                  ref
 | 
				
			||||||
 | 
					                      .read(appNotificationsProvider.notifier)
 | 
				
			||||||
 | 
					                      .removeNotification(notification);
 | 
				
			||||||
 | 
					                },
 | 
				
			||||||
 | 
					              );
 | 
				
			||||||
 | 
					            }).toList(),
 | 
				
			||||||
 | 
					      ),
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class _NotificationCard extends StatelessWidget {
 | 
				
			||||||
 | 
					  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) {
 | 
				
			||||||
 | 
					    return Dismissible(
 | 
				
			||||||
 | 
					      key: ValueKey(notification.data.id),
 | 
				
			||||||
 | 
					      direction: DismissDirection.horizontal,
 | 
				
			||||||
 | 
					      onDismissed: (_) => onDismiss(),
 | 
				
			||||||
 | 
					      child: Card(
 | 
				
			||||||
 | 
					        elevation: 4,
 | 
				
			||||||
 | 
					        margin: const EdgeInsets.only(bottom: 8),
 | 
				
			||||||
 | 
					        shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
 | 
				
			||||||
 | 
					        child: InkWell(
 | 
				
			||||||
 | 
					          onTap: () {},
 | 
				
			||||||
 | 
					          borderRadius: BorderRadius.circular(12),
 | 
				
			||||||
 | 
					          child: Column(
 | 
				
			||||||
 | 
					            crossAxisAlignment: CrossAxisAlignment.start,
 | 
				
			||||||
 | 
					            children: [
 | 
				
			||||||
 | 
					              ClipRRect(
 | 
				
			||||||
 | 
					                borderRadius: const BorderRadius.only(
 | 
				
			||||||
 | 
					                  topLeft: Radius.circular(12),
 | 
				
			||||||
 | 
					                  topRight: Radius.circular(12),
 | 
				
			||||||
 | 
					                ),
 | 
				
			||||||
 | 
					                child: LinearProgressIndicator(
 | 
				
			||||||
 | 
					                  value: progress,
 | 
				
			||||||
 | 
					                  backgroundColor: Colors.transparent,
 | 
				
			||||||
 | 
					                  minHeight: 2,
 | 
				
			||||||
 | 
					                ),
 | 
				
			||||||
 | 
					              ),
 | 
				
			||||||
 | 
					              Padding(
 | 
				
			||||||
 | 
					                padding: const EdgeInsets.all(12),
 | 
				
			||||||
 | 
					                child: Row(
 | 
				
			||||||
 | 
					                  crossAxisAlignment: CrossAxisAlignment.start,
 | 
				
			||||||
 | 
					                  children: [
 | 
				
			||||||
 | 
					                    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(
 | 
				
			||||||
 | 
					                            notification.data.title,
 | 
				
			||||||
 | 
					                            style: Theme.of(context).textTheme.titleMedium
 | 
				
			||||||
 | 
					                                ?.copyWith(fontWeight: FontWeight.bold),
 | 
				
			||||||
 | 
					                          ),
 | 
				
			||||||
 | 
					                          if (notification.data.content.isNotEmpty)
 | 
				
			||||||
 | 
					                            Text(
 | 
				
			||||||
 | 
					                              notification.data.content,
 | 
				
			||||||
 | 
					                              style: Theme.of(context).textTheme.bodyMedium,
 | 
				
			||||||
 | 
					                            ).padding(top: 4),
 | 
				
			||||||
 | 
					                          if (notification.data.subtitle.isNotEmpty)
 | 
				
			||||||
 | 
					                            Text(
 | 
				
			||||||
 | 
					                              notification.data.subtitle,
 | 
				
			||||||
 | 
					                              style: Theme.of(context).textTheme.bodySmall,
 | 
				
			||||||
 | 
					                            ).padding(top: 2),
 | 
				
			||||||
 | 
					                        ],
 | 
				
			||||||
 | 
					                      ),
 | 
				
			||||||
 | 
					                    ),
 | 
				
			||||||
 | 
					                    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,
 | 
				
			||||||
 | 
					  }) = _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) {
 | 
				
			||||||
 | 
					            default:
 | 
				
			||||||
 | 
					              icon = Symbols.info;
 | 
				
			||||||
 | 
					              break;
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          addNotification(
 | 
				
			||||||
 | 
					            AppNotification(data: data, icon: icon, createdAt: data.createdAt),
 | 
				
			||||||
 | 
					          );
 | 
				
			||||||
 | 
					        } catch (e) {
 | 
				
			||||||
 | 
					          print('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, () {
 | 
				
			||||||
 | 
					      removeNotification(newNotification);
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  void removeNotification(AppNotification notification) {
 | 
				
			||||||
 | 
					    state = state.where((n) => n != notification).toList();
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // 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,
 | 
				
			||||||
 | 
					      ),
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										187
									
								
								lib/widgets/app_scaffold.freezed.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										187
									
								
								lib/widgets/app_scaffold.freezed.dart
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,187 @@
 | 
				
			|||||||
 | 
					// 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_scaffold.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;
 | 
				
			||||||
 | 
					/// 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));
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@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));
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@JsonKey(includeFromJson: false, includeToJson: false)
 | 
				
			||||||
 | 
					@override
 | 
				
			||||||
 | 
					int get hashCode => Object.hash(runtimeType,data,icon,duration,createdAt);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@override
 | 
				
			||||||
 | 
					String toString({ DiagnosticLevel minLevel = DiagnosticLevel.info }) {
 | 
				
			||||||
 | 
					  return 'AppNotification(data: $data, icon: $icon, duration: $duration, createdAt: $createdAt)';
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// @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
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					$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,}) {
 | 
				
			||||||
 | 
					  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?,
 | 
				
			||||||
 | 
					  ));
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					/// 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});
 | 
				
			||||||
 | 
					  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;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// 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));
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@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));
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@JsonKey(includeFromJson: false, includeToJson: false)
 | 
				
			||||||
 | 
					@override
 | 
				
			||||||
 | 
					int get hashCode => Object.hash(runtimeType,data,icon,duration,createdAt);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@override
 | 
				
			||||||
 | 
					String toString({ DiagnosticLevel minLevel = DiagnosticLevel.info }) {
 | 
				
			||||||
 | 
					  return 'AppNotification(data: $data, icon: $icon, duration: $duration, createdAt: $createdAt)';
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// @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
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@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,}) {
 | 
				
			||||||
 | 
					  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?,
 | 
				
			||||||
 | 
					  ));
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// 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
 | 
				
			||||||
							
								
								
									
										48
									
								
								lib/widgets/app_scaffold.g.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										48
									
								
								lib/widgets/app_scaffold.g.dart
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,48 @@
 | 
				
			|||||||
 | 
					// GENERATED CODE - DO NOT MODIFY BY HAND
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					part of 'app_scaffold.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'8ab8b2b23f7f7953b05f08b90a57f495fd6f88d0';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// 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
 | 
				
			||||||
		Reference in New Issue
	
	Block a user