Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Themes #286

Merged
merged 9 commits into from
May 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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