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 'dart:convert';
import 'package:shared_preferences/shared_preferences.dart';
const String wellKnownPath = "/.well-known/outbag/server";
const int defaultPort = 7223;
@ -48,15 +49,32 @@ class OutbagServer {
}
Future<void> toDisk() async {
final db = Localstore.instance;
await db.collection('meta').doc('server').set(toMap());
final SharedPreferences prefs = await SharedPreferences.getInstance();
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 {
final db = Localstore.instance;
final data = await db.collection('meta').doc('server').get();
final SharedPreferences prefs = await SharedPreferences.getInstance();
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:localstore/localstore.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:shared_preferences/shared_preferences.dart';
class AppTheme {
ThemeMode mode;
@ -47,27 +47,14 @@ class AppTheme {
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 {
final db = Localstore.instance;
await db.collection('settings').doc('ui').set({'theme': mode.index});
final SharedPreferences prefs = await SharedPreferences.getInstance();
await prefs.setInt('theme', mode.index);
}
static Future<AppTheme> fromDisk() async {
final db = Localstore.instance;
final doc = await db.collection('settings').doc('ui').get();
try {
final index = doc?['theme'];
final mode = ThemeMode.values[index];
return AppTheme(mode);
} catch (_) {
return AppTheme(ThemeMode.system);
}
final SharedPreferences prefs = await SharedPreferences.getInstance();
return AppTheme(ThemeMode.values[(prefs.getInt('theme')) ?? 0]);
}
@override

View file

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

View file

@ -44,57 +44,41 @@ class _OutbagAppState extends State {
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
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) async {
// wait for user to be authorized
User.listen((_) async {
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;
});
}
loadTheme();
loadUser();
});
}
@ -184,20 +168,17 @@ class _OutbagAppState extends State {
GoRoute(
name: 'signin',
path: 'signin',
builder: (context, state) =>
const AuthPage(mode: Mode.signin),
builder: (context, state) => AuthPage(mode: Mode.signin, refresh: loadUser),
),
GoRoute(
name: 'signup',
path: 'signup',
builder: (context, state) =>
const AuthPage(mode: Mode.signup),
builder: (context, state) => AuthPage(mode: Mode.signup, refresh: loadUser),
),
GoRoute(
name: 'signup-ota',
path: 'signup-ota',
builder: (context, state) =>
const AuthPage(mode: Mode.signupOTA),
builder: (context, state) => AuthPage(mode: Mode.signupOTA, refresh: loadUser),
),
]),
@ -215,8 +196,7 @@ class _OutbagAppState extends State {
GoRoute(
name: 'settings',
path: 'settings',
builder: (context, state) =>
const SettingsPage()),
builder: (context, state) => SettingsPage(refreshTheme: loadTheme)),
GoRoute(
path: 'join-room',
name: 'add-room',
@ -239,26 +219,22 @@ class _OutbagAppState extends State {
GoRoute(
name: 'edit-room',
path: 'edit',
builder: (context, state) =>
EditRoomPage(
state.params['server'] ??
'',
builder: (context, state) => EditRoomPage(
state.params['server'] ?? '',
state.params['id'] ?? '')),
GoRoute(
name: 'room-members',
path: 'members',
builder: (context, state) =>
ManageRoomMembersPage(
state.params['server'] ??
'',
state.params['server'] ?? '',
state.params['id'] ?? '')),
GoRoute(
name: 'room-permissions',
path: 'roles',
builder: (context, state) =>
EditRoomPermissionSetPage(
state.params['server'] ??
'',
state.params['server'] ?? '',
state.params['id'] ?? '')),
])
]),

View file

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

View file

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

View file

@ -6,7 +6,9 @@ import FlutterMacOS
import Foundation
import path_provider_foundation
import shared_preferences_foundation
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
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"
source: hosted
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:
dependency: transitive
description: flutter

View file

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