maui-linux/Services/FilePickerService.cs

213 lines
6.1 KiB
C#

// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.Diagnostics;
using System.Text;
using Microsoft.Maui.Storage;
namespace Microsoft.Maui.Platform.Linux.Services;
/// <summary>
/// Linux file picker implementation using zenity or kdialog.
/// </summary>
public class FilePickerService : IFilePicker
{
private enum DialogTool
{
None,
Zenity,
Kdialog
}
private static DialogTool? _availableTool;
private static DialogTool GetAvailableTool()
{
if (_availableTool.HasValue)
return _availableTool.Value;
// Check for zenity first (GNOME/GTK)
if (IsToolAvailable("zenity"))
{
_availableTool = DialogTool.Zenity;
return DialogTool.Zenity;
}
// Check for kdialog (KDE)
if (IsToolAvailable("kdialog"))
{
_availableTool = DialogTool.Kdialog;
return DialogTool.Kdialog;
}
_availableTool = DialogTool.None;
return DialogTool.None;
}
private static bool IsToolAvailable(string tool)
{
try
{
var psi = new ProcessStartInfo
{
FileName = "which",
Arguments = tool,
UseShellExecute = false,
RedirectStandardOutput = true,
CreateNoWindow = true
};
using var process = Process.Start(psi);
process?.WaitForExit(1000);
return process?.ExitCode == 0;
}
catch
{
return false;
}
}
public Task<FileResult?> PickAsync(PickOptions? options = null)
{
return PickInternalAsync(options, false);
}
public Task<IEnumerable<FileResult>> PickMultipleAsync(PickOptions? options = null)
{
return PickMultipleInternalAsync(options);
}
private async Task<FileResult?> PickInternalAsync(PickOptions? options, bool multiple)
{
var results = await PickMultipleInternalAsync(options, multiple);
return results.FirstOrDefault();
}
private Task<IEnumerable<FileResult>> PickMultipleInternalAsync(PickOptions? options, bool multiple = true)
{
return Task.Run<IEnumerable<FileResult>>(() =>
{
var tool = GetAvailableTool();
if (tool == DialogTool.None)
{
// Fall back to console path input
Console.WriteLine("No file dialog available. Please enter file path:");
var path = Console.ReadLine();
if (!string.IsNullOrEmpty(path) && File.Exists(path))
{
return new[] { new LinuxFileResult(path) };
}
return Array.Empty<FileResult>();
}
string arguments;
if (tool == DialogTool.Zenity)
{
arguments = BuildZenityArguments(options, multiple);
}
else
{
arguments = BuildKdialogArguments(options, multiple);
}
var psi = new ProcessStartInfo
{
FileName = tool == DialogTool.Zenity ? "zenity" : "kdialog",
Arguments = arguments,
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true,
CreateNoWindow = true
};
try
{
using var process = Process.Start(psi);
if (process == null)
return Array.Empty<FileResult>();
var output = process.StandardOutput.ReadToEnd().Trim();
process.WaitForExit();
if (process.ExitCode != 0 || string.IsNullOrEmpty(output))
return Array.Empty<FileResult>();
// Parse output (paths separated by | for zenity, newlines for kdialog)
var separator = tool == DialogTool.Zenity ? '|' : '\n';
var paths = output.Split(separator, StringSplitOptions.RemoveEmptyEntries);
return paths
.Where(File.Exists)
.Select(p => (FileResult)new LinuxFileResult(p))
.ToArray();
}
catch
{
return Array.Empty<FileResult>();
}
});
}
private string BuildZenityArguments(PickOptions? options, bool multiple)
{
var sb = new StringBuilder("--file-selection");
if (multiple)
sb.Append(" --multiple --separator='|'");
if (!string.IsNullOrEmpty(options?.PickerTitle))
sb.Append($" --title=\"{EscapeArgument(options.PickerTitle)}\"");
if (options?.FileTypes != null)
{
foreach (var ext in options.FileTypes.Value)
{
var extension = ext.StartsWith(".") ? ext : $".{ext}";
sb.Append($" --file-filter='*{extension}'");
}
}
return sb.ToString();
}
private string BuildKdialogArguments(PickOptions? options, bool multiple)
{
var sb = new StringBuilder("--getopenfilename");
if (multiple)
sb.Insert(0, "--multiple ");
sb.Append(" .");
if (options?.FileTypes != null)
{
var extensions = string.Join(" ", options.FileTypes.Value.Select(e =>
e.StartsWith(".") ? $"*{e}" : $"*.{e}"));
if (!string.IsNullOrEmpty(extensions))
{
sb.Append($" \"{extensions}\"");
}
}
if (!string.IsNullOrEmpty(options?.PickerTitle))
sb.Append($" --title \"{EscapeArgument(options.PickerTitle)}\"");
return sb.ToString();
}
private static string EscapeArgument(string arg)
{
return arg.Replace("\"", "\\\"").Replace("'", "\\'");
}
}
/// <summary>
/// Linux-specific FileResult implementation.
/// </summary>
internal class LinuxFileResult : FileResult
{
public LinuxFileResult(string fullPath) : base(fullPath)
{
}
}