Nikolas Rimikis
1 year ago
8 changed files with 233 additions and 149 deletions
@ -0,0 +1,40 @@
|
||||
import 'dart:async'; |
||||
|
||||
import 'package:collection/collection.dart'; |
||||
import 'package:neon/src/settings/models/option.dart'; |
||||
|
||||
/// Exportable data interface. |
||||
abstract interface class Exportable { |
||||
/// Exports into a json map entry. |
||||
MapEntry<String, Object?> export(); |
||||
|
||||
/// Imports [data] from an export. |
||||
FutureOr<void> import(final Map<String, Object?> data); |
||||
} |
||||
|
||||
/// Serialization helpers for a collection of [Option]s. |
||||
extension SerializeOptions on Iterable<Option<dynamic>> { |
||||
/// Serializes into an [Iterable<JsonEntry>]. |
||||
/// |
||||
/// Use [Map.fromEntries] to get a json Map. |
||||
Iterable<MapEntry<String, Object?>> serialize() sync* { |
||||
for (final option in this) { |
||||
if (option.enabled) { |
||||
yield MapEntry(option.key.value, option.serialize()); |
||||
} |
||||
} |
||||
} |
||||
|
||||
/// Deserializes [data] and updates the [Option]s. |
||||
void deserialize(final Map<String, Object?> data) { |
||||
for (final entry in data.entries) { |
||||
final option = firstWhereOrNull((final option) => option.key.value == entry.key); |
||||
|
||||
if (entry.value != null) { |
||||
option?.load(entry.value); |
||||
} else { |
||||
option?.reset(); |
||||
} |
||||
} |
||||
} |
||||
} |
@ -1,23 +0,0 @@
|
||||
import 'package:neon/src/settings/models/option.dart'; |
||||
import 'package:neon/src/settings/models/options_category.dart'; |
||||
import 'package:neon/src/settings/models/storage.dart'; |
||||
|
||||
abstract class NextcloudAppOptions { |
||||
NextcloudAppOptions(this.storage); |
||||
|
||||
final AppStorage storage; |
||||
late final List<OptionsCategory> categories; |
||||
late final List<Option> options; |
||||
|
||||
void reset() { |
||||
for (final option in options) { |
||||
option.reset(); |
||||
} |
||||
} |
||||
|
||||
void dispose() { |
||||
for (final option in options) { |
||||
option.dispose(); |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,147 @@
|
||||
import 'dart:async'; |
||||
import 'dart:convert'; |
||||
import 'dart:typed_data'; |
||||
|
||||
import 'package:meta/meta.dart'; |
||||
import 'package:neon/src/blocs/accounts.dart'; |
||||
import 'package:neon/src/models/account.dart' show Account, AccountFind; |
||||
import 'package:neon/src/models/app_implementation.dart'; |
||||
import 'package:neon/src/settings/models/exportable.dart'; |
||||
import 'package:neon/src/settings/models/option.dart'; |
||||
import 'package:neon/src/settings/models/storage.dart'; |
||||
|
||||
/// Helper class to export all [Option]s. |
||||
/// |
||||
/// Json based operations: |
||||
/// * [exportToJson] |
||||
/// * [applyFromJson] |
||||
/// |
||||
/// [Uint8List] based operations: |
||||
/// * [exportToFile] |
||||
/// * [applyFromFile] |
||||
@internal |
||||
@immutable |
||||
class SettingsExportHelper { |
||||
const SettingsExportHelper({ |
||||
required this.exportables, |
||||
}); |
||||
|
||||
/// Collections of elements to export. |
||||
final Set<Exportable> exportables; |
||||
|
||||
/// Imports [file] and applies the stored [Option]s. |
||||
/// |
||||
/// See: |
||||
/// * [applyFromJson] to import a json map. |
||||
Future<void> applyFromFile(final Stream<List<int>>? file) async { |
||||
final transformer = const Utf8Decoder().fuse(const JsonDecoder()); |
||||
|
||||
final data = await file?.transform(transformer).single; |
||||
|
||||
if (data == null) { |
||||
return; |
||||
} |
||||
|
||||
await applyFromJson(data as Map<String, Object?>); |
||||
} |
||||
|
||||
/// Imports the [json] data and applies the stored [Option]s. |
||||
/// |
||||
/// See: |
||||
/// * [applyFromFile] to import data from a [Stream<Uint8List>]. |
||||
Future<void> applyFromJson(final Map<String, Object?> json) async { |
||||
for (final exportable in exportables) { |
||||
await exportable.import(json); |
||||
} |
||||
} |
||||
|
||||
/// Exports the stored [Option]s into a [Uint8List]. |
||||
/// |
||||
/// See: |
||||
/// * [exportToJson] to export to a json map. |
||||
Uint8List exportToFile() { |
||||
final transformer = JsonUtf8Encoder(); |
||||
|
||||
final json = exportToJson(); |
||||
return transformer.convert(json) as Uint8List; |
||||
} |
||||
|
||||
/// Exports the stored [Option]s into a json map. |
||||
/// |
||||
/// See: |
||||
/// * [exportToFile] to export data to a [Uint8List]. |
||||
Map<String, Object?> exportToJson() => Map.fromEntries(exportables.map((final e) => e.export())); |
||||
} |
||||
|
||||
/// Helper class to export [AppImplementation]s implementing the [Exportable] interface. |
||||
@immutable |
||||
class AppImplementationsExporter implements Exportable { |
||||
const AppImplementationsExporter(this.appImplementations); |
||||
|
||||
/// List of apps to export. |
||||
final Iterable<AppImplementation> appImplementations; |
||||
|
||||
/// Key the exported value will be stored at. |
||||
static final _key = StorageKeys.apps.value; |
||||
|
||||
@override |
||||
MapEntry<String, Object?> export() => MapEntry( |
||||
_key, |
||||
Map.fromEntries(appImplementations.map((final app) => app.options.export())), |
||||
); |
||||
|
||||
@override |
||||
void import(final Map<String, Object?> data) { |
||||
final values = data[_key] as Map<String, Object?>?; |
||||
|
||||
if (values == null) { |
||||
return; |
||||
} |
||||
|
||||
for (final element in values.entries) { |
||||
final app = appImplementations.tryFind(element.key); |
||||
|
||||
if (app != null) { |
||||
app.options.import(values); |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
/// Helper class to export [Account]s implementing the [Exportable] interface. |
||||
@immutable |
||||
class AccountsBlocExporter implements Exportable { |
||||
const AccountsBlocExporter(this.accountsBloc); |
||||
|
||||
/// AccountsBloc containing the accounts to export. |
||||
final AccountsBloc accountsBloc; |
||||
|
||||
/// Key the exported value will be stored at. |
||||
static final _key = StorageKeys.accounts.value; |
||||
|
||||
@override |
||||
MapEntry<String, Object?> export() => MapEntry(_key, Map.fromEntries(_serialize())); |
||||
|
||||
Iterable<MapEntry<String, Object?>> _serialize() sync* { |
||||
for (final account in accountsBloc.accounts.value) { |
||||
yield accountsBloc.getOptionsFor(account).export(); |
||||
} |
||||
} |
||||
|
||||
@override |
||||
void import(final Map<String, Object?> data) { |
||||
final values = data[_key] as Map<String, Object?>?; |
||||
|
||||
if (values == null) { |
||||
return; |
||||
} |
||||
|
||||
for (final element in values.entries) { |
||||
final account = accountsBloc.accounts.value.tryFind(element.key); |
||||
|
||||
if (account != null) { |
||||
accountsBloc.getOptionsFor(account).import(values); |
||||
} |
||||
} |
||||
} |
||||
} |
@ -1,98 +0,0 @@
|
||||
import 'package:meta/meta.dart'; |
||||
import 'package:neon/src/models/account.dart'; |
||||
import 'package:neon/src/models/app_implementation.dart'; |
||||
import 'package:neon/src/settings/models/option.dart'; |
||||
import 'package:neon/src/utils/global_options.dart'; |
||||
|
||||
@internal |
||||
class SettingsExportHelper { |
||||
SettingsExportHelper({ |
||||
required this.globalOptions, |
||||
required this.appImplementations, |
||||
required this.accountSpecificOptions, |
||||
}); |
||||
|
||||
final GlobalOptions globalOptions; |
||||
final Iterable<AppImplementation> appImplementations; |
||||
final Map<Account, List<Option>> accountSpecificOptions; |
||||
|
||||
Future applyFromJson(final Map<String, dynamic> data) async { |
||||
final globalOptionsData = data['global'] as Map<String, dynamic>; |
||||
await _applyOptionsMapToOptions( |
||||
globalOptions.options, |
||||
globalOptionsData, |
||||
); |
||||
|
||||
final appImplementationsData = data['apps'] as Map<String, dynamic>; |
||||
for (final appId in appImplementationsData.keys) { |
||||
final app = appImplementations.tryFind(appId); |
||||
if (app == null) { |
||||
return; |
||||
} |
||||
final appImplementationData = appImplementationsData[appId]! as Map<String, dynamic>; |
||||
await _applyOptionsMapToOptions( |
||||
app.options.options, |
||||
appImplementationData, |
||||
); |
||||
} |
||||
|
||||
final accountsData = data['accounts'] as Map<String, dynamic>; |
||||
for (final accountId in accountsData.keys) { |
||||
final account = accountSpecificOptions.keys.tryFind(accountId); |
||||
if (account == null) { |
||||
return; |
||||
} |
||||
final accountData = accountsData[accountId]! as Map<String, dynamic>; |
||||
await _applyOptionsMapToOptions( |
||||
accountSpecificOptions[account]!, |
||||
accountData, |
||||
); |
||||
} |
||||
} |
||||
|
||||
Future _applyOptionsMapToOptions(final Iterable<Option> options, final Map<String, dynamic> data) async { |
||||
for (final optionKey in data.keys) { |
||||
for (final option in options) { |
||||
if (option.key.value == optionKey) { |
||||
final Object? value = data[optionKey]; |
||||
|
||||
if (value != null) { |
||||
option.value = await option.deserialize(value); |
||||
} |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
Map<String, dynamic> toJsonExport() => { |
||||
'global': { |
||||
for (final option in globalOptions.options) ...{ |
||||
if (option.enabled) ...{ |
||||
option.key: option.serialize(), |
||||
}, |
||||
}, |
||||
}, |
||||
'apps': { |
||||
for (final appImplementation in appImplementations) ...{ |
||||
appImplementation.id: { |
||||
for (final option in appImplementation.options.options) ...{ |
||||
if (option.enabled) ...{ |
||||
option.key: option.serialize(), |
||||
}, |
||||
}, |
||||
}, |
||||
}, |
||||
}, |
||||
'accounts': { |
||||
for (final account in accountSpecificOptions.keys) ...{ |
||||
account.id: { |
||||
for (final option in accountSpecificOptions[account]!) ...{ |
||||
if (option.enabled) ...{ |
||||
option.key: option.serialize(), |
||||
}, |
||||
}, |
||||
}, |
||||
}, |
||||
}, |
||||
}; |
||||
} |
Loading…
Reference in new issue