diff --git a/src/UniGetUI.Avalonia.slnx b/src/UniGetUI.Avalonia.slnx
index 42647b832..e65b0bdb5 100644
--- a/src/UniGetUI.Avalonia.slnx
+++ b/src/UniGetUI.Avalonia.slnx
@@ -177,6 +177,12 @@
+
+
+
+
+
+
diff --git a/src/UniGetUI.Interface.Enums/Enums.cs b/src/UniGetUI.Interface.Enums/Enums.cs
index 0ca9570f7..7d813080c 100644
--- a/src/UniGetUI.Interface.Enums/Enums.cs
+++ b/src/UniGetUI.Interface.Enums/Enums.cs
@@ -89,6 +89,7 @@ public enum IconType
Dnf = '\uE945',
Pacman = '\uE946',
Snap = '\uE947',
+ Flatpak = '\uE948',
}
public class NotificationArguments
diff --git a/src/UniGetUI.PackageEngine.Managers.Flatpak/Flatpak.cs b/src/UniGetUI.PackageEngine.Managers.Flatpak/Flatpak.cs
new file mode 100644
index 000000000..39c4fdea8
--- /dev/null
+++ b/src/UniGetUI.PackageEngine.Managers.Flatpak/Flatpak.cs
@@ -0,0 +1,224 @@
+using System.Diagnostics;
+using UniGetUI.Core.Tools;
+using UniGetUI.Interface.Enums;
+using UniGetUI.PackageEngine.Classes.Manager;
+using UniGetUI.PackageEngine.Enums;
+using UniGetUI.PackageEngine.ManagerClasses.Classes;
+using UniGetUI.PackageEngine.ManagerClasses.Manager;
+using UniGetUI.PackageEngine.PackageClasses;
+
+namespace UniGetUI.PackageEngine.Managers.FlatpakManager;
+
+public partial class Flatpak : PackageManager
+{
+ private static readonly string[] FLATPAK_PATHS =
+ [
+ "/usr/bin/flatpak",
+ "/usr/local/bin/flatpak",
+ ];
+
+ public Flatpak()
+ {
+ Dependencies = [];
+
+ var flathubSource = new ManagerSource(this, "flathub", new Uri("https://dl.flathub.org/repo/"));
+
+ Capabilities = new ManagerCapabilities
+ {
+ CanRunAsAdmin = true,
+ CanSkipIntegrityChecks = true,
+ SupportsCustomSources = true,
+ Sources = new SourceCapabilities
+ {
+ KnowsPackageCount = false,
+ KnowsUpdateDate = false,
+ },
+ SupportsProxy = ProxySupport.No,
+ SupportsProxyAuth = false,
+ };
+
+ Properties = new ManagerProperties
+ {
+ Name = "Flatpak",
+ Description = CoreTools.Translate(
+ "The universal Linux package manager for desktop applications.
Contains: Flatpak applications from configured remotes"
+ ),
+ IconId = IconType.Flatpak,
+ ColorIconId = "flatpak",
+ ExecutableFriendlyName = "flatpak",
+ InstallVerb = "install",
+ UpdateVerb = "update",
+ UninstallVerb = "uninstall",
+ DefaultSource = flathubSource,
+ KnownSources = [flathubSource],
+ };
+
+ SourcesHelper = new FlatpakSourceHelper(this);
+ DetailsHelper = new FlatpakPkgDetailsHelper(this);
+ OperationHelper = new FlatpakPkgOperationHelper(this);
+ }
+
+ public override IReadOnlyList FindCandidateExecutableFiles()
+ {
+ var candidates = new List(CoreTools.WhichMultiple("flatpak"));
+ foreach (var path in FLATPAK_PATHS)
+ {
+ if (File.Exists(path) && !candidates.Contains(path))
+ candidates.Add(path);
+ }
+ return candidates;
+ }
+
+ protected override void _loadManagerExecutableFile(
+ out bool found,
+ out string path,
+ out string callArguments)
+ {
+ (found, path) = GetExecutableFile();
+ callArguments = "";
+ }
+
+ protected override void _loadManagerVersion(out string version)
+ {
+ using var p = new Process
+ {
+ StartInfo = new ProcessStartInfo
+ {
+ FileName = Status.ExecutablePath,
+ Arguments = "--version",
+ UseShellExecute = false,
+ RedirectStandardOutput = true,
+ RedirectStandardError = true,
+ CreateNoWindow = true,
+ },
+ };
+ p.Start();
+ var line = p.StandardOutput.ReadLine()?.Trim() ?? "";
+ version = line.Replace("Flatpak ", "").Trim();
+ p.StandardError.ReadToEnd();
+ p.WaitForExit();
+ }
+
+ public override void RefreshPackageIndexes()
+ {
+ using var p = new Process
+ {
+ StartInfo = new ProcessStartInfo
+ {
+ FileName = Status.ExecutablePath,
+ Arguments = "update --appstream",
+ UseShellExecute = false,
+ RedirectStandardOutput = true,
+ RedirectStandardError = true,
+ CreateNoWindow = true,
+ },
+ };
+ IProcessTaskLogger logger = TaskLogger.CreateNew(LoggableTaskType.RefreshIndexes, p);
+ p.Start();
+ logger.AddToStdOut(p.StandardOutput.ReadToEnd());
+ logger.AddToStdErr(p.StandardError.ReadToEnd());
+ p.WaitForExit();
+ logger.Close(p.ExitCode);
+ }
+
+ protected override IReadOnlyList FindPackages_UnSafe(string query)
+ {
+ using var p = new Process
+ {
+ StartInfo = new ProcessStartInfo
+ {
+ FileName = Status.ExecutablePath,
+ Arguments = $"search {CoreTools.EnsureSafeQueryString(query)}",
+ UseShellExecute = false,
+ RedirectStandardOutput = true,
+ RedirectStandardError = true,
+ CreateNoWindow = true,
+ },
+ };
+ p.StartInfo.Environment["LANG"] = "C";
+ p.StartInfo.Environment["LC_ALL"] = "C";
+ IProcessTaskLogger logger = TaskLogger.CreateNew(LoggableTaskType.FindPackages, p);
+ p.Start();
+
+ List outputLines = [];
+ while (p.StandardOutput.ReadLine() is { } line)
+ {
+ logger.AddToStdOut(line);
+ outputLines.Add(line);
+ }
+
+ logger.AddToStdErr(p.StandardError.ReadToEnd());
+ p.WaitForExit();
+ logger.Close(p.ExitCode);
+
+ return ParseSearchResults(outputLines, DefaultSource, this);
+ }
+
+ protected override IReadOnlyList GetInstalledPackages_UnSafe()
+ {
+ using var p = new Process
+ {
+ StartInfo = new ProcessStartInfo
+ {
+ FileName = Status.ExecutablePath,
+ Arguments = "list --app --columns=application,version,branch,origin,name",
+ UseShellExecute = false,
+ RedirectStandardOutput = true,
+ RedirectStandardError = true,
+ CreateNoWindow = true,
+ },
+ };
+ p.StartInfo.Environment["LANG"] = "C";
+ p.StartInfo.Environment["LC_ALL"] = "C";
+ IProcessTaskLogger logger = TaskLogger.CreateNew(LoggableTaskType.ListInstalledPackages, p);
+ p.Start();
+
+ List outputLines = [];
+ while (p.StandardOutput.ReadLine() is { } line)
+ {
+ logger.AddToStdOut(line);
+ outputLines.Add(line);
+ }
+
+ logger.AddToStdErr(p.StandardError.ReadToEnd());
+ p.WaitForExit();
+ logger.Close(p.ExitCode);
+
+ return ParseInstalledPackages(outputLines, DefaultSource, this);
+ }
+
+ protected override IReadOnlyList GetAvailableUpdates_UnSafe()
+ {
+ using var p = new Process
+ {
+ StartInfo = new ProcessStartInfo
+ {
+ FileName = Status.ExecutablePath,
+ Arguments = "remote-ls --updates --columns=application,version,branch,origin,name",
+ UseShellExecute = false,
+ RedirectStandardOutput = true,
+ RedirectStandardError = true,
+ CreateNoWindow = true,
+ },
+ };
+
+ p.StartInfo.Environment["LANG"] = "C";
+ p.StartInfo.Environment["LC_ALL"] = "C";
+ IProcessTaskLogger logger = TaskLogger.CreateNew(LoggableTaskType.ListUpdates, p);
+
+ p.Start();
+
+ List outputLines = [];
+ while (p.StandardOutput.ReadLine() is { } line)
+ {
+ logger.AddToStdOut(line);
+ outputLines.Add(line);
+ }
+
+ logger.AddToStdErr(p.StandardError.ReadToEnd());
+ p.WaitForExit();
+ logger.Close(p.ExitCode);
+
+ return ParseAvailableUpdates(outputLines, DefaultSource, this, GetInstalledPackages_UnSafe());
+ }
+}
diff --git a/src/UniGetUI.PackageEngine.Managers.Flatpak/FlatpakParsing.cs b/src/UniGetUI.PackageEngine.Managers.Flatpak/FlatpakParsing.cs
new file mode 100644
index 000000000..dc2e6db33
--- /dev/null
+++ b/src/UniGetUI.PackageEngine.Managers.Flatpak/FlatpakParsing.cs
@@ -0,0 +1,173 @@
+using System.Text.RegularExpressions;
+using UniGetUI.Core.Tools;
+using UniGetUI.PackageEngine.Interfaces;
+using UniGetUI.PackageEngine.PackageClasses;
+
+namespace UniGetUI.PackageEngine.Managers.FlatpakManager;
+
+public partial class Flatpak
+{
+ [GeneratedRegex(@"[a-zA-Z0-9][-a-zA-Z0-9]*(\.[a-zA-Z0-9][-a-zA-Z0-9]*)+")]
+ private static partial Regex _searchAppIdRegex();
+
+ [GeneratedRegex(@"\s{2,}")]
+ private static partial Regex _multiSpaceRegex();
+
+ private static void ParseColumns(IReadOnlyList parts, out string appId, out string version, out string name)
+ {
+ appId = parts[0];
+ version = parts[1];
+ if (string.IsNullOrEmpty(version) && parts.Count > 2)
+ version = parts[2];
+ name = parts[4];
+ }
+
+ public static IReadOnlyList ParseInstalledPackages(
+ IEnumerable outputLines,
+ IManagerSource source,
+ IPackageManager manager)
+ {
+ var packages = new List();
+
+ foreach (var line in outputLines)
+ {
+ var trimmed = line.Trim();
+ if (string.IsNullOrEmpty(trimmed))
+ {
+ continue;
+ }
+
+ var parts = trimmed.Split('\t');
+ if (parts.Length < 5)
+ {
+ continue;
+ }
+
+ var appId = parts[0];
+ var version = parts[1];
+ if (string.IsNullOrEmpty(version))
+ {
+ version = parts[2];
+ }
+
+ var name = parts[4];
+
+ packages.Add(new Package(
+ CoreTools.FormatAsName(name),
+ appId,
+ version,
+ source,
+ manager));
+ }
+
+ return packages;
+ }
+
+ public static IReadOnlyList ParseAvailableUpdates(
+ IEnumerable outputLines,
+ IManagerSource source,
+ IPackageManager manager,
+ IReadOnlyList installedPackages)
+ {
+ var installedPackageMap = new Dictionary();
+ foreach (var installedPackage in installedPackages)
+ {
+ installedPackageMap.TryAdd(installedPackage.Id, installedPackage);
+ }
+
+ var packages = new List();
+
+ foreach (var line in outputLines)
+ {
+ var trimmed = line.Trim();
+ if (string.IsNullOrEmpty(trimmed))
+ {
+ continue;
+ }
+
+ var parts = trimmed.Split('\t');
+ if (parts.Length < 5)
+ {
+ continue;
+ }
+
+ ParseColumns(parts, out var appId, out var newVersion, out var name);
+
+ if (installedPackageMap.TryGetValue(appId, out var installedPackage))
+ {
+ packages.Add(new Package(
+ CoreTools.FormatAsName(name),
+ appId,
+ installedPackage.VersionString,
+ newVersion,
+ source,
+ manager));
+ }
+ }
+
+ return packages;
+ }
+
+ public static IReadOnlyList ParseSearchResults(
+ IEnumerable outputLines,
+ IManagerSource source,
+ IPackageManager manager)
+ {
+ var lines = outputLines.ToArray();
+ if (lines.Length == 0)
+ {
+ return [];
+ }
+
+ var packages = new List();
+
+ // Detect format: tab-separated (no header) vs space-padded (with header)
+ int startIndex = lines[0].Contains('\t') ? 0 : 1;
+
+ for (int i = startIndex; i < lines.Length; i++)
+ {
+ var trimmed = lines[i].Trim();
+ if (string.IsNullOrEmpty(trimmed))
+ {
+ continue;
+ }
+
+ string name;
+ string appId;
+ string version;
+
+ var tabParts = trimmed.Split('\t');
+ if (tabParts.Length >= 4)
+ {
+ name = tabParts[0];
+ appId = tabParts[2];
+ version = tabParts[3];
+ }
+ else
+ {
+ var appIdMatch = _searchAppIdRegex().Match(trimmed);
+ if (!appIdMatch.Success)
+ {
+ continue;
+ }
+
+ appId = appIdMatch.Value;
+ var before = trimmed[..appIdMatch.Index].TrimEnd();
+ var after = trimmed[(appIdMatch.Index + appIdMatch.Length)..].Trim();
+
+ name = _multiSpaceRegex().Split(before)[0];
+ var parts = after.Split(' ', StringSplitOptions.RemoveEmptyEntries);
+ version = parts.Length > 0 ? parts[0] : "";
+ }
+
+ packages.Add(new Package(
+ CoreTools.FormatAsName(name),
+ appId,
+ version,
+ source,
+ manager));
+ }
+
+ return packages;
+ }
+}
diff --git a/src/UniGetUI.PackageEngine.Managers.Flatpak/Helpers/FlatpakPkgDetailsHelper.cs b/src/UniGetUI.PackageEngine.Managers.Flatpak/Helpers/FlatpakPkgDetailsHelper.cs
new file mode 100644
index 000000000..337d590b5
--- /dev/null
+++ b/src/UniGetUI.PackageEngine.Managers.Flatpak/Helpers/FlatpakPkgDetailsHelper.cs
@@ -0,0 +1,86 @@
+using System.Diagnostics;
+using UniGetUI.Core.IconEngine;
+using UniGetUI.PackageEngine.Classes.Manager.BaseProviders;
+using UniGetUI.PackageEngine.Enums;
+using UniGetUI.PackageEngine.Interfaces;
+using UniGetUI.PackageEngine.ManagerClasses.Classes;
+
+namespace UniGetUI.PackageEngine.Managers.FlatpakManager;
+
+internal sealed class FlatpakPkgDetailsHelper : BasePkgDetailsHelper
+{
+ public FlatpakPkgDetailsHelper(Flatpak manager)
+ : base(manager) { }
+
+ protected override void GetDetails_UnSafe(IPackageDetails details)
+ {
+ using var p = new Process
+ {
+ StartInfo = new ProcessStartInfo
+ {
+ FileName = Manager.Status.ExecutablePath,
+ Arguments = $"info {details.Package.Id}",
+ UseShellExecute = false,
+ RedirectStandardOutput = true,
+ RedirectStandardError = true,
+ CreateNoWindow = true,
+ },
+ };
+ p.StartInfo.Environment["LANG"] = "C";
+ p.StartInfo.Environment["LC_ALL"] = "C";
+
+ IProcessTaskLogger logger = Manager.TaskLogger.CreateNew(
+ LoggableTaskType.LoadPackageDetails, p);
+ p.Start();
+
+ while (p.StandardOutput.ReadLine() is { } line)
+ {
+ logger.AddToStdOut(line);
+
+ var colonIdx = line.IndexOf(':', StringComparison.Ordinal);
+ if (colonIdx <= 0) continue;
+
+ var key = line[..colonIdx].Trim().ToLowerInvariant();
+ var value = line[(colonIdx + 1)..].Trim();
+
+ switch (key)
+ {
+ case "name":
+ break;
+ case "summary":
+ details.Description = value;
+ break;
+ case "license":
+ details.License = value;
+ break;
+ case "homepage":
+ if (Uri.TryCreate(value, UriKind.Absolute, out var homepage))
+ details.HomepageUrl = homepage;
+ break;
+ case "origin":
+ details.Publisher = value;
+ break;
+ case "url":
+ if (Uri.TryCreate(value, UriKind.Absolute, out var url))
+ details.ManifestUrl = url;
+ break;
+ }
+ }
+
+ logger.AddToStdErr(p.StandardError.ReadToEnd());
+ p.WaitForExit();
+ logger.Close(p.ExitCode);
+ }
+
+ protected override CacheableIcon? GetIcon_UnSafe(IPackage package)
+ => throw new NotImplementedException();
+
+ protected override IReadOnlyList GetScreenshots_UnSafe(IPackage package)
+ => Array.Empty();
+
+ protected override string? GetInstallLocation_UnSafe(IPackage package)
+ => $"/var/lib/flatpak/app/{package.Id}";
+
+ protected override IReadOnlyList GetInstallableVersions_UnSafe(IPackage package)
+ => throw new InvalidOperationException("Flatpak does not support installing arbitrary versions");
+}
diff --git a/src/UniGetUI.PackageEngine.Managers.Flatpak/Helpers/FlatpakPkgOperationHelper.cs b/src/UniGetUI.PackageEngine.Managers.Flatpak/Helpers/FlatpakPkgOperationHelper.cs
new file mode 100644
index 000000000..a60e7d7ab
--- /dev/null
+++ b/src/UniGetUI.PackageEngine.Managers.Flatpak/Helpers/FlatpakPkgOperationHelper.cs
@@ -0,0 +1,54 @@
+using UniGetUI.PackageEngine.Classes.Manager.BaseProviders;
+using UniGetUI.PackageEngine.Enums;
+using UniGetUI.PackageEngine.Interfaces;
+using UniGetUI.PackageEngine.Serializable;
+
+namespace UniGetUI.PackageEngine.Managers.FlatpakManager;
+
+internal sealed class FlatpakPkgOperationHelper : BasePkgOperationHelper
+{
+ public FlatpakPkgOperationHelper(Flatpak manager)
+ : base(manager) { }
+
+ protected override IReadOnlyList _getOperationParameters(
+ IPackage package,
+ InstallOptions options,
+ OperationType operation)
+ {
+ options.RunAsAdministrator = true;
+
+ List parameters =
+ [
+ operation switch
+ {
+ OperationType.Install => Manager.Properties.InstallVerb,
+ OperationType.Uninstall => Manager.Properties.UninstallVerb,
+ OperationType.Update => Manager.Properties.UpdateVerb,
+ _ => throw new InvalidDataException("Invalid package operation"),
+ },
+ ];
+
+ parameters.Add("--noninteractive");
+ parameters.Add("-y");
+ parameters.Add(package.Id);
+
+ parameters.AddRange(
+ operation switch
+ {
+ OperationType.Update => options.CustomParameters_Update,
+ OperationType.Uninstall => options.CustomParameters_Uninstall,
+ _ => options.CustomParameters_Install,
+ });
+
+ return parameters;
+ }
+
+ protected override OperationVeredict _getOperationResult(
+ IPackage package,
+ OperationType operation,
+ IReadOnlyList processOutput,
+ int returnCode)
+ {
+ return returnCode == 0 ? OperationVeredict.Success : OperationVeredict.Failure;
+ }
+}
diff --git a/src/UniGetUI.PackageEngine.Managers.Flatpak/Helpers/FlatpakSourceHelper.cs b/src/UniGetUI.PackageEngine.Managers.Flatpak/Helpers/FlatpakSourceHelper.cs
new file mode 100644
index 000000000..7336d4c2d
--- /dev/null
+++ b/src/UniGetUI.PackageEngine.Managers.Flatpak/Helpers/FlatpakSourceHelper.cs
@@ -0,0 +1,84 @@
+using System.Diagnostics;
+using System.Text.RegularExpressions;
+using UniGetUI.Core.Logging;
+using UniGetUI.PackageEngine.Classes.Manager;
+using UniGetUI.PackageEngine.Classes.Manager.Providers;
+using UniGetUI.PackageEngine.Enums;
+using UniGetUI.PackageEngine.Interfaces;
+using UniGetUI.PackageEngine.ManagerClasses.Classes;
+
+namespace UniGetUI.PackageEngine.Managers.FlatpakManager;
+
+public partial class Flatpak
+{
+ [GeneratedRegex(@"^(\S+)\s+(https?://\S+)$")]
+ internal static partial Regex RemoteListLineRegex();
+}
+
+internal sealed class FlatpakSourceHelper : BaseSourceHelper
+{
+ public FlatpakSourceHelper(Flatpak manager)
+ : base(manager) { }
+
+ protected override IReadOnlyList GetSources_UnSafe()
+ {
+ var sources = new List();
+
+ using var p = new Process
+ {
+ StartInfo = new ProcessStartInfo
+ {
+ FileName = Manager.Status.ExecutablePath,
+ Arguments = "remote-list --columns=name,url",
+ UseShellExecute = false,
+ RedirectStandardOutput = true,
+ RedirectStandardError = true,
+ CreateNoWindow = true,
+ },
+ };
+ IProcessTaskLogger logger = Manager.TaskLogger.CreateNew(LoggableTaskType.ListSources, p);
+ p.Start();
+
+ string? line;
+ while ((line = p.StandardOutput.ReadLine()) is not null)
+ {
+ logger.AddToStdOut(line);
+ var match = Flatpak.RemoteListLineRegex().Match(line.Trim());
+ if (!match.Success)
+ {
+ continue;
+ }
+
+ var name = match.Groups[1].Value;
+ var url = match.Groups[2].Value;
+
+ try
+ {
+ sources.Add(new ManagerSource(Manager, name, new Uri(url)));
+ }
+ catch (Exception ex)
+ {
+ Logger.Warn($"FlatpakSourceHelper: could not add remote '{name}': {ex.Message}");
+ }
+ }
+
+ logger.AddToStdErr(p.StandardError.ReadToEnd());
+ p.WaitForExit();
+ logger.Close(p.ExitCode);
+ return sources;
+ }
+
+ public override string[] GetAddSourceParameters(IManagerSource source)
+ => ["remote-add", "--if-not-exists", source.Name, source.Url.ToString()];
+
+ public override string[] GetRemoveSourceParameters(IManagerSource source)
+ => ["remote-delete", source.Name];
+
+ protected override OperationVeredict _getAddSourceOperationVeredict(
+ IManagerSource source, int ReturnCode, string[] Output)
+ => ReturnCode == 0 ? OperationVeredict.Success : OperationVeredict.Failure;
+
+ protected override OperationVeredict _getRemoveSourceOperationVeredict(
+ IManagerSource source, int ReturnCode, string[] Output)
+ => ReturnCode == 0 ? OperationVeredict.Success : OperationVeredict.Failure;
+}
diff --git a/src/UniGetUI.PackageEngine.Managers.Flatpak/UniGetUI.PackageEngine.Managers.Flatpak.csproj b/src/UniGetUI.PackageEngine.Managers.Flatpak/UniGetUI.PackageEngine.Managers.Flatpak.csproj
new file mode 100644
index 000000000..00d99aa0e
--- /dev/null
+++ b/src/UniGetUI.PackageEngine.Managers.Flatpak/UniGetUI.PackageEngine.Managers.Flatpak.csproj
@@ -0,0 +1,23 @@
+
+
+ $(SharedTargetFrameworks)
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/UniGetUI.PackageEngine.PackageEngine/PEInterface.cs b/src/UniGetUI.PackageEngine.PackageEngine/PEInterface.cs
index fdf99b3ea..16af27c48 100644
--- a/src/UniGetUI.PackageEngine.PackageEngine/PEInterface.cs
+++ b/src/UniGetUI.PackageEngine.PackageEngine/PEInterface.cs
@@ -20,6 +20,7 @@
using UniGetUI.PackageEngine.Managers.HomebrewManager;
using UniGetUI.PackageEngine.Managers.PacmanManager;
using UniGetUI.PackageEngine.Managers.SnapManager;
+using UniGetUI.PackageEngine.Managers.FlatpakManager;
#endif
namespace UniGetUI.PackageEngine
@@ -50,6 +51,7 @@ public static class PEInterface
public static readonly Pacman Pacman = new();
public static readonly Homebrew Homebrew = new();
public static readonly Snap Snap = new();
+ public static readonly Flatpak Flatpak = new();
#endif
public static readonly IPackageManager[] Managers = CreateManagers();
@@ -74,7 +76,10 @@ private static IPackageManager[] CreateManagers()
if (unknown || families.Contains("arch"))
managers.Add(Pacman);
if (unknown || families.Contains("ubuntu") || families.Contains("debian") || families.Contains("fedora") || families.Contains("arch"))
+ {
managers.Add(Snap);
+ managers.Add(Flatpak);
+ }
}
#endif
return [.. managers];
diff --git a/src/UniGetUI.PackageEngine.PackageEngine/UniGetUI.PackageEngine.PEInterface.csproj b/src/UniGetUI.PackageEngine.PackageEngine/UniGetUI.PackageEngine.PEInterface.csproj
index f77f54607..9a8df247c 100644
--- a/src/UniGetUI.PackageEngine.PackageEngine/UniGetUI.PackageEngine.PEInterface.csproj
+++ b/src/UniGetUI.PackageEngine.PackageEngine/UniGetUI.PackageEngine.PEInterface.csproj
@@ -36,6 +36,7 @@
+
diff --git a/src/UniGetUI.PackageEngine.Tests/Fixtures/Flatpak/installed-list.txt b/src/UniGetUI.PackageEngine.Tests/Fixtures/Flatpak/installed-list.txt
new file mode 100644
index 000000000..31db72bb8
--- /dev/null
+++ b/src/UniGetUI.PackageEngine.Tests/Fixtures/Flatpak/installed-list.txt
@@ -0,0 +1,3 @@
+com.github.tchx84.Flatseal 2.4.0 stable flathub Flatseal
+com.heroicgameslauncher.hgl v2.21.0 stable flathub Heroic
+io.github.nokse22.high-tide 1.3.1 stable flathub High Tide
diff --git a/src/UniGetUI.PackageEngine.Tests/Fixtures/Flatpak/search-sqlitebrowser-tab.txt b/src/UniGetUI.PackageEngine.Tests/Fixtures/Flatpak/search-sqlitebrowser-tab.txt
new file mode 100644
index 000000000..c4f1bf21d
--- /dev/null
+++ b/src/UniGetUI.PackageEngine.Tests/Fixtures/Flatpak/search-sqlitebrowser-tab.txt
@@ -0,0 +1 @@
+DB Browser for SQLite light GUI editor for SQLite databases org.sqlitebrowser.sqlitebrowser 3.13.1 stable flathub
diff --git a/src/UniGetUI.PackageEngine.Tests/Fixtures/Flatpak/search-sqlitebrowser.txt b/src/UniGetUI.PackageEngine.Tests/Fixtures/Flatpak/search-sqlitebrowser.txt
new file mode 100644
index 000000000..68239b385
--- /dev/null
+++ b/src/UniGetUI.PackageEngine.Tests/Fixtures/Flatpak/search-sqlitebrowser.txt
@@ -0,0 +1,2 @@
+Name Description Application ID Version Branch Remotes
+DB Browser for SQLite light GUI editor for SQLite databases org.sqlitebrowser.sqlitebrowser 3.13.1 stable flathub
diff --git a/src/UniGetUI.PackageEngine.Tests/Fixtures/Flatpak/updates-list.txt b/src/UniGetUI.PackageEngine.Tests/Fixtures/Flatpak/updates-list.txt
new file mode 100644
index 000000000..130275f2e
--- /dev/null
+++ b/src/UniGetUI.PackageEngine.Tests/Fixtures/Flatpak/updates-list.txt
@@ -0,0 +1,3 @@
+io.podman_desktop.PodmanDesktop 1.27.1 stable flathub Podman Desktop
+org.freedesktop.Platform.Compat.i386 25.08 flathub i386
+org.freedesktop.Platform.GL.default 26.0.4 24.08 flathub Mesa
diff --git a/src/UniGetUI.PackageEngine.Tests/FlatpakManagerTests.cs b/src/UniGetUI.PackageEngine.Tests/FlatpakManagerTests.cs
new file mode 100644
index 000000000..e0feee003
--- /dev/null
+++ b/src/UniGetUI.PackageEngine.Tests/FlatpakManagerTests.cs
@@ -0,0 +1,385 @@
+using UniGetUI.Core.Data;
+using UniGetUI.Core.SettingsEngine;
+using UniGetUI.PackageEngine.Enums;
+using UniGetUI.PackageEngine.Managers.FlatpakManager;
+using UniGetUI.PackageEngine.PackageClasses;
+using UniGetUI.PackageEngine.Serializable;
+using UniGetUI.PackageEngine.Tests.Infrastructure.Assertions;
+using UniGetUI.PackageEngine.Tests.Infrastructure.Builders;
+using UniGetUI.PackageEngine.Tests.Infrastructure.Helpers;
+
+namespace UniGetUI.PackageEngine.Tests;
+
+[CollectionDefinition("Flatpak manager tests", DisableParallelization = true)]
+public sealed class FlatpakManagerTestCollection
+{
+ public const string Name = "Flatpak manager tests";
+}
+
+[Collection(FlatpakManagerTestCollection.Name)]
+public sealed class FlatpakManagerTests : IDisposable
+{
+ private readonly string _testRoot = Path.Combine(
+ AppContext.BaseDirectory,
+ nameof(FlatpakManagerTests),
+ Guid.NewGuid().ToString("N")
+ );
+
+ public FlatpakManagerTests()
+ {
+ Directory.CreateDirectory(_testRoot);
+ CoreData.TEST_DataDirectoryOverride = Path.Combine(_testRoot, "Data");
+ Directory.CreateDirectory(CoreData.UniGetUIUserConfigurationDirectory);
+ Settings.ResetSettings();
+ }
+
+ public void Dispose()
+ {
+ Settings.ResetSettings();
+ CoreData.TEST_DataDirectoryOverride = null;
+ if (Directory.Exists(_testRoot))
+ {
+ Directory.Delete(_testRoot, recursive: true);
+ }
+ }
+
+ [Fact]
+ public void ParseInstalledPackagesBuildsPackagesFromFixture()
+ {
+ var manager = new Flatpak();
+
+ var packages = Flatpak.ParseInstalledPackages(
+ File.ReadLines(PackageEngineFixtureFiles.GetPath(Path.Combine("Flatpak", "installed-list.txt"))),
+ manager.DefaultSource,
+ manager
+ );
+
+ Assert.Collection(
+ packages,
+ package =>
+ {
+ PackageAssert.Matches(package, "Flatseal", "com.github.tchx84.Flatseal", "2.4.0");
+ PackageAssert.BelongsTo(package, manager, manager.DefaultSource);
+ },
+ package =>
+ {
+ PackageAssert.Matches(package, "Heroic", "com.heroicgameslauncher.hgl", "v2.21.0");
+ PackageAssert.BelongsTo(package, manager, manager.DefaultSource);
+ },
+ package =>
+ {
+ PackageAssert.Matches(package, "High Tide", "io.github.nokse22.high-tide", "1.3.1");
+ PackageAssert.BelongsTo(package, manager, manager.DefaultSource);
+ }
+ );
+ }
+
+ [Fact]
+ public void ParseAvailableUpdatesBuildsPackagesFromFixture()
+ {
+ var manager = new Flatpak();
+
+ var installedPackages = new List
+ {
+ new("Podman Desktop", "io.podman_desktop.PodmanDesktop", "1.26.0", manager.DefaultSource, manager),
+ new("Mesa", "org.freedesktop.Platform.GL.default", "26.0.3", manager.DefaultSource, manager),
+ };
+
+ var packages = Flatpak.ParseAvailableUpdates(
+ File.ReadLines(PackageEngineFixtureFiles.GetPath(Path.Combine("Flatpak", "updates-list.txt"))),
+ manager.DefaultSource,
+ manager,
+ installedPackages
+ );
+
+ Assert.Collection(
+ packages,
+ package =>
+ {
+ PackageAssert.Matches(package, "Podman Desktop", "io.podman_desktop.PodmanDesktop", "1.26.0", "1.27.1");
+ Assert.True(package.IsUpgradable);
+ PackageAssert.BelongsTo(package, manager, manager.DefaultSource);
+ },
+ package =>
+ {
+ PackageAssert.Matches(package, "Mesa", "org.freedesktop.Platform.GL.default", "26.0.3", "26.0.4");
+ Assert.True(package.IsUpgradable);
+ PackageAssert.BelongsTo(package, manager, manager.DefaultSource);
+ }
+ );
+ }
+
+ [Fact]
+ public void ParseSearchResultsBuildsPackagesFromFixture()
+ {
+ var manager = new Flatpak();
+
+ var packages = Flatpak.ParseSearchResults(
+ File.ReadLines(PackageEngineFixtureFiles.GetPath(Path.Combine("Flatpak", "search-sqlitebrowser.txt"))),
+ manager.DefaultSource,
+ manager
+ );
+
+ Assert.Collection(
+ packages,
+ package =>
+ {
+ PackageAssert.Matches(package, "DB Browser For SQLite", "org.sqlitebrowser.sqlitebrowser", "3.13.1");
+ PackageAssert.BelongsTo(package, manager, manager.DefaultSource);
+ }
+ );
+ }
+
+ [Fact]
+ public void ParseInstalledPackagesFiltersEmptyLines()
+ {
+ var manager = new Flatpak();
+ var lines = new[]
+ {
+ "",
+ " ",
+ "com.github.tchx84.Flatseal\t2.4.0\tstable\tflathub\tFlatseal",
+ };
+
+ var packages = Flatpak.ParseInstalledPackages(lines, manager.DefaultSource, manager);
+
+ Assert.Single(packages);
+ Assert.Equal("com.github.tchx84.Flatseal", packages[0].Id);
+ }
+
+ [Fact]
+ public void ParseAvailableUpdatesFiltersEmptyLines()
+ {
+ var manager = new Flatpak();
+ var lines = new[]
+ {
+ "",
+ "io.podman_desktop.PodmanDesktop\t1.27.1\tstable\tflathub\tPodman Desktop",
+ };
+
+ var installedPackages = new List
+ {
+ new Package("Podman Desktop", "io.podman_desktop.PodmanDesktop", "1.26.0", manager.DefaultSource, manager),
+ };
+
+ var packages = Flatpak.ParseAvailableUpdates(lines, manager.DefaultSource, manager, installedPackages);
+
+ Assert.Single(packages);
+ Assert.Equal("io.podman_desktop.PodmanDesktop", packages[0].Id);
+ Assert.Equal("1.26.0", packages[0].VersionString);
+ Assert.Equal("1.27.1", packages[0].NewVersionString);
+ }
+
+ [Fact]
+ public void ParseSearchResultsHandlesTabSeparatedOutputWithoutHeader()
+ {
+ var manager = new Flatpak();
+
+ var packages = Flatpak.ParseSearchResults(
+ File.ReadLines(PackageEngineFixtureFiles.GetPath(Path.Combine("Flatpak", "search-sqlitebrowser-tab.txt"))),
+ manager.DefaultSource,
+ manager
+ );
+
+ Assert.Collection(
+ packages,
+ package =>
+ {
+ PackageAssert.Matches(package, "DB Browser For SQLite", "org.sqlitebrowser.sqlitebrowser", "3.13.1");
+ PackageAssert.BelongsTo(package, manager, manager.DefaultSource);
+ }
+ );
+ }
+
+ [Fact]
+ public void ParseSearchResultsSkipsHeaderAndEmptyLines()
+ {
+ var manager = new Flatpak();
+ var lines = new[]
+ {
+ "Name Description Application ID Version Branch Remotes",
+ "",
+ "DB Browser for SQLite light GUI editor for SQLite databases org.sqlitebrowser.sqlitebrowser 3.13.1 stable flathub",
+ };
+
+ var packages = Flatpak.ParseSearchResults(lines, manager.DefaultSource, manager);
+
+ Assert.Single(packages);
+ Assert.Equal("org.sqlitebrowser.sqlitebrowser", packages[0].Id);
+ }
+
+ [Fact]
+ public void ParseInstalledPackagesReturnsEmptyForNoResults()
+ {
+ var manager = new Flatpak();
+
+ var packages = Flatpak.ParseInstalledPackages([], manager.DefaultSource, manager);
+
+ Assert.Empty(packages);
+ }
+
+ [Fact]
+ public void ParseAvailableUpdatesReturnsEmptyForNoResults()
+ {
+ var manager = new Flatpak();
+
+ var packages = Flatpak.ParseAvailableUpdates([], manager.DefaultSource, manager, []);
+ Assert.Empty(packages);
+ }
+
+ [Fact]
+ public void ParseSearchResultsReturnsEmptyForNoResults()
+ {
+ var manager = new Flatpak();
+ var lines = new[]
+ {
+ "Name Description Application ID Version Branch Remotes",
+ };
+
+ var packages = Flatpak.ParseSearchResults(lines, manager.DefaultSource, manager);
+
+ Assert.Empty(packages);
+ }
+
+ [Fact]
+ public void OperationHelperBuildsInstallAndUninstallParameters()
+ {
+ var manager = new Flatpak();
+ var package = new PackageBuilder()
+ .WithManager(manager)
+ .WithId("org.sqlitebrowser.sqlitebrowser")
+ .Build();
+ var installOptions = new InstallOptions
+ {
+ CustomParameters_Install = [],
+ };
+ var uninstallOptions = new InstallOptions();
+
+ var installParameters = manager.OperationHelper.GetParameters(
+ package,
+ installOptions,
+ OperationType.Install
+ );
+ var uninstallParameters = manager.OperationHelper.GetParameters(
+ package,
+ uninstallOptions,
+ OperationType.Uninstall
+ );
+
+ Assert.Equal(
+ [
+ "install",
+ "--noninteractive",
+ "-y",
+ "org.sqlitebrowser.sqlitebrowser",
+ ],
+ installParameters
+ );
+ Assert.Equal(
+ [
+ "uninstall",
+ "--noninteractive",
+ "-y",
+ "org.sqlitebrowser.sqlitebrowser",
+ ],
+ uninstallParameters
+ );
+ }
+
+ [Fact]
+ public void OperationHelperForcesAdministratorForAllOperations()
+ {
+ var manager = new Flatpak();
+ var package = new PackageBuilder()
+ .WithManager(manager)
+ .WithId("com.github.tchx84.Flatseal")
+ .Build();
+ var options = new InstallOptions();
+
+ _ = manager.OperationHelper.GetParameters(package, options, OperationType.Install);
+ Assert.True(options.RunAsAdministrator);
+
+ options = new InstallOptions();
+ _ = manager.OperationHelper.GetParameters(package, options, OperationType.Update);
+ Assert.True(options.RunAsAdministrator);
+
+ options = new InstallOptions();
+ _ = manager.OperationHelper.GetParameters(package, options, OperationType.Uninstall);
+ Assert.True(options.RunAsAdministrator);
+ }
+
+ [Fact]
+ public void OperationHelperReturnsSuccessOnZeroExitCode()
+ {
+ var manager = new Flatpak();
+ var package = new PackageBuilder()
+ .WithManager(manager)
+ .WithId("com.github.tchx84.Flatseal")
+ .Build();
+
+ var result = manager.OperationHelper.GetResult(
+ package,
+ OperationType.Install,
+ ["Installing..."],
+ 0
+ );
+
+ Assert.Equal(OperationVeredict.Success, result);
+ }
+
+ [Fact]
+ public void OperationHelperReturnsFailureOnNonZeroExitCode()
+ {
+ var manager = new Flatpak();
+ var package = new PackageBuilder()
+ .WithManager(manager)
+ .WithId("com.github.tchx84.Flatseal")
+ .Build();
+
+ var result = manager.OperationHelper.GetResult(
+ package,
+ OperationType.Install,
+ ["error: something went wrong"],
+ 1
+ );
+
+ Assert.Equal(OperationVeredict.Failure, result);
+ }
+
+ [Fact]
+ public void ManagerHasCorrectCapabilities()
+ {
+ var manager = new Flatpak();
+
+ Assert.True(manager.Capabilities.CanRunAsAdmin);
+ Assert.True(manager.Capabilities.CanSkipIntegrityChecks);
+ Assert.True(manager.Capabilities.SupportsCustomSources);
+ }
+
+ [Fact]
+ public void ManagerHasCorrectProperties()
+ {
+ var manager = new Flatpak();
+
+ Assert.Equal("Flatpak", manager.Name);
+ Assert.Equal("flatpak", manager.Properties.ExecutableFriendlyName);
+ Assert.Equal("install", manager.Properties.InstallVerb);
+ Assert.Equal("update", manager.Properties.UpdateVerb);
+ Assert.Equal("uninstall", manager.Properties.UninstallVerb);
+ Assert.Equal("flathub", manager.DefaultSource.Name);
+ Assert.Equal("https://dl.flathub.org/repo/", manager.DefaultSource.Url.ToString());
+ }
+
+ [Fact]
+ public void GetInstallableVersionsReturnsEmptyList()
+ {
+ var manager = new Flatpak();
+ var package = new PackageBuilder()
+ .WithManager(manager)
+ .WithId("org.sqlitebrowser.sqlitebrowser")
+ .Build();
+
+ var versions = manager.DetailsHelper.GetVersions(package);
+ Assert.Empty(versions);
+ }
+}
diff --git a/src/UniGetUI.PackageEngine.Tests/UniGetUI.PackageEngine.Tests.csproj b/src/UniGetUI.PackageEngine.Tests/UniGetUI.PackageEngine.Tests.csproj
index 01d61ac4d..97f08a85f 100644
--- a/src/UniGetUI.PackageEngine.Tests/UniGetUI.PackageEngine.Tests.csproj
+++ b/src/UniGetUI.PackageEngine.Tests/UniGetUI.PackageEngine.Tests.csproj
@@ -29,6 +29,7 @@
+
diff --git a/src/UniGetUI.Windows.slnx b/src/UniGetUI.Windows.slnx
index 8f1754aeb..d3292646f 100644
--- a/src/UniGetUI.Windows.slnx
+++ b/src/UniGetUI.Windows.slnx
@@ -184,6 +184,10 @@
+
+
+
+
diff --git a/src/UniGetUI/Assets/Symbols/flatpak.svg b/src/UniGetUI/Assets/Symbols/flatpak.svg
new file mode 100644
index 000000000..49b91308d
--- /dev/null
+++ b/src/UniGetUI/Assets/Symbols/flatpak.svg
@@ -0,0 +1 @@
+
\ No newline at end of file