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 {
|
||||
static const String _settingsKey = "settings";
|
||||
static const _storage = FlutterSecureStorage();
|
||||
Settings _currentSettings = Settings.def();
|
||||
Settings _currentSettings = const Settings();
|
||||
|
||||
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_local_notifications/flutter_local_notifications.dart';
|
||||
import 'package:flutter_phoenix/flutter_phoenix.dart';
|
||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'clients/api_client.dart';
|
||||
import 'models/authentication_data.dart';
|
||||
|
@ -49,41 +48,6 @@ class _ContactsPlusPlusState extends State<ContactsPlusPlus> {
|
|||
const FriendsList() :
|
||||
LoginScreen(
|
||||
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) {
|
||||
setState(() {
|
||||
_authData = authData;
|
||||
|
|
|
@ -1,24 +1,52 @@
|
|||
class SettingsEntry<T> {
|
||||
final T? value;
|
||||
final T deflt;
|
||||
|
||||
class Settings {
|
||||
static const Settings _defaultSettings = Settings(notificationsDenied: true, unreadCheckIntervalMinutes: 0);
|
||||
final bool notificationsDenied;
|
||||
final int unreadCheckIntervalMinutes;
|
||||
const SettingsEntry({this.value, required this.deflt});
|
||||
|
||||
const Settings({required this.notificationsDenied, required this.unreadCheckIntervalMinutes});
|
||||
|
||||
factory Settings.def() => _defaultSettings;
|
||||
|
||||
factory Settings.fromMap(Map map) {
|
||||
return Settings(
|
||||
notificationsDenied: map["notificationsDenied"] ?? _defaultSettings.notificationsDenied,
|
||||
unreadCheckIntervalMinutes: map["unreadCheckIntervalMinutes"] ?? _defaultSettings.unreadCheckIntervalMinutes,
|
||||
factory SettingsEntry.fromMap(Map map) {
|
||||
return SettingsEntry<T>(
|
||||
value: map["value"] as T,
|
||||
deflt: map["default"],
|
||||
);
|
||||
}
|
||||
|
||||
Map toMap() {
|
||||
return {
|
||||
"notificationsDenied": notificationsDenied,
|
||||
"unreadCheckIntervalMinutes": unreadCheckIntervalMinutes,
|
||||
"value": value.toString(),
|
||||
"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}) {
|
||||
return Settings(
|
||||
notificationsDenied: notificationsDenied ?? this.notificationsDenied,
|
||||
unreadCheckIntervalMinutes: unreadCheckIntervalMinutes ?? this.unreadCheckIntervalMinutes,
|
||||
notificationsDenied: this.notificationsDenied.passThrough(notificationsDenied),
|
||||
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/settings_page.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_local_notifications/flutter_local_notifications.dart' as fln;
|
||||
|
||||
class FriendsList extends StatefulWidget {
|
||||
const FriendsList({super.key});
|
||||
|
@ -32,7 +33,7 @@ class _FriendsListState extends State<FriendsList> {
|
|||
}
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
void didChangeDependencies() async {
|
||||
super.didChangeDependencies();
|
||||
final clientHolder = ClientHolder.of(context);
|
||||
if (_clientHolder != clientHolder) {
|
||||
|
@ -89,7 +90,7 @@ class _FriendsListState extends State<FriendsList> {
|
|||
RefreshIndicator(
|
||||
onRefresh: () async {
|
||||
_refreshFriendsList();
|
||||
await _friendsFuture;
|
||||
await _friendsFuture; // Keep the indicator running until everything's loaded
|
||||
},
|
||||
child: FutureBuilder(
|
||||
future: _friendsFuture,
|
||||
|
@ -133,7 +134,7 @@ class _FriendsListState extends State<FriendsList> {
|
|||
},
|
||||
);
|
||||
} 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/models/authentication_data.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
|
||||
|
||||
class LoginScreen extends StatefulWidget {
|
||||
const LoginScreen({this.onLoginSuccessful, this.cachedUsername, super.key});
|
||||
|
@ -17,7 +18,7 @@ class _LoginScreenState extends State<LoginScreen> {
|
|||
final TextEditingController _passwordController = TextEditingController();
|
||||
late final Future<AuthenticationData> _cachedLoginFuture = ApiClient.tryCachedLogin().then((value) async {
|
||||
if (value.isAuthenticated) {
|
||||
await widget.onLoginSuccessful?.call(value);
|
||||
await loginSuccessful(value);
|
||||
}
|
||||
return value;
|
||||
});
|
||||
|
@ -68,7 +69,7 @@ class _LoginScreenState extends State<LoginScreen> {
|
|||
_error = "";
|
||||
_isLoading = false;
|
||||
});
|
||||
await widget.onLoginSuccessful?.call(authData);
|
||||
await loginSuccessful(authData);
|
||||
} catch (e, s) {
|
||||
FlutterError.reportError(FlutterErrorDetails(exception: e, stack: s));
|
||||
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
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
|
@ -129,7 +168,7 @@ class _LoginScreenState extends State<LoginScreen> {
|
|||
),
|
||||
),
|
||||
if (_isLoading)
|
||||
const LinearProgressIndicator()
|
||||
const CircularProgressIndicator()
|
||||
else
|
||||
TextButton.icon(
|
||||
onPressed: submit,
|
||||
|
|
|
@ -5,8 +5,10 @@ import 'package:url_launcher/url_launcher.dart';
|
|||
class SettingsPage extends StatelessWidget {
|
||||
const SettingsPage({super.key});
|
||||
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final sClient = ClientHolder.of(context).settingsClient;
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
leading: IconButton(
|
||||
|
@ -19,11 +21,39 @@ class SettingsPage extends StatelessWidget {
|
|||
),
|
||||
body: ListView(
|
||||
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(
|
||||
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),
|
||||
title: const Text("Sign out"),
|
||||
onTap: () {
|
||||
|
@ -49,10 +79,6 @@ class SettingsPage extends StatelessWidget {
|
|||
},
|
||||
),
|
||||
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),
|
||||
title: const Text("About Contacts++"),
|
||||
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;
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
}
|
Loading…
Reference in a new issue