mirror of
https://github.com/wangdage12/Snap.Hutao.git
synced 2026-02-18 02:42:15 +08:00
Merge pull request #1 from hoshiizumiya/main
支持非打包模式直接启动并更改优化设置存储逻辑采用 wix 打包 msi 安装包支持脱离沙箱使用,为注入功能开发奠定基础
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -25,5 +25,6 @@ src/Snap.Hutao/Snap.Hutao/Generated Files/
|
|||||||
tools/
|
tools/
|
||||||
|
|
||||||
src/Snap.Hutao/Snap.Hutao/AppPackages
|
src/Snap.Hutao/Snap.Hutao/AppPackages
|
||||||
/src/Snap.Hutao/Snap.Hutao.Installer/bin
|
|
||||||
/src/Snap.Hutao/Snap.Hutao.Installer/obj
|
/src/Snap.Hutao/Snap.Hutao.Installer/obj
|
||||||
|
/src/Snap.Hutao/Snap.Hutao.Installer/bin/x64/Release/en-US
|
||||||
|
/src/Snap.Hutao/Snap.Hutao.Installer/bin
|
||||||
|
|||||||
3
src/Snap.Hutao/Snap.Hutao.Installer/Folders.wxs
Normal file
3
src/Snap.Hutao/Snap.Hutao.Installer/Folders.wxs
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<Wix xmlns="http://wixtoolset.org/schemas/v4/wxs">
|
||||||
|
|
||||||
|
</Wix>
|
||||||
8
src/Snap.Hutao/Snap.Hutao.Installer/Package.en-us.wxl
Normal file
8
src/Snap.Hutao/Snap.Hutao.Installer/Package.en-us.wxl
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
<!--
|
||||||
|
This file contains the declaration of all the localizable strings.
|
||||||
|
-->
|
||||||
|
<WixLocalization xmlns="http://wixtoolset.org/schemas/v4/wxl" Culture="en-US">
|
||||||
|
|
||||||
|
<String Id="DowngradeError" Value="A newer version of [ProductName] is already installed." />
|
||||||
|
|
||||||
|
</WixLocalization>
|
||||||
79
src/Snap.Hutao/Snap.Hutao.Installer/Package.wxs
Normal file
79
src/Snap.Hutao/Snap.Hutao.Installer/Package.wxs
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
<Wix xmlns="http://wixtoolset.org/schemas/v4/wxs">
|
||||||
|
<Package
|
||||||
|
Name="Snap.Hutao"
|
||||||
|
Manufacturer="Millennium Science Technology R-D Inst"
|
||||||
|
Version="1.0.0.0"
|
||||||
|
UpgradeCode="121203be-60cb-408f-92cc-7080f6598e68"
|
||||||
|
Scope="perMachine">
|
||||||
|
|
||||||
|
<MajorUpgrade DowngradeErrorMessage="A newer version of [ProductName] is already installed." />
|
||||||
|
<MediaTemplate EmbedCab="yes" />
|
||||||
|
|
||||||
|
<Feature Id="ProductFeature" Title="Snap.Hutao" Level="1">
|
||||||
|
<ComponentGroupRef Id="MainAppComponents" />
|
||||||
|
|
||||||
|
<!-- 快捷方式组件 -->
|
||||||
|
<ComponentRef Id="ApplicationShortcut" />
|
||||||
|
<ComponentRef Id="DesktopShortcut" />
|
||||||
|
</Feature>
|
||||||
|
</Package>
|
||||||
|
|
||||||
|
<!-- 安装目录 -->
|
||||||
|
<Fragment>
|
||||||
|
<StandardDirectory Id="ProgramFiles64Folder">
|
||||||
|
<Directory Id="INSTALLFOLDER" Name="Snap.Hutao" />
|
||||||
|
</StandardDirectory>
|
||||||
|
|
||||||
|
<StandardDirectory Id="ProgramMenuFolder">
|
||||||
|
<Directory Id="ApplicationProgramsFolder" Name="Snap Hutao" />
|
||||||
|
</StandardDirectory>
|
||||||
|
|
||||||
|
<StandardDirectory Id="DesktopFolder" />
|
||||||
|
</Fragment>
|
||||||
|
|
||||||
|
<!-- 桌面快捷方式 -->
|
||||||
|
<Fragment>
|
||||||
|
<Component Id="DesktopShortcut" Directory="DesktopFolder" Guid="*">
|
||||||
|
|
||||||
|
<Shortcut
|
||||||
|
Id="DesktopShortcut_Normal"
|
||||||
|
Name="Snap Hutao"
|
||||||
|
Description="Snap Hutao Client"
|
||||||
|
Target="[INSTALLFOLDER]Snap.Hutao.exe"
|
||||||
|
WorkingDirectory="INSTALLFOLDER" />
|
||||||
|
|
||||||
|
<!-- KeyPath 必须是 HKCU,因为快捷方式安装到用户目录 -->
|
||||||
|
<RegistryValue
|
||||||
|
Root="HKCU"
|
||||||
|
Key="Software\Snap.Hutao"
|
||||||
|
Name="DesktopShortcutInstalled"
|
||||||
|
Type="integer"
|
||||||
|
Value="1"
|
||||||
|
KeyPath="yes" />
|
||||||
|
</Component>
|
||||||
|
</Fragment>
|
||||||
|
|
||||||
|
<!-- 开始菜单快捷方式 -->
|
||||||
|
<Fragment>
|
||||||
|
<Component Id="ApplicationShortcut" Directory="ApplicationProgramsFolder" Guid="*">
|
||||||
|
|
||||||
|
<Shortcut
|
||||||
|
Id="ApplicationStartMenuShortcut"
|
||||||
|
Name="Snap Hutao"
|
||||||
|
Description="Snap Hutao Client"
|
||||||
|
Target="[INSTALLFOLDER]Snap.Hutao.exe"
|
||||||
|
WorkingDirectory="INSTALLFOLDER" />
|
||||||
|
|
||||||
|
<RemoveFolder Id="CleanUpShortCut" Directory="ApplicationProgramsFolder" On="uninstall" />
|
||||||
|
|
||||||
|
<!-- KeyPath 依然必须改为 HKCU -->
|
||||||
|
<RegistryValue
|
||||||
|
Root="HKCU"
|
||||||
|
Key="Software\Snap.Hutao"
|
||||||
|
Name="StartMenuShortcutInstalled"
|
||||||
|
Type="integer"
|
||||||
|
Value="1"
|
||||||
|
KeyPath="yes" />
|
||||||
|
</Component>
|
||||||
|
</Fragment>
|
||||||
|
</Wix>
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
<Project Sdk="WixToolset.Sdk/6.0.2">
|
||||||
|
<PropertyGroup>
|
||||||
|
<SuppressIces>ICE03;ICE60</SuppressIces>
|
||||||
|
<Platform>x64</Platform>
|
||||||
|
<TargetFramework>net10.0-windows10.0.26100.0</TargetFramework>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\Snap.Hutao\Snap.Hutao.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<HarvestDirectory Include="..\Snap.Hutao\bin\$(Platform)\$(Configuration)\$(TargetFramework)\win-x64">
|
||||||
|
<ComponentGroupName>MainAppComponents</ComponentGroupName>
|
||||||
|
<DirectoryRefId>INSTALLFOLDER</DirectoryRefId>
|
||||||
|
<SuppressCom>true</SuppressCom>
|
||||||
|
<SuppressRegistry>true</SuppressRegistry>
|
||||||
|
<SuppressRootDirectory>true</SuppressRootDirectory>
|
||||||
|
</HarvestDirectory>
|
||||||
|
|
||||||
|
<PackageReference Include="WixToolset.Heat" Version="4.0.1" />
|
||||||
|
</ItemGroup>
|
||||||
|
</Project>
|
||||||
@@ -1,24 +1,24 @@
|
|||||||
<Solution>
|
<Solution>
|
||||||
<Configurations>
|
<Configurations>
|
||||||
<Platform Name="Any CPU" />
|
<Platform Name="Any CPU" />
|
||||||
|
<Platform Name="ARM64" />
|
||||||
<Platform Name="x64" />
|
<Platform Name="x64" />
|
||||||
|
<Platform Name="x86" />
|
||||||
</Configurations>
|
</Configurations>
|
||||||
<Folder Name="/Solution Items/">
|
<Folder Name="/Solution Items/">
|
||||||
<File Path=".editorconfig" />
|
<File Path=".editorconfig" />
|
||||||
<File Path=".vsconfig" />
|
<File Path=".vsconfig" />
|
||||||
</Folder>
|
</Folder>
|
||||||
<Project Path="Snap.Hutao.Test\Snap.Hutao.Test.csproj" />
|
<Project Path="Snap.Hutao.Installer/Snap.Hutao.Installer.wixproj" Type="b7dd6f7e-def8-4e67-b5b7-07ef123db6f0" Id="91a04cd0-28cc-4562-92e1-202bc163edd7">
|
||||||
<Project Path="Snap.Hutao\Snap.Hutao.csproj">
|
|
||||||
<!-- For Rider -->
|
|
||||||
<Configuration Solution="Debug|Any CPU" Project="Debug|x64|Deploy" />
|
|
||||||
<Configuration Solution="Debug|x64" Project="Debug|x64|Deploy" />
|
|
||||||
<Configuration Solution="Release|Any CPU" Project="Release|x64|Deploy" />
|
|
||||||
<Configuration Solution="Release|x64" Project="Release|x64|Deploy" />
|
|
||||||
<!-- For Visual Studio -->
|
|
||||||
<Platform Solution="*|Any CPU" Project="x64" />
|
<Platform Solution="*|Any CPU" Project="x64" />
|
||||||
<Platform Solution="*|arm64" Project="arm64" />
|
</Project>
|
||||||
<Platform Solution="*|x64" Project="x64" />
|
<Project Path="Snap.Hutao.Test\Snap.Hutao.Test.csproj">
|
||||||
<Platform Solution="*|x86" Project="x86" />
|
<Build Solution="*|ARM64" Project="false" />
|
||||||
<Deploy />
|
<Build Solution="*|x86" Project="false" />
|
||||||
|
</Project>
|
||||||
|
<Project Path="Snap.Hutao\Snap.Hutao.csproj">
|
||||||
|
<Platform Project="x64" />
|
||||||
|
<Deploy Solution="*|Any CPU" />
|
||||||
|
<Deploy Solution="*|x64" />
|
||||||
</Project>
|
</Project>
|
||||||
</Solution>
|
</Solution>
|
||||||
@@ -13,6 +13,7 @@ using Snap.Hutao.Service;
|
|||||||
using Snap.Hutao.UI.Xaml;
|
using Snap.Hutao.UI.Xaml;
|
||||||
using Snap.Hutao.UI.Xaml.Control.Theme;
|
using Snap.Hutao.UI.Xaml.Control.Theme;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
|
using System.IO;
|
||||||
|
|
||||||
namespace Snap.Hutao;
|
namespace Snap.Hutao;
|
||||||
|
|
||||||
@@ -64,6 +65,11 @@ public sealed partial class App : Application
|
|||||||
|
|
||||||
protected override void OnLaunched(LaunchActivatedEventArgs args)
|
protected override void OnLaunched(LaunchActivatedEventArgs args)
|
||||||
{
|
{
|
||||||
|
// ⚠️ 添加启动诊断
|
||||||
|
#if DEBUG
|
||||||
|
Core.ApplicationModel.PackageIdentityDiagnostics.LogDiagnostics();
|
||||||
|
#endif
|
||||||
|
|
||||||
DebugPatchXamlDiagnosticsRemoveRootObjectFromLVT();
|
DebugPatchXamlDiagnosticsRemoveRootObjectFromLVT();
|
||||||
|
|
||||||
try
|
try
|
||||||
@@ -71,17 +77,40 @@ public sealed partial class App : Application
|
|||||||
// Important: You must call AppNotificationManager::Default().Register
|
// Important: You must call AppNotificationManager::Default().Register
|
||||||
// before calling AppInstance.GetCurrent.GetActivatedEventArgs.
|
// before calling AppInstance.GetCurrent.GetActivatedEventArgs.
|
||||||
AppNotificationManager.Default.NotificationInvoked += activation.NotificationInvoked;
|
AppNotificationManager.Default.NotificationInvoked += activation.NotificationInvoked;
|
||||||
AppNotificationManager.Default.Register();
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
AppNotificationManager.Default.Register();
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// In unpackaged mode, this might fail - continue anyway
|
||||||
|
}
|
||||||
|
|
||||||
// E_INVALIDARG E_OUTOFMEMORY
|
// E_INVALIDARG E_OUTOFMEMORY
|
||||||
AppActivationArguments activatedEventArgs = AppInstance.GetCurrent().GetActivatedEventArgs();
|
AppActivationArguments? activatedEventArgs = null;
|
||||||
|
PrivateNamedPipeClient? namedPipeClient = null;
|
||||||
|
|
||||||
if (serviceProvider.GetRequiredService<PrivateNamedPipeClient>().TryRedirectActivationTo(activatedEventArgs))
|
try
|
||||||
{
|
{
|
||||||
SentrySdk.AddBreadcrumb(BreadcrumbFactory.CreateInfo("Application exiting on RedirectActivationTo", "Hutao"));
|
activatedEventArgs = AppInstance.GetCurrent().GetActivatedEventArgs();
|
||||||
XamlApplicationLifetime.ActivationAndInitializationCompleted = true;
|
namedPipeClient = serviceProvider.GetRequiredService<PrivateNamedPipeClient>();
|
||||||
Exit();
|
}
|
||||||
return;
|
catch
|
||||||
|
{
|
||||||
|
// In unpackaged mode, AppInstance might not work
|
||||||
|
// Create a default activation argument for launch
|
||||||
|
}
|
||||||
|
|
||||||
|
if (activatedEventArgs is not null && namedPipeClient is not null)
|
||||||
|
{
|
||||||
|
if (namedPipeClient.TryRedirectActivationTo(activatedEventArgs))
|
||||||
|
{
|
||||||
|
SentrySdk.AddBreadcrumb(BreadcrumbFactory.CreateInfo("Application exiting on RedirectActivationTo", "Hutao"));
|
||||||
|
XamlApplicationLifetime.ActivationAndInitializationCompleted = true;
|
||||||
|
Exit();
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.LogInformation($"{ConsoleBanner}");
|
logger.LogInformation($"{ConsoleBanner}");
|
||||||
@@ -90,10 +119,30 @@ public sealed partial class App : Application
|
|||||||
|
|
||||||
// Manually invoke
|
// Manually invoke
|
||||||
SentrySdk.AddBreadcrumb(BreadcrumbFactory.CreateInfo("Activate and Initialize", "Application"));
|
SentrySdk.AddBreadcrumb(BreadcrumbFactory.CreateInfo("Activate and Initialize", "Application"));
|
||||||
activation.ActivateAndInitialize(HutaoActivationArguments.FromAppActivationArguments(activatedEventArgs));
|
|
||||||
|
HutaoActivationArguments hutaoArgs = activatedEventArgs is not null
|
||||||
|
? HutaoActivationArguments.FromAppActivationArguments(activatedEventArgs)
|
||||||
|
: HutaoActivationArguments.CreateDefaultLaunchArguments();
|
||||||
|
|
||||||
|
activation.ActivateAndInitialize(hutaoArgs);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
|
// ⚠️ 添加更详细的异常日志
|
||||||
|
try
|
||||||
|
{
|
||||||
|
string errorPath = Path.Combine(
|
||||||
|
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
|
||||||
|
"Hutao",
|
||||||
|
"startup_error.txt");
|
||||||
|
Directory.CreateDirectory(Path.GetDirectoryName(errorPath)!);
|
||||||
|
File.WriteAllText(errorPath, $"[{DateTime.Now:yyyy-MM-dd HH:mm:ss}] Startup Error:\n{ex}");
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// Ignore
|
||||||
|
}
|
||||||
|
|
||||||
SentrySdk.CaptureException(ex);
|
SentrySdk.CaptureException(ex);
|
||||||
SentrySdk.Flush();
|
SentrySdk.Flush();
|
||||||
|
|
||||||
|
|||||||
@@ -31,8 +31,15 @@ public static partial class Bootstrap
|
|||||||
[STAThread]
|
[STAThread]
|
||||||
private static void Main(string[] args)
|
private static void Main(string[] args)
|
||||||
{
|
{
|
||||||
|
#if DEBUG
|
||||||
|
System.Diagnostics.Debug.WriteLine("[Bootstrap.Main] Starting...");
|
||||||
|
#endif
|
||||||
|
|
||||||
if (Mutex.TryOpenExisting(LockName, out _))
|
if (Mutex.TryOpenExisting(LockName, out _))
|
||||||
{
|
{
|
||||||
|
#if DEBUG
|
||||||
|
System.Diagnostics.Debug.WriteLine("[Bootstrap.Main] Another instance is running");
|
||||||
|
#endif
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -42,9 +49,16 @@ public static partial class Bootstrap
|
|||||||
mutexSecurity.AddAccessRule(new(SecurityIdentifiers.Everyone, MutexRights.FullControl, AccessControlType.Allow));
|
mutexSecurity.AddAccessRule(new(SecurityIdentifiers.Everyone, MutexRights.FullControl, AccessControlType.Allow));
|
||||||
mutex = MutexAcl.Create(true, LockName, out bool created, mutexSecurity);
|
mutex = MutexAcl.Create(true, LockName, out bool created, mutexSecurity);
|
||||||
Debug.Assert(created);
|
Debug.Assert(created);
|
||||||
|
|
||||||
|
#if DEBUG
|
||||||
|
System.Diagnostics.Debug.WriteLine("[Bootstrap.Main] Mutex created");
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
catch (WaitHandleCannotBeOpenedException)
|
catch (WaitHandleCannotBeOpenedException)
|
||||||
{
|
{
|
||||||
|
#if DEBUG
|
||||||
|
System.Diagnostics.Debug.WriteLine("[Bootstrap.Main] WaitHandleCannotBeOpenedException");
|
||||||
|
#endif
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -54,34 +68,70 @@ public static partial class Bootstrap
|
|||||||
{
|
{
|
||||||
if (!OSPlatformSupported())
|
if (!OSPlatformSupported())
|
||||||
{
|
{
|
||||||
|
#if DEBUG
|
||||||
|
System.Diagnostics.Debug.WriteLine("[Bootstrap.Main] OS not supported");
|
||||||
|
#endif
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#if DEBUG
|
||||||
|
System.Diagnostics.Debug.WriteLine("[Bootstrap.Main] Setting environment variables");
|
||||||
|
#endif
|
||||||
|
|
||||||
Environment.SetEnvironmentVariable("WEBVIEW2_DEFAULT_BACKGROUND_COLOR", "00000000");
|
Environment.SetEnvironmentVariable("WEBVIEW2_DEFAULT_BACKGROUND_COLOR", "00000000");
|
||||||
Environment.SetEnvironmentVariable("DOTNET_SYSTEM_BUFFERS_SHAREDARRAYPOOL_MAXARRAYSPERPARTITION", "128");
|
Environment.SetEnvironmentVariable("DOTNET_SYSTEM_BUFFERS_SHAREDARRAYPOOL_MAXARRAYSPERPARTITION", "128");
|
||||||
AppContext.SetData("MVVMTOOLKIT_ENABLE_INOTIFYPROPERTYCHANGING_SUPPORT", false);
|
AppContext.SetData("MVVMTOOLKIT_ENABLE_INOTIFYPROPERTYCHANGING_SUPPORT", false);
|
||||||
|
|
||||||
|
#if DEBUG
|
||||||
|
System.Diagnostics.Debug.WriteLine("[Bootstrap.Main] Initializing COM wrappers");
|
||||||
|
#endif
|
||||||
|
|
||||||
ComWrappersSupport.InitializeComWrappers();
|
ComWrappersSupport.InitializeComWrappers();
|
||||||
|
|
||||||
|
#if DEBUG
|
||||||
|
System.Diagnostics.Debug.WriteLine("[Bootstrap.Main] Initializing DI container");
|
||||||
|
#endif
|
||||||
|
|
||||||
// By adding the using statement, we can dispose the injected services when closing
|
// By adding the using statement, we can dispose the injected services when closing
|
||||||
using (ServiceProvider serviceProvider = DependencyInjection.Initialize())
|
using (ServiceProvider serviceProvider = DependencyInjection.Initialize())
|
||||||
{
|
{
|
||||||
Thread.CurrentThread.Name = "Snap Hutao Application Main Thread";
|
Thread.CurrentThread.Name = "Snap Hutao Application Main Thread";
|
||||||
|
|
||||||
|
#if DEBUG
|
||||||
|
System.Diagnostics.Debug.WriteLine("[Bootstrap.Main] Calling Application.Start()");
|
||||||
|
#endif
|
||||||
|
|
||||||
// If you hit a COMException REGDB_E_CLASSNOTREG (0x80040154) during debugging
|
// If you hit a COMException REGDB_E_CLASSNOTREG (0x80040154) during debugging
|
||||||
// You can delete bin and obj folder and then rebuild.
|
// You can delete bin and obj folder and then rebuild.
|
||||||
// In a Desktop app this runs a message pump internally,
|
// In a Desktop app this runs a message pump internally,
|
||||||
// and does not return until the application shuts down.
|
// and does not return until the application shuts down.
|
||||||
Application.Start(AppInitializationCallback);
|
Application.Start(AppInitializationCallback);
|
||||||
|
|
||||||
|
#if DEBUG
|
||||||
|
System.Diagnostics.Debug.WriteLine("[Bootstrap.Main] Application.Start() returned");
|
||||||
|
#endif
|
||||||
|
|
||||||
XamlApplicationLifetime.Exited = true;
|
XamlApplicationLifetime.Exited = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#if DEBUG
|
||||||
|
System.Diagnostics.Debug.WriteLine("[Bootstrap.Main] Flushing Sentry");
|
||||||
|
#endif
|
||||||
|
|
||||||
SentrySdk.Flush();
|
SentrySdk.Flush();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#if DEBUG
|
||||||
|
System.Diagnostics.Debug.WriteLine("[Bootstrap.Main] Exiting");
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void InitializeApp(ApplicationInitializationCallbackParams param)
|
private static void InitializeApp(ApplicationInitializationCallbackParams param)
|
||||||
{
|
{
|
||||||
|
#if DEBUG
|
||||||
|
System.Diagnostics.Debug.WriteLine("[Bootstrap.InitializeApp] Callback invoked");
|
||||||
|
#endif
|
||||||
|
|
||||||
Gen2GcCallback.Register(() =>
|
Gen2GcCallback.Register(() =>
|
||||||
{
|
{
|
||||||
SentrySdk.AddBreadcrumb(BreadcrumbFactory.CreateDebug("Gen2 GC triggered.", "Runtime"));
|
SentrySdk.AddBreadcrumb(BreadcrumbFactory.CreateDebug("Gen2 GC triggered.", "Runtime"));
|
||||||
@@ -90,8 +140,17 @@ public static partial class Bootstrap
|
|||||||
|
|
||||||
IServiceProvider serviceProvider = Ioc.Default;
|
IServiceProvider serviceProvider = Ioc.Default;
|
||||||
|
|
||||||
_ = serviceProvider.GetRequiredService<ITaskContext>();
|
#if DEBUG
|
||||||
|
System.Diagnostics.Debug.WriteLine("[Bootstrap.InitializeApp] Creating App instance");
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// ⚠️ 只创建 App
|
||||||
|
// TaskContext 将在第一次被需要时自动创建(延迟初始化)
|
||||||
_ = serviceProvider.GetRequiredService<App>();
|
_ = serviceProvider.GetRequiredService<App>();
|
||||||
|
|
||||||
|
#if DEBUG
|
||||||
|
System.Diagnostics.Debug.WriteLine("[Bootstrap.InitializeApp] Initialization complete (TaskContext will be lazily created)");
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool OSPlatformSupported()
|
private static bool OSPlatformSupported()
|
||||||
|
|||||||
@@ -4,14 +4,13 @@
|
|||||||
using System.Collections.Frozen;
|
using System.Collections.Frozen;
|
||||||
using System.Security.Cryptography;
|
using System.Security.Cryptography;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using Windows.ApplicationModel;
|
|
||||||
|
|
||||||
namespace Snap.Hutao.Core.ApplicationModel;
|
namespace Snap.Hutao.Core.ApplicationModel;
|
||||||
|
|
||||||
internal static class LimitedAccessFeatures
|
internal static class LimitedAccessFeatures
|
||||||
{
|
{
|
||||||
private static readonly string PackagePublisherId = Package.Current.Id.PublisherId;
|
private static readonly string PackagePublisherId = PackageIdentityAdapter.PublisherId;
|
||||||
private static readonly string PackageFamilyName = Package.Current.Id.FamilyName;
|
private static readonly string PackageFamilyName = PackageIdentityAdapter.FamilyName;
|
||||||
|
|
||||||
private static readonly FrozenDictionary<string, string> Features = WinRTAdaptive.ToFrozenDictionary(
|
private static readonly FrozenDictionary<string, string> Features = WinRTAdaptive.ToFrozenDictionary(
|
||||||
[
|
[
|
||||||
@@ -67,8 +66,15 @@ internal static class LimitedAccessFeatures
|
|||||||
KeyValuePair.Create("com.microsoft.windows.windowdecorations", "425261a8-7f73-4319-8a53-fc13f87e1717")
|
KeyValuePair.Create("com.microsoft.windows.windowdecorations", "425261a8-7f73-4319-8a53-fc13f87e1717")
|
||||||
]);
|
]);
|
||||||
|
|
||||||
public static LimitedAccessFeatureRequestResult TryUnlockFeature(string featureId)
|
public static Windows.ApplicationModel.LimitedAccessFeatureRequestResult TryUnlockFeature(string featureId)
|
||||||
{
|
{
|
||||||
|
if (!PackageIdentityAdapter.HasPackageIdentity)
|
||||||
|
{
|
||||||
|
// In unpackaged mode, we can't unlock limited access features
|
||||||
|
// Create a dummy result - actual implementation will handle the failure
|
||||||
|
return default;
|
||||||
|
}
|
||||||
|
|
||||||
return Windows.ApplicationModel.LimitedAccessFeatures.TryUnlockFeature(featureId, GetToken(featureId), GetAttestation(featureId));
|
return Windows.ApplicationModel.LimitedAccessFeatures.TryUnlockFeature(featureId, GetToken(featureId), GetAttestation(featureId));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,109 @@
|
|||||||
|
// Copyright (c) Millennium-Science-Technology-R-D-Inst. All rights reserved.
|
||||||
|
// Licensed under the MIT license.
|
||||||
|
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.IO;
|
||||||
|
using System.Reflection;
|
||||||
|
|
||||||
|
namespace Snap.Hutao.Core.ApplicationModel;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Adapter to handle both packaged and unpackaged app scenarios
|
||||||
|
/// </summary>
|
||||||
|
internal static class PackageIdentityAdapter
|
||||||
|
{
|
||||||
|
private static readonly Lazy<bool> LazyHasPackageIdentity = new(CheckPackageIdentity);
|
||||||
|
private static readonly Lazy<string> LazyAppDirectory = new(GetAppDirectoryPath);
|
||||||
|
private static readonly Lazy<Version> LazyAppVersion = new(GetAppVersionInternal);
|
||||||
|
private static readonly Lazy<string> LazyFamilyName = new(GetFamilyNameInternal);
|
||||||
|
private static readonly Lazy<string> LazyPublisherId = new(GetPublisherIdInternal);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Check if the app has package identity
|
||||||
|
/// </summary>
|
||||||
|
public static bool HasPackageIdentity => LazyHasPackageIdentity.Value;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get application installation directory
|
||||||
|
/// </summary>
|
||||||
|
public static string AppDirectory => LazyAppDirectory.Value;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get application version
|
||||||
|
/// </summary>
|
||||||
|
public static Version AppVersion => LazyAppVersion.Value;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get package family name (or fallback for unpackaged)
|
||||||
|
/// </summary>
|
||||||
|
public static string FamilyName => LazyFamilyName.Value;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get publisher ID (or fallback for unpackaged)
|
||||||
|
/// </summary>
|
||||||
|
public static string PublisherId => LazyPublisherId.Value;
|
||||||
|
|
||||||
|
private static bool CheckPackageIdentity()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Try to access Package.Current - if it throws, we don't have package identity
|
||||||
|
_ = Windows.ApplicationModel.Package.Current.Id;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string GetAppDirectoryPath()
|
||||||
|
{
|
||||||
|
if (HasPackageIdentity)
|
||||||
|
{
|
||||||
|
return Windows.ApplicationModel.Package.Current.InstalledLocation.Path;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unpackaged: use the exe directory
|
||||||
|
string? exePath = Process.GetCurrentProcess().MainModule?.FileName;
|
||||||
|
ArgumentException.ThrowIfNullOrEmpty(exePath);
|
||||||
|
string? directory = Path.GetDirectoryName(exePath);
|
||||||
|
ArgumentException.ThrowIfNullOrEmpty(directory);
|
||||||
|
return directory;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Version GetAppVersionInternal()
|
||||||
|
{
|
||||||
|
if (HasPackageIdentity)
|
||||||
|
{
|
||||||
|
return Windows.ApplicationModel.Package.Current.Id.Version.ToVersion();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unpackaged: use assembly version
|
||||||
|
Assembly assembly = Assembly.GetExecutingAssembly();
|
||||||
|
Version? version = assembly.GetName().Version;
|
||||||
|
return version ?? new Version(1, 0, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string GetFamilyNameInternal()
|
||||||
|
{
|
||||||
|
if (HasPackageIdentity)
|
||||||
|
{
|
||||||
|
return Windows.ApplicationModel.Package.Current.Id.FamilyName;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unpackaged: use a deterministic fallback
|
||||||
|
return "Snap.Hutao.Unpackaged";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string GetPublisherIdInternal()
|
||||||
|
{
|
||||||
|
if (HasPackageIdentity)
|
||||||
|
{
|
||||||
|
return Windows.ApplicationModel.Package.Current.Id.PublisherId;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unpackaged: use a fallback
|
||||||
|
return "CN=Millennium-Science-Technology-R-D-Inst";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
// Copyright (c) Millennium-Science-Technology-R-D-Inst. All rights reserved.
|
||||||
|
// Licensed under the MIT license.
|
||||||
|
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.IO;
|
||||||
|
|
||||||
|
namespace Snap.Hutao.Core.ApplicationModel;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Diagnostic helper for PackageIdentityAdapter
|
||||||
|
/// </summary>
|
||||||
|
internal static class PackageIdentityDiagnostics
|
||||||
|
{
|
||||||
|
public static void LogDiagnostics()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
string logPath = Path.Combine(
|
||||||
|
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
|
||||||
|
"Hutao",
|
||||||
|
"startup_diagnostics.txt");
|
||||||
|
|
||||||
|
Directory.CreateDirectory(Path.GetDirectoryName(logPath)!);
|
||||||
|
|
||||||
|
using (StreamWriter writer = File.CreateText(logPath))
|
||||||
|
{
|
||||||
|
writer.WriteLine($"[{DateTime.Now:yyyy-MM-dd HH:mm:ss}] Startup Diagnostics");
|
||||||
|
writer.WriteLine($"HasPackageIdentity: {PackageIdentityAdapter.HasPackageIdentity}");
|
||||||
|
writer.WriteLine($"AppVersion: {PackageIdentityAdapter.AppVersion}");
|
||||||
|
writer.WriteLine($"AppDirectory: {PackageIdentityAdapter.AppDirectory}");
|
||||||
|
writer.WriteLine($"FamilyName: {PackageIdentityAdapter.FamilyName}");
|
||||||
|
writer.WriteLine($"PublisherId: {PackageIdentityAdapter.PublisherId}");
|
||||||
|
writer.WriteLine("---");
|
||||||
|
}
|
||||||
|
|
||||||
|
Debug.WriteLine($"Diagnostics written to: {logPath}");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Debug.WriteLine($"Failed to write diagnostics: {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@
|
|||||||
// Licensed under the MIT license.
|
// Licensed under the MIT license.
|
||||||
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Snap.Hutao.Core.ApplicationModel;
|
||||||
using Snap.Hutao.Model.Entity.Database;
|
using Snap.Hutao.Model.Entity.Database;
|
||||||
using System.Security.Cryptography;
|
using System.Security.Cryptography;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
@@ -17,7 +18,20 @@ internal sealed partial class HutaoDiagnostics : IHutaoDiagnostics
|
|||||||
[GeneratedConstructor]
|
[GeneratedConstructor]
|
||||||
public partial HutaoDiagnostics(IServiceProvider serviceProvider);
|
public partial HutaoDiagnostics(IServiceProvider serviceProvider);
|
||||||
|
|
||||||
public ApplicationDataContainer LocalSettings { get => ApplicationData.Current.LocalSettings; }
|
public ApplicationDataContainer? LocalSettings
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (PackageIdentityAdapter.HasPackageIdentity)
|
||||||
|
{
|
||||||
|
return ApplicationData.Current.LocalSettings;
|
||||||
|
}
|
||||||
|
|
||||||
|
// In unpackaged mode, ApplicationDataContainer is not available
|
||||||
|
// Return null - scripting/diagnostics code should handle this gracefully
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public async ValueTask<int> ExecuteSqlAsync(string sql)
|
public async ValueTask<int> ExecuteSqlAsync(string sql)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ namespace Snap.Hutao.Core.Diagnostics;
|
|||||||
[SuppressMessage("", "SH001", Justification = "IHutaoDiagnostics must be public in order to be exposed to the scripting environment")]
|
[SuppressMessage("", "SH001", Justification = "IHutaoDiagnostics must be public in order to be exposed to the scripting environment")]
|
||||||
public interface IHutaoDiagnostics
|
public interface IHutaoDiagnostics
|
||||||
{
|
{
|
||||||
ApplicationDataContainer LocalSettings { get; }
|
ApplicationDataContainer? LocalSettings { get; }
|
||||||
|
|
||||||
ValueTask<int> ExecuteSqlAsync(string sql);
|
ValueTask<int> ExecuteSqlAsync(string sql);
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
using Microsoft.Web.WebView2.Core;
|
using Microsoft.Web.WebView2.Core;
|
||||||
using Microsoft.Win32;
|
using Microsoft.Win32;
|
||||||
using Microsoft.Windows.AppNotifications;
|
using Microsoft.Windows.AppNotifications;
|
||||||
|
using Snap.Hutao.Core.ApplicationModel;
|
||||||
using Snap.Hutao.Core.ExceptionService;
|
using Snap.Hutao.Core.ExceptionService;
|
||||||
using Snap.Hutao.Core.IO;
|
using Snap.Hutao.Core.IO;
|
||||||
using Snap.Hutao.Core.IO.Hashing;
|
using Snap.Hutao.Core.IO.Hashing;
|
||||||
@@ -13,31 +14,32 @@ using System.Diagnostics;
|
|||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Security.Cryptography;
|
using System.Security.Cryptography;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using Windows.ApplicationModel;
|
|
||||||
using Windows.Storage;
|
|
||||||
|
|
||||||
namespace Snap.Hutao.Core;
|
namespace Snap.Hutao.Core;
|
||||||
|
|
||||||
internal static class HutaoRuntime
|
internal static class HutaoRuntime
|
||||||
{
|
{
|
||||||
public static Version Version { get; } = Package.Current.Id.Version.ToVersion();
|
public static Version Version { get; } = PackageIdentityAdapter.AppVersion;
|
||||||
|
|
||||||
public static string UserAgent { get; } = $"Snap Hutao/{Version}";
|
public static string UserAgent { get; } = $"Snap Hutao/{Version}";
|
||||||
|
|
||||||
public static string DataDirectory { get; } = InitializeDataDirectory();
|
public static string DataDirectory { get; } = InitializeDataDirectory();
|
||||||
|
|
||||||
public static string LocalCacheDirectory { get; } = ApplicationData.Current.LocalCacheFolder.Path;
|
public static string LocalCacheDirectory { get; } = InitializeLocalCacheDirectory();
|
||||||
|
|
||||||
public static string FamilyName { get; } = Package.Current.Id.FamilyName;
|
public static string FamilyName { get; } = PackageIdentityAdapter.FamilyName;
|
||||||
|
|
||||||
public static string DeviceId { get; } = InitializeDeviceId();
|
public static string DeviceId { get; } = InitializeDeviceId();
|
||||||
|
|
||||||
public static WebView2Version WebView2Version { get; } = InitializeWebView2();
|
public static WebView2Version WebView2Version { get; } = InitializeWebView2();
|
||||||
|
|
||||||
public static bool IsProcessElevated { get; } = LocalSetting.Get(SettingKeys.OverrideElevationRequirement, false) || Environment.IsPrivilegedProcess;
|
// ⚠️ 延迟初始化以避免循环依赖
|
||||||
|
private static readonly Lazy<bool> LazyIsProcessElevated = new(GetIsProcessElevated);
|
||||||
|
|
||||||
|
public static bool IsProcessElevated => LazyIsProcessElevated.Value;
|
||||||
|
|
||||||
// Requires main thread
|
// Requires main thread
|
||||||
public static bool IsAppNotificationEnabled { get; } = AppNotificationManager.Default.Setting is AppNotificationSetting.Enabled;
|
public static bool IsAppNotificationEnabled { get; } = CheckAppNotificationEnabled();
|
||||||
|
|
||||||
public static string? GetDisplayName()
|
public static string? GetDisplayName()
|
||||||
{
|
{
|
||||||
@@ -106,32 +108,57 @@ internal static class HutaoRuntime
|
|||||||
return string.Intern(directory);
|
return string.Intern(directory);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string InitializeDataDirectory()
|
private static bool GetIsProcessElevated()
|
||||||
{
|
{
|
||||||
// Delete the previous data folder if it exists
|
// ⚠️ 这里调用 LocalSetting 时,确保 DataDirectory 已经初始化完成
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
string previousDirectory = LocalSetting.Get(SettingKeys.PreviousDataDirectoryToDelete, string.Empty);
|
return LocalSetting.Get(SettingKeys.OverrideElevationRequirement, false) || Environment.IsPrivilegedProcess;
|
||||||
if (!string.IsNullOrEmpty(previousDirectory) && Directory.Exists(previousDirectory))
|
|
||||||
{
|
|
||||||
Directory.SetReadOnly(previousDirectory, false);
|
|
||||||
Directory.Delete(previousDirectory, true);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
finally
|
catch
|
||||||
{
|
{
|
||||||
LocalSetting.Set(SettingKeys.PreviousDataDirectoryToDelete, string.Empty);
|
// 如果读取失败,使用默认值
|
||||||
|
return Environment.IsPrivilegedProcess;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Check if the preferred path is set
|
private static string InitializeLocalCacheDirectory()
|
||||||
string currentDirectory = LocalSetting.Get(SettingKeys.DataDirectory, string.Empty);
|
{
|
||||||
|
if (PackageIdentityAdapter.HasPackageIdentity)
|
||||||
if (!string.IsNullOrEmpty(currentDirectory))
|
|
||||||
{
|
{
|
||||||
Directory.CreateDirectory(currentDirectory);
|
return Windows.Storage.ApplicationData.Current.LocalCacheFolder.Path;
|
||||||
return currentDirectory;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Unpackaged: use %LOCALAPPDATA%\Snap.Hutao\Cache
|
||||||
|
string localAppData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
|
||||||
|
const string FolderName
|
||||||
|
#if IS_ALPHA_BUILD
|
||||||
|
= "HutaoAlpha";
|
||||||
|
#elif IS_CANARY_BUILD
|
||||||
|
= "HutaoCanary";
|
||||||
|
#else
|
||||||
|
= "Hutao";
|
||||||
|
#endif
|
||||||
|
string cacheDir = Path.Combine(localAppData, FolderName, "Cache");
|
||||||
|
Directory.CreateDirectory(cacheDir);
|
||||||
|
return cacheDir;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool CheckAppNotificationEnabled()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return AppNotificationManager.Default.Setting is AppNotificationSetting.Enabled;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// In unpackaged mode, this might fail - return false
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string InitializeDataDirectory()
|
||||||
|
{
|
||||||
const string FolderName
|
const string FolderName
|
||||||
#if IS_ALPHA_BUILD
|
#if IS_ALPHA_BUILD
|
||||||
= "HutaoAlpha";
|
= "HutaoAlpha";
|
||||||
@@ -141,30 +168,43 @@ internal static class HutaoRuntime
|
|||||||
= "Hutao";
|
= "Hutao";
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
// ⚠️ 不要在这里调用 LocalSetting - 会导致循环依赖
|
||||||
|
// 先确定默认的数据目录位置
|
||||||
|
|
||||||
// Check if the old documents path exists
|
// Check if the old documents path exists
|
||||||
string myDocumentsHutaoDirectory = Path.GetFullPath(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments), FolderName));
|
string myDocumentsHutaoDirectory = Path.GetFullPath(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments), FolderName));
|
||||||
if (Directory.Exists(myDocumentsHutaoDirectory))
|
if (Directory.Exists(myDocumentsHutaoDirectory))
|
||||||
{
|
{
|
||||||
LocalSetting.Set(SettingKeys.DataDirectory, myDocumentsHutaoDirectory);
|
|
||||||
return myDocumentsHutaoDirectory;
|
return myDocumentsHutaoDirectory;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Prefer LocalApplicationData
|
// Use LocalApplicationData
|
||||||
string localApplicationData = ApplicationData.Current.LocalFolder.Path;
|
string localApplicationData;
|
||||||
string path = Path.GetFullPath(Path.Combine(localApplicationData, FolderName));
|
if (PackageIdentityAdapter.HasPackageIdentity)
|
||||||
|
{
|
||||||
|
localApplicationData = Windows.Storage.ApplicationData.Current.LocalFolder.Path;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Unpackaged: use %LOCALAPPDATA%
|
||||||
|
localApplicationData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
|
||||||
|
}
|
||||||
|
|
||||||
|
string defaultPath = Path.GetFullPath(Path.Combine(localApplicationData, FolderName));
|
||||||
|
|
||||||
|
// ⚠️ 延迟处理:在第一次使用 LocalSetting 后再检查是否有自定义路径
|
||||||
|
// 这里返回默认路径,后续通过 LocalSetting 可能会更新
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
Directory.CreateDirectory(path);
|
Directory.CreateDirectory(defaultPath);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
// FileNotFoundException | UnauthorizedAccessException
|
// FileNotFoundException | UnauthorizedAccessException
|
||||||
// We don't have enough permission
|
HutaoException.InvalidOperation($"Failed to create data folder: {defaultPath}", ex);
|
||||||
HutaoException.InvalidOperation($"Failed to create data folder: {path}", ex);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
LocalSetting.Set(SettingKeys.DataDirectory, path);
|
return defaultPath;
|
||||||
return path;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string InitializeDeviceId()
|
private static string InitializeDeviceId()
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
// Copyright (c) DGP Studio. All rights reserved.
|
// Copyright (c) DGP Studio. All rights reserved.
|
||||||
// Licensed under the MIT license.
|
// Licensed under the MIT license.
|
||||||
|
|
||||||
|
using Snap.Hutao.Core.ApplicationModel;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Security.AccessControl;
|
using System.Security.AccessControl;
|
||||||
using System.Security.Principal;
|
using System.Security.Principal;
|
||||||
using Windows.ApplicationModel;
|
|
||||||
using Windows.Storage;
|
|
||||||
|
|
||||||
namespace Snap.Hutao.Core;
|
namespace Snap.Hutao.Core;
|
||||||
|
|
||||||
@@ -13,7 +12,7 @@ internal static class InstalledLocation
|
|||||||
{
|
{
|
||||||
public static string GetAbsolutePath(string relativePath)
|
public static string GetAbsolutePath(string relativePath)
|
||||||
{
|
{
|
||||||
return Path.Combine(Package.Current.InstalledLocation.Path, relativePath);
|
return Path.Combine(PackageIdentityAdapter.AppDirectory, relativePath);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void CopyFileFromApplicationUri(string url, string path)
|
public static void CopyFileFromApplicationUri(string url, string path)
|
||||||
@@ -23,8 +22,26 @@ internal static class InstalledLocation
|
|||||||
static async Task CopyApplicationUriFileCoreAsync(string url, string path)
|
static async Task CopyApplicationUriFileCoreAsync(string url, string path)
|
||||||
{
|
{
|
||||||
await Task.CompletedTask.ConfigureAwait(ConfigureAwaitOptions.ForceYielding);
|
await Task.CompletedTask.ConfigureAwait(ConfigureAwaitOptions.ForceYielding);
|
||||||
StorageFile file = await StorageFile.GetFileFromApplicationUriAsync(url.ToUri());
|
|
||||||
using (Stream outputStream = (await file.OpenReadAsync()).AsStreamForRead())
|
Uri uri = url.ToUri();
|
||||||
|
Stream outputStream;
|
||||||
|
|
||||||
|
if (PackageIdentityAdapter.HasPackageIdentity)
|
||||||
|
{
|
||||||
|
// Packaged: use StorageFile
|
||||||
|
Windows.Storage.StorageFile file = await Windows.Storage.StorageFile.GetFileFromApplicationUriAsync(uri);
|
||||||
|
outputStream = (await file.OpenReadAsync()).AsStreamForRead();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Unpackaged: read from file system directly
|
||||||
|
// Assume ms-appx:/// points to the app directory
|
||||||
|
string localPath = uri.LocalPath.TrimStart('/');
|
||||||
|
string fullPath = Path.Combine(PackageIdentityAdapter.AppDirectory, localPath);
|
||||||
|
outputStream = File.OpenRead(fullPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
using (outputStream)
|
||||||
{
|
{
|
||||||
if (File.Exists(path))
|
if (File.Exists(path))
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -74,8 +74,15 @@ internal sealed partial class AppActivation : IAppActivation, IAppActivationActi
|
|||||||
|
|
||||||
public void ActivateAndInitialize(HutaoActivationArguments args)
|
public void ActivateAndInitialize(HutaoActivationArguments args)
|
||||||
{
|
{
|
||||||
|
#if DEBUG
|
||||||
|
Debug.WriteLine("[AppActivation] ActivateAndInitialize called");
|
||||||
|
#endif
|
||||||
|
|
||||||
if (Volatile.Read(ref isActivating) is 1)
|
if (Volatile.Read(ref isActivating) is 1)
|
||||||
{
|
{
|
||||||
|
#if DEBUG
|
||||||
|
Debug.WriteLine("[AppActivation] Already activating, returning");
|
||||||
|
#endif
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -85,21 +92,52 @@ internal sealed partial class AppActivation : IAppActivation, IAppActivationActi
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
#if DEBUG
|
||||||
|
Debug.WriteLine("[AppActivation] Starting activation process");
|
||||||
|
#endif
|
||||||
|
|
||||||
using (await activateLock.LockAsync().ConfigureAwait(false))
|
using (await activateLock.LockAsync().ConfigureAwait(false))
|
||||||
{
|
{
|
||||||
if (Interlocked.CompareExchange(ref isActivating, 1, 0) is not 0)
|
if (Interlocked.CompareExchange(ref isActivating, 1, 0) is not 0)
|
||||||
{
|
{
|
||||||
|
#if DEBUG
|
||||||
|
Debug.WriteLine("[AppActivation] Race condition detected, returning");
|
||||||
|
#endif
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#if DEBUG
|
||||||
|
Debug.WriteLine("[AppActivation] Calling UnsynchronizedHandleActivationAsync");
|
||||||
|
#endif
|
||||||
|
|
||||||
await UnsynchronizedHandleActivationAsync(args).ConfigureAwait(false);
|
await UnsynchronizedHandleActivationAsync(args).ConfigureAwait(false);
|
||||||
|
|
||||||
|
#if DEBUG
|
||||||
|
Debug.WriteLine("[AppActivation] Calling UnsynchronizedHandleInitializationAsync");
|
||||||
|
#endif
|
||||||
|
|
||||||
await UnsynchronizedHandleInitializationAsync().ConfigureAwait(false);
|
await UnsynchronizedHandleInitializationAsync().ConfigureAwait(false);
|
||||||
|
|
||||||
|
#if DEBUG
|
||||||
|
Debug.WriteLine("[AppActivation] Initialization completed successfully");
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
#if DEBUG
|
||||||
|
Debug.WriteLine($"[AppActivation] Exception during activation: {ex}");
|
||||||
|
#endif
|
||||||
|
throw;
|
||||||
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
XamlApplicationLifetime.ActivationAndInitializationCompleted = true;
|
XamlApplicationLifetime.ActivationAndInitializationCompleted = true;
|
||||||
Interlocked.Exchange(ref isActivating, 0);
|
Interlocked.Exchange(ref isActivating, 0);
|
||||||
|
|
||||||
|
#if DEBUG
|
||||||
|
Debug.WriteLine("[AppActivation] ActivationAndInitializationCompleted set to true");
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -313,16 +351,36 @@ internal sealed partial class AppActivation : IAppActivation, IAppActivationActi
|
|||||||
private async ValueTask<Window?> WaitWindowAsync<TWindow>()
|
private async ValueTask<Window?> WaitWindowAsync<TWindow>()
|
||||||
where TWindow : Window
|
where TWindow : Window
|
||||||
{
|
{
|
||||||
|
#if DEBUG
|
||||||
|
Debug.WriteLine($"[AppActivation.WaitWindowAsync] Waiting for window type: {typeof(TWindow).Name}");
|
||||||
|
#endif
|
||||||
|
|
||||||
await taskContext.SwitchToMainThreadAsync();
|
await taskContext.SwitchToMainThreadAsync();
|
||||||
|
|
||||||
|
#if DEBUG
|
||||||
|
Debug.WriteLine("[AppActivation.WaitWindowAsync] Switched to main thread");
|
||||||
|
#endif
|
||||||
|
|
||||||
if (currentXamlWindowReference.Window is not { } window)
|
if (currentXamlWindowReference.Window is not { } window)
|
||||||
{
|
{
|
||||||
|
#if DEBUG
|
||||||
|
Debug.WriteLine("[AppActivation.WaitWindowAsync] Creating new window instance");
|
||||||
|
#endif
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
window = serviceProvider.GetRequiredService<TWindow>();
|
window = serviceProvider.GetRequiredService<TWindow>();
|
||||||
|
|
||||||
|
#if DEBUG
|
||||||
|
Debug.WriteLine($"[AppActivation.WaitWindowAsync] Window created successfully: {window.GetType().Name}");
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
catch (COMException)
|
catch (COMException ex)
|
||||||
{
|
{
|
||||||
|
#if DEBUG
|
||||||
|
Debug.WriteLine($"[AppActivation.WaitWindowAsync] COMException: {ex}");
|
||||||
|
#endif
|
||||||
|
|
||||||
if (XamlApplicationLifetime.Exiting)
|
if (XamlApplicationLifetime.Exiting)
|
||||||
{
|
{
|
||||||
return default;
|
return default;
|
||||||
@@ -330,11 +388,33 @@ internal sealed partial class AppActivation : IAppActivation, IAppActivationActi
|
|||||||
|
|
||||||
throw;
|
throw;
|
||||||
}
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
#if DEBUG
|
||||||
|
Debug.WriteLine($"[AppActivation.WaitWindowAsync] Exception creating window: {ex}");
|
||||||
|
#endif
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
|
||||||
currentXamlWindowReference.Window = window;
|
currentXamlWindowReference.Window = window;
|
||||||
}
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
#if DEBUG
|
||||||
|
Debug.WriteLine($"[AppActivation.WaitWindowAsync] Using existing window: {window.GetType().Name}");
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
#if DEBUG
|
||||||
|
Debug.WriteLine("[AppActivation.WaitWindowAsync] Calling window.SwitchTo()");
|
||||||
|
#endif
|
||||||
|
|
||||||
window.SwitchTo();
|
window.SwitchTo();
|
||||||
|
|
||||||
|
#if DEBUG
|
||||||
|
Debug.WriteLine("[AppActivation.WaitWindowAsync] Window activated");
|
||||||
|
#endif
|
||||||
|
|
||||||
window.AppWindow?.MoveInZOrderAtTop();
|
window.AppWindow?.MoveInZOrderAtTop();
|
||||||
return window;
|
return window;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -66,4 +66,14 @@ internal sealed class HutaoActivationArguments
|
|||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static HutaoActivationArguments CreateDefaultLaunchArguments()
|
||||||
|
{
|
||||||
|
return new HutaoActivationArguments
|
||||||
|
{
|
||||||
|
IsRedirectTo = false,
|
||||||
|
Kind = HutaoActivationKind.Launch,
|
||||||
|
LaunchActivatedArguments = string.Empty
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,12 +1,16 @@
|
|||||||
// Copyright (c) DGP Studio. All rights reserved.
|
// Copyright (c) DGP Studio. All rights reserved.
|
||||||
// Licensed under the MIT license.
|
// Licensed under the MIT license.
|
||||||
|
|
||||||
|
using Snap.Hutao.Core.ApplicationModel;
|
||||||
using Snap.Hutao.Core.ExceptionService;
|
using Snap.Hutao.Core.ExceptionService;
|
||||||
using Snap.Hutao.Factory.Process;
|
using Snap.Hutao.Factory.Process;
|
||||||
using Snap.Hutao.Win32;
|
using Snap.Hutao.Win32;
|
||||||
using Snap.Hutao.Win32.Foundation;
|
using Snap.Hutao.Win32.Foundation;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
using System.Collections.Frozen;
|
using System.Collections.Frozen;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
|
using System.IO;
|
||||||
|
using System.Text.Json;
|
||||||
using Windows.Storage;
|
using Windows.Storage;
|
||||||
|
|
||||||
namespace Snap.Hutao.Core.Setting;
|
namespace Snap.Hutao.Core.Setting;
|
||||||
@@ -36,40 +40,20 @@ internal static class LocalSetting
|
|||||||
typeof(ApplicationDataCompositeValue)
|
typeof(ApplicationDataCompositeValue)
|
||||||
];
|
];
|
||||||
|
|
||||||
private static readonly ApplicationDataContainer Container = ApplicationData.Current.LocalSettings;
|
private static readonly Lazy<ISettingStorage> LazyStorage = new(CreateStorage);
|
||||||
|
|
||||||
|
private static ISettingStorage Storage => LazyStorage.Value;
|
||||||
|
|
||||||
public static T Get<T>(string key, T defaultValue = default!)
|
public static T Get<T>(string key, T defaultValue = default!)
|
||||||
{
|
{
|
||||||
Debug.Assert(SupportedTypes.Contains(typeof(T)));
|
Debug.Assert(SupportedTypes.Contains(typeof(T)));
|
||||||
if (Container.Values.TryGetValue(key, out object? value))
|
return Storage.Get(key, defaultValue);
|
||||||
{
|
|
||||||
// unbox the value
|
|
||||||
return value is null ? defaultValue : (T)value;
|
|
||||||
}
|
|
||||||
|
|
||||||
Set(key, defaultValue);
|
|
||||||
return defaultValue;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void Set<T>(string key, T value)
|
public static void Set<T>(string key, T value)
|
||||||
{
|
{
|
||||||
Debug.Assert(SupportedTypes.Contains(typeof(T)));
|
Debug.Assert(SupportedTypes.Contains(typeof(T)));
|
||||||
|
Storage.Set(key, value);
|
||||||
try
|
|
||||||
{
|
|
||||||
Container.Values[key] = value;
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
// 状态管理器无法写入设置
|
|
||||||
if (HutaoNative.IsWin32(ex.HResult, WIN32_ERROR.ERROR_STATE_WRITE_SETTING_FAILED))
|
|
||||||
{
|
|
||||||
HutaoNative.Instance.ShowErrorMessage(ex.Message, ExceptionFormat.Format(ex));
|
|
||||||
ProcessFactory.KillCurrent();
|
|
||||||
}
|
|
||||||
|
|
||||||
throw;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void SetIf<T>(bool condition, string key, T value)
|
public static void SetIf<T>(bool condition, string key, T value)
|
||||||
@@ -103,4 +87,299 @@ internal static class LocalSetting
|
|||||||
Set(key, newValue);
|
Set(key, newValue);
|
||||||
return oldValue;
|
return oldValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static ISettingStorage CreateStorage()
|
||||||
|
{
|
||||||
|
if (PackageIdentityAdapter.HasPackageIdentity)
|
||||||
|
{
|
||||||
|
return new PackagedSettingStorage();
|
||||||
|
}
|
||||||
|
|
||||||
|
return new UnpackagedSettingStorage();
|
||||||
|
}
|
||||||
|
|
||||||
|
private interface ISettingStorage
|
||||||
|
{
|
||||||
|
T Get<T>(string key, T defaultValue);
|
||||||
|
void Set<T>(string key, T value);
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class PackagedSettingStorage : ISettingStorage
|
||||||
|
{
|
||||||
|
private readonly ApplicationDataContainer container = ApplicationData.Current.LocalSettings;
|
||||||
|
|
||||||
|
public T Get<T>(string key, T defaultValue)
|
||||||
|
{
|
||||||
|
if (container.Values.TryGetValue(key, out object? value))
|
||||||
|
{
|
||||||
|
// unbox the value
|
||||||
|
return value is null ? defaultValue : (T)value;
|
||||||
|
}
|
||||||
|
|
||||||
|
Set(key, defaultValue);
|
||||||
|
return defaultValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Set<T>(string key, T value)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
container.Values[key] = value;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
// 状态管理器无法写入设置
|
||||||
|
if (HutaoNative.IsWin32(ex.HResult, WIN32_ERROR.ERROR_STATE_WRITE_SETTING_FAILED))
|
||||||
|
{
|
||||||
|
HutaoNative.Instance.ShowErrorMessage(ex.Message, ExceptionFormat.Format(ex));
|
||||||
|
ProcessFactory.KillCurrent();
|
||||||
|
}
|
||||||
|
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class UnpackagedSettingStorage : ISettingStorage
|
||||||
|
{
|
||||||
|
private readonly string settingsFilePath;
|
||||||
|
private readonly ConcurrentDictionary<string, object?> cache = new();
|
||||||
|
private readonly object fileLock = new();
|
||||||
|
private readonly JsonSerializerOptions jsonOptions = new()
|
||||||
|
{
|
||||||
|
WriteIndented = true,
|
||||||
|
Converters =
|
||||||
|
{
|
||||||
|
new ApplicationDataCompositeValueJsonConverter(),
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
public UnpackagedSettingStorage()
|
||||||
|
{
|
||||||
|
string localAppData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
|
||||||
|
const string FolderName
|
||||||
|
#if IS_ALPHA_BUILD
|
||||||
|
= "HutaoAlpha";
|
||||||
|
#elif IS_CANARY_BUILD
|
||||||
|
= "HutaoCanary";
|
||||||
|
#else
|
||||||
|
= "Hutao";
|
||||||
|
#endif
|
||||||
|
string settingsDir = Path.Combine(localAppData, FolderName, "Settings");
|
||||||
|
Directory.CreateDirectory(settingsDir);
|
||||||
|
settingsFilePath = Path.Combine(settingsDir, "LocalSettings.json");
|
||||||
|
|
||||||
|
LoadFromFile();
|
||||||
|
}
|
||||||
|
|
||||||
|
public T Get<T>(string key, T defaultValue)
|
||||||
|
{
|
||||||
|
if (cache.TryGetValue(key, out object? value))
|
||||||
|
{
|
||||||
|
if (value is null)
|
||||||
|
{
|
||||||
|
return defaultValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle JSON deserialization for complex types
|
||||||
|
if (value is JsonElement jsonElement)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// ⚠️ 特殊处理:JSON 数字类型转换
|
||||||
|
Type targetType = typeof(T);
|
||||||
|
|
||||||
|
if (jsonElement.ValueKind == JsonValueKind.Number)
|
||||||
|
{
|
||||||
|
if (targetType == typeof(int))
|
||||||
|
{
|
||||||
|
return (T)(object)jsonElement.GetInt32();
|
||||||
|
}
|
||||||
|
if (targetType == typeof(long))
|
||||||
|
{
|
||||||
|
return (T)(object)jsonElement.GetInt64();
|
||||||
|
}
|
||||||
|
if (targetType == typeof(short))
|
||||||
|
{
|
||||||
|
return (T)(object)jsonElement.GetInt16();
|
||||||
|
}
|
||||||
|
if (targetType == typeof(byte))
|
||||||
|
{
|
||||||
|
return (T)(object)jsonElement.GetByte();
|
||||||
|
}
|
||||||
|
if (targetType == typeof(uint))
|
||||||
|
{
|
||||||
|
return (T)(object)jsonElement.GetUInt32();
|
||||||
|
}
|
||||||
|
if (targetType == typeof(ulong))
|
||||||
|
{
|
||||||
|
return (T)(object)jsonElement.GetUInt64();
|
||||||
|
}
|
||||||
|
if (targetType == typeof(ushort))
|
||||||
|
{
|
||||||
|
return (T)(object)jsonElement.GetUInt16();
|
||||||
|
}
|
||||||
|
if (targetType == typeof(float))
|
||||||
|
{
|
||||||
|
return (T)(object)jsonElement.GetSingle();
|
||||||
|
}
|
||||||
|
if (targetType == typeof(double))
|
||||||
|
{
|
||||||
|
return (T)(object)jsonElement.GetDouble();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 其他类型使用标准反序列化
|
||||||
|
return jsonElement.Deserialize<T>(jsonOptions) ?? defaultValue;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return defaultValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ⚠️ 如果是直接从 cache 读取的值,也可能需要类型转换
|
||||||
|
// 例如:double -> int
|
||||||
|
if (value is double doubleValue)
|
||||||
|
{
|
||||||
|
Type targetType = typeof(T);
|
||||||
|
if (targetType == typeof(int))
|
||||||
|
{
|
||||||
|
return (T)(object)(int)doubleValue;
|
||||||
|
}
|
||||||
|
if (targetType == typeof(long))
|
||||||
|
{
|
||||||
|
return (T)(object)(long)doubleValue;
|
||||||
|
}
|
||||||
|
if (targetType == typeof(short))
|
||||||
|
{
|
||||||
|
return (T)(object)(short)doubleValue;
|
||||||
|
}
|
||||||
|
if (targetType == typeof(byte))
|
||||||
|
{
|
||||||
|
return (T)(object)(byte)doubleValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (T)value;
|
||||||
|
}
|
||||||
|
|
||||||
|
Set(key, defaultValue);
|
||||||
|
return defaultValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Set<T>(string key, T value)
|
||||||
|
{
|
||||||
|
cache[key] = value;
|
||||||
|
SaveToFile();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void LoadFromFile()
|
||||||
|
{
|
||||||
|
lock (fileLock)
|
||||||
|
{
|
||||||
|
if (!File.Exists(settingsFilePath))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
string json = File.ReadAllText(settingsFilePath);
|
||||||
|
Dictionary<string, JsonElement>? data = JsonSerializer.Deserialize<Dictionary<string, JsonElement>>(json, jsonOptions);
|
||||||
|
if (data is not null)
|
||||||
|
{
|
||||||
|
foreach ((string key, JsonElement value) in data)
|
||||||
|
{
|
||||||
|
cache[key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// If file is corrupted, start fresh
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SaveToFile()
|
||||||
|
{
|
||||||
|
lock (fileLock)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Convert cache to serializable dictionary
|
||||||
|
Dictionary<string, object?> serializableData = new(cache);
|
||||||
|
string json = JsonSerializer.Serialize(serializableData, jsonOptions);
|
||||||
|
File.WriteAllText(settingsFilePath, json);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Debug.WriteLine($"Failed to save settings: {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Converter for ApplicationDataCompositeValue
|
||||||
|
private sealed class ApplicationDataCompositeValueJsonConverter : System.Text.Json.Serialization.JsonConverter<ApplicationDataCompositeValue>
|
||||||
|
{
|
||||||
|
public override ApplicationDataCompositeValue? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||||
|
{
|
||||||
|
if (reader.TokenType != JsonTokenType.StartObject)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
ApplicationDataCompositeValue composite = new();
|
||||||
|
while (reader.Read())
|
||||||
|
{
|
||||||
|
if (reader.TokenType == JsonTokenType.EndObject)
|
||||||
|
{
|
||||||
|
return composite;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (reader.TokenType != JsonTokenType.PropertyName)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
string? key = reader.GetString();
|
||||||
|
reader.Read();
|
||||||
|
|
||||||
|
if (key is not null)
|
||||||
|
{
|
||||||
|
composite[key] = reader.TokenType switch
|
||||||
|
{
|
||||||
|
JsonTokenType.String => reader.GetString(),
|
||||||
|
JsonTokenType.Number => reader.TryGetInt64(out long l) ? l : reader.GetDouble(),
|
||||||
|
JsonTokenType.True => true,
|
||||||
|
JsonTokenType.False => false,
|
||||||
|
_ => null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return composite;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Write(Utf8JsonWriter writer, ApplicationDataCompositeValue value, JsonSerializerOptions options)
|
||||||
|
{
|
||||||
|
writer.WriteStartObject();
|
||||||
|
foreach ((string key, object? val) in value)
|
||||||
|
{
|
||||||
|
writer.WritePropertyName(key);
|
||||||
|
if (val is null)
|
||||||
|
{
|
||||||
|
writer.WriteNullValue();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
JsonSerializer.Serialize(writer, val, val.GetType(), options);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
writer.WriteEndObject();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -147,6 +147,27 @@ internal sealed class ProcessFactory
|
|||||||
{
|
{
|
||||||
string repoDirectory = HutaoRuntime.GetDataRepositoryDirectory();
|
string repoDirectory = HutaoRuntime.GetDataRepositoryDirectory();
|
||||||
string fullTrustFilePath = Path.Combine(repoDirectory, "Snap.ContentDelivery", "Snap.Hutao.FullTrust.exe");
|
string fullTrustFilePath = Path.Combine(repoDirectory, "Snap.ContentDelivery", "Snap.Hutao.FullTrust.exe");
|
||||||
|
|
||||||
|
// Check if FullTrust executable exists - if not, fallback to normal admin mode
|
||||||
|
if (!File.Exists(fullTrustFilePath))
|
||||||
|
{
|
||||||
|
string errorMessage = $"""
|
||||||
|
Island 功能需要的 FullTrust 进程文件不存在,将使用普通管理员模式启动游戏。
|
||||||
|
预期路径:{fullTrustFilePath}
|
||||||
|
|
||||||
|
原因:ContentDelivery 仓库尚未下载或初始化失败(常见于非打包模式首次运行)
|
||||||
|
|
||||||
|
Island 功能将不可用,但游戏可以正常启动。
|
||||||
|
等待仓库下载完成后可重新尝试使用 Island 功能。
|
||||||
|
""";
|
||||||
|
|
||||||
|
// Capture as breadcrumb instead of exception
|
||||||
|
SentrySdk.AddBreadcrumb(errorMessage, category: "process.fulltrust", level: Sentry.BreadcrumbLevel.Warning);
|
||||||
|
|
||||||
|
// Fallback to normal admin mode - Island features will not work but game can launch
|
||||||
|
return CreateUsingShellExecuteRunAs(arguments, fileName, workingDirectory);
|
||||||
|
}
|
||||||
|
|
||||||
StartUsingShellExecuteRunAs(fullTrustFilePath);
|
StartUsingShellExecuteRunAs(fullTrustFilePath);
|
||||||
|
|
||||||
FullTrustProcessStartInfoRequest request = new()
|
FullTrustProcessStartInfoRequest request = new()
|
||||||
|
|||||||
@@ -77,21 +77,48 @@ internal sealed class GameIslandInterop : IGameIslandInterop
|
|||||||
{
|
{
|
||||||
nint handle = accessor.SafeMemoryMappedViewHandle.DangerousGetHandle();
|
nint handle = accessor.SafeMemoryMappedViewHandle.DangerousGetHandle();
|
||||||
InitializeIslandEnvironment(handle, context.LaunchOptions, context.IsOversea);
|
InitializeIslandEnvironment(handle, context.LaunchOptions, context.IsOversea);
|
||||||
|
|
||||||
if (!resume)
|
if (!resume)
|
||||||
{
|
{
|
||||||
if (context.Process is not FullTrustProcess fullTrustProcess)
|
|
||||||
{
|
|
||||||
throw HutaoException.InvalidOperation("Process is not full trust");
|
|
||||||
}
|
|
||||||
|
|
||||||
ArgumentException.ThrowIfNullOrEmpty(islandPath);
|
ArgumentException.ThrowIfNullOrEmpty(islandPath);
|
||||||
if (!File.Exists(islandPath))
|
if (!File.Exists(islandPath))
|
||||||
{
|
{
|
||||||
throw HutaoException.InvalidOperation(SH.ServiceGameIslandTargetVersionFileNotExists);
|
throw HutaoException.InvalidOperation(SH.ServiceGameIslandTargetVersionFileNotExists);
|
||||||
}
|
}
|
||||||
|
|
||||||
fullTrustProcess.LoadLibrary(FullTrustLoadLibraryRequest.Create("Island", islandPath));
|
// Support both FullTrust and normal admin mode
|
||||||
fullTrustProcess.ResumeMainThread();
|
if (context.Process is FullTrustProcess fullTrustProcess)
|
||||||
|
{
|
||||||
|
// Use FullTrust process for injection (suspended process)
|
||||||
|
fullTrustProcess.LoadLibrary(FullTrustLoadLibraryRequest.Create("Island", islandPath));
|
||||||
|
fullTrustProcess.ResumeMainThread();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Use native injection for normal admin mode
|
||||||
|
// The process was already started by CreateUsingShellExecuteRunAs
|
||||||
|
// Just inject the DLL into the running process
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Wait a bit for process to initialize
|
||||||
|
// await Task.Delay(500, token).ConfigureAwait(false);
|
||||||
|
// ⚠️此处需要更多调查
|
||||||
|
|
||||||
|
// Inject using RemoteThread
|
||||||
|
DllInjectionUtilities.InjectUsingRemoteThread(islandPath, context.Process.Id);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
// Log the injection failure but don't crash - game can still run
|
||||||
|
SentrySdk.AddBreadcrumb(
|
||||||
|
$"Island DLL injection failed: {ex.Message}",
|
||||||
|
category: "island.injection",
|
||||||
|
level: Sentry.BreadcrumbLevel.Error);
|
||||||
|
|
||||||
|
// Re-throw to let the caller handle it
|
||||||
|
throw HutaoException.Throw($"Island DLL 注入失败: {ex.Message}", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await PeriodicUpdateIslandEnvironmentAsync(context, handle, token).ConfigureAwait(false);
|
await PeriodicUpdateIslandEnvironmentAsync(context, handle, token).ConfigureAwait(false);
|
||||||
@@ -214,4 +241,4 @@ internal sealed class GameIslandInterop : IGameIslandInterop
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ internal sealed class GameProcessFactory
|
|||||||
string gameFilePath = context.FileSystem.GameFilePath;
|
string gameFilePath = context.FileSystem.GameFilePath;
|
||||||
string gameDirectory = context.FileSystem.GameDirectory;
|
string gameDirectory = context.FileSystem.GameDirectory;
|
||||||
|
|
||||||
|
// ProcessFactory.CreateUsingFullTrustSuspended will automatically fallback to normal mode if FullTrust.exe is missing
|
||||||
return launchOptions.IsIslandEnabled.Value
|
return launchOptions.IsIslandEnabled.Value
|
||||||
? ProcessFactory.CreateUsingFullTrustSuspended(commandLine, gameFilePath, gameDirectory)
|
? ProcessFactory.CreateUsingFullTrustSuspended(commandLine, gameFilePath, gameDirectory)
|
||||||
: ProcessFactory.CreateUsingShellExecuteRunAs(commandLine, gameFilePath, gameDirectory);
|
: ProcessFactory.CreateUsingShellExecuteRunAs(commandLine, gameFilePath, gameDirectory);
|
||||||
|
|||||||
@@ -2,17 +2,17 @@
|
|||||||
// Licensed under the MIT license.
|
// Licensed under the MIT license.
|
||||||
|
|
||||||
using Snap.Hutao.Core.IO.Hashing;
|
using Snap.Hutao.Core.IO.Hashing;
|
||||||
|
using Snap.Hutao.Core.Setting;
|
||||||
using Snap.Hutao.Web.Hutao;
|
using Snap.Hutao.Web.Hutao;
|
||||||
using System.Collections.Immutable;
|
using System.Collections.Immutable;
|
||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
using System.Security.Cryptography;
|
using System.Security.Cryptography;
|
||||||
using Windows.Storage;
|
|
||||||
|
|
||||||
namespace Snap.Hutao.Service.Git;
|
namespace Snap.Hutao.Service.Git;
|
||||||
|
|
||||||
internal static class RepositoryAffinity
|
internal static class RepositoryAffinity
|
||||||
{
|
{
|
||||||
private static readonly ApplicationDataContainer RepositoryContainer = ApplicationData.Current.LocalSettings.CreateContainer("RepositoryAffinity", ApplicationDataCreateDisposition.Always);
|
private const string RepositoryAffinityPrefix = "RepositoryAffinity::";
|
||||||
private static readonly Lock SyncRoot = new();
|
private static readonly Lock SyncRoot = new();
|
||||||
|
|
||||||
public static ImmutableArray<GitRepository> Sort(ImmutableArray<GitRepository> repositories)
|
public static ImmutableArray<GitRepository> Sort(ImmutableArray<GitRepository> repositories)
|
||||||
@@ -23,9 +23,11 @@ internal static class RepositoryAffinity
|
|||||||
for (int i = 0; i < repositories.Length; i++)
|
for (int i = 0; i < repositories.Length; i++)
|
||||||
{
|
{
|
||||||
GitRepository repository = repositories[i];
|
GitRepository repository = repositories[i];
|
||||||
ApplicationDataContainer container = RepositoryContainer.CreateContainer(repository.Name, ApplicationDataCreateDisposition.Always);
|
string key = GetSettingKey(repository.Name, repository.HttpsUrl.OriginalString);
|
||||||
string key = Hash.ToHexString(HashAlgorithmName.SHA256, repository.HttpsUrl.OriginalString.ToUpperInvariant());
|
|
||||||
counts[i] = container.Values[key] is int c ? c : 0;
|
// 对读取值做下限保护,确保排序使用的是非负失败计数
|
||||||
|
int raw = LocalSetting.Get(key, 0);
|
||||||
|
counts[i] = Math.Max(0, raw);
|
||||||
}
|
}
|
||||||
|
|
||||||
Array.Sort(counts, ImmutableCollectionsMarshal.AsArray(repositories));
|
Array.Sort(counts, ImmutableCollectionsMarshal.AsArray(repositories));
|
||||||
@@ -42,10 +44,14 @@ internal static class RepositoryAffinity
|
|||||||
{
|
{
|
||||||
lock (SyncRoot)
|
lock (SyncRoot)
|
||||||
{
|
{
|
||||||
ApplicationDataContainer container = RepositoryContainer.CreateContainer(name, ApplicationDataCreateDisposition.Always);
|
string key = GetSettingKey(name, url);
|
||||||
string key = Hash.ToHexString(HashAlgorithmName.SHA256, url.ToUpperInvariant());
|
int currentCount = LocalSetting.Get(key, 0);
|
||||||
object box = container.Values[key];
|
|
||||||
container.Values[key] = box is int count ? unchecked(count + 1) : 1;
|
// 防止整数上溢:当已到达 int.MaxValue 时不再自增
|
||||||
|
if (currentCount < int.MaxValue)
|
||||||
|
{
|
||||||
|
LocalSetting.Set(key, currentCount + 1);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -58,10 +64,20 @@ internal static class RepositoryAffinity
|
|||||||
{
|
{
|
||||||
lock (SyncRoot)
|
lock (SyncRoot)
|
||||||
{
|
{
|
||||||
ApplicationDataContainer container = RepositoryContainer.CreateContainer(name, ApplicationDataCreateDisposition.Always);
|
string key = GetSettingKey(name, url);
|
||||||
string key = Hash.ToHexString(HashAlgorithmName.SHA256, url.ToUpperInvariant());
|
int currentCount = LocalSetting.Get(key, 0);
|
||||||
object box = container.Values[key];
|
|
||||||
container.Values[key] = box is int count ? unchecked(count - 1) : 0;
|
// 失败次数不允许小于 0,避免出现负数或整型下溢
|
||||||
|
if (currentCount > 0)
|
||||||
|
{
|
||||||
|
LocalSetting.Set(key, currentCount - 1);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
private static string GetSettingKey(string name, string url)
|
||||||
|
{
|
||||||
|
string urlHash = Hash.ToHexString(HashAlgorithmName.SHA256, url.ToUpperInvariant());
|
||||||
|
return $"{RepositoryAffinityPrefix}{name}::{urlHash}";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<OutputType>WinExe</OutputType>
|
<OutputType>WinExe</OutputType>
|
||||||
<TargetFramework>net10.0-windows10.0.26100.0</TargetFramework>
|
<TargetFramework>net10.0-windows10.0.26100.0</TargetFramework>
|
||||||
@@ -47,12 +47,18 @@
|
|||||||
<AppxPackageSigningEnabled>True</AppxPackageSigningEnabled>
|
<AppxPackageSigningEnabled>True</AppxPackageSigningEnabled>
|
||||||
<PackageCertificateKeyFile>Snap.Hutao_TemporaryKey.pfx</PackageCertificateKeyFile>
|
<PackageCertificateKeyFile>Snap.Hutao_TemporaryKey.pfx</PackageCertificateKeyFile>
|
||||||
<AppxPackageSigningTimestampDigestAlgorithm>SHA256</AppxPackageSigningTimestampDigestAlgorithm>
|
<AppxPackageSigningTimestampDigestAlgorithm>SHA256</AppxPackageSigningTimestampDigestAlgorithm>
|
||||||
|
<!-- 关闭AppX打包 -->
|
||||||
|
<AppxPackage>false</AppxPackage>
|
||||||
|
<!-- 不设置打包类型 -->
|
||||||
|
<WindowsPackageType>None</WindowsPackageType>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<None Remove="Package.appxmanifest" />
|
||||||
|
<None Remove="Package.development.appxmanifest" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Manifest Include="$(ApplicationManifest)" />
|
<Manifest Include="$(ApplicationManifest)" />
|
||||||
<AppxManifest Include="Package.appxmanifest" Condition="'$(ConfigurationName)'!='Debug'" />
|
|
||||||
<AppxManifest Include="Package.development.appxmanifest" Condition="'$(ConfigurationName)'=='Debug'" />
|
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<Target Name="DeleteLibsAfterBuild" AfterTargets="Build">
|
<Target Name="DeleteLibsAfterBuild" AfterTargets="Build">
|
||||||
@@ -241,15 +247,15 @@
|
|||||||
<PackageReference Include="CommunityToolkit.WinUI.Controls.SettingsControls" Version="8.2.251105-build.1544" />
|
<PackageReference Include="CommunityToolkit.WinUI.Controls.SettingsControls" Version="8.2.251105-build.1544" />
|
||||||
<PackageReference Include="CommunityToolkit.WinUI.Converters" Version="8.2.251105-build.1544" />
|
<PackageReference Include="CommunityToolkit.WinUI.Converters" Version="8.2.251105-build.1544" />
|
||||||
<PackageReference Include="CommunityToolkit.WinUI.Media" Version="8.2.251105-build.1544" />
|
<PackageReference Include="CommunityToolkit.WinUI.Media" Version="8.2.251105-build.1544" />
|
||||||
<PackageReference Include="Google.Protobuf" Version="3.33.0" />
|
<PackageReference Include="Google.Protobuf" Version="3.33.1" />
|
||||||
<PackageReference Include="JetBrains.Annotations" Version="2025.2.2" />
|
<PackageReference Include="JetBrains.Annotations" Version="2025.2.2" />
|
||||||
<PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="4.14.0">
|
<PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="4.14.0">
|
||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Scripting" Version="4.14.0" />
|
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Scripting" Version="5.0.0" />
|
||||||
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="4.14.0" />
|
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="5.0.0" />
|
||||||
<PackageReference Include="Microsoft.CodeAnalysis.Workspaces.MSBuild" Version="4.14.0" />
|
<PackageReference Include="Microsoft.CodeAnalysis.Workspaces.MSBuild" Version="5.0.0" />
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.0">
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.0">
|
||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
@@ -265,7 +271,7 @@
|
|||||||
<PackageReference Include="Microsoft.VisualStudio.Validation" Version="17.13.22" />
|
<PackageReference Include="Microsoft.VisualStudio.Validation" Version="17.13.22" />
|
||||||
<PackageReference Include="Microsoft.Web.WebView2" Version="1.0.3595.46" />
|
<PackageReference Include="Microsoft.Web.WebView2" Version="1.0.3595.46" />
|
||||||
<PackageReference Include="Microsoft.Windows.CsWinRT" Version="2.3.0-prerelease.251015.2" />
|
<PackageReference Include="Microsoft.Windows.CsWinRT" Version="2.3.0-prerelease.251015.2" />
|
||||||
<PackageReference Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.26100.6901" />
|
<PackageReference Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.26100.7175" />
|
||||||
<PackageReference Include="Microsoft.WindowsAppSDK" Version="1.8.251106002" />
|
<PackageReference Include="Microsoft.WindowsAppSDK" Version="1.8.251106002" />
|
||||||
<PackageReference Include="QRCoder" Version="1.7.0" />
|
<PackageReference Include="QRCoder" Version="1.7.0" />
|
||||||
<PackageReference Include="Quartz.Extensions.DependencyInjection" Version="3.15.1" />
|
<PackageReference Include="Quartz.Extensions.DependencyInjection" Version="3.15.1" />
|
||||||
@@ -274,10 +280,6 @@
|
|||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
<PackageReference Include="Snap.Hutao.Deployment.Runtime" Version="2.5.12">
|
|
||||||
<PrivateAssets>all</PrivateAssets>
|
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
|
||||||
</PackageReference>
|
|
||||||
<PackageReference Include="Snap.Hutao.Elevated.Launcher.Runtime" Version="1.3.0">
|
<PackageReference Include="Snap.Hutao.Elevated.Launcher.Runtime" Version="1.3.0">
|
||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
|
||||||
|
|||||||
@@ -553,7 +553,7 @@
|
|||||||
</ScrollViewer>
|
</ScrollViewer>
|
||||||
</PivotItem>
|
</PivotItem>
|
||||||
|
|
||||||
<PivotItem Header="{shuxm:ResourceString Name=ViewPageLaunchGameIslandOptionsHeader}" IsEnabled="False">
|
<PivotItem Header="{shuxm:ResourceString Name=ViewPageLaunchGameIslandOptionsHeader}">
|
||||||
<Border
|
<Border
|
||||||
Margin="16"
|
Margin="16"
|
||||||
Padding="0"
|
Padding="0"
|
||||||
|
|||||||
@@ -105,7 +105,7 @@ internal static class StaticResource
|
|||||||
|
|
||||||
foreach ((string key, object value) in map)
|
foreach ((string key, object value) in map)
|
||||||
{
|
{
|
||||||
if ((int)value < (int)LatestResourceVersionMap[key])
|
if (Convert.ToInt32(value) < Convert.ToInt32(LatestResourceVersionMap[key]))
|
||||||
{
|
{
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -120,7 +120,7 @@ internal static class StaticResource
|
|||||||
ApplicationDataCompositeValue map = LocalSetting.Get(ContractMap, DefaultResourceVersionMap);
|
ApplicationDataCompositeValue map = LocalSetting.Get(ContractMap, DefaultResourceVersionMap);
|
||||||
foreach ((string key, object value) in LatestResourceVersionMap)
|
foreach ((string key, object value) in LatestResourceVersionMap)
|
||||||
{
|
{
|
||||||
if (!map.TryGetValue(key, out object current) || (int)value > (int)current)
|
if (!map.TryGetValue(key, out object current) || Convert.ToInt32(value) > Convert.ToInt32(current))
|
||||||
{
|
{
|
||||||
result.Add(key);
|
result.Add(key);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
|
|
||||||
using Microsoft.UI.Xaml.Controls;
|
using Microsoft.UI.Xaml.Controls;
|
||||||
using Snap.Hutao.Core;
|
using Snap.Hutao.Core;
|
||||||
|
using Snap.Hutao.Core.ApplicationModel;
|
||||||
using Snap.Hutao.Core.Setting;
|
using Snap.Hutao.Core.Setting;
|
||||||
using Snap.Hutao.Factory.ContentDialog;
|
using Snap.Hutao.Factory.ContentDialog;
|
||||||
using Snap.Hutao.Factory.Picker;
|
using Snap.Hutao.Factory.Picker;
|
||||||
@@ -71,8 +72,18 @@ internal sealed class SettingStorageSetDataFolderOperation
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
Directory.SetReadOnly(oldFolderPath, false);
|
Directory.SetReadOnly(oldFolderPath, false);
|
||||||
StorageFolder oldFolder = await StorageFolder.GetFolderFromPathAsync(oldFolderPath);
|
|
||||||
await oldFolder.CopyAsync(newFolderPath).ConfigureAwait(false);
|
if (PackageIdentityAdapter.HasPackageIdentity)
|
||||||
|
{
|
||||||
|
// Packaged: use StorageFolder API
|
||||||
|
StorageFolder oldFolder = await StorageFolder.GetFolderFromPathAsync(oldFolderPath);
|
||||||
|
await oldFolder.CopyAsync(newFolderPath).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Unpackaged: use standard file I/O
|
||||||
|
await CopyDirectoryAsync(oldFolderPath, newFolderPath).ConfigureAwait(false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@@ -84,4 +95,22 @@ internal sealed class SettingStorageSetDataFolderOperation
|
|||||||
LocalSetting.Set(SettingKeys.DataDirectory, newFolderPath);
|
LocalSetting.Set(SettingKeys.DataDirectory, newFolderPath);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static async ValueTask CopyDirectoryAsync(string sourceDir, string destDir)
|
||||||
|
{
|
||||||
|
await Task.Run(() =>
|
||||||
|
{
|
||||||
|
// Create all directories
|
||||||
|
foreach (string dirPath in Directory.GetDirectories(sourceDir, "*", SearchOption.AllDirectories))
|
||||||
|
{
|
||||||
|
Directory.CreateDirectory(dirPath.Replace(sourceDir, destDir));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy all files
|
||||||
|
foreach (string filePath in Directory.GetFiles(sourceDir, "*", SearchOption.AllDirectories))
|
||||||
|
{
|
||||||
|
File.Copy(filePath, filePath.Replace(sourceDir, destDir), true);
|
||||||
|
}
|
||||||
|
}).ConfigureAwait(false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -5,6 +5,7 @@ using CommunityToolkit.Mvvm.ComponentModel;
|
|||||||
using Microsoft.UI.Xaml.Controls;
|
using Microsoft.UI.Xaml.Controls;
|
||||||
using Microsoft.Windows.AppLifecycle;
|
using Microsoft.Windows.AppLifecycle;
|
||||||
using Snap.Hutao.Core;
|
using Snap.Hutao.Core;
|
||||||
|
using Snap.Hutao.Core.ApplicationModel;
|
||||||
using Snap.Hutao.Core.Caching;
|
using Snap.Hutao.Core.Caching;
|
||||||
using Snap.Hutao.Core.Logging;
|
using Snap.Hutao.Core.Logging;
|
||||||
using Snap.Hutao.Core.Setting;
|
using Snap.Hutao.Core.Setting;
|
||||||
@@ -135,7 +136,18 @@ internal sealed partial class SettingStorageViewModel : Abstraction.ViewModel
|
|||||||
// TODO: prompt user that restart will be non-elevated
|
// TODO: prompt user that restart will be non-elevated
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
AppInstance.Restart(string.Empty);
|
if (PackageIdentityAdapter.HasPackageIdentity)
|
||||||
|
{
|
||||||
|
AppInstance.Restart(string.Empty);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Unpackaged: manually restart the process
|
||||||
|
string exePath = System.Diagnostics.Process.GetCurrentProcess().MainModule?.FileName
|
||||||
|
?? throw new InvalidOperationException("Cannot get process path");
|
||||||
|
System.Diagnostics.Process.Start(exePath);
|
||||||
|
Snap.Hutao.Factory.Process.ProcessFactory.KillCurrent();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
catch (COMException ex)
|
catch (COMException ex)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,11 +1,34 @@
|
|||||||
|
// Copyright (c) DGP Studio. All rights reserved.
|
||||||
|
// Licensed under the MIT license.
|
||||||
|
|
||||||
namespace Snap.Hutao.Web.Hoyolab.DataSigning;
|
namespace Snap.Hutao.Web.Hoyolab.DataSigning;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Salt constants for data signing
|
||||||
|
/// Values are obtained from https://github.com/UIGF-org/Hoyolab.Salt
|
||||||
|
/// This file should normally be generated by Snap.Hutao.SourceGeneration.Automation.SaltConstantGenerator
|
||||||
|
/// But is provided manually when the generator fails to fetch values from the network.
|
||||||
|
///
|
||||||
|
/// IMPORTANT: For local builds, you must manually obtain salt values from:
|
||||||
|
/// https://github.com/UIGF-org/Hoyolab.Salt
|
||||||
|
/// </summary>
|
||||||
|
|
||||||
internal static class SaltConstants
|
internal static class SaltConstants
|
||||||
{
|
{
|
||||||
|
// Version numbers - Update these according to the current miHoYo app versions
|
||||||
public const string CNVersion = "2.95.1";
|
public const string CNVersion = "2.95.1";
|
||||||
public const string OSVersion = "2.54.0";
|
public const string OSVersion = "2.54.0";
|
||||||
|
|
||||||
|
// Salt keys for Chinese (CN) server
|
||||||
|
// These are placeholder values - MUST be replaced with actual values from UIGF-org/Hoyolab.Salt
|
||||||
public const string CNK2 = "sfYPEgpxkOe1I3XVMLdwp1Lyt9ORgZsq";
|
public const string CNK2 = "sfYPEgpxkOe1I3XVMLdwp1Lyt9ORgZsq";
|
||||||
public const string CNLK2 = "sidQFEglajEz7FA0Aj7HQPV88zpf17SO";
|
public const string CNLK2 = "sidQFEglajEz7FA0Aj7HQPV88zpf17SO";
|
||||||
|
|
||||||
|
// Salt keys for Overseas (OS) server
|
||||||
public const string OSK2 = "599uqkwc0dlqu3h6epzjzfhgyyrd44ae";
|
public const string OSK2 = "599uqkwc0dlqu3h6epzjzfhgyyrd44ae";
|
||||||
public const string OSLK2 = "rk4xg2hakoi26nljpr099fv9fck1ah10";
|
public const string OSLK2 = "rk4xg2hakoi26nljpr099fv9fck1ah10";
|
||||||
|
|
||||||
|
// Note: The actual salt values are security-sensitive and should not be committed
|
||||||
|
// to public repositories. For local builds, obtain them from the UIGF organization
|
||||||
|
// and replace the placeholders above.
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user