diff --git a/lib/main.dart b/lib/main.dart index 06fddfe..4b7e01f 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,21 +1,27 @@ import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:unionapp/providers/user_provider.dart'; -import 'home_screen.dart'; // 1. 导入我们写好的主界面 +import 'home_screen.dart'; import 'package:provider/provider.dart'; - -void main() async { +void main() { WidgetsFlutterBinding.ensureInitialized(); + HardwareKeyboard.instance.clearState(); + + // 🔥 不 await!直接取实例 final userProvider = UserProvider.instance; - await userProvider.initUser(); + + // 🔥 后台异步初始化,不卡界面 + userProvider.initUser(); runApp( ChangeNotifierProvider.value( value: userProvider, - child: const MaterialApp(home: MyApp()), + child: const MyApp(), ), ); } + class MyApp extends StatelessWidget { const MyApp({super.key}); @@ -23,38 +29,24 @@ class MyApp extends StatelessWidget { Widget build(BuildContext context) { return MaterialApp( title: 'Glass Nav Demo', - // 2. 配置主题 theme: ThemeData( useMaterial3: true, - - // 🔑 关键点:背景色不能是纯白或不透明的纯色 - // 建议设置一个浅灰色或带渐变的背景,这样玻璃效果才明显 scaffoldBackgroundColor: const Color(0xFFF0F2F5), - colorScheme: ColorScheme.fromSeed( seedColor: Colors.blue, brightness: Brightness.light, ), ), - - // 3. 配置暗色模式主题(可选,但推荐) darkTheme: ThemeData( useMaterial3: true, - // 暗色模式下,背景深一点,玻璃效果会更像“磨砂黑玻璃” scaffoldBackgroundColor: const Color(0xFF121212), colorScheme: ColorScheme.fromSeed( seedColor: Colors.blue, brightness: Brightness.dark, ), ), - - // 4. 跟随系统自动切换亮/暗模式 themeMode: ThemeMode.system, - - // 5. 设置首页为 HomeScreen home: const HomeScreen(), - - // 去掉默认的 debug 标签(可选) debugShowCheckedModeBanner: false, ); } diff --git a/lib/model/song_model.dart b/lib/model/song_model.dart index 4e0f4c2..7ed810a 100644 --- a/lib/model/song_model.dart +++ b/lib/model/song_model.dart @@ -13,9 +13,9 @@ class SongModel { final String from; // 难度详情映射 key通常是 "0"(Basic), "1"(Advanced) 等,或者 ut 的特殊id - final Map? dxLevels; - final Map? sdLevels; - final Map? utLevels; + final Map? dx; + final Map? sd; + final Map? ut; SongModel({ required this.id, @@ -30,9 +30,9 @@ class SongModel { required this.bpm, required this.releaseDate, required this.from, - this.dxLevels, - this.sdLevels, - this.utLevels, + this.dx, + this.sd, + this.ut, }); factory SongModel.fromJson(Map json) { @@ -49,9 +49,9 @@ class SongModel { bpm: json['bpm'] ?? 0, releaseDate: json['releaseDate'] ?? '', from: json['from'] ?? '', - dxLevels: json['dx'], - sdLevels: json['sd'], - utLevels: json['ut'], + dx: json['dx'], + sd: json['sd'], + ut: json['ut'], ); } } diff --git a/lib/pages/home/home_page.dart b/lib/pages/home/home_page.dart index 80c6353..1348037 100644 --- a/lib/pages/home/home_page.dart +++ b/lib/pages/home/home_page.dart @@ -1,15 +1,15 @@ import 'package:flutter/material.dart'; +import '../../service/recommendation_helper.dart'; +import '../../service/song_service.dart'; import '../../tool/gradientText.dart'; -// 注意:如果 UserPage, SongListPage 等只是作为内部卡片展示,不需要再 import 用于 Navigator push -// 但如果其他卡片还需要跳转,保留 import 即可 import '../user/userpage.dart'; import '../songlistpage.dart'; import '../scorelist.dart'; import 'package:provider/provider.dart'; import '../../providers/user_provider.dart'; +import '../../model/song_model.dart'; class HomePage extends StatelessWidget { - // ✅ 1. 添加回调函数参数 final Function(int)? onSwitchTab; const HomePage({super.key, this.onSwitchTab}); @@ -57,7 +57,7 @@ class HomePage extends StatelessWidget { ), const SizedBox(width: 8), ClipRRect( - borderRadius: BorderRadius.circular(0), // 修正语法错误,原代码可能有误 + borderRadius: BorderRadius.circular(0), child: SizedBox( width: 54, height: 54, @@ -89,13 +89,10 @@ class HomePage extends StatelessWidget { child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ - - // ✅ 2. 修改:用户中心 -> 切换到 Tab 3 (UserPage) _buildCardWithTitle( context: context, title: "用户中心", icon: Icons.person_outline, - // 不再传递 targetPage,而是传递 targetIndex targetIndex: 3, gradient: const LinearGradient( colors: [Colors.black, Colors.grey], @@ -106,12 +103,11 @@ class HomePage extends StatelessWidget { ), const SizedBox(width: 20), - // 歌曲列表 (保持原有跳转逻辑或自行定义) _buildCardWithTitle( context: context, title: "歌曲列表", icon: Icons.music_note_outlined, - targetPage: const SongListPage(), // 假设这个还是用 Push 跳转 + targetPage: const SongListPage(), gradient: const LinearGradient( colors: [Color(0xFFff9a9e), Color(0xFFfecfef)], begin: Alignment.topLeft, @@ -121,7 +117,6 @@ class HomePage extends StatelessWidget { ), const SizedBox(width: 20), - // ✅ 3. 修改:成绩管理 -> 切换到 Tab 1 (ScorePage) _buildCardWithTitle( context: context, title: "成绩管理", @@ -136,7 +131,6 @@ class HomePage extends StatelessWidget { ), const SizedBox(width: 20), - // 娱乐功能 _buildCardWithTitle( context: context, title: "娱乐功能", @@ -151,7 +145,6 @@ class HomePage extends StatelessWidget { ), const SizedBox(width: 20), - // 评分列表 _buildCardWithTitle( context: context, title: "评分列表", @@ -171,27 +164,176 @@ class HomePage extends StatelessWidget { ), // ====================== 海报 ====================== - // 假设 PosterImage 是你自定义的一个 Widget const PosterImage(imageUrl: 'https://cdn.godserver.cn/post/post%20unionapp1.png'), const SizedBox(height: 20), - // ====================== 新增:用户数据展示卡片 ====================== + // ====================== 用户数据展示卡片 ====================== const _UserInfoCard(), const SizedBox(height: 30), + + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // 左侧:智能推荐乐曲(占 6 份宽度) + Expanded( + flex: 6, + child: const _RecommendedSongsSection(), + ), + const SizedBox(width: 20), // 左右间距 + // 右侧:快捷按钮区域(占 4 份宽度) + Expanded( + flex: 6, + child: _buildQuickActionButtons(), // 快捷按钮组件 + ), + const SizedBox(width: 10), // 左右间距 + ], + ), + const SizedBox(height: 30), ], ), ), ), ); } +// 右侧快捷按钮区域(你可以自由修改图标、文字、点击事件) + Widget _buildQuickActionButtons() { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + "快捷操作", + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 15), + // 按钮网格 + GridView.count( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + crossAxisCount: 2, // 一行 2 个按钮 + crossAxisSpacing: 12, + mainAxisSpacing: 12, + childAspectRatio: 1.0, + children: [ + _quickActionItem( + icon: Icons.download, + label: "更新成绩", + gradient: LinearGradient( + colors: [Colors.lightBlueAccent.withAlpha(10), Colors.pinkAccent.withOpacity(0.1)], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + color: Colors.pinkAccent.shade100, + onTap: () {}, + ), + _quickActionItem( + icon: Icons.stars_outlined, + label: "ADX", + color: Colors.white, + gradient: LinearGradient( + colors: [Colors.blueAccent, Colors.pinkAccent.shade100], + begin: Alignment.topLeft, + end: Alignment.topRight, + ), + boxShadow: [ + BoxShadow( + color: Colors.blueAccent.withOpacity(0.3), + blurRadius: 8, + offset: const Offset(2, 4), + ), + ], + onTap: () {}, + ), - // ✅ 4. 修改构建方法,支持 targetIndex 和 targetPage 两种模式 + // 橙色渐变 + _quickActionItem( + icon: Icons.generating_tokens, + label: "ReiSasol", + color: Colors.white, + gradient: LinearGradient( + colors: [Colors.orange, Colors.deepOrangeAccent], + ), + boxShadow: [ + BoxShadow( + color: Colors.orange.withOpacity(0.3), + blurRadius: 8, + offset: const Offset(2, 4), + ), + ], + onTap: () {}, + ), + _quickActionItem( + icon: Icons.link, + label: "友站", + color: Colors.white, + gradient: LinearGradient( + colors: [Colors.grey[600]!, Colors.grey[400]!], + ), + boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(0.2), + blurRadius: 6, + offset: const Offset(1, 2), + ), + ], + onTap: () {}, + ), + ], + ), + ], + ); + } + + +// 修改后的组件方法 + Widget _quickActionItem({ + required IconData icon, + required String label, + required Color color, + required VoidCallback onTap, + // 新增:渐变背景(可选) + Gradient? gradient, + // 新增:阴影(可选) + List? boxShadow, + }) { + return InkWell( + onTap: onTap, + borderRadius: BorderRadius.circular(16), + child: Container( + decoration: BoxDecoration( + // 优先级:渐变 > 纯色背景 + color: gradient == null ? Colors.black.withOpacity(0.1) : null, + gradient: gradient, + borderRadius: BorderRadius.circular(16), + boxShadow: boxShadow, + ), + padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 8), // 优化内边距 + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(icon, color: color, size: 28), + const SizedBox(height: 8), + Text( + label, + style: TextStyle( + fontSize: 13, + color: color, + fontWeight: FontWeight.w500, + ), + ), + ], + ), + ), + ); + } Widget _buildCardWithTitle({ required BuildContext context, required String title, required IconData icon, - int? targetIndex, // 新增:如果是切换 Tab,传这个 - Widget? targetPage, // 保留:如果是页面跳转,传这个 + int? targetIndex, + Widget? targetPage, required LinearGradient gradient, required Color shadowColor, }) { @@ -201,10 +343,8 @@ class HomePage extends StatelessWidget { GestureDetector( onTap: () { if (targetIndex != null && onSwitchTab != null) { - // ✅ 执行 Tab 切换 onSwitchTab!(targetIndex); } else if (targetPage != null) { - // ✅ 执行页面跳转 Navigator.push( context, MaterialPageRoute(builder: (context) => targetPage), @@ -269,7 +409,7 @@ class HomePage extends StatelessWidget { } } -// ====================== 新增:用户数据卡片组件 ====================== +// ====================== 用户数据卡片 ====================== class _UserInfoCard extends StatelessWidget { const _UserInfoCard(); @@ -278,19 +418,18 @@ class _UserInfoCard extends StatelessWidget { final userProvider = Provider.of(context); return Padding( - // 内边距和海报完全一致,保证同宽 padding: const EdgeInsets.symmetric(horizontal: 17.0), child: Container( width: double.infinity, decoration: BoxDecoration( borderRadius: BorderRadius.circular(10), - color: Colors.white.withOpacity(0.8), + color: Colors.white, boxShadow: [ BoxShadow( - color: Colors.pink.shade100, + color: Colors.lightBlueAccent.shade100, blurRadius: 4, - offset: const Offset(0, 6), - spreadRadius: 1, + offset: const Offset(0, 2), + spreadRadius: 3, ), ], ), @@ -298,22 +437,8 @@ class _UserInfoCard extends StatelessWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - // 标题 - const Text( - "当前用户信息", - style: TextStyle( - fontSize: 18, - color: Colors.purpleAccent, - fontWeight: FontWeight.bold, - ), - ), - const Divider(height: 5, thickness: 1), - const SizedBox(height: 6), - - // 用户信息行 Row( children: [ - // 头像 ClipRRect( borderRadius: BorderRadius.circular(0), child: SizedBox( @@ -331,14 +456,12 @@ class _UserInfoCard extends StatelessWidget { ), ), const SizedBox(width: 18), - - // 文字信息 Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ GradientText( - data:"用户名:${userProvider.username}", + data:"用户名:${userProvider.username} ", style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold), gradientLayers: [ GradientLayer( @@ -349,7 +472,19 @@ class _UserInfoCard extends StatelessWidget { ), ], ), - + GradientText( + data:"Ra${userProvider.user?.rating} ", + style: const TextStyle(fontSize: 12, fontWeight: FontWeight.bold), + gradientLayers: [ + GradientLayer( + gradient: const LinearGradient( + colors: [Colors.deepPurple, Colors.pinkAccent], + ), + blendMode: BlendMode.srcIn, + ), + ], + ), + const Divider(height: 5, thickness: 1), const SizedBox(height: 6), Text( userProvider.username == "未登录" @@ -374,6 +509,270 @@ class _UserInfoCard extends StatelessWidget { } } +// ====================== 🎵 修复完成:推荐乐曲组件 ====================== +class _RecommendedSongsSection extends StatelessWidget { + const _RecommendedSongsSection(); + + Future> _loadData(UserProvider userProvider) async { + // 加载歌曲 + 用户数据 + final allSongs = await SongService.getAllSongs(); + List userMusicList = []; + final token = userProvider.token; + + try { + final scoreData = await SongService.getUserAllScores( + token!, + name: userProvider.selectedCnUserName, + ); + if (scoreData.containsKey('userScoreAll_')) { + userMusicList = scoreData['userScoreAll_']['userMusicList'] ?? []; + } else if (scoreData.containsKey('userMusicList')) { + userMusicList = scoreData['userMusicList'] ?? []; + } + } catch (e) { + } + + final int currentRating = userProvider.user?.rating ?? 0; + final int estimatedB35Min = (currentRating / 50).toInt(); + + final recommended = RecommendationHelper.getSmartRecommendations( + allSongs: allSongs, + userMusicList: userMusicList, + userRating: currentRating, // 传入非空 int + b35MinRating: estimatedB35Min, // 传入非空 int + count: 6, + ); + + return { + 'songs': recommended, + }; + } + + @override + Widget build(BuildContext context) { + final userProvider = context.read(); + + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 20), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Row( + children: [ + Icon(Icons.music_note, color: Colors.purpleAccent), + SizedBox(width: 8), + Text( + "为你推荐", + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + ), + ), + ], + ), + const SizedBox(height: 15), + FutureBuilder>( + future: _loadData(userProvider), + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.waiting) { + return const Center(child: CircularProgressIndicator()); + } + if (snapshot.hasError || !snapshot.hasData) { + return const Center(child: Text("加载推荐失败")); + } + + final List songs = snapshot.data!['songs'] ?? []; + if (songs.isEmpty) { + return const Center( + child: Padding( + padding: EdgeInsets.all(20.0), + child: Text("暂无推荐歌曲,快去游玩更多曲目吧!"), + ), + ); + } + + return SizedBox( + height: 200, + child: ListView.builder( + scrollDirection: Axis.horizontal, + itemCount: songs.length, + itemBuilder: (context, index) { + return _SongItemCard(song: songs[index]); + }, + ), + ); + }, + ), + ], + ), + ); + } +} + +class _SongItemCard extends StatelessWidget { + final SongModel song; + + const _SongItemCard({required this.song}); + + 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"; + } else { + return "https://cdn.godserver.cn/resource/static/mai/cover/$displayId.png"; + } + } + + // 获取难度颜色 + 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 + 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), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // ✅ 1. 图片优化:增加 loadingBuilder 和 cacheWidth + ClipRRect( + borderRadius: const BorderRadius.vertical(top: Radius.circular(12)), + child: SizedBox( + height: 120, + 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), + ); + }, + ), + ), + ), + + 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.artist ?? "未知艺术家", + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle(fontSize: 11, color: Colors.grey[600]), + ), + ), + + 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), + ] + ], + ), + ), + ], + ), + ); + } + + Widget _buildLevelTag(String prefix, double level, Color color) { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 2), + decoration: BoxDecoration( + color: color.withOpacity(0.1), + borderRadius: BorderRadius.circular(4), + border: Border.all(color: color.withOpacity(0.5), width: 0.5), + ), + child: Text( + "$prefix ${level.toStringAsFixed(1)}", + style: TextStyle( + fontSize: 10, + fontWeight: FontWeight.bold, + color: color, + ), + ), + ); + } + + // 辅助方法:获取定数 + double? _getLevelValue(SongModel song, int levelIndex) { + 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; + } +} + class PosterImage extends StatelessWidget { final String imageUrl; @@ -385,7 +784,6 @@ class PosterImage extends StatelessWidget { padding: const EdgeInsets.symmetric(horizontal: 20.0), child: ConstrainedBox( constraints: const BoxConstraints( - maxWidth: 800, ), child: Container( decoration: BoxDecoration( @@ -426,5 +824,4 @@ class PosterImage extends StatelessWidget { ), ); } -} - +} \ No newline at end of file diff --git a/lib/pages/user/userpage.dart b/lib/pages/user/userpage.dart index 6441514..4aab21c 100644 --- a/lib/pages/user/userpage.dart +++ b/lib/pages/user/userpage.dart @@ -1,5 +1,4 @@ import 'dart:math' as math; - import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:provider/provider.dart'; @@ -8,6 +7,7 @@ import 'package:dio/dio.dart'; import '../../model/user_model.dart'; import '../../providers/user_provider.dart'; +import '../../service/sega_service.dart'; import 'login_page.dart'; class UserPage extends StatefulWidget { @@ -27,12 +27,10 @@ class _UserPageState extends State { final _lxKeyController = TextEditingController(); static const _pinkColor = Color(0xFFFFC0D6); - // 开关状态 bool _isDisagreeRecommend = false; bool _isDisagreeFriend = false; Map? _radarData; -// 获取雷达图方法 Future _loadRadarData() async { final provider = Provider.of(context, listen: false); try { @@ -53,10 +51,11 @@ class _UserPageState extends State { } } } + @override void initState() { - _loadRadarData; super.initState(); + _loadRadarData(); WidgetsBinding.instance.addPostFrameCallback((_) { final provider = Provider.of(context, listen: false); provider.fetchSexTags(); @@ -75,7 +74,6 @@ class _UserPageState extends State { _dfPwdController.text = user?.dfPassword ?? ''; _lxKeyController.text = user?.lxKey ?? ''; - // 初始化开关 _isDisagreeRecommend = user?.isDisagreeRecommend ?? false; _isDisagreeFriend = user?.isDisagreeFriend ?? false; } @@ -92,7 +90,6 @@ class _UserPageState extends State { super.dispose(); } - // 2. 添加打开链接的方法 Future _launchURL(String urlString) async { final Uri url = Uri.parse(urlString); if (!await launchUrl(url, mode: LaunchMode.externalApplication)) { @@ -108,7 +105,6 @@ class _UserPageState extends State { final username = _dfUserController.text.trim(); final password = _dfPwdController.text.trim(); - // 1. 判空校验 if (username.isEmpty || password.isEmpty) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text("请输入水鱼用户名和密码")), @@ -116,7 +112,6 @@ class _UserPageState extends State { return; } - // 2. 显示加载提示 if (mounted) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( @@ -127,7 +122,6 @@ class _UserPageState extends State { } try { - // 3. 创建Dio实例,发送POST请求 final dio = Dio(); final response = await dio.post( 'https://maimai.diving-fish.com/api/maimaidxprober/login', @@ -135,18 +129,15 @@ class _UserPageState extends State { "username": username, "password": password, }, - // 可选:设置请求超时 options: Options( sendTimeout: const Duration(seconds: 10), receiveTimeout: const Duration(seconds: 10), ), ); - // 4. 解析返回结果 final Map result = response.data; if (mounted) { - // 登录成功 if (result['message'] == "登录成功") { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( @@ -155,9 +146,7 @@ class _UserPageState extends State { duration: Duration(seconds: 1), ), ); - // TODO: 验证成功后的逻辑(保存信息、跳转页面等) } else { - // 账号密码错误 ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text(result['message'] ?? "验证失败"), @@ -168,7 +157,6 @@ class _UserPageState extends State { } } } on DioException catch (e) { - // 5. Dio网络错误处理(无网络、超时、服务器异常) if (mounted) { String errorMsg = "网络请求失败"; if (e.type == DioExceptionType.connectionTimeout) { @@ -187,14 +175,12 @@ class _UserPageState extends State { ); } } catch (e) { - // 其他未知错误 if (mounted) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text("未知错误,请重试"), backgroundColor: Colors.red, duration: Duration(seconds: 1), - ), ); } @@ -213,8 +199,6 @@ class _UserPageState extends State { try { final dio = Dio(); - - // 发起请求 final response = await dio.get( 'https://maimai.lxns.net/api/v0/user/maimai/player', options: Options( @@ -224,7 +208,6 @@ class _UserPageState extends State { ), ); - // ✅ 关键:解析外层响应 + 内层 data final result = LuoxueResponse.fromJson(response.data); if (result.success && mounted) { @@ -233,12 +216,6 @@ class _UserPageState extends State { content: Text("✅ 验证成功!玩家:${result.data.name}\nDX Rating:${result.data.rating}"), ), ); - - // 在这里可以使用完整的玩家数据 - // result.data.name - // result.data.rating - // result.data.friendCode - // result.data.trophy.name } } on DioException catch (e) { String msg = "验证失败"; @@ -303,6 +280,288 @@ class _UserPageState extends State { ); } + void _showSyncProgressDialog(BuildContext context, ValueNotifier status) { + showDialog( + context: context, + barrierDismissible: false, + builder: (ctx) => AlertDialog( + content: ValueListenableBuilder( + valueListenable: status, + builder: (_, text, __) { + return Row( + children: [ + const CircularProgressIndicator(), + const SizedBox(width: 20), + Expanded(child: Text(text)), + ], + ); + }, + ), + ), + ); + } + + Future _verifyBoundSega(SegaCard card) async { + final segaId = card.segaId; + final pwd = card.password; + final type = card.type; + + if (segaId == null || pwd == null || type == null) { + ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text("账号信息不完整"))); + return; + } + + try { + ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text("正在验证 SegaID…"))); + + final region = type == "jp" ? Region.jp : Region.intl; + final data = await SegaService.verifyOnlyLogin( + region: region, + segaId: segaId, + password: pwd, + ); + + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text("✅ 验证成功:${data['user']['name']}"), backgroundColor: Colors.green), + ); + } + } on NetImportError catch (e) { + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text("❌ ${e.message}"), backgroundColor: Colors.red), + ); + } + } catch (e) { + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text("验证失败"))); + } + } + } + + Future _refreshBoundSegaScore(SegaCard card) async { + final segaId = card.segaId; + final pwd = card.password; + final type = card.type; + + if (segaId == null || pwd == null || type == null) { + ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text("账号信息不完整"))); + return; + } + + final statusNotifier = ValueNotifier("准备同步成绩..."); + if (mounted) _showSyncProgressDialog(context, statusNotifier); + + try { + final region = type == "jp" ? Region.jp : Region.intl; + final data = await SegaService.fetchAndSync( + region: region, + segaId: segaId, + password: pwd, + onProgress: (status, percent) { + statusNotifier.value = status; + }, + ); + + // 同步到后端 + final provider = Provider.of(context, listen: false); + await provider.syncSegaScore(data); + + // 关闭弹窗 + if (mounted) Navigator.pop(context); + + // ✅【正确提示:上传成功】 + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text("✅ 成绩同步成功(已上传至服务器)"), + backgroundColor: Colors.green, + ), + ); + } + + } on NetImportError catch (e) { + if (mounted) Navigator.pop(context); + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text("❌ ${e.message}"), backgroundColor: Colors.red), + ); + } + } catch (e) { + if (mounted) Navigator.pop(context); + if (mounted) { + // ❌【上传失败提示】 + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text("❌ 成绩同步失败(服务器上传失败)"), + backgroundColor: Colors.red, + ), + ); + } + } + } + + void _showAddSegaSheet() { + final idController = TextEditingController(); + final pwdController = TextEditingController(); + Region selectedRegion = Region.intl; + + showModalBottomSheet( + context: context, + isScrollControlled: true, + shape: const RoundedRectangleBorder(borderRadius: BorderRadius.vertical(top: Radius.circular(16))), + builder: (ctx) => Padding( + padding: EdgeInsets.fromLTRB(20, 20, 20, MediaQuery.of(ctx).viewInsets.bottom + 20), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const Text("添加 Sega 账号", style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600)), + const SizedBox(height: 16), + TextField( + controller: idController, + decoration: const InputDecoration(labelText: "SegaID", isDense: true, border: OutlineInputBorder()), + ), + const SizedBox(height: 12), + TextField( + controller: pwdController, + obscureText: true, + decoration: const InputDecoration(labelText: "密码", isDense: true, border: OutlineInputBorder()), + ), + const SizedBox(height: 12), + DropdownButtonFormField( + value: selectedRegion, + decoration: const InputDecoration(border: OutlineInputBorder(), isDense: true), + items: const [ + DropdownMenuItem(value: Region.intl, child: Text("国际服")), + DropdownMenuItem(value: Region.jp, child: Text("日服")), + ], + onChanged: (v) { + if (v != null) selectedRegion = v; + }, + ), + const SizedBox(height: 20), + SizedBox( + width: double.infinity, + child: ElevatedButton( + onPressed: () async { + final id = idController.text.trim(); + final pwd = pwdController.text.trim(); + if (id.isEmpty || pwd.isEmpty) { + ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text("请输入完整信息"))); + return; + } + + final newCard = SegaCard( + segaId: id, + password: pwd, + type: selectedRegion.name, + ); + + final provider = Provider.of(context, listen: false); + final user = provider.user; + if (user != null) { + final list = user.segaCards ?? []; + final newList = List.from(list)..add(newCard); + + final 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(const SnackBar(content: Text("✅ 添加成功"))); + Navigator.pop(ctx); + } + }, + child: const Text("保存"), + ), + ), + ], + ), + ), + ); + } + @override Widget build(BuildContext context) { final userProvider = Provider.of(context); @@ -312,11 +571,8 @@ class _UserPageState extends State { return Scaffold( appBar: AppBar(title: const Text("用户中心")), body: Center( - child: Transform.translate( - // Offset(水平, 垂直) - // 垂直向上:负值 - offset: const Offset(0, -40), // 往上移 10 像素 + offset: const Offset(0, -40), child: _buildLoginCard( onTap: () { Navigator.push( @@ -379,14 +635,10 @@ class _UserPageState extends State { _buildRadarChartSection(), const SizedBox(height: 20), - - _buildSaveButton(userProvider), const SizedBox(height: 12), _buildLogoutButton(context, userProvider), const SizedBox(height: 100), - - ], ), ), @@ -396,7 +648,6 @@ class _UserPageState extends State { ); } - // 5. 修改 _buildScoreCheckerCard以添加按钮 Widget _buildScoreCheckerCard(UserModel user) { return _webCard( child: Column( @@ -408,7 +659,6 @@ class _UserPageState extends State { ), const SizedBox(height: 16), - // --- 水鱼部分 --- const Text("水鱼查分器", style: TextStyle(fontSize: 15, fontWeight: FontWeight.w500)), const SizedBox(height: 8), TextField( @@ -430,7 +680,6 @@ class _UserPageState extends State { ), ), const SizedBox(height: 8), - // 水鱼操作按钮行 Row( mainAxisAlignment: MainAxisAlignment.end, children: [ @@ -443,14 +692,13 @@ class _UserPageState extends State { TextButton.icon( icon: const Icon(Icons.open_in_new, size: 18), label: const Text("跳转水鱼官网"), - onPressed: () => _launchURL("https://www.diving-fish.com/"), // 替换为实际水鱼官网地址 + onPressed: () => _launchURL("https://www.diving-fish.com/"), ), ], ), const SizedBox(height: 16), - // --- 落雪部分 --- const Text("落雪查分器", style: TextStyle(fontSize: 15, fontWeight: FontWeight.w500)), const SizedBox(height: 8), TextField( @@ -462,7 +710,6 @@ class _UserPageState extends State { ), ), const SizedBox(height: 8), - // 落雪操作按钮行 Row( mainAxisAlignment: MainAxisAlignment.end, children: [ @@ -475,7 +722,7 @@ class _UserPageState extends State { TextButton.icon( icon: const Icon(Icons.open_in_new, size: 18), label: const Text("跳转落雪官网"), - onPressed: () => _launchURL("https://maimai.lxns.net/"), // 替换为实际落雪官网地址 + onPressed: () => _launchURL("https://maimai.lxns.net/"), ), ], ), @@ -525,9 +772,9 @@ class _UserPageState extends State { color: Colors.pinkAccent, borderRadius: BorderRadius.circular(4), ), - child: Text( - "舞萌账号" , - style: const TextStyle(color: Colors.white, fontSize: 12), + child: const Text( + "舞萌账号", + style: TextStyle(color: Colors.white, fontSize: 12), ), ), const SizedBox(width: 10), @@ -535,7 +782,7 @@ class _UserPageState extends State { ], ), ); - }) + }) ], ), ); @@ -712,7 +959,7 @@ class _UserPageState extends State { image: NetworkImage( "https://cdn.godserver.cn/resource/static/coll/Chara/UI_Chara_$charaId.png", ), - fit: BoxFit.cover, + fit:BoxFit.cover, colorFilter: ColorFilter.mode( Colors.black.withOpacity(0.4), BlendMode.darken), ), @@ -799,7 +1046,6 @@ class _UserPageState extends State { friendBio: user.friendBio, sex: _sexController.text.trim(), - // 开关已正确保存 isDisagreeRecommend: _isDisagreeRecommend, isDisagreeFriend: _isDisagreeFriend, @@ -876,13 +1122,16 @@ class _UserPageState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - const Text("SegaID 账号", - style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600)), + const Text( + "SegaID 账号", + style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600), + ), const SizedBox(height: 10), + if (user.segaCards != null && user.segaCards!.isNotEmpty) - ...user.segaCards!.map((card) { + ...user.segaCards!.map((SegaCard card) { return Padding( - padding: const EdgeInsets.only(bottom: 8), + padding: const EdgeInsets.only(bottom: 12), child: Row( children: [ Container( @@ -897,13 +1146,38 @@ class _UserPageState extends State { ), ), const SizedBox(width: 10), - Text(card.segaId ?? ""), + Expanded(child: Text(card.segaId ?? "")), + const SizedBox(width: 6), + TextButton( + onPressed: () => _verifyBoundSega(card), + child: const Text("验证", style: TextStyle(fontSize: 12)), + ), + const SizedBox(width: 4), + TextButton( + onPressed: () => _refreshBoundSegaScore(card), + child: const Text("刷新成绩", style: TextStyle(fontSize: 12)), + ), ], ), ); - }), + }).toList(), + if (user.segaCards == null || user.segaCards!.isEmpty) - const Text("暂无 SegaID"), + const Padding( + padding: EdgeInsets.symmetric(vertical: 8), + child: Text("暂无 SegaID"), + ), + + const SizedBox(height: 12), + + SizedBox( + width: double.infinity, + child: OutlinedButton.icon( + onPressed: _showAddSegaSheet, + icon: const Icon(Icons.add, size: 18), + label: const Text("添加 Sega 账号"), + ), + ), ], ), ); @@ -1055,7 +1329,6 @@ class _UserPageState extends State { child: Column( mainAxisSize: MainAxisSize.min, children: [ - // 1. 头部图标与标题 Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( @@ -1089,7 +1362,6 @@ class _UserPageState extends State { const SizedBox(height: 24), - // 2. 登录好处列表 (Value Proposition) Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( @@ -1125,7 +1397,6 @@ class _UserPageState extends State { const SizedBox(height: 24), - // 3. 登录按钮 SizedBox( width: double.infinity, height: 50, @@ -1159,7 +1430,6 @@ class _UserPageState extends State { ); } - // 辅助方法:构建单个权益项 Widget _buildBenefitItem({ required IconData icon, required String title, @@ -1212,17 +1482,15 @@ class _UserPageState extends State { child: const Text("点击加载雷达图数据"), ) else - // 使用我们自定义的雷达图 SizedBox( height: 300, width: double.infinity, child: CustomRadarChart( - // 传入你的数据,确保 Map data: _radarData!.map((key, value) => MapEntry(key.toString(), double.tryParse(value.toString()) ?? 0.0) ), - maxValue: 1.3, // 强制最大值为 1.5,不再自动缩放 - lineColor: Colors.grey.shade200, // 极淡的网格线,视觉上接近“无” + maxValue: 1.3, + lineColor: Colors.grey.shade200, areaColor: Colors.pink.withOpacity(0.15), borderColor: Colors.pinkAccent, ), @@ -1230,9 +1498,6 @@ class _UserPageState extends State { const SizedBox(height: 20), - // ———————————————————————————————— - // 下面的列表保持不变 - // ———————————————————————————————— if (_radarData != null) GridView.count( shrinkWrap: true, @@ -1260,12 +1525,11 @@ class _UserPageState extends State { children: [ Text( key, - style: const TextStyle(fontSize: 12, - fontWeight: FontWeight.w500), + style: const TextStyle(fontSize: 12, fontWeight: FontWeight.w500), ), Text( value.toStringAsFixed(2), - style: TextStyle( + style: const TextStyle( fontSize: 12, fontWeight: FontWeight.bold, ), diff --git a/lib/providers/user_provider.dart b/lib/providers/user_provider.dart index 743a96b..034aecd 100644 --- a/lib/providers/user_provider.dart +++ b/lib/providers/user_provider.dart @@ -193,4 +193,32 @@ class UserProvider with ChangeNotifier { } notifyListeners(); } + + Future syncSegaScore(Map data) async { + if (_token == null || _user == null) { + throw "请先登录"; + } + if (_selectedSegaId == null || _selectedSegaId!.isEmpty) { + throw "请先选择要同步的 SegaID"; + } + + try { + // 调用上传 + final result = await UserService.uploadSegaRating( + _token!, + _selectedSegaId!, + data, + ); + + if (result["code"] == 200) { + print("✅ 同步成功:${result["msg"]}"); + } else { + print("❌ 同步失败:${result["msg"]}"); + throw result["msg"] ?? "同步失败"; + } + } catch (e) { + print("❌ 同步异常:$e"); + rethrow; + } + } } \ No newline at end of file diff --git a/lib/service/recommendation_helper.dart b/lib/service/recommendation_helper.dart index e69de29..52a802e 100644 --- a/lib/service/recommendation_helper.dart +++ b/lib/service/recommendation_helper.dart @@ -0,0 +1,293 @@ +import 'dart:math'; +import '../model/song_model.dart'; + +class RecommendationHelper { + + /// 基于 Java MusicService 逻辑的智能推荐 + /// + /// [allSongs]: 全量歌曲库 (SongModel) + /// [userMusicList]: 用户成绩列表 (dynamic) + /// - 情况A: List,每个 Map 包含 musicId, level, achievement, dx_rating + /// - 情况B: List,每个 Map 包含 userMusicDetailList (嵌套结构) + /// [userRating]: 用户当前总 Rating (DX Rating),用于确定推荐难度区间 + /// [b35MinRating]: 用户当前 B35 中最低的那首曲子的 Rating (用于判断推分是否有意义) + /// [count]: 推荐数量 + static List getSmartRecommendations({ + required List allSongs, + required List userMusicList, + required int userRating, + required int b35MinRating, + int count = 6, + }) { + if (allSongs.isEmpty) return []; + + final random = Random(); + + // 1. 解析用户成绩,构建映射: Key = "musicId_level", Value = { achievement, rating } + // 使用 HashMap 提高查找速度 + final Map> playedMap = {}; + + for (var group in userMusicList) { + if (group is! Map) continue; + + List details = []; + + // 兼容两种数据结构: + if (group.containsKey('userMusicDetailList')) { + details = group['userMusicDetailList'] as List? ?? []; + } else if (group.containsKey('achievement') || group.containsKey('dx_rating')) { + // 如果当前 group 本身就是成绩对象 + details = [group]; + } + + if (details.isEmpty) continue; + + for (var detail in details) { + if (detail is! Map) continue; + + // 提取关键字段 + 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 + final dynamic rawRating = detail['dx_rating'] ?? detail['rating'] ?? 0; + final double rating = rawRating is num ? rawRating.toDouble() : 0.0; + + if (musicId == 0) continue; + + String key = "${musicId}_${level}"; + + // 只保留该难度下的最高成绩 + if (!playedMap.containsKey(key) || achievement > (playedMap[key]!['achievement'] as int)) { + playedMap[key] = { + 'musicId': musicId, + 'level': level, + 'achievement': achievement, + 'rating': rating, + }; + } + } + } + + // 2. 计算推荐难度区间 (基于 Java logic) + double minRecommendedLevel = 0.0; + + if (userRating >= 15500) { + minRecommendedLevel = 13.6; + } else if (userRating >= 15000) { + minRecommendedLevel = 13.0; + } else if (userRating >= 14500) { + minRecommendedLevel = 12.5; + } else if (userRating >= 14000) { + minRecommendedLevel = 12.0; + } else if (userRating >= 13000) { + minRecommendedLevel = 11.5; + } else if (userRating >= 12000) { + minRecommendedLevel = 11.0; + } else if (userRating >= 11000) { + minRecommendedLevel = 10.5; + } else { + minRecommendedLevel = 9.0; // 新手保护线 + } + + List candidatesForImprovement = []; // 用于推分 + List candidatesNew = []; // 新曲/未玩 + + // 用于快速判断是否已加入结果集,避免 O(N^2) 的 contains 检查 + final Set addedSongIds = {}; + + for (var song in allSongs) { + // 过滤无效 ID + if (song.id < 100) continue; + + // 获取 Master (Level 3) 的定数,如果没有则获取 Expert (Level 2) + double? masterLevel = _getSongLevel(song, 3); + double? expertLevel = _getSongLevel(song, 2); + + double? targetLevel; + int targetLevelIndex = 3; // 默认优先推荐 Master + + if (masterLevel != null) { + targetLevel = masterLevel; + } else if (expertLevel != null) { + targetLevel = expertLevel; + targetLevelIndex = 2; + } else { + continue; // 没有可用难度数据 + } + + // --- 检查是否已游玩 --- + bool isPlayed = false; + Map? bestChart; + + // 尝试直接匹配 (SD 或 基础ID) + String keyDirect = "${song.id}_$targetLevelIndex"; + if (playedMap.containsKey(keyDirect)) { + isPlayed = true; + bestChart = playedMap[keyDirect]; + } + // 尝试匹配 DX 偏移 (DX 歌曲 ID 通常 +10000) + else { + int dxId = song.id + 10000; + String keyDx = "${dxId}_$targetLevelIndex"; + if (playedMap.containsKey(keyDx)) { + isPlayed = true; + bestChart = playedMap[keyDx]; + } + } + + if (isPlayed && bestChart != null) { + // --- 策略 A: 推分逻辑 (Improvement) --- + int currentAch = bestChart['achievement'] as int; + + // 如果已经 AP+ (100.5%),跳过 + if (currentAch >= 1005000) continue; + + // 计算下一个档位的目标达成率 + double nextTargetAch = 0; + const List targets = [97.0, 98.0, 99.0, 99.5, 100.0, 100.5]; + + double currentPercent = currentAch / 10000.0; + for (double t in targets) { + if (currentPercent < t) { + nextTargetAch = t; + break; + } + } + + if (nextTargetAch == 0) continue; + + // 计算目标 Rating + int targetRating = _calculateRating(targetLevel!, nextTargetAch); + + // 核心判断:如果推分后的 Rating 大于当前 B35 最低分,则值得推荐 + if (targetRating > b35MinRating) { + candidatesForImprovement.add(song); + } + + } else { + // --- 策略 B: 新曲逻辑 (New) --- + // 判断定数是否在用户能力范围内 + if (targetLevel != null && targetLevel >= minRecommendedLevel && targetLevel <= 15.0) { + candidatesNew.add(song); + } + } + } + + // 3. 混合推荐结果 + List result = []; + + // 60% 来自“提升空间大”的曲子 + candidatesForImprovement.shuffle(random); + int improveCount = (count * 0.6).round(); + for (int i = 0; i < improveCount && i < candidatesForImprovement.length; i++) { + final song = candidatesForImprovement[i]; + result.add(song); + addedSongIds.add(song.id); + } + + // 40% 来自“新曲/潜力曲” + candidatesNew.shuffle(random); + int remaining = count - result.length; + for (int i = 0; i < remaining && i < candidatesNew.length; i++) { + final song = candidatesNew[i]; + // 避免重复添加(虽然逻辑上 New 和 Improve 不重叠,但防万一) + if (!addedSongIds.contains(song.id)) { + result.add(song); + addedSongIds.add(song.id); + } + } + + // 如果不够,用纯随机补齐 + if (result.length < count) { + // 复制一份并打乱,避免修改原数组 + List fallback = List.from(allSongs); + fallback.shuffle(random); + + for (var s in fallback) { + if (result.length >= count) break; + // 使用 Set 进行 O(1) 查找,避免 List.contains 的 O(N) 查找 + if (!addedSongIds.contains(s.id)) { + result.add(s); + addedSongIds.add(s.id); + } + } + } + + return result.take(count).toList(); + } + + /// 辅助:获取歌曲指定难度的定数 (Level Value) + static double? _getSongLevel(SongModel song, int levelIndex) { + // levelIndex: 0=Basic, 1=Advanced, 2=Expert, 3=Master, 4=Re:Master + + // 1. 尝试从 SD (Standard) 获取 + if (song.sd != null && song.sd is Map) { + final sdMap = song.sd as Map; + // 键可能是字符串 "3" 或整数 3,这里做兼容 + var data = sdMap["$levelIndex"] ?? sdMap[levelIndex]; + if (data is Map && data.containsKey("level_value")) { + final val = data["level_value"]; + if (val is num) return val.toDouble(); + } + } + + // 2. 尝试从 DX 获取 + if (song.dx != null && song.dx is Map) { + final dxMap = song.dx as Map; + var data = dxMap["$levelIndex"] ?? dxMap[levelIndex]; + if (data is Map && data.containsKey("level_value")) { + final val = data["level_value"]; + if (val is num) return val.toDouble(); + } + } + return null; + } + + /// 辅助:根据定数和达成率计算 Rating (完全复刻 Java getRatingChart) + static int _calculateRating(double diff, double achievementPercent) { + double sys = 22.4; + double ach = achievementPercent; // 例如 99.5 + + 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; + + // Java: (int) (diff * sys * achievement / 100) + return (diff * sys * ach / 100).floor(); + } +} \ No newline at end of file diff --git a/lib/service/sega_service.dart b/lib/service/sega_service.dart new file mode 100644 index 0000000..7b1b581 --- /dev/null +++ b/lib/service/sega_service.dart @@ -0,0 +1,701 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; +import 'package:dio/dio.dart'; +import 'package:dio/io.dart'; +import 'package:html/parser.dart' as html_parser; +import 'package:html/dom.dart' as dom; + +typedef void OnProgress(String status, double? percent); + +class SegaService { + static Future> fetchAndSync({ + required Region region, + required String segaId, + required String password, + OnProgress? onProgress, + }) async { + print("[SegaService] fetchAndSync 开始执行,区域:${region.name}"); + print("[SegaService] SegaID:$segaId"); + + final MaimaiNetClient client; + if (region == Region.jp) { + client = MaimaiNETJpClient(); + print("[SegaService] 使用日服客户端"); + } else { + client = MaimaiNETIntlClient(); + print("[SegaService] 使用国际服客户端"); + } + + try { + onProgress?.call("正在登录 Sega 账号...", null); + await client.login(AuthParams(id: segaId, password: password)); + print("[SegaService] 登录成功"); + + onProgress?.call("登录成功,获取用户信息...", null); + final userInfo = await client.fetchUserInfo(); + print("[SegaService] 用户信息获取成功:${userInfo.name}"); + + onProgress?.call("开始拉取歌曲成绩...", null); + final musicRecords = await client.fetchMusicRecords(onProgress: onProgress); + print("[SegaService] 歌曲成绩获取完成,总数:${musicRecords.length}"); + + return { + "user": userInfo.toJson(), + "music": musicRecords.map((e) => e.toJson()).toList(), + "region": region.name, + }; + } catch (e) { + print("[SegaService] fetchAndSync 发生错误:$e"); + rethrow; + } + } + + static Future> verifyOnlyLogin({ + required Region region, + required String segaId, + required String password, + }) async { + print("[SegaService] verifyOnlyLogin 开始验证登录,区域:${region.name}"); + print("[SegaService] 验证账号:$segaId"); + + final MaimaiNetClient client; + if (region == Region.jp) { + client = MaimaiNETJpClient(); + } else { + client = MaimaiNETIntlClient(); + } + try { + await client.login(AuthParams(id: segaId, password: password)); + print("[SegaService] 验证登录成功"); + final userInfo = await client.fetchUserInfo(); + print("[SegaService] 验证成功,用户名:${userInfo.name}"); + + return { + "user": userInfo.toJson(), + "music": [], + "region": region.name, + }; + } catch (e) { + print("[SegaService] 验证登录失败:$e"); + rethrow; + } + } +} + +enum Region { jp, intl } + +class NetImportError implements Exception { + final String code; + final String message; + NetImportError(this.code, [String? msg]) : message = msg ?? code; + @override + String toString() => 'NetImportError($code): $message'; +} + +class AuthParams { + final String id; + final String password; + AuthParams({required this.id, required this.password}); +} + +class SheetInfo { + final String songId; + final String type; + final String difficulty; + + SheetInfo({required this.songId, required this.type, required this.difficulty}); + + Map toJson() => { + 'songId': songId, + 'type': type, + 'difficulty': difficulty, + }; +} + +class DxScore { + final int achieved; + final int total; + DxScore({required this.achieved, required this.total}); + + Map toJson() => { + 'achieved': achieved, + 'total': total, + }; +} + +class Achievement { + final int rate; + final DxScore dxScore; + final List flags; + + Achievement({required this.rate, required this.dxScore, required this.flags}); + + Map toJson() => { + 'rate': rate, + 'dxScore': dxScore.toJson(), + 'flags': flags, + }; +} + +class MusicRecord { + final SheetInfo sheet; + final Achievement achievement; + + MusicRecord({required this.sheet, required this.achievement}); + + Map toJson() => { + 'sheet': sheet.toJson(), + 'achievement': achievement.toJson(), + }; +} + +class UserInfo { + final String avatar; + final String name; + final Map trophy; + final String courseIcon; + final String classIcon; + final String plateUrl; + + UserInfo({ + required this.avatar, + required this.name, + required this.trophy, + required this.courseIcon, + required this.classIcon, + required this.plateUrl, + }); + + Map toJson() => { + 'avatar': avatar, + 'name': name, + 'trophy': trophy, + 'courseIcon': courseIcon, + 'classIcon': classIcon, + 'plateUrl': plateUrl, + }; +} + +class URLs { + static const Map INTL = { + "LOGIN_PAGE": "https://lng-tgk-aime-gw.am-all.net/common_auth/login?site_id=maimaidxex&redirect_url=https://maimaidx-eng.com/maimai-mobile/&back_url=https://maima.sega.com/", + "LOGIN_ENDPOINT": "https://lng-tgk-aime-gw.am-all.net/common_auth/login/sid", + "RECORD_RECENT_PAGE": "https://maimaidx-eng.com/maimai-mobile/record", + "RECORD_MUSICS_PAGE": "https://maimaidx-eng.com/maimai-mobile/record/musicGenre/search/", + "HOME": "https://maimaidx-eng.com/maimai-mobile/home/", + "NAMEPLATE": "https://maimaidx-eng.com/maimai-mobile/collection/nameplate/", + }; + + static const Map JP = { + "LOGIN_PAGE": "https://maimaidx.jp/maimai-mobile/", + "LOGIN_ENDPOINT": "https://maimaidx.jp/maimai-mobile/submit/", + "LOGIN_AIMELIST": "https://maimaidx.jp/maimai-mobile/aimeList/", + "LOGIN_AIMELIST_SUBMIT": "https://maimaidx.jp/maimai-mobile/aimeList/submit/?idx=0", + "HOMEPAGE": "https://maimaidx.jp/maimai-mobile/home/", + "RECORD_RECENT_PAGE": "https://maimaidx.jp/maimai-mobile/record", + "RECORD_MUSICS_PAGE": "https://maimaidx.jp/maimai-mobile/record/musicGenre/search/", + "NAMEPLATE": "https://maimaidx.jp/maimai-mobile/collection/nameplate/", + }; + + static const List ERROR_URLS = [ + "https://maimaidx-eng.com/maimai-mobile/error/", + "https://maimaidx.jp/maimai-mobile/error/" + ]; + + static const List MAINTENANCE_TEXTS = [ + "定期メンテナンス中です", + "Sorry, servers are under maintenance." + ]; +} + +abstract class MaimaiNetClient { + late Dio dio; + late Map urls; + + final Map _cookies = {}; + + MaimaiNetClient() { + dio = Dio(); + _setupDio(); + print("[MaimaiNetClient] 客户端初始化完成"); + } + + void _setupDio() { + print("[MaimaiNetClient] 配置 Dio"); + + (dio.httpClientAdapter as IOHttpClientAdapter).createHttpClient = () { + final client = HttpClient(); + client.badCertificateCallback = (X509Certificate cert, String host, int port) => true; + return client; + }; + + dio.interceptors.add(InterceptorsWrapper( + onRequest: (options, handler) { + print("[DIO] 请求:${options.method} ${options.uri}"); + if (_cookies.isNotEmpty) { + final cookieStr = _cookies.entries.map((e) => '${e.key}=${e.value}').join('; '); + options.headers['Cookie'] = cookieStr; + } + return handler.next(options); + }, + onResponse: (response, handler) { + print("[DIO] 响应:${response.statusCode} ${response.requestOptions.uri}"); + final setCookies = response.headers['set-cookie']; + if (setCookies != null) { + for (var cookieHeader in setCookies) { + if (cookieHeader.contains('=')) { + final parts = cookieHeader.split(';')[0].split('='); + if (parts.length >= 2) { + final key = parts[0].trim(); + final value = parts.sublist(1).join('=').trim(); + if (key.isNotEmpty) { + _cookies[key] = value; + } + } + } + } + } + return handler.next(response); + }, + onError: (DioException e, handler) { + print("[DIO] 请求错误:$e"); + return handler.next(e); + })); + + dio.options.headers = { + "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8", + "Accept-Language": "ja;q=0.9,en;q=0.8", + "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36", + "Sec-Fetch-Dest": "document", + "Sec-Fetch-Mode": "navigate", + "Upgrade-Insecure-Requests": "1", + }; + + dio.options.connectTimeout = const Duration(seconds: 10); + dio.options.receiveTimeout = const Duration(seconds: 10); + dio.options.followRedirects = false; + dio.options.maxRedirects = 0; + dio.options.validateStatus = (status) => true; + } + + Future fetchAsSoupWithRedirects(String url, {Map? queryParameters}) async { + print("[fetchAsSoup] 开始请求(支持重定向):$url 参数:$queryParameters"); + + String currentUrl = url; + int redirectCount = 0; + const maxRedirects = 5; + + while (redirectCount < maxRedirects) { + try { + final response = await dio.get(currentUrl, queryParameters: queryParameters); + + if (response.data is String) { + checkMaintenance(response.data.toString()); + } + + if ([301, 302, 303, 307, 308].contains(response.statusCode)) { + final location = response.headers.value('location'); + if (location != null) { + print("[fetchAsSoup] 重定向 -> $location"); + currentUrl = location; + redirectCount++; + continue; + } else { + print("[fetchAsSoup] 重定向无 location"); + throw NetImportError("REDIRECT_WITHOUT_LOCATION"); + } + } + + if (response.statusCode! >= 400) { + print("[fetchAsSoup] HTTP 错误:${response.statusCode}"); + throw NetImportError("HTTP_ERROR_${response.statusCode}", "Failed to fetch $url"); + } + + print("[fetchAsSoup] 请求完成,解析 HTML"); + return html_parser.parse(response.data); + } on DioException catch (e) { + print("[fetchAsSoup] Dio 异常:$e"); + rethrow; + } + } + print("[fetchAsSoup] 重定向次数过多"); + throw NetImportError("TOO_MANY_REDIRECTS"); + } + + Future fetchAsSoup(String url, {Map? queryParameters}) async { + return fetchAsSoupWithRedirects(url, queryParameters: queryParameters); + } + + void checkMaintenance(String text) { + for (final maintText in URLs.MAINTENANCE_TEXTS) { + if (text.contains(maintText)) { + print("[检查维护] 检测到服务器维护!"); + throw NetImportError("NET_MAINTENANCE"); + } + } + } + + Future login(AuthParams auth); + Future fetchUserInfo(); + Future> fetchMusicRecords({OnProgress? onProgress}); + + SheetInfo? parseSheetInfo(dom.Element div) { + final songIdEl = div.querySelector(".music_name_block"); + if (songIdEl == null) return null; + final songId = songIdEl.text.trim(); + + String? type; + if (div.querySelector(".music_kind_icon_dx._btn_on") != null) { + type = "dx"; + } else if (div.querySelector(".music_kind_icon_standard._btn_on") != null) { + type = "standard"; + } else { + final typeIcon = div.querySelector(".music_kind_icon"); + if (typeIcon != null) { + final src = typeIcon.attributes['src'] ?? ''; + if (src.contains('music_dx.png')) type = 'dx'; + else if (src.contains('music_standard.png')) type = 'standard'; + } + } + + String? difficulty; + final diffIcon = div.querySelector(".h_20.f_l"); + if (diffIcon != null) { + final src = diffIcon.attributes['src'] ?? ''; + final match = RegExp(r'diff_(.*)\.png').firstMatch(src); + if (match != null) { + difficulty = match.group(1); + } + } + + if (difficulty == "utage") type = "utage"; + if (songId.isEmpty || type == null || difficulty == null) return null; + + return SheetInfo(songId: songId, type: type, difficulty: difficulty); + } + + Achievement parseAchievement(dom.Element div) { + final rateEl = div.querySelector(".music_score_block.w_112"); + final rateStr = rateEl?.text.trim().replaceAll('%', '').replaceAll('.', '') ?? '0'; + final rate = int.tryParse(rateStr) ?? 0; + + final dxScoreEl = div.querySelector(".music_score_block.w_190"); + final dxScoreText = dxScoreEl?.text.trim() ?? "0 / 0"; + final parts = dxScoreText.split('/'); + final achieved = int.tryParse(parts[0].trim().replaceAll(',', '')) ?? 0; + final total = parts.length > 1 ? (int.tryParse(parts[1].trim().replaceAll(',', '')) ?? 0) : 0; + + final flags = []; + final flagImages = div.querySelectorAll("form img.f_r"); + final flagMatchers = { + "fullCombo": "fc.png", + "fullCombo+": "fcplus.png", + "allPerfect": "ap.png", + "allPerfect+": "applus.png", + "syncPlay": "sync.png", + "fullSync": "fs.png", + "fullSync+": "fsplus.png", + "fullSyncDX": "fsd.png", + "fullSyncDX+": "fsdplus.png", + }; + + for (final img in flagImages) { + final src = img.attributes['src'] ?? ''; + for (final entry in flagMatchers.entries) { + if (src.contains(entry.value)) { + flags.add(entry.key); + break; + } + } + } + + return Achievement( + rate: rate, + dxScore: DxScore(achieved: achieved, total: total), + flags: flags, + ); + } +} + +class MaimaiNETJpClient extends MaimaiNetClient { + MaimaiNETJpClient() { + urls = URLs.JP; + print("[MaimaiNETJpClient] 日服客户端初始化"); + } + + @override + Future login(AuthParams auth) async { + print("[日服客户端] 开始登录流程"); + + final soup = await fetchAsSoup(urls["LOGIN_PAGE"]!); + final tokenInput = soup.querySelector("input[name='token']"); + if (tokenInput == null) { + print("[日服客户端] 未找到 token,登录失败"); + throw NetImportError("TOKEN_ERROR"); + } + final token = tokenInput.attributes['value']; + print("[日服客户端] 获取 token 成功:$token"); + + final loginData = { + "segaId": auth.id, + "password": auth.password, + "save_cookie": "on", + "token": token!, + }; + + final response = await dio.post( + urls["LOGIN_ENDPOINT"]!, + data: FormData.fromMap(loginData), + options: Options( + contentType: Headers.formUrlEncodedContentType, + ), + ); + + final location = response.headers.value('location'); + if (location != null && URLs.ERROR_URLS.any((e) => location.contains(e))) { + print("[日服客户端] 账号或密码错误"); + throw NetImportError("INVALID_CREDENTIALS"); + } + + print("[日服客户端] 登录接口请求成功,处理跳转"); + await fetchAsSoup(urls["LOGIN_AIMELIST"]!); + await fetchAsSoup(urls["LOGIN_AIMELIST_SUBMIT"]!); + await fetchAsSoup(urls["HOMEPAGE"]!); + print("[日服客户端] 登录流程全部完成"); + } + + @override + Future fetchUserInfo() async { + print("[日服客户端] 获取用户信息"); + final soup = await fetchAsSoup(urls["HOMEPAGE"]!); + final avatarImg = soup.querySelector('img[loading="lazy"].w_112.f_l'); + final avatar = avatarImg?.attributes['src'] ?? ''; + final nameEl = soup.querySelector(".name_block"); + final name = nameEl?.text.trim() ?? ''; + + String trophyClass = "Normal"; + String trophyName = ""; + final trophyBlock = soup.querySelector("div.trophy_block"); + if (trophyBlock != null) { + for (final c in trophyBlock.classes) { + if (["trophy_Normal", "trophy_Silver", "trophy_Gold", "trophy_Rainbow"].contains(c)) { + trophyClass = c.replaceFirst('trophy_', ''); + break; + } + } + final trophyInner = trophyBlock.querySelector(".trophy_inner_block span"); + if (trophyInner != null) trophyName = trophyInner.text.trim(); + } + + final courseImg = soup.querySelector('img[src*="course_rank_"]'); + final courseIcon = courseImg?.attributes['src'] ?? ''; + final classImg = soup.querySelector('img[src*="class_rank_"]'); + final classIcon = classImg?.attributes['src'] ?? ''; + + String plateUrl = ""; + try { + print("[日服客户端] 获取铭牌"); + final plateSoup = await fetchAsSoup(urls["NAMEPLATE"]!); + final plateImg = plateSoup.querySelector('img[loading="lazy"].w_396.m_r_10'); + if (plateImg != null) plateUrl = plateImg.attributes['src'] ?? ''; + } catch (e) { + print("[日服客户端] 获取铭牌失败:$e"); + } + + print("[日服客户端] 用户信息解析完成:$name"); + return UserInfo( + avatar: avatar, + name: name, + trophy: {'rarity': trophyClass, 'text': trophyName}, + courseIcon: courseIcon, + classIcon: classIcon, + plateUrl: plateUrl, + ); + } + + // ====================== 【修复点】只拉一遍,不统计 ====================== + @override + Future> fetchMusicRecords({OnProgress? onProgress}) async { + print("[日服客户端] 开始获取所有歌曲成绩(修复版:仅拉取1次)"); + final records = []; + + final difficulties = [ + ("0", "Basic"), + ("1", "Advanced"), + ("2", "Expert"), + ("3", "Master"), + ("4", "Remaster"), + ("10", "Utage"), + ]; + + for (final diff in difficulties) { + try { + onProgress?.call("正在拉取:${diff.$2}", null); + final url = urls["RECORD_MUSICS_PAGE"]!; + final soup = await fetchAsSoup(url, queryParameters: {"genre": "99", "diff": diff.$1}); + final items = soup.querySelectorAll(".w_450.m_15.p_r.f_0"); + + for (final item in items) { + final sheet = parseSheetInfo(item); + if (sheet != null) { + final achievement = parseAchievement(item); + records.add(MusicRecord(sheet: sheet, achievement: achievement)); + } + } + print("[日服客户端] ${diff.$2} 完成,累计:${records.length} 首"); + } catch (e) { + print("[日服客户端] ${diff.$2} 拉取失败:$e"); + } + } + + print("[日服客户端] 所有成绩解析完成,总数:${records.length}"); + return records; + } +} + +class MaimaiNETIntlClient extends MaimaiNetClient { + MaimaiNETIntlClient() { + urls = URLs.INTL; + dio.options.headers["Referer"] = URLs.INTL["LOGIN_PAGE"]; + print("[MaimaiNETIntlClient] 国际服客户端初始化"); + } + + @override + Future login(AuthParams auth) async { + print("[国际服客户端] 开始登录流程"); + + await fetchAsSoup(urls["LOGIN_PAGE"]!); + print("[国际服客户端] 访问登录页面成功"); + + final loginData = { + "sid": auth.id, + "password": auth.password, + "retention": "1", + }; + + final response = await dio.post( + urls["LOGIN_ENDPOINT"]!, + data: FormData.fromMap(loginData), + options: Options( + contentType: Headers.formUrlEncodedContentType, + ), + ); + + final location = response.headers.value('location'); + if (location != null) { + print("[国际服客户端] 登录后跳转:$location"); + await fetchAsSoupWithRedirects(location); + } + await fetchAsSoup(urls["HOME"]!); + print("[国际服客户端] 登录流程完成"); + } + + @override + Future fetchUserInfo() async { + print("[国际服客户端] 获取用户信息"); + final soup = await fetchAsSoup(urls["HOME"]!); + final avatarImg = soup.querySelector('img[loading="lazy"].w_112'); + final avatar = avatarImg?.attributes['src'] ?? ''; + final nameEl = soup.querySelector(".name_block"); + final name = nameEl?.text.trim() ?? ''; + + String trophyClass = "Normal"; + String trophyName = ""; + final trophyEl = soup.querySelector("div.trophy_block"); + if (trophyEl != null) { + for (final c in trophyEl.classes) { + if (["trophy_Normal", "trophy_Silver", "trophy_Gold", "trophy_Rainbow"].contains(c)) { + trophyClass = c.replaceFirst('trophy_', ''); + break; + } + } + final trophyInner = trophyEl.querySelector(".trophy_inner_block span"); + if (trophyInner != null) trophyName = trophyInner.text.trim(); + } + + final courseImg = soup.querySelector('img[src*="course/course_rank_"]'); + final courseIcon = courseImg?.attributes['src'] ?? ''; + final classImg = soup.querySelector('img[src*="class/class_rank_"]'); + final classIcon = classImg?.attributes['src'] ?? ''; + + String plateUrl = ""; + try { + print("[国际服客户端] 获取铭牌"); + final plateSoup = await fetchAsSoup(urls["NAMEPLATE"]!); + final plateImg = plateSoup.querySelector('img[loading="lazy"].w_396.m_r_10'); + if (plateImg != null) plateUrl = plateImg.attributes['src'] ?? ''; + } catch (e) { + print("[国际服客户端] 获取铭牌失败:$e"); + } + + print("[国际服客户端] 用户信息解析完成:$name"); + return UserInfo( + avatar: avatar, + name: name, + trophy: {'rarity': trophyClass, 'text': trophyName}, + courseIcon: courseIcon, + classIcon: classIcon, + plateUrl: plateUrl, + ); + } + + @override + Future> fetchMusicRecords({OnProgress? onProgress}) async { + print("[国际服客户端] 开始获取所有歌曲成绩"); + final difficulties = [ + ("0", "Basic"), + ("1", "Advanced"), + ("2", "Expert"), + ("3", "Master"), + ("4", "Remaster"), + ("10", "Utage"), + ]; + + int total = 0; + int current = 0; + + final futures = difficulties.map((diff) async { + onProgress?.call("正在并发拉取难度..", null); + final url = urls["RECORD_MUSICS_PAGE"]!; + final soup = await fetchAsSoup(url, queryParameters: {"genre": "99", "diff": diff.$1}); + final items = soup.querySelectorAll(".w_450.m_15.p_r.f_0"); + final list = []; + + for (final item in items) { + final sheet = parseSheetInfo(item); + if (sheet != null) { + final achievement = parseAchievement(item); + list.add(MusicRecord(sheet: sheet, achievement: achievement)); + } + current++; + } + return list; + }); + + final results = await Future.wait(futures); + final all = results.expand((e) => e).toList(); + print("[国际服客户端] 所有成绩解析完成,总数:${all.length}"); + return all; + } +} + +Future> fetchNetRecords(Region region, AuthParams auth) async { + print("[fetchNetRecords] 外部接口调用,区域:${region.name}"); + final MaimaiNetClient client; + if (region == Region.jp) { + client = MaimaiNETJpClient(); + } else { + client = MaimaiNETIntlClient(); + } + + await client.login(auth); + final userInfo = await client.fetchUserInfo(); + final musicRecords = await client.fetchMusicRecords(); + + return { + "user": userInfo.toJson(), + "music": musicRecords.map((e) => e.toJson()).toList(), + "recent": [], + }; +} \ No newline at end of file diff --git a/lib/service/user_service.dart b/lib/service/user_service.dart index 1d60f1f..7d3ed68 100644 --- a/lib/service/user_service.dart +++ b/lib/service/user_service.dart @@ -283,4 +283,28 @@ class UserService { throw _getErrorMessage(e); } } + // 上传 Sega 成绩数据 + static Future> uploadSegaRating( + String token, + String segaId, + Map segaResult, + ) async { + try { + final res = await _dio.post( + '$baseUrl/api/union/segaReisaRating', + queryParameters: { + "segaId": segaId, + }, + data: segaResult, + options: Options( + headers: { + "Authorization": token, + }, + ), + ); + return Map.from(res.data); + } on DioException catch (e) { + throw _getErrorMessage(e); + } + } } \ No newline at end of file diff --git a/macos/Runner.xcodeproj/project.pbxproj b/macos/Runner.xcodeproj/project.pbxproj index 9eb8b9d..3837f11 100644 --- a/macos/Runner.xcodeproj/project.pbxproj +++ b/macos/Runner.xcodeproj/project.pbxproj @@ -21,14 +21,14 @@ /* End PBXAggregateTarget section */ /* Begin PBXBuildFile section */ - 1115E964EE7344044EA87340 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = EB4F5A4FE440294B3214794A /* Pods_Runner.framework */; }; 331C80D8294CF71000263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C80D7294CF71000263BE5 /* RunnerTests.swift */; }; 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */; }; 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC10F02044A3C60003C045 /* AppDelegate.swift */; }; 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; }; 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; }; 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; }; - 3706021F7EEB2AC8761EF83E /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 80F792EE2D670777FD7CA688 /* Pods_RunnerTests.framework */; }; + 347C8783FFA8594D3475C1B1 /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 8403A1BE4D0285716060FA7E /* Pods_RunnerTests.framework */; }; + D001A0C52A7A968BD1BBDC89 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D349F9576E7496D04E1998B2 /* Pods_Runner.framework */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -62,7 +62,8 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ - 03E70FFEBD3ABEFEA74186E9 /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = ""; }; + 061A6DA9B784560E62EA378E /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = ""; }; + 1E3EDE577A6C1F411A56772B /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = ""; }; 331C80D5294CF71000263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 331C80D7294CF71000263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; 333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = ""; }; @@ -79,15 +80,14 @@ 33E51913231747F40026EE4D /* DebugProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DebugProfile.entitlements; sourceTree = ""; }; 33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = ""; }; 33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = ""; }; - 439331F7BBC4C5200C47CD53 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; - 79CAB73FB66F4C32DE422CDE /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = ""; }; + 716AF5A705D0555D88DF2B29 /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = ""; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = ""; }; - 80F792EE2D670777FD7CA688 /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 8403A1BE4D0285716060FA7E /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 925906409C141F8E699BDA54 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = ""; }; - B439C3977658BF3659A03040 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; - D48F6E62A7A5E80DDD97417B /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = ""; }; - E0D55815B799E5060DE15BB1 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; - EB4F5A4FE440294B3214794A /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 9AC33D131F9E1EA9EB03F15A /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; + C52A2A0CB2BACFFF5FF7072B /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; + D349F9576E7496D04E1998B2 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -95,7 +95,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 3706021F7EEB2AC8761EF83E /* Pods_RunnerTests.framework in Frameworks */, + 347C8783FFA8594D3475C1B1 /* Pods_RunnerTests.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -103,13 +103,27 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 1115E964EE7344044EA87340 /* Pods_Runner.framework in Frameworks */, + D001A0C52A7A968BD1BBDC89 /* Pods_Runner.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 10C14F25FB53CAE6F47361B9 /* Pods */ = { + isa = PBXGroup; + children = ( + C52A2A0CB2BACFFF5FF7072B /* Pods-Runner.debug.xcconfig */, + 9AC33D131F9E1EA9EB03F15A /* Pods-Runner.release.xcconfig */, + 925906409C141F8E699BDA54 /* Pods-Runner.profile.xcconfig */, + 061A6DA9B784560E62EA378E /* Pods-RunnerTests.debug.xcconfig */, + 1E3EDE577A6C1F411A56772B /* Pods-RunnerTests.release.xcconfig */, + 716AF5A705D0555D88DF2B29 /* Pods-RunnerTests.profile.xcconfig */, + ); + name = Pods; + path = Pods; + sourceTree = ""; + }; 331C80D6294CF71000263BE5 /* RunnerTests */ = { isa = PBXGroup; children = ( @@ -137,7 +151,7 @@ 331C80D6294CF71000263BE5 /* RunnerTests */, 33CC10EE2044A3C60003C045 /* Products */, D73912EC22F37F3D000D13A0 /* Frameworks */, - 643F9CEC1C25FED65C6CBC14 /* Pods */, + 10C14F25FB53CAE6F47361B9 /* Pods */, ); sourceTree = ""; }; @@ -185,25 +199,11 @@ path = Runner; sourceTree = ""; }; - 643F9CEC1C25FED65C6CBC14 /* Pods */ = { - isa = PBXGroup; - children = ( - B439C3977658BF3659A03040 /* Pods-Runner.debug.xcconfig */, - E0D55815B799E5060DE15BB1 /* Pods-Runner.release.xcconfig */, - 439331F7BBC4C5200C47CD53 /* Pods-Runner.profile.xcconfig */, - 79CAB73FB66F4C32DE422CDE /* Pods-RunnerTests.debug.xcconfig */, - D48F6E62A7A5E80DDD97417B /* Pods-RunnerTests.release.xcconfig */, - 03E70FFEBD3ABEFEA74186E9 /* Pods-RunnerTests.profile.xcconfig */, - ); - name = Pods; - path = Pods; - sourceTree = ""; - }; D73912EC22F37F3D000D13A0 /* Frameworks */ = { isa = PBXGroup; children = ( - EB4F5A4FE440294B3214794A /* Pods_Runner.framework */, - 80F792EE2D670777FD7CA688 /* Pods_RunnerTests.framework */, + D349F9576E7496D04E1998B2 /* Pods_Runner.framework */, + 8403A1BE4D0285716060FA7E /* Pods_RunnerTests.framework */, ); name = Frameworks; sourceTree = ""; @@ -215,7 +215,7 @@ isa = PBXNativeTarget; buildConfigurationList = 331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; buildPhases = ( - A3B693C9D43FF0298BC47F87 /* [CP] Check Pods Manifest.lock */, + 2C72CB55051DF188C184CCE5 /* [CP] Check Pods Manifest.lock */, 331C80D1294CF70F00263BE5 /* Sources */, 331C80D2294CF70F00263BE5 /* Frameworks */, 331C80D3294CF70F00263BE5 /* Resources */, @@ -234,13 +234,13 @@ isa = PBXNativeTarget; buildConfigurationList = 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */; buildPhases = ( - 837EC17E2DC8DD04D788FAEB /* [CP] Check Pods Manifest.lock */, + D60F7A1773BBDE533CA61546 /* [CP] Check Pods Manifest.lock */, 33CC10E92044A3C60003C045 /* Sources */, 33CC10EA2044A3C60003C045 /* Frameworks */, 33CC10EB2044A3C60003C045 /* Resources */, 33CC110E2044A8840003C045 /* Bundle Framework */, 3399D490228B24CF009A79C7 /* ShellScript */, - 000CA14DDCCC50A5CBB6774D /* [CP] Embed Pods Frameworks */, + 38C831F56EAB5041148AF6D2 /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); @@ -323,21 +323,26 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ - 000CA14DDCCC50A5CBB6774D /* [CP] Embed Pods Frameworks */ = { + 2C72CB55051DF188C184CCE5 /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", ); - name = "[CP] Embed Pods Frameworks"; + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; outputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; showEnvVarsInLog = 0; }; 3399D490228B24CF009A79C7 /* ShellScript */ = { @@ -378,7 +383,24 @@ shellPath = /bin/sh; shellScript = "\"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh && touch Flutter/ephemeral/tripwire"; }; - 837EC17E2DC8DD04D788FAEB /* [CP] Check Pods Manifest.lock */ = { + 38C831F56EAB5041148AF6D2 /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; + D60F7A1773BBDE533CA61546 /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( @@ -400,28 +422,6 @@ shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; showEnvVarsInLog = 0; }; - A3B693C9D43FF0298BC47F87 /* [CP] Check Pods Manifest.lock */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - ); - inputPaths = ( - "${PODS_PODFILE_DIR_PATH}/Podfile.lock", - "${PODS_ROOT}/Manifest.lock", - ); - name = "[CP] Check Pods Manifest.lock"; - outputFileListPaths = ( - ); - outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; - showEnvVarsInLog = 0; - }; /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ @@ -473,7 +473,7 @@ /* Begin XCBuildConfiguration section */ 331C80DB294CF71000263BE5 /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 79CAB73FB66F4C32DE422CDE /* Pods-RunnerTests.debug.xcconfig */; + baseConfigurationReference = 061A6DA9B784560E62EA378E /* Pods-RunnerTests.debug.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CURRENT_PROJECT_VERSION = 1; @@ -488,7 +488,7 @@ }; 331C80DC294CF71000263BE5 /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = D48F6E62A7A5E80DDD97417B /* Pods-RunnerTests.release.xcconfig */; + baseConfigurationReference = 1E3EDE577A6C1F411A56772B /* Pods-RunnerTests.release.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CURRENT_PROJECT_VERSION = 1; @@ -503,7 +503,7 @@ }; 331C80DD294CF71000263BE5 /* Profile */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 03E70FFEBD3ABEFEA74186E9 /* Pods-RunnerTests.profile.xcconfig */; + baseConfigurationReference = 716AF5A705D0555D88DF2B29 /* Pods-RunnerTests.profile.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CURRENT_PROJECT_VERSION = 1; diff --git a/macos/Runner/DebugProfile.entitlements b/macos/Runner/DebugProfile.entitlements index de7f345..08c3ab1 100644 --- a/macos/Runner/DebugProfile.entitlements +++ b/macos/Runner/DebugProfile.entitlements @@ -4,13 +4,11 @@ com.apple.security.app-sandbox - com.apple.security.network.client + com.apple.security.cs.allow-jit com.apple.security.network.server - com.apple.security.cs.allow-jit - - com.apple.security.cs.allow-unsigned-executable-memory + com.apple.security.network.client - \ No newline at end of file + diff --git a/pubspec.lock b/pubspec.lock index bb0f53a..b08be07 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -97,6 +97,14 @@ packages: url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "3.0.7" + csslib: + dependency: transitive + description: + name: csslib + sha256: "09bad715f418841f976c77db72d5398dc1253c21fb9c0c7f0b0b985860b2d58e" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + source: hosted + version: "1.0.2" cupertino_icons: dependency: "direct main" description: @@ -344,6 +352,14 @@ packages: url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "1.0.2" + html: + dependency: "direct dev" + description: + name: html + sha256: "6d1264f2dffa1b1101c25a91dff0dc2daee4c18e87cd8538729773c073dbf602" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + source: hosted + version: "0.15.6" http: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index d5a09fe..acfdf66 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -42,6 +42,7 @@ dev_dependencies: flutter_map: ^6.1.0 # 地图组件 latlong2: ^0.9.0 # flutter_map 依赖的坐标库 geolocator: ^14.0.2 + html: ^0.15.4 geocoding: ^4.0.0 # The "flutter_lints" package below contains a set of recommended lints to # encourage good coding practices. The lint set provided by the package is