mirror of
				https://github.com/immich-app/immich.git
				synced 2025-11-03 11:07:10 -05:00 
			
		
		
		
	* thumbnail widget * use animation ticker, improvements * use static thumbnail resolution for now * fix android sample size * free memory sooner * formatting * tweaks * wait for disposal * remove debug prints * take two on animation * fix * remote constructor * missed one * unused imports * unnecessary import * formatting
		
			
				
	
	
		
			217 lines
		
	
	
		
			7.5 KiB
		
	
	
	
		
			Dart
		
	
	
	
	
	
			
		
		
	
	
			217 lines
		
	
	
		
			7.5 KiB
		
	
	
	
		
			Dart
		
	
	
	
	
	
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/presentation/widgets/images/thumbnail.widget.dart';
 | 
						|
import 'package:immich_mobile/presentation/widgets/timeline/constants.dart';
 | 
						|
import 'package:immich_mobile/providers/infrastructure/setting.provider.dart';
 | 
						|
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
 | 
						|
 | 
						|
class ThumbnailTile extends ConsumerWidget {
 | 
						|
  const ThumbnailTile(
 | 
						|
    this.asset, {
 | 
						|
    this.size = kThumbnailResolution,
 | 
						|
    this.fit = BoxFit.cover,
 | 
						|
    this.showStorageIndicator,
 | 
						|
    this.lockSelection = false,
 | 
						|
    this.heroOffset,
 | 
						|
    super.key,
 | 
						|
  });
 | 
						|
 | 
						|
  final BaseAsset? asset;
 | 
						|
  final Size size;
 | 
						|
  final BoxFit fit;
 | 
						|
  final bool? showStorageIndicator;
 | 
						|
  final bool lockSelection;
 | 
						|
  final int? heroOffset;
 | 
						|
 | 
						|
  @override
 | 
						|
  Widget build(BuildContext context, WidgetRef ref) {
 | 
						|
    final asset = this.asset;
 | 
						|
    final heroIndex = heroOffset ?? TabsRouterScope.of(context)?.controller.activeIndex ?? 0;
 | 
						|
 | 
						|
    final assetContainerColor = context.isDarkTheme
 | 
						|
        ? context.primaryColor.darken(amount: 0.4)
 | 
						|
        : context.primaryColor.lighten(amount: 0.75);
 | 
						|
 | 
						|
    final isSelected = ref.watch(
 | 
						|
      multiSelectProvider.select((multiselect) => multiselect.selectedAssets.contains(asset)),
 | 
						|
    );
 | 
						|
 | 
						|
    final borderStyle = lockSelection
 | 
						|
        ? BoxDecoration(
 | 
						|
            color: context.colorScheme.surfaceContainerHighest,
 | 
						|
            border: Border.all(color: context.colorScheme.surfaceContainerHighest, width: 6),
 | 
						|
          )
 | 
						|
        : isSelected
 | 
						|
        ? BoxDecoration(
 | 
						|
            color: assetContainerColor,
 | 
						|
            border: Border.all(color: assetContainerColor, width: 6),
 | 
						|
          )
 | 
						|
        : const BoxDecoration();
 | 
						|
 | 
						|
    final hasStack = asset is RemoteAsset && asset.stackId != null;
 | 
						|
 | 
						|
    final bool storageIndicator =
 | 
						|
        showStorageIndicator ?? ref.watch(settingsProvider.select((s) => s.get(Setting.showStorageIndicator)));
 | 
						|
 | 
						|
    return Stack(
 | 
						|
      children: [
 | 
						|
        AnimatedContainer(
 | 
						|
          duration: Durations.short4,
 | 
						|
          curve: Curves.decelerate,
 | 
						|
          decoration: borderStyle,
 | 
						|
          child: ClipRRect(
 | 
						|
            borderRadius: isSelected || lockSelection
 | 
						|
                ? const BorderRadius.all(Radius.circular(15.0))
 | 
						|
                : BorderRadius.zero,
 | 
						|
            child: Stack(
 | 
						|
              children: [
 | 
						|
                Positioned.fill(
 | 
						|
                  child: Hero(
 | 
						|
                    tag: '${asset?.heroTag ?? ''}_$heroIndex',
 | 
						|
                    child: Thumbnail.fromAsset(asset: asset, size: size),
 | 
						|
                  ),
 | 
						|
                ),
 | 
						|
                if (hasStack)
 | 
						|
                  Align(
 | 
						|
                    alignment: Alignment.topRight,
 | 
						|
                    child: Padding(
 | 
						|
                      padding: EdgeInsets.only(right: 10.0, top: asset.isVideo ? 24.0 : 6.0),
 | 
						|
                      child: const _TileOverlayIcon(Icons.burst_mode_rounded),
 | 
						|
                    ),
 | 
						|
                  ),
 | 
						|
                if (asset != null && asset.isVideo)
 | 
						|
                  Align(
 | 
						|
                    alignment: Alignment.topRight,
 | 
						|
                    child: Padding(
 | 
						|
                      padding: const EdgeInsets.only(right: 10.0, top: 6.0),
 | 
						|
                      child: _VideoIndicator(asset.duration),
 | 
						|
                    ),
 | 
						|
                  ),
 | 
						|
                if (storageIndicator && asset != null)
 | 
						|
                  switch (asset.storage) {
 | 
						|
                    AssetState.local => const Align(
 | 
						|
                      alignment: Alignment.bottomRight,
 | 
						|
                      child: Padding(
 | 
						|
                        padding: EdgeInsets.only(right: 10.0, bottom: 6.0),
 | 
						|
                        child: _TileOverlayIcon(Icons.cloud_off_outlined),
 | 
						|
                      ),
 | 
						|
                    ),
 | 
						|
                    AssetState.remote => const Align(
 | 
						|
                      alignment: Alignment.bottomRight,
 | 
						|
                      child: Padding(
 | 
						|
                        padding: EdgeInsets.only(right: 10.0, bottom: 6.0),
 | 
						|
                        child: _TileOverlayIcon(Icons.cloud_outlined),
 | 
						|
                      ),
 | 
						|
                    ),
 | 
						|
                    AssetState.merged => const Align(
 | 
						|
                      alignment: Alignment.bottomRight,
 | 
						|
                      child: Padding(
 | 
						|
                        padding: EdgeInsets.only(right: 10.0, bottom: 6.0),
 | 
						|
                        child: _TileOverlayIcon(Icons.cloud_done_outlined),
 | 
						|
                      ),
 | 
						|
                    ),
 | 
						|
                  },
 | 
						|
                if (asset != null && asset.isFavorite)
 | 
						|
                  const Align(
 | 
						|
                    alignment: Alignment.bottomLeft,
 | 
						|
                    child: Padding(
 | 
						|
                      padding: EdgeInsets.only(left: 10.0, bottom: 6.0),
 | 
						|
                      child: _TileOverlayIcon(Icons.favorite_rounded),
 | 
						|
                    ),
 | 
						|
                  ),
 | 
						|
              ],
 | 
						|
            ),
 | 
						|
          ),
 | 
						|
        ),
 | 
						|
        if (isSelected || lockSelection)
 | 
						|
          Padding(
 | 
						|
            padding: const EdgeInsets.all(3.0),
 | 
						|
            child: Align(
 | 
						|
              alignment: Alignment.topLeft,
 | 
						|
              child: _SelectionIndicator(
 | 
						|
                isSelected: isSelected,
 | 
						|
                isLocked: lockSelection,
 | 
						|
                color: lockSelection ? context.colorScheme.surfaceContainerHighest : assetContainerColor,
 | 
						|
              ),
 | 
						|
            ),
 | 
						|
          ),
 | 
						|
      ],
 | 
						|
    );
 | 
						|
  }
 | 
						|
}
 | 
						|
 | 
						|
class _SelectionIndicator extends StatelessWidget {
 | 
						|
  final bool isSelected;
 | 
						|
  final bool isLocked;
 | 
						|
  final Color? color;
 | 
						|
 | 
						|
  const _SelectionIndicator({required this.isSelected, required this.isLocked, this.color});
 | 
						|
 | 
						|
  @override
 | 
						|
  Widget build(BuildContext context) {
 | 
						|
    if (isLocked) {
 | 
						|
      return DecoratedBox(
 | 
						|
        decoration: BoxDecoration(shape: BoxShape.circle, color: color),
 | 
						|
        child: const Icon(Icons.check_circle_rounded, color: Colors.grey),
 | 
						|
      );
 | 
						|
    } else if (isSelected) {
 | 
						|
      return DecoratedBox(
 | 
						|
        decoration: BoxDecoration(shape: BoxShape.circle, color: color),
 | 
						|
        child: Icon(Icons.check_circle_rounded, color: context.primaryColor),
 | 
						|
      );
 | 
						|
    } else {
 | 
						|
      return const Icon(Icons.circle_outlined, color: Colors.white);
 | 
						|
    }
 | 
						|
  }
 | 
						|
}
 | 
						|
 | 
						|
class _VideoIndicator extends StatelessWidget {
 | 
						|
  final Duration duration;
 | 
						|
  const _VideoIndicator(this.duration);
 | 
						|
 | 
						|
  @override
 | 
						|
  Widget build(BuildContext context) {
 | 
						|
    return Row(
 | 
						|
      spacing: 3,
 | 
						|
      mainAxisSize: MainAxisSize.min,
 | 
						|
      mainAxisAlignment: MainAxisAlignment.end,
 | 
						|
      // CrossAxisAlignment.start looks more centered vertically than CrossAxisAlignment.center
 | 
						|
      crossAxisAlignment: CrossAxisAlignment.start,
 | 
						|
      children: [
 | 
						|
        Text(
 | 
						|
          duration.format(),
 | 
						|
          style: const TextStyle(
 | 
						|
            color: Colors.white,
 | 
						|
            fontSize: 12,
 | 
						|
            fontWeight: FontWeight.bold,
 | 
						|
            shadows: [Shadow(blurRadius: 5.0, color: Color.fromRGBO(0, 0, 0, 0.6))],
 | 
						|
          ),
 | 
						|
        ),
 | 
						|
        const _TileOverlayIcon(Icons.play_circle_outline_rounded),
 | 
						|
      ],
 | 
						|
    );
 | 
						|
  }
 | 
						|
}
 | 
						|
 | 
						|
class _TileOverlayIcon extends StatelessWidget {
 | 
						|
  final IconData icon;
 | 
						|
 | 
						|
  const _TileOverlayIcon(this.icon);
 | 
						|
 | 
						|
  @override
 | 
						|
  Widget build(BuildContext context) {
 | 
						|
    return Icon(
 | 
						|
      icon,
 | 
						|
      color: Colors.white,
 | 
						|
      size: 16,
 | 
						|
      shadows: [const Shadow(blurRadius: 5.0, color: Color.fromRGBO(0, 0, 0, 0.6), offset: Offset(0.0, 0.0))],
 | 
						|
    );
 | 
						|
  }
 | 
						|
}
 |