ver1.00.00
bindQR
This commit is contained in:
@@ -179,8 +179,7 @@ class HomePage extends StatelessWidget {
|
|||||||
flex: 6,
|
flex: 6,
|
||||||
child: const _RecommendedSongsSection(),
|
child: const _RecommendedSongsSection(),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 20), // 左右间距
|
const SizedBox(width: 4),
|
||||||
// 右侧:快捷按钮区域(占 4 份宽度)
|
|
||||||
Expanded(
|
Expanded(
|
||||||
flex: 6,
|
flex: 6,
|
||||||
child: _buildQuickActionButtons(), // 快捷按钮组件
|
child: _buildQuickActionButtons(), // 快捷按钮组件
|
||||||
|
|||||||
@@ -3,7 +3,8 @@ import 'package:flutter/cupertino.dart';
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:http/http.dart' as http;
|
import 'package:http/http.dart' as http;
|
||||||
import 'package:path_provider/path_provider.dart';
|
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 '../../../model/song_model.dart';
|
||||||
import '../../../providers/user_provider.dart';
|
import '../../../providers/user_provider.dart';
|
||||||
import '../../../service/song_service.dart';
|
import '../../../service/song_service.dart';
|
||||||
@@ -28,17 +29,34 @@ class SongDetailPage extends StatefulWidget {
|
|||||||
class _SongDetailPageState extends State<SongDetailPage> {
|
class _SongDetailPageState extends State<SongDetailPage> {
|
||||||
String? _selectedType;
|
String? _selectedType;
|
||||||
bool _isAliasExpanded = false;
|
bool _isAliasExpanded = false;
|
||||||
|
bool _isDownloading = false;
|
||||||
// 用于跟踪下载状态,key为 "type_levelId"
|
|
||||||
final Map<String, bool> _isDownloading = {};
|
|
||||||
|
|
||||||
final Map<String, List<Map<String, dynamic>>> _chartDataCache = {};
|
final Map<String, List<Map<String, dynamic>>> _chartDataCache = {};
|
||||||
final Map<String, bool> _isLoadingChart = {};
|
final Map<String, bool> _isLoadingChart = {};
|
||||||
|
|
||||||
|
// 【新增】用于控制 WebView 显示和加载 URL
|
||||||
|
String? _previewUrl;
|
||||||
|
late WebViewController _webViewController;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_initSelectedType();
|
_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() {
|
void _initSelectedType() {
|
||||||
@@ -82,74 +100,50 @@ class _SongDetailPageState extends State<SongDetailPage> {
|
|||||||
return all;
|
return all;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===================== 【新增】下载并打开 ADX 谱面功能 =====================
|
Future<void> _downloadCurrentTypeAdx() async {
|
||||||
Future<void> _downloadAndOpenAdx(String type, Map diff) async {
|
if (_isDownloading) return;
|
||||||
int levelId = diff['level_id'] ?? 0;
|
if (_selectedType == null || _selectedType == 'UT') {
|
||||||
String downloadKey = "${type}_$levelId";
|
|
||||||
|
|
||||||
// 防止重复点击
|
|
||||||
if (_isDownloading[downloadKey] == true) return;
|
|
||||||
|
|
||||||
setState(() {
|
|
||||||
_isDownloading[downloadKey] = true;
|
|
||||||
});
|
|
||||||
|
|
||||||
try {
|
|
||||||
// 1. 构建 URL
|
|
||||||
int songId = widget.song.id;
|
|
||||||
String url;
|
|
||||||
|
|
||||||
// UT 谱面通常没有标准的 CDN adx 下载地址,这里只处理 SD 和 DX
|
|
||||||
if (type == 'UT') {
|
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
const SnackBar(content: Text("UT 宴会谱暂不支持直接下载 ADX 文件")),
|
const SnackBar(content: Text("仅支持下载 SD 或 DX 谱面")),
|
||||||
);
|
);
|
||||||
setState(() => _isDownloading[downloadKey] = false);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
_isDownloading = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
int songId = widget.song.id;
|
||||||
|
String url;
|
||||||
|
String type = _selectedType!;
|
||||||
|
|
||||||
if (type == 'SD') {
|
if (type == 'SD') {
|
||||||
// SD: cdn.godserver.cn/resource/static/adx/00000/{songId}.adx
|
String idStr = songId.toString();
|
||||||
String idStr = songId.toString().padLeft(5, '0');
|
|
||||||
url = "https://cdn.godserver.cn/resource/static/adx/$idStr.adx";
|
url = "https://cdn.godserver.cn/resource/static/adx/$idStr.adx";
|
||||||
} else {
|
} else {
|
||||||
// DX: cdn.godserver.cn/resource/static/adx/00000/{songId+10000}.adx
|
|
||||||
int dxId = songId + 10000;
|
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";
|
url = "https://cdn.godserver.cn/resource/static/adx/$idStr.adx";
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. 下载文件
|
|
||||||
final response = await http.get(Uri.parse(url));
|
final response = await http.get(Uri.parse(url));
|
||||||
|
|
||||||
if (response.statusCode != 200) {
|
if (response.statusCode != 200) {
|
||||||
throw Exception("下载失败: HTTP ${response.statusCode}");
|
throw Exception("下载失败: HTTP ${response.statusCode}");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. 获取临时目录并保存文件
|
|
||||||
final directory = await getApplicationDocumentsDirectory();
|
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 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 filePath = "${directory.path}/$fileName";
|
||||||
final file = File(filePath);
|
final file = File(filePath);
|
||||||
|
|
||||||
await file.writeAsBytes(response.bodyBytes);
|
await file.writeAsBytes(response.bodyBytes);
|
||||||
|
|
||||||
// 4. 调用系统原生打开
|
|
||||||
// open_file 会尝试寻找能打开 .adx 的应用
|
|
||||||
final result = await OpenFile.open(filePath);
|
final result = await OpenFile.open(filePath);
|
||||||
|
|
||||||
if (result.type != ResultType.done) {
|
if (result.type != ResultType.done) {
|
||||||
// 如果没有安装能打开 adx 的应用,提示用户
|
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
SnackBar(content: Text("无法打开文件: ${result.message},请确保已安装谱面编辑器")),
|
SnackBar(content: Text("无法打开文件: ${result.message}")),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -160,29 +154,26 @@ class _SongDetailPageState extends State<SongDetailPage> {
|
|||||||
);
|
);
|
||||||
} finally {
|
} finally {
|
||||||
setState(() {
|
setState(() {
|
||||||
_isDownloading[downloadKey] = false;
|
_isDownloading = false;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 【新增】加载图表数据逻辑保持不变...
|
||||||
Future<void> _loadTypeChartData(String type) async {
|
Future<void> _loadTypeChartData(String type) async {
|
||||||
if (_chartDataCache.containsKey(type) || _isLoadingChart[type] == true) {
|
if (_chartDataCache.containsKey(type) || _isLoadingChart[type] == true) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setState(() {
|
setState(() {
|
||||||
_isLoadingChart[type] = true;
|
_isLoadingChart[type] = true;
|
||||||
});
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
final userProvider = UserProvider.instance;
|
final userProvider = UserProvider.instance;
|
||||||
final token = userProvider.token;
|
final token = userProvider.token;
|
||||||
|
|
||||||
if (token == null || token.isEmpty) {
|
if (token == null || token.isEmpty) {
|
||||||
setState(() => _isLoadingChart[type] = false);
|
setState(() => _isLoadingChart[type] = false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
int apiMusicId;
|
int apiMusicId;
|
||||||
if (type == 'SD') {
|
if (type == 'SD') {
|
||||||
apiMusicId = widget.song.id;
|
apiMusicId = widget.song.id;
|
||||||
@@ -192,10 +183,8 @@ class _SongDetailPageState extends State<SongDetailPage> {
|
|||||||
setState(() => _isLoadingChart[type] = false);
|
setState(() => _isLoadingChart[type] = false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
final logs = await UserService.getChartLog(token, [apiMusicId]);
|
final logs = await UserService.getChartLog(token, [apiMusicId]);
|
||||||
logs.sort((a, b) => a.time.compareTo(b.time));
|
logs.sort((a, b) => a.time.compareTo(b.time));
|
||||||
|
|
||||||
final chartList = logs.map((log) => {
|
final chartList = logs.map((log) => {
|
||||||
'achievement': log.segaChartNew?.achievement ?? 0,
|
'achievement': log.segaChartNew?.achievement ?? 0,
|
||||||
'time': log.time,
|
'time': log.time,
|
||||||
@@ -206,7 +195,6 @@ class _SongDetailPageState extends State<SongDetailPage> {
|
|||||||
'playCount': log.segaChartNew?.playCount ?? 0,
|
'playCount': log.segaChartNew?.playCount ?? 0,
|
||||||
'level': log.segaChartNew?.level ?? 0,
|
'level': log.segaChartNew?.level ?? 0,
|
||||||
}).toList();
|
}).toList();
|
||||||
|
|
||||||
setState(() {
|
setState(() {
|
||||||
_chartDataCache[type] = chartList;
|
_chartDataCache[type] = chartList;
|
||||||
_isLoadingChart[type] = false;
|
_isLoadingChart[type] = false;
|
||||||
@@ -221,9 +209,7 @@ class _SongDetailPageState extends State<SongDetailPage> {
|
|||||||
if (_chartDataCache.containsKey(cacheKey) || _isLoadingChart[cacheKey] == true) {
|
if (_chartDataCache.containsKey(cacheKey) || _isLoadingChart[cacheKey] == true) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setState(() => _isLoadingChart[cacheKey] = true);
|
setState(() => _isLoadingChart[cacheKey] = true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
final userProvider = UserProvider.instance;
|
final userProvider = UserProvider.instance;
|
||||||
final token = userProvider.token;
|
final token = userProvider.token;
|
||||||
@@ -231,10 +217,8 @@ class _SongDetailPageState extends State<SongDetailPage> {
|
|||||||
setState(() => _isLoadingChart[cacheKey] = false);
|
setState(() => _isLoadingChart[cacheKey] = false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
final logs = await UserService.getChartLog(token, [realId]);
|
final logs = await UserService.getChartLog(token, [realId]);
|
||||||
logs.sort((a, b) => a.time.compareTo(b.time));
|
logs.sort((a, b) => a.time.compareTo(b.time));
|
||||||
|
|
||||||
final chartList = logs.map((log) => {
|
final chartList = logs.map((log) => {
|
||||||
'achievement': log.segaChartNew?.achievement ?? 0,
|
'achievement': log.segaChartNew?.achievement ?? 0,
|
||||||
'time': log.time,
|
'time': log.time,
|
||||||
@@ -244,7 +228,6 @@ class _SongDetailPageState extends State<SongDetailPage> {
|
|||||||
'deluxscoreMax': log.segaChartNew?.deluxscoreMax ?? 0,
|
'deluxscoreMax': log.segaChartNew?.deluxscoreMax ?? 0,
|
||||||
'playCount': log.segaChartNew?.playCount ?? 0,
|
'playCount': log.segaChartNew?.playCount ?? 0,
|
||||||
}).toList();
|
}).toList();
|
||||||
|
|
||||||
setState(() {
|
setState(() {
|
||||||
_chartDataCache[cacheKey] = chartList;
|
_chartDataCache[cacheKey] = chartList;
|
||||||
_isLoadingChart[cacheKey] = false;
|
_isLoadingChart[cacheKey] = false;
|
||||||
@@ -324,6 +307,27 @@ class _SongDetailPageState extends State<SongDetailPage> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
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 coverUrl = _getCoverUrl(widget.song.id);
|
||||||
final diffs = _getCurrentDifficulties();
|
final diffs = _getCurrentDifficulties();
|
||||||
final availableTypes = _getAvailableTypes();
|
final availableTypes = _getAvailableTypes();
|
||||||
@@ -332,13 +336,24 @@ class _SongDetailPageState extends State<SongDetailPage> {
|
|||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: Text(widget.song.title ?? "歌曲详情"),
|
title: Text(widget.song.title ?? "歌曲详情"),
|
||||||
elevation: 2,
|
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(
|
// 【修改】使用 Column 布局,将固定内容和滚动内容分离
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
body: Column(
|
||||||
children: [
|
children: [
|
||||||
Card(
|
// 1. 固定顶部:歌曲信息卡片
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 16),
|
||||||
|
child: Card(
|
||||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||||
elevation: 4,
|
elevation: 4,
|
||||||
shadowColor: Colors.purpleAccent.withOpacity(0.2),
|
shadowColor: Colors.purpleAccent.withOpacity(0.2),
|
||||||
@@ -401,9 +416,15 @@ class _SongDetailPageState extends State<SongDetailPage> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
|
|
||||||
const SizedBox(height: 20),
|
// 2. 滚动区域:难度详情
|
||||||
|
Expanded(
|
||||||
|
child: SingleChildScrollView(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 12), // 注意:vertical padding 可以减小或移除,因为顶部已有 padding
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
if (availableTypes.isNotEmpty) ...[
|
if (availableTypes.isNotEmpty) ...[
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 4),
|
padding: const EdgeInsets.symmetric(horizontal: 4),
|
||||||
@@ -461,9 +482,13 @@ class _SongDetailPageState extends State<SongDetailPage> {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 【修改】_diffItem 方法,增加预览按钮
|
||||||
Widget _diffItem({required String type, required Map diff}) {
|
Widget _diffItem({required String type, required Map diff}) {
|
||||||
int levelId = diff['level_id'] ?? 0;
|
int levelId = diff['level_id'] ?? 0;
|
||||||
final double lvValue = double.tryParse(diff['level_value']?.toString() ?? '') ?? 0;
|
final double lvValue = double.tryParse(diff['level_value']?.toString() ?? '') ?? 0;
|
||||||
@@ -536,10 +561,6 @@ class _SongDetailPageState extends State<SongDetailPage> {
|
|||||||
final rating1003 = _calculateRating(lvValue, 100.3);
|
final rating1003 = _calculateRating(lvValue, 100.3);
|
||||||
final rating1005 = _calculateRating(lvValue, 100.5);
|
final rating1005 = _calculateRating(lvValue, 100.5);
|
||||||
|
|
||||||
// 下载状态的 Key
|
|
||||||
String downloadKey = "${type}_$levelId";
|
|
||||||
bool isDownloading = _isDownloading[downloadKey] ?? false;
|
|
||||||
|
|
||||||
return Card(
|
return Card(
|
||||||
margin: const EdgeInsets.symmetric(horizontal: 4, vertical: 8),
|
margin: const EdgeInsets.symmetric(horizontal: 4, vertical: 8),
|
||||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14)),
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14)),
|
||||||
@@ -570,38 +591,38 @@ class _SongDetailPageState extends State<SongDetailPage> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
Row(
|
Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
if (hasUserScore)
|
if (hasUserScore)
|
||||||
const Icon(Icons.star_rounded, color: Colors.amber, size: 22),
|
const Icon(Icons.star_rounded, color: Colors.amber, size: 22),
|
||||||
|
|
||||||
// ===================== 【新增】下载/预览按钮 =====================
|
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
InkWell(
|
// 【新增】谱面预览按钮
|
||||||
onTap: isDownloading || type == 'UT' ? null : () {
|
IconButton(
|
||||||
_downloadAndOpenAdx(type, diff);
|
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>(color),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
: Icon(
|
|
||||||
Icons.download_rounded,
|
|
||||||
size: 18,
|
|
||||||
color: type == 'UT' ? Colors.grey : color,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -838,7 +859,7 @@ class _SongDetailPageState extends State<SongDetailPage> {
|
|||||||
crossAxisAlignment: CrossAxisAlignment.end,
|
crossAxisAlignment: CrossAxisAlignment.end,
|
||||||
children: [
|
children: [
|
||||||
GradientText(
|
GradientText(
|
||||||
data: "${ach.toStringAsFixed(4)}%",
|
data:"${ach.toStringAsFixed(4)}%",
|
||||||
style: const TextStyle(fontSize: 30, fontWeight: FontWeight.bold),
|
style: const TextStyle(fontSize: 30, fontWeight: FontWeight.bold),
|
||||||
gradientLayers: [
|
gradientLayers: [
|
||||||
GradientLayer(
|
GradientLayer(
|
||||||
|
|||||||
322
lib/pages/score/updateScorePage.dart
Normal file
322
lib/pages/score/updateScorePage.dart
Normal file
@@ -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<UpdateScorePage> createState() => _UpdateScorePageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _UpdateScorePageState extends State<UpdateScorePage> {
|
||||||
|
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<void> _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<Barcode> 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<void> _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 = <int>[];
|
||||||
|
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<UserProvider>();
|
||||||
|
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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
import 'dart:typed_data'; // 确保导入,因为 uploadStegImage 需要 Uint8List
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
import '../model/user_model.dart';
|
import '../model/user_model.dart';
|
||||||
@@ -25,11 +26,11 @@ class UserProvider with ChangeNotifier {
|
|||||||
|
|
||||||
String get scoreDataSource => _scoreDataSource;
|
String get scoreDataSource => _scoreDataSource;
|
||||||
String? get selectedSegaId => _selectedSegaId;
|
String? get selectedSegaId => _selectedSegaId;
|
||||||
String? get selectedCnUserName => _selectedCnUserName; // Getter for CN username
|
String? get selectedCnUserName => _selectedCnUserName;
|
||||||
|
|
||||||
List<SegaCard> get availableSegaCards => _user?.segaCards ?? [];
|
List<SegaCard> get availableSegaCards => _user?.segaCards ?? [];
|
||||||
|
|
||||||
// 获取可用的国服账号列表 (从 userName2userId 提取 keys)
|
// 获取可用的国服账号列表
|
||||||
List<String> get availableCnUserNames {
|
List<String> get availableCnUserNames {
|
||||||
final map = _user?.userName2userId;
|
final map = _user?.userName2userId;
|
||||||
if (map == null || map.isEmpty) return [];
|
if (map == null || map.isEmpty) return [];
|
||||||
@@ -82,7 +83,6 @@ class UserProvider with ChangeNotifier {
|
|||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ... (login, register, logout 保持不变) ...
|
|
||||||
Future<void> login(String username, String twoKeyCode) async {
|
Future<void> login(String username, String twoKeyCode) async {
|
||||||
try {
|
try {
|
||||||
_token = await UserService.login(username, twoKeyCode);
|
_token = await UserService.login(username, twoKeyCode);
|
||||||
@@ -101,18 +101,22 @@ class UserProvider with ChangeNotifier {
|
|||||||
|
|
||||||
Future<void> register(String username, String password, String inviter) async {
|
Future<void> register(String username, String password, String inviter) async {
|
||||||
await UserService.register(username, password, inviter);
|
await UserService.register(username, password, inviter);
|
||||||
await login(username, password);
|
// 注意:注册后通常不直接登录,或者根据业务需求决定。这里保持原逻辑。
|
||||||
|
// await login(username, password);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> logout() async {
|
Future<void> logout() async {
|
||||||
_token = null;
|
_token = null;
|
||||||
_user = null;
|
_user = null;
|
||||||
|
_selectedSegaId = null;
|
||||||
|
_selectedCnUserName = null;
|
||||||
final prefs = await SharedPreferences.getInstance();
|
final prefs = await SharedPreferences.getInstance();
|
||||||
await prefs.remove("token");
|
await prefs.remove("token");
|
||||||
|
await prefs.remove("selectedSegaId");
|
||||||
|
await prefs.remove("selectedCnUserName");
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ... (saveUserInfo, refreshToken, fetchRadarData, fetchSexTags, updateUser 保持不变) ...
|
|
||||||
Future<void> saveUserInfo() async {
|
Future<void> saveUserInfo() async {
|
||||||
if (_token == null || _user == null) return;
|
if (_token == null || _user == null) return;
|
||||||
try {
|
try {
|
||||||
@@ -182,7 +186,7 @@ class UserProvider with ChangeNotifier {
|
|||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- 【新增】设置选中的国服用户名 ---
|
// --- 设置选中的国服用户名 ---
|
||||||
Future<void> setSelectedCnUserName(String? userName) async {
|
Future<void> setSelectedCnUserName(String? userName) async {
|
||||||
_selectedCnUserName = userName;
|
_selectedCnUserName = userName;
|
||||||
final prefs = await SharedPreferences.getInstance();
|
final prefs = await SharedPreferences.getInstance();
|
||||||
@@ -203,7 +207,6 @@ class UserProvider with ChangeNotifier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 调用上传
|
|
||||||
final result = await UserService.uploadSegaRating(
|
final result = await UserService.uploadSegaRating(
|
||||||
_token!,
|
_token!,
|
||||||
_selectedSegaId!,
|
_selectedSegaId!,
|
||||||
@@ -221,4 +224,38 @@ class UserProvider with ChangeNotifier {
|
|||||||
rethrow;
|
rethrow;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ================= 新增:上传隐写图片 =================
|
||||||
|
|
||||||
|
/// 上传带有隐写数据的图片
|
||||||
|
/// [imageBytes] 原始图片的字节数据 (Uint8List)
|
||||||
|
/// [segaId] 要隐藏进图片的 SegaID (如果为 null,则使用当前选中的 _selectedSegaId)
|
||||||
|
Future<Map<String, dynamic>> 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
114
lib/service/steganography_util.dart
Normal file
114
lib/service/steganography_util.dart
Normal file
@@ -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<int> 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++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,8 +1,10 @@
|
|||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'package:dio/dio.dart';
|
import 'package:dio/dio.dart';
|
||||||
|
import 'package:unionapp/service/steganography_util.dart';
|
||||||
import '../model/song_model.dart';
|
import '../model/song_model.dart';
|
||||||
import '../model/user_model.dart';
|
import '../model/user_model.dart';
|
||||||
import '../tool/encryption_util.dart';
|
import '../tool/encryption_util.dart';
|
||||||
|
import 'dart:typed_data';
|
||||||
|
|
||||||
class UserService {
|
class UserService {
|
||||||
static final Dio _dio = Dio();
|
static final Dio _dio = Dio();
|
||||||
@@ -337,4 +339,82 @@ class UserService {
|
|||||||
throw _getErrorMessage(e);
|
throw _getErrorMessage(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
/// 上传隐写图片
|
||||||
|
/// [token] 用户令牌
|
||||||
|
/// [segaId] 要隐藏的 Sega ID
|
||||||
|
/// [originalImageBytes] 原始图片字节数据 (Uint8List)
|
||||||
|
static Future<Map<String, dynamic>> 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<String, dynamic>.from(res.data);
|
||||||
|
}
|
||||||
|
return {"code": 200, "msg": "success", "data": res.data};
|
||||||
|
|
||||||
|
} on DioException catch (e) {
|
||||||
|
throw _getErrorMessage(e);
|
||||||
|
} catch (e) {
|
||||||
|
throw Exception("隐写或上传失败: $e");
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -6,10 +6,14 @@
|
|||||||
|
|
||||||
#include "generated_plugin_registrant.h"
|
#include "generated_plugin_registrant.h"
|
||||||
|
|
||||||
|
#include <file_selector_linux/file_selector_plugin.h>
|
||||||
#include <open_file_linux/open_file_linux_plugin.h>
|
#include <open_file_linux/open_file_linux_plugin.h>
|
||||||
#include <url_launcher_linux/url_launcher_plugin.h>
|
#include <url_launcher_linux/url_launcher_plugin.h>
|
||||||
|
|
||||||
void fl_register_plugins(FlPluginRegistry* registry) {
|
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 =
|
g_autoptr(FlPluginRegistrar) open_file_linux_registrar =
|
||||||
fl_plugin_registry_get_registrar_for_plugin(registry, "OpenFileLinuxPlugin");
|
fl_plugin_registry_get_registrar_for_plugin(registry, "OpenFileLinuxPlugin");
|
||||||
open_file_linux_plugin_register_with_registrar(open_file_linux_registrar);
|
open_file_linux_plugin_register_with_registrar(open_file_linux_registrar);
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
#
|
#
|
||||||
|
|
||||||
list(APPEND FLUTTER_PLUGIN_LIST
|
list(APPEND FLUTTER_PLUGIN_LIST
|
||||||
|
file_selector_linux
|
||||||
open_file_linux
|
open_file_linux
|
||||||
url_launcher_linux
|
url_launcher_linux
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -5,18 +5,22 @@
|
|||||||
import FlutterMacOS
|
import FlutterMacOS
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
|
import file_selector_macos
|
||||||
import geolocator_apple
|
import geolocator_apple
|
||||||
import open_file_mac
|
import open_file_mac
|
||||||
import package_info_plus
|
import package_info_plus
|
||||||
import share_plus
|
import share_plus
|
||||||
import shared_preferences_foundation
|
import shared_preferences_foundation
|
||||||
import url_launcher_macos
|
import url_launcher_macos
|
||||||
|
import webview_flutter_wkwebview
|
||||||
|
|
||||||
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
||||||
|
FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin"))
|
||||||
GeolocatorPlugin.register(with: registry.registrar(forPlugin: "GeolocatorPlugin"))
|
GeolocatorPlugin.register(with: registry.registrar(forPlugin: "GeolocatorPlugin"))
|
||||||
OpenFilePlugin.register(with: registry.registrar(forPlugin: "OpenFilePlugin"))
|
OpenFilePlugin.register(with: registry.registrar(forPlugin: "OpenFilePlugin"))
|
||||||
FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin"))
|
FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin"))
|
||||||
SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin"))
|
SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin"))
|
||||||
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
|
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
|
||||||
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
|
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
|
||||||
|
WebViewFlutterPlugin.register(with: registry.registrar(forPlugin: "WebViewFlutterPlugin"))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ PODS:
|
|||||||
- geolocator_apple (1.2.0):
|
- geolocator_apple (1.2.0):
|
||||||
- Flutter
|
- Flutter
|
||||||
- FlutterMacOS
|
- FlutterMacOS
|
||||||
|
- open_file_mac (1.0.3):
|
||||||
|
- FlutterMacOS
|
||||||
- package_info_plus (0.0.1):
|
- package_info_plus (0.0.1):
|
||||||
- FlutterMacOS
|
- FlutterMacOS
|
||||||
- share_plus (0.0.1):
|
- share_plus (0.0.1):
|
||||||
@@ -12,20 +14,27 @@ PODS:
|
|||||||
- FlutterMacOS
|
- FlutterMacOS
|
||||||
- url_launcher_macos (0.0.1):
|
- url_launcher_macos (0.0.1):
|
||||||
- FlutterMacOS
|
- FlutterMacOS
|
||||||
|
- webview_flutter_wkwebview (0.0.1):
|
||||||
|
- Flutter
|
||||||
|
- FlutterMacOS
|
||||||
|
|
||||||
DEPENDENCIES:
|
DEPENDENCIES:
|
||||||
- FlutterMacOS (from `Flutter/ephemeral`)
|
- FlutterMacOS (from `Flutter/ephemeral`)
|
||||||
- geolocator_apple (from `Flutter/ephemeral/.symlinks/plugins/geolocator_apple/darwin`)
|
- 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`)
|
- package_info_plus (from `Flutter/ephemeral/.symlinks/plugins/package_info_plus/macos`)
|
||||||
- share_plus (from `Flutter/ephemeral/.symlinks/plugins/share_plus/macos`)
|
- share_plus (from `Flutter/ephemeral/.symlinks/plugins/share_plus/macos`)
|
||||||
- shared_preferences_foundation (from `Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin`)
|
- shared_preferences_foundation (from `Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin`)
|
||||||
- url_launcher_macos (from `Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos`)
|
- 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:
|
EXTERNAL SOURCES:
|
||||||
FlutterMacOS:
|
FlutterMacOS:
|
||||||
:path: Flutter/ephemeral
|
:path: Flutter/ephemeral
|
||||||
geolocator_apple:
|
geolocator_apple:
|
||||||
:path: Flutter/ephemeral/.symlinks/plugins/geolocator_apple/darwin
|
:path: Flutter/ephemeral/.symlinks/plugins/geolocator_apple/darwin
|
||||||
|
open_file_mac:
|
||||||
|
:path: Flutter/ephemeral/.symlinks/plugins/open_file_mac/macos
|
||||||
package_info_plus:
|
package_info_plus:
|
||||||
:path: Flutter/ephemeral/.symlinks/plugins/package_info_plus/macos
|
:path: Flutter/ephemeral/.symlinks/plugins/package_info_plus/macos
|
||||||
share_plus:
|
share_plus:
|
||||||
@@ -34,14 +43,18 @@ EXTERNAL SOURCES:
|
|||||||
:path: Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin
|
:path: Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin
|
||||||
url_launcher_macos:
|
url_launcher_macos:
|
||||||
:path: Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos
|
:path: Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos
|
||||||
|
webview_flutter_wkwebview:
|
||||||
|
:path: Flutter/ephemeral/.symlinks/plugins/webview_flutter_wkwebview/darwin
|
||||||
|
|
||||||
SPEC CHECKSUMS:
|
SPEC CHECKSUMS:
|
||||||
FlutterMacOS: d0db08ddef1a9af05a5ec4b724367152bb0500b1
|
FlutterMacOS: d0db08ddef1a9af05a5ec4b724367152bb0500b1
|
||||||
geolocator_apple: ab36aa0e8b7d7a2d7639b3b4e48308394e8cef5e
|
geolocator_apple: ab36aa0e8b7d7a2d7639b3b4e48308394e8cef5e
|
||||||
|
open_file_mac: 76f06c8597551249bdb5e8fd8827a98eae0f4585
|
||||||
package_info_plus: f0052d280d17aa382b932f399edf32507174e870
|
package_info_plus: f0052d280d17aa382b932f399edf32507174e870
|
||||||
share_plus: 3c787998077d6b31e839225a282e9e27edf99274
|
share_plus: 3c787998077d6b31e839225a282e9e27edf99274
|
||||||
shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb
|
shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb
|
||||||
url_launcher_macos: f87a979182d112f911de6820aefddaf56ee9fbfd
|
url_launcher_macos: f87a979182d112f911de6820aefddaf56ee9fbfd
|
||||||
|
webview_flutter_wkwebview: 8ebf4fded22593026f7dbff1fbff31ea98573c8d
|
||||||
|
|
||||||
PODFILE CHECKSUM: 54d867c82ac51cbd61b565781b9fada492027009
|
PODFILE CHECKSUM: 54d867c82ac51cbd61b565781b9fada492027009
|
||||||
|
|
||||||
|
|||||||
160
pubspec.lock
160
pubspec.lock
@@ -177,6 +177,38 @@ packages:
|
|||||||
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
|
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "7.0.1"
|
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:
|
fixnum:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -214,6 +246,14 @@ packages:
|
|||||||
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
|
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "6.2.1"
|
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:
|
flutter_test:
|
||||||
dependency: "direct dev"
|
dependency: "direct dev"
|
||||||
description: flutter
|
description: flutter
|
||||||
@@ -336,6 +376,22 @@ packages:
|
|||||||
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
|
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.3"
|
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:
|
gsettings:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -376,6 +432,78 @@ packages:
|
|||||||
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
|
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "4.1.2"
|
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:
|
intl:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -981,6 +1109,38 @@ packages:
|
|||||||
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
|
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.1.1"
|
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:
|
win32:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|||||||
10
pubspec.yaml
10
pubspec.yaml
@@ -44,11 +44,6 @@ dev_dependencies:
|
|||||||
geolocator: ^14.0.2
|
geolocator: ^14.0.2
|
||||||
html: ^0.15.4
|
html: ^0.15.4
|
||||||
geocoding: ^4.0.0
|
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
|
flutter_lints: ^6.0.0
|
||||||
provider: ^6.1.2
|
provider: ^6.1.2
|
||||||
dio: ^5.4.0 # 网络请求
|
dio: ^5.4.0 # 网络请求
|
||||||
@@ -61,6 +56,11 @@ dev_dependencies:
|
|||||||
url_launcher: ^6.2.2
|
url_launcher: ^6.2.2
|
||||||
share_plus: ^7.2.2
|
share_plus: ^7.2.2
|
||||||
open_file: ^3.3.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
|
# For information on the generic Dart part of this file, see the
|
||||||
# following page: https://dart.dev/tools/pub/pubspec
|
# following page: https://dart.dev/tools/pub/pubspec
|
||||||
|
|
||||||
|
|||||||
@@ -6,11 +6,14 @@
|
|||||||
|
|
||||||
#include "generated_plugin_registrant.h"
|
#include "generated_plugin_registrant.h"
|
||||||
|
|
||||||
|
#include <file_selector_windows/file_selector_windows.h>
|
||||||
#include <geolocator_windows/geolocator_windows.h>
|
#include <geolocator_windows/geolocator_windows.h>
|
||||||
#include <share_plus/share_plus_windows_plugin_c_api.h>
|
#include <share_plus/share_plus_windows_plugin_c_api.h>
|
||||||
#include <url_launcher_windows/url_launcher_windows.h>
|
#include <url_launcher_windows/url_launcher_windows.h>
|
||||||
|
|
||||||
void RegisterPlugins(flutter::PluginRegistry* registry) {
|
void RegisterPlugins(flutter::PluginRegistry* registry) {
|
||||||
|
FileSelectorWindowsRegisterWithRegistrar(
|
||||||
|
registry->GetRegistrarForPlugin("FileSelectorWindows"));
|
||||||
GeolocatorWindowsRegisterWithRegistrar(
|
GeolocatorWindowsRegisterWithRegistrar(
|
||||||
registry->GetRegistrarForPlugin("GeolocatorWindows"));
|
registry->GetRegistrarForPlugin("GeolocatorWindows"));
|
||||||
SharePlusWindowsPluginCApiRegisterWithRegistrar(
|
SharePlusWindowsPluginCApiRegisterWithRegistrar(
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
#
|
#
|
||||||
|
|
||||||
list(APPEND FLUTTER_PLUGIN_LIST
|
list(APPEND FLUTTER_PLUGIN_LIST
|
||||||
|
file_selector_windows
|
||||||
geolocator_windows
|
geolocator_windows
|
||||||
share_plus
|
share_plus
|
||||||
url_launcher_windows
|
url_launcher_windows
|
||||||
|
|||||||
Reference in New Issue
Block a user