数据层: - PlaylistSummary(歌单名/歌曲数/封面),DB 用一条 SQL 取摘要, 封面取歌单内第一首有封面的歌 - PlaylistRepository.getSummaries() / addSongs()(批量加入并去重) - 修复 AudioService.playPlaylist:之前把文件路径直接当 URL, 歌单播放必然失败;现在用 WebDAVService.getFileUrl 构造 入口(视觉为临时实现,等设计稿替换): - 文件列表长按 -> "加入歌单" 底部弹层 - 可在弹层里新建歌单并直接加入 真机验证:长按歌曲 -> 新建 "TL" -> 数据库 playlists/playlist_songs 均有记录。 测试:新增 playlist_summary 4 个用例,累计 29 个全绿。
167 lines
5.4 KiB
Dart
167 lines
5.4 KiB
Dart
// lib/widgets/add_to_playlist_sheet.dart
|
|
import 'package:flutter/material.dart';
|
|
|
|
import '../models/playlist_summary.dart';
|
|
import '../repositories/playlist_repository.dart';
|
|
|
|
/// 把一首歌加入歌单的底部弹层。
|
|
///
|
|
/// 视觉先保持系统样式,等设计定稿后只换这个文件即可,
|
|
/// 调用方(文件列表长按)不用改。
|
|
Future<void> showAddToPlaylistSheet(
|
|
BuildContext context, {
|
|
required String songPath,
|
|
}) async {
|
|
final repo = PlaylistRepository();
|
|
List<PlaylistSummary> playlists;
|
|
try {
|
|
playlists = await repo.getSummaries();
|
|
} catch (e) {
|
|
if (context.mounted) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
SnackBar(content: Text('读取歌单失败: $e')),
|
|
);
|
|
}
|
|
return;
|
|
}
|
|
if (!context.mounted) return;
|
|
|
|
await showModalBottomSheet<void>(
|
|
context: context,
|
|
backgroundColor: const Color(0xFF1A1F1E),
|
|
builder: (sheetContext) {
|
|
return SafeArea(
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
const Padding(
|
|
padding: EdgeInsets.fromLTRB(20, 18, 20, 8),
|
|
child: Align(
|
|
alignment: Alignment.centerLeft,
|
|
child: Text(
|
|
'加入歌单',
|
|
style: TextStyle(
|
|
color: Colors.white,
|
|
fontSize: 16,
|
|
fontWeight: FontWeight.w500,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
ListTile(
|
|
leading: const Icon(Icons.add, color: Color(0xFFB8D4D0)),
|
|
title: const Text(
|
|
'新建歌单',
|
|
style: TextStyle(color: Color(0xFFB8D4D0)),
|
|
),
|
|
onTap: () async {
|
|
Navigator.pop(sheetContext);
|
|
await _createPlaylistAndAdd(context, repo, songPath);
|
|
},
|
|
),
|
|
const Divider(height: 1, color: Colors.white12),
|
|
if (playlists.isEmpty)
|
|
const Padding(
|
|
padding: EdgeInsets.symmetric(vertical: 24),
|
|
child: Text(
|
|
'还没有歌单,先新建一个',
|
|
style: TextStyle(color: Colors.grey, fontSize: 13),
|
|
),
|
|
)
|
|
else
|
|
ConstrainedBox(
|
|
constraints: const BoxConstraints(maxHeight: 320),
|
|
child: ListView.builder(
|
|
shrinkWrap: true,
|
|
itemCount: playlists.length,
|
|
itemBuilder: (context, index) {
|
|
final playlist = playlists[index];
|
|
return ListTile(
|
|
leading: const Icon(
|
|
Icons.queue_music,
|
|
color: Color(0xFFB8D4D0),
|
|
),
|
|
title: Text(
|
|
playlist.name,
|
|
style: const TextStyle(color: Colors.white),
|
|
),
|
|
subtitle: Text(
|
|
'${playlist.songCount} 首',
|
|
style: const TextStyle(
|
|
color: Colors.grey, fontSize: 12),
|
|
),
|
|
onTap: () async {
|
|
final added = await repo.addSongs(
|
|
playlist.id,
|
|
[songPath],
|
|
);
|
|
if (!sheetContext.mounted) return;
|
|
Navigator.pop(sheetContext);
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
SnackBar(
|
|
content: Text(added > 0
|
|
? '已加入「${playlist.name}」'
|
|
: '这首歌已经在「${playlist.name}」里了'),
|
|
),
|
|
);
|
|
},
|
|
);
|
|
},
|
|
),
|
|
),
|
|
const SizedBox(height: 8),
|
|
],
|
|
),
|
|
);
|
|
},
|
|
);
|
|
}
|
|
|
|
Future<void> _createPlaylistAndAdd(
|
|
BuildContext context,
|
|
PlaylistRepository repo,
|
|
String songPath,
|
|
) async {
|
|
final controller = TextEditingController();
|
|
final name = await showDialog<String>(
|
|
context: context,
|
|
builder: (dialogContext) => AlertDialog(
|
|
backgroundColor: const Color(0xFF1A1F1E),
|
|
title: const Text('新建歌单', style: TextStyle(color: Colors.white)),
|
|
content: TextField(
|
|
controller: controller,
|
|
autofocus: true,
|
|
style: const TextStyle(color: Colors.white),
|
|
decoration: const InputDecoration(hintText: '歌单名称'),
|
|
),
|
|
actions: [
|
|
TextButton(
|
|
onPressed: () => Navigator.pop(dialogContext),
|
|
child: const Text('取消'),
|
|
),
|
|
TextButton(
|
|
onPressed: () =>
|
|
Navigator.pop(dialogContext, controller.text.trim()),
|
|
child: const Text('创建'),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
|
|
if (name == null || name.isEmpty) return;
|
|
|
|
try {
|
|
final playlist = await repo.createPlaylist(name);
|
|
await repo.addSongs(playlist.id, [songPath]);
|
|
if (!context.mounted) return;
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
SnackBar(content: Text('已新建「$name」并加入这首歌')),
|
|
);
|
|
} catch (e) {
|
|
if (!context.mounted) return;
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
SnackBar(content: Text('新建歌单失败: $e')),
|
|
);
|
|
}
|
|
}
|