Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions src/Languages/lang_en.json
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,48 @@
"The selected package manager is not ready.": "The selected package manager is not ready.",
"Catalog package search failed: {0}": "Catalog package search failed: {0}",
"Discard unsaved catalog changes?": "Discard unsaved catalog changes?",
"New file": "New file",
Comment thread
GabrielDuf marked this conversation as resolved.
Resolved
"Add packages": "Add packages",
"Add packages to {0}": "Add packages to {0}",
"Search for packages, check the ones you want, then click Add selection to catalog.": "Search for packages, check the ones you want, then click Add selection to catalog.",
"Packages added to catalog": "Packages added to catalog",
"More actions": "More actions",
"No software catalog has been set up on this device yet.": "No software catalog has been set up on this device yet.",
"Expected at {0}": "Expected at {0}",
"The software catalog is not valid. Ask an administrator to fix it.": "The software catalog is not valid. Ask an administrator to fix it.",
"The software catalog could not be read. Ask an administrator to check its permissions.": "The software catalog could not be read. Ask an administrator to check its permissions.",
"Version {0}": "Version {0}",
"Loading versions": "Loading versions",
"Pin version": "Pin version",
"Enable this package's manager and source to pin its version.": "Enable this package's manager and source to pin its version.",
"{0} cannot install a specific version, so this package always installs the latest version.": "{0} cannot install a specific version, so this package always installs the latest version.",
"Choose the version of {0} that this catalog installs.": "Choose the version of {0} that this catalog installs.",
"The available versions could not be loaded: {0}": "The available versions could not be loaded: {0}",
"The sources of {0} could not be listed. Check the log, then try again.": "The sources of {0} could not be listed. Check the log, then try again.",
"1 package added to catalog {0}. Save the catalog to keep your changes.": "1 package added to catalog {0}. Save the catalog to keep your changes.",
"1 package added to {0} catalogs. Save the catalog to keep your changes.": "1 package added to {0} catalogs. Save the catalog to keep your changes.",
"Choose the catalogs to add the selected package to.": "Choose the catalogs to add the selected package to.",
"1 was already in the catalog.": "1 was already in the catalog.",
"Remove catalog {0} and its package?": "Remove catalog {0} and its package?",
"Choose the catalogs to add the {0} selected packages to.": "Choose the catalogs to add the {0} selected packages to.",
"Add to {0} catalogs": "Add to {0} catalogs",
"{0} were already in the catalog.": "{0} were already in the catalog.",
"{0}: {1} added": "{0}: {1} added",
"{0}: {1} added, {2} already there": "{0}: {1} added, {2} already there",
"Save the catalog to keep your changes.": "Save the catalog to keep your changes.",
"Packages added to {0} catalogs": "Packages added to {0} catalogs",
"{0} packages added to {1} catalogs. Save the catalog to keep your changes.": "{0} packages added to {1} catalogs. Save the catalog to keep your changes.",
"Create a catalog in the Catalog Editor first.": "Create a catalog in the Catalog Editor first.",
"Catalog saved": "Catalog saved",
"The catalog was not saved.": "The catalog was not saved.",
"UniGetUI needs administrator rights to save the software catalog.": "UniGetUI needs administrator rights to save the software catalog.",
"Could not copy the file to {0} as an administrator (robocopy exit code {1}).": "Could not copy the file to {0} as an administrator (robocopy exit code {1}).",
"Remove": "Remove",
"Remove catalog": "Remove catalog",
"Remove catalog {0}": "Remove catalog {0}",
"Remove catalog {0} and its {1} packages?": "Remove catalog {0} and its {1} packages?",
"Discard unsaved catalog changes and start a new catalog file?": "Discard unsaved catalog changes and start a new catalog file?",
"Close the current catalog file and start a new one? Use New catalog to add a catalog to this file instead.": "Close the current catalog file and start a new one? Use New catalog to add a catalog to this file instead.",
"Select a local catalog file.": "Select a local catalog file.",
"Could not select a catalog file: {0}": "Could not select a catalog file: {0}",
"The catalog editor could not be opened: {0}": "The catalog editor could not be opened: {0}",
Expand Down
3 changes: 3 additions & 0 deletions src/SharedAssets/Assets/Symbols/catalog_add.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions src/SharedAssets/Assets/Symbols/collections_add.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions src/SharedAssets/Assets/Symbols/document_add.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions src/SharedAssets/Assets/Symbols/rename.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
18 changes: 18 additions & 0 deletions src/UniGetUI.Avalonia/Assets/Styles/Styles.MacGlassControls.axaml
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,20 @@
<Setter Property="CornerRadius" Value="18"/>
<Setter Property="Margin" Value="0,2"/>
</Style>
<!-- Grouped buttons sit inside a capsule with a 1px rim; keep their highlight inside that rim. -->
<Style Selector="controls|ToolbarOverflowPanel#ToolBar Button:not(:disabled) /template/ ContentPresenter#PART_ContentPresenter,
controls|ToolbarOverflowPanel#ToolBar Button:disabled /template/ ContentPresenter#PART_ContentPresenter">
<Setter Property="Margin" Value="1,3"/>
</Style>
<!-- A dropdown in a group (e.g. the catalog selector) blends into the capsule the same way. -->
<Style Selector="controls|ToolbarOverflowPanel#ToolBar ComboBox">
<Setter Property="Background" Value="Transparent"/>
<Setter Property="BorderThickness" Value="0"/>
<Setter Property="CornerRadius" Value="18"/>
<Setter Property="MinHeight" Value="0"/>
<Setter Property="Margin" Value="1,3"/>
<Setter Property="VerticalAlignment" Value="Stretch"/>
</Style>
<Style Selector="Button#MainToolbarButton:not(:disabled) /template/ ContentPresenter#PART_ContentPresenter,
Button#MainToolbarButton:disabled /template/ ContentPresenter#PART_ContentPresenter">
<Setter Property="CornerRadius" Value="18,0,0,18"/>
Expand All @@ -90,6 +104,10 @@
Button#MainToolbarButtonDropdown:disabled /template/ ContentPresenter#PART_ContentPresenter">
<Setter Property="CornerRadius" Value="0,18,18,0"/>
</Style>
<Style Selector="Button#MainToolbarButton.standalone:not(:disabled) /template/ ContentPresenter#PART_ContentPresenter,
Button#MainToolbarButton.standalone:disabled /template/ ContentPresenter#PART_ContentPresenter">
<Setter Property="CornerRadius" Value="18"/>
</Style>
<Style Selector="#ToolbarRow ToggleButton:not(:disabled), #ToolbarRow ToggleButton:disabled">
<Setter Property="Background" Value="{DynamicResource MacGlassControlBackground}"/>
<Setter Property="BorderBrush" Value="{DynamicResource MacGlassPanelBorderBrush}"/>
Expand Down
49 changes: 48 additions & 1 deletion src/UniGetUI.Avalonia/Infrastructure/AvaloniaAppHost.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
using System.IO;
using System.Runtime.ExceptionServices;
using System.Runtime.InteropServices;
using Avalonia;
using Avalonia.Automation;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Media;
#if WINDOWS
using Avalonia.Win32;
Expand Down Expand Up @@ -134,7 +137,51 @@ Welcome to UniGetUI Version {CoreData.VersionName}
return;
}

BuildAvaloniaApp().StartWithClassicDesktopLifetime(args);
try
{
BuildAvaloniaApp().StartWithClassicDesktopLifetime(args);
}
catch (ElementNotEnabledException ex)
{
ResumeAfterRefusedAutomation(ex);
}
}

/// <summary>
/// An accessibility client (VoiceOver, UI automation) invoking a disabled control makes Avalonia
/// throw <see cref="ElementNotEnabledException"/>. On macOS that escapes the native run loop with
/// no handler and would end the app, although the right outcome is to ignore the request. The
/// windows and lifetime are still intact, so log it and keep running the main loop until the
/// lifetime's normal exit.
/// </summary>
private static void ResumeAfterRefusedAutomation(ElementNotEnabledException first)
{
if (Application.Current?.ApplicationLifetime is not IClassicDesktopStyleApplicationLifetime lifetime)
{
ExceptionDispatchInfo.Capture(first).Throw();
return;
}
using var exited = new CancellationTokenSource();
lifetime.Exit += (_, e) =>
{
Environment.ExitCode = e.ApplicationExitCode;
exited.Cancel();
};
Exception? refused = first;
while (!exited.IsCancellationRequested)
{
Logger.Warn("An accessibility client tried to invoke a disabled control; ignoring it.");
Logger.Warn(refused!);
try
{
Dispatcher.UIThread.MainLoop(exited.Token);
refused = null;
}
catch (ElementNotEnabledException ex)
{
refused = ex;
}
}
}

/// <summary>
Expand Down
55 changes: 55 additions & 0 deletions src/UniGetUI.Avalonia/Infrastructure/MacAdminFileInstaller.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
using System.Diagnostics;
using System.Runtime.Versioning;

namespace UniGetUI.Avalonia.Infrastructure;

/// <summary>
/// Writes a file to a root-owned location on macOS through the system administrator prompt.
/// </summary>
[SupportedOSPlatform("macos")]
internal static class MacAdminFileInstaller
{
private const int UserCanceledError = -128;

/// <summary>
/// Copies <paramref name="contents"/> to <paramref name="destination"/> as root, creating its folder.
/// <paramref name="prompt"/> is shown in the password dialog. Returns false when the user cancels it.
/// </summary>
public static async Task<bool> WriteAsync(string destination, string contents, string prompt)
{
string stagingDirectory = Directory.CreateTempSubdirectory("unigetui-admin-write-").FullName;
try
{
string staged = Path.Combine(stagingDirectory, Path.GetFileName(destination));
await File.WriteAllTextAsync(staged, contents);

string folder = Path.GetDirectoryName(destination)!;
string command = $"/bin/mkdir -p {ShellQuote(folder)} && /usr/bin/install -m 0644 {ShellQuote(staged)} {ShellQuote(destination)}";
var startInfo = new ProcessStartInfo("/usr/bin/osascript")
{
RedirectStandardError = true,
RedirectStandardOutput = true,
UseShellExecute = false,
};
startInfo.ArgumentList.Add("-e");
startInfo.ArgumentList.Add($"do shell script \"{AppleScriptEscape(command)}\" with prompt \"{AppleScriptEscape(prompt)}\" with administrator privileges");

using var process = Process.Start(startInfo)
?? throw new IOException("Could not start the administrator prompt.");
string error = await process.StandardError.ReadToEndAsync();
await process.WaitForExitAsync();
if (process.ExitCode == 0) return true;
if (error.Contains($"({UserCanceledError})", StringComparison.Ordinal)) return false;
throw new IOException(error.Trim());
}
finally
{
try { Directory.Delete(stagingDirectory, recursive: true); }
catch { /* best effort cleanup of a temp folder */ }
}
}

internal static string ShellQuote(string value) => "'" + value.Replace("'", "'\\''") + "'";

internal static string AppleScriptEscape(string value) => value.Replace("\\", "\\\\").Replace("\"", "\\\"");
}
60 changes: 60 additions & 0 deletions src/UniGetUI.Avalonia/Infrastructure/WindowsAdminFileInstaller.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
using System.ComponentModel;
using System.Diagnostics;
using System.Runtime.Versioning;
using UniGetUI.Core.Tools;

namespace UniGetUI.Avalonia.Infrastructure;

[SupportedOSPlatform("windows")]
internal static class WindowsAdminFileInstaller
{
private const int ErrorCancelled = 1223;
private const int RobocopyFirstFailureCode = 8;

public static async Task<bool> WriteAsync(string destination, string contents)
{
string stagingDirectory = Directory.CreateTempSubdirectory("unigetui-admin-write-").FullName;
try
{
await File.WriteAllTextAsync(Path.Combine(stagingDirectory, Path.GetFileName(destination)), contents);

using var process = new Process { StartInfo = CreateStartInfo(stagingDirectory, destination) };
CoreTools.PrepareForegroundForElevation();
try
{
process.Start();
}
catch (Win32Exception ex) when (ex.NativeErrorCode == ErrorCancelled)
{
return false;
}
await process.WaitForExitAsync();
if (process.ExitCode >= RobocopyFirstFailureCode)
throw new IOException(CoreTools.Translate("Could not copy the file to {0} as an administrator (robocopy exit code {1}).",
destination, process.ExitCode));
return true;
}
finally
{
try { Directory.Delete(stagingDirectory, recursive: true); }
catch { }
}
}

internal static ProcessStartInfo CreateStartInfo(string stagingDirectory, string destination)
{
var startInfo = new ProcessStartInfo(Path.Combine(Environment.SystemDirectory, "robocopy.exe"))
{
UseShellExecute = true,
Verb = "runas",
WindowStyle = ProcessWindowStyle.Hidden,
};
foreach (string argument in new[]
{
stagingDirectory, Path.GetDirectoryName(destination)!, Path.GetFileName(destination),
"/IS", "/IT", "/R:0", "/W:0", "/NP", "/NJH", "/NJS",
})
startInfo.ArgumentList.Add(argument);
return startInfo;
}
}
Loading
Loading