Files
QingTing-Player/lib/pages/playlist_detail_page.dart
T
lxh2875931338 e89bd6381a feat(playlist): 歌单集合页 + 歌单详情页 + 首页入口
style(ui): 播放页红心与标题居中对齐、标题单行

- playlists_page:歌单列表(封面/名称/歌曲数)、新建、重命名、删除
- playlist_detail_page:歌单内歌曲、播放全部、单曲播放、移除
- 首页"歌单列表"按钮接入 PlaylistsPage
- 播放页:标题行改为 center 对齐、标题限制单行,
  收藏按钮压到 36x36,红心与标题不再错位
2026-10-31 23:37:47 +08:00

184 lines
5.6 KiB
Dart

// lib/pages/playlist_detail_page.dart
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../database/song_database.dart';
import '../repositories/playlist_repository.dart';
import '../services/audio_service.dart';
import '../widgets/song_artwork.dart';
/// 歌单详情:展示歌单内歌曲,支持播放全部 / 单曲播放 / 移除
class PlaylistDetailPage extends StatefulWidget {
final String playlistId;
final String playlistName;
const PlaylistDetailPage({
super.key,
required this.playlistId,
required this.playlistName,
});
@override
State<PlaylistDetailPage> createState() => _PlaylistDetailPageState();
}
class _SongRow {
final String path;
final String title;
final String artist;
final String? artworkPath;
const _SongRow({
required this.path,
required this.title,
required this.artist,
this.artworkPath,
});
}
class _PlaylistDetailPageState extends State<PlaylistDetailPage> {
final PlaylistRepository _repo = PlaylistRepository();
final SongDatabase _db = SongDatabase();
List<_SongRow> _songs = [];
bool _loading = true;
@override
void initState() {
super.initState();
_load();
}
Future<void> _load() async {
setState(() => _loading = true);
try {
final ids = await _repo.getPlaylistSongIds(widget.playlistId);
final meta = await _db.getSongsByPaths(ids);
final rows = <_SongRow>[];
for (final id in ids) {
final row = meta[id];
final title = (row?['title'] as String?)?.trim() ?? '';
final artist = (row?['artist'] as String?)?.trim() ?? '';
String fallbackName = id.split('/').last;
try {
fallbackName = Uri.decodeComponent(fallbackName);
} catch (_) {}
rows.add(_SongRow(
path: id,
title: title.isNotEmpty
? title
: fallbackName.replaceAll(RegExp(r'\.[^.]*$'), ''),
artist: artist.isNotEmpty ? artist : '未知艺术家',
artworkPath: (row?['artwork_path'] as String?)?.trim().isNotEmpty ==
true
? (row!['artwork_path'] as String).trim()
: null,
));
}
if (!mounted) return;
setState(() {
_songs = rows;
_loading = false;
});
} catch (e) {
if (!mounted) return;
setState(() => _loading = false);
ScaffoldMessenger.of(context)
.showSnackBar(SnackBar(content: Text('读取歌单失败: $e')));
}
}
Future<void> _play(int index) async {
await context
.read<AudioService>()
.playPlaylist(widget.playlistId, startIndex: index);
}
Future<void> _remove(_SongRow song) async {
await _repo.removeSong(widget.playlistId, song.path);
await _load();
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: const Color(0xFF0E1211),
appBar: AppBar(
backgroundColor: Colors.transparent,
elevation: 0,
title: Text(widget.playlistName),
actions: [
IconButton(
icon: const Icon(Icons.play_arrow_rounded, color: Color(0xFFB8D4D0)),
tooltip: '播放全部',
onPressed: _songs.isEmpty ? null : () => _play(0),
),
],
),
body: _loading
? const Center(child: CircularProgressIndicator())
: _songs.isEmpty
? _buildEmpty()
: ListView.builder(
padding:
const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
itemCount: _songs.length,
itemBuilder: (context, index) {
final song = _songs[index];
return ListTile(
contentPadding:
const EdgeInsets.symmetric(horizontal: 4),
visualDensity: VisualDensity.compact,
leading: SongArtwork(
filePath: song.artworkPath,
size: 44,
fallbackIcon: Icons.music_note,
),
title: Text(
song.title,
style:
const TextStyle(color: Colors.white, fontSize: 16),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
subtitle: Text(
song.artist,
style: const TextStyle(color: Colors.grey, fontSize: 13),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
trailing: IconButton(
icon: const Icon(Icons.close,
color: Colors.white24, size: 18),
tooltip: '从歌单移除',
onPressed: () => _remove(song),
),
onTap: () => _play(index),
);
},
),
);
}
Widget _buildEmpty() {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.music_off_outlined, size: 44, color: Colors.grey[700]),
const SizedBox(height: 14),
Text(
'歌单还是空的',
style: TextStyle(color: Colors.grey[500], fontSize: 15),
),
const SizedBox(height: 6),
Text(
'在文件列表长按歌曲即可加入',
style: TextStyle(color: Colors.grey[700], fontSize: 12),
),
],
),
);
}
}