Merge branch 'main' into upload-to-album

This commit is contained in:
Alex
2026-05-12 21:00:28 -05:00
committed by GitHub
789 changed files with 38034 additions and 6611 deletions
@@ -50,7 +50,9 @@ class _AddActionButtonState extends ConsumerState<AddActionButton> {
List<Widget> _buildMenuChildren() {
final asset = ref.read(assetViewerProvider).currentAsset;
if (asset == null) return [];
if (asset == null) {
return [];
}
final user = ref.read(currentUserProvider);
final isOwner = asset is RemoteAsset && asset.ownerId == user?.id;
@@ -12,7 +12,9 @@ import 'package:immich_mobile/widgets/common/immich_toast.dart';
// used to allow performing archive action from different sources (without duplicating code)
Future<void> performArchiveAction(BuildContext context, WidgetRef ref, {required ActionSource source}) async {
if (!context.mounted) return;
if (!context.mounted) {
return;
}
if (source == ActionSource.viewer) {
EventStream.shared.emit(const ViewerReloadAssetEvent());
@@ -54,7 +54,9 @@ class DeleteActionButton extends ConsumerWidget {
],
),
);
if (confirm != true) return;
if (confirm != true) {
return;
}
}
if (source == ActionSource.viewer) {
@@ -33,7 +33,9 @@ class DeletePermanentActionButton extends ConsumerWidget {
builder: (context) => PermanentDeleteDialog(count: count),
) ??
false;
if (!confirm) return;
if (!confirm) {
return;
}
if (source == ActionSource.viewer) {
EventStream.shared.emit(const ViewerReloadAssetEvent());
@@ -12,7 +12,9 @@ import 'package:immich_mobile/widgets/common/immich_toast.dart';
// Reusable helper: move to locked folder from any source (e.g called from menu)
Future<void> performMoveToLockFolderAction(BuildContext context, WidgetRef ref, {required ActionSource source}) async {
if (!context.mounted) return;
if (!context.mounted) {
return;
}
if (source == ActionSource.viewer) {
EventStream.shared.emit(const ViewerReloadAssetEvent());
@@ -12,7 +12,6 @@ class OpenInBrowserActionButton extends ConsumerWidget {
final TimelineOrigin origin;
final bool iconOnly;
final bool menuItem;
final Color? iconColor;
const OpenInBrowserActionButton({
super.key,
@@ -20,7 +19,6 @@ class OpenInBrowserActionButton extends ConsumerWidget {
required this.origin,
this.iconOnly = false,
this.menuItem = false,
this.iconColor,
});
void _onTap() async {
@@ -52,7 +50,6 @@ class OpenInBrowserActionButton extends ConsumerWidget {
return BaseActionButton(
label: 'open_in_browser'.t(context: context),
iconData: Icons.open_in_browser,
iconColor: iconColor,
iconOnly: iconOnly,
menuItem: menuItem,
onPressed: _onTap,
@@ -37,7 +37,7 @@ class SimilarPhotosActionButton extends ConsumerWidget {
date: SearchDateFilter(),
display: SearchDisplayFilters(isNotInAlbum: false, isArchive: false, isFavorite: false),
rating: SearchRatingFilter(),
mediaType: AssetType.image,
mediaType: AssetType.other,
),
);
@@ -14,7 +14,9 @@ import 'package:immich_mobile/domain/utils/event_stream.dart';
// used to allow performing unarchive action from different sources (without duplicating code)
Future<void> performUnArchiveAction(BuildContext context, WidgetRef ref, {required ActionSource source}) async {
if (!context.mounted) return;
if (!context.mounted) {
return;
}
if (source == ActionSource.viewer) {
EventStream.shared.emit(const ViewerReloadAssetEvent());
@@ -26,6 +26,14 @@ class AssetDetails extends ConsumerWidget {
decoration: BoxDecoration(
color: context.colorScheme.surface,
borderRadius: const BorderRadius.vertical(top: Radius.circular(20)),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.2),
offset: const Offset(0, -3),
blurRadius: 10,
spreadRadius: 2,
),
],
),
child: SafeArea(
top: false,
@@ -21,21 +21,27 @@ class AppearsInDetails extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
if (!asset.hasRemote) return const SizedBox.shrink();
if (!asset.hasRemote) {
return const SizedBox.shrink();
}
final remoteAssetId = switch (asset) {
RemoteAsset(:final id) => id,
LocalAsset(:final remoteAssetId) => remoteAssetId,
};
if (remoteAssetId == null) return const SizedBox.shrink();
if (remoteAssetId == null) {
return const SizedBox.shrink();
}
final userId = ref.watch(currentUserProvider)?.id;
final assetAlbums = ref.watch(albumsContainingAssetProvider(remoteAssetId));
return assetAlbums.when(
data: (albums) {
if (albums.isEmpty) return const SizedBox.shrink();
if (albums.isEmpty) {
return const SizedBox.shrink();
}
albums.sortBy((a) => a.name);
@@ -20,7 +20,9 @@ class RatingDetails extends ConsumerWidget {
.watch(userMetadataPreferencesProvider)
.maybeWhen(data: (prefs) => prefs?.ratingsEnabled ?? false, orElse: () => false);
if (!isRatingEnabled) return const SizedBox.shrink();
if (!isRatingEnabled) {
return const SizedBox.shrink();
}
return Padding(
padding: const EdgeInsets.only(left: 16.0, top: 16.0),
@@ -22,6 +22,7 @@ class TechnicalDetails extends ConsumerWidget {
final exifInfo = this.exifInfo;
final cameraTitle = _getCameraInfoTitle(exifInfo);
final lensTitle = exifInfo?.lens != null && exifInfo!.lens!.isNotEmpty ? exifInfo.lens : null;
final lensSubtitle = _getLensInfoSubtitle(exifInfo);
return Column(
children: [
@@ -46,9 +47,16 @@ class TechnicalDetails extends ConsumerWidget {
title: lensTitle,
titleStyle: context.textTheme.labelLarge,
leading: Icon(Icons.camera_outlined, size: 24, color: context.textTheme.labelLarge?.color),
subtitle: _getLensInfoSubtitle(exifInfo),
subtitle: lensSubtitle,
subtitleStyle: context.textTheme.bodyMedium?.copyWith(color: context.colorScheme.onSurfaceSecondary),
),
] else if (lensSubtitle != null) ...[
const SizedBox(height: 16),
SheetTile(
title: lensSubtitle,
titleStyle: context.textTheme.bodyMedium?.copyWith(color: context.colorScheme.onSurfaceSecondary),
leading: Icon(Icons.camera_outlined, size: 24, color: context.textTheme.labelLarge?.color),
),
],
],
);
@@ -103,7 +111,9 @@ class TechnicalDetails extends ConsumerWidget {
}
static String? _getCameraInfoTitle(ExifInfo? exifInfo) {
if (exifInfo == null) return null;
if (exifInfo == null) {
return null;
}
return switch ((exifInfo.make, exifInfo.model)) {
(null, null) => null,
(String make, null) => make,
@@ -113,16 +123,23 @@ class TechnicalDetails extends ConsumerWidget {
}
static String? _getCameraInfoSubtitle(ExifInfo? exifInfo) {
if (exifInfo == null) return null;
if (exifInfo == null) {
return null;
}
final exposureTime = exifInfo.exposureTime.isNotEmpty ? exifInfo.exposureTime : null;
final iso = exifInfo.iso != null ? 'ISO ${exifInfo.iso}' : null;
return [exposureTime, iso].where((spec) => spec != null && spec.isNotEmpty).join(_kSeparator);
}
static String? _getLensInfoSubtitle(ExifInfo? exifInfo) {
if (exifInfo == null) return null;
if (exifInfo == null) {
return null;
}
final fNumber = exifInfo.fNumber.isNotEmpty ? 'ƒ/${exifInfo.fNumber}' : null;
final focalLength = exifInfo.focalLength.isNotEmpty ? '${exifInfo.focalLength} mm' : null;
if (fNumber == null && focalLength == null) {
return null;
}
return [fNumber, focalLength].where((spec) => spec != null && spec.isNotEmpty).join(_kSeparator);
}
}
@@ -14,14 +14,14 @@ import 'package:immich_mobile/extensions/scroll_extensions.dart';
import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_details.widget.dart';
import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_stack.provider.dart';
import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_stack.widget.dart';
import 'package:immich_mobile/providers/asset_viewer/asset_viewer.provider.dart';
import 'package:immich_mobile/presentation/widgets/asset_viewer/video_viewer.widget.dart';
import 'package:immich_mobile/presentation/widgets/images/image_provider.dart';
import 'package:immich_mobile/presentation/widgets/images/thumbnail.widget.dart';
import 'package:immich_mobile/providers/app_settings.provider.dart';
import 'package:immich_mobile/providers/asset_viewer/asset_viewer.provider.dart';
import 'package:immich_mobile/providers/asset_viewer/is_motion_video_playing.provider.dart';
import 'package:immich_mobile/services/app_settings.service.dart';
import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart';
import 'package:immich_mobile/services/app_settings.service.dart';
import 'package:immich_mobile/widgets/common/immich_loading_indicator.dart';
import 'package:immich_mobile/widgets/photo_view/photo_view.dart';
@@ -62,7 +62,9 @@ class _AssetPageState extends ConsumerState<AssetPage> {
super.initState();
_eventSubscription = EventStream.shared.listen(_onEvent);
WidgetsBinding.instance.addPostFrameCallback((_) {
if (!mounted || !_scrollController.hasClients) return;
if (!mounted || !_scrollController.hasClients) {
return;
}
_scrollController.snapPosition.snapOffset = _snapOffset;
if (_showingDetails && _snapOffset > 0) {
_scrollController.jumpTo(_snapOffset);
@@ -87,7 +89,9 @@ class _AssetPageState extends ConsumerState<AssetPage> {
}
void _showDetails() {
if (!_scrollController.hasClients || _snapOffset <= 0) return;
if (!_scrollController.hasClients || _snapOffset <= 0) {
return;
}
_viewer.setShowingDetails(true);
_scrollController.animateTo(_snapOffset, duration: Durations.medium2, curve: Curves.easeOutCubic);
}
@@ -128,7 +132,9 @@ class _AssetPageState extends ConsumerState<AssetPage> {
}
void _updateDrag(DragUpdateDetails details) {
if (_dragStart == null) return;
if (_dragStart == null) {
return;
}
if (_dragIntent == _DragIntent.none) {
_dragIntent = switch ((details.globalPosition - _dragStart!.globalPosition).dy) {
@@ -141,7 +147,9 @@ class _AssetPageState extends ConsumerState<AssetPage> {
switch (_dragIntent) {
case _DragIntent.none:
case _DragIntent.scroll:
if (_drag == null) _startProxyDrag();
if (_drag == null) {
_startProxyDrag();
}
_drag?.update(details);
_syncShowingDetails();
@@ -151,7 +159,9 @@ class _AssetPageState extends ConsumerState<AssetPage> {
}
void _endDrag(DragEndDetails details) {
if (_dragStart == null) return;
if (_dragStart == null) {
return;
}
final start = _dragStart;
_dragStart = null;
@@ -188,7 +198,9 @@ class _AssetPageState extends ConsumerState<AssetPage> {
PhotoViewControllerBase controller,
PhotoViewScaleStateController scaleStateController,
) {
if (!_showingDetails && _isZoomed) return;
if (!_showingDetails && _isZoomed) {
return;
}
_beginDrag(details);
}
@@ -215,7 +227,9 @@ class _AssetPageState extends ConsumerState<AssetPage> {
}
void _onTapUp(BuildContext context, TapUpDetails details, PhotoViewControllerValue controllerValue) {
if (_showingDetails || _dragStart != null) return;
if (_showingDetails || _dragStart != null) {
return;
}
final tapToNavigate = ref.read(appSettingsServiceProvider).getSetting<bool>(AppSettingsEnum.tapToNavigate);
if (!tapToNavigate) {
@@ -247,31 +261,43 @@ class _AssetPageState extends ConsumerState<AssetPage> {
_viewer.setZoomed(_isZoomed);
if (scaleState != PhotoViewScaleState.initial) {
if (_dragStart == null) _viewer.setControls(false);
if (_dragStart == null) {
_viewer.setControls(false);
}
return;
}
if (!_showingDetails) _viewer.setControls(true);
if (!_showingDetails) {
_viewer.setControls(true);
}
}
void _listenForScaleBoundaries(PhotoViewControllerBase? controller) {
_scaleBoundarySub?.cancel();
_scaleBoundarySub = null;
if (controller == null || controller.scaleBoundaries != null) return;
if (controller == null || controller.scaleBoundaries != null) {
return;
}
_scaleBoundarySub = controller.outputStateStream.listen((_) {
if (controller.scaleBoundaries != null) {
_scaleBoundarySub?.cancel();
_scaleBoundarySub = null;
if (mounted) setState(() {});
if (mounted) {
setState(() {});
}
}
});
}
double _getImageHeight(double maxWidth, double maxHeight, BaseAsset? asset) {
final sb = _viewController?.scaleBoundaries;
if (sb != null) return sb.childSize.height * sb.initialScale;
if (sb != null) {
return sb.childSize.height * sb.initialScale;
}
if (asset == null || asset.width == null || asset.height == null) return maxHeight;
if (asset == null || asset.width == null || asset.height == null) {
return maxHeight;
}
final r = asset.width! / asset.height!;
return math.min(maxWidth / r, maxHeight);
@@ -21,12 +21,16 @@ class AssetPreloader {
unawaited(timelineService.preloadAssets(index));
_timer?.cancel();
_timer = Timer(Durations.medium4, () async {
if (!mounted()) return;
if (!mounted()) {
return;
}
final (prev, next) = await (
timelineService.getAssetAsync(index - 1),
timelineService.getAssetAsync(index + 1),
).wait;
if (!mounted()) return;
if (!mounted()) {
return;
}
_prevStream?.removeListener(_dummyListener);
_nextStream?.removeListener(_dummyListener);
_prevStream = prev != null ? _resolveImage(prev, size) : null;
@@ -17,9 +17,9 @@ import 'package:immich_mobile/presentation/widgets/action_buttons/download_statu
import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_page.widget.dart';
import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_preloader.dart';
import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_stack.provider.dart';
import 'package:immich_mobile/providers/asset_viewer/asset_viewer.provider.dart';
import 'package:immich_mobile/presentation/widgets/asset_viewer/viewer_top_app_bar.widget.dart';
import 'package:immich_mobile/presentation/widgets/asset_viewer/viewer_bottom_app_bar.widget.dart';
import 'package:immich_mobile/presentation/widgets/asset_viewer/viewer_top_app_bar.widget.dart';
import 'package:immich_mobile/providers/asset_viewer/asset_viewer.provider.dart';
import 'package:immich_mobile/providers/cast.provider.dart';
import 'package:immich_mobile/providers/infrastructure/current_album.provider.dart';
import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart';
@@ -67,7 +67,9 @@ class AssetViewer extends ConsumerStatefulWidget {
ref.read(assetViewerProvider.notifier).reset();
// Hide controls by default for videos
if (asset.isVideo) ref.read(assetViewerProvider.notifier).setControls(false);
if (asset.isVideo) {
ref.read(assetViewerProvider.notifier).setControls(false);
}
_setAsset(ref, asset);
}
@@ -90,7 +92,9 @@ class _AssetViewerState extends ConsumerState<AssetViewer> {
void _onTapNavigate(int direction) {
final page = _pageController.page?.toInt();
if (page == null) return;
if (page == null) {
return;
}
final target = page + direction;
final maxPage = _totalAssets - 1;
if (target >= 0 && target <= maxPage) {
@@ -105,7 +109,9 @@ class _AssetViewerState extends ConsumerState<AssetViewer> {
final asset = ref.read(assetViewerProvider).currentAsset;
assert(asset != null, "Current asset should not be null when opening the AssetViewer");
if (asset != null) _stackChildrenKeepAlive = ref.read(stackChildrenNotifier(asset).notifier).ref.keepAlive();
if (asset != null) {
_stackChildrenKeepAlive = ref.read(stackChildrenNotifier(asset).notifier).ref.keepAlive();
}
_reloadSubscription = EventStream.shared.listen(_onEvent);
@@ -137,7 +143,9 @@ class _AssetViewerState extends ConsumerState<AssetViewer> {
// playing, and preventing the video on the next page from becoming ready
// unnecessarily.
bool _onScrollEnd(ScrollEndNotification notification) {
if (notification.depth != 0) return false;
if (notification.depth != 0) {
return false;
}
final page = _pageController.page?.round();
if (page != null && page != _currentPage) {
@@ -155,7 +163,9 @@ class _AssetViewerState extends ConsumerState<AssetViewer> {
_currentPage = index;
final asset = await ref.read(timelineServiceProvider).getAssetAsync(index);
if (asset == null) return;
if (asset == null) {
return;
}
AssetViewer._setAsset(ref, asset);
_preloader.preload(index, context.sizeData);
@@ -165,9 +175,13 @@ class _AssetViewerState extends ConsumerState<AssetViewer> {
}
void _handleCasting() {
if (!ref.read(castProvider).isCasting) return;
if (!ref.read(castProvider).isCasting) {
return;
}
final asset = ref.read(assetViewerProvider).currentAsset;
if (asset == null) return;
if (asset == null) {
return;
}
if (asset is RemoteAsset) {
context.scaffoldMessenger.hideCurrentSnackBar();
@@ -199,7 +213,9 @@ class _AssetViewerState extends ConsumerState<AssetViewer> {
}
void _onViewerReloadEvent() {
if (_totalAssets <= 1) return;
if (_totalAssets <= 1) {
return;
}
final index = _pageController.page?.round() ?? 0;
final target = index >= _totalAssets - 1 ? index - 1 : index + 1;
@@ -252,7 +268,9 @@ class _AssetViewerState extends ConsumerState<AssetViewer> {
// Listen for casting changes and send initial asset to the cast provider
ref.listen(castProvider.select((value) => value.isCasting), (_, isCasting) {
if (!isCasting) return;
if (!isCasting) {
return;
}
WidgetsBinding.instance.addPostFrameCallback((_) {
_handleCasting();
});
@@ -65,26 +65,37 @@ class ViewerBottomBar extends ConsumerWidget {
labelLarge: context.themeData.textTheme.labelLarge?.copyWith(color: Colors.white),
),
),
child: Container(
decoration: const BoxDecoration(
gradient: LinearGradient(
begin: Alignment.bottomCenter,
end: Alignment.topCenter,
colors: [Colors.black45, Colors.black12, Colors.transparent],
stops: [0.0, 0.7, 1.0],
child: Stack(
children: [
const Positioned.fill(
child: IgnorePointer(
child: DecoratedBox(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.bottomCenter,
end: Alignment.topCenter,
colors: [Colors.black45, Colors.black12, Colors.transparent],
stops: [0.0, 0.7, 1.0],
),
),
),
),
),
),
child: SafeArea(
top: false,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
if (asset.isVideo) VideoControls(videoPlayerName: asset.heroTag),
if (!isReadonlyModeEnabled)
Row(mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: actions),
],
SafeArea(
top: false,
child: Padding(
padding: const EdgeInsets.only(top: 16),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
if (asset.isVideo) VideoControls(videoPlayerName: asset.heroTag),
if (!isReadonlyModeEnabled)
Row(mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: actions),
],
),
),
),
),
],
),
),
);
@@ -53,7 +53,9 @@ class _RatingBarState extends State<RatingBar> {
final totalWidth = widget.itemCount * widget.itemSize + (widget.itemCount - 1) * widget.starPadding;
double dx = localPosition.dx;
if (isRTL) dx = totalWidth - dx;
if (isRTL) {
dx = totalWidth - dx;
}
double newRating;
@@ -9,8 +9,8 @@ import 'package:immich_mobile/domain/services/setting.service.dart';
import 'package:immich_mobile/entities/store.entity.dart';
import 'package:immich_mobile/extensions/platform_extensions.dart';
import 'package:immich_mobile/infrastructure/repositories/storage.repository.dart';
import 'package:immich_mobile/providers/asset_viewer/asset_viewer.provider.dart';
import 'package:immich_mobile/providers/app_settings.provider.dart';
import 'package:immich_mobile/providers/asset_viewer/asset_viewer.provider.dart';
import 'package:immich_mobile/providers/asset_viewer/is_motion_video_playing.provider.dart';
import 'package:immich_mobile/providers/asset_viewer/video_player_provider.dart';
import 'package:immich_mobile/providers/cast.provider.dart';
@@ -61,7 +61,9 @@ class _NativeVideoViewerState extends ConsumerState<NativeVideoViewer> with Widg
void didUpdateWidget(NativeVideoViewer oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.isCurrent == oldWidget.isCurrent || _controller == null) return;
if (widget.isCurrent == oldWidget.isCurrent || _controller == null) {
return;
}
if (!widget.isCurrent) {
_loadTimer?.cancel();
@@ -85,25 +87,35 @@ class _NativeVideoViewerState extends ConsumerState<NativeVideoViewer> with Widg
void didChangeAppLifecycleState(AppLifecycleState state) async {
switch (state) {
case AppLifecycleState.resumed:
if (_shouldPlayOnForeground) await _notifier.play();
if (_shouldPlayOnForeground) {
await _notifier.play();
}
case AppLifecycleState.paused:
_shouldPlayOnForeground = await _controller?.isPlaying() ?? true;
if (_shouldPlayOnForeground) await _notifier.pause();
if (_shouldPlayOnForeground) {
await _notifier.pause();
}
default:
}
}
Future<VideoSource?> _createSource() async {
if (!mounted) return null;
if (!mounted) {
return null;
}
final videoAsset = await ref.read(assetServiceProvider).getAsset(widget.asset) ?? widget.asset;
if (!mounted) return null;
if (!mounted) {
return null;
}
try {
if (videoAsset.hasLocal && videoAsset.livePhotoVideoId == null) {
final id = videoAsset is LocalAsset ? videoAsset.id : (videoAsset as RemoteAsset).localId!;
final file = await StorageRepository().getFileForAsset(id);
if (!mounted) return null;
if (!mounted) {
return null;
}
if (file == null) {
throw Exception('No file found for the video');
@@ -134,25 +146,35 @@ class _NativeVideoViewerState extends ConsumerState<NativeVideoViewer> with Widg
}
void _onPlaybackReady() async {
if (!mounted || !widget.isCurrent) return;
if (!mounted || !widget.isCurrent) {
return;
}
_notifier.onNativePlaybackReady();
// onPlaybackReady may be called multiple times, usually when more data
// loads. If this is not the first time that the player has become ready, we
// should not autoplay.
if (_isVideoReady) return;
if (_isVideoReady) {
return;
}
setState(() => _isVideoReady = true);
if (ref.read(assetViewerProvider).showingDetails) return;
if (ref.read(assetViewerProvider).showingDetails) {
return;
}
final autoPlayVideo = AppSetting.get(Setting.autoPlayVideo);
if (autoPlayVideo || widget.asset.isMotionPhoto) await _notifier.play();
if (autoPlayVideo || widget.asset.isMotionPhoto) {
await _notifier.play();
}
}
void _onPlaybackEnded() {
if (!mounted) return;
if (!mounted) {
return;
}
_notifier.onNativePlaybackEnded();
@@ -162,12 +184,16 @@ class _NativeVideoViewerState extends ConsumerState<NativeVideoViewer> with Widg
}
void _onPlaybackPositionChanged() {
if (!mounted) return;
if (!mounted) {
return;
}
_notifier.onNativePositionChanged();
}
void _onPlaybackStatusChanged() {
if (!mounted) return;
if (!mounted) {
return;
}
_notifier.onNativeStatusChanged();
}
@@ -180,10 +206,14 @@ class _NativeVideoViewerState extends ConsumerState<NativeVideoViewer> with Widg
void _loadVideo() async {
final nc = _controller;
if (nc == null || nc.videoSource != null || !mounted) return;
if (nc == null || nc.videoSource != null || !mounted) {
return;
}
final source = await _videoSource;
if (source == null || !mounted) return;
if (source == null || !mounted) {
return;
}
await _notifier.load(source);
final loopVideo = ref.read(appSettingsServiceProvider).getSetting<bool>(AppSettingsEnum.loopVideo);
@@ -192,7 +222,9 @@ class _NativeVideoViewerState extends ConsumerState<NativeVideoViewer> with Widg
}
void _initController(NativeVideoPlayerController nc) {
if (_controller != null || !mounted) return;
if (_controller != null || !mounted) {
return;
}
_notifier.attachController(nc);
@@ -203,7 +235,9 @@ class _NativeVideoViewerState extends ConsumerState<NativeVideoViewer> with Widg
_controller = nc;
if (widget.isCurrent) _loadVideo();
if (widget.isCurrent) {
_loadVideo();
}
}
@override
@@ -4,8 +4,8 @@ import 'package:immich_mobile/constants/enums.dart';
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
import 'package:immich_mobile/domain/models/setting.model.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/providers/cast.provider.dart';
import 'package:immich_mobile/providers/asset_viewer/asset_viewer.provider.dart';
import 'package:immich_mobile/providers/cast.provider.dart';
import 'package:immich_mobile/providers/infrastructure/current_album.provider.dart';
import 'package:immich_mobile/providers/infrastructure/setting.provider.dart';
import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart';
@@ -48,7 +48,6 @@ class ViewerKebabMenu extends ConsumerWidget {
source: ActionSource.viewer,
isCasting: isCasting,
timelineOrigin: timelineOrigin,
originalTheme: originalTheme,
);
final menuChildren = ActionButtonBuilder.buildViewerKebabMenu(actionContext, context, ref);
@@ -67,10 +66,13 @@ class ViewerKebabMenu extends ConsumerWidget {
menuChildren: [
ConstrainedBox(
constraints: const BoxConstraints(minWidth: 150),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: menuChildren,
child: Theme(
data: originalTheme ?? context.themeData,
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: menuChildren,
),
),
),
],
@@ -75,29 +75,41 @@ class ViewerTopAppBar extends ConsumerWidget implements PreferredSizeWidget {
child: AnimatedOpacity(
opacity: opacity,
duration: Durations.short2,
child: DecoratedBox(
decoration: BoxDecoration(
gradient: showingDetails
? null
: const LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [Colors.black45, Colors.black12, Colors.transparent],
stops: [0.0, 0.7, 1.0],
child: Stack(
children: [
Positioned.fill(
child: IgnorePointer(
child: DecoratedBox(
decoration: BoxDecoration(
gradient: showingDetails
? null
: const LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [Colors.black45, Colors.black12, Colors.transparent],
stops: [0.0, 0.7, 1.0],
),
),
),
child: AppBar(
backgroundColor: Colors.transparent,
leading: const _AppBarBackButton(),
iconTheme: const IconThemeData(size: 22, color: Colors.white),
actionsIconTheme: const IconThemeData(size: 22, color: Colors.white),
shape: const Border(),
actions: showingDetails || isReadonlyModeEnabled
? null
: isInLockedView
? lockedViewActions
: actions,
),
),
),
),
SafeArea(
bottom: false,
child: SizedBox.square(
child: Theme(
data: context.themeData.copyWith(iconTheme: const IconThemeData(size: 22, color: Colors.white)),
child: Row(
children: [
const _AppBarBackButton(),
const Spacer(),
if (!showingDetails && !isReadonlyModeEnabled)
if (isInLockedView) ...lockedViewActions else ...actions,
],
),
),
),
),
],
),
),
);
@@ -113,20 +125,17 @@ class _AppBarBackButton extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final showingDetails = ref.watch(assetViewerProvider.select((state) => state.showingDetails));
return Padding(
padding: const EdgeInsets.only(left: 12.0),
child: ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: showingDetails ? context.colorScheme.surface : Colors.transparent,
shape: const CircleBorder(),
iconSize: 22,
iconColor: showingDetails ? context.colorScheme.onSurface : Colors.white,
padding: EdgeInsets.zero,
elevation: showingDetails ? 4 : 0,
),
onPressed: context.maybePop,
child: const Icon(Icons.arrow_back_rounded),
return ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: showingDetails ? context.colorScheme.surface : Colors.transparent,
shape: const CircleBorder(),
iconSize: 22,
iconColor: showingDetails ? context.colorScheme.onSurface : Colors.white,
padding: const EdgeInsets.all(10.0),
elevation: showingDetails ? 4 : 0,
),
onPressed: context.maybePop,
child: const Icon(Icons.arrow_back_rounded),
);
}
}
@@ -3,9 +3,8 @@ import 'dart:ui' as ui;
import 'package:async/async.dart';
import 'package:flutter/widgets.dart';
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
import 'package:immich_mobile/domain/models/setting.model.dart';
import 'package:immich_mobile/domain/services/setting.service.dart';
import 'package:immich_mobile/infrastructure/loaders/image_request.dart';
import 'package:immich_mobile/infrastructure/repositories/metadata.repository.dart';
import 'package:immich_mobile/presentation/widgets/images/local_image_provider.dart';
import 'package:immich_mobile/presentation/widgets/images/remote_image_provider.dart';
import 'package:immich_mobile/presentation/widgets/timeline/constants.dart';
@@ -189,4 +188,6 @@ ImageProvider? getThumbnailImageProvider(BaseAsset asset, {Size size = kThumbnai
}
bool _shouldUseLocalAsset(BaseAsset asset) =>
asset.hasLocal && (!asset.hasRemote || !AppSetting.get(Setting.preferRemoteImage)) && !asset.isEdited;
asset.hasLocal &&
(!asset.hasRemote || !MetadataRepository.instance.appConfig.image.preferRemote) &&
!asset.isEdited;
@@ -1,9 +1,8 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart';
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
import 'package:immich_mobile/domain/models/store.model.dart';
import 'package:immich_mobile/entities/store.entity.dart';
import 'package:immich_mobile/infrastructure/loaders/image_request.dart';
import 'package:immich_mobile/infrastructure/repositories/metadata.repository.dart';
import 'package:immich_mobile/presentation/widgets/images/animated_image_stream_completer.dart';
import 'package:immich_mobile/presentation/widgets/images/image_provider.dart';
import 'package:immich_mobile/presentation/widgets/images/one_frame_multi_image_stream_completer.dart';
@@ -41,7 +40,9 @@ class LocalThumbProvider extends CancellableImageProvider<LocalThumbProvider>
@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
if (identical(this, other)) {
return true;
}
if (other is LocalThumbProvider) {
return id == other.id;
}
@@ -103,7 +104,7 @@ class LocalFullImageProvider extends CancellableImageProvider<LocalFullImageProv
return;
}
final loadOriginal = Store.get(StoreKey.loadOriginal, false);
final loadOriginal = MetadataRepository.instance.appConfig.image.loadOriginal;
final devicePixelRatio = PlatformDispatcher.instance.views.first.devicePixelRatio;
var request = this.request = LocalImageRequest(
localId: key.id,
@@ -148,7 +149,9 @@ class LocalFullImageProvider extends CancellableImageProvider<LocalFullImageProv
final originalRequest = request = LocalImageRequest(localId: key.id, size: Size.zero, assetType: key.assetType);
final codec = await loadCodecRequest(originalRequest, isFinal: true);
if (codec == null) {
if (isCancelled) return;
if (isCancelled) {
return;
}
throw StateError('Failed to load animated codec for local asset ${key.id}');
}
yield codec;
@@ -156,7 +159,9 @@ class LocalFullImageProvider extends CancellableImageProvider<LocalFullImageProv
@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
if (identical(this, other)) {
return true;
}
if (other is LocalFullImageProvider) {
return id == other.id && size == other.size && isAnimated == other.isAnimated;
}
@@ -1,9 +1,8 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/painting.dart';
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
import 'package:immich_mobile/domain/models/setting.model.dart';
import 'package:immich_mobile/domain/services/setting.service.dart';
import 'package:immich_mobile/infrastructure/loaders/image_request.dart';
import 'package:immich_mobile/infrastructure/repositories/metadata.repository.dart';
import 'package:immich_mobile/presentation/widgets/images/animated_image_stream_completer.dart';
import 'package:immich_mobile/presentation/widgets/images/image_provider.dart';
import 'package:immich_mobile/presentation/widgets/images/one_frame_multi_image_stream_completer.dart';
@@ -44,7 +43,9 @@ class RemoteImageProvider extends CancellableImageProvider<RemoteImageProvider>
@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
if (identical(this, other)) {
return true;
}
if (other is RemoteImageProvider) {
return url == other.url && edited == other.edited;
}
@@ -121,7 +122,7 @@ class RemoteFullImageProvider extends CancellableImageProvider<RemoteFullImagePr
edited: key.edited,
),
);
final loadOriginal = assetType == AssetType.image && AppSetting.get(Setting.loadOriginal);
final loadOriginal = assetType == AssetType.image && MetadataRepository.instance.appConfig.image.loadOriginal;
yield* loadRequest(previewRequest, decode, isFinal: !loadOriginal);
if (!loadOriginal) {
@@ -175,7 +176,9 @@ class RemoteFullImageProvider extends CancellableImageProvider<RemoteFullImagePr
@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
if (identical(this, other)) {
return true;
}
if (other is RemoteFullImageProvider) {
return assetId == other.assetId &&
thumbhash == other.thumbhash &&
@@ -27,7 +27,9 @@ class ThumbHashProvider extends CancellableImageProvider<ThumbHashProvider>
@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
if (identical(this, other)) {
return true;
}
if (other is ThumbHashProvider) {
return thumbHash == other.thumbHash;
}
@@ -82,7 +82,9 @@ class _ThumbnailState extends State<Thumbnail> with SingleTickerProviderStateMix
void _loadFromThumbhashProvider() {
_stopListeningToThumbhashStream();
final thumbhashProvider = widget.thumbhashProvider;
if (thumbhashProvider == null || _providerImage != null) return;
if (thumbhashProvider == null || _providerImage != null) {
return;
}
final thumbhashStream = _thumbhashStream = thumbhashProvider.resolve(ImageConfiguration.empty);
final thumbhashStreamListener = _thumbhashStreamListener = ImageStreamListener(
@@ -108,7 +110,9 @@ class _ThumbnailState extends State<Thumbnail> with SingleTickerProviderStateMix
void _loadFromImageProvider() {
_stopListeningToImageStream();
final imageProvider = widget.imageProvider;
if (imageProvider == null) return;
if (imageProvider == null) {
return;
}
final imageStream = _imageStream = imageProvider.resolve(ImageConfiguration.empty);
final imageStreamListener = _imageStreamListener = ImageStreamListener(
@@ -201,7 +205,9 @@ class _ThumbnailState extends State<Thumbnail> with SingleTickerProviderStateMix
bool _isVisible() {
final renderObject = context.findRenderObject() as RenderBox?;
if (renderObject == null || !renderObject.attached) return false;
if (renderObject == null || !renderObject.attached) {
return false;
}
final topLeft = renderObject.localToGlobal(Offset.zero);
final bottomRight = renderObject.localToGlobal(Offset(renderObject.size.width, renderObject.size.height));
@@ -2,15 +2,14 @@ import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
import 'package:immich_mobile/domain/models/setting.model.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/extensions/duration_extensions.dart';
import 'package:immich_mobile/extensions/theme_extensions.dart';
import 'package:immich_mobile/providers/asset_viewer/asset_viewer.provider.dart';
import 'package:immich_mobile/presentation/widgets/images/thumbnail.widget.dart';
import 'package:immich_mobile/presentation/widgets/timeline/constants.dart';
import 'package:immich_mobile/providers/asset_viewer/asset_viewer.provider.dart';
import 'package:immich_mobile/providers/backup/asset_upload_progress.provider.dart';
import 'package:immich_mobile/providers/infrastructure/setting.provider.dart';
import 'package:immich_mobile/providers/infrastructure/metadata.provider.dart';
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
class ThumbnailTile extends ConsumerStatefulWidget {
@@ -61,7 +60,7 @@ class _ThumbnailTileState extends ConsumerState<ThumbnailTile> {
);
final bool storageIndicator =
ref.watch(settingsProvider.select((s) => s.get(Setting.showStorageIndicator))) && widget.showStorageIndicator;
ref.watch(appConfigProvider.select((s) => s.timeline.storageIndicator)) && widget.showStorageIndicator;
if (!isCurrentAsset) {
_hideIndicators = false;
@@ -1,12 +1,12 @@
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/models/events.model.dart';
import 'package:immich_mobile/domain/models/metadata_key.dart';
import 'package:immich_mobile/domain/utils/event_stream.dart';
import 'package:immich_mobile/infrastructure/repositories/timeline.repository.dart';
import 'package:immich_mobile/providers/app_settings.provider.dart';
import 'package:immich_mobile/providers/infrastructure/map.provider.dart';
import 'package:immich_mobile/providers/infrastructure/metadata.provider.dart';
import 'package:immich_mobile/providers/map/map_state.provider.dart';
import 'package:immich_mobile/services/app_settings.service.dart';
import 'package:maplibre_gl/maplibre_gl.dart';
class MapState {
@@ -81,38 +81,38 @@ class MapStateNotifier extends Notifier<MapState> {
}
void switchFavoriteOnly(bool isFavoriteOnly) {
ref.read(appSettingsServiceProvider).setSetting(AppSettingsEnum.mapShowFavoriteOnly, isFavoriteOnly);
ref.read(metadataProvider).write(MetadataKey.mapShowFavoriteOnly, isFavoriteOnly);
state = state.copyWith(onlyFavorites: isFavoriteOnly);
EventStream.shared.emit(const MapMarkerReloadEvent());
}
void switchIncludeArchived(bool isIncludeArchived) {
ref.read(appSettingsServiceProvider).setSetting(AppSettingsEnum.mapIncludeArchived, isIncludeArchived);
ref.read(metadataProvider).write(MetadataKey.mapIncludeArchived, isIncludeArchived);
state = state.copyWith(includeArchived: isIncludeArchived);
EventStream.shared.emit(const MapMarkerReloadEvent());
}
void switchWithPartners(bool isWithPartners) {
ref.read(appSettingsServiceProvider).setSetting(AppSettingsEnum.mapwithPartners, isWithPartners);
ref.read(metadataProvider).write(MetadataKey.mapWithPartners, isWithPartners);
state = state.copyWith(withPartners: isWithPartners);
EventStream.shared.emit(const MapMarkerReloadEvent());
}
void setRelativeTime(int relativeDays) {
ref.read(appSettingsServiceProvider).setSetting(AppSettingsEnum.mapRelativeDate, relativeDays);
ref.read(metadataProvider).write(MetadataKey.mapRelativeDate, relativeDays);
state = state.copyWith(relativeDays: relativeDays);
EventStream.shared.emit(const MapMarkerReloadEvent());
}
@override
MapState build() {
final appSettingsService = ref.read(appSettingsServiceProvider);
final mapConfig = ref.read(appConfigProvider.select((config) => config.map));
return MapState(
themeMode: ThemeMode.values[appSettingsService.getSetting(AppSettingsEnum.mapThemeMode)],
onlyFavorites: appSettingsService.getSetting(AppSettingsEnum.mapShowFavoriteOnly),
includeArchived: appSettingsService.getSetting(AppSettingsEnum.mapIncludeArchived),
withPartners: appSettingsService.getSetting(AppSettingsEnum.mapwithPartners),
relativeDays: appSettingsService.getSetting(AppSettingsEnum.mapRelativeDate),
themeMode: mapConfig.themeMode,
onlyFavorites: mapConfig.favoritesOnly,
includeArchived: mapConfig.includeArchived,
withPartners: mapConfig.withPartners,
relativeDays: mapConfig.relativeDays,
bounds: LatLngBounds(northeast: const LatLng(0, 0), southwest: const LatLng(0, 0)),
);
}
@@ -54,7 +54,9 @@ class DriftMemoryCard extends StatelessWidget {
}
}
if (asset.isImage) return FullImage(asset, fit: fit, size: const Size(double.infinity, double.infinity));
if (asset.isImage) {
return FullImage(asset, fit: fit, size: const Size(double.infinity, double.infinity));
}
return Center(
child: AspectRatio(
@@ -136,7 +136,9 @@ class QuickDatePicker extends HookWidget {
menuStyle: MenuStyle(maximumSize: WidgetStateProperty.all(Size(size.width, size.height * 0.5))),
dropdownMenuEntries: _recentYears.map((e) => DropdownMenuEntry(value: e, label: e.toString())).toList(),
onSelected: (year) {
if (year == null) return;
if (year == null) {
return;
}
onSelect(YearFilter(year));
},
),
@@ -179,7 +181,9 @@ class QuickDatePicker extends HookWidget {
child: SingleChildScrollView(
child: RadioGroup(
onChanged: (value) {
if (value == null) return;
if (value == null) {
return;
}
final _ = switch (value) {
_QuickPickerType.custom => onRequestPicker(),
_QuickPickerType.last1Month => onSelect(RecentMonthRangeFilter(1)),
@@ -77,7 +77,9 @@ class RenderFixedRow extends RenderBox
double _height;
set height(double value) {
if (_height == value) return;
if (_height == value) {
return;
}
_height = value;
markNeedsLayout();
}
@@ -86,7 +88,9 @@ class RenderFixedRow extends RenderBox
List<double> _widths;
set widths(List<double> value) {
if (listEquals(_widths, value)) return;
if (listEquals(_widths, value)) {
return;
}
_widths = value;
markNeedsLayout();
}
@@ -95,7 +99,9 @@ class RenderFixedRow extends RenderBox
double _spacing;
set spacing(double value) {
if (_spacing == value) return;
if (_spacing == value) {
return;
}
_spacing = value;
markNeedsLayout();
}
@@ -104,7 +110,9 @@ class RenderFixedRow extends RenderBox
TextDirection _textDirection;
set textDirection(TextDirection value) {
if (_textDirection == value) return;
if (_textDirection == value) {
return;
}
_textDirection = value;
markNeedsLayout();
}
@@ -53,14 +53,18 @@ class FixedSegment extends Segment {
@override
int getMinChildIndexForScrollOffset(double scrollOffset) {
final adjustedOffset = scrollOffset - gridOffset;
if (!adjustedOffset.isFinite || adjustedOffset < 0) return firstIndex;
if (!adjustedOffset.isFinite || adjustedOffset < 0) {
return firstIndex;
}
return gridIndex + (adjustedOffset / mainAxisExtend).floor();
}
@override
int getMaxChildIndexForScrollOffset(double scrollOffset) {
final adjustedOffset = scrollOffset - gridOffset;
if (!adjustedOffset.isFinite || adjustedOffset < 0) return firstIndex;
if (!adjustedOffset.isFinite || adjustedOffset < 0) {
return firstIndex;
}
return gridIndex + (adjustedOffset / mainAxisExtend).ceil() - 1;
}
@@ -162,8 +166,12 @@ class _FixedSegmentRow extends ConsumerWidget {
// 0.5: width < mean - threshold
// 1.5: width > mean + threshold
final arConfiguration = aspectRatios.map((e) {
if (e - meanAspectRatio > 0.3) return 1.5;
if (e - meanAspectRatio < -0.3) return 0.5;
if (e - meanAspectRatio > 0.3) {
return 1.5;
}
if (e - meanAspectRatio < -0.3) {
return 0.5;
}
return 1.0;
});
@@ -104,7 +104,9 @@ class ScrubberState extends ConsumerState<Scrubber> with TickerProviderStateMixi
late ScrollController _scrollController;
double get _currentOffset {
if (_scrollController.hasClients != true) return 0.0;
if (_scrollController.hasClients != true) {
return 0.0;
}
return _scrollController.offset * _scrubberHeight / _scrollController.position.maxScrollExtent;
}
@@ -52,7 +52,9 @@ abstract class Segment {
@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
if (identical(this, other)) {
return true;
}
return other is Segment &&
other.firstIndex == firstIndex &&
@@ -1,12 +1,11 @@
import 'dart:math' as math;
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/models/setting.model.dart';
import 'package:immich_mobile/domain/models/timeline.model.dart';
import 'package:immich_mobile/presentation/widgets/timeline/constants.dart';
import 'package:immich_mobile/presentation/widgets/timeline/fixed/segment_builder.dart';
import 'package:immich_mobile/presentation/widgets/timeline/segment.model.dart';
import 'package:immich_mobile/providers/infrastructure/setting.provider.dart';
import 'package:immich_mobile/providers/infrastructure/metadata.provider.dart';
import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart';
class TimelineArgs {
@@ -93,7 +92,7 @@ final timelineSegmentProvider = StreamProvider.autoDispose<List<Segment>>((ref)
final availableTileWidth = args.maxWidth - (spacing * (columnCount - 1));
final tileExtent = math.max(0, availableTileWidth) / columnCount;
final groupBy = args.groupBy ?? GroupAssetsBy.values[ref.watch(settingsProvider).get(Setting.groupAssetsBy)];
final groupBy = args.groupBy ?? ref.watch(appConfigProvider.select((config) => config.timeline.groupAssetsBy));
final timelineService = ref.watch(timelineServiceProvider);
yield* timelineService.watchBuckets().map((buckets) {
@@ -102,7 +101,7 @@ final timelineSegmentProvider = StreamProvider.autoDispose<List<Segment>>((ref)
tileHeight: tileExtent,
columnCount: columnCount,
spacing: spacing,
groupBy: groupBy,
groupBy: groupBy!,
).generate();
});
}, dependencies: [timelineServiceProvider, timelineArgsProvider]);
@@ -10,7 +10,7 @@ import 'package:flutter/rendering.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
import 'package:immich_mobile/domain/models/events.model.dart';
import 'package:immich_mobile/domain/models/setting.model.dart';
import 'package:immich_mobile/domain/models/metadata_key.dart';
import 'package:immich_mobile/domain/models/timeline.model.dart';
import 'package:immich_mobile/domain/utils/event_stream.dart';
import 'package:immich_mobile/extensions/asyncvalue_extensions.dart';
@@ -22,8 +22,8 @@ import 'package:immich_mobile/presentation/widgets/timeline/scrubber.widget.dart
import 'package:immich_mobile/presentation/widgets/timeline/segment.model.dart';
import 'package:immich_mobile/presentation/widgets/timeline/timeline.state.dart';
import 'package:immich_mobile/presentation/widgets/timeline/timeline_drag_region.dart';
import 'package:immich_mobile/providers/infrastructure/metadata.provider.dart';
import 'package:immich_mobile/providers/infrastructure/readonly_mode.provider.dart';
import 'package:immich_mobile/providers/infrastructure/setting.provider.dart';
import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart';
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
import 'package:immich_mobile/widgets/common/immich_sliver_app_bar.dart';
@@ -74,7 +74,7 @@ class Timeline extends StatelessWidget {
(ref) => TimelineArgs(
maxWidth: constraints.maxWidth,
maxHeight: constraints.maxHeight,
columnCount: ref.watch(settingsProvider.select((s) => s.get(Setting.tilesPerRow))),
columnCount: ref.watch(appConfigProvider.select((config) => config.timeline.tilesPerRow)),
showStorageIndicator: showStorageIndicator,
withStack: withStack,
groupBy: groupBy,
@@ -161,7 +161,7 @@ class _SliverTimelineState extends ConsumerState<_SliverTimeline> {
_scrollController = ScrollController(onAttach: _restoreAssetPosition);
_eventSubscription = EventStream.shared.listen(_onEvent);
final currentTilesPerRow = ref.read(settingsProvider).get(Setting.tilesPerRow);
final currentTilesPerRow = ref.read(appConfigProvider.select((config) => config.timeline.tilesPerRow));
_perRow = currentTilesPerRow;
_scaleFactor = 7.0 - _perRow;
_baseScaleFactor = _scaleFactor;
@@ -201,7 +201,9 @@ class _SliverTimelineState extends ConsumerState<_SliverTimeline> {
}
void _restoreAssetPosition(_) {
if (_restoreAssetIndex == null) return;
if (_restoreAssetIndex == null) {
return;
}
final asyncSegments = ref.read(timelineSegmentProvider);
asyncSegments.whenData((segments) {
@@ -329,7 +331,9 @@ class _SliverTimelineState extends ConsumerState<_SliverTimeline> {
}
void _handleDragAssetEnter(TimelineAssetIndex index) {
if (_dragAnchorIndex == null || !_dragging) return;
if (_dragAnchorIndex == null || !_dragging) {
return;
}
final timelineService = ref.read(timelineServiceProvider);
final dragAnchorIndex = _dragAnchorIndex!;
@@ -399,7 +403,9 @@ class _SliverTimelineState extends ConsumerState<_SliverTimeline> {
segments: segments,
delegate: SliverChildBuilderDelegate(
(ctx, index) {
if (index >= childCount) return null;
if (index >= childCount) {
return null;
}
final segment = segments.findByIndex(index);
return segment?.builder(ctx, index) ?? const SizedBox.shrink();
},
@@ -453,7 +459,7 @@ class _SliverTimelineState extends ConsumerState<_SliverTimeline> {
_restoreAssetIndex = targetAssetIndex;
});
ref.read(settingsProvider.notifier).set(Setting.tilesPerRow, _perRow);
ref.read(metadataProvider).write(MetadataKey.timelineTilesPerRow, _perRow);
}
};
},
@@ -81,11 +81,15 @@ class _TimelineDragRegionState extends State<TimelineDragRegion> {
TimelineAssetIndex? _getValueKeyAtPosition(Offset position) {
final box = context.findAncestorRenderObjectOfType<RenderBox>();
if (box == null) return null;
if (box == null) {
return null;
}
final hitTestResult = BoxHitTestResult();
final local = box.globalToLocal(position);
if (!box.hitTest(hitTestResult, position: local)) return null;
if (!box.hitTest(hitTestResult, position: local)) {
return null;
}
return (hitTestResult.path.firstWhereOrNull((hit) => hit.target is _TimelineAssetIndexProxy)?.target
as _TimelineAssetIndexProxy?)
@@ -103,7 +107,9 @@ class _TimelineDragRegionState extends State<TimelineDragRegion> {
final initialHit = _getValueKeyAtPosition(event.globalPosition);
anchorAsset = initialHit;
if (initialHit == null) return;
if (initialHit == null) {
return;
}
if (anchorAsset != null) {
widget.onStart?.call(anchorAsset!);
@@ -117,8 +123,12 @@ class _TimelineDragRegionState extends State<TimelineDragRegion> {
}
void _onLongPressMove(LongPressMoveUpdateDetails event) {
if (anchorAsset == null) return;
if (topScrollOffset == null || bottomScrollOffset == null) return;
if (anchorAsset == null) {
return;
}
if (topScrollOffset == null || bottomScrollOffset == null) {
return;
}
final currentDy = event.localPosition.dy;
@@ -138,7 +148,9 @@ class _TimelineDragRegionState extends State<TimelineDragRegion> {
}
final currentlyTouchingAsset = _getValueKeyAtPosition(event.globalPosition);
if (currentlyTouchingAsset == null) return;
if (currentlyTouchingAsset == null) {
return;
}
if (assetUnderPointer != currentlyTouchingAsset) {
if (!scrollNotified) {
@@ -202,7 +214,9 @@ class TimelineAssetIndex {
@override
bool operator ==(covariant TimelineAssetIndex other) {
if (identical(this, other)) return true;
if (identical(this, other)) {
return true;
}
return other.assetIndex == assetIndex && other.segmentIndex == segmentIndex;
}