mirror of
https://github.com/babalae/better-genshin-impact.git
synced 2026-05-27 10:15:50 +08:00
* feat(i18n): 添加界面与日志的国际化支持
- 新增 ITranslationService 接口及 JsonTranslationService 实现,提供基于 JSON 的翻译服务
- 添加 TrConverter 转换器,支持通过绑定动态翻译界面文本
- 引入 AutoTranslateInterceptor 行为,自动扫描并翻译界面中的静态文本
- 集成 TranslatingSerilogLoggerProvider,实现日志输出的实时翻译
- 在 App.xaml 中注册全局样式,为 Window、UserControl 和 Page 启用自动翻译
* refactor(AutoTranslateInterceptor): 优化自动翻译拦截器的加载与应用机制
- 移除 HomePage 中冗余的 EnableAutoTranslate 属性设置,改为继承属性
- 通过类构造函数注册全局 Loaded 事件处理器,替代在每个元素上单独添加
- 引入请求队列机制,批量处理待应用翻译的元素,避免重复调度
- 扩展属性类型检查,支持 object 类型以处理更多动态内容场景
* fix: 移除全局自动翻译拦截器以避免冲突
移除在 App.xaml 中为 Window、UserControl 和 Page 全局设置的 AutoTranslateInterceptor,
改为仅在 PickerWindow 中显式启用。这解决了全局样式可能导致的意外行为或冲突。
* feat(ui): 为多个窗口启用自动翻译拦截器
为 MapLabelSearchWindow、ArtifactOcrDialog、PromptDialog 等 14 个窗口添加了 AutoTranslateInterceptor.EnableAutoTranslate 属性,以启用自动翻译拦截功能。
* feat(i18n): 添加国际化目录支持并优化异常处理
* feat(ui): 添加软件UI语言设置并改进翻译服务
- 在通用设置页面新增UI语言选择控件,支持动态切换界面语言
- 修改游戏语言标签为“原神游戏语言”以明确区分
- 改进JsonTranslationService,支持UI语言切换时的实时翻译更新
- 优化AutoTranslateInterceptor,缓存原始文本值并在语言切换时恢复
- 添加属性变更监听机制,确保UI元素在语言切换后正确刷新
* feat(自动翻译): 添加排除自动翻译的依赖属性
在 AutoTranslateInterceptor 中新增 ExcludeAutoTranslate 附加属性,允许对特定依赖对象禁用自动翻译功能。当遍历元素进行翻译时,会检查此属性并跳过已标记排除的元素。
* feat(translation): 为缺失文本翻译添加详细上下文信息
扩展翻译服务以收集缺失文本的详细上下文,包括视图路径、元素类型、属性名等。
重构 `ITranslationService` 接口,引入 `TranslationSourceInfo` 类封装上下文信息。
修改 `AutoTranslateInterceptor` 自动收集 UI 元素信息,`JsonTranslationService` 合并多来源上下文。
* Revert "feat(自动翻译): 添加排除自动翻译的依赖属性"
This reverts commit a1c2334951.
* fix: 跳过 GridViewRowPresenter 中的文本翻译
添加 IsInGridViewRowPresenter 检查,避免在 GridViewRowPresenter 控件内进行自动翻译,防止潜在的界面显示问题。
* fix: 修复自动翻译拦截器在组合框上下文中的误触发
在自动翻译拦截器中添加了 IsInComboBoxContext 方法,用于检测依赖对象是否处于 ComboBox 或其相关弹出菜单的上下文中。当检测到对象位于组合框上下文时,跳过自动翻译逻辑,避免对下拉选项等界面元素进行不必要的翻译操作,从而解决潜在的界面干扰问题。
* feat(translation): 添加缺失翻译上报至 Supabase 的功能
- 新增 IMissingTranslationReporter 接口及 SupabaseMissingTranslationReporter 实现
- 在 JsonTranslationService 中集成缺失翻译上报逻辑
- 添加缺失翻译收集的配置设置(MissingTranslationCollectionSettings)
- 优化缺失翻译文件的序列化格式,将 Source 字段改为紧凑的数字表示
- 移除 ScriptRepoUpdater 中未使用的 using 语句
- 在 App.xaml.cs 中注册 SupabaseMissingTranslationReporter 服务
* fix: 修复自动翻译功能中原始值恢复和重复报告问题
- 移除未使用的法语翻译支持以简化语言选项
- 修复 Supabase 报告序列化时移除冗余字段
- 添加已缺失翻译键的缓存以避免重复报告
- 重构自动翻译拦截器,将原始值存储移至依赖属性
- 修复原始值恢复逻辑,确保正确遍历所有子元素
* feat(ui): 添加更新UI语言文件功能
- 在 ITranslationService 接口中添加 Reload 方法
- 在 JsonTranslationService 中实现 Reload 方法,支持重新加载语言文件并发送变更通知
- 在通用设置页面添加“更新”按钮,点击后从远程仓库下载最新语言文件
- 实现 OnUpdateUiLanguageAsync 命令,支持从 GitHub 和镜像源下载语言文件
- 下载后自动替换本地文件并重新加载翻译服务
468 lines
15 KiB
C#
468 lines
15 KiB
C#
using System;
|
||
using System.Collections.Frozen;
|
||
using System.Collections.Generic;
|
||
using System.Collections.ObjectModel;
|
||
using System.Diagnostics;
|
||
using System.Globalization;
|
||
using System.IO;
|
||
using System.IO.Compression;
|
||
using System.Linq;
|
||
using System.Net;
|
||
using System.Net.Http;
|
||
using System.Text;
|
||
using System.Threading.Tasks;
|
||
using System.Windows;
|
||
using Windows.System;
|
||
using BetterGenshinImpact.Core.Config;
|
||
using BetterGenshinImpact.Core.Recognition;
|
||
using BetterGenshinImpact.Core.Recognition.OCR;
|
||
using BetterGenshinImpact.Core.Recognition.OCR.Paddle;
|
||
using BetterGenshinImpact.Core.Script;
|
||
using BetterGenshinImpact.GameTask;
|
||
using BetterGenshinImpact.GameTask.AutoTrackPath;
|
||
using BetterGenshinImpact.GameTask.Common.Element.Assets;
|
||
using BetterGenshinImpact.GameTask.LogParse;
|
||
using BetterGenshinImpact.Helpers;
|
||
using BetterGenshinImpact.Helpers.Http;
|
||
using BetterGenshinImpact.Model;
|
||
using BetterGenshinImpact.Service.Interface;
|
||
using BetterGenshinImpact.Service.Notification;
|
||
using BetterGenshinImpact.View.Controls.Webview;
|
||
using BetterGenshinImpact.View.Converters;
|
||
using BetterGenshinImpact.View.Pages;
|
||
using BetterGenshinImpact.View.Windows;
|
||
using CommunityToolkit.Mvvm.ComponentModel;
|
||
using CommunityToolkit.Mvvm.Input;
|
||
using CommunityToolkit.Mvvm.Messaging;
|
||
using CommunityToolkit.Mvvm.Messaging.Messages;
|
||
using Microsoft.Extensions.DependencyInjection;
|
||
using Microsoft.Extensions.Localization;
|
||
using Microsoft.Win32;
|
||
using Newtonsoft.Json;
|
||
using Wpf.Ui;
|
||
|
||
namespace BetterGenshinImpact.ViewModel.Pages;
|
||
|
||
public partial class CommonSettingsPageViewModel : ViewModel
|
||
{
|
||
private readonly INavigationService _navigationService;
|
||
|
||
private readonly NotificationService _notificationService;
|
||
private readonly TpConfig _tpConfig = TaskContext.Instance().Config.TpConfig;
|
||
|
||
private string _selectedArea = string.Empty;
|
||
|
||
|
||
private string _selectedCountry = string.Empty;
|
||
[ObservableProperty] private List<string> _adventurersGuildCountry = ["无", "枫丹", "稻妻", "璃月", "蒙德"];
|
||
|
||
[ObservableProperty] private List<Tuple<TimeSpan, string>> _serverTimeZones =
|
||
[
|
||
Tuple.Create(TimeSpan.FromHours(8), "其他 UTC+08"),
|
||
Tuple.Create(TimeSpan.FromHours(1), "欧服 UTC+01"),
|
||
Tuple.Create(TimeSpan.FromHours(-5), "美服 UTC-05")
|
||
];
|
||
|
||
public CommonSettingsPageViewModel(IConfigService configService, INavigationService navigationService,
|
||
NotificationService notificationService)
|
||
{
|
||
Config = configService.Get();
|
||
_navigationService = navigationService;
|
||
_notificationService = notificationService;
|
||
InitializeCountries();
|
||
InitializeMiyousheCookie();
|
||
// 初始化OCR模型选择
|
||
SelectedPaddleOcrModelConfig = Config.OtherConfig.OcrConfig.PaddleOcrModelConfig;
|
||
}
|
||
|
||
public AllConfig Config { get; set; }
|
||
public ObservableCollection<string> CountryList { get; } = new();
|
||
public ObservableCollection<string> Areas { get; } = new();
|
||
|
||
public ObservableCollection<string> MapPathingTypes { get; } = ["SIFT", "TemplateMatch"];
|
||
|
||
[ObservableProperty] private FrozenDictionary<string, string> _languageDict =
|
||
new string[] { "zh-Hans", "zh-Hant", "en"}
|
||
.ToFrozenDictionary(
|
||
c => c,
|
||
c =>
|
||
{
|
||
CultureInfo.CurrentUICulture = new CultureInfo(c);
|
||
var stringLocalizer = App.GetService<IStringLocalizer<CultureInfoNameToKVPConverter>>() ??
|
||
throw new NullReferenceException();
|
||
return stringLocalizer["简体中文"].ToString();
|
||
}
|
||
);
|
||
|
||
[RelayCommand]
|
||
private async Task OnUpdateUiLanguageAsync()
|
||
{
|
||
var cultureName = Config.OtherConfig.UiCultureInfoName ?? string.Empty;
|
||
if (string.IsNullOrWhiteSpace(cultureName))
|
||
{
|
||
throw new InvalidOperationException("当前UI语言为空,无法更新语言文件。");
|
||
}
|
||
|
||
if (cultureName == "zh-Hans")
|
||
{
|
||
await ThemedMessageBox.InformationAsync("zh-Hans 无语言文件,无需更新。");
|
||
return;
|
||
}
|
||
|
||
var urls = new[]
|
||
{
|
||
$"https://raw.githubusercontent.com/babalae/bettergi-i18n/refs/heads/main/i18n/{cultureName}.json",
|
||
$"https://cnb.cool/bettergi/bettergi-i18n/-/git/raw/main/i18n/{cultureName}.json"
|
||
};
|
||
|
||
using var httpClient = new HttpClient
|
||
{
|
||
Timeout = TimeSpan.FromSeconds(30)
|
||
};
|
||
|
||
byte[]? bytes = null;
|
||
Exception? lastError = null;
|
||
var allNotFound = true;
|
||
foreach (var url in urls)
|
||
{
|
||
try
|
||
{
|
||
using var request = new HttpRequestMessage(HttpMethod.Get, url);
|
||
request.Headers.UserAgent.ParseAdd("BetterGenshinImpact");
|
||
using var response = await httpClient.SendAsync(request);
|
||
if (response.StatusCode == HttpStatusCode.NotFound)
|
||
{
|
||
lastError = new HttpRequestException("Language file not found.", null, response.StatusCode);
|
||
continue;
|
||
}
|
||
|
||
allNotFound = false;
|
||
response.EnsureSuccessStatusCode();
|
||
bytes = await response.Content.ReadAsByteArrayAsync();
|
||
|
||
var json = Encoding.UTF8.GetString(bytes);
|
||
_ = JsonConvert.DeserializeObject<Dictionary<string, string>>(json)
|
||
?? throw new JsonException("翻译文件不是有效的 JSON 字典。");
|
||
break;
|
||
}
|
||
catch (Exception e)
|
||
{
|
||
lastError = e;
|
||
allNotFound = false;
|
||
}
|
||
}
|
||
|
||
if (bytes == null)
|
||
{
|
||
if (allNotFound)
|
||
{
|
||
await ThemedMessageBox.WarningAsync($"语言文件不存在:{cultureName}.json");
|
||
return;
|
||
}
|
||
|
||
throw new Exception($"下载语言文件失败:{cultureName}.json", lastError);
|
||
}
|
||
|
||
var dir = Global.Absolute(@"User\I18n");
|
||
Directory.CreateDirectory(dir);
|
||
var path = Path.Combine(dir, $"{cultureName}.json");
|
||
var tmp = $"{path}.{Guid.NewGuid():N}.tmp";
|
||
await File.WriteAllBytesAsync(tmp, bytes);
|
||
|
||
if (File.Exists(path))
|
||
{
|
||
File.Replace(tmp, path, null);
|
||
}
|
||
else
|
||
{
|
||
File.Move(tmp, path);
|
||
}
|
||
|
||
var translator = App.GetService<ITranslationService>() ?? throw new NullReferenceException();
|
||
translator.Reload();
|
||
}
|
||
|
||
public string SelectedCountry
|
||
{
|
||
get => _selectedCountry;
|
||
set
|
||
{
|
||
if (SetProperty(ref _selectedCountry, value))
|
||
{
|
||
UpdateAreas(value);
|
||
SelectedArea = Areas.FirstOrDefault() ?? string.Empty;
|
||
}
|
||
}
|
||
}
|
||
|
||
public string SelectedArea
|
||
{
|
||
get => _selectedArea;
|
||
set
|
||
{
|
||
if (SetProperty(ref _selectedArea, value))
|
||
{
|
||
UpdateRevivePoint(SelectedCountry, SelectedArea);
|
||
}
|
||
}
|
||
}
|
||
|
||
public ObservableCollection<PaddleOcrModelConfig> PaddleOcrModelConfigs { get; } =
|
||
new(Enum.GetValues(typeof(PaddleOcrModelConfig)).Cast<PaddleOcrModelConfig>());
|
||
|
||
[ObservableProperty] private PaddleOcrModelConfig _selectedPaddleOcrModelConfig;
|
||
|
||
[RelayCommand]
|
||
public void OnQuestionButtonOnClick()
|
||
{
|
||
// Owner = this,
|
||
WebpageWindow cookieWin = new()
|
||
{
|
||
Title = "日志分析",
|
||
Width = 800,
|
||
Height = 600,
|
||
|
||
WindowStartupLocation = WindowStartupLocation.CenterOwner
|
||
};
|
||
cookieWin.NavigateToHtml(TravelsDiaryDetailManager.generHtmlMessage());
|
||
cookieWin.Show();
|
||
}
|
||
|
||
private void InitializeMiyousheCookie()
|
||
{
|
||
OtherConfig.Miyoushe mcfg = TaskContext.Instance().Config.OtherConfig.MiyousheConfig;
|
||
if (mcfg.Cookie == string.Empty &&
|
||
mcfg.LogSyncCookie)
|
||
{
|
||
var config = LogParse.LoadConfig();
|
||
mcfg.Cookie = config.Cookie;
|
||
}
|
||
}
|
||
|
||
private void InitializeCountries()
|
||
{
|
||
var countries = MapLazyAssets.Instance.GoddessPositions.Values
|
||
.OrderBy(g => int.TryParse(g.Id, out var id) ? id : int.MaxValue)
|
||
.GroupBy(g => g.Country)
|
||
.Select(grp => grp.Key);
|
||
CountryList.Clear();
|
||
foreach (var country in countries)
|
||
{
|
||
if (!string.IsNullOrEmpty(country))
|
||
{
|
||
CountryList.Add(country);
|
||
}
|
||
}
|
||
|
||
_selectedCountry = _tpConfig.ReviveStatueOfTheSevenCountry;
|
||
UpdateAreas(SelectedCountry);
|
||
_selectedArea = _tpConfig.ReviveStatueOfTheSevenArea;
|
||
UpdateRevivePoint(SelectedCountry, SelectedArea);
|
||
}
|
||
|
||
private void UpdateAreas(string country)
|
||
{
|
||
Areas.Clear();
|
||
SelectedArea = string.Empty;
|
||
if (string.IsNullOrEmpty(country)) return;
|
||
|
||
var areas = MapLazyAssets.Instance.GoddessPositions.Values
|
||
.Where(g => g.Country == country)
|
||
.OrderBy(g => int.TryParse(g.Id, out var id) ? id : int.MaxValue)
|
||
.GroupBy(g => g.Level1Area)
|
||
.Select(grp => grp.Key);
|
||
foreach (var area in areas)
|
||
{
|
||
if (!string.IsNullOrEmpty(area))
|
||
{
|
||
Areas.Add(area);
|
||
}
|
||
}
|
||
}
|
||
|
||
// 当国家或区域改变时更新坐标
|
||
private void UpdateRevivePoint(string country, string area)
|
||
{
|
||
if (string.IsNullOrEmpty(country) || string.IsNullOrEmpty(area)) return;
|
||
|
||
var goddess = MapLazyAssets.Instance.GoddessPositions.Values
|
||
.FirstOrDefault(g => g.Country == country && g.Level1Area == area);
|
||
if (goddess == null) return;
|
||
_tpConfig.ReviveStatueOfTheSevenCountry = country;
|
||
_tpConfig.ReviveStatueOfTheSevenArea = area;
|
||
_tpConfig.ReviveStatueOfTheSevenPointX = goddess.X;
|
||
_tpConfig.ReviveStatueOfTheSevenPointY = goddess.Y;
|
||
_tpConfig.ReviveStatueOfTheSeven = goddess;
|
||
}
|
||
|
||
[RelayCommand]
|
||
public void OnRefreshMaskSettings()
|
||
{
|
||
WeakReferenceMessenger.Default.Send(
|
||
new PropertyChangedMessage<object>(this, "RefreshSettings", new object(), "重新计算控件位置"));
|
||
}
|
||
|
||
[RelayCommand]
|
||
private void OnResetMaskOverlayLayout()
|
||
{
|
||
var c = Config.MaskWindowConfig;
|
||
c.StatusListLeftRatio = 20.0 / 1920;
|
||
c.StatusListTopRatio = 807.0 / 1080;
|
||
c.StatusListWidthRatio = 477.0 / 1920;
|
||
c.StatusListHeightRatio = 24.0 / 1080;
|
||
|
||
c.LogTextBoxLeftRatio = 20.0 / 1920;
|
||
c.LogTextBoxTopRatio = 832.0 / 1080;
|
||
c.LogTextBoxWidthRatio = 477.0 / 1920;
|
||
c.LogTextBoxHeightRatio = 188.0 / 1080;
|
||
|
||
OnRefreshMaskSettings();
|
||
}
|
||
|
||
[RelayCommand]
|
||
private void OnSwitchMaskEnabled()
|
||
{
|
||
// if (Config.MaskWindowConfig.MaskEnabled)
|
||
// {
|
||
// MaskWindow.Instance().Show();
|
||
// }
|
||
// else
|
||
// {
|
||
// MaskWindow.Instance().Hide();
|
||
// }
|
||
}
|
||
|
||
[RelayCommand]
|
||
public void OnGoToHotKeyPage()
|
||
{
|
||
_navigationService.Navigate(typeof(HotKeyPage));
|
||
}
|
||
|
||
[RelayCommand]
|
||
public void OnSwitchTakenScreenshotEnabled()
|
||
{
|
||
}
|
||
|
||
[RelayCommand]
|
||
public void OnGoToFolder()
|
||
{
|
||
var path = Global.Absolute(@"log\screenshot\");
|
||
if (!Directory.Exists(path))
|
||
{
|
||
Directory.CreateDirectory(path);
|
||
}
|
||
|
||
Process.Start("explorer.exe", path);
|
||
}
|
||
|
||
[RelayCommand]
|
||
public void OnGoToLogFolder()
|
||
{
|
||
var path = Global.Absolute(@"log");
|
||
if (!Directory.Exists(path))
|
||
{
|
||
Directory.CreateDirectory(path);
|
||
}
|
||
|
||
Process.Start("explorer.exe", path);
|
||
}
|
||
|
||
[RelayCommand]
|
||
private void ImportLocalScriptsRepoZip()
|
||
{
|
||
Directory.CreateDirectory(ScriptRepoUpdater.ReposPath);
|
||
|
||
var dialog = new OpenFileDialog
|
||
{
|
||
Filter = "Zip Files (*.zip)|*.zip",
|
||
Multiselect = false
|
||
};
|
||
|
||
if (dialog.ShowDialog() == true)
|
||
{
|
||
var zipPath = dialog.FileName;
|
||
// 删除旧文件夹
|
||
if (Directory.Exists(ScriptRepoUpdater.CenterRepoPathOld))
|
||
{
|
||
DirectoryHelper.DeleteReadOnlyDirectory(ScriptRepoUpdater.CenterRepoPathOld);
|
||
}
|
||
|
||
ZipFile.ExtractToDirectory(zipPath, ScriptRepoUpdater.ReposPath, true);
|
||
|
||
if (Directory.Exists(ScriptRepoUpdater.CenterRepoPathOld))
|
||
{
|
||
DirectoryHelper.CopyDirectory(ScriptRepoUpdater.CenterRepoPathOld, ScriptRepoUpdater.CenterRepoPath);
|
||
ThemedMessageBox.Information("脚本仓库离线包导入成功!");
|
||
}
|
||
else
|
||
{
|
||
ThemedMessageBox.Error("脚本仓库离线包导入失败,不正确的脚本仓库离线包内容!");
|
||
DirectoryHelper.DeleteReadOnlyDirectory(ScriptRepoUpdater.ReposPath);
|
||
}
|
||
}
|
||
}
|
||
|
||
[RelayCommand]
|
||
private void OpenAboutWindow()
|
||
{
|
||
var aboutWindow = new AboutWindow();
|
||
aboutWindow.Owner = Application.Current.MainWindow;
|
||
aboutWindow.ShowDialog();
|
||
}
|
||
|
||
[RelayCommand]
|
||
private void OpenKeyBindingsWindow()
|
||
{
|
||
var keyBindingsWindow = KeyBindingsWindow.Instance;
|
||
keyBindingsWindow.Owner = Application.Current.MainWindow;
|
||
keyBindingsWindow.ShowDialog();
|
||
}
|
||
|
||
|
||
[RelayCommand]
|
||
private async Task CheckUpdateAsync()
|
||
{
|
||
await App.GetService<IUpdateService>()!.CheckUpdateAsync(new UpdateOption
|
||
{
|
||
Trigger = UpdateTrigger.Manual,
|
||
Channel = UpdateChannel.Stable
|
||
});
|
||
}
|
||
|
||
[RelayCommand]
|
||
private async Task CheckUpdateAlphaAsync()
|
||
{
|
||
var result = await ThemedMessageBox.ShowAsync("测试版本非常不稳定!\n测试版本非常不稳定!\n测试版本非常不稳定!\n\n是否继续检查更新?", "警告", MessageBoxButton.YesNo, ThemedMessageBox.MessageBoxIcon.Warning);
|
||
if (result != MessageBoxResult.Yes)
|
||
{
|
||
return;
|
||
}
|
||
|
||
await App.GetService<IUpdateService>()!.CheckUpdateAsync(new UpdateOption
|
||
{
|
||
Trigger = UpdateTrigger.Manual,
|
||
Channel = UpdateChannel.Alpha,
|
||
});
|
||
}
|
||
|
||
// [RelayCommand]
|
||
// private async Task GotoGithubActionAsync()
|
||
// {
|
||
// await Launcher.LaunchUriAsync(
|
||
// new Uri("https://github.com/babalae/better-genshin-impact/actions/workflows/publish.yml"));
|
||
// }
|
||
|
||
[RelayCommand]
|
||
private async Task OnGameLangSelectionChanged(KeyValuePair<string, string> type)
|
||
{
|
||
await App.ServiceProvider.GetRequiredService<OcrFactory>().Unload();
|
||
}
|
||
|
||
[RelayCommand]
|
||
private async Task OnPaddleOcrModelConfigChanged(PaddleOcrModelConfig value)
|
||
{
|
||
Config.OtherConfig.OcrConfig.PaddleOcrModelConfig = value;
|
||
await App.ServiceProvider.GetRequiredService<OcrFactory>().Unload();
|
||
}
|
||
}
|