replace font

This commit is contained in:
DismissedLight
2023-01-16 14:10:28 +08:00
parent e4d2b3055c
commit 0732ea0e06
57 changed files with 714 additions and 204 deletions

View File

@@ -1,12 +1,13 @@
# [Snap.Hutao](https://hut.ao)
![](https://repository-images.githubusercontent.com/482734649/5f8cf574-2ef0-43e9-aa8d-6cf094b54dd9)
> 唷,找本堂主有何贵干呀?
![Snap.Hutao](https://repobeats.axiom.co/api/embed/f029553fbe0c60689b1710476ec8512452163fc9.svg)
# 特别感谢
### 原神组织与个人
* [HolographicHat](https://github.com/HolographicHat)
* [UIGF organization](https://uigf.org)

View File

@@ -11,6 +11,7 @@
<ResourceDictionary.MergedDictionaries>
<muxc:XamlControlsResources/>
<ResourceDictionary Source="ms-appx:///SettingsUI/Themes/Generic.xaml"/>
<ResourceDictionary Source="Control/Theme/FontStyle.xaml"/>
</ResourceDictionary.MergedDictionaries>
<ResourceDictionary.ThemeDictionaries>
@@ -27,8 +28,6 @@
<StaticResource x:Key="WindowCaptionBackgroundDisabled" ResourceKey="ControlFillColorTransparentBrush"/>
<!-- Page Transparent Background -->
<StaticResource x:Key="ApplicationPageBackgroundThemeBrush" ResourceKey="ControlFillColorTransparentBrush"/>
<!-- IconFont -->
<FontFamily x:Key="SymbolThemeFontFamily">ms-appx:///Resource/Font/Segoe Fluent Icons.ttf#Segoe Fluent Icons</FontFamily>
<!-- InfoBar Resource -->
<Thickness x:Key="InfoBarIconMargin">6,16,16,16</Thickness>
<Thickness x:Key="InfoBarContentRootPadding">16,0,0,0</Thickness>
@@ -90,6 +89,7 @@
<shvc:Int32ToVisibilityConverter x:Key="Int32ToVisibilityConverter"/>
<shvc:Int32ToVisibilityRevertConverter x:Key="Int32ToVisibilityRevertConverter"/>
<!-- Styles -->
<Style
x:Key="LargeGridViewItemStyle"
BasedOn="{StaticResource DefaultGridViewItemStyle}"

View File

@@ -3,6 +3,7 @@
using CommunityToolkit.WinUI.Notifications;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Media;
using Microsoft.Windows.AppLifecycle;
using Snap.Hutao.Core;
using Snap.Hutao.Core.ExceptionService;

View File

@@ -0,0 +1,210 @@
<!-- Copyright (c) Microsoft Corporation and Contributors. -->
<!-- Licensed under the MIT License. -->
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:wsc="using:WinUICommunity.SettingsUI.Controls">
<FontFamily x:Key="MiSans">ms-appx:///Resource/Font/MiSans-Regular.ttf#MiSans</FontFamily>
<FontFamily x:Key="CascadiaMonoAndMiSans">ms-appx:///Resource/Font/CascadiaMono.ttf#Cascadia Mono, ms-appx:///Resource/Font/MiSans-Regular.ttf#MiSans</FontFamily>
<StaticResource x:Key="PivotHeaderItemFontFamily" ResourceKey="MiSans"/>
<StaticResource x:Key="ContentControlThemeFontFamily" ResourceKey="MiSans"/>
<Style BasedOn="{StaticResource BodyTextBlockStyle}" TargetType="TextBlock"/>
<Style x:Key="BaseTextBlockStyle" TargetType="TextBlock">
<Setter Property="FontFamily" Value="{StaticResource MiSans}"/>
<Setter Property="FontSize" Value="{StaticResource BodyTextBlockFontSize}"/>
<Setter Property="FontWeight" Value="SemiBold"/>
<Setter Property="TextTrimming" Value="CharacterEllipsis"/>
<Setter Property="TextWrapping" Value="Wrap"/>
<Setter Property="LineStackingStrategy" Value="MaxHeight"/>
<Setter Property="TextLineBounds" Value="Full"/>
</Style>
<Style
x:Key="HeaderTextBlockStyle"
BasedOn="{StaticResource BaseTextBlockStyle}"
TargetType="TextBlock">
<Setter Property="FontSize" Value="46"/>
<Setter Property="FontWeight" Value="Light"/>
<Setter Property="OpticalMarginAlignment" Value="TrimSideBearings"/>
</Style>
<Style
x:Key="SubheaderTextBlockStyle"
BasedOn="{StaticResource BaseTextBlockStyle}"
TargetType="TextBlock">
<Setter Property="FontSize" Value="34"/>
<Setter Property="FontWeight" Value="Light"/>
<Setter Property="OpticalMarginAlignment" Value="TrimSideBearings"/>
</Style>
<Style
x:Key="TitleTextBlockStyle"
BasedOn="{StaticResource BaseTextBlockStyle}"
TargetType="TextBlock">
<Setter Property="FontSize" Value="{StaticResource TitleTextBlockFontSize}"/>
<Setter Property="OpticalMarginAlignment" Value="TrimSideBearings"/>
</Style>
<Style
x:Key="SubtitleTextBlockStyle"
BasedOn="{StaticResource BaseTextBlockStyle}"
TargetType="TextBlock">
<Setter Property="FontSize" Value="{StaticResource SubtitleTextBlockFontSize}"/>
<Setter Property="OpticalMarginAlignment" Value="TrimSideBearings"/>
</Style>
<Style
x:Key="BodyTextBlockStyle"
BasedOn="{StaticResource BaseTextBlockStyle}"
TargetType="TextBlock">
<Setter Property="FontWeight" Value="Normal"/>
</Style>
<Style
x:Key="CaptionTextBlockStyle"
BasedOn="{StaticResource BaseTextBlockStyle}"
TargetType="TextBlock">
<Setter Property="FontSize" Value="{StaticResource CaptionTextBlockFontSize}"/>
<Setter Property="FontWeight" Value="Normal"/>
</Style>
<Style
x:Key="BodyStrongTextBlockStyle"
BasedOn="{StaticResource BaseTextBlockStyle}"
TargetType="TextBlock">
<Setter Property="FontSize" Value="{StaticResource BodyStrongTextBlockFontSize}"/>
</Style>
<Style
x:Key="TitleLargeTextBlockStyle"
BasedOn="{StaticResource BaseTextBlockStyle}"
TargetType="TextBlock">
<Setter Property="FontSize" Value="{StaticResource TitleLargeTextBlockFontSize}"/>
<Setter Property="OpticalMarginAlignment" Value="TrimSideBearings"/>
</Style>
<Style
x:Key="DisplayTextBlockStyle"
BasedOn="{StaticResource BaseTextBlockStyle}"
TargetType="TextBlock">
<Setter Property="FontSize" Value="{StaticResource DisplayTextBlockFontSize}"/>
<Setter Property="OpticalMarginAlignment" Value="TrimSideBearings"/>
</Style>
<Style BasedOn="{StaticResource DefaultMenuFlyoutItemStyle}" TargetType="MenuFlyoutItem">
<Setter Property="FontFamily" Value="{StaticResource MiSans}"/>
</Style>
<Style BasedOn="{StaticResource DefaultMenuFlyoutSubItemStyle}" TargetType="MenuFlyoutSubItem">
<Setter Property="FontFamily" Value="{StaticResource MiSans}"/>
</Style>
<Style BasedOn="{StaticResource DefaultScrollViewerStyle}" TargetType="ScrollViewer">
<Setter Property="FontFamily" Value="{StaticResource MiSans}"/>
</Style>
<Style TargetType="InfoBar">
<Setter Property="FontFamily" Value="{StaticResource MiSans}"/>
</Style>
<Style BasedOn="{StaticResource DefaultSettingStyle}" TargetType="wsc:Setting"/>
<Style x:Key="DefaultSettingStyle" TargetType="wsc:Setting">
<Setter Property="CornerRadius" Value="{ThemeResource ControlCornerRadius}"/>
<Setter Property="Background" Value="{ThemeResource CardBackgroundBrush}"/>
<Setter Property="BorderThickness" Value="{ThemeResource CardBorderThickness}"/>
<Setter Property="BorderBrush" Value="{ThemeResource CardStrokeColorDefaultBrush}"/>
<Setter Property="IsTabStop" Value="False"/>
<Setter Property="HorizontalAlignment" Value="Stretch"/>
<Setter Property="HorizontalContentAlignment" Value="Stretch"/>
<Setter Property="Padding" Value="16"/>
<Setter Property="Margin" Value="0,0,0,0"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="wsc:Setting">
<Grid
x:Name="RootGrid"
MinHeight="48"
Padding="{TemplateBinding Padding}"
HorizontalAlignment="Stretch"
VerticalAlignment="Center"
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="{TemplateBinding CornerRadius}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<!-- Icon -->
<ColumnDefinition Width="*"/>
<!-- Header and subtitle -->
<ColumnDefinition Width="Auto"/>
<!-- Action control -->
</Grid.ColumnDefinitions>
<ContentPresenter
x:Name="IconPresenter"
MaxWidth="20"
Margin="2,0,18,0"
HorizontalAlignment="Center"
VerticalAlignment="Center"
AutomationProperties.AccessibilityView="Raw"
Content="{TemplateBinding Icon}"
FontFamily="{ThemeResource SymbolThemeFontFamily}"
FontSize="20"
Foreground="{ThemeResource CardPrimaryForegroundBrush}"
IsTextScaleFactorEnabled="False"/>
<StackPanel
Grid.Column="1"
Margin="0,0,16,0"
HorizontalAlignment="Stretch"
VerticalAlignment="Center">
<TextBlock
x:Name="HeaderPresenter"
VerticalAlignment="Center"
FontFamily="{StaticResource MiSans}"
Foreground="{ThemeResource CardPrimaryForegroundBrush}"
Text="{TemplateBinding Header}"/>
<ContentPresenter
x:Name="DescriptionPresenter"
Content="{TemplateBinding Description}"
FontFamily="{StaticResource MiSans}"
FontSize="{StaticResource SecondaryTextFontSize}"
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
TextWrapping="WrapWholeWords">
<ContentPresenter.Resources>
<Style BasedOn="{StaticResource CaptionTextBlockStyle}" TargetType="TextBlock">
<Style.Setters>
<Setter Property="TextWrapping" Value="WrapWholeWords"/>
</Style.Setters>
</Style>
<Style BasedOn="{StaticResource TextButtonStyle}" TargetType="HyperlinkButton">
<Style.Setters>
<Setter Property="FontSize" Value="12"/>
<Setter Property="Padding" Value="0,0,0,0"/>
</Style.Setters>
</Style>
</ContentPresenter.Resources>
</ContentPresenter>
</StackPanel>
<ContentPresenter
x:Name="ContentPresenter"
Grid.Column="2"
HorizontalAlignment="Right"
VerticalAlignment="Center"
Content="{TemplateBinding ActionContent}"/>
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal"/>
<VisualState x:Name="Disabled">
<VisualState.Setters>
<Setter Target="HeaderPresenter.Foreground" Value="{ThemeResource TextFillColorDisabledBrush}"/>
<Setter Target="DescriptionPresenter.Foreground" Value="{ThemeResource TextFillColorDisabledBrush}"/>
<Setter Target="IconPresenter.Foreground" Value="{ThemeResource TextFillColorDisabledBrush}"/>
<Setter Target="ContentPresenter.Foreground" Value="{ThemeResource TextFillColorDisabledBrush}"/>
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>

View File

@@ -3,6 +3,7 @@
using Snap.Hutao.Core.DependencyInjection.Annotation.HttpClient;
using Snap.Hutao.Core.Logging;
using System.Collections.Concurrent;
using System.Collections.Immutable;
using System.IO;
using System.Net;
@@ -24,7 +25,7 @@ public class ImageCache : IImageCache, IImageCacheFilePathOperation
{
private const string CacheFolderName = nameof(ImageCache);
private static readonly ImmutableDictionary<int, TimeSpan> RetryCountToDelay = new Dictionary<int, TimeSpan>()
private static readonly Dictionary<int, TimeSpan> RetryCountToDelay = new Dictionary<int, TimeSpan>()
{
[0] = TimeSpan.FromSeconds(4),
[1] = TimeSpan.FromSeconds(16),
@@ -32,13 +33,13 @@ public class ImageCache : IImageCache, IImageCacheFilePathOperation
[3] = TimeSpan.FromSeconds(4),
[4] = TimeSpan.FromSeconds(16),
[5] = TimeSpan.FromSeconds(64),
}.ToImmutableDictionary();
};
private readonly ILogger logger;
// violate di rule
private readonly HttpClient httpClient;
private readonly ConcurrentDictionary<string, Task> concurrentTasks = new();
private string? baseFolder;
private string? cacheFolder;
@@ -100,11 +101,30 @@ public class ImageCache : IImageCache, IImageCacheFilePathOperation
/// <inheritdoc/>
public async Task<string> GetFileFromCacheAsync(Uri uri)
{
string filePath = Path.Combine(GetCacheFolder(), GetCacheFileName(uri));
string fileName = GetCacheFileName(uri);
string filePath = Path.Combine(GetCacheFolder(), fileName);
if (!File.Exists(filePath) || new FileInfo(filePath).Length == 0)
{
await DownloadFileAsync(uri, filePath).ConfigureAwait(false);
TaskCompletionSource taskCompletionSource = new();
try
{
if (concurrentTasks.TryAdd(fileName, taskCompletionSource.Task))
{
await DownloadFileAsync(uri, filePath).ConfigureAwait(false);
}
else
{
if (concurrentTasks.TryGetValue(fileName, out Task? task))
{
await task.ConfigureAwait(false);
}
}
}
finally
{
taskCompletionSource.TrySetResult();
}
}
return filePath;
@@ -191,7 +211,7 @@ public class ImageCache : IImageCache, IImageCacheFilePathOperation
if (retryCount == 3)
{
uri = new UriBuilder(uri) { Host = Web.HutaoEndpoints.StaticHutao, }.Uri;
uri = new UriBuilder(uri) { Host = Web.HutaoEndpoints.StaticHutao }.Uri;
}
}
}

View File

@@ -10,7 +10,8 @@ namespace Snap.Hutao.Core.DependencyInjection;
/// </summary>
public static class ServiceScopeExtension
{
private static IServiceScope? scopeReference;
// Allow GC to Collect the IServiceScope
private static readonly WeakReference<IServiceScope> ScopeReference = new(null!);
/// <summary>
/// 追踪服务范围
@@ -19,7 +20,7 @@ public static class ServiceScopeExtension
public static void Track(this IServiceScope scope)
{
DisposeLast();
scopeReference = scope;
ScopeReference.SetTarget(scope);
}
/// <summary>
@@ -27,6 +28,9 @@ public static class ServiceScopeExtension
/// </summary>
public static void DisposeLast()
{
scopeReference?.Dispose();
if (ScopeReference.TryGetTarget(out IServiceScope? scope))
{
scope.Dispose();
}
}
}
}

View File

@@ -45,4 +45,4 @@ internal class ExceptionRecorder
{
logger.LogCritical(EventIds.XamlBindingError, "XAML绑定失败: {message}", e.Message);
}
}
}

View File

@@ -0,0 +1,20 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Core.ExceptionService;
/// <summary>
/// 用户数据损坏异常
/// </summary>
internal class UserdataCorruptedException : Exception
{
/// <summary>
/// 构造一个新的用户数据损坏异常
/// </summary>
/// <param name="message">消息</param>
/// <param name="innerException">内部错误</param>
public UserdataCorruptedException(string message, Exception innerException)
: base($"用户数据已损坏: {message}", innerException)
{
}
}

View File

@@ -2,6 +2,7 @@
// Licensed under the MIT license.
using Microsoft.Win32.TaskScheduler;
using System.IO;
using System.Runtime.InteropServices;
using SchedulerTask = Microsoft.Win32.TaskScheduler.Task;
@@ -23,6 +24,7 @@ internal static class ScheduleTaskHelper
{
try
{
// TODO: 似乎可以不删除任务,直接注册已经包含了更新功能
SchedulerTask? targetTask = TaskService.Instance.GetTask(DailyNoteRefreshTaskName);
if (targetTask != null)
{
@@ -36,12 +38,9 @@ internal static class ScheduleTaskHelper
TaskService.Instance.RootFolder.RegisterTaskDefinition(DailyNoteRefreshTaskName, task);
return true;
}
catch (UnauthorizedAccessException)
{
return false;
}
catch (COMException)
catch (Exception ex)
{
_ = ex;
return false;
}
}

View File

@@ -32,4 +32,9 @@ internal static class SettingKeys
/// 静态资源合约V2 成就图标与物品图标
/// </summary>
public const string StaticResourceV2Contract = "StaticResourceV2Contract";
/// <summary>
/// 静态资源合约V3 刷新 Skill Talent
/// </summary>
public const string StaticResourceV3Contract = "StaticResourceV3Contract";
}

View File

@@ -39,6 +39,13 @@ internal static class ThreadHelper
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void InvokeOnMainThread(Action action)
{
Program.DispatcherQueue!.Invoke(action);
if (Program.DispatcherQueue!.HasThreadAccess)
{
action();
}
else
{
Program.DispatcherQueue.Invoke(action);
}
}
}

View File

@@ -9,7 +9,7 @@ namespace Snap.Hutao.Model.Intrinsic;
/// <summary>
/// 不可变的原生枚举
/// </summary>
public static class ImmutableIntrinsics
public static class IntrinsicImmutables
{
/// <summary>
/// 所属地区

View File

@@ -12,7 +12,7 @@
<Identity
Name="7f0db578-026f-4e0b-a75b-d5d06bb0a74d"
Publisher="CN=DGP Studio"
Version="1.3.9.0" />
Version="1.3.10.0" />
<Properties>
<DisplayName>胡桃</DisplayName>

View File

@@ -5,6 +5,7 @@ using CommunityToolkit.Mvvm.Messaging;
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.Entity.Database;
@@ -114,7 +115,17 @@ internal class AchievementService : IAchievementService
List<BindingAchievement> results = new();
foreach (MetadataAchievement meta in metadata)
{
EntityAchievement entity = entities.SingleOrDefault(e => e.Id == meta.Id) ?? EntityAchievement.Create(archiveId, meta.Id);
EntityAchievement? entity;
try
{
entity = entities.SingleOrDefault(e => e.Id == meta.Id);
}
catch (InvalidOperationException ex)
{
throw new UserdataCorruptedException("单个成就存档内发现多个相同的成就 Id", ex);
}
entity ??= EntityAchievement.Create(archiveId, meta.Id);
results.Add(new(meta, entity));
}

View File

@@ -22,7 +22,6 @@ namespace Snap.Hutao.Service.AvatarInfo;
[Injection(InjectAs.Scoped, typeof(IAvatarInfoService))]
internal class AvatarInfoService : IAvatarInfoService
{
private readonly AppDbContext appDbContext;
private readonly ISummaryFactory summaryFactory;
private readonly IMetadataService metadataService;
private readonly ILogger<AvatarInfoService> logger;
@@ -42,7 +41,6 @@ internal class AvatarInfoService : IAvatarInfoService
ISummaryFactory summaryFactory,
ILogger<AvatarInfoService> logger)
{
this.appDbContext = appDbContext;
this.metadataService = metadataService;
this.summaryFactory = summaryFactory;
this.logger = logger;

View File

@@ -43,7 +43,7 @@ internal class SummaryAvatarFactory
ReliquaryAndWeapon reliquaryAndWeapon = ProcessEquip(avatarInfo.EquipList.EmptyIfNull());
MetadataAvatar avatar = metadataContext.IdAvatarMap[avatarInfo.AvatarId];
return new()
PropertyAvatar propertyAvatar = new()
{
Id = avatar.Id,
Name = avatar.Name,
@@ -52,8 +52,6 @@ internal class SummaryAvatarFactory
NameCard = AvatarNameCardPicConverter.AvatarToUri(avatar),
Quality = avatar.Quality,
Element = ElementNameIconConverter.ElementNameToElementType(avatar.FetterInfo.VisionBefore),
Level = $"Lv.{avatarInfo.PropMap[PlayerProperty.PROP_LEVEL].Value}",
LevelNumber = int.Parse(avatarInfo.PropMap[PlayerProperty.PROP_LEVEL].Value ?? string.Empty),
FetterLevel = avatarInfo.FetterInfo.ExpLevel,
Weapon = reliquaryAndWeapon.Weapon,
Reliquaries = reliquaryAndWeapon.Reliquaries,
@@ -62,7 +60,11 @@ internal class SummaryAvatarFactory
Properties = SummaryHelper.CreateAvatarProperties(avatarInfo.FightPropMap),
Score = reliquaryAndWeapon.Reliquaries.Sum(r => r.Score).ToString("F2"),
CritScore = $"{SummaryHelper.ScoreCrit(avatarInfo.FightPropMap):F2}",
LevelNumber = int.Parse(avatarInfo.PropMap?[PlayerProperty.PROP_LEVEL].Value ?? string.Empty),
};
propertyAvatar.Level = $"Lv.{propertyAvatar.LevelNumber}";
return propertyAvatar;
}
private ReliquaryAndWeapon ProcessEquip(List<Equip> equipments)
@@ -85,7 +87,7 @@ internal class SummaryAvatarFactory
}
}
return new(reliquaryList, weapon!);
return new(reliquaryList, weapon);
}
private PropertyWeapon CreateWeapon(Equip equip)

View File

@@ -47,6 +47,7 @@ internal class DailyNoteService : IDailyNoteService, IRecipient<UserRemovedMessa
{
ThreadHelper.InvokeOnMainThread(() =>
{
// Database items have been deleted by cascade deleting.
entries?.RemoveWhere(n => n.UserId == message.RemovedUserId);
});
}
@@ -151,6 +152,7 @@ internal class DailyNoteService : IDailyNoteService, IRecipient<UserRemovedMessa
using (IServiceScope scope = scopeFactory.CreateScope())
{
// DbUpdateConcurrencyException: The database operation was expected to affect 1 row(s), but actually affected 0 row(s)
scope.ServiceProvider.GetRequiredService<AppDbContext>().DailyNotes.RemoveAndSave(entry);
}
}

View File

@@ -2,6 +2,7 @@
// Licensed under the MIT license.
using CommunityToolkit.Mvvm.Messaging;
using Microsoft.Data.Sqlite;
using Microsoft.EntityFrameworkCore;
using Snap.Hutao.Core.Database;
using Snap.Hutao.Core.Diagnostics;
@@ -296,6 +297,7 @@ internal class GachaLogService : IGachaLogService
break;
}
token.ThrowIfCancellationRequested();
SaveGachaItems(itemsToAdd, isLazy, archive, configration.EndId);
await RandomDelayAsync(token).ConfigureAwait(false);
}
@@ -327,14 +329,21 @@ internal class GachaLogService : IGachaLogService
if (archive != null)
{
// TODO: replace with MaxBy
// https://github.com/dotnet/efcore/issues/25566
// .MaxBy(i => i.Id);
item = appDbContext.GachaItems
.Where(i => i.ArchiveId == archive.InnerId)
.Where(i => i.QueryType == configType)
.OrderByDescending(i => i.Id)
.FirstOrDefault();
try
{
// TODO: replace with MaxBy
// https://github.com/dotnet/efcore/issues/25566
// .MaxBy(i => i.Id);
item = appDbContext.GachaItems
.Where(i => i.ArchiveId == archive.InnerId)
.Where(i => i.QueryType == configType)
.OrderByDescending(i => i.Id)
.FirstOrDefault();
}
catch (SqliteException ex)
{
throw new Core.ExceptionService.UserdataCorruptedException("无法获取祈愿记录 End Id", ex);
}
}
return item?.Id ?? 0L;
@@ -369,11 +378,12 @@ internal class GachaLogService : IGachaLogService
private INameQuality GetNameQualityByItemId(int id)
{
return id.Place() switch
int place = id.Place();
return place switch
{
8 => idAvatarMap![id],
5 => idWeaponMap![id],
_ => throw Must.NeverHappen(),
_ => throw Must.NeverHappen($"Id places: {place}"),
};
}

View File

@@ -143,6 +143,11 @@ internal class GameService : IGameService, IDisposable
string gamePath = GetGamePathSkipLocator();
string configPath = Path.Combine(Path.GetDirectoryName(gamePath) ?? string.Empty, ConfigFile);
if (!File.Exists(configPath))
{
return new(null, null, configPath);
}
using (FileStream stream = File.OpenRead(configPath))
{
List<IniElement> elements = IniSerializer.Deserialize(stream).ToList();

View File

@@ -18,14 +18,21 @@ public struct MultiChannel
/// </summary>
public string SubChannel;
/// <summary>
/// 配置文件路径 当不为 null 时则存在文件读写问题
/// </summary>
public string? ConfigFilePath;
/// <summary>
/// 构造一个新的多通道
/// </summary>
/// <param name="channel">通道</param>
/// <param name="subChannel">子通道</param>
public MultiChannel(string? channel, string? subChannel)
/// <param name="configFilePath">配置文件路径</param>
public MultiChannel(string? channel, string? subChannel, string? configFilePath = null)
{
Channel = channel ?? string.Empty;
SubChannel = subChannel ?? string.Empty;
ConfigFilePath = configFilePath;
}
}

View File

@@ -6,6 +6,7 @@ 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;
@@ -75,21 +76,24 @@ internal class GameFpsUnlocker : IGameFpsUnlocker
private static unsafe MODULEENTRY32 FindModule(int processId, string moduleName)
{
using (SafeFileHandle snapshot = CreateToolhelp32Snapshot_SafeHandle(CREATE_TOOLHELP_SNAPSHOT_FLAGS.TH32CS_SNAPMODULE, (uint)processId))
HANDLE snapshot = CreateToolhelp32Snapshot(CREATE_TOOLHELP_SNAPSHOT_FLAGS.TH32CS_SNAPMODULE, (uint)processId);
using (snapshot.AutoClose())
{
Marshal.ThrowExceptionForHR(Marshal.GetLastPInvokeError());
MODULEENTRY32 entry = StructMarshal.MODULEENTRY32();
bool found = false;
// First module must be exe. Ignoring it.
for (Module32First(snapshot, ref entry); Module32Next(snapshot, ref entry);)
bool loop = Module32First(snapshot, &entry);
while (loop)
{
if (entry.th32ProcessID == processId && entry.szModule.AsString() == moduleName)
{
found = true;
break;
}
loop = Module32Next(snapshot, &entry);
}
return found ? entry : default;

View File

@@ -181,7 +181,7 @@ internal class NavigationService : INavigationService
/// <inheritdoc/>
public void GoBack()
{
Program.DispatcherQueue!.TryEnqueue(() =>
ThreadHelper.InvokeOnMainThread(() =>
{
bool canGoBack = Frame?.CanGoBack ?? false;

View File

@@ -124,7 +124,14 @@ internal class UserService : IUserService
}
userCollection = users.ToObservableCollection();
Current = users.SingleOrDefault(user => user.IsSelected);
try
{
Current = users.SingleOrDefault(user => user.IsSelected);
}
catch (InvalidOperationException ex)
{
throw new Core.ExceptionService.UserdataCorruptedException("无法设置当前用户", ex);
}
}
return userCollection;
@@ -157,8 +164,16 @@ internal class UserService : IUserService
{
if (userCollection != null)
{
// TODO: optimize match speed.
return userCollection.SelectMany(u => u.UserGameRoles).SingleOrDefault(r => r.GameUid == uid);
try
{
// TODO: optimize match speed.
return userCollection.SelectMany(u => u.UserGameRoles).SingleOrDefault(r => r.GameUid == uid);
}
catch (InvalidOperationException)
{
// Sequence contains more than one matching element
// TODO: return a specialize UserGameRole to indicate error
}
}
return null;

View File

@@ -37,9 +37,12 @@
<ItemGroup>
<None Remove="Control\Panel\PanelSelector.xaml" />
<None Remove="Control\Theme\FontStyle.xaml" />
<None Remove="LaunchGameWindow.xaml" />
<None Remove="NativeMethods.json" />
<None Remove="NativeMethods.txt" />
<None Remove="Resource\Font\CascadiaMono.ttf" />
<None Remove="Resource\Font\MiSans-Regular.ttf" />
<None Remove="Resource\Icon\UI_AchievementIcon_3_3.png" />
<None Remove="Resource\Icon\UI_BagTabIcon_Avatar.png" />
<None Remove="Resource\Icon\UI_BagTabIcon_Weapon.png" />
@@ -122,7 +125,8 @@
<Content Include="Assets\Square44x44Logo.targetsize-24_altform-unplated.png" />
<Content Include="Assets\StoreLogo.png" />
<Content Include="Assets\Wide310x150Logo.scale-200.png" />
<Content Include="Resource\Font\Segoe Fluent Icons.ttf" />
<Content Include="Resource\Font\CascadiaMono.ttf" />
<Content Include="Resource\Font\MiSans-Regular.ttf" />
<Content Include="Resource\Icon\UI_AchievementIcon_3_3.png" />
<Content Include="Resource\Icon\UI_BagTabIcon_Avatar.png" />
<Content Include="Resource\Icon\UI_BagTabIcon_Weapon.png" />
@@ -154,8 +158,8 @@
<PackageReference Include="CommunityToolkit.WinUI.Notifications" Version="7.1.2" />
<PackageReference Include="CommunityToolkit.WinUI.UI.Behaviors" Version="7.1.2" />
<PackageReference Include="CommunityToolkit.WinUI.UI.Controls" Version="7.1.2" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="7.0.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="7.0.1">
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="7.0.2" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="7.0.2">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
@@ -171,14 +175,14 @@
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.25267-preview" />
<PackageReference Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.25276-preview" />
<PackageReference Include="Microsoft.WindowsAppSDK" Version="1.2.221209.1" />
<PackageReference Include="StyleCop.Analyzers.Unstable" Version="1.2.0.435">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="TaskScheduler" Version="2.10.1" />
<PackageReference Include="WinUICommunity.SettingsUI" Version="3.0.0" />
<PackageReference Include="WinUICommunity.SettingsUI" Version="3.0.1" />
<Manifest Include="$(ApplicationManifest)" />
</ItemGroup>
@@ -415,4 +419,9 @@
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<ItemGroup>
<Page Update="Control\Style\FontStyle.xaml">
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
</Project>

View File

@@ -64,25 +64,18 @@
<DataTemplate x:Key="OrangeGridTemplate" x:DataType="shmbg:SummaryItem">
<Grid Width="40" Margin="0,4,4,0">
<Border
Background="{StaticResource CardBackgroundFillColorDefault}"
BorderBrush="{StaticResource CardStrokeColorDefault}"
BorderThickness="1"
CornerRadius="{StaticResource CompatCornerRadius}"
ToolTipService.ToolTip="{Binding TimeFormatted}">
<Border Style="{StaticResource BorderCardStyle}" ToolTipService.ToolTip="{Binding TimeFormatted}">
<StackPanel>
<shvc:ItemIcon
Width="40"
Height="40"
Icon="{Binding Icon}"
Quality="QUALITY_ORANGE"/>
<!--<shci:CachedImage
Source="{Binding Icon}"
Height="40" Width="40"/>-->
<TextBlock
HorizontalAlignment="Center"
HorizontalTextAlignment="Center"
Style="{StaticResource CaptionTextBlockStyle}"
Text="{Binding LastPull}"
TextTrimming="None"
TextWrapping="NoWrap">
<TextBlock.Foreground>
<SolidColorBrush Color="{Binding Color}"/>
@@ -94,7 +87,7 @@
</DataTemplate>
</UserControl.Resources>
<Border Background="{StaticResource CardBackgroundFillColorDefaultBrush}" CornerRadius="{StaticResource CompatCornerRadius}">
<Border Style="{StaticResource BorderCardStyle}">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="auto"/>
@@ -108,19 +101,23 @@
HorizontalContentAlignment="Stretch"
Background="Transparent"
BorderBrush="{x:Null}"
BorderThickness="0"
CornerRadius="4,4,0,0"
IsExpanded="True">
<Expander.Resources>
<Thickness x:Key="ExpanderHeaderBorderThickness">0,0,0,1</Thickness>
</Expander.Resources>
<Expander.Header>
<Grid Grid.Row="0">
<Grid>
<TextBlock
VerticalAlignment="Center"
Style="{StaticResource SubtitleTextBlockStyle}"
Text="{Binding Name}"/>
<StackPanel HorizontalAlignment="Right" Orientation="Horizontal">
<TextBlock
Margin="0,4,12,2"
VerticalAlignment="Bottom"
FontFamily="Consolas"
FontSize="24"
Margin="0,0,6,0"
VerticalAlignment="Center"
FontSize="20"
Text="{Binding TotalCount}"
Visibility="{Binding ElementName=DetailExpander, Path=IsExpanded, Converter={StaticResource BoolToVisibilityRevertConverter}}"/>
<shcp:PanelSelector
@@ -135,7 +132,7 @@
<StackPanel Orientation="Horizontal">
<TextBlock
Margin="0,4,0,4"
FontFamily="Consolas"
FontFamily="{StaticResource CascadiaMonoAndMiSans}"
FontSize="48"
Text="{Binding TotalCount}"/>
<TextBlock
@@ -149,12 +146,7 @@
<ColumnDefinition/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<Border
Grid.Column="0"
Background="{StaticResource CardBackgroundFillColorDefault}"
BorderBrush="{StaticResource CardStrokeColorDefault}"
BorderThickness="1"
CornerRadius="{StaticResource CompatCornerRadius}">
<Border Grid.Column="0" Style="{StaticResource BorderCardStyle}">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="auto"/>
@@ -165,14 +157,13 @@
Width="40"
Height="40"
Margin="4"
Background="{StaticResource CardBackgroundFillColorDefaultBrush}"
Background="{ThemeResource CardBackgroundFillColorDefaultBrush}"
Foreground="{StaticResource OrangeBrush}"
IsIndeterminate="False"
Maximum="{Binding GuarenteeOrangeThreshold}"
Value="{Binding LastOrangePull}"/>
<TextBlock
Grid.Column="0"
Margin="0,0,0,2"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Foreground="{StaticResource OrangeBrush}"
@@ -180,7 +171,6 @@
Text="{Binding LastOrangePull}"/>
<TextBlock
Grid.Column="1"
Margin="0,0,0,2"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Foreground="{StaticResource OrangeBrush}"
@@ -188,12 +178,7 @@
Text="五星"/>
</Grid>
</Border>
<Border
Grid.Column="1"
Background="{StaticResource CardBackgroundFillColorDefault}"
BorderBrush="{StaticResource CardStrokeColorDefault}"
BorderThickness="1"
CornerRadius="{StaticResource CompatCornerRadius}">
<Border Grid.Column="1" Style="{StaticResource BorderCardStyle}">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="auto"/>
@@ -204,14 +189,13 @@
Width="40"
Height="40"
Margin="4"
Background="{StaticResource CardBackgroundFillColorDefaultBrush}"
Background="{ThemeResource CardBackgroundFillColorDefaultBrush}"
Foreground="{StaticResource PurpleBrush}"
IsIndeterminate="False"
Maximum="{Binding GuarenteePurpleThreshold}"
Value="{Binding LastPurplePull}"/>
<TextBlock
Grid.Column="0"
Margin="0,0,0,2"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Foreground="{StaticResource PurpleBrush}"
@@ -219,7 +203,6 @@
Text="{Binding LastPurplePull}"/>
<TextBlock
Grid.Column="1"
Margin="0,0,0,2"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Foreground="{StaticResource PurpleBrush}"
@@ -232,20 +215,17 @@
<StackPanel Margin="0,8,0,0" Orientation="Horizontal">
<TextBlock
HorizontalAlignment="Left"
FontFamily="Consolas"
Opacity="0.6"
Style="{StaticResource CaptionTextBlockStyle}"
Text="{Binding FromFormatted}"/>
<TextBlock
HorizontalAlignment="Left"
VerticalAlignment="Center"
FontFamily="Consolas"
Opacity="0.6"
Style="{StaticResource CaptionTextBlockStyle}"
Text="-"/>
<TextBlock
HorizontalAlignment="Left"
FontFamily="Consolas"
Opacity="0.6"
Style="{StaticResource CaptionTextBlockStyle}"
Text="{Binding ToFormatted}"/>
@@ -260,7 +240,7 @@
Text="五星"/>
<TextBlock
HorizontalAlignment="Right"
FontFamily="Consolas"
FontFamily="{StaticResource CascadiaMonoAndMiSans}"
Foreground="{StaticResource OrangeBrush}"
Style="{StaticResource BodyTextBlockStyle}"
Text="{Binding TotalOrangeFormatted}"/>
@@ -272,7 +252,7 @@
Text="四星"/>
<TextBlock
HorizontalAlignment="Right"
FontFamily="Consolas"
FontFamily="{StaticResource CascadiaMonoAndMiSans}"
Foreground="{StaticResource PurpleBrush}"
Style="{StaticResource BodyTextBlockStyle}"
Text="{Binding TotalPurpleFormatted}"/>
@@ -284,7 +264,7 @@
Text="三星"/>
<TextBlock
HorizontalAlignment="Right"
FontFamily="Consolas"
FontFamily="{StaticResource CascadiaMonoAndMiSans}"
Foreground="{StaticResource BlueBrush}"
Style="{StaticResource BodyTextBlockStyle}"
Text="{Binding TotalBlueFormatted}"/>
@@ -294,7 +274,6 @@
<TextBlock Style="{StaticResource BodyTextBlockStyle}" Text="五星平均抽数"/>
<TextBlock
HorizontalAlignment="Right"
FontFamily="Consolas,MicroSoft YaHei UI"
Style="{StaticResource BodyTextBlockStyle}"
Text="{Binding AverageOrangePullFormatted}"/>
</Grid>
@@ -302,19 +281,16 @@
<TextBlock Style="{StaticResource BodyTextBlockStyle}" Text="UP 平均抽数"/>
<TextBlock
HorizontalAlignment="Right"
FontFamily="Consolas,MicroSoft YaHei UI"
Style="{StaticResource BodyTextBlockStyle}"
Text="{Binding AverageUpOrangePullFormatted}"/>
</Grid>
<Grid Margin="0,2,0,0">
<TextBlock
HorizontalAlignment="Left"
FontFamily="Consolas,MicroSoft YaHei UI"
Style="{StaticResource BodyTextBlockStyle}"
Text="{Binding MaxOrangePullFormatted}"/>
<TextBlock
HorizontalAlignment="Right"
FontFamily="Consolas,MicroSoft YaHei UI"
Style="{StaticResource BodyTextBlockStyle}"
Text="{Binding MinOrangePullFormatted}"/>
</Grid>

View File

@@ -4,17 +4,18 @@
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"
Title="米游社养成计算"
Closed="OnContentDialogClosed"
DefaultButton="Primary"
PrimaryButtonText="完成"
Style="{StaticResource DefaultContentDialogStyle}"
mc:Ignorable="d">
<ContentControl.Resources>
<Thickness x:Key="ContentDialogPadding">0</Thickness>
<CornerRadius x:Key="OverlayCornerRadius">0</CornerRadius>
</ContentControl.Resources>
<Grid Loaded="OnGridLoaded">
<WebView2
Name="WebView"
Width="380"
Height="500"/>
Height="500"
AllowFocusOnInteraction="False"/>
</Grid>
</ContentDialog>

View File

@@ -8,15 +8,17 @@
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
Closed="OnContentDialogClosed"
DefaultButton="Primary"
PrimaryButtonText="完成"
Style="{StaticResource DefaultContentDialogStyle}"
mc:Ignorable="d">
<ContentControl.Resources>
<Thickness x:Key="ContentDialogPadding">0</Thickness>
<CornerRadius x:Key="OverlayCornerRadius">0</CornerRadius>
</ContentControl.Resources>
<Grid Loaded="OnGridLoaded">
<WebView2
Name="WebView"
Width="360"
Height="580"/>
Height="580"
AllowFocusOnInteraction="True"/>
</Grid>
</ContentDialog>

View File

@@ -4,17 +4,19 @@
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"
Title="米游社实时便笺"
Closed="OnContentDialogClosed"
DefaultButton="Primary"
PrimaryButtonText="完成"
Style="{StaticResource DefaultContentDialogStyle}"
mc:Ignorable="d">
<ContentControl.Resources>
<Thickness x:Key="ContentDialogPadding">0</Thickness>
<CornerRadius x:Key="OverlayCornerRadius">0</CornerRadius>
</ContentControl.Resources>
<Grid Loaded="OnGridLoaded">
<WebView2
Name="WebView"
Width="380"
Height="448"/>
Height="448"
AllowFocusOnInteraction="False"
DefaultBackgroundColor="Transparent"/>
</Grid>
</ContentDialog>
</ContentDialog>

View File

@@ -1,4 +1,4 @@
// Copyright (c) DGP Studio. All rights reserved.
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.Extensions.DependencyInjection;
@@ -11,20 +11,19 @@ using Snap.Hutao.Web.Bridge;
namespace Snap.Hutao.View.Dialog;
/// <summary>
/// ʵʱ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>֤<EFBFBD>Ի<EFBFBD><EFBFBD><EFBFBD>
/// 实时便笺验证对话框
/// </summary>
public sealed partial class DailyNoteVerificationDialog : ContentDialog
{
private readonly IServiceScope scope;
private readonly UserAndUid userAndUid;
[SuppressMessage("", "IDE0052")]
private DailyNoteJsInterface? dailyNoteJsInterface;
/// <summary>
/// <EFBFBD><EFBFBD><EFBFBD><EFBFBD>һ<EFBFBD><EFBFBD><EFBFBD>µ<EFBFBD>ʵʱ<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>֤<EFBFBD>Ի<EFBFBD><EFBFBD><EFBFBD>
/// 构造一个新的实时便笺验证对话框
/// </summary>
/// <param name="userAndUid"><EFBFBD>û<EFBFBD><EFBFBD><EFBFBD><EFBFBD>ɫ</param>
/// <param name="userAndUid">用户与角色</param>
public DailyNoteVerificationDialog(UserAndUid userAndUid)
{
InitializeComponent();
@@ -46,14 +45,21 @@ public sealed partial class DailyNoteVerificationDialog : ContentDialog
Model.Entity.User user = userAndUid.User;
coreWebView2.SetCookie(user.CookieToken, user.Ltoken, null).SetMobileUserAgent();
dailyNoteJsInterface = new(coreWebView2, scope.ServiceProvider);
dailyNoteJsInterface.ClosePageRequested += OnClosePageRequested;
string query = $"?role_id={userAndUid.Uid.Value}&server={userAndUid.Uid.Region}";
coreWebView2.Navigate($"https://webstatic.mihoyo.com/app/community-game-records/index.html?bbs_presentation_style=fullscreen#/ys/daily/{query}");
}
private void OnClosePageRequested()
{
ThreadHelper.InvokeOnMainThread(Hide);
}
private void OnContentDialogClosed(ContentDialog sender, ContentDialogClosedEventArgs args)
{
dailyNoteJsInterface.ClosePageRequested -= OnClosePageRequested;
dailyNoteJsInterface = null;
scope.Dispose();
}
}
}

View File

@@ -0,0 +1,31 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.Web.WebView2.Core;
using System.Diagnostics;
namespace Snap.Hutao.View.Extension;
/// <summary>
/// <see cref="CoreWebView2Environment"/> 扩展
/// </summary>
internal static class CoreWebView2EnvironmentExtension
{
/// <summary>
/// 退出
/// </summary>
/// <param name="environment">环境</param>
public static void Exit(this CoreWebView2Environment environment)
{
IReadOnlyList<CoreWebView2ProcessInfo> processInfos = environment.GetProcessInfos();
foreach (CoreWebView2ProcessInfo processInfo in processInfos)
{
Process p = Process.GetProcessById(processInfo.ProcessId);
if (p.ProcessName == "msedgewebview2.exe")
{
p.Kill();
}
}
}
}

View File

@@ -160,6 +160,9 @@
Margin="16,0,0,16"
ItemsPanel="{StaticResource ItemsStackPanelTemplate}"
ItemsSource="{Binding Achievements}">
<ItemsControl.ItemContainerTransitions>
<ContentThemeTransition/>
</ItemsControl.ItemContainerTransitions>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Grid

View File

@@ -6,6 +6,8 @@ using Microsoft.UI.Xaml.Navigation;
using Microsoft.Web.WebView2.Core;
using Snap.Hutao.Core;
using Snap.Hutao.Service.Navigation;
using Snap.Hutao.View.Extension;
using System.Diagnostics;
using Windows.System;
namespace Snap.Hutao.View.Page;
@@ -34,10 +36,12 @@ public sealed partial class AnnouncementContentPage : Microsoft.UI.Xaml.Controls
private const string DarkAccentColor2 = "background-color: rgb(254, 245, 231);";
// support click open browser.
private const string MihoyoSDKDefinition =
@"window.miHoYoGameJSSDK = {
openInBrowser: function(url){ window.chrome.webview.postMessage(url); },
openInWebview: function(url){ location.href = url }}";
private const string MihoyoSDKDefinition = """
window.miHoYoGameJSSDK = {
openInBrowser: function(url){ window.chrome.webview.postMessage(url); },
openInWebview: function(url){ location.href = url }
}
""";
private string? targetContent;
@@ -61,6 +65,13 @@ openInWebview: function(url){ location.href = url }}";
}
}
/// <inheritdoc/>
protected override void OnNavigatedFrom(NavigationEventArgs e)
{
base.OnNavigatedFrom(e);
//WebView.CoreWebView2.Environment.Exit();
}
private static string? ReplaceForeground(string? rawContent, ElementTheme theme)
{
if (string.IsNullOrWhiteSpace(rawContent))

View File

@@ -149,7 +149,7 @@
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<Pivot>
<Pivot Style="{StaticResource DefaultPivotStyle}">
<PivotItem
Content="{Binding Announcement.List[0]}"
ContentTemplate="{StaticResource AnnouncementTemplate}"

View File

@@ -203,8 +203,12 @@
Margin="4,0,0,2"
VerticalAlignment="Center"
Foreground="#FFFFFFFF"
HorizontalTextAlignment="Center"
Style="{StaticResource CaptionTextBlockStyle}"
Text="{Binding SelectedAvatar.FetterLevel}"/>
Text="{Binding SelectedAvatar.FetterLevel}"
TextAlignment="Center"
TextTrimming="None"
TextWrapping="NoWrap"/>
</StackPanel>
</StackPanel>
</Grid>

View File

@@ -481,7 +481,8 @@
<wsc:Setting
Description="使用当前账号的Cookie信息刷新祈愿记录"
Header="Stoken 刷新"
Icon="{shcm:FontIcon Glyph=&#xE192;}">
Icon="{shcm:FontIcon Glyph=&#xE192;}"
Style="{StaticResource DefaultSettingStyle}">
<Button Command="{Binding RefreshByStokenCommand}" Content="获取"/>
</wsc:Setting>
<wsc:Setting

View File

@@ -468,7 +468,16 @@ internal class AchievementViewModel : Abstraction.ViewModel, INavigationRecipien
private async Task UpdateAchievementsAsync(Model.Entity.AchievementArchive archive)
{
List<Model.Metadata.Achievement.Achievement> rawAchievements = await metadataService.GetAchievementsAsync(CancellationToken).ConfigureAwait(false);
List<Model.Binding.Achievement.Achievement> combined = achievementService.GetAchievements(archive, rawAchievements);
List<Model.Binding.Achievement.Achievement> combined;
try
{
combined = achievementService.GetAchievements(archive, rawAchievements);
}
catch (Core.ExceptionService.UserdataCorruptedException ex)
{
infoBarService.Error(ex);
return;
}
// Assemble achievements on the UI thread.
await ThreadHelper.SwitchToMainThreadAsync();

View File

@@ -88,9 +88,15 @@ internal class DailyNoteViewModel : Abstraction.ViewModel
{
if (value != null)
{
refreshSecondsEntry!.SetInt32(value.Value);
appDbContext.Settings.UpdateAndSave(refreshSecondsEntry!);
ScheduleTaskHelper.RegisterForDailyNoteRefresh(value.Value);
if (!ScheduleTaskHelper.RegisterForDailyNoteRefresh(value.Value))
{
Ioc.Default.GetRequiredService<IInfoBarService>().Warning("注册计划任务失败,请使用管理员模式重试");
}
else
{
refreshSecondsEntry!.SetInt32(value.Value);
appDbContext.Settings.UpdateAndSave(refreshSecondsEntry!);
}
}
}
}
@@ -170,24 +176,38 @@ internal class DailyNoteViewModel : Abstraction.ViewModel
private async Task OpenUIAsync()
{
UserAndUids = await userService.GetRoleCollectionAsync().ConfigureAwait(true);
UserAndUids = await userService.GetRoleCollectionAsync().ConfigureAwait(false);
try
{
ThrowIfViewDisposed();
using (await DisposeLock.EnterAsync().ConfigureAwait(false))
{
ThrowIfViewDisposed();
await ThreadHelper.SwitchToMainThreadAsync();
refreshSecondsEntry = appDbContext.Settings.SingleOrAdd(SettingEntry.DailyNoteRefreshSeconds, "480");
selectedRefreshTime = refreshTimes.Single(t => t.Value == refreshSecondsEntry.GetInt32());
ScheduleTaskHelper.RegisterForDailyNoteRefresh(480);
OnPropertyChanged(nameof(SelectedRefreshTime));
refreshSecondsEntry = appDbContext.Settings.SingleOrAdd(SettingEntry.DailyNoteRefreshSeconds, "480");
selectedRefreshTime = refreshTimes.Single(t => t.Value == refreshSecondsEntry.GetInt32());
OnPropertyChanged(nameof(SelectedRefreshTime));
ScheduleTaskHelper.RegisterForDailyNoteRefresh(480);
reminderNotifyEntry = appDbContext.Settings.SingleOrAdd(SettingEntry.DailyNoteReminderNotify, SettingEntryHelper.FalseString);
isReminderNotification = reminderNotifyEntry.GetBoolean();
OnPropertyChanged(nameof(IsReminderNotification));
reminderNotifyEntry = appDbContext.Settings.SingleOrAdd(SettingEntry.DailyNoteReminderNotify, SettingEntryHelper.FalseString);
isReminderNotification = reminderNotifyEntry.GetBoolean();
OnPropertyChanged(nameof(IsReminderNotification));
silentModeEntry = appDbContext.Settings.SingleOrAdd(SettingEntry.DailyNoteSilentWhenPlayingGame, SettingEntryHelper.FalseString);
isSilentWhenPlayingGame = silentModeEntry.GetBoolean();
OnPropertyChanged(nameof(IsSilentWhenPlayingGame));
silentModeEntry = appDbContext.Settings.SingleOrAdd(SettingEntry.DailyNoteSilentWhenPlayingGame, SettingEntryHelper.FalseString);
isSilentWhenPlayingGame = silentModeEntry.GetBoolean();
OnPropertyChanged(nameof(IsSilentWhenPlayingGame));
ObservableCollection<DailyNoteEntry> temp = await dailyNoteService.GetDailyNoteEntriesAsync().ConfigureAwait(false);
await ThreadHelper.SwitchToMainThreadAsync();
DailyNoteEntries = temp;
await ThreadHelper.SwitchToBackgroundAsync();
}
ObservableCollection<DailyNoteEntry> temp = await dailyNoteService.GetDailyNoteEntriesAsync().ConfigureAwait(false);
await ThreadHelper.SwitchToMainThreadAsync();
DailyNoteEntries = temp;
}
catch (OperationCanceledException)
{
}
}
private async Task TrackRoleAsync(UserAndUid? role)

View File

@@ -1,6 +1,7 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.Data.Sqlite;
using Microsoft.UI.Xaml.Controls;
using Snap.Hutao.Control.Extension;
using Snap.Hutao.Core.IO;
@@ -199,7 +200,26 @@ internal class GachaLogViewModel : Abstraction.ViewModel
GachaLogRefreshProgressDialog dialog = new();
IAsyncDisposable dialogHider = await dialog.BlockAsync().ConfigureAwait(false);
Progress<FetchState> progress = new(dialog.OnReport);
bool authkeyValid = await gachaLogService.RefreshGachaLogAsync(query, strategy, progress, default).ConfigureAwait(false);
bool authkeyValid;
using (await DisposeLock.EnterAsync().ConfigureAwait(false))
{
try
{
authkeyValid = await gachaLogService.RefreshGachaLogAsync(query, strategy, progress, CancellationToken).ConfigureAwait(false);
}
catch (OperationCanceledException)
{
// We set true here in order to hide the dialog.
authkeyValid = true;
infoBarService.Warning("祈愿记录刷新操作被异常取消");
}
catch (Core.ExceptionService.UserdataCorruptedException ex)
{
authkeyValid = false;
infoBarService.Error(ex);
}
}
await ThreadHelper.SwitchToMainThreadAsync();
if (authkeyValid)

View File

@@ -221,13 +221,21 @@ internal class LaunchGameViewModel : Abstraction.ViewModel
ThrowIfViewDisposed();
MultiChannel multi = gameService.GetMultiChannel();
SelectedScheme = KnownSchemes.FirstOrDefault(s => s.Channel == multi.Channel && s.SubChannel == multi.SubChannel);
if (string.IsNullOrEmpty(multi.ConfigFilePath))
{
SelectedScheme = KnownSchemes.FirstOrDefault(s => s.Channel == multi.Channel && s.SubChannel == multi.SubChannel);
}
else
{
Ioc.Default.GetRequiredService<IInfoBarService>().Warning("无法读取游戏配置文件");
}
GameAccounts = await gameService.GetGameAccountCollectionAsync().ConfigureAwait(true);
// Sync uid
if (memoryCache.TryGetValue(DesiredUid, out object? value) && value is string uid)
{
SelectedGameAccount = GameAccounts.SingleOrDefault(g => g.AttachUid == uid);
SelectedGameAccount = GameAccounts.FirstOrDefault(g => g.AttachUid == uid);
}
// Sync from Settings

View File

@@ -207,7 +207,16 @@ internal class SettingViewModel : Abstraction.ViewModel
IInfoBarService infoBarService = Ioc.Default.GetRequiredService<IInfoBarService>();
if (Directory.Exists(cacheFolder))
{
Directory.Delete(cacheFolder, true);
try
{
Directory.Delete(cacheFolder, true);
}
catch (UnauthorizedAccessException)
{
infoBarService.Warning($"清除失败,文件目录权限不足,请使用管理员模式重试");
return;
}
infoBarService.Success("清除完成");
}
else

View File

@@ -52,7 +52,42 @@ internal class WelcomeViewModel : ObservableObject
private async Task OpenUIAsync()
{
List<DownloadSummary> downloadSummaries = new();
HashSet<DownloadSummary> downloadSummaries = GenerateStaticResourceDownloadTasks();
DownloadSummaries = downloadSummaries.ToObservableCollection();
// Cancel all previous created jobs
serviceProvider.GetRequiredService<BitsManager>().CancelAllJobs();
await Task.WhenAll(downloadSummaries.Select(async d =>
{
await d.DownloadAndExtractAsync().ConfigureAwait(false);
await ThreadHelper.SwitchToMainThreadAsync();
DownloadSummaries.Remove(d);
})).ConfigureAwait(true);
serviceProvider.GetRequiredService<IMessenger>().Send(new Message.WelcomeStateCompleteMessage());
// Complete StaticResourceContracts
LocalSetting.Set(SettingKeys.StaticResourceV1Contract, true);
LocalSetting.Set(SettingKeys.StaticResourceV2Contract, true);
LocalSetting.Set(SettingKeys.StaticResourceV3Contract, true);
try
{
new ToastContentBuilder()
.AddText("下载完成")
.AddText("现在可以开始使用胡桃了")
.Show();
}
catch (COMException)
{
// 0x803E0105
}
}
private HashSet<DownloadSummary> GenerateStaticResourceDownloadTasks()
{
HashSet<DownloadSummary> downloadSummaries = new(EqualityComparer<DownloadSummary>.Default);
if (!LocalSetting.Get(SettingKeys.StaticResourceV1Contract, false))
{
@@ -75,35 +110,20 @@ internal class WelcomeViewModel : ObservableObject
downloadSummaries.Add(new(serviceProvider, "圣遗物图标", "RelicIcon"));
}
DownloadSummaries = new(downloadSummaries);
// Cancel all previous created jobs
serviceProvider.GetRequiredService<BitsManager>().CancelAllJobs();
await Task.WhenAll(downloadSummaries.Select(d => d.DownloadAndExtractAsync())).ConfigureAwait(true);
serviceProvider.GetRequiredService<IMessenger>().Send(new Message.WelcomeStateCompleteMessage());
// Complete StaticResourceContracts
LocalSetting.Set(SettingKeys.StaticResourceV1Contract, true);
LocalSetting.Set(SettingKeys.StaticResourceV2Contract, true);
try
if (!LocalSetting.Get(SettingKeys.StaticResourceV3Contract, false))
{
new ToastContentBuilder()
.AddText("下载完成")
.AddText("现在可以开始使用胡桃了")
.Show();
}
catch (COMException)
{
// 0x803E0105
downloadSummaries.Add(new(serviceProvider, "天赋图标更新", "Skill"));
downloadSummaries.Add(new(serviceProvider, "命之座图标更新", "Talent"));
}
return downloadSummaries;
}
/// <summary>
/// 下载信息
/// </summary>
public class DownloadSummary : ObservableObject
[SuppressMessage("", "CA1067")]
public class DownloadSummary : ObservableObject, IEquatable<DownloadSummary>
{
private readonly IServiceProvider serviceProvider;
private readonly BitsManager bitsManager;
@@ -145,6 +165,12 @@ internal class WelcomeViewModel : ObservableObject
/// </summary>
public double ProgressValue { get => progressValue; set => SetProperty(ref progressValue, value); }
/// <inheritdoc/>
public bool Equals(DownloadSummary? other)
{
return fileName == other?.fileName;
}
/// <summary>
/// 异步下载并解压
/// </summary>

View File

@@ -211,25 +211,25 @@ internal class WikiAvatarViewModel : Abstraction.ViewModel
continue;
}
if (ImmutableIntrinsics.AssociationTypes.Contains(value))
if (IntrinsicImmutables.AssociationTypes.Contains(value))
{
keep = keep || avatar.FetterInfo.Association.GetDescriptionOrNull() == value;
continue;
}
if (ImmutableIntrinsics.WeaponTypes.Contains(value))
if (IntrinsicImmutables.WeaponTypes.Contains(value))
{
keep = keep || avatar.Weapon.GetDescriptionOrNull() == value;
continue;
}
if (ImmutableIntrinsics.ItemQualities.Contains(value))
if (IntrinsicImmutables.ItemQualities.Contains(value))
{
keep = keep || avatar.Quality.GetDescriptionOrNull() == value;
continue;
}
if (ImmutableIntrinsics.BodyTypes.Contains(value))
if (IntrinsicImmutables.BodyTypes.Contains(value))
{
keep = keep || avatar.Body.GetDescriptionOrNull() == value;
continue;

View File

@@ -203,19 +203,19 @@ internal class WikiWeaponViewModel : Abstraction.ViewModel
{
string value = segment.ToString();
if (ImmutableIntrinsics.WeaponTypes.Contains(value))
if (IntrinsicImmutables.WeaponTypes.Contains(value))
{
keep = keep || weapon.WeaponType.GetDescriptionOrNull() == value;
continue;
}
if (ImmutableIntrinsics.ItemQualities.Contains(value))
if (IntrinsicImmutables.ItemQualities.Contains(value))
{
keep = keep || weapon.Quality.GetDescriptionOrNull() == value;
continue;
}
if (ImmutableIntrinsics.FightProperties.Contains(value))
if (IntrinsicImmutables.FightProperties.Contains(value))
{
keep = keep || weapon.Property.Properties.ElementAtOrDefault(1).GetDescriptionOrNull() == value;
continue;

View File

@@ -41,6 +41,7 @@ public class MiHoYoJSInterface
private readonly CoreWebView2 webView;
private readonly IServiceProvider serviceProvider;
private readonly ILogger<MiHoYoJSInterface> logger;
private readonly SemaphoreSlim webMessageSemaphore = new(1);
/// <summary>
/// 构造一个新的调用桥
@@ -59,6 +60,8 @@ public class MiHoYoJSInterface
webView.NavigationStarting += OnNavigationStarting;
}
public event Action? ClosePageRequested;
/// <summary>
/// 获取ActionTicket
/// </summary>
@@ -160,7 +163,7 @@ public class MiHoYoJSInterface
string q = param.Payload.GetQueryParam();
string check = Md5Convert.ToHexString($"salt={salt}&t={t}&r={r}&b={b}&q={q}").ToLowerInvariant();
return new() { Data = new() { ["DS"] = $"{t},{r},{check}", }, };
return new() { Data = new() { ["DS"] = $"{t},{r},{check}" } };
static int GetRandom()
{
@@ -214,9 +217,12 @@ public class MiHoYoJSInterface
cookieToken = cookieTokenResponse.Data.CookieToken;
}
// Check null and create a new one to avoid System.NullReferenceException
user.CookieToken ??= new();
// sync ui and database
user.CookieToken![Cookie.COOKIE_TOKEN] = cookieToken!;
Ioc.Default.GetRequiredService<AppDbContext>().Users.UpdateAndSave(user.Entity);
user.CookieToken[Cookie.COOKIE_TOKEN] = cookieToken!;
serviceProvider.GetRequiredService<AppDbContext>().Users.UpdateAndSave(user.Entity);
}
else
{
@@ -234,6 +240,7 @@ public class MiHoYoJSInterface
[JsMethod("closePage")]
public virtual IJsResult? ClosePage(JsParam param)
{
ClosePageRequested?.Invoke();
return null;
}
@@ -256,7 +263,7 @@ public class MiHoYoJSInterface
[JsMethod("getStatusBarHeight")]
public virtual JsResult<Dictionary<string, object>> GetStatusBarHeight(JsParam param)
{
return new() { Data = new() { { "statusBarHeight", 0 } } };
return new() { Data = new() { ["statusBarHeight"] = 0 } };
}
[JsMethod("pushPage")]
@@ -352,11 +359,14 @@ public class MiHoYoJSInterface
JsParam param = JsonSerializer.Deserialize<JsParam>(message)!;
logger.LogInformation("[OnMessage]\nMethod : {method}\nPayload : {payload}\nCallback: {callback}", param.Method, param.Payload, param.Callback);
IJsResult? result = await TryGetJsResultFromJsParamAsync(param).ConfigureAwait(false);
if (result != null && param.Callback != null)
using (await webMessageSemaphore.EnterAsync().ConfigureAwait(false))
{
await ExecuteCallbackScriptAsync(param.Callback, result.ToString()).ConfigureAwait(false);
IJsResult? result = await TryGetJsResultFromJsParamAsync(param).ConfigureAwait(false);
if (result != null && param.Callback != null)
{
await ExecuteCallbackScriptAsync(param.Callback, result.ToString()).ConfigureAwait(false);
}
}
}

View File

@@ -45,7 +45,7 @@ public partial class Cookie
{
SortedDictionary<string, string> cookieMap = new();
cookieString = cookieString.Replace(" ", string.Empty);
string[] values = cookieString.TrimEnd(';').Split(';');
string[] values = cookieString.Split(';', StringSplitOptions.RemoveEmptyEntries);
foreach (string[] parts in values.Select(c => c.Split('=', 2)))
{
string name = parts[0].Trim();

View File

@@ -15,8 +15,8 @@ public class Game
public Package Latest { get; set; } = default!;
/// <summary>
/// 差异文件
/// 相对于当前版本的之前版本的差异文件(非预下载)
/// </summary>
[JsonPropertyName("diffs")]
public IList<DiffPackage> Diffs { get; set; } = default!;
public List<DiffPackage> Diffs { get; set; } = default!;
}

View File

@@ -21,13 +21,13 @@ public class GameResource
public Plugin Plugin { get; set; } = default!;
/// <summary>
/// 官网地址
/// 官网地址 https://ys.mihoyo.com/launcher
/// </summary>
[JsonPropertyName("web_url")]
public Uri WebUrl { get; set; } = default!;
/// <summary>
/// 强制更新文件
/// 强制更新文件 null
/// </summary>
[JsonPropertyName("force_update")]
public object? ForceUpdate { get; set; }
@@ -36,17 +36,23 @@ public class GameResource
/// 预下载
/// </summary>
[JsonPropertyName("pre_download_game")]
public object? PreDownloadGame { get; set; }
public Game? PreDownloadGame { get; set; }
/// <summary>
/// 过期更新包
/// </summary>
[JsonPropertyName("deprecated_packages")]
public List<LocalFile> DeprecatedPackages { get; set; } = default!;
public List<NameMd5> DeprecatedPackages { get; set; } = default!;
/// <summary>
/// 渠道服sdk
/// </summary>
[JsonPropertyName("sdk")]
public object? Sdk { get; set; }
/// <summary>
/// 过期的单个文件
/// </summary>
[JsonPropertyName("deprecated_files")]
public List<LocalFile>? DeprecatedFiles { get; set; }
public List<NameMd5>? DeprecatedFiles { get; set; }
}

View File

@@ -6,7 +6,7 @@ namespace Snap.Hutao.Web.Hoyolab.SdkStatic.Hk4e.Launcher;
/// <summary>
/// 资源文件
/// </summary>
public class LocalFile
public class NameMd5
{
/// <summary>
/// 文件名称

View File

@@ -6,7 +6,7 @@ namespace Snap.Hutao.Web.Hoyolab.SdkStatic.Hk4e.Launcher;
/// <summary>
/// 最新客户端
/// </summary>
public class Package : DownloadFile
public class Package : PathMd5
{
/// <summary>
/// 名称 空
@@ -27,14 +27,22 @@ public class Package : DownloadFile
[JsonNumberHandling(JsonNumberHandling.AllowReadingFromString)]
public long Size { get; set; } = default!;
/// <summary>
/// 主程序相对路径 YuanShen.exe
/// </summary>
public string Entry { get; set; } = default!;
/// <summary>
/// 语音包
/// </summary>
[JsonPropertyName("voice_packs")]
public List<VoicePackage> VoicePacks { get; set; } = default!;
// We don't want to support
// decompressed_path & segments
/// <summary>
/// 包大小
/// 包大小 bytes
/// </summary>
[JsonPropertyName("package_size")]
[JsonNumberHandling(JsonNumberHandling.AllowReadingFromString)]

View File

@@ -6,7 +6,7 @@ namespace Snap.Hutao.Web.Hoyolab.SdkStatic.Hk4e.Launcher;
/// <summary>
/// 下载的文件
/// </summary>
public class DownloadFile
public class PathMd5
{
/// <summary>
/// 下载地址

View File

@@ -6,7 +6,7 @@ namespace Snap.Hutao.Web.Hoyolab.SdkStatic.Hk4e.Launcher;
/// <summary>
/// 插件项
/// </summary>
public class PluginItem : LocalFile
public class PluginItem : NameMd5
{
/// <summary>
/// 版本 一般为空

View File

@@ -6,7 +6,7 @@ namespace Snap.Hutao.Web.Hoyolab.SdkStatic.Hk4e.Launcher;
/// <summary>
/// 语音包
/// </summary>
public class VoicePackage : DownloadFile
public class VoicePackage : PathMd5
{
/// <summary>
/// 语音

View File

@@ -51,7 +51,7 @@ internal class HomaClient
public async Task<Response<bool>> CheckRecordUploadedAsync(PlayerUid uid, CancellationToken token = default)
{
Response<bool>? resp = await httpClient
.GetFromJsonAsync<Response<bool>>(HutaoEndpoints.RecordCheck(uid.Value), token)
.TryCatchGetFromJsonAsync<Response<bool>>(HutaoEndpoints.RecordCheck(uid.Value), options, logger, token)
.ConfigureAwait(false);
return Response.Response.DefaultIfNull(resp);
@@ -67,7 +67,7 @@ internal class HomaClient
public async Task<Response<RankInfo>> GetRankAsync(PlayerUid uid, CancellationToken token = default)
{
Response<RankInfo>? resp = await httpClient
.GetFromJsonAsync<Response<RankInfo>>(HutaoEndpoints.RecordRank(uid.Value), token)
.TryCatchGetFromJsonAsync<Response<RankInfo>>(HutaoEndpoints.RecordRank(uid.Value), options, logger, token)
.ConfigureAwait(false);
return Response.Response.DefaultIfNull(resp);
@@ -82,7 +82,7 @@ internal class HomaClient
public async Task<Response<Overview>> GetOverviewAsync(CancellationToken token = default)
{
Response<Overview>? resp = await httpClient
.GetFromJsonAsync<Response<Overview>>(HutaoEndpoints.StatisticsOverview, token)
.TryCatchGetFromJsonAsync<Response<Overview>>(HutaoEndpoints.StatisticsOverview, options, logger, token)
.ConfigureAwait(false);
return Response.Response.DefaultIfNull(resp);

View File

@@ -2,6 +2,8 @@
// Licensed under the MIT license.
using Windows.Graphics;
using Windows.Win32.Foundation;
using static Windows.Win32.PInvoke;
namespace Snap.Hutao.Win32;
@@ -40,4 +42,29 @@ internal static class StructExtension
{
return sizeInt32.Width * sizeInt32.Height;
}
/// <summary>
/// 使用完成后自动关闭句柄
/// </summary>
/// <param name="handle">句柄</param>
/// <returns>用于关闭句柄的对象</returns>
public static IDisposable AutoClose(this HANDLE handle)
{
return new HandleCloser(handle);
}
private readonly struct HandleCloser : IDisposable
{
private readonly HANDLE handle;
public HandleCloser(HANDLE handle)
{
this.handle = handle;
}
public void Dispose()
{
CloseHandle(handle);
}
}
}