fix: make theme uploads more tolerant (#5617)

This commit is contained in:
Mathias Mogensen 2024-06-25 13:52:59 +02:00 committed by GitHub
parent 6b1e7b6ac8
commit 271b250eac
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 109 additions and 21 deletions

View File

@ -1,6 +1,7 @@
import 'package:flutter/material.dart';
import 'package:flowy_infra/plugins/bloc/dynamic_plugin_bloc.dart'; import 'package:flowy_infra/plugins/bloc/dynamic_plugin_bloc.dart';
import 'package:flowy_infra/plugins/bloc/dynamic_plugin_state.dart'; import 'package:flowy_infra/plugins/bloc/dynamic_plugin_state.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'theme_upload_decoration.dart'; import 'theme_upload_decoration.dart';
@ -57,8 +58,9 @@ class _ThemeUploadWidgetState extends State<ThemeUploadWidget> {
}); });
} }
Widget child = Widget child = const UploadNewThemeWidget(
const UploadNewThemeWidget(key: Key('upload_new_theme_widget')); key: Key('upload_new_theme_widget'),
);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {

View File

@ -149,4 +149,39 @@ class FlowyColorScheme {
_$FlowyColorSchemeFromJson(json); _$FlowyColorSchemeFromJson(json);
Map<String, dynamic> toJson() => _$FlowyColorSchemeToJson(this); Map<String, dynamic> toJson() => _$FlowyColorSchemeToJson(this);
/// Merges the given [json] with the default color scheme
/// based on the given [brightness].
///
factory FlowyColorScheme.fromJsonSoft(
Map<String, dynamic> json, [
Brightness brightness = Brightness.light,
]) {
final colorScheme = brightness == Brightness.light
? const DefaultColorScheme.light()
: const DefaultColorScheme.dark();
final defaultMap = colorScheme.toJson();
final mergedMap = Map<String, dynamic>.from(defaultMap)..addAll(json);
return FlowyColorScheme.fromJson(mergedMap);
}
/// Useful in validating that a teheme adheres to the default color scheme.
/// Returns the keys that are missing from the [json].
///
/// We use this for testing and debugging, and we might make it possible for users to
/// check their themes for missing keys in the future.
///
/// Sample usage:
/// ```dart
/// final lightJson = await jsonDecode(await light.readAsString());
/// final lightMissingKeys = FlowyColorScheme.getMissingKeys(lightJson);
/// ```
///
static List<String> getMissingKeys(Map<String, dynamic> json) {
final defaultKeys = const DefaultColorScheme.light().toJson().keys;
final jsonKeys = json.keys;
return defaultKeys.where((key) => !jsonKeys.contains(key)).toList();
}
} }

View File

@ -24,8 +24,11 @@ class DynamicPluginBloc extends Bloc<DynamicPluginEvent, DynamicPluginState> {
} }
Future<void> onLoadRequested(Emitter<DynamicPluginState> emit) async { Future<void> onLoadRequested(Emitter<DynamicPluginState> emit) async {
emit(DynamicPluginState.ready( emit(
plugins: await FlowyPluginService.instance.plugins)); DynamicPluginState.ready(
plugins: await FlowyPluginService.instance.plugins,
),
);
} }
Future<void> addPlugin(Emitter<DynamicPluginState> emit) async { Future<void> addPlugin(Emitter<DynamicPluginState> emit) async {
@ -33,31 +36,41 @@ class DynamicPluginBloc extends Bloc<DynamicPluginEvent, DynamicPluginState> {
try { try {
final plugin = await FlowyPluginService.pick(); final plugin = await FlowyPluginService.pick();
if (plugin == null) { if (plugin == null) {
emit(DynamicPluginState.ready( return emit(
plugins: await FlowyPluginService.instance.plugins)); DynamicPluginState.ready(
return; plugins: await FlowyPluginService.instance.plugins,
),
);
} }
await FlowyPluginService.instance.addPlugin(plugin); await FlowyPluginService.instance.addPlugin(plugin);
} on PluginCompilationException catch (exception) { } on PluginCompilationException catch (exception) {
return emit(DynamicPluginState.compilationFailure( return emit(
errorMessage: exception.message)); DynamicPluginState.compilationFailure(errorMessage: exception.message),
);
} }
emit(const DynamicPluginState.compilationSuccess()); emit(const DynamicPluginState.compilationSuccess());
emit(DynamicPluginState.ready( emit(
plugins: await FlowyPluginService.instance.plugins)); DynamicPluginState.ready(
plugins: await FlowyPluginService.instance.plugins,
),
);
} }
Future<void> removePlugin( Future<void> removePlugin(
Emitter<DynamicPluginState> emit, String name) async { Emitter<DynamicPluginState> emit,
String name,
) async {
emit(const DynamicPluginState.processing()); emit(const DynamicPluginState.processing());
final plugin = await FlowyPluginService.instance.lookup(name: name); final plugin = await FlowyPluginService.instance.lookup(name: name);
if (plugin == null) { if (plugin == null) {
emit(DynamicPluginState.ready( return emit(
plugins: await FlowyPluginService.instance.plugins)); DynamicPluginState.ready(
return; plugins: await FlowyPluginService.instance.plugins,
),
);
} }
await FlowyPluginService.removePlugin(plugin); await FlowyPluginService.removePlugin(plugin);
@ -65,7 +78,8 @@ class DynamicPluginBloc extends Bloc<DynamicPluginEvent, DynamicPluginState> {
emit(const DynamicPluginState.deletionSuccess()); emit(const DynamicPluginState.deletionSuccess());
emit( emit(
DynamicPluginState.ready( DynamicPluginState.ready(
plugins: await FlowyPluginService.instance.plugins), plugins: await FlowyPluginService.instance.plugins,
),
); );
} }
} }

View File

@ -1,6 +1,8 @@
import 'dart:convert'; import 'dart:convert';
import 'dart:io'; import 'dart:io';
import 'package:flutter/material.dart';
import 'package:file/memory.dart'; import 'package:file/memory.dart';
import 'package:flowy_infra/colorscheme/colorscheme.dart'; import 'package:flowy_infra/colorscheme/colorscheme.dart';
import 'package:flowy_infra/plugins/service/models/exceptions.dart'; import 'package:flowy_infra/plugins/service/models/exceptions.dart';
@ -123,8 +125,9 @@ class FlowyDynamicPlugin {
late final FlowyColorScheme darkTheme; late final FlowyColorScheme darkTheme;
try { try {
lightTheme = FlowyColorScheme.fromJson( lightTheme = FlowyColorScheme.fromJsonSoft(
await jsonDecode(await light.readAsString())); await jsonDecode(await light.readAsString()),
);
} catch (e) { } catch (e) {
throw PluginCompilationException( throw PluginCompilationException(
'The light theme json file is not valid.', 'The light theme json file is not valid.',
@ -132,8 +135,10 @@ class FlowyDynamicPlugin {
} }
try { try {
darkTheme = FlowyColorScheme.fromJson( darkTheme = FlowyColorScheme.fromJsonSoft(
await jsonDecode(await dark.readAsString())); await jsonDecode(await dark.readAsString()),
Brightness.dark,
);
} catch (e) { } catch (e) {
throw PluginCompilationException( throw PluginCompilationException(
'The dark theme json file is not valid.', 'The dark theme json file is not valid.',

View File

@ -0,0 +1,32 @@
import 'package:flowy_infra/colorscheme/colorscheme.dart';
import 'package:flowy_infra/colorscheme/default_colorscheme.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
group('Theme missing keys', () {
test('no missing keys', () {
const colorScheme = DefaultColorScheme.light();
final toJson = colorScheme.toJson();
expect(toJson.containsKey('surface'), true);
final missingKeys = FlowyColorScheme.getMissingKeys(toJson);
expect(missingKeys.isEmpty, true);
});
test('missing surface and bg2', () {
const colorScheme = DefaultColorScheme.light();
final toJson = colorScheme.toJson()
..remove('surface')
..remove('bg2');
expect(toJson.containsKey('surface'), false);
expect(toJson.containsKey('bg2'), false);
final missingKeys = FlowyColorScheme.getMissingKeys(toJson);
expect(missingKeys.length, 2);
expect(missingKeys.contains('surface'), true);
expect(missingKeys.contains('bg2'), true);
});
});
}