Move account settings out of pref

This commit is contained in:
Ming Ming 2021-12-05 20:02:22 +08:00
parent 617f121810
commit dbd9679cba
17 changed files with 492 additions and 256 deletions

View file

@ -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<String>? 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<String>();
JsonObj toJson() => {
"id": id,
"scheme": scheme,
"address": address,
"username": username.toString(),
@ -62,10 +76,11 @@ class Account with EquatableMixin {
};
@override
List<Object> get props => [scheme, address, username, password, _roots];
List<Object> get props => [id, scheme, address, username, password, _roots];
List<String> 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<String> _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";

View file

@ -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;
}

View file

@ -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<void> _initPref() async {
}
}
Future<void> _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<void> _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");

View file

@ -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<void> 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 = <String, dynamic>{};
}

View file

@ -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<PrefAccount>? getAccounts2() {
final jsonObjs = provider.getStringList(PrefKey.accounts2);
return jsonObjs?.map((e) => PrefAccount.fromJson(jsonDecode(e))).toList();
List<Account>? getAccounts3() {
final jsonObjs = provider.getStringList(PrefKey.accounts3);
return jsonObjs?.map((e) => Account.fromJson(jsonDecode(e))).toList();
}
List<PrefAccount> getAccounts2Or(List<PrefAccount> def) =>
getAccounts2() ?? def;
Future<bool> setAccounts2(List<PrefAccount> value) {
List<Account> getAccounts3Or(List<Account> def) => getAccounts3() ?? def;
Future<bool> setAccounts3(List<Account> 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<bool> setEnableFaceRecognitionApp(bool value) =>
provider.setBool(PrefKey.isEnableFaceRecognitionApp, value);
String? getShareFolder() => provider.getString(PrefKey.shareFolder);
String getShareFolderOr([String def = ""]) => getShareFolder() ?? def;
Future<bool> setShareFolder(String value) =>
provider.setString(PrefKey.shareFolder, value);
final PrefProvider provider;
static final _insts = <String, AccountPref>{};
}
/// Provide the data for [Pref]
abstract class PrefProvider {
bool? getBool(PrefKey key);
@ -163,9 +200,14 @@ abstract class PrefProvider {
int? getInt(PrefKey key);
Future<bool> setInt(PrefKey key, int value);
String? getString(PrefKey key);
Future<bool> setString(PrefKey key, String value);
List<String>? getStringList(PrefKey key);
Future<bool> setStringList(PrefKey key, List<String> value);
Future<bool> clear();
bool _onPostSet(bool result, PrefKey key, dynamic value) {
if (result) {
KiwiContainer().resolve<EventBus>().fire(PrefUpdatedEvent(key, value));
@ -179,8 +221,12 @@ abstract class PrefProvider {
/// [Pref] stored with [SharedPreferences] lib
class PrefSharedPreferencesProvider extends PrefProvider {
Future<void> 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<void> init() async {
final prefStr = await platform.UniversalStorage().getString(name) ?? "{}";
_data
..clear()
..addAll(jsonDecode(prefStr));
}
@override
getBool(PrefKey key) => _get<bool>(key);
@override
setBool(PrefKey key, bool value) => _set(key, value);
@override
getInt(PrefKey key) => _get<int>(key);
@override
setInt(PrefKey key, int value) => _set(key, value);
@override
getString(PrefKey key) => _get<String>(key);
@override
setString(PrefKey key, String value) => _set(key, value);
@override
getStringList(PrefKey key) => _get<List<String>>(key);
@override
setStringList(PrefKey key, List<String> value) => _set(key, value);
@override
clear() async {
await platform.UniversalStorage().remove(name);
_data.clear();
return true;
}
T? _get<T>(PrefKey key) => _data[key.toStringKey()];
Future<bool> _set<T>(PrefKey key, T value) async {
return _onPostSet(await _update(key, value), key, value);
}
Future<bool> _update<T>(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 = <String, dynamic>{};
}
/// [Pref] stored in memory, useful in unit tests
class PrefMemoryProvider extends PrefProvider {
PrefMemoryProvider([
Map<String, dynamic> initialData = const <String, dynamic>{},
]) : _data = Map.of(initialData);
@override
getBool(PrefKey key) => _data[key.toStringKey()];
getBool(PrefKey key) => _get<bool>(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<int>(key);
@override
setInt(PrefKey key, int value) => _set(key, value);
@override
getString(PrefKey key) => _get<String>(key);
@override
setString(PrefKey key, String value) => _set(key, value);
@override
getStringList(PrefKey key) => _get<List<String>>(key);
@override
setStringList(PrefKey key, List<String> value) => _set(key, value);
@override
clear() async {
_data.clear();
return true;
}
@override
getInt(PrefKey key) => _data[key.toStringKey()];
T? _get<T>(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<String> value) async {
Future<bool> _set<T>(PrefKey key, T value) async {
return _onPostSet(() {
_data[key.toStringKey()] = value;
return true;
@ -258,48 +374,8 @@ class PrefMemoryProvider extends PrefProvider {
final Map<String, dynamic> _data;
}
class PrefAccount {
const PrefAccount(
this.account, [
this.settings = const AccountSettings(),
]);
factory PrefAccount.fromJson(JsonObj json) {
return PrefAccount(
Account.fromJson(json["account"].cast<String, dynamic>()),
AccountSettings.fromJson(json["settings"].cast<String, dynamic>()),
);
}
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;
}
}

8
lib/pref_util.dart Normal file
View file

@ -0,0 +1,8 @@
import 'package:nc_photos/account.dart';
import 'package:nc_photos/pref.dart';
Future<AccountPref> loadAccountPref(Account account) async {
final provider = PrefUniversalStorageProvider("accounts/${account.id}/pref");
await provider.init();
return AccountPref.scoped(provider);
}

View file

@ -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<bool> isPrefNeedMigration() async {
final pref = await SharedPreferences.getInstance();
return pref.containsKey("accounts2") || pref.containsKey("accounts");
}
static Future<void> migratePref(UniversalStorage storage) async {
final pref = await SharedPreferences.getInstance();
if (pref.containsKey("accounts2")) {
return _migratePrefV2(pref, storage);
} else {
return _migratePrefV1(pref, storage);
}
}
static Future<void> _migratePrefV2(
SharedPreferences pref, UniversalStorage storage) async {
final jsons = pref.getStringList("accounts2");
if (jsons == null) {
return;
}
_log.info("[migratePref] Migrate Pref.accounts2");
final newJsons = <JsonObj>[];
for (final j in jsons) {
final JsonObj account2 = jsonDecode(j).cast<String, dynamic>();
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<void> _migratePrefV1(
SharedPreferences pref, UniversalStorage storage) async {
final jsons = pref.getStringList("accounts");
if (jsons == null) {
return;
}
_log.info("[migratePref] Migrate Pref.accounts");
final newJsons = <JsonObj>[];
for (final j in jsons) {
final JsonObj account = jsonDecode(j).cast<String, dynamic>();
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");
}

View file

@ -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<List<Album>> call(Account account, AccountSettings settings) async {
Future<List<Album>> call(Account account, AccountPref accountPref) async {
_log.info("[call] $account");
final products = <Album>[];
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 {

View file

@ -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<List<File>> call(Account account, AccountSettings settings) async {
Future<List<File>> call(Account account, AccountPref accountPref) async {
final results = <File>[];
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

View file

@ -29,20 +29,20 @@ class _AccountPickerDialogState extends State<AccountPickerDialog> {
@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<AccountPickerDialog> {
);
}
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<void> _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<AccountPickerDialog> {
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<void> _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<PrefAccount> _accounts;
late List<Account> _accounts;
static final _log =
Logger("widget.account_picker_dialog._AccountPickerDialogState");

View file

@ -131,7 +131,7 @@ class _HomeState extends State<Home> {
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",

View file

@ -459,10 +459,7 @@ class _HomeAlbumsState extends State<HomeAlbums>
}
}).map((e) => e.item2);
itemStreamListItems = [
if (Pref()
.getAccountSettings(widget.account)
.isEnableFaceRecognitionApp ==
true)
if (AccountPref.of(widget.account).isEnableFaceRecognitionAppOr())
_buildPersonItem(context),
_buildSharingItem(context),
_buildArchiveItem(context),

View file

@ -408,9 +408,9 @@ class _AccountSettingsState extends State<AccountSettingsWidget> {
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<AccountSettingsWidget> {
_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<AccountSettingsWidget> {
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<AccountSettingsWidget> {
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<AccountSettingsWidget> {
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<AccountSettingsWidget> {
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<AccountSettingsWidget> {
}
}
static Future<bool> _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<PrefAccount>? accounts]) {
final from = accounts ?? Pref().getAccounts2Or([]);
return from.indexWhere((element) => element.account == account);
}
bool _hasModified = false;
late Account _account;
late bool _isEnableFaceRecognitionApp;

View file

@ -325,7 +325,7 @@ class _SharingBrowserState extends State<SharingBrowser> {
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",

View file

@ -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<SignIn> {
);
}
void _connect() {
Future<void> _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<Account>(context, Connect.routeName,
arguments: ConnectArguments(account))
.then<Account?>((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<Account>(context, Connect.routeName,
arguments: ConnectArguments(account));
if (account == null) {
// connection failed
return;
}
account = await Navigator.pushNamed<Account>(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<FormState>();

View file

@ -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<String> 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
///

View file

@ -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":""}""",
);
});
});
});
}