ver1.00.00
bindQR
This commit is contained in:
@@ -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(), // 快捷按钮组件
|
||||
|
||||
@@ -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<SongDetailPage> {
|
||||
String? _selectedType;
|
||||
bool _isAliasExpanded = false;
|
||||
|
||||
// 用于跟踪下载状态,key为 "type_levelId"
|
||||
final Map<String, bool> _isDownloading = {};
|
||||
bool _isDownloading = false;
|
||||
|
||||
final Map<String, List<Map<String, dynamic>>> _chartDataCache = {};
|
||||
final Map<String, bool> _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<SongDetailPage> {
|
||||
return all;
|
||||
}
|
||||
|
||||
// ===================== 【新增】下载并打开 ADX 谱面功能 =====================
|
||||
Future<void> _downloadAndOpenAdx(String type, Map diff) async {
|
||||
int levelId = diff['level_id'] ?? 0;
|
||||
String downloadKey = "${type}_$levelId";
|
||||
|
||||
// 防止重复点击
|
||||
if (_isDownloading[downloadKey] == true) return;
|
||||
Future<void> _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<SongDetailPage> {
|
||||
);
|
||||
} finally {
|
||||
setState(() {
|
||||
_isDownloading[downloadKey] = false;
|
||||
_isDownloading = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 【新增】加载图表数据逻辑保持不变...
|
||||
Future<void> _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<SongDetailPage> {
|
||||
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<SongDetailPage> {
|
||||
'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<SongDetailPage> {
|
||||
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<SongDetailPage> {
|
||||
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<SongDetailPage> {
|
||||
'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<SongDetailPage> {
|
||||
|
||||
@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<SongDetailPage> {
|
||||
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<SongDetailPage> {
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
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<String>(
|
||||
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<String>(
|
||||
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<SongDetailPage> {
|
||||
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<SongDetailPage> {
|
||||
),
|
||||
),
|
||||
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>(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,
|
||||
children: [
|
||||
GradientText(
|
||||
data: "${ach.toStringAsFixed(4)}%",
|
||||
data:"${ach.toStringAsFixed(4)}%",
|
||||
style: const TextStyle(fontSize: 30, fontWeight: FontWeight.bold),
|
||||
gradientLayers: [
|
||||
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: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<SegaCard> get availableSegaCards => _user?.segaCards ?? [];
|
||||
|
||||
// 获取可用的国服账号列表 (从 userName2userId 提取 keys)
|
||||
// 获取可用的国服账号列表
|
||||
List<String> 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<void> login(String username, String twoKeyCode) async {
|
||||
try {
|
||||
_token = await UserService.login(username, twoKeyCode);
|
||||
@@ -101,18 +101,22 @@ class UserProvider with ChangeNotifier {
|
||||
|
||||
Future<void> register(String username, String password, String inviter) async {
|
||||
await UserService.register(username, password, inviter);
|
||||
await login(username, password);
|
||||
// 注意:注册后通常不直接登录,或者根据业务需求决定。这里保持原逻辑。
|
||||
// await login(username, password);
|
||||
}
|
||||
|
||||
Future<void> 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<void> saveUserInfo() async {
|
||||
if (_token == null || _user == null) return;
|
||||
try {
|
||||
@@ -182,7 +186,7 @@ class UserProvider with ChangeNotifier {
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
// --- 【新增】设置选中的国服用户名 ---
|
||||
// --- 设置选中的国服用户名 ---
|
||||
Future<void> 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<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 '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<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 <file_selector_linux/file_selector_plugin.h>
|
||||
#include <open_file_linux/open_file_linux_plugin.h>
|
||||
#include <url_launcher_linux/url_launcher_plugin.h>
|
||||
|
||||
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);
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#
|
||||
|
||||
list(APPEND FLUTTER_PLUGIN_LIST
|
||||
file_selector_linux
|
||||
open_file_linux
|
||||
url_launcher_linux
|
||||
)
|
||||
|
||||
@@ -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"))
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
160
pubspec.lock
160
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:
|
||||
|
||||
10
pubspec.yaml
10
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
|
||||
|
||||
|
||||
@@ -6,11 +6,14 @@
|
||||
|
||||
#include "generated_plugin_registrant.h"
|
||||
|
||||
#include <file_selector_windows/file_selector_windows.h>
|
||||
#include <geolocator_windows/geolocator_windows.h>
|
||||
#include <share_plus/share_plus_windows_plugin_c_api.h>
|
||||
#include <url_launcher_windows/url_launcher_windows.h>
|
||||
|
||||
void RegisterPlugins(flutter::PluginRegistry* registry) {
|
||||
FileSelectorWindowsRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("FileSelectorWindows"));
|
||||
GeolocatorWindowsRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("GeolocatorWindows"));
|
||||
SharePlusWindowsPluginCApiRegisterWithRegistrar(
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#
|
||||
|
||||
list(APPEND FLUTTER_PLUGIN_LIST
|
||||
file_selector_windows
|
||||
geolocator_windows
|
||||
share_plus
|
||||
url_launcher_windows
|
||||
|
||||
Reference in New Issue
Block a user