Made Settings persistent with autosave

This commit is contained in:
Elias Müller 2023-06-03 23:26:18 +02:00
parent e26a1e9598
commit 3f05f68ac1
9 changed files with 253 additions and 156 deletions

@ -21,6 +21,7 @@ class App extends StatefulWidget {
class _AppState extends State<App> {
int currentPage = 0;
late Timer refetchChats;
@override
void initState() {
@ -29,7 +30,8 @@ class _AppState extends State<App> {
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
Provider.of<ChatListProps>(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<ChatListProps>(context, listen: false).run();
});
@ -101,4 +103,10 @@ class _AppState extends State<App> {
);
}
@override
void dispose() {
refetchChats.cancel();
super.dispose();
}
}

@ -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<void> 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<Main> {
return Directionality(
textDirection: TextDirection.ltr,
child: Consumer<AppTheme>(
builder: (context, value, child) {
child: Consumer<SettingsProvider>(
builder: (context, settings, child) {
return MaterialApp(
debugShowCheckedModeBanner: false,
localizationsDelegates: const [
@ -95,7 +96,7 @@ class _MainState extends State<Main> {
title: 'Marianum Fulda',
themeMode: value.getMode,
themeMode: settings.val().appTheme,
theme: LightAppTheme.theme,
darkTheme: DarkAppTheme.theme,

@ -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<String, dynamic> json) => _$SettingsFromJson(json);
Map<String, dynamic> toJson() => _$SettingsToJson(this);
}

@ -0,0 +1,17 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'settings.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
Settings _$SettingsFromJson(Map<String, dynamic> json) => Settings(
Settings._themeFromJson(json['appTheme'] as String),
json['devToolsEnabled'] as bool,
);
Map<String, dynamic> _$SettingsToJson(Settings instance) => <String, dynamic>{
'appTheme': Settings._themeToJson(instance.appTheme),
'devToolsEnabled': instance.devToolsEnabled,
};

@ -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,
);
}
}

@ -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:

@ -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';

@ -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';

@ -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<Settings> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text("Einstellungen"),
),
body: ListView(
children: [
return Consumer<SettingsProvider>(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<AccountModel>(context, listen: false).logout(),
Navigator.popUntil(context, (route) => !Navigator.canPop(context)),
});
},
),
);
},
),
const Divider(),
Consumer<AppTheme>(
builder: (context, value, child) {
return ListTile(
leading: const Icon(Icons.dark_mode_outlined),
title: const Text("Farbgebung"),
trailing: DropdownButton<ThemeMode>(
value: value.getMode,
icon: const Icon(Icons.arrow_drop_down),
items: ThemeMode.values.map((e) => DropdownMenuItem<ThemeMode>(
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<AppTheme>(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<AccountModel>(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<String>(
value: "1",
items: ["1", "2", "3"].map((e) => DropdownMenuItem<String>(value: e, child: Text(e))).toList(),
const Divider(),
ListTile(
leading: const Icon(Icons.dark_mode_outlined),
title: const Text("Farbgebung"),
trailing: DropdownButton<ThemeMode>(
value: settings.val().appTheme,
icon: const Icon(Icons.arrow_drop_down),
items: ThemeMode.values.map((e) => DropdownMenuItem<ThemeMode>(
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<String>(
value: "1",
items: ["1", "2", "3"].map((e) => DropdownMenuItem<String>(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());
},
),
],
),
),
],
),
);
});
}
}