add script repo downloader

This commit is contained in:
辉鸭蛋 2024-10-13 13:36:06 +08:00
parent bb18350457
commit c75dbbc241
9 changed files with 369 additions and 26 deletions

View File

@ -29,12 +29,7 @@ public class Global
public static string ScriptPath()
{
return Absolute("Script");
}
public static string ScriptPath(string folderName)
{
return Path.Combine(Absolute("Script"), folderName);
return Absolute("User\\JsScript");
}
public static string? ReadAllTextIfExist(string relativePath)

View File

@ -0,0 +1,329 @@
using BetterGenshinImpact.Core.Config;
using BetterGenshinImpact.Helpers.Http;
using BetterGenshinImpact.Model;
using Newtonsoft.Json.Linq;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Net.Http;
using System.Threading.Tasks;
using System.Windows;
using Wpf.Ui.Violeta.Controls;
namespace BetterGenshinImpact.Core.Script;
public class ScriptRepoUpdater : Singleton<ScriptRepoUpdater>
{
private static readonly HttpClient _httpClient = new() { Timeout = TimeSpan.FromSeconds(30) };
// 仓储位置
public static readonly string ReposPath = Global.Absolute("Repos");
// 仓储临时目录 用于下载与解压
public static readonly string ReposTempPath = Path.Combine(ReposPath, "Temp");
// 中央仓库信息地址
public static readonly string CenterRepoInfoUrl = "https://raw.githubusercontent.com/babalae/bettergi-scripts-list/refs/heads/main/repo.json";
// 中央仓库解压后文件夹名
public static readonly string CenterRepoUnzipName = "bettergi-scripts-list-main";
public static readonly string CenterRepoPath = Path.Combine(ReposPath, CenterRepoUnzipName);
public static readonly Dictionary<string, string> PathMapper = new Dictionary<string, string>
{
{ "pathing", Global.Absolute("User\\AutoPathing") },
{ "js", Global.Absolute("User\\JsScript") },
{ "combat", Global.Absolute("User\\AutoFight") },
{ "tcg", Global.Absolute("User\\AutoGeniusInvokation") },
};
public async Task<string> UpdateCenterRepo()
{
// 测速并获取信息
var res = await ProxySpeedTester.GetFastestProxyAsync(CenterRepoInfoUrl);
// 解析信息
var fastProxyUrl = res.Item1;
var jsonString = res.Item2;
var (time, url, file) = ParseJson(jsonString);
// 检查仓库是否存在,不存在则下载
if (!Directory.Exists(CenterRepoPath))
{
await DownloadRepoAndUnzip(string.Format(fastProxyUrl, url));
}
// 搜索本地的 repo.json
var localRepoJsonPath = Directory.GetFiles(CenterRepoPath, file, SearchOption.AllDirectories).FirstOrDefault();
if (localRepoJsonPath is null)
{
throw new Exception("本地仓库缺少 repo.json");
}
var (time2, url2, file2) = ParseJson(await File.ReadAllTextAsync(localRepoJsonPath));
// 检查是否需要更新
if (long.Parse(time) > long.Parse(time2))
{
await DownloadRepoAndUnzip(string.Format(fastProxyUrl, url2));
}
// 获取与 localRepoJsonPath 同名(无扩展名)的文件夹路径
var folderName = Path.GetFileNameWithoutExtension(localRepoJsonPath);
var folderPath = Path.Combine(Path.GetDirectoryName(localRepoJsonPath)!, folderName);
if (!Directory.Exists(folderPath))
{
throw new Exception("本地仓库文件夹不存在");
}
return folderPath;
}
private (string time, string url, string file) ParseJson(string jsonString)
{
var json = JObject.Parse(jsonString);
var time = json["time"]?.ToString();
var url = json["url"]?.ToString();
var file = json["file"]?.ToString();
// 检查是否有空值
if (time is null || url is null || file is null)
{
throw new Exception("repo.json 解析失败");
}
return (time, url, file);
}
public async Task DownloadRepoAndUnzip(string url)
{
// 下载
var res = await _httpClient.GetAsync(url);
if (!res.IsSuccessStatusCode)
{
throw new Exception("下载失败");
}
var bytes = await res.Content.ReadAsByteArrayAsync();
// 获取文件名
var contentDisposition = res.Content.Headers.ContentDisposition;
var fileName = contentDisposition is { FileName: not null } ? contentDisposition.FileName.Trim('"') : "temp.zip";
// 创建临时目录
if (!Directory.Exists(ReposTempPath))
{
Directory.CreateDirectory(ReposTempPath);
}
// 保存下载的文件
var zipPath = Path.Combine(ReposTempPath, fileName);
await File.WriteAllBytesAsync(zipPath, bytes);
// 删除旧文件夹
if (Directory.Exists(CenterRepoPath))
{
Directory.Delete(CenterRepoPath, true);
}
// 使用 System.IO.Compression 解压
ZipFile.ExtractToDirectory(zipPath, ReposPath, true);
}
public async Task ImportScriptFromClipboard()
{
// 获取剪切板内容
if (Clipboard.ContainsText())
{
string clipboardText = Clipboard.GetText();
// 检查剪切板内容是否符合特定的URL格式
if (!string.IsNullOrEmpty(clipboardText) && clipboardText.Trim().ToLower().StartsWith("bettergi://script?import="))
{
Debug.WriteLine($"窗口激活时发现剪切板内容为脚本订购内容:{clipboardText}");
// 执行相关操作
var pathJson = ParseUri(clipboardText);
if (!string.IsNullOrEmpty(pathJson))
{
var uiMessageBox = new Wpf.Ui.Controls.MessageBox
{
Title = "脚本订阅",
Content = $"检测到剪切板上存在脚本订阅链接,解析后需要导入的脚本为:{pathJson}。\n是否导入并覆盖此文件或者文件夹下的脚本",
CloseButtonText = "关闭",
PrimaryButtonText = "确认导入",
WindowStartupLocation = WindowStartupLocation.CenterOwner,
Owner = Application.Current.MainWindow,
};
var result = await uiMessageBox.ShowDialogAsync();
if (result == Wpf.Ui.Controls.MessageBoxResult.Primary)
{
await ImportScriptFromPathJson(pathJson);
}
}
// 清空剪切板内容
Clipboard.Clear();
}
}
}
private string? ParseUri(string uriString)
{
var uri = new Uri(uriString);
// 获取 query 参数
string query = uri.Query;
Debug.WriteLine($"Query: {query}");
// 解析 query 参数
var queryParams = System.Web.HttpUtility.ParseQueryString(query);
var import = queryParams["import"];
if (string.IsNullOrEmpty(import))
{
Debug.WriteLine("未找到 import 参数");
return null;
}
// Base64 解码后再使用url解码
byte[] data = Convert.FromBase64String(import);
return System.Web.HttpUtility.UrlDecode(System.Text.Encoding.UTF8.GetString(data));
}
public async Task ImportScriptFromPathJson(string pathJson)
{
var paths = Newtonsoft.Json.JsonConvert.DeserializeObject<List<string>>(pathJson);
if (paths is null || paths.Count == 0)
{
Toast.Warning("订阅脚本路径为空");
return;
}
Toast.Information("获取最新仓库信息中...");
// 更新仓库
var repoPath = await Task.Run(UpdateCenterRepo);
// // 收集将被覆盖的文件和文件夹
// var filesToOverwrite = new List<string>();
// foreach (var path in paths)
// {
// var first = GetFirstFolder(path);
// if (PathMapper.TryGetValue(first, out var userPath))
// {
// var scriptPath = Path.Combine(repoPath, path);
// var destPath = Path.Combine(userPath, path.Replace(first, ""));
// if (Directory.Exists(scriptPath))
// {
// if (Directory.Exists(destPath))
// {
// filesToOverwrite.Add(destPath);
// }
// }
// else if (File.Exists(scriptPath))
// {
// if (File.Exists(destPath))
// {
// filesToOverwrite.Add(destPath);
// }
// }
// }
// else
// {
// Toast.Warning($"未知的脚本路径:{path}");
// }
// }
//
// // 提示用户确认
// if (filesToOverwrite.Count > 0)
// {
// var message = "以下文件和文件夹将被覆盖:\n" + string.Join("\n", filesToOverwrite) + "\n是否覆盖所有文件和文件夹";
// var uiMessageBox = new Wpf.Ui.Controls.MessageBox
// {
// Title = "确认覆盖",
// Content = message,
// CloseButtonText = "取消",
// PrimaryButtonText = "确认覆盖",
// WindowStartupLocation = WindowStartupLocation.CenterOwner,
// Owner = Application.Current.MainWindow,
// };
//
// var result = await uiMessageBox.ShowDialogAsync();
// if (result != Wpf.Ui.Controls.MessageBoxResult.Primary)
// {
// return;
// }
// }
// 拷贝文件
foreach (var path in paths)
{
var (first, remainingPath) = GetFirstFolderAndRemainingPath(path);
if (PathMapper.TryGetValue(first, out var userPath))
{
var scriptPath = Path.Combine(repoPath, path);
var destPath = Path.Combine(userPath, remainingPath);
if (Directory.Exists(scriptPath))
{
if (Directory.Exists(destPath))
{
Directory.Delete(destPath, true);
}
CopyDirectory(scriptPath, destPath);
}
else if (File.Exists(scriptPath))
{
if (File.Exists(destPath))
{
File.Delete(destPath);
}
File.Copy(scriptPath, destPath, true);
}
Toast.Success("脚本订阅链接导入完成");
}
else
{
Toast.Warning($"未知的脚本路径:{path}");
}
}
}
private void CopyDirectory(string sourceDir, string destDir)
{
// 创建目标目录
Directory.CreateDirectory(destDir);
// 拷贝文件
foreach (var file in Directory.GetFiles(sourceDir))
{
var destFile = Path.Combine(destDir, Path.GetFileName(file));
File.Copy(file, destFile, true);
}
// 拷贝子目录
foreach (var dir in Directory.GetDirectories(sourceDir))
{
var destSubDir = Path.Combine(destDir, Path.GetFileName(dir));
CopyDirectory(dir, destSubDir);
}
}
private static (string firstFolder, string remainingPath) GetFirstFolderAndRemainingPath(string path)
{
// 检查路径是否为空或仅包含部分字符
if (string.IsNullOrEmpty(path))
{
return (string.Empty, string.Empty);
}
// 使用路径分隔符分割路径
string[] parts = path.Split(new char[] { Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar }, StringSplitOptions.RemoveEmptyEntries);
// 返回第一个文件夹和剩余路径
return parts.Length > 0 ? (parts[0], string.Join(Path.DirectorySeparatorChar, parts.Skip(1))) : (string.Empty, string.Empty);
}
}

View File

@ -33,7 +33,7 @@ public class ProxySpeedTester
public static async Task<(string, string)> GetFastestProxyAsync(List<string> proxyAddresses, string target)
{
var tasks = new List<Task<(string, string)>>();
var tasks = new List<Task<(string, string, bool)>>(); // 修改为包含成功标志的元组
var cts = new CancellationTokenSource();
foreach (var proxy in proxyAddresses)
@ -47,24 +47,34 @@ public class ProxySpeedTester
tasks.Add(TestProxyAsync(proxy, target, cts.Token));
}
var firstCompletedTask = await Task.WhenAny(tasks);
await cts.CancelAsync(); // 取消所有其他请求
while (tasks.Count > 0)
{
var firstCompletedTask = await Task.WhenAny(tasks);
tasks.Remove(firstCompletedTask);
var result = await firstCompletedTask;
if (result.Item3) // 检查是否成功
{
await cts.CancelAsync(); // 取消所有其他请求
return (result.Item1, result.Item2); // 返回第一个成功的代理地址
}
}
return (string.Empty, string.Empty); // 如果没有成功的结果,返回空
}
private static async Task<(string, string, bool)> TestProxyAsync(string proxyAddress, string target, CancellationToken cancellationToken)
{
try
{
return await firstCompletedTask; // 返回第一个完成的代理地址
// 模拟代理测试请求
var response = await _httpClient.GetAsync(string.Format(proxyAddress, target), cancellationToken);
response.EnsureSuccessStatusCode();
return (proxyAddress, await response.Content.ReadAsStringAsync(cancellationToken), true);
}
catch (OperationCanceledException)
catch (Exception e)
{
return (string.Empty, string.Empty); // 如果第一个任务被取消,返回空
return (proxyAddress, e.Message, false);
}
}
private static async Task<(string, string)> TestProxyAsync(string proxyAddress, string target, CancellationToken cancellationToken)
{
// 模拟代理测试请求
var response = await _httpClient.GetAsync(string.Format(proxyAddress, target), cancellationToken);
response.EnsureSuccessStatusCode();
return (proxyAddress, response.Content.ToString()!);
}
}

View File

@ -27,6 +27,9 @@
<BooleanToVisibilityConverter x:Key="BooleanToVisibilityConverter" />
</Window.Resources>
<b:Interaction.Triggers>
<b:EventTrigger EventName="Activated">
<b:InvokeCommandAction Command="{Binding ActivatedCommand}" />
</b:EventTrigger>
<b:EventTrigger EventName="Loaded">
<b:InvokeCommandAction Command="{Binding LoadedCommand}" CommandParameter="{Binding}" />
</b:EventTrigger>

View File

@ -53,7 +53,7 @@
<ui:Button Command="{Binding OpenScriptFolderCommand}" Icon="{ui:SymbolIcon Archive24}">
<Grid HorizontalAlignment="Stretch" VerticalAlignment="Stretch">
<ui:TextBlock>脚本仓库</ui:TextBlock>
<ui:InfoBadge Margin="0,-3,-8,0"
<ui:InfoBadge Margin="0,-8,-14,0"
HorizontalAlignment="Right"
VerticalAlignment="Top"
Severity="Attention"

View File

@ -53,7 +53,7 @@
<ui:Button Command="{Binding OpenScriptFolderCommand}" Icon="{ui:SymbolIcon Archive24}">
<Grid HorizontalAlignment="Stretch" VerticalAlignment="Stretch">
<ui:TextBlock>脚本仓库</ui:TextBlock>
<ui:InfoBadge Margin="0,-3,-8,0"
<ui:InfoBadge Margin="0,-8,-14,0"
HorizontalAlignment="Right"
VerticalAlignment="Top"
Severity="Attention"

View File

@ -55,7 +55,7 @@
Icon="{ui:SymbolIcon Archive24}">
<Grid HorizontalAlignment="Stretch" VerticalAlignment="Stretch">
<ui:TextBlock>脚本仓库</ui:TextBlock>
<ui:InfoBadge Margin="0,-3,-8,0"
<ui:InfoBadge Margin="0,-8,-14,0"
HorizontalAlignment="Right"
VerticalAlignment="Top"
Severity="Attention"

View File

@ -1,5 +1,6 @@
using BetterGenshinImpact.Core.Config;
using BetterGenshinImpact.Core.Recognition.OCR;
using BetterGenshinImpact.Core.Script;
using BetterGenshinImpact.Helpers;
using BetterGenshinImpact.Model;
using BetterGenshinImpact.Service.Interface;
@ -40,6 +41,12 @@ public partial class MainWindowViewModel : ObservableObject, IViewModel
_logger = App.GetLogger<MainWindowViewModel>();
}
[RelayCommand]
private async Task OnActivated()
{
await ScriptRepoUpdater.Instance.ImportScriptFromClipboard();
}
[RelayCommand]
private void OnHide()
{

View File

@ -1,4 +1,5 @@
using BetterGenshinImpact.Core.Config;
using BetterGenshinImpact.Core.Script.Group;
using BetterGenshinImpact.Core.Script.Project;
using BetterGenshinImpact.Service.Interface;
using CommunityToolkit.Mvvm.ComponentModel;
@ -10,18 +11,16 @@ using System.Collections.ObjectModel;
using System.Diagnostics;
using System.IO;
using System.Threading.Tasks;
using BetterGenshinImpact.Core.Script.Group;
using Wpf.Ui;
using Wpf.Ui.Controls;
using Wpf.Ui.Violeta.Controls;
using BetterGenshinImpact.GameTask.AutoPathing.Model;
namespace BetterGenshinImpact.ViewModel.Pages;
public partial class JsListViewModel : ObservableObject, INavigationAware, IViewModel
{
private readonly ILogger<JsListViewModel> _logger = App.GetLogger<JsListViewModel>();
private readonly string scriptPath = Global.Absolute("Script");
private readonly string scriptPath = Global.ScriptPath();
[ObservableProperty]
private ObservableCollection<ScriptProject> _scriptItems = [];