Notifications

This commit is contained in:
LittleSheep 2024-05-25 13:00:40 +08:00
parent 806ae602d5
commit f376603482
13 changed files with 652 additions and 56 deletions

View File

@ -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

View File

@ -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;

View File

@ -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<AccountProvider>().connect();
}
});
},
builder: (context, child) {
return ScaffoldMessenger(

161
lib/providers/account.dart Normal file
View File

@ -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<Notification> notifications =
List<Notification>.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<void> 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;
}
}
}
}

View File

@ -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<void> 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<Request<T?>> requestAuthenticator<T>(Request<T?> 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<AccountProvider>().connect();
return credentials!;
}
void signout() {
_cacheUserProfileResponse = null;
Get.find<AccountProvider>().disconnect();
storage.deleteAll();
}

View File

@ -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<NotificationScreen> createState() => _NotificationScreenState();
}
class _NotificationScreenState extends State<NotificationScreen> {
bool _isBusy = false;
Future<void> markAllRead() async {
final AuthProvider auth = Get.find();
if (!await auth.isAuthorized) return;
setState(() => _isBusy = true);
final AccountProvider provider = Get.find();
List<int> 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<void> 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;
}
});
}
}

View File

@ -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<SocialScreen> {
}),
body: Material(
color: Theme.of(context).colorScheme.surface,
child: RefreshIndicator(
onRefresh: () => Future.sync(() => _pagingController.refresh()),
child: PagedListView<int, Post>.separated(
pagingController: _pagingController,
builderDelegate: PagedChildBuilderDelegate<Post>(
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<int, Post>.separated(
pagingController: _pagingController,
builderDelegate: PagedChildBuilderDelegate<Post>(
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),
),
),
),

View File

@ -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': '你将会以本身份发表帖子',

View File

@ -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"))

View File

@ -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"

View File

@ -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:

View File

@ -8,6 +8,7 @@
#include <file_selector_windows/file_selector_windows.h>
#include <flutter_secure_storage_windows/flutter_secure_storage_windows_plugin.h>
#include <permission_handler_windows/permission_handler_windows_plugin.h>
#include <url_launcher_windows/url_launcher_windows.h>
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"));
}

View File

@ -5,6 +5,7 @@
list(APPEND FLUTTER_PLUGIN_LIST
file_selector_windows
flutter_secure_storage_windows
permission_handler_windows
url_launcher_windows
)