mirror of
https://github.com/wanghongenpin/proxypin.git
synced 2026-03-19 05:19:47 +08:00
379 lines
12 KiB
Dart
379 lines
12 KiB
Dart
/*
|
|
* Copyright 2024 Hongen Wang All rights reserved.
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* https://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*/
|
|
|
|
import 'dart:async';
|
|
import 'dart:io';
|
|
import 'dart:ui' as ui;
|
|
|
|
import 'package:desktop_multi_window/desktop_multi_window.dart';
|
|
import 'package:file_picker/file_picker.dart';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter/rendering.dart';
|
|
import 'package:flutter/services.dart';
|
|
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
|
import 'package:flutter_qr_reader/flutter_qr_reader.dart';
|
|
import 'package:flutter_toastr/flutter_toastr.dart';
|
|
import 'package:image_pickers/image_pickers.dart';
|
|
import 'package:proxypin/ui/component/app_dialog.dart';
|
|
import 'package:proxypin/ui/component/qrcode/qr_scan_view.dart';
|
|
import 'package:proxypin/ui/component/text_field.dart';
|
|
import 'package:proxypin/utils/platform.dart';
|
|
import 'package:qr_flutter/qr_flutter.dart';
|
|
|
|
///二维码
|
|
///@author Hongen Wang
|
|
class QrCodePage extends StatefulWidget {
|
|
final int? windowId;
|
|
|
|
const QrCodePage({super.key, this.windowId});
|
|
|
|
@override
|
|
State<StatefulWidget> createState() {
|
|
return _QrCodePageState();
|
|
}
|
|
}
|
|
|
|
class _QrCodePageState extends State<QrCodePage> with SingleTickerProviderStateMixin {
|
|
TabController? tabController;
|
|
|
|
late List<Tab> tabs = [
|
|
Tab(text: 'Encode'),
|
|
Tab(text: 'Decode'),
|
|
];
|
|
|
|
AppLocalizations get localizations => AppLocalizations.of(context)!;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
if (Platforms.isMobile()) {
|
|
tabController = TabController(initialIndex: 0, length: tabs.length, vsync: this);
|
|
}
|
|
|
|
if (Platforms.isDesktop() && widget.windowId != null) {
|
|
HardwareKeyboard.instance.addHandler(onKeyEvent);
|
|
}
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
tabController?.dispose();
|
|
HardwareKeyboard.instance.removeHandler(onKeyEvent);
|
|
super.dispose();
|
|
}
|
|
|
|
bool onKeyEvent(KeyEvent event) {
|
|
if (widget.windowId == null) return false;
|
|
if ((HardwareKeyboard.instance.isMetaPressed || HardwareKeyboard.instance.isControlPressed) &&
|
|
event.logicalKey == LogicalKeyboardKey.keyW) {
|
|
HardwareKeyboard.instance.removeHandler(onKeyEvent);
|
|
WindowController.fromWindowId(widget.windowId!).close();
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
if (Platforms.isDesktop()) {
|
|
return Scaffold(
|
|
appBar: AppBar(title: Text(localizations.qrCode, style: const TextStyle(fontSize: 16)), centerTitle: true),
|
|
body: _QrEncode(windowId: widget.windowId));
|
|
}
|
|
|
|
tabs = [
|
|
Tab(text: localizations.encode),
|
|
Tab(text: localizations.decode),
|
|
];
|
|
|
|
return Scaffold(
|
|
appBar: AppBar(
|
|
title: Text(localizations.qrCode, style: const TextStyle(fontSize: 16)),
|
|
centerTitle: true,
|
|
bottom: TabBar(tabs: tabs, controller: tabController)),
|
|
resizeToAvoidBottomInset: false,
|
|
body: TabBarView(
|
|
controller: tabController,
|
|
children: [_QrEncode(windowId: widget.windowId), _QrDecode(windowId: widget.windowId)],
|
|
));
|
|
}
|
|
}
|
|
|
|
class _QrDecode extends StatefulWidget {
|
|
final int? windowId;
|
|
|
|
const _QrDecode({this.windowId});
|
|
|
|
@override
|
|
State<StatefulWidget> createState() {
|
|
return _QrDecodeState();
|
|
}
|
|
}
|
|
|
|
class _QrDecodeState extends State<_QrDecode> with AutomaticKeepAliveClientMixin {
|
|
TextEditingController decodeData = TextEditingController();
|
|
|
|
AppLocalizations get localizations => AppLocalizations.of(context)!;
|
|
|
|
@override
|
|
void dispose() {
|
|
decodeData.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
@override
|
|
bool get wantKeepAlive => true;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
super.build(context);
|
|
|
|
return ListView(children: [
|
|
SizedBox(height: 15),
|
|
Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
const SizedBox(width: 10),
|
|
FilledButton.icon(
|
|
onPressed: () async {
|
|
String? path = await selectImage();
|
|
if (path == null) return;
|
|
var result = await FlutterQrReader.imgScan(path);
|
|
if (result.isEmpty) {
|
|
if (context.mounted) FlutterToastr.show(localizations.decodeFail, context, duration: 2);
|
|
return;
|
|
}
|
|
decodeData.text = result;
|
|
},
|
|
icon: const Icon(Icons.photo, size: 18),
|
|
style: ButtonStyle(
|
|
padding: MaterialStateProperty.all<EdgeInsets>(const EdgeInsets.symmetric(horizontal: 15, vertical: 8)),
|
|
shape: MaterialStateProperty.all<RoundedRectangleBorder>(
|
|
RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)))),
|
|
label: Text(localizations.selectImage)),
|
|
const SizedBox(width: 10),
|
|
if (Platforms.isMobile())
|
|
FilledButton.icon(
|
|
onPressed: () async {
|
|
var scanRes = await QrCodeScanner.scan(context);
|
|
if (scanRes == null) return;
|
|
|
|
if (scanRes == "-1") {
|
|
if (context.mounted) FlutterToastr.show(localizations.invalidQRCode, context, duration: 2);
|
|
return;
|
|
}
|
|
decodeData.text = scanRes;
|
|
},
|
|
style: ButtonStyle(
|
|
padding: MaterialStateProperty.all<EdgeInsets>(const EdgeInsets.symmetric(horizontal: 15, vertical: 8)),
|
|
shape: MaterialStateProperty.all<RoundedRectangleBorder>(
|
|
RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)))),
|
|
icon: const Icon(Icons.qr_code_scanner_outlined, size: 18),
|
|
label: Text(localizations.scanQrCode, style: const TextStyle(fontSize: 14))),
|
|
const SizedBox(width: 10),
|
|
],
|
|
),
|
|
const SizedBox(height: 20),
|
|
Container(
|
|
padding: const EdgeInsets.all(10),
|
|
height: 300,
|
|
child: Column(children: [
|
|
TextField(
|
|
controller: decodeData,
|
|
maxLines: 7,
|
|
minLines: 7,
|
|
readOnly: true,
|
|
decoration: decoration(context, label: localizations.encodeResult),
|
|
),
|
|
SizedBox(height: 8),
|
|
TextButton.icon(
|
|
icon: const Icon(Icons.copy_rounded),
|
|
onPressed: () {
|
|
if (decodeData.text.isEmpty) return;
|
|
Clipboard.setData(ClipboardData(text: decodeData.text));
|
|
FlutterToastr.show(localizations.copied, context);
|
|
},
|
|
label: Text(localizations.copy),
|
|
),
|
|
])),
|
|
SizedBox(height: 10),
|
|
]);
|
|
}
|
|
|
|
//选择照片
|
|
Future<String?> selectImage() async {
|
|
if (Platforms.isMobile()) {
|
|
List<Media> listImagePaths = await ImagePickers.pickerPaths(showCamera: true);
|
|
if (listImagePaths.isEmpty) return null;
|
|
return listImagePaths[0].path;
|
|
}
|
|
|
|
if (Platforms.isDesktop()) {
|
|
//<String>['jpg', 'png', 'jpeg']
|
|
FilePickerResult? result = await FilePicker.platform.pickFiles(type: FileType.image);
|
|
if (result == null || result.files.isEmpty) return null;
|
|
return result.files.single.path;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
}
|
|
|
|
class _QrEncode extends StatefulWidget {
|
|
final int? windowId;
|
|
|
|
const _QrEncode({this.windowId});
|
|
|
|
@override
|
|
State<StatefulWidget> createState() => _QrEncodeState();
|
|
}
|
|
|
|
//生成二维码
|
|
class _QrEncodeState extends State<_QrEncode> with AutomaticKeepAliveClientMixin {
|
|
var errorCorrectLevel = QrErrorCorrectLevel.M;
|
|
String? data;
|
|
TextEditingController inputData = TextEditingController();
|
|
final GlobalKey imageKey = GlobalKey();
|
|
|
|
AppLocalizations get localizations => AppLocalizations.of(context)!;
|
|
|
|
@override
|
|
void dispose() {
|
|
inputData.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
@override
|
|
bool get wantKeepAlive => true;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
super.build(context);
|
|
|
|
return ListView(children: [
|
|
Container(
|
|
padding: const EdgeInsets.all(10),
|
|
height: 180,
|
|
child: TextField(
|
|
controller: inputData,
|
|
maxLines: 8,
|
|
onTapOutside: (event) => FocusManager.instance.primaryFocus?.unfocus(),
|
|
decoration: decoration(context, label: localizations.inputContent))),
|
|
Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
const SizedBox(width: 10),
|
|
Row(children: [
|
|
Text("${localizations.errorCorrectLevel}: "),
|
|
DropdownButton<int>(
|
|
value: errorCorrectLevel,
|
|
items: QrErrorCorrectLevel.levels
|
|
.map((e) => DropdownMenuItem<int>(value: e, child: Text(QrErrorCorrectLevel.getName(e))))
|
|
.toList(),
|
|
onChanged: (value) {
|
|
setState(() {
|
|
errorCorrectLevel = value!;
|
|
});
|
|
}),
|
|
]),
|
|
const SizedBox(width: 15),
|
|
FilledButton.icon(
|
|
onPressed: () {
|
|
setState(() {
|
|
data = inputData.text;
|
|
});
|
|
},
|
|
style: ButtonStyle(
|
|
padding: MaterialStateProperty.all<EdgeInsets>(const EdgeInsets.symmetric(horizontal: 15, vertical: 8)),
|
|
shape: MaterialStateProperty.all<RoundedRectangleBorder>(
|
|
RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)))),
|
|
icon: const Icon(Icons.qr_code, size: 18),
|
|
label: Text(localizations.generateQrCode, style: TextStyle(fontSize: 14))),
|
|
const SizedBox(width: 10),
|
|
],
|
|
),
|
|
const SizedBox(height: 10),
|
|
if (data != null && data?.isNotEmpty == true)
|
|
Column(
|
|
children: [
|
|
Row(mainAxisAlignment: MainAxisAlignment.end, children: [
|
|
TextButton.icon(
|
|
onPressed: () async {
|
|
await saveImage();
|
|
},
|
|
icon: const Icon(Icons.download_rounded),
|
|
label: Text(localizations.saveImage)),
|
|
SizedBox(width: 20),
|
|
]),
|
|
SizedBox(height: 5),
|
|
Center(
|
|
child: RepaintBoundary(
|
|
key: imageKey,
|
|
child: QrImageView(
|
|
size: 300,
|
|
data: inputData.text,
|
|
backgroundColor: Colors.white,
|
|
errorCorrectionLevel: errorCorrectLevel))),
|
|
],
|
|
),
|
|
SizedBox(height: 15),
|
|
]);
|
|
}
|
|
|
|
//保存相册
|
|
saveImage() async {
|
|
if (data == null || data!.isEmpty) {
|
|
return;
|
|
}
|
|
|
|
if (Platforms.isMobile()) {
|
|
var imageBytes = await toImageBytes();
|
|
if (imageBytes == null) return;
|
|
String? path = await ImagePickers.saveByteDataImageToGallery(imageBytes);
|
|
if (path != null && mounted) {
|
|
FlutterToastr.show(localizations.saveSuccess, context, duration: 2, rootNavigator: true);
|
|
}
|
|
return;
|
|
}
|
|
|
|
String? path;
|
|
if (Platform.isMacOS) {
|
|
path = await DesktopMultiWindow.invokeMethod(0, "saveFile", {"fileName": "qrcode.png"});
|
|
WindowController.fromWindowId(widget.windowId!).show();
|
|
} else {
|
|
path = (await FilePicker.platform.saveFile(fileName: "qrcode.png", initialDirectory: "~/Downloads"));
|
|
}
|
|
|
|
if (path == null) return;
|
|
|
|
var imageBytes = await toImageBytes();
|
|
if (imageBytes == null) return;
|
|
|
|
await File(path).writeAsBytes(imageBytes);
|
|
if (mounted) {
|
|
CustomToast.success(localizations.saveSuccess).show(context);
|
|
}
|
|
}
|
|
|
|
Future<Uint8List?> toImageBytes() async {
|
|
RenderRepaintBoundary render = imageKey.currentContext!.findRenderObject() as RenderRepaintBoundary;
|
|
ui.Image image = await render.toImage();
|
|
ByteData? byteData = await image.toByteData(format: ui.ImageByteFormat.png);
|
|
return byteData?.buffer.asUint8List();
|
|
}
|
|
}
|