diff --git a/app/lib/controller/pref_controller.dart b/app/lib/controller/pref_controller.dart index 2f7536d9..fb36e01b 100644 --- a/app/lib/controller/pref_controller.dart +++ b/app/lib/controller/pref_controller.dart @@ -427,6 +427,9 @@ class PrefController { .map(PrefHomeCollectionsNavButton.fromJson) .toList()) ?? _homeCollectionsNavBarButtonsDefault); + @npSubjectAccessor + late final _isAutoUpdateCheckAvailableController = + BehaviorSubject.seeded(pref.isAutoUpdateCheckAvailableOr(false)); } extension PrefControllerExtension on PrefController { diff --git a/app/lib/controller/pref_controller.g.dart b/app/lib/controller/pref_controller.g.dart index 5da6d873..dc180940 100644 --- a/app/lib/controller/pref_controller.g.dart +++ b/app/lib/controller/pref_controller.g.dart @@ -266,6 +266,15 @@ extension $PrefControllerNpSubjectAccessor on PrefController { homeCollectionsNavBarButtons.distinct().skip(1); List get homeCollectionsNavBarButtonsValue => _homeCollectionsNavBarButtonsController.value; +// _isAutoUpdateCheckAvailableController + ValueStream get isAutoUpdateCheckAvailable => + _isAutoUpdateCheckAvailableController.stream; + Stream get isAutoUpdateCheckAvailableNew => + isAutoUpdateCheckAvailable.skip(1); + Stream get isAutoUpdateCheckAvailableChange => + isAutoUpdateCheckAvailable.distinct().skip(1); + bool get isAutoUpdateCheckAvailableValue => + _isAutoUpdateCheckAvailableController.value; } extension $SecurePrefControllerNpSubjectAccessor on SecurePrefController { diff --git a/app/lib/entity/pref.dart b/app/lib/entity/pref.dart index 11a35d9a..386ccf42 100644 --- a/app/lib/entity/pref.dart +++ b/app/lib/entity/pref.dart @@ -120,6 +120,9 @@ enum PrefKey implements PrefKeyInterface { homeCollectionsNavBarButtons, lastDonationDialogTime, shouldRemindDonationLater, + lastAutoUpdateCheckTime, + isAutoUpdateCheckAvailable, + isEnableAutoUpdateCheck, ; @override @@ -224,6 +227,12 @@ enum PrefKey implements PrefKeyInterface { return "lastDonationDialogTime"; case PrefKey.shouldRemindDonationLater: return "shouldRemindDonationLater"; + case PrefKey.lastAutoUpdateCheckTime: + return "lastAutoUpdateCheckTime"; + case PrefKey.isAutoUpdateCheckAvailable: + return "isAutoUpdateCheckAvailable"; + case PrefKey.isEnableAutoUpdateCheck: + return "isEnableAutoUpdateCheck"; } } } diff --git a/app/lib/entity/pref/extension.dart b/app/lib/entity/pref/extension.dart index 5b46ec75..4477a711 100644 --- a/app/lib/entity/pref/extension.dart +++ b/app/lib/entity/pref/extension.dart @@ -183,6 +183,33 @@ extension PrefExtension on Pref { PrefKey.shouldRemindDonationLater, value, (key, value) => provider.setBool(key, value)); + + int? getLastAutoUpdateCheckTime() => + provider.getInt(PrefKey.lastAutoUpdateCheckTime); + int getLastAutoUpdateCheckTimeOr(int def) => + getLastAutoUpdateCheckTime() ?? def; + Future setLastAutoUpdateCheckTime(int value) => _set( + PrefKey.lastAutoUpdateCheckTime, + value, + (key, value) => provider.setInt(key, value)); + + bool? isAutoUpdateCheckAvailable() => + provider.getBool(PrefKey.isAutoUpdateCheckAvailable); + bool isAutoUpdateCheckAvailableOr([bool def = false]) => + isAutoUpdateCheckAvailable() ?? def; + Future setIsAutoUpdateCheckAvailable(bool value) => _set( + PrefKey.isAutoUpdateCheckAvailable, + value, + (key, value) => provider.setBool(key, value)); + + bool? isEnableAutoUpdateCheck() => + provider.getBool(PrefKey.isEnableAutoUpdateCheck); + bool isEnableAutoUpdateCheckOr([bool def = true]) => + isEnableAutoUpdateCheck() ?? def; + Future setIsEnableAutoUpdateCheck(bool value) => _set( + PrefKey.isEnableAutoUpdateCheck, + value, + (key, value) => provider.setBool(key, value)); } extension AccountPrefExtension on AccountPref { diff --git a/app/lib/update_checker.dart b/app/lib/update_checker.dart index facc088f..6bb47d91 100644 --- a/app/lib/update_checker.dart +++ b/app/lib/update_checker.dart @@ -1,8 +1,11 @@ +import 'dart:async'; import 'dart:convert'; import 'package:http/http.dart' as http; import 'package:logging/logging.dart'; +import 'package:nc_photos/entity/pref.dart'; import 'package:nc_photos/k.dart' as k; +import 'package:nc_photos/object_extension.dart'; enum UpdateCheckerResult { updateAvailable, @@ -51,3 +54,40 @@ class UpdateChecker { static final _log = Logger("update_checker.UpdateChecker"); } + +class AutoUpdateChecker { + const AutoUpdateChecker(); + + Future call() async { + try { + if (Pref().isAutoUpdateCheckAvailableOr()) { + return; + } + + final prev = Pref().getLastAutoUpdateCheckTime()?.run( + (obj) => DateTime.fromMillisecondsSinceEpoch(obj).toUtc()) ?? + DateTime(0); + final now = DateTime.now().toUtc(); + if (now.isAfter(prev) && now.difference(prev) > const Duration(days: 7)) { + unawaited( + Pref().setLastAutoUpdateCheckTime(now.millisecondsSinceEpoch), + ); + await _check(); + } + } catch (e, stackTrace) { + _log.severe("[call] Exception", e, stackTrace); + } + } + + Future _check() async { + _log.info("[_check] Auto update check"); + final checker = UpdateChecker(); + final result = await checker(); + if (result == UpdateCheckerResult.updateAvailable) { + _log.info("[_check] New update available"); + unawaited(Pref().setIsAutoUpdateCheckAvailable(true)); + } + } + + static final _log = Logger("update_checker.AutoUpdateChecker"); +} diff --git a/app/lib/widget/home_app_bar.dart b/app/lib/widget/home_app_bar.dart index be705374..a1ada04c 100644 --- a/app/lib/widget/home_app_bar.dart +++ b/app/lib/widget/home_app_bar.dart @@ -5,6 +5,7 @@ import 'package:nc_photos/account.dart'; import 'package:nc_photos/api/api_util.dart' as api_util; import 'package:nc_photos/controller/account_controller.dart'; import 'package:nc_photos/controller/account_pref_controller.dart'; +import 'package:nc_photos/controller/pref_controller.dart'; import 'package:nc_photos/stream_util.dart'; import 'package:nc_photos/theme.dart'; import 'package:nc_photos/widget/account_picker_dialog.dart'; @@ -53,15 +54,48 @@ class HomeSliverAppBar extends StatelessWidget { } }, ), - _ProfileIconView( - account: account, - isProcessing: isShowProgressIcon, - onTap: () { - showDialog( - context: context, - builder: (_) => const AccountPickerDialog(), - ); - }, + Stack( + fit: StackFit.passthrough, + children: [ + _ProfileIconView( + account: account, + isProcessing: isShowProgressIcon, + onTap: () { + showDialog( + context: context, + builder: (_) => const AccountPickerDialog(), + ); + }, + ), + ValueStreamBuilder( + stream: context.read().isAutoUpdateCheckAvailable, + builder: (context, snapshot) { + final isAutoUpdateCheckAvailable = snapshot.requireData; + if (isAutoUpdateCheckAvailable) { + return Positioned.directional( + textDirection: Directionality.of(context), + end: 0, + top: 0, + child: Container( + width: 12, + height: 12, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8), + color: Colors.red, + ), + child: const Icon( + Icons.upload, + color: Colors.white, + size: 12, + ), + ), + ); + } else { + return const SizedBox.shrink(); + } + }, + ), + ], ), const SizedBox(width: 8), ], diff --git a/app/lib/widget/settings.dart b/app/lib/widget/settings.dart index 6d74b8f3..df543f54 100644 --- a/app/lib/widget/settings.dart +++ b/app/lib/widget/settings.dart @@ -6,6 +6,7 @@ import 'package:logging/logging.dart'; import 'package:nc_photos/app_localizations.dart'; import 'package:nc_photos/controller/pref_controller.dart'; import 'package:nc_photos/debug_util.dart'; +import 'package:nc_photos/entity/pref.dart'; import 'package:nc_photos/help_utils.dart' as help_util; import 'package:nc_photos/k.dart' as k; import 'package:nc_photos/language_util.dart' as language_util; @@ -13,6 +14,7 @@ import 'package:nc_photos/mobile/platform.dart' if (dart.library.html) 'package:nc_photos/web/platform.dart' as platform; import 'package:nc_photos/platform/features.dart' as features; import 'package:nc_photos/platform/notification.dart'; +import 'package:nc_photos/snack_bar_manager.dart'; import 'package:nc_photos/stream_util.dart'; import 'package:nc_photos/url_launcher_util.dart'; import 'package:nc_photos/widget/list_tile_center_leading.dart'; @@ -48,6 +50,12 @@ class Settings extends StatefulWidget { @npLog class _SettingsState extends State { + @override + void initState() { + super.initState(); + _isEnableAutoUpdateCheck = Pref().isEnableAutoUpdateCheckOr(); + } + @override Widget build(BuildContext context) { final translator = L10n.global().translator; @@ -148,11 +156,34 @@ class _SettingsState extends State { }, ), ListTile( + trailing: Pref().isAutoUpdateCheckAvailableOr() + ? Stack( + fit: StackFit.passthrough, + children: [ + const Icon(Icons.upload), + Positioned.directional( + textDirection: Directionality.of(context), + end: 0, + top: 0, + child: const Icon( + Icons.circle, + color: Colors.red, + size: 8, + ), + ), + ], + ) + : null, title: const Text("Check for updates"), onTap: () { Navigator.of(context).pushNamed(UpdateChecker.routeName); }, ), + SwitchListTile( + title: const Text("Check for updates automatically"), + value: _isEnableAutoUpdateCheck, + onChanged: _onEnableAutoUpdateCheckChanged, + ), ListTile( title: Text(L10n.global().settingsSourceCodeTitle), onTap: () { @@ -237,6 +268,37 @@ class _SettingsState extends State { } } + Future _onEnableAutoUpdateCheckChanged(bool value) async { + _log.info("[_onEnableAutoUpdateCheckChanged] New value: $value"); + final oldValue = _isEnableAutoUpdateCheck; + setState(() { + _isEnableAutoUpdateCheck = value; + }); + if (!await Pref().setIsEnableAutoUpdateCheck(value)) { + _log.severe("[_onEnableAutoUpdateCheckChanged] Failed writing pref"); + SnackBarManager().showSnackBar(SnackBar( + content: Text(L10n.global().writePreferenceFailureNotification), + duration: k.snackBarDurationNormal, + )); + setState(() { + _isEnableAutoUpdateCheck = oldValue; + }); + } else { + if (!value) { + // reset state after disabling + if (mounted) { + setState(() { + Pref() + ..setIsAutoUpdateCheckAvailable(false) + ..setLastAutoUpdateCheckTime(0); + }); + } + } + } + } + + late bool _isEnableAutoUpdateCheck; + var _devSettingsUnlockCount = 3; var _isShowDevSettings = false; diff --git a/app/lib/widget/splash.dart b/app/lib/widget/splash.dart index 6773f644..b448bbcb 100644 --- a/app/lib/widget/splash.dart +++ b/app/lib/widget/splash.dart @@ -9,10 +9,12 @@ import 'package:nc_photos/app_localizations.dart'; import 'package:nc_photos/bloc_util.dart'; import 'package:nc_photos/controller/pref_controller.dart'; import 'package:nc_photos/db/entity_converter.dart'; +import 'package:nc_photos/entity/pref.dart'; import 'package:nc_photos/k.dart' as k; import 'package:nc_photos/mobile/android/activity.dart'; import 'package:nc_photos/mobile/android/permission_util.dart'; import 'package:nc_photos/protected_page_handler.dart'; +import 'package:nc_photos/update_checker.dart'; import 'package:nc_photos/use_case/compat/v29.dart'; import 'package:nc_photos/use_case/compat/v46.dart'; import 'package:nc_photos/use_case/compat/v55.dart'; diff --git a/app/lib/widget/splash/bloc.dart b/app/lib/widget/splash/bloc.dart index 071f69e9..14377ff3 100644 --- a/app/lib/widget/splash/bloc.dart +++ b/app/lib/widget/splash/bloc.dart @@ -28,6 +28,9 @@ class _Bloc extends Bloc<_Event, _State> with BlocLogger { _initFirstRun(), _migrateApp(emit), ]); + if (Pref().isEnableAutoUpdateCheckOr()) { + unawaited(const AutoUpdateChecker()()); + } emit(state.copyWith(isDone: true)); } @@ -50,6 +53,7 @@ class _Bloc extends Bloc<_Event, _State> with BlocLogger { Future _migrateApp(Emitter<_State> emit) async { if (_shouldUpgrade()) { + unawaited(Pref().setIsAutoUpdateCheckAvailable(false)); await _handleUpgrade(emit); } }