feat: CommunityInputMethod

This commit is contained in:
2024-11-05 22:55:00 +08:00
parent 281f6ee995
commit 9f7e0dad52
9 changed files with 573 additions and 5 deletions

View File

@ -0,0 +1,133 @@
import 'package:fluent_ui/fluent_ui.dart';
import 'package:flutter/services.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:go_router/go_router.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/widgets/widgets.dart';
class InputMethodDialogUI extends HookConsumerWidget {
const InputMethodDialogUI({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final state = ref.watch(inputMethodDialogUIModelProvider);
final model = ref.read(inputMethodDialogUIModelProvider.notifier);
final srcTextCtrl = useTextEditingController();
final destTextCtrl = useTextEditingController();
return ContentDialog(
constraints: BoxConstraints(
maxWidth: MediaQuery.of(context).size.width * .8,
),
title: makeTitle(context, state, model, destTextCtrl),
content: state.keyMaps == null
? makeLoading(context)
: Column(
mainAxisSize: MainAxisSize.min,
children: [
SizedBox(height: 12),
InfoBar(
title: Text("使用说明"),
content: Text(
"在上方文本框中输入文字,并将下方转码后的文本复制到游戏的文本框中,即可在聊天频道中发送游戏不支持输入的文字。"),
),
SizedBox(height: 24),
TextFormBox(
placeholder: "请输入文本...",
controller: srcTextCtrl,
maxLines: 5,
placeholderStyle:
TextStyle(color: Colors.white.withOpacity(.6)),
style: TextStyle(fontSize: 16, color: Colors.white),
onChanged: (str) {
final text = model.onTextChange("src", str);
if (text != null) {
destTextCtrl.text = text;
}
},
),
SizedBox(height: 16),
Center(
child: Icon(FluentIcons.down),
),
SizedBox(height: 16),
TextFormBox(
placeholder: "这里是转码后的文本...",
controller: destTextCtrl,
maxLines: 5,
placeholderStyle:
TextStyle(color: Colors.white.withOpacity(.6)),
style: TextStyle(fontSize: 16, color: Colors.white),
enabled: true,
onChanged: (str) {
// final text = model.onTextChange("dest", str);
// if (text != null) {
// srcTextCtrl.text = text;
// }
},
),
SizedBox(height: 32),
Row(
children: [
Expanded(
child: Text(
textAlign: TextAlign.end,
"*本功能建议仅在非公共频道中使用。若用户选择在公共频道中使用本功能,由此产生的任何后果(包括但不限于被其他玩家举报刷屏等),均由用户自行承担。\n*若该功能被滥用,我们将关闭该功能。",
style: TextStyle(
fontSize: 13,
color: Colors.white.withOpacity(.6),
),
),
)
],
),
],
),
);
}
Widget makeTitle(BuildContext context, InputMethodDialogUIState state,
InputMethodDialogUIModel model, TextEditingController destTextCtrl) {
return Row(
children: [
IconButton(
icon: const Icon(
FluentIcons.back,
size: 22,
),
onPressed: () {
context.pop();
}),
const SizedBox(width: 12),
Text("社区输入法"),
Spacer(),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
Text(
"自动复制",
style: TextStyle(fontSize: 14),
),
SizedBox(width: 12),
ToggleSwitch(
checked: state.enableAutoCopy,
onChanged: model.onSwitchAutoCopy),
],
),
SizedBox(width: 24),
FilledButton(
child: Padding(
padding: const EdgeInsets.all(6),
child: Icon(FluentIcons.copy),
),
onPressed: () {
if (destTextCtrl.text.isNotEmpty) {
Clipboard.setData(ClipboardData(text: destTextCtrl.text));
}
},
)
],
);
}
}

View File

@ -0,0 +1,117 @@
import 'dart:async';
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:hive/hive.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:starcitizen_doctor/common/utils/log.dart';
import 'package:starcitizen_doctor/ui/home/localization/localization_ui_model.dart';
part 'input_method_dialog_ui_model.g.dart';
part 'input_method_dialog_ui_model.freezed.dart';
@freezed
class InputMethodDialogUIState with _$InputMethodDialogUIState {
factory InputMethodDialogUIState(
Map<String, String>? keyMaps,
Map<String, String>? worldMaps, {
@Default(false) bool enableAutoCopy,
}) = _InputMethodDialogUIState;
}
@riverpod
class InputMethodDialogUIModel extends _$InputMethodDialogUIModel {
@override
InputMethodDialogUIState build() {
state = InputMethodDialogUIState(null, null);
_init();
return state;
}
_init() async {
final localizationState = ref.watch(localizationUIModelProvider);
final localizationModel = ref.read(localizationUIModelProvider.notifier);
if (localizationState.installedCommunityInputMethodSupportVersion == null) {
return;
}
final keyMaps =
await localizationModel.getCommunityInputMethodSupportData();
dPrint("[InputMethodDialogUIModel] keyMapsLen: ${keyMaps?.length}");
final worldMaps = keyMaps?.map((key, value) => MapEntry(value.trim(), key));
final appBox = await Hive.openBox("app_conf");
final enableAutoCopy = appBox.get("enableAutoCopy", defaultValue: false);
state = state.copyWith(
keyMaps: keyMaps,
worldMaps: worldMaps,
enableAutoCopy: enableAutoCopy,
);
}
void onSwitchAutoCopy(bool value) async {
final appBox = await Hive.openBox("app_conf");
appBox.put("enableAutoCopy", value);
state = state.copyWith(enableAutoCopy: value);
}
String? onTextChange(String type, String str) {
if (state.keyMaps == null || state.worldMaps == null) return null;
StringBuffer sb = StringBuffer();
final r = RegExp(r'^[a-zA-Z0-9\p{P}\p{S}]+$');
if (type == "src") {
final map = state.worldMaps!;
// text to code
var leftSafe = true;
for (var c in str.characters) {
if (r.hasMatch((c))) {
if (leftSafe) {
sb.write(c);
} else {
sb.write(" $c");
}
leftSafe = true;
continue;
} else {
// 特殊字符,开始转码
final code = map[c.trim()];
// dPrint("c:$c code: $code");
if (code != null) {
if (leftSafe) {
sb.write(" ");
}
sb.write("@$code");
} else {
// 不支持转码,用空格代替
sb.write(" ");
}
leftSafe = false;
}
}
}
if (sb.toString().trim().isEmpty) {
return "";
}
final text = "[zh] ${sb.toString()}";
_handleAutoCopy(text);
return text;
}
Timer? _autoCopyTimer;
// 打字结束后的1秒后自动复制避免频繁复制
void _handleAutoCopy(String text) {
if (_autoCopyTimer != null) {
_autoCopyTimer?.cancel();
_autoCopyTimer = null;
}
if (!state.enableAutoCopy) return;
_autoCopyTimer = Timer(const Duration(seconds: 1), () {
if (state.enableAutoCopy) {
dPrint("auto copy: $text");
Clipboard.setData(ClipboardData(text: text));
}
});
}
}

View File

@ -0,0 +1,215 @@
// 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 'input_method_dialog_ui_model.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 _$InputMethodDialogUIState {
Map<String, String>? get keyMaps => throw _privateConstructorUsedError;
Map<String, String>? get worldMaps => throw _privateConstructorUsedError;
bool get enableAutoCopy => throw _privateConstructorUsedError;
/// Create a copy of InputMethodDialogUIState
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
$InputMethodDialogUIStateCopyWith<InputMethodDialogUIState> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $InputMethodDialogUIStateCopyWith<$Res> {
factory $InputMethodDialogUIStateCopyWith(InputMethodDialogUIState value,
$Res Function(InputMethodDialogUIState) then) =
_$InputMethodDialogUIStateCopyWithImpl<$Res, InputMethodDialogUIState>;
@useResult
$Res call(
{Map<String, String>? keyMaps,
Map<String, String>? worldMaps,
bool enableAutoCopy});
}
/// @nodoc
class _$InputMethodDialogUIStateCopyWithImpl<$Res,
$Val extends InputMethodDialogUIState>
implements $InputMethodDialogUIStateCopyWith<$Res> {
_$InputMethodDialogUIStateCopyWithImpl(this._value, this._then);
// ignore: unused_field
final $Val _value;
// ignore: unused_field
final $Res Function($Val) _then;
/// Create a copy of InputMethodDialogUIState
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
Object? keyMaps = freezed,
Object? worldMaps = freezed,
Object? enableAutoCopy = null,
}) {
return _then(_value.copyWith(
keyMaps: freezed == keyMaps
? _value.keyMaps
: keyMaps // ignore: cast_nullable_to_non_nullable
as Map<String, String>?,
worldMaps: freezed == worldMaps
? _value.worldMaps
: worldMaps // ignore: cast_nullable_to_non_nullable
as Map<String, String>?,
enableAutoCopy: null == enableAutoCopy
? _value.enableAutoCopy
: enableAutoCopy // ignore: cast_nullable_to_non_nullable
as bool,
) as $Val);
}
}
/// @nodoc
abstract class _$$InputMethodDialogUIStateImplCopyWith<$Res>
implements $InputMethodDialogUIStateCopyWith<$Res> {
factory _$$InputMethodDialogUIStateImplCopyWith(
_$InputMethodDialogUIStateImpl value,
$Res Function(_$InputMethodDialogUIStateImpl) then) =
__$$InputMethodDialogUIStateImplCopyWithImpl<$Res>;
@override
@useResult
$Res call(
{Map<String, String>? keyMaps,
Map<String, String>? worldMaps,
bool enableAutoCopy});
}
/// @nodoc
class __$$InputMethodDialogUIStateImplCopyWithImpl<$Res>
extends _$InputMethodDialogUIStateCopyWithImpl<$Res,
_$InputMethodDialogUIStateImpl>
implements _$$InputMethodDialogUIStateImplCopyWith<$Res> {
__$$InputMethodDialogUIStateImplCopyWithImpl(
_$InputMethodDialogUIStateImpl _value,
$Res Function(_$InputMethodDialogUIStateImpl) _then)
: super(_value, _then);
/// Create a copy of InputMethodDialogUIState
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
Object? keyMaps = freezed,
Object? worldMaps = freezed,
Object? enableAutoCopy = null,
}) {
return _then(_$InputMethodDialogUIStateImpl(
freezed == keyMaps
? _value._keyMaps
: keyMaps // ignore: cast_nullable_to_non_nullable
as Map<String, String>?,
freezed == worldMaps
? _value._worldMaps
: worldMaps // ignore: cast_nullable_to_non_nullable
as Map<String, String>?,
enableAutoCopy: null == enableAutoCopy
? _value.enableAutoCopy
: enableAutoCopy // ignore: cast_nullable_to_non_nullable
as bool,
));
}
}
/// @nodoc
class _$InputMethodDialogUIStateImpl implements _InputMethodDialogUIState {
_$InputMethodDialogUIStateImpl(
final Map<String, String>? keyMaps, final Map<String, String>? worldMaps,
{this.enableAutoCopy = false})
: _keyMaps = keyMaps,
_worldMaps = worldMaps;
final Map<String, String>? _keyMaps;
@override
Map<String, String>? get keyMaps {
final value = _keyMaps;
if (value == null) return null;
if (_keyMaps is EqualUnmodifiableMapView) return _keyMaps;
// ignore: implicit_dynamic_type
return EqualUnmodifiableMapView(value);
}
final Map<String, String>? _worldMaps;
@override
Map<String, String>? get worldMaps {
final value = _worldMaps;
if (value == null) return null;
if (_worldMaps is EqualUnmodifiableMapView) return _worldMaps;
// ignore: implicit_dynamic_type
return EqualUnmodifiableMapView(value);
}
@override
@JsonKey()
final bool enableAutoCopy;
@override
String toString() {
return 'InputMethodDialogUIState(keyMaps: $keyMaps, worldMaps: $worldMaps, enableAutoCopy: $enableAutoCopy)';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$InputMethodDialogUIStateImpl &&
const DeepCollectionEquality().equals(other._keyMaps, _keyMaps) &&
const DeepCollectionEquality()
.equals(other._worldMaps, _worldMaps) &&
(identical(other.enableAutoCopy, enableAutoCopy) ||
other.enableAutoCopy == enableAutoCopy));
}
@override
int get hashCode => Object.hash(
runtimeType,
const DeepCollectionEquality().hash(_keyMaps),
const DeepCollectionEquality().hash(_worldMaps),
enableAutoCopy);
/// Create a copy of InputMethodDialogUIState
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@override
@pragma('vm:prefer-inline')
_$$InputMethodDialogUIStateImplCopyWith<_$InputMethodDialogUIStateImpl>
get copyWith => __$$InputMethodDialogUIStateImplCopyWithImpl<
_$InputMethodDialogUIStateImpl>(this, _$identity);
}
abstract class _InputMethodDialogUIState implements InputMethodDialogUIState {
factory _InputMethodDialogUIState(
final Map<String, String>? keyMaps, final Map<String, String>? worldMaps,
{final bool enableAutoCopy}) = _$InputMethodDialogUIStateImpl;
@override
Map<String, String>? get keyMaps;
@override
Map<String, String>? get worldMaps;
@override
bool get enableAutoCopy;
/// Create a copy of InputMethodDialogUIState
/// with the given fields replaced by the non-null parameter values.
@override
@JsonKey(includeFromJson: false, includeToJson: false)
_$$InputMethodDialogUIStateImplCopyWith<_$InputMethodDialogUIStateImpl>
get copyWith => throw _privateConstructorUsedError;
}

View File

@ -0,0 +1,28 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'input_method_dialog_ui_model.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
String _$inputMethodDialogUIModelHash() =>
r'48955b06db0b5fdc8ae5e59b93fdd9a95b391487';
/// See also [InputMethodDialogUIModel].
@ProviderFor(InputMethodDialogUIModel)
final inputMethodDialogUIModelProvider = AutoDisposeNotifierProvider<
InputMethodDialogUIModel, InputMethodDialogUIState>.internal(
InputMethodDialogUIModel.new,
name: r'inputMethodDialogUIModelProvider',
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
? null
: _$inputMethodDialogUIModelHash,
dependencies: null,
allTransitiveDependencies: null,
);
typedef _$InputMethodDialogUIModel
= AutoDisposeNotifier<InputMethodDialogUIState>;
// 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