Compare commits
	
		
			2 Commits
		
	
	
		
			9fa666d0b8
			...
			3a2894b533
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 3a2894b533 | |||
| 0d96a6f9ac | 
| @@ -1,6 +1,9 @@ | ||||
| <manifest xmlns:android="http://schemas.android.com/apk/res/android"> | ||||
|     <uses-permission android:name="android.permission.INTERNET" /> | ||||
|     <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> | ||||
|  | ||||
|     <application | ||||
|         android:label="solian" | ||||
|         android:label="Solian" | ||||
|         android:name="${applicationName}" | ||||
|         android:icon="@mipmap/launcher_icon"> | ||||
|         <activity | ||||
| @@ -24,6 +27,7 @@ | ||||
|                 <category android:name="android.intent.category.LAUNCHER"/> | ||||
|             </intent-filter> | ||||
|         </activity> | ||||
|  | ||||
|         <!-- Don't delete the meta-data below. | ||||
|              This is used by the Flutter tool to generate GeneratedPluginRegistrant.java --> | ||||
|         <meta-data | ||||
|   | ||||
							
								
								
									
										
											BIN
										
									
								
								assets/icon-macos.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 90 KiB | 
| @@ -49,6 +49,8 @@ | ||||
| 		<string>UIInterfaceOrientationLandscapeLeft</string> | ||||
| 		<string>UIInterfaceOrientationLandscapeRight</string> | ||||
| 	</array> | ||||
| 	<key>ITSAppUsesNonExemptEncryption</key> | ||||
| 	<false/> | ||||
| 	<key>UISupportedInterfaceOrientations~ipad</key> | ||||
| 	<array> | ||||
| 		<string>UIInterfaceOrientationPortrait</string> | ||||
|   | ||||
| @@ -26,6 +26,10 @@ | ||||
|   "report": "Report", | ||||
|   "reply": "Reply", | ||||
|   "settings": "Settings", | ||||
|   "notification": "Notification", | ||||
|   "notifyDone": "You're done!", | ||||
|   "notifyDoneCaption": "There are no notifications unread for you.", | ||||
|   "notifyListHint": "Pull to refresh, swipe to dismiss", | ||||
|   "reaction": "Reaction", | ||||
|   "reactVerb": "React", | ||||
|   "post": "Post", | ||||
|   | ||||
| @@ -26,6 +26,10 @@ | ||||
|   "report": "举报", | ||||
|   "reply": "回复", | ||||
|   "settings": "设置", | ||||
|   "notification": "通知", | ||||
|   "notifyDone": "所有通知已读!", | ||||
|   "notifyDoneCaption": "这里没有什么东西可以给你看的了~", | ||||
|   "notifyListHint": "下拉以刷新,左滑来已读", | ||||
|   "reaction": "反应", | ||||
|   "reactVerb": "作出反应", | ||||
|   "post": "帖子", | ||||
| @@ -49,6 +53,13 @@ | ||||
|   "chatNew": "新聊天", | ||||
|   "chatNewCreate": "新建频道", | ||||
|   "chatNewJoin": "加入已有频道", | ||||
|   "chatChannelUsage": "频道", | ||||
|   "chatChannelUsageCaption": "频道是一个地方供你聊天,跟一个人,或者一堆人", | ||||
|   "chatChannelOrganize": "组织频道", | ||||
|   "chatChannelEditNotify": "你正在编辑一个已经存在的频道……", | ||||
|   "chatChannelAliasLabel": "频道别名", | ||||
|   "chatChannelNameLabel": "频道名称", | ||||
|   "chatChannelDescriptionLabel": "频道简介", | ||||
|   "chatMessagePlaceholder": "发条消息……", | ||||
|   "chatMessageEditNotify": "你正在编辑信息中……", | ||||
|   "chatMessageReplyNotify": "你正在回复消息中……", | ||||
|   | ||||
| @@ -3,10 +3,12 @@ import 'package:provider/provider.dart'; | ||||
| import 'package:solian/providers/auth.dart'; | ||||
| import 'package:solian/providers/chat.dart'; | ||||
| import 'package:solian/providers/navigation.dart'; | ||||
| import 'package:solian/providers/notify.dart'; | ||||
| import 'package:solian/router.dart'; | ||||
| import 'package:solian/utils/timeago.dart'; | ||||
| import 'package:flutter_gen/gen_l10n/app_localizations.dart'; | ||||
| import 'package:solian/utils/video_player.dart'; | ||||
| import 'package:solian/widgets/notification_notifier.dart'; | ||||
|  | ||||
| void main() { | ||||
|   initVideo(); | ||||
| @@ -39,8 +41,9 @@ class SolianApp extends StatelessWidget { | ||||
|                   Provider(create: (_) => NavigationProvider()), | ||||
|                   Provider(create: (_) => AuthProvider()), | ||||
|                   Provider(create: (_) => ChatProvider()), | ||||
|                   ChangeNotifierProvider(create: (_) => NotifyProvider()), | ||||
|                 ], | ||||
|                 child: child, | ||||
|                 child: NotificationNotifier(child: child ?? Container()), | ||||
|               ); | ||||
|             }) | ||||
|           ], | ||||
|   | ||||
| @@ -7,6 +7,7 @@ class Notification { | ||||
|   String content; | ||||
|   List<Link>? links; | ||||
|   bool isImportant; | ||||
|   bool isRealtime; | ||||
|   DateTime? readAt; | ||||
|   int senderId; | ||||
|   int recipientId; | ||||
| @@ -20,6 +21,7 @@ class Notification { | ||||
|     required this.content, | ||||
|     this.links, | ||||
|     required this.isImportant, | ||||
|     required this.isRealtime, | ||||
|     this.readAt, | ||||
|     required this.senderId, | ||||
|     required this.recipientId, | ||||
| @@ -32,10 +34,9 @@ class Notification { | ||||
|         deletedAt: json["deleted_at"], | ||||
|         subject: json["subject"], | ||||
|         content: json["content"], | ||||
|         links: json["links"] != null | ||||
|             ? List<Link>.from(json["links"].map((x) => Link.fromJson(x))) | ||||
|             : List.empty(), | ||||
|         links: json["links"] != null ? List<Link>.from(json["links"].map((x) => Link.fromJson(x))) : List.empty(), | ||||
|         isImportant: json["is_important"], | ||||
|         isRealtime: json["is_realtime"], | ||||
|         readAt: json["read_at"], | ||||
|         senderId: json["sender_id"], | ||||
|         recipientId: json["recipient_id"], | ||||
| @@ -48,10 +49,9 @@ class Notification { | ||||
|         "deleted_at": deletedAt, | ||||
|         "subject": subject, | ||||
|         "content": content, | ||||
|         "links": links != null | ||||
|             ? List<dynamic>.from(links!.map((x) => x.toJson())) | ||||
|             : List.empty(), | ||||
|         "links": links != null ? List<dynamic>.from(links!.map((x) => x.toJson())) : List.empty(), | ||||
|         "is_important": isImportant, | ||||
|         "is_realtime": isRealtime, | ||||
|         "read_at": readAt, | ||||
|         "sender_id": senderId, | ||||
|         "recipient_id": recipientId, | ||||
|   | ||||
							
								
								
									
										56
									
								
								lib/providers/notify.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,56 @@ | ||||
| import 'dart:convert'; | ||||
|  | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:solian/models/pagination.dart'; | ||||
| import 'package:solian/providers/auth.dart'; | ||||
| import 'package:solian/utils/service_url.dart'; | ||||
| import 'package:solian/models/notification.dart' as model; | ||||
| import 'package:web_socket_channel/web_socket_channel.dart'; | ||||
|  | ||||
| class NotifyProvider extends ChangeNotifier { | ||||
|   bool isOpened = false; | ||||
|  | ||||
|   List<model.Notification> notifications = List.empty(growable: true); | ||||
|  | ||||
|   Future<void> fetch(AuthProvider auth) async { | ||||
|     if (!await auth.isAuthorized()) return; | ||||
|  | ||||
|     var uri = getRequestUri('passport', '/api/notifications?skip=0&take=25'); | ||||
|     var res = await auth.client!.get(uri); | ||||
|     if (res.statusCode == 200) { | ||||
|       final result = PaginationResult.fromJson(jsonDecode(utf8.decode(res.bodyBytes))); | ||||
|       notifications = result.data?.map((x) => model.Notification.fromJson(x)).toList() ?? List.empty(growable: true); | ||||
|     } | ||||
|  | ||||
|     notifyListeners(); | ||||
|   } | ||||
|  | ||||
|   Future<WebSocketChannel?> connect(AuthProvider auth) async { | ||||
|     if (auth.client == null) await auth.pickClient(); | ||||
|     if (!await auth.isAuthorized()) return null; | ||||
|  | ||||
|     await auth.refreshToken(); | ||||
|  | ||||
|     var ori = getRequestUri('passport', '/api/notifications/listen'); | ||||
|     var uri = Uri( | ||||
|       scheme: ori.scheme.replaceFirst('http', 'ws'), | ||||
|       host: ori.host, | ||||
|       path: ori.path, | ||||
|       queryParameters: {'tk': Uri.encodeComponent(auth.client!.credentials.accessToken)}, | ||||
|     ); | ||||
|  | ||||
|     final channel = WebSocketChannel.connect(uri); | ||||
|     await channel.ready; | ||||
|  | ||||
|     return channel; | ||||
|   } | ||||
|  | ||||
|   void onRemoteMessage(model.Notification item) { | ||||
|     notifications.add(item); | ||||
|     notifyListeners(); | ||||
|   } | ||||
|  | ||||
|   void clearNonRealtime() { | ||||
|     notifications = notifications.where((x) => !x.isRealtime).toList(); | ||||
|   } | ||||
| } | ||||
| @@ -5,6 +5,7 @@ import 'package:solian/screens/account.dart'; | ||||
| import 'package:solian/screens/chat/chat.dart'; | ||||
| import 'package:solian/screens/chat/index.dart'; | ||||
| import 'package:solian/screens/explore.dart'; | ||||
| import 'package:solian/screens/notification.dart'; | ||||
| import 'package:solian/screens/posts/comment_editor.dart'; | ||||
| import 'package:solian/screens/posts/moment_editor.dart'; | ||||
| import 'package:solian/screens/posts/screen.dart'; | ||||
| @@ -59,6 +60,11 @@ final router = GoRouter( | ||||
|         dataset: state.pathParameters['dataset'] as String, | ||||
|       ), | ||||
|     ), | ||||
|     GoRoute( | ||||
|       path: '/notification', | ||||
|       name: 'notification', | ||||
|       builder: (context, state) => const NotificationScreen(), | ||||
|     ), | ||||
|     GoRoute( | ||||
|       path: '/auth/sign-in', | ||||
|       name: 'auth.sign-in', | ||||
|   | ||||
| @@ -11,6 +11,7 @@ import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart'; | ||||
| import 'package:flutter_gen/gen_l10n/app_localizations.dart'; | ||||
| import 'package:http/http.dart' as http; | ||||
| import 'package:solian/widgets/indent_wrapper.dart'; | ||||
| import 'package:solian/widgets/notification_notifier.dart'; | ||||
| import 'package:solian/widgets/posts/item.dart'; | ||||
|  | ||||
| class ExploreScreen extends StatefulWidget { | ||||
| @@ -76,6 +77,7 @@ class _ExploreScreenState extends State<ExploreScreen> { | ||||
|           } | ||||
|         }, | ||||
|       ), | ||||
|       appBarActions: const [NotificationButton()], | ||||
|       title: AppLocalizations.of(context)!.explore, | ||||
|       child: RefreshIndicator( | ||||
|         onRefresh: () => Future.sync( | ||||
|   | ||||
							
								
								
									
										177
									
								
								lib/screens/notification.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,177 @@ | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:provider/provider.dart'; | ||||
| import 'package:solian/providers/auth.dart'; | ||||
| import 'package:solian/providers/notify.dart'; | ||||
| import 'package:solian/utils/service_url.dart'; | ||||
| import 'package:solian/widgets/indent_wrapper.dart'; | ||||
| import 'package:flutter_gen/gen_l10n/app_localizations.dart'; | ||||
| import 'package:url_launcher/url_launcher_string.dart'; | ||||
| import 'package:solian/models/notification.dart' as model; | ||||
|  | ||||
| class NotificationScreen extends StatefulWidget { | ||||
|   const NotificationScreen({super.key}); | ||||
|  | ||||
|   @override | ||||
|   State<NotificationScreen> createState() => _NotificationScreenState(); | ||||
| } | ||||
|  | ||||
| class _NotificationScreenState extends State<NotificationScreen> { | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     final auth = context.read<AuthProvider>(); | ||||
|     final nty = context.watch<NotifyProvider>(); | ||||
|  | ||||
|     return IndentWrapper( | ||||
|       noSafeArea: true, | ||||
|       title: AppLocalizations.of(context)!.notification, | ||||
|       child: RefreshIndicator( | ||||
|         onRefresh: () => nty.fetch(auth), | ||||
|         child: CustomScrollView( | ||||
|           slivers: [ | ||||
|             nty.notifications.isEmpty | ||||
|                 ? SliverToBoxAdapter( | ||||
|                     child: Container( | ||||
|                       padding: const EdgeInsets.symmetric(horizontal: 10), | ||||
|                       color: Theme.of(context).colorScheme.surfaceVariant, | ||||
|                       child: ListTile( | ||||
|                         leading: const Icon(Icons.check), | ||||
|                         title: Text(AppLocalizations.of(context)!.notifyDone), | ||||
|                         subtitle: Text(AppLocalizations.of(context)!.notifyDoneCaption), | ||||
|                       ), | ||||
|                     ), | ||||
|                   ) | ||||
|                 : SliverList.builder( | ||||
|                     itemCount: nty.notifications.length, | ||||
|                     itemBuilder: (BuildContext context, int index) { | ||||
|                       var element = nty.notifications[index]; | ||||
|                       return NotificationItem( | ||||
|                         index: index, | ||||
|                         item: element, | ||||
|                         onDismiss: () => setState(() { | ||||
|                           nty.notifications.removeAt(index); | ||||
|                         }), | ||||
|                       ); | ||||
|                     }, | ||||
|                   ), | ||||
|             SliverToBoxAdapter( | ||||
|               child: Container( | ||||
|                 padding: const EdgeInsets.only(top: 12), | ||||
|                 child: Text( | ||||
|                   AppLocalizations.of(context)!.notifyListHint, | ||||
|                   textAlign: TextAlign.center, | ||||
|                   style: Theme.of(context).textTheme.bodySmall, | ||||
|                 ), | ||||
|               ), | ||||
|             ), | ||||
|           ], | ||||
|         ), | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
| } | ||||
|  | ||||
| class NotificationItem extends StatelessWidget { | ||||
|   final int index; | ||||
|   final model.Notification item; | ||||
|   final void Function()? onDismiss; | ||||
|  | ||||
|   const NotificationItem({super.key, required this.index, required this.item, this.onDismiss}); | ||||
|  | ||||
|   bool hasLinks() => item.links != null && item.links!.isNotEmpty; | ||||
|  | ||||
|   void showLinks(BuildContext context) { | ||||
|     if (!hasLinks()) return; | ||||
|  | ||||
|     showModalBottomSheet<void>( | ||||
|       context: context, | ||||
|       builder: (BuildContext context) { | ||||
|         return Column( | ||||
|           crossAxisAlignment: CrossAxisAlignment.start, | ||||
|           children: [ | ||||
|             Padding( | ||||
|               padding: const EdgeInsets.only(left: 16, right: 16, top: 34, bottom: 12), | ||||
|               child: Text( | ||||
|                 "Links", | ||||
|                 style: Theme.of(context).textTheme.headlineSmall, | ||||
|               ), | ||||
|             ), | ||||
|             Expanded( | ||||
|               child: ListView.builder( | ||||
|                 itemCount: item.links!.length, | ||||
|                 itemBuilder: (BuildContext context, int index) { | ||||
|                   var element = item.links![index]; | ||||
|                   return ListTile( | ||||
|                     title: Text(element.label), | ||||
|                     onTap: () async { | ||||
|                       await launchUrlString(element.url); | ||||
|                       if (Navigator.canPop(context)) { | ||||
|                         Navigator.pop(context); | ||||
|                       } | ||||
|                     }, | ||||
|                   ); | ||||
|                 }, | ||||
|               ), | ||||
|             ), | ||||
|           ], | ||||
|         ); | ||||
|       }, | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   Future<void> markAsRead(model.Notification element, BuildContext context) async { | ||||
|     if (element.isRealtime) return; | ||||
|  | ||||
|     final auth = context.read<AuthProvider>(); | ||||
|     if (!await auth.isAuthorized()) return; | ||||
|  | ||||
|     var id = element.id; | ||||
|     var uri = getRequestUri('passport', '/api/notifications/$id/read'); | ||||
|     await auth.client!.put(uri); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     return Dismissible( | ||||
|       key: Key('n$index'), | ||||
|       onDismissed: (direction) { | ||||
|         markAsRead(item, context).then((value) { | ||||
|           ScaffoldMessenger.of(context).showSnackBar( | ||||
|             SnackBar( | ||||
|               content: RichText( | ||||
|                 text: TextSpan( | ||||
|                   children: [ | ||||
|                     TextSpan( | ||||
|                       text: item.subject, | ||||
|                       style: const TextStyle(fontWeight: FontWeight.bold), | ||||
|                     ), | ||||
|                     const TextSpan(text: " is marked as read") | ||||
|                   ], | ||||
|                 ), | ||||
|               ), | ||||
|             ), | ||||
|           ); | ||||
|         }); | ||||
|         if (onDismiss != null) { | ||||
|           onDismiss!(); | ||||
|         } | ||||
|       }, | ||||
|       background: Container( | ||||
|         color: Colors.lightBlue, | ||||
|       ), | ||||
|       child: Container( | ||||
|         padding: const EdgeInsets.only(left: 10), | ||||
|         child: ListTile( | ||||
|           title: Text(item.subject), | ||||
|           subtitle: Text(item.content), | ||||
|           trailing: hasLinks() | ||||
|               ? TextButton( | ||||
|                   onPressed: () => showLinks(context), | ||||
|                   style: TextButton.styleFrom(shape: const CircleBorder()), | ||||
|                   child: const Icon(Icons.more_vert), | ||||
|                 ) | ||||
|               : null, | ||||
|         ), | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
| } | ||||
| @@ -34,7 +34,7 @@ class SignInScreen extends StatelessWidget { | ||||
|                 decoration: InputDecoration( | ||||
|                   isDense: true, | ||||
|                   border: const UnderlineInputBorder(), | ||||
|                   hintText: AppLocalizations.of(context)!.username, | ||||
|                   labelText: AppLocalizations.of(context)!.username, | ||||
|                 ), | ||||
|                 onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(), | ||||
|               ), | ||||
| @@ -47,7 +47,7 @@ class SignInScreen extends StatelessWidget { | ||||
|                 decoration: InputDecoration( | ||||
|                   isDense: true, | ||||
|                   border: const UnderlineInputBorder(), | ||||
|                   hintText: AppLocalizations.of(context)!.password, | ||||
|                   labelText: AppLocalizations.of(context)!.password, | ||||
|                 ), | ||||
|                 onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(), | ||||
|               ), | ||||
|   | ||||
| @@ -31,7 +31,7 @@ class _ChatMaintainerState extends State<ChatMaintainer> { | ||||
|     final notify = ScaffoldMessenger.of(context).showSnackBar( | ||||
|       SnackBar( | ||||
|         content: Text(AppLocalizations.of(context)!.connectingServer), | ||||
|         duration: const Duration(days: 1), | ||||
|         duration: const Duration(minutes: 1), | ||||
|       ), | ||||
|     ); | ||||
|  | ||||
| @@ -55,6 +55,7 @@ class _ChatMaintainerState extends State<ChatMaintainer> { | ||||
|           } | ||||
|         }, | ||||
|         onError: (_, __) => connect(), | ||||
|         onDone: () => connect(), | ||||
|       ); | ||||
|  | ||||
|       notify.close(); | ||||
| @@ -72,6 +73,8 @@ class _ChatMaintainerState extends State<ChatMaintainer> { | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     ScaffoldMessenger.of(context).clearSnackBars(); | ||||
|  | ||||
|     return widget.child; | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -36,7 +36,7 @@ class _ChatMessageEditorState extends State<ChatMessageEditor> { | ||||
|       builder: (context) => AttachmentEditor( | ||||
|         provider: 'messaging', | ||||
|         current: _attachments, | ||||
|         onUpdate: (value) => _attachments = value, | ||||
|         onUpdate: (value) => setState(() => _attachments = value), | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
|   | ||||
							
								
								
									
										90
									
								
								lib/widgets/notification_notifier.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,90 @@ | ||||
| import 'dart:convert'; | ||||
|  | ||||
| import 'package:flutter/material.dart'; | ||||
| import 'package:provider/provider.dart'; | ||||
| import 'package:solian/providers/auth.dart'; | ||||
| import 'package:solian/providers/notify.dart'; | ||||
| import 'package:solian/router.dart'; | ||||
| import 'package:flutter_gen/gen_l10n/app_localizations.dart'; | ||||
| import 'package:solian/models/notification.dart' as model; | ||||
| import 'package:badges/badges.dart' as badge; | ||||
|  | ||||
| class NotificationNotifier extends StatefulWidget { | ||||
|   final Widget child; | ||||
|  | ||||
|   const NotificationNotifier({super.key, required this.child}); | ||||
|  | ||||
|   @override | ||||
|   State<NotificationNotifier> createState() => _NotificationNotifierState(); | ||||
| } | ||||
|  | ||||
| class _NotificationNotifierState extends State<NotificationNotifier> { | ||||
|   void connect() { | ||||
|     final notify = ScaffoldMessenger.of(context).showSnackBar( | ||||
|       SnackBar( | ||||
|         content: Text(AppLocalizations.of(context)!.connectingServer), | ||||
|         duration: const Duration(minutes: 1), | ||||
|       ), | ||||
|     ); | ||||
|  | ||||
|     final auth = context.read<AuthProvider>(); | ||||
|     final nty = context.read<NotifyProvider>(); | ||||
|  | ||||
|     nty.fetch(auth); | ||||
|     nty.connect(auth).then((snapshot) { | ||||
|       snapshot!.stream.listen( | ||||
|         (event) { | ||||
|           final result = model.Notification.fromJson(jsonDecode(event)); | ||||
|           nty.onRemoteMessage(result); | ||||
|         }, | ||||
|         onError: (_, __) => connect(), | ||||
|         onDone: () => connect(), | ||||
|       ); | ||||
|  | ||||
|       notify.close(); | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   void initState() { | ||||
|     Future.delayed(Duration.zero, () { | ||||
|       connect(); | ||||
|     }); | ||||
|  | ||||
|     super.initState(); | ||||
|   } | ||||
|  | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     return widget.child; | ||||
|   } | ||||
| } | ||||
|  | ||||
| class NotificationButton extends StatefulWidget { | ||||
|   const NotificationButton({super.key}); | ||||
|  | ||||
|   @override | ||||
|   State<NotificationButton> createState() => _NotificationButtonState(); | ||||
| } | ||||
|  | ||||
| class _NotificationButtonState extends State<NotificationButton> { | ||||
|   @override | ||||
|   Widget build(BuildContext context) { | ||||
|     final nty = context.watch<NotifyProvider>(); | ||||
|  | ||||
|     return badge.Badge( | ||||
|       showBadge: nty.notifications.isNotEmpty, | ||||
|       position: badge.BadgePosition.custom(top: -2, end: 8), | ||||
|       badgeContent: Text( | ||||
|         nty.notifications.length.toString(), | ||||
|         style: const TextStyle(color: Colors.white), | ||||
|       ), | ||||
|       child: IconButton( | ||||
|         icon: const Icon(Icons.notifications), | ||||
|         onPressed: () { | ||||
|           router.pushNamed("notification"); | ||||
|         }, | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
| } | ||||
| @@ -69,7 +69,7 @@ | ||||
| 		331C80D7294CF71000263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = "<group>"; }; | ||||
| 		333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = "<group>"; }; | ||||
| 		335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = "<group>"; }; | ||||
| 		33CC10ED2044A3C60003C045 /* solian.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = solian.app; sourceTree = BUILT_PRODUCTS_DIR; }; | ||||
| 		33CC10ED2044A3C60003C045 /* Solian.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Solian.app; sourceTree = BUILT_PRODUCTS_DIR; }; | ||||
| 		33CC10F02044A3C60003C045 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; }; | ||||
| 		33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = "<group>"; }; | ||||
| 		33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = "<group>"; }; | ||||
| @@ -144,7 +144,7 @@ | ||||
| 		33CC10EE2044A3C60003C045 /* Products */ = { | ||||
| 			isa = PBXGroup; | ||||
| 			children = ( | ||||
| 				33CC10ED2044A3C60003C045 /* solian.app */, | ||||
| 				33CC10ED2044A3C60003C045 /* Solian.app */, | ||||
| 				331C80D5294CF71000263BE5 /* RunnerTests.xctest */, | ||||
| 			); | ||||
| 			name = Products; | ||||
| @@ -195,7 +195,6 @@ | ||||
| 				1B09C6EB2D1B711F9DE8626F /* Pods-RunnerTests.release.xcconfig */, | ||||
| 				8CBB406EC5E794824CF74930 /* Pods-RunnerTests.profile.xcconfig */, | ||||
| 			); | ||||
| 			name = Pods; | ||||
| 			path = Pods; | ||||
| 			sourceTree = "<group>"; | ||||
| 		}; | ||||
| @@ -249,7 +248,7 @@ | ||||
| 			); | ||||
| 			name = Runner; | ||||
| 			productName = Runner; | ||||
| 			productReference = 33CC10ED2044A3C60003C045 /* solian.app */; | ||||
| 			productReference = 33CC10ED2044A3C60003C045 /* Solian.app */; | ||||
| 			productType = "com.apple.product-type.application"; | ||||
| 		}; | ||||
| /* End PBXNativeTarget section */ | ||||
| @@ -270,7 +269,6 @@ | ||||
| 					33CC10EC2044A3C60003C045 = { | ||||
| 						CreatedOnToolsVersion = 9.2; | ||||
| 						LastSwiftMigration = 1100; | ||||
| 						ProvisioningStyle = Automatic; | ||||
| 						SystemCapabilities = { | ||||
| 							com.apple.Sandbox = { | ||||
| 								enabled = 1; | ||||
| @@ -572,10 +570,14 @@ | ||||
| 				ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; | ||||
| 				CLANG_ENABLE_MODULES = YES; | ||||
| 				CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; | ||||
| 				CODE_SIGN_IDENTITY = "Apple Development"; | ||||
| 				"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; | ||||
| 				CODE_SIGN_STYLE = Automatic; | ||||
| 				COMBINE_HIDPI_IMAGES = YES; | ||||
| 				DEVELOPMENT_TEAM = W7HPZ53V6B; | ||||
| 				INFOPLIST_FILE = Runner/Info.plist; | ||||
| 				INFOPLIST_KEY_CFBundleDisplayName = Solian; | ||||
| 				INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking"; | ||||
| 				LD_RUNPATH_SEARCH_PATHS = ( | ||||
| 					"$(inherited)", | ||||
| 					"@executable_path/../Frameworks", | ||||
| @@ -706,10 +708,14 @@ | ||||
| 				ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; | ||||
| 				CLANG_ENABLE_MODULES = YES; | ||||
| 				CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; | ||||
| 				CODE_SIGN_IDENTITY = "Apple Development"; | ||||
| 				"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; | ||||
| 				CODE_SIGN_STYLE = Automatic; | ||||
| 				COMBINE_HIDPI_IMAGES = YES; | ||||
| 				DEVELOPMENT_TEAM = W7HPZ53V6B; | ||||
| 				INFOPLIST_FILE = Runner/Info.plist; | ||||
| 				INFOPLIST_KEY_CFBundleDisplayName = Solian; | ||||
| 				INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking"; | ||||
| 				LD_RUNPATH_SEARCH_PATHS = ( | ||||
| 					"$(inherited)", | ||||
| 					"@executable_path/../Frameworks", | ||||
| @@ -728,10 +734,14 @@ | ||||
| 				ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; | ||||
| 				CLANG_ENABLE_MODULES = YES; | ||||
| 				CODE_SIGN_ENTITLEMENTS = Runner/Release.entitlements; | ||||
| 				CODE_SIGN_IDENTITY = "Apple Development"; | ||||
| 				"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; | ||||
| 				CODE_SIGN_STYLE = Automatic; | ||||
| 				COMBINE_HIDPI_IMAGES = YES; | ||||
| 				DEVELOPMENT_TEAM = W7HPZ53V6B; | ||||
| 				INFOPLIST_FILE = Runner/Info.plist; | ||||
| 				INFOPLIST_KEY_CFBundleDisplayName = Solian; | ||||
| 				INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking"; | ||||
| 				LD_RUNPATH_SEARCH_PATHS = ( | ||||
| 					"$(inherited)", | ||||
| 					"@executable_path/../Frameworks", | ||||
|   | ||||
| @@ -15,7 +15,7 @@ | ||||
|             <BuildableReference | ||||
|                BuildableIdentifier = "primary" | ||||
|                BlueprintIdentifier = "33CC10EC2044A3C60003C045" | ||||
|                BuildableName = "solian.app" | ||||
|                BuildableName = "Solian.app" | ||||
|                BlueprintName = "Runner" | ||||
|                ReferencedContainer = "container:Runner.xcodeproj"> | ||||
|             </BuildableReference> | ||||
| @@ -31,7 +31,7 @@ | ||||
|          <BuildableReference | ||||
|             BuildableIdentifier = "primary" | ||||
|             BlueprintIdentifier = "33CC10EC2044A3C60003C045" | ||||
|             BuildableName = "solian.app" | ||||
|             BuildableName = "Solian.app" | ||||
|             BlueprintName = "Runner" | ||||
|             ReferencedContainer = "container:Runner.xcodeproj"> | ||||
|          </BuildableReference> | ||||
| @@ -65,7 +65,7 @@ | ||||
|          <BuildableReference | ||||
|             BuildableIdentifier = "primary" | ||||
|             BlueprintIdentifier = "33CC10EC2044A3C60003C045" | ||||
|             BuildableName = "solian.app" | ||||
|             BuildableName = "Solian.app" | ||||
|             BlueprintName = "Runner" | ||||
|             ReferencedContainer = "container:Runner.xcodeproj"> | ||||
|          </BuildableReference> | ||||
| @@ -82,7 +82,7 @@ | ||||
|          <BuildableReference | ||||
|             BuildableIdentifier = "primary" | ||||
|             BlueprintIdentifier = "33CC10EC2044A3C60003C045" | ||||
|             BuildableName = "solian.app" | ||||
|             BuildableName = "Solian.app" | ||||
|             BlueprintName = "Runner" | ||||
|             ReferencedContainer = "container:Runner.xcodeproj"> | ||||
|          </BuildableReference> | ||||
|   | ||||
| Before Width: | Height: | Size: 63 KiB After Width: | Height: | Size: 73 KiB | 
| Before Width: | Height: | Size: 6.0 KiB After Width: | Height: | Size: 6.0 KiB | 
| Before Width: | Height: | Size: 480 B After Width: | Height: | Size: 527 B | 
| Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 13 KiB | 
| Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB | 
| Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 30 KiB | 
| Before Width: | Height: | Size: 2.5 KiB After Width: | Height: | Size: 2.6 KiB | 
| @@ -1,8 +1,8 @@ | ||||
| <?xml version="1.0" encoding="UTF-8"?> | ||||
| <document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="14490.70" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct"> | ||||
| <document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="32700.99.1234" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct"> | ||||
|     <dependencies> | ||||
|         <deployment identifier="macosx"/> | ||||
|         <plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="14490.70"/> | ||||
|         <plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="22689"/> | ||||
|         <capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/> | ||||
|     </dependencies> | ||||
|     <objects> | ||||
| @@ -13,7 +13,7 @@ | ||||
|         </customObject> | ||||
|         <customObject id="-1" userLabel="First Responder" customClass="FirstResponder"/> | ||||
|         <customObject id="-3" userLabel="Application" customClass="NSObject"/> | ||||
|         <customObject id="Voe-Tx-rLC" customClass="AppDelegate" customModule="Runner" customModuleProvider="target"> | ||||
|         <customObject id="Voe-Tx-rLC" customClass="AppDelegate" customModule="Solian" customModuleProvider="target"> | ||||
|             <connections> | ||||
|                 <outlet property="applicationMenu" destination="uQy-DD-JDr" id="XBo-yE-nKs"/> | ||||
|                 <outlet property="mainFlutterWindow" destination="QvC-M9-y7g" id="gIp-Ho-8D9"/> | ||||
| @@ -330,14 +330,15 @@ | ||||
|             </items> | ||||
|             <point key="canvasLocation" x="142" y="-258"/> | ||||
|         </menu> | ||||
|         <window title="APP_NAME" allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" releasedWhenClosed="NO" animationBehavior="default" id="QvC-M9-y7g" customClass="MainFlutterWindow" customModule="Runner" customModuleProvider="target"> | ||||
|         <window title="APP_NAME" allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" releasedWhenClosed="NO" animationBehavior="default" titlebarAppearsTransparent="YES" id="QvC-M9-y7g" customClass="MainFlutterWindow" customModule="Solian" customModuleProvider="target"> | ||||
|             <windowStyleMask key="styleMask" titled="YES" closable="YES" miniaturizable="YES" resizable="YES"/> | ||||
|             <rect key="contentRect" x="335" y="390" width="800" height="600"/> | ||||
|             <rect key="screenRect" x="0.0" y="0.0" width="2560" height="1577"/> | ||||
|             <rect key="contentRect" x="335" y="390" width="380" height="640"/> | ||||
|             <rect key="screenRect" x="0.0" y="0.0" width="1512" height="944"/> | ||||
|             <view key="contentView" wantsLayer="YES" id="EiT-Mj-1SZ"> | ||||
|                 <rect key="frame" x="0.0" y="0.0" width="800" height="600"/> | ||||
|                 <rect key="frame" x="0.0" y="0.0" width="380" height="640"/> | ||||
|                 <autoresizingMask key="autoresizingMask"/> | ||||
|             </view> | ||||
|             <point key="canvasLocation" x="142" y="156"/> | ||||
|         </window> | ||||
|     </objects> | ||||
| </document> | ||||
|   | ||||
| @@ -10,5 +10,7 @@ | ||||
| 	<true/> | ||||
| 	<key>com.apple.security.network.client</key> | ||||
|     <true/> | ||||
|     <key>com.apple.security.files.user-selected.read-only</key> | ||||
|     <true/> | ||||
| </dict> | ||||
| </plist> | ||||
|   | ||||
| @@ -26,6 +26,8 @@ | ||||
| 	<string>$(PRODUCT_COPYRIGHT)</string> | ||||
| 	<key>NSMainNibFile</key> | ||||
| 	<string>MainMenu</string> | ||||
| 	<key>LSApplicationCategoryType</key> | ||||
| 	<string>public.app-category.social-networking</string> | ||||
| 	<key>NSPrincipalClass</key> | ||||
| 	<string>NSApplication</string> | ||||
| </dict> | ||||
|   | ||||
| @@ -7,6 +7,7 @@ class MainFlutterWindow: NSWindow { | ||||
|     let windowFrame = self.frame | ||||
|     self.contentViewController = flutterViewController | ||||
|     self.setFrame(windowFrame, display: true) | ||||
|     self.minSize = NSSize(width: 380, height: 600) | ||||
|  | ||||
|     RegisterGeneratedPlugins(registry: flutterViewController) | ||||
|  | ||||
|   | ||||
| @@ -6,5 +6,7 @@ | ||||
| 	<true/> | ||||
| 	<key>com.apple.security.network.client</key> | ||||
|     <true/> | ||||
|     <key>com.apple.security.files.user-selected.read-only</key> | ||||
|     <true/> | ||||
| </dict> | ||||
| </plist> | ||||
|   | ||||
| @@ -2,7 +2,7 @@ name: solian | ||||
| description: "A new Flutter project." | ||||
| # The following line prevents the package from being accidentally published to | ||||
| # pub.dev using `flutter pub publish`. This is preferred for private packages. | ||||
| publish_to: 'none' # Remove this line if you wish to publish to pub.dev | ||||
| publish_to: "none" # Remove this line if you wish to publish to pub.dev | ||||
|  | ||||
| # The following defines the version and build number for your application. | ||||
| # A version number is three numbers separated by dots, like 1.2.43 | ||||
| @@ -19,7 +19,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev | ||||
| version: 1.0.0+1 | ||||
|  | ||||
| environment: | ||||
|   sdk: '>=3.3.3 <4.0.0' | ||||
|   sdk: ">=3.3.3 <4.0.0" | ||||
|  | ||||
| # Dependencies specify other packages that your package needs in order to work. | ||||
| # To automatically upgrade your package dependencies to the latest versions | ||||
| @@ -31,7 +31,6 @@ dependencies: | ||||
|   flutter: | ||||
|     sdk: flutter | ||||
|  | ||||
|  | ||||
|   # The following adds the Cupertino Icons font to your application. | ||||
|   # Use with the CupertinoIcons class for iOS style icons. | ||||
|   cupertino_icons: ^1.0.6 | ||||
| @@ -132,4 +131,4 @@ flutter_launcher_icons: | ||||
|     icon_size: 256 | ||||
|   macos: | ||||
|     generate: true | ||||
|     image_path: "assets/icon.png" | ||||
|     image_path: "assets/icon-macos.png" | ||||
|   | ||||