|
@@ -0,0 +1,114 @@
|
|
|
|
|
+import 'package:flutter/cupertino.dart';
|
|
|
|
|
+import 'package:flutter/foundation.dart';
|
|
|
|
|
+import 'package:flutter/services.dart';
|
|
|
|
|
+import 'package:get/get.dart';
|
|
|
|
|
+import 'package:get/get_core/src/get_main.dart';
|
|
|
|
|
+import 'package:xml/xml.dart';
|
|
|
|
|
+import '../config/flutter_string_get_config.dart';
|
|
|
|
|
+import '../util/de_bounce.dart';
|
|
|
|
|
+import '../util/string_gen_util.dart';
|
|
|
|
|
+
|
|
|
|
|
+class StringHotRenewalApp extends StatelessWidget {
|
|
|
|
|
+ final Widget child;
|
|
|
|
|
+
|
|
|
|
|
+ const StringHotRenewalApp({required this.child});
|
|
|
|
|
+
|
|
|
|
|
+ @override
|
|
|
|
|
+ Widget build(BuildContext context) {
|
|
|
|
|
+ if (kDebugMode) {
|
|
|
|
|
+ return HotRenewalView(child: child);
|
|
|
|
|
+ } else {
|
|
|
|
|
+ return child;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+class HotRenewalView extends StatefulWidget {
|
|
|
|
|
+ final Widget child;
|
|
|
|
|
+
|
|
|
|
|
+ late final String parentDirPath;
|
|
|
|
|
+
|
|
|
|
|
+ final FlutterStringGetConfig config = FlutterStringGetConfig.fromProject();
|
|
|
|
|
+
|
|
|
|
|
+ HotRenewalView({required this.child}) {
|
|
|
|
|
+ parentDirPath = StringGenUtil.extractBaseFolder(config.inputDir);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @override
|
|
|
|
|
+ State<HotRenewalView> createState() => _HotRenewalViewState();
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+class _HotRenewalViewState extends State<HotRenewalView> {
|
|
|
|
|
+ final Debounce reloadDebounce = Debounce(debounceTime: 1000);
|
|
|
|
|
+
|
|
|
|
|
+ final Map<String, int> _lastXmlHashMap = {}; // 记录每个 XML 的上次内容哈希值
|
|
|
|
|
+
|
|
|
|
|
+ @override
|
|
|
|
|
+ void reassemble() {
|
|
|
|
|
+ super.reassemble();
|
|
|
|
|
+ reloadDebounce.onClick(() => _reloadTranslations());
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ void _reloadTranslations() async {
|
|
|
|
|
+ final dirList =
|
|
|
|
|
+ await StringGenUtil.listStringXmlFiles(widget.parentDirPath);
|
|
|
|
|
+ if (dirList.isEmpty) return;
|
|
|
|
|
+
|
|
|
|
|
+ bool hasChanged = false;
|
|
|
|
|
+ final updatedXmlMap = <String, String>{};
|
|
|
|
|
+
|
|
|
|
|
+ for (final path in dirList) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ final xml = await rootBundle.loadString(path);
|
|
|
|
|
+ final hash = xml.hashCode;
|
|
|
|
|
+
|
|
|
|
|
+ if (_lastXmlHashMap[path] != hash) {
|
|
|
|
|
+ hasChanged = true;
|
|
|
|
|
+ _lastXmlHashMap[path] = hash;
|
|
|
|
|
+ updatedXmlMap[path] = xml;
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (e) {}
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (!hasChanged) {
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ final map = await convertMapData(updatedXmlMap, widget.config.language);
|
|
|
|
|
+ Get.clearTranslations();
|
|
|
|
|
+ Get.addTranslations(map);
|
|
|
|
|
+ Get.forceAppUpdate();
|
|
|
|
|
+ print('♻️ 热重载触发,更新翻译');
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ Future<Map<String, Map<String, String>>> convertMapData(
|
|
|
|
|
+ Map<String, String> xmlContentMap, String defaultLanguage) async {
|
|
|
|
|
+ Map<String, Map<String, String>> multiLangMap = {};
|
|
|
|
|
+
|
|
|
|
|
+ for (final entry in xmlContentMap.entries) {
|
|
|
|
|
+ final assetPath = entry.key;
|
|
|
|
|
+ final xmlString = entry.value;
|
|
|
|
|
+
|
|
|
|
|
+ final languageCode =
|
|
|
|
|
+ StringGenUtil.extractLanguageCode(assetPath, defaultLanguage);
|
|
|
|
|
+ final document = XmlDocument.parse(xmlString);
|
|
|
|
|
+
|
|
|
|
|
+ for (final element in document.findAllElements('string')) {
|
|
|
|
|
+ final name = element.getAttribute('name');
|
|
|
|
|
+ final value = StringGenUtil.processXmlText(element.text);
|
|
|
|
|
+
|
|
|
|
|
+ multiLangMap.putIfAbsent(languageCode, () => {});
|
|
|
|
|
+ if (name != null) {
|
|
|
|
|
+ multiLangMap[languageCode]![name] = value;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return multiLangMap;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @override
|
|
|
|
|
+ Widget build(BuildContext context) {
|
|
|
|
|
+ return widget.child;
|
|
|
|
|
+ }
|
|
|
|
|
+}
|