리스트뷰 가로로 수정

This commit is contained in:
girinb
2025-07-10 18:45:20 +09:00
parent ae99bd661d
commit 2ad2af76e0

View File

@@ -142,12 +142,17 @@ class _PlanPageDetailState extends State<PlanPageDetail> {
toolbarHeight: 40,
title: Text(_planTitle.toString()),
),
body: _planId == null
? const Center(
child: Text(
'Plan ID가 전달되지 않았습니다.',
body: _planId == null && _planTitle == null && _planDetails == null
? Center( // 초기 로딩 상태 또는 인자 오류
child: _planTitle == 'Error'
? const Text(
'플랜 정보를 불러올 수 없습니다.',
style: TextStyle(fontSize: 18, color: Colors.red),
),
textAlign: TextAlign.center,
maxLines: 3,
overflow: TextOverflow.ellipsis,
)
: const CircularProgressIndicator(),
)
: FutureBuilder<List<PlanDetailItem>>(
future: _planDetails,
@@ -155,82 +160,136 @@ class _PlanPageDetailState extends State<PlanPageDetail> {
if (snapshot.connectionState == ConnectionState.waiting) {
return const Center(child: CircularProgressIndicator());
} else if (snapshot.hasError) {
return Center(child: Text('Error: ${snapshot.error}'));
return Center(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Text('Error loading details: ${snapshot.error}', textAlign: TextAlign.center,
maxLines: 3,
overflow: TextOverflow.ellipsis,
)));
} else if (!snapshot.hasData || snapshot.data!.isEmpty) {
return const Center(child: Text('세부 계획 데이터가 없습니다.'));
} else {
final details = snapshot.data!;
return ListView.builder(
itemCount: details.length,
itemBuilder: (context, index) {
final item = details[index];
return Card(
margin: const EdgeInsets.symmetric(vertical: 8.0, horizontal: 16.0),
child: ListTile(
leading: item.thumbnail.isNotEmpty
? SizedBox(
width: 100,
height: 100,
child: Image.network(
item.thumbnail,
fit: BoxFit.cover,
errorBuilder: (context, error, stackTrace) {
return const Icon(Icons.broken_image, size: 40, color: Colors.grey);
// 첫 번째 비디오의 URL을 가져와 _selectedYoutubeUrl을 초기화합니다.
if (_selectedYoutubeUrl == null && details.isNotEmpty) {
_selectedYoutubeUrl = details.firstWhere(
(item) => item.lessonUrl.isNotEmpty &&
(item.lessonUrl.contains('youtube.com') ||
item.lessonUrl.contains('youtu.be')),
orElse: () => PlanDetailItem(lessonId: '', lessonTag: '', lessonUrl: '', thumbnail: ''),
).lessonUrl.isNotEmpty ? details.firstWhere(
(item) => item.lessonUrl.isNotEmpty &&
(item.lessonUrl.contains('youtube.com') ||
item.lessonUrl.contains('youtu.be')),
orElse: () => PlanDetailItem(lessonId: '', lessonTag: '', lessonUrl: '', thumbnail: ''),
).lessonUrl : null;
}
return Column( // ListView와 YoutubePlayerPage를 세로로 배치하기 위해 Column 사용
children: [
SizedBox(
height: 150, // 가로 스크롤 리스트의 높이
child: ListView.builder(
scrollDirection: Axis.horizontal,
padding: const EdgeInsets.symmetric(horizontal: 8.0),
itemCount: details.length,
itemBuilder: (context, index) {
final item = details[index];
return GestureDetector(
onTap: () {
if (item.lessonUrl.isNotEmpty &&
(item.lessonUrl.contains('youtube.com') ||
item.lessonUrl.contains('youtu.be'))) {
setState(() {
_selectedYoutubeUrl = item.lessonUrl;
});
} else {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
'유효한 YouTube URL이 아닙니다: ${item.lessonUrl}')),
);
}
},
loadingBuilder: (BuildContext context, Widget child, ImageChunkEvent? loadingProgress) {
if (loadingProgress == null) return child;
return Center(
child: CircularProgressIndicator(
value: loadingProgress.expectedTotalBytes != null
? loadingProgress.cumulativeBytesLoaded / loadingProgress.expectedTotalBytes!
: null,
),
);
},
),
)
: Container(
width: 100,
height: 100,
color: Colors.grey[200],
child: const Icon(Icons.image_not_supported, size: 40, color: Colors.grey),
),
title: Text(item.lessonTag, style: const TextStyle(fontWeight: FontWeight.bold)),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text('ID: ${item.lessonId}', style: TextStyle(fontSize: 12, color: Colors.grey[700])),
const SizedBox(height: 2),
Text(
item.lessonUrl,
style: const TextStyle(fontSize: 12, color: Colors.blueAccent),
overflow: TextOverflow.ellipsis,
maxLines: 2,
),
],
),
isThreeLine: true,
onTap: () {
if (item.lessonUrl.isNotEmpty && (item.lessonUrl.contains('youtube.com') || item.lessonUrl.contains('youtu.be'))) {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => YoutubePlayerPage(
lessonUrl: item.lessonUrl,
// currentBottomNavIndex: _currentBottomNavIndex, // 현재 탭 인덱스 전달
),
child: Container(
width: 110,
margin: const EdgeInsets.symmetric(
horizontal: 8.0, vertical: 8.0),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
SizedBox(
width: 100,
height: 100,
child: ClipRRect(
borderRadius:
BorderRadius.circular(8.0),
child: item.thumbnail.isNotEmpty
? Image.network(
item.thumbnail,
fit: BoxFit.cover,
errorBuilder: (context, error,
stackTrace) {
return Container(
color: Colors.grey[200],
child: const Icon(
Icons.broken_image,
size: 40,
color: Colors.grey),
);
},
loadingBuilder: (BuildContext
context,
Widget child,
ImageChunkEvent?
loadingProgress) {
if (loadingProgress == null)
return child;
return Center(
child:
CircularProgressIndicator(
value: loadingProgress
.expectedTotalBytes !=
null
? loadingProgress
.cumulativeBytesLoaded /
loadingProgress
.expectedTotalBytes!
: null,
strokeWidth: 2.0,
),
);
},
)
: Container(
color: Colors.grey[200],
child: const Icon(
Icons.image_not_supported,
size: 40,
color: Colors.grey),
),
),
),
const SizedBox(height: 6),
Text(
item.lessonTag,
style: const TextStyle(
fontSize: 13,
fontWeight: FontWeight.w500),
maxLines: 2,
overflow: TextOverflow.ellipsis,
textAlign: TextAlign.center,
),
],
),
);
} else {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('유효한 YouTube URL이 아닙니다: ${item.lessonUrl}')),
);
}
),
);
},
),
);
},
)],
);
}
},