Add notification requester and settings entries

This commit is contained in:
Nutcake 2023-05-03 23:24:27 +02:00
parent 9ebe4fc63d
commit 22deb64bce
6 changed files with 192 additions and 67 deletions

View file

@ -7,7 +7,7 @@ import 'package:flutter_secure_storage/flutter_secure_storage.dart';
class SettingsClient { class SettingsClient {
static const String _settingsKey = "settings"; static const String _settingsKey = "settings";
static const _storage = FlutterSecureStorage(); static const _storage = FlutterSecureStorage();
Settings _currentSettings = Settings.def(); Settings _currentSettings = const Settings();
Settings get currentSettings => _currentSettings; Settings get currentSettings => _currentSettings;

View file

@ -6,7 +6,6 @@ import 'package:contacts_plus_plus/widgets/login_screen.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:flutter_phoenix/flutter_phoenix.dart'; import 'package:flutter_phoenix/flutter_phoenix.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:logging/logging.dart'; import 'package:logging/logging.dart';
import 'clients/api_client.dart'; import 'clients/api_client.dart';
import 'models/authentication_data.dart'; import 'models/authentication_data.dart';
@ -49,41 +48,6 @@ class _ContactsPlusPlusState extends State<ContactsPlusPlus> {
const FriendsList() : const FriendsList() :
LoginScreen( LoginScreen(
onLoginSuccessful: (AuthenticationData authData) async { onLoginSuccessful: (AuthenticationData authData) async {
final notificationManager = FlutterLocalNotificationsPlugin();
final settings = widget.settingsClient.currentSettings;
final platformNotifications = notificationManager.resolvePlatformSpecificImplementation<AndroidFlutterLocalNotificationsPlugin>();
if (!settings.notificationsDenied && !(await platformNotifications?.areNotificationsEnabled() ?? true)) {
if (context.mounted) {
await showDialog(
context: context,
builder: (context) {
return AlertDialog(
title: Text("This app needs to ask your permission to send background notifications."),
content: Text("Are you okay with that?"),
actions: [
TextButton(
onPressed: () async {
Navigator.of(context).pop();
await widget.settingsClient.changeSettings(settings.copyWith(notificationsDenied: true));
},
child: const Text("No"),
),
TextButton(
onPressed: () async {
Navigator.of(context).pop();
await widget.settingsClient.changeSettings(settings.copyWith(notificationsDenied: false));
await notificationManager.resolvePlatformSpecificImplementation<
AndroidFlutterLocalNotificationsPlugin>()
?.requestPermission();
},
child: const Text("Yes"),
)
],
);
},
);
}
}
if (authData.isAuthenticated) { if (authData.isAuthenticated) {
setState(() { setState(() {
_authData = authData; _authData = authData;

View file

@ -1,24 +1,52 @@
class SettingsEntry<T> {
final T? value;
final T deflt;
class Settings { const SettingsEntry({this.value, required this.deflt});
static const Settings _defaultSettings = Settings(notificationsDenied: true, unreadCheckIntervalMinutes: 0);
final bool notificationsDenied;
final int unreadCheckIntervalMinutes;
const Settings({required this.notificationsDenied, required this.unreadCheckIntervalMinutes}); factory SettingsEntry.fromMap(Map map) {
return SettingsEntry<T>(
factory Settings.def() => _defaultSettings; value: map["value"] as T,
deflt: map["default"],
factory Settings.fromMap(Map map) {
return Settings(
notificationsDenied: map["notificationsDenied"] ?? _defaultSettings.notificationsDenied,
unreadCheckIntervalMinutes: map["unreadCheckIntervalMinutes"] ?? _defaultSettings.unreadCheckIntervalMinutes,
); );
} }
Map toMap() { Map toMap() {
return { return {
"notificationsDenied": notificationsDenied, "value": value.toString(),
"unreadCheckIntervalMinutes": unreadCheckIntervalMinutes, "default": deflt,
};
}
T get valueOrDefault => value ?? deflt;
SettingsEntry<T> withValue({required T newValue}) => SettingsEntry(value: newValue, deflt: deflt);
SettingsEntry<T> passThrough(T? newValue) {
return newValue == null ? this : this.withValue(newValue: newValue);
}
}
class Settings {
final SettingsEntry<bool> notificationsDenied;
final SettingsEntry<int> unreadCheckIntervalMinutes;
const Settings({
this.notificationsDenied = const SettingsEntry(deflt: false),
this.unreadCheckIntervalMinutes = const SettingsEntry(deflt: 60),
});
factory Settings.fromMap(Map map) {
return Settings(
notificationsDenied: SettingsEntry.fromMap(map["notificationsDenied"]),
unreadCheckIntervalMinutes: SettingsEntry.fromMap(map["unreadCheckIntervalMinutes"]),
);
}
Map toMap() {
return {
"notificationsDenied": notificationsDenied.toMap(),
"unreadCheckIntervalMinutes": unreadCheckIntervalMinutes.toMap(),
}; };
} }
@ -26,8 +54,10 @@ class Settings {
Settings copyWith({bool? notificationsDenied, int? unreadCheckIntervalMinutes}) { Settings copyWith({bool? notificationsDenied, int? unreadCheckIntervalMinutes}) {
return Settings( return Settings(
notificationsDenied: notificationsDenied ?? this.notificationsDenied, notificationsDenied: this.notificationsDenied.passThrough(notificationsDenied),
unreadCheckIntervalMinutes: unreadCheckIntervalMinutes ?? this.unreadCheckIntervalMinutes, unreadCheckIntervalMinutes: this.unreadCheckIntervalMinutes.passThrough(unreadCheckIntervalMinutes),
); );
} }
} }

View file

@ -10,6 +10,7 @@ import 'package:contacts_plus_plus/widgets/expanding_input_fab.dart';
import 'package:contacts_plus_plus/widgets/friend_list_tile.dart'; import 'package:contacts_plus_plus/widgets/friend_list_tile.dart';
import 'package:contacts_plus_plus/widgets/settings_page.dart'; import 'package:contacts_plus_plus/widgets/settings_page.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart' as fln;
class FriendsList extends StatefulWidget { class FriendsList extends StatefulWidget {
const FriendsList({super.key}); const FriendsList({super.key});
@ -32,7 +33,7 @@ class _FriendsListState extends State<FriendsList> {
} }
@override @override
void didChangeDependencies() { void didChangeDependencies() async {
super.didChangeDependencies(); super.didChangeDependencies();
final clientHolder = ClientHolder.of(context); final clientHolder = ClientHolder.of(context);
if (_clientHolder != clientHolder) { if (_clientHolder != clientHolder) {
@ -89,7 +90,7 @@ class _FriendsListState extends State<FriendsList> {
RefreshIndicator( RefreshIndicator(
onRefresh: () async { onRefresh: () async {
_refreshFriendsList(); _refreshFriendsList();
await _friendsFuture; await _friendsFuture; // Keep the indicator running until everything's loaded
}, },
child: FutureBuilder( child: FutureBuilder(
future: _friendsFuture, future: _friendsFuture,
@ -133,7 +134,7 @@ class _FriendsListState extends State<FriendsList> {
}, },
); );
} else { } else {
return const SizedBox.shrink(); return const LinearProgressIndicator();
} }
} }
), ),

View file

@ -1,6 +1,7 @@
import 'package:contacts_plus_plus/clients/api_client.dart'; import 'package:contacts_plus_plus/clients/api_client.dart';
import 'package:contacts_plus_plus/models/authentication_data.dart'; import 'package:contacts_plus_plus/models/authentication_data.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
class LoginScreen extends StatefulWidget { class LoginScreen extends StatefulWidget {
const LoginScreen({this.onLoginSuccessful, this.cachedUsername, super.key}); const LoginScreen({this.onLoginSuccessful, this.cachedUsername, super.key});
@ -17,7 +18,7 @@ class _LoginScreenState extends State<LoginScreen> {
final TextEditingController _passwordController = TextEditingController(); final TextEditingController _passwordController = TextEditingController();
late final Future<AuthenticationData> _cachedLoginFuture = ApiClient.tryCachedLogin().then((value) async { late final Future<AuthenticationData> _cachedLoginFuture = ApiClient.tryCachedLogin().then((value) async {
if (value.isAuthenticated) { if (value.isAuthenticated) {
await widget.onLoginSuccessful?.call(value); await loginSuccessful(value);
} }
return value; return value;
}); });
@ -68,7 +69,7 @@ class _LoginScreenState extends State<LoginScreen> {
_error = ""; _error = "";
_isLoading = false; _isLoading = false;
}); });
await widget.onLoginSuccessful?.call(authData); await loginSuccessful(authData);
} catch (e, s) { } catch (e, s) {
FlutterError.reportError(FlutterErrorDetails(exception: e, stack: s)); FlutterError.reportError(FlutterErrorDetails(exception: e, stack: s));
setState(() { setState(() {
@ -78,6 +79,44 @@ class _LoginScreenState extends State<LoginScreen> {
} }
} }
Future<void> loginSuccessful(AuthenticationData authData) async {
final settingsClient = ClientHolder.of(context).settingsClient;
final notificationManager = FlutterLocalNotificationsPlugin();
if (settingsClient.currentSettings.notificationsDenied.value == null) {
if (context.mounted) {
await showDialog(
context: context,
builder: (context) {
return AlertDialog(
title: const Text("This app needs to ask your permission to send background notifications."),
content: const Text("Are you okay with that?"),
actions: [
TextButton(
onPressed: () async {
Navigator.of(context).pop();
await settingsClient.changeSettings(settingsClient.currentSettings.copyWith(notificationsDenied: true));
},
child: const Text("No"),
),
TextButton(
onPressed: () async {
Navigator.of(context).pop();
final requestResult = await notificationManager.resolvePlatformSpecificImplementation<
AndroidFlutterLocalNotificationsPlugin>()
?.requestPermission();
await settingsClient.changeSettings(settingsClient.currentSettings.copyWith(notificationsDenied: requestResult));
},
child: const Text("Yes"),
)
],
);
},
);
}
}
await widget.onLoginSuccessful?.call(authData);
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
@ -129,7 +168,7 @@ class _LoginScreenState extends State<LoginScreen> {
), ),
), ),
if (_isLoading) if (_isLoading)
const LinearProgressIndicator() const CircularProgressIndicator()
else else
TextButton.icon( TextButton.icon(
onPressed: submit, onPressed: submit,

View file

@ -5,8 +5,10 @@ import 'package:url_launcher/url_launcher.dart';
class SettingsPage extends StatelessWidget { class SettingsPage extends StatelessWidget {
const SettingsPage({super.key}); const SettingsPage({super.key});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final sClient = ClientHolder.of(context).settingsClient;
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
leading: IconButton( leading: IconButton(
@ -19,11 +21,39 @@ class SettingsPage extends StatelessWidget {
), ),
body: ListView( body: ListView(
children: [ children: [
const ListSectionHeader(name: "Notifications"),
BooleanSettingsTile(
title: "Send Notifications",
initialState: sClient.currentSettings.notificationsDenied.valueOrDefault,
onChanged: (value) async => await sClient.changeSettings(sClient.currentSettings.copyWith(notificationsDenied: value)),
),
ListTile(
trailing: const Icon(Icons.logout),
title: const Text("Sign out"),
onTap: () {
showDialog(
context: context,
builder: (context) =>
AlertDialog(
title: Text("Are you sure you want to sign out?", style: Theme
.of(context)
.textTheme
.titleLarge,),
actions: [
TextButton(onPressed: () => Navigator.of(context).pop(), child: const Text("No")),
TextButton(
onPressed: () async {
await ClientHolder.of(context).apiClient.logout(context);
},
child: const Text("Yes"),
),
],
),
);
},
),
const ListSectionHeader(name: "Other"),
ListTile( ListTile(
shape: Border(
bottom: BorderSide(color: Theme.of(context).colorScheme.secondaryContainer, width: 0.5),
top: BorderSide(color: Theme.of(context).colorScheme.secondaryContainer, width: 0.5)
),
trailing: const Icon(Icons.logout), trailing: const Icon(Icons.logout),
title: const Text("Sign out"), title: const Text("Sign out"),
onTap: () { onTap: () {
@ -49,10 +79,6 @@ class SettingsPage extends StatelessWidget {
}, },
), ),
ListTile( ListTile(
shape: Border(
bottom: BorderSide(color: Theme.of(context).colorScheme.secondaryContainer, width: 0.5),
top: BorderSide(color: Theme.of(context).colorScheme.secondaryContainer, width: 0.5)
),
trailing: const Icon(Icons.info_outline), trailing: const Icon(Icons.info_outline),
title: const Text("About Contacts++"), title: const Text("About Contacts++"),
onTap: () { onTap: () {
@ -81,4 +107,69 @@ class SettingsPage extends StatelessWidget {
), ),
); );
} }
}
class ListSectionHeader extends StatelessWidget {
const ListSectionHeader({required this.name, super.key});
final String name;
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return Padding(
padding: const EdgeInsets.all(8.0),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text(name, style: theme.textTheme.labelSmall?.copyWith(color: theme.colorScheme.onSurfaceVariant)),
Expanded(
child: Container(
margin: const EdgeInsets.symmetric(horizontal: 8),
color: Colors.white12,
height: 1,
),
)
],
),
);
}
}
class BooleanSettingsTile extends StatefulWidget {
const BooleanSettingsTile({required this.title, required this.initialState, required this.onChanged, super.key});
final String title;
final bool initialState;
final Function(bool) onChanged;
@override
State<StatefulWidget> createState() => _BooleanSettingsTileState();
}
class _BooleanSettingsTileState extends State<BooleanSettingsTile> {
late bool state = widget.initialState;
@override
Widget build(BuildContext context) {
return ListTile(
trailing: Switch(
onChanged: (value) async {
await widget.onChanged(value);
setState(() {
state = value;
});
},
value: state,
),
title: Text(widget.title),
onTap: () async {
await widget.onChanged(!state);
setState(() {
state = !state;
});
},
);
}
} }