mirror of
https://github.com/immich-app/immich.git
synced 2025-07-09 03:04:16 -04:00
feat: sliver timeline selection optimization (#19504)
This commit is contained in:
parent
9240bbc6ff
commit
522cdbac99
@ -63,7 +63,8 @@ class FixedSegment extends Segment {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _handleOnTap(WidgetRef ref, BaseAsset asset) {
|
void _handleOnTap(WidgetRef ref, BaseAsset asset) {
|
||||||
if (!ref.read(multiSelectProvider.select((s) => s.isEnabled))) {
|
final multiSelectState = ref.read(multiSelectProvider);
|
||||||
|
if (!multiSelectState.isEnabled) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -71,7 +72,8 @@ class FixedSegment extends Segment {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _handleOnLongPress(WidgetRef ref, BaseAsset asset) {
|
void _handleOnLongPress(WidgetRef ref, BaseAsset asset) {
|
||||||
if (ref.read(multiSelectProvider.select((s) => s.isEnabled))) {
|
final multiSelectState = ref.read(multiSelectProvider);
|
||||||
|
if (multiSelectState.isEnabled) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -98,55 +100,63 @@ class FixedSegment extends Segment {
|
|||||||
return _buildRow(firstAssetIndex + assetIndex, numberOfAssets);
|
return _buildRow(firstAssetIndex + assetIndex, numberOfAssets);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildRow(int assetIndex, int count) => Consumer(
|
Widget _buildRow(int assetIndex, int count) => RepaintBoundary(
|
||||||
builder: (ctx, ref, _) {
|
child: Consumer(
|
||||||
final isScrubbing =
|
builder: (ctx, ref, _) {
|
||||||
ref.watch(timelineStateProvider.select((s) => s.isScrubbing));
|
final isScrubbing =
|
||||||
final timelineService = ref.read(timelineServiceProvider);
|
ref.watch(timelineStateProvider.select((s) => s.isScrubbing));
|
||||||
|
final timelineService = ref.read(timelineServiceProvider);
|
||||||
|
|
||||||
// Timeline is being scrubbed, show placeholders
|
// Create stable callback references to prevent unnecessary rebuilds
|
||||||
if (isScrubbing) {
|
onTap(BaseAsset asset) => _handleOnTap(ref, asset);
|
||||||
return SegmentBuilder.buildPlaceholder(
|
onLongPress(BaseAsset asset) => _handleOnLongPress(ref, asset);
|
||||||
ctx,
|
|
||||||
count,
|
|
||||||
size: Size.square(tileHeight),
|
|
||||||
spacing: spacing,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Bucket is already loaded, show the assets
|
// Timeline is being scrubbed, show placeholders
|
||||||
if (timelineService.hasRange(assetIndex, count)) {
|
if (isScrubbing) {
|
||||||
final assets = timelineService.getAssets(assetIndex, count);
|
return SegmentBuilder.buildPlaceholder(
|
||||||
return _buildAssetRow(
|
ctx,
|
||||||
ctx,
|
count,
|
||||||
assets,
|
size: Size.square(tileHeight),
|
||||||
onTap: (asset) => _handleOnTap(ref, asset),
|
spacing: spacing,
|
||||||
onLongPress: (asset) => _handleOnLongPress(ref, asset),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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,
|
|
||||||
onTap: (asset) => _handleOnTap(ref, asset),
|
|
||||||
onLongPress: (asset) => _handleOnLongPress(ref, asset),
|
|
||||||
);
|
);
|
||||||
},
|
}
|
||||||
);
|
|
||||||
},
|
// Bucket is already loaded, show the assets
|
||||||
|
if (timelineService.hasRange(assetIndex, count)) {
|
||||||
|
final assets = timelineService.getAssets(assetIndex, count);
|
||||||
|
return _buildAssetRow(
|
||||||
|
ctx,
|
||||||
|
assets,
|
||||||
|
baseAssetIndex: assetIndex,
|
||||||
|
onTap: onTap,
|
||||||
|
onLongPress: onLongPress,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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,
|
||||||
|
baseAssetIndex: assetIndex,
|
||||||
|
onTap: onTap,
|
||||||
|
onLongPress: onLongPress,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
Widget _buildAssetRow(
|
Widget _buildAssetRow(
|
||||||
@ -154,6 +164,7 @@ class FixedSegment extends Segment {
|
|||||||
List<BaseAsset> assets, {
|
List<BaseAsset> assets, {
|
||||||
required void Function(BaseAsset) onTap,
|
required void Function(BaseAsset) onTap,
|
||||||
required void Function(BaseAsset) onLongPress,
|
required void Function(BaseAsset) onLongPress,
|
||||||
|
required int baseAssetIndex,
|
||||||
}) =>
|
}) =>
|
||||||
FixedTimelineRow(
|
FixedTimelineRow(
|
||||||
dimension: tileHeight,
|
dimension: tileHeight,
|
||||||
@ -161,13 +172,59 @@ class FixedSegment extends Segment {
|
|||||||
textDirection: Directionality.of(context),
|
textDirection: Directionality.of(context),
|
||||||
children: List.generate(
|
children: List.generate(
|
||||||
assets.length,
|
assets.length,
|
||||||
(i) => RepaintBoundary(
|
(i) => _AssetTileWidget(
|
||||||
child: GestureDetector(
|
key: ValueKey(_generateUniqueKey(assets[i], baseAssetIndex + i)),
|
||||||
onTap: () => onTap(assets[i]),
|
asset: assets[i],
|
||||||
onLongPress: () => onLongPress(assets[i]),
|
onTap: onTap,
|
||||||
child: ThumbnailTile(assets[i]),
|
onLongPress: onLongPress,
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/// Generates a unique key for an asset that handles different asset types
|
||||||
|
/// and prevents duplicate keys even when assets have the same name/timestamp
|
||||||
|
String _generateUniqueKey(BaseAsset asset, int assetIndex) {
|
||||||
|
// Try to get the most unique identifier based on asset type
|
||||||
|
if (asset is Asset) {
|
||||||
|
// For remote/merged assets, use the remote ID which is globally unique
|
||||||
|
return 'asset_${asset.id}';
|
||||||
|
} else if (asset is LocalAsset) {
|
||||||
|
// For local assets, use the local ID which should be unique per device
|
||||||
|
return 'local_${asset.id}';
|
||||||
|
} else {
|
||||||
|
// Fallback for any other BaseAsset implementation
|
||||||
|
// Use checksum if available for additional uniqueness
|
||||||
|
final checksum = asset.checksum;
|
||||||
|
if (checksum != null && checksum.isNotEmpty) {
|
||||||
|
return 'checksum_${checksum.hashCode}';
|
||||||
|
} else {
|
||||||
|
// Last resort: use global asset index + object hash for uniqueness
|
||||||
|
return 'fallback_${assetIndex}_${asset.hashCode}_${asset.createdAt.microsecondsSinceEpoch}';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _AssetTileWidget extends StatelessWidget {
|
||||||
|
final BaseAsset asset;
|
||||||
|
final void Function(BaseAsset) onTap;
|
||||||
|
final void Function(BaseAsset) onLongPress;
|
||||||
|
|
||||||
|
const _AssetTileWidget({
|
||||||
|
super.key,
|
||||||
|
required this.asset,
|
||||||
|
required this.onTap,
|
||||||
|
required this.onLongPress,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return RepaintBoundary(
|
||||||
|
child: GestureDetector(
|
||||||
|
onTap: () => onTap(asset),
|
||||||
|
onLongPress: () => onLongPress(asset),
|
||||||
|
child: ThumbnailTile(asset),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user