From dbd9679cba2ba244f5e2705b2aceb87662e566e3 Mon Sep 17 00:00:00 2001 From: Ming Ming Date: Sun, 5 Dec 2021 20:02:22 +0800 Subject: [PATCH] Move account settings out of pref --- lib/account.dart | 71 ++--- lib/bloc/scan_account_dir.dart | 20 +- lib/main.dart | 15 ++ lib/platform/universal_storage.dart | 29 ++ lib/pref.dart | 247 ++++++++++++------ lib/pref_util.dart | 8 + lib/use_case/compat/v34.dart | 75 ++++++ .../import_potential_shared_album.dart | 6 +- lib/use_case/list_potential_shared_album.dart | 7 +- lib/widget/account_picker_dialog.dart | 47 ++-- lib/widget/home.dart | 2 +- lib/widget/home_albums.dart | 5 +- lib/widget/settings.dart | 59 +---- lib/widget/sharing_browser.dart | 2 +- lib/widget/sign_in.dart | 65 +++-- test/test_util.dart | 3 +- test/use_case/compat/v34_test.dart | 87 ++++++ 17 files changed, 492 insertions(+), 256 deletions(-) create mode 100644 lib/pref_util.dart create mode 100644 lib/use_case/compat/v34.dart create mode 100644 test/use_case/compat/v34_test.dart diff --git a/lib/account.dart b/lib/account.dart index dcdbf1bf..d8909c6b 100644 --- a/lib/account.dart +++ b/lib/account.dart @@ -1,3 +1,5 @@ +import 'dart:math'; + import 'package:equatable/equatable.dart'; import 'package:flutter/foundation.dart'; import 'package:nc_photos/ci_string.dart'; @@ -7,6 +9,7 @@ import 'package:nc_photos/type.dart'; /// Details of a remote Nextcloud server account class Account with EquatableMixin { Account( + this.id, this.scheme, String address, this.username, @@ -20,6 +23,7 @@ class Account with EquatableMixin { } Account copyWith({ + String? id, String? scheme, String? address, CiString? username, @@ -27,6 +31,7 @@ class Account with EquatableMixin { List? roots, }) { return Account( + id ?? this.id, scheme ?? this.scheme, address ?? this.address, username ?? this.username, @@ -35,9 +40,16 @@ class Account with EquatableMixin { ); } + static String newId() { + final timestamp = DateTime.now().millisecondsSinceEpoch; + final random = Random().nextInt(0xFFFFFF); + return "${timestamp.toRadixString(16)}-${random.toRadixString(16).padLeft(6, '0')}"; + } + @override toString() { return "$runtimeType {" + "id: '$id', " "scheme: '$scheme', " "address: '${kDebugMode ? address : "***"}', " "username: '${kDebugMode ? username : "***"}', " @@ -47,13 +59,15 @@ class Account with EquatableMixin { } Account.fromJson(JsonObj json) - : scheme = json["scheme"], + : id = json["id"], + scheme = json["scheme"], address = json["address"], username = CiString(json["username"]), password = json["password"], _roots = json["roots"].cast(); JsonObj toJson() => { + "id": id, "scheme": scheme, "address": address, "username": username.toString(), @@ -62,10 +76,11 @@ class Account with EquatableMixin { }; @override - List get props => [scheme, address, username, password, _roots]; + List get props => [id, scheme, address, username, password, _roots]; List get roots => _roots; + final String id; final String scheme; final String address; final CiString username; @@ -73,58 +88,6 @@ class Account with EquatableMixin { final List _roots; } -class AccountSettings with EquatableMixin { - const AccountSettings({ - this.isEnableFaceRecognitionApp = true, - this.shareFolder = "", - }); - - factory AccountSettings.fromJson(JsonObj json) { - return AccountSettings( - isEnableFaceRecognitionApp: json["isEnableFaceRecognitionApp"] ?? true, - shareFolder: json["shareFolder"] ?? "", - ); - } - - JsonObj toJson() => { - "isEnableFaceRecognitionApp": isEnableFaceRecognitionApp, - "shareFolder": shareFolder, - }; - - @override - toString() { - return "$runtimeType {" - "isEnableFaceRecognitionApp: $isEnableFaceRecognitionApp, " - "shareFolder: $shareFolder, " - "}"; - } - - AccountSettings copyWith({ - bool? isEnableFaceRecognitionApp, - String? shareFolder, - }) { - return AccountSettings( - isEnableFaceRecognitionApp: - isEnableFaceRecognitionApp ?? this.isEnableFaceRecognitionApp, - shareFolder: shareFolder ?? this.shareFolder, - ); - } - - @override - get props => [ - isEnableFaceRecognitionApp, - shareFolder, - ]; - - final bool isEnableFaceRecognitionApp; - - /// Path of the share folder - /// - /// Share folder is where files shared with you are initially placed. Must - /// match the value of share_folder in config.php - final String shareFolder; -} - extension AccountExtension on Account { String get url => "$scheme://$address"; diff --git a/lib/bloc/scan_account_dir.dart b/lib/bloc/scan_account_dir.dart index 940c0c5a..7aa03d1d 100644 --- a/lib/bloc/scan_account_dir.dart +++ b/lib/bloc/scan_account_dir.dart @@ -274,7 +274,7 @@ class ScanAccountDirBloc // no data in this bloc, ignore return; } - if (ev.key == PrefKey.accounts2) { + if (ev.key == PrefKey.accounts3) { _refreshThrottler.trigger( maxResponceTime: const Duration(seconds: 3), maxPendingCount: 10, @@ -301,9 +301,9 @@ class ScanAccountDirBloc try { final fileRepo = FileRepo(dataSrc); // include files shared with this account - final settings = Pref().getAccountSettings(account); - final shareDir = - File(path: file_util.unstripPath(account, settings.shareFolder)); + final settings = AccountPref.of(account); + final shareDir = File( + path: file_util.unstripPath(account, settings.getShareFolderOr())); bool isShareDirIncluded = false; for (final r in account.roots) { @@ -320,8 +320,12 @@ class ScanAccountDirBloc } if (!isShareDirIncluded) { - final files = await Ls(fileRepo)(account, - File(path: file_util.unstripPath(account, settings.shareFolder))); + final files = await Ls(fileRepo)( + account, + File( + path: file_util.unstripPath(account, settings.getShareFolderOr()), + ), + ); final sharedFiles = files.where((f) => !f.isOwned(account.username)).toList(); yield ScanAccountDirBlocSuccess(getState().files + sharedFiles); @@ -347,9 +351,9 @@ class ScanAccountDirBloc } } - final settings = Pref().getAccountSettings(account); + final settings = AccountPref.of(account); final shareDir = - File(path: file_util.unstripPath(account, settings.shareFolder)); + File(path: file_util.unstripPath(account, settings.getShareFolderOr())); if (file_util.isUnderDir(file, shareDir)) { return true; } diff --git a/lib/main.dart b/lib/main.dart index a98975cc..03835678 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -26,6 +26,7 @@ import 'package:nc_photos/mobile/self_signed_cert_manager.dart'; import 'package:nc_photos/platform/features.dart' as features; import 'package:nc_photos/platform/k.dart' as platform_k; import 'package:nc_photos/pref.dart'; +import 'package:nc_photos/pref_util.dart' as pref_util; import 'package:nc_photos/widget/my_app.dart'; void main() async { @@ -34,6 +35,7 @@ void main() async { _initLog(); _initKiwi(); await _initPref(); + await _initAccountPrefs(); await _initDeviceInfo(); _initBloc(); _initEquatable(); @@ -105,6 +107,17 @@ Future _initPref() async { } } +Future _initAccountPrefs() async { + for (final a in Pref().getAccounts3Or([])) { + try { + AccountPref.setGlobalInstance(a, await pref_util.loadAccountPref(a)); + } catch (e, stackTrace) { + _log.shout("[_initAccountPrefs] Failed reading pref for account: $a", e, + stackTrace); + } + } +} + Future _initDeviceInfo() async { if (platform_k.isAndroid) { await AndroidInfo.init(); @@ -150,3 +163,5 @@ class _BlocObserver extends BlocObserver { static final _log = Logger("main._BlocObserver"); } + +final _log = Logger("main"); diff --git a/lib/platform/universal_storage.dart b/lib/platform/universal_storage.dart index b40b7922..07754c63 100644 --- a/lib/platform/universal_storage.dart +++ b/lib/platform/universal_storage.dart @@ -1,5 +1,7 @@ import 'dart:typed_data'; +import 'package:flutter/foundation.dart'; + /// Store simple contents across different platforms /// /// On mobile, the contents will be persisted as a file. On web, the contents @@ -19,3 +21,30 @@ abstract class UniversalStorage { Future remove(String name); } + +/// UniversalStorage backed by memory, useful in unit tests +@visibleForTesting +class UniversalMemoryStorage implements UniversalStorage { + @override + putBinary(String name, Uint8List content) async { + data[name] = content; + } + + @override + getBinary(String name) async => data[name]; + + @override + putString(String name, String content) async { + data[name] = content; + } + + @override + getString(String name) async => data[name]; + + @override + remove(String name) async { + data.remove(name); + } + + final data = {}; +} diff --git a/lib/pref.dart b/lib/pref.dart index 9fc15d7e..4464b2cd 100644 --- a/lib/pref.dart +++ b/lib/pref.dart @@ -4,8 +4,9 @@ import 'package:event_bus/event_bus.dart'; import 'package:kiwi/kiwi.dart'; import 'package:nc_photos/account.dart'; import 'package:nc_photos/event/event.dart'; -import 'package:nc_photos/type.dart'; -import 'package:nc_photos/use_case/compat/v32.dart'; +import 'package:nc_photos/mobile/platform.dart' + if (dart.library.html) 'package:nc_photos/web/platform.dart' as platform; +import 'package:nc_photos/use_case/compat/v34.dart'; import 'package:shared_preferences/shared_preferences.dart'; class Pref { @@ -22,16 +23,15 @@ class Pref { _inst = pref; } - List? getAccounts2() { - final jsonObjs = provider.getStringList(PrefKey.accounts2); - return jsonObjs?.map((e) => PrefAccount.fromJson(jsonDecode(e))).toList(); + List? getAccounts3() { + final jsonObjs = provider.getStringList(PrefKey.accounts3); + return jsonObjs?.map((e) => Account.fromJson(jsonDecode(e))).toList(); } - List getAccounts2Or(List def) => - getAccounts2() ?? def; - Future setAccounts2(List value) { + List getAccounts3Or(List def) => getAccounts3() ?? def; + Future setAccounts3(List value) { final jsons = value.map((e) => jsonEncode(e.toJson())).toList(); - return provider.setStringList(PrefKey.accounts2, jsons); + return provider.setStringList(PrefKey.accounts3, jsons); } int? getCurrentAccountIndex() => provider.getInt(PrefKey.currentAccountIndex); @@ -155,6 +155,43 @@ class Pref { static Pref? _inst; } +class AccountPref { + AccountPref.scoped(this.provider); + + static AccountPref of(Account account) { + _insts.putIfAbsent( + account.id, () => AccountPref.scoped(PrefMemoryProvider())); + return _insts[account.id]!; + } + + /// Set the global [AccountPref] instance returned by the default constructor + static void setGlobalInstance(Account account, AccountPref? pref) { + if (pref != null) { + assert(!_insts.containsKey(account.id)); + _insts[account.id] = pref; + } else { + assert(_insts.containsKey(account.id)); + _insts.remove(account.id); + } + } + + bool? isEnableFaceRecognitionApp() => + provider.getBool(PrefKey.isEnableFaceRecognitionApp); + bool isEnableFaceRecognitionAppOr([bool def = true]) => + isEnableFaceRecognitionApp() ?? def; + Future setEnableFaceRecognitionApp(bool value) => + provider.setBool(PrefKey.isEnableFaceRecognitionApp, value); + + String? getShareFolder() => provider.getString(PrefKey.shareFolder); + String getShareFolderOr([String def = ""]) => getShareFolder() ?? def; + Future setShareFolder(String value) => + provider.setString(PrefKey.shareFolder, value); + + final PrefProvider provider; + + static final _insts = {}; +} + /// Provide the data for [Pref] abstract class PrefProvider { bool? getBool(PrefKey key); @@ -163,9 +200,14 @@ abstract class PrefProvider { int? getInt(PrefKey key); Future setInt(PrefKey key, int value); + String? getString(PrefKey key); + Future setString(PrefKey key, String value); + List? getStringList(PrefKey key); Future setStringList(PrefKey key, List value); + Future clear(); + bool _onPostSet(bool result, PrefKey key, dynamic value) { if (result) { KiwiContainer().resolve().fire(PrefUpdatedEvent(key, value)); @@ -179,8 +221,12 @@ abstract class PrefProvider { /// [Pref] stored with [SharedPreferences] lib class PrefSharedPreferencesProvider extends PrefProvider { Future init() async { - if (await CompatV32.isPrefNeedMigration()) { - await CompatV32.migratePref(); + // Obsolete, CompatV34 is compatible with pre v32 versions + // if (await CompatV32.isPrefNeedMigration()) { + // await CompatV32.migratePref(); + // } + if (await CompatV34.isPrefNeedMigration()) { + await CompatV34.migratePref(platform.UniversalStorage()); } return SharedPreferences.getInstance().then((pref) { _pref = pref; @@ -204,6 +250,15 @@ class PrefSharedPreferencesProvider extends PrefProvider { return _onPostSet(await _pref.setInt(key.toStringKey(), value), key, value); } + @override + getString(PrefKey key) => _pref.getString(key.toStringKey()); + + @override + setString(PrefKey key, String value) async { + return _onPostSet( + await _pref.setString(key.toStringKey(), value), key, value); + } + @override getStringList(PrefKey key) => _pref.getStringList(key.toStringKey()); @@ -213,42 +268,103 @@ class PrefSharedPreferencesProvider extends PrefProvider { await _pref.setStringList(key.toStringKey(), value), key, value); } + @override + clear() => _pref.clear(); + late SharedPreferences _pref; } -/// [Pref] stored in memory +/// [Pref] backed by [UniversalStorage] +class PrefUniversalStorageProvider extends PrefProvider { + PrefUniversalStorageProvider(this.name); + + Future init() async { + final prefStr = await platform.UniversalStorage().getString(name) ?? "{}"; + _data + ..clear() + ..addAll(jsonDecode(prefStr)); + } + + @override + getBool(PrefKey key) => _get(key); + @override + setBool(PrefKey key, bool value) => _set(key, value); + + @override + getInt(PrefKey key) => _get(key); + @override + setInt(PrefKey key, int value) => _set(key, value); + + @override + getString(PrefKey key) => _get(key); + @override + setString(PrefKey key, String value) => _set(key, value); + + @override + getStringList(PrefKey key) => _get>(key); + @override + setStringList(PrefKey key, List value) => _set(key, value); + + @override + clear() async { + await platform.UniversalStorage().remove(name); + _data.clear(); + return true; + } + + T? _get(PrefKey key) => _data[key.toStringKey()]; + + Future _set(PrefKey key, T value) async { + return _onPostSet(await _update(key, value), key, value); + } + + Future _update(PrefKey key, T value) async { + final newData = Map.of(_data) + ..addEntries([MapEntry(key.toStringKey(), value)]); + await platform.UniversalStorage().putString(name, jsonEncode(newData)); + _data[key.toStringKey()] = value; + return true; + } + + final String name; + final _data = {}; +} + +/// [Pref] stored in memory, useful in unit tests class PrefMemoryProvider extends PrefProvider { PrefMemoryProvider([ Map initialData = const {}, ]) : _data = Map.of(initialData); @override - getBool(PrefKey key) => _data[key.toStringKey()]; + getBool(PrefKey key) => _get(key); + @override + setBool(PrefKey key, bool value) => _set(key, value); @override - setBool(PrefKey key, bool value) async { - return _onPostSet(() { - _data[key.toStringKey()] = value; - return true; - }(), key, value); + getInt(PrefKey key) => _get(key); + @override + setInt(PrefKey key, int value) => _set(key, value); + + @override + getString(PrefKey key) => _get(key); + @override + setString(PrefKey key, String value) => _set(key, value); + + @override + getStringList(PrefKey key) => _get>(key); + @override + setStringList(PrefKey key, List value) => _set(key, value); + + @override + clear() async { + _data.clear(); + return true; } - @override - getInt(PrefKey key) => _data[key.toStringKey()]; + T? _get(PrefKey key) => _data[key.toStringKey()]; - @override - setInt(PrefKey key, int value) async { - return _onPostSet(() { - _data[key.toStringKey()] = value; - return true; - }(), key, value); - } - - @override - getStringList(PrefKey key) => _data[key.toStringKey()]; - - @override - setStringList(PrefKey key, List value) async { + Future _set(PrefKey key, T value) async { return _onPostSet(() { _data[key.toStringKey()] = value; return true; @@ -258,48 +374,8 @@ class PrefMemoryProvider extends PrefProvider { final Map _data; } -class PrefAccount { - const PrefAccount( - this.account, [ - this.settings = const AccountSettings(), - ]); - - factory PrefAccount.fromJson(JsonObj json) { - return PrefAccount( - Account.fromJson(json["account"].cast()), - AccountSettings.fromJson(json["settings"].cast()), - ); - } - - JsonObj toJson() => { - "account": account.toJson(), - "settings": settings.toJson(), - }; - - PrefAccount copyWith({ - Account? account, - AccountSettings? settings, - }) { - return PrefAccount( - account ?? this.account, - settings ?? this.settings, - ); - } - - @override - toString() { - return "$runtimeType {" - "account: $account, " - "settings: $settings, " - "}"; - } - - final Account account; - final AccountSettings settings; -} - enum PrefKey { - accounts2, + accounts3, currentAccountIndex, homePhotosZoomLevel, albumBrowserZoomLevel, @@ -321,13 +397,17 @@ enum PrefKey { isAlbumBrowserShowDate, gpsMapProvider, hasShownSharedAlbumInfo, + + // account pref + isEnableFaceRecognitionApp, + shareFolder, } extension on PrefKey { String toStringKey() { switch (this) { - case PrefKey.accounts2: - return "accounts2"; + case PrefKey.accounts3: + return "accounts3"; case PrefKey.currentAccountIndex: return "currentAccountIndex"; case PrefKey.homePhotosZoomLevel: @@ -370,6 +450,12 @@ extension on PrefKey { return "gpsMapProvider"; case PrefKey.hasShownSharedAlbumInfo: return "hasShownSharedAlbumInfo"; + + // account pref + case PrefKey.isEnableFaceRecognitionApp: + return "isEnableFaceRecognitionApp"; + case PrefKey.shareFolder: + return "shareFolder"; } } } @@ -377,16 +463,9 @@ extension on PrefKey { extension PrefExtension on Pref { Account? getCurrentAccount() { try { - return Pref().getAccounts2()![Pref().getCurrentAccountIndex()!].account; + return Pref().getAccounts3()![Pref().getCurrentAccountIndex()!]; } catch (_) { return null; } } - - AccountSettings getAccountSettings(Account account) { - return Pref() - .getAccounts2()! - .firstWhere((element) => element.account == account) - .settings; - } } diff --git a/lib/pref_util.dart b/lib/pref_util.dart new file mode 100644 index 00000000..156bf34a --- /dev/null +++ b/lib/pref_util.dart @@ -0,0 +1,8 @@ +import 'package:nc_photos/account.dart'; +import 'package:nc_photos/pref.dart'; + +Future loadAccountPref(Account account) async { + final provider = PrefUniversalStorageProvider("accounts/${account.id}/pref"); + await provider.init(); + return AccountPref.scoped(provider); +} diff --git a/lib/use_case/compat/v34.dart b/lib/use_case/compat/v34.dart new file mode 100644 index 00000000..07f5eb80 --- /dev/null +++ b/lib/use_case/compat/v34.dart @@ -0,0 +1,75 @@ +import 'dart:convert'; + +import 'package:logging/logging.dart'; +import 'package:nc_photos/account.dart'; +import 'package:nc_photos/platform/universal_storage.dart'; +import 'package:nc_photos/type.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +/// Compatibility helper for v34 +class CompatV34 { + static Future isPrefNeedMigration() async { + final pref = await SharedPreferences.getInstance(); + return pref.containsKey("accounts2") || pref.containsKey("accounts"); + } + + static Future migratePref(UniversalStorage storage) async { + final pref = await SharedPreferences.getInstance(); + if (pref.containsKey("accounts2")) { + return _migratePrefV2(pref, storage); + } else { + return _migratePrefV1(pref, storage); + } + } + + static Future _migratePrefV2( + SharedPreferences pref, UniversalStorage storage) async { + final jsons = pref.getStringList("accounts2"); + if (jsons == null) { + return; + } + _log.info("[migratePref] Migrate Pref.accounts2"); + final newJsons = []; + for (final j in jsons) { + final JsonObj account2 = jsonDecode(j).cast(); + final id = Account.newId(); + account2["account"]["id"] = id; + newJsons.add(account2["account"]); + await storage.putString("accounts/$id/pref", jsonEncode(account2["settings"])); + } + if (await pref.setStringList( + "accounts3", newJsons.map((e) => jsonEncode(e)).toList())) { + _log.info("[migratePref] Migrated ${newJsons.length} accounts2"); + await pref.remove("accounts2"); + } else { + _log.severe("[migratePref] Failed while writing pref"); + } + } + + static Future _migratePrefV1( + SharedPreferences pref, UniversalStorage storage) async { + final jsons = pref.getStringList("accounts"); + if (jsons == null) { + return; + } + _log.info("[migratePref] Migrate Pref.accounts"); + final newJsons = []; + for (final j in jsons) { + final JsonObj account = jsonDecode(j).cast(); + final id = Account.newId(); + account["id"] = id; + newJsons.add(account); + await storage.putString("accounts/$id/pref", + """{"isEnableFaceRecognitionApp":true,"shareFolder":""}"""); + } + if (await pref.setStringList( + "accounts3", newJsons.map((e) => jsonEncode(e)).toList())) { + _log.info("[migratePref] Migrated ${newJsons.length} accounts"); + await pref.remove("accounts"); + } else { + _log.severe("[migratePref] Failed while writing pref"); + } + } + + static final _log = Logger("use_case.compat.v34.CompatV34"); +} diff --git a/lib/use_case/import_potential_shared_album.dart b/lib/use_case/import_potential_shared_album.dart index 698d9bf0..e1fd6b73 100644 --- a/lib/use_case/import_potential_shared_album.dart +++ b/lib/use_case/import_potential_shared_album.dart @@ -2,6 +2,7 @@ import 'package:logging/logging.dart'; import 'package:nc_photos/account.dart'; import 'package:nc_photos/entity/album.dart'; import 'package:nc_photos/entity/file.dart'; +import 'package:nc_photos/pref.dart'; import 'package:nc_photos/remote_storage_util.dart' as remote_storage_util; import 'package:nc_photos/use_case/list_potential_shared_album.dart'; import 'package:nc_photos/use_case/move.dart'; @@ -10,10 +11,11 @@ import 'package:nc_photos/use_case/move.dart'; class ImportPotentialSharedAlbum { ImportPotentialSharedAlbum(this.fileRepo, this.albumRepo); - Future> call(Account account, AccountSettings settings) async { + Future> call(Account account, AccountPref accountPref) async { _log.info("[call] $account"); final products = []; - final files = await ListPotentialSharedAlbum(fileRepo)(account, settings); + final files = + await ListPotentialSharedAlbum(fileRepo)(account, accountPref); for (final f in files) { // check if the file is actually an album try { diff --git a/lib/use_case/list_potential_shared_album.dart b/lib/use_case/list_potential_shared_album.dart index 21f8115c..53be4a8a 100644 --- a/lib/use_case/list_potential_shared_album.dart +++ b/lib/use_case/list_potential_shared_album.dart @@ -2,6 +2,7 @@ import 'package:logging/logging.dart'; import 'package:nc_photos/account.dart'; import 'package:nc_photos/entity/file.dart'; import 'package:nc_photos/entity/file_util.dart' as file_util; +import 'package:nc_photos/pref.dart'; import 'package:nc_photos/use_case/ls.dart'; /// List all shared files that are potentially albums @@ -10,11 +11,13 @@ import 'package:nc_photos/use_case/ls.dart'; class ListPotentialSharedAlbum { ListPotentialSharedAlbum(this.fileRepo); - Future> call(Account account, AccountSettings settings) async { + Future> call(Account account, AccountPref accountPref) async { final results = []; final ls = await Ls(fileRepo)( account, - File(path: file_util.unstripPath(account, settings.shareFolder)), + File( + path: file_util.unstripPath(account, accountPref.getShareFolderOr()), + ), ); for (final f in ls) { // check owner diff --git a/lib/widget/account_picker_dialog.dart b/lib/widget/account_picker_dialog.dart index 01d2ae1e..7148c209 100644 --- a/lib/widget/account_picker_dialog.dart +++ b/lib/widget/account_picker_dialog.dart @@ -29,20 +29,20 @@ class _AccountPickerDialogState extends State { @override initState() { super.initState(); - _accounts = Pref().getAccounts2Or([]); + _accounts = Pref().getAccounts3Or([]); } @override build(BuildContext context) { final otherAccountOptions = _accounts - .where((a) => a.account != widget.account) + .where((a) => a != widget.account) .map((a) => SimpleDialogOption( padding: const EdgeInsets.symmetric(horizontal: 8), onPressed: () => _onItemPressed(a), child: ListTile( dense: true, - title: Text(a.account.url), - subtitle: Text(a.account.username.toString()), + title: Text(a.url), + subtitle: Text(a.username.toString()), trailing: IconButton( icon: Icon( Icons.close, @@ -101,21 +101,21 @@ class _AccountPickerDialogState extends State { ); } - void _onItemPressed(PrefAccount account) { + void _onItemPressed(Account account) { Pref().setCurrentAccountIndex(_accounts.indexOf(account)); Navigator.of(context).pushNamedAndRemoveUntil(Home.routeName, (_) => false, - arguments: HomeArguments(account.account)); + arguments: HomeArguments(account)); } - void _onRemoveItemPressed(PrefAccount account) { + Future _onRemoveItemPressed(Account account) async { try { - _removeAccount(account); + await _removeAccount(account); setState(() { - _accounts = Pref().getAccounts2()!; + _accounts = Pref().getAccounts3()!; }); SnackBarManager().showSnackBar(SnackBar( - content: Text( - L10n.global().removeServerSuccessNotification(account.account.url)), + content: + Text(L10n.global().removeServerSuccessNotification(account.url)), duration: k.snackBarDurationNormal, )); } catch (e) { @@ -133,24 +133,27 @@ class _AccountPickerDialogState extends State { arguments: AccountSettingsWidgetArguments(widget.account)); } - void _removeAccount(PrefAccount account) { - _log.info("[_removeAccount] Remove account: ${account.account}"); - final currentAccounts = Pref().getAccounts2()!; - final currentAccount = - currentAccounts[Pref().getCurrentAccountIndex()!]; - final newAccounts = currentAccounts - .where((element) => element.account != account.account) - .toList(); - final newAccountIndex = newAccounts.indexOf(currentAccount); + Future _removeAccount(Account account) async { + _log.info("[_removeAccount] Remove account: $account"); + final accounts = Pref().getAccounts3()!; + final currentAccount = accounts[Pref().getCurrentAccountIndex()!]; + accounts.remove(account); + final newAccountIndex = accounts.indexOf(currentAccount); if (newAccountIndex == -1) { throw StateError("Active account not found in resulting account list"); } + try { + await AccountPref.of(account).provider.clear(); + } catch (e, stackTrace) { + _log.shout( + "[_removeAccount] Failed while removing account pref", e, stackTrace); + } Pref() - ..setAccounts2(newAccounts) + ..setAccounts3(accounts) ..setCurrentAccountIndex(newAccountIndex); } - late List _accounts; + late List _accounts; static final _log = Logger("widget.account_picker_dialog._AccountPickerDialogState"); diff --git a/lib/widget/home.dart b/lib/widget/home.dart index 5e237a77..f44a4e32 100644 --- a/lib/widget/home.dart +++ b/lib/widget/home.dart @@ -131,7 +131,7 @@ class _HomeState extends State { final albumRepo = AlbumRepo(AlbumRemoteDataSource()); try { return await ImportPotentialSharedAlbum(fileRepo, albumRepo)( - widget.account, Pref().getAccountSettings(widget.account)); + widget.account, AccountPref.of(widget.account)); } catch (e, stacktrace) { _log.shout( "[_importPotentialSharedAlbum] Failed while ImportPotentialSharedAlbum", diff --git a/lib/widget/home_albums.dart b/lib/widget/home_albums.dart index a8c941ce..f2c8dc5a 100644 --- a/lib/widget/home_albums.dart +++ b/lib/widget/home_albums.dart @@ -459,10 +459,7 @@ class _HomeAlbumsState extends State } }).map((e) => e.item2); itemStreamListItems = [ - if (Pref() - .getAccountSettings(widget.account) - .isEnableFaceRecognitionApp == - true) + if (AccountPref.of(widget.account).isEnableFaceRecognitionAppOr()) _buildPersonItem(context), _buildSharingItem(context), _buildArchiveItem(context), diff --git a/lib/widget/settings.dart b/lib/widget/settings.dart index 3bdc01f7..266b01da 100644 --- a/lib/widget/settings.dart +++ b/lib/widget/settings.dart @@ -408,9 +408,9 @@ class _AccountSettingsState extends State { super.initState(); _account = widget.account; - final settings = Pref().getAccountSettings(_account); - _isEnableFaceRecognitionApp = settings.isEnableFaceRecognitionApp; - _shareFolder = settings.shareFolder; + final settings = AccountPref.of(_account); + _isEnableFaceRecognitionApp = settings.isEnableFaceRecognitionAppOr(); + _shareFolder = settings.getShareFolderOr(); } @override @@ -491,8 +491,8 @@ class _AccountSettingsState extends State { _log.fine("[_onIncludedFoldersPressed] No changes"); return; } - final accounts = Pref().getAccounts2()!; - if (accounts.any((element) => element.account == result)) { + final accounts = Pref().getAccounts3()!; + if (accounts.contains(result)) { // conflict with another account. This normally won't happen because // the app passwords are unique to each entry, but just in case Navigator.of(context).pop(); @@ -503,7 +503,7 @@ class _AccountSettingsState extends State { return; } - final index = _findAccount(_account, accounts); + final index = accounts.indexOf(_account); if (index < 0) { _log.shout("[_onIncludedFoldersPressed] Account not found: $_account"); SnackBarManager().showSnackBar(SnackBar( @@ -513,11 +513,8 @@ class _AccountSettingsState extends State { return; } - final newAccount = accounts[index].copyWith( - account: result, - ); - accounts[index] = newAccount; - if (!await Pref().setAccounts2(accounts)) { + accounts[index] = result; + if (!await Pref().setAccounts3(accounts)) { SnackBarManager().showSnackBar(SnackBar( content: Text(L10n.global().writePreferenceFailureNotification), duration: k.snackBarDurationNormal, @@ -557,10 +554,7 @@ class _AccountSettingsState extends State { setState(() { _isEnableFaceRecognitionApp = value; }); - if (!await _modifyAccountSettings( - _account, - isEnableFaceRecognitionApp: value, - )) { + if (!await AccountPref.of(_account).setEnableFaceRecognitionApp(value)) { _log.severe("[_onEnableFaceRecognitionAppChanged] Failed writing pref"); SnackBarManager().showSnackBar(SnackBar( content: Text(L10n.global().writePreferenceFailureNotification), @@ -582,10 +576,7 @@ class _AccountSettingsState extends State { setState(() { _shareFolder = value; }); - if (!await _modifyAccountSettings( - _account, - shareFolder: value, - )) { + if (!await AccountPref.of(_account).setShareFolder(value)) { _log.severe("[_setShareFolder] Failed writing pref"); SnackBarManager().showSnackBar(SnackBar( content: Text(L10n.global().writePreferenceFailureNotification), @@ -601,36 +592,6 @@ class _AccountSettingsState extends State { } } - static Future _modifyAccountSettings( - Account account, { - bool? isEnableFaceRecognitionApp, - String? shareFolder, - }) { - try { - final accounts = Pref().getAccounts2()!; - final index = _findAccount(account, accounts); - accounts[index] = accounts[index].copyWith( - settings: accounts[index].settings.copyWith( - isEnableFaceRecognitionApp: isEnableFaceRecognitionApp, - shareFolder: shareFolder, - ), - ); - return Pref().setAccounts2(accounts); - } catch (e, stackTrace) { - _log.severe( - "[_modifyAccountSettings] Failed while setting account settings", - e, - stackTrace); - return Future.value(false); - } - } - - /// Return the index of [account] in [Pref.getAccounts2] - static int _findAccount(Account account, [List? accounts]) { - final from = accounts ?? Pref().getAccounts2Or([]); - return from.indexWhere((element) => element.account == account); - } - bool _hasModified = false; late Account _account; late bool _isEnableFaceRecognitionApp; diff --git a/lib/widget/sharing_browser.dart b/lib/widget/sharing_browser.dart index 48fe00d1..16f28694 100644 --- a/lib/widget/sharing_browser.dart +++ b/lib/widget/sharing_browser.dart @@ -325,7 +325,7 @@ class _SharingBrowserState extends State { final albumRepo = AlbumRepo(AlbumRemoteDataSource()); try { return await ImportPotentialSharedAlbum(fileRepo, albumRepo)( - widget.account, Pref().getAccountSettings(widget.account)); + widget.account, AccountPref.of(widget.account)); } catch (e, stackTrace) { _log.shout( "[_importPotentialSharedAlbum] Failed while ImportPotentialSharedAlbum", diff --git a/lib/widget/sign_in.dart b/lib/widget/sign_in.dart index 83522688..661360a2 100644 --- a/lib/widget/sign_in.dart +++ b/lib/widget/sign_in.dart @@ -9,6 +9,7 @@ import 'package:nc_photos/help_utils.dart' as help_utils; import 'package:nc_photos/iterable_extension.dart'; import 'package:nc_photos/platform/k.dart' as platform_k; import 'package:nc_photos/pref.dart'; +import 'package:nc_photos/pref_util.dart' as pref_util; import 'package:nc_photos/string_extension.dart'; import 'package:nc_photos/theme.dart'; import 'package:nc_photos/widget/connect.dart'; @@ -229,36 +230,44 @@ class _SignInState extends State { ); } - void _connect() { + Future _connect() async { _formKey.currentState!.save(); - final account = Account(_formValue.scheme, _formValue.address, - _formValue.username.toCi(), _formValue.password, [""]); + Account? account = Account( + Account.newId(), + _formValue.scheme, + _formValue.address, + _formValue.username.toCi(), + _formValue.password, + [""], + ); _log.info("[_connect] Try connecting with account: $account"); - Navigator.pushNamed(context, Connect.routeName, - arguments: ConnectArguments(account)) - .then((result) { - return result != null - ? Navigator.pushNamed(context, RootPicker.routeName, - arguments: RootPickerArguments(result)) - : Future.value(null); - }).then((result) { - if (result != null) { - // we've got a good account - final pa = PrefAccount(result); - // only signing in with app password would trigger distinct - final accounts = (Pref().getAccounts2Or([])..add(pa)).distinctIf( - (a, b) => a.account == b.account, - (a) => a.account.hashCode, - ); - Pref() - ..setAccounts2(accounts) - ..setCurrentAccountIndex( - accounts.indexWhere((element) => element.account == result)); - Navigator.pushNamedAndRemoveUntil( - context, Home.routeName, (route) => false, - arguments: HomeArguments(pa.account)); - } - }); + account = await Navigator.pushNamed(context, Connect.routeName, + arguments: ConnectArguments(account)); + if (account == null) { + // connection failed + return; + } + account = await Navigator.pushNamed(context, RootPicker.routeName, + arguments: RootPickerArguments(account)); + if (account == null) { + // ??? + return; + } + // we've got a good account + // only signing in with app password would trigger distinct + final accounts = (Pref().getAccounts3Or([])..add(account)).distinct(); + try { + AccountPref.setGlobalInstance( + account, await pref_util.loadAccountPref(account)); + } catch (e, stackTrace) { + _log.shout("[_connect] Failed reading pref for account: $account", e, + stackTrace); + } + Pref() + ..setAccounts3(accounts) + ..setCurrentAccountIndex(accounts.indexOf(account)); + Navigator.pushNamedAndRemoveUntil(context, Home.routeName, (route) => false, + arguments: HomeArguments(account)); } final _formKey = GlobalKey(); diff --git a/test/test_util.dart b/test/test_util.dart index 45f031bb..f030811e 100644 --- a/test/test_util.dart +++ b/test/test_util.dart @@ -167,13 +167,14 @@ void initLog() { } Account buildAccount({ + String id = "123456-000000", String scheme = "http", String address = "example.com", String username = "admin", String password = "pass", List roots = const [""], }) => - Account(scheme, address, username.toCi(), password, roots); + Account(id, scheme, address, username.toCi(), password, roots); /// Build a mock [File] pointing to a album JSON file /// diff --git a/test/use_case/compat/v34_test.dart b/test/use_case/compat/v34_test.dart new file mode 100644 index 00000000..c655f8ff --- /dev/null +++ b/test/use_case/compat/v34_test.dart @@ -0,0 +1,87 @@ +import 'dart:convert'; + +import 'package:nc_photos/platform/universal_storage.dart'; +import 'package:nc_photos/use_case/compat/v34.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:test/test.dart'; + +void main() { + group("CompatV34", () { + group("isPrefNeedMigration", () { + test("w/ accounts2", () async { + SharedPreferences.setMockInitialValues({ + "accounts2": [ + """{"account":{"scheme":"http","address":"example.com","username":"admin","password":"123456","roots":["dir","dir2"]},"settings":{"isEnableFaceRecognitionApp":true,"shareFolder":""}}""", + ], + }); + expect(await CompatV34.isPrefNeedMigration(), true); + }); + + test("w/ accounts", () async { + SharedPreferences.setMockInitialValues({ + "accounts": [ + """{"scheme":"http","address":"example.com","username":"admin","password":"123456","roots":["dir","dir2"]}""", + ], + }); + expect(await CompatV34.isPrefNeedMigration(), true); + }); + + test("w/o accounts(2)", () async { + SharedPreferences.setMockInitialValues({ + "hello": "world", + }); + expect(await CompatV34.isPrefNeedMigration(), false); + }); + }); + + group("migratePref", () { + test("from v1", () async { + SharedPreferences.setMockInitialValues({ + "accounts": [ + """{"scheme":"http","address":"example.com","username":"admin","password":"123456","roots":["dir","dir2"]}""", + ], + }); + final storage = UniversalMemoryStorage(); + await CompatV34.migratePref(storage); + final pref = await SharedPreferences.getInstance(); + final result = pref.getStringList("accounts3"); + expect(result?.length, 1); + expect( + result![0], + matches(RegExp( + r"""\{"scheme":"http","address":"example.com","username":"admin","password":"123456","roots":\["dir","dir2"\],"id":"[0-9a-f]+-[0-9a-f]+"\}""")), + ); + expect(pref.containsKey("accounts"), false); + final id = jsonDecode(result[0])["id"]; + expect( + await storage.getString("accounts/$id/pref"), + """{"isEnableFaceRecognitionApp":true,"shareFolder":""}""", + ); + }); + + test("from v32", () async { + SharedPreferences.setMockInitialValues({ + "accounts2": [ + """{"account":{"scheme":"http","address":"example.com","username":"admin","password":"123456","roots":["dir","dir2"]},"settings":{"isEnableFaceRecognitionApp":true,"shareFolder":""}}""", + ], + }); + final storage = UniversalMemoryStorage(); + await CompatV34.migratePref(storage); + final pref = await SharedPreferences.getInstance(); + final result = pref.getStringList("accounts3"); + expect(result?.length, 1); + expect( + result![0], + matches(RegExp( + r"""\{"scheme":"http","address":"example.com","username":"admin","password":"123456","roots":\["dir","dir2"\],"id":"[0-9a-f]+-[0-9a-f]+"\}""")), + ); + expect(pref.containsKey("accounts2"), false); + final id = jsonDecode(result[0])["id"]; + expect( + await storage.getString("accounts/$id/pref"), + """{"isEnableFaceRecognitionApp":true,"shareFolder":""}""", + ); + }); + }); + }); +}