This commit is contained in:
2025-11-24 17:53:00 +09:00
parent e992a5ca5e
commit dde81cab65
34 changed files with 3718 additions and 469 deletions
@@ -0,0 +1,229 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:image_picker/image_picker.dart';
import '../manager/global_chat_manager.dart';
import '../manager/media_manager.dart'; // 미디어 매니저
import '../database/ephemeral_database.dart'; // DB 모델
class GameChatOverlay extends StatefulWidget {
const GameChatOverlay({super.key});
@override
State<GameChatOverlay> createState() => _GameChatOverlayState();
}
class _GameChatOverlayState extends State<GameChatOverlay> {
final TextEditingController _textController = TextEditingController();
final ScrollController _scrollController = ScrollController();
bool _isExpanded = false;
@override
Widget build(BuildContext context) {
return Align(
alignment: Alignment.bottomCenter,
child: AnimatedContainer(
duration: const Duration(milliseconds: 300),
// 높이 조정: 미디어 갤러리가 보일 공간 확보 (펼쳤을 때)
height: _isExpanded ? 500 : 60,
margin: const EdgeInsets.all(10),
decoration: BoxDecoration(
color: Colors.black.withOpacity(0.85),
borderRadius: BorderRadius.circular(20),
boxShadow: [BoxShadow(color: Colors.black26, blurRadius: 10, spreadRadius: 2)],
),
child: Column(
children: [
// 1. 상단 핸들 (접기/펼치기)
GestureDetector(
onTap: () => setState(() => _isExpanded = !_isExpanded),
behavior: HitTestBehavior.translucent,
child: Container(
width: double.infinity,
height: 30,
alignment: Alignment.center,
child: Icon(
_isExpanded ? Icons.keyboard_arrow_down : Icons.keyboard_arrow_up,
color: Colors.white,
),
),
),
// 펼쳤을 때만 보이는 영역
if (_isExpanded) ...[
// 2. 미디어 갤러리 (가로 스크롤)
// DB의 변경사항을 실시간으로 감지(Stream)하여 보여줌
SizedBox(
height: 100,
child: StreamBuilder<List<MediaItem>>(
stream: MediaManager().galleryStream,
builder: (context, snapshot) {
final mediaList = snapshot.data ?? [];
if (mediaList.isEmpty) {
return const Center(child: Text("공유된 미디어가 없습니다.", style: TextStyle(color: Colors.white54, fontSize: 12)));
}
return ListView.builder(
scrollDirection: Axis.horizontal,
padding: const EdgeInsets.symmetric(horizontal: 10),
itemCount: mediaList.length,
itemBuilder: (context, index) {
final item = mediaList[index];
return Padding(
padding: const EdgeInsets.only(right: 8.0),
child: GestureDetector(
onTap: () => _showFullImage(context, item),
child: ClipRRect(
borderRadius: BorderRadius.circular(8),
child: Image.file(
File(item.filePath),
width: 100,
height: 100,
fit: BoxFit.cover,
errorBuilder: (_,__,___) => Container(
width: 100, height: 100, color: Colors.grey,
child: const Icon(Icons.broken_image),
),
),
),
),
);
},
);
},
),
),
const Divider(color: Colors.white24),
// 3. 채팅 리스트
Expanded(
child: StreamBuilder<List<ChatMessage>>(
stream: GlobalChatManager().messageStream,
builder: (context, snapshot) {
final messages = snapshot.data ?? [];
WidgetsBinding.instance.addPostFrameCallback((_) {
if (_scrollController.hasClients) {
_scrollController.jumpTo(_scrollController.position.maxScrollExtent);
}
});
return ListView.builder(
controller: _scrollController,
padding: const EdgeInsets.symmetric(horizontal: 10),
itemCount: messages.length,
itemBuilder: (context, index) {
final msg = messages[index];
return Padding(
padding: const EdgeInsets.symmetric(vertical: 2),
child: Text(
"${msg.senderName}: ${msg.text}",
style: TextStyle(
color: msg.isMe ? Colors.yellow : Colors.white,
fontSize: 14,
),
),
);
},
);
},
),
),
// 4. 입력창 (+ 미디어 버튼)
Padding(
padding: const EdgeInsets.all(8.0),
child: Row(
children: [
// [추가] 이미지 전송 버튼
IconButton(
icon: const Icon(Icons.add_photo_alternate, color: Colors.blueAccent),
onPressed: _pickAndSendImage,
),
Expanded(
child: TextField(
controller: _textController,
style: const TextStyle(color: Colors.white),
decoration: const InputDecoration(
hintText: "채팅 입력...",
hintStyle: TextStyle(color: Colors.white54),
border: InputBorder.none,
isDense: true,
),
onSubmitted: _sendMessage,
),
),
IconButton(
icon: const Icon(Icons.send, color: Colors.blue),
onPressed: () => _sendMessage(_textController.text),
),
],
),
),
],
],
),
),
);
}
void _sendMessage(String text) {
if (text.trim().isEmpty) return;
GlobalChatManager().sendMessage(text);
_textController.clear();
}
// [이미지 선택 및 전송 로직]
Future<void> _pickAndSendImage() async {
final picker = ImagePicker();
// 갤러리에서 이미지 선택 (압축 옵션 추가 권장)
final XFile? image = await picker.pickImage(
source: ImageSource.gallery,
imageQuality: 50, // 전송 속도를 위해 품질 낮춤
maxWidth: 800,
);
if (image != null) {
// MediaManager를 통해 전송
await MediaManager().sendMedia(
filePath: image.path,
type: 'IMAGE',
);
}
}
// [이미지 크게 보기 팝업]
void _showFullImage(BuildContext context, MediaItem item) {
showDialog(
context: context,
builder: (ctx) => Dialog(
backgroundColor: Colors.transparent,
insetPadding: EdgeInsets.zero,
child: Stack(
alignment: Alignment.center,
children: [
InteractiveViewer(
child: Image.file(File(item.filePath)),
),
Positioned(
top: 40,
right: 20,
child: IconButton(
icon: const Icon(Icons.close, color: Colors.white, size: 30),
onPressed: () => Navigator.pop(ctx),
),
),
Positioned(
bottom: 20,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5),
color: Colors.black54,
child: Text("보낸 사람: ${item.senderName}", style: const TextStyle(color: Colors.white)),
),
)
],
),
),
);
}
}