diff --git a/ios/Podfile.lock b/ios/Podfile.lock index bfb58c6..adb0115 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -34,6 +34,8 @@ PODS: - DKImagePickerController/PhotoGallery - Flutter - Flutter (1.0.0) + - flutter_local_notifications (0.0.1): + - Flutter - flutter_secure_storage (6.0.0): - Flutter - image_picker_ios (0.0.1): @@ -41,6 +43,8 @@ PODS: - path_provider_foundation (0.0.1): - Flutter - FlutterMacOS + - permission_handler_apple (9.3.0): + - Flutter - SDWebImage (5.19.2): - SDWebImage/Core (= 5.19.2) - SDWebImage/Core (5.19.2) @@ -51,9 +55,11 @@ PODS: DEPENDENCIES: - file_picker (from `.symlinks/plugins/file_picker/ios`) - Flutter (from `Flutter`) + - flutter_local_notifications (from `.symlinks/plugins/flutter_local_notifications/ios`) - flutter_secure_storage (from `.symlinks/plugins/flutter_secure_storage/ios`) - image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`) - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`) + - permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`) - url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`) SPEC REPOS: @@ -68,12 +74,16 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/file_picker/ios" Flutter: :path: Flutter + flutter_local_notifications: + :path: ".symlinks/plugins/flutter_local_notifications/ios" flutter_secure_storage: :path: ".symlinks/plugins/flutter_secure_storage/ios" image_picker_ios: :path: ".symlinks/plugins/image_picker_ios/ios" path_provider_foundation: :path: ".symlinks/plugins/path_provider_foundation/darwin" + permission_handler_apple: + :path: ".symlinks/plugins/permission_handler_apple/ios" url_launcher_ios: :path: ".symlinks/plugins/url_launcher_ios/ios" @@ -82,9 +92,11 @@ SPEC CHECKSUMS: DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60 file_picker: 09aa5ec1ab24135ccd7a1621c46c84134bfd6655 Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 + flutter_local_notifications: 4cde75091f6327eb8517fa068a0a5950212d2086 flutter_secure_storage: d33dac7ae2ea08509be337e775f6b59f1ff45f12 image_picker_ios: c560581cceedb403a6ff17f2f816d7fea1421fc1 path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46 + permission_handler_apple: 9878588469a2b0d0fc1e048d9f43605f92e6cec2 SDWebImage: dfe95b2466a9823cf9f0c6d01217c06550d7b29a SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4 url_launcher_ios: 5334b05cef931de560670eeae103fd3e431ac3fe diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 30118d7..7d03597 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -199,6 +199,7 @@ 9705A1C41CF9048500538489 /* Embed Frameworks */, 3B06AD1E1E4923F5004D2608 /* Thin Binary */, 287A33C298CA352A7E7F32A4 /* [CP] Embed Pods Frameworks */, + 0818E8E4321C0D7433E07576 /* [CP] Copy Pods Resources */, ); buildRules = ( ); @@ -270,6 +271,23 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ + 0818E8E4321C0D7433E07576 /* [CP] Copy Pods Resources */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Copy Pods Resources"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n"; + showEnvVarsInLog = 0; + }; 259653AE41D478F4C6BAE9B2 /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; diff --git a/lib/main.dart b/lib/main.dart index c4f28c4..fb7597b 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; +import 'package:solian/providers/account.dart'; import 'package:solian/providers/auth.dart'; import 'package:solian/providers/content/attachment.dart'; import 'package:solian/providers/friend.dart'; @@ -31,6 +32,14 @@ class SolianApp extends StatelessWidget { Get.lazyPut(() => AuthProvider()); Get.lazyPut(() => FriendProvider()); Get.lazyPut(() => AttachmentProvider()); + Get.lazyPut(() => AccountProvider()); + + final AuthProvider auth = Get.find(); + auth.isAuthorized.then((value) async { + if (value) { + Get.find().connect(); + } + }); }, builder: (context, child) { return ScaffoldMessenger( diff --git a/lib/providers/account.dart b/lib/providers/account.dart new file mode 100644 index 0000000..1fae236 --- /dev/null +++ b/lib/providers/account.dart @@ -0,0 +1,161 @@ +import 'dart:convert'; +import 'dart:io'; +import 'dart:math' as math; + +import 'package:get/get.dart'; +import 'package:permission_handler/permission_handler.dart'; +import 'package:solian/models/notification.dart'; +import 'package:solian/models/packet.dart'; +import 'package:solian/models/pagination.dart'; +import 'package:solian/providers/auth.dart'; +import 'package:solian/services.dart'; +import 'package:web_socket_channel/io.dart'; +import 'package:flutter_local_notifications/flutter_local_notifications.dart'; + +class AccountProvider extends GetxController { + final FlutterLocalNotificationsPlugin localNotify = + FlutterLocalNotificationsPlugin(); + + RxBool isConnected = false.obs; + RxBool isConnecting = false.obs; + + RxInt notificationUnread = 0.obs; + RxList notifications = + List.empty(growable: true).obs; + + IOWebSocketChannel? websocket; + + @override + onInit() { + Permission.notification.request().then((status) { + notifyInitialization(); + notifyPrefetch(); + }); + + super.onInit(); + } + + void connect({noRetry = false}) async { + final AuthProvider auth = Get.find(); + if (!await auth.isAuthorized) throw Exception('unauthorized'); + + if (auth.credentials == null) await auth.loadCredentials(); + + final uri = Uri.parse( + '${ServiceFinder.services['passport']}/api/ws?tk=${auth.credentials!.accessToken}' + .replaceFirst('http', 'ws'), + ); + + isConnecting.value = true; + + try { + websocket = IOWebSocketChannel.connect(uri); + await websocket?.ready; + } catch (e) { + if (!noRetry) { + await auth.refreshCredentials(); + return connect(noRetry: true); + } + } + + listen(); + + isConnected.value = true; + isConnecting.value = false; + } + + void disconnect() { + websocket?.sink.close(WebSocketStatus.normalClosure); + isConnected.value = false; + } + + void listen() { + websocket?.stream.listen( + (event) { + final packet = NetworkPackage.fromJson(jsonDecode(event)); + switch (packet.method) { + case 'notifications.new': + final notification = Notification.fromJson(packet.payload!); + notificationUnread++; + notifications.add(notification); + notifyMessage(notification.subject, notification.content); + break; + } + }, + onDone: () { + isConnected.value = false; + }, + onError: (err) { + isConnected.value = false; + }, + ); + } + + void notifyInitialization() { + const androidSettings = AndroidInitializationSettings('app_icon'); + const darwinSettings = DarwinInitializationSettings( + notificationCategories: [ + DarwinNotificationCategory('general'), + ], + ); + const linuxSettings = + LinuxInitializationSettings(defaultActionName: 'Open notification'); + const InitializationSettings initializationSettings = + InitializationSettings( + android: androidSettings, + iOS: darwinSettings, + macOS: darwinSettings, + linux: linuxSettings, + ); + + localNotify.initialize(initializationSettings); + } + + void notifyMessage(String title, String body) { + const androidSettings = AndroidNotificationDetails( + 'general', + 'General', + importance: Importance.high, + priority: Priority.high, + silent: true, + ); + const darwinSettings = DarwinNotificationDetails( + presentAlert: true, + presentBanner: true, + presentBadge: true, + presentSound: false, + ); + const linuxSettings = LinuxNotificationDetails(); + + localNotify.show( + math.max(1, math.Random().nextInt(100000000)), + title, + body, + const NotificationDetails( + android: androidSettings, + iOS: darwinSettings, + macOS: darwinSettings, + linux: linuxSettings, + ), + ); + } + + Future notifyPrefetch() async { + final AuthProvider auth = Get.find(); + if (!await auth.isAuthorized) return; + + final client = GetConnect(); + client.httpClient.baseUrl = ServiceFinder.services['passport']; + client.httpClient.addAuthenticator(auth.requestAuthenticator); + + final resp = await client.get('/api/notifications?skip=0&take=100'); + if (resp.statusCode == 200) { + final result = PaginationResult.fromJson(resp.body); + final data = result.data?.map((x) => Notification.fromJson(x)).toList(); + if (data != null) { + notifications.addAll(data); + notificationUnread.value = data.length; + } + } + } +} diff --git a/lib/providers/auth.dart b/lib/providers/auth.dart index 5b86920..ec93e36 100644 --- a/lib/providers/auth.dart +++ b/lib/providers/auth.dart @@ -5,6 +5,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:get/get.dart'; import 'package:get/get_connect/http/src/request/request.dart'; +import 'package:solian/providers/account.dart'; import 'package:solian/services.dart'; import 'package:oauth2/oauth2.dart' as oauth2; @@ -25,26 +26,30 @@ class AuthProvider extends GetConnect { oauth2.Credentials? credentials; + Future refreshCredentials() async { + final resp = await post('/api/auth/token', { + 'refresh_token': credentials!.refreshToken, + 'grant_type': 'refresh_token', + }); + if (resp.statusCode != 200) { + throw Exception(resp.bodyString); + } + credentials = oauth2.Credentials( + resp.body['access_token'], + refreshToken: resp.body['refresh_token'], + idToken: resp.body['access_token'], + tokenEndpoint: tokenEndpoint, + expiration: DateTime.now().add(const Duration(minutes: 3)), + ); + storage.write( + key: 'auth_credentials', + value: jsonEncode(credentials!.toJson()), + ); + } + Future> requestAuthenticator(Request request) async { if (credentials != null && credentials!.isExpired) { - final resp = await post('/api/auth/token', { - 'refresh_token': credentials!.refreshToken, - 'grant_type': 'refresh_token', - }); - if (resp.statusCode != 200) { - throw Exception(resp.bodyString); - } - credentials = oauth2.Credentials( - resp.body['access_token'], - refreshToken: resp.body['refresh_token'], - idToken: resp.body['access_token'], - tokenEndpoint: tokenEndpoint, - expiration: DateTime.now().add(const Duration(minutes: 3)), - ); - storage.write( - key: 'auth_credentials', - value: jsonEncode(credentials!.toJson()), - ); + refreshCredentials(); } if (credentials != null) { @@ -91,12 +96,16 @@ class AuthProvider extends GetConnect { value: jsonEncode(credentials!.toJson()), ); + Get.find().connect(); + return credentials!; } void signout() { _cacheUserProfileResponse = null; + Get.find().disconnect(); + storage.deleteAll(); } diff --git a/lib/screens/account/notification.dart b/lib/screens/account/notification.dart new file mode 100644 index 0000000..eb8bda0 --- /dev/null +++ b/lib/screens/account/notification.dart @@ -0,0 +1,210 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_animate/flutter_animate.dart'; +import 'package:get/get.dart'; +import 'package:solian/providers/account.dart'; +import 'package:solian/providers/auth.dart'; +import 'package:solian/services.dart'; +import 'package:solian/models/notification.dart' as notify; +import 'package:url_launcher/url_launcher_string.dart'; +import 'package:uuid/uuid.dart'; + +class NotificationScreen extends StatefulWidget { + const NotificationScreen({super.key}); + + @override + State createState() => _NotificationScreenState(); +} + +class _NotificationScreenState extends State { + bool _isBusy = false; + + Future markAllRead() async { + final AuthProvider auth = Get.find(); + if (!await auth.isAuthorized) return; + + setState(() => _isBusy = true); + + final AccountProvider provider = Get.find(); + + List markList = List.empty(growable: true); + for (final element in provider.notifications) { + if (element.isRealtime) continue; + markList.add(element.id); + } + + if (markList.isNotEmpty) { + final client = GetConnect(); + client.httpClient.baseUrl = ServiceFinder.services['passport']; + client.httpClient.addAuthenticator(auth.requestAuthenticator); + + await client.put('/api/notifications/batch/read', {'messages': markList}); + } + + provider.notifications.clear(); + + setState(() => _isBusy = false); + } + + Future markOneRead(notify.Notification element, int index) async { + final AuthProvider auth = Get.find(); + if (!await auth.isAuthorized) return; + + final AccountProvider provider = Get.find(); + + if (element.isRealtime) { + provider.notifications.removeAt(index); + return; + } + + setState(() => _isBusy = true); + + final client = GetConnect(); + client.httpClient.baseUrl = ServiceFinder.services['passport']; + client.httpClient.addAuthenticator(auth.requestAuthenticator); + + await client.put('/api/notifications/${element.id}/read', {}); + + provider.notifications.removeAt(index); + + setState(() => _isBusy = false); + } + + @override + Widget build(BuildContext context) { + final AccountProvider provider = Get.find(); + + return SizedBox( + height: MediaQuery.of(context).size.height * 0.85, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'notification'.tr, + style: Theme.of(context).textTheme.headlineSmall, + ).paddingOnly(left: 24, right: 24, top: 32, bottom: 16), + Expanded( + child: Obx(() { + return CustomScrollView( + slivers: [ + if (_isBusy) + SliverToBoxAdapter( + child: const LinearProgressIndicator().animate().scaleX(), + ), + if (provider.notifications.isEmpty) + SliverToBoxAdapter( + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 10), + color: + Theme.of(context).colorScheme.surfaceContainerHigh, + child: ListTile( + leading: const Icon(Icons.check), + title: Text('notifyEmpty'.tr), + subtitle: Text('notifyEmptyCaption'.tr), + ), + ), + ), + if (provider.notifications.isNotEmpty) + SliverToBoxAdapter( + child: ListTile( + tileColor: + Theme.of(context).colorScheme.secondaryContainer, + leading: const Icon(Icons.checklist), + title: Text('notifyAllRead'.tr), + contentPadding: + const EdgeInsets.symmetric(horizontal: 28), + onTap: _isBusy ? null : () => markAllRead(), + ), + ), + SliverList.separated( + itemCount: provider.notifications.length, + itemBuilder: (BuildContext context, int index) { + var element = provider.notifications[index]; + return Dismissible( + key: Key(const Uuid().v4()), + background: Container( + color: Colors.lightBlue, + padding: const EdgeInsets.symmetric(horizontal: 20), + alignment: Alignment.centerLeft, + child: const Icon(Icons.check, color: Colors.white), + ), + child: ListTile( + contentPadding: const EdgeInsets.symmetric( + horizontal: 24, + vertical: 8, + ), + title: Text(element.subject), + subtitle: Column( + children: [ + Text(element.content), + if (element.links != null) + Row( + children: element.links! + .map((e) => InkWell( + child: Text( + e.label, + style: TextStyle( + color: Theme.of(context) + .colorScheme + .onSecondaryContainer, + decoration: + TextDecoration.underline, + ), + ), + onTap: () { + launchUrlString(e.url); + }, + ).paddingOnly(right: 5)) + .toList(), + ), + ], + ), + ), + onDismissed: (_) => markOneRead(element, index), + ); + }, + separatorBuilder: (_, __) => + const Divider(thickness: 0.3, height: 0.3), + ), + ], + ); + }), + ), + ], + ), + ); + } +} + +class NotificationButton extends StatelessWidget { + const NotificationButton({super.key}); + + @override + Widget build(BuildContext context) { + final AccountProvider provider = Get.find(); + + final button = IconButton( + icon: const Icon(Icons.notifications), + onPressed: () { + showModalBottomSheet( + useRootNavigator: true, + isScrollControlled: true, + context: context, + builder: (context) => const NotificationScreen(), + ).then((_) => provider.notificationUnread.value = 0); + }, + ); + + return Obx(() { + if (provider.notificationUnread.value > 0) { + return Badge( + isLabelVisible: true, + offset: const Offset(-8, 2), + label: Text(provider.notificationUnread.value.toString()), + child: button, + ); + } else { + return button; + } + }); + } +} diff --git a/lib/screens/social.dart b/lib/screens/social.dart index f87e036..f1b4597 100644 --- a/lib/screens/social.dart +++ b/lib/screens/social.dart @@ -6,6 +6,8 @@ import 'package:solian/models/post.dart'; import 'package:solian/providers/auth.dart'; import 'package:solian/providers/content/post_explore.dart'; import 'package:solian/router.dart'; +import 'package:solian/screens/account/notification.dart'; +import 'package:solian/theme.dart'; import 'package:solian/widgets/posts/post_action.dart'; import 'package:solian/widgets/posts/post_item.dart'; @@ -69,32 +71,61 @@ class _SocialScreenState extends State { }), body: Material( color: Theme.of(context).colorScheme.surface, - child: RefreshIndicator( - onRefresh: () => Future.sync(() => _pagingController.refresh()), - child: PagedListView.separated( - pagingController: _pagingController, - builderDelegate: PagedChildBuilderDelegate( - itemBuilder: (context, item, index) { - return GestureDetector( - child: PostItem(key: Key('p${item.alias}'), item: item) - .paddingSymmetric( - vertical: (item.attachments?.isEmpty ?? false) ? 8 : 0, + child: SafeArea( + child: NestedScrollView( + headerSliverBuilder: (context, innerBoxIsScrolled) { + return [ + SliverOverlapAbsorber( + handle: + NestedScrollView.sliverOverlapAbsorberHandleFor(context), + sliver: SliverAppBar( + title: Text('social'.tr), + centerTitle: false, + titleSpacing: + SolianTheme.isLargeScreen(context) ? null : 24, + forceElevated: innerBoxIsScrolled, + actions: const [ + NotificationButton(), + ], ), - onTap: () {}, - onLongPress: () { - showModalBottomSheet( - useRootNavigator: true, - context: context, - builder: (context) => PostAction(item: item), - ).then((value) { - if (value == true) _pagingController.refresh(); - }); - }, - ); - }, + ), + ]; + }, + body: MediaQuery.removePadding( + removeTop: true, + context: context, + child: RefreshIndicator( + onRefresh: () => Future.sync(() => _pagingController.refresh()), + child: PagedListView.separated( + pagingController: _pagingController, + builderDelegate: PagedChildBuilderDelegate( + itemBuilder: (context, item, index) { + return GestureDetector( + child: PostItem( + key: Key('p${item.alias}'), + item: item, + ).paddingSymmetric( + vertical: + (item.attachments?.isEmpty ?? false) ? 8 : 0, + ), + onTap: () {}, + onLongPress: () { + showModalBottomSheet( + useRootNavigator: true, + context: context, + builder: (context) => PostAction(item: item), + ).then((value) { + if (value == true) _pagingController.refresh(); + }); + }, + ); + }, + ), + separatorBuilder: (_, __) => + const Divider(thickness: 0.3, height: 0.3), + ), + ), ), - separatorBuilder: (_, __) => - const Divider(thickness: 0.3, height: 0.3), ), ), ), diff --git a/lib/translations.dart b/lib/translations.dart index 02dc9f7..3287c8d 100644 --- a/lib/translations.dart +++ b/lib/translations.dart @@ -18,6 +18,7 @@ class SolianMessages extends Translations { 'delete': 'Delete', 'reply': 'Reply', 'repost': 'Repost', + 'notification': 'Notification', 'errorHappened': 'An error occurred', 'email': 'Email', 'username': 'Username', @@ -49,7 +50,7 @@ class SolianMessages extends Translations { 'signinRiskDetected': 'Risk detected, click Next to open a webpage and signin through it to pass security check.', 'signup': 'Sign up', - 'signupGreeting': 'Welcome onboard 👋', + 'signupGreeting': 'Welcome onboard', 'signupCaption': 'Create an account on Solarpass and then get the access of entire Solar Network!', 'signout': 'Sign out', @@ -57,6 +58,9 @@ class SolianMessages extends Translations { 'matureContent': 'Mature Content', 'matureContentCaption': 'The content is rated and may not suitable for everyone to view', + 'notifyAllRead': 'Mark all as read', + 'notifyEmpty': 'All notifications read', + 'notifyEmptyCaption': 'It seems like nothing happened recently', 'postAction': 'Post', 'postPublishing': 'Post a post', 'postIdentityNotify': 'You will post this post as', @@ -98,6 +102,7 @@ class SolianMessages extends Translations { 'apply': '应用', 'reply': '回复', 'repost': '转帖', + 'notification': '通知', 'errorHappened': '发生错误了', 'email': '邮件地址', 'username': '用户名', @@ -131,6 +136,9 @@ class SolianMessages extends Translations { 'riskDetection': '检测到风险', 'matureContent': '评级内容', 'matureContentCaption': '该内容已被评级为家长指导级或以上,这可能说明内容包含一系列不友好的成分', + 'notifyAllRead': '已读所有通知', + 'notifyEmpty': '通知箱为空', + 'notifyEmptyCaption': '看起来最近没发生什么呢', 'postAction': '发表', 'postPublishing': '发表帖子', 'postIdentityNotify': '你将会以本身份发表帖子', diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index 3e0199c..ed5dfb7 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -6,12 +6,14 @@ import FlutterMacOS import Foundation import file_selector_macos +import flutter_local_notifications import flutter_secure_storage_macos import path_provider_foundation import url_launcher_macos func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin")) + FlutterLocalNotificationsPlugin.register(with: registry.registrar(forPlugin: "FlutterLocalNotificationsPlugin")) FlutterSecureStoragePlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStoragePlugin")) PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) diff --git a/pubspec.lock b/pubspec.lock index 2cdec55..a196265 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -89,6 +89,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.8" + dbus: + dependency: transitive + description: + name: dbus + sha256: "365c771ac3b0e58845f39ec6deebc76e3276aa9922b0cc60840712094d9047ac" + url: "https://pub.dev" + source: hosted + version: "0.7.10" fake_async: dependency: transitive description: @@ -145,6 +153,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.9.3+1" + fixnum: + dependency: transitive + description: + name: fixnum + sha256: "25517a4deb0c03aa0f32fd12db525856438902d9c16536311e76cdc57b31d7d1" + url: "https://pub.dev" + source: hosted + version: "1.1.0" flutter: dependency: "direct main" description: flutter @@ -166,6 +182,30 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.2" + flutter_local_notifications: + dependency: "direct main" + description: + name: flutter_local_notifications + sha256: "40e6fbd2da7dcc7ed78432c5cdab1559674b4af035fddbfb2f9a8f9c2112fcef" + url: "https://pub.dev" + source: hosted + version: "17.1.2" + flutter_local_notifications_linux: + dependency: transitive + description: + name: flutter_local_notifications_linux + sha256: "33f741ef47b5f63cc7f78fe75eeeac7e19f171ff3c3df054d84c1e38bedb6a03" + url: "https://pub.dev" + source: hosted + version: "4.0.0+1" + flutter_local_notifications_platform_interface: + dependency: transitive + description: + name: flutter_local_notifications_platform_interface + sha256: "340abf67df238f7f0ef58f4a26d2a83e1ab74c77ab03cd2b2d5018ac64db30b7" + url: "https://pub.dev" + source: hosted + version: "7.1.0" flutter_markdown: dependency: "direct main" description: @@ -186,10 +226,10 @@ packages: dependency: "direct main" description: name: flutter_secure_storage - sha256: "8496a89eea74e23f92581885f876455d9d460e71201405dffe5f55dfe1155864" + sha256: "165164745e6afb5c0e3e3fcc72a012fb9e58496fb26ffb92cf22e16a821e85d0" url: "https://pub.dev" source: hosted - version: "9.2.1" + version: "9.2.2" flutter_secure_storage_linux: dependency: transitive description: @@ -202,10 +242,10 @@ packages: dependency: transitive description: name: flutter_secure_storage_macos - sha256: b768a7dab26d6186b68e2831b3104f8968154f0f4fdbf66e7c2dd7bdf299daaf + sha256: "1693ab11121a5f925bbea0be725abfcfbbcf36c1e29e571f84a0c0f436147a81" url: "https://pub.dev" source: hosted - version: "3.1.1" + version: "3.1.2" flutter_secure_storage_platform_interface: dependency: transitive description: @@ -276,10 +316,10 @@ packages: dependency: "direct main" description: name: go_router - sha256: "7685acd06244ba4be60f455c5cafe5790c63dc91fc03f7385b1e922a6b85b17c" + sha256: "6ad5662b014c06c20fa46ab78715c96b2222a7fe4f87bf77e0289592c2539e86" url: "https://pub.dev" source: hosted - version: "14.1.1" + version: "14.1.3" http: dependency: transitive description: @@ -300,10 +340,10 @@ packages: dependency: "direct main" description: name: image - sha256: "4c68bfd5ae83e700b5204c1e74451e7bf3cf750e6843c6e158289cf56bda018e" + sha256: "2237616a36c0d69aef7549ab439b833fb7f9fb9fc861af2cc9ac3eedddd69ca8" url: "https://pub.dev" source: hosted - version: "4.1.7" + version: "4.2.0" image_picker: dependency: "direct main" description: @@ -536,6 +576,54 @@ packages: url: "https://pub.dev" source: hosted version: "2.2.1" + permission_handler: + dependency: "direct main" + description: + name: permission_handler + sha256: "18bf33f7fefbd812f37e72091a15575e72d5318854877e0e4035a24ac1113ecb" + url: "https://pub.dev" + source: hosted + version: "11.3.1" + permission_handler_android: + dependency: transitive + description: + name: permission_handler_android + sha256: "8bb852cd759488893805c3161d0b2b5db55db52f773dbb014420b304055ba2c5" + url: "https://pub.dev" + source: hosted + version: "12.0.6" + permission_handler_apple: + dependency: transitive + description: + name: permission_handler_apple + sha256: e9ad66020b89ff1b63908f247c2c6f931c6e62699b756ef8b3c4569350cd8662 + url: "https://pub.dev" + source: hosted + version: "9.4.4" + permission_handler_html: + dependency: transitive + description: + name: permission_handler_html + sha256: "54bf176b90f6eddd4ece307e2c06cf977fb3973719c35a93b85cc7093eb6070d" + url: "https://pub.dev" + source: hosted + version: "0.1.1" + permission_handler_platform_interface: + dependency: transitive + description: + name: permission_handler_platform_interface + sha256: "48d4fcf201a1dad93ee869ab0d4101d084f49136ec82a8a06ed9cfeacab9fd20" + url: "https://pub.dev" + source: hosted + version: "4.2.1" + permission_handler_windows: + dependency: transitive + description: + name: permission_handler_windows + sha256: "1a790728016f79a41216d88672dbc5df30e686e811ad4e698bfc51f76ad91f1e" + url: "https://pub.dev" + source: hosted + version: "0.2.1" petitparser: dependency: transitive description: @@ -581,6 +669,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.10.0" + sprintf: + dependency: transitive + description: + name: sprintf + sha256: "1fc9ffe69d4df602376b52949af107d8f5703b77cda567c4d7d86a0693120f23" + url: "https://pub.dev" + source: hosted + version: "7.0.0" stack_trace: dependency: transitive description: @@ -629,6 +725,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.6.1" + timezone: + dependency: transitive + description: + name: timezone + sha256: a6ccda4a69a442098b602c44e61a1e2b4bf6f5516e875bbf0f427d5df14745d5 + url: "https://pub.dev" + source: hosted + version: "0.9.3" typed_data: dependency: transitive description: @@ -649,10 +753,10 @@ packages: dependency: transitive description: name: url_launcher_android - sha256: "360a6ed2027f18b73c8d98e159dda67a61b7f2e0f6ec26e86c3ada33b0621775" + sha256: "17cd5e205ea615e2c6ea7a77323a11712dffa0720a8a90540db57a01347f9ad9" url: "https://pub.dev" source: hosted - version: "6.3.1" + version: "6.3.2" url_launcher_ios: dependency: transitive description: @@ -701,6 +805,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.1.1" + uuid: + dependency: "direct main" + description: + name: uuid + sha256: "814e9e88f21a176ae1359149021870e87f7cddaf633ab678a5d2b0bff7fd1ba8" + url: "https://pub.dev" + source: hosted + version: "4.4.0" vector_math: dependency: transitive description: @@ -725,14 +837,30 @@ packages: url: "https://pub.dev" source: hosted version: "0.5.1" + web_socket: + dependency: transitive + description: + name: web_socket + sha256: "217f49b5213796cb508d6a942a5dc604ce1cb6a0a6b3d8cb3f0c314f0ecea712" + url: "https://pub.dev" + source: hosted + version: "0.1.4" + web_socket_channel: + dependency: "direct main" + description: + name: web_socket_channel + sha256: a2d56211ee4d35d9b344d9d4ce60f362e4f5d1aafb988302906bd732bc731276 + url: "https://pub.dev" + source: hosted + version: "3.0.0" win32: dependency: transitive description: name: win32 - sha256: "0eaf06e3446824099858367950a813472af675116bf63f008a4c2a75ae13e9cb" + sha256: a79dbe579cb51ecd6d30b17e0cae4e0ea15e2c0e66f69ad4198f22a6789e94f4 url: "https://pub.dev" source: hosted - version: "5.5.0" + version: "5.5.1" xdg_directories: dependency: transitive description: @@ -750,5 +878,5 @@ packages: source: hosted version: "6.5.0" sdks: - dart: ">=3.3.4 <4.0.0" + dart: ">=3.4.0 <4.0.0" flutter: ">=3.19.0" diff --git a/pubspec.yaml b/pubspec.yaml index 46258a5..5fb3b59 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -52,6 +52,10 @@ dependencies: intl: ^0.19.0 image: ^4.1.7 font_awesome_flutter: ^10.7.0 + web_socket_channel: ^3.0.0 + flutter_local_notifications: ^17.1.2 + permission_handler: ^11.3.1 + uuid: ^4.4.0 dev_dependencies: flutter_test: diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index 602b168..5206490 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -8,6 +8,7 @@ #include #include +#include #include void RegisterPlugins(flutter::PluginRegistry* registry) { @@ -15,6 +16,8 @@ void RegisterPlugins(flutter::PluginRegistry* registry) { registry->GetRegistrarForPlugin("FileSelectorWindows")); FlutterSecureStorageWindowsPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("FlutterSecureStorageWindowsPlugin")); + PermissionHandlerWindowsPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("PermissionHandlerWindowsPlugin")); UrlLauncherWindowsRegisterWithRegistrar( registry->GetRegistrarForPlugin("UrlLauncherWindows")); } diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index b918cf8..53447da 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -5,6 +5,7 @@ list(APPEND FLUTTER_PLUGIN_LIST file_selector_windows flutter_secure_storage_windows + permission_handler_windows url_launcher_windows )