Files
iykyk_msn/mobile/lib/features/profile/user_profile_sheet.dart
2026-04-07 16:17:03 +09:00

179 lines
5.6 KiB
Dart

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import '../../core/msn_api.dart';
import '../../core/session_controller.dart';
import '../../models/profile_model.dart';
import '../../theme/toss_theme.dart';
/// Kakao-style profile sheet: large avatar, name, status, optional 1:1 chat.
Future<void> showUserProfileSheet(
BuildContext parentContext,
WidgetRef ref, {
required String contextId,
required String userId,
}) async {
await showModalBottomSheet<void>(
context: parentContext,
isScrollControlled: true,
showDragHandle: true,
backgroundColor: TossColors.surface,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
),
builder: (sheetContext) => _UserProfileSheetBody(
parentContext: parentContext,
contextId: contextId,
userId: userId,
),
);
}
class _UserProfileSheetBody extends ConsumerStatefulWidget {
const _UserProfileSheetBody({
required this.parentContext,
required this.contextId,
required this.userId,
});
final BuildContext parentContext;
final String contextId;
final String userId;
@override
ConsumerState<_UserProfileSheetBody> createState() => _UserProfileSheetBodyState();
}
class _UserProfileSheetBodyState extends ConsumerState<_UserProfileSheetBody> {
late Future<ProfileModel> _future;
@override
void initState() {
super.initState();
final myId = ref.read(sessionProvider).value?.userId;
final isSelf = myId != null && widget.userId == myId;
_future = isSelf
? ref.read(msnApiProvider).getMyProfile(widget.contextId)
: ref.read(msnApiProvider).getUserProfile(widget.contextId, widget.userId);
}
Future<void> _openDirect() async {
final roomId =
await ref.read(msnApiProvider).openDirectRoom(widget.contextId, widget.userId);
if (!mounted) return;
Navigator.of(context).pop();
if (widget.parentContext.mounted) {
await widget.parentContext.push('/chat?roomId=$roomId&contextId=${widget.contextId}');
}
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final myId = ref.watch(sessionProvider).value?.userId;
final isSelf = myId != null && widget.userId == myId;
return SafeArea(
child: Padding(
padding: EdgeInsets.only(
left: 24,
right: 24,
top: 8,
bottom: 24 + MediaQuery.paddingOf(context).bottom,
),
child: FutureBuilder<ProfileModel>(
future: _future,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const SizedBox(
height: 220,
child: Center(child: CircularProgressIndicator(color: TossColors.blue)),
);
}
if (snapshot.hasError) {
return SizedBox(
height: 120,
child: Center(child: Text('${snapshot.error}')),
);
}
final p = snapshot.data!;
return SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Center(
child: _LargeAvatar(
displayName: p.displayName,
avatarUrl: p.avatarUrl,
),
),
const SizedBox(height: 20),
Text(
p.displayName,
style: theme.textTheme.headlineSmall?.copyWith(fontWeight: FontWeight.w700),
textAlign: TextAlign.center,
),
if (p.statusMessage != null && p.statusMessage!.isNotEmpty) ...[
const SizedBox(height: 12),
Text(
p.statusMessage!,
style: theme.textTheme.bodyLarge,
textAlign: TextAlign.center,
),
],
const SizedBox(height: 16),
Text(
'이 맥락에서만 보이는 프로필이에요.',
style: theme.textTheme.bodySmall,
textAlign: TextAlign.center,
),
if (!isSelf) ...[
const SizedBox(height: 24),
FilledButton.icon(
onPressed: _openDirect,
icon: const Icon(Icons.chat_bubble_outline),
label: const Text('1:1 채팅'),
),
],
],
),
);
},
),
),
);
}
}
class _LargeAvatar extends StatelessWidget {
const _LargeAvatar({
required this.displayName,
this.avatarUrl,
});
final String displayName;
final String? avatarUrl;
@override
Widget build(BuildContext context) {
final hasUrl = avatarUrl != null && avatarUrl!.isNotEmpty;
return CircleAvatar(
radius: 56,
backgroundColor: TossColors.blue.withValues(alpha: 0.12),
backgroundImage: hasUrl ? NetworkImage(avatarUrl!) : null,
child: hasUrl
? null
: Text(
displayName.isNotEmpty ? displayName[0].toUpperCase() : '?',
style: const TextStyle(
fontSize: 40,
fontWeight: FontWeight.w600,
color: TossColors.blue,
),
),
);
}
}