added support for multiple accounts with an account switcher in settings
This commit is contained in:
@@ -0,0 +1,78 @@
|
||||
import 'dart:developer';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:hydrated_bloc/hydrated_bloc.dart';
|
||||
|
||||
import '../api/cache_store.dart';
|
||||
import '../session/account_codec.dart';
|
||||
import 'hydrated_storage_bootstrap.dart';
|
||||
|
||||
/// Per-account local data: every account gets its own HydratedBloc box and
|
||||
/// request cache directory (see [CacheStore]), so switching back is instant
|
||||
/// and offline. Only the app settings live in the shared [global] box.
|
||||
abstract final class AccountStorage {
|
||||
static const _settingsKey = 'SettingsCubit';
|
||||
|
||||
static late String _baseDir;
|
||||
static final Map<String, Storage> _open = {};
|
||||
static Storage _global = InMemoryStorage();
|
||||
|
||||
static Storage get global => _global;
|
||||
|
||||
/// Opens the global box. Settings of installs from before multi-account
|
||||
/// support are copied over from the legacy box once.
|
||||
static Future<void> init(String baseDir) async {
|
||||
_baseDir = baseDir;
|
||||
_global = await buildHydratedStorageWithFallback(
|
||||
_ensureDir('$baseDir/hydrated_global'),
|
||||
);
|
||||
HydratedBloc.storage = InMemoryStorage();
|
||||
if (_global.read(_settingsKey) != null) return;
|
||||
final legacySettings = (await _storageFor('')).read(_settingsKey);
|
||||
if (legacySettings != null) {
|
||||
await _global.write(_settingsKey, legacySettings);
|
||||
}
|
||||
}
|
||||
|
||||
/// Points HydratedBloc at [account]'s data; the request cache follows the
|
||||
/// active account on its own. Blocs keep the storage they were created
|
||||
/// with, so the per-account blocs have to be recreated afterwards.
|
||||
static Future<void> activate(AccountEntry? account) async {
|
||||
HydratedBloc.storage = account == null
|
||||
? InMemoryStorage()
|
||||
: await _storageFor(account.namespace);
|
||||
}
|
||||
|
||||
/// Removes all local data of the account with [namespace].
|
||||
static Future<void> delete(String namespace) async {
|
||||
try {
|
||||
final storage = await _storageFor(namespace);
|
||||
await storage.clear();
|
||||
// Closed boxes ignore writes, so blocs of the account that are still
|
||||
// mounted until the remount cannot leave data behind.
|
||||
await storage.close();
|
||||
_open.remove(namespace);
|
||||
if (namespace.isNotEmpty) {
|
||||
final dir = Directory(_dirFor(namespace));
|
||||
if (dir.existsSync()) await dir.delete(recursive: true);
|
||||
}
|
||||
} on Object catch (e, s) {
|
||||
log('Account storage delete failed: $e', stackTrace: s);
|
||||
}
|
||||
await CacheStore.instance.deleteNamespace(namespace);
|
||||
}
|
||||
|
||||
static Future<Storage> _storageFor(String namespace) async =>
|
||||
_open[namespace] ??= await buildHydratedStorageWithFallback(
|
||||
_ensureDir(_dirFor(namespace)),
|
||||
);
|
||||
|
||||
// The legacy account keeps the box at the root of the base directory.
|
||||
static String _dirFor(String namespace) =>
|
||||
namespace.isEmpty ? _baseDir : '$_baseDir/accounts/$namespace';
|
||||
|
||||
static String _ensureDir(String path) {
|
||||
Directory(path).createSync(recursive: true);
|
||||
return path;
|
||||
}
|
||||
}
|
||||
@@ -9,19 +9,43 @@ import 'package:hydrated_bloc/hydrated_bloc.dart';
|
||||
Future<Storage> buildHydratedStorageWithFallback(String directoryPath) async {
|
||||
final directory = HydratedStorageDirectory(directoryPath);
|
||||
try {
|
||||
return await HydratedStorage.build(storageDirectory: directory);
|
||||
return await _build(directory);
|
||||
} catch (e, s) {
|
||||
log('HydratedStorage open failed, rebuilding: $e', stackTrace: s);
|
||||
}
|
||||
try {
|
||||
await _deleteHydratedBox(directoryPath);
|
||||
return await HydratedStorage.build(storageDirectory: directory);
|
||||
return await _build(directory);
|
||||
} catch (e, s) {
|
||||
log('HydratedStorage rebuild failed, using memory: $e', stackTrace: s);
|
||||
return InMemoryStorage();
|
||||
}
|
||||
}
|
||||
|
||||
final Expando<List<String> Function()> _keyReaders = Expando();
|
||||
|
||||
Future<Storage> _build(HydratedStorageDirectory directory) async {
|
||||
final storage = await HydratedStorage.build(storageDirectory: directory);
|
||||
// hydrated_bloc exposes no key listing; the Hive instance is the only way
|
||||
// in. Every build replaces the static one, so it is captured per storage.
|
||||
// ignore: invalid_use_of_visible_for_testing_member
|
||||
final hive = HydratedStorage.hive;
|
||||
_keyReaders[storage] = () =>
|
||||
hive.box<dynamic>('hydrated_box').keys.map((k) => '$k').toList();
|
||||
return storage;
|
||||
}
|
||||
|
||||
/// All keys persisted in [storage], sorted; `null` when they can't be
|
||||
/// enumerated.
|
||||
List<String>? hydratedStorageKeys(Storage storage) {
|
||||
if (storage is InMemoryStorage) return storage.keys.toList()..sort();
|
||||
try {
|
||||
return _keyReaders[storage]?.call()?..sort();
|
||||
} on Object {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _deleteHydratedBox(String directoryPath) async {
|
||||
final directory = Directory(directoryPath);
|
||||
if (!directory.existsSync()) return;
|
||||
|
||||
Reference in New Issue
Block a user