diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
index b5de554..5dd468d 100644
--- a/android/app/src/main/AndroidManifest.xml
+++ b/android/app/src/main/AndroidManifest.xml
@@ -44,5 +44,9 @@
+
+
+
+
diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist
index 24000c4..b08ddbc 100644
--- a/ios/Runner/Info.plist
+++ b/ios/Runner/Info.plist
@@ -73,5 +73,10 @@
UIInterfaceOrientationLandscapeLeft
UIInterfaceOrientationLandscapeRight
+ LSApplicationQueriesSchemes
+
+ mqq
+
+
\ No newline at end of file
diff --git a/lib/main.dart b/lib/main.dart
index 4b7e01f..e518522 100644
--- a/lib/main.dart
+++ b/lib/main.dart
@@ -8,7 +8,6 @@ void main() {
WidgetsFlutterBinding.ensureInitialized();
HardwareKeyboard.instance.clearState();
- // 🔥 不 await!直接取实例
final userProvider = UserProvider.instance;
// 🔥 后台异步初始化,不卡界面
diff --git a/lib/pages/home/home_page.dart b/lib/pages/home/home_page.dart
index 7f25b79..d79171a 100644
--- a/lib/pages/home/home_page.dart
+++ b/lib/pages/home/home_page.dart
@@ -2,13 +2,17 @@ import 'package:flutter/material.dart';
import 'package:unionapp/pages/music/music_page.dart';
import '../../service/recommendation_helper.dart';
import '../../service/song_service.dart';
+import '../../tool/cacheImage.dart';
import '../../tool/gradientText.dart';
+import '../music/adx.dart';
+import '../music/score_single.dart';
import '../score/updateScorePage.dart';
import '../user/userpage.dart';
import '../scorelist.dart';
import 'package:provider/provider.dart';
import '../../providers/user_provider.dart';
import '../../model/song_model.dart';
+import 'package:url_launcher/url_launcher.dart';
class HomePage extends StatelessWidget {
final Function(int)? onSwitchTab;
@@ -177,7 +181,7 @@ class HomePage extends StatelessWidget {
children: [
// 左侧:智能推荐乐曲(占 6 份宽度)
Expanded(
- flex: 6,
+ flex: 7,
child: const _RecommendedSongsSection(),
),
const SizedBox(width: 4),
@@ -249,7 +253,14 @@ class HomePage extends StatelessWidget {
offset: const Offset(2, 4),
),
],
- onTap: () {},
+ onTap: () {
+ Navigator.push(
+ context,
+ MaterialPageRoute(
+ builder: (_) => AdxDownloadGridPage(),
+ ),
+ );
+ },
),
// 橙色渐变
@@ -269,7 +280,27 @@ class HomePage extends StatelessWidget {
offset: const Offset(2, 4),
),
],
- onTap: () {},
+ onTap: () async {
+ // 你的固定分享链接
+ const url = "https://bot.q.qq.com/s/c2mloqdgv?id=102172520";
+ const title = "ReiSasol";
+
+ // QQ 分享 Scheme
+ final qqScheme = 'mqq://im/chat?chat_type=wpa&url=${Uri.encodeComponent(url)}&title=${Uri.encodeComponent(title)}';
+ final uri = Uri.parse(qqScheme);
+
+ try {
+ if (await canLaunchUrl(uri)) {
+ await launchUrl(uri);
+ } else {
+ // 未安装QQ → 用浏览器打开链接
+ await launchUrl(Uri.parse(url), mode: LaunchMode.externalApplication);
+ }
+ } catch (e) {
+ // 兜底打开
+ await launchUrl(Uri.parse(url));
+ }
+ },
),
_quickActionItem(
icon: Icons.link,
@@ -452,7 +483,7 @@ class _UserInfoCard extends StatelessWidget {
width: 60,
height: 60,
child: userProvider.avatarUrl.isNotEmpty
- ? Image.network(
+ ? CacheImage.network(
userProvider.avatarUrl,
fit: BoxFit.cover,
)
@@ -621,7 +652,6 @@ class _SongItemCard extends StatelessWidget {
String _getCoverUrl(int musicId) {
int displayId = musicId % 10000;
- // 注意:这里逻辑可能需要根据你的实际资源调整,通常 DX 歌曲 ID > 10000
if (musicId >= 10000) {
String idStr = displayId.toString().padLeft(6, '0');
return "https://u.mai2.link/jacket/UI_Jacket_$idStr.jpg";
@@ -630,116 +660,117 @@ class _SongItemCard extends StatelessWidget {
}
}
- // 获取难度颜色
Color _getLevelColor(int levelIndex) {
switch (levelIndex) {
- case 0: return Colors.green; // Basic
- case 1: return Colors.blue; // Advanced
- case 2: return Colors.yellow[700]!; // Expert
- case 3: return Colors.red; // Master
- case 4: return Colors.purple; // Re:Master
+ case 0: return Colors.green;
+ case 1: return Colors.blue;
+ case 2: return Colors.yellow[700]!;
+ case 3: return Colors.red;
+ case 4: return Colors.purple;
default: return Colors.grey;
}
}
@override
Widget build(BuildContext context) {
- // 假设我们要显示 Master (3) 和 Re:Master (4) 的难度
- // 你需要从 song.sd 或 song.dx 中解析出具体的 level_value (定数)
double? masterLv = _getLevelValue(song, 3);
double? reMasterLv = _getLevelValue(song, 4);
- return Container(
- width: 140,
- margin: const EdgeInsets.only(right: 12),
- decoration: BoxDecoration(
- borderRadius: BorderRadius.circular(12),
- boxShadow: [
- BoxShadow(
- color: Colors.grey.withOpacity(0.15),
- blurRadius: 8,
- offset: const Offset(0, 4),
+ // ⭐ 核心:用 InkWell / GestureDetector 包裹整个卡片,实现点击跳转
+ return InkWell(
+ borderRadius: BorderRadius.circular(12),
+ onTap: () {
+ // 跳转到歌曲详情页
+ Navigator.push(
+ context,
+ MaterialPageRoute(
+ builder: (context) => SongDetailPage(
+ song: song,
+ userScoreCache: {}, // 你可以根据实际页面传值
+ ),
),
- ],
- ),
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- // ✅ 1. 图片优化:增加 loadingBuilder 和 cacheWidth
- ClipRRect(
- borderRadius: const BorderRadius.vertical(top: Radius.circular(12)),
- child: SizedBox(
- height: 140,
- width: double.infinity,
- child: Image.network(
- _getCoverUrl(song.id),
- fit: BoxFit.cover,
- // 关键优化:指定缓存宽度,减少内存占用和解码时间
- cacheWidth: 280,
- loadingBuilder: (context, child, loadingProgress) {
- if (loadingProgress == null) return child;
- return Container(
- color: Colors.grey[200],
- child: Center(child: CircularProgressIndicator(
- strokeWidth: 2,
- value: loadingProgress.expectedTotalBytes != null
- ? loadingProgress.cumulativeBytesLoaded /
- loadingProgress.expectedTotalBytes!
- : null,
- )),
- );
- },
- errorBuilder: (_, __, ___) {
- return Container(
- color: Colors.grey[200],
- child: const Icon(Icons.music_note, size: 40, color: Colors.grey),
- );
- },
+ );
+ },
+ child: Container(
+ width: 140,
+ margin: const EdgeInsets.only(right: 12),
+ decoration: BoxDecoration(
+ borderRadius: BorderRadius.circular(12),
+ boxShadow: [
+ BoxShadow(
+ color: Colors.grey.withOpacity(0.15),
+ blurRadius: 8,
+ offset: const Offset(0, 4),
+ ),
+ ],
+ ),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ ClipRRect(
+ borderRadius: const BorderRadius.vertical(top: Radius.circular(12)),
+ child: SizedBox(
+ height: 140,
+ width: double.infinity,
+ child: CacheImage.network(
+ _getCoverUrl(song.id),
+ fit: BoxFit.cover,
+ loadingBuilder: (context, child, loadingProgress) {
+ if (loadingProgress == null) return child;
+ return Container(
+ color: Colors.grey[200],
+ child: Center(child: CircularProgressIndicator(
+ strokeWidth: 2,
+ value: loadingProgress.expectedTotalBytes != null
+ ? loadingProgress.cumulativeBytesLoaded /
+ loadingProgress.expectedTotalBytes!
+ : null,
+ )),
+ );
+ },
+ ),
),
),
- ),
- const SizedBox(height: 8),
+ const SizedBox(height: 8),
- // 标题
- Padding(
- padding: const EdgeInsets.symmetric(horizontal: 8),
- child: Text(
- song.title ?? "未知歌曲",
- maxLines: 1,
- overflow: TextOverflow.ellipsis,
- style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w600),
+ Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 8),
+ child: Text(
+ song.title ?? "未知歌曲",
+ maxLines: 1,
+ overflow: TextOverflow.ellipsis,
+ style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w600),
+ ),
),
- ),
- // 艺术家 (修复了重复显示的问题)
- Padding(
- padding: const EdgeInsets.symmetric(horizontal: 8),
- child: Text(
- song.artist ?? "未知艺术家",
- maxLines: 1,
- overflow: TextOverflow.ellipsis,
- style: TextStyle(fontSize: 11, color: Colors.grey[600]),
+ Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 8),
+ child: Text(
+ song.artist ?? "未知艺术家",
+ maxLines: 1,
+ overflow: TextOverflow.ellipsis,
+ style: TextStyle(fontSize: 11, color: Colors.grey[600]),
+ ),
),
- ),
- const SizedBox(height: 6),
+ const SizedBox(height: 6),
- // ✅ 2. 底部难度标签
- Padding(
- padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
- child: Row(
- children: [
- if (masterLv != null)
- _buildLevelTag("MAS", masterLv, Colors.purple),
- if (reMasterLv != null) ...[
- const SizedBox(width: 4),
- _buildLevelTag("ReM", reMasterLv, Colors.deepPurple),
- ]
- ],
+ Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
+ child: Row(
+ children: [
+ if (masterLv != null)
+ _buildLevelTag("MAS", masterLv, Colors.purple),
+ if (reMasterLv != null) ...[
+ const SizedBox(width: 4),
+ _buildLevelTag("ReM", reMasterLv, Colors.deepPurple),
+ ]
+ ],
+ ),
),
- ),
- ],
+ ],
+ ),
),
);
}
@@ -763,18 +794,17 @@ class _SongItemCard extends StatelessWidget {
);
}
- // 辅助方法:获取定数
double? _getLevelValue(SongModel song, int levelIndex) {
- Map ?diffMap = song.dx;
- if (diffMap==null|| diffMap.isEmpty) {
+ Map? diffMap = song.dx;
+ if (diffMap == null || diffMap.isEmpty) {
diffMap = song.sd;
}
- var data = diffMap?["$levelIndex"] ?? diffMap?[levelIndex];
- if (data is Map && data["level_value"] != null) {
- return (data["level_value"] as num).toDouble();
- }
- return null;
+ var data = diffMap?["$levelIndex"] ?? diffMap?[levelIndex];
+ if (data is Map && data["level_value"] != null) {
+ return (data["level_value"] as num).toDouble();
+ }
+ return null;
}
}
@@ -804,7 +834,7 @@ class PosterImage extends StatelessWidget {
),
child: ClipRRect(
borderRadius: BorderRadius.circular(8),
- child: Image.network(
+ child: CacheImage.network(
imageUrl,
fit: BoxFit.cover,
loadingBuilder: (context, child, loadingProgress) {
diff --git a/lib/pages/music/adx.dart b/lib/pages/music/adx.dart
new file mode 100644
index 0000000..de6c8ba
--- /dev/null
+++ b/lib/pages/music/adx.dart
@@ -0,0 +1,755 @@
+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