Add notification requester and settings entries
This commit is contained in:
parent
9ebe4fc63d
commit
22deb64bce
6 changed files with 192 additions and 67 deletions
|
@ -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;
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
|
@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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: [
|
||||||
ListTile(
|
const ListSectionHeader(name: "Notifications"),
|
||||||
shape: Border(
|
BooleanSettingsTile(
|
||||||
bottom: BorderSide(color: Theme.of(context).colorScheme.secondaryContainer, width: 0.5),
|
title: "Send Notifications",
|
||||||
top: BorderSide(color: Theme.of(context).colorScheme.secondaryContainer, width: 0.5)
|
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(
|
||||||
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: () {
|
||||||
|
@ -82,3 +108,68 @@ 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;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
Loading…
Reference in a new issue