feat: Flutter 앱 일정 상세 화면 구현

- ScheduleDetail, ScheduleMember, RelatedDate 모델 추가
- getSchedule API 서비스 함수 추가
- schedule_detail_view.dart 구현 (유튜브, X, 콘서트, 기본 섹션)
- 라우터에 /schedule/:id 경로 추가
- 일정 목록 및 검색 결과에서 상세 화면 이동 기능 추가

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
caadiq 2026-01-15 21:27:24 +09:00
parent 7d96407bfe
commit 122460b2ad
5 changed files with 973 additions and 8 deletions

View file

@ -11,6 +11,7 @@ import '../views/album/album_detail_view.dart';
import '../views/album/album_gallery_view.dart';
import '../views/album/track_detail_view.dart';
import '../views/schedule/schedule_view.dart';
import '../views/schedule/schedule_detail_view.dart';
///
final GlobalKey<NavigatorState> rootNavigatorKey = GlobalKey<NavigatorState>();
@ -81,5 +82,14 @@ final GoRouter appRouter = GoRouter(
return AlbumGalleryView(albumName: albumName);
},
),
// ( )
GoRoute(
path: '/schedule/:id',
parentNavigatorKey: rootNavigatorKey,
builder: (context, state) {
final scheduleId = int.parse(state.pathParameters['id']!);
return ScheduleDetailView(scheduleId: scheduleId);
},
),
],
);

View file

@ -1,6 +1,116 @@
///
library;
///
class ScheduleMember {
final int id;
final String name;
ScheduleMember({required this.id, required this.name});
factory ScheduleMember.fromJson(Map<String, dynamic> json) {
return ScheduleMember(
id: json['id'] as int,
name: json['name'] as String,
);
}
}
/// ( )
class RelatedDate {
final int id;
final String date;
final String? time;
RelatedDate({required this.id, required this.date, this.time});
factory RelatedDate.fromJson(Map<String, dynamic> json) {
return RelatedDate(
id: json['id'] as int,
date: json['date'] as String,
time: json['time'] as String?,
);
}
}
///
class ScheduleDetail {
final int id;
final String title;
final String date;
final String? time;
final String? description;
final int categoryId;
final String? categoryName;
final String? categoryColor;
final String? sourceUrl;
final String? sourceName;
final String? imageUrl;
final List<String> images;
final String? locationName;
final String? locationAddress;
final String? locationLat;
final String? locationLng;
final List<ScheduleMember> members;
final List<RelatedDate> relatedDates;
ScheduleDetail({
required this.id,
required this.title,
required this.date,
this.time,
this.description,
required this.categoryId,
this.categoryName,
this.categoryColor,
this.sourceUrl,
this.sourceName,
this.imageUrl,
this.images = const [],
this.locationName,
this.locationAddress,
this.locationLat,
this.locationLng,
this.members = const [],
this.relatedDates = const [],
});
factory ScheduleDetail.fromJson(Map<String, dynamic> json) {
return ScheduleDetail(
id: json['id'] as int,
title: json['title'] as String,
date: json['date'] as String,
time: json['time'] as String?,
description: json['description'] as String?,
categoryId: json['category_id'] as int,
categoryName: json['category_name'] as String?,
categoryColor: json['category_color'] as String?,
sourceUrl: json['source_url'] as String?,
sourceName: json['source_name'] as String?,
imageUrl: json['image_url'] as String?,
images: (json['images'] as List<dynamic>?)?.cast<String>() ?? [],
locationName: json['location_name'] as String?,
locationAddress: json['location_address'] as String?,
locationLat: json['location_lat'] as String?,
locationLng: json['location_lng'] as String?,
members: (json['members'] as List<dynamic>?)
?.map((m) => ScheduleMember.fromJson(m))
.toList() ??
[],
relatedDates: (json['related_dates'] as List<dynamic>?)
?.map((r) => RelatedDate.fromJson(r))
.toList() ??
[],
);
}
/// (HH:mm)
String? get formattedTime {
if (time == null) return null;
return time!.length >= 5 ? time!.substring(0, 5) : time;
}
}
class Schedule {
final int id;
final String title;

View file

@ -63,6 +63,12 @@ Future<SearchResult> searchSchedules(String query, {int offset = 0, int limit =
);
}
///
Future<ScheduleDetail> getSchedule(int id) async {
final response = await dio.get('/schedules/$id');
return ScheduleDetail.fromJson(response.data);
}
///
Future<List<String>> getSuggestions(String query, {int limit = 10}) async {
if (query.trim().isEmpty) return [];

View file

@ -0,0 +1,832 @@
///
library;
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.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;
}
/// 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;
}
///
void _changeSchedule(int newId) {
setState(() {
_currentScheduleId = newId;
});
}
/// URL
Future<void> _launchUrl(String url) async {
final uri = Uri.parse(url);
if (await canLaunchUrl(uri)) {
await launchUrl(uri, mode: LaunchMode.externalApplication);
}
}
/// ID
String? _extractYoutubeVideoId(String? url) {
if (url == null) return null;
final shortMatch = RegExp(r'youtu\.be/([a-zA-Z0-9_-]{11})').firstMatch(url);
if (shortMatch != null) return shortMatch.group(1);
final watchMatch =
RegExp(r'youtube\.com/watch\?v=([a-zA-Z0-9_-]{11})').firstMatch(url);
if (watchMatch != null) return watchMatch.group(1);
final shortsMatch =
RegExp(r'youtube\.com/shorts/([a-zA-Z0-9_-]{11})').firstMatch(url);
if (shortsMatch != null) return shortsMatch.group(1);
return null;
}
/// (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]})';
}
/// (1 15 () 19:00)
String _formatSingleDate(String dateStr, String? timeStr) {
final date = DateTime.parse(dateStr);
final dayNames = ['', '', '', '', '', '', ''];
var result = '${date.month}${date.day}일 (${dayNames[date.weekday % 7]})';
if (timeStr != null && timeStr.length >= 5) {
result += ' ${timeStr.substring(0, 5)}';
}
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(Icons.arrow_back_ios_new, size: 20),
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(
Icons.calendar_today,
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(Icons.arrow_back),
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) {
return SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: _buildCategorySection(schedule),
);
}
///
Widget _buildCategorySection(ScheduleDetail schedule) {
switch (schedule.categoryId) {
case CategoryId.youtube:
return _buildYoutubeSection(schedule);
case CategoryId.x:
return _buildXSection(schedule);
case CategoryId.concert:
return _buildConcertSection(schedule);
default:
return _buildDefaultSection(schedule);
}
}
///
Widget _buildYoutubeSection(ScheduleDetail schedule) {
final videoId = _extractYoutubeVideoId(schedule.sourceUrl);
final isShorts = schedule.sourceUrl?.contains('/shorts/') ?? false;
if (videoId == null) return _buildDefaultSection(schedule);
// URL
final thumbnailUrl = 'https://img.youtube.com/vi/$videoId/maxresdefault.jpg';
return Column(
children: [
// +
GestureDetector(
onTap: () => _launchUrl(schedule.sourceUrl!),
child: ClipRRect(
borderRadius: BorderRadius.circular(12),
child: isShorts
? Center(
child: SizedBox(
width: 200,
child: AspectRatio(
aspectRatio: 9 / 16,
child: Stack(
alignment: Alignment.center,
children: [
CachedNetworkImage(
imageUrl: thumbnailUrl,
fit: BoxFit.cover,
width: double.infinity,
height: double.infinity,
placeholder: (_, _) => Container(
color: Colors.grey[200],
),
errorWidget: (_, _, _) => Container(
color: Colors.grey[200],
child: const Icon(Icons.play_circle_outline, size: 48),
),
),
_buildPlayButton(),
],
),
),
),
)
: AspectRatio(
aspectRatio: 16 / 9,
child: Stack(
alignment: Alignment.center,
children: [
CachedNetworkImage(
imageUrl: thumbnailUrl,
fit: BoxFit.cover,
width: double.infinity,
height: double.infinity,
placeholder: (_, _) => Container(
color: Colors.grey[200],
),
errorWidget: (_, _, _) => Container(
color: Colors.grey[200],
child: const Icon(Icons.play_circle_outline, size: 48),
),
),
_buildPlayButton(),
],
),
),
),
),
const SizedBox(height: 16),
//
_buildInfoCard(
schedule,
bottomButton: _buildYoutubeButton(schedule.sourceUrl),
),
],
);
}
///
Widget _buildPlayButton() {
return Container(
width: 64,
height: 64,
decoration: BoxDecoration(
color: Colors.red,
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.3),
blurRadius: 8,
offset: const Offset(0, 2),
),
],
),
child: const Icon(
Icons.play_arrow,
color: Colors.white,
size: 40,
),
);
}
/// X
Widget _buildXSection(ScheduleDetail schedule) {
return Container(
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.all(16),
child: Row(
children: [
Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color: Colors.grey[800],
shape: BoxShape.circle,
),
child: Center(
child: Text(
(schedule.sourceName ?? 'X')[0].toUpperCase(),
style: const TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
),
),
),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Flexible(
child: Text(
schedule.sourceName ?? '',
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 14,
),
overflow: TextOverflow.ellipsis,
),
),
const SizedBox(width: 4),
const Icon(Icons.verified,
color: Colors.blue, size: 16),
],
),
],
),
),
],
),
),
//
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Text(
decodeHtmlEntities(schedule.description ?? schedule.title),
style: const TextStyle(fontSize: 15, height: 1.5),
),
),
//
if (schedule.imageUrl != null) ...[
const SizedBox(height: 12),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: ClipRRect(
borderRadius: BorderRadius.circular(12),
child: Image.network(
schedule.imageUrl!,
fit: BoxFit.cover,
errorBuilder: (_, _, _) => const SizedBox.shrink(),
),
),
),
],
//
Padding(
padding: const EdgeInsets.all(16),
child: Text(
_formatFullDate(schedule.date),
style: const TextStyle(
fontSize: 14,
color: AppColors.textSecondary,
),
),
),
// X에서
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.grey[50],
border: Border(top: BorderSide(color: AppColors.border)),
),
child: SizedBox(
width: double.infinity,
child: ElevatedButton.icon(
onPressed: () => _launchUrl(schedule.sourceUrl ?? ''),
icon: const Icon(Icons.open_in_new, size: 18),
label: const Text('X에서 보기'),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.black,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(vertical: 14),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(24),
),
),
),
),
),
],
),
);
}
///
Widget _buildConcertSection(ScheduleDetail schedule) {
final hasLocation =
schedule.locationLat != null && schedule.locationLng != null;
final hasPoster = schedule.images.isNotEmpty;
final hasMultipleDates = schedule.relatedDates.length > 1;
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),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
//
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [AppColors.primary, AppColors.primary.withValues(alpha: 0.8)],
),
borderRadius:
const BorderRadius.vertical(top: Radius.circular(12)),
),
child: Row(
children: [
if (hasPoster)
ClipRRect(
borderRadius: BorderRadius.circular(8),
child: Image.network(
schedule.images[0],
width: 56,
height: 72,
fit: BoxFit.cover,
errorBuilder: (_, _, _) => const SizedBox.shrink(),
),
),
if (hasPoster) const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
decodeHtmlEntities(schedule.title),
style: const TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: 15,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
if (schedule.locationName != null) ...[
const SizedBox(height: 4),
Text(
schedule.locationName!,
style: TextStyle(
color: Colors.white.withValues(alpha: 0.8),
fontSize: 12,
),
overflow: TextOverflow.ellipsis,
),
],
],
),
),
],
),
),
//
Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
//
_buildSectionLabel(Icons.calendar_today, '공연 일정'),
const SizedBox(height: 8),
if (hasMultipleDates)
...schedule.relatedDates.asMap().entries.map((entry) {
final index = entry.key;
final item = entry.value;
final isCurrent = item.id == schedule.id;
return Padding(
padding: const EdgeInsets.only(bottom: 8),
child: InkWell(
onTap: isCurrent ? null : () => _changeSchedule(item.id),
borderRadius: BorderRadius.circular(8),
child: Container(
width: double.infinity,
padding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 10,
),
decoration: BoxDecoration(
color: isCurrent
? AppColors.primary
: Colors.grey[100],
borderRadius: BorderRadius.circular(8),
),
child: Text(
'${index + 1}회차 ${_formatSingleDate(item.date, item.time)}',
style: TextStyle(
fontSize: 13,
fontWeight:
isCurrent ? FontWeight.bold : FontWeight.normal,
color: isCurrent
? Colors.white
: AppColors.textPrimary,
),
),
),
),
);
})
else
Text(
_formatSingleDate(schedule.date, schedule.time),
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 14,
),
),
const SizedBox(height: 16),
//
if (schedule.locationName != null) ...[
_buildSectionLabel(Icons.place, '장소'),
const SizedBox(height: 8),
Text(
schedule.locationName!,
style: const TextStyle(
fontWeight: FontWeight.w600,
fontSize: 14,
),
),
if (schedule.locationAddress != null) ...[
const SizedBox(height: 2),
Text(
schedule.locationAddress!,
style: const TextStyle(
fontSize: 12,
color: AppColors.textSecondary,
),
),
],
const SizedBox(height: 16),
],
//
if (schedule.description != null &&
schedule.description!.isNotEmpty) ...[
Container(
width: double.infinity,
padding: const EdgeInsets.only(top: 16),
decoration: BoxDecoration(
border: Border(
top: BorderSide(color: AppColors.divider),
),
),
child: Text(
decodeHtmlEntities(schedule.description!),
style: const TextStyle(
fontSize: 14,
color: AppColors.textSecondary,
height: 1.5,
),
),
),
],
],
),
),
//
Padding(
padding: const EdgeInsets.fromLTRB(16, 0, 16, 16),
child: Column(
children: [
if (hasLocation)
SizedBox(
width: double.infinity,
child: ElevatedButton.icon(
onPressed: () => _launchUrl(
'https://map.kakao.com/link/to/${Uri.encodeComponent(schedule.locationName!)},${schedule.locationLat},${schedule.locationLng}'),
icon: const Icon(Icons.navigation, size: 18),
label: const Text('길찾기'),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.blue,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(vertical: 14),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
),
),
if (hasLocation && schedule.sourceUrl != null)
const SizedBox(height: 8),
if (schedule.sourceUrl != null)
SizedBox(
width: double.infinity,
child: ElevatedButton.icon(
onPressed: () => _launchUrl(schedule.sourceUrl!),
icon: const Icon(Icons.open_in_new, size: 18),
label: const Text('상세 정보 보기'),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.grey[900],
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(vertical: 14),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
),
),
],
),
),
],
),
);
}
///
Widget _buildDefaultSection(ScheduleDetail schedule) {
return _buildInfoCard(
schedule,
bottomButton: schedule.sourceUrl != null
? SizedBox(
width: double.infinity,
child: ElevatedButton.icon(
onPressed: () => _launchUrl(schedule.sourceUrl!),
icon: const Icon(Icons.open_in_new, size: 18),
label: const Text('원본 보기'),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.grey[900],
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(vertical: 14),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
),
)
: null,
);
}
///
Widget _buildInfoCard(ScheduleDetail schedule, {Widget? bottomButton}) {
final isFullGroup = schedule.members.length >= 5;
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(Icons.calendar_today, _formatFullDate(schedule.date)),
if (schedule.formattedTime != null)
_buildMetaItem(Icons.access_time, schedule.formattedTime!),
if (schedule.sourceName != null)
_buildMetaItem(Icons.link, schedule.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(),
),
],
//
if (schedule.description != null &&
schedule.description!.isNotEmpty) ...[
const SizedBox(height: 16),
Text(
decodeHtmlEntities(schedule.description!),
style: const TextStyle(
fontSize: 14,
color: AppColors.textSecondary,
height: 1.5,
),
),
],
//
if (bottomButton != null) ...[
const SizedBox(height: 16),
bottomButton,
],
],
),
);
}
///
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 _buildSectionLabel(IconData icon, String text) {
return Row(
children: [
Icon(icon, size: 14, color: AppColors.textSecondary),
const SizedBox(width: 6),
Text(
text,
style: const TextStyle(
fontSize: 12,
fontWeight: FontWeight.w500,
color: AppColors.textSecondary,
),
),
],
);
}
///
Widget _buildYoutubeButton(String? url) {
if (url == null) return const SizedBox.shrink();
return SizedBox(
width: double.infinity,
child: ElevatedButton.icon(
onPressed: () => _launchUrl(url),
icon: const Icon(Icons.play_circle_filled, size: 20),
label: const Text('YouTube에서 보기'),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.red,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(vertical: 14),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
),
);
}
}

View file

@ -8,6 +8,7 @@ import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:expandable_page_view/expandable_page_view.dart';
import 'package:go_router/go_router.dart';
import '../../core/constants.dart';
import '../../models/schedule.dart';
import '../../controllers/schedule_controller.dart';
@ -777,10 +778,13 @@ class _ScheduleViewState extends ConsumerState<ScheduleView>
padding: EdgeInsets.only(
bottom: index < searchState.results.length - 1 ? 12 : 0,
),
child: GestureDetector(
onTap: () => context.push('/schedule/${schedule.id}'),
child: SearchScheduleCard(
schedule: schedule,
categoryColor: parseColor(schedule.categoryColor),
),
),
);
},
),
@ -1581,12 +1585,15 @@ class _ScheduleViewState extends ConsumerState<ScheduleView>
padding: EdgeInsets.only(
bottom: index < state.selectedDateSchedules.length - 1 ? 12 : 0,
),
child: GestureDetector(
onTap: () => context.push('/schedule/${schedule.id}'),
child: AnimatedScheduleCard(
key: ValueKey('${schedule.id}_${state.selectedDate.toString()}'),
index: index,
schedule: schedule,
categoryColor: parseColor(schedule.categoryColor),
),
),
);
},
);