From c5c3f7c8f574b132886b72de7243a02d4564506a Mon Sep 17 00:00:00 2001 From: spasolreisa Date: Sun, 19 Apr 2026 22:22:04 +0800 Subject: [PATCH] ver1.00.00 bindQR --- lib/pages/home/home_page.dart | 3 +- lib/pages/music/score_single.dart | 315 +++++++++-------- lib/pages/score/updateScorePage.dart | 322 ++++++++++++++++++ lib/providers/user_provider.dart | 51 ++- lib/service/steganography_util.dart | 114 +++++++ lib/service/user_service.dart | 80 +++++ linux/flutter/generated_plugin_registrant.cc | 4 + linux/flutter/generated_plugins.cmake | 1 + macos/Flutter/GeneratedPluginRegistrant.swift | 4 + macos/Podfile.lock | 13 + pubspec.lock | 160 +++++++++ pubspec.yaml | 10 +- .../flutter/generated_plugin_registrant.cc | 3 + windows/flutter/generated_plugins.cmake | 1 + 14 files changed, 920 insertions(+), 161 deletions(-) create mode 100644 lib/pages/score/updateScorePage.dart create mode 100644 lib/service/steganography_util.dart diff --git a/lib/pages/home/home_page.dart b/lib/pages/home/home_page.dart index 5220c23..539f3d7 100644 --- a/lib/pages/home/home_page.dart +++ b/lib/pages/home/home_page.dart @@ -179,8 +179,7 @@ class HomePage extends StatelessWidget { flex: 6, child: const _RecommendedSongsSection(), ), - const SizedBox(width: 20), // 左右间距 - // 右侧:快捷按钮区域(占 4 份宽度) + const SizedBox(width: 4), Expanded( flex: 6, child: _buildQuickActionButtons(), // 快捷按钮组件 diff --git a/lib/pages/music/score_single.dart b/lib/pages/music/score_single.dart index 89b062f..882f477 100644 --- a/lib/pages/music/score_single.dart +++ b/lib/pages/music/score_single.dart @@ -3,7 +3,8 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:http/http.dart' as http; import 'package:path_provider/path_provider.dart'; -import 'package:open_file/open_file.dart'; // 引入打开文件的包 +import 'package:open_file/open_file.dart'; +import 'package:webview_flutter/webview_flutter.dart'; // 【新增】导入 webview import '../../../model/song_model.dart'; import '../../../providers/user_provider.dart'; import '../../../service/song_service.dart'; @@ -28,17 +29,34 @@ class SongDetailPage extends StatefulWidget { class _SongDetailPageState extends State { String? _selectedType; bool _isAliasExpanded = false; - - // 用于跟踪下载状态,key为 "type_levelId" - final Map _isDownloading = {}; + bool _isDownloading = false; final Map>> _chartDataCache = {}; final Map _isLoadingChart = {}; + // 【新增】用于控制 WebView 显示和加载 URL + String? _previewUrl; + late WebViewController _webViewController; + @override void initState() { super.initState(); _initSelectedType(); + + // 【新增】初始化 WebViewController + _webViewController = WebViewController() + ..setJavaScriptMode(JavaScriptMode.unrestricted) + ..setNavigationDelegate( + NavigationDelegate( + onProgress: (int progress) { + // 可选:更新加载进度 + }, + onPageStarted: (String url) {}, + onPageFinished: (String url) {}, + onHttpError: (HttpResponseError error) {}, + onWebResourceError: (WebResourceError error) {}, + ), + ); } void _initSelectedType() { @@ -82,74 +100,50 @@ class _SongDetailPageState extends State { return all; } - // ===================== 【新增】下载并打开 ADX 谱面功能 ===================== - Future _downloadAndOpenAdx(String type, Map diff) async { - int levelId = diff['level_id'] ?? 0; - String downloadKey = "${type}_$levelId"; - - // 防止重复点击 - if (_isDownloading[downloadKey] == true) return; + Future _downloadCurrentTypeAdx() async { + if (_isDownloading) return; + if (_selectedType == null || _selectedType == 'UT') { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text("仅支持下载 SD 或 DX 谱面")), + ); + return; + } setState(() { - _isDownloading[downloadKey] = true; + _isDownloading = true; }); try { - // 1. 构建 URL int songId = widget.song.id; String url; - - // UT 谱面通常没有标准的 CDN adx 下载地址,这里只处理 SD 和 DX - if (type == 'UT') { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text("UT 宴会谱暂不支持直接下载 ADX 文件")), - ); - setState(() => _isDownloading[downloadKey] = false); - return; - } + String type = _selectedType!; if (type == 'SD') { - // SD: cdn.godserver.cn/resource/static/adx/00000/{songId}.adx - String idStr = songId.toString().padLeft(5, '0'); + String idStr = songId.toString(); url = "https://cdn.godserver.cn/resource/static/adx/$idStr.adx"; } else { - // DX: cdn.godserver.cn/resource/static/adx/00000/{songId+10000}.adx int dxId = songId + 10000; - String idStr = dxId.toString().padLeft(5, '0'); + String idStr = dxId.toString(); url = "https://cdn.godserver.cn/resource/static/adx/$idStr.adx"; } - // 2. 下载文件 final response = await http.get(Uri.parse(url)); - if (response.statusCode != 200) { throw Exception("下载失败: HTTP ${response.statusCode}"); } - // 3. 获取临时目录并保存文件 final directory = await getApplicationDocumentsDirectory(); - // 注意:iOS 上 getApplicationDocumentsDirectory 是持久化的。 - // 如果希望每次下载都清理,可以使用 getTemporaryDirectory()。 - // 这里为了稳定性,使用 DocumentsDirectory,但文件名保持唯一性。 - - // 生成文件名: SongTitle_Type_Level.adx String safeTitle = widget.song.title?.replaceAll(RegExp(r'[^\w\s\u4e00-\u9fa5]'), '_') ?? "song"; - String fileName = "${safeTitle}_${type}_Lv${levelId}.adx"; - - // 确保文件名不冲突,可以加时间戳或者随机数,这里简单处理 + String fileName = "${safeTitle}_$type.adx"; final filePath = "${directory.path}/$fileName"; final file = File(filePath); await file.writeAsBytes(response.bodyBytes); - - // 4. 调用系统原生打开 - // open_file 会尝试寻找能打开 .adx 的应用 final result = await OpenFile.open(filePath); if (result.type != ResultType.done) { - // 如果没有安装能打开 adx 的应用,提示用户 ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text("无法打开文件: ${result.message},请确保已安装谱面编辑器")), + SnackBar(content: Text("无法打开文件: ${result.message}")), ); } @@ -160,29 +154,26 @@ class _SongDetailPageState extends State { ); } finally { setState(() { - _isDownloading[downloadKey] = false; + _isDownloading = false; }); } } + // 【新增】加载图表数据逻辑保持不变... Future _loadTypeChartData(String type) async { if (_chartDataCache.containsKey(type) || _isLoadingChart[type] == true) { return; } - setState(() { _isLoadingChart[type] = true; }); - try { final userProvider = UserProvider.instance; final token = userProvider.token; - if (token == null || token.isEmpty) { setState(() => _isLoadingChart[type] = false); return; } - int apiMusicId; if (type == 'SD') { apiMusicId = widget.song.id; @@ -192,10 +183,8 @@ class _SongDetailPageState extends State { setState(() => _isLoadingChart[type] = false); return; } - final logs = await UserService.getChartLog(token, [apiMusicId]); logs.sort((a, b) => a.time.compareTo(b.time)); - final chartList = logs.map((log) => { 'achievement': log.segaChartNew?.achievement ?? 0, 'time': log.time, @@ -206,7 +195,6 @@ class _SongDetailPageState extends State { 'playCount': log.segaChartNew?.playCount ?? 0, 'level': log.segaChartNew?.level ?? 0, }).toList(); - setState(() { _chartDataCache[type] = chartList; _isLoadingChart[type] = false; @@ -221,9 +209,7 @@ class _SongDetailPageState extends State { if (_chartDataCache.containsKey(cacheKey) || _isLoadingChart[cacheKey] == true) { return; } - setState(() => _isLoadingChart[cacheKey] = true); - try { final userProvider = UserProvider.instance; final token = userProvider.token; @@ -231,10 +217,8 @@ class _SongDetailPageState extends State { setState(() => _isLoadingChart[cacheKey] = false); return; } - final logs = await UserService.getChartLog(token, [realId]); logs.sort((a, b) => a.time.compareTo(b.time)); - final chartList = logs.map((log) => { 'achievement': log.segaChartNew?.achievement ?? 0, 'time': log.time, @@ -244,7 +228,6 @@ class _SongDetailPageState extends State { 'deluxscoreMax': log.segaChartNew?.deluxscoreMax ?? 0, 'playCount': log.segaChartNew?.playCount ?? 0, }).toList(); - setState(() { _chartDataCache[cacheKey] = chartList; _isLoadingChart[cacheKey] = false; @@ -324,6 +307,27 @@ class _SongDetailPageState extends State { @override Widget build(BuildContext context) { + // 【新增】如果存在预览 URL,则显示 WebView 页面 + if (_previewUrl != null) { + return Scaffold( + appBar: AppBar( + title: const Text("谱面预览"), + actions: [ + IconButton( + icon: const Icon(Icons.close), + onPressed: () { + setState(() { + _previewUrl = null; + }); + }, + ), + ], + ), + body: WebViewWidget(controller: _webViewController), + ); + } + + // 否则显示原有的详情页面 final coverUrl = _getCoverUrl(widget.song.id); final diffs = _getCurrentDifficulties(); final availableTypes = _getAvailableTypes(); @@ -332,13 +336,24 @@ class _SongDetailPageState extends State { appBar: AppBar( title: Text(widget.song.title ?? "歌曲详情"), elevation: 2, + actions: [ + if (_selectedType != 'UT' && _selectedType != null) + IconButton( + icon: _isDownloading + ? const SizedBox(width: 20, height: 20, child: CircularProgressIndicator(strokeWidth: 2)) + : const Icon(Icons.download_rounded), + tooltip: "下载 ${_selectedType} 谱面", + onPressed: _isDownloading ? null : _downloadCurrentTypeAdx, + ), + ], ), - body: SingleChildScrollView( - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Card( + // 【修改】使用 Column 布局,将固定内容和滚动内容分离 + body: Column( + children: [ + // 1. 固定顶部:歌曲信息卡片 + Padding( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 16), + child: Card( shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), elevation: 4, shadowColor: Colors.purpleAccent.withOpacity(0.2), @@ -401,69 +416,79 @@ class _SongDetailPageState extends State { ), ), ), + ), - const SizedBox(height: 20), - - if (availableTypes.isNotEmpty) ...[ - Padding( - padding: const EdgeInsets.symmetric(horizontal: 4), - child: Row( - children: [ - const Text( - "难度详情", - style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold), - ), - const SizedBox(width: 12), - if (availableTypes.length > 1) - Expanded( - child: CupertinoSlidingSegmentedControl( - groupValue: _selectedType, - backgroundColor: Colors.grey.shade200, - thumbColor: Theme.of(context).primaryColor, - children: { - for (var type in availableTypes) - type: Padding( - padding: const EdgeInsets.symmetric(vertical: 6, horizontal: 8), - child: Text( - type, - style: TextStyle( - color: _selectedType == type ? Colors.white : Colors.black87, - fontWeight: FontWeight.w600, - ), - ), + // 2. 滚动区域:难度详情 + Expanded( + child: SingleChildScrollView( + padding: const EdgeInsets.symmetric(horizontal: 12), // 注意:vertical padding 可以减小或移除,因为顶部已有 padding + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (availableTypes.isNotEmpty) ...[ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 4), + child: Row( + children: [ + const Text( + "难度详情", + style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold), + ), + const SizedBox(width: 12), + if (availableTypes.length > 1) + Expanded( + child: CupertinoSlidingSegmentedControl( + groupValue: _selectedType, + backgroundColor: Colors.grey.shade200, + thumbColor: Theme.of(context).primaryColor, + children: { + for (var type in availableTypes) + type: Padding( + padding: const EdgeInsets.symmetric(vertical: 6, horizontal: 8), + child: Text( + type, + style: TextStyle( + color: _selectedType == type ? Colors.white : Colors.black87, + fontWeight: FontWeight.w600, + ), + ), + ), + }, + onValueChanged: (val) { + if (val != null) setState(() => _selectedType = val); + }, ), - }, - onValueChanged: (val) { - if (val != null) setState(() => _selectedType = val); - }, - ), + ), + ], ), + ), + const SizedBox(height: 12), ], - ), + + if (diffs.isEmpty) + const Center( + child: Padding( + padding: EdgeInsets.all(30), + child: Text("暂无谱面数据", style: TextStyle(color: Colors.grey)), + ), + ) + else + ...diffs.map((item) => _diffItem( + type: item['type'], + diff: item['diff'], + )), + + const SizedBox(height: 30), + ], ), - const SizedBox(height: 12), - ], - - if (diffs.isEmpty) - const Center( - child: Padding( - padding: EdgeInsets.all(30), - child: Text("暂无谱面数据", style: TextStyle(color: Colors.grey)), - ), - ) - else - ...diffs.map((item) => _diffItem( - type: item['type'], - diff: item['diff'], - )), - - const SizedBox(height: 30), - ], - ), + ), + ), + ], ), ); } + // 【修改】_diffItem 方法,增加预览按钮 Widget _diffItem({required String type, required Map diff}) { int levelId = diff['level_id'] ?? 0; final double lvValue = double.tryParse(diff['level_value']?.toString() ?? '') ?? 0; @@ -536,10 +561,6 @@ class _SongDetailPageState extends State { final rating1003 = _calculateRating(lvValue, 100.3); final rating1005 = _calculateRating(lvValue, 100.5); - // 下载状态的 Key - String downloadKey = "${type}_$levelId"; - bool isDownloading = _isDownloading[downloadKey] ?? false; - return Card( margin: const EdgeInsets.symmetric(horizontal: 4, vertical: 8), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14)), @@ -570,38 +591,38 @@ class _SongDetailPageState extends State { ), ), Row( + mainAxisSize: MainAxisSize.min, children: [ if (hasUserScore) const Icon(Icons.star_rounded, color: Colors.amber, size: 22), - - // ===================== 【新增】下载/预览按钮 ===================== const SizedBox(width: 8), - InkWell( - onTap: isDownloading || type == 'UT' ? null : () { - _downloadAndOpenAdx(type, diff); + // 【新增】谱面预览按钮 + IconButton( + icon: const Icon(Icons.preview_rounded, size: 22), + tooltip: "查看谱面预览", + onPressed: () { + // 构造 URL + // 规则: https://maimai.lxns.net/chart?chart_id={id}&difficulty={level} + int chartId; + if (type == 'SD') { + chartId = widget.song.id; + } else if (type == 'DX') { + chartId = widget.song.id + 10000; + } else { + // UT 情况,通常 UT 的 ID 在 diff['id'] 中,或者需要特殊处理 + // 这里假设 UT 也使用类似的 ID 逻辑,如果 lxns 支持 UT 预览的话 + // 如果 lxns 不支持 UT,可以禁用此按钮或提示 + chartId = (diff['id'] as num?)?.toInt() ?? 0; + } + + final url = "https://maimai.lxns.net/chart?chart_id=$chartId&difficulty=$levelId"; + + // 加载 URL 并切换视图 + _webViewController.loadRequest(Uri.parse(url)); + setState(() { + _previewUrl = url; + }); }, - borderRadius: BorderRadius.circular(20), - child: Container( - padding: const EdgeInsets.all(6), - decoration: BoxDecoration( - color: type == 'UT' ? Colors.grey.shade300 : Theme.of(context).primaryColor.withOpacity(0.1), - shape: BoxShape.circle, - ), - child: isDownloading - ? SizedBox( - width: 16, - height: 16, - child: CircularProgressIndicator( - strokeWidth: 2, - valueColor: AlwaysStoppedAnimation(color), - ), - ) - : Icon( - Icons.download_rounded, - size: 18, - color: type == 'UT' ? Colors.grey : color, - ), - ), ), ], ), @@ -838,7 +859,7 @@ class _SongDetailPageState extends State { crossAxisAlignment: CrossAxisAlignment.end, children: [ GradientText( - data: "${ach.toStringAsFixed(4)}%", + data:"${ach.toStringAsFixed(4)}%", style: const TextStyle(fontSize: 30, fontWeight: FontWeight.bold), gradientLayers: [ GradientLayer( diff --git a/lib/pages/score/updateScorePage.dart b/lib/pages/score/updateScorePage.dart new file mode 100644 index 0000000..acfd8c1 --- /dev/null +++ b/lib/pages/score/updateScorePage.dart @@ -0,0 +1,322 @@ +import 'dart:io'; +import 'dart:typed_data'; +import 'package:flutter/material.dart'; +import 'package:image_picker/image_picker.dart'; +import 'package:google_mlkit_barcode_scanning/google_mlkit_barcode_scanning.dart'; +import 'package:provider/provider.dart'; + +import '../../providers/user_provider.dart'; + +class UpdateScorePage extends StatefulWidget { + const UpdateScorePage({super.key}); + + @override + State createState() => _UpdateScorePageState(); +} + +class _UpdateScorePageState extends State { + final TextEditingController _segaIdController = TextEditingController(); + final ImagePicker _picker = ImagePicker(); + + bool _isProcessing = false; + String? _statusMessage; + File? _selectedImageFile; // 用于预览 + + // 条码扫描器 (支持 QR Code, Data Matrix, Aztec 等) + final BarcodeScanner _barcodeScanner = BarcodeScanner( + formats: [BarcodeFormat.qrCode, BarcodeFormat.dataMatrix, BarcodeFormat.aztec], + ); + + @override + void dispose() { + _segaIdController.dispose(); + _barcodeScanner.close(); + super.dispose(); + } + + // 1. 选择图片并识别二维码 + Future _pickImageAndScan() async { + try { + final XFile? pickedFile = await _picker.pickImage(source: ImageSource.gallery); + if (pickedFile == null) return; + + setState(() { + _isProcessing = true; + _statusMessage = "正在识别二维码..."; + _selectedImageFile = File(pickedFile.path); + }); + + // 执行二维码识别 + final inputImage = InputImage.fromFilePath(pickedFile.path); + final List barcodes = await _barcodeScanner.processImage(inputImage); + + setState(() { + _isProcessing = false; + }); + + if (barcodes.isNotEmpty) { + // 取第一个识别到的二维码内容 + final String code = barcodes.first.rawValue ?? ""; + + if (code.isNotEmpty) { + setState(() { + _segaIdController.text = code; + _statusMessage = "识别成功: $code"; + }); + } else { + setState(() { + _statusMessage = "二维码内容为空"; + }); + } + } else { + setState(() { + _statusMessage = "未在图中发现二维码,请手动输入"; + }); + } + + } catch (e) { + setState(() { + _isProcessing = false; + _statusMessage = "识别失败: $e"; + }); + } + } + + // 2. 执行隐写上传 + Future _handleUpload() async { + final segaId = _segaIdController.text.trim(); + + if (segaId.isEmpty) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text("请输入或扫描二维码"), backgroundColor: Colors.orange), + ); + return; + } + + Uint8List? imageBytes; + + setState(() { + _isProcessing = true; + _statusMessage = "正在准备图片..."; + }); + + try { + // 策略:优先使用用户选择的截图作为载体。 + // 如果用户没选图,或者选图后删除了,我们可以下载默认背景图作为载体。 + if (_selectedImageFile != null) { + imageBytes = await _selectedImageFile!.readAsBytes(); + } else { + // 下载默认背景图作为隐写载体 + final httpClient = HttpClient(); + final request = await httpClient.getUrl(Uri.parse('https://union.godserver.cn/assets/jpeg/20180621142015_5FmGZ-wYXkyL4y.jpeg')); + final response = await request.close(); + + // 辅助函数:将 HttpClientResponse 转为 Uint8List + final bytes = []; + await for (final chunk in response) { + bytes.addAll(chunk); + } + imageBytes = Uint8List.fromList(bytes); + } + + if (imageBytes == null || imageBytes.isEmpty) { + throw Exception("图片数据无效"); + } + + setState(() { + _statusMessage = "正在隐写并上传..."; + }); + + // 调用 Provider 上传 + final userProvider = context.read(); + final result = await userProvider.uploadStegImage(imageBytes, segaId: segaId); + + setState(() { + _isProcessing = false; + _statusMessage = "上传成功!"; + }); + + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text("上传成功: ${result['msg']}"), backgroundColor: Colors.green), + ); + // 可选:清空表单 + // _segaIdController.clear(); + // _selectedImageFile = null; + } + + } catch (e) { + setState(() { + _isProcessing = false; + _statusMessage = "上传失败: $e"; + }); + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text("错误: $e"), backgroundColor: Colors.red), + ); + } + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + body: Stack( + fit: StackFit.expand, + children: [ + // 1. 背景图片 + Image.network( + 'https://union.godserver.cn/assets/jpeg/20180621142015_5FmGZ-wYXkyL4y.jpeg', + fit: BoxFit.cover, + loadingBuilder: (context, child, loadingProgress) { + if (loadingProgress == null) return child; + return Container(color: Colors.black); + }, + errorBuilder: (context, error, stackTrace) { + return Container(color: Colors.grey[900]); + }, + ), + + // 2. 深色遮罩 + Container(color: Colors.black.withOpacity(0.7)), + + // 3. 主体内容 + SafeArea( + child: Center( + child: SingleChildScrollView( + padding: const EdgeInsets.all(24.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + // 标题 + const Text( + "更新成绩 / 二维码", + style: TextStyle( + fontSize: 24, + fontWeight: FontWeight.bold, + color: Colors.white, + ), + ), + const SizedBox(height: 30), + + // 卡片容器 + Container( + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.15), + borderRadius: BorderRadius.circular(16), + border: Border.all(color: Colors.white.withOpacity(0.2)), + ), + child: Column( + children: [ + // 输入框 + TextField( + controller: _segaIdController, + style: const TextStyle(color: Colors.white), + decoration: InputDecoration( + labelText: "Sega ID / 二维码内容", + labelStyle: const TextStyle(color: Colors.white70), + hintText: "手动输入或点击下方按钮识别", + hintStyle: const TextStyle(color: Colors.white38), + prefixIcon: const Icon(Icons.qr_code, color: Colors.white70), + enabledBorder: OutlineInputBorder( + borderSide: const BorderSide(color: Colors.white38), + borderRadius: BorderRadius.circular(12), + ), + focusedBorder: OutlineInputBorder( + borderSide: const BorderSide(color: Colors.white), + borderRadius: BorderRadius.circular(12), + ), + ), + ), + + const SizedBox(height: 20), + + // 图片预览区域 (如果有) + if (_selectedImageFile != null) + ClipRRect( + borderRadius: BorderRadius.circular(8), + child: Image.file( + _selectedImageFile!, + height: 150, + fit: BoxFit.contain, + ), + ), + + if (_selectedImageFile != null) const SizedBox(height: 15), + + // 操作按钮组 + Row( + children: [ + // 识别二维码按钮 + Expanded( + child: ElevatedButton.icon( + onPressed: _isProcessing ? null : _pickImageAndScan, + icon: const Icon(Icons.camera_alt), + label: const Text("识别截图"), + style: ElevatedButton.styleFrom( + backgroundColor: Colors.blueAccent, + foregroundColor: Colors.white, + padding: const EdgeInsets.symmetric(vertical: 12), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + ), + ), + ), + + const SizedBox(width: 10), + + // 上传按钮 + Expanded( + flex: 1, + child: ElevatedButton.icon( + onPressed: _isProcessing ? null : _handleUpload, + icon: _isProcessing + ? const SizedBox( + width: 16, height: 16, + child: CircularProgressIndicator(strokeWidth: 2, color: Colors.white), + ) + : const Icon(Icons.cloud_upload), + label: Text(_isProcessing ? "处理中..." : "上传"), + style: ElevatedButton.styleFrom( + backgroundColor: Colors.green, + foregroundColor: Colors.white, + padding: const EdgeInsets.symmetric(vertical: 12), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + ), + ), + ), + ], + ), + ], + ), + ), + + // 状态提示 + if (_statusMessage != null) + Padding( + padding: const EdgeInsets.only(top: 20), + child: Text( + _statusMessage!, + style: TextStyle( + color: _statusMessage!.contains("失败") || _statusMessage!.contains("错误") + ? Colors.redAccent + : Colors.white70, + fontSize: 14, + ), + textAlign: TextAlign.center, + ), + ), + ], + ), + ), + ), + ), + ], + ), + ); + } +} \ No newline at end of file diff --git a/lib/providers/user_provider.dart b/lib/providers/user_provider.dart index 034aecd..ab0d1a1 100644 --- a/lib/providers/user_provider.dart +++ b/lib/providers/user_provider.dart @@ -1,4 +1,5 @@ import 'dart:convert'; +import 'dart:typed_data'; // 确保导入,因为 uploadStegImage 需要 Uint8List import 'package:flutter/material.dart'; import 'package:shared_preferences/shared_preferences.dart'; import '../model/user_model.dart'; @@ -25,11 +26,11 @@ class UserProvider with ChangeNotifier { String get scoreDataSource => _scoreDataSource; String? get selectedSegaId => _selectedSegaId; - String? get selectedCnUserName => _selectedCnUserName; // Getter for CN username + String? get selectedCnUserName => _selectedCnUserName; List get availableSegaCards => _user?.segaCards ?? []; - // 获取可用的国服账号列表 (从 userName2userId 提取 keys) + // 获取可用的国服账号列表 List get availableCnUserNames { final map = _user?.userName2userId; if (map == null || map.isEmpty) return []; @@ -82,7 +83,6 @@ class UserProvider with ChangeNotifier { notifyListeners(); } - // ... (login, register, logout 保持不变) ... Future login(String username, String twoKeyCode) async { try { _token = await UserService.login(username, twoKeyCode); @@ -101,18 +101,22 @@ class UserProvider with ChangeNotifier { Future register(String username, String password, String inviter) async { await UserService.register(username, password, inviter); - await login(username, password); + // 注意:注册后通常不直接登录,或者根据业务需求决定。这里保持原逻辑。 + // await login(username, password); } Future logout() async { _token = null; _user = null; + _selectedSegaId = null; + _selectedCnUserName = null; final prefs = await SharedPreferences.getInstance(); await prefs.remove("token"); + await prefs.remove("selectedSegaId"); + await prefs.remove("selectedCnUserName"); notifyListeners(); } - // ... (saveUserInfo, refreshToken, fetchRadarData, fetchSexTags, updateUser 保持不变) ... Future saveUserInfo() async { if (_token == null || _user == null) return; try { @@ -182,7 +186,7 @@ class UserProvider with ChangeNotifier { notifyListeners(); } - // --- 【新增】设置选中的国服用户名 --- + // --- 设置选中的国服用户名 --- Future setSelectedCnUserName(String? userName) async { _selectedCnUserName = userName; final prefs = await SharedPreferences.getInstance(); @@ -203,7 +207,6 @@ class UserProvider with ChangeNotifier { } try { - // 调用上传 final result = await UserService.uploadSegaRating( _token!, _selectedSegaId!, @@ -221,4 +224,38 @@ class UserProvider with ChangeNotifier { rethrow; } } + + // ================= 新增:上传隐写图片 ================= + + /// 上传带有隐写数据的图片 + /// [imageBytes] 原始图片的字节数据 (Uint8List) + /// [segaId] 要隐藏进图片的 SegaID (如果为 null,则使用当前选中的 _selectedSegaId) + Future> uploadStegImage(Uint8List imageBytes, {String? segaId}) async { + if (_token == null) { + throw Exception("请先登录"); + } + + // 如果未指定 segaId,则使用当前选中的 + final targetSegaId = segaId ?? _selectedSegaId; + + if (targetSegaId == null || targetSegaId.isEmpty) { + throw Exception("请提供或选择要隐藏的 SegaID"); + } + + try { + print("🚀 开始隐写并上传... SegaID: $targetSegaId"); + + final result = await UserService.uploadStegImage( + token: _token!, + segaId: targetSegaId, + originalImageBytes: imageBytes, + ); + + print("✅ 隐写上传成功: ${result['msg']}"); + return result; + } catch (e) { + print("❌ 隐写上传失败: $e"); + rethrow; + } + } } \ No newline at end of file diff --git a/lib/service/steganography_util.dart b/lib/service/steganography_util.dart new file mode 100644 index 0000000..c7b23e2 --- /dev/null +++ b/lib/service/steganography_util.dart @@ -0,0 +1,114 @@ +import 'dart:convert'; +import 'dart:typed_data'; +import 'package:image/image.dart' as img; +import '../tool/encryption_util.dart'; + +class SteganographyUtil { + static const String MAGIC_HEADER = 'SGWCMAID'; + + /// 图片隐写:将数据藏进图片 R 通道最低位 + static Uint8List hideDataInImage(Uint8List imageBytes, String secretData) { + if (!secretData.startsWith(MAGIC_HEADER)) { + throw Exception('数据格式不正确,必须以 $MAGIC_HEADER 开头'); + } + + // 1. 加密 + 压缩 + final encryptedString = EncryptionUtil.encryptAndCompress(secretData); + + // 2. 转为字节并添加 \0 结束符 + final dataBytes = utf8.encode(encryptedString); + final payload = Uint8List(dataBytes.length + 1); + payload.setRange(0, dataBytes.length, dataBytes); + payload[dataBytes.length] = 0; // \0 + + // 3. 解码图片 + final image = img.decodeImage(imageBytes); + if (image == null) throw Exception('图片解码失败'); + + // 4. 获取像素数据 + final imageData = image.data; + if (imageData == null) throw Exception('图片数据为空'); + + // 【关键修复】ByteBuffer 必须转换为 Uint8List 才能进行 [] 操作和获取 length + // 注意:asUint8List() 返回的是原 buffer 的视图,修改它会直接修改 image 数据 + final Uint8List pixels = imageData.buffer.asUint8List(); + + // 检查容量 (R通道只有 width * height 个字节可用) + // pixels.length 是总字节数 (width * height * 4) + final maxBits = pixels.length ~/ 4; + if (payload.length * 8 > maxBits) { + throw Exception('图片容量不足!需要 ${payload.length * 8} bits,图片仅提供 ${maxBits} bits'); + } + + // 5. 执行 LSB 隐写 + _writeLsbRChannel(pixels, payload); + + // 6. 导出 PNG + return img.encodePng(image); + } + + /// 从图片中提取数据 + static String? extractDataFromImage(Uint8List imageBytes) { + final image = img.decodeImage(imageBytes); + if (image == null) return null; + + final imageData = image.data; + if (imageData == null) return null; + + // 【关键修复】转换为 Uint8List + final Uint8List pixels = imageData.buffer.asUint8List(); + + final List extractedBytes = []; + int currentByteValue = 0; + int bitCount = 0; + + // 遍历像素 (RGBA, 步长 4) + for (int i = 0; i < pixels.length; i += 4) { + // 只取 R 通道 (index i) 的最低位 + int bit = pixels[i] & 1; + + currentByteValue = (currentByteValue << 1) | bit; + bitCount++; + + if (bitCount == 8) { + if (currentByteValue == 0) { + break; // 遇到 \0 停止 + } + extractedBytes.add(currentByteValue); + currentByteValue = 0; + bitCount = 0; + } + } + + if (extractedBytes.isEmpty) return null; + + try { + return utf8.decode(extractedBytes); + } catch (e) { + return null; + } + } + + /// 核心写入:仅修改 R 通道 + static void _writeLsbRChannel(Uint8List pixels, Uint8List payload) { + int payloadIndex = 0; + int bitIndex = 0; // 0-7 + + for (int i = 0; i < pixels.length; i += 4) { + if (payloadIndex >= payload.length) break; + + int currentByte = payload[payloadIndex]; + // 取当前字节的第 bitIndex 位 (从高位到低位) + int bit = (currentByte >> (7 - bitIndex)) & 1; + + // 修改 R 通道: 清除最低位,写入新位 + pixels[i] = (pixels[i] & 0xFE) | bit; + + bitIndex++; + if (bitIndex > 7) { + bitIndex = 0; + payloadIndex++; + } + } + } +} \ No newline at end of file diff --git a/lib/service/user_service.dart b/lib/service/user_service.dart index 6e27c2a..462a55c 100644 --- a/lib/service/user_service.dart +++ b/lib/service/user_service.dart @@ -1,8 +1,10 @@ import 'dart:convert'; import 'package:dio/dio.dart'; +import 'package:unionapp/service/steganography_util.dart'; import '../model/song_model.dart'; import '../model/user_model.dart'; import '../tool/encryption_util.dart'; +import 'dart:typed_data'; class UserService { static final Dio _dio = Dio(); @@ -337,4 +339,82 @@ class UserService { throw _getErrorMessage(e); } } + /// 上传隐写图片 + /// [token] 用户令牌 + /// [segaId] 要隐藏的 Sega ID + /// [originalImageBytes] 原始图片字节数据 (Uint8List) + static Future> uploadStegImage({ + required String token, + required String segaId, + required Uint8List originalImageBytes, + }) async { + try { + // 1. 拼接隐写数据(SGWCMAID 开头) + final secretData = 'SGWCMAID$segaId'; + + // 2. 执行隐写 + 加密 + // 使用 Uint8List.fromList 确保类型兼容,避免 typed_data_patch 错误 + final safeImageBytes = Uint8List.fromList(originalImageBytes); + + final stegImageBytes = SteganographyUtil.hideDataInImage( + safeImageBytes, + secretData, + ); + + // 3. 构建上传表单 + final formData = FormData.fromMap({ + 'path': MultipartFile.fromBytes( + stegImageBytes, + filename: 'encoded.png', + contentType: DioMediaType.parse('image/png'), + ), + }); + + // 4. 前置接口(完全对齐 JS 逻辑) + await _dio.get( + '$baseUrl/api/sega/3egrsz53et/w35eshdk76e', + options: Options(headers: {"Authorization": token}), + ); + + // 5. 【核心上传】隐写图片 + final res = await _dio.post( + '$baseUrl/api/sega/steg', + data: formData, + options: Options( + headers: { + "Authorization": token, + // Dio 会自动处理 multipart/form-data 的 boundary,通常不需要手动指定 Content-Type + // 但为了严格对齐 JS,如果后端强校验,可以保留,不过 Dio 推荐让它在 FormData 时自动设置 + }, + ), + ); + + // 6. 后续接口链(对齐 JS 逻辑) + await _dio.post( + '$baseUrl/api/sega/454szhghs/45ustzdxzsd', + options: Options(headers: {"Authorization": token}), + ); + + await _dio.get( + '$baseUrl/api/sega/stxfghb347/b46se', + options: Options(headers: {"Authorization": token}), + ); + + await _dio.put( + '$baseUrl/api/sega/sertjdffgh/4666drzzf', + options: Options(headers: {"Authorization": token}), + ); + + // 返回最终结果 + if (res.data is Map) { + return Map.from(res.data); + } + return {"code": 200, "msg": "success", "data": res.data}; + + } on DioException catch (e) { + throw _getErrorMessage(e); + } catch (e) { + throw Exception("隐写或上传失败: $e"); + } + } } \ No newline at end of file diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc index a78b14c..86be7eb 100644 --- a/linux/flutter/generated_plugin_registrant.cc +++ b/linux/flutter/generated_plugin_registrant.cc @@ -6,10 +6,14 @@ #include "generated_plugin_registrant.h" +#include #include #include void fl_register_plugins(FlPluginRegistry* registry) { + g_autoptr(FlPluginRegistrar) file_selector_linux_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "FileSelectorPlugin"); + file_selector_plugin_register_with_registrar(file_selector_linux_registrar); g_autoptr(FlPluginRegistrar) open_file_linux_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "OpenFileLinuxPlugin"); open_file_linux_plugin_register_with_registrar(open_file_linux_registrar); diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake index 0c65fa2..f087eec 100644 --- a/linux/flutter/generated_plugins.cmake +++ b/linux/flutter/generated_plugins.cmake @@ -3,6 +3,7 @@ # list(APPEND FLUTTER_PLUGIN_LIST + file_selector_linux open_file_linux url_launcher_linux ) diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index 0bdafcd..35a2ead 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -5,18 +5,22 @@ import FlutterMacOS import Foundation +import file_selector_macos import geolocator_apple import open_file_mac import package_info_plus import share_plus import shared_preferences_foundation import url_launcher_macos +import webview_flutter_wkwebview func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { + FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin")) GeolocatorPlugin.register(with: registry.registrar(forPlugin: "GeolocatorPlugin")) OpenFilePlugin.register(with: registry.registrar(forPlugin: "OpenFilePlugin")) FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin")) SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin")) SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) + WebViewFlutterPlugin.register(with: registry.registrar(forPlugin: "WebViewFlutterPlugin")) } diff --git a/macos/Podfile.lock b/macos/Podfile.lock index 2ce9107..878e86b 100644 --- a/macos/Podfile.lock +++ b/macos/Podfile.lock @@ -3,6 +3,8 @@ PODS: - geolocator_apple (1.2.0): - Flutter - FlutterMacOS + - open_file_mac (1.0.3): + - FlutterMacOS - package_info_plus (0.0.1): - FlutterMacOS - share_plus (0.0.1): @@ -12,20 +14,27 @@ PODS: - FlutterMacOS - url_launcher_macos (0.0.1): - FlutterMacOS + - webview_flutter_wkwebview (0.0.1): + - Flutter + - FlutterMacOS DEPENDENCIES: - FlutterMacOS (from `Flutter/ephemeral`) - geolocator_apple (from `Flutter/ephemeral/.symlinks/plugins/geolocator_apple/darwin`) + - open_file_mac (from `Flutter/ephemeral/.symlinks/plugins/open_file_mac/macos`) - package_info_plus (from `Flutter/ephemeral/.symlinks/plugins/package_info_plus/macos`) - share_plus (from `Flutter/ephemeral/.symlinks/plugins/share_plus/macos`) - shared_preferences_foundation (from `Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin`) - url_launcher_macos (from `Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos`) + - webview_flutter_wkwebview (from `Flutter/ephemeral/.symlinks/plugins/webview_flutter_wkwebview/darwin`) EXTERNAL SOURCES: FlutterMacOS: :path: Flutter/ephemeral geolocator_apple: :path: Flutter/ephemeral/.symlinks/plugins/geolocator_apple/darwin + open_file_mac: + :path: Flutter/ephemeral/.symlinks/plugins/open_file_mac/macos package_info_plus: :path: Flutter/ephemeral/.symlinks/plugins/package_info_plus/macos share_plus: @@ -34,14 +43,18 @@ EXTERNAL SOURCES: :path: Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin url_launcher_macos: :path: Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos + webview_flutter_wkwebview: + :path: Flutter/ephemeral/.symlinks/plugins/webview_flutter_wkwebview/darwin SPEC CHECKSUMS: FlutterMacOS: d0db08ddef1a9af05a5ec4b724367152bb0500b1 geolocator_apple: ab36aa0e8b7d7a2d7639b3b4e48308394e8cef5e + open_file_mac: 76f06c8597551249bdb5e8fd8827a98eae0f4585 package_info_plus: f0052d280d17aa382b932f399edf32507174e870 share_plus: 3c787998077d6b31e839225a282e9e27edf99274 shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb url_launcher_macos: f87a979182d112f911de6820aefddaf56ee9fbfd + webview_flutter_wkwebview: 8ebf4fded22593026f7dbff1fbff31ea98573c8d PODFILE CHECKSUM: 54d867c82ac51cbd61b565781b9fada492027009 diff --git a/pubspec.lock b/pubspec.lock index b6d7da4..d599882 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -177,6 +177,38 @@ packages: url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "7.0.1" + file_selector_linux: + dependency: transitive + description: + name: file_selector_linux + sha256: "2567f398e06ac72dcf2e98a0c95df2a9edd03c2c2e0cacd4780f20cdf56263a0" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + source: hosted + version: "0.9.4" + file_selector_macos: + dependency: transitive + description: + name: file_selector_macos + sha256: "5e0bbe9c312416f1787a68259ea1505b52f258c587f12920422671807c4d618a" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + source: hosted + version: "0.9.5" + file_selector_platform_interface: + dependency: transitive + description: + name: file_selector_platform_interface + sha256: "35e0bd61ebcdb91a3505813b055b09b79dfdc7d0aee9c09a7ba59ae4bb13dc85" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + source: hosted + version: "2.7.0" + file_selector_windows: + dependency: transitive + description: + name: file_selector_windows + sha256: "62197474ae75893a62df75939c777763d39c2bc5f73ce5b88497208bc269abfd" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + source: hosted + version: "0.9.3+5" fixnum: dependency: transitive description: @@ -214,6 +246,14 @@ packages: url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "6.2.1" + flutter_plugin_android_lifecycle: + dependency: transitive + description: + name: flutter_plugin_android_lifecycle + sha256: "38d1c268de9097ff59cf0e844ac38759fc78f76836d37edad06fa21e182055a0" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + source: hosted + version: "2.0.34" flutter_test: dependency: "direct dev" description: flutter @@ -336,6 +376,22 @@ packages: url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "2.1.3" + google_mlkit_barcode_scanning: + dependency: "direct dev" + description: + name: google_mlkit_barcode_scanning + sha256: "965183a8cd5cef8477ceea5dbdf29c34a739cf0cfbf1bdad54cd3f9f1807afe5" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + source: hosted + version: "0.10.0" + google_mlkit_commons: + dependency: transitive + description: + name: google_mlkit_commons + sha256: "046586b381cdd139f7f6a05ad6998f7e339d061bd70158249907358394b5f496" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + source: hosted + version: "0.6.1" gsettings: dependency: transitive description: @@ -376,6 +432,78 @@ packages: url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "4.1.2" + image: + dependency: "direct dev" + description: + name: image + sha256: f31d52537dc417fdcde36088fdf11d191026fd5e4fae742491ebd40e5a8bea7d + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + source: hosted + version: "4.3.0" + image_picker: + dependency: "direct dev" + description: + name: image_picker + sha256: "784210112be18ea55f69d7076e2c656a4e24949fa9e76429fe53af0c0f4fa320" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + source: hosted + version: "1.2.1" + image_picker_android: + dependency: transitive + description: + name: image_picker_android + sha256: "66810af8e99b2657ee98e5c6f02064f69bb63f7a70e343937f70946c5f8c6622" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + source: hosted + version: "0.8.13+16" + image_picker_for_web: + dependency: transitive + description: + name: image_picker_for_web + sha256: "66257a3191ab360d23a55c8241c91a6e329d31e94efa7be9cf7a212e65850214" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + source: hosted + version: "3.1.1" + image_picker_ios: + dependency: transitive + description: + name: image_picker_ios + sha256: b9c4a438a9ff4f60808c9cf0039b93a42bb6c2211ef6ebb647394b2b3fa84588 + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + source: hosted + version: "0.8.13+6" + image_picker_linux: + dependency: transitive + description: + name: image_picker_linux + sha256: "1f81c5f2046b9ab724f85523e4af65be1d47b038160a8c8deed909762c308ed4" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + source: hosted + version: "0.2.2" + image_picker_macos: + dependency: transitive + description: + name: image_picker_macos + sha256: "86f0f15a309de7e1a552c12df9ce5b59fe927e71385329355aec4776c6a8ec91" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + source: hosted + version: "0.2.2+1" + image_picker_platform_interface: + dependency: transitive + description: + name: image_picker_platform_interface + sha256: "567e056716333a1647c64bb6bd873cff7622233a5c3f694be28a583d4715690c" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + source: hosted + version: "2.11.1" + image_picker_windows: + dependency: transitive + description: + name: image_picker_windows + sha256: d248c86554a72b5495a31c56f060cf73a41c7ff541689327b1a7dbccc33adfae + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + source: hosted + version: "0.2.2" intl: dependency: transitive description: @@ -981,6 +1109,38 @@ packages: url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" source: hosted version: "1.1.1" + webview_flutter: + dependency: "direct dev" + description: + name: webview_flutter + sha256: a3da219916aba44947d3a5478b1927876a09781174b5a2b67fa5be0555154bf9 + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + source: hosted + version: "4.13.1" + webview_flutter_android: + dependency: transitive + description: + name: webview_flutter_android + sha256: f560f57d0f529c1dcdaf4edc3a3217b099560622f9f4a10b6bdbb566553c61ea + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + source: hosted + version: "4.11.0" + webview_flutter_platform_interface: + dependency: transitive + description: + name: webview_flutter_platform_interface + sha256: "1221c1b12f5278791042f2ec2841743784cf25c5a644e23d6680e5d718824f04" + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + source: hosted + version: "2.15.1" + webview_flutter_wkwebview: + dependency: transitive + description: + name: webview_flutter_wkwebview + sha256: e15d8828e014291324a4d0cf6e272090167f4fa5673ffcf8fe446f4a4cd35861 + url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/" + source: hosted + version: "3.24.3" win32: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 87f9fb6..05431bb 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -44,11 +44,6 @@ dev_dependencies: 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 - # activated in the `analysis_options.yaml` file located at the root of your - # package. See that file for information about deactivating specific lint - # rules and activating additional ones. flutter_lints: ^6.0.0 provider: ^6.1.2 dio: ^5.4.0 # 网络请求 @@ -61,6 +56,11 @@ dev_dependencies: url_launcher: ^6.2.2 share_plus: ^7.2.2 open_file: ^3.3.2 + webview_flutter: ^4.8.0 + image: ^4.0.17 + google_mlkit_barcode_scanning: ^0.10.0 # 用于识别二维码/条形码 + image_picker: ^1.0.4 + # For information on the generic Dart part of this file, see the # following page: https://dart.dev/tools/pub/pubspec diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index cf2eeb8..58f82d3 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -6,11 +6,14 @@ #include "generated_plugin_registrant.h" +#include #include #include #include void RegisterPlugins(flutter::PluginRegistry* registry) { + FileSelectorWindowsRegisterWithRegistrar( + registry->GetRegistrarForPlugin("FileSelectorWindows")); GeolocatorWindowsRegisterWithRegistrar( registry->GetRegistrarForPlugin("GeolocatorWindows")); SharePlusWindowsPluginCApiRegisterWithRegistrar( diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index 131f8eb..c11c1ff 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -3,6 +3,7 @@ # list(APPEND FLUTTER_PLUGIN_LIST + file_selector_windows geolocator_windows share_plus url_launcher_windows