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