mirror of
https://github.com/KevinMidboe/immich.git
synced 2025-10-29 17:40:28 +00:00
feat(mobile): iOS background sync notifications (#1811)
* adds notification handling logic * notification on background updates for iOS * fixed regression where i accidentally removed load translations from the background sync * fixed ios translations --------- Co-authored-by: Marty Fuhry <marty@fuhry.farm> Co-authored-by: Alex <alex.tran1502@gmail.com>
This commit is contained in:
@@ -0,0 +1,44 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:permission_handler/permission_handler.dart';
|
||||
|
||||
class NotificationPermissionNotifier extends StateNotifier<PermissionStatus> {
|
||||
NotificationPermissionNotifier() :
|
||||
super(Platform.isAndroid
|
||||
? PermissionStatus.granted
|
||||
: PermissionStatus.restricted,
|
||||
) {
|
||||
// Sets the initial state
|
||||
getNotificationPermission().then((p) => state = p);
|
||||
}
|
||||
|
||||
/// Requests the notification permission
|
||||
/// Note: In Android, this is always granted
|
||||
Future<PermissionStatus> requestNotificationPermission() async {
|
||||
final permission = await Permission.notification.request();
|
||||
state = permission;
|
||||
return permission;
|
||||
}
|
||||
|
||||
/// Whether the user has the permission or not
|
||||
/// Note: In Android, this is always true
|
||||
Future<bool> hasNotificationPermission() {
|
||||
return Permission.notification.isGranted;
|
||||
}
|
||||
|
||||
Future<PermissionStatus> getNotificationPermission() async {
|
||||
final status = await Permission.notification.status;
|
||||
state = status;
|
||||
return status;
|
||||
}
|
||||
|
||||
/// Either the permission was granted already or else ask for the permission
|
||||
Future<bool> hasOrAskForNotificationPermission() {
|
||||
return requestNotificationPermission().then((p) => p.isGranted);
|
||||
}
|
||||
|
||||
}
|
||||
final notificationPermissionProvider
|
||||
= StateNotifierProvider<NotificationPermissionNotifier, PermissionStatus>
|
||||
((ref) => NotificationPermissionNotifier());
|
||||
21
mobile/lib/modules/settings/services/permission.service.dart
Normal file
21
mobile/lib/modules/settings/services/permission.service.dart
Normal file
@@ -0,0 +1,21 @@
|
||||
import 'package:permission_handler/permission_handler.dart';
|
||||
|
||||
/// This class is for requesting permissions in the app
|
||||
class PermissionService {
|
||||
/// Requests the notification permission
|
||||
/// Note: In Android, this is always granted
|
||||
Future<PermissionStatus> requestNotificationPermission() {
|
||||
return Permission.notification.request();
|
||||
}
|
||||
|
||||
/// Whether the user has the permission or not
|
||||
/// Note: In Android, this is always true
|
||||
Future<bool> hasNotificationPermission() {
|
||||
return Permission.notification.isGranted;
|
||||
}
|
||||
|
||||
/// Either the permission was granted already or else ask for the permission
|
||||
Future<bool> hasOrAskForNotificationPermission() {
|
||||
return requestNotificationPermission().then((p) => p.isGranted);
|
||||
}
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:immich_mobile/modules/settings/services/app_settings.service.dart';
|
||||
|
||||
SwitchListTile buildSwitchListTile(
|
||||
BuildContext context,
|
||||
AppSettingsService appSettingService,
|
||||
ValueNotifier<bool> valueNotifier,
|
||||
AppSettingsEnum settingsEnum, {
|
||||
required String title,
|
||||
String? subtitle,
|
||||
}) {
|
||||
return SwitchListTile.adaptive(
|
||||
key: Key(settingsEnum.name),
|
||||
value: valueNotifier.value,
|
||||
onChanged: (value) {
|
||||
valueNotifier.value = value;
|
||||
appSettingService.setSetting(settingsEnum, value);
|
||||
},
|
||||
activeColor: Theme.of(context).primaryColor,
|
||||
dense: true,
|
||||
title: Text(title, style: const TextStyle(fontWeight: FontWeight.bold)),
|
||||
subtitle: subtitle != null ? Text(subtitle) : null,
|
||||
);
|
||||
}
|
||||
@@ -4,7 +4,7 @@ import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/modules/settings/providers/app_settings.provider.dart';
|
||||
import 'package:immich_mobile/modules/settings/services/app_settings.service.dart';
|
||||
import 'package:immich_mobile/modules/settings/ui/common.dart';
|
||||
import 'package:immich_mobile/modules/settings/ui/settings_switch_list_tile.dart';
|
||||
|
||||
class ImageViewerQualitySetting extends HookConsumerWidget {
|
||||
const ImageViewerQualitySetting({
|
||||
@@ -44,19 +44,17 @@ class ImageViewerQualitySetting extends HookConsumerWidget {
|
||||
title: const Text('setting_image_viewer_help').tr(),
|
||||
dense: true,
|
||||
),
|
||||
buildSwitchListTile(
|
||||
context,
|
||||
settings,
|
||||
isPreview,
|
||||
AppSettingsEnum.loadPreview,
|
||||
SettingsSwitchListTile(
|
||||
appSettingService: settings,
|
||||
valueNotifier: isPreview,
|
||||
settingsEnum: AppSettingsEnum.loadPreview,
|
||||
title: "setting_image_viewer_preview_title".tr(),
|
||||
subtitle: "setting_image_viewer_preview_subtitle".tr(),
|
||||
),
|
||||
buildSwitchListTile(
|
||||
context,
|
||||
settings,
|
||||
isOriginal,
|
||||
AppSettingsEnum.loadOriginal,
|
||||
SettingsSwitchListTile(
|
||||
appSettingService: settings,
|
||||
valueNotifier: isOriginal,
|
||||
settingsEnum: AppSettingsEnum.loadOriginal,
|
||||
title: "setting_image_viewer_original_title".tr(),
|
||||
subtitle: "setting_image_viewer_original_subtitle".tr(),
|
||||
),
|
||||
|
||||
@@ -3,8 +3,10 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/modules/settings/providers/app_settings.provider.dart';
|
||||
import 'package:immich_mobile/modules/settings/providers/permission.provider.dart';
|
||||
import 'package:immich_mobile/modules/settings/services/app_settings.service.dart';
|
||||
import 'package:immich_mobile/modules/settings/ui/common.dart';
|
||||
import 'package:immich_mobile/modules/settings/ui/settings_switch_list_tile.dart';
|
||||
import 'package:permission_handler/permission_handler.dart';
|
||||
|
||||
class NotificationSetting extends HookConsumerWidget {
|
||||
const NotificationSetting({
|
||||
@@ -14,12 +16,14 @@ class NotificationSetting extends HookConsumerWidget {
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final appSettingService = ref.watch(appSettingsServiceProvider);
|
||||
final permissionService = ref.watch(notificationPermissionProvider);
|
||||
|
||||
final sliderValue = useState(0.0);
|
||||
final totalProgressValue =
|
||||
useState(AppSettingsEnum.backgroundBackupTotalProgress.defaultValue);
|
||||
final singleProgressValue =
|
||||
useState(AppSettingsEnum.backgroundBackupSingleProgress.defaultValue);
|
||||
final hasPermission = permissionService == PermissionStatus.granted;
|
||||
|
||||
useEffect(
|
||||
() {
|
||||
@@ -35,6 +39,30 @@ class NotificationSetting extends HookConsumerWidget {
|
||||
[],
|
||||
);
|
||||
|
||||
// When permissions are permanently denied, you need to go to settings to
|
||||
// allow them
|
||||
showPermissionsDialog() {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
content: const Text('notification_permission_dialog_content').tr(),
|
||||
actions: [
|
||||
TextButton(
|
||||
child: const Text('notification_permission_dialog_cancel').tr(),
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
),
|
||||
TextButton(
|
||||
child: const Text('notification_permission_dialog_settings').tr(),
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
openAppSettings();
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
final String formattedValue = _formatSliderValue(sliderValue.value);
|
||||
return ExpansionTile(
|
||||
textColor: Theme.of(context).primaryColor,
|
||||
@@ -51,23 +79,49 @@ class NotificationSetting extends HookConsumerWidget {
|
||||
),
|
||||
).tr(),
|
||||
children: [
|
||||
buildSwitchListTile(
|
||||
context,
|
||||
appSettingService,
|
||||
totalProgressValue,
|
||||
AppSettingsEnum.backgroundBackupTotalProgress,
|
||||
if (!hasPermission)
|
||||
ListTile(
|
||||
leading: const Icon(Icons.notifications_outlined),
|
||||
title: const Text('notification_permission_list_tile_title').tr(),
|
||||
subtitle: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text('notification_permission_list_tile_content').tr(),
|
||||
const SizedBox(height: 8),
|
||||
ElevatedButton(
|
||||
onPressed: ()
|
||||
=> ref.watch(notificationPermissionProvider.notifier)
|
||||
.requestNotificationPermission().then((permission) {
|
||||
if (permission == PermissionStatus.permanentlyDenied) {
|
||||
showPermissionsDialog();
|
||||
}
|
||||
}),
|
||||
child:
|
||||
const Text('notification_permission_list_tile_enable_button')
|
||||
.tr(),
|
||||
),
|
||||
],
|
||||
),
|
||||
isThreeLine: true,
|
||||
),
|
||||
SettingsSwitchListTile(
|
||||
enabled: hasPermission,
|
||||
appSettingService: appSettingService,
|
||||
valueNotifier: totalProgressValue,
|
||||
settingsEnum: AppSettingsEnum.backgroundBackupTotalProgress,
|
||||
title: 'setting_notifications_total_progress_title'.tr(),
|
||||
subtitle: 'setting_notifications_total_progress_subtitle'.tr(),
|
||||
),
|
||||
buildSwitchListTile(
|
||||
context,
|
||||
appSettingService,
|
||||
singleProgressValue,
|
||||
AppSettingsEnum.backgroundBackupSingleProgress,
|
||||
SettingsSwitchListTile(
|
||||
enabled: hasPermission,
|
||||
appSettingService: appSettingService,
|
||||
valueNotifier: singleProgressValue,
|
||||
settingsEnum: AppSettingsEnum.backgroundBackupSingleProgress,
|
||||
title: 'setting_notifications_single_progress_title'.tr(),
|
||||
subtitle: 'setting_notifications_single_progress_subtitle'.tr(),
|
||||
),
|
||||
ListTile(
|
||||
enabled: hasPermission,
|
||||
isThreeLine: false,
|
||||
dense: true,
|
||||
title: const Text(
|
||||
@@ -76,7 +130,7 @@ class NotificationSetting extends HookConsumerWidget {
|
||||
).tr(args: [formattedValue]),
|
||||
subtitle: Slider(
|
||||
value: sliderValue.value,
|
||||
onChanged: (double v) => sliderValue.value = v,
|
||||
onChanged: !hasPermission ? null : (double v) => sliderValue.value = v,
|
||||
onChangeEnd: (double v) => appSettingService.setSetting(
|
||||
AppSettingsEnum.uploadErrorNotificationGracePeriod,
|
||||
v.toInt(),
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:immich_mobile/modules/settings/services/app_settings.service.dart';
|
||||
|
||||
class SettingsSwitchListTile extends StatelessWidget {
|
||||
final AppSettingsService appSettingService;
|
||||
final ValueNotifier<bool> valueNotifier;
|
||||
final AppSettingsEnum settingsEnum;
|
||||
final String title;
|
||||
final bool enabled;
|
||||
final String? subtitle;
|
||||
|
||||
SettingsSwitchListTile({
|
||||
required this.appSettingService,
|
||||
required this.valueNotifier,
|
||||
required this.settingsEnum,
|
||||
required this.title,
|
||||
this.subtitle,
|
||||
this.enabled = true,
|
||||
}) : super(key: Key(settingsEnum.name));
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SwitchListTile.adaptive(
|
||||
value: valueNotifier.value,
|
||||
onChanged: !enabled ? null : (value) {
|
||||
valueNotifier.value = value;
|
||||
appSettingService.setSetting(settingsEnum, value);
|
||||
},
|
||||
activeColor: Theme
|
||||
.of(context)
|
||||
.primaryColor,
|
||||
dense: true,
|
||||
title: Text(title, style: const TextStyle(fontWeight: FontWeight.bold)),
|
||||
subtitle: subtitle != null ? Text(subtitle!) : null,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,3 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
@@ -41,7 +39,7 @@ class SettingsPage extends HookConsumerWidget {
|
||||
const ImageViewerQualitySetting(),
|
||||
const ThemeSetting(),
|
||||
const AssetListSettings(),
|
||||
if (Platform.isAndroid) const NotificationSetting(),
|
||||
const NotificationSetting(),
|
||||
//const ExperimentalSettings(),
|
||||
],
|
||||
).toList(),
|
||||
|
||||
Reference in New Issue
Block a user