mirror of
https://github.com/immich-app/immich.git
synced 2026-05-18 03:10:24 +03:00
Merge remote-tracking branch 'origin/main' into feature/new-activity-timeline
This commit is contained in:
@@ -1,3 +1,5 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
@@ -57,7 +59,7 @@ class AlbumViewerAppbar extends HookConsumerWidget implements PreferredSizeWidge
|
||||
deleteAlbum() async {
|
||||
final bool success = await ref.watch(albumProvider.notifier).deleteAlbum(album);
|
||||
|
||||
context.navigateTo(const TabControllerRoute(children: [AlbumsRoute()]));
|
||||
unawaited(context.navigateTo(const TabControllerRoute(children: [AlbumsRoute()])));
|
||||
|
||||
if (!success) {
|
||||
ImmichToast.show(
|
||||
@@ -105,7 +107,7 @@ class AlbumViewerAppbar extends HookConsumerWidget implements PreferredSizeWidge
|
||||
bool isSuccess = await ref.watch(albumProvider.notifier).leaveAlbum(album);
|
||||
|
||||
if (isSuccess) {
|
||||
context.navigateTo(const TabControllerRoute(children: [AlbumsRoute()]));
|
||||
unawaited(context.navigateTo(const TabControllerRoute(children: [AlbumsRoute()])));
|
||||
} else {
|
||||
context.pop();
|
||||
ImmichToast.show(
|
||||
|
||||
@@ -25,7 +25,7 @@ class RemoteAlbumSharedUserIcons extends ConsumerWidget {
|
||||
}
|
||||
|
||||
return GestureDetector(
|
||||
onTap: () => context.pushRoute(const DriftAlbumOptionsRoute()),
|
||||
onTap: () => context.pushRoute(DriftAlbumOptionsRoute(album: currentAlbum)),
|
||||
child: SizedBox(
|
||||
height: 50,
|
||||
child: ListView.builder(
|
||||
|
||||
@@ -314,10 +314,10 @@ class MultiselectGrid extends HookConsumerWidget {
|
||||
final result = await ref.read(albumServiceProvider).createAlbumWithGeneratedName(assets);
|
||||
|
||||
if (result != null) {
|
||||
ref.watch(albumProvider.notifier).refreshRemoteAlbums();
|
||||
unawaited(ref.watch(albumProvider.notifier).refreshRemoteAlbums());
|
||||
selectionEnabledHook.value = false;
|
||||
|
||||
context.pushRoute(AlbumViewerRoute(albumId: result.id));
|
||||
unawaited(context.pushRoute(AlbumViewerRoute(albumId: result.id)));
|
||||
}
|
||||
} finally {
|
||||
processing.value = false;
|
||||
@@ -346,7 +346,7 @@ class MultiselectGrid extends HookConsumerWidget {
|
||||
);
|
||||
|
||||
if (remoteAssets.isNotEmpty) {
|
||||
handleEditDateTime(ref, context, remoteAssets.toList());
|
||||
unawaited(handleEditDateTime(ref, context, remoteAssets.toList()));
|
||||
}
|
||||
} finally {
|
||||
selectionEnabledHook.value = false;
|
||||
@@ -361,7 +361,7 @@ class MultiselectGrid extends HookConsumerWidget {
|
||||
);
|
||||
|
||||
if (remoteAssets.isNotEmpty) {
|
||||
handleEditLocation(ref, context, remoteAssets.toList());
|
||||
unawaited(handleEditLocation(ref, context, remoteAssets.toList()));
|
||||
}
|
||||
} finally {
|
||||
selectionEnabledHook.value = false;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
@@ -81,7 +82,7 @@ class BottomGalleryBar extends ConsumerWidget {
|
||||
// to not throw the error when the next preCache index is called
|
||||
if (totalAssets.value == 1 || assetIndex.value == totalAssets.value - 1) {
|
||||
// Handle only one asset
|
||||
context.maybePop();
|
||||
await context.maybePop();
|
||||
}
|
||||
|
||||
totalAssets.value -= 1;
|
||||
@@ -111,18 +112,20 @@ class BottomGalleryBar extends ConsumerWidget {
|
||||
}
|
||||
|
||||
// Asset is permanently removed
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (BuildContext _) {
|
||||
return DeleteDialog(
|
||||
onDelete: () async {
|
||||
final isDeleted = await onDelete(true);
|
||||
if (isDeleted) {
|
||||
removeAssetFromStack();
|
||||
}
|
||||
},
|
||||
);
|
||||
},
|
||||
unawaited(
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (BuildContext _) {
|
||||
return DeleteDialog(
|
||||
onDelete: () async {
|
||||
final isDeleted = await onDelete(true);
|
||||
if (isDeleted) {
|
||||
removeAssetFromStack();
|
||||
}
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -150,7 +153,7 @@ class BottomGalleryBar extends ConsumerWidget {
|
||||
onTap: () async {
|
||||
await unStack();
|
||||
ctx.pop();
|
||||
context.maybePop();
|
||||
await context.maybePop();
|
||||
},
|
||||
title: const Text("viewer_unstack", style: TextStyle(fontWeight: FontWeight.bold)).tr(),
|
||||
),
|
||||
@@ -178,9 +181,11 @@ class BottomGalleryBar extends ConsumerWidget {
|
||||
void handleEdit() async {
|
||||
final image = Image(image: ImmichImage.imageProvider(asset: asset));
|
||||
|
||||
context.navigator.push(
|
||||
MaterialPageRoute(
|
||||
builder: (context) => EditImagePage(asset: asset, image: image, isEdited: false),
|
||||
unawaited(
|
||||
context.navigator.push(
|
||||
MaterialPageRoute(
|
||||
builder: (context) => EditImagePage(asset: asset, image: image, isEdited: false),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
@@ -93,7 +95,7 @@ class CastDialog extends ConsumerWidget {
|
||||
}
|
||||
|
||||
if (!isCurrentDevice(deviceName)) {
|
||||
ref.read(castProvider.notifier).connect(type, deviceObj);
|
||||
unawaited(ref.read(castProvider.notifier).connect(type, deviceObj));
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:immich_mobile/domain/models/exif.model.dart';
|
||||
import 'package:immich_mobile/utils/debug_print.dart';
|
||||
import 'package:immich_mobile/widgets/map/map_thumbnail.dart';
|
||||
import 'package:maplibre_gl/maplibre_gl.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
import 'package:immich_mobile/utils/debug_print.dart';
|
||||
|
||||
class ExifMap extends StatelessWidget {
|
||||
final ExifInfo exifInfo;
|
||||
@@ -68,7 +69,7 @@ class ExifMap extends StatelessWidget {
|
||||
}
|
||||
|
||||
dPrint(() => 'Opening Map Uri: $uri');
|
||||
launchUrl(uri);
|
||||
unawaited(launchUrl(uri));
|
||||
},
|
||||
onCreated: onMapCreated,
|
||||
);
|
||||
|
||||
@@ -61,7 +61,7 @@ class VideoPosition extends HookConsumerWidget {
|
||||
return;
|
||||
}
|
||||
|
||||
ref.read(videoPlayerControlsProvider.notifier).position = seekToDuration.inSeconds.toDouble();
|
||||
ref.read(videoPlayerControlsProvider.notifier).position = seekToDuration;
|
||||
|
||||
// This immediately updates the slider position without waiting for the video to update
|
||||
ref.read(videoPlaybackValueProvider.notifier).position = seekToDuration;
|
||||
|
||||
@@ -1,19 +1,21 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart' hide Store;
|
||||
import 'package:immich_mobile/entities/store.entity.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/entities/store.entity.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/asset.provider.dart';
|
||||
import 'package:immich_mobile/providers/auth.provider.dart';
|
||||
import 'package:immich_mobile/providers/backup/backup.provider.dart';
|
||||
import 'package:immich_mobile/providers/backup/manual_upload.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/readonly_mode.provider.dart';
|
||||
import 'package:immich_mobile/providers/locale_provider.dart';
|
||||
import 'package:immich_mobile/providers/user.provider.dart';
|
||||
import 'package:immich_mobile/providers/websocket.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/readonly_mode.provider.dart';
|
||||
import 'package:immich_mobile/routing/router.dart';
|
||||
import 'package:immich_mobile/utils/bytes_units.dart';
|
||||
import 'package:immich_mobile/widgets/common/app_bar_dialog/app_bar_profile_info.dart';
|
||||
@@ -44,19 +46,24 @@ class ImmichAppBarDialog extends HookConsumerWidget {
|
||||
}, []);
|
||||
|
||||
buildTopRow() {
|
||||
return Stack(
|
||||
children: [
|
||||
Align(
|
||||
alignment: Alignment.topLeft,
|
||||
child: InkWell(onTap: () => context.pop(), child: const Icon(Icons.close, size: 20)),
|
||||
),
|
||||
Center(
|
||||
child: Image.asset(
|
||||
context.isDarkTheme ? 'assets/immich-text-dark.png' : 'assets/immich-text-light.png',
|
||||
height: 16,
|
||||
return SizedBox(
|
||||
height: 56,
|
||||
child: Stack(
|
||||
alignment: Alignment.centerLeft,
|
||||
children: [
|
||||
IconButton(onPressed: () => context.pop(), icon: const Icon(Icons.close, size: 20)),
|
||||
Align(
|
||||
alignment: Alignment.center,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(bottom: 4),
|
||||
child: Image.asset(
|
||||
context.isDarkTheme ? 'assets/immich-text-dark.png' : 'assets/immich-text-light.png',
|
||||
height: 16,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -97,25 +104,27 @@ class ImmichAppBarDialog extends HookConsumerWidget {
|
||||
return;
|
||||
}
|
||||
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (BuildContext ctx) {
|
||||
return ConfirmDialog(
|
||||
title: "app_bar_signout_dialog_title",
|
||||
content: "app_bar_signout_dialog_content",
|
||||
ok: "yes",
|
||||
onOk: () async {
|
||||
isLoggingOut.value = true;
|
||||
await ref.read(authProvider.notifier).logout().whenComplete(() => isLoggingOut.value = false);
|
||||
unawaited(
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (BuildContext ctx) {
|
||||
return ConfirmDialog(
|
||||
title: "app_bar_signout_dialog_title",
|
||||
content: "app_bar_signout_dialog_content",
|
||||
ok: "yes",
|
||||
onOk: () async {
|
||||
isLoggingOut.value = true;
|
||||
await ref.read(authProvider.notifier).logout().whenComplete(() => isLoggingOut.value = false);
|
||||
|
||||
ref.read(manualUploadProvider.notifier).cancelBackup();
|
||||
ref.read(backupProvider.notifier).cancelBackup();
|
||||
ref.read(assetProvider.notifier).clearAllAssets();
|
||||
ref.read(websocketProvider.notifier).disconnect();
|
||||
context.replaceRoute(const LoginRoute());
|
||||
},
|
||||
);
|
||||
},
|
||||
ref.read(manualUploadProvider.notifier).cancelBackup();
|
||||
ref.read(backupProvider.notifier).cancelBackup();
|
||||
unawaited(ref.read(assetProvider.notifier).clearAllAssets());
|
||||
ref.read(websocketProvider.notifier).disconnect();
|
||||
unawaited(context.replaceRoute(const LoginRoute()));
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
trailing: isLoggingOut.value
|
||||
@@ -256,7 +265,7 @@ class ImmichAppBarDialog extends HookConsumerWidget {
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Container(padding: const EdgeInsets.all(20), child: buildTopRow()),
|
||||
Container(padding: const EdgeInsets.symmetric(horizontal: 8), child: buildTopRow()),
|
||||
const AppBarProfileInfoBox(),
|
||||
buildStorageInformation(),
|
||||
const AppBarServerInfo(),
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
@@ -6,8 +8,8 @@ import 'package:immich_mobile/entities/store.entity.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/extensions/theme_extensions.dart';
|
||||
import 'package:immich_mobile/providers/auth.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/readonly_mode.provider.dart';
|
||||
import 'package:immich_mobile/providers/backup/backup.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/readonly_mode.provider.dart';
|
||||
import 'package:immich_mobile/providers/upload_profile_image.provider.dart';
|
||||
import 'package:immich_mobile/providers/user.provider.dart';
|
||||
import 'package:immich_mobile/widgets/common/immich_loading_indicator.dart';
|
||||
@@ -54,7 +56,7 @@ class AppBarProfileInfoBox extends HookConsumerWidget {
|
||||
ref.read(currentUserProvider.notifier).refresh();
|
||||
}
|
||||
|
||||
ref.read(backupProvider.notifier).updateDiskInfo();
|
||||
unawaited(ref.read(backupProvider.notifier).updateDiskInfo());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/models/server_info/server_features.model.dart';
|
||||
import 'package:immich_mobile/providers/server_info.provider.dart';
|
||||
|
||||
/// A utility widget that conditionally renders its child based on a server feature flag.
|
||||
///
|
||||
/// Example usage:
|
||||
/// ```dart
|
||||
/// FeatureCheck(
|
||||
/// feature: (features) => features.ocr,
|
||||
/// child: Text('OCR is enabled'),
|
||||
/// fallback: Text('OCR is not available'),
|
||||
/// )
|
||||
/// ```
|
||||
class FeatureCheck extends ConsumerWidget {
|
||||
/// A function that extracts the specific feature flag from ServerFeatures
|
||||
final bool Function(ServerFeatures) feature;
|
||||
|
||||
/// The widget to display when the feature is enabled
|
||||
final Widget child;
|
||||
|
||||
/// Optional widget to display when the feature is disabled
|
||||
final Widget? fallback;
|
||||
|
||||
const FeatureCheck({super.key, required this.feature, required this.child, this.fallback});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final serverFeatures = ref.watch(serverInfoProvider.select((s) => s.serverFeatures));
|
||||
final isFeatureEnabled = feature(serverFeatures);
|
||||
if (isFeatureEnabled) {
|
||||
return child;
|
||||
}
|
||||
|
||||
return fallback ?? const SizedBox.shrink();
|
||||
}
|
||||
}
|
||||
@@ -18,7 +18,6 @@ import 'package:immich_mobile/providers/infrastructure/current_album.provider.da
|
||||
import 'package:immich_mobile/providers/infrastructure/remote_album.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart';
|
||||
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
|
||||
import 'package:immich_mobile/routing/router.dart';
|
||||
import 'package:immich_mobile/widgets/album/remote_album_shared_user_icons.dart';
|
||||
|
||||
class RemoteAlbumSliverAppBar extends ConsumerStatefulWidget {
|
||||
@@ -89,7 +88,7 @@ class _MesmerizingSliverAppBarState extends ConsumerState<RemoteAlbumSliverAppBa
|
||||
color: actionIconColor,
|
||||
shadows: actionIconShadows,
|
||||
),
|
||||
onPressed: () => context.navigateTo(const TabShellRoute(children: [DriftAlbumsRoute()])),
|
||||
onPressed: () => context.maybePop(),
|
||||
),
|
||||
actions: [
|
||||
if (widget.onToggleAlbumOrder != null)
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'dart:math';
|
||||
@@ -188,17 +189,17 @@ class LoginForm extends HookConsumerWidget {
|
||||
final result = await ref.read(authProvider.notifier).login(emailController.text, passwordController.text);
|
||||
|
||||
if (result.shouldChangePassword && !result.isAdmin) {
|
||||
context.pushRoute(const ChangePasswordRoute());
|
||||
unawaited(context.pushRoute(const ChangePasswordRoute()));
|
||||
} else {
|
||||
final isBeta = Store.isBetaTimelineEnabled;
|
||||
if (isBeta) {
|
||||
await ref.read(galleryPermissionNotifier.notifier).requestGalleryPermission();
|
||||
handleSyncFlow();
|
||||
unawaited(handleSyncFlow());
|
||||
ref.read(websocketProvider.notifier).connect();
|
||||
context.replaceRoute(const TabShellRoute());
|
||||
unawaited(context.replaceRoute(const TabShellRoute()));
|
||||
return;
|
||||
}
|
||||
context.replaceRoute(const TabControllerRoute());
|
||||
unawaited(context.replaceRoute(const TabControllerRoute()));
|
||||
}
|
||||
} catch (error) {
|
||||
ImmichToast.show(
|
||||
@@ -288,15 +289,15 @@ class LoginForm extends HookConsumerWidget {
|
||||
final permission = ref.watch(galleryPermissionNotifier);
|
||||
final isBeta = Store.isBetaTimelineEnabled;
|
||||
if (!isBeta && (permission.isGranted || permission.isLimited)) {
|
||||
ref.watch(backupProvider.notifier).resumeBackup();
|
||||
unawaited(ref.watch(backupProvider.notifier).resumeBackup());
|
||||
}
|
||||
if (isBeta) {
|
||||
await ref.read(galleryPermissionNotifier.notifier).requestGalleryPermission();
|
||||
handleSyncFlow();
|
||||
context.replaceRoute(const TabShellRoute());
|
||||
unawaited(handleSyncFlow());
|
||||
unawaited(context.replaceRoute(const TabShellRoute()));
|
||||
return;
|
||||
}
|
||||
context.replaceRoute(const TabControllerRoute());
|
||||
unawaited(context.replaceRoute(const TabControllerRoute()));
|
||||
}
|
||||
} catch (error, stack) {
|
||||
log.severe('Error logging in with OAuth: $error', stack);
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
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/map/map_event.model.dart';
|
||||
import 'package:immich_mobile/widgets/map/map_asset_grid.dart';
|
||||
import 'package:immich_mobile/entities/asset.entity.dart';
|
||||
import 'package:immich_mobile/utils/draggable_scroll_controller.dart';
|
||||
import 'package:immich_mobile/widgets/map/map_asset_grid.dart';
|
||||
|
||||
class MapBottomSheet extends HookConsumerWidget {
|
||||
final Stream<MapEvent> mapEventStream;
|
||||
@@ -34,7 +34,11 @@ class MapBottomSheet extends HookConsumerWidget {
|
||||
|
||||
void handleMapEvents(MapEvent event) async {
|
||||
if (event is MapCloseBottomSheet) {
|
||||
sheetController.animateTo(0.1, duration: const Duration(milliseconds: 200), curve: Curves.linearToEaseOut);
|
||||
await sheetController.animateTo(
|
||||
0.1,
|
||||
duration: const Duration(milliseconds: 200),
|
||||
curve: Curves.linearToEaseOut,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
@@ -42,7 +44,7 @@ class BetaTimelineListTile extends ConsumerWidget {
|
||||
ElevatedButton(
|
||||
onPressed: () async {
|
||||
Navigator.of(context).pop();
|
||||
context.router.replaceAll([ChangeExperienceRoute(switchingToBeta: value)]);
|
||||
unawaited(context.router.replaceAll([ChangeExperienceRoute(switchingToBeta: value)]));
|
||||
},
|
||||
child: Text("ok".t(context: context)),
|
||||
),
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
@@ -102,13 +104,13 @@ class LocalNetworkPreference extends HookConsumerWidget {
|
||||
),
|
||||
);
|
||||
} else {
|
||||
saveWifiName(wifiName);
|
||||
unawaited(saveWifiName(wifiName));
|
||||
}
|
||||
|
||||
final serverEndpoint = ref.read(authProvider.notifier).getServerEndpoint();
|
||||
|
||||
if (serverEndpoint != null) {
|
||||
saveLocalEndpoint(serverEndpoint);
|
||||
unawaited(saveLocalEndpoint(serverEndpoint));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user