Merge branch 'main' into feat/custom-date-range

This commit is contained in:
Yaros
2026-05-13 18:46:08 +02:00
426 changed files with 18698 additions and 4342 deletions
@@ -35,7 +35,9 @@ class CommentBubble extends ConsumerWidget {
Future<void> openAssetViewer() async {
final activityService = ref.read(activityServiceProvider);
final route = await activityService.buildAssetViewerRoute(activity.assetId!, ref);
if (route != null) await context.pushRoute(route);
if (route != null) {
await context.pushRoute(route);
}
}
// avatar (hidden for own messages)
@@ -49,7 +49,9 @@ class _VideoControlsState extends ConsumerState<VideoControls> {
}
void _onHideTimer() {
if (!mounted) return;
if (!mounted) {
return;
}
if (ref.read(_provider).status == VideoPlaybackStatus.playing) {
ref.read(assetViewerProvider.notifier).setControls(false);
}
@@ -91,7 +93,9 @@ class _VideoControlsState extends ConsumerState<VideoControls> {
final isFinished = !isCasting && videoStatus == VideoPlaybackStatus.completed;
ref.listen(assetViewerProvider.select((v) => v.showingControls), (prev, showing) {
if (showing && prev != showing) _hideTimer.reset();
if (showing && prev != showing) {
_hideTimer.reset();
}
});
ref.listen(_provider.select((v) => v.status), (_, __) => _hideTimer.reset());
@@ -119,13 +123,15 @@ class _VideoControlsState extends ConsumerState<VideoControls> {
onPressed: () => _toggle(isCasting),
),
const Spacer(),
Text(
"${position.format()} / ${duration.format()}",
style: const TextStyle(
color: Colors.white,
fontWeight: FontWeight.w500,
fontFeatures: [FontFeature.tabularFigures()],
shadows: VideoControls._controlShadows,
IgnorePointer(
child: Text(
"${position.format()} / ${duration.format()}",
style: const TextStyle(
color: Colors.white,
fontWeight: FontWeight.w500,
fontFeatures: [FontFeature.tabularFigures()],
shadows: VideoControls._controlShadows,
),
),
),
const SizedBox(width: 12),
@@ -190,7 +190,9 @@ class _TimeZoneOffset implements Comparable<_TimeZoneOffset> {
@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
if (identical(this, other)) {
return true;
}
return other is _TimeZoneOffset && other.display == display && other.offsetInMilliseconds == offsetInMilliseconds;
}
+12 -4
View File
@@ -40,7 +40,9 @@ class LoginForm extends HookConsumerWidget {
final log = Logger('LoginForm');
String? _validateUrl(String? url) {
if (url == null || url.isEmpty) return null;
if (url == null || url.isEmpty) {
return null;
}
final parsedUrl = Uri.tryParse(url);
if (parsedUrl == null || !parsedUrl.isAbsolute || !parsedUrl.scheme.startsWith("http") || parsedUrl.host.isEmpty) {
@@ -51,9 +53,15 @@ class LoginForm extends HookConsumerWidget {
}
String? _validateEmail(String? email) {
if (email == null || email == '') return null;
if (email.endsWith(' ')) return 'login_form_err_trailing_whitespace'.tr();
if (email.startsWith(' ')) return 'login_form_err_leading_whitespace'.tr();
if (email == null || email == '') {
return null;
}
if (email.endsWith(' ')) {
return 'login_form_err_trailing_whitespace'.tr();
}
if (email.startsWith(' ')) {
return 'login_form_err_leading_whitespace'.tr();
}
if (email.contains(' ') || !email.contains('@')) {
return 'login_form_err_invalid_email'.tr();
}
@@ -15,7 +15,9 @@ class StarRatingPicker extends HookWidget {
return RadioGroup(
groupValue: selectedRating.value?.rating,
onChanged: (int? newValue) {
if (newValue == null) return;
if (newValue == null) {
return;
}
final newFilter = SearchRatingFilter(rating: newValue);
selectedRating.value = newFilter;
onSelect(newFilter);
@@ -32,7 +32,11 @@ class AdvancedSettings extends HookConsumerWidget {
final isManageMediaSupported = useState(false);
final manageMediaAndroidPermission = useState(false);
final levelId = useState<int>(ref.read(systemConfigProvider).logLevel.index);
final preferRemote = useAppSettingsState(AppSettingsEnum.preferRemoteImage);
final preferRemote = useState(ref.read(appConfigProvider).image.preferRemote);
useValueChanged(
preferRemote.value,
(_, __) => ref.read(metadataProvider).write(.imagePreferRemote, preferRemote.value),
);
final readonlyModeEnabled = useAppSettingsState(AppSettingsEnum.readonlyModeEnabled);
final logLevel = Level.LEVELS[levelId.value].name;
@@ -1,12 +1,13 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/models/metadata_key.dart';
import 'package:immich_mobile/domain/models/timeline.model.dart';
import 'package:immich_mobile/extensions/translate_extensions.dart';
import 'package:immich_mobile/providers/app_settings.provider.dart';
import 'package:immich_mobile/services/app_settings.service.dart';
import 'package:immich_mobile/utils/hooks/app_settings_update_hook.dart';
import 'package:immich_mobile/providers/infrastructure/metadata.provider.dart';
import 'package:immich_mobile/widgets/settings/setting_group_title.dart';
import 'package:immich_mobile/widgets/settings/settings_radio_list_tile.dart';
@@ -15,18 +16,17 @@ class GroupSettings extends HookConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final groupByIndex = useAppSettingsState(AppSettingsEnum.groupAssetsBy);
final groupBy = GroupAssetsBy.values[groupByIndex.value];
final groupBy = useValueNotifier(ref.watch(appConfigProvider.select((s) => s.timeline.groupAssetsBy)));
Future<void> updateAppSettings(GroupAssetsBy groupBy) async {
await ref.watch(appSettingsServiceProvider).setSetting(AppSettingsEnum.groupAssetsBy, groupBy.index);
await ref.read(metadataProvider).write(MetadataKey.timelineGroupAssetsBy, groupBy);
ref.invalidate(appSettingsServiceProvider);
}
void changeGroupValue(GroupAssetsBy? value) {
if (value != null) {
groupByIndex.value = value.index;
unawaited(updateAppSettings(groupBy));
groupBy.value = value;
unawaited(updateAppSettings(value));
}
}
@@ -52,7 +52,7 @@ class GroupSettings extends HookConsumerWidget {
value: GroupAssetsBy.auto,
),
],
groupBy: groupBy,
groupBy: groupBy.value,
onRadioChanged: changeGroupValue,
),
],
@@ -1,10 +1,11 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/models/metadata_key.dart';
import 'package:immich_mobile/extensions/translate_extensions.dart';
import 'package:immich_mobile/providers/app_settings.provider.dart';
import 'package:immich_mobile/services/app_settings.service.dart';
import 'package:immich_mobile/utils/hooks/app_settings_update_hook.dart';
import 'package:immich_mobile/providers/infrastructure/metadata.provider.dart';
import 'package:immich_mobile/widgets/settings/setting_group_title.dart';
import 'package:immich_mobile/widgets/settings/settings_slider_list_tile.dart';
@@ -13,7 +14,10 @@ class LayoutSettings extends HookConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final tilesPerRow = useAppSettingsState(AppSettingsEnum.tilesPerRow);
final tilesPerRow = useState(ref.read(appConfigProvider.select((s) => s.timeline.tilesPerRow)));
useValueChanged<int, void>(tilesPerRow.value, (_, __) {
ref.read(metadataProvider).write(MetadataKey.timelineTilesPerRow, tilesPerRow.value);
});
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
@@ -29,7 +33,9 @@ class LayoutSettings extends HookConsumerWidget {
maxValue: 6,
minValue: 2,
noDivisons: 4,
onChangeEnd: (_) => ref.invalidate(appSettingsServiceProvider),
onChangeEnd: (value) {
ref.invalidate(appSettingsServiceProvider);
},
),
],
);
@@ -1,10 +1,11 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/models/metadata_key.dart';
import 'package:immich_mobile/providers/app_settings.provider.dart';
import 'package:immich_mobile/providers/infrastructure/metadata.provider.dart';
import 'package:immich_mobile/providers/infrastructure/setting.provider.dart';
import 'package:immich_mobile/services/app_settings.service.dart';
import 'package:immich_mobile/utils/hooks/app_settings_update_hook.dart';
import 'package:immich_mobile/widgets/settings/asset_list_settings/asset_list_group_settings.dart';
import 'package:immich_mobile/widgets/settings/asset_list_settings/asset_list_layout_settings.dart';
import 'package:immich_mobile/widgets/settings/settings_sub_page_scaffold.dart';
@@ -15,13 +16,14 @@ class AssetListSettings extends HookConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final showStorageIndicator = useAppSettingsState(AppSettingsEnum.storageIndicator);
final storageIndicator = useValueNotifier(ref.watch(appConfigProvider.select((s) => s.timeline.storageIndicator)));
final assetListSetting = [
SettingsSwitchListTile(
valueNotifier: showStorageIndicator,
valueNotifier: storageIndicator,
title: 'theme_setting_asset_list_storage_indicator_title'.tr(),
onChanged: (_) {
onChanged: (value) {
ref.read(metadataProvider).write(MetadataKey.timelineStorageIndicator, value);
ref.invalidate(appSettingsServiceProvider);
ref.invalidate(settingsProvider);
},
@@ -1,19 +1,21 @@
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/extensions/translate_extensions.dart';
import 'package:immich_mobile/providers/app_settings.provider.dart';
import 'package:immich_mobile/services/app_settings.service.dart';
import 'package:immich_mobile/providers/infrastructure/metadata.provider.dart';
import 'package:immich_mobile/widgets/settings/setting_group_title.dart';
import 'package:immich_mobile/widgets/settings/settings_switch_list_tile.dart';
import 'package:immich_mobile/utils/hooks/app_settings_update_hook.dart';
class ImageViewerQualitySetting extends HookConsumerWidget {
const ImageViewerQualitySetting({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final isPreview = useAppSettingsState(AppSettingsEnum.loadPreview);
final isOriginal = useAppSettingsState(AppSettingsEnum.loadOriginal);
final isOriginal = useState(ref.read(appConfigProvider).image.loadOriginal);
useValueChanged<bool, void>(isOriginal.value, (_, __) {
ref.read(metadataProvider).write(.imageLoadOriginal, isOriginal.value);
});
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
@@ -23,12 +25,6 @@ class ImageViewerQualitySetting extends HookConsumerWidget {
icon: Icons.image_outlined,
subtitle: "setting_image_viewer_help".t(context: context),
),
SettingsSwitchListTile(
valueNotifier: isPreview,
title: "setting_image_viewer_preview_title".t(context: context),
subtitle: "setting_image_viewer_preview_subtitle".t(context: context),
onChanged: (_) => ref.invalidate(appSettingsServiceProvider),
),
SettingsSwitchListTile(
valueNotifier: isOriginal,
title: "setting_image_viewer_original_title".t(context: context),
@@ -1,18 +1,20 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/providers/app_settings.provider.dart';
import 'package:immich_mobile/services/app_settings.service.dart';
import 'package:immich_mobile/providers/infrastructure/metadata.provider.dart';
import 'package:immich_mobile/widgets/settings/settings_sub_title.dart';
import 'package:immich_mobile/widgets/settings/settings_switch_list_tile.dart';
import 'package:immich_mobile/utils/hooks/app_settings_update_hook.dart';
class ImageViewerTapToNavigateSetting extends HookConsumerWidget {
const ImageViewerTapToNavigateSetting({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final tapToNavigate = useAppSettingsState(AppSettingsEnum.tapToNavigate);
final tapToNavigate = useState(ref.read(appConfigProvider).viewer.tapToNavigate);
useValueChanged<bool, void>(tapToNavigate.value, (_, __) {
ref.read(metadataProvider).write(.viewerTapToNavigate, tapToNavigate.value);
});
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
@@ -22,7 +24,6 @@ class ImageViewerTapToNavigateSetting extends HookConsumerWidget {
valueNotifier: tapToNavigate,
title: "setting_image_navigation_enable_title".tr(),
subtitle: "setting_image_navigation_enable_subtitle".tr(),
onChanged: (_) => ref.invalidate(appSettingsServiceProvider),
),
],
);
@@ -1,20 +1,30 @@
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/extensions/translate_extensions.dart';
import 'package:immich_mobile/providers/app_settings.provider.dart';
import 'package:immich_mobile/services/app_settings.service.dart';
import 'package:immich_mobile/providers/infrastructure/metadata.provider.dart';
import 'package:immich_mobile/widgets/settings/setting_group_title.dart';
import 'package:immich_mobile/widgets/settings/settings_switch_list_tile.dart';
import 'package:immich_mobile/utils/hooks/app_settings_update_hook.dart';
class VideoViewerSettings extends HookConsumerWidget {
const VideoViewerSettings({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final useLoopVideo = useAppSettingsState(AppSettingsEnum.loopVideo);
final useOriginalVideo = useAppSettingsState(AppSettingsEnum.loadOriginalVideo);
final useAutoPlayVideo = useAppSettingsState(AppSettingsEnum.autoPlayVideo);
final viewer = ref.read(appConfigProvider).viewer;
final useAutoPlayVideo = useState(viewer.autoPlayVideo);
final useLoopVideo = useState(viewer.loopVideo);
final useOriginalVideo = useState(viewer.loadOriginalVideo);
useValueChanged<bool, void>(useAutoPlayVideo.value, (_, __) {
ref.read(metadataProvider).write(.viewerAutoPlayVideo, useAutoPlayVideo.value);
});
useValueChanged<bool, void>(useLoopVideo.value, (_, __) {
ref.read(metadataProvider).write(.viewerLoopVideo, useLoopVideo.value);
});
useValueChanged<bool, void>(useOriginalVideo.value, (_, __) {
ref.read(metadataProvider).write(.viewerLoadOriginalVideo, useOriginalVideo.value);
});
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
@@ -27,19 +37,16 @@ class VideoViewerSettings extends HookConsumerWidget {
valueNotifier: useAutoPlayVideo,
title: "setting_video_viewer_auto_play_title".t(context: context),
subtitle: "setting_video_viewer_auto_play_subtitle".t(context: context),
onChanged: (_) => ref.invalidate(appSettingsServiceProvider),
),
SettingsSwitchListTile(
valueNotifier: useLoopVideo,
title: "setting_video_viewer_looping_title".t(context: context),
subtitle: "loop_videos_description".t(context: context),
onChanged: (_) => ref.invalidate(appSettingsServiceProvider),
),
SettingsSwitchListTile(
valueNotifier: useOriginalVideo,
title: "setting_video_viewer_original_video_title".t(context: context),
subtitle: "setting_video_viewer_original_video_subtitle".t(context: context),
onChanged: (_) => ref.invalidate(appSettingsServiceProvider),
),
],
);
@@ -74,6 +74,9 @@ class _AlbumSyncActionButtonState extends ConsumerState<_AlbumSyncActionButton>
} catch (_) {
} finally {
Future.delayed(const Duration(seconds: 1), () {
if (!mounted) {
return;
}
setState(() {
isAlbumSyncInProgress = false;
});
@@ -80,7 +80,9 @@ class _FreeUpSpaceSettingsState extends ConsumerState<FreeUpSpaceSettings> {
bool _isPresetSelected(int? daysAgo) {
final state = ref.read(cleanupProvider);
if (state.selectedDate == null) return false;
if (state.selectedDate == null) {
return false;
}
final expectedDate = daysAgo != null ? DateTime.now().subtract(Duration(days: daysAgo)) : DateTime(2000);
@@ -8,7 +8,6 @@ import 'package:immich_mobile/utils/hooks/app_settings_update_hook.dart';
import 'package:immich_mobile/widgets/settings/settings_button_list_tile.dart';
import 'package:immich_mobile/widgets/settings/settings_slider_list_tile.dart';
import 'package:immich_mobile/widgets/settings/settings_sub_page_scaffold.dart';
import 'package:immich_mobile/widgets/settings/settings_switch_list_tile.dart';
import 'package:permission_handler/permission_handler.dart';
class NotificationSetting extends HookConsumerWidget {
@@ -19,8 +18,6 @@ class NotificationSetting extends HookConsumerWidget {
final permissionService = ref.watch(notificationPermissionProvider);
final sliderValue = useAppSettingsState(AppSettingsEnum.uploadErrorNotificationGracePeriod);
final totalProgressValue = useAppSettingsState(AppSettingsEnum.backgroundBackupTotalProgress);
final singleProgressValue = useAppSettingsState(AppSettingsEnum.backgroundBackupSingleProgress);
final hasPermission = permissionService == PermissionStatus.granted;
@@ -60,18 +57,6 @@ class NotificationSetting extends HookConsumerWidget {
}
}),
),
SettingsSwitchListTile(
enabled: hasPermission,
valueNotifier: totalProgressValue,
title: 'setting_notifications_total_progress_title'.tr(),
subtitle: 'setting_notifications_total_progress_subtitle'.tr(),
),
SettingsSwitchListTile(
enabled: hasPermission,
valueNotifier: singleProgressValue,
title: 'setting_notifications_single_progress_title'.tr(),
subtitle: 'setting_notifications_single_progress_subtitle'.tr(),
),
SettingsSliderListTile(
enabled: hasPermission,
valueNotifier: sliderValue,
@@ -2,7 +2,6 @@ import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/constants/colors.dart';
import 'package:immich_mobile/domain/models/metadata_key.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/extensions/theme_extensions.dart';
import 'package:immich_mobile/providers/infrastructure/metadata.provider.dart';
@@ -27,16 +26,16 @@ class PrimaryColorSetting extends HookConsumerWidget {
}
onUseSystemColorChange(bool newValue) {
ref.read(metadataProvider).write(MetadataKey.dynamicTheme, newValue);
ref.read(metadataProvider).write(.themeDynamic, newValue);
popBottomSheet();
}
onPrimaryColorChange(ImmichColorPreset colorPreset) {
ref.read(metadataProvider).write(MetadataKey.primaryColor, colorPreset);
ref.read(metadataProvider).write(.themePrimaryColor, colorPreset);
//turn off system color setting
if (themeConfig.dynamicTheme) {
ref.read(metadataProvider).write(MetadataKey.dynamicTheme, false);
ref.read(metadataProvider).write(.themeDynamic, false);
}
popBottomSheet();
}
@@ -1,7 +1,6 @@
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/models/metadata_key.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/extensions/translate_extensions.dart';
import 'package:immich_mobile/providers/infrastructure/metadata.provider.dart';
@@ -23,7 +22,7 @@ class ThemeSetting extends HookConsumerWidget {
void onThemeChange(bool isDark) {
currentTheme.value = isDark ? ThemeMode.dark : ThemeMode.light;
ref.read(metadataProvider).write(MetadataKey.themeMode, currentTheme.value);
ref.read(metadataProvider).write(.themeMode, currentTheme.value);
}
void onSystemThemeChange(bool isSystem) {
@@ -40,11 +39,11 @@ class ThemeSetting extends HookConsumerWidget {
currentTheme.value = ThemeMode.dark;
}
}
ref.read(metadataProvider).write(MetadataKey.themeMode, currentTheme.value);
ref.read(metadataProvider).write(.themeMode, currentTheme.value);
}
void onSurfaceColorSettingChange(bool useColorfulInterface) {
ref.read(metadataProvider).write(MetadataKey.colorfulInterface, useColorfulInterface);
ref.read(metadataProvider).write(.themeColorfulInterface, useColorfulInterface);
colorfulInterface.value = useColorfulInterface;
}
@@ -29,7 +29,9 @@ class SettingsSwitchListTile extends StatelessWidget {
@override
Widget build(BuildContext context) {
void onSwitchChanged(bool value) {
if (!enabled) return;
if (!enabled) {
return;
}
valueNotifier.value = value;
onChanged?.call(value);