mirror of
				https://github.com/immich-app/immich.git
				synced 2025-11-03 19:17:11 -05:00 
			
		
		
		
	* chore: bump dart sdk to 3.8 * chore: make build * make pigeon * chore: format files --------- Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
		
			
				
	
	
		
			260 lines
		
	
	
		
			8.0 KiB
		
	
	
	
		
			Dart
		
	
	
	
	
	
			
		
		
	
	
			260 lines
		
	
	
		
			8.0 KiB
		
	
	
	
		
			Dart
		
	
	
	
	
	
import 'package:flutter/material.dart';
 | 
						|
import 'package:immich_mobile/constants/constants.dart';
 | 
						|
import 'package:immich_mobile/extensions/build_context_extensions.dart';
 | 
						|
import 'package:immich_mobile/entities/asset.entity.dart';
 | 
						|
import 'package:immich_mobile/extensions/duration_extensions.dart';
 | 
						|
import 'package:immich_mobile/extensions/theme_extensions.dart';
 | 
						|
import 'package:immich_mobile/widgets/common/immich_thumbnail.dart';
 | 
						|
 | 
						|
class ThumbnailImage extends StatelessWidget {
 | 
						|
  /// The asset to show the thumbnail image for
 | 
						|
  final Asset asset;
 | 
						|
 | 
						|
  /// Whether to show the storage indicator icont over the image or not
 | 
						|
  final bool showStorageIndicator;
 | 
						|
 | 
						|
  /// Whether to show the show stack icon over the image or not
 | 
						|
  final bool showStack;
 | 
						|
 | 
						|
  /// Whether to show the checkmark indicating that this image is selected
 | 
						|
  final bool isSelected;
 | 
						|
 | 
						|
  /// Can override [isSelected] and never show the selection indicator
 | 
						|
  final bool multiselectEnabled;
 | 
						|
 | 
						|
  /// If we are allowed to deselect this image
 | 
						|
  final bool canDeselect;
 | 
						|
 | 
						|
  /// The offset index to apply to this hero tag for animation
 | 
						|
  final int heroOffset;
 | 
						|
 | 
						|
  const ThumbnailImage({
 | 
						|
    super.key,
 | 
						|
    required this.asset,
 | 
						|
    this.showStorageIndicator = true,
 | 
						|
    this.showStack = false,
 | 
						|
    this.isSelected = false,
 | 
						|
    this.multiselectEnabled = false,
 | 
						|
    this.heroOffset = 0,
 | 
						|
    this.canDeselect = true,
 | 
						|
  });
 | 
						|
 | 
						|
  @override
 | 
						|
  Widget build(BuildContext context) {
 | 
						|
    final assetContainerColor = context.isDarkTheme
 | 
						|
        ? context.primaryColor.darken(amount: 0.6)
 | 
						|
        : context.primaryColor.lighten(amount: 0.8);
 | 
						|
 | 
						|
    return Stack(
 | 
						|
      children: [
 | 
						|
        AnimatedContainer(
 | 
						|
          duration: const Duration(milliseconds: 300),
 | 
						|
          curve: Curves.decelerate,
 | 
						|
          decoration: BoxDecoration(
 | 
						|
            border: multiselectEnabled && isSelected
 | 
						|
                ? canDeselect
 | 
						|
                      ? Border.all(color: assetContainerColor, width: 8)
 | 
						|
                      : const Border(
 | 
						|
                          top: BorderSide(color: Colors.grey, width: 8),
 | 
						|
                          right: BorderSide(color: Colors.grey, width: 8),
 | 
						|
                          bottom: BorderSide(color: Colors.grey, width: 8),
 | 
						|
                          left: BorderSide(color: Colors.grey, width: 8),
 | 
						|
                        )
 | 
						|
                : const Border(),
 | 
						|
          ),
 | 
						|
          child: Stack(
 | 
						|
            children: [
 | 
						|
              _ImageIcon(
 | 
						|
                heroOffset: heroOffset,
 | 
						|
                asset: asset,
 | 
						|
                assetContainerColor: assetContainerColor,
 | 
						|
                multiselectEnabled: multiselectEnabled,
 | 
						|
                canDeselect: canDeselect,
 | 
						|
                isSelected: isSelected,
 | 
						|
              ),
 | 
						|
              if (showStorageIndicator) _StorageIcon(storage: asset.storage),
 | 
						|
              if (asset.isFavorite)
 | 
						|
                const Positioned(left: 8, bottom: 5, child: Icon(Icons.favorite, color: Colors.white, size: 16)),
 | 
						|
              if (asset.isVideo) _VideoIcon(duration: asset.duration),
 | 
						|
              if (asset.stackCount > 0) _StackIcon(isVideo: asset.isVideo, stackCount: asset.stackCount),
 | 
						|
            ],
 | 
						|
          ),
 | 
						|
        ),
 | 
						|
        if (multiselectEnabled)
 | 
						|
          isSelected
 | 
						|
              ? const Padding(
 | 
						|
                  padding: EdgeInsets.all(3.0),
 | 
						|
                  child: Align(alignment: Alignment.topLeft, child: _SelectedIcon()),
 | 
						|
                )
 | 
						|
              : const Icon(Icons.circle_outlined, color: Colors.white),
 | 
						|
      ],
 | 
						|
    );
 | 
						|
  }
 | 
						|
}
 | 
						|
 | 
						|
class _SelectedIcon extends StatelessWidget {
 | 
						|
  const _SelectedIcon();
 | 
						|
 | 
						|
  @override
 | 
						|
  Widget build(BuildContext context) {
 | 
						|
    final assetContainerColor = context.isDarkTheme
 | 
						|
        ? context.primaryColor.darken(amount: 0.6)
 | 
						|
        : context.primaryColor.lighten(amount: 0.8);
 | 
						|
 | 
						|
    return DecoratedBox(
 | 
						|
      decoration: BoxDecoration(shape: BoxShape.circle, color: assetContainerColor),
 | 
						|
      child: Icon(Icons.check_circle_rounded, color: context.primaryColor),
 | 
						|
    );
 | 
						|
  }
 | 
						|
}
 | 
						|
 | 
						|
class _VideoIcon extends StatelessWidget {
 | 
						|
  final Duration duration;
 | 
						|
 | 
						|
  const _VideoIcon({required this.duration});
 | 
						|
 | 
						|
  @override
 | 
						|
  Widget build(BuildContext context) {
 | 
						|
    return Positioned(
 | 
						|
      top: 5,
 | 
						|
      right: 8,
 | 
						|
      child: Row(
 | 
						|
        children: [
 | 
						|
          Text(
 | 
						|
            duration.format(),
 | 
						|
            style: const TextStyle(color: Colors.white, fontSize: 10, fontWeight: FontWeight.bold),
 | 
						|
          ),
 | 
						|
          const SizedBox(width: 3),
 | 
						|
          const Icon(Icons.play_circle_fill_rounded, color: Colors.white, size: 18),
 | 
						|
        ],
 | 
						|
      ),
 | 
						|
    );
 | 
						|
  }
 | 
						|
}
 | 
						|
 | 
						|
class _StackIcon extends StatelessWidget {
 | 
						|
  final bool isVideo;
 | 
						|
  final int stackCount;
 | 
						|
 | 
						|
  const _StackIcon({required this.isVideo, required this.stackCount});
 | 
						|
 | 
						|
  @override
 | 
						|
  Widget build(BuildContext context) {
 | 
						|
    return Positioned(
 | 
						|
      top: isVideo ? 28 : 5,
 | 
						|
      right: 8,
 | 
						|
      child: Row(
 | 
						|
        children: [
 | 
						|
          if (stackCount > 1)
 | 
						|
            Text(
 | 
						|
              "$stackCount",
 | 
						|
              style: const TextStyle(color: Colors.white, fontSize: 10, fontWeight: FontWeight.bold),
 | 
						|
            ),
 | 
						|
          if (stackCount > 1) const SizedBox(width: 3),
 | 
						|
          const Icon(Icons.burst_mode_rounded, color: Colors.white, size: 18),
 | 
						|
        ],
 | 
						|
      ),
 | 
						|
    );
 | 
						|
  }
 | 
						|
}
 | 
						|
 | 
						|
class _StorageIcon extends StatelessWidget {
 | 
						|
  final AssetState storage;
 | 
						|
 | 
						|
  const _StorageIcon({required this.storage});
 | 
						|
 | 
						|
  @override
 | 
						|
  Widget build(BuildContext context) {
 | 
						|
    return switch (storage) {
 | 
						|
      AssetState.local => const Positioned(
 | 
						|
        right: 8,
 | 
						|
        bottom: 5,
 | 
						|
        child: Icon(
 | 
						|
          Icons.cloud_off_outlined,
 | 
						|
          color: Color.fromRGBO(255, 255, 255, 0.8),
 | 
						|
          size: 16,
 | 
						|
          shadows: [Shadow(blurRadius: 5.0, color: Color.fromRGBO(0, 0, 0, 0.6), offset: Offset(0.0, 0.0))],
 | 
						|
        ),
 | 
						|
      ),
 | 
						|
      AssetState.remote => const Positioned(
 | 
						|
        right: 8,
 | 
						|
        bottom: 5,
 | 
						|
        child: Icon(
 | 
						|
          Icons.cloud_outlined,
 | 
						|
          color: Color.fromRGBO(255, 255, 255, 0.8),
 | 
						|
          size: 16,
 | 
						|
          shadows: [Shadow(blurRadius: 5.0, color: Color.fromRGBO(0, 0, 0, 0.6), offset: Offset(0.0, 0.0))],
 | 
						|
        ),
 | 
						|
      ),
 | 
						|
      AssetState.merged => const Positioned(
 | 
						|
        right: 8,
 | 
						|
        bottom: 5,
 | 
						|
        child: Icon(
 | 
						|
          Icons.cloud_done_outlined,
 | 
						|
          color: Color.fromRGBO(255, 255, 255, 0.8),
 | 
						|
          size: 16,
 | 
						|
          shadows: [Shadow(blurRadius: 5.0, color: Color.fromRGBO(0, 0, 0, 0.6), offset: Offset(0.0, 0.0))],
 | 
						|
        ),
 | 
						|
      ),
 | 
						|
    };
 | 
						|
  }
 | 
						|
}
 | 
						|
 | 
						|
class _ImageIcon extends StatelessWidget {
 | 
						|
  final int heroOffset;
 | 
						|
  final Asset asset;
 | 
						|
  final Color assetContainerColor;
 | 
						|
  final bool multiselectEnabled;
 | 
						|
  final bool canDeselect;
 | 
						|
  final bool isSelected;
 | 
						|
 | 
						|
  const _ImageIcon({
 | 
						|
    required this.heroOffset,
 | 
						|
    required this.asset,
 | 
						|
    required this.assetContainerColor,
 | 
						|
    required this.multiselectEnabled,
 | 
						|
    required this.canDeselect,
 | 
						|
    required this.isSelected,
 | 
						|
  });
 | 
						|
 | 
						|
  @override
 | 
						|
  Widget build(BuildContext context) {
 | 
						|
    // Assets from response DTOs do not have an isar id, querying which would give us the default autoIncrement id
 | 
						|
    final isDto = asset.id == noDbId;
 | 
						|
    final image = SizedBox.expand(
 | 
						|
      child: Hero(
 | 
						|
        tag: isDto ? '${asset.remoteId}-$heroOffset' : asset.id + heroOffset,
 | 
						|
        child: Stack(
 | 
						|
          children: [
 | 
						|
            SizedBox.expand(child: ImmichThumbnail(asset: asset, height: 250, width: 250)),
 | 
						|
            const DecoratedBox(
 | 
						|
              decoration: BoxDecoration(
 | 
						|
                gradient: LinearGradient(
 | 
						|
                  colors: [
 | 
						|
                    Color.fromRGBO(0, 0, 0, 0.1),
 | 
						|
                    Colors.transparent,
 | 
						|
                    Colors.transparent,
 | 
						|
                    Color.fromRGBO(0, 0, 0, 0.1),
 | 
						|
                  ],
 | 
						|
                  begin: Alignment.topCenter,
 | 
						|
                  end: Alignment.bottomCenter,
 | 
						|
                  stops: [0, 0.3, 0.6, 1],
 | 
						|
                ),
 | 
						|
              ),
 | 
						|
            ),
 | 
						|
          ],
 | 
						|
        ),
 | 
						|
      ),
 | 
						|
    );
 | 
						|
 | 
						|
    if (!multiselectEnabled || !isSelected) {
 | 
						|
      return image;
 | 
						|
    }
 | 
						|
 | 
						|
    return DecoratedBox(
 | 
						|
      decoration: canDeselect ? BoxDecoration(color: assetContainerColor) : const BoxDecoration(color: Colors.grey),
 | 
						|
      child: ClipRRect(borderRadius: const BorderRadius.all(Radius.circular(15.0)), child: image),
 | 
						|
    );
 | 
						|
  }
 | 
						|
}
 |