mirror of
https://github.com/immich-app/immich
synced 2025-11-14 17:36:12 +00:00
feat(mobile): Change the UI of asset activity list to bottom sheet (#23075)
* init of activities bottom sheet * reverse list order, padding bottom... * chore: remove scrolling * chore: clean up * chore --------- Co-authored-by: Alex <alex.tran1502@gmail.com>
This commit is contained in:
parent
05f174a180
commit
becb56e1b1
8 changed files with 243 additions and 27 deletions
|
|
@ -7,6 +7,7 @@ import 'package:immich_mobile/widgets/common/user_circle_avatar.dart';
|
|||
|
||||
class DriftActivityTextField extends ConsumerStatefulWidget {
|
||||
final bool isEnabled;
|
||||
final bool isBottomSheet;
|
||||
final String? likeId;
|
||||
final Function(String) onSubmit;
|
||||
final Function()? onKeyboardFocus;
|
||||
|
|
@ -16,6 +17,7 @@ class DriftActivityTextField extends ConsumerStatefulWidget {
|
|||
this.isEnabled = true,
|
||||
this.likeId,
|
||||
this.onKeyboardFocus,
|
||||
this.isBottomSheet = false,
|
||||
super.key,
|
||||
});
|
||||
|
||||
|
|
@ -34,7 +36,9 @@ class _DriftActivityTextFieldState extends ConsumerState<DriftActivityTextField>
|
|||
inputController = TextEditingController();
|
||||
inputFocusNode = FocusNode();
|
||||
|
||||
if (!widget.isBottomSheet) {
|
||||
inputFocusNode.requestFocus();
|
||||
}
|
||||
|
||||
inputFocusNode.addListener(() {
|
||||
if (inputFocusNode.hasFocus) {
|
||||
|
|
@ -72,7 +76,7 @@ class _DriftActivityTextFieldState extends ConsumerState<DriftActivityTextField>
|
|||
}
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 10),
|
||||
padding: EdgeInsets.symmetric(vertical: widget.isBottomSheet ? 0 : 10),
|
||||
child: TextField(
|
||||
controller: inputController,
|
||||
enabled: widget.isEnabled,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,95 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||
import 'package:immich_mobile/extensions/asyncvalue_extensions.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/album/drift_activity_text_field.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/bottom_sheet/base_bottom_sheet.widget.dart';
|
||||
import 'package:immich_mobile/providers/activity.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/asset_viewer/current_asset.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/current_album.provider.dart';
|
||||
import 'package:immich_mobile/providers/user.provider.dart';
|
||||
import 'package:immich_mobile/widgets/activities/activity_tile.dart';
|
||||
import 'package:immich_mobile/widgets/activities/dismissible_activity.dart';
|
||||
|
||||
class ActivitiesBottomSheet extends HookConsumerWidget {
|
||||
final DraggableScrollableController controller;
|
||||
final double initialChildSize;
|
||||
final bool scrollToBottomInitially;
|
||||
|
||||
const ActivitiesBottomSheet({
|
||||
required this.controller,
|
||||
this.initialChildSize = 0.35,
|
||||
this.scrollToBottomInitially = true,
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final album = ref.watch(currentRemoteAlbumProvider)!;
|
||||
final asset = ref.watch(currentAssetNotifier) as RemoteAsset?;
|
||||
final user = ref.watch(currentUserProvider);
|
||||
|
||||
final activityNotifier = ref.read(albumActivityProvider(album.id, asset?.id).notifier);
|
||||
final activities = ref.watch(albumActivityProvider(album.id, asset?.id));
|
||||
|
||||
Future<void> onAddComment(String comment) async {
|
||||
await activityNotifier.addComment(comment);
|
||||
}
|
||||
|
||||
Widget buildActivitiesSliver() {
|
||||
return activities.widgetWhen(
|
||||
onLoading: () => const SliverToBoxAdapter(child: SizedBox.shrink()),
|
||||
onData: (data) {
|
||||
return SliverList(
|
||||
delegate: SliverChildBuilderDelegate((context, index) {
|
||||
if (index == data.length) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
final activity = data[data.length - 1 - index];
|
||||
final canDelete = activity.user.id == user?.id || album.ownerId == user?.id;
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 1),
|
||||
child: DismissibleActivity(
|
||||
activity.id,
|
||||
ActivityTile(activity, isBottomSheet: true),
|
||||
onDismiss: canDelete
|
||||
? (activityId) async => await activityNotifier.removeActivity(activity.id)
|
||||
: null,
|
||||
),
|
||||
);
|
||||
}, childCount: data.length + 1),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
return BaseBottomSheet(
|
||||
actions: [],
|
||||
slivers: [buildActivitiesSliver()],
|
||||
footer: Padding(
|
||||
// TODO: avoid fixed padding, use context.padding.bottom
|
||||
padding: const EdgeInsets.only(bottom: 32),
|
||||
child: Column(
|
||||
children: [
|
||||
const Divider(indent: 16, endIndent: 16),
|
||||
DriftActivityTextField(
|
||||
isEnabled: album.isActivityEnabled,
|
||||
isBottomSheet: true,
|
||||
// likeId: likedId,
|
||||
onSubmit: onAddComment,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
controller: controller,
|
||||
initialChildSize: initialChildSize,
|
||||
minChildSize: 0.1,
|
||||
maxChildSize: 0.88,
|
||||
expand: false,
|
||||
shouldCloseOnMinExtent: false,
|
||||
resizeOnScroll: false,
|
||||
backgroundColor: context.isDarkTheme ? Colors.black : Colors.white,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -20,6 +20,7 @@ import 'package:immich_mobile/presentation/widgets/asset_viewer/bottom_bar.widge
|
|||
import 'package:immich_mobile/presentation/widgets/asset_viewer/bottom_sheet.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/asset_viewer/top_app_bar.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/asset_viewer/video_viewer.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/asset_viewer/activities_bottom_sheet.widget.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/asset_viewer/is_motion_video_playing.provider.dart';
|
||||
|
|
@ -418,7 +419,7 @@ class _AssetViewerState extends ConsumerState<AssetViewer> {
|
|||
|
||||
if (event is ViewerOpenBottomSheetEvent) {
|
||||
final extent = _kBottomSheetMinimumExtent + 0.3;
|
||||
_openBottomSheet(scaffoldContext!, extent: extent);
|
||||
_openBottomSheet(scaffoldContext!, extent: extent, activitiesMode: event.activitiesMode);
|
||||
final offset = _getVerticalOffsetForBottomSheet(extent);
|
||||
viewController?.position = Offset(0, -offset);
|
||||
return;
|
||||
|
|
@ -460,7 +461,7 @@ class _AssetViewerState extends ConsumerState<AssetViewer> {
|
|||
});
|
||||
}
|
||||
|
||||
void _openBottomSheet(BuildContext ctx, {double extent = _kBottomSheetMinimumExtent}) {
|
||||
void _openBottomSheet(BuildContext ctx, {double extent = _kBottomSheetMinimumExtent, bool activitiesMode = false}) {
|
||||
ref.read(assetViewerProvider.notifier).setBottomSheet(true);
|
||||
initialScale = viewController?.scale;
|
||||
// viewController?.updateMultiple(scale: (viewController?.scale ?? 1.0) + 0.01);
|
||||
|
|
@ -474,7 +475,9 @@ class _AssetViewerState extends ConsumerState<AssetViewer> {
|
|||
builder: (_) {
|
||||
return NotificationListener<Notification>(
|
||||
onNotification: _onNotification,
|
||||
child: AssetDetailBottomSheet(controller: bottomSheetController, initialChildSize: extent),
|
||||
child: activitiesMode
|
||||
? ActivitiesBottomSheet(controller: bottomSheetController, initialChildSize: extent)
|
||||
: AssetDetailBottomSheet(controller: bottomSheetController, initialChildSize: extent),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
|
|
|||
|
|
@ -4,7 +4,8 @@ import 'package:immich_mobile/providers/asset_viewer/video_player_controls_provi
|
|||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
|
||||
class ViewerOpenBottomSheetEvent extends Event {
|
||||
const ViewerOpenBottomSheetEvent();
|
||||
final bool activitiesMode;
|
||||
const ViewerOpenBottomSheetEvent({this.activitiesMode = false});
|
||||
}
|
||||
|
||||
class ViewerReloadAssetEvent extends Event {
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import 'package:immich_mobile/presentation/widgets/action_buttons/favorite_actio
|
|||
import 'package:immich_mobile/presentation/widgets/action_buttons/motion_photo_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/action_buttons/unfavorite_action_button.widget.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_viewer.state.dart';
|
||||
import 'package:immich_mobile/providers/activity.provider.dart';
|
||||
import 'package:immich_mobile/providers/cast.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/asset_viewer/current_asset.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/current_album.provider.dart';
|
||||
|
|
@ -53,6 +54,10 @@ class ViewerTopAppBar extends ConsumerWidget implements PreferredSizeWidget {
|
|||
int opacity = ref.watch(assetViewerProvider.select((state) => state.backgroundOpacity));
|
||||
final showControls = ref.watch(assetViewerProvider.select((s) => s.showingControls));
|
||||
|
||||
if (album != null && album.isActivityEnabled && album.isShared && asset is RemoteAsset) {
|
||||
ref.watch(albumActivityProvider(album.id, asset.id));
|
||||
}
|
||||
|
||||
if (!showControls) {
|
||||
opacity = 0;
|
||||
}
|
||||
|
|
@ -66,7 +71,7 @@ class ViewerTopAppBar extends ConsumerWidget implements PreferredSizeWidget {
|
|||
IconButton(
|
||||
icon: const Icon(Icons.chat_outlined),
|
||||
onPressed: () {
|
||||
context.navigateTo(const DriftActivitiesRoute());
|
||||
EventStream.shared.emit(const ViewerOpenBottomSheetEvent(activitiesMode: true));
|
||||
},
|
||||
),
|
||||
if (showViewInTimelineButton)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,92 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||
import 'package:immich_mobile/extensions/asyncvalue_extensions.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/album/drift_activity_text_field.dart';
|
||||
import 'package:immich_mobile/providers/activity.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/asset_viewer/current_asset.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/current_album.provider.dart';
|
||||
import 'package:immich_mobile/providers/user.provider.dart';
|
||||
import 'package:immich_mobile/widgets/activities/activity_tile.dart';
|
||||
import 'package:immich_mobile/widgets/activities/dismissible_activity.dart';
|
||||
import 'base_bottom_sheet.widget.dart';
|
||||
|
||||
class ActivitiesBottomSheet extends HookConsumerWidget {
|
||||
final DraggableScrollableController controller;
|
||||
final double initialChildSize;
|
||||
final bool scrollToBottomInitially;
|
||||
|
||||
const ActivitiesBottomSheet({
|
||||
required this.controller,
|
||||
this.initialChildSize = 0.35,
|
||||
this.scrollToBottomInitially = true,
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final album = ref.watch(currentRemoteAlbumProvider)!;
|
||||
final asset = ref.watch(currentAssetNotifier) as RemoteAsset?;
|
||||
final user = ref.watch(currentUserProvider);
|
||||
|
||||
final activityNotifier = ref.read(albumActivityProvider(album.id, asset?.id).notifier);
|
||||
final activities = ref.watch(albumActivityProvider(album.id, asset?.id));
|
||||
|
||||
Future<void> onAddComment(String comment) async {
|
||||
await activityNotifier.addComment(comment);
|
||||
}
|
||||
|
||||
Widget buildActivitiesSliver() {
|
||||
return activities.widgetWhen(
|
||||
onLoading: () => const SliverToBoxAdapter(child: SizedBox.shrink()),
|
||||
onData: (data) {
|
||||
return SliverList(
|
||||
delegate: SliverChildBuilderDelegate((context, index) {
|
||||
if (index == data.length) {
|
||||
// return const SizedBox(height: 5);
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
final activity = data[index];
|
||||
final canDelete = activity.user.id == user?.id || album.ownerId == user?.id;
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 1),
|
||||
child: DismissibleActivity(
|
||||
activity.id,
|
||||
ActivityTile(activity),
|
||||
onDismiss: canDelete
|
||||
? (activityId) async => await activityNotifier.removeActivity(activity.id)
|
||||
: null,
|
||||
),
|
||||
);
|
||||
}, childCount: data.length + 1),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
return BaseBottomSheet(
|
||||
actions: [],
|
||||
slivers: [buildActivitiesSliver()],
|
||||
footer: Column(
|
||||
children: [
|
||||
const Divider(indent: 16, endIndent: 16),
|
||||
DriftActivityTextField(
|
||||
isEnabled: album.isActivityEnabled,
|
||||
isBottomSheet: true,
|
||||
// likeId: likedId,
|
||||
onSubmit: onAddComment,
|
||||
),
|
||||
],
|
||||
),
|
||||
controller: controller,
|
||||
initialChildSize: initialChildSize,
|
||||
minChildSize: 0.1,
|
||||
maxChildSize: 0.88,
|
||||
expand: false,
|
||||
shouldCloseOnMinExtent: false,
|
||||
resizeOnScroll: false,
|
||||
backgroundColor: context.isDarkTheme ? Colors.black : Colors.white,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -8,6 +8,7 @@ class BaseBottomSheet extends ConsumerStatefulWidget {
|
|||
final List<Widget> actions;
|
||||
final DraggableScrollableController? controller;
|
||||
final List<Widget>? slivers;
|
||||
final Widget? footer;
|
||||
final double initialChildSize;
|
||||
final double minChildSize;
|
||||
final double maxChildSize;
|
||||
|
|
@ -20,6 +21,7 @@ class BaseBottomSheet extends ConsumerStatefulWidget {
|
|||
super.key,
|
||||
required this.actions,
|
||||
this.slivers,
|
||||
this.footer,
|
||||
this.controller,
|
||||
this.initialChildSize = 0.35,
|
||||
double? minChildSize,
|
||||
|
|
@ -73,6 +75,9 @@ class _BaseDraggableScrollableSheetState extends ConsumerState<BaseBottomSheet>
|
|||
elevation: 3.0,
|
||||
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.vertical(top: Radius.circular(18))),
|
||||
margin: const EdgeInsets.symmetric(horizontal: 0),
|
||||
child: Column(
|
||||
children: [
|
||||
Expanded(
|
||||
child: CustomScrollView(
|
||||
controller: scrollController,
|
||||
slivers: [
|
||||
|
|
@ -83,7 +88,11 @@ class _BaseDraggableScrollableSheetState extends ConsumerState<BaseBottomSheet>
|
|||
children: [
|
||||
SizedBox(
|
||||
height: 115,
|
||||
child: ListView(shrinkWrap: true, scrollDirection: Axis.horizontal, children: widget.actions),
|
||||
child: ListView(
|
||||
shrinkWrap: true,
|
||||
scrollDirection: Axis.horizontal,
|
||||
children: widget.actions,
|
||||
),
|
||||
),
|
||||
const Divider(indent: 16, endIndent: 16),
|
||||
const SizedBox(height: 16),
|
||||
|
|
@ -93,6 +102,10 @@ class _BaseDraggableScrollableSheetState extends ConsumerState<BaseBottomSheet>
|
|||
if (widget.slivers != null) ...widget.slivers!,
|
||||
],
|
||||
),
|
||||
),
|
||||
if (widget.footer != null) widget.footer!,
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
|
|
|||
|
|
@ -9,8 +9,9 @@ import 'package:immich_mobile/widgets/common/user_circle_avatar.dart';
|
|||
|
||||
class ActivityTile extends HookConsumerWidget {
|
||||
final Activity activity;
|
||||
final bool isBottomSheet;
|
||||
|
||||
const ActivityTile(this.activity, {super.key});
|
||||
const ActivityTile(this.activity, {super.key, this.isBottomSheet = false});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
|
|
@ -18,21 +19,23 @@ class ActivityTile extends HookConsumerWidget {
|
|||
final isLike = activity.type == ActivityType.like;
|
||||
// Asset thumbnail is displayed when we are accessing activities from the album page
|
||||
// currentAssetProvider will not be set until we open the gallery viewer
|
||||
final showAssetThumbnail = asset == null && activity.assetId != null;
|
||||
final showAssetThumbnail = asset == null && activity.assetId != null && !isBottomSheet;
|
||||
|
||||
return ListTile(
|
||||
minVerticalPadding: 15,
|
||||
leading: isLike
|
||||
? Container(
|
||||
width: 44,
|
||||
width: isBottomSheet ? 30 : 44,
|
||||
alignment: Alignment.center,
|
||||
child: Icon(Icons.favorite_rounded, color: Colors.red[700]),
|
||||
)
|
||||
: isBottomSheet
|
||||
? UserCircleAvatar(user: activity.user, size: 30, radius: 15)
|
||||
: UserCircleAvatar(user: activity.user),
|
||||
title: _ActivityTitle(
|
||||
userName: activity.user.name,
|
||||
createdAt: activity.createdAt.timeAgo(),
|
||||
leftAlign: isLike || showAssetThumbnail,
|
||||
leftAlign: isBottomSheet ? false : (isLike || showAssetThumbnail),
|
||||
),
|
||||
// No subtitle for like, so center title
|
||||
titleAlignment: !isLike ? ListTileTitleAlignment.top : ListTileTitleAlignment.center,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue