feat(app): 예능 일정 상세 화면 추가
- ScheduleDetail 모델: broadcaster, replayUrl, varietyThumbnailUrl 필드 추가 - 예능 섹션: 썸네일(블러 배경 + contain) + 정보 카드 - 방송사 뱃지(카테고리 색상), 날짜/시간, 제목, 멤버, 다시보기 버튼 - 썸네일 없을 때 카테고리 색상 배경 + Tv 아이콘 표시 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
aae606725f
commit
f73314d5dc
2 changed files with 174 additions and 0 deletions
|
|
@ -58,6 +58,10 @@ class ScheduleDetail {
|
|||
// X 프로필
|
||||
final String? profileDisplayName;
|
||||
final String? profileAvatarUrl;
|
||||
// 예능 관련
|
||||
final String? broadcaster;
|
||||
final String? replayUrl;
|
||||
final String? varietyThumbnailUrl;
|
||||
|
||||
ScheduleDetail({
|
||||
required this.id,
|
||||
|
|
@ -80,6 +84,9 @@ class ScheduleDetail {
|
|||
this.postUrl,
|
||||
this.profileDisplayName,
|
||||
this.profileAvatarUrl,
|
||||
this.broadcaster,
|
||||
this.replayUrl,
|
||||
this.varietyThumbnailUrl,
|
||||
});
|
||||
|
||||
factory ScheduleDetail.fromJson(Map<String, dynamic> json) {
|
||||
|
|
@ -110,6 +117,9 @@ class ScheduleDetail {
|
|||
postUrl: json['postUrl'] as String?,
|
||||
profileDisplayName: (json['profile'] as Map<String, dynamic>?)?['displayName'] as String?,
|
||||
profileAvatarUrl: (json['profile'] as Map<String, dynamic>?)?['avatarUrl'] as String?,
|
||||
broadcaster: json['broadcaster'] as String?,
|
||||
replayUrl: json['replayUrl'] as String?,
|
||||
varietyThumbnailUrl: json['thumbnailUrl'] as String?,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
/// 일정 상세 화면
|
||||
library;
|
||||
|
||||
import 'dart:ui';
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
|
@ -21,6 +22,7 @@ class CategoryId {
|
|||
static const int fansign = 5;
|
||||
static const int concert = 6;
|
||||
static const int ticket = 7;
|
||||
static const int variety = 10;
|
||||
}
|
||||
|
||||
/// 일정 상세 Provider
|
||||
|
|
@ -190,6 +192,8 @@ class _ScheduleDetailViewState extends ConsumerState<ScheduleDetailView> {
|
|||
return _buildYoutubeSection(schedule);
|
||||
case CategoryId.x:
|
||||
return _buildXSection(schedule);
|
||||
case CategoryId.variety:
|
||||
return _buildVarietySection(schedule);
|
||||
default:
|
||||
return _buildDefaultSection(schedule);
|
||||
}
|
||||
|
|
@ -599,6 +603,166 @@ class _ScheduleDetailViewState extends ConsumerState<ScheduleDetailView> {
|
|||
);
|
||||
}
|
||||
|
||||
/// 예능 섹션
|
||||
Widget _buildVarietySection(ScheduleDetail schedule) {
|
||||
final members = schedule.members;
|
||||
final isFullGroup = members.length >= 5;
|
||||
final hasThumbnail = schedule.varietyThumbnailUrl != null;
|
||||
final categoryColor = parseColor(schedule.categoryColor);
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
// 썸네일
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
child: SizedBox(
|
||||
width: double.infinity,
|
||||
height: 200,
|
||||
child: hasThumbnail
|
||||
? Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
// 블러 배경
|
||||
CachedNetworkImage(
|
||||
imageUrl: schedule.varietyThumbnailUrl!,
|
||||
fit: BoxFit.cover,
|
||||
color: Colors.black.withValues(alpha: 0.3),
|
||||
colorBlendMode: BlendMode.darken,
|
||||
),
|
||||
ClipRect(
|
||||
child: BackdropFilter(
|
||||
filter: ImageFilter.blur(sigmaX: 20, sigmaY: 20),
|
||||
child: Container(color: Colors.transparent),
|
||||
),
|
||||
),
|
||||
// 메인 이미지
|
||||
CachedNetworkImage(
|
||||
imageUrl: schedule.varietyThumbnailUrl!,
|
||||
fit: BoxFit.contain,
|
||||
placeholder: (_, _) => Container(color: Colors.grey[200]),
|
||||
),
|
||||
],
|
||||
)
|
||||
: Container(
|
||||
color: categoryColor.withValues(alpha: 0.1),
|
||||
child: Center(
|
||||
child: Icon(LucideIcons.tv, size: 40, color: categoryColor),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
// 정보 카드
|
||||
Container(
|
||||
width: double.infinity,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withValues(alpha: 0.04),
|
||||
blurRadius: 12,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// 방송사 + 날짜
|
||||
Row(
|
||||
children: [
|
||||
if (schedule.broadcaster != null)
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
|
||||
margin: const EdgeInsets.only(right: 8),
|
||||
decoration: BoxDecoration(
|
||||
color: categoryColor.withValues(alpha: 0.1),
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(LucideIcons.tv, size: 11, color: categoryColor),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
schedule.broadcaster!,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: categoryColor,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Text(
|
||||
_formatFullDate(schedule.date),
|
||||
style: TextStyle(fontSize: 12, color: Colors.grey[400]),
|
||||
),
|
||||
if (schedule.formattedTime != null) ...[
|
||||
Text(
|
||||
' · ${schedule.formattedTime}',
|
||||
style: TextStyle(fontSize: 12, color: Colors.grey[400]),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
// 제목
|
||||
Text(
|
||||
decodeHtmlEntities(schedule.title),
|
||||
style: const TextStyle(
|
||||
fontSize: 17,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppColors.textPrimary,
|
||||
height: 1.4,
|
||||
),
|
||||
),
|
||||
// 멤버
|
||||
if (members.isNotEmpty) ...[
|
||||
const SizedBox(height: 12),
|
||||
Wrap(
|
||||
spacing: 6,
|
||||
runSpacing: 6,
|
||||
children: isFullGroup
|
||||
? [const MemberChip(name: '프로미스나인')]
|
||||
: members.map((m) => MemberChip(name: m.name)).toList(),
|
||||
),
|
||||
],
|
||||
// 다시보기 버튼
|
||||
if (schedule.replayUrl != null) ...[
|
||||
const SizedBox(height: 16),
|
||||
Container(
|
||||
padding: const EdgeInsets.only(top: 16),
|
||||
decoration: const BoxDecoration(
|
||||
border: Border(top: BorderSide(color: AppColors.divider)),
|
||||
),
|
||||
child: SizedBox(
|
||||
width: double.infinity,
|
||||
child: ElevatedButton.icon(
|
||||
onPressed: () => _launchUrl(schedule.replayUrl!),
|
||||
icon: const Icon(LucideIcons.externalLink, size: 16),
|
||||
label: const Text('다시보기', style: TextStyle(fontSize: 14, fontWeight: FontWeight.w500)),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.grey[900],
|
||||
foregroundColor: Colors.white,
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(24)),
|
||||
elevation: 0,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
/// 기본 섹션
|
||||
Widget _buildDefaultSection(ScheduleDetail schedule) {
|
||||
return _buildInfoCard(schedule);
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue