added support for multiple accounts, guardian login bugfixes, ui changes
This commit is contained in:
@@ -0,0 +1,221 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'session.dart';
|
||||
import 'session_codec.dart';
|
||||
|
||||
/// One signed-in account on this device. The session itself lives in the
|
||||
/// keychain (active slots or the account's vault); this is the index entry.
|
||||
class AccountEntry {
|
||||
final String id;
|
||||
final String kind;
|
||||
|
||||
/// Username or e-mail — the identity of the account.
|
||||
final String label;
|
||||
|
||||
/// Real name as known to the server; null until it was loaded once.
|
||||
final String? displayName;
|
||||
final bool isDemo;
|
||||
|
||||
/// Storage namespace of the account's local data. Empty for the account that
|
||||
/// existed before multi-account support, so its data stays where it was.
|
||||
final String namespace;
|
||||
final int lastUsed;
|
||||
|
||||
const AccountEntry({
|
||||
required this.id,
|
||||
required this.kind,
|
||||
required this.label,
|
||||
required this.namespace,
|
||||
this.displayName,
|
||||
this.isDemo = false,
|
||||
this.lastUsed = 0,
|
||||
});
|
||||
|
||||
bool get isGuardian => kind == SessionKeys.kindGuardian;
|
||||
|
||||
AccountEntry copyWith({int? lastUsed, String? displayName}) => AccountEntry(
|
||||
id: id,
|
||||
kind: kind,
|
||||
label: label,
|
||||
namespace: namespace,
|
||||
displayName: displayName ?? this.displayName,
|
||||
isDemo: isDemo,
|
||||
lastUsed: lastUsed ?? this.lastUsed,
|
||||
);
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
'id': id,
|
||||
'kind': kind,
|
||||
'label': label,
|
||||
'namespace': namespace,
|
||||
'displayName': displayName,
|
||||
'isDemo': isDemo,
|
||||
'lastUsed': lastUsed,
|
||||
};
|
||||
|
||||
static AccountEntry? fromJson(Object? json) {
|
||||
if (json is! Map) return null;
|
||||
final id = json['id'];
|
||||
final kind = json['kind'];
|
||||
final label = json['label'];
|
||||
if (id is! String || kind is! String || label is! String) return null;
|
||||
return AccountEntry(
|
||||
id: id,
|
||||
kind: kind,
|
||||
label: label,
|
||||
namespace: json['namespace'] is String ? json['namespace'] as String : id,
|
||||
displayName: json['displayName'] is String
|
||||
? json['displayName'] as String
|
||||
: null,
|
||||
isDemo: json['isDemo'] == true,
|
||||
lastUsed: json['lastUsed'] is int ? json['lastUsed'] as int : 0,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class AccountIndex {
|
||||
final List<AccountEntry> accounts;
|
||||
final String? activeId;
|
||||
|
||||
const AccountIndex({this.accounts = const [], this.activeId});
|
||||
|
||||
static const empty = AccountIndex();
|
||||
|
||||
AccountEntry? get active => byId(activeId);
|
||||
|
||||
AccountEntry? byId(String? id) {
|
||||
if (id == null) return null;
|
||||
for (final entry in accounts) {
|
||||
if (entry.id == id) return entry;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// The entry [session] belongs to — same kind, identity and demo flag.
|
||||
AccountEntry? matching(Session session) {
|
||||
final (kind, label) = identityOf(session);
|
||||
for (final entry in accounts) {
|
||||
if (entry.kind == kind &&
|
||||
entry.label == label &&
|
||||
entry.isDemo == session.isDemo) {
|
||||
return entry;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Makes [session] the active account, reusing its entry when it is already
|
||||
/// known and otherwise adding one with [newId] (also its namespace).
|
||||
AccountIndex activate(
|
||||
Session session, {
|
||||
required String newId,
|
||||
String? namespace,
|
||||
int now = 0,
|
||||
}) {
|
||||
final existing = matching(session);
|
||||
if (existing != null) return select(existing.id, now: now);
|
||||
final (kind, label) = identityOf(session);
|
||||
return AccountIndex(
|
||||
accounts: [
|
||||
...accounts,
|
||||
AccountEntry(
|
||||
id: newId,
|
||||
kind: kind,
|
||||
label: label,
|
||||
namespace: namespace ?? newId,
|
||||
isDemo: session.isDemo,
|
||||
lastUsed: now,
|
||||
),
|
||||
],
|
||||
activeId: newId,
|
||||
);
|
||||
}
|
||||
|
||||
AccountIndex select(String id, {int now = 0}) => AccountIndex(
|
||||
accounts: accounts
|
||||
.map((e) => e.id == id ? e.copyWith(lastUsed: now) : e)
|
||||
.toList(),
|
||||
activeId: id,
|
||||
);
|
||||
|
||||
AccountIndex rename(String id, String displayName) => AccountIndex(
|
||||
accounts: accounts
|
||||
.map((e) => e.id == id ? e.copyWith(displayName: displayName) : e)
|
||||
.toList(),
|
||||
activeId: activeId,
|
||||
);
|
||||
|
||||
/// Drops [id]; the active account is left unset when it was the one removed.
|
||||
AccountIndex remove(String id) => AccountIndex(
|
||||
accounts: [
|
||||
for (final e in accounts)
|
||||
if (e.id != id) e,
|
||||
],
|
||||
activeId: activeId == id ? null : activeId,
|
||||
);
|
||||
|
||||
/// Most recently used account other than [excludedId], if any.
|
||||
AccountEntry? mostRecentExcept(String? excludedId) {
|
||||
AccountEntry? best;
|
||||
for (final entry in accounts) {
|
||||
if (entry.id == excludedId) continue;
|
||||
if (best == null || entry.lastUsed > best.lastUsed) best = entry;
|
||||
}
|
||||
return best;
|
||||
}
|
||||
|
||||
String encode() => jsonEncode({
|
||||
'activeId': activeId,
|
||||
'accounts': [for (final e in accounts) e.toJson()],
|
||||
});
|
||||
|
||||
static AccountIndex decode(String? raw) {
|
||||
if (raw == null || raw.isEmpty) return empty;
|
||||
try {
|
||||
final json = jsonDecode(raw);
|
||||
if (json is! Map) return empty;
|
||||
final list = json['accounts'];
|
||||
return AccountIndex(
|
||||
accounts: [
|
||||
if (list is List)
|
||||
for (final item in list) ?AccountEntry.fromJson(item),
|
||||
],
|
||||
activeId: json['activeId'] is String
|
||||
? json['activeId'] as String
|
||||
: null,
|
||||
);
|
||||
} on FormatException {
|
||||
return empty;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
(String kind, String label) identityOf(Session session) => switch (session) {
|
||||
CredentialSession(:final username) => (SessionKeys.kindCredential, username),
|
||||
GuardianSession(:final email) => (SessionKeys.kindGuardian, email),
|
||||
};
|
||||
|
||||
/// Keychain fields of [session] including the app passwords, as stored in an
|
||||
/// inactive account's vault. Unset fields are omitted.
|
||||
Map<String, String> sessionVaultFields(Session session) => {
|
||||
for (final MapEntry(:key, :value) in encodeSessionFields(session).entries)
|
||||
key: ?value,
|
||||
SessionKeys.appPassword: ?session.nextcloud?.appPassword,
|
||||
SessionKeys.appPasswordTalk: ?session.nextcloud?.appPasswordTalk,
|
||||
};
|
||||
|
||||
String encodeVault(Map<String, String> fields) => jsonEncode(fields);
|
||||
|
||||
Map<String, String> decodeVault(String? raw) {
|
||||
if (raw == null || raw.isEmpty) return const {};
|
||||
try {
|
||||
final json = jsonDecode(raw);
|
||||
if (json is! Map) return const {};
|
||||
return {
|
||||
for (final MapEntry(:key, :value) in json.entries)
|
||||
if (key is String && value is String) key: value,
|
||||
};
|
||||
} on FormatException {
|
||||
return const {};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user