add theme mode feature

This commit is contained in:
kodjodevf 2023-04-07 16:52:01 +01:00
parent 143756cff6
commit 7205018e20
22 changed files with 452 additions and 89 deletions

View file

@ -1,3 +1,3 @@
{
"java.configuration.updateBuildConfiguration": "interactive"
"java.configuration.updateBuildConfiguration": "automatic"
}

View file

@ -27,7 +27,7 @@ apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
android {
compileSdkVersion flutter.compileSdkVersion
ndkVersion flutter.ndkVersion
ndkVersion "25.1.8937393"
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8

BIN
assets/switch.riv Normal file

Binary file not shown.

View file

@ -4,4 +4,5 @@ class HiveConstant {
static String get hiveBoxMangaHistory => "_manga_box_history_";
static String get hiveBoxMangaSource => "_manga_box_source_";
static String get hiveBoxMangaFilter => "_manga_box_filter_";
static String get hiveBoxAppSettings => "_app_box_settings_";
}

View file

@ -1,6 +1,7 @@
import 'package:flex_color_scheme/flex_color_scheme.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:hive_flutter/hive_flutter.dart';
import 'package:mangayomi/constant.dart';
import 'package:mangayomi/models/manga_history.dart';
@ -8,6 +9,9 @@ import 'package:mangayomi/models/model_manga.dart';
import 'package:mangayomi/router/router.dart';
import 'package:mangayomi/source/source_model.dart';
import 'views/more/settings/appearance/flex_scheme_color_provider.dart';
import 'views/more/settings/appearance/thememode_provider.dart';
void main() async {
await Hive.initFlutter();
Hive.registerAdapter(ModelMangaAdapter());
@ -19,6 +23,7 @@ void main() async {
await Hive.openBox<SourceModel>(HiveConstant.hiveBoxMangaSource);
await Hive.openBox(HiveConstant.hiveBoxMangaInfo);
await Hive.openBox(HiveConstant.hiveBoxMangaFilter);
await Hive.openBox(HiveConstant.hiveBoxAppSettings);
runApp(const ProviderScope(child: MyApp()));
}
@ -28,25 +33,45 @@ class MyApp extends ConsumerWidget {
// This widget is the root of your application.
@override
Widget build(BuildContext context, WidgetRef ref) {
ThemeData themeLight = FlexThemeData.light(
colors: ref.watch(flexSchemeColorProvider),
surfaceMode: FlexSurfaceMode.highScaffoldLevelSurface,
blendLevel: 24,
appBarOpacity: 0.00,
subThemesData: const FlexSubThemesData(
blendOnLevel: 24,
thinBorderWidth: 2.0,
unselectedToggleIsColored: true,
inputDecoratorRadius: 24.0,
chipRadius: 24.0,
dialogBackgroundSchemeColor: SchemeColor.background,
),
useMaterial3ErrorColors: true,
visualDensity: FlexColorScheme.comfortablePlatformDensity,
useMaterial3: true,
fontFamily: GoogleFonts.aBeeZee().fontFamily,
);
ThemeData themeDark = FlexThemeData.dark(
colors: ref.watch(flexSchemeColorProvider),
surfaceMode: FlexSurfaceMode.highScaffoldLevelSurface,
blendLevel: 24,
appBarOpacity: 0.00,
subThemesData: const FlexSubThemesData(
blendOnLevel: 24,
thinBorderWidth: 2.0,
unselectedToggleIsColored: true,
inputDecoratorRadius: 24.0,
chipRadius: 24.0,
dialogBackgroundSchemeColor: SchemeColor.background,
),
useMaterial3ErrorColors: true,
visualDensity: FlexColorScheme.comfortablePlatformDensity,
useMaterial3: true,
fontFamily: GoogleFonts.aBeeZee().fontFamily,
);
final router = ref.watch(routerProvider);
return MaterialApp.router(
theme: FlexThemeData.light(
colors: ThemeAA.schemes[6].light,
surfaceMode: FlexSurfaceMode.highScaffoldLevelSurface,
blendLevel: 24,
appBarOpacity: 0.00,
subThemesData: const FlexSubThemesData(
blendOnLevel: 24,
thinBorderWidth: 2.0,
unselectedToggleIsColored: true,
inputDecoratorRadius: 24.0,
chipRadius: 24.0,
dialogBackgroundSchemeColor: SchemeColor.background,
),
useMaterial3ErrorColors: true,
visualDensity: FlexColorScheme.comfortablePlatformDensity,
useMaterial3: true,
),
theme: ref.watch(themeModeProvider) ? themeLight : themeDark,
debugShowCheckedModeBanner: false,
routeInformationParser: router.routeInformationParser,
routerDelegate: router.routerDelegate,

View file

@ -23,3 +23,6 @@ final hiveBoxMangaFilterProvider = Provider<Box>((ref) {
final hiveBoxMangaSourceProvider = Provider<Box<SourceModel>>((ref) {
return Hive.box<SourceModel>(HiveConstant.hiveBoxMangaSource);
});
final hiveBoxSettings = Provider<Box>((ref) {
return Hive.box(HiveConstant.hiveBoxAppSettings);
});

View file

@ -13,6 +13,8 @@ import 'package:mangayomi/views/manga/detail/manga_reader_detail.dart';
import 'package:mangayomi/views/manga/home/home.dart';
import 'package:mangayomi/views/manga/reader/manga_reader_view.dart';
import 'package:mangayomi/views/more/more_screen.dart';
import 'package:mangayomi/views/more/settings/appearance/appearance_screen.dart';
import 'package:mangayomi/views/more/settings/settings_screen.dart';
import 'package:mangayomi/views/updates/updates_screen.dart';
final routerProvider = Provider<GoRouter>((ref) {
@ -149,6 +151,32 @@ class AsyncRouterNotifier extends ChangeNotifier {
);
},
),
GoRoute(
path: "/settings",
name: "settings",
builder: (context, state) {
return const SettingsScreen();
},
pageBuilder: (context, state) {
return CustomTransition(
key: state.pageKey,
child: const SettingsScreen(),
);
},
),
GoRoute(
path: "/appearance",
name: "appearance",
builder: (context, state) {
return const AppearanceScreen();
},
pageBuilder: (context, state) {
return CustomTransition(
key: state.pageKey,
child: const AppearanceScreen(),
);
},
),
];
}

View file

@ -102,6 +102,8 @@ class _HistoryScreenState extends ConsumerState<HistoryScreen> {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
width: 60,
height: 90,
child: GestureDetector(
onTap: () {
final model = ModelManga(
@ -127,7 +129,7 @@ class _HistoryScreenState extends ConsumerState<HistoryScreen> {
child: cachedNetworkImage(
imageUrl: element.modelManga.imageUrl!,
width: 60,
height: 100,
height: 90,
fit: BoxFit.cover),
),
),
@ -168,7 +170,7 @@ class _HistoryScreenState extends ConsumerState<HistoryScreen> {
fontSize: 14,
fontWeight:
FontWeight.bold),
textAlign: TextAlign.center,
textAlign: TextAlign.start,
),
Row(
crossAxisAlignment:

View file

@ -266,6 +266,7 @@ class _MangaDetailViewState extends ConsumerState<MangaDetailView>
: SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(
mainAxisAlignment: MainAxisAlignment.start,
children: [
for (var i = 0;
i < widget.modelManga!.genre!.length;
@ -402,7 +403,7 @@ class _MangaDetailViewState extends ConsumerState<MangaDetailView>
child: Column(
children: const [
Icon(
Icons.travel_explore,
FontAwesomeIcons.earthAfrica,
size: 25,
),
SizedBox(

View file

@ -21,25 +21,28 @@ class ReadMoreWidgetState extends State<ReadMoreWidget>
children: [
Stack(
children: [
ExpandableText(
animationDuration: const Duration(milliseconds: 500),
onExpandedChanged: (ok) {
setState(() => expanded = ok);
widget.onChanged(ok);
},
expandOnTextTap: true,
widget.text,
expandText: '',
maxLines: 3,
expanded: false,
onPrefixTap: () {
setState(() => expanded = !expanded);
widget.onChanged(expanded);
},
linkColor: Theme.of(context).scaffoldBackgroundColor,
animation: true,
collapseOnTextTap: true,
prefixText: '',
Padding(
padding: const EdgeInsets.symmetric(horizontal: 2),
child: ExpandableText(
animationDuration: const Duration(milliseconds: 500),
onExpandedChanged: (ok) {
setState(() => expanded = ok);
widget.onChanged(ok);
},
expandOnTextTap: true,
widget.text.trim(),
expandText: '',
maxLines: 3,
expanded: false,
onPrefixTap: () {
setState(() => expanded = !expanded);
widget.onChanged(expanded);
},
linkColor: Theme.of(context).scaffoldBackgroundColor,
animation: true,
collapseOnTextTap: true,
prefixText: '',
),
),
if (!expanded)
Positioned(

View file

@ -1,5 +1,6 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
class MoreScreen extends StatelessWidget {
const MoreScreen({super.key});
@ -17,17 +18,17 @@ class MoreScreen extends StatelessWidget {
const Divider(
color: Colors.grey,
),
ListTile(
onTap: () {},
leading:
const SizedBox(height: 40, child: Icon(Icons.cloud_off)),
subtitle: const Text('Filter all entries in your library'),
title: const Text('Donloaded only'),
trailing: Switch(
value: false,
onChanged: (value) {},
),
),
// ListTile(
// onTap: () {},
// leading:
// const SizedBox(height: 40, child: Icon(Icons.cloud_off)),
// subtitle: const Text('Filter all entries in your library'),
// title: const Text('Donloaded only'),
// trailing: Switch(
// value: false,
// onChanged: (value) {},
// ),
// ),
ListTile(
onTap: () {},
leading: const SizedBox(
@ -42,45 +43,47 @@ class MoreScreen extends StatelessWidget {
const Divider(
color: Colors.grey,
),
// ListTile(
// onTap: () {},
// leading: const SizedBox(
// height: 40, child: Icon(Icons.download_outlined)),
// title: const Text('Donwload queue'),
// ),
// ListTile(
// onTap: () {},
// leading: Container(
// height: 20,
// width: 20,
// color: Colors.grey,
// ),
// title: const Text('Categories'),
// ),
// ListTile(
// onTap: () {},
// leading: Container(
// height: 20,
// width: 20,
// color: Colors.grey,
// ),
// title: const Text('Statistics'),
// ),
// ListTile(
// onTap: () {},
// leading: const SizedBox(
// height: 40,
// child: Icon(Icons.settings_backup_restore_sharp)),
// title: const Text('Backup and restore'),
// ),
// const Divider(
// color: Colors.grey,
// ),
ListTile(
onTap: () {},
leading: const SizedBox(
height: 40, child: Icon(Icons.download_outlined)),
title: const Text('Donwload queue'),
),
ListTile(
onTap: () {},
leading: Container(
height: 20,
width: 20,
color: Colors.grey,
),
title: const Text('Categories'),
),
ListTile(
onTap: () {},
leading: Container(
height: 20,
width: 20,
color: Colors.grey,
),
title: const Text('Statistics'),
),
ListTile(
onTap: () {},
leading: const SizedBox(
height: 40,
child: Icon(Icons.settings_backup_restore_sharp)),
title: const Text('Backup and restore'),
),
const Divider(
color: Colors.grey,
),
ListTile(
onTap: () {},
onTap: () {
context.push('/settings');
},
leading: const SizedBox(
height: 40, child: Icon(Icons.settings_outlined)),
title: const Text('Backup and restore'),
title: const Text('Settings'),
),
ListTile(
onTap: () {},

View file

@ -0,0 +1,21 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:mangayomi/views/more/settings/appearance/dark_mode_button.dart';
import 'package:mangayomi/views/more/settings/appearance/theme_selector.dart';
import 'package:mangayomi/views/more/settings/appearance/thememode_provider.dart';
class AppearanceScreen extends ConsumerWidget {
const AppearanceScreen({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
return Scaffold(
appBar: AppBar(
title: const Text("Appearance"),
),
body: Column(
children: const [DarkModeButton(), ThemeSelector()],
),
);
}
}

View file

@ -0,0 +1,52 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:mangayomi/views/more/settings/appearance/thememode_provider.dart';
import 'package:rive/rive.dart';
class DarkModeButton extends ConsumerStatefulWidget {
const DarkModeButton({
super.key,
});
@override
ConsumerState<DarkModeButton> createState() => _DarkModeButtonState();
}
class _DarkModeButtonState extends ConsumerState<DarkModeButton> {
SMIBool? _bump;
void _onRiveInit(Artboard artboard) {
final controller =
StateMachineController.fromArtboard(artboard, 'State Machine 1');
artboard.addController(controller!);
_bump = controller.findInput<bool>('isDark') as SMIBool;
_bump?.value = !ref.watch(themeModeProvider);
}
void _hitBump(bool value) => _bump?.value = value;
@override
Widget build(BuildContext context) {
bool isLight = ref.watch(themeModeProvider);
_hitBump(!isLight);
return ListTile(
onTap: () {
if (!isLight == true) {
ref.read(themeModeProvider.notifier).setLightTheme();
} else {
ref.read(themeModeProvider.notifier).setDarkTheme();
}
},
title: const Text("Theme mode"),
subtitle: Text(ref.watch(themeModeProvider) ? 'Light' : 'Dark'),
trailing: SizedBox(
height: 80,
width: 80,
child: RiveAnimation.asset(
'assets/switch.riv',
onInit: _onRiveInit,
),
),
);
}
}

View file

@ -0,0 +1,36 @@
import 'package:flex_color_scheme/flex_color_scheme.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:mangayomi/providers/hive_provider.dart';
import 'package:mangayomi/views/more/settings/appearance/thememode_provider.dart';
final flexSchemeColorProvider =
StateNotifierProvider<ThemeColorState, FlexSchemeColor>((ref) {
return ThemeColorState(ref);
});
class ThemeColorState extends StateNotifier<FlexSchemeColor> {
final Ref _ref;
ThemeColorState(this._ref) : super(FlexColor.deepBlue.light) {
if (_ref.watch(themeModeProvider)) {
if (_ref.watch(hiveBoxSettings).get('FlexColorIndex') != null) {
state = ThemeAA
.schemes[_ref.watch(hiveBoxSettings).get('FlexColorIndex')].light;
}
} else {
if (_ref.watch(hiveBoxSettings).get('FlexColorIndex') != null) {
state = ThemeAA
.schemes[_ref.watch(hiveBoxSettings).get('FlexColorIndex')].dark;
}
}
}
void setTheme(FlexSchemeColor color, int index) {
state = color;
_ref.watch(hiveBoxSettings).put('FlexColorIndex', index);
}
}
class ThemeAA {
static const List<FlexSchemeData> schemes = <FlexSchemeData>[
...FlexColor.schemesList,
];
}

View file

@ -0,0 +1,103 @@
import 'package:flex_color_scheme/flex_color_scheme.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:mangayomi/providers/hive_provider.dart';
import 'package:mangayomi/views/more/settings/appearance/flex_scheme_color_provider.dart';
class ThemeSelector extends ConsumerStatefulWidget {
const ThemeSelector({
super.key,
this.contentPadding,
});
final EdgeInsetsGeometry? contentPadding;
@override
ConsumerState<ThemeSelector> createState() => _ThemeSelectorState();
}
class _ThemeSelectorState extends ConsumerState<ThemeSelector> {
@override
Widget build(BuildContext context) {
int selected =
ref.watch(hiveBoxSettings).get('FlexColorIndex', defaultValue: 7);
const double height = 45;
const double width = height * 1.5;
final ThemeData theme = Theme.of(context);
final bool isLight = Theme.of(context).brightness == Brightness.light;
final ColorScheme scheme = Theme.of(context).colorScheme;
return SizedBox(
height: 130,
child: Row(
children: <Widget>[
Expanded(
child: ListView.builder(
padding: const EdgeInsetsDirectional.only(start: 8, end: 16),
physics: const ClampingScrollPhysics(),
scrollDirection: Axis.horizontal,
itemCount: ThemeAA.schemes.length,
itemBuilder: (BuildContext context, int index) {
return Padding(
padding: const EdgeInsets.all(8.0),
child: Stack(
children: [
Column(
children: [
FlexThemeModeOptionButton(
flexSchemeColor: isLight
? ThemeAA.schemes[index].light
: ThemeAA.schemes[index].dark,
selected: selected == index,
selectedBorder: BorderSide(
color: theme.primaryColorLight,
width: 4,
),
unselectedBorder: BorderSide.none,
backgroundColor: scheme.background,
width: width,
height: height,
padding: EdgeInsets.zero,
borderRadius: 0,
onSelect: () {
setState(() {
selected = index;
});
isLight
? ref
.read(flexSchemeColorProvider.notifier)
.setTheme(ThemeAA.schemes[selected].light,
selected)
: ref
.read(flexSchemeColorProvider.notifier)
.setTheme(ThemeAA.schemes[selected].dark,
selected);
},
optionButtonPadding: EdgeInsets.zero,
optionButtonMargin: EdgeInsets.zero,
),
Text(ThemeAA.schemes[index].name)
],
),
if (selected == index)
Padding(
padding: const EdgeInsets.all(5),
child: CircleAvatar(
radius: 14,
backgroundColor: theme.primaryColorLight,
child: const Icon(
FontAwesomeIcons.check,
color: Colors.black,
size: 16,
)),
)
],
),
);
},
),
),
],
),
);
}
}

View file

@ -0,0 +1,26 @@
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:mangayomi/providers/hive_provider.dart';
final themeModeProvider = StateNotifierProvider<ThemeModeState, bool>((ref) {
return ThemeModeState(ref);
});
final onPressedProvider = StateProvider<String>((ref) {
return '';
});
class ThemeModeState extends StateNotifier<bool> {
final Ref _ref;
ThemeModeState(this._ref)
: super(_ref.watch(hiveBoxSettings).get('isLight', defaultValue: true)!);
void setLightTheme() {
state = true;
_ref.watch(hiveBoxSettings).put('isLight', state);
}
void setDarkTheme() {
state = false;
_ref.watch(hiveBoxSettings).put('isLight', state);
}
}

View file

@ -0,0 +1,26 @@
import 'package:flutter/material.dart';
import 'package:flutter/src/widgets/framework.dart';
import 'package:flutter/src/widgets/placeholder.dart';
import 'package:go_router/go_router.dart';
class SettingsScreen extends StatelessWidget {
const SettingsScreen({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Settings"),
actions: [],
),
body: Column(
children: [
ListTile(
title: Text("Appearance"),
leading: const Icon(Icons.color_lens_rounded),
onTap: () => context.push('/appearance')),
],
),
);
}
}

View file

@ -7,10 +7,12 @@ import Foundation
import flutter_js
import path_provider_foundation
import rive_common
import sqflite
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
FlutterJsPlugin.register(with: registry.registrar(forPlugin: "FlutterJsPlugin"))
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
RivePlugin.register(with: registry.registrar(forPlugin: "RivePlugin"))
SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin"))
}

View file

@ -416,6 +416,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "6.5.2"
google_fonts:
dependency: "direct main"
description:
name: google_fonts
sha256: "927573f2e8a8d65c17931e21918ad0ab0666b1b636537de7c4932bdb487b190f"
url: "https://pub.dev"
source: hosted
version: "4.0.3"
graphs:
dependency: transitive
description:
@ -720,6 +728,22 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.2.2"
rive:
dependency: "direct main"
description:
name: rive
sha256: "763b4915b5245428f1188d38c2ff8c26da83ca194d345daa319ca32d69ed670f"
url: "https://pub.dev"
source: hosted
version: "0.10.3"
rive_common:
dependency: transitive
description:
name: rive_common
sha256: "12ea4a1ca1aa2ddeb2ef212afa20517d6c140a5deb32149c713912a7e6b7e26e"
url: "https://pub.dev"
source: hosted
version: "0.0.3"
riverpod:
dependency: transitive
description:

View file

@ -50,6 +50,8 @@ dependencies:
draggable_scrollbar: ^0.1.0
grouped_list: ^5.1.2
intl: ^0.18.0
rive: ^0.10.3
google_fonts: ^4.0.3
# The following adds the Cupertino Icons font to your application.
@ -81,7 +83,8 @@ flutter:
uses-material-design: true
# To add assets to your application, add an assets section, like this:
# assets:
assets:
- assets/
# - images/a_dot_burr.jpeg
# - images/a_dot_ham.jpeg

View file

@ -7,8 +7,11 @@
#include "generated_plugin_registrant.h"
#include <flutter_js/flutter_js_plugin.h>
#include <rive_common/rive_plugin.h>
void RegisterPlugins(flutter::PluginRegistry* registry) {
FlutterJsPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("FlutterJsPlugin"));
RivePluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("RivePlugin"));
}

View file

@ -4,6 +4,7 @@
list(APPEND FLUTTER_PLUGIN_LIST
flutter_js
rive_common
)
list(APPEND FLUTTER_FFI_PLUGIN_LIST