Add settings page and add dark theme

Also use shared preferences for Storage
parent 1a629ebd
......@@ -15,6 +15,7 @@
// You should have received a copy of the GNU Affero General Public License
// along with Pattle. If not, see <https://www.gnu.org/licenses/>.
import 'package:dynamic_theme/dynamic_theme.dart';
import 'package:flutter/material.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:matrix_sdk/matrix_sdk.dart';
......@@ -23,6 +24,7 @@ import 'package:pattle/src/ui/main/chat/chat_page.dart';
import 'package:pattle/src/ui/main/chat/image/image_page.dart';
import 'package:pattle/src/ui/main/chat/settings/chat_settings_page.dart';
import 'package:pattle/src/ui/main/overview/chat_overview_page.dart';
import 'package:pattle/src/ui/main/settings/appearance_page.dart';
import 'package:pattle/src/ui/resources/localizations.dart';
import 'package:pattle/src/ui/resources/theme.dart';
import 'package:pattle/src/ui/start/advanced_page.dart';
......@@ -31,12 +33,21 @@ import 'package:pattle/src/ui/start/phase/key/password_page.dart';
import 'package:pattle/src/ui/start/start_page.dart';
import 'package:pattle/src/ui/main/overview/create/group/create_group_members_page.dart';
import 'ui/main/overview/create/group/create_group_details_page.dart';
import 'ui/main/settings/settings_page.dart';
final routes = {
Routes.root: (Object params) => MaterialPageRoute(
settings: RouteSettings(name: Routes.root),
builder: (context) => InitialPage(),
),
Routes.settings: (Object params) => MaterialPageRoute(
settings: RouteSettings(name: Routes.settings),
builder: (context) => SettingsPage(),
),
Routes.settingsAppearance: (Object params) => MaterialPageRoute(
settings: RouteSettings(name: Routes.settingsAppearance),
builder: (context) => AppearancePage(),
),
Routes.chats: (Object arguments) => MaterialPageRoute(
settings: RouteSettings(name: Routes.chats),
builder: (context) =>
......@@ -79,6 +90,8 @@ class Routes {
Routes._();
static const root = '/';
static const settings = '/settings';
static const settingsAppearance = '/settings/appearance';
static const chats = '/chats';
static const chatsSettings = '/chats/settings';
static const image = '/image';
......@@ -96,21 +109,27 @@ class App extends StatelessWidget {
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
onGenerateTitle: (BuildContext context) => l(context).appName,
localizationsDelegates: [
const AppLocalizationsDelegate(),
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
],
supportedLocales: [
const Locale('en', 'US'),
],
initialRoute: Routes.root,
onGenerateRoute: (settings) {
return routes[settings.name](settings.arguments);
return DynamicTheme(
defaultBrightness: Brightness.light,
data: (brightness) => theme(brightness),
themedWidgetBuilder: (context, theme) {
return MaterialApp(
onGenerateTitle: (BuildContext context) => l(context).appName,
localizationsDelegates: [
const AppLocalizationsDelegate(),
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
],
supportedLocales: [
const Locale('en', 'US'),
],
initialRoute: Routes.root,
onGenerateRoute: (settings) {
return routes[settings.name](settings.arguments);
},
theme: theme,
);
},
theme: lightTheme,
);
}
}
......@@ -60,6 +60,10 @@ class AppBloc {
if (localUser != null) {
di.registerLocalUser(localUser);
} else {
// Delete db if user could not be retrieved from store
await di.getStore().delete();
di.registerStore();
}
final loggedIn = localUser != null;
......
......@@ -15,55 +15,30 @@
// You should have received a copy of the GNU Affero General Public License
// along with Pattle. If not, see <https://www.gnu.org/licenses/>.
import 'package:sqflite/sqflite.dart';
import 'package:shared_preferences/shared_preferences.dart';
class Storage {
static const _preferencesTable = 'preferences';
static const _keyColumn = 'key';
static const _valueColumn = 'value';
final SharedPreferences prefs;
final Database db;
Storage(this.db);
Storage(this.prefs);
static Future<Storage> open() async {
final db = await openDatabase(
'pattle.sqlite',
version: 1,
onCreate: (db, version) async {
await db.execute(
'''
CREATE TABLE IF NOT EXISTS $_preferencesTable (
$_keyColumn TEXT PRIMARY KEY,
$_valueColumn TEXT
);
''',
);
},
);
return Storage(db);
return Storage(await SharedPreferences.getInstance());
}
Future<dynamic> operator [](String key) async {
final results = await db.query(
_preferencesTable,
where: '$_keyColumn = ?',
whereArgs: [key],
);
dynamic operator [](String key) async => prefs.get(key);
if (results.isNotEmpty) {
return results.first[_valueColumn];
void operator []=(String key, dynamic value) {
if (value is String) {
prefs.setString(key, value);
} else if (value is bool) {
prefs.setBool(key, value);
} else if (value is int) {
prefs.setInt(key, value);
} else if (value is double) {
prefs.setDouble(key, value);
} else {
return null;
throw TypeError();
}
}
void operator []=(String key, dynamic value) {
db.insert(
_preferencesTable,
{_keyColumn: key, _valueColumn: value},
conflictAlgorithm: ConflictAlgorithm.replace,
);
}
}
......@@ -31,6 +31,7 @@ import 'package:pattle/src/ui/main/widgets/error.dart';
import 'package:pattle/src/ui/main/widgets/title_with_sub.dart';
import 'package:pattle/src/ui/resources/localizations.dart';
import 'package:pattle/src/ui/resources/theme.dart';
import 'package:pattle/src/ui/util/color.dart';
import 'package:pattle/src/ui/util/future_or_builder.dart';
import 'package:pattle/src/ui/util/matrix_image.dart';
......@@ -128,7 +129,7 @@ class ChatPageState extends State<ChatPage> {
: ChatName(room: room);
return Scaffold(
backgroundColor: LightColors.red[50],
backgroundColor: chatBackgroundColor(context),
appBar: AppBar(
titleSpacing: 0,
title: settingsGestureDetector(
......@@ -171,14 +172,20 @@ class ChatPageState extends State<ChatPage> {
if (bloc.room is JoinedRoom) {
return Material(
elevation: elevation,
color: LightColors.red[50],
color: chatBackgroundColor(context),
// On dark theme, draw a divider line because the shadow is gone
shape: Theme.of(context).brightness == Brightness.dark
? Border(top: BorderSide(color: Colors.grey[800]))
: null,
child: Padding(
padding: EdgeInsets.symmetric(vertical: 8, horizontal: 16),
child: Material(
elevation: elevation,
borderRadius: BorderRadius.only(
topLeft: Radius.circular(8), topRight: Radius.circular(8)),
color: Colors.white,
topLeft: Radius.circular(8),
topRight: Radius.circular(8),
),
color: themed(context, light: Colors.white, dark: Colors.grey[800]),
child: TextField(
controller: textController,
textInputAction: TextInputAction.newline,
......
......@@ -54,13 +54,13 @@ class LoadingBubble extends MessageBubble {
class LoadingBubbleState extends MessageBubbleState<LoadingBubble> {
@override
Color mineColor() => Colors.grey[350];
Color mineColor(BuildContext context) => Colors.grey[350];
@protected
Widget buildMine(BuildContext context) => buildContent(context);
@override
Color theirsColor() => Colors.grey[350];
Color theirsColor(BuildContext context) => Colors.grey[350];
@protected
Widget buildTheirs(BuildContext context) => buildContent(context);
......
......@@ -20,6 +20,7 @@ import 'package:flutter/material.dart';
import 'package:matrix_sdk/matrix_sdk.dart';
import 'package:pattle/src/ui/main/models/chat_item.dart';
import 'package:pattle/src/ui/resources/theme.dart';
import 'package:pattle/src/ui/util/color.dart';
import 'package:pattle/src/ui/util/date_format.dart';
import 'package:pattle/src/ui/util/user.dart';
......@@ -297,7 +298,11 @@ abstract class MessageBubbleState<T extends MessageBubble>
return bottom;
}
Color mineColor() => LightColors.red[450];
Color mineColor(BuildContext context) => themed(
context,
light: LightColors.red[450],
dark: LightColors.red[700],
);
Widget _buildMine(BuildContext context) => Column(
crossAxisAlignment: CrossAxisAlignment.end,
......@@ -317,7 +322,7 @@ abstract class MessageBubbleState<T extends MessageBubble>
)
: EdgeInsets.only(),
child: Material(
color: mineColor(),
color: mineColor(context),
elevation: 1,
shape: border(),
child: buildMine(context),
......@@ -332,7 +337,11 @@ abstract class MessageBubbleState<T extends MessageBubble>
@protected
Widget buildMine(BuildContext context);
Color theirsColor() => Colors.white;
Color theirsColor(BuildContext context) => themed(
context,
light: Colors.white,
dark: Colors.grey[800],
);
Widget _buildTheirs(BuildContext context) => Column(
crossAxisAlignment: CrossAxisAlignment.start,
......@@ -352,7 +361,7 @@ abstract class MessageBubbleState<T extends MessageBubble>
)
: EdgeInsets.only(),
child: Material(
color: theirsColor(),
color: theirsColor(context),
elevation: 1,
shape: border(),
child: buildTheirs(context),
......
......@@ -20,6 +20,7 @@ import 'package:matrix_sdk/matrix_sdk.dart';
import 'package:pattle/src/ui/main/models/chat_item.dart';
import 'package:pattle/src/di.dart' as di;
import 'package:pattle/src/ui/main/widgets/redacted.dart';
import 'package:pattle/src/ui/util/color.dart';
import 'bubble.dart';
import 'message_bubble.dart';
......@@ -51,7 +52,11 @@ class RedactedBubbleState extends MessageBubbleState<RedactedBubble> {
@protected
Widget buildContent(BuildContext context) => Redacted(
event: widget.event,
color: widget.isMine ? Colors.grey[300] : Colors.grey[700],
color: themed(
context,
light: widget.isMine ? Colors.grey[300] : Colors.grey[700],
dark: widget.isMine ? Colors.white30 : Colors.white70,
),
textStyle: textStyle(context),
);
......
......@@ -19,6 +19,7 @@
import 'package:flutter/material.dart';
import 'package:pattle/src/ui/main/models/chat_item.dart';
import 'package:pattle/src/ui/resources/theme.dart';
import 'package:pattle/src/ui/util/color.dart';
import 'package:pattle/src/ui/util/date_format.dart';
import '../bubble.dart';
......@@ -85,7 +86,11 @@ abstract class StateBubbleState<T extends StateBubble> extends ItemState<T> {
),
child: Material(
elevation: 1,
color: LightColors.red[100],
color: themed(
context,
light: LightColors.red[100],
dark: LightColors.red[700],
),
borderRadius: StateBubble.borderRadius,
child: InkWell(
customBorder: RoundedRectangleBorder(
......
......@@ -18,6 +18,7 @@
import 'package:flutter/material.dart';
import 'package:matrix_sdk/matrix_sdk.dart';
import 'package:pattle/src/ui/main/models/chat_item.dart';
import 'package:pattle/src/ui/util/color.dart';
import 'package:pattle/src/ui/util/future_or_builder.dart';
import 'package:pattle/src/ui/util/user.dart';
import 'package:url_launcher/url_launcher.dart';
......@@ -63,7 +64,11 @@ class TextBubbleState extends MessageBubbleState<TextBubble> {
);
} else {
return textStyle(context).copyWith(
color: Theme.of(context).primaryColor,
color: themed(
context,
light: Theme.of(context).primaryColor,
dark: Colors.white,
),
decoration: TextDecoration.underline,
);
}
......
......@@ -53,6 +53,12 @@ class ChatOverviewPageState extends State<ChatOverviewPage> {
return Scaffold(
appBar: AppBar(
title: Text(l(context).appName),
actions: <Widget>[
IconButton(
icon: Icon(Icons.settings),
onPressed: () => Navigator.pushNamed(context, Routes.settings),
)
],
),
body: Column(
children: <Widget>[
......@@ -155,7 +161,7 @@ class ChatOverviewPageState extends State<ChatOverviewPage> {
time,
style: Theme.of(context).textTheme.subtitle.copyWith(
fontWeight: FontWeight.normal,
color: Colors.black54,
color: Theme.of(context).textTheme.caption.color,
),
),
],
......
......@@ -38,7 +38,7 @@ class TypingSubtitle extends Subtitle {
overflow: TextOverflow.ellipsis,
text: TextSpan(
style: textStyle(context).copyWith(
color: LightColors.red,
color: redOnBackground(context),
fontWeight: FontWeight.bold,
),
children: typingSpan(context, room),
......
// Copyright (C) 2019 Wilko Manger
//
// This file is part of Pattle.
//
// Pattle is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Pattle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with Pattle. If not, see <https://www.gnu.org/licenses/>.
import 'package:dynamic_theme/dynamic_theme.dart';
import 'package:flutter/material.dart';
import 'package:pattle/src/ui/main/settings/settings_bloc.dart';
import 'package:pattle/src/ui/resources/theme.dart';
class AppearancePageState extends State<AppearancePage> {
final bloc = SettingsBloc();
Brightness brightness;
@override
void initState() {
super.initState();
}
@override
void dispose() {
super.dispose();
}
@override
Widget build(BuildContext context) {
brightness = Theme.of(context).brightness;
return Scaffold(
appBar: AppBar(
title: Text('Appearance'),
),
body: ListView(
children: <Widget>[
Padding(
padding: EdgeInsets.only(left: 16, top: 16, bottom: 8),
child: Row(
children: <Widget>[
Icon(
brightness == Brightness.light
? Icons.brightness_high
: Icons.brightness_3,
color: redOnBackground(context),
),
SizedBox(width: 8),
Text(
'Brightness',
style: TextStyle(
color: redOnBackground(context),
fontWeight: FontWeight.bold,
),
),
],
)),
RadioListTile(
groupValue: brightness,
value: Brightness.light,
onChanged: (brightness) {
DynamicTheme.of(context).setBrightness(brightness);
},
title: Text('Light'),
),
RadioListTile(
groupValue: brightness,
value: Brightness.dark,
onChanged: (brightness) {
DynamicTheme.of(context).setBrightness(brightness);
},
title: Text('Dark'),
),
Divider(height: 1)
],
),
);
}
}
class AppearancePage extends StatefulWidget {
@override
State<StatefulWidget> createState() => AppearancePageState();
}
// Copyright (C) 2019 Wilko Manger
//
// This file is part of Pattle.
//
// Pattle is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Pattle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with Pattle. If not, see <https://www.gnu.org/licenses/>.
import 'package:dynamic_theme/dynamic_theme.dart';
import 'package:flutter/material.dart';
import 'package:pattle/src/ui/main/settings/settings_bloc.dart';
import 'package:pattle/src/ui/main/widgets/user_avatar.dart';
import 'package:pattle/src/ui/resources/theme.dart';
class ProfilePageState extends State<ProfilePage> {
final bloc = SettingsBloc();
Brightness brightness;
@override
void initState() {
super.initState();
}
@override
void dispose() {
super.dispose();
}
@override
Widget build(BuildContext context) {
brightness = Theme.of(context).brightness;
return Scaffold(
appBar: AppBar(
title: Text('Profile'),
),
body: Column(
children: <Widget>[
UserAvatar(
user: bloc.me,
radius: 48,
)
],
),
);
}
}
class ProfilePage extends StatefulWidget {
@override
State<StatefulWidget> createState() => ProfilePageState();
}
// Copyright (C) 2019 Wilko Manger
//
// This file is part of Pattle.
//
// Pattle is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Pattle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with Pattle. If not, see <https://www.gnu.org/licenses/>.
import 'package:pattle/src/di.dart' as di;
import '../../bloc.dart';
class SettingsBloc extends Bloc {
final me = di.getLocalUser();
static SettingsBloc _instance = SettingsBloc._();
SettingsBloc._();
factory SettingsBloc() => _instance;
void useDarkTheme() {}
}
// Copyright (C) 2019 Wilko Manger
//
// This file is part of Pattle.
//
// Pattle is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Pattle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with Pattle. If not, see <https://www.gnu.org/licenses/>.
import 'package:flutter/material.dart';
import 'package:pattle/src/ui/main/settings/settings_bloc.dart';
import 'package:pattle/src/ui/main/widgets/user_avatar.dart';
import 'package:pattle/src/ui/resources/theme.dart';
import 'package:pattle/src/ui/util/user.dart';
import '../../../app.dart';
class SettingsPageState extends State<SettingsPage> {
final bloc = SettingsBloc();
@override
void initState() {
super.initState();
}
@override
void dispose() {
super.dispose();
}
@override
Widget build(BuildContext context) {
print(bloc.me.avatarUrl);
return Scaffold(
appBar: AppBar(
title: Text('Settings'),
),
body: ListView(
children: <Widget>[
Material(
child: InkWell(
onTap: () {},
child: Padding(
padding: EdgeInsets.all(16),
child: Row(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
UserAvatar(
user: bloc.me,
radius: 36,
),
Padding(
padding: EdgeInsets.only(left: 16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text(
displayNameOf(bloc.me),
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w600,
),
),
Text(bloc.me.id.toString())
],
),
)
],
),
),
),
),
Divider(height: 1),
ListTile(
leading: Icon(Icons.vpn_key, color: LightColors.red),
title: Text('Account'),
subtitle: Text('Privacy, security, change password'),
),
ListTile(
leading: Icon(Icons.landscape, color: LightColors.red),
title: Text('Appearance'),
subtitle: Text('Theme, font size'),
onTap: () =>
Navigator.of(context).pushNamed(Routes.settingsAppearance),
)
],
),
);
}
}
class SettingsPage extends StatefulWidget {
@override
State<StatefulWidget> createState() => SettingsPageState();
}
......@@ -24,7 +24,7 @@ class UserAvatar extends StatelessWidget {
final User user;
final double radius;
UserAvatar({this.user, this.radius});
UserAvatar({@required this.user, this.radius});
@override
Widget build(BuildContext context) {
......
......@@ -16,19 +16,42 @@
// along with Pattle. If not, see <https://www.gnu.org/licenses/>.
import 'package:flutter/material.dart';
import 'package:pattle/src/ui/util/color.dart';
final ThemeData lightTheme = ThemeData(
primarySwatch: LightColors.red,
brightness: Brightness.light,
primaryColorBrightness: Brightness.dark,
accentColorBrightness: Brightness.dark,
cursorColor: LightColors.red,
buttonTheme: ButtonThemeData(
buttonColor: LightColors.red[500],
textTheme: ButtonTextTheme.primary,
),
textTheme: Typography.blackMountainView,
);
ThemeData theme(Brightness brightness) {
return ThemeData(
primarySwatch: LightColors.red,
primaryColorDark: LightColors.red[700],
accentColor: LightColors.red,
brightness: brightness,
primaryColorBrightness: Brightness.dark,
accentColorBrightness: Brightness.dark,
cursorColor: LightColors.red,
buttonTheme: ButtonThemeData(
buttonColor: LightColors.red[500],
textTheme: ButtonTextTheme.primary,
),
appBarTheme: AppBarTheme(
color: LightColors.red,
),
);
}
Color chatBackgroundColor(BuildContext context) {
return themed(
context,
light: LightColors.red[50],
dark: Colors.grey[900],
);
}
Color redOnBackground(BuildContext context) {
return themed(
context,
light: LightColors.red[500],