Files
UnionApp/lib/pages/music/music_page.dart
spasolreisa 00bd43dc7f 0418 0222
更新2
2026-04-18 02:11:45 +08:00

920 lines
33 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:unionapp/pages/music/score_single.dart';
import '../../../model/song_model.dart';
import '../../../providers/user_provider.dart';
import '../../../service/song_service.dart';
import '../../../service/user_service.dart';
class MusicPage extends StatefulWidget {
const MusicPage({super.key});
@override
State<MusicPage> createState() => _MusicPageState();
}
class _MusicPageState extends State<MusicPage> with SingleTickerProviderStateMixin {
bool _isLoading = true;
bool _isRefreshing = false;
String _errorMessage = '';
List<SongModel> _allSongs = [];
List<SongModel> _displaySongs = [];
// 筛选
String _searchQuery = '';
int? _filterLevelType;
String? _filterFromVersion;
String? _filterGenre;
double? _selectedMinLevel;
double? _selectedMaxLevel;
bool _isAdvancedFilterExpanded = false;
late AnimationController _animationController;
late Animation<double> _animation;
static const List<double> _levelOptions = [
1.0, 1.5, 2.0, 2.5, 3.0, 3.5, 4.0, 4.5, 5.0, 5.5,
6.0, 6.5, 7.0, 7.5, 8.0, 8.5, 9.0, 9.5, 10.0, 10.5,
11.0, 11.5, 12.0, 12.5,
13.0, 13.1, 13.2, 13.3, 13.4, 13.5, 13.6, 13.7, 13.8, 13.9,
14.0, 14.1, 14.2, 14.3, 14.4, 14.5, 14.6, 14.7, 14.8, 14.9,
15.0,
];
late FixedExtentScrollController _minLevelScrollController;
late FixedExtentScrollController _maxLevelScrollController;
Set<String> _availableVersions = {};
Set<String> _availableGenres = {};
// 用户成绩缓存key = 真实完整 musicId
Map<int, Map<int, dynamic>> _userScoreCache = {};
@override
void initState() {
super.initState();
_animationController = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 300),
);
_animation = CurvedAnimation(
parent: _animationController,
curve: Curves.easeInOut,
);
_minLevelScrollController = FixedExtentScrollController(initialItem: 0);
_maxLevelScrollController = FixedExtentScrollController(initialItem: _levelOptions.length - 1);
_loadAllSongs();
_loadUserScores();
UserProvider.instance.addListener(_onUserChanged);
}
void _onUserChanged() {
if (mounted) {
_loadUserScores();
}
}
@override
void dispose() {
UserProvider.instance.removeListener(_onUserChanged);
_animationController.dispose();
_minLevelScrollController.dispose();
_maxLevelScrollController.dispose();
super.dispose();
}
// 加载所有歌曲
Future<void> _loadAllSongs() async {
setState(() => _isLoading = true);
try {
final songs = await SongService.getAllSongs();
_allSongs = songs;
final uniqueSongs = <int, SongModel>{};
for (final song in songs) {
uniqueSongs.putIfAbsent(song.id, () => song);
}
_allSongs = uniqueSongs.values.toList();
_allSongs.sort((a, b) => (b.players ?? 0).compareTo(a.players ?? 0));
_displaySongs = List.from(_allSongs);
_availableVersions = songs.map((s) => s.from).where((v) => v.isNotEmpty).toSet();
_availableGenres = songs.map((s) => s.genre).where((g) => g.isNotEmpty).toSet();
setState(() => _errorMessage = '');
} catch (e) {
setState(() => _errorMessage = e.toString());
} finally {
setState(() => _isLoading = false);
}
}
// 加载用户成绩 —— ✅ 修复 1直接用完整ID存储
Future<void> _loadUserScores() async {
try {
final userProvider = UserProvider.instance;
final token = userProvider.token;
if (token == null) return;
final userName = userProvider.selectedCnUserName;
Map<String, dynamic> scoreData;
if (userProvider.scoreDataSource == 'sega') {
final segaId = userProvider.selectedSegaId;
if (segaId == null || segaId.isEmpty) return;
scoreData = await UserService.getSegaRatingData(token, segaId);
} else {
scoreData = await SongService.getUserAllScores(token, name: userName);
}
List<dynamic> userMusicList = [];
if (scoreData.containsKey('userScoreAll_')) {
userMusicList = scoreData['userScoreAll_']['userMusicList'] ?? [];
} else if (scoreData.containsKey('userMusicList')) {
userMusicList = scoreData['userMusicList'] ?? [];
} else if (scoreData.containsKey('segaId2chartlist')) {
final segaId = userProvider.selectedSegaId;
final map = scoreData['segaId2chartlist'] ?? {};
userMusicList = (map[segaId] as List?) ?? [];
}
_userScoreCache.clear();
for (var group in userMusicList) {
final details = group['userMusicDetailList'] ?? [];
for (var d in details) {
int realMusicId = d['musicId']; // 完整ID
int level = d['level'] ?? 0;
_userScoreCache[realMusicId] ??= {};
_userScoreCache[realMusicId]![level] = d;
}
}
setState(() {});
} catch (e) {
print("加载成绩失败: $e");
}
}
// 搜索 + 过滤
void _applyFilters() {
final q = _normalizeSearch(_searchQuery);
_displaySongs = _allSongs.where((song) {
if (q.isNotEmpty) {
bool match = false;
if (_normalizeSearch(song.title).contains(q)) match = true;
if (_normalizeSearch(song.artist).contains(q)) match = true;
if (song.id.toString().contains(q)) match = true;
for (var a in song.albums) {
if (_normalizeSearch(a).contains(q)) match = true;
}
if (!match) return false;
}
if (_filterFromVersion != null && song.from != _filterFromVersion) return false;
if (_filterGenre != null && song.genre != _filterGenre) return false;
if (_filterLevelType != null) {
bool hasLevel = false;
final allDiffs = _getAllDifficultiesWithType(song);
for (var d in allDiffs) {
if (d['diff']['level_id'] == _filterLevelType) {
hasLevel = true;
break;
}
}
if (!hasLevel) return false;
}
if (_selectedMinLevel != null || _selectedMaxLevel != null) {
bool inRange = false;
final allDiffs = _getAllDifficultiesWithType(song);
for (var d in allDiffs) {
final lv = double.tryParse(d['diff']['level_value']?.toString() ?? '');
if (lv == null) continue;
final minOk = _selectedMinLevel == null || lv >= _selectedMinLevel!;
final maxOk = _selectedMaxLevel == null || lv <= _selectedMaxLevel!;
if (minOk && maxOk) {
inRange = true;
break;
}
}
if (!inRange) return false;
}
return true;
}).toList();
setState(() {});
}
String _normalizeSearch(String? s) {
if (s == null) return '';
return s.trim().toLowerCase();
}
// 带类型的难度SD/DX/UT
List<Map<String, dynamic>> _getAllDifficultiesWithType(SongModel song) {
List<Map<String, dynamic>> all = [];
if (song.sd != null && song.sd!.isNotEmpty) {
for (var d in song.sd!.values) {
all.add({'type': 'SD', 'diff': d});
}
}
if (song.dx != null && song.dx!.isNotEmpty) {
for (var d in song.dx!.values) {
all.add({'type': 'DX', 'diff': d});
}
}
if (song.ut != null && song.ut!.isNotEmpty) {
for (var d in song.ut!.values) {
all.add({'type': 'UT', 'diff': d});
}
}
return all;
}
// 根据歌曲类型获取真实完整ID
int _getRealMusicId(SongModel song, String type) {
if (type == "SD") {
return song.id;
} else if (type == "DX") {
return 10000 + song.id;
} else {
return 100000 + song.id;
}
}
void _resetFilters() {
setState(() {
_searchQuery = '';
_filterLevelType = null;
_filterFromVersion = null;
_filterGenre = null;
_selectedMinLevel = null;
_selectedMaxLevel = null;
});
_minLevelScrollController.jumpToItem(0);
_maxLevelScrollController.jumpToItem(_levelOptions.length - 1);
_applyFilters();
}
String _getLevelRangeText() {
if (_selectedMinLevel == null && _selectedMaxLevel == null) return "全部";
final min = _selectedMinLevel?.toStringAsFixed(1) ?? "1.0";
final max = _selectedMaxLevel?.toStringAsFixed(1) ?? "15.0";
return "$min ~ $max";
}
void _showLevelPickerDialog() {
int minIdx = _selectedMinLevel != null ? _levelOptions.indexOf(_selectedMinLevel!) : 0;
int maxIdx = _selectedMaxLevel != null ? _levelOptions.indexOf(_selectedMaxLevel!) : _levelOptions.length - 1;
showModalBottomSheet(
context: context,
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.vertical(top: Radius.circular(16))),
builder: (context) {
WidgetsBinding.instance.addPostFrameCallback((_) {
_minLevelScrollController.jumpToItem(minIdx);
_maxLevelScrollController.jumpToItem(maxIdx);
});
return Container(
height: 300,
padding: const EdgeInsets.only(top: 16),
child: Column(
children: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
TextButton(
onPressed: () {
setState(() {
_selectedMinLevel = null;
_selectedMaxLevel = null;
});
Navigator.pop(context);
},
child: const Text("重置", style: TextStyle(color: Colors.red)),
),
const Text("定数范围", style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16)),
TextButton(
onPressed: () {
final min = _levelOptions[_minLevelScrollController.selectedItem];
final max = _levelOptions[_maxLevelScrollController.selectedItem];
setState(() {
_selectedMinLevel = min;
_selectedMaxLevel = max;
_applyFilters();
});
Navigator.pop(context);
},
child: const Text("确定", style: TextStyle(fontWeight: FontWeight.bold)),
),
],
),
),
const Divider(),
Expanded(
child: Row(
children: [
Expanded(
child: Column(
children: [
const Text("最小定数", style: TextStyle(fontSize: 12, color: Colors.grey)),
Expanded(
child: CupertinoPicker(
scrollController: _minLevelScrollController,
itemExtent: 40,
onSelectedItemChanged: (_) {},
children: _levelOptions.map((e) => Center(child: Text(e.toStringAsFixed(1)))).toList(),
),
),
],
),
),
Expanded(
child: Column(
children: [
const Text("最大定数", style: TextStyle(fontSize: 12, color: Colors.grey)),
Expanded(
child: CupertinoPicker(
scrollController: _maxLevelScrollController,
itemExtent: 40,
onSelectedItemChanged: (_) {},
children: _levelOptions.map((e) => Center(child: Text(e.toStringAsFixed(1)))).toList(),
),
),
],
),
),
],
),
),
],
),
);
},
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text("歌曲列表"),
actions: [
if (_isRefreshing)
const Padding(
padding: EdgeInsets.all(8.0),
child: SizedBox(width: 20, height: 20, child: CircularProgressIndicator(strokeWidth: 2)),
)
else
IconButton(icon: const Icon(Icons.refresh), onPressed: () async {
setState(() => _isRefreshing = true);
await _loadAllSongs();
await _loadUserScores();
setState(() => _isRefreshing = false);
})
],
),
body: Column(
children: [
_buildFilterBar(),
Expanded(child: _buildBody()),
],
),
);
}
Widget _buildFilterBar() {
return Container(
color: Theme.of(context).cardColor,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Padding(
padding: const EdgeInsets.fromLTRB(12, 8, 12, 8),
child: Row(
children: [
Expanded(
child: TextField(
decoration: InputDecoration(
hintText: "搜索歌名/别名/艺术家/ID",
prefixIcon: const Icon(Icons.search),
border: OutlineInputBorder(borderRadius: BorderRadius.circular(25)),
contentPadding: const EdgeInsets.symmetric(vertical: 0, horizontal: 16),
isDense: true,
),
onChanged: (v) {
setState(() => _searchQuery = v);
_applyFilters();
},
),
),
IconButton(
icon: Icon(_isAdvancedFilterExpanded ? Icons.expand_less : Icons.expand_more),
onPressed: () {
setState(() {
_isAdvancedFilterExpanded = !_isAdvancedFilterExpanded;
_isAdvancedFilterExpanded ? _animationController.forward() : _animationController.reverse();
});
},
),
if (_hasFilters())
IconButton(icon: const Icon(Icons.clear, color: Colors.redAccent), onPressed: _resetFilters),
],
),
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
child: Row(
children: [
Expanded(
child: _buildDropdown(
label: "难度类型",
value: _filterLevelType,
items: const [
DropdownMenuItem(value: null, child: Text("全部")),
DropdownMenuItem(value: 0, child: Text("Basic")),
DropdownMenuItem(value: 1, child: Text("Advanced")),
DropdownMenuItem(value: 2, child: Text("Expert")),
DropdownMenuItem(value: 3, child: Text("Master")),
DropdownMenuItem(value: 4, child: Text("Re:Master")),
],
onChanged: (v) {
setState(() => _filterLevelType = v);
_applyFilters();
},
),
),
],
),
),
SizeTransition(
sizeFactor: _animation,
axisAlignment: -1,
child: Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(color: Colors.grey.withAlpha(20), border: Border(top: BorderSide(color: Colors.grey.shade300))),
child: Column(
children: [
Row(
children: [
Expanded(
child: _buildDropdown(
label: "版本",
value: _filterFromVersion,
items: [
const DropdownMenuItem(value: null, child: Text("全部")),
..._availableVersions.map((v) => DropdownMenuItem(value: v, child: Text(v))).toList()
],
onChanged: (v) {
setState(() => _filterFromVersion = v);
_applyFilters();
},
),
),
const SizedBox(width: 8),
Expanded(
child: _buildDropdown(
label: "流派",
value: _filterGenre,
items: [
const DropdownMenuItem(value: null, child: Text("全部")),
..._availableGenres.map((g) => DropdownMenuItem(value: g, child: Text(g))).toList()
],
onChanged: (v) {
setState(() => _filterGenre = v);
_applyFilters();
},
),
),
],
),
const SizedBox(height: 8),
InkWell(
onTap: _showLevelPickerDialog,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 12),
decoration: BoxDecoration(border: Border.all(color: Colors.grey.shade400), borderRadius: BorderRadius.circular(4)),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text("定数范围", style: TextStyle(fontWeight: FontWeight.bold)),
Text(_getLevelRangeText(), style: TextStyle(color: Colors.grey[600])),
const Icon(Icons.arrow_drop_down, color: Colors.grey),
],
),
),
),
],
),
),
),
],
),
);
}
String getLeftEllipsisText(String text, int maxLength) {
if (text.length <= maxLength) return text;
return '...${text.substring(text.length - maxLength + 3)}'; // +3 是因为 "..." 占3个字符
}
Widget _buildDropdown({required String label, required dynamic value, required List<DropdownMenuItem> items, required ValueChanged onChanged}) {
return DropdownButtonFormField(
value: value,
isExpanded: true,
decoration: InputDecoration(labelText: label, border: const OutlineInputBorder(), contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 12), isDense: false),
items: items,
onChanged: onChanged,
);
}
bool _hasFilters() => _searchQuery.isNotEmpty || _filterLevelType != null || _filterFromVersion != null || _filterGenre != null || _selectedMinLevel != null || _selectedMaxLevel != null;
Widget _buildBody() {
if (_isLoading) return const Center(child: CircularProgressIndicator());
if (_errorMessage.isNotEmpty) return Center(child: Text(_errorMessage, style: const TextStyle(color: Colors.red)));
if (_displaySongs.isEmpty) return const Center(child: Text("无匹配歌曲"));
return ListView.builder(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
itemCount: _displaySongs.length,
itemBuilder: (_, idx) {
final song = _displaySongs[idx];
return _songItem(song);
},
);
}
Widget _songItem(SongModel song) {
final cover = _getCoverUrl(song.id);
bool hasScore = false;
// 检查SD/DX/UT任意一个有成绩
if (song.sd != null && _userScoreCache.containsKey(song.id)) hasScore = true;
if (song.dx != null && _userScoreCache.containsKey(10000 + song.id)) hasScore = true;
if (song.ut != null && _userScoreCache.containsKey(100000 + song.id)) hasScore = true;
// --- 新增:处理别名逻辑 ---
// 1. 获取原始列表
List<String> rawAliases = song.albums ?? [];
// 2. 去重 (保留插入顺序可以使用 LinkedHashSet或者简单转为 Set 再转 List)
// 注意Set.from 可能会打乱顺序,如果顺序不重要可以直接用。
// 如果希望保持原顺序去重:
final seen = <String>{};
final uniqueAliases = rawAliases.where((alias) => seen.add(alias)).toList();
// 3. 过滤掉与标题或艺术家完全相同的重复项(可选,为了更整洁)
final filteredAliases = uniqueAliases.where((alias) =>
alias != song.title &&
alias != song.artist
).toList();
// 4. 拼接字符串,如果太长则截断
// 我们设定一个最大显示长度或者最大行数,这里采用“尝试放入尽可能多的词,直到超过一定长度”的策略
String aliasDisplayText = "";
if (filteredAliases.isNotEmpty) {
// 尝试加入前 N 个别名,总长度控制在合理范围,例如 60-80 个字符,或者固定个数如 5-8 个
// 这里采用固定个数策略,因为每个词长度不一,固定个数更容易预测高度
int maxAliasCount = 6;
List<String> displayList = filteredAliases.take(maxAliasCount).toList();
aliasDisplayText = displayList.join(" · ");
// 如果还有更多未显示的,加上省略提示
if (filteredAliases.length > maxAliasCount) {
aliasDisplayText += " ...";
}
}
// -------------------------
return Card(
margin: const EdgeInsets.only(bottom: 10),
elevation: 4,
shadowColor: Colors.purpleAccent.withOpacity(0.5),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
child: InkWell(
onTap: () {
Navigator.push(
context,
MaterialPageRoute(builder: (_) => SongDetailPage(
song: song,
userScoreCache: _userScoreCache,
)),
);
},
borderRadius: BorderRadius.circular(12),
child: Padding(
padding: const EdgeInsets.all(12),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 左侧:封面 + ID 区域
Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
ClipRRect(
borderRadius: BorderRadius.circular(2),
child: Image.network(
cover,
width: 100,
height: 100,
fit: BoxFit.cover,
errorBuilder: (_, __, ___) => Container(
width: 60,
height: 60,
color: Colors.grey[200],
child: const Icon(Icons.music_note),
),
),
),
const SizedBox(height: 2),
// 这一行id + 左侧标签
Row(
mainAxisAlignment: MainAxisAlignment.end,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
// 标签区域cn / jp / m2l / long
if (song.cn == true)
_buildTag(
"CN",
backgroundColor: Colors.redAccent,
shadowColor: Colors.red.withOpacity(0.3),
),
if (song.jp == true)
_buildTag(
"JP",
backgroundColor: Colors.blueAccent,
shadowColor: Colors.red.withOpacity(0.3),
),
if (song.m2l == true)
_buildTag(
"M2L",
gradient: const LinearGradient(
colors: [Colors.purple, Colors.blueAccent],
begin: Alignment.centerLeft,
end: Alignment.centerRight,
),
),
if (song.long == true)
_buildTag(
"LONG",
gradient: const LinearGradient(
colors: [Colors.black12, Colors.lightBlueAccent],
begin: Alignment.topRight,
end: Alignment.bottomLeft,
),
),
// id 文本
Text(
" ${song.id}",
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 11,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
],
),
SizedBox(
width: 100,
child: Text(
getLeftEllipsisText("${song.from}", 15),
style: const TextStyle(
fontSize: 9,
color: Colors.grey,
),
maxLines: 1,
textAlign: TextAlign.right,
overflow: TextOverflow.visible,
),
)
],
),
const SizedBox(width: 12),
// 右侧:详细信息
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
// 关键:让内容在垂直方向上尽量分布,或者使用 MainAxisSize.min 配合 Spacer
mainAxisSize: MainAxisSize.min,
children: [
Text(
song.title,
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 15),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 4),
Text(
song.artist ?? "Unknown",
style: TextStyle(fontSize: 12, color: Colors.grey[600]),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 6),
..._getDifficultyChipsByType(song),
if (aliasDisplayText.isNotEmpty) ...[
const SizedBox(height: 6),
LayoutBuilder(
builder: (context, constraints) {
// 可选根据剩余宽度动态决定显示多少行这里简单处理为最多2行
return Text(
aliasDisplayText,
style: TextStyle(
fontSize: 10,
color: Colors.grey[500],
height: 1.2, // 紧凑行高
),
maxLines: 5,
overflow: TextOverflow.ellipsis,
);
},
),
],
],
),
),
if (hasScore)
Container(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 3),
decoration: BoxDecoration(color: Colors.greenAccent.withOpacity(0.15), borderRadius: BorderRadius.circular(4)),
child: const Text("Score", style: TextStyle(color: Colors.green, fontSize: 12, fontWeight: FontWeight.bold)),
),
],
),
),
),
);
}
Widget _buildTag(
String text, {
// 背景:纯色 或 渐变 二选一
Color? backgroundColor,
Gradient? gradient,
// 阴影配置
Color? shadowColor,
double shadowBlurRadius = 2,
Offset shadowOffset = const Offset(0, 1),
// 可选样式扩展
double borderRadius = 2,
double fontSize = 8,
Color textColor = Colors.white,
}) {
return Container(
margin: const EdgeInsets.only(right: 3),
padding: const EdgeInsets.symmetric(horizontal: 3, vertical: 1),
decoration: BoxDecoration(
// 优先使用渐变,没有渐变则使用纯色
color: gradient == null ? (backgroundColor ?? Colors.blueAccent) : null,
gradient: gradient,
borderRadius: BorderRadius.circular(borderRadius),
// 阴影:只有传入 shadowColor 才显示
boxShadow: shadowColor != null
? [
BoxShadow(
color: shadowColor,
blurRadius: shadowBlurRadius,
offset: shadowOffset,
)
]
: null,
),
child: Text(
text,
style: TextStyle(
color: textColor,
fontSize: fontSize,
fontWeight: FontWeight.bold,
),
),
);
}
List<Widget> _getDifficultyChipsByType(SongModel song) {
final diffs = _getAllDifficultiesWithType(song);
// 按 type 分组SD / DX / UT
final Map<String, List<dynamic>> typeGroups = {};
for (var item in diffs) {
final type = item['type'] as String;
typeGroups.putIfAbsent(type, () => []);
typeGroups[type]!.add(item);
}
// 固定顺序SD → DX → UT
final order = ['SD', 'DX', 'UT'];
List<Widget> rows = [];
for (final type in order) {
final items = typeGroups[type];
if (items == null || items.isEmpty) continue;
// 每一个类型 = 一行 Wrap
final row = Wrap(
spacing: 6,
runSpacing: 4,
children: items.map<Widget>((item) {
final diff = item['diff'] as Map;
final lid = diff['level_id'] as int;
final lvValue = double.tryParse(diff['level_value']?.toString() ?? '') ?? 0;
bool isBanquet = lvValue == 10.0 || song.id > 100000 || type == 'UT';
int realId = _getRealMusicId(song, type);
bool hasScore = _userScoreCache[realId]?[lid] != null;
Color color = Colors.grey;
String label = "";
if (isBanquet) {
color = Colors.pinkAccent;
// 多UT按ID显示对应名称
if (type == 'UT') {
final utTitleMap = song.utTitle as Map?;
if (utTitleMap != null && utTitleMap.isNotEmpty) {
final key = item["diff"]['id'].toString();
if (utTitleMap.containsKey(key)) {
label = utTitleMap[key].toString();
} else {
label = "UT";
}
} else {
label = "UT";
}
} else {
label = type;
}
} else {
switch (lid) {
case 0:
color = Colors.green;
label = type;
break;
case 1:
color = Colors.yellow.shade700;
label = type;
break;
case 2:
color = Colors.red;
label = type;
break;
case 3:
color = Colors.purple;
label = type;
break;
case 4:
color = Colors.purpleAccent.shade100;
label = type;
break;
default:
return const SizedBox();
}
}
return Container(
padding: const EdgeInsets.symmetric(horizontal: 5, vertical: 2),
decoration: BoxDecoration(
color: color.withOpacity(hasScore ? 0.25 : 0.1),
borderRadius: BorderRadius.circular(4),
border: Border.all(color: color.withOpacity(0.5)),
),
child: Text(
isBanquet ? label : "$label ${lvValue.toStringAsFixed(1)}",
style: TextStyle(
color: color,
fontSize: 10,
fontWeight: FontWeight.bold,
),
),
);
}).toList(),
);
rows.add(row);
rows.add(const SizedBox(height: 4));
}
return rows;
}
String _getCoverUrl(int musicId) {
int displayId = musicId % 10000;
if (musicId >= 16000 && musicId <= 20000) {
String idStr = displayId.toString().padLeft(6, '0');
return "https://u.mai2.link/jacket/UI_Jacket_$idStr.jpg";
} else {
return "https://cdn.godserver.cn/resource/static/mai/cover/$displayId.png";
}
}
}