mirror of
https://github.com/immich-app/immich.git
synced 2025-05-31 20:25:32 -04: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:
parent
2d2cfb0349
commit
e9c9b7a3e2
@ -214,5 +214,11 @@
|
|||||||
"version_announcement_overlay_text_1": "Hi friend, there is a new release of",
|
"version_announcement_overlay_text_1": "Hi friend, there is a new release of",
|
||||||
"version_announcement_overlay_text_2": "please take your time to visit the ",
|
"version_announcement_overlay_text_2": "please take your time to visit the ",
|
||||||
"version_announcement_overlay_text_3": " and ensure your docker-compose and .env setup is up-to-date to prevent any misconfigurations, especially if you use WatchTower or any mechanism that handles updating your server application automatically.",
|
"version_announcement_overlay_text_3": " and ensure your docker-compose and .env setup is up-to-date to prevent any misconfigurations, especially if you use WatchTower or any mechanism that handles updating your server application automatically.",
|
||||||
"version_announcement_overlay_title": "New Server Version Available \uD83C\uDF89"
|
"version_announcement_overlay_title": "New Server Version Available \uD83C\uDF89",
|
||||||
|
"notification_permission_dialog_content": "To enable notifications, go to Settings and select allow.",
|
||||||
|
"notification_permission_dialog_cancel": "Cancel",
|
||||||
|
"notification_permission_dialog_settings": "Settings",
|
||||||
|
"notification_permission_list_tile_title": "Notification Permission",
|
||||||
|
"notification_permission_list_tile_content": "Grant permission to enable notifications.",
|
||||||
|
"notification_permission_list_tile_enable_button": "Enable Notifications"
|
||||||
}
|
}
|
@ -37,5 +37,66 @@ end
|
|||||||
post_install do |installer|
|
post_install do |installer|
|
||||||
installer.pods_project.targets.each do |target|
|
installer.pods_project.targets.each do |target|
|
||||||
flutter_additional_ios_build_settings(target)
|
flutter_additional_ios_build_settings(target)
|
||||||
|
|
||||||
|
# Permission macros for the permission_handler (https://pub.dev/packages/permission_handler)
|
||||||
|
# Start of the permission_handler configuration
|
||||||
|
# Remove the # character in front of the permission you do want to use.
|
||||||
|
target.build_configurations.each do |config|
|
||||||
|
|
||||||
|
# You can enable the permissions needed here. For example to enable camera
|
||||||
|
# permission, just remove the `#` character in front so it looks like this:
|
||||||
|
#
|
||||||
|
# ## dart: PermissionGroup.camera
|
||||||
|
# 'PERMISSION_CAMERA=1'
|
||||||
|
#
|
||||||
|
# Preprocessor definitions can be found in: https://github.com/Baseflow/flutter-permission-handler/blob/master/permission_handler_apple/ios/Classes/PermissionHandlerEnums.h
|
||||||
|
config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] ||= [
|
||||||
|
'$(inherited)',
|
||||||
|
|
||||||
|
## dart: PermissionGroup.calendar
|
||||||
|
# 'PERMISSION_EVENTS=1',
|
||||||
|
|
||||||
|
## dart: PermissionGroup.reminders
|
||||||
|
# 'PERMISSION_REMINDERS=1',
|
||||||
|
|
||||||
|
## dart: PermissionGroup.contacts
|
||||||
|
# 'PERMISSION_CONTACTS=1',
|
||||||
|
|
||||||
|
## dart: PermissionGroup.camera
|
||||||
|
# 'PERMISSION_CAMERA=1',
|
||||||
|
|
||||||
|
## dart: PermissionGroup.microphone
|
||||||
|
# 'PERMISSION_MICROPHONE=1',
|
||||||
|
|
||||||
|
## dart: PermissionGroup.speech
|
||||||
|
# 'PERMISSION_SPEECH_RECOGNIZER=1',
|
||||||
|
|
||||||
|
## dart: PermissionGroup.photos
|
||||||
|
# 'PERMISSION_PHOTOS=1',
|
||||||
|
|
||||||
|
## dart: [PermissionGroup.location, PermissionGroup.locationAlways, PermissionGroup.locationWhenInUse]
|
||||||
|
# 'PERMISSION_LOCATION=1',
|
||||||
|
|
||||||
|
## dart: PermissionGroup.notification
|
||||||
|
'PERMISSION_NOTIFICATIONS=1',
|
||||||
|
|
||||||
|
## dart: PermissionGroup.mediaLibrary
|
||||||
|
# 'PERMISSION_MEDIA_LIBRARY=1',
|
||||||
|
|
||||||
|
## dart: PermissionGroup.sensors
|
||||||
|
# 'PERMISSION_SENSORS=1',
|
||||||
|
|
||||||
|
## dart: PermissionGroup.bluetooth
|
||||||
|
# 'PERMISSION_BLUETOOTH=1',
|
||||||
|
|
||||||
|
## dart: PermissionGroup.appTrackingTransparency
|
||||||
|
# 'PERMISSION_APP_TRACKING_TRANSPARENCY=1',
|
||||||
|
|
||||||
|
## dart: PermissionGroup.criticalAlerts
|
||||||
|
# 'PERMISSION_CRITICAL_ALERTS=1'
|
||||||
|
]
|
||||||
|
|
||||||
|
end
|
||||||
|
# End of the permission_handler configuration
|
||||||
end
|
end
|
||||||
end
|
end
|
@ -26,6 +26,8 @@ PODS:
|
|||||||
- FlutterMacOS
|
- FlutterMacOS
|
||||||
- path_provider_ios (0.0.1):
|
- path_provider_ios (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
|
- permission_handler_apple (9.0.4):
|
||||||
|
- Flutter
|
||||||
- photo_manager (2.0.0):
|
- photo_manager (2.0.0):
|
||||||
- Flutter
|
- Flutter
|
||||||
- FlutterMacOS
|
- FlutterMacOS
|
||||||
@ -58,6 +60,7 @@ DEPENDENCIES:
|
|||||||
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
|
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
|
||||||
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/ios`)
|
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/ios`)
|
||||||
- path_provider_ios (from `.symlinks/plugins/path_provider_ios/ios`)
|
- path_provider_ios (from `.symlinks/plugins/path_provider_ios/ios`)
|
||||||
|
- permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`)
|
||||||
- photo_manager (from `.symlinks/plugins/photo_manager/ios`)
|
- photo_manager (from `.symlinks/plugins/photo_manager/ios`)
|
||||||
- share_plus (from `.symlinks/plugins/share_plus/ios`)
|
- share_plus (from `.symlinks/plugins/share_plus/ios`)
|
||||||
- shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/ios`)
|
- shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/ios`)
|
||||||
@ -95,6 +98,8 @@ EXTERNAL SOURCES:
|
|||||||
:path: ".symlinks/plugins/path_provider_foundation/ios"
|
:path: ".symlinks/plugins/path_provider_foundation/ios"
|
||||||
path_provider_ios:
|
path_provider_ios:
|
||||||
:path: ".symlinks/plugins/path_provider_ios/ios"
|
:path: ".symlinks/plugins/path_provider_ios/ios"
|
||||||
|
permission_handler_apple:
|
||||||
|
:path: ".symlinks/plugins/permission_handler_apple/ios"
|
||||||
photo_manager:
|
photo_manager:
|
||||||
:path: ".symlinks/plugins/photo_manager/ios"
|
:path: ".symlinks/plugins/photo_manager/ios"
|
||||||
share_plus:
|
share_plus:
|
||||||
@ -123,6 +128,7 @@ SPEC CHECKSUMS:
|
|||||||
package_info_plus: 6c92f08e1f853dc01228d6f553146438dafcd14e
|
package_info_plus: 6c92f08e1f853dc01228d6f553146438dafcd14e
|
||||||
path_provider_foundation: 37748e03f12783f9de2cb2c4eadfaa25fe6d4852
|
path_provider_foundation: 37748e03f12783f9de2cb2c4eadfaa25fe6d4852
|
||||||
path_provider_ios: 14f3d2fd28c4fdb42f44e0f751d12861c43cee02
|
path_provider_ios: 14f3d2fd28c4fdb42f44e0f751d12861c43cee02
|
||||||
|
permission_handler_apple: 44366e37eaf29454a1e7b1b7d736c2cceaeb17ce
|
||||||
photo_manager: 4f6810b7dfc4feb03b461ac1a70dacf91fba7604
|
photo_manager: 4f6810b7dfc4feb03b461ac1a70dacf91fba7604
|
||||||
SAMKeychain: 483e1c9f32984d50ca961e26818a534283b4cd5c
|
SAMKeychain: 483e1c9f32984d50ca961e26818a534283b4cd5c
|
||||||
share_plus: 056a1e8ac890df3e33cb503afffaf1e9b4fbae68
|
share_plus: 056a1e8ac890df3e33cb503afffaf1e9b4fbae68
|
||||||
@ -133,6 +139,6 @@ SPEC CHECKSUMS:
|
|||||||
video_player_avfoundation: e489aac24ef5cf7af82702979ed16f2a5ef84cff
|
video_player_avfoundation: e489aac24ef5cf7af82702979ed16f2a5ef84cff
|
||||||
wakelock: d0fc7c864128eac40eba1617cb5264d9c940b46f
|
wakelock: d0fc7c864128eac40eba1617cb5264d9c940b46f
|
||||||
|
|
||||||
PODFILE CHECKSUM: c798208781ca5116c4a3d5927d689946791f0189
|
PODFILE CHECKSUM: 4a7e0475ea85ab7bf89955bc4c7ea9d18b54dfd8
|
||||||
|
|
||||||
COCOAPODS: 1.11.3
|
COCOAPODS: 1.11.3
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import UIKit
|
import UIKit
|
||||||
|
import shared_preferences_foundation
|
||||||
import Flutter
|
import Flutter
|
||||||
import BackgroundTasks
|
import BackgroundTasks
|
||||||
import path_provider_ios
|
import path_provider_ios
|
||||||
@ -25,6 +26,10 @@ import photo_manager
|
|||||||
if !registry.hasPlugin("org.cocoapods.photo-manager") {
|
if !registry.hasPlugin("org.cocoapods.photo-manager") {
|
||||||
PhotoManagerPlugin.register(with: registry.registrar(forPlugin: "org.cocoapods.photo-manager")!)
|
PhotoManagerPlugin.register(with: registry.registrar(forPlugin: "org.cocoapods.photo-manager")!)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !registry.hasPlugin("org.cocoapods.shared-preferences-foundation") {
|
||||||
|
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "org.cocoapods.shared-preferences-foundation")!)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
|
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
|
||||||
|
@ -25,6 +25,7 @@ class BackgroundSyncWorker {
|
|||||||
name: "BackgroundImmich"
|
name: "BackgroundImmich"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
let notificationId = "com.alextran.immich/backgroundNotifications"
|
||||||
// The background message passing channel
|
// The background message passing channel
|
||||||
var channel: FlutterMethodChannel?
|
var channel: FlutterMethodChannel?
|
||||||
|
|
||||||
@ -67,15 +68,15 @@ class BackgroundSyncWorker {
|
|||||||
})
|
})
|
||||||
break
|
break
|
||||||
case "updateNotification":
|
case "updateNotification":
|
||||||
// TODO: implement update notification
|
let handled = self.handleNotification(call)
|
||||||
result(true)
|
result(handled)
|
||||||
break
|
break
|
||||||
case "showError":
|
case "showError":
|
||||||
// TODO: implement show error
|
let handled = self.handleError(call)
|
||||||
result(true)
|
result(handled)
|
||||||
break
|
break
|
||||||
case "clearErrorNotifications":
|
case "clearErrorNotifications":
|
||||||
// TODO: implement clear error notifications
|
self.handleClearErrorNotifications()
|
||||||
result(true)
|
result(true)
|
||||||
break
|
break
|
||||||
case "hasContentChanged":
|
case "hasContentChanged":
|
||||||
@ -184,5 +185,87 @@ class BackgroundSyncWorker {
|
|||||||
channel = nil
|
channel = nil
|
||||||
completionHandler(fetchResult)
|
completionHandler(fetchResult)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func handleNotification(_ call: FlutterMethodCall) -> Bool {
|
||||||
|
|
||||||
|
// Parse the arguments as an array list
|
||||||
|
guard let args = call.arguments as? Array<Any> else {
|
||||||
|
print("Failed to parse \(call.arguments) as array")
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Requires 7 arguments passed or else fail
|
||||||
|
guard args.count == 7 else {
|
||||||
|
print("Needs 7 arguments, but was only passed \(args.count)")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse the arguments to send the notification update
|
||||||
|
let title = args[0] as? String
|
||||||
|
let content = args[1] as? String
|
||||||
|
let progress = args[2] as? Int
|
||||||
|
let maximum = args[3] as? Int
|
||||||
|
let indeterminate = args[4] as? Bool
|
||||||
|
let isDetail = args[5] as? Bool
|
||||||
|
let onlyIfForeground = args[6] as? Bool
|
||||||
|
|
||||||
|
// Build the notification
|
||||||
|
let notificationContent = UNMutableNotificationContent()
|
||||||
|
notificationContent.body = content ?? "Uploading..."
|
||||||
|
notificationContent.title = title ?? "Immich"
|
||||||
|
|
||||||
|
// Add it to the Notification center
|
||||||
|
let notification = UNNotificationRequest(
|
||||||
|
identifier: notificationId,
|
||||||
|
content: notificationContent,
|
||||||
|
trigger: nil
|
||||||
|
)
|
||||||
|
let center = UNUserNotificationCenter.current()
|
||||||
|
center.add(notification) { (error: Error?) in
|
||||||
|
if let theError = error {
|
||||||
|
print("Error showing notifications: \(theError)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
private func handleError(_ call: FlutterMethodCall) -> Bool {
|
||||||
|
// Parse the arguments as an array list
|
||||||
|
guard let args = call.arguments as? Array<Any> else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Requires 7 arguments passed or else fail
|
||||||
|
guard args.count == 3 else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
let title = args[0] as? String
|
||||||
|
let content = args[1] as? String
|
||||||
|
let individualTag = args[2] as? String
|
||||||
|
|
||||||
|
// Build the notification
|
||||||
|
let notificationContent = UNMutableNotificationContent()
|
||||||
|
notificationContent.body = content ?? "Error running the backup job."
|
||||||
|
notificationContent.title = title ?? "Immich"
|
||||||
|
|
||||||
|
// Add it to the Notification center
|
||||||
|
let notification = UNNotificationRequest(
|
||||||
|
identifier: notificationId,
|
||||||
|
content: notificationContent,
|
||||||
|
trigger: nil
|
||||||
|
)
|
||||||
|
let center = UNUserNotificationCenter.current()
|
||||||
|
center.add(notification)
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
private func handleClearErrorNotifications() {
|
||||||
|
let center = UNUserNotificationCenter.current()
|
||||||
|
center.removeDeliveredNotifications(withIdentifiers: [notificationId])
|
||||||
|
center.removePendingNotificationRequests(withIdentifiers: [notificationId])
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -14,6 +14,7 @@ import 'package:immich_mobile/modules/backup/models/hive_duplicated_assets.model
|
|||||||
import 'package:immich_mobile/modules/backup/providers/backup.provider.dart';
|
import 'package:immich_mobile/modules/backup/providers/backup.provider.dart';
|
||||||
import 'package:immich_mobile/modules/login/models/hive_saved_login_info.model.dart';
|
import 'package:immich_mobile/modules/login/models/hive_saved_login_info.model.dart';
|
||||||
import 'package:immich_mobile/modules/login/providers/authentication.provider.dart';
|
import 'package:immich_mobile/modules/login/providers/authentication.provider.dart';
|
||||||
|
import 'package:immich_mobile/modules/settings/providers/permission.provider.dart';
|
||||||
import 'package:immich_mobile/routing/router.dart';
|
import 'package:immich_mobile/routing/router.dart';
|
||||||
import 'package:immich_mobile/routing/tab_navigation_observer.dart';
|
import 'package:immich_mobile/routing/tab_navigation_observer.dart';
|
||||||
import 'package:immich_mobile/shared/models/immich_logger_message.model.dart';
|
import 'package:immich_mobile/shared/models/immich_logger_message.model.dart';
|
||||||
@ -126,6 +127,9 @@ class ImmichAppState extends ConsumerState<ImmichApp>
|
|||||||
|
|
||||||
ref.watch(releaseInfoProvider.notifier).checkGithubReleaseInfo();
|
ref.watch(releaseInfoProvider.notifier).checkGithubReleaseInfo();
|
||||||
|
|
||||||
|
ref.watch(notificationPermissionProvider.notifier)
|
||||||
|
.getNotificationPermission();
|
||||||
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case AppLifecycleState.inactive:
|
case AppLifecycleState.inactive:
|
||||||
|
@ -314,10 +314,9 @@ class BackgroundService {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Notifications aren't enabled in iOS yet, and this line
|
final translationsOk = await loadTranslations();
|
||||||
// below crashes the iOS background service
|
if (!translationsOk) {
|
||||||
if (Platform.isAndroid) {
|
debugPrint("[_callHandler] could not load translations");
|
||||||
await loadTranslations();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
final bool ok = await _onAssetsChanged();
|
final bool ok = await _onAssetsChanged();
|
||||||
|
@ -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:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/modules/settings/providers/app_settings.provider.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/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 {
|
class ImageViewerQualitySetting extends HookConsumerWidget {
|
||||||
const ImageViewerQualitySetting({
|
const ImageViewerQualitySetting({
|
||||||
@ -44,19 +44,17 @@ class ImageViewerQualitySetting extends HookConsumerWidget {
|
|||||||
title: const Text('setting_image_viewer_help').tr(),
|
title: const Text('setting_image_viewer_help').tr(),
|
||||||
dense: true,
|
dense: true,
|
||||||
),
|
),
|
||||||
buildSwitchListTile(
|
SettingsSwitchListTile(
|
||||||
context,
|
appSettingService: settings,
|
||||||
settings,
|
valueNotifier: isPreview,
|
||||||
isPreview,
|
settingsEnum: AppSettingsEnum.loadPreview,
|
||||||
AppSettingsEnum.loadPreview,
|
|
||||||
title: "setting_image_viewer_preview_title".tr(),
|
title: "setting_image_viewer_preview_title".tr(),
|
||||||
subtitle: "setting_image_viewer_preview_subtitle".tr(),
|
subtitle: "setting_image_viewer_preview_subtitle".tr(),
|
||||||
),
|
),
|
||||||
buildSwitchListTile(
|
SettingsSwitchListTile(
|
||||||
context,
|
appSettingService: settings,
|
||||||
settings,
|
valueNotifier: isOriginal,
|
||||||
isOriginal,
|
settingsEnum: AppSettingsEnum.loadOriginal,
|
||||||
AppSettingsEnum.loadOriginal,
|
|
||||||
title: "setting_image_viewer_original_title".tr(),
|
title: "setting_image_viewer_original_title".tr(),
|
||||||
subtitle: "setting_image_viewer_original_subtitle".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:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.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/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/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 {
|
class NotificationSetting extends HookConsumerWidget {
|
||||||
const NotificationSetting({
|
const NotificationSetting({
|
||||||
@ -14,12 +16,14 @@ class NotificationSetting extends HookConsumerWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final appSettingService = ref.watch(appSettingsServiceProvider);
|
final appSettingService = ref.watch(appSettingsServiceProvider);
|
||||||
|
final permissionService = ref.watch(notificationPermissionProvider);
|
||||||
|
|
||||||
final sliderValue = useState(0.0);
|
final sliderValue = useState(0.0);
|
||||||
final totalProgressValue =
|
final totalProgressValue =
|
||||||
useState(AppSettingsEnum.backgroundBackupTotalProgress.defaultValue);
|
useState(AppSettingsEnum.backgroundBackupTotalProgress.defaultValue);
|
||||||
final singleProgressValue =
|
final singleProgressValue =
|
||||||
useState(AppSettingsEnum.backgroundBackupSingleProgress.defaultValue);
|
useState(AppSettingsEnum.backgroundBackupSingleProgress.defaultValue);
|
||||||
|
final hasPermission = permissionService == PermissionStatus.granted;
|
||||||
|
|
||||||
useEffect(
|
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);
|
final String formattedValue = _formatSliderValue(sliderValue.value);
|
||||||
return ExpansionTile(
|
return ExpansionTile(
|
||||||
textColor: Theme.of(context).primaryColor,
|
textColor: Theme.of(context).primaryColor,
|
||||||
@ -51,23 +79,49 @@ class NotificationSetting extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
).tr(),
|
).tr(),
|
||||||
children: [
|
children: [
|
||||||
buildSwitchListTile(
|
if (!hasPermission)
|
||||||
context,
|
ListTile(
|
||||||
appSettingService,
|
leading: const Icon(Icons.notifications_outlined),
|
||||||
totalProgressValue,
|
title: const Text('notification_permission_list_tile_title').tr(),
|
||||||
AppSettingsEnum.backgroundBackupTotalProgress,
|
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(),
|
title: 'setting_notifications_total_progress_title'.tr(),
|
||||||
subtitle: 'setting_notifications_total_progress_subtitle'.tr(),
|
subtitle: 'setting_notifications_total_progress_subtitle'.tr(),
|
||||||
),
|
),
|
||||||
buildSwitchListTile(
|
SettingsSwitchListTile(
|
||||||
context,
|
enabled: hasPermission,
|
||||||
appSettingService,
|
appSettingService: appSettingService,
|
||||||
singleProgressValue,
|
valueNotifier: singleProgressValue,
|
||||||
AppSettingsEnum.backgroundBackupSingleProgress,
|
settingsEnum: AppSettingsEnum.backgroundBackupSingleProgress,
|
||||||
title: 'setting_notifications_single_progress_title'.tr(),
|
title: 'setting_notifications_single_progress_title'.tr(),
|
||||||
subtitle: 'setting_notifications_single_progress_subtitle'.tr(),
|
subtitle: 'setting_notifications_single_progress_subtitle'.tr(),
|
||||||
),
|
),
|
||||||
ListTile(
|
ListTile(
|
||||||
|
enabled: hasPermission,
|
||||||
isThreeLine: false,
|
isThreeLine: false,
|
||||||
dense: true,
|
dense: true,
|
||||||
title: const Text(
|
title: const Text(
|
||||||
@ -76,7 +130,7 @@ class NotificationSetting extends HookConsumerWidget {
|
|||||||
).tr(args: [formattedValue]),
|
).tr(args: [formattedValue]),
|
||||||
subtitle: Slider(
|
subtitle: Slider(
|
||||||
value: sliderValue.value,
|
value: sliderValue.value,
|
||||||
onChanged: (double v) => sliderValue.value = v,
|
onChanged: !hasPermission ? null : (double v) => sliderValue.value = v,
|
||||||
onChangeEnd: (double v) => appSettingService.setSetting(
|
onChangeEnd: (double v) => appSettingService.setSetting(
|
||||||
AppSettingsEnum.uploadErrorNotificationGracePeriod,
|
AppSettingsEnum.uploadErrorNotificationGracePeriod,
|
||||||
v.toInt(),
|
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:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
@ -41,7 +39,7 @@ class SettingsPage extends HookConsumerWidget {
|
|||||||
const ImageViewerQualitySetting(),
|
const ImageViewerQualitySetting(),
|
||||||
const ThemeSetting(),
|
const ThemeSetting(),
|
||||||
const AssetListSettings(),
|
const AssetListSettings(),
|
||||||
if (Platform.isAndroid) const NotificationSetting(),
|
const NotificationSetting(),
|
||||||
//const ExperimentalSettings(),
|
//const ExperimentalSettings(),
|
||||||
],
|
],
|
||||||
).toList(),
|
).toList(),
|
||||||
|
@ -891,6 +891,46 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.11.1"
|
version: "1.11.1"
|
||||||
|
permission_handler:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: permission_handler
|
||||||
|
sha256: "33c6a1253d1f95fd06fa74b65b7ba907ae9811f9d5c1d3150e51417d04b8d6a8"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "10.2.0"
|
||||||
|
permission_handler_android:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: permission_handler_android
|
||||||
|
sha256: "8028362b40c4a45298f1cbfccd227c8dd6caf0e27088a69f2ba2ab15464159e2"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "10.2.0"
|
||||||
|
permission_handler_apple:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: permission_handler_apple
|
||||||
|
sha256: "9c370ef6a18b1c4b2f7f35944d644a56aa23576f23abee654cf73968de93f163"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "9.0.7"
|
||||||
|
permission_handler_platform_interface:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: permission_handler_platform_interface
|
||||||
|
sha256: "68abbc472002b5e6dfce47fe9898c6b7d8328d58b5d2524f75e277c07a97eb84"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.9.0"
|
||||||
|
permission_handler_windows:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: permission_handler_windows
|
||||||
|
sha256: f67cab14b4328574938ecea2db3475dad7af7ead6afab6338772c5f88963e38b
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.1.2"
|
||||||
petitparser:
|
petitparser:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -45,6 +45,7 @@ dependencies:
|
|||||||
easy_image_viewer: ^1.2.0
|
easy_image_viewer: ^1.2.0
|
||||||
isar: *isar_version
|
isar: *isar_version
|
||||||
isar_flutter_libs: *isar_version # contains Isar Core
|
isar_flutter_libs: *isar_version # contains Isar Core
|
||||||
|
permission_handler: ^10.2.0
|
||||||
|
|
||||||
openapi:
|
openapi:
|
||||||
path: openapi
|
path: openapi
|
||||||
|
Loading…
x
Reference in New Issue
Block a user