fromis_9/app/lib/views/schedule/schedule_detail_view.dart
caadiq f73314d5dc feat(app): 예능 일정 상세 화면 추가
- ScheduleDetail 모델: broadcaster, replayUrl, varietyThumbnailUrl 필드 추가
- 예능 섹션: 썸네일(블러 배경 + contain) + 정보 카드
- 방송사 뱃지(카테고리 색상), 날짜/시간, 제목, 멤버, 다시보기 버튼
- 썸네일 없을 때 카테고리 색상 배경 + Tv 아이콘 표시

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 17:06:06 +09:00

967 lines
33 KiB
Dart

/// 일정 상세 화면
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';
import 'package:lucide_icons/lucide_icons.dart';
import 'package:omni_video_player/omni_video_player.dart';
import 'package:url_launcher/url_launcher.dart';
import '../../core/constants.dart';
import '../../models/schedule.dart';
import '../../services/schedules_service.dart';
import 'widgets/schedule_card.dart';
import 'widgets/member_chip.dart';
/// 카테고리 ID 상수
class CategoryId {
static const int youtube = 2;
static const int x = 3;
static const int album = 4;
static const int fansign = 5;
static const int concert = 6;
static const int ticket = 7;
static const int variety = 10;
}
/// 일정 상세 Provider
final scheduleDetailProvider =
FutureProvider.family<ScheduleDetail, int>((ref, id) async {
return await getSchedule(id);
});
class ScheduleDetailView extends ConsumerStatefulWidget {
final int scheduleId;
const ScheduleDetailView({super.key, required this.scheduleId});
@override
ConsumerState<ScheduleDetailView> createState() => _ScheduleDetailViewState();
}
class _ScheduleDetailViewState extends ConsumerState<ScheduleDetailView> {
late int _currentScheduleId;
@override
void initState() {
super.initState();
_currentScheduleId = widget.scheduleId;
}
/// URL 열기 (외부 앱)
Future<void> _launchUrl(String url) async {
final uri = Uri.parse(url);
if (await canLaunchUrl(uri)) {
await launchUrl(uri, mode: LaunchMode.externalApplication);
}
}
/// 날짜 포맷팅 (2026. 1. 15. (수))
String _formatFullDate(String dateStr) {
final date = DateTime.parse(dateStr);
final dayNames = ['', '', '', '', '', '', ''];
return '${date.year}. ${date.month}. ${date.day}. (${dayNames[date.weekday % 7]})';
}
/// X용 날짜/시간 포맷팅 (오후 2:30 · 2026년 1월 15일)
String _formatXDateTime(String dateStr, String? timeStr) {
final date = DateTime.parse(dateStr);
var result = '${date.year}${date.month}${date.day}';
if (timeStr != null && timeStr.length >= 5) {
final parts = timeStr.split(':');
final hours = int.parse(parts[0]);
final minutes = parts[1];
final period = hours < 12 ? '오전' : '오후';
final hour12 = hours == 0 ? 12 : (hours > 12 ? hours - 12 : hours);
result = '$period $hour12:$minutes · $result';
}
return result;
}
@override
Widget build(BuildContext context) {
final scheduleAsync = ref.watch(scheduleDetailProvider(_currentScheduleId));
return Scaffold(
backgroundColor: AppColors.background,
appBar: AppBar(
backgroundColor: Colors.white,
elevation: 0,
scrolledUnderElevation: 0.5,
leading: IconButton(
icon: const Icon(LucideIcons.chevronLeft, size: 24),
onPressed: () => Navigator.of(context).pop(),
),
title: scheduleAsync.whenOrNull(
data: (schedule) => Text(
schedule.categoryName ?? '',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: parseColor(schedule.categoryColor),
),
),
),
centerTitle: true,
),
body: scheduleAsync.when(
loading: () => const Center(
child: CircularProgressIndicator(color: AppColors.primary),
),
error: (error, stack) => _buildErrorView(),
data: (schedule) => _buildContent(schedule),
),
);
}
/// 에러 화면
Widget _buildErrorView() {
return Center(
child: Padding(
padding: const EdgeInsets.all(32),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
width: 80,
height: 80,
decoration: BoxDecoration(
color: AppColors.primary.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(20),
),
child: Icon(
LucideIcons.calendar,
size: 40,
color: AppColors.primary.withValues(alpha: 0.4),
),
),
const SizedBox(height: 24),
const Text(
'일정을 찾을 수 없습니다',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: AppColors.textPrimary,
),
),
const SizedBox(height: 8),
const Text(
'요청하신 일정이 존재하지 않거나\n삭제되었을 수 있습니다.',
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 14,
color: AppColors.textSecondary,
),
),
const SizedBox(height: 32),
OutlinedButton.icon(
onPressed: () => Navigator.of(context).pop(),
icon: const Icon(LucideIcons.arrowLeft, size: 18),
label: const Text('돌아가기'),
style: OutlinedButton.styleFrom(
foregroundColor: AppColors.primary,
side: const BorderSide(color: AppColors.primary),
padding:
const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
),
),
],
),
),
);
}
/// 메인 컨텐츠
Widget _buildContent(ScheduleDetail schedule) {
final bottomPadding = MediaQuery.of(context).padding.bottom;
return SingleChildScrollView(
padding: EdgeInsets.fromLTRB(16, 16, 16, 16 + bottomPadding),
child: _buildCategorySection(schedule),
);
}
/// 카테고리별 섹션
Widget _buildCategorySection(ScheduleDetail schedule) {
switch (schedule.categoryId) {
case CategoryId.youtube:
return _buildYoutubeSection(schedule);
case CategoryId.x:
return _buildXSection(schedule);
case CategoryId.variety:
return _buildVarietySection(schedule);
default:
return _buildDefaultSection(schedule);
}
}
/// 유튜브 섹션
Widget _buildYoutubeSection(ScheduleDetail schedule) {
final videoId = schedule.videoId;
final isScheduled = videoId == null;
final members = schedule.members;
final isFullGroup = members.length >= 5;
return Column(
children: [
// 영상 썸네일 또는 예정 플레이스홀더
if (isScheduled)
_buildScheduledPlaceholder(schedule.bannerUrl)
else
ClipRRect(
borderRadius: BorderRadius.circular(12),
child: AspectRatio(
aspectRatio: 16 / 9,
child: OmniVideoPlayer(
configuration: VideoPlayerConfiguration(
videoSourceConfiguration: VideoSourceConfiguration.youtube(
videoUrl: Uri.parse('https://www.youtube.com/watch?v=$videoId'),
preferredQualities: [OmniVideoQuality.high720],
),
),
callbacks: const VideoPlayerCallbacks(),
),
),
),
const SizedBox(height: 16),
// 영상 정보 카드
Container(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [Colors.grey[100]!, Colors.grey[200]!.withValues(alpha: 0.8)],
),
borderRadius: BorderRadius.circular(12),
),
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 제목 + 예정 뱃지
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: Text(
decodeHtmlEntities(schedule.title),
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: AppColors.textPrimary,
height: 1.4,
),
),
),
if (isScheduled) ...[
const SizedBox(width: 8),
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
decoration: BoxDecoration(
color: const Color(0xFFFEF3C7), // amber-100
borderRadius: BorderRadius.circular(12),
),
child: const Text(
'예정',
style: TextStyle(
fontSize: 11,
fontWeight: FontWeight.w600,
color: Color(0xFFB45309), // amber-700
),
),
),
],
],
),
const SizedBox(height: 12),
// 메타 정보
Wrap(
spacing: 12,
runSpacing: 6,
children: [
_buildMetaItem(LucideIcons.calendar, _formatXDateTime(schedule.date, schedule.time)),
if (schedule.channelName != null)
_buildMetaItem(LucideIcons.link2, schedule.channelName!),
],
),
// 멤버
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(),
),
],
// YouTube에서 보기 버튼
if (!isScheduled) ...[
const SizedBox(height: 16),
Container(
padding: const EdgeInsets.only(top: 16),
decoration: BoxDecoration(
border: Border(
top: BorderSide(color: Colors.grey[300]!.withValues(alpha: 0.5)),
),
),
child: SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: () => _launchUrl(schedule.videoUrl!),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.red[500],
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(vertical: 12),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
elevation: 0,
),
child: const Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(LucideIcons.youtube, size: 20),
SizedBox(width: 8),
Text(
'YouTube에서 보기',
style: TextStyle(fontWeight: FontWeight.w500),
),
],
),
),
),
),
],
],
),
),
],
);
}
/// 예정 일정 플레이스홀더
Widget _buildScheduledPlaceholder(String? bannerUrl) {
return ClipRRect(
borderRadius: BorderRadius.circular(12),
child: AspectRatio(
aspectRatio: 16 / 9,
child: Stack(
fit: StackFit.expand,
children: [
// 배경
if (bannerUrl != null)
Stack(
fit: StackFit.expand,
children: [
CachedNetworkImage(
imageUrl: bannerUrl,
fit: BoxFit.cover,
placeholder: (_, _) => Container(color: Colors.grey[900]),
errorWidget: (_, _, _) => Container(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [Colors.grey[800]!, Colors.grey[900]!],
),
),
),
),
// 그라데이션 오버레이
Container(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
Colors.transparent,
Colors.black.withValues(alpha: 0.7),
],
),
),
),
],
)
else
Container(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [Colors.grey[800]!, Colors.grey[900]!],
),
),
),
// 하단 텍스트
Positioned(
left: 16,
bottom: 16,
child: Row(
children: [
Icon(
LucideIcons.clock,
size: 16,
color: Colors.amber[400],
),
const SizedBox(width: 8),
Text(
'업로드 예정',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
color: Colors.white.withValues(alpha: 0.9),
),
),
],
),
),
],
),
),
);
}
/// X 섹션
Widget _buildXSection(ScheduleDetail schedule) {
final username = schedule.username ?? 'Unknown';
final displayName = schedule.profileDisplayName ?? username;
final avatarUrl = schedule.profileAvatarUrl;
return Container(
clipBehavior: Clip.antiAlias,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
border: Border.all(color: AppColors.border),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 헤더
Padding(
padding: const EdgeInsets.fromLTRB(16, 16, 16, 0),
child: Row(
children: [
// 프로필 이미지
avatarUrl != null
? ClipOval(
child: CachedNetworkImage(
imageUrl: avatarUrl,
width: 40,
height: 40,
fit: BoxFit.cover,
placeholder: (_, _) => Container(
width: 40, height: 40,
decoration: BoxDecoration(
color: Colors.grey[300],
shape: BoxShape.circle,
),
),
errorWidget: (_, _, _) => _buildAvatarFallback(displayName),
),
)
: _buildAvatarFallback(displayName),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Flexible(
child: Text(
displayName,
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 14,
),
overflow: TextOverflow.ellipsis,
),
),
const SizedBox(width: 4),
_buildVerifiedBadge(),
],
),
Text(
'@$username',
style: TextStyle(
fontSize: 13,
color: Colors.grey[500],
),
),
],
),
),
],
),
),
// 본문
Padding(
padding: const EdgeInsets.all(16),
child: Text(
decodeHtmlEntities(schedule.content ?? schedule.title),
style: const TextStyle(
fontSize: 15,
height: 1.5,
color: AppColors.textPrimary,
),
),
),
// 이미지
if (schedule.imageUrls.isNotEmpty) ...[
Padding(
padding: const EdgeInsets.fromLTRB(16, 0, 16, 12),
child: ClipRRect(
borderRadius: BorderRadius.circular(12),
child: schedule.imageUrls.length == 1
? CachedNetworkImage(
imageUrl: schedule.imageUrls[0],
fit: BoxFit.cover,
placeholder: (_, _) => Container(
height: 200,
color: Colors.grey[200],
),
errorWidget: (_, _, _) => const SizedBox.shrink(),
)
: GridView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
crossAxisSpacing: 4,
mainAxisSpacing: 4,
),
itemCount: schedule.imageUrls.length,
itemBuilder: (context, index) {
return CachedNetworkImage(
imageUrl: schedule.imageUrls[index],
fit: BoxFit.cover,
placeholder: (_, _) => Container(color: Colors.grey[200]),
errorWidget: (_, _, _) => const SizedBox.shrink(),
);
},
),
),
),
],
// 날짜/시간
Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
decoration: BoxDecoration(
border: Border(top: BorderSide(color: Colors.grey[100]!)),
),
child: Text(
_formatXDateTime(schedule.date, schedule.time),
style: TextStyle(
fontSize: 14,
color: Colors.grey[500],
),
),
),
// X에서 보기 버튼
if (schedule.postUrl != null)
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.grey[50],
borderRadius: const BorderRadius.vertical(bottom: Radius.circular(12)),
border: Border(top: BorderSide(color: Colors.grey[100]!)),
),
child: SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: () => _launchUrl(schedule.postUrl!),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.grey[900],
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(vertical: 12),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(24),
),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
_buildXLogo(14),
const SizedBox(width: 8),
const Text(
'X에서 보기',
style: TextStyle(fontWeight: FontWeight.w500),
),
],
),
),
),
),
],
),
);
}
/// 예능 섹션
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);
}
/// 정보 카드
Widget _buildInfoCard(ScheduleDetail schedule) {
final isFullGroup = schedule.members.length >= 5;
// 채널명 또는 소스 정보
final sourceName = schedule.channelName ?? schedule.username;
return Container(
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: [
// 제목
Text(
decodeHtmlEntities(schedule.title),
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: AppColors.textPrimary,
height: 1.4,
),
),
const SizedBox(height: 12),
// 메타 정보
Wrap(
spacing: 12,
runSpacing: 8,
children: [
_buildMetaItem(LucideIcons.calendar, _formatFullDate(schedule.date)),
if (schedule.formattedTime != null)
_buildMetaItem(LucideIcons.clock, schedule.formattedTime!),
if (sourceName != null)
_buildMetaItem(LucideIcons.link2, sourceName),
],
),
// 멤버
if (schedule.members.isNotEmpty) ...[
const SizedBox(height: 16),
Container(
width: double.infinity,
height: 1,
color: AppColors.divider,
),
const SizedBox(height: 16),
Wrap(
spacing: 6,
runSpacing: 6,
children: isFullGroup
? [const MemberChip(name: '프로미스나인')]
: schedule.members
.map((m) => MemberChip(name: m.name))
.toList(),
),
],
],
),
);
}
/// 메타 아이템
Widget _buildMetaItem(IconData icon, String text) {
return Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(icon, size: 14, color: AppColors.textSecondary),
const SizedBox(width: 4),
Text(
text,
style: const TextStyle(
fontSize: 12,
color: AppColors.textSecondary,
),
),
],
);
}
/// 아바타 폴백 (이니셜)
Widget _buildAvatarFallback(String name) {
return Container(
width: 40,
height: 40,
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [Colors.grey[700]!, Colors.grey[900]!],
),
shape: BoxShape.circle,
),
child: Center(
child: Text(
name[0].toUpperCase(),
style: const TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: 16,
),
),
),
);
}
/// 인증 배지
Widget _buildVerifiedBadge() {
return SizedBox(
width: 16,
height: 16,
child: CustomPaint(
painter: _VerifiedBadgePainter(),
),
);
}
/// X 로고
Widget _buildXLogo(double size) {
return SizedBox(
width: size,
height: size,
child: CustomPaint(
painter: _XLogoPainter(),
),
);
}
}
/// 인증 배지 페인터
class _VerifiedBadgePainter extends CustomPainter {
@override
void paint(Canvas canvas, Size size) {
final paint = Paint()
..color = const Color(0xFF3B82F6)
..style = PaintingStyle.fill;
final center = Offset(size.width / 2, size.height / 2);
final radius = size.width / 2;
canvas.drawCircle(center, radius, paint);
final checkPaint = Paint()
..color = Colors.white
..style = PaintingStyle.stroke
..strokeWidth = 1.5
..strokeCap = StrokeCap.round
..strokeJoin = StrokeJoin.round;
final checkPath = Path()
..moveTo(size.width * 0.28, size.height * 0.52)
..lineTo(size.width * 0.45, size.height * 0.68)
..lineTo(size.width * 0.72, size.height * 0.35);
canvas.drawPath(checkPath, checkPaint);
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
}
/// X 로고 페인터
class _XLogoPainter extends CustomPainter {
@override
void paint(Canvas canvas, Size size) {
final paint = Paint()
..color = Colors.white
..style = PaintingStyle.fill;
final path = Path();
path.moveTo(size.width * 0.76, size.height * 0.09);
path.lineTo(size.width * 0.9, size.height * 0.09);
path.lineTo(size.width * 0.6, size.height * 0.44);
path.lineTo(size.width * 0.95, size.height * 0.91);
path.lineTo(size.width * 0.67, size.height * 0.91);
path.lineTo(size.width * 0.46, size.height * 0.63);
path.lineTo(size.width * 0.21, size.height * 0.91);
path.lineTo(size.width * 0.07, size.height * 0.91);
path.lineTo(size.width * 0.4, size.height * 0.53);
path.lineTo(size.width * 0.05, size.height * 0.09);
path.lineTo(size.width * 0.34, size.height * 0.09);
path.lineTo(size.width * 0.52, size.height * 0.35);
path.close();
canvas.drawPath(path, paint);
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
}