diff --git a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/theme_upload/theme_upload_view.dart b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/theme_upload/theme_upload_view.dart index f9705843d1..1b22dba659 100644 --- a/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/theme_upload/theme_upload_view.dart +++ b/frontend/appflowy_flutter/lib/workspace/presentation/settings/widgets/theme_upload/theme_upload_view.dart @@ -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_state.dart'; -import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'theme_upload_decoration.dart'; @@ -57,8 +58,9 @@ class _ThemeUploadWidgetState extends State { }); } - Widget child = - const UploadNewThemeWidget(key: Key('upload_new_theme_widget')); + Widget child = const UploadNewThemeWidget( + key: Key('upload_new_theme_widget'), + ); @override Widget build(BuildContext context) { diff --git a/frontend/appflowy_flutter/packages/flowy_infra/lib/colorscheme/colorscheme.dart b/frontend/appflowy_flutter/packages/flowy_infra/lib/colorscheme/colorscheme.dart index 0c672a1f85..14941e5477 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra/lib/colorscheme/colorscheme.dart +++ b/frontend/appflowy_flutter/packages/flowy_infra/lib/colorscheme/colorscheme.dart @@ -149,4 +149,39 @@ class FlowyColorScheme { _$FlowyColorSchemeFromJson(json); Map toJson() => _$FlowyColorSchemeToJson(this); + + /// Merges the given [json] with the default color scheme + /// based on the given [brightness]. + /// + factory FlowyColorScheme.fromJsonSoft( + Map json, [ + Brightness brightness = Brightness.light, + ]) { + final colorScheme = brightness == Brightness.light + ? const DefaultColorScheme.light() + : const DefaultColorScheme.dark(); + final defaultMap = colorScheme.toJson(); + final mergedMap = Map.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 getMissingKeys(Map json) { + final defaultKeys = const DefaultColorScheme.light().toJson().keys; + final jsonKeys = json.keys; + + return defaultKeys.where((key) => !jsonKeys.contains(key)).toList(); + } } diff --git a/frontend/appflowy_flutter/packages/flowy_infra/lib/plugins/bloc/dynamic_plugin_bloc.dart b/frontend/appflowy_flutter/packages/flowy_infra/lib/plugins/bloc/dynamic_plugin_bloc.dart index ee1adee332..0dbfc84564 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra/lib/plugins/bloc/dynamic_plugin_bloc.dart +++ b/frontend/appflowy_flutter/packages/flowy_infra/lib/plugins/bloc/dynamic_plugin_bloc.dart @@ -24,8 +24,11 @@ class DynamicPluginBloc extends Bloc { } Future onLoadRequested(Emitter emit) async { - emit(DynamicPluginState.ready( - plugins: await FlowyPluginService.instance.plugins)); + emit( + DynamicPluginState.ready( + plugins: await FlowyPluginService.instance.plugins, + ), + ); } Future addPlugin(Emitter emit) async { @@ -33,31 +36,41 @@ class DynamicPluginBloc extends Bloc { try { final plugin = await FlowyPluginService.pick(); if (plugin == null) { - emit(DynamicPluginState.ready( - plugins: await FlowyPluginService.instance.plugins)); - return; + return emit( + DynamicPluginState.ready( + plugins: await FlowyPluginService.instance.plugins, + ), + ); } await FlowyPluginService.instance.addPlugin(plugin); } on PluginCompilationException catch (exception) { - return emit(DynamicPluginState.compilationFailure( - errorMessage: exception.message)); + return emit( + DynamicPluginState.compilationFailure(errorMessage: exception.message), + ); } emit(const DynamicPluginState.compilationSuccess()); - emit(DynamicPluginState.ready( - plugins: await FlowyPluginService.instance.plugins)); + emit( + DynamicPluginState.ready( + plugins: await FlowyPluginService.instance.plugins, + ), + ); } Future removePlugin( - Emitter emit, String name) async { + Emitter emit, + String name, + ) async { emit(const DynamicPluginState.processing()); final plugin = await FlowyPluginService.instance.lookup(name: name); if (plugin == null) { - emit(DynamicPluginState.ready( - plugins: await FlowyPluginService.instance.plugins)); - return; + return emit( + DynamicPluginState.ready( + plugins: await FlowyPluginService.instance.plugins, + ), + ); } await FlowyPluginService.removePlugin(plugin); @@ -65,7 +78,8 @@ class DynamicPluginBloc extends Bloc { emit(const DynamicPluginState.deletionSuccess()); emit( DynamicPluginState.ready( - plugins: await FlowyPluginService.instance.plugins), + plugins: await FlowyPluginService.instance.plugins, + ), ); } } diff --git a/frontend/appflowy_flutter/packages/flowy_infra/lib/plugins/service/models/flowy_dynamic_plugin.dart b/frontend/appflowy_flutter/packages/flowy_infra/lib/plugins/service/models/flowy_dynamic_plugin.dart index 93e972eeba..2f31ffbf1e 100644 --- a/frontend/appflowy_flutter/packages/flowy_infra/lib/plugins/service/models/flowy_dynamic_plugin.dart +++ b/frontend/appflowy_flutter/packages/flowy_infra/lib/plugins/service/models/flowy_dynamic_plugin.dart @@ -1,6 +1,8 @@ import 'dart:convert'; import 'dart:io'; +import 'package:flutter/material.dart'; + import 'package:file/memory.dart'; import 'package:flowy_infra/colorscheme/colorscheme.dart'; import 'package:flowy_infra/plugins/service/models/exceptions.dart'; @@ -123,8 +125,9 @@ class FlowyDynamicPlugin { late final FlowyColorScheme darkTheme; try { - lightTheme = FlowyColorScheme.fromJson( - await jsonDecode(await light.readAsString())); + lightTheme = FlowyColorScheme.fromJsonSoft( + await jsonDecode(await light.readAsString()), + ); } catch (e) { throw PluginCompilationException( 'The light theme json file is not valid.', @@ -132,8 +135,10 @@ class FlowyDynamicPlugin { } try { - darkTheme = FlowyColorScheme.fromJson( - await jsonDecode(await dark.readAsString())); + darkTheme = FlowyColorScheme.fromJsonSoft( + await jsonDecode(await dark.readAsString()), + Brightness.dark, + ); } catch (e) { throw PluginCompilationException( 'The dark theme json file is not valid.', diff --git a/frontend/appflowy_flutter/test/unit_test/settings/theme_missing_keys_test.dart b/frontend/appflowy_flutter/test/unit_test/settings/theme_missing_keys_test.dart new file mode 100644 index 0000000000..646ce7fbac --- /dev/null +++ b/frontend/appflowy_flutter/test/unit_test/settings/theme_missing_keys_test.dart @@ -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); + }); + }); +}