diff --git a/assets/translations/strings_en.i18n.json b/assets/translations/strings_en.i18n.json index 23451f35..444c0c7e 100644 --- a/assets/translations/strings_en.i18n.json +++ b/assets/translations/strings_en.i18n.json @@ -73,8 +73,11 @@ "update": { "buttonTxt": "Update", "tooltip": "Update Profile", + "updateSubscriptions": "Update Subscriptions", "failureMsg": "Failed to update profile", - "successMsg": "Profile updated successfully" + "successMsg": "Profile updated successfully", + "namedFailureMsg": "Failed to update \"${name}\"", + "namedSuccessMsg": "\"${name}\" updated successfully" }, "share": { "buttonText": "Share", diff --git a/assets/translations/strings_fa.i18n.json b/assets/translations/strings_fa.i18n.json index e154af38..bf6d0aa5 100644 --- a/assets/translations/strings_fa.i18n.json +++ b/assets/translations/strings_fa.i18n.json @@ -74,7 +74,10 @@ "buttonTxt": "بروزرسانی", "tooltip": "بروزرسانی پروفایل", "failureMsg": "در بروزرسانی پروفایل خطایی رخ داد", - "successMsg": "پروفایل با موفقیت بروزرسانی شد" + "successMsg": "پروفایل با موفقیت بروزرسانی شد", + "namedFailureMsg": "در بروزرسانی \"${name}\" خطایی رخ داد", + "namedSuccessMsg": "\"${name}\" با موفقیت به روز شد", + "updateSubscriptions": "بروزرسانی اشتراک‌ها" }, "share": { "buttonText": "اشتراک گذاری", diff --git a/assets/translations/strings_ru.i18n.json b/assets/translations/strings_ru.i18n.json index 97216be3..380dbeb0 100644 --- a/assets/translations/strings_ru.i18n.json +++ b/assets/translations/strings_ru.i18n.json @@ -74,7 +74,10 @@ "buttonTxt": "Обновить", "tooltip": "Обновить профиль", "failureMsg": "Не удалось обновить профиль", - "successMsg": "Профиль успешно обновлён" + "successMsg": "Профиль успешно обновлён", + "namedFailureMsg": "Не удалось обновить \"${name}\".", + "namedSuccessMsg": "\"${name}\" успешно обновлено", + "updateSubscriptions": "Обновить подписки" }, "share": { "buttonText": "Поделиться", diff --git a/assets/translations/strings_tr.i18n.json b/assets/translations/strings_tr.i18n.json index 0f99746c..cbd2df77 100644 --- a/assets/translations/strings_tr.i18n.json +++ b/assets/translations/strings_tr.i18n.json @@ -74,7 +74,10 @@ "buttonTxt": "Güncelle", "tooltip": "Profili Güncelle", "failureMsg": "Profil güncellenemedi", - "successMsg": "Profil başarıyla güncellendi" + "successMsg": "Profil başarıyla güncellendi", + "namedFailureMsg": "\"${name}\" güncellenemedi", + "namedSuccessMsg": "\"${name}\" başarıyla güncellendi", + "updateSubscriptions": "Abonelikleri Güncelle" }, "share": { "buttonText": "Paylaş", diff --git a/assets/translations/strings_zh-CN.i18n.json b/assets/translations/strings_zh-CN.i18n.json index 108259ca..702765bf 100644 --- a/assets/translations/strings_zh-CN.i18n.json +++ b/assets/translations/strings_zh-CN.i18n.json @@ -74,7 +74,10 @@ "buttonTxt": "更新", "tooltip": "更新配置文件", "failureMsg": "更新配置文件失败", - "successMsg": "配置文件更新成功" + "successMsg": "配置文件更新成功", + "namedFailureMsg": "无法更新\"${name}\"", + "namedSuccessMsg": "\"${name}\" 更新成功", + "updateSubscriptions": "更新订阅" }, "share": { "buttonText": "分享", diff --git a/lib/features/profile/notifier/profiles_update_notifier.dart b/lib/features/profile/notifier/profiles_update_notifier.dart index 55b7ed51..ff5388ce 100644 --- a/lib/features/profile/notifier/profiles_update_notifier.dart +++ b/lib/features/profile/notifier/profiles_update_notifier.dart @@ -10,6 +10,8 @@ import 'package:riverpod_annotation/riverpod_annotation.dart'; part 'profiles_update_notifier.g.dart'; +typedef ProfileUpdateStatus = ({String name, bool success}); + @Riverpod(keepAlive: true) class ForegroundProfilesUpdateNotifier extends _$ForegroundProfilesUpdateNotifier with AppLogger { @@ -17,9 +19,9 @@ class ForegroundProfilesUpdateNotifier static const interval = Duration(minutes: 15); @override - Future build() async { + Stream build() { var cycleCount = 0; - final scheduler = NeatPeriodicTaskScheduler( + _scheduler = NeatPeriodicTaskScheduler( name: 'profiles update worker', interval: interval, timeout: const Duration(minutes: 5), @@ -30,30 +32,51 @@ class ForegroundProfilesUpdateNotifier ); ref.onDispose(() async { - await scheduler.stop(); + await _scheduler?.stop(); + _scheduler = null; }); if (ref.watch(introCompletedProvider)) { loggy.debug("intro done, starting"); - return scheduler.start(); + _scheduler?.start(); } else { loggy.debug("intro in process, skipping"); } + return const Stream.empty(); + } + + NeatPeriodicTaskScheduler? _scheduler; + bool _forceNextRun = false; + + Future trigger() async { + loggy.debug("triggering update"); + _forceNextRun = true; + await _scheduler?.trigger(); } @visibleForTesting Future updateProfiles() async { + var force = false; + if (_forceNextRun) { + force = true; + _forceNextRun = false; + } + try { final previousRun = DateTime.tryParse( ref.read(sharedPreferencesProvider).requireValue.getString(prefKey) ?? "", ); - if (previousRun != null && previousRun.add(interval) > DateTime.now()) { + if (!force && + previousRun != null && + previousRun.add(interval) > DateTime.now()) { loggy.debug("too soon! previous run: [$previousRun]"); return; } - loggy.debug("running, previous run: [$previousRun]"); + loggy.debug( + "${force ? "[FORCED] " : ""}running, previous run: [$previousRun]", + ); final remoteProfiles = await ref .read(profileRepositoryProvider) @@ -69,20 +92,25 @@ class ForegroundProfilesUpdateNotifier await for (final profile in Stream.fromIterable(remoteProfiles)) { final updateInterval = profile.options?.updateInterval; - if (updateInterval != null && - updateInterval <= DateTime.now().difference(profile.lastUpdate)) { + if (force || + updateInterval != null && + updateInterval <= + DateTime.now().difference(profile.lastUpdate)) { await ref .read(profileRepositoryProvider) .requireValue .updateSubscription(profile) .mapLeft( - (l) => loggy.debug("error updating profile [${profile.id}]", l), - ) - .map( - (_) => - loggy.debug("profile [${profile.id}] updated successfully"), - ) - .run(); + (l) { + loggy.debug("error updating profile [${profile.id}]", l); + state = AsyncData((name: profile.name, success: false)); + }, + ).map( + (_) { + loggy.debug("profile [${profile.id}] updated successfully"); + state = AsyncData((name: profile.name, success: true)); + }, + ).run(); } else { loggy.debug( "skipping profile [${profile.id}] update. last successful update: [${profile.lastUpdate}] - interval: [${profile.options?.updateInterval}]", diff --git a/lib/features/profile/overview/profiles_overview_page.dart b/lib/features/profile/overview/profiles_overview_page.dart index 3e875b13..07231312 100644 --- a/lib/features/profile/overview/profiles_overview_page.dart +++ b/lib/features/profile/overview/profiles_overview_page.dart @@ -2,8 +2,10 @@ import 'package:flutter/material.dart'; import 'package:gap/gap.dart'; import 'package:hiddify/core/localization/translations.dart'; import 'package:hiddify/core/model/failures.dart'; +import 'package:hiddify/core/notification/in_app_notification_controller.dart'; import 'package:hiddify/core/router/router.dart'; import 'package:hiddify/features/profile/model/profile_sort_enum.dart'; +import 'package:hiddify/features/profile/notifier/profiles_update_notifier.dart'; import 'package:hiddify/features/profile/overview/profiles_overview_notifier.dart'; import 'package:hiddify/features/profile/widget/profile_tile.dart'; import 'package:hiddify/utils/placeholders.dart'; @@ -22,6 +24,25 @@ class ProfilesOverviewModal extends HookConsumerWidget { final t = ref.watch(translationsProvider); final asyncProfiles = ref.watch(profilesOverviewNotifierProvider); + ref.listen( + foregroundProfilesUpdateNotifierProvider, + (_, next) { + if (next case AsyncData(:final value?)) { + final t = ref.read(translationsProvider); + final notification = ref.read(inAppNotificationControllerProvider); + if (value.success) { + notification.showSuccessToast( + t.profile.update.namedSuccessMsg(name: value.name), + ); + } else { + notification.showErrorToast( + t.profile.update.namedFailureMsg(name: value.name), + ); + } + } + }, + ); + return Stack( children: [ CustomScrollView( @@ -47,29 +68,44 @@ class ProfilesOverviewModal extends HookConsumerWidget { Positioned.fill( child: Align( alignment: Alignment.bottomCenter, - child: ButtonBar( - alignment: MainAxisAlignment.center, - children: [ - FilledButton.icon( - onPressed: () { - const AddProfileRoute().push(context); - }, - icon: const Icon(Icons.add), - label: Text(t.profile.add.shortBtnTxt), - ), - FilledButton.icon( - onPressed: () { - showDialog( - context: context, - builder: (context) { - return const ProfilesSortModal(); - }, - ); - }, - icon: const Icon(Icons.sort), - label: Text(t.general.sort), - ), - ], + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 8), + child: Wrap( + alignment: WrapAlignment.center, + spacing: 8, + children: [ + FilledButton.icon( + onPressed: () { + const AddProfileRoute().push(context); + }, + icon: const Icon(Icons.add), + label: Text(t.profile.add.shortBtnTxt), + ), + FilledButton.icon( + onPressed: () { + showDialog( + context: context, + builder: (context) { + return const ProfilesSortModal(); + }, + ); + }, + icon: const Icon(Icons.sort), + label: Text(t.general.sort), + ), + FilledButton.icon( + onPressed: () async { + await ref + .read( + foregroundProfilesUpdateNotifierProvider.notifier, + ) + .trigger(); + }, + icon: const Icon(Icons.update), + label: Text(t.profile.update.updateSubscriptions), + ), + ], + ), ), ), ),