merge main
This commit is contained in:
@@ -145,9 +145,10 @@ class Album {
|
||||
.remoteIdEqualTo(dto.albumThumbnailAssetId)
|
||||
.findFirst();
|
||||
}
|
||||
if (dto.sharedUsers.isNotEmpty) {
|
||||
final users = await db.users
|
||||
.getAllById(dto.sharedUsers.map((e) => e.id).toList(growable: false));
|
||||
if (dto.albumUsers.isNotEmpty) {
|
||||
final users = await db.users.getAllById(
|
||||
dto.albumUsers.map((e) => e.user.id).toList(growable: false),
|
||||
);
|
||||
a.sharedUsers.addAll(users.cast());
|
||||
}
|
||||
if (dto.assets.isNotEmpty) {
|
||||
|
||||
@@ -182,6 +182,7 @@ enum StoreKey<T> {
|
||||
advancedTroubleshooting<bool>(114, type: bool),
|
||||
logLevel<int>(115, type: int),
|
||||
preferRemoteImage<bool>(116, type: bool),
|
||||
loopVideo<bool>(117, type: bool),
|
||||
// map related settings
|
||||
mapShowFavoriteOnly<bool>(118, type: bool),
|
||||
mapRelativeDate<int>(119, type: int),
|
||||
|
||||
@@ -16,7 +16,7 @@ import 'package:immich_mobile/entities/backup_album.entity.dart';
|
||||
import 'package:immich_mobile/entities/duplicated_asset.entity.dart';
|
||||
import 'package:immich_mobile/routing/router.dart';
|
||||
import 'package:immich_mobile/routing/tab_navigation_observer.dart';
|
||||
import 'package:immich_mobile/shared/cache/widgets_binding.dart';
|
||||
import 'package:immich_mobile/utils/cache/widgets_binding.dart';
|
||||
import 'package:immich_mobile/entities/album.entity.dart';
|
||||
import 'package:immich_mobile/entities/android_device_asset.entity.dart';
|
||||
import 'package:immich_mobile/entities/asset.entity.dart';
|
||||
|
||||
@@ -32,7 +32,7 @@ class ServerDiskInfo {
|
||||
return 'ServerDiskInfo(diskAvailable: $diskAvailable, diskSize: $diskSize, diskUse: $diskUse, diskUsagePercentage: $diskUsagePercentage)';
|
||||
}
|
||||
|
||||
ServerDiskInfo.fromDto(ServerInfoResponseDto dto)
|
||||
ServerDiskInfo.fromDto(ServerStorageResponseDto dto)
|
||||
: diskAvailable = dto.diskAvailable,
|
||||
diskSize = dto.diskSize,
|
||||
diskUse = dto.diskUse,
|
||||
|
||||
@@ -54,7 +54,7 @@ class AlbumPreviewPage extends HookConsumerWidget {
|
||||
],
|
||||
),
|
||||
leading: IconButton(
|
||||
onPressed: () => context.popRoute(),
|
||||
onPressed: () => context.maybePop(),
|
||||
icon: const Icon(Icons.arrow_back_ios_new_rounded),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -191,7 +191,7 @@ class BackupAlbumSelectionPage extends HookConsumerWidget {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
leading: IconButton(
|
||||
onPressed: () => context.popRoute(),
|
||||
onPressed: () => context.maybePop(),
|
||||
icon: const Icon(Icons.arrow_back_ios_rounded),
|
||||
),
|
||||
title: const Text(
|
||||
|
||||
@@ -7,16 +7,16 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/models/backup/backup_state.model.dart';
|
||||
import 'package:immich_mobile/providers/album/album.provider.dart';
|
||||
import 'package:immich_mobile/providers/backup/backup.provider.dart';
|
||||
import 'package:immich_mobile/providers/backup/error_backup_list.provider.dart';
|
||||
import 'package:immich_mobile/providers/backup/ios_background_settings.provider.dart';
|
||||
import 'package:immich_mobile/providers/backup/manual_upload.provider.dart';
|
||||
import 'package:immich_mobile/widgets/backup/current_backup_asset_info_box.dart';
|
||||
import 'package:immich_mobile/models/backup/backup_state.model.dart';
|
||||
import 'package:immich_mobile/providers/backup/backup.provider.dart';
|
||||
import 'package:immich_mobile/routing/router.dart';
|
||||
import 'package:immich_mobile/providers/websocket.provider.dart';
|
||||
import 'package:immich_mobile/routing/router.dart';
|
||||
import 'package:immich_mobile/widgets/backup/backup_info_card.dart';
|
||||
import 'package:immich_mobile/widgets/backup/current_backup_asset_info_box.dart';
|
||||
|
||||
@RoutePage()
|
||||
class BackupControllerPage extends HookConsumerWidget {
|
||||
@@ -260,7 +260,7 @@ class BackupControllerPage extends HookConsumerWidget {
|
||||
leading: IconButton(
|
||||
onPressed: () {
|
||||
ref.watch(websocketProvider.notifier).listenUploadEvent();
|
||||
context.popRoute(true);
|
||||
context.maybePop(true);
|
||||
},
|
||||
splashRadius: 24,
|
||||
icon: const Icon(
|
||||
|
||||
@@ -13,7 +13,7 @@ class BackupOptionsPage extends StatelessWidget {
|
||||
elevation: 0,
|
||||
title: const Text("backup_options_page_title").tr(),
|
||||
leading: IconButton(
|
||||
onPressed: () => context.popRoute(true),
|
||||
onPressed: () => context.maybePop(true),
|
||||
splashRadius: 24,
|
||||
icon: const Icon(
|
||||
Icons.arrow_back_ios_rounded,
|
||||
|
||||
@@ -23,7 +23,7 @@ class FailedBackupStatusPage extends HookConsumerWidget {
|
||||
),
|
||||
leading: IconButton(
|
||||
onPressed: () {
|
||||
context.popRoute(true);
|
||||
context.maybePop(true);
|
||||
},
|
||||
splashRadius: 24,
|
||||
icon: const Icon(
|
||||
|
||||
@@ -26,7 +26,7 @@ class AlbumAdditionalSharedUserSelectionPage extends HookConsumerWidget {
|
||||
final sharedUsersList = useState<Set<User>>({});
|
||||
|
||||
addNewUsersHandler() {
|
||||
context.popRoute(sharedUsersList.value.map((e) => e.id).toList());
|
||||
context.maybePop(sharedUsersList.value.map((e) => e.id).toList());
|
||||
}
|
||||
|
||||
buildTileIcon(User user) {
|
||||
@@ -55,10 +55,9 @@ class AlbumAdditionalSharedUserSelectionPage extends HookConsumerWidget {
|
||||
child: Chip(
|
||||
backgroundColor: context.primaryColor.withOpacity(0.15),
|
||||
label: Text(
|
||||
user.email,
|
||||
user.name,
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.black87,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
@@ -88,13 +87,20 @@ class AlbumAdditionalSharedUserSelectionPage extends HookConsumerWidget {
|
||||
itemBuilder: ((context, index) {
|
||||
return ListTile(
|
||||
leading: buildTileIcon(users[index]),
|
||||
dense: true,
|
||||
title: Text(
|
||||
users[index].email,
|
||||
users[index].name,
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
subtitle: Text(
|
||||
users[index].email,
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
onTap: () {
|
||||
if (sharedUsersList.value.contains(users[index])) {
|
||||
sharedUsersList.value = sharedUsersList.value
|
||||
@@ -127,7 +133,7 @@ class AlbumAdditionalSharedUserSelectionPage extends HookConsumerWidget {
|
||||
leading: IconButton(
|
||||
icon: const Icon(Icons.close_rounded),
|
||||
onPressed: () {
|
||||
context.popRoute(null);
|
||||
context.maybePop(null);
|
||||
},
|
||||
),
|
||||
actions: [
|
||||
|
||||
@@ -184,7 +184,7 @@ class AlbumOptionsPage extends HookConsumerWidget {
|
||||
appBar: AppBar(
|
||||
leading: IconButton(
|
||||
icon: const Icon(Icons.arrow_back_ios_new_rounded),
|
||||
onPressed: () => context.popRoute(null),
|
||||
onPressed: () => context.maybePop(null),
|
||||
),
|
||||
centerTitle: true,
|
||||
title: Text("translated_text_options".tr()),
|
||||
|
||||
@@ -36,7 +36,7 @@ class AlbumSharedUserSelectionPage extends HookConsumerWidget {
|
||||
await ref.watch(sharedAlbumProvider.notifier).getAllSharedAlbums();
|
||||
// ref.watch(assetSelectionProvider.notifier).removeAll();
|
||||
ref.watch(albumTitleProvider.notifier).clearAlbumTitle();
|
||||
context.popRoute(true);
|
||||
context.maybePop(true);
|
||||
context
|
||||
.navigateTo(const TabControllerRoute(children: [SharingRoute()]));
|
||||
}
|
||||
@@ -152,7 +152,7 @@ class AlbumSharedUserSelectionPage extends HookConsumerWidget {
|
||||
leading: IconButton(
|
||||
icon: const Icon(Icons.close_rounded),
|
||||
onPressed: () async {
|
||||
context.popRoute();
|
||||
context.maybePop();
|
||||
},
|
||||
),
|
||||
actions: [
|
||||
|
||||
@@ -105,7 +105,7 @@ class AppLogPage extends HookConsumerWidget {
|
||||
],
|
||||
leading: IconButton(
|
||||
onPressed: () {
|
||||
context.popRoute();
|
||||
context.maybePop();
|
||||
},
|
||||
icon: const Icon(
|
||||
Icons.arrow_back_ios_new_rounded,
|
||||
|
||||
@@ -3,16 +3,16 @@ 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/entities/asset.entity.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/models/albums/asset_selection_page_result.model.dart';
|
||||
import 'package:immich_mobile/providers/album/album.provider.dart';
|
||||
import 'package:immich_mobile/providers/album/album_title.provider.dart';
|
||||
import 'package:immich_mobile/providers/asset.provider.dart';
|
||||
import 'package:immich_mobile/routing/router.dart';
|
||||
import 'package:immich_mobile/widgets/album/album_action_outlined_button.dart';
|
||||
import 'package:immich_mobile/widgets/album/album_title_text_field.dart';
|
||||
import 'package:immich_mobile/widgets/album/shared_album_thumbnail_image.dart';
|
||||
import 'package:immich_mobile/routing/router.dart';
|
||||
import 'package:immich_mobile/entities/asset.entity.dart';
|
||||
import 'package:immich_mobile/providers/asset.provider.dart';
|
||||
|
||||
@RoutePage()
|
||||
// ignore: must_be_immutable
|
||||
@@ -216,7 +216,7 @@ class CreateAlbumPage extends HookConsumerWidget {
|
||||
leading: IconButton(
|
||||
onPressed: () {
|
||||
selectedAssets.value = {};
|
||||
context.popRoute();
|
||||
context.maybePop();
|
||||
},
|
||||
icon: const Icon(Icons.close_rounded),
|
||||
),
|
||||
|
||||
@@ -2,32 +2,33 @@ import 'dart:async';
|
||||
import 'dart:io';
|
||||
import 'dart:math';
|
||||
import 'dart:ui' as ui;
|
||||
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart' hide Store;
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/entities/asset.entity.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/providers/image/immich_remote_image_provider.dart';
|
||||
import 'package:immich_mobile/pages/common/video_viewer.page.dart';
|
||||
import 'package:immich_mobile/providers/app_settings.provider.dart';
|
||||
import 'package:immich_mobile/providers/asset_viewer/asset_stack.provider.dart';
|
||||
import 'package:immich_mobile/providers/asset_viewer/current_asset.provider.dart';
|
||||
import 'package:immich_mobile/providers/asset_viewer/show_controls.provider.dart';
|
||||
import 'package:immich_mobile/providers/asset_viewer/video_player_value_provider.dart';
|
||||
import 'package:immich_mobile/providers/haptic_feedback.provider.dart';
|
||||
import 'package:immich_mobile/providers/image/immich_remote_image_provider.dart';
|
||||
import 'package:immich_mobile/services/app_settings.service.dart';
|
||||
import 'package:immich_mobile/widgets/asset_viewer/advanced_bottom_sheet.dart';
|
||||
import 'package:immich_mobile/widgets/asset_viewer/bottom_gallery_bar.dart';
|
||||
import 'package:immich_mobile/widgets/asset_viewer/exif_sheet/exif_bottom_sheet.dart';
|
||||
import 'package:immich_mobile/widgets/asset_viewer/gallery_app_bar.dart';
|
||||
import 'package:immich_mobile/providers/app_settings.provider.dart';
|
||||
import 'package:immich_mobile/pages/common/video_viewer.page.dart';
|
||||
import 'package:immich_mobile/services/app_settings.service.dart';
|
||||
import 'package:immich_mobile/providers/haptic_feedback.provider.dart';
|
||||
import 'package:immich_mobile/widgets/common/immich_image.dart';
|
||||
import 'package:immich_mobile/widgets/common/immich_thumbnail.dart';
|
||||
import 'package:immich_mobile/widgets/photo_view/photo_view_gallery.dart';
|
||||
import 'package:immich_mobile/widgets/photo_view/src/photo_view_computed_scale.dart';
|
||||
import 'package:immich_mobile/widgets/photo_view/src/photo_view_scale_state.dart';
|
||||
import 'package:immich_mobile/widgets/photo_view/src/utils/photo_view_hero_attributes.dart';
|
||||
import 'package:immich_mobile/entities/asset.entity.dart';
|
||||
import 'package:isar/isar.dart';
|
||||
import 'package:openapi/api.dart' show ThumbnailFormat;
|
||||
|
||||
@@ -59,6 +60,7 @@ class GalleryViewerPage extends HookConsumerWidget {
|
||||
final settings = ref.watch(appSettingsServiceProvider);
|
||||
final isLoadPreview = useState(AppSettingsEnum.loadPreview.defaultValue);
|
||||
final isLoadOriginal = useState(AppSettingsEnum.loadOriginal.defaultValue);
|
||||
final shouldLoopVideo = useState(AppSettingsEnum.loopVideo.defaultValue);
|
||||
final isZoomed = useState(false);
|
||||
final isPlayingVideo = useState(false);
|
||||
final localPosition = useState<Offset?>(null);
|
||||
@@ -101,6 +103,8 @@ class GalleryViewerPage extends HookConsumerWidget {
|
||||
settings.getSetting<bool>(AppSettingsEnum.loadPreview);
|
||||
isLoadOriginal.value =
|
||||
settings.getSetting<bool>(AppSettingsEnum.loadOriginal);
|
||||
shouldLoopVideo.value =
|
||||
settings.getSetting<bool>(AppSettingsEnum.loopVideo);
|
||||
return null;
|
||||
},
|
||||
[],
|
||||
@@ -174,7 +178,7 @@ class GalleryViewerPage extends HookConsumerWidget {
|
||||
|
||||
final ratio = d.dy / max(d.dx.abs(), 1);
|
||||
if (d.dy > sensitivity && ratio > ratioThreshold) {
|
||||
context.popRoute();
|
||||
context.maybePop();
|
||||
} else if (d.dy < -sensitivity && ratio < -ratioThreshold) {
|
||||
showInfo();
|
||||
}
|
||||
@@ -261,12 +265,9 @@ class GalleryViewerPage extends HookConsumerWidget {
|
||||
}
|
||||
|
||||
return PopScope(
|
||||
canPop: false,
|
||||
onPopInvoked: (_) {
|
||||
// Change immersive mode back to normal "edgeToEdge" mode
|
||||
SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);
|
||||
context.pop();
|
||||
},
|
||||
// Change immersive mode back to normal "edgeToEdge" mode
|
||||
onPopInvoked: (_) =>
|
||||
SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge),
|
||||
child: Scaffold(
|
||||
backgroundColor: Colors.black,
|
||||
body: Stack(
|
||||
@@ -370,6 +371,7 @@ class GalleryViewerPage extends HookConsumerWidget {
|
||||
key: ValueKey(a),
|
||||
asset: a,
|
||||
isMotionVideo: a.livePhotoVideoId != null,
|
||||
loopVideo: shouldLoopVideo.value,
|
||||
placeholder: Image(
|
||||
image: provider,
|
||||
fit: BoxFit.contain,
|
||||
|
||||
@@ -5,8 +5,8 @@ import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/widgets/settings/advanced_settings.dart';
|
||||
import 'package:immich_mobile/widgets/settings/asset_list_settings/asset_list_settings.dart';
|
||||
import 'package:immich_mobile/widgets/settings/asset_viewer_settings/asset_viewer_settings.dart';
|
||||
import 'package:immich_mobile/widgets/settings/backup_settings/backup_settings.dart';
|
||||
import 'package:immich_mobile/widgets/settings/image_viewer_quality_setting.dart';
|
||||
import 'package:immich_mobile/widgets/settings/language_settings.dart';
|
||||
import 'package:immich_mobile/widgets/settings/notification_setting.dart';
|
||||
import 'package:immich_mobile/widgets/settings/preference_settings/preference_setting.dart';
|
||||
@@ -33,7 +33,7 @@ enum SettingSection {
|
||||
SettingSection.preferences => const PreferenceSetting(),
|
||||
SettingSection.backup => const BackupSettings(),
|
||||
SettingSection.timeline => const AssetListSettings(),
|
||||
SettingSection.viewer => const ImageViewerQualitySetting(),
|
||||
SettingSection.viewer => const AssetViewerSettings(),
|
||||
SettingSection.advanced => const AdvancedSettings(),
|
||||
};
|
||||
|
||||
|
||||
@@ -1,18 +1,15 @@
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/entities/asset.entity.dart';
|
||||
import 'package:immich_mobile/providers/asset_viewer/show_controls.provider.dart';
|
||||
import 'package:immich_mobile/providers/asset_viewer/video_player_controller_provider.dart';
|
||||
import 'package:immich_mobile/providers/asset_viewer/video_player_controls_provider.dart';
|
||||
import 'package:immich_mobile/providers/asset_viewer/video_player_value_provider.dart';
|
||||
import 'package:immich_mobile/widgets/asset_viewer/video_player.dart';
|
||||
import 'package:immich_mobile/entities/asset.entity.dart';
|
||||
import 'package:immich_mobile/widgets/common/delayed_loading_indicator.dart';
|
||||
import 'package:wakelock_plus/wakelock_plus.dart';
|
||||
|
||||
@RoutePage()
|
||||
// ignore: must_be_immutable
|
||||
class VideoViewerPage extends HookConsumerWidget {
|
||||
final Asset asset;
|
||||
final bool isMotionVideo;
|
||||
@@ -20,6 +17,7 @@ class VideoViewerPage extends HookConsumerWidget {
|
||||
final Duration hideControlsTimer;
|
||||
final bool showControls;
|
||||
final bool showDownloadingIndicator;
|
||||
final bool loopVideo;
|
||||
|
||||
const VideoViewerPage({
|
||||
super.key,
|
||||
@@ -29,6 +27,7 @@ class VideoViewerPage extends HookConsumerWidget {
|
||||
this.showControls = true,
|
||||
this.hideControlsTimer = const Duration(seconds: 5),
|
||||
this.showDownloadingIndicator = true,
|
||||
this.loopVideo = false,
|
||||
});
|
||||
|
||||
@override
|
||||
@@ -76,7 +75,9 @@ class VideoViewerPage extends HookConsumerWidget {
|
||||
// Also sets the error if there is an error in the playback
|
||||
void updateVideoPlayback() {
|
||||
final videoPlayback = VideoPlaybackValue.fromController(controller);
|
||||
ref.read(videoPlaybackValueProvider.notifier).value = videoPlayback;
|
||||
if (!loopVideo) {
|
||||
ref.read(videoPlaybackValueProvider.notifier).value = videoPlayback;
|
||||
}
|
||||
final state = videoPlayback.state;
|
||||
|
||||
// Enable the WakeLock while the video is playing
|
||||
@@ -156,6 +157,7 @@ class VideoViewerPage extends HookConsumerWidget {
|
||||
hideControlsTimer: hideControlsTimer,
|
||||
showControls: showControls,
|
||||
showDownloadingIndicator: showDownloadingIndicator,
|
||||
loopVideo: loopVideo,
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -17,7 +17,7 @@ class ArchivePage extends HookConsumerWidget {
|
||||
final count = archivedAssets.value?.totalAssets.toString() ?? "?";
|
||||
return AppBar(
|
||||
leading: IconButton(
|
||||
onPressed: () => context.popRoute(),
|
||||
onPressed: () => context.maybePop(),
|
||||
icon: const Icon(Icons.arrow_back_ios_rounded),
|
||||
),
|
||||
centerTitle: true,
|
||||
|
||||
@@ -15,7 +15,7 @@ class FavoritesPage extends HookConsumerWidget {
|
||||
AppBar buildAppBar() {
|
||||
return AppBar(
|
||||
leading: IconButton(
|
||||
onPressed: () => context.popRoute(),
|
||||
onPressed: () => context.maybePop(),
|
||||
icon: const Icon(Icons.arrow_back_ios_rounded),
|
||||
),
|
||||
centerTitle: true,
|
||||
|
||||
@@ -143,7 +143,7 @@ class TrashPage extends HookConsumerWidget {
|
||||
return AppBar(
|
||||
leading: IconButton(
|
||||
onPressed: !selectionEnabledHook.value
|
||||
? () => context.popRoute()
|
||||
? () => context.maybePop()
|
||||
: () {
|
||||
selectionEnabledHook.value = false;
|
||||
selection.value = {};
|
||||
|
||||
@@ -176,7 +176,7 @@ class PermissionOnboardingPage extends HookConsumerWidget {
|
||||
),
|
||||
TextButton(
|
||||
child: const Text('permission_onboarding_back').tr(),
|
||||
onPressed: () => context.popRoute(),
|
||||
onPressed: () => context.maybePop(),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -3,14 +3,14 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/entities/asset.entity.dart';
|
||||
import 'package:immich_mobile/models/memories/memory.model.dart';
|
||||
import 'package:immich_mobile/providers/haptic_feedback.provider.dart';
|
||||
import 'package:immich_mobile/widgets/common/immich_image.dart';
|
||||
import 'package:immich_mobile/widgets/memories/memory_bottom_info.dart';
|
||||
import 'package:immich_mobile/widgets/memories/memory_card.dart';
|
||||
import 'package:immich_mobile/widgets/memories/memory_epilogue.dart';
|
||||
import 'package:immich_mobile/widgets/memories/memory_progress_indicator.dart';
|
||||
import 'package:immich_mobile/entities/asset.entity.dart';
|
||||
import 'package:immich_mobile/providers/haptic_feedback.provider.dart';
|
||||
import 'package:immich_mobile/widgets/common/immich_image.dart';
|
||||
|
||||
@RoutePage()
|
||||
class MemoryPage extends HookConsumerWidget {
|
||||
@@ -153,7 +153,7 @@ class MemoryPage extends HookConsumerWidget {
|
||||
final offset = notification.metrics.pixels;
|
||||
if (isEpiloguePage &&
|
||||
(offset > notification.metrics.maxScrollExtent + 150)) {
|
||||
context.popRoute();
|
||||
context.maybePop();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -256,7 +256,7 @@ class MemoryPage extends HookConsumerWidget {
|
||||
// auto_route doesn't invoke pop scope, so
|
||||
// turn off full screen mode here
|
||||
// https://github.com/Milad-Akarie/auto_route_library/issues/1799
|
||||
context.popRoute();
|
||||
context.maybePop();
|
||||
SystemChrome.setEnabledSystemUIMode(
|
||||
SystemUiMode.edgeToEdge,
|
||||
);
|
||||
|
||||
@@ -33,7 +33,6 @@ class PhotosPage extends HookConsumerWidget {
|
||||
() {
|
||||
ref.read(websocketProvider.notifier).connect();
|
||||
Future(() => ref.read(assetProvider.notifier).getAllAsset());
|
||||
ref.read(assetProvider.notifier).getPartnerAssets();
|
||||
ref.read(albumProvider.notifier).getAllAlbums();
|
||||
ref.read(sharedAlbumProvider.notifier).getAllSharedAlbums();
|
||||
ref.read(serverInfoProvider.notifier).getServerInfo();
|
||||
@@ -85,9 +84,6 @@ class PhotosPage extends HookConsumerWidget {
|
||||
Future<void> refreshAssets() async {
|
||||
final fullRefresh = refreshCount.value > 0;
|
||||
await ref.read(assetProvider.notifier).getAllAsset(clear: fullRefresh);
|
||||
if (timelineUsers.length > 1) {
|
||||
await ref.read(assetProvider.notifier).getPartnerAssets();
|
||||
}
|
||||
if (fullRefresh) {
|
||||
// refresh was forced: user requested another refresh within 2 seconds
|
||||
refreshCount.value = 0;
|
||||
|
||||
@@ -18,7 +18,7 @@ class AllMotionPhotosPage extends HookConsumerWidget {
|
||||
appBar: AppBar(
|
||||
title: const Text('motion_photos_page_title').tr(),
|
||||
leading: IconButton(
|
||||
onPressed: () => context.popRoute(),
|
||||
onPressed: () => context.maybePop(),
|
||||
icon: const Icon(Icons.arrow_back_ios_rounded),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -21,7 +21,7 @@ class AllPeoplePage extends HookConsumerWidget {
|
||||
'all_people_page_title',
|
||||
).tr(),
|
||||
leading: IconButton(
|
||||
onPressed: () => context.popRoute(),
|
||||
onPressed: () => context.maybePop(),
|
||||
icon: const Icon(Icons.arrow_back_ios_rounded),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -22,7 +22,7 @@ class AllPlacesPage extends HookConsumerWidget {
|
||||
'curated_location_page_title',
|
||||
).tr(),
|
||||
leading: IconButton(
|
||||
onPressed: () => context.popRoute(),
|
||||
onPressed: () => context.maybePop(),
|
||||
icon: const Icon(Icons.arrow_back_ios_rounded),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -15,7 +15,7 @@ class AllVideosPage extends HookConsumerWidget {
|
||||
appBar: AppBar(
|
||||
title: const Text('all_videos_page_title').tr(),
|
||||
leading: IconButton(
|
||||
onPressed: () => context.popRoute(),
|
||||
onPressed: () => context.maybePop(),
|
||||
icon: const Icon(Icons.arrow_back_ios_rounded),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -41,7 +41,7 @@ class MapLocationPickerPage extends HookConsumerWidget {
|
||||
}
|
||||
|
||||
void onClose([LatLng? selected]) {
|
||||
context.popRoute(selected);
|
||||
context.maybePop(selected);
|
||||
}
|
||||
|
||||
Future<void> getCurrentLocation() async {
|
||||
|
||||
@@ -102,7 +102,7 @@ class PersonResultPage extends HookConsumerWidget {
|
||||
appBar: AppBar(
|
||||
title: Text(name.value),
|
||||
leading: IconButton(
|
||||
onPressed: () => context.popRoute(),
|
||||
onPressed: () => context.maybePop(),
|
||||
icon: const Icon(Icons.arrow_back_ios_rounded),
|
||||
),
|
||||
actions: [
|
||||
|
||||
@@ -18,7 +18,7 @@ class RecentlyAddedPage extends HookConsumerWidget {
|
||||
appBar: AppBar(
|
||||
title: const Text('recently_added_page_title').tr(),
|
||||
leading: IconButton(
|
||||
onPressed: () => context.popRoute(),
|
||||
onPressed: () => context.maybePop(),
|
||||
icon: const Icon(Icons.arrow_back_ios_rounded),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -4,9 +4,11 @@ import 'package:auto_route/auto_route.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/entities/asset.entity.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/models/search/search_filter.model.dart';
|
||||
import 'package:immich_mobile/providers/search/paginated_search.provider.dart';
|
||||
import 'package:immich_mobile/widgets/asset_grid/multiselect_grid.dart';
|
||||
import 'package:immich_mobile/widgets/search/search_filter/camera_picker.dart';
|
||||
import 'package:immich_mobile/widgets/search/search_filter/display_option_picker.dart';
|
||||
import 'package:immich_mobile/widgets/search/search_filter/filter_bottom_sheet_scaffold.dart';
|
||||
@@ -15,8 +17,6 @@ import 'package:immich_mobile/widgets/search/search_filter/media_type_picker.dar
|
||||
import 'package:immich_mobile/widgets/search/search_filter/people_picker.dart';
|
||||
import 'package:immich_mobile/widgets/search/search_filter/search_filter_chip.dart';
|
||||
import 'package:immich_mobile/widgets/search/search_filter/search_filter_utils.dart';
|
||||
import 'package:immich_mobile/entities/asset.entity.dart';
|
||||
import 'package:immich_mobile/widgets/asset_grid/multiselect_grid.dart';
|
||||
import 'package:openapi/api.dart';
|
||||
|
||||
@RoutePage()
|
||||
@@ -480,9 +480,7 @@ class SearchInputPage extends HookConsumerWidget {
|
||||
],
|
||||
leading: IconButton(
|
||||
icon: const Icon(Icons.arrow_back_ios_new_rounded),
|
||||
onPressed: () {
|
||||
context.router.pop();
|
||||
},
|
||||
onPressed: () => context.router.maybePop(),
|
||||
),
|
||||
title: TextField(
|
||||
controller: textSearchController,
|
||||
|
||||
@@ -22,7 +22,7 @@ class PartnerDetailPage extends HookConsumerWidget {
|
||||
|
||||
useEffect(
|
||||
() {
|
||||
ref.read(assetProvider.notifier).getPartnerAssets(partner);
|
||||
ref.read(assetProvider.notifier).getAllAsset();
|
||||
return null;
|
||||
},
|
||||
[],
|
||||
@@ -78,8 +78,7 @@ class PartnerDetailPage extends HookConsumerWidget {
|
||||
),
|
||||
body: MultiselectGrid(
|
||||
renderListProvider: assetsProvider(partner.isarId),
|
||||
onRefresh: () =>
|
||||
ref.read(assetProvider.notifier).getPartnerAssets(partner),
|
||||
onRefresh: () => ref.read(assetProvider.notifier).getAllAsset(),
|
||||
deleteEnabled: false,
|
||||
favoriteEnabled: false,
|
||||
),
|
||||
|
||||
@@ -328,7 +328,7 @@ class SharedLinkEditPage extends HookConsumerWidget {
|
||||
alignment: Alignment.bottomRight,
|
||||
child: ElevatedButton(
|
||||
onPressed: () {
|
||||
context.popRoute();
|
||||
context.maybePop();
|
||||
},
|
||||
child: const Text(
|
||||
"share_done",
|
||||
@@ -431,7 +431,7 @@ class SharedLinkEditPage extends HookConsumerWidget {
|
||||
changeExpiry: changeExpiry,
|
||||
);
|
||||
ref.invalidate(sharedLinksStateProvider);
|
||||
context.popRoute();
|
||||
context.maybePop();
|
||||
}
|
||||
|
||||
return Scaffold(
|
||||
|
||||
@@ -56,11 +56,10 @@ class AppLifeCycleNotifier extends StateNotifier<AppLifeCycleEnum> {
|
||||
switch (_ref.read(tabProvider)) {
|
||||
case TabEnum.home:
|
||||
_ref.read(assetProvider.notifier).getAllAsset();
|
||||
_ref.read(assetProvider.notifier).getPartnerAssets();
|
||||
case TabEnum.search:
|
||||
// nothing to do
|
||||
case TabEnum.sharing:
|
||||
_ref.read(assetProvider.notifier).getPartnerAssets();
|
||||
_ref.read(assetProvider.notifier).getAllAsset();
|
||||
_ref.read(sharedAlbumProvider.notifier).getAllSharedAlbums();
|
||||
case TabEnum.library:
|
||||
_ref.read(albumProvider.notifier).getAllAlbums();
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/providers/memory.provider.dart';
|
||||
import 'package:immich_mobile/services/album.service.dart';
|
||||
import 'package:immich_mobile/entities/exif_info.entity.dart';
|
||||
import 'package:immich_mobile/entities/store.entity.dart';
|
||||
import 'package:immich_mobile/entities/user.entity.dart';
|
||||
import 'package:immich_mobile/providers/db.provider.dart';
|
||||
import 'package:immich_mobile/providers/user.provider.dart';
|
||||
import 'package:immich_mobile/services/asset.service.dart';
|
||||
@@ -23,10 +23,10 @@ class AssetNotifier extends StateNotifier<bool> {
|
||||
final UserService _userService;
|
||||
final SyncService _syncService;
|
||||
final Isar _db;
|
||||
final StateNotifierProviderRef _ref;
|
||||
final log = Logger('AssetNotifier');
|
||||
bool _getAllAssetInProgress = false;
|
||||
bool _deleteInProgress = false;
|
||||
bool _getPartnerAssetsInProgress = false;
|
||||
|
||||
AssetNotifier(
|
||||
this._assetService,
|
||||
@@ -34,6 +34,7 @@ class AssetNotifier extends StateNotifier<bool> {
|
||||
this._userService,
|
||||
this._syncService,
|
||||
this._db,
|
||||
this._ref,
|
||||
) : super(false);
|
||||
|
||||
Future<void> getAllAsset({bool clear = false}) async {
|
||||
@@ -49,9 +50,15 @@ class AssetNotifier extends StateNotifier<bool> {
|
||||
await clearAssetsAndAlbums(_db);
|
||||
log.info("Manual refresh requested, cleared assets and albums from db");
|
||||
}
|
||||
final bool changedUsers = await _userService.refreshUsers();
|
||||
final bool newRemote = await _assetService.refreshRemoteAssets();
|
||||
final bool newLocal = await _albumService.refreshDeviceAlbums();
|
||||
debugPrint("newRemote: $newRemote, newLocal: $newLocal");
|
||||
debugPrint(
|
||||
"changedUsers: $changedUsers, newRemote: $newRemote, newLocal: $newLocal",
|
||||
);
|
||||
if (newRemote) {
|
||||
_ref.invalidate(memoryFutureProvider);
|
||||
}
|
||||
|
||||
log.info("Load assets: ${stopwatch.elapsedMilliseconds}ms");
|
||||
} finally {
|
||||
@@ -60,27 +67,6 @@ class AssetNotifier extends StateNotifier<bool> {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> getPartnerAssets([User? partner]) async {
|
||||
if (_getPartnerAssetsInProgress) return;
|
||||
try {
|
||||
final stopwatch = Stopwatch()..start();
|
||||
_getPartnerAssetsInProgress = true;
|
||||
if (partner == null) {
|
||||
await _userService.refreshUsers();
|
||||
final List<User> partners =
|
||||
await _db.users.filter().isPartnerSharedWithEqualTo(true).findAll();
|
||||
for (User u in partners) {
|
||||
await _assetService.refreshRemoteAssets(u);
|
||||
}
|
||||
} else {
|
||||
await _assetService.refreshRemoteAssets(partner);
|
||||
}
|
||||
log.info("Load partner assets: ${stopwatch.elapsedMilliseconds}ms");
|
||||
} finally {
|
||||
_getPartnerAssetsInProgress = false;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> clearAllAsset() {
|
||||
return clearAssetsAndAlbums(_db);
|
||||
}
|
||||
@@ -321,6 +307,7 @@ final assetProvider = StateNotifierProvider<AssetNotifier, bool>((ref) {
|
||||
ref.watch(userServiceProvider),
|
||||
ref.watch(syncServiceProvider),
|
||||
ref.watch(dbProvider),
|
||||
ref,
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -374,7 +374,7 @@ class BackupNotifier extends StateNotifier<BackUpState> {
|
||||
|
||||
if (state.backupProgress != BackUpProgressEnum.inBackground) {
|
||||
await _getBackupAlbumsInfo();
|
||||
await updateServerInfo();
|
||||
await updateDiskInfo();
|
||||
await _updateBackupAssetCount();
|
||||
} else {
|
||||
log.warning("cannot get backup info - background backup is in progress!");
|
||||
@@ -542,7 +542,7 @@ class BackupNotifier extends StateNotifier<BackUpState> {
|
||||
_updatePersistentAlbumsSelection();
|
||||
}
|
||||
|
||||
updateServerInfo();
|
||||
updateDiskInfo();
|
||||
}
|
||||
|
||||
void _onUploadProgress(int sent, int total) {
|
||||
@@ -579,13 +579,13 @@ class BackupNotifier extends StateNotifier<BackUpState> {
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> updateServerInfo() async {
|
||||
final serverInfo = await _serverInfoService.getServerInfo();
|
||||
Future<void> updateDiskInfo() async {
|
||||
final diskInfo = await _serverInfoService.getDiskInfo();
|
||||
|
||||
// Update server info
|
||||
if (serverInfo != null) {
|
||||
if (diskInfo != null) {
|
||||
state = state.copyWith(
|
||||
serverInfo: serverInfo,
|
||||
serverInfo: diskInfo,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -121,7 +121,7 @@ class ManualUploadNotifier extends StateNotifier<ManualUploadState> {
|
||||
bool isDuplicated,
|
||||
) {
|
||||
state = state.copyWith(successfulUploads: state.successfulUploads + 1);
|
||||
_backupProvider.updateServerInfo();
|
||||
_backupProvider.updateDiskInfo();
|
||||
}
|
||||
|
||||
void _onAssetUploadError(ErrorUploadAsset errorAssetInfo) {
|
||||
|
||||
@@ -1,35 +1,32 @@
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/models/memories/memory.model.dart';
|
||||
import 'package:immich_mobile/models/search/search_filter.model.dart';
|
||||
import 'package:immich_mobile/pages/common/activities.page.dart';
|
||||
import 'package:immich_mobile/models/albums/asset_selection_page_result.model.dart';
|
||||
import 'package:immich_mobile/pages/common/album_options.page.dart';
|
||||
import 'package:immich_mobile/pages/common/album_viewer.page.dart';
|
||||
import 'package:immich_mobile/pages/common/album_asset_selection.page.dart';
|
||||
import 'package:immich_mobile/pages/common/create_album.page.dart';
|
||||
import 'package:immich_mobile/models/shared_link/shared_link.model.dart';
|
||||
import 'package:immich_mobile/pages/common/album_additional_shared_user_selection.page.dart';
|
||||
import 'package:immich_mobile/pages/common/album_shared_user_selection.page.dart';
|
||||
import 'package:immich_mobile/providers/gallery_permission.provider.dart';
|
||||
import 'package:immich_mobile/routing/auth_guard.dart';
|
||||
import 'package:immich_mobile/routing/custom_transition_builders.dart';
|
||||
import 'package:immich_mobile/routing/duplicate_guard.dart';
|
||||
import 'package:immich_mobile/routing/backup_permission_guard.dart';
|
||||
import 'package:immich_mobile/entities/asset.entity.dart';
|
||||
import 'package:immich_mobile/entities/album.entity.dart';
|
||||
import 'package:immich_mobile/entities/asset.entity.dart';
|
||||
import 'package:immich_mobile/entities/logger_message.entity.dart';
|
||||
import 'package:immich_mobile/entities/user.entity.dart';
|
||||
import 'package:immich_mobile/providers/api.provider.dart';
|
||||
import 'package:immich_mobile/pages/common/app_log_detail.page.dart';
|
||||
import 'package:immich_mobile/pages/common/app_log.page.dart';
|
||||
import 'package:immich_mobile/models/albums/asset_selection_page_result.model.dart';
|
||||
import 'package:immich_mobile/models/memories/memory.model.dart';
|
||||
import 'package:immich_mobile/models/search/search_filter.model.dart';
|
||||
import 'package:immich_mobile/models/shared_link/shared_link.model.dart';
|
||||
import 'package:immich_mobile/pages/backup/album_preview.page.dart';
|
||||
import 'package:immich_mobile/pages/backup/backup_album_selection.page.dart';
|
||||
import 'package:immich_mobile/pages/backup/backup_controller.page.dart';
|
||||
import 'package:immich_mobile/pages/backup/backup_options.page.dart';
|
||||
import 'package:immich_mobile/pages/backup/failed_backup_status.page.dart';
|
||||
import 'package:immich_mobile/pages/common/activities.page.dart';
|
||||
import 'package:immich_mobile/pages/common/album_additional_shared_user_selection.page.dart';
|
||||
import 'package:immich_mobile/pages/common/album_asset_selection.page.dart';
|
||||
import 'package:immich_mobile/pages/common/album_options.page.dart';
|
||||
import 'package:immich_mobile/pages/common/album_shared_user_selection.page.dart';
|
||||
import 'package:immich_mobile/pages/common/album_viewer.page.dart';
|
||||
import 'package:immich_mobile/pages/common/app_log.page.dart';
|
||||
import 'package:immich_mobile/pages/common/app_log_detail.page.dart';
|
||||
import 'package:immich_mobile/pages/common/create_album.page.dart';
|
||||
import 'package:immich_mobile/pages/common/gallery_viewer.page.dart';
|
||||
import 'package:immich_mobile/pages/common/settings.page.dart';
|
||||
import 'package:immich_mobile/pages/common/splash_screen.page.dart';
|
||||
import 'package:immich_mobile/pages/common/tab_controller.page.dart';
|
||||
import 'package:immich_mobile/pages/library/archive.page.dart';
|
||||
import 'package:immich_mobile/pages/library/favorite.page.dart';
|
||||
import 'package:immich_mobile/pages/library/library.page.dart';
|
||||
@@ -43,21 +40,23 @@ import 'package:immich_mobile/pages/search/all_motion_videos.page.dart';
|
||||
import 'package:immich_mobile/pages/search/all_people.page.dart';
|
||||
import 'package:immich_mobile/pages/search/all_places.page.dart';
|
||||
import 'package:immich_mobile/pages/search/all_videos.page.dart';
|
||||
import 'package:immich_mobile/pages/search/map/map_location_picker.page.dart';
|
||||
import 'package:immich_mobile/pages/search/map/map.page.dart';
|
||||
import 'package:immich_mobile/pages/search/map/map_location_picker.page.dart';
|
||||
import 'package:immich_mobile/pages/search/person_result.page.dart';
|
||||
import 'package:immich_mobile/pages/search/recently_added.page.dart';
|
||||
import 'package:immich_mobile/pages/search/search_input.page.dart';
|
||||
import 'package:immich_mobile/pages/search/search.page.dart';
|
||||
import 'package:immich_mobile/pages/common/settings.page.dart';
|
||||
import 'package:immich_mobile/pages/sharing/partner/partner_detail.page.dart';
|
||||
import 'package:immich_mobile/pages/search/search_input.page.dart';
|
||||
import 'package:immich_mobile/pages/sharing/partner/partner.page.dart';
|
||||
import 'package:immich_mobile/pages/sharing/shared_link/shared_link_edit.page.dart';
|
||||
import 'package:immich_mobile/pages/sharing/partner/partner_detail.page.dart';
|
||||
import 'package:immich_mobile/pages/sharing/shared_link/shared_link.page.dart';
|
||||
import 'package:immich_mobile/pages/sharing/shared_link/shared_link_edit.page.dart';
|
||||
import 'package:immich_mobile/pages/sharing/sharing.page.dart';
|
||||
import 'package:immich_mobile/pages/common/splash_screen.page.dart';
|
||||
import 'package:immich_mobile/pages/common/tab_controller.page.dart';
|
||||
import 'package:immich_mobile/pages/common/video_viewer.page.dart';
|
||||
import 'package:immich_mobile/providers/api.provider.dart';
|
||||
import 'package:immich_mobile/providers/gallery_permission.provider.dart';
|
||||
import 'package:immich_mobile/routing/auth_guard.dart';
|
||||
import 'package:immich_mobile/routing/backup_permission_guard.dart';
|
||||
import 'package:immich_mobile/routing/custom_transition_builders.dart';
|
||||
import 'package:immich_mobile/routing/duplicate_guard.dart';
|
||||
import 'package:immich_mobile/services/api.service.dart';
|
||||
import 'package:isar/isar.dart';
|
||||
import 'package:maplibre_gl/maplibre_gl.dart';
|
||||
@@ -120,10 +119,6 @@ class AppRouter extends _$AppRouter {
|
||||
guards: [_authGuard, _duplicateGuard],
|
||||
transitionsBuilder: CustomTransitionsBuilders.zoomedPage,
|
||||
),
|
||||
AutoRoute(
|
||||
page: VideoViewerRoute.page,
|
||||
guards: [_authGuard, _duplicateGuard],
|
||||
),
|
||||
AutoRoute(
|
||||
page: BackupControllerRoute.page,
|
||||
guards: [_authGuard, _duplicateGuard, _backupPermissionGuard],
|
||||
|
||||
+180
-256
@@ -21,6 +21,29 @@ abstract class _$AppRouter extends RootStackRouter {
|
||||
child: const ActivitiesPage(),
|
||||
);
|
||||
},
|
||||
AlbumAdditionalSharedUserSelectionRoute.name: (routeData) {
|
||||
final args =
|
||||
routeData.argsAs<AlbumAdditionalSharedUserSelectionRouteArgs>();
|
||||
return AutoRoutePage<List<String>?>(
|
||||
routeData: routeData,
|
||||
child: AlbumAdditionalSharedUserSelectionPage(
|
||||
key: args.key,
|
||||
album: args.album,
|
||||
),
|
||||
);
|
||||
},
|
||||
AlbumAssetSelectionRoute.name: (routeData) {
|
||||
final args = routeData.argsAs<AlbumAssetSelectionRouteArgs>();
|
||||
return AutoRoutePage<AssetSelectionPageResult?>(
|
||||
routeData: routeData,
|
||||
child: AlbumAssetSelectionPage(
|
||||
key: args.key,
|
||||
existingAssets: args.existingAssets,
|
||||
canDeselect: args.canDeselect,
|
||||
query: args.query,
|
||||
),
|
||||
);
|
||||
},
|
||||
AlbumOptionsRoute.name: (routeData) {
|
||||
final args = routeData.argsAs<AlbumOptionsRouteArgs>();
|
||||
return AutoRoutePage<dynamic>(
|
||||
@@ -41,6 +64,16 @@ abstract class _$AppRouter extends RootStackRouter {
|
||||
),
|
||||
);
|
||||
},
|
||||
AlbumSharedUserSelectionRoute.name: (routeData) {
|
||||
final args = routeData.argsAs<AlbumSharedUserSelectionRouteArgs>();
|
||||
return AutoRoutePage<List<String>>(
|
||||
routeData: routeData,
|
||||
child: AlbumSharedUserSelectionPage(
|
||||
key: args.key,
|
||||
assets: args.assets,
|
||||
),
|
||||
);
|
||||
},
|
||||
AlbumViewerRoute.name: (routeData) {
|
||||
final args = routeData.argsAs<AlbumViewerRouteArgs>();
|
||||
return AutoRoutePage<dynamic>(
|
||||
@@ -97,18 +130,6 @@ abstract class _$AppRouter extends RootStackRouter {
|
||||
child: const ArchivePage(),
|
||||
);
|
||||
},
|
||||
AlbumAssetSelectionRoute.name: (routeData) {
|
||||
final args = routeData.argsAs<AssetSelectionRouteArgs>();
|
||||
return AutoRoutePage<AssetSelectionPageResult?>(
|
||||
routeData: routeData,
|
||||
child: AlbumAssetSelectionPage(
|
||||
key: args.key,
|
||||
existingAssets: args.existingAssets,
|
||||
canDeselect: args.canDeselect,
|
||||
query: args.query,
|
||||
),
|
||||
);
|
||||
},
|
||||
BackupAlbumSelectionRoute.name: (routeData) {
|
||||
return AutoRoutePage<dynamic>(
|
||||
routeData: routeData,
|
||||
@@ -170,12 +191,6 @@ abstract class _$AppRouter extends RootStackRouter {
|
||||
),
|
||||
);
|
||||
},
|
||||
PhotosRoute.name: (routeData) {
|
||||
return AutoRoutePage<dynamic>(
|
||||
routeData: routeData,
|
||||
child: const PhotosPage(),
|
||||
);
|
||||
},
|
||||
LibraryRoute.name: (routeData) {
|
||||
return AutoRoutePage<dynamic>(
|
||||
routeData: routeData,
|
||||
@@ -249,6 +264,12 @@ abstract class _$AppRouter extends RootStackRouter {
|
||||
),
|
||||
);
|
||||
},
|
||||
PhotosRoute.name: (routeData) {
|
||||
return AutoRoutePage<dynamic>(
|
||||
routeData: routeData,
|
||||
child: const PhotosPage(),
|
||||
);
|
||||
},
|
||||
RecentlyAddedRoute.name: (routeData) {
|
||||
return AutoRoutePage<dynamic>(
|
||||
routeData: routeData,
|
||||
@@ -272,26 +293,6 @@ abstract class _$AppRouter extends RootStackRouter {
|
||||
child: const SearchPage(),
|
||||
);
|
||||
},
|
||||
AlbumAdditionalSharedUserSelectionRoute.name: (routeData) {
|
||||
final args = routeData.argsAs<SelectAdditionalUserForSharingRouteArgs>();
|
||||
return AutoRoutePage<List<String>?>(
|
||||
routeData: routeData,
|
||||
child: AlbumAdditionalSharedUserSelectionPage(
|
||||
key: args.key,
|
||||
album: args.album,
|
||||
),
|
||||
);
|
||||
},
|
||||
AlbumSharedUserSelectionRoute.name: (routeData) {
|
||||
final args = routeData.argsAs<SelectUserForSharingRouteArgs>();
|
||||
return AutoRoutePage<List<String>>(
|
||||
routeData: routeData,
|
||||
child: AlbumSharedUserSelectionPage(
|
||||
key: args.key,
|
||||
assets: args.assets,
|
||||
),
|
||||
);
|
||||
},
|
||||
SettingsRoute.name: (routeData) {
|
||||
return AutoRoutePage<dynamic>(
|
||||
routeData: routeData,
|
||||
@@ -351,21 +352,6 @@ abstract class _$AppRouter extends RootStackRouter {
|
||||
child: const TrashPage(),
|
||||
);
|
||||
},
|
||||
VideoViewerRoute.name: (routeData) {
|
||||
final args = routeData.argsAs<VideoViewerRouteArgs>();
|
||||
return AutoRoutePage<dynamic>(
|
||||
routeData: routeData,
|
||||
child: VideoViewerPage(
|
||||
key: args.key,
|
||||
asset: args.asset,
|
||||
isMotionVideo: args.isMotionVideo,
|
||||
placeholder: args.placeholder,
|
||||
showControls: args.showControls,
|
||||
hideControlsTimer: args.hideControlsTimer,
|
||||
showDownloadingIndicator: args.showDownloadingIndicator,
|
||||
),
|
||||
);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -383,6 +369,94 @@ class ActivitiesRoute extends PageRouteInfo<void> {
|
||||
static const PageInfo<void> page = PageInfo<void>(name);
|
||||
}
|
||||
|
||||
/// generated route for
|
||||
/// [AlbumAdditionalSharedUserSelectionPage]
|
||||
class AlbumAdditionalSharedUserSelectionRoute
|
||||
extends PageRouteInfo<AlbumAdditionalSharedUserSelectionRouteArgs> {
|
||||
AlbumAdditionalSharedUserSelectionRoute({
|
||||
Key? key,
|
||||
required Album album,
|
||||
List<PageRouteInfo>? children,
|
||||
}) : super(
|
||||
AlbumAdditionalSharedUserSelectionRoute.name,
|
||||
args: AlbumAdditionalSharedUserSelectionRouteArgs(
|
||||
key: key,
|
||||
album: album,
|
||||
),
|
||||
initialChildren: children,
|
||||
);
|
||||
|
||||
static const String name = 'AlbumAdditionalSharedUserSelectionRoute';
|
||||
|
||||
static const PageInfo<AlbumAdditionalSharedUserSelectionRouteArgs> page =
|
||||
PageInfo<AlbumAdditionalSharedUserSelectionRouteArgs>(name);
|
||||
}
|
||||
|
||||
class AlbumAdditionalSharedUserSelectionRouteArgs {
|
||||
const AlbumAdditionalSharedUserSelectionRouteArgs({
|
||||
this.key,
|
||||
required this.album,
|
||||
});
|
||||
|
||||
final Key? key;
|
||||
|
||||
final Album album;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'AlbumAdditionalSharedUserSelectionRouteArgs{key: $key, album: $album}';
|
||||
}
|
||||
}
|
||||
|
||||
/// generated route for
|
||||
/// [AlbumAssetSelectionPage]
|
||||
class AlbumAssetSelectionRoute
|
||||
extends PageRouteInfo<AlbumAssetSelectionRouteArgs> {
|
||||
AlbumAssetSelectionRoute({
|
||||
Key? key,
|
||||
required Set<Asset> existingAssets,
|
||||
bool canDeselect = false,
|
||||
required QueryBuilder<Asset, Asset, QAfterSortBy>? query,
|
||||
List<PageRouteInfo>? children,
|
||||
}) : super(
|
||||
AlbumAssetSelectionRoute.name,
|
||||
args: AlbumAssetSelectionRouteArgs(
|
||||
key: key,
|
||||
existingAssets: existingAssets,
|
||||
canDeselect: canDeselect,
|
||||
query: query,
|
||||
),
|
||||
initialChildren: children,
|
||||
);
|
||||
|
||||
static const String name = 'AlbumAssetSelectionRoute';
|
||||
|
||||
static const PageInfo<AlbumAssetSelectionRouteArgs> page =
|
||||
PageInfo<AlbumAssetSelectionRouteArgs>(name);
|
||||
}
|
||||
|
||||
class AlbumAssetSelectionRouteArgs {
|
||||
const AlbumAssetSelectionRouteArgs({
|
||||
this.key,
|
||||
required this.existingAssets,
|
||||
this.canDeselect = false,
|
||||
required this.query,
|
||||
});
|
||||
|
||||
final Key? key;
|
||||
|
||||
final Set<Asset> existingAssets;
|
||||
|
||||
final bool canDeselect;
|
||||
|
||||
final QueryBuilder<Asset, Asset, QAfterSortBy>? query;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'AlbumAssetSelectionRouteArgs{key: $key, existingAssets: $existingAssets, canDeselect: $canDeselect, query: $query}';
|
||||
}
|
||||
}
|
||||
|
||||
/// generated route for
|
||||
/// [AlbumOptionsPage]
|
||||
class AlbumOptionsRoute extends PageRouteInfo<AlbumOptionsRouteArgs> {
|
||||
@@ -459,6 +533,45 @@ class AlbumPreviewRouteArgs {
|
||||
}
|
||||
}
|
||||
|
||||
/// generated route for
|
||||
/// [AlbumSharedUserSelectionPage]
|
||||
class AlbumSharedUserSelectionRoute
|
||||
extends PageRouteInfo<AlbumSharedUserSelectionRouteArgs> {
|
||||
AlbumSharedUserSelectionRoute({
|
||||
Key? key,
|
||||
required Set<Asset> assets,
|
||||
List<PageRouteInfo>? children,
|
||||
}) : super(
|
||||
AlbumSharedUserSelectionRoute.name,
|
||||
args: AlbumSharedUserSelectionRouteArgs(
|
||||
key: key,
|
||||
assets: assets,
|
||||
),
|
||||
initialChildren: children,
|
||||
);
|
||||
|
||||
static const String name = 'AlbumSharedUserSelectionRoute';
|
||||
|
||||
static const PageInfo<AlbumSharedUserSelectionRouteArgs> page =
|
||||
PageInfo<AlbumSharedUserSelectionRouteArgs>(name);
|
||||
}
|
||||
|
||||
class AlbumSharedUserSelectionRouteArgs {
|
||||
const AlbumSharedUserSelectionRouteArgs({
|
||||
this.key,
|
||||
required this.assets,
|
||||
});
|
||||
|
||||
final Key? key;
|
||||
|
||||
final Set<Asset> assets;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'AlbumSharedUserSelectionRouteArgs{key: $key, assets: $assets}';
|
||||
}
|
||||
}
|
||||
|
||||
/// generated route for
|
||||
/// [AlbumViewerPage]
|
||||
class AlbumViewerRoute extends PageRouteInfo<AlbumViewerRouteArgs> {
|
||||
@@ -619,54 +732,6 @@ class ArchiveRoute extends PageRouteInfo<void> {
|
||||
static const PageInfo<void> page = PageInfo<void>(name);
|
||||
}
|
||||
|
||||
/// generated route for
|
||||
/// [AlbumAssetSelectionPage]
|
||||
class AlbumAssetSelectionRoute extends PageRouteInfo<AssetSelectionRouteArgs> {
|
||||
AlbumAssetSelectionRoute({
|
||||
Key? key,
|
||||
required Set<Asset> existingAssets,
|
||||
bool canDeselect = false,
|
||||
required QueryBuilder<Asset, Asset, QAfterSortBy>? query,
|
||||
List<PageRouteInfo>? children,
|
||||
}) : super(
|
||||
AlbumAssetSelectionRoute.name,
|
||||
args: AssetSelectionRouteArgs(
|
||||
key: key,
|
||||
existingAssets: existingAssets,
|
||||
canDeselect: canDeselect,
|
||||
query: query,
|
||||
),
|
||||
initialChildren: children,
|
||||
);
|
||||
|
||||
static const String name = 'AlbumAssetSelectionRoute';
|
||||
|
||||
static const PageInfo<AssetSelectionRouteArgs> page =
|
||||
PageInfo<AssetSelectionRouteArgs>(name);
|
||||
}
|
||||
|
||||
class AssetSelectionRouteArgs {
|
||||
const AssetSelectionRouteArgs({
|
||||
this.key,
|
||||
required this.existingAssets,
|
||||
this.canDeselect = false,
|
||||
required this.query,
|
||||
});
|
||||
|
||||
final Key? key;
|
||||
|
||||
final Set<Asset> existingAssets;
|
||||
|
||||
final bool canDeselect;
|
||||
|
||||
final QueryBuilder<Asset, Asset, QAfterSortBy>? query;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'AssetSelectionRouteArgs{key: $key, existingAssets: $existingAssets, canDeselect: $canDeselect, query: $query}';
|
||||
}
|
||||
}
|
||||
|
||||
/// generated route for
|
||||
/// [BackupAlbumSelectionPage]
|
||||
class BackupAlbumSelectionRoute extends PageRouteInfo<void> {
|
||||
@@ -852,20 +917,6 @@ class GalleryViewerRouteArgs {
|
||||
}
|
||||
}
|
||||
|
||||
/// generated route for
|
||||
/// [PhotosPage]
|
||||
class PhotosRoute extends PageRouteInfo<void> {
|
||||
const PhotosRoute({List<PageRouteInfo>? children})
|
||||
: super(
|
||||
PhotosRoute.name,
|
||||
initialChildren: children,
|
||||
);
|
||||
|
||||
static const String name = 'PhotosRoute';
|
||||
|
||||
static const PageInfo<void> page = PageInfo<void>(name);
|
||||
}
|
||||
|
||||
/// generated route for
|
||||
/// [LibraryPage]
|
||||
class LibraryRoute extends PageRouteInfo<void> {
|
||||
@@ -1097,6 +1148,20 @@ class PersonResultRouteArgs {
|
||||
}
|
||||
}
|
||||
|
||||
/// generated route for
|
||||
/// [PhotosPage]
|
||||
class PhotosRoute extends PageRouteInfo<void> {
|
||||
const PhotosRoute({List<PageRouteInfo>? children})
|
||||
: super(
|
||||
PhotosRoute.name,
|
||||
initialChildren: children,
|
||||
);
|
||||
|
||||
static const String name = 'PhotosRoute';
|
||||
|
||||
static const PageInfo<void> page = PageInfo<void>(name);
|
||||
}
|
||||
|
||||
/// generated route for
|
||||
/// [RecentlyAddedPage]
|
||||
class RecentlyAddedRoute extends PageRouteInfo<void> {
|
||||
@@ -1163,84 +1228,6 @@ class SearchRoute extends PageRouteInfo<void> {
|
||||
static const PageInfo<void> page = PageInfo<void>(name);
|
||||
}
|
||||
|
||||
/// generated route for
|
||||
/// [AlbumAdditionalSharedUserSelectionPage]
|
||||
class AlbumAdditionalSharedUserSelectionRoute
|
||||
extends PageRouteInfo<SelectAdditionalUserForSharingRouteArgs> {
|
||||
AlbumAdditionalSharedUserSelectionRoute({
|
||||
Key? key,
|
||||
required Album album,
|
||||
List<PageRouteInfo>? children,
|
||||
}) : super(
|
||||
AlbumAdditionalSharedUserSelectionRoute.name,
|
||||
args: SelectAdditionalUserForSharingRouteArgs(
|
||||
key: key,
|
||||
album: album,
|
||||
),
|
||||
initialChildren: children,
|
||||
);
|
||||
|
||||
static const String name = 'AlbumAdditionalSharedUserSelectionRoute';
|
||||
|
||||
static const PageInfo<SelectAdditionalUserForSharingRouteArgs> page =
|
||||
PageInfo<SelectAdditionalUserForSharingRouteArgs>(name);
|
||||
}
|
||||
|
||||
class SelectAdditionalUserForSharingRouteArgs {
|
||||
const SelectAdditionalUserForSharingRouteArgs({
|
||||
this.key,
|
||||
required this.album,
|
||||
});
|
||||
|
||||
final Key? key;
|
||||
|
||||
final Album album;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'SelectAdditionalUserForSharingRouteArgs{key: $key, album: $album}';
|
||||
}
|
||||
}
|
||||
|
||||
/// generated route for
|
||||
/// [AlbumSharedUserSelectionPage]
|
||||
class AlbumSharedUserSelectionRoute
|
||||
extends PageRouteInfo<SelectUserForSharingRouteArgs> {
|
||||
AlbumSharedUserSelectionRoute({
|
||||
Key? key,
|
||||
required Set<Asset> assets,
|
||||
List<PageRouteInfo>? children,
|
||||
}) : super(
|
||||
AlbumSharedUserSelectionRoute.name,
|
||||
args: SelectUserForSharingRouteArgs(
|
||||
key: key,
|
||||
assets: assets,
|
||||
),
|
||||
initialChildren: children,
|
||||
);
|
||||
|
||||
static const String name = 'AlbumSharedUserSelectionRoute';
|
||||
|
||||
static const PageInfo<SelectUserForSharingRouteArgs> page =
|
||||
PageInfo<SelectUserForSharingRouteArgs>(name);
|
||||
}
|
||||
|
||||
class SelectUserForSharingRouteArgs {
|
||||
const SelectUserForSharingRouteArgs({
|
||||
this.key,
|
||||
required this.assets,
|
||||
});
|
||||
|
||||
final Key? key;
|
||||
|
||||
final Set<Asset> assets;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'SelectUserForSharingRouteArgs{key: $key, assets: $assets}';
|
||||
}
|
||||
}
|
||||
|
||||
/// generated route for
|
||||
/// [SettingsPage]
|
||||
class SettingsRoute extends PageRouteInfo<void> {
|
||||
@@ -1410,66 +1397,3 @@ class TrashRoute extends PageRouteInfo<void> {
|
||||
|
||||
static const PageInfo<void> page = PageInfo<void>(name);
|
||||
}
|
||||
|
||||
/// generated route for
|
||||
/// [VideoViewerPage]
|
||||
class VideoViewerRoute extends PageRouteInfo<VideoViewerRouteArgs> {
|
||||
VideoViewerRoute({
|
||||
Key? key,
|
||||
required Asset asset,
|
||||
bool isMotionVideo = false,
|
||||
Widget? placeholder,
|
||||
bool showControls = true,
|
||||
Duration hideControlsTimer = const Duration(seconds: 5),
|
||||
bool showDownloadingIndicator = true,
|
||||
List<PageRouteInfo>? children,
|
||||
}) : super(
|
||||
VideoViewerRoute.name,
|
||||
args: VideoViewerRouteArgs(
|
||||
key: key,
|
||||
asset: asset,
|
||||
isMotionVideo: isMotionVideo,
|
||||
placeholder: placeholder,
|
||||
showControls: showControls,
|
||||
hideControlsTimer: hideControlsTimer,
|
||||
showDownloadingIndicator: showDownloadingIndicator,
|
||||
),
|
||||
initialChildren: children,
|
||||
);
|
||||
|
||||
static const String name = 'VideoViewerRoute';
|
||||
|
||||
static const PageInfo<VideoViewerRouteArgs> page =
|
||||
PageInfo<VideoViewerRouteArgs>(name);
|
||||
}
|
||||
|
||||
class VideoViewerRouteArgs {
|
||||
const VideoViewerRouteArgs({
|
||||
this.key,
|
||||
required this.asset,
|
||||
this.isMotionVideo = false,
|
||||
this.placeholder,
|
||||
this.showControls = true,
|
||||
this.hideControlsTimer = const Duration(seconds: 5),
|
||||
this.showDownloadingIndicator = true,
|
||||
});
|
||||
|
||||
final Key? key;
|
||||
|
||||
final Asset asset;
|
||||
|
||||
final bool isMotionVideo;
|
||||
|
||||
final Widget? placeholder;
|
||||
|
||||
final bool showControls;
|
||||
|
||||
final Duration hideControlsTimer;
|
||||
|
||||
final bool showDownloadingIndicator;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'VideoViewerRouteArgs{key: $key, asset: $asset, isMotionVideo: $isMotionVideo, placeholder: $placeholder, showControls: $showControls, hideControlsTimer: $hideControlsTimer, showDownloadingIndicator: $showDownloadingIndicator}';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,7 +43,7 @@ class TabNavigationObserver extends AutoRouterObserver {
|
||||
|
||||
if (route.name == 'SharingRoute') {
|
||||
ref.read(sharedAlbumProvider.notifier).getAllSharedAlbums();
|
||||
ref.read(assetProvider.notifier).getPartnerAssets();
|
||||
Future(() => ref.read(assetProvider.notifier).getAllAsset());
|
||||
}
|
||||
|
||||
if (route.name == 'LibraryRoute') {
|
||||
|
||||
@@ -180,7 +180,14 @@ class AlbumService {
|
||||
CreateAlbumDto(
|
||||
albumName: albumName,
|
||||
assetIds: assets.map((asset) => asset.remoteId!).toList(),
|
||||
sharedWithUserIds: sharedUsers.map((e) => e.id).toList(),
|
||||
albumUsers: sharedUsers
|
||||
.map(
|
||||
(e) => AlbumUserCreateDto(
|
||||
userId: e.id,
|
||||
role: AlbumUserRole.editor,
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
),
|
||||
);
|
||||
if (remote != null) {
|
||||
|
||||
@@ -23,6 +23,7 @@ class ApiService {
|
||||
late PersonApi personApi;
|
||||
late AuditApi auditApi;
|
||||
late SharedLinkApi sharedLinkApi;
|
||||
late SyncApi syncApi;
|
||||
late SystemConfigApi systemConfigApi;
|
||||
late ActivityApi activityApi;
|
||||
late DownloadApi downloadApi;
|
||||
@@ -53,6 +54,7 @@ class ApiService {
|
||||
personApi = PersonApi(_apiClient);
|
||||
auditApi = AuditApi(_apiClient);
|
||||
sharedLinkApi = SharedLinkApi(_apiClient);
|
||||
syncApi = SyncApi(_apiClient);
|
||||
systemConfigApi = SystemConfigApi(_apiClient);
|
||||
activityApi = ActivityApi(_apiClient);
|
||||
downloadApi = DownloadApi(_apiClient);
|
||||
|
||||
@@ -46,6 +46,7 @@ enum AppSettingsEnum<T> {
|
||||
advancedTroubleshooting<bool>(StoreKey.advancedTroubleshooting, null, false),
|
||||
logLevel<int>(StoreKey.logLevel, null, 5), // Level.INFO = 5
|
||||
preferRemoteImage<bool>(StoreKey.preferRemoteImage, null, false),
|
||||
loopVideo<bool>(StoreKey.loopVideo, "loopVideo", true),
|
||||
mapThemeMode<int>(StoreKey.mapThemeMode, null, 0),
|
||||
mapShowFavoriteOnly<bool>(StoreKey.mapShowFavoriteOnly, null, false),
|
||||
mapIncludeArchived<bool>(StoreKey.mapIncludeArchived, null, false),
|
||||
|
||||
@@ -5,13 +5,14 @@ import 'dart:async';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/entities/asset.entity.dart';
|
||||
import 'package:immich_mobile/entities/etag.entity.dart';
|
||||
import 'package:immich_mobile/entities/exif_info.entity.dart';
|
||||
import 'package:immich_mobile/entities/store.entity.dart';
|
||||
import 'package:immich_mobile/entities/user.entity.dart';
|
||||
import 'package:immich_mobile/providers/api.provider.dart';
|
||||
import 'package:immich_mobile/providers/db.provider.dart';
|
||||
import 'package:immich_mobile/services/api.service.dart';
|
||||
import 'package:immich_mobile/services/sync.service.dart';
|
||||
import 'package:immich_mobile/services/user.service.dart';
|
||||
import 'package:isar/isar.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:maplibre_gl/maplibre_gl.dart';
|
||||
@@ -21,6 +22,7 @@ final assetServiceProvider = Provider(
|
||||
(ref) => AssetService(
|
||||
ref.watch(apiServiceProvider),
|
||||
ref.watch(syncServiceProvider),
|
||||
ref.watch(userServiceProvider),
|
||||
ref.watch(dbProvider),
|
||||
),
|
||||
);
|
||||
@@ -28,24 +30,33 @@ final assetServiceProvider = Provider(
|
||||
class AssetService {
|
||||
final ApiService _apiService;
|
||||
final SyncService _syncService;
|
||||
final UserService _userService;
|
||||
final log = Logger('AssetService');
|
||||
final Isar _db;
|
||||
|
||||
AssetService(
|
||||
this._apiService,
|
||||
this._syncService,
|
||||
this._userService,
|
||||
this._db,
|
||||
);
|
||||
|
||||
/// Checks the server for updated assets and updates the local database if
|
||||
/// required. Returns `true` if there were any changes.
|
||||
Future<bool> refreshRemoteAssets([User? user]) async {
|
||||
user ??= Store.get<User>(StoreKey.currentUser);
|
||||
Future<bool> refreshRemoteAssets() async {
|
||||
final syncedUserIds = await _db.eTags.where().idProperty().findAll();
|
||||
final List<User> syncedUsers = syncedUserIds.isEmpty
|
||||
? []
|
||||
: await _db.users
|
||||
.where()
|
||||
.anyOf(syncedUserIds, (q, id) => q.idEqualTo(id))
|
||||
.findAll();
|
||||
final Stopwatch sw = Stopwatch()..start();
|
||||
final bool changes = await _syncService.syncRemoteAssetsToDb(
|
||||
user,
|
||||
_getRemoteAssetChanges,
|
||||
_getRemoteAssets,
|
||||
users: syncedUsers,
|
||||
getChangedAssets: _getRemoteAssetChanges,
|
||||
loadAssets: _getRemoteAssets,
|
||||
refreshUsers: _userService.getUsersFromServer,
|
||||
);
|
||||
debugPrint("refreshRemoteAssets full took ${sw.elapsedMilliseconds}ms");
|
||||
return changes;
|
||||
@@ -53,14 +64,15 @@ class AssetService {
|
||||
|
||||
/// Returns `(null, null)` if changes are invalid -> requires full sync
|
||||
Future<(List<Asset>? toUpsert, List<String>? toDelete)>
|
||||
_getRemoteAssetChanges(User user, DateTime since) async {
|
||||
final deleted = await _apiService.auditApi
|
||||
.getAuditDeletes(since, EntityType.ASSET, userId: user.id);
|
||||
if (deleted == null || deleted.needsFullSync) return (null, null);
|
||||
final assetDto = await _apiService.assetApi
|
||||
.getAllAssets(userId: user.id, updatedAfter: since);
|
||||
if (assetDto == null) return (null, null);
|
||||
return (assetDto.map(Asset.remote).toList(), deleted.ids);
|
||||
_getRemoteAssetChanges(List<User> users, DateTime since) async {
|
||||
final dto = AssetDeltaSyncDto(
|
||||
updatedAfter: since,
|
||||
userIds: users.map((e) => e.id).toList(),
|
||||
);
|
||||
final changes = await _apiService.syncApi.getDeltaSync(dto);
|
||||
return changes == null || changes.needsFullSync
|
||||
? (null, null)
|
||||
: (changes.upserted.map(Asset.remote).toList(), changes.deleted);
|
||||
}
|
||||
|
||||
/// Returns the list of people of the given asset id.
|
||||
@@ -85,38 +97,32 @@ class AssetService {
|
||||
}
|
||||
|
||||
/// Returns `null` if the server state did not change, else list of assets
|
||||
Future<List<Asset>?> _getRemoteAssets(User user) async {
|
||||
Future<List<Asset>?> _getRemoteAssets(User user, DateTime until) async {
|
||||
const int chunkSize = 10000;
|
||||
try {
|
||||
final DateTime now = DateTime.now().toUtc();
|
||||
final List<Asset> allAssets = [];
|
||||
for (int i = 0;; i += chunkSize) {
|
||||
final List<AssetResponseDto>? assets =
|
||||
await _apiService.assetApi.getAllAssets(
|
||||
DateTime? lastCreationDate;
|
||||
String? lastId;
|
||||
// will break on error or once all assets are loaded
|
||||
while (true) {
|
||||
final dto = AssetFullSyncDto(
|
||||
limit: chunkSize,
|
||||
updatedUntil: until,
|
||||
lastId: lastId,
|
||||
lastCreationDate: lastCreationDate,
|
||||
userId: user.id,
|
||||
// updatedBefore is important! without it we could
|
||||
// a) get the same Asset multiple times in different versions (when
|
||||
// the asset is modified while the chunks are loaded from the server)
|
||||
// b) miss assets when new assets are inserted in between the calls
|
||||
updatedBefore: now,
|
||||
skip: i,
|
||||
take: chunkSize,
|
||||
);
|
||||
if (assets == null) {
|
||||
return null;
|
||||
}
|
||||
final List<AssetResponseDto>? assets =
|
||||
await _apiService.syncApi.getFullSyncForUser(dto);
|
||||
if (assets == null) return null;
|
||||
allAssets.addAll(assets.map(Asset.remote));
|
||||
if (assets.length < chunkSize) {
|
||||
break;
|
||||
}
|
||||
if (assets.isEmpty) break;
|
||||
lastCreationDate = assets.last.fileCreatedAt;
|
||||
lastId = assets.last.id;
|
||||
}
|
||||
return allAssets;
|
||||
} catch (error, stack) {
|
||||
log.severe(
|
||||
'Error while getting remote assets',
|
||||
error,
|
||||
stack,
|
||||
);
|
||||
log.severe('Error while getting remote assets', error, stack);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ import 'package:immich_mobile/entities/store.entity.dart';
|
||||
import 'package:immich_mobile/services/api.service.dart';
|
||||
import 'package:immich_mobile/utils/backup_progress.dart';
|
||||
import 'package:immich_mobile/utils/diff.dart';
|
||||
import 'package:immich_mobile/utils/http_ssl_cert_override.dart';
|
||||
import 'package:isar/isar.dart';
|
||||
import 'package:path_provider_ios/path_provider_ios.dart';
|
||||
import 'package:photo_manager/photo_manager.dart';
|
||||
@@ -590,6 +591,7 @@ enum IosBackgroundTask { fetch, processing }
|
||||
/// entry point called by Kotlin/Java code; needs to be a top-level function
|
||||
@pragma('vm:entry-point')
|
||||
void _nativeEntry() {
|
||||
HttpOverrides.global = HttpSSLCertOverride();
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
DartPluginRegistrant.ensureInitialized();
|
||||
BackgroundService backgroundService = BackgroundService();
|
||||
|
||||
@@ -8,6 +8,8 @@ import 'package:isar/isar.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:openapi/api.dart';
|
||||
|
||||
import '../utils/string_helper.dart';
|
||||
|
||||
final memoryServiceProvider = StateProvider<MemoryService>((ref) {
|
||||
return MemoryService(
|
||||
ref.watch(apiServiceProvider),
|
||||
@@ -36,13 +38,17 @@ class MemoryService {
|
||||
}
|
||||
|
||||
List<Memory> memories = [];
|
||||
for (final MemoryLaneResponseDto(:title, :assets) in data) {
|
||||
memories.add(
|
||||
Memory(
|
||||
title: title,
|
||||
assets: await _db.assets.getAllByRemoteId(assets.map((e) => e.id)),
|
||||
),
|
||||
);
|
||||
for (final MemoryLaneResponseDto(:yearsAgo, :assets) in data) {
|
||||
final dbAssets =
|
||||
await _db.assets.getAllByRemoteId(assets.map((e) => e.id));
|
||||
if (dbAssets.isNotEmpty) {
|
||||
memories.add(
|
||||
Memory(
|
||||
title: '$yearsAgo year${s(yearsAgo)} ago',
|
||||
assets: dbAssets,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return memories.isNotEmpty ? memories : null;
|
||||
|
||||
@@ -18,14 +18,14 @@ class ServerInfoService {
|
||||
|
||||
ServerInfoService(this._apiService);
|
||||
|
||||
Future<ServerDiskInfo?> getServerInfo() async {
|
||||
Future<ServerDiskInfo?> getDiskInfo() async {
|
||||
try {
|
||||
final dto = await _apiService.serverInfoApi.getServerInfo();
|
||||
final dto = await _apiService.serverInfoApi.getStorage();
|
||||
if (dto != null) {
|
||||
return ServerDiskInfo.fromDto(dto);
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint("Error [getServerInfo] ${e.toString()}");
|
||||
debugPrint("Error [getDiskInfo] ${e.toString()}");
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -40,18 +40,20 @@ class SyncService {
|
||||
|
||||
/// Syncs remote assets owned by the logged-in user to the DB
|
||||
/// Returns `true` if there were any changes
|
||||
Future<bool> syncRemoteAssetsToDb(
|
||||
User user,
|
||||
Future<(List<Asset>? toUpsert, List<String>? toDelete)> Function(
|
||||
User user,
|
||||
Future<bool> syncRemoteAssetsToDb({
|
||||
required List<User> users,
|
||||
required Future<(List<Asset>? toUpsert, List<String>? toDelete)> Function(
|
||||
List<User> users,
|
||||
DateTime since,
|
||||
) getChangedAssets,
|
||||
FutureOr<List<Asset>?> Function(User user) loadAssets,
|
||||
) =>
|
||||
required FutureOr<List<Asset>?> Function(User user, DateTime until)
|
||||
loadAssets,
|
||||
required FutureOr<List<User>?> Function() refreshUsers,
|
||||
}) =>
|
||||
_lock.run(
|
||||
() async =>
|
||||
await _syncRemoteAssetChanges(user, getChangedAssets) ??
|
||||
await _syncRemoteAssetsFull(user, loadAssets),
|
||||
await _syncRemoteAssetChanges(users, getChangedAssets) ??
|
||||
await _syncRemoteAssetsFull(refreshUsers, loadAssets),
|
||||
);
|
||||
|
||||
/// Syncs remote albums to the database
|
||||
@@ -111,7 +113,8 @@ class SyncService {
|
||||
both: (User a, User b) {
|
||||
if (!a.updatedAt.isAtSameMomentAs(b.updatedAt) ||
|
||||
a.isPartnerSharedBy != b.isPartnerSharedBy ||
|
||||
a.isPartnerSharedWith != b.isPartnerSharedWith) {
|
||||
a.isPartnerSharedWith != b.isPartnerSharedWith ||
|
||||
a.inTimeline != b.inTimeline) {
|
||||
toUpsert.add(a);
|
||||
return true;
|
||||
}
|
||||
@@ -149,17 +152,22 @@ class SyncService {
|
||||
|
||||
/// Efficiently syncs assets via changes. Returns `null` when a full sync is required.
|
||||
Future<bool?> _syncRemoteAssetChanges(
|
||||
User user,
|
||||
List<User> users,
|
||||
Future<(List<Asset>? toUpsert, List<String>? toDelete)> Function(
|
||||
User user,
|
||||
List<User> users,
|
||||
DateTime since,
|
||||
) getChangedAssets,
|
||||
) async {
|
||||
final DateTime? since = _db.eTags.getByIdSync(user.id)?.time?.toUtc();
|
||||
final currentUser = Store.get(StoreKey.currentUser);
|
||||
final DateTime? since =
|
||||
_db.eTags.getSync(currentUser.isarId)?.time?.toUtc();
|
||||
if (since == null) return null;
|
||||
final DateTime now = DateTime.now();
|
||||
final (toUpsert, toDelete) = await getChangedAssets(user, since);
|
||||
if (toUpsert == null || toDelete == null) return null;
|
||||
final (toUpsert, toDelete) = await getChangedAssets(users, since);
|
||||
if (toUpsert == null || toDelete == null) {
|
||||
await _clearUserAssetsETag(users);
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
if (toDelete.isNotEmpty) {
|
||||
await handleRemoteAssetRemoval(toDelete);
|
||||
@@ -169,7 +177,7 @@ class SyncService {
|
||||
await upsertAssetsWithExif(updated);
|
||||
}
|
||||
if (toUpsert.isNotEmpty || toDelete.isNotEmpty) {
|
||||
await _updateUserAssetsETag(user, now);
|
||||
await _updateUserAssetsETag(users, now);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
@@ -203,11 +211,34 @@ class SyncService {
|
||||
|
||||
/// Syncs assets by loading and comparing all assets from the server.
|
||||
Future<bool> _syncRemoteAssetsFull(
|
||||
FutureOr<List<User>?> Function() refreshUsers,
|
||||
FutureOr<List<Asset>?> Function(User user, DateTime until) loadAssets,
|
||||
) async {
|
||||
final serverUsers = await refreshUsers();
|
||||
if (serverUsers == null) {
|
||||
_log.warning("_syncRemoteAssetsFull aborted because user refresh failed");
|
||||
return false;
|
||||
}
|
||||
await _syncUsersFromServer(serverUsers);
|
||||
final List<User> users = await _db.users
|
||||
.filter()
|
||||
.isPartnerSharedWithEqualTo(true)
|
||||
.or()
|
||||
.isarIdEqualTo(Store.get(StoreKey.currentUser).isarId)
|
||||
.findAll();
|
||||
bool changes = false;
|
||||
for (User u in users) {
|
||||
changes |= await _syncRemoteAssetsForUser(u, loadAssets);
|
||||
}
|
||||
return changes;
|
||||
}
|
||||
|
||||
Future<bool> _syncRemoteAssetsForUser(
|
||||
User user,
|
||||
FutureOr<List<Asset>?> Function(User user) loadAssets,
|
||||
FutureOr<List<Asset>?> Function(User user, DateTime until) loadAssets,
|
||||
) async {
|
||||
final DateTime now = DateTime.now().toUtc();
|
||||
final List<Asset>? remote = await loadAssets(user);
|
||||
final List<Asset>? remote = await loadAssets(user, now);
|
||||
if (remote == null) {
|
||||
return false;
|
||||
}
|
||||
@@ -225,7 +256,7 @@ class SyncService {
|
||||
|
||||
final (toAdd, toUpdate, toRemove) = _diffAssets(remote, inDb, remote: true);
|
||||
if (toAdd.isEmpty && toUpdate.isEmpty && toRemove.isEmpty) {
|
||||
await _updateUserAssetsETag(user, now);
|
||||
await _updateUserAssetsETag([user], now);
|
||||
return false;
|
||||
}
|
||||
final idsToDelete = toRemove.map((e) => e.id).toList();
|
||||
@@ -235,12 +266,19 @@ class SyncService {
|
||||
} on IsarError catch (e) {
|
||||
_log.severe("Failed to sync remote assets to db", e);
|
||||
}
|
||||
await _updateUserAssetsETag(user, now);
|
||||
await _updateUserAssetsETag([user], now);
|
||||
return true;
|
||||
}
|
||||
|
||||
Future<void> _updateUserAssetsETag(User user, DateTime time) =>
|
||||
_db.writeTxn(() => _db.eTags.put(ETag(id: user.id, time: time)));
|
||||
Future<void> _updateUserAssetsETag(List<User> users, DateTime time) {
|
||||
final etags = users.map((u) => ETag(id: u.id, time: time)).toList();
|
||||
return _db.writeTxn(() => _db.eTags.putAll(etags));
|
||||
}
|
||||
|
||||
Future<void> _clearUserAssetsETag(List<User> users) {
|
||||
final ids = users.map((u) => u.id).toList();
|
||||
return _db.writeTxn(() => _db.eTags.deleteAllById(ids));
|
||||
}
|
||||
|
||||
/// Syncs remote albums to the database
|
||||
/// returns `true` if there were any changes
|
||||
@@ -324,15 +362,15 @@ class SyncService {
|
||||
// update shared users
|
||||
final List<User> sharedUsers = album.sharedUsers.toList(growable: false);
|
||||
sharedUsers.sort((a, b) => a.id.compareTo(b.id));
|
||||
dto.sharedUsers.sort((a, b) => a.id.compareTo(b.id));
|
||||
dto.albumUsers.sort((a, b) => a.user.id.compareTo(b.user.id));
|
||||
final List<String> userIdsToAdd = [];
|
||||
final List<User> usersToUnlink = [];
|
||||
diffSortedListsSync(
|
||||
dto.sharedUsers,
|
||||
dto.albumUsers,
|
||||
sharedUsers,
|
||||
compare: (UserResponseDto a, User b) => a.id.compareTo(b.id),
|
||||
compare: (AlbumUserResponseDto a, User b) => a.user.id.compareTo(b.id),
|
||||
both: (a, b) => false,
|
||||
onlyFirst: (UserResponseDto a) => userIdsToAdd.add(a.id),
|
||||
onlyFirst: (AlbumUserResponseDto a) => userIdsToAdd.add(a.user.id),
|
||||
onlySecond: (User a) => usersToUnlink.add(a),
|
||||
);
|
||||
|
||||
@@ -867,7 +905,7 @@ bool _hasAlbumResponseDtoChanged(AlbumResponseDto dto, Album a) {
|
||||
dto.albumName != a.name ||
|
||||
dto.albumThumbnailAssetId != a.thumbnail.value?.remoteId ||
|
||||
dto.shared != a.shared ||
|
||||
dto.sharedUsers.length != a.sharedUsers.length ||
|
||||
dto.albumUsers.length != a.sharedUsers.length ||
|
||||
!dto.updatedAt.isAtSameMomentAs(a.modifiedAt) ||
|
||||
!isAtSameMomentAs(dto.startDate, a.startDate) ||
|
||||
!isAtSameMomentAs(dto.endDate, a.endDate) ||
|
||||
|
||||
@@ -70,7 +70,7 @@ class UserService {
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> refreshUsers() async {
|
||||
Future<List<User>?> getUsersFromServer() async {
|
||||
final List<User>? users = await _getAllUsers(isAll: true);
|
||||
final List<User>? sharedBy =
|
||||
await _partnerService.getPartners(PartnerDirection.sharedBy);
|
||||
@@ -79,7 +79,7 @@ class UserService {
|
||||
|
||||
if (users == null || sharedBy == null || sharedWith == null) {
|
||||
_log.warning("Failed to refresh users");
|
||||
return false;
|
||||
return null;
|
||||
}
|
||||
|
||||
users.sortBy((u) => u.id);
|
||||
@@ -108,6 +108,12 @@ class UserService {
|
||||
onlySecond: (_) {},
|
||||
);
|
||||
|
||||
return users;
|
||||
}
|
||||
|
||||
Future<bool> refreshUsers() async {
|
||||
final users = await getUsersFromServer();
|
||||
if (users == null) return false;
|
||||
return _syncService.syncUsersFromServer(users);
|
||||
}
|
||||
}
|
||||
|
||||
Vendored
+5
@@ -17,6 +17,7 @@ ChewieController useChewieController({
|
||||
bool allowFullScreen = false,
|
||||
bool allowedScreenSleep = false,
|
||||
bool showControls = true,
|
||||
bool loopVideo = false,
|
||||
Widget? customControls,
|
||||
Widget? placeholder,
|
||||
Duration hideControlsTimer = const Duration(seconds: 1),
|
||||
@@ -36,6 +37,7 @@ ChewieController useChewieController({
|
||||
hideControlsTimer: hideControlsTimer,
|
||||
showControlsOnInitialize: showControlsOnInitialize,
|
||||
showControls: showControls,
|
||||
loopVideo: loopVideo,
|
||||
allowedScreenSleep: allowedScreenSleep,
|
||||
onPlaying: onPlaying,
|
||||
onPaused: onPaused,
|
||||
@@ -53,6 +55,7 @@ class _ChewieControllerHook extends Hook<ChewieController> {
|
||||
final bool allowFullScreen;
|
||||
final bool allowedScreenSleep;
|
||||
final bool showControls;
|
||||
final bool loopVideo;
|
||||
final Widget? customControls;
|
||||
final Widget? placeholder;
|
||||
final Duration hideControlsTimer;
|
||||
@@ -71,6 +74,7 @@ class _ChewieControllerHook extends Hook<ChewieController> {
|
||||
this.allowFullScreen = false,
|
||||
this.allowedScreenSleep = false,
|
||||
this.showControls = true,
|
||||
this.loopVideo = false,
|
||||
this.customControls,
|
||||
this.placeholder,
|
||||
this.hideControlsTimer = const Duration(seconds: 3),
|
||||
@@ -94,6 +98,7 @@ class _ChewieControllerHookState
|
||||
allowFullScreen: hook.allowFullScreen,
|
||||
allowedScreenSleep: hook.allowedScreenSleep,
|
||||
showControls: hook.showControls,
|
||||
looping: hook.loopVideo,
|
||||
customControls: hook.customControls,
|
||||
placeholder: hook.placeholder,
|
||||
hideControlsTimer: hook.hideControlsTimer,
|
||||
@@ -77,5 +77,5 @@ String getThumbnailUrlForRemoteId(
|
||||
}
|
||||
|
||||
String getFaceThumbnailUrl(final String personId) {
|
||||
return '${Store.get(StoreKey.serverEndpoint)}/person/$personId/thumbnail';
|
||||
return '${Store.get(StoreKey.serverEndpoint)}/people/$personId/thumbnail';
|
||||
}
|
||||
|
||||
@@ -121,12 +121,12 @@ final ThemeData immichLightTheme = ThemeData(
|
||||
),
|
||||
navigationBarTheme: NavigationBarThemeData(
|
||||
indicatorColor: Colors.indigo.withOpacity(0.15),
|
||||
iconTheme: MaterialStatePropertyAll(
|
||||
iconTheme: WidgetStatePropertyAll(
|
||||
IconThemeData(color: Colors.grey[700]),
|
||||
),
|
||||
backgroundColor: immichBackgroundColor,
|
||||
surfaceTintColor: Colors.transparent,
|
||||
labelTextStyle: MaterialStatePropertyAll(
|
||||
labelTextStyle: WidgetStatePropertyAll(
|
||||
TextStyle(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w500,
|
||||
@@ -249,12 +249,12 @@ final ThemeData immichDarkTheme = ThemeData(
|
||||
),
|
||||
navigationBarTheme: NavigationBarThemeData(
|
||||
indicatorColor: immichDarkThemePrimaryColor.withOpacity(0.4),
|
||||
iconTheme: MaterialStatePropertyAll(
|
||||
iconTheme: WidgetStatePropertyAll(
|
||||
IconThemeData(color: Colors.grey[500]),
|
||||
),
|
||||
backgroundColor: Colors.grey[900],
|
||||
surfaceTintColor: Colors.transparent,
|
||||
labelTextStyle: MaterialStatePropertyAll(
|
||||
labelTextStyle: WidgetStatePropertyAll(
|
||||
TextStyle(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w500,
|
||||
|
||||
@@ -4,17 +4,12 @@ import 'package:immich_mobile/entities/store.entity.dart';
|
||||
import 'package:immich_mobile/utils/db.dart';
|
||||
import 'package:isar/isar.dart';
|
||||
|
||||
const int targetVersion = 6;
|
||||
|
||||
Future<void> migrateDatabaseIfNeeded(Isar db) async {
|
||||
final int version = Store.get(StoreKey.version, 1);
|
||||
switch (version) {
|
||||
case 1:
|
||||
await _migrateTo(db, 2);
|
||||
case 2:
|
||||
await _migrateTo(db, 3);
|
||||
case 3:
|
||||
await _migrateTo(db, 4);
|
||||
case 4:
|
||||
await _migrateTo(db, 5);
|
||||
if (version < targetVersion) {
|
||||
_migrateTo(db, targetVersion);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,3 +3,5 @@ extension StringExtension on String {
|
||||
return "${this[0].toUpperCase()}${substring(1).toLowerCase()}";
|
||||
}
|
||||
}
|
||||
|
||||
String s(num count) => (count == 1 ? '' : 's');
|
||||
@@ -275,7 +275,7 @@ class AlbumViewerAppbar extends HookConsumerWidget
|
||||
);
|
||||
} else {
|
||||
return IconButton(
|
||||
onPressed: () async => await context.popRoute(),
|
||||
onPressed: () async => await context.maybePop(),
|
||||
icon: const Icon(Icons.arrow_back_ios_rounded),
|
||||
splashRadius: 25,
|
||||
);
|
||||
|
||||
@@ -110,7 +110,7 @@ class BottomGalleryBar extends ConsumerWidget {
|
||||
if (isDeleted && isParent) {
|
||||
if (totalAssets == 1) {
|
||||
// Handle only one asset
|
||||
context.popRoute();
|
||||
context.maybePop();
|
||||
} else {
|
||||
// Go to next page otherwise
|
||||
controller.nextPage(
|
||||
@@ -181,7 +181,7 @@ class BottomGalleryBar extends ConsumerWidget {
|
||||
stackElements.elementAt(stackIndex),
|
||||
);
|
||||
ctx.pop();
|
||||
context.popRoute();
|
||||
context.maybePop();
|
||||
},
|
||||
title: const Text(
|
||||
"viewer_stack_use_as_main_asset",
|
||||
@@ -208,7 +208,7 @@ class BottomGalleryBar extends ConsumerWidget {
|
||||
childrenToRemove: [asset],
|
||||
);
|
||||
ctx.pop();
|
||||
context.popRoute();
|
||||
context.maybePop();
|
||||
} else {
|
||||
await ref.read(assetStackServiceProvider).updateStack(
|
||||
asset,
|
||||
@@ -236,7 +236,7 @@ class BottomGalleryBar extends ConsumerWidget {
|
||||
childrenToRemove: stack,
|
||||
);
|
||||
ctx.pop();
|
||||
context.popRoute();
|
||||
context.maybePop();
|
||||
},
|
||||
title: const Text(
|
||||
"viewer_unstack",
|
||||
@@ -267,7 +267,7 @@ class BottomGalleryBar extends ConsumerWidget {
|
||||
handleArchive() {
|
||||
ref.read(assetProvider.notifier).toggleArchive([asset]);
|
||||
if (isParent) {
|
||||
context.popRoute();
|
||||
context.maybePop();
|
||||
return;
|
||||
}
|
||||
removeAssetFromStack();
|
||||
|
||||
@@ -6,7 +6,7 @@ import 'package:immich_mobile/providers/asset_viewer/video_player_controls_provi
|
||||
import 'package:immich_mobile/providers/asset_viewer/video_player_value_provider.dart';
|
||||
import 'package:immich_mobile/widgets/asset_viewer/center_play_button.dart';
|
||||
import 'package:immich_mobile/widgets/common/delayed_loading_indicator.dart';
|
||||
import 'package:immich_mobile/shared/ui/hooks/timer_hook.dart';
|
||||
import 'package:immich_mobile/utils/hooks/timer_hook.dart';
|
||||
|
||||
class CustomVideoPlayerControls extends HookConsumerWidget {
|
||||
final Duration hideTimerDuration;
|
||||
|
||||
@@ -161,7 +161,7 @@ class TopControlAppBar extends HookConsumerWidget {
|
||||
Widget buildBackButton() {
|
||||
return IconButton(
|
||||
onPressed: () {
|
||||
context.popRoute();
|
||||
context.maybePop();
|
||||
},
|
||||
icon: Icon(
|
||||
Icons.arrow_back_ios_new_rounded,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import 'package:chewie/chewie.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/widgets/asset_viewer/hooks/chewiew_controller_hook.dart';
|
||||
import 'package:immich_mobile/utils/hooks/chewiew_controller_hook.dart';
|
||||
import 'package:immich_mobile/widgets/asset_viewer/custom_video_player_controls.dart';
|
||||
import 'package:video_player/video_player.dart';
|
||||
|
||||
@@ -12,6 +12,7 @@ class VideoPlayerViewer extends HookConsumerWidget {
|
||||
final Duration hideControlsTimer;
|
||||
final bool showControls;
|
||||
final bool showDownloadingIndicator;
|
||||
final bool loopVideo;
|
||||
|
||||
const VideoPlayerViewer({
|
||||
super.key,
|
||||
@@ -21,6 +22,7 @@ class VideoPlayerViewer extends HookConsumerWidget {
|
||||
required this.hideControlsTimer,
|
||||
required this.showControls,
|
||||
required this.showDownloadingIndicator,
|
||||
required this.loopVideo,
|
||||
});
|
||||
|
||||
@override
|
||||
@@ -36,6 +38,7 @@ class VideoPlayerViewer extends HookConsumerWidget {
|
||||
),
|
||||
showControls: showControls && !isMotionVideo,
|
||||
hideControlsTimer: hideControlsTimer,
|
||||
loopVideo: loopVideo,
|
||||
);
|
||||
|
||||
return Chewie(
|
||||
|
||||
@@ -31,7 +31,7 @@ class ImmichAppBarDialog extends HookConsumerWidget {
|
||||
|
||||
useEffect(
|
||||
() {
|
||||
ref.read(backupProvider.notifier).updateServerInfo();
|
||||
ref.read(backupProvider.notifier).updateDiskInfo();
|
||||
ref.read(currentUserProvider.notifier).refresh();
|
||||
return null;
|
||||
},
|
||||
|
||||
@@ -164,7 +164,7 @@ class _DateTimePicker extends HookWidget {
|
||||
color: context.primaryColor,
|
||||
),
|
||||
menuStyle: const MenuStyle(
|
||||
fixedSize: MaterialStatePropertyAll(Size.fromWidth(350)),
|
||||
fixedSize: WidgetStatePropertyAll(Size.fromWidth(350)),
|
||||
alignment: Alignment(-1.25, 0.5),
|
||||
),
|
||||
onSelected: (value) => tzOffset.value = value!,
|
||||
@@ -175,7 +175,7 @@ class _DateTimePicker extends HookWidget {
|
||||
value: t,
|
||||
label: t.display,
|
||||
style: ButtonStyle(
|
||||
textStyle: MaterialStatePropertyAll(
|
||||
textStyle: WidgetStatePropertyAll(
|
||||
context.textTheme.bodyMedium,
|
||||
),
|
||||
),
|
||||
|
||||
@@ -5,7 +5,7 @@ import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:immich_mobile/providers/image/immich_local_thumbnail_provider.dart';
|
||||
import 'package:immich_mobile/providers/image/immich_remote_thumbnail_provider.dart';
|
||||
import 'package:immich_mobile/entities/asset.entity.dart';
|
||||
import 'package:immich_mobile/shared/ui/hooks/blurhash_hook.dart';
|
||||
import 'package:immich_mobile/utils/hooks/blurhash_hook.dart';
|
||||
import 'package:immich_mobile/widgets/common/immich_image.dart';
|
||||
import 'package:immich_mobile/widgets/common/thumbhash_placeholder.dart';
|
||||
import 'package:octo_image/octo_image.dart';
|
||||
|
||||
@@ -79,7 +79,7 @@ class _LocationPicker extends HookWidget {
|
||||
).tr(),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () => context.popRoute(latlng),
|
||||
onPressed: () => context.maybePop(latlng),
|
||||
child: Text(
|
||||
"action_common_update",
|
||||
style: context.textTheme.bodyMedium?.copyWith(
|
||||
@@ -215,7 +215,7 @@ class _ManualPicker extends HookWidget {
|
||||
decorationText: "location_picker_longitude",
|
||||
hintText: "location_picker_longitude_hint",
|
||||
errorText: "location_picker_longitude_error",
|
||||
focusNode: latitiudeFocusNode,
|
||||
focusNode: longitudeFocusNode,
|
||||
validator: _validateLong,
|
||||
onUpdated: onLongitudeEditingCompleted,
|
||||
),
|
||||
|
||||
@@ -6,7 +6,7 @@ import 'package:immich_mobile/entities/user.entity.dart';
|
||||
|
||||
Widget userAvatar(BuildContext context, User u, {double? radius}) {
|
||||
final url =
|
||||
"${Store.get(StoreKey.serverEndpoint)}/user/profile-image/${u.id}";
|
||||
"${Store.get(StoreKey.serverEndpoint)}/users/${u.id}/profile-image";
|
||||
final nameFirstLetter = u.name.isNotEmpty ? u.name[0] : "";
|
||||
return CircleAvatar(
|
||||
radius: radius,
|
||||
|
||||
@@ -24,7 +24,7 @@ class UserCircleAvatar extends ConsumerWidget {
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
bool isDarkTheme = Theme.of(context).brightness == Brightness.dark;
|
||||
final profileImageUrl =
|
||||
'${Store.get(StoreKey.serverEndpoint)}/user/profile-image/${user.id}?d=${Random().nextInt(1024)}';
|
||||
'${Store.get(StoreKey.serverEndpoint)}/users/${user.id}/profile-image?d=${Random().nextInt(1024)}';
|
||||
|
||||
final textIcon = Text(
|
||||
user.name[0].toUpperCase(),
|
||||
|
||||
@@ -50,7 +50,7 @@ class _NonSelectionRow extends StatelessWidget {
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
ElevatedButton(
|
||||
onPressed: () => context.popRoute(),
|
||||
onPressed: () => context.maybePop(),
|
||||
style: ElevatedButton.styleFrom(
|
||||
shape: const CircleBorder(),
|
||||
),
|
||||
|
||||
@@ -43,7 +43,7 @@ class MemoryBottomInfo extends StatelessWidget {
|
||||
MaterialButton(
|
||||
minWidth: 0,
|
||||
onPressed: () {
|
||||
context.popRoute();
|
||||
context.maybePop();
|
||||
scrollToDateNotifierProvider
|
||||
.scrollToDate(memory.assets[0].fileCreatedAt);
|
||||
},
|
||||
|
||||
@@ -5,7 +5,7 @@ import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/entities/asset.entity.dart';
|
||||
import 'package:immich_mobile/pages/common/video_viewer.page.dart';
|
||||
import 'package:immich_mobile/shared/ui/hooks/blurhash_hook.dart';
|
||||
import 'package:immich_mobile/utils/hooks/blurhash_hook.dart';
|
||||
import 'package:immich_mobile/widgets/common/immich_image.dart';
|
||||
|
||||
class MemoryCard extends StatelessWidget {
|
||||
|
||||
@@ -40,7 +40,7 @@ class CameraPicker extends HookConsumerWidget {
|
||||
);
|
||||
|
||||
final menuStyle = MenuStyle(
|
||||
shape: MaterialStatePropertyAll<OutlinedBorder>(
|
||||
shape: WidgetStatePropertyAll<OutlinedBorder>(
|
||||
RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(15),
|
||||
),
|
||||
|
||||
@@ -56,7 +56,7 @@ class LocationPicker extends HookConsumerWidget {
|
||||
);
|
||||
|
||||
final menuStyle = MenuStyle(
|
||||
shape: MaterialStatePropertyAll<OutlinedBorder>(
|
||||
shape: WidgetStatePropertyAll<OutlinedBorder>(
|
||||
RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(15),
|
||||
),
|
||||
|
||||
@@ -6,7 +6,7 @@ import 'package:immich_mobile/widgets/settings/local_storage_settings.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:immich_mobile/widgets/settings/utils/app_settings_update_hook.dart';
|
||||
import 'package:immich_mobile/utils/hooks/app_settings_update_hook.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/services/app_settings.service.dart';
|
||||
import 'package:immich_mobile/providers/user.provider.dart';
|
||||
|
||||
@@ -6,7 +6,7 @@ import 'package:immich_mobile/providers/app_settings.provider.dart';
|
||||
import 'package:immich_mobile/services/app_settings.service.dart';
|
||||
import 'package:immich_mobile/widgets/settings/settings_radio_list_tile.dart';
|
||||
import 'package:immich_mobile/widgets/settings/settings_sub_title.dart';
|
||||
import 'package:immich_mobile/widgets/settings/utils/app_settings_update_hook.dart';
|
||||
import 'package:immich_mobile/utils/hooks/app_settings_update_hook.dart';
|
||||
|
||||
class GroupSettings extends HookConsumerWidget {
|
||||
const GroupSettings({
|
||||
|
||||
@@ -6,7 +6,7 @@ import 'package:immich_mobile/services/app_settings.service.dart';
|
||||
import 'package:immich_mobile/widgets/settings/settings_slider_list_tile.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/widgets/settings/utils/app_settings_update_hook.dart';
|
||||
import 'package:immich_mobile/utils/hooks/app_settings_update_hook.dart';
|
||||
|
||||
class LayoutSettings extends HookConsumerWidget {
|
||||
const LayoutSettings({
|
||||
|
||||
@@ -6,7 +6,7 @@ import 'package:immich_mobile/services/app_settings.service.dart';
|
||||
import 'package:immich_mobile/widgets/settings/asset_list_settings/asset_list_group_settings.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:immich_mobile/widgets/settings/utils/app_settings_update_hook.dart';
|
||||
import 'package:immich_mobile/utils/hooks/app_settings_update_hook.dart';
|
||||
import 'asset_list_layout_settings.dart';
|
||||
|
||||
class AssetListSettings extends HookConsumerWidget {
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:immich_mobile/widgets/settings/asset_viewer_settings/image_viewer_quality_setting.dart';
|
||||
import 'package:immich_mobile/widgets/settings/settings_sub_page_scaffold.dart';
|
||||
import 'video_viewer_settings.dart';
|
||||
|
||||
class AssetViewerSettings extends StatelessWidget {
|
||||
const AssetViewerSettings({
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final assetViewerSetting = [
|
||||
const ImageViewerQualitySetting(),
|
||||
const VideoViewerSettings(),
|
||||
];
|
||||
|
||||
return SettingsSubPageScaffold(
|
||||
settings: assetViewerSetting,
|
||||
showDivider: true,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/providers/app_settings.provider.dart';
|
||||
import 'package:immich_mobile/services/app_settings.service.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 ImageViewerQualitySetting extends HookConsumerWidget {
|
||||
const ImageViewerQualitySetting({
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final isPreview = useAppSettingsState(AppSettingsEnum.loadPreview);
|
||||
final isOriginal = useAppSettingsState(AppSettingsEnum.loadOriginal);
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
SettingsSubTitle(title: "setting_image_viewer_title".tr()),
|
||||
ListTile(
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 20),
|
||||
title: Text(
|
||||
'setting_image_viewer_help',
|
||||
style: context.textTheme.bodyMedium,
|
||||
).tr(),
|
||||
),
|
||||
SettingsSwitchListTile(
|
||||
valueNotifier: isPreview,
|
||||
title: "setting_image_viewer_preview_title".tr(),
|
||||
subtitle: "setting_image_viewer_preview_subtitle".tr(),
|
||||
onChanged: (_) => ref.invalidate(appSettingsServiceProvider),
|
||||
),
|
||||
SettingsSwitchListTile(
|
||||
valueNotifier: isOriginal,
|
||||
title: "setting_image_viewer_original_title".tr(),
|
||||
subtitle: "setting_image_viewer_original_subtitle".tr(),
|
||||
onChanged: (_) => ref.invalidate(appSettingsServiceProvider),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.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/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 VideoViewerSettings extends HookConsumerWidget {
|
||||
const VideoViewerSettings({
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final useLoopVideo = useAppSettingsState(AppSettingsEnum.loopVideo);
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
SettingsSubTitle(title: "setting_video_viewer_title".tr()),
|
||||
SettingsSwitchListTile(
|
||||
valueNotifier: useLoopVideo,
|
||||
title: "setting_video_viewer_looping_title".tr(),
|
||||
subtitle: "setting_video_viewer_looping_subtitle".tr(),
|
||||
onChanged: (_) => ref.invalidate(appSettingsServiceProvider),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -9,7 +9,7 @@ import 'package:immich_mobile/widgets/settings/backup_settings/foreground_settin
|
||||
import 'package:immich_mobile/widgets/settings/settings_button_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:immich_mobile/widgets/settings/utils/app_settings_update_hook.dart';
|
||||
import 'package:immich_mobile/utils/hooks/app_settings_update_hook.dart';
|
||||
import 'package:immich_mobile/widgets/common/immich_loading_indicator.dart';
|
||||
|
||||
class BackupSettings extends HookConsumerWidget {
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/services/app_settings.service.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:immich_mobile/widgets/settings/utils/app_settings_update_hook.dart';
|
||||
|
||||
class ImageViewerQualitySetting extends HookWidget {
|
||||
const ImageViewerQualitySetting({
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isPreview = useAppSettingsState(AppSettingsEnum.loadPreview);
|
||||
final isOriginal = useAppSettingsState(AppSettingsEnum.loadOriginal);
|
||||
|
||||
final viewerSettings = [
|
||||
ListTile(
|
||||
title: Text(
|
||||
'setting_image_viewer_help',
|
||||
style: context.textTheme.bodyMedium,
|
||||
).tr(),
|
||||
),
|
||||
SettingsSwitchListTile(
|
||||
valueNotifier: isPreview,
|
||||
title: "setting_image_viewer_preview_title".tr(),
|
||||
subtitle: "setting_image_viewer_preview_subtitle".tr(),
|
||||
),
|
||||
SettingsSwitchListTile(
|
||||
valueNotifier: isOriginal,
|
||||
title: "setting_image_viewer_original_title".tr(),
|
||||
subtitle: "setting_image_viewer_original_subtitle".tr(),
|
||||
),
|
||||
];
|
||||
|
||||
return SettingsSubPageScaffold(settings: viewerSettings);
|
||||
}
|
||||
}
|
||||
@@ -34,12 +34,12 @@ class LanguageSettings extends HookConsumerWidget {
|
||||
contentPadding: const EdgeInsets.only(left: 16),
|
||||
),
|
||||
menuStyle: MenuStyle(
|
||||
shape: MaterialStatePropertyAll<OutlinedBorder>(
|
||||
shape: WidgetStatePropertyAll<OutlinedBorder>(
|
||||
RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(15),
|
||||
),
|
||||
),
|
||||
backgroundColor: MaterialStatePropertyAll<Color>(
|
||||
backgroundColor: WidgetStatePropertyAll<Color>(
|
||||
context.isDarkTheme
|
||||
? Colors.grey[900]!
|
||||
: context.scaffoldBackgroundColor,
|
||||
|
||||
@@ -8,7 +8,7 @@ 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:immich_mobile/widgets/settings/utils/app_settings_update_hook.dart';
|
||||
import 'package:immich_mobile/utils/hooks/app_settings_update_hook.dart';
|
||||
import 'package:permission_handler/permission_handler.dart';
|
||||
|
||||
class NotificationSetting extends HookConsumerWidget {
|
||||
|
||||
@@ -5,7 +5,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/services/app_settings.service.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/widgets/settings/utils/app_settings_update_hook.dart';
|
||||
import 'package:immich_mobile/utils/hooks/app_settings_update_hook.dart';
|
||||
|
||||
class HapticSetting extends HookConsumerWidget {
|
||||
const HapticSetting({
|
||||
|
||||
@@ -5,7 +5,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/services/app_settings.service.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/widgets/settings/utils/app_settings_update_hook.dart';
|
||||
import 'package:immich_mobile/utils/hooks/app_settings_update_hook.dart';
|
||||
import 'package:immich_mobile/utils/immich_app_theme.dart';
|
||||
|
||||
class ThemeSetting extends HookConsumerWidget {
|
||||
|
||||
Reference in New Issue
Block a user