Skip to content

Commit

Permalink
Added app themes (#286)
Browse files Browse the repository at this point in the history
* Initial setup for themes

* Greyout palette + change color and save (not yet apply)

* Settings page tweaks

* Settings sections

* Save device accent color

* Some renaming

* Fix bug

* Now the theme is correctly applied

* Added translations
  • Loading branch information
ferraridamiano authored May 5, 2024
1 parent c1df8e3 commit 9705280
Show file tree
Hide file tree
Showing 23 changed files with 704 additions and 28 deletions.
1 change: 1 addition & 0 deletions lib/app_router.dart
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ final isEverythingLoadedProvider = Provider<bool>((ref) =>
ref.watch(SignificantFigures.provider).hasValue &&
ref.watch(RemoveTrailingZeros.provider).hasValue &&
ref.watch(IsDarkAmoled.provider).hasValue &&
ref.watch(ThemeColorNotifier.provider).hasValue &&
ref.watch(RevokeInternetNotifier.provider).hasValue &&
ref.watch(CurrentThemeMode.provider).hasValue &&
ref.watch(CurrentLocale.provider).hasValue &&
Expand Down
66 changes: 40 additions & 26 deletions lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -16,38 +16,52 @@ class MyApp extends ConsumerWidget {

@override
Widget build(BuildContext context, WidgetRef ref) {
const Color fallbackColorSchemeSeed = Colors.blue;

return DynamicColorBuilder(
builder: (ColorScheme? lightDynamic, ColorScheme? darkDynamic) {
ColorScheme lightColorScheme;
ColorScheme darkColorScheme;
if (lightDynamic != null && darkDynamic != null) {
lightColorScheme = lightDynamic.harmonized();
darkColorScheme = darkDynamic.harmonized();
} else {
// Otherwise, use fallback schemes.
lightColorScheme = ColorScheme.fromSeed(
seedColor: fallbackColorSchemeSeed,
);
darkColorScheme = ColorScheme.fromSeed(
seedColor: fallbackColorSchemeSeed,
brightness: Brightness.dark,
if (lightDynamic != null) {
WidgetsBinding.instance.addPostFrameCallback(
(_) => ref.read(deviceAccentColorProvider.notifier).state =
lightDynamic.primary,
);
}

ThemeData lightTheme = ThemeData(colorScheme: lightColorScheme);
ThemeData darkTheme = ThemeData(
brightness: Brightness.dark,
colorScheme: darkColorScheme,
);
ThemeData amoledTheme = darkTheme.copyWith(
scaffoldBackgroundColor: Colors.black,
drawerTheme: const DrawerThemeData(backgroundColor: Colors.black),
);

return Consumer(builder: (context, ref, child) {
Locale? settingsLocale = ref.watch(CurrentLocale.provider).valueOrNull;
final settingsLocale = ref.watch(CurrentLocale.provider).valueOrNull;
final themeColor = ref.watch(ThemeColorNotifier.provider).valueOrNull ??
(useDeviceColor: false, colorTheme: Colors.blue);

final ThemeData lightTheme, darkTheme, amoledTheme;
// Use device accent color
if (ref.watch(deviceAccentColorProvider) != null &&
themeColor.useDeviceColor) {
lightTheme = ThemeData(colorScheme: lightDynamic!.harmonized());
darkTheme = ThemeData(
brightness: Brightness.dark,
colorScheme: darkDynamic!.harmonized(),
);
amoledTheme = darkTheme.copyWith(
scaffoldBackgroundColor: Colors.black,
drawerTheme: const DrawerThemeData(backgroundColor: Colors.black),
);
} else {
lightTheme = ThemeData(
colorScheme: ColorScheme.fromSeed(
seedColor: themeColor.colorTheme,
),
);
darkTheme = ThemeData(
colorScheme: ColorScheme.fromSeed(
seedColor: themeColor.colorTheme,
brightness: Brightness.dark,
),
brightness: Brightness.dark,
);
amoledTheme = darkTheme.copyWith(
scaffoldBackgroundColor: Colors.black,
drawerTheme: const DrawerThemeData(backgroundColor: Colors.black),
);
}

String deviceLocaleLanguageCode = Platform.localeName.split('_')[0];
Locale appLocale;
if (settingsLocale != null) {
Expand Down
46 changes: 46 additions & 0 deletions lib/models/settings.dart
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,52 @@ class IsDarkAmoled extends AsyncNotifier<bool> {
}
}

/// `null` means no accent color
final deviceAccentColorProvider = StateProvider<Color?>((ref) => null);

class ThemeColorNotifier
extends AsyncNotifier<({bool useDeviceColor, Color colorTheme})> {
static const _prefKeyDefault = 'useDeviceColor';
static const _prefKeyColor = 'colorTheme';
// Here we set default theme to Colors.blue (it is easier to support device
// that does not have a color accent)
static const deafultUseDeviceColor = false;
static const defaultColorTheme = Colors.blue;

static final provider = AsyncNotifierProvider<ThemeColorNotifier,
({bool useDeviceColor, Color colorTheme})>(ThemeColorNotifier.new);

@override
Future<({bool useDeviceColor, Color colorTheme})> build() async {
var pref = await ref.watch(sharedPref.future);
return (
useDeviceColor: pref.getBool(_prefKeyDefault) ?? deafultUseDeviceColor,
colorTheme: Color(pref.getInt(_prefKeyColor) ?? defaultColorTheme.value)
);
}

void setDefaultTheme(bool value) {
state = AsyncData((
useDeviceColor: value,
colorTheme: state.valueOrNull?.colorTheme ?? defaultColorTheme
));
ref
.read(sharedPref.future)
.then((pref) => pref.setBool(_prefKeyDefault, value));
}

void setColorTheme(Color color) {
state = AsyncData((
useDeviceColor:
state.valueOrNull?.useDeviceColor ?? deafultUseDeviceColor,
colorTheme: color
));
ref
.read(sharedPref.future)
.then((pref) => pref.setInt(_prefKeyColor, color.value));
}
}

class RevokeInternetNotifier extends AsyncNotifier<bool> {
static const _prefKey = 'revokeInternet';
static final provider = AsyncNotifierProvider<RevokeInternetNotifier, bool>(
Expand Down
108 changes: 106 additions & 2 deletions lib/pages/settings_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import 'dart:io';
import 'package:converterpro/models/currencies.dart';
import 'package:converterpro/models/settings.dart';
import 'package:converterpro/styles/consts.dart';
import 'package:converterpro/utils/palette.dart';
import 'package:converterpro/utils/utils_widgets.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_svg/flutter_svg.dart';
Expand Down Expand Up @@ -36,12 +37,24 @@ class SettingsPage extends ConsumerWidget {

updateNavBarColor(Theme.of(context).colorScheme);

Color iconColor = getIconColor(Theme.of(context));
final themeColor = ref.watch(ThemeColorNotifier.provider).valueOrNull!;

final iconColor = getIconColor(Theme.of(context));

return CustomScrollView(slivers: <Widget>[
SliverAppBar.large(title: Text(AppLocalizations.of(context)!.settings)),
SliverList(
delegate: SliverChildListDelegate([
Padding(
padding: const EdgeInsetsDirectional.only(start: 16),
child: Text(
AppLocalizations.of(context)!.appearance,
style: Theme.of(context)
.textTheme
.titleSmall
?.copyWith(color: Theme.of(context).primaryColor),
),
),
DropdownListTile(
key: const ValueKey('language'),
leading: Icon(Icons.language, color: iconColor),
Expand All @@ -63,7 +76,7 @@ class SettingsPage extends ConsumerWidget {
},
),
DropdownListTile(
leading: Icon(Icons.palette_outlined, color: iconColor),
leading: Icon(Icons.contrast, color: iconColor),
title: AppLocalizations.of(context)!.theme,
textStyle: textStyle,
items: mapTheme.values.toList(),
Expand All @@ -87,6 +100,38 @@ class SettingsPage extends ConsumerWidget {
},
shape: const RoundedRectangleBorder(borderRadius: borderRadius),
),
ListTile(
title: Text(AppLocalizations.of(context)!.themeColor),
leading: Icon(Icons.palette_outlined, color: iconColor),
shape: const RoundedRectangleBorder(borderRadius: borderRadius),
trailing: Padding(
padding: const EdgeInsets.symmetric(horizontal: 18),
child: Container(
width: 24,
height: 24,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(24 / 2),
color: themeColor.useDeviceColor
? ref.watch(deviceAccentColorProvider)!
: themeColor.colorTheme,
),
),
),
onTap: () => showDialog(
context: context,
builder: (context) => const ColorPickerDialog(),
),
),
Padding(
padding: const EdgeInsetsDirectional.only(start: 16, top: 16),
child: Text(
AppLocalizations.of(context)!.conversions,
style: Theme.of(context)
.textTheme
.titleSmall
?.copyWith(color: Theme.of(context).primaryColor),
),
),
if (!kIsWeb)
SwitchListTile(
secondary: Icon(Icons.public_off, color: iconColor),
Expand Down Expand Up @@ -210,6 +255,16 @@ class SettingsPage extends ConsumerWidget {
onTap: () => context.goNamed('reorder-units'),
shape: const RoundedRectangleBorder(borderRadius: borderRadius),
),
Padding(
padding: const EdgeInsetsDirectional.only(start: 16, top: 16),
child: Text(
AppLocalizations.of(context)!.findOutMore,
style: Theme.of(context)
.textTheme
.titleSmall
?.copyWith(color: Theme.of(context).primaryColor),
),
),
ListTile(
leading: Icon(Icons.computer, color: iconColor),
title: Text(
Expand Down Expand Up @@ -403,3 +458,52 @@ class SettingsPage extends ConsumerWidget {
]);
}
}

class ColorPickerDialog extends ConsumerWidget {
const ColorPickerDialog({super.key});

@override
Widget build(BuildContext context, WidgetRef ref) {
final themeColor = ref.watch(ThemeColorNotifier.provider).valueOrNull!;
final deviceAccentColor = ref.watch(deviceAccentColorProvider);

return AlertDialog(
title: Text(AppLocalizations.of(context)!.themeColor),
content: SizedBox(
width: 300,
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (deviceAccentColor != null) ...[
SwitchListTile(
value: themeColor.useDeviceColor,
onChanged: (val) {
ref
.read(ThemeColorNotifier.provider.notifier)
.setDefaultTheme(val);
},
title: Text(AppLocalizations.of(context)!.useDeviceColor),
),
const SizedBox(height: 8),
],
Text(
!themeColor.useDeviceColor
? AppLocalizations.of(context)!.pickColor
: '',
style: Theme.of(context).textTheme.titleMedium,
),
const SizedBox(height: 4),
Palette(
initial: themeColor.colorTheme,
enabled: !themeColor.useDeviceColor,
onSelected: (color) => ref
.read(ThemeColorNotifier.provider.notifier)
.setColorTheme(color),
)
],
),
),
);
}
}
Loading

0 comments on commit 9705280

Please sign in to comment.