0419 0318

更新4
This commit is contained in:
spasolreisa
2026-04-19 03:18:42 +08:00
parent d4bbc424c6
commit 74e47971ca
25 changed files with 1221 additions and 371 deletions

View File

@@ -1,10 +1,15 @@
import 'dart:io';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'package:path_provider/path_provider.dart';
import 'package:open_file/open_file.dart'; // 引入打开文件的包
import '../../../model/song_model.dart';
import '../../../providers/user_provider.dart';
import '../../../service/song_service.dart';
import '../../../service/user_service.dart';
import '../../../widgets/score_progress_chart.dart';
import '../../tool/gradientText.dart';
class SongDetailPage extends StatefulWidget {
final SongModel song;
@@ -22,8 +27,11 @@ class SongDetailPage extends StatefulWidget {
class _SongDetailPageState extends State<SongDetailPage> {
String? _selectedType;
bool _isAliasExpanded = false;
// 用于跟踪下载状态key为 "type_levelId"
final Map<String, bool> _isDownloading = {};
// 缓存图表数据: Key为 "SD" / "DX" / "UT_realId"
final Map<String, List<Map<String, dynamic>>> _chartDataCache = {};
final Map<String, bool> _isLoadingChart = {};
@@ -74,9 +82,90 @@ class _SongDetailPageState extends State<SongDetailPage> {
return all;
}
// 核心SD/DX 只加载一次UT 每个谱面加载一次
// ===================== 【新增】下载并打开 ADX 谱面功能 =====================
Future<void> _downloadAndOpenAdx(String type, Map diff) async {
int levelId = diff['level_id'] ?? 0;
String downloadKey = "${type}_$levelId";
// 防止重复点击
if (_isDownloading[downloadKey] == true) return;
setState(() {
_isDownloading[downloadKey] = true;
});
try {
// 1. 构建 URL
int songId = widget.song.id;
String url;
// UT 谱面通常没有标准的 CDN adx 下载地址,这里只处理 SD 和 DX
if (type == 'UT') {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text("UT 宴会谱暂不支持直接下载 ADX 文件")),
);
setState(() => _isDownloading[downloadKey] = false);
return;
}
if (type == 'SD') {
// SD: cdn.godserver.cn/resource/static/adx/00000/{songId}.adx
String idStr = songId.toString().padLeft(5, '0');
url = "https://cdn.godserver.cn/resource/static/adx/$idStr.adx";
} else {
// DX: cdn.godserver.cn/resource/static/adx/00000/{songId+10000}.adx
int dxId = songId + 10000;
String idStr = dxId.toString().padLeft(5, '0');
url = "https://cdn.godserver.cn/resource/static/adx/$idStr.adx";
}
// 2. 下载文件
final response = await http.get(Uri.parse(url));
if (response.statusCode != 200) {
throw Exception("下载失败: HTTP ${response.statusCode}");
}
// 3. 获取临时目录并保存文件
final directory = await getApplicationDocumentsDirectory();
// 注意iOS 上 getApplicationDocumentsDirectory 是持久化的。
// 如果希望每次下载都清理,可以使用 getTemporaryDirectory()。
// 这里为了稳定性,使用 DocumentsDirectory但文件名保持唯一性。
// 生成文件名: SongTitle_Type_Level.adx
String safeTitle = widget.song.title?.replaceAll(RegExp(r'[^\w\s\u4e00-\u9fa5]'), '_') ?? "song";
String fileName = "${safeTitle}_${type}_Lv${levelId}.adx";
// 确保文件名不冲突,可以加时间戳或者随机数,这里简单处理
final filePath = "${directory.path}/$fileName";
final file = File(filePath);
await file.writeAsBytes(response.bodyBytes);
// 4. 调用系统原生打开
// open_file 会尝试寻找能打开 .adx 的应用
final result = await OpenFile.open(filePath);
if (result.type != ResultType.done) {
// 如果没有安装能打开 adx 的应用,提示用户
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text("无法打开文件: ${result.message},请确保已安装谱面编辑器")),
);
}
} catch (e) {
print("Download error: $e");
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text("下载出错: $e")),
);
} finally {
setState(() {
_isDownloading[downloadKey] = false;
});
}
}
Future<void> _loadTypeChartData(String type) async {
// 已经加载/正在加载 → 直接返回
if (_chartDataCache.containsKey(type) || _isLoadingChart[type] == true) {
return;
}
@@ -94,7 +183,6 @@ class _SongDetailPageState extends State<SongDetailPage> {
return;
}
// 获取当前类型的 API ID
int apiMusicId;
if (type == 'SD') {
apiMusicId = widget.song.id;
@@ -105,11 +193,9 @@ class _SongDetailPageState extends State<SongDetailPage> {
return;
}
// 调用一次 API拿到全部难度数据
final logs = await UserService.getChartLog(token, [apiMusicId]);
logs.sort((a, b) => a.time.compareTo(b.time));
// 转换数据
final chartList = logs.map((log) => {
'achievement': log.segaChartNew?.achievement ?? 0,
'time': log.time,
@@ -118,7 +204,7 @@ class _SongDetailPageState extends State<SongDetailPage> {
'syncStatus': log.segaChartNew?.syncStatus ?? 0,
'deluxscoreMax': log.segaChartNew?.deluxscoreMax ?? 0,
'playCount': log.segaChartNew?.playCount ?? 0,
'level': log.segaChartNew?.level ?? 0, // 保存难度等级用于过滤
'level': log.segaChartNew?.level ?? 0,
}).toList();
setState(() {
@@ -131,7 +217,6 @@ class _SongDetailPageState extends State<SongDetailPage> {
}
}
// UT 单独加载
Future<void> _loadUtChartData(String cacheKey, int realId) async {
if (_chartDataCache.containsKey(cacheKey) || _isLoadingChart[cacheKey] == true) {
return;
@@ -170,6 +255,73 @@ class _SongDetailPageState extends State<SongDetailPage> {
}
}
Widget _buildAliasSection(List<String> rawAliases) {
final uniqueAliases = rawAliases
.where((e) => e != null && e.trim().isNotEmpty)
.map((e) => e.trim())
.toSet()
.toList();
if (uniqueAliases.isEmpty) return const SizedBox.shrink();
final displayLimit = _isAliasExpanded ? uniqueAliases.length : 3;
final displayedAliases = uniqueAliases.take(displayLimit).toList();
final hasMore = uniqueAliases.length > 3;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Wrap(
spacing: 6.0,
runSpacing: 4.0,
children: [
...displayedAliases.map((alias) => Container(
decoration: BoxDecoration(
color: Colors.grey.shade100,
borderRadius: BorderRadius.circular(4),
border: Border.all(color: Colors.grey.shade300, width: 0.5),
),
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
child: Text(
alias,
style: TextStyle(
fontSize: 11,
color: Colors.grey[700],
fontStyle: FontStyle.italic,
),
),
)).toList(),
],
),
if (hasMore)
InkWell(
onTap: () {
setState(() {
_isAliasExpanded = !_isAliasExpanded;
});
},
child: Padding(
padding: const EdgeInsets.only(top: 4),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(
_isAliasExpanded ? "收起" : "查看更多 (${uniqueAliases.length - 3})",
style: TextStyle(fontSize: 11),
),
Icon(
_isAliasExpanded ? Icons.keyboard_arrow_up : Icons.keyboard_arrow_down,
size: 14,
color: Theme.of(context).primaryColor,
),
],
),
),
),
],
);
}
@override
Widget build(BuildContext context) {
final coverUrl = _getCoverUrl(widget.song.id);
@@ -177,129 +329,133 @@ class _SongDetailPageState extends State<SongDetailPage> {
final availableTypes = _getAvailableTypes();
return Scaffold(
appBar: AppBar(title: Text(widget.song.title ?? "歌曲详情")),
appBar: AppBar(
title: Text(widget.song.title ?? "歌曲详情"),
elevation: 2,
),
body: SingleChildScrollView(
padding: const EdgeInsets.all(16),
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 歌曲封面 + 信息
Row(
crossAxisAlignment: CrossAxisAlignment.start,
// 修复:这里缺少了括号
children: [
ClipRRect(
borderRadius: BorderRadius.circular(12),
child: Image.network(
coverUrl,
width: 120,
height: 120,
fit: BoxFit.cover,
errorBuilder: (_, __, ___) => Container(
width: 120,
height: 120,
color: Colors.grey[200],
child: const Icon(Icons.music_note, size: 40),
),
),
),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"[${widget.song.id}] ${widget.song.title}",
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
Card(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
elevation: 4,
shadowColor: Colors.purpleAccent.withOpacity(0.2),
child: Padding(
padding: const EdgeInsets.all(14),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
ClipRRect(
borderRadius: BorderRadius.circular(10),
child: Image.network(
coverUrl,
width: 90,
height: 90,
fit: BoxFit.cover,
errorBuilder: (_, __, ___) => Container(
width: 90,
height: 90,
color: Colors.grey[200],
child: const Icon(Icons.music_note, size: 36),
),
),
const SizedBox(height: 4),
Text(
"艺术家:${widget.song.artist ?? '未知'}",
style: const TextStyle(fontSize: 14),
),
Text(
"流派:${widget.song.genre} | 版本:${widget.song.from}",
style: const TextStyle(fontSize: 13, color: Colors.grey),
),
Text(
"BPM${widget.song.bpm ?? '未知'}",
style: const TextStyle(fontSize: 13, color: Colors.grey),
),
],
),
),
],
),
const SizedBox(height: 24),
// 难度选择器
if (availableTypes.length > 1) ...[
Row(
children: [
const Text(
"难度详情",
style: TextStyle(
fontSize: 17,
fontWeight: FontWeight.bold,
),
),
const SizedBox(width: 10),
Expanded(
child: CupertinoSlidingSegmentedControl<String>(
groupValue: _selectedType,
backgroundColor: Colors.grey.shade200,
thumbColor: Theme.of(context).primaryColor.withOpacity(0.8),
children: {
for (var type in availableTypes)
type: Padding(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
child: Text(
type,
style: TextStyle(
color: _selectedType == type ? Colors.white : Colors.black87,
fontWeight: FontWeight.bold,
),
const SizedBox(width: 14),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"[${widget.song.id}] ${widget.song.title}",
style: const TextStyle(
fontSize: 17,
fontWeight: FontWeight.bold,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
},
onValueChanged: (value) {
if (value != null) {
setState(() {
_selectedType = value;
});
}
},
const SizedBox(height: 6),
Text(
"艺术家:${widget.song.artist ?? '未知'}",
style: const TextStyle(fontSize: 13),
),
const SizedBox(height: 4),
if (widget.song.albums?.isNotEmpty == true)
_buildAliasSection(widget.song.albums!),
const SizedBox(height: 6),
Text(
"${widget.song.genre ?? ''} | ${widget.song.from ?? ''}",
style: TextStyle(fontSize: 12, color: Colors.grey[600]),
),
const SizedBox(height: 2),
Text(
"BPM${widget.song.bpm ?? '未知'}",
style: TextStyle(fontSize: 12, color: Colors.grey[600]),
),
],
),
),
),
],
],
),
),
] else ...[
const Text(
"难度详情",
style: TextStyle(fontSize: 17, fontWeight: FontWeight.bold),
),
],
const SizedBox(height: 12),
),
const SizedBox(height: 20),
if (availableTypes.isNotEmpty) ...[
Padding(
padding: const EdgeInsets.symmetric(horizontal: 4),
child: Row(
children: [
const Text(
"难度详情",
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
),
const SizedBox(width: 12),
if (availableTypes.length > 1)
Expanded(
child: CupertinoSlidingSegmentedControl<String>(
groupValue: _selectedType,
backgroundColor: Colors.grey.shade200,
thumbColor: Theme.of(context).primaryColor,
children: {
for (var type in availableTypes)
type: Padding(
padding: const EdgeInsets.symmetric(vertical: 6, horizontal: 8),
child: Text(
type,
style: TextStyle(
color: _selectedType == type ? Colors.white : Colors.black87,
fontWeight: FontWeight.w600,
),
),
),
},
onValueChanged: (val) {
if (val != null) setState(() => _selectedType = val);
},
),
),
],
),
),
const SizedBox(height: 12),
],
// 难度列表
if (diffs.isEmpty)
Center(
const Center(
child: Padding(
padding: const EdgeInsets.all(20.0),
child: Text(
"暂无 ${_selectedType ?? ''} 谱面数据",
style: const TextStyle(color: Colors.grey),
),
padding: EdgeInsets.all(30),
child: Text("暂无谱面数据", style: TextStyle(color: Colors.grey)),
),
)
else
...diffs.map((item) => _diffItem(
type: item['type'],
diff: item['diff'],
)).toList(),
)),
const SizedBox(height: 30),
],
@@ -310,8 +466,7 @@ class _SongDetailPageState extends State<SongDetailPage> {
Widget _diffItem({required String type, required Map diff}) {
int levelId = diff['level_id'] ?? 0;
final double lvValue =
double.tryParse(diff['level_value']?.toString() ?? '') ?? 0;
final double lvValue = double.tryParse(diff['level_value']?.toString() ?? '') ?? 0;
final designer = diff['note_designer'] ?? "-";
final notes = diff['notes'] ?? <String, dynamic>{};
final total = notes['total'] ?? 0;
@@ -323,9 +478,7 @@ class _SongDetailPageState extends State<SongDetailPage> {
int realId;
String? utTitleName;
String cacheKey = "";
bool hasScoreData = false;
// 加载逻辑SD/DX 只加载一次UT 每个加载一次
if (type == 'UT') {
realId = (diff['id'] as num?)?.toInt() ?? 0;
final utTitleMap = widget.song.utTitle as Map?;
@@ -339,31 +492,26 @@ class _SongDetailPageState extends State<SongDetailPage> {
});
} else {
realId = _getRealMusicId(type);
cacheKey = type; // SD/DX 用类型做缓存 key
cacheKey = type;
WidgetsBinding.instance.addPostFrameCallback((_) {
_loadTypeChartData(type);
});
}
// 图表数据SD/DX 自动过滤当前难度UT 直接使用
List<Map<String, dynamic>> chartHistory = [];
if (_chartDataCache.containsKey(cacheKey)) {
if (type == 'UT') {
chartHistory = _chartDataCache[cacheKey]!;
} else {
// SD/DX 从全量数据中过滤当前难度
chartHistory = _chartDataCache[cacheKey]!
.where((e) => e['level'] == levelId)
.toList();
}
}
hasScoreData = chartHistory.isNotEmpty;
// 成绩信息
bool hasScoreData = chartHistory.isNotEmpty;
final score = widget.userScoreCache[realId]?[levelId];
bool hasUserScore = score != null;
// 显示名称与颜色
String name = "";
Color color = Colors.grey;
bool isBanquet = type == 'UT';
@@ -382,11 +530,23 @@ class _SongDetailPageState extends State<SongDetailPage> {
}
}
final rating990 = _calculateRating(lvValue, 99.0);
final rating995 = _calculateRating(lvValue, 99.5);
final rating1000 = _calculateRating(lvValue, 100.0);
final rating1003 = _calculateRating(lvValue, 100.3);
final rating1005 = _calculateRating(lvValue, 100.5);
// 下载状态的 Key
String downloadKey = "${type}_$levelId";
bool isDownloading = _isDownloading[downloadKey] ?? false;
return Card(
margin: const EdgeInsets.only(bottom: 10),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
margin: const EdgeInsets.symmetric(horizontal: 4, vertical: 8),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14)),
elevation: 5,
shadowColor: color.withOpacity(0.2),
child: Padding(
padding: const EdgeInsets.all(14),
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
@@ -394,11 +554,11 @@ class _SongDetailPageState extends State<SongDetailPage> {
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
decoration: BoxDecoration(
color: color.withOpacity(0.15),
borderRadius: BorderRadius.circular(6),
border: Border.all(color: color.withOpacity(0.3)),
borderRadius: BorderRadius.circular(8),
border: Border.all(color: color.withOpacity(0.4), width: 1.2),
),
child: Text(
isBanquet ? name : "$name (${lvValue.toStringAsFixed(1)})",
@@ -409,20 +569,116 @@ class _SongDetailPageState extends State<SongDetailPage> {
),
),
),
if (hasUserScore)
const Icon(Icons.star, color: Colors.amber, size: 20),
Row(
children: [
if (hasUserScore)
const Icon(Icons.star_rounded, color: Colors.amber, size: 22),
// ===================== 【新增】下载/预览按钮 =====================
const SizedBox(width: 8),
InkWell(
onTap: isDownloading || type == 'UT' ? null : () {
_downloadAndOpenAdx(type, diff);
},
borderRadius: BorderRadius.circular(20),
child: Container(
padding: const EdgeInsets.all(6),
decoration: BoxDecoration(
color: type == 'UT' ? Colors.grey.shade300 : Theme.of(context).primaryColor.withOpacity(0.1),
shape: BoxShape.circle,
),
child: isDownloading
? SizedBox(
width: 16,
height: 16,
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor: AlwaysStoppedAnimation<Color>(color),
),
)
: Icon(
Icons.download_rounded,
size: 18,
color: type == 'UT' ? Colors.grey : color,
),
),
),
],
),
],
),
const SizedBox(height: 10),
Text("谱师:$designer", style: const TextStyle(fontSize: 14)),
const SizedBox(height: 8),
Text("谱师:$designer", style: const TextStyle(fontSize: 13)),
const SizedBox(height: 4),
Text(
"物量:$total | TAP:$tap HOLD:$hold SLIDE:$slide BRK:$brk",
style: const TextStyle(color: Colors.grey, fontSize: 12),
Table(
columnWidths: const {
0: FixedColumnWidth(40),
1: FixedColumnWidth(40),
2: FixedColumnWidth(40),
3: FixedColumnWidth(40),
4: FixedColumnWidth(40),
},
children: [
TableRow(
children: [
_tableCell("总物量", color),
_tableCell("TAP", color),
_tableCell("HOLD", color),
_tableCell("SLIDE", color),
_tableCell("BRK", color),
],
),
TableRow(
children: [
_tableCell(total.toString(), color, isHeader: false),
_tableCell(tap.toString(), color, isHeader: false),
_tableCell(hold.toString(), color, isHeader: false),
_tableCell(slide.toString(), color, isHeader: false),
_tableCell(brk.toString(), color, isHeader: false),
],
),
],
),
const SizedBox(height: 10),
Table(
columnWidths: const {
0: FixedColumnWidth(50),
1: FixedColumnWidth(50),
2: FixedColumnWidth(50),
3: FixedColumnWidth(50),
4: FixedColumnWidth(50),
5: FixedColumnWidth(50),
},
children: [
TableRow(
children: [
_tableCell("完成度", color),
_tableCell("99.0%", color),
_tableCell("99.5%", color),
_tableCell("100.0%", color),
_tableCell("100.3%", color),
_tableCell("100.5%", color),
_tableCell("", color),
],
),
TableRow(
children: [
_tableCell("Rating", color, isHeader: false),
_tableCell(rating990.toStringAsFixed(2), color, isHeader: false),
_tableCell(rating995.toStringAsFixed(2), color, isHeader: false),
_tableCell(rating1000.toStringAsFixed(2), color, isHeader: false),
_tableCell(rating1003.toStringAsFixed(2), color, isHeader: false),
_tableCell(rating1005.toStringAsFixed(2), color, isHeader: false),
_tableCell("", color, isHeader: false),
],
),
],
),
const SizedBox(height: 12),
// ✅ 修复:没有成绩直接显示文字,绝不显示加载圈
const SizedBox(height: 14),
if (hasScoreData)
Column(
crossAxisAlignment: CrossAxisAlignment.start,
@@ -430,30 +686,32 @@ class _SongDetailPageState extends State<SongDetailPage> {
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text("推分进程", style: TextStyle(fontSize: 12, color: Colors.grey)),
const Text("推分进程", style: TextStyle(fontSize: 13, color: Colors.grey)),
Text(
"最近: ${(chartHistory.last['achievement'] / 10000.0).toStringAsFixed(4)}%",
style: TextStyle(fontSize: 12, color: color),
style: TextStyle(fontSize: 13, color: color, fontWeight: FontWeight.w600),
),
],
),
const SizedBox(height: 4),
const SizedBox(height: 8),
ScoreProgressChart(
historyScores: chartHistory,
lineColor: color,
fillColor: color,
),
const SizedBox(height: 5),
],
)
else
const Text("暂无历史记录", style: TextStyle(fontSize: 12, color: Colors.grey)),
const Padding(
padding: EdgeInsets.symmetric(vertical: 8),
child: Text("暂无历史记录", style: TextStyle(fontSize: 13, color: Colors.grey)),
),
if (hasUserScore) ...[
const SizedBox(height: 10),
const Divider(height: 1),
const SizedBox(height: 6),
_buildScoreInfo(score!),
const SizedBox(height: 12),
Divider(height: 1, color: Colors.grey[300]),
const SizedBox(height: 12),
_buildScoreInfo(score!,diff),
],
],
),
@@ -461,41 +719,212 @@ class _SongDetailPageState extends State<SongDetailPage> {
);
}
Widget _buildScoreInfo(Map score) {
Widget _tableCell(String text, Color color, {bool isHeader = true}) {
return Center(
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 4),
child: Text(
text,
style: TextStyle(
fontSize: isHeader ? 11 : 12,
color: isHeader ? color : color.withOpacity(0.99),
fontWeight: isHeader ? FontWeight.bold : FontWeight.w500,
),
),
),
);
}
static int _calculateRating(double diff, double achievementPercent) {
double sys = 22.4;
double ach = achievementPercent;
if (ach >= 100.5000) {
return (diff * 22.512).floor();
}
if (ach == 100.4999) {
sys = 22.2;
} else if (ach >= 100.0000) {
sys = 21.6;
} else if (ach == 99.9999) {
sys = 21.4;
} else if (ach >= 99.5000) {
sys = 21.1;
} else if (ach >= 99.0000) {
sys = 20.8;
} else if (ach >= 98.0000) {
sys = 20.3;
} else if (ach >= 97.0000) {
sys = 20.0;
} else if (ach >= 94.0000) {
sys = 16.8;
} else if (ach >= 90.0000) {
sys = 15.2;
} else if (ach >= 80.0000) {
sys = 13.6;
} else if (ach >= 75.0000) {
sys = 12.0;
} else if (ach >= 70.0000) {
sys = 11.2;
} else if (ach >= 60.0000) {
sys = 9.6;
} else if (ach >= 50.0000) {
sys = 8.0;
} else {
sys = 0.0;
}
if (sys == 0.0) return 0;
return (diff * sys * ach / 100).floor();
}
Widget _buildScoreInfo(Map<String, dynamic> score, Map diff) {
final ach = (score['achievement'] ?? 0) / 10000;
final rank = _getRankText(score['scoreRank'] ?? 0);
final combo = _comboText(score['comboStatus'] ?? 0);
final sync = _syncText(score['syncStatus'] ?? 0);
final dxScore = score['deluxscoreMax'] ?? 0;
final playCount = score['playCount'] ?? 0;
final rating = score['rating'] ?? 0;
return Column(
int comboStatus = score['comboStatus'] ?? 0;
int syncStatus = score['syncStatus'] ?? 0;
Color achColor = _getColorByAchievement(ach);
Color rankColor = _getColorByRank(rank);
int totalNotes = diff['notes']['total'] ?? 0;
int allDx = totalNotes * 3;
double perc = allDx > 0 ? dxScore / allDx : 0.0;
String? comboIconPath;
switch (comboStatus) {
case 1: comboIconPath = "images/UI_MSS_MBase_Icon_FC.png"; break;
case 2: comboIconPath = "images/UI_MSS_MBase_Icon_FCp.png"; break;
case 3: comboIconPath = "images/UI_MSS_MBase_Icon_AP.png"; break;
case 4: comboIconPath = "images/UI_MSS_MBase_Icon_APp.png"; break;
}
String? syncIconPath;
switch (syncStatus) {
case 1: syncIconPath = "images/UI_MSS_MBase_Icon_FS.png"; break;
case 2: syncIconPath = "images/UI_MSS_MBase_Icon_FSp.png"; break;
case 3: syncIconPath = "images/UI_MSS_MBase_Icon_FSD.png"; break;
case 4: syncIconPath = "images/UI_MSS_MBase_Icon_FSDp.png"; break;
case 5: syncIconPath = "images/UI_MSS_MBase_Icon_Sync.png"; break;
}
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text("你的成绩:",
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 13)),
const SizedBox(height: 4),
Row(
children: [
Text("达成率:${ach.toStringAsFixed(4)}%",
style: TextStyle(
color: _getColorByAchievement(ach),
fontWeight: FontWeight.bold)),
const Spacer(),
Text("评级:$rank",
style: TextStyle(
color: _getColorByRank(rank), fontWeight: FontWeight.bold)),
],
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Row(
children: [
Icon(Icons.emoji_events_rounded, color: Colors.amber, size: 18),
SizedBox(width: 4),
Text(
"你的成绩",
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 15,
height: 1.1,
),
),
],
),
const SizedBox(height: 10),
Row(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
GradientText(
data: "${ach.toStringAsFixed(4)}%",
style: const TextStyle(fontSize: 30, fontWeight: FontWeight.bold),
gradientLayers: [
GradientLayer(
gradient: const LinearGradient(
colors: [Colors.pinkAccent, Colors.blue],
),
blendMode: BlendMode.srcIn,
),
],
),
const SizedBox(width: 8),
_buildTag(rank, rankColor),
if (comboIconPath != null)
Padding(
padding: const EdgeInsets.only(left: 2),
child: Image.asset(comboIconPath, width: 40, height: 40),
),
if (syncIconPath != null)
Padding(
padding: const EdgeInsets.only(left: 4),
child: Image.asset(syncIconPath, width: 40, height: 40),
),
],
),
const SizedBox(height: 12),
Row(
children: [
const Icon(Icons.score_rounded, size: 14, color: Colors.blueGrey),
const SizedBox(width: 4),
Text("DX$dxScore / $allDx", style: TextStyle(fontSize: 13, color: Colors.grey[700])),
const SizedBox(width: 10),
if (perc >= 0.85)
Padding(
padding: const EdgeInsets.only(right: 4),
child: _getDxStarIcon(perc),
),
const Spacer(),
Icon(Icons.star_rate_rounded, size: 14, color: Colors.amber),
const SizedBox(width: 3),
Text("Rating$rating", style: TextStyle(fontSize: 12, color: Colors.grey[700], fontWeight: FontWeight.w500)),
const SizedBox(width: 10),
Icon(Icons.play_circle_outline_rounded, size: 14, color: Colors.grey[600]),
const SizedBox(width: 3),
Text("$playCount", style: TextStyle(fontSize: 12, color: Colors.grey[600])),
],
),
],
),
),
const SizedBox(height: 2),
Text("Combo$combo | Sync$sync",
style: const TextStyle(fontSize: 12, color: Colors.blueGrey)),
Text("DX分数$dxScore | 游玩次数:$playCount",
style: const TextStyle(fontSize: 12, color: Colors.grey)),
],
);
}
Widget _getDxStarIcon(double perc) {
String assetPath;
if (perc >= 0.97) {
assetPath = "images/UI_GAM_Gauge_DXScoreIcon_05.png";
} else if (perc >= 0.95) {
assetPath = "images/UI_GAM_Gauge_DXScoreIcon_04.png";
} else if (perc >= 0.93) {
assetPath = "images/UI_GAM_Gauge_DXScoreIcon_03.png";
} else if (perc >= 0.90) {
assetPath = "images/UI_GAM_Gauge_DXScoreIcon_02.png";
} else if (perc >= 0.85) {
assetPath = "images/UI_GAM_Gauge_DXScoreIcon_01.png";
} else {
return const SizedBox();
}
return Image.asset(assetPath, width: 30, height: 30, fit: BoxFit.contain);
}
Widget _buildTag(String text, Color color) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5),
decoration: BoxDecoration(
color: color.withOpacity(0.15),
borderRadius: BorderRadius.circular(8),
border: Border.all(color: color.withOpacity(0.5), width: 1.2),
),
child: Text(
text,
style: TextStyle(color: color, fontWeight: FontWeight.bold, fontSize: 14),
),
);
}
Color _getColorByAchievement(double ach) {
if (ach >= 100.5) return const Color(0xFFD4AF37);
if (ach >= 100.0) return Colors.purple;
@@ -507,9 +936,10 @@ class _SongDetailPageState extends State<SongDetailPage> {
}
Color _getColorByRank(String rank) {
if (rank.contains("SSS+")) return const Color(0xFFD4AF37);
if (rank.contains("SSS")) return Colors.purple;
if (rank.contains("SS")) return Colors.deepPurple;
if (rank.contains("S")) return Colors.blue;
if (rank.contains("SS+") || rank.contains("SS")) return Colors.deepPurple;
if (rank.contains("S+") || rank.contains("S")) return Colors.blue;
return Colors.green;
}
@@ -542,26 +972,6 @@ class _SongDetailPageState extends State<SongDetailPage> {
}
}
String _comboText(int s) {
switch (s) {
case 1: return "FC";
case 2: return "FC+";
case 3: return "AP";
case 4: return "AP+";
default: return "";
}
}
String _syncText(int s) {
switch (s) {
case 1: return "FS";
case 2: return "FS+";
case 3: return "FDX";
case 4: return "FDX+";
default: return "";
}
}
int _getRealMusicId(String type) {
if (type == "SD") return widget.song.id;
if (type == "DX") return 10000 + widget.song.id;