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:
martyfuhry 2023-02-21 07:28:52 -05:00 committed by GitHub
parent 2d2cfb0349
commit e9c9b7a3e2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 396 additions and 63 deletions

View File

@ -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"
}

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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])
}
} }

View File

@ -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:

View File

@ -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();

View File

@ -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());

View 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);
}
}

View File

@ -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,
);
}

View File

@ -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(),
), ),

View File

@ -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(),

View File

@ -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,
);
}
}

View File

@ -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(),

View File

@ -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:

View File

@ -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