mirror of
https://gitlab.com/nkming2/nc-photos.git
synced 2025-03-22 06:59:21 +01:00
Move account settings out of pref
This commit is contained in:
parent
617f121810
commit
dbd9679cba
17 changed files with 492 additions and 256 deletions
|
@ -1,3 +1,5 @@
|
||||||
|
import 'dart:math';
|
||||||
|
|
||||||
import 'package:equatable/equatable.dart';
|
import 'package:equatable/equatable.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:nc_photos/ci_string.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
|
/// Details of a remote Nextcloud server account
|
||||||
class Account with EquatableMixin {
|
class Account with EquatableMixin {
|
||||||
Account(
|
Account(
|
||||||
|
this.id,
|
||||||
this.scheme,
|
this.scheme,
|
||||||
String address,
|
String address,
|
||||||
this.username,
|
this.username,
|
||||||
|
@ -20,6 +23,7 @@ class Account with EquatableMixin {
|
||||||
}
|
}
|
||||||
|
|
||||||
Account copyWith({
|
Account copyWith({
|
||||||
|
String? id,
|
||||||
String? scheme,
|
String? scheme,
|
||||||
String? address,
|
String? address,
|
||||||
CiString? username,
|
CiString? username,
|
||||||
|
@ -27,6 +31,7 @@ class Account with EquatableMixin {
|
||||||
List<String>? roots,
|
List<String>? roots,
|
||||||
}) {
|
}) {
|
||||||
return Account(
|
return Account(
|
||||||
|
id ?? this.id,
|
||||||
scheme ?? this.scheme,
|
scheme ?? this.scheme,
|
||||||
address ?? this.address,
|
address ?? this.address,
|
||||||
username ?? this.username,
|
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
|
@override
|
||||||
toString() {
|
toString() {
|
||||||
return "$runtimeType {"
|
return "$runtimeType {"
|
||||||
|
"id: '$id', "
|
||||||
"scheme: '$scheme', "
|
"scheme: '$scheme', "
|
||||||
"address: '${kDebugMode ? address : "***"}', "
|
"address: '${kDebugMode ? address : "***"}', "
|
||||||
"username: '${kDebugMode ? username : "***"}', "
|
"username: '${kDebugMode ? username : "***"}', "
|
||||||
|
@ -47,13 +59,15 @@ class Account with EquatableMixin {
|
||||||
}
|
}
|
||||||
|
|
||||||
Account.fromJson(JsonObj json)
|
Account.fromJson(JsonObj json)
|
||||||
: scheme = json["scheme"],
|
: id = json["id"],
|
||||||
|
scheme = json["scheme"],
|
||||||
address = json["address"],
|
address = json["address"],
|
||||||
username = CiString(json["username"]),
|
username = CiString(json["username"]),
|
||||||
password = json["password"],
|
password = json["password"],
|
||||||
_roots = json["roots"].cast<String>();
|
_roots = json["roots"].cast<String>();
|
||||||
|
|
||||||
JsonObj toJson() => {
|
JsonObj toJson() => {
|
||||||
|
"id": id,
|
||||||
"scheme": scheme,
|
"scheme": scheme,
|
||||||
"address": address,
|
"address": address,
|
||||||
"username": username.toString(),
|
"username": username.toString(),
|
||||||
|
@ -62,10 +76,11 @@ class Account with EquatableMixin {
|
||||||
};
|
};
|
||||||
|
|
||||||
@override
|
@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;
|
List<String> get roots => _roots;
|
||||||
|
|
||||||
|
final String id;
|
||||||
final String scheme;
|
final String scheme;
|
||||||
final String address;
|
final String address;
|
||||||
final CiString username;
|
final CiString username;
|
||||||
|
@ -73,58 +88,6 @@ class Account with EquatableMixin {
|
||||||
final List<String> _roots;
|
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 {
|
extension AccountExtension on Account {
|
||||||
String get url => "$scheme://$address";
|
String get url => "$scheme://$address";
|
||||||
|
|
||||||
|
|
|
@ -274,7 +274,7 @@ class ScanAccountDirBloc
|
||||||
// no data in this bloc, ignore
|
// no data in this bloc, ignore
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (ev.key == PrefKey.accounts2) {
|
if (ev.key == PrefKey.accounts3) {
|
||||||
_refreshThrottler.trigger(
|
_refreshThrottler.trigger(
|
||||||
maxResponceTime: const Duration(seconds: 3),
|
maxResponceTime: const Duration(seconds: 3),
|
||||||
maxPendingCount: 10,
|
maxPendingCount: 10,
|
||||||
|
@ -301,9 +301,9 @@ class ScanAccountDirBloc
|
||||||
try {
|
try {
|
||||||
final fileRepo = FileRepo(dataSrc);
|
final fileRepo = FileRepo(dataSrc);
|
||||||
// include files shared with this account
|
// include files shared with this account
|
||||||
final settings = Pref().getAccountSettings(account);
|
final settings = AccountPref.of(account);
|
||||||
final shareDir =
|
final shareDir = File(
|
||||||
File(path: file_util.unstripPath(account, settings.shareFolder));
|
path: file_util.unstripPath(account, settings.getShareFolderOr()));
|
||||||
bool isShareDirIncluded = false;
|
bool isShareDirIncluded = false;
|
||||||
|
|
||||||
for (final r in account.roots) {
|
for (final r in account.roots) {
|
||||||
|
@ -320,8 +320,12 @@ class ScanAccountDirBloc
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isShareDirIncluded) {
|
if (!isShareDirIncluded) {
|
||||||
final files = await Ls(fileRepo)(account,
|
final files = await Ls(fileRepo)(
|
||||||
File(path: file_util.unstripPath(account, settings.shareFolder)));
|
account,
|
||||||
|
File(
|
||||||
|
path: file_util.unstripPath(account, settings.getShareFolderOr()),
|
||||||
|
),
|
||||||
|
);
|
||||||
final sharedFiles =
|
final sharedFiles =
|
||||||
files.where((f) => !f.isOwned(account.username)).toList();
|
files.where((f) => !f.isOwned(account.username)).toList();
|
||||||
yield ScanAccountDirBlocSuccess(getState().files + sharedFiles);
|
yield ScanAccountDirBlocSuccess(getState().files + sharedFiles);
|
||||||
|
@ -347,9 +351,9 @@ class ScanAccountDirBloc
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final settings = Pref().getAccountSettings(account);
|
final settings = AccountPref.of(account);
|
||||||
final shareDir =
|
final shareDir =
|
||||||
File(path: file_util.unstripPath(account, settings.shareFolder));
|
File(path: file_util.unstripPath(account, settings.getShareFolderOr()));
|
||||||
if (file_util.isUnderDir(file, shareDir)) {
|
if (file_util.isUnderDir(file, shareDir)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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/features.dart' as features;
|
||||||
import 'package:nc_photos/platform/k.dart' as platform_k;
|
import 'package:nc_photos/platform/k.dart' as platform_k;
|
||||||
import 'package:nc_photos/pref.dart';
|
import 'package:nc_photos/pref.dart';
|
||||||
|
import 'package:nc_photos/pref_util.dart' as pref_util;
|
||||||
import 'package:nc_photos/widget/my_app.dart';
|
import 'package:nc_photos/widget/my_app.dart';
|
||||||
|
|
||||||
void main() async {
|
void main() async {
|
||||||
|
@ -34,6 +35,7 @@ void main() async {
|
||||||
_initLog();
|
_initLog();
|
||||||
_initKiwi();
|
_initKiwi();
|
||||||
await _initPref();
|
await _initPref();
|
||||||
|
await _initAccountPrefs();
|
||||||
await _initDeviceInfo();
|
await _initDeviceInfo();
|
||||||
_initBloc();
|
_initBloc();
|
||||||
_initEquatable();
|
_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 {
|
Future<void> _initDeviceInfo() async {
|
||||||
if (platform_k.isAndroid) {
|
if (platform_k.isAndroid) {
|
||||||
await AndroidInfo.init();
|
await AndroidInfo.init();
|
||||||
|
@ -150,3 +163,5 @@ class _BlocObserver extends BlocObserver {
|
||||||
|
|
||||||
static final _log = Logger("main._BlocObserver");
|
static final _log = Logger("main._BlocObserver");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final _log = Logger("main");
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
import 'dart:typed_data';
|
import 'dart:typed_data';
|
||||||
|
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
|
||||||
/// Store simple contents across different platforms
|
/// Store simple contents across different platforms
|
||||||
///
|
///
|
||||||
/// On mobile, the contents will be persisted as a file. On web, the contents
|
/// 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);
|
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>{};
|
||||||
|
}
|
||||||
|
|
247
lib/pref.dart
247
lib/pref.dart
|
@ -4,8 +4,9 @@ import 'package:event_bus/event_bus.dart';
|
||||||
import 'package:kiwi/kiwi.dart';
|
import 'package:kiwi/kiwi.dart';
|
||||||
import 'package:nc_photos/account.dart';
|
import 'package:nc_photos/account.dart';
|
||||||
import 'package:nc_photos/event/event.dart';
|
import 'package:nc_photos/event/event.dart';
|
||||||
import 'package:nc_photos/type.dart';
|
import 'package:nc_photos/mobile/platform.dart'
|
||||||
import 'package:nc_photos/use_case/compat/v32.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';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
|
||||||
class Pref {
|
class Pref {
|
||||||
|
@ -22,16 +23,15 @@ class Pref {
|
||||||
_inst = pref;
|
_inst = pref;
|
||||||
}
|
}
|
||||||
|
|
||||||
List<PrefAccount>? getAccounts2() {
|
List<Account>? getAccounts3() {
|
||||||
final jsonObjs = provider.getStringList(PrefKey.accounts2);
|
final jsonObjs = provider.getStringList(PrefKey.accounts3);
|
||||||
return jsonObjs?.map((e) => PrefAccount.fromJson(jsonDecode(e))).toList();
|
return jsonObjs?.map((e) => Account.fromJson(jsonDecode(e))).toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
List<PrefAccount> getAccounts2Or(List<PrefAccount> def) =>
|
List<Account> getAccounts3Or(List<Account> def) => getAccounts3() ?? def;
|
||||||
getAccounts2() ?? def;
|
Future<bool> setAccounts3(List<Account> value) {
|
||||||
Future<bool> setAccounts2(List<PrefAccount> value) {
|
|
||||||
final jsons = value.map((e) => jsonEncode(e.toJson())).toList();
|
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);
|
int? getCurrentAccountIndex() => provider.getInt(PrefKey.currentAccountIndex);
|
||||||
|
@ -155,6 +155,43 @@ class Pref {
|
||||||
static Pref? _inst;
|
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]
|
/// Provide the data for [Pref]
|
||||||
abstract class PrefProvider {
|
abstract class PrefProvider {
|
||||||
bool? getBool(PrefKey key);
|
bool? getBool(PrefKey key);
|
||||||
|
@ -163,9 +200,14 @@ abstract class PrefProvider {
|
||||||
int? getInt(PrefKey key);
|
int? getInt(PrefKey key);
|
||||||
Future<bool> setInt(PrefKey key, int value);
|
Future<bool> setInt(PrefKey key, int value);
|
||||||
|
|
||||||
|
String? getString(PrefKey key);
|
||||||
|
Future<bool> setString(PrefKey key, String value);
|
||||||
|
|
||||||
List<String>? getStringList(PrefKey key);
|
List<String>? getStringList(PrefKey key);
|
||||||
Future<bool> setStringList(PrefKey key, List<String> value);
|
Future<bool> setStringList(PrefKey key, List<String> value);
|
||||||
|
|
||||||
|
Future<bool> clear();
|
||||||
|
|
||||||
bool _onPostSet(bool result, PrefKey key, dynamic value) {
|
bool _onPostSet(bool result, PrefKey key, dynamic value) {
|
||||||
if (result) {
|
if (result) {
|
||||||
KiwiContainer().resolve<EventBus>().fire(PrefUpdatedEvent(key, value));
|
KiwiContainer().resolve<EventBus>().fire(PrefUpdatedEvent(key, value));
|
||||||
|
@ -179,8 +221,12 @@ abstract class PrefProvider {
|
||||||
/// [Pref] stored with [SharedPreferences] lib
|
/// [Pref] stored with [SharedPreferences] lib
|
||||||
class PrefSharedPreferencesProvider extends PrefProvider {
|
class PrefSharedPreferencesProvider extends PrefProvider {
|
||||||
Future<void> init() async {
|
Future<void> init() async {
|
||||||
if (await CompatV32.isPrefNeedMigration()) {
|
// Obsolete, CompatV34 is compatible with pre v32 versions
|
||||||
await CompatV32.migratePref();
|
// if (await CompatV32.isPrefNeedMigration()) {
|
||||||
|
// await CompatV32.migratePref();
|
||||||
|
// }
|
||||||
|
if (await CompatV34.isPrefNeedMigration()) {
|
||||||
|
await CompatV34.migratePref(platform.UniversalStorage());
|
||||||
}
|
}
|
||||||
return SharedPreferences.getInstance().then((pref) {
|
return SharedPreferences.getInstance().then((pref) {
|
||||||
_pref = pref;
|
_pref = pref;
|
||||||
|
@ -204,6 +250,15 @@ class PrefSharedPreferencesProvider extends PrefProvider {
|
||||||
return _onPostSet(await _pref.setInt(key.toStringKey(), value), key, value);
|
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
|
@override
|
||||||
getStringList(PrefKey key) => _pref.getStringList(key.toStringKey());
|
getStringList(PrefKey key) => _pref.getStringList(key.toStringKey());
|
||||||
|
|
||||||
|
@ -213,42 +268,103 @@ class PrefSharedPreferencesProvider extends PrefProvider {
|
||||||
await _pref.setStringList(key.toStringKey(), value), key, value);
|
await _pref.setStringList(key.toStringKey(), value), key, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
clear() => _pref.clear();
|
||||||
|
|
||||||
late SharedPreferences _pref;
|
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 {
|
class PrefMemoryProvider extends PrefProvider {
|
||||||
PrefMemoryProvider([
|
PrefMemoryProvider([
|
||||||
Map<String, dynamic> initialData = const <String, dynamic>{},
|
Map<String, dynamic> initialData = const <String, dynamic>{},
|
||||||
]) : _data = Map.of(initialData);
|
]) : _data = Map.of(initialData);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
getBool(PrefKey key) => _data[key.toStringKey()];
|
getBool(PrefKey key) => _get<bool>(key);
|
||||||
|
@override
|
||||||
|
setBool(PrefKey key, bool value) => _set(key, value);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
setBool(PrefKey key, bool value) async {
|
getInt(PrefKey key) => _get<int>(key);
|
||||||
return _onPostSet(() {
|
@override
|
||||||
_data[key.toStringKey()] = value;
|
setInt(PrefKey key, int value) => _set(key, value);
|
||||||
return true;
|
|
||||||
}(), 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
|
T? _get<T>(PrefKey key) => _data[key.toStringKey()];
|
||||||
getInt(PrefKey key) => _data[key.toStringKey()];
|
|
||||||
|
|
||||||
@override
|
Future<bool> _set<T>(PrefKey key, T value) async {
|
||||||
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 {
|
|
||||||
return _onPostSet(() {
|
return _onPostSet(() {
|
||||||
_data[key.toStringKey()] = value;
|
_data[key.toStringKey()] = value;
|
||||||
return true;
|
return true;
|
||||||
|
@ -258,48 +374,8 @@ class PrefMemoryProvider extends PrefProvider {
|
||||||
final Map<String, dynamic> _data;
|
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 {
|
enum PrefKey {
|
||||||
accounts2,
|
accounts3,
|
||||||
currentAccountIndex,
|
currentAccountIndex,
|
||||||
homePhotosZoomLevel,
|
homePhotosZoomLevel,
|
||||||
albumBrowserZoomLevel,
|
albumBrowserZoomLevel,
|
||||||
|
@ -321,13 +397,17 @@ enum PrefKey {
|
||||||
isAlbumBrowserShowDate,
|
isAlbumBrowserShowDate,
|
||||||
gpsMapProvider,
|
gpsMapProvider,
|
||||||
hasShownSharedAlbumInfo,
|
hasShownSharedAlbumInfo,
|
||||||
|
|
||||||
|
// account pref
|
||||||
|
isEnableFaceRecognitionApp,
|
||||||
|
shareFolder,
|
||||||
}
|
}
|
||||||
|
|
||||||
extension on PrefKey {
|
extension on PrefKey {
|
||||||
String toStringKey() {
|
String toStringKey() {
|
||||||
switch (this) {
|
switch (this) {
|
||||||
case PrefKey.accounts2:
|
case PrefKey.accounts3:
|
||||||
return "accounts2";
|
return "accounts3";
|
||||||
case PrefKey.currentAccountIndex:
|
case PrefKey.currentAccountIndex:
|
||||||
return "currentAccountIndex";
|
return "currentAccountIndex";
|
||||||
case PrefKey.homePhotosZoomLevel:
|
case PrefKey.homePhotosZoomLevel:
|
||||||
|
@ -370,6 +450,12 @@ extension on PrefKey {
|
||||||
return "gpsMapProvider";
|
return "gpsMapProvider";
|
||||||
case PrefKey.hasShownSharedAlbumInfo:
|
case PrefKey.hasShownSharedAlbumInfo:
|
||||||
return "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 {
|
extension PrefExtension on Pref {
|
||||||
Account? getCurrentAccount() {
|
Account? getCurrentAccount() {
|
||||||
try {
|
try {
|
||||||
return Pref().getAccounts2()![Pref().getCurrentAccountIndex()!].account;
|
return Pref().getAccounts3()![Pref().getCurrentAccountIndex()!];
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
AccountSettings getAccountSettings(Account account) {
|
|
||||||
return Pref()
|
|
||||||
.getAccounts2()!
|
|
||||||
.firstWhere((element) => element.account == account)
|
|
||||||
.settings;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
8
lib/pref_util.dart
Normal file
8
lib/pref_util.dart
Normal 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);
|
||||||
|
}
|
75
lib/use_case/compat/v34.dart
Normal file
75
lib/use_case/compat/v34.dart
Normal 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");
|
||||||
|
}
|
|
@ -2,6 +2,7 @@ import 'package:logging/logging.dart';
|
||||||
import 'package:nc_photos/account.dart';
|
import 'package:nc_photos/account.dart';
|
||||||
import 'package:nc_photos/entity/album.dart';
|
import 'package:nc_photos/entity/album.dart';
|
||||||
import 'package:nc_photos/entity/file.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/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/list_potential_shared_album.dart';
|
||||||
import 'package:nc_photos/use_case/move.dart';
|
import 'package:nc_photos/use_case/move.dart';
|
||||||
|
@ -10,10 +11,11 @@ import 'package:nc_photos/use_case/move.dart';
|
||||||
class ImportPotentialSharedAlbum {
|
class ImportPotentialSharedAlbum {
|
||||||
ImportPotentialSharedAlbum(this.fileRepo, this.albumRepo);
|
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");
|
_log.info("[call] $account");
|
||||||
final products = <Album>[];
|
final products = <Album>[];
|
||||||
final files = await ListPotentialSharedAlbum(fileRepo)(account, settings);
|
final files =
|
||||||
|
await ListPotentialSharedAlbum(fileRepo)(account, accountPref);
|
||||||
for (final f in files) {
|
for (final f in files) {
|
||||||
// check if the file is actually an album
|
// check if the file is actually an album
|
||||||
try {
|
try {
|
||||||
|
|
|
@ -2,6 +2,7 @@ import 'package:logging/logging.dart';
|
||||||
import 'package:nc_photos/account.dart';
|
import 'package:nc_photos/account.dart';
|
||||||
import 'package:nc_photos/entity/file.dart';
|
import 'package:nc_photos/entity/file.dart';
|
||||||
import 'package:nc_photos/entity/file_util.dart' as file_util;
|
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';
|
import 'package:nc_photos/use_case/ls.dart';
|
||||||
|
|
||||||
/// List all shared files that are potentially albums
|
/// List all shared files that are potentially albums
|
||||||
|
@ -10,11 +11,13 @@ import 'package:nc_photos/use_case/ls.dart';
|
||||||
class ListPotentialSharedAlbum {
|
class ListPotentialSharedAlbum {
|
||||||
ListPotentialSharedAlbum(this.fileRepo);
|
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 results = <File>[];
|
||||||
final ls = await Ls(fileRepo)(
|
final ls = await Ls(fileRepo)(
|
||||||
account,
|
account,
|
||||||
File(path: file_util.unstripPath(account, settings.shareFolder)),
|
File(
|
||||||
|
path: file_util.unstripPath(account, accountPref.getShareFolderOr()),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
for (final f in ls) {
|
for (final f in ls) {
|
||||||
// check owner
|
// check owner
|
||||||
|
|
|
@ -29,20 +29,20 @@ class _AccountPickerDialogState extends State<AccountPickerDialog> {
|
||||||
@override
|
@override
|
||||||
initState() {
|
initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_accounts = Pref().getAccounts2Or([]);
|
_accounts = Pref().getAccounts3Or([]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
build(BuildContext context) {
|
build(BuildContext context) {
|
||||||
final otherAccountOptions = _accounts
|
final otherAccountOptions = _accounts
|
||||||
.where((a) => a.account != widget.account)
|
.where((a) => a != widget.account)
|
||||||
.map((a) => SimpleDialogOption(
|
.map((a) => SimpleDialogOption(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||||
onPressed: () => _onItemPressed(a),
|
onPressed: () => _onItemPressed(a),
|
||||||
child: ListTile(
|
child: ListTile(
|
||||||
dense: true,
|
dense: true,
|
||||||
title: Text(a.account.url),
|
title: Text(a.url),
|
||||||
subtitle: Text(a.account.username.toString()),
|
subtitle: Text(a.username.toString()),
|
||||||
trailing: IconButton(
|
trailing: IconButton(
|
||||||
icon: Icon(
|
icon: Icon(
|
||||||
Icons.close,
|
Icons.close,
|
||||||
|
@ -101,21 +101,21 @@ class _AccountPickerDialogState extends State<AccountPickerDialog> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _onItemPressed(PrefAccount account) {
|
void _onItemPressed(Account account) {
|
||||||
Pref().setCurrentAccountIndex(_accounts.indexOf(account));
|
Pref().setCurrentAccountIndex(_accounts.indexOf(account));
|
||||||
Navigator.of(context).pushNamedAndRemoveUntil(Home.routeName, (_) => false,
|
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 {
|
try {
|
||||||
_removeAccount(account);
|
await _removeAccount(account);
|
||||||
setState(() {
|
setState(() {
|
||||||
_accounts = Pref().getAccounts2()!;
|
_accounts = Pref().getAccounts3()!;
|
||||||
});
|
});
|
||||||
SnackBarManager().showSnackBar(SnackBar(
|
SnackBarManager().showSnackBar(SnackBar(
|
||||||
content: Text(
|
content:
|
||||||
L10n.global().removeServerSuccessNotification(account.account.url)),
|
Text(L10n.global().removeServerSuccessNotification(account.url)),
|
||||||
duration: k.snackBarDurationNormal,
|
duration: k.snackBarDurationNormal,
|
||||||
));
|
));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
@ -133,24 +133,27 @@ class _AccountPickerDialogState extends State<AccountPickerDialog> {
|
||||||
arguments: AccountSettingsWidgetArguments(widget.account));
|
arguments: AccountSettingsWidgetArguments(widget.account));
|
||||||
}
|
}
|
||||||
|
|
||||||
void _removeAccount(PrefAccount account) {
|
Future<void> _removeAccount(Account account) async {
|
||||||
_log.info("[_removeAccount] Remove account: ${account.account}");
|
_log.info("[_removeAccount] Remove account: $account");
|
||||||
final currentAccounts = Pref().getAccounts2()!;
|
final accounts = Pref().getAccounts3()!;
|
||||||
final currentAccount =
|
final currentAccount = accounts[Pref().getCurrentAccountIndex()!];
|
||||||
currentAccounts[Pref().getCurrentAccountIndex()!];
|
accounts.remove(account);
|
||||||
final newAccounts = currentAccounts
|
final newAccountIndex = accounts.indexOf(currentAccount);
|
||||||
.where((element) => element.account != account.account)
|
|
||||||
.toList();
|
|
||||||
final newAccountIndex = newAccounts.indexOf(currentAccount);
|
|
||||||
if (newAccountIndex == -1) {
|
if (newAccountIndex == -1) {
|
||||||
throw StateError("Active account not found in resulting account list");
|
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()
|
Pref()
|
||||||
..setAccounts2(newAccounts)
|
..setAccounts3(accounts)
|
||||||
..setCurrentAccountIndex(newAccountIndex);
|
..setCurrentAccountIndex(newAccountIndex);
|
||||||
}
|
}
|
||||||
|
|
||||||
late List<PrefAccount> _accounts;
|
late List<Account> _accounts;
|
||||||
|
|
||||||
static final _log =
|
static final _log =
|
||||||
Logger("widget.account_picker_dialog._AccountPickerDialogState");
|
Logger("widget.account_picker_dialog._AccountPickerDialogState");
|
||||||
|
|
|
@ -131,7 +131,7 @@ class _HomeState extends State<Home> {
|
||||||
final albumRepo = AlbumRepo(AlbumRemoteDataSource());
|
final albumRepo = AlbumRepo(AlbumRemoteDataSource());
|
||||||
try {
|
try {
|
||||||
return await ImportPotentialSharedAlbum(fileRepo, albumRepo)(
|
return await ImportPotentialSharedAlbum(fileRepo, albumRepo)(
|
||||||
widget.account, Pref().getAccountSettings(widget.account));
|
widget.account, AccountPref.of(widget.account));
|
||||||
} catch (e, stacktrace) {
|
} catch (e, stacktrace) {
|
||||||
_log.shout(
|
_log.shout(
|
||||||
"[_importPotentialSharedAlbum] Failed while ImportPotentialSharedAlbum",
|
"[_importPotentialSharedAlbum] Failed while ImportPotentialSharedAlbum",
|
||||||
|
|
|
@ -459,10 +459,7 @@ class _HomeAlbumsState extends State<HomeAlbums>
|
||||||
}
|
}
|
||||||
}).map((e) => e.item2);
|
}).map((e) => e.item2);
|
||||||
itemStreamListItems = [
|
itemStreamListItems = [
|
||||||
if (Pref()
|
if (AccountPref.of(widget.account).isEnableFaceRecognitionAppOr())
|
||||||
.getAccountSettings(widget.account)
|
|
||||||
.isEnableFaceRecognitionApp ==
|
|
||||||
true)
|
|
||||||
_buildPersonItem(context),
|
_buildPersonItem(context),
|
||||||
_buildSharingItem(context),
|
_buildSharingItem(context),
|
||||||
_buildArchiveItem(context),
|
_buildArchiveItem(context),
|
||||||
|
|
|
@ -408,9 +408,9 @@ class _AccountSettingsState extends State<AccountSettingsWidget> {
|
||||||
super.initState();
|
super.initState();
|
||||||
_account = widget.account;
|
_account = widget.account;
|
||||||
|
|
||||||
final settings = Pref().getAccountSettings(_account);
|
final settings = AccountPref.of(_account);
|
||||||
_isEnableFaceRecognitionApp = settings.isEnableFaceRecognitionApp;
|
_isEnableFaceRecognitionApp = settings.isEnableFaceRecognitionAppOr();
|
||||||
_shareFolder = settings.shareFolder;
|
_shareFolder = settings.getShareFolderOr();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -491,8 +491,8 @@ class _AccountSettingsState extends State<AccountSettingsWidget> {
|
||||||
_log.fine("[_onIncludedFoldersPressed] No changes");
|
_log.fine("[_onIncludedFoldersPressed] No changes");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
final accounts = Pref().getAccounts2()!;
|
final accounts = Pref().getAccounts3()!;
|
||||||
if (accounts.any((element) => element.account == result)) {
|
if (accounts.contains(result)) {
|
||||||
// conflict with another account. This normally won't happen because
|
// conflict with another account. This normally won't happen because
|
||||||
// the app passwords are unique to each entry, but just in case
|
// the app passwords are unique to each entry, but just in case
|
||||||
Navigator.of(context).pop();
|
Navigator.of(context).pop();
|
||||||
|
@ -503,7 +503,7 @@ class _AccountSettingsState extends State<AccountSettingsWidget> {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
final index = _findAccount(_account, accounts);
|
final index = accounts.indexOf(_account);
|
||||||
if (index < 0) {
|
if (index < 0) {
|
||||||
_log.shout("[_onIncludedFoldersPressed] Account not found: $_account");
|
_log.shout("[_onIncludedFoldersPressed] Account not found: $_account");
|
||||||
SnackBarManager().showSnackBar(SnackBar(
|
SnackBarManager().showSnackBar(SnackBar(
|
||||||
|
@ -513,11 +513,8 @@ class _AccountSettingsState extends State<AccountSettingsWidget> {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
final newAccount = accounts[index].copyWith(
|
accounts[index] = result;
|
||||||
account: result,
|
if (!await Pref().setAccounts3(accounts)) {
|
||||||
);
|
|
||||||
accounts[index] = newAccount;
|
|
||||||
if (!await Pref().setAccounts2(accounts)) {
|
|
||||||
SnackBarManager().showSnackBar(SnackBar(
|
SnackBarManager().showSnackBar(SnackBar(
|
||||||
content: Text(L10n.global().writePreferenceFailureNotification),
|
content: Text(L10n.global().writePreferenceFailureNotification),
|
||||||
duration: k.snackBarDurationNormal,
|
duration: k.snackBarDurationNormal,
|
||||||
|
@ -557,10 +554,7 @@ class _AccountSettingsState extends State<AccountSettingsWidget> {
|
||||||
setState(() {
|
setState(() {
|
||||||
_isEnableFaceRecognitionApp = value;
|
_isEnableFaceRecognitionApp = value;
|
||||||
});
|
});
|
||||||
if (!await _modifyAccountSettings(
|
if (!await AccountPref.of(_account).setEnableFaceRecognitionApp(value)) {
|
||||||
_account,
|
|
||||||
isEnableFaceRecognitionApp: value,
|
|
||||||
)) {
|
|
||||||
_log.severe("[_onEnableFaceRecognitionAppChanged] Failed writing pref");
|
_log.severe("[_onEnableFaceRecognitionAppChanged] Failed writing pref");
|
||||||
SnackBarManager().showSnackBar(SnackBar(
|
SnackBarManager().showSnackBar(SnackBar(
|
||||||
content: Text(L10n.global().writePreferenceFailureNotification),
|
content: Text(L10n.global().writePreferenceFailureNotification),
|
||||||
|
@ -582,10 +576,7 @@ class _AccountSettingsState extends State<AccountSettingsWidget> {
|
||||||
setState(() {
|
setState(() {
|
||||||
_shareFolder = value;
|
_shareFolder = value;
|
||||||
});
|
});
|
||||||
if (!await _modifyAccountSettings(
|
if (!await AccountPref.of(_account).setShareFolder(value)) {
|
||||||
_account,
|
|
||||||
shareFolder: value,
|
|
||||||
)) {
|
|
||||||
_log.severe("[_setShareFolder] Failed writing pref");
|
_log.severe("[_setShareFolder] Failed writing pref");
|
||||||
SnackBarManager().showSnackBar(SnackBar(
|
SnackBarManager().showSnackBar(SnackBar(
|
||||||
content: Text(L10n.global().writePreferenceFailureNotification),
|
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;
|
bool _hasModified = false;
|
||||||
late Account _account;
|
late Account _account;
|
||||||
late bool _isEnableFaceRecognitionApp;
|
late bool _isEnableFaceRecognitionApp;
|
||||||
|
|
|
@ -325,7 +325,7 @@ class _SharingBrowserState extends State<SharingBrowser> {
|
||||||
final albumRepo = AlbumRepo(AlbumRemoteDataSource());
|
final albumRepo = AlbumRepo(AlbumRemoteDataSource());
|
||||||
try {
|
try {
|
||||||
return await ImportPotentialSharedAlbum(fileRepo, albumRepo)(
|
return await ImportPotentialSharedAlbum(fileRepo, albumRepo)(
|
||||||
widget.account, Pref().getAccountSettings(widget.account));
|
widget.account, AccountPref.of(widget.account));
|
||||||
} catch (e, stackTrace) {
|
} catch (e, stackTrace) {
|
||||||
_log.shout(
|
_log.shout(
|
||||||
"[_importPotentialSharedAlbum] Failed while ImportPotentialSharedAlbum",
|
"[_importPotentialSharedAlbum] Failed while ImportPotentialSharedAlbum",
|
||||||
|
|
|
@ -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/iterable_extension.dart';
|
||||||
import 'package:nc_photos/platform/k.dart' as platform_k;
|
import 'package:nc_photos/platform/k.dart' as platform_k;
|
||||||
import 'package:nc_photos/pref.dart';
|
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/string_extension.dart';
|
||||||
import 'package:nc_photos/theme.dart';
|
import 'package:nc_photos/theme.dart';
|
||||||
import 'package:nc_photos/widget/connect.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();
|
_formKey.currentState!.save();
|
||||||
final account = Account(_formValue.scheme, _formValue.address,
|
Account? account = Account(
|
||||||
_formValue.username.toCi(), _formValue.password, [""]);
|
Account.newId(),
|
||||||
|
_formValue.scheme,
|
||||||
|
_formValue.address,
|
||||||
|
_formValue.username.toCi(),
|
||||||
|
_formValue.password,
|
||||||
|
[""],
|
||||||
|
);
|
||||||
_log.info("[_connect] Try connecting with account: $account");
|
_log.info("[_connect] Try connecting with account: $account");
|
||||||
Navigator.pushNamed<Account>(context, Connect.routeName,
|
account = await Navigator.pushNamed<Account>(context, Connect.routeName,
|
||||||
arguments: ConnectArguments(account))
|
arguments: ConnectArguments(account));
|
||||||
.then<Account?>((result) {
|
if (account == null) {
|
||||||
return result != null
|
// connection failed
|
||||||
? Navigator.pushNamed(context, RootPicker.routeName,
|
return;
|
||||||
arguments: RootPickerArguments(result))
|
}
|
||||||
: Future.value(null);
|
account = await Navigator.pushNamed<Account>(context, RootPicker.routeName,
|
||||||
}).then((result) {
|
arguments: RootPickerArguments(account));
|
||||||
if (result != null) {
|
if (account == null) {
|
||||||
// we've got a good account
|
// ???
|
||||||
final pa = PrefAccount(result);
|
return;
|
||||||
// only signing in with app password would trigger distinct
|
}
|
||||||
final accounts = (Pref().getAccounts2Or([])..add(pa)).distinctIf(
|
// we've got a good account
|
||||||
(a, b) => a.account == b.account,
|
// only signing in with app password would trigger distinct
|
||||||
(a) => a.account.hashCode,
|
final accounts = (Pref().getAccounts3Or([])..add(account)).distinct();
|
||||||
);
|
try {
|
||||||
Pref()
|
AccountPref.setGlobalInstance(
|
||||||
..setAccounts2(accounts)
|
account, await pref_util.loadAccountPref(account));
|
||||||
..setCurrentAccountIndex(
|
} catch (e, stackTrace) {
|
||||||
accounts.indexWhere((element) => element.account == result));
|
_log.shout("[_connect] Failed reading pref for account: $account", e,
|
||||||
Navigator.pushNamedAndRemoveUntil(
|
stackTrace);
|
||||||
context, Home.routeName, (route) => false,
|
}
|
||||||
arguments: HomeArguments(pa.account));
|
Pref()
|
||||||
}
|
..setAccounts3(accounts)
|
||||||
});
|
..setCurrentAccountIndex(accounts.indexOf(account));
|
||||||
|
Navigator.pushNamedAndRemoveUntil(context, Home.routeName, (route) => false,
|
||||||
|
arguments: HomeArguments(account));
|
||||||
}
|
}
|
||||||
|
|
||||||
final _formKey = GlobalKey<FormState>();
|
final _formKey = GlobalKey<FormState>();
|
||||||
|
|
|
@ -167,13 +167,14 @@ void initLog() {
|
||||||
}
|
}
|
||||||
|
|
||||||
Account buildAccount({
|
Account buildAccount({
|
||||||
|
String id = "123456-000000",
|
||||||
String scheme = "http",
|
String scheme = "http",
|
||||||
String address = "example.com",
|
String address = "example.com",
|
||||||
String username = "admin",
|
String username = "admin",
|
||||||
String password = "pass",
|
String password = "pass",
|
||||||
List<String> roots = const [""],
|
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
|
/// Build a mock [File] pointing to a album JSON file
|
||||||
///
|
///
|
||||||
|
|
87
test/use_case/compat/v34_test.dart
Normal file
87
test/use_case/compat/v34_test.dart
Normal 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":""}""",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
Loading…
Reference in a new issue