encrypt notification api and hive api

main
htylight 2023-09-10 11:30:20 +08:00
parent a260e72e04
commit eda27359af
16 changed files with 518 additions and 689 deletions

View File

@ -8,57 +8,130 @@ import 'package:together_mobile/models/init_get_it.dart';
import 'package:together_mobile/models/user_model.dart';
import 'package:together_mobile/utils/app_dir.dart';
void initDatabase() async {
List<int> encryptionKeyUint8List = await getEncryptKey();
class HiveDatabase {
static bool _isInitialised = false;
await Hive.initFlutter(await getBoxDir());
static Future<void> init() async {
if (_isInitialised) {
return;
}
await Hive.close();
Box<ChatSetting> chatSettingBox =
await Hive.openBox<ChatSetting>('chat_setting');
List<int> encryptionKeyUint8List = await _getEncryptKey();
final openedChats = chatSettingBox.values.where((element) => element.isOpen);
await Hive.initFlutter(await getBoxDir());
for (var chatBox in openedChats) {
Hive.openBox<MessageT>(
'message_${chatBox.contactId}',
Box<ChatSetting> chatSettingBox =
await Hive.openBox<ChatSetting>('chat_setting');
final openedChats =
chatSettingBox.values.where((element) => element.isOpen);
for (var chatBox in openedChats) {
Hive.openBox<MessageT>(
'message_${chatBox.contactId}',
encryptionCipher: HiveAesCipher(encryptionKeyUint8List),
compactionStrategy: (entries, deletedEntries) => entries > 200,
);
}
_isInitialised = true;
}
static void registerAdapter() {
Hive.registerAdapter(ChatSettingAdapter());
Hive.registerAdapter(MessageTAdapter());
}
static Future<void> openNewMessageBox(String contactId, int type) async {
final encryptionKeyUint8List = await _getEncryptKey();
var chatSettingBox = Hive.box<ChatSetting>('chat_setting');
chatSettingBox.add(
ChatSetting(contactId, type, false, false, false, DateTime.now(), 0));
await Hive.openBox<MessageT>(
'message_$contactId',
encryptionCipher: HiveAesCipher(encryptionKeyUint8List),
compactionStrategy: (entries, deletedEntries) => entries > 200,
);
}
}
void registerAdapter() {
Hive.registerAdapter(ChatSettingAdapter());
Hive.registerAdapter(MessageTAdapter());
}
static Future<List<int>> _getEncryptKey() async {
final id = getIt.get<UserAccount>().id;
const secureStorage = FlutterSecureStorage();
final encryptionKeyString = await secureStorage.read(key: 'encryptKey:$id');
if (encryptionKeyString == null) {
final key = Hive.generateSecureKey();
await secureStorage.write(
key: 'encryptKey:$id',
value: base64Encode(key),
);
}
String? key = await secureStorage.read(key: 'encryptKey:$id');
final encryptionKeyUint8List = base64Url.decode(key!);
Future<List<int>> getEncryptKey() async {
final id = getIt.get<UserAccount>().id;
const secureStorage = FlutterSecureStorage();
final encryptionKeyString = await secureStorage.read(key: 'encryptKey:$id');
if (encryptionKeyString == null) {
final key = Hive.generateSecureKey();
await secureStorage.write(
key: 'encryptKey:$id',
value: base64Encode(key),
);
return encryptionKeyUint8List;
}
// String? key = await secureStorage.read(key: 'encryptKey:$id');
final encryptionKeyUint8List = base64Url.decode(encryptionKeyString!);
return encryptionKeyUint8List;
static void close() {
_isInitialised = false;
}
}
Future<void> openNewMessageBox(String contactId, int type) async {
final encryptionKeyUint8List = await getEncryptKey();
// Future<void> initDatabase() async {
// await Hive.close();
var chatSettingBox = Hive.box<ChatSetting>('chat_setting');
chatSettingBox.add(
ChatSetting(contactId, type, false, false, false, DateTime.now(), 0));
// List<int> encryptionKeyUint8List = await getEncryptKey();
await Hive.openBox<MessageT>(
'message_$contactId',
encryptionCipher: HiveAesCipher(encryptionKeyUint8List),
compactionStrategy: (entries, deletedEntries) => entries > 200,
);
}
// await Hive.initFlutter(await getBoxDir());
// Box<ChatSetting> chatSettingBox =
// await Hive.openBox<ChatSetting>('chat_setting');
// final openedChats = chatSettingBox.values.where((element) => element.isOpen);
// for (var chatBox in openedChats) {
// Hive.openBox<MessageT>(
// 'message_${chatBox.contactId}',
// encryptionCipher: HiveAesCipher(encryptionKeyUint8List),
// compactionStrategy: (entries, deletedEntries) => entries > 200,
// );
// }
// }
// void registerAdapter() {
// Hive.registerAdapter(ChatSettingAdapter());
// Hive.registerAdapter(MessageTAdapter());
// }
// Future<List<int>> getEncryptKey() async {
// final id = getIt.get<UserAccount>().id;
// const secureStorage = FlutterSecureStorage();
// final encryptionKeyString = await secureStorage.read(key: 'encryptKey:$id');
// if (encryptionKeyString == null) {
// final key = Hive.generateSecureKey();
// await secureStorage.write(
// key: 'encryptKey:$id',
// value: base64Encode(key),
// );
// }
// String? key = await secureStorage.read(key: 'encryptKey:$id');
// final encryptionKeyUint8List = base64Url.decode(key!);
// return encryptionKeyUint8List;
// }
// Future<void> openNewMessageBox(String contactId, int type) async {
// final encryptionKeyUint8List = await getEncryptKey();
// var chatSettingBox = Hive.box<ChatSetting>('chat_setting');
// chatSettingBox.add(
// ChatSetting(contactId, type, false, false, false, DateTime.now(), 0));
// await Hive.openBox<MessageT>(
// 'message_$contactId',
// encryptionCipher: HiveAesCipher(encryptionKeyUint8List),
// compactionStrategy: (entries, deletedEntries) => entries > 200,
// );
// }

View File

@ -6,11 +6,10 @@ import 'package:together_mobile/router/router.dart';
import 'package:together_mobile/models/init_get_it.dart';
import 'notification_api.dart';
void main() async {
initGetIt();
await initNotification();
registerAdapter();
await NotificationAPI.init();
HiveDatabase.registerAdapter();
runApp(const Together());
}
@ -29,7 +28,11 @@ class Together extends StatelessWidget {
// screen excluding the input widgets.
builder: (context, child) {
return GestureDetector(
onTap: () => FocusManager.instance.primaryFocus?.unfocus(),
onTap: () {
if (FocusManager.instance.primaryFocus != null) {
FocusManager.instance.primaryFocus!.unfocus();
}
},
child: child,
);
},

View File

@ -37,12 +37,16 @@ class Contact extends ChangeNotifier {
friendCount += 1;
friends[key] = FriendSetting.fromJson(value);
});
json['groupChats'].forEach((key, value) {
groupChats[key] = GroupChatSetting.fromJson(value);
groupChatCount += 1;
});
// json['groupChats'].forEach((key, value) {
// groupChats[key] = GroupChatSetting.fromJson(value);
// groupChatCount += 1;
// });
friendGroups = List.from(json['friendGroups']);
}

View File

@ -3,14 +3,11 @@ import 'dart:convert';
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:hive_flutter/hive_flutter.dart';
import 'package:together_mobile/models/route_state_model.dart';
import 'package:together_mobile/request/user_profile.dart';
import 'package:together_mobile/utils/app_dir.dart';
import 'package:web_socket_channel/web_socket_channel.dart';
import 'package:web_socket_channel/status.dart' as status;
import 'package:together_mobile/models/route_state_model.dart';
import 'package:together_mobile/database/box_type.dart';
import 'package:together_mobile/models/apply_list_model.dart';
import 'package:together_mobile/models/contact_model.dart';
@ -24,8 +21,6 @@ enum SocketStatus {
error,
}
Map<String, (int, List<Message>)> messages = {};
class WebSocketManager extends ChangeNotifier {
late Uri wsUrl;
late WebSocketChannel channel;
@ -46,7 +41,7 @@ class WebSocketManager extends ChangeNotifier {
print('websocket connected <$channel>');
heartBeatInspect();
if (reconnectTimer != null) {
reconnectTimer!.cancel();
// reconnectTimer!.cancel();
reconnectTimer = null;
reconnectTimes = 0;
}
@ -219,7 +214,7 @@ void receiveFriendMsg(Map<String, dynamic> msg) async {
String avatar = getIt.get<ContactAccountProfile>().friends[senderId]!.avatar;
if (!getIt.get<RouteState>().isVisible) {
_showMessageNotifications(
NotificationAPI.showMessageNotifications(
senderId: senderId,
name: name,
avatar: avatar,
@ -227,7 +222,7 @@ void receiveFriendMsg(Map<String, dynamic> msg) async {
);
} else if (routeName == 'Message') {
if (!getIt.get<RouteState>().query.containsValue(senderId)) {
_showMessageNotifications(
NotificationAPI.showMessageNotifications(
senderId: senderId,
name: name,
avatar: avatar,
@ -235,7 +230,7 @@ void receiveFriendMsg(Map<String, dynamic> msg) async {
);
}
} else if (routeName != 'Chat') {
_showMessageNotifications(
NotificationAPI.showMessageNotifications(
senderId: senderId,
name: name,
avatar: avatar,
@ -381,7 +376,7 @@ void receiveGroupChatMsg(Map<String, dynamic> msg) async {
String routeName = getIt.get<RouteState>().currentPathName;
if (!getIt.get<RouteState>().isVisible) {
_showMessageNotifications(
NotificationAPI.showMessageNotifications(
senderId: senderId,
name: name,
avatar: avatar,
@ -390,7 +385,7 @@ void receiveGroupChatMsg(Map<String, dynamic> msg) async {
);
} else if (routeName == 'Message') {
if (!getIt.get<RouteState>().query.containsValue(senderId)) {
_showMessageNotifications(
NotificationAPI.showMessageNotifications(
senderId: senderId,
name: name,
avatar: avatar,
@ -399,7 +394,7 @@ void receiveGroupChatMsg(Map<String, dynamic> msg) async {
);
}
} else if (routeName != 'Chat') {
_showMessageNotifications(
NotificationAPI.showMessageNotifications(
senderId: senderId,
name: name,
avatar: avatar,
@ -408,122 +403,3 @@ void receiveGroupChatMsg(Map<String, dynamic> msg) async {
);
}
}
Future<void> _showMessageNotifications({
required String senderId,
required String name,
required String avatar,
required String text,
String? groupChatId,
}) async {
String groupChannelId = 'TOGETHER_${getIt.get<UserAccount>().id}';
const String groupChannelName = 'together messages';
String avatarPath = '';
if (avatar.isNotEmpty) {
if (groupChatId == null) {
avatarPath = await getAvatarPath('user', avatar);
if (!File(avatarPath).existsSync()) {
File file = await File(avatarPath).create(recursive: true);
final bytes = await downloadUserAvatar(avatar);
await file.writeAsBytes(bytes);
}
} else {
// TODO: download group chat avatar. This will done after changing group chat avatar is implemented
}
}
late Person person;
if (avatarPath.isNotEmpty) {
person = Person(
name: name,
key: senderId,
bot: false,
icon: BitmapFilePathAndroidIcon(avatarPath),
);
} else {
person = Person(
name: name,
key: senderId,
bot: false,
);
}
if (groupChatId == null) {
if (messages.containsKey(senderId)) {
messages[senderId]!.$2.add(Message(text, DateTime.now(), person));
} else {
messages[senderId] = (++id, [Message(text, DateTime.now(), person)]);
}
} else {
if (messages.containsKey(groupChatId)) {
messages[groupChatId]!.$2.add(Message(text, DateTime.now(), person));
} else {
messages[groupChatId] = (++id, [Message(text, DateTime.now(), person)]);
}
}
late final MessagingStyleInformation messagingStyle;
if (groupChatId == null) {
messagingStyle = MessagingStyleInformation(
person,
messages: messages[senderId]!.$2,
conversationTitle: name,
);
} else {
String groupChatName =
getIt.get<Contact>().groupChats[groupChatId]!.nameRemark.isEmpty
? getIt.get<ContactAccountProfile>().groupChats[groupChatId]!.name
: getIt.get<Contact>().groupChats[groupChatId]!.nameRemark;
messagingStyle = MessagingStyleInformation(
person,
messages: messages[groupChatId]!.$2,
conversationTitle: groupChatName,
groupConversation: true,
);
}
AndroidNotificationDetails androidNotificationDetails =
AndroidNotificationDetails(
groupChannelId,
groupChannelName,
importance: Importance.max,
priority: Priority.high,
category: AndroidNotificationCategory.message,
styleInformation: messagingStyle,
);
NotificationDetails notificationDetails = NotificationDetails(
android: androidNotificationDetails,
);
late String payload;
if (groupChatId == null) {
payload = json.encode({
'event': 'friend-message',
'friendId': senderId,
});
await flutterLocalNotificationsPlugin.show(
messages[senderId]!.$1,
name,
text,
notificationDetails,
payload: payload,
);
} else {
payload = json.encode({
'event': 'group-chat-message',
'groupChatId': groupChatId,
});
await flutterLocalNotificationsPlugin.show(
messages[groupChatId]!.$1,
name,
text,
notificationDetails,
payload: payload,
);
}
}

View File

@ -1,228 +0,0 @@
import 'dart:async';
import 'dart:io';
import 'package:flutter/cupertino.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:flutter_timezone/flutter_timezone.dart';
import 'package:timezone/data/latest_all.dart' as tz;
import 'package:timezone/timezone.dart' as tz;
class NotificationAPI {
static int id = 0;
static final FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin =
FlutterLocalNotificationsPlugin();
static final StreamController<ReceivedNotification?>
didReceiveNotificationStream =
StreamController<ReceivedNotification>.broadcast();
final StreamController<String?> selectNotificationStream =
StreamController<String?>.broadcast();
static Future<void> init() async {}
}
/// Before release build of app, read [Release build configuration]
/// https://pub-web.flutter-io.cn/packages/flutter_local_notifications
int id = 0;
/// Init notification plugin
final FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin =
FlutterLocalNotificationsPlugin();
/// Streams are created so that app can respond to notification-related events
/// since the plugin is initialised in the `main` function
/// Only be used in IOS?
final StreamController<ReceivedNotification?> didReceiveNotificationStream =
StreamController<ReceivedNotification>.broadcast();
final StreamController<String?> selectNotificationStream =
StreamController<String?>.broadcast();
const MethodChannel platform =
MethodChannel('dexterx.dev/flutter_local_notifications_example');
const String portName = 'notification_send_port';
/// Here, the first argument is the id of notification and is common to all
/// methods that would result in a notification being shown. This is typically
/// set a unique value per notification as using the same id multiple times
/// would result in a notification being updated/overwritten.
class ReceivedNotification {
final int id;
final String? title;
final String? body;
final String? payload;
const ReceivedNotification({
required this.id,
required this.title,
required this.body,
required this.payload,
});
}
String? selectedNotificationPayload;
late final NotificationAppLaunchDetails? notificationAppLaunchDetails;
/// A notification action which triggers a url launch event
const String urlLanunchActionId = 'id_1';
/// A notification action which triggers a App navigation event
const String navigationActionId = 'id-3';
/// Defines a iOS/MacOS notification category for text input actions.
const String darwinNotificationCategoryText = 'textCategory';
/// Defines a iOS/MacOS notification category for plain actions.
const String darwinNotificationCategoryPlain = 'plainCategory';
@pragma('vm:entry-point')
void notificationTapBackground(NotificationResponse notificationResponse) {
print('notification(${notificationResponse.id}) action tapped: '
'${notificationResponse.actionId} with'
' payload: ${notificationResponse.payload}');
if (notificationResponse.input?.isNotEmpty ?? false) {
print(
'notification action tapped with input: ${notificationResponse.input}');
}
}
/// Scheduling notifications now requires developers to specify a date and
/// time relative to a specific time zone.
/// This is to solve issues with daylight saving time that existed in the
/// schedule method that is now deprecated.
Future<void> _configureLocalTimeZone() async {
if (kIsWeb || Platform.isLinux) {
return;
}
tz.initializeTimeZones();
final String timeZoneName = await FlutterTimezone.getLocalTimezone();
tz.setLocalLocation(tz.getLocation(timeZoneName));
}
Future<void> initNotification() async {
WidgetsFlutterBinding.ensureInitialized();
await _configureLocalTimeZone();
// not null if platform is Linux
notificationAppLaunchDetails = !kIsWeb && Platform.isLinux
? null
// Use the getNotificationAppLaunchDetails method when the app starts
// if you need to handle when a notification triggering the launch for
// an app e.g. change the home route of the app for deep-linking.
: await flutterLocalNotificationsPlugin.getNotificationAppLaunchDetails();
String initRouteName = 'Home';
if (notificationAppLaunchDetails?.didNotificationLaunchApp ?? false) {
// If the app was launched via notification
// (e.g. taps a notification out of the app).
// route change depends on what type message is. Such as receiving a chat
// message, this route should change to message screen, and receiving a apply
// request shuold change to apply screen
initRouteName = 'Message'; // TODO: Set the route according to message type
}
const AndroidInitializationSettings androidInitializationSettings =
AndroidInitializationSettings('app_icon');
final List<DarwinNotificationCategory> darwinNotificationCategories =
<DarwinNotificationCategory>[
DarwinNotificationCategory(
darwinNotificationCategoryText,
actions: <DarwinNotificationAction>[
DarwinNotificationAction.text(
'text_1',
'action 1',
buttonTitle: 'send',
placeholder: 'Placeholder',
),
],
),
DarwinNotificationCategory(
darwinNotificationCategoryPlain,
actions: <DarwinNotificationAction>[
DarwinNotificationAction.plain('id_1', 'Action 1'),
DarwinNotificationAction.plain(
'id_2',
'Action 2 (destructive)',
options: <DarwinNotificationActionOption>{
DarwinNotificationActionOption.destructive,
},
),
DarwinNotificationAction.plain(
navigationActionId,
'Action 3 (foreground)',
options: <DarwinNotificationActionOption>{
DarwinNotificationActionOption.foreground,
},
),
DarwinNotificationAction.plain(
'id_4',
'Action 4 (auth required)',
options: <DarwinNotificationActionOption>{
DarwinNotificationActionOption.authenticationRequired,
},
),
],
options: <DarwinNotificationCategoryOption>{
DarwinNotificationCategoryOption.hiddenPreviewShowTitle,
},
),
];
/// Note: permissions aren't requested here just to demonstrate that can be
/// done later
final DarwinInitializationSettings darwinInitializationSettings =
DarwinInitializationSettings(
requestAlertPermission: false,
requestBadgePermission: false,
requestSoundPermission: false,
onDidReceiveLocalNotification: (
int id,
String? title,
String? body,
String? payload,
) async {
didReceiveNotificationStream.add(
ReceivedNotification(
id: id,
title: title,
body: body,
payload: payload,
),
);
},
notificationCategories: darwinNotificationCategories,
);
final InitializationSettings initializationSettings = InitializationSettings(
android: androidInitializationSettings,
iOS: darwinInitializationSettings,
);
await flutterLocalNotificationsPlugin.initialize(
initializationSettings,
// This callback fire when a notification has tapped
onDidReceiveNotificationResponse: (NotificationResponse notificationRes) {
switch (notificationRes.notificationResponseType) {
case NotificationResponseType.selectedNotification:
selectNotificationStream.add(notificationRes.payload);
break;
case NotificationResponseType.selectedNotificationAction:
if (notificationRes.actionId == navigationActionId) {
selectNotificationStream.add(notificationRes.payload);
}
break;
}
},
onDidReceiveBackgroundNotificationResponse: notificationTapBackground,
);
}

View File

@ -1,4 +1,5 @@
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:flutter/cupertino.dart';
@ -10,28 +11,11 @@ import 'package:flutter_timezone/flutter_timezone.dart';
import 'package:timezone/data/latest_all.dart' as tz;
import 'package:timezone/timezone.dart' as tz;
/// Before release build of app, read [Release build configuration]
/// https://pub-web.flutter-io.cn/packages/flutter_local_notifications
int id = 0;
/// Init notification plugin
final FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin =
FlutterLocalNotificationsPlugin();
/// Streams are created so that app can respond to notification-related events
/// since the plugin is initialised in the `main` function
/// Only be used in IOS?
final StreamController<ReceivedNotification?> didReceiveNotificationStream =
StreamController<ReceivedNotification>.broadcast();
final StreamController<String?> selectNotificationStream =
StreamController<String?>.broadcast();
const MethodChannel platform =
MethodChannel('dexterx.dev/flutter_local_notifications_example');
const String portName = 'notification_send_port';
import 'models/init_get_it.dart';
import 'models/contact_model.dart';
import 'models/user_model.dart';
import 'request/user_profile.dart';
import 'utils/app_dir.dart';
/// Here, the first argument is the id of notification and is common to all
/// methods that would result in a notification being shown. This is typically
@ -51,166 +35,317 @@ class ReceivedNotification {
});
}
String? selectedNotificationPayload;
/// Before release build of app, read [Release build configuration]
/// https://pub-web.flutter-io.cn/packages/flutter_local_notifications
late final NotificationAppLaunchDetails? notificationAppLaunchDetails;
class NotificationAPI {
static int id = 0;
/// A notification action which triggers a url launch event
const String urlLanunchActionId = 'id_1';
static Map<String, (int, List<Message>)> messages = {};
/// A notification action which triggers a App navigation event
const String navigationActionId = 'id-3';
/// Init notification plugin
static final FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin =
FlutterLocalNotificationsPlugin();
/// Defines a iOS/MacOS notification category for text input actions.
const String darwinNotificationCategoryText = 'textCategory';
/// Streams are created so that app can respond to notification-related events
/// since the plugin is initialised in the `main` function
/// Only be used in IOS?
static final StreamController<ReceivedNotification?>
didReceiveNotificationStream =
StreamController<ReceivedNotification>.broadcast();
/// Defines a iOS/MacOS notification category for plain actions.
const String darwinNotificationCategoryPlain = 'plainCategory';
static final StreamController<String?> selectNotificationStream =
StreamController<String?>.broadcast();
@pragma('vm:entry-point')
void notificationTapBackground(NotificationResponse notificationResponse) {
print('notification(${notificationResponse.id}) action tapped: '
'${notificationResponse.actionId} with'
' payload: ${notificationResponse.payload}');
static const MethodChannel platform =
MethodChannel('dexterx.dev/flutter_local_notifications_example');
if (notificationResponse.input?.isNotEmpty ?? false) {
print(
'notification action tapped with input: ${notificationResponse.input}');
}
}
static const String portName = 'notification_send_port';
/// Scheduling notifications now requires developers to specify a date and
/// time relative to a specific time zone.
/// This is to solve issues with daylight saving time that existed in the
/// schedule method that is now deprecated.
Future<void> _configureLocalTimeZone() async {
if (kIsWeb || Platform.isLinux) {
return;
}
String? selectedNotificationPayload;
tz.initializeTimeZones();
final String timeZoneName = await FlutterTimezone.getLocalTimezone();
tz.setLocalLocation(tz.getLocation(timeZoneName));
}
static late final NotificationAppLaunchDetails? notificationAppLaunchDetails;
Future<void> initNotification() async {
WidgetsFlutterBinding.ensureInitialized();
/// A notification action which triggers a url launch event
static const String urlLanunchActionId = 'id_1';
await _configureLocalTimeZone();
/// A notification action which triggers a App navigation event
static const String navigationActionId = 'id-3';
// not null if platform is Linux
notificationAppLaunchDetails = !kIsWeb && Platform.isLinux
? null
// Use the getNotificationAppLaunchDetails method when the app starts
// if you need to handle when a notification triggering the launch for
// an app e.g. change the home route of the app for deep-linking.
: await flutterLocalNotificationsPlugin.getNotificationAppLaunchDetails();
/// Defines a iOS/MacOS notification category for text input actions.
static const String darwinNotificationCategoryText = 'textCategory';
String initRouteName = 'Home';
/// Defines a iOS/MacOS notification category for plain actions.
static const String darwinNotificationCategoryPlain = 'plainCategory';
if (notificationAppLaunchDetails?.didNotificationLaunchApp ?? false) {
// If the app was launched via notification
// (e.g. taps a notification out of the app).
static Future<void> init() async {
WidgetsFlutterBinding.ensureInitialized();
// route change depends on what type message is. Such as receiving a chat
// message, this route should change to message screen, and receiving a apply
// request shuold change to apply screen
initRouteName = 'Message'; // TODO: Set the route according to message type
}
await _configureLocalTimeZone();
const AndroidInitializationSettings androidInitializationSettings =
AndroidInitializationSettings('app_icon');
// not null if platform is Linux
notificationAppLaunchDetails = !kIsWeb && Platform.isLinux
? null
// Use the getNotificationAppLaunchDetails method when the app starts
// if you need to handle when a notification triggering the launch for
// an app e.g. change the home route of the app for deep-linking.
: await flutterLocalNotificationsPlugin
.getNotificationAppLaunchDetails();
final List<DarwinNotificationCategory> darwinNotificationCategories =
<DarwinNotificationCategory>[
DarwinNotificationCategory(
darwinNotificationCategoryText,
actions: <DarwinNotificationAction>[
DarwinNotificationAction.text(
'text_1',
'action 1',
buttonTitle: 'send',
placeholder: 'Placeholder',
),
],
),
DarwinNotificationCategory(
darwinNotificationCategoryPlain,
actions: <DarwinNotificationAction>[
DarwinNotificationAction.plain('id_1', 'Action 1'),
DarwinNotificationAction.plain(
'id_2',
'Action 2 (destructive)',
options: <DarwinNotificationActionOption>{
DarwinNotificationActionOption.destructive,
},
),
DarwinNotificationAction.plain(
navigationActionId,
'Action 3 (foreground)',
options: <DarwinNotificationActionOption>{
DarwinNotificationActionOption.foreground,
},
),
DarwinNotificationAction.plain(
'id_4',
'Action 4 (auth required)',
options: <DarwinNotificationActionOption>{
DarwinNotificationActionOption.authenticationRequired,
},
),
],
options: <DarwinNotificationCategoryOption>{
DarwinNotificationCategoryOption.hiddenPreviewShowTitle,
String initRouteName = 'Home';
if (notificationAppLaunchDetails?.didNotificationLaunchApp ?? false) {
// If the app was launched via notification
// (e.g. taps a notification out of the app).
// route change depends on what type message is. Such as receiving a chat
// message, this route should change to message screen, and receiving a apply
// request shuold change to apply screen
initRouteName =
'Message'; // TODO: Set the route according to message type
}
const AndroidInitializationSettings androidInitializationSettings =
AndroidInitializationSettings('app_icon');
final List<DarwinNotificationCategory> darwinNotificationCategories =
<DarwinNotificationCategory>[
DarwinNotificationCategory(
darwinNotificationCategoryText,
actions: <DarwinNotificationAction>[
DarwinNotificationAction.text(
'text_1',
'action 1',
buttonTitle: 'send',
placeholder: 'Placeholder',
),
],
),
DarwinNotificationCategory(
darwinNotificationCategoryPlain,
actions: <DarwinNotificationAction>[
DarwinNotificationAction.plain('id_1', 'Action 1'),
DarwinNotificationAction.plain(
'id_2',
'Action 2 (destructive)',
options: <DarwinNotificationActionOption>{
DarwinNotificationActionOption.destructive,
},
),
DarwinNotificationAction.plain(
navigationActionId,
'Action 3 (foreground)',
options: <DarwinNotificationActionOption>{
DarwinNotificationActionOption.foreground,
},
),
DarwinNotificationAction.plain(
'id_4',
'Action 4 (auth required)',
options: <DarwinNotificationActionOption>{
DarwinNotificationActionOption.authenticationRequired,
},
),
],
options: <DarwinNotificationCategoryOption>{
DarwinNotificationCategoryOption.hiddenPreviewShowTitle,
},
),
];
/// Note: permissions aren't requested here just to demonstrate that can be
/// done later
final DarwinInitializationSettings darwinInitializationSettings =
DarwinInitializationSettings(
requestAlertPermission: false,
requestBadgePermission: false,
requestSoundPermission: false,
onDidReceiveLocalNotification: (
int id,
String? title,
String? body,
String? payload,
) async {
didReceiveNotificationStream.add(
ReceivedNotification(
id: id,
title: title,
body: body,
payload: payload,
),
);
},
),
];
notificationCategories: darwinNotificationCategories,
);
/// Note: permissions aren't requested here just to demonstrate that can be
/// done later
final DarwinInitializationSettings darwinInitializationSettings =
DarwinInitializationSettings(
requestAlertPermission: false,
requestBadgePermission: false,
requestSoundPermission: false,
onDidReceiveLocalNotification: (
int id,
String? title,
String? body,
String? payload,
) async {
didReceiveNotificationStream.add(
ReceivedNotification(
id: id,
title: title,
body: body,
payload: payload,
),
);
},
notificationCategories: darwinNotificationCategories,
);
final InitializationSettings initializationSettings =
InitializationSettings(
android: androidInitializationSettings,
iOS: darwinInitializationSettings,
);
final InitializationSettings initializationSettings = InitializationSettings(
android: androidInitializationSettings,
iOS: darwinInitializationSettings,
);
await flutterLocalNotificationsPlugin.initialize(
initializationSettings,
// This callback fire when a notification has tapped
onDidReceiveNotificationResponse: (NotificationResponse notificationRes) {
switch (notificationRes.notificationResponseType) {
case NotificationResponseType.selectedNotification:
selectNotificationStream.add(notificationRes.payload);
break;
case NotificationResponseType.selectedNotificationAction:
if (notificationRes.actionId == navigationActionId) {
await flutterLocalNotificationsPlugin.initialize(
initializationSettings,
// This callback fire when a notification has tapped
onDidReceiveNotificationResponse: (NotificationResponse notificationRes) {
switch (notificationRes.notificationResponseType) {
case NotificationResponseType.selectedNotification:
selectNotificationStream.add(notificationRes.payload);
}
break;
break;
case NotificationResponseType.selectedNotificationAction:
if (notificationRes.actionId == navigationActionId) {
selectNotificationStream.add(notificationRes.payload);
}
break;
}
},
onDidReceiveBackgroundNotificationResponse: _notificationTapBackground,
);
}
@pragma('vm:entry-point')
static void _notificationTapBackground(
NotificationResponse notificationResponse) {
print('notification(${notificationResponse.id}) action tapped: '
'${notificationResponse.actionId} with'
' payload: ${notificationResponse.payload}');
if (notificationResponse.input?.isNotEmpty ?? false) {
print(
'notification action tapped with input: ${notificationResponse.input}');
}
}
/// Scheduling notifications now requires developers to specify a date and
/// time relative to a specific time zone.
/// This is to solve issues with daylight saving time that existed in the
/// schedule method that is now deprecated.
static Future<void> _configureLocalTimeZone() async {
if (kIsWeb || Platform.isLinux) {
return;
}
tz.initializeTimeZones();
final String timeZoneName = await FlutterTimezone.getLocalTimezone();
tz.setLocalLocation(tz.getLocation(timeZoneName));
}
static Future<void> showMessageNotifications({
required String senderId,
required String name,
required String avatar,
required String text,
String? groupChatId,
}) async {
String groupChannelId = 'TOGETHER_${getIt.get<UserAccount>().id}';
const String groupChannelName = 'together messages';
String avatarPath = '';
if (avatar.isNotEmpty) {
if (groupChatId == null) {
avatarPath = await getAvatarPath('user', avatar);
if (!File(avatarPath).existsSync()) {
File file = await File(avatarPath).create(recursive: true);
final bytes = await downloadUserAvatar(avatar);
await file.writeAsBytes(bytes);
}
} else {
// TODO: download group chat avatar. This will done after changing group chat avatar is implemented
}
},
onDidReceiveBackgroundNotificationResponse: notificationTapBackground,
);
}
late Person person;
if (avatarPath.isNotEmpty) {
person = Person(
name: name,
key: senderId,
bot: false,
icon: BitmapFilePathAndroidIcon(avatarPath),
);
} else {
person = Person(
name: name,
key: senderId,
bot: false,
);
}
if (groupChatId == null) {
if (messages.containsKey(senderId)) {
messages[senderId]!.$2.add(Message(text, DateTime.now(), person));
} else {
messages[senderId] = (++id, [Message(text, DateTime.now(), person)]);
}
} else {
if (messages.containsKey(groupChatId)) {
messages[groupChatId]!.$2.add(Message(text, DateTime.now(), person));
} else {
messages[groupChatId] = (++id, [Message(text, DateTime.now(), person)]);
}
}
late final MessagingStyleInformation messagingStyle;
if (groupChatId == null) {
messagingStyle = MessagingStyleInformation(
person,
messages: messages[senderId]!.$2,
conversationTitle: name,
);
} else {
String groupChatName =
getIt.get<Contact>().groupChats[groupChatId]!.nameRemark.isEmpty
? getIt.get<ContactAccountProfile>().groupChats[groupChatId]!.name
: getIt.get<Contact>().groupChats[groupChatId]!.nameRemark;
messagingStyle = MessagingStyleInformation(
person,
messages: messages[groupChatId]!.$2,
conversationTitle: groupChatName,
groupConversation: true,
);
}
AndroidNotificationDetails androidNotificationDetails =
AndroidNotificationDetails(
groupChannelId,
groupChannelName,
importance: Importance.max,
priority: Priority.high,
category: AndroidNotificationCategory.message,
styleInformation: messagingStyle,
);
NotificationDetails notificationDetails = NotificationDetails(
android: androidNotificationDetails,
);
late String payload;
if (groupChatId == null) {
payload = json.encode({
'event': 'friend-message',
'friendId': senderId,
});
await flutterLocalNotificationsPlugin.show(
messages[senderId]!.$1,
name,
text,
notificationDetails,
payload: payload,
);
} else {
payload = json.encode({
'event': 'group-chat-message',
'groupChatId': groupChatId,
});
await flutterLocalNotificationsPlugin.show(
messages[groupChatId]!.$1,
name,
text,
notificationDetails,
payload: payload,
);
}
}
}

View File

@ -1,6 +1,7 @@
import 'package:flutter/widgets.dart';
import 'package:go_router/go_router.dart';
import 'package:together_mobile/database/hive_database.dart';
import 'router_key.dart';
import 'chat_router.dart';
@ -18,14 +19,13 @@ import 'package:together_mobile/notification_api.dart';
// Used to listen the page transition
// https://blog.csdn.net/sinat_17775997/article/details/106570011#:~:text=Flutter%E7%9B%91%E5%90%AC%E8%B7%AF%E7%94%B1%E8%BF%94%E5%9B%9E%20%E6%96%B9%E6%B3%95%E4%B8%80%EF%BC%9A,push%E6%96%B9%E6%B3%95.then%EF%BC%8C%E6%94%AF%E6%8C%81%E6%89%8B%E5%8A%BF%E8%BF%94%E5%9B%9E%E2%9C%85%EF%BC%8C%E5%8F%AF%E4%BB%A5%E8%8E%B7%E5%8F%96%E4%BC%A0%E5%9B%9E%E5%8F%82%E6%95%B0%E2%9C%85%E3%80%82%20%E6%96%B9%E6%B3%95%E4%BA%8C%EF%BC%9A%20didPopNext%EF%BC%8C%E6%94%AF%E6%8C%81%E6%89%8B%E5%8A%BF%E8%BF%94%E5%9B%9E%E2%9C%85%EF%BC%8C%E4%BD%86%E5%9B%9E%E4%BC%A0%E5%8F%82%E6%95%B0%E8%8E%B7%E5%8F%96%E4%B8%8D%E5%88%B0%E2%9D%8C%E3%80%82
final RouteObserver<ModalRoute<void>> routeObserver =
RouteObserver<ModalRoute<void>>();
// final RouteObserver<ModalRoute<void>> routeObserver =
// RouteObserver<ModalRoute<void>>();
final GoRouter router = GoRouter(
initialLocation: '/welcome',
navigatorKey: rootNavigatorKey,
debugLogDiagnostics: true,
observers: [routeObserver],
routes: <RouteBase>[
GoRoute(
path: '/welcome',
@ -39,6 +39,7 @@ final GoRouter router = GoRouter(
if (res['code'] == 10200) {
await getIt.get<Token>().updateToken(res['token']);
getIt.get<UserAccount>().init(res['data']);
await HiveDatabase.init();
return '/chat';
}
}
@ -109,7 +110,8 @@ final GoRouter router = GoRouter(
initIndex = 2;
}
return HomeScreenWithNavBar(
notificationAppLaunchDetails: notificationAppLaunchDetails,
notificationAppLaunchDetails:
NotificationAPI.notificationAppLaunchDetails,
initIndex: initIndex,
child: child,
);

View File

@ -5,7 +5,6 @@ import 'package:hive_flutter/hive_flutter.dart';
import 'package:together_mobile/database/hive_database.dart';
import 'package:together_mobile/screens/chat/components/group_chat_chat_tile.dart';
import 'package:together_mobile/utils/app_dir.dart';
import 'components/friend_chat_tile.dart';
import 'components/add_menu.dart';
import 'package:together_mobile/database/box_type.dart';
@ -30,23 +29,7 @@ class ChatScreen extends StatefulWidget {
class _ChatScreenState extends State<ChatScreen> {
Future<bool> _initData() async {
if (!getIt.get<UserProfile>().isInitialised) {
List<int> encryptionKeyUint8List = await getEncryptKey();
await Hive.initFlutter(await getBoxDir());
Box<ChatSetting> chatSettingBox =
await Hive.openBox<ChatSetting>('chat_setting');
final openedChats =
chatSettingBox.values.where((element) => element.isOpen);
for (var chatBox in openedChats) {
await Hive.openBox<MessageT>(
'message_${chatBox.contactId}',
encryptionCipher: HiveAesCipher(encryptionKeyUint8List),
compactionStrategy: (entries, deletedEntries) => entries > 200,
);
}
await HiveDatabase.init();
getIt.get<WebSocketManager>().connect(getIt.get<UserAccount>().id);

View File

@ -8,10 +8,8 @@ import 'package:get_it_mixin/get_it_mixin.dart';
import 'package:together_mobile/common/constants.dart';
import 'package:together_mobile/models/apply_list_model.dart';
import 'package:together_mobile/models/contact_model.dart';
import 'package:together_mobile/models/route_state_model.dart';
import 'package:together_mobile/models/user_model.dart';
import 'package:together_mobile/request/server.dart';
import 'package:together_mobile/router/router.dart';
import 'package:together_mobile/screens/contact/components/friend_group.dart';
import 'package:together_mobile/screens/contact/components/friend_tile.dart';
import 'package:together_mobile/screens/contact/components/group_chat_tile.dart';
@ -23,31 +21,13 @@ class ContactScreen extends StatefulWidget with GetItStatefulWidgetMixin {
State<ContactScreen> createState() => _ContactScreenState();
}
class _ContactScreenState extends State<ContactScreen>
with GetItStateMixin, RouteAware {
class _ContactScreenState extends State<ContactScreen> with GetItStateMixin {
final Map<String, bool> _shows = {
'friendGroups': true,
'allFriends': false,
'groupChats': false
};
@override
void didChangeDependencies() {
super.didChangeDependencies();
routeObserver.subscribe(this, ModalRoute.of(context)!);
}
@override
void didPush() {
get<RouteState>().changeRoute('Contact');
}
@override
void dispose() {
routeObserver.unsubscribe(this);
super.dispose();
}
@override
Widget build(BuildContext context) {
// Seems like `watchOnly` cannot watch collecion type.

View File

@ -196,7 +196,7 @@ class _CreateGroupChatScreenState extends State<CreateGroupChatScreen> {
'groupGhat': res['data']
}),
);
await openNewMessageBox(id, 1);
await HiveDatabase.openNewMessageBox(id, 1);
// ignore: use_build_context_synchronously
context.goNamed(
'Message',

View File

@ -15,9 +15,7 @@ import 'package:together_mobile/models/apply_list_model.dart';
import 'package:together_mobile/models/init_get_it.dart';
import 'package:together_mobile/models/route_state_model.dart';
import 'package:together_mobile/models/user_model.dart';
import 'package:together_mobile/models/websocket_model.dart';
import 'package:together_mobile/notification_api.dart';
import 'package:together_mobile/utils/app_dir.dart';
class HomeScreenWithNavBar extends StatefulWidget
with GetItStatefulWidgetMixin {
@ -47,23 +45,7 @@ class _HomeScreenWithNavBarState extends State<HomeScreenWithNavBar>
with GetItStateMixin, WidgetsBindingObserver {
Future<bool> _openBox() async {
if (!getIt.get<UserProfile>().isInitialised) {
List<int> encryptionKeyUint8List = await getEncryptKey();
await Hive.initFlutter(await getBoxDir());
Box<ChatSetting> chatSettingBox =
await Hive.openBox<ChatSetting>('chat_setting');
final openedChats =
chatSettingBox.values.where((element) => element.isOpen);
for (var chatBox in openedChats) {
await Hive.openBox<MessageT>(
'message_${chatBox.contactId}',
encryptionCipher: HiveAesCipher(encryptionKeyUint8List),
compactionStrategy: (entries, deletedEntries) => entries > 200,
);
}
await HiveDatabase.init();
}
return Future(() => true);
}
@ -84,8 +66,8 @@ class _HomeScreenWithNavBarState extends State<HomeScreenWithNavBar>
void dispose() {
super.dispose();
WidgetsBinding.instance.removeObserver(this);
didReceiveNotificationStream.close();
selectNotificationStream.close();
NotificationAPI.didReceiveNotificationStream.close();
NotificationAPI.selectNotificationStream.close();
}
@override
@ -94,8 +76,8 @@ class _HomeScreenWithNavBarState extends State<HomeScreenWithNavBar>
switch (state) {
case AppLifecycleState.resumed:
print('应用进入前台==================');
messages.clear();
flutterLocalNotificationsPlugin.cancelAll();
NotificationAPI.messages.clear();
NotificationAPI.flutterLocalNotificationsPlugin.cancelAll();
getIt.get<RouteState>().changeVisibility(true);
break;
case AppLifecycleState.inactive:
@ -110,12 +92,14 @@ class _HomeScreenWithNavBarState extends State<HomeScreenWithNavBar>
print('当前页面即将退出===================');
getIt.get<RouteState>().changeVisibility(false);
break;
case AppLifecycleState.hidden:
print("当前应用被隐藏=======================");
}
}
Future<void> _isAndroidPermissionGranted() async {
if (Platform.isAndroid) {
final bool granted = await flutterLocalNotificationsPlugin
final bool granted = await NotificationAPI.flutterLocalNotificationsPlugin
.resolvePlatformSpecificImplementation<
AndroidFlutterLocalNotificationsPlugin>()
?.areNotificationsEnabled() ??
@ -129,7 +113,7 @@ class _HomeScreenWithNavBarState extends State<HomeScreenWithNavBar>
Future<void> _requestPermissions() async {
if (Platform.isIOS || Platform.isMacOS) {
await flutterLocalNotificationsPlugin
await NotificationAPI.flutterLocalNotificationsPlugin
.resolvePlatformSpecificImplementation<
IOSFlutterLocalNotificationsPlugin>()
?.requestPermissions(
@ -137,7 +121,7 @@ class _HomeScreenWithNavBarState extends State<HomeScreenWithNavBar>
badge: true,
sound: true,
);
await flutterLocalNotificationsPlugin
await NotificationAPI.flutterLocalNotificationsPlugin
.resolvePlatformSpecificImplementation<
MacOSFlutterLocalNotificationsPlugin>()
?.requestPermissions(
@ -147,8 +131,9 @@ class _HomeScreenWithNavBarState extends State<HomeScreenWithNavBar>
);
} else if (Platform.isAndroid) {
final AndroidFlutterLocalNotificationsPlugin? androidImplementation =
flutterLocalNotificationsPlugin.resolvePlatformSpecificImplementation<
AndroidFlutterLocalNotificationsPlugin>();
NotificationAPI.flutterLocalNotificationsPlugin
.resolvePlatformSpecificImplementation<
AndroidFlutterLocalNotificationsPlugin>();
final bool? grantedNotificationPermission =
await androidImplementation?.requestPermission();
@ -159,7 +144,7 @@ class _HomeScreenWithNavBarState extends State<HomeScreenWithNavBar>
}
void _configureDidReceiveLocalNotificationSubject() {
didReceiveNotificationStream.stream.listen(
NotificationAPI.didReceiveNotificationStream.stream.listen(
(ReceivedNotification? receivedNotification) async {
await showDialog(
context: context,
@ -184,8 +169,9 @@ class _HomeScreenWithNavBarState extends State<HomeScreenWithNavBar>
void _configureSelectNotificationSubject() {
// this listener will be call after taps a notification
selectNotificationStream.stream.listen((String? payload) async {
messages.clear();
NotificationAPI.selectNotificationStream.stream
.listen((String? payload) async {
NotificationAPI.messages.clear();
if (payload != null) {
Map<String, dynamic> payloadData = json.decode(payload);
switch (payloadData['event']) {
@ -288,8 +274,8 @@ class _HomeScreenWithNavBarState extends State<HomeScreenWithNavBar>
void _onItemTapped(int index, BuildContext context) {
switch (index) {
case 0:
messages.clear();
flutterLocalNotificationsPlugin.cancelAll();
NotificationAPI.messages.clear();
NotificationAPI.flutterLocalNotificationsPlugin.cancelAll();
GoRouter.of(context).go('/chat');
break;

View File

@ -86,7 +86,7 @@ class _MoreScreenState extends State<MoreScreen> {
),
ListTile(
onTap: () {
context.pushNamed('Setting');
context.goNamed('Setting');
},
contentPadding: const EdgeInsets.all(10),
visualDensity: VisualDensity.compact,

View File

@ -2,7 +2,7 @@ import 'dart:async';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:hive_flutter/hive_flutter.dart';
import 'package:together_mobile/database/hive_database.dart';
import 'package:together_mobile/models/apply_list_model.dart';
import 'package:together_mobile/models/contact_model.dart';
@ -33,16 +33,21 @@ class SettingScreen extends StatelessWidget {
child: TextButton(
onPressed: () async {
await getIt.get<Token>().clear();
getIt.get<WebSocketManager>().disconnect();
getIt.get<UserAccount>().clear();
getIt.get<UserProfile>().clear();
getIt.get<ApplyList>().clear();
getIt.get<Contact>().clear();
getIt.get<ContactAccountProfile>().clear();
// Hive.deleteFromDisk();
Hive.close();
// ignore: use_build_context_synchronously
context.go('/welcome');
// ignore: use_build_context_synchronously
Timer(
const Duration(milliseconds: 200),
() {
getIt.get<WebSocketManager>().disconnect();
getIt.get<UserAccount>().clear();
getIt.get<UserProfile>().clear();
getIt.get<ApplyList>().clear();
getIt.get<Contact>().clear();
getIt.get<ContactAccountProfile>().clear();
// Hive.deleteFromDisk();
HiveDatabase.close();
},
);
},
child: const Text('退出登陆'),
),

View File

@ -31,7 +31,7 @@ class _SignupBodyState extends State<SignupBody> {
'code': false,
};
late Timer _timer;
Timer? _timer;
int _count = 60;
bool _isGetCode = false;
@ -46,7 +46,7 @@ class _SignupBodyState extends State<SignupBody> {
void dispose() {
usernameController.dispose();
passwordController.dispose();
_timer.cancel();
_timer?.cancel();
super.dispose();
}

View File

@ -3,6 +3,7 @@ import 'package:flutter/material.dart';
import 'package:cherry_toast/cherry_toast.dart';
import 'package:go_router/go_router.dart';
import 'package:device_info_plus/device_info_plus.dart';
import 'package:together_mobile/database/hive_database.dart';
import 'package:together_mobile/models/token_model.dart';
import 'common_widgets.dart';
@ -139,6 +140,7 @@ class _UsernameSigninBodyState extends State<UsernameSigninBody> {
getIt.get<Token>().init();
getIt.get<Token>().updateToken(res['token']);
await HiveDatabase.init();
// ignore: use_build_context_synchronously
context.go('/chat');
} else {

View File

@ -21,10 +21,10 @@ packages:
dependency: "direct main"
description:
name: archive
sha256: "0c8368c9b3f0abbc193b9d6133649a614204b528982bebc7026372d61677ce3a"
sha256: "49b1fad315e57ab0bbc15bcbb874e83116a1d78f77ebd500a4af6c9407d6b28e"
url: "https://pub.flutter-io.cn"
source: hosted
version: "3.3.7"
version: "3.3.8"
args:
dependency: transitive
description:
@ -85,10 +85,10 @@ packages:
dependency: transitive
description:
name: build_resolvers
sha256: "6c4dd11d05d056e76320b828a1db0fc01ccd376922526f8e9d6c796a5adbac20"
sha256: d912852cce27c9e80a93603db721c267716894462e7033165178b91138587972
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.2.1"
version: "2.3.2"
build_runner:
dependency: "direct dev"
description:
@ -189,10 +189,10 @@ packages:
dependency: transitive
description:
name: collection
sha256: "4a07be6cb69c84d677a6c3096fcf960cc3285a8330b4603e0d463d15d9bd934c"
sha256: f092b211a4319e98e5ff58223576de6c2803db36221657b46c82574721240687
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.17.1"
version: "1.17.2"
colorfilter_generator:
dependency: transitive
description:
@ -293,18 +293,18 @@ packages:
dependency: transitive
description:
name: extended_image
sha256: e77d18f956649ba6e5ecebd0cb68542120886336a75ee673788145bd4c3f0767
sha256: b4d72a27851751cfadaf048936d42939db7cd66c08fdcfe651eeaa1179714ee6
url: "https://pub.flutter-io.cn"
source: hosted
version: "8.0.2"
version: "8.1.1"
extended_image_library:
dependency: transitive
description:
name: extended_image_library
sha256: bb8d08c504ebc73d476ec1c99451a61f12e95538869e734fc4f55a3a2d5c98ec
sha256: "8bf87c0b14dcb59200c923a9a3952304e4732a0901e40811428834ef39018ee1"
url: "https://pub.flutter-io.cn"
source: hosted
version: "3.5.3"
version: "3.6.0"
fake_async:
dependency: transitive
description:
@ -596,18 +596,18 @@ packages:
dependency: "direct main"
description:
name: get_it
sha256: "529de303c739fca98cd7ece5fca500d8ff89649f1bb4b4e94fb20954abcd7468"
sha256: f79870884de16d689cf9a7d15eedf31ed61d750e813c538a6efb92660fea83c3
url: "https://pub.flutter-io.cn"
source: hosted
version: "7.6.0"
version: "7.6.4"
get_it_mixin:
dependency: "direct main"
description:
name: get_it_mixin
sha256: d59fe7e49e258ddf9f10580b50cce5c129d3f2f6a340b684847615128c641261
sha256: "0ab5c9f3cdaab813ec396de5d43ee3833c418424b3a99bec0071fcbf693c0bad"
url: "https://pub.flutter-io.cn"
source: hosted
version: "4.2.0"
version: "4.2.2"
glob:
dependency: transitive
description:
@ -756,10 +756,10 @@ packages:
dependency: "direct main"
description:
name: image_picker
sha256: "866724408a53806722893012bc8081ed78dc756b36f19e37712819445da3ad3a"
sha256: "7d7f2768df2a8b0a3cefa5ef4f84636121987d403130e70b17ef7e2cf650ba84"
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.0.3"
version: "1.0.4"
image_picker_android:
dependency: transitive
description:
@ -860,18 +860,18 @@ packages:
dependency: transitive
description:
name: matcher
sha256: "6501fbd55da300384b768785b83e5ce66991266cec21af89ab9ae7f5ce1c4cbb"
sha256: "1803e76e6653768d64ed8ff2e1e67bea3ad4b923eb5c56a295c3e634bad5960e"
url: "https://pub.flutter-io.cn"
source: hosted
version: "0.12.15"
version: "0.12.16"
material_color_utilities:
dependency: transitive
description:
name: material_color_utilities
sha256: d92141dc6fe1dad30722f9aa826c7fbc896d021d792f80678280601aff8cf724
sha256: "9528f2f296073ff54cb9fee677df673ace1218163c3bc7628093e7eed5203d41"
url: "https://pub.flutter-io.cn"
source: hosted
version: "0.2.0"
version: "0.5.0"
matrix2d:
dependency: transitive
description:
@ -932,10 +932,10 @@ packages:
dependency: transitive
description:
name: path_provider_android
sha256: "71c95ee27a9938693fa326b54ae60ae996f35a357acd957c71b2735aee2688c5"
sha256: "6b8b19bd80da4f11ce91b2d1fb931f3006911477cec227cce23d3253d80df3f1"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.1.1"
version: "2.2.0"
path_provider_foundation:
dependency: transitive
description:
@ -1153,10 +1153,10 @@ packages:
dependency: transitive
description:
name: source_span
sha256: dd904f795d4b4f3b870833847c461801f6750a9fa8e61ea5ac53f9422b31f250
sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c"
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.9.1"
version: "1.10.0"
sqflite:
dependency: transitive
description:
@ -1225,10 +1225,10 @@ packages:
dependency: transitive
description:
name: test_api
sha256: eb6ac1540b26de412b3403a163d919ba86f6a973fe6cc50ae3541b80092fdcfb
sha256: "75760ffd7786fffdfb9597c35c5b27eaeec82be8edfb6d71d32651128ed7aab8"
url: "https://pub.flutter-io.cn"
source: hosted
version: "0.5.1"
version: "0.6.0"
timezone:
dependency: "direct main"
description:
@ -1273,18 +1273,18 @@ packages:
dependency: "direct main"
description:
name: video_player
sha256: "3fd106c74da32f336dc7feb65021da9b0207cb3124392935f1552834f7cce822"
sha256: d3910a8cefc0de8a432a4411dcf85030e885d8fef3ddea291f162253a05dbf01
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.7.0"
version: "2.7.1"
video_player_android:
dependency: transitive
description:
name: video_player_android
sha256: f338a5a396c845f4632959511cad3542cdf3167e1b2a1a948ef07f7123c03608
sha256: "3fe89ab07fdbce786e7eb25b58532d6eaf189ceddc091cb66cba712f8d9e8e55"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.4.9"
version: "2.4.10"
video_player_avfoundation:
dependency: transitive
description:
@ -1305,10 +1305,10 @@ packages:
dependency: transitive
description:
name: video_player_web
sha256: "44ce41424d104dfb7cf6982cc6b84af2b007a24d126406025bf40de5d481c74c"
sha256: "66fc0d56554143fee4c623f70e45e4272b94fd246283cb67edabb9d1e4122a4f"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.0.16"
version: "2.1.0"
watcher:
dependency: transitive
description:
@ -1317,6 +1317,14 @@ packages:
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.1.0"
web:
dependency: transitive
description:
name: web
sha256: dc8ccd225a2005c1be616fe02951e2e342092edf968cf0844220383757ef8f10
url: "https://pub.flutter-io.cn"
source: hosted
version: "0.1.4-beta"
web_socket_channel:
dependency: "direct main"
description:
@ -1366,5 +1374,5 @@ packages:
source: hosted
version: "3.1.2"
sdks:
dart: ">=3.0.2 <4.0.0"
flutter: ">=3.10.0"
dart: ">=3.1.0 <4.0.0"
flutter: ">=3.13.0"