Compare commits

...

8 Commits

Author SHA1 Message Date
Mistmoon 6c7119ced4
Merge pull request #153 from Frieren-Violet/dev
fix Windows App SDK
2025-04-05 17:21:45 +08:00
Zhengxi Yao 9a354ed2fc
Merge branch 'eesast:dev' into dev 2025-04-03 20:35:34 +08:00
frieren violet da1915c6c0 feat: 2025-04-01 21:17:27 +08:00
frieren violet eac4f23422 fix: 2025-04-01 21:05:51 +08:00
frieren violet 167212bbf2 feat: 2025-04-01 20:54:23 +08:00
frieren violet 63e495ffa7 style: 2025-04-01 19:45:48 +08:00
Zhengxi Yao 1b2d418d6a
Merge branch 'eesast:dev' into dev 2025-04-01 00:23:14 +08:00
frieren violet 0a3da756bf fix: 🐛 2025-04-01 00:20:19 +08:00
13 changed files with 657 additions and 90 deletions

View File

@ -50,4 +50,9 @@
</StackLayout>
</Shell.FlyoutFooter>
<!-- 注册开发者页面的路由但不在TabBar中显示 -->
<ShellContent
Title="开发者模式"
ContentTemplate="{DataTemplate page:DeveloperPage}"
Route="DeveloperPage" />
</Shell>

View File

@ -29,6 +29,8 @@ namespace installer
Routing.RegisterRoute("DebugPage", typeof(Page.DebugPage));
Routing.RegisterRoute("PlaybackPage", typeof(Page.PlaybackPage));
Routing.RegisterRoute("LoginPage", typeof(Page.LoginPage));
Routing.RegisterRoute("HelpPage", typeof(Page.HelpPage));
Routing.RegisterRoute("DeveloperPage", typeof(Page.DeveloperPage));
}
protected override void OnNavigating(ShellNavigatingEventArgs args)

View File

@ -0,0 +1,33 @@
# THUAI8 开发者指南
### 访问开发者模式
1. 打开"帮助"页面
2. 在3秒内快速连续点击页面顶部的"THUAI8 帮助"标题5次
### 配置密钥步骤
1. 在开发者模式界面输入腾讯云SecretID和SecretKey
2. 设置加密密码(仅用于本地加密,不会被保存)
3. 点击"生成加密密钥"按钮
4. 将生成的secured_key.csv文件添加为嵌入式资源
5. 重新编译项目
### 嵌入式资源配置
项目已配置自动识别Resources\Raw目录下的secured_key.csv文件作为嵌入式资源
```xml
<ItemGroup>
<EmbeddedResource Include="Resources\Raw\secured_key.csv" Condition="Exists('Resources\Raw\secured_key.csv')" />
</ItemGroup>
```
**添加方法**
1. **VS界面**创建Resources/Raw文件夹 → 添加文件 → 属性设为"嵌入式资源"
2. **手动方式**复制文件到Resources\Raw目录 → 确认项目文件包含上述XML配置
---
*注意:请妥善保管加密密码。如果忘记密码,将需要重新生成密钥。*

View File

@ -24,115 +24,71 @@ namespace installer
// public static Model.Logger logger = Model.LoggerProvider.FromFile(@"E:\bin\log\123.log");
public static bool ErrorTrigger_WhileDebug = true;
public static bool RefreshLogs_WhileDebug = false;
public static string SecretID = "***";
public static string SecretKey = "***";
public static string? SecretID = null;
public static string? SecretKey = null;
public static MauiApp CreateMauiApp()
{
try
{
// 首先初始化调试工具
// 确保初始化调试工具
DebugTool.Initialize();
DebugTool.Log("开始创建MAUI应用程序");
DebugTool.Log("调试工具已初始化");
}
catch (Exception ex)
{
// 无法记录日志,但尝试继续
Debug.WriteLine($"初始化调试工具失败: {ex.Message}");
}
// 捕获UI线程同步上下文
UISynchronizationContext = SynchronizationContext.Current;
DebugTool.Log("UI同步上下文已捕获");
var builder = MauiApp.CreateBuilder();
try
{
builder
.UseMauiApp<App>()
.ConfigureFonts(fonts =>
{
fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
fonts.AddFont("OpenSans-Semibold.ttf", "OpenSansSemibold");
});
// 初始化默认空密钥
SecretID = "***";
SecretKey = "***";
// read SecretID & SecretKey from filePath for debug
var filePath = Debugger.IsAttached ? "D:\\Secret.csv" : Path.Combine(AppContext.BaseDirectory, "Secret.csv");
try
{
if (File.Exists(filePath))
// 首先尝试从嵌入式资源中读取密钥
try
{
DebugTool.Log($"正在读取Secret文件: {filePath}");
var lines = File.ReadAllLines(filePath);
if (lines.Length >= 4)
{
try
{
lines = lines.Select(s => s.Trim().Trim('\r', '\n')).ToArray();
using (Aes aes = Aes.Create())
{
try
{
aes.Key = Convert.FromBase64String(lines[0]);
aes.IV = Convert.FromBase64String(lines[1]);
var decryptor = aes.CreateDecryptor(aes.Key, aes.IV);
try
{
using (MemoryStream memory = new MemoryStream(Convert.FromBase64String(lines[2])))
{
using (CryptoStream crypto = new CryptoStream(memory, decryptor, CryptoStreamMode.Read))
{
using (StreamReader reader = new StreamReader(crypto, Encoding.ASCII))
SecretID = reader.ReadToEnd();
}
}
using (MemoryStream memory = new MemoryStream(Convert.FromBase64String(lines[3])))
{
using (CryptoStream crypto = new CryptoStream(memory, decryptor, CryptoStreamMode.Read))
{
using (StreamReader reader = new StreamReader(crypto, Encoding.ASCII))
SecretKey = reader.ReadToEnd();
}
}
DebugTool.Log("Secret文件解密完成");
}
catch (Exception ex)
{
DebugTool.LogException(ex, "解密Secret内容");
}
}
catch (Exception ex)
{
DebugTool.LogException(ex, "初始化AES解密器");
}
}
}
catch (Exception ex)
{
DebugTool.LogException(ex, "处理Secret文件行");
}
}
else
{
DebugTool.Log($"Secret文件格式不正确行数: {lines.Length}");
}
LoadSecretFromEmbeddedResource();
}
else
catch (Exception ex)
{
DebugTool.Log($"从嵌入式资源加载密钥失败: {ex.Message}");
}
// 如果嵌入式资源中没有密钥,则尝试从外部文件读取
if (string.IsNullOrEmpty(SecretID) || SecretID == "***" || string.IsNullOrEmpty(SecretKey) || SecretKey == "***")
{
DebugTool.Log($"Secret文件不存在: {filePath}");
// 创建一个简单的Secret.csv用于测试
try
{
DebugTool.Log("创建默认Secret.csv文件");
using (StreamWriter sw = new StreamWriter(filePath))
{
// 使用固定密钥,仅用于测试
sw.WriteLine("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="); // Key
sw.WriteLine("AAAAAAAAAAAAAAAAAAAAAA=="); // IV
sw.WriteLine("AAAAAAAAAAAAAAAAAAAAAA=="); // Encrypted SecretID
sw.WriteLine("AAAAAAAAAAAAAAAAAAAAAA=="); // Encrypted SecretKey
}
LoadSecretFromExternalFile();
}
catch (Exception ex)
{
DebugTool.LogException(ex, "创建默认Secret.csv");
DebugTool.Log($"从外部文件加载密钥失败: {ex.Message}");
}
}
}
catch (Exception ex)
{
DebugTool.LogException(ex, "读取Secret文件");
// 确保任何密钥加载问题都不会导致应用崩溃
DebugTool.LogException(ex, "密钥加载过程中发生未处理的异常");
}
DebugTool.Log("开始配置MAUI应用程序");
var builder = MauiApp.CreateBuilder();
builder
.UseMauiApp<App>()
.UseMauiCommunityToolkitCore();
try
@ -206,11 +162,166 @@ namespace installer
}
catch (Exception ex)
{
DebugTool.LogException(ex, "创建MAUI应用");
DebugTool.LogException(ex, "MAUI应用程序初始化");
throw; // 重新抛出异常以便能够看到崩溃信息
}
}
private static void LoadSecretFromEmbeddedResource()
{
try
{
var assembly = Assembly.GetExecutingAssembly();
string resourceName = "installer.Resources.Raw.secured_key.csv";
// 列出所有嵌入资源以进行调试
var resources = assembly.GetManifestResourceNames();
DebugTool.Log($"可用资源: {string.Join(", ", resources)}");
using (Stream? stream = assembly.GetManifestResourceStream(resourceName))
{
if (stream != null)
{
using (StreamReader reader = new StreamReader(stream))
{
string content = reader.ReadToEnd();
var lines = content.Split('\n').Select(s => s.Trim().Trim('\r')).ToArray();
if (lines.Length >= 4)
{
try
{
using (Aes aes = Aes.Create())
{
aes.Key = Convert.FromBase64String(lines[0]);
aes.IV = Convert.FromBase64String(lines[1]);
var decryptor = aes.CreateDecryptor(aes.Key, aes.IV);
try
{
// 解密SecretID
using (MemoryStream memory = new MemoryStream(Convert.FromBase64String(lines[2])))
using (CryptoStream crypto = new CryptoStream(memory, decryptor, CryptoStreamMode.Read))
using (StreamReader cryptoReader = new StreamReader(crypto))
{
SecretID = cryptoReader.ReadToEnd();
}
// 解密SecretKey
using (MemoryStream memory = new MemoryStream(Convert.FromBase64String(lines[3])))
using (CryptoStream crypto = new CryptoStream(memory, decryptor, CryptoStreamMode.Read))
using (StreamReader cryptoReader = new StreamReader(crypto))
{
SecretKey = cryptoReader.ReadToEnd();
}
DebugTool.Log("从嵌入式资源成功加载密钥");
}
catch (Exception ex)
{
DebugTool.LogException(ex, "解密嵌入式资源密钥内容");
// 不抛出异常,使程序可以继续运行
}
}
}
catch (Exception ex)
{
DebugTool.LogException(ex, "初始化嵌入式资源AES解密器");
// 不抛出异常,使程序可以继续运行
}
}
else
{
DebugTool.Log($"嵌入式资源密钥文件格式不正确,行数: {lines.Length}");
}
}
}
else
{
DebugTool.Log("未找到嵌入式资源密钥文件");
}
}
}
catch (Exception ex)
{
DebugTool.LogException(ex, "读取嵌入式资源出错");
// 不抛出异常,允许回退到其他加载方式
}
}
private static void LoadSecretFromExternalFile()
{
// 保留原有的外部文件读取逻辑
// read SecretID & SecretKey from filePath for debug
var filePath = Debugger.IsAttached ? "D:\\Secret.csv" : Path.Combine(AppContext.BaseDirectory, "Secret.csv");
try
{
if (File.Exists(filePath))
{
DebugTool.Log($"正在读取Secret文件: {filePath}");
var lines = File.ReadAllLines(filePath);
if (lines.Length >= 4)
{
try
{
lines = lines.Select(s => s.Trim().Trim('\r', '\n')).ToArray();
using (Aes aes = Aes.Create())
{
try
{
aes.Key = Convert.FromBase64String(lines[0]);
aes.IV = Convert.FromBase64String(lines[1]);
var decryptor = aes.CreateDecryptor(aes.Key, aes.IV);
try
{
using (MemoryStream memory = new MemoryStream(Convert.FromBase64String(lines[2])))
{
using (CryptoStream crypto = new CryptoStream(memory, decryptor, CryptoStreamMode.Read))
{
using (StreamReader reader = new StreamReader(crypto, Encoding.ASCII))
SecretID = reader.ReadToEnd();
}
}
using (MemoryStream memory = new MemoryStream(Convert.FromBase64String(lines[3])))
{
using (CryptoStream crypto = new CryptoStream(memory, decryptor, CryptoStreamMode.Read))
{
using (StreamReader reader = new StreamReader(crypto, Encoding.ASCII))
SecretKey = reader.ReadToEnd();
}
}
DebugTool.Log("Secret文件解密完成");
}
catch (Exception ex)
{
DebugTool.LogException(ex, "解密Secret内容");
}
}
catch (Exception ex)
{
DebugTool.LogException(ex, "初始化AES解密器");
}
}
}
catch (Exception ex)
{
DebugTool.LogException(ex, "处理Secret文件行");
}
}
else
{
DebugTool.Log($"Secret文件格式不正确行数: {lines.Length}");
}
}
}
catch (Exception ex)
{
DebugTool.LogException(ex, "读取外部Secret文件");
}
}
public static void AddViewModelService(MauiAppBuilder builder)
{
var a = typeof(MauiProgram).Assembly;

View File

@ -36,11 +36,18 @@ namespace installer.Model
public Logger Log;
public LoginStatus Status = LoginStatus.offline;
public Tencent_Cos EEsast_Cos { get; protected set; } = new Tencent_Cos("1255334966", "ap-beijing", "eesast");//服务器应该还能用吧
public Tencent_Cos EEsast_Cos { get; protected set; } = new Tencent_Cos("1255334966", "ap-beijing", "eesast");
public EEsast(Logger? _log = null)
{
Log = _log ?? LoggerProvider.FromConsole();
Log.PartnerInfo = "[EESAST]";
// 使用全局密钥
if (!string.IsNullOrEmpty(MauiProgram.SecretID) && !string.IsNullOrEmpty(MauiProgram.SecretKey))
{
EEsast_Cos.UpdateSecret(MauiProgram.SecretID, MauiProgram.SecretKey);
}
}
public async Task LoginToEEsast(HttpClient client, string useremail = "", string userpassword = "")
{

View File

@ -45,7 +45,25 @@ namespace installer.Model
.SetRegion(Region) // 设置一个默认的存储桶地域
.SetDebugLog(true) // 显示日志
.Build(); // 创建 CosXmlConfig 对象
QCloudCredentialProvider cosCredentialProvider = new DefaultQCloudCredentialProvider("***", "***", 1000);
// 使用全局密钥
string secretId = MauiProgram.SecretID;
string secretKey = MauiProgram.SecretKey;
// 确保密钥值有效,如果没有则使用默认值
if (string.IsNullOrEmpty(secretId) || secretId == "***")
{
secretId = "***"; // 默认值或占位符
Log.LogWarning("使用默认SecretID - 注意这将导致API访问受限");
}
if (string.IsNullOrEmpty(secretKey) || secretKey == "***")
{
secretKey = "***"; // 默认值或占位符
Log.LogWarning("使用默认SecretKey - 注意这将导致API访问受限");
}
QCloudCredentialProvider cosCredentialProvider = new DefaultQCloudCredentialProvider(secretId, secretKey, 1000);
cosXml = new CosXmlServer(config, cosCredentialProvider);
transfer = new TransferConfig()
{

View File

@ -0,0 +1,69 @@
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="installer.Page.DeveloperPage"
Title="开发者模式">
<ScrollView>
<VerticalStackLayout Padding="20" Spacing="20">
<Frame BorderColor="Gray" Padding="15" CornerRadius="5">
<VerticalStackLayout Spacing="10">
<Label Text="腾讯云密钥配置" FontSize="20" FontAttributes="Bold" />
<Label Text="此配置仅供开发人员使用。普通用户无需设置此项,直接使用程序内置的密钥即可。"
TextColor="Gray" />
<Grid ColumnDefinitions="Auto,*" RowDefinitions="Auto,Auto" ColumnSpacing="10" RowSpacing="15">
<Label Text="腾讯云SecretID:" Grid.Row="0" Grid.Column="0" VerticalOptions="Center" />
<Entry x:Name="SecretIDEntry" Grid.Row="0" Grid.Column="1" IsPassword="True" />
<Label Text="腾讯云SecretKey:" Grid.Row="1" Grid.Column="0" VerticalOptions="Center" />
<Entry x:Name="SecretKeyEntry" Grid.Row="1" Grid.Column="1" IsPassword="True" />
</Grid>
<Label Text="加密密码(用于保护密钥,请务必记住此密码):" />
<Entry x:Name="EncryptionPasswordEntry" IsPassword="True" />
<Button x:Name="GenerateKeyButton" Text="生成加密密钥" Clicked="OnGenerateKeyClicked"
HorizontalOptions="Start" Margin="0,10,0,0" />
<Label x:Name="StatusLabel" TextColor="Gray" />
</VerticalStackLayout>
</Frame>
<Frame BorderColor="Gray" Padding="15" CornerRadius="5" IsVisible="{Binding ResourceKeyGenerated}">
<VerticalStackLayout Spacing="10">
<Label Text="嵌入式资源密钥" FontSize="18" FontAttributes="Bold" />
<Label Text="请将以下文件添加为项目的嵌入式资源。添加后,所有用户都可以使用此密钥,无需额外配置。"
TextColor="Gray" />
<Label Text="加密密钥文件路径:" />
<Label x:Name="KeyFilePathLabel" TextColor="Blue" />
<Button x:Name="OpenFolderButton" Text="打开文件位置" Clicked="OnOpenFolderClicked"
HorizontalOptions="Start" />
<Button x:Name="CopyPathButton" Text="复制路径" Clicked="OnCopyPathClicked"
HorizontalOptions="Start" />
</VerticalStackLayout>
</Frame>
<Frame BorderColor="Gray" Padding="15" CornerRadius="5">
<VerticalStackLayout Spacing="10">
<Label Text="使用说明" FontSize="18" FontAttributes="Bold" />
<Label Text="1. 输入腾讯云SecretID、SecretKey和用于加密的密码" />
<Label Text="2. 点击'生成加密密钥'按钮生成密钥文件" />
<Label Text="3. 将生成的密钥文件添加为项目的嵌入式资源" />
<Label Text="4. 重新编译项目后,所有用户都能无需配置地使用腾讯云服务" />
<Label Text="注意:密码仅用于加密本地文件,不会被保存或上传" TextColor="Red" />
</VerticalStackLayout>
</Frame>
<Button x:Name="ExitButton"
Text="退出开发者模式"
Clicked="OnExitButtonClicked"
BackgroundColor="#FFD2D2"
TextColor="Black"
Margin="0,20,0,0"
HorizontalOptions="Center" />
</VerticalStackLayout>
</ScrollView>
</ContentPage>

View File

@ -0,0 +1,226 @@
using System;
using System.Diagnostics;
using System.IO;
using System.Security.Cryptography;
using System.Text;
using System.Threading.Tasks;
using Microsoft.Maui.ApplicationModel;
using Microsoft.Maui.Storage;
namespace installer.Page
{
public partial class DeveloperPage : ContentPage
{
private bool _resourceKeyGenerated = false;
private string _encryptedKeyFilePath = string.Empty;
public bool ResourceKeyGenerated
{
get => _resourceKeyGenerated;
set
{
_resourceKeyGenerated = value;
OnPropertyChanged(nameof(ResourceKeyGenerated));
}
}
public DeveloperPage()
{
InitializeComponent();
BindingContext = this;
}
private async void OnGenerateKeyClicked(object sender, EventArgs e)
{
try
{
if (string.IsNullOrWhiteSpace(SecretIDEntry.Text) ||
string.IsNullOrWhiteSpace(SecretKeyEntry.Text) ||
string.IsNullOrWhiteSpace(EncryptionPasswordEntry.Text))
{
await DisplayAlert("输入错误", "请填写所有必填字段", "确定");
return;
}
// 显示进度指示
StatusLabel.Text = "正在生成密钥...";
StatusLabel.TextColor = Colors.Blue;
GenerateKeyButton.IsEnabled = false;
// 在后台线程中执行密钥生成
await Task.Run(() =>
{
try
{
// 创建安全随机密钥和IV
using (Aes aes = Aes.Create())
{
// 从密码生成密钥
using (var deriveBytes = new Rfc2898DeriveBytes(
EncryptionPasswordEntry.Text,
new byte[16], // 静态盐,在解密时需要相同
10000,
HashAlgorithmName.SHA256))
{
aes.Key = deriveBytes.GetBytes(32); // 256位密钥
aes.IV = deriveBytes.GetBytes(16); // 128位IV
}
// 加密SecretID和SecretKey
var encryptedSecretID = EncryptString(SecretIDEntry.Text, aes.Key, aes.IV);
var encryptedSecretKey = EncryptString(SecretKeyEntry.Text, aes.Key, aes.IV);
// 创建密钥文件内容
string resourceContent = $"{Convert.ToBase64String(aes.Key)}\n{Convert.ToBase64String(aes.IV)}\n{encryptedSecretID}\n{encryptedSecretKey}";
try
{
// 为嵌入式资源创建目录
string resourceDirPath = Path.Combine(AppContext.BaseDirectory, "Resources", "Raw");
Directory.CreateDirectory(resourceDirPath);
// 保存密钥文件
_encryptedKeyFilePath = Path.Combine(resourceDirPath, "secured_key.csv");
File.WriteAllText(_encryptedKeyFilePath, resourceContent);
// 更新UI必须在主线程中执行
MainThread.BeginInvokeOnMainThread(() =>
{
KeyFilePathLabel.Text = _encryptedKeyFilePath;
ResourceKeyGenerated = true;
StatusLabel.Text = "密钥已生成,请将其添加为嵌入式资源";
StatusLabel.TextColor = Colors.Green;
GenerateKeyButton.IsEnabled = true;
});
}
catch (Exception ex)
{
DebugTool.LogException(ex, "保存加密密钥文件");
MainThread.BeginInvokeOnMainThread(async () =>
{
await DisplayAlert("错误", $"保存密钥文件失败: {ex.Message}", "确定");
StatusLabel.Text = "保存密钥文件失败,请检查日志";
StatusLabel.TextColor = Colors.Red;
GenerateKeyButton.IsEnabled = true;
});
}
}
}
catch (Exception ex)
{
DebugTool.LogException(ex, "生成密钥加密过程");
MainThread.BeginInvokeOnMainThread(async () =>
{
await DisplayAlert("错误", $"加密密钥失败: {ex.Message}", "确定");
StatusLabel.Text = "加密密钥失败,请重试";
StatusLabel.TextColor = Colors.Red;
GenerateKeyButton.IsEnabled = true;
});
}
});
}
catch (Exception ex)
{
// 处理UI线程异常
await DisplayAlert("错误", $"生成密钥时出错: {ex.Message}", "确定");
StatusLabel.Text = "密钥生成失败,请重试";
StatusLabel.TextColor = Colors.Red;
GenerateKeyButton.IsEnabled = true;
DebugTool.LogException(ex, "生成密钥");
}
}
private string EncryptString(string plainText, byte[] key, byte[] iv)
{
using (Aes aes = Aes.Create())
{
aes.Key = key;
aes.IV = iv;
using (MemoryStream memoryStream = new MemoryStream())
{
using (CryptoStream cryptoStream = new CryptoStream(
memoryStream,
aes.CreateEncryptor(),
CryptoStreamMode.Write))
{
using (StreamWriter writer = new StreamWriter(cryptoStream))
{
writer.Write(plainText);
}
}
return Convert.ToBase64String(memoryStream.ToArray());
}
}
}
private void OnOpenFolderClicked(object sender, EventArgs e)
{
try
{
if (!string.IsNullOrEmpty(_encryptedKeyFilePath) && File.Exists(_encryptedKeyFilePath))
{
Process.Start("explorer.exe", $"/select,\"{_encryptedKeyFilePath}\"");
}
else
{
StatusLabel.Text = "密钥文件不存在,请先生成密钥";
StatusLabel.TextColor = Colors.Red;
}
}
catch (Exception ex)
{
StatusLabel.Text = $"无法打开文件位置: {ex.Message}";
StatusLabel.TextColor = Colors.Red;
DebugTool.LogException(ex, "打开文件位置");
}
}
private async void OnCopyPathClicked(object sender, EventArgs e)
{
try
{
if (!string.IsNullOrEmpty(_encryptedKeyFilePath))
{
await Clipboard.SetTextAsync(_encryptedKeyFilePath);
StatusLabel.Text = "路径已复制到剪贴板";
StatusLabel.TextColor = Colors.Green;
}
else
{
StatusLabel.Text = "密钥文件不存在,请先生成密钥";
StatusLabel.TextColor = Colors.Red;
}
}
catch (Exception ex)
{
StatusLabel.Text = $"无法复制路径: {ex.Message}";
StatusLabel.TextColor = Colors.Red;
DebugTool.LogException(ex, "复制路径");
}
}
private async void OnExitButtonClicked(object sender, EventArgs e)
{
try
{
// 返回到主Shell
await Shell.Current.GoToAsync("//InstallPage");
}
catch (Exception ex)
{
// 如果导航失败,记录错误并尝试使用其他导航方式
DebugTool.LogException(ex, "退出开发者模式");
try
{
// 尝试返回
await Shell.Current.GoToAsync("..");
}
catch
{
// 忽略错误
}
}
}
}
}

View File

@ -5,11 +5,32 @@
x:Class="installer.Page.HelpPage"
Title="Help">
<ContentPage.Resources>
<ResourceDictionary>
<Style x:Key="DevModeHeaderStyle" TargetType="Label">
<Setter Property="FontSize" Value="10" />
<Setter Property="HorizontalOptions" Value="Center" />
<Setter Property="TextColor" Value="Transparent" />
</Style>
</ResourceDictionary>
</ContentPage.Resources>
<ScrollView
WidthRequest="750"
HeightRequest="500"
HorizontalOptions="Center">
<VerticalStackLayout>
<VerticalStackLayout Padding="10" Spacing="10">
<Label x:Name="HeaderLabel"
Text="THUAI8 帮助"
FontSize="20"
FontAttributes="Bold"
HorizontalOptions="Center"
Margin="0,0,0,10">
<Label.GestureRecognizers>
<TapGestureRecognizer Tapped="OnHeaderTapped" />
</Label.GestureRecognizers>
</Label>
<Label
Text="Installer"

View File

@ -1,12 +1,72 @@
using System;
using System.Diagnostics;
using installer.ViewModel;
namespace installer.Page;
public partial class HelpPage : ContentPage
{
private int _tapCount = 0;
private DateTime _lastTapTime = DateTime.MinValue;
public HelpPage(HelpViewModel viewModel)
{
InitializeComponent();
BindingContext = viewModel;
// 初始化点击计数器
_tapCount = 0;
_lastTapTime = DateTime.MinValue;
}
protected override void OnAppearing()
{
base.OnAppearing();
// 页面显示时重置计数器
_tapCount = 0;
_lastTapTime = DateTime.MinValue;
Debug.WriteLine("帮助页面已显示,点击计数器已重置");
}
private async void OnHeaderTapped(object sender, EventArgs e)
{
try
{
DateTime now = DateTime.Now;
// 检查是否在3秒内点击
if ((now - _lastTapTime).TotalSeconds <= 3)
{
_tapCount++;
Debug.WriteLine($"帮助页面标题被点击,当前点击次数: {_tapCount}");
// 给用户一些视觉反馈,但不明显
await HeaderLabel.ScaleTo(1.05, 50);
await HeaderLabel.ScaleTo(1.0, 50);
if (_tapCount >= 5)
{
_tapCount = 0;
Debug.WriteLine("触发开发者模式");
// 导航到开发者页面
await Shell.Current.GoToAsync("//DeveloperPage");
}
}
else
{
// 超过3秒重置计数器
_tapCount = 1;
Debug.WriteLine("点击计数器已重置(超时)");
}
_lastTapTime = now;
}
catch (Exception ex)
{
Debug.WriteLine($"处理标题点击错误: {ex.Message}");
}
}
}

View File

@ -82,7 +82,7 @@
<Grid ColumnDefinitions="Auto, *" ColumnSpacing="15">
<Label Grid.Column="0"
Text="{Binding PlaybackSpeed, StringFormat='速度: {0:F2}x'}"
Text="{Binding PlaybackSpeed, StringFormat='速度: {0:F1}x'}"
VerticalOptions="Center"
WidthRequest="100"
TextColor="#2c3e50"/>

View File

@ -29,7 +29,7 @@ namespace installer.ViewModel
IP = Downloader.Data.Config.Commands.IP;
Port = Downloader.Data.Config.Commands.Port;
PlaybackFile = Downloader.Data.Config.Commands.PlaybackFile;
PlaybackSpeed = Downloader.Data.Config.Commands.PlaybackSpeed.ToString();
PlaybackSpeed = Downloader.Data.Config.Commands.PlaybackSpeed.ToString("F1");
ipChanged = false;
portChanged = false;
@ -127,8 +127,15 @@ namespace installer.ViewModel
get => playbackSpeed;
set
{
playbackSpeed = value;
if (playbackSpeed == Downloader.Data.Config.Commands.PlaybackSpeed.ToString())
if (double.TryParse(value, out double speed))
{
playbackSpeed = speed.ToString("F1");
}
else
{
playbackSpeed = value;
}
if (playbackSpeed == Downloader.Data.Config.Commands.PlaybackSpeed.ToString("F1"))
playbackSpeedChanged = false;
else
playbackSpeedChanged = true;

View File

@ -46,6 +46,9 @@
<PropertyGroup Condition="'$(Configuration)|$(TargetFramework)|$(Platform)'=='Debug|net8.0-windows10.0.19041.0|AnyCPU'">
<ApplicationDisplayVersion>1.0.0</ApplicationDisplayVersion>
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
<WindowsAppSDKSelfContained>true</WindowsAppSDKSelfContained>
<WindowsAppSDKBootstrapAutoInitialize>true</WindowsAppSDKBootstrapAutoInitialize>
<UseRidGraph>true</UseRidGraph>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(TargetFramework)|$(Platform)'=='Release|net8.0-windows10.0.19041.0|AnyCPU'">
@ -131,7 +134,12 @@
Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'windows'">
<PropertyGroup>
<WindowsAppSDKBootstrapAutoInitialize>true</WindowsAppSDKBootstrapAutoInitialize>
<WindowsAppSDKSelfContained>true</WindowsAppSDKSelfContained>
</PropertyGroup>
</Target>
<ItemGroup>
<EmbeddedResource Include="Resources\Raw\secured_key.csv" Condition="Exists('Resources\Raw\secured_key.csv')" />
</ItemGroup>
</Project>