diff --git a/android/app/build.gradle b/android/app/build.gradle index ef81a5fd4..ff8f43508 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -35,6 +35,7 @@ android { ndkVersion flutter.ndkVersion compileOptions { + coreLibraryDesugaringEnabled true sourceCompatibility JavaVersion.VERSION_17 targetCompatibility JavaVersion.VERSION_17 } @@ -53,6 +54,7 @@ android { targetSdkVersion flutter.targetSdkVersion versionCode flutterVersionCode.toInteger() versionName flutterVersionName + multiDexEnabled true resValue "string", "app_name", "Chaldea" } @@ -106,4 +108,6 @@ flutter { source '../..' } -dependencies {} +dependencies { + coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.2.2' +} diff --git a/android/app/proguard-rules.pro b/android/app/proguard-rules.pro new file mode 100644 index 000000000..f8768e4ba --- /dev/null +++ b/android/app/proguard-rules.pro @@ -0,0 +1,27 @@ +## Gson rules +# Gson uses generic type information stored in a class file when working with fields. Proguard +# removes such information by default, so configure it to keep all of it. +-keepattributes Signature + +# For using GSON @Expose annotation +-keepattributes *Annotation* + +# Gson specific classes +-dontwarn sun.misc.** +#-keep class com.google.gson.stream.** { *; } + +# Prevent proguard from stripping interface information from TypeAdapter, TypeAdapterFactory, +# JsonSerializer, JsonDeserializer instances (so they can be used in @JsonAdapter) +-keep class * extends com.google.gson.TypeAdapter +-keep class * implements com.google.gson.TypeAdapterFactory +-keep class * implements com.google.gson.JsonSerializer +-keep class * implements com.google.gson.JsonDeserializer + +# Prevent R8 from leaving Data object members always null +-keepclassmembers,allowobfuscation class * { + @com.google.gson.annotations.SerializedName ; +} + +# Retain generic signatures of TypeToken and its subclasses with R8 version 3.0 and higher. +-keep,allowobfuscation,allowshrinking class com.google.gson.reflect.TypeToken +-keep,allowobfuscation,allowshrinking class * extends com.google.gson.reflect.TypeToken \ No newline at end of file diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 828108ce8..d2274b792 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -7,6 +7,11 @@ + + + + + + + + + + + + + + + + + + Bool { + FlutterLocalNotificationsPlugin.setPluginRegistrantCallback { (registry) in + GeneratedPluginRegistrant.register(with: registry) + } + if #available(iOS 10.0, *) { + UNUserNotificationCenter.current().delegate = self as UNUserNotificationCenterDelegate + } + let controller : FlutterViewController = window?.rootViewController as! FlutterViewController let chaldeaChannel = FlutterMethodChannel(name: "chaldea.narumi.cc/chaldea", binaryMessenger: controller.binaryMessenger) diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 667ad3127..e77e45a39 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -45,8 +45,12 @@ PODS: - Flutter (1.0.0) - flutter_js (0.1.0): - Flutter + - flutter_local_notifications (0.0.1): + - Flutter - flutter_mailer (0.0.1): - Flutter + - flutter_timezone (0.0.1): + - Flutter - fluttertoast (0.0.2): - Flutter - Toast @@ -56,7 +60,7 @@ PODS: - Flutter - Google-Mobile-Ads-SDK (~> 11.2.0) - webview_flutter_wkwebview - - GoogleUserMessagingPlatform (2.5.0) + - GoogleUserMessagingPlatform (2.6.0) - image_gallery_saver (2.0.2): - Flutter - just_audio (0.0.1): @@ -102,7 +106,9 @@ DEPENDENCIES: - file_picker (from `.symlinks/plugins/file_picker/ios`) - Flutter (from `Flutter`) - flutter_js (from `.symlinks/plugins/flutter_js/ios`) + - flutter_local_notifications (from `.symlinks/plugins/flutter_local_notifications/ios`) - flutter_mailer (from `.symlinks/plugins/flutter_mailer/ios`) + - flutter_timezone (from `.symlinks/plugins/flutter_timezone/ios`) - fluttertoast (from `.symlinks/plugins/fluttertoast/ios`) - google_mobile_ads (from `.symlinks/plugins/google_mobile_ads/ios`) - image_gallery_saver (from `.symlinks/plugins/image_gallery_saver/ios`) @@ -144,8 +150,12 @@ EXTERNAL SOURCES: :path: Flutter flutter_js: :path: ".symlinks/plugins/flutter_js/ios" + flutter_local_notifications: + :path: ".symlinks/plugins/flutter_local_notifications/ios" flutter_mailer: :path: ".symlinks/plugins/flutter_mailer/ios" + flutter_timezone: + :path: ".symlinks/plugins/flutter_timezone/ios" fluttertoast: :path: ".symlinks/plugins/fluttertoast/ios" google_mobile_ads: @@ -187,11 +197,13 @@ SPEC CHECKSUMS: file_picker: 09aa5ec1ab24135ccd7a1621c46c84134bfd6655 Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 flutter_js: 95929d4e146e8ceb1c8e1889d8c2065c5d840076 + flutter_local_notifications: 4cde75091f6327eb8517fa068a0a5950212d2086 flutter_mailer: 2ef5a67087bc8c6c4cefd04a178bf1ae2c94cd83 + flutter_timezone: ffb07bdad3c6276af8dada0f11978d8a1f8a20bb fluttertoast: e9a18c7be5413da53898f660530c56f35edfba9c Google-Mobile-Ads-SDK: 5a6d005a6cb5b5e8f4c7b69ca05cdea79c181139 google_mobile_ads: 9379c80fdfa9988fb0e105a407890ff8deb3cf86 - GoogleUserMessagingPlatform: 6b4f48a370e77ce121d034c908cc6ee4fdafaf13 + GoogleUserMessagingPlatform: 0c3a08353e53ce8c2feab7addd0b652cde522450 image_gallery_saver: cb43cc43141711190510e92c460eb1655cd343cb just_audio: baa7252489dbcf47a4c7cc9ca663e9661c99aafa package_info_plus: 58f0028419748fad15bf008b270aaa8e54380b1c diff --git a/lib/app/modules/faker/faker.dart b/lib/app/modules/faker/faker.dart index 050bd38cb..9c10beb20 100644 --- a/lib/app/modules/faker/faker.dart +++ b/lib/app/modules/faker/faker.dart @@ -23,6 +23,7 @@ import 'package:chaldea/models/faker/shared/agent.dart'; import 'package:chaldea/models/gamedata/toplogin.dart'; import 'package:chaldea/models/models.dart'; import 'package:chaldea/packages/logger.dart'; +import 'package:chaldea/utils/notification.dart'; import 'package:chaldea/utils/utils.dart'; import 'package:chaldea/widgets/widgets.dart'; import '../import_data/import_https_page.dart'; @@ -1122,12 +1123,23 @@ class _FakeGrandOrderState extends State { children: [ SwitchListTile( dense: true, - value: fakerSettings.dumpResponse, - title: const Text('Dump Responses'), - onChanged: (v) { + value: fakerSettings.apRecoveredNotification, + title: const Text('AP Recovered Notification'), + onChanged: (v) async { setState(() { - fakerSettings.dumpResponse = v; + fakerSettings.apRecoveredNotification = v; }); + if (v) { + await LocalNotificationUtil.requestPermissions(); + await agent.network.setLocalNotification(); + } else { + final notifications = await LocalNotificationUtil.plugin.getActiveNotifications(); + for (final notification in notifications) { + if (notification.id != null && LocalNotificationUtil.isUserApFullId(notification.id!)) { + LocalNotificationUtil.plugin.cancel(notification.id!); + } + } + } }, ), ListTile( @@ -1148,6 +1160,16 @@ class _FakeGrandOrderState extends State { }, child: Text(fakerSettings.maxFollowerListRetryCount.toString())), ), + SwitchListTile( + dense: true, + value: fakerSettings.dumpResponse, + title: const Text('Dump Responses'), + onChanged: (v) { + setState(() { + fakerSettings.dumpResponse = v; + }); + }, + ), Center( child: TextButton( onPressed: () { diff --git a/lib/app/modules/home/subpage/display_setting_page.dart b/lib/app/modules/home/subpage/display_setting_page.dart index 17d561a5b..fa8fd1e12 100644 --- a/lib/app/modules/home/subpage/display_setting_page.dart +++ b/lib/app/modules/home/subpage/display_setting_page.dart @@ -10,8 +10,10 @@ import 'package:chaldea/generated/l10n.dart'; import 'package:chaldea/models/models.dart'; import 'package:chaldea/packages/ads/ads.dart'; import 'package:chaldea/packages/app_info.dart'; +import 'package:chaldea/packages/logger.dart'; import 'package:chaldea/packages/platform/platform.dart'; import 'package:chaldea/packages/split_route/split_route.dart'; +import 'package:chaldea/utils/notification.dart'; import 'package:chaldea/widgets/custom_dialogs.dart'; import 'package:chaldea/widgets/tile_items.dart'; import '../../root/global_fab.dart'; @@ -375,6 +377,23 @@ class _DisplaySettingPageState extends State { router.pushPage(const AdSettingPage()); }, ), + if (LocalNotificationUtil.supported) + ListTile( + title: const Text('Request Notification Permissions'), + onTap: () async { + try { + final result = await LocalNotificationUtil.requestPermissions(); + if (result == true) { + EasyLoading.showSuccess(S.current.success); + } else { + EasyLoading.showError(result.toString()); + } + } catch (e, s) { + EasyLoading.showError(e.toString()); + logger.e('request notification permission failed', e, s); + } + }, + ), ], ), ], diff --git a/lib/generated/intl/messages_en.dart b/lib/generated/intl/messages_en.dart index 865421593..a435fb108 100644 --- a/lib/generated/intl/messages_en.dart +++ b/lib/generated/intl/messages_en.dart @@ -120,6 +120,7 @@ class MessageLookup extends MessageLookupByLibrary { "ap_campaign_time_mismatch_hint": MessageLookupByLibrary.simpleMessage("Quest Campaigns\' start time of non-JP regions may be incorrect"), "ap_efficiency": MessageLookupByLibrary.simpleMessage("AP rate"), + "ap_fully_recovered": MessageLookupByLibrary.simpleMessage("All AP Recovered"), "app_data_folder": MessageLookupByLibrary.simpleMessage("Data Folder"), "app_data_use_external_storage": MessageLookupByLibrary.simpleMessage("Use External Storage (SD card)"), "appearance": MessageLookupByLibrary.simpleMessage("Appearance"), diff --git a/lib/generated/intl/messages_ja.dart b/lib/generated/intl/messages_ja.dart index 2afdeaf10..32232b76f 100644 --- a/lib/generated/intl/messages_ja.dart +++ b/lib/generated/intl/messages_ja.dart @@ -85,6 +85,7 @@ class MessageLookup extends MessageLookupByLibrary { "anniversary": MessageLookupByLibrary.simpleMessage("周年"), "ap": MessageLookupByLibrary.simpleMessage("AP"), "ap_efficiency": MessageLookupByLibrary.simpleMessage("AP効率"), + "ap_fully_recovered": MessageLookupByLibrary.simpleMessage("APが全回復しました"), "app_data_folder": MessageLookupByLibrary.simpleMessage("データフォルダ"), "app_data_use_external_storage": MessageLookupByLibrary.simpleMessage("外部ストレージ(SDカード)を使用"), "appearance": MessageLookupByLibrary.simpleMessage("外観"), diff --git a/lib/generated/intl/messages_zh.dart b/lib/generated/intl/messages_zh.dart index d4ccc9ae1..b5a1e598d 100644 --- a/lib/generated/intl/messages_zh.dart +++ b/lib/generated/intl/messages_zh.dart @@ -112,6 +112,7 @@ class MessageLookup extends MessageLookupByLibrary { "ap": MessageLookupByLibrary.simpleMessage("AP"), "ap_campaign_time_mismatch_hint": MessageLookupByLibrary.simpleMessage("关卡AP等相关活动显示的时间(日服除外)可能不准确"), "ap_efficiency": MessageLookupByLibrary.simpleMessage("AP效率"), + "ap_fully_recovered": MessageLookupByLibrary.simpleMessage("AP已全部回复"), "app_data_folder": MessageLookupByLibrary.simpleMessage("数据目录"), "app_data_use_external_storage": MessageLookupByLibrary.simpleMessage("使用外部储存(SD卡)"), "appearance": MessageLookupByLibrary.simpleMessage("外观"), diff --git a/lib/generated/intl/messages_zh_Hant.dart b/lib/generated/intl/messages_zh_Hant.dart index 5ac4a7d85..ecb5187b2 100644 --- a/lib/generated/intl/messages_zh_Hant.dart +++ b/lib/generated/intl/messages_zh_Hant.dart @@ -112,6 +112,7 @@ class MessageLookup extends MessageLookupByLibrary { "ap": MessageLookupByLibrary.simpleMessage("AP"), "ap_campaign_time_mismatch_hint": MessageLookupByLibrary.simpleMessage("關卡AP等相關活動顯示的時間(日服除外)可能不準確"), "ap_efficiency": MessageLookupByLibrary.simpleMessage("AP效率"), + "ap_fully_recovered": MessageLookupByLibrary.simpleMessage("AP已全部回复"), "app_data_folder": MessageLookupByLibrary.simpleMessage("數據目錄"), "app_data_use_external_storage": MessageLookupByLibrary.simpleMessage("使用外部儲存(SD卡)"), "appearance": MessageLookupByLibrary.simpleMessage("外觀"), diff --git a/lib/generated/l10n.dart b/lib/generated/l10n.dart index 8592481a2..3a344d583 100644 --- a/lib/generated/l10n.dart +++ b/lib/generated/l10n.dart @@ -305,6 +305,17 @@ class S { ); } + /// `All AP Recovered` + String get ap_fully_recovered { + return Intl.message( + 'All AP Recovered', + name: 'ap_fully_recovered', + desc: '', + locale: localeName, + args: [], + ); + } + /// `Data Folder` String get app_data_folder { return Intl.message( diff --git a/lib/generated/models/userdata/autologin.g.dart b/lib/generated/models/userdata/autologin.g.dart index 4161bb522..cf4d0d2a6 100644 --- a/lib/generated/models/userdata/autologin.g.dart +++ b/lib/generated/models/userdata/autologin.g.dart @@ -12,6 +12,7 @@ FakerSettings _$FakerSettingsFromJson(Map json) => $checkedCreate( ($checkedConvert) { final val = FakerSettings( dumpResponse: $checkedConvert('dumpResponse', (v) => v as bool? ?? false), + apRecoveredNotification: $checkedConvert('apRecoveredNotification', (v) => v as bool? ?? false), maxFollowerListRetryCount: $checkedConvert('maxFollowerListRetryCount', (v) => (v as num?)?.toInt() ?? 20), jpAutoLogins: $checkedConvert( 'jpAutoLogins', @@ -30,6 +31,7 @@ FakerSettings _$FakerSettingsFromJson(Map json) => $checkedCreate( Map _$FakerSettingsToJson(FakerSettings instance) => { 'dumpResponse': instance.dumpResponse, + 'apRecoveredNotification': instance.apRecoveredNotification, 'maxFollowerListRetryCount': instance.maxFollowerListRetryCount, 'jpAutoLogins': instance.jpAutoLogins.map((e) => e.toJson()).toList(), 'cnAutoLogins': instance.cnAutoLogins.map((e) => e.toJson()).toList(), diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index 3952ed6da..102884d67 100644 --- a/lib/l10n/intl_en.arb +++ b/lib/l10n/intl_en.arb @@ -22,6 +22,7 @@ "ap": "AP", "ap_campaign_time_mismatch_hint": "Quest Campaigns' start time of non-JP regions may be incorrect", "ap_efficiency": "AP rate", + "ap_fully_recovered": "All AP Recovered", "app_data_folder": "Data Folder", "app_data_use_external_storage": "Use External Storage (SD card)", "appearance": "Appearance", diff --git a/lib/l10n/intl_ja.arb b/lib/l10n/intl_ja.arb index fed318fdf..825945cc3 100644 --- a/lib/l10n/intl_ja.arb +++ b/lib/l10n/intl_ja.arb @@ -22,6 +22,7 @@ "ap": "AP", "ap_campaign_time_mismatch_hint": null, "ap_efficiency": "AP効率", + "ap_fully_recovered": "APが全回復しました", "app_data_folder": "データフォルダ", "app_data_use_external_storage": "外部ストレージ(SDカード)を使用", "appearance": "外観", diff --git a/lib/l10n/intl_ko.arb b/lib/l10n/intl_ko.arb index 9785b29de..996864b44 100644 --- a/lib/l10n/intl_ko.arb +++ b/lib/l10n/intl_ko.arb @@ -22,6 +22,7 @@ "ap": "AP", "ap_campaign_time_mismatch_hint": "퀴스트 AP 등 관련 이벤트 표시 시간(일본 서버) 정확하지 않을 수 있음", "ap_efficiency": "AP 효율", + "ap_fully_recovered": null, "app_data_folder": "데이터 폴더", "app_data_use_external_storage": "외부 스토리지 (SD 카드)를 사용", "appearance": "디스플레이 설정", diff --git a/lib/l10n/intl_zh.arb b/lib/l10n/intl_zh.arb index 683fca913..de2e40794 100644 --- a/lib/l10n/intl_zh.arb +++ b/lib/l10n/intl_zh.arb @@ -22,6 +22,7 @@ "ap": "AP", "ap_campaign_time_mismatch_hint": "关卡AP等相关活动显示的时间(日服除外)可能不准确", "ap_efficiency": "AP效率", + "ap_fully_recovered": "AP已全部回复", "app_data_folder": "数据目录", "app_data_use_external_storage": "使用外部储存(SD卡)", "appearance": "外观", diff --git a/lib/l10n/intl_zh_Hant.arb b/lib/l10n/intl_zh_Hant.arb index c3f9ed3b5..c126a2dc1 100644 --- a/lib/l10n/intl_zh_Hant.arb +++ b/lib/l10n/intl_zh_Hant.arb @@ -22,6 +22,7 @@ "ap": "AP", "ap_campaign_time_mismatch_hint": "關卡AP等相關活動顯示的時間(日服除外)可能不準確", "ap_efficiency": "AP效率", + "ap_fully_recovered": null, "app_data_folder": "數據目錄", "app_data_use_external_storage": "使用外部儲存(SD卡)", "appearance": "外觀", diff --git a/lib/main.dart b/lib/main.dart index b98541f43..33ad41921 100755 --- a/lib/main.dart +++ b/lib/main.dart @@ -20,6 +20,7 @@ import 'packages/split_route/split_route.dart'; import 'utils/catcher/catcher_util.dart'; import 'utils/catcher/server_feedback_handler.dart'; import 'utils/http_override.dart'; +import 'utils/notification.dart'; import 'utils/utils.dart'; void main() async { @@ -92,4 +93,5 @@ Future _initiateCommon() async { HttpOverrides.global = CustomHttpOverrides(); } SplitRoute.defaultMasterFillPageBuilder = (context) => const BlankPage(); + await LocalNotificationUtil.init(); } diff --git a/lib/models/faker/shared/network.dart b/lib/models/faker/shared/network.dart index 0ffc956ff..5a508a513 100644 --- a/lib/models/faker/shared/network.dart +++ b/lib/models/faker/shared/network.dart @@ -3,11 +3,13 @@ import 'dart:convert'; import 'package:dio/dio.dart'; import 'package:flutter_easyloading/flutter_easyloading.dart'; +import 'package:chaldea/generated/l10n.dart'; import 'package:chaldea/models/db.dart'; import 'package:chaldea/models/gamedata/gamedata.dart'; import 'package:chaldea/models/gamedata/toplogin.dart'; import 'package:chaldea/models/userdata/autologin.dart'; import 'package:chaldea/packages/packages.dart'; +import 'package:chaldea/utils/notification.dart'; import 'package:chaldea/utils/utils.dart'; import '../quiz/cat_mouse.dart'; @@ -203,6 +205,7 @@ abstract class NetworkManagerBase requestStartImpl(TRequest request); + + Future setLocalNotification() async { + if (!db.settings.fakerSettings.apRecoveredNotification) return; + final userGame = mstData.user; + if (userGame == null) return; + final int id = LocalNotificationUtil.generateUserApFullId(user.region.index, userGame.userId); + if (userGame.actRecoverAt < DateTime.now().timestamp + 2) return; + final recoverAt = DateTime.fromMillisecondsSinceEpoch(userGame.actRecoverAt * 1000); + await LocalNotificationUtil.scheduleNotification( + id: id, + dateTime: recoverAt.subtract(const Duration(minutes: 5)), + title: S.current.ap_fully_recovered, + body: [ + '[${user.serverName}] ${userGame.name}', + recoverAt.toCustomString(year: false, millisecond: false), + ].join('\n'), + ); + } } class WWWForm { diff --git a/lib/models/userdata/autologin.dart b/lib/models/userdata/autologin.dart index 10c0f37ed..7eca77569 100644 --- a/lib/models/userdata/autologin.dart +++ b/lib/models/userdata/autologin.dart @@ -11,12 +11,14 @@ part '../../generated/models/userdata/autologin.g.dart'; @JsonSerializable() class FakerSettings { bool dumpResponse; + bool apRecoveredNotification; int maxFollowerListRetryCount; List jpAutoLogins; List cnAutoLogins; FakerSettings({ this.dumpResponse = false, + this.apRecoveredNotification = false, this.maxFollowerListRetryCount = 20, List? jpAutoLogins, List? cnAutoLogins, diff --git a/lib/utils/notification.dart b/lib/utils/notification.dart new file mode 100644 index 000000000..23a8074bd --- /dev/null +++ b/lib/utils/notification.dart @@ -0,0 +1,141 @@ +// ignore_for_file: use_build_context_synchronously + +import 'dart:async'; +import 'dart:io'; + +import 'package:flutter/foundation.dart'; + +import 'package:flutter_local_notifications/flutter_local_notifications.dart'; +import 'package:flutter_timezone/flutter_timezone.dart'; +import 'package:timezone/data/latest_10y.dart' as tz; +import 'package:timezone/timezone.dart' as tz; + +import '../packages/logger.dart'; + +abstract class LocalNotificationUtil { + static int _id = 0; + static final plugin = FlutterLocalNotificationsPlugin(); + + static final bool supported = !kIsWeb && (Platform.isIOS || Platform.isMacOS || Platform.isAndroid); + + static Future init() async { + try { + await _configureLocalTimeZone(); + const darwinSettings = DarwinInitializationSettings( + requestAlertPermission: false, + requestBadgePermission: false, + requestSoundPermission: false, + defaultPresentSound: false, + ); + await plugin.initialize( + const InitializationSettings( + android: AndroidInitializationSettings('ic_launcher'), + iOS: darwinSettings, + macOS: darwinSettings, + ), + onDidReceiveNotificationResponse: (NotificationResponse resp) { + print( + 'onDidReceiveNotificationResponse: ${resp.id},${resp.actionId},${resp.notificationResponseType},${resp.input},${resp.payload}'); + }, + onDidReceiveBackgroundNotificationResponse: notificationTapBackground, + ); + } catch (e, s) { + logger.e('init local notification failed', e, s); + } + } + + static Future requestPermissions() async { + if (kIsWeb) return null; + if (Platform.isIOS) { + return plugin + .resolvePlatformSpecificImplementation() + ?.requestPermissions(alert: true, badge: true, sound: true); + } else if (Platform.isMacOS) { + return plugin + .resolvePlatformSpecificImplementation() + ?.requestPermissions(alert: true, badge: true, sound: true); + } else if (Platform.isAndroid) { + final AndroidFlutterLocalNotificationsPlugin? androidImplementation = + plugin.resolvePlatformSpecificImplementation(); + return androidImplementation?.requestNotificationsPermission(); + } + return null; + } + + static const _defaultAndroidDetails = AndroidNotificationDetails( + 'chaldea_common', + 'Chaldea Common', + channelDescription: 'Chaldea Common', + importance: Importance.max, + priority: Priority.high, + ); + + static Future showNotification({ + int? id, + required String? title, + required String? body, + }) async { + return plugin.show( + id ?? _id++, + title, + body, + const NotificationDetails(android: _defaultAndroidDetails), + ); + } + + static Future scheduleNotification({ + int? id, + required DateTime dateTime, + required String? title, + required String? body, + bool autoCancelPrevious = true, + }) async { + if (id != null && autoCancelPrevious) { + await plugin.cancel(id); + } + return plugin.zonedSchedule( + id ?? _id++, + title, + body, + tz.TZDateTime.from(dateTime, _hasLocalTz ? tz.local : tz.UTC), + const NotificationDetails(android: _defaultAndroidDetails), + androidScheduleMode: AndroidScheduleMode.exactAllowWhileIdle, + uiLocalNotificationDateInterpretation: UILocalNotificationDateInterpretation.absoluteTime, + ); + } + + static bool _hasLocalTz = false; + static Future _configureLocalTimeZone() async { + if (kIsWeb || Platform.isLinux) { + return; + } + tz.initializeTimeZones(); + try { + final String timeZoneName = await FlutterTimezone.getLocalTimezone(); + tz.setLocalLocation(tz.getLocation(timeZoneName)); + _hasLocalTz = true; + } catch (e, s) { + FlutterError.dumpErrorToConsole(FlutterErrorDetails(exception: e, stack: s)); + } + } + + @pragma('vm:entry-point') + static void notificationTapBackground(NotificationResponse resp) { + print('notification(${resp.id}) action tapped: ' + '${resp.actionId} with' + ' payload: ${resp.payload}'); + if (resp.input?.isNotEmpty ?? false) { + print('notification action tapped with input: ${resp.input}'); + } + } + + // + static int generateUserApFullId(int region, int userId) { + // 8 digits + return (10 + region + 1) * 1000000 + userId % 1000000; + } + + static bool isUserApFullId(int id) { + return id ~/ 10000000 == 1; + } +} diff --git a/macos/Podfile.lock b/macos/Podfile.lock index 7c24da1c9..5c1c2e907 100644 --- a/macos/Podfile.lock +++ b/macos/Podfile.lock @@ -8,6 +8,10 @@ PODS: - FlutterMacOS - flutter_js (0.0.1): - FlutterMacOS + - flutter_local_notifications (0.0.1): + - FlutterMacOS + - flutter_timezone (0.1.0): + - FlutterMacOS - FlutterMacOS (1.0.0) - just_audio (0.0.1): - FlutterMacOS @@ -48,6 +52,8 @@ DEPENDENCIES: - connectivity_plus (from `Flutter/ephemeral/.symlinks/plugins/connectivity_plus/darwin`) - device_info_plus (from `Flutter/ephemeral/.symlinks/plugins/device_info_plus/macos`) - flutter_js (from `Flutter/ephemeral/.symlinks/plugins/flutter_js/macos`) + - flutter_local_notifications (from `Flutter/ephemeral/.symlinks/plugins/flutter_local_notifications/macos`) + - flutter_timezone (from `Flutter/ephemeral/.symlinks/plugins/flutter_timezone/macos`) - FlutterMacOS (from `Flutter/ephemeral`) - just_audio (from `Flutter/ephemeral/.symlinks/plugins/just_audio/macos`) - package_info_plus (from `Flutter/ephemeral/.symlinks/plugins/package_info_plus/macos`) @@ -73,6 +79,10 @@ EXTERNAL SOURCES: :path: Flutter/ephemeral/.symlinks/plugins/device_info_plus/macos flutter_js: :path: Flutter/ephemeral/.symlinks/plugins/flutter_js/macos + flutter_local_notifications: + :path: Flutter/ephemeral/.symlinks/plugins/flutter_local_notifications/macos + flutter_timezone: + :path: Flutter/ephemeral/.symlinks/plugins/flutter_timezone/macos FlutterMacOS: :path: Flutter/ephemeral just_audio: @@ -109,6 +119,8 @@ SPEC CHECKSUMS: connectivity_plus: ddd7f30999e1faaef5967c23d5b6d503d10434db device_info_plus: ce1b7762849d3ec103d0e0517299f2db7ad60720 flutter_js: c664361655af1d8fc24e278c7d086e9cbe0d68bc + flutter_local_notifications: 3805ca215b2fb7f397d78b66db91f6a747af52e4 + flutter_timezone: 6b906d1740654acb16e50b639835628fea851037 FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24 just_audio: 9b67ca7b97c61cfc9784ea23cd8cc55eb226d489 package_info_plus: fa739dd842b393193c5ca93c26798dff6e3d0e0c @@ -119,7 +131,7 @@ SPEC CHECKSUMS: shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78 sqflite: 673a0e54cc04b7d6dba8d24fb8095b31c3a99eec tray_manager: 9064e219c56d75c476e46b9a21182087930baf90 - url_launcher_macos: 5f437abeda8c85500ceb03f5c1938a8c5a705399 + url_launcher_macos: c82c93949963e55b228a30115bd219499a6fe404 video_player_avfoundation: 7c6c11d8470e1675df7397027218274b6d2360b3 wakelock_plus: 4783562c9a43d209c458cb9b30692134af456269 webview_flutter_wkwebview: 0982481e3d9c78fd5c6f62a002fcd24fc791f1e4 diff --git a/pubspec.lock b/pubspec.lock index 8a40c85c1..6904f5792 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -483,6 +483,30 @@ packages: url: "https://pub.dev" source: hosted version: "4.0.0" + flutter_local_notifications: + dependency: "direct main" + description: + name: flutter_local_notifications + sha256: "49eeef364fddb71515bc78d5a8c51435a68bccd6e4d68e25a942c5e47761ae71" + url: "https://pub.dev" + source: hosted + version: "17.2.3" + flutter_local_notifications_linux: + dependency: transitive + description: + name: flutter_local_notifications_linux + sha256: c49bd06165cad9beeb79090b18cd1eb0296f4bf4b23b84426e37dd7c027fc3af + url: "https://pub.dev" + source: hosted + version: "4.0.1" + flutter_local_notifications_platform_interface: + dependency: transitive + description: + name: flutter_local_notifications_platform_interface + sha256: "85f8d07fe708c1bdcf45037f2c0109753b26ae077e9d9e899d55971711a4ea66" + url: "https://pub.dev" + source: hosted + version: "7.2.0" flutter_localizations: dependency: "direct main" description: flutter @@ -500,10 +524,10 @@ packages: dependency: "direct main" description: name: flutter_markdown - sha256: a23c41ee57573e62fc2190a1f36a0480c4d90bde3a8a8d7126e5d5992fb53fb7 + sha256: e17575ca576a34b46c58c91f9948891117a1bd97815d2e661813c7f90c647a78 url: "https://pub.dev" source: hosted - version: "0.7.3+1" + version: "0.7.3+2" flutter_plugin_android_lifecycle: dependency: transitive description: @@ -533,6 +557,14 @@ packages: description: flutter source: sdk version: "0.0.0" + flutter_timezone: + dependency: "direct main" + description: + name: flutter_timezone + sha256: ea53c61c9152f271a5e30624a624184804947b6a733ff2b64186bb2579446892 + url: "https://pub.dev" + source: hosted + version: "3.0.1" flutter_web_plugins: dependency: transitive description: flutter @@ -729,18 +761,18 @@ packages: dependency: transitive description: name: just_audio_web - sha256: b163878529d9b028c53a6972fcd58cae2405bcd11cbfcea620b6fb9f151429d6 + sha256: "9a98035b8b24b40749507687520ec5ab404e291d2b0937823ff45d92cb18d448" url: "https://pub.dev" source: hosted - version: "0.4.12" + version: "0.4.13" just_audio_windows: dependency: "direct main" description: name: just_audio_windows - sha256: "48ab2dec79cf6097550602fe07b1a644f341450e138dc8fdc23e42ce0ed2d928" + sha256: b1ba5305d841c0e3883644e20fc11aaa23f28cfdd43ec20236d1e119a402ef29 url: "https://pub.dev" source: hosted - version: "0.2.1" + version: "0.2.2" kana_kit: dependency: "direct main" description: @@ -817,10 +849,10 @@ packages: dependency: transitive description: name: mailer - sha256: "3b27d204ff92a20aba227c25bc6467e245b0f19f9fbbc83aa357a9b7fa40267f" + sha256: "21fde1497c79f402cb5fa7c50abd58927d360139e492546c941ee10767684fac" url: "https://pub.dev" source: hosted - version: "6.1.2" + version: "6.2.0" markdown: dependency: "direct main" description: @@ -930,7 +962,7 @@ packages: description: path: "packages/pasteboard" ref: HEAD - resolved-ref: bd5dbc30b71e825ba6d53d2d27f8a5796ea25802 + resolved-ref: "3e4247ba5b71049704395b70b5a827fb113b88ea" url: "https://github.com/MixinNetwork/flutter-plugins" source: git version: "0.3.0" @@ -1295,18 +1327,18 @@ packages: dependency: transitive description: name: sqflite - sha256: a43e5a27235518c03ca238e7b4732cf35eabe863a369ceba6cbefa537a66f16d + sha256: ff5a2436ef8ebdfda748fbfe957f9981524cb5ff11e7bafa8c42771840e8a788 url: "https://pub.dev" source: hosted - version: "2.3.3+1" + version: "2.3.3+2" sqflite_common: dependency: transitive description: name: sqflite_common - sha256: "7b41b6c3507854a159e24ae90a8e3e9cc01eb26a477c118d6dca065b5f55453e" + sha256: "2d8e607db72e9cb7748c9c6e739e2c9618320a5517de693d5a24609c4671b1a4" url: "https://pub.dev" source: hosted - version: "2.5.4+2" + version: "2.5.4+4" stack_trace: dependency: transitive description: @@ -1359,10 +1391,10 @@ packages: dependency: transitive description: name: synchronized - sha256: a824e842b8a054f91a728b783c177c1e4731f6b124f9192468457a8913371255 + sha256: "69fe30f3a8b04a0be0c15ae6490fc859a78ef4c43ae2dd5e8a623d45bfcf9225" url: "https://pub.dev" source: hosted - version: "3.2.0" + version: "3.3.0+3" table_sticky_headers: dependency: "direct main" description: @@ -1387,6 +1419,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.7.2" + timezone: + dependency: "direct main" + description: + name: timezone + sha256: "2236ec079a174ce07434e89fcd3fcda430025eb7692244139a9cf54fdcf1fc7d" + url: "https://pub.dev" + source: hosted + version: "0.9.4" timing: dependency: transitive description: @@ -1407,10 +1447,10 @@ packages: dependency: "direct main" description: name: tray_manager - sha256: c9a63fd88bd3546287a7eb8ccc978d707eef82c775397af17dda3a4f4c039e64 + sha256: bdc3ac6c36f3d12d871459e4a9822705ce5a1165a17fa837103bc842719bf3f7 url: "https://pub.dev" source: hosted - version: "0.2.3" + version: "0.2.4" tuple: dependency: "direct main" description: @@ -1479,10 +1519,10 @@ packages: dependency: transitive description: name: url_launcher_macos - sha256: "9a1a42d5d2d95400c795b2914c36fdcb525870c752569438e4ebb09a2b5d90de" + sha256: "769549c999acdb42b8bcfa7c43d72bf79a382ca7441ab18a808e101149daf672" url: "https://pub.dev" source: hosted - version: "3.2.0" + version: "3.2.1" url_launcher_platform_interface: dependency: transitive description: @@ -1511,10 +1551,10 @@ packages: dependency: "direct main" description: name: uuid - sha256: f33d6bb662f0e4f79dcd7ada2e6170f3b3a2530c28fc41f49a411ddedd576a77 + sha256: a5be9ef6618a7ac1e964353ef476418026db906c4facdedaa299b7a2e71690ff url: "https://pub.dev" source: hosted - version: "4.5.0" + version: "4.5.1" vector_graphics: dependency: transitive description: @@ -1559,10 +1599,10 @@ packages: dependency: transitive description: name: video_player_android - sha256: "38d8fe136c427abdce68b5e8c3c08ea29d7a794b453c7a51b12ecfad4aad9437" + sha256: "45d21bbba3d10b7182aa08ade5d4c68ed3367016d40f29732bb04a799b26842d" url: "https://pub.dev" source: hosted - version: "2.7.3" + version: "2.7.5" video_player_avfoundation: dependency: transitive description: @@ -1639,10 +1679,10 @@ packages: dependency: transitive description: name: web - sha256: d43c1d6b787bf0afad444700ae7f4db8827f701bc61c255ac8d328c6f4d52062 + sha256: cd3543bd5798f6ad290ea73d210f423502e71900302dde696f8bff84bf89a1cb url: "https://pub.dev" source: hosted - version: "1.0.0" + version: "1.1.0" web_socket: dependency: transitive description: @@ -1719,10 +1759,10 @@ packages: dependency: "direct main" description: name: worker_manager - sha256: bee503f785e6e3a6354a61197ab5a5dce563c228a5a5e89e878dd90e113a9ce3 + sha256: "334262e1b43dfb2097290098bbe8d868540b9dee484e4f0b5372b0d31d99efa1" url: "https://pub.dev" source: hosted - version: "7.1.0" + version: "7.2.1" xdg_directories: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 8903bbc27..dd67d68e5 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -54,10 +54,12 @@ dependencies: flutter_cache_manager: ^3.3.1 flutter_easyloading: ^3.0.5 flutter_hooks: ^0.20.1 + flutter_local_notifications: ^17.2.3 flutter_markdown: ^0.7.1 # flutter_qjs: ^0.3.7 flutter_js: ^0.8.1 flutter_svg: ^2.0.7 + flutter_timezone: ^3.0.1 font_awesome_flutter: ^10.6.0 github: ^9.16.0 hive: ^2.2.3 @@ -97,6 +99,7 @@ dependencies: smooth_page_indicator: ^1.1.0 string_validator: ^1.0.0 table_sticky_headers: ^2.0.1 + timezone: ^0.9.4 tray_manager: ^0.2.2 tuple: ^2.0.1 url_launcher: ^6.1.12