diff --git a/src/App.xaml.cs b/src/App.xaml.cs index 4b4f7a8..d4d4436 100644 --- a/src/App.xaml.cs +++ b/src/App.xaml.cs @@ -4,45 +4,16 @@ using Microsoft.Extensions.Logging; using Serilog; using System.IO; -using System.Net; -using System.Net.Http; -using System.Runtime.InteropServices; +using Windows.Win32; using System.Windows.Threading; -using PipManager.Core.Services.PackageSearchService; +using PipManager.Core.Services; +using PipManager.Windows.Extensions; using PipManager.Windows.Services; -using PipManager.Windows.Services.Action; -using PipManager.Windows.Services.Environment; -using PipManager.Windows.Services.Mask; -using PipManager.Windows.Services.Overlay; -using PipManager.Windows.Services.Toast; -using PipManager.Windows.ViewModels.Pages.Action; -using PipManager.Windows.ViewModels.Pages.Environment; -using PipManager.Windows.ViewModels.Pages.Lab; -using PipManager.Windows.ViewModels.Pages.Library; -using PipManager.Windows.ViewModels.Pages.Overlay; -using PipManager.Windows.ViewModels.Pages.Search; -using PipManager.Windows.ViewModels.Pages.Tools; using PipManager.Windows.ViewModels.Windows; -using PipManager.Windows.Views.Pages.About; -using PipManager.Windows.Views.Pages.Action; -using PipManager.Windows.Views.Pages.Environment; -using PipManager.Windows.Views.Pages.Lab; -using PipManager.Windows.Views.Pages.Library; -using PipManager.Windows.Views.Pages.Overlay; -using PipManager.Windows.Views.Pages.Search; -using PipManager.Windows.Views.Pages.Settings; -using PipManager.Windows.Views.Pages.Tools; using PipManager.Windows.Views.Windows; -using Wpf.Ui; namespace PipManager.Windows; -using AboutViewModel = ViewModels.Pages.About.AboutViewModel; -using SettingsViewModel = ViewModels.Pages.Settings.SettingsViewModel; - -/// -/// Interaction logic for App.xaml -/// public partial class App { private static readonly IHost Host = Microsoft.Extensions.Hosting.Host @@ -57,90 +28,25 @@ public partial class App { services.AddHostedService(); - services.AddTransient(_ => - { - var client = new HttpClient(new HttpClientHandler { AutomaticDecompression = DecompressionMethods.All }) { DefaultRequestVersion = HttpVersion.Version20 }; - client.DefaultRequestHeaders.Add("User-Agent", $"PipManager.Windows/{AppInfo.AppVersion}"); - client.Timeout = TimeSpan.FromSeconds(6); - return client; - }); + services.AddHttpClient(AppInfo.AppVersion); - // Window services.AddSingleton(); services.AddSingleton(); - - // Services - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - - // Pages - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - - services.AddSingleton(); - services.AddSingleton(); - - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); + services.RegisterServices(); + services.RegisterViews(); + services.RegisterViewModels(); }).Build(); - /// - /// Gets registered service. - /// - /// Type of the service to get. - /// Instance of the service or . public static T GetService() where T : class { return Host.Services.GetService(typeof(T)) as T ?? throw new InvalidOperationException("Service not found."); } - [LibraryImport("kernel32.dll")] - [return: MarshalAs(UnmanagedType.Bool)] - private static partial void FreeConsole(); - private bool _showConsoleWindow; public static bool IsDebugMode { get; private set; } - /// - /// Occurs when the application is loading. - /// private void OnStartup(object sender, StartupEventArgs e) { Environment.SetEnvironmentVariable("WEBVIEW2_USER_DATA_FOLDER", AppInfo.CachesDir); @@ -154,35 +60,26 @@ private void OnStartup(object sender, StartupEventArgs e) break; } } + IsDebugMode = _showConsoleWindow; var appStarting = new AppStarting { ShowConsoleWindow = _showConsoleWindow }; appStarting.StartLogging(); - appStarting.LoadLanguage(); - appStarting.CachesDeletion(); + AppStarting.LoadLanguage(); + AppStarting.CachesDeletion(); Host.Start(); - IsDebugMode = _showConsoleWindow; } - /// - /// Occurs when the application is closing. - /// private async void OnExit(object sender, ExitEventArgs e) { - if (_showConsoleWindow) - { - FreeConsole(); - } + if (_showConsoleWindow) PInvoke.FreeConsole(); await Host.StopAsync(); Host.Dispose(); Log.Information("Logging ended"); await Log.CloseAndFlushAsync(); } - /// - /// Occurs when an exception is thrown by an application but not handled. - /// private void OnDispatcherUnhandledException(object sender, DispatcherUnhandledExceptionEventArgs e) { Log.Error($"Exception: {e.Exception}"); diff --git a/src/AppStarting.cs b/src/AppStarting.cs index 2f92515..ce1db34 100644 --- a/src/AppStarting.cs +++ b/src/AppStarting.cs @@ -1,17 +1,13 @@ using Serilog; using System.Globalization; using System.IO; -using System.Runtime.InteropServices; +using Windows.Win32; using PipManager.Core.Configuration; namespace PipManager.Windows; -public partial class AppStarting +public class AppStarting { - [LibraryImport("kernel32.dll")] - [return: MarshalAs(UnmanagedType.Bool)] - private static partial void AllocConsole(); - public bool ShowConsoleWindow = false; public AppStarting() @@ -22,7 +18,7 @@ public AppStarting() Directory.CreateDirectory(AppInfo.CachesDir); } - public void LoadLanguage() + public static void LoadLanguage() { var language = Configuration.AppConfig!.Personalization.Language; if (language != "Auto") @@ -36,7 +32,7 @@ public void StartLogging() { if (ShowConsoleWindow) { - AllocConsole(); + PInvoke.AllocConsole(); } Log.Logger = new LoggerConfiguration() .Enrich.WithProperty("Version", AppInfo.AppVersion) @@ -47,7 +43,7 @@ public void StartLogging() Log.Information("Logging started"); } - public void CachesDeletion() + public static void CachesDeletion() { if (!Directory.Exists(AppInfo.CachesDir)) return; var directoryInfo = new DirectoryInfo(AppInfo.CachesDir); diff --git a/src/Controls/Toast.xaml b/src/Controls/Toast.xaml index 0ab8439..e4d8b82 100644 --- a/src/Controls/Toast.xaml +++ b/src/Controls/Toast.xaml @@ -23,10 +23,7 @@ Margin="1" VerticalAlignment="Stretch"> - + (); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + } + + public static void RegisterViews(this IServiceCollection services) + { + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + } + + public static void RegisterViewModels(this IServiceCollection services) + { + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + } +} \ No newline at end of file diff --git a/src/Models/Action/ActionListItem.cs b/src/Models/Action/ActionListItem.cs index 3f61a23..c4de1bc 100644 --- a/src/Models/Action/ActionListItem.cs +++ b/src/Models/Action/ActionListItem.cs @@ -16,7 +16,7 @@ public ActionListItem(ActionType operationType, string[] operationCommand, strin DisplayCommand = displayCommand switch { "" => string.Join(' ', operationCommand), - _ => displayCommand, + _ => displayCommand }; OperationDescription = operationType switch { @@ -25,7 +25,7 @@ public ActionListItem(ActionType operationType, string[] operationCommand, strin ActionType.InstallByRequirements => Lang.Action_Operation_InstallByRequirements, ActionType.Download => Lang.Action_Operation_Download, ActionType.Update => Lang.Action_Operation_Update, - _ => "Unknown", + _ => "Unknown" }; OperationIcon = operationType switch @@ -41,7 +41,7 @@ public ActionListItem(ActionType operationType, string[] operationCommand, strin ActionType.Uninstall => "Danger", ActionType.Install or ActionType.InstallByRequirements => "Success", ActionType.Update or ActionType.Download => "Caution", - _ => "Primary", + _ => "Primary" }; ConsoleOutput = ""; ConsoleError = Lang.Action_ConsoleError_Empty; diff --git a/src/NativeMethods.txt b/src/NativeMethods.txt new file mode 100644 index 0000000..bda9ff2 --- /dev/null +++ b/src/NativeMethods.txt @@ -0,0 +1,2 @@ +AllocConsole +FreeConsole \ No newline at end of file diff --git a/src/PipManager.Windows.csproj b/src/PipManager.Windows.csproj index e05ef0e..4ed9c9d 100644 --- a/src/PipManager.Windows.csproj +++ b/src/PipManager.Windows.csproj @@ -3,7 +3,7 @@ WinExe net8.0-windows - 12.0 + preview app.manifest Assets\icon.ico enable @@ -28,21 +28,26 @@ + + - + - - - - + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/src/Services/Action/ActionService.cs b/src/Services/Action/ActionService.cs index c5a12c9..ae20e8b 100644 --- a/src/Services/Action/ActionService.cs +++ b/src/Services/Action/ActionService.cs @@ -71,7 +71,6 @@ public void Runner() ? $"[Runner] {item} uninstall sub-task completed" : $"[Runner] {item} uninstall sub-task failed\n Reason:{result.Message}"); } - Log.Information($"[Runner] Task {currentAction.OperationType} Completed"); break; } @@ -93,7 +92,6 @@ public void Runner() ? $"[Runner] {item} install sub-task completed" : $"[Runner] {item} install sub-task failed\n Reason:{result.Message}"); } - Log.Information($"[Runner] Task {currentAction.OperationType} Completed"); break; } case ActionType.InstallByRequirements: @@ -108,7 +106,6 @@ public void Runner() currentAction.DetectIssue = true; consoleError.AppendLine(result.Message); } - Log.Information($"[Runner] Task {currentAction.OperationType} Completed"); break; } case ActionType.Download: @@ -129,7 +126,6 @@ public void Runner() ? $"[Runner] {item} download sub-task completed" : $"[Runner] {item} download sub-task failed\n Reason:{result.Message}"); } - Log.Information($"[Runner] Task {currentAction.OperationType} Completed"); break; } case ActionType.Update: @@ -150,12 +146,13 @@ public void Runner() ? $"[Runner] {item} update sub-task completed" : $"[Runner] {item} update sub-task failed\n Reason:{result.Message}"); } - Log.Information($"[Runner] Task {currentAction.OperationType} Completed"); break; } default: throw new ArgumentOutOfRangeException(); } + + Log.Information($"[Runner] Task {currentAction.OperationType} Completed"); currentAction.CompletedSubTaskNumber = currentAction.TotalSubTaskNumber; currentAction.OperationStatus = "Completed"; if (errorDetection) diff --git a/src/Services/Environment/EnvironmentService.cs b/src/Services/Environment/EnvironmentService.cs index c71c575..365918d 100644 --- a/src/Services/Environment/EnvironmentService.cs +++ b/src/Services/Environment/EnvironmentService.cs @@ -3,8 +3,8 @@ using System.IO; using System.Net.Http; using System.Text; +using System.Text.Json; using System.Text.RegularExpressions; -using Newtonsoft.Json; using PipManager.Core.Configuration; using PipManager.Core.Configuration.Models; using PipManager.Core.Extensions; @@ -63,7 +63,7 @@ public ActionResponse PurgeEnvironmentCache(EnvironmentModel environmentModel) public async Task?> GetLibraries() { - if (Configuration.AppConfig!.SelectedEnvironment is null) + if (Configuration.AppConfig?.SelectedEnvironment == null) { return null; } @@ -72,191 +72,192 @@ public ActionResponse PurgeEnvironmentCache(EnvironmentModel environmentModel) Path.GetDirectoryName(Configuration.AppConfig.SelectedEnvironment!.PythonPath)!, @"Lib\site-packages")); - var packages = new ConcurrentBag(); - var ioTaskList = new List(); + var packages = new ConcurrentQueue(); var distInfoDirectories = packageDirInfo.GetDirectories() .Where(path => path.Name.EndsWith(".dist-info")) - .AsParallel() .ToList(); + var semaphore = new SemaphoreSlim(20); + var ioTaskList = new List(); + foreach (var distInfoDirectory in distInfoDirectories) { + await semaphore.WaitAsync(); var task = Task.Run(async () => { - var distInfoDirectoryFullName = distInfoDirectory.FullName; - var distInfoDirectoryName = distInfoDirectory.Name; - - // Basic - var packageBasicInfo = distInfoDirectoryName[..^10].Split('-'); - var packageName = packageBasicInfo[0]; - var packageVersion = packageBasicInfo[1]; - - if (packageName == "pip") return; - - // Metadata - var metadataDict = new Dictionary>(); - var lastValidKey = ""; - var lastValidIndex = 0; - var classifiers = new Dictionary>(); - await foreach (var line in File.ReadLinesAsync(Path.Combine(distInfoDirectoryFullName, "METADATA"))) + try { - if (line == "") - { - break; - } - - var key = line.Split(": ")[0]; - var value = line.Replace(key + ": ", ""); - if (!string.IsNullOrWhiteSpace(key) && !key.StartsWith(' ')) - { - key = key.ToLower(); - if (!metadataDict.ContainsKey(key)) - { - metadataDict.Add(key, []); - lastValidKey = key; - lastValidIndex = 0; - } - else - { - lastValidIndex++; - } - - metadataDict[key].Add(new StringBuilder(value)); - } - else - { - metadataDict[lastValidKey][lastValidIndex].Append('\n').Append(value); - } + await ProcessDistInfoDirectory(distInfoDirectory, packages); } - var metadata = metadataDict.ToDictionary( - pair => pair.Key, - pair => pair.Value.Select(sb => sb.ToString()).ToList() - ); - foreach (var item in metadata.GetValueOrDefault("classifier", [])) + finally { - var keyValues = item.Split(" :: "); + semaphore.Release(); + } + }); + ioTaskList.Add(task); + } + await Task.WhenAll(ioTaskList); + Log.Information($"[EnvironmentService] Found {packages.Count} packages"); + + return packages.OrderBy(x => x.Name).ToList(); + } - if (keyValues.Length < 2) - { - continue; - } + private static async Task ProcessDistInfoDirectory(DirectoryInfo distInfoDirectory, ConcurrentQueue packages) + { + var distInfoDirectoryFullName = distInfoDirectory.FullName; + var distInfoDirectoryName = distInfoDirectory.Name; - var key = keyValues[0]; - var value = string.Join(" :: ", keyValues[1..]); + // Basic + var packageBasicInfo = distInfoDirectoryName[..^10].Split('-'); + var packageName = packageBasicInfo[0]; + var packageVersion = packageBasicInfo[1]; - if (!classifiers.TryGetValue(key, out var existingList)) - { - existingList = []; - classifiers.Add(key, existingList); - } + if (packageName == "pip") return; - existingList.Add(value); - } + // Metadata + var metadataDict = new Dictionary>(); + var classifiers = new Dictionary>(); + + var metadataFilePath = Path.Combine(distInfoDirectoryFullName, "METADATA"); + if (File.Exists(metadataFilePath)) + { + string? currentKey = null; + await foreach (var line in File.ReadLinesAsync(metadataFilePath)) + { + if (string.IsNullOrWhiteSpace(line)) break; - // Record - var record = new List(); - var actualPath = ""; - await foreach (var line in File.ReadLinesAsync(Path.Combine(distInfoDirectoryFullName, "RECORD"))) + var parts = line.Split(": ", 2); + if (parts.Length == 2) { - var dirIdentifier = line.Split('/')[0]; - if (dirIdentifier == distInfoDirectoryName || dirIdentifier[0] == '.') continue; - record.Add(line); - if (actualPath == "") + currentKey = parts[0].ToLower(); + if (!metadataDict.TryGetValue(currentKey, out List? value)) { - actualPath = Path.Combine(packageDirInfo.FullName, dirIdentifier); + value = []; + metadataDict[currentKey] = value; } - } - - // Extra - var projectUrl = metadata.GetValueOrDefault("project-url", []); - var projectUrlDictionary = new List(); - if (projectUrl.Count != 0) - { - projectUrlDictionary.AddRange(projectUrl.Select(url => new LibraryDetailProjectUrlModel - { - Icon = url.Split(", ")[0].ToLower() switch - { - "homepage" or "home" => SymbolRegular.Home24, - "download" => SymbolRegular.ArrowDownload24, - "changelog" or "changes" or "release notes" => SymbolRegular - .ClipboardTextEdit24, - "bug tracker" or "issue tracker" or "bug reports" or "issues" or "tracker" => - SymbolRegular.Bug24, - "source code" or "source" or "repository" or "code" => SymbolRegular - .Code24, - "funding" or "donate" or "donations" => SymbolRegular.Money24, - "documentation" => SymbolRegular.Document24, - "commercial" => SymbolRegular.PeopleMoney24, - "support" => SymbolRegular.PersonSupport24, - "chat" or "q & a" => SymbolRegular.ChatHelp24, - _ => SymbolRegular.Link24 - }, - UrlType = url.Split(", ")[0], - Url = url.Split(", ")[1] - })); + value.Add(parts[1]); } - else + else if (currentKey != null) { - projectUrlDictionary.Add(new LibraryDetailProjectUrlModel - { - Icon = SymbolRegular.Question24, - UrlType = "Unknown", - Url = "" - }); + metadataDict[currentKey][^1] += "\n" + line; } + } - - packages.Add(new PackageItem + foreach (var item in metadataDict.GetValueOrDefault("classifier", [])) + { + var keyValues = item.Split(" :: "); + if (keyValues.Length < 2) continue; + + var key = keyValues[0]; + var value = string.Join(" :: ", keyValues[1..]); + + if (!classifiers.ContainsKey(key)) { - Name = packageName, - Version = packageVersion, - DetailedVersion = PackageValidator.CheckVersion(packageVersion), - Path = actualPath, - DistInfoPath = distInfoDirectoryFullName, - Summary = metadata.GetValueOrDefault("summary", [""])[0], - Author = metadata.GetValueOrDefault("author", []), - AuthorEmail = metadata.GetValueOrDefault("author-email", [""])[0], - ProjectUrl = projectUrlDictionary, - Classifier = classifiers, - Metadata = metadata, - Record = record - }); - }); - ioTaskList.Add(task); + classifiers[key] = []; + } + classifiers[key].Add(value); + } } - await Task.WhenAll([.. ioTaskList]); - Log.Information($"[EnvironmentService] Found {packages.Count} packages"); - return [.. packages.OrderBy(x => x.Name)]; + + // Record + var record = new List(); + var recordFilePath = Path.Combine(distInfoDirectoryFullName, "RECORD"); + if (File.Exists(recordFilePath)) + { + using var reader = new StreamReader(recordFilePath); + while (await reader.ReadLineAsync() is { } line) + { + ReadOnlySpan lineSpan = line.AsSpan(); + int slashIndex = lineSpan.IndexOf('/'); + ReadOnlySpan dirIdentifierSpan = (slashIndex == -1) ? lineSpan : lineSpan[..slashIndex]; + + if (dirIdentifierSpan.SequenceEqual(distInfoDirectoryName.AsSpan()) || dirIdentifierSpan[0] == '.') + continue; + + record.Add(line); + } + } + + // Extra + var projectUrls = metadataDict.GetValueOrDefault("project-url", []); + var projectUrlDictionary = projectUrls.Count != 0 + ? projectUrls.Select(url => new LibraryDetailProjectUrlModel + { + Icon = url.Split(", ")[0].ToLower() switch + { + "homepage" or "home" => SymbolRegular.Home24, + "download" => SymbolRegular.ArrowDownload24, + "changelog" or "changes" or "release notes" => SymbolRegular.ClipboardTextEdit24, + "bug tracker" or "issue tracker" or "bug reports" or "issues" or "tracker" => SymbolRegular.Bug24, + "source code" or "source" or "repository" or "code" => SymbolRegular.Code24, + "funding" or "donate" or "donations" => SymbolRegular.Money24, + "documentation" => SymbolRegular.Document24, + "commercial" => SymbolRegular.PeopleMoney24, + "support" => SymbolRegular.PersonSupport24, + "chat" or "q & a" => SymbolRegular.ChatHelp24, + _ => SymbolRegular.Link24 + }, + UrlType = url.Split(", ")[0], + Url = url.Split(", ")[1] + }).ToList() + : [new LibraryDetailProjectUrlModel { Icon = SymbolRegular.Question24, UrlType = "Unknown", Url = "" }]; + + packages.Enqueue(new PackageItem + { + Name = packageName, + Version = packageVersion, + DetailedVersion = PackageValidator.CheckVersion(packageVersion), + Path = distInfoDirectoryFullName, + DistInfoPath = distInfoDirectoryFullName, + Summary = metadataDict.GetValueOrDefault("summary", [""]).First(), + Author = metadataDict.GetValueOrDefault("author", []), + AuthorEmail = metadataDict.GetValueOrDefault("author-email", [""]).First(), + ProjectUrl = projectUrlDictionary, + Classifier = classifiers, + Metadata = metadataDict, + Record = record + }); } public async Task GetVersions(string packageName, CancellationToken cancellationToken, bool detectNonRelease = true) { try { + var emptyArray = Array.Empty(); + packageName = PackageNameFilterRegex().Replace(packageName, ""); packageName = PackageNameNormalizerRegex().Replace(packageName, "-").ToLower(); + if (!PackageNameVerificationRegex().IsMatch(packageName)) - return new GetVersionsResponse { Status = 2, Versions = [] }; - var responseMessage = - await httpClient.GetAsync( - $"{Configuration.AppConfig!.PackageSource.Source.GetPackageSourceUrl("pypi")}{packageName}/json", cancellationToken); + return new GetVersionsResponse { Status = 2, Versions = emptyArray }; + + var responseMessage = await httpClient.GetAsync( + $"{Configuration.AppConfig!.PackageSource.Source.GetPackageSourceUrl("pypi")}{packageName}/json", cancellationToken); + if (!responseMessage.IsSuccessStatusCode) + { + Log.Warning($"[EnvironmentService] Failed to fetch package {packageName}, StatusCode: {responseMessage.StatusCode}"); + return new GetVersionsResponse { Status = 1, Versions = emptyArray }; + } + var response = await responseMessage.Content.ReadAsStringAsync(cancellationToken); - - var pypiPackageInfo = JsonConvert.DeserializeObject(response) + var pypiPackageInfo = JsonSerializer.Deserialize(response) ?.Releases? - .Where(item => item.Value.Count != 0).OrderBy(e => e.Value[0].UploadTime) - .ThenBy(e => e.Value[0].UploadTime).ToDictionary(pair => pair.Key, pair => pair.Value); + .Where(item => item.Value.Count != 0) + .OrderBy(e => e.Value[0].UploadTime) + .ToDictionary(pair => pair.Key, pair => pair.Value); if (!detectNonRelease && pypiPackageInfo != null) { - pypiPackageInfo = pypiPackageInfo.Where(item => PackageValidator.IsReleaseVersion(item.Key)).ToDictionary(); + pypiPackageInfo = pypiPackageInfo + .Where(item => PackageValidator.IsReleaseVersion(item.Key)) + .ToDictionary(); } if (pypiPackageInfo == null || pypiPackageInfo.Count == 0) { Log.Warning($"[EnvironmentService] {packageName} package not found"); - return new GetVersionsResponse { Status = 1, Versions = [] }; + return new GetVersionsResponse { Status = 1, Versions = emptyArray }; } Log.Information($"[EnvironmentService] Found {packageName}"); @@ -264,12 +265,22 @@ await httpClient.GetAsync( } catch (TaskCanceledException) { - return new GetVersionsResponse { Status = 1, Versions = [] }; + return new GetVersionsResponse { Status = 1, Versions = Array.Empty() }; } - catch (Exception) + catch (HttpRequestException e) + { + Log.Warning($"[EnvironmentService] Network error while fetching versions for {packageName}: {e.Message}"); + return new GetVersionsResponse { Status = 1, Versions = Array.Empty() }; + } + catch (JsonException e) + { + Log.Warning($"[EnvironmentService] JSON deserialization error for {packageName}: {e.Message}"); + return new GetVersionsResponse { Status = 1, Versions = Array.Empty() }; + } + catch (Exception e) { - Log.Warning($"[EnvironmentService] Unexpected error when get versions of {packageName} package"); - return new GetVersionsResponse { Status = 1, Versions = [] }; + Log.Warning($"[EnvironmentService] Unexpected error while fetching versions for {packageName}: {e.Message}"); + return new GetVersionsResponse { Status = 1, Versions = Array.Empty() }; } } diff --git a/src/Services/Environment/Response/ActionResponse.cs b/src/Services/Environment/Response/ActionResponse.cs index 51c3f80..cc05e64 100644 --- a/src/Services/Environment/Response/ActionResponse.cs +++ b/src/Services/Environment/Response/ActionResponse.cs @@ -4,7 +4,7 @@ namespace PipManager.Windows.Services.Environment.Response; public class ActionResponse { - public bool Success { get; set; } + public bool Success { get; init; } public ExceptionType? Exception { get; set; } - public string Message { get; set; } = string.Empty; + public string Message { get; init; } = string.Empty; } \ No newline at end of file diff --git a/src/Services/Environment/Response/GetVersionsResponse.cs b/src/Services/Environment/Response/GetVersionsResponse.cs index a597e10..30a4f3e 100644 --- a/src/Services/Environment/Response/GetVersionsResponse.cs +++ b/src/Services/Environment/Response/GetVersionsResponse.cs @@ -2,6 +2,6 @@ public class GetVersionsResponse { - public int Status { get; set; } // 0: Success 1: Not found 2: Invalid Package Name - public string[]? Versions { get; set; } + public int Status { get; init; } // 0: Success 1: Not found 2: Invalid Package Name + public string[]? Versions { get; init; } } \ No newline at end of file diff --git a/src/Services/Environment/Response/ParseRequirementsResponse.cs b/src/Services/Environment/Response/ParseRequirementsResponse.cs deleted file mode 100644 index 3587f4f..0000000 --- a/src/Services/Environment/Response/ParseRequirementsResponse.cs +++ /dev/null @@ -1,13 +0,0 @@ -namespace PipManager.Windows.Services.Environment.Response; - -public class ParseRequirementsResponse -{ - public bool Success { get; init; } - public List? Requirements { get; init; } -} - -public class ParsedRequirement -{ - public required string Name { get; init; } - public required string Specifier { get; init; } -} diff --git a/src/Services/Page/PageService.cs b/src/Services/Page/PageService.cs new file mode 100644 index 0000000..8d59528 --- /dev/null +++ b/src/Services/Page/PageService.cs @@ -0,0 +1,16 @@ +using Wpf.Ui.Abstractions; + +namespace PipManager.Windows.Services.Page; + +public class PageService(IServiceProvider serviceProvider) : INavigationViewPageProvider +{ + public object? GetPage(Type pageType) + { + if (!typeof(FrameworkElement).IsAssignableFrom(pageType)) + { + throw new InvalidOperationException("The page should be a WPF control."); + } + + return serviceProvider.GetService(pageType) as FrameworkElement; + } +} diff --git a/src/ViewModels/Pages/About/AboutViewModel.cs b/src/ViewModels/Pages/About/AboutViewModel.cs index 459f643..4edd1af 100644 --- a/src/ViewModels/Pages/About/AboutViewModel.cs +++ b/src/ViewModels/Pages/About/AboutViewModel.cs @@ -1,7 +1,7 @@ using Serilog; using System.Collections.ObjectModel; using PipManager.Windows.Models.Pages; -using Wpf.Ui.Controls; +using Wpf.Ui.Abstractions.Controls; namespace PipManager.Windows.ViewModels.Pages.About; @@ -12,16 +12,6 @@ public partial class AboutViewModel : ObservableObject, INavigationAware [ObservableProperty] private string _appVersion = "Development"; [ObservableProperty] private bool _debugMode; - public void OnNavigatedTo() - { - if (!_isInitialized) - InitializeViewModel(); - } - - public void OnNavigatedFrom() - { - } - private void InitializeViewModel() { DebugMode = App.IsDebugMode; @@ -33,8 +23,6 @@ private void InitializeViewModel() "https://github.com/Antelcat/Antelcat.I18N"), new AboutNugetLibraryListItem("CommunityToolkit.Mvvm", "MIT", "Copyright © .NET Foundation and Contributors", "https://github.com/CommunityToolkit/dotnet"), - new AboutNugetLibraryListItem("HtmlAgilityPack", "MIT", "Copyright © ZZZ Projects Inc.", - "https://github.com/zzzprojects/html-agility-pack"), new AboutNugetLibraryListItem("Meziantou.Framework.WPF", "MIT", "Copyright (c) 2019 Gérald Barré", "https://github.com/meziantou/Meziantou.Framework"), new AboutNugetLibraryListItem("Microsoft.Extensions.Hosting", "MIT", @@ -43,10 +31,8 @@ private void InitializeViewModel() "© Microsoft Corporation. All rights reserved.", "https://github.com/dotnet/runtime"), new AboutNugetLibraryListItem("Microsoft.Xaml.Behaviors.Wpf", "MIT", "Copyright (c) 2015 Microsoft", "https://github.com/microsoft/XamlBehaviorsWpf"), - new AboutNugetLibraryListItem("Newtonsoft.Json", "MIT", "Copyright (c) 2007 James Newton-King", - "https://github.com/JamesNK/Newtonsoft.Json"), - new AboutNugetLibraryListItem("pythonnet", "MIT", "Copyright (c) 2006-2021 the contributors of the Python.NET project", - "https://github.com/pythonnet/pythonnet"), + new AboutNugetLibraryListItem("System.Text.Json", "MIT", "Microsoft Corporation. All rights reserved.", + "https://github.com/dotnet/runtime"), new AboutNugetLibraryListItem("Serilog", "Apache-2.0", "Copyright © 2013-2020 Serilog Contributors", "https://github.com/serilog/serilog"), new AboutNugetLibraryListItem("Serilog.Extensions.Logging", "Apache-2.0", @@ -74,4 +60,16 @@ private void InitializeViewModel() [ObservableProperty] private ObservableCollection _nugetLibraryList = []; + + public Task OnNavigatedToAsync() + { + if (!_isInitialized) + InitializeViewModel(); + return Task.CompletedTask; + } + + public Task OnNavigatedFromAsync() + { + return Task.CompletedTask; + } } \ No newline at end of file diff --git a/src/ViewModels/Pages/Action/ActionExceptionViewModel.cs b/src/ViewModels/Pages/Action/ActionExceptionViewModel.cs index 3dc8bac..a3764bc 100644 --- a/src/ViewModels/Pages/Action/ActionExceptionViewModel.cs +++ b/src/ViewModels/Pages/Action/ActionExceptionViewModel.cs @@ -8,7 +8,7 @@ using PipManager.Windows.Models.Action; using PipManager.Windows.Services.Action; using PipManager.Windows.Services.Toast; -using Wpf.Ui.Controls; +using Wpf.Ui.Abstractions.Controls; namespace PipManager.Windows.ViewModels.Pages.Action; @@ -29,17 +29,6 @@ public ActionExceptionViewModel(IActionService actionService, IToastService toas Exceptions = new ObservableCollection(_actionService.ExceptionList); } - public void OnNavigatedTo() - { - if (!_isInitialized) - InitializeViewModel(); - UpdateActionExceptionList(); - } - - public void OnNavigatedFrom() - { - } - private void InitializeViewModel() { _isInitialized = true; @@ -100,4 +89,17 @@ private void ExceptionCopyToClipboard(string? parameter) _toastService.Success(Lang.ActionException_CopyToClipboardNotice); Log.Information("[Action][Exceptions] Copied exception to clipboard"); } + + public Task OnNavigatedToAsync() + { + if (!_isInitialized) + InitializeViewModel(); + UpdateActionExceptionList(); + return Task.CompletedTask; + } + + public Task OnNavigatedFromAsync() + { + return Task.CompletedTask; + } } \ No newline at end of file diff --git a/src/ViewModels/Pages/Action/ActionViewModel.cs b/src/ViewModels/Pages/Action/ActionViewModel.cs index e415041..bd46f23 100644 --- a/src/ViewModels/Pages/Action/ActionViewModel.cs +++ b/src/ViewModels/Pages/Action/ActionViewModel.cs @@ -6,7 +6,7 @@ using PipManager.Windows.Views.Pages.Action; using Serilog; using Wpf.Ui; -using Wpf.Ui.Controls; +using Wpf.Ui.Abstractions.Controls; namespace PipManager.Windows.ViewModels.Pages.Action; @@ -29,16 +29,6 @@ public ActionViewModel(IActionService actionService, INavigationService navigati Actions = _actionService.ActionList; } - public void OnNavigatedTo() - { - if (!_isInitialized) - InitializeViewModel(); - } - - public void OnNavigatedFrom() - { - } - private void InitializeViewModel() { _isInitialized = true; @@ -70,4 +60,16 @@ private void CancelAction(string? operationId) Log.Information("[Action] Operation canceled: {OperationId}", operationId); } } + + public Task OnNavigatedToAsync() + { + if (!_isInitialized) + InitializeViewModel(); + return Task.CompletedTask; + } + + public Task OnNavigatedFromAsync() + { + return Task.CompletedTask; + } } \ No newline at end of file diff --git a/src/ViewModels/Pages/Environment/AddEnvironmentViewModel.cs b/src/ViewModels/Pages/Environment/AddEnvironmentViewModel.cs index 6088e83..5c3bcfc 100644 --- a/src/ViewModels/Pages/Environment/AddEnvironmentViewModel.cs +++ b/src/ViewModels/Pages/Environment/AddEnvironmentViewModel.cs @@ -9,7 +9,7 @@ using PipManager.Windows.Services.Environment; using PipManager.Windows.Services.Toast; using Wpf.Ui; -using Wpf.Ui.Controls; +using Wpf.Ui.Abstractions.Controls; namespace PipManager.Windows.ViewModels.Pages.Environment; @@ -17,18 +17,6 @@ public partial class AddEnvironmentViewModel(INavigationService navigationServic { private bool _isInitialized; - public void OnNavigatedTo() - { - if (!_isInitialized) - InitializeViewModel(); - ByWay = 0; - _ = RefreshPipList(); - } - - public void OnNavigatedFrom() - { - } - private void InitializeViewModel() { _isInitialized = true; @@ -42,7 +30,7 @@ private void InitializeViewModel() [RelayCommand] private void ChangeWay() { - Log.Information($"[AddEnvironment] Addition way changed to index:{ByWay}"); + Log.Information($"[AddEnvironment] Addition way changed to index: {ByWay}"); } #endregion ByWaysList @@ -216,4 +204,18 @@ private void AddEnvironment(string parameter) } #endregion Add + + public Task OnNavigatedToAsync() + { + if (!_isInitialized) + InitializeViewModel(); + ByWay = 0; + _ = RefreshPipList(); + return Task.CompletedTask; + } + + public Task OnNavigatedFromAsync() + { + return Task.CompletedTask; + } } \ No newline at end of file diff --git a/src/ViewModels/Pages/Environment/EnvironmentViewModel.cs b/src/ViewModels/Pages/Environment/EnvironmentViewModel.cs index 63e8b06..a864eb5 100644 --- a/src/ViewModels/Pages/Environment/EnvironmentViewModel.cs +++ b/src/ViewModels/Pages/Environment/EnvironmentViewModel.cs @@ -14,6 +14,7 @@ using PipManager.Windows.Views.Pages.Action; using PipManager.Windows.Views.Pages.Environment; using Wpf.Ui; +using Wpf.Ui.Abstractions.Controls; using Wpf.Ui.Controls; using Wpf.Ui.Extensions; @@ -27,36 +28,6 @@ public partial class EnvironmentViewModel(INavigationService navigationService, { private bool _isInitialized; - public void OnNavigatedTo() - { - if (!_isInitialized) - InitializeViewModel(); - - Configuration.RefreshAllEnvironments(); - EnvironmentItems = - new ObservableCollection(Configuration.AppConfig!.Environments); - var currentEnvironment = Configuration.AppConfig.SelectedEnvironment; - foreach (var environmentItem in EnvironmentItems) - { - if (currentEnvironment is null || environmentItem.PythonPath != currentEnvironment.PythonPath) - { - continue; - } - - CurrentEnvironment = environmentItem; - - var mainWindowViewModel = App.GetService(); - mainWindowViewModel.ApplicationTitle = - $"Pip Manager | {CurrentEnvironment.PipVersion} for {CurrentEnvironment.PythonVersion}"; - Log.Information($"[Environment] Current Environment changed: {CurrentEnvironment.PythonPath}"); - break; - } - } - - public void OnNavigatedFrom() - { - } - private void InitializeViewModel() { _isInitialized = true; @@ -77,12 +48,15 @@ private async Task DeleteEnvironment() ContentDialogCreateOptions.Warning(Lang.ContentDialog_Message_EnvironmentDeletion, Lang.ContentDialog_PrimaryButton_Action)); if (result != ContentDialogResult.Primary) return; + Log.Information($"[Environment] Environment has been removed from list ({CurrentEnvironment!.PipVersion} for {CurrentEnvironment.PythonVersion})"); + EnvironmentItems.Remove(CurrentEnvironment!); CurrentEnvironment = null; Configuration.AppConfig!.SelectedEnvironment = null; Configuration.AppConfig.Environments = [..EnvironmentItems]; Configuration.Save(); + var mainWindowViewModel = App.GetService(); mainWindowViewModel.ApplicationTitle = "Pip Manager"; EnvironmentSelected = false; @@ -91,7 +65,11 @@ private async Task DeleteEnvironment() [RelayCommand] private async Task CheckEnvironment() { - var environmentAvailable = environmentService.CheckEnvironmentAvailable(CurrentEnvironment!); + maskService.Show(Lang.Environment_Operation_VerifyEnvironment); + var environmentAvailable = await Task.Run(() => environmentService.CheckEnvironmentAvailable(CurrentEnvironment!)); + Task.WaitAll(); + maskService.Hide(); + if (environmentAvailable.Success) { Log.Information($"[Environment] Environment is available ({CurrentEnvironment!.PipVersion} for {CurrentEnvironment.PythonVersion})"); @@ -115,22 +93,26 @@ private async Task CheckEnvironmentUpdate() { maskService.Show(Lang.Environment_Operation_CheckEnvironmentUpdate); var latest = ""; - await Task.Run(async () => + try { - var versions = await environmentService.GetVersions("pip", new CancellationToken(), Configuration.AppConfig!.PackageSource.AllowNonRelease); + var versions = await Task.Run(() => environmentService.GetVersions("pip", new CancellationToken(), Configuration.AppConfig!.PackageSource.AllowNonRelease)); if (versions.Status == 0) { latest = versions.Versions!.Last(); } - }); - Task.WaitAll(); - maskService.Hide(); + } + finally + { + maskService.Hide(); + } + var current = Configuration.AppConfig!.SelectedEnvironment!.PipVersion.Trim(); - if (latest != current && latest != string.Empty) + if (!string.IsNullOrEmpty(latest) && latest != current) { Log.Information($"[Environment] Environment update available ({current} => {latest})"); var message = $"{Lang.ContentDialog_Message_FindUpdate}\n\n{Lang.EnvironmentCheckEnvironmentUpdate_CurrentVersion}{current}\n{Lang.EnvironmentCheckEnvironmentUpdate_LatestVersion}{latest}"; var result = await contentDialogService.ShowSimpleDialogAsync(ContentDialogCreateOptions.Notice(message)); + if (result == ContentDialogResult.Primary) { actionService.AddOperation(new ActionListItem @@ -143,7 +125,7 @@ await Task.Run(async () => Configuration.RefreshAllEnvironments(); } } - else if (latest == string.Empty) + else if (string.IsNullOrEmpty(latest)) { toastService.Error(Lang.ContentDialog_Message_NetworkError); Log.Error("[Environment] Network error while checking for updates (environment: {environment})", CurrentEnvironment!.PipVersion); @@ -198,4 +180,29 @@ private void AddEnvironment() } #endregion Add Environment + + public async Task OnNavigatedToAsync() + { + if (!_isInitialized) + InitializeViewModel(); + + Configuration.RefreshAllEnvironments(); + EnvironmentItems = + new ObservableCollection(Configuration.AppConfig!.Environments); + EnvironmentItems = new ObservableCollection(Configuration.AppConfig.Environments); + await Task.Delay(50); + CurrentEnvironment = EnvironmentItems.FirstOrDefault(item => item.PythonPath == Configuration.AppConfig.SelectedEnvironment?.PythonPath); + + if (CurrentEnvironment != null) + { + var mainWindowViewModel = App.GetService(); + mainWindowViewModel.ApplicationTitle = $"Pip Manager | {CurrentEnvironment.PipVersion} for {CurrentEnvironment.PythonVersion}"; + Log.Information($"[Environment] Current Environment set: {CurrentEnvironment.PythonPath}"); + } + } + + public Task OnNavigatedFromAsync() + { + return Task.CompletedTask; + } } \ No newline at end of file diff --git a/src/ViewModels/Pages/Lab/LabViewModel.cs b/src/ViewModels/Pages/Lab/LabViewModel.cs index 5c0797e..2d54258 100644 --- a/src/ViewModels/Pages/Lab/LabViewModel.cs +++ b/src/ViewModels/Pages/Lab/LabViewModel.cs @@ -1,7 +1,7 @@ using PipManager.Windows.Models.Action; using PipManager.Windows.Services.Action; using Serilog; -using Wpf.Ui.Controls; +using Wpf.Ui.Abstractions.Controls; namespace PipManager.Windows.ViewModels.Pages.Lab; @@ -20,19 +20,22 @@ private void ActionTest() progressIntermediate: false )); } - public void OnNavigatedTo() + + private void InitializeViewModel() { - if (!_isInitialized) - InitializeViewModel(); + _isInitialized = true; + Log.Information("[Lab] Initialized"); } - public void OnNavigatedFrom() + public Task OnNavigatedToAsync() { + if (!_isInitialized) + InitializeViewModel(); + return Task.CompletedTask; } - private void InitializeViewModel() + public Task OnNavigatedFromAsync() { - _isInitialized = true; - Log.Information("[Lab] Initialized"); + return Task.CompletedTask; } } \ No newline at end of file diff --git a/src/ViewModels/Pages/Library/LibraryDetailViewModel.cs b/src/ViewModels/Pages/Library/LibraryDetailViewModel.cs index c979fc5..0111c02 100644 --- a/src/ViewModels/Pages/Library/LibraryDetailViewModel.cs +++ b/src/ViewModels/Pages/Library/LibraryDetailViewModel.cs @@ -6,6 +6,7 @@ using PipManager.Windows.Services.Toast; using PipManager.Windows.Views.Pages.Library; using Wpf.Ui; +using Wpf.Ui.Abstractions.Controls; using Wpf.Ui.Controls; namespace PipManager.Windows.ViewModels.Pages.Library; @@ -48,16 +49,6 @@ public LibraryDetailViewModel(INavigationService navigationService, IToastServic WeakReferenceMessenger.Default.Register(this, Receive); } - public void OnNavigatedTo() - { - if (!_isInitialized) - InitializeViewModel(); - } - - public void OnNavigatedFrom() - { - } - private void InitializeViewModel() { _isInitialized = true; @@ -144,4 +135,16 @@ private void Receive(object recipient, LibraryDetailMessage message) #endregion Classifier } + + public Task OnNavigatedToAsync() + { + if (!_isInitialized) + InitializeViewModel(); + return Task.CompletedTask; + } + + public Task OnNavigatedFromAsync() + { + return Task.CompletedTask; + } } \ No newline at end of file diff --git a/src/ViewModels/Pages/Library/LibraryInstallViewModel.cs b/src/ViewModels/Pages/Library/LibraryInstallViewModel.cs index 6ad031e..2a8fcff 100644 --- a/src/ViewModels/Pages/Library/LibraryInstallViewModel.cs +++ b/src/ViewModels/Pages/Library/LibraryInstallViewModel.cs @@ -17,7 +17,7 @@ using PipManager.Windows.Services.Toast; using PipManager.Windows.Views.Pages.Action; using Wpf.Ui; -using Wpf.Ui.Controls; +using Wpf.Ui.Abstractions.Controls; namespace PipManager.Windows.ViewModels.Pages.Library; @@ -46,19 +46,6 @@ public LibraryInstallViewModel(IActionService actionService, IMaskService maskSe WeakReferenceMessenger.Default.Register(this, Receive); } - public void OnNavigatedTo() - { - if (!_isInitialized) - InitializeViewModel(); - InstallWheelDependencies = true; - } - - public void OnNavigatedFrom() - { - PreInstallPackages.Clear(); - PreDownloadPackages.Clear(); - } - private void InitializeViewModel() { _isInitialized = true; @@ -431,4 +418,19 @@ private void InstallDistributionsToAction() } #endregion + + public Task OnNavigatedToAsync() + { + if (!_isInitialized) + InitializeViewModel(); + InstallWheelDependencies = true; + return Task.CompletedTask; + } + + public Task OnNavigatedFromAsync() + { + PreInstallPackages.Clear(); + PreDownloadPackages.Clear(); + return Task.CompletedTask; + } } \ No newline at end of file diff --git a/src/ViewModels/Pages/Library/LibraryViewModel.cs b/src/ViewModels/Pages/Library/LibraryViewModel.cs index 25fc9f8..eae2763 100644 --- a/src/ViewModels/Pages/Library/LibraryViewModel.cs +++ b/src/ViewModels/Pages/Library/LibraryViewModel.cs @@ -17,6 +17,7 @@ using PipManager.Windows.Views.Pages.Environment; using PipManager.Windows.Views.Pages.Library; using Wpf.Ui; +using Wpf.Ui.Abstractions.Controls; using Wpf.Ui.Appearance; using Wpf.Ui.Controls; @@ -53,17 +54,6 @@ public LibraryViewModel(INavigationService navigationService, IEnvironmentServic }); } - public void OnNavigatedTo() - { - if (!_isInitialized) - InitializeViewModel(); - _ = RefreshLibrary(); - } - - public void OnNavigatedFrom() - { - } - private void InitializeViewModel() { _isInitialized = true; @@ -219,4 +209,20 @@ await Task.Run(async () => } RefreshTimeUsage = 0; } + + public Task OnNavigatedToAsync() + { + if (!_isInitialized) + InitializeViewModel(); + Application.Current.Dispatcher.InvokeAsync(async () => + { + await RefreshLibrary(); + }); + return Task.CompletedTask; + } + + public Task OnNavigatedFromAsync() + { + return Task.CompletedTask; + } } \ No newline at end of file diff --git a/src/ViewModels/Pages/Search/SearchDetailViewModel.cs b/src/ViewModels/Pages/Search/SearchDetailViewModel.cs index 7b0c7c6..c3fafc5 100644 --- a/src/ViewModels/Pages/Search/SearchDetailViewModel.cs +++ b/src/ViewModels/Pages/Search/SearchDetailViewModel.cs @@ -15,8 +15,8 @@ using PipManager.Windows.Services.Toast; using PipManager.Windows.Views.Pages.Search; using Wpf.Ui; +using Wpf.Ui.Abstractions.Controls; using Wpf.Ui.Appearance; -using Wpf.Ui.Controls; namespace PipManager.Windows.ViewModels.Pages.Search; @@ -72,37 +72,6 @@ public SearchDetailViewModel(INavigationService navigationService, HttpClient ht WeakReferenceMessenger.Default.Register(this, Receive); } - public void OnNavigatedTo() - { - if (!_isInitialized) - InitializeViewModel(); - _navigationService.GetNavigationControl().BreadcrumbBar!.Visibility = Visibility.Collapsed; - switch (_themeService.GetTheme()) - { - case ApplicationTheme.Light: - _themeType = "light"; - ThemeTypeInHex = "#FFFFFF"; - _themeTypeInInteger = 16777215; - break; - - case ApplicationTheme.Dark: - _themeType = "dark"; - ThemeTypeInHex = "#0D1117"; - _themeTypeInInteger = 856343; - break; - case ApplicationTheme.Unknown: - case ApplicationTheme.HighContrast: - default: - throw new ArgumentOutOfRangeException(); - } - SearchDetailPage.ProjectDescriptionWebView!.DefaultBackgroundColor = Color.FromArgb(_themeTypeInInteger); - } - - public void OnNavigatedFrom() - { - _navigationService.GetNavigationControl().BreadcrumbBar!.Visibility = Visibility.Visible; - } - private void InitializeViewModel() { _isInitialized = true; @@ -160,50 +129,105 @@ private void Receive(object recipient, SearchDetailMessage message) { Package = message.Package; - SearchDetailPage.ProjectDescriptionWebView!.Loaded += async (_, _) => + SearchDetailPage.ProjectDescriptionWebView!.Loaded += async (_, _) => await LoadPackageDetailsAsync(message); + } + + private async Task LoadPackageDetailsAsync(SearchDetailMessage message) + { + try { ProjectDescriptionVisibility = false; - var packageVersions = await _environmentService.GetVersions(Package!.Name, new CancellationToken(), Configuration.AppConfig!.PackageSource.AllowNonRelease); - switch (packageVersions.Status) - { - case 1 or 2: - _toastService.Error(Lang.SearchDetail_Exception_NetworkError); - await Task.Delay(1000); - _navigationService.GoBack(); - return; - - default: - AvailableVersions = new ObservableCollection(packageVersions.Versions!.Reverse()); - TargetVersion = AvailableVersions.First(); - break; - } - await CoreWebView2Environment.CreateAsync(null, AppInfo.CachesDir); - await SearchDetailPage.ProjectDescriptionWebView.EnsureCoreWebView2Async().ConfigureAwait(true); - try - { - var projectDescriptionUrl = message.Package.Url; - var html = await _httpClient.GetStringAsync(projectDescriptionUrl); - var htmlDocument = new HtmlDocument(); - htmlDocument.LoadHtml(html); - string projectDescriptionHtml = string.Format(HtmlModel, _themeType, ThemeTypeInHex, htmlDocument.DocumentNode.SelectSingleNode("//*[@id=\"description\"]/div").InnerHtml); - - SearchDetailPage.ProjectDescriptionWebView.CoreWebView2.Profile.PreferredColorScheme = CoreWebView2PreferredColorScheme.Dark; - SearchDetailPage.ProjectDescriptionWebView.NavigateToString(projectDescriptionHtml); - } - catch (Exception ex) - { - Log.Error(ex.Message); - _toastService.Error(Lang.SearchDetail_ProjectDescription_LoadFailed); - string projectDescriptionHtml = string.Format(HtmlModel, _themeType, ThemeTypeInHex, $"

{Lang.SearchDetail_ProjectDescription_LoadFailed}

"); - - SearchDetailPage.ProjectDescriptionWebView.CoreWebView2.Profile.PreferredColorScheme = CoreWebView2PreferredColorScheme.Dark; - SearchDetailPage.ProjectDescriptionWebView.NavigateToString(projectDescriptionHtml); - } - finally - { - await Task.Delay(500); - ProjectDescriptionVisibility = true; - } - }; + await SetupWebViewAsync(message.Package); + } + catch (Exception ex) + { + HandleLoadingError(ex); + } + finally + { + await Task.Delay(500); + ProjectDescriptionVisibility = true; + } + } + + private async Task SetupWebViewAsync(QueryListItemModel package) + { + var packageVersions = await _environmentService.GetVersions(package.Name, new CancellationToken(), Configuration.AppConfig!.PackageSource.AllowNonRelease); + if (packageVersions.Status is 1 or 2) + { + _toastService.Error(Lang.SearchDetail_Exception_NetworkError); + await Task.Delay(1000); + _navigationService.GoBack(); + return; + } + + AvailableVersions = new ObservableCollection(packageVersions.Versions!.Reverse()); + TargetVersion = AvailableVersions.First(); + + await CoreWebView2Environment.CreateAsync(null, AppInfo.CachesDir); + await SearchDetailPage.ProjectDescriptionWebView!.EnsureCoreWebView2Async().ConfigureAwait(true); + await LoadProjectDescriptionAsync(package.Url); + } + + private async Task LoadProjectDescriptionAsync(string projectDescriptionUrl) + { + try + { + var html = await _httpClient.GetStringAsync(projectDescriptionUrl); + var htmlDocument = new HtmlDocument(); + htmlDocument.LoadHtml(html); + string projectDescriptionHtml = string.Format(HtmlModel, _themeType, ThemeTypeInHex, htmlDocument.DocumentNode.SelectSingleNode("//*[@id=\"description\"]/div").InnerHtml); + + SearchDetailPage.ProjectDescriptionWebView!.CoreWebView2.Profile.PreferredColorScheme = CoreWebView2PreferredColorScheme.Dark; + SearchDetailPage.ProjectDescriptionWebView.NavigateToString(projectDescriptionHtml); + } + catch (Exception ex) + { + HandleLoadingError(ex); + } + } + + private void HandleLoadingError(Exception ex) + { + Log.Error(ex.Message); + _toastService.Error(Lang.SearchDetail_ProjectDescription_LoadFailed); + string projectDescriptionHtml = string.Format(HtmlModel, _themeType, ThemeTypeInHex, $"

{Lang.SearchDetail_ProjectDescription_LoadFailed}

"); + SearchDetailPage.ProjectDescriptionWebView!.CoreWebView2.Profile.PreferredColorScheme = CoreWebView2PreferredColorScheme.Dark; + SearchDetailPage.ProjectDescriptionWebView.NavigateToString(projectDescriptionHtml); + } + + public async Task OnNavigatedToAsync() + { + if (!_isInitialized) + InitializeViewModel(); + await Application.Current.Dispatcher.InvokeAsync(() => + { + _navigationService.GetNavigationControl().BreadcrumbBar!.Visibility = Visibility.Collapsed; + }); + switch (_themeService.GetTheme()) + { + case ApplicationTheme.Light: + _themeType = "light"; + ThemeTypeInHex = "#FFFFFF"; + _themeTypeInInteger = 16777215; + break; + + case ApplicationTheme.Dark: + _themeType = "dark"; + ThemeTypeInHex = "#0D1117"; + _themeTypeInInteger = 856343; + break; + case ApplicationTheme.Unknown: + case ApplicationTheme.HighContrast: + default: + throw new ArgumentOutOfRangeException(); + } + SearchDetailPage.ProjectDescriptionWebView!.DefaultBackgroundColor = Color.FromArgb(_themeTypeInInteger); + } + + public Task OnNavigatedFromAsync() + { + _navigationService.GetNavigationControl().BreadcrumbBar!.Visibility = Visibility.Visible; + return Task.CompletedTask; } } \ No newline at end of file diff --git a/src/ViewModels/Pages/Search/SearchViewModel.cs b/src/ViewModels/Pages/Search/SearchViewModel.cs index e398299..2f5e630 100644 --- a/src/ViewModels/Pages/Search/SearchViewModel.cs +++ b/src/ViewModels/Pages/Search/SearchViewModel.cs @@ -8,50 +8,24 @@ using PipManager.Windows.Services.Toast; using PipManager.Windows.Views.Pages.Search; using Wpf.Ui; -using Wpf.Ui.Controls; +using Wpf.Ui.Abstractions.Controls; namespace PipManager.Windows.ViewModels.Pages.Search; -public partial class SearchViewModel(IPackageSearchService packageSearchService, IToastService toastService, IMaskService maskService, INavigationService navigationService) : ObservableObject, INavigationAware +public partial class SearchViewModel(IPackageSearchService packageSearchService, IToastService toastService, IMaskService maskService, INavigationService navigationService) + : ObservableObject, INavigationAware { private bool _isInitialized; - [ObservableProperty] - private ObservableCollection _queryList = []; - - [ObservableProperty] - private string _queryPackageName = ""; - - [ObservableProperty] - private string _totalResultNumber = ""; - - [ObservableProperty] - private bool _onQuerying; - - [ObservableProperty] - private bool _successQueried; - - [ObservableProperty] - private bool _reachesFirstPage = true; - - [ObservableProperty] - private bool _reachesLastPage; - - [ObservableProperty] - private int _currentPage = 1; - - [ObservableProperty] - private int _maxPage = 1; - - public void OnNavigatedTo() - { - if (!_isInitialized) - InitializeViewModel(); - } - - public void OnNavigatedFrom() - { - } + [ObservableProperty] private ObservableCollection _queryList = []; + [ObservableProperty] private string _queryPackageName = ""; + [ObservableProperty] private string _totalResultNumber = ""; + [ObservableProperty] private bool _onQuerying; + [ObservableProperty] private bool _successQueried; + [ObservableProperty] private bool _reachesFirstPage = true; + [ObservableProperty] private bool _reachesLastPage; + [ObservableProperty] private int _currentPage = 1; + [ObservableProperty] private int _maxPage = 1; private void InitializeViewModel() { @@ -162,4 +136,16 @@ private async Task Search(string? parameter) OnQuerying = false; } } + + public Task OnNavigatedToAsync() + { + if (!_isInitialized) + InitializeViewModel(); + return Task.CompletedTask; + } + + public Task OnNavigatedFromAsync() + { + return Task.CompletedTask; + } } \ No newline at end of file diff --git a/src/ViewModels/Pages/Settings/SettingsViewModel.cs b/src/ViewModels/Pages/Settings/SettingsViewModel.cs index cdc6cd0..d7b84af 100644 --- a/src/ViewModels/Pages/Settings/SettingsViewModel.cs +++ b/src/ViewModels/Pages/Settings/SettingsViewModel.cs @@ -10,8 +10,8 @@ using PipManager.Windows.Views.Pages.About; using PipManager.Windows.Views.Pages.Settings; using Wpf.Ui; +using Wpf.Ui.Abstractions.Controls; using Wpf.Ui.Appearance; -using Wpf.Ui.Controls; using Wpf.Ui.Extensions; namespace PipManager.Windows.ViewModels.Pages.Settings; @@ -41,16 +41,6 @@ public SettingsViewModel(ISnackbarService snackbarService, IThemeService themeSe private bool _isInitialized; - public void OnNavigatedTo() - { - if (!_isInitialized) - InitializeViewModel(); - } - - public void OnNavigatedFrom() - { - } - private void InitializeViewModel() { var config = Configuration.AppConfig!; @@ -219,7 +209,7 @@ private void WebViewClearCache() { Directory.Delete(Path.Combine(AppInfo.CachesDir, "EBWebView"), true); _toastService.Success(Lang.Settings_FileManagement_WebViewSettings_CacheCleared); - Log.Information($"[Settings] WebView cache removed"); + Log.Information("[Settings] WebView cache removed"); } catch (DirectoryNotFoundException) { @@ -289,4 +279,16 @@ private static async Task ResetConfigurationAsync() } #endregion File Management + + public Task OnNavigatedToAsync() + { + if (!_isInitialized) + InitializeViewModel(); + return Task.CompletedTask; + } + + public Task OnNavigatedFromAsync() + { + return Task.CompletedTask; + } } \ No newline at end of file diff --git a/src/ViewModels/Pages/Tools/ToolsViewModel.cs b/src/ViewModels/Pages/Tools/ToolsViewModel.cs index 087acff..ad6d238 100644 --- a/src/ViewModels/Pages/Tools/ToolsViewModel.cs +++ b/src/ViewModels/Pages/Tools/ToolsViewModel.cs @@ -1,5 +1,5 @@ using Serilog; -using Wpf.Ui.Controls; +using Wpf.Ui.Abstractions.Controls; namespace PipManager.Windows.ViewModels.Pages.Tools; @@ -10,19 +10,21 @@ public partial class ToolsViewModel : ObservableObject, INavigationAware [ObservableProperty] private string? _testProperty; - public void OnNavigatedTo() + private void InitializeViewModel() { - if (!_isInitialized) - InitializeViewModel(); + _isInitialized = true; + Log.Information("[Tools] Initialized"); } - public void OnNavigatedFrom() + public Task OnNavigatedToAsync() { + if (!_isInitialized) + InitializeViewModel(); + return Task.CompletedTask; } - private void InitializeViewModel() + public Task OnNavigatedFromAsync() { - _isInitialized = true; - Log.Information("[Tools] Initialized"); + return Task.CompletedTask; } } \ No newline at end of file diff --git a/src/ViewModels/Windows/MainWindowViewModel.cs b/src/ViewModels/Windows/MainWindowViewModel.cs index 13ded03..7575dbe 100644 --- a/src/ViewModels/Windows/MainWindowViewModel.cs +++ b/src/ViewModels/Windows/MainWindowViewModel.cs @@ -6,6 +6,9 @@ namespace PipManager.Windows.ViewModels.Windows; public partial class MainWindowViewModel : ObservableObject { [ObservableProperty] private bool _experimentMode; + [ObservableProperty] private bool _debugMode = App.IsDebugMode; + [ObservableProperty] private bool _isTitleBarCoverageGridVisible; + [ObservableProperty] private string _applicationTitle = "Pip Manager"; public MainWindowViewModel() { @@ -21,10 +24,4 @@ public MainWindowViewModel() ApplicationTitle = "Pip Manager"; } } - - [ObservableProperty] - private bool _isTitleBarCoverageGridVisible; - - [ObservableProperty] - private string _applicationTitle = "Pip Manager"; } \ No newline at end of file diff --git a/src/Views/Pages/About/AboutPage.xaml b/src/Views/Pages/About/AboutPage.xaml index 4652b81..4b6c558 100644 --- a/src/Views/Pages/About/AboutPage.xaml +++ b/src/Views/Pages/About/AboutPage.xaml @@ -29,26 +29,21 @@ Source="pack://application:,,,/Assets/icon.png" Width="200" /> - + + + FontTypography="BodyStrong" + Text="{Binding ViewModel.AppVersion, StringFormat=Version: {0}}" + VerticalAlignment="Center" + Visibility="{Binding ViewModel.DebugMode, Converter={StaticResource InverseBoolToVisibility}}" /> - - - - - @@ -98,7 +93,7 @@ + Text="Copyright (c) 2023-2024 AuroraZiling" />
diff --git a/src/Views/Pages/About/AboutPage.xaml.cs b/src/Views/Pages/About/AboutPage.xaml.cs index f1418e4..095a4b0 100644 --- a/src/Views/Pages/About/AboutPage.xaml.cs +++ b/src/Views/Pages/About/AboutPage.xaml.cs @@ -1,14 +1,13 @@ -using Wpf.Ui.Controls; -using About_AboutViewModel = PipManager.Windows.ViewModels.Pages.About.AboutViewModel; +using Wpf.Ui.Abstractions.Controls; using AboutViewModel = PipManager.Windows.ViewModels.Pages.About.AboutViewModel; namespace PipManager.Windows.Views.Pages.About; -public partial class AboutPage : INavigableView +public partial class AboutPage : INavigableView { - public About_AboutViewModel ViewModel { get; } + public AboutViewModel ViewModel { get; } - public AboutPage(About_AboutViewModel viewModel) + public AboutPage(AboutViewModel viewModel) { ViewModel = viewModel; DataContext = this; diff --git a/src/Views/Pages/Action/ActionExceptionPage.xaml.cs b/src/Views/Pages/Action/ActionExceptionPage.xaml.cs index 1399dc9..5da6794 100644 --- a/src/Views/Pages/Action/ActionExceptionPage.xaml.cs +++ b/src/Views/Pages/Action/ActionExceptionPage.xaml.cs @@ -1,14 +1,13 @@ -using Wpf.Ui.Controls; -using Action_ActionExceptionViewModel = PipManager.Windows.ViewModels.Pages.Action.ActionExceptionViewModel; +using Wpf.Ui.Abstractions.Controls; using ActionExceptionViewModel = PipManager.Windows.ViewModels.Pages.Action.ActionExceptionViewModel; namespace PipManager.Windows.Views.Pages.Action; -public partial class ActionExceptionPage : INavigableView +public partial class ActionExceptionPage : INavigableView { - public Action_ActionExceptionViewModel ViewModel { get; } + public ActionExceptionViewModel ViewModel { get; } - public ActionExceptionPage(Action_ActionExceptionViewModel viewModel) + public ActionExceptionPage(ActionExceptionViewModel viewModel) { ViewModel = viewModel; DataContext = this; diff --git a/src/Views/Pages/Action/ActionPage.xaml.cs b/src/Views/Pages/Action/ActionPage.xaml.cs index b8986e6..29eeadd 100644 --- a/src/Views/Pages/Action/ActionPage.xaml.cs +++ b/src/Views/Pages/Action/ActionPage.xaml.cs @@ -1,14 +1,13 @@ -using Wpf.Ui.Controls; -using Action_ActionViewModel = PipManager.Windows.ViewModels.Pages.Action.ActionViewModel; +using Wpf.Ui.Abstractions.Controls; using ActionViewModel = PipManager.Windows.ViewModels.Pages.Action.ActionViewModel; namespace PipManager.Windows.Views.Pages.Action; -public partial class ActionPage : INavigableView +public partial class ActionPage : INavigableView { - public Action_ActionViewModel ViewModel { get; } + public ActionViewModel ViewModel { get; } - public ActionPage(Action_ActionViewModel viewModel) + public ActionPage(ActionViewModel viewModel) { ViewModel = viewModel; DataContext = this; diff --git a/src/Views/Pages/Environment/AddEnvironmentPage.xaml.cs b/src/Views/Pages/Environment/AddEnvironmentPage.xaml.cs index 34d24f2..a0a595d 100644 --- a/src/Views/Pages/Environment/AddEnvironmentPage.xaml.cs +++ b/src/Views/Pages/Environment/AddEnvironmentPage.xaml.cs @@ -1,5 +1,5 @@ using PipManager.Windows.ViewModels.Pages.Environment; -using Wpf.Ui.Controls; +using Wpf.Ui.Abstractions.Controls; namespace PipManager.Windows.Views.Pages.Environment; diff --git a/src/Views/Pages/Environment/EnvironmentPage.xaml b/src/Views/Pages/Environment/EnvironmentPage.xaml index 8ebeab5..ddcfb53 100644 --- a/src/Views/Pages/Environment/EnvironmentPage.xaml +++ b/src/Views/Pages/Environment/EnvironmentPage.xaml @@ -17,9 +17,9 @@ xmlns:environment="clr-namespace:PipManager.Windows.Views.Pages.Environment" xmlns:lang="clr-namespace:PipManager.Windows.Languages" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" + xmlns:models="clr-namespace:PipManager.Core.Configuration.Models;assembly=PipManager.Core" xmlns:ui="http://schemas.lepo.co/wpfui/2022/xaml" - xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" - xmlns:models="clr-namespace:PipManager.Core.Configuration.Models;assembly=PipManager.Core"> + xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"> @@ -58,7 +58,7 @@ ItemsSource="{Binding ViewModel.EnvironmentItems}" Margin="0,10,0,0" ScrollViewer.HorizontalScrollBarVisibility="Disabled" - SelectedItem="{Binding ViewModel.CurrentEnvironment}" + SelectedItem="{Binding ViewModel.CurrentEnvironment, UpdateSourceTrigger=PropertyChanged}" VerticalAlignment="Stretch"> @@ -93,7 +93,5 @@ - - \ No newline at end of file diff --git a/src/Views/Pages/Environment/EnvironmentPage.xaml.cs b/src/Views/Pages/Environment/EnvironmentPage.xaml.cs index cfe7f00..25767b1 100644 --- a/src/Views/Pages/Environment/EnvironmentPage.xaml.cs +++ b/src/Views/Pages/Environment/EnvironmentPage.xaml.cs @@ -1,14 +1,13 @@ -using Wpf.Ui.Controls; -using Environment_EnvironmentViewModel = PipManager.Windows.ViewModels.Pages.Environment.EnvironmentViewModel; +using Wpf.Ui.Abstractions.Controls; using EnvironmentViewModel = PipManager.Windows.ViewModels.Pages.Environment.EnvironmentViewModel; namespace PipManager.Windows.Views.Pages.Environment; -public partial class EnvironmentPage : INavigableView +public partial class EnvironmentPage : INavigableView { - public Environment_EnvironmentViewModel ViewModel { get; } + public EnvironmentViewModel ViewModel { get; } - public EnvironmentPage(Environment_EnvironmentViewModel viewModel) + public EnvironmentPage(EnvironmentViewModel viewModel) { ViewModel = viewModel; DataContext = this; diff --git a/src/Views/Pages/Lab/LabPage.xaml.cs b/src/Views/Pages/Lab/LabPage.xaml.cs index cf74467..ef13ea7 100644 --- a/src/Views/Pages/Lab/LabPage.xaml.cs +++ b/src/Views/Pages/Lab/LabPage.xaml.cs @@ -1,5 +1,5 @@ using PipManager.Windows.ViewModels.Pages.Lab; -using Wpf.Ui.Controls; +using Wpf.Ui.Abstractions.Controls; namespace PipManager.Windows.Views.Pages.Lab; diff --git a/src/Views/Pages/Library/LibraryDetailPage.xaml.cs b/src/Views/Pages/Library/LibraryDetailPage.xaml.cs index 38c1081..f191b12 100644 --- a/src/Views/Pages/Library/LibraryDetailPage.xaml.cs +++ b/src/Views/Pages/Library/LibraryDetailPage.xaml.cs @@ -1,14 +1,13 @@ -using Wpf.Ui.Controls; -using Library_LibraryDetailViewModel = PipManager.Windows.ViewModels.Pages.Library.LibraryDetailViewModel; +using Wpf.Ui.Abstractions.Controls; using LibraryDetailViewModel = PipManager.Windows.ViewModels.Pages.Library.LibraryDetailViewModel; namespace PipManager.Windows.Views.Pages.Library; -public partial class LibraryDetailPage : INavigableView +public partial class LibraryDetailPage : INavigableView { - public Library_LibraryDetailViewModel ViewModel { get; } + public LibraryDetailViewModel ViewModel { get; } - public LibraryDetailPage(Library_LibraryDetailViewModel viewModel) + public LibraryDetailPage(LibraryDetailViewModel viewModel) { ViewModel = viewModel; DataContext = this; diff --git a/src/Views/Pages/Library/LibraryInstallPage.xaml.cs b/src/Views/Pages/Library/LibraryInstallPage.xaml.cs index 3613bd8..d68f674 100644 --- a/src/Views/Pages/Library/LibraryInstallPage.xaml.cs +++ b/src/Views/Pages/Library/LibraryInstallPage.xaml.cs @@ -1,14 +1,13 @@ -using Wpf.Ui.Controls; -using Library_LibraryInstallViewModel = PipManager.Windows.ViewModels.Pages.Library.LibraryInstallViewModel; +using Wpf.Ui.Abstractions.Controls; using LibraryInstallViewModel = PipManager.Windows.ViewModels.Pages.Library.LibraryInstallViewModel; namespace PipManager.Windows.Views.Pages.Library; -public partial class LibraryInstallPage : INavigableView +public partial class LibraryInstallPage : INavigableView { - public Library_LibraryInstallViewModel ViewModel { get; } + public LibraryInstallViewModel ViewModel { get; } - public LibraryInstallPage(Library_LibraryInstallViewModel viewModel) + public LibraryInstallPage(LibraryInstallViewModel viewModel) { ViewModel = viewModel; DataContext = this; diff --git a/src/Views/Pages/Library/LibraryPage.xaml.cs b/src/Views/Pages/Library/LibraryPage.xaml.cs index c6593c1..cd11a77 100644 --- a/src/Views/Pages/Library/LibraryPage.xaml.cs +++ b/src/Views/Pages/Library/LibraryPage.xaml.cs @@ -1,14 +1,13 @@ -using Wpf.Ui.Controls; -using Library_LibraryViewModel = PipManager.Windows.ViewModels.Pages.Library.LibraryViewModel; +using Wpf.Ui.Abstractions.Controls; using LibraryViewModel = PipManager.Windows.ViewModels.Pages.Library.LibraryViewModel; namespace PipManager.Windows.Views.Pages.Library; -public partial class LibraryPage : INavigableView +public partial class LibraryPage : INavigableView { - public Library_LibraryViewModel ViewModel { get; } + public LibraryViewModel ViewModel { get; } - public LibraryPage(Library_LibraryViewModel viewModel) + public LibraryPage(LibraryViewModel viewModel) { ViewModel = viewModel; DataContext = this; diff --git a/src/Views/Pages/Search/SearchDetailPage.xaml.cs b/src/Views/Pages/Search/SearchDetailPage.xaml.cs index 90b5e7e..83fc33a 100644 --- a/src/Views/Pages/Search/SearchDetailPage.xaml.cs +++ b/src/Views/Pages/Search/SearchDetailPage.xaml.cs @@ -1,18 +1,19 @@ -using Microsoft.Web.WebView2.Core; +using System.Diagnostics; +using Microsoft.Web.WebView2.Core; using Microsoft.Web.WebView2.Wpf; -using Wpf.Ui.Controls; -using Search_SearchDetailViewModel = PipManager.Windows.ViewModels.Pages.Search.SearchDetailViewModel; +using Serilog; +using Wpf.Ui.Abstractions.Controls; using SearchDetailViewModel = PipManager.Windows.ViewModels.Pages.Search.SearchDetailViewModel; namespace PipManager.Windows.Views.Pages.Search; -public partial class SearchDetailPage : INavigableView +public partial class SearchDetailPage : INavigableView { public static WebView2? ProjectDescriptionWebView { get; private set; } - public Search_SearchDetailViewModel ViewModel { get; } + public SearchDetailViewModel ViewModel { get; } - public SearchDetailPage(Search_SearchDetailViewModel viewModel) + public SearchDetailPage(SearchDetailViewModel viewModel) { ViewModel = viewModel; DataContext = this; @@ -22,9 +23,23 @@ public SearchDetailPage(Search_SearchDetailViewModel viewModel) private void SearchDetailProjectDescriptionWebView_NavigationStarting(object sender, CoreWebView2NavigationStartingEventArgs e) { - if (e.Uri.StartsWith("http://") || e.Uri.StartsWith("https://")) + var uri = e.Uri; + if (uri.StartsWith("http://") || uri.StartsWith("https://")) { e.Cancel = true; + + try + { + Process.Start(new ProcessStartInfo + { + FileName = uri, + UseShellExecute = true + }); + } + catch (Exception ex) + { + Log.Error("Failed to open link in external browser: " + ex.Message); + } } } diff --git a/src/Views/Pages/Search/SearchPage.xaml.cs b/src/Views/Pages/Search/SearchPage.xaml.cs index 0ce1a3e..ca431f7 100644 --- a/src/Views/Pages/Search/SearchPage.xaml.cs +++ b/src/Views/Pages/Search/SearchPage.xaml.cs @@ -1,5 +1,5 @@ using PipManager.Windows.ViewModels.Pages.Search; -using Wpf.Ui.Controls; +using Wpf.Ui.Abstractions.Controls; namespace PipManager.Windows.Views.Pages.Search; diff --git a/src/Views/Pages/Settings/SettingsPage.xaml.cs b/src/Views/Pages/Settings/SettingsPage.xaml.cs index e4d4745..60560ba 100644 --- a/src/Views/Pages/Settings/SettingsPage.xaml.cs +++ b/src/Views/Pages/Settings/SettingsPage.xaml.cs @@ -1,13 +1,13 @@ -using Wpf.Ui.Controls; -using Settings_SettingsViewModel = PipManager.Windows.ViewModels.Pages.Settings.SettingsViewModel; +using Wpf.Ui.Abstractions.Controls; +using SettingsViewModel = PipManager.Windows.ViewModels.Pages.Settings.SettingsViewModel; namespace PipManager.Windows.Views.Pages.Settings; -public partial class SettingsPage : INavigableView +public partial class SettingsPage : INavigableView { - public Settings_SettingsViewModel ViewModel { get; } + public SettingsViewModel ViewModel { get; } - public SettingsPage(Settings_SettingsViewModel viewModel) + public SettingsPage(SettingsViewModel viewModel) { ViewModel = viewModel; DataContext = this; diff --git a/src/Views/Pages/Tools/ToolsPage.xaml.cs b/src/Views/Pages/Tools/ToolsPage.xaml.cs index d21c3c6..113eb91 100644 --- a/src/Views/Pages/Tools/ToolsPage.xaml.cs +++ b/src/Views/Pages/Tools/ToolsPage.xaml.cs @@ -1,5 +1,5 @@ using PipManager.Windows.ViewModels.Pages.Tools; -using Wpf.Ui.Controls; +using Wpf.Ui.Abstractions.Controls; namespace PipManager.Windows.Views.Pages.Tools; diff --git a/src/Views/Windows/MainWindow.xaml b/src/Views/Windows/MainWindow.xaml index 3e01912..c060929 100644 --- a/src/Views/Windows/MainWindow.xaml +++ b/src/Views/Windows/MainWindow.xaml @@ -109,8 +109,20 @@ + + + + + @@ -121,8 +133,7 @@ Background="{DynamicResource ApplicationBackgroundBrush}" Grid.Row="0" Margin="0,0,300,15" - Visibility="{Binding ViewModel.IsTitleBarCoverageGridVisible, Converter={StaticResource BoolToVisibility}}" - x:Name="TitleBarCoverageGrid" /> + Visibility="{Binding ViewModel.IsTitleBarCoverageGridVisible, Converter={StaticResource BoolToVisibility}}" />