Browse Source

fix(neon): SettingsExportHelper

Signed-off-by: Nikolas Rimikis <rimikis.nikolas@gmail.com>
pull/497/head
Nikolas Rimikis 1 year ago
parent
commit
51d8c08ea0
No known key found for this signature in database
GPG Key ID: 85ED1DE9786A4FF2
  1. 41
      packages/neon/neon/lib/src/pages/settings.dart
  2. 40
      packages/neon/neon/lib/src/settings/models/exportable.dart
  3. 23
      packages/neon/neon/lib/src/settings/models/nextcloud_app_options.dart
  4. 10
      packages/neon/neon/lib/src/settings/models/option.dart
  5. 21
      packages/neon/neon/lib/src/settings/models/options_collection.dart
  6. 2
      packages/neon/neon/lib/src/settings/models/storage.dart
  7. 147
      packages/neon/neon/lib/src/settings/utils/settings_export_helper.dart
  8. 98
      packages/neon/neon/lib/src/utils/settings_export_helper.dart

41
packages/neon/neon/lib/src/pages/settings.dart

@ -1,6 +1,3 @@
import 'dart:convert';
import 'dart:typed_data';
import 'package:file_picker/file_picker.dart'; import 'package:file_picker/file_picker.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:material_design_icons_flutter/material_design_icons_flutter.dart'; import 'package:material_design_icons_flutter/material_design_icons_flutter.dart';
@ -10,6 +7,7 @@ import 'package:neon/src/models/account.dart';
import 'package:neon/src/models/app_implementation.dart'; import 'package:neon/src/models/app_implementation.dart';
import 'package:neon/src/platform/platform.dart'; import 'package:neon/src/platform/platform.dart';
import 'package:neon/src/router.dart'; import 'package:neon/src/router.dart';
import 'package:neon/src/settings/utils/settings_export_helper.dart';
import 'package:neon/src/settings/widgets/account_settings_tile.dart'; import 'package:neon/src/settings/widgets/account_settings_tile.dart';
import 'package:neon/src/settings/widgets/checkbox_settings_tile.dart'; import 'package:neon/src/settings/widgets/checkbox_settings_tile.dart';
import 'package:neon/src/settings/widgets/custom_settings_tile.dart'; import 'package:neon/src/settings/widgets/custom_settings_tile.dart';
@ -23,7 +21,6 @@ import 'package:neon/src/theme/dialog.dart';
import 'package:neon/src/utils/confirmation_dialog.dart'; import 'package:neon/src/utils/confirmation_dialog.dart';
import 'package:neon/src/utils/global_options.dart'; import 'package:neon/src/utils/global_options.dart';
import 'package:neon/src/utils/save_file.dart'; import 'package:neon/src/utils/save_file.dart';
import 'package:neon/src/utils/settings_export_helper.dart';
import 'package:neon/src/widgets/exception.dart'; import 'package:neon/src/widgets/exception.dart';
import 'package:package_info_plus/package_info_plus.dart'; import 'package:package_info_plus/package_info_plus.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
@ -250,16 +247,10 @@ class _SettingsPageState extends State<SettingsPage> {
final settingsExportHelper = _buildSettingsExportHelper(context); final settingsExportHelper = _buildSettingsExportHelper(context);
try { try {
final fileName = final fileName = 'nextcloud-neon-settings-${DateTime.now().millisecondsSinceEpoch ~/ 1000}.json';
'nextcloud-neon-settings-${DateTime.now().millisecondsSinceEpoch ~/ 1000}.json.base64';
final data = base64.encode( final data = settingsExportHelper.exportToFile();
utf8.encode( await saveFileWithPickDialog(fileName, data);
json.encode(
settingsExportHelper.toJsonExport(),
),
),
);
await saveFileWithPickDialog(fileName, utf8.encode(data) as Uint8List);
} catch (e, s) { } catch (e, s) {
debugPrint(e.toString()); debugPrint(e.toString());
debugPrint(s.toString()); debugPrint(s.toString());
@ -280,14 +271,14 @@ class _SettingsPageState extends State<SettingsPage> {
try { try {
final result = await FilePicker.platform.pickFiles( final result = await FilePicker.platform.pickFiles(
withData: true, withReadStream: true,
); );
if (result == null) { if (result == null) {
return; return;
} }
if (!result.files.single.path!.endsWith('.json.base64')) { if (!result.files.single.path!.endsWith('.json')) {
if (mounted) { if (mounted) {
NeonException.showSnackbar( NeonException.showSnackbar(
context, context,
@ -297,9 +288,7 @@ class _SettingsPageState extends State<SettingsPage> {
return; return;
} }
final data = json.decode(utf8.decode(base64.decode(utf8.decode(result.files.single.bytes!)))); await settingsExportHelper.applyFromFile(result.files.single.readStream);
await settingsExportHelper.applyFromJson(data as Map<String, dynamic>);
} catch (e, s) { } catch (e, s) {
debugPrint(e.toString()); debugPrint(e.toString());
debugPrint(s.toString()); debugPrint(s.toString());
@ -329,16 +318,16 @@ class _SettingsPageState extends State<SettingsPage> {
} }
SettingsExportHelper _buildSettingsExportHelper(final BuildContext context) { SettingsExportHelper _buildSettingsExportHelper(final BuildContext context) {
final globalOptions = Provider.of<GlobalOptions>(context); final globalOptions = Provider.of<GlobalOptions>(context, listen: false);
final accountsBloc = Provider.of<AccountsBloc>(context, listen: false); final accountsBloc = Provider.of<AccountsBloc>(context, listen: false);
final appImplementations = Provider.of<Iterable<AppImplementation>>(context); final appImplementations = Provider.of<Iterable<AppImplementation>>(context, listen: false);
return SettingsExportHelper( return SettingsExportHelper(
globalOptions: globalOptions, exportables: {
appImplementations: appImplementations, globalOptions,
accountSpecificOptions: accountsBloc.accounts.value.asMap().map( AccountsBlocExporter(accountsBloc),
(final _, final account) => MapEntry(account, accountsBloc.getOptionsFor(account).options), AppImplementationsExporter(appImplementations),
), },
); );
} }
} }

40
packages/neon/neon/lib/src/settings/models/exportable.dart

@ -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();
}
}
}
}

23
packages/neon/neon/lib/src/settings/models/nextcloud_app_options.dart

@ -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();
}
}
}

10
packages/neon/neon/lib/src/settings/models/option.dart

@ -79,10 +79,20 @@ abstract class Option<T> extends ChangeNotifier implements ValueListenable<T> {
} }
/// Resets the option to its [default] value. /// Resets the option to its [default] value.
@mustBeOverridden
void reset() { void reset() {
value = defaultValue; value = defaultValue;
} }
/// Loads [data] into [value] by calling [deserialize] on it.
void load(final Object? data) {
final value = deserialize(data);
if (value != null) {
this.value = value;
}
}
/// Deserializes the data. /// Deserializes the data.
T? deserialize(final Object? data); T? deserialize(final Object? data);

21
packages/neon/neon/lib/src/settings/models/options_collection.dart

@ -1,15 +1,16 @@
import 'package:meta/meta.dart'; import 'package:meta/meta.dart';
import 'package:neon/src/settings/models/exportable.dart';
import 'package:neon/src/settings/models/option.dart'; import 'package:neon/src/settings/models/option.dart';
import 'package:neon/src/settings/models/options_category.dart'; import 'package:neon/src/settings/models/options_category.dart';
import 'package:neon/src/settings/models/storage.dart'; import 'package:neon/src/settings/models/storage.dart';
/// Collection of [Option]s. /// Collection of [Option]s.
abstract class OptionsCollection { abstract class OptionsCollection implements Exportable {
OptionsCollection(this.storage); OptionsCollection(this.storage);
/// Storage backend to use. /// Storage backend to use.
@protected @protected
final SettingsStorage storage; final AppStorage storage;
/// Collection of options. /// Collection of options.
@protected @protected
@ -34,6 +35,22 @@ abstract class OptionsCollection {
option.dispose(); option.dispose();
} }
} }
@override
MapEntry<String, Object?> export() {
final data = Map.fromEntries(options.serialize());
return MapEntry(storage.id, data);
}
@override
void import(final Map<String, Object?> data) {
final values = data[storage.id] as Map<String, Object?>?;
if (values != null) {
options.deserialize(values);
}
}
} }
/// OpptionsCollection for a neon app. /// OpptionsCollection for a neon app.

2
packages/neon/neon/lib/src/settings/models/storage.dart

@ -108,6 +108,8 @@ final class AppStorage implements SettingsStorage {
final String? suffix; final String? suffix;
String get id => suffix ?? key.value;
@visibleForTesting @visibleForTesting
String formatKey(final String key) { String formatKey(final String key) {
if (suffix != null) { if (suffix != null) {

147
packages/neon/neon/lib/src/settings/utils/settings_export_helper.dart

@ -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);
}
}
}
}

98
packages/neon/neon/lib/src/utils/settings_export_helper.dart

@ -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…
Cancel
Save