From 726cdeac5cc18b26213516c522c6e34404e6c9df Mon Sep 17 00:00:00 2001 From: idubnori Date: Thu, 23 Oct 2025 02:07:16 +0900 Subject: [PATCH] init of v2... --- .../pages/drift_activities.page.dart | 326 +++++++++++++++++- 1 file changed, 307 insertions(+), 19 deletions(-) diff --git a/mobile/lib/presentation/pages/drift_activities.page.dart b/mobile/lib/presentation/pages/drift_activities.page.dart index d8f8799f7d..f32c7407da 100644 --- a/mobile/lib/presentation/pages/drift_activities.page.dart +++ b/mobile/lib/presentation/pages/drift_activities.page.dart @@ -15,6 +15,10 @@ import 'package:immich_mobile/providers/infrastructure/current_album.provider.da 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 'package:immich_mobile/widgets/common/user_circle_avatar.dart'; +import 'package:immich_mobile/widgets/common/confirm_dialog.dart'; +import 'package:immich_mobile/providers/image/immich_remote_thumbnail_provider.dart'; +import 'package:immich_mobile/providers/activity_service.provider.dart'; @RoutePage() class DriftActivitiesPage extends HookConsumerWidget { @@ -55,29 +59,147 @@ class DriftActivitiesPage extends HookConsumerWidget { (a) => a.type == ActivityType.like && a.user.id == user?.id && a.assetId == asset?.id, ); + // チャット風レイアウト: 日付セパレータを削除し、ActivityTile をバブルに置換(コメント+画像は引用風) + final List activityWidgets = []; + + for (final activity in data) { + final isOwn = activity.user.id == user?.id; + final canDelete = isOwn || album.ownerId == user?.id; + + // Like: 自分の like はコメントと同じ青背景バブルで日時表示、他人の like は他人の comment と同形式にする + if (activity.type == ActivityType.like) { + final bubble = Padding( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6), + child: _CommentBubble( + activity: activity, + isOwn: isOwn, + showAssetImage: true, + showHeart: true, + onDelete: canDelete ? () async => await activityNotifier.removeActivity(activity.id) : null, + ), + ); + + if (canDelete) { + activityWidgets.add( + Dismissible( + key: Key(activity.id), + dismissThresholds: const {DismissDirection.horizontal: 0.7}, + direction: DismissDirection.horizontal, + confirmDismiss: (direction) => showDialog( + context: context, + builder: (context) => ConfirmDialog( + onOk: () {}, + title: "shared_album_activity_remove_title", + content: "shared_album_activity_remove_content", + ok: "delete", + ), + ), + onDismissed: (_) async => await activityNotifier.removeActivity(activity.id), + background: Container( + alignment: AlignmentDirectional.centerStart, + color: Colors.red[400], + child: const Padding( + padding: EdgeInsets.all(15), + child: Icon(Icons.delete_sweep_rounded, color: Colors.black), + ), + ), + secondaryBackground: Container( + alignment: AlignmentDirectional.centerEnd, + color: Colors.red[400], + child: const Padding( + padding: EdgeInsets.all(15), + child: Icon(Icons.delete_sweep_rounded, color: Colors.black), + ), + ), + child: bubble, + ), + ); + } else { + activityWidgets.add(bubble); + } + + continue; + } + + // コメント(テキストまたは画像付き)はバブルで表示。ActivityTile は使わない + if (activity.comment != null && activity.comment!.isNotEmpty) { + // 画像がある場合は引用風: 画像のみを右寄せに表示する場合は isOwn で左右揃え + final bubble = Padding( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6), + child: _CommentBubble( + activity: activity, + isOwn: isOwn, + showAssetImage: activity.assetId != null, + onDelete: canDelete ? () async => await activityNotifier.removeActivity(activity.id) : null, + ), + ); + + if (canDelete) { + activityWidgets.add( + Dismissible( + key: Key(activity.id), + dismissThresholds: const {DismissDirection.horizontal: 0.7}, + direction: DismissDirection.horizontal, + confirmDismiss: (direction) => showDialog( + context: context, + builder: (context) => ConfirmDialog( + onOk: () {}, + title: "shared_album_activity_remove_title", + content: "shared_album_activity_remove_content", + ok: "delete", + ), + ), + onDismissed: (_) async => await activityNotifier.removeActivity(activity.id), + background: Container( + alignment: AlignmentDirectional.centerStart, + color: Colors.red[400], + child: const Padding( + padding: EdgeInsets.all(15), + child: Icon(Icons.delete_sweep_rounded, color: Colors.black), + ), + ), + secondaryBackground: Container( + alignment: AlignmentDirectional.centerEnd, + color: Colors.red[400], + child: const Padding( + padding: EdgeInsets.all(15), + child: Icon(Icons.delete_sweep_rounded, color: Colors.black), + ), + ), + child: bubble, + ), + ); + } else { + activityWidgets.add(bubble); + } + continue; + } + + // その他(例: plain like without asset or system messages) - フォールバックで既存タイル表示 + activityWidgets.add( + Padding( + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4), + child: DismissibleActivity( + activity.id, + ActivityTile(activity), + onDismiss: canDelete + ? (activityId) async => await activityNotifier.removeActivity(activity.id) + : null, + ), + ), + ); + } + + // 下部余白確保 + activityWidgets.add(const SizedBox(height: 80)); + return SafeArea( child: Stack( children: [ - ListView.builder( + ListView( controller: listViewScrollController, - itemCount: data.length + 1, - itemBuilder: (context, index) { - if (index == data.length) { - return const SizedBox(height: 80); - } - final activity = data[index]; - final canDelete = activity.user.id == user?.id || album.ownerId == user?.id; - return Padding( - padding: const EdgeInsets.all(5), - child: DismissibleActivity( - activity.id, - ActivityTile(activity), - onDismiss: canDelete - ? (activityId) async => await activityNotifier.removeActivity(activity.id) - : null, - ), - ); - }, + padding: const EdgeInsets.only(top: 8, bottom: 0), + children: activityWidgets, ), Align( alignment: Alignment.bottomCenter, @@ -102,3 +224,169 @@ class DriftActivitiesPage extends HookConsumerWidget { ); } } + +// 日付フォーマット関数は現時点では不要 + +// _QuotedImage は使用しなくなったため削除。 + +class _CommentBubble extends ConsumerWidget { + final Activity activity; + final bool isOwn; + final bool showAssetImage; + final bool showHeart; + final VoidCallback? onDelete; + + const _CommentBubble({ + required this.activity, + required this.isOwn, + required this.showAssetImage, + this.showHeart = false, + this.onDelete, + }); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final bg = isOwn ? context.colorScheme.primaryContainer : context.colorScheme.surfaceContainer; + return Align( + alignment: isOwn ? Alignment.centerRight : Alignment.centerLeft, + child: ConstrainedBox( + constraints: BoxConstraints(maxWidth: MediaQuery.of(context).size.width * 0.86), + child: Container( + margin: const EdgeInsets.symmetric(vertical: 6, horizontal: 10), + padding: const EdgeInsets.all(12), + // decoration: BoxDecoration(color: bg, borderRadius: BorderRadius.circular(12)), + child: Column( + crossAxisAlignment: isOwn ? CrossAxisAlignment.end : CrossAxisAlignment.start, + children: [ + // 名前やアイコンは不要(仕様) + if (showAssetImage && activity.assetId != null) + Padding( + padding: const EdgeInsets.only(bottom: 8.0), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + if (!isOwn) ...[ + UserCircleAvatar(user: activity.user, size: 28, radius: 14), + const SizedBox(width: 8), + ], + ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 150, maxHeight: 150), + child: Stack( + children: [ + // Make thumbnail tappable to open asset viewer + GestureDetector( + onTap: () async { + final activityService = ref.read(activityServiceProvider); + if (activity.assetId == null) return; + final route = await activityService.buildAssetViewerRoute(activity.assetId!, ref); + if (route != null) { + await context.pushRoute(route); + } + }, + child: ClipRRect( + borderRadius: BorderRadius.circular(8), + child: Image( + image: ImmichRemoteThumbnailProvider(assetId: activity.assetId!), + fit: BoxFit.contain, + ), + ), + ), + if (showHeart) + Positioned( + right: 6, + bottom: 6, + child: Container( + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.9), + shape: BoxShape.circle, + ), + padding: const EdgeInsets.all(4), + child: Icon(Icons.favorite, color: Colors.red[600], size: 18), + ), + ), + ], + ), + ), + ], + ), + ), + // asset がない like の場合はバブル内にハートを表示 + if (showHeart && (activity.assetId == null || activity.assetId!.isEmpty)) + Padding( + padding: const EdgeInsets.only(bottom: 8.0), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + if (!isOwn) ...[ + UserCircleAvatar(user: activity.user, size: 28, radius: 14), + const SizedBox(width: 8), + ], + // For own like-only, push to right by wrapping with Align when parent is right-aligned. + Container( + padding: const EdgeInsets.all(6), + decoration: BoxDecoration(color: Colors.white.withOpacity(0.9), shape: BoxShape.circle), + child: Icon(Icons.favorite, color: Colors.red[600], size: 18), + ), + ], + ), + ), + // コメント本文: コメントが存在する場合のみ表示(showAssetImage の有無でレイアウトを切替) + if (activity.comment != null && activity.comment!.isNotEmpty) ...[ + if (!showAssetImage) ...[ + // コメントのみの表示: 自分の投稿は右寄せかつ本文幅に最小化、他人の投稿はアイコン表示とともに最小化 + Row( + crossAxisAlignment: isOwn ? CrossAxisAlignment.end : CrossAxisAlignment.start, + children: [ + if (!isOwn) ...[ + UserCircleAvatar(user: activity.user, size: 28, radius: 14), + const SizedBox(width: 8), + ], + // For both own and non-own comment-only: minimize width to content, up to 50% of screen width. + Expanded( + child: Align( + alignment: isOwn ? Alignment.centerRight : Alignment.centerLeft, + child: ConstrainedBox( + constraints: BoxConstraints(maxWidth: MediaQuery.of(context).size.width * 0.5), + child: IntrinsicWidth( + child: Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration(color: bg, borderRadius: BorderRadius.circular(12)), + child: Text( + activity.comment ?? '', + style: context.textTheme.bodyMedium?.copyWith(color: context.colorScheme.onSurface), + softWrap: true, + ), + ), + ), + ), + ), + ), + ], + ), + ] else ...[ + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration(color: bg, borderRadius: BorderRadius.circular(12)), + child: Text( + activity.comment ?? '', + style: context.textTheme.bodyMedium?.copyWith(color: context.colorScheme.onSurface), + ), + ), + ], + const SizedBox(height: 8), + ], + Text( + // ユーザー名・日付と時間を表示 + '${activity.user.name} ・ ${activity.createdAt.year}/${activity.createdAt.month.toString().padLeft(2, '0')}/${activity.createdAt.day.toString().padLeft(2, '0')} ' + '${activity.createdAt.toLocal().toString().substring(11, 16)}', + style: context.textTheme.labelSmall?.copyWith( + color: context.colorScheme.onSurface.withValues(alpha: 0.6), + ), + ), + ], + ), + ), + ), + ); + } +}