immich/mobile/lib/presentation/widgets/images/thumbnail_tile.widget.dart
shenlong bcda2c6e22
feat(mobile): sqlite timeline (#19197)
* wip: timeline

* more segment extensions

* added scrubber

* refactor: timeline state

* more refactors

* fix scrubber segments

* added remote thumb & thumbhash provider

* feat: merged view

* scrub / merged asset fixes

* rename stuff & add tile indicators

* fix local album timeline query

* ignore hidden assets during sync

* ignore recovered assets during sync

* old scrubber

* add video indicator

* handle groupBy

* handle partner inTimeline

* show duration

* reduce widget nesting in thumb tile

* merge main

* chore: extend cacheExtent

* ignore touch events on scrub label when not visible

* scrub label ignore events and hide immediately

* auto reload on sync

* refactor image providers

* throttle db updates

---------

Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
2025-06-16 20:37:45 +05:30

128 lines
3.6 KiB
Dart

import 'package:flutter/material.dart';
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
import 'package:immich_mobile/presentation/widgets/images/thumbnail.widget.dart';
class ThumbnailTile extends StatelessWidget {
const ThumbnailTile(
this.asset, {
this.size = const Size.square(256),
this.fit = BoxFit.cover,
this.showStorageIndicator = true,
super.key,
});
final BaseAsset asset;
final Size size;
final BoxFit fit;
final bool showStorageIndicator;
@override
Widget build(BuildContext context) {
return Stack(
children: [
Positioned.fill(child: Thumbnail(asset: asset, fit: fit, size: size)),
if (asset.isVideo)
Align(
alignment: Alignment.topRight,
child: Padding(
padding: const EdgeInsets.only(right: 10.0, top: 6.0),
child: _VideoIndicator(asset.durationInSeconds ?? 0),
),
),
if (showStorageIndicator)
Align(
alignment: Alignment.bottomRight,
child: Padding(
padding: const EdgeInsets.only(right: 10.0, bottom: 6.0),
child: _TileOverlayIcon(
switch (asset.storage) {
AssetState.local => Icons.cloud_off_outlined,
AssetState.remote => Icons.cloud_outlined,
AssetState.merged => Icons.cloud_done_outlined,
},
),
),
),
if (asset.isFavorite)
const Align(
alignment: Alignment.bottomLeft,
child: Padding(
padding: EdgeInsets.only(left: 10.0, bottom: 6.0),
child: _TileOverlayIcon(Icons.favorite_rounded),
),
),
],
);
}
}
class _VideoIndicator extends StatelessWidget {
final int durationInSeconds;
const _VideoIndicator(this.durationInSeconds);
String _formatDuration(int durationInSec) {
final int hours = durationInSec ~/ 3600;
final int minutes = (durationInSec % 3600) ~/ 60;
final int seconds = durationInSec % 60;
final String minutesPadded = minutes.toString().padLeft(2, '0');
final String secondsPadded = seconds.toString().padLeft(2, '0');
if (hours > 0) {
return "$hours:$minutesPadded:$secondsPadded"; // H:MM:SS
} else {
return "$minutesPadded:$secondsPadded"; // MM:SS
}
}
@override
Widget build(BuildContext context) {
return Row(
spacing: 3,
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.end,
// CrossAxisAlignment.end looks more centered vertically than CrossAxisAlignment.center
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Text(
_formatDuration(durationInSeconds),
style: TextStyle(
color: Colors.white,
fontSize: 12,
fontWeight: FontWeight.bold,
shadows: [
Shadow(
blurRadius: 5.0,
color: Colors.black.withValues(alpha: 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: [
Shadow(
blurRadius: 5.0,
color: Colors.black.withValues(alpha: 0.6),
offset: const Offset(0.0, 0.0),
),
],
);
}
}