locale zh-cn

This commit is contained in:
DismissedLight
2023-02-01 20:28:32 +08:00
parent 9fb2da41cd
commit d9bcb3b16b
87 changed files with 1393 additions and 965 deletions

View File

@@ -25,4 +25,4 @@
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.4.0" />
</ItemGroup>
</Project>
</Project>

View File

@@ -8,6 +8,8 @@ namespace Snap.Hutao.Control;
/// <summary>
/// 绑定探针
/// 用于处理特定情况下需要穿透数据上下文的工作
/// DependencyObject will dispose inner ReferenceTracker in any time
/// when object is not used anymore.
/// </summary>
public class BindingProxy : DependencyObject
{

View File

@@ -8,7 +8,7 @@ namespace Snap.Hutao.Control.Extension;
/// <summary>
/// 对话框扩展
/// </summary>
internal static class ContentDialogExtensions
internal static class ContentDialogExtension
{
/// <summary>
/// 阻止用户交互

View File

@@ -32,7 +32,7 @@ public class CachedImage : ImageEx
try
{
Verify.Operation(imageUri.Host != string.Empty, "无效的Uri");
Verify.Operation(imageUri.Host != string.Empty, SH.ControlImageCachedImageInvalidResourceUri);
string file = await imageCache.GetFileFromCacheAsync(imageUri).ConfigureAwait(true);
// check token state to determine whether the operation should be canceled.

View File

@@ -2,6 +2,7 @@
// Licensed under the MIT license.
using CommunityToolkit.WinUI.UI.Animations;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.UI.Composition;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Hosting;
@@ -11,6 +12,7 @@ using Snap.Hutao.Extension;
using Snap.Hutao.Service.Abstraction;
using System.IO;
using System.Net.Http;
using System.Reflection;
using System.Runtime.InteropServices;
namespace Snap.Hutao.Control.Image;
@@ -24,7 +26,7 @@ public abstract class CompositionImage : Microsoft.UI.Xaml.Controls.Control
private static readonly DependencyProperty SourceProperty = Property<CompositionImage>.Depend(nameof(Source), default(Uri), OnSourceChanged);
private static readonly ConcurrentCancellationTokenSource<CompositionImage> LoadingTokenSource = new();
private readonly IImageCache imageCache;
private readonly IServiceProvider serviceProvider;
private SpriteVisual? spriteVisual;
private bool isShow = true;
@@ -34,8 +36,15 @@ public abstract class CompositionImage : Microsoft.UI.Xaml.Controls.Control
/// </summary>
public CompositionImage()
{
imageCache = Ioc.Default.GetRequiredService<IImageCache>();
serviceProvider = Ioc.Default.GetRequiredService<IServiceProvider>();
AllowFocusOnInteraction = false;
IsDoubleTapEnabled = false;
IsHitTestVisible = false;
IsHoldingEnabled = false;
IsRightTapEnabled = false;
IsTabStop = false;
SizeChanged += OnSizeChanged;
}
@@ -86,11 +95,11 @@ public abstract class CompositionImage : Microsoft.UI.Xaml.Controls.Control
if (exception is HttpRequestException httpRequestException)
{
infoBarService.Error(httpRequestException, $"GET {uri}");
infoBarService.Error(httpRequestException, string.Format(SH.ControlImageCompositionImageHttpRequest, uri));
}
else
{
infoBarService.Error(exception, $"应用 {nameof(CompositionImage)} 的源时发生异常");
infoBarService.Error(exception.GetBaseException(), SH.ControlImageCompositionImageSystemException);
}
}
@@ -124,26 +133,20 @@ public abstract class CompositionImage : Microsoft.UI.Xaml.Controls.Control
if (uri != null)
{
if (uri.Scheme == "ms-appx")
{
imageSurface = LoadedImageSurface.StartLoadFromUri(uri);
}
else
{
string storageFile = await imageCache.GetFileFromCacheAsync(uri).ConfigureAwait(true);
IImageCache imageCache = serviceProvider.GetRequiredService<IImageCache>();
string file = await imageCache.GetFileFromCacheAsync(uri).ConfigureAwait(true);
try
{
imageSurface = await LoadImageSurfaceAsync(storageFile, token).ConfigureAwait(true);
}
catch (COMException)
{
imageCache.Remove(uri.Enumerate());
}
catch (IOException)
{
imageCache.Remove(uri.Enumerate());
}
try
{
imageSurface = await LoadImageSurfaceAsync(file, token).ConfigureAwait(true);
}
catch (COMException)
{
imageCache.Remove(uri.Enumerate());
}
catch (IOException)
{
imageCache.Remove(uri.Enumerate());
}
if (imageSurface != null)

View File

@@ -6,6 +6,7 @@ using Microsoft.UI;
using Microsoft.UI.Composition;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Media;
using Snap.Hutao.Control.Theme;
namespace Snap.Hutao.Control.Image;
@@ -49,12 +50,7 @@ public class MonoChrome : CompositionImage
private void SetBackgroundColor(CompositionColorBrush backgroundBrush)
{
ApplicationTheme theme = ActualTheme switch
{
ElementTheme.Light => ApplicationTheme.Light,
ElementTheme.Dark => ApplicationTheme.Dark,
_ => Ioc.Default.GetRequiredService<App>().RequestedTheme,
};
ApplicationTheme theme = ThemeHelper.ElementToApplication(ActualTheme);
backgroundBrush.Color = theme switch
{

View File

@@ -0,0 +1,36 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.UI.Xaml.Markup;
namespace Snap.Hutao.Control.Markup;
/// <summary>
/// Xaml extension to return a <see cref="string"/> value from resource file associated with a resource key
/// </summary>
[MarkupExtensionReturnType(ReturnType = typeof(string))]
public sealed class ResourceStringExtension : MarkupExtension
{
/// <summary>
/// Gets or sets associated ID from resource strings.
/// </summary>
public string? Name { get; set; }
/// <summary>
/// Gets a string value from resource file associated with a resource key.
/// </summary>
/// <param name="name">Resource key name.</param>
/// <returns>A string value from resource file associated with a resource key.</returns>
public static string GetValue(string name)
{
// This function is needed to accomodate compiled function usage without second paramater,
// which doesn't work with optional values.
return SH.ResourceManager.GetString(name)!;
}
/// <inheritdoc/>
protected override object ProvideValue()
{
return GetValue(Name ?? string.Empty);
}
}

View File

@@ -9,8 +9,10 @@ namespace Snap.Hutao.Control.Panel;
/// <summary>
/// 纵横比控件
/// </summary>
internal class AspectRatio : Microsoft.UI.Xaml.Controls.ContentControl
internal class AspectRatio : Microsoft.UI.Xaml.Controls.Control
{
private const double Epsilon = 2.2204460492503131e-016;
private static readonly DependencyProperty TargetWidthProperty = Property<AspectRatio>.Depend(nameof(TargetWidth), 1D);
private static readonly DependencyProperty TargetHeightProperty = Property<AspectRatio>.Depend(nameof(TargetHeight), 1D);
@@ -38,6 +40,11 @@ internal class AspectRatio : Microsoft.UI.Xaml.Controls.ContentControl
double ratio = TargetWidth / TargetHeight;
double ratioAvailable = availableSize.Width / availableSize.Height;
if (Math.Abs(ratioAvailable - ratio) < Epsilon)
{
return availableSize;
}
// 更宽
if (ratioAvailable > ratio)
{

View File

@@ -21,12 +21,12 @@
Click="RadioMenuFlyoutItemClick"
Icon="{shcm:FontIcon Glyph=&#xE8FD;}"
Tag="List"
Text="列表"/>
Text="{shcm:ResourceString Name=ControlPanelPanelSelectorDropdownListName}"/>
<RadioMenuFlyoutItem
Click="RadioMenuFlyoutItemClick"
Icon="{shcm:FontIcon Glyph=&#xF0E2;}"
Tag="Grid"
Text="网格"/>
Text="{shcm:ResourceString Name=ControlPanelPanelSelectorDropdownGridName}"/>
</MenuFlyout>
</SplitButton.Flyout>
</SplitButton>

View File

@@ -16,16 +16,34 @@ namespace Snap.Hutao.Control;
[SuppressMessage("", "CA1001")]
public class ScopedPage : Page
{
// Allow GC to Collect the IServiceScope
private static readonly WeakReference<IServiceScope> PreviousScopeReference = new(null!);
private readonly CancellationTokenSource viewCancellationTokenSource = new();
private readonly IServiceScope serviceScope;
private readonly IServiceScope currentScope;
/// <summary>
/// 构造一个新的页面
/// </summary>
public ScopedPage()
{
serviceScope = Ioc.Default.CreateScope();
serviceScope.Track();
Unloaded += OnScopedPageUnloaded;
currentScope = Ioc.Default.CreateScope();
DisposePreviousScope();
// track current
PreviousScopeReference.SetTarget(currentScope);
}
/// <summary>
/// 释放上个范围
/// </summary>
public static void DisposePreviousScope()
{
if (PreviousScopeReference.TryGetTarget(out IServiceScope? scope))
{
scope.Dispose();
}
}
/// <summary>
@@ -36,7 +54,7 @@ public class ScopedPage : Page
public void InitializeWith<TViewModel>()
where TViewModel : class, IViewModel
{
IViewModel viewModel = serviceScope.ServiceProvider.GetRequiredService<TViewModel>();
IViewModel viewModel = currentScope.ServiceProvider.GetRequiredService<TViewModel>();
viewModel.CancellationToken = viewCancellationTokenSource.Token;
DataContext = viewModel;
}
@@ -59,7 +77,6 @@ public class ScopedPage : Page
/// <inheritdoc/>
protected override void OnNavigatingFrom(NavigatingCancelEventArgs e)
{
base.OnNavigatingFrom(e);
using (viewCancellationTokenSource)
{
// Cancel all tasks executed by the view model
@@ -73,7 +90,7 @@ public class ScopedPage : Page
viewModel.IsViewDisposed = true;
// Dispose the scope
serviceScope.Dispose();
currentScope.Dispose();
}
}
}
@@ -81,11 +98,15 @@ public class ScopedPage : Page
/// <inheritdoc/>
protected override void OnNavigatedTo(NavigationEventArgs e)
{
base.OnNavigatedTo(e);
if (e.Parameter is INavigationData extra)
{
NotifyRecipentAsync(extra).SafeForget();
}
}
private void OnScopedPageUnloaded(object sender, Microsoft.UI.Xaml.RoutedEventArgs e)
{
DataContext = null;
Unloaded -= OnScopedPageUnloaded;
}
}

View File

@@ -9,7 +9,7 @@ using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Documents;
using Microsoft.UI.Xaml.Media;
using Snap.Hutao.Control.Media;
using Snap.Hutao.Core;
using Snap.Hutao.Control.Theme;
using Windows.UI;
namespace Snap.Hutao.Control.Text;

View File

@@ -4,7 +4,7 @@
using Microsoft.UI.Composition.SystemBackdrops;
using Microsoft.UI.Xaml;
namespace Snap.Hutao.Core;
namespace Snap.Hutao.Control.Theme;
/// <summary>
/// 主题帮助工具类
@@ -42,6 +42,21 @@ public static class ThemeHelper
};
}
/// <summary>
/// 从 <see cref="ElementTheme"/> 转换到 <see cref="ApplicationTheme"/>
/// </summary>
/// <param name="applicationTheme">元素主题</param>
/// <returns>应用主题</returns>
public static ApplicationTheme ElementToApplication(ElementTheme applicationTheme)
{
return applicationTheme switch
{
ElementTheme.Light => ApplicationTheme.Light,
ElementTheme.Dark => ApplicationTheme.Dark,
_ => Ioc.Default.GetRequiredService<App>().RequestedTheme,
};
}
/// <summary>
/// 从 <see cref="ElementTheme"/> 转换到 <see cref="SystemBackdropTheme"/>
/// </summary>

View File

@@ -70,6 +70,11 @@ internal static class CoreEnvironment
/// </summary>
public static readonly string FamilyName;
/// <summary>
/// 安装位置
/// </summary>
public static readonly string InstalledLocation;
/// <summary>
/// 数据文件夹
/// </summary>
@@ -98,9 +103,10 @@ internal static class CoreEnvironment
static CoreEnvironment()
{
DataFolder = GetDocumentsHutaoPath();
DataFolder = GetDatafolderPath();
Version = Package.Current.Id.Version.ToVersion();
FamilyName = Package.Current.Id.FamilyName;
InstalledLocation = Package.Current.InstalledLocation.Path;
CommonUA = $"Snap Hutao/{Version}";
// simply assign a random guid
@@ -115,7 +121,7 @@ internal static class CoreEnvironment
return Convert.ToMd5HexString($"{userName}{machineGuid}");
}
private static string GetDocumentsHutaoPath()
private static string GetDatafolderPath()
{
string myDocument = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments);
#if RELEASE

View File

@@ -18,7 +18,7 @@ internal static class IocConfiguration
/// </summary>
/// <param name="services">集合</param>
/// <returns>可继续操作的集合</returns>
public static IServiceCollection AddJsonSerializerOptions(this IServiceCollection services)
public static IServiceCollection AddJsonOptions(this IServiceCollection services)
{
return services.AddSingleton(CoreEnvironment.JsonOptions);
}

View File

@@ -1,36 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.Extensions.DependencyInjection;
namespace Snap.Hutao.Core.DependencyInjection;
/// <summary>
/// 服务范围扩展
/// </summary>
public static class ServiceScopeExtension
{
// Allow GC to Collect the IServiceScope
private static readonly WeakReference<IServiceScope> ScopeReference = new(null!);
/// <summary>
/// 追踪服务范围
/// </summary>
/// <param name="scope">范围</param>
public static void Track(this IServiceScope scope)
{
DisposeLast();
ScopeReference.SetTarget(scope);
}
/// <summary>
/// 释放上个范围
/// </summary>
public static void DisposeLast()
{
if (ScopeReference.TryGetTarget(out IServiceScope? scope))
{
scope.Dispose();
}
}
}

View File

@@ -0,0 +1,56 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Service.Game.Package;
using System.Runtime.CompilerServices;
namespace Snap.Hutao.Core.ExceptionService;
/// <summary>
/// 帮助更好的抛出异常
/// </summary>
[System.Diagnostics.StackTraceHidden]
internal static class ThrowHelper
{
/// <summary>
/// 操作取消
/// </summary>
/// <param name="message">消息</param>
/// <param name="inner">内部错误</param>
/// <exception cref="OperationCanceledException">操作取消异常</exception>
/// <returns>nothing</returns>
[DoesNotReturn]
[MethodImpl(MethodImplOptions.NoInlining)]
public static OperationCanceledException OperationCanceled(string message, Exception? inner)
{
throw new OperationCanceledException(message, inner);
}
/// <summary>
/// 包转换错误
/// </summary>
/// <param name="message">消息</param>
/// <param name="inner">内部错误</param>
/// <returns>nothing</returns>
/// <exception cref="PackageConvertException">包转换错误异常</exception>
[DoesNotReturn]
[MethodImpl(MethodImplOptions.NoInlining)]
public static PackageConvertException PackageConvert(string message, Exception inner)
{
throw new PackageConvertException(message, inner);
}
/// <summary>
/// 用户数据损坏
/// </summary>
/// <param name="message">消息</param>
/// <param name="inner">内部错误</param>
/// <exception cref="UserdataCorruptedException">数据损坏</exception>
/// <returns>nothing</returns>
[DoesNotReturn]
[MethodImpl(MethodImplOptions.NoInlining)]
public static UserdataCorruptedException UserdataCorrupted(string message, Exception inner)
{
throw new UserdataCorruptedException(message, inner);
}
}

View File

@@ -14,7 +14,7 @@ internal class UserdataCorruptedException : Exception
/// <param name="message">消息</param>
/// <param name="innerException">内部错误</param>
public UserdataCorruptedException(string message, Exception innerException)
: base($"用户数据已损坏: {message}", innerException)
: base(string.Format(SH.CoreExceptionServiceUserdataCorruptedMessage, message), innerException)
{
}
}

View File

@@ -30,7 +30,7 @@ internal class BitsJob : DisposableObject, IBackgroundCopyCallback
private readonly object lockObj = new();
private IBackgroundCopyJob? nativeJob;
private System.Exception? jobException;
private Exception? jobException;
private BG_JOB_PROGRESS progress;
private BG_JOB_STATE state;
private bool isJobComplete;
@@ -79,7 +79,7 @@ internal class BitsJob : DisposableObject, IBackgroundCopyCallback
UpdateJobState();
CompleteOrCancel();
}
catch (System.Exception ex)
catch (Exception ex)
{
log.LogInformation("Failed to job transfer: {message}", ex.Message);
}
@@ -101,7 +101,7 @@ internal class BitsJob : DisposableObject, IBackgroundCopyCallback
CompleteOrCancel();
log.LogInformation(jobException, "Job Exception:");
}
catch (System.Exception ex)
catch (Exception ex)
{
log?.LogInformation("Failed to handle job error: {message}", ex.Message);
}
@@ -141,7 +141,7 @@ internal class BitsJob : DisposableObject, IBackgroundCopyCallback
CompleteOrCancel();
}
}
catch (System.Exception ex)
catch (Exception ex)
{
log.LogInformation(ex, "message");
}
@@ -283,7 +283,7 @@ internal class BitsJob : DisposableObject, IBackgroundCopyCallback
{
action();
}
catch (System.Exception ex)
catch (Exception ex)
{
log.LogInformation("{name} failed. {exception}", displayName, ex);
if (throwOnFailure)

View File

@@ -39,7 +39,8 @@ internal class BitsManager
public async Task<ValueResult<bool, TempFile>> DownloadAsync(Uri uri, IProgress<ProgressUpdateStatus> progress, CancellationToken token = default)
{
TempFile tempFile = new(true);
bool result = await Task.Run(() => DownloadCore(uri, tempFile.Path, progress.Report, token), token).ConfigureAwait(false);
await ThreadHelper.SwitchToBackgroundAsync();
bool result = DownloadCore(uri, tempFile.Path, progress.Report, token);
return new(result, tempFile);
}

View File

@@ -1,11 +1,14 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using System.Diagnostics;
namespace Snap.Hutao.Core.IO.Bits;
/// <summary>
/// 进度更新状态
/// </summary>
[DebuggerDisplay("{BytesRead}/{TotalBytes}")]
public class ProgressUpdateStatus
{
/// <summary>

View File

@@ -20,7 +20,6 @@ internal static class Clipboard
public static async Task<T?> DeserializeTextAsync<T>(JsonSerializerOptions options)
where T : class
{
await ThreadHelper.SwitchToMainThreadAsync();
DataPackageView view = Windows.ApplicationModel.DataTransfer.Clipboard.GetContent();
string json = await view.GetTextAsync();
return JsonSerializer.Deserialize<T>(json, options);

View File

@@ -32,13 +32,7 @@ internal static class PickerExtension
}
else
{
if (exception != null)
{
Ioc.Default
.GetRequiredService<Service.Abstraction.IInfoBarService>()
.Warning("无法打开文件选择器", $"请勿在管理员模式下使用此功能 {exception.Message}");
}
InfoBarWaringPickerException(exception);
return new(false, null!);
}
}
@@ -64,14 +58,20 @@ internal static class PickerExtension
}
else
{
if (exception != null)
{
Ioc.Default
.GetRequiredService<Service.Abstraction.IInfoBarService>()
.Warning("无法打开文件选择器", $"请勿在管理员模式下使用此功能 {exception.Message}");
}
InfoBarWaringPickerException(exception);
return new(false, null!);
}
}
private static void InfoBarWaringPickerException(Exception? exception)
{
if (exception != null)
{
Ioc.Default
.GetRequiredService<Service.Abstraction.IInfoBarService>()
.Warning(
SH.CoreIOPickerExtensionPickerExceptionInfoBarTitle,
string.Format(SH.CoreIOPickerExtensionPickerExceptionInfoBarMessage, exception.Message));
}
}
}

View File

@@ -23,8 +23,8 @@ public static class JumpListHelper
list.Items.Clear();
JumpListItem launchGameItem = JumpListItem.CreateWithArguments(Activation.LaunchGame, "启动游戏");
launchGameItem.GroupName = "快捷操作";
JumpListItem launchGameItem = JumpListItem.CreateWithArguments(Activation.LaunchGame, SH.CoreJumpListHelperLaunchGameItemDisplayName);
launchGameItem.GroupName = SH.CoreJumpListHelperLaunchGameItemGroupName;
launchGameItem.Logo = new("ms-appx:///Resource/Icon/UI_GuideIcon_PlayMethod.png");
list.Items.Add(launchGameItem);

View File

@@ -1,24 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
namespace Snap.Hutao.Core.Logging;
/// <summary>
/// Extension methods for the <see cref="ILoggerFactory"/> class.
/// </summary>
public static class DatabaseLoggerFactoryExtensions
{
/// <summary>
/// Adds a debug logger named 'Debug' to the factory.
/// </summary>
/// <param name="builder">The extension method argument.</param>
/// <returns>日志构造器</returns>
public static ILoggingBuilder AddDatabase(this ILoggingBuilder builder)
{
builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton<ILoggerProvider, DatebaseLoggerProvider>());
return builder;
}
}

View File

@@ -1,80 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Core.Logging;
/// <summary>
/// A logger that writes messages in the database table
/// </summary>
internal sealed partial class DatebaseLogger : ILogger
{
private readonly string name;
private readonly LogEntryQueue logEntryQueue;
/// <summary>
/// Initializes a new instance of the <see cref="DatebaseLogger"/> class.
/// </summary>
/// <param name="name">The name of the logger.</param>
/// <param name="logEntryQueue">日志队列</param>
public DatebaseLogger(string name, LogEntryQueue logEntryQueue)
{
this.name = name;
this.logEntryQueue = logEntryQueue;
}
/// <inheritdoc />
public IDisposable BeginScope<TState>(TState state)
where TState : notnull
{
return new NullScope();
}
/// <inheritdoc />
public bool IsEnabled(LogLevel logLevel)
{
return logLevel != LogLevel.None;
}
/// <inheritdoc />
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, System.Exception? exception, Func<TState, System.Exception?, string> formatter)
{
if (!IsEnabled(logLevel))
{
return;
}
string message = formatter(state, exception);
if (string.IsNullOrEmpty(message))
{
return;
}
LogEntry entry = new()
{
Time = DateTimeOffset.Now,
Category = name,
LogLevel = logLevel,
EventId = eventId.Id,
Message = message,
Exception = exception?.ToString(),
};
logEntryQueue.Enqueue(entry);
}
/// <summary>
/// An empty scope without any logic
/// </summary>
private struct NullScope : IDisposable
{
public NullScope()
{
}
/// <inheritdoc />
public void Dispose()
{
}
}
}

View File

@@ -1,25 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Core.Logging;
/// <summary>
/// The provider for the <see cref="DatebaseLogger"/>.
/// </summary>
[ProviderAlias("Database")]
public sealed class DatebaseLoggerProvider : ILoggerProvider
{
private readonly LogEntryQueue logEntryQueue = new();
/// <inheritdoc/>
public ILogger CreateLogger(string name)
{
return new DatebaseLogger(name, logEntryQueue);
}
/// <inheritdoc/>
public void Dispose()
{
logEntryQueue.Dispose();
}
}

View File

@@ -1,51 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace Snap.Hutao.Core.Logging;
/// <summary>
/// 数据库日志入口点
/// </summary>
[Table("logs")]
public class LogEntry
{
/// <summary>
/// 内部Id
/// </summary>
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public Guid InnerId { get; set; }
/// <summary>
/// 日志时间
/// </summary>
public DateTimeOffset Time { get; set; }
/// <summary>
/// 类别
/// </summary>
public string Category { get; set; } = default!;
/// <summary>
/// 日志等级
/// </summary>
public LogLevel LogLevel { get; set; }
/// <summary>
/// 事件Id
/// </summary>
public int EventId { get; set; }
/// <summary>
/// 消息
/// </summary>
public string Message { get; set; } = default!;
/// <summary>
/// 可能的异常
/// </summary>
public string? Exception { get; set; }
}

View File

@@ -1,110 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.EntityFrameworkCore;
using Snap.Hutao.Model.Entity.Database;
using System.Collections.Concurrent;
using System.Diagnostics;
namespace Snap.Hutao.Core.Logging;
/// <summary>
/// 日志队列
/// </summary>
public sealed class LogEntryQueue : IDisposable
{
private readonly ConcurrentQueue<LogEntry> entryQueue = new();
private readonly CancellationTokenSource disposeTokenSource = new();
private readonly TaskCompletionSource writeDbCompletionSource = new();
private readonly LogDbContext logDbContext;
private bool disposed;
/// <summary>
/// 构造一个新的日志队列
/// </summary>
public LogEntryQueue()
{
logDbContext = InitializeDbContext();
Task.Run(() => WritePendingLogsAsync(disposeTokenSource.Token)).SafeForget();
}
/// <summary>
/// 将日志消息存入队列
/// </summary>
/// <param name="logEntry">日志</param>
public void Enqueue(LogEntry logEntry)
{
entryQueue.Enqueue(logEntry);
}
/// <inheritdoc/>
[SuppressMessage("", "VSTHRD002")]
public void Dispose()
{
if (disposed)
{
return;
}
// notify the write task to complete.
disposeTokenSource.Cancel();
// Wait the db operation complete.
writeDbCompletionSource.Task.GetAwaiter().GetResult();
logDbContext.Dispose();
disposed = true;
}
private static LogDbContext InitializeDbContext()
{
string logDbName = System.IO.Path.Combine(CoreEnvironment.DataFolder, "Log.db");
LogDbContext logDbContext = LogDbContext.Create($"Data Source={logDbName}");
if (logDbContext.Database.GetPendingMigrations().Any())
{
Debug.WriteLine("[Debug] Performing LogDbContext Migrations");
logDbContext.Database.Migrate();
}
// only raw sql can pass
logDbContext.Logs.Where(log => log.Exception == null).ExecuteDelete();
return logDbContext;
}
private async Task WritePendingLogsAsync(CancellationToken token)
{
bool hasAdded = false;
while (true)
{
if (entryQueue.TryDequeue(out LogEntry? logEntry))
{
logDbContext.Logs.Add(logEntry);
hasAdded = true;
}
else
{
if (hasAdded)
{
logDbContext.SaveChanges();
hasAdded = false;
}
if (token.IsCancellationRequested)
{
writeDbCompletionSource.TrySetResult();
break;
}
try
{
await Task.Delay(5000, token).ConfigureAwait(false);
}
catch (TaskCanceledException)
{
}
}
}
}
}

View File

@@ -31,7 +31,7 @@ internal static class ScheduleTaskHelper
}
TaskDefinition task = TaskService.Instance.NewTask();
task.RegistrationInfo.Description = "胡桃实时便笺刷新任务 | 请勿编辑或删除。";
task.RegistrationInfo.Description = SH.CoreScheduleTaskHelperDailyNoteRefreshTaskDescription;
task.Triggers.Add(new TimeTrigger() { Repetition = new(TimeSpan.FromSeconds(interval), TimeSpan.Zero), });
task.Actions.Add("explorer", "hutao://DailyNote/Refresh");
TaskService.Instance.RootFolder.RegisterTaskDefinition(DailyNoteRefreshTaskName, task);

View File

@@ -1,12 +1,14 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Core.ExceptionService;
namespace Snap.Hutao.Core.Threading;
/// <summary>
/// 信号量扩展
/// </summary>
public static class SemaphoreSlimExtensions
public static class SemaphoreSlimExtension
{
/// <summary>
/// 异步进入信号量
@@ -22,7 +24,7 @@ public static class SemaphoreSlimExtensions
}
catch (ObjectDisposedException ex)
{
throw new OperationCanceledException("信号量已经被释放,操作取消", ex);
ThrowHelper.OperationCanceled(SH.CoreThreadingSemaphoreSlimDisposed, ex);
}
return new SemaphoreSlimReleaser(semaphoreSlim);

View File

@@ -22,7 +22,7 @@ public static class TaskExtensions
{
await task.ConfigureAwait(false);
}
catch (System.Exception ex)
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine(ex);
}
@@ -39,11 +39,11 @@ public static class TaskExtensions
{
await task.ConfigureAwait(false);
}
catch (TaskCanceledException)
catch (OperationCanceledException)
{
// Do nothing
}
catch (System.Exception e)
catch (Exception e)
{
logger?.LogError(EventIds.TaskException, e, "{caller}:\r\n{exception}", nameof(SafeForget), e.GetBaseException());
}
@@ -55,17 +55,17 @@ public static class TaskExtensions
/// <param name="task">任务</param>
/// <param name="logger">日志器</param>
/// <param name="onException">发生异常时调用</param>
public static async void SafeForget(this Task task, ILogger? logger = null, Action<System.Exception>? onException = null)
public static async void SafeForget(this Task task, ILogger? logger = null, Action<Exception>? onException = null)
{
try
{
await task.ConfigureAwait(false);
}
catch (TaskCanceledException)
catch (OperationCanceledException)
{
// Do nothing
}
catch (System.Exception e)
catch (Exception e)
{
logger?.LogError(EventIds.TaskException, e, "{caller}:\r\n{exception}", nameof(SafeForget), e.GetBaseException());
onException?.Invoke(e);
@@ -79,17 +79,17 @@ public static class TaskExtensions
/// <param name="logger">日志器</param>
/// <param name="onCanceled">任务取消时调用</param>
/// <param name="onException">发生异常时调用</param>
public static async void SafeForget(this Task task, ILogger? logger = null, Action? onCanceled = null, Action<System.Exception>? onException = null)
public static async void SafeForget(this Task task, ILogger? logger = null, Action? onCanceled = null, Action<Exception>? onException = null)
{
try
{
await task.ConfigureAwait(false);
}
catch (TaskCanceledException)
catch (OperationCanceledException)
{
onCanceled?.Invoke();
}
catch (System.Exception e)
catch (Exception e)
{
logger?.LogError(EventIds.TaskException, e, "{caller}:\r\n{exception}", nameof(SafeForget), e.GetBaseException());
onException?.Invoke(e);

View File

@@ -30,13 +30,13 @@ public readonly struct ThreadPoolSwitchOperation : IAwaitable<ThreadPoolSwitchOp
/// <inheritdoc/>
public void OnCompleted(Action continuation)
{
QueueContinuation(continuation, flowContext: true);
QueueContinuation(continuation, true);
}
/// <inheritdoc/>
public void UnsafeOnCompleted(Action continuation)
{
QueueContinuation(continuation, flowContext: false);
QueueContinuation(continuation, false);
}
private static void QueueContinuation(Action continuation, bool flowContext)

View File

@@ -44,7 +44,7 @@ public static class Must
/// <param name="context">上下文</param>
/// <returns>Nothing. This method always throws.</returns>
[DoesNotReturn]
public static System.Exception NeverHappen(string? context = null)
public static Exception NeverHappen(string? context = null)
{
throw new NotSupportedException(context);
}
@@ -62,21 +62,4 @@ public static class Must
{
return value ?? throw new ArgumentNullException(parameterName);
}
/// <summary>
/// Throws an <see cref="ArgumentNullException"/> if the specified parameter's value is IntPtr.Zero.
/// </summary>
/// <param name="value">The value of the argument.</param>
/// <param name="parameterName">The name of the parameter to include in any thrown exception.</param>
/// <returns>The value of the parameter.</returns>
/// <exception cref="ArgumentNullException">Thrown if <paramref name="value"/> is <see cref="IntPtr.Zero"/>.</exception>
public static Windows.Win32.Foundation.HWND NotNull(Windows.Win32.Foundation.HWND value, [CallerArgumentExpression("value")] string? parameterName = null)
{
if (value == default)
{
throw new ArgumentNullException(parameterName);
}
return value;
}
}

View File

@@ -16,7 +16,7 @@ internal abstract class WebView2Helper
{
private static bool hasEverDetected;
private static bool isSupported;
private static string version = "未检测到 WebView2 运行时";
private static string version = SH.CoreWebView2HelperVersionUndetected;
/// <summary>
/// 检测 WebView2 是否存在
@@ -36,7 +36,7 @@ internal abstract class WebView2Helper
catch (FileNotFoundException ex)
{
ILogger<WebView2Helper> logger = Ioc.Default.GetRequiredService<ILogger<WebView2Helper>>();
logger.LogError(EventIds.WebView2EnvironmentException, ex, "WebView2 运行时未安装");
logger.LogError(EventIds.WebView2EnvironmentException, ex, "WebView2 Runtime not installed.");
isSupported = false;
}
}

View File

@@ -124,8 +124,8 @@ internal sealed class ExtendedWindow<TWindow> : IRecipient<BackdropTypeChangedMe
private void InitializeWindow()
{
appWindow.Title = "胡桃";
appWindow.SetIcon(Path.Combine(Package.Current.InstalledLocation.Path, "Assets/Logos/Logo.ico"));
appWindow.Title = string.Format(SH.AppNameAndVersion, CoreEnvironment.Version);
appWindow.SetIcon(Path.Combine(CoreEnvironment.InstalledLocation, "Assets/Logo.ico"));
ExtendsContentIntoTitleBar();
Persistence.RecoverOrInit(appWindow, window.PersistSize, window.InitSize);

View File

@@ -5,6 +5,7 @@ using Microsoft.Extensions.DependencyInjection;
using Microsoft.UI.Composition;
using Microsoft.UI.Composition.SystemBackdrops;
using Microsoft.UI.Xaml;
using Snap.Hutao.Control.Theme;
using Snap.Hutao.Core.Database;
using Snap.Hutao.Model.Entity;
using Snap.Hutao.Model.Entity.Database;
@@ -140,7 +141,7 @@ public class SystemBackdrop
/// <summary>
/// 确保系统调度队列控制器存在
/// </summary>
public void Ensure()
public unsafe void Ensure()
{
if (DispatcherQueue.GetForCurrentThread() != null)
{
@@ -152,7 +153,7 @@ public class SystemBackdrop
{
DispatcherQueueOptions options = new()
{
DwSize = Marshal.SizeOf<DispatcherQueueOptions>(),
DwSize = sizeof(DispatcherQueueOptions),
ThreadType = 2, // DQTYPE_THREAD_CURRENT
ApartmentType = 2, // DQTAT_COM_STA
};

View File

@@ -37,7 +37,7 @@ internal class WindowSubclassManager<TWindow> : IDisposable
public WindowSubclassManager(TWindow window, HWND hwnd, bool isLegacyDragBar)
{
this.window = window;
this.hwnd = Must.NotNull(hwnd);
this.hwnd = hwnd;
this.isLegacyDragBar = isLegacyDragBar;
}
@@ -45,7 +45,7 @@ internal class WindowSubclassManager<TWindow> : IDisposable
/// 尝试设置窗体子类
/// </summary>
/// <returns>是否设置成功</returns>
public bool TrySetWindowSubclass()
public unsafe bool TrySetWindowSubclass()
{
windowProc = new(OnSubclassProcedure);
bool windowHooked = SetWindowSubclass(hwnd, windowProc, WindowSubclassId, 0);

View File

@@ -8,22 +8,6 @@ namespace Snap.Hutao.Extension;
/// </summary>
public static class DateTimeOffsetExtension
{
/// <summary>
/// Converts the current <see cref="DateTimeOffset"/> to a <see cref="DateTimeOffset"/> that represents the local time.
/// </summary>
/// <param name="dateTimeOffset">时间偏移</param>
/// <param name="keepTicks">保留主时间部分</param>
/// <returns>A <see cref="DateTimeOffset"/> that represents the local time.</returns>
public static DateTimeOffset ToLocalTime(this DateTimeOffset dateTimeOffset, bool keepTicks)
{
if (keepTicks)
{
dateTimeOffset -= TimeZoneInfo.Local.GetUtcOffset(DateTimeOffset.Now);
}
return dateTimeOffset.ToLocalTime();
}
/// <summary>
/// 从Unix时间戳转换
/// </summary>

View File

@@ -1,18 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Extension;
/// <summary>
/// 日志器扩展
/// </summary>
[SuppressMessage("", "CA2254")]
public static class LoggerExtension
{
/// <inheritdoc cref="LoggerExtensions.LogWarning(ILogger, string?, object?[])"/>
public static T LogWarning<T>(this ILogger logger, string message, params object?[] param)
{
logger.LogWarning(message, param);
return default!;
}
}

View File

@@ -33,17 +33,4 @@ public static class StringBuilderExtensions
{
return condition ? sb.Append(value) : sb;
}
/// <summary>
/// 当条件符合时执行 <see cref="StringBuilder.Append(string?)"/>
/// </summary>
/// <param name="sb">字符串建造器</param>
/// <param name="condition">条件</param>
/// <param name="trueValue">条件符合时附加的字符串</param>
/// <param name="falseValue">条件不符合时附加的字符串</param>
/// <returns>同一个字符串建造器</returns>
public static StringBuilder AppendIfElse(this StringBuilder sb, bool condition, string? trueValue, string? falseValue)
{
return condition ? sb.Append(trueValue) : sb.Append(falseValue);
}
}

View File

@@ -60,7 +60,7 @@ internal class ContentDialogFactory : IContentDialogFactory
Title = title,
Content = content,
DefaultButton = ContentDialogButton.Primary,
PrimaryButtonText = "确认",
PrimaryButtonText = SH.FactoryContentDialogFactoryConfirmPrimaryButtonText,
};
return dialog;
@@ -75,8 +75,8 @@ internal class ContentDialogFactory : IContentDialogFactory
Title = title,
Content = content,
DefaultButton = defaultButton,
PrimaryButtonText = "确认",
CloseButtonText = "取消",
PrimaryButtonText = SH.FactoryContentDialogFactoryConfirmPrimaryButtonText,
CloseButtonText = SH.FactoryContentDialogFactoryCancelCloseButtonText,
};
return dialog;

View File

@@ -13,6 +13,7 @@ global using Snap.Hutao.Core.DependencyInjection;
global using Snap.Hutao.Core.DependencyInjection.Annotation;
global using Snap.Hutao.Core.Threading;
global using Snap.Hutao.Core.Validation;
global using Snap.Hutao.Resource.Localization;
// Runtime
global using System;

View File

@@ -1,52 +0,0 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Snap.Hutao.Model.Entity.Database;
#nullable disable
namespace Snap.Hutao.Migrations.LogDb
{
[DbContext(typeof(LogDbContext))]
[Migration("20220720121521_Logs")]
partial class Logs
{
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder.HasAnnotation("ProductVersion", "6.0.7");
modelBuilder.Entity("Snap.Hutao.Core.Logging.LogEntry", b =>
{
b.Property<Guid>("InnerId")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<string>("Category")
.IsRequired()
.HasColumnType("TEXT");
b.Property<int>("EventId")
.HasColumnType("INTEGER");
b.Property<string>("Exception")
.HasColumnType("TEXT");
b.Property<int>("LogLevel")
.HasColumnType("INTEGER");
b.Property<string>("Message")
.IsRequired()
.HasColumnType("TEXT");
b.HasKey("InnerId");
b.ToTable("logs");
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -1,35 +0,0 @@
// <auto-generated />
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Snap.Hutao.Migrations.LogDb
{
public partial class Logs : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "logs",
columns: table => new
{
InnerId = table.Column<Guid>(type: "TEXT", nullable: false),
Category = table.Column<string>(type: "TEXT", nullable: false),
LogLevel = table.Column<int>(type: "INTEGER", nullable: false),
EventId = table.Column<int>(type: "INTEGER", nullable: false),
Message = table.Column<string>(type: "TEXT", nullable: false),
Exception = table.Column<string>(type: "TEXT", nullable: true),
},
constraints: table =>
{
table.PrimaryKey("PK_logs", x => x.InnerId);
});
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "logs");
}
}
}

View File

@@ -1,55 +0,0 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Snap.Hutao.Model.Entity.Database;
#nullable disable
namespace Snap.Hutao.Migrations.LogDb
{
[DbContext(typeof(LogDbContext))]
[Migration("20220903071033_LogTime")]
partial class LogTime
{
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder.HasAnnotation("ProductVersion", "6.0.8");
modelBuilder.Entity("Snap.Hutao.Core.Logging.LogEntry", b =>
{
b.Property<Guid>("InnerId")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<string>("Category")
.IsRequired()
.HasColumnType("TEXT");
b.Property<int>("EventId")
.HasColumnType("INTEGER");
b.Property<string>("Exception")
.HasColumnType("TEXT");
b.Property<int>("LogLevel")
.HasColumnType("INTEGER");
b.Property<string>("Message")
.IsRequired()
.HasColumnType("TEXT");
b.Property<DateTimeOffset>("Time")
.HasColumnType("TEXT");
b.HasKey("InnerId");
b.ToTable("logs");
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -1,27 +0,0 @@
// <auto-generated />
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Snap.Hutao.Migrations.LogDb
{
public partial class LogTime : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<DateTimeOffset>(
name: "Time",
table: "logs",
type: "TEXT",
nullable: false,
defaultValue: new DateTimeOffset(new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)));
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "Time",
table: "logs");
}
}
}

View File

@@ -1,53 +0,0 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Snap.Hutao.Model.Entity.Database;
#nullable disable
namespace Snap.Hutao.Migrations.LogDb
{
[DbContext(typeof(LogDbContext))]
partial class LogDbContextModelSnapshot : ModelSnapshot
{
protected override void BuildModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder.HasAnnotation("ProductVersion", "6.0.8");
modelBuilder.Entity("Snap.Hutao.Core.Logging.LogEntry", b =>
{
b.Property<Guid>("InnerId")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<string>("Category")
.IsRequired()
.HasColumnType("TEXT");
b.Property<int>("EventId")
.HasColumnType("INTEGER");
b.Property<string>("Exception")
.HasColumnType("TEXT");
b.Property<int>("LogLevel")
.HasColumnType("INTEGER");
b.Property<string>("Message")
.IsRequired()
.HasColumnType("TEXT");
b.Property<DateTimeOffset>("Time")
.HasColumnType("TEXT");
b.HasKey("InnerId");
b.ToTable("logs");
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -15,9 +15,9 @@ public class LaunchScheme
/// </summary>
public static readonly ImmutableList<LaunchScheme> KnownSchemes = new List<LaunchScheme>()
{
new LaunchScheme("官方服 | 天空岛", "eYd89JmJ", "18", "1", "1"),
new LaunchScheme("渠道服 | 世界树", "KAtdSsoQ", "17", "14", "0"),
new LaunchScheme("国际服 | 部分支持", "gcStgarh", "10", "1", "0"),
new LaunchScheme("官方服", "eYd89JmJ", "18", "1", "1"),
new LaunchScheme("渠道服", "KAtdSsoQ", "17", "14", "0"),
new LaunchScheme("国际服", "gcStgarh", "10", "1", "0"),
}.ToImmutableList();
/// <summary>

View File

@@ -1,39 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.EntityFrameworkCore;
using Snap.Hutao.Core.Logging;
namespace Snap.Hutao.Model.Entity.Database;
/// <summary>
/// 日志数据库上下文
/// 由于写入日志的行为需要锁定数据库上下文
/// 所以将日志单独分离出来进行读写
/// </summary>
public class LogDbContext : DbContext
{
/// <summary>
/// 创建一个新的
/// </summary>
/// <param name="options">选项</param>
private LogDbContext(DbContextOptions<LogDbContext> options)
: base(options)
{
}
/// <summary>
/// 日志记录
/// </summary>
public DbSet<LogEntry> Logs { get; set; } = default!;
/// <summary>
/// 构造一个临时的日志数据库上下文
/// </summary>
/// <param name="sqlConnectionString">连接字符串</param>
/// <returns>日志数据库上下文</returns>
public static LogDbContext Create(string sqlConnectionString)
{
return new(new DbContextOptionsBuilder<LogDbContext>().UseSqlite(sqlConnectionString).Options);
}
}

View File

@@ -1,22 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.EntityFrameworkCore.Design;
using Snap.Hutao.Model.Entity.Database;
namespace Snap.Hutao.Context.Database;
/// <summary>
/// 此类只用于在生成迁移时提供数据库上下文
/// </summary>
[EditorBrowsable(EditorBrowsableState.Never)]
public class LogDbContextDesignTimeFactory : IDesignTimeDbContextFactory<LogDbContext>
{
/// <inheritdoc/>
[EditorBrowsable(EditorBrowsableState.Never)]
public LogDbContext CreateDbContext(string[] args)
{
string logDbName = System.IO.Path.Combine(Core.CoreEnvironment.DataFolder, "Log.db");
return LogDbContext.Create($"Data Source={logDbName}");
}
}

View File

@@ -5,6 +5,7 @@
<DeveloperAccountType>MSA</DeveloperAccountType>
<GeneratePackageHash>http://www.w3.org/2001/04/xmlenc#sha256</GeneratePackageHash>
<SupportedLocales>
<Language Code="en-us" InMinimumRequirementSet="true" />
<Language Code="zh-cn" InMinimumRequirementSet="true" />
</SupportedLocales>
<ProductReservedInfo>

View File

@@ -25,7 +25,8 @@
</Dependencies>
<Resources>
<Resource Language="zh-CN"/>
<Resource Language="en-us"/>
<Resource Language="zh-cn"/>
</Resources>
<Applications>

View File

@@ -5,7 +5,6 @@ using CommunityToolkit.Mvvm.Messaging;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.UI.Xaml;
using Microsoft.Windows.AppLifecycle;
using Snap.Hutao.Core.Logging;
using System.Runtime.InteropServices;
using WinRT;
@@ -33,7 +32,7 @@ public static partial class Program
// In a Desktop app this runs a message pump internally,
// and does not return until the application shuts down.
Application.Start(InitializeApp);
ServiceScopeExtension.DisposeLast();
Control.ScopedPage.DisposePreviousScope();
}
AppInstance.GetCurrent().UnregisterKey();
@@ -54,11 +53,11 @@ public static partial class Program
ServiceProvider services = new ServiceCollection()
// Microsoft extension
.AddLogging(builder => builder.AddDebug().AddDatabase())
.AddLogging(builder => builder.AddDebug())
.AddMemoryCache()
// Hutao extensions
.AddJsonSerializerOptions()
.AddJsonOptions()
.AddDatebase()
.AddInjections()
.AddHttpClients()
@@ -66,7 +65,7 @@ public static partial class Program
// Discrete services
.AddSingleton<IMessenger>(WeakReferenceMessenger.Default)
.BuildServiceProvider();
.BuildServiceProvider(true);
Ioc.Default.ConfigureServices(services);
return services;

View File

@@ -0,0 +1,648 @@
//------------------------------------------------------------------------------
// <auto-generated>
// 此代码由工具生成。
// 运行时版本:4.0.30319.42000
//
// 对此文件的更改可能会导致不正确的行为,并且如果
// 重新生成代码,这些更改将会丢失。
// </auto-generated>
//------------------------------------------------------------------------------
namespace Snap.Hutao.Resource.Localization {
using System;
/// <summary>
/// 一个强类型的资源类,用于查找本地化的字符串等。
/// </summary>
// 此类是由 StronglyTypedResourceBuilder
// 类通过类似于 ResGen 或 Visual Studio 的工具自动生成的。
// 若要添加或移除成员,请编辑 .ResX 文件,然后重新运行 ResGen
// (以 /str 作为命令选项),或重新生成 VS 项目。
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
internal class SH {
private static global::System.Resources.ResourceManager resourceMan;
private static global::System.Globalization.CultureInfo resourceCulture;
[global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
internal SH() {
}
/// <summary>
/// 返回此类使用的缓存的 ResourceManager 实例。
/// </summary>
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
internal static global::System.Resources.ResourceManager ResourceManager {
get {
if (object.ReferenceEquals(resourceMan, null)) {
global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Snap.Hutao.Resource.Localization.SH", typeof(SH).Assembly);
resourceMan = temp;
}
return resourceMan;
}
}
/// <summary>
/// 重写当前线程的 CurrentUICulture 属性,对
/// 使用此强类型资源类的所有资源查找执行重写。
/// </summary>
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
internal static global::System.Globalization.CultureInfo Culture {
get {
return resourceCulture;
}
set {
resourceCulture = value;
}
}
/// <summary>
/// 查找类似 胡桃 Dev {0} 的本地化字符串。
/// </summary>
internal static string AppDevNameAndVersion {
get {
return ResourceManager.GetString("AppDevNameAndVersion", resourceCulture);
}
}
/// <summary>
/// 查找类似 胡桃 {0} 的本地化字符串。
/// </summary>
internal static string AppNameAndVersion {
get {
return ResourceManager.GetString("AppNameAndVersion", resourceCulture);
}
}
/// <summary>
/// 查找类似 无效的 Uri 的本地化字符串。
/// </summary>
internal static string ControlImageCachedImageInvalidResourceUri {
get {
return ResourceManager.GetString("ControlImageCachedImageInvalidResourceUri", resourceCulture);
}
}
/// <summary>
/// 查找类似 HTTP GET {0} 的本地化字符串。
/// </summary>
internal static string ControlImageCompositionImageHttpRequest {
get {
return ResourceManager.GetString("ControlImageCompositionImageHttpRequest", resourceCulture);
}
}
/// <summary>
/// 查找类似 应用 CompositionImage 的源时发生异常 的本地化字符串。
/// </summary>
internal static string ControlImageCompositionImageSystemException {
get {
return ResourceManager.GetString("ControlImageCompositionImageSystemException", resourceCulture);
}
}
/// <summary>
/// 查找类似 网格 的本地化字符串。
/// </summary>
internal static string ControlPanelPanelSelectorDropdownGridName {
get {
return ResourceManager.GetString("ControlPanelPanelSelectorDropdownGridName", resourceCulture);
}
}
/// <summary>
/// 查找类似 列表 的本地化字符串。
/// </summary>
internal static string ControlPanelPanelSelectorDropdownListName {
get {
return ResourceManager.GetString("ControlPanelPanelSelectorDropdownListName", resourceCulture);
}
}
/// <summary>
/// 查找类似 用户数据已损坏: {0} 的本地化字符串。
/// </summary>
internal static string CoreExceptionServiceUserdataCorruptedMessage {
get {
return ResourceManager.GetString("CoreExceptionServiceUserdataCorruptedMessage", resourceCulture);
}
}
/// <summary>
/// 查找类似 请勿在管理员模式下使用此功能 {0} 的本地化字符串。
/// </summary>
internal static string CoreIOPickerExtensionPickerExceptionInfoBarMessage {
get {
return ResourceManager.GetString("CoreIOPickerExtensionPickerExceptionInfoBarMessage", resourceCulture);
}
}
/// <summary>
/// 查找类似 无法打开文件选择器 的本地化字符串。
/// </summary>
internal static string CoreIOPickerExtensionPickerExceptionInfoBarTitle {
get {
return ResourceManager.GetString("CoreIOPickerExtensionPickerExceptionInfoBarTitle", resourceCulture);
}
}
/// <summary>
/// 查找类似 启动游戏 的本地化字符串。
/// </summary>
internal static string CoreJumpListHelperLaunchGameItemDisplayName {
get {
return ResourceManager.GetString("CoreJumpListHelperLaunchGameItemDisplayName", resourceCulture);
}
}
/// <summary>
/// 查找类似 快捷操作 的本地化字符串。
/// </summary>
internal static string CoreJumpListHelperLaunchGameItemGroupName {
get {
return ResourceManager.GetString("CoreJumpListHelperLaunchGameItemGroupName", resourceCulture);
}
}
/// <summary>
/// 查找类似 胡桃实时便笺刷新任务 | 请勿编辑或删除。 的本地化字符串。
/// </summary>
internal static string CoreScheduleTaskHelperDailyNoteRefreshTaskDescription {
get {
return ResourceManager.GetString("CoreScheduleTaskHelperDailyNoteRefreshTaskDescription", resourceCulture);
}
}
/// <summary>
/// 查找类似 信号量已经被释放,操作取消 的本地化字符串。
/// </summary>
internal static string CoreThreadingSemaphoreSlimDisposed {
get {
return ResourceManager.GetString("CoreThreadingSemaphoreSlimDisposed", resourceCulture);
}
}
/// <summary>
/// 查找类似 未检测到 WebView2 运行时 的本地化字符串。
/// </summary>
internal static string CoreWebView2HelperVersionUndetected {
get {
return ResourceManager.GetString("CoreWebView2HelperVersionUndetected", resourceCulture);
}
}
/// <summary>
/// 查找类似 取消 的本地化字符串。
/// </summary>
internal static string FactoryContentDialogFactoryCancelCloseButtonText {
get {
return ResourceManager.GetString("FactoryContentDialogFactoryCancelCloseButtonText", resourceCulture);
}
}
/// <summary>
/// 查找类似 确认 的本地化字符串。
/// </summary>
internal static string FactoryContentDialogFactoryConfirmPrimaryButtonText {
get {
return ResourceManager.GetString("FactoryContentDialogFactoryConfirmPrimaryButtonText", resourceCulture);
}
}
/// <summary>
/// 查找类似 单个成就存档内发现多个相同的成就 Id 的本地化字符串。
/// </summary>
internal static string ServiceAchievementUserdataCorruptedInnerIdNotUnique {
get {
return ResourceManager.GetString("ServiceAchievementUserdataCorruptedInnerIdNotUnique", resourceCulture);
}
}
/// <summary>
/// 查找类似 开始游戏 的本地化字符串。
/// </summary>
internal static string ServiceDailyNoteNotifierActionLaunchGameButton {
get {
return ResourceManager.GetString("ServiceDailyNoteNotifierActionLaunchGameButton", resourceCulture);
}
}
/// <summary>
/// 查找类似 我知道了 的本地化字符串。
/// </summary>
internal static string ServiceDailyNoteNotifierActionLaunchGameDismiss {
get {
return ResourceManager.GetString("ServiceDailyNoteNotifierActionLaunchGameDismiss", resourceCulture);
}
}
/// <summary>
/// 查找类似 请求异常 的本地化字符串。
/// </summary>
internal static string ServiceDailyNoteNotifierAttribution {
get {
return ResourceManager.GetString("ServiceDailyNoteNotifierAttribution", resourceCulture);
}
}
/// <summary>
/// 查找类似 每日委托 的本地化字符串。
/// </summary>
internal static string ServiceDailyNoteNotifierDailyTask {
get {
return ResourceManager.GetString("ServiceDailyNoteNotifierDailyTask", resourceCulture);
}
}
/// <summary>
/// 查找类似 奖励未领取 的本地化字符串。
/// </summary>
internal static string ServiceDailyNoteNotifierDailyTaskHint {
get {
return ResourceManager.GetString("ServiceDailyNoteNotifierDailyTaskHint", resourceCulture);
}
}
/// <summary>
/// 查找类似 探索派遣 的本地化字符串。
/// </summary>
internal static string ServiceDailyNoteNotifierExpedition {
get {
return ResourceManager.GetString("ServiceDailyNoteNotifierExpedition", resourceCulture);
}
}
/// <summary>
/// 查找类似 已完成 的本地化字符串。
/// </summary>
internal static string ServiceDailyNoteNotifierExpeditionAdaptiveHint {
get {
return ResourceManager.GetString("ServiceDailyNoteNotifierExpeditionAdaptiveHint", resourceCulture);
}
}
/// <summary>
/// 查找类似 探索派遣已完成 的本地化字符串。
/// </summary>
internal static string ServiceDailyNoteNotifierExpeditionHint {
get {
return ResourceManager.GetString("ServiceDailyNoteNotifierExpeditionHint", resourceCulture);
}
}
/// <summary>
/// 查找类似 洞天宝钱 的本地化字符串。
/// </summary>
internal static string ServiceDailyNoteNotifierHomeCoin {
get {
return ResourceManager.GetString("ServiceDailyNoteNotifierHomeCoin", resourceCulture);
}
}
/// <summary>
/// 查找类似 当前洞天宝钱:{0} 的本地化字符串。
/// </summary>
internal static string ServiceDailyNoteNotifierHomeCoinCurrent {
get {
return ResourceManager.GetString("ServiceDailyNoteNotifierHomeCoinCurrent", resourceCulture);
}
}
/// <summary>
/// 查找类似 多个提醒项达到设定值 的本地化字符串。
/// </summary>
internal static string ServiceDailyNoteNotifierMultiValueReached {
get {
return ResourceManager.GetString("ServiceDailyNoteNotifierMultiValueReached", resourceCulture);
}
}
/// <summary>
/// 查找类似 原粹树脂 的本地化字符串。
/// </summary>
internal static string ServiceDailyNoteNotifierResin {
get {
return ResourceManager.GetString("ServiceDailyNoteNotifierResin", resourceCulture);
}
}
/// <summary>
/// 查找类似 当前原粹树脂:{0} 的本地化字符串。
/// </summary>
internal static string ServiceDailyNoteNotifierResinCurrent {
get {
return ResourceManager.GetString("ServiceDailyNoteNotifierResinCurrent", resourceCulture);
}
}
/// <summary>
/// 查找类似 实时便笺提醒 的本地化字符串。
/// </summary>
internal static string ServiceDailyNoteNotifierTitle {
get {
return ResourceManager.GetString("ServiceDailyNoteNotifierTitle", resourceCulture);
}
}
/// <summary>
/// 查找类似 参量质变仪 的本地化字符串。
/// </summary>
internal static string ServiceDailyNoteNotifierTransformer {
get {
return ResourceManager.GetString("ServiceDailyNoteNotifierTransformer", resourceCulture);
}
}
/// <summary>
/// 查找类似 准备完成 的本地化字符串。
/// </summary>
internal static string ServiceDailyNoteNotifierTransformerAdaptiveHint {
get {
return ResourceManager.GetString("ServiceDailyNoteNotifierTransformerAdaptiveHint", resourceCulture);
}
}
/// <summary>
/// 查找类似 参量质变仪已准备完成 的本地化字符串。
/// </summary>
internal static string ServiceDailyNoteNotifierTransformerHint {
get {
return ResourceManager.GetString("ServiceDailyNoteNotifierTransformerHint", resourceCulture);
}
}
/// <summary>
/// 查找类似 无法获取祈愿记录: {0} 的本地化字符串。
/// </summary>
internal static string ServiceGachaLogArchiveCollectionUserdataCorruptedMessage {
get {
return ResourceManager.GetString("ServiceGachaLogArchiveCollectionUserdataCorruptedMessage", resourceCulture);
}
}
/// <summary>
/// 查找类似 无法获取祈愿记录 End Id 的本地化字符串。
/// </summary>
internal static string ServiceGachaLogEndIdUserdataCorruptedMessage {
get {
return ResourceManager.GetString("ServiceGachaLogEndIdUserdataCorruptedMessage", resourceCulture);
}
}
/// <summary>
/// 查找类似 角色活动 的本地化字符串。
/// </summary>
internal static string ServiceGachaLogFactoryAvatarWishName {
get {
return ResourceManager.GetString("ServiceGachaLogFactoryAvatarWishName", resourceCulture);
}
}
/// <summary>
/// 查找类似 奔行世间 的本地化字符串。
/// </summary>
internal static string ServiceGachaLogFactoryPermanentWishName {
get {
return ResourceManager.GetString("ServiceGachaLogFactoryPermanentWishName", resourceCulture);
}
}
/// <summary>
/// 查找类似 神铸赋形 的本地化字符串。
/// </summary>
internal static string ServiceGachaLogFactoryWeaponWishName {
get {
return ResourceManager.GetString("ServiceGachaLogFactoryWeaponWishName", resourceCulture);
}
}
/// <summary>
/// 查找类似 请求验证密钥失败 的本地化字符串。
/// </summary>
internal static string ServiceGachaLogUrlProviderAuthkeyRequestFailed {
get {
return ResourceManager.GetString("ServiceGachaLogUrlProviderAuthkeyRequestFailed", resourceCulture);
}
}
/// <summary>
/// 查找类似 未正确提供原神路径,或当前设置的路径不正确 的本地化字符串。
/// </summary>
internal static string ServiceGachaLogUrlProviderCachePathInvalid {
get {
return ResourceManager.GetString("ServiceGachaLogUrlProviderCachePathInvalid", resourceCulture);
}
}
/// <summary>
/// 查找类似 找不到原神内置浏览器缓存路径:\n{0} 的本地化字符串。
/// </summary>
internal static string ServiceGachaLogUrlProviderCachePathNotFound {
get {
return ResourceManager.GetString("ServiceGachaLogUrlProviderCachePathNotFound", resourceCulture);
}
}
/// <summary>
/// 查找类似 未找到可用的 Url 的本地化字符串。
/// </summary>
internal static string ServiceGachaLogUrlProviderCacheUrlNotFound {
get {
return ResourceManager.GetString("ServiceGachaLogUrlProviderCacheUrlNotFound", resourceCulture);
}
}
/// <summary>
/// 查找类似 提供的Url无效 的本地化字符串。
/// </summary>
internal static string ServiceGachaLogUrlProviderManualInputInvalid {
get {
return ResourceManager.GetString("ServiceGachaLogUrlProviderManualInputInvalid", resourceCulture);
}
}
/// <summary>
/// 查找类似 存在多个匹配账号,请删除重复的账号 的本地化字符串。
/// </summary>
internal static string ServiceGameDetectGameAccountMultiMatched {
get {
return ResourceManager.GetString("ServiceGameDetectGameAccountMultiMatched", resourceCulture);
}
}
/// <summary>
/// 查找类似 查询游戏资源信息 的本地化字符串。
/// </summary>
internal static string ServiceGameEnsureGameResourceQueryResourceInformation {
get {
return ResourceManager.GetString("ServiceGameEnsureGameResourceQueryResourceInformation", resourceCulture);
}
}
/// <summary>
/// 查找类似 游戏文件操作失败: {0} 的本地化字符串。
/// </summary>
internal static string ServiceGameFileOperationExceptionMessage {
get {
return ResourceManager.GetString("ServiceGameFileOperationExceptionMessage", resourceCulture);
}
}
/// <summary>
/// 查找类似 选择游戏本体 的本地化字符串。
/// </summary>
internal static string ServiceGameLocatorFileOpenPickerCommitText {
get {
return ResourceManager.GetString("ServiceGameLocatorFileOpenPickerCommitText", resourceCulture);
}
}
/// <summary>
/// 查找类似 找不到 Unity 日志文件 的本地化字符串。
/// </summary>
internal static string ServiceGameLocatorUnityLogFileNotFound {
get {
return ResourceManager.GetString("ServiceGameLocatorUnityLogFileNotFound", resourceCulture);
}
}
/// <summary>
/// 查找类似 在 Unity 日志文件中找不到游戏路径 的本地化字符串。
/// </summary>
internal static string ServiceGameLocatorUnityLogGamePathNotFound {
get {
return ResourceManager.GetString("ServiceGameLocatorUnityLogGamePathNotFound", resourceCulture);
}
}
/// <summary>
/// 查找类似 获取 Package Version 的本地化字符串。
/// </summary>
internal static string ServiceGamePackageRequestPackageVerion {
get {
return ResourceManager.GetString("ServiceGamePackageRequestPackageVerion", resourceCulture);
}
}
/// <summary>
/// 查找类似 获取 Package Version 失败 的本地化字符串。
/// </summary>
internal static string ServiceGamePackageRequestPackageVerionFailed {
get {
return ResourceManager.GetString("ServiceGamePackageRequestPackageVerionFailed", resourceCulture);
}
}
/// <summary>
/// 查找类似 无法找到游戏路径,请前往设置修改 的本地化字符串。
/// </summary>
internal static string ServiceGamePathLocateFailed {
get {
return ResourceManager.GetString("ServiceGamePathLocateFailed", resourceCulture);
}
}
/// <summary>
/// 查找类似 找不到游戏配置文件 {0} 的本地化字符串。
/// </summary>
internal static string ServiceGameSetMultiChannelConfigFileNotFound {
get {
return ResourceManager.GetString("ServiceGameSetMultiChannelConfigFileNotFound", resourceCulture);
}
}
/// <summary>
/// 查找类似 无法读取或保存配置文件,请以管理员模式重试 的本地化字符串。
/// </summary>
internal static string ServiceGameSetMultiChannelUnauthorizedAccess {
get {
return ResourceManager.GetString("ServiceGameSetMultiChannelUnauthorizedAccess", resourceCulture);
}
}
/// <summary>
/// 查找类似 元数据服务尚未初始化,或初始化失败 的本地化字符串。
/// </summary>
internal static string ServiceMetadataNotInitialized {
get {
return ResourceManager.GetString("ServiceMetadataNotInitialized", resourceCulture);
}
}
/// <summary>
/// 查找类似 元数据校验文件解析失败 的本地化字符串。
/// </summary>
internal static string ServiceMetadataParseFailed {
get {
return ResourceManager.GetString("ServiceMetadataParseFailed", resourceCulture);
}
}
/// <summary>
/// 查找类似 元数据校验文件下载失败 的本地化字符串。
/// </summary>
internal static string ServiceMetadataRequestFailed {
get {
return ResourceManager.GetString("ServiceMetadataRequestFailed", resourceCulture);
}
}
/// <summary>
/// 查找类似 尚未选择任何用户以及角色 的本地化字符串。
/// </summary>
internal static string ServiceUserAndRoleUnselected {
get {
return ResourceManager.GetString("ServiceUserAndRoleUnselected", resourceCulture);
}
}
/// <summary>
/// 查找类似 多个用户记录为选中状态 的本地化字符串。
/// </summary>
internal static string ServiceUserCurrentMultiMatched {
get {
return ResourceManager.GetString("ServiceUserCurrentMultiMatched", resourceCulture);
}
}
/// <summary>
/// 查找类似 用户 {0} 状态保存失败 的本地化字符串。
/// </summary>
internal static string ServiceUserCurrentUpdateAndSaveFailed {
get {
return ResourceManager.GetString("ServiceUserCurrentUpdateAndSaveFailed", resourceCulture);
}
}
/// <summary>
/// 查找类似 输入的 Cookie 必须包含 Mid 的本地化字符串。
/// </summary>
internal static string ServiceUserProcessCookieNoMid {
get {
return ResourceManager.GetString("ServiceUserProcessCookieNoMid", resourceCulture);
}
}
/// <summary>
/// 查找类似 输入的 Cookie 必须包含 Stoken 的本地化字符串。
/// </summary>
internal static string ServiceUserProcessCookieNoStoken {
get {
return ResourceManager.GetString("ServiceUserProcessCookieNoStoken", resourceCulture);
}
}
/// <summary>
/// 查找类似 输入的 Cookie 无法获取用户信息 的本地化字符串。
/// </summary>
internal static string ServiceUserProcessCookieRequestUserInfoFailed {
get {
return ResourceManager.GetString("ServiceUserProcessCookieRequestUserInfoFailed", resourceCulture);
}
}
}
}

View File

@@ -0,0 +1,315 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="AppDevNameAndVersion" xml:space="preserve">
<value>胡桃 Dev {0}</value>
</data>
<data name="AppNameAndVersion" xml:space="preserve">
<value>胡桃 {0}</value>
</data>
<data name="ControlImageCachedImageInvalidResourceUri" xml:space="preserve">
<value>无效的 Uri</value>
</data>
<data name="ControlImageCompositionImageHttpRequest" xml:space="preserve">
<value>HTTP GET {0}</value>
</data>
<data name="ControlImageCompositionImageSystemException" xml:space="preserve">
<value>应用 CompositionImage 的源时发生异常</value>
</data>
<data name="ControlPanelPanelSelectorDropdownGridName" xml:space="preserve">
<value>网格</value>
</data>
<data name="ControlPanelPanelSelectorDropdownListName" xml:space="preserve">
<value>列表</value>
</data>
<data name="CoreExceptionServiceUserdataCorruptedMessage" xml:space="preserve">
<value>用户数据已损坏: {0}</value>
</data>
<data name="CoreIOPickerExtensionPickerExceptionInfoBarMessage" xml:space="preserve">
<value>请勿在管理员模式下使用此功能 {0}</value>
</data>
<data name="CoreIOPickerExtensionPickerExceptionInfoBarTitle" xml:space="preserve">
<value>无法打开文件选择器</value>
</data>
<data name="CoreJumpListHelperLaunchGameItemDisplayName" xml:space="preserve">
<value>启动游戏</value>
</data>
<data name="CoreJumpListHelperLaunchGameItemGroupName" xml:space="preserve">
<value>快捷操作</value>
</data>
<data name="CoreScheduleTaskHelperDailyNoteRefreshTaskDescription" xml:space="preserve">
<value>胡桃实时便笺刷新任务 | 请勿编辑或删除。</value>
</data>
<data name="CoreThreadingSemaphoreSlimDisposed" xml:space="preserve">
<value>信号量已经被释放,操作取消</value>
</data>
<data name="CoreWebView2HelperVersionUndetected" xml:space="preserve">
<value>未检测到 WebView2 运行时</value>
</data>
<data name="FactoryContentDialogFactoryCancelCloseButtonText" xml:space="preserve">
<value>取消</value>
</data>
<data name="FactoryContentDialogFactoryConfirmPrimaryButtonText" xml:space="preserve">
<value>确认</value>
</data>
<data name="ServiceAchievementUserdataCorruptedInnerIdNotUnique" xml:space="preserve">
<value>单个成就存档内发现多个相同的成就 Id</value>
</data>
<data name="ServiceDailyNoteNotifierActionLaunchGameButton" xml:space="preserve">
<value>开始游戏</value>
</data>
<data name="ServiceDailyNoteNotifierActionLaunchGameDismiss" xml:space="preserve">
<value>我知道了</value>
</data>
<data name="ServiceDailyNoteNotifierAttribution" xml:space="preserve">
<value>请求异常</value>
</data>
<data name="ServiceDailyNoteNotifierDailyTask" xml:space="preserve">
<value>每日委托</value>
</data>
<data name="ServiceDailyNoteNotifierDailyTaskHint" xml:space="preserve">
<value>奖励未领取</value>
</data>
<data name="ServiceDailyNoteNotifierExpedition" xml:space="preserve">
<value>探索派遣</value>
</data>
<data name="ServiceDailyNoteNotifierExpeditionAdaptiveHint" xml:space="preserve">
<value>已完成</value>
</data>
<data name="ServiceDailyNoteNotifierExpeditionHint" xml:space="preserve">
<value>探索派遣已完成</value>
</data>
<data name="ServiceDailyNoteNotifierHomeCoin" xml:space="preserve">
<value>洞天宝钱</value>
</data>
<data name="ServiceDailyNoteNotifierHomeCoinCurrent" xml:space="preserve">
<value>当前洞天宝钱:{0}</value>
</data>
<data name="ServiceDailyNoteNotifierMultiValueReached" xml:space="preserve">
<value>多个提醒项达到设定值</value>
</data>
<data name="ServiceDailyNoteNotifierResin" xml:space="preserve">
<value>原粹树脂</value>
</data>
<data name="ServiceDailyNoteNotifierResinCurrent" xml:space="preserve">
<value>当前原粹树脂:{0}</value>
</data>
<data name="ServiceDailyNoteNotifierTitle" xml:space="preserve">
<value>实时便笺提醒</value>
</data>
<data name="ServiceDailyNoteNotifierTransformer" xml:space="preserve">
<value>参量质变仪</value>
</data>
<data name="ServiceDailyNoteNotifierTransformerAdaptiveHint" xml:space="preserve">
<value>准备完成</value>
</data>
<data name="ServiceDailyNoteNotifierTransformerHint" xml:space="preserve">
<value>参量质变仪已准备完成</value>
</data>
<data name="ServiceGachaLogArchiveCollectionUserdataCorruptedMessage" xml:space="preserve">
<value>无法获取祈愿记录: {0}</value>
</data>
<data name="ServiceGachaLogEndIdUserdataCorruptedMessage" xml:space="preserve">
<value>无法获取祈愿记录 End Id</value>
</data>
<data name="ServiceGachaLogFactoryAvatarWishName" xml:space="preserve">
<value>角色活动</value>
</data>
<data name="ServiceGachaLogFactoryPermanentWishName" xml:space="preserve">
<value>奔行世间</value>
</data>
<data name="ServiceGachaLogFactoryWeaponWishName" xml:space="preserve">
<value>神铸赋形</value>
</data>
<data name="ServiceGachaLogUrlProviderAuthkeyRequestFailed" xml:space="preserve">
<value>请求验证密钥失败</value>
</data>
<data name="ServiceGachaLogUrlProviderCachePathInvalid" xml:space="preserve">
<value>未正确提供原神路径,或当前设置的路径不正确</value>
</data>
<data name="ServiceGachaLogUrlProviderCachePathNotFound" xml:space="preserve">
<value>找不到原神内置浏览器缓存路径:\n{0}</value>
</data>
<data name="ServiceGachaLogUrlProviderCacheUrlNotFound" xml:space="preserve">
<value>未找到可用的 Url</value>
</data>
<data name="ServiceGachaLogUrlProviderManualInputInvalid" xml:space="preserve">
<value>提供的Url无效</value>
</data>
<data name="ServiceGameDetectGameAccountMultiMatched" xml:space="preserve">
<value>存在多个匹配账号,请删除重复的账号</value>
</data>
<data name="ServiceGameEnsureGameResourceQueryResourceInformation" xml:space="preserve">
<value>查询游戏资源信息</value>
</data>
<data name="ServiceGameFileOperationExceptionMessage" xml:space="preserve">
<value>游戏文件操作失败: {0}</value>
</data>
<data name="ServiceGameLocatorFileOpenPickerCommitText" xml:space="preserve">
<value>选择游戏本体</value>
</data>
<data name="ServiceGameLocatorUnityLogFileNotFound" xml:space="preserve">
<value>找不到 Unity 日志文件</value>
</data>
<data name="ServiceGameLocatorUnityLogGamePathNotFound" xml:space="preserve">
<value>在 Unity 日志文件中找不到游戏路径</value>
</data>
<data name="ServiceGamePackageRequestPackageVerion" xml:space="preserve">
<value>获取 Package Version</value>
</data>
<data name="ServiceGamePackageRequestPackageVerionFailed" xml:space="preserve">
<value>获取 Package Version 失败</value>
</data>
<data name="ServiceGamePathLocateFailed" xml:space="preserve">
<value>无法找到游戏路径,请前往设置修改</value>
</data>
<data name="ServiceGameSetMultiChannelConfigFileNotFound" xml:space="preserve">
<value>找不到游戏配置文件 {0}</value>
</data>
<data name="ServiceGameSetMultiChannelUnauthorizedAccess" xml:space="preserve">
<value>无法读取或保存配置文件,请以管理员模式重试</value>
</data>
<data name="ServiceMetadataNotInitialized" xml:space="preserve">
<value>元数据服务尚未初始化,或初始化失败</value>
</data>
<data name="ServiceMetadataParseFailed" xml:space="preserve">
<value>元数据校验文件解析失败</value>
</data>
<data name="ServiceMetadataRequestFailed" xml:space="preserve">
<value>元数据校验文件下载失败</value>
</data>
<data name="ServiceUserAndRoleUnselected" xml:space="preserve">
<value>尚未选择任何用户以及角色</value>
</data>
<data name="ServiceUserCurrentMultiMatched" xml:space="preserve">
<value>多个用户记录为选中状态</value>
</data>
<data name="ServiceUserCurrentUpdateAndSaveFailed" xml:space="preserve">
<value>用户 {0} 状态保存失败</value>
</data>
<data name="ServiceUserProcessCookieNoMid" xml:space="preserve">
<value>输入的 Cookie 必须包含 Mid</value>
</data>
<data name="ServiceUserProcessCookieNoStoken" xml:space="preserve">
<value>输入的 Cookie 必须包含 Stoken</value>
</data>
<data name="ServiceUserProcessCookieRequestUserInfoFailed" xml:space="preserve">
<value>输入的 Cookie 无法获取用户信息</value>
</data>
</root>

View File

@@ -114,14 +114,14 @@ internal class AchievementService : IAchievementService
List<BindingAchievement> results = new();
foreach (MetadataAchievement meta in metadata)
{
EntityAchievement? entity;
EntityAchievement? entity = null;
try
{
entity = entities.SingleOrDefault(e => e.Id == meta.Id);
}
catch (InvalidOperationException ex)
{
throw new UserdataCorruptedException("单个成就存档内发现多个相同的成就 Id", ex);
ThrowHelper.UserdataCorrupted(SH.ServiceAchievementUserdataCorruptedInnerIdNotUnique, ex);
}
entity ??= EntityAchievement.Create(archiveId, meta.Id);
@@ -154,7 +154,6 @@ internal class AchievementService : IAchievementService
/// <inheritdoc/>
public async Task<ImportResult> ImportFromUIAFAsync(EntityArchive archive, List<UIAFItem> list, ImportStrategy strategy)
{
// return Task.Run(() => ImportFromUIAF(archive, list, strategy));
await ThreadHelper.SwitchToBackgroundAsync();
Guid archiveId = archive.InnerId;
@@ -190,7 +189,7 @@ internal class AchievementService : IAchievementService
public void SaveAchievements(EntityArchive archive, IList<BindingAchievement> achievements)
{
string name = archive.Name;
logger.LogInformation(EventIds.Achievement, "Begin saving achievements for [{name}]", name);
logger.LogInformation("Begin saving achievements for [{name}]", name);
ValueStopwatch stopwatch = ValueStopwatch.StartNew();
IEnumerable<EntityAchievement> newData = achievements
@@ -200,8 +199,8 @@ internal class AchievementService : IAchievementService
ImportResult result = achievementDbOperation.Overwrite(archive.InnerId, newData);
double time = stopwatch.GetElapsedTime().TotalMilliseconds;
logger.LogInformation(EventIds.Achievement, "{add} added, {update} updated, {remove} removed", result.Add, result.Update, result.Remove);
logger.LogInformation(EventIds.Achievement, "Save achievements for [{name}] completed in {time}ms", name, time);
logger.LogInformation("{add} added, {update} updated, {remove} removed", result.Add, result.Update, result.Remove);
logger.LogInformation("Save achievements for [{name}] completed in {time}ms", name, time);
}
/// <inheritdoc/>

View File

@@ -57,7 +57,7 @@ internal partial class AnnouncementService : IAnnouncementService
Dictionary<int, string> contentMap = contents
.ToDictionary(id => id.AnnId, content => content.Content);
// 将活动公告置于
// 将活动公告置于
wrapper.List.Reverse();
// 将公告内容联入公告列表

View File

@@ -35,11 +35,10 @@ internal class SummaryFactoryImplementation
return new()
{
Player = SummaryHelper.CreatePlayer(playerInfo),
Avatars = avatarInfos.Where(a => !AvatarIds.IsPlayer(a.AvatarId)).Select(a =>
{
SummaryAvatarFactory summaryAvatarFactory = new(metadataContext, a);
return summaryAvatarFactory.CreateAvatar();
}).ToList(),
Avatars = avatarInfos
.Where(a => !AvatarIds.IsPlayer(a.AvatarId))
.Select(a => new SummaryAvatarFactory(metadataContext, a).CreateAvatar())
.ToList(),
};
}
}

View File

@@ -54,10 +54,10 @@ internal class DailyNoteNotifier
if (!entry.ResinNotifySuppressed)
{
notifyInfos.Add(new(
"原粹树脂",
SH.ServiceDailyNoteNotifierResin,
"ms-appx:///Resource/Icon/UI_ItemIcon_210_256.png",
$"{entry.DailyNote.CurrentResin}",
$"当前原粹树脂:{entry.DailyNote.CurrentResin}"));
string.Format(SH.ServiceDailyNoteNotifierResinCurrent, entry.DailyNote.CurrentResin)));
entry.ResinNotifySuppressed = true;
}
}
@@ -71,10 +71,10 @@ internal class DailyNoteNotifier
if (!entry.HomeCoinNotifySuppressed)
{
notifyInfos.Add(new(
"洞天宝钱",
SH.ServiceDailyNoteNotifierHomeCoin,
"ms-appx:///Resource/Icon/UI_ItemIcon_204.png",
$"{entry.DailyNote.CurrentHomeCoin}",
$"当前洞天宝钱:{entry.DailyNote.CurrentHomeCoin}"));
string.Format(SH.ServiceDailyNoteNotifierHomeCoinCurrent, entry.DailyNote.CurrentHomeCoin)));
entry.HomeCoinNotifySuppressed = true;
}
}
@@ -88,9 +88,9 @@ internal class DailyNoteNotifier
if (!entry.DailyTaskNotifySuppressed)
{
notifyInfos.Add(new(
"每日委托",
SH.ServiceDailyNoteNotifierDailyTask,
"ms-appx:///Resource/Icon/UI_MarkQuest_Events_Proce.png",
$"奖励待领取",
SH.ServiceDailyNoteNotifierDailyTaskHint,
entry.DailyNote.ExtraTaskRewardDescription));
entry.DailyTaskNotifySuppressed = true;
}
@@ -105,10 +105,10 @@ internal class DailyNoteNotifier
if (!entry.TransformerNotifySuppressed)
{
notifyInfos.Add(new(
"参量质变仪",
SH.ServiceDailyNoteNotifierTransformer,
"ms-appx:///Resource/Icon/UI_ItemIcon_220021.png",
$"准备完成",
"参量质变仪已准备完成"));
SH.ServiceDailyNoteNotifierTransformerAdaptiveHint,
SH.ServiceDailyNoteNotifierTransformerHint));
entry.TransformerNotifySuppressed = true;
}
}
@@ -122,10 +122,10 @@ internal class DailyNoteNotifier
if (!entry.ExpeditionNotifySuppressed)
{
notifyInfos.Add(new(
"探索派遣",
AvatarIconConverter.IconNameToUri("UI_AvatarIcon_Side_None.png").ToString(),
$"已完成",
"探索派遣已完成"));
SH.ServiceDailyNoteNotifierExpedition,
Web.HutaoEndpoints.UIAvatarIconSideNone.ToString(), // TODO: embed this
SH.ServiceDailyNoteNotifierExpeditionAdaptiveHint,
SH.ServiceDailyNoteNotifierExpeditionHint));
entry.ExpeditionNotifySuppressed = true;
}
}
@@ -150,7 +150,7 @@ internal class DailyNoteNotifier
.GetActionTicketByStokenAsync("game_role", entry.User)
.ConfigureAwait(false);
string? attribution = "请求异常";
string? attribution = SH.ServiceDailyNoteNotifierAttribution;
if (actionTicketResponse.IsOk())
{
Response<ListWrapper<UserGameRole>> rolesResponse = await scope.ServiceProvider
@@ -166,10 +166,13 @@ internal class DailyNoteNotifier
}
ToastContentBuilder builder = new ToastContentBuilder()
.AddHeader("DAILYNOTE", "实时便笺提醒", "DAILYNOTE")
.AddHeader("DAILYNOTE", SH.ServiceDailyNoteNotifierTitle, "DAILYNOTE")
.AddAttributionText(attribution)
.AddButton(new ToastButton().SetContent("开始游戏").AddArgument("Action", "LaunchGame").AddArgument("Uid", entry.Uid))
.AddButton(new ToastButtonDismiss("我知道了"));
.AddButton(new ToastButton()
.SetContent(SH.ServiceDailyNoteNotifierActionLaunchGameButton)
.AddArgument("Action", Core.LifeCycle.Activation.LaunchGame)
.AddArgument("Uid", entry.Uid))
.AddButton(new ToastButtonDismiss(SH.ServiceDailyNoteNotifierActionLaunchGameDismiss));
if (appDbContext.Settings.SingleOrAdd(SettingEntry.DailyNoteReminderNotify, SettingEntryHelper.FalseString).GetBoolean())
{
@@ -178,7 +181,7 @@ internal class DailyNoteNotifier
if (notifyInfos.Count > 2)
{
builder.AddText("多个提醒项达到设定值");
builder.AddText(SH.ServiceDailyNoteNotifierMultiValueReached);
// Desktop and Mobile started supporting adaptive toasts in API contract 3 (Anniversary Update)
if (ApiInformation.IsApiContractPresent("Windows.Foundation.UniversalApiContract", 3))

View File

@@ -65,9 +65,9 @@ internal class GachaStatisticsFactory : IGachaStatisticsFactory
Dictionary<WeaponId, Weapon> weaponMap,
bool isEmptyHistoryWishVisible)
{
TypedWishSummaryBuilder permanentWishBuilder = new("奔行世间", TypedWishSummaryBuilder.IsPermanentWish, 90, 10);
TypedWishSummaryBuilder avatarWishBuilder = new("角色活动", TypedWishSummaryBuilder.IsAvatarEventWish, 90, 10);
TypedWishSummaryBuilder weaponWishBuilder = new("神铸赋形", TypedWishSummaryBuilder.IsWeaponEventWish, 80, 10);
TypedWishSummaryBuilder permanentWishBuilder = new(SH.ServiceGachaLogFactoryPermanentWishName, TypedWishSummaryBuilder.IsPermanentWish, 90, 10);
TypedWishSummaryBuilder avatarWishBuilder = new(SH.ServiceGachaLogFactoryAvatarWishName, TypedWishSummaryBuilder.IsAvatarEventWish, 90, 10);
TypedWishSummaryBuilder weaponWishBuilder = new(SH.ServiceGachaLogFactoryWeaponWishName, TypedWishSummaryBuilder.IsWeaponEventWish, 80, 10);
Dictionary<Avatar, int> orangeAvatarCounter = new();
Dictionary<Avatar, int> purpleAvatarCounter = new();
@@ -136,6 +136,7 @@ internal class GachaStatisticsFactory : IGachaStatisticsFactory
else
{
// ItemId place not correct.
// TODO: check items id when importing
Must.NeverHappen();
}
}

View File

@@ -6,6 +6,7 @@ using Microsoft.Data.Sqlite;
using Microsoft.EntityFrameworkCore;
using Snap.Hutao.Core.Database;
using Snap.Hutao.Core.Diagnostics;
using Snap.Hutao.Core.ExceptionService;
using Snap.Hutao.Core.Logging;
using Snap.Hutao.Extension;
using Snap.Hutao.Model.Binding.Gacha;
@@ -119,12 +120,14 @@ internal class GachaLogService : IGachaLogService
await ThreadHelper.SwitchToMainThreadAsync();
try
{
return archiveCollection ??= appDbContext.GachaArchives.AsNoTracking().ToObservableCollection();
archiveCollection ??= appDbContext.GachaArchives.AsNoTracking().ToObservableCollection();
}
catch (SqliteException ex)
{
throw new Core.ExceptionService.UserdataCorruptedException($"无法获取祈愿记录: {ex.Message}", ex);
ThrowHelper.UserdataCorrupted(string.Format(SH.ServiceGachaLogArchiveCollectionUserdataCorruptedMessage, ex.Message), ex);
}
return archiveCollection;
}
/// <inheritdoc/>
@@ -349,7 +352,7 @@ internal class GachaLogService : IGachaLogService
}
catch (SqliteException ex)
{
throw new Core.ExceptionService.UserdataCorruptedException("无法获取祈愿记录 End Id", ex);
ThrowHelper.UserdataCorrupted(SH.ServiceGachaLogEndIdUserdataCorruptedMessage, ex);
}
}

View File

@@ -29,7 +29,7 @@ internal class GachaLogUrlManualInputProvider : IGachaLogUrlProvider
}
else
{
return new(false, "提供的Url无效");
return new(false, SH.ServiceGachaLogUrlProviderManualInputInvalid);
}
}
else

View File

@@ -1,10 +1,11 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Model.Binding.User;
using Snap.Hutao.Service.User;
using Snap.Hutao.Web.Hoyolab;
using Snap.Hutao.Web.Hoyolab.Hk4e.Event.GachaInfo;
using Snap.Hutao.Web.Hoyolab.Takumi.Binding;
using Snap.Hutao.Web.Response;
namespace Snap.Hutao.Service.GachaLog;
@@ -34,32 +35,23 @@ internal class GachaLogUrlStokenProvider : IGachaLogUrlProvider
/// <inheritdoc/>
public async Task<ValueResult<bool, string>> GetQueryAsync()
{
Model.Binding.User.User? user = userService.Current;
if (user != null && user.SelectedUserGameRole != null)
if (UserAndUid.TryFromUser(userService.Current, out UserAndUid? userAndUid))
{
if (user.Stoken != null)
{
PlayerUid uid = (PlayerUid)user.SelectedUserGameRole;
GenAuthKeyData data = GenAuthKeyData.CreateForWebViewGacha(uid);
GenAuthKeyData data = GenAuthKeyData.CreateForWebViewGacha(userAndUid.Uid);
Response<GameAuthKey> authkeyResponse = await bindingClient2.GenerateAuthenticationKeyAsync(userAndUid.User, data).ConfigureAwait(false);
Web.Response.Response<GameAuthKey> authkeyResponse = await bindingClient2.GenerateAuthenticationKeyAsync(user.Entity, data).ConfigureAwait(false);
if (authkeyResponse.IsOk())
{
return new(true, GachaLogConfigration.AsQuery(data, authkeyResponse.Data));
}
else
{
return new(false, "请求验证密钥失败");
}
if (authkeyResponse.IsOk())
{
return new(true, GachaLogConfigration.AsQuery(data, authkeyResponse.Data));
}
else
{
return new(false, "当前用户的 Cookie 不包含 Stoken");
return new(false, SH.ServiceGachaLogUrlProviderAuthkeyRequestFailed);
}
}
else
{
return new(false, "尚未选择要刷新的用户以及角色");
return new(false, SH.ServiceUserAndRoleUnselected);
}
}
}

View File

@@ -56,7 +56,7 @@ internal class GachaLogUrlWebCacheProvider : IGachaLogUrlProvider
{
if (tempFile == null)
{
return new(false, $"找不到原神内置浏览器缓存路径:\n{cacheFile}");
return new(false, string.Format(SH.ServiceGachaLogUrlProviderCachePathNotFound, cacheFile));
}
using (FileStream fileStream = new(tempFile.Path, FileMode.Open, FileAccess.Read, FileShare.Read))
@@ -65,14 +65,14 @@ internal class GachaLogUrlWebCacheProvider : IGachaLogUrlProvider
{
await fileStream.CopyToAsync(memoryStream).ConfigureAwait(false);
string? result = Match(memoryStream, cacheFile.Contains(GameConstants.GenshinImpactData));
return new(!string.IsNullOrEmpty(result), result ?? "未找到可用的 Url");
return new(!string.IsNullOrEmpty(result), result ?? SH.ServiceGachaLogUrlProviderCacheUrlNotFound);
}
}
}
}
else
{
return new(false, "未正确提供原神路径,或当前设置的路径不正确");
return new(false, SH.ServiceGachaLogUrlProviderCachePathInvalid);
}
}

View File

@@ -14,7 +14,7 @@ internal class GameFileOperationException : Exception
/// <param name="message">消息</param>
/// <param name="innerException">内部错误</param>
public GameFileOperationException(string message, Exception innerException)
: base($"游戏文件操作失败: {message}", innerException)
: base(string.Format(SH.ServiceGameFileOperationExceptionMessage, message), innerException)
{
}
}

View File

@@ -6,6 +6,7 @@ using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.DependencyInjection;
using Snap.Hutao.Core;
using Snap.Hutao.Core.Database;
using Snap.Hutao.Core.ExceptionService;
using Snap.Hutao.Core.IO.Ini;
using Snap.Hutao.Extension;
using Snap.Hutao.Model.Binding.LaunchGame;
@@ -92,7 +93,7 @@ internal class GameService : IGameService
}
else
{
return new(false, "请启动游戏后再次尝试");
return new(false, SH.ServiceGamePathLocateFailed);
}
}
@@ -181,15 +182,15 @@ internal class GameService : IGameService
}
catch (FileNotFoundException ex)
{
throw new GameFileOperationException($"找不到游戏配置文件 {configPath}", ex);
throw new GameFileOperationException(string.Format(SH.ServiceGameSetMultiChannelConfigFileNotFound, configPath), ex);
}
catch (DirectoryNotFoundException ex)
{
throw new GameFileOperationException($"找不到游戏配置文件 {configPath}", ex);
throw new GameFileOperationException(string.Format(SH.ServiceGameSetMultiChannelConfigFileNotFound, configPath), ex);
}
catch (UnauthorizedAccessException ex)
{
throw new GameFileOperationException($"无法读取或保存配置文件,请以管理员模式重试。", ex);
throw new GameFileOperationException(SH.ServiceGameSetMultiChannelUnauthorizedAccess, ex);
}
bool changed = false;
@@ -236,7 +237,7 @@ internal class GameService : IGameService
string gameFolder = Path.GetDirectoryName(gamePath)!;
string gameFileName = Path.GetFileName(gamePath);
progress.Report(new("查询游戏资源信息"));
progress.Report(new(SH.ServiceGameEnsureGameResourceQueryResourceInformation));
Response<GameResource> response = await Ioc.Default
.GetRequiredService<ResourceClient>()
.GetResourceAsync(launchScheme)
@@ -321,6 +322,7 @@ internal class GameService : IGameService
}
// https://docs.unity.cn/cn/current/Manual/PlayerCommandLineArguments.html
// TODO: impl monitor option.
string commandLine = new CommandLineBuilder()
.AppendIf("-popupwindow", configuration.IsBorderless)
.Append("-screen-fullscreen", configuration.IsFullScreen ? 1 : 0)
@@ -343,31 +345,24 @@ internal class GameService : IGameService
using (await gameSemaphore.EnterAsync().ConfigureAwait(false))
{
try
if (configuration.UnlockFPS)
{
if (configuration.UnlockFPS)
{
IGameFpsUnlocker unlocker = new GameFpsUnlocker(game, configuration.TargetFPS);
IGameFpsUnlocker unlocker = new GameFpsUnlocker(game, configuration.TargetFPS);
TimeSpan findModuleDelay = TimeSpan.FromMilliseconds(100);
TimeSpan findModuleLimit = TimeSpan.FromMilliseconds(10000);
TimeSpan adjustFpsDelay = TimeSpan.FromMilliseconds(2000);
if (game.Start())
{
await unlocker.UnlockAsync(findModuleDelay, findModuleLimit, adjustFpsDelay).ConfigureAwait(false);
}
}
else
TimeSpan findModuleDelay = TimeSpan.FromMilliseconds(100);
TimeSpan findModuleLimit = TimeSpan.FromMilliseconds(10000);
TimeSpan adjustFpsDelay = TimeSpan.FromMilliseconds(2000);
if (game.Start())
{
if (game.Start())
{
await game.WaitForExitAsync().ConfigureAwait(false);
}
await unlocker.UnlockAsync(findModuleDelay, findModuleLimit, adjustFpsDelay).ConfigureAwait(false);
}
}
catch (Win32Exception)
else
{
// 通常是用户取消了UAC
if (game.Start())
{
await game.WaitForExitAsync().ConfigureAwait(false);
}
}
}
}
@@ -380,14 +375,14 @@ internal class GameService : IGameService
string? registrySdk = RegistryInterop.Get();
if (!string.IsNullOrEmpty(registrySdk))
{
GameAccount? account;
GameAccount? account = null;
try
{
account = gameAccounts.SingleOrDefault(a => a.MihoyoSDK == registrySdk);
}
catch (InvalidOperationException ex)
{
throw new Core.ExceptionService.UserdataCorruptedException("已存在多个匹配账号,请先删除重复的账号", ex);
ThrowHelper.UserdataCorrupted(SH.ServiceGameDetectGameAccountMultiMatched, ex);
}
if (account == null)

View File

@@ -36,7 +36,11 @@ internal class ManualGameLocator : IGameLocator
private async Task<ValueResult<bool, string>> LocateInternalAsync(List<string> fileNames)
{
FileOpenPicker picker = pickerFactory.GetFileOpenPicker(PickerLocationId.Desktop, "选择游戏本体", ".exe");
FileOpenPicker picker = pickerFactory.GetFileOpenPicker(
PickerLocationId.Desktop,
SH.ServiceGameLocatorFileOpenPickerCommitText,
".exe");
(bool isPickerOk, FilePath file) = await picker.TryPickSingleFileAsync().ConfigureAwait(false);
if (isPickerOk)

View File

@@ -20,6 +20,7 @@ internal partial class UnityLogGameLocator : IGameLocator
public async Task<ValueResult<bool, string>> LocateGamePathAsync()
{
await ThreadHelper.SwitchToBackgroundAsync();
string appDataPath = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData);
string logFilePathChinese = Path.Combine(appDataPath, @"..\LocalLow\miHoYo\原神\output_log.txt");
string logFilePathOvsesea = Path.Combine(appDataPath, @"..\LocalLow\miHoYo\Genshin Impact\output_log.txt");
@@ -36,7 +37,7 @@ internal partial class UnityLogGameLocator : IGameLocator
Match matchResult = WarmupFileLine().Match(content);
if (!matchResult.Success)
{
return new(false, $"在 Unity 日志文件中找不到游戏路径");
return new(false, SH.ServiceGameLocatorUnityLogGamePathNotFound);
}
string entryName = matchResult.Groups[0].Value.Replace("_Data", ".exe");
@@ -45,7 +46,7 @@ internal partial class UnityLogGameLocator : IGameLocator
}
else
{
return new(false, $"找不到 Unity 日志文件");
return new(false, SH.ServiceGameLocatorUnityLogFileNotFound);
}
}
}

View File

@@ -2,6 +2,7 @@
// Licensed under the MIT license.
using Snap.Hutao.Core.DependencyInjection.Annotation.HttpClient;
using Snap.Hutao.Core.ExceptionService;
using Snap.Hutao.Core.IO;
using Snap.Hutao.Model.Binding.LaunchGame;
using Snap.Hutao.Web.Hoyolab.SdkStatic.Hk4e.Launcher;
@@ -49,8 +50,8 @@ internal class PackageConverter
Uri pkgVersionUri = new($"{scatteredFilesUrl}/pkg_version");
ConvertDirection direction = targetScheme.IsOversea ? ConvertDirection.ChineseToOversea : ConvertDirection.OverseaToChinese;
progress.Report(new("获取 Package Version"));
Dictionary<string, VersionItem> remoteItems;
progress.Report(new(SH.ServiceGamePackageRequestPackageVerion));
Dictionary<string, VersionItem> remoteItems = default!;
try
{
using (Stream remoteSteam = await httpClient.GetStreamAsync(pkgVersionUri).ConfigureAwait(false))
@@ -60,7 +61,7 @@ internal class PackageConverter
}
catch (IOException ex)
{
throw new PackageConvertException("下载 Package Version 失败", ex);
ThrowHelper.PackageConvert(SH.ServiceGamePackageRequestPackageVerionFailed, ex);
}
Dictionary<string, VersionItem> localItems;

View File

@@ -33,7 +33,7 @@ internal class GameFpsUnlocker : IGameFpsUnlocker
/// <param name="targetFPS">目标fps</param>
public GameFpsUnlocker(Process gameProcess, int targetFPS)
{
Must.Range(targetFPS >= 30 && targetFPS <= 2000, "目标FPS超过允许值");
Must.Range(targetFPS >= 30 && targetFPS <= 2000, "Target FPS threshold exceeded");
TargetFps = targetFPS;
this.gameProcess = gameProcess;
@@ -45,12 +45,12 @@ internal class GameFpsUnlocker : IGameFpsUnlocker
/// <inheritdoc/>
public async Task UnlockAsync(TimeSpan findModuleDelay, TimeSpan findModuleLimit, TimeSpan adjustFpsDelay)
{
Verify.Operation(isValid, "此解锁器已经失效");
Verify.Operation(isValid, "This Unlocker is invalid");
MODULEENTRY32 unityPlayer = await FindModuleAsync(findModuleDelay, findModuleLimit).ConfigureAwait(false);
// Read UnityPlayer.dll
TryReadModuleMemoryFindFpsAddress(unityPlayer);
UnsafeTryReadModuleMemoryFindFpsAddress(unityPlayer);
// When player switch between scenes, we have to re adjust the fps
// So we keep a loop here
@@ -73,7 +73,7 @@ internal class GameFpsUnlocker : IGameFpsUnlocker
return WriteProcessMemory(process.SafeHandle, (void*)baseAddress, lpBuffer, sizeof(int), null);
}
private static unsafe MODULEENTRY32 FindModule(int processId, string moduleName)
private static unsafe MODULEENTRY32 UnsafeFindModule(int processId, string moduleName)
{
HANDLE snapshot = CreateToolhelp32Snapshot(CREATE_TOOLHELP_SNAPSHOT_FLAGS.TH32CS_SNAPMODULE, (uint)processId);
try
@@ -109,7 +109,7 @@ internal class GameFpsUnlocker : IGameFpsUnlocker
while (true)
{
MODULEENTRY32 module = FindModule(gameProcess.Id, "UnityPlayer.dll");
MODULEENTRY32 module = UnsafeFindModule(gameProcess.Id, "UnityPlayer.dll");
if (!StructMarshal.IsDefault(module))
{
return module;
@@ -145,7 +145,7 @@ internal class GameFpsUnlocker : IGameFpsUnlocker
}
}
private unsafe void TryReadModuleMemoryFindFpsAddress(MODULEENTRY32 unityPlayer)
private unsafe void UnsafeTryReadModuleMemoryFindFpsAddress(MODULEENTRY32 unityPlayer)
{
bool readOk = UnsafeReadModuleMemory(gameProcess, unityPlayer, out Span<byte> image);
Verify.Operation(readOk, "读取内存失败");

View File

@@ -1,28 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Snap.Hutao.Core.Diagnostics;
using Snap.Hutao.Win32;
using System.Diagnostics;
using System.Runtime.InteropServices;
using Windows.Win32.Foundation;
using Windows.Win32.System.Diagnostics.ToolHelp;
using static Windows.Win32.PInvoke;
namespace Snap.Hutao.Service.Game.Unlocker;
/// <summary>
/// 游戏帧率解锁器异常
/// </summary>
internal class GameFpsUnlockerException : Exception
{
/// <summary>
/// 构造一个新的用户数据损坏异常
/// </summary>
/// <param name="message">消息</param>
/// <param name="innerException">内部错误</param>
public GameFpsUnlockerException(Exception innerException)
: base($"解锁帧率失败: {innerException.Message}", innerException)
{
}
}

View File

@@ -94,13 +94,13 @@ internal partial class MetadataService : IMetadataService, IMetadataServiceIniti
if (metaMd5Map is null)
{
infoBarService.Error("元数据校验文件解析失败");
infoBarService.Error(SH.ServiceMetadataParseFailed);
return false;
}
}
catch (HttpRequestException ex)
{
infoBarService.Error(ex, "元数据校验文件下载失败");
infoBarService.Error(ex, SH.ServiceMetadataRequestFailed);
return false;
}
@@ -140,7 +140,7 @@ internal partial class MetadataService : IMetadataService, IMetadataServiceIniti
if (!skip)
{
logger.LogInformation(EventIds.MetadataFileMD5Check, "MD5 of {file} not matched, begin downloading", fileFullName);
logger.LogInformation("MD5 of {file} not matched, begin downloading", fileFullName);
await DownloadMetadataAsync(fileFullName, token).ConfigureAwait(false);
}
@@ -172,7 +172,7 @@ internal partial class MetadataService : IMetadataService, IMetadataServiceIniti
private async ValueTask<T> FromCacheOrFileAsync<T>(string fileName, CancellationToken token)
where T : class
{
Verify.Operation(isInitialized, "元数据服务尚未初始化,或初始化失败");
Verify.Operation(isInitialized, SH.ServiceMetadataNotInitialized);
string cacheKey = $"{nameof(MetadataService)}.Cache.{fileName}";
if (memoryCache.TryGetValue(cacheKey, out object? value))

View File

@@ -48,6 +48,7 @@ public interface INavigationService
/// <summary>
/// 导航到指定类型的页面
/// 若已经处于当前页面不会向页面发送消息
/// </summary>
/// <typeparam name="T">指定的页面类型</typeparam>
/// <param name="data">要传递的数据</param>
@@ -58,6 +59,7 @@ public interface INavigationService
/// <summary>
/// 异步的导航到指定类型的页面
/// 若已经处于当前页面则会向页面发送消息
/// </summary>
/// <typeparam name="TPage">指定的页面类型</typeparam>
/// <param name="data">要传递的数据</param>

View File

@@ -102,7 +102,7 @@ internal class NavigationService : INavigationService
if (currentType == pageType)
{
logger.LogInformation(EventIds.NavigationHistory, "Navigate to {pageType} : succeed, already in", pageType);
logger.LogInformation("Navigate to {pageType} : succeed, already in", pageType);
return NavigationResult.AlreadyNavigatedTo;
}
@@ -112,11 +112,11 @@ internal class NavigationService : INavigationService
try
{
navigated = Frame?.Navigate(pageType, data) ?? false;
logger.LogInformation(EventIds.NavigationHistory, "Navigate to {pageType} : {result}", pageType, navigated ? "succeed" : "failed");
logger.LogInformation("Navigate to {pageType} : {result}", pageType, navigated ? "succeed" : "failed");
}
catch (Exception ex)
{
logger.LogError(EventIds.NavigationFailed, ex, "An error occurred while navigating to {pageType}", pageType);
logger.LogError(ex, "An error occurred while navigating to {pageType}", pageType);
infoBarService.Error(ex);
}
@@ -148,7 +148,7 @@ internal class NavigationService : INavigationService
}
catch (Exception ex)
{
logger.LogError(EventIds.NavigationFailed, ex, "异步导航时发生异常");
logger.LogError(ex, "异步导航时发生异常");
return NavigationResult.Failed;
}
}

View File

@@ -5,6 +5,7 @@ using CommunityToolkit.Mvvm.Messaging;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Snap.Hutao.Core.Database;
using Snap.Hutao.Core.ExceptionService;
using Snap.Hutao.Extension;
using Snap.Hutao.Message;
using Snap.Hutao.Model.Entity.Database;
@@ -82,7 +83,7 @@ internal class UserService : IUserService
}
catch (InvalidOperationException ex)
{
throw new Core.ExceptionService.UserdataCorruptedException($"用户 {currentUser.UserInfo?.Uid} 状态保存失败", ex);
ThrowHelper.UserdataCorrupted(string.Format(SH.ServiceUserCurrentUpdateAndSaveFailed, currentUser.UserInfo?.Uid), ex);
}
}
@@ -104,15 +105,10 @@ internal class UserService : IUserService
await ThreadHelper.SwitchToBackgroundAsync();
using (IServiceScope scope = scopeFactory.CreateScope())
{
try
{
// Note: cascade deleted dailynotes
await scope.ServiceProvider.GetRequiredService<AppDbContext>().Users.RemoveAndSaveAsync(user.Entity).ConfigureAwait(false);
}
catch (DbUpdateConcurrencyException ex)
{
throw new Core.ExceptionService.UserdataCorruptedException("用户已被其他功能删除", ex);
}
AppDbContext appDbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
await appDbContext.Users
.ExecuteDeleteWhereAsync(u => u.InnerId == user.Entity.InnerId)
.ConfigureAwait(false);
}
messenger.Send(new UserRemovedMessage(user.Entity));
@@ -144,7 +140,7 @@ internal class UserService : IUserService
}
catch (InvalidOperationException ex)
{
throw new Core.ExceptionService.UserdataCorruptedException("无法设置当前用户", ex);
throw new UserdataCorruptedException(SH.ServiceUserCurrentMultiMatched, ex);
}
}
@@ -201,7 +197,7 @@ internal class UserService : IUserService
if (mid == null)
{
return new(UserOptionResult.Invalid, "输入的Cookie无法获取用户信息");
return new(UserOptionResult.Invalid, SH.ServiceUserProcessCookieNoMid);
}
// 检查 mid 对应用户是否存在
@@ -222,7 +218,7 @@ internal class UserService : IUserService
}
else
{
return new(UserOptionResult.Invalid, "必须包含 Stoken");
return new(UserOptionResult.Invalid, SH.ServiceUserProcessCookieNoStoken);
}
}
}
@@ -266,7 +262,7 @@ internal class UserService : IUserService
}
else
{
return new(UserOptionResult.Invalid, "输入的 Cookie 无法获取用户信息");
return new(UserOptionResult.Invalid, SH.ServiceUserProcessCookieRequestUserInfoFailed);
}
}
}

View File

@@ -174,13 +174,13 @@
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.VisualStudio.Validation" Version="17.0.65" />
<PackageReference Include="Microsoft.VisualStudio.Validation" Version="17.6.4-alpha" />
<PackageReference Include="Microsoft.Windows.CsWin32" Version="0.2.188-beta">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.22621.755" />
<PackageReference Include="Microsoft.WindowsAppSDK" Version="1.2.221209.1" />
<PackageReference Include="Microsoft.WindowsAppSDK" Version="1.2.230118.102" />
<PackageReference Include="StyleCop.Analyzers.Unstable" Version="1.2.0.435">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
@@ -438,4 +438,20 @@
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<ItemGroup>
<Folder Include="Control\Extension\" />
</ItemGroup>
<ItemGroup>
<Compile Update="Resource\Localization\SH.Designer.cs">
<DesignTime>True</DesignTime>
<AutoGen>True</AutoGen>
<DependentUpon>SH.resx</DependentUpon>
</Compile>
</ItemGroup>
<ItemGroup>
<EmbeddedResource Update="Resource\Localization\SH.resx">
<Generator>ResXFileCodeGenerator</Generator>
<LastGenOutput>SH.Designer.cs</LastGenOutput>
</EmbeddedResource>
</ItemGroup>
</Project>

View File

@@ -5,7 +5,7 @@ using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Navigation;
using Microsoft.Web.WebView2.Core;
using Snap.Hutao.Control;
using Snap.Hutao.Core;
using Snap.Hutao.Control.Theme;
using Snap.Hutao.Service.Navigation;
using Snap.Hutao.Web.Hoyolab.Hk4e.Common.Announcement;
using Windows.System;

View File

@@ -100,7 +100,6 @@
<wsc:Setting Content="{Binding Overview.SpiralAbyssBattleAverage}" Header="平均战斗次数"/>
</wsc:SettingsGroup>
</StackPanel>
</Flyout>
</AppBarButton.Flyout>
</AppBarButton>

View File

@@ -1,9 +1,10 @@
<Page
<shc:ScopedPage
x:Class="Snap.Hutao.View.Page.SettingPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:shc="using:Snap.Hutao.Control"
xmlns:shv="using:Snap.Hutao.ViewModel"
xmlns:wsc="using:WinUICommunity.SettingsUI.Controls"
d:DataContext="{d:DesignInstance shv:SettingViewModel}"
@@ -214,4 +215,4 @@
</StackPanel>
</Grid>
</ScrollViewer>
</Page>
</shc:ScopedPage>

View File

@@ -2,6 +2,7 @@
// Licensed under the MIT license.
using Microsoft.UI.Xaml.Navigation;
using Snap.Hutao.Control;
using Snap.Hutao.Service.Navigation;
using Snap.Hutao.ViewModel;
@@ -10,25 +11,14 @@ namespace Snap.Hutao.View.Page;
/// <summary>
/// 设置页面
/// </summary>
public sealed partial class SettingPage : Microsoft.UI.Xaml.Controls.Page
public sealed partial class SettingPage : ScopedPage
{
/// <summary>
/// 构造新的设置页面
/// </summary>
public SettingPage()
{
DataContext = Ioc.Default.GetRequiredService<SettingViewModel>();
InitializeWith<SettingViewModel>();
InitializeComponent();
}
/// <inheritdoc/>
protected override void OnNavigatedTo(NavigationEventArgs e)
{
base.OnNavigatedTo(e);
if (e.Parameter is INavigationData data)
{
data.NotifyNavigationCompleted();
}
}
}

View File

@@ -1,4 +1,4 @@
<Page
<shc:ScopedPage
x:Class="Snap.Hutao.View.Page.WikiAvatarPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
@@ -7,6 +7,7 @@
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:mxi="using:Microsoft.Xaml.Interactivity"
xmlns:mxic="using:Microsoft.Xaml.Interactions.Core"
xmlns:shc="using:Snap.Hutao.Control"
xmlns:shcb="using:Snap.Hutao.Control.Behavior"
xmlns:shci="using:Snap.Hutao.Control.Image"
xmlns:shcm="using:Snap.Hutao.Control.Markup"
@@ -602,4 +603,4 @@
</Grid>
<shvc:LoadingView IsLoading="{Binding Avatars, Converter={StaticResource EmptyObjectToBoolRevertConverter}}"/>
</Grid>
</Page>
</shc:ScopedPage>

View File

@@ -2,6 +2,7 @@
// Licensed under the MIT license.
using Microsoft.UI.Xaml.Navigation;
using Snap.Hutao.Control;
using Snap.Hutao.Service.Navigation;
using Snap.Hutao.ViewModel;
@@ -10,25 +11,14 @@ namespace Snap.Hutao.View.Page;
/// <summary>
/// 角色资料页
/// </summary>
public sealed partial class WikiAvatarPage : Microsoft.UI.Xaml.Controls.Page
public sealed partial class WikiAvatarPage : ScopedPage
{
/// <summary>
/// 构造一个新的角色资料页
/// </summary>
public WikiAvatarPage()
{
DataContext = Ioc.Default.GetRequiredService<WikiAvatarViewModel>();
InitializeWith<WikiAvatarViewModel>();
InitializeComponent();
}
/// <inheritdoc/>
protected override void OnNavigatedTo(NavigationEventArgs e)
{
base.OnNavigatedTo(e);
if (e.Parameter is INavigationData data)
{
data.NotifyNavigationCompleted();
}
}
}

View File

@@ -26,9 +26,9 @@ public sealed partial class TitleView : UserControl
public string Title
{
#if DEBUG
get => $"胡桃 Dev Build";
get => string.Format(SH.AppDevNameAndVersion, Core.CoreEnvironment.Version);
#else
get => $"胡桃 {Core.CoreEnvironment.Version}";
get => string.Format(SH.AppNameAndVersion, Core.CoreEnvironment.Version);
#endif
}

View File

@@ -8,6 +8,7 @@
xmlns:shcb="using:Snap.Hutao.Control.Behavior"
xmlns:shv="using:Snap.Hutao.ViewModel"
d:DataContext="{d:DesignInstance shv:WelcomeViewModel}"
Unloaded="OnUnloaded"
mc:Ignorable="d">
<mxi:Interaction.Behaviors>
@@ -34,6 +35,7 @@
<ItemsControl.ItemContainerTransitions>
<EntranceThemeTransition IsStaggeringEnabled="False"/>
<AddDeleteThemeTransition/>
<RepositionThemeTransition/>
</ItemsControl.ItemContainerTransitions>
<ItemsControl.ItemTemplate>
<DataTemplate>

View File

@@ -1,6 +1,7 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.Extensions.DependencyInjection;
using Microsoft.UI.Xaml.Controls;
using Snap.Hutao.ViewModel;
@@ -11,12 +12,21 @@ namespace Snap.Hutao.View;
/// </summary>
public sealed partial class WelcomeView : UserControl
{
private readonly IServiceScope serviceScope;
/// <summary>
/// <20><><EFBFBD><EFBFBD>һ<EFBFBD><D2BB><EFBFBD>µĻ<C2B5>ӭ<EFBFBD><D3AD>ͼ
/// </summary>
public WelcomeView()
{
InitializeComponent();
DataContext = Ioc.Default.GetRequiredService<WelcomeViewModel>();
serviceScope = Ioc.Default.CreateScope();
DataContext = serviceScope.ServiceProvider.GetRequiredService<WelcomeViewModel>();
}
private void OnUnloaded(object sender, Microsoft.UI.Xaml.RoutedEventArgs e)
{
DataContext = null;
serviceScope.Dispose();
}
}

View File

@@ -370,6 +370,12 @@ public class MiHoYoJSInterface
}
}
private IJsResult? LogUnhandledMessage([StringSyntax(StringSyntaxAttribute.CompositeFormat)] string message, params object?[] param)
{
logger.LogWarning(message, param);
return default;
}
private async Task<IJsResult?> TryGetJsResultFromJsParamAsync(JsParam param)
{
try
@@ -391,7 +397,7 @@ public class MiHoYoJSInterface
"login" => null,
"pushPage" => await PushPageAsync(param).ConfigureAwait(false),
"showLoading" => null,
_ => logger.LogWarning<IJsResult>("Unhandled Message Type: {method}", param.Method),
_ => LogUnhandledMessage("Unhandled Message Type: {method}", param.Method),
};
}
catch (ObjectDisposedException)

View File

@@ -116,6 +116,11 @@ internal static class HutaoEndpoints
/// </summary>
public static readonly Uri UIItemIconNone = new(StaticFile("Bg", "UI_ItemIcon_None.png"));
/// <summary>
/// UI_AvatarIcon_Side_None
/// </summary>
public static readonly Uri UIAvatarIconSideNone = new(StaticFile("AvatarIcon", "UI_AvatarIcon_Side_None.png"));
/// <summary>
/// 压缩包资源
/// </summary>