diff --git a/app/lib/controller/pref_controller.dart b/app/lib/controller/pref_controller.dart index 418eb40b..7d95e4dd 100644 --- a/app/lib/controller/pref_controller.dart +++ b/app/lib/controller/pref_controller.dart @@ -9,8 +9,8 @@ import 'package:nc_photos/di_container.dart'; import 'package:nc_photos/entity/collection/util.dart'; import 'package:nc_photos/entity/pref.dart'; import 'package:nc_photos/json_util.dart'; +import 'package:nc_photos/k.dart' as k; import 'package:nc_photos/language_util.dart'; -import 'package:nc_photos/object_extension.dart'; import 'package:nc_photos/protected_page_handler.dart'; import 'package:nc_photos/size.dart'; import 'package:np_codegen/np_codegen.dart'; @@ -32,6 +32,13 @@ class PrefController { value: value, ); + Future setCurrentAccountIndex(int value) => _setOrRemove( + controller: _currentAccountIndexController, + setter: (pref, value) => pref.setCurrentAccountIndex(value), + remover: (pref) => pref.setCurrentAccountIndex(null), + value: value, + ); + Future setAppLanguage(AppLanguage value) => _set( controller: _languageController, setter: (pref, value) => pref.setLanguage(value.langId), @@ -176,6 +183,20 @@ class PrefController { value: value, ); + Future setFirstRunTime(DateTime? value) => _setOrRemove( + controller: _firstRunTimeController, + setter: (pref, value) => + pref.setFirstRunTime(value.millisecondsSinceEpoch), + remover: (pref) => pref.setFirstRunTime(null), + value: value, + ); + + Future setLastVersion(int value) => _set( + controller: _lastVersionController, + setter: (pref, value) => pref.setLastVersion(value), + value: value, + ); + Future _set({ required BehaviorSubject controller, required Future Function(Pref pref, T value) setter, @@ -217,6 +238,9 @@ class PrefController { late final _accountsController = BehaviorSubject.seeded(_c.pref.getAccounts3() ?? []); @npSubjectAccessor + late final _currentAccountIndexController = + BehaviorSubject.seeded(_c.pref.getCurrentAccountIndex()); + @npSubjectAccessor late final _languageController = BehaviorSubject.seeded(_langIdToAppLanguage(_c.pref.getLanguageOr(0))); @npSubjectAccessor @@ -284,6 +308,25 @@ class PrefController { @npSubjectAccessor late final _isNewHttpEngineController = BehaviorSubject.seeded(_c.pref.isNewHttpEngine() ?? false); + @npSubjectAccessor + late final _firstRunTimeController = BehaviorSubject.seeded(_c.pref + .getFirstRunTime() + ?.let((v) => DateTime.fromMillisecondsSinceEpoch(v).toUtc())); + @npSubjectAccessor + late final _lastVersionController = + BehaviorSubject.seeded(_c.pref.getLastVersion() ?? + // v6 is the last version without saving the version number in pref + (_c.pref.getSetupProgress() == null ? k.version : 6)); +} + +extension PrefControllerExtension on PrefController { + Account? get currentAccountValue { + try { + return accountsValue[currentAccountIndexValue!]; + } catch (_) { + return null; + } + } } @npSubjectAccessor diff --git a/app/lib/controller/pref_controller.g.dart b/app/lib/controller/pref_controller.g.dart index a907f513..769cef33 100644 --- a/app/lib/controller/pref_controller.g.dart +++ b/app/lib/controller/pref_controller.g.dart @@ -23,6 +23,13 @@ extension $PrefControllerNpSubjectAccessor on PrefController { Stream> get accountsNew => accounts.skip(1); Stream> get accountsChange => accounts.distinct().skip(1); List get accountsValue => _accountsController.value; +// _currentAccountIndexController + ValueStream get currentAccountIndex => + _currentAccountIndexController.stream; + Stream get currentAccountIndexNew => currentAccountIndex.skip(1); + Stream get currentAccountIndexChange => + currentAccountIndex.distinct().skip(1); + int? get currentAccountIndexValue => _currentAccountIndexController.value; // _languageController ValueStream get language => _languageController.stream; Stream get languageNew => language.skip(1); @@ -171,6 +178,16 @@ extension $PrefControllerNpSubjectAccessor on PrefController { Stream get isNewHttpEngineNew => isNewHttpEngine.skip(1); Stream get isNewHttpEngineChange => isNewHttpEngine.distinct().skip(1); bool get isNewHttpEngineValue => _isNewHttpEngineController.value; +// _firstRunTimeController + ValueStream get firstRunTime => _firstRunTimeController.stream; + Stream get firstRunTimeNew => firstRunTime.skip(1); + Stream get firstRunTimeChange => firstRunTime.distinct().skip(1); + DateTime? get firstRunTimeValue => _firstRunTimeController.value; +// _lastVersionController + ValueStream get lastVersion => _lastVersionController.stream; + Stream get lastVersionNew => lastVersion.skip(1); + Stream get lastVersionChange => lastVersion.distinct().skip(1); + int get lastVersionValue => _lastVersionController.value; } extension $SecurePrefControllerNpSubjectAccessor on SecurePrefController { diff --git a/app/lib/controller/pref_controller/util.dart b/app/lib/controller/pref_controller/util.dart index 7408a20b..7398d0e5 100644 --- a/app/lib/controller/pref_controller/util.dart +++ b/app/lib/controller/pref_controller/util.dart @@ -101,6 +101,15 @@ extension on Pref { Future setNewHttpEngine(bool value) => provider.setBool(PrefKey.isNewHttpEngine, value); + + int? getFirstRunTime() => provider.getInt(PrefKey.firstRunTime); + Future setFirstRunTime(int? value) { + if (value == null) { + return provider.remove(PrefKey.firstRunTime); + } else { + return provider.setInt(PrefKey.firstRunTime, value); + } + } } MapCoord? _tryMapCoordFromJson(dynamic json) { diff --git a/app/lib/entity/pref/extension.dart b/app/lib/entity/pref/extension.dart index 7c7f37f2..58628546 100644 --- a/app/lib/entity/pref/extension.dart +++ b/app/lib/entity/pref/extension.dart @@ -161,11 +161,6 @@ extension PrefExtension on Pref { value, (key, value) => provider.setBool(key, value)); - int? getFirstRunTime() => provider.getInt(PrefKey.firstRunTime); - int getFirstRunTimeOr(int def) => getFirstRunTime() ?? def; - Future setFirstRunTime(int value) => _set( - PrefKey.firstRunTime, value, (key, value) => provider.setInt(key, value)); - bool? shouldProcessExifWifiOnly() => provider.getBool(PrefKey.shouldProcessExifWifiOnly); bool shouldProcessExifWifiOnlyOr([bool def = true]) => diff --git a/app/lib/use_case/compat/v46.dart b/app/lib/use_case/compat/v46.dart index 4a13c7b5..73321fbc 100644 --- a/app/lib/use_case/compat/v46.dart +++ b/app/lib/use_case/compat/v46.dart @@ -1,6 +1,6 @@ import 'package:logging/logging.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:np_codegen/np_codegen.dart'; import 'package:np_db/np_db.dart'; @@ -8,9 +8,10 @@ part 'v46.g.dart'; @npLog class CompatV46 { - static Future insertDbAccounts(Pref pref, NpDb db) async { + static Future insertDbAccounts( + PrefController prefController, NpDb db) async { _log.info("[insertDbAccounts] Insert current accounts to Sqlite database"); - final accounts = pref.getAccounts3Or([]); + final accounts = prefController.accountsValue; await db.addAccounts(accounts.toDb()); } diff --git a/app/lib/widget/splash.dart b/app/lib/widget/splash.dart index c347c82d..6f6dd25f 100644 --- a/app/lib/widget/splash.dart +++ b/app/lib/widget/splash.dart @@ -1,13 +1,14 @@ import 'dart:async'; import 'package:clock/clock.dart'; +import 'package:copy_with/copy_with.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:kiwi/kiwi.dart'; import 'package:logging/logging.dart'; 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/di_container.dart'; import 'package:nc_photos/entity/pref.dart'; import 'package:nc_photos/k.dart' as k; import 'package:nc_photos/mobile/android/activity.dart'; @@ -28,120 +29,105 @@ import 'package:np_platform_util/np_platform_util.dart'; import 'package:to_string/to_string.dart'; part 'splash.g.dart'; +part 'splash/bloc.dart'; +part 'splash/state_event.dart'; +part 'splash/view.dart'; -class Splash extends StatefulWidget { +class Splash extends StatelessWidget { static const routeName = "/splash"; const Splash({super.key}); @override - createState() => _SplashState(); + Widget build(BuildContext context) { + return BlocProvider( + create: (context) => _Bloc( + prefController: context.read(), + npDb: context.read(), + )..add(const _Init()), + child: const _WrappedSplash(), + ); + } } @npLog -class _SplashState extends State { - @override - void initState() { - super.initState(); - WidgetsBinding.instance.addPostFrameCallback((_) { - _doWork(); - }); - } - - Future _doWork() async { - if (!await Permission.hasPostNotifications()) { - await requestPostNotificationsForResult(); - } - if (Pref().getFirstRunTime() == null) { - await Pref().setFirstRunTime(clock.now().millisecondsSinceEpoch); - } - if (_shouldUpgrade()) { - setState(() { - _isUpgrading = true; - }); - await _handleUpgrade(); - setState(() { - _isUpgrading = false; - }); - } - _exit(); - } +class _WrappedSplash extends StatelessWidget { + const _WrappedSplash(); @override - build(BuildContext context) { - return Scaffold( - body: PopScope( - canPop: false, - child: Builder(builder: (context) => _buildContent(context)), - ), - ); - } - - Widget _buildContent(BuildContext context) { - return Padding( - padding: const EdgeInsets.symmetric(horizontal: 16), - child: Stack( - fit: StackFit.expand, - children: [ - Center( - child: Column( - crossAxisAlignment: CrossAxisAlignment.center, - mainAxisAlignment: MainAxisAlignment.center, + Widget build(BuildContext context) { + return PopScope( + canPop: false, + child: Scaffold( + body: MultiBlocListener( + listeners: [ + _BlocListenerT( + selector: (state) => state.changelogFromVersion, + listener: (context, changelogFromVersion) { + if (changelogFromVersion != null) { + Navigator.of(context) + .pushNamed(Changelog.routeName, + arguments: ChangelogArguments(changelogFromVersion)) + .whenComplete(() { + if (context.mounted) { + context.addEvent(const _ChangelogDismissed()); + } + }); + } + }, + ), + _BlocListenerT( + selector: (state) => state.isDone, + listener: (context, isDone) { + if (isDone) { + _exit(context); + } + }, + ), + ], + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Stack( + fit: StackFit.expand, children: [ - Icon( - Icons.cloud, - size: 96, - color: Theme.of(context).colorScheme.primary, + Column( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.cloud, + size: 96, + color: Theme.of(context).colorScheme.primary, + ), + const SizedBox(height: 8), + Text( + L10n.global().appTitle, + textAlign: TextAlign.center, + style: Theme.of(context).textTheme.headlineMedium, + ), + ], ), - const SizedBox(height: 8), - Text( - L10n.global().appTitle, - textAlign: TextAlign.center, - style: Theme.of(context).textTheme.headlineMedium, + const Positioned( + left: 0, + right: 0, + bottom: 64, + child: _UpgradeProgressView(), ), ], ), ), - if (_isUpgrading) - BlocBuilder<_UpgradeCubit, _UpgradeState>( - bloc: _upgradeCubit, - builder: (context, state) { - return Positioned( - left: 0, - right: 0, - bottom: 64, - child: Column( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Text(state.text), - const SizedBox(height: 8), - if (state.count == null) - const SizedBox( - width: 24, - height: 24, - child: AppIntermediateCircularProgressIndicator(), - ) - else - LinearProgressIndicator( - value: state.current / state.count!, - ), - ], - ), - ); - }, - ), - ], + ), ), ); } - void _exit() { + void _exit(BuildContext context) { _log.info("[_exit]"); - final account = Pref().getCurrentAccount(); + final account = context.read().currentAccountValue; if (isNeedSetup()) { - Navigator.pushReplacementNamed(context, Setup.routeName); + Navigator.of(context).pushReplacementNamed(Setup.routeName); } else if (account == null) { - Navigator.pushReplacementNamed(context, SignIn.routeName); + Navigator.of(context).pushReplacementNamed(SignIn.routeName); } else { Navigator.of(context) .pushReplacementNamedProtected(Home.routeName, @@ -150,159 +136,28 @@ class _SplashState extends State { if (getRawPlatform() == NpPlatform.android) { final initialRoute = await Activity.consumeInitialRoute(); if (initialRoute != null) { - unawaited(Navigator.pushNamed(context, initialRoute)); + unawaited(Navigator.of(context).pushNamed(initialRoute)); } } }).onError((_, __) async { _log.warning("[_exit] Auth failed"); await Future.delayed(const Duration(seconds: 2)); - _exit(); + if (context.mounted) { + _exit(context); + } return null; }); } } - - bool _shouldUpgrade() { - final lastVersion = Pref().getLastVersionOr(k.version); - return lastVersion < k.version; - } - - Future _handleUpgrade() async { - try { - final lastVersion = Pref().getLastVersionOr(k.version); - unawaited(_showChangelogIfAvailable(lastVersion)); - // begin upgrade while showing the changelog - try { - _log.info("[_handleUpgrade] Upgrade: $lastVersion -> ${k.version}"); - await _upgrade(lastVersion); - _log.info("[_handleUpgrade] Upgrade done"); - } finally { - // ensure user has closed the changelog - await _changelogCompleter.future; - } - } catch (e, stackTrace) { - _log.shout("[_handleUpgrade] Failed while upgrade", e, stackTrace); - } finally { - await Pref().setLastVersion(k.version); - } - } - - Future _upgrade(int lastVersion) async { - if (lastVersion < 290) { - await _upgrade29(lastVersion); - } - if (lastVersion < 460) { - await _upgrade46(lastVersion); - } - if (lastVersion < 550) { - await _upgrade55(lastVersion); - } - } - - Future _upgrade29(int lastVersion) async { - try { - _log.info("[_upgrade29] clearDefaultCache"); - await CompatV29.clearDefaultCache(); - } catch (e, stackTrace) { - _log.shout("[_upgrade29] Failed while clearDefaultCache", e, stackTrace); - // just leave the cache then - } - } - - Future _upgrade46(int lastVersion) async { - try { - _log.info("[_upgrade46] insertDbAccounts"); - final c = KiwiContainer().resolve(); - await CompatV46.insertDbAccounts(c.pref, context.read()); - } catch (e, stackTrace) { - _log.shout("[_upgrade46] Failed while clearDefaultCache", e, stackTrace); - unawaited(Pref().setAccounts3(null)); - unawaited(Pref().setCurrentAccountIndex(null)); - await showDialog( - context: context, - builder: (context) => AlertDialog( - title: const Text("Error"), - content: const Text( - "Failed upgrading app, please sign in to your servers again"), - actions: [ - TextButton( - onPressed: () { - Navigator.of(context).pop(); - }, - child: Text(MaterialLocalizations.of(context).okButtonLabel), - ), - ], - ), - ); - } - } - - Future _upgrade55(int lastVersion) async { - final c = KiwiContainer().resolve(); - try { - _log.info("[_upgrade55] migrate DB"); - await CompatV55.migrateDb( - c.npDb, - onProgress: (current, count) { - _upgradeCubit.setState( - L10n.global().migrateDatabaseProcessingNotification, - current, - count, - ); - }, - ); - } catch (e, stackTrace) { - _log.shout("[_upgrade55] Failed while migrateDb", e, stackTrace); - final accounts = Pref().getAccounts3Or([]); - await context.read().clearAndInitWithAccounts(accounts.toDb()); - } - _upgradeCubit.setIntermediate(); - } - - Future _showChangelogIfAvailable(int lastVersion) async { - if (Changelog.hasContent(lastVersion)) { - try { - await Navigator.of(context).pushNamed(Changelog.routeName, - arguments: ChangelogArguments(lastVersion)); - } catch (e, stackTrace) { - _log.severe( - "[_showChangelogIfAvailable] Uncaught exception", e, stackTrace); - } finally { - _changelogCompleter.complete(); - } - } else { - _changelogCompleter.complete(); - } - } - - final _changelogCompleter = Completer(); - var _isUpgrading = false; - late final _upgradeCubit = _UpgradeCubit(); } -@toString -class _UpgradeState { - const _UpgradeState(String text, int current, int count) - : this._(text, current, count); +// typedef _BlocBuilder = BlocBuilder<_Bloc, _State>; +// typedef _BlocListener = BlocListener<_Bloc, _State>; +typedef _BlocListenerT = BlocListenerT<_Bloc, _State, T>; +typedef _BlocSelector = BlocSelector<_Bloc, _State, T>; - const _UpgradeState.intermediate([String? text]) - : this._(text ?? "Updating", 0, null); - - const _UpgradeState._(this.text, this.current, this.count); - - @override - String toString() => _$toString(); - - final String text; - final int current; - final int? count; -} - -class _UpgradeCubit extends Cubit<_UpgradeState> { - _UpgradeCubit() : super(const _UpgradeState.intermediate()); - - void setIntermediate() => emit(const _UpgradeState.intermediate()); - - void setState(String text, int current, int count) => - emit(_UpgradeState(text, current, count)); +extension on BuildContext { + _Bloc get bloc => read<_Bloc>(); + // _State get state => bloc.state; + void addEvent(_Event event) => bloc.add(event); } diff --git a/app/lib/widget/splash.g.dart b/app/lib/widget/splash.g.dart index 89940d04..2a3f7607 100644 --- a/app/lib/widget/splash.g.dart +++ b/app/lib/widget/splash.g.dart @@ -2,24 +2,93 @@ part of 'splash.dart'; +// ************************************************************************** +// CopyWithLintRuleGenerator +// ************************************************************************** + +// ignore_for_file: library_private_types_in_public_api, duplicate_ignore + +// ************************************************************************** +// CopyWithGenerator +// ************************************************************************** + +abstract class $_StateCopyWithWorker { + _State call( + {int? changelogFromVersion, + double? upgradeProgress, + String? upgradeText, + bool? isDone}); +} + +class _$_StateCopyWithWorkerImpl implements $_StateCopyWithWorker { + _$_StateCopyWithWorkerImpl(this.that); + + @override + _State call( + {dynamic changelogFromVersion = copyWithNull, + dynamic upgradeProgress = copyWithNull, + dynamic upgradeText = copyWithNull, + dynamic isDone}) { + return _State( + changelogFromVersion: changelogFromVersion == copyWithNull + ? that.changelogFromVersion + : changelogFromVersion as int?, + upgradeProgress: upgradeProgress == copyWithNull + ? that.upgradeProgress + : upgradeProgress as double?, + upgradeText: upgradeText == copyWithNull + ? that.upgradeText + : upgradeText as String?, + isDone: isDone as bool? ?? that.isDone); + } + + final _State that; +} + +extension $_StateCopyWith on _State { + $_StateCopyWithWorker get copyWith => _$copyWith; + $_StateCopyWithWorker get _$copyWith => _$_StateCopyWithWorkerImpl(this); +} + // ************************************************************************** // NpLogGenerator // ************************************************************************** -extension _$_SplashStateNpLog on _SplashState { +extension _$_WrappedSplashNpLog on _WrappedSplash { // ignore: unused_element Logger get _log => log; - static final log = Logger("widget.splash._SplashState"); + static final log = Logger("widget.splash._WrappedSplash"); +} + +extension _$_BlocNpLog on _Bloc { + // ignore: unused_element + Logger get _log => log; + + static final log = Logger("widget.splash._Bloc"); } // ************************************************************************** // ToStringGenerator // ************************************************************************** -extension _$_UpgradeStateToString on _UpgradeState { +extension _$_StateToString on _State { String _$toString() { // ignore: unnecessary_string_interpolations - return "_UpgradeState {text: $text, current: $current, count: $count}"; + return "_State {changelogFromVersion: $changelogFromVersion, upgradeProgress: ${upgradeProgress == null ? null : "${upgradeProgress!.toStringAsFixed(3)}"}, upgradeText: $upgradeText, isDone: $isDone}"; + } +} + +extension _$_InitToString on _Init { + String _$toString() { + // ignore: unnecessary_string_interpolations + return "_Init {}"; + } +} + +extension _$_ChangelogDismissedToString on _ChangelogDismissed { + String _$toString() { + // ignore: unnecessary_string_interpolations + return "_ChangelogDismissed {}"; } } diff --git a/app/lib/widget/splash/bloc.dart b/app/lib/widget/splash/bloc.dart new file mode 100644 index 00000000..9349ee43 --- /dev/null +++ b/app/lib/widget/splash/bloc.dart @@ -0,0 +1,157 @@ +part of '../splash.dart'; + +@npLog +class _Bloc extends Bloc<_Event, _State> with BlocLogger { + _Bloc({ + required this.prefController, + required this.npDb, + }) : super(_State.init()) { + on<_Init>(_onInit); + on<_ChangelogDismissed>(_onChangelogDismissed); + } + + @override + Future close() { + for (final s in _subscriptions) { + s.cancel(); + } + return super.close(); + } + + @override + String get tag => _log.fullName; + + Future _onInit(_Init ev, Emitter<_State> emit) async { + _log.info(ev); + await Future.wait([ + _initNotification(), + _initFirstRun(), + _migrateApp(emit), + ]); + emit(state.copyWith(isDone: true)); + } + + void _onChangelogDismissed(_ChangelogDismissed ev, Emitter<_State> emit) { + _log.info(ev); + _changelogCompleter.complete(); + } + + Future _initNotification() async { + if (!await Permission.hasPostNotifications()) { + await requestPostNotificationsForResult(); + } + } + + Future _initFirstRun() async { + if (prefController.firstRunTimeValue == null) { + await prefController.setFirstRunTime(clock.now().toUtc()); + } + } + + Future _migrateApp(Emitter<_State> emit) async { + if (_shouldUpgrade()) { + await _handleUpgrade(emit); + } + } + + bool _shouldUpgrade() { + final lastVersion = prefController.lastVersionValue; + return lastVersion < k.version; + } + + Future _handleUpgrade(Emitter<_State> emit) async { + try { + final lastVersion = prefController.lastVersionValue; + unawaited(_showChangelogIfAvailable(lastVersion, emit)); + // begin upgrade while showing the changelog + try { + _log.info("[_handleUpgrade] Upgrade: $lastVersion -> ${k.version}"); + await _upgrade(lastVersion, emit); + await Future.delayed(Duration(seconds: 5)); + _log.info("[_handleUpgrade] Upgrade done"); + } finally { + // ensure user has closed the changelog + await _changelogCompleter.future; + } + } catch (e, stackTrace) { + _log.shout("[_handleUpgrade] Failed while upgrade", e, stackTrace); + } finally { + await prefController.setLastVersion(k.version); + } + } + + Future _showChangelogIfAvailable( + int lastVersion, Emitter<_State> emit) async { + if (Changelog.hasContent(lastVersion)) { + emit(state.copyWith(changelogFromVersion: lastVersion)); + } else { + _changelogCompleter.complete(); + } + } + + Future _upgrade(int lastVersion, Emitter<_State> emit) async { + if (lastVersion < 290) { + await _upgrade29(lastVersion); + } + if (lastVersion < 460) { + await _upgrade46(lastVersion); + } + if (lastVersion < 550) { + await _upgrade55(lastVersion, emit); + } + } + + Future _upgrade29(int lastVersion) async { + try { + _log.info("[_upgrade29] clearDefaultCache"); + await CompatV29.clearDefaultCache(); + } catch (e, stackTrace) { + _log.shout("[_upgrade29] Failed while clearDefaultCache", e, stackTrace); + // just leave the cache then + } + } + + Future _upgrade46(int lastVersion) async { + try { + _log.info("[_upgrade46] insertDbAccounts"); + await CompatV46.insertDbAccounts(prefController, npDb); + } catch (e, stackTrace) { + _log.shout("[_upgrade46] Failed while clearDefaultCache", e, stackTrace); + unawaited(Pref().setAccounts3(null)); + unawaited(Pref().setCurrentAccountIndex(null)); + } + } + + Future _upgrade55(int lastVersion, Emitter<_State> emit) async { + try { + _log.info("[_upgrade55] migrate DB"); + await CompatV55.migrateDb( + npDb, + onProgress: (current, count) { + if (!isClosed) { + emit(state.copyWith( + upgradeProgress: current / count, + upgradeText: L10n.global().migrateDatabaseProcessingNotification, + )); + } + }, + ); + } catch (e, stackTrace) { + _log.shout("[_upgrade55] Failed while migrateDb", e, stackTrace); + final accounts = prefController.accountsValue; + await npDb.clearAndInitWithAccounts(accounts.toDb()); + } + if (!isClosed) { + emit(state.copyWith( + upgradeProgress: null, + upgradeText: null, + )); + } + } + + final PrefController prefController; + final NpDb npDb; + + final _subscriptions = []; + final _changelogCompleter = Completer(); +} diff --git a/app/lib/widget/splash/state_event.dart b/app/lib/widget/splash/state_event.dart new file mode 100644 index 00000000..3fc8339b --- /dev/null +++ b/app/lib/widget/splash/state_event.dart @@ -0,0 +1,42 @@ +part of '../splash.dart'; + +@genCopyWith +@toString +class _State { + const _State({ + this.changelogFromVersion, + this.upgradeProgress, + this.upgradeText, + required this.isDone, + }); + + factory _State.init() => const _State( + isDone: false, + ); + + @override + String toString() => _$toString(); + + final int? changelogFromVersion; + final double? upgradeProgress; + final String? upgradeText; + final bool isDone; +} + +abstract class _Event {} + +@toString +class _Init implements _Event { + const _Init(); + + @override + String toString() => _$toString(); +} + +@toString +class _ChangelogDismissed implements _Event { + const _ChangelogDismissed(); + + @override + String toString() => _$toString(); +} diff --git a/app/lib/widget/splash/view.dart b/app/lib/widget/splash/view.dart new file mode 100644 index 00000000..ac1ba8c1 --- /dev/null +++ b/app/lib/widget/splash/view.dart @@ -0,0 +1,35 @@ +part of '../splash.dart'; + +class _UpgradeProgressView extends StatelessWidget { + const _UpgradeProgressView(); + + @override + Widget build(BuildContext context) { + return _BlocSelector( + selector: (state) => state.upgradeProgress, + builder: (context, upgradeProgress) { + if (upgradeProgress == null) { + return const Center( + child: SizedBox.square( + dimension: 24, + child: AppIntermediateCircularProgressIndicator(), + ), + ); + } else { + return Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + _BlocSelector( + selector: (state) => state.upgradeText, + builder: (context, upgradeText) => + Text(upgradeText ?? "Updating"), + ), + const SizedBox(height: 8), + LinearProgressIndicator(value: upgradeProgress), + ], + ); + } + }, + ); + } +}