Skip to content

Commit 9ae4210

Browse files
authored
fix(mobile): stack row blocking gestures and not showing up (#21854)
1 parent 28e9892 commit 9ae4210

File tree

6 files changed

+120
-85
lines changed

6 files changed

+120
-85
lines changed

mobile/lib/domain/services/asset.service.dart

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -40,13 +40,12 @@ class AssetService {
4040

4141
Future<List<RemoteAsset>> getStack(RemoteAsset asset) async {
4242
if (asset.stackId == null) {
43-
return [];
43+
return const [];
4444
}
4545

46-
return _remoteAssetRepository.getStackChildren(asset).then((assets) {
47-
// Include the primary asset in the stack as the first item
48-
return [asset, ...assets];
49-
});
46+
final stack = await _remoteAssetRepository.getStackChildren(asset);
47+
// Include the primary asset in the stack as the first item
48+
return [asset, ...stack];
5049
}
5150

5251
Future<ExifInfo?> getExif(BaseAsset asset) async {

mobile/lib/infrastructure/repositories/remote_asset.repository.dart

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -62,12 +62,13 @@ class RemoteAssetRepository extends DriftDatabaseRepository {
6262
}
6363

6464
Future<List<RemoteAsset>> getStackChildren(RemoteAsset asset) {
65-
if (asset.stackId == null) {
66-
return Future.value([]);
65+
final stackId = asset.stackId;
66+
if (stackId == null) {
67+
return Future.value(const []);
6768
}
6869

6970
final query = _db.remoteAssetEntity.select()
70-
..where((row) => row.stackId.equals(asset.stackId!) & row.id.equals(asset.id).not())
71+
..where((row) => row.stackId.equals(stackId) & row.id.equals(asset.id).not())
7172
..orderBy([(row) => OrderingTerm.desc(row.createdAt)]);
7273

7374
return query.map((row) => row.toDto()).get();

mobile/lib/presentation/widgets/asset_viewer/asset_stack.provider.dart

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,16 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
22
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
33
import 'package:immich_mobile/providers/infrastructure/asset.provider.dart';
44

5-
class StackChildrenNotifier extends AutoDisposeFamilyAsyncNotifier<List<RemoteAsset>, BaseAsset?> {
5+
class StackChildrenNotifier extends AutoDisposeFamilyAsyncNotifier<List<RemoteAsset>, BaseAsset> {
66
@override
7-
Future<List<RemoteAsset>> build(BaseAsset? asset) async {
8-
if (asset == null || asset is! RemoteAsset || asset.stackId == null) {
9-
return const [];
7+
Future<List<RemoteAsset>> build(BaseAsset asset) {
8+
if (asset is! RemoteAsset || asset.stackId == null) {
9+
return Future.value(const []);
1010
}
1111

1212
return ref.watch(assetServiceProvider).getStack(asset);
1313
}
1414
}
1515

1616
final stackChildrenNotifier = AsyncNotifierProvider.autoDispose
17-
.family<StackChildrenNotifier, List<RemoteAsset>, BaseAsset?>(StackChildrenNotifier.new);
17+
.family<StackChildrenNotifier, List<RemoteAsset>, BaseAsset>(StackChildrenNotifier.new);

mobile/lib/presentation/widgets/asset_viewer/asset_stack.widget.dart

Lines changed: 81 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -3,35 +3,33 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
33
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
44
import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_stack.provider.dart';
55
import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_viewer.state.dart';
6-
import 'package:immich_mobile/presentation/widgets/images/image_provider.dart';
6+
import 'package:immich_mobile/presentation/widgets/images/thumbnail.widget.dart';
77
import 'package:immich_mobile/providers/infrastructure/asset_viewer/current_asset.provider.dart';
88

99
class AssetStackRow extends ConsumerWidget {
1010
const AssetStackRow({super.key});
1111

1212
@override
1313
Widget build(BuildContext context, WidgetRef ref) {
14-
int opacity = ref.watch(assetViewerProvider.select((state) => state.backgroundOpacity));
15-
final showControls = ref.watch(assetViewerProvider.select((s) => s.showingControls));
14+
final asset = ref.watch(assetViewerProvider.select((state) => state.currentAsset));
15+
if (asset == null) {
16+
return const SizedBox.shrink();
17+
}
1618

17-
if (!showControls) {
18-
opacity = 0;
19+
final stackChildren = ref.watch(stackChildrenNotifier(asset)).valueOrNull;
20+
if (stackChildren == null || stackChildren.isEmpty) {
21+
return const SizedBox.shrink();
1922
}
2023

21-
final asset = ref.watch(assetViewerProvider.select((s) => s.currentAsset));
24+
final showControls = ref.watch(assetViewerProvider.select((s) => s.showingControls));
25+
final opacity = showControls ? ref.watch(assetViewerProvider.select((state) => state.backgroundOpacity)) : 0;
2226

2327
return IgnorePointer(
2428
ignoring: opacity < 255,
2529
child: AnimatedOpacity(
2630
opacity: opacity / 255,
2731
duration: Durations.short2,
28-
child: ref
29-
.watch(stackChildrenNotifier(asset))
30-
.when(
31-
data: (state) => SizedBox.square(dimension: 80, child: _StackList(stack: state)),
32-
error: (_, __) => const SizedBox.shrink(),
33-
loading: () => const SizedBox.shrink(),
34-
),
32+
child: _StackList(stack: stackChildren),
3533
),
3634
);
3735
}
@@ -44,58 +42,77 @@ class _StackList extends ConsumerWidget {
4442

4543
@override
4644
Widget build(BuildContext context, WidgetRef ref) {
47-
return ListView.builder(
48-
scrollDirection: Axis.horizontal,
49-
padding: const EdgeInsets.only(left: 5, right: 5, bottom: 30),
50-
itemCount: stack.length,
51-
itemBuilder: (ctx, index) {
52-
final asset = stack[index];
53-
return Padding(
54-
padding: const EdgeInsets.only(right: 5),
55-
child: GestureDetector(
56-
onTap: () {
57-
ref.read(assetViewerProvider.notifier).setStackIndex(index);
58-
ref.read(currentAssetNotifier.notifier).setAsset(asset);
59-
},
60-
child: Container(
61-
height: 60,
62-
width: 60,
63-
decoration: index == ref.watch(assetViewerProvider.select((s) => s.stackIndex))
64-
? const BoxDecoration(
65-
color: Colors.white,
66-
borderRadius: BorderRadius.all(Radius.circular(6)),
67-
border: Border.fromBorderSide(BorderSide(color: Colors.white, width: 2)),
68-
)
69-
: const BoxDecoration(
70-
color: Colors.white,
71-
borderRadius: BorderRadius.all(Radius.circular(6)),
72-
border: null,
73-
),
74-
child: ClipRRect(
75-
borderRadius: const BorderRadius.all(Radius.circular(4)),
76-
child: Stack(
77-
fit: StackFit.expand,
78-
children: [
79-
Image(
80-
fit: BoxFit.cover,
81-
image: getThumbnailImageProvider(remoteId: asset.id, size: const Size.square(60)),
82-
),
83-
if (asset.isVideo)
84-
const Icon(
85-
Icons.play_circle_outline_rounded,
86-
color: Colors.white,
87-
size: 16,
88-
shadows: [
89-
Shadow(blurRadius: 5.0, color: Color.fromRGBO(0, 0, 0, 0.6), offset: Offset(0.0, 0.0)),
90-
],
91-
),
92-
],
93-
),
94-
),
95-
),
45+
return Center(
46+
child: SingleChildScrollView(
47+
scrollDirection: Axis.horizontal,
48+
child: Padding(
49+
padding: const EdgeInsets.only(left: 10.0, right: 10.0, bottom: 20.0),
50+
child: Row(
51+
mainAxisAlignment: MainAxisAlignment.center,
52+
spacing: 5.0,
53+
children: List.generate(stack.length, (i) {
54+
final asset = stack[i];
55+
return _StackItem(key: ValueKey(asset.heroTag), asset: asset, index: i);
56+
}),
9657
),
97-
);
98-
},
58+
),
59+
),
60+
);
61+
}
62+
}
63+
64+
class _StackItem extends ConsumerStatefulWidget {
65+
final RemoteAsset asset;
66+
final int index;
67+
68+
const _StackItem({super.key, required this.asset, required this.index});
69+
70+
@override
71+
ConsumerState<_StackItem> createState() => _StackItemState();
72+
}
73+
74+
class _StackItemState extends ConsumerState<_StackItem> {
75+
void _onTap() {
76+
ref.read(currentAssetNotifier.notifier).setAsset(widget.asset);
77+
ref.read(assetViewerProvider.notifier).setStackIndex(widget.index);
78+
}
79+
80+
@override
81+
Widget build(BuildContext context) {
82+
const playIcon = Center(
83+
child: Icon(
84+
Icons.play_circle_outline_rounded,
85+
color: Colors.white,
86+
size: 16,
87+
shadows: [Shadow(blurRadius: 5.0, color: Color.fromRGBO(0, 0, 0, 0.6), offset: Offset(0.0, 0.0))],
88+
),
89+
);
90+
const selectedDecoration = BoxDecoration(
91+
border: Border.fromBorderSide(BorderSide(color: Colors.white, width: 2)),
92+
borderRadius: BorderRadius.all(Radius.circular(10)),
93+
);
94+
const unselectedDecoration = BoxDecoration(
95+
border: Border.fromBorderSide(BorderSide(color: Colors.grey, width: 0.5)),
96+
borderRadius: BorderRadius.all(Radius.circular(10)),
97+
);
98+
99+
Widget thumbnail = Thumbnail.fromAsset(asset: widget.asset, size: const Size(60, 40));
100+
if (widget.asset.isVideo) {
101+
thumbnail = Stack(children: [thumbnail, playIcon]);
102+
}
103+
thumbnail = ClipRRect(borderRadius: const BorderRadius.all(Radius.circular(10)), child: thumbnail);
104+
final isSelected = ref.watch(assetViewerProvider.select((s) => s.stackIndex == widget.index));
105+
return SizedBox(
106+
width: 60,
107+
height: 40,
108+
child: GestureDetector(
109+
onTap: _onTap,
110+
child: DecoratedBox(
111+
decoration: isSelected ? selectedDecoration : unselectedDecoration,
112+
position: DecorationPosition.foreground,
113+
child: thumbnail,
114+
),
115+
),
99116
);
100117
}
101118
}

mobile/lib/presentation/widgets/asset_viewer/asset_viewer.page.dart

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -61,8 +61,15 @@ class AssetViewer extends ConsumerStatefulWidget {
6161
ConsumerState createState() => _AssetViewerState();
6262

6363
static void setAsset(WidgetRef ref, BaseAsset asset) {
64-
// Always dim the background
65-
ref.read(assetViewerProvider.notifier).setOpacity(255);
64+
ref.read(assetViewerProvider.notifier).reset();
65+
_setAsset(ref, asset);
66+
}
67+
68+
void changeAsset(WidgetRef ref, BaseAsset asset) {
69+
_setAsset(ref, asset);
70+
}
71+
72+
static void _setAsset(WidgetRef ref, BaseAsset asset) {
6673
// Always holds the current asset from the timeline
6774
ref.read(assetViewerProvider.notifier).setAsset(asset);
6875
// The currentAssetNotifier actually holds the current asset that is displayed
@@ -109,6 +116,8 @@ class _AssetViewerState extends ConsumerState<AssetViewer> {
109116
ImageStream? _prevPreCacheStream;
110117
ImageStream? _nextPreCacheStream;
111118

119+
KeepAliveLink? _stackChildrenKeepAlive;
120+
112121
@override
113122
void initState() {
114123
super.initState();
@@ -119,6 +128,10 @@ class _AssetViewerState extends ConsumerState<AssetViewer> {
119128
WidgetsBinding.instance.addPostFrameCallback(_onAssetInit);
120129
reloadSubscription = EventStream.shared.listen(_onEvent);
121130
heroOffset = widget.heroOffset ?? TabsRouterScope.of(context)?.controller.activeIndex ?? 0;
131+
final asset = ref.read(currentAssetNotifier);
132+
if (asset != null) {
133+
_stackChildrenKeepAlive = ref.read(stackChildrenNotifier(asset).notifier).ref.keepAlive();
134+
}
122135
}
123136

124137
@override
@@ -130,6 +143,7 @@ class _AssetViewerState extends ConsumerState<AssetViewer> {
130143
_prevPreCacheStream?.removeListener(_dummyListener);
131144
_nextPreCacheStream?.removeListener(_dummyListener);
132145
SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);
146+
_stackChildrenKeepAlive?.close();
133147
super.dispose();
134148
}
135149

@@ -190,9 +204,11 @@ class _AssetViewerState extends ConsumerState<AssetViewer> {
190204
return;
191205
}
192206

193-
AssetViewer.setAsset(ref, asset);
207+
widget.changeAsset(ref, asset);
194208
_precacheAssets(index);
195209
_handleCasting();
210+
_stackChildrenKeepAlive?.close();
211+
_stackChildrenKeepAlive = ref.read(stackChildrenNotifier(asset).notifier).ref.keepAlive();
196212
}
197213

198214
void _handleCasting() {
@@ -520,7 +536,7 @@ class _AssetViewerState extends ConsumerState<AssetViewer> {
520536
BaseAsset displayAsset = asset;
521537
final stackChildren = ref.read(stackChildrenNotifier(asset)).valueOrNull;
522538
if (stackChildren != null && stackChildren.isNotEmpty) {
523-
displayAsset = stackChildren.elementAt(ref.read(assetViewerProvider.select((s) => s.stackIndex)));
539+
displayAsset = stackChildren.elementAt(ref.read(assetViewerProvider).stackIndex);
524540
}
525541

526542
final isPlayingMotionVideo = ref.read(isPlayingMotionVideoProvider);

mobile/lib/presentation/widgets/asset_viewer/asset_viewer.state.dart

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -68,12 +68,16 @@ class AssetViewerState {
6868
stackIndex.hashCode;
6969
}
7070

71-
class AssetViewerStateNotifier extends AutoDisposeNotifier<AssetViewerState> {
71+
class AssetViewerStateNotifier extends Notifier<AssetViewerState> {
7272
@override
7373
AssetViewerState build() {
7474
return const AssetViewerState();
7575
}
7676

77+
void reset() {
78+
state = const AssetViewerState();
79+
}
80+
7781
void setAsset(BaseAsset? asset) {
7882
if (asset == state.currentAsset) {
7983
return;
@@ -117,6 +121,4 @@ class AssetViewerStateNotifier extends AutoDisposeNotifier<AssetViewerState> {
117121
}
118122
}
119123

120-
final assetViewerProvider = AutoDisposeNotifierProvider<AssetViewerStateNotifier, AssetViewerState>(
121-
AssetViewerStateNotifier.new,
122-
);
124+
final assetViewerProvider = NotifierProvider<AssetViewerStateNotifier, AssetViewerState>(AssetViewerStateNotifier.new);

0 commit comments

Comments
 (0)