mirror of
https://github.com/immich-app/immich
synced 2025-11-07 17:27:20 +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 {
|
Future<List<RemoteAsset>> getStack(RemoteAsset asset) async {
|
||||||
if (asset.stackId == null) {
|
if (asset.stackId == null) {
|
||||||
return [];
|
return const [];
|
||||||
}
|
}
|
||||||
|
|
||||||
return _remoteAssetRepository.getStackChildren(asset).then((assets) {
|
final stack = await _remoteAssetRepository.getStackChildren(asset);
|
||||||
// Include the primary asset in the stack as the first item
|
// Include the primary asset in the stack as the first item
|
||||||
return [asset, ...assets];
|
return [asset, ...stack];
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<ExifInfo?> getExif(BaseAsset asset) async {
|
Future<ExifInfo?> getExif(BaseAsset asset) async {
|
||||||
|
|
|
||||||
|
|
@ -62,12 +62,13 @@ class RemoteAssetRepository extends DriftDatabaseRepository {
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<List<RemoteAsset>> getStackChildren(RemoteAsset asset) {
|
Future<List<RemoteAsset>> getStackChildren(RemoteAsset asset) {
|
||||||
if (asset.stackId == null) {
|
final stackId = asset.stackId;
|
||||||
return Future.value([]);
|
if (stackId == null) {
|
||||||
|
return Future.value(const []);
|
||||||
}
|
}
|
||||||
|
|
||||||
final query = _db.remoteAssetEntity.select()
|
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)]);
|
..orderBy([(row) => OrderingTerm.desc(row.createdAt)]);
|
||||||
|
|
||||||
return query.map((row) => row.toDto()).get();
|
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/domain/models/asset/base_asset.model.dart';
|
||||||
import 'package:immich_mobile/providers/infrastructure/asset.provider.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
|
@override
|
||||||
Future<List<RemoteAsset>> build(BaseAsset? asset) async {
|
Future<List<RemoteAsset>> build(BaseAsset asset) {
|
||||||
if (asset == null || asset is! RemoteAsset || asset.stackId == null) {
|
if (asset is! RemoteAsset || asset.stackId == null) {
|
||||||
return const [];
|
return Future.value(const []);
|
||||||
}
|
}
|
||||||
|
|
||||||
return ref.watch(assetServiceProvider).getStack(asset);
|
return ref.watch(assetServiceProvider).getStack(asset);
|
||||||
|
|
@ -14,4 +14,4 @@ class StackChildrenNotifier extends AutoDisposeFamilyAsyncNotifier<List<RemoteAs
|
||||||
}
|
}
|
||||||
|
|
||||||
final stackChildrenNotifier = AsyncNotifierProvider.autoDispose
|
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/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_stack.provider.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_viewer.state.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';
|
import 'package:immich_mobile/providers/infrastructure/asset_viewer/current_asset.provider.dart';
|
||||||
|
|
||||||
class AssetStackRow extends ConsumerWidget {
|
class AssetStackRow extends ConsumerWidget {
|
||||||
|
|
@ -11,27 +11,25 @@ class AssetStackRow extends ConsumerWidget {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
int opacity = ref.watch(assetViewerProvider.select((state) => state.backgroundOpacity));
|
final asset = ref.watch(assetViewerProvider.select((state) => state.currentAsset));
|
||||||
final showControls = ref.watch(assetViewerProvider.select((s) => s.showingControls));
|
if (asset == null) {
|
||||||
|
return const SizedBox.shrink();
|
||||||
if (!showControls) {
|
|
||||||
opacity = 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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(
|
return IgnorePointer(
|
||||||
ignoring: opacity < 255,
|
ignoring: opacity < 255,
|
||||||
child: AnimatedOpacity(
|
child: AnimatedOpacity(
|
||||||
opacity: opacity / 255,
|
opacity: opacity / 255,
|
||||||
duration: Durations.short2,
|
duration: Durations.short2,
|
||||||
child: ref
|
child: _StackList(stack: stackChildren),
|
||||||
.watch(stackChildrenNotifier(asset))
|
|
||||||
.when(
|
|
||||||
data: (state) => SizedBox.square(dimension: 80, child: _StackList(stack: state)),
|
|
||||||
error: (_, __) => const SizedBox.shrink(),
|
|
||||||
loading: () => const SizedBox.shrink(),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -44,58 +42,77 @@ class _StackList extends ConsumerWidget {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
return ListView.builder(
|
return Center(
|
||||||
|
child: SingleChildScrollView(
|
||||||
scrollDirection: Axis.horizontal,
|
scrollDirection: Axis.horizontal,
|
||||||
padding: const EdgeInsets.only(left: 5, right: 5, bottom: 30),
|
child: Padding(
|
||||||
itemCount: stack.length,
|
padding: const EdgeInsets.only(left: 10.0, right: 10.0, bottom: 20.0),
|
||||||
itemBuilder: (ctx, index) {
|
child: Row(
|
||||||
final asset = stack[index];
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
return Padding(
|
spacing: 5.0,
|
||||||
padding: const EdgeInsets.only(right: 5),
|
children: List.generate(stack.length, (i) {
|
||||||
child: GestureDetector(
|
final asset = stack[i];
|
||||||
onTap: () {
|
return _StackItem(key: ValueKey(asset.heroTag), asset: asset, index: i);
|
||||||
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(
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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,
|
Icons.play_circle_outline_rounded,
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
size: 16,
|
size: 16,
|
||||||
shadows: [
|
shadows: [Shadow(blurRadius: 5.0, color: Color.fromRGBO(0, 0, 0, 0.6), offset: Offset(0.0, 0.0))],
|
||||||
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();
|
ConsumerState createState() => _AssetViewerState();
|
||||||
|
|
||||||
static void setAsset(WidgetRef ref, BaseAsset asset) {
|
static void setAsset(WidgetRef ref, BaseAsset asset) {
|
||||||
// Always dim the background
|
ref.read(assetViewerProvider.notifier).reset();
|
||||||
ref.read(assetViewerProvider.notifier).setOpacity(255);
|
_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
|
// Always holds the current asset from the timeline
|
||||||
ref.read(assetViewerProvider.notifier).setAsset(asset);
|
ref.read(assetViewerProvider.notifier).setAsset(asset);
|
||||||
// The currentAssetNotifier actually holds the current asset that is displayed
|
// The currentAssetNotifier actually holds the current asset that is displayed
|
||||||
|
|
@ -109,6 +116,8 @@ class _AssetViewerState extends ConsumerState<AssetViewer> {
|
||||||
ImageStream? _prevPreCacheStream;
|
ImageStream? _prevPreCacheStream;
|
||||||
ImageStream? _nextPreCacheStream;
|
ImageStream? _nextPreCacheStream;
|
||||||
|
|
||||||
|
KeepAliveLink? _stackChildrenKeepAlive;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
|
|
@ -119,6 +128,10 @@ class _AssetViewerState extends ConsumerState<AssetViewer> {
|
||||||
WidgetsBinding.instance.addPostFrameCallback(_onAssetInit);
|
WidgetsBinding.instance.addPostFrameCallback(_onAssetInit);
|
||||||
reloadSubscription = EventStream.shared.listen(_onEvent);
|
reloadSubscription = EventStream.shared.listen(_onEvent);
|
||||||
heroOffset = widget.heroOffset ?? TabsRouterScope.of(context)?.controller.activeIndex ?? 0;
|
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
|
@override
|
||||||
|
|
@ -130,6 +143,7 @@ class _AssetViewerState extends ConsumerState<AssetViewer> {
|
||||||
_prevPreCacheStream?.removeListener(_dummyListener);
|
_prevPreCacheStream?.removeListener(_dummyListener);
|
||||||
_nextPreCacheStream?.removeListener(_dummyListener);
|
_nextPreCacheStream?.removeListener(_dummyListener);
|
||||||
SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);
|
SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);
|
||||||
|
_stackChildrenKeepAlive?.close();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -190,9 +204,11 @@ class _AssetViewerState extends ConsumerState<AssetViewer> {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
AssetViewer.setAsset(ref, asset);
|
widget.changeAsset(ref, asset);
|
||||||
_precacheAssets(index);
|
_precacheAssets(index);
|
||||||
_handleCasting();
|
_handleCasting();
|
||||||
|
_stackChildrenKeepAlive?.close();
|
||||||
|
_stackChildrenKeepAlive = ref.read(stackChildrenNotifier(asset).notifier).ref.keepAlive();
|
||||||
}
|
}
|
||||||
|
|
||||||
void _handleCasting() {
|
void _handleCasting() {
|
||||||
|
|
@ -520,7 +536,7 @@ class _AssetViewerState extends ConsumerState<AssetViewer> {
|
||||||
BaseAsset displayAsset = asset;
|
BaseAsset displayAsset = asset;
|
||||||
final stackChildren = ref.read(stackChildrenNotifier(asset)).valueOrNull;
|
final stackChildren = ref.read(stackChildrenNotifier(asset)).valueOrNull;
|
||||||
if (stackChildren != null && stackChildren.isNotEmpty) {
|
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);
|
final isPlayingMotionVideo = ref.read(isPlayingMotionVideoProvider);
|
||||||
|
|
|
||||||
|
|
@ -68,12 +68,16 @@ class AssetViewerState {
|
||||||
stackIndex.hashCode;
|
stackIndex.hashCode;
|
||||||
}
|
}
|
||||||
|
|
||||||
class AssetViewerStateNotifier extends AutoDisposeNotifier<AssetViewerState> {
|
class AssetViewerStateNotifier extends Notifier<AssetViewerState> {
|
||||||
@override
|
@override
|
||||||
AssetViewerState build() {
|
AssetViewerState build() {
|
||||||
return const AssetViewerState();
|
return const AssetViewerState();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void reset() {
|
||||||
|
state = const AssetViewerState();
|
||||||
|
}
|
||||||
|
|
||||||
void setAsset(BaseAsset? asset) {
|
void setAsset(BaseAsset? asset) {
|
||||||
if (asset == state.currentAsset) {
|
if (asset == state.currentAsset) {
|
||||||
return;
|
return;
|
||||||
|
|
@ -117,6 +121,4 @@ class AssetViewerStateNotifier extends AutoDisposeNotifier<AssetViewerState> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final assetViewerProvider = AutoDisposeNotifierProvider<AssetViewerStateNotifier, AssetViewerState>(
|
final assetViewerProvider = NotifierProvider<AssetViewerStateNotifier, AssetViewerState>(AssetViewerStateNotifier.new);
|
||||||
AssetViewerStateNotifier.new,
|
|
||||||
);
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue