Migrated from localstore to shared preferences

(only for user, server and theme)

This was done, because localstore is somewhat inconsistent
in terms of events on different platforms.
Also storing user, server and theme using shared-preferences
should fit into flutters ecosystem a little better
This commit is contained in:
Jakob Meier 2023-03-29 18:02:00 +02:00
parent e492a3f8ce
commit 1af8d6f068
No known key found for this signature in database
GPG key ID: 66BDC7E6A01A6152
9 changed files with 183 additions and 127 deletions

View file

@ -1,7 +1,8 @@
import 'package:localstore/localstore.dart';
import 'package:http/http.dart' as http; import 'package:http/http.dart' as http;
import 'dart:convert'; import 'dart:convert';
import 'package:shared_preferences/shared_preferences.dart';
const String wellKnownPath = "/.well-known/outbag/server"; const String wellKnownPath = "/.well-known/outbag/server";
const int defaultPort = 7223; const int defaultPort = 7223;
@ -48,15 +49,32 @@ class OutbagServer {
} }
Future<void> toDisk() async { Future<void> toDisk() async {
final db = Localstore.instance; final SharedPreferences prefs = await SharedPreferences.getInstance();
await db.collection('meta').doc('server').set(toMap());
await prefs.setString('server-host', host);
await prefs.setInt('server-port', port);
await prefs.setString('server-path', path);
await prefs.setString('server-tag', tag);
} }
static Future<OutbagServer> fromDisk() async { static Future<OutbagServer> fromDisk() async {
final db = Localstore.instance; final SharedPreferences prefs = await SharedPreferences.getInstance();
final data = await db.collection('meta').doc('server').get();
return OutbagServer.fromMap(data!); return OutbagServer(
path: prefs.getString('server-path')!,
port: prefs.getInt('server-port')!,
tag: prefs.getString('server-tag')!,
host: prefs.getString('server-host')!
);
}
static Future<void> removeDisk() async {
final SharedPreferences prefs = await SharedPreferences.getInstance();
await prefs.remove('server-host');
await prefs.remove('server-port');
await prefs.remove('server-path');
await prefs.remove('server-tag');
} }
} }

View file

@ -1,6 +1,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:localstore/localstore.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:shared_preferences/shared_preferences.dart';
class AppTheme { class AppTheme {
ThemeMode mode; ThemeMode mode;
@ -47,27 +47,14 @@ class AppTheme {
return [AppTheme.auto, AppTheme.light, AppTheme.dark]; return [AppTheme.auto, AppTheme.light, AppTheme.dark];
} }
static listen(Function(Map<String, dynamic>) cb) {
final db = Localstore.instance;
final stream = db.collection('settings').stream;
stream.listen(cb);
}
Future<void> toDisk() async { Future<void> toDisk() async {
final db = Localstore.instance; final SharedPreferences prefs = await SharedPreferences.getInstance();
await db.collection('settings').doc('ui').set({'theme': mode.index}); await prefs.setInt('theme', mode.index);
} }
static Future<AppTheme> fromDisk() async { static Future<AppTheme> fromDisk() async {
final db = Localstore.instance; final SharedPreferences prefs = await SharedPreferences.getInstance();
final doc = await db.collection('settings').doc('ui').get(); return AppTheme(ThemeMode.values[(prefs.getInt('theme')) ?? 0]);
try {
final index = doc?['theme'];
final mode = ThemeMode.values[index];
return AppTheme(mode);
} catch (_) {
return AppTheme(ThemeMode.system);
}
} }
@override @override

View file

@ -1,4 +1,4 @@
import 'package:localstore/localstore.dart'; import 'package:shared_preferences/shared_preferences.dart';
import './resolve_url.dart'; import './resolve_url.dart';
class User { class User {
@ -10,36 +10,30 @@ class User {
final OutbagServer server; final OutbagServer server;
Future<void> toDisk() async { Future<void> toDisk() async {
final db = Localstore.instance; final SharedPreferences prefs = await SharedPreferences.getInstance();
await db
.collection('meta') await prefs.setString('username', username);
.doc('auth') await prefs.setString('password', password);
.set({'username': username, 'password': password});
await server.toDisk(); await server.toDisk();
} }
static Future<User> fromDisk() async { static Future<User> fromDisk() async {
final db = Localstore.instance; final SharedPreferences prefs = await SharedPreferences.getInstance();
final data = await db.collection('meta').doc('auth').get();
final server = await OutbagServer.fromDisk(); final server = await OutbagServer.fromDisk();
return User( return User(
username: data?['username'], username: prefs.getString('username')!,
password: data?['password'], password: prefs.getString('password')!,
server: server, server: server);
);
} }
static Future<void> removeDisk() async { static Future<void> removeDisk() async {
final db = Localstore.instance; final SharedPreferences prefs = await SharedPreferences.getInstance();
await db.collection('meta').doc('auth').delete();
return;
}
static listen(Function(Map<String, dynamic>) cb) async { await prefs.remove('username');
final db = Localstore.instance; await prefs.remove('password');
final stream = db.collection('meta').stream; await OutbagServer.removeDisk();
stream.listen(cb);
} }
String get humanReadable { String get humanReadable {

View file

@ -44,57 +44,41 @@ class _OutbagAppState extends State {
AppTheme theme = AppTheme.auto; AppTheme theme = AppTheme.auto;
void loadTheme() async {
// load theme
try {
final theme = await AppTheme.fromDisk();
setState(() {
this.theme = theme;
});
} catch (_) {}
}
void loadUser() async {
// load user
try {
final user = await User.fromDisk();
setState(() {
this.user = user;
});
fetchInfo(user);
} catch (_) {
// user unavailable
// invalid credentials
// log out
setState(() {
user = null;
});
}
}
@override @override
void initState() { void initState() {
super.initState(); super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) async { WidgetsBinding.instance.addPostFrameCallback((_) async {
// wait for user to be authorized loadTheme();
User.listen((_) async { loadUser();
try {
final user = await User.fromDisk();
setState(() {
this.user = user;
});
fetchInfo(user);
} catch (_) {
// no userdata found
setState(() {
user = null;
});
}
});
AppTheme.listen((_) async {
final theme = await AppTheme.fromDisk();
setState(() {
this.theme = theme;
});
});
// load theme
try {
final theme = await AppTheme.fromDisk();
setState(() {
this.theme = theme;
});
} catch (_) {}
// load user
try {
final user = await User.fromDisk();
setState(() {
this.user = user;
});
fetchInfo(user);
} catch (_) {
// user unavailable
// invalid credentials
// log out
setState(() {
user = null;
});
}
}); });
} }
@ -184,20 +168,17 @@ class _OutbagAppState extends State {
GoRoute( GoRoute(
name: 'signin', name: 'signin',
path: 'signin', path: 'signin',
builder: (context, state) => builder: (context, state) => AuthPage(mode: Mode.signin, refresh: loadUser),
const AuthPage(mode: Mode.signin),
), ),
GoRoute( GoRoute(
name: 'signup', name: 'signup',
path: 'signup', path: 'signup',
builder: (context, state) => builder: (context, state) => AuthPage(mode: Mode.signup, refresh: loadUser),
const AuthPage(mode: Mode.signup),
), ),
GoRoute( GoRoute(
name: 'signup-ota', name: 'signup-ota',
path: 'signup-ota', path: 'signup-ota',
builder: (context, state) => builder: (context, state) => AuthPage(mode: Mode.signupOTA, refresh: loadUser),
const AuthPage(mode: Mode.signupOTA),
), ),
]), ]),
@ -215,8 +196,7 @@ class _OutbagAppState extends State {
GoRoute( GoRoute(
name: 'settings', name: 'settings',
path: 'settings', path: 'settings',
builder: (context, state) => builder: (context, state) => SettingsPage(refreshTheme: loadTheme)),
const SettingsPage()),
GoRoute( GoRoute(
path: 'join-room', path: 'join-room',
name: 'add-room', name: 'add-room',
@ -239,26 +219,22 @@ class _OutbagAppState extends State {
GoRoute( GoRoute(
name: 'edit-room', name: 'edit-room',
path: 'edit', path: 'edit',
builder: (context, state) => builder: (context, state) => EditRoomPage(
EditRoomPage( state.params['server'] ?? '',
state.params['server'] ??
'',
state.params['id'] ?? '')), state.params['id'] ?? '')),
GoRoute( GoRoute(
name: 'room-members', name: 'room-members',
path: 'members', path: 'members',
builder: (context, state) => builder: (context, state) =>
ManageRoomMembersPage( ManageRoomMembersPage(
state.params['server'] ?? state.params['server'] ?? '',
'',
state.params['id'] ?? '')), state.params['id'] ?? '')),
GoRoute( GoRoute(
name: 'room-permissions', name: 'room-permissions',
path: 'roles', path: 'roles',
builder: (context, state) => builder: (context, state) =>
EditRoomPermissionSetPage( EditRoomPermissionSetPage(
state.params['server'] ?? state.params['server'] ?? '',
'',
state.params['id'] ?? '')), state.params['id'] ?? '')),
]) ])
]), ]),

View file

@ -15,7 +15,8 @@ enum Mode {
class AuthPage extends StatefulWidget { class AuthPage extends StatefulWidget {
final Mode mode; final Mode mode;
const AuthPage({super.key, this.mode = Mode.signin}); Function refresh;
AuthPage({super.key, required this.mode, required this.refresh});
@override @override
State<StatefulWidget> createState() => _AuthPageState(); State<StatefulWidget> createState() => _AuthPageState();
@ -53,7 +54,8 @@ class _AuthPageState extends State<AuthPage> {
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
const CircularProgressIndicator(), const CircularProgressIndicator(),
Text(AppLocalizations.of(context)!.loading, style: textTheme.titleLarge), Text(AppLocalizations.of(context)!.loading,
style: textTheme.titleLarge),
]))) ])))
: Scaffold( : Scaffold(
appBar: AppBar( appBar: AppBar(
@ -73,9 +75,12 @@ class _AuthPageState extends State<AuthPage> {
keyboardType: TextInputType.url, keyboardType: TextInputType.url,
decoration: InputDecoration( decoration: InputDecoration(
prefixIcon: const Icon(Icons.dns), prefixIcon: const Icon(Icons.dns),
labelText: AppLocalizations.of(context)!.inputServerLabel, labelText: AppLocalizations.of(context)!
hintText: AppLocalizations.of(context)!.inputServerHint, .inputServerLabel,
helperText:AppLocalizations.of(context)!.inputServerHelp, hintText:
AppLocalizations.of(context)!.inputServerHint,
helperText:
AppLocalizations.of(context)!.inputServerHelp,
border: const OutlineInputBorder(), border: const OutlineInputBorder(),
), ),
), ),
@ -87,10 +92,13 @@ class _AuthPageState extends State<AuthPage> {
keyboardType: TextInputType.emailAddress, keyboardType: TextInputType.emailAddress,
decoration: InputDecoration( decoration: InputDecoration(
prefixIcon: const Icon(Icons.person), prefixIcon: const Icon(Icons.person),
labelText: AppLocalizations.of(context)!.inputUsernameLabel, labelText: AppLocalizations.of(context)!
hintText: AppLocalizations.of(context)!.inputUsernameHint, .inputUsernameLabel,
helperText:AppLocalizations.of(context)!.inputUsernameHelp, hintText: AppLocalizations.of(context)!
border: const OutlineInputBorder(), .inputUsernameHint,
helperText: AppLocalizations.of(context)!
.inputUsernameHelp,
border: const OutlineInputBorder(),
), ),
), ),
), ),
@ -102,9 +110,12 @@ class _AuthPageState extends State<AuthPage> {
obscureText: true, obscureText: true,
decoration: InputDecoration( decoration: InputDecoration(
prefixIcon: const Icon(Icons.lock), prefixIcon: const Icon(Icons.lock),
labelText: AppLocalizations.of(context)!.inputPasswordLabel, labelText: AppLocalizations.of(context)!
hintText: AppLocalizations.of(context)!.inputPasswordHint, .inputPasswordLabel,
helperText:AppLocalizations.of(context)!.inputPasswordHelp, hintText: AppLocalizations.of(context)!
.inputPasswordHint,
helperText: AppLocalizations.of(context)!
.inputPasswordHelp,
border: const OutlineInputBorder(), border: const OutlineInputBorder(),
), ),
), ),
@ -120,9 +131,12 @@ class _AuthPageState extends State<AuthPage> {
obscureText: true, obscureText: true,
decoration: InputDecoration( decoration: InputDecoration(
prefixIcon: const Icon(Icons.lock), prefixIcon: const Icon(Icons.lock),
labelText: AppLocalizations.of(context)!.inputPasswordRepeatLabel, labelText: AppLocalizations.of(context)!
hintText: AppLocalizations.of(context)!.inputPasswordRepeatHint, .inputPasswordRepeatLabel,
helperText:AppLocalizations.of(context)!.inputPasswordRepeatHelp, hintText: AppLocalizations.of(context)!
.inputPasswordRepeatHint,
helperText: AppLocalizations.of(context)!
.inputPasswordRepeatHelp,
border: const OutlineInputBorder(), border: const OutlineInputBorder(),
), ),
), ),
@ -139,9 +153,12 @@ class _AuthPageState extends State<AuthPage> {
keyboardType: TextInputType.visiblePassword, keyboardType: TextInputType.visiblePassword,
decoration: InputDecoration( decoration: InputDecoration(
prefixIcon: const Icon(Icons.key), prefixIcon: const Icon(Icons.key),
labelText: AppLocalizations.of(context)!.inputOTALabel, labelText: AppLocalizations.of(context)!
hintText: AppLocalizations.of(context)!.inputOTAHint, .inputOTALabel,
helperText:AppLocalizations.of(context)!.inputOTAHelp, hintText: AppLocalizations.of(context)!
.inputOTAHint,
helperText: AppLocalizations.of(context)!
.inputOTAHelp,
border: const OutlineInputBorder(), border: const OutlineInputBorder(),
), ),
), ),
@ -166,7 +183,8 @@ class _AuthPageState extends State<AuthPage> {
}); });
showSimpleSnackbar(scaffMgr, showSimpleSnackbar(scaffMgr,
text: AppLocalizations.of(context)!.errorPasswordsDoNotMatch, text: AppLocalizations.of(context)!
.errorPasswordsDoNotMatch,
action: AppLocalizations.of(context)!.dismiss); action: AppLocalizations.of(context)!.dismiss);
_ctrPasswordRpt.clear(); _ctrPasswordRpt.clear();
@ -201,7 +219,8 @@ class _AuthPageState extends State<AuthPage> {
}); });
showSimpleSnackbar(scaffMgr, showSimpleSnackbar(scaffMgr,
text: AppLocalizations.of(context)!.errorInvalidServer(_ctrServer.text), text: AppLocalizations.of(context)!
.errorInvalidServer(_ctrServer.text),
action: AppLocalizations.of(context)!.dismiss); action: AppLocalizations.of(context)!.dismiss);
return; return;
@ -248,6 +267,7 @@ class _AuthPageState extends State<AuthPage> {
password: password, password: password,
server: server) server: server)
.toDisk(); .toDisk();
widget.refresh();
}, after: () { }, after: () {
setState(() { setState(() {
showSpinner = false; showSpinner = false;

View file

@ -9,7 +9,8 @@ import 'package:provider/provider.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart';
class SettingsPage extends StatefulWidget { class SettingsPage extends StatefulWidget {
const SettingsPage({super.key}); Function refreshTheme;
SettingsPage({super.key, required this.refreshTheme});
@override @override
State<StatefulWidget> createState() => _SettingsPageState(); State<StatefulWidget> createState() => _SettingsPageState();
@ -113,6 +114,7 @@ class _SettingsPageState extends State<SettingsPage> {
onSelectionChanged: (item) async { onSelectionChanged: (item) async {
try { try {
await item.first.toDisk(); await item.first.toDisk();
widget.refreshTheme();
} catch (_) {} } catch (_) {}
}, },
), ),

View file

@ -6,7 +6,9 @@ import FlutterMacOS
import Foundation import Foundation
import path_provider_foundation import path_provider_foundation
import shared_preferences_foundation
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
} }

View file

@ -381,6 +381,62 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "6.0.5" version: "6.0.5"
shared_preferences:
dependency: "direct main"
description:
name: shared_preferences
sha256: "78528fd87d0d08ffd3e69551173c026e8eacc7b7079c82eb6a77413957b7e394"
url: "https://pub.dev"
source: hosted
version: "2.0.20"
shared_preferences_android:
dependency: transitive
description:
name: shared_preferences_android
sha256: ad423a80fe7b4e48b50d6111b3ea1027af0e959e49d485712e134863d9c1c521
url: "https://pub.dev"
source: hosted
version: "2.0.17"
shared_preferences_foundation:
dependency: transitive
description:
name: shared_preferences_foundation
sha256: "1e755f8583229f185cfca61b1d80fb2344c9d660e1c69ede5450d8f478fa5310"
url: "https://pub.dev"
source: hosted
version: "2.1.5"
shared_preferences_linux:
dependency: transitive
description:
name: shared_preferences_linux
sha256: "3a59ed10890a8409ad0faad7bb2957dab4b92b8fbe553257b05d30ed8af2c707"
url: "https://pub.dev"
source: hosted
version: "2.1.5"
shared_preferences_platform_interface:
dependency: transitive
description:
name: shared_preferences_platform_interface
sha256: "824bfd02713e37603b2bdade0842e47d56e7db32b1dcdd1cae533fb88e2913fc"
url: "https://pub.dev"
source: hosted
version: "2.1.1"
shared_preferences_web:
dependency: transitive
description:
name: shared_preferences_web
sha256: "0dc2633f215a3d4aa3184c9b2c5766f4711e4e5a6b256e62aafee41f89f1bfb8"
url: "https://pub.dev"
source: hosted
version: "2.0.6"
shared_preferences_windows:
dependency: transitive
description:
name: shared_preferences_windows
sha256: "71bcd669bb9cdb6b39f22c4a7728b6d49e934f6cba73157ffa5a54f1eed67436"
url: "https://pub.dev"
source: hosted
version: "2.1.5"
sky_engine: sky_engine:
dependency: transitive dependency: transitive
description: flutter description: flutter

View file

@ -39,6 +39,7 @@ dependencies:
flutter_localizations: flutter_localizations:
sdk: flutter sdk: flutter
intl: any intl: any
shared_preferences: ^2.0.20
dev_dependencies: dev_dependencies:
flutter_test: flutter_test: