feat: web 输入支持

This commit is contained in:
xkeyC 2024-11-07 00:34:11 +08:00
parent 1681e2407b
commit 472fdb08fb
20 changed files with 759 additions and 27 deletions

View File

@ -0,0 +1,53 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, shrink-to-fit=no"/>
<meta name="renderer" content="webkit"/>
<link rel="stylesheet" href="style/mdui2.css">
<link href="style/google_icons.css" rel="stylesheet">
<script src="js/mdui2.global.js"></script>
<script src="js/main.js"></script>
<title>SCToolBox Community Input Method Web</title>
<style>
</style>
</head>
<body>
<div class="mdui-theme-light" style="position: relative;overflow: hidden">
<mdui-top-app-bar
scroll-behavior="shrink"
scroll-threshold="30"
scroll-target=".scroll-behavior-shrink">
<mdui-top-app-bar-title>SC汉化盒子社区输入法</mdui-top-app-bar-title>
<div style="flex-grow: 1"></div>
<mdui-button-icon icon="help" onclick="onShowHelp()"></mdui-button-icon>
</mdui-top-app-bar>
<div class="scroll-behavior-shrink" style="overflow: auto;">
<mdui-text-field id="input_message" style="padding-top: 6pt;padding-bottom: 6pt" rows="6" variant="outlined"
label="输入消息..."></mdui-text-field>
<div style="text-align: end">
<mdui-checkbox id="auto_copy" checked>自动复制</mdui-checkbox>
</div>
<mdui-button id="send_button" icon="send" onclick="onSendMessage()" full-width>发送</mdui-button>
</div>
<mdui-snackbar id="snackbar_message" auto-close-delay="1000">Text</mdui-snackbar>
</div>
<script>
init();
</script>
</body>
</html>

View File

@ -0,0 +1,57 @@
async function init() {
try {
let response = await fetch("/api");
let responseJson = await response.json();
if (responseJson.status === "ok") {
showMessage("服务连接成功!");
} else {
showMessage("服务连接失败!" + responseJson);
}
} catch (e) {
showMessage("服务连接失败!" + e);
}
}
async function onSendMessage() {
let send_button = document.getElementById("send_button");
let input = document.getElementById("input_message");
let isAutoCopy = document.getElementById("auto_copy").checked;
let isAutoSend = document.getElementById("auto_send").checked;
let messageJson = {
"text": input.value,
"autoCopy": isAutoCopy,
"autoInput": isAutoSend
};
send_button.loading = true;
try {
let response = await fetch("/api/send", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify(messageJson)
});
let responseJson = await response.json();
console.log(responseJson);
showMessage(responseJson.message);
if (response.ok) {
input.value = "";
}
} catch (e) {
showMessage("发送失败!" + e);
}
send_button.loading = false;
}
function showMessage(message) {
let snack = document.getElementById("snackbar_message");
snack.open = false;
snack.innerText = message;
snack.open = true;
}
function onShowHelp() {
alert("在浏览器中输入文本,将发送给汉化盒子转码。" +
"\n\n自动复制勾选后自动复制转码结果到剪贴板。");
}

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,23 @@
/* fallback */
@font-face {
font-family: 'Material Icons';
font-style: normal;
font-weight: 400;
src: url(flUhRq6tzZclQEJ-Vdg-IuiaDsNc.woff2) format('woff2');
}
.material-icons {
font-family: 'Material Icons';
font-weight: normal;
font-style: normal;
font-size: 24px;
line-height: 1;
letter-spacing: normal;
text-transform: none;
display: inline-block;
white-space: nowrap;
word-wrap: normal;
direction: ltr;
-webkit-font-feature-settings: 'liga';
-webkit-font-smoothing: antialiased;
}

File diff suppressed because one or more lines are too long

View File

@ -6,4 +6,5 @@ class ConstConf {
static const isMSE = static const isMSE =
String.fromEnvironment("MSE", defaultValue: "false") == "true"; String.fromEnvironment("MSE", defaultValue: "false") == "true";
static const dohAddress = "https://223.6.6.6/resolve"; static const dohAddress = "https://223.6.6.6/resolve";
static const inputMethodServerPort = 59399;
} }

View File

@ -4,8 +4,11 @@ import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:starcitizen_doctor/ui/home/input_method/input_method_dialog_ui_model.dart'; import 'package:starcitizen_doctor/ui/home/input_method/input_method_dialog_ui_model.dart';
import 'package:starcitizen_doctor/ui/home/input_method/server.dart';
import 'package:starcitizen_doctor/widgets/widgets.dart'; import 'package:starcitizen_doctor/widgets/widgets.dart';
import 'server_qr_dialog_ui.dart';
class InputMethodDialogUI extends HookConsumerWidget { class InputMethodDialogUI extends HookConsumerWidget {
const InputMethodDialogUI({super.key}); const InputMethodDialogUI({super.key});
@ -13,9 +16,16 @@ class InputMethodDialogUI extends HookConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final state = ref.watch(inputMethodDialogUIModelProvider); final state = ref.watch(inputMethodDialogUIModelProvider);
final model = ref.read(inputMethodDialogUIModelProvider.notifier); final model = ref.read(inputMethodDialogUIModelProvider.notifier);
final serverState = ref.watch(inputMethodServerProvider);
final serverModel = ref.read(inputMethodServerProvider.notifier);
final srcTextCtrl = useTextEditingController(); final srcTextCtrl = useTextEditingController();
final destTextCtrl = useTextEditingController(); final destTextCtrl = useTextEditingController();
useEffect(() {
model.setUpController(srcTextCtrl, destTextCtrl);
return null;
}, const []);
return ContentDialog( return ContentDialog(
constraints: BoxConstraints( constraints: BoxConstraints(
maxWidth: MediaQuery.of(context).size.width * .8, maxWidth: MediaQuery.of(context).size.width * .8,
@ -67,7 +77,36 @@ class InputMethodDialogUI extends HookConsumerWidget {
// } // }
}, },
), ),
SizedBox(height: 32), SizedBox(height: 24),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
Text("远程输入服务:"),
SizedBox(width: 6),
if (serverState.isServerStartup)
Button(
onPressed: () {
showDialog(
context: context,
builder: (BuildContext context) =>
ServerQrDialogUI(),
);
},
child: Text(
serverState.serverAddressText ?? "...",
style: TextStyle(
fontSize: 14,
),
),
),
SizedBox(width: 14),
ToggleSwitch(
checked: serverState.isServerStartup,
onChanged: (b) =>
_onSwitchServer(context, b, serverModel)),
],
),
SizedBox(height: 24),
Row( Row(
children: [ children: [
Expanded( Expanded(
@ -130,4 +169,27 @@ class InputMethodDialogUI extends HookConsumerWidget {
], ],
); );
} }
Future<void> _onSwitchServer(
BuildContext context, bool value, InputMethodServer serverModel) async {
if (value) {
final userOK = await showConfirmDialogs(
context,
"确认启用远程输入?",
Text(
"开启此功能后,可通过手机访问远程服务地址,快捷输入文字,省去切换窗口的麻烦,游戏流程不中断。\n\n若弹出防火墙提示,请展开弹窗,手动勾选所有网络类型并允许,否则可能无法正常访问此功能。"),
);
if (userOK) {
// ignore: use_build_context_synchronously
await serverModel.startServer().unwrap(context: context);
if (!context.mounted) return;
showDialog(
context: context,
builder: (BuildContext context) => ServerQrDialogUI(),
);
}
} else {
await serverModel.stopServer().unwrap(context: context);
}
}
} }

View File

@ -55,7 +55,7 @@ class InputMethodDialogUIModel extends _$InputMethodDialogUIModel {
state = state.copyWith(enableAutoCopy: value); state = state.copyWith(enableAutoCopy: value);
} }
String? onTextChange(String type, String str) { String? onTextChange(String type, String str, {formWeb = false}) {
if (state.keyMaps == null || state.worldMaps == null) return null; if (state.keyMaps == null || state.worldMaps == null) return null;
StringBuffer sb = StringBuffer(); StringBuffer sb = StringBuffer();
final r = RegExp(r'^[a-zA-Z0-9\p{P}\p{S}]+$'); final r = RegExp(r'^[a-zA-Z0-9\p{P}\p{S}]+$');
@ -93,7 +93,9 @@ class InputMethodDialogUIModel extends _$InputMethodDialogUIModel {
return ""; return "";
} }
final text = "[zh] ${sb.toString()}"; final text = "[zh] ${sb.toString()}";
if (!formWeb) {
_handleAutoCopy(text); _handleAutoCopy(text);
}
return text; return text;
} }
@ -114,4 +116,26 @@ class InputMethodDialogUIModel extends _$InputMethodDialogUIModel {
}); });
} }
TextEditingController? _srcTextCtrl;
TextEditingController? _destTextCtrl;
void setUpController(
TextEditingController srcTextCtrl, TextEditingController destTextCtrl) {
_srcTextCtrl = srcTextCtrl;
_destTextCtrl = destTextCtrl;
}
Future<void> onSendText(
String text, {
bool autoCopy = false,
bool autoInput = false,
}) async {
debugPrint("[InputMethodDialogUIState] onSendText: $text");
_srcTextCtrl?.text = text;
_destTextCtrl?.text = onTextChange("src", text) ?? "";
if (_destTextCtrl?.text.isEmpty ?? true) return;
if (autoCopy) {
Clipboard.setData(ClipboardData(text: _destTextCtrl?.text ?? ""));
}
}
} }

View File

@ -7,7 +7,7 @@ part of 'input_method_dialog_ui_model.dart';
// ************************************************************************** // **************************************************************************
String _$inputMethodDialogUIModelHash() => String _$inputMethodDialogUIModelHash() =>
r'48955b06db0b5fdc8ae5e59b93fdd9a95b391487'; r'93440d8f9c5372d5350ceaa8cb00a1b0d3b0046e';
/// See also [InputMethodDialogUIModel]. /// See also [InputMethodDialogUIModel].
@ProviderFor(InputMethodDialogUIModel) @ProviderFor(InputMethodDialogUIModel)

View File

@ -0,0 +1,212 @@
import 'dart:convert';
import 'dart:io';
import 'package:flutter/rendering.dart';
import 'package:flutter/services.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:shelf/shelf.dart';
import 'package:shelf/shelf_io.dart' as shelf_io;
import 'package:starcitizen_doctor/common/conf/const_conf.dart';
import 'package:starcitizen_doctor/common/utils/log.dart';
import 'package:starcitizen_doctor/ui/home/localization/localization_ui_model.dart';
import 'input_method_dialog_ui_model.dart';
part 'server.g.dart';
part 'server.freezed.dart';
@freezed
class InputMethodServerState with _$InputMethodServerState {
const factory InputMethodServerState({
@Default(false) bool isServerStartup,
String? serverAddressText,
}) = _InputMethodServerState;
}
@riverpod
class InputMethodServer extends _$InputMethodServer {
@override
InputMethodServerState build() {
state = InputMethodServerState(isServerStartup: false);
ref.onDispose(() {
stopServer();
});
return state;
}
LocalizationUIState get _localizationUIState =>
ref.read(localizationUIModelProvider);
InputMethodDialogUIModel get _inputMethodDialogUIModel =>
ref.read(inputMethodDialogUIModelProvider.notifier);
HttpServer? _httpServer;
Future<void> stopServer() async {
if (_httpServer != null) {
await _httpServer!.close(force: true);
_httpServer = null;
state = state.copyWith(
isServerStartup: false,
);
dPrint("[InputMethodServer] stopServer");
}
}
Future<void> startServer() async {
dPrint("[InputMethodServer] startServer");
var handler =
const Pipeline().addMiddleware(logRequests()).addHandler(_onHandler);
var server = await shelf_io.serve(
handler, "0.0.0.0", ConstConf.inputMethodServerPort);
// Enable content compression
server.autoCompress = true;
dPrint('Serving at http://${server.address.host}:${server.port}');
server.autoCompress = true;
_httpServer = server;
final address = await _findAddress();
state = state.copyWith(
isServerStartup: true,
serverAddressText: address,
);
}
Future<String> _findAddress() async {
final list = <String>[];
final List<NetworkInterface> address = await NetworkInterface.list();
bool has192168 = false;
for (var value in address) {
for (var addr in value.addresses) {
if (addr.type == InternetAddressType.IPv4) {
list.add("http://${addr.address}:${ConstConf.inputMethodServerPort}");
if (addr.address.startsWith('192.168.')) {
has192168 = true;
}
}
}
}
if (has192168) {
list.removeWhere((element) => !element.contains('192.168.'));
}
if (list.isEmpty) {
list.add("获取地址失败请手动查看电脑IP");
}
return list.join(", ");
}
Future<Response> _onHandler(Request request) async {
final path = request.url.path;
dPrint("[InputMethodServer] path: $path");
Uint8List? contentByte;
String mimeType;
try {
if (path.startsWith('api')) {
return _onHandlerApi(request);
}
if (path == '/' || path == '') {
contentByte =
(await rootBundle.load('assets/web/input_method/index.html'))
.buffer
.asUint8List();
mimeType = 'text/html; charset=utf-8';
} else {
var dotOffset = path.lastIndexOf('.');
if (path.substring(dotOffset) == '.png' ||
path.substring(dotOffset) == '.ttf' ||
path.substring(dotOffset) == '.otf') {
contentByte = (await rootBundle.load('assets/web/input_method/$path'))
.buffer
.asUint8List();
} else {
contentByte = (await rootBundle.load('assets/web/input_method/$path'))
.buffer
.asUint8List();
}
mimeType = dotOffset == -1
? 'text/plain; charset=utf-8'
: {
'.html': 'text/html; charset=utf-8',
'.css': 'text/css; charset=utf-8',
'.js': 'text/javascript; charset=utf-8',
'.csv': 'text/csv; charset=utf-8',
'.txt': 'text/plain; charset=utf-8',
'.ico': 'image/x-icon',
'.jpg': 'image/jpg',
'.jpeg': 'image/jpeg',
'.png': 'image/png',
'.gif': 'image/gif',
'.svg': 'image/svg+xml',
'.json': 'application/json',
'.xml': 'application/xml',
'.ttf': 'font/ttf',
'.otf': 'font/otf'
}[path.substring(dotOffset)] ??
"application/octet-stream";
}
return Response.ok(
contentByte,
headers: {
'Content-Type': mimeType,
},
);
} catch (e) {
debugPrint(e.toString());
return Response.internalServerError();
}
}
Future<Response> _onHandlerApi(Request request) async {
final path = request.url.path;
if (path == "api") {
return Response.ok(json.encode({
"status": "ok",
"appVersion": ConstConf.appVersion,
"appVersionCode": ConstConf.appVersionCode,
"appVersionDate": ConstConf.appVersionDate,
"isMSE": ConstConf.isMSE,
"installedCommunityInputMethodSupportVersion":
_localizationUIState.installedCommunityInputMethodSupportVersion,
}));
} else if (path.startsWith("api/send") && request.method == "POST") {
final body = await request.readAsString();
final data = json.decode(body);
final text = data["text"] ?? "";
if (text.isEmpty) {
return Response.badRequest(
body: json.encode({
"result": "error",
"message": "文本不能为空!",
}));
}
final autoCopy = data["autoCopy"] ?? false;
final autoInput = data["autoInput"] ?? false;
try {
await _inputMethodDialogUIModel.onSendText(
text,
autoCopy: autoCopy,
autoInput: autoInput,
);
return Response.ok(json.encode({
"result": "ok",
"message": "发送成功!",
}));
} catch (e) {
return Response.internalServerError(
body: json.encode({
"result": "error",
"message": e.toString(),
}));
}
} else {
return Response.notFound("Not Found");
}
}
}

View File

@ -0,0 +1,171 @@
// coverage:ignore-file
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
part of 'server.dart';
// **************************************************************************
// FreezedGenerator
// **************************************************************************
T _$identity<T>(T value) => value;
final _privateConstructorUsedError = UnsupportedError(
'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models');
/// @nodoc
mixin _$InputMethodServerState {
bool get isServerStartup => throw _privateConstructorUsedError;
String? get serverAddressText => throw _privateConstructorUsedError;
/// Create a copy of InputMethodServerState
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
$InputMethodServerStateCopyWith<InputMethodServerState> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $InputMethodServerStateCopyWith<$Res> {
factory $InputMethodServerStateCopyWith(InputMethodServerState value,
$Res Function(InputMethodServerState) then) =
_$InputMethodServerStateCopyWithImpl<$Res, InputMethodServerState>;
@useResult
$Res call({bool isServerStartup, String? serverAddressText});
}
/// @nodoc
class _$InputMethodServerStateCopyWithImpl<$Res,
$Val extends InputMethodServerState>
implements $InputMethodServerStateCopyWith<$Res> {
_$InputMethodServerStateCopyWithImpl(this._value, this._then);
// ignore: unused_field
final $Val _value;
// ignore: unused_field
final $Res Function($Val) _then;
/// Create a copy of InputMethodServerState
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
Object? isServerStartup = null,
Object? serverAddressText = freezed,
}) {
return _then(_value.copyWith(
isServerStartup: null == isServerStartup
? _value.isServerStartup
: isServerStartup // ignore: cast_nullable_to_non_nullable
as bool,
serverAddressText: freezed == serverAddressText
? _value.serverAddressText
: serverAddressText // ignore: cast_nullable_to_non_nullable
as String?,
) as $Val);
}
}
/// @nodoc
abstract class _$$InputMethodServerStateImplCopyWith<$Res>
implements $InputMethodServerStateCopyWith<$Res> {
factory _$$InputMethodServerStateImplCopyWith(
_$InputMethodServerStateImpl value,
$Res Function(_$InputMethodServerStateImpl) then) =
__$$InputMethodServerStateImplCopyWithImpl<$Res>;
@override
@useResult
$Res call({bool isServerStartup, String? serverAddressText});
}
/// @nodoc
class __$$InputMethodServerStateImplCopyWithImpl<$Res>
extends _$InputMethodServerStateCopyWithImpl<$Res,
_$InputMethodServerStateImpl>
implements _$$InputMethodServerStateImplCopyWith<$Res> {
__$$InputMethodServerStateImplCopyWithImpl(
_$InputMethodServerStateImpl _value,
$Res Function(_$InputMethodServerStateImpl) _then)
: super(_value, _then);
/// Create a copy of InputMethodServerState
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
Object? isServerStartup = null,
Object? serverAddressText = freezed,
}) {
return _then(_$InputMethodServerStateImpl(
isServerStartup: null == isServerStartup
? _value.isServerStartup
: isServerStartup // ignore: cast_nullable_to_non_nullable
as bool,
serverAddressText: freezed == serverAddressText
? _value.serverAddressText
: serverAddressText // ignore: cast_nullable_to_non_nullable
as String?,
));
}
}
/// @nodoc
class _$InputMethodServerStateImpl implements _InputMethodServerState {
const _$InputMethodServerStateImpl(
{this.isServerStartup = false, this.serverAddressText});
@override
@JsonKey()
final bool isServerStartup;
@override
final String? serverAddressText;
@override
String toString() {
return 'InputMethodServerState(isServerStartup: $isServerStartup, serverAddressText: $serverAddressText)';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$InputMethodServerStateImpl &&
(identical(other.isServerStartup, isServerStartup) ||
other.isServerStartup == isServerStartup) &&
(identical(other.serverAddressText, serverAddressText) ||
other.serverAddressText == serverAddressText));
}
@override
int get hashCode =>
Object.hash(runtimeType, isServerStartup, serverAddressText);
/// Create a copy of InputMethodServerState
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@override
@pragma('vm:prefer-inline')
_$$InputMethodServerStateImplCopyWith<_$InputMethodServerStateImpl>
get copyWith => __$$InputMethodServerStateImplCopyWithImpl<
_$InputMethodServerStateImpl>(this, _$identity);
}
abstract class _InputMethodServerState implements InputMethodServerState {
const factory _InputMethodServerState(
{final bool isServerStartup,
final String? serverAddressText}) = _$InputMethodServerStateImpl;
@override
bool get isServerStartup;
@override
String? get serverAddressText;
/// Create a copy of InputMethodServerState
/// with the given fields replaced by the non-null parameter values.
@override
@JsonKey(includeFromJson: false, includeToJson: false)
_$$InputMethodServerStateImplCopyWith<_$InputMethodServerStateImpl>
get copyWith => throw _privateConstructorUsedError;
}

View File

@ -0,0 +1,26 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'server.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
String _$inputMethodServerHash() => r'4ea07de4bca3268933b78335b670c09e6fac61bc';
/// See also [InputMethodServer].
@ProviderFor(InputMethodServer)
final inputMethodServerProvider = AutoDisposeNotifierProvider<InputMethodServer,
InputMethodServerState>.internal(
InputMethodServer.new,
name: r'inputMethodServerProvider',
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
? null
: _$inputMethodServerHash,
dependencies: null,
allTransitiveDependencies: null,
);
typedef _$InputMethodServer = AutoDisposeNotifier<InputMethodServerState>;
// ignore_for_file: type=lint
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package

View File

@ -0,0 +1,95 @@
import 'package:fluent_ui/fluent_ui.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:qr_flutter/qr_flutter.dart';
import 'server.dart';
class ServerQrDialogUI extends HookConsumerWidget {
const ServerQrDialogUI({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final serverState = ref.watch(inputMethodServerProvider);
final urls = serverState.serverAddressText?.split(",") ?? [""];
final hasMultipleUrls = urls.length > 1;
final index = useState(0);
return ContentDialog(
constraints: BoxConstraints(
maxWidth: MediaQuery.of(context).size.width * .4,
),
title: makeTitle(context),
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
SizedBox(width: double.infinity, height: 12),
if (hasMultipleUrls) ...[
Text("我们没能找到合适的 ip 地址来访问服务,请您尝试以下地址(左右切换)"),
] else
Text(
"请使用您的移动设备扫描以下二维码,或手动访问连接",
style: TextStyle(color: Colors.white.withOpacity(.8)),
),
SizedBox(height: 24),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
if (hasMultipleUrls)
IconButton(
icon: Icon(FluentIcons.chevron_left),
onPressed: () {
index.value = (index.value - 1) % urls.length;
}),
SizedBox(width: 24),
Container(
color: Colors.white,
child: QrImageView(
data: urls[index.value],
size: 200,
padding: EdgeInsets.all(12),
),
),
SizedBox(width: 24),
if (hasMultipleUrls)
IconButton(
icon: Icon(FluentIcons.chevron_right),
onPressed: () {
index.value = (index.value + 1) % urls.length;
}),
],
),
SizedBox(height: 12),
Text(
hasMultipleUrls
? "(${index.value + 1} / ${urls.length})"
: urls[index.value],
style: TextStyle(fontSize: 13, color: Colors.white.withOpacity(.6)),
),
],
),
);
}
Widget makeTitle(BuildContext context) {
return Row(
children: [
IconButton(
icon: const Icon(
FluentIcons.back,
size: 22,
),
onPressed: () {
context.pop();
}),
const SizedBox(width: 12),
Text("服务二维码"),
],
);
}
}

View File

@ -3,7 +3,6 @@ import 'dart:io';
import 'package:fluent_ui/fluent_ui.dart'; import 'package:fluent_ui/fluent_ui.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/src/widgets/framework.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
@ -12,7 +11,6 @@ import 'package:re_highlight/languages/ini.dart';
import 'package:re_highlight/styles/vs2015.dart'; import 'package:re_highlight/styles/vs2015.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:starcitizen_doctor/api/analytics.dart'; import 'package:starcitizen_doctor/api/analytics.dart';
import 'package:starcitizen_doctor/common/utils/base_utils.dart';
import 'package:starcitizen_doctor/common/utils/log.dart'; import 'package:starcitizen_doctor/common/utils/log.dart';
import 'package:starcitizen_doctor/common/utils/provider.dart'; import 'package:starcitizen_doctor/common/utils/provider.dart';
import 'package:starcitizen_doctor/data/app_advanced_localization_data.dart'; import 'package:starcitizen_doctor/data/app_advanced_localization_data.dart';

View File

@ -212,7 +212,6 @@ class __$$AdvancedLocalizationUIStateImplCopyWithImpl<$Res>
/// @nodoc /// @nodoc
class _$AdvancedLocalizationUIStateImpl class _$AdvancedLocalizationUIStateImpl
with DiagnosticableTreeMixin
implements _AdvancedLocalizationUIState { implements _AdvancedLocalizationUIState {
_$AdvancedLocalizationUIStateImpl( _$AdvancedLocalizationUIStateImpl(
{this.workingText = "", {this.workingText = "",
@ -258,26 +257,10 @@ class _$AdvancedLocalizationUIStateImpl
final String errorMessage; final String errorMessage;
@override @override
String toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) { String toString() {
return 'AdvancedLocalizationUIState(workingText: $workingText, classMap: $classMap, p4kGlobalIni: $p4kGlobalIni, serverGlobalIni: $serverGlobalIni, customizeGlobalIni: $customizeGlobalIni, apiLocalizationData: $apiLocalizationData, p4kGlobalIniLines: $p4kGlobalIniLines, serverGlobalIniLines: $serverGlobalIniLines, errorMessage: $errorMessage)'; return 'AdvancedLocalizationUIState(workingText: $workingText, classMap: $classMap, p4kGlobalIni: $p4kGlobalIni, serverGlobalIni: $serverGlobalIni, customizeGlobalIni: $customizeGlobalIni, apiLocalizationData: $apiLocalizationData, p4kGlobalIniLines: $p4kGlobalIniLines, serverGlobalIniLines: $serverGlobalIniLines, errorMessage: $errorMessage)';
} }
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties
..add(DiagnosticsProperty('type', 'AdvancedLocalizationUIState'))
..add(DiagnosticsProperty('workingText', workingText))
..add(DiagnosticsProperty('classMap', classMap))
..add(DiagnosticsProperty('p4kGlobalIni', p4kGlobalIni))
..add(DiagnosticsProperty('serverGlobalIni', serverGlobalIni))
..add(DiagnosticsProperty('customizeGlobalIni', customizeGlobalIni))
..add(DiagnosticsProperty('apiLocalizationData', apiLocalizationData))
..add(DiagnosticsProperty('p4kGlobalIniLines', p4kGlobalIniLines))
..add(DiagnosticsProperty('serverGlobalIniLines', serverGlobalIniLines))
..add(DiagnosticsProperty('errorMessage', errorMessage));
}
@override @override
bool operator ==(Object other) { bool operator ==(Object other) {
return identical(this, other) || return identical(this, other) ||

View File

@ -7,7 +7,7 @@ part of 'advanced_localization_ui_model.dart';
// ************************************************************************** // **************************************************************************
String _$advancedLocalizationUIModelHash() => String _$advancedLocalizationUIModelHash() =>
r'8241143c6dec93cd705e6b2e65cbca711cdfe2fb'; r'6b65988e71733c01e9352765cf600c4781bdccb4';
/// See also [AdvancedLocalizationUIModel]. /// See also [AdvancedLocalizationUIModel].
@ProviderFor(AdvancedLocalizationUIModel) @ProviderFor(AdvancedLocalizationUIModel)

View File

@ -4,7 +4,6 @@ import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart';
import 'package:flutter_tilt/flutter_tilt.dart'; import 'package:flutter_tilt/flutter_tilt.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:starcitizen_doctor/common/utils/log.dart';
import 'package:starcitizen_doctor/data/sc_localization_data.dart'; import 'package:starcitizen_doctor/data/sc_localization_data.dart';
import 'package:starcitizen_doctor/ui/tools/tools_ui_model.dart'; import 'package:starcitizen_doctor/ui/tools/tools_ui_model.dart';
import 'package:starcitizen_doctor/widgets/widgets.dart'; import 'package:starcitizen_doctor/widgets/widgets.dart';

View File

@ -7,7 +7,7 @@ part of 'localization_ui_model.dart';
// ************************************************************************** // **************************************************************************
String _$localizationUIModelHash() => String _$localizationUIModelHash() =>
r'b8c893413fa8a314d0fa3b2cfffb63f723226bae'; r'206512f457acdb0aaa2cd638fcdb31b6a88848a6';
/// See also [LocalizationUIModel]. /// See also [LocalizationUIModel].
@ProviderFor(LocalizationUIModel) @ProviderFor(LocalizationUIModel)

View File

@ -63,6 +63,8 @@ dependencies:
file: ^7.0.0 file: ^7.0.0
re_editor: ^0.6.0 re_editor: ^0.6.0
re_highlight: ^0.0.3 re_highlight: ^0.0.3
shelf: ^1.4.1
qr_flutter: ^4.1.0
dependency_overrides: dependency_overrides:
http: ^1.1.2 http: ^1.1.2
@ -89,6 +91,9 @@ flutter:
- assets/ - assets/
- assets/binary/ - assets/binary/
- assets/countdown/ - assets/countdown/
- assets/web/input_method/
- assets/web/input_method/js/
- assets/web/input_method/style/
fonts: fonts:
- family: SourceHanSansCN-Regular - family: SourceHanSansCN-Regular