From 3f05f68ac1ec3c73cc28934ba1a3f1fdc5c65236 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elias=20M=C3=BCller?= Date: Sat, 3 Jun 2023 23:26:18 +0200 Subject: [PATCH] Made Settings persistent with autosave --- lib/app.dart | 10 +- lib/main.dart | 11 +- lib/storage/settings/settings.dart | 22 ++ lib/storage/settings/settings.g.dart | 17 ++ lib/storage/settings/settingsProvider.dart | 49 ++++ lib/{model => theming}/appTheme.dart | 10 +- lib/view/pages/talk/chatBubble.dart | 2 +- lib/view/pages/talk/chatView.dart | 2 +- lib/view/settings/settings.dart | 286 +++++++++++---------- 9 files changed, 253 insertions(+), 156 deletions(-) create mode 100644 lib/storage/settings/settings.dart create mode 100644 lib/storage/settings/settings.g.dart create mode 100644 lib/storage/settings/settingsProvider.dart rename lib/{model => theming}/appTheme.dart (78%) diff --git a/lib/app.dart b/lib/app.dart index 1ec154e..4a4e417 100644 --- a/lib/app.dart +++ b/lib/app.dart @@ -21,6 +21,7 @@ class App extends StatefulWidget { class _AppState extends State { int currentPage = 0; + late Timer refetchChats; @override void initState() { @@ -29,7 +30,8 @@ class _AppState extends State { WidgetsBinding.instance.addPostFrameCallback((timeStamp) { Provider.of(context, listen: false).run(); }); - Timer.periodic(const Duration(minutes: 1), (timer) { + refetchChats = Timer.periodic(const Duration(minutes: 1), (timer) { + if(!context.mounted) return; Provider.of(context, listen: false).run(); }); @@ -101,4 +103,10 @@ class _AppState extends State { ); } + + @override + void dispose() { + refetchChats.cancel(); + super.dispose(); + } } \ No newline at end of file diff --git a/lib/main.dart b/lib/main.dart index 917e826..f914b3b 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -9,12 +9,12 @@ import 'package:flutter_localizations/flutter_localizations.dart'; import 'app.dart'; import 'model/accountModel.dart'; -import 'model/appTheme.dart'; import 'model/chatList/chatListProps.dart'; import 'model/chatList/chatProps.dart'; import 'model/files/filesProps.dart'; import 'model/message/messageProps.dart'; import 'model/timetable/timetableProps.dart'; +import 'storage/settings/settingsProvider.dart'; import 'theming/darkAppTheme.dart'; import 'theming/lightAppTheme.dart'; import 'view/login/login.dart'; @@ -33,8 +33,9 @@ Future main() async { runApp( MultiProvider( providers: [ + ChangeNotifierProvider(create: (context) => SettingsProvider()), ChangeNotifierProvider(create: (context) => AccountModel()), - ChangeNotifierProvider(create: (context) => AppTheme()), + ChangeNotifierProvider(create: (context) => TimetableProps()), ChangeNotifierProvider(create: (context) => ChatListProps()), ChangeNotifierProvider(create: (context) => ChatProps()), @@ -78,8 +79,8 @@ class _MainState extends State
{ return Directionality( textDirection: TextDirection.ltr, - child: Consumer( - builder: (context, value, child) { + child: Consumer( + builder: (context, settings, child) { return MaterialApp( debugShowCheckedModeBanner: false, localizationsDelegates: const [ @@ -95,7 +96,7 @@ class _MainState extends State
{ title: 'Marianum Fulda', - themeMode: value.getMode, + themeMode: settings.val().appTheme, theme: LightAppTheme.theme, darkTheme: DarkAppTheme.theme, diff --git a/lib/storage/settings/settings.dart b/lib/storage/settings/settings.dart new file mode 100644 index 0000000..7a522ab --- /dev/null +++ b/lib/storage/settings/settings.dart @@ -0,0 +1,22 @@ +import 'package:flutter/material.dart'; +import 'package:json_annotation/json_annotation.dart'; + +part 'settings.g.dart'; + +@JsonSerializable(explicitToJson: true) +class Settings { + @JsonKey( + toJson: _themeToJson, + fromJson: _themeFromJson, + ) + ThemeMode appTheme; + bool devToolsEnabled; + + Settings(this.appTheme, this.devToolsEnabled); + + static String _themeToJson(ThemeMode m) => m.name; + static ThemeMode _themeFromJson(String m) => ThemeMode.values.firstWhere((element) => element.name == m); + + factory Settings.fromJson(Map json) => _$SettingsFromJson(json); + Map toJson() => _$SettingsToJson(this); +} \ No newline at end of file diff --git a/lib/storage/settings/settings.g.dart b/lib/storage/settings/settings.g.dart new file mode 100644 index 0000000..9b01986 --- /dev/null +++ b/lib/storage/settings/settings.g.dart @@ -0,0 +1,17 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'settings.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +Settings _$SettingsFromJson(Map json) => Settings( + Settings._themeFromJson(json['appTheme'] as String), + json['devToolsEnabled'] as bool, + ); + +Map _$SettingsToJson(Settings instance) => { + 'appTheme': Settings._themeToJson(instance.appTheme), + 'devToolsEnabled': instance.devToolsEnabled, + }; diff --git a/lib/storage/settings/settingsProvider.dart b/lib/storage/settings/settingsProvider.dart new file mode 100644 index 0000000..32ed2a1 --- /dev/null +++ b/lib/storage/settings/settingsProvider.dart @@ -0,0 +1,49 @@ +import 'dart:convert'; +import 'dart:developer'; +import 'package:flutter/material.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +import 'settings.dart'; + +class SettingsProvider extends ChangeNotifier { + static const String _fieldName = "settings"; + + late SharedPreferences _storage; + late Settings _settings = _defaults(); + + Settings val({bool write = false}) { + if(write) { + notifyListeners(); + Future.delayed(const Duration(milliseconds: 300)).then((_) => update()); + } + return _settings; + } + + SettingsProvider() { + init(); + } + + void init() async { + _storage = await SharedPreferences.getInstance(); + + if(_storage.containsKey(_fieldName)) { + log("Settings from disk: ${_storage.getString(_fieldName)}"); + _settings = Settings.fromJson(jsonDecode(_storage.getString(_fieldName)!)); + } else { + _settings = _defaults(); + } + + notifyListeners(); + } + + void update() async { + await _storage.setString(_fieldName, jsonEncode(_settings.toJson())); + } + + Settings _defaults() { + return Settings( + ThemeMode.system, + false, + ); + } +} \ No newline at end of file diff --git a/lib/model/appTheme.dart b/lib/theming/appTheme.dart similarity index 78% rename from lib/model/appTheme.dart rename to lib/theming/appTheme.dart index fbb9dac..9bcd806 100644 --- a/lib/model/appTheme.dart +++ b/lib/theming/appTheme.dart @@ -1,14 +1,6 @@ import 'package:flutter/material.dart'; -class AppTheme extends ChangeNotifier { - ThemeMode _mode = ThemeMode.system; - ThemeMode get getMode => _mode; - - void setTheme(ThemeMode newMode) { - _mode = newMode; - notifyListeners(); - } - +class AppTheme { static ThemeModeDisplay getDisplayOptions(ThemeMode theme) { switch(theme) { case ThemeMode.system: diff --git a/lib/view/pages/talk/chatBubble.dart b/lib/view/pages/talk/chatBubble.dart index 17983f2..7a118e1 100644 --- a/lib/view/pages/talk/chatBubble.dart +++ b/lib/view/pages/talk/chatBubble.dart @@ -7,7 +7,7 @@ import 'package:jiffy/jiffy.dart'; import '../../../api/marianumcloud/talk/chat/getChatResponse.dart'; import '../../../api/marianumcloud/talk/room/getRoomResponse.dart'; -import '../../../model/appTheme.dart'; +import '../../../theming/appTheme.dart'; import '../../settings/debug/jsonViewer.dart'; import '../files/fileElement.dart'; import 'chatMessage.dart'; diff --git a/lib/view/pages/talk/chatView.dart b/lib/view/pages/talk/chatView.dart index 98bc3a3..f9ce0ba 100644 --- a/lib/view/pages/talk/chatView.dart +++ b/lib/view/pages/talk/chatView.dart @@ -6,7 +6,7 @@ import 'package:provider/provider.dart'; import '../../../api/marianumcloud/talk/chat/getChatResponse.dart'; import '../../../api/marianumcloud/talk/room/getRoomResponse.dart'; -import '../../../model/appTheme.dart'; +import '../../../theming/appTheme.dart'; import '../../../model/chatList/chatProps.dart'; import 'chatBubble.dart'; import 'chatTextfield.dart'; diff --git a/lib/view/settings/settings.dart b/lib/view/settings/settings.dart index 86575a1..0973c7e 100644 --- a/lib/view/settings/settings.dart +++ b/lib/view/settings/settings.dart @@ -6,9 +6,11 @@ import 'package:shared_preferences/shared_preferences.dart'; import 'package:url_launcher/url_launcher.dart'; import '../../model/accountModel.dart'; -import '../../model/appTheme.dart'; +import '../../theming/appTheme.dart'; +import '../../storage/settings/settingsProvider.dart'; import '../../widget/confirmDialog.dart'; import 'debug/debugOverview.dart'; +import 'debug/jsonViewer.dart'; class Settings extends StatefulWidget { const Settings({Key? key}) : super(key: key); @@ -29,152 +31,158 @@ class _SettingsState extends State { @override Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - title: const Text("Einstellungen"), - ), - body: ListView( - children: [ + return Consumer(builder: (context, settings, child) { + return Scaffold( + appBar: AppBar( + title: const Text("Einstellungen"), + ), + body: ListView( + children: [ - ListTile( - leading: const Icon(Icons.logout_outlined), - title: const Text("Konto abmelden"), - onTap: () { - showDialog( - context: context, - builder: (context) => ConfirmDialog( - title: "Abmelden?", - content: "Möchtest du dich wirklich abmelden?", - confirmButton: "Abmelden", - onConfirm: () { - SharedPreferences.getInstance().then((value) => { - value.clear(), - }).then((value) => { - Provider.of(context, listen: false).logout(), - Navigator.popUntil(context, (route) => !Navigator.canPop(context)), - }); - }, - ), - ); - }, - ), - - const Divider(), - - Consumer( - builder: (context, value, child) { - return ListTile( - leading: const Icon(Icons.dark_mode_outlined), - title: const Text("Farbgebung"), - trailing: DropdownButton( - value: value.getMode, - icon: const Icon(Icons.arrow_drop_down), - items: ThemeMode.values.map((e) => DropdownMenuItem( - value: e, - enabled: e != value.getMode, - child: Row( - children: [ - Icon(AppTheme.getDisplayOptions(e).icon), - const SizedBox(width: 10), - Text(AppTheme.getDisplayOptions(e).displayName), - ], - ), - )).toList(), - onChanged: (e) { - Provider.of(context, listen: false).setTheme(e ?? ThemeMode.system); - }, - ), - ); - }, - ), - - const Divider(), - - ListTile( - leading: const Icon(Icons.live_help_outlined), - title: const Text("Informationen und Lizenzen"), - onTap: () async { - final appInfo = await PackageInfo.fromPlatform(); - - if(!context.mounted) return; // TODO Fix context used in async - showAboutDialog( - context: context, - applicationIcon: const Icon(Icons.apps), - applicationName: "MarianumMobile", - applicationVersion: "${appInfo.appName}\n\nPackage: ${appInfo.packageName}\n\nVersion: ${appInfo.version}\nBuild: ${appInfo.buildNumber}", - applicationLegalese: "Dies ist ein Inoffizieller Nextcloud & Webuntis Client und wird nicht vom Marianum selbst betrieben.\n" - "Keinerlei Gewähr für Vollständigkeit, Richtigkeit und Aktualität!\n\n" - "Development build\n" - "Marianum Fulda 2023 Elias Müller", - ); - }, - trailing: const Icon(Icons.arrow_right), - ), - - ListTile( - leading: const Icon(Icons.policy_outlined), - title: const Text("Datenschutz"), - onTap: () { - launchUrl(Uri.parse("https://mhsl.eu/datenschutz.html")); - }, - trailing: const Icon(Icons.open_in_new), - ), - - ListTile( - leading: const Icon(Icons.badge_outlined), - title: const Text("Impressum"), - onTap: () { - launchUrl(Uri.parse("https://mhsl.eu/id.html")); - }, - trailing: const Icon(Icons.open_in_new), - ), - - const Divider(), - - ListTile( - leading: const Icon(Icons.developer_mode_outlined), - title: const Text("Entwicklermodus"), - trailing: Checkbox( - visualDensity: const VisualDensity(horizontal: VisualDensity.minimumDensity), - value: developerMode, - onChanged: (state) { - setState(() { - developerMode = !developerMode; - }); - }, - ), - ), - - Visibility( - visible: developerMode, - child: ListTile( - leading: const Icon(Icons.data_object), - title: const Text("Storage view"), + ListTile( + leading: const Icon(Icons.logout_outlined), + title: const Text("Konto abmelden"), onTap: () { - Navigator.push(context, MaterialPageRoute(builder: (context) { - return const DebugOverview(); - })); + showDialog( + context: context, + builder: (context) => ConfirmDialog( + title: "Abmelden?", + content: "Möchtest du dich wirklich abmelden?", + confirmButton: "Abmelden", + onConfirm: () { + SharedPreferences.getInstance().then((value) => { + value.clear(), + }).then((value) => { + Provider.of(context, listen: false).logout(), + Navigator.popUntil(context, (route) => !Navigator.canPop(context)), + }); + }, + ), + ); }, - trailing: const Icon(Icons.arrow_right), ), - ), - Visibility( - visible: developerMode && false, // TODO Implement verbose logging - child: ListTile( - leading: const Icon(Icons.logo_dev), - title: const Text("Logging verbosity"), - trailing: DropdownButton( - value: "1", - items: ["1", "2", "3"].map((e) => DropdownMenuItem(value: e, child: Text(e))).toList(), + const Divider(), + + ListTile( + leading: const Icon(Icons.dark_mode_outlined), + title: const Text("Farbgebung"), + trailing: DropdownButton( + value: settings.val().appTheme, + icon: const Icon(Icons.arrow_drop_down), + items: ThemeMode.values.map((e) => DropdownMenuItem( + value: e, + enabled: e != settings.val().appTheme, + child: Row( + children: [ + Icon(AppTheme.getDisplayOptions(e).icon), + const SizedBox(width: 10), + Text(AppTheme.getDisplayOptions(e).displayName), + ], + ), + )).toList(), onChanged: (e) { - + setState(() { + settings.val(write: true).appTheme = e!; + }); }, ), ), - ), - ], - ), - ); + + const Divider(), + + ListTile( + leading: const Icon(Icons.live_help_outlined), + title: const Text("Informationen und Lizenzen"), + onTap: () { + PackageInfo.fromPlatform().then((appInfo) { + showAboutDialog( + context: context, + applicationIcon: const Icon(Icons.apps), + applicationName: "MarianumMobile", + applicationVersion: "${appInfo.appName}\n\nPackage: ${appInfo.packageName}\n\nVersion: ${appInfo.version}\nBuild: ${appInfo.buildNumber}", + applicationLegalese: "Dies ist ein Inoffizieller Nextcloud & Webuntis Client und wird nicht vom Marianum selbst betrieben.\n" + "Keinerlei Gewähr für Vollständigkeit, Richtigkeit und Aktualität!\n\n" + "Development build\n" + "Marianum Fulda 2023 Elias Müller", + ); + }); + }, + trailing: const Icon(Icons.arrow_right), + ), + + ListTile( + leading: const Icon(Icons.policy_outlined), + title: const Text("Datenschutz"), + onTap: () { + launchUrl(Uri.parse("https://mhsl.eu/datenschutz.html")); + }, + trailing: const Icon(Icons.open_in_new), + ), + + ListTile( + leading: const Icon(Icons.badge_outlined), + title: const Text("Impressum"), + onTap: () { + launchUrl(Uri.parse("https://mhsl.eu/id.html")); + }, + trailing: const Icon(Icons.open_in_new), + ), + + const Divider(), + + ListTile( + leading: const Icon(Icons.developer_mode_outlined), + title: const Text("Entwicklermodus"), + trailing: Checkbox( + visualDensity: const VisualDensity(horizontal: VisualDensity.minimumDensity), + value: settings.val().devToolsEnabled, + onChanged: (state) { + setState(() { + settings.val(write: true).devToolsEnabled = state ?? false; + }); + }, + ), + ), + + Visibility( + visible: settings.val().devToolsEnabled, + child: Column( + children: [ + ListTile( + leading: const Icon(Icons.data_object), + title: const Text("Storage view"), + onTap: () { + Navigator.push(context, MaterialPageRoute(builder: (context) { + return const DebugOverview(); + })); + }, + trailing: const Icon(Icons.arrow_right), + ), + ListTile( + leading: const Icon(Icons.logo_dev_outlined), + title: const Text("Logging verbosity"), + trailing: DropdownButton( + value: "1", + items: ["1", "2", "3"].map((e) => DropdownMenuItem(value: e, child: Text(e))).toList(), + onChanged: (e) { + + }, + ), + ), + ListTile( + leading: const Icon(Icons.settings_applications_outlined), + title: const Text("Settings JSON dump"), + onTap: () { + JsonViewer.asDialog(context, settings.val().toJson()); + }, + ), + ], + ), + ), + ], + ), + ); + }); } }