diff --git a/images/UI_GAM_Gauge_DXScoreIcon_01.png b/images/UI_GAM_Gauge_DXScoreIcon_01.png new file mode 100644 index 0000000..8632773 Binary files /dev/null and b/images/UI_GAM_Gauge_DXScoreIcon_01.png differ diff --git a/images/UI_GAM_Gauge_DXScoreIcon_02.png b/images/UI_GAM_Gauge_DXScoreIcon_02.png new file mode 100644 index 0000000..e2ba3c6 Binary files /dev/null and b/images/UI_GAM_Gauge_DXScoreIcon_02.png differ diff --git a/images/UI_GAM_Gauge_DXScoreIcon_03.png b/images/UI_GAM_Gauge_DXScoreIcon_03.png new file mode 100644 index 0000000..274b0b6 Binary files /dev/null and b/images/UI_GAM_Gauge_DXScoreIcon_03.png differ diff --git a/images/UI_GAM_Gauge_DXScoreIcon_04.png b/images/UI_GAM_Gauge_DXScoreIcon_04.png new file mode 100644 index 0000000..f1adfbe Binary files /dev/null and b/images/UI_GAM_Gauge_DXScoreIcon_04.png differ diff --git a/images/UI_GAM_Gauge_DXScoreIcon_05.png b/images/UI_GAM_Gauge_DXScoreIcon_05.png new file mode 100644 index 0000000..28c6d2b Binary files /dev/null and b/images/UI_GAM_Gauge_DXScoreIcon_05.png differ diff --git a/images/UI_MSS_MBase_Icon_AP.png b/images/UI_MSS_MBase_Icon_AP.png new file mode 100644 index 0000000..fb9db0e Binary files /dev/null and b/images/UI_MSS_MBase_Icon_AP.png differ diff --git a/images/UI_MSS_MBase_Icon_APp.png b/images/UI_MSS_MBase_Icon_APp.png new file mode 100644 index 0000000..451b266 Binary files /dev/null and b/images/UI_MSS_MBase_Icon_APp.png differ diff --git a/images/UI_MSS_MBase_Icon_FC.png b/images/UI_MSS_MBase_Icon_FC.png new file mode 100644 index 0000000..79780a5 Binary files /dev/null and b/images/UI_MSS_MBase_Icon_FC.png differ diff --git a/images/UI_MSS_MBase_Icon_FCp.png b/images/UI_MSS_MBase_Icon_FCp.png new file mode 100644 index 0000000..34d6eba Binary files /dev/null and b/images/UI_MSS_MBase_Icon_FCp.png differ diff --git a/images/UI_MSS_MBase_Icon_FS.png b/images/UI_MSS_MBase_Icon_FS.png new file mode 100644 index 0000000..dd36793 Binary files /dev/null and b/images/UI_MSS_MBase_Icon_FS.png differ diff --git a/images/UI_MSS_MBase_Icon_FSD.png b/images/UI_MSS_MBase_Icon_FSD.png new file mode 100644 index 0000000..cf43898 Binary files /dev/null and b/images/UI_MSS_MBase_Icon_FSD.png differ diff --git a/images/UI_MSS_MBase_Icon_FSDp.png b/images/UI_MSS_MBase_Icon_FSDp.png new file mode 100644 index 0000000..c82b70a Binary files /dev/null and b/images/UI_MSS_MBase_Icon_FSDp.png differ diff --git a/images/UI_MSS_MBase_Icon_FSp.png b/images/UI_MSS_MBase_Icon_FSp.png new file mode 100644 index 0000000..a427a44 Binary files /dev/null and b/images/UI_MSS_MBase_Icon_FSp.png differ diff --git a/images/UI_MSS_MBase_Icon_Sync.png b/images/UI_MSS_MBase_Icon_Sync.png new file mode 100644 index 0000000..c0badc5 Binary files /dev/null and b/images/UI_MSS_MBase_Icon_Sync.png differ diff --git a/lib/pages/home/home_page.dart b/lib/pages/home/home_page.dart index c490e4e..5220c23 100644 --- a/lib/pages/home/home_page.dart +++ b/lib/pages/home/home_page.dart @@ -592,7 +592,7 @@ class _RecommendedSongsSection extends StatelessWidget { } return SizedBox( - height: 200, + height: 220, child: ListView.builder( scrollDirection: Axis.horizontal, itemCount: songs.length, @@ -664,7 +664,7 @@ class _SongItemCard extends StatelessWidget { ClipRRect( borderRadius: const BorderRadius.vertical(top: Radius.circular(12)), child: SizedBox( - height: 120, + height: 140, width: double.infinity, child: Image.network( _getCoverUrl(song.id), diff --git a/lib/pages/music/music_page.dart b/lib/pages/music/music_page.dart index 48fb80c..1ce08ea 100644 --- a/lib/pages/music/music_page.dart +++ b/lib/pages/music/music_page.dart @@ -24,8 +24,11 @@ class _MusicPageState extends State with SingleTickerProviderStateMix // 筛选 String _searchQuery = ''; int? _filterLevelType; - String? _filterFromVersion; - String? _filterGenre; + + // --- 修改点 1: 将单选 String? 改为多选 Set --- + Set _filterVersions = {}; + Set _filterGenres = {}; + double? _selectedMinLevel; double? _selectedMaxLevel; bool _isAdvancedFilterExpanded = false; @@ -99,6 +102,8 @@ class _MusicPageState extends State with SingleTickerProviderStateMix _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(); @@ -110,7 +115,7 @@ class _MusicPageState extends State with SingleTickerProviderStateMix } } - // 加载用户成绩 —— ✅ 修复 1:直接用完整ID存储 + // 加载用户成绩 Future _loadUserScores() async { try { final userProvider = UserProvider.instance; @@ -171,8 +176,12 @@ class _MusicPageState extends State with SingleTickerProviderStateMix if (!match) return false; } - if (_filterFromVersion != null && song.from != _filterFromVersion) return false; - if (_filterGenre != null && song.genre != _filterGenre) return false; + // --- 修改点 2: 多选过滤逻辑 --- + // 如果选择了版本,歌曲版本必须在选中集合中 + if (_filterVersions.isNotEmpty && !_filterVersions.contains(song.from)) return false; + + // 如果选择了流派,歌曲流派必须在选中集合中 + if (_filterGenres.isNotEmpty && !_filterGenres.contains(song.genre)) return false; if (_filterLevelType != null) { bool hasLevel = false; @@ -248,8 +257,8 @@ class _MusicPageState extends State with SingleTickerProviderStateMix setState(() { _searchQuery = ''; _filterLevelType = null; - _filterFromVersion = null; - _filterGenre = null; + _filterVersions.clear(); // 清空集合 + _filterGenres.clear(); // 清空集合 _selectedMinLevel = null; _selectedMaxLevel = null; }); @@ -359,6 +368,123 @@ class _MusicPageState extends State with SingleTickerProviderStateMix ); } + // --- 新增:多选 Chip 构建器 --- + Widget _buildMultiSelectChip({ + required String label, + required Set selectedValues, + required Set allOptions, + required Function(Set) onSelectionChanged, + }) { + return InkWell( + onTap: () { + _showMultiSelectDialog( + context, + title: label, + allOptions: allOptions.toList()..sort(), + selectedValues: selectedValues, + onConfirm: (newSelection) { + setState(() { + onSelectionChanged(newSelection); + }); + _applyFilters(); + }, + ); + }, + 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: [ + Text(label, style: const TextStyle(fontWeight: FontWeight.bold)), + Flexible( + child: Text( + selectedValues.isEmpty ? "全部" : "已选 ${selectedValues.length}", + style: TextStyle(color: Colors.grey[600], fontSize: 12), + overflow: TextOverflow.ellipsis, + ), + ), + const Icon(Icons.arrow_drop_down, color: Colors.grey), + ], + ), + ), + ); + } + + void _showMultiSelectDialog( + BuildContext context, { + required String title, + required List allOptions, + required Set selectedValues, + required Function(Set) onConfirm, + }) { + // 临时存储选择状态 + final tempSelection = Set.from(selectedValues); + + showDialog( + context: context, + builder: (ctx) { + return StatefulBuilder( + builder: (context, setDialogState) { + return AlertDialog( + title: Text(title), + content: SizedBox( + width: double.maxFinite, + child: ListView.builder( + shrinkWrap: true, + itemCount: allOptions.length, + itemBuilder: (context, index) { + final option = allOptions[index]; + final isSelected = tempSelection.contains(option); + return CheckboxListTile( + title: Text(option), + value: isSelected, + onChanged: (bool? value) { + setDialogState(() { + if (value == true) { + tempSelection.add(option); + } else { + tempSelection.remove(option); + } + }); + }, + controlAffinity: ListTileControlAffinity.leading, + dense: true, + ); + }, + ), + ), + actions: [ + TextButton( + onPressed: () { + setDialogState(() { + tempSelection.clear(); + }); + }, + child: const Text("清空"), + ), + TextButton( + onPressed: () => Navigator.pop(ctx), + child: const Text("取消"), + ), + ElevatedButton( + onPressed: () { + onConfirm(tempSelection); + Navigator.pop(ctx); + }, + child: const Text("确定"), + ), + ], + ); + }, + ); + }, + ); + } + @override Widget build(BuildContext context) { return Scaffold( @@ -460,34 +586,27 @@ class _MusicPageState extends State with SingleTickerProviderStateMix decoration: BoxDecoration(color: Colors.grey.withAlpha(20), border: Border(top: BorderSide(color: Colors.grey.shade300))), child: Column( children: [ + // --- 修改点 3: UI 替换为多选 Chip --- Row( children: [ Expanded( - child: _buildDropdown( + child: _buildMultiSelectChip( 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(); + selectedValues: _filterVersions, + allOptions: _availableVersions, + onSelectionChanged: (newSet) { + _filterVersions = newSet; }, ), ), const SizedBox(width: 8), Expanded( - child: _buildDropdown( + child: _buildMultiSelectChip( 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(); + selectedValues: _filterGenres, + allOptions: _availableGenres, + onSelectionChanged: (newSet) { + _filterGenres = newSet; }, ), ), @@ -517,10 +636,12 @@ class _MusicPageState extends State with SingleTickerProviderStateMix ), ); } + 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 items, required ValueChanged onChanged}) { return DropdownButtonFormField( value: value, @@ -531,7 +652,13 @@ class _MusicPageState extends State with SingleTickerProviderStateMix ); } - bool _hasFilters() => _searchQuery.isNotEmpty || _filterLevelType != null || _filterFromVersion != null || _filterGenre != null || _selectedMinLevel != null || _selectedMaxLevel != null; + bool _hasFilters() => + _searchQuery.isNotEmpty || + _filterLevelType != null || + _filterVersions.isNotEmpty || // 修改判断逻辑 + _filterGenres.isNotEmpty || // 修改判断逻辑 + _selectedMinLevel != null || + _selectedMaxLevel != null; Widget _buildBody() { if (_isLoading) return const Center(child: CircularProgressIndicator()); @@ -558,33 +685,19 @@ class _MusicPageState extends State with SingleTickerProviderStateMix if (song.ut != null && _userScoreCache.containsKey(100000 + song.id)) hasScore = true; // --- 新增:处理别名逻辑 --- - // 1. 获取原始列表 List rawAliases = song.albums ?? []; - - // 2. 去重 (保留插入顺序可以使用 LinkedHashSet,或者简单转为 Set 再转 List) - // 注意:Set.from 可能会打乱顺序,如果顺序不重要可以直接用。 - // 如果希望保持原顺序去重: final seen = {}; 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 displayList = filteredAliases.take(maxAliasCount).toList(); - aliasDisplayText = displayList.join(" · "); - - // 如果还有更多未显示的,加上省略提示 if (filteredAliases.length > maxAliasCount) { aliasDisplayText += " ..."; } @@ -632,12 +745,10 @@ class _MusicPageState extends State with SingleTickerProviderStateMix ), ), const SizedBox(height: 2), - // 这一行:id + 左侧标签 Row( mainAxisAlignment: MainAxisAlignment.end, crossAxisAlignment: CrossAxisAlignment.center, children: [ - // 标签区域(cn / jp / m2l / long) if (song.cn == true) _buildTag( "CN", @@ -669,8 +780,6 @@ class _MusicPageState extends State with SingleTickerProviderStateMix ), ), - - // id 文本 Text( " ${song.id}", style: const TextStyle( @@ -703,7 +812,6 @@ class _MusicPageState extends State with SingleTickerProviderStateMix Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, - // 关键:让内容在垂直方向上尽量分布,或者使用 MainAxisSize.min 配合 Spacer mainAxisSize: MainAxisSize.min, children: [ Text( @@ -725,13 +833,12 @@ class _MusicPageState extends State with SingleTickerProviderStateMix const SizedBox(height: 6), LayoutBuilder( builder: (context, constraints) { - // 可选:根据剩余宽度动态决定显示多少行,这里简单处理为最多2行 return Text( aliasDisplayText, style: TextStyle( fontSize: 10, color: Colors.grey[500], - height: 1.2, // 紧凑行高 + height: 1.2, ), maxLines: 5, overflow: TextOverflow.ellipsis, @@ -757,16 +864,11 @@ class _MusicPageState extends State with SingleTickerProviderStateMix } 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, @@ -775,11 +877,9 @@ class _MusicPageState extends State with SingleTickerProviderStateMix 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( @@ -802,8 +902,6 @@ class _MusicPageState extends State with SingleTickerProviderStateMix } List _getDifficultyChipsByType(SongModel song) { final diffs = _getAllDifficultiesWithType(song); - - // 按 type 分组:SD / DX / UT final Map> typeGroups = {}; for (var item in diffs) { final type = item['type'] as String; @@ -811,7 +909,6 @@ class _MusicPageState extends State with SingleTickerProviderStateMix typeGroups[type]!.add(item); } - // 固定顺序:SD → DX → UT final order = ['SD', 'DX', 'UT']; List rows = []; @@ -819,7 +916,6 @@ class _MusicPageState extends State with SingleTickerProviderStateMix final items = typeGroups[type]; if (items == null || items.isEmpty) continue; - // 每一个类型 = 一行 Wrap final row = Wrap( spacing: 6, runSpacing: 4, @@ -837,8 +933,6 @@ class _MusicPageState extends State with SingleTickerProviderStateMix if (isBanquet) { color = Colors.pinkAccent; - - // 多UT按ID显示对应名称 if (type == 'UT') { final utTitleMap = song.utTitle as Map?; if (utTitleMap != null && utTitleMap.isNotEmpty) { @@ -882,7 +976,7 @@ class _MusicPageState extends State with SingleTickerProviderStateMix } return Container( - padding: const EdgeInsets.symmetric(horizontal: 5, vertical: 2), + padding: const EdgeInsets.symmetric(horizontal: 2, vertical: 1), decoration: BoxDecoration( color: color.withOpacity(hasScore ? 0.25 : 0.1), borderRadius: BorderRadius.circular(4), @@ -892,7 +986,7 @@ class _MusicPageState extends State with SingleTickerProviderStateMix isBanquet ? label : "$label ${lvValue.toStringAsFixed(1)}", style: TextStyle( color: color, - fontSize: 10, + fontSize: 9, fontWeight: FontWeight.bold, ), ), @@ -916,4 +1010,4 @@ class _MusicPageState extends State with SingleTickerProviderStateMix return "https://cdn.godserver.cn/resource/static/mai/cover/$displayId.png"; } } -} +} \ No newline at end of file diff --git a/lib/pages/music/score_single.dart b/lib/pages/music/score_single.dart index c063d4d..89b062f 100644 --- a/lib/pages/music/score_single.dart +++ b/lib/pages/music/score_single.dart @@ -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 { String? _selectedType; + bool _isAliasExpanded = false; + + // 用于跟踪下载状态,key为 "type_levelId" + final Map _isDownloading = {}; - // 缓存图表数据: Key为 "SD" / "DX" / "UT_realId" final Map>> _chartDataCache = {}; final Map _isLoadingChart = {}; @@ -74,9 +82,90 @@ class _SongDetailPageState extends State { return all; } - // 核心:SD/DX 只加载一次,UT 每个谱面加载一次 + // ===================== 【新增】下载并打开 ADX 谱面功能 ===================== + Future _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 _loadTypeChartData(String type) async { - // 已经加载/正在加载 → 直接返回 if (_chartDataCache.containsKey(type) || _isLoadingChart[type] == true) { return; } @@ -94,7 +183,6 @@ class _SongDetailPageState extends State { return; } - // 获取当前类型的 API ID int apiMusicId; if (type == 'SD') { apiMusicId = widget.song.id; @@ -105,11 +193,9 @@ class _SongDetailPageState extends State { 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 { '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 { } } - // UT 单独加载 Future _loadUtChartData(String cacheKey, int realId) async { if (_chartDataCache.containsKey(cacheKey) || _isLoadingChart[cacheKey] == true) { return; @@ -170,6 +255,73 @@ class _SongDetailPageState extends State { } } + Widget _buildAliasSection(List 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 { 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( - 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( + 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 { 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'] ?? {}; final total = notes['total'] ?? 0; @@ -323,9 +478,7 @@ class _SongDetailPageState extends State { 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 { }); } else { realId = _getRealMusicId(type); - cacheKey = type; // SD/DX 用类型做缓存 key + cacheKey = type; WidgetsBinding.instance.addPostFrameCallback((_) { _loadTypeChartData(type); }); } - // 图表数据:SD/DX 自动过滤当前难度,UT 直接使用 List> 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 { } } + 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 { 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 { ), ), ), - 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), + ), + ) + : 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 { 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 { ); } - 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 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 { } 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 { } } - 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; diff --git a/lib/pages/score/score_page.dart b/lib/pages/score/score_page.dart index 1bd2aed..6ff07d1 100644 --- a/lib/pages/score/score_page.dart +++ b/lib/pages/score/score_page.dart @@ -32,8 +32,9 @@ class _ScorePageState extends State with SingleTickerProviderStateMix bool _isAdvancedFilterExpanded = false; double? _minAchievement; late final TextEditingController _minAchievementController; - String? _filterFromVersion; - String? _filterGenre; + // 改为多选 + List _filterFromVersions = []; + List _filterGenres = []; double? _selectedMinLevel; double? _selectedMaxLevel; int? _filterComboStatus; @@ -57,7 +58,7 @@ class _ScorePageState extends State with SingleTickerProviderStateMix String get _currentDataSource => UserProvider.instance.scoreDataSource; String? get _currentSegaId => UserProvider.instance.selectedSegaId; - String? get _currentCnUserName => UserProvider.instance.selectedCnUserName; // 新增 + String? get _currentCnUserName => UserProvider.instance.selectedCnUserName; @override void initState() { @@ -148,7 +149,6 @@ class _ScorePageState extends State with SingleTickerProviderStateMix } } - /// 【核心修改】加载数据逻辑 Future _loadData({bool isInitialLoad = false}) async { if (isInitialLoad) { setState(() { _isLoading = true; }); @@ -161,7 +161,6 @@ class _ScorePageState extends State with SingleTickerProviderStateMix final token = userProvider.token; if (token == null) throw "未登录,请先登录"; - // 1. 获取歌曲元数据 if (_allSongs.isEmpty || isInitialLoad) { final songs = await SongService.getAllSongs(); _allSongs = songs; @@ -170,9 +169,7 @@ class _ScorePageState extends State with SingleTickerProviderStateMix _availableGenres = songs.map((s) => s.genre).where((g) => g.isNotEmpty).toSet(); } - // 2. 获取用户成绩数据 if (_currentDataSource == 'sega') { - // --- Sega ID 模式 --- final segaId = _currentSegaId; if (segaId == null || segaId.isEmpty) { throw "请选择一个有效的 Sega ID"; @@ -207,8 +204,6 @@ class _ScorePageState extends State with SingleTickerProviderStateMix }).toList(); } else { - // --- 国服模式 --- - // 【修改点】传递选中的用户名 final scoreData = await SongService.getUserAllScores(token,name: _currentCnUserName); if (scoreData.containsKey('userScoreAll_')) { @@ -243,13 +238,12 @@ class _ScorePageState extends State with SingleTickerProviderStateMix } } - /// 【核心修改】获取过滤并排序后的列表 List get _filteredMusicList { bool isNoFilter = _searchQuery.isEmpty && _filterLevelType == null && _filterRank == null && - _filterFromVersion == null && - _filterGenre == null && + _filterFromVersions.isEmpty && + _filterGenres.isEmpty && _selectedMinLevel == null && _selectedMaxLevel == null && _minAchievement == null && @@ -315,10 +309,12 @@ class _ScorePageState extends State with SingleTickerProviderStateMix } if (song != null) { - if (_filterFromVersion != null && song.from != _filterFromVersion) return false; - if (_filterGenre != null && song.genre != _filterGenre) return false; + // 版本多选过滤 + if (_filterFromVersions.isNotEmpty && !_filterFromVersions.contains(song.from)) return false; + // 流派多选过滤 + if (_filterGenres.isNotEmpty && !_filterGenres.contains(song.genre)) return false; } else { - if (_filterFromVersion != null || _filterGenre != null) { + if (_filterFromVersions.isNotEmpty || _filterGenres.isNotEmpty) { return false; } } @@ -373,8 +369,8 @@ class _ScorePageState extends State with SingleTickerProviderStateMix _searchQuery = ''; _filterLevelType = null; _filterRank = null; - _filterFromVersion = null; - _filterGenre = null; + _filterFromVersions.clear(); + _filterGenres.clear(); _selectedMinLevel = null; _selectedMaxLevel = null; _minAchievement = null; @@ -415,16 +411,13 @@ class _ScorePageState extends State with SingleTickerProviderStateMix ); } - /// 【核心修改】构建数据源选择器,支持国服多账号 Widget _buildDataSourceSelector() { final userProvider = UserProvider.instance; final isSegaMode = userProvider.scoreDataSource == 'sega'; - // 国服相关数据 final cnUserNames = userProvider.availableCnUserNames; final currentCnUserName = userProvider.selectedCnUserName; - // Sega相关数据 final segaCards = userProvider.availableSegaCards; final currentSegaId = userProvider.selectedSegaId; @@ -459,10 +452,10 @@ class _ScorePageState extends State with SingleTickerProviderStateMix flex: 1, child: DropdownButtonHideUnderline( child: DropdownButtonFormField( - // 如果是 Sega 模式,显示 SegaID;如果是国服模式,显示用户名 value: isSegaMode ? currentSegaId - : (currentCnUserName != null && currentCnUserName.isNotEmpty ? currentCnUserName : null), decoration: InputDecoration( + : (currentCnUserName != null && currentCnUserName.isNotEmpty ? currentCnUserName : null), + decoration: InputDecoration( hintText: isSegaMode ? "选择卡片" : "选择账号", border: OutlineInputBorder(borderRadius: BorderRadius.circular(8)), contentPadding: const EdgeInsets.symmetric(horizontal: 8, vertical: 0), @@ -476,7 +469,6 @@ class _ScorePageState extends State with SingleTickerProviderStateMix ); }).toList() : [ - // 国服选项:第一个是“全部/默认”,其余是具体用户名 const DropdownMenuItem( value: null, child: Text("全部/默认", overflow: TextOverflow.ellipsis), @@ -598,27 +590,31 @@ class _ScorePageState extends State with SingleTickerProviderStateMix 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: (val) => setState(() => _filterFromVersion = val), + child: _buildMultiSelectButton( + title: "版本", + selectedList: _filterFromVersions, + allItems: _availableVersions.toList(), + onConfirm: (selected) { + setState(() { + _filterFromVersions = selected; + }); + }, ), ), 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: (val) => setState(() => _filterGenre = val), + child: _buildMultiSelectButton( + title: "流派", + selectedList: _filterGenres, + allItems: _availableGenres.toList(), + onConfirm: (selected) { + setState(() { + _filterGenres = selected; + }); + }, ), ), ], @@ -707,6 +703,115 @@ class _ScorePageState extends State with SingleTickerProviderStateMix ); } + // 多选弹窗组件 + Widget _buildMultiSelectButton({ + required String title, + required List selectedList, + required List allItems, + required Function(List) onConfirm, + }) { + return InkWell( + onTap: () { + _showMultiSelectDialog( + context: context, + title: title, + selected: List.from(selectedList), + items: allItems, + onConfirm: onConfirm, + ); + }, + 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: [ + Text(title, style: TextStyle(fontWeight: FontWeight.bold)), + Expanded( + child: Text( + selectedList.isEmpty + ? "全部" + : selectedList.join(", "), + style: TextStyle(color: Colors.grey[600], fontSize: 13), + maxLines: 1, + overflow: TextOverflow.ellipsis, + textAlign: TextAlign.right, + ), + ), + const Icon(Icons.arrow_drop_down, color: Colors.grey), + ], + ), + ), + ); + } + // 显示多选弹窗 + Future _showMultiSelectDialog({ + required BuildContext context, + required String title, + required List selected, + required List items, + required Function(List) onConfirm, + }) async { + List tempSelected = List.from(selected); + + await showDialog( + context: context, + builder: (dialogContext) => AlertDialog( + title: Text("选择$title"), + content: SingleChildScrollView( + child: StatefulBuilder( // <--- 添加 StatefulBuilder + builder: (context, setState) { // <--- 这里的 setState 只用于刷新 Dialog 内容 + return Column( + mainAxisSize: MainAxisSize.min, + children: items.map((item) { + return CheckboxListTile( + title: Text(item), + value: tempSelected.contains(item), + onChanged: (isChecked) { + // 使用 StatefulBuilder 提供的 setState + setState(() { + if (isChecked == true) { + tempSelected.add(item); + } else { + tempSelected.remove(item); + } + }); + }, + ); + }).toList(), + ); + }, + ), + ), + actions: [ + TextButton( + onPressed: () { + Navigator.pop(dialogContext); + }, + child: const Text("取消"), + ), + TextButton( + onPressed: () { + onConfirm(tempSelected); + Navigator.pop(dialogContext); + }, + child: const Text("确定"), + ), + TextButton( + onPressed: () { + onConfirm([]); + Navigator.pop(dialogContext); + }, + child: const Text("重置", style: TextStyle(color: Colors.red)), + ), + ], + ), + ); + } + void _showLevelPickerDialog() { int targetMinIndex = 0; if (_selectedMinLevel != null) { @@ -884,8 +989,8 @@ class _ScorePageState extends State with SingleTickerProviderStateMix return _searchQuery.isNotEmpty || _filterLevelType != null || _filterRank != null || - _filterFromVersion != null || - _filterGenre != null || + _filterFromVersions.isNotEmpty || + _filterGenres.isNotEmpty || _selectedMinLevel != null || _selectedMaxLevel != null || _minAchievement != null || @@ -1145,77 +1250,7 @@ class _ScorePageState extends State with SingleTickerProviderStateMix ), ], ), - subtitle: Padding( - padding: const EdgeInsets.only(top: 8.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - Icon(Icons.play_circle_outline, size: 16, color: Colors.grey[600]), - const SizedBox(width: 4), - Text( - "Play: ${detail['playCount'] ?? 0}", - style: TextStyle(fontSize: 12, color: Colors.grey[700]), - ), - const SizedBox(width: 12), - Icon(Icons.score, size: 16, color: Colors.grey[600]), - const SizedBox(width: 4), - Text( - "DX: ${detail['deluxscoreMax'] ?? 0}", - style: TextStyle(fontSize: 12, color: Colors.grey[700]), - ), - const SizedBox(width: 12), - Icon(Icons.attach_file, size: 16, color: Colors.grey[600]), - const SizedBox(width: 4), - Text( - "Rating: ${detail['rating'] ?? 0}", - style: TextStyle(fontSize: 12, color: Colors.grey[700]), - ), - ], - ), - const SizedBox(height: 4), - Row( - children: [ - if (comboStatus > 0) - Container( - margin: const EdgeInsets.only(right: 8), - padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(4), - border: Border.all(color: comboColor.withAlpha(100)), - ), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - if (comboIcon != null) Icon(comboIcon, size: 12, color: comboColor), - if (comboIcon != null) const SizedBox(width: 2), - Text(comboText, style: TextStyle(fontSize: 14, color: comboColor, fontWeight: FontWeight.bold)), - ], - ), - ), - - if (syncStatus > 0) - Container( - padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(4), - border: Border.all(color: syncColor.withAlpha(100)), - ), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - if (syncIcon != null) Icon(syncIcon, size: 12, color: syncColor), - if (syncIcon != null) const SizedBox(width: 2), - Text(syncText, style: TextStyle(fontSize: 14, color: syncColor, fontWeight: FontWeight.bold)), - ], - ), - ), - ], - ), - ], - ), - ), + subtitle: _buildScoreInfo(detail,songInfo!), trailing: Column( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.end, @@ -1273,7 +1308,122 @@ class _ScorePageState extends State with SingleTickerProviderStateMix } return Colors.black; } + Widget _buildScoreInfo(Map score, SongModel song) { + final dxScore = score['deluxscoreMax'] ?? 0; + final playCount = score['playCount'] ?? 0; + final type = score['type'] ?? ''; + final level = score['level'] ?? 0; + final rating = score['rating'] ?? 0; + int comboStatus = score['comboStatus'] ?? 0; + int syncStatus = score['syncStatus'] ?? 0; + + int totalNotes = 0; + if ((type == 'dx' && song.dx != null) || (type == 'sd' && song.sd != null)) { + final levelKey = level.toString(); + final levelData = type == 'dx' ? song.dx![levelKey] : song.sd![levelKey]; + if (levelData != null && levelData['notes'] != null) { + totalNotes = levelData['notes']['total'] ?? 0; + } + } + + final allDx = totalNotes * 3; + final 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 Padding( + padding: const EdgeInsets.symmetric(vertical: 4.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Flexible( + child: Row( + children: [ + const Icon(Icons.score_rounded, size: 14, color: Colors.blueGrey), + const SizedBox(width: 4), + Text( + "DX: $dxScore / $allDx", + style: const TextStyle(fontSize: 13, color: Colors.blueGrey, fontWeight: FontWeight.w500), + ), + const SizedBox(width: 10), + + const Icon(Icons.play_arrow_rounded, size: 14, color: Colors.grey), + const SizedBox(width: 4), + Text( + "$playCount 次", + style: const TextStyle(fontSize: 13, color: Colors.grey), + ), + const SizedBox(width: 10), + + const Icon(Icons.star_rate_rounded, size: 14, color: Colors.amber), + const SizedBox(width: 4), + Text( + "Rating: $rating", + style: const TextStyle(fontSize: 13,fontWeight: FontWeight.w500), + ), + + const SizedBox(width: 8), + + if (comboIconPath != null) + Padding( + padding: const EdgeInsets.only(left: 4), + child: Image.asset(comboIconPath, width: 30, height: 30), + ), + + if (syncIconPath != null) + Padding( + padding: const EdgeInsets.only(left: 4), + child: Image.asset(syncIconPath, width: 30, height: 30), + ), + ], + ), + ), + + if (perc >= 0.85) _dxStarIcon(perc), + ], + ), + ); + } + + Widget _dxStarIcon(double perc) { + String asset; + + if (perc >= 0.97) { + asset = "images/UI_GAM_Gauge_DXScoreIcon_05.png"; + } else if (perc >= 0.95) { + asset = "images/UI_GAM_Gauge_DXScoreIcon_04.png"; + } else if (perc >= 0.93) { + asset = "images/UI_GAM_Gauge_DXScoreIcon_03.png"; + } else if (perc >= 0.90) { + asset = "images/UI_GAM_Gauge_DXScoreIcon_02.png"; + } else { + asset = "images/UI_GAM_Gauge_DXScoreIcon_01.png"; + } + + return Image.asset( + asset, + width: 30, + height: 30, + fit: BoxFit.contain, + ); + } Color _getRankColor(int rank) { return Colors.blueGrey; } diff --git a/lib/pages/user/userpage.dart b/lib/pages/user/userpage.dart index 4aab21c..65f5a5e 100644 --- a/lib/pages/user/userpage.dart +++ b/lib/pages/user/userpage.dart @@ -34,7 +34,7 @@ class _UserPageState extends State { Future _loadRadarData() async { final provider = Provider.of(context, listen: false); try { - final data = await provider.fetchRadarData("684a6ee7f62aed83538ded34"); + final data = await provider.fetchRadarData(provider.user?.id ?? 'default_id'); setState(() { _radarData = data; }); @@ -648,6 +648,123 @@ class _UserPageState extends State { ); } + // 移除 Sega 账号 + Future _removeSegaCard(SegaCard card, UserModel user) async { + final confirm = await showDialog( + context: context, + builder: (ctx) => AlertDialog( + title: const Text("确认移除"), + content: Text("确定要删除 ${card.segaId} 吗?"), + actions: [ + TextButton(onPressed: () => Navigator.pop(ctx), child: const Text("取消")), + TextButton( + onPressed: () => Navigator.pop(ctx, true), + style: TextButton.styleFrom(foregroundColor: Colors.red), + child: const Text("删除"), + ), + ], + ), + ); + + if (confirm != true) return; + + final provider = Provider.of(context, listen: false); + + // 直接删除,不使用 copyWith + List newList = List.from(user.segaCards ?? [])..remove(card); + + // 完全沿用你原来的 UserModel 构造方式 + UserModel updatedUser = UserModel( + id: user.id, + name: user.name, + userId: user.userId, + teamId: user.teamId, + email: user.email, + password: user.password, + twoFactorKey: user.twoFactorKey, + apiKey: user.apiKey, + apiBindKey: user.apiBindKey, + protectRole: user.protectRole, + risks: user.risks, + mcName: user.mcName, + userName2userId: user.userName2userId, + lxKey: user.lxKey, + dfUsername: user.dfUsername, + dfPassword: user.dfPassword, + nuoId: user.nuoId, + botId: user.botId, + spasolBotId: user.spasolBotId, + githubId: user.githubId, + rating: user.rating, + ratingMax: user.ratingMax, + iconId: user.iconId, + plateId: user.plateId, + plateIds: user.plateIds, + frameId: user.frameId, + charaSlots: user.charaSlots, + qiandaoDay: user.qiandaoDay, + inviter: user.inviter, + successLogoutTime: user.successLogoutTime, + lastLoginTime: user.lastLoginTime, + friendIds: user.friendIds, + bio: user.bio, + friendBio: user.friendBio, + sex: user.sex, + isDisagreeRecommend: user.isDisagreeRecommend, + isDisagreeFriend: user.isDisagreeFriend, + points: user.points, + planPoints: user.planPoints, + cardIds: user.cardIds, + userCards: user.userCards, + tags: user.tags, + useBeta: user.useBeta, + useNuo: user.useNuo, + useServer: user.useServer, + useB50Type: user.useB50Type, + userHot: user.userHot, + chatInGroupNumbers: user.chatInGroupNumbers, + sc: user.sc, + id2pcNuo: user.id2pcNuo, + mai2links: user.mai2links, + key2KeychipEn: user.key2KeychipEn, + key2key2KeychipEn: user.key2key2KeychipEn, + mai2link: user.mai2link, + userRegion: user.userRegion, + rinUsernameOrEmail: user.rinUsernameOrEmail, + rinPassword: user.rinPassword, + rinChusanUser: user.rinChusanUser, + segaCards: newList, // 这里更新 + placeList: user.placeList, + lastKeyChip: user.lastKeyChip, + token: user.token, + timesRegionData: user.timesRegionData, + yearTotal: user.yearTotal, + yearTotalComment: user.yearTotalComment, + userCollCardMap: user.userCollCardMap, + collName2musicIds: user.collName2musicIds, + ai: user.ai, + pkScore: user.pkScore, + pkScoreStr: user.pkScoreStr, + pkScoreReality: user.pkScoreReality, + pkUserId: user.pkUserId, + limitPkTimestamp: user.limitPkTimestamp, + hasAcceptPk: user.hasAcceptPk, + pkPlayNum: user.pkPlayNum, + pkWin: user.pkWin, + userData: user.userData, + banState: user.banState, + ); + + provider.updateUser(updatedUser); + await provider.saveUserInfo(); + + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text("✅ 已移除:${card.segaId}")), + ); + } + } + Widget _buildScoreCheckerCard(UserModel user) { return _webCard( child: Column( @@ -1148,6 +1265,12 @@ class _UserPageState extends State { const SizedBox(width: 10), Expanded(child: Text(card.segaId ?? "")), const SizedBox(width: 6), + TextButton( + style: TextButton.styleFrom(foregroundColor: Colors.red), + onPressed: () => _removeSegaCard(card, user), + child: const Text("移除", style: TextStyle(fontSize: 12)), + ), + const SizedBox(width: 4), TextButton( onPressed: () => _verifyBoundSega(card), child: const Text("验证", style: TextStyle(fontSize: 12)), diff --git a/lib/service/recommendation_helper.dart b/lib/service/recommendation_helper.dart index 52a802e..dbddb59 100644 --- a/lib/service/recommendation_helper.dart +++ b/lib/service/recommendation_helper.dart @@ -47,7 +47,6 @@ class RecommendationHelper { // 提取关键字段 final int musicId = detail['musicId'] ?? detail['id'] ?? 0; - if(musicId>16000) continue; final int level = detail['level'] ?? detail['levelIndex'] ?? 3; // 默认 Master final int achievement = detail['achievement'] ?? 0; // 确保 rating 是 double @@ -100,6 +99,7 @@ class RecommendationHelper { for (var song in allSongs) { // 过滤无效 ID if (song.id < 100) continue; + if (song.id > 16000) continue; // 获取 Master (Level 3) 的定数,如果没有则获取 Expert (Level 2) double? masterLevel = _getSongLevel(song, 3); diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc index f6f23bf..a78b14c 100644 --- a/linux/flutter/generated_plugin_registrant.cc +++ b/linux/flutter/generated_plugin_registrant.cc @@ -6,9 +6,13 @@ #include "generated_plugin_registrant.h" +#include #include void fl_register_plugins(FlPluginRegistry* registry) { + g_autoptr(FlPluginRegistrar) open_file_linux_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "OpenFileLinuxPlugin"); + open_file_linux_plugin_register_with_registrar(open_file_linux_registrar); g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin"); url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar); diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake index df8d2f7..0c65fa2 100644 --- a/linux/flutter/generated_plugins.cmake +++ b/linux/flutter/generated_plugins.cmake @@ -3,6 +3,7 @@ # list(APPEND FLUTTER_PLUGIN_LIST + open_file_linux url_launcher_linux ) diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index c968169..0bdafcd 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -6,6 +6,7 @@ import FlutterMacOS import Foundation import geolocator_apple +import open_file_mac import package_info_plus import share_plus import shared_preferences_foundation @@ -13,6 +14,7 @@ import url_launcher_macos func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { GeolocatorPlugin.register(with: registry.registrar(forPlugin: "GeolocatorPlugin")) + OpenFilePlugin.register(with: registry.registrar(forPlugin: "OpenFilePlugin")) FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin")) SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin")) SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) diff --git a/pubspec.lock b/pubspec.lock index b08be07..b6d7da4 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -536,6 +536,70 @@ packages: url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "9.3.0" + open_file: + dependency: "direct dev" + description: + name: open_file + sha256: b22decdae85b459eac24aeece48f33845c6f16d278a9c63d75c5355345ca236b + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + source: hosted + version: "3.5.11" + open_file_android: + dependency: transitive + description: + name: open_file_android + sha256: "58141fcaece2f453a9684509a7275f231ac0e3d6ceb9a5e6de310a7dff9084aa" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + source: hosted + version: "1.0.6" + open_file_ios: + dependency: transitive + description: + name: open_file_ios + sha256: a5acd07ba1f304f807a97acbcc489457e1ad0aadff43c467987dd9eef814098f + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + source: hosted + version: "1.0.4" + open_file_linux: + dependency: transitive + description: + name: open_file_linux + sha256: d189f799eecbb139c97f8bc7d303f9e720954fa4e0fa1b0b7294767e5f2d7550 + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + source: hosted + version: "0.0.5" + open_file_mac: + dependency: transitive + description: + name: open_file_mac + sha256: cd293f6750de6438ab2390513c99128ade8c974825d4d8128886d1cda8c64d01 + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + source: hosted + version: "1.0.4" + open_file_platform_interface: + dependency: transitive + description: + name: open_file_platform_interface + sha256: "101b424ca359632699a7e1213e83d025722ab668b9fd1412338221bf9b0e5757" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + source: hosted + version: "1.0.3" + open_file_web: + dependency: transitive + description: + name: open_file_web + sha256: e3dbc9584856283dcb30aef5720558b90f88036360bd078e494ab80a80130c4f + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + source: hosted + version: "0.0.4" + open_file_windows: + dependency: transitive + description: + name: open_file_windows + sha256: d26c31ddf935a94a1a3aa43a23f4fff8a5ff4eea395fe7a8cb819cf55431c875 + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + source: hosted + version: "0.0.3" package_config: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index acfdf66..87f9fb6 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -60,12 +60,14 @@ dev_dependencies: fl_chart: ^0.68.0 url_launcher: ^6.2.2 share_plus: ^7.2.2 + open_file: ^3.3.2 # For information on the generic Dart part of this file, see the # following page: https://dart.dev/tools/pub/pubspec # The following section is specific to Flutter packages. flutter: - + assets: + - images/ # The following line ensures that the Material Icons font is # included with your application, so that you can use the icons in # the material Icons class.