리스트뷰 가로로 수정

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