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 'dart:io'; import '../../../model/song_model.dart'; import '../../../service/song_service.dart'; import 'package:flutter/cupertino.dart'; import '../../tool/cacheImage.dart'; class AdxDownloadGridPage extends StatefulWidget { const AdxDownloadGridPage({super.key}); @override State createState() => _AdxDownloadGridPageState(); } class _AdxDownloadGridPageState extends State with SingleTickerProviderStateMixin { bool _isLoading = true; String _error = ''; List _songs = []; List _displaySongs = []; // 记录正在下载的歌曲ID与类型,避免重复点击 final Map _downloading = {}; // 筛选相关 String _searchQuery = ''; int? _filterLevelType; Set _filterVersions = {}; Set _filterGenres = {}; double? _selectedMinLevel; double? _selectedMaxLevel; bool _isAdvancedFilterExpanded = false; late AnimationController _animationController; late Animation _animation; static const List _levelOptions = [ 1.0, 1.5, 2.0, 2.5, 3.0, 3.5, 4.0, 4.5, 5.0, 5.5, 6.0, 6.5, 7.0, 7.5, 8.0, 8.5, 9.0, 9.5, 10.0, 10.5, 11.0, 11.5, 12.0, 12.5, 13.0, 13.1, 13.2, 13.3, 13.4, 13.5, 13.6, 13.7, 13.8, 13.9, 14.0, 14.1, 14.2, 14.3, 14.4, 14.5, 14.6, 14.7, 14.8, 14.9, 15.0, ]; late FixedExtentScrollController _minLevelScrollController; late FixedExtentScrollController _maxLevelScrollController; Set _availableVersions = {}; Set _availableGenres = {}; @override void initState() { super.initState(); _animationController = AnimationController( vsync: this, duration: const Duration(milliseconds: 300), ); _animation = CurvedAnimation( parent: _animationController, curve: Curves.easeInOut, ); _minLevelScrollController = FixedExtentScrollController(initialItem: 0); _maxLevelScrollController = FixedExtentScrollController(initialItem: _levelOptions.length - 1); _loadSongs(); } @override void dispose() { _animationController.dispose(); _minLevelScrollController.dispose(); _maxLevelScrollController.dispose(); super.dispose(); } Future _loadSongs() async { setState(() => _isLoading = true); try { final songs = await SongService.getAllSongs(); // 去重 final map = {}; for (var s in songs) { map.putIfAbsent(s.id, () => s); } _songs = map.values.toList(); _displaySongs = List.from(_songs); // 初始化筛选选项 _availableVersions = songs.map((s) => s.from).where((v) => v.isNotEmpty).toSet(); _availableGenres = songs.map((s) => s.genre).where((g) => g.isNotEmpty).toSet(); setState(() => _error = ''); } catch (e) { setState(() => _error = e.toString()); } finally { setState(() => _isLoading = false); } } // 核心:下载 ADX Future _downloadAdx(SongModel song, String type) async { final key = "${song.id}_$type"; if (_downloading[key] == true) return; setState(() => _downloading[key] = true); try { int id = type == 'SD' ? song.id : song.id + 10000; final url = "https://cdn.godserver.cn/resource/static/adx/$id.adx"; final res = await http.get(Uri.parse(url)); if (res.statusCode != 200) throw Exception("文件不存在"); final dir = await getApplicationDocumentsDirectory(); final safeTitle = song.title?.replaceAll(RegExp(r'[^\w\s\u4e00-\u9fa5]'), '_') ?? "song"; final file = File("${dir.path}/${safeTitle}_$type.adx"); await file.writeAsBytes(res.bodyBytes); if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text("✅ ${song.title} ($type) 下载完成")), ); await OpenFile.open(file.path); } catch (e) { if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text("❌ 下载失败:$e")), ); } finally { if (mounted) setState(() => _downloading[key] = false); } } // 筛选应用 void _applyFilters() { final q = _normalizeSearch(_searchQuery); _displaySongs = _songs.where((song) { // 搜索过滤 if (q.isNotEmpty) { bool match = false; if (_normalizeSearch(song.title).contains(q)) match = true; if (_normalizeSearch(song.artist).contains(q)) match = true; if (song.id.toString().contains(q)) match = true; if (!match) return false; } // 版本过滤 if (_filterVersions.isNotEmpty && !_filterVersions.contains(song.from)) return false; // 流派过滤 if (_filterGenres.isNotEmpty && !_filterGenres.contains(song.genre)) return false; // 难度类型过滤 if (_filterLevelType != null) { bool hasLevel = false; final allDiffs = _getAllDifficultiesWithType(song); for (var d in allDiffs) { if (d['diff']['level_id'] == _filterLevelType) { hasLevel = true; break; } } if (!hasLevel) return false; } // 定数范围过滤 if (_selectedMinLevel != null || _selectedMaxLevel != null) { bool inRange = false; final allDiffs = _getAllDifficultiesWithType(song); for (var d in allDiffs) { final lv = double.tryParse(d['diff']['level_value']?.toString() ?? ''); if (lv == null) continue; final minOk = _selectedMinLevel == null || lv >= _selectedMinLevel!; final maxOk = _selectedMaxLevel == null || lv <= _selectedMaxLevel!; if (minOk && maxOk) { inRange = true; break; } } if (!inRange) return false; } return true; }).toList(); setState(() {}); } String _normalizeSearch(String? s) { if (s == null) return ''; return s.trim().toLowerCase(); } // 获取所有难度 List> _getAllDifficultiesWithType(SongModel song) { List> all = []; if (song.sd != null && song.sd!.isNotEmpty) { for (var d in song.sd!.values) { all.add({'type': 'SD', 'diff': d}); } } if (song.dx != null && song.dx!.isNotEmpty) { for (var d in song.dx!.values) { all.add({'type': 'DX', 'diff': d}); } } return all; } // 重置筛选 void _resetFilters() { setState(() { _searchQuery = ''; _filterLevelType = null; _filterVersions.clear(); _filterGenres.clear(); _selectedMinLevel = null; _selectedMaxLevel = null; }); _minLevelScrollController.jumpToItem(0); _maxLevelScrollController.jumpToItem(_levelOptions.length - 1); _applyFilters(); } // 定数范围文本 String _getLevelRangeText() { if (_selectedMinLevel == null && _selectedMaxLevel == null) return "全部"; final min = _selectedMinLevel?.toStringAsFixed(1) ?? "1.0"; final max = _selectedMaxLevel?.toStringAsFixed(1) ?? "15.0"; return "$min ~ $max"; } // 定数选择器 void _showLevelPickerDialog() { int minIdx = _selectedMinLevel != null ? _levelOptions.indexOf(_selectedMinLevel!) : 0; int maxIdx = _selectedMaxLevel != null ? _levelOptions.indexOf(_selectedMaxLevel!) : _levelOptions.length - 1; showModalBottomSheet( context: context, shape: const RoundedRectangleBorder(borderRadius: BorderRadius.vertical(top: Radius.circular(16))), builder: (context) { WidgetsBinding.instance.addPostFrameCallback((_) { _minLevelScrollController.jumpToItem(minIdx); _maxLevelScrollController.jumpToItem(maxIdx); }); return Container( height: 300, padding: const EdgeInsets.only(top: 16), child: Column( children: [ Padding( padding: const EdgeInsets.symmetric(horizontal: 16), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ TextButton( onPressed: () { setState(() { _selectedMinLevel = null; _selectedMaxLevel = null; }); Navigator.pop(context); }, child: const Text("重置", style: TextStyle(color: Colors.red)), ), const Text("定数范围", style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16)), TextButton( onPressed: () { final min = _levelOptions[_minLevelScrollController.selectedItem]; final max = _levelOptions[_maxLevelScrollController.selectedItem]; setState(() { _selectedMinLevel = min; _selectedMaxLevel = max; _applyFilters(); }); Navigator.pop(context); }, child: const Text("确定", style: TextStyle(fontWeight: FontWeight.bold)), ), ], ), ), const Divider(), Expanded( child: Row( children: [ Expanded( child: Column( children: [ const Text("最小定数", style: TextStyle(fontSize: 12, color: Colors.grey)), Expanded( child: CupertinoPicker( scrollController: _minLevelScrollController, itemExtent: 40, onSelectedItemChanged: (_) {}, children: _levelOptions.map((e) => Center(child: Text(e.toStringAsFixed(1)))).toList(), ), ), ], ), ), Expanded( child: Column( children: [ const Text("最大定数", style: TextStyle(fontSize: 12, color: Colors.grey)), Expanded( child: CupertinoPicker( scrollController: _maxLevelScrollController, itemExtent: 40, onSelectedItemChanged: (_) {}, children: [], ), ), ], ), ), ], ), ), ], ), ); }, ); } // 多选筛选构建 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("确定"), ), ], ); }, ); }, ); } // 下拉框 Widget _buildDropdown({required String label, required dynamic value, required List items, required ValueChanged onChanged}) { return DropdownButtonFormField( value: value, isExpanded: true, decoration: InputDecoration(labelText: label, border: const OutlineInputBorder(), contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 12), isDense: false), items: items, onChanged: onChanged, ); } // 是否有筛选条件 bool _hasFilters() => _searchQuery.isNotEmpty || _filterLevelType != null || _filterVersions.isNotEmpty || _filterGenres.isNotEmpty || _selectedMinLevel != null || _selectedMaxLevel != null; String _coverUrl(int id) { final d = id % 10000; if (id >= 16000 && id <= 20000) { final s = d.toString().padLeft(6, '0'); return "https://u.mai2.link/jacket/UI_Jacket_$s.jpg"; } else { return "https://cdn.godserver.cn/resource/static/mai/cover/$d.png"; } } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text("ADX 谱面下载库"), centerTitle: true, actions: [ IconButton( icon: const Icon(Icons.refresh), onPressed: _loadSongs, ), ], ), body: Column( children: [ _buildFilterBar(), Expanded(child: _buildBody()), ], ), ); } // 筛选栏 Widget _buildFilterBar() { return Container( color: Theme.of(context).cardColor, child: Column( mainAxisSize: MainAxisSize.min, children: [ Padding( padding: const EdgeInsets.fromLTRB(12, 8, 12, 8), child: Row( children: [ Expanded( child: TextField( decoration: InputDecoration( hintText: "搜索歌名/艺术家/ID", prefixIcon: const Icon(Icons.search), border: OutlineInputBorder(borderRadius: BorderRadius.circular(25)), contentPadding: const EdgeInsets.symmetric(vertical: 0, horizontal: 16), isDense: true, ), onChanged: (v) { setState(() => _searchQuery = v); _applyFilters(); }, ), ), IconButton( icon: Icon(_isAdvancedFilterExpanded ? Icons.expand_less : Icons.expand_more), onPressed: () { setState(() { _isAdvancedFilterExpanded = !_isAdvancedFilterExpanded; _isAdvancedFilterExpanded ? _animationController.forward() : _animationController.reverse(); }); }, ), if (_hasFilters()) IconButton(icon: const Icon(Icons.clear, color: Colors.redAccent), onPressed: _resetFilters), ], ), ), Padding( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4), child: Row( children: [ Expanded( child: _buildDropdown( label: "难度类型", value: _filterLevelType, items: const [ DropdownMenuItem(value: null, child: Text("全部")), DropdownMenuItem(value: 0, child: Text("Basic")), DropdownMenuItem(value: 1, child: Text("Advanced")), DropdownMenuItem(value: 2, child: Text("Expert")), DropdownMenuItem(value: 3, child: Text("Master")), DropdownMenuItem(value: 4, child: Text("Re:Master")), ], onChanged: (v) { setState(() => _filterLevelType = v); _applyFilters(); }, ), ), ], ), ), SizeTransition( sizeFactor: _animation, axisAlignment: -1, child: Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration(color: Colors.grey.withAlpha(20), border: Border(top: BorderSide(color: Colors.grey.shade300))), child: Column( children: [ Row( children: [ Expanded( child: _buildMultiSelectChip( label: "版本", selectedValues: _filterVersions, allOptions: _availableVersions, onSelectionChanged: (newSet) { _filterVersions = newSet; }, ), ), const SizedBox(width: 8), Expanded( child: _buildMultiSelectChip( label: "流派", selectedValues: _filterGenres, allOptions: _availableGenres, onSelectionChanged: (newSet) { _filterGenres = newSet; }, ), ), ], ), const SizedBox(height: 8), InkWell( onTap: _showLevelPickerDialog, child: Container( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 12), decoration: BoxDecoration(border: Border.all(color: Colors.grey.shade400), borderRadius: BorderRadius.circular(4)), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ const Text("定数范围", style: TextStyle(fontWeight: FontWeight.bold)), Text(_getLevelRangeText(), style: TextStyle(color: Colors.grey[600])), const Icon(Icons.arrow_drop_down, color: Colors.grey), ], ), ), ), ], ), ), ), ], ), ); } Widget _buildBody() { if (_isLoading) { return const Center(child: CircularProgressIndicator()); } if (_error.isNotEmpty) { return Center(child: Text(_error, style: const TextStyle(color: Colors.red))); } if (_displaySongs.isEmpty) { return const Center(child: Text("暂无匹配歌曲")); } return GridView.builder( padding: const EdgeInsets.all(12), gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: 2, crossAxisSpacing: 12, mainAxisSpacing: 12, childAspectRatio: 0.75, ), itemCount: _displaySongs.length, itemBuilder: (ctx, i) => _songCard(_displaySongs[i]), ); } Widget _songCard(SongModel song) { final cover = _coverUrl(song.id); final hasSD = song.sd?.isNotEmpty ?? false; final hasDX = song.dx?.isNotEmpty ?? false; final keySD = "${song.id}_SD"; final keyDX = "${song.id}_DX"; return Card( elevation: 4, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(16), ), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Expanded( flex: 3, child: ClipRRect( borderRadius: const BorderRadius.vertical(top: Radius.circular(16)), child: CacheImage.network( cover, errorBuilder: (_, __, ___) => Container( color: Colors.grey[200], child: const Icon(Icons.music_note, size: 40), ), ), ), ), Padding( padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( "[${song.id}] ${song.title}", style: const TextStyle( fontSize: 13, fontWeight: FontWeight.bold, ), maxLines: 1, overflow: TextOverflow.ellipsis, ), const SizedBox(height: 2), Text( song.artist ?? "Unknown", style: TextStyle(fontSize: 11, color: Colors.grey[600]), maxLines: 1, overflow: TextOverflow.ellipsis, ), ], ), ), Padding( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6), child: Row( children: [ if (hasSD) Expanded( child: _downloadBtn( text: "SD", color: Colors.green, loading: _downloading[keySD] ?? false, onTap: () => _downloadAdx(song, 'SD'), ), ), if (hasSD && hasDX) const SizedBox(width: 6), if (hasDX) Expanded( child: _downloadBtn( text: "DX", color: Colors.purple, loading: _downloading[keyDX] ?? false, onTap: () => _downloadAdx(song, 'DX'), ), ), ], ), ), const SizedBox(height: 6), ], ), ); } Widget _downloadBtn({ required String text, required Color color, required bool loading, required VoidCallback onTap, }) { return ElevatedButton( onPressed: loading ? null : onTap, style: ElevatedButton.styleFrom( backgroundColor: color, padding: const EdgeInsets.symmetric(vertical: 10), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(10), ), ), child: loading ? const SizedBox( width: 16, height: 16, child: CircularProgressIndicator( color: Colors.white, strokeWidth: 2, ), ) : Text( text, style: const TextStyle(color: Colors.white, fontSize: 12), ), ); } }