diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index b65071e..f65b9ef 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -3,7 +3,7 @@ name: "PipManager Continuous Integration" on: push: branches: - - main + - development jobs: build: diff --git a/README.md b/README.md index 2a312fb..5e46c93 100644 --- a/README.md +++ b/README.md @@ -74,36 +74,8 @@ Double click `PipManager.exe` or `PipManager_withRuntime.exe` *If you have not i

(back to top)

-## Roadmap - -### Features - -- [x] Install Package - - [x] Default *pip install* - - [x] requirements.txt Import - - [x] Download Distributions - - [ ] Install via distributions - - [ ] Install via VCS -- [x] Update Package -- [x] Delete Package -- [x] Show Packages in list -- [x] Multi-environment Management -- [x] Search Package -- [ ] Dependency Completion Check -- [ ] Scenario Recommendation -- [ ] Cache Management -- [ ] Tools - - [ ] Embedded Lite Python Script Editor - -### Long-term - -- Logging -- Localization -- Rules of action exceptions - See the [Open Issues](https://github.com/AuroraZiling/PipManager/issues) for a full list of proposed features (and known issues). -

(back to top)

## Contributing @@ -113,8 +85,6 @@ See the [Open Issues](https://github.com/AuroraZiling/PipManager/issues) for a f 4. Push to the Branch `development` 5. Open a Pull Request -

(back to top)

- ## License Distributed under the MIT License. See `LICENSE` for more information. diff --git a/src/PipManager.PackageSearch/PackageSearchService.cs b/src/PipManager.PackageSearch/PackageSearchService.cs index 8f787a3..4fffcaf 100644 --- a/src/PipManager.PackageSearch/PackageSearchService.cs +++ b/src/PipManager.PackageSearch/PackageSearchService.cs @@ -1,12 +1,11 @@ using HtmlAgilityPack; using PipManager.PackageSearch.Wrappers.Query; -using Serilog; namespace PipManager.PackageSearch; public class PackageSearchService(HttpClient httpClient) : IPackageSearchService { - public Dictionary<(string, int), QueryWrapper> QueryCaches { get; set; } = []; + private Dictionary<(string, int), QueryWrapper> QueryCaches { get; } = []; public async Task Query(string name, int page = 1) { @@ -14,12 +13,12 @@ public async Task Query(string name, int page = 1) { return QueryCaches[(name, page)]; } - var htmlContent = ""; + string htmlContent; try { htmlContent = await httpClient.GetStringAsync($"https://pypi.org/search/?q={name}&page={page}"); } - catch (Exception exception) when (exception is TaskCanceledException || exception is HttpRequestException) + catch (Exception exception) when (exception is TaskCanceledException or HttpRequestException) { return new QueryWrapper { @@ -53,6 +52,7 @@ public async Task Query(string name, int page = 1) Version = resultItem.ChildNodes[1].ChildNodes[3].InnerText, Description = resultItem.ChildNodes[3].InnerText, Url = $"https://pypi.org{resultItem.Attributes["href"].Value}", + // ReSharper disable once StringLiteralTypo UpdateTime = DateTime.ParseExact(resultItem.ChildNodes[1].ChildNodes[5].ChildNodes[0].Attributes["datetime"].Value, "yyyy-MM-ddTHH:mm:sszzz", null, System.Globalization.DateTimeStyles.RoundtripKind) }); } diff --git a/src/PipManager/App.xaml.cs b/src/PipManager/App.xaml.cs index 2811883..56f99d6 100644 --- a/src/PipManager/App.xaml.cs +++ b/src/PipManager/App.xaml.cs @@ -129,7 +129,7 @@ public static T GetService() [LibraryImport("kernel32.dll")] [return: MarshalAs(UnmanagedType.Bool)] - private static partial bool FreeConsole(); + private static partial void FreeConsole(); private bool _showConsoleWindow; private bool _experimentMode; diff --git a/src/PipManager/AppInfo.cs b/src/PipManager/AppInfo.cs index 8db2990..3e9da3c 100644 --- a/src/PipManager/AppInfo.cs +++ b/src/PipManager/AppInfo.cs @@ -5,7 +5,7 @@ namespace PipManager; public static class AppInfo { - public static readonly string AppVersion = Assembly.GetExecutingAssembly().GetName().Version!.ToString(3) ?? string.Empty; + public static readonly string AppVersion = Assembly.GetExecutingAssembly().GetName().Version!.ToString(3); public static readonly string ConfigPath = Path.Combine(Directory.GetCurrentDirectory(), "config.json"); public static readonly string CrushesDir = Path.Combine(Directory.GetCurrentDirectory(), "crashes"); diff --git a/src/PipManager/AppStarting.cs b/src/PipManager/AppStarting.cs index b1abef8..a9aa8e8 100644 --- a/src/PipManager/AppStarting.cs +++ b/src/PipManager/AppStarting.cs @@ -4,6 +4,7 @@ using System.Globalization; using System.IO; using System.Runtime.InteropServices; +using PipManager.Helpers; namespace PipManager; @@ -11,14 +12,14 @@ public partial class AppStarting { [LibraryImport("kernel32.dll")] [return: MarshalAs(UnmanagedType.Bool)] - private static partial bool AllocConsole(); + private static partial void AllocConsole(); - public readonly AppConfig Config; + private readonly AppConfig _config; public bool ShowConsoleWindow = false; public AppStarting() { - Config = ConfigurationService.LoadConfiguration(); + _config = ConfigurationService.LoadConfiguration(); Directory.CreateDirectory(AppInfo.CrushesDir); Directory.CreateDirectory(AppInfo.LogDir); Directory.CreateDirectory(AppInfo.CachesDir); @@ -26,7 +27,7 @@ public AppStarting() public void LoadLanguage() { - var language = Config.Personalization.Language; + var language = _config.Personalization.Language; if (language != "Auto") { I18NExtension.Culture = new CultureInfo(language); @@ -61,10 +62,10 @@ public void StartLogging() public void LogDeletion() { - if (!Config.Personalization.LogAutoDeletion || !Directory.Exists(AppInfo.LogDir)) return; + if (!_config.Personalization.LogAutoDeletion || !Directory.Exists(AppInfo.LogDir)) return; var fileList = Directory.GetFileSystemEntries(AppInfo.LogDir); var logFileAmount = fileList.Count(file => File.Exists(file) && file.EndsWith(".txt")); - if (logFileAmount >= Config.Personalization.LogAutoDeletionTimes) + if (logFileAmount >= _config.Personalization.LogAutoDeletionTimes) { var directoryInfo = new DirectoryInfo(AppInfo.LogDir); var filesInfo = directoryInfo.GetFileSystemInfos(); @@ -77,7 +78,7 @@ public void LogDeletion() } catch { - continue; + // ignored } } Log.Information($"{logFileAmount} log file(s) deleted"); @@ -86,27 +87,29 @@ public void LogDeletion() public void CrushesDeletion() { - if (!Config.Personalization.CrushesAutoDeletion || !Directory.Exists(AppInfo.CrushesDir)) return; + if (!_config.Personalization.CrushesAutoDeletion || !Directory.Exists(AppInfo.CrushesDir)) return; var fileList = Directory.GetFileSystemEntries(AppInfo.CrushesDir); var crushFileAmount = fileList.Count(file => File.Exists(file) && file.EndsWith(".txt")); - if (crushFileAmount >= Config.Personalization.CrushesAutoDeletionTimes) + if (crushFileAmount < _config.Personalization.CrushesAutoDeletionTimes) { - var directoryInfo = new DirectoryInfo(AppInfo.CrushesDir); - var filesInfo = directoryInfo.GetFileSystemInfos(); - foreach (var file in filesInfo) + return; + } + + var directoryInfo = new DirectoryInfo(AppInfo.CrushesDir); + var filesInfo = directoryInfo.GetFileSystemInfos(); + foreach (var file in filesInfo) + { + if (file.Extension != ".txt") continue; + try { - if (file.Extension != ".txt") continue; - try - { - File.Delete(file.FullName); - } - catch - { - continue; - } + File.Delete(file.FullName); + } + catch + { + // ignored } - Log.Information($"{crushFileAmount} crush file(s) deleted"); } + Log.Information($"{crushFileAmount} crush file(s) deleted"); } public void CachesDeletion() @@ -115,19 +118,32 @@ public void CachesDeletion() var directoryInfo = new DirectoryInfo(AppInfo.CachesDir); var filesInfo = directoryInfo.GetFileSystemInfos(); var cacheFileAmount = 0; + foreach (var subDir in directoryInfo.GetDirectories("tempTarGz-*", SearchOption.AllDirectories)) + { + try + { + subDir.Delete(true); + } + catch (Exception) + { + // ignored + } + } foreach (var file in filesInfo) { - if (file.Name.StartsWith("temp_")) + if (!file.Name.StartsWith("temp_")) { - try - { - File.Delete(file.FullName); - cacheFileAmount++; - } - catch - { - continue; - } + continue; + } + + try + { + File.Delete(file.FullName); + cacheFileAmount++; + } + catch + { + // ignored } } Log.Information($"{cacheFileAmount} cache file(s) deleted"); diff --git a/src/PipManager/Helpers/ThreadIdEnricher.cs b/src/PipManager/Helpers/ThreadIdEnricher.cs index c232723..1e3de84 100644 --- a/src/PipManager/Helpers/ThreadIdEnricher.cs +++ b/src/PipManager/Helpers/ThreadIdEnricher.cs @@ -1,11 +1,13 @@ using Serilog.Core; using Serilog.Events; +namespace PipManager.Helpers; + internal class ThreadIdEnricher : ILogEventEnricher { public void Enrich(LogEvent logEvent, ILogEventPropertyFactory propertyFactory) { logEvent.AddPropertyIfAbsent(propertyFactory.CreateProperty( - "ThreadId", Environment.CurrentManagedThreadId)); + "ThreadId", Environment.CurrentManagedThreadId)); } } \ No newline at end of file diff --git a/src/PipManager/Languages/Lang.Designer.cs b/src/PipManager/Languages/Lang.Designer.cs index fe36478..6eaa83e 100644 --- a/src/PipManager/Languages/Lang.Designer.cs +++ b/src/PipManager/Languages/Lang.Designer.cs @@ -1,7 +1,6 @@ //------------------------------------------------------------------------------ // // This code was generated by a tool. -// Runtime Version:4.0.30319.42000 // // Changes to this file may cause incorrect behavior and will be lost if // the code is regenerated. @@ -258,6 +257,15 @@ public static string Action_NoCurrentOperations { } } + /// + /// Looks up a localized string similar to Cancel. + /// + public static string Action_Operation_Cancel { + get { + return ResourceManager.GetString("Action_Operation_Cancel", resourceCulture); + } + } + /// /// Looks up a localized string similar to Download. /// @@ -303,6 +311,24 @@ public static string Action_Operation_Update { } } + /// + /// Looks up a localized string similar to Action Already Running. + /// + public static string Action_OperationCanceled_AlreadyRunning { + get { + return ResourceManager.GetString("Action_OperationCanceled_AlreadyRunning", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Action Cancelled. + /// + public static string Action_OperationCanceled_Success { + get { + return ResourceManager.GetString("Action_OperationCanceled_Success", resourceCulture); + } + } + /// /// Looks up a localized string similar to Time. /// @@ -429,6 +455,24 @@ public static string ContentDialog_Message_ActionStillRunning { } } + /// + /// Looks up a localized string similar to {0} Caches Cleared. + /// + public static string ContentDialog_Message_CacheCleared { + get { + return ResourceManager.GetString("ContentDialog_Message_CacheCleared", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Failed to clear caches. + /// + public static string ContentDialog_Message_CacheClearFailed { + get { + return ResourceManager.GetString("ContentDialog_Message_CacheClearFailed", resourceCulture); + } + } + /// /// Looks up a localized string similar to Environment already exists. /// @@ -619,7 +663,16 @@ public static string Environment_Operation_CheckEnvironmentUpdate { } /// - /// Looks up a localized string similar to Remove Environment (from list). + /// Looks up a localized string similar to Clear Cache. + /// + public static string Environment_Operation_ClearEnvironmentCache { + get { + return ResourceManager.GetString("Environment_Operation_ClearEnvironmentCache", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Remove from list. /// public static string Environment_Operation_RemoveEnvironment { get { @@ -628,7 +681,7 @@ public static string Environment_Operation_RemoveEnvironment { } /// - /// Looks up a localized string similar to Verify Environment. + /// Looks up a localized string similar to Verify Availability. /// public static string Environment_Operation_VerifyEnvironment { get { @@ -1212,6 +1265,51 @@ public static string LibraryInstall_Header { } } + /// + /// Looks up a localized string similar to Distribution File already added to the list. + /// + public static string LibraryInstall_InstallDistributions_AlreadyExists { + get { + return ResourceManager.GetString("LibraryInstall_InstallDistributions_AlreadyExists", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Install via distributions. + /// + public static string LibraryInstall_InstallDistributions_Header { + get { + return ResourceManager.GetString("LibraryInstall_InstallDistributions_Header", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Install dependencies. + /// + public static string LibraryInstall_InstallDistributions_InstallDependencies { + get { + return ResourceManager.GetString("LibraryInstall_InstallDistributions_InstallDependencies", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Invalid Distribution File. + /// + public static string LibraryInstall_InstallDistributions_InvalidFile { + get { + return ResourceManager.GetString("LibraryInstall_InstallDistributions_InvalidFile", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Error while reading a file, check if the file is occupied. + /// + public static string LibraryInstall_InstallDistributions_IOError { + get { + return ResourceManager.GetString("LibraryInstall_InstallDistributions_IOError", resourceCulture); + } + } + /// /// Looks up a localized string similar to The newest and compatible version will be installed. /// diff --git a/src/PipManager/Languages/Lang.resx b/src/PipManager/Languages/Lang.resx index 6fc5f0a..ed1af42 100644 --- a/src/PipManager/Languages/Lang.resx +++ b/src/PipManager/Languages/Lang.resx @@ -208,10 +208,10 @@ Add Environment - Remove Environment (from list) + Remove from list - Verify Environment + Verify Availability Package Source @@ -699,4 +699,37 @@ Copied to clipboard + + Action Cancelled + + + Action Already Running + + + Cancel + + + Error while reading a file, check if the file is occupied + + + Invalid Distribution File + + + Distribution File already added to the list + + + Install via distributions + + + Install dependencies + + + Clear Cache + + + {0} Caches Cleared + + + Failed to clear caches + \ No newline at end of file diff --git a/src/PipManager/Languages/Lang.zh-cn.resx b/src/PipManager/Languages/Lang.zh-cn.resx index 7723af0..0e72601 100644 --- a/src/PipManager/Languages/Lang.zh-cn.resx +++ b/src/PipManager/Languages/Lang.zh-cn.resx @@ -208,10 +208,10 @@ 添加环境 - 从列表中移除环境 + 从列表中移除 - 验证环境可用性 + 验证可用性 包源 @@ -699,4 +699,37 @@ 已复制 + + 任务已取消 + + + 任务已运行 + + + 取消 + + + 在读取文件时出现错误,检查文件是否被占用 + + + 该发行文件不可用 + + + 该发行文件已在列表中 + + + 通过发行文件安装 + + + 安装依赖包 + + + 清除缓存 + + + 已清除 {0} 个缓存文件 + + + 清除缓存失败 + \ No newline at end of file diff --git a/src/PipManager/Models/Action/ActionListItem.cs b/src/PipManager/Models/Action/ActionListItem.cs index feb1295..01b56ce 100644 --- a/src/PipManager/Models/Action/ActionListItem.cs +++ b/src/PipManager/Models/Action/ActionListItem.cs @@ -5,7 +5,7 @@ namespace PipManager.Models.Action; public partial class ActionListItem : ObservableObject { - public ActionListItem(ActionType operationType, string operationCommand, string displayCommand = "", string path = "", string[]? extraParameters = null, bool progressIntermediate = false, int totalSubTaskNumber = 1) + public ActionListItem(ActionType operationType, string[] operationCommand, string displayCommand = "", string path = "", string[]? extraParameters = null, bool progressIntermediate = false, int totalSubTaskNumber = 1) { OperationType = operationType; OperationCommand = operationCommand; @@ -15,7 +15,7 @@ public ActionListItem(ActionType operationType, string operationCommand, string ExtraParameters = extraParameters; DisplayCommand = displayCommand switch { - "" => operationCommand, + "" => string.Join(' ', operationCommand), _ => displayCommand, }; OperationDescription = operationType switch @@ -55,7 +55,7 @@ public ActionListItem(ActionType operationType, string operationCommand, string public ActionType OperationType { get; set; } public string OperationDescription { get; set; } public string OperationTimestamp { get; set; } = DateTime.Now.ToLocalTime().ToString("yyyy-M-d HH:mm:ss"); - public string OperationCommand { get; set; } + public string[] OperationCommand { get; set; } public string DisplayCommand { get; set; } public string Path { get; set; } public string[]? ExtraParameters { get; set; } @@ -72,7 +72,7 @@ public ActionListItem(ActionType operationType, string operationCommand, string [ObservableProperty] [NotifyPropertyChangedFor(nameof(ProgressBarValue))] - private int _completedSubTaskNumber = 0; + private int _completedSubTaskNumber; public double ProgressBarValue { diff --git a/src/PipManager/Models/Pages/LibraryInstallPackageItem.cs b/src/PipManager/Models/Pages/LibraryInstallPackageItem.cs index 828348c..e1b0ea2 100644 --- a/src/PipManager/Models/Pages/LibraryInstallPackageItem.cs +++ b/src/PipManager/Models/Pages/LibraryInstallPackageItem.cs @@ -3,8 +3,8 @@ public class LibraryInstallPackageItem { public string? PackageName { get; set; } - public bool VersionSpecified { get; set; } = false; - public string VersionSpecifiedType { get; set; } = "~="; + public bool VersionSpecified { get; set; } + public string? DistributionFilePath { get; set; } // Distribution Install Only public string TargetVersion { get; set; } = string.Empty; public List? AvailableVersions { get; set; } } \ No newline at end of file diff --git a/src/PipManager/PipManager.csproj b/src/PipManager/PipManager.csproj index cb198a3..c8ebb3d 100644 --- a/src/PipManager/PipManager.csproj +++ b/src/PipManager/PipManager.csproj @@ -29,6 +29,7 @@ + @@ -36,11 +37,12 @@ + - + - + @@ -98,10 +100,6 @@ - - - - diff --git a/src/PipManager/Services/Action/ActionService.cs b/src/PipManager/Services/Action/ActionService.cs index 318bc41..7cfd836 100644 --- a/src/PipManager/Services/Action/ActionService.cs +++ b/src/PipManager/Services/Action/ActionService.cs @@ -6,13 +6,14 @@ using System.Collections.ObjectModel; using System.IO; using System.Text; +using Meziantou.Framework.WPF.Collections; namespace PipManager.Services.Action; public class ActionService(IEnvironmentService environmentService, IToastService toastService) : IActionService { - public ObservableCollection ActionList { get; set; } = []; + public ConcurrentObservableCollection ActionList { get; set; } = []; public ObservableCollection ExceptionList { get; set; } = []; public void AddOperation(ActionListItem actionListItem) @@ -21,9 +22,19 @@ public void AddOperation(ActionListItem actionListItem) ActionList.Add(actionListItem); } + public string TryCancelOperation(string operationId) + { + var targetAction = ActionList.ToList().FindIndex(action => action.OperationId == operationId); + if (ActionList[targetAction].OperationStatus != Lang.Action_CurrentStatus_WaitingInQueue) + { + return Lang.Action_OperationCanceled_AlreadyRunning; + } + ActionList.Remove(ActionList[targetAction]); + return Lang.Action_OperationCanceled_Success; + } + public void Runner() { - // TODO: Refactor (FUCKING MESSY COMMAND and output) while (true) { if (ActionList.Count > 0) @@ -31,18 +42,27 @@ public void Runner() var errorDetection = false; var consoleError = new StringBuilder(512); var currentAction = ActionList[0]; - currentAction.ConsoleOutput = Lang.Action_CurrentStatus_WaitingInQueue + '\n'; + var currentActionRunning = false; + currentAction.ConsoleOutput = Lang.Action_CurrentStatus_WaitingInQueue; switch (currentAction.OperationType) { case ActionType.Uninstall: { - var queue = currentAction.OperationCommand.Split(' '); + var queue = currentAction.OperationCommand; foreach (var item in queue) { currentAction.OperationStatus = $"Uninstalling {item}"; - var result = environmentService.Uninstall(item, (sender, eventArgs) => + var result = environmentService.Uninstall(item, (_, eventArgs) => { - currentAction.ConsoleOutput += string.IsNullOrEmpty(eventArgs.Data) ? Lang.Action_CurrentStatus_WaitingInQueue : eventArgs.Data.Trim() + '\n'; + if (!currentActionRunning && !string.IsNullOrEmpty(eventArgs.Data)) + { + currentAction.ConsoleOutput = eventArgs.Data.Trim(); + currentActionRunning = true; + } + else if (!string.IsNullOrEmpty(eventArgs.Data)) + { + currentAction.ConsoleOutput += '\n' + eventArgs.Data.Trim(); + } }); currentAction.CompletedSubTaskNumber++; Log.Information(result.Success @@ -55,14 +75,22 @@ public void Runner() case ActionType.Install: { - var queue = currentAction.OperationCommand.Split(' '); + var queue = currentAction.OperationCommand; foreach (var item in queue) { currentAction.OperationStatus = $"Installing {item}"; - var result = environmentService.Install(item, (sender, eventArgs) => + var result = environmentService.Install(item, (_, eventArgs) => { - currentAction.ConsoleOutput += string.IsNullOrEmpty(eventArgs.Data) ? Lang.Action_CurrentStatus_WaitingInQueue : eventArgs.Data.Trim() + '\n'; - }); + if (!currentActionRunning && !string.IsNullOrEmpty(eventArgs.Data)) + { + currentAction.ConsoleOutput = eventArgs.Data.Trim(); + currentActionRunning = true; + } + else if (!string.IsNullOrEmpty(eventArgs.Data)) + { + currentAction.ConsoleOutput += '\n' + eventArgs.Data.Trim(); + } + }, extraParameters: currentAction.ExtraParameters); currentAction.CompletedSubTaskNumber++; if (!result.Success) { @@ -80,11 +108,19 @@ public void Runner() case ActionType.InstallByRequirements: { var requirementsTempFilePath = Path.Combine(AppInfo.CachesDir, $"temp_install_requirements_{currentAction.OperationId}.txt"); - File.WriteAllText(requirementsTempFilePath, currentAction.OperationCommand); - currentAction.OperationStatus = $"Installing from requirements.txt"; - var result = environmentService.InstallByRequirements(requirementsTempFilePath, (sender, eventArgs) => + File.WriteAllText(requirementsTempFilePath, currentAction.OperationCommand[0]); + currentAction.OperationStatus = "Installing from requirements.txt"; + var result = environmentService.InstallByRequirements(requirementsTempFilePath, (_, eventArgs) => { - currentAction.ConsoleOutput += string.IsNullOrEmpty(eventArgs.Data) ? Lang.Action_CurrentStatus_WaitingInQueue : eventArgs.Data.Trim() + '\n'; + if (!currentActionRunning && !string.IsNullOrEmpty(eventArgs.Data)) + { + currentAction.ConsoleOutput = eventArgs.Data.Trim(); + currentActionRunning = true; + } + else if (!string.IsNullOrEmpty(eventArgs.Data)) + { + currentAction.ConsoleOutput += '\n' + eventArgs.Data.Trim(); + } }); if (!result.Success) { @@ -97,13 +133,21 @@ public void Runner() } case ActionType.Download: { - var queue = currentAction.OperationCommand.Split(' '); + var queue = currentAction.OperationCommand; foreach (var item in queue) { currentAction.OperationStatus = $"Downloading {item}"; - var result = environmentService.Download(item, currentAction.Path, (sender, eventArgs) => + var result = environmentService.Download(item, currentAction.Path, (_, eventArgs) => { - currentAction.ConsoleOutput += string.IsNullOrEmpty(eventArgs.Data) ? Lang.Action_CurrentStatus_WaitingInQueue : eventArgs.Data.Trim() + '\n'; + if (!currentActionRunning && !string.IsNullOrEmpty(eventArgs.Data)) + { + currentAction.ConsoleOutput = eventArgs.Data.Trim(); + currentActionRunning = true; + } + else if (!string.IsNullOrEmpty(eventArgs.Data)) + { + currentAction.ConsoleOutput += '\n' + eventArgs.Data.Trim(); + } }, extraParameters: currentAction.ExtraParameters); currentAction.CompletedSubTaskNumber++; if (!result.Success) @@ -121,13 +165,21 @@ public void Runner() } case ActionType.Update: { - var queue = currentAction.OperationCommand.Split(' '); + var queue = currentAction.OperationCommand; foreach (var item in queue) { currentAction.OperationStatus = $"Updating {item}"; - var result = environmentService.Update(item, (sender, eventArgs) => + var result = environmentService.Update(item, (_, eventArgs) => { - currentAction.ConsoleOutput += string.IsNullOrEmpty(eventArgs.Data) ? Lang.Action_CurrentStatus_WaitingInQueue : eventArgs.Data.Trim() + '\n'; + if (!currentActionRunning && !string.IsNullOrEmpty(eventArgs.Data)) + { + currentAction.ConsoleOutput = eventArgs.Data.Trim(); + currentActionRunning = true; + } + else if (!string.IsNullOrEmpty(eventArgs.Data)) + { + currentAction.ConsoleOutput += '\n' + eventArgs.Data.Trim(); + } }); currentAction.CompletedSubTaskNumber++; if (!result.Success) @@ -154,7 +206,7 @@ public void Runner() { toastService.Error(Lang.Action_IssueDetectedToast); }); - currentAction.ConsoleError = consoleError.ToString(); + currentAction.ConsoleError = consoleError.ToString().TrimEnd(); ExceptionList.Add(currentAction); } Thread.Sleep(100); diff --git a/src/PipManager/Services/Action/IActionService.cs b/src/PipManager/Services/Action/IActionService.cs index 8b2592d..fc43d29 100644 --- a/src/PipManager/Services/Action/IActionService.cs +++ b/src/PipManager/Services/Action/IActionService.cs @@ -1,14 +1,17 @@ -using PipManager.Models.Action; +using Meziantou.Framework.WPF.Collections; +using PipManager.Models.Action; using System.Collections.ObjectModel; namespace PipManager.Services.Action; public interface IActionService { - public ObservableCollection ActionList { get; set; } + public ConcurrentObservableCollection ActionList { get; set; } public ObservableCollection ExceptionList { get; set; } public void AddOperation(ActionListItem actionListItem); + public string? TryCancelOperation(string operationId); + public void Runner(); } \ No newline at end of file diff --git a/src/PipManager/Services/Environment/EnvironmentService.cs b/src/PipManager/Services/Environment/EnvironmentService.cs index 8b4b545..573cb8c 100644 --- a/src/PipManager/Services/Environment/EnvironmentService.cs +++ b/src/PipManager/Services/Environment/EnvironmentService.cs @@ -36,6 +36,30 @@ public ActionResponse CheckEnvironmentAvailable(EnvironmentItem environmentItem) : new ActionResponse { Success = false, Exception = ExceptionType.Environment_Broken }; } + public ActionResponse PurgeEnvironmentCache(EnvironmentItem environmentItem) + { + var process = new Process + { + StartInfo = new ProcessStartInfo + { + FileName = configurationService.AppConfig.CurrentEnvironment!.PythonPath, + Arguments = "-m pip cache purge", + UseShellExecute = false, + RedirectStandardOutput = true, + RedirectStandardError = true, + CreateNoWindow = true + } + }; + process.Start(); + var output = process.StandardOutput.ReadToEnd(); + var error = process.StandardError.ReadToEnd(); + process.WaitForExit(); + process.Close(); + process.Dispose(); + error = error.Replace("WARNING: No matching packages", "").Trim(); + return !string.IsNullOrEmpty(error) ? new ActionResponse { Success = false, Exception = ExceptionType.Process_Error, Message = error } : new ActionResponse { Success = true, Message = output[15..].TrimEnd()}; + } + public async Task?> GetLibraries() { if (configurationService.AppConfig.CurrentEnvironment is null) @@ -205,7 +229,6 @@ public async Task GetVersions(string packageName) packageName = PackageNameNormalizerRegex().Replace(packageName, "-").ToLower(); if (!PackageNameVerificationRegex().IsMatch(packageName)) return new GetVersionsResponse { Status = 2, Versions = [] }; - var sth = $"{configurationService.GetUrlFromPackageSourceType("pypi")}{packageName}/json"; var responseMessage = await _httpClient.GetAsync($"{configurationService.GetUrlFromPackageSourceType("pypi")}{packageName}/json"); var response = await responseMessage.Content.ReadAsStringAsync(); @@ -214,13 +237,13 @@ public async Task GetVersions(string packageName) ?.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); - if (pypiPackageInfo == null || pypiPackageInfo?.Count == 0) + if (pypiPackageInfo == null || pypiPackageInfo.Count == 0) { Log.Warning($"[EnvironmentService] {packageName} package not found"); return new GetVersionsResponse { Status = 1, Versions = [] }; } Log.Information($"[EnvironmentService] Found {packageName}"); - return new GetVersionsResponse { Status = 0, Versions = pypiPackageInfo?.Keys.ToArray() }; + return new GetVersionsResponse { Status = 0, Versions = pypiPackageInfo.Keys.ToArray() }; } catch (Exception) { @@ -229,15 +252,16 @@ public async Task GetVersions(string packageName) } } - public ActionResponse Install(string packageName, DataReceivedEventHandler consoleOutputCallback) + public ActionResponse Install(string packageName, DataReceivedEventHandler consoleOutputCallback, string[]? extraParameters = null) { + string? extra = extraParameters != null ? string.Join(" ", extraParameters) : null; var process = new Process { StartInfo = new ProcessStartInfo { - FileName = configurationService.AppConfig!.CurrentEnvironment!.PythonPath, + FileName = configurationService.AppConfig.CurrentEnvironment!.PythonPath, Arguments = - $"-m pip install \"{packageName}\" -i {configurationService.GetUrlFromPackageSourceType()} --retries 1 --timeout 6", + $"-m pip install \"{packageName}\" -i {configurationService.GetUrlFromPackageSourceType()} --retries 1 --timeout 6 {extra}", UseShellExecute = false, RedirectStandardOutput = true, RedirectStandardError = true, @@ -260,7 +284,7 @@ public ActionResponse InstallByRequirements(string requirementsFilePath, DataRec { StartInfo = new ProcessStartInfo { - FileName = configurationService.AppConfig!.CurrentEnvironment!.PythonPath, + FileName = configurationService.AppConfig.CurrentEnvironment!.PythonPath, Arguments = $"-m pip install -r \"{requirementsFilePath}\" -i {configurationService.GetUrlFromPackageSourceType()} --retries 1 --timeout 6", UseShellExecute = false, @@ -286,7 +310,7 @@ public ActionResponse Download(string packageName, string downloadPath, DataRece { StartInfo = new ProcessStartInfo { - FileName = configurationService.AppConfig!.CurrentEnvironment!.PythonPath, + FileName = configurationService.AppConfig.CurrentEnvironment!.PythonPath, Arguments = $"-m pip download -d \"{downloadPath}\" \"{packageName}\" -i {configurationService.GetUrlFromPackageSourceType()} --retries 1 --timeout 6 {extra}", UseShellExecute = false, @@ -311,7 +335,7 @@ public ActionResponse Update(string packageName, DataReceivedEventHandler consol { StartInfo = new ProcessStartInfo { - FileName = configurationService.AppConfig!.CurrentEnvironment!.PythonPath, + FileName = configurationService.AppConfig.CurrentEnvironment!.PythonPath, Arguments = $"-m pip install --upgrade \"{packageName}\" -i {configurationService.GetUrlFromPackageSourceType()} --retries 1 --timeout 6", UseShellExecute = false, @@ -336,7 +360,7 @@ public ActionResponse Uninstall(string packageName, DataReceivedEventHandler con { StartInfo = new ProcessStartInfo { - FileName = configurationService.AppConfig!.CurrentEnvironment!.PythonPath, + FileName = configurationService.AppConfig.CurrentEnvironment!.PythonPath, Arguments = $"-m pip uninstall -y \"{packageName}\"", UseShellExecute = false, RedirectStandardOutput = true, diff --git a/src/PipManager/Services/Environment/IEnvironmentService.cs b/src/PipManager/Services/Environment/IEnvironmentService.cs index ba5f5c0..9267e32 100644 --- a/src/PipManager/Services/Environment/IEnvironmentService.cs +++ b/src/PipManager/Services/Environment/IEnvironmentService.cs @@ -11,11 +11,13 @@ public interface IEnvironmentService public ActionResponse CheckEnvironmentAvailable(EnvironmentItem environmentItem); + public ActionResponse PurgeEnvironmentCache(EnvironmentItem environmentItem); + public Task?> GetLibraries(); public Task GetVersions(string packageName); - public ActionResponse Install(string packageName, DataReceivedEventHandler consoleOutputCallback); + public ActionResponse Install(string packageName, DataReceivedEventHandler consoleOutputCallback, string[]? extraParameters = null); public ActionResponse InstallByRequirements(string requirementsFilePath, DataReceivedEventHandler consoleOutputCallback); diff --git a/src/PipManager/Services/Mask/IMaskService.cs b/src/PipManager/Services/Mask/IMaskService.cs index 97ff839..e452d74 100644 --- a/src/PipManager/Services/Mask/IMaskService.cs +++ b/src/PipManager/Services/Mask/IMaskService.cs @@ -1,5 +1,4 @@ using PipManager.Controls.Mask; -using PipManager.Models.Action; namespace PipManager.Services.Mask; @@ -7,8 +6,6 @@ public interface IMaskService { public void SetMaskPresenter(MaskPresenter maskPresenter); - public MaskPresenter GetMaskPresenter(); - public void Show(string message = ""); public void Hide(); diff --git a/src/PipManager/Services/Mask/MaskService.cs b/src/PipManager/Services/Mask/MaskService.cs index 98c317d..b44e18c 100644 --- a/src/PipManager/Services/Mask/MaskService.cs +++ b/src/PipManager/Services/Mask/MaskService.cs @@ -1,6 +1,5 @@ using PipManager.Controls.Mask; using PipManager.Languages; -using PipManager.Models.Action; using System.Windows.Controls; namespace PipManager.Services.Mask; @@ -16,12 +15,11 @@ public void SetMaskPresenter(MaskPresenter maskPresenter) _grid = Application.Current.TryFindResource("MaskGrid") as Grid; } - public MaskPresenter GetMaskPresenter() => _presenter ?? throw new ArgumentNullException("The MaskPresenter didn't set previously."); public void Show(string message = "") { if (_presenter == null || _grid == null) - throw new ArgumentNullException("The MaskPresenter didn't set previously."); + throw new ArgumentNullException($"The MaskPresenter didn't set previously."); ((_grid.Children[0] as StackPanel)!.Children[1] as TextBlock)!.Text = Lang.Mask_Loading; ((_grid.Children[0] as StackPanel)!.Children[2] as TextBlock)!.Text = message; _ = _presenter.ShowGrid(_grid); @@ -30,7 +28,7 @@ public void Show(string message = "") public void Hide() { if (_presenter == null) - throw new ArgumentNullException("The MaskPresenter didn't set previously."); + throw new ArgumentNullException($"The MaskPresenter didn't set previously."); _ = _presenter.HideGrid(); } } \ No newline at end of file diff --git a/src/PipManager/ViewModels/Pages/About/AboutViewModel.cs b/src/PipManager/ViewModels/Pages/About/AboutViewModel.cs index 1bcceae..c63e46d 100644 --- a/src/PipManager/ViewModels/Pages/About/AboutViewModel.cs +++ b/src/PipManager/ViewModels/Pages/About/AboutViewModel.cs @@ -30,25 +30,46 @@ private void InitializeViewModel() ExperimentMode = configurationService.ExperimentMode; AppVersion = AppInfo.AppVersion; _isInitialized = true; + NugetLibraryList = + [ + new AboutNugetLibraryListItem("Antelcat.I18N.WPF", "MIT", "Copyright (c) 2023 Feast", + "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", + "Copyright © .NET Foundation and Contributors", "https://github.com/dotnet/runtime"), + new AboutNugetLibraryListItem("Microsoft.Web.WebView2", "Custom License", + "© 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("Serilog", "Apache-2.0", "Copyright © 2013-2020 Serilog Contributors", + "https://github.com/serilog/serilog"), + new AboutNugetLibraryListItem("Serilog.Extensions.Logging", "Apache-2.0", + "Copyright © 2013-2020 Serilog Contributors", "https://github.com/serilog/serilog-extensions-logging"), + new AboutNugetLibraryListItem("Serilog.Sinks.Console", "Apache-2.0", + "Copyright © 2016 Serilog Contributors", "https://github.com/serilog/serilog-sinks-console"), + new AboutNugetLibraryListItem("Serilog.Sinks.File", "Apache-2.0", "Copyright © 2016 Serilog Contributors", + "https://github.com/serilog/serilog-sinks-file"), + new AboutNugetLibraryListItem("SharpZipLib", "MIT", "Copyright © 2000-2018 SharpZipLib Contributors", + "https://github.com/icsharpcode/SharpZipLib"), + new AboutNugetLibraryListItem("ValueConverters", "MIT", "Copyright (c) 2019 Thomas Galliker", + "https://github.com/thomasgalliker/ValueConverters.NET"), + new AboutNugetLibraryListItem("WPF-UI", "MIT", + "Copyright (c) 2021-2023 Leszek Pomianowski and WPF UI Contributors", + "https://github.com/lepoco/wpfui"), + new AboutNugetLibraryListItem("WPF-UI.Tray", "MIT", + "Copyright (c) 2021-2023 Leszek Pomianowski and WPF UI Contributors", + "https://github.com/lepoco/wpfui"), + ]; Log.Information("[About] Initialized"); } [ObservableProperty] - private ObservableCollection _nugetLibraryList = - [ - new AboutNugetLibraryListItem("Antelcat.I18N.WPF", "MIT", "Copyright (c) 2023 Feast", "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("Microsoft.Extensions.Hosting", "MIT", "Copyright © .NET Foundation and Contributors", "https://github.com/dotnet/runtime"), - new AboutNugetLibraryListItem("Microsoft.Web.WebView2", "Custom License", "© 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("Serilog", "Apache-2.0", "Copyright © 2013-2020 Serilog Contributors", "https://github.com/serilog/serilog"), - new AboutNugetLibraryListItem("Serilog.Extensions.Logging", "Apache-2.0", "Copyright © 2013-2020 Serilog Contributors", "https://github.com/serilog/serilog-extensions-logging"), - new AboutNugetLibraryListItem("Serilog.Sinks.Console", "Apache-2.0", "Copyright © 2016 Serilog Contributors", "https://github.com/serilog/serilog-sinks-console"), - new AboutNugetLibraryListItem("Serilog.Sinks.File", "Apache-2.0", "Copyright © 2016 Serilog Contributors", "https://github.com/serilog/serilog-sinks-file"), - new AboutNugetLibraryListItem("ValueConverters", "MIT", "Copyright (c) 2019 Thomas Galliker", "https://github.com/thomasgalliker/ValueConverters.NET"), - new AboutNugetLibraryListItem("WPF-UI", "MIT", "Copyright (c) 2021-2023 Leszek Pomianowski and WPF UI Contributors", "https://github.com/lepoco/wpfui"), - new AboutNugetLibraryListItem("WPF-UI.Tray", "MIT", "Copyright (c) 2021-2023 Leszek Pomianowski and WPF UI Contributors", "https://github.com/lepoco/wpfui"), - ]; + private ObservableCollection _nugetLibraryList = []; } \ No newline at end of file diff --git a/src/PipManager/ViewModels/Pages/Action/ActionExceptionViewModel.cs b/src/PipManager/ViewModels/Pages/Action/ActionExceptionViewModel.cs index 389deff..d8466d7 100644 --- a/src/PipManager/ViewModels/Pages/Action/ActionExceptionViewModel.cs +++ b/src/PipManager/ViewModels/Pages/Action/ActionExceptionViewModel.cs @@ -5,6 +5,8 @@ using Serilog; using System.Collections.ObjectModel; using System.Diagnostics; +using System.IO; +using System.Text; using System.Web; using Wpf.Ui.Controls; @@ -44,9 +46,28 @@ private void InitializeViewModel() Log.Information("[Action][Exceptions] Initialized"); } - public void UpdateActionExceptionList() + private void UpdateActionExceptionList() { Exceptions = new ObservableCollection(_actionService.ExceptionList); + Log.Information("[Action][Exceptions] Exception List updated ({Count} items)", Exceptions.Count); + } + + private static string ExceptionFilter(string parameter) + { + var exceptionBuilder = new StringBuilder(); + using (StringReader reader = new (parameter)) + { + while (reader.ReadLine() is { } line) + { + line = line.Trim(); + if (line.StartsWith("ERROR")) + { + exceptionBuilder.Append(line).Append(' '); + } + } + } + + return exceptionBuilder.ToString(); } [RelayCommand] @@ -54,7 +75,7 @@ private static void ExceptionBingSearch(string? parameter) { if (parameter != null) { - Process.Start(new ProcessStartInfo($"https://bing.com/search?q={HttpUtility.UrlEncode(parameter)}") { UseShellExecute = true }); + Process.Start(new ProcessStartInfo($"https://bing.com/search?q={HttpUtility.UrlEncode(ExceptionFilter(parameter))}") { UseShellExecute = true }); } } @@ -63,17 +84,20 @@ private static void ExceptionGoogleSearch(string? parameter) { if (parameter != null) { - Process.Start(new ProcessStartInfo($"https://www.google.com/search?q={HttpUtility.UrlEncode(parameter)}") { UseShellExecute = true }); + Process.Start(new ProcessStartInfo($"https://www.google.com/search?q={HttpUtility.UrlEncode(ExceptionFilter(parameter))}") { UseShellExecute = true }); } } [RelayCommand] private void ExceptionCopyToClipboard(string? parameter) { - if (parameter != null) + if (parameter == null) { - Clipboard.SetDataObject(parameter); - _toastService.Success(Lang.ActionException_CopyToClipboardNotice); + return; } + + Clipboard.SetDataObject(ExceptionFilter(parameter)); + _toastService.Success(Lang.ActionException_CopyToClipboardNotice); + Log.Information("[Action][Exceptions] Copied exception to clipboard"); } } \ No newline at end of file diff --git a/src/PipManager/ViewModels/Pages/Action/ActionViewModel.cs b/src/PipManager/ViewModels/Pages/Action/ActionViewModel.cs index 7c7367e..bc4300c 100644 --- a/src/PipManager/ViewModels/Pages/Action/ActionViewModel.cs +++ b/src/PipManager/ViewModels/Pages/Action/ActionViewModel.cs @@ -1,8 +1,10 @@ -using PipManager.Models.Action; +using Meziantou.Framework.WPF.Collections; +using PipManager.Languages; +using PipManager.Models.Action; using PipManager.Services.Action; +using PipManager.Services.Toast; using PipManager.Views.Pages.Action; using Serilog; -using System.Collections.ObjectModel; using Wpf.Ui; using Wpf.Ui.Controls; @@ -13,15 +15,17 @@ public partial class ActionViewModel : ObservableObject, INavigationAware private bool _isInitialized; [ObservableProperty] - private ObservableCollection _actions; + private ConcurrentObservableCollection _actions; private readonly IActionService _actionService; + private readonly IToastService _toastService; private readonly INavigationService _navigationService; - public ActionViewModel(IActionService actionService, INavigationService navigationService) + public ActionViewModel(IActionService actionService, INavigationService navigationService, IToastService toastService) { _actionService = actionService; _navigationService = navigationService; + _toastService = toastService; Actions = _actionService.ActionList; } @@ -46,4 +50,25 @@ private void ShowExceptions() { _navigationService.NavigateWithHierarchy(typeof(ActionExceptionPage)); } + + [RelayCommand] + private void CancelAction(string? operationId) + { + if (string.IsNullOrEmpty(operationId)) + { + return; + } + + var result = _actionService.TryCancelOperation(operationId); + if(result == Lang.Action_OperationCanceled_AlreadyRunning) + { + _toastService.Error(Lang.Action_OperationCanceled_AlreadyRunning); + Log.Warning("[Action] Operation cancellation failed (already running): {OperationId}", operationId); + } + else + { + _toastService.Success(Lang.Action_OperationCanceled_Success); + Log.Information("[Action] Operation canceled: {OperationId}", operationId); + } + } } \ No newline at end of file diff --git a/src/PipManager/ViewModels/Pages/Environment/AddEnvironmentViewModel.cs b/src/PipManager/ViewModels/Pages/Environment/AddEnvironmentViewModel.cs index d968b98..0aa1738 100644 --- a/src/PipManager/ViewModels/Pages/Environment/AddEnvironmentViewModel.cs +++ b/src/PipManager/ViewModels/Pages/Environment/AddEnvironmentViewModel.cs @@ -14,10 +14,6 @@ namespace PipManager.ViewModels.Pages.Environment; public partial class AddEnvironmentViewModel(INavigationService navigationService, IConfigurationService configurationService, IEnvironmentService environmentService, IToastService toastService) : ObservableObject, INavigationAware { private bool _isInitialized; - private readonly INavigationService _navigationService = navigationService; - private readonly IConfigurationService _configurationService = configurationService; - private readonly IEnvironmentService _environmentService = environmentService; - private readonly IToastService _toastService = toastService; public void OnNavigatedTo() { @@ -77,7 +73,7 @@ await Task.Run(() => if (!File.Exists(Path.Combine(item, "python.exe"))) continue; var environmentItem = - _configurationService.GetEnvironmentItem(Path.Combine(item, "python.exe")); + configurationService.GetEnvironmentItem(Path.Combine(item, "python.exe")); if (environmentItem == null) continue; EnvironmentItems.Add(environmentItem); } @@ -133,83 +129,83 @@ private void AddEnvironment(string parameter) switch (ByWay) { case 0 when EnvironmentItemInList == null: - _toastService.Error(Lang.ContentDialog_Message_EnvironmentNoSelection); + toastService.Error(Lang.ContentDialog_Message_EnvironmentNoSelection); break; case 0: { - var result = _environmentService.CheckEnvironmentAvailable(EnvironmentItemInList); - var alreadyExists = _environmentService.CheckEnvironmentExists(EnvironmentItemInList); + var result = environmentService.CheckEnvironmentAvailable(EnvironmentItemInList); + var alreadyExists = environmentService.CheckEnvironmentExists(EnvironmentItemInList); if (result.Success) { if (alreadyExists) { - _toastService.Error(Lang.ContentDialog_Message_EnvironmentAlreadyExists); + toastService.Error(Lang.ContentDialog_Message_EnvironmentAlreadyExists); } else { - _configurationService.AppConfig.CurrentEnvironment = EnvironmentItemInList; - _configurationService.AppConfig.EnvironmentItems.Add(EnvironmentItemInList); - _configurationService.Save(); + configurationService.AppConfig.CurrentEnvironment = EnvironmentItemInList; + configurationService.AppConfig.EnvironmentItems.Add(EnvironmentItemInList); + configurationService.Save(); Log.Information($"[AddEnvironment] Environment added ({EnvironmentItemInList.PipVersion} for {EnvironmentItemInList.PythonVersion})"); - _navigationService.GoBack(); + navigationService.GoBack(); } } else { - _toastService.Error(result.Message); + toastService.Error(result.Message); } break; } case 1: { - var result = _configurationService.GetEnvironmentItemFromCommand(PipCommand, "-V"); + var result = configurationService.GetEnvironmentItemFromCommand(PipCommand, "-V"); if (result != null) { - var alreadyExists = _environmentService.CheckEnvironmentExists(result); + var alreadyExists = environmentService.CheckEnvironmentExists(result); if (alreadyExists) { - _toastService.Error(Lang.ContentDialog_Message_EnvironmentAlreadyExists); + toastService.Error(Lang.ContentDialog_Message_EnvironmentAlreadyExists); } else { - _configurationService.AppConfig.CurrentEnvironment = result; - _configurationService.AppConfig.EnvironmentItems.Add(result); + configurationService.AppConfig.CurrentEnvironment = result; + configurationService.AppConfig.EnvironmentItems.Add(result); Log.Information($"[AddEnvironment] Environment added ({result.PipVersion} for {result.PythonVersion})"); - _configurationService.Save(); - _navigationService.GoBack(); + configurationService.Save(); + navigationService.GoBack(); } } else { - _toastService.Error(Lang.ContentDialog_Message_EnvironmentInvaild); + toastService.Error(Lang.ContentDialog_Message_EnvironmentInvaild); } break; } case 2: { - var result = _configurationService.GetEnvironmentItem(PythonPath); + var result = configurationService.GetEnvironmentItem(PythonPath); if (result != null) { - var alreadyExists = _environmentService.CheckEnvironmentExists(result); + var alreadyExists = environmentService.CheckEnvironmentExists(result); if (alreadyExists) { - _toastService.Error(Lang.ContentDialog_Message_EnvironmentAlreadyExists); + toastService.Error(Lang.ContentDialog_Message_EnvironmentAlreadyExists); } else { - _configurationService.AppConfig.CurrentEnvironment = result; - _configurationService.AppConfig.EnvironmentItems.Add(result); + configurationService.AppConfig.CurrentEnvironment = result; + configurationService.AppConfig.EnvironmentItems.Add(result); Log.Information($"[AddEnvironment] Environment added ({result.PipVersion} for {result.PythonVersion})"); - _configurationService.Save(); - _navigationService.GoBack(); + configurationService.Save(); + navigationService.GoBack(); } } else { - _toastService.Error(Lang.ContentDialog_Message_EnvironmentInvaild); + toastService.Error(Lang.ContentDialog_Message_EnvironmentInvaild); } break; diff --git a/src/PipManager/ViewModels/Pages/Environment/EnvironmentViewModel.cs b/src/PipManager/ViewModels/Pages/Environment/EnvironmentViewModel.cs index 7eaca89..c87d8a6 100644 --- a/src/PipManager/ViewModels/Pages/Environment/EnvironmentViewModel.cs +++ b/src/PipManager/ViewModels/Pages/Environment/EnvironmentViewModel.cs @@ -36,13 +36,15 @@ public void OnNavigatedTo() var currentEnvironment = configurationService.AppConfig.CurrentEnvironment; foreach (var environmentItem in EnvironmentItems) { - if (currentEnvironment is not null && environmentItem.PythonPath == currentEnvironment.PythonPath) + if (currentEnvironment is null || environmentItem.PythonPath != currentEnvironment.PythonPath) { - CurrentEnvironment = environmentItem; - var mainWindowViewModel = App.GetService(); - mainWindowViewModel.ApplicationTitle = $"Pip Manager | {CurrentEnvironment.PipVersion} for {CurrentEnvironment.PythonVersion}"; - Log.Information($"[Environment] Current Environment changed: {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}"); } } @@ -64,7 +66,7 @@ private void InitializeViewModel() private bool _environmentSelected; [RelayCommand] - private async Task DeleteEnvironmentAsync() + private async Task DeleteEnvironment() { var result = await contentDialogService.ShowSimpleDialogAsync( ContentDialogCreateOptions.Warning(Lang.ContentDialog_Message_EnvironmentDeletion, @@ -74,10 +76,10 @@ private async Task DeleteEnvironmentAsync() EnvironmentItems.Remove(CurrentEnvironment!); CurrentEnvironment = null; configurationService.AppConfig.CurrentEnvironment = null; - configurationService.AppConfig.EnvironmentItems = new List(EnvironmentItems); + configurationService.AppConfig.EnvironmentItems = [..EnvironmentItems]; configurationService.Save(); var mainWindowViewModel = App.GetService(); - mainWindowViewModel.ApplicationTitle = $"Pip Manager"; + mainWindowViewModel.ApplicationTitle = "Pip Manager"; EnvironmentSelected = false; } @@ -98,7 +100,7 @@ private async Task CheckEnvironment() Lang.ContentDialog_PrimaryButton_EnvironmentDeletion)); if (result == ContentDialogResult.Primary) { - await DeleteEnvironmentAsync(); + await DeleteEnvironment(); } } } @@ -129,7 +131,7 @@ await Task.Run(async () => actionService.AddOperation(new ActionListItem ( ActionType.Update, - "pip", + ["pip"], progressIntermediate: false, totalSubTaskNumber: 1 )); @@ -140,10 +142,28 @@ await Task.Run(async () => else if (latest == string.Empty) { toastService.Error(Lang.ContentDialog_Message_NetworkError); + Log.Error("[Environment] Network error while checking for updates (environment: {environment})", CurrentEnvironment!.PipVersion); } else { toastService.Info(Lang.ContentDialog_Message_EnvironmentIsLatest); + Log.Information("[Environment] Environment is already up to date (environment: {environment})", CurrentEnvironment!.PipVersion); + } + } + + [RelayCommand] + private void ClearCache() + { + var result = environmentService.PurgeEnvironmentCache(CurrentEnvironment!); + if (result.Success) + { + Log.Information($"[Environment] Cache cleared ({CurrentEnvironment!.PipVersion} for {CurrentEnvironment.PythonVersion})"); + toastService.Info(string.Format(Lang.ContentDialog_Message_CacheCleared, result.Message)); + } + else + { + Log.Error($"[Environment] Cache clear failed ({CurrentEnvironment!.PipVersion} for {CurrentEnvironment.PythonVersion})"); + toastService.Error(Lang.ContentDialog_Message_CacheClearFailed); } } diff --git a/src/PipManager/ViewModels/Pages/Lab/LabViewModel.cs b/src/PipManager/ViewModels/Pages/Lab/LabViewModel.cs index af3abce..f461853 100644 --- a/src/PipManager/ViewModels/Pages/Lab/LabViewModel.cs +++ b/src/PipManager/ViewModels/Pages/Lab/LabViewModel.cs @@ -16,7 +16,7 @@ private void ActionTest() actionService.AddOperation(new ActionListItem ( ActionType.Install, - "114514==114", + ["pytorch"], totalSubTaskNumber: 1, progressIntermediate: false )); diff --git a/src/PipManager/ViewModels/Pages/Library/LibraryDetailViewModel.cs b/src/PipManager/ViewModels/Pages/Library/LibraryDetailViewModel.cs index 4bb0ea0..56a9712 100644 --- a/src/PipManager/ViewModels/Pages/Library/LibraryDetailViewModel.cs +++ b/src/PipManager/ViewModels/Pages/Library/LibraryDetailViewModel.cs @@ -3,7 +3,6 @@ using PipManager.Models.Package; using PipManager.Models.Pages; using System.Collections.ObjectModel; -using Wpf.Ui; using Wpf.Ui.Controls; namespace PipManager.ViewModels.Pages.Library; @@ -12,7 +11,6 @@ public partial class LibraryDetailViewModel : ObservableObject, INavigationAware { public record LibraryDetailMessage(PackageItem Package); private bool _isInitialized; - private readonly INavigationService _navigationService; [ObservableProperty] private PackageItem? _package; @@ -38,9 +36,8 @@ public record LibraryDetailMessage(PackageItem Package); #endregion Classifier - public LibraryDetailViewModel(INavigationService navigationService) + public LibraryDetailViewModel() { - _navigationService = navigationService; WeakReferenceMessenger.Default.Register(this, Receive); } diff --git a/src/PipManager/ViewModels/Pages/Library/LibraryInstallViewModel.cs b/src/PipManager/ViewModels/Pages/Library/LibraryInstallViewModel.cs index ac83c7b..919cecc 100644 --- a/src/PipManager/ViewModels/Pages/Library/LibraryInstallViewModel.cs +++ b/src/PipManager/ViewModels/Pages/Library/LibraryInstallViewModel.cs @@ -11,6 +11,10 @@ using PipManager.Views.Pages.Action; using System.Collections.ObjectModel; using System.IO; +using System.IO.Compression; +using System.Text; +using ICSharpCode.SharpZipLib.GZip; +using ICSharpCode.SharpZipLib.Tar; using Wpf.Ui; using Wpf.Ui.Controls; @@ -36,6 +40,7 @@ public LibraryInstallViewModel(IActionService actionService, IMaskService maskSe _contentDialogService = contentDialogService; _environmentService = environmentService; _toastService = toastService; + _installWheelDependencies = false; _navigationService = navigationService; WeakReferenceMessenger.Default.Register(this, Receive); } @@ -44,6 +49,7 @@ public void OnNavigatedTo() { if (!_isInitialized) InitializeViewModel(); + InstallWheelDependencies = true; } public void OnNavigatedFrom() @@ -118,7 +124,7 @@ private void AddDefaultToAction() _actionService.AddOperation(new ActionListItem ( ActionType.Install, - string.Join(' ', operationCommand), + operationCommand.ToArray(), totalSubTaskNumber: operationCommand.Count )); PreInstallPackages.Clear(); @@ -172,7 +178,7 @@ private void AddRequirementsToAction() _actionService.AddOperation(new ActionListItem ( ActionType.InstallByRequirements, - Requirements, + [Requirements], displayCommand: "requirements.txt" )); Requirements = ""; @@ -185,8 +191,8 @@ private void AddRequirementsToAction() [ObservableProperty] private ObservableCollection _preDownloadPackages = []; [ObservableProperty] private string _downloadDistributionsFolderPath = ""; - [ObservableProperty] private bool _downloadDistributionsEnabled = false; - [ObservableProperty] private bool _downloadDependencies = false; + [ObservableProperty] private bool _downloadDistributionsEnabled; + [ObservableProperty] private bool _downloadWheelDependencies; [RelayCommand] private async Task DownloadDistributionsTask() @@ -251,9 +257,9 @@ private void DownloadDistributionsToAction() _actionService.AddOperation(new ActionListItem ( ActionType.Download, - string.Join(' ', operationCommand), + operationCommand.ToArray(), path: DownloadDistributionsFolderPath, - extraParameters: DownloadDependencies ? null : ["--no-deps"], + extraParameters: DownloadWheelDependencies ? null : ["--no-deps"], totalSubTaskNumber: operationCommand.Count )); PreDownloadPackages.Clear(); @@ -280,4 +286,145 @@ private void DeleteDownloadDistributionsTask(object? parameter) } #endregion Download Wheel File + + #region Install via distributions + [ObservableProperty] private ObservableCollection _preInstallDistributions = []; + [ObservableProperty] private bool _installWheelDependencies; + + [RelayCommand] + private async Task SelectDistributions() + { + var openFileDialog = new OpenFileDialog + { + Filter = "Wheel Files|*.whl;*.tar.gz", + Multiselect = true + }; + if (openFileDialog.ShowDialog() != true) + { + return; + } + + foreach (var fileName in openFileDialog.FileNames) + { + var packageName = ""; + var packageVersion = ""; + try + { + if (fileName.EndsWith(".whl")) + { + await using var wheelFileStream = new FileStream(fileName, FileMode.Open); + using var wheelFileArchive = new ZipArchive(wheelFileStream, ZipArchiveMode.Read); + foreach (ZipArchiveEntry entry in wheelFileArchive.Entries) + { + if (!entry.FullName.Contains(".dist-info/METADATA") || !entry.FullName.Contains("PKG-INFO")) + { + continue; + } + + using var streamReader = new StreamReader(entry.Open()); + while (await streamReader.ReadLineAsync() is { } line) + { + if (line.StartsWith("Name: ")) + { + packageName = line[6..]; + } + else if (line.StartsWith("Version: ")) + { + packageVersion = line[9..]; + break; + } + } + } + } + else if (fileName.EndsWith(".tar.gz")) + { + var inStream = File.OpenRead(fileName); + var gzipStream = new GZipInputStream(inStream); + var tarArchive = TarArchive.CreateInputTarArchive(gzipStream, Encoding.UTF8); + var randomizedDirectory = Path.Combine(AppInfo.CachesDir, $"tempTarGz-{Guid.NewGuid():N}"); + tarArchive.ExtractContents(randomizedDirectory); + tarArchive.Close(); + gzipStream.Close(); + inStream.Close(); + string targetDirectory = Directory.GetDirectories(randomizedDirectory)[0]; + + if (File.Exists(Path.Combine(targetDirectory, "PKG-INFO"))) + { + using var streamReader = File.OpenText(Path.Combine(targetDirectory, "PKG-INFO")); + while (await streamReader.ReadLineAsync() is { } line) + { + if (line.StartsWith("Name: ")) + { + packageName = line[6..]; + } + else if (line.StartsWith("Version: ")) + { + packageVersion = line[9..]; + break; + } + } + } + } + } + catch (Exception e) when (e is IOException or InvalidDataException) + { + _toastService.Error(Lang.LibraryInstall_InstallDistributions_IOError); + return; + } + + if (packageName == "" || packageVersion == "") + { + _toastService.Error(Lang.LibraryInstall_InstallDistributions_InvalidFile); + return; + } + + if (PreInstallDistributions.Any(item => item.PackageName == packageName)) + { + _toastService.Error(Lang.LibraryInstall_InstallDistributions_AlreadyExists); + return; + } + + PreInstallDistributions.Add(new LibraryInstallPackageItem + { + PackageName = packageName, + TargetVersion = packageVersion, + DistributionFilePath = fileName + }); + } + } + + [RelayCommand] + private void DeleteInstallDistributions(object? parameter) + { + var target = -1; + for (int index = 0; index < PreInstallDistributions.Count; index++) + { + if (ReferenceEquals(PreInstallDistributions[index].PackageName, parameter)) + { + target = index; + } + } + + if (target != -1) + { + PreInstallDistributions.RemoveAt(target); + } + } + + [RelayCommand] + private void InstallDistributionsToAction() + { + List operationCommand = []; + operationCommand.AddRange(PreInstallDistributions.Select(preInstallPackage => preInstallPackage.DistributionFilePath)!); + _actionService.AddOperation(new ActionListItem + ( + ActionType.Install, + operationCommand.ToArray(), + totalSubTaskNumber: operationCommand.Count, + extraParameters: DownloadWheelDependencies ? null : ["--no-deps"] + )); + PreInstallDistributions.Clear(); + } + + #endregion } \ No newline at end of file diff --git a/src/PipManager/ViewModels/Pages/Library/LibraryViewModel.cs b/src/PipManager/ViewModels/Pages/Library/LibraryViewModel.cs index 9fe0121..2628ecb 100644 --- a/src/PipManager/ViewModels/Pages/Library/LibraryViewModel.cs +++ b/src/PipManager/ViewModels/Pages/Library/LibraryViewModel.cs @@ -94,7 +94,7 @@ private async Task DeletePackageAsync() _actionService.AddOperation(new ActionListItem ( ActionType.Uninstall, - command.Trim(), + command.Trim().Split(' '), progressIntermediate: false, totalSubTaskNumber: selected.Count )); @@ -141,7 +141,7 @@ await Task.Run(() => _actionService.AddOperation(new ActionListItem ( ActionType.Update, - operationList.Trim(), + operationList.Trim().Split(' '), progressIntermediate: false, totalSubTaskNumber: msgList.Count )); diff --git a/src/PipManager/ViewModels/Pages/Search/SearchDetailViewModel.cs b/src/PipManager/ViewModels/Pages/Search/SearchDetailViewModel.cs index 1c8603c..a49b5e9 100644 --- a/src/PipManager/ViewModels/Pages/Search/SearchDetailViewModel.cs +++ b/src/PipManager/ViewModels/Pages/Search/SearchDetailViewModel.cs @@ -4,7 +4,6 @@ using PipManager.Languages; using PipManager.PackageSearch.Wrappers.Query; using PipManager.Services.Environment; -using PipManager.Services.Mask; using PipManager.Services.Toast; using PipManager.Views.Pages.Search; using Serilog; @@ -24,11 +23,10 @@ public record SearchDetailMessage(QueryListItemModel Package); private readonly HttpClient _httpClient; private readonly IThemeService _themeService; private readonly IToastService _toastService; - private readonly IMaskService _maskService; private readonly IEnvironmentService _environmentService; [ObservableProperty] - private bool _projectDescriptionVisibility = false; + private bool _projectDescriptionVisibility; private string _themeType = "light"; @@ -56,13 +54,12 @@ public record SearchDetailMessage(QueryListItemModel Package); [ObservableProperty] private QueryListItemModel? _package; - public SearchDetailViewModel(INavigationService navigationService, HttpClient httpClient, IThemeService themeService, IToastService toastService, IMaskService maskService, IEnvironmentService environmentService) + public SearchDetailViewModel(INavigationService navigationService, HttpClient httpClient, IThemeService themeService, IToastService toastService, IEnvironmentService environmentService) { _navigationService = navigationService; _httpClient = httpClient; _themeService = themeService; _toastService = toastService; - _maskService = maskService; _environmentService = environmentService; WeakReferenceMessenger.Default.Register(this, Receive); @@ -118,7 +115,6 @@ private async Task InstallPackage() if (installedPackages.Any(item => item.Name == Package!.Name)) { _toastService.Error(Lang.LibraryInstall_Add_AlreadyInstalled); - return; } } @@ -126,7 +122,7 @@ public void Receive(object recipient, SearchDetailMessage message) { Package = message.Package; - SearchDetailPage.ProjectDescriptionWebView!.Loaded += async (sender, e) => + SearchDetailPage.ProjectDescriptionWebView!.Loaded += async (_, _) => { ProjectDescriptionVisibility = false; var packageVersions = await _environmentService.GetVersions(Package!.Name); @@ -145,8 +141,8 @@ public void Receive(object recipient, SearchDetailMessage message) TargetVersion = AvailableVersions.First(); break; } - var webView2Environment = await CoreWebView2Environment.CreateAsync(null, AppInfo.CachesDir); - await SearchDetailPage.ProjectDescriptionWebView!.EnsureCoreWebView2Async().ConfigureAwait(true); + await CoreWebView2Environment.CreateAsync(null, AppInfo.CachesDir); + await SearchDetailPage.ProjectDescriptionWebView.EnsureCoreWebView2Async().ConfigureAwait(true); try { var projectDescriptionUrl = message.Package.Url; @@ -155,8 +151,8 @@ public void Receive(object recipient, SearchDetailMessage message) 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); + SearchDetailPage.ProjectDescriptionWebView.CoreWebView2.Profile.PreferredColorScheme = CoreWebView2PreferredColorScheme.Dark; + SearchDetailPage.ProjectDescriptionWebView.NavigateToString(projectDescriptionHtml); } catch (Exception ex) { @@ -164,8 +160,8 @@ public void Receive(object recipient, SearchDetailMessage 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); + SearchDetailPage.ProjectDescriptionWebView.CoreWebView2.Profile.PreferredColorScheme = CoreWebView2PreferredColorScheme.Dark; + SearchDetailPage.ProjectDescriptionWebView.NavigateToString(projectDescriptionHtml); } finally { diff --git a/src/PipManager/ViewModels/Pages/Search/SearchViewModel.cs b/src/PipManager/ViewModels/Pages/Search/SearchViewModel.cs index d49ada0..d16c340 100644 --- a/src/PipManager/ViewModels/Pages/Search/SearchViewModel.cs +++ b/src/PipManager/ViewModels/Pages/Search/SearchViewModel.cs @@ -27,16 +27,16 @@ public partial class SearchViewModel(IPackageSearchService packageSearchService, private string _totalResultNumber = ""; [ObservableProperty] - private bool _onQuerying = false; + private bool _onQuerying; [ObservableProperty] - private bool _successQueried = false; + private bool _successQueried; [ObservableProperty] private bool _reachesFirstPage = true; [ObservableProperty] - private bool _reachesLastPage = false; + private bool _reachesLastPage; [ObservableProperty] private int _currentPage = 1; @@ -65,7 +65,6 @@ private void InitializeViewModel() [RelayCommand] private void ToDetailPage(object parameter) { - if (QueryList is null) return; navigationService.Navigate(typeof(SearchDetailPage)); var current = QueryList.Where(searchListItem => searchListItem.Name == parameter as string).ToList()[0]; WeakReferenceMessenger.Default.Send(new SearchDetailMessage(current)); @@ -157,7 +156,7 @@ public async Task Search(string? parameter) MaxPage = 1; CurrentPage = 1; QueryPackageName = parameter; - var result = await packageSearchService.Query(parameter, 1); + var result = await packageSearchService.Query(parameter); Process(result); OnQuerying = false; } diff --git a/src/PipManager/ViewModels/Pages/Tools/ToolsViewModel.cs b/src/PipManager/ViewModels/Pages/Tools/ToolsViewModel.cs index 5daf698..1b8d115 100644 --- a/src/PipManager/ViewModels/Pages/Tools/ToolsViewModel.cs +++ b/src/PipManager/ViewModels/Pages/Tools/ToolsViewModel.cs @@ -6,6 +6,9 @@ namespace PipManager.ViewModels.Pages.Tools; public partial class ToolsViewModel : ObservableObject, INavigationAware { private bool _isInitialized; + + [ObservableProperty] + private string? _testProperty; public void OnNavigatedTo() { diff --git a/src/PipManager/ViewModels/Windows/MainWindowViewModel.cs b/src/PipManager/ViewModels/Windows/MainWindowViewModel.cs index 2216cb2..3ccc622 100644 --- a/src/PipManager/ViewModels/Windows/MainWindowViewModel.cs +++ b/src/PipManager/ViewModels/Windows/MainWindowViewModel.cs @@ -5,16 +5,14 @@ namespace PipManager.ViewModels.Windows; public partial class MainWindowViewModel : ObservableObject { - private readonly IConfigurationService _configurationService; [ObservableProperty] private bool _experimentMode; public MainWindowViewModel(IConfigurationService configurationService) { - _configurationService = configurationService; - if (_configurationService.AppConfig.CurrentEnvironment != null) + if (configurationService.AppConfig.CurrentEnvironment != null) { - Log.Information($"[MainWindow] Environment loaded ({_configurationService.AppConfig.CurrentEnvironment.PipVersion} for {_configurationService.AppConfig.CurrentEnvironment.PythonVersion})"); - ApplicationTitle = $"Pip Manager | {_configurationService.AppConfig.CurrentEnvironment.PipVersion} for {_configurationService.AppConfig.CurrentEnvironment.PythonVersion}"; + Log.Information($"[MainWindow] Environment loaded ({configurationService.AppConfig.CurrentEnvironment.PipVersion} for {configurationService.AppConfig.CurrentEnvironment.PythonVersion})"); + ApplicationTitle = $"Pip Manager | {configurationService.AppConfig.CurrentEnvironment.PipVersion} for {configurationService.AppConfig.CurrentEnvironment.PythonVersion}"; } else { diff --git a/src/PipManager/Views/Pages/Action/ActionExceptionPage.xaml b/src/PipManager/Views/Pages/Action/ActionExceptionPage.xaml index b1a4a7f..380a2b1 100644 --- a/src/PipManager/Views/Pages/Action/ActionExceptionPage.xaml +++ b/src/PipManager/Views/Pages/Action/ActionExceptionPage.xaml @@ -1,25 +1,25 @@ + x:Class="PipManager.Views.Pages.Action.ActionExceptionPage" + x:Name="ActionException" + xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" + xmlns:action="clr-namespace:PipManager.Views.Pages.Action" + xmlns:action1="clr-namespace:PipManager.Models.Action" + xmlns:d="http://schemas.microsoft.com/expression/blend/2008" + xmlns:lang="clr-namespace:PipManager.Languages" + xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" + xmlns:ui="http://schemas.lepo.co/wpfui/2022/xaml" + xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"> @@ -29,39 +29,39 @@ - + + Text="{Binding OperationDescription}" + VerticalAlignment="Center" /> + VerticalAlignment="Center"> - + + TextWrapping="WrapWithOverflow" + VerticalAlignment="Center" /> - + + Margin="5,0,0,0" + Text="{Binding OperationTimestamp}" + VerticalAlignment="Center" /> @@ -76,47 +76,44 @@ + Grid.Row="1" + Orientation="Horizontal" + VerticalAlignment="Bottom"> + Icon="{ui:SymbolIcon Search24}" + Margin="3,0,0,0" /> + Content="{I18N {x:Static lang:LangKeys.ActionException_CopyToClipboard}}" + Margin="3,0,0,0" /> diff --git a/src/PipManager/Views/Pages/Action/ActionPage.xaml b/src/PipManager/Views/Pages/Action/ActionPage.xaml index f659997..171afa9 100644 --- a/src/PipManager/Views/Pages/Action/ActionPage.xaml +++ b/src/PipManager/Views/Pages/Action/ActionPage.xaml @@ -1,10 +1,12 @@ + ItemsSource="{Binding ViewModel.Actions.AsObservable}"> + + Text="{Binding ConsoleOutput}"> + + + + + + diff --git a/src/PipManager/Views/Pages/Environment/AddEnvironmentPage.xaml b/src/PipManager/Views/Pages/Environment/AddEnvironmentPage.xaml index f641eb9..ed65968 100644 --- a/src/PipManager/Views/Pages/Environment/AddEnvironmentPage.xaml +++ b/src/PipManager/Views/Pages/Environment/AddEnvironmentPage.xaml @@ -76,7 +76,7 @@ FontSize="22" Text="{I18N {x:Static lang:LangKeys.EnvironmentAdd_EnvironmentVariable_NotFound}}" Visibility="{Binding ViewModel.Found, Converter={StaticResource BoolToVisibility}}" /> - - + @@ -114,8 +114,8 @@ - - + + + xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"> @@ -28,44 +29,51 @@ Content="{I18N {x:Static lang:LangKeys.Environment_Operation_AddEnvironment}}" Icon="{ui:SymbolIcon Add24}" /> + IsEnabled="{Binding ViewModel.CurrentEnvironment, Converter={StaticResource NullToBool}}" + Margin="5,0,0,0" /> + IsEnabled="{Binding ViewModel.CurrentEnvironment, Converter={StaticResource NullToBool}}" + Margin="5,0,0,0" /> + IsEnabled="{Binding ViewModel.CurrentEnvironment, Converter={StaticResource NullToBool}}" + Margin="5,0,0,0" /> + - - + SelectedItem="{Binding ViewModel.CurrentEnvironment}" + VerticalAlignment="Stretch"> + - + + + Source="../../../Assets/logo/python-logo-only.png" + Width="48" /> @@ -81,7 +89,7 @@ - - + + \ No newline at end of file diff --git a/src/PipManager/Views/Pages/Library/LibraryInstallPage.xaml b/src/PipManager/Views/Pages/Library/LibraryInstallPage.xaml index 510190f..e327794 100644 --- a/src/PipManager/Views/Pages/Library/LibraryInstallPage.xaml +++ b/src/PipManager/Views/Pages/Library/LibraryInstallPage.xaml @@ -1,37 +1,37 @@  + x:Class="PipManager.Views.Pages.Library.LibraryInstallPage" + x:Name="LibraryInstall" + xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" + xmlns:d="http://schemas.microsoft.com/expression/blend/2008" + xmlns:lang="clr-namespace:PipManager.Languages" + xmlns:library="clr-namespace:PipManager.Views.Pages.Library" + xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" + xmlns:pages="clr-namespace:PipManager.Models.Pages" + xmlns:ui="http://schemas.lepo.co/wpfui/2022/xaml" + xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"> - + + + IsEnabled="{Binding ElementName=AddDefaultTaskList, Path=Items.Count, Converter={StaticResource IntegerToBool}}" + Margin="5,0,0,0" /> - + @@ -69,38 +69,37 @@ + Text="{Binding PackageName}" + VerticalAlignment="Center" /> + IsChecked="{Binding VersionSpecified, Mode=TwoWay}" + x:Name="VersionSpecifiedCheckbox" /> - + + + Text="{Binding ViewModel.Requirements, Mode=TwoWay}" + x:Name="AddRequirementsTextBox" /> + IsEnabled="{Binding ElementName=AddRequirementsTextBox, Path=Text, Converter={StaticResource StringIsNotNullOrEmpty}}" + Margin="0,10,0,0" /> + - + - - + + + Text="{Binding PackageName}" + VerticalAlignment="Center" /> + IsChecked="{Binding VersionSpecified, Mode=TwoWay}" + x:Name="DownloadDistributionsVersionSpecifiedCheckbox" /> - + + Text="{I18N {x:Static lang:LangKeys.LibraryInstall_Requirements_DownloadFolder}}" + VerticalAlignment="Center" /> + Margin="10,0,0,0" + Text="{Binding ViewModel.DownloadDistributionsFolderPath, Mode=TwoWay}" + x:Name="DownloadDistributionsFolderBrowseTextBox" /> + IsEnabled="{Binding ElementName=DownloadDistributionsTaskList, Path=Items.Count, Converter={StaticResource IntegerToBool}}" + Margin="5,0,0,0" /> + IsEnabled="{Binding ViewModel.DownloadDistributionsEnabled}" + Margin="5,0,0,0" /> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/PipManager/Views/Pages/Library/LibraryPage.xaml b/src/PipManager/Views/Pages/Library/LibraryPage.xaml index e83c747..5452a22 100644 --- a/src/PipManager/Views/Pages/Library/LibraryPage.xaml +++ b/src/PipManager/Views/Pages/Library/LibraryPage.xaml @@ -1,27 +1,25 @@  + xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"> @@ -37,28 +35,28 @@ Icon="{ui:SymbolIcon Add24}" IsEnabled="{Binding ViewModel.EnvironmentFoundVisible}" /> + IsEnabled="{Binding ElementName=LibraryList, Path=SelectedItems.Count, Converter={StaticResource IntegerToBool}}" + Margin="5,0,0,0" /> + IsEnabled="{Binding ElementName=LibraryList, Path=SelectedItems.Count, Converter={StaticResource IntegerToBool}}" + Margin="5,0,0,0" /> + Orientation="Horizontal" + VerticalAlignment="Center"> @@ -71,14 +69,14 @@ - + @@ -94,17 +92,17 @@ - - + Visibility="{Binding ViewModel.LibraryList, Converter={StaticResource NotNullToVisibility}}" + x:Name="LibraryList"> + @@ -116,9 +114,9 @@ + Text="{Binding PackageName}" + VerticalAlignment="Center" /> @@ -143,37 +141,38 @@ TextWrapping="Wrap" /> + Grid.Column="0" + Grid.ColumnSpan="2" + HorizontalAlignment="Right" + Icon="{ui:SymbolIcon ChevronRight24}" + Margin="0,0,10,0" /> - - - - - + + + FontSize="16" + HorizontalAlignment="Center" + Margin="0,5,0,0" /> \ No newline at end of file diff --git a/src/PipManager/Views/Pages/Search/SearchDetailPage.xaml b/src/PipManager/Views/Pages/Search/SearchDetailPage.xaml index 310bb14..683135c 100644 --- a/src/PipManager/Views/Pages/Search/SearchDetailPage.xaml +++ b/src/PipManager/Views/Pages/Search/SearchDetailPage.xaml @@ -1,25 +1,25 @@  + x:Class="PipManager.Views.Pages.Search.SearchDetailPage" + x:Name="SearchDetail" + xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" + xmlns:d="http://schemas.microsoft.com/expression/blend/2008" + xmlns:lang="clr-namespace:PipManager.Languages" + xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" + xmlns:search="clr-namespace:PipManager.Views.Pages.Search" + xmlns:ui="http://schemas.lepo.co/wpfui/2022/xaml" + xmlns:wpf="clr-namespace:Microsoft.Web.WebView2.Wpf;assembly=Microsoft.Web.WebView2.Wpf" + xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"> @@ -28,27 +28,27 @@ + CornerRadius="5" + Grid.Row="1" + Margin="0,10,0,0" + Padding="3"> + VerticalAlignment="Center" + Visibility="{Binding ViewModel.ProjectDescriptionVisibility, Converter={StaticResource InverseBoolToVisibility}}" + Width="100" /> + NavigationStarting="SearchDetailProjectDescriptionWebView_NavigationStarting" + Visibility="{Binding ViewModel.ProjectDescriptionVisibility, Converter={StaticResource BoolToVisibility}}" + x:Name="SearchDetailProjectDescriptionWebView" /> @@ -57,19 +57,19 @@ + Text="{Binding ViewModel.Package.Name}" + VerticalAlignment="Center" /> + VerticalAlignment="Center"> @@ -78,35 +78,35 @@ FontTypography="Caption" Text="{I18N {x:Static lang:LangKeys.SearchDetail_LatestUpdatedTime}}" /> + Orientation="Horizontal" + VerticalAlignment="Center"> + SelectedItem="{Binding ViewModel.TargetVersion, Mode=TwoWay}" + VerticalAlignment="Center" + Width="150" /> + IsEnabled="False" + Margin="15,0,0,0" /> + IsEnabled="False" + Margin="15,0,0,0" /> diff --git a/src/PipManager/Views/Pages/Search/SearchDetailPage.xaml.cs b/src/PipManager/Views/Pages/Search/SearchDetailPage.xaml.cs index ae5e662..5ef7642 100644 --- a/src/PipManager/Views/Pages/Search/SearchDetailPage.xaml.cs +++ b/src/PipManager/Views/Pages/Search/SearchDetailPage.xaml.cs @@ -1,4 +1,5 @@ -using Microsoft.Web.WebView2.Wpf; +using Microsoft.Web.WebView2.Core; +using Microsoft.Web.WebView2.Wpf; using Wpf.Ui.Controls; using SearchDetailViewModel = PipManager.ViewModels.Pages.Search.SearchDetailViewModel; @@ -6,7 +7,7 @@ namespace PipManager.Views.Pages.Search; public partial class SearchDetailPage : INavigableView { - public static WebView2? ProjectDescriptionWebView { get; set; } + public static WebView2? ProjectDescriptionWebView { get; private set; } public SearchDetailViewModel ViewModel { get; } @@ -15,10 +16,10 @@ public SearchDetailPage(SearchDetailViewModel viewModel) ViewModel = viewModel; DataContext = this; InitializeComponent(); - ProjectDescriptionWebView = SearchDetailProjectDesciptionWebView; + ProjectDescriptionWebView = SearchDetailProjectDescriptionWebView; } - private void SearchDetailProjectDesciptionWebView_NavigationStarting(object sender, Microsoft.Web.WebView2.Core.CoreWebView2NavigationStartingEventArgs e) + private void SearchDetailProjectDescriptionWebView_NavigationStarting(object sender, CoreWebView2NavigationStartingEventArgs e) { if (e.Uri.StartsWith("http://") || e.Uri.StartsWith("https://")) { @@ -26,9 +27,9 @@ private void SearchDetailProjectDesciptionWebView_NavigationStarting(object send } } - private void SearchDetailProjectDesciptionWebView_CoreWebView2InitializationCompleted(object sender, Microsoft.Web.WebView2.Core.CoreWebView2InitializationCompletedEventArgs e) + private void SearchDetailProjectDescriptionWebView_CoreWebView2InitializationCompleted(object sender, CoreWebView2InitializationCompletedEventArgs? e) { - if (e != null && e.IsSuccess) + if (e is { IsSuccess: true }) { ProjectDescriptionWebView!.CoreWebView2.AddScriptToExecuteOnDocumentCreatedAsync( "document.addEventListener('contextmenu', event => event.preventDefault());"); diff --git a/src/PipManager/Views/Pages/Search/SearchPage.xaml b/src/PipManager/Views/Pages/Search/SearchPage.xaml index f2d6e80..f9cba8d 100644 --- a/src/PipManager/Views/Pages/Search/SearchPage.xaml +++ b/src/PipManager/Views/Pages/Search/SearchPage.xaml @@ -1,25 +1,25 @@ + xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"> @@ -34,77 +34,76 @@ + x:Name="SearchTextBox" /> + IsDefault="True" + Margin="5,0,0,0" + VerticalAlignment="Stretch" /> + VerticalAlignment="Center" + Visibility="{Binding ViewModel.OnQuerying, Converter={StaticResource BoolToVisibility}}" + Width="80" /> - - + x:Name="SearchList"> + + Text="{Binding Name}" + VerticalAlignment="Center" /> + Text="{Binding Version}" + VerticalAlignment="Center" /> + TextWrapping="Wrap" + Width="800" /> + Grid.Column="0" + HorizontalAlignment="Right" + Icon="{ui:SymbolIcon ChevronRight24}" + Margin="0,0,10,0" /> - - + + + Text="{Binding ViewModel.CurrentPage}" + VerticalAlignment="Center" /> + Text=" / " + VerticalAlignment="Center" /> + Text="{Binding ViewModel.MaxPage}" + VerticalAlignment="Center" /> diff --git a/src/PipManager/Views/Pages/Settings/SettingsPage.xaml b/src/PipManager/Views/Pages/Settings/SettingsPage.xaml index f9e6953..899bba2 100644 --- a/src/PipManager/Views/Pages/Settings/SettingsPage.xaml +++ b/src/PipManager/Views/Pages/Settings/SettingsPage.xaml @@ -1,7 +1,16 @@  + xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"> - @@ -35,7 +31,7 @@ - + @@ -43,23 +39,23 @@ + Margin="0,8,0,0" + Message="{I18N {x:Static lang:LangKeys.Settings_PackageSource_Notice}}" + Title="{I18N {x:Static lang:LangKeys.Common_NoticeTitle_Notice}}" /> @@ -72,69 +68,69 @@ + Icon="{ui:SymbolIcon NetworkCheck20}" + Margin="0,10,0,0" /> @@ -147,7 +143,7 @@ - + @@ -155,23 +151,23 @@ + SelectedItem="{Binding ViewModel.Language, Mode=TwoWay}" + Width="200" + x:Name="LanguageComboBox"> @@ -181,7 +177,7 @@ - + @@ -189,13 +185,13 @@ @@ -207,17 +203,17 @@ GroupName="themeSelect" IsChecked="{Binding ViewModel.CurrentTheme, Converter={StaticResource ThemeEnumToBooleanConverter}, ConverterParameter=Light, Mode=TwoWay}" /> + IsChecked="{Binding ViewModel.CurrentTheme, Converter={StaticResource ThemeEnumToBooleanConverter}, ConverterParameter=Dark, Mode=TwoWay}" + Margin="10,0,0,0" /> - + @@ -225,33 +221,33 @@ - + + Width="200" + x:Name="LogAutoDeletionSlider"> @@ -259,9 +255,9 @@ @@ -273,7 +269,7 @@ - + @@ -281,33 +277,33 @@ - + + Width="200" + x:Name="CrushesAutoDeletionSlider"> @@ -315,9 +311,9 @@ @@ -337,9 +333,9 @@ + Icon="{ui:SymbolIcon Delete24}" + Margin="0,5,0,0"> @@ -347,13 +343,13 @@ @@ -361,9 +357,9 @@ + Icon="{ui:SymbolIcon AppFolder20}" + Margin="0,3,0,0"> @@ -371,13 +367,13 @@ @@ -385,9 +381,9 @@ + Icon="{ui:SymbolIcon Record20}" + Margin="0,3,0,0"> @@ -395,13 +391,13 @@ @@ -409,9 +405,9 @@ + Icon="{ui:SymbolIcon CircleOff20}" + Margin="0,3,0,0"> @@ -419,20 +415,20 @@ - + @@ -440,13 +436,13 @@ diff --git a/src/PipManager/Views/Windows/ExceptionWindow.xaml.cs b/src/PipManager/Views/Windows/ExceptionWindow.xaml.cs index 5d8deae..f474603 100644 --- a/src/PipManager/Views/Windows/ExceptionWindow.xaml.cs +++ b/src/PipManager/Views/Windows/ExceptionWindow.xaml.cs @@ -16,7 +16,10 @@ public void Initialize(Exception exception) { TypeTextBlock.Text = exception.GetType().ToString(); MessageTextBlock.Text = exception.Message; - StackTraceTextBox.Text = exception.StackTrace; + if (exception.StackTrace != null) + { + StackTraceTextBox.Text = exception.StackTrace; + } } private void ReportButton_OnClick(object sender, RoutedEventArgs e) diff --git a/src/PipManager/Views/Windows/MainWindow.xaml b/src/PipManager/Views/Windows/MainWindow.xaml index 0a561ad..776aa14 100644 --- a/src/PipManager/Views/Windows/MainWindow.xaml +++ b/src/PipManager/Views/Windows/MainWindow.xaml @@ -1,7 +1,25 @@  + xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"> @@ -43,18 +43,18 @@ + Padding="42,0,42,0" + Transition="FadeInWithSlide" + x:Name="NavigationView"> - + + Grid.RowSpan="2" + x:Name="MaskPresenter" /> + Grid.RowSpan="2" + x:Name="MaskActionExceptionPresenter" /> + Grid.RowSpan="2" + x:Name="RootContentDialog" /> + + Grid.Row="0" + ShowMaximize="False" + Title="{Binding ViewModel.ApplicationTitle}"> + diff --git a/src/PipManager/Views/Windows/MainWindow.xaml.cs b/src/PipManager/Views/Windows/MainWindow.xaml.cs index e26dd1f..04c72fa 100644 --- a/src/PipManager/Views/Windows/MainWindow.xaml.cs +++ b/src/PipManager/Views/Windows/MainWindow.xaml.cs @@ -18,7 +18,6 @@ public MainWindow( MainWindowViewModel viewModel, INavigationService navigationService, IServiceProvider serviceProvider, - ISnackbarService snackbarService, IContentDialogService contentDialogService, IMaskService maskPresenter, IActionService actionService