Skip to content

Commit

Permalink
Theme Scope (#238)
Browse files Browse the repository at this point in the history
* WIP on 231-fr-implement-themescope

* Reworked brightness

* Add paddings

* Move system theme

* update schema

* update schema

* Added `theme_scope` to README.
  • Loading branch information
hawkkiller authored Aug 17, 2023
1 parent 3491249 commit 08b8d7f
Show file tree
Hide file tree
Showing 20 changed files with 953 additions and 46 deletions.
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,10 @@ Color scheme is generated by [Material Theme Builder](https://www.figma.com/comm
- Easy to change the color scheme
- All Material widgets that come with Flutter are already supported and use the color scheme

### Theme Scope

`ThemeScope` is an inherited widget that enables users to operate themes. It supports custom themes and system themes as well as the standard ones. The chosen theme is saved in the local memory (shared_preferences) See material_context, theme_scope, app_theme and home_screen for more details.

## Database

`Sizzle starter` comes with drift and its executors preinstalled in `packages/database`. It is **recommended** to use drift for database management.
Expand Down
6 changes: 3 additions & 3 deletions lib/src/core/localization/generated/intl/messages_en.dart
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,11 @@ typedef String MessageIfAbsent(String messageStr, List<dynamic> args);
class MessageLookup extends MessageLookupByLibrary {
String get localeName => 'en';

static String m0(value) => "Sample placeholder ${value}";

final messages = _notInlinedMessages(_notInlinedMessages);
static Map<String, Function> _notInlinedMessages(_) => <String, Function>{
"appTitle": MessageLookupByLibrary.simpleMessage("sizzle_starter"),
"samplePlaceholder": m0
"dark_themes": MessageLookupByLibrary.simpleMessage("Dark Themes"),
"light_themes": MessageLookupByLibrary.simpleMessage("Light Themes"),
"system_theme": MessageLookupByLibrary.simpleMessage("System Theme")
};
}
6 changes: 3 additions & 3 deletions lib/src/core/localization/generated/intl/messages_es.dart
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,11 @@ typedef String MessageIfAbsent(String messageStr, List<dynamic> args);
class MessageLookup extends MessageLookupByLibrary {
String get localeName => 'es';

static String m0(value) => "Espacio reservado de muestra ${value}";

final messages = _notInlinedMessages(_notInlinedMessages);
static Map<String, Function> _notInlinedMessages(_) => <String, Function>{
"appTitle": MessageLookupByLibrary.simpleMessage("sizzle_starter"),
"samplePlaceholder": m0
"dark_themes": MessageLookupByLibrary.simpleMessage("Tema oscuro"),
"light_themes": MessageLookupByLibrary.simpleMessage("Tema claro"),
"system_theme": MessageLookupByLibrary.simpleMessage("Tema del sistema")
};
}
32 changes: 26 additions & 6 deletions lib/src/core/localization/generated/l10n.dart

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 3 additions & 4 deletions lib/src/core/localization/translations/intl_en.arb
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,7 @@
"@appTitle": {
"description": "The title of the application"
},
"samplePlaceholder": "Sample placeholder {value}",
"@samplePlaceholder": {
"description": "Sample placeholder"
}
"light_themes": "Light Themes",
"dark_themes": "Dark Themes",
"system_theme": "System Theme"
}
7 changes: 3 additions & 4 deletions lib/src/core/localization/translations/intl_es.arb
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,7 @@
"@appTitle": {
"description": "El título de la aplicación"
},
"samplePlaceholder": "Espacio reservado de muestra {value}",
"@samplePlaceholder": {
"description": "Espacio reservado de muestra"
}
"light_themes": "Tema claro",
"dark_themes": "Tema oscuro",
"system_theme": "Tema del sistema"
}
2 changes: 1 addition & 1 deletion lib/src/core/utils/logger.dart
Original file line number Diff line number Diff line change
Expand Up @@ -239,7 +239,7 @@ final class AppLogger$Logging extends Logger {
extension on DateTime {
/// Transforms DateTime to String with format: 00:00:00
String formatTime() =>
[hour, minute, second].map((i) => i.toString().padLeft(2)).join(':');
[hour, minute, second].map((i) => i.toString().padLeft(2, '0')).join(':');
}

extension on logging.Level {
Expand Down
2 changes: 2 additions & 0 deletions lib/src/core/utils/pattern_match.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
/// A function that takes value of type [S] and returns a value of type [T].
typedef PatternMatch<T, S> = T Function(S value);
217 changes: 217 additions & 0 deletions lib/src/feature/app/data/theme_datasource.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,217 @@
import 'dart:async';
import 'dart:convert';

import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:sizzle_starter/src/feature/app/model/app_theme.dart';

/// {@template theme_datasource}
/// [ThemeDataSource] is an entry point to the theme data layer.
///
/// This is used to set and get theme.
/// {@endtemplate}
abstract interface class ThemeDataSource {
/// Set theme
Future<void> setTheme(AppTheme theme);

/// Get current theme from cache
AppTheme? loadThemeFromCache();
}

/// {@macro theme_datasource}
final class ThemeDataSourceImpl implements ThemeDataSource {
/// {@macro theme_datasource}
const ThemeDataSourceImpl({
required SharedPreferences sharedPreferences,
}) : _sharedPreferences = sharedPreferences;

final SharedPreferences _sharedPreferences;

static const _prefix = 'theme_';

@override
Future<void> setTheme(AppTheme theme) async {
await _sharedPreferences.setString(
'$_prefix.theme',
_themeCodec.encode(theme),
);

return;
}

@override
AppTheme? loadThemeFromCache() {
final theme = _sharedPreferences.getString('$_prefix.theme');

if (theme != null) {
return _themeCodec.decode(theme);
}

return null;
}
}

const _themeCodec = _AppThemeCodec();

final class _AppThemeCodec extends Codec<AppTheme, String> {
const _AppThemeCodec();

@override
Converter<String, AppTheme> get decoder => const _AppThemeDecoder();

@override
Converter<AppTheme, String> get encoder => const _AppThemeEncoder();
}

final class _AppThemeDecoder extends Converter<String, AppTheme> {
const _AppThemeDecoder();

@pragma('vm:prefer-inline')
static Color? _colorOrNull(int? value) => value != null ? Color(value) : null;

@override
AppTheme convert(String input) {
final json = jsonDecode(input) as Map<String, Object?>;

if (json
case {
'type': final String type,
'colorScheme': {
'brightness': final String? brightness,
'primary': final int? primary,
'primaryContainer': final int? primaryContainer,
'onPrimary': final int? onPrimary,
'onPrimaryContainer': final int? onPrimaryContainer,
'secondary': final int? secondary,
'secondaryContainer': final int? secondaryContainer,
'onSecondary': final int? onSecondary,
'onSecondaryContainer': final int? onSecondaryContainer,
'tertiary': final int? tertiary,
'onTertiary': final int? onTertiary,
'tertiaryContainer': final int? tertiaryContainer,
'onTertiaryContainer': final int? onTertiaryContainer,
'surface': final int? surface,
'onSurface': final int? onSurface,
'background': final int? background,
'onBackground': final int? onBackground,
'error': final int? error,
'onError': final int? onError,
'errorContainer': final int? errorContainer,
'onErrorContainer': final int? onErrorContainer,
'surfaceVariant': final int? surfaceVariant,
'onSurfaceVariant': final int? onSurfaceVariant,
'outline': final int? outline,
'outlineVariant': final int? outlineVariant,
'shadow': final int? shadow,
'scrim': final int? scrim,
'inverseSurface': final int? inverseSurface,
'onInverseSurface': final int? onInverseSurface,
'inversePrimary': final int? inversePrimary,
'surfaceTint': final int? surfaceTint,
},
}) {
if (primary == null ||
secondary == null ||
surface == null ||
background == null ||
error == null ||
onPrimary == null ||
onSecondary == null ||
onSurface == null ||
onBackground == null ||
onError == null ||
brightness == null) {
return AppTheme.create(
type: AppColorSchemeType.fromString(type),
);
}
return AppTheme.create(
type: AppColorSchemeType.fromString(type),
colorScheme: ColorScheme(
primary: Color(primary),
secondary: Color(secondary),
surface: Color(surface),
background: Color(background),
error: Color(error),
onPrimary: Color(onPrimary),
onSecondary: Color(onSecondary),
onSurface: Color(onSurface),
onBackground: Color(onBackground),
onError: Color(onError),
primaryContainer: _colorOrNull(primaryContainer),
secondaryContainer: _colorOrNull(secondaryContainer),
errorContainer: _colorOrNull(errorContainer),
onPrimaryContainer: _colorOrNull(onPrimaryContainer),
onSecondaryContainer: _colorOrNull(onSecondaryContainer),
onErrorContainer: _colorOrNull(onErrorContainer),
tertiary: _colorOrNull(tertiary),
onTertiary: _colorOrNull(onTertiary),
tertiaryContainer: _colorOrNull(tertiaryContainer),
onTertiaryContainer: _colorOrNull(onTertiaryContainer),
surfaceVariant: _colorOrNull(surfaceVariant),
onSurfaceVariant: _colorOrNull(onSurfaceVariant),
outline: _colorOrNull(outline),
outlineVariant: _colorOrNull(outlineVariant),
shadow: _colorOrNull(shadow),
scrim: _colorOrNull(scrim),
inverseSurface: _colorOrNull(inverseSurface),
onInverseSurface: _colorOrNull(onInverseSurface),
inversePrimary: _colorOrNull(inversePrimary),
surfaceTint: _colorOrNull(surfaceTint),
brightness: switch (brightness) {
'light' => Brightness.light,
'dark' => Brightness.dark,
_ => throw Exception('Unknown brightness: $brightness'),
},
),
);
}
throw Exception('Unknown json: $json');
}
}

final class _AppThemeEncoder extends Converter<AppTheme, String> {
const _AppThemeEncoder();

@override
String convert(AppTheme input) {
final json = {
'type': input.type.toString(),
'colorScheme': {
'brightness': input.colorScheme?.brightness.name,
'primary': input.colorScheme?.primary.value,
'onPrimary': input.colorScheme?.onPrimary.value,
'primaryContainer': input.colorScheme?.primaryContainer.value,
'onPrimaryContainer': input.colorScheme?.onPrimaryContainer.value,
'secondary': input.colorScheme?.secondary.value,
'onSecondary': input.colorScheme?.onSecondary.value,
'secondaryContainer': input.colorScheme?.secondaryContainer.value,
'onSecondaryContainer': input.colorScheme?.onSecondaryContainer.value,
'surface': input.colorScheme?.surface.value,
'onSurface': input.colorScheme?.onSurface.value,
'background': input.colorScheme?.background.value,
'onBackground': input.colorScheme?.onBackground.value,
'error': input.colorScheme?.error.value,
'onError': input.colorScheme?.onError.value,
'errorContainer': input.colorScheme?.errorContainer.value,
'onErrorContainer': input.colorScheme?.onErrorContainer.value,
'tertiary': input.colorScheme?.tertiary.value,
'onTertiary': input.colorScheme?.onTertiary.value,
'tertiaryContainer': input.colorScheme?.tertiaryContainer.value,
'onTertiaryContainer': input.colorScheme?.onTertiaryContainer.value,
'surfaceVariant': input.colorScheme?.surfaceVariant.value,
'onSurfaceVariant': input.colorScheme?.onSurfaceVariant.value,
'outline': input.colorScheme?.outline.value,
'outlineVariant': input.colorScheme?.outlineVariant.value,
'shadow': input.colorScheme?.shadow.value,
'scrim': input.colorScheme?.scrim.value,
'inverseSurface': input.colorScheme?.inverseSurface.value,
'onInverseSurface': input.colorScheme?.onInverseSurface.value,
'inversePrimary': input.colorScheme?.inversePrimary.value,
'surfaceTint': input.colorScheme?.surfaceTint.value,
}
};

return jsonEncode(json);
}
}
30 changes: 30 additions & 0 deletions lib/src/feature/app/data/theme_repository.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import 'dart:async';

import 'package:sizzle_starter/src/feature/app/data/theme_datasource.dart';
import 'package:sizzle_starter/src/feature/app/model/app_theme.dart';

/// {@template theme_repository}
/// Repository which manages the current theme.
/// {@endtemplate}
abstract interface class ThemeRepository {
/// Set theme
Future<void> setTheme(AppTheme theme);

/// Observe current theme changes
AppTheme loadAppThemeFromCache();
}

/// {@macro theme_repository}
final class ThemeRepositoryImpl implements ThemeRepository {
final ThemeDataSource _dataSource;

/// {@macro theme_repository}
const ThemeRepositoryImpl(this._dataSource);

@override
Future<void> setTheme(AppTheme theme) => _dataSource.setTheme(theme);

@override
AppTheme loadAppThemeFromCache() =>
_dataSource.loadThemeFromCache() ?? AppTheme.systemScheme;
}
Loading

0 comments on commit 08b8d7f

Please sign in to comment.