encrypt notification api and hive api
parent
a260e72e04
commit
eda27359af
|
@ -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,
|
||||
// );
|
||||
// }
|
||||
|
|
|
@ -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,
|
||||
);
|
||||
},
|
||||
|
|
|
@ -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']);
|
||||
}
|
||||
|
||||
|
|
|
@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
);
|
||||
}
|
|
@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
);
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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('退出登陆'),
|
||||
),
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
76
pubspec.lock
76
pubspec.lock
|
@ -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"
|
||||
|
|
Loading…
Reference in New Issue