mirror of
https://github.com/immich-app/immich
synced 2025-10-17 18:19:27 +00:00
fix(mobile): stack row blocking gestures and not showing up (#21854)
This commit is contained in:
parent
28e9892ed3
commit
9ae42106cc
6 changed files with 121 additions and 86 deletions
|
|
@ -40,13 +40,12 @@ class AssetService {
|
|||
|
||||
Future<List<RemoteAsset>> getStack(RemoteAsset asset) async {
|
||||
if (asset.stackId == null) {
|
||||
return [];
|
||||
return const [];
|
||||
}
|
||||
|
||||
return _remoteAssetRepository.getStackChildren(asset).then((assets) {
|
||||
// Include the primary asset in the stack as the first item
|
||||
return [asset, ...assets];
|
||||
});
|
||||
final stack = await _remoteAssetRepository.getStackChildren(asset);
|
||||
// Include the primary asset in the stack as the first item
|
||||
return [asset, ...stack];
|
||||
}
|
||||
|
||||
Future<ExifInfo?> getExif(BaseAsset asset) async {
|
||||
|
|
|
|||
|
|
@ -62,12 +62,13 @@ class RemoteAssetRepository extends DriftDatabaseRepository {
|
|||
}
|
||||
|
||||
Future<List<RemoteAsset>> getStackChildren(RemoteAsset asset) {
|
||||
if (asset.stackId == null) {
|
||||
return Future.value([]);
|
||||
final stackId = asset.stackId;
|
||||
if (stackId == null) {
|
||||
return Future.value(const []);
|
||||
}
|
||||
|
||||
final query = _db.remoteAssetEntity.select()
|
||||
..where((row) => row.stackId.equals(asset.stackId!) & row.id.equals(asset.id).not())
|
||||
..where((row) => row.stackId.equals(stackId) & row.id.equals(asset.id).not())
|
||||
..orderBy([(row) => OrderingTerm.desc(row.createdAt)]);
|
||||
|
||||
return query.map((row) => row.toDto()).get();
|
||||
|
|
|
|||
|
|
@ -2,11 +2,11 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
|
|||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/asset.provider.dart';
|
||||
|
||||
class StackChildrenNotifier extends AutoDisposeFamilyAsyncNotifier<List<RemoteAsset>, BaseAsset?> {
|
||||
class StackChildrenNotifier extends AutoDisposeFamilyAsyncNotifier<List<RemoteAsset>, BaseAsset> {
|
||||
@override
|
||||
Future<List<RemoteAsset>> build(BaseAsset? asset) async {
|
||||
if (asset == null || asset is! RemoteAsset || asset.stackId == null) {
|
||||
return const [];
|
||||
Future<List<RemoteAsset>> build(BaseAsset asset) {
|
||||
if (asset is! RemoteAsset || asset.stackId == null) {
|
||||
return Future.value(const []);
|
||||
}
|
||||
|
||||
return ref.watch(assetServiceProvider).getStack(asset);
|
||||
|
|
@ -14,4 +14,4 @@ class StackChildrenNotifier extends AutoDisposeFamilyAsyncNotifier<List<RemoteAs
|
|||
}
|
||||
|
||||
final stackChildrenNotifier = AsyncNotifierProvider.autoDispose
|
||||
.family<StackChildrenNotifier, List<RemoteAsset>, BaseAsset?>(StackChildrenNotifier.new);
|
||||
.family<StackChildrenNotifier, List<RemoteAsset>, BaseAsset>(StackChildrenNotifier.new);
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
|
|||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_stack.provider.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_viewer.state.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/images/image_provider.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/images/thumbnail.widget.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/asset_viewer/current_asset.provider.dart';
|
||||
|
||||
class AssetStackRow extends ConsumerWidget {
|
||||
|
|
@ -11,27 +11,25 @@ class AssetStackRow extends ConsumerWidget {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
int opacity = ref.watch(assetViewerProvider.select((state) => state.backgroundOpacity));
|
||||
final showControls = ref.watch(assetViewerProvider.select((s) => s.showingControls));
|
||||
|
||||
if (!showControls) {
|
||||
opacity = 0;
|
||||
final asset = ref.watch(assetViewerProvider.select((state) => state.currentAsset));
|
||||
if (asset == null) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
final asset = ref.watch(assetViewerProvider.select((s) => s.currentAsset));
|
||||
final stackChildren = ref.watch(stackChildrenNotifier(asset)).valueOrNull;
|
||||
if (stackChildren == null || stackChildren.isEmpty) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
final showControls = ref.watch(assetViewerProvider.select((s) => s.showingControls));
|
||||
final opacity = showControls ? ref.watch(assetViewerProvider.select((state) => state.backgroundOpacity)) : 0;
|
||||
|
||||
return IgnorePointer(
|
||||
ignoring: opacity < 255,
|
||||
child: AnimatedOpacity(
|
||||
opacity: opacity / 255,
|
||||
duration: Durations.short2,
|
||||
child: ref
|
||||
.watch(stackChildrenNotifier(asset))
|
||||
.when(
|
||||
data: (state) => SizedBox.square(dimension: 80, child: _StackList(stack: state)),
|
||||
error: (_, __) => const SizedBox.shrink(),
|
||||
loading: () => const SizedBox.shrink(),
|
||||
),
|
||||
child: _StackList(stack: stackChildren),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
@ -44,58 +42,77 @@ class _StackList extends ConsumerWidget {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
return ListView.builder(
|
||||
scrollDirection: Axis.horizontal,
|
||||
padding: const EdgeInsets.only(left: 5, right: 5, bottom: 30),
|
||||
itemCount: stack.length,
|
||||
itemBuilder: (ctx, index) {
|
||||
final asset = stack[index];
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(right: 5),
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
ref.read(assetViewerProvider.notifier).setStackIndex(index);
|
||||
ref.read(currentAssetNotifier.notifier).setAsset(asset);
|
||||
},
|
||||
child: Container(
|
||||
height: 60,
|
||||
width: 60,
|
||||
decoration: index == ref.watch(assetViewerProvider.select((s) => s.stackIndex))
|
||||
? const BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.all(Radius.circular(6)),
|
||||
border: Border.fromBorderSide(BorderSide(color: Colors.white, width: 2)),
|
||||
)
|
||||
: const BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.all(Radius.circular(6)),
|
||||
border: null,
|
||||
),
|
||||
child: ClipRRect(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(4)),
|
||||
child: Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
Image(
|
||||
fit: BoxFit.cover,
|
||||
image: getThumbnailImageProvider(remoteId: asset.id, size: const Size.square(60)),
|
||||
),
|
||||
if (asset.isVideo)
|
||||
const Icon(
|
||||
Icons.play_circle_outline_rounded,
|
||||
color: Colors.white,
|
||||
size: 16,
|
||||
shadows: [
|
||||
Shadow(blurRadius: 5.0, color: Color.fromRGBO(0, 0, 0, 0.6), offset: Offset(0.0, 0.0)),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
return Center(
|
||||
child: SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(left: 10.0, right: 10.0, bottom: 20.0),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
spacing: 5.0,
|
||||
children: List.generate(stack.length, (i) {
|
||||
final asset = stack[i];
|
||||
return _StackItem(key: ValueKey(asset.heroTag), asset: asset, index: i);
|
||||
}),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _StackItem extends ConsumerStatefulWidget {
|
||||
final RemoteAsset asset;
|
||||
final int index;
|
||||
|
||||
const _StackItem({super.key, required this.asset, required this.index});
|
||||
|
||||
@override
|
||||
ConsumerState<_StackItem> createState() => _StackItemState();
|
||||
}
|
||||
|
||||
class _StackItemState extends ConsumerState<_StackItem> {
|
||||
void _onTap() {
|
||||
ref.read(currentAssetNotifier.notifier).setAsset(widget.asset);
|
||||
ref.read(assetViewerProvider.notifier).setStackIndex(widget.index);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
const playIcon = Center(
|
||||
child: Icon(
|
||||
Icons.play_circle_outline_rounded,
|
||||
color: Colors.white,
|
||||
size: 16,
|
||||
shadows: [Shadow(blurRadius: 5.0, color: Color.fromRGBO(0, 0, 0, 0.6), offset: Offset(0.0, 0.0))],
|
||||
),
|
||||
);
|
||||
const selectedDecoration = BoxDecoration(
|
||||
border: Border.fromBorderSide(BorderSide(color: Colors.white, width: 2)),
|
||||
borderRadius: BorderRadius.all(Radius.circular(10)),
|
||||
);
|
||||
const unselectedDecoration = BoxDecoration(
|
||||
border: Border.fromBorderSide(BorderSide(color: Colors.grey, width: 0.5)),
|
||||
borderRadius: BorderRadius.all(Radius.circular(10)),
|
||||
);
|
||||
|
||||
Widget thumbnail = Thumbnail.fromAsset(asset: widget.asset, size: const Size(60, 40));
|
||||
if (widget.asset.isVideo) {
|
||||
thumbnail = Stack(children: [thumbnail, playIcon]);
|
||||
}
|
||||
thumbnail = ClipRRect(borderRadius: const BorderRadius.all(Radius.circular(10)), child: thumbnail);
|
||||
final isSelected = ref.watch(assetViewerProvider.select((s) => s.stackIndex == widget.index));
|
||||
return SizedBox(
|
||||
width: 60,
|
||||
height: 40,
|
||||
child: GestureDetector(
|
||||
onTap: _onTap,
|
||||
child: DecoratedBox(
|
||||
decoration: isSelected ? selectedDecoration : unselectedDecoration,
|
||||
position: DecorationPosition.foreground,
|
||||
child: thumbnail,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -61,8 +61,15 @@ class AssetViewer extends ConsumerStatefulWidget {
|
|||
ConsumerState createState() => _AssetViewerState();
|
||||
|
||||
static void setAsset(WidgetRef ref, BaseAsset asset) {
|
||||
// Always dim the background
|
||||
ref.read(assetViewerProvider.notifier).setOpacity(255);
|
||||
ref.read(assetViewerProvider.notifier).reset();
|
||||
_setAsset(ref, asset);
|
||||
}
|
||||
|
||||
void changeAsset(WidgetRef ref, BaseAsset asset) {
|
||||
_setAsset(ref, asset);
|
||||
}
|
||||
|
||||
static void _setAsset(WidgetRef ref, BaseAsset asset) {
|
||||
// Always holds the current asset from the timeline
|
||||
ref.read(assetViewerProvider.notifier).setAsset(asset);
|
||||
// The currentAssetNotifier actually holds the current asset that is displayed
|
||||
|
|
@ -109,6 +116,8 @@ class _AssetViewerState extends ConsumerState<AssetViewer> {
|
|||
ImageStream? _prevPreCacheStream;
|
||||
ImageStream? _nextPreCacheStream;
|
||||
|
||||
KeepAliveLink? _stackChildrenKeepAlive;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
|
@ -119,6 +128,10 @@ class _AssetViewerState extends ConsumerState<AssetViewer> {
|
|||
WidgetsBinding.instance.addPostFrameCallback(_onAssetInit);
|
||||
reloadSubscription = EventStream.shared.listen(_onEvent);
|
||||
heroOffset = widget.heroOffset ?? TabsRouterScope.of(context)?.controller.activeIndex ?? 0;
|
||||
final asset = ref.read(currentAssetNotifier);
|
||||
if (asset != null) {
|
||||
_stackChildrenKeepAlive = ref.read(stackChildrenNotifier(asset).notifier).ref.keepAlive();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
|
|
@ -130,6 +143,7 @@ class _AssetViewerState extends ConsumerState<AssetViewer> {
|
|||
_prevPreCacheStream?.removeListener(_dummyListener);
|
||||
_nextPreCacheStream?.removeListener(_dummyListener);
|
||||
SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);
|
||||
_stackChildrenKeepAlive?.close();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
|
|
@ -190,9 +204,11 @@ class _AssetViewerState extends ConsumerState<AssetViewer> {
|
|||
return;
|
||||
}
|
||||
|
||||
AssetViewer.setAsset(ref, asset);
|
||||
widget.changeAsset(ref, asset);
|
||||
_precacheAssets(index);
|
||||
_handleCasting();
|
||||
_stackChildrenKeepAlive?.close();
|
||||
_stackChildrenKeepAlive = ref.read(stackChildrenNotifier(asset).notifier).ref.keepAlive();
|
||||
}
|
||||
|
||||
void _handleCasting() {
|
||||
|
|
@ -520,7 +536,7 @@ class _AssetViewerState extends ConsumerState<AssetViewer> {
|
|||
BaseAsset displayAsset = asset;
|
||||
final stackChildren = ref.read(stackChildrenNotifier(asset)).valueOrNull;
|
||||
if (stackChildren != null && stackChildren.isNotEmpty) {
|
||||
displayAsset = stackChildren.elementAt(ref.read(assetViewerProvider.select((s) => s.stackIndex)));
|
||||
displayAsset = stackChildren.elementAt(ref.read(assetViewerProvider).stackIndex);
|
||||
}
|
||||
|
||||
final isPlayingMotionVideo = ref.read(isPlayingMotionVideoProvider);
|
||||
|
|
|
|||
|
|
@ -68,12 +68,16 @@ class AssetViewerState {
|
|||
stackIndex.hashCode;
|
||||
}
|
||||
|
||||
class AssetViewerStateNotifier extends AutoDisposeNotifier<AssetViewerState> {
|
||||
class AssetViewerStateNotifier extends Notifier<AssetViewerState> {
|
||||
@override
|
||||
AssetViewerState build() {
|
||||
return const AssetViewerState();
|
||||
}
|
||||
|
||||
void reset() {
|
||||
state = const AssetViewerState();
|
||||
}
|
||||
|
||||
void setAsset(BaseAsset? asset) {
|
||||
if (asset == state.currentAsset) {
|
||||
return;
|
||||
|
|
@ -117,6 +121,4 @@ class AssetViewerStateNotifier extends AutoDisposeNotifier<AssetViewerState> {
|
|||
}
|
||||
}
|
||||
|
||||
final assetViewerProvider = AutoDisposeNotifierProvider<AssetViewerStateNotifier, AssetViewerState>(
|
||||
AssetViewerStateNotifier.new,
|
||||
);
|
||||
final assetViewerProvider = NotifierProvider<AssetViewerStateNotifier, AssetViewerState>(AssetViewerStateNotifier.new);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue