You've already forked immich
mirror of
https://github.com/immich-app/immich.git
synced 2025-07-16 07:24:40 +02:00
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>
This commit is contained in:
@ -0,0 +1,132 @@
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/images/thumbnail_tile.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/timeline/fixed/row.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/timeline/header.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/timeline/segment.model.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/timeline/segment_builder.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/timeline/timeline.state.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart';
|
||||
|
||||
class FixedSegment extends Segment {
|
||||
final double tileHeight;
|
||||
final int columnCount;
|
||||
final double mainAxisExtend;
|
||||
|
||||
const FixedSegment({
|
||||
required super.firstIndex,
|
||||
required super.lastIndex,
|
||||
required super.startOffset,
|
||||
required super.endOffset,
|
||||
required super.firstAssetIndex,
|
||||
required super.bucket,
|
||||
required this.tileHeight,
|
||||
required this.columnCount,
|
||||
required super.headerExtent,
|
||||
required super.spacing,
|
||||
required super.header,
|
||||
}) : assert(tileHeight != 0),
|
||||
mainAxisExtend = tileHeight + spacing;
|
||||
|
||||
@override
|
||||
double indexToLayoutOffset(int index) {
|
||||
index -= gridIndex;
|
||||
if (index < 0) {
|
||||
return startOffset;
|
||||
}
|
||||
return gridOffset + (mainAxisExtend * index);
|
||||
}
|
||||
|
||||
@override
|
||||
int getMinChildIndexForScrollOffset(double scrollOffset) {
|
||||
scrollOffset -= gridOffset;
|
||||
if (!scrollOffset.isFinite || scrollOffset < 0) {
|
||||
return firstIndex;
|
||||
}
|
||||
final rowsAbove = (scrollOffset / mainAxisExtend).floor();
|
||||
return gridIndex + rowsAbove;
|
||||
}
|
||||
|
||||
@override
|
||||
int getMaxChildIndexForScrollOffset(double scrollOffset) {
|
||||
scrollOffset -= gridOffset;
|
||||
if (!scrollOffset.isFinite || scrollOffset < 0) {
|
||||
return firstIndex;
|
||||
}
|
||||
final firstRowBelow = (scrollOffset / mainAxisExtend).ceil();
|
||||
return gridIndex + firstRowBelow - 1;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget builder(BuildContext context, int index) {
|
||||
if (index == firstIndex) {
|
||||
return TimelineHeader(
|
||||
bucket: bucket,
|
||||
header: header,
|
||||
height: headerExtent,
|
||||
);
|
||||
}
|
||||
|
||||
final rowIndexInSegment = index - (firstIndex + 1);
|
||||
final assetIndex = rowIndexInSegment * columnCount;
|
||||
final assetCount = bucket.assetCount;
|
||||
final numberOfAssets = math.min(columnCount, assetCount - assetIndex);
|
||||
|
||||
return _buildRow(firstAssetIndex + assetIndex, numberOfAssets);
|
||||
}
|
||||
|
||||
Widget _buildRow(int assetIndex, int count) => Consumer(
|
||||
builder: (ctx, ref, _) {
|
||||
final isScrubbing =
|
||||
ref.watch(timelineStateProvider.select((s) => s.isScrubbing));
|
||||
final timelineService = ref.read(timelineServiceProvider);
|
||||
|
||||
// Timeline is being scrubbed, show placeholders
|
||||
if (isScrubbing) {
|
||||
return SegmentBuilder.buildPlaceholder(
|
||||
ctx,
|
||||
count,
|
||||
size: Size.square(tileHeight),
|
||||
spacing: spacing,
|
||||
);
|
||||
}
|
||||
|
||||
// Bucket is already loaded, show the assets
|
||||
if (timelineService.hasRange(assetIndex, count)) {
|
||||
final assets = timelineService.getAssets(assetIndex, count);
|
||||
return _buildAssetRow(ctx, assets);
|
||||
}
|
||||
|
||||
// Bucket is not loaded, show placeholders and load the bucket
|
||||
return FutureBuilder(
|
||||
future: timelineService.loadAssets(assetIndex, count),
|
||||
builder: (ctxx, snap) {
|
||||
if (snap.connectionState != ConnectionState.done) {
|
||||
return SegmentBuilder.buildPlaceholder(
|
||||
ctx,
|
||||
count,
|
||||
size: Size.square(tileHeight),
|
||||
spacing: spacing,
|
||||
);
|
||||
}
|
||||
|
||||
return _buildAssetRow(ctxx, snap.requireData);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
Widget _buildAssetRow(BuildContext context, List<BaseAsset> assets) =>
|
||||
FixedTimelineRow(
|
||||
dimension: tileHeight,
|
||||
spacing: spacing,
|
||||
textDirection: Directionality.of(context),
|
||||
children: List.generate(
|
||||
assets.length,
|
||||
(i) => RepaintBoundary(child: ThumbnailTile(assets[i])),
|
||||
),
|
||||
);
|
||||
}
|
Reference in New Issue
Block a user